diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4ffecc8e2..ee8d4508d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,9 +52,13 @@ jobs: const stackBase = pr?.stack?.base?.ref; const prBase = pr?.base?.ref; // Position 1 can already be merged; compare the current base branches. + // A PR into the default branch is the lowest unmerged layer of any + // stack, whatever the stack metadata names as its base. + const defaultBranch = event.repository?.default_branch; const upperLayer = process.env.GITHUB_EVENT_NAME === 'pull_request' && sameRepository && trustedAuthor && typeof stackBase === 'string' && stackBase.length > 0 && - typeof prBase === 'string' && prBase.length > 0 && stackBase !== prBase; + typeof prBase === 'string' && prBase.length > 0 && stackBase !== prBase && + prBase !== defaultBranch; const full = !upperLayer; appendFileSync(process.env.GITHUB_OUTPUT, `full=${full}\n`); console.log(full ? 'Run full CI' : 'Defer full CI until this PR becomes the lowest unmerged layer'); @@ -122,6 +126,8 @@ jobs: SCRIPTC_TEST_WORKERS: "1" - name: Verify mixed-source generated AST getters run: pnpm generate:ts7-ast --check + - name: Verify the generated IR field readers + run: pnpm generate:ir-fields --check native_macos: needs: [validation_scope, lanes] @@ -220,9 +226,24 @@ jobs: # Start the corpus immediately beside a serial side queue. One shared # worker budget covers both processes; file and case shard axes stay # separate so every fixture still runs exactly once in each lane. + # The tests run in a transient scope capped below the runner's 16 GB, so + # the runner itself keeps memory: past 15 GB of RAM the scope swaps, as + # the whole runner did before, and when RAM and swap run out the kernel + # kills the scope's largest process. A sanitized shard peaks near 16 GB, + # and the sanitized self-hosting native-frontend build alone needs 15 GB + # (its clang compile of the frontend takes 10.5 GB). + # OOMPolicy=continue leaves the rest of the scope running, so vitest + # reports the killed worker and the job log names the test. Node sizes + # its default heap from the memory it may use, which the scope lowers + # below 4 GB; the workers keep the 4 GB heap they had on the bare + # runner. - name: Tests (corpus and integration suites) run: | ulimit -c 0 + if [ -d /run/systemd/system ]; then + exec sudo --preserve-env systemd-run --scope --quiet -p MemoryMax=15G -p OOMPolicy=continue \ + --uid="$(id -u)" --gid="$(id -g)" -- env PATH="$PATH" HOME="$HOME" NODE_OPTIONS="--max-old-space-size=4096" node scripts/ci-test.mjs + fi node scripts/ci-test.mjs tests_macos: @@ -335,6 +356,37 @@ jobs: - run: tar -xzf "$RUNNER_TEMP/native-linux-x64-gnu.tar.gz" - run: pnpm test tests/harness/effect.test.ts tests/harness/effect4.test.ts + # The sanitized three.js nodes build alone: beside the san corpus's other + # workers it took 860 to more than 1200 seconds of its shard + # (SCRIPTC_THREE_SANITIZED_NODES_JOB in scripts/ci-test-plan.mjs). It keeps + # the corpus's LeakSanitizer settings. + three_nodes_san: + needs: [validation_scope, lanes, native_linux] + if: ${{ needs.validation_scope.outputs.full == 'true' && needs.lanes.outputs.linux == 'true' }} + name: test (Linux, san, three.js nodes) + runs-on: ubuntu-24.04 + timeout-minutes: 40 + env: + SCRIPTC_SAN: "1" + ASAN_OPTIONS: detect_leaks=1 + LSAN_OPTIONS: suppressions=${{ github.workspace }}/tests/harness/lsan.supp:print_suppressions=0 + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 11.1.3 + - uses: actions/setup-node@v4 + with: + node-version-file: .node-version + cache: pnpm + - run: pnpm install --frozen-lockfile + - uses: actions/download-artifact@v4 + with: + name: native-linux-x64-gnu + path: ${{ runner.temp }} + - run: tar -xzf "$RUNNER_TEMP/native-linux-x64-gnu.tar.gz" + - run: pnpm test tests/harness/three.test.ts -t "'nodes' runs statically through LLVM on 'native'" + # Build each instrumented seed once. Downstream contracts retain the same # compiler and source artifacts without repeating seed construction. bootstrap_seed: @@ -528,7 +580,9 @@ jobs: if: ${{ needs.validation_scope.outputs.full == 'true' && needs.lanes.outputs.linux == 'true' }} name: test (tsgo and tsc-ts give the same IR) runs-on: ubuntu-24.04 - timeout-minutes: 30 + # The two corpus compiles run back to back and take close to 30 minutes on a + # slow hosted runner, so a 30-minute limit cancels otherwise healthy runs. + timeout-minutes: 60 steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 @@ -622,8 +676,9 @@ jobs: # Shard 1 additionally runs package/install and link-parity contracts # before its LLVM differential slice. The slice itself can take ~10 # minutes on a cold hosted runner, so 20 minutes cancels an otherwise - # healthy job before the differential completes. - timeout-minutes: 35 + # healthy job before the differential completes. Slow runners need more + # headroom than that, so the limit is 50 minutes. + timeout-minutes: 50 strategy: fail-fast: false matrix: @@ -936,6 +991,7 @@ jobs: tests_macos, native_linux, effects, + three_nodes_san, bootstrap_seed, bootstrap, linux_host_clang, @@ -968,6 +1024,7 @@ jobs: lane tests_macos "${{ needs.tests_macos.result }}" "${{ needs.lanes.outputs.macos }}" lane native_linux "${{ needs.native_linux.result }}" "${{ needs.lanes.outputs.linux }}" lane effects "${{ needs.effects.result }}" "${{ needs.lanes.outputs.effects }}" + lane three_nodes_san "${{ needs.three_nodes_san.result }}" "${{ needs.lanes.outputs.linux }}" lane bootstrap_seed "${{ needs.bootstrap_seed.result }}" "${{ needs.lanes.outputs.bootstrap }}" lane bootstrap "${{ needs.bootstrap.result }}" "${{ needs.lanes.outputs.bootstrap }}" lane linux_host_clang "${{ needs.linux_host_clang.result }}" "${{ needs.lanes.outputs.linux }}" diff --git a/.github/workflows/llvm-dist.yml b/.github/workflows/llvm-dist.yml new file mode 100644 index 000000000..233e97e98 --- /dev/null +++ b/.github/workflows/llvm-dist.yml @@ -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 diff --git a/.github/workflows/prune-caches.yml b/.github/workflows/prune-caches.yml new file mode 100644 index 000000000..1509d56ce --- /dev/null +++ b/.github/workflows/prune-caches.yml @@ -0,0 +1,41 @@ +name: Prune caches + +# Pull request caches are scoped to that pull request, so once it is closed no +# run can use them again. Deleting them keeps the repository under its cache +# size limit, so caches that main and open pull requests use are not evicted. +on: + pull_request: + types: [closed] + schedule: + - cron: "17 6 * * *" + workflow_dispatch: + +permissions: + actions: write + pull-requests: read + +jobs: + prune: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - name: Delete caches of closed pull requests + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + run: | + set -euo pipefail + # Read both lists in full before deleting anything: deleting while the cache + # listing pages shifts later pages, and a failed request must stop the job + # instead of leaving open pull requests out of the list. + open=$(gh api --paginate "repos/$REPO/pulls?state=open&per_page=100" --jq '.[].number') + caches=$(gh api --paginate "repos/$REPO/actions/caches?per_page=100" \ + --jq '.actions_caches[] | select(.ref | startswith("refs/pull/")) | "\(.id) \(.ref)"') + while read -r id ref; do + [ -n "$id" ] || continue + number=$(echo "$ref" | cut -d/ -f3) + if ! grep -qx "$number" <<<"$open"; then + gh api --method DELETE "repos/$REPO/actions/caches/$id" >/dev/null + echo "deleted cache $id ($ref)" + fi + done <<<"$caches" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c697c699f..e59cf7739 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -83,6 +83,7 @@ jobs: spawnSync('git', ['diff', '--quiet', run.head_sha, 'HEAD', '--', '.', ':(exclude).github/workflows/release.yml', ':(exclude)scripts/verify-native-cli.mjs', ':(exclude)scripts/package-native-cli.mjs', ':(exclude)tests/harness/native-cli-packaging.test.ts', + ':(exclude)scripts/wait-npm-available.mjs', ':(exclude)tests/harness/npm-available.test.ts', ]).status !== 0) continue; const artifacts = api(`repos/${repo}/actions/runs/${run.id}/artifacts?per_page=100`).artifacts .filter((artifact) => !artifact.expired && artifact.size_in_bytes > 0); @@ -100,10 +101,65 @@ jobs: if (receiptRun) appendFileSync(process.env.GITHUB_OUTPUT, `receipt_run=${receiptRun}\n`); NODE - build-native-packages: - name: Build native package (${{ matrix.platform }}) + # Collects the native compiler's profile and function layout on this commit + # (scripts/refresh-cli-profile.mjs, scripts/refresh-cli-layout.mjs), so every + # platform's CLI build optimizes and lays out the code it compiles. The + # committed ones fall behind main within a day or two. If this job fails, + # the CLI builds use the committed profile and layout. + cli-profile: + name: Collect the native compiler profile needs: check-release if: needs.check-release.outputs.should_release == 'true' && needs.check-release.outputs.reuse_artifacts != 'true' + runs-on: ubuntu-24.04 + timeout-minutes: 45 + continue-on-error: true + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 11.1.3 + - uses: actions/setup-node@v4 + with: + node-version: "24" + cache: pnpm + - uses: ./.github/actions/llvm-linux + - run: pnpm install --frozen-lockfile + - uses: ./.github/actions/zig + - uses: ./.github/actions/native-packages + with: + target: linux-x64-gnu + - run: pnpm --filter @scriptc/compiler --filter scriptc build + - name: Check out tsc-ts + run: | + git clone --quiet --depth 1 https://github.com/vercel-labs/tsc-ts "$RUNNER_TEMP/tsc-ts" + echo "tsc-ts $(git -C "$RUNNER_TEMP/tsc-ts" rev-parse HEAD)" + npm ci --prefix "$RUNNER_TEMP/tsc-ts" + - name: Collect the profile + run: >- + node scripts/refresh-cli-profile.mjs --tsc-ts "$RUNNER_TEMP/tsc-ts" --jobs 4 + --output "$RUNNER_TEMP/cli-profile/cli.profdata.gz" --work "$RUNNER_TEMP/cli-profile-work" + - name: Collect the function layout + run: >- + node scripts/refresh-cli-layout.mjs --jobs 4 --instrumented "$RUNNER_TEMP/cli-profile-work/cli" + --output "$RUNNER_TEMP/cli-profile/cli.layout.gz" + - uses: actions/upload-artifact@v4 + with: + name: cli-profile + path: | + ${{ runner.temp }}/cli-profile/cli.profdata.gz + ${{ runner.temp }}/cli-profile/cli.layout.gz + retention-days: 7 + if-no-files-found: error + + build-native-packages: + name: Build native package (${{ matrix.platform }}) + needs: [check-release, cli-profile] + # Runs when the profile job fails; the CLI builds then use the committed profile. + if: >- + !cancelled() && needs.check-release.outputs.should_release == 'true' && + needs.check-release.outputs.reuse_artifacts != 'true' strategy: fail-fast: false matrix: @@ -124,6 +180,7 @@ jobs: runtime: runtime-linux-x64-gnu cli: cli-linux-x64-gnu llvm_asset: LLVM-22.1.8-Linux-X64 + llvm_dist: x64 use_zig: true - platform: linux-arm64 runner: ubuntu-24.04-arm @@ -131,6 +188,7 @@ jobs: runtime: runtime-linux-arm64-gnu cli: cli-linux-arm64-gnu llvm_asset: LLVM-22.1.8-Linux-ARM64 + llvm_dist: arm64 use_zig: true - platform: windows-x64 runner: windows-2022 @@ -146,6 +204,7 @@ jobs: seed_helper: llvm-linux-x64-gnu cli_target: x86_64-linux-musl llvm_asset: LLVM-22.1.8-Linux-X64 + llvm_dist: x64 use_zig: true - platform: linux-arm64-musl runner: ubuntu-24.04-arm @@ -155,12 +214,14 @@ jobs: seed_helper: llvm-linux-arm64-gnu cli_target: aarch64-linux-musl llvm_asset: LLVM-22.1.8-Linux-ARM64 + llvm_dist: arm64 use_zig: true - platform: wasm32-wasi runner: ubuntu-24.04 helper: llvm-linux-x64-gnu runtime: runtime-wasm32-wasi llvm_asset: LLVM-22.1.8-Linux-X64 + llvm_dist: x64 use_zig: true runs-on: ${{ matrix.runner }} # The Intel macOS build took 24 minutes for 0.3.2 and passed 30 for 0.3.3. @@ -191,6 +252,28 @@ jobs: mv "$RUNNER_TEMP/${{ matrix.llvm_asset }}" "$RUNNER_TEMP/llvm-22.1.8" echo "LLVM_DIR=$RUNNER_TEMP/llvm-22.1.8/lib/cmake/llvm" >> "$GITHUB_ENV" echo "$RUNNER_TEMP/llvm-22.1.8/bin" >> "$GITHUB_PATH" + # The Linux helpers link LLVM built with ThinLTO and a profile of the + # helper's code generation (native/llvm-codegen/llvm-dist.mjs), from a + # release of this repository. Without that archive they link LLVM's + # release archive, which gives the same objects more slowly. The + # wasm32-wasi job also uploads the x64 GNU helper, so it selects the same. + - name: Select the profiled LLVM distribution (Linux) + if: matrix.llvm_dist != '' + env: + ARCH: ${{ matrix.llvm_dist }} + run: | + tag=$(node --input-type=module -e "import { LLVM_DIST_TAG } from './native/llvm-codegen/llvm-dist.mjs'; console.log(LLVM_DIST_TAG)") + asset=$(node --input-type=module -e "import { llvmDistAsset } from './native/llvm-codegen/llvm-dist.mjs'; console.log(llvmDistAsset(process.env.ARCH))") + base="https://github.com/$GITHUB_REPOSITORY/releases/download/$tag" + if curl -fsSL --retry 3 -o "$RUNNER_TEMP/$asset" "$base/$asset" && + curl -fsSL --retry 3 -o "$RUNNER_TEMP/$asset.sha256" "$base/$asset.sha256" && + (cd "$RUNNER_TEMP" && sha256sum --check "$asset.sha256"); then + tar -xJf "$RUNNER_TEMP/$asset" -C "$RUNNER_TEMP" + echo "SCRIPTC_HELPER_LLVM_DIR=$RUNNER_TEMP/${asset%.tar.xz}/lib/cmake/llvm" >> "$GITHUB_ENV" + echo "The helper links $asset from release $tag" + else + echo "::warning::$asset is not available from release $tag; the helper links LLVM's release archive" + fi - uses: vercel-labs/setup-zig@v1 if: matrix.use_zig == true with: @@ -229,7 +312,15 @@ jobs: - name: Build native packages (POSIX) if: matrix.platform != 'windows-x64' run: | - pnpm --filter @scriptc/${{ matrix.helper }} build:native + if [ -n "${SCRIPTC_HELPER_LLVM_DIR:-}" ]; then + LLVM_DIR="$SCRIPTC_HELPER_LLVM_DIR" SCRIPTC_LLVM_THINLTO=1 pnpm --filter @scriptc/${{ matrix.helper }} build:native || { + echo "::warning::the helper did not build against the profiled LLVM; it links LLVM's release archive" + rm -rf node_modules/.cache/scriptc-${{ matrix.helper }} + pnpm --filter @scriptc/${{ matrix.helper }} build:native + } + else + pnpm --filter @scriptc/${{ matrix.helper }} build:native + fi if [ "${{ matrix.use_zig }}" = true ]; then CC=zig AR=zig pnpm --filter @scriptc/${{ matrix.runtime }} build:native else @@ -243,6 +334,33 @@ jobs: - name: Build host helper for cross-libc bootstrap if: matrix.seed_helper != '' run: pnpm --filter @scriptc/${{ matrix.seed_helper }} build:native + - name: Download the profile collected on this commit + id: cli-profile + if: matrix.cli != '' + continue-on-error: true + uses: actions/download-artifact@v4 + with: + name: cli-profile + path: ${{ runner.temp }}/cli-profile + # Without the artifact, SCRIPTC_NATIVE_PROFILE_USE and SCRIPTC_NATIVE_LAYOUT + # stay unset and build-native-cli.mts uses the committed profile and layout. + - name: Select the native compiler profile + if: matrix.cli != '' + shell: bash + env: + COLLECTED: ${{ steps.cli-profile.outcome == 'success' && format('{0}/cli-profile/cli.profdata.gz', runner.temp) || '' }} + run: | + if [ -n "$COLLECTED" ]; then + echo "SCRIPTC_NATIVE_PROFILE_USE=$COLLECTED" >> "$GITHUB_ENV" + echo "The native CLI builds with the profile collected on $GITHUB_SHA" + layout="$(dirname "$COLLECTED")/cli.layout.gz" + if [ -f "$layout" ]; then + echo "SCRIPTC_NATIVE_LAYOUT=$layout" >> "$GITHUB_ENV" + echo "The native CLI builds with the function layout collected on $GITHUB_SHA" + fi + else + echo "::warning::No profile was collected on this commit; the native CLI builds with the committed profile" + fi - name: Build native CLI distribution if: matrix.cli != '' env: @@ -317,12 +435,15 @@ jobs: (needs.check-release.outputs.reuse_artifacts == 'true' || (needs.build-native-packages.result == 'success' && needs.build-mobile-runtime-packs.result == 'success')) runs-on: ubuntu-latest - timeout-minutes: 15 + # Two waits for npm availability of at most 25 minutes each. + timeout-minutes: 70 environment: Release permissions: contents: read actions: read id-token: write + outputs: + uploads: ${{ steps.publish.outputs.uploads }} steps: - name: Checkout repository uses: actions/checkout@v4 @@ -342,7 +463,7 @@ jobs: VERSION: ${{ needs.check-release.outputs.version }} RECEIPT_RUN: ${{ needs.check-release.outputs.receipt_run }} run: | - touch "$RUNNER_TEMP/npm-accepted.txt" + touch "$RUNNER_TEMP/npm-accepted.txt" "$RUNNER_TEMP/npm-accepted-times.txt" # Include this run so re-running only failed jobs restores its receipts. for run in "$GITHUB_RUN_ID" "$RECEIPT_RUN"; do if [ -z "$run" ]; then continue; fi @@ -351,6 +472,7 @@ jobs: if [ "$count" -eq 0 ]; then continue; fi gh run download "$run" --repo "$GITHUB_REPOSITORY" --pattern "npm-accepted-$VERSION-*" --dir "$RUNNER_TEMP/npm-receipts/$run" find "$RUNNER_TEMP/npm-receipts/$run" -name npm-accepted.txt -exec cat {} + >> "$RUNNER_TEMP/npm-accepted.txt" + find "$RUNNER_TEMP/npm-receipts/$run" -name npm-accepted-times.txt -exec cat {} + >> "$RUNNER_TEMP/npm-accepted-times.txt" done - name: Setup pnpm @@ -400,6 +522,7 @@ jobs: done - name: Publish to npm + id: publish shell: bash env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -419,10 +542,14 @@ jobs: # Upload acceptance and public availability are separate states. # Save acceptance immediately so retries never depend on registry lag. + # npm-release.txt lists every package of the release, and + # npm-accepted-times.txt the time npm accepted each upload, for the + # npm-available job. publish_dir() { dir="$1" packed="${2:-}" name=$(node -p "require('./$dir/package.json').name") + echo "$name@$VERSION" >> "$RUNNER_TEMP/npm-release.txt" if grep -Fxq "$name@$VERSION" "$RUNNER_TEMP/npm-accepted.txt"; then echo "$name@$VERSION upload already accepted, skipping" return 0 @@ -439,6 +566,7 @@ jobs: publish_log="$RUNNER_TEMP/npm-publish.log" if npm publish "$packed" $PROVENANCE --access public 2>&1 | tee "$publish_log"; then echo "$name@$VERSION" >> "$RUNNER_TEMP/npm-accepted.txt" + echo "$name@$VERSION $(date +%s)" >> "$RUNNER_TEMP/npm-accepted-times.txt" return 0 else publish_status=${PIPESTATUS[0]} @@ -461,8 +589,8 @@ jobs: } # pnpm pack resolves workspace dependencies; npm handles OIDC. - # Publish dependencies first. Registry propagation after an accepted - # upload must not block the remaining packages or the GitHub release. + # Publish dependencies first. The npm-available job waits for the + # packages that nothing in this release depends on. publish_dir packages/runtime publish_dir packages/runtime-darwin-arm64 publish_dir packages/llvm-darwin-arm64 @@ -482,32 +610,88 @@ jobs: publish_dir packages/runtime-wasm32-wasi publish_dir packages/runtime-win32-x64-msvc publish_dir packages/llvm-win32-x64-msvc + # A native CLI package depends on its platform's LLVM helper and + # runtime pack packages, and scriptc on the native CLI packages. + # Each group publishes only once the packages it depends on + # resolve, so an install never finds a package whose dependencies + # npm is still processing. A rerun skips accepted uploads and waits + # again. + wait_available() { + awk 'FILENAME == ARGV[1] { accepted[$1] = $2; next } { print $1, accepted[$1] }' \ + "$RUNNER_TEMP/npm-accepted-times.txt" "$RUNNER_TEMP/npm-release.txt" > "$RUNNER_TEMP/npm-wait.txt" + node scripts/wait-npm-available.mjs "$RUNNER_TEMP/npm-wait.txt" --interval 30 --timeout 1500 + } + wait_available for pkg in packages/cli-*; do publish_dir "$pkg"; done publish_dir packages/compiler publish_dir packages/experimental publish_dir packages/node publish_dir packages/threads publish_dir packages/types + wait_available publish_dir packages/cli echo "All package uploads accepted; npm registry propagation may continue after this job." >> "$GITHUB_STEP_SUMMARY" + # Each package of the release with the time its upload was accepted, + # when a run recorded it. + { + echo "uploads<> "$GITHUB_OUTPUT" + - name: Save accepted upload receipts if: always() uses: actions/upload-artifact@v4 with: name: npm-accepted-${{ needs.check-release.outputs.version }}-${{ github.run_attempt }} - path: ${{ runner.temp }}/npm-accepted.txt + path: | + ${{ runner.temp }}/npm-accepted.txt + ${{ runner.temp }}/npm-accepted-times.txt if-no-files-found: ignore retention-days: 90 - # Publish standalone native distributions and the surface manifest after - # npm publishing succeeds. The release body comes from the marked changelog. - github-release: - name: Create GitHub Release + # Waits until every package of the release resolves on the npm registry, + # and reports each package's time from upload to available. Fails with the + # packages that are still missing after 2 hours. It only reads from the + # registry; the next release run retries it, and publishing skips uploads + # npm already accepted. + npm-available: + name: Wait for npm availability needs: [check-release, publish] if: >- !cancelled() && needs.check-release.outputs.should_release == 'true' && needs.publish.result == 'success' runs-on: ubuntu-latest + timeout-minutes: 135 + permissions: + contents: read + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "24" + registry-url: "https://registry.npmjs.org" + + - name: Wait for every package to resolve + env: + UPLOADS: ${{ needs.publish.outputs.uploads }} + run: | + printf '%s\n' "$UPLOADS" > "$RUNNER_TEMP/npm-uploads.txt" + node scripts/wait-npm-available.mjs "$RUNNER_TEMP/npm-uploads.txt" + + # Publish standalone native distributions and the surface manifest after + # every npm package resolves. The release body comes from the marked + # changelog. + github-release: + name: Create GitHub Release + needs: [check-release, publish, npm-available] + if: >- + !cancelled() && needs.check-release.outputs.should_release == 'true' && needs.npm-available.result == 'success' + runs-on: ubuntu-latest timeout-minutes: 10 permissions: contents: write @@ -559,6 +743,21 @@ jobs: - name: Package standalone compilers run: node scripts/package-native-cli.mjs packages /tmp/scriptc-release-assets + # The profile the native CLIs were built with. Runs that reuse native + # builds from before the cli-profile job, or whose profile job failed, + # have none; their CLIs used the committed profile. + - name: Download the native compiler profile + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RUN: ${{ needs.check-release.outputs.artifact_run }} + VERSION: ${{ needs.check-release.outputs.version }} + run: | + if gh run download "$RUN" --repo "$GITHUB_REPOSITORY" --name cli-profile --dir "$RUNNER_TEMP/cli-profile"; then + mv "$RUNNER_TEMP/cli-profile/cli.profdata.gz" "/tmp/scriptc-release-assets/scriptc-$VERSION-cli.profdata.gz" + else + echo "Run $RUN has no cli-profile artifact" + fi + - name: Create GitHub Release run: | VERSION="${{ needs.check-release.outputs.version }}" diff --git a/.oxlintrc.jsonc b/.oxlintrc.jsonc index b0acea28a..55c8d0353 100644 --- a/.oxlintrc.jsonc +++ b/.oxlintrc.jsonc @@ -283,10 +283,22 @@ }, { // Ambient declarations describe an existing constructor and its prototype methods. - "files": ["packages/compiler/ambient/scriptc-node-fallback.d.ts"], + "files": [ + "packages/compiler/ambient/scriptc-node-fallback.d.ts", + "packages/compiler/ambient/node-fallback/*.d.ts", + ], "rules": { "typescript/no-unsafe-declaration-merging": "off", }, }, + { + // Each module's fallback declarations are a script that declares the + // module ambiently; it names the files of the modules it uses by + // reference, since a script cannot import them. + "files": ["packages/compiler/ambient/node-fallback/*.d.ts"], + "rules": { + "typescript/triple-slash-reference": "off", + }, + }, ], } diff --git a/AGENTS.md b/AGENTS.md index 2350d7337..55ccb2eab 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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//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/.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. diff --git a/CHANGELOG.md b/CHANGELOG.md index ec0b27dbf..e8ab725f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,85 @@ All notable changes to scriptc will be documented in this file. +## 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. + + + +## 0.4.4 + +### Highlights + +- **Fixes for 0.4.3:** + - Unchanged rebuilds hit the frontend cache again. An unchanged rebuild of a 23-module program takes 93 ms instead of 1.26 s, and of tsc-ts (dev) 1.6 s instead of 58.2 s. + - The native compiler builds programs with several typed-array accesses at one masked index in a loop again, including tsc-ts's release build, which failed with SC3004. + - `x!` returned from a function, used as an arrow function's result, or passed as an argument keeps null or undefined when the destination type holds it, as JavaScript does. `x!` operands of `+`, `+=` and other conversions that hold undefined or null convert as Node does. + - Effect 4 programs whose generators yield clients built by other generators run again. + - The cache for untyped property access in programs that declare classes is reverted, fixing a crash in Effect 4 observability programs. +- **Faster:** + - Number compares and typed-array indexes use the exact integer of sums like `i + 1`: a heap sift-down over an `Int32Array` runs 17.5% faster. + - glibc's per-thread heaps use huge pages: the VS Code type-check runs 3.4% faster. + - Mid-size executables (over 1 MiB of LLVM text) build in at least eight partitions: the LLVM backend phase takes 34 to 74% less time for three.js programs of 1.4 to 7.8 MB. + - JavaScript class instances are boxed at their exact class, and stores into nested string-keyed dictionaries keep class method calls direct. + - `--target-cpu` selects an x86-64 microarchitecture level. +- **Node.js parity:** + - X25519 keys and `crypto.diffieHellman`; `existsSync` and `fs.promises` accept `file:` URLs; `Buffer.from` ranges over ArrayBuffers, the deprecated `Buffer()` constructor and `Buffer` as a value. + - Dates are objects with identity; `Promise.all` over different entry types settles on Node's turns; completion callbacks run in their call's AsyncLocalStorage context. + - TypedArray methods run on typed arrays held as untyped values, and unions of typed-array classes compile. Borrowed `TypedArray.prototype.set` calls compile and throw Node's TypeError for other targets. + - Split CommonJS `require` bindings, `__`-prefixed exports, string-key destructuring of `any` values, and record key order for values JavaScript built. +- **npm packages:** zod 4 and `@hono/zod-validator` values are projected at the declaration boundary. + +## 0.4.3 + +### Highlights + +- **Faster checking and smaller builds with tsc-ts 0.0.9:** scriptc checks with tsc-ts 0.0.9, and builds reuse the bundled declaration files' diagnostics instead of checking them again. Small IR builds are 10.6 to 16.0% faster and small executable builds 5.5 to 7.6% faster, with identical output. A native build of tsc-ts with this release checks 0.4 to 6.6% faster than with 0.4.2 and peaks 4 to 16% lower. +- **Integer arrays and loops:** + - Number arrays that only hold int32 values use 4-byte slots in locals, across direct calls and in module globals, and loop-carried locals written from their elements stay int32. A workload that walks arrays of cursors runs 54.5% faster. + - Loops with two counters closing in on each other (`for (let i = lo, j = hi; i < j; i++, j--)`) and element copies through `a[i]!` run without per-element checks, and two-counter `while` loops run on integer counters. + - Loops over typed arrays and charAt scans are versioned on their bounds, and ASCII strings are scanned as bytes. +- **Less work for untyped and dynamic code:** + - String-keyed record reads under `??` and `!` no longer allocate: a dictionary-walking workload makes no union boxes instead of 363,359 and runs 12 to 15% faster. + - Checked-dynamic method calls dispatch on method ids, observed class methods call natively while their slot is unchanged, and calls on bag-tracked class instances skip building their views. +- **Node.js parity:** + - `crypto.getCipherInfo`, AES-CCM, AES key wrap, the legacy `createCipheriv` modes, `checkPrime`/`generatePrime`, and Node's argument checks for `generateKeyPair`. + - Streams: `construct`, `writev`, `decodeStrings` and `defaultEncoding`, including `writev` on `Transform` and `PassThrough`. + - `FinalizationRegistry`, `new.target`, `String.prototype.at` on untyped values, `Error.stackTraceLimit`, `spawnSync`'s `env` option, and class constructors called through `call`/`apply` throw Node's TypeError. + - Records enumerate in the key order their object literal wrote, and objects converting to a union with an empty-record arm keep every field. +- **npm packages:** zod 3 compiles natively and zod 4's core runs, minified packages such as marked compile, chalk 5 and hot-shots 8 build from built-in shims, and `@hono/node-server` 2.x is built in. User code types npm package declarations at a checked, identity-preserving boundary. +- **Builds:** the cached build is reused after whitespace and comment edits, function bodies are emitted on forked workers, and the LLVM emitter reads its analyses from one recorded walk of each module. + ## 0.4.2 ### Highlights @@ -159,8 +238,6 @@ All notable changes to scriptc will be documented in this file. - **The same program compiles to the same executable wherever it sits on disk** and whatever order its functions lower in, and edits outside a declaration do not rename the helpers and closures inside it. - **Libraries that use untyped byte arrays** link. - - ## 0.4.1 ### Highlights diff --git a/RELEASING.md b/RELEASING.md index 55ed7fae2..c01673806 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -14,9 +14,11 @@ To prepare a release: CI (`.github/workflows/release.yml`) compares the version in `packages/cli/package.json` to what `scriptc` has on npm. If it differs, it builds platform packages on matching macOS, Linux, and Windows runners, verifies the version spine, and publishes the runtime, helper, compiler, and CLI packages in dependency order. After the publish succeeds, a separate job creates the git tag `v` and the GitHub release with the marked changelog entry as its body, and attaches `surface-manifest.json` — the machine-readable listing of the surface the static tier compiles at that version (stable per-entry ids, so two releases diff mechanically; see `packages/compiler/src/coverage/surface-manifest.ts` for the schema). The job regenerates the manifest from the tree and fails on any byte difference from the committed file before attaching, so the asset is always the manifest of the code being released. The same file ships inside the `@scriptc/compiler` package as `@scriptc/compiler/surface-manifest.json`. -The release job builds each pinned LLVM helper, precompiled runtime pack, and native CLI on its matching host. The native CLI bundles its TypeScript checker, LLVM helper, runtime pack, declarations, and comptime evaluator. npm postinstall installs the matching native executable; Node is needed for npm installation but not native compilation or execution. The user's toolchain performs the final platform link. Sanitized builds also use it to instrument program LLVM and compile the C runtime. Platform packages publish ahead of `@scriptc/compiler` and `scriptc`. The GitHub release attaches standalone compiler archives, their SHA-256 checksums, and the surface manifest after npm publication succeeds. +The release job builds each pinned LLVM helper, precompiled runtime pack, and native CLI on its matching host. The native CLI package bundles its TypeScript checker, declarations, and comptime evaluator, and depends on its platform's LLVM helper and runtime pack packages at the same version; the compiler resolves them beside itself. The standalone archives bundle the helper and runtime pack too. npm postinstall installs the matching native executable; Node is needed for npm installation but not native compilation or execution. The user's toolchain performs the final platform link. Sanitized builds also use it to instrument program LLVM and compile the C runtime. Platform packages publish ahead of `@scriptc/compiler` and `scriptc`. The GitHub release attaches standalone compiler archives, their SHA-256 checksums, and the surface manifest after npm publication succeeds. -Release builds of the native CLI optimize the compiler with the committed profile `packages/compiler/native/cli.profdata` on every platform. Code the profile does not describe, or describes from older compiler sources, is optimized as without a profile. Refresh the profile on a quiet Linux x64 host when compiler code has changed substantially since the last refresh: `node scripts/refresh-cli-profile.mjs --tsc-ts ` builds an instrumented compiler, runs it on tsc-ts, a slice of the corpus, and the runtime benchmark workloads, and writes the merged sparse profile. Commit the new profile on its own. +Release builds of the native CLI optimize the compiler with a profile collected on the release commit. The `cli-profile` job runs `node scripts/refresh-cli-profile.mjs` on Linux x64, which builds an instrumented compiler, runs it on tsc-ts, a slice of the corpus, and the runtime benchmark workloads, and writes the merged sparse profile. Every platform's CLI build uses that profile (`SCRIPTC_NATIVE_PROFILE_USE`), and the GitHub release attaches it as `scriptc--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 ` 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 diff --git a/benchmarks/runtime/README.md b/benchmarks/runtime/README.md index 4f152e4bb..9af4ace00 100644 --- a/benchmarks/runtime/README.md +++ b/benchmarks/runtime/README.md @@ -10,12 +10,18 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea | `atomics-contended` | cpu | four worker threads updating `SharedArrayBuffer` counters with `Atomics`: one shared counter, one counter per worker, a `compareExchange` loop, and `Atomics.wait`/`notify` handoffs | | `channel-messages` | cpu | 200,000 MessageChannel messages in one thread, half numbers and half small objects, all posted before the port delivers any | | `csv-numbers` | cpu | `split`, `Number`, `parseInt`, `toFixed` | +| `date-math` | cpu | schedules built with Date setters (local and UTC), weekday and month-end rules through the getters, Dates sorted by time value | | `cyclic-graphs` | cpu | object graphs with back references built and dropped: trees with parent and sibling links, rings of records sharing a long-lived registry, closures capturing their owner | | `dictionary-walks` | cpu | `Record` dictionaries walked with for-in and `Object.keys`/`values`/`entries` while entries are added, updated and deleted | | `dynamic-methods` | cpu | untyped JavaScript calling builtin methods on values it does not type: string `slice`/`indexOf`/`toUpperCase`/`padStart`, array `push`/`pop`/`includes`, `Map` and `Set`, and an object's own methods | +| `dynamic-properties` | cpu | untyped JavaScript reading and writing properties of values it does not type: records built by object literals in two key orders, nested records, and instances of a class and its subclass | | `functional-pipeline` | cpu | `filter`/`map`/`reduce`/`flatMap` chains, closures, small records | +| `handler-tables` | cpu | dispatch tables of functions: `Record` read as `t[k]!(…)`, `t[k]?.(…)` and through a checked local, a `Map`, an array of handlers, and an object literal of methods; some handlers declare fewer parameters than the table type | | `intern-table`, `intern-table-map`, `intern-table-1`, `intern-table-map-1` | cpu | worker threads interning the same type-like objects under string keys and records under number keys, then hits and misses: one shared `InternTable` per kind of key, or a module-level `Map` per thread (the `-map` entries), with 8 threads or 1 (the `-1` entries); under Node.js every thread has its own tables | | `json-records` | cpu | `JSON.stringify`, typed `JSON.parse`, Map aggregation | +| `json-unions` | cpu | `JSON.parse` into discriminated unions: an event log with eight kinds and nested expression trees with nine, evaluated after the cast | +| `keyed-option-writes` | cpu | class methods whose names are also keys of option records written through element accesses: `enabled[name] = true` over a union of literal names and `totals["count"] = n` | +| `line-reader` | cpu | `readline` over a file stream: 'line' events for a 67 MB access log (every 50th line not ASCII), with a status count, a path tally and byte totals | | `log-lines` | cpu | 200k immediate `console.log`/`process.stdout.write` lines into the runner's pipe | | `log-summary`, `inventory-report` | cpu | the multi-module applications from `benchmarks/builds`, on 300k-line inputs | | `number-maps` | cpu | number-keyed Map/Set: a dense-id table, sparse ids in many small maps, get/has/set mixes | @@ -25,12 +31,16 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea | `record-boundaries` | cpu | records built from unchecked array reads passed to and returned from annotated functions | | `records-sort` | cpu | comparator sorts, group-by, `reduce`/`filter` | | `regex-logs` | cpu | regex `exec` with groups, `test`, global `replace` | +| `regex-replacers` | cpu | regex `replace`/`replaceAll` with inline callbacks whose parameters have no annotations: dictionary placeholders, snake_case to camelCase, date reordering, optional captures, capitalization, and a string search that reads the offset | | `region-builds` | cpu | a watch-mode build loop over `LifetimeRegion`: each build binds a program in a new region and releases the previous one, which a cache keeps referenced for two builds | +| `string-iteration` | cpu | code point iteration over text that mixes ASCII, Latin-1 letters, CJK and emoji: a for-of word tokenizer, spread reversal, and `Array.from` over words | | `template-render` | cpu | template literals, escaping, `map`/`join` | +| `throw-chains` | cpu | exceptions caught 2, 5 and 20 recursive calls above the throw, through direct calls and through a callback, and a backtracking parser that throws on a failed alternative | | `thread-slots`, `thread-slots-cache` | cpu | worker threads reading one published type graph from an `InternTable` and keeping per-thread state for each type they visit: a `ThreadSlots` family, or a direct-mapped cache by id in front of a `Map` by id (the `-cache` entry); under Node.js both are per thread | | `typed-array-kernels` | cpu | element-wise loops that run to one typed array's length and index others at the same position: `Float64Array` axpy, `Float32Array` blend, `Uint8Array` mask, wrapping `Int32Array` add, `Uint8ClampedArray` brighten, RGBA to gray, a row-major `Float32Array` matrix-vector product, and `reduce`/`some`/`map` callbacks | | `typed-array-scan`, `typed-array-scan-workers` | cpu | element-by-element scan loops over `Uint8Array`, `Uint16Array`, `Int32Array` and `Float64Array`; the second entry is a worker-enabled executable (it can start a worker), so each access also checks for shared storage | | `unicode-lexer` | cpu | a hand-written lexer over a 2.7M-unit bundled module whose comments and strings are in several languages: `charCodeAt` scans, line starts | +| `union-node-sets` | cpu | syntax-tree passes over a discriminated union of object literals: a `Set` of visited nodes, a `Map` from node to value, conversions to a sub-union of leaves | | `validate-errors` | cpu | throw/catch on a fraction of inputs, small call chains | | `value-objects` | cpu | small immutable records and class instances: vector helpers, method chains, particle updates | | `value-records` | cpu | records that hold strings, spread state updates and destructured tuples: token helpers, an immutable reducer, tuple returns | diff --git a/benchmarks/runtime/workloads/date-math.json b/benchmarks/runtime/workloads/date-math.json new file mode 100644 index 000000000..b43078582 --- /dev/null +++ b/benchmarks/runtime/workloads/date-math.json @@ -0,0 +1,6 @@ +{ + "entry": "workloads/date-math.ts", + "args": [ + "40" + ] +} diff --git a/benchmarks/runtime/workloads/date-math.ts b/benchmarks/runtime/workloads/date-math.ts new file mode 100644 index 000000000..56ec1066e --- /dev/null +++ b/benchmarks/runtime/workloads/date-math.ts @@ -0,0 +1,52 @@ +// Calendar arithmetic on Dates: schedules built by adjusting Dates with the +// UTC and local setters, weekday and month-end rules read back through the +// getters, Dates compared and sorted by time value, and ISO text for a +// sample. Argument: the number of rounds. +process.env.TZ = "America/New_York"; + +const rounds = Number(process.argv[2] ?? "40"); +let seed = 2026; +function random(): number { + seed = (Math.imul(seed, 1103515245) + 12345) >>> 0; + return seed / 4294967296; +} + +/** The next business day at 09:00 local time on or after `from`. */ +function nextBusinessDay(from: Date): Date { + const day = new Date(from.getTime()); + day.setHours(9, 0, 0, 0); + if (day < from) day.setDate(day.getDate() + 1); + while (day.getDay() === 0 || day.getDay() === 6) day.setDate(day.getDate() + 1); + return day; +} + +/** The last day of `date`'s month, in UTC. */ +function monthEnd(date: Date): number { + const end = new Date(date.getTime()); + end.setUTCDate(1); + end.setUTCMonth(end.getUTCMonth() + 1); + end.setUTCDate(0); + return end.getUTCDate(); +} + +let checksum = 0; +let sample = ""; +for (let round = 0; round < rounds; round++) { + const due: Date[] = []; + const cursor = new Date(Date.UTC(2020 + (round % 7), 0, 1, 12)); + for (let i = 0; i < 4000; i++) { + cursor.setUTCMinutes(cursor.getUTCMinutes() + Math.floor(random() * 4000)); + const next = nextBusinessDay(cursor); + checksum += next.getDate() + monthEnd(next) + next.getHours(); + if (i % 3 === 0) next.setMonth(next.getMonth() + 1, 15); + due.push(next); + } + due.sort((a, b) => +a - +b); + let previous = due[0]!; + for (const date of due) { + checksum += (+date - +previous) / 3600000; + previous = date; + } + sample = due[due.length >> 1]!.toISOString(); +} +console.log(Math.round(checksum), sample); diff --git a/benchmarks/runtime/workloads/dynamic-properties.json b/benchmarks/runtime/workloads/dynamic-properties.json new file mode 100644 index 000000000..398981ef3 --- /dev/null +++ b/benchmarks/runtime/workloads/dynamic-properties.json @@ -0,0 +1,6 @@ +{ + "entry": "workloads/dynamic-properties.mjs", + "args": [ + "1000000" + ] +} diff --git a/benchmarks/runtime/workloads/dynamic-properties.mjs b/benchmarks/runtime/workloads/dynamic-properties.mjs new file mode 100644 index 000000000..d0ee3802c --- /dev/null +++ b/benchmarks/runtime/workloads/dynamic-properties.mjs @@ -0,0 +1,56 @@ +// Untyped JavaScript reading and writing properties of values it never +// declares types for: records built by object literals in two key orders, +// nested records, property updates, and instances of a class and its +// subclass read and written the same way. Argument: the number of +// iterations. +const rounds = Number(process.argv[2] ?? "1000000"); +class Cursor { + constructor(x, y) { + this.x = x; + this.y = y; + } + step(dx, dy) { + this.x += dx; + this.y += dy; + return this; + } +} +class Marker extends Cursor { + constructor(x, y, label) { + super(x, y); + this.label = label; + } +} +function makeItem(i) { + if (i % 3 === 0) + return { id: i, price: i % 17, qty: i % 5, meta: { tag: i % 7, weight: i % 11 } }; + return { id: i, qty: i % 5, price: i % 13, meta: { weight: i % 9, tag: i % 5 } }; +} +function cost(item) { + return item.price * item.qty + item.meta.weight; +} +function restock(item, round) { + item.qty = (item.qty + round) % 7; + item.meta.tag = (item.meta.tag + 1) % 7; +} +function distance(a, b) { + return Math.abs(a.x - b.x) + Math.abs(a.y - b.y); +} +const items = []; +for (let i = 0; i < 1024; i++) items.push(makeItem(i)); +const cursors = []; +for (let i = 0; i < 64; i++) + cursors.push(i % 2 === 0 ? new Cursor(i - 32, 7 - i) : new Marker(i - 32, 7 - i, "m" + i)); +const origin = { x: 0, y: 0 }; +let checksum = 0; +for (let r = 0; r < rounds; r++) { + const item = items[r & 1023]; + checksum += cost(item); + restock(item, r & 3); + checksum += item.meta.tag; + const cursor = cursors[r & 63]; + checksum += distance(cursor.step((r % 3) - 1, (r % 5) - 2), origin); + if (cursor.x > 100 || cursor.x < -100) cursor.x = 0; + if (cursor.y > 100 || cursor.y < -100) cursor.y = 0; +} +console.log(checksum, items[5].qty, items[6].meta.tag, cursors[3].x, cursors[3].y, cursors[3].label); diff --git a/benchmarks/runtime/workloads/handler-tables.json b/benchmarks/runtime/workloads/handler-tables.json new file mode 100644 index 000000000..1408ae91e --- /dev/null +++ b/benchmarks/runtime/workloads/handler-tables.json @@ -0,0 +1,6 @@ +{ + "entry": "workloads/handler-tables.ts", + "args": [ + "4" + ] +} diff --git a/benchmarks/runtime/workloads/handler-tables.ts b/benchmarks/runtime/workloads/handler-tables.ts new file mode 100644 index 000000000..b0a61fcfe --- /dev/null +++ b/benchmarks/runtime/workloads/handler-tables.ts @@ -0,0 +1,108 @@ +// Dispatch tables of functions: a `Record` of opcode handlers +// called as `ops[name]!(…)`, `ops[name]?.(…)` and through a checked local, +// a `Map` 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 = { + 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(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(" ")); diff --git a/benchmarks/runtime/workloads/index-cursors.json b/benchmarks/runtime/workloads/index-cursors.json new file mode 100644 index 000000000..24f2a263c --- /dev/null +++ b/benchmarks/runtime/workloads/index-cursors.json @@ -0,0 +1,6 @@ +{ + "entry": "workloads/index-cursors.ts", + "args": [ + "20000" + ] +} diff --git a/benchmarks/runtime/workloads/index-cursors.ts b/benchmarks/runtime/workloads/index-cursors.ts new file mode 100644 index 000000000..b65ded46b --- /dev/null +++ b/benchmarks/runtime/workloads/index-cursors.ts @@ -0,0 +1,25 @@ +// Arrays of indices walked by loop-carried cursors: each step reads the +// next index from the array and relinks the visited entries, so the +// cursor and every stored value are small integers read back from the +// array itself. +const N = 4096; + +function relink(rounds: number): number { + const next: number[] = []; + for (let i = 0; i < N; i++) next.push(i + 1 < N ? i + 1 : 0); + let at = next[0]!; + let total = 0; + for (let r = 0; r < rounds; r++) { + for (let s = 0; s < N; s++) { + const t = next[at]!; + next[at] = next[t]!; + next[t] = at; + at = t; + total = (total + at) | 0; + } + } + return total; +} + +const rounds = Number(process.argv[2] ?? "1"); +console.log("relink", relink(rounds)); diff --git a/benchmarks/runtime/workloads/json-unions.json b/benchmarks/runtime/workloads/json-unions.json new file mode 100644 index 000000000..925e8457b --- /dev/null +++ b/benchmarks/runtime/workloads/json-unions.json @@ -0,0 +1,6 @@ +{ + "entry": "workloads/json-unions.ts", + "args": [ + "6" + ] +} diff --git a/benchmarks/runtime/workloads/json-unions.ts b/benchmarks/runtime/workloads/json-unions.ts new file mode 100644 index 000000000..0f684c35f --- /dev/null +++ b/benchmarks/runtime/workloads/json-unions.ts @@ -0,0 +1,155 @@ +// JSON text parsed into discriminated unions: an event log with eight kinds +// of event and a nested expression tree with nine kinds of node, cast with +// `JSON.parse(text) as T`, then aggregated and evaluated. Argument: the +// number of rounds. +type Event = + | { kind: "click"; target: string; x: number; y: number } + | { key: string; kind: "key"; repeat: boolean } + | { kind: "scroll"; offset: number } + | { kind: "resize"; height: number; width: number } + | { kind: "focus"; target: string } + | { kind: "blur"; target: string } + | { kind: "load"; ms: number; url: string } + | { code: number; kind: "error"; message: string }; + +type Expr = + | { kind: "num"; value: number } + | { kind: "var"; name: string } + | { kind: "neg"; operand: Expr } + | { kind: "add"; left: Expr; right: Expr } + | { kind: "sub"; left: Expr; right: Expr } + | { kind: "mul"; left: Expr; right: Expr } + | { kind: "max"; items: Expr[] } + | { else: Expr; kind: "if"; test: Expr; then: Expr } + | { body: Expr; kind: "let"; name: string; value: Expr }; + +let seed = 12345; +function random(): number { + seed = (Math.imul(seed, 1103515245) + 12345) >>> 0; + return seed / 4294967296; +} + +const targets = ["menu", "search", "button", "list", "footer"]; +function eventText(i: number): string { + const target = targets[i % targets.length]!; + switch (i % 8) { + case 0: + return `{"kind":"click","target":"${target}","x":${i % 640},"y":${i % 480}}`; + case 1: + return `{"key":"k${i % 26}","kind":"key","repeat":${i % 3 === 0}}`; + case 2: + return `{"kind":"scroll","offset":${(i * 7) % 2000}}`; + case 3: + return `{"height":${400 + (i % 300)},"kind":"resize","width":${600 + (i % 500)}}`; + case 4: + return `{"kind":"focus","target":"${target}"}`; + case 5: + return `{"kind":"blur","target":"${target}"}`; + case 6: + return `{"kind":"load","ms":${i % 900},"url":"/page/${i % 50}"}`; + default: + return `{"code":${400 + (i % 100)},"kind":"error","message":"failed ${i % 13}"}`; + } +} + +const names = ["a", "b", "c", "d"]; +function exprText(depth: number): string { + const pick = depth <= 0 ? Math.floor(random() * 2) : Math.floor(random() * 9); + switch (pick) { + case 0: + return `{"kind":"num","value":${Math.floor(random() * 100)}}`; + case 1: + return `{"kind":"var","name":"${names[Math.floor(random() * names.length)]!}"}`; + case 2: + return `{"kind":"neg","operand":${exprText(depth - 1)}}`; + case 3: + case 4: + case 5: { + const kind = pick === 3 ? "add" : pick === 4 ? "sub" : "mul"; + return `{"kind":"${kind}","left":${exprText(depth - 1)},"right":${exprText(depth - 1)}}`; + } + case 6: + return `{"items":[${exprText(depth - 1)},${exprText(depth - 2)},${exprText(depth - 2)}],"kind":"max"}`; + case 7: + return `{"else":${exprText(depth - 2)},"kind":"if","test":${exprText(depth - 1)},"then":${exprText(depth - 2)}}`; + default: + return `{"body":${exprText(depth - 1)},"kind":"let","name":"${names[Math.floor(random() * names.length)]!}","value":${exprText(depth - 2)}}`; + } +} + +function evaluate(e: Expr, env: Map): number { + switch (e.kind) { + case "num": + return e.value; + case "var": + return env.get(e.name) ?? 1; + case "neg": + return -evaluate(e.operand, env); + case "add": + return (evaluate(e.left, env) + evaluate(e.right, env)) % 1000003; + case "sub": + return (evaluate(e.left, env) - evaluate(e.right, env)) % 1000003; + case "mul": + return (evaluate(e.left, env) * evaluate(e.right, env)) % 1000003; + case "max": { + let best = -Infinity; + for (const item of e.items) best = Math.max(best, evaluate(item, env)); + return best; + } + case "if": + return evaluate(e.test, env) > 0 ? evaluate(e.then, env) : evaluate(e.else, env); + case "let": { + const inner = new Map(env); + inner.set(e.name, evaluate(e.value, env)); + return evaluate(e.body, inner); + } + } +} + +const rounds = Number(process.argv[2] ?? "6"); +const eventsText = `[${Array.from({ length: 60000 }, (_, i) => eventText(i)).join(",")}]`; +const treesText = `[${Array.from({ length: 400 }, () => exprText(9)).join(",")}]`; +let clicks = 0; +let keys = 0; +let scrolled = 0; +let area = 0; +let loadMs = 0; +let errors = 0; +let focus = 0; +let total = 0; +for (let round = 0; round < rounds; round++) { + const events = JSON.parse(eventsText) as Event[]; + for (const event of events) { + switch (event.kind) { + case "click": + clicks += event.x + event.y; + break; + case "key": + if (event.repeat) keys++; + break; + case "scroll": + scrolled += event.offset; + break; + case "resize": + area += (event.width * event.height) % 1000; + break; + case "focus": + case "blur": + focus += event.target.length; + break; + case "load": + loadMs += event.ms; + break; + case "error": + errors += event.code % 7; + break; + } + } + const trees = JSON.parse(treesText) as Expr[]; + const env = new Map([ + ["a", 3], + ["b", 5], + ]); + for (const tree of trees) total = (total + evaluate(tree, env)) % 1000003; +} +console.log(clicks, keys, scrolled, area, loadMs, errors, focus, total); diff --git a/benchmarks/runtime/workloads/keyed-option-writes.json b/benchmarks/runtime/workloads/keyed-option-writes.json new file mode 100644 index 000000000..802911e7d --- /dev/null +++ b/benchmarks/runtime/workloads/keyed-option-writes.json @@ -0,0 +1,6 @@ +{ + "entry": "workloads/keyed-option-writes.ts", + "args": [ + "4" + ] +} diff --git a/benchmarks/runtime/workloads/keyed-option-writes.ts b/benchmarks/runtime/workloads/keyed-option-writes.ts new file mode 100644 index 000000000..95cdbb86b --- /dev/null +++ b/benchmarks/runtime/workloads/keyed-option-writes.ts @@ -0,0 +1,76 @@ +// Class methods whose names are also keys of option records written through +// element accesses: `enabled[name] = true` over a union of literal names and +// `totals["count"] = n`. The writes store booleans and numbers into plain +// records, so the methods keep their native calls. +type Step = "scale" | "offset" | "clamp"; + +const enabled: Record = { scale: false, offset: false, clamp: false }; +function enable(name: Step): void { + enabled[name] = true; +} + +const totals: Record = {}; + +class Transform { + private readonly factor: number; + private readonly shift: number; + constructor(factor: number, shift: number) { + this.factor = factor; + this.shift = shift; + } + scale(x: number): number { + return enabled.scale ? x * this.factor : x; + } + offset(x: number): number { + return enabled.offset ? x + this.shift : x; + } + clamp(x: number): number { + if (!enabled.clamp) return x; + return x < 0 ? 0 : x > 1000 ? 1000 : x; + } +} + +class Counter { + private seen = 0; + private sum = 0; + add(value: number): void { + this.seen++; + this.sum += value; + } + count(): number { + return this.seen; + } + mean(): number { + return this.seen === 0 ? 0 : this.sum / this.seen; + } +} + +const scale = Number(process.argv[2] ?? "1"); +const steps: Step[] = ["scale", "offset", "clamp"]; +for (const step of steps) enable(step); + +const transforms: Transform[] = []; +for (let i = 0; i < 16; i++) transforms.push(new Transform(1 + (i % 5) * 0.25, (i % 7) - 3)); +const counters: Counter[] = []; +for (let i = 0; i < 8; i++) counters.push(new Counter()); + +let seed = 12345; +let checksum = 0; +const rounds = Math.floor(2_000_000 * scale); +for (let i = 0; i < rounds; i++) { + seed = (Math.imul(seed, 1103515245) + 12345) >>> 0; + const t = transforms[seed & 15]!; + const value = t.clamp(t.offset(t.scale((seed >>> 8) % 1200))); + const counter = counters[(seed >>> 4) & 7]!; + counter.add(value); + checksum += value; + if ((i & 65535) === 0) totals["count"] = counter.count(); +} + +let mean = 0; +let seen = 0; +for (const counter of counters) { + mean += counter.mean(); + seen += counter.count(); +} +console.log("seen", seen, "count", totals["count"], "mean", (mean / counters.length).toFixed(4), "checksum", checksum); diff --git a/benchmarks/runtime/workloads/line-reader.json b/benchmarks/runtime/workloads/line-reader.json new file mode 100644 index 000000000..85194f70c --- /dev/null +++ b/benchmarks/runtime/workloads/line-reader.json @@ -0,0 +1,7 @@ +{ + "entry": "workloads/line-reader.ts", + "input": "line-reader", + "args": [ + "{input}" + ] +} diff --git a/benchmarks/runtime/workloads/line-reader.ts b/benchmarks/runtime/workloads/line-reader.ts new file mode 100644 index 000000000..cbdf384bb --- /dev/null +++ b/benchmarks/runtime/workloads/line-reader.ts @@ -0,0 +1,34 @@ +// Line-by-line processing of a log file with readline over a file stream: +// 'line' events for about 67 MB of access-log lines, with light work per +// line (a status count, a path tally, byte totals). +import { createReadStream } from "node:fs"; +import { createInterface } from "node:readline"; + +const path = process.argv[2]!; +const statuses = new Map(); +const paths = new Map(); +let lines = 0; +let chars = 0; +let bytes = 0; +let longest = 0; + +const input = createInterface({ input: createReadStream(path), crlfDelay: Infinity }); +input.on("line", (line: string) => { + lines++; + chars += line.length; + if (line.length > longest) longest = line.length; + const quote = line.indexOf('"'); + const space = line.indexOf(" ", quote); + const path = line.slice(space + 1, line.indexOf("/", line.indexOf("/", space) + 1)); + paths.set(path, (paths.get(path) ?? 0) + 1); + const close = line.indexOf('"', space); + const status = line.slice(close + 2, close + 5); + statuses.set(status, (statuses.get(status) ?? 0) + 1); + bytes += Number(line.slice(close + 6, line.indexOf(" ", close + 6))); +}); +input.on("close", () => { + console.log(`${lines} lines, ${chars} characters, longest ${longest}`); + console.log(`bytes ${bytes}`); + for (const status of [...statuses.keys()].sort()) console.log(`status ${status}: ${statuses.get(status)}`); + for (const prefix of [...paths.keys()].sort()) console.log(`path ${prefix}: ${paths.get(prefix)}`); +}); diff --git a/benchmarks/runtime/workloads/regex-replacers.json b/benchmarks/runtime/workloads/regex-replacers.json new file mode 100644 index 000000000..c01c6c272 --- /dev/null +++ b/benchmarks/runtime/workloads/regex-replacers.json @@ -0,0 +1,6 @@ +{ + "entry": "workloads/regex-replacers.ts", + "args": [ + "1" + ] +} diff --git a/benchmarks/runtime/workloads/regex-replacers.ts b/benchmarks/runtime/workloads/regex-replacers.ts new file mode 100644 index 000000000..be3820b89 --- /dev/null +++ b/benchmarks/runtime/workloads/regex-replacers.ts @@ -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(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 = { + 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); diff --git a/benchmarks/runtime/workloads/string-iteration.json b/benchmarks/runtime/workloads/string-iteration.json new file mode 100644 index 000000000..21456bd23 --- /dev/null +++ b/benchmarks/runtime/workloads/string-iteration.json @@ -0,0 +1,6 @@ +{ + "entry": "workloads/string-iteration.ts", + "args": [ + "1" + ] +} diff --git a/benchmarks/runtime/workloads/string-iteration.ts b/benchmarks/runtime/workloads/string-iteration.ts new file mode 100644 index 000000000..2abebd635 --- /dev/null +++ b/benchmarks/runtime/workloads/string-iteration.ts @@ -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(); +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); diff --git a/benchmarks/runtime/workloads/throw-chains.json b/benchmarks/runtime/workloads/throw-chains.json new file mode 100644 index 000000000..79cf5ea87 --- /dev/null +++ b/benchmarks/runtime/workloads/throw-chains.json @@ -0,0 +1,6 @@ +{ + "entry": "workloads/throw-chains.ts", + "args": [ + "1" + ] +} diff --git a/benchmarks/runtime/workloads/throw-chains.ts b/benchmarks/runtime/workloads/throw-chains.ts new file mode 100644 index 000000000..c96816aed --- /dev/null +++ b/benchmarks/runtime/workloads/throw-chains.ts @@ -0,0 +1,196 @@ +// Exceptions that cross many frames: throws from helpers below a recursion, +// caught 2, 5 and 20 recursive calls up (direct calls, then calls through a +// callback), with each frame holding an array across its call, and a +// backtracking parser that uses exceptions for control flow. +class ParseError extends Error { + readonly at: number; + constructor(at: number, message: string) { + super(message); + this.at = at; + } +} + +/** A thrown value that is not an Error (no stack to capture). */ +class Stop { + readonly code: number; + constructor(code: number) { + this.code = code; + } +} + +const held: number[][] = []; +function fail(n: number, plain: boolean): number { + if (plain) throw new Stop(n); + throw new RangeError("chain " + n); +} +function check(n: number, plain: boolean): number { + return (n & 3) === 0 ? fail(n, plain) : n; +} +function validate(n: number, plain: boolean): number { + return check(n, plain) + (n & 1); +} +function bottom(n: number, plain: boolean): number { + const box = [n, -1]; + const r = validate(n, plain); + held[0] = box; + return r + box.length; +} +function walk(n: number, depth: number, plain: boolean): number { + const box = [n, depth]; + const r = depth === 0 ? bottom(n, plain) : walk(n, depth - 1, plain); + held[depth] = box; + return r + box.length; +} + +function chains(rounds: number): string { + const out: string[] = []; + for (const depth of [2, 5, 20]) + for (const plain of [false, true]) { + let caught = 0; + let sum = 0; + for (let i = 0; i < rounds; i++) { + try { + sum += walk(i, depth - 2, plain); + } catch (error) { + caught++; + sum += error instanceof Stop ? error.code & 7 : 1; + } + } + out.push(`depth ${depth} ${plain ? "object" : "error"}: caught ${caught} sum ${sum}`); + } + return out.join("\n"); +} + +// The same chains through a callback: each level visits the next with a +// closure, as tree visitors do. +function visit(n: number, depth: number, each: (n: number, depth: number) => number): number { + const box = [n, depth]; + const r = depth === 0 ? bottom(n, false) : each(n, depth - 1); + held[depth] = box; + return r + box.length; +} +function visits(rounds: number): string { + const out: string[] = []; + const each = (n: number, depth: number): number => visit(n, depth, each); + for (const depth of [2, 5, 20]) { + let caught = 0; + let sum = 0; + for (let i = 0; i < rounds; i++) { + try { + sum += visit(i, depth - 2, each); + } catch (error) { + caught++; + sum += error instanceof RangeError ? 1 : 0; + } + } + out.push(`visit depth ${depth}: caught ${caught} sum ${sum}`); + } + return out.join("\n"); +} + +// expr := term (("+" | "-") term)*; term := atom ("*" atom)*; +// atom := number | "(" expr ")" | name "(" expr ("," expr)* ")" | name. +// A call and a name start alike: the parser tries the call first and +// backtracks when it fails. +let text = ""; +let pos = 0; +function peek(): string { + return pos < text.length ? text[pos]! : ""; +} +function expect(c: string): void { + if (peek() !== c) throw new ParseError(pos, `expected ${c}`); + pos++; +} +function name(): string { + const start = pos; + while (pos < text.length && text.charCodeAt(pos) >= 97 && text.charCodeAt(pos) <= 122) pos++; + if (pos === start) throw new ParseError(pos, "expected a name"); + return text.slice(start, pos); +} +function call(): number { + const callee = name(); + expect("("); + let value = expr() + callee.length; + while (peek() === ",") { + pos++; + value += expr(); + } + expect(")"); + return value; +} +function atom(): number { + const c = peek(); + if (c >= "0" && c <= "9") { + let value = 0; + while (peek() >= "0" && peek() <= "9") value = value * 10 + (text.charCodeAt(pos++) - 48); + return value; + } + if (c === "(") { + pos++; + const value = expr(); + expect(")"); + return value; + } + const start = pos; + try { + return call(); + } catch (error) { + if (!(error instanceof ParseError)) throw error; + pos = start; + return name().length; + } +} +function term(): number { + let value = atom(); + while (peek() === "*") { + pos++; + value = (value * atom()) % 1000003; + } + return value; +} +function expr(): number { + let value = term(); + while (peek() === "+" || peek() === "-") { + const op = text[pos++]; + value = op === "+" ? value + term() : value - term(); + } + return value; +} + +let seed = 4242; +function random(): number { + seed = (Math.imul(seed, 1103515245) + 12345) >>> 0; + return seed / 4294967296; +} +function source(depth: number): string { + const r = random(); + if (depth > 6 || r < 0.25) return String(Math.floor(random() * 100)); + if (r < 0.45) return "abcdefgh".slice(0, 1 + Math.floor(random() * 8)); + if (r < 0.6) return `(${source(depth + 1)})`; + if (r < 0.8) return `${source(depth + 1)}${random() < 0.5 ? "+" : "*"}${source(depth + 1)}`; + return `f${"xyz".slice(0, Math.floor(random() * 3))}(${source(depth + 1)},${source(depth + 1)})`; +} + +function parse(count: number): string { + let total = 0; + let failed = 0; + for (let i = 0; i < count; i++) { + // Every tenth input lacks its last character, which fails deep inside it. + const full = source(0); + text = i % 10 === 0 ? full.slice(0, -1) : full; + pos = 0; + try { + total = (total + expr()) % 1000003; + if (pos !== text.length) throw new ParseError(pos, "trailing input"); + } catch (error) { + if (!(error instanceof ParseError)) throw error; + failed++; + } + } + return `parsed ${count}: total ${total} failed ${failed}`; +} + +const scale = Number(process.argv[2] ?? "1"); +console.log(chains(Math.floor(60000 * scale))); +console.log(visits(Math.floor(60000 * scale))); +console.log(parse(Math.floor(40000 * scale))); diff --git a/benchmarks/runtime/workloads/union-node-sets.json b/benchmarks/runtime/workloads/union-node-sets.json new file mode 100644 index 000000000..98e9fe2e5 --- /dev/null +++ b/benchmarks/runtime/workloads/union-node-sets.json @@ -0,0 +1,6 @@ +{ + "entry": "workloads/union-node-sets.ts", + "args": [ + "1" + ] +} diff --git a/benchmarks/runtime/workloads/union-node-sets.ts b/benchmarks/runtime/workloads/union-node-sets.ts new file mode 100644 index 000000000..16d6f4dfe --- /dev/null +++ b/benchmarks/runtime/workloads/union-node-sets.ts @@ -0,0 +1,141 @@ +// Syntax-tree passes over a discriminated union of object literals, the way +// compilers walk their IR: a DAG of shared subexpressions, a `Set` of +// visited nodes, a `Map` from node to its value, and conversions between +// the whole union and the sub-unions passes narrow it to. +type Num = { kind: "num"; value: number }; +type Var = { kind: "var"; slot: number }; +type Neg = { kind: "neg"; operand: Node }; +type Add = { kind: "add"; left: Node; right: Node }; +type Mul = { kind: "mul"; left: Node; right: Node }; +type Pick = { kind: "pick"; cond: Node; then: Node; else_: Node }; +type Leaf = Num | Var; +type Node = Leaf | Neg | Add | Mul | Pick; + +function children(node: Node, visit: (child: Node) => void): void { + switch (node.kind) { + case "num": + case "var": + return; + case "neg": + visit(node.operand); + return; + case "add": + case "mul": + visit(node.left); + visit(node.right); + return; + case "pick": + visit(node.cond); + visit(node.then); + visit(node.else_); + return; + } +} + +function leafValue(leaf: Leaf, env: number[]): number { + return leaf.kind === "num" ? leaf.value : env[leaf.slot]!; +} + +function evaluate(node: Node, env: number[], memo: Map): number { + const cached = memo.get(node); + if (cached !== undefined) return cached; + let result: number; + switch (node.kind) { + case "num": + case "var": + result = leafValue(node, env); + break; + case "neg": + result = -evaluate(node.operand, env, memo); + break; + case "add": + result = (evaluate(node.left, env, memo) + evaluate(node.right, env, memo)) % 1000003; + break; + case "mul": + result = (evaluate(node.left, env, memo) * evaluate(node.right, env, memo)) % 1000003; + break; + case "pick": + result = + evaluate(node.cond, env, memo) % 2 === 0 + ? evaluate(node.then, env, memo) + : evaluate(node.else_, env, memo); + break; + } + memo.set(node, result); + return result; +} + +function reachable(root: Node): number { + const seen = new Set(); + const stack: Node[] = [root]; + while (stack.length > 0) { + const node = stack.pop()!; + if (seen.has(node)) continue; + seen.add(node); + children(node, (child) => { + if (!seen.has(child)) stack.push(child); + }); + } + return seen.size; +} + +function leaves(root: Node): Leaf[] { + const out: Leaf[] = []; + const seen = new Set(); + const walk = (node: Node): void => { + if (seen.has(node)) return; + seen.add(node); + if (node.kind === "num" || node.kind === "var") out.push(node); + else children(node, walk); + }; + walk(root); + return out; +} + +let seed = 2463534242; +function next(): number { + seed ^= seed << 13; + seed ^= seed >>> 17; + seed ^= seed << 5; + return seed >>> 0; +} + +function build(size: number): Node { + const pool: Node[] = []; + for (let i = 0; i < 64; i++) + pool.push(i % 2 === 0 ? { kind: "num", value: i * 7 + 1 } : { kind: "var", slot: i % 16 }); + // Operands come from the most recent nodes, so most of the DAG is + // reachable from the root and many nodes are shared. + const recent = (): Node => pool[pool.length - 1 - (next() % 48)]!; + for (let i = 0; i < size; i++) { + const a = recent(); + const b = recent(); + const r = next() % 10; + let node: Node; + if (r < 1) node = { kind: "neg", operand: a }; + else if (r < 5) node = { kind: "add", left: a, right: b }; + else if (r < 8) node = { kind: "mul", left: a, right: b }; + else node = { kind: "pick", cond: a, then: b, else_: recent() }; + pool.push(node); + } + return pool[pool.length - 1]!; +} + +const scale = Number(process.argv[2] ?? "1"); +const rounds = Math.max(1, Math.round(40 * scale)); +let checksum = 0; +let nodes = 0; +let leafCount = 0; +for (let round = 0; round < rounds; round++) { + const root = build(20_000); + nodes += reachable(root); + const found = leaves(root); + leafCount += found.length; + for (let pass = 0; pass < 4; pass++) { + const env: number[] = []; + for (let i = 0; i < 16; i++) env.push((round * 31 + pass * 7 + i) % 97); + checksum = (checksum + evaluate(root, env, new Map())) % 1000003; + for (const leaf of found) checksum = (checksum + leafValue(leaf, env)) % 1000003; + } +} +console.log("rounds", rounds, "nodes", nodes, "leaves", leafCount, "checksum", checksum); diff --git a/docs/content/docs/cli.mdx b/docs/content/docs/cli.mdx index 8b0353620..0a82775ca 100644 --- a/docs/content/docs/cli.mdx +++ b/docs/content/docs/cli.mdx @@ -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 [--range ] [--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 [--sparse] @@ -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. + +
+
scriptc types patch <package> <selector> <type>
+
Finds the package installed for the working directory, prints every declaration the selector matches in its declaration files, its JSDoc and its @types/ package with the type before and after, and adds the patch to the nearest package.json. The patch joins an entry whose range covers the installed version, or gets the range ^<installed version>. Nothing is written when the selector matches nothing. --range <range> chooses the range, and --dry-run only prints.
+
scriptc types list
+
Prints each type patch in the nearest package.json, whether it applies to the installed version, and what it matches.
+
+ +```console +$ scriptc types list +commander@^15.0.0 + HelpContext.helpWidth: | undefined + node_modules/commander/lib/command.js:2481 number -> number | undefined +``` + ## scriptc cache warm Verifies and loads the installed precompiled runtime objects and vendored libraries, and prints the cache directory. Pass `runtime`, `tls`, or both to select which to warm. Ordinary builds don't need this step. With `--sanitize`, it builds the instrumented runtime objects using a C toolchain. @@ -135,11 +160,17 @@ With `--sparse`, the profile leaves out the functions the runs never reached. Th
--detached-exit
On Linux, when the program calls process.exit(), hand the release of its memory to a helper process so the parent sees the exit sooner. The kernel otherwise frees the whole address space before the exit is reported, which takes tens of milliseconds per gigabyte. Opt in only where the trade-offs are acceptable: for up to about 100 ms after the exit, the helper keeps the executable mapped (overwriting the binary in place fails with "text file busy"), its memory stays charged to the cgroup, and the teardown CPU time no longer appears in the parent's resource usage. Valid only for executable builds; it has no effect on other platforms.
+
--target-cpu <baseline|x86-64-v2|x86-64-v3|x86-64-v4|native>
+
On x86-64 targets, compile the program's code for an x86-64 microarchitecture level. The default, baseline, 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. native uses the highest level the build machine supports. Valid for executable, assembly, and object output on x86-64 targets. See Target CPU.
+ +
--cpu-dispatch
+
On x86-64 targets, compile the program's code twice, for the baseline and for x86-64-v3, and choose the copy when the program starts. The executable runs on any x86-64 CPU and uses the x86-64-v3 code on CPUs that support it. Not used with a --target-cpu level above baseline. Valid only for executable builds. See Runtime CPU dispatch.
+
--profile-generate <pattern>
On Linux, build an instrumented executable for profile-guided optimization. Each run writes a raw profile to the path pattern, where %p expands to the process ID; the LLVM_PROFILE_FILE 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 --optimization=release or speed.
--profile-use <file.profdata>
-
Optimize the executable with a profile from scriptc merge-profiles: 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 --optimization=release or speed.
+
Optimize the executable with a profile from scriptc merge-profiles: 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. scriptc build prints how the profile matched the program's functions on stderr; see Profile-guided optimization. Valid only for executable builds with --optimization=release or speed.
--npm-static <pkg[,pkg…]|auto>
Name npm packages to compile even if they don't ship their own type declarations (repeatable). A trailing ..., as in fs-extra..., also names every package that package depends on. auto uses the default automatic selection. Packages that can't compile produce diagnostics. See npm Dependencies.
@@ -207,6 +238,9 @@ With `--sparse`, the profile leaves out the functions the runs never reached. Th
SCRIPTC_CHECKER
The TypeScript 7 API server that checks the program: tsc-ts (the default), which scriptc includes, or tsgo, from the typescript package scriptc also includes. Both give the same diagnostics and generated code.
+ +
SCRIPTC_CHECKER_SERVER
+
Set to 1 on Linux and macOS to check with a tsc-ts server that later builds share. It takes effect with a tsc-ts release that serves tsc-ts --api --listen. The tsc-ts 0.0.9 that this version of scriptc includes does not, so each build checks with its own tsc-ts process, as without the variable. A build connects to the server for its working directory, starting it when none answers, and the server keeps the files it parsed, so the next build of the same program loads it faster. A build that finds the server in use by another build, or cannot start or reach it, checks with its own tsc-ts process. The server exits after 5 minutes without a build, or after a build when its resident memory is above 2048 MB (TSC_TS_LISTEN_MEMORY sets another limit in megabytes). Diagnostics and generated code are the same with and without it. It applies to the default checker, tsc-ts.
```console @@ -228,6 +262,78 @@ $ scriptc build app.ts --optimization=speed --profile-use app.profdata Train on workloads like the ones the program will run: code the training never reached is optimized as without a profile. The profile can be committed and reused as the program changes. Functions whose control flow changed since the profile was collected lose their profile, and the rest keep theirs, so regenerate it when much of the program has changed. [Build parallelism](/docs/how-it-works#build-parallelism) describes what a profile adds to build time. +`scriptc build --profile-use` prints one line on stderr with how the profile matched the program's functions: + +``` +scriptc: profile app.profdata: 9170 functions annotated, 0 mismatched, 28636 without a record +``` + +- **annotated:** functions optimized with the profile's counts. +- **mismatched:** functions whose control flow changed since the profile was collected. They are optimized without counts. +- **without a record:** functions the profile does not describe: new functions, and functions the profiled runs never reached. +- When the build reuses cached partitions whose counts were not recorded, the line ends with the number of those partitions. + +When more than 10% of the functions the profile describes (annotated and mismatched) are mismatched, the build also prints a warning: + +``` +scriptc: warning: 3739 of the 8947 functions app.profdata describes (41.8%) changed since it was collected and are optimized without their counts; collect the profile again +``` + +The build still succeeds. Collecting the profile again with the current program and scriptc version restores the counts. + +## Target CPU + +By default, x86-64 executables run on any x86-64 CPU, so the compiler uses only the baseline instruction set (SSE2). `--target-cpu` compiles the program's code for a higher x86-64 microarchitecture level. Each level includes the ones below it: + + + + + + + + + + + + + + + + + + + + + + +
LevelAdds
x86-64-v2SSE3, SSSE3, SSE4.1, SSE4.2, POPCNT
x86-64-v3AVX, AVX2, BMI1, BMI2, F16C, FMA, LZCNT, MOVBE
x86-64-v4AVX-512 (F, BW, CD, DQ, VL)
+ +```console +$ scriptc build app.ts --target-cpu=x86-64-v3 -o app +``` + +Loops over typed arrays and numbers gain the most, because LLVM vectorizes them with wider registers. Some loops run slower at a higher level, so measure the program on the machines it will run on. + +An executable built for a level runs only on CPUs that support that level. On an older CPU, it crashes with an illegal-instruction signal (`SIGILL`) when it reaches an instruction the CPU lacks. A named level is portable to every machine of that level. `native` selects the highest level the build machine supports, so use it only for executables that run on the machine that built them or on identical machines. + +The level applies to the program's own code. The runtime pack keeps its baseline code. Builds for different levels never share cached artifacts, and `baseline` builds are the same as builds without the option. + +### Runtime CPU dispatch + +`--cpu-dispatch` keeps the executable portable and still uses the higher level where the CPU has it. The program's functions are compiled for the baseline and for `x86-64-v3`. At startup, the executable checks the CPU and the operating system's register support, and then runs the `x86-64-v3` code on CPUs that support every feature of that level. Calls between the program's functions stay within the chosen copy. + +```console +$ scriptc build app.ts --optimization=speed --cpu-dispatch -o app +``` + +The second copy makes the executable larger and the first build of the program slower, since LLVM compiles the program's code twice. Unchanged rebuilds are not affected. Both copies compute the same results: neither contracts floating-point operations into fused multiply-adds. + +The `SCRIPTC_CPU_LEVEL` environment variable caps the level the executable uses, for example to test the baseline code on a newer machine. It takes a level from `0` to `4`, `baseline`, or a level name such as `x86-64-v3`; any other value selects the baseline code. + +```console +$ SCRIPTC_CPU_LEVEL=baseline ./app +``` + ## LLVM backend scriptc generates LLVM IR and compiles it to native code. Executable builds link the result with the matching precompiled runtime pack. Use --emit=llvm to inspect the generated code. diff --git a/docs/content/docs/contracts.mdx b/docs/content/docs/contracts.mdx index 948fe7960..ad5953fa6 100644 --- a/docs/content/docs/contracts.mdx +++ b/docs/content/docs/contracts.mdx @@ -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 diff --git a/docs/content/docs/dependencies.mdx b/docs/content/docs/dependencies.mdx index a9c5ae138..2288aeb83 100644 --- a/docs/content/docs/dependencies.mdx +++ b/docs/content/docs/dependencies.mdx @@ -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: + +
+
`"| undefined"`
+
A type that starts with `|` widens the declared type, here to `number | undefined`.
+
`"string | null"`
+
Any other type replaces the declared type.
+
+ +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//`, 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. diff --git a/docs/content/docs/how-it-works.mdx b/docs/content/docs/how-it-works.mdx index f1b64948d..a6520b46e 100644 --- a/docs/content/docs/how-it-works.mdx +++ b/docs/content/docs/how-it-works.mdx @@ -42,28 +42,27 @@ Parsing, type checking, lowering, and LLVM IR generation run one after another o ### Partitions -The LLVM helper divides an executable's LLVM IR into partitions by its size: one partition per 4 MiB of LLVM text, rounded up to a power of two, and at most 64. `--emit=llvm` writes this text, so its file size shows how many partitions a program gets. +The LLVM helper divides an executable's LLVM IR into partitions by its size. Up to 1 MiB of LLVM text is one partition, and up to 2 MiB is four. Larger programs get one partition per 4 MiB of LLVM text, rounded up to a power of two, and at least 8 and at most 64. `--emit=llvm` writes this text, so its file size shows how many partitions a program gets. - - - - + + +
LLVM textPartitions
Up to 4 MiB1
Over 4 MiB, up to 8 MiB2
Over 8 MiB, up to 16 MiB4
Over 16 MiB, up to 32 MiB8
Up to 1 MiB1
Over 1 MiB, up to 2 MiB4
Over 2 MiB, up to 32 MiB8
Over 32 MiB, up to 64 MiB16
Over 64 MiB, up to 128 MiB32
Over 128 MiB64
-The count depends only on the size of the LLVM text, so a program has the same partitions, and builds the same executable, on every host. A function's partition follows from its name, and a small internal function goes with the one function that calls it, so an edit leaves the other functions in their partitions. +The count depends only on the size of the LLVM text, so a program has the same partitions, and builds the same executable, on every host. In programs with 32 partitions or more, functions from the same source file are placed in the same partition, so that their calls to each other stay inside it; a file larger than 1 MiB of LLVM text or than an average partition is divided into parts by function name. Other functions, and the functions of smaller programs, are placed by their name, and a small internal function goes with the one function that calls it. An edit leaves almost every other function in its partition. These builds are not divided into partitions and compile on one thread: `--emit=asm`, `--emit=obj`, and `--print=native-link-info` output, library builds (`--lib`; `dev` library builds are compiled in pieces, one after another), and `--sanitize` builds, which compile the LLVM IR with the C compiler. -The helper compiles partitions on a pool with one thread per hardware thread of the host, so an executable with two partitions uses at most two cores in the backend. A program of one partition is optimized and compiled on one thread. +The helper compiles partitions on a pool with one thread per hardware thread of the host, so an executable with four partitions uses at most four cores in the backend. A program of one partition is optimized and compiled on one thread. How partitions are compiled depends on `--optimization`: diff --git a/docs/content/docs/limitations.mdx b/docs/content/docs/limitations.mdx index a86ed217e..0e7afa2b1 100644 --- a/docs/content/docs/limitations.mdx +++ b/docs/content/docs/limitations.mdx @@ -52,6 +52,8 @@ title: "Limitations" ../limitations/types-and-class-definitions/static-field-updates.mdx +../limitations/types-and-class-definitions/array-subclasses.mdx + ../limitations/types-and-class-definitions/local-class-expressions.mdx ../limitations/types-and-class-definitions/constructor-function-inheritance.mdx @@ -220,6 +222,14 @@ title: "Limitations" ../limitations/tls-ciphers.mdx +### TLS certificates + +../limitations/tls-certificates.mdx + +### TLS sessions + +../limitations/tls-sessions.mdx + ### Console and process streams ../limitations/console-and-process-streams.mdx diff --git a/docs/content/limitations/buffer-construction.mdx b/docs/content/limitations/buffer-construction.mdx index aca0c74ca..c88e01336 100644 --- a/docs/content/limitations/buffer-construction.mdx +++ b/docs/content/limitations/buffer-construction.mdx @@ -1,3 +1,8 @@ `Buffer.from` accepts untyped strings, byte arrays, arrays, and plain array-like or Buffer JSON objects. Encoding arguments must be supported literals. +`Buffer.from(arrayBuffer, byteOffset, length)` is a view of the ArrayBuffer or SharedArrayBuffer, with Node's offset and length checks. Custom input `valueOf` hooks and opaque class or native-handle inputs throw an unsupported-operation error. +The deprecated `Buffer()` and `new Buffer()` take up to three arguments, without spreads, and follow Node: the first call made outside `node_modules` reports DEP0005 on the next tick, a number allocates zero-filled bytes, and other arguments convert as `Buffer.from` does. +In JavaScript, `Buffer` read as a value (the global or the buffer module's `Buffer`) is one function with Node's statics and `poolSize`. +Calling or constructing it through a stored value, reading its `prototype`, and its `copyBytesFrom` and `of` statics throw an unsupported-operation error. +`Buffer.prototype` members read as values are functions that run on Buffer receivers; inherited and symbol-keyed members throw an unsupported-operation error. diff --git a/docs/content/limitations/builtin-apis/assertions.mdx b/docs/content/limitations/builtin-apis/assertions.mdx index f7f5c9354..381fc3802 100644 --- a/docs/content/limitations/builtin-apis/assertions.mdx +++ b/docs/content/limitations/builtin-apis/assertions.mdx @@ -3,6 +3,8 @@ When `strictEqual` compares function values, `actual` and `expected` read as `undefined`; when `assert.throws` or `assert.rejects` reports an error that is not an instance of the expected class, or that does not match an expected object, `expected` reads as `undefined`. Values whose type can be cyclic, such as recursive records, also read as `undefined`. `assert.AssertionError` used as a value outside `instanceof` throws `SC2020`. + The comparison methods, `ok`, `fail` and `ifError` are function values that assert as the methods do; called through a stored value, `ok` without a message reports `value == true`, where Node quotes the call, and `fail` takes at most one argument. + `throws`, `rejects`, `doesNotThrow`, `doesNotReject`, `match` and `doesNotMatch` as values throw `SC2020`. `assert.deepEqual` and `assert.notDeepEqual` follow Node's loose comparison, including Maps, Sets, Dates, regular expressions, typed arrays, and errors; operands whose type can be cyclic or holds functions inside a structure are refused. `assert.partialDeepStrictEqual` follows Node's partial comparison over the same values, with the same refusals. Checked-dynamic values compared deeply have no cycle detection. diff --git a/docs/content/limitations/builtin-apis/asynclocalstorage.mdx b/docs/content/limitations/builtin-apis/asynclocalstorage.mdx index 341994faa..ce77d57f5 100644 --- a/docs/content/limitations/builtin-apis/asynclocalstorage.mdx +++ b/docs/content/limitations/builtin-apis/asynclocalstorage.mdx @@ -1,2 +1,12 @@ - **AsyncLocalStorage:** timer, interval, `setImmediate`, `process.nextTick` and `queueMicrotask` callbacks run with the stores that were active when they were scheduled, as in Node. - Completion callbacks of callback-style I/O APIs, such as `fs.readFile` and `crypto.randomBytes`, run with no store, where Node runs them with the stores that were active when the operation started. + The completion callbacks of `fs`, `crypto` and `zlib` callback APIs, `dns.lookup`, `child_process.execFile`, `server.listen`, `net.connect`, `http.request`, `http.get` and `dgram` run with the stores that were active at the call, as in Node. + Events of `net`, `tls`, `http` and `https` sockets, servers, requests and responses run with the stores Node runs them with. + A socket's or client request's events see the context of the call that created it, and a server's events and its requests' and responses' events see the context of its `listen()` call. + Socket `write` and `end` callbacks, `'finish'` and `server.close(callback)` see the context of their call. + Stream events and callbacks run with the stores of the code that triggers them, as in Node. + Stream steps that Node defers with `process.nextTick` see the context that scheduled them, and `stream.finished` and `stream.pipeline` callbacks see the context of their call. + `fs` read and write streams and `zlib` streams run their events and callbacks in the context that created them. + Events of `child_process` children, their standard streams and their IPC channels see the context of the `spawn()` or `fork()` call; stdin's `'finish'` and `send()` callbacks see the context of their call. + A client `http2` session's events see the context of its `http2.connect()` call, and a client stream's events the context of its `request()` call. + An `http2` server's `'session'` and `'stream'` events and the compatibility API's request handler see the context of its `listen()` call; a server stream's own events, and its compatibility request's and response's, run with no store, as in Node. + `AsyncResource` has no lowering yet. diff --git a/docs/content/limitations/builtin-apis/compression.mdx b/docs/content/limitations/builtin-apis/compression.mdx index 36cec0bc4..83b0fd683 100644 --- a/docs/content/limitations/builtin-apis/compression.mdx +++ b/docs/content/limitations/builtin-apis/compression.mdx @@ -4,4 +4,5 @@ The `Deflate`, `Inflate`, `Gzip`, `Gunzip`, `DeflateRaw`, `InflateRaw`, and `Unzip` streams, from `createGzip()` and the other factories or `new zlib.Gzip()`, accept Node's options and support writes, piping, `pipeline`, async iteration, `flush`, `params`, `close`, `reset`, and `bytesWritten`. Each output chunk is its own Buffer rather than a slice of a shared chunk-size buffer. `zlib.constants` members and their module-level copies read as Node's values. - `util.inspect`, `instanceof zlib.Gzip`, and Brotli and Zstd compression are unsupported. + The Brotli one-shots `brotliCompress` and `brotliDecompress` and their `Sync` forms, and the `BrotliCompress` and `BrotliDecompress` streams, accept Node's options, including `params` and `dictionary`, and produce the same bytes as Node. + `util.inspect` and Zstd compression are unsupported. diff --git a/docs/content/limitations/builtin-apis/crypto.mdx b/docs/content/limitations/builtin-apis/crypto.mdx index e4b555806..e314fc386 100644 --- a/docs/content/limitations/builtin-apis/crypto.mdx +++ b/docs/content/limitations/builtin-apis/crypto.mdx @@ -27,20 +27,29 @@ A call on a DiffieHellman value whose arguments no @types/node overload accepts, or whose output encoding is typed `any`, is refused at compile time, as are its `Object.prototype` methods; other property reads, and property writes, throw a coded SC2020 error. `Object.keys()` and `in` do not see a DiffieHellman's own `verifyError` property, and `JSON.stringify()` and `instanceof` are refused. After `createDiffieHellman(primeLength)` throws `Invalid DH parameters`, the next OpenSSL error Node throws can list the failed generation's errors in `opensslErrorStack`; errors in a compiled binary list only their own. - `crypto.diffieHellman`, which takes DH, EC, or X25519 KeyObjects, and the `DiffieHellman` and `DiffieHellmanGroup` constructors are not available. + `crypto.diffieHellman` computes ECDH and X25519 secrets from EC and X25519 keys, given as KeyObjects or in any form `createPrivateKey` and `createPublicKey` read, with Node's errors; DH keys have no KeyObjects here, and the `DiffieHellman` and `DiffieHellmanGroup` constructors are not available. `checkPrime`, `checkPrimeSync`, `generatePrime`, and `generatePrimeSync` support bigint, ArrayBuffer, typed-array, Buffer, and DataView values and the `checks`, `safe`, `add`, `rem`, and `bigint` options on native targets, with Node's errors; the callback forms compute before they return and call back in a later macrotask. Candidates and sizes longer than 262144 bits throw a coded SC2020 error, and wasm32-wasi builds are refused. Node calls their callbacks with `undefined` as the error, which a callback whose error parameter is typed `Error | null`, as @types/node declares it, receives as `null`. - RSA, EC, and Ed25519 private and public KeyObjects come from `generateKeyPairSync`, `generateKeyPair`, `createPrivateKey`, and `createPublicKey`, on native targets. + RSA, RSA-PSS, EC, Ed25519, and X25519 private and public KeyObjects come from `generateKeyPairSync`, `generateKeyPair`, `createPrivateKey`, and `createPublicKey`, on native targets. `generateKeyPairSync` and `generateKeyPair` check their arguments in Node's order, with Node's errors, including the OpenSSL errors for RSA modulus lengths below 512 or above 2147483647 bits and for public exponents of 0, 1, or an even number, which `generateKeyPair` passes to its callback. - RSA keys generate with even modulus lengths from 1024 to 8192 bits and public exponents up to 2147483647; other lengths and exponents that Node accepts, the `raw-public`, `raw-private`, and `raw-seed` encodings, and `paramEncoding: 'explicit'` throw a coded SC2020 error. + RSA keys generate with modulus lengths from 512 to 8192 bits, an odd length from 2048 bits yielding one bit less as in Node, and odd public exponents up to 4294967295; longer moduli that Node accepts, and `paramEncoding: 'explicit'`, throw a coded SC2020 error. The EC curves are prime256v1 (P-256), secp384r1 (P-384), secp521r1 (P-521), and secp256k1. - Keys read and write PEM and DER as PKCS#1, SEC1, PKCS#8, and SubjectPublicKeyInfo, and JWK (Ed25519 keys use PKCS#8, SubjectPublicKeyInfo, and JWK); `createPublicKey` also takes a private key or an X.509 certificate. + Keys read and write PEM and DER as PKCS#1, SEC1, PKCS#8, and SubjectPublicKeyInfo, and JWK (Ed25519 and X25519 keys use PKCS#8, SubjectPublicKeyInfo, and JWK); `createPublicKey` also takes a private key or an X.509 certificate. + EC, Ed25519, and X25519 keys also read and write the `raw-public` and `raw-private` formats, including compressed EC points. + Raw X448, Ed448, and post-quantum keys, EC scalars of 0 or at least the curve order, and the EC point at infinity, which Node accepts, throw a coded SC2020 error. They support `type`, `asymmetricKeyType`, `asymmetricKeyDetails`, `export()`, `equals()`, and `instanceof KeyObject`. `crypto.sign`, `crypto.verify`, `createSign`, and `createVerify` support RSA PKCS#1 v1.5 and PSS padding and ECDSA with DER and IEEE P1363 encodings over SHA-1 and SHA-2 digests, and Ed25519 through `crypto.sign` and `crypto.verify` with a `null` or `undefined` algorithm. + RSA-PSS keys follow their RSASSA-PSS-params as Node does: PSS padding only, the key's digest and MGF1 digest, and salts of at least the key's length, with OpenSSL's errors; their details name the parameters, they export as PKCS#8 and SubjectPublicKeyInfo only, and digests other than SHA-1 and SHA-2 in their options throw a coded SC2020 error. Ed25519 signing, verification, and private keys link when a string literal in the program names `ed25519` (in any case); without one they throw a coded SC2020 error, while Ed25519 public keys still import and export. + X509Certificate supports `publicKey`, `verify()`, `checkIssued()`, `checkPrivateKey()`, `checkHost()`, `checkEmail()`, `checkIP()`, `toLegacyObject()`, `toString()`, and `toJSON()`, and `String()`, template literals, and `+` give its PEM text as `toString()` does. + `publicKey`, `verify()`, and `checkPrivateKey()` are refused in wasm32-wasi builds, and an Ed25519 certificate's `verify()` needs the `ed25519` literal like the other Ed25519 operations. + Own-key walks over a certificate (`Object.keys()`, `Object.entries()`, spread, `for...in`, and `hasOwnProperty`) are refused, since Node keeps its fields as prototype getters. + `JSON.stringify()` and `console.log()` of a certificate, a certificate stored in an `unknown` or `any` slot, and its methods on a union-typed value are also refused. Sign and Verify values accumulate `update()` data and are not writable streams. - Encrypted private keys (`passphrase`, `cipher`), the other key types (`rsa-pss`, `dsa`, `ed448`, `x25519`, and the rest), and wasm32-wasi builds throw a coded SC2020 error or are refused. + Private keys export encrypted (`cipher`, `passphrase`) with aes-128, aes-192, and aes-256 in CBC and ECB modes and with des-ede3-cbc, PKCS#8 as PBES2 with PBKDF2-HMAC-SHA256 and PKCS#1 and SEC1 as traditional PEM, and the ciphers OpenSSL refuses for a form throw its errors; a cipher option can also be an OID; other ciphers Node encrypts with throw a coded SC2020 error. + Encrypted private keys import with a `passphrase` from PBES2 with HMAC-SHA PRFs and the CBC and ECB ciphers, and from traditional PEM with the CBC ciphers, with Node's errors for a missing or wrong passphrase; other schemes and ciphers throw a coded SC2020 error once a passphrase is given. + The other key types (`dsa`, `ed448`, `x448`, and the rest) and wasm32-wasi builds throw a coded SC2020 error or are refused. Comparing keys of different types with `equals()` answers false; Node also leaves an OpenSSL error queued, which a later key operation can report. `publicEncrypt`/`privateDecrypt` remain unsupported. The WebCrypto `crypto` global, which node:crypto also exports as `webcrypto`, supports `randomUUID()` and `getRandomValues()`, also read as values from untyped JavaScript. diff --git a/docs/content/limitations/builtin-apis/date.mdx b/docs/content/limitations/builtin-apis/date.mdx index ac8bf9115..d783984fa 100644 --- a/docs/content/limitations/builtin-apis/date.mdx +++ b/docs/content/limitations/builtin-apis/date.mdx @@ -1,5 +1,7 @@ -- **Date:** Date unions, spread constructor arguments, setters, statically typed TypeScript identity comparisons, thrown Date values, and locale formatters are unsupported. - Zero-argument, single number/string, and local-time year/month field constructors, storage and passing, `getTime`, `valueOf`, `toISOString`, `toUTCString`, `toJSON` (a `string | null`: null for an invalid date, as in Node), `toString`, `toDateString`, `toTimeString`, `String(date)` and template literal conversion, calendar getters, `getTimezoneOffset`, and single-string `Date.parse` compile. - JavaScript Dates retain identity through checked storage and support Date-copy construction and JSON serialization. - A TypeScript Date converted to `unknown` becomes a Date with the same time value; each conversion creates a new Date, so two `unknown` values converted from one TypeScript Date are not `===`. +- **Date:** Spread constructor arguments and locale formatters are unsupported. + Dates are objects with identity: `===` and `==` compare identity, `new Date(date)` makes a new Date, setters change the one Date every reference holds, and Dates compile in arrays (`includes`, `indexOf`, `lastIndexOf`, `join`, and default `sort` by `toString()` text), in unions with other types, as Map and Set keys and values, and in records, tuples, class fields, closures, promises and generators. + Thrown and rejected Dates keep their identity; `structuredClone` and `postMessage` copy them. + Zero-argument, single number/string, and local-time year/month field constructors, `getTime`, `valueOf`, `toISOString`, `toUTCString`, `toJSON` (a `string | null`: null for an invalid date, as in Node), `toString`, `toDateString`, `toTimeString`, the local and UTC calendar getters and setters, `setTime`, `getTimezoneOffset`, and single-string `Date.parse` compile. + Unary `+`, `-` and `Number(date)` read the time value; `String(date)`, template literals and `+` with a string read the `toString()` text; `date[Symbol.toPrimitive](hint)` compiles with a literal hint. + A Date converted to `unknown` is the same Date object. See [Dates and timezones](#dates-and-timezones) for timezone data. diff --git a/docs/content/limitations/builtin-apis/filesystem-url-paths.mdx b/docs/content/limitations/builtin-apis/filesystem-url-paths.mdx index d5079ed5a..9b3580647 100644 --- a/docs/content/limitations/builtin-apis/filesystem-url-paths.mdx +++ b/docs/content/limitations/builtin-apis/filesystem-url-paths.mdx @@ -1,3 +1,3 @@ -- **Filesystem URL paths:** Synchronous `fs` functions accept `file:` URL paths and convert them as `fileURLToPath` does. - `existsSync` and the `fs.promises` functions refuse URL paths; pass `fileURLToPath(url)` instead. - A URL path after a string path, or with options that are not literals, is refused, because Node checks those arguments before it converts the URL. +- **Filesystem URL paths:** `fs` and `fs.promises` functions accept `file:` URL paths and convert them as `fileURLToPath` does; `existsSync` answers `false` and the `fs.promises` functions reject for a URL that does not convert. + A URL path with options that are not literals is refused where Node checks those options before it converts the URL, except a `readFileSync` encoding. + `fs.promises` functions also refuse a string path before a URL path; pass `fileURLToPath(url)` instead. diff --git a/docs/content/limitations/builtin-apis/globals.mdx b/docs/content/limitations/builtin-apis/globals.mdx index f875bd761..e54530b29 100644 --- a/docs/content/limitations/builtin-apis/globals.mdx +++ b/docs/content/limitations/builtin-apis/globals.mdx @@ -1,4 +1,8 @@ - **Globals:** Stored `globalThis` references support identity and probes for absent capabilities. - Most builtin members require direct global access; access through a stored global object can raise `SC2020`. + In JavaScript, reading a builtin global through the global object by name (`globalThis[name]`, or a stored global object) answers what the global's own spelling reads, and builtin module members that are global objects in Node, such as `require("buffer").atob` and `require("timers").setTimeout`, are the same values. + `WebAssembly` read this way, and builtin members read through a global object that untyped code received, can raise `SC2020`. + `for (key in globalThis)` visits Node's enumerable globals in Node's order, then the program's own global properties. + `navigator` answers Node's `hardwareConcurrency`, `language`, `languages`, `platform`, and `userAgent`, with `language` derived from `LC_ALL`, `LC_MESSAGES`, or `LANG` as Node does. + `navigator.locks` raises `SC2020`, as does `navigator.language` for a locale outside the `language_TERRITORY.codeset@modifier` form, or one whose language or territory needs ICU's tables (a three-letter territory, for example). `Temporal` is not defined, as in Node.js 24: `typeof Temporal` is `"undefined"`, `globalThis.Temporal` is `undefined`, and any other use throws `ReferenceError: Temporal is not defined`. Programs that name it type-check against TypeScript's `Temporal` declarations. diff --git a/docs/content/limitations/builtin-apis/intl-segmenter.mdx b/docs/content/limitations/builtin-apis/intl-segmenter.mdx index 41a61711c..2682acaf0 100644 --- a/docs/content/limitations/builtin-apis/intl-segmenter.mdx +++ b/docs/content/limitations/builtin-apis/intl-segmenter.mdx @@ -1,2 +1,3 @@ -- **Intl.Segmenter:** Default Unicode grapheme segmentation, iteration, and `containing()` compile. - Locale negotiation, word and sentence segmentation, `resolvedOptions()`, and detached methods are unsupported. +- **Intl.Segmenter:** Unicode grapheme segmentation, iteration, and `containing()` compile, with the default locale or one language tag of the form language[-script][-region] written as a string literal (grapheme clusters do not depend on the locale). + In JavaScript dependencies, word and sentence segmenters construct, and their `segment()` throws an SC2020 error. + Locale negotiation over several locales, word and sentence segmentation, `resolvedOptions()`, and detached methods are unsupported. diff --git a/docs/content/limitations/builtin-apis/object-freezing.mdx b/docs/content/limitations/builtin-apis/object-freezing.mdx index 99b78ded5..030b03b4b 100644 --- a/docs/content/limitations/builtin-apis/object-freezing.mdx +++ b/docs/content/limitations/builtin-apis/object-freezing.mdx @@ -4,7 +4,8 @@ Class instances support `Object.freeze`, `Object.seal`, `Object.preventExtensions`, `Object.isFrozen`, `Object.isSealed`, and `Object.isExtensible` when the static type of the argument names the class. A write to a read-only field throws Node's `TypeError` in strict mode code and leaves the field unchanged in sloppy mode code. `Object.defineProperty` with a string-literal key changes the value, `writable`, `enumerable`, and `configurable` attributes of a declared field; an object-literal accessor descriptor (`get` or `set`) for a declared field is refused at compile time with `SC2020`, and another accessor descriptor for a declared field throws `SC2020` when the call runs. - Freezing, sealing, or preventing extensions of a class instance held in an untyped value throws `SC2020`. + `Object.freeze`, `Object.seal`, and `Object.preventExtensions` of a class instance held in an untyped value change the instance itself, so writes through its typed references are checked the same way. + The `TypeError` for a write to a read-only property that was added to a class instance through an untyped value names `#` where Node names the class. Functions support `Object.freeze`, `Object.seal`, `Object.preventExtensions`, `Object.isFrozen`, `Object.isSealed`, and `Object.isExtensible`; the operations apply to the function's own properties (`name`, `length`, `prototype`, and the ones a program assigns). Class constructors are not supported. A write to a read-only property of a checked dictionary or a function throws a `TypeError` whose message omits Node's `'x' of object '#'` (or `of function '…'`) suffix. diff --git a/docs/content/limitations/builtin-apis/prototypes.mdx b/docs/content/limitations/builtin-apis/prototypes.mdx index 170b11793..858de3c7e 100644 --- a/docs/content/limitations/builtin-apis/prototypes.mdx +++ b/docs/content/limitations/builtin-apis/prototypes.mdx @@ -1,8 +1,14 @@ - **Prototypes:** Named properties and methods on program class prototypes support replacement, inheritance, and deletion. - Bare prototype reflection, accessor replacement, and prototype storage on local, generic, mixin, or runtime-provided classes are unsupported. + A module-level JavaScript class's prototype can become the prototype of other objects: `Object.create(C.prototype)`, `Object.setPrototypeOf(F.prototype, C.prototype)`, `Object.setPrototypeOf(object, C.prototype)`, and `util.inherits(F, C)` with an ordinary constructor `F`. + The inherited methods and accessors run with the inheriting object as `this`. + Methods that use `#private` members or `super` throw a TypeError for a receiver that is not an instance of the class. + `C.prototype` also reads as a value for `Object.getPrototypeOf` and for `===` and `!==` comparisons. + Changing the prototype of a class prototype (`Object.setPrototypeOf(C.prototype, X)`, `util.inherits(C, F)`, or `Object.setPrototypeOf` on a class prototype reached through an instance) is unsupported. + Other bare prototype reflection, TypeScript class prototypes as values, accessor replacement, and prototype storage on local, generic, mixin, or runtime-provided classes are unsupported. Plain objects, arrays, functions, class instances, and Date, RegExp, Map, Set, WeakMap, WeakSet, and Promise values inherit `constructor`, `valueOf`, `toString`, `hasOwnProperty`, and `propertyIsEnumerable` from `Object.prototype` as in Node, so `value.constructor === Object` and `value.valueOf === Object.prototype.valueOf` compare identities exactly. Named reads such as `value.valueOf` answer these inherited members; computed-key reads such as `value[key]` do not. `Object.prototype`'s own members read as values; its mutation and other reflection, such as `isPrototypeOf` and `__proto__`, are unsupported. The constructors `Object`, `Function`, `Date`, `RegExp`, `Map`, `Set`, `WeakMap`, `WeakSet`, and `Promise` read as function values; through a stored value only `Object` can be called. `new` through a stored `Object`, `Date`, `Map`, `Set`, `WeakMap`, or `WeakSet` builds the same value as the `new` expression. `new` through a stored `RegExp` or `Promise`, `new` of a stored `Object` with a primitive argument, and `instanceof` against these constructors are unsupported. + `instanceof Object` and `instanceof Function` on untyped values follow the prototype chain as in Node, so an object built with `Object.create(null)` is not an `Object`. diff --git a/docs/content/limitations/builtin-apis/streams-and-readline.mdx b/docs/content/limitations/builtin-apis/streams-and-readline.mdx index 45a784742..25427a307 100644 --- a/docs/content/limitations/builtin-apis/streams-and-readline.mdx +++ b/docs/content/limitations/builtin-apis/streams-and-readline.mdx @@ -3,7 +3,14 @@ `readable.pipe(process.stdout)` and `readable.pipe(process.stderr)` compile and, as in Node, do not end the process stream. When an untyped value reaches `NodeJS.ReadableStream` or `NodeJS.WritableStream`, those two interfaces hold any value, so JavaScript stream objects (constructor-function streams, plain objects with `on` and `pipe`) pass through TypeScript functions typed with them and come back to JavaScript as the same objects. Member calls on such a value use the native stream when it is one and call the object's own members otherwise; `readable.pipe()` into a JavaScript destination follows Node's pipe protocol. - A `pipeline` stage and a `for await` source that arrive as untyped values must be native streams, or `Cipheriv` and `Decipheriv` values, at run time; other values throw a not-supported error. + The first `pipeline` stage may arrive as an untyped value that is a native stream, a `Cipheriv` or `Decipheriv` value, or an iterable: a string, an array, a generator object, or any value with a `Symbol.asyncIterator` or `Symbol.iterator` method, which the pipeline pumps into the next stage as Node does. + It may also be a source function, which the pipeline calls with `{ signal }` and whose iterable result it pumps, or a `{ readable, writable }` pair, whose readable side it pumps. + Later `pipeline` stages that arrive as untyped values may be native streams, `Cipheriv` and `Decipheriv` values, functions, and iterables. + A function is called with the previous stage and `{ signal }` as a function stage: before the last stage it must return an async iterable, and as the last stage it may return a promise, whose value resolves the pipeline, or an async iterable, which feeds the PassThrough that `pipeline()` returns. + An untyped source function, function stage, or destination that returns a stream is unsupported. + An iterable becomes `Duplex.from` of it, which the next stage reads from while the stages before it are not read, as in Node. + A pipeline branches at run time on whether each of its first three untyped stages after the first is a function; a function in a later untyped stage, an untyped web stream, and a `{ readable, writable }` pair after the first stage throw a not-supported error. + `for await` sources that arrive as untyped values must be native streams, or `Cipheriv` and `Decipheriv` values, at run time; other values throw a not-supported error. Native streams expose no `emit` to JavaScript, so a JavaScript stream whose own `pipe()` emits on its destination (the legacy `Stream` pipe) cannot pipe into one. `Readable.from` accepts strings, Buffers, arrays, Sets, Maps, Map and Set iterators, generators, async generators, and class iterables and async iterables, and in JavaScript any iterable or async iterable. In JavaScript, a sync iterator value with a `then` method that is not a promise, and an object whose `[Symbol.asyncIterator]` or `[Symbol.iterator]` method is a generator method, are unsupported. @@ -14,7 +21,7 @@ `node:stream/consumers` and readline interfaces read object-mode streams of strings and Buffers; other values throw a not-supported error. `pipeline` accepts function stages anywhere among its streams: async generator functions (`async function* (source)`, iterating `source` with `for await`), an async generator source function (`async function* ({ signal })`), an async generator function as the destination, and an async function destination whose value resolves the promise form and reaches the callback's second argument. A stage is written inline, or named by a function declaration or a `const` bound to a function expression that nothing reassigns. - The source may also be an array, a string, a synchronous generator object, or a synchronous generator function without parameters; their elements reach the next stage on the same ticks as in Node. + The source may also be an array, a string, a `Set`, a `Map`, a generator or async generator object, or a synchronous generator function without parameters; their elements reach the next stage on the same ticks as in Node. Stages may take the `{ signal }` argument. With a function destination, `pipeline()` returns an object-mode PassThrough that receives an async generator destination's values, or an async function destination's value unless it is `null` or `undefined`. `@types/node` types that result as `NodeJS.WritableStream`, so `for await` over it needs the `PassThrough` type (`pipeline(...) as PassThrough`, or a JSDoc `@type`). @@ -27,7 +34,8 @@ A `Duplex.from` body function may take the `{ signal }` argument, which is aborted with the error the Duplex is destroyed with. `stream.compose` accepts streams and async generator functions in any position, an async function as the last stage, and an array, a string, or a generator as the first stage; one stage is `Duplex.from` of it. A web `ReadableStream` is read as an async iterable by `Readable.from` and as the first stage of `compose` or `pipeline`, and through its reader by `Readable.fromWeb` (with `objectMode`, `highWaterMark`, `encoding`, and `signal` in an options object literal) and `Duplex.from`. - A web stream in a later compose stage, `Readable.toWeb`, and web `WritableStream` and `TransformStream` interop are unsupported. + `new TransformStream()` without arguments builds the identity transform: its readable side is a web `ReadableStream`, its writable side supports `getWriter`, `abort`, `close`, and `locked`, and the writer supports `write`, `close`, `abort`, `releaseLock`, `ready`, `closed`, and `desiredSize`, with Node's backpressure (a write completes when the readable side reads its chunk). + A web stream in a later compose stage, `Readable.toWeb`, a `TransformStream` with a transformer or queuing strategies, the `WritableStream` constructor, `pipeTo`, and `pipeThrough` are unsupported. The `ERR_INVALID_ARG_VALUE` message for a stream that is no longer readable or writable names the stream's class instead of printing the stream. `readable.compose(stage, { signal })` is `stream.compose(readable, stage)` with the signal added; the options are an object literal whose `signal` is a variable or a property read. `pipeline` from `node:stream/promises` accepts a trailing `{ signal, end }` object, with or without function stages, and `finished` accepts `readable`, `writable`, `error`, `cleanup`, and `signal` options, with boolean literals for the flags. diff --git a/docs/content/limitations/builtin-apis/synchronous-iteration.mdx b/docs/content/limitations/builtin-apis/synchronous-iteration.mdx index 564f9af45..38cc9c475 100644 --- a/docs/content/limitations/builtin-apis/synchronous-iteration.mdx +++ b/docs/content/limitations/builtin-apis/synchronous-iteration.mdx @@ -1,4 +1,4 @@ - **Synchronous iteration:** Checked `for...of` follows live iterator methods, including compiled class methods and abrupt-completion cleanup. Mapper-less checked `Array.from` accepts those iterators and plain array-like objects. Sparse checked arrays and general borrowed iterator factories are unsupported. - Direct statically typed class loops also restrict iterator `return` and `throw` methods. + Direct statically typed class loops accept iterators that are class instances or object literals; a loop that exits early calls the iterator's zero-parameter `return()`, as Node's IteratorClose does. diff --git a/docs/content/limitations/builtin-apis/text-encoding.mdx b/docs/content/limitations/builtin-apis/text-encoding.mdx index bffd24424..e20cfd943 100644 --- a/docs/content/limitations/builtin-apis/text-encoding.mdx +++ b/docs/content/limitations/builtin-apis/text-encoding.mdx @@ -1,6 +1,7 @@ - **Text encoding:** `TextEncoder` and `TextDecoder` can be stored in fields, arguments, return values, arrays, and closures. Encoding supports UTF-8, including `encodeInto` into a Uint8Array or Buffer view. Codec properties expose the canonical encoding and decoder flags. + Codecs held in `any` or `unknown` storage keep their identity and streaming state, and dynamic code can call `encode`, `encodeInto` and `decode`, with options objects, and read their properties. Decoding accepts recognized WHATWG labels, including runtime labels and omitted or undefined encodings. Literal constructor options support `fatal` and `ignoreBOM`; literal decode options support streaming UTF-8, UTF-16, and single-byte encodings. Other legacy streaming encodings, runtime options objects, inspection, JSON conversion, and module-scope declarations using `var` or lacking an initializer are unsupported. diff --git a/docs/content/limitations/builtin-apis/url.mdx b/docs/content/limitations/builtin-apis/url.mdx index 6345e2616..dddf2a8f7 100644 --- a/docs/content/limitations/builtin-apis/url.mdx +++ b/docs/content/limitations/builtin-apis/url.mdx @@ -1,9 +1,7 @@ -- **URL:** Non-ASCII or percent-escaped hosts are unsupported. - Absolute inputs and relative inputs with runtime string or URL bases, including optional bases, compile. +- **URL:** Absolute inputs and relative inputs with runtime string or URL bases, including optional bases, compile. `URL.canParse`, `URL.parse`, `toString()`, and `toJSON()` compile; the factories use the same parser as construction, returning `false` or `null` for rejected inputs while preserving conversion errors. `href`, `protocol`, `username`, `password`, `host`, `hostname`, `port`, `pathname`, `search`, and `hash` support assignment in native code. Invalid component writes are ignored except for invalid `href` values, which throw; credentials are percent-encoded and ports accept valid leading digits. Native URL assignments require statically known property keys; `+=` accepts primitive operands, while other assignment operators and object operands for `+=` are refused. `origin` is read-only. `searchParams` is a live view whose identity survives query and URL replacement. - Opaque paths retain their input bytes without WHATWG C0 encoding. diff --git a/docs/content/limitations/builtin-function-values.mdx b/docs/content/limitations/builtin-function-values.mdx index 0fa62e558..96c48375c 100644 --- a/docs/content/limitations/builtin-function-values.mdx +++ b/docs/content/limitations/builtin-function-values.mdx @@ -7,6 +7,7 @@ Some builtins can be stored and passed as functions: In JavaScript, `setTimeout`, `setInterval`, `setImmediate`, `clearTimeout`, `clearInterval`, `clearImmediate`, `queueMicrotask`, and `process.nextTick` are function values wherever they are stored, including conditional and logical expressions and assignments: `typeof` answers `"function"`, identity holds across spellings, and calls schedule as the direct calls do, returning the same numeric timer handles. Calls through these values bypass `node:test` mock timers. +In JavaScript, `atob`, `btoa`, and `structuredClone` are function values the same way, and calls through them run what the direct calls run. Other builtin globals stored in JavaScript remain opaque identity values. In JavaScript, `require("events")` and the `EventEmitter` class assigned to an untyped binding or stored in an object (`ReleaseEmitter = EventEmitter`) are the untyped `EventEmitter` constructor that constructor functions inherit from (see below): `new` through it builds such an emitter object, and identity and `instanceof` agree with the class. A class can extend `require("events")` directly. @@ -14,7 +15,7 @@ A class can extend `require("events")` directly. Other immutable aliases of table-backed Node builtins support direct calls, but cannot be passed elsewhere. `util.promisify` supports compile-time conversions of `child_process.execFile`, `child_process.exec`, `setTimeout` and `setImmediate` (to the `timers/promises` functions, as in Node), `stream.finished` and `stream.pipeline` (to the `stream/promises` functions, as in Node), the `zlib` callback one-shots `deflate`, `inflate`, `deflateRaw`, `inflateRaw`, `gzip`, `gunzip` and `unzip` (with or without options), the `crypto` callback functions `randomBytes`, `pbkdf2`, `scrypt`, `hkdf`, `randomFill`, `randomInt` and `generateKey`, `dns.lookup` (settling with `{ address, family }`, or the address array with `all: true`) and `dns.lookupService` (settling with `{ hostname, service }`), and the `fs` callback functions that have an `fs/promises` counterpart (`access`, `appendFile`, `chmod`, `chown`, `copyFile`, `lchown`, `link`, `lstat`, `lutimes`, `mkdir`, `mkdtemp`, `readFile`, `readdir`, `readlink`, `realpath`, `rename`, `rm`, `rmdir`, `stat`, `statfs`, `symlink`, `truncate`, `unlink`, `utimes`, and `writeFile`); calls through the converted function lower as the `fs/promises` call. -`util.promisify` over an untyped (`any`) function returns a native wrapper that follows Node's callback rules and carries the original's `name` and `length`. +`util.promisify` over an untyped function (a value typed `any`, a function whose parameters are untyped, or any other target in JavaScript) returns a native wrapper that follows Node's callback rules and carries the original's `name` and `length`, and throws Node's `ERR_INVALID_ARG_TYPE` for values that are not functions. Such a function with other own properties, `util.promisify.custom` among them, throws `SC2020` where `util.promisify` runs, and the wrapper does not report Node's DEP0174 warning when the original returns a promise. `util.styleText` supports standard formats and aliases, `#RGB` and `#RRGGBB` hex colors, arrays of formats, nested ANSI resets, and explicit color enabling with `{ validateStream: false }`. diff --git a/docs/content/limitations/console-and-process-streams.mdx b/docs/content/limitations/console-and-process-streams.mdx index c62b82f0c..946cb8061 100644 --- a/docs/content/limitations/console-and-process-streams.mdx +++ b/docs/content/limitations/console-and-process-streams.mdx @@ -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. diff --git a/docs/content/limitations/contract-types.mdx b/docs/content/limitations/contract-types.mdx index 35d7a95c0..1fd24a81b 100644 --- a/docs/content/limitations/contract-types.mdx +++ b/docs/content/limitations/contract-types.mdx @@ -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). diff --git a/docs/content/limitations/language-features/async-iteration.mdx b/docs/content/limitations/language-features/async-iteration.mdx index 863c9be90..b15dca7fc 100644 --- a/docs/content/limitations/language-features/async-iteration.mdx +++ b/docs/content/limitations/language-features/async-iteration.mdx @@ -1,6 +1,7 @@ - **Async iteration:** Async `yield*` delegation, stored Web or Node iterator handles, and general structural async-iterator objects are unsupported. Typed async generators support lazy execution, `await`, direct `yield`, queued `.next()`/`.return()`/`.throw()` requests, and `for await`. `for await` also accepts Node `Readable` streams, Web `ReadableStream` values, and compiled class iterators whose zero-parameter `next()` returns a promise of a `{ value, done? }` record. + A class instance whose `[Symbol.asyncIterator]()` returns an object literal, or that has only `[Symbol.iterator]()`, iterates through the checked-dynamic async protocol, and an early exit awaits the iterator's `return()`. Abrupt completion runs cleanup, including literal `destroyOnReturn` and `preventCancel` options. The `node:timers/promises` `setInterval(delay, value)` iterator compiles without AbortSignal options. `Array.fromAsync` accepts arrays of values or of promises and typed async generators, with an optional mapping function; a `thisArg` and arrays that mix values and promises are unsupported. diff --git a/docs/content/limitations/native-addons.mdx b/docs/content/limitations/native-addons.mdx index 469d1cdd2..988df3660 100644 --- a/docs/content/limitations/native-addons.mdx +++ b/docs/content/limitations/native-addons.mdx @@ -2,5 +2,18 @@ Node-API (N-API) and V8 `.node` addons depend on Node.js itself, so compiled pro Direct `createRequire` calls to local addons are rejected at compile time. A package that requires an addon needs a JavaScript implementation that compiles, or a replacement through native FFI. +A package that looks for an optional addon at run time compiles with `--npm-static`. +In JavaScript dependencies, a `createRequire` require call with a computed specifier, and `require.resolve` called with a computed request or read as a value, resolve the specifier with the Node.js algorithm when the program runs. +`require.resolve` returns the path Node.js returns. +When nothing resolves, both throw the `MODULE_NOT_FOUND` error that Node.js throws, so a package that probes for a missing addon takes the same branch as in Node.js. +When the specifier of a require call resolves to a native addon, a builtin, or a file that is not part of the compiled program, the program prints an SC2020 error that names it and exits with status 1, because Node.js would load that module. +A `catch` clause around the call does not run in that case. +Resolution steps that the runtime does not reproduce, such as package `imports` specifiers, `require.resolve` options, and subpaths that a package does not export, end the program the same way. +Coverage lists these calls as deferred to runtime. +Other computed uses compile when they are inside a `try` block: a require call with spread arguments, a `require.resolve` call with another argument count, and `require.resolve.paths` called with a computed request evaluate their arguments and then throw an error, and `require.resolve.paths` read as a value is a function that throws when called. +Coverage lists these as deferred to runtime as well, and the package's `catch` clause then runs as it does in Node.js when the addon is missing. +Outside a `try` block, these uses keep the package refused. +This applies until compiled programs can load addons through Node.js. + Use [Native FFI](/docs/ffi#replacing-a-node-api-addon) to call a plain C ABI function linked from an object or static archive. The guide includes a replacement for a small Node-API helper. diff --git a/docs/content/limitations/record-identity-and-property-order.mdx b/docs/content/limitations/record-identity-and-property-order.mdx index cc16f74d5..fc0b2be92 100644 --- a/docs/content/limitations/record-identity-and-property-order.mdx +++ b/docs/content/limitations/record-identity-and-property-order.mdx @@ -4,6 +4,8 @@ Empty record layouts are also snapshots because they have no property storage. `scriptc coverage` and `scriptc build` report `SC6001` where a copied value is then written or compared, whether the record was passed as an argument, stored in a typed binding, asserted with `as`, assigned to a typed binding later, or stored as an element of a typed array literal. Typed records with declared fields, indexed records, and arrays retain identity and mutations through native checked storage. +A record or class instance converted to a string-keyed dictionary type such as `Record`, directly or through a `{}`, `object` or `unknown` binding or parameter, keeps its identity when the program writes through, compares or inspects values of that dictionary type. +Every dictionary type with the same value type then uses checked dynamic storage, which is slower than a native dictionary. Class instances retain identity at their exact type or a base class, and at any interface or object type that a class instance is passed, stored, returned, or asserted as when the program could tell a copy from the instance: values of such a type are checked views of the original object, so writes, identity comparisons, Set and Map keys, `util.inspect`, and `JSON.stringify` through them reach the instance, as in Node.js. These types use checked dynamic storage, which is slower than native record fields, for all of their values, including plain objects. A type whose values are only read field by field, from fields the class does not write after construction and does not implement as accessors, stays a native record and holds a copy. @@ -23,3 +25,10 @@ Index-signature records list declared fields before the other keys. Optional fields and properties added through a record's `unknown` view can be deleted. A required field has no absent state, so deleting it through the view throws `TypeError: Cannot delete property 'x' of #` 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`, 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 number>`. +Other inherited names, such as `constructor`, read `undefined` from typed records. diff --git a/docs/content/limitations/tls-certificates.mdx b/docs/content/limitations/tls-certificates.mdx new file mode 100644 index 000000000..639dd3d83 --- /dev/null +++ b/docs/content/limitations/tls-certificates.mdx @@ -0,0 +1,14 @@ +`tlsSocket.getPeerCertificate()`, `getCertificate()`, `getPeerX509Certificate()`, and `getX509Certificate()` follow Node, including the `detailed` issuer chain, on sockets whose type is known. +On a TLS socket held in an `unknown` or `any` value, or in an untyped JavaScript parameter, they throw an error that names the call as not supported. +Client certificates follow Node: `tls.createServer`, `https.createServer`, and `new tls.TLSSocket` take `requestCert`, `rejectUnauthorized`, and `ca`, and `tls.connect` and `https.request` take `cert` and `key`. +A server's `authorized` and `authorizationError` report the client certificate's verification as Node does, and a server, client, or `tls.createSecureContext` given a `ca` also sends the chain it completes, as OpenSSL's automatic chain does. +A server verifies client certificates against the `ca` of the context it serves, including contexts from `addContext()` and `setSecureContext()`: an empty `ca` (an empty Buffer or array) trusts no certificate, and an omitted `ca` trusts the default CAs. +The same holds for a client's `ca` in `tls.connect`, `https.request`, and `http2.connect`, where an empty string is no `ca`, as in Node. +In `http2.connect`, a `ca` held in an `unknown` or `any` value that is an empty string is refused. +`requestCert` beside a TLSSocket's `secureContext` or an `SNICallback`, client certificates on http2 secure servers, and a `cert` without a `key` (or the reverse) are refused. +`server.addContext()` and `server.setSecureContext()` follow Node on tls and https servers, including wildcard names, which match one label without regard to case. +A name with regular-expression groups or alternation, or one outside ASCII, is refused, as are both methods on a server that is not a tls or https server. +The `'tlsClientError'` event reports Node's error for a failed handshake: OpenSSL's code and reason, or `socket hang up` for a client that leaves. +OpenSSL's messages leave out the thread identifier Node prints before `error:` and the source location after the reason. +A failure whose OpenSSL reason the runtime does not know, such as a malformed client hello, is reported as an error with code `SC2020` that says so. +The X509Certificate those accessors return has the same refusals as any other certificate (see Crypto), so in JavaScript narrow it, for example with `if (cert)`, before reading its fields. diff --git a/docs/content/limitations/tls-sessions.mdx b/docs/content/limitations/tls-sessions.mdx new file mode 100644 index 000000000..8d34573a8 --- /dev/null +++ b/docs/content/limitations/tls-sessions.mdx @@ -0,0 +1,3 @@ +TLS servers send no session tickets, so a connection to them cannot be resumed. +A client's `'session'` event fires once per connection, when its handshake finishes and before `secureConnect`. +Node fires it for each ticket the server sends, after `secureConnect`, and OpenSSL servers send two. diff --git a/docs/content/limitations/types-and-class-definitions/array-subclasses.mdx b/docs/content/limitations/types-and-class-definitions/array-subclasses.mdx new file mode 100644 index 000000000..ea76e7313 --- /dev/null +++ b/docs/content/limitations/types-and-class-definitions/array-subclasses.mdx @@ -0,0 +1,3 @@ +- **Array subclasses:** A class that extends `Array` compiles: its instances are arrays, and `map`, `filter`, `slice` and the other methods that create arrays return instances of the subclass, as in Node.js. + A generic subclass (`class Bag extends Array`), a class that extends such a subclass, and a subclass that declares `Symbol.species` are unsupported. + A method whose result the subclass's constructor cannot construct throws a `TypeError`: the constructor's parameters must take the one length argument, and a `map` to another element type needs a constructor that adds no elements. diff --git a/docs/content/limitations/types-and-class-definitions/symbol-fields.mdx b/docs/content/limitations/types-and-class-definitions/symbol-fields.mdx index baff4d190..45659b493 100644 --- a/docs/content/limitations/types-and-class-definitions/symbol-fields.mdx +++ b/docs/content/limitations/types-and-class-definitions/symbol-fields.mdx @@ -1,4 +1,5 @@ - **Symbol fields:** Instance fields require a stable module-level `Symbol()` or `Symbol.for()` binding with a literal description or constant string binding. + A `[Symbol.toStringTag]` field or getter with a string value sets the tag that `String()`, `Object.prototype.toString` and `util.inspect` print; fields keyed by other well-known symbols are unsupported. Reassigned keys, runtime-computed descriptions, distinct symbols with the same description in one class, and copying symbol fields with rest bindings or `Object.assign` are unsupported. JavaScript object spreads copy represented own symbol fields. Equal registry keys share a field across modules. diff --git a/docs/content/limitations/untyped-values.mdx b/docs/content/limitations/untyped-values.mdx index 6a881aec3..05245a396 100644 --- a/docs/content/limitations/untyped-values.mdx +++ b/docs/content/limitations/untyped-values.mdx @@ -15,3 +15,6 @@ JavaScript argument and return conversions preserve native array references; oth Supported operations on `unknown` include truthiness, `typeof` narrowing, property access, `+`, `switch`, and `throw`. Cast before using other operations. + +In JavaScript, a call whose untyped argument could select an overload with another result type returns a checked value, whichever overload the type checker picks, and so do the unannotated bindings and functions that hold or return it. +In TypeScript, an `any` argument keeps the overload the type checker picks, and a value that selects another overload at runtime throws a `TypeError` where the result is used. diff --git a/docs/package.json b/docs/package.json index 3c2a950fd..51d74d0a1 100644 --- a/docs/package.json +++ b/docs/package.json @@ -10,9 +10,10 @@ "build": "fumadocs-mdx && next build", "start": "next start", "typecheck": "fumadocs-mdx && next typegen && tsc --noEmit", + "test:unit": "node --test test/compatibility-matrix.test.mjs", "test:routes": "node scripts/test-routes.mjs", "compat:check": "pnpm --dir .. node-compat:check", - "check": "pnpm compat:check && pnpm typecheck && pnpm build && pnpm test:routes" + "check": "pnpm compat:check && pnpm typecheck && pnpm test:unit && pnpm build && pnpm test:routes" }, "dependencies": { "@vercel/geistdocs": "2.7.8", diff --git a/docs/src/components/node-compatibility-matrix.tsx b/docs/src/components/node-compatibility-matrix.tsx index 095bcdaf2..af2435668 100644 --- a/docs/src/components/node-compatibility-matrix.tsx +++ b/docs/src/components/node-compatibility-matrix.tsx @@ -4,52 +4,28 @@ // empty after an edit. // @refresh reset -import { useEffect, useMemo, useState } from "react"; +import { memo, startTransition, useCallback, useDeferredValue, useEffect, useId, useMemo, useState, type FocusEvent, type KeyboardEvent, type ReactNode } from "react"; +import { + buildTree, + expandableIds, + filterTree, + flattenTree, + nodeCount, + rowMatcher, + searchTexts, + statusCounts, + statuses, + type NodeStability, + type Row, + type Snapshot, + type Status, + type Verification, +} from "@/lib/compatibility-tree"; // Set by next.config.mjs from the committed artifact. const artifactVersion = process.env.COMPATIBILITY_ARTIFACT_VERSION; const rowCount = Number(process.env.COMPATIBILITY_ROW_COUNT); -type Status = - | "supported" - | "partial" - | "refused" - | "not-implemented" - | "by-design" - | "unreviewed" - | "not-applicable"; -type VerificationBasis = - | "test-backed" - | "explicit-refusal" - | "verified-absence" - | "declared-gap" - | "registry-gap" - | "architectural-policy" - | "unreviewed" - | "not-applicable" - | "derived"; -type Verification = { label: string; detail: string }; -type Tier = { status: Status; detail: string; verification: VerificationBasis }; -type NodeStability = { index: string; level: number; text: string; inherited: boolean } | null; -type Row = { - id: string; - chapter: string; - kind: string; - name: string; - signature: string; - label: string; - apiSymbol: string; - depth: number; - scope: "api" | "documentation" | "metadata" | "configuration"; - anchor: string; - nodeStability: NodeStability; - static: Tier; -}; -type Chapter = { slug: string; title: string; entries: number; apiEntries: number }; -type Snapshot = { nodeVersion: string; verificationBases: Record; chapters: Chapter[]; rows: Row[] }; -type TreeNode = { row: Row; children: TreeNode[] }; -type VisibleNode = { node: TreeNode; level: number }; - const labels: Record = { supported: "Supported", partial: "Partial", @@ -70,7 +46,6 @@ const styles: Record = { "not-applicable": "border-gray-alpha-400 bg-background-200 text-gray-700", }; -const statuses = ["supported", "partial", "not-implemented", "refused", "by-design", "unreviewed", "not-applicable"] as const; const stabilityStyles: Record = { 0: "border-red-500/40 bg-red-100 text-red-900 dark:bg-red-200", 1: "border-amber-500/50 bg-amber-100 text-amber-900 dark:bg-amber-200", @@ -78,114 +53,83 @@ const stabilityStyles: Record = { 3: "border-gray-alpha-500 bg-gray-alpha-100 text-gray-900", }; -function StatusBadge({ status, detail, verification }: { status: Status; detail?: string; verification?: Verification }) { - const tooltip = verification ? `${detail ?? ""}${detail ? "\n\n" : ""}Verification: ${verification.label}. ${verification.detail}` : detail; +/** A badge's tooltip, mounted only while the badge is hovered or has + * keyboard focus (the :focus-visible rule the CSS-only tooltip used), and + * named by aria-describedby while open. Hover and focus are tracked apart, + * so moving the pointer away keeps a keyboard-focused badge's tooltip. + * Escape dismisses it until the next hover or focus. */ +function BadgeTooltip({ tooltipClass, children, badge }: { tooltipClass: string; children: ReactNode; badge: ReactNode }) { + const [hovered, setHovered] = useState(false); + const [focused, setFocused] = useState(false); + const open = hovered || focused; + const id = useId(); + const enter = useCallback(() => setHovered(true), []); + const leave = useCallback(() => setHovered(false), []); + const focus = useCallback((event: FocusEvent) => { + if (event.currentTarget.matches(":focus-visible")) setFocused(true); + }, []); + const blur = useCallback(() => setFocused(false), []); + const key = useCallback((event: KeyboardEvent) => { + if (event.key !== "Escape") return; + setHovered(false); + setFocused(false); + }, []); return ( - - - {labels[status]} - - {tooltip ? ( - - {detail ? {detail} : null} - {verification ? Verification: {verification.label}. {verification.detail} : null} + + {badge} + {open ? ( + + {children} ) : null} ); } -function StabilityBadge({ stability }: { stability: NodeStability }) { +export const StatusBadge = memo(function StatusBadge({ status, detail, verification }: { status: Status; detail?: string; verification?: Verification }) { + const tooltip = verification ? `${detail ?? ""}${detail ? "\n\n" : ""}Verification: ${verification.label}. ${verification.detail}` : detail; + const badge = ( + + {labels[status]} + + ); + if (!tooltip) return {badge}; + return ( + + {detail ? {detail} : null} + {verification ? Verification: {verification.label}. {verification.detail} : null} + + ); +}); + +export const StabilityBadge = memo(function StabilityBadge({ stability }: { stability: NodeStability }) { if (stability === null) return —; const detail = `Node.js Stability ${stability.index}: ${stability.text}${stability.inherited ? " (inherited from the nearest parent section)" : ""}`; return ( - - - {stability.index} - - - {detail} - - - ); -} - -function statusCounts(rows: Row[], tier: "static") { - return rows.reduce>( - (counts, row) => { - if (row.scope !== "api" || row.depth === 0) return counts; - counts[row[tier].status] += 1; - return counts; - }, - { - supported: 0, - partial: 0, - refused: 0, - "not-implemented": 0, - "by-design": 0, - unreviewed: 0, - "not-applicable": 0, - }, - ); -} - -/** Node's all.json is preorder with an explicit heading depth. */ -function buildTree(rows: Row[]): TreeNode[] { - const roots: TreeNode[] = []; - const stack: TreeNode[] = []; - for (const row of rows) { - const node: TreeNode = { row, children: [] }; - while (stack.length > row.depth) stack.pop(); - const parent = stack[row.depth - 1]; - if (row.depth === 0 || parent === undefined) roots.push(node); - else parent.children.push(node); - stack[row.depth] = node; - stack.length = row.depth + 1; - } - return roots; -} - -function filterTree(nodes: TreeNode[], matches: (row: Row) => boolean): TreeNode[] { - const filtered: TreeNode[] = []; - for (const node of nodes) { - const children = filterTree(node.children, matches); - if (matches(node.row) || children.length > 0) filtered.push({ row: node.row, children }); - } - return filtered; -} - -function flattenTree(nodes: TreeNode[], expanded: Set, forceExpanded: boolean): VisibleNode[] { - const visible: VisibleNode[] = []; - const visit = (items: TreeNode[], level: number) => { - for (const node of items) { - visible.push({ node, level }); - if (node.children.length > 0 && (forceExpanded || expanded.has(node.row.id))) { - visit(node.children, level + 1); + + {stability.index} + } - } - }; - visit(nodes, 1); - return visible; -} - -function nodeCount(nodes: TreeNode[]): number { - return nodes.reduce((count, node) => count + 1 + nodeCount(node.children), 0); -} - -function expandableIds(nodes: TreeNode[]): Set { - const ids = new Set(); - const visit = (items: TreeNode[]) => { - for (const node of items) { - if (node.children.length > 0) ids.add(node.row.id); - visit(node.children); - } - }; - visit(nodes); - return ids; -} + > + {detail} + + ); +}); function Chevron({ open }: { open: boolean }) { return ( @@ -195,6 +139,47 @@ function Chevron({ open }: { open: boolean }) { ); } +type MatrixRowProps = { + row: Row; + level: number; + hasChildren: boolean; + open: boolean; + filtering: boolean; + displayName: string; + href: string; + verification: Verification | undefined; + onToggle: (id: string) => void; +}; + +/** One table row. Memoized on its own values, so a filter or expansion + * change re-renders only the rows whose values changed. */ +const MatrixRow = memo(function MatrixRow({ row, level, hasChildren, open, filtering, displayName, href, verification, onToggle }: MatrixRowProps) { + const isRoot = level === 1; + return ( + + +
+ {hasChildren ? ( + + ) : } +
+ + {displayName} + +
+ {row.kind}{row.apiSymbol && row.apiSymbol !== row.signature && !isRoot ? ` · ${row.apiSymbol}` : ""} +
+
+
+ + + + + ); +}); + export function NodeCompatibilityMatrix() { const [snapshot, setSnapshot] = useState(null); const [loadError, setLoadError] = useState(false); @@ -202,15 +187,16 @@ export function NodeCompatibilityMatrix() { const [staticFilter, setStaticFilter] = useState<"all" | Status>("all"); const [expanded, setExpanded] = useState>(() => new Set()); - const rows = snapshot?.rows ?? []; - const chapters = snapshot?.chapters ?? []; + const rows = useMemo(() => snapshot?.rows ?? [], [snapshot]); + const chapters = useMemo(() => snapshot?.chapters ?? [], [snapshot]); const verificationBases = snapshot?.verificationBases; const apiRowCount = useMemo(() => rows.filter((row) => row.scope === "api").length, [rows]); const chapterTitles = useMemo(() => new Map(chapters.map((item) => [item.slug, item.title])), [chapters]); const base = `https://nodejs.org/docs/v${snapshot?.nodeVersion ?? "24.22.0"}/api/`; - const staticCounts = useMemo(() => statusCounts(rows, "static"), [rows]); + const staticCounts = useMemo(() => statusCounts(rows), [rows]); const visibleStatuses = useMemo(() => statuses.filter((status) => staticCounts[status] > 0), [staticCounts]); const tree = useMemo(() => buildTree(rows), [rows]); + const texts = useMemo(() => searchTexts(rows, chapterTitles), [rows, chapterTitles]); useEffect(() => { const controller = new AbortController(); @@ -231,36 +217,23 @@ export function NodeCompatibilityMatrix() { return () => controller.abort(); }, []); - const needle = query.trim().toLocaleLowerCase(); - const hasResultFilter = needle !== "" || staticFilter !== "all"; - const rowMatches = (row: Row) => { - if (staticFilter !== "all" && row.static.status !== staticFilter) return false; - if (!needle) return true; - return [chapterTitles.get(row.chapter), row.kind, row.name, row.signature, row.apiSymbol, row.static.detail] - .join(" ") - .toLocaleLowerCase() - .includes(needle); - }; + // The search field and the status select update at once; the table + // follows the deferred values, so input stays responsive while the rows + // re-render. + const deferredQuery = useDeferredValue(query); + const deferredStatic = useDeferredValue(staticFilter); + const needle = deferredQuery.trim().toLocaleLowerCase(); + const hasResultFilter = needle !== "" || deferredStatic !== "all"; - const resultTree = useMemo( - () => hasResultFilter ? filterTree(tree, rowMatches) : tree, - // rowMatches closes over these filter values. - // eslint-disable-next-line react-hooks/exhaustive-deps - [hasResultFilter, needle, staticFilter, tree], + const result = useMemo( + () => hasResultFilter ? filterTree(tree, rowMatcher(texts, needle, deferredStatic)) : { nodes: tree, matched: nodeCount(tree) }, + [hasResultFilter, needle, deferredStatic, texts, tree], ); const visible = useMemo( - () => flattenTree(resultTree, expanded, hasResultFilter), - [expanded, hasResultFilter, resultTree], - ); - const matchedCount = useMemo( - () => hasResultFilter ? tree.reduce((count, node) => { - const visit = (item: TreeNode): number => (rowMatches(item.row) ? 1 : 0) + item.children.reduce((sum, child) => sum + visit(child), 0); - return count + visit(node); - }, 0) : nodeCount(tree), - // rowMatches closes over these filter values. - // eslint-disable-next-line react-hooks/exhaustive-deps - [hasResultFilter, needle, staticFilter, tree], + () => flattenTree(result.nodes, expanded, hasResultFilter), + [expanded, hasResultFilter, result], ); + const matchedCount = result.matched; const changeQuery = (value: string) => setQuery(value); const resetFilters = () => { @@ -268,12 +241,12 @@ export function NodeCompatibilityMatrix() { setStaticFilter("all"); setExpanded(new Set()); }; - const toggle = (id: string) => setExpanded((current) => { + const toggle = useCallback((id: string) => setExpanded((current) => { const next = new Set(current); if (next.has(id)) next.delete(id); else next.add(id); return next; - }); + }), []); useEffect(() => { if (!snapshot) return; @@ -316,8 +289,8 @@ export function NodeCompatibilityMatrix() { {visible.length.toLocaleString()} visible · {matchedCount.toLocaleString()} {hasResultFilter ? "matching" : "API"} rows {!hasResultFilter ? ( - - + + ) : null} @@ -335,33 +308,19 @@ export function NodeCompatibilityMatrix() { {visible.map(({ node, level }) => { const row = node.row; - const hasChildren = node.children.length > 0; - const open = hasResultFilter || expanded.has(row.id); - const href = `${base}${row.chapter}.html${row.anchor ? `#${row.anchor}` : ""}`; - const isRoot = level === 1; - const displayName = isRoot ? chapterTitles.get(row.chapter) ?? row.label : row.label; return ( - - -
- {hasChildren ? ( - - ) : } -
- - {displayName} - -
- {row.kind}{row.apiSymbol && row.apiSymbol !== row.signature && !isRoot ? ` · ${row.apiSymbol}` : ""} -
-
-
- - - - + 0} + open={hasResultFilter || expanded.has(row.id)} + filtering={hasResultFilter} + displayName={level === 1 ? chapterTitles.get(row.chapter) ?? row.label : row.label} + href={`${base}${row.chapter}.html${row.anchor ? `#${row.anchor}` : ""}`} + verification={verificationBases?.[row.static.verification]} + onToggle={toggle} + /> ); })} diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json index c3f1835ab..56e26c232 100644 --- a/docs/src/generated/node-v24-compatibility.json +++ b/docs/src/generated/node-v24-compatibility.json @@ -11476,9 +11476,9 @@ "inherited": true }, "static": { - "status": "refused", - "detail": "Explicitly rejected by a named compiler diagnostic.", - "verification": "explicit-refusal" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { @@ -13822,9 +13822,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -13845,9 +13845,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -13868,9 +13868,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -13891,9 +13891,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -13914,9 +13914,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -13937,9 +13937,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -13960,9 +13960,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -13983,9 +13983,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -14006,9 +14006,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -14029,9 +14029,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -14236,9 +14236,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -18268,9 +18268,9 @@ "anchor": "err_brotli_invalid_param", "nodeStability": null, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -21382,9 +21382,9 @@ "anchor": "err_invalid_mime_syntax", "nodeStability": null, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -23520,9 +23520,9 @@ "anchor": "err_throttled", "nodeStability": null, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -23790,9 +23790,9 @@ "anchor": "err_tls_required_server_name", "nodeStability": null, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -35971,9 +35971,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -36684,9 +36684,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "supported", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -36707,9 +36707,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -36730,9 +36730,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -36776,9 +36776,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "supported", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -67484,9 +67484,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -67599,9 +67599,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -67783,9 +67783,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -67921,9 +67921,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -68013,9 +68013,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -68059,9 +68059,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -68174,9 +68174,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -68312,9 +68312,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -68335,9 +68335,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -71509,9 +71509,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { @@ -71946,9 +71946,9 @@ "inherited": false }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { @@ -71969,9 +71969,9 @@ "inherited": false }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { @@ -71992,9 +71992,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -72015,9 +72015,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -72038,9 +72038,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -72061,9 +72061,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -72176,9 +72176,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -72199,9 +72199,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -72222,9 +72222,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -72245,9 +72245,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -72268,9 +72268,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -72291,9 +72291,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -72314,9 +72314,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -72337,9 +72337,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -72360,9 +72360,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -72636,9 +72636,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { @@ -72751,9 +72751,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { @@ -72774,9 +72774,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { @@ -72797,9 +72797,9 @@ "inherited": false }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { @@ -73027,9 +73027,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -73050,9 +73050,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -73395,9 +73395,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { @@ -81657,9 +81657,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented static async-iteration subset.", + "verification": "test-backed" } }, { @@ -84302,9 +84302,9 @@ "inherited": true }, "static": { - "status": "refused", - "detail": "Explicitly rejected by a named compiler diagnostic.", - "verification": "explicit-refusal" + "status": "partial", + "detail": "BrotliCompress and BrotliDecompress are Transform streams over the brotli version Node bundles, with Node's parameters, dictionaries, flush, reset, error codes, and option validation; like Node's, they have no params().", + "verification": "test-backed" } }, { @@ -84325,9 +84325,9 @@ "inherited": true }, "static": { - "status": "refused", - "detail": "Explicitly rejected by a named compiler diagnostic.", - "verification": "explicit-refusal" + "status": "partial", + "detail": "BrotliCompress and BrotliDecompress are Transform streams over the brotli version Node bundles, with Node's parameters, dictionaries, flush, reset, error codes, and option validation; like Node's, they have no params().", + "verification": "test-backed" } }, { @@ -84349,7 +84349,7 @@ }, "static": { "status": "partial", - "detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.", + "detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.", "verification": "test-backed" } }, @@ -84372,7 +84372,7 @@ }, "static": { "status": "partial", - "detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.", + "detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.", "verification": "test-backed" } }, @@ -84395,7 +84395,7 @@ }, "static": { "status": "partial", - "detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.", + "detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.", "verification": "test-backed" } }, @@ -84418,7 +84418,7 @@ }, "static": { "status": "partial", - "detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.", + "detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.", "verification": "test-backed" } }, @@ -84441,7 +84441,7 @@ }, "static": { "status": "partial", - "detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.", + "detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.", "verification": "test-backed" } }, @@ -84464,7 +84464,7 @@ }, "static": { "status": "partial", - "detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.", + "detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.", "verification": "test-backed" } }, @@ -84487,7 +84487,7 @@ }, "static": { "status": "partial", - "detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.", + "detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.", "verification": "test-backed" } }, @@ -84510,7 +84510,7 @@ }, "static": { "status": "partial", - "detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.", + "detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.", "verification": "test-backed" } }, @@ -84533,7 +84533,7 @@ }, "static": { "status": "partial", - "detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.", + "detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.", "verification": "test-backed" } }, @@ -84556,7 +84556,7 @@ }, "static": { "status": "partial", - "detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.", + "detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.", "verification": "test-backed" } }, @@ -84579,7 +84579,7 @@ }, "static": { "status": "partial", - "detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.", + "detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.", "verification": "test-backed" } }, @@ -84602,7 +84602,7 @@ }, "static": { "status": "partial", - "detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.", + "detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.", "verification": "test-backed" } }, @@ -84625,7 +84625,7 @@ }, "static": { "status": "partial", - "detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.", + "detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.", "verification": "test-backed" } }, @@ -84716,9 +84716,9 @@ "inherited": true }, "static": { - "status": "refused", - "detail": "Explicitly rejected by a named compiler diagnostic.", - "verification": "explicit-refusal" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { @@ -84739,9 +84739,9 @@ "inherited": true }, "static": { - "status": "refused", - "detail": "Explicitly rejected by a named compiler diagnostic.", - "verification": "explicit-refusal" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { @@ -84969,9 +84969,9 @@ "inherited": true }, "static": { - "status": "refused", - "detail": "Explicitly rejected by a named compiler diagnostic.", - "verification": "explicit-refusal" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { @@ -84992,9 +84992,9 @@ "inherited": true }, "static": { - "status": "refused", - "detail": "Explicitly rejected by a named compiler diagnostic.", - "verification": "explicit-refusal" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { @@ -85015,9 +85015,9 @@ "inherited": true }, "static": { - "status": "refused", - "detail": "Explicitly rejected by a named compiler diagnostic.", - "verification": "explicit-refusal" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { @@ -85038,9 +85038,9 @@ "inherited": true }, "static": { - "status": "refused", - "detail": "Explicitly rejected by a named compiler diagnostic.", - "verification": "explicit-refusal" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { @@ -85499,7 +85499,7 @@ }, "static": { "status": "partial", - "detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.", + "detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.", "verification": "test-backed" } }, @@ -85521,9 +85521,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "BrotliCompress and BrotliDecompress are Transform streams over the brotli version Node bundles, with Node's parameters, dictionaries, flush, reset, error codes, and option validation; like Node's, they have no params().", + "verification": "test-backed" } }, { @@ -85567,9 +85567,9 @@ "inherited": true }, "static": { - "status": "refused", - "detail": "Explicitly rejected by a named compiler diagnostic.", - "verification": "explicit-refusal" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { @@ -85590,9 +85590,9 @@ "inherited": true }, "static": { - "status": "refused", - "detail": "Explicitly rejected by a named compiler diagnostic.", - "verification": "explicit-refusal" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { @@ -85613,9 +85613,9 @@ "inherited": true }, "static": { - "status": "refused", - "detail": "Explicitly rejected by a named compiler diagnostic.", - "verification": "explicit-refusal" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { @@ -85636,9 +85636,9 @@ "inherited": true }, "static": { - "status": "refused", - "detail": "Explicitly rejected by a named compiler diagnostic.", - "verification": "explicit-refusal" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { diff --git a/docs/src/lib/compatibility-tree.ts b/docs/src/lib/compatibility-tree.ts new file mode 100644 index 000000000..4044bbc8d --- /dev/null +++ b/docs/src/lib/compatibility-tree.ts @@ -0,0 +1,157 @@ +// The compatibility matrix's data model: the ledger rows as a tree, and the +// search and filter passes over it. Kept free of React so the passes can be +// tested directly. + +export type Status = + | "supported" + | "partial" + | "refused" + | "not-implemented" + | "by-design" + | "unreviewed" + | "not-applicable"; +export type VerificationBasis = + | "test-backed" + | "explicit-refusal" + | "verified-absence" + | "declared-gap" + | "registry-gap" + | "architectural-policy" + | "unreviewed" + | "not-applicable" + | "derived"; +export type Verification = { label: string; detail: string }; +export type Tier = { status: Status; detail: string; verification: VerificationBasis }; +export type NodeStability = { index: string; level: number; text: string; inherited: boolean } | null; +export type Row = { + id: string; + chapter: string; + kind: string; + name: string; + signature: string; + label: string; + apiSymbol: string; + depth: number; + scope: "api" | "documentation" | "metadata" | "configuration"; + anchor: string; + nodeStability: NodeStability; + static: Tier; +}; +export type Chapter = { slug: string; title: string; entries: number; apiEntries: number }; +export type Snapshot = { + nodeVersion: string; + verificationBases: Record; + chapters: Chapter[]; + rows: Row[]; +}; +export type TreeNode = { row: Row; children: TreeNode[] }; +export type VisibleNode = { node: TreeNode; level: number }; + +export const statuses = ["supported", "partial", "not-implemented", "refused", "by-design", "unreviewed", "not-applicable"] as const; + +export function statusCounts(rows: Row[]): Record { + return rows.reduce>( + (counts, row) => { + if (row.scope !== "api" || row.depth === 0) return counts; + counts[row.static.status] += 1; + return counts; + }, + { + supported: 0, + partial: 0, + refused: 0, + "not-implemented": 0, + "by-design": 0, + unreviewed: 0, + "not-applicable": 0, + }, + ); +} + +/** Node's all.json is preorder with an explicit heading depth. */ +export function buildTree(rows: Row[]): TreeNode[] { + const roots: TreeNode[] = []; + const stack: TreeNode[] = []; + for (const row of rows) { + const node: TreeNode = { row, children: [] }; + while (stack.length > row.depth) stack.pop(); + const parent = stack[row.depth - 1]; + if (row.depth === 0 || parent === undefined) roots.push(node); + else parent.children.push(node); + stack[row.depth] = node; + stack.length = row.depth + 1; + } + return roots; +} + +/** The lowercase text a search matches against, built once per row: the + * chapter title, kind, name, signature, API symbol and support detail. */ +export function searchTexts(rows: Row[], chapterTitles: ReadonlyMap): Map { + return new Map( + rows.map((row) => [ + row.id, + [chapterTitles.get(row.chapter), row.kind, row.name, row.signature, row.apiSymbol, row.static.detail] + .join(" ") + .toLocaleLowerCase(), + ]), + ); +} + +/** The rows a status filter and a lowercase search needle keep. */ +export function rowMatcher( + texts: ReadonlyMap, + needle: string, + status: "all" | Status, +): (row: Row) => boolean { + return (row) => { + if (status !== "all" && row.static.status !== status) return false; + return needle === "" || (texts.get(row.id) ?? "").includes(needle); + }; +} + +/** The matching rows with their ancestors, and the number of matching + * rows, in one pass. */ +export function filterTree(nodes: TreeNode[], matches: (row: Row) => boolean): { nodes: TreeNode[]; matched: number } { + let matched = 0; + const visit = (items: TreeNode[]): TreeNode[] => { + const filtered: TreeNode[] = []; + for (const node of items) { + const children = visit(node.children); + const own = matches(node.row); + if (own) matched += 1; + if (own || children.length > 0) filtered.push({ row: node.row, children }); + } + return filtered; + }; + return { nodes: visit(nodes), matched }; +} + +export function flattenTree(nodes: TreeNode[], expanded: ReadonlySet, forceExpanded: boolean): VisibleNode[] { + const visible: VisibleNode[] = []; + const visit = (items: TreeNode[], level: number) => { + for (const node of items) { + visible.push({ node, level }); + if (node.children.length > 0 && (forceExpanded || expanded.has(node.row.id))) { + visit(node.children, level + 1); + } + } + }; + visit(nodes, 1); + return visible; +} + +export function nodeCount(nodes: TreeNode[]): number { + return nodes.reduce((count, node) => count + 1 + nodeCount(node.children), 0); +} + +export function expandableIds(nodes: TreeNode[]): Set { + const ids = new Set(); + const visit = (items: TreeNode[]) => { + for (const node of items) { + if (node.children.length > 0) ids.add(node.row.id); + visit(node.children); + } + }; + visit(nodes); + return ids; +} diff --git a/docs/test/compatibility-matrix.test.mjs b/docs/test/compatibility-matrix.test.mjs new file mode 100644 index 000000000..abd3df945 --- /dev/null +++ b/docs/test/compatibility-matrix.test.mjs @@ -0,0 +1,115 @@ +import assert from "node:assert/strict"; +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { test } from "node:test"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import { createElement } from "react"; +import { renderToStaticMarkup } from "react-dom/server"; +import ts from "typescript"; +import { + buildTree, + expandableIds, + filterTree, + flattenTree, + nodeCount, + rowMatcher, + searchTexts, + statusCounts, +} from "../src/lib/compatibility-tree.ts"; + +const row = (id, depth, label, status, detail = "") => ({ + id, + chapter: "zlib", + kind: depth === 0 ? "module" : "method", + name: label, + signature: label, + label, + apiSymbol: label, + depth, + scope: "api", + anchor: "", + nodeStability: null, + static: { status, detail, verification: "test-backed" }, +}); +const rows = [ + row("root", 0, "zlib", "partial"), + row("a", 1, "zlib.gzipSync()", "supported", "Lowered with Node's options"), + row("b", 1, "zlib.Gzip", "partial"), + row("b1", 2, "gzip.flush()", "supported"), + row("c", 1, "zlib.zstdCompress()", "not-implemented"), +]; +const titles = new Map([["zlib", "Zlib"]]); + +test("the tree follows the rows' heading depths", () => { + const tree = buildTree(rows); + assert.equal(tree.length, 1); + assert.deepEqual(tree[0].children.map((node) => node.row.id), ["a", "b", "c"]); + assert.deepEqual(tree[0].children[1].children.map((node) => node.row.id), ["b1"]); + assert.equal(nodeCount(tree), 5); + assert.deepEqual([...expandableIds(tree)].sort(), ["b", "root"]); +}); + +test("filtering keeps matching rows with their ancestors and counts the matches once", () => { + const tree = buildTree(rows); + const texts = searchTexts(rows, titles); + const byText = filterTree(tree, rowMatcher(texts, "flush", "all")); + assert.equal(byText.matched, 1); + assert.deepEqual(flattenTree(byText.nodes, new Set(), true).map(({ node, level }) => `${node.row.id}@${level}`), ["root@1", "b@2", "b1@3"]); + const byDetail = filterTree(tree, rowMatcher(texts, "node's options", "all")); + assert.deepEqual(flattenTree(byDetail.nodes, new Set(), true).map(({ node }) => node.row.id), ["root", "a"]); + const byStatus = filterTree(tree, rowMatcher(texts, "", "supported")); + assert.equal(byStatus.matched, 2); + const both = filterTree(tree, rowMatcher(texts, "gzip", "partial")); + assert.equal(both.matched, 1); + assert.equal(filterTree(tree, rowMatcher(texts, "brotli", "all")).nodes.length, 0); +}); + +test("collapsed rows hide their children until expanded", () => { + const tree = buildTree(rows); + assert.deepEqual(flattenTree(tree, new Set(), false).map(({ node }) => node.row.id), ["root"]); + assert.deepEqual(flattenTree(tree, new Set(["root"]), false).map(({ node }) => node.row.id), ["root", "a", "b", "c"]); + assert.deepEqual(flattenTree(tree, new Set(["root", "b"]), false).map(({ node }) => node.row.id), ["root", "a", "b", "b1", "c"]); +}); + +test("status counts cover API rows below the chapters", () => { + const counts = statusCounts(rows); + assert.equal(counts.supported, 2); + assert.equal(counts.partial, 1); + assert.equal(counts["not-implemented"], 1); +}); + +/** The matrix component, compiled for Node from its TSX source. */ +async function loadMatrix() { + const docs = new URL("../", import.meta.url); + const source = await readFile(new URL("src/components/node-compatibility-matrix.tsx", docs), "utf8"); + const { outputText } = ts.transpileModule(source, { + compilerOptions: { jsx: ts.JsxEmit.ReactJSX, module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022 }, + }); + const lib = pathToFileURL(fileURLToPath(new URL("src/lib/compatibility-tree.ts", docs))).href; + const dir = new URL("node_modules/.cache/compatibility-matrix-test/", docs); + await mkdir(dir, { recursive: true }); + const file = new URL("node-compatibility-matrix.mjs", dir); + await writeFile(file, outputText.replace('"@/lib/compatibility-tree"', JSON.stringify(lib))); + return import(file.href); +} + +test("badges render without their tooltips until hovered or focused", async () => { + const { StatusBadge, StabilityBadge } = await loadMatrix(); + const status = renderToStaticMarkup( + createElement(StatusBadge, { + status: "partial", + detail: "Lowered with Node's options", + verification: { label: "Test-backed", detail: "Corpus programs cover it." }, + }), + ); + assert.ok(status.includes('tabindex="0"'), status); + assert.ok(status.includes('title="Lowered with Node's options'), status); + assert.ok(!status.includes('role="tooltip"'), status); + assert.ok(!status.includes("aria-describedby"), status); + const stability = renderToStaticMarkup( + createElement(StabilityBadge, { stability: { index: "2", level: 2, text: "Stable", inherited: false } }), + ); + assert.ok(stability.includes('tabindex="0"'), stability); + assert.ok(!stability.includes('role="tooltip"'), stability); + const legend = renderToStaticMarkup(createElement(StatusBadge, { status: "supported" })); + assert.ok(!legend.includes("tabindex"), legend); +}); diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index ba8692b49..aac6cfb14 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -1387,7 +1387,20 @@ "evidence": "derived:descendants", "tests": [ "tests/corpus/2213-async-local-storage.cjs", - "tests/corpus/5815-als-callback-context.ts" + "tests/corpus/5815-als-callback-context.ts", + "tests/corpus/7110-als-completion-callbacks.ts", + "tests/corpus/7111-als-bind-snapshot-options.ts", + "tests/corpus/7112-als-bind-snapshot-dynamic.mjs", + "tests/corpus/7119-als-net-event-context.mjs", + "tests/corpus/7380-als-http-event-context.mjs", + "tests/corpus/7383-als-stream-events.ts", + "tests/corpus/7384-als-stream-events.mjs", + "tests/corpus/7385-als-child-process-events.ts", + "tests/corpus/7386-als-child-fork-events/main.mjs", + "tests/corpus/7387-als-http2-events.mjs", + "tests/corpus/7388-als-http2-compat-events.mjs", + "tests/corpus/7389-als-http2-secure-events.mjs", + "tests/corpus/7391-als-response-end-and-finished.mjs" ] }, "anchorSource": "chapter" @@ -1437,7 +1450,20 @@ "evidence": "compiler-dedicated:AsyncLocalStorage", "tests": [ "tests/corpus/2213-async-local-storage.cjs", - "tests/corpus/5815-als-callback-context.ts" + "tests/corpus/5815-als-callback-context.ts", + "tests/corpus/7110-als-completion-callbacks.ts", + "tests/corpus/7111-als-bind-snapshot-options.ts", + "tests/corpus/7112-als-bind-snapshot-dynamic.mjs", + "tests/corpus/7119-als-net-event-context.mjs", + "tests/corpus/7380-als-http-event-context.mjs", + "tests/corpus/7383-als-stream-events.ts", + "tests/corpus/7384-als-stream-events.mjs", + "tests/corpus/7385-als-child-process-events.ts", + "tests/corpus/7386-als-child-fork-events/main.mjs", + "tests/corpus/7387-als-http2-events.mjs", + "tests/corpus/7388-als-http2-compat-events.mjs", + "tests/corpus/7389-als-http2-secure-events.mjs", + "tests/corpus/7391-als-response-end-and-finished.mjs" ] }, "anchorSource": "exact" @@ -1510,7 +1536,20 @@ "evidence": "compiler-dedicated:asyncLocalStorage.disable", "tests": [ "tests/corpus/2213-async-local-storage.cjs", - "tests/corpus/5815-als-callback-context.ts" + "tests/corpus/5815-als-callback-context.ts", + "tests/corpus/7110-als-completion-callbacks.ts", + "tests/corpus/7111-als-bind-snapshot-options.ts", + "tests/corpus/7112-als-bind-snapshot-dynamic.mjs", + "tests/corpus/7119-als-net-event-context.mjs", + "tests/corpus/7380-als-http-event-context.mjs", + "tests/corpus/7383-als-stream-events.ts", + "tests/corpus/7384-als-stream-events.mjs", + "tests/corpus/7385-als-child-process-events.ts", + "tests/corpus/7386-als-child-fork-events/main.mjs", + "tests/corpus/7387-als-http2-events.mjs", + "tests/corpus/7388-als-http2-compat-events.mjs", + "tests/corpus/7389-als-http2-secure-events.mjs", + "tests/corpus/7391-als-response-end-and-finished.mjs" ] }, "anchorSource": "exact" @@ -1537,7 +1576,20 @@ "evidence": "compiler-dedicated:asyncLocalStorage.getStore", "tests": [ "tests/corpus/2213-async-local-storage.cjs", - "tests/corpus/5815-als-callback-context.ts" + "tests/corpus/5815-als-callback-context.ts", + "tests/corpus/7110-als-completion-callbacks.ts", + "tests/corpus/7111-als-bind-snapshot-options.ts", + "tests/corpus/7112-als-bind-snapshot-dynamic.mjs", + "tests/corpus/7119-als-net-event-context.mjs", + "tests/corpus/7380-als-http-event-context.mjs", + "tests/corpus/7383-als-stream-events.ts", + "tests/corpus/7384-als-stream-events.mjs", + "tests/corpus/7385-als-child-process-events.ts", + "tests/corpus/7386-als-child-fork-events/main.mjs", + "tests/corpus/7387-als-http2-events.mjs", + "tests/corpus/7388-als-http2-compat-events.mjs", + "tests/corpus/7389-als-http2-secure-events.mjs", + "tests/corpus/7391-als-response-end-and-finished.mjs" ] }, "anchorSource": "exact" @@ -1564,7 +1616,20 @@ "evidence": "compiler-dedicated:asyncLocalStorage.enterWith", "tests": [ "tests/corpus/2213-async-local-storage.cjs", - "tests/corpus/5815-als-callback-context.ts" + "tests/corpus/5815-als-callback-context.ts", + "tests/corpus/7110-als-completion-callbacks.ts", + "tests/corpus/7111-als-bind-snapshot-options.ts", + "tests/corpus/7112-als-bind-snapshot-dynamic.mjs", + "tests/corpus/7119-als-net-event-context.mjs", + "tests/corpus/7380-als-http-event-context.mjs", + "tests/corpus/7383-als-stream-events.ts", + "tests/corpus/7384-als-stream-events.mjs", + "tests/corpus/7385-als-child-process-events.ts", + "tests/corpus/7386-als-child-fork-events/main.mjs", + "tests/corpus/7387-als-http2-events.mjs", + "tests/corpus/7388-als-http2-compat-events.mjs", + "tests/corpus/7389-als-http2-secure-events.mjs", + "tests/corpus/7391-als-response-end-and-finished.mjs" ] }, "anchorSource": "exact" @@ -1591,7 +1656,20 @@ "evidence": "compiler-dedicated:asyncLocalStorage.run", "tests": [ "tests/corpus/2213-async-local-storage.cjs", - "tests/corpus/5815-als-callback-context.ts" + "tests/corpus/5815-als-callback-context.ts", + "tests/corpus/7110-als-completion-callbacks.ts", + "tests/corpus/7111-als-bind-snapshot-options.ts", + "tests/corpus/7112-als-bind-snapshot-dynamic.mjs", + "tests/corpus/7119-als-net-event-context.mjs", + "tests/corpus/7380-als-http-event-context.mjs", + "tests/corpus/7383-als-stream-events.ts", + "tests/corpus/7384-als-stream-events.mjs", + "tests/corpus/7385-als-child-process-events.ts", + "tests/corpus/7386-als-child-fork-events/main.mjs", + "tests/corpus/7387-als-http2-events.mjs", + "tests/corpus/7388-als-http2-compat-events.mjs", + "tests/corpus/7389-als-http2-secure-events.mjs", + "tests/corpus/7391-als-response-end-and-finished.mjs" ] }, "anchorSource": "exact" @@ -1618,7 +1696,20 @@ "evidence": "compiler-dedicated:asyncLocalStorage.exit", "tests": [ "tests/corpus/2213-async-local-storage.cjs", - "tests/corpus/5815-als-callback-context.ts" + "tests/corpus/5815-als-callback-context.ts", + "tests/corpus/7110-als-completion-callbacks.ts", + "tests/corpus/7111-als-bind-snapshot-options.ts", + "tests/corpus/7112-als-bind-snapshot-dynamic.mjs", + "tests/corpus/7119-als-net-event-context.mjs", + "tests/corpus/7380-als-http-event-context.mjs", + "tests/corpus/7383-als-stream-events.ts", + "tests/corpus/7384-als-stream-events.mjs", + "tests/corpus/7385-als-child-process-events.ts", + "tests/corpus/7386-als-child-fork-events/main.mjs", + "tests/corpus/7387-als-http2-events.mjs", + "tests/corpus/7388-als-http2-compat-events.mjs", + "tests/corpus/7389-als-http2-secure-events.mjs", + "tests/corpus/7391-als-response-end-and-finished.mjs" ] }, "anchorSource": "exact" @@ -1922,6 +2013,19 @@ "tests": [ "tests/corpus/2213-async-local-storage.cjs", "tests/corpus/5815-als-callback-context.ts", + "tests/corpus/7110-als-completion-callbacks.ts", + "tests/corpus/7111-als-bind-snapshot-options.ts", + "tests/corpus/7112-als-bind-snapshot-dynamic.mjs", + "tests/corpus/7119-als-net-event-context.mjs", + "tests/corpus/7380-als-http-event-context.mjs", + "tests/corpus/7383-als-stream-events.ts", + "tests/corpus/7384-als-stream-events.mjs", + "tests/corpus/7385-als-child-process-events.ts", + "tests/corpus/7386-als-child-fork-events/main.mjs", + "tests/corpus/7387-als-http2-events.mjs", + "tests/corpus/7388-als-http2-compat-events.mjs", + "tests/corpus/7389-als-http2-secure-events.mjs", + "tests/corpus/7391-als-response-end-and-finished.mjs", "tests/harness/node-test.test.ts" ] }, @@ -2484,7 +2588,20 @@ "evidence": "compiler-dedicated:AsyncLocalStorage", "tests": [ "tests/corpus/2213-async-local-storage.cjs", - "tests/corpus/5815-als-callback-context.ts" + "tests/corpus/5815-als-callback-context.ts", + "tests/corpus/7110-als-completion-callbacks.ts", + "tests/corpus/7111-als-bind-snapshot-options.ts", + "tests/corpus/7112-als-bind-snapshot-dynamic.mjs", + "tests/corpus/7119-als-net-event-context.mjs", + "tests/corpus/7380-als-http-event-context.mjs", + "tests/corpus/7383-als-stream-events.ts", + "tests/corpus/7384-als-stream-events.mjs", + "tests/corpus/7385-als-child-process-events.ts", + "tests/corpus/7386-als-child-fork-events/main.mjs", + "tests/corpus/7387-als-http2-events.mjs", + "tests/corpus/7388-als-http2-compat-events.mjs", + "tests/corpus/7389-als-http2-secure-events.mjs", + "tests/corpus/7391-als-response-end-and-finished.mjs" ] }, "anchorSource": "exact" @@ -19277,6 +19394,8 @@ "tests": [ "tests/corpus/1358-crypto-random.ts", "tests/corpus/1534-crypto-hash-chain.ts", + "tests/corpus/1568-x509-fingerprint.ts", + "tests/corpus/1572-x509-validity-date-gettime.ts", "tests/corpus/2556-crypto-introspection.ts", "tests/corpus/2890-crypto-static-utility.ts", "tests/corpus/5250-cipher-aes-gcm.ts", @@ -19319,6 +19438,7 @@ "tests/corpus/6709-dh-generate.ts", "tests/corpus/6710-dh-encodings.cjs", "tests/corpus/6711-dh-handles.ts", + "tests/corpus/6936-x509-certificate-fields.ts", "tests/corpus/7055-cipher-streams.ts", "tests/corpus/7056-cipher-stream-errors.ts", "tests/corpus/7057-cipher-streams.mjs", @@ -19326,6 +19446,10 @@ "tests/corpus/7082-crypto-key-objects.ts", "tests/corpus/7083-crypto-sign-verify.ts", "tests/corpus/7084-crypto-key-pairs.cjs", + "tests/corpus/7090-x509-check-names.ts", + "tests/corpus/7091-x509-certificate-keys.ts", + "tests/corpus/7092-x509-legacy-object.ts", + "tests/corpus/7093-x509-methods-js.mjs", "tests/corpus/7122-webcrypto-node-crypto.ts", "tests/corpus/7123-webcrypto-node-crypto.cjs", "tests/corpus/7150-cipher-legacy-modes.ts", @@ -19344,6 +19468,7 @@ "tests/fixtures/node-types/dh.ts", "tests/fixtures/node-types/ecdh.ts", "tests/fixtures/node-types/keys.ts", + "tests/fixtures/node-types/x509-methods.ts", "tests/harness/npm-static.test.ts" ] }, @@ -20228,8 +20353,19 @@ "inherited": true }, "static": { - "status": "refused", - "evidence": "surface-manifest:unsupported:node-builtin.crypto.diffieHellman" + "status": "partial", + "evidence": "surface-manifest:node-builtin.crypto.diffieHellman", + "tests": [ + "tests/corpus/1358-crypto-random.ts", + "tests/corpus/1534-crypto-hash-chain.ts", + "tests/corpus/2556-crypto-introspection.ts", + "tests/corpus/2890-crypto-static-utility.ts", + "tests/harness/npm-static.test.ts", + "tests/corpus/node24-crypto-sha2.ts", + "tests/corpus/node24-crypto-kdf.ts", + "tests/corpus/node24-crypto-kdf-errors.ts", + "tests/corpus/node24-crypto-kdf-options.ts" + ] }, "anchorSource": "exact" }, @@ -23656,8 +23792,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "compiler-dedicated:X509Certificate", + "tests": [ + "tests/corpus/1568-x509-fingerprint.ts", + "tests/corpus/1572-x509-validity-date-gettime.ts", + "tests/corpus/6936-x509-certificate-fields.ts" + ] }, "anchorSource": "exact" }, @@ -23679,8 +23820,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "compiler-dedicated:x509.checkEmail", + "tests": [ + "tests/corpus/7090-x509-check-names.ts", + "tests/corpus/7091-x509-certificate-keys.ts", + "tests/corpus/7092-x509-legacy-object.ts", + "tests/corpus/7093-x509-methods-js.mjs", + "tests/fixtures/node-types/x509-methods.ts" + ] }, "anchorSource": "exact" }, @@ -23702,8 +23850,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "compiler-dedicated:x509.checkHost", + "tests": [ + "tests/corpus/7090-x509-check-names.ts", + "tests/corpus/7091-x509-certificate-keys.ts", + "tests/corpus/7092-x509-legacy-object.ts", + "tests/corpus/7093-x509-methods-js.mjs", + "tests/fixtures/node-types/x509-methods.ts" + ] }, "anchorSource": "exact" }, @@ -23725,8 +23880,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "compiler-dedicated:x509.checkIP", + "tests": [ + "tests/corpus/7090-x509-check-names.ts", + "tests/corpus/7091-x509-certificate-keys.ts", + "tests/corpus/7092-x509-legacy-object.ts", + "tests/corpus/7093-x509-methods-js.mjs", + "tests/fixtures/node-types/x509-methods.ts" + ] }, "anchorSource": "exact" }, @@ -23748,8 +23910,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "compiler-dedicated:x509.checkIssued", + "tests": [ + "tests/corpus/7090-x509-check-names.ts", + "tests/corpus/7091-x509-certificate-keys.ts", + "tests/corpus/7092-x509-legacy-object.ts", + "tests/corpus/7093-x509-methods-js.mjs", + "tests/fixtures/node-types/x509-methods.ts" + ] }, "anchorSource": "exact" }, @@ -23771,8 +23940,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "compiler-dedicated:x509.checkPrivateKey", + "tests": [ + "tests/corpus/7090-x509-check-names.ts", + "tests/corpus/7091-x509-certificate-keys.ts", + "tests/corpus/7092-x509-legacy-object.ts", + "tests/corpus/7093-x509-methods-js.mjs", + "tests/fixtures/node-types/x509-methods.ts" + ] }, "anchorSource": "exact" }, @@ -23794,8 +23970,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "compiler-dedicated:x509.toJSON", + "tests": [ + "tests/corpus/7090-x509-check-names.ts", + "tests/corpus/7091-x509-certificate-keys.ts", + "tests/corpus/7092-x509-legacy-object.ts", + "tests/corpus/7093-x509-methods-js.mjs", + "tests/fixtures/node-types/x509-methods.ts" + ] }, "anchorSource": "exact" }, @@ -23817,8 +24000,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "compiler-dedicated:x509.toLegacyObject", + "tests": [ + "tests/corpus/7090-x509-check-names.ts", + "tests/corpus/7091-x509-certificate-keys.ts", + "tests/corpus/7092-x509-legacy-object.ts", + "tests/corpus/7093-x509-methods-js.mjs", + "tests/fixtures/node-types/x509-methods.ts" + ] }, "anchorSource": "exact" }, @@ -23840,8 +24030,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "compiler-dedicated:x509.toString", + "tests": [ + "tests/corpus/7090-x509-check-names.ts", + "tests/corpus/7091-x509-certificate-keys.ts", + "tests/corpus/7092-x509-legacy-object.ts", + "tests/corpus/7093-x509-methods-js.mjs", + "tests/fixtures/node-types/x509-methods.ts" + ] }, "anchorSource": "exact" }, @@ -23863,8 +24060,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "compiler-dedicated:x509.verify", + "tests": [ + "tests/corpus/7090-x509-check-names.ts", + "tests/corpus/7091-x509-certificate-keys.ts", + "tests/corpus/7092-x509-legacy-object.ts", + "tests/corpus/7093-x509-methods-js.mjs", + "tests/fixtures/node-types/x509-methods.ts" + ] }, "anchorSource": "exact" }, @@ -24070,8 +24274,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "compiler-feature:crypto.publicKey", + "tests": [ + "tests/corpus/7090-x509-check-names.ts", + "tests/corpus/7091-x509-certificate-keys.ts", + "tests/corpus/7092-x509-legacy-object.ts", + "tests/corpus/7093-x509-methods-js.mjs", + "tests/fixtures/node-types/x509-methods.ts" + ] }, "anchorSource": "ancestor" }, @@ -25595,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" }, @@ -25883,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" }, @@ -32551,6 +32770,10 @@ "tests": [ "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", "tests/corpus/error-stack-limit.ts", @@ -33343,8 +33566,11 @@ "anchor": "err_brotli_invalid_param", "nodeStability": null, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:errors" + "status": "partial", + "evidence": "compiler-feature:errors.ERR_BROTLI_INVALID_PARAM", + "tests": [ + "tests/corpus/7714-zlib-brotli-compress.cjs" + ] }, "anchorSource": "exact" }, @@ -36472,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" }, @@ -38610,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" }, @@ -38880,8 +39112,11 @@ "anchor": "err_tls_required_server_name", "nodeStability": null, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:errors" + "status": "partial", + "evidence": "compiler-dedicated:ERR_TLS_REQUIRED_SERVER_NAME", + "tests": [ + "tests/corpus/7440-tls-server-add-context.ts" + ] }, "anchorSource": "exact" }, @@ -52194,6 +52429,9 @@ "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs", "tests/corpus/6331-jose-root-entry/main.ts", + "tests/corpus/7743-transform-stream-identity.ts", + "tests/corpus/7747-text-codec-untyped-slot.js", + "tests/corpus/8162-navigator-global.cjs", "tests/corpus/text-codec-values/main.ts", "tests/corpus/webassembly-api.mjs", "tests/corpus/webassembly-dylink/main.mjs", @@ -53346,7 +53584,8 @@ "tests": [ "tests/corpus/1423-text-codec.ts", "tests/corpus/text-codec-values/main.ts", - "tests/corpus/3133-text-decoder-streaming.ts" + "tests/corpus/3133-text-decoder-streaming.ts", + "tests/corpus/7747-text-codec-untyped-slot.js" ] }, "anchorSource": "exact" @@ -53397,7 +53636,8 @@ "tests": [ "tests/corpus/1423-text-codec.ts", "tests/corpus/text-codec-values/main.ts", - "tests/corpus/3133-text-decoder-streaming.ts" + "tests/corpus/3133-text-decoder-streaming.ts", + "tests/corpus/7747-text-codec-untyped-slot.js" ] }, "anchorSource": "exact" @@ -53443,8 +53683,11 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:globals" + "status": "partial", + "evidence": "compiler-feature:globals.TransformStream", + "tests": [ + "tests/corpus/7743-transform-stream-identity.ts" + ] }, "anchorSource": "exact" }, @@ -54241,6 +54484,7 @@ "status": "partial", "evidence": "compiler-feature:globals.navigator", "tests": [ + "tests/corpus/8162-navigator-global.cjs", "tests/fixtures/node-types/navigator.ts", "tests/harness/project-config.test.ts", "tests/corpus/6331-jose-root-entry/main.ts" @@ -54266,8 +54510,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:globals" + "status": "supported", + "evidence": "compiler-feature:globals.hardwareConcurrency", + "tests": [ + "tests/corpus/8162-navigator-global.cjs", + "tests/fixtures/node-types/navigator.ts" + ] }, "anchorSource": "ancestor" }, @@ -54289,8 +54537,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:globals" + "status": "partial", + "evidence": "compiler-feature:globals.language", + "tests": [ + "tests/corpus/8162-navigator-global.cjs", + "tests/fixtures/node-types/navigator.ts" + ] }, "anchorSource": "ancestor" }, @@ -54312,8 +54564,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:globals" + "status": "partial", + "evidence": "compiler-feature:globals.languages", + "tests": [ + "tests/corpus/8162-navigator-global.cjs", + "tests/fixtures/node-types/navigator.ts" + ] }, "anchorSource": "ancestor" }, @@ -54358,8 +54614,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:globals" + "status": "supported", + "evidence": "compiler-feature:globals.platform", + "tests": [ + "tests/corpus/8162-navigator-global.cjs", + "tests/fixtures/node-types/navigator.ts" + ] }, "anchorSource": "ancestor" }, @@ -95738,6 +95998,17 @@ "tests/corpus/6722-tls-min-max-version.ts", "tests/corpus/7087-tls-socket-wrap.ts", "tests/corpus/7088-tls-socket-connect-tunnel.ts", + "tests/corpus/7095-tls-peer-certificates.ts", + "tests/corpus/7096-tls-peer-certificates.mjs", + "tests/corpus/7097-tls-client-certificates.ts", + "tests/corpus/7098-tls-client-certificates-paths.ts", + "tests/corpus/7099-tls-client-certificates.mjs", + "tests/corpus/7440-tls-server-add-context.ts", + "tests/corpus/7441-tls-client-error.ts", + "tests/corpus/7442-tls-server-contexts.mjs", + "tests/fixtures/node-types/tls-client-certificates.ts", + "tests/fixtures/node-types/tls-peer-certificates.ts", + "tests/fixtures/node-types/tls-server-contexts.ts", "tests/fixtures/node-types/tls-socket-wrap.ts" ] }, @@ -96106,8 +96377,14 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:tls" + "status": "partial", + "evidence": "compiler-dedicated:server.addContext", + "tests": [ + "tests/corpus/7440-tls-server-add-context.ts", + "tests/corpus/7441-tls-client-error.ts", + "tests/corpus/7442-tls-server-contexts.mjs", + "tests/fixtures/node-types/tls-server-contexts.ts" + ] }, "anchorSource": "exact" }, @@ -96233,8 +96510,14 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:tls" + "status": "partial", + "evidence": "compiler-dedicated:server.setSecureContext", + "tests": [ + "tests/corpus/7440-tls-server-add-context.ts", + "tests/corpus/7441-tls-client-error.ts", + "tests/corpus/7442-tls-server-contexts.mjs", + "tests/fixtures/node-types/tls-server-contexts.ts" + ] }, "anchorSource": "exact" }, @@ -96417,8 +96700,14 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:tls" + "status": "partial", + "evidence": "compiler-dedicated:'tlsClientError'", + "tests": [ + "tests/corpus/7440-tls-server-add-context.ts", + "tests/corpus/7441-tls-client-error.ts", + "tests/corpus/7442-tls-server-contexts.mjs", + "tests/fixtures/node-types/tls-server-contexts.ts" + ] }, "anchorSource": "exact" }, @@ -96560,8 +96849,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:tls" + "status": "partial", + "evidence": "compiler-dedicated:tlsSocket.getCertificate", + "tests": [ + "tests/corpus/7095-tls-peer-certificates.ts", + "tests/corpus/7096-tls-peer-certificates.mjs", + "tests/fixtures/node-types/tls-peer-certificates.ts" + ] }, "anchorSource": "exact" }, @@ -96652,8 +96946,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:tls" + "status": "partial", + "evidence": "compiler-dedicated:tlsSocket.getPeerCertificate", + "tests": [ + "tests/corpus/7095-tls-peer-certificates.ts", + "tests/corpus/7096-tls-peer-certificates.mjs", + "tests/fixtures/node-types/tls-peer-certificates.ts" + ] }, "anchorSource": "exact" }, @@ -96721,8 +97020,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:tls" + "status": "partial", + "evidence": "compiler-dedicated:tlsSocket.getPeerX509Certificate", + "tests": [ + "tests/corpus/7095-tls-peer-certificates.ts", + "tests/corpus/7096-tls-peer-certificates.mjs", + "tests/fixtures/node-types/tls-peer-certificates.ts" + ] }, "anchorSource": "exact" }, @@ -96839,8 +97143,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:tls" + "status": "partial", + "evidence": "compiler-dedicated:tlsSocket.getX509Certificate", + "tests": [ + "tests/corpus/7095-tls-peer-certificates.ts", + "tests/corpus/7096-tls-peer-certificates.mjs", + "tests/fixtures/node-types/tls-peer-certificates.ts" + ] }, "anchorSource": "exact" }, @@ -96980,8 +97289,14 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:tls" + "status": "partial", + "evidence": "compiler-dedicated:tlsSocket.authorizationError", + "tests": [ + "tests/corpus/7097-tls-client-certificates.ts", + "tests/corpus/7098-tls-client-certificates-paths.ts", + "tests/corpus/7099-tls-client-certificates.mjs", + "tests/fixtures/node-types/tls-client-certificates.ts" + ] }, "anchorSource": "exact" }, @@ -97003,8 +97318,14 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:tls" + "status": "partial", + "evidence": "compiler-feature:tls.authorized", + "tests": [ + "tests/corpus/7097-tls-client-certificates.ts", + "tests/corpus/7098-tls-client-certificates-paths.ts", + "tests/corpus/7099-tls-client-certificates.mjs", + "tests/fixtures/node-types/tls-client-certificates.ts" + ] }, "anchorSource": "ancestor" }, @@ -99348,6 +99669,7 @@ "evidence": "derived:descendants", "tests": [ "packages/runtime/test/url-legacy.test.ts", + "tests/corpus/1794-searchparams-url-live.ts", "tests/corpus/5220-url-parse-warnings.ts", "tests/corpus/5221-url-parse-shapes.ts", "tests/corpus/5222-url-parse-hosts.ts", @@ -100606,8 +100928,11 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:url" + "status": "partial", + "evidence": "surface-manifest:node-builtin.url.urlToHttpOptions", + "tests": [ + "tests/corpus/1794-searchparams-url-live.ts" + ] }, "anchorSource": "exact" }, @@ -101277,6 +101602,26 @@ "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", "tests/corpus/node24-deep-equality.ts", @@ -101362,8 +101707,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" }, @@ -101385,8 +101734,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" }, @@ -101408,8 +101761,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" }, @@ -101431,8 +101788,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" }, @@ -101454,8 +101815,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" }, @@ -101477,8 +101842,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" }, @@ -101592,8 +101961,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" }, @@ -101615,8 +101988,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" }, @@ -101638,8 +102015,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" }, @@ -101661,8 +102042,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" }, @@ -101684,8 +102069,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" }, @@ -101707,8 +102096,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" }, @@ -101730,8 +102123,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" }, @@ -101753,8 +102150,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" }, @@ -101776,8 +102177,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" }, @@ -101805,7 +102210,8 @@ "tests/corpus/text-codec-values/main.ts", "tests/fixtures/node-types/text-codecs.mts", "tests/harness/project-config.test.ts", - "tests/corpus/3133-text-decoder-streaming.ts" + "tests/corpus/3133-text-decoder-streaming.ts", + "tests/corpus/7747-text-codec-untyped-slot.js" ] }, "anchorSource": "exact" @@ -101926,7 +102332,8 @@ "tests/corpus/text-codec-values/main.ts", "tests/fixtures/node-types/text-codecs.mts", "tests/harness/project-config.test.ts", - "tests/corpus/3133-text-decoder-streaming.ts" + "tests/corpus/3133-text-decoder-streaming.ts", + "tests/corpus/7747-text-codec-untyped-slot.js" ] }, "anchorSource": "exact" @@ -102036,7 +102443,8 @@ "tests/corpus/text-codec-values/main.ts", "tests/fixtures/node-types/text-codecs.mts", "tests/harness/project-config.test.ts", - "tests/corpus/3133-text-decoder-streaming.ts" + "tests/corpus/3133-text-decoder-streaming.ts", + "tests/corpus/7747-text-codec-untyped-slot.js" ] }, "anchorSource": "exact" @@ -102065,7 +102473,8 @@ "tests/corpus/text-codec-values/main.ts", "tests/fixtures/node-types/text-codecs.mts", "tests/harness/project-config.test.ts", - "tests/corpus/3133-text-decoder-streaming.ts" + "tests/corpus/3133-text-decoder-streaming.ts", + "tests/corpus/7747-text-codec-untyped-slot.js" ] }, "anchorSource": "exact" @@ -102172,8 +102581,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" }, @@ -102307,8 +102719,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" }, @@ -102330,8 +102746,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" }, @@ -102353,8 +102773,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" }, @@ -102381,7 +102805,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" @@ -102409,7 +102835,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" @@ -102600,7 +103028,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" @@ -102623,8 +103052,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" }, @@ -102646,8 +103078,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" }, @@ -102964,6 +103400,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" ] }, @@ -103179,8 +103617,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" }, @@ -111483,6 +111925,7 @@ "tests/corpus/2630-stream-consumers-errors.ts", "tests/corpus/2862-web-readable-async-iteration.ts", "tests/corpus/4875-stream-consumers-array-buffer.ts", + "tests/corpus/7743-transform-stream-identity.ts", "tests/corpus/stream-web-readable.ts", "tests/corpus/stream-web-require.cjs", "tests/harness/fetch-conformance.test.ts" @@ -112957,8 +113400,11 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webstreams" + "status": "partial", + "evidence": "compiler-feature:webstreams.TransformStream", + "tests": [ + "tests/corpus/7743-transform-stream-identity.ts" + ] }, "anchorSource": "exact" }, @@ -115612,7 +116058,10 @@ "tests/corpus/5377-zlib-stream-backpressure.cjs", "tests/corpus/5378-zlib-sync-options.cjs", "tests/corpus/5379-zlib-callback-options.ts", - "tests/corpus/5415-zlib-dynamic-inputs.cjs" + "tests/corpus/5415-zlib-dynamic-inputs.cjs", + "tests/corpus/7714-zlib-brotli-compress.cjs", + "tests/corpus/7715-zlib-brotli-decompress.cjs", + "tests/corpus/7716-zlib-brotli.ts" ] }, "anchorSource": "chapter" @@ -116095,8 +116544,11 @@ "inherited": true }, "static": { - "status": "refused", - "evidence": "surface-manifest:unsupported:node-builtin.zlib.BrotliCompress" + "status": "partial", + "evidence": "compiler-feature:zlib.zlib.BrotliCompress", + "tests": [ + "tests/corpus/7714-zlib-brotli-compress.cjs" + ] }, "anchorSource": "exact" }, @@ -116118,8 +116570,12 @@ "inherited": true }, "static": { - "status": "refused", - "evidence": "surface-manifest:unsupported:node-builtin.zlib.BrotliDecompress" + "status": "partial", + "evidence": "compiler-feature:zlib.zlib.BrotliDecompress", + "tests": [ + "tests/corpus/7715-zlib-brotli-decompress.cjs", + "tests/corpus/7716-zlib-brotli.ts" + ] }, "anchorSource": "exact" }, @@ -116637,8 +117093,11 @@ "inherited": true }, "static": { - "status": "refused", - "evidence": "surface-manifest:unsupported:node-builtin.zlib.createBrotliCompress" + "status": "partial", + "evidence": "surface-manifest:node-builtin.zlib.createBrotliCompress", + "tests": [ + "tests/corpus/7714-zlib-brotli-compress.cjs" + ] }, "anchorSource": "exact" }, @@ -116660,8 +117119,12 @@ "inherited": true }, "static": { - "status": "refused", - "evidence": "surface-manifest:unsupported:node-builtin.zlib.createBrotliDecompress" + "status": "partial", + "evidence": "surface-manifest:node-builtin.zlib.createBrotliDecompress", + "tests": [ + "tests/corpus/7715-zlib-brotli-decompress.cjs", + "tests/corpus/7716-zlib-brotli.ts" + ] }, "anchorSource": "exact" }, @@ -116967,8 +117430,11 @@ "inherited": true }, "static": { - "status": "refused", - "evidence": "surface-manifest:unsupported:node-builtin.zlib.brotliCompress" + "status": "partial", + "evidence": "surface-manifest:node-builtin.zlib.brotliCompress", + "tests": [ + "tests/corpus/7714-zlib-brotli-compress.cjs" + ] }, "anchorSource": "exact" }, @@ -116990,8 +117456,11 @@ "inherited": true }, "static": { - "status": "refused", - "evidence": "surface-manifest:unsupported:node-builtin.zlib.brotliCompressSync" + "status": "partial", + "evidence": "surface-manifest:node-builtin.zlib.brotliCompressSync", + "tests": [ + "tests/corpus/7714-zlib-brotli-compress.cjs" + ] }, "anchorSource": "exact" }, @@ -117013,8 +117482,12 @@ "inherited": true }, "static": { - "status": "refused", - "evidence": "surface-manifest:unsupported:node-builtin.zlib.brotliDecompress" + "status": "partial", + "evidence": "surface-manifest:node-builtin.zlib.brotliDecompress", + "tests": [ + "tests/corpus/7715-zlib-brotli-decompress.cjs", + "tests/corpus/7716-zlib-brotli.ts" + ] }, "anchorSource": "exact" }, @@ -117036,8 +117509,12 @@ "inherited": true }, "static": { - "status": "refused", - "evidence": "surface-manifest:unsupported:node-builtin.zlib.brotliDecompressSync" + "status": "partial", + "evidence": "surface-manifest:node-builtin.zlib.brotliDecompressSync", + "tests": [ + "tests/corpus/7715-zlib-brotli-decompress.cjs", + "tests/corpus/7716-zlib-brotli.ts" + ] }, "anchorSource": "exact" }, @@ -118054,8 +118531,11 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:zlib" + "status": "partial", + "evidence": "compiler-feature:zlib.BrotliOptions", + "tests": [ + "tests/corpus/7714-zlib-brotli-compress.cjs" + ] }, "anchorSource": "exact" }, @@ -118123,8 +118603,11 @@ "inherited": true }, "static": { - "status": "refused", - "evidence": "surface-manifest:unsupported:node-builtin.zlib.brotliCompress" + "status": "partial", + "evidence": "surface-manifest:node-builtin.zlib.brotliCompress", + "tests": [ + "tests/corpus/7714-zlib-brotli-compress.cjs" + ] }, "anchorSource": "ancestor" }, @@ -118146,8 +118629,11 @@ "inherited": true }, "static": { - "status": "refused", - "evidence": "surface-manifest:unsupported:node-builtin.zlib.brotliCompressSync" + "status": "partial", + "evidence": "surface-manifest:node-builtin.zlib.brotliCompressSync", + "tests": [ + "tests/corpus/7714-zlib-brotli-compress.cjs" + ] }, "anchorSource": "ancestor" }, @@ -118169,8 +118655,12 @@ "inherited": true }, "static": { - "status": "refused", - "evidence": "surface-manifest:unsupported:node-builtin.zlib.brotliDecompress" + "status": "partial", + "evidence": "surface-manifest:node-builtin.zlib.brotliDecompress", + "tests": [ + "tests/corpus/7715-zlib-brotli-decompress.cjs", + "tests/corpus/7716-zlib-brotli.ts" + ] }, "anchorSource": "ancestor" }, @@ -118192,8 +118682,12 @@ "inherited": true }, "static": { - "status": "refused", - "evidence": "surface-manifest:unsupported:node-builtin.zlib.brotliDecompressSync" + "status": "partial", + "evidence": "surface-manifest:node-builtin.zlib.brotliDecompressSync", + "tests": [ + "tests/corpus/7715-zlib-brotli-decompress.cjs", + "tests/corpus/7716-zlib-brotli.ts" + ] }, "anchorSource": "ancestor" }, diff --git a/internal/compatibility/src/generate.mjs b/internal/compatibility/src/generate.mjs index 662c0abd6..1b598fbd3 100644 --- a/internal/compatibility/src/generate.mjs +++ b/internal/compatibility/src/generate.mjs @@ -652,7 +652,14 @@ function publicDetail(tier) { source, ) ) { - return "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported."; + return "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported."; + } + if ( + /^compiler-feature:zlib\.(?:zlib\.(?:BrotliCompress|BrotliDecompress)|BrotliOptions)$/.test( + source, + ) + ) { + return "BrotliCompress and BrotliDecompress are Transform streams over the brotli version Node bundles, with Node's parameters, dictionaries, flush, reset, error codes, and option validation; like Node's, they have no params()."; } if (source === "compiler-feature:zlib.zlib.constants") { return "Each zlib.constants member and its module-level copy reads as Node's value; the constants object itself is not a runtime value."; diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index 100882c1a..033e199c7 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -187,9 +187,22 @@ "node-builtin.util.callbackify": ["tests/corpus/5190-util-callbackify.cjs", "tests/corpus/5191-util-callbackify.ts", "tests/fixtures/node-types/util-callables.ts"], "node-builtin.util.debuglog": ["tests/corpus/5192-util-debuglog.cjs", "tests/corpus/5193-util-debuglog.ts", "tests/fixtures/node-types/util-callables.ts"], "node-builtin.util.debug": ["tests/corpus/5192-util-debuglog.cjs", "tests/corpus/5193-util-debuglog.ts"], + "node-builtin.util.isArray": ["tests/corpus/7700-util-deprecated-helpers.cjs", "tests/corpus/7701-util-deprecated-helpers.ts"], + "node-builtin.util._extend": ["tests/corpus/7700-util-deprecated-helpers.cjs", "tests/corpus/7701-util-deprecated-helpers.ts"], + "node-builtin.util.convertProcessSignalToExitCode": ["tests/corpus/7702-util-signal-exit-code.cjs"], + "node-builtin.util.aborted": ["tests/corpus/7703-util-aborted.cjs", "tests/corpus/7704-util-aborted.ts"], + "node-builtin.util.diff": ["tests/corpus/7705-util-diff.cjs", "tests/corpus/7706-util-diff.ts"], + "node-builtin.util.debounce": ["tests/corpus/7710-util-debounce.cjs", "tests/corpus/7712-util-debounce-throttle.ts"], + "node-builtin.util.throttle": ["tests/corpus/7711-util-throttle.cjs", "tests/corpus/7712-util-debounce-throttle.ts"], "node-builtin.util": ["tests/corpus/2440-console-inspect-args.ts", "tests/corpus/2912-promisify-fs-readfile.ts"], "node-builtin.worker_threads": ["tests/corpus/957-builtins-namespace.ts", "tests/corpus/native-worker-messages.ts", "tests/corpus/native-worker-references.ts"], "node-builtin.zlib": ["tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", "tests/corpus/3115-zlib-static-compression-levels.ts", "tests/corpus/5370-zlib-stream-roundtrip.ts", "tests/corpus/5371-zlib-stream-chunking.cjs", "tests/corpus/5373-zlib-stream-options.cjs", "tests/corpus/5378-zlib-sync-options.cjs", "tests/corpus/5379-zlib-callback-options.ts", "tests/corpus/5415-zlib-dynamic-inputs.cjs"], + "node-builtin.zlib.brotliCompressSync": ["tests/corpus/7714-zlib-brotli-compress.cjs"], + "node-builtin.zlib.brotliCompress": ["tests/corpus/7714-zlib-brotli-compress.cjs"], + "node-builtin.zlib.createBrotliCompress": ["tests/corpus/7714-zlib-brotli-compress.cjs"], + "node-builtin.zlib.brotliDecompressSync": ["tests/corpus/7715-zlib-brotli-decompress.cjs", "tests/corpus/7716-zlib-brotli.ts"], + "node-builtin.zlib.brotliDecompress": ["tests/corpus/7715-zlib-brotli-decompress.cjs", "tests/corpus/7716-zlib-brotli.ts"], + "node-builtin.zlib.createBrotliDecompress": ["tests/corpus/7715-zlib-brotli-decompress.cjs", "tests/corpus/7716-zlib-brotli.ts"], "stdlib.fetch": ["tests/harness/fetch-conformance.test.ts"], "stdlib.map": ["tests/corpus/typed-collection-iterators.ts", "tests/corpus/typed-collection-iterator-references.ts", "tests/corpus/537-map-iter-drains.ts"], "stdlib.set": ["tests/corpus/typed-collection-iterators.ts", "tests/corpus/typed-collection-iterator-references.ts", "tests/corpus/checked-collection-iterators.ts"], @@ -202,13 +215,17 @@ "stdlib.abort": ["tests/harness/fetch-conformance.test.ts"] }, "features": [ + { "chapter": "tls", "parent": "Class: `tls.TLSSocket`", "signatures": ["`authorized` Type: {boolean}"], "status": "partial", "evidence": ["tests/corpus/7097-tls-client-certificates.ts", "tests/corpus/7098-tls-client-certificates-paths.ts", "tests/corpus/7099-tls-client-certificates.mjs", "tests/fixtures/node-types/tls-client-certificates.ts"] }, + { "chapter": "crypto", "parent": "Class: `X509Certificate`", "signatures": ["`publicKey` Type: {KeyObject}"], "status": "partial", "evidence": ["tests/corpus/7090-x509-check-names.ts", "tests/corpus/7091-x509-certificate-keys.ts", "tests/corpus/7092-x509-legacy-object.ts", "tests/corpus/7093-x509-methods-js.mjs", "tests/fixtures/node-types/x509-methods.ts"] }, { "chapter": "webcrypto", "symbols": ["subtle.importKey", "subtle.generateKey", "subtle.exportKey", "subtle.encrypt", "subtle.decrypt", "subtle.wrapKey", "subtle.unwrapKey", "subtle.deriveBits", "subtle.deriveKey", "subtle.sign", "subtle.verify", "subtle.digest"], "status": "partial", "evidence": ["tests/corpus/7130-webcrypto-wrap-key.ts", "tests/corpus/7131-webcrypto-wrap-key-errors.mjs", "tests/corpus/7132-jose-pbes2/main.mjs", "tests/corpus/7128-webcrypto-derive.ts", "tests/corpus/7129-webcrypto-derive-errors.mjs", "tests/corpus/7126-webcrypto-aes.ts", "tests/corpus/7127-webcrypto-aes-errors.mjs", "tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/7125-webcrypto-key-errors.mjs", "tests/fixtures/node-types/webcrypto-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs", "tests/fixtures/node-types/webcrypto-hmac.ts", "tests/corpus/7081-webcrypto-ed25519.mjs", "tests/corpus/7085-webcrypto-ecdsa-rsa.ts", "tests/corpus/7086-webcrypto-ecdsa-rsa-errors.mjs"] }, { "chapter": "webcrypto", "signatures": ["Class: `SubtleCrypto`", "`subtle` Type: {SubtleCrypto}"], "status": "partial", "evidence": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"] }, { "chapter": "webcrypto", "signatures": ["Class: `CryptoKey`", "`cryptoKey.algorithm`", "`extractable` Type: {boolean}", "`type` Type: {string} One of `'secret'`, `'private'`, or `'public'`.", "`usages` Type: {string\\[]}"], "status": "partial", "evidence": ["tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"] }, { "chapter": "webcrypto", "signatures": ["Class: `Crypto`"], "status": "partial", "evidence": ["tests/corpus/7120-webcrypto-random-uuid.ts", "tests/corpus/7121-webcrypto-crypto-object.mjs", "tests/corpus/6293-webcrypto-digest.ts"] }, { "chapter": "webcrypto", "symbols": ["crypto.randomUUID", "crypto.getRandomValues"], "status": "supported", "evidence": ["tests/corpus/7120-webcrypto-random-uuid.ts", "tests/corpus/7121-webcrypto-crypto-object.mjs", "tests/corpus/6204-crypto-get-random-values.ts", "tests/corpus/6205-crypto-get-random-values.js"] }, - { "chapter": "globals", "signatures": ["`navigator`"], "status": "partial", "evidence": ["tests/fixtures/node-types/navigator.ts", "tests/harness/project-config.test.ts", "tests/corpus/6331-jose-root-entry/main.ts"] }, + { "chapter": "globals", "signatures": ["`navigator`"], "status": "partial", "evidence": ["tests/corpus/8162-navigator-global.cjs", "tests/fixtures/node-types/navigator.ts", "tests/harness/project-config.test.ts", "tests/corpus/6331-jose-root-entry/main.ts"] }, { "chapter": "globals", "signatures": ["`userAgent` Type: {string}"], "status": "supported", "evidence": ["tests/fixtures/node-types/navigator.ts", "tests/harness/project-config.test.ts"] }, + { "chapter": "globals", "signatures": ["`hardwareConcurrency` Type: {number}", "`platform` Type: {string}"], "status": "supported", "evidence": ["tests/corpus/8162-navigator-global.cjs", "tests/fixtures/node-types/navigator.ts"] }, + { "chapter": "globals", "signatures": ["`language` Type: {string}", "`languages` Type: {string\\[]}"], "status": "partial", "evidence": ["tests/corpus/8162-navigator-global.cjs", "tests/fixtures/node-types/navigator.ts"] }, { "chapter": "globals", "signatures": ["Class: `CryptoKey`", "Class: `SubtleCrypto`"], "status": "partial", "evidence": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"] }, { "chapter": "errors", "parent": "Class: `SystemError`", "symbols": ["code", "errno", "syscall", "path", "dest"], "status": "partial", "evidence": ["tests/corpus/caught-system-error-fields.ts", "tests/fixtures/node-types/terminal-error-fields.ts", "tests/harness/project-config.test.ts"] }, {"chapter": "events", "symbols": ["EventTarget", "eventTarget.addEventListener", "eventTarget.removeEventListener", "eventTarget.dispatchEvent"], "status": "partial", "evidence": ["tests/corpus/5130-event-target-dispatch.js", "tests/corpus/5131-event-target-listener-errors.js", "tests/corpus/5132-event-target-typed.ts", "tests/corpus/5133-abort-signal-event-target.ts", "tests/corpus/5135-event-target-warnings.js", "tests/corpus/5240-event-target-subclass.ts", "tests/corpus/5242-event-target-subclass-hierarchy.ts", "tests/corpus/5243-event-target-subclass-dynamic.ts", "tests/corpus/5244-event-target-subclass.js", "tests/corpus/5245-event-target-subclass-signals.ts", "tests/corpus/5248-event-target-subclass-warnings.ts"]}, @@ -245,9 +262,11 @@ {"chapter": "worker_threads", "symbols": ["resourceLimits"], "status": "partial", "evidence": ["tests/corpus/5806-worker-resource-limits.ts"]}, {"chapter": "worker_threads", "symbols": ["BroadcastChannel extends EventTarget", "broadcastChannel.close", "broadcastChannel.postMessage", "broadcastChannel.ref", "broadcastChannel.unref", "onmessage"], "status": "partial", "evidence": ["tests/corpus/5800-broadcast-channel.ts", "tests/corpus/5801-broadcast-channel.cjs", "tests/corpus/5802-broadcast-channel-workers.ts"]}, {"chapter": "util", "symbols": ["util.types.isAnyArrayBuffer", "util.types.isArrayBuffer", "util.types.isArrayBufferView", "util.types.isDataView", "util.types.isTypedArray", "util.types.isUint8Array", "util.types.isUint8ClampedArray", "util.types.isUint16Array", "util.types.isUint32Array", "util.types.isInt8Array", "util.types.isInt16Array", "util.types.isInt32Array", "util.types.isFloat32Array", "util.types.isFloat64Array", "util.types.isMap", "util.types.isSet", "util.types.isWeakMap", "util.types.isWeakSet", "util.types.isRegExp", "util.types.isNativeError", "util.types.isPromise", "util.types.isProxy", "util.types.isAsyncFunction", "util.types.isGeneratorFunction", "util.types.isGeneratorObject"], "status": "partial", "evidence": ["tests/corpus/node24-util-types.ts", "tests/corpus/node24-util-types.js", "tests/corpus/node24-util-types.cjs", "tests/fixtures/node-types/util-types.ts"]}, - {"chapter": "util", "symbols": ["util.format", "util.formatWithOptions"], "status": "partial", "evidence": ["tests/corpus/1636-util-format.ts", "tests/corpus/2440-console-inspect-args.ts", "tests/corpus/5835-util-format-integer-specifiers.ts"]}, + {"chapter": "util", "symbols": ["util.format", "util.formatWithOptions"], "status": "partial", "evidence": ["tests/corpus/1636-util-format.ts", "tests/corpus/2440-console-inspect-args.ts", "tests/corpus/5835-util-format-integer-specifiers.ts", "tests/corpus/7520-util-format-runtime.ts", "tests/corpus/7521-util-format-runtime.cjs"]}, {"chapter": "util", "symbols": ["util.types.isAnyArrayBuffer", "util.types.isArrayBuffer", "util.types.isArrayBufferView", "util.types.isDataView", "util.types.isTypedArray", "util.types.isUint8Array", "util.types.isUint8ClampedArray", "util.types.isUint16Array", "util.types.isUint32Array", "util.types.isInt8Array", "util.types.isInt16Array", "util.types.isInt32Array", "util.types.isFloat32Array", "util.types.isFloat64Array", "util.types.isMap", "util.types.isSet", "util.types.isWeakMap", "util.types.isWeakSet", "util.types.isRegExp", "util.types.isNativeError", "util.types.isPromise", "util.types.isProxy", "util.types.isAsyncFunction", "util.types.isGeneratorFunction", "util.types.isGeneratorObject", "util.types.isDate", "util.types.isSharedArrayBuffer", "util.types.isMapIterator", "util.types.isSetIterator", "util.types.isKeyObject", "util.types.isNumberObject", "util.types.isStringObject", "util.types.isBooleanObject", "util.types.isBigIntObject", "util.types.isSymbolObject", "util.types.isBoxedPrimitive", "util.types.isBigInt64Array", "util.types.isBigUint64Array", "util.types.isFloat16Array", "util.types.isCryptoKey", "util.types.isExternal"], "status": "partial", "evidence": ["tests/corpus/5831-util-types-brands.ts", "tests/corpus/5832-util-types-brands-dynamic.js", "tests/corpus/node24-util-types.ts", "tests/corpus/node24-util-types.js", "tests/corpus/node24-util-types.cjs", "tests/fixtures/node-types/util-types.ts"]}, - { "chapter": "util", "signatures": ["`util.inspect(object[, options])`"], "status": "partial", "evidence": ["tests/corpus/1630-inspect-scalars.ts", "tests/corpus/1632-inspect-records.ts", "tests/corpus/1637-inspect-dyn.ts", "tests/corpus/2485-inspect-circular-refs.ts", "tests/corpus/5884-inspect-layout-options.ts"] }, + { "chapter": "util", "signatures": ["`util.inspect(object[, options])`"], "status": "partial", "evidence": ["tests/corpus/1630-inspect-scalars.ts", "tests/corpus/1632-inspect-records.ts", "tests/corpus/1637-inspect-dyn.ts", "tests/corpus/2485-inspect-circular-refs.ts", "tests/corpus/5884-inspect-layout-options.ts", "tests/corpus/7526-inspect-options-runtime.ts"] }, + { "chapter": "util", "signatures": ["`util.inspect(object[, showHidden[, depth[, colors]]])`"], "status": "partial", "evidence": ["tests/corpus/7526-inspect-options-runtime.ts"] }, + { "chapter": "util", "signatures": ["`custom` Type: {symbol} that can be used to declare custom inspect functions."], "status": "partial", "evidence": ["tests/corpus/7524-inspect-custom.ts", "tests/corpus/7525-inspect-custom.cjs"] }, { "chapter": "util", "symbols": ["util.styleText"], "status": "partial", "evidence": ["tests/corpus/node24-style-text.ts", "tests/corpus/node24-style-text-validation.js", "tests/corpus/node24-style-text-environment.js", "tests/corpus/node24-style-text.cjs", "tests/fixtures/node-types/style-text.ts"] }, { "chapter": "util", "symbols": ["util.isDeepStrictEqual"], "status": "partial", "evidence": ["tests/corpus/node24-deep-equality.ts", "tests/corpus/node24-deep-equality.cjs", "tests/corpus/node24-deep-equality-properties.js", "tests/fixtures/node-types/deep-equality.ts", "packages/compiler/test/deep-equality.test.ts"] }, { "chapter": "deprecations", "symbols": ["util.toUSVString"], "status": "not-applicable" }, @@ -257,6 +276,10 @@ { "chapter": "buffer", "symbols": ["buffer.isAscii", "buffer.isUtf8", "buffer.transcode"], "status": "partial", "evidence": ["tests/corpus/node24-buffer-encoding.ts", "tests/corpus/node24-buffer-encoding-errors.ts", "tests/fixtures/node-types/text-encoding.ts"] }, { "chapter": "util", "parent": "Class: `util.TextEncoder`", "signatures": ["`textEncoder.encodeInto(src, dest)`", "`encoding` Type: {string}"], "status": "partial", "evidence": ["tests/corpus/node24-text-encode-into.ts", "tests/corpus/node24-text-codec-properties.ts", "tests/fixtures/node-types/text-encoding.ts"] }, { "chapter": "util", "parent": "Class: `util.TextDecoder`", "signatures": ["`encoding` Type: {string}", "`fatal` Type: {boolean}", "`ignoreBOM` Type: {boolean}"], "status": "partial", "evidence": ["tests/corpus/node24-text-codec-properties.ts", "tests/fixtures/node-types/text-encoding.ts"] }, + { "chapter": "util", "symbols": ["util.MIMEType", "util.MIMEParams", "MIMEType.parse", "mime.toString", "mime.toJSON", "mimeParams.delete", "mimeParams.entries", "mimeParams.get", "mimeParams.has", "mimeParams.keys", "mimeParams.set", "mimeParams.values"], "status": "partial", "evidence": ["tests/corpus/7707-util-mime-type.cjs", "tests/corpus/7708-util-mime-type.ts"] }, + { "chapter": "util", "signatures": ["`mimeParams[Symbol.iterator]()`"], "status": "partial", "evidence": ["tests/corpus/7707-util-mime-type.cjs", "tests/corpus/7708-util-mime-type.ts"] }, + { "chapter": "errors", "symbols": ["ERR_INVALID_MIME_SYNTAX"], "status": "partial", "evidence": ["tests/corpus/7707-util-mime-type.cjs"] }, + { "chapter": "errors", "symbols": ["ERR_THROTTLED"], "status": "partial", "evidence": ["tests/corpus/7711-util-throttle.cjs"] }, { "chapter": "util", "symbols": ["util.parseEnv"], "status": "partial", "evidence": ["tests/corpus/node24-env-parse.ts", "tests/corpus/node24-env-parse-errors.ts", "tests/corpus/node24-env-imports.cjs", "tests/fixtures/node-types/env-files.ts", "tests/harness/project-config.test.ts"] }, { "chapter": "process", "symbols": ["process.memoryUsage.rss"], "status": "partial", "evidence": ["tests/corpus/5210-process-memory-usage.ts"] }, { "chapter": "perf_hooks", "symbols": ["performance.now"], "status": "partial", "evidence": ["tests/corpus/2427-perf-hooks-now.ts", "tests/corpus/2446-performance-global.ts"] }, @@ -332,11 +355,11 @@ { "chapter": "buffer", "signatures": ["`buf.readBigInt64BE([offset])`", "`buf.readBigInt64LE([offset])`", "`buf.readBigUInt64BE([offset])`", "`buf.readBigUInt64LE([offset])`", "`buf.writeBigInt64BE(value[, offset])`", "`buf.writeBigInt64LE(value[, offset])`", "`buf.writeBigUInt64BE(value[, offset])`", "`buf.writeBigUInt64LE(value[, offset])`"], "status": "supported", "evidence": ["tests/corpus/2902-bigint-bytes.ts", "tests/corpus/2904-bigint-errors.ts"] }, { "chapter": "environment_variables", "symbols": ["export"], "status": "not-applicable" }, { "chapter": "globals", "symbols": ["fetch"], "status": "partial", "evidence": ["tests/harness/fetch-conformance.test.ts"] }, - { "chapter": "globals", "signatures": ["Class: `TextEncoder`", "Class: `TextDecoder`"], "status": "partial", "evidence": ["tests/corpus/1423-text-codec.ts", "tests/corpus/text-codec-values/main.ts", "tests/corpus/3133-text-decoder-streaming.ts"] }, + { "chapter": "globals", "signatures": ["Class: `TextEncoder`", "Class: `TextDecoder`"], "status": "partial", "evidence": ["tests/corpus/1423-text-codec.ts", "tests/corpus/text-codec-values/main.ts", "tests/corpus/3133-text-decoder-streaming.ts", "tests/corpus/7747-text-codec-untyped-slot.js"] }, { "chapter": "globals", "signatures": ["Class: `Blob`", "Class: `File`"], "status": "partial", "evidence": ["tests/corpus/5285-blob-construct.ts", "tests/corpus/5286-blob-readers.ts", "tests/corpus/5287-blob-inspect.ts", "tests/corpus/5288-blob-validation.js", "tests/corpus/5290-blob-dynamic.js"] }, { "chapter": "buffer", "signatures": ["Class: `Blob`", "Class: `File`"], "status": "partial", "evidence": ["tests/corpus/5285-blob-construct.ts", "tests/corpus/5288-blob-validation.js", "tests/fixtures/node-types/blob-file.ts"] }, { "chapter": "buffer", "signatures": ["`blob.arrayBuffer()`", "`blob.bytes()`", "`blob.text()`", "`blob.slice([start[, end[, type]]])`", "`blob.size`", "`type` Type: {string}", "`name` Type: {string}", "`lastModified` Type: {number}"], "status": "supported", "evidence": ["tests/corpus/5285-blob-construct.ts", "tests/corpus/5286-blob-readers.ts", "tests/corpus/5288-blob-validation.js", "tests/corpus/5289-blob-read-order.ts", "tests/corpus/5290-blob-dynamic.js", "tests/fixtures/node-types/blob-file.ts"] }, - { "chapter": "util", "signatures": ["Class: `util.TextEncoder`", "Class: `util.TextDecoder`", "`textEncoder.encode([input])`", "`textDecoder.decode([input[, options]])`"], "status": "partial", "evidence": ["tests/corpus/text-codec-values/main.ts", "tests/fixtures/node-types/text-codecs.mts", "tests/harness/project-config.test.ts", "tests/corpus/3133-text-decoder-streaming.ts"] }, + { "chapter": "util", "signatures": ["Class: `util.TextEncoder`", "Class: `util.TextDecoder`", "`textEncoder.encode([input])`", "`textDecoder.decode([input[, options]])`"], "status": "partial", "evidence": ["tests/corpus/text-codec-values/main.ts", "tests/fixtures/node-types/text-codecs.mts", "tests/harness/project-config.test.ts", "tests/corpus/3133-text-decoder-streaming.ts", "tests/corpus/7747-text-codec-untyped-slot.js"] }, { "chapter": "modules", "symbols": ["__dirname", "__filename", "exports", "require", "module.require", "main", "filename"], "status": "partial", "evidence": ["tests/corpus/1612-cjs-module-globals.cjs", "tests/corpus/1596-cjs-modules/main.js", "tests/corpus/1629-require-main-filename.cjs", "tests/corpus/2830-cjs-require-self-name/main.cjs", "tests/corpus/2880-cjs-module-graph/main.cjs", "tests/corpus/2923-cjs-factory-export/main.cjs"] }, { "chapter": "modules", "symbols": ["require.resolve", "require.resolve.paths"], "status": "partial", "evidence": ["tests/corpus/2858-import-meta-resolve/main.mjs", "tests/corpus/2860-require-resolve/main.cjs"] }, { "chapter": "modules", "symbols": ["cache", "children", "id", "isPreloading", "loaded", "parent", "path", "paths", "Module"], "status": "partial", "evidence": ["tests/corpus/2880-cjs-module-graph/main.cjs"] }, @@ -401,13 +424,18 @@ { "chapter": "test", "parent": "Class: `TestContext`", "symbols": ["context.before", "context.after", "context.beforeEach", "context.afterEach", "context.plan", "context.fullName"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/ctx-hooks.ts", "tests/fixtures/node-test/cases/ctx-plan.ts", "tests/harness/node-test.test.ts"] }, { "chapter": "test", "symbols": ["getTestContext"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/get-test-context.ts", "tests/fixtures/node-test/cases/get-test-context-cancelled.ts", "tests/harness/node-test.test.ts"] }, { "chapter": "test", "symbols": ["describe.skip", "describe.todo", "describe.only", "suite.skip", "suite.todo", "suite.only", "it.skip", "it.todo", "it.only"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/suite-variants.ts", "tests/fixtures/node-test/cases/only-nested.ts", "tests/fixtures/node-test/cases/todo-suites.ts", "tests/harness/node-test.test.ts"] }, - { "chapter": "util", "signatures": ["`util.promisify(original)`"], "status": "partial", "evidence": ["tests/corpus/1473-promisify-execfile.ts", "tests/corpus/1573-promisify-execfile-env-spread.ts", "tests/corpus/2912-promisify-fs-readfile.ts", "tests/corpus/5625-promisify-exec.ts", "tests/corpus/5626-promisify-forms.ts", "tests/fixtures/node-types/promisify-fs.ts", "tests/corpus/5628-promisify-timers.ts", "tests/fixtures/node-types/promisify-timers.ts", "tests/corpus/5629-promisify-stream.ts", "tests/corpus/5630-promisify-zlib.ts", "tests/fixtures/node-types/promisify-zlib.ts", "tests/corpus/5631-promisify-crypto.ts", "tests/fixtures/node-types/promisify-crypto.ts", "tests/corpus/5632-promisify-dns.ts", "tests/fixtures/node-types/promisify-dns.ts", "tests/corpus/6428-promisify-callback-functions.ts", "tests/fixtures/node-types/promisify-callback-functions.ts", "tests/harness/project-config.test.ts"] }, + { "chapter": "util", "signatures": ["`util.promisify(original)`"], "status": "partial", "evidence": ["tests/corpus/1473-promisify-execfile.ts", "tests/corpus/1573-promisify-execfile-env-spread.ts", "tests/corpus/2912-promisify-fs-readfile.ts", "tests/corpus/5625-promisify-exec.ts", "tests/corpus/5626-promisify-forms.ts", "tests/fixtures/node-types/promisify-fs.ts", "tests/corpus/5628-promisify-timers.ts", "tests/fixtures/node-types/promisify-timers.ts", "tests/corpus/5629-promisify-stream.ts", "tests/corpus/5630-promisify-zlib.ts", "tests/fixtures/node-types/promisify-zlib.ts", "tests/corpus/5631-promisify-crypto.ts", "tests/fixtures/node-types/promisify-crypto.ts", "tests/corpus/5632-promisify-dns.ts", "tests/fixtures/node-types/promisify-dns.ts", "tests/corpus/6428-promisify-callback-functions.ts", "tests/fixtures/node-types/promisify-callback-functions.ts", "tests/corpus/7522-promisify-untyped.cjs", "tests/corpus/7523-promisify-untyped.ts", "tests/harness/project-config.test.ts"] }, { "chapter": "typescript", "symbols": ["type"], "status": "supported", "evidence": ["tests/corpus/2702-type-import-link-static/main.ts", "tests/corpus/2703-type-import-link-static-reexport/main.ts", "tests/corpus/2704-type-import-link-static-success/main.ts", "tests/corpus/2707-type-import-link-static-default-reexport/main.ts", "tests/harness/errors.test.ts"] }, { "chapter": "zlib", "signatures": ["Class: `zlib.Deflate`", "Class: `zlib.Inflate`", "Class: `zlib.Gzip`", "Class: `zlib.Gunzip`", "Class: `zlib.DeflateRaw`", "Class: `zlib.InflateRaw`", "Class: `zlib.Unzip`", "Class: `zlib.ZlibBase`", "`zlib.close([callback])`", "`zlib.flush([kind, ]callback)`", "`zlib.params(level, strategy, callback)`", "`zlib.reset()`", "`bytesWritten` Type: {number}", "Class: `Options`"], "status": "partial", "evidence": ["tests/corpus/5370-zlib-stream-roundtrip.ts", "tests/corpus/5371-zlib-stream-chunking.cjs", "tests/corpus/5372-zlib-stream-errors.cjs", "tests/corpus/5373-zlib-stream-options.cjs", "tests/corpus/5374-zlib-stream-flush-params.ts", "tests/corpus/5375-zlib-stream-promises.ts", "tests/corpus/5377-zlib-stream-backpressure.cjs"] }, + { "chapter": "zlib", "signatures": ["Class: `zlib.BrotliCompress`", "Class: `BrotliOptions`"], "status": "partial", "evidence": ["tests/corpus/7714-zlib-brotli-compress.cjs"] }, + { "chapter": "errors", "symbols": ["ERR_BROTLI_INVALID_PARAM"], "status": "partial", "evidence": ["tests/corpus/7714-zlib-brotli-compress.cjs"] }, + { "chapter": "zlib", "signatures": ["Class: `zlib.BrotliDecompress`"], "status": "partial", "evidence": ["tests/corpus/7715-zlib-brotli-decompress.cjs", "tests/corpus/7716-zlib-brotli.ts"] }, { "chapter": "zlib", "signatures": ["`zlib.constants`"], "status": "partial", "evidence": ["tests/corpus/5376-zlib-constants.cjs", "tests/corpus/5374-zlib-stream-flush-params.ts"] }, { "chapter": "dns", "symbols": ["dns.lookup", "dns.lookupService", "dns.getServers", "dns.getDefaultResultOrder", "dns.setDefaultResultOrder"], "status": "partial", "evidence": ["tests/corpus/5648-dns-lookup-forms.ts", "tests/corpus/5649-dns-service-servers.ts", "tests/corpus/5650-dns-lookup-errors-untyped.cjs"] }, { "chapter": "dns", "symbols": ["dnsPromises.lookup", "dnsPromises.lookupService", "dnsPromises.getDefaultResultOrder"], "status": "partial", "evidence": ["tests/corpus/dns-promises-lookup.ts", "tests/corpus/dns-promises-untyped.cjs"] }, - { "chapter": "webstreams", "symbols": ["ReadableStream"], "status": "partial", "evidence": ["tests/corpus/stream-web-readable.ts", "tests/corpus/stream-web-require.cjs", "tests/corpus/2862-web-readable-async-iteration.ts"] } + { "chapter": "webstreams", "symbols": ["ReadableStream"], "status": "partial", "evidence": ["tests/corpus/stream-web-readable.ts", "tests/corpus/stream-web-require.cjs", "tests/corpus/2862-web-readable-async-iteration.ts"] }, + { "chapter": "webstreams", "symbols": ["TransformStream"], "status": "partial", "evidence": ["tests/corpus/7743-transform-stream-identity.ts"] }, + { "chapter": "globals", "signatures": ["Class: `TransformStream`"], "status": "partial", "evidence": ["tests/corpus/7743-transform-stream-identity.ts"] } ], "dedicated": [ { "symbols": ["assert.doesNotThrow", "assert.doesNotReject"], "status": "partial", "evidence": ["tests/corpus/node24-no-error-assertions.ts", "tests/corpus/node24-no-error-assertions.cjs", "tests/corpus/node24-no-error-assertions-coercion.js", "tests/fixtures/node-types/no-error-assertions.ts", "tests/harness/project-config.test.ts"] }, @@ -425,6 +453,11 @@ { "symbols": ["assert", "assert.ok", "assert.equal", "assert.notEqual", "assert.strictEqual", "assert.notStrictEqual", "assert.deepStrictEqual", "assert.notDeepStrictEqual", "assert.fail", "assert.match", "assert.doesNotMatch", "assert.throws", "assert.rejects", "assert.ifError"], "status": "partial", "evidence": ["tests/corpus/1600-assert-passing.ts", "tests/corpus/1604-assert-deep-structures.ts", "tests/corpus/1609-assert-async.ts", "tests/corpus/1721-assert-throws-regex-class.ts", "tests/corpus/2936-static-loose-equality.ts", "tests/corpus/5515-assert-deep-strict-typed-messages.ts"] }, { "chapter": "crypto", "symbols": ["Cipheriv", "cipher.update", "cipher.final", "cipher.setAAD", "cipher.getAuthTag", "cipher.setAutoPadding", "Decipheriv", "decipher.update", "decipher.final", "decipher.setAAD", "decipher.setAuthTag", "decipher.setAutoPadding"], "status": "partial", "evidence": ["tests/corpus/5250-cipher-aes-gcm.ts", "tests/corpus/5251-cipher-aes-cbc.ts", "tests/corpus/5252-cipher-aes-ctr.ts", "tests/corpus/5253-cipher-chacha20-poly1305.ts", "tests/corpus/5254-cipher-errors.js", "tests/corpus/5255-cipher-encodings.cjs", "tests/corpus/5256-cipher-deprecation.cjs", "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", "tests/corpus/7055-cipher-streams.ts", "tests/corpus/7056-cipher-stream-errors.ts", "tests/corpus/7057-cipher-streams.mjs", "tests/fixtures/node-types/cipher.ts"] }, { "chapter": "crypto", "symbols": ["crypto.webcrypto", "subtle"], "status": "partial", "evidence": ["tests/corpus/7122-webcrypto-node-crypto.ts", "tests/corpus/7123-webcrypto-node-crypto.cjs"] }, + { "chapter": "tls", "symbols": ["server.addContext", "server.setSecureContext", "'tlsClientError'"], "status": "partial", "evidence": ["tests/corpus/7440-tls-server-add-context.ts", "tests/corpus/7441-tls-client-error.ts", "tests/corpus/7442-tls-server-contexts.mjs", "tests/fixtures/node-types/tls-server-contexts.ts"] }, + { "chapter": "errors", "symbols": ["ERR_TLS_REQUIRED_SERVER_NAME"], "status": "partial", "evidence": ["tests/corpus/7440-tls-server-add-context.ts"] }, + { "chapter": "tls", "symbols": ["tlsSocket.authorizationError"], "status": "partial", "evidence": ["tests/corpus/7097-tls-client-certificates.ts", "tests/corpus/7098-tls-client-certificates-paths.ts", "tests/corpus/7099-tls-client-certificates.mjs", "tests/fixtures/node-types/tls-client-certificates.ts"] }, + { "chapter": "tls", "symbols": ["tlsSocket.getPeerCertificate", "tlsSocket.getCertificate", "tlsSocket.getPeerX509Certificate", "tlsSocket.getX509Certificate"], "status": "partial", "evidence": ["tests/corpus/7095-tls-peer-certificates.ts", "tests/corpus/7096-tls-peer-certificates.mjs", "tests/fixtures/node-types/tls-peer-certificates.ts"] }, + { "chapter": "crypto", "symbols": ["x509.checkEmail", "x509.checkHost", "x509.checkIP", "x509.checkIssued", "x509.checkPrivateKey", "x509.toJSON", "x509.toLegacyObject", "x509.toString", "x509.verify"], "status": "partial", "evidence": ["tests/corpus/7090-x509-check-names.ts", "tests/corpus/7091-x509-certificate-keys.ts", "tests/corpus/7092-x509-legacy-object.ts", "tests/corpus/7093-x509-methods-js.mjs", "tests/fixtures/node-types/x509-methods.ts"] }, { "chapter": "crypto", "symbols": ["ECDH", "ecdh.computeSecret", "ecdh.generateKeys", "ecdh.getPrivateKey", "ecdh.getPublicKey", "ecdh.setPrivateKey", "ecdh.setPublicKey"], "status": "partial", "evidence": ["tests/corpus/6700-ecdh-shared-secret.ts", "tests/corpus/6701-ecdh-formats.ts", "tests/corpus/6702-ecdh-errors.js", "tests/corpus/6703-ecdh-private-keys.ts", "tests/corpus/6704-ecdh-set-public-key.cjs", "tests/corpus/6705-ecdh-handles.ts", "tests/fixtures/node-types/ecdh.ts"] }, { "chapter": "crypto", "symbols": ["DiffieHellman", "DiffieHellmanGroup", "diffieHellman.computeSecret", "diffieHellman.generateKeys", "diffieHellman.getGenerator", "diffieHellman.getPrime", "diffieHellman.getPrivateKey", "diffieHellman.getPublicKey", "diffieHellman.setPrivateKey", "diffieHellman.setPublicKey", "diffieHellman.verifyError"], "status": "partial", "evidence": ["tests/corpus/6706-dh-groups.ts", "tests/corpus/6707-dh-explicit-params.ts", "tests/corpus/6708-dh-errors.js", "tests/corpus/6709-dh-generate.ts", "tests/corpus/6710-dh-encodings.cjs", "tests/corpus/6711-dh-handles.ts", "tests/fixtures/node-types/dh.ts"] }, { "chapter": "errors", "symbols": ["ERR_CRYPTO_UNKNOWN_DH_GROUP", "ERR_CRYPTO_INVALID_KEYLEN", "ERR_CRYPTO_INVALID_STATE"], "status": "partial", "evidence": ["tests/corpus/6708-dh-errors.js"] }, @@ -435,11 +468,12 @@ { "symbols": ["crypto.createPrivateKey", "crypto.createPublicKey", "crypto.generateKeyPair", "crypto.generateKeyPairSync", "crypto.sign", "crypto.verify", "crypto.createSign", "crypto.createVerify", "Sign", "sign.update", "sign.sign", "Verify", "verify.update", "verify.verify", "asymmetricKeyType", "asymmetricKeyDetails"], "status": "partial", "evidence": ["tests/corpus/7080-crypto-ed25519.mjs", "tests/corpus/7082-crypto-key-objects.ts", "tests/corpus/7083-crypto-sign-verify.ts", "tests/corpus/7084-crypto-key-pairs.cjs", "tests/fixtures/node-types/keys.ts"] }, { "symbols": ["crypto.getRandomValues"], "status": "supported", "evidence": ["tests/corpus/6204-crypto-get-random-values.ts", "tests/corpus/6205-crypto-get-random-values.js"] }, { "symbols": ["crypto.timingSafeEqual"], "status": "supported", "evidence": ["tests/corpus/5444-crypto-timing-safe-equal.js", "tests/corpus/2890-crypto-static-utility.ts"] }, + { "symbols": ["X509Certificate"], "status": "partial", "evidence": ["tests/corpus/1568-x509-fingerprint.ts", "tests/corpus/1572-x509-validity-date-gettime.ts", "tests/corpus/6936-x509-certificate-fields.ts"] }, { "symbols": ["crypto.randomInt"], "status": "partial", "evidence": ["tests/corpus/5442-crypto-random-values.ts", "tests/corpus/2890-crypto-static-utility.ts"] }, { "symbols": ["crypto.hkdf", "crypto.hkdfSync", "crypto.scrypt", "crypto.scryptSync"], "status": "partial", "evidence": ["tests/corpus/node24-crypto-kdf.ts", "tests/corpus/node24-crypto-kdf-errors.ts", "tests/corpus/node24-crypto-kdf-options.ts", "tests/corpus/5438-crypto-kdf-digests.ts"] }, { "symbols": ["EventEmitter", "emitter.on", "emitter.addListener", "emitter.once", "emitter.prependListener", "emitter.prependOnceListener", "emitter.off", "emitter.removeListener", "emitter.removeAllListeners", "emitter.emit", "emitter.listenerCount", "emitter.listeners", "emitter.rawListeners", "emitter.eventNames", "emitter.setMaxListeners", "emitter.getMaxListeners", "events.once"], "status": "partial", "evidence": ["tests/corpus/1677-emitter-listeners.ts", "tests/corpus/2620-ee-override-once-order.ts"] }, { "symbols": ["events.getEventListeners", "events.getMaxListeners", "events.listenerCount", "events.setMaxListeners", "events.defaultMaxListeners"], "status": "partial", "evidence": ["tests/corpus/5885-events-max-listener-helpers.ts", "tests/corpus/5846-events-esm-default-max.ts", "tests/corpus/5847-events-cjs-default-max.cjs"] }, - { "symbols": ["AsyncLocalStorage", "asyncLocalStorage.run", "asyncLocalStorage.exit", "asyncLocalStorage.getStore", "asyncLocalStorage.enterWith", "asyncLocalStorage.disable"], "status": "partial", "evidence": ["tests/corpus/2213-async-local-storage.cjs", "tests/corpus/5815-als-callback-context.ts"] }, + { "symbols": ["AsyncLocalStorage", "asyncLocalStorage.run", "asyncLocalStorage.exit", "asyncLocalStorage.getStore", "asyncLocalStorage.enterWith", "asyncLocalStorage.disable", "asyncLocalStorage.name", "AsyncLocalStorage.bind", "AsyncLocalStorage.snapshot"], "status": "partial", "evidence": ["tests/corpus/2213-async-local-storage.cjs", "tests/corpus/5815-als-callback-context.ts", "tests/corpus/7110-als-completion-callbacks.ts", "tests/corpus/7111-als-bind-snapshot-options.ts", "tests/corpus/7112-als-bind-snapshot-dynamic.mjs", "tests/corpus/7119-als-net-event-context.mjs", "tests/corpus/7380-als-http-event-context.mjs", "tests/corpus/7383-als-stream-events.ts", "tests/corpus/7384-als-stream-events.mjs", "tests/corpus/7385-als-child-process-events.ts", "tests/corpus/7386-als-child-fork-events/main.mjs", "tests/corpus/7387-als-http2-events.mjs", "tests/corpus/7388-als-http2-compat-events.mjs", "tests/corpus/7389-als-http2-secure-events.mjs", "tests/corpus/7391-als-response-end-and-finished.mjs"] }, { "symbols": ["StringDecoder", "stringDecoder.write", "stringDecoder.end"], "status": "partial", "evidence": ["tests/corpus/1662-string-decoder-encodings.ts", "tests/corpus/node24-string-decoder.ts", "tests/corpus/5882-string-decoder-runtime-encoding.ts"] }, { "symbols": ["setImmediate", "setInterval", "setTimeout", "clearImmediate", "clearInterval", "clearTimeout", "timersPromises.setTimeout", "timersPromises.setImmediate", "timersPromises.setInterval", "immediate.hasRef", "immediate.ref", "immediate.unref", "timeout.hasRef", "timeout.ref", "timeout.unref", "timeout.refresh"], "status": "partial", "evidence": ["tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", "tests/corpus/2093-timers-promises.ts", "tests/corpus/2852-timers-promises-interval.ts", "tests/corpus/5830-timers-promises-signal.ts"] }, { "symbols": ["timersPromises.scheduler.wait", "timersPromises.scheduler.yield"], "status": "partial", "evidence": ["tests/corpus/5839-timers-promises-scheduler.ts", "tests/corpus/5889-timers-scheduler-signal.ts"] }, diff --git a/native/llvm-codegen/CMakeLists.txt b/native/llvm-codegen/CMakeLists.txt index 07abb35ec..7ce20086f 100644 --- a/native/llvm-codegen/CMakeLists.txt +++ b/native/llvm-codegen/CMakeLists.txt @@ -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 diff --git a/native/llvm-codegen/README.md b/native/llvm-codegen/README.md index 0e8abbc47..0af8a11d2 100644 --- a/native/llvm-codegen/README.md +++ b/native/llvm-codegen/README.md @@ -33,11 +33,11 @@ lowering, verifies before and after optimization, and publishes through a private sibling file so a failed or interrupted request cannot truncate the requested output. -Repeating `--output` divides the program into that many partitions, which are compiled concurrently and linked together. The helper splits the emitter's LLVM assembly text directly, so partitions also parse concurrently; input outside the emitter's narrow grammar is parsed whole and split with LLVM instead. Internal definitions become hidden so partitions can reference each other. At opt-level 0 each partition is compiled independently. Optimized partitions follow the ThinLTO model: each is simplified and summarized on its own thread, a thin link decides which small functions every partition imports for inlining and which hidden definitions become internal again, and each partition is then optimized and compiled on its own thread. Partition placement depends only on the module and the partition count, so output never depends on the host. +Repeating `--output` divides the program into that many partitions, which are compiled concurrently and linked together. The helper splits the emitter's LLVM assembly text directly, so partitions also parse concurrently; input outside the emitter's narrow grammar is parsed whole and split with LLVM instead. Internal definitions that no other definition reaches are left out of every partition, and the others become hidden so partitions can reference each other. At opt-level 0 each partition is compiled independently. Optimized partitions follow the ThinLTO model: each is simplified and summarized on its own thread, a thin link decides which small functions every partition imports for inlining and which hidden definitions become internal again, and each partition is then optimized and compiled on its own thread. Partition placement depends only on the module and the partition count, so output never depends on the host. WebAssembly code generation differs from LLVM's default pipeline in two ways that do not change its output. The pass that gives multiple-entry loops a single entry runs only on functions whose control flow is irreducible; its reachability analysis grows with the square of a function's block count even when it has nothing to change. Object emission also leaves out the module pass that collects external symbols for assembly declarations, so each function's machine code is freed once it is printed instead of when the whole module is compiled. -`--cache-dir ` 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 ` 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 `, 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 +$ LLVM_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`. diff --git a/native/llvm-codegen/linux-helper.mjs b/native/llvm-codegen/linux-helper.mjs new file mode 100644 index 000000000..68d738af6 --- /dev/null +++ b/native/llvm-codegen/linux-helper.mjs @@ -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); +} diff --git a/native/llvm-codegen/llvm-dist.mjs b/native/llvm-codegen/llvm-dist.mjs new file mode 100644 index 000000000..aea90dd54 --- /dev/null +++ b/native/llvm-codegen/llvm-dist.mjs @@ -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; +} diff --git a/native/llvm-codegen/llvm.profdata.gz b/native/llvm-codegen/llvm.profdata.gz new file mode 100644 index 000000000..5ebb5076f Binary files /dev/null and b/native/llvm-codegen/llvm.profdata.gz differ diff --git a/native/llvm-codegen/src/emit.cpp b/native/llvm-codegen/src/emit.cpp index 9cae164d3..c7efef13f 100644 --- a/native/llvm-codegen/src/emit.cpp +++ b/native/llvm-codegen/src/emit.cpp @@ -19,6 +19,7 @@ #include "llvm/CodeGen/MachineFunction.h" #include "llvm/CodeGen/MachineFunctionPass.h" #include "llvm/CodeGen/MachineModuleInfo.h" +#include "llvm/IR/DiagnosticHandler.h" #include "llvm/IR/DiagnosticInfo.h" #include "llvm/IR/DiagnosticPrinter.h" #include "llvm/IR/LegacyPassManager.h" @@ -41,11 +42,13 @@ #include "llvm/Support/SourceMgr.h" #include "llvm/Support/ThreadPool.h" #include "llvm/Support/Threading.h" +#include "llvm/Support/JSON.h" #include "llvm/Support/raw_ostream.h" #include "llvm/Target/TargetMachine.h" #include "llvm/TargetParser/Triple.h" #include "llvm/Transforms/Utils/SplitModule.h" +#include #include #include #include @@ -61,6 +64,9 @@ static constexpr size_t MaxPartitions = 64; // multiple of the usual size limit, in builds with a profile. See // limitProfileImports(). static constexpr const char *ProfileImportMultiplier = "2"; +// The thin link's size limit, in instructions, for a callee it imports into +// a partition that calls it. See limitImports(). +static constexpr const char *ImportInstructionLimit = "40"; std::optional parseEmitOptions(int Argc, char **Argv) { EmitOptions Options; @@ -170,6 +176,141 @@ void limitProfileImports() { (void)Limited; } +// The thin link imports a callee of up to 100 instructions, LLVM's default, +// into every partition that calls it, and each partition then optimizes and +// compiles its imported copies again. On tsc-ts (64 partitions) a limit of 40 +// takes about 7% off the helper's time, and the program runs as fast; 30 and +// below made it run 1-8% slower. The limit is a process-wide option; the +// helper sets it once. +void limitImports() { + static const bool Limited = [] { + if (cl::Option *Option = + cl::getRegisteredOptions().lookup("import-instr-limit")) + Option->addOccurrence(0, "import-instr-limit", ImportInstructionLimit); + return true; + }(); + (void)Limited; +} + +// Drops the names of a partition's struct types before its summary is +// written. Every partition declares the program's types under the same +// names, and a thin link loads the importing partition and the partitions +// it imports from into one context, which tells same-named types apart by +// appending a counter that runs across the whole link. An edit that adds a +// type anywhere shifts the counter, so the module after importing, the +// object cache key, changed in partitions whose code had not. Unnamed types +// carry no such suffix; importing still matches them by their bodies. +struct UnnameStructTypes : PassInfoMixin { + PreservedAnalyses run(Module &M, ModuleAnalysisManager &) { + for (StructType *Type : M.getIdentifiedStructTypes()) + Type->setName(""); + return PreservedAnalyses::all(); + } + static bool isRequired() { return true; } +}; + +// How the --profile-use profile matched the functions it annotated, summed +// over partitions: functions that received its counts, functions whose +// control flow changed since it was collected (hash mismatch), and functions +// it has no record of (new, or never run in training: the merged profile is +// sparse). A partition reused from the cache without its counts is counted +// in Unknown. +struct ProfileMatch { + std::atomic Annotated{0}; + std::atomic Mismatched{0}; + std::atomic Missing{0}; + std::atomic Unknown{0}; + + void add(const ProfileMatch &Other) { + Annotated += Other.Annotated; + Mismatched += Other.Mismatched; + Missing += Other.Missing; + } + + std::string serialize() const { + return formatv("{0} {1} {2}", Annotated.load(), Mismatched.load(), + Missing.load()) + .str(); + } + + bool deserialize(StringRef Text) { + SmallVector Parts; + Text.split(Parts, ' '); + uint64_t Values[3]; + if (Parts.size() != 3) + return false; + for (size_t I = 0; I < 3; ++I) + if (Parts[I].getAsInteger(10, Values[I])) + return false; + Annotated += Values[0]; + Mismatched += Values[1]; + Missing += Values[2]; + return true; + } +}; + +// Counts the profile annotation's hash-mismatch and missing-function +// warnings instead of printing them. Every other diagnostic takes the +// context's default handling. +class ProfileMatchHandler : public DiagnosticHandler { +public: + explicit ProfileMatchHandler(ProfileMatch &Match) : Match(Match) {} + + bool handleDiagnostics(const DiagnosticInfo &Info) override { + if (Info.getKind() != DK_PGOProfile) + return false; + std::string Message; + raw_string_ostream Stream(Message); + DiagnosticPrinterRawOStream Printer(Stream); + Info.print(Printer); + if (StringRef(Message).contains("hash mismatch")) + ++Match.Mismatched; + else if (StringRef(Message).contains_insensitive("no profile data")) + ++Match.Missing; + else + return false; + return true; + } + +private: + ProfileMatch &Match; +}; + +// The annotation warns about functions the profile does not describe only +// when asked; the match counts need those warnings. Process-wide, set once. +void reportMissingProfiles() { + static const bool Reported = [] { + if (cl::Option *Option = + cl::getRegisteredOptions().lookup("pgo-warn-missing-function")) + Option->addOccurrence(0, "pgo-warn-missing-function", "true"); + return true; + }(); + (void)Reported; +} + +// Functions of `M` that the profile annotated with counts. +uint64_t annotatedFunctions(const Module &M) { + uint64_t Count = 0; + for (const Function &F : M) { + if (F.isDeclaration()) + continue; + std::optional Entry = F.getEntryCount(); + if (Entry && !Entry->isSynthetic()) + ++Count; + } + return Count; +} + +// Prints the profile match as the successful emit's one line of JSON. +void reportProfileMatch(const ProfileMatch &Match) { + json::Object Profile{{"annotated", Match.Annotated.load()}, + {"mismatched", Match.Mismatched.load()}, + {"missing", Match.Missing.load()}, + {"unknown_partitions", Match.Unknown.load()}}; + json::Object Response{{"ok", true}, {"profile", std::move(Profile)}}; + outs() << formatv("{0}\n", json::Value(std::move(Response))); +} + // Reads the profile's header once, before any partition needs it: it must // hold IR-level counts. std::optional inspectProfile(const EmitOptions &Options) { @@ -191,6 +332,7 @@ std::optional inspectProfile(const EmitOptions &Options) { // Owns the analysis managers a pipeline needs for one module. struct Pipeline { + PassInstrumentationCallbacks Callbacks; LoopAnalysisManager LAM; FunctionAnalysisManager FAM; CGSCCAnalysisManager CGAM; @@ -199,13 +341,26 @@ struct Pipeline { Pipeline(TargetMachine &Machine, OptimizationLevel Level, std::optional Profile = std::nullopt) - : PB(&Machine, tuningFor(Level), std::move(Profile)) { + : PB(&Machine, tuningFor(Level), std::move(Profile), &Callbacks) { PB.registerModuleAnalyses(MAM); PB.registerCGSCCAnalyses(CGAM); PB.registerFunctionAnalyses(FAM); PB.registerLoopAnalyses(LAM); PB.crossRegisterProxies(LAM, FAM, CGAM, MAM); } + + // Adds to Count the functions the profile annotation gave counts to, as + // soon as that pass has run: inlining and dead code elimination later in + // the pipeline delete annotated functions. + void countAnnotated(std::atomic &Count) { + Callbacks.registerAfterPassCallback( + [&Count](StringRef PassID, Any IR, const PreservedAnalyses &) { + if (PassID != "PGOInstrumentationUse") + return; + if (const auto *M = llvm::any_cast(&IR)) + Count += annotatedFunctions(**M); + }); + } }; std::optional verify(Module &M, StringRef Code) { @@ -387,8 +542,15 @@ std::optional publish(StringRef TemporaryPath, std::optional emitModule(Module &M, TargetMachine &Machine, OptimizationLevel Level, std::optional Profile, CodeGenFileType Type, - StringRef OutputPath, std::vector> &Temporaries) { + StringRef OutputPath, std::vector> &Temporaries, + ProfileMatch &Match) { + bool Annotating = Profile && Profile->Action == PGOOptions::IRUse; + if (Annotating) + M.getContext().setDiagnosticHandler( + std::make_unique(Match)); Pipeline P(Machine, Level, std::move(Profile)); + if (Annotating) + P.countAnnotated(Match.Annotated); P.PB.buildPerModuleDefaultPipeline(Level).run(M, P.MAM); if (std::optional Failure = verify(M, "post_optimization_verification_failed")) @@ -555,20 +717,32 @@ std::optional compileOptimizedPartitions(const EmitOptions &Options, const PartitionPlan &Plan, OptimizationLevel Level, StringRef Salt, const ArtifactCache &Cache, - std::vector> &Objects) { + std::vector> &Objects, + ProfileMatch &Match) { size_t Count = Plan.Modules.size(); + bool Annotating = !Options.ProfileUse.empty(); std::vector> Summaries(Count); SharedFailure Failures; forEachPartition(Count, [&](size_t I) { std::string Key = hashParts({Salt, "summary", Plan.Modules[I]}); - if ((Summaries[I] = Cache.lookup(Key, "bc"))) + if ((Summaries[I] = Cache.lookup(Key, "bc"))) { + if (Annotating) { + std::unique_ptr Counts = Cache.lookup(Key, "match"); + if (!Counts || !Match.deserialize(Counts->getBuffer())) + ++Match.Unknown; + } return; + } std::string LookupError; std::unique_ptr Machine = createTargetMachine(Options.Target, Options.OptLevel, LookupError); if (!Machine) return Failures.set({"target_machine_failed", LookupError}); LLVMContext Context; + ProfileMatch PartitionMatch; + if (Annotating) + Context.setDiagnosticHandler( + std::make_unique(PartitionMatch)); Expected> Loaded = loadPartition(Options, Plan.Modules[I], I, Context, *Machine); if (!Loaded) @@ -592,12 +766,20 @@ compileOptimizedPartitions(const EmitOptions &Options, // Profile instrumentation or annotation happens here, before the thin // link; the post-link optimization reads the annotated weights. Pipeline P(*Machine, Level, profileOptions(Options)); + if (Annotating) + P.countAnnotated(PartitionMatch.Annotated); ModulePassManager MPM = P.PB.buildThinLTOPreLinkDefaultPipeline(Level); + MPM.addPass(UnnameStructTypes()); MPM.addPass(BitcodeWriterPass(Stream, /*ShouldPreserveUseListOrder=*/false, /*EmitSummaryIndex=*/true, /*EmitModuleHash=*/true)); MPM.run(Mod, P.MAM); } + if (Annotating) { + Match.add(PartitionMatch); + // Stored before the summary, so a reused summary finds its counts. + Cache.store(Key, "match", PartitionMatch.serialize()); + } StringRef Contents(Bitcode.data(), Bitcode.size()); Cache.store(Key, "bc", Contents); Summaries[I] = MemoryBuffer::getMemBufferCopy(Contents); @@ -689,6 +871,7 @@ compileOptimizedPartitions(const EmitOptions &Options, std::vector Names(Count); for (size_t I = 0; I < Count; ++I) Names[I] = "partition-" + std::to_string(I); + limitImports(); if (!Options.ProfileUse.empty()) limitProfileImports(); lto::LTO Link(std::move(Config), @@ -744,7 +927,8 @@ compileOptimizedPartitions(const EmitOptions &Options, std::optional compilePartitions(const EmitOptions &Options, const PartitionPlan &Plan, - std::vector> &Objects) { + std::vector> &Objects, + ProfileMatch &Match) { // Everything besides a partition's own bytes that determines its // artifacts. The caller's cache directory already identifies the exact // helper build; source content and partition count arrive per module. @@ -778,7 +962,7 @@ compilePartitions(const EmitOptions &Options, const PartitionPlan &Plan, return compileUnoptimizedPartitions(Options, Plan, Salt, Cache, Objects); return compileOptimizedPartitions(Options, Plan, optimizationLevel(Options.OptLevel), Salt, - Cache, Objects); + Cache, Objects, Match); } // Publishes in-memory partition objects through private sibling files. @@ -819,6 +1003,8 @@ int emitPartitions(const EmitOptions &Options) { std::vector> Objects; std::optional Failure; bool Compiled = false; + // A failed attempt's counts are discarded with its objects. + auto Match = std::make_unique(); { ErrorOr> Input = MemoryBuffer::getFile(Options.Input, /*IsText=*/false, @@ -830,15 +1016,16 @@ int emitPartitions(const EmitOptions &Options) { Input->reset(); PartitionPlan Plan{std::move(Text->Sources), std::move(Text->PublicSymbols)}; - Compiled = !compilePartitions(Options, Plan, Objects); + Compiled = !compilePartitions(Options, Plan, Objects, *Match); } } } if (!Compiled) { + Match = std::make_unique(); PartitionPlan Plan; Failure = splitParsedModule(Options, Plan); if (!Failure) - Failure = compilePartitions(Options, Plan, Objects); + Failure = compilePartitions(Options, Plan, Objects, *Match); } std::vector> Temporaries; if (!Failure) @@ -851,6 +1038,8 @@ int emitPartitions(const EmitOptions &Options) { return reportError(Failure->Code, Failure->Message, Options.DiagnosticFormat); } + if (!Options.ProfileUse.empty()) + reportProfileMatch(*Match); return 0; } @@ -895,6 +1084,8 @@ int emit(const EmitOptions &Options) { return reportError("invalid_import", "runtime bitcode import requires an optimized build", Options.DiagnosticFormat); + if (!Options.ProfileUse.empty()) + reportMissingProfiles(); if (std::optional Failure = inspectProfile(Options)) return reportError(Failure->Code, Failure->Message, Options.DiagnosticFormat); @@ -957,9 +1148,10 @@ int emit(const EmitOptions &Options) { ? CodeGenFileType::ObjectFile : CodeGenFileType::AssemblyFile; std::vector> Temporaries; + ProfileMatch Match; std::optional Failure = emitModule(*Mod, *Machine, Level, profileOptions(Options), Type, - Options.Outputs[0], Temporaries); + Options.Outputs[0], Temporaries, Match); if (!Failure) Failure = publish(Temporaries[0], Options.Outputs[0]); if (Failure) { @@ -968,6 +1160,8 @@ int emit(const EmitOptions &Options) { return reportError(Failure->Code, Failure->Message, Options.DiagnosticFormat); } + if (!Options.ProfileUse.empty()) + reportProfileMatch(Match); return 0; } diff --git a/native/llvm-codegen/src/partition.cpp b/native/llvm-codegen/src/partition.cpp index f4add0e6d..ef5230129 100644 --- a/native/llvm-codegen/src/partition.cpp +++ b/native/llvm-codegen/src/partition.cpp @@ -11,6 +11,7 @@ #include #include +#include using namespace llvm; @@ -36,6 +37,9 @@ struct Symbol { std::string PromotedHeader; std::vector Refs; std::vector Metadata; + // The source-module group the emitter wrote before this definition, or + // empty. + StringRef Group; unsigned Partition = NoPartition; }; @@ -70,6 +74,85 @@ uint32_t stableHash(StringRef Name) { return Hash; } +// Places the functions whose root carries a source-module group by group: a +// module's functions call each other most, so keeping them together leaves +// fewer calls for the thin link to import across partitions. A group larger +// than the mean partition, or than MaxChunkBytes, is divided into a power of +// two of chunks by the hash of each root's name, so the division changes only +// when the group's size doubles. Chunks are placed largest first, each in the +// lighter of two partitions chosen by hashes of its key, after the functions +// placed by name. The placement depends only on the module text and the +// partition count. Modules with fewer than MinGroupedPartitions partitions +// keep the placement by name: with few partitions per core, the balance of +// the largest partitions decides the build time more than the imports. +void placeGroups(std::vector &Symbols, ArrayRef Roots, + unsigned Count) { + constexpr unsigned MinGroupedPartitions = 32; + if (Count < MinGroupedPartitions) + return; + auto Bytes = [](const Symbol &S) -> uint64_t { + return S.Header.size() + S.Body.size(); + }; + std::vector Load(Count, 0); + uint64_t Total = 0; + StringMap GroupBytes; + for (unsigned I = 0; I < Symbols.size(); ++I) { + const Symbol &S = Symbols[I]; + if (S.Kind != SymbolKind::Function || S.Partition == NoPartition) + continue; + Total += Bytes(S); + StringRef Group = Symbols[Roots[I]].Group; + if (Group.empty()) + Load[S.Partition] += Bytes(S); + else + GroupBytes[Group] += Bytes(S); + } + if (GroupBytes.empty()) + return; + // A chunk is at most MaxChunkBytes, so the code a partition optimizes + // together, and the inlining within it, does not grow when a program has + // fewer partitions. + constexpr uint64_t MaxChunkBytes = 1024 * 1024; + uint64_t Chunk = + std::min(MaxChunkBytes, std::max(1, Total / Count)); + std::vector Keys(Symbols.size()); + StringMap KeyBytes; + for (unsigned I = 0; I < Symbols.size(); ++I) { + const Symbol &S = Symbols[I]; + if (S.Kind != SymbolKind::Function || S.Partition == NoPartition) + continue; + StringRef Group = Symbols[Roots[I]].Group; + if (Group.empty()) + continue; + uint64_t Chunks = 1; + while (Chunks * Chunk < GroupBytes[Group]) + Chunks *= 2; + Keys[I] = Chunks == 1 + ? Group.str() + : (Group + "#" + + Twine(stableHash(Symbols[Roots[I]].Name) % Chunks)) + .str(); + KeyBytes[Keys[I]] += Bytes(S); + } + std::vector> Order; + for (const auto &Entry : KeyBytes) + Order.push_back({Entry.getKey(), Entry.getValue()}); + llvm::sort(Order, [](const auto &A, const auto &B) { + return A.second != B.second ? A.second > B.second : A.first < B.first; + }); + StringMap Where; + for (const auto &[Key, Size] : Order) { + unsigned First = stableHash(Key) % Count; + unsigned Second = stableHash((Key + "/2").str()) % Count; + unsigned Chosen = Load[Second] < Load[First] ? Second : First; + Load[Chosen] += Size; + Where[Key] = Chosen; + } + for (unsigned I = 0; I < Symbols.size(); ++I) + if (!Keys[I].empty()) + Symbols[I].Partition = Where[Keys[I]]; +} + bool startsWithLinkage(StringRef Rest) { for (StringRef Linkage : {"linkonce", "weak", "common", "appending", "extern_weak", @@ -326,6 +409,11 @@ std::optional partitionProgramText(StringRef Text, return true; }; + // The emitter writes "; scriptc-group " before the definitions of each + // source function; the group applies until the next such line, and an + // empty key ends it. + constexpr StringRef GroupLine = "; scriptc-group"; + StringRef Group; size_t Pos = 0; while (Pos < Text.size()) { size_t End = Text.find('\n', Pos); @@ -335,10 +423,15 @@ std::optional partitionProgramText(StringRef Text, size_t Next = End + 1; if (Line.empty() || Line.trim().empty()) { // Blank separator. + } else if (Line.starts_with(GroupLine) && + (Line.size() == GroupLine.size() || + Line[GroupLine.size()] == ' ')) { + Group = Line.drop_front(GroupLine.size()).trim(); } else if (Line.starts_with("define ")) { Symbol S; S.Kind = SymbolKind::Function; S.Header = Line; + S.Group = Group; if (!parseFunctionHeader(Line, S)) return std::nullopt; // The emitter indents every body line, so the first line that is @@ -443,61 +536,6 @@ std::optional partitionProgramText(StringRef Text, } } - // An internal function referenced by exactly one other function, and by - // no global, is a whole-program inlining candidate; keep it with that - // function so its partition can inline it without importing. A function - // body larger than this is past LLVM's inline cost limit even for the only - // call to a local function, so it is placed by its own name. Otherwise a - // program's module initializers, which only the entry point calls, would - // all share the entry point's partition. - constexpr size_t MaxInlinedBodyBytes = 256 * 1024; - constexpr unsigned Shared = ~1u; - std::vector Referrer(Symbols.size(), NoPartition); - for (unsigned I = 0; I < Symbols.size(); ++I) { - const Symbol &S = Symbols[I]; - if (S.Kind == SymbolKind::Declaration) - continue; - for (unsigned Ref : S.Refs) { - if (Ref == I) - continue; - if (S.Kind == SymbolKind::Variable || Referrer[Ref] != NoPartition) - Referrer[Ref] = Shared; - else - Referrer[Ref] = I; - } - } - for (unsigned I = 0; I < Symbols.size(); ++I) { - if (Symbols[I].Kind != SymbolKind::Function) - continue; - unsigned Root = I; - for (unsigned Steps = 0; Steps < 1024; ++Steps) { - const Symbol &S = Symbols[Root]; - unsigned Parent = Referrer[Root]; - if (!S.Local || S.Body.size() > MaxInlinedBodyBytes || - Parent == NoPartition || Parent == Shared || - Symbols[Parent].Kind != SymbolKind::Function || Parent == I) - break; - Root = Parent; - } - Symbols[I].Partition = stableHash(Symbols[Root].Name) % Count; - } - for (const Symbol &S : Symbols) { - if (S.Kind != SymbolKind::Function) - continue; - for (unsigned Ref : S.Refs) - if (Symbols[Ref].Kind == SymbolKind::Variable && - Symbols[Ref].Partition == NoPartition) - Symbols[Ref].Partition = S.Partition; - } - for (Symbol &S : Symbols) - if (S.Kind == SymbolKind::Variable && S.Partition == NoPartition) - S.Partition = stableHash(S.Name) % Count; - - ProgramPartitions Result; - for (const Symbol &S : Symbols) - if (S.Kind != SymbolKind::Declaration && !S.Local) - Result.PublicSymbols.insert(S.Name); - // Metadata nodes are numbered in the emitter's global order, so one added // node would renumber the references in every partition. Each partition // instead numbers the nodes it uses in order of first use, which keeps an @@ -512,10 +550,122 @@ std::optional partitionProgramText(StringRef Text, for (StringRef Line : NamedMetadata) scanReferences(Line, Index, NamedSymbols, NamedRoots); + // An internal definition that no other definition reaches is left out of + // every partition: the optimizer would delete it, and the emitter writes + // variants of helpers that the program never calls. Leaving them out + // shortens every partition's parse, summary and optimization, and since + // placement ignores them too, an edit that adds one changes no partition. + std::vector Live(Symbols.size(), 0); + { + std::vector Work; + auto Reach = [&](unsigned I) { + if (!Live[I]) { + Live[I] = 1; + Work.push_back(I); + } + }; + for (unsigned I = 0; I < Symbols.size(); ++I) + if (Symbols[I].Kind != SymbolKind::Declaration && !Symbols[I].Local) + Reach(I); + for (unsigned I : NamedSymbols) + Reach(I); + for (auto &Entry : Metadata) + for (unsigned I : Entry.second.Symbols) + Reach(I); + while (!Work.empty()) { + unsigned I = Work.back(); + Work.pop_back(); + for (unsigned Ref : Symbols[I].Refs) + Reach(Ref); + } + } + + // An internal definition reached from exactly one other definition is an + // inlining candidate of it, or data only it uses; keep it with that + // definition so its partition can inline it without importing, and so a + // helper an edit adds lands in the partition of the code that uses it. A + // chain through a global continues to the function that uses the global. + // A function body larger than this is past LLVM's inline cost limit even + // for the only call to a local function, so it is placed by its own name. + // A chain stops where it would leave a function's source-module group: + // module initializers, which only the entry point calls, keep their own + // partitions instead of all sharing the entry point's. + constexpr size_t MaxInlinedBodyBytes = 256 * 1024; + constexpr unsigned Shared = ~1u; + std::vector Referrer(Symbols.size(), NoPartition); + for (unsigned I = 0; I < Symbols.size(); ++I) { + const Symbol &S = Symbols[I]; + if (S.Kind == SymbolKind::Declaration || !Live[I]) + continue; + for (unsigned Ref : S.Refs) { + if (Ref == I) + continue; + Referrer[Ref] = Referrer[Ref] == NoPartition ? I : Shared; + } + } + auto Follows = [&](unsigned Child, unsigned Parent) { + const Symbol &S = Symbols[Child]; + if (!S.Local || Parent == NoPartition || Parent == Shared || + Symbols[Parent].Kind == SymbolKind::Declaration) + return false; + if (S.Kind == SymbolKind::Function && + (S.Body.size() > MaxInlinedBodyBytes || + (Symbols[Parent].Kind == SymbolKind::Function && !S.Group.empty() && + S.Group != Symbols[Parent].Group))) + return false; + return true; + }; + std::vector Roots(Symbols.size()); + for (unsigned I = 0; I < Symbols.size(); ++I) { + if (Symbols[I].Kind == SymbolKind::Declaration || !Live[I]) + continue; + unsigned Root = I; + for (unsigned Steps = 0; Steps < 1024; ++Steps) { + unsigned Parent = Referrer[Root]; + if (!Follows(Root, Parent) || Parent == I) + break; + Root = Parent; + } + // A function whose chain ends at a global that is not itself placed + // with a function (a table many functions use) is placed by its own + // name, so the functions of one table spread over the partitions. + if (Symbols[I].Kind == SymbolKind::Function && + Symbols[Root].Kind != SymbolKind::Function) + Root = I; + Roots[I] = Root; + if (Symbols[I].Kind == SymbolKind::Function) + Symbols[I].Partition = stableHash(Symbols[Root].Name) % Count; + } + placeGroups(Symbols, Roots, Count); + // A global follows the function its chain ends at, then the first + // function that references it, then its own name. + for (unsigned I = 0; I < Symbols.size(); ++I) + if (Symbols[I].Kind == SymbolKind::Variable && Live[I] && + Symbols[Roots[I]].Kind == SymbolKind::Function) + Symbols[I].Partition = Symbols[Roots[I]].Partition; + for (unsigned I = 0; I < Symbols.size(); ++I) { + const Symbol &S = Symbols[I]; + if (S.Kind != SymbolKind::Function || !Live[I]) + continue; + for (unsigned Ref : S.Refs) + if (Symbols[Ref].Kind == SymbolKind::Variable && + Symbols[Ref].Partition == NoPartition) + Symbols[Ref].Partition = S.Partition; + } + for (unsigned I = 0; I < Symbols.size(); ++I) + if (Symbols[I].Kind == SymbolKind::Variable && Live[I] && + Symbols[I].Partition == NoPartition) + Symbols[I].Partition = stableHash(Symbols[I].Name) % Count; + + ProgramPartitions Result; + for (const Symbol &S : Symbols) + if (S.Kind != SymbolKind::Declaration && !S.Local) + Result.PublicSymbols.insert(S.Name); + Result.Sources.resize(Count); std::vector> Owned(Count); for (unsigned I = 0; I < Symbols.size(); ++I) - if (Symbols[I].Kind != SymbolKind::Declaration) + if (Symbols[I].Kind != SymbolKind::Declaration && Live[I]) Owned[Symbols[I].Partition].push_back(I); auto Assemble = [&](unsigned P) { diff --git a/native/llvm-codegen/src/partition.h b/native/llvm-codegen/src/partition.h index 838919443..dc000d751 100644 --- a/native/llvm-codegen/src/partition.h +++ b/native/llvm-codegen/src/partition.h @@ -20,11 +20,19 @@ struct ProgramPartitions { }; // Divides the LLVM assembly scriptc emits into Count modules without parsing -// it, so the partitions can be parsed concurrently. Functions are placed by a -// stable hash of their name; an internal function referenced by exactly one -// other function stays with that function unless its body is too large to -// inline, and a global stays with the first function that references it. Returns nullopt for any construct outside the -// emitter's narrow top-level grammar; callers then split a parsed module. +// it, so the partitions can be parsed concurrently. In modules of 32 or +// more partitions, functions under a "; scriptc-group " line are placed +// with the other functions of that source-module group, which is divided +// into chunks when it is larger than the mean partition or 1 MiB; other +// functions are placed by a stable hash of their name. An internal +// definition referenced by exactly one other definition stays with it, also +// through a global, unless it is a function whose body is too large to +// inline or that would join a function of another source-module group; a +// global otherwise stays with the first function that references it. +// Internal definitions that no non-internal definition, named metadata or +// metadata node reaches are left out of every partition. Returns nullopt +// for any construct outside the emitter's narrow top-level grammar; callers +// then split a parsed module. std::optional partitionProgramText(llvm::StringRef Text, unsigned Count); diff --git a/package.json b/package.json index f4efc6d8d..b392696a9 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "test:ts7": "node scripts/test-ts7.mjs", "test:checker-identity": "node scripts/checker-identity.mjs", "generate:ts7-ast": "node scripts/generate-ts7-ast-schema.mjs", + "generate:ir-fields": "node scripts/generate-ir-fields.mjs", "test:fetch-conformance": "vitest run tests/harness/fetch-conformance.test.ts", "test:test262": "tsx tests/test262/run.ts", "test:test262:fetch": "node tests/test262/fetch.mjs", diff --git a/packages/cli-darwin-arm64/package.json b/packages/cli-darwin-arm64/package.json index 23584b60a..1d4921f48 100644 --- a/packages/cli-darwin-arm64/package.json +++ b/packages/cli-darwin-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/cli-darwin-arm64", - "version": "0.4.2", + "version": "0.4.5", "description": "Native scriptc compiler for darwin-arm64", "license": "Apache-2.0", "homepage": "https://scriptc.dev", @@ -18,9 +18,13 @@ "files": [ "dist" ], + "dependencies": { + "@scriptc/llvm-darwin-arm64": "workspace:*", + "@scriptc/runtime-darwin-arm64": "workspace:*" + }, "scripts": { "build": "node -e \"\"", - "build:native": "node --max-old-space-size=8192 --import tsx ../../scripts/build-native-cli.mts dist", + "build:native": "node --max-old-space-size=8192 --import tsx ../../scripts/build-native-cli.mts dist --platform-packages", "prepack": "node ../../scripts/verify-native-cli.mjs ." }, "publishConfig": { diff --git a/packages/cli-darwin-x64/package.json b/packages/cli-darwin-x64/package.json index d9e6dafc6..dca236b32 100644 --- a/packages/cli-darwin-x64/package.json +++ b/packages/cli-darwin-x64/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/cli-darwin-x64", - "version": "0.4.2", + "version": "0.4.5", "description": "Native scriptc compiler for darwin-x64", "license": "Apache-2.0", "homepage": "https://scriptc.dev", @@ -18,9 +18,13 @@ "files": [ "dist" ], + "dependencies": { + "@scriptc/llvm-darwin-x64": "workspace:*", + "@scriptc/runtime-darwin-x64": "workspace:*" + }, "scripts": { "build": "node -e \"\"", - "build:native": "node --max-old-space-size=8192 --import tsx ../../scripts/build-native-cli.mts dist", + "build:native": "node --max-old-space-size=8192 --import tsx ../../scripts/build-native-cli.mts dist --platform-packages", "prepack": "node ../../scripts/verify-native-cli.mjs ." }, "publishConfig": { diff --git a/packages/cli-linux-arm64-gnu/package.json b/packages/cli-linux-arm64-gnu/package.json index 5b8cb4db2..1459caa1d 100644 --- a/packages/cli-linux-arm64-gnu/package.json +++ b/packages/cli-linux-arm64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/cli-linux-arm64-gnu", - "version": "0.4.2", + "version": "0.4.5", "description": "Native scriptc compiler for linux-arm64-gnu", "license": "Apache-2.0", "homepage": "https://scriptc.dev", @@ -18,9 +18,13 @@ "files": [ "dist" ], + "dependencies": { + "@scriptc/llvm-linux-arm64-gnu": "workspace:*", + "@scriptc/runtime-linux-arm64-gnu": "workspace:*" + }, "scripts": { "build": "node -e \"\"", - "build:native": "node --max-old-space-size=8192 --import tsx ../../scripts/build-native-cli.mts dist", + "build:native": "node --max-old-space-size=8192 --import tsx ../../scripts/build-native-cli.mts dist --platform-packages", "prepack": "node ../../scripts/verify-native-cli.mjs ." }, "publishConfig": { diff --git a/packages/cli-linux-arm64-musl/package.json b/packages/cli-linux-arm64-musl/package.json index 0901602ec..3703c0059 100644 --- a/packages/cli-linux-arm64-musl/package.json +++ b/packages/cli-linux-arm64-musl/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/cli-linux-arm64-musl", - "version": "0.4.2", + "version": "0.4.5", "description": "Native scriptc compiler for linux-arm64-musl", "license": "Apache-2.0", "homepage": "https://scriptc.dev", @@ -18,9 +18,13 @@ "files": [ "dist" ], + "dependencies": { + "@scriptc/llvm-linux-arm64-musl": "workspace:*", + "@scriptc/runtime-linux-arm64-musl": "workspace:*" + }, "scripts": { "build": "node -e \"\"", - "build:native": "node --max-old-space-size=8192 --import tsx ../../scripts/build-native-cli.mts dist", + "build:native": "node --max-old-space-size=8192 --import tsx ../../scripts/build-native-cli.mts dist --platform-packages", "prepack": "node ../../scripts/verify-native-cli.mjs ." }, "publishConfig": { diff --git a/packages/cli-linux-x64-gnu/package.json b/packages/cli-linux-x64-gnu/package.json index f01478aad..6d70361f9 100644 --- a/packages/cli-linux-x64-gnu/package.json +++ b/packages/cli-linux-x64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/cli-linux-x64-gnu", - "version": "0.4.2", + "version": "0.4.5", "description": "Native scriptc compiler for linux-x64-gnu", "license": "Apache-2.0", "homepage": "https://scriptc.dev", @@ -18,9 +18,13 @@ "files": [ "dist" ], + "dependencies": { + "@scriptc/llvm-linux-x64-gnu": "workspace:*", + "@scriptc/runtime-linux-x64-gnu": "workspace:*" + }, "scripts": { "build": "node -e \"\"", - "build:native": "node --max-old-space-size=8192 --import tsx ../../scripts/build-native-cli.mts dist", + "build:native": "node --max-old-space-size=8192 --import tsx ../../scripts/build-native-cli.mts dist --platform-packages", "prepack": "node ../../scripts/verify-native-cli.mjs ." }, "publishConfig": { diff --git a/packages/cli-linux-x64-musl/package.json b/packages/cli-linux-x64-musl/package.json index a6986a66a..0c12fc60d 100644 --- a/packages/cli-linux-x64-musl/package.json +++ b/packages/cli-linux-x64-musl/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/cli-linux-x64-musl", - "version": "0.4.2", + "version": "0.4.5", "description": "Native scriptc compiler for linux-x64-musl", "license": "Apache-2.0", "homepage": "https://scriptc.dev", @@ -18,9 +18,13 @@ "files": [ "dist" ], + "dependencies": { + "@scriptc/llvm-linux-x64-musl": "workspace:*", + "@scriptc/runtime-linux-x64-musl": "workspace:*" + }, "scripts": { "build": "node -e \"\"", - "build:native": "node --max-old-space-size=8192 --import tsx ../../scripts/build-native-cli.mts dist", + "build:native": "node --max-old-space-size=8192 --import tsx ../../scripts/build-native-cli.mts dist --platform-packages", "prepack": "node ../../scripts/verify-native-cli.mjs ." }, "publishConfig": { diff --git a/packages/cli-win32-x64-msvc/package.json b/packages/cli-win32-x64-msvc/package.json index 6b03811b8..8872d2f15 100644 --- a/packages/cli-win32-x64-msvc/package.json +++ b/packages/cli-win32-x64-msvc/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/cli-win32-x64-msvc", - "version": "0.4.2", + "version": "0.4.5", "description": "Native scriptc compiler for win32-x64-msvc", "license": "Apache-2.0", "homepage": "https://scriptc.dev", @@ -18,9 +18,13 @@ "files": [ "dist" ], + "dependencies": { + "@scriptc/llvm-win32-x64-msvc": "workspace:*", + "@scriptc/runtime-win32-x64-msvc": "workspace:*" + }, "scripts": { "build": "node -e \"\"", - "build:native": "node --max-old-space-size=8192 --import tsx ../../scripts/build-native-cli.mts dist", + "build:native": "node --max-old-space-size=8192 --import tsx ../../scripts/build-native-cli.mts dist --platform-packages", "prepack": "node ../../scripts/verify-native-cli.mjs ." }, "publishConfig": { diff --git a/packages/cli/package.json b/packages/cli/package.json index 034747c29..59f9db90a 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "scriptc", - "version": "0.4.2", + "version": "0.4.5", "description": "Compile ordinary TypeScript and JavaScript to small, fast native executables and WebAssembly modules that run without Node.js", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/cli/scripts/install-native.mjs b/packages/cli/scripts/install-native.mjs index 84eb846bc..e8317d341 100644 --- a/packages/cli/scripts/install-native.mjs +++ b/packages/cli/scripts/install-native.mjs @@ -49,6 +49,7 @@ export function relocateToolchain(manifest, sourceDirectory, destinationDirector "declarations", "comptime", "wasi_node_runner", + "lib_diagnostics", ]) { if (typeof relocated[name] === "string") relocated[name] = pathFrom(relocated[name]); } @@ -92,6 +93,34 @@ export function installNativeCli( const bin = join(directory, "bin"); mkdirSync(bin, { recursive: true }); const manifest = relocateToolchain(original, sourceDirectory, bin); + // A platform package depends on its LLVM helper and runtime pack packages + // instead of bundling them. Record where the package manager put them. + if (manifest.llvm_package === undefined || manifest.runtime_pack === undefined) { + const dependencies = Object.keys(identity.dependencies ?? {}); + const installed = (prefix) => { + const name = dependencies.find((dependency) => dependency.startsWith(prefix)); + if (name === undefined) + throw new Error(`${packageName} does not depend on a ${prefix}* package`); + let path, found; + try { + path = createRequire(platformManifest).resolve(`${name}/package.json`); + found = JSON.parse(readFileSync(path, "utf8")).version; + } catch { + throw new Error(`scriptc requires ${name}@${version}; reinstall scriptc`); + } + if (found !== version) + throw new Error(`scriptc requires ${name}@${version}, found ${found}; reinstall scriptc`); + return relative(bin, dirname(path)); + }; + manifest.llvm_package ??= installed("@scriptc/llvm-"); + if (manifest.runtime_pack === undefined) { + manifest.runtime_pack = installed("@scriptc/runtime-"); + manifest.runtime_packs = [ + { target: manifest.target, path: manifest.runtime_pack }, + ...(manifest.runtime_packs ?? []), + ]; + } + } // npm normalizes modes for payloads outside package bin entries. These // tools are launched directly by the compiler after installation. if (process.platform !== "win32") { diff --git a/packages/cli/src/bootstrap.ts b/packages/cli/src/bootstrap.ts index 2628d26a7..615fe694f 100644 --- a/packages/cli/src/bootstrap.ts +++ b/packages/cli/src/bootstrap.ts @@ -73,6 +73,8 @@ async function tryFastPath(): Promise { // The full compiler keys these builds by the recorded libnode pack. values.libnode !== undefined || process.env["SCRIPTC_LIBNODE"] !== undefined || + // The full compiler resolves `native` against the build host. + values["target-cpu"] !== undefined || (values["external-types"] ?? []).length > 0 || // The full CLI validates the Node island policy flags. hasNodeIslandFlags(values) @@ -142,8 +144,14 @@ async function tryFastPath(): Promise { ...(optimization === "dev" || optimization === "speed" ? { optimization } : {}), ...(values.strip ? { strip: true as const } : {}), ...(values["detached-exit"] ? { detachedExit: true as const } : {}), + ...(values["cpu-dispatch"] === true ? { cpuDispatch: true as const } : {}), ...(checker === undefined ? {} : { checker }), npmStatic, + // Mirrors the full compiler's key: shape inference's overlay is opt-in + // (SCRIPTC_SHAPE_OVERLAY=1) and --no-shape-overlay turns it off. + ...(process.env["SCRIPTC_SHAPE_OVERLAY"] === "1" && values["shape-overlay"] !== false + ? { shapeOverlay: true as const } + : {}), ffiProfile: ffiPath === null ? null : { path: ffiPath, bytes: ffiBytes! }, target: `${process.env["SCRIPTC_TARGET"] ?? "native"}:${buildPlatform}:${process.arch}:${ helperObjectRoute ? "runtime-pack" : "driver-tu" @@ -161,6 +169,8 @@ async function tryFastPath(): Promise { ) : startup.executableNativeEnvironmentFingerprint(), nodeVersion: process.version, + // An entry of a program that calls into Node.js serves only the same pack. + libnodeIdentity: () => startup.libnodeCacheKey(values.libnode), }); if (hit === null) return null; if (!values["keep-llvm"]) await rm(hit.llvmPath, { force: true }); @@ -220,6 +230,8 @@ async function coverageFastPath( values.emit !== undefined || values.strip || values["detached-exit"] || + values["target-cpu"] !== undefined || + values["cpu-dispatch"] !== undefined || values.lib || values.profile !== undefined || values.ffi !== undefined || @@ -229,6 +241,7 @@ async function coverageFastPath( values["provenance-sources"] || values["emit-ir"] || (values["npm-static"] ?? []).length > 0 || + values["shape-overlay"] === false || (values["external-types"] ?? []).length > 0 || // The replay covers the default Node island policy only. hasNodeIslandFlags(values) diff --git a/packages/cli/src/main.ts b/packages/cli/src/main.ts index 8203b959e..5deb4ca29 100644 --- a/packages/cli/src/main.ts +++ b/packages/cli/src/main.ts @@ -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((resolveExit) => { let child; diff --git a/packages/cli/test/cpu-dispatch.test.ts b/packages/cli/test/cpu-dispatch.test.ts new file mode 100644 index 000000000..c20202aa0 --- /dev/null +++ b/packages/cli/test/cpu-dispatch.test.ts @@ -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 => { + 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 => + ( + 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, +); diff --git a/packages/cli/test/native-install.test.ts b/packages/cli/test/native-install.test.ts index c75b54267..7a6d3ef16 100644 --- a/packages/cli/test/native-install.test.ts +++ b/packages/cli/test/native-install.test.ts @@ -7,6 +7,7 @@ import { renameSync, rmSync, statSync, + symlinkSync, writeFileSync, } from "node:fs"; import { tmpdir } from "node:os"; @@ -53,6 +54,7 @@ function fixture() { dsymutil: "dsymutil", comptime: "../lib/comptime", wasi_node_runner: "../lib/wasi/cli/wasi-runner.js", + lib_diagnostics: "../lib/lib-diagnostics.json", }; writeFileSync(binary + ".json", JSON.stringify(toolchain)); const helpers = [ @@ -93,6 +95,9 @@ test("installs a direct executable and relocatable references to platform assets expect(resolve(dirname(installed), manifest.wasi_node_runner)).toBe( join(platform, "wasi/cli/wasi-runner.js"), ); + expect(resolve(dirname(installed), manifest.lib_diagnostics)).toBe( + join(platform, "lib-diagnostics.json"), + ); expect(resolve(dirname(installed), manifest.runtime_packs[0].path)).toBe( join(platform, "runtime"), ); @@ -100,6 +105,71 @@ test("installs a direct executable and relocatable references to platform assets expect(JSON.stringify(manifest)).not.toContain(f.directory); }); +test.each(["hoisted", "pnpm"])( + "resolves the LLVM helper and runtime pack a platform package depends on (%s)", + (layout) => { + const f = fixture(); + const helper = "@scriptc/llvm-darwin-arm64"; + const runtime = "@scriptc/runtime-darwin-arm64"; + const toolchain: Record = { ...f.toolchain }; + delete toolchain.llvm_package; + delete toolchain.runtime_pack; + delete toolchain.runtime_packs; + writeFileSync(f.binary + ".json", JSON.stringify(toolchain)); + // pnpm links a package's dependencies beside it in its virtual store, + // where the installation's node_modules does not see them. + const platform = + layout === "pnpm" + ? join(f.directory, "node_modules/.pnpm/cli@1.2.3/node_modules", f.packageName) + : f.platform; + if (layout === "pnpm") { + mkdirSync(dirname(platform), { recursive: true }); + renameSync(f.platform, platform); + symlinkSync(platform, f.platform, "junction"); + } + const modules = + layout === "pnpm" ? dirname(dirname(platform)) : join(f.directory, "node_modules"); + writeFileSync( + join(platform, "package.json"), + JSON.stringify({ + name: f.packageName, + version: "1.2.3", + dependencies: { [helper]: "1.2.3", [runtime]: "1.2.3" }, + }), + ); + for (const name of [helper, runtime]) { + mkdirSync(join(modules, name), { recursive: true }); + writeFileSync( + join(modules, name, "package.json"), + JSON.stringify({ name, version: "1.2.3" }), + ); + } + mkdirSync(join(modules, helper, "bin")); + writeFileSync(join(modules, helper, "bin/scriptc-llvm-codegen"), "helper", { mode: 0o644 }); + const command = installNativeCli(f.directory, f.packageName); + const manifest = JSON.parse(readFileSync(command + ".json", "utf8")); + const bin = dirname(command); + expect(resolve(bin, manifest.llvm_package)).toBe(join(modules, helper)); + expect(resolve(bin, manifest.runtime_pack)).toBe(join(modules, runtime)); + expect(manifest.runtime_packs).toEqual([ + { target: "macos-arm64", path: manifest.runtime_pack }, + ]); + if (process.platform !== "win32") + expect(statSync(join(modules, helper, "bin/scriptc-llvm-codegen")).mode & 0o111).toBe(0o111); + writeFileSync( + join(modules, runtime, "package.json"), + JSON.stringify({ name: runtime, version: "1.2.4" }), + ); + expect(() => installNativeCli(f.directory, f.packageName)).toThrow( + `scriptc requires ${runtime}@1.2.3, found 1.2.4`, + ); + rmSync(join(modules, runtime), { recursive: true }); + expect(() => installNativeCli(f.directory, f.packageName)).toThrow( + `scriptc requires ${runtime}@1.2.3; reinstall scriptc`, + ); + }, +); + test("missing and mismatched packages fail without replacing an installed command", () => { const f = fixture(); const command = installNativeCli(f.directory, f.packageName); diff --git a/packages/cli/test/native-link-info.test.ts b/packages/cli/test/native-link-info.test.ts index 8ac579449..b838b3155 100644 --- a/packages/cli/test/native-link-info.test.ts +++ b/packages/cli/test/native-link-info.test.ts @@ -72,7 +72,7 @@ describe.runIf(supported)("macOS arm64 native link info", () => { minimum_os: "14.0", }, program: { object, entry_symbol: "main" }, - runtime_abi: { version: 37, marker: "scr_runtime_abi_v37" }, + runtime_abi: { version: 47, marker: "scr_runtime_abi_v47" }, runtime_pack: { kind: "precompiled", package: "@scriptc/runtime-darwin-arm64", @@ -165,6 +165,6 @@ describe.runIf(supported)("macOS arm64 native link info", () => { () => null, (failure: { stderr?: string }) => failure, ); - expect(error?.stderr).toContain("scr_runtime_abi_v37"); + expect(error?.stderr).toContain("scr_runtime_abi_v47"); }); }); diff --git a/packages/cli/test/output-options.test.ts b/packages/cli/test/output-options.test.ts index 21c5726c7..b9eb94843 100644 --- a/packages/cli/test/output-options.test.ts +++ b/packages/cli/test/output-options.test.ts @@ -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/], diff --git a/packages/cli/test/profile.test.ts b/packages/cli/test/profile.test.ts index 24c759e92..da40a15b6 100644 --- a/packages/cli/test/profile.test.ts +++ b/packages/cli/test/profile.test.ts @@ -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 }); } diff --git a/packages/cli/test/target-cpu.test.ts b/packages/cli/test/target-cpu.test.ts new file mode 100644 index 000000000..5bc6717c3 --- /dev/null +++ b/packages/cli/test/target-cpu.test.ts @@ -0,0 +1,102 @@ +import { execFile } from "node:child_process"; +import { createRequire } from "node:module"; +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import { promisify } from "node:util"; +import { expect, test } from "vitest"; +import { hostTargetCpu } from "../../compiler/src/backend/target-cpu.js"; + +const execFileAsync = promisify(execFile); +const require = createRequire(import.meta.url); +const repoRoot = join(import.meta.dirname, "../../.."); +const cliEntry = join(repoRoot, "packages/cli/src/main.ts"); +const bootstrapEntry = join(repoRoot, "packages/cli/dist/bootstrap.js"); +const tsxLoader = join(dirname(require.resolve("tsx/package.json")), "dist/loader.mjs"); +const host = hostTargetCpu(); +const runsV3 = host === "x86-64-v3" || host === "x86-64-v4"; + +const program = `const a = new Int32Array(4096); +for (let i = 0; i < a.length; i++) a[i] = (i * 2654435761) | 0; +let s = 0; +for (let r = 0; r < 64; r++) for (let i = 0; i < a.length; i++) s = (s + a[i]!) | 0; +console.log(s); +`; + +test.runIf(process.platform === "linux" && process.arch === "x64")( + "--target-cpu compiles the program for the level without mixing cache variants", + async () => { + const dir = await mkdtemp(join("/tmp", "scriptc-target-cpu-")); + const entry = join(dir, "main.ts"); + const outPath = join(dir, "program"); + const llvmPath = join(dir, "main.ll"); + const env = { ...process.env, SCRIPTC_CACHE_DIR: join(dir, "cache") }; + const build = async (args: string[], bootstrap = false): Promise => { + await execFileAsync( + process.execPath, + [ + ...(bootstrap ? [bootstrapEntry] : ["--import", tsxLoader, cliEntry]), + "build", + entry, + "-o", + outPath, + ...args, + ], + { env, maxBuffer: 4 * 1024 * 1024 }, + ); + return readFile(llvmPath, "utf8"); + }; + try { + await writeFile(entry, program); + const expected = `${(await execFileAsync(process.execPath, [entry])).stdout}`; + const baseline = await build([]); + expect(baseline).not.toContain('"target-cpu"'); + // Only --sanitize builds mark the functions for ASan instrumentation. + expect(baseline).not.toContain("sanitize_address"); + expect(await build(["--target-cpu=baseline"])).toBe(baseline); + const v3 = await build(["--target-cpu=x86-64-v3"]); + expect(v3).toMatch(/^attributes #0 = \{ "target-cpu"="x86-64-v3" \}$/m); + if (runsV3) expect((await execFileAsync(outPath)).stdout).toBe(expected); + // The bootstrap's cached route builds without a level. + expect(await build([], true)).toBe(baseline); + expect((await execFileAsync(outPath)).stdout).toBe(expected); + await expect(build(["--target-cpu=haswell"])).rejects.toThrow(/unknown target CPU "haswell"/); + } finally { + await rm(dir, { recursive: true, force: true }); + } + }, + 240_000, +); + +test("--target-cpu levels are refused for arm64 and WASI targets", async () => { + const dir = await mkdtemp(join("/tmp", "scriptc-target-cpu-refusal-")); + const entry = join(dir, "main.ts"); + try { + await writeFile(entry, program); + for (const target of ["aarch64-unknown-linux-gnu", "wasm32-wasi"]) { + for (const kind of ["llvm", "asm", "obj", "exe"]) { + const build = execFileAsync( + process.execPath, + [ + "--import", + tsxLoader, + cliEntry, + "build", + entry, + `--emit=${kind}`, + "-o", + join(dir, `out-${kind}`), + "--target-cpu=x86-64-v3", + ], + { + env: { ...process.env, SCRIPTC_TARGET: target, SCRIPTC_CACHE_DIR: join(dir, "cache") }, + }, + ); + await expect(build, `${target} ${kind}`).rejects.toThrow( + /--target-cpu=x86-64-v3 requires an x86-64 target/, + ); + } + } + } finally { + await rm(dir, { recursive: true, force: true }); + } +}, 240_000); diff --git a/packages/compiler/ambient/node-fallback/assert-strict.d.ts b/packages/compiler/ambient/node-fallback/assert-strict.d.ts new file mode 100644 index 000000000..ee0eaf8fa --- /dev/null +++ b/packages/compiler/ambient/node-fallback/assert-strict.d.ts @@ -0,0 +1,47 @@ +/// + +/* node:assert/strict — the same surface with the loose names bound to the + * strict comparisons (equal IS strictEqual, ...), exactly Node's aliasing; + * the module object is callable like assert's. */ +declare module "assert/strict" { + function strict(value: unknown, message?: string): void; + namespace strict { + function ok(value: unknown, message?: string): void; + function equal(actual: unknown, expected: unknown, message?: string): void; + function notEqual(actual: unknown, expected: unknown, message?: string): void; + function deepEqual(actual: unknown, expected: unknown, message?: string): void; + function notDeepEqual(actual: unknown, expected: unknown, message?: string): void; + function strictEqual(actual: unknown, expected: unknown, message?: string): void; + function notStrictEqual(actual: unknown, expected: unknown, message?: string): void; + function deepStrictEqual(actual: unknown, expected: unknown, message?: string): void; + function notDeepStrictEqual(actual: unknown, expected: unknown, message?: string): void; + function partialDeepStrictEqual(actual: unknown, expected: unknown, message?: string): void; + function fail(message?: string): never; + function throws(fn: () => unknown, expected?: unknown, message?: string): void; + function doesNotThrow(fn: () => unknown, expected?: unknown, message?: string): void; + function rejects( + fn: (() => Promise) | Promise, + expected?: unknown, + message?: string, + ): Promise; + function doesNotReject( + fn: (() => Promise) | Promise, + expected?: unknown, + message?: string, + ): Promise; + function match(value: string, regExp: RegExp, message?: string): void; + function doesNotMatch(value: string, regExp: RegExp, message?: string): void; + function ifError(value: unknown): void; + const strict: typeof import("assert/strict"); + type AssertOptions = import("assert").AssertOptions; + type Assert = import("assert").Assert; + type AssertStrict = import("assert").AssertStrict; + const Assert: typeof import("assert").Assert; + } + export = strict; +} + +declare module "node:assert/strict" { + import strict = require("assert/strict"); + export = strict; +} diff --git a/packages/compiler/ambient/node-fallback/assert.d.ts b/packages/compiler/ambient/node-fallback/assert.d.ts new file mode 100644 index 000000000..830d33e01 --- /dev/null +++ b/packages/compiler/ambient/node-fallback/assert.d.ts @@ -0,0 +1,86 @@ +/// + +declare module "assert" { + function assert(value: unknown, message?: string): void; + namespace assert { + interface AssertionErrorOptions { + message?: string | undefined; + actual?: unknown; + expected?: unknown; + operator?: string | undefined; + stackStartFn?: Function | undefined; + diff?: "simple" | "full" | undefined; + } + class AssertionError extends Error { + constructor(options: AssertionErrorOptions); + actual: unknown; + expected: unknown; + generatedMessage: boolean; + code: "ERR_ASSERTION"; + operator: string; + } + function ok(value: unknown, message?: string): void; + function strictEqual(actual: unknown, expected: unknown, message?: string): void; + function notStrictEqual(actual: unknown, expected: unknown, message?: string): void; + function deepStrictEqual(actual: unknown, expected: unknown, message?: string): void; + function notDeepStrictEqual(actual: unknown, expected: unknown, message?: string): void; + function partialDeepStrictEqual(actual: unknown, expected: unknown, message?: string): void; + function equal(actual: unknown, expected: unknown, message?: string): void; + function notEqual(actual: unknown, expected: unknown, message?: string): void; + function deepEqual(actual: unknown, expected: unknown, message?: string): void; + function notDeepEqual(actual: unknown, expected: unknown, message?: string): void; + function fail(message?: string): never; + function throws(fn: () => unknown, expected?: unknown, message?: string): void; + function doesNotThrow(fn: () => unknown, expected?: unknown, message?: string): void; + function rejects( + fn: (() => Promise) | Promise, + expected?: unknown, + message?: string, + ): Promise; + function doesNotReject( + fn: (() => Promise) | Promise, + expected?: unknown, + message?: string, + ): Promise; + function match(value: string, regExp: RegExp, message?: string): void; + function doesNotMatch(value: string, regExp: RegExp, message?: string): void; + function ifError(value: unknown): void; + const strict: typeof import("assert/strict"); + interface AssertOptions { + diff?: "simple" | "full" | undefined; + strict?: boolean | undefined; + skipPrototype?: boolean | undefined; + } + type AssertMethodNames = + | "deepEqual" + | "deepStrictEqual" + | "doesNotMatch" + | "doesNotReject" + | "doesNotThrow" + | "equal" + | "fail" + | "ifError" + | "match" + | "notDeepEqual" + | "notDeepStrictEqual" + | "notEqual" + | "notStrictEqual" + | "ok" + | "partialDeepStrictEqual" + | "rejects" + | "strictEqual" + | "throws"; + interface Assert extends Pick {} + interface AssertStrict extends Pick {} + const Assert: { + new (options?: AssertOptions & { strict?: true | undefined }): AssertStrict; + new (options: AssertOptions): Assert; + }; + } + export = assert; +} + +declare module "node:assert" { + import assert = require("assert"); + export = assert; +} diff --git a/packages/compiler/ambient/node-fallback/async_hooks.d.ts b/packages/compiler/ambient/node-fallback/async_hooks.d.ts new file mode 100644 index 000000000..91082ee12 --- /dev/null +++ b/packages/compiler/ambient/node-fallback/async_hooks.d.ts @@ -0,0 +1,41 @@ +/* node:async_hooks — declared surface without lowering: async-hook + * instrumentation is guarded behind env flags in harness code; a reached + * use fences at its site. */ +declare module "async_hooks" { + /* AsyncLocalStorage: fiber-carried context snapshots — run/getStore/ + * exit/enterWith/disable lower onto the runtime's active-slot machinery + * (als.* libCalls); values cross as dyn values. bind and snapshot bind + * functions to the active context; the options carry the store's + * default value and name. */ + export interface AsyncLocalStorageOptions { + defaultValue?: any; + name?: string | undefined; + } + export class AsyncLocalStorage { + constructor(options?: AsyncLocalStorageOptions); + static bind any>(fn: Func): Func; + static snapshot(): (fn: (...args: TArgs) => R, ...args: TArgs) => R; + readonly name: string; + run(store: T, callback: (...args: any[]) => R, ...args: any[]): R; + exit(callback: (...args: any[]) => R, ...args: any[]): R; + getStore(): T | undefined; + enterWith(store: T): void; + disable(): void; + } + export interface AsyncHook { + enable(): AsyncHook; + disable(): AsyncHook; + } + export function createHook(callbacks: { + init?: (asyncId: number, type: string, triggerAsyncId: number, resource: unknown) => void; + before?: (asyncId: number) => void; + after?: (asyncId: number) => void; + destroy?: (asyncId: number) => void; + }): AsyncHook; + export function executionAsyncId(): number; + export function triggerAsyncId(): number; +} + +declare module "node:async_hooks" { + export * from "async_hooks"; +} diff --git a/packages/compiler/ambient/node-fallback/buffer.d.ts b/packages/compiler/ambient/node-fallback/buffer.d.ts new file mode 100644 index 000000000..c37b91c70 --- /dev/null +++ b/packages/compiler/ambient/node-fallback/buffer.d.ts @@ -0,0 +1,23 @@ +/* node:buffer — the module spelling of the Buffer/atob/btoa globals. + * Buffer re-exports the GLOBAL declaration, so an imported binding + * resolves to the same symbol the lowerings already answer for (and + * third-party .d.ts files that `import { Buffer } from "node:buffer"` — + * file-entry-cache in a typed-JS source graph — typecheck). */ +declare module "buffer" { + export function isAscii(input: ArrayBuffer | ArrayBufferView): boolean; + export function isUtf8(input: ArrayBuffer | ArrayBufferView): boolean; + export function transcode(source: Uint8Array, fromEncoding: string, toEncoding: string): Buffer; + export function atob(data: string): string; + export function btoa(data: string): string; + export const kMaxLength: number; + export const kStringMaxLength: number; + export const Buffer: BufferConstructor; + export type Blob = globalThis.Blob; + export type File = globalThis.File; + export const Blob: typeof globalThis.Blob; + export const File: typeof globalThis.File; +} + +declare module "node:buffer" { + export * from "buffer"; +} diff --git a/packages/compiler/ambient/node-fallback/child_process.d.ts b/packages/compiler/ambient/node-fallback/child_process.d.ts new file mode 100644 index 000000000..c9e6483c4 --- /dev/null +++ b/packages/compiler/ambient/node-fallback/child_process.d.ts @@ -0,0 +1,223 @@ +/* node:child_process — the synchronous slice: spawnSync(command, args?) + * runs the child to completion (posix_spawn + waitpid) with stdout/stderr + * captured as utf8 strings. `status` is the exit code, or null when the + * child died to a signal OR could not be spawned at all (nonexistent + * binary, permission denied) — Node reports spawn failure through the + * result's `error` property, which this surface does not carry, and types + * stdout/stderr null there where scriptc returns "" (SEMANTICS.md). + * No shell, no options object: the command runs directly (PATH-searched, + * like Node). */ +declare module "child_process" { + export interface SpawnSyncReturns { + readonly status: number | null; + readonly stdout: string; + readonly stderr: string; + /* The termination signal's name ("SIGTERM", ...) when a signal + * killed the child (a timeout's killSignal included), null otherwise + * — Node's shape. */ + readonly signal: string | null; + /* Node's spawn-failure carrier: a real Error ("spawnSync + * ENOENT", `code` stamped) when the spawn itself failed — status is + * null and both outputs "" then — or ETIMEDOUT after a timeout kill; + * undefined otherwise. */ + readonly error?: Error; + } + /* scriptc always captures utf8 strings; pass { encoding: "utf8" } + * whenever stdout/stderr is READ so plain Node agrees (without it Node + * hands Buffers). The other supported options: timeout (ms — sends + * killSignal at the deadline and the result carries error: ETIMEDOUT + + * the signal, never a throw), killSignal (a signal NAME literal), + * stdio ("ignore"/"inherit"/"pipe" or a 3-tuple of those — captures + * read "" for non-piped streams), env (replaces the child's + * environment), and windowsHide (a POSIX no-op). */ + export function spawnSync( + command: string, + args?: string[], + options?: { + encoding?: "utf8" | "utf-8"; + timeout?: number; + killSignal?: string; + stdio?: "pipe" | "ignore" | "inherit" | ("pipe" | "ignore" | "inherit")[]; + env?: { [k: string]: string | undefined }; + windowsHide?: boolean; + }, + ): SpawnSyncReturns; + + /* The asynchronous slice: spawn with stdio "ignore"/"inherit"/fd/"pipe" + * tuples (piped stdin is writable through child.stdin; piped stdout/stderr + * deliver through child.stdout/stderr) and the terminal events. "exit" + * fires once with the exit code, or + * null when the child died to a signal; "error" fires ONLY when the + * child could not be spawned at all (Node's split: a spawn failure + * emits "error" and never "exit"). "close" fires after exit/error and + * every piped stdio handle reaches EOF. Exit/close listeners may take + * Node's code and signal parameters. `on`/`once` return void here (Node + * returns the child; chaining is fenced). The event loop keeps the process + * alive until every spawned child is reaped, like Node — reaping polls at + * loop quiescence (SEMANTICS.md documents the divergence). An "error" + * event with no registered listener prints the error and exits 1, + * exactly the unhandled-'error' EventEmitter behavior. */ + export interface ChildStdin { + readonly writable: boolean; + write(chunk: string | Uint8Array): boolean; + end(): void; + destroy(): void; + on(event: "drain" | "finish", listener: () => void): void; + on(event: "error", listener: (err: Error) => void): void; + on(event: string, listener: (...args: any[]) => void): void; + once(event: "drain" | "finish", listener: () => void): void; + once(event: "error", listener: (err: Error) => void): void; + once(event: string, listener: (...args: any[]) => void): void; + } + + export interface ChildProcess extends Disposable { + ref(): void; + on(event: "spawn", listener: () => void): this; + once(event: "spawn", listener: () => void): this; + /* The exit listener may also take Node's second parameter — the + * terminating signal's name, null for a normal exit. */ + on(event: "exit", listener: (code: number | null, signal: string | null) => void): this; + on(event: "close", listener: (code: number | null, signal: string | null) => void): this; + on(event: "error", listener: (err: Error) => void): this; + once(event: "exit", listener: (code: number | null, signal: string | null) => void): this; + once(event: "close", listener: (code: number | null, signal: string | null) => void): this; + once(event: "error", listener: (err: Error) => void): this; + on(event: "message", listener: (message: any) => void): this; + on(event: "disconnect", listener: () => void): this; + once(event: "message", listener: (message: any) => void): this; + once(event: "disconnect", listener: () => void): this; + /* The lifecycle members, Node's exact shapes: pid is undefined exactly + * when the spawn failed; exitCode is null while running, the code + * after a normal exit, null for a signal death, and -errno once a + * spawn failure's "error" event has fired; kill resolves signal names + * through Node's table (unknown names throw the Unknown signal + * TypeError), answers true when the signal was sent, false once the + * child was reaped (or never spawned), and sets `killed` on success; + * unref() drops the child from the event loop's keep-alive set (it is + * still reaped while the loop runs for other reasons — SEMANTICS.md + * has the reap story). */ + readonly pid: number | undefined; + readonly exitCode: number | null; + readonly killed: boolean; + kill(signal?: string | number): boolean; + unref(): void; + readonly connected: boolean; + send(message: object, callback?: (error: Error | null) => void): boolean; + disconnect(): void; + [Symbol.dispose](): void; + /* The piped streams — non-null exactly when the matching stdio slot + * was "pipe" (Node's shape). */ + readonly stdin: ChildStdin | null; + readonly stdout: NodeJS.ReadableStream | null; + readonly stderr: NodeJS.ReadableStream | null; + } + export interface SpawnOptions { + /* The 3-tuple form admits number fds in the stdout/stderr slots — + * openSync results dup2'd into the child (the daemon-log idiom + * ["ignore", logFd, logFd]) — and "pipe" there too (child.stdout/ + * child.stderr streams); "pipe" in the stdin slot exposes + * child.stdin. */ + stdio?: "ignore" | "inherit" | "pipe" | ("ignore" | "inherit" | "pipe" | number)[]; + /* detached gives the child its own session and process group + * (POSIX_SPAWN_SETSID); env REPLACES the child environment; cwd + * sets its working directory; windowsHide is a POSIX no-op; shell + * runs the complete command string through /bin/sh or cmd.exe. */ + detached?: boolean; + env?: { [k: string]: string | undefined }; + cwd?: string; + windowsHide?: boolean; + shell?: boolean; + } + export function spawn(command: string, args?: string[], options?: SpawnOptions): ChildProcess; + export function spawn(command: string, options?: SpawnOptions): ChildProcess; + + export function fork( + modulePath: string | URL, + args?: string[], + options?: { + cwd?: string; + env?: { [k: string]: string | undefined }; + execArgv?: string[]; + silent?: boolean; + stdio?: "pipe" | "ignore" | "inherit" | ("pipe" | "ignore" | "inherit" | "ipc")[]; + serialization?: "json" | "advanced"; + windowsHide?: boolean; + }, + ): ChildProcess; + + /* The synchronous exec pair: execFileSync runs a file directly (no + * shell), execSync runs a command through /bin/sh -c. Both capture + * stdout as a utf8 string (pass { encoding: "utf8" } so plain Node + * agrees — without it Node returns a Buffer) and THROW on a non-zero + * exit / signal death — the Error's message is Node's "Command failed: + * " (with the captured stderr appended). Supported options: + * encoding ("utf8"), cwd, env (REPLACES the child environment), input + * (fed to stdin), timeout (ms — SIGTERMs the child, then throws + * ETIMEDOUT), stdio ("pipe"/"ignore" or a 3-tuple of those — the + * default captures stderr AND echoes it to the parent, like Node), and + * maxBuffer (accepted, not enforced). Node's status/stdout/stderr error + * properties are not carried (SEMANTICS.md). */ + interface ExecSyncOptions { + encoding?: "utf8" | "utf-8"; + cwd?: string; + env?: { [k: string]: string | undefined }; + input?: string; + timeout?: number; + stdio?: "pipe" | "ignore" | "inherit" | ("pipe" | "ignore" | "inherit")[]; + maxBuffer?: number; + killSignal?: string; + windowsHide?: boolean; + shell?: boolean; + } + /* The named options interface for the string-encoding exec form — + * @types/node's ExecFileSyncOptionsWithStringEncoding, narrowed to the + * honestly-implemented members. VALUES of this type are a real record + * (type-mapper.ts interns the shape), so a typed const or a runner function's + * options parameter flows to execFileSync at runtime — the windows-ca + * command-runner idiom. */ + export interface ExecFileSyncOptionsWithStringEncoding { + encoding: "utf8" | "utf-8"; + cwd?: string; + input?: string; + timeout?: number; + stdio?: "pipe" | "ignore" | "inherit" | ("pipe" | "ignore" | "inherit")[]; + maxBuffer?: number; + windowsHide?: boolean; + } + export function execFileSync(file: string, args?: string[], options?: ExecSyncOptions): string; + export function execSync(command: string, options?: ExecSyncOptions): string; + export type ExecFileCallback = (error: Error | null, stdout: string, stderr: string) => void; + /* The callback form starts an asynchronous child and captures utf8 + * stdout/stderr. The no-callback overload remains for util.promisify's + * static target recognition; reached no-callback calls still fence. */ + export function execFile(file: string, callback: ExecFileCallback): ChildProcess; + export function execFile( + file: string, + args: string[] | null, + callback: ExecFileCallback, + ): ChildProcess; + export function execFile( + file: string, + args: string[] | null, + options: ExecSyncOptions, + callback: ExecFileCallback, + ): ChildProcess; + export function execFile( + file: string, + args?: string[] | null, + options?: ExecSyncOptions, + ): unknown; + /* The async shell form. The callback forms lower like execFile's, with + * the command line run by /bin/sh -c, and util.promisify recognizes it + * statically, as it does execFile; reached calls without a callback + * fence at their site. */ + export function exec( + command: string, + options?: ExecSyncOptions | ((error: Error | null, stdout: string, stderr: string) => void), + callback?: (error: Error | null, stdout: string, stderr: string) => void, + ): ChildProcess; +} + +declare module "node:child_process" { + export * from "child_process"; +} diff --git a/packages/compiler/ambient/node-fallback/cluster.d.ts b/packages/compiler/ambient/node-fallback/cluster.d.ts new file mode 100644 index 000000000..7cb89b09f --- /dev/null +++ b/packages/compiler/ambient/node-fallback/cluster.d.ts @@ -0,0 +1,17 @@ +/* node:cluster — the primary-probe slice. A compiled binary never runs + * as a cluster worker (cluster forks the node binary itself — the + * mechanism is na for compiled programs), so isPrimary lowers to `true` + * and isWorker to `false`; everything else is undeclared. */ +declare module "cluster" { + const cluster: { + readonly isPrimary: boolean; + readonly isMaster: boolean; + readonly isWorker: boolean; + }; + export = cluster; +} + +declare module "node:cluster" { + import cluster = require("cluster"); + export = cluster; +} diff --git a/packages/compiler/ambient/node-fallback/constants.d.ts b/packages/compiler/ambient/node-fallback/constants.d.ts new file mode 100644 index 000000000..8ff410bfa --- /dev/null +++ b/packages/compiler/ambient/node-fallback/constants.d.ts @@ -0,0 +1,15 @@ +/* node:constants (deprecated, DEP0008) — one frozen object: os.constants' + * dlopen, errno, priority and signals groups, fs.constants, and + * crypto.constants. A platform's missing constant reads as undefined. */ +declare module "constants" { + const constants: { readonly [name: string]: number } & { + readonly defaultCoreCipherList: string; + readonly defaultCipherList: string; + }; + export = constants; +} + +declare module "node:constants" { + import constants = require("constants"); + export = constants; +} diff --git a/packages/compiler/ambient/node-fallback/crypto.d.ts b/packages/compiler/ambient/node-fallback/crypto.d.ts new file mode 100644 index 000000000..05ed6c69e --- /dev/null +++ b/packages/compiler/ambient/node-fallback/crypto.d.ts @@ -0,0 +1,837 @@ +/// +/// + +/* node:crypto: the static hashing/MAC/random/key-derivation utility slice. */ +declare module "crypto" { + import { Transform } from "stream"; + export type KeyObjectType = "secret" | "public" | "private"; + export interface JsonWebKey { + crv?: string; + d?: string; + dp?: string; + dq?: string; + e?: string; + k?: string; + kty?: string; + n?: string; + p?: string; + q?: string; + qi?: string; + x?: string; + y?: string; + [key: string]: unknown; + } + export interface JwkKeyExportOptions { + format: "jwk"; + } + export interface KeyExportOptions { + format?: "buffer" | undefined; + } + export type KeyFormat = "pem" | "der" | "jwk"; + export interface AsymmetricKeyExportOptions { + type: "pkcs1" | "spki" | "pkcs8" | "sec1"; + format: T; + cipher?: string | undefined; + passphrase?: string | Buffer | undefined; + } + export interface AsymmetricKeyDetails { + modulusLength?: number; + publicExponent?: bigint; + namedCurve?: string; + } + /* Secret keys, and RSA and EC private and public keys. */ + export class KeyObject { + private constructor(); + type: KeyObjectType; + symmetricKeySize?: number; + asymmetricKeyType?: string; + asymmetricKeyDetails?: AsymmetricKeyDetails; + export(options: AsymmetricKeyExportOptions<"pem">): string | Buffer; + export(options: AsymmetricKeyExportOptions<"der">): Buffer; + export(options?: KeyExportOptions): Buffer; + export(options: JwkKeyExportOptions): JsonWebKey; + equals(otherKeyObject: KeyObject): boolean; + } + export interface PrivateKeyInput { + key: string | Buffer; + format?: "pem" | "der" | undefined; + type?: "pkcs1" | "pkcs8" | "sec1" | undefined; + encoding?: string | undefined; + passphrase?: string | Buffer | undefined; + } + export interface PublicKeyInput { + key: string | Buffer; + format?: "pem" | "der" | undefined; + type?: "pkcs1" | "spki" | undefined; + encoding?: string | undefined; + } + export interface JsonWebKeyInput { + key: JsonWebKey; + format: "jwk"; + } + export function createPrivateKey( + key: PrivateKeyInput | string | Buffer | JsonWebKeyInput, + ): KeyObject; + export function createPublicKey( + key: PublicKeyInput | string | Buffer | KeyObject | JsonWebKeyInput, + ): KeyObject; + export interface KeyPairKeyObjectResult { + publicKey: KeyObject; + privateKey: KeyObject; + } + export interface KeyPairSyncResult { + publicKey: T1; + privateKey: T2; + } + export interface RSAKeyPairKeyObjectOptions { + modulusLength: number; + publicExponent?: number | undefined; + } + export interface ECKeyPairKeyObjectOptions { + namedCurve: string; + } + export interface RSAKeyPairOptions { + modulusLength: number; + publicExponent?: number | undefined; + publicKeyEncoding: { type: "pkcs1" | "spki"; format: PubF }; + privateKeyEncoding: { + type: "pkcs1" | "pkcs8"; + format: PrivF; + cipher?: string | undefined; + passphrase?: string | undefined; + }; + } + export interface ECKeyPairOptions { + namedCurve: string; + publicKeyEncoding: { type: "pkcs1" | "spki"; format: PubF }; + privateKeyEncoding: { + type: "sec1" | "pkcs8"; + format: PrivF; + cipher?: string | undefined; + passphrase?: string | undefined; + }; + } + export function generateKeyPairSync( + type: "rsa", + options: RSAKeyPairOptions<"pem", "pem">, + ): KeyPairSyncResult; + export function generateKeyPairSync( + type: "rsa", + options: RSAKeyPairOptions<"der", "der">, + ): KeyPairSyncResult; + export function generateKeyPairSync( + type: "rsa", + options: RSAKeyPairKeyObjectOptions, + ): KeyPairKeyObjectResult; + export function generateKeyPairSync( + type: "ec", + options: ECKeyPairOptions<"pem", "pem">, + ): KeyPairSyncResult; + export function generateKeyPairSync( + type: "ec", + options: ECKeyPairOptions<"der", "der">, + ): KeyPairSyncResult; + export function generateKeyPairSync( + type: "ec", + options: ECKeyPairKeyObjectOptions, + ): KeyPairKeyObjectResult; + export interface ED25519KeyPairOptions { + publicKeyEncoding: { type: "spki"; format: PubF }; + privateKeyEncoding: { + type: "pkcs8"; + format: PrivF; + cipher?: string | undefined; + passphrase?: string | undefined; + }; + } + export function generateKeyPairSync( + type: "ed25519", + options: ED25519KeyPairOptions<"pem", "pem">, + ): KeyPairSyncResult; + export function generateKeyPairSync( + type: "ed25519", + options: ED25519KeyPairOptions<"der", "der">, + ): KeyPairSyncResult; + export function generateKeyPairSync(type: "ed25519", options?: {}): KeyPairKeyObjectResult; + export interface X25519KeyPairOptions { + publicKeyEncoding: { type: "spki"; format: PubF }; + privateKeyEncoding: { + type: "pkcs8"; + format: PrivF; + cipher?: string | undefined; + passphrase?: string | undefined; + }; + } + export function generateKeyPairSync( + type: "x25519", + options: X25519KeyPairOptions<"pem", "pem">, + ): KeyPairSyncResult; + export function generateKeyPairSync( + type: "x25519", + options: X25519KeyPairOptions<"der", "der">, + ): KeyPairSyncResult; + export function generateKeyPairSync(type: "x25519", options?: {}): KeyPairKeyObjectResult; + export function generateKeyPair( + type: "x25519", + options: {} | undefined, + callback: (err: Error | null, publicKey: KeyObject, privateKey: KeyObject) => void, + ): void; + export function generateKeyPair( + type: "ed25519", + options: {} | undefined, + callback: (err: Error | null, publicKey: KeyObject, privateKey: KeyObject) => void, + ): void; + export function generateKeyPair( + type: "rsa", + options: RSAKeyPairKeyObjectOptions, + callback: (err: Error | null, publicKey: KeyObject, privateKey: KeyObject) => void, + ): void; + export function generateKeyPair( + type: "ec", + options: ECKeyPairKeyObjectOptions, + callback: (err: Error | null, publicKey: KeyObject, privateKey: KeyObject) => void, + ): void; + export interface SigningOptions { + padding?: number | undefined; + saltLength?: number | undefined; + dsaEncoding?: "der" | "ieee-p1363" | undefined; + } + export interface SignPrivateKeyInput extends PrivateKeyInput, SigningOptions {} + export interface SignKeyObjectInput extends SigningOptions { + key: KeyObject; + } + export interface VerifyPublicKeyInput extends PublicKeyInput, SigningOptions {} + export interface VerifyKeyObjectInput extends SigningOptions { + key: KeyObject; + } + export function diffieHellman(options: { privateKey: KeyObject; publicKey: KeyObject }): Buffer; + export function diffieHellman( + options: { privateKey: KeyObject; publicKey: KeyObject }, + callback: (err: Error | null, secret: Buffer) => void, + ): void; + export function sign( + algorithm: string | null | undefined, + data: ArrayBufferView, + key: KeyObject | string | Buffer | SignKeyObjectInput | SignPrivateKeyInput, + ): Buffer; + export function verify( + algorithm: string | null | undefined, + data: ArrayBufferView, + key: KeyObject | string | Buffer | VerifyKeyObjectInput | VerifyPublicKeyInput, + signature: ArrayBufferView, + ): boolean; + /* Sign and Verify accumulate update() data; they are not usable as + * writable streams. */ + export class Sign { + private constructor(); + update(data: string | ArrayBufferView): this; + update(data: string, inputEncoding: Encoding): this; + sign( + privateKey: KeyObject | string | Buffer | SignKeyObjectInput | SignPrivateKeyInput, + ): Buffer; + sign( + privateKey: KeyObject | string | Buffer | SignKeyObjectInput | SignPrivateKeyInput, + outputFormat: BinaryToTextEncoding, + ): string; + } + export class Verify { + private constructor(); + update(data: string | ArrayBufferView): this; + update(data: string, inputEncoding: Encoding): this; + verify( + object: KeyObject | string | Buffer | VerifyKeyObjectInput | VerifyPublicKeyInput, + signature: ArrayBufferView, + ): boolean; + verify( + object: KeyObject | string | Buffer | VerifyKeyObjectInput | VerifyPublicKeyInput, + signature: string, + signatureEncoding?: BinaryToTextEncoding, + ): boolean; + } + export function createSign(algorithm: string): Sign; + export function createVerify(algorithm: string): Verify; + export function createSecretKey(key: ArrayBufferView | ArrayBufferLike): KeyObject; + export function createSecretKey(key: string, encoding: BufferEncoding): KeyObject; + export function generateKeySync(type: "hmac" | "aes", options: { length: number }): KeyObject; + export function generateKey( + type: "hmac" | "aes", + options: { length: number }, + callback: (err: Error | null, key: KeyObject) => void, + ): void; + export interface RandomUUIDOptions { + disableEntropyCache?: boolean | undefined; + } + export function randomUUID(options?: RandomUUIDOptions): string; + export function randomUUIDv7(options?: RandomUUIDOptions): string; + export function randomBytes(size: number): Buffer; + export type BinaryToTextEncoding = "base64" | "base64url" | "hex" | "binary"; + export type CharacterEncoding = "utf8" | "utf-8" | "utf16le" | "utf-16le" | "latin1"; + export type LegacyCharacterEncoding = "ascii" | "binary" | "ucs2" | "ucs-2"; + export type Encoding = BinaryToTextEncoding | CharacterEncoding | LegacyCharacterEncoding; + export interface HashOptions { + outputLength?: number | undefined; + } + /* Hash and Hmac are Transforms: written chunks update the digest, which + * the readable side delivers once the writable side ends. */ + export interface Hash extends Transform { + copy(options?: HashOptions): Hash; + update(data: string | ArrayBufferView): Hash; + update(data: string, inputEncoding: Encoding): Hash; + digest(): Buffer; + digest(encoding: BinaryToTextEncoding): string; + } + export interface Hmac extends Transform { + update(data: string | ArrayBufferView): Hmac; + update(data: string, inputEncoding: Encoding): Hmac; + digest(): Buffer; + digest(encoding: BinaryToTextEncoding): string; + } + export function createHash(algorithm: string, options?: HashOptions): Hash; + export function createHmac( + algorithm: string, + key: BinaryLike | KeyObject, + options?: { encoding?: BufferEncoding | undefined }, + ): Hmac; + export interface OneShotDigestOptions { + outputEncoding?: BinaryToTextEncoding | "buffer" | undefined; + outputLength?: number | undefined; + } + export interface OneShotDigestOptionsWithStringEncoding extends OneShotDigestOptions { + outputEncoding?: BinaryToTextEncoding | undefined; + } + export interface OneShotDigestOptionsWithBufferEncoding extends OneShotDigestOptions { + outputEncoding: "buffer"; + } + export function hash( + algorithm: string, + data: string | ArrayBufferView, + options?: OneShotDigestOptionsWithStringEncoding | BinaryToTextEncoding, + ): string; + export function hash( + algorithm: string, + data: string | ArrayBufferView, + options: OneShotDigestOptionsWithBufferEncoding | "buffer", + ): Buffer; + export function hash( + algorithm: string, + data: string | ArrayBufferView, + options: OneShotDigestOptions | BinaryToTextEncoding | "buffer", + ): string | Buffer; + export function timingSafeEqual( + a: ArrayBufferView | ArrayBufferLike, + b: ArrayBufferView | ArrayBufferLike, + ): boolean; + /* WebCrypto's getRandomValues (also the webcrypto object's): integer + * typed arrays, filled in place and returned. */ + type RandomValuesArray = + | Int8Array + | Uint8Array + | Uint8ClampedArray + | Int16Array + | Uint16Array + | Int32Array + | Uint32Array + | BigInt64Array + | BigUint64Array; + export function getRandomValues(typedArray: T): T; + /* The WebCrypto global's object and its SubtleCrypto. */ + export const webcrypto: Crypto; + export const subtle: SubtleCrypto; + export function randomFillSync( + buffer: T, + offset?: number, + size?: number, + ): T; + export function randomFill( + buffer: T, + callback: (err: Error | null, buf: T) => void, + ): void; + export function randomFill( + buffer: T, + offset: number, + callback: (err: Error | null, buf: T) => void, + ): void; + export function randomFill( + buffer: T, + offset: number, + size: number, + callback: (err: Error | null, buf: T) => void, + ): void; + export function randomInt(max: number): number; + export function randomInt(min: number, max: number): number; + export function randomInt( + max: number, + callback: (err: Error | null, value: number) => void, + ): void; + export function randomInt( + min: number, + max: number, + callback: (err: Error | null, value: number) => void, + ): void; + export function pbkdf2Sync( + password: string | Uint8Array, + salt: string | Uint8Array, + iterations: number, + keylen: number, + digest: string, + ): Buffer; + export function pbkdf2( + password: string | Uint8Array, + salt: string | Uint8Array, + iterations: number, + keylen: number, + digest: string, + callback: (error: Error | null, derivedKey: Buffer) => void, + ): void; + export function randomBytes( + size: number, + callback: (error: Error | null, buffer: Buffer) => void, + ): void; + export type BinaryLike = string | ArrayBuffer | ArrayBufferView; + /* Cipheriv/Decipheriv: aes-128/192/256 in cbc, ctr, gcm, ccm, ecb, cfb, + * cfb8, cfb1, and ofb modes and their key wraps, des-ede3 and des-ede, + * chacha20, and chacha20-poly1305. Both are Transforms: written chunks go + * through update(), and the end pushes final(). */ + export type CipherCCMTypes = "aes-128-ccm" | "aes-192-ccm" | "aes-256-ccm"; + export type CipherGCMTypes = "aes-128-gcm" | "aes-192-gcm" | "aes-256-gcm"; + export type CipherChaCha20Poly1305Types = "chacha20-poly1305"; + export type CipherKey = BinaryLike | KeyObject; + export interface CipherCCMOptions { + authTagLength: number; + } + export interface CipherGCMOptions { + authTagLength?: number; + } + export interface CipherChaCha20Poly1305Options { + authTagLength?: number; + } + export interface Cipheriv extends Transform { + update(data: string | ArrayBufferView): Buffer; + update(data: string, inputEncoding: BufferEncoding): Buffer; + update(data: ArrayBufferView, inputEncoding: undefined, outputEncoding: BufferEncoding): string; + update( + data: string, + inputEncoding: BufferEncoding | undefined, + outputEncoding: BufferEncoding, + ): string; + final(): Buffer; + final(outputEncoding: BufferEncoding): string; + setAutoPadding(autoPadding?: boolean): this; + } + export interface CipherCCM extends Cipheriv { + setAAD(buffer: ArrayBufferView, options: { plaintextLength: number }): this; + getAuthTag(): Buffer; + } + export interface CipherGCM extends Cipheriv { + setAAD(buffer: ArrayBufferView, options?: { plaintextLength: number }): this; + getAuthTag(): Buffer; + } + export interface CipherChaCha20Poly1305 extends Cipheriv { + setAAD(buffer: ArrayBufferView, options?: { plaintextLength: number }): this; + getAuthTag(): Buffer; + } + export interface Decipheriv extends Transform { + update(data: ArrayBufferView): Buffer; + update(data: string, inputEncoding: BufferEncoding): Buffer; + update(data: ArrayBufferView, inputEncoding: undefined, outputEncoding: BufferEncoding): string; + update( + data: string, + inputEncoding: BufferEncoding | undefined, + outputEncoding: BufferEncoding, + ): string; + final(): Buffer; + final(outputEncoding: BufferEncoding): string; + setAutoPadding(autoPadding?: boolean): this; + } + export interface DecipherCCM extends Decipheriv { + setAuthTag(buffer: ArrayBufferView): this; + setAAD(buffer: ArrayBufferView, options: { plaintextLength: number }): this; + } + export interface DecipherGCM extends Decipheriv { + setAuthTag(buffer: ArrayBufferView): this; + setAAD(buffer: ArrayBufferView, options?: { plaintextLength: number }): this; + } + export interface DecipherChaCha20Poly1305 extends Decipheriv { + setAuthTag(buffer: ArrayBufferView): this; + setAAD(buffer: ArrayBufferView, options?: { plaintextLength: number }): this; + } + export function createCipheriv( + algorithm: CipherCCMTypes, + key: CipherKey, + iv: BinaryLike, + options: CipherCCMOptions, + ): CipherCCM; + export function createCipheriv( + algorithm: CipherGCMTypes, + key: CipherKey, + iv: BinaryLike, + options?: CipherGCMOptions, + ): CipherGCM; + export function createCipheriv( + algorithm: CipherChaCha20Poly1305Types, + key: CipherKey, + iv: BinaryLike, + options?: CipherChaCha20Poly1305Options, + ): CipherChaCha20Poly1305; + export function createCipheriv( + algorithm: string, + key: CipherKey, + iv: BinaryLike | null, + options?: { authTagLength?: number }, + ): Cipheriv; + export function createDecipheriv( + algorithm: CipherCCMTypes, + key: CipherKey, + iv: BinaryLike, + options: CipherCCMOptions, + ): DecipherCCM; + export function createDecipheriv( + algorithm: CipherGCMTypes, + key: CipherKey, + iv: BinaryLike, + options?: CipherGCMOptions, + ): DecipherGCM; + export function createDecipheriv( + algorithm: CipherChaCha20Poly1305Types, + key: CipherKey, + iv: BinaryLike, + options?: CipherChaCha20Poly1305Options, + ): DecipherChaCha20Poly1305; + export function createDecipheriv( + algorithm: string, + key: CipherKey, + iv: BinaryLike | null, + options?: { authTagLength?: number }, + ): Decipheriv; + export type ECDHKeyFormat = "compressed" | "uncompressed" | "hybrid"; + export class ECDH { + private constructor(); + static convertKey( + key: BinaryLike, + curve: string, + inputEncoding?: BinaryToTextEncoding, + outputEncoding?: "latin1" | "hex" | "base64" | "base64url", + format?: ECDHKeyFormat, + ): Buffer | string; + generateKeys(): Buffer; + generateKeys(encoding: BinaryToTextEncoding, format?: ECDHKeyFormat): string; + computeSecret(otherPublicKey: ArrayBufferView): Buffer; + computeSecret(otherPublicKey: string, inputEncoding: BinaryToTextEncoding): Buffer; + computeSecret(otherPublicKey: ArrayBufferView, outputEncoding: BinaryToTextEncoding): string; + computeSecret( + otherPublicKey: string, + inputEncoding: BinaryToTextEncoding, + outputEncoding: BinaryToTextEncoding, + ): string; + getPrivateKey(): Buffer; + getPrivateKey(encoding: BinaryToTextEncoding): string; + getPublicKey(encoding?: null, format?: ECDHKeyFormat): Buffer; + getPublicKey(encoding: BinaryToTextEncoding, format?: ECDHKeyFormat): string; + setPrivateKey(privateKey: ArrayBufferView): void; + setPrivateKey(privateKey: string, encoding: BinaryToTextEncoding): void; + } + export function createECDH(curveName: string): ECDH; + export class DiffieHellman { + private constructor(); + generateKeys(): Buffer; + generateKeys(encoding: BinaryToTextEncoding): string; + computeSecret( + otherPublicKey: ArrayBufferView, + inputEncoding?: null, + outputEncoding?: null, + ): Buffer; + computeSecret( + otherPublicKey: string, + inputEncoding: BinaryToTextEncoding, + outputEncoding?: null, + ): Buffer; + computeSecret( + otherPublicKey: ArrayBufferView, + inputEncoding: null, + outputEncoding: BinaryToTextEncoding, + ): string; + computeSecret( + otherPublicKey: string, + inputEncoding: BinaryToTextEncoding, + outputEncoding: BinaryToTextEncoding, + ): string; + getPrime(): Buffer; + getPrime(encoding: BinaryToTextEncoding): string; + getGenerator(): Buffer; + getGenerator(encoding: BinaryToTextEncoding): string; + getPublicKey(): Buffer; + getPublicKey(encoding: BinaryToTextEncoding): string; + getPrivateKey(): Buffer; + getPrivateKey(encoding: BinaryToTextEncoding): string; + setPublicKey(publicKey: ArrayBufferView): void; + setPublicKey(publicKey: string, encoding: BufferEncoding): void; + setPrivateKey(privateKey: ArrayBufferView): void; + setPrivateKey(privateKey: string, encoding: BufferEncoding): void; + verifyError: number; + } + export type DiffieHellmanGroup = Omit; + export function createDiffieHellman(primeLength: number, generator?: number): DiffieHellman; + export function createDiffieHellman( + prime: ArrayBuffer | ArrayBufferView, + generator?: number | ArrayBuffer | ArrayBufferView, + ): DiffieHellman; + export function createDiffieHellman( + prime: ArrayBuffer | ArrayBufferView, + generator: string, + generatorEncoding: BinaryToTextEncoding, + ): DiffieHellman; + export function createDiffieHellman( + prime: string, + primeEncoding: BinaryToTextEncoding, + generator?: number | ArrayBuffer | ArrayBufferView, + ): DiffieHellman; + export function createDiffieHellman( + prime: string, + primeEncoding: BinaryToTextEncoding, + generator: string, + generatorEncoding: BinaryToTextEncoding, + ): DiffieHellman; + export function getDiffieHellman(groupName: string): DiffieHellmanGroup; + export function createDiffieHellmanGroup(name: string): DiffieHellmanGroup; + export type LargeNumberLike = ArrayBufferView | SharedArrayBuffer | ArrayBuffer | bigint; + export interface GeneratePrimeOptions { + add?: LargeNumberLike | undefined; + rem?: LargeNumberLike | undefined; + safe?: boolean | undefined; + bigint?: boolean | undefined; + } + export interface GeneratePrimeOptionsBigInt extends GeneratePrimeOptions { + bigint: true; + } + export interface GeneratePrimeOptionsArrayBuffer extends GeneratePrimeOptions { + bigint?: false | undefined; + } + export function generatePrime( + size: number, + callback: (err: Error | null, prime: ArrayBuffer) => void, + ): void; + export function generatePrime( + size: number, + options: GeneratePrimeOptionsBigInt, + callback: (err: Error | null, prime: bigint) => void, + ): void; + export function generatePrime( + size: number, + options: GeneratePrimeOptionsArrayBuffer, + callback: (err: Error | null, prime: ArrayBuffer) => void, + ): void; + export function generatePrime( + size: number, + options: GeneratePrimeOptions, + callback: (err: Error | null, prime: ArrayBuffer | bigint) => void, + ): void; + export function generatePrimeSync(size: number): ArrayBuffer; + export function generatePrimeSync(size: number, options: GeneratePrimeOptionsBigInt): bigint; + export function generatePrimeSync( + size: number, + options: GeneratePrimeOptionsArrayBuffer, + ): ArrayBuffer; + export function generatePrimeSync( + size: number, + options: GeneratePrimeOptions, + ): ArrayBuffer | bigint; + export interface CheckPrimeOptions { + checks?: number | undefined; + } + export function checkPrime( + value: LargeNumberLike, + callback: (err: Error | null, result: boolean) => void, + ): void; + export function checkPrime( + value: LargeNumberLike, + options: CheckPrimeOptions, + callback: (err: Error | null, result: boolean) => void, + ): void; + export function checkPrimeSync(candidate: LargeNumberLike, options?: CheckPrimeOptions): boolean; + export interface ScryptOptions { + N?: number; + r?: number; + p?: number; + cost?: number; + blockSize?: number; + parallelization?: number; + maxmem?: number; + } + export function scryptSync( + password: BinaryLike, + salt: BinaryLike, + keylen: number, + options?: ScryptOptions, + ): Buffer; + export function scrypt( + password: BinaryLike, + salt: BinaryLike, + keylen: number, + callback: (error: Error | null, derivedKey: Buffer) => void, + ): void; + export function scrypt( + password: BinaryLike, + salt: BinaryLike, + keylen: number, + options: ScryptOptions, + callback: (error: Error | null, derivedKey: Buffer) => void, + ): void; + export function hkdfSync( + digest: string, + ikm: BinaryLike | KeyObject, + salt: BinaryLike, + info: BinaryLike, + keylen: number, + ): ArrayBuffer; + export function hkdf( + digest: string, + ikm: BinaryLike | KeyObject, + salt: BinaryLike, + info: BinaryLike, + keylen: number, + callback: (error: Error | null, derivedKey: ArrayBuffer) => void, + ): void; + /* The lowered X509Certificate surface is the data-record slice: the + * fields below, all computed at construction (raw decodes the input, + * and the others read raw). validFrom/validTo are Node's + * ASN1_TIME_print strings ("Jul 1 00:00:00 2026 GMT"). Unparseable + * input throws Node's ERR_OSSL_PEM_NO_START_LINE Error. */ + export class X509Certificate { + constructor(buffer: Uint8Array | string); + readonly ca: boolean; + readonly fingerprint: string; + readonly fingerprint256: string; + readonly fingerprint512: string; + readonly subject: string; + readonly subjectAltName: string | undefined; + readonly infoAccess: string | undefined; + readonly issuer: string; + readonly raw: Buffer; + readonly serialNumber: string; + readonly signatureAlgorithm: string | undefined; + readonly signatureAlgorithmOid: string; + readonly validFrom: string; + readonly validFromDate: Date; + readonly validTo: string; + readonly validToDate: Date; + /* the certificate's public key; every read answers the same KeyObject */ + readonly publicKey: KeyObject; + verify(publicKey: KeyObject): boolean; + checkPrivateKey(privateKey: KeyObject): boolean; + checkIssued(otherCert: X509Certificate): boolean; + checkHost(name: string, options?: X509CheckOptions): string | undefined; + checkEmail(email: string, options?: X509CheckOptions): string | undefined; + checkIP(ip: string): string | undefined; + toLegacyObject(): import("tls").PeerCertificate; + } + export interface X509CheckOptions { + subject?: "always" | "default" | "never"; + wildcards?: boolean; + partialWildcards?: boolean; + multiLabelWildcards?: boolean; + singleLabelSubdomains?: boolean; + } + /* The crypto introspection statics: getFips() answers 0 (a compiled + * binary is never a FIPS build), the three name lists bake as fresh + * string[] literals of Node v24 answers, and constants (below) bakes + * per member like http2.constants. */ + export function getFips(): 1 | 0; + export function getCiphers(): string[]; + export type CipherMode = + | "cbc" + | "ccm" + | "cfb" + | "ctr" + | "ecb" + | "gcm" + | "ocb" + | "ofb" + | "stream" + | "wrap" + | "xts"; + export interface CipherInfoOptions { + keyLength?: number | undefined; + ivLength?: number | undefined; + } + export interface CipherInfo { + name: string; + nid: number; + blockSize?: number | undefined; + ivLength?: number | undefined; + keyLength: number; + mode: CipherMode; + } + export function getCipherInfo( + nameOrNid: string | number, + options?: CipherInfoOptions, + ): CipherInfo | undefined; + export function getHashes(): string[]; + export function getCurves(): string[]; + /* Node v24 crypto.constants, literal-typed (the Http2Constants + * stance). Every ACCESS bakes as its literal at lowering; the object + * itself never materializes (bare crypto.constants value uses fence). */ + export interface CryptoConstants { + readonly OPENSSL_VERSION_NUMBER: 810549376; + readonly SSL_OP_ALL: 2147485776; + readonly SSL_OP_ALLOW_NO_DHE_KEX: 1024; + readonly SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION: 262144; + readonly SSL_OP_CIPHER_SERVER_PREFERENCE: 4194304; + readonly SSL_OP_CISCO_ANYCONNECT: 32768; + readonly SSL_OP_COOKIE_EXCHANGE: 8192; + readonly SSL_OP_CRYPTOPRO_TLSEXT_BUG: 2147483648; + readonly SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS: 2048; + readonly SSL_OP_LEGACY_SERVER_CONNECT: 4; + readonly SSL_OP_NO_COMPRESSION: 131072; + readonly SSL_OP_NO_ENCRYPT_THEN_MAC: 524288; + readonly SSL_OP_NO_QUERY_MTU: 4096; + readonly SSL_OP_NO_RENEGOTIATION: 1073741824; + readonly SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION: 65536; + readonly SSL_OP_NO_SSLv2: 0; + readonly SSL_OP_NO_SSLv3: 33554432; + readonly SSL_OP_NO_TICKET: 16384; + readonly SSL_OP_NO_TLSv1: 67108864; + readonly SSL_OP_NO_TLSv1_1: 268435456; + readonly SSL_OP_NO_TLSv1_2: 134217728; + readonly SSL_OP_NO_TLSv1_3: 536870912; + readonly SSL_OP_PRIORITIZE_CHACHA: 2097152; + readonly SSL_OP_TLS_ROLLBACK_BUG: 8388608; + readonly ENGINE_METHOD_RSA: 1; + readonly ENGINE_METHOD_DSA: 2; + readonly ENGINE_METHOD_DH: 4; + readonly ENGINE_METHOD_RAND: 8; + readonly ENGINE_METHOD_EC: 2048; + readonly ENGINE_METHOD_CIPHERS: 64; + readonly ENGINE_METHOD_DIGESTS: 128; + readonly ENGINE_METHOD_PKEY_METHS: 512; + readonly ENGINE_METHOD_PKEY_ASN1_METHS: 1024; + readonly ENGINE_METHOD_ALL: 65535; + readonly ENGINE_METHOD_NONE: 0; + readonly DH_CHECK_P_NOT_SAFE_PRIME: 2; + readonly DH_CHECK_P_NOT_PRIME: 1; + readonly DH_UNABLE_TO_CHECK_GENERATOR: 4; + readonly DH_NOT_SUITABLE_GENERATOR: 8; + readonly RSA_PKCS1_PADDING: 1; + readonly RSA_SSLV23_PADDING: 2; + readonly RSA_NO_PADDING: 3; + readonly RSA_PKCS1_OAEP_PADDING: 4; + readonly RSA_X931_PADDING: 5; + readonly RSA_PKCS1_PSS_PADDING: 6; + readonly RSA_PSS_SALTLEN_DIGEST: -1; + readonly RSA_PSS_SALTLEN_MAX_SIGN: -2; + readonly RSA_PSS_SALTLEN_AUTO: -2; + readonly defaultCoreCipherList: "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:DHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA256:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!SRP:!CAMELLIA"; + readonly TLS1_VERSION: 769; + readonly TLS1_1_VERSION: 770; + readonly TLS1_2_VERSION: 771; + readonly TLS1_3_VERSION: 772; + readonly POINT_CONVERSION_COMPRESSED: 2; + readonly POINT_CONVERSION_UNCOMPRESSED: 4; + readonly POINT_CONVERSION_HYBRID: 6; + readonly defaultCipherList: "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:DHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA256:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!SRP:!CAMELLIA"; + } + export const constants: CryptoConstants; +} + +declare module "node:crypto" { + export * from "crypto"; +} diff --git a/packages/compiler/ambient/node-fallback/dgram.d.ts b/packages/compiler/ambient/node-fallback/dgram.d.ts new file mode 100644 index 000000000..27a98b26d --- /dev/null +++ b/packages/compiler/ambient/node-fallback/dgram.d.ts @@ -0,0 +1,58 @@ +/* node:dgram — UDP sockets over the event loop (scr_dgram.c, linked only + * into using binaries). createSocket takes "udp4" or the + * { type: "udp4", reuseAddr? } literal; bind/connect bind NOW and defer + * 'listening'/'connect' to the next loop turn; send takes one string or + * Buffer datagram with an explicit port + address (the connected-send + * and callback forms have no lowering); address() returns the real + * {address, family, port} record and THROWS "Not running" before bind, + * like Node. The socket options throw Node's errors (EBADF before bind); + * add/dropMembership before bind is refused at run time (SC2020). 'error' + * with no listener exits 1 (the unhandled EventEmitter 'error' behavior). */ +declare module "dgram" { + export interface AddressInfo { + address: string; + family: string; + port: number; + } + export interface RemoteInfo { + address: string; + family: string; + port: number; + size: number; + } + export interface Socket { + bind(port: number, address?: string, callback?: () => void): void; + connect(port: number, address?: string, callback?: () => void): void; + send(msg: string | Uint8Array, port: number, address: string): void; + address(): AddressInfo; + close(callback?: () => void): void; + disconnect(): void; + unref(): void; + ref(): void; + setBroadcast(flag: boolean): void; + setTTL(ttl: number): number; + setMulticastTTL(ttl: number): number; + setMulticastLoopback(flag: boolean): boolean; + setMulticastInterface(multicastInterface: string): void; + addMembership(multicastAddress: string, multicastInterface?: string): void; + dropMembership(multicastAddress: string, multicastInterface?: string): void; + getRecvBufferSize(): number; + getSendBufferSize(): number; + setRecvBufferSize(size: number): void; + setSendBufferSize(size: number): void; + getSendQueueSize(): number; + getSendQueueCount(): number; + on(event: "message", listener: (msg: Buffer, rinfo: RemoteInfo) => void): void; + on(event: "listening" | "close" | "connect", listener: () => void): void; + on(event: "error", listener: (err: Error) => void): void; + once(event: "message", listener: (msg: Buffer, rinfo: RemoteInfo) => void): void; + once(event: "listening" | "close" | "connect", listener: () => void): void; + once(event: "error", listener: (err: Error) => void): void; + } + export function createSocket(type: "udp4"): Socket; + export function createSocket(options: { type: "udp4"; reuseAddr?: boolean }): Socket; +} + +declare module "node:dgram" { + export * from "dgram"; +} diff --git a/packages/compiler/ambient/node-fallback/diagnostics_channel.d.ts b/packages/compiler/ambient/node-fallback/diagnostics_channel.d.ts new file mode 100644 index 000000000..53e195e89 --- /dev/null +++ b/packages/compiler/ambient/node-fallback/diagnostics_channel.d.ts @@ -0,0 +1,87 @@ +/// + +/* node:diagnostics_channel — the in-process pub/sub core: named channels, + * subscribe/unsubscribe (module-level and channel-level), publish with any + * dyn-convertible message. Channel values are f64 handles into the + * runtime's channel registry (the readline.Interface precedent — channels + * are process-lived in Node too, its WeakRef machinery aside). + * The store-binding surface (bindStore/unbindStore/runStores) lowers onto + * the AsyncLocalStorage integration (dc.chan* libCalls). + * + * TracingChannel (tracingChannel) is the five-event-channel collection: + * an f64 handle into the runtime's tracing registry (type-mapper.ts maps the + * name like Channel). subscribe/unsubscribe walk a handlers object; + * traceSync/traceCallback run Node's publish choreography in the runtime + * (context defaults to a fresh {}); tracePromise is declared (the member + * exists in Node) but fences at the call site — the traced function's + * promise cannot cross the checked-dynamic tree boundary yet. */ +declare module "node:diagnostics_channel" { + export type ChannelListener = (message: unknown, name: string) => void; + export interface Channel { + readonly name: string; + readonly hasSubscribers: boolean; + publish(message: unknown): void; + subscribe(onMessage: ChannelListener): void; + unsubscribe(onMessage: ChannelListener): boolean; + bindStore( + store: import("async_hooks").AsyncLocalStorage, + transform?: (context: any) => any, + ): void; + unbindStore(store: import("async_hooks").AsyncLocalStorage): boolean; + runStores(context: unknown, fn: (...args: any[]) => any, thisArg?: any, ...args: any[]): any; + } + export function channel(name: string): Channel; + export function subscribe(name: string, onMessage: ChannelListener): void; + export function unsubscribe(name: string, onMessage: ChannelListener): boolean; + export function hasSubscribers(name: string): boolean; + export interface TracingChannelSubscribers { + start?: (message: any) => void; + end?: (message: any) => void; + asyncStart?: (message: any) => void; + asyncEnd?: (message: any) => void; + error?: (message: any) => void; + } + export interface TracingChannelCollection { + start: Channel; + end: Channel; + asyncStart: Channel; + asyncEnd: Channel; + error: Channel; + } + export interface TracingChannel { + start: Channel; + end: Channel; + asyncStart: Channel; + asyncEnd: Channel; + error: Channel; + readonly hasSubscribers: boolean; + subscribe(subscribers: TracingChannelSubscribers): void; + /** True when every present handler was found and removed (Node's + * all-found conjunction; @types/node under-declares this as void). */ + unsubscribe(subscribers: TracingChannelSubscribers): boolean; + traceSync( + fn: (this: ThisArg, ...args: Args) => Result, + context?: object, + thisArg?: ThisArg, + ...args: Args + ): Result; + tracePromise( + fn: (this: ThisArg, ...args: Args) => Promise, + context?: object, + thisArg?: ThisArg, + ...args: Args + ): Promise; + traceCallback( + fn: (this: ThisArg, ...args: Args) => Result, + position?: number, + context?: object, + thisArg?: ThisArg, + ...args: Args + ): Result; + } + export function tracingChannel(nameOrChannels: string | TracingChannelCollection): TracingChannel; +} + +declare module "diagnostics_channel" { + export * from "node:diagnostics_channel"; +} diff --git a/packages/compiler/ambient/node-fallback/dns-promises.d.ts b/packages/compiler/ambient/node-fallback/dns-promises.d.ts new file mode 100644 index 000000000..29fadd6d4 --- /dev/null +++ b/packages/compiler/ambient/node-fallback/dns-promises.d.ts @@ -0,0 +1,27 @@ +/// + +/* node:dns/promises — lookup and lookupService as promises over the same + * resolution as the callback forms: argument errors throw synchronously, + * failures reject with the callbacks' errors. */ +declare module "dns/promises" { + import type { LookupAddress, LookupAllOptions, LookupOneOptions, LookupOptions } from "dns"; + export function lookup(hostname: string, family: number): Promise; + export function lookup(hostname: string, options: LookupOneOptions): Promise; + export function lookup(hostname: string, options: LookupAllOptions): Promise; + export function lookup( + hostname: string, + options: LookupOptions, + ): Promise; + export function lookup(hostname: string): Promise; + export function lookupService( + address: string, + port: number, + ): Promise<{ hostname: string; service: string }>; + export function getServers(): string[]; + export function getDefaultResultOrder(): "ipv4first" | "ipv6first" | "verbatim"; + export function setDefaultResultOrder(order: "ipv4first" | "ipv6first" | "verbatim"): void; +} + +declare module "node:dns/promises" { + export * from "dns/promises"; +} diff --git a/packages/compiler/ambient/node-fallback/dns.d.ts b/packages/compiler/ambient/node-fallback/dns.d.ts new file mode 100644 index 000000000..234f03463 --- /dev/null +++ b/packages/compiler/ambient/node-fallback/dns.d.ts @@ -0,0 +1,65 @@ +/// + +/* node:dns — the lookup slice (scr_dgram.c hosts it; getaddrinfo runs AT + * CALL TIME and the callback defers to the next loop turn — SEMANTICS.md + * documents the blocking divergence). Only the { family: 4 } options + * form lowers; failures deliver Node's error shape ("getaddrinfo + * ENOTFOUND ") as the callback's Error argument, with "" for + * the address where Node passes undefined. */ +declare module "dns" { + export interface LookupAddress { + address: string; + family: number; + } + export interface LookupOptions { + family?: number | "IPv4" | "IPv6"; + hints?: number; + all?: boolean; + order?: "ipv4first" | "ipv6first" | "verbatim"; + verbatim?: boolean; + } + export interface LookupOneOptions extends LookupOptions { + all?: false; + } + export interface LookupAllOptions extends LookupOptions { + all: true; + } + /* The callback comes with the other lookup deliveries (literal + * addresses on the next tick); the options follow lib/dns.js. */ + export function lookup( + hostname: string, + callback: (err: NodeJS.ErrnoException | null, address: string, family: number) => void, + ): void; + export function lookup( + hostname: string, + family: number, + callback: (err: NodeJS.ErrnoException | null, address: string, family: number) => void, + ): void; + export function lookup( + hostname: string, + options: LookupOneOptions, + callback: (err: NodeJS.ErrnoException | null, address: string, family: number) => void, + ): void; + export function lookup( + hostname: string, + options: LookupAllOptions, + callback: (err: NodeJS.ErrnoException | null, addresses: LookupAddress[]) => void, + ): void; + /* getnameinfo with NI_NAMEREQD: the host's name and the port's service + * name (the number when it has none). */ + export function lookupService( + address: string, + port: number, + callback: (err: NodeJS.ErrnoException | null, hostname: string, service: string) => void, + ): void; + /* The nameservers /etc/resolv.conf lists. */ + export function getServers(): string[]; + export function getDefaultResultOrder(): "ipv4first" | "ipv6first" | "verbatim"; + export function setDefaultResultOrder(order: "ipv4first" | "ipv6first" | "verbatim"): void; + /* The promise forms (node:dns/promises). */ + export const promises: typeof import("dns/promises"); +} + +declare module "node:dns" { + export * from "dns"; +} diff --git a/packages/compiler/ambient/node-fallback/events.d.ts b/packages/compiler/ambient/node-fallback/events.d.ts new file mode 100644 index 000000000..dda1cd086 --- /dev/null +++ b/packages/compiler/ambient/node-fallback/events.d.ts @@ -0,0 +1,60 @@ +/* node:events — the EventEmitter class (the module object IS the class, + * like Node's `module.exports = EventEmitter`). Listener/emit signatures + * mirror @types/node's untyped `...args: any[]` surface; the compiler's + * lowering unifies each event name's argument tuple program-wide and + * checks annotated listener parameters against it (unannotated non-empty + * parameter lists have no static types and fence at the registration). */ +declare module "events" { + class EventEmitter { + constructor(); + static defaultMaxListeners: number; + /* Listeners registered under this symbol observe 'error' emits first + * (an emit with no 'error' listener still throws). */ + static readonly errorMonitor: unique symbol; + static addAbortListener(signal: AbortSignal, listener: (event: Event) => void): Disposable; + on(eventName: string | symbol, listener: (...args: any[]) => void): this; + addListener(eventName: string | symbol, listener: (...args: any[]) => void): this; + once(eventName: string | symbol, listener: (...args: any[]) => void): this; + prependListener(eventName: string | symbol, listener: (...args: any[]) => void): this; + prependOnceListener(eventName: string | symbol, listener: (...args: any[]) => void): this; + off(eventName: string | symbol, listener: (...args: any[]) => void): this; + removeListener(eventName: string | symbol, listener: (...args: any[]) => void): this; + removeAllListeners(eventName?: string | symbol): this; + emit(eventName: string | symbol, ...args: any[]): boolean; + listenerCount(eventName: string | symbol, listener?: (...args: any[]) => void): number; + /* Declared as zero-parameter closures so the RESULT is statically + * consumable (.length, indexing, identity): the lowering only admits + * these calls for events whose argument tuple is empty — a listener + * pulled from the array is then safely callable too. Events with + * arguments fence (their element type would lie). */ + listeners(eventName: string): Array<() => void>; + rawListeners(eventName: string): Array<() => void>; + eventNames(): string[]; + setMaxListeners(n: number): this; + getMaxListeners(): number; + } + import internal = require("node:events"); + namespace EventEmitter { + // The class under its own name, so `import { EventEmitter }` resolves + // (the @types/node namespace-alias pattern). + export { internal as EventEmitter }; + // The event's arguments, as checked-dynamic values. + export function once( + emitter: EventEmitter | NodeJS.ReadableStream | NodeJS.WritableStream, + eventName: string, + options?: { signal?: AbortSignal }, + ): Promise; + /* The emitter's own listenerCount/getMaxListeners/setMaxListeners/ + * listeners, over EventEmitter instances. */ + export function listenerCount(emitter: EventEmitter, eventName: string | symbol): number; + export function getMaxListeners(emitter: EventEmitter): number; + export function setMaxListeners(n?: number, ...eventTargets: EventEmitter[]): void; + export function getEventListeners(emitter: EventEmitter, name: string): Array<() => void>; + } + export = EventEmitter; +} + +declare module "node:events" { + import events = require("events"); + export = events; +} diff --git a/packages/compiler/ambient/node-fallback/fs-promises.d.ts b/packages/compiler/ambient/node-fallback/fs-promises.d.ts new file mode 100644 index 000000000..6f3f2c460 --- /dev/null +++ b/packages/compiler/ambient/node-fallback/fs-promises.d.ts @@ -0,0 +1,225 @@ +/// + +/* fs/promises — the SAME synchronous operations behind already-settled + * promises: success fulfills, failure REJECTS with the would-be thrown + * error (catchable at the await, like Node). DOCUMENTED DIVERGENCE + * (SEMANTICS.md): the syscall blocks the event loop, so I/O never + * interleaves with timers or other fibers — observable only in concurrent + * code. readFile is utf8-only, like readFileSync. */ +declare module "fs/promises" { + export interface FileReadResult { + bytesRead: number; + buffer: T; + } + export interface FileWriteResult { + bytesWritten: number; + buffer: T; + } + export interface FileReadvResult { + bytesRead: number; + buffers: T; + } + export interface FileWritevResult { + bytesWritten: number; + buffers: T; + } + export interface FileReadOptions { + buffer?: T; + offset?: number | null; + length?: number | null; + position?: number | null; + } + /* The streams over a FileHandle (createReadStream, createWriteStream, + * readLines): fs streams that hold the handle until they close; closing + * the stream closes the handle unless autoClose is false. */ + export interface CreateReadStreamOptions { + encoding?: BufferEncoding | null; + autoClose?: boolean; + emitClose?: boolean; + start?: number; + end?: number; + highWaterMark?: number; + } + export interface CreateWriteStreamOptions { + encoding?: BufferEncoding | null; + autoClose?: boolean; + emitClose?: boolean; + start?: number; + highWaterMark?: number; + flush?: boolean; + } + export interface FileHandle extends AsyncDisposable { + readonly fd: number; + close(): Promise; + createReadStream(options?: CreateReadStreamOptions): import("node:fs").ReadStream; + createWriteStream(options?: CreateWriteStreamOptions): import("node:fs").WriteStream; + readLines(options?: CreateReadStreamOptions): import("node:readline").Interface; + sync(): Promise; + datasync(): Promise; + truncate(len?: number): Promise; + chmod(mode: number): Promise; + utimes(atime: number | string | Date, mtime: number | string | Date): Promise; + readv( + buffers: T, + position?: number | null, + ): Promise>; + writev( + buffers: T, + position?: number | null, + ): Promise>; + read( + options?: FileReadOptions | null, + ): Promise>; + read( + buffer: T, + options: Omit, "buffer">, + ): Promise>; + read( + buffer: T, + offset?: number | null, + length?: number | null, + position?: number | null, + ): Promise>; + write( + buffer: T, + offset?: number | null, + length?: number | null, + position?: number | null, + ): Promise>; + write( + buffer: T, + options: Omit, "buffer">, + ): Promise>; + write( + data: string, + position?: number | null, + encoding?: "utf8" | "utf-8" | null, + ): Promise>; + readFile(options?: null): Promise; + readFile(encoding: "utf8" | "utf-8"): Promise; + writeFile(data: string | Uint8Array, encoding?: "utf8" | "utf-8" | null): Promise; + appendFile(data: string | Uint8Array, encoding?: "utf8" | "utf-8" | null): Promise; + stat(): Promise; + [Symbol.asyncDispose](): Promise; + } + export function open(path: string | URL, flags?: string, mode?: number): Promise; + export function link( + existingPath: string | Uint8Array | URL, + newPath: string | Uint8Array | URL, + ): Promise; + export function symlink( + target: string | Uint8Array | URL, + path: string | Uint8Array | URL, + type?: string | null, + ): Promise; + export function cp( + source: string | URL, + destination: string | URL, + options?: import("node:fs").CopyOptions, + ): Promise; + export function readlink( + path: string | Uint8Array | URL, + options: "buffer" | { encoding: "buffer" }, + ): Promise; + export function readlink( + path: string | Uint8Array | URL, + options?: BufferEncoding | { encoding?: BufferEncoding | null } | null, + ): Promise; + export function readlink( + path: string | Uint8Array | URL, + options: string | { encoding?: string | null } | null, + ): Promise; + export function utimes( + path: string | Uint8Array | URL, + atime: number | string | Date, + mtime: number | string | Date, + ): Promise; + export function lutimes( + path: string | Uint8Array | URL, + atime: number | string | Date, + mtime: number | string | Date, + ): Promise; + export function readFile(path: string | URL, encoding: "utf8" | "utf-8"): Promise; + export function readFile( + path: string | URL, + options: BufferEncoding | { encoding: BufferEncoding }, + ): Promise; + export function readFile(path: string | URL): Promise; + export function writeFile( + path: string | URL, + data: string, + options?: { + mode?: number; + encoding?: "utf8" | "utf-8"; + flag?: string; + [option: string]: unknown; + }, + ): Promise; + export function writeFile( + path: string | URL, + data: string, + encoding: "utf8" | "utf-8", + ): Promise; + export function mkdir( + path: string | URL, + options?: { recursive?: boolean; mode?: number }, + ): Promise; + export function readdir(path: string | URL): Promise; + export function readdir( + path: string | URL, + options: { + encoding?: "utf8" | "utf-8"; + withFileTypes: true; + recursive?: boolean; + [option: string]: unknown; + }, + ): Promise; + /* The async iterator form of fs.globSync: iterate it with for await. */ + export function glob( + pattern: string | readonly string[], + options?: import("node:fs").GlobOptions, + ): AsyncIterableIterator; + export function glob( + pattern: string | readonly string[], + options: import("node:fs").GlobOptionsWithFileTypes, + ): AsyncIterableIterator; + export function rm(path: string | URL): Promise; + export function rm( + path: string | URL, + options: { recursive?: boolean; force?: boolean; maxRetries?: number; retryDelay?: number }, + ): Promise; + export function stat(path: string | URL): Promise; + export function statfs( + path: string | Uint8Array | URL, + options?: import("node:fs").StatFsOptions & { bigint?: false }, + ): Promise; + export function statfs( + path: string | Uint8Array | URL, + options: import("node:fs").StatFsOptions & { bigint: true }, + ): Promise; + export function statfs( + path: string | Uint8Array | URL, + options?: import("node:fs").StatFsOptions, + ): Promise; + export function realpath(path: string | URL): Promise; + export function lstat(path: string | URL): Promise; + export function unlink(path: string | URL): Promise; + export function chmod(path: string | URL, mode: number): Promise; + export function chown(path: string | URL, uid: number, gid: number): Promise; + export function lchown(path: string | URL, uid: number, gid: number): Promise; + export function access(path: string | URL, mode?: number): Promise; + export function truncate(path: string | URL, len?: number): Promise; + export function appendFile(path: string | URL, data: string | Uint8Array): Promise; + export function copyFile(src: string | URL, dest: string | URL): Promise; + export function mkdtemp(prefix: string | URL): Promise; + export function rmdir(path: string | URL): Promise; + export function rename(oldPath: string | URL, newPath: string | URL): Promise; + export function opendir( + path: string | URL, + options?: import("node:fs").OpenDirOptions, + ): Promise; +} + +declare module "node:fs/promises" { + export * from "fs/promises"; +} diff --git a/packages/compiler/ambient/node-fallback/fs.d.ts b/packages/compiler/ambient/node-fallback/fs.d.ts new file mode 100644 index 000000000..6399558bd --- /dev/null +++ b/packages/compiler/ambient/node-fallback/fs.d.ts @@ -0,0 +1,587 @@ +/// +/// + +/* The synchronous node:fs surface — utf8-only, no options objects, no + * Buffers. Importing "node:fs" resolves here (preflight allowlists exactly + * the ambient-declared node: modules); every function lowers to a `libCall` + * with a scr_fs_* runtime implementation. Failures throw CATCHABLE string + * values formatted like Node's error messages ("ENOENT: no such file or + * directory, open 'x'"). `rmSync` removes FILES only (like Node without + * `recursive`); empty directories go through `rmdirSync`. */ +declare module "node:fs" { + import { Readable, Writable } from "stream"; + export function readFileSync(path: string | number | URL, encoding: BufferEncoding): string; + export function readFileSync( + path: string | number | URL, + options: { encoding: BufferEncoding }, + ): string; + export function readFileSync(path: string | number | URL): Buffer; + export function readFileSync(path: string | URL, options: unknown): string | Buffer; + /* Error-first callbacks select string or Buffer data from their encoding. + * Unknown options retain that union until the caller validates the value. */ + export function readFile( + path: string, + options: unknown, + callback: (error: Error | null, data: string | Buffer) => void, + ): void; + /* The options form carries the mode (applied at CREATION only, like + * Node — an existing file keeps its permissions) and/or the utf8 + * encoding spelling. */ + export function writeFileSync( + path: string | URL, + data: string, + /* The options-record stance (see http.RequestOptions): flag fences + * by name at the call site; undocumented keys drop like Node. */ + options?: { + mode?: number; + encoding?: "utf8" | "utf-8"; + flag?: string; + [option: string]: unknown; + }, + ): void; + /* The bare-encoding spelling — the options record's encoding key alone. */ + export function writeFileSync(path: string | URL, data: string, encoding: "utf8" | "utf-8"): void; + export function writeFileSync(path: string | URL, data: string | Uint8Array): void; + export function appendFileSync(path: string | URL, data: string | Uint8Array): void; + export function existsSync(path: string | URL): boolean; + export function mkdirSync(path: string | URL): void; + export function mkdirSync( + path: string | URL, + options: { recursive?: boolean; mode?: number }, + ): void; + export function unlinkSync(path: string | URL): void; + export function chmodSync(path: string | URL, mode: number): void; + export function fchmodSync(fd: number, mode: number): void; + export function fsyncSync(fd: number): void; + export function linkSync( + existingPath: string | Uint8Array | URL, + newPath: string | Uint8Array | URL, + ): void; + export function symlinkSync( + target: string | Uint8Array | URL, + path: string | Uint8Array | URL, + type?: string | null, + ): void; + /* fs.cpSync / fs.cp: copies a file, symlink or (with recursive) a + * directory tree. filter decides per path; the callback and promise + * forms complete the copy before reporting. */ + export interface CopySyncOptions { + dereference?: boolean; + errorOnExist?: boolean; + filter?: (source: string, destination: string) => boolean; + force?: boolean; + mode?: number; + preserveTimestamps?: boolean; + recursive?: boolean; + verbatimSymlinks?: boolean; + } + export interface CopyOptions { + dereference?: boolean; + errorOnExist?: boolean; + filter?: (source: string, destination: string) => boolean | Promise; + force?: boolean; + mode?: number; + preserveTimestamps?: boolean; + recursive?: boolean; + verbatimSymlinks?: boolean; + } + export function cpSync( + source: string | URL, + destination: string | URL, + options?: CopySyncOptions, + ): void; + export function cp( + source: string | URL, + destination: string | URL, + callback: (err: NodeJS.ErrnoException | null) => void, + ): void; + export function cp( + source: string | URL, + destination: string | URL, + options: CopyOptions, + callback: (err: NodeJS.ErrnoException | null) => void, + ): void; + export function symlink( + target: string | Uint8Array | URL, + path: string | Uint8Array | URL, + callback: (err: NodeJS.ErrnoException | null) => void, + ): void; + export function symlink( + target: string | Uint8Array | URL, + path: string | Uint8Array | URL, + type: string | null | undefined, + callback: (err: NodeJS.ErrnoException | null) => void, + ): void; + export function link( + existingPath: string | Uint8Array | URL, + newPath: string | Uint8Array | URL, + callback: (err: NodeJS.ErrnoException | null) => void, + ): void; + export function readlink( + path: string | Uint8Array | URL, + callback: (err: NodeJS.ErrnoException | null, target: string) => void, + ): void; + export function readlink( + path: string | Uint8Array | URL, + options: "buffer" | { encoding: "buffer" }, + callback: (err: NodeJS.ErrnoException | null, target: Buffer) => void, + ): void; + export function readlink( + path: string | Uint8Array | URL, + options: BufferEncoding | { encoding?: BufferEncoding | null } | null, + callback: (err: NodeJS.ErrnoException | null, target: string) => void, + ): void; + export function readlink( + path: string | Uint8Array | URL, + options: string | { encoding?: string | null } | null, + callback: (err: NodeJS.ErrnoException | null, target: string | Buffer) => void, + ): void; + export function readlinkSync( + path: string | Uint8Array | URL, + options: "buffer" | { encoding: "buffer" }, + ): Buffer; + export function readlinkSync( + path: string | Uint8Array | URL, + options?: BufferEncoding | { encoding?: BufferEncoding | null } | null, + ): string; + export function readlinkSync( + path: string | Uint8Array | URL, + options: string | { encoding?: string | null } | null, + ): string | Buffer; + export function chownSync(path: string | URL, uid: number, gid: number): void; + export function lchownSync(path: string | URL, uid: number, gid: number): void; + export function fchownSync(fd: number, uid: number, gid: number): void; + export function truncateSync(path: string | URL, len?: number): void; + /* The 2-argument form only (Node's mode flags have no lowering). The + * destination is created or truncated carrying the SOURCE's mode. */ + export function copyFileSync(src: string | URL, dest: string | URL): void; + export function rename( + oldPath: string, + newPath: string, + callback: (err: NodeJS.ErrnoException | null) => void, + ): void; + export function renameSync(oldPath: string | URL, newPath: string | URL): void; + export function rmSync(path: string | URL): void; + /* maxRetries/retryDelay are accepted no-ops (Node retries around + * EBUSY-class races; the synchronous lowering has no re-entrancy that + * needs them). */ + export function rmSync( + path: string | URL, + options: { recursive?: boolean; force?: boolean; maxRetries?: number; retryDelay?: number }, + ): 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 + * carries the entry kind; isFile/isDirectory/isSymbolicLink lower as + * reads of it in builtins/process.ts). */ + export interface Dirent { + name: string; + parentPath: string; + isFile(): boolean; + isDirectory(): boolean; + isSymbolicLink(): boolean; + } + export function readdirSync( + path: string | URL, + /* The options-record stance (see http.RequestOptions): recursive + * fences by name at the call site; undocumented keys drop like Node. */ + options: { withFileTypes: true; recursive?: boolean; [option: string]: unknown }, + ): Dirent[]; + /* fs.globSync / fs.glob: Node's glob walk over the file system. Patterns + * are minimatch globs (case-insensitive on macOS and Windows); results + * are relative to cwd unless the pattern is absolute. withFileTypes must + * be a literal so the result type is known. */ + export interface GlobOptions { + cwd?: string | URL; + exclude?: readonly string[] | ((fileName: string) => boolean); + followSymlinks?: boolean; + withFileTypes?: false; + } + export interface GlobOptionsWithFileTypes { + cwd?: string | URL; + exclude?: readonly string[]; + followSymlinks?: boolean; + withFileTypes: true; + } + export function globSync(pattern: string | readonly string[], options?: GlobOptions): string[]; + export function globSync( + pattern: string | readonly string[], + options: GlobOptionsWithFileTypes, + ): Dirent[]; + export function glob( + pattern: string | readonly string[], + callback: (err: NodeJS.ErrnoException | null, matches?: string[]) => void, + ): void; + export function glob( + pattern: string | readonly string[], + options: GlobOptions, + callback: (err: NodeJS.ErrnoException | null, matches?: string[]) => void, + ): void; + /* fs.Dir: an open directory stream (opendirSync, fs.promises.opendir) + * read one Dirent at a time in OS order; utf8 names, no recursive walk. */ + export interface Dir { + readonly path: string; + readSync(): Dirent | null; + read(): Promise; + closeSync(): void; + close(): Promise; + [Symbol.asyncIterator](): AsyncIterableIterator; + } + export interface OpenDirOptions { + encoding?: "utf8" | "utf-8"; + bufferSize?: number; + } + export function opendirSync(path: string | URL, options?: OpenDirOptions): Dir; + /* fs.ReadStream / fs.WriteStream: runtime Readable and Writable streams + * over a file (scr_fs_stream.c). The options are checked at runtime + * with Node's rules; flags and mode are checked when the file opens, so + * their errors arrive as 'error' events. Instances also emit 'open' + * (with the fd) and 'ready'. */ + export interface StreamOptions { + flags?: string; + encoding?: BufferEncoding; + fd?: number; + mode?: number | string; + autoClose?: boolean; + emitClose?: boolean; + start?: number; + highWaterMark?: number; + } + export interface ReadStreamOptions extends StreamOptions { + end?: number; + } + export interface WriteStreamOptions extends StreamOptions { + flush?: boolean; + } + export class ReadStream extends Readable { + readonly bytesRead: number; + readonly path: string | Buffer; + readonly pending: boolean; + close(callback?: () => void): void; + } + export class WriteStream extends Writable { + readonly bytesWritten: number; + readonly path: string | Buffer; + readonly pending: boolean; + close(callback?: () => void): void; + } + export function createReadStream( + path: string | Buffer | URL, + options?: BufferEncoding | ReadStreamOptions, + ): ReadStream; + export function createWriteStream( + path: string | Buffer | URL, + options?: BufferEncoding | WriteStreamOptions, + ): WriteStream; + export function accessSync(path: string | URL, mode?: number): void; + export function mkdtempSync(prefix: string | URL): string; + export const constants: { + readonly F_OK: number; + readonly R_OK: number; + readonly W_OK: number; + readonly X_OK: number; + readonly O_RDONLY: number; + readonly O_WRONLY: number; + readonly O_RDWR: number; + readonly O_CREAT: number; + readonly O_EXCL: number; + readonly O_NOFOLLOW: number; + readonly O_NONBLOCK: number; + readonly O_TRUNC: number; + readonly O_APPEND: number; + readonly COPYFILE_EXCL: number; + readonly COPYFILE_FICLONE: number; + readonly COPYFILE_FICLONE_FORCE: number; + readonly UV_FS_SYMLINK_DIR: number; + readonly UV_FS_SYMLINK_JUNCTION: number; + readonly UV_DIRENT_UNKNOWN: number; + readonly UV_DIRENT_FILE: number; + readonly UV_DIRENT_DIR: number; + readonly UV_DIRENT_LINK: number; + readonly UV_DIRENT_FIFO: number; + readonly UV_DIRENT_SOCKET: number; + readonly UV_DIRENT_CHAR: number; + readonly UV_DIRENT_BLOCK: number; + readonly UV_FS_O_FILEMAP: number; + readonly UV_FS_O_TEMPORARY: number; + readonly UV_FS_O_SHORT_LIVED: number; + readonly UV_FS_O_SEQUENTIAL: number; + readonly UV_FS_O_RANDOM: number; + readonly UV_FS_COPYFILE_EXCL: number; + readonly UV_FS_COPYFILE_FICLONE: number; + readonly UV_FS_COPYFILE_FICLONE_FORCE: number; + readonly S_IFMT: number; + readonly S_IFREG: number; + readonly S_IFDIR: number; + readonly S_IFCHR: number; + readonly S_IFBLK: number; + readonly S_IFIFO: number; + readonly S_IFLNK: number; + readonly S_IFSOCK: number; + readonly O_NOCTTY: number; + readonly O_DIRECTORY: number; + readonly O_NOATIME: number; + readonly O_SYNC: number; + readonly O_DSYNC: number; + readonly O_SYMLINK: number; + readonly O_DIRECT: number; + readonly S_IRWXU: number; + readonly S_IRUSR: number; + readonly S_IWUSR: number; + readonly S_IXUSR: number; + readonly S_IRWXG: number; + readonly S_IRGRP: number; + readonly S_IWGRP: number; + readonly S_IXGRP: number; + readonly S_IRWXO: number; + readonly S_IROTH: number; + readonly S_IWOTH: number; + readonly S_IXOTH: number; + }; + /* A stat(2) snapshot (statSync follows symlinks, lstatSync does not — + * Node's split) — immutable; the supported surface is exactly these + * members. dev/ino identify the filesystem entry, blocks is the allocated + * size in 512-byte units, and the time fields are milliseconds with their + * sub-second fractions. */ + export interface Stats { + isFile(): boolean; + isDirectory(): boolean; + isSymbolicLink(): boolean; + isBlockDevice(): boolean; + isCharacterDevice(): boolean; + isFIFO(): boolean; + isSocket(): boolean; + readonly dev: number; + readonly mode: number; + readonly nlink: number; + readonly uid: number; + readonly gid: number; + readonly rdev: number; + readonly blksize: number; + readonly ino: number; + readonly size: number; + readonly blocks: number; + readonly atimeMs: number; + readonly mtimeMs: number; + readonly ctimeMs: number; + readonly birthtimeMs: number; + readonly atime: Date; + readonly mtime: Date; + readonly ctime: Date; + readonly birthtime: Date; + } + /* throwIfNoEntry: false answers undefined for a missing entry (ENOENT or + * ENOTDIR) instead of throwing. BigIntStats (bigint: true) has no + * lowering. */ + export interface StatSyncOptions { + bigint?: boolean; + throwIfNoEntry?: boolean; + } + export function statSync(path: string | URL, options?: undefined): Stats; + export function statSync( + path: string | URL, + options: StatSyncOptions & { bigint?: false; throwIfNoEntry: false }, + ): Stats | undefined; + export function statSync( + path: string | URL, + options: StatSyncOptions & { bigint?: false; throwIfNoEntry?: true }, + ): Stats; + /* bigint: true type-checks so the lowering can name it as unsupported. */ + export function statSync(path: string | URL, options: StatSyncOptions & { bigint: true }): Stats; + export function statSync(path: string | URL, options: StatSyncOptions): Stats | undefined; + export function lstatSync(path: string | URL, options?: undefined): Stats; + export function lstatSync( + path: string | URL, + options: StatSyncOptions & { bigint?: false; throwIfNoEntry: false }, + ): Stats | undefined; + export function lstatSync( + path: string | URL, + options: StatSyncOptions & { bigint?: false; throwIfNoEntry?: true }, + ): Stats; + /* bigint: true type-checks so the lowering can name it as unsupported. */ + export function lstatSync(path: string | URL, options: StatSyncOptions & { bigint: true }): Stats; + export function lstatSync(path: string | URL, options: StatSyncOptions): Stats | undefined; + /* The fd pair behind spawn's fd-stdio form: openSync(path, flags) → + * the raw fd. Numeric flags use an inline OR of the O_* constants. */ + export function openSync(path: string | URL, flags: string): number; + export function openSync(path: string | URL, flags: number, mode?: number): number; + export function closeSync(fd: number): void; + export function fdatasyncSync(fd: number): void; + export function ftruncateSync(fd: number, len?: number): void; + export function utimesSync( + path: string | Uint8Array | URL, + atime: number | string | Date, + mtime: number | string | Date, + ): void; + export function futimesSync( + fd: number, + atime: number | string | Date, + mtime: number | string | Date, + ): void; + export function lutimesSync( + path: string | Uint8Array | URL, + atime: number | string | Date, + mtime: number | string | Date, + ): void; + export function utimes( + path: string | Uint8Array | URL, + atime: number | string | Date, + mtime: number | string | Date, + callback: (err: NodeJS.ErrnoException | null) => void, + ): void; + export function futimes( + fd: number, + atime: number | string | Date, + mtime: number | string | Date, + callback: (err: NodeJS.ErrnoException | null) => void, + ): void; + export function lutimes( + path: string | Uint8Array | URL, + atime: number | string | Date, + mtime: number | string | Date, + callback: (err: NodeJS.ErrnoException | null) => void, + ): void; + export function readvSync(fd: number, buffers: Uint8Array[], position?: number | null): number; + export function writevSync(fd: number, buffers: Uint8Array[], position?: number | null): number; + /* Read into a caller buffer from the fd's current position when position + * is omitted/null, or from a numeric byte position without advancing the + * fd. Answers the byte count, 0 at EOF. */ + export interface ReadSyncOptions { + offset?: number; + length?: number; + position?: number | null; + } + export function readSync(fd: number, buffer: Uint8Array, options?: ReadSyncOptions): number; + export function readSync( + fd: number, + buffer: Uint8Array, + offset: number, + length: number, + position?: number | null, + ): number; + /* Write a caller-buffer window, or a utf8 string, at the fd's current + * position when position is omitted/null (advancing it), or at a numeric + * byte position without advancing it. Answers the byte count. */ + export interface WriteSyncOptions { + offset?: number; + length?: number; + position?: number | null; + } + export function writeSync(fd: number, buffer: Uint8Array, options: WriteSyncOptions): number; + export function writeSync( + fd: number, + buffer: Uint8Array, + offset?: number, + length?: number, + position?: number | null, + ): number; + export function writeSync( + fd: number, + string: string, + position?: number | null, + encoding?: "utf8" | "utf-8", + ): number; + /* statSync over an open fd — the same Stats snapshot. */ + export function fstatSync(fd: number): Stats; + /* realpath(3) — resolves symlinks, `.`/`..`, throwing Node's fs error + * shapes for missing paths. */ + export function realpathSync(path: string | URL): string; + export namespace realpathSync { + function native(path: string | URL): string; + } + /* statfs(2)/statvfs(3) — the filesystem-capacity snapshot (the fields + * Node's statfsSync reports; bavail × bsize is the free-space probe). */ + export interface StatsFsBase { + type: T; + bsize: T; + blocks: T; + bfree: T; + bavail: T; + files: T; + ffree: T; + } + export interface StatsFs extends StatsFsBase {} + export interface BigIntStatsFs extends StatsFsBase {} + export interface StatFsOptions { + bigint?: boolean; + } + export function statfsSync( + path: string | Uint8Array | URL, + options?: StatFsOptions & { bigint?: false }, + ): StatsFs; + export function statfsSync( + path: string | Uint8Array | URL, + options: StatFsOptions & { bigint: true }, + ): BigIntStatsFs; + export function statfsSync( + path: string | Uint8Array | URL, + options?: StatFsOptions, + ): StatsFs | BigIntStatsFs; + export function statfs( + path: string | Uint8Array | URL, + callback: (err: NodeJS.ErrnoException | null, stats: StatsFs) => void, + ): void; + export function statfs( + path: string | Uint8Array | URL, + options: (StatFsOptions & { bigint?: false }) | undefined, + callback: (err: NodeJS.ErrnoException | null, stats: StatsFs) => void, + ): void; + export function statfs( + path: string | Uint8Array | URL, + options: StatFsOptions & { bigint: true }, + callback: (err: NodeJS.ErrnoException | null, stats: BigIntStatsFs) => void, + ): void; + export function statfs( + path: string | Uint8Array | URL, + options: StatFsOptions | undefined, + callback: (err: NodeJS.ErrnoException | null, stats: StatsFs | BigIntStatsFs) => void, + ): void; + /* fs.watch — the file-watching slice (scr_watch.c, kqueue EVFILT_VNODE): + * "rename"/"change" events on ONE path, watcher.close() to stop. The + * listener takes zero parameters or the eventType string (the filename + * parameter has no lowering); an open watcher keeps the loop alive. A + * path that cannot be opened THROWS Node's fs error synchronously. */ + export interface FSWatcher { + close(): void; + } + /* The options-record stance (see http.RequestOptions): persistent: true, + * recursive: false, and encoding: "utf8" state the lowered behavior and + * are accepted; persistent: false, recursive: true, and signal fence by + * name at the call site; undocumented keys drop like Node. */ + export interface WatchOptions { + persistent?: boolean; + recursive?: boolean; + encoding?: string; + [option: string]: unknown; + } + export function watch(path: string, listener?: (eventType: string) => void): FSWatcher; + export function watch( + path: string, + options: WatchOptions, + listener?: (eventType: string) => void, + ): FSWatcher; + /* fs.promises IS the fs/promises module (Node's rule) — the namespace- + * import form `fs.promises.readFile(...)` lowers through the same + * fs/promises table as `import { readFile } from "node:fs/promises"`. */ + export const promises: typeof import("node:fs/promises"); +} + +/* Every builtin answers to BOTH specifier forms, like in Node: the bare + * module re-exports its node:-prefixed twin (or holds the declarations + * itself — one of the pair forwards). The lowerer recognizes members by + * their import SPECIFIER, so both forms lower identically. */ +declare module "fs" { + export * from "node:fs"; +} diff --git a/packages/compiler/ambient/node-fallback/graceful-fs.d.ts b/packages/compiler/ambient/node-fallback/graceful-fs.d.ts new file mode 100644 index 000000000..e735fa3e6 --- /dev/null +++ b/packages/compiler/ambient/node-fallback/graceful-fs.d.ts @@ -0,0 +1,9 @@ +/// + +/* graceful-fs — served as node:fs (it monkeypatches fs and retries on + * EMFILE/ENFILE; compiled programs call fs directly, without the retry + * queue). gracefulify is a no-op statement. The @types/graceful-fs shape. */ +declare module "graceful-fs" { + export * from "node:fs"; + export function gracefulify(fsModule: T): T; +} diff --git a/packages/compiler/ambient/node-fallback/http.d.ts b/packages/compiler/ambient/node-fallback/http.d.ts new file mode 100644 index 000000000..5588767eb --- /dev/null +++ b/packages/compiler/ambient/node-fallback/http.d.ts @@ -0,0 +1,326 @@ +/// +/// + +/* node:http — the SERVER and CLIENT slices (scr_http.c layers a + * hand-written HTTP/1.1 parser + serializer on scr_net.c; both link only + * into using binaries). createServer(handler) returns a net.Server — + * listen/close/address().port/'error' are the same lowered surface. The + * request carries url/method (always present on server requests), + * statusCode (a number on client responses, undefined on server + * requests), the underlying socket, and lowercased headers read as + * `string | undefined`; the body streams through on("data"/"end"). The + * response is setHeader/writeHead/write/end/destroy with Node's framing + * (Content-Length for a single end(body), chunked after writeHead/write) + * and the auto Date/Connection headers; writeHead's headers argument is + * an object literal or a Record. request/get dial one + * connection per call (no agent pooling) with Node's exact wire head. */ +declare module "http" { + import { Server as NetServer, Socket } from "net"; + export const METHODS: string[]; + export const STATUS_CODES: { [status: number]: string | undefined }; + export const maxHeaderSize: number; + export interface Server extends NetServer { + timeout: number; + setTimeout(msecs: number, callback?: (socket: Socket) => void): this; + closeAllConnections(): void; + closeIdleConnections(): void; + keepAliveTimeout: number; + keepAliveTimeoutBuffer: number; + headersTimeout: number; + requestTimeout: number; + } + export type RequestListener = (req: IncomingMessage, res: ServerResponse) => void; + /* The Server VALUE — Node's constructor works with and without `new` + * (test/parallel's http.Server(fn) spelling); both route to the + * createServer lowering. */ + export const Server: { + new (requestListener?: (req: IncomingMessage, res: ServerResponse) => void): Server; + new ( + options: ServerOptions, + requestListener?: (req: IncomingMessage, res: ServerResponse) => void, + ): Server; + (requestListener?: (req: IncomingMessage, res: ServerResponse) => void): Server; + ( + options: ServerOptions, + requestListener?: (req: IncomingMessage, res: ServerResponse) => void, + ): Server; + }; + /* The options-record stance (the RequestOptions precedent): every + * documented key typechecks; the lowering's option walk decides per + * key — requireHostHeader: false, joinDuplicateHeaders, and + * keepAliveTimeoutBuffer lower, + * documented-but-unlowered keys fence by name, unknown keys drop. */ + export interface ServerOptions { + requireHostHeader?: boolean; + joinDuplicateHeaders?: boolean; + keepAliveTimeoutBuffer?: number; + maxHeaderSize?: number; + [option: string]: unknown; + } + /* The outgoing-header shape, @types/node's matrix: numbers format via + * String(n), arrays write one line per element (set-cookie's shape). + * Incoming headers stay `string | undefined` — what the parser answers + * (under @types/node the two SLOTS unify so `{ ...req.headers }` + * merges into outgoing literals as a copy; the fallback keeps reads + * simple and spreads target the same string | undefined slot). */ + export type OutgoingHttpHeaders = { [name: string]: number | string | string[] | undefined }; + export interface IncomingMessage { + readonly url: string; + readonly method: string; + readonly httpVersion: string; + readonly complete: boolean; + readonly aborted: boolean; + readonly statusCode: number | undefined; + readonly statusMessage: string | undefined; + readonly socket: Socket; + readonly connection: Socket; + readonly headers: { [name: string]: string | undefined }; + readonly headersDistinct: { [name: string]: string[] | undefined }; + readonly rawHeaders: string[]; + readonly trailers: { [name: string]: string | undefined }; + readonly trailersDistinct: { [name: string]: string[] | undefined }; + readonly rawTrailers: string[]; + resume(): void; + pause(): void; + destroy(): void; + setTimeout(msecs: number, callback?: () => void): this; + /* setEncoding('utf8'): 'data' delivers strings (other real encodings + * throw a "not supported yet" Error at runtime; unknown names throw + * ERR_UNKNOWN_ENCODING). */ + setEncoding(encoding: string): void; + /* The proxy legs: the body streams into a ServerResponse, a + * ClientRequest, or a raw Socket; natural end ends the destination. */ + pipe( + destination: ServerResponse | import("http2").Http2ServerResponse | ClientRequest | Socket, + ): void; + on(event: "data", listener: (chunk: any) => void): void; + on(event: "end" | "close" | "aborted", listener: () => void): void; + on(event: "error", listener: (err: Error) => void): void; + /* addListener IS on (Node aliases them) — the suite spells both. */ + addListener(event: "data", listener: (chunk: any) => void): void; + addListener(event: "end" | "close" | "aborted", listener: () => void): void; + addListener(event: "error", listener: (err: Error) => void): void; + once(event: "data", listener: (chunk: any) => void): void; + once(event: "end" | "close" | "aborted", listener: () => void): void; + once(event: "error", listener: (err: Error) => void): void; + } + export interface ServerResponse { + readonly headersSent: boolean; + readonly writableEnded: boolean; + readonly finished: boolean; + readonly writableFinished: boolean; + readonly writableCorked: number; + readonly req: IncomingMessage; + readonly socket: Socket | null; + readonly connection: Socket | null; + sendDate: boolean; + strictContentLength: boolean; + /* Node's writable head properties: the implicit head reads them. */ + statusCode: number; + statusMessage: string | undefined; + /* The value keeps its type: getHeader answers the number, string or + * array that was set. */ + setHeader(name: string, value: number | string | readonly string[]): void; + appendHeader(name: string, value: string | readonly string[]): void; + getHeader(name: string): number | string | string[] | undefined; + getHeaderNames(): string[]; + getRawHeaderNames(): string[]; + getHeaders(): OutgoingHttpHeaders; + hasHeader(name: string): boolean; + removeHeader(name: string): void; + /* Both overloads answer the response (`return this` chaining); the + * headers argument also takes Node's flat [name, value, ...] array. */ + writeHead(statusCode: number, headers?: OutgoingHttpHeaders | string[]): ServerResponse; + writeHead( + statusCode: number, + statusMessage: string, + headers?: OutgoingHttpHeaders | string[], + ): ServerResponse; + writeContinue(callback?: () => void): void; + writeProcessing(callback?: () => void): void; + writeEarlyHints(hints: Record, callback?: () => void): void; + write(data: string | Uint8Array): void; + flushHeaders(): void; + cork(): void; + uncork(): void; + addTrailers(headers: OutgoingHttpHeaders | ReadonlyArray<[string, string]>): void; + setTimeout(msecs: number, callback?: () => void): this; + /* end's callback forms fire once the body went out (the 'finish' + * emit, deferred past the handler's synchronous tail). */ + end(data?: string | Uint8Array, callback?: () => void): void; + end(callback: () => void): void; + destroy(): void; + on(event: "close" | "finish", listener: () => void): void; + /* addListener IS on (Node aliases them) — the suite spells both. */ + addListener(event: "close" | "finish", listener: () => void): void; + once(event: "close" | "finish", listener: () => void): void; + } + /* The CLIENT slice (http.request/http.get): the options-object form + * with hostname/host, port, path, method, timeout, and headers; the + * callback receives the response as an IncomingMessage (statusCode, + * headers, body via on("data"/"end")). */ + export interface RequestOptions { + hostname?: string | null; + host?: string | null; + /* A string port (a URL's .port) is accepted, as by Node. */ + port?: number | string | null; + path?: string | null; + method?: string; + timeout?: number; + headers?: OutgoingHttpHeaders; + /* The caller's own dialer (the proxy's loopback dial): invoked once, + * synchronously; its socket carries the exchange. */ + createConnection?: () => Socket; + /* agent: false is a one-shot dial with Connection: close — exactly + * the compiled client's model; null/undefined are the default; an + * Agent VALUE threads through (getName-keyed maxSockets accounting + * over one-dial-per-request connections). */ + agent?: Agent | boolean | null; + /* Aborting destroys the request with an AbortError. */ + signal?: AbortSignal; + /* HTTP over the Unix socket at this path instead of host:port. */ + socketPath?: string; + /* The options-record stance: Node ignores an options record's + * unknown keys, so every key typechecks here — the option WALK in + * the lowering decides per key (lower, fence by name, or drop + * undocumented keys exactly like Node). */ + [option: string]: unknown; + } + /* The Agent lowers to a checked-dynamic HANDLE: option parsing at the + * literal construction site, getName, destroy, the sockets/requests/ + * freeSockets snapshots, and REAL maxSockets accounting (over-limit + * requests defer their dial and queue). keepAlive: true keeps finished + * connections in freeSockets for the next request to the same name, + * taken in `scheduling` order. */ + export interface AgentOptions { + keepAlive?: boolean; + keepAliveMsecs?: number; + maxSockets?: number; + maxFreeSockets?: number; + maxTotalSockets?: number; + scheduling?: "fifo" | "lifo"; + timeout?: number; + port?: number; + [option: string]: unknown; + } + export class Agent { + constructor(options?: AgentOptions); + destroy(): void; + /* Node's exact key shape: host:port:localAddress[:family][:socketPath]. */ + getName(options?: { + host?: string; + port?: number; + localAddress?: string; + family?: number; + socketPath?: string; + [option: string]: unknown; + }): string; + maxSockets: number; + maxFreeSockets: number; + keepAliveMsecs: number; + readonly keepAlive: boolean; + readonly protocol: string; + /* Settable — a portless request through this agent dials it (Node's + * option merge). */ + defaultPort: number; + readonly totalSocketCount: number; + readonly sockets: { [key: string]: unknown[] }; + readonly freeSockets: { [key: string]: unknown[] }; + readonly requests: { [key: string]: unknown[] }; + } + /* Node's keepAlive global agent for requests without an agent option. */ + export const globalAgent: Agent; + export interface ClientRequest { + readonly destroyed: boolean; + readonly aborted: boolean; + readonly socket: Socket; + readonly connection: Socket; + readonly writableCorked: number; + readonly method: string; + readonly path: string; + readonly host: string; + readonly protocol: string; + readonly headersSent: boolean; + readonly writableEnded: boolean; + readonly writableFinished: boolean; + readonly finished: boolean; + readonly reusedSocket: boolean; + setHeader(name: string, value: string): void; + getHeader(name: string): string | undefined; + hasHeader(name: string): boolean; + removeHeader(name: string): void; + getHeaderNames(): string[]; + getRawHeaderNames(): string[]; + getHeaders(): OutgoingHttpHeaders; + flushHeaders(): void; + addTrailers(headers: OutgoingHttpHeaders | ReadonlyArray<[string, string]>): void; + cork(): void; + uncork(): void; + setNoDelay(noDelay?: boolean): void; + setSocketKeepAlive(enable?: boolean, initialDelay?: number): void; + setTimeout(timeout: number, callback?: () => void): this; + write(data: string | Uint8Array): void; + end(data?: string | Uint8Array): void; + destroy(error?: Error): void; + abort(): void; + on(event: "response", listener: (res: IncomingMessage) => void): void; + on(event: "socket", listener: (socket: Socket) => void): void; + on( + event: "upgrade", + listener: (res: IncomingMessage, socket: Socket, head: Buffer) => void, + ): void; + on(event: "timeout" | "close" | "finish" | "abort" | "continue", listener: () => void): void; + on(event: "error", listener: (err: Error) => void): void; + /* addListener IS on (Node aliases them) — the suite spells both. */ + addListener(event: "response", listener: (res: IncomingMessage) => void): void; + addListener(event: "socket", listener: (socket: Socket) => void): void; + addListener( + event: "upgrade", + listener: (res: IncomingMessage, socket: Socket, head: Buffer) => void, + ): void; + addListener(event: "timeout" | "close" | "finish" | "abort", listener: () => void): void; + addListener(event: "error", listener: (err: Error) => void): void; + once(event: "response", listener: (res: IncomingMessage) => void): void; + once(event: "socket", listener: (socket: Socket) => void): void; + once( + event: "upgrade", + listener: (res: IncomingMessage, socket: Socket, head: Buffer) => void, + ): void; + once(event: "timeout" | "close" | "finish" | "abort" | "continue", listener: () => void): void; + once(event: "error", listener: (err: Error) => void): void; + } + export function createServer( + requestListener?: (req: IncomingMessage, res: ServerResponse) => void, + ): Server; + export function createServer( + options: ServerOptions, + requestListener?: (req: IncomingMessage, res: ServerResponse) => void, + ): Server; + /* One signature with a UNION first parameter rather than Node's two + * overloads: `const requestFn = tls ? https.request : http.request` + * (the portless isProxyRunning shape) unions two function types, and + * resolving a call on that union collapses each constituent to a + * single signature — with an overload SET on both sides the options + * literal ends up checked against URL, in either declaration order. + * A union parameter has nothing to collapse. The lowering dispatches + * on the argument's own type, so it reads the same either way. + * + * `target` is a URL string or URL object (the spelling a from-scratch + * client reaches for first — dialed directly, with a non-http scheme + * an ERR_INVALID_PROTOCOL at runtime rather than a type error), or + * the options record. */ + export function request( + target: RequestOptions | string | URL, + callback?: (res: IncomingMessage) => void, + ): ClientRequest; + export function get( + target: RequestOptions | string | URL, + callback?: (res: IncomingMessage) => void, + ): ClientRequest; + export function validateHeaderName(name: string, label?: string): void; + export function validateHeaderValue(name: string, value: unknown): void; +} + +declare module "node:http" { + export * from "http"; +} diff --git a/packages/compiler/ambient/node-fallback/http2.d.ts b/packages/compiler/ambient/node-fallback/http2.d.ts new file mode 100644 index 000000000..d47be0189 --- /dev/null +++ b/packages/compiler/ambient/node-fallback/http2.d.ts @@ -0,0 +1,816 @@ +/// +/// +/// +/// + +/* node:http2 — the compatibility surface: allowHTTP1 servers negotiate + * h2 or HTTP/1.1 and expose the shared request/response API. HTTP/2 + * requests retain their backing stream; server session failures surface + * through sessionError. */ +declare module "http2" { + import { Socket } from "net"; + export interface Http2Stream { + on(event: "error" | "close", listener: (err: Error) => void): void; + once(event: "error" | "close", listener: (err: Error) => void): void; + addListener(event: "error" | "close", listener: (err: Error) => void): void; + destroy(): void; + } + export interface Http2ServerRequest { + readonly url: string; + readonly method: string; + readonly statusCode: number | undefined; + readonly socket: Socket; + readonly headers: { [name: string]: string | undefined }; + readonly stream: Http2Stream; + /* The h2 compat request's version triple ("2.0" over real h2 streams; + * the allowHTTP1 lowering's HTTP/1.1 connections answer 1.x). */ + readonly httpVersion: string; + readonly httpVersionMajor: number; + readonly httpVersionMinor: number; + readonly aborted: boolean; + readonly complete: boolean; + resume(): void; + destroy(): void; + setEncoding(encoding: string): void; + pipe(destination: Socket | import("http").ClientRequest): void; + on(event: "data", listener: (chunk: Buffer) => void): void; + on(event: "end" | "close" | "aborted", listener: () => void): void; + on(event: "error", listener: (err: Error) => void): void; + /* addListener IS on (Node aliases them) — and the compat req IS the + * http.IncomingMessage surface, so the alias mirrors here too. */ + addListener(event: "data", listener: (chunk: Buffer) => void): void; + addListener(event: "end" | "close" | "aborted", listener: () => void): void; + addListener(event: "error", listener: (err: Error) => void): void; + once(event: "data", listener: (chunk: Buffer) => void): void; + once(event: "end" | "close" | "aborted", listener: () => void): void; + once(event: "error", listener: (err: Error) => void): void; + } + export interface Http2ServerResponse { + readonly headersSent: boolean; + readonly writableEnded: boolean; + readonly writableCorked: number; + /* The same lowered surface as http.ServerResponse — the allowHTTP1 + * lowering serves every connection as HTTP/1.1, where the compat + * response IS this parser's response handle. */ + statusCode: number; + statusMessage: string; + setHeader(name: string, value: string | number): void; + getHeader(name: string): string | undefined; + getHeaderNames(): string[]; + getRawHeaderNames(): string[]; + getHeaders(): import("http").OutgoingHttpHeaders; + hasHeader(name: string): boolean; + removeHeader(name: string): void; + writeHead( + statusCode: number, + headers?: import("http").OutgoingHttpHeaders | string[], + ): Http2ServerResponse; + writeHead( + statusCode: number, + statusMessage: string, + headers?: import("http").OutgoingHttpHeaders | string[], + ): Http2ServerResponse; + writeContinue(): void; + writeEarlyHints(hints: Record): void; + write(data: string | Uint8Array): void; + flushHeaders(): void; + cork(): void; + uncork(): void; + addTrailers( + headers: import("http").OutgoingHttpHeaders | ReadonlyArray<[string, string]>, + ): void; + end(data?: string | Uint8Array, callback?: () => void): void; + end(callback: () => void): void; + destroy(): void; + on(event: "close", listener: () => void): void; + /* addListener IS on (Node aliases them) — and the compat res IS the + * http.ServerResponse surface (assignability into ServerResponse + * unions — IncomingMessage.pipe's destination — needs the alias). */ + addListener(event: "close", listener: () => void): void; + once(event: "close", listener: () => void): void; + } + export interface Http2SecureServer { + listen(port: number, callback?: () => void): void; + close(callback?: () => void): void; + /* The bound AddressInfo (Node answers null before listen — this + * surface answers the record with port 0 there, the serverPort + * stance). */ + address(): import("node:dgram").AddressInfo; + emit(event: "connection", socket: Socket): void; + on(event: "connection", listener: (socket: Socket) => void): void; + on( + event: "request", + listener: (req: Http2ServerRequest, res: Http2ServerResponse) => void, + ): void; + on(event: "error", listener: (err: Error) => void): void; + on(event: "close", listener: () => void): void; + on(event: "sessionError", listener: (err: Error, session: ServerHttp2Session) => void): void; + /* The ALPN=h2 server's surface (createSecureServer WITHOUT + * allowHTTP1 — the real h2-over-TLS stack): per-stream and + * per-session listeners, exactly the h2c server's. */ + on( + event: "stream", + listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void, + ): void; + on(event: "session", listener: (session: ServerHttp2Session) => void): void; + /* HTTP CONNECT (and, in Node, the h2 extended CONNECT — which the + * allowHTTP1 lowering never sees): the second argument is the raw + * socket on HTTP/1.1 connections; narrow with instanceof. */ + on( + event: "connect", + listener: ( + req: Http2ServerRequest | import("http").IncomingMessage, + resOrSocket: Http2ServerResponse | Socket, + ) => void, + ): void; + once(event: "connection", listener: (socket: Socket) => void): void; + once( + event: "request", + listener: (req: Http2ServerRequest, res: Http2ServerResponse) => void, + ): void; + once(event: "error", listener: (err: Error) => void): void; + once(event: "close", listener: () => void): void; + once(event: "sessionError", listener: (err: Error, session: ServerHttp2Session) => void): void; + once( + event: "stream", + listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void, + ): void; + once(event: "session", listener: (session: ServerHttp2Session) => void): void; + once( + event: "connect", + listener: ( + req: Http2ServerRequest | import("http").IncomingMessage, + resOrSocket: Http2ServerResponse | Socket, + ) => void, + ): void; + addListener(event: "connection", listener: (socket: Socket) => void): void; + addListener( + event: "request", + listener: (req: Http2ServerRequest, res: Http2ServerResponse) => void, + ): void; + addListener(event: "error", listener: (err: Error) => void): void; + addListener(event: "close", listener: () => void): void; + addListener( + event: "sessionError", + listener: (err: Error, session: ServerHttp2Session) => void, + ): void; + addListener( + event: "stream", + listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void, + ): void; + addListener(event: "session", listener: (session: ServerHttp2Session) => void): void; + addListener( + event: "connect", + listener: ( + req: Http2ServerRequest | import("http").IncomingMessage, + resOrSocket: Http2ServerResponse | Socket, + ) => void, + ): void; + } + export interface SecureServerOptions { + allowHTTP1?: boolean; + cert?: string | Uint8Array; + key?: string | Uint8Array; + SNICallback?: ( + servername: string, + cb: (err: Error | null, ctx?: import("tls").SecureContext) => void, + ) => void; + /* The options-record stance (see http.RequestOptions): h2 session- + * tuning knobs are accepted as literals and ignored (no h2 session + * ever exists under the allowHTTP1 lowering); other documented keys + * fence by name; undocumented keys drop like Node. */ + [option: string]: unknown; + } + /* The full Node v24 constants table (240 members), literal-typed so + * computed header keys ([HTTP2_HEADER_PATH]: value) stay literal keys. + * Every ACCESS bakes as its literal at lowering; the object itself + * never materializes (bare http2.constants value uses fence). */ + export interface Http2Constants { + readonly NGHTTP2_ERR_FRAME_SIZE_ERROR: -522; + readonly NGHTTP2_SESSION_SERVER: 0; + readonly NGHTTP2_SESSION_CLIENT: 1; + readonly NGHTTP2_STREAM_STATE_IDLE: 1; + readonly NGHTTP2_STREAM_STATE_OPEN: 2; + readonly NGHTTP2_STREAM_STATE_RESERVED_LOCAL: 3; + readonly NGHTTP2_STREAM_STATE_RESERVED_REMOTE: 4; + readonly NGHTTP2_STREAM_STATE_HALF_CLOSED_LOCAL: 5; + readonly NGHTTP2_STREAM_STATE_HALF_CLOSED_REMOTE: 6; + readonly NGHTTP2_STREAM_STATE_CLOSED: 7; + readonly NGHTTP2_FLAG_NONE: 0; + readonly NGHTTP2_FLAG_END_STREAM: 1; + readonly NGHTTP2_FLAG_END_HEADERS: 4; + readonly NGHTTP2_FLAG_ACK: 1; + readonly NGHTTP2_FLAG_PADDED: 8; + readonly NGHTTP2_FLAG_PRIORITY: 32; + readonly DEFAULT_SETTINGS_HEADER_TABLE_SIZE: 4096; + readonly DEFAULT_SETTINGS_ENABLE_PUSH: 1; + readonly DEFAULT_SETTINGS_MAX_CONCURRENT_STREAMS: 4294967295; + readonly DEFAULT_SETTINGS_INITIAL_WINDOW_SIZE: 65535; + readonly DEFAULT_SETTINGS_MAX_FRAME_SIZE: 16384; + readonly DEFAULT_SETTINGS_MAX_HEADER_LIST_SIZE: 65535; + readonly DEFAULT_SETTINGS_ENABLE_CONNECT_PROTOCOL: 0; + readonly MAX_MAX_FRAME_SIZE: 16777215; + readonly MIN_MAX_FRAME_SIZE: 16384; + readonly MAX_INITIAL_WINDOW_SIZE: 2147483647; + readonly NGHTTP2_SETTINGS_HEADER_TABLE_SIZE: 1; + readonly NGHTTP2_SETTINGS_ENABLE_PUSH: 2; + readonly NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS: 3; + readonly NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE: 4; + readonly NGHTTP2_SETTINGS_MAX_FRAME_SIZE: 5; + readonly NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE: 6; + readonly NGHTTP2_SETTINGS_ENABLE_CONNECT_PROTOCOL: 8; + readonly PADDING_STRATEGY_NONE: 0; + readonly PADDING_STRATEGY_ALIGNED: 1; + readonly PADDING_STRATEGY_MAX: 2; + readonly PADDING_STRATEGY_CALLBACK: 1; + readonly NGHTTP2_NO_ERROR: 0; + readonly NGHTTP2_PROTOCOL_ERROR: 1; + readonly NGHTTP2_INTERNAL_ERROR: 2; + readonly NGHTTP2_FLOW_CONTROL_ERROR: 3; + readonly NGHTTP2_SETTINGS_TIMEOUT: 4; + readonly NGHTTP2_STREAM_CLOSED: 5; + readonly NGHTTP2_FRAME_SIZE_ERROR: 6; + readonly NGHTTP2_REFUSED_STREAM: 7; + readonly NGHTTP2_CANCEL: 8; + readonly NGHTTP2_COMPRESSION_ERROR: 9; + readonly NGHTTP2_CONNECT_ERROR: 10; + readonly NGHTTP2_ENHANCE_YOUR_CALM: 11; + readonly NGHTTP2_INADEQUATE_SECURITY: 12; + readonly NGHTTP2_HTTP_1_1_REQUIRED: 13; + readonly NGHTTP2_DEFAULT_WEIGHT: 16; + readonly HTTP2_HEADER_STATUS: ":status"; + readonly HTTP2_HEADER_METHOD: ":method"; + readonly HTTP2_HEADER_AUTHORITY: ":authority"; + readonly HTTP2_HEADER_SCHEME: ":scheme"; + readonly HTTP2_HEADER_PATH: ":path"; + readonly HTTP2_HEADER_PROTOCOL: ":protocol"; + readonly HTTP2_HEADER_ACCEPT_ENCODING: "accept-encoding"; + readonly HTTP2_HEADER_ACCEPT_LANGUAGE: "accept-language"; + readonly HTTP2_HEADER_ACCEPT_RANGES: "accept-ranges"; + readonly HTTP2_HEADER_ACCEPT: "accept"; + readonly HTTP2_HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS: "access-control-allow-credentials"; + readonly HTTP2_HEADER_ACCESS_CONTROL_ALLOW_HEADERS: "access-control-allow-headers"; + readonly HTTP2_HEADER_ACCESS_CONTROL_ALLOW_METHODS: "access-control-allow-methods"; + readonly HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN: "access-control-allow-origin"; + readonly HTTP2_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS: "access-control-expose-headers"; + readonly HTTP2_HEADER_ACCESS_CONTROL_REQUEST_HEADERS: "access-control-request-headers"; + readonly HTTP2_HEADER_ACCESS_CONTROL_REQUEST_METHOD: "access-control-request-method"; + readonly HTTP2_HEADER_AGE: "age"; + readonly HTTP2_HEADER_AUTHORIZATION: "authorization"; + readonly HTTP2_HEADER_CACHE_CONTROL: "cache-control"; + readonly HTTP2_HEADER_CONNECTION: "connection"; + readonly HTTP2_HEADER_CONTENT_DISPOSITION: "content-disposition"; + readonly HTTP2_HEADER_CONTENT_ENCODING: "content-encoding"; + readonly HTTP2_HEADER_CONTENT_LENGTH: "content-length"; + readonly HTTP2_HEADER_CONTENT_TYPE: "content-type"; + readonly HTTP2_HEADER_COOKIE: "cookie"; + readonly HTTP2_HEADER_DATE: "date"; + readonly HTTP2_HEADER_ETAG: "etag"; + readonly HTTP2_HEADER_FORWARDED: "forwarded"; + readonly HTTP2_HEADER_HOST: "host"; + readonly HTTP2_HEADER_IF_MODIFIED_SINCE: "if-modified-since"; + readonly HTTP2_HEADER_IF_NONE_MATCH: "if-none-match"; + readonly HTTP2_HEADER_IF_RANGE: "if-range"; + readonly HTTP2_HEADER_LAST_MODIFIED: "last-modified"; + readonly HTTP2_HEADER_LINK: "link"; + readonly HTTP2_HEADER_LOCATION: "location"; + readonly HTTP2_HEADER_RANGE: "range"; + readonly HTTP2_HEADER_REFERER: "referer"; + readonly HTTP2_HEADER_SERVER: "server"; + readonly HTTP2_HEADER_SET_COOKIE: "set-cookie"; + readonly HTTP2_HEADER_STRICT_TRANSPORT_SECURITY: "strict-transport-security"; + readonly HTTP2_HEADER_TRANSFER_ENCODING: "transfer-encoding"; + readonly HTTP2_HEADER_TE: "te"; + readonly HTTP2_HEADER_UPGRADE_INSECURE_REQUESTS: "upgrade-insecure-requests"; + readonly HTTP2_HEADER_UPGRADE: "upgrade"; + readonly HTTP2_HEADER_USER_AGENT: "user-agent"; + readonly HTTP2_HEADER_VARY: "vary"; + readonly HTTP2_HEADER_X_CONTENT_TYPE_OPTIONS: "x-content-type-options"; + readonly HTTP2_HEADER_X_FRAME_OPTIONS: "x-frame-options"; + readonly HTTP2_HEADER_KEEP_ALIVE: "keep-alive"; + readonly HTTP2_HEADER_PROXY_CONNECTION: "proxy-connection"; + readonly HTTP2_HEADER_X_XSS_PROTECTION: "x-xss-protection"; + readonly HTTP2_HEADER_ALT_SVC: "alt-svc"; + readonly HTTP2_HEADER_CONTENT_SECURITY_POLICY: "content-security-policy"; + readonly HTTP2_HEADER_EARLY_DATA: "early-data"; + readonly HTTP2_HEADER_EXPECT_CT: "expect-ct"; + readonly HTTP2_HEADER_ORIGIN: "origin"; + readonly HTTP2_HEADER_PURPOSE: "purpose"; + readonly HTTP2_HEADER_TIMING_ALLOW_ORIGIN: "timing-allow-origin"; + readonly HTTP2_HEADER_X_FORWARDED_FOR: "x-forwarded-for"; + readonly HTTP2_HEADER_PRIORITY: "priority"; + readonly HTTP2_HEADER_ACCEPT_CHARSET: "accept-charset"; + readonly HTTP2_HEADER_ACCESS_CONTROL_MAX_AGE: "access-control-max-age"; + readonly HTTP2_HEADER_ALLOW: "allow"; + readonly HTTP2_HEADER_CONTENT_LANGUAGE: "content-language"; + readonly HTTP2_HEADER_CONTENT_LOCATION: "content-location"; + readonly HTTP2_HEADER_CONTENT_MD5: "content-md5"; + readonly HTTP2_HEADER_CONTENT_RANGE: "content-range"; + readonly HTTP2_HEADER_DNT: "dnt"; + readonly HTTP2_HEADER_EXPECT: "expect"; + readonly HTTP2_HEADER_EXPIRES: "expires"; + readonly HTTP2_HEADER_FROM: "from"; + readonly HTTP2_HEADER_IF_MATCH: "if-match"; + readonly HTTP2_HEADER_IF_UNMODIFIED_SINCE: "if-unmodified-since"; + readonly HTTP2_HEADER_MAX_FORWARDS: "max-forwards"; + readonly HTTP2_HEADER_PREFER: "prefer"; + readonly HTTP2_HEADER_PROXY_AUTHENTICATE: "proxy-authenticate"; + readonly HTTP2_HEADER_PROXY_AUTHORIZATION: "proxy-authorization"; + readonly HTTP2_HEADER_REFRESH: "refresh"; + readonly HTTP2_HEADER_RETRY_AFTER: "retry-after"; + readonly HTTP2_HEADER_TRAILER: "trailer"; + readonly HTTP2_HEADER_TK: "tk"; + readonly HTTP2_HEADER_VIA: "via"; + readonly HTTP2_HEADER_WARNING: "warning"; + readonly HTTP2_HEADER_WWW_AUTHENTICATE: "www-authenticate"; + readonly HTTP2_HEADER_HTTP2_SETTINGS: "http2-settings"; + readonly HTTP2_METHOD_ACL: "ACL"; + readonly HTTP2_METHOD_BASELINE_CONTROL: "BASELINE-CONTROL"; + readonly HTTP2_METHOD_BIND: "BIND"; + readonly HTTP2_METHOD_CHECKIN: "CHECKIN"; + readonly HTTP2_METHOD_CHECKOUT: "CHECKOUT"; + readonly HTTP2_METHOD_CONNECT: "CONNECT"; + readonly HTTP2_METHOD_COPY: "COPY"; + readonly HTTP2_METHOD_DELETE: "DELETE"; + readonly HTTP2_METHOD_GET: "GET"; + readonly HTTP2_METHOD_HEAD: "HEAD"; + readonly HTTP2_METHOD_LABEL: "LABEL"; + readonly HTTP2_METHOD_LINK: "LINK"; + readonly HTTP2_METHOD_LOCK: "LOCK"; + readonly HTTP2_METHOD_MERGE: "MERGE"; + readonly HTTP2_METHOD_MKACTIVITY: "MKACTIVITY"; + readonly HTTP2_METHOD_MKCALENDAR: "MKCALENDAR"; + readonly HTTP2_METHOD_MKCOL: "MKCOL"; + readonly HTTP2_METHOD_MKREDIRECTREF: "MKREDIRECTREF"; + readonly HTTP2_METHOD_MKWORKSPACE: "MKWORKSPACE"; + readonly HTTP2_METHOD_MOVE: "MOVE"; + readonly HTTP2_METHOD_OPTIONS: "OPTIONS"; + readonly HTTP2_METHOD_ORDERPATCH: "ORDERPATCH"; + readonly HTTP2_METHOD_PATCH: "PATCH"; + readonly HTTP2_METHOD_POST: "POST"; + readonly HTTP2_METHOD_PRI: "PRI"; + readonly HTTP2_METHOD_PROPFIND: "PROPFIND"; + readonly HTTP2_METHOD_PROPPATCH: "PROPPATCH"; + readonly HTTP2_METHOD_PUT: "PUT"; + readonly HTTP2_METHOD_REBIND: "REBIND"; + readonly HTTP2_METHOD_REPORT: "REPORT"; + readonly HTTP2_METHOD_SEARCH: "SEARCH"; + readonly HTTP2_METHOD_TRACE: "TRACE"; + readonly HTTP2_METHOD_UNBIND: "UNBIND"; + readonly HTTP2_METHOD_UNCHECKOUT: "UNCHECKOUT"; + readonly HTTP2_METHOD_UNLINK: "UNLINK"; + readonly HTTP2_METHOD_UNLOCK: "UNLOCK"; + readonly HTTP2_METHOD_UPDATE: "UPDATE"; + readonly HTTP2_METHOD_UPDATEREDIRECTREF: "UPDATEREDIRECTREF"; + readonly HTTP2_METHOD_VERSION_CONTROL: "VERSION-CONTROL"; + readonly HTTP_STATUS_CONTINUE: 100; + readonly HTTP_STATUS_SWITCHING_PROTOCOLS: 101; + readonly HTTP_STATUS_PROCESSING: 102; + readonly HTTP_STATUS_EARLY_HINTS: 103; + readonly HTTP_STATUS_OK: 200; + readonly HTTP_STATUS_CREATED: 201; + readonly HTTP_STATUS_ACCEPTED: 202; + readonly HTTP_STATUS_NON_AUTHORITATIVE_INFORMATION: 203; + readonly HTTP_STATUS_NO_CONTENT: 204; + readonly HTTP_STATUS_RESET_CONTENT: 205; + readonly HTTP_STATUS_PARTIAL_CONTENT: 206; + readonly HTTP_STATUS_MULTI_STATUS: 207; + readonly HTTP_STATUS_ALREADY_REPORTED: 208; + readonly HTTP_STATUS_IM_USED: 226; + readonly HTTP_STATUS_MULTIPLE_CHOICES: 300; + readonly HTTP_STATUS_MOVED_PERMANENTLY: 301; + readonly HTTP_STATUS_FOUND: 302; + readonly HTTP_STATUS_SEE_OTHER: 303; + readonly HTTP_STATUS_NOT_MODIFIED: 304; + readonly HTTP_STATUS_USE_PROXY: 305; + readonly HTTP_STATUS_TEMPORARY_REDIRECT: 307; + readonly HTTP_STATUS_PERMANENT_REDIRECT: 308; + readonly HTTP_STATUS_BAD_REQUEST: 400; + readonly HTTP_STATUS_UNAUTHORIZED: 401; + readonly HTTP_STATUS_PAYMENT_REQUIRED: 402; + readonly HTTP_STATUS_FORBIDDEN: 403; + readonly HTTP_STATUS_NOT_FOUND: 404; + readonly HTTP_STATUS_METHOD_NOT_ALLOWED: 405; + readonly HTTP_STATUS_NOT_ACCEPTABLE: 406; + readonly HTTP_STATUS_PROXY_AUTHENTICATION_REQUIRED: 407; + readonly HTTP_STATUS_REQUEST_TIMEOUT: 408; + readonly HTTP_STATUS_CONFLICT: 409; + readonly HTTP_STATUS_GONE: 410; + readonly HTTP_STATUS_LENGTH_REQUIRED: 411; + readonly HTTP_STATUS_PRECONDITION_FAILED: 412; + readonly HTTP_STATUS_PAYLOAD_TOO_LARGE: 413; + readonly HTTP_STATUS_URI_TOO_LONG: 414; + readonly HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE: 415; + readonly HTTP_STATUS_RANGE_NOT_SATISFIABLE: 416; + readonly HTTP_STATUS_EXPECTATION_FAILED: 417; + readonly HTTP_STATUS_TEAPOT: 418; + readonly HTTP_STATUS_MISDIRECTED_REQUEST: 421; + readonly HTTP_STATUS_UNPROCESSABLE_ENTITY: 422; + readonly HTTP_STATUS_LOCKED: 423; + readonly HTTP_STATUS_FAILED_DEPENDENCY: 424; + readonly HTTP_STATUS_TOO_EARLY: 425; + readonly HTTP_STATUS_UPGRADE_REQUIRED: 426; + readonly HTTP_STATUS_PRECONDITION_REQUIRED: 428; + readonly HTTP_STATUS_TOO_MANY_REQUESTS: 429; + readonly HTTP_STATUS_REQUEST_HEADER_FIELDS_TOO_LARGE: 431; + readonly HTTP_STATUS_UNAVAILABLE_FOR_LEGAL_REASONS: 451; + readonly HTTP_STATUS_INTERNAL_SERVER_ERROR: 500; + readonly HTTP_STATUS_NOT_IMPLEMENTED: 501; + readonly HTTP_STATUS_BAD_GATEWAY: 502; + readonly HTTP_STATUS_SERVICE_UNAVAILABLE: 503; + readonly HTTP_STATUS_GATEWAY_TIMEOUT: 504; + readonly HTTP_STATUS_HTTP_VERSION_NOT_SUPPORTED: 505; + readonly HTTP_STATUS_VARIANT_ALSO_NEGOTIATES: 506; + readonly HTTP_STATUS_INSUFFICIENT_STORAGE: 507; + readonly HTTP_STATUS_LOOP_DETECTED: 508; + readonly HTTP_STATUS_BANDWIDTH_LIMIT_EXCEEDED: 509; + readonly HTTP_STATUS_NOT_EXTENDED: 510; + readonly HTTP_STATUS_NETWORK_AUTHENTICATION_REQUIRED: 511; + } + export const constants: Http2Constants; + /* The eager handler is the first COMPAT 'request' listener — Node's + * route on either flavor (the allowHTTP1 server's HTTP/1.1 handles, + * the ALPN=h2 server's compat handles over streams). */ + export function createSecureServer( + options: SecureServerOptions, + onRequestHandler?: ( + req: import("http").IncomingMessage, + res: import("http").ServerResponse, + ) => void, + ): Http2SecureServer; + + /* ── the REAL h2c surface (scr_http2.c: frame codec + HPACK over the + * net loop) — createServer/connect and the session/stream handles the + * Node suite's http2 family exercises. Headers cross as the canonical + * header record (the IncomingHttpHeaders index-signature shape); the + * response :status reads back as a number. Members declared here + * without a lowering fence by name at their use sites. */ + + /** The http2 module's own header shapes: pure index-signature records + * (the HEADER-FAMILY canonicalization — string[] arm included). */ + export interface IncomingHttpHeaders { + [name: string]: number | string | string[] | undefined; + } + export interface OutgoingHttpHeaders { + [name: string]: number | string | string[] | undefined; + } + + export interface Http2Session { + close(callback?: () => void): void; + destroy(): void; + readonly closed: boolean; + readonly destroyed: boolean; + readonly encrypted: boolean; + readonly type: number; + readonly alpnProtocol: string; + readonly socket: import("net").Socket; + readonly pendingSettingsAck: boolean; + ref(): void; + unref(): void; + setTimeout(msecs: number, callback?: () => void): void; + ping(callback: (err: Error | null, duration: number, payload: Uint8Array) => void): boolean; + /* The settings surface rides the checked-dynamic machinery (records + * cross as dyn values — the suite's mustCall listeners are dyn). */ + settings(settings?: any, callback?: any): void; + readonly localSettings: any; + readonly remoteSettings: any; + goaway(code?: number, lastStreamID?: number, opaqueData?: Uint8Array): void; + on(event: "close" | "timeout", listener: () => void): void; + on(event: "error", listener: (err: Error) => void): void; + on( + event: "connect", + listener: (session: Http2Session, socket: import("net").Socket) => void, + ): void; + on( + event: "stream", + listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void, + ): void; + on( + event: "goaway", + listener: (errorCode: number, lastStreamID: number, opaqueData?: Uint8Array) => void, + ): void; + on(event: "localSettings" | "remoteSettings", listener: (settings: any) => void): void; + on(event: "frameError", listener: (type: number, code: number, id: number) => void): void; + on(event: "ping", listener: (payload: Uint8Array) => void): void; + once(event: "close" | "timeout", listener: () => void): void; + once(event: "error", listener: (err: Error) => void): void; + once( + event: "connect", + listener: (session: Http2Session, socket: import("net").Socket) => void, + ): void; + once( + event: "stream", + listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void, + ): void; + once( + event: "goaway", + listener: (errorCode: number, lastStreamID: number, opaqueData?: Uint8Array) => void, + ): void; + once(event: "localSettings" | "remoteSettings", listener: (settings: any) => void): void; + once(event: "frameError", listener: (type: number, code: number, id: number) => void): void; + once(event: "ping", listener: (payload: Uint8Array) => void): void; + addListener(event: "close" | "timeout", listener: () => void): void; + addListener(event: "error", listener: (err: Error) => void): void; + addListener( + event: "connect", + listener: (session: Http2Session, socket: import("net").Socket) => void, + ): void; + addListener( + event: "stream", + listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void, + ): void; + addListener( + event: "goaway", + listener: (errorCode: number, lastStreamID: number, opaqueData?: Uint8Array) => void, + ): void; + addListener(event: "localSettings" | "remoteSettings", listener: (settings: any) => void): void; + addListener( + event: "frameError", + listener: (type: number, code: number, id: number) => void, + ): void; + addListener(event: "ping", listener: (payload: Uint8Array) => void): void; + } + + export interface ClientHttp2Session extends Http2Session { + request( + headers?: OutgoingHttpHeaders, + options?: { + endStream?: boolean; + exclusive?: boolean; + parent?: number; + weight?: number; + waitForTrailers?: boolean; + }, + ): ClientHttp2Stream; + } + + export interface ServerHttp2Session extends Http2Session { + altsvc(alt: string, originOrStream: number | string): void; + origin(...origins: string[]): void; + } + + /** The shared Http2Stream core (both roles). 'data' chunks are Buffers + * (strings once setEncoding('utf8') is in force — the IncomingMessage + * chunk:any stance keeps both working). */ + interface Http2StreamCore { + readonly id: number | undefined; + readonly rstCode: number; + readonly closed: boolean; + readonly destroyed: boolean; + readonly pending: boolean; + readonly aborted: boolean; + readonly session: Http2Session; + readonly sentHeaders: OutgoingHttpHeaders; + readonly state: Record; + write(chunk: string | Uint8Array, callback?: (err?: Error | null) => void): boolean; + end(data?: string | Uint8Array, callback?: () => void): void; + end(callback: () => void): void; + close(code?: number, callback?: () => void): void; + destroy(error?: Error): void; + setEncoding(encoding: string): this; + resume(): void; + pause(): void; + setTimeout(msecs: number, callback?: () => void): void; + priority(options: Record): void; + sendTrailers(headers: OutgoingHttpHeaders): void; + } + + export interface ServerHttp2Stream extends Http2StreamCore { + on(event: "data", listener: (chunk: any) => void): void; + on( + event: + | "end" + | "close" + | "aborted" + | "ready" + | "timeout" + | "drain" + | "finish" + | "wantTrailers", + listener: () => void, + ): void; + on(event: "error", listener: (err: Error) => void): void; + on(event: "frameError", listener: (type: number, code: number, id: number) => void): void; + on(event: "trailers", listener: (trailers: IncomingHttpHeaders, flags: number) => void): void; + once(event: "data", listener: (chunk: any) => void): void; + once( + event: + | "end" + | "close" + | "aborted" + | "ready" + | "timeout" + | "drain" + | "finish" + | "wantTrailers", + listener: () => void, + ): void; + once(event: "error", listener: (err: Error) => void): void; + once(event: "frameError", listener: (type: number, code: number, id: number) => void): void; + once(event: "trailers", listener: (trailers: IncomingHttpHeaders, flags: number) => void): void; + addListener(event: "data", listener: (chunk: any) => void): void; + addListener( + event: + | "end" + | "close" + | "aborted" + | "ready" + | "timeout" + | "drain" + | "finish" + | "wantTrailers", + listener: () => void, + ): void; + addListener(event: "error", listener: (err: Error) => void): void; + addListener( + event: "frameError", + listener: (type: number, code: number, id: number) => void, + ): void; + addListener( + event: "trailers", + listener: (trailers: IncomingHttpHeaders, flags: number) => void, + ): void; + respond( + headers?: OutgoingHttpHeaders, + options?: { endStream?: boolean; waitForTrailers?: boolean }, + ): void; + respondWithFile( + path: string, + headers?: OutgoingHttpHeaders, + options?: Record, + ): void; + respondWithFD( + fd: number, + headers?: OutgoingHttpHeaders, + options?: Record, + ): void; + pushStream( + headers: OutgoingHttpHeaders, + callback: ( + err: Error | null, + pushStream: ServerHttp2Stream, + headers: OutgoingHttpHeaders, + ) => void, + ): void; + readonly headersSent: boolean; + readonly pushAllowed: boolean; + additionalHeaders(headers: OutgoingHttpHeaders): void; + } + + export interface ClientHttp2Stream extends Http2StreamCore { + on(event: "response", listener: (headers: IncomingHttpHeaders, flags: number) => void): void; + on(event: "push", listener: (headers: IncomingHttpHeaders, flags: number) => void): void; + on(event: "headers", listener: (headers: IncomingHttpHeaders, flags: number) => void): void; + on(event: "data", listener: (chunk: any) => void): void; + on( + event: + | "end" + | "close" + | "aborted" + | "ready" + | "timeout" + | "drain" + | "finish" + | "wantTrailers", + listener: () => void, + ): void; + on(event: "error", listener: (err: Error) => void): void; + on(event: "frameError", listener: (type: number, code: number, id: number) => void): void; + on(event: "trailers", listener: (trailers: IncomingHttpHeaders, flags: number) => void): void; + once(event: "response", listener: (headers: IncomingHttpHeaders, flags: number) => void): void; + once(event: "push", listener: (headers: IncomingHttpHeaders, flags: number) => void): void; + once(event: "headers", listener: (headers: IncomingHttpHeaders, flags: number) => void): void; + once(event: "data", listener: (chunk: any) => void): void; + once( + event: + | "end" + | "close" + | "aborted" + | "ready" + | "timeout" + | "drain" + | "finish" + | "wantTrailers", + listener: () => void, + ): void; + once(event: "error", listener: (err: Error) => void): void; + once(event: "frameError", listener: (type: number, code: number, id: number) => void): void; + once(event: "trailers", listener: (trailers: IncomingHttpHeaders, flags: number) => void): void; + addListener( + event: "response", + listener: (headers: IncomingHttpHeaders, flags: number) => void, + ): void; + addListener( + event: "push", + listener: (headers: IncomingHttpHeaders, flags: number) => void, + ): void; + addListener( + event: "headers", + listener: (headers: IncomingHttpHeaders, flags: number) => void, + ): void; + addListener(event: "data", listener: (chunk: any) => void): void; + addListener( + event: + | "end" + | "close" + | "aborted" + | "ready" + | "timeout" + | "drain" + | "finish" + | "wantTrailers", + listener: () => void, + ): void; + addListener(event: "error", listener: (err: Error) => void): void; + addListener( + event: "frameError", + listener: (type: number, code: number, id: number) => void, + ): void; + addListener( + event: "trailers", + listener: (trailers: IncomingHttpHeaders, flags: number) => void, + ): void; + } + + export interface Http2Server { + listen(port?: number, callback?: () => void): void; + listen(port: number, host: string, callback?: () => void): void; + close(callback?: () => void): void; + address(): import("node:dgram").AddressInfo; + setTimeout(msecs?: number, callback?: () => void): void; + ref(): void; + unref(): void; + on( + event: "stream", + listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void, + ): void; + on(event: "session", listener: (session: ServerHttp2Session) => void): void; + on( + event: "request", + listener: (req: Http2ServerRequest, res: Http2ServerResponse) => void, + ): void; + on(event: "error", listener: (err: Error) => void): void; + on(event: "close" | "listening" | "timeout", listener: () => void): void; + on(event: "connection", listener: (socket: import("net").Socket) => void): void; + on(event: "sessionError", listener: (err: Error, session: ServerHttp2Session) => void): void; + once( + event: "stream", + listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void, + ): void; + once(event: "session", listener: (session: ServerHttp2Session) => void): void; + once( + event: "request", + listener: (req: Http2ServerRequest, res: Http2ServerResponse) => void, + ): void; + once(event: "error", listener: (err: Error) => void): void; + once(event: "close" | "listening" | "timeout", listener: () => void): void; + once(event: "connection", listener: (socket: import("net").Socket) => void): void; + once(event: "sessionError", listener: (err: Error, session: ServerHttp2Session) => void): void; + addListener( + event: "stream", + listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void, + ): void; + addListener(event: "session", listener: (session: ServerHttp2Session) => void): void; + addListener( + event: "request", + listener: (req: Http2ServerRequest, res: Http2ServerResponse) => void, + ): void; + addListener(event: "error", listener: (err: Error) => void): void; + addListener(event: "close" | "listening" | "timeout", listener: () => void): void; + addListener(event: "connection", listener: (socket: import("net").Socket) => void): void; + addListener( + event: "sessionError", + listener: (err: Error, session: ServerHttp2Session) => void, + ): void; + } + + export interface ServerOptions { + [option: string]: unknown; + } + + export function createServer( + onRequestHandler?: (req: Http2ServerRequest, res: Http2ServerResponse) => void, + ): Http2Server; + export function createServer( + options: ServerOptions, + onRequestHandler?: (req: Http2ServerRequest, res: Http2ServerResponse) => void, + ): Http2Server; + + export function connect( + authority: string, + listener?: (session: ClientHttp2Session, socket: import("net").Socket) => void, + ): ClientHttp2Session; + export function connect( + authority: string, + options: Record, + listener?: (session: ClientHttp2Session, socket: import("net").Socket) => void, + ): ClientHttp2Session; + + export function getDefaultSettings(): any; + export function getPackedSettings(settings: Record): Uint8Array; + export const sensitiveHeaders: unique symbol; +} + +declare module "node:http2" { + export * from "http2"; +} diff --git a/packages/compiler/ambient/node-fallback/https.d.ts b/packages/compiler/ambient/node-fallback/https.d.ts new file mode 100644 index 000000000..a3171f401 --- /dev/null +++ b/packages/compiler/ambient/node-fallback/https.d.ts @@ -0,0 +1,59 @@ +/// + +/* node:https — http over the TLS transport (scr_tls.c). + * createServer({ cert, key }, handler) is the http server behind a + * handshake; request/get are the http client with port 443 default and + * the local-CA knobs: `ca` (PEM string/Buffer) replaces the trust + * anchors, rejectUnauthorized: false disables verification (the + * self-signed probe shape). With neither, /etc/ssl/cert.pem stands in + * for Node's bundled roots. */ +declare module "https" { + import { Server } from "http"; + import { Agent, ClientRequest, IncomingMessage, ServerResponse } from "http"; + export { Agent, Server }; + /* Node's keepAlive global agent for requests without an agent option. */ + export const globalAgent: Agent; + export interface ServerOptions { + cert: string | Uint8Array; + key: string | Uint8Array; + /* The options-record stance (see http.RequestOptions). */ + [option: string]: unknown; + } + export interface RequestOptions { + hostname?: string; + host?: string; + port?: number; + path?: string; + method?: string; + timeout?: number; + headers?: { [name: string]: string }; + rejectUnauthorized?: boolean; + ca?: string | Uint8Array; + agent?: Agent | boolean | null; + /* Aborting destroys the request with an AbortError. */ + signal?: AbortSignal; + socketPath?: string; + /* The options-record stance (see http.RequestOptions). */ + [option: string]: unknown; + } + export function createServer( + options: ServerOptions, + requestListener: (req: IncomingMessage, res: ServerResponse) => void, + ): Server; + /* One signature with a union first parameter, for the request-fn + * ternary's sake (see the http block). A URL target takes no options, + * which means Node's defaults — the certificate is verified against + * the default trust anchors. */ + export function request( + target: RequestOptions | string | URL, + callback?: (res: IncomingMessage) => void, + ): ClientRequest; + export function get( + target: RequestOptions | string | URL, + callback?: (res: IncomingMessage) => void, + ): ClientRequest; +} + +declare module "node:https" { + export * from "https"; +} diff --git a/packages/compiler/ambient/node-fallback/module.d.ts b/packages/compiler/ambient/node-fallback/module.d.ts new file mode 100644 index 000000000..0ea020456 --- /dev/null +++ b/packages/compiler/ambient/node-fallback/module.d.ts @@ -0,0 +1,10 @@ +declare module "node:module" { + export function createRequire(filename: string | URL): ScriptcRequire; + export const builtinModules: string[]; + export function isBuiltin(moduleName: string): boolean; + export function syncBuiltinESMExports(): void; +} + +declare module "module" { + export * from "node:module"; +} diff --git a/packages/compiler/ambient/node-fallback/net.d.ts b/packages/compiler/ambient/node-fallback/net.d.ts new file mode 100644 index 000000000..806039fb5 --- /dev/null +++ b/packages/compiler/ambient/node-fallback/net.d.ts @@ -0,0 +1,374 @@ +/// +/// +/// +/// +/// + +/* node:net — the inbound-networking slice (scr_net.c links only into + * net-using binaries). TCP only, Node's shapes narrowed to what lowers: + * createServer with an optional connection handler; listen(port[, cb]) + * 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/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'. */ +declare module "net" { + export interface Socket { + /* Whether the socket keeps the event loop alive (ref'd by default). */ + ref(): void; + unref(): void; + readonly remoteAddress: string | undefined; + /* The peer's fields are cached at first read, like remoteAddress; the + * local ones are undefined while connecting and after close. */ + readonly remotePort: number | undefined; + readonly remoteFamily: string | undefined; + readonly localAddress: string | undefined; + readonly localPort: number | undefined; + readonly localFamily: string | undefined; + readonly bytesRead: number; + readonly connecting: boolean; + readonly pending: boolean; + readonly readyState: "opening" | "open" | "readOnly" | "writeOnly" | "closed"; + /* true once the fd is gone (destroy() or full close) — Node's flag. */ + readonly destroyed: boolean; + /* the write half is open: no end() yet, no FIN sent, fd alive. */ + readonly writable: boolean; + /* true until the read half is done (peer FIN / destroy). */ + readonly readable: boolean; + /* every byte the write paths accepted (buffered included — Node + * counts those too; plaintext on TLS sockets). */ + readonly bytesWritten: number; + /* write's callback runs once the data is flushed, with null; end's + * once the socket finishes. */ + /* 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; + /* Flow control: pause holds reads off (kernel/TCP backpressure is + * the buffer); resume flows — and discards without listeners, so + * 'end' stays reachable. Both chain, Node's shape. */ + pause(): Socket; + resume(): Socket; + /* TCP_NODELAY on the live fd; missing means true. Chains. */ + setNoDelay(noDelay?: boolean): Socket; + /* SO_KEEPALIVE with the initial delay in ms. Chains. */ + setKeepAlive(enable?: boolean, initialDelay?: number): Socket; + /* end() now, destroy once the FIN actually flushed. */ + destroySoon(): void; + /* An RST instead of a FIN; a connecting socket resets once it + * connects. Throws ERR_INVALID_HANDLE_TYPE on a TLS socket. */ + resetAndDestroy(): Socket; + /* The local address, or {} while connecting and once destroyed. */ + address(): AddressInfo | {}; + /* setEncoding('utf8'): 'data' delivers strings — the IncomingMessage + * twin's contract. */ + setEncoding(encoding: string): void; + /* socket→socket, and socket→response (the extended-CONNECT bridge + * leg: raw chunks become response body writes; EOF ends it). */ + pipe( + destination: Socket | import("http").ServerResponse | import("http2").Http2ServerResponse, + ): void; + /* The callback is a once('timeout') listener (removed again by 0). */ + setTimeout(ms: number, callback?: () => void): void; + /* The paused-mode demux surface: once('readable') + read(1) + + * unshift — read answers exactly n buffered bytes or null. */ + read(size?: number): Buffer | null; + unshift(chunk: Uint8Array): void; + /* secureConnect/secure/session are TLS-socket events declared on the + * shared socket surface (the lowering collapses TLSSocket onto this kind): + * the runtime gates on the transport, so a plain socket's + * registration never fires — Node's exact split. 'session' fires + * once with the serialized session (the received-ticket event). */ + on(event: "data", listener: (chunk: Buffer) => void): void; + /* After setEncoding the chunk is a string. */ + on(event: "data", listener: (chunk: string) => void): void; + on( + event: + | "end" + | "connect" + | "ready" + | "timeout" + | "drain" + | "readable" + | "finish" + | "secureConnect" + | "secure", + listener: () => void, + ): void; + on(event: "close", listener: (hadError: boolean) => void): void; + on(event: "error", listener: (err: Error) => void): void; + on(event: "session", listener: (session: Buffer) => void): void; + /* The host name's resolution, before the connect. */ + on( + event: "lookup", + listener: (err: Error | null, address: string, family: number, host: string) => void, + ): void; + /* addListener IS on (Node aliases them) — the suite spells both. */ + addListener(event: "data", listener: (chunk: Buffer) => void): void; + /* After setEncoding the chunk is a string. */ + addListener(event: "data", listener: (chunk: string) => void): void; + addListener( + event: + | "end" + | "connect" + | "ready" + | "timeout" + | "drain" + | "readable" + | "finish" + | "secureConnect" + | "secure", + listener: () => void, + ): void; + addListener(event: "close", listener: (hadError: boolean) => void): void; + addListener(event: "error", listener: (err: Error) => void): void; + addListener(event: "session", listener: (session: Buffer) => void): void; + /* The host name's resolution, before the connect. */ + addListener( + event: "lookup", + listener: (err: Error | null, address: string, family: number, host: string) => void, + ): void; + once(event: "data", listener: (chunk: Buffer) => void): void; + /* After setEncoding the chunk is a string. */ + once(event: "data", listener: (chunk: string) => void): void; + once( + event: + | "end" + | "connect" + | "ready" + | "timeout" + | "drain" + | "readable" + | "finish" + | "secureConnect" + | "secure", + listener: () => void, + ): void; + once(event: "close", listener: (hadError: boolean) => void): void; + once(event: "error", listener: (err: Error) => void): void; + once(event: "session", listener: (session: Buffer) => void): void; + /* The host name's resolution, before the connect. */ + once( + event: "lookup", + listener: (err: Error | null, address: string, family: number, host: string) => void, + ): void; + } + export interface Server { + /* Whether the server keeps the event loop alive (ref'd by default). */ + ref(): void; + unref(): void; + readonly listening: boolean; + /* Connections past this many are closed with a 'drop' event; + * undefined (the default) or null lifts the cap. */ + maxConnections: number | undefined; + /* Node reads it only in cluster mode: a stored value here. */ + dropMaxConnection: boolean | undefined; + /* cb(null, count) on the next tick. */ + getConnections(callback: (error: Error | null, count: number) => void): Server; + /* Node answers the server itself (`return this` chaining). */ + listen(port: number, callback?: () => void): Server; + /* The positional bind address (the options form's host in argument + * spelling). */ + listen(port: number, host: string, callback?: () => void): Server; + /* A Unix-socket server; the socket file is removed when it closes. */ + listen(path: string, callback?: () => void): Server; + /* The explicit-interface bind: host is an IP literal (absent = the + * host-less dual-stack any); ipv6Only sets IPV6_V6ONLY and reusePort + * requests kernel connection distribution where Node supports it. */ + listen( + options: { port: number; host?: string; ipv6Only?: boolean; reusePort?: boolean }, + callback?: () => void, + ): Server; + close(callback?: () => void): void; + /* The bound AddressInfo (Node answers null before listen — this + * surface answers the record with port 0 there, the serverPort + * stance). */ + address(): import("node:dgram").AddressInfo; + /* The demux route: hand an accepted socket to another server. */ + emit(event: "connection", socket: Socket): boolean; + on(event: "connection" | "secureConnection", listener: (socket: Socket) => void): void; + /* tls and https servers: a handshake that failed */ + on( + event: "tlsClientError", + listener: (err: Error, tlsSocket: import("tls").TLSSocket) => void, + ): void; + on(event: "close" | "listening", listener: () => void): void; + on(event: "drop", listener: (data?: DropArgument) => void): void; + on(event: "timeout", listener: (socket: Socket) => void): void; + on(event: "error", listener: (err: Error) => void): void; + /* The WebSocket handover: fires INSTEAD of 'request' for + * Connection: upgrade requests, with the raw socket + head bytes; + * 'connect' is its twin for CONNECT requests (HTTP tunneling). */ + on( + event: "upgrade" | "connect", + listener: (req: import("http").IncomingMessage, socket: Socket, head: Buffer) => void, + ): void; + /* http.createServer(handler)'s event twin (a no-parser net server + * never fires it, like Node). */ + on( + event: "request" | "checkContinue" | "checkExpectation", + listener: (req: import("http").IncomingMessage, res: import("http").ServerResponse) => void, + ): void; + /* addListener IS on (Node aliases them) — the suite spells both. */ + addListener(event: "connection" | "secureConnection", listener: (socket: Socket) => void): void; + /* tls and https servers: a handshake that failed */ + addListener( + event: "tlsClientError", + listener: (err: Error, tlsSocket: import("tls").TLSSocket) => void, + ): void; + addListener(event: "close" | "listening", listener: () => void): void; + addListener(event: "drop", listener: (data?: DropArgument) => void): void; + addListener(event: "timeout", listener: (socket: Socket) => void): void; + addListener(event: "error", listener: (err: Error) => void): void; + addListener( + event: "upgrade" | "connect", + listener: (req: import("http").IncomingMessage, socket: Socket, head: Buffer) => void, + ): void; + addListener( + event: "request" | "checkContinue" | "checkExpectation", + listener: (req: import("http").IncomingMessage, res: import("http").ServerResponse) => void, + ): void; + once(event: "connection" | "secureConnection", listener: (socket: Socket) => void): void; + /* tls and https servers: a handshake that failed */ + once( + event: "tlsClientError", + listener: (err: Error, tlsSocket: import("tls").TLSSocket) => void, + ): void; + once(event: "close" | "listening", listener: () => void): void; + once(event: "drop", listener: (data?: DropArgument) => void): void; + once(event: "timeout", listener: (socket: Socket) => void): void; + once(event: "error", listener: (err: Error) => void): void; + once( + event: "upgrade" | "connect", + listener: (req: import("http").IncomingMessage, socket: Socket, head: Buffer) => void, + ): void; + once( + event: "request" | "checkContinue" | "checkExpectation", + listener: (req: import("http").IncomingMessage, res: import("http").ServerResponse) => void, + ): void; + } + /* The 'drop' payload: the turned-away connection's addresses. */ + export interface DropArgument { + localAddress?: string; + localPort?: number; + localFamily?: string; + remoteAddress?: string; + remotePort?: number; + remoteFamily?: string; + } + export type AddressInfo = import("node:dgram").AddressInfo; + export type IPVersion = "ipv4" | "ipv6"; + export interface SocketAddressInitOptions { + address?: string; + family?: IPVersion; + flowlabel?: number; + port?: number; + } + /* A native handle (scr_net.c); the getters read its parsed address. */ + export class SocketAddress { + constructor(options?: SocketAddressInitOptions); + readonly address: string; + readonly family: IPVersion; + readonly port: number; + readonly flowlabel: number; + toJSON(): { address: string; port: number; family: IPVersion; flowlabel: number }; + /* undefined when the input is not "[:]" */ + static parse(input: string): SocketAddress | undefined; + static isSocketAddress(value: unknown): value is SocketAddress; + } + /* Address, range and subnet rules; check() matches IPv4 addresses + * against IPv6 rules through their IPv4-mapped form, as Node does. */ + export class BlockList { + constructor(); + readonly rules: readonly string[]; + readonly size: number; + addAddress(address: string | SocketAddress, type?: IPVersion): void; + addAddresses(addresses: readonly (string | SocketAddress)[], type?: IPVersion): void; + addRange(start: string | SocketAddress, end: string | SocketAddress, type?: IPVersion): void; + addSubnet(net: string | SocketAddress, prefix: number, type?: IPVersion): void; + addCIDR(cidr: string): void; + addCIDRs(cidrs: readonly string[]): void; + removeAddress(address: string | SocketAddress, type?: IPVersion): void; + removeRange(start: string | SocketAddress, end: string | SocketAddress, type?: IPVersion): void; + removeSubnet(net: string | SocketAddress, prefix: number, type?: IPVersion): void; + removeCIDR(cidr: string): void; + clear(): void; + check(address: string | SocketAddress, type?: IPVersion): boolean; + toJSON(): string[]; + fromJSON(data: string | readonly string[]): void; + static isBlockList(value: unknown): value is BlockList; + static readonly PRIVATE_RANGES: readonly string[]; + } + /* Node's IPv4Reg and IPv6Reg tests: 4, 6, or 0 for isIP. */ + export function isIP(input: string): number; + export function isIPv4(input: string): boolean; + export function isIPv6(input: string): boolean; + /* The Socket VALUE — the `x instanceof net.Socket` narrowing target + * (the h2 compat 'connect' listener's test). Constructing bare sockets + * has no lowering; the construct signature exists for instanceof. */ + export const Socket: abstract new () => Socket; + export function createServer(connectionListener?: (socket: Socket) => void): Server; + export function connect(port: number, host?: string, connectListener?: () => void): Socket; + export function createConnection( + port: number, + host?: string, + connectListener?: () => void, + ): Socket; + /* The caller-resolver dial (@types/node's net.LookupFunction): the + * runtime invokes it as Node's autoSelectFamily dial does (options + * { hints, all: true }, so the callback answers an address list) and + * dials the answered addresses in order. */ + export type LookupFunction = ( + hostname: string, + options: import("node:dns").LookupOptions, + callback: ( + err: NodeJS.ErrnoException | null, + address: string | import("node:dns").LookupAddress[], + family?: number, + ) => void, + ) => void; + export function connect( + options: { + port: number; + host?: string; + autoSelectFamily?: boolean; + lookup?: LookupFunction; + }, + connectListener?: () => void, + ): Socket; + export function createConnection( + options: { + port: number; + host?: string; + autoSelectFamily?: boolean; + lookup?: LookupFunction; + }, + connectListener?: () => void, + ): Socket; + /* A Unix domain socket client (Node's IpcNetConnectOpts and path + * overloads). */ + export function connect(path: string, connectListener?: () => void): Socket; + export function connect(options: { path: string }, connectListener?: () => void): Socket; + export function createConnection(path: string, connectListener?: () => void): Socket; + export function createConnection(options: { path: string }, connectListener?: () => void): Socket; + /* The happy-eyeballs attempt budget (Node's process-wide knob; the + * autoSelectFamily dial consults it): one runtime int, default 250ms + * like Node's. */ + export function getDefaultAutoSelectFamilyAttemptTimeout(): number; + export function setDefaultAutoSelectFamilyAttemptTimeout(value: number): void; +} + +declare module "node:net" { + export * from "net"; +} diff --git a/packages/compiler/ambient/node-fallback/os.d.ts b/packages/compiler/ambient/node-fallback/os.d.ts new file mode 100644 index 000000000..d58592df5 --- /dev/null +++ b/packages/compiler/ambient/node-fallback/os.d.ts @@ -0,0 +1,288 @@ +/* node:os — the slice a CLI needs: platform() (same implementation as + * process.platform), homedir(), tmpdir() (Node's env cascade), EOL + * (always "\n" — POSIX pinned, like path.sep), and networkInterfaces() + * (getifaddrs(3); the interface shapes mirror @types/node's exactly so + * both type worlds map to the same IR structure). */ +declare module "os" { + export function platform(): string; + export function homedir(): string; + export function tmpdir(): string; + /* uname(2)'s release field — Node's own implementation. */ + export function release(): string; + /* uname(2)'s sysname field ("Darwin", "Linux") — Node's os.type(). */ + export function type(): string; + /* Total system memory in bytes (sysctl hw.memsize / sysconf). */ + export function totalmem(): number; + export function availableParallelism(): number; + /* The passwd-entry snapshot (uv_os_get_passwd): shell is `string | + * null` to match @types/node (POSIX always answers the string arm); + * homedir is pw_dir — NOT os.homedir()'s $HOME-first cascade. The type + * is interned explicitly in type-mapper.ts (the AddressInfo pattern — this + * concrete form and @types/node's UserInfo both), and the call + * assembles field-by-field in lowerOsUserInfoCall. */ + export interface UserInfo { + username: string; + uid: number; + gid: number; + shell: string | null; + homedir: string; + } + export function userInfo(): UserInfo; + /* The TARGET platform's line terminator (a compile-time constant: + * "\r\n" under a win32 triple, "\n" everywhere else). */ + export const EOL: "\r\n" | "\n"; + export interface NetworkInterfaceBase { + address: string; + netmask: string; + mac: string; + internal: boolean; + cidr: string | null; + scopeid?: number; + } + export interface NetworkInterfaceInfoIPv4 extends NetworkInterfaceBase { + family: "IPv4"; + } + export interface NetworkInterfaceInfoIPv6 extends NetworkInterfaceBase { + family: "IPv6"; + scopeid: number; + } + export type NetworkInterfaceInfo = NetworkInterfaceInfoIPv4 | NetworkInterfaceInfoIPv6; + export function networkInterfaces(): { [name: string]: NetworkInterfaceInfo[] | undefined }; + /* Host facts, libuv's answers: model/speed/times per logical CPU, the + * host name, the 1/5/15-minute load averages, uptime in seconds, free + * memory in bytes, process.arch, uname's machine and version fields, + * the byte order, and a process's scheduling priority (an omitted pid + * is the current process). Shapes mirror @types/node's. */ + export interface CpuInfo { + model: string; + speed: number; + times: { + user: number; + nice: number; + sys: number; + idle: number; + irq: number; + }; + } + export function cpus(): CpuInfo[]; + export function hostname(): string; + export function loadavg(): number[]; + export function uptime(): number; + export function freemem(): number; + export function arch(): string; + export function machine(): string; + export function version(): string; + export function endianness(): "BE" | "LE"; + export function getPriority(pid?: number): number; + export function setPriority(priority: number): void; + export function setPriority(pid: number, priority: number): void; + /* The TARGET platform's null device ("\\\\.\\nul" under a win32 triple). */ + export const devNull: string; + /* The platform's constants: null-prototype objects with read-only + * members, `signals` frozen (Node's internalBinding('constants').os). */ + export type SignalConstants = { + [ + key in + | "SIGABRT" + | "SIGALRM" + | "SIGBUS" + | "SIGCHLD" + | "SIGCONT" + | "SIGFPE" + | "SIGHUP" + | "SIGILL" + | "SIGINT" + | "SIGIO" + | "SIGIOT" + | "SIGKILL" + | "SIGPIPE" + | "SIGPOLL" + | "SIGPROF" + | "SIGPWR" + | "SIGQUIT" + | "SIGSEGV" + | "SIGSTKFLT" + | "SIGSTOP" + | "SIGSYS" + | "SIGTERM" + | "SIGTRAP" + | "SIGTSTP" + | "SIGTTIN" + | "SIGTTOU" + | "SIGUNUSED" + | "SIGURG" + | "SIGUSR1" + | "SIGUSR2" + | "SIGVTALRM" + | "SIGWINCH" + | "SIGXCPU" + | "SIGXFSZ" + | "SIGBREAK" + | "SIGLOST" + | "SIGINFO" + ]: number; + }; + export namespace constants { + const UV_UDP_REUSEADDR: number; + namespace signals {} + const signals: SignalConstants; + namespace errno { + const E2BIG: number; + const EACCES: number; + const EADDRINUSE: number; + const EADDRNOTAVAIL: number; + const EAFNOSUPPORT: number; + const EAGAIN: number; + const EALREADY: number; + const EBADF: number; + const EBADMSG: number; + const EBUSY: number; + const ECANCELED: number; + const ECHILD: number; + const ECONNABORTED: number; + const ECONNREFUSED: number; + const ECONNRESET: number; + const EDEADLK: number; + const EDESTADDRREQ: number; + const EDOM: number; + const EDQUOT: number; + const EEXIST: number; + const EFAULT: number; + const EFBIG: number; + const EHOSTUNREACH: number; + const EIDRM: number; + const EILSEQ: number; + const EINPROGRESS: number; + const EINTR: number; + const EINVAL: number; + const EIO: number; + const EISCONN: number; + const EISDIR: number; + const ELOOP: number; + const EMFILE: number; + const EMLINK: number; + const EMSGSIZE: number; + const EMULTIHOP: number; + const ENAMETOOLONG: number; + const ENETDOWN: number; + const ENETRESET: number; + const ENETUNREACH: number; + const ENFILE: number; + const ENOBUFS: number; + const ENODATA: number; + const ENODEV: number; + const ENOENT: number; + const ENOEXEC: number; + const ENOLCK: number; + const ENOLINK: number; + const ENOMEM: number; + const ENOMSG: number; + const ENOPROTOOPT: number; + const ENOSPC: number; + const ENOSR: number; + const ENOSTR: number; + const ENOSYS: number; + const ENOTCONN: number; + const ENOTDIR: number; + const ENOTEMPTY: number; + const ENOTSOCK: number; + const ENOTSUP: number; + const ENOTTY: number; + const ENXIO: number; + const EOPNOTSUPP: number; + const EOVERFLOW: number; + const EPERM: number; + const EPIPE: number; + const EPROTO: number; + const EPROTONOSUPPORT: number; + const EPROTOTYPE: number; + const ERANGE: number; + const EROFS: number; + const ESPIPE: number; + const ESRCH: number; + const ESTALE: number; + const ETIME: number; + const ETIMEDOUT: number; + const ETXTBSY: number; + const EWOULDBLOCK: number; + const EXDEV: number; + const WSAEINTR: number; + const WSAEBADF: number; + const WSAEACCES: number; + const WSAEFAULT: number; + const WSAEINVAL: number; + const WSAEMFILE: number; + const WSAEWOULDBLOCK: number; + const WSAEINPROGRESS: number; + const WSAEALREADY: number; + const WSAENOTSOCK: number; + const WSAEDESTADDRREQ: number; + const WSAEMSGSIZE: number; + const WSAEPROTOTYPE: number; + const WSAENOPROTOOPT: number; + const WSAEPROTONOSUPPORT: number; + const WSAESOCKTNOSUPPORT: number; + const WSAEOPNOTSUPP: number; + const WSAEPFNOSUPPORT: number; + const WSAEAFNOSUPPORT: number; + const WSAEADDRINUSE: number; + const WSAEADDRNOTAVAIL: number; + const WSAENETDOWN: number; + const WSAENETUNREACH: number; + const WSAENETRESET: number; + const WSAECONNABORTED: number; + const WSAECONNRESET: number; + const WSAENOBUFS: number; + const WSAEISCONN: number; + const WSAENOTCONN: number; + const WSAESHUTDOWN: number; + const WSAETOOMANYREFS: number; + const WSAETIMEDOUT: number; + const WSAECONNREFUSED: number; + const WSAELOOP: number; + const WSAENAMETOOLONG: number; + const WSAEHOSTDOWN: number; + const WSAEHOSTUNREACH: number; + const WSAENOTEMPTY: number; + const WSAEPROCLIM: number; + const WSAEUSERS: number; + const WSAEDQUOT: number; + const WSAESTALE: number; + const WSAEREMOTE: number; + const WSASYSNOTREADY: number; + const WSAVERNOTSUPPORTED: number; + const WSANOTINITIALISED: number; + const WSAEDISCON: number; + const WSAENOMORE: number; + const WSAECANCELLED: number; + const WSAEINVALIDPROCTABLE: number; + const WSAEINVALIDPROVIDER: number; + const WSAEPROVIDERFAILEDINIT: number; + const WSASYSCALLFAILURE: number; + const WSASERVICE_NOT_FOUND: number; + const WSATYPE_NOT_FOUND: number; + const WSA_E_NO_MORE: number; + const WSA_E_CANCELLED: number; + const WSAEREFUSED: number; + } + namespace dlopen { + const RTLD_LAZY: number; + const RTLD_NOW: number; + const RTLD_GLOBAL: number; + const RTLD_LOCAL: number; + const RTLD_DEEPBIND: number; + } + namespace priority { + const PRIORITY_LOW: number; + const PRIORITY_BELOW_NORMAL: number; + const PRIORITY_NORMAL: number; + const PRIORITY_ABOVE_NORMAL: number; + const PRIORITY_HIGH: number; + const PRIORITY_HIGHEST: number; + } + } +} + +declare module "node:os" { + export * from "os"; +} diff --git a/packages/compiler/ambient/node-fallback/path-posix.d.ts b/packages/compiler/ambient/node-fallback/path-posix.d.ts new file mode 100644 index 000000000..66db6a876 --- /dev/null +++ b/packages/compiler/ambient/node-fallback/path-posix.d.ts @@ -0,0 +1,22 @@ +/// + +declare module "path/posix" { + export function join(...paths: string[]): string; + export function resolve(...paths: string[]): string; + export function normalize(path: string): string; + export function dirname(path: string): string; + export function basename(path: string, suffix?: string): string; + export function extname(path: string): string; + export function isAbsolute(path: string): boolean; + export function relative(from: string, to: string): string; + export function toNamespacedPath(path: string): string; + export function matchesGlob(path: string, pattern: string): boolean; + export function parse(path: string): import("path").ParsedPath; + export function format(pathObject: import("path").FormatInputPathObject): string; + export const sep: "/"; + export const delimiter: ":"; +} + +declare module "node:path/posix" { + export * from "path/posix"; +} diff --git a/packages/compiler/ambient/node-fallback/path-win32.d.ts b/packages/compiler/ambient/node-fallback/path-win32.d.ts new file mode 100644 index 000000000..b889a34e0 --- /dev/null +++ b/packages/compiler/ambient/node-fallback/path-win32.d.ts @@ -0,0 +1,22 @@ +/// + +declare module "path/win32" { + export function join(...paths: string[]): string; + export function resolve(...paths: string[]): string; + export function normalize(path: string): string; + export function dirname(path: string): string; + export function basename(path: string, suffix?: string): string; + export function extname(path: string): string; + export function isAbsolute(path: string): boolean; + export function relative(from: string, to: string): string; + export function toNamespacedPath(path: string): string; + export function matchesGlob(path: string, pattern: string): boolean; + export function parse(path: string): import("path").ParsedPath; + export function format(pathObject: import("path").FormatInputPathObject): string; + export const sep: "\\"; + export const delimiter: ";"; +} + +declare module "node:path/win32" { + export * from "path/win32"; +} diff --git a/packages/compiler/ambient/node-fallback/path.d.ts b/packages/compiler/ambient/node-fallback/path.d.ts new file mode 100644 index 000000000..a57e40bb7 --- /dev/null +++ b/packages/compiler/ambient/node-fallback/path.d.ts @@ -0,0 +1,55 @@ +/// +/// + +/* node:path — the TARGET platform's implementation (Node's rule): posix + * on posix triples, path.win32 under a win32 triple, with the posix and + * win32 namespaces answering their own platform anywhere. Every function + * is a pure string algorithm matching Node's implementations + * exactly; join/resolve are variadic like Node's. */ +declare module "path" { + export function join(...paths: string[]): string; + export function resolve(...paths: string[]): string; + export function normalize(path: string): string; + export function dirname(path: string): string; + export function basename(path: string, suffix?: string): string; + export function extname(path: string): string; + export function isAbsolute(path: string): boolean; + export function relative(from: string, to: string): string; + export function toNamespacedPath(path: string): string; + /* matchesGlob() is Node's glob matcher: brace expansion, extglobs, POSIX + * classes and the ** and dot rules. */ + export function matchesGlob(path: string, pattern: string): boolean; + /* parse() splits a path into its root, directory, base, extension and + * name; format() joins those fields back (Node's `||` defaults). */ + export interface ParsedPath { + root: string; + dir: string; + base: string; + ext: string; + name: string; + } + export interface FormatInputPathObject { + root?: string | undefined; + dir?: string | undefined; + base?: string | undefined; + ext?: string | undefined; + name?: string | undefined; + } + export function parse(path: string): ParsedPath; + export function format(pathObject: FormatInputPathObject): string; + /* The union @types/node declares: the bare module answers the TARGET + * platform's value (a compile-time constant — "\\"/";" under a win32 + * triple, "/"/":" everywhere else). */ + export const sep: "\\" | "/"; + export const delimiter: ";" | ":"; + /* The platform-specific namespaces (real Node submodules too — + * "node:path/posix" resolves). Each carries the FULL surface and + * answers ITS platform's rules on any target, Node's own contract; + * the bare module IS the target platform's implementation. */ + export const posix: typeof import("path/posix"); + export const win32: typeof import("path/win32"); +} + +declare module "node:path" { + export * from "path"; +} diff --git a/packages/compiler/ambient/node-fallback/process.d.ts b/packages/compiler/ambient/node-fallback/process.d.ts new file mode 100644 index 000000000..7782a1b2d --- /dev/null +++ b/packages/compiler/ambient/node-fallback/process.d.ts @@ -0,0 +1,11 @@ +/* Node's process module default export is the global process object. These + * declaration aliases let the ESM spelling typecheck against the same + * deliberately narrow fallback surface when @types/node is absent. */ +declare module "process" { + export = process; +} + +declare module "node:process" { + import process = require("process"); + export = process; +} diff --git a/packages/compiler/ambient/node-fallback/punycode.d.ts b/packages/compiler/ambient/node-fallback/punycode.d.ts new file mode 100644 index 000000000..fc2725c5d --- /dev/null +++ b/packages/compiler/ambient/node-fallback/punycode.d.ts @@ -0,0 +1,18 @@ +/* node:punycode (deprecated, DEP0040) — punycode.js 2.1.0: decode and + * toUnicode throw its RangeErrors on malformed input; ucs2 converts + * between strings and UTF-16 code point arrays. */ +declare module "punycode" { + export function decode(string: string): string; + export function encode(string: string): string; + export function toUnicode(domain: string): string; + export function toASCII(domain: string): string; + export namespace ucs2 { + function decode(string: string): number[]; + function encode(codePoints: readonly number[]): string; + } + export const version: string; +} + +declare module "node:punycode" { + export * from "punycode"; +} diff --git a/packages/compiler/ambient/node-fallback/querystring.d.ts b/packages/compiler/ambient/node-fallback/querystring.d.ts new file mode 100644 index 000000000..4ee4346bb --- /dev/null +++ b/packages/compiler/ambient/node-fallback/querystring.d.ts @@ -0,0 +1,55 @@ +/* node:querystring — Node's legacy query-string codec (NOT + * URLSearchParams: '+' means space on the parse side and escape encodes + * spaces as %20). parse answers the null-prototype dictionary as a pure + * index-signature record (repeated keys become string[] buckets; + * @types/node's Dict adds an undefined arm the lowering tolerates); + * stringify serializes string/number/boolean values and arrays of those + * (Node's rules: arrays expand to repeated keys, null/undefined and + * anything else serialize as the empty value). The maxKeys option lowers + * (0 removes the cap, Node's rule); the custom encoder/decoder options + * typecheck under the options-record stance and fence by name at the + * call. decode/encode are Node's own aliases of parse/stringify. */ +declare module "querystring" { + export interface ParseOptions { + maxKeys?: number; + decodeURIComponent?: (str: string) => string; + [option: string]: unknown; + } + export interface StringifyOptions { + encodeURIComponent?: (str: string) => string; + [option: string]: unknown; + } + export interface ParsedUrlQuery { + [key: string]: string | string[] | undefined; + } + export interface ParsedUrlQueryInput { + [key: string]: + | string + | number + | boolean + | bigint + | ReadonlyArray + | null + | undefined; + } + export function parse( + str: string, + sep?: string | null, + eq?: string | null, + options?: ParseOptions, + ): ParsedUrlQuery; + export function stringify( + obj?: ParsedUrlQueryInput, + sep?: string | null, + eq?: string | null, + options?: StringifyOptions, + ): string; + export const decode: typeof parse; + export const encode: typeof stringify; + export function escape(str: string): string; + export function unescape(str: string): string; +} + +declare module "node:querystring" { + export * from "querystring"; +} diff --git a/packages/compiler/ambient/node-fallback/stream-consumers.d.ts b/packages/compiler/ambient/node-fallback/stream-consumers.d.ts new file mode 100644 index 000000000..9e220ea17 --- /dev/null +++ b/packages/compiler/ambient/node-fallback/stream-consumers.d.ts @@ -0,0 +1,21 @@ +declare module "stream/consumers" { + /* The promise consumers over a readable: accumulate every chunk and + * settle — text (the utf8 decode), json (the text through JSON.parse: + * `unknown`, validated with a checked cast; malformed input rejects + * with the parse's SyntaxError), buffer (the concatenated bytes; + * string chunks contribute their utf8 bytes, Node's Blob rule). + * Stream errors reject; an early close rejects with + * ERR_STREAM_PREMATURE_CLOSE. arrayBuffer answers an ArrayBuffer over + * the bytes; blob fences per site. */ + function text(stream: unknown): Promise; + function json(stream: unknown): Promise; + function buffer(stream: unknown): Promise; + function arrayBuffer(stream: unknown): Promise; + function blob(stream: unknown): Promise; + export { arrayBuffer, blob, buffer, json, text }; +} + +declare module "node:stream/consumers" { + import streamConsumers = require("stream/consumers"); + export = streamConsumers; +} diff --git a/packages/compiler/ambient/node-fallback/stream-promises.d.ts b/packages/compiler/ambient/node-fallback/stream-promises.d.ts new file mode 100644 index 000000000..bf771d571 --- /dev/null +++ b/packages/compiler/ambient/node-fallback/stream-promises.d.ts @@ -0,0 +1,28 @@ +/// + +declare module "stream/promises" { + /* The promise forms of finished/pipeline: a void promise the stream's + * terminal point settles (rejected with the error, or + * ERR_STREAM_PREMATURE_CLOSE on an early close). Stream arguments are + * the lowered surface — iterables/generators/functions fence per site. + * pipeline's trailing { signal, end } object and finished's options + * (readable, writable, error, cleanup, signal) are supported. A last + * stage that is an async function resolves the promise with its value. */ + function pipeline(...streams: T): import("stream").PipelinePromise; + function finished( + stream: unknown, + options?: { + readable?: boolean; + writable?: boolean; + error?: boolean; + cleanup?: boolean; + signal?: AbortSignal; + } | null, + ): Promise; + export { pipeline, finished }; +} + +declare module "node:stream/promises" { + import streamPromises = require("stream/promises"); + export = streamPromises; +} diff --git a/packages/compiler/ambient/node-fallback/stream-web.d.ts b/packages/compiler/ambient/node-fallback/stream-web.d.ts new file mode 100644 index 000000000..f947e667d --- /dev/null +++ b/packages/compiler/ambient/node-fallback/stream-web.d.ts @@ -0,0 +1,17 @@ +/* node:stream/web — the module exports the web stream globals themselves + * (require("stream/web").ReadableStream === globalThis.ReadableStream). + * ReadableStream is the implemented class; its reader and controller are + * exported as types. */ +declare module "stream/web" { + export const ReadableStream: typeof globalThis.ReadableStream; + export type ReadableStream = globalThis.ReadableStream; + export type ReadableStreamDefaultReader = globalThis.ReadableStreamDefaultReader; + export type ReadableStreamDefaultController = + globalThis.ReadableStreamDefaultController; + export type UnderlyingSource = globalThis.UnderlyingSource; + export type QueuingStrategy = globalThis.QueuingStrategy; +} + +declare module "node:stream/web" { + export * from "stream/web"; +} diff --git a/packages/compiler/ambient/node-fallback/stream.d.ts b/packages/compiler/ambient/node-fallback/stream.d.ts new file mode 100644 index 000000000..24f290fd9 --- /dev/null +++ b/packages/compiler/ambient/node-fallback/stream.d.ts @@ -0,0 +1,644 @@ +/// + +/* node:stream — the static stream classes (runtime-provided, the + * EventEmitter-hierarchy precedent). Phase 1 is the OPTIONS-OBJECT + * constructor surface: `new Readable({ read() {} })`, `new Writable({ + * write(chunk, enc, cb) {} })`, Duplex/Transform/PassThrough, push/read, + * flowing vs paused, write/end backpressure + 'drain', pipe/unpipe, and + * destroy — with Node's event orderings. Chunks are BYTES (Buffer), with + * strings converted at push/write like Node, and any value on an + * object-mode side (objectMode, readableObjectMode, writableObjectMode — + * the object-mode constructor overloads come first). The runtime-fired + * events carry per-class FORCED argument tuples ('data' is one chunk, + * 'error' one Error, 'pipe'/'unpipe' one Readable, the rest empty) — the + * overloads below hand tsc the same contract so unannotated listener + * parameters contextually type in .js sources. */ +declare module "stream" { + import { EventEmitter } from "events"; + + interface ReadableOptions { + highWaterMark?: number; + encoding?: string; + objectMode?: boolean; + read?(this: Readable, size: number): void; + destroy?(this: Readable, error: Error | null, callback: (error?: Error | null) => void): void; + construct?(this: Readable, callback: (error?: Error | null) => void): void; + autoDestroy?: boolean; + emitClose?: boolean; + captureRejections?: boolean; + defaultEncoding?: string; + signal?: AbortSignal; + } + interface WritableOptions { + highWaterMark?: number; + decodeStrings?: boolean; + defaultEncoding?: string; + objectMode?: boolean; + write?( + this: Writable, + chunk: Buffer, + encoding: string, + callback: (error?: Error | null) => void, + ): void; + writev?( + this: Writable, + chunks: Array<{ chunk: any; encoding: string }>, + callback: (error?: Error | null) => void, + ): void; + final?(this: Writable, callback: (error?: Error | null) => void): void; + destroy?(this: Writable, error: Error | null, callback: (error?: Error | null) => void): void; + construct?(this: Writable, callback: (error?: Error | null) => void): void; + autoDestroy?: boolean; + emitClose?: boolean; + captureRejections?: boolean; + signal?: AbortSignal; + } + interface DuplexOptions { + highWaterMark?: number; + readableHighWaterMark?: number; + writableHighWaterMark?: number; + encoding?: string; + decodeStrings?: boolean; + objectMode?: boolean; + readableObjectMode?: boolean; + writableObjectMode?: boolean; + allowHalfOpen?: boolean; + read?(this: Duplex, size: number): void; + write?( + this: Duplex, + chunk: Buffer, + encoding: string, + callback: (error?: Error | null) => void, + ): void; + final?(this: Duplex, callback: (error?: Error | null) => void): void; + destroy?(this: Duplex, error: Error | null, callback: (error?: Error | null) => void): void; + construct?(this: Duplex, callback: (error?: Error | null) => void): void; + autoDestroy?: boolean; + emitClose?: boolean; + captureRejections?: boolean; + readable?: boolean; + writable?: boolean; + defaultEncoding?: string; + writev?( + this: Duplex, + chunks: Array<{ chunk: any; encoding: string }>, + callback: (error?: Error | null) => void, + ): void; + signal?: AbortSignal; + } + interface TransformOptions { + highWaterMark?: number; + readableHighWaterMark?: number; + writableHighWaterMark?: number; + encoding?: string; + decodeStrings?: boolean; + objectMode?: boolean; + readableObjectMode?: boolean; + writableObjectMode?: boolean; + allowHalfOpen?: boolean; + transform?( + this: Transform, + chunk: Buffer, + encoding: string, + callback: (error?: Error | null, data?: Buffer | string) => void, + ): void; + flush?(this: Transform, callback: (error?: Error | null, data?: Buffer | string) => void): void; + destroy?(this: Transform, error: Error | null, callback: (error?: Error | null) => void): void; + construct?(this: Transform, callback: (error?: Error | null) => void): void; + read?(this: Transform, size: number): void; + write?( + this: Transform, + chunk: Buffer, + encoding: string, + callback: (error?: Error | null) => void, + ): void; + final?(this: Transform, callback: (error?: Error | null) => void): void; + writev?(this: Transform, chunks: unknown[], callback: (error?: Error | null) => void): void; + autoDestroy?: boolean; + emitClose?: boolean; + captureRejections?: boolean; + readable?: boolean; + writable?: boolean; + defaultEncoding?: string; + signal?: AbortSignal; + } + + /* Object-mode options (objectMode: true, or a Duplex's or Transform's + * readableObjectMode / writableObjectMode): chunks and completion data + * are any values, and an object-mode write's encoding argument reaches + * write/transform as passed (undefined when absent, null when a + * callback took its place). The constructors take these before the + * byte-mode options, so a byte-mode stream's callbacks keep Buffer + * chunks. */ + type ObjectModeEncoding = string | null | undefined; + type ObjectModeFlag = + | { objectMode: true } + | { readableObjectMode: true } + | { writableObjectMode: true }; + interface ObjectWritableOptions extends Omit { + objectMode: true; + write?( + this: Writable, + chunk: any, + encoding: ObjectModeEncoding, + callback: (error?: Error | null) => void, + ): void; + } + type ObjectDuplexOptions = Omit & { + write?( + this: Duplex, + chunk: any, + encoding: ObjectModeEncoding, + callback: (error?: Error | null) => void, + ): void; + } & ObjectModeFlag; + type ObjectTransformOptions = Omit & { + transform?( + this: Transform, + chunk: any, + encoding: ObjectModeEncoding, + callback: (error?: Error | null, data?: any) => void, + ): void; + flush?(this: Transform, callback: (error?: Error | null, data?: any) => void): void; + write?( + this: Transform, + chunk: any, + encoding: ObjectModeEncoding, + callback: (error?: Error | null) => void, + ): void; + } & ObjectModeFlag; + + /* decodeStrings: false — a string write reaches write/transform as the + * string with its encoding (Buffers keep 'buffer'). */ + interface StringWritableOptions extends Omit { + decodeStrings: false; + write?( + this: Writable, + chunk: string | Buffer, + encoding: string, + callback: (error?: Error | null) => void, + ): void; + } + type StringDuplexOptions = Omit & { + decodeStrings: false; + write?( + this: Duplex, + chunk: string | Buffer, + encoding: string, + callback: (error?: Error | null) => void, + ): void; + }; + type StringTransformOptions = Omit & { + decodeStrings: false; + transform?( + this: Transform, + chunk: string | Buffer, + encoding: string, + callback: (error?: Error | null, data?: Buffer | string) => void, + ): void; + }; + + /* The operators' options: concurrency and highWaterMark bound map's + * in-flight callbacks and queued results. */ + interface ArrayOptions { + concurrency?: number; + highWaterMark?: number; + signal?: AbortSignal; + } + + class Readable extends EventEmitter { + constructor(opts?: ReadableOptions); + /* The underscore-method surface (@types/node declares these on the + * classes; assigning them post-construction shadows the prototype + * method and the machinery calls through the assignment — lowered + * onto the option-callback slots). */ + _read(size: number): void; + _destroy(error: Error | null, callback: (error?: Error | null) => void): void; + [Symbol.asyncIterator](): AsyncIterableIterator; + iterator(options?: { destroyOnReturn?: boolean }): AsyncIterableIterator; + /* compose(this, stage), destroyed when options.signal aborts. */ + compose(stage: ComposeStage, options?: { signal: AbortSignal }): Duplex; + push(chunk: Buffer | string | null, encoding?: string): boolean; + /* An object-mode stream's values. */ + push(chunk: any, encoding?: string): boolean; + unshift(chunk: Buffer | string, encoding?: string): void; + unshift(chunk: any, encoding?: string): void; + /* A Buffer, a string once encoded, an object-mode value, or null (as + * @types/node types it). */ + read(size?: number): any; + pause(): this; + resume(): this; + isPaused(): boolean; + setEncoding(encoding: string): this; + pipe(destination: T, options?: { end?: boolean }): T; + unpipe(destination?: NodeJS.WritableStream): this; + destroy(error?: Error): this; + readonly readable: boolean; + readonly readableEnded: boolean; + readonly readableFlowing: boolean | null; + readonly readableLength: number; + readonly readableHighWaterMark: number; + readonly readableObjectMode: boolean; + readonly closed: boolean; + readonly destroyed: boolean; + readonly errored: Error | null; + static from(iterable: unknown, options?: ReadableOptions): Readable; + /* A Readable over a web ReadableStream's reader. */ + static fromWeb( + readableStream: globalThis.ReadableStream, + options?: { + encoding?: string; + highWaterMark?: number; + objectMode?: boolean; + signal?: AbortSignal; + }, + ): Readable; + /* The Readable operators: chunks are strings or Buffers, map and + * flatMap callbacks return strings or Buffers (flatMap: arrays of + * them, or strings), and callbacks that declare the options argument + * are refused. */ + map( + fn: (data: any, options?: Pick) => any, + options?: ArrayOptions, + ): Readable; + filter( + fn: (data: any, options?: Pick) => boolean | Promise, + options?: ArrayOptions, + ): Readable; + forEach( + fn: (data: any, options?: Pick) => void | Promise, + options?: ArrayOptions, + ): Promise; + toArray(options?: Pick): Promise; + some( + fn: (data: any, options?: Pick) => boolean | Promise, + options?: ArrayOptions, + ): Promise; + find( + fn: (data: any, options?: Pick) => boolean | Promise, + options?: ArrayOptions, + ): Promise; + every( + fn: (data: any, options?: Pick) => boolean | Promise, + options?: ArrayOptions, + ): Promise; + flatMap( + fn: (data: any, options?: Pick) => any, + options?: ArrayOptions, + ): Readable; + drop(limit: number, options?: Pick): Readable; + take(limit: number, options?: Pick): Readable; + reduce( + fn: (previous: any, data: any, options?: Pick) => T, + initial?: undefined, + options?: Pick, + ): Promise; + reduce( + fn: (previous: T, data: any, options?: Pick) => T, + initial: T, + options?: Pick, + ): Promise; + on(event: "data", listener: (chunk: any) => void): this; + on(event: "end" | "close" | "readable" | "pause" | "resume", listener: () => void): this; + on(event: "error", listener: (err: Error) => void): this; + on(event: string, listener: (...args: any[]) => void): this; + once(event: "data", listener: (chunk: any) => void): this; + once(event: "end" | "close" | "readable" | "pause" | "resume", listener: () => void): this; + once(event: "error", listener: (err: Error) => void): this; + once(event: string, listener: (...args: any[]) => void): this; + addListener(event: "data", listener: (chunk: any) => void): this; + addListener( + event: "end" | "close" | "readable" | "pause" | "resume", + listener: () => void, + ): this; + addListener(event: "error", listener: (err: Error) => void): this; + addListener(event: string, listener: (...args: any[]) => void): this; + prependListener(event: "data", listener: (chunk: any) => void): this; + prependListener( + event: "end" | "close" | "readable" | "pause" | "resume", + listener: () => void, + ): this; + prependListener(event: "error", listener: (err: Error) => void): this; + prependListener(event: string, listener: (...args: any[]) => void): this; + prependOnceListener(event: "data", listener: (chunk: any) => void): this; + prependOnceListener( + event: "end" | "close" | "readable" | "pause" | "resume", + listener: () => void, + ): this; + prependOnceListener(event: "error", listener: (err: Error) => void): this; + prependOnceListener(event: string, listener: (...args: any[]) => void): this; + off(event: "data", listener: (chunk: any) => void): this; + off(event: "end" | "close" | "readable" | "pause" | "resume", listener: () => void): this; + off(event: "error", listener: (err: Error) => void): this; + off(event: string, listener: (...args: any[]) => void): this; + removeListener(event: "data", listener: (chunk: any) => void): this; + removeListener( + event: "end" | "close" | "readable" | "pause" | "resume", + listener: () => void, + ): this; + removeListener(event: "error", listener: (err: Error) => void): this; + removeListener(event: string, listener: (...args: any[]) => void): this; + } + + class Writable extends EventEmitter { + constructor(opts: ObjectWritableOptions); + constructor(opts: StringWritableOptions); + constructor(opts?: WritableOptions); + /* The underscore-method surface (see Readable). */ + _write(chunk: any, encoding: string, callback: (error?: Error | null) => void): void; + _writev( + chunks: Array<{ chunk: any; encoding: string }>, + callback: (error?: Error | null) => void, + ): void; + _final(callback: (error?: Error | null) => void): void; + _destroy(error: Error | null, callback: (error?: Error | null) => void): void; + write(chunk: Buffer | string, callback?: (error?: Error | null) => void): boolean; + write( + chunk: Buffer | string, + encoding: string, + callback?: (error?: Error | null) => void, + ): boolean; + /* An object-mode stream's values (the encoding argument reaches + * _write as passed). */ + write(chunk: any, callback?: (error?: Error | null) => void): boolean; + write( + chunk: any, + encoding: ObjectModeEncoding, + callback?: (error?: Error | null) => void, + ): boolean; + end(callback?: () => void): this; + end(chunk: Buffer | string, callback?: () => void): this; + end(chunk: Buffer | string, encoding: string, callback?: () => void): this; + end(chunk: any, callback?: () => void): this; + end(chunk: any, encoding: ObjectModeEncoding, callback?: () => void): this; + cork(): void; + uncork(): void; + setDefaultEncoding(encoding: string): this; + destroy(error?: Error): this; + readonly writable: boolean; + readonly writableEnded: boolean; + readonly writableFinished: boolean; + readonly writableLength: number; + readonly writableHighWaterMark: number; + readonly writableObjectMode: boolean; + readonly writableNeedDrain: boolean; + readonly writableCorked: number; + readonly closed: boolean; + readonly destroyed: boolean; + readonly errored: Error | null; + on(event: "drain" | "finish" | "close", listener: () => void): this; + on(event: "error", listener: (err: Error) => void): this; + on(event: "pipe" | "unpipe", listener: (src: Readable) => void): this; + on(event: string, listener: (...args: any[]) => void): this; + once(event: "drain" | "finish" | "close", listener: () => void): this; + once(event: "error", listener: (err: Error) => void): this; + once(event: "pipe" | "unpipe", listener: (src: Readable) => void): this; + once(event: string, listener: (...args: any[]) => void): this; + addListener(event: "drain" | "finish" | "close", listener: () => void): this; + addListener(event: "error", listener: (err: Error) => void): this; + addListener(event: "pipe" | "unpipe", listener: (src: Readable) => void): this; + addListener(event: string, listener: (...args: any[]) => void): this; + prependListener(event: "drain" | "finish" | "close", listener: () => void): this; + prependListener(event: "error", listener: (err: Error) => void): this; + prependListener(event: "pipe" | "unpipe", listener: (src: Readable) => void): this; + prependListener(event: string, listener: (...args: any[]) => void): this; + prependOnceListener(event: "drain" | "finish" | "close", listener: () => void): this; + prependOnceListener(event: "error", listener: (err: Error) => void): this; + prependOnceListener(event: "pipe" | "unpipe", listener: (src: Readable) => void): this; + prependOnceListener(event: string, listener: (...args: any[]) => void): this; + off(event: "drain" | "finish" | "close", listener: () => void): this; + off(event: "error", listener: (err: Error) => void): this; + off(event: "pipe" | "unpipe", listener: (src: Readable) => void): this; + off(event: string, listener: (...args: any[]) => void): this; + removeListener(event: "drain" | "finish" | "close", listener: () => void): this; + removeListener(event: "error", listener: (err: Error) => void): this; + removeListener(event: "pipe" | "unpipe", listener: (src: Readable) => void): this; + removeListener(event: string, listener: (...args: any[]) => void): this; + } + + /* Duplex is Readable + Writable in one object. TS single inheritance + * mirrors @types/node: extends Readable, the writable half declared + * again. The lowering treats both halves as first-class. */ + class Duplex extends Readable { + /* Duplex.from: an async generator function, an iterable, a string or + * a stream (duplexify.js). */ + static from(body: unknown): Duplex; + constructor(opts: ObjectDuplexOptions); + constructor(opts: StringDuplexOptions); + constructor(opts?: DuplexOptions); + /* The underscore-method surface (see Readable). */ + _write(chunk: any, encoding: string, callback: (error?: Error | null) => void): void; + _writev( + chunks: Array<{ chunk: any; encoding: string }>, + callback: (error?: Error | null) => void, + ): void; + _final(callback: (error?: Error | null) => void): void; + write(chunk: Buffer | string, callback?: (error?: Error | null) => void): boolean; + write( + chunk: Buffer | string, + encoding: string, + callback?: (error?: Error | null) => void, + ): boolean; + /* An object-mode stream's values (the encoding argument reaches + * _write as passed). */ + write(chunk: any, callback?: (error?: Error | null) => void): boolean; + write( + chunk: any, + encoding: ObjectModeEncoding, + callback?: (error?: Error | null) => void, + ): boolean; + end(callback?: () => void): this; + end(chunk: Buffer | string, callback?: () => void): this; + end(chunk: Buffer | string, encoding: string, callback?: () => void): this; + end(chunk: any, callback?: () => void): this; + end(chunk: any, encoding: ObjectModeEncoding, callback?: () => void): this; + cork(): void; + uncork(): void; + setDefaultEncoding(encoding: string): this; + readonly writable: boolean; + readonly writableEnded: boolean; + readonly writableFinished: boolean; + readonly writableLength: number; + readonly writableHighWaterMark: number; + readonly writableObjectMode: boolean; + readonly writableNeedDrain: boolean; + readonly writableCorked: number; + readonly allowHalfOpen: boolean; + on(event: "data", listener: (chunk: any) => void): this; + on( + event: "end" | "close" | "readable" | "pause" | "resume" | "drain" | "finish", + listener: () => void, + ): this; + on(event: "error", listener: (err: Error) => void): this; + on(event: "pipe" | "unpipe", listener: (src: Readable) => void): this; + on(event: string, listener: (...args: any[]) => void): this; + once(event: "data", listener: (chunk: any) => void): this; + once( + event: "end" | "close" | "readable" | "pause" | "resume" | "drain" | "finish", + listener: () => void, + ): this; + once(event: "error", listener: (err: Error) => void): this; + once(event: "pipe" | "unpipe", listener: (src: Readable) => void): this; + once(event: string, listener: (...args: any[]) => void): this; + addListener(event: "data", listener: (chunk: any) => void): this; + addListener( + event: "end" | "close" | "readable" | "pause" | "resume" | "drain" | "finish", + listener: () => void, + ): this; + addListener(event: "error", listener: (err: Error) => void): this; + addListener(event: "pipe" | "unpipe", listener: (src: Readable) => void): this; + addListener(event: string, listener: (...args: any[]) => void): this; + prependListener(event: "data", listener: (chunk: any) => void): this; + prependListener( + event: "end" | "close" | "readable" | "pause" | "resume" | "drain" | "finish", + listener: () => void, + ): this; + prependListener(event: "error", listener: (err: Error) => void): this; + prependListener(event: "pipe" | "unpipe", listener: (src: Readable) => void): this; + prependListener(event: string, listener: (...args: any[]) => void): this; + prependOnceListener(event: "data", listener: (chunk: any) => void): this; + prependOnceListener( + event: "end" | "close" | "readable" | "pause" | "resume" | "drain" | "finish", + listener: () => void, + ): this; + prependOnceListener(event: "error", listener: (err: Error) => void): this; + prependOnceListener(event: "pipe" | "unpipe", listener: (src: Readable) => void): this; + prependOnceListener(event: string, listener: (...args: any[]) => void): this; + off(event: "data", listener: (chunk: any) => void): this; + off( + event: "end" | "close" | "readable" | "pause" | "resume" | "drain" | "finish", + listener: () => void, + ): this; + off(event: "error", listener: (err: Error) => void): this; + off(event: "pipe" | "unpipe", listener: (src: Readable) => void): this; + off(event: string, listener: (...args: any[]) => void): this; + removeListener(event: "data", listener: (chunk: any) => void): this; + removeListener( + event: "end" | "close" | "readable" | "pause" | "resume" | "drain" | "finish", + listener: () => void, + ): this; + removeListener(event: "error", listener: (err: Error) => void): this; + removeListener(event: "pipe" | "unpipe", listener: (src: Readable) => void): this; + removeListener(event: string, listener: (...args: any[]) => void): this; + } + + class Transform extends Duplex { + constructor(opts: ObjectTransformOptions); + constructor(opts: StringTransformOptions); + constructor(opts?: TransformOptions); + /* The underscore-method surface (see Readable). */ + _transform( + chunk: any, + encoding: string, + callback: (error?: Error | null, data?: any) => void, + ): void; + _flush(callback: (error?: Error | null, data?: any) => void): void; + } + class PassThrough extends Transform { + constructor(opts: ObjectTransformOptions); + constructor(opts: StringTransformOptions); + constructor(opts?: TransformOptions); + } + + /* addAbortSignal(signal, stream): the stream, destroyed with an + * AbortError (cause: signal.reason) when the signal aborts. */ + function addAbortSignal(signal: AbortSignal, stream: T): T; + /* pipeline(...stages, callback): the last stream, or the object-mode + * PassThrough that a function destination feeds. */ + type PipelineResult = T extends [ + ...unknown[], + infer D, + (...args: any[]) => unknown, + ] + ? D extends (...args: any[]) => unknown + ? PassThrough + : D + : unknown; + function pipeline(...streams: T): PipelineResult; + /* util.promisify's pipeline overload matches only pipeline itself: a + * generic signature alone admits every function. */ + namespace pipeline { + const __promisify__: typeof promises.pipeline; + } + /* A compose stage: a stream, an async generator function over the + * previous stage, or (first) an iterable, async iterable or web + * ReadableStream. */ + type ComposeStage = + | Readable + | Writable + | Iterable + | AsyncIterable + | globalThis.ReadableStream + | ((source: any, options?: { signal: AbortSignal }) => unknown); + /* compose(...streams): a Duplex that writes to the first stage and + * reads from the last, with the stages running as a pipeline. */ + function compose(...streams: ComposeStage[]): Duplex; + /* finished's options: readable/writable choose which ends count, + * error: false ignores 'error' events, signal aborts the wait. */ + interface FinishedOptions { + readable?: boolean; + writable?: boolean; + error?: boolean; + signal?: AbortSignal; + } + function finished(stream: unknown, callback: (err?: Error | null) => void): () => void; + function finished( + stream: unknown, + options: FinishedOptions | null, + callback: (err?: Error | null) => void, + ): () => void; + /* Folds to the TARGET platform's default at compile time (win32 16384, + * else 65536 — Node's own state.js split); literal argument only. */ + function getDefaultHighWaterMark(objectMode: boolean): number; + /* The promise pipeline's result: a last stage that is an async + * function resolves the promise with its value (before the options + * argument, when one follows). */ + type PipelinePromise = T extends [ + ...unknown[], + (...args: any[]) => Promise, + ] + ? Promise

+ : T extends [ + ...unknown[], + (...args: any[]) => Promise, + { signal?: AbortSignal; end?: boolean }, + ] + ? Promise

+ : Promise; + namespace promises { + function pipeline(...streams: T): PipelinePromise; + function finished( + stream: unknown, + options?: FinishedOptions & { cleanup?: boolean }, + ): Promise; + } + class Stream extends EventEmitter {} + + export { + Readable, + Writable, + Duplex, + Transform, + PassThrough, + Stream, + addAbortSignal, + pipeline, + compose, + finished, + getDefaultHighWaterMark, + promises, + }; + export type { + ReadableOptions, + WritableOptions, + DuplexOptions, + TransformOptions, + FinishedOptions, + PipelinePromise, + }; +} + +declare module "node:stream" { + import stream = require("stream"); + export = stream; +} diff --git a/packages/compiler/ambient/node-fallback/string_decoder.d.ts b/packages/compiler/ambient/node-fallback/string_decoder.d.ts new file mode 100644 index 000000000..617a7871c --- /dev/null +++ b/packages/compiler/ambient/node-fallback/string_decoder.d.ts @@ -0,0 +1,20 @@ +/* node:string_decoder — StringDecoder over Node's whole encoding set: + * write() decodes complete sequences and buffers a trailing partial one + * across chunk boundaries (utf8's truncated sequence, base64's mod-3 + * remainder, utf16le's odd byte / trailing lead surrogate; latin1, ascii + * and hex are stateless); end() flushes the buffered partial. `encoding` + * answers the normalized name. Node's exact algorithms (SEMANTICS.md); + * the encoding must be a literal, and end(buffer) fences — write the + * buffer, then end(). */ +declare module "string_decoder" { + export class StringDecoder { + constructor(encoding?: BufferEncoding); + readonly encoding: string; + write(buffer: Uint8Array): string; + end(buffer?: Uint8Array): string; + } +} + +declare module "node:string_decoder" { + export * from "string_decoder"; +} diff --git a/packages/compiler/ambient/node-fallback/test.d.ts b/packages/compiler/ambient/node-fallback/test.d.ts new file mode 100644 index 000000000..2ca1c0fe7 --- /dev/null +++ b/packages/compiler/ambient/node-fallback/test.d.ts @@ -0,0 +1,236 @@ +/// + +/* node:assert — the static assertion surface. The module object IS a + * callable function (`assert(x)` is `assert.ok(x)`), so the declaration + * is the callable-namespace `export =` shape and BOTH `import assert + * from "node:assert"` and `const assert = require("assert")` bind the + * callable; named imports take the members. Failures throw + * AssertionError (name "AssertionError", code "ERR_ASSERTION" — catch and + * read .name/.message/.code). strictEqual/deepStrictEqual compare with + * Object.is over scalars; deepStrictEqual compares composites + * structurally per their static types. The messages here are plain + * strings (Node accepts Errors — that form fences per site), and the + * shallow legacy pair (equal/notEqual) follows Node's == coercions for + * primitive and primitive-union operands, including Node v24's NaN + * exception. deepEqual/notDeepEqual remain declared-but-fenced; + * assert/strict's loose names alias the strict forms and lower. */ +/* node:test — the in-process test runner (prefix-only in Node too: + * require("test") is MODULE_NOT_FOUND). test/it register tests (sync and + * async bodies, skip/todo/only options and method twins), describe/suite + * group them (bodies run at registration, Node's collection phase), + * before/after/beforeEach/afterEach hook the enclosing suite, and the + * TestContext argument carries subtests (t.test), t.skip/t.todo, + * t.diagnostic, and t.assert. mock/run/snapshot are DECLARED so real + * suites typecheck but fence at their use sites (no lowering). */ +declare module "node:test" { + type TestFn = (t: TestContext) => void | Promise; + type SuiteFn = (s: SuiteContext) => void; + type HookFn = (c: TestContext | SuiteContext) => void | Promise; + /* The describe/suite callback's context, and the before/after hook + * context of a suite. */ + interface SuiteContext { + readonly name: string; + readonly fullName: string; + readonly filePath: string | undefined; + readonly attempt: number; + readonly passed: boolean; + diagnostic(message: string): void; + } + interface TestOptions { + skip?: boolean | string; + todo?: boolean | string; + only?: boolean; + concurrency?: number | boolean; + timeout?: number; + plan?: number; + } + interface TestContext { + readonly name: string; + readonly fullName: string; + readonly filePath: string | undefined; + readonly attempt: number; + readonly passed: boolean; + readonly workerId: number | undefined; + readonly tags: readonly string[]; + readonly assert: TestContextAssert; + readonly mock: MockTracker; + test(name?: string, fn?: TestFn): Promise; + test(name?: string, options?: TestOptions, fn?: TestFn): Promise; + test(options?: TestOptions, fn?: TestFn): Promise; + test(fn?: TestFn): Promise; + skip(message?: string): void; + todo(message?: string): void; + diagnostic(message: string): void; + plan(count: number): void; + after(fn: HookFn): void; + before(fn: HookFn): void; + beforeEach(fn: HookFn): void; + afterEach(fn: HookFn): void; + } + interface AssertSnapshotOptions { + readonly serializers?: ReadonlyArray<(value: any) => any> | undefined; + } + interface TestContextAssert { + snapshot(value: any, options?: AssertSnapshotOptions): void; + fileSnapshot(value: any, path: string, options?: AssertSnapshotOptions): void; + ok(value: unknown, message?: string): void; + strictEqual(actual: unknown, expected: unknown, message?: string): void; + notStrictEqual(actual: unknown, expected: unknown, message?: string): void; + deepStrictEqual(actual: unknown, expected: unknown, message?: string): void; + notDeepStrictEqual(actual: unknown, expected: unknown, message?: string): void; + equal(actual: unknown, expected: unknown, message?: string): void; + notEqual(actual: unknown, expected: unknown, message?: string): void; + deepEqual(actual: unknown, expected: unknown, message?: string): void; + notDeepEqual(actual: unknown, expected: unknown, message?: string): void; + fail(message?: string): never; + throws(fn: () => unknown, expected?: unknown, message?: string): void; + match(value: string, regExp: RegExp, message?: string): void; + doesNotMatch(value: string, regExp: RegExp, message?: string): void; + } + /* The name, options and function are each optional, as in Node: a test + * without a name takes the function's name, or "". */ + function test(name?: string, fn?: TestFn): Promise; + function test(name?: string, options?: TestOptions, fn?: TestFn): Promise; + function test(options?: TestOptions, fn?: TestFn): Promise; + function test(fn?: TestFn): Promise; + namespace test { + function skip(name?: string, fn?: TestFn): Promise; + function skip(name?: string, options?: TestOptions, fn?: TestFn): Promise; + function skip(options?: TestOptions, fn?: TestFn): Promise; + function skip(fn?: TestFn): Promise; + function todo(name?: string, fn?: TestFn): Promise; + function todo(name?: string, options?: TestOptions, fn?: TestFn): Promise; + function todo(options?: TestOptions, fn?: TestFn): Promise; + function todo(fn?: TestFn): Promise; + function only(name?: string, fn?: TestFn): Promise; + function only(name?: string, options?: TestOptions, fn?: TestFn): Promise; + function only(options?: TestOptions, fn?: TestFn): Promise; + function only(fn?: TestFn): Promise; + } + const it: typeof test; + function describe(name?: string, fn?: SuiteFn): void; + function describe(name?: string, options?: TestOptions, fn?: SuiteFn): void; + function describe(options?: TestOptions, fn?: SuiteFn): void; + function describe(fn?: SuiteFn): void; + namespace describe { + function skip(name?: string, fn?: SuiteFn): void; + function skip(name?: string, options?: TestOptions, fn?: SuiteFn): void; + function skip(options?: TestOptions, fn?: SuiteFn): void; + function skip(fn?: SuiteFn): void; + function todo(name?: string, fn?: SuiteFn): void; + function todo(name?: string, options?: TestOptions, fn?: SuiteFn): void; + function todo(options?: TestOptions, fn?: SuiteFn): void; + function todo(fn?: SuiteFn): void; + function only(name?: string, fn?: SuiteFn): void; + function only(name?: string, options?: TestOptions, fn?: SuiteFn): void; + function only(options?: TestOptions, fn?: SuiteFn): void; + function only(fn?: SuiteFn): void; + } + const suite: typeof describe; + function before(fn: HookFn): void; + function after(fn: HookFn): void; + function beforeEach(fn: HookFn): void; + function afterEach(fn: HookFn): void; + /* The mock function surface: mock.fn, mock.method and their t.mock + * twins, the tracker's restoreAll and reset, and the calls log and the + * implementation controls on fn.mock. */ + interface MockFunctionCall< + F extends Function, + ReturnType = F extends (...args: any) => infer T ? T : unknown, + Args = F extends (...args: infer Y) => any ? Y : unknown[], + > { + arguments: Args; + error: unknown | undefined; + result: ReturnType | undefined; + this: unknown; + } + interface MockFunctionContext { + readonly calls: MockFunctionCall[]; + callCount(): number; + mockImplementation(implementation: F): void; + mockImplementationOnce(implementation: F, onCall?: number): void; + resetCalls(): void; + restore(): void; + } + type Mock = F & { mock: MockFunctionContext }; + interface MockFunctionOptions { + times?: number; + } + type FunctionPropertyNames = { + [K in keyof T]: T[K] extends Function ? K : never; + }[keyof T]; + interface MockTracker { + fn undefined>(original?: F): Mock; + fn undefined, Implementation extends Function = F>( + original?: F, + implementation?: Implementation, + ): Mock; + method>( + object: MockedObject, + methodName: MethodName, + options?: MockFunctionOptions, + ): MockedObject[MethodName] extends Function ? Mock : never; + method< + MockedObject extends object, + MethodName extends FunctionPropertyNames, + Implementation extends Function, + >( + object: MockedObject, + methodName: MethodName, + implementation: Implementation, + options?: MockFunctionOptions, + ): MockedObject[MethodName] extends Function + ? Mock + : never; + reset(): void; + restoreAll(): void; + readonly timers: MockTimers; + } + interface MockTimersOptions { + apis: ReadonlyArray<"setInterval" | "setTimeout" | "setImmediate" | "Date">; + now?: number | Date | undefined; + } + interface MockTimers { + enable(options?: MockTimersOptions): void; + setTime(milliseconds?: number): void; + reset(): void; + tick(milliseconds?: number): void; + runAll(): void; + [Symbol.dispose](): void; + } + const mock: MockTracker; + /* The context of the test, suite or hook running in the current async + * context; undefined outside one. */ + function getTestContext(): TestContext | SuiteContext | undefined; + /* Declared for typechecking only — every use fences (no lowering). + * `assert` is the module-level custom-assertion registry (v24's + * `const { assert } = require('node:test')`), not TestContext.assert. */ + const assert: { register(name: string, fn: (...args: unknown[]) => unknown): void }; + function run(options?: unknown): unknown; + function snapshot(...args: unknown[]): unknown; + export { + test, + it, + describe, + suite, + before, + after, + beforeEach, + afterEach, + mock, + assert, + getTestContext, + run, + snapshot, + TestContext, + SuiteContext, + TestOptions, + Mock, + MockFunctionContext, + MockFunctionCall, + MockTracker, + MockTimers, + MockTimersOptions, + }; + export default test; +} diff --git a/packages/compiler/ambient/node-fallback/timers-promises.d.ts b/packages/compiler/ambient/node-fallback/timers-promises.d.ts new file mode 100644 index 000000000..619b92f79 --- /dev/null +++ b/packages/compiler/ambient/node-fallback/timers-promises.d.ts @@ -0,0 +1,31 @@ +/* node:timers/promises — the promisified pair the Node test harness leans + * on (`await setTimeout(ms)` as a sleep). setTimeout and setImmediate + * settle with their resolve value (undefined without one) from the shared + * timer heap and the immediate queue; setInterval(delay, value) lowers to a + * typed async iterator. The options object takes `ref`; AbortSignal + * options remain declared surface that fences per site. */ +declare module "node:timers/promises" { + export interface TimerOptions { + ref?: boolean; + signal?: AbortSignal; + } + export function setTimeout( + delay?: number, + value?: T, + options?: TimerOptions, + ): Promise; + export function setImmediate(value?: T, options?: TimerOptions): Promise; + export function setInterval( + delay?: number, + value?: T, + options?: unknown, + ): AsyncGenerator; + export const scheduler: { + wait(delay?: number, options?: TimerOptions): Promise; + yield(): Promise; + }; +} + +declare module "timers/promises" { + export * from "node:timers/promises"; +} diff --git a/packages/compiler/ambient/node-fallback/timers.d.ts b/packages/compiler/ambient/node-fallback/timers.d.ts new file mode 100644 index 000000000..f6a1d3946 --- /dev/null +++ b/packages/compiler/ambient/node-fallback/timers.d.ts @@ -0,0 +1,28 @@ +/* node:timers — the module twins of the timer globals (Node's timers + * module re-exports them: require('timers').setTimeout IS setTimeout). + * Named imports, destructured requires, and namespace calls all lower to + * the same libCalls as the globals; the surface beyond these six + * (enroll/unenroll, promises) fences per member. */ +declare module "node:timers" { + export function setTimeout(callback: () => void, ms?: number): Timeout; + export function setTimeout( + callback: (...args: never[]) => void, + ms?: number, + ...args: unknown[] + ): Timeout; + export function clearTimeout(handle?: Timeout | number | null | undefined): void; + export function setInterval(callback: () => void, ms?: number): Timeout; + export function setInterval( + callback: (...args: never[]) => void, + ms?: number, + ...args: unknown[] + ): Timeout; + export function clearInterval(handle?: Timeout | number | null | undefined): void; + export function setImmediate(callback: () => void): Immediate; + export function setImmediate(callback: (...args: never[]) => void, ...args: unknown[]): Immediate; + export function clearImmediate(handle?: Immediate | null | undefined): void; +} + +declare module "timers" { + export * from "node:timers"; +} diff --git a/packages/compiler/ambient/node-fallback/tls.d.ts b/packages/compiler/ambient/node-fallback/tls.d.ts new file mode 100644 index 000000000..6ddc08b35 --- /dev/null +++ b/packages/compiler/ambient/node-fallback/tls.d.ts @@ -0,0 +1,169 @@ +/// +/// + +/* node:tls — the TLS server slice (scr_tls.c over scr_net.c with the + * vendored mbedTLS; linked only into using binaries). + * createServer({ cert, key }, handler) returns a net.Server; the handler + * is Node's 'secureConnection' — it fires post-handshake with a socket + * that behaves exactly like a net socket (write/end/destroy/pipe and the + * data/end/close/error events are the same lowered surface). cert/key + * are PEM strings or Buffers. Handshake failures tear the socket down + * without emitting an event, Node's default for 'tlsClientError' with no listener. */ +declare module "tls" { + import { Server as NetServer, Socket } from "net"; + /** tls.Server: a net server whose connections handshake first. */ + export interface Server extends NetServer { + /* serve `context` to clients whose SNI name matches `hostname` ('*' + * matches within one label); the newest match wins */ + addContext(hostname: string, context: TlsOptions | SecureContext): void; + /* new connections serve a context made from `options` */ + setSecureContext(options: TlsOptions): void; + } + export interface TlsOptions { + /* Optional in the DECLARATION only (real Node programs build these + * literals with every TLS knob): the lowering still requires both — + * a literal without them fences at the call site. */ + cert?: string | Uint8Array; + key?: string | Uint8Array; + /* The options-record stance (see http.RequestOptions): every key + * typechecks; the option walk fences documented-but-unlowered keys + * by name and drops undocumented ones like Node. */ + [option: string]: unknown; + } + export function createServer( + options: TlsOptions, + secureConnectionListener?: (socket: TLSSocket) => void, + ): Server; + /** TLSSocket — post-handshake it IS a net socket to every lowered + * member (the mapping collapses it onto the socket kind; the + * secureConnect/session events ride the shared socket declarations). + * The TLS extras: authorized/authorizationError answer Node's verify + * split — authorizationError is the verify-failure CODE STRING + * (DEPTH_ZERO_SELF_SIGNED_CERT, ...), or null. */ + export interface TLSSocket extends Socket { + readonly encrypted: true; + authorized: boolean; + authorizationError: string | null; + /* null before the handshake, then the negotiated protocol or false */ + readonly alpnProtocol: string | false | null; + /* "TLSv1.2" or "TLSv1.3"; null before the handshake and after close */ + getProtocol(): string | null; + /* OpenSSL's suite name, the IANA name and the suite's version */ + getCipher(): CipherNameAndProtocol; + /* whether the handshake resumed a cached session */ + isSessionReused(): boolean; + /* the peer's certificate: {} before the handshake or without one, + * null once the socket is destroyed; detailed adds issuerCertificate */ + getPeerCertificate(detailed: true): DetailedPeerCertificate; + getPeerCertificate(detailed?: false): PeerCertificate; + getPeerCertificate(detailed?: boolean): PeerCertificate | DetailedPeerCertificate; + /* the certificate this socket serves, the same way */ + getCertificate(): PeerCertificate | object | null; + /* a new X509Certificate on each call, or undefined without one */ + getPeerX509Certificate(): import("crypto").X509Certificate | undefined; + getX509Certificate(): import("crypto").X509Certificate | undefined; + } + /* A certificate as getPeerCertificate() and X509Certificate's + * toLegacyObject() answer it: subject and issuer map attribute short + * names to values, infoAccess its " - " keys to values. */ + export interface PeerCertificate { + subject: { [attribute: string]: string | string[] }; + issuer: { [attribute: string]: string | string[] }; + subjectaltname?: string; + infoAccess?: { [key: string]: string[] }; + ca: boolean; + modulus?: string; + exponent?: string; + pubkey?: Buffer; + bits?: number; + valid_from: string; + valid_to: string; + fingerprint: string; + fingerprint256: string; + fingerprint512: string; + ext_key_usage?: string[]; + serialNumber: string; + raw: Buffer; + asn1Curve?: string; + nistCurve?: string; + } + /* getPeerCertificate(true): each certificate links to its issuer; a + * self-issued root is its own issuerCertificate */ + export interface DetailedPeerCertificate extends PeerCertificate { + issuerCertificate: DetailedPeerCertificate; + } + + /** new tls.TLSSocket(socket, options): the server-mode wrap of an open + * socket (a server connection or an http CONNECT socket). The options + * record reads at runtime; isServer, cert, key, secureContext, + * ALPNProtocols, minVersion and maxVersion are implemented. */ + export interface TLSSocketOptions { + isServer?: boolean; + cert?: string | Uint8Array; + key?: string | Uint8Array; + secureContext?: SecureContext; + ALPNProtocols?: string[] | Uint8Array; + minVersion?: string; + maxVersion?: string; + [option: string]: unknown; + } + export const TLSSocket: new (socket: Socket, options: TLSSocketOptions) => TLSSocket; + export interface CipherNameAndProtocol { + name: string; + standardName: string; + version: string; + } + export const DEFAULT_MIN_VERSION: string; + export const DEFAULT_MAX_VERSION: string; + /** tls.connect options — the RUNTIME options record (members read at + * runtime; port/host/socket/rejectUnauthorized/ca/servername are the + * honestly-implemented set, every other documented member throws the + * runtime fence — the options-record stance's divergence-66 shape). */ + export interface ConnectionOptions { + port?: number; + host?: string; + /* an open socket to run the client handshake over */ + socket?: Socket; + rejectUnauthorized?: boolean; + ca?: string | Uint8Array | ReadonlyArray; + servername?: string; + [option: string]: unknown; + } + export function connect( + options: ConnectionOptions, + secureConnectListener?: () => void, + ): TLSSocket; + export function connect( + port: number, + options?: ConnectionOptions, + secureConnectListener?: () => void, + ): TLSSocket; + export function connect( + port: number, + host?: string, + options?: ConnectionOptions, + secureConnectListener?: () => void, + ): TLSSocket; + /** An opaque parsed cert/key pair — what an SNI callback answers with + * (the value is a runtime handle; the members are runtime-internal). */ + export interface SecureContext {} + /* @types/node's name for createSecureContext's options */ + export type SecureContextOptions = TlsOptions; + export function createSecureContext(options: TlsOptions): SecureContext; + /** The CA-store introspection surface (scr_tls_ca.c): per-type cached + * PEM string arrays. The host bundle stands in for Node's compiled-in + * Mozilla roots ('bundled', and rootCertificates) AND the platform + * store ('system') — the Windows certificate stores or POSIX bundle probe; + * 'extra' reads NODE_EXTRA_CA_CERTS. An unknown type string throws + * Node's ERR_INVALID_ARG_VALUE TypeError. */ + export function getCACertificates(type?: string): string[]; + export const rootCertificates: readonly string[]; + /** Replaces the 'default' set (deduped) and the trust anchors the TLS + * client verifies against; a certificate-free non-empty array throws + * Node's ERR_CRYPTO_OPERATION_FAILED Error. */ + export function setDefaultCACertificates(certs: readonly string[]): void; +} + +declare module "node:tls" { + export * from "tls"; +} diff --git a/packages/compiler/ambient/node-fallback/url.d.ts b/packages/compiler/ambient/node-fallback/url.d.ts new file mode 100644 index 000000000..c690c52b1 --- /dev/null +++ b/packages/compiler/ambient/node-fallback/url.d.ts @@ -0,0 +1,95 @@ +/// + +/* node:url — the file-URL bridge pair. fileURLToPath accepts a URL value + * or a URL string and throws Node's TypeErrors (non-file scheme, encoded + * slashes, non-empty host); pathToFileURL resolves the path and percent- + * encodes it into a file: URL. The legacy API: parse() answers a Url + * object, format() serializes a Url-shaped object, a string, or a URL, + * resolve() resolves one URL string against another, the domain pair + * converts a hostname to and from its Punycode form, and urlToHttpOptions + * builds http.request options from a URL. */ +declare module "url" { + export type URL = globalThis.URL; + export type URLSearchParams = globalThis.URLSearchParams; + export const URL: typeof globalThis.URL; + export const URLSearchParams: typeof globalThis.URLSearchParams; + export function fileURLToPath(url: string | URL, options?: { windows?: boolean }): string; + export function fileURLToPathBuffer(url: string | URL, options?: { windows?: boolean }): Buffer; + export function pathToFileURL(path: string, options?: { windows?: boolean }): URL; + export interface UrlObject { + auth?: string | null | undefined; + hash?: string | null | undefined; + host?: string | null | undefined; + hostname?: string | null | undefined; + href?: string | null | undefined; + pathname?: string | null | undefined; + protocol?: string | null | undefined; + search?: string | null | undefined; + slashes?: boolean | null | undefined; + port?: string | number | null | undefined; + query?: string | null | import("querystring").ParsedUrlQueryInput | undefined; + } + export interface Url { + auth: string | null; + hash: string | null; + host: string | null; + hostname: string | null; + href: string; + path: string | null; + pathname: string | null; + protocol: string | null; + search: string | null; + slashes: boolean | null; + port: string | null; + query: string | null | import("querystring").ParsedUrlQuery; + } + export interface UrlWithParsedQuery extends Url { + query: import("querystring").ParsedUrlQuery; + } + export interface UrlWithStringQuery extends Url { + query: string | null; + } + export interface URLFormatOptions { + auth?: boolean | undefined; + fragment?: boolean | undefined; + search?: boolean | undefined; + unicode?: boolean | undefined; + } + export function parse( + urlString: string, + parseQueryString?: false, + slashesDenoteHost?: boolean, + ): UrlWithStringQuery; + export function parse( + urlString: string, + parseQueryString: true, + slashesDenoteHost?: boolean, + ): UrlWithParsedQuery; + export function parse( + urlString: string, + parseQueryString: boolean, + slashesDenoteHost?: boolean, + ): Url; + export function format(urlObject: URL, options?: URLFormatOptions): string; + export function format(urlObject: UrlObject | string): string; + export function resolve(from: string, to: string): string; + export function domainToASCII(domain: string): string; + export function domainToUnicode(domain: string): string; + /** urlToHttpOptions's null-prototype request options. */ + export interface UrlHttpOptions { + protocol: string; + hostname: string; + hash: string; + search: string; + pathname: string; + path: string; + href: string; + port?: number; + auth?: string; + } + export function urlToHttpOptions(url: URL): UrlHttpOptions; +} + +declare module "node:url" { + export * from "url"; +} diff --git a/packages/compiler/ambient/node-fallback/util-types.d.ts b/packages/compiler/ambient/node-fallback/util-types.d.ts new file mode 100644 index 000000000..3af1b6f00 --- /dev/null +++ b/packages/compiler/ambient/node-fallback/util-types.d.ts @@ -0,0 +1,50 @@ +/* node:util/types — native runtime brands. Other forms retain their + * per-member lowering boundaries. */ +declare module "util/types" { + export function isAnyArrayBuffer(value: unknown): boolean; + export function isArrayBuffer(value: unknown): value is ArrayBuffer; + export function isArrayBufferView(value: unknown): value is ArrayBufferView; + export function isDataView(value: unknown): value is DataView; + export function isTypedArray(value: unknown): boolean; + export function isUint8Array(value: unknown): value is Uint8Array; + export function isUint8ClampedArray(value: unknown): value is Uint8ClampedArray; + export function isUint16Array(value: unknown): value is Uint16Array; + export function isUint32Array(value: unknown): value is Uint32Array; + export function isInt8Array(value: unknown): value is Int8Array; + export function isInt16Array(value: unknown): value is Int16Array; + export function isInt32Array(value: unknown): value is Int32Array; + export function isFloat16Array(value: unknown): value is Float16Array; + export function isFloat32Array(value: unknown): value is Float32Array; + export function isFloat64Array(value: unknown): value is Float64Array; + export function isMap(value: unknown): value is Map; + export function isSet(value: unknown): value is Set; + export function isWeakMap(value: unknown): value is WeakMap; + export function isWeakSet(value: unknown): value is WeakSet; + export function isRegExp(value: unknown): value is RegExp; + export function isNativeError(value: unknown): value is Error; + export function isPromise(value: unknown): value is Promise; + export function isProxy(value: unknown): boolean; + export function isAsyncFunction(value: unknown): boolean; + export function isGeneratorFunction(value: unknown): boolean; + export function isGeneratorObject(value: unknown): boolean; + export function isModuleNamespaceObject(value: unknown): boolean; + export function isDate(value: unknown): value is Date; + export function isSharedArrayBuffer(value: unknown): value is SharedArrayBuffer; + export function isMapIterator(value: unknown): boolean; + export function isSetIterator(value: unknown): boolean; + export function isKeyObject(value: unknown): boolean; + export function isNumberObject(value: unknown): boolean; + export function isStringObject(value: unknown): boolean; + export function isBooleanObject(value: unknown): boolean; + export function isBigIntObject(value: unknown): boolean; + export function isSymbolObject(value: unknown): boolean; + export function isBoxedPrimitive(value: unknown): boolean; + export function isBigInt64Array(value: unknown): value is BigInt64Array; + export function isBigUint64Array(value: unknown): value is BigUint64Array; + export function isCryptoKey(value: unknown): boolean; + export function isExternal(value: unknown): boolean; +} + +declare module "node:util/types" { + export * from "util/types"; +} diff --git a/packages/compiler/ambient/node-fallback/util.d.ts b/packages/compiler/ambient/node-fallback/util.d.ts new file mode 100644 index 000000000..d75829827 --- /dev/null +++ b/packages/compiler/ambient/node-fallback/util.d.ts @@ -0,0 +1,396 @@ +/// +/// +/// +/// +/// +/// +/// +/// +/// + +/* node:util — compile-time projections for child_process.execFile, + * child_process.exec and fs.readFile. The first two retain Node's custom + * { stdout, stderr } result; the last resolves the callback's data + * argument. Other targets and bare + * promisify values fence per site. */ +declare module "util" { + export * as types from "util/types"; + export function getSystemErrorName(err: number): string; + export function getSystemErrorMessage(err: number): string; + export function getSystemErrorMap(): Map; + export function stripVTControlCharacters(str: string): string; + export function toUSVString(string: string): string; + export function isDeepStrictEqual(val1: unknown, val2: unknown, skipPrototype?: boolean): boolean; + export function styleText( + format: string | readonly string[], + text: string, + options?: { validateStream?: boolean; stream?: unknown }, + ): 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; + set(name: string, value: string): void; + values(): IterableIterator; + 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 any> { + (...args: Parameters): Promise>>; + cancel(reason?: unknown): void; + flush(): void; + ref(): this; + unref(): this; + readonly pending: Promise>> | null; + readonly pendingCount: number; + } + export function debounce any>( + fn: T, + wait: number, + options?: DebounceOptions, + ): DebouncedFunction; + export interface ThrottleOptions { + concurrency?: number; + maxPending?: number; + overflow?: "queue" | "drop"; + signal?: AbortSignal; + strict?: boolean; + } + export interface ThrottledFunction any> { + (...args: Parameters): Promise>>; + cancel(reason?: unknown): void; + hasImmediateCapacity(): boolean; + ref(): this; + unref(): this; + readonly pending: Promise>> | null; + readonly pendingCount: number; + readonly activeCount: number; + } + export function throttle any>( + fn: T, + limit: number, + interval: number, + options?: ThrottleOptions, + ): ThrottledFunction; + /** Resolves with the abort event once `signal` aborts, or with undefined + * when it already has. */ + export function aborted(signal: AbortSignal, resource: any): Promise; + /** 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(target: T, source: object | null | undefined): T; + export interface DeprecateOptions { + modifyPrototype?: boolean; + } + export function deprecate( + fn: T, + msg: string, + code?: string, + options?: DeprecateOptions, + ): T; + /* The callback receives null on success and the rejection reason + * otherwise (falsy reasons arrive as ERR_FALSY_VALUE_REJECTION). */ + export function callbackify( + fn: () => Promise, + ): (callback: (err: NodeJS.ErrnoException | null) => void) => void; + export function callbackify( + fn: () => Promise, + ): (callback: (err: NodeJS.ErrnoException | null, result: TResult) => void) => void; + export function callbackify( + fn: (arg1: T1) => Promise, + ): (arg1: T1, callback: (err: NodeJS.ErrnoException | null) => void) => void; + export function callbackify( + fn: (arg1: T1) => Promise, + ): (arg1: T1, callback: (err: NodeJS.ErrnoException | null, result: TResult) => void) => void; + export function callbackify( + fn: (arg1: T1, arg2: T2) => Promise, + ): (arg1: T1, arg2: T2, callback: (err: NodeJS.ErrnoException | null) => void) => void; + export function callbackify( + fn: (arg1: T1, arg2: T2) => Promise, + ): ( + arg1: T1, + arg2: T2, + callback: (err: NodeJS.ErrnoException | null, result: TResult) => void, + ) => void; + export function callbackify( + fn: (arg1: T1, arg2: T2, arg3: T3) => Promise, + ): (arg1: T1, arg2: T2, arg3: T3, callback: (err: NodeJS.ErrnoException | null) => void) => void; + export function callbackify( + fn: (arg1: T1, arg2: T2, arg3: T3) => Promise, + ): ( + arg1: T1, + arg2: T2, + arg3: T3, + callback: (err: NodeJS.ErrnoException | null, result: TResult) => void, + ) => void; + export type DebugLoggerFunction = (msg: string, ...param: unknown[]) => void; + export interface DebugLogger extends DebugLoggerFunction { + enabled: boolean; + } + export function debuglog( + section: string, + callback?: (fn: DebugLoggerFunction) => void, + ): DebugLogger; + export { debuglog as debug }; + export type TextEncoder = globalThis.TextEncoder; + export type TextDecoder = globalThis.TextDecoder; + export var TextEncoder: typeof globalThis.TextEncoder; + export var TextDecoder: typeof globalThis.TextDecoder; + /* 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, + ): typeof import("node:stream/promises").pipeline; + export function promisify( + fn: typeof import("node:fs").readFile, + ): (path: string, encoding: "utf8" | "utf-8") => Promise; + export function promisify( + fn: (file: string, args?: string[] | null, options?: object) => unknown, + ): ( + file: string, + args?: string[], + options?: { + encoding?: "utf8" | "utf-8"; + cwd?: string; + env?: { [k: string]: string | undefined }; + timeout?: number; + maxBuffer?: number; + killSignal?: string; + windowsHide?: boolean; + }, + ) => Promise<{ stdout: string; stderr: string }>; + export function promisify( + fn: typeof globalThis.setTimeout, + ): typeof import("node:timers/promises").setTimeout; + export function promisify( + fn: typeof globalThis.setImmediate, + ): typeof import("node:timers/promises").setImmediate; + export function promisify(fn: typeof import("node:child_process").exec): ( + command: string, + options?: { + encoding?: "utf8" | "utf-8"; + cwd?: string; + env?: { [k: string]: string | undefined }; + timeout?: number; + maxBuffer?: number; + killSignal?: string; + windowsHide?: boolean; + }, + ) => Promise<{ stdout: string; stderr: string }>; + /* The zlib callback one-shots (deflate, inflate, deflateRaw, inflateRaw, + * gzip, gunzip, unzip) share one type. */ + export function promisify( + fn: typeof import("node:zlib").gzip, + ): (data: string | Uint8Array, options?: import("node:zlib").ZlibOptions) => Promise; + export function promisify( + fn: typeof import("node:stream").finished, + ): typeof import("node:stream/promises").finished; + /* The crypto callback functions: the promise settles with the callback's + * value. */ + export function promisify( + fn: typeof import("node:crypto").randomBytes, + ): (size: number) => Promise; + export function promisify( + fn: typeof import("node:crypto").pbkdf2, + ): ( + password: string | Uint8Array, + salt: string | Uint8Array, + iterations: number, + keylen: number, + digest: string, + ) => Promise; + export function promisify( + fn: typeof import("node:crypto").scrypt, + ): ( + password: import("node:crypto").BinaryLike, + salt: import("node:crypto").BinaryLike, + keylen: number, + options?: import("node:crypto").ScryptOptions, + ) => Promise; + export function promisify( + fn: typeof import("node:crypto").hkdf, + ): ( + digest: string, + ikm: import("node:crypto").BinaryLike | import("node:crypto").KeyObject, + salt: import("node:crypto").BinaryLike, + info: import("node:crypto").BinaryLike, + keylen: number, + ) => Promise; + /* A callback-only function (`obj.close.bind(obj)`): before the targets + * whose leading parameters would otherwise capture it. Builtins that take + * arguments before their callback never match these. */ + export function promisify( + fn: (callback: (err: any, result: TResult) => void) => void, + ): () => Promise; + export function promisify(fn: (callback: (err?: any) => void) => void): () => Promise; + export function promisify( + fn: typeof import("node:crypto").randomFill, + ): ( + buffer: T, + offset?: number, + size?: number, + ) => Promise; + export function promisify(fn: typeof import("node:crypto").randomInt): { + (max: number): Promise; + (min: number, max: number): Promise; + }; + export function promisify( + fn: typeof import("node:crypto").generateKey, + ): ( + type: "hmac" | "aes", + options: { length: number }, + ) => Promise; + /* dns.lookup's promise settles with { address, family } (the address + * array with options.all), and lookupService's with { hostname, service }. */ + export function promisify(fn: typeof import("node:dns").lookup): { + ( + hostname: string, + options: import("node:dns").LookupAllOptions, + ): Promise; + ( + hostname: string, + options?: number | import("node:dns").LookupOneOptions, + ): Promise; + }; + export function promisify( + fn: typeof import("node:dns").lookupService, + ): (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). */ + 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, + 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: InspectOptions, + format?: unknown, + ...args: unknown[] + ): string; + export type ParseArgsOptionsType = "boolean" | "string"; + export interface ParseArgsOptionDescriptor { + type: ParseArgsOptionsType; + multiple?: boolean; + short?: string; + default?: string | boolean | string[] | boolean[]; + } + export interface ParseArgsOptionsConfig { + [longOption: string]: ParseArgsOptionDescriptor; + } + export interface ParseArgsConfig { + args?: readonly string[]; + options?: ParseArgsOptionsConfig; + strict?: boolean; + allowPositionals?: boolean; + allowNegative?: boolean; + tokens?: boolean; + } + export type ParseArgsToken = + | { + kind: "option"; + index: number; + name: string; + rawName: string; + value: string | undefined; + inlineValue: boolean | undefined; + } + | { kind: "positional"; index: number; value: string } + | { kind: "option-terminator"; index: number }; + export interface ParseArgsResult { + values: { [longOption: string]: undefined | string | boolean | Array }; + positionals: string[]; + tokens?: ParseArgsToken[]; + } + export function parseArgs(config?: ParseArgsConfig): ParseArgsResult; + /* util.getCallSites (Node ≥22.9): the captured-frame slice harness + * code reads. No stack bookkeeping exists in a compiled binary — every + * reached call fences per site. */ + export interface CallSite { + readonly functionName: string; + readonly scriptName: string; + readonly lineNumber: number; + readonly column: number; + } + export function getCallSites(frameCount?: number): CallSite[]; +} + +declare module "node:util" { + export * from "util"; +} diff --git a/packages/compiler/ambient/node-fallback/v8.d.ts b/packages/compiler/ambient/node-fallback/v8.d.ts new file mode 100644 index 000000000..16b112b1c --- /dev/null +++ b/packages/compiler/ambient/node-fallback/v8.d.ts @@ -0,0 +1,43 @@ +/* node:v8 — importable for the packages that require it at top level. + * getHeapStatistics answers allocator-derived figures in Node's shape + * (approximate: the runtime heap's total and used bytes, the machine's + * memory as the limit; V8-only figures are 0). The other members observe + * V8 itself and fence at their use with the by-design reason; they are + * declared so programs that type-check under their own tsc reach that + * fence. */ +declare module "v8" { + type DoesZapCodeSpaceFlag = 0 | 1; + export interface HeapInfo { + total_heap_size: number; + total_heap_size_executable: number; + total_physical_size: number; + total_available_size: number; + used_heap_size: number; + heap_size_limit: number; + malloced_memory: number; + peak_malloced_memory: number; + does_zap_garbage: DoesZapCodeSpaceFlag; + number_of_native_contexts: number; + number_of_detached_contexts: number; + total_global_handles_size: number; + used_global_handles_size: number; + external_memory: number; + } + export function getHeapStatistics(): HeapInfo; + export function cachedDataVersionTag(): number; + export function getHeapSpaceStatistics(): unknown[]; + export function getHeapCodeStatistics(): unknown; + export function getHeapSnapshot(options?: unknown): unknown; + export function writeHeapSnapshot(fileName?: string, options?: unknown): string; + export function setFlagsFromString(flags: string): void; + export function setHeapSnapshotNearHeapLimit(limit: number): void; + export function serialize(value: unknown): Buffer; + export function deserialize(buffer: Uint8Array): unknown; + export function takeCoverage(): void; + export function stopCoverage(): void; + export function isStringOneByteRepresentation(content: string): boolean; +} + +declare module "node:v8" { + export * from "v8"; +} diff --git a/packages/compiler/ambient/node-fallback/worker_threads.d.ts b/packages/compiler/ambient/node-fallback/worker_threads.d.ts new file mode 100644 index 000000000..81873f115 --- /dev/null +++ b/packages/compiler/ambient/node-fallback/worker_threads.d.ts @@ -0,0 +1,113 @@ +/// + +/* node:worker_threads — native workers use statically compiled entry points. + * Lowering and runtime diagnostics define the supported options and methods. */ +declare module "worker_threads" { + export const isMainThread: boolean; + export const isInternalThread: boolean; + export const threadId: number; + export interface ResourceLimits { + maxYoungGenerationSizeMb?: number; + maxOldGenerationSizeMb?: number; + codeRangeSizeMb?: number; + stackSizeMb?: number; + } + /* {} in the main thread */ + export const resourceLimits: ResourceLimits; + /* the Worker's name option, trimmed ("WorkerThread" by default); "" in the main thread */ + export const threadName: string | null; + export interface WorkerOptions { + workerData?: unknown; + transferList?: readonly unknown[]; + argv?: readonly unknown[]; + name?: string; + resourceLimits?: ResourceLimits; + stdout?: boolean; + stderr?: boolean; + stdin?: boolean; + [option: string]: unknown; + } + export class Worker { + constructor(filename: string | URL, options?: WorkerOptions); + readonly threadId: number; + readonly resourceLimits?: ResourceLimits; + readonly threadName: string | null; + /* with the stdout/stderr options: the worker's writes to those streams */ + readonly stdout: import("stream").Readable; + readonly stderr: import("stream").Readable; + /* with the stdin option: what the worker reads from process.stdin */ + readonly stdin: import("stream").Writable | null; + /* terminate(), resolving with undefined once the worker exited */ + [Symbol.asyncDispose](): Promise; + on(event: "online", listener: () => void): this; + on(event: "message", listener: (value: any) => void): this; + on(event: "error", listener: (error: any) => void): this; + on(event: "exit", listener: (code: number) => void): this; + on(event: string, listener: (...args: any[]) => void): this; + once(event: string, listener: (...args: any[]) => void): this; + off(event: string, listener: (...args: any[]) => void): this; + addListener(event: string, listener: (...args: any[]) => void): this; + removeListener(event: string, listener: (...args: any[]) => void): this; + prependListener(event: string, listener: (...args: any[]) => void): this; + prependOnceListener(event: string, listener: (...args: any[]) => void): this; + postMessage(value: unknown, transferList?: readonly unknown[]): void; + ref(): void; + unref(): void; + terminate(): Promise; + removeAllListeners(event?: string): this; + listenerCount(event: string, listener?: (...args: any[]) => void): number; + } + export class MessagePort { + on(event: "message", listener: (value: any) => void): this; + on(event: "close", listener: () => void): this; + on(event: string, listener: (...args: any[]) => void): this; + once(event: string, listener: (...args: any[]) => void): this; + off(event: string, listener: (...args: any[]) => void): this; + addListener(event: string, listener: (...args: any[]) => void): this; + removeListener(event: string, listener: (...args: any[]) => void): this; + prependListener(event: string, listener: (...args: any[]) => void): this; + prependOnceListener(event: string, listener: (...args: any[]) => void): this; + postMessage(value: unknown, transferList?: readonly unknown[]): void; + start(): void; + close(callback?: () => void): void; + ref(): void; + unref(): void; + hasRef(): boolean; + removeAllListeners(event?: string): this; + listenerCount(event: string, listener?: (...args: any[]) => void): number; + } + /* Two entangled ports; a port moves to another thread through a + * postMessage or workerData transferList. */ + export class MessageChannel { + readonly port1: MessagePort; + readonly port2: MessagePort; + } + export function receiveMessageOnPort(port: MessagePort): { message: any } | undefined; + /* This thread's environment data; each new Worker starts from a clone. */ + export function setEnvironmentData(key: any, value?: any): void; + export function getEnvironmentData(key: any): any; + /* A marked object cannot be listed in a transfer list. */ + export function markAsUntransferable(object: any): void; + /* structuredClone and postMessage throw DataCloneError for a marked + * object, error or class instance */ + export function markAsUncloneable(object: any): void; + export function isMarkedAsUntransferable(object: any): boolean; + /* Delivers each message to every other open channel with the name, in + * any thread, as a 'message' MessageEvent. */ + export class BroadcastChannel extends EventTarget { + constructor(name: string); + readonly name: string; + onmessage: ((event: MessageEvent) => void) | null; + onmessageerror: ((event: MessageEvent) => void) | null; + postMessage(message: unknown): void; + close(): void; + ref(): this; + unref(): this; + } + export const parentPort: MessagePort | null; + export const workerData: any; +} + +declare module "node:worker_threads" { + export * from "worker_threads"; +} diff --git a/packages/compiler/ambient/node-fallback/zlib.d.ts b/packages/compiler/ambient/node-fallback/zlib.d.ts new file mode 100644 index 000000000..c601596fe --- /dev/null +++ b/packages/compiler/ambient/node-fallback/zlib.d.ts @@ -0,0 +1,422 @@ +/// + +/* node:zlib — the one-shot zlib/raw/gzip codecs lower for string/Buffer + * inputs with Node's default options; libz links only into zlib-using + * binaries. Callback forms use the executable worker pool. Options stay + * declared so reached uses receive a named SC2020 refusal. The Deflate, + * Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, Unzip, BrotliCompress + * and BrotliDecompress streams are runtime Transform streams; Zstd remains + * outside the lowered surface. */ +declare module "zlib" { + import { Transform } from "stream"; + export interface ZlibOptions { + [option: string]: unknown; + } + export type ZlibCallback = (error: NodeJS.ErrnoException | null, result: Buffer) => void; + export function deflate(data: string | Uint8Array, callback: ZlibCallback): void; + export function deflate( + data: string | Uint8Array, + options: ZlibOptions, + callback: ZlibCallback, + ): void; + export function deflateSync(data: string | Uint8Array, options?: ZlibOptions): Buffer; + export function inflate(data: string | Uint8Array, callback: ZlibCallback): void; + export function inflate( + data: string | Uint8Array, + options: ZlibOptions, + callback: ZlibCallback, + ): void; + export function inflateSync(data: string | Uint8Array, options?: ZlibOptions): Buffer; + export function deflateRaw(data: string | Uint8Array, callback: ZlibCallback): void; + export function deflateRaw( + data: string | Uint8Array, + options: ZlibOptions, + callback: ZlibCallback, + ): void; + export function deflateRawSync(data: string | Uint8Array, options?: ZlibOptions): Buffer; + export function inflateRaw(data: string | Uint8Array, callback: ZlibCallback): void; + export function inflateRaw( + data: string | Uint8Array, + options: ZlibOptions, + callback: ZlibCallback, + ): void; + export function inflateRawSync(data: string | Uint8Array, options?: ZlibOptions): Buffer; + export function gzip(data: string | Uint8Array, callback: ZlibCallback): void; + export function gzip( + data: string | Uint8Array, + options: ZlibOptions, + callback: ZlibCallback, + ): void; + export function gzipSync(data: string | Uint8Array, options?: ZlibOptions): Buffer; + export function gunzip(data: string | Uint8Array, callback: ZlibCallback): void; + export function gunzip( + data: string | Uint8Array, + options: ZlibOptions, + callback: ZlibCallback, + ): void; + export function gunzipSync(data: string | Uint8Array, options?: ZlibOptions): Buffer; + export function unzip(data: string | Uint8Array, callback: ZlibCallback): void; + export function unzip( + data: string | Uint8Array, + options: ZlibOptions, + callback: ZlibCallback, + ): void; + export function unzipSync(data: string | Uint8Array, options?: ZlibOptions): Buffer; + export function crc32(data: string | Uint8Array, value?: number): number; + /* Brotli over the vendored brotli 1.2.0 (scr_brotli.c): `params` keys + * are the numeric BROTLI_PARAM_* and BROTLI_DECODER_PARAM_* constants. */ + export interface BrotliOptions { + [option: string]: unknown; + } + export function brotliCompress(data: string | Uint8Array, callback: ZlibCallback): void; + export function brotliCompress( + data: string | Uint8Array, + options: BrotliOptions, + callback: ZlibCallback, + ): void; + export function brotliCompressSync(data: string | Uint8Array, options?: BrotliOptions): Buffer; + export function brotliDecompress(data: string | Uint8Array, callback: ZlibCallback): void; + export function brotliDecompress( + data: string | Uint8Array, + options: BrotliOptions, + callback: ZlibCallback, + ): void; + export function brotliDecompressSync(data: string | Uint8Array, options?: BrotliOptions): Buffer; + /* The stream classes: runtime Transform streams over the zlib engine + * (scr_zlib_stream.c). createGzip(options) and new Gzip(options) build + * the same stream; options are validated at runtime with Node's rules. + * Brotli streams have no params(), as in Node. */ + class ZlibBase extends Transform { + readonly bytesWritten: number; + flush(kind?: number, callback?: () => void): void; + flush(callback: () => void): void; + close(callback?: () => void): void; + reset(): void; + } + class Zlib extends ZlibBase { + params(level: number, strategy: number, callback?: () => void): void; + } + export class BrotliCompress extends ZlibBase { + constructor(options?: BrotliOptions); + } + export class BrotliDecompress extends ZlibBase { + constructor(options?: BrotliOptions); + } + export function createBrotliCompress(options?: BrotliOptions): BrotliCompress; + export function createBrotliDecompress(options?: BrotliOptions): BrotliDecompress; + export class Deflate extends Zlib { + constructor(options?: ZlibOptions); + } + export class Inflate extends Zlib { + constructor(options?: ZlibOptions); + } + export class Gzip extends Zlib { + constructor(options?: ZlibOptions); + } + export class Gunzip extends Zlib { + constructor(options?: ZlibOptions); + } + export class DeflateRaw extends Zlib { + constructor(options?: ZlibOptions); + } + export class InflateRaw extends Zlib { + constructor(options?: ZlibOptions); + } + export class Unzip extends Zlib { + constructor(options?: ZlibOptions); + } + export function createDeflate(options?: ZlibOptions): Deflate; + export function createInflate(options?: ZlibOptions): Inflate; + export function createGzip(options?: ZlibOptions): Gzip; + export function createGunzip(options?: ZlibOptions): Gunzip; + export function createDeflateRaw(options?: ZlibOptions): DeflateRaw; + export function createInflateRaw(options?: ZlibOptions): InflateRaw; + export function createUnzip(options?: ZlibOptions): Unzip; + /* Node v24 zlib.constants, literal-typed (the CryptoConstants stance): + * every access bakes as its literal; the object never materializes. The + * members outside BROTLI_* are also module properties (zlib.Z_SYNC_FLUSH). */ + export interface ZlibConstants { + readonly Z_NO_FLUSH: 0; + readonly Z_PARTIAL_FLUSH: 1; + readonly Z_SYNC_FLUSH: 2; + readonly Z_FULL_FLUSH: 3; + readonly Z_FINISH: 4; + readonly Z_BLOCK: 5; + readonly Z_OK: 0; + readonly Z_STREAM_END: 1; + readonly Z_NEED_DICT: 2; + readonly Z_ERRNO: -1; + readonly Z_STREAM_ERROR: -2; + readonly Z_DATA_ERROR: -3; + readonly Z_MEM_ERROR: -4; + readonly Z_BUF_ERROR: -5; + readonly Z_VERSION_ERROR: -6; + readonly Z_NO_COMPRESSION: 0; + readonly Z_BEST_SPEED: 1; + readonly Z_BEST_COMPRESSION: 9; + readonly Z_DEFAULT_COMPRESSION: -1; + readonly Z_FILTERED: 1; + readonly Z_HUFFMAN_ONLY: 2; + readonly Z_RLE: 3; + readonly Z_FIXED: 4; + readonly Z_DEFAULT_STRATEGY: 0; + readonly ZLIB_VERNUM: 4897; + readonly DEFLATE: 1; + readonly INFLATE: 2; + readonly GZIP: 3; + readonly GUNZIP: 4; + readonly DEFLATERAW: 5; + readonly INFLATERAW: 6; + readonly UNZIP: 7; + readonly BROTLI_DECODE: 8; + readonly BROTLI_ENCODE: 9; + readonly ZSTD_DECOMPRESS: 11; + readonly ZSTD_COMPRESS: 10; + readonly Z_MIN_WINDOWBITS: 8; + readonly Z_MAX_WINDOWBITS: 15; + readonly Z_DEFAULT_WINDOWBITS: 15; + readonly Z_MIN_CHUNK: 64; + readonly Z_MAX_CHUNK: number; + readonly Z_DEFAULT_CHUNK: 16384; + readonly Z_MIN_MEMLEVEL: 1; + readonly Z_MAX_MEMLEVEL: 9; + readonly Z_DEFAULT_MEMLEVEL: 8; + readonly Z_MIN_LEVEL: -1; + readonly Z_MAX_LEVEL: 9; + readonly Z_DEFAULT_LEVEL: -1; + readonly BROTLI_OPERATION_PROCESS: 0; + readonly BROTLI_OPERATION_FLUSH: 1; + readonly BROTLI_OPERATION_FINISH: 2; + readonly BROTLI_OPERATION_EMIT_METADATA: 3; + readonly BROTLI_PARAM_MODE: 0; + readonly BROTLI_MODE_GENERIC: 0; + readonly BROTLI_MODE_TEXT: 1; + readonly BROTLI_MODE_FONT: 2; + readonly BROTLI_DEFAULT_MODE: 0; + readonly BROTLI_PARAM_QUALITY: 1; + readonly BROTLI_MIN_QUALITY: 0; + readonly BROTLI_MAX_QUALITY: 11; + readonly BROTLI_DEFAULT_QUALITY: 11; + readonly BROTLI_PARAM_LGWIN: 2; + readonly BROTLI_MIN_WINDOW_BITS: 10; + readonly BROTLI_MAX_WINDOW_BITS: 24; + readonly BROTLI_LARGE_MAX_WINDOW_BITS: 30; + readonly BROTLI_DEFAULT_WINDOW: 22; + readonly BROTLI_PARAM_LGBLOCK: 3; + readonly BROTLI_MIN_INPUT_BLOCK_BITS: 16; + readonly BROTLI_MAX_INPUT_BLOCK_BITS: 24; + readonly BROTLI_PARAM_DISABLE_LITERAL_CONTEXT_MODELING: 4; + readonly BROTLI_PARAM_SIZE_HINT: 5; + readonly BROTLI_PARAM_LARGE_WINDOW: 6; + readonly BROTLI_PARAM_NPOSTFIX: 7; + readonly BROTLI_PARAM_NDIRECT: 8; + readonly BROTLI_DECODER_RESULT_ERROR: 0; + readonly BROTLI_DECODER_RESULT_SUCCESS: 1; + readonly BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT: 2; + readonly BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT: 3; + readonly BROTLI_DECODER_PARAM_DISABLE_RING_BUFFER_REALLOCATION: 0; + readonly BROTLI_DECODER_PARAM_LARGE_WINDOW: 1; + readonly BROTLI_DECODER_NO_ERROR: 0; + readonly BROTLI_DECODER_SUCCESS: 1; + readonly BROTLI_DECODER_NEEDS_MORE_INPUT: 2; + readonly BROTLI_DECODER_NEEDS_MORE_OUTPUT: 3; + readonly BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_NIBBLE: -1; + readonly BROTLI_DECODER_ERROR_FORMAT_RESERVED: -2; + readonly BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_META_NIBBLE: -3; + readonly BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_ALPHABET: -4; + readonly BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_SAME: -5; + readonly BROTLI_DECODER_ERROR_FORMAT_CL_SPACE: -6; + readonly BROTLI_DECODER_ERROR_FORMAT_HUFFMAN_SPACE: -7; + readonly BROTLI_DECODER_ERROR_FORMAT_CONTEXT_MAP_REPEAT: -8; + readonly BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_1: -9; + readonly BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_2: -10; + readonly BROTLI_DECODER_ERROR_FORMAT_TRANSFORM: -11; + readonly BROTLI_DECODER_ERROR_FORMAT_DICTIONARY: -12; + readonly BROTLI_DECODER_ERROR_FORMAT_WINDOW_BITS: -13; + readonly BROTLI_DECODER_ERROR_FORMAT_PADDING_1: -14; + readonly BROTLI_DECODER_ERROR_FORMAT_PADDING_2: -15; + readonly BROTLI_DECODER_ERROR_FORMAT_DISTANCE: -16; + readonly BROTLI_DECODER_ERROR_DICTIONARY_NOT_SET: -19; + readonly BROTLI_DECODER_ERROR_INVALID_ARGUMENTS: -20; + readonly BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MODES: -21; + readonly BROTLI_DECODER_ERROR_ALLOC_TREE_GROUPS: -22; + readonly BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MAP: -25; + readonly BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_1: -26; + readonly BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_2: -27; + readonly BROTLI_DECODER_ERROR_ALLOC_BLOCK_TYPE_TREES: -30; + readonly BROTLI_DECODER_ERROR_UNREACHABLE: -31; + readonly ZSTD_e_continue: 0; + readonly ZSTD_e_flush: 1; + readonly ZSTD_e_end: 2; + readonly ZSTD_fast: 1; + readonly ZSTD_dfast: 2; + readonly ZSTD_greedy: 3; + readonly ZSTD_lazy: 4; + readonly ZSTD_lazy2: 5; + readonly ZSTD_btlazy2: 6; + readonly ZSTD_btopt: 7; + readonly ZSTD_btultra: 8; + readonly ZSTD_btultra2: 9; + readonly ZSTD_c_compressionLevel: 100; + readonly ZSTD_c_windowLog: 101; + readonly ZSTD_c_hashLog: 102; + readonly ZSTD_c_chainLog: 103; + readonly ZSTD_c_searchLog: 104; + readonly ZSTD_c_minMatch: 105; + readonly ZSTD_c_targetLength: 106; + readonly ZSTD_c_strategy: 107; + readonly ZSTD_c_enableLongDistanceMatching: 160; + readonly ZSTD_c_ldmHashLog: 161; + readonly ZSTD_c_ldmMinMatch: 162; + readonly ZSTD_c_ldmBucketSizeLog: 163; + readonly ZSTD_c_ldmHashRateLog: 164; + readonly ZSTD_c_contentSizeFlag: 200; + readonly ZSTD_c_checksumFlag: 201; + readonly ZSTD_c_dictIDFlag: 202; + readonly ZSTD_c_nbWorkers: 400; + readonly ZSTD_c_jobSize: 401; + readonly ZSTD_c_overlapLog: 402; + readonly ZSTD_d_windowLogMax: 100; + readonly ZSTD_CLEVEL_DEFAULT: 3; + readonly ZSTD_error_no_error: 0; + readonly ZSTD_error_GENERIC: 1; + readonly ZSTD_error_prefix_unknown: 10; + readonly ZSTD_error_version_unsupported: 12; + readonly ZSTD_error_frameParameter_unsupported: 14; + readonly ZSTD_error_frameParameter_windowTooLarge: 16; + readonly ZSTD_error_corruption_detected: 20; + readonly ZSTD_error_checksum_wrong: 22; + readonly ZSTD_error_literals_headerWrong: 24; + readonly ZSTD_error_dictionary_corrupted: 30; + readonly ZSTD_error_dictionary_wrong: 32; + readonly ZSTD_error_dictionaryCreation_failed: 34; + readonly ZSTD_error_parameter_unsupported: 40; + readonly ZSTD_error_parameter_combination_unsupported: 41; + readonly ZSTD_error_parameter_outOfBound: 42; + readonly ZSTD_error_tableLog_tooLarge: 44; + readonly ZSTD_error_maxSymbolValue_tooLarge: 46; + readonly ZSTD_error_maxSymbolValue_tooSmall: 48; + readonly ZSTD_error_stabilityCondition_notRespected: 50; + readonly ZSTD_error_stage_wrong: 60; + readonly ZSTD_error_init_missing: 62; + readonly ZSTD_error_memory_allocation: 64; + readonly ZSTD_error_workSpace_tooSmall: 66; + readonly ZSTD_error_dstSize_tooSmall: 70; + readonly ZSTD_error_srcSize_wrong: 72; + readonly ZSTD_error_dstBuffer_null: 74; + readonly ZSTD_error_noForwardProgress_destFull: 80; + readonly ZSTD_error_noForwardProgress_inputEmpty: 82; + } + export const constants: ZlibConstants; + export const Z_NO_FLUSH: 0; + export const Z_PARTIAL_FLUSH: 1; + export const Z_SYNC_FLUSH: 2; + export const Z_FULL_FLUSH: 3; + export const Z_FINISH: 4; + export const Z_BLOCK: 5; + export const Z_OK: 0; + export const Z_STREAM_END: 1; + export const Z_NEED_DICT: 2; + export const Z_ERRNO: -1; + export const Z_STREAM_ERROR: -2; + export const Z_DATA_ERROR: -3; + export const Z_MEM_ERROR: -4; + export const Z_BUF_ERROR: -5; + export const Z_VERSION_ERROR: -6; + export const Z_NO_COMPRESSION: 0; + export const Z_BEST_SPEED: 1; + export const Z_BEST_COMPRESSION: 9; + export const Z_DEFAULT_COMPRESSION: -1; + export const Z_FILTERED: 1; + export const Z_HUFFMAN_ONLY: 2; + export const Z_RLE: 3; + export const Z_FIXED: 4; + export const Z_DEFAULT_STRATEGY: 0; + export const ZLIB_VERNUM: 4897; + export const DEFLATE: 1; + export const INFLATE: 2; + export const GZIP: 3; + export const GUNZIP: 4; + export const DEFLATERAW: 5; + export const INFLATERAW: 6; + export const UNZIP: 7; + export const ZSTD_DECOMPRESS: 11; + export const ZSTD_COMPRESS: 10; + export const Z_MIN_WINDOWBITS: 8; + export const Z_MAX_WINDOWBITS: 15; + export const Z_DEFAULT_WINDOWBITS: 15; + export const Z_MIN_CHUNK: 64; + export const Z_MAX_CHUNK: number; + export const Z_DEFAULT_CHUNK: 16384; + export const Z_MIN_MEMLEVEL: 1; + export const Z_MAX_MEMLEVEL: 9; + export const Z_DEFAULT_MEMLEVEL: 8; + export const Z_MIN_LEVEL: -1; + export const Z_MAX_LEVEL: 9; + export const Z_DEFAULT_LEVEL: -1; + export const ZSTD_e_continue: 0; + export const ZSTD_e_flush: 1; + export const ZSTD_e_end: 2; + export const ZSTD_fast: 1; + export const ZSTD_dfast: 2; + export const ZSTD_greedy: 3; + export const ZSTD_lazy: 4; + export const ZSTD_lazy2: 5; + export const ZSTD_btlazy2: 6; + export const ZSTD_btopt: 7; + export const ZSTD_btultra: 8; + export const ZSTD_btultra2: 9; + export const ZSTD_c_compressionLevel: 100; + export const ZSTD_c_windowLog: 101; + export const ZSTD_c_hashLog: 102; + export const ZSTD_c_chainLog: 103; + export const ZSTD_c_searchLog: 104; + export const ZSTD_c_minMatch: 105; + export const ZSTD_c_targetLength: 106; + export const ZSTD_c_strategy: 107; + export const ZSTD_c_enableLongDistanceMatching: 160; + export const ZSTD_c_ldmHashLog: 161; + export const ZSTD_c_ldmMinMatch: 162; + export const ZSTD_c_ldmBucketSizeLog: 163; + export const ZSTD_c_ldmHashRateLog: 164; + export const ZSTD_c_contentSizeFlag: 200; + export const ZSTD_c_checksumFlag: 201; + export const ZSTD_c_dictIDFlag: 202; + export const ZSTD_c_nbWorkers: 400; + export const ZSTD_c_jobSize: 401; + export const ZSTD_c_overlapLog: 402; + export const ZSTD_d_windowLogMax: 100; + export const ZSTD_CLEVEL_DEFAULT: 3; + export const ZSTD_error_no_error: 0; + export const ZSTD_error_GENERIC: 1; + export const ZSTD_error_prefix_unknown: 10; + export const ZSTD_error_version_unsupported: 12; + export const ZSTD_error_frameParameter_unsupported: 14; + export const ZSTD_error_frameParameter_windowTooLarge: 16; + export const ZSTD_error_corruption_detected: 20; + export const ZSTD_error_checksum_wrong: 22; + export const ZSTD_error_literals_headerWrong: 24; + export const ZSTD_error_dictionary_corrupted: 30; + export const ZSTD_error_dictionary_wrong: 32; + export const ZSTD_error_dictionaryCreation_failed: 34; + export const ZSTD_error_parameter_unsupported: 40; + export const ZSTD_error_parameter_combination_unsupported: 41; + export const ZSTD_error_parameter_outOfBound: 42; + export const ZSTD_error_tableLog_tooLarge: 44; + export const ZSTD_error_maxSymbolValue_tooLarge: 46; + export const ZSTD_error_maxSymbolValue_tooSmall: 48; + export const ZSTD_error_stabilityCondition_notRespected: 50; + export const ZSTD_error_stage_wrong: 60; + export const ZSTD_error_init_missing: 62; + export const ZSTD_error_memory_allocation: 64; + export const ZSTD_error_workSpace_tooSmall: 66; + export const ZSTD_error_dstSize_tooSmall: 70; + export const ZSTD_error_srcSize_wrong: 72; + export const ZSTD_error_dstBuffer_null: 74; + export const ZSTD_error_noForwardProgress_destFull: 80; + export const ZSTD_error_noForwardProgress_inputEmpty: 82; +} + +declare module "node:zlib" { + export * from "zlib"; +} diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index d7abe70df..dff6b6b05 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -416,17 +416,6 @@ declare var process: { once(event: "disconnect", listener: () => void): void; }; -/* Node's process module default export is the global process object. These - * declaration aliases let the ESM spelling typecheck against the same - * deliberately narrow fallback surface when @types/node is absent. */ -declare module "process" { - export = process; -} -declare module "node:process" { - import process = require("process"); - export = process; -} - /* ── globals a real CLI's sources reference (all @types/node or dyn-lib * territory; the fallback declares the slice so projects PREFLIGHT and * every reached use lands on the SC2020 fence — or a real lowering where @@ -917,6 +906,14 @@ interface Buffer< readBigUint64LE(offset?: number): bigint; } interface BufferConstructor { + /** @deprecated since v10.0.0 - Use `Buffer.from(string[, encoding])` instead. */ + new (str: string, encoding?: BufferEncoding): Buffer; + /** @deprecated since v10.0.0 - Use `Buffer.alloc()` instead. */ + new (size: number): Buffer; + /** @deprecated since v10.0.0 - Use `Buffer.from(array)` instead. */ + new (array: ArrayLike): Buffer; + /** @deprecated since v10.0.0 - Use `Buffer.from(arrayBuffer[, byteOffset[, length]])` instead. */ + new (arrayBuffer: ArrayBuffer | SharedArrayBuffer): Buffer; from(data: string, encoding?: BufferEncoding): Buffer; /* The ArrayBuffer form is the VIEW construction: x.buffer with an * optional byte offset/length shares x's storage (the subarray rule). */ @@ -1106,6 +1103,34 @@ declare var ReadableStream: { from(iterable: string): ReadableStream; readonly prototype: ReadableStream; }; +/* The writable side of a TransformStream and its writer. The + * WritableStream constructor has no native lowering; writable streams come + * from `new TransformStream()`. */ +interface WritableStreamDefaultWriter { + readonly closed: Promise; + readonly desiredSize: number | null; + readonly ready: Promise; + abort(reason?: unknown): Promise; + close(): Promise; + releaseLock(): void; + write(chunk?: W): Promise; +} +interface WritableStream { + readonly locked: boolean; + abort(reason?: unknown): Promise; + close(): Promise; + getWriter(): WritableStreamDefaultWriter; +} +/* The identity transform: chunks written to `writable` are read from + * `readable`, and a write completes when its chunk is read. */ +interface TransformStream { + readonly readable: ReadableStream; + readonly writable: WritableStream; +} +declare var TransformStream: { + new (): TransformStream; + readonly prototype: TransformStream; +}; /* fetch, typed as the native JSON + readable-stream slice: one URL, an * optional init with the members CLI requests carry, and a Response @@ -1256,150 +1281,6 @@ declare function setInterval( ): Timeout; declare function clearInterval(handle?: Timeout | number | null | undefined): void; -/* node:timers — the module twins of the timer globals (Node's timers - * module re-exports them: require('timers').setTimeout IS setTimeout). - * Named imports, destructured requires, and namespace calls all lower to - * the same libCalls as the globals; the surface beyond these six - * (enroll/unenroll, promises) fences per member. */ -declare module "node:timers" { - export function setTimeout(callback: () => void, ms?: number): Timeout; - export function setTimeout( - callback: (...args: never[]) => void, - ms?: number, - ...args: unknown[] - ): Timeout; - export function clearTimeout(handle?: Timeout | number | null | undefined): void; - export function setInterval(callback: () => void, ms?: number): Timeout; - export function setInterval( - callback: (...args: never[]) => void, - ms?: number, - ...args: unknown[] - ): Timeout; - export function clearInterval(handle?: Timeout | number | null | undefined): void; - export function setImmediate(callback: () => void): Immediate; - export function setImmediate(callback: (...args: never[]) => void, ...args: unknown[]): Immediate; - export function clearImmediate(handle?: Immediate | null | undefined): void; -} -declare module "timers" { - export * from "node:timers"; -} - -/* node:timers/promises — the promisified pair the Node test harness leans - * on (`await setTimeout(ms)` as a sleep). setTimeout and setImmediate - * settle with their resolve value (undefined without one) from the shared - * timer heap and the immediate queue; setInterval(delay, value) lowers to a - * typed async iterator. The options object takes `ref`; AbortSignal - * options remain declared surface that fences per site. */ -declare module "node:timers/promises" { - export interface TimerOptions { - ref?: boolean; - signal?: AbortSignal; - } - export function setTimeout( - delay?: number, - value?: T, - options?: TimerOptions, - ): Promise; - export function setImmediate(value?: T, options?: TimerOptions): Promise; - export function setInterval( - delay?: number, - value?: T, - options?: unknown, - ): AsyncGenerator; - export const scheduler: { - wait(delay?: number, options?: TimerOptions): Promise; - yield(): Promise; - }; -} -declare module "timers/promises" { - export * from "node:timers/promises"; -} - -/* node:diagnostics_channel — the in-process pub/sub core: named channels, - * subscribe/unsubscribe (module-level and channel-level), publish with any - * dyn-convertible message. Channel values are f64 handles into the - * runtime's channel registry (the readline.Interface precedent — channels - * are process-lived in Node too, its WeakRef machinery aside). - * The store-binding surface (bindStore/unbindStore/runStores) lowers onto - * the AsyncLocalStorage integration (dc.chan* libCalls). - * - * TracingChannel (tracingChannel) is the five-event-channel collection: - * an f64 handle into the runtime's tracing registry (type-mapper.ts maps the - * name like Channel). subscribe/unsubscribe walk a handlers object; - * traceSync/traceCallback run Node's publish choreography in the runtime - * (context defaults to a fresh {}); tracePromise is declared (the member - * exists in Node) but fences at the call site — the traced function's - * promise cannot cross the checked-dynamic tree boundary yet. */ -declare module "node:diagnostics_channel" { - export type ChannelListener = (message: unknown, name: string) => void; - export interface Channel { - readonly name: string; - readonly hasSubscribers: boolean; - publish(message: unknown): void; - subscribe(onMessage: ChannelListener): void; - unsubscribe(onMessage: ChannelListener): boolean; - bindStore( - store: import("async_hooks").AsyncLocalStorage, - transform?: (context: any) => any, - ): void; - unbindStore(store: import("async_hooks").AsyncLocalStorage): boolean; - runStores(context: unknown, fn: (...args: any[]) => any, thisArg?: any, ...args: any[]): any; - } - export function channel(name: string): Channel; - export function subscribe(name: string, onMessage: ChannelListener): void; - export function unsubscribe(name: string, onMessage: ChannelListener): boolean; - export function hasSubscribers(name: string): boolean; - export interface TracingChannelSubscribers { - start?: (message: any) => void; - end?: (message: any) => void; - asyncStart?: (message: any) => void; - asyncEnd?: (message: any) => void; - error?: (message: any) => void; - } - export interface TracingChannelCollection { - start: Channel; - end: Channel; - asyncStart: Channel; - asyncEnd: Channel; - error: Channel; - } - export interface TracingChannel { - start: Channel; - end: Channel; - asyncStart: Channel; - asyncEnd: Channel; - error: Channel; - readonly hasSubscribers: boolean; - subscribe(subscribers: TracingChannelSubscribers): void; - /** True when every present handler was found and removed (Node's - * all-found conjunction; @types/node under-declares this as void). */ - unsubscribe(subscribers: TracingChannelSubscribers): boolean; - traceSync( - fn: (this: ThisArg, ...args: Args) => Result, - context?: object, - thisArg?: ThisArg, - ...args: Args - ): Result; - tracePromise( - fn: (this: ThisArg, ...args: Args) => Promise, - context?: object, - thisArg?: ThisArg, - ...args: Args - ): Promise; - traceCallback( - fn: (this: ThisArg, ...args: Args) => Result, - position?: number, - context?: object, - thisArg?: ThisArg, - ...args: Args - ): Result; - } - export function tracingChannel(nameOrChannels: string | TracingChannelCollection): TracingChannel; -} -declare module "diagnostics_channel" { - export * from "node:diagnostics_channel"; -} - /* node:perf_hooks — performance.now() over the runtime's process-start- * anchored monotonic clock (Node's timeOrigin for a compiled program), * fractional milliseconds. The .bind(performance) spelling lowers to the @@ -1489,15 +1370,6 @@ declare module "perf_hooks" { * performance.now() either way. */ declare var performance: import("node:perf_hooks").Performance; -declare module "node:module" { - export function createRequire(filename: string | URL): ScriptcRequire; - export const builtinModules: string[]; - export function isBuiltin(moduleName: string): boolean; - export function syncBuiltinESMExports(): void; -} -declare module "module" { - export * from "node:module"; -} /* import.meta: module-loader metadata with no value representation — * direct file-backed fields lower to constants, while the object and * remaining members fence (SC1090) EXCEPT as createRequire's base, where it @@ -1512,1184 +1384,6 @@ interface ImportMeta { resolve(specifier: string, parent?: string | URL): string; } -/* The synchronous node:fs surface — utf8-only, no options objects, no - * Buffers. Importing "node:fs" resolves here (preflight allowlists exactly - * the ambient-declared node: modules); every function lowers to a `libCall` - * with a scr_fs_* runtime implementation. Failures throw CATCHABLE string - * values formatted like Node's error messages ("ENOENT: no such file or - * directory, open 'x'"). `rmSync` removes FILES only (like Node without - * `recursive`); empty directories go through `rmdirSync`. */ -declare module "node:fs" { - import { Readable, Writable } from "stream"; - export function readFileSync(path: string | number | URL, encoding: BufferEncoding): string; - export function readFileSync( - path: string | number | URL, - options: { encoding: BufferEncoding }, - ): string; - export function readFileSync(path: string | number | URL): Buffer; - export function readFileSync(path: string | URL, options: unknown): string | Buffer; - /* Error-first callbacks select string or Buffer data from their encoding. - * Unknown options retain that union until the caller validates the value. */ - export function readFile( - path: string, - options: unknown, - callback: (error: Error | null, data: string | Buffer) => void, - ): void; - /* The options form carries the mode (applied at CREATION only, like - * Node — an existing file keeps its permissions) and/or the utf8 - * encoding spelling. */ - export function writeFileSync( - path: string | URL, - data: string, - /* The options-record stance (see http.RequestOptions): flag fences - * by name at the call site; undocumented keys drop like Node. */ - options?: { - mode?: number; - encoding?: "utf8" | "utf-8"; - flag?: string; - [option: string]: unknown; - }, - ): void; - /* The bare-encoding spelling — the options record's encoding key alone. */ - export function writeFileSync(path: string | URL, data: string, encoding: "utf8" | "utf-8"): void; - export function writeFileSync(path: string | URL, data: string | Uint8Array): void; - export function appendFileSync(path: string | URL, data: string | Uint8Array): void; - export function existsSync(path: string): boolean; - export function mkdirSync(path: string | URL): void; - export function mkdirSync( - path: string | URL, - options: { recursive?: boolean; mode?: number }, - ): void; - export function unlinkSync(path: string | URL): void; - export function chmodSync(path: string | URL, mode: number): void; - export function fchmodSync(fd: number, mode: number): void; - export function fsyncSync(fd: number): void; - export function linkSync( - existingPath: string | Uint8Array | URL, - newPath: string | Uint8Array | URL, - ): void; - export function symlinkSync( - target: string | Uint8Array | URL, - path: string | Uint8Array | URL, - type?: string | null, - ): void; - /* fs.cpSync / fs.cp: copies a file, symlink or (with recursive) a - * directory tree. filter decides per path; the callback and promise - * forms complete the copy before reporting. */ - export interface CopySyncOptions { - dereference?: boolean; - errorOnExist?: boolean; - filter?: (source: string, destination: string) => boolean; - force?: boolean; - mode?: number; - preserveTimestamps?: boolean; - recursive?: boolean; - verbatimSymlinks?: boolean; - } - export interface CopyOptions { - dereference?: boolean; - errorOnExist?: boolean; - filter?: (source: string, destination: string) => boolean | Promise; - force?: boolean; - mode?: number; - preserveTimestamps?: boolean; - recursive?: boolean; - verbatimSymlinks?: boolean; - } - export function cpSync( - source: string | URL, - destination: string | URL, - options?: CopySyncOptions, - ): void; - export function cp( - source: string | URL, - destination: string | URL, - callback: (err: NodeJS.ErrnoException | null) => void, - ): void; - export function cp( - source: string | URL, - destination: string | URL, - options: CopyOptions, - callback: (err: NodeJS.ErrnoException | null) => void, - ): void; - export function symlink( - target: string | Uint8Array | URL, - path: string | Uint8Array | URL, - callback: (err: NodeJS.ErrnoException | null) => void, - ): void; - export function symlink( - target: string | Uint8Array | URL, - path: string | Uint8Array | URL, - type: string | null | undefined, - callback: (err: NodeJS.ErrnoException | null) => void, - ): void; - export function link( - existingPath: string | Uint8Array | URL, - newPath: string | Uint8Array | URL, - callback: (err: NodeJS.ErrnoException | null) => void, - ): void; - export function readlink( - path: string | Uint8Array | URL, - callback: (err: NodeJS.ErrnoException | null, target: string) => void, - ): void; - export function readlink( - path: string | Uint8Array | URL, - options: "buffer" | { encoding: "buffer" }, - callback: (err: NodeJS.ErrnoException | null, target: Buffer) => void, - ): void; - export function readlink( - path: string | Uint8Array | URL, - options: BufferEncoding | { encoding?: BufferEncoding | null } | null, - callback: (err: NodeJS.ErrnoException | null, target: string) => void, - ): void; - export function readlink( - path: string | Uint8Array | URL, - options: string | { encoding?: string | null } | null, - callback: (err: NodeJS.ErrnoException | null, target: string | Buffer) => void, - ): void; - export function readlinkSync( - path: string | Uint8Array | URL, - options: "buffer" | { encoding: "buffer" }, - ): Buffer; - export function readlinkSync( - path: string | Uint8Array | URL, - options?: BufferEncoding | { encoding?: BufferEncoding | null } | null, - ): string; - export function readlinkSync( - path: string | Uint8Array | URL, - options: string | { encoding?: string | null } | null, - ): string | Buffer; - export function chownSync(path: string | URL, uid: number, gid: number): void; - export function lchownSync(path: string | URL, uid: number, gid: number): void; - export function fchownSync(fd: number, uid: number, gid: number): void; - export function truncateSync(path: string | URL, len?: number): void; - /* The 2-argument form only (Node's mode flags have no lowering). The - * destination is created or truncated carrying the SOURCE's mode. */ - export function copyFileSync(src: string | URL, dest: string | URL): void; - export function rename( - oldPath: string, - newPath: string, - callback: (err: NodeJS.ErrnoException | null) => void, - ): void; - export function renameSync(oldPath: string | URL, newPath: string | URL): void; - export function rmSync(path: string | URL): void; - /* maxRetries/retryDelay are accepted no-ops (Node retries around - * EBUSY-class races; the synchronous lowering has no re-entrancy that - * needs them). */ - export function rmSync( - path: string | URL, - options: { recursive?: boolean; force?: boolean; maxRetries?: number; retryDelay?: number }, - ): void; - export function rmdirSync(path: string | URL): void; - export function readdirSync(path: string | URL): 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 - * carries the entry kind; isFile/isDirectory/isSymbolicLink lower as - * reads of it in builtins/process.ts). */ - export interface Dirent { - name: string; - parentPath: string; - isFile(): boolean; - isDirectory(): boolean; - isSymbolicLink(): boolean; - } - export function readdirSync( - path: string | URL, - /* The options-record stance (see http.RequestOptions): recursive - * fences by name at the call site; undocumented keys drop like Node. */ - options: { withFileTypes: true; recursive?: boolean; [option: string]: unknown }, - ): Dirent[]; - /* fs.globSync / fs.glob: Node's glob walk over the file system. Patterns - * are minimatch globs (case-insensitive on macOS and Windows); results - * are relative to cwd unless the pattern is absolute. withFileTypes must - * be a literal so the result type is known. */ - export interface GlobOptions { - cwd?: string | URL; - exclude?: readonly string[] | ((fileName: string) => boolean); - followSymlinks?: boolean; - withFileTypes?: false; - } - export interface GlobOptionsWithFileTypes { - cwd?: string | URL; - exclude?: readonly string[]; - followSymlinks?: boolean; - withFileTypes: true; - } - export function globSync(pattern: string | readonly string[], options?: GlobOptions): string[]; - export function globSync( - pattern: string | readonly string[], - options: GlobOptionsWithFileTypes, - ): Dirent[]; - export function glob( - pattern: string | readonly string[], - callback: (err: NodeJS.ErrnoException | null, matches?: string[]) => void, - ): void; - export function glob( - pattern: string | readonly string[], - options: GlobOptions, - callback: (err: NodeJS.ErrnoException | null, matches?: string[]) => void, - ): void; - /* fs.Dir: an open directory stream (opendirSync, fs.promises.opendir) - * read one Dirent at a time in OS order; utf8 names, no recursive walk. */ - export interface Dir { - readonly path: string; - readSync(): Dirent | null; - read(): Promise; - closeSync(): void; - close(): Promise; - [Symbol.asyncIterator](): AsyncIterableIterator; - } - export interface OpenDirOptions { - encoding?: "utf8" | "utf-8"; - bufferSize?: number; - } - export function opendirSync(path: string | URL, options?: OpenDirOptions): Dir; - /* fs.ReadStream / fs.WriteStream: runtime Readable and Writable streams - * over a file (scr_fs_stream.c). The options are checked at runtime - * with Node's rules; flags and mode are checked when the file opens, so - * their errors arrive as 'error' events. Instances also emit 'open' - * (with the fd) and 'ready'. */ - export interface StreamOptions { - flags?: string; - encoding?: BufferEncoding; - fd?: number; - mode?: number | string; - autoClose?: boolean; - emitClose?: boolean; - start?: number; - highWaterMark?: number; - } - export interface ReadStreamOptions extends StreamOptions { - end?: number; - } - export interface WriteStreamOptions extends StreamOptions { - flush?: boolean; - } - export class ReadStream extends Readable { - readonly bytesRead: number; - readonly path: string | Buffer; - readonly pending: boolean; - close(callback?: () => void): void; - } - export class WriteStream extends Writable { - readonly bytesWritten: number; - readonly path: string | Buffer; - readonly pending: boolean; - close(callback?: () => void): void; - } - export function createReadStream( - path: string | Buffer | URL, - options?: BufferEncoding | ReadStreamOptions, - ): ReadStream; - export function createWriteStream( - path: string | Buffer | URL, - options?: BufferEncoding | WriteStreamOptions, - ): WriteStream; - export function accessSync(path: string | URL, mode?: number): void; - export function mkdtempSync(prefix: string | URL): string; - export const constants: { - readonly F_OK: number; - readonly R_OK: number; - readonly W_OK: number; - readonly X_OK: number; - readonly O_RDONLY: number; - readonly O_WRONLY: number; - readonly O_RDWR: number; - readonly O_CREAT: number; - readonly O_EXCL: number; - readonly O_NOFOLLOW: number; - readonly O_NONBLOCK: number; - readonly O_TRUNC: number; - readonly O_APPEND: number; - readonly COPYFILE_EXCL: number; - readonly COPYFILE_FICLONE: number; - readonly COPYFILE_FICLONE_FORCE: number; - readonly UV_FS_SYMLINK_DIR: number; - readonly UV_FS_SYMLINK_JUNCTION: number; - readonly UV_DIRENT_UNKNOWN: number; - readonly UV_DIRENT_FILE: number; - readonly UV_DIRENT_DIR: number; - readonly UV_DIRENT_LINK: number; - readonly UV_DIRENT_FIFO: number; - readonly UV_DIRENT_SOCKET: number; - readonly UV_DIRENT_CHAR: number; - readonly UV_DIRENT_BLOCK: number; - readonly UV_FS_O_FILEMAP: number; - readonly UV_FS_O_TEMPORARY: number; - readonly UV_FS_O_SHORT_LIVED: number; - readonly UV_FS_O_SEQUENTIAL: number; - readonly UV_FS_O_RANDOM: number; - readonly UV_FS_COPYFILE_EXCL: number; - readonly UV_FS_COPYFILE_FICLONE: number; - readonly UV_FS_COPYFILE_FICLONE_FORCE: number; - readonly S_IFMT: number; - readonly S_IFREG: number; - readonly S_IFDIR: number; - readonly S_IFCHR: number; - readonly S_IFBLK: number; - readonly S_IFIFO: number; - readonly S_IFLNK: number; - readonly S_IFSOCK: number; - readonly O_NOCTTY: number; - readonly O_DIRECTORY: number; - readonly O_NOATIME: number; - readonly O_SYNC: number; - readonly O_DSYNC: number; - readonly O_SYMLINK: number; - readonly O_DIRECT: number; - readonly S_IRWXU: number; - readonly S_IRUSR: number; - readonly S_IWUSR: number; - readonly S_IXUSR: number; - readonly S_IRWXG: number; - readonly S_IRGRP: number; - readonly S_IWGRP: number; - readonly S_IXGRP: number; - readonly S_IRWXO: number; - readonly S_IROTH: number; - readonly S_IWOTH: number; - readonly S_IXOTH: number; - }; - /* A stat(2) snapshot (statSync follows symlinks, lstatSync does not — - * Node's split) — immutable; the supported surface is exactly these - * members. dev/ino identify the filesystem entry, blocks is the allocated - * size in 512-byte units, and the time fields are milliseconds with their - * sub-second fractions. */ - export interface Stats { - isFile(): boolean; - isDirectory(): boolean; - isSymbolicLink(): boolean; - isBlockDevice(): boolean; - isCharacterDevice(): boolean; - isFIFO(): boolean; - isSocket(): boolean; - readonly dev: number; - readonly mode: number; - readonly nlink: number; - readonly uid: number; - readonly gid: number; - readonly rdev: number; - readonly blksize: number; - readonly ino: number; - readonly size: number; - readonly blocks: number; - readonly atimeMs: number; - readonly mtimeMs: number; - readonly ctimeMs: number; - readonly birthtimeMs: number; - readonly atime: Date; - readonly mtime: Date; - readonly ctime: Date; - readonly birthtime: Date; - } - /* throwIfNoEntry: false answers undefined for a missing entry (ENOENT or - * ENOTDIR) instead of throwing. BigIntStats (bigint: true) has no - * lowering. */ - export interface StatSyncOptions { - bigint?: boolean; - throwIfNoEntry?: boolean; - } - export function statSync(path: string | URL, options?: undefined): Stats; - export function statSync( - path: string | URL, - options: StatSyncOptions & { bigint?: false; throwIfNoEntry: false }, - ): Stats | undefined; - export function statSync( - path: string | URL, - options: StatSyncOptions & { bigint?: false; throwIfNoEntry?: true }, - ): Stats; - /* bigint: true type-checks so the lowering can name it as unsupported. */ - export function statSync(path: string | URL, options: StatSyncOptions & { bigint: true }): Stats; - export function statSync(path: string | URL, options: StatSyncOptions): Stats | undefined; - export function lstatSync(path: string | URL, options?: undefined): Stats; - export function lstatSync( - path: string | URL, - options: StatSyncOptions & { bigint?: false; throwIfNoEntry: false }, - ): Stats | undefined; - export function lstatSync( - path: string | URL, - options: StatSyncOptions & { bigint?: false; throwIfNoEntry?: true }, - ): Stats; - /* bigint: true type-checks so the lowering can name it as unsupported. */ - export function lstatSync(path: string | URL, options: StatSyncOptions & { bigint: true }): Stats; - export function lstatSync(path: string | URL, options: StatSyncOptions): Stats | undefined; - /* The fd pair behind spawn's fd-stdio form: openSync(path, flags) → - * the raw fd. Numeric flags use an inline OR of the O_* constants. */ - export function openSync(path: string | URL, flags: string): number; - export function openSync(path: string | URL, flags: number, mode?: number): number; - export function closeSync(fd: number): void; - export function fdatasyncSync(fd: number): void; - export function ftruncateSync(fd: number, len?: number): void; - export function utimesSync( - path: string | Uint8Array | URL, - atime: number | string | Date, - mtime: number | string | Date, - ): void; - export function futimesSync( - fd: number, - atime: number | string | Date, - mtime: number | string | Date, - ): void; - export function lutimesSync( - path: string | Uint8Array | URL, - atime: number | string | Date, - mtime: number | string | Date, - ): void; - export function utimes( - path: string | Uint8Array | URL, - atime: number | string | Date, - mtime: number | string | Date, - callback: (err: NodeJS.ErrnoException | null) => void, - ): void; - export function futimes( - fd: number, - atime: number | string | Date, - mtime: number | string | Date, - callback: (err: NodeJS.ErrnoException | null) => void, - ): void; - export function lutimes( - path: string | Uint8Array | URL, - atime: number | string | Date, - mtime: number | string | Date, - callback: (err: NodeJS.ErrnoException | null) => void, - ): void; - export function readvSync(fd: number, buffers: Uint8Array[], position?: number | null): number; - export function writevSync(fd: number, buffers: Uint8Array[], position?: number | null): number; - /* Read into a caller buffer from the fd's current position when position - * is omitted/null, or from a numeric byte position without advancing the - * fd. Answers the byte count, 0 at EOF. */ - export interface ReadSyncOptions { - offset?: number; - length?: number; - position?: number | null; - } - export function readSync(fd: number, buffer: Uint8Array, options?: ReadSyncOptions): number; - export function readSync( - fd: number, - buffer: Uint8Array, - offset: number, - length: number, - position?: number | null, - ): number; - /* Write a caller-buffer window, or a utf8 string, at the fd's current - * position when position is omitted/null (advancing it), or at a numeric - * byte position without advancing it. Answers the byte count. */ - export interface WriteSyncOptions { - offset?: number; - length?: number; - position?: number | null; - } - export function writeSync(fd: number, buffer: Uint8Array, options: WriteSyncOptions): number; - export function writeSync( - fd: number, - buffer: Uint8Array, - offset?: number, - length?: number, - position?: number | null, - ): number; - export function writeSync( - fd: number, - string: string, - position?: number | null, - encoding?: "utf8" | "utf-8", - ): number; - /* statSync over an open fd — the same Stats snapshot. */ - export function fstatSync(fd: number): Stats; - /* realpath(3) — resolves symlinks, `.`/`..`, throwing Node's fs error - * shapes for missing paths. */ - export function realpathSync(path: string | URL): string; - export namespace realpathSync { - function native(path: string | URL): string; - } - /* statfs(2)/statvfs(3) — the filesystem-capacity snapshot (the fields - * Node's statfsSync reports; bavail × bsize is the free-space probe). */ - export interface StatsFsBase { - type: T; - bsize: T; - blocks: T; - bfree: T; - bavail: T; - files: T; - ffree: T; - } - export interface StatsFs extends StatsFsBase {} - export interface BigIntStatsFs extends StatsFsBase {} - export interface StatFsOptions { - bigint?: boolean; - } - export function statfsSync( - path: string | Uint8Array | URL, - options?: StatFsOptions & { bigint?: false }, - ): StatsFs; - export function statfsSync( - path: string | Uint8Array | URL, - options: StatFsOptions & { bigint: true }, - ): BigIntStatsFs; - export function statfsSync( - path: string | Uint8Array | URL, - options?: StatFsOptions, - ): StatsFs | BigIntStatsFs; - export function statfs( - path: string | Uint8Array | URL, - callback: (err: NodeJS.ErrnoException | null, stats: StatsFs) => void, - ): void; - export function statfs( - path: string | Uint8Array | URL, - options: (StatFsOptions & { bigint?: false }) | undefined, - callback: (err: NodeJS.ErrnoException | null, stats: StatsFs) => void, - ): void; - export function statfs( - path: string | Uint8Array | URL, - options: StatFsOptions & { bigint: true }, - callback: (err: NodeJS.ErrnoException | null, stats: BigIntStatsFs) => void, - ): void; - export function statfs( - path: string | Uint8Array | URL, - options: StatFsOptions | undefined, - callback: (err: NodeJS.ErrnoException | null, stats: StatsFs | BigIntStatsFs) => void, - ): void; - /* fs.watch — the file-watching slice (scr_watch.c, kqueue EVFILT_VNODE): - * "rename"/"change" events on ONE path, watcher.close() to stop. The - * listener takes zero parameters or the eventType string (the filename - * parameter has no lowering); an open watcher keeps the loop alive. A - * path that cannot be opened THROWS Node's fs error synchronously. */ - export interface FSWatcher { - close(): void; - } - /* The options-record stance (see http.RequestOptions): persistent: true, - * recursive: false, and encoding: "utf8" state the lowered behavior and - * are accepted; persistent: false, recursive: true, and signal fence by - * name at the call site; undocumented keys drop like Node. */ - export interface WatchOptions { - persistent?: boolean; - recursive?: boolean; - encoding?: string; - [option: string]: unknown; - } - export function watch(path: string, listener?: (eventType: string) => void): FSWatcher; - export function watch( - path: string, - options: WatchOptions, - listener?: (eventType: string) => void, - ): FSWatcher; - /* fs.promises IS the fs/promises module (Node's rule) — the namespace- - * import form `fs.promises.readFile(...)` lowers through the same - * fs/promises table as `import { readFile } from "node:fs/promises"`. */ - export const promises: typeof import("node:fs/promises"); -} -/* Every builtin answers to BOTH specifier forms, like in Node: the bare - * module re-exports its node:-prefixed twin (or holds the declarations - * itself — one of the pair forwards). The lowerer recognizes members by - * their import SPECIFIER, so both forms lower identically. */ -declare module "fs" { - export * from "node:fs"; -} -/* graceful-fs — served as node:fs (it monkeypatches fs and retries on - * EMFILE/ENFILE; compiled programs call fs directly, without the retry - * queue). gracefulify is a no-op statement. The @types/graceful-fs shape. */ -declare module "graceful-fs" { - export * from "node:fs"; - export function gracefulify(fsModule: T): T; -} - -/* fs/promises — the SAME synchronous operations behind already-settled - * promises: success fulfills, failure REJECTS with the would-be thrown - * error (catchable at the await, like Node). DOCUMENTED DIVERGENCE - * (SEMANTICS.md): the syscall blocks the event loop, so I/O never - * interleaves with timers or other fibers — observable only in concurrent - * code. readFile is utf8-only, like readFileSync. */ -declare module "fs/promises" { - export interface FileReadResult { - bytesRead: number; - buffer: T; - } - export interface FileWriteResult { - bytesWritten: number; - buffer: T; - } - export interface FileReadvResult { - bytesRead: number; - buffers: T; - } - export interface FileWritevResult { - bytesWritten: number; - buffers: T; - } - export interface FileReadOptions { - buffer?: T; - offset?: number | null; - length?: number | null; - position?: number | null; - } - /* The streams over a FileHandle (createReadStream, createWriteStream, - * readLines): fs streams that hold the handle until they close; closing - * the stream closes the handle unless autoClose is false. */ - export interface CreateReadStreamOptions { - encoding?: BufferEncoding | null; - autoClose?: boolean; - emitClose?: boolean; - start?: number; - end?: number; - highWaterMark?: number; - } - export interface CreateWriteStreamOptions { - encoding?: BufferEncoding | null; - autoClose?: boolean; - emitClose?: boolean; - start?: number; - highWaterMark?: number; - flush?: boolean; - } - export interface FileHandle extends AsyncDisposable { - readonly fd: number; - close(): Promise; - createReadStream(options?: CreateReadStreamOptions): import("node:fs").ReadStream; - createWriteStream(options?: CreateWriteStreamOptions): import("node:fs").WriteStream; - readLines(options?: CreateReadStreamOptions): import("node:readline").Interface; - sync(): Promise; - datasync(): Promise; - truncate(len?: number): Promise; - chmod(mode: number): Promise; - utimes(atime: number | string | Date, mtime: number | string | Date): Promise; - readv( - buffers: T, - position?: number | null, - ): Promise>; - writev( - buffers: T, - position?: number | null, - ): Promise>; - read( - options?: FileReadOptions | null, - ): Promise>; - read( - buffer: T, - options: Omit, "buffer">, - ): Promise>; - read( - buffer: T, - offset?: number | null, - length?: number | null, - position?: number | null, - ): Promise>; - write( - buffer: T, - offset?: number | null, - length?: number | null, - position?: number | null, - ): Promise>; - write( - buffer: T, - options: Omit, "buffer">, - ): Promise>; - write( - data: string, - position?: number | null, - encoding?: "utf8" | "utf-8" | null, - ): Promise>; - readFile(options?: null): Promise; - readFile(encoding: "utf8" | "utf-8"): Promise; - writeFile(data: string | Uint8Array, encoding?: "utf8" | "utf-8" | null): Promise; - appendFile(data: string | Uint8Array, encoding?: "utf8" | "utf-8" | null): Promise; - stat(): Promise; - [Symbol.asyncDispose](): Promise; - } - export function open(path: string, flags?: string, mode?: number): Promise; - export function link( - existingPath: string | Uint8Array | URL, - newPath: string | Uint8Array | URL, - ): Promise; - export function symlink( - target: string | Uint8Array | URL, - path: string | Uint8Array | URL, - type?: string | null, - ): Promise; - export function cp( - source: string | URL, - destination: string | URL, - options?: import("node:fs").CopyOptions, - ): Promise; - export function readlink( - path: string | Uint8Array | URL, - options: "buffer" | { encoding: "buffer" }, - ): Promise; - export function readlink( - path: string | Uint8Array | URL, - options?: BufferEncoding | { encoding?: BufferEncoding | null } | null, - ): Promise; - export function readlink( - path: string | Uint8Array | URL, - options: string | { encoding?: string | null } | null, - ): Promise; - export function utimes( - path: string | Uint8Array | URL, - atime: number | string | Date, - mtime: number | string | Date, - ): Promise; - export function lutimes( - path: string | Uint8Array | URL, - atime: number | string | Date, - mtime: number | string | Date, - ): Promise; - export function readFile(path: string, encoding: "utf8" | "utf-8"): Promise; - export function readFile( - path: string, - options: BufferEncoding | { encoding: BufferEncoding }, - ): Promise; - export function readFile(path: string): Promise; - export function writeFile( - path: string, - data: string, - options?: { - mode?: number; - encoding?: "utf8" | "utf-8"; - flag?: string; - [option: string]: unknown; - }, - ): Promise; - export function writeFile(path: string, data: string, encoding: "utf8" | "utf-8"): Promise; - export function mkdir( - path: string, - options?: { recursive?: boolean; mode?: number }, - ): Promise; - export function readdir(path: string): Promise; - export function readdir( - path: string, - options: { - encoding?: "utf8" | "utf-8"; - withFileTypes: true; - recursive?: boolean; - [option: string]: unknown; - }, - ): Promise; - /* The async iterator form of fs.globSync: iterate it with for await. */ - export function glob( - pattern: string | readonly string[], - options?: import("node:fs").GlobOptions, - ): AsyncIterableIterator; - export function glob( - pattern: string | readonly string[], - options: import("node:fs").GlobOptionsWithFileTypes, - ): AsyncIterableIterator; - export function rm(path: string): Promise; - export function rm( - path: string, - options: { recursive?: boolean; force?: boolean; maxRetries?: number; retryDelay?: number }, - ): Promise; - export function stat(path: string): Promise; - export function statfs( - path: string | Uint8Array | URL, - options?: import("node:fs").StatFsOptions & { bigint?: false }, - ): Promise; - export function statfs( - path: string | Uint8Array | URL, - options: import("node:fs").StatFsOptions & { bigint: true }, - ): Promise; - export function statfs( - path: string | Uint8Array | URL, - options?: import("node:fs").StatFsOptions, - ): Promise; - export function realpath(path: string): Promise; - export function lstat(path: string): Promise; - export function unlink(path: string): Promise; - export function chmod(path: string, mode: number): Promise; - export function chown(path: string, uid: number, gid: number): Promise; - export function lchown(path: string, uid: number, gid: number): Promise; - export function access(path: string, mode?: number): Promise; - export function truncate(path: string, len?: number): Promise; - export function appendFile(path: string, data: string | Uint8Array): Promise; - export function copyFile(src: string, dest: string): Promise; - export function mkdtemp(prefix: string): Promise; - export function rmdir(path: string): Promise; - export function rename(oldPath: string, newPath: string): Promise; - export function opendir( - path: string, - options?: import("node:fs").OpenDirOptions, - ): Promise; -} -declare module "node:fs/promises" { - export * from "fs/promises"; -} - -/* node:path — the TARGET platform's implementation (Node's rule): posix - * on posix triples, path.win32 under a win32 triple, with the posix and - * win32 namespaces answering their own platform anywhere. Every function - * is a pure string algorithm matching Node's implementations - * exactly; join/resolve are variadic like Node's. */ -declare module "path" { - export function join(...paths: string[]): string; - export function resolve(...paths: string[]): string; - export function normalize(path: string): string; - export function dirname(path: string): string; - export function basename(path: string, suffix?: string): string; - export function extname(path: string): string; - export function isAbsolute(path: string): boolean; - export function relative(from: string, to: string): string; - export function toNamespacedPath(path: string): string; - /* matchesGlob() is Node's glob matcher: brace expansion, extglobs, POSIX - * classes and the ** and dot rules. */ - export function matchesGlob(path: string, pattern: string): boolean; - /* parse() splits a path into its root, directory, base, extension and - * name; format() joins those fields back (Node's `||` defaults). */ - export interface ParsedPath { - root: string; - dir: string; - base: string; - ext: string; - name: string; - } - export interface FormatInputPathObject { - root?: string | undefined; - dir?: string | undefined; - base?: string | undefined; - ext?: string | undefined; - name?: string | undefined; - } - export function parse(path: string): ParsedPath; - export function format(pathObject: FormatInputPathObject): string; - /* The union @types/node declares: the bare module answers the TARGET - * platform's value (a compile-time constant — "\\"/";" under a win32 - * triple, "/"/":" everywhere else). */ - export const sep: "\\" | "/"; - export const delimiter: ";" | ":"; - /* The platform-specific namespaces (real Node submodules too — - * "node:path/posix" resolves). Each carries the FULL surface and - * answers ITS platform's rules on any target, Node's own contract; - * the bare module IS the target platform's implementation. */ - export const posix: typeof import("path/posix"); - export const win32: typeof import("path/win32"); -} -declare module "node:path" { - export * from "path"; -} -declare module "path/posix" { - export function join(...paths: string[]): string; - export function resolve(...paths: string[]): string; - export function normalize(path: string): string; - export function dirname(path: string): string; - export function basename(path: string, suffix?: string): string; - export function extname(path: string): string; - export function isAbsolute(path: string): boolean; - export function relative(from: string, to: string): string; - export function toNamespacedPath(path: string): string; - export function matchesGlob(path: string, pattern: string): boolean; - export function parse(path: string): import("path").ParsedPath; - export function format(pathObject: import("path").FormatInputPathObject): string; - export const sep: "/"; - export const delimiter: ":"; -} -declare module "node:path/posix" { - export * from "path/posix"; -} -declare module "path/win32" { - export function join(...paths: string[]): string; - export function resolve(...paths: string[]): string; - export function normalize(path: string): string; - export function dirname(path: string): string; - export function basename(path: string, suffix?: string): string; - export function extname(path: string): string; - export function isAbsolute(path: string): boolean; - export function relative(from: string, to: string): string; - export function toNamespacedPath(path: string): string; - export function matchesGlob(path: string, pattern: string): boolean; - export function parse(path: string): import("path").ParsedPath; - export function format(pathObject: import("path").FormatInputPathObject): string; - export const sep: "\\"; - export const delimiter: ";"; -} -declare module "node:path/win32" { - export * from "path/win32"; -} - -/* node:os — the slice a CLI needs: platform() (same implementation as - * process.platform), homedir(), tmpdir() (Node's env cascade), EOL - * (always "\n" — POSIX pinned, like path.sep), and networkInterfaces() - * (getifaddrs(3); the interface shapes mirror @types/node's exactly so - * both type worlds map to the same IR structure). */ -declare module "os" { - export function platform(): string; - export function homedir(): string; - export function tmpdir(): string; - /* uname(2)'s release field — Node's own implementation. */ - export function release(): string; - /* uname(2)'s sysname field ("Darwin", "Linux") — Node's os.type(). */ - export function type(): string; - /* Total system memory in bytes (sysctl hw.memsize / sysconf). */ - export function totalmem(): number; - export function availableParallelism(): number; - /* The passwd-entry snapshot (uv_os_get_passwd): shell is `string | - * null` to match @types/node (POSIX always answers the string arm); - * homedir is pw_dir — NOT os.homedir()'s $HOME-first cascade. The type - * is interned explicitly in type-mapper.ts (the AddressInfo pattern — this - * concrete form and @types/node's UserInfo both), and the call - * assembles field-by-field in lowerOsUserInfoCall. */ - export interface UserInfo { - username: string; - uid: number; - gid: number; - shell: string | null; - homedir: string; - } - export function userInfo(): UserInfo; - /* The TARGET platform's line terminator (a compile-time constant: - * "\r\n" under a win32 triple, "\n" everywhere else). */ - export const EOL: "\r\n" | "\n"; - export interface NetworkInterfaceBase { - address: string; - netmask: string; - mac: string; - internal: boolean; - cidr: string | null; - scopeid?: number; - } - export interface NetworkInterfaceInfoIPv4 extends NetworkInterfaceBase { - family: "IPv4"; - } - export interface NetworkInterfaceInfoIPv6 extends NetworkInterfaceBase { - family: "IPv6"; - scopeid: number; - } - export type NetworkInterfaceInfo = NetworkInterfaceInfoIPv4 | NetworkInterfaceInfoIPv6; - export function networkInterfaces(): { [name: string]: NetworkInterfaceInfo[] | undefined }; - /* Host facts, libuv's answers: model/speed/times per logical CPU, the - * host name, the 1/5/15-minute load averages, uptime in seconds, free - * memory in bytes, process.arch, uname's machine and version fields, - * the byte order, and a process's scheduling priority (an omitted pid - * is the current process). Shapes mirror @types/node's. */ - export interface CpuInfo { - model: string; - speed: number; - times: { - user: number; - nice: number; - sys: number; - idle: number; - irq: number; - }; - } - export function cpus(): CpuInfo[]; - export function hostname(): string; - export function loadavg(): number[]; - export function uptime(): number; - export function freemem(): number; - export function arch(): string; - export function machine(): string; - export function version(): string; - export function endianness(): "BE" | "LE"; - export function getPriority(pid?: number): number; - export function setPriority(priority: number): void; - export function setPriority(pid: number, priority: number): void; - /* The TARGET platform's null device ("\\\\.\\nul" under a win32 triple). */ - export const devNull: string; - /* The platform's constants: null-prototype objects with read-only - * members, `signals` frozen (Node's internalBinding('constants').os). */ - export type SignalConstants = { - [ - key in - | "SIGABRT" - | "SIGALRM" - | "SIGBUS" - | "SIGCHLD" - | "SIGCONT" - | "SIGFPE" - | "SIGHUP" - | "SIGILL" - | "SIGINT" - | "SIGIO" - | "SIGIOT" - | "SIGKILL" - | "SIGPIPE" - | "SIGPOLL" - | "SIGPROF" - | "SIGPWR" - | "SIGQUIT" - | "SIGSEGV" - | "SIGSTKFLT" - | "SIGSTOP" - | "SIGSYS" - | "SIGTERM" - | "SIGTRAP" - | "SIGTSTP" - | "SIGTTIN" - | "SIGTTOU" - | "SIGUNUSED" - | "SIGURG" - | "SIGUSR1" - | "SIGUSR2" - | "SIGVTALRM" - | "SIGWINCH" - | "SIGXCPU" - | "SIGXFSZ" - | "SIGBREAK" - | "SIGLOST" - | "SIGINFO" - ]: number; - }; - export namespace constants { - const UV_UDP_REUSEADDR: number; - namespace signals {} - const signals: SignalConstants; - namespace errno { - const E2BIG: number; - const EACCES: number; - const EADDRINUSE: number; - const EADDRNOTAVAIL: number; - const EAFNOSUPPORT: number; - const EAGAIN: number; - const EALREADY: number; - const EBADF: number; - const EBADMSG: number; - const EBUSY: number; - const ECANCELED: number; - const ECHILD: number; - const ECONNABORTED: number; - const ECONNREFUSED: number; - const ECONNRESET: number; - const EDEADLK: number; - const EDESTADDRREQ: number; - const EDOM: number; - const EDQUOT: number; - const EEXIST: number; - const EFAULT: number; - const EFBIG: number; - const EHOSTUNREACH: number; - const EIDRM: number; - const EILSEQ: number; - const EINPROGRESS: number; - const EINTR: number; - const EINVAL: number; - const EIO: number; - const EISCONN: number; - const EISDIR: number; - const ELOOP: number; - const EMFILE: number; - const EMLINK: number; - const EMSGSIZE: number; - const EMULTIHOP: number; - const ENAMETOOLONG: number; - const ENETDOWN: number; - const ENETRESET: number; - const ENETUNREACH: number; - const ENFILE: number; - const ENOBUFS: number; - const ENODATA: number; - const ENODEV: number; - const ENOENT: number; - const ENOEXEC: number; - const ENOLCK: number; - const ENOLINK: number; - const ENOMEM: number; - const ENOMSG: number; - const ENOPROTOOPT: number; - const ENOSPC: number; - const ENOSR: number; - const ENOSTR: number; - const ENOSYS: number; - const ENOTCONN: number; - const ENOTDIR: number; - const ENOTEMPTY: number; - const ENOTSOCK: number; - const ENOTSUP: number; - const ENOTTY: number; - const ENXIO: number; - const EOPNOTSUPP: number; - const EOVERFLOW: number; - const EPERM: number; - const EPIPE: number; - const EPROTO: number; - const EPROTONOSUPPORT: number; - const EPROTOTYPE: number; - const ERANGE: number; - const EROFS: number; - const ESPIPE: number; - const ESRCH: number; - const ESTALE: number; - const ETIME: number; - const ETIMEDOUT: number; - const ETXTBSY: number; - const EWOULDBLOCK: number; - const EXDEV: number; - const WSAEINTR: number; - const WSAEBADF: number; - const WSAEACCES: number; - const WSAEFAULT: number; - const WSAEINVAL: number; - const WSAEMFILE: number; - const WSAEWOULDBLOCK: number; - const WSAEINPROGRESS: number; - const WSAEALREADY: number; - const WSAENOTSOCK: number; - const WSAEDESTADDRREQ: number; - const WSAEMSGSIZE: number; - const WSAEPROTOTYPE: number; - const WSAENOPROTOOPT: number; - const WSAEPROTONOSUPPORT: number; - const WSAESOCKTNOSUPPORT: number; - const WSAEOPNOTSUPP: number; - const WSAEPFNOSUPPORT: number; - const WSAEAFNOSUPPORT: number; - const WSAEADDRINUSE: number; - const WSAEADDRNOTAVAIL: number; - const WSAENETDOWN: number; - const WSAENETUNREACH: number; - const WSAENETRESET: number; - const WSAECONNABORTED: number; - const WSAECONNRESET: number; - const WSAENOBUFS: number; - const WSAEISCONN: number; - const WSAENOTCONN: number; - const WSAESHUTDOWN: number; - const WSAETOOMANYREFS: number; - const WSAETIMEDOUT: number; - const WSAECONNREFUSED: number; - const WSAELOOP: number; - const WSAENAMETOOLONG: number; - const WSAEHOSTDOWN: number; - const WSAEHOSTUNREACH: number; - const WSAENOTEMPTY: number; - const WSAEPROCLIM: number; - const WSAEUSERS: number; - const WSAEDQUOT: number; - const WSAESTALE: number; - const WSAEREMOTE: number; - const WSASYSNOTREADY: number; - const WSAVERNOTSUPPORTED: number; - const WSANOTINITIALISED: number; - const WSAEDISCON: number; - const WSAENOMORE: number; - const WSAECANCELLED: number; - const WSAEINVALIDPROCTABLE: number; - const WSAEINVALIDPROVIDER: number; - const WSAEPROVIDERFAILEDINIT: number; - const WSASYSCALLFAILURE: number; - const WSASERVICE_NOT_FOUND: number; - const WSATYPE_NOT_FOUND: number; - const WSA_E_NO_MORE: number; - const WSA_E_CANCELLED: number; - const WSAEREFUSED: number; - } - namespace dlopen { - const RTLD_LAZY: number; - const RTLD_NOW: number; - const RTLD_GLOBAL: number; - const RTLD_LOCAL: number; - const RTLD_DEEPBIND: number; - } - namespace priority { - const PRIORITY_LOW: number; - const PRIORITY_BELOW_NORMAL: number; - const PRIORITY_NORMAL: number; - const PRIORITY_ABOVE_NORMAL: number; - const PRIORITY_HIGH: number; - const PRIORITY_HIGHEST: number; - } - } -} -declare module "node:os" { - export * from "os"; -} - /* The WHATWG URL class (a Node global; the es2023 lib doesn't declare it), * typed as exactly the supported surface: construction from an input and * optional base (invalid input throws a catchable TypeError, like Node), the @@ -2751,1849 +1445,6 @@ declare var URLSearchParams: { new (init?: string | string[][] | { [key: string]: string } | URLSearchParams): URLSearchParams; }; -/* node:url — the file-URL bridge pair. fileURLToPath accepts a URL value - * or a URL string and throws Node's TypeErrors (non-file scheme, encoded - * slashes, non-empty host); pathToFileURL resolves the path and percent- - * encodes it into a file: URL. The legacy API: parse() answers a Url - * object, format() serializes a Url-shaped object, a string, or a URL, - * resolve() resolves one URL string against another, and the domain pair - * converts a hostname to and from its Punycode form. */ -declare module "url" { - export type URL = globalThis.URL; - export type URLSearchParams = globalThis.URLSearchParams; - export const URL: typeof globalThis.URL; - export const URLSearchParams: typeof globalThis.URLSearchParams; - export function fileURLToPath(url: string | URL, options?: { windows?: boolean }): string; - export function fileURLToPathBuffer(url: string | URL, options?: { windows?: boolean }): Buffer; - export function pathToFileURL(path: string, options?: { windows?: boolean }): URL; - export interface UrlObject { - auth?: string | null | undefined; - hash?: string | null | undefined; - host?: string | null | undefined; - hostname?: string | null | undefined; - href?: string | null | undefined; - pathname?: string | null | undefined; - protocol?: string | null | undefined; - search?: string | null | undefined; - slashes?: boolean | null | undefined; - port?: string | number | null | undefined; - query?: string | null | import("querystring").ParsedUrlQueryInput | undefined; - } - export interface Url { - auth: string | null; - hash: string | null; - host: string | null; - hostname: string | null; - href: string; - path: string | null; - pathname: string | null; - protocol: string | null; - search: string | null; - slashes: boolean | null; - port: string | null; - query: string | null | import("querystring").ParsedUrlQuery; - } - export interface UrlWithParsedQuery extends Url { - query: import("querystring").ParsedUrlQuery; - } - export interface UrlWithStringQuery extends Url { - query: string | null; - } - export interface URLFormatOptions { - auth?: boolean | undefined; - fragment?: boolean | undefined; - search?: boolean | undefined; - unicode?: boolean | undefined; - } - export function parse( - urlString: string, - parseQueryString?: false, - slashesDenoteHost?: boolean, - ): UrlWithStringQuery; - export function parse( - urlString: string, - parseQueryString: true, - slashesDenoteHost?: boolean, - ): UrlWithParsedQuery; - export function parse( - urlString: string, - parseQueryString: boolean, - slashesDenoteHost?: boolean, - ): Url; - export function format(urlObject: URL, options?: URLFormatOptions): string; - export function format(urlObject: UrlObject | string): string; - export function resolve(from: string, to: string): string; - export function domainToASCII(domain: string): string; - export function domainToUnicode(domain: string): string; -} -declare module "node:url" { - export * from "url"; -} - -/* node:crypto: the static hashing/MAC/random/key-derivation utility slice. */ -declare module "crypto" { - import { Transform } from "stream"; - export type KeyObjectType = "secret" | "public" | "private"; - export interface JsonWebKey { - crv?: string; - d?: string; - dp?: string; - dq?: string; - e?: string; - k?: string; - kty?: string; - n?: string; - p?: string; - q?: string; - qi?: string; - x?: string; - y?: string; - [key: string]: unknown; - } - export interface JwkKeyExportOptions { - format: "jwk"; - } - export interface KeyExportOptions { - format?: "buffer" | undefined; - } - export type KeyFormat = "pem" | "der" | "jwk"; - export interface AsymmetricKeyExportOptions { - type: "pkcs1" | "spki" | "pkcs8" | "sec1"; - format: T; - } - export interface AsymmetricKeyDetails { - modulusLength?: number; - publicExponent?: bigint; - namedCurve?: string; - } - /* Secret keys, and RSA and EC private and public keys. */ - export class KeyObject { - private constructor(); - type: KeyObjectType; - symmetricKeySize?: number; - asymmetricKeyType?: string; - asymmetricKeyDetails?: AsymmetricKeyDetails; - export(options: AsymmetricKeyExportOptions<"pem">): string | Buffer; - export(options: AsymmetricKeyExportOptions<"der">): Buffer; - export(options?: KeyExportOptions): Buffer; - export(options: JwkKeyExportOptions): JsonWebKey; - equals(otherKeyObject: KeyObject): boolean; - } - export interface PrivateKeyInput { - key: string | Buffer; - format?: "pem" | "der" | undefined; - type?: "pkcs1" | "pkcs8" | "sec1" | undefined; - encoding?: string | undefined; - } - export interface PublicKeyInput { - key: string | Buffer; - format?: "pem" | "der" | undefined; - type?: "pkcs1" | "spki" | undefined; - encoding?: string | undefined; - } - export interface JsonWebKeyInput { - key: JsonWebKey; - format: "jwk"; - } - export function createPrivateKey( - key: PrivateKeyInput | string | Buffer | JsonWebKeyInput, - ): KeyObject; - export function createPublicKey( - key: PublicKeyInput | string | Buffer | KeyObject | JsonWebKeyInput, - ): KeyObject; - export interface KeyPairKeyObjectResult { - publicKey: KeyObject; - privateKey: KeyObject; - } - export interface KeyPairSyncResult { - publicKey: T1; - privateKey: T2; - } - export interface RSAKeyPairKeyObjectOptions { - modulusLength: number; - publicExponent?: number | undefined; - } - export interface ECKeyPairKeyObjectOptions { - namedCurve: string; - } - export interface RSAKeyPairOptions { - modulusLength: number; - publicExponent?: number | undefined; - publicKeyEncoding: { type: "pkcs1" | "spki"; format: PubF }; - privateKeyEncoding: { type: "pkcs1" | "pkcs8"; format: PrivF }; - } - export interface ECKeyPairOptions { - namedCurve: string; - publicKeyEncoding: { type: "pkcs1" | "spki"; format: PubF }; - privateKeyEncoding: { type: "sec1" | "pkcs8"; format: PrivF }; - } - export function generateKeyPairSync( - type: "rsa", - options: RSAKeyPairOptions<"pem", "pem">, - ): KeyPairSyncResult; - export function generateKeyPairSync( - type: "rsa", - options: RSAKeyPairOptions<"der", "der">, - ): KeyPairSyncResult; - export function generateKeyPairSync( - type: "rsa", - options: RSAKeyPairKeyObjectOptions, - ): KeyPairKeyObjectResult; - export function generateKeyPairSync( - type: "ec", - options: ECKeyPairOptions<"pem", "pem">, - ): KeyPairSyncResult; - export function generateKeyPairSync( - type: "ec", - options: ECKeyPairOptions<"der", "der">, - ): KeyPairSyncResult; - export function generateKeyPairSync( - type: "ec", - options: ECKeyPairKeyObjectOptions, - ): KeyPairKeyObjectResult; - export interface ED25519KeyPairOptions { - publicKeyEncoding: { type: "spki"; format: PubF }; - privateKeyEncoding: { type: "pkcs8"; format: PrivF }; - } - export function generateKeyPairSync( - type: "ed25519", - options: ED25519KeyPairOptions<"pem", "pem">, - ): KeyPairSyncResult; - export function generateKeyPairSync( - type: "ed25519", - options: ED25519KeyPairOptions<"der", "der">, - ): KeyPairSyncResult; - export function generateKeyPairSync(type: "ed25519", options?: {}): KeyPairKeyObjectResult; - export function generateKeyPair( - type: "ed25519", - options: {} | undefined, - callback: (err: Error | null, publicKey: KeyObject, privateKey: KeyObject) => void, - ): void; - export function generateKeyPair( - type: "rsa", - options: RSAKeyPairKeyObjectOptions, - callback: (err: Error | null, publicKey: KeyObject, privateKey: KeyObject) => void, - ): void; - export function generateKeyPair( - type: "ec", - options: ECKeyPairKeyObjectOptions, - callback: (err: Error | null, publicKey: KeyObject, privateKey: KeyObject) => void, - ): void; - export interface SigningOptions { - padding?: number | undefined; - saltLength?: number | undefined; - dsaEncoding?: "der" | "ieee-p1363" | undefined; - } - export interface SignPrivateKeyInput extends PrivateKeyInput, SigningOptions {} - export interface SignKeyObjectInput extends SigningOptions { - key: KeyObject; - } - export interface VerifyPublicKeyInput extends PublicKeyInput, SigningOptions {} - export interface VerifyKeyObjectInput extends SigningOptions { - key: KeyObject; - } - export function sign( - algorithm: string | null | undefined, - data: ArrayBufferView, - key: KeyObject | string | Buffer | SignKeyObjectInput | SignPrivateKeyInput, - ): Buffer; - export function verify( - algorithm: string | null | undefined, - data: ArrayBufferView, - key: KeyObject | string | Buffer | VerifyKeyObjectInput | VerifyPublicKeyInput, - signature: ArrayBufferView, - ): boolean; - /* Sign and Verify accumulate update() data; they are not usable as - * writable streams. */ - export class Sign { - private constructor(); - update(data: string | ArrayBufferView): this; - update(data: string, inputEncoding: Encoding): this; - sign( - privateKey: KeyObject | string | Buffer | SignKeyObjectInput | SignPrivateKeyInput, - ): Buffer; - sign( - privateKey: KeyObject | string | Buffer | SignKeyObjectInput | SignPrivateKeyInput, - outputFormat: BinaryToTextEncoding, - ): string; - } - export class Verify { - private constructor(); - update(data: string | ArrayBufferView): this; - update(data: string, inputEncoding: Encoding): this; - verify( - object: KeyObject | string | Buffer | VerifyKeyObjectInput | VerifyPublicKeyInput, - signature: ArrayBufferView, - ): boolean; - verify( - object: KeyObject | string | Buffer | VerifyKeyObjectInput | VerifyPublicKeyInput, - signature: string, - signatureEncoding?: BinaryToTextEncoding, - ): boolean; - } - export function createSign(algorithm: string): Sign; - export function createVerify(algorithm: string): Verify; - export function createSecretKey(key: ArrayBufferView | ArrayBufferLike): KeyObject; - export function createSecretKey(key: string, encoding: BufferEncoding): KeyObject; - export function generateKeySync(type: "hmac" | "aes", options: { length: number }): KeyObject; - export function generateKey( - type: "hmac" | "aes", - options: { length: number }, - callback: (err: Error | null, key: KeyObject) => void, - ): void; - export interface RandomUUIDOptions { - disableEntropyCache?: boolean | undefined; - } - export function randomUUID(options?: RandomUUIDOptions): string; - export function randomUUIDv7(options?: RandomUUIDOptions): string; - export function randomBytes(size: number): Buffer; - export type BinaryToTextEncoding = "base64" | "base64url" | "hex" | "binary"; - export type CharacterEncoding = "utf8" | "utf-8" | "utf16le" | "utf-16le" | "latin1"; - export type LegacyCharacterEncoding = "ascii" | "binary" | "ucs2" | "ucs-2"; - export type Encoding = BinaryToTextEncoding | CharacterEncoding | LegacyCharacterEncoding; - export interface HashOptions { - outputLength?: number | undefined; - } - /* Hash and Hmac are Transforms: written chunks update the digest, which - * the readable side delivers once the writable side ends. */ - export interface Hash extends Transform { - copy(options?: HashOptions): Hash; - update(data: string | ArrayBufferView): Hash; - update(data: string, inputEncoding: Encoding): Hash; - digest(): Buffer; - digest(encoding: BinaryToTextEncoding): string; - } - export interface Hmac extends Transform { - update(data: string | ArrayBufferView): Hmac; - update(data: string, inputEncoding: Encoding): Hmac; - digest(): Buffer; - digest(encoding: BinaryToTextEncoding): string; - } - export function createHash(algorithm: string, options?: HashOptions): Hash; - export function createHmac( - algorithm: string, - key: BinaryLike | KeyObject, - options?: { encoding?: BufferEncoding | undefined }, - ): Hmac; - export interface OneShotDigestOptions { - outputEncoding?: BinaryToTextEncoding | "buffer" | undefined; - outputLength?: number | undefined; - } - export interface OneShotDigestOptionsWithStringEncoding extends OneShotDigestOptions { - outputEncoding?: BinaryToTextEncoding | undefined; - } - export interface OneShotDigestOptionsWithBufferEncoding extends OneShotDigestOptions { - outputEncoding: "buffer"; - } - export function hash( - algorithm: string, - data: string | ArrayBufferView, - options?: OneShotDigestOptionsWithStringEncoding | BinaryToTextEncoding, - ): string; - export function hash( - algorithm: string, - data: string | ArrayBufferView, - options: OneShotDigestOptionsWithBufferEncoding | "buffer", - ): Buffer; - export function hash( - algorithm: string, - data: string | ArrayBufferView, - options: OneShotDigestOptions | BinaryToTextEncoding | "buffer", - ): string | Buffer; - export function timingSafeEqual( - a: ArrayBufferView | ArrayBufferLike, - b: ArrayBufferView | ArrayBufferLike, - ): boolean; - /* WebCrypto's getRandomValues (also the webcrypto object's): integer - * typed arrays, filled in place and returned. */ - type RandomValuesArray = - | Int8Array - | Uint8Array - | Uint8ClampedArray - | Int16Array - | Uint16Array - | Int32Array - | Uint32Array - | BigInt64Array - | BigUint64Array; - export function getRandomValues(typedArray: T): T; - /* The WebCrypto global's object and its SubtleCrypto. */ - export const webcrypto: Crypto; - export const subtle: SubtleCrypto; - export function randomFillSync( - buffer: T, - offset?: number, - size?: number, - ): T; - export function randomFill( - buffer: T, - callback: (err: Error | null, buf: T) => void, - ): void; - export function randomFill( - buffer: T, - offset: number, - callback: (err: Error | null, buf: T) => void, - ): void; - export function randomFill( - buffer: T, - offset: number, - size: number, - callback: (err: Error | null, buf: T) => void, - ): void; - export function randomInt(max: number): number; - export function randomInt(min: number, max: number): number; - export function randomInt( - max: number, - callback: (err: Error | null, value: number) => void, - ): void; - export function randomInt( - min: number, - max: number, - callback: (err: Error | null, value: number) => void, - ): void; - export function pbkdf2Sync( - password: string | Uint8Array, - salt: string | Uint8Array, - iterations: number, - keylen: number, - digest: string, - ): Buffer; - export function pbkdf2( - password: string | Uint8Array, - salt: string | Uint8Array, - iterations: number, - keylen: number, - digest: string, - callback: (error: Error | null, derivedKey: Buffer) => void, - ): void; - export function randomBytes( - size: number, - callback: (error: Error | null, buffer: Buffer) => void, - ): void; - export type BinaryLike = string | ArrayBuffer | ArrayBufferView; - /* Cipheriv/Decipheriv: aes-128/192/256 in cbc, ctr, gcm, ccm, ecb, cfb, - * cfb8, cfb1, and ofb modes and their key wraps, des-ede3 and des-ede, - * chacha20, and chacha20-poly1305. Both are Transforms: written chunks go - * through update(), and the end pushes final(). */ - export type CipherCCMTypes = "aes-128-ccm" | "aes-192-ccm" | "aes-256-ccm"; - export type CipherGCMTypes = "aes-128-gcm" | "aes-192-gcm" | "aes-256-gcm"; - export type CipherChaCha20Poly1305Types = "chacha20-poly1305"; - export type CipherKey = BinaryLike | KeyObject; - export interface CipherCCMOptions { - authTagLength: number; - } - export interface CipherGCMOptions { - authTagLength?: number; - } - export interface CipherChaCha20Poly1305Options { - authTagLength?: number; - } - export interface Cipheriv extends Transform { - update(data: string | ArrayBufferView): Buffer; - update(data: string, inputEncoding: BufferEncoding): Buffer; - update(data: ArrayBufferView, inputEncoding: undefined, outputEncoding: BufferEncoding): string; - update( - data: string, - inputEncoding: BufferEncoding | undefined, - outputEncoding: BufferEncoding, - ): string; - final(): Buffer; - final(outputEncoding: BufferEncoding): string; - setAutoPadding(autoPadding?: boolean): this; - } - export interface CipherCCM extends Cipheriv { - setAAD(buffer: ArrayBufferView, options: { plaintextLength: number }): this; - getAuthTag(): Buffer; - } - export interface CipherGCM extends Cipheriv { - setAAD(buffer: ArrayBufferView, options?: { plaintextLength: number }): this; - getAuthTag(): Buffer; - } - export interface CipherChaCha20Poly1305 extends Cipheriv { - setAAD(buffer: ArrayBufferView, options?: { plaintextLength: number }): this; - getAuthTag(): Buffer; - } - export interface Decipheriv extends Transform { - update(data: ArrayBufferView): Buffer; - update(data: string, inputEncoding: BufferEncoding): Buffer; - update(data: ArrayBufferView, inputEncoding: undefined, outputEncoding: BufferEncoding): string; - update( - data: string, - inputEncoding: BufferEncoding | undefined, - outputEncoding: BufferEncoding, - ): string; - final(): Buffer; - final(outputEncoding: BufferEncoding): string; - setAutoPadding(autoPadding?: boolean): this; - } - export interface DecipherCCM extends Decipheriv { - setAuthTag(buffer: ArrayBufferView): this; - setAAD(buffer: ArrayBufferView, options: { plaintextLength: number }): this; - } - export interface DecipherGCM extends Decipheriv { - setAuthTag(buffer: ArrayBufferView): this; - setAAD(buffer: ArrayBufferView, options?: { plaintextLength: number }): this; - } - export interface DecipherChaCha20Poly1305 extends Decipheriv { - setAuthTag(buffer: ArrayBufferView): this; - setAAD(buffer: ArrayBufferView, options?: { plaintextLength: number }): this; - } - export function createCipheriv( - algorithm: CipherCCMTypes, - key: CipherKey, - iv: BinaryLike, - options: CipherCCMOptions, - ): CipherCCM; - export function createCipheriv( - algorithm: CipherGCMTypes, - key: CipherKey, - iv: BinaryLike, - options?: CipherGCMOptions, - ): CipherGCM; - export function createCipheriv( - algorithm: CipherChaCha20Poly1305Types, - key: CipherKey, - iv: BinaryLike, - options?: CipherChaCha20Poly1305Options, - ): CipherChaCha20Poly1305; - export function createCipheriv( - algorithm: string, - key: CipherKey, - iv: BinaryLike | null, - options?: { authTagLength?: number }, - ): Cipheriv; - export function createDecipheriv( - algorithm: CipherCCMTypes, - key: CipherKey, - iv: BinaryLike, - options: CipherCCMOptions, - ): DecipherCCM; - export function createDecipheriv( - algorithm: CipherGCMTypes, - key: CipherKey, - iv: BinaryLike, - options?: CipherGCMOptions, - ): DecipherGCM; - export function createDecipheriv( - algorithm: CipherChaCha20Poly1305Types, - key: CipherKey, - iv: BinaryLike, - options?: CipherChaCha20Poly1305Options, - ): DecipherChaCha20Poly1305; - export function createDecipheriv( - algorithm: string, - key: CipherKey, - iv: BinaryLike | null, - options?: { authTagLength?: number }, - ): Decipheriv; - export type ECDHKeyFormat = "compressed" | "uncompressed" | "hybrid"; - export class ECDH { - private constructor(); - static convertKey( - key: BinaryLike, - curve: string, - inputEncoding?: BinaryToTextEncoding, - outputEncoding?: "latin1" | "hex" | "base64" | "base64url", - format?: ECDHKeyFormat, - ): Buffer | string; - generateKeys(): Buffer; - generateKeys(encoding: BinaryToTextEncoding, format?: ECDHKeyFormat): string; - computeSecret(otherPublicKey: ArrayBufferView): Buffer; - computeSecret(otherPublicKey: string, inputEncoding: BinaryToTextEncoding): Buffer; - computeSecret(otherPublicKey: ArrayBufferView, outputEncoding: BinaryToTextEncoding): string; - computeSecret( - otherPublicKey: string, - inputEncoding: BinaryToTextEncoding, - outputEncoding: BinaryToTextEncoding, - ): string; - getPrivateKey(): Buffer; - getPrivateKey(encoding: BinaryToTextEncoding): string; - getPublicKey(encoding?: null, format?: ECDHKeyFormat): Buffer; - getPublicKey(encoding: BinaryToTextEncoding, format?: ECDHKeyFormat): string; - setPrivateKey(privateKey: ArrayBufferView): void; - setPrivateKey(privateKey: string, encoding: BinaryToTextEncoding): void; - } - export function createECDH(curveName: string): ECDH; - export class DiffieHellman { - private constructor(); - generateKeys(): Buffer; - generateKeys(encoding: BinaryToTextEncoding): string; - computeSecret( - otherPublicKey: ArrayBufferView, - inputEncoding?: null, - outputEncoding?: null, - ): Buffer; - computeSecret( - otherPublicKey: string, - inputEncoding: BinaryToTextEncoding, - outputEncoding?: null, - ): Buffer; - computeSecret( - otherPublicKey: ArrayBufferView, - inputEncoding: null, - outputEncoding: BinaryToTextEncoding, - ): string; - computeSecret( - otherPublicKey: string, - inputEncoding: BinaryToTextEncoding, - outputEncoding: BinaryToTextEncoding, - ): string; - getPrime(): Buffer; - getPrime(encoding: BinaryToTextEncoding): string; - getGenerator(): Buffer; - getGenerator(encoding: BinaryToTextEncoding): string; - getPublicKey(): Buffer; - getPublicKey(encoding: BinaryToTextEncoding): string; - getPrivateKey(): Buffer; - getPrivateKey(encoding: BinaryToTextEncoding): string; - setPublicKey(publicKey: ArrayBufferView): void; - setPublicKey(publicKey: string, encoding: BufferEncoding): void; - setPrivateKey(privateKey: ArrayBufferView): void; - setPrivateKey(privateKey: string, encoding: BufferEncoding): void; - verifyError: number; - } - export type DiffieHellmanGroup = Omit; - export function createDiffieHellman(primeLength: number, generator?: number): DiffieHellman; - export function createDiffieHellman( - prime: ArrayBuffer | ArrayBufferView, - generator?: number | ArrayBuffer | ArrayBufferView, - ): DiffieHellman; - export function createDiffieHellman( - prime: ArrayBuffer | ArrayBufferView, - generator: string, - generatorEncoding: BinaryToTextEncoding, - ): DiffieHellman; - export function createDiffieHellman( - prime: string, - primeEncoding: BinaryToTextEncoding, - generator?: number | ArrayBuffer | ArrayBufferView, - ): DiffieHellman; - export function createDiffieHellman( - prime: string, - primeEncoding: BinaryToTextEncoding, - generator: string, - generatorEncoding: BinaryToTextEncoding, - ): DiffieHellman; - export function getDiffieHellman(groupName: string): DiffieHellmanGroup; - export function createDiffieHellmanGroup(name: string): DiffieHellmanGroup; - export type LargeNumberLike = ArrayBufferView | SharedArrayBuffer | ArrayBuffer | bigint; - export interface GeneratePrimeOptions { - add?: LargeNumberLike | undefined; - rem?: LargeNumberLike | undefined; - safe?: boolean | undefined; - bigint?: boolean | undefined; - } - export interface GeneratePrimeOptionsBigInt extends GeneratePrimeOptions { - bigint: true; - } - export interface GeneratePrimeOptionsArrayBuffer extends GeneratePrimeOptions { - bigint?: false | undefined; - } - export function generatePrime( - size: number, - callback: (err: Error | null, prime: ArrayBuffer) => void, - ): void; - export function generatePrime( - size: number, - options: GeneratePrimeOptionsBigInt, - callback: (err: Error | null, prime: bigint) => void, - ): void; - export function generatePrime( - size: number, - options: GeneratePrimeOptionsArrayBuffer, - callback: (err: Error | null, prime: ArrayBuffer) => void, - ): void; - export function generatePrime( - size: number, - options: GeneratePrimeOptions, - callback: (err: Error | null, prime: ArrayBuffer | bigint) => void, - ): void; - export function generatePrimeSync(size: number): ArrayBuffer; - export function generatePrimeSync(size: number, options: GeneratePrimeOptionsBigInt): bigint; - export function generatePrimeSync( - size: number, - options: GeneratePrimeOptionsArrayBuffer, - ): ArrayBuffer; - export function generatePrimeSync( - size: number, - options: GeneratePrimeOptions, - ): ArrayBuffer | bigint; - export interface CheckPrimeOptions { - checks?: number | undefined; - } - export function checkPrime( - value: LargeNumberLike, - callback: (err: Error | null, result: boolean) => void, - ): void; - export function checkPrime( - value: LargeNumberLike, - options: CheckPrimeOptions, - callback: (err: Error | null, result: boolean) => void, - ): void; - export function checkPrimeSync(candidate: LargeNumberLike, options?: CheckPrimeOptions): boolean; - export interface ScryptOptions { - N?: number; - r?: number; - p?: number; - cost?: number; - blockSize?: number; - parallelization?: number; - maxmem?: number; - } - export function scryptSync( - password: BinaryLike, - salt: BinaryLike, - keylen: number, - options?: ScryptOptions, - ): Buffer; - export function scrypt( - password: BinaryLike, - salt: BinaryLike, - keylen: number, - callback: (error: Error | null, derivedKey: Buffer) => void, - ): void; - export function scrypt( - password: BinaryLike, - salt: BinaryLike, - keylen: number, - options: ScryptOptions, - callback: (error: Error | null, derivedKey: Buffer) => void, - ): void; - export function hkdfSync( - digest: string, - ikm: BinaryLike | KeyObject, - salt: BinaryLike, - info: BinaryLike, - keylen: number, - ): ArrayBuffer; - export function hkdf( - digest: string, - ikm: BinaryLike | KeyObject, - salt: BinaryLike, - info: BinaryLike, - keylen: number, - callback: (error: Error | null, derivedKey: ArrayBuffer) => void, - ): void; - /* The lowered X509Certificate surface is the data-record slice: - * fingerprint (the SHA-1 of the DER, uppercase colon-separated) and - * the validFrom/validTo validity window (Node's ASN1_TIME_print - * strings — "Jul 1 00:00:00 2026 GMT"; the cert-expiry idiom composes - * them with new Date(...).getTime()). All compute at construction; - * unparseable input throws Node's ERR_OSSL_PEM_NO_START_LINE Error. */ - export class X509Certificate { - constructor(buffer: Uint8Array | string); - readonly fingerprint: string; - readonly validFrom: string; - readonly validTo: string; - } - /* The crypto introspection statics: getFips() answers 0 (a compiled - * binary is never a FIPS build), the three name lists bake as fresh - * string[] literals of Node v24 answers, and constants (below) bakes - * per member like http2.constants. */ - export function getFips(): 1 | 0; - export function getCiphers(): string[]; - export type CipherMode = - | "cbc" - | "ccm" - | "cfb" - | "ctr" - | "ecb" - | "gcm" - | "ocb" - | "ofb" - | "stream" - | "wrap" - | "xts"; - export interface CipherInfoOptions { - keyLength?: number | undefined; - ivLength?: number | undefined; - } - export interface CipherInfo { - name: string; - nid: number; - blockSize?: number | undefined; - ivLength?: number | undefined; - keyLength: number; - mode: CipherMode; - } - export function getCipherInfo( - nameOrNid: string | number, - options?: CipherInfoOptions, - ): CipherInfo | undefined; - export function getHashes(): string[]; - export function getCurves(): string[]; - /* Node v24 crypto.constants, literal-typed (the Http2Constants - * stance). Every ACCESS bakes as its literal at lowering; the object - * itself never materializes (bare crypto.constants value uses fence). */ - export interface CryptoConstants { - readonly OPENSSL_VERSION_NUMBER: 810549376; - readonly SSL_OP_ALL: 2147485776; - readonly SSL_OP_ALLOW_NO_DHE_KEX: 1024; - readonly SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION: 262144; - readonly SSL_OP_CIPHER_SERVER_PREFERENCE: 4194304; - readonly SSL_OP_CISCO_ANYCONNECT: 32768; - readonly SSL_OP_COOKIE_EXCHANGE: 8192; - readonly SSL_OP_CRYPTOPRO_TLSEXT_BUG: 2147483648; - readonly SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS: 2048; - readonly SSL_OP_LEGACY_SERVER_CONNECT: 4; - readonly SSL_OP_NO_COMPRESSION: 131072; - readonly SSL_OP_NO_ENCRYPT_THEN_MAC: 524288; - readonly SSL_OP_NO_QUERY_MTU: 4096; - readonly SSL_OP_NO_RENEGOTIATION: 1073741824; - readonly SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION: 65536; - readonly SSL_OP_NO_SSLv2: 0; - readonly SSL_OP_NO_SSLv3: 33554432; - readonly SSL_OP_NO_TICKET: 16384; - readonly SSL_OP_NO_TLSv1: 67108864; - readonly SSL_OP_NO_TLSv1_1: 268435456; - readonly SSL_OP_NO_TLSv1_2: 134217728; - readonly SSL_OP_NO_TLSv1_3: 536870912; - readonly SSL_OP_PRIORITIZE_CHACHA: 2097152; - readonly SSL_OP_TLS_ROLLBACK_BUG: 8388608; - readonly ENGINE_METHOD_RSA: 1; - readonly ENGINE_METHOD_DSA: 2; - readonly ENGINE_METHOD_DH: 4; - readonly ENGINE_METHOD_RAND: 8; - readonly ENGINE_METHOD_EC: 2048; - readonly ENGINE_METHOD_CIPHERS: 64; - readonly ENGINE_METHOD_DIGESTS: 128; - readonly ENGINE_METHOD_PKEY_METHS: 512; - readonly ENGINE_METHOD_PKEY_ASN1_METHS: 1024; - readonly ENGINE_METHOD_ALL: 65535; - readonly ENGINE_METHOD_NONE: 0; - readonly DH_CHECK_P_NOT_SAFE_PRIME: 2; - readonly DH_CHECK_P_NOT_PRIME: 1; - readonly DH_UNABLE_TO_CHECK_GENERATOR: 4; - readonly DH_NOT_SUITABLE_GENERATOR: 8; - readonly RSA_PKCS1_PADDING: 1; - readonly RSA_SSLV23_PADDING: 2; - readonly RSA_NO_PADDING: 3; - readonly RSA_PKCS1_OAEP_PADDING: 4; - readonly RSA_X931_PADDING: 5; - readonly RSA_PKCS1_PSS_PADDING: 6; - readonly RSA_PSS_SALTLEN_DIGEST: -1; - readonly RSA_PSS_SALTLEN_MAX_SIGN: -2; - readonly RSA_PSS_SALTLEN_AUTO: -2; - readonly defaultCoreCipherList: "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:DHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA256:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!SRP:!CAMELLIA"; - readonly TLS1_VERSION: 769; - readonly TLS1_1_VERSION: 770; - readonly TLS1_2_VERSION: 771; - readonly TLS1_3_VERSION: 772; - readonly POINT_CONVERSION_COMPRESSED: 2; - readonly POINT_CONVERSION_UNCOMPRESSED: 4; - readonly POINT_CONVERSION_HYBRID: 6; - readonly defaultCipherList: "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:DHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA256:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!SRP:!CAMELLIA"; - } - export const constants: CryptoConstants; -} -declare module "node:crypto" { - export * from "crypto"; -} - -/* node:child_process — the synchronous slice: spawnSync(command, args?) - * runs the child to completion (posix_spawn + waitpid) with stdout/stderr - * captured as utf8 strings. `status` is the exit code, or null when the - * child died to a signal OR could not be spawned at all (nonexistent - * binary, permission denied) — Node reports spawn failure through the - * result's `error` property, which this surface does not carry, and types - * stdout/stderr null there where scriptc returns "" (SEMANTICS.md). - * No shell, no options object: the command runs directly (PATH-searched, - * like Node). */ -declare module "child_process" { - export interface SpawnSyncReturns { - readonly status: number | null; - readonly stdout: string; - readonly stderr: string; - /* The termination signal's name ("SIGTERM", ...) when a signal - * killed the child (a timeout's killSignal included), null otherwise - * — Node's shape. */ - readonly signal: string | null; - /* Node's spawn-failure carrier: a real Error ("spawnSync - * ENOENT", `code` stamped) when the spawn itself failed — status is - * null and both outputs "" then — or ETIMEDOUT after a timeout kill; - * undefined otherwise. */ - readonly error?: Error; - } - /* scriptc always captures utf8 strings; pass { encoding: "utf8" } - * whenever stdout/stderr is READ so plain Node agrees (without it Node - * hands Buffers). The other supported options: timeout (ms — sends - * killSignal at the deadline and the result carries error: ETIMEDOUT + - * the signal, never a throw), killSignal (a signal NAME literal), - * stdio ("ignore"/"inherit"/"pipe" or a 3-tuple of those — captures - * read "" for non-piped streams), env (replaces the child's - * environment), and windowsHide (a POSIX no-op). */ - export function spawnSync( - command: string, - args?: string[], - options?: { - encoding?: "utf8" | "utf-8"; - timeout?: number; - killSignal?: string; - stdio?: "pipe" | "ignore" | "inherit" | ("pipe" | "ignore" | "inherit")[]; - env?: { [k: string]: string | undefined }; - windowsHide?: boolean; - }, - ): SpawnSyncReturns; - - /* The asynchronous slice: spawn with stdio "ignore"/"inherit"/fd/"pipe" - * tuples (piped stdin is writable through child.stdin; piped stdout/stderr - * deliver through child.stdout/stderr) and the terminal events. "exit" - * fires once with the exit code, or - * null when the child died to a signal; "error" fires ONLY when the - * child could not be spawned at all (Node's split: a spawn failure - * emits "error" and never "exit"). "close" fires after exit/error and - * every piped stdio handle reaches EOF. Exit/close listeners may take - * Node's code and signal parameters. `on`/`once` return void here (Node - * returns the child; chaining is fenced). The event loop keeps the process - * alive until every spawned child is reaped, like Node — reaping polls at - * loop quiescence (SEMANTICS.md documents the divergence). An "error" - * event with no registered listener prints the error and exits 1, - * exactly the unhandled-'error' EventEmitter behavior. */ - export interface ChildStdin { - readonly writable: boolean; - write(chunk: string | Uint8Array): boolean; - end(): void; - destroy(): void; - on(event: "drain" | "finish", listener: () => void): void; - on(event: "error", listener: (err: Error) => void): void; - on(event: string, listener: (...args: any[]) => void): void; - once(event: "drain" | "finish", listener: () => void): void; - once(event: "error", listener: (err: Error) => void): void; - once(event: string, listener: (...args: any[]) => void): void; - } - - export interface ChildProcess extends Disposable { - ref(): void; - on(event: "spawn", listener: () => void): this; - once(event: "spawn", listener: () => void): this; - /* The exit listener may also take Node's second parameter — the - * terminating signal's name, null for a normal exit. */ - on(event: "exit", listener: (code: number | null, signal: string | null) => void): this; - on(event: "close", listener: (code: number | null, signal: string | null) => void): this; - on(event: "error", listener: (err: Error) => void): this; - once(event: "exit", listener: (code: number | null, signal: string | null) => void): this; - once(event: "close", listener: (code: number | null, signal: string | null) => void): this; - once(event: "error", listener: (err: Error) => void): this; - on(event: "message", listener: (message: any) => void): this; - on(event: "disconnect", listener: () => void): this; - once(event: "message", listener: (message: any) => void): this; - once(event: "disconnect", listener: () => void): this; - /* The lifecycle members, Node's exact shapes: pid is undefined exactly - * when the spawn failed; exitCode is null while running, the code - * after a normal exit, null for a signal death, and -errno once a - * spawn failure's "error" event has fired; kill resolves signal names - * through Node's table (unknown names throw the Unknown signal - * TypeError), answers true when the signal was sent, false once the - * child was reaped (or never spawned), and sets `killed` on success; - * unref() drops the child from the event loop's keep-alive set (it is - * still reaped while the loop runs for other reasons — SEMANTICS.md - * has the reap story). */ - readonly pid: number | undefined; - readonly exitCode: number | null; - readonly killed: boolean; - kill(signal?: string | number): boolean; - unref(): void; - readonly connected: boolean; - send(message: object, callback?: (error: Error | null) => void): boolean; - disconnect(): void; - [Symbol.dispose](): void; - /* The piped streams — non-null exactly when the matching stdio slot - * was "pipe" (Node's shape). */ - readonly stdin: ChildStdin | null; - readonly stdout: NodeJS.ReadableStream | null; - readonly stderr: NodeJS.ReadableStream | null; - } - export interface SpawnOptions { - /* The 3-tuple form admits number fds in the stdout/stderr slots — - * openSync results dup2'd into the child (the daemon-log idiom - * ["ignore", logFd, logFd]) — and "pipe" there too (child.stdout/ - * child.stderr streams); "pipe" in the stdin slot exposes - * child.stdin. */ - stdio?: "ignore" | "inherit" | "pipe" | ("ignore" | "inherit" | "pipe" | number)[]; - /* detached gives the child its own session and process group - * (POSIX_SPAWN_SETSID); env REPLACES the child environment; cwd - * sets its working directory; windowsHide is a POSIX no-op; shell - * runs the complete command string through /bin/sh or cmd.exe. */ - detached?: boolean; - env?: { [k: string]: string | undefined }; - cwd?: string; - windowsHide?: boolean; - shell?: boolean; - } - export function spawn(command: string, args?: string[], options?: SpawnOptions): ChildProcess; - export function spawn(command: string, options?: SpawnOptions): ChildProcess; - - export function fork( - modulePath: string | URL, - args?: string[], - options?: { - cwd?: string; - env?: { [k: string]: string | undefined }; - execArgv?: string[]; - silent?: boolean; - stdio?: "pipe" | "ignore" | "inherit" | ("pipe" | "ignore" | "inherit" | "ipc")[]; - serialization?: "json" | "advanced"; - windowsHide?: boolean; - }, - ): ChildProcess; - - /* The synchronous exec pair: execFileSync runs a file directly (no - * shell), execSync runs a command through /bin/sh -c. Both capture - * stdout as a utf8 string (pass { encoding: "utf8" } so plain Node - * agrees — without it Node returns a Buffer) and THROW on a non-zero - * exit / signal death — the Error's message is Node's "Command failed: - * " (with the captured stderr appended). Supported options: - * encoding ("utf8"), cwd, env (REPLACES the child environment), input - * (fed to stdin), timeout (ms — SIGTERMs the child, then throws - * ETIMEDOUT), stdio ("pipe"/"ignore" or a 3-tuple of those — the - * default captures stderr AND echoes it to the parent, like Node), and - * maxBuffer (accepted, not enforced). Node's status/stdout/stderr error - * properties are not carried (SEMANTICS.md). */ - interface ExecSyncOptions { - encoding?: "utf8" | "utf-8"; - cwd?: string; - env?: { [k: string]: string | undefined }; - input?: string; - timeout?: number; - stdio?: "pipe" | "ignore" | "inherit" | ("pipe" | "ignore" | "inherit")[]; - maxBuffer?: number; - killSignal?: string; - windowsHide?: boolean; - shell?: boolean; - } - /* The named options interface for the string-encoding exec form — - * @types/node's ExecFileSyncOptionsWithStringEncoding, narrowed to the - * honestly-implemented members. VALUES of this type are a real record - * (type-mapper.ts interns the shape), so a typed const or a runner function's - * options parameter flows to execFileSync at runtime — the windows-ca - * command-runner idiom. */ - export interface ExecFileSyncOptionsWithStringEncoding { - encoding: "utf8" | "utf-8"; - cwd?: string; - input?: string; - timeout?: number; - stdio?: "pipe" | "ignore" | "inherit" | ("pipe" | "ignore" | "inherit")[]; - maxBuffer?: number; - windowsHide?: boolean; - } - export function execFileSync(file: string, args?: string[], options?: ExecSyncOptions): string; - export function execSync(command: string, options?: ExecSyncOptions): string; - export type ExecFileCallback = (error: Error | null, stdout: string, stderr: string) => void; - /* The callback form starts an asynchronous child and captures utf8 - * stdout/stderr. The no-callback overload remains for util.promisify's - * static target recognition; reached no-callback calls still fence. */ - export function execFile(file: string, callback: ExecFileCallback): ChildProcess; - export function execFile( - file: string, - args: string[] | null, - callback: ExecFileCallback, - ): ChildProcess; - export function execFile( - file: string, - args: string[] | null, - options: ExecSyncOptions, - callback: ExecFileCallback, - ): ChildProcess; - export function execFile( - file: string, - args?: string[] | null, - options?: ExecSyncOptions, - ): unknown; - /* The async shell form. The callback forms lower like execFile's, with - * the command line run by /bin/sh -c, and util.promisify recognizes it - * statically, as it does execFile; reached calls without a callback - * fence at their site. */ - export function exec( - command: string, - options?: ExecSyncOptions | ((error: Error | null, stdout: string, stderr: string) => void), - callback?: (error: Error | null, stdout: string, stderr: string) => void, - ): ChildProcess; -} -declare module "node:child_process" { - export * from "child_process"; -} - -/* node:util — compile-time projections for child_process.execFile, - * child_process.exec and fs.readFile. The first two retain Node's custom - * { stdout, stderr } result; the last resolves the callback's data - * argument. Other targets and bare - * promisify values fence per site. */ -declare module "util" { - export * as types from "util/types"; - export function getSystemErrorName(err: number): string; - export function getSystemErrorMessage(err: number): string; - export function getSystemErrorMap(): Map; - export function stripVTControlCharacters(str: string): string; - export function toUSVString(string: string): string; - export function isDeepStrictEqual(val1: unknown, val2: unknown, skipPrototype?: boolean): boolean; - export function styleText( - format: string | readonly string[], - text: string, - options?: { validateStream?: boolean; stream?: unknown }, - ): string; - export function parseEnv(content: string): { [key: string]: string | undefined }; - export function inherits(constructor: unknown, superConstructor: unknown): void; - export interface DeprecateOptions { - modifyPrototype?: boolean; - } - export function deprecate( - fn: T, - msg: string, - code?: string, - options?: DeprecateOptions, - ): T; - /* The callback receives null on success and the rejection reason - * otherwise (falsy reasons arrive as ERR_FALSY_VALUE_REJECTION). */ - export function callbackify( - fn: () => Promise, - ): (callback: (err: NodeJS.ErrnoException | null) => void) => void; - export function callbackify( - fn: () => Promise, - ): (callback: (err: NodeJS.ErrnoException | null, result: TResult) => void) => void; - export function callbackify( - fn: (arg1: T1) => Promise, - ): (arg1: T1, callback: (err: NodeJS.ErrnoException | null) => void) => void; - export function callbackify( - fn: (arg1: T1) => Promise, - ): (arg1: T1, callback: (err: NodeJS.ErrnoException | null, result: TResult) => void) => void; - export function callbackify( - fn: (arg1: T1, arg2: T2) => Promise, - ): (arg1: T1, arg2: T2, callback: (err: NodeJS.ErrnoException | null) => void) => void; - export function callbackify( - fn: (arg1: T1, arg2: T2) => Promise, - ): ( - arg1: T1, - arg2: T2, - callback: (err: NodeJS.ErrnoException | null, result: TResult) => void, - ) => void; - export function callbackify( - fn: (arg1: T1, arg2: T2, arg3: T3) => Promise, - ): (arg1: T1, arg2: T2, arg3: T3, callback: (err: NodeJS.ErrnoException | null) => void) => void; - export function callbackify( - fn: (arg1: T1, arg2: T2, arg3: T3) => Promise, - ): ( - arg1: T1, - arg2: T2, - arg3: T3, - callback: (err: NodeJS.ErrnoException | null, result: TResult) => void, - ) => void; - export type DebugLoggerFunction = (msg: string, ...param: unknown[]) => void; - export interface DebugLogger extends DebugLoggerFunction { - enabled: boolean; - } - export function debuglog( - section: string, - callback?: (fn: DebugLoggerFunction) => void, - ): DebugLogger; - export { debuglog as debug }; - export type TextEncoder = globalThis.TextEncoder; - 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 - * every other target out of it. */ - export function promisify( - fn: typeof import("node:stream").pipeline, - ): typeof import("node:stream/promises").pipeline; - export function promisify( - fn: typeof import("node:fs").readFile, - ): (path: string, encoding: "utf8" | "utf-8") => Promise; - export function promisify( - fn: (file: string, args?: string[] | null, options?: object) => unknown, - ): ( - file: string, - args?: string[], - options?: { - encoding?: "utf8" | "utf-8"; - cwd?: string; - env?: { [k: string]: string | undefined }; - timeout?: number; - maxBuffer?: number; - killSignal?: string; - windowsHide?: boolean; - }, - ) => Promise<{ stdout: string; stderr: string }>; - export function promisify( - fn: typeof globalThis.setTimeout, - ): typeof import("node:timers/promises").setTimeout; - export function promisify( - fn: typeof globalThis.setImmediate, - ): typeof import("node:timers/promises").setImmediate; - export function promisify(fn: typeof import("node:child_process").exec): ( - command: string, - options?: { - encoding?: "utf8" | "utf-8"; - cwd?: string; - env?: { [k: string]: string | undefined }; - timeout?: number; - maxBuffer?: number; - killSignal?: string; - windowsHide?: boolean; - }, - ) => Promise<{ stdout: string; stderr: string }>; - /* The zlib callback one-shots (deflate, inflate, deflateRaw, inflateRaw, - * gzip, gunzip, unzip) share one type. */ - export function promisify( - fn: typeof import("node:zlib").gzip, - ): (data: string | Uint8Array, options?: import("node:zlib").ZlibOptions) => Promise; - export function promisify( - fn: typeof import("node:stream").finished, - ): typeof import("node:stream/promises").finished; - /* The crypto callback functions: the promise settles with the callback's - * value. */ - export function promisify( - fn: typeof import("node:crypto").randomBytes, - ): (size: number) => Promise; - export function promisify( - fn: typeof import("node:crypto").pbkdf2, - ): ( - password: string | Uint8Array, - salt: string | Uint8Array, - iterations: number, - keylen: number, - digest: string, - ) => Promise; - export function promisify( - fn: typeof import("node:crypto").scrypt, - ): ( - password: import("node:crypto").BinaryLike, - salt: import("node:crypto").BinaryLike, - keylen: number, - options?: import("node:crypto").ScryptOptions, - ) => Promise; - export function promisify( - fn: typeof import("node:crypto").hkdf, - ): ( - digest: string, - ikm: import("node:crypto").BinaryLike | import("node:crypto").KeyObject, - salt: import("node:crypto").BinaryLike, - info: import("node:crypto").BinaryLike, - keylen: number, - ) => Promise; - /* A callback-only function (`obj.close.bind(obj)`): before the targets - * whose leading parameters would otherwise capture it. Builtins that take - * arguments before their callback never match these. */ - export function promisify( - fn: (callback: (err: any, result: TResult) => void) => void, - ): () => Promise; - export function promisify(fn: (callback: (err?: any) => void) => void): () => Promise; - export function promisify( - fn: typeof import("node:crypto").randomFill, - ): ( - buffer: T, - offset?: number, - size?: number, - ) => Promise; - export function promisify(fn: typeof import("node:crypto").randomInt): { - (max: number): Promise; - (min: number, max: number): Promise; - }; - export function promisify( - fn: typeof import("node:crypto").generateKey, - ): ( - type: "hmac" | "aes", - options: { length: number }, - ) => Promise; - /* dns.lookup's promise settles with { address, family } (the address - * array with options.all), and lookupService's with { hostname, service }. */ - export function promisify(fn: typeof import("node:dns").lookup): { - ( - hostname: string, - options: import("node:dns").LookupAllOptions, - ): Promise; - ( - hostname: string, - options?: number | import("node:dns").LookupOneOptions, - ): Promise; - }; - export function promisify( - fn: typeof import("node:dns").lookupService, - ): (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. */ - export function inspect( - object: unknown, - options?: { depth?: number | null; colors?: boolean; [k: string]: unknown }, - ): string; - export function format(format?: unknown, ...args: unknown[]): string; - export function formatWithOptions( - inspectOptions: { depth?: number | null; colors?: boolean; [k: string]: unknown }, - format?: unknown, - ...args: unknown[] - ): string; - export type ParseArgsOptionsType = "boolean" | "string"; - export interface ParseArgsOptionDescriptor { - type: ParseArgsOptionsType; - multiple?: boolean; - short?: string; - default?: string | boolean | string[] | boolean[]; - } - export interface ParseArgsOptionsConfig { - [longOption: string]: ParseArgsOptionDescriptor; - } - export interface ParseArgsConfig { - args?: readonly string[]; - options?: ParseArgsOptionsConfig; - strict?: boolean; - allowPositionals?: boolean; - allowNegative?: boolean; - tokens?: boolean; - } - export type ParseArgsToken = - | { - kind: "option"; - index: number; - name: string; - rawName: string; - value: string | undefined; - inlineValue: boolean | undefined; - } - | { kind: "positional"; index: number; value: string } - | { kind: "option-terminator"; index: number }; - export interface ParseArgsResult { - values: { [longOption: string]: undefined | string | boolean | Array }; - positionals: string[]; - tokens?: ParseArgsToken[]; - } - export function parseArgs(config?: ParseArgsConfig): ParseArgsResult; - /* util.getCallSites (Node ≥22.9): the captured-frame slice harness - * code reads. No stack bookkeeping exists in a compiled binary — every - * reached call fences per site. */ - export interface CallSite { - readonly functionName: string; - readonly scriptName: string; - readonly lineNumber: number; - readonly column: number; - } - export function getCallSites(frameCount?: number): CallSite[]; -} -declare module "node:util" { - export * from "util"; -} - -/* node:util/types — native runtime brands. Other forms retain their - * per-member lowering boundaries. */ -declare module "util/types" { - export function isAnyArrayBuffer(value: unknown): boolean; - export function isArrayBuffer(value: unknown): value is ArrayBuffer; - export function isArrayBufferView(value: unknown): value is ArrayBufferView; - export function isDataView(value: unknown): value is DataView; - export function isTypedArray(value: unknown): boolean; - export function isUint8Array(value: unknown): value is Uint8Array; - export function isUint8ClampedArray(value: unknown): value is Uint8ClampedArray; - export function isUint16Array(value: unknown): value is Uint16Array; - export function isUint32Array(value: unknown): value is Uint32Array; - export function isInt8Array(value: unknown): value is Int8Array; - export function isInt16Array(value: unknown): value is Int16Array; - export function isInt32Array(value: unknown): value is Int32Array; - export function isFloat16Array(value: unknown): value is Float16Array; - export function isFloat32Array(value: unknown): value is Float32Array; - export function isFloat64Array(value: unknown): value is Float64Array; - export function isMap(value: unknown): value is Map; - export function isSet(value: unknown): value is Set; - export function isWeakMap(value: unknown): value is WeakMap; - export function isWeakSet(value: unknown): value is WeakSet; - export function isRegExp(value: unknown): value is RegExp; - export function isNativeError(value: unknown): value is Error; - export function isPromise(value: unknown): value is Promise; - export function isProxy(value: unknown): boolean; - export function isAsyncFunction(value: unknown): boolean; - export function isGeneratorFunction(value: unknown): boolean; - export function isGeneratorObject(value: unknown): boolean; - export function isModuleNamespaceObject(value: unknown): boolean; - export function isDate(value: unknown): value is Date; - export function isSharedArrayBuffer(value: unknown): value is SharedArrayBuffer; - export function isMapIterator(value: unknown): boolean; - export function isSetIterator(value: unknown): boolean; - export function isKeyObject(value: unknown): boolean; - export function isNumberObject(value: unknown): boolean; - export function isStringObject(value: unknown): boolean; - export function isBooleanObject(value: unknown): boolean; - export function isBigIntObject(value: unknown): boolean; - export function isSymbolObject(value: unknown): boolean; - export function isBoxedPrimitive(value: unknown): boolean; - export function isBigInt64Array(value: unknown): value is BigInt64Array; - export function isBigUint64Array(value: unknown): value is BigUint64Array; - export function isCryptoKey(value: unknown): boolean; - export function isExternal(value: unknown): boolean; -} -declare module "node:util/types" { - export * from "util/types"; -} - -/* node:assert — the static assertion surface. The module object IS a - * callable function (`assert(x)` is `assert.ok(x)`), so the declaration - * is the callable-namespace `export =` shape and BOTH `import assert - * from "node:assert"` and `const assert = require("assert")` bind the - * callable; named imports take the members. Failures throw - * AssertionError (name "AssertionError", code "ERR_ASSERTION" — catch and - * read .name/.message/.code). strictEqual/deepStrictEqual compare with - * Object.is over scalars; deepStrictEqual compares composites - * structurally per their static types. The messages here are plain - * strings (Node accepts Errors — that form fences per site), and the - * shallow legacy pair (equal/notEqual) follows Node's == coercions for - * primitive and primitive-union operands, including Node v24's NaN - * exception. deepEqual/notDeepEqual remain declared-but-fenced; - * assert/strict's loose names alias the strict forms and lower. */ -/* node:test — the in-process test runner (prefix-only in Node too: - * require("test") is MODULE_NOT_FOUND). test/it register tests (sync and - * async bodies, skip/todo/only options and method twins), describe/suite - * group them (bodies run at registration, Node's collection phase), - * before/after/beforeEach/afterEach hook the enclosing suite, and the - * TestContext argument carries subtests (t.test), t.skip/t.todo, - * t.diagnostic, and t.assert. mock/run/snapshot are DECLARED so real - * suites typecheck but fence at their use sites (no lowering). */ -declare module "node:test" { - type TestFn = (t: TestContext) => void | Promise; - type SuiteFn = (s: SuiteContext) => void; - type HookFn = (c: TestContext | SuiteContext) => void | Promise; - /* The describe/suite callback's context, and the before/after hook - * context of a suite. */ - interface SuiteContext { - readonly name: string; - readonly fullName: string; - readonly filePath: string | undefined; - readonly attempt: number; - readonly passed: boolean; - diagnostic(message: string): void; - } - interface TestOptions { - skip?: boolean | string; - todo?: boolean | string; - only?: boolean; - concurrency?: number | boolean; - timeout?: number; - plan?: number; - } - interface TestContext { - readonly name: string; - readonly fullName: string; - readonly filePath: string | undefined; - readonly attempt: number; - readonly passed: boolean; - readonly workerId: number | undefined; - readonly tags: readonly string[]; - readonly assert: TestContextAssert; - readonly mock: MockTracker; - test(name?: string, fn?: TestFn): Promise; - test(name?: string, options?: TestOptions, fn?: TestFn): Promise; - test(options?: TestOptions, fn?: TestFn): Promise; - test(fn?: TestFn): Promise; - skip(message?: string): void; - todo(message?: string): void; - diagnostic(message: string): void; - plan(count: number): void; - after(fn: HookFn): void; - before(fn: HookFn): void; - beforeEach(fn: HookFn): void; - afterEach(fn: HookFn): void; - } - interface AssertSnapshotOptions { - readonly serializers?: ReadonlyArray<(value: any) => any> | undefined; - } - interface TestContextAssert { - snapshot(value: any, options?: AssertSnapshotOptions): void; - fileSnapshot(value: any, path: string, options?: AssertSnapshotOptions): void; - ok(value: unknown, message?: string): void; - strictEqual(actual: unknown, expected: unknown, message?: string): void; - notStrictEqual(actual: unknown, expected: unknown, message?: string): void; - deepStrictEqual(actual: unknown, expected: unknown, message?: string): void; - notDeepStrictEqual(actual: unknown, expected: unknown, message?: string): void; - equal(actual: unknown, expected: unknown, message?: string): void; - notEqual(actual: unknown, expected: unknown, message?: string): void; - deepEqual(actual: unknown, expected: unknown, message?: string): void; - notDeepEqual(actual: unknown, expected: unknown, message?: string): void; - fail(message?: string): never; - throws(fn: () => unknown, expected?: unknown, message?: string): void; - match(value: string, regExp: RegExp, message?: string): void; - doesNotMatch(value: string, regExp: RegExp, message?: string): void; - } - /* The name, options and function are each optional, as in Node: a test - * without a name takes the function's name, or "". */ - function test(name?: string, fn?: TestFn): Promise; - function test(name?: string, options?: TestOptions, fn?: TestFn): Promise; - function test(options?: TestOptions, fn?: TestFn): Promise; - function test(fn?: TestFn): Promise; - namespace test { - function skip(name?: string, fn?: TestFn): Promise; - function skip(name?: string, options?: TestOptions, fn?: TestFn): Promise; - function skip(options?: TestOptions, fn?: TestFn): Promise; - function skip(fn?: TestFn): Promise; - function todo(name?: string, fn?: TestFn): Promise; - function todo(name?: string, options?: TestOptions, fn?: TestFn): Promise; - function todo(options?: TestOptions, fn?: TestFn): Promise; - function todo(fn?: TestFn): Promise; - function only(name?: string, fn?: TestFn): Promise; - function only(name?: string, options?: TestOptions, fn?: TestFn): Promise; - function only(options?: TestOptions, fn?: TestFn): Promise; - function only(fn?: TestFn): Promise; - } - const it: typeof test; - function describe(name?: string, fn?: SuiteFn): void; - function describe(name?: string, options?: TestOptions, fn?: SuiteFn): void; - function describe(options?: TestOptions, fn?: SuiteFn): void; - function describe(fn?: SuiteFn): void; - namespace describe { - function skip(name?: string, fn?: SuiteFn): void; - function skip(name?: string, options?: TestOptions, fn?: SuiteFn): void; - function skip(options?: TestOptions, fn?: SuiteFn): void; - function skip(fn?: SuiteFn): void; - function todo(name?: string, fn?: SuiteFn): void; - function todo(name?: string, options?: TestOptions, fn?: SuiteFn): void; - function todo(options?: TestOptions, fn?: SuiteFn): void; - function todo(fn?: SuiteFn): void; - function only(name?: string, fn?: SuiteFn): void; - function only(name?: string, options?: TestOptions, fn?: SuiteFn): void; - function only(options?: TestOptions, fn?: SuiteFn): void; - function only(fn?: SuiteFn): void; - } - const suite: typeof describe; - function before(fn: HookFn): void; - function after(fn: HookFn): void; - function beforeEach(fn: HookFn): void; - function afterEach(fn: HookFn): void; - /* The mock function surface: mock.fn, mock.method and their t.mock - * twins, the tracker's restoreAll and reset, and the calls log and the - * implementation controls on fn.mock. */ - interface MockFunctionCall< - F extends Function, - ReturnType = F extends (...args: any) => infer T ? T : unknown, - Args = F extends (...args: infer Y) => any ? Y : unknown[], - > { - arguments: Args; - error: unknown | undefined; - result: ReturnType | undefined; - this: unknown; - } - interface MockFunctionContext { - readonly calls: MockFunctionCall[]; - callCount(): number; - mockImplementation(implementation: F): void; - mockImplementationOnce(implementation: F, onCall?: number): void; - resetCalls(): void; - restore(): void; - } - type Mock = F & { mock: MockFunctionContext }; - interface MockFunctionOptions { - times?: number; - } - type FunctionPropertyNames = { - [K in keyof T]: T[K] extends Function ? K : never; - }[keyof T]; - interface MockTracker { - fn undefined>(original?: F): Mock; - fn undefined, Implementation extends Function = F>( - original?: F, - implementation?: Implementation, - ): Mock; - method>( - object: MockedObject, - methodName: MethodName, - options?: MockFunctionOptions, - ): MockedObject[MethodName] extends Function ? Mock : never; - method< - MockedObject extends object, - MethodName extends FunctionPropertyNames, - Implementation extends Function, - >( - object: MockedObject, - methodName: MethodName, - implementation: Implementation, - options?: MockFunctionOptions, - ): MockedObject[MethodName] extends Function - ? Mock - : never; - reset(): void; - restoreAll(): void; - readonly timers: MockTimers; - } - interface MockTimersOptions { - apis: ReadonlyArray<"setInterval" | "setTimeout" | "setImmediate" | "Date">; - now?: number | Date | undefined; - } - interface MockTimers { - enable(options?: MockTimersOptions): void; - setTime(milliseconds?: number): void; - reset(): void; - tick(milliseconds?: number): void; - runAll(): void; - [Symbol.dispose](): void; - } - const mock: MockTracker; - /* The context of the test, suite or hook running in the current async - * context; undefined outside one. */ - function getTestContext(): TestContext | SuiteContext | undefined; - /* Declared for typechecking only — every use fences (no lowering). - * `assert` is the module-level custom-assertion registry (v24's - * `const { assert } = require('node:test')`), not TestContext.assert. */ - const assert: { register(name: string, fn: (...args: unknown[]) => unknown): void }; - function run(options?: unknown): unknown; - function snapshot(...args: unknown[]): unknown; - export { - test, - it, - describe, - suite, - before, - after, - beforeEach, - afterEach, - mock, - assert, - getTestContext, - run, - snapshot, - TestContext, - SuiteContext, - TestOptions, - Mock, - MockFunctionContext, - MockFunctionCall, - MockTracker, - MockTimers, - MockTimersOptions, - }; - export default test; -} - -declare module "assert" { - function assert(value: unknown, message?: string): void; - namespace assert { - interface AssertionErrorOptions { - message?: string | undefined; - actual?: unknown; - expected?: unknown; - operator?: string | undefined; - stackStartFn?: Function | undefined; - diff?: "simple" | "full" | undefined; - } - class AssertionError extends Error { - constructor(options: AssertionErrorOptions); - actual: unknown; - expected: unknown; - generatedMessage: boolean; - code: "ERR_ASSERTION"; - operator: string; - } - function ok(value: unknown, message?: string): void; - function strictEqual(actual: unknown, expected: unknown, message?: string): void; - function notStrictEqual(actual: unknown, expected: unknown, message?: string): void; - function deepStrictEqual(actual: unknown, expected: unknown, message?: string): void; - function notDeepStrictEqual(actual: unknown, expected: unknown, message?: string): void; - function partialDeepStrictEqual(actual: unknown, expected: unknown, message?: string): void; - function equal(actual: unknown, expected: unknown, message?: string): void; - function notEqual(actual: unknown, expected: unknown, message?: string): void; - function deepEqual(actual: unknown, expected: unknown, message?: string): void; - function notDeepEqual(actual: unknown, expected: unknown, message?: string): void; - function fail(message?: string): never; - function throws(fn: () => unknown, expected?: unknown, message?: string): void; - function doesNotThrow(fn: () => unknown, expected?: unknown, message?: string): void; - function rejects( - fn: (() => Promise) | Promise, - expected?: unknown, - message?: string, - ): Promise; - function doesNotReject( - fn: (() => Promise) | Promise, - expected?: unknown, - message?: string, - ): Promise; - function match(value: string, regExp: RegExp, message?: string): void; - function doesNotMatch(value: string, regExp: RegExp, message?: string): void; - function ifError(value: unknown): void; - const strict: typeof import("assert/strict"); - interface AssertOptions { - diff?: "simple" | "full" | undefined; - strict?: boolean | undefined; - skipPrototype?: boolean | undefined; - } - type AssertMethodNames = - | "deepEqual" - | "deepStrictEqual" - | "doesNotMatch" - | "doesNotReject" - | "doesNotThrow" - | "equal" - | "fail" - | "ifError" - | "match" - | "notDeepEqual" - | "notDeepStrictEqual" - | "notEqual" - | "notStrictEqual" - | "ok" - | "partialDeepStrictEqual" - | "rejects" - | "strictEqual" - | "throws"; - interface Assert extends Pick {} - interface AssertStrict extends Pick {} - const Assert: { - new (options?: AssertOptions & { strict?: true | undefined }): AssertStrict; - new (options: AssertOptions): Assert; - }; - } - export = assert; -} -declare module "node:assert" { - import assert = require("assert"); - export = assert; -} -/* node:assert/strict — the same surface with the loose names bound to the - * strict comparisons (equal IS strictEqual, ...), exactly Node's aliasing; - * the module object is callable like assert's. */ -declare module "assert/strict" { - function strict(value: unknown, message?: string): void; - namespace strict { - function ok(value: unknown, message?: string): void; - function equal(actual: unknown, expected: unknown, message?: string): void; - function notEqual(actual: unknown, expected: unknown, message?: string): void; - function deepEqual(actual: unknown, expected: unknown, message?: string): void; - function notDeepEqual(actual: unknown, expected: unknown, message?: string): void; - function strictEqual(actual: unknown, expected: unknown, message?: string): void; - function notStrictEqual(actual: unknown, expected: unknown, message?: string): void; - function deepStrictEqual(actual: unknown, expected: unknown, message?: string): void; - function notDeepStrictEqual(actual: unknown, expected: unknown, message?: string): void; - function partialDeepStrictEqual(actual: unknown, expected: unknown, message?: string): void; - function fail(message?: string): never; - function throws(fn: () => unknown, expected?: unknown, message?: string): void; - function doesNotThrow(fn: () => unknown, expected?: unknown, message?: string): void; - function rejects( - fn: (() => Promise) | Promise, - expected?: unknown, - message?: string, - ): Promise; - function doesNotReject( - fn: (() => Promise) | Promise, - expected?: unknown, - message?: string, - ): Promise; - function match(value: string, regExp: RegExp, message?: string): void; - function doesNotMatch(value: string, regExp: RegExp, message?: string): void; - function ifError(value: unknown): void; - const strict: typeof import("assert/strict"); - type AssertOptions = import("assert").AssertOptions; - type Assert = import("assert").Assert; - type AssertStrict = import("assert").AssertStrict; - const Assert: typeof import("assert").Assert; - } - export = strict; -} -declare module "node:assert/strict" { - import strict = require("assert/strict"); - export = strict; -} - -/* node:string_decoder — StringDecoder over Node's whole encoding set: - * write() decodes complete sequences and buffers a trailing partial one - * across chunk boundaries (utf8's truncated sequence, base64's mod-3 - * remainder, utf16le's odd byte / trailing lead surrogate; latin1, ascii - * and hex are stateless); end() flushes the buffered partial. `encoding` - * answers the normalized name. Node's exact algorithms (SEMANTICS.md); - * the encoding must be a literal, and end(buffer) fences — write the - * buffer, then end(). */ -declare module "string_decoder" { - export class StringDecoder { - constructor(encoding?: BufferEncoding); - readonly encoding: string; - write(buffer: Uint8Array): string; - end(buffer?: Uint8Array): string; - } -} -declare module "node:string_decoder" { - export * from "string_decoder"; -} - -/* node:querystring — Node's legacy query-string codec (NOT - * URLSearchParams: '+' means space on the parse side and escape encodes - * spaces as %20). parse answers the null-prototype dictionary as a pure - * index-signature record (repeated keys become string[] buckets; - * @types/node's Dict adds an undefined arm the lowering tolerates); - * stringify serializes string/number/boolean values and arrays of those - * (Node's rules: arrays expand to repeated keys, null/undefined and - * anything else serialize as the empty value). The maxKeys option lowers - * (0 removes the cap, Node's rule); the custom encoder/decoder options - * typecheck under the options-record stance and fence by name at the - * call. decode/encode are Node's own aliases of parse/stringify. */ -declare module "querystring" { - export interface ParseOptions { - maxKeys?: number; - decodeURIComponent?: (str: string) => string; - [option: string]: unknown; - } - export interface StringifyOptions { - encodeURIComponent?: (str: string) => string; - [option: string]: unknown; - } - export interface ParsedUrlQuery { - [key: string]: string | string[] | undefined; - } - export interface ParsedUrlQueryInput { - [key: string]: - | string - | number - | boolean - | bigint - | ReadonlyArray - | null - | undefined; - } - export function parse( - str: string, - sep?: string | null, - eq?: string | null, - options?: ParseOptions, - ): ParsedUrlQuery; - export function stringify( - obj?: ParsedUrlQueryInput, - sep?: string | null, - eq?: string | null, - options?: StringifyOptions, - ): string; - export const decode: typeof parse; - export const encode: typeof stringify; - export function escape(str: string): string; - export function unescape(str: string): string; -} -declare module "node:querystring" { - export * from "querystring"; -} - -/* node:punycode (deprecated, DEP0040) — punycode.js 2.1.0: decode and - * toUnicode throw its RangeErrors on malformed input; ucs2 converts - * between strings and UTF-16 code point arrays. */ -declare module "punycode" { - export function decode(string: string): string; - export function encode(string: string): string; - export function toUnicode(domain: string): string; - export function toASCII(domain: string): string; - export namespace ucs2 { - function decode(string: string): number[]; - function encode(codePoints: readonly number[]): string; - } - export const version: string; -} -declare module "node:punycode" { - export * from "punycode"; -} - -/* node:constants (deprecated, DEP0008) — one frozen object: os.constants' - * dlopen, errno, priority and signals groups, fs.constants, and - * crypto.constants. A platform's missing constant reads as undefined. */ -declare module "constants" { - const constants: { readonly [name: string]: number } & { - readonly defaultCoreCipherList: string; - readonly defaultCipherList: string; - }; - export = constants; -} -declare module "node:constants" { - import constants = require("constants"); - export = constants; -} - /* node:readline — the question/close slice: createInterface over exactly * { input: process.stdin, output: process.stdout }, question(query, cb) * (the query writes to stdout, the callback gets the next line's text), @@ -4791,2395 +1642,6 @@ declare module "node:readline/promises" { export * from "readline/promises"; } -/* node:zlib — the one-shot zlib/raw/gzip codecs lower for string/Buffer - * inputs with Node's default options; libz links only into zlib-using - * binaries. Callback forms use the executable worker pool. Options stay - * declared so reached uses receive a named SC2020 refusal. The Deflate, - * Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw and Unzip streams are - * runtime Transform streams; Brotli and Zstd remain outside the lowered - * surface. */ -declare module "zlib" { - import { Transform } from "stream"; - export interface ZlibOptions { - [option: string]: unknown; - } - export type ZlibCallback = (error: NodeJS.ErrnoException | null, result: Buffer) => void; - export function deflate(data: string | Uint8Array, callback: ZlibCallback): void; - export function deflate( - data: string | Uint8Array, - options: ZlibOptions, - callback: ZlibCallback, - ): void; - export function deflateSync(data: string | Uint8Array, options?: ZlibOptions): Buffer; - export function inflate(data: string | Uint8Array, callback: ZlibCallback): void; - export function inflate( - data: string | Uint8Array, - options: ZlibOptions, - callback: ZlibCallback, - ): void; - export function inflateSync(data: string | Uint8Array, options?: ZlibOptions): Buffer; - export function deflateRaw(data: string | Uint8Array, callback: ZlibCallback): void; - export function deflateRaw( - data: string | Uint8Array, - options: ZlibOptions, - callback: ZlibCallback, - ): void; - export function deflateRawSync(data: string | Uint8Array, options?: ZlibOptions): Buffer; - export function inflateRaw(data: string | Uint8Array, callback: ZlibCallback): void; - export function inflateRaw( - data: string | Uint8Array, - options: ZlibOptions, - callback: ZlibCallback, - ): void; - export function inflateRawSync(data: string | Uint8Array, options?: ZlibOptions): Buffer; - export function gzip(data: string | Uint8Array, callback: ZlibCallback): void; - export function gzip( - data: string | Uint8Array, - options: ZlibOptions, - callback: ZlibCallback, - ): void; - export function gzipSync(data: string | Uint8Array, options?: ZlibOptions): Buffer; - export function gunzip(data: string | Uint8Array, callback: ZlibCallback): void; - export function gunzip( - data: string | Uint8Array, - options: ZlibOptions, - callback: ZlibCallback, - ): void; - export function gunzipSync(data: string | Uint8Array, options?: ZlibOptions): Buffer; - export function unzip(data: string | Uint8Array, callback: ZlibCallback): void; - export function unzip( - data: string | Uint8Array, - options: ZlibOptions, - callback: ZlibCallback, - ): void; - export function unzipSync(data: string | Uint8Array, options?: ZlibOptions): Buffer; - export function crc32(data: string | Uint8Array, value?: number): number; - export function brotliCompressSync(data: string | Uint8Array): Buffer; - export function brotliDecompressSync(data: Uint8Array): Buffer; - /* Brotli needs a codec the static runtime does not vendor: these are - * declared so their uses receive the named refusal. */ - export function brotliCompress(data: string | Uint8Array, callback: ZlibCallback): void; - export function brotliDecompress(data: Uint8Array, callback: ZlibCallback): void; - export function createBrotliCompress(options?: ZlibOptions): Transform; - export function createBrotliDecompress(options?: ZlibOptions): Transform; - export class BrotliCompress extends Transform { - constructor(options?: ZlibOptions); - } - export class BrotliDecompress extends Transform { - constructor(options?: ZlibOptions); - } - /* The stream classes: runtime Transform streams over the zlib engine - * (scr_zlib_stream.c). createGzip(options) and new Gzip(options) build - * the same stream; options are validated at runtime with Node's rules. */ - class ZlibBase extends Transform { - readonly bytesWritten: number; - flush(kind?: number, callback?: () => void): void; - flush(callback: () => void): void; - params(level: number, strategy: number, callback?: () => void): void; - close(callback?: () => void): void; - reset(): void; - } - export class Deflate extends ZlibBase { - constructor(options?: ZlibOptions); - } - export class Inflate extends ZlibBase { - constructor(options?: ZlibOptions); - } - export class Gzip extends ZlibBase { - constructor(options?: ZlibOptions); - } - export class Gunzip extends ZlibBase { - constructor(options?: ZlibOptions); - } - export class DeflateRaw extends ZlibBase { - constructor(options?: ZlibOptions); - } - export class InflateRaw extends ZlibBase { - constructor(options?: ZlibOptions); - } - export class Unzip extends ZlibBase { - constructor(options?: ZlibOptions); - } - export function createDeflate(options?: ZlibOptions): Deflate; - export function createInflate(options?: ZlibOptions): Inflate; - export function createGzip(options?: ZlibOptions): Gzip; - export function createGunzip(options?: ZlibOptions): Gunzip; - export function createDeflateRaw(options?: ZlibOptions): DeflateRaw; - export function createInflateRaw(options?: ZlibOptions): InflateRaw; - export function createUnzip(options?: ZlibOptions): Unzip; - /* Node v24 zlib.constants, literal-typed (the CryptoConstants stance): - * every access bakes as its literal; the object never materializes. The - * members outside BROTLI_* are also module properties (zlib.Z_SYNC_FLUSH). */ - export interface ZlibConstants { - readonly Z_NO_FLUSH: 0; - readonly Z_PARTIAL_FLUSH: 1; - readonly Z_SYNC_FLUSH: 2; - readonly Z_FULL_FLUSH: 3; - readonly Z_FINISH: 4; - readonly Z_BLOCK: 5; - readonly Z_OK: 0; - readonly Z_STREAM_END: 1; - readonly Z_NEED_DICT: 2; - readonly Z_ERRNO: -1; - readonly Z_STREAM_ERROR: -2; - readonly Z_DATA_ERROR: -3; - readonly Z_MEM_ERROR: -4; - readonly Z_BUF_ERROR: -5; - readonly Z_VERSION_ERROR: -6; - readonly Z_NO_COMPRESSION: 0; - readonly Z_BEST_SPEED: 1; - readonly Z_BEST_COMPRESSION: 9; - readonly Z_DEFAULT_COMPRESSION: -1; - readonly Z_FILTERED: 1; - readonly Z_HUFFMAN_ONLY: 2; - readonly Z_RLE: 3; - readonly Z_FIXED: 4; - readonly Z_DEFAULT_STRATEGY: 0; - readonly ZLIB_VERNUM: 4897; - readonly DEFLATE: 1; - readonly INFLATE: 2; - readonly GZIP: 3; - readonly GUNZIP: 4; - readonly DEFLATERAW: 5; - readonly INFLATERAW: 6; - readonly UNZIP: 7; - readonly BROTLI_DECODE: 8; - readonly BROTLI_ENCODE: 9; - readonly ZSTD_DECOMPRESS: 11; - readonly ZSTD_COMPRESS: 10; - readonly Z_MIN_WINDOWBITS: 8; - readonly Z_MAX_WINDOWBITS: 15; - readonly Z_DEFAULT_WINDOWBITS: 15; - readonly Z_MIN_CHUNK: 64; - readonly Z_MAX_CHUNK: number; - readonly Z_DEFAULT_CHUNK: 16384; - readonly Z_MIN_MEMLEVEL: 1; - readonly Z_MAX_MEMLEVEL: 9; - readonly Z_DEFAULT_MEMLEVEL: 8; - readonly Z_MIN_LEVEL: -1; - readonly Z_MAX_LEVEL: 9; - readonly Z_DEFAULT_LEVEL: -1; - readonly BROTLI_OPERATION_PROCESS: 0; - readonly BROTLI_OPERATION_FLUSH: 1; - readonly BROTLI_OPERATION_FINISH: 2; - readonly BROTLI_OPERATION_EMIT_METADATA: 3; - readonly BROTLI_PARAM_MODE: 0; - readonly BROTLI_MODE_GENERIC: 0; - readonly BROTLI_MODE_TEXT: 1; - readonly BROTLI_MODE_FONT: 2; - readonly BROTLI_DEFAULT_MODE: 0; - readonly BROTLI_PARAM_QUALITY: 1; - readonly BROTLI_MIN_QUALITY: 0; - readonly BROTLI_MAX_QUALITY: 11; - readonly BROTLI_DEFAULT_QUALITY: 11; - readonly BROTLI_PARAM_LGWIN: 2; - readonly BROTLI_MIN_WINDOW_BITS: 10; - readonly BROTLI_MAX_WINDOW_BITS: 24; - readonly BROTLI_LARGE_MAX_WINDOW_BITS: 30; - readonly BROTLI_DEFAULT_WINDOW: 22; - readonly BROTLI_PARAM_LGBLOCK: 3; - readonly BROTLI_MIN_INPUT_BLOCK_BITS: 16; - readonly BROTLI_MAX_INPUT_BLOCK_BITS: 24; - readonly BROTLI_PARAM_DISABLE_LITERAL_CONTEXT_MODELING: 4; - readonly BROTLI_PARAM_SIZE_HINT: 5; - readonly BROTLI_PARAM_LARGE_WINDOW: 6; - readonly BROTLI_PARAM_NPOSTFIX: 7; - readonly BROTLI_PARAM_NDIRECT: 8; - readonly BROTLI_DECODER_RESULT_ERROR: 0; - readonly BROTLI_DECODER_RESULT_SUCCESS: 1; - readonly BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT: 2; - readonly BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT: 3; - readonly BROTLI_DECODER_PARAM_DISABLE_RING_BUFFER_REALLOCATION: 0; - readonly BROTLI_DECODER_PARAM_LARGE_WINDOW: 1; - readonly BROTLI_DECODER_NO_ERROR: 0; - readonly BROTLI_DECODER_SUCCESS: 1; - readonly BROTLI_DECODER_NEEDS_MORE_INPUT: 2; - readonly BROTLI_DECODER_NEEDS_MORE_OUTPUT: 3; - readonly BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_NIBBLE: -1; - readonly BROTLI_DECODER_ERROR_FORMAT_RESERVED: -2; - readonly BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_META_NIBBLE: -3; - readonly BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_ALPHABET: -4; - readonly BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_SAME: -5; - readonly BROTLI_DECODER_ERROR_FORMAT_CL_SPACE: -6; - readonly BROTLI_DECODER_ERROR_FORMAT_HUFFMAN_SPACE: -7; - readonly BROTLI_DECODER_ERROR_FORMAT_CONTEXT_MAP_REPEAT: -8; - readonly BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_1: -9; - readonly BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_2: -10; - readonly BROTLI_DECODER_ERROR_FORMAT_TRANSFORM: -11; - readonly BROTLI_DECODER_ERROR_FORMAT_DICTIONARY: -12; - readonly BROTLI_DECODER_ERROR_FORMAT_WINDOW_BITS: -13; - readonly BROTLI_DECODER_ERROR_FORMAT_PADDING_1: -14; - readonly BROTLI_DECODER_ERROR_FORMAT_PADDING_2: -15; - readonly BROTLI_DECODER_ERROR_FORMAT_DISTANCE: -16; - readonly BROTLI_DECODER_ERROR_DICTIONARY_NOT_SET: -19; - readonly BROTLI_DECODER_ERROR_INVALID_ARGUMENTS: -20; - readonly BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MODES: -21; - readonly BROTLI_DECODER_ERROR_ALLOC_TREE_GROUPS: -22; - readonly BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MAP: -25; - readonly BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_1: -26; - readonly BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_2: -27; - readonly BROTLI_DECODER_ERROR_ALLOC_BLOCK_TYPE_TREES: -30; - readonly BROTLI_DECODER_ERROR_UNREACHABLE: -31; - readonly ZSTD_e_continue: 0; - readonly ZSTD_e_flush: 1; - readonly ZSTD_e_end: 2; - readonly ZSTD_fast: 1; - readonly ZSTD_dfast: 2; - readonly ZSTD_greedy: 3; - readonly ZSTD_lazy: 4; - readonly ZSTD_lazy2: 5; - readonly ZSTD_btlazy2: 6; - readonly ZSTD_btopt: 7; - readonly ZSTD_btultra: 8; - readonly ZSTD_btultra2: 9; - readonly ZSTD_c_compressionLevel: 100; - readonly ZSTD_c_windowLog: 101; - readonly ZSTD_c_hashLog: 102; - readonly ZSTD_c_chainLog: 103; - readonly ZSTD_c_searchLog: 104; - readonly ZSTD_c_minMatch: 105; - readonly ZSTD_c_targetLength: 106; - readonly ZSTD_c_strategy: 107; - readonly ZSTD_c_enableLongDistanceMatching: 160; - readonly ZSTD_c_ldmHashLog: 161; - readonly ZSTD_c_ldmMinMatch: 162; - readonly ZSTD_c_ldmBucketSizeLog: 163; - readonly ZSTD_c_ldmHashRateLog: 164; - readonly ZSTD_c_contentSizeFlag: 200; - readonly ZSTD_c_checksumFlag: 201; - readonly ZSTD_c_dictIDFlag: 202; - readonly ZSTD_c_nbWorkers: 400; - readonly ZSTD_c_jobSize: 401; - readonly ZSTD_c_overlapLog: 402; - readonly ZSTD_d_windowLogMax: 100; - readonly ZSTD_CLEVEL_DEFAULT: 3; - readonly ZSTD_error_no_error: 0; - readonly ZSTD_error_GENERIC: 1; - readonly ZSTD_error_prefix_unknown: 10; - readonly ZSTD_error_version_unsupported: 12; - readonly ZSTD_error_frameParameter_unsupported: 14; - readonly ZSTD_error_frameParameter_windowTooLarge: 16; - readonly ZSTD_error_corruption_detected: 20; - readonly ZSTD_error_checksum_wrong: 22; - readonly ZSTD_error_literals_headerWrong: 24; - readonly ZSTD_error_dictionary_corrupted: 30; - readonly ZSTD_error_dictionary_wrong: 32; - readonly ZSTD_error_dictionaryCreation_failed: 34; - readonly ZSTD_error_parameter_unsupported: 40; - readonly ZSTD_error_parameter_combination_unsupported: 41; - readonly ZSTD_error_parameter_outOfBound: 42; - readonly ZSTD_error_tableLog_tooLarge: 44; - readonly ZSTD_error_maxSymbolValue_tooLarge: 46; - readonly ZSTD_error_maxSymbolValue_tooSmall: 48; - readonly ZSTD_error_stabilityCondition_notRespected: 50; - readonly ZSTD_error_stage_wrong: 60; - readonly ZSTD_error_init_missing: 62; - readonly ZSTD_error_memory_allocation: 64; - readonly ZSTD_error_workSpace_tooSmall: 66; - readonly ZSTD_error_dstSize_tooSmall: 70; - readonly ZSTD_error_srcSize_wrong: 72; - readonly ZSTD_error_dstBuffer_null: 74; - readonly ZSTD_error_noForwardProgress_destFull: 80; - readonly ZSTD_error_noForwardProgress_inputEmpty: 82; - } - export const constants: ZlibConstants; - export const Z_NO_FLUSH: 0; - export const Z_PARTIAL_FLUSH: 1; - export const Z_SYNC_FLUSH: 2; - export const Z_FULL_FLUSH: 3; - export const Z_FINISH: 4; - export const Z_BLOCK: 5; - export const Z_OK: 0; - export const Z_STREAM_END: 1; - export const Z_NEED_DICT: 2; - export const Z_ERRNO: -1; - export const Z_STREAM_ERROR: -2; - export const Z_DATA_ERROR: -3; - export const Z_MEM_ERROR: -4; - export const Z_BUF_ERROR: -5; - export const Z_VERSION_ERROR: -6; - export const Z_NO_COMPRESSION: 0; - export const Z_BEST_SPEED: 1; - export const Z_BEST_COMPRESSION: 9; - export const Z_DEFAULT_COMPRESSION: -1; - export const Z_FILTERED: 1; - export const Z_HUFFMAN_ONLY: 2; - export const Z_RLE: 3; - export const Z_FIXED: 4; - export const Z_DEFAULT_STRATEGY: 0; - export const ZLIB_VERNUM: 4897; - export const DEFLATE: 1; - export const INFLATE: 2; - export const GZIP: 3; - export const GUNZIP: 4; - export const DEFLATERAW: 5; - export const INFLATERAW: 6; - export const UNZIP: 7; - export const ZSTD_DECOMPRESS: 11; - export const ZSTD_COMPRESS: 10; - export const Z_MIN_WINDOWBITS: 8; - export const Z_MAX_WINDOWBITS: 15; - export const Z_DEFAULT_WINDOWBITS: 15; - export const Z_MIN_CHUNK: 64; - export const Z_MAX_CHUNK: number; - export const Z_DEFAULT_CHUNK: 16384; - export const Z_MIN_MEMLEVEL: 1; - export const Z_MAX_MEMLEVEL: 9; - export const Z_DEFAULT_MEMLEVEL: 8; - export const Z_MIN_LEVEL: -1; - export const Z_MAX_LEVEL: 9; - export const Z_DEFAULT_LEVEL: -1; - export const ZSTD_e_continue: 0; - export const ZSTD_e_flush: 1; - export const ZSTD_e_end: 2; - export const ZSTD_fast: 1; - export const ZSTD_dfast: 2; - export const ZSTD_greedy: 3; - export const ZSTD_lazy: 4; - export const ZSTD_lazy2: 5; - export const ZSTD_btlazy2: 6; - export const ZSTD_btopt: 7; - export const ZSTD_btultra: 8; - export const ZSTD_btultra2: 9; - export const ZSTD_c_compressionLevel: 100; - export const ZSTD_c_windowLog: 101; - export const ZSTD_c_hashLog: 102; - export const ZSTD_c_chainLog: 103; - export const ZSTD_c_searchLog: 104; - export const ZSTD_c_minMatch: 105; - export const ZSTD_c_targetLength: 106; - export const ZSTD_c_strategy: 107; - export const ZSTD_c_enableLongDistanceMatching: 160; - export const ZSTD_c_ldmHashLog: 161; - export const ZSTD_c_ldmMinMatch: 162; - export const ZSTD_c_ldmBucketSizeLog: 163; - export const ZSTD_c_ldmHashRateLog: 164; - export const ZSTD_c_contentSizeFlag: 200; - export const ZSTD_c_checksumFlag: 201; - export const ZSTD_c_dictIDFlag: 202; - export const ZSTD_c_nbWorkers: 400; - export const ZSTD_c_jobSize: 401; - export const ZSTD_c_overlapLog: 402; - export const ZSTD_d_windowLogMax: 100; - export const ZSTD_CLEVEL_DEFAULT: 3; - export const ZSTD_error_no_error: 0; - export const ZSTD_error_GENERIC: 1; - export const ZSTD_error_prefix_unknown: 10; - export const ZSTD_error_version_unsupported: 12; - export const ZSTD_error_frameParameter_unsupported: 14; - export const ZSTD_error_frameParameter_windowTooLarge: 16; - export const ZSTD_error_corruption_detected: 20; - export const ZSTD_error_checksum_wrong: 22; - export const ZSTD_error_literals_headerWrong: 24; - export const ZSTD_error_dictionary_corrupted: 30; - export const ZSTD_error_dictionary_wrong: 32; - export const ZSTD_error_dictionaryCreation_failed: 34; - export const ZSTD_error_parameter_unsupported: 40; - export const ZSTD_error_parameter_combination_unsupported: 41; - export const ZSTD_error_parameter_outOfBound: 42; - export const ZSTD_error_tableLog_tooLarge: 44; - export const ZSTD_error_maxSymbolValue_tooLarge: 46; - export const ZSTD_error_maxSymbolValue_tooSmall: 48; - export const ZSTD_error_stabilityCondition_notRespected: 50; - export const ZSTD_error_stage_wrong: 60; - export const ZSTD_error_init_missing: 62; - export const ZSTD_error_memory_allocation: 64; - export const ZSTD_error_workSpace_tooSmall: 66; - export const ZSTD_error_dstSize_tooSmall: 70; - export const ZSTD_error_srcSize_wrong: 72; - export const ZSTD_error_dstBuffer_null: 74; - export const ZSTD_error_noForwardProgress_destFull: 80; - export const ZSTD_error_noForwardProgress_inputEmpty: 82; -} -declare module "node:zlib" { - export * from "zlib"; -} - -/* node:net — the inbound-networking slice (scr_net.c links only into - * net-using binaries). TCP only, Node's shapes narrowed to what lowers: - * createServer with an optional connection handler; listen(port[, cb]) - * 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 - * 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'. */ -declare module "net" { - export interface Socket { - /* Whether the socket keeps the event loop alive (ref'd by default). */ - ref(): void; - unref(): void; - readonly remoteAddress: string | undefined; - /* The peer's fields are cached at first read, like remoteAddress; the - * local ones are undefined while connecting and after close. */ - readonly remotePort: number | undefined; - readonly remoteFamily: string | undefined; - readonly localAddress: string | undefined; - readonly localPort: number | undefined; - readonly localFamily: string | undefined; - readonly bytesRead: number; - readonly connecting: boolean; - readonly pending: boolean; - readonly readyState: "opening" | "open" | "readOnly" | "writeOnly" | "closed"; - /* true once the fd is gone (destroy() or full close) — Node's flag. */ - readonly destroyed: boolean; - /* the write half is open: no end() yet, no FIN sent, fd alive. */ - readonly writable: boolean; - /* true until the read half is done (peer FIN / destroy). */ - readonly readable: boolean; - /* every byte the write paths accepted (buffered included — Node - * counts those too; plaintext on TLS sockets). */ - 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; - end(callback?: () => void): void; - end(data: string | Uint8Array, callback?: () => void): void; - destroy(): void; - /* Flow control: pause holds reads off (kernel/TCP backpressure is - * the buffer); resume flows — and discards without listeners, so - * 'end' stays reachable. Both chain, Node's shape. */ - pause(): Socket; - resume(): Socket; - /* TCP_NODELAY on the live fd; missing means true. Chains. */ - setNoDelay(noDelay?: boolean): Socket; - /* SO_KEEPALIVE with the initial delay in ms. Chains. */ - setKeepAlive(enable?: boolean, initialDelay?: number): Socket; - /* end() now, destroy once the FIN actually flushed. */ - destroySoon(): void; - /* An RST instead of a FIN; a connecting socket resets once it - * connects. Throws ERR_INVALID_HANDLE_TYPE on a TLS socket. */ - resetAndDestroy(): Socket; - /* The local address, or {} while connecting and once destroyed. */ - address(): AddressInfo | {}; - /* setEncoding('utf8'): 'data' delivers strings — the IncomingMessage - * twin's contract. */ - setEncoding(encoding: string): void; - /* socket→socket, and socket→response (the extended-CONNECT bridge - * leg: raw chunks become response body writes; EOF ends it). */ - pipe( - destination: Socket | import("http").ServerResponse | import("http2").Http2ServerResponse, - ): void; - /* The callback is a once('timeout') listener (removed again by 0). */ - setTimeout(ms: number, callback?: () => void): void; - /* The paused-mode demux surface: once('readable') + read(1) + - * unshift — read answers exactly n buffered bytes or null. */ - read(size?: number): Buffer | null; - unshift(chunk: Uint8Array): void; - /* secureConnect/secure/session are TLS-socket events declared on the - * shared socket surface (the lowering collapses TLSSocket onto this kind): - * the runtime gates on the transport, so a plain socket's - * registration never fires — Node's exact split. 'session' fires - * once with the serialized session (the received-ticket event). */ - on(event: "data", listener: (chunk: Buffer) => void): void; - /* After setEncoding the chunk is a string. */ - on(event: "data", listener: (chunk: string) => void): void; - on( - event: - | "end" - | "connect" - | "ready" - | "timeout" - | "readable" - | "finish" - | "secureConnect" - | "secure", - listener: () => void, - ): void; - on(event: "close", listener: (hadError: boolean) => void): void; - on(event: "error", listener: (err: Error) => void): void; - on(event: "session", listener: (session: Buffer) => void): void; - /* The host name's resolution, before the connect. */ - on( - event: "lookup", - listener: (err: Error | null, address: string, family: number, host: string) => void, - ): void; - /* addListener IS on (Node aliases them) — the suite spells both. */ - addListener(event: "data", listener: (chunk: Buffer) => void): void; - /* After setEncoding the chunk is a string. */ - addListener(event: "data", listener: (chunk: string) => void): void; - addListener( - event: - | "end" - | "connect" - | "ready" - | "timeout" - | "readable" - | "finish" - | "secureConnect" - | "secure", - listener: () => void, - ): void; - addListener(event: "close", listener: (hadError: boolean) => void): void; - addListener(event: "error", listener: (err: Error) => void): void; - addListener(event: "session", listener: (session: Buffer) => void): void; - /* The host name's resolution, before the connect. */ - addListener( - event: "lookup", - listener: (err: Error | null, address: string, family: number, host: string) => void, - ): void; - once(event: "data", listener: (chunk: Buffer) => void): void; - /* After setEncoding the chunk is a string. */ - once(event: "data", listener: (chunk: string) => void): void; - once( - event: - | "end" - | "connect" - | "ready" - | "timeout" - | "readable" - | "finish" - | "secureConnect" - | "secure", - listener: () => void, - ): void; - once(event: "close", listener: (hadError: boolean) => void): void; - once(event: "error", listener: (err: Error) => void): void; - once(event: "session", listener: (session: Buffer) => void): void; - /* The host name's resolution, before the connect. */ - once( - event: "lookup", - listener: (err: Error | null, address: string, family: number, host: string) => void, - ): void; - } - export interface Server { - /* Whether the server keeps the event loop alive (ref'd by default). */ - ref(): void; - unref(): void; - readonly listening: boolean; - /* Connections past this many are closed with a 'drop' event; - * undefined (the default) or null lifts the cap. */ - maxConnections: number | undefined; - /* Node reads it only in cluster mode: a stored value here. */ - dropMaxConnection: boolean | undefined; - /* cb(null, count) on the next tick. */ - getConnections(callback: (error: Error | null, count: number) => void): Server; - /* Node answers the server itself (`return this` chaining). */ - listen(port: number, callback?: () => void): Server; - /* The positional bind address (the options form's host in argument - * spelling). */ - listen(port: number, host: string, callback?: () => void): Server; - /* A Unix-socket server; the socket file is removed when it closes. */ - listen(path: string, callback?: () => void): Server; - /* The explicit-interface bind: host is an IP literal (absent = the - * host-less dual-stack any); ipv6Only sets IPV6_V6ONLY and reusePort - * requests kernel connection distribution where Node supports it. */ - listen( - options: { port: number; host?: string; ipv6Only?: boolean; reusePort?: boolean }, - callback?: () => void, - ): Server; - close(callback?: () => void): void; - /* The bound AddressInfo (Node answers null before listen — this - * surface answers the record with port 0 there, the serverPort - * stance). */ - address(): import("node:dgram").AddressInfo; - /* The demux route: hand an accepted socket to another server. */ - emit(event: "connection", socket: Socket): boolean; - on(event: "connection" | "secureConnection", listener: (socket: Socket) => void): void; - on(event: "close" | "listening", listener: () => void): void; - on(event: "drop", listener: (data?: DropArgument) => void): void; - on(event: "timeout", listener: (socket: Socket) => void): void; - on(event: "error", listener: (err: Error) => void): void; - /* The WebSocket handover: fires INSTEAD of 'request' for - * Connection: upgrade requests, with the raw socket + head bytes; - * 'connect' is its twin for CONNECT requests (HTTP tunneling). */ - on( - event: "upgrade" | "connect", - listener: (req: import("http").IncomingMessage, socket: Socket, head: Buffer) => void, - ): void; - /* http.createServer(handler)'s event twin (a no-parser net server - * never fires it, like Node). */ - on( - event: "request" | "checkContinue" | "checkExpectation", - listener: (req: import("http").IncomingMessage, res: import("http").ServerResponse) => void, - ): void; - /* addListener IS on (Node aliases them) — the suite spells both. */ - addListener(event: "connection" | "secureConnection", listener: (socket: Socket) => void): void; - addListener(event: "close" | "listening", listener: () => void): void; - addListener(event: "drop", listener: (data?: DropArgument) => void): void; - addListener(event: "timeout", listener: (socket: Socket) => void): void; - addListener(event: "error", listener: (err: Error) => void): void; - addListener( - event: "upgrade" | "connect", - listener: (req: import("http").IncomingMessage, socket: Socket, head: Buffer) => void, - ): void; - addListener( - event: "request" | "checkContinue" | "checkExpectation", - listener: (req: import("http").IncomingMessage, res: import("http").ServerResponse) => void, - ): void; - once(event: "connection" | "secureConnection", listener: (socket: Socket) => void): void; - once(event: "close" | "listening", listener: () => void): void; - once(event: "drop", listener: (data?: DropArgument) => void): void; - once(event: "timeout", listener: (socket: Socket) => void): void; - once(event: "error", listener: (err: Error) => void): void; - once( - event: "upgrade" | "connect", - listener: (req: import("http").IncomingMessage, socket: Socket, head: Buffer) => void, - ): void; - once( - event: "request" | "checkContinue" | "checkExpectation", - listener: (req: import("http").IncomingMessage, res: import("http").ServerResponse) => void, - ): void; - } - /* The 'drop' payload: the turned-away connection's addresses. */ - export interface DropArgument { - localAddress?: string; - localPort?: number; - localFamily?: string; - remoteAddress?: string; - remotePort?: number; - remoteFamily?: string; - } - export type AddressInfo = import("node:dgram").AddressInfo; - export type IPVersion = "ipv4" | "ipv6"; - export interface SocketAddressInitOptions { - address?: string; - family?: IPVersion; - flowlabel?: number; - port?: number; - } - /* A native handle (scr_net.c); the getters read its parsed address. */ - export class SocketAddress { - constructor(options?: SocketAddressInitOptions); - readonly address: string; - readonly family: IPVersion; - readonly port: number; - readonly flowlabel: number; - toJSON(): { address: string; port: number; family: IPVersion; flowlabel: number }; - /* undefined when the input is not "[:]" */ - static parse(input: string): SocketAddress | undefined; - static isSocketAddress(value: unknown): value is SocketAddress; - } - /* Address, range and subnet rules; check() matches IPv4 addresses - * against IPv6 rules through their IPv4-mapped form, as Node does. */ - export class BlockList { - constructor(); - readonly rules: readonly string[]; - readonly size: number; - addAddress(address: string | SocketAddress, type?: IPVersion): void; - addAddresses(addresses: readonly (string | SocketAddress)[], type?: IPVersion): void; - addRange(start: string | SocketAddress, end: string | SocketAddress, type?: IPVersion): void; - addSubnet(net: string | SocketAddress, prefix: number, type?: IPVersion): void; - addCIDR(cidr: string): void; - addCIDRs(cidrs: readonly string[]): void; - removeAddress(address: string | SocketAddress, type?: IPVersion): void; - removeRange(start: string | SocketAddress, end: string | SocketAddress, type?: IPVersion): void; - removeSubnet(net: string | SocketAddress, prefix: number, type?: IPVersion): void; - removeCIDR(cidr: string): void; - clear(): void; - check(address: string | SocketAddress, type?: IPVersion): boolean; - toJSON(): string[]; - fromJSON(data: string | readonly string[]): void; - static isBlockList(value: unknown): value is BlockList; - static readonly PRIVATE_RANGES: readonly string[]; - } - /* Node's IPv4Reg and IPv6Reg tests: 4, 6, or 0 for isIP. */ - export function isIP(input: string): number; - export function isIPv4(input: string): boolean; - export function isIPv6(input: string): boolean; - /* The Socket VALUE — the `x instanceof net.Socket` narrowing target - * (the h2 compat 'connect' listener's test). Constructing bare sockets - * has no lowering; the construct signature exists for instanceof. */ - export const Socket: abstract new () => Socket; - export function createServer(connectionListener?: (socket: Socket) => void): Server; - export function connect(port: number, host?: string, connectListener?: () => void): Socket; - export function createConnection( - port: number, - host?: string, - connectListener?: () => void, - ): Socket; - /* The caller-resolver dial (@types/node's net.LookupFunction): the - * runtime invokes it as Node's autoSelectFamily dial does (options - * { hints, all: true }, so the callback answers an address list) and - * dials the answered addresses in order. */ - export type LookupFunction = ( - hostname: string, - options: import("node:dns").LookupOptions, - callback: ( - err: NodeJS.ErrnoException | null, - address: string | import("node:dns").LookupAddress[], - family?: number, - ) => void, - ) => void; - export function connect( - options: { - port: number; - host?: string; - autoSelectFamily?: boolean; - lookup?: LookupFunction; - }, - connectListener?: () => void, - ): Socket; - export function createConnection( - options: { - port: number; - host?: string; - autoSelectFamily?: boolean; - lookup?: LookupFunction; - }, - connectListener?: () => void, - ): Socket; - /* The happy-eyeballs attempt budget (Node's process-wide knob; the - * autoSelectFamily dial consults it): one runtime int, default 250ms - * like Node's. */ - export function getDefaultAutoSelectFamilyAttemptTimeout(): number; - export function setDefaultAutoSelectFamilyAttemptTimeout(value: number): void; -} -declare module "node:net" { - export * from "net"; -} - -/* node:http — the SERVER and CLIENT slices (scr_http.c layers a - * hand-written HTTP/1.1 parser + serializer on scr_net.c; both link only - * into using binaries). createServer(handler) returns a net.Server — - * listen/close/address().port/'error' are the same lowered surface. The - * request carries url/method (always present on server requests), - * statusCode (a number on client responses, undefined on server - * requests), the underlying socket, and lowercased headers read as - * `string | undefined`; the body streams through on("data"/"end"). The - * response is setHeader/writeHead/write/end/destroy with Node's framing - * (Content-Length for a single end(body), chunked after writeHead/write) - * and the auto Date/Connection headers; writeHead's headers argument is - * an object literal or a Record. request/get dial one - * connection per call (no agent pooling) with Node's exact wire head. */ -declare module "http" { - import { Server as NetServer, Socket } from "net"; - export const METHODS: string[]; - export const STATUS_CODES: { [status: number]: string | undefined }; - export const maxHeaderSize: number; - export interface Server extends NetServer { - timeout: number; - setTimeout(msecs: number, callback?: (socket: Socket) => void): this; - closeAllConnections(): void; - closeIdleConnections(): void; - keepAliveTimeout: number; - keepAliveTimeoutBuffer: number; - headersTimeout: number; - requestTimeout: number; - } - export type RequestListener = (req: IncomingMessage, res: ServerResponse) => void; - /* The Server VALUE — Node's constructor works with and without `new` - * (test/parallel's http.Server(fn) spelling); both route to the - * createServer lowering. */ - export const Server: { - new (requestListener?: (req: IncomingMessage, res: ServerResponse) => void): Server; - new ( - options: ServerOptions, - requestListener?: (req: IncomingMessage, res: ServerResponse) => void, - ): Server; - (requestListener?: (req: IncomingMessage, res: ServerResponse) => void): Server; - ( - options: ServerOptions, - requestListener?: (req: IncomingMessage, res: ServerResponse) => void, - ): Server; - }; - /* The options-record stance (the RequestOptions precedent): every - * documented key typechecks; the lowering's option walk decides per - * key — requireHostHeader: false, joinDuplicateHeaders, and - * keepAliveTimeoutBuffer lower, - * documented-but-unlowered keys fence by name, unknown keys drop. */ - export interface ServerOptions { - requireHostHeader?: boolean; - joinDuplicateHeaders?: boolean; - keepAliveTimeoutBuffer?: number; - maxHeaderSize?: number; - [option: string]: unknown; - } - /* The outgoing-header shape, @types/node's matrix: numbers format via - * String(n), arrays write one line per element (set-cookie's shape). - * Incoming headers stay `string | undefined` — what the parser answers - * (under @types/node the two SLOTS unify so `{ ...req.headers }` - * merges into outgoing literals as a copy; the fallback keeps reads - * simple and spreads target the same string | undefined slot). */ - export type OutgoingHttpHeaders = { [name: string]: number | string | string[] | undefined }; - export interface IncomingMessage { - readonly url: string; - readonly method: string; - readonly httpVersion: string; - readonly complete: boolean; - readonly aborted: boolean; - readonly statusCode: number | undefined; - readonly statusMessage: string | undefined; - readonly socket: Socket; - readonly connection: Socket; - readonly headers: { [name: string]: string | undefined }; - readonly headersDistinct: { [name: string]: string[] | undefined }; - readonly rawHeaders: string[]; - readonly trailers: { [name: string]: string | undefined }; - readonly trailersDistinct: { [name: string]: string[] | undefined }; - readonly rawTrailers: string[]; - resume(): void; - pause(): void; - destroy(): void; - setTimeout(msecs: number, callback?: () => void): this; - /* setEncoding('utf8'): 'data' delivers strings (other real encodings - * throw a "not supported yet" Error at runtime; unknown names throw - * ERR_UNKNOWN_ENCODING). */ - setEncoding(encoding: string): void; - /* The proxy legs: the body streams into a ServerResponse, a - * ClientRequest, or a raw Socket; natural end ends the destination. */ - pipe( - destination: ServerResponse | import("http2").Http2ServerResponse | ClientRequest | Socket, - ): void; - on(event: "data", listener: (chunk: any) => void): void; - on(event: "end" | "close" | "aborted", listener: () => void): void; - on(event: "error", listener: (err: Error) => void): void; - /* addListener IS on (Node aliases them) — the suite spells both. */ - addListener(event: "data", listener: (chunk: any) => void): void; - addListener(event: "end" | "close" | "aborted", listener: () => void): void; - addListener(event: "error", listener: (err: Error) => void): void; - once(event: "data", listener: (chunk: any) => void): void; - once(event: "end" | "close" | "aborted", listener: () => void): void; - once(event: "error", listener: (err: Error) => void): void; - } - export interface ServerResponse { - readonly headersSent: boolean; - readonly writableEnded: boolean; - readonly finished: boolean; - readonly writableFinished: boolean; - readonly writableCorked: number; - readonly req: IncomingMessage; - readonly socket: Socket | null; - readonly connection: Socket | null; - sendDate: boolean; - strictContentLength: boolean; - /* Node's writable head properties: the implicit head reads them. */ - statusCode: number; - statusMessage: string | undefined; - /* The value keeps its type: getHeader answers the number, string or - * array that was set. */ - setHeader(name: string, value: number | string | readonly string[]): void; - appendHeader(name: string, value: string | readonly string[]): void; - getHeader(name: string): number | string | string[] | undefined; - getHeaderNames(): string[]; - getRawHeaderNames(): string[]; - getHeaders(): OutgoingHttpHeaders; - hasHeader(name: string): boolean; - removeHeader(name: string): void; - /* Both overloads answer the response (`return this` chaining); the - * headers argument also takes Node's flat [name, value, ...] array. */ - writeHead(statusCode: number, headers?: OutgoingHttpHeaders | string[]): ServerResponse; - writeHead( - statusCode: number, - statusMessage: string, - headers?: OutgoingHttpHeaders | string[], - ): ServerResponse; - writeContinue(callback?: () => void): void; - writeProcessing(callback?: () => void): void; - writeEarlyHints(hints: Record, callback?: () => void): void; - write(data: string | Uint8Array): void; - flushHeaders(): void; - cork(): void; - uncork(): void; - addTrailers(headers: OutgoingHttpHeaders | ReadonlyArray<[string, string]>): void; - setTimeout(msecs: number, callback?: () => void): this; - /* end's callback forms fire once the body went out (the 'finish' - * emit, deferred past the handler's synchronous tail). */ - end(data?: string | Uint8Array, callback?: () => void): void; - end(callback: () => void): void; - destroy(): void; - on(event: "close" | "finish", listener: () => void): void; - /* addListener IS on (Node aliases them) — the suite spells both. */ - addListener(event: "close" | "finish", listener: () => void): void; - once(event: "close" | "finish", listener: () => void): void; - } - /* The CLIENT slice (http.request/http.get): the options-object form - * with hostname/host, port, path, method, timeout, and headers; the - * callback receives the response as an IncomingMessage (statusCode, - * headers, body via on("data"/"end")). */ - export interface RequestOptions { - hostname?: string | null; - host?: string | null; - /* A string port (a URL's .port) is accepted, as by Node. */ - port?: number | string | null; - path?: string | null; - method?: string; - timeout?: number; - headers?: OutgoingHttpHeaders; - /* The caller's own dialer (the proxy's loopback dial): invoked once, - * synchronously; its socket carries the exchange. */ - createConnection?: () => Socket; - /* agent: false is a one-shot dial with Connection: close — exactly - * the compiled client's model; null/undefined are the default; an - * Agent VALUE threads through (getName-keyed maxSockets accounting - * over one-dial-per-request connections). */ - agent?: Agent | boolean | null; - /* Aborting destroys the request with an AbortError. */ - signal?: AbortSignal; - /* HTTP over the Unix socket at this path instead of host:port. */ - socketPath?: string; - /* The options-record stance: Node ignores an options record's - * unknown keys, so every key typechecks here — the option WALK in - * the lowering decides per key (lower, fence by name, or drop - * undocumented keys exactly like Node). */ - [option: string]: unknown; - } - /* The Agent lowers to a checked-dynamic HANDLE: option parsing at the - * literal construction site, getName, destroy, the sockets/requests/ - * freeSockets snapshots, and REAL maxSockets accounting (over-limit - * requests defer their dial and queue). keepAlive: true keeps finished - * connections in freeSockets for the next request to the same name, - * taken in `scheduling` order. */ - export interface AgentOptions { - keepAlive?: boolean; - keepAliveMsecs?: number; - maxSockets?: number; - maxFreeSockets?: number; - maxTotalSockets?: number; - scheduling?: "fifo" | "lifo"; - timeout?: number; - port?: number; - [option: string]: unknown; - } - export class Agent { - constructor(options?: AgentOptions); - destroy(): void; - /* Node's exact key shape: host:port:localAddress[:family][:socketPath]. */ - getName(options?: { - host?: string; - port?: number; - localAddress?: string; - family?: number; - socketPath?: string; - [option: string]: unknown; - }): string; - maxSockets: number; - maxFreeSockets: number; - keepAliveMsecs: number; - readonly keepAlive: boolean; - readonly protocol: string; - /* Settable — a portless request through this agent dials it (Node's - * option merge). */ - defaultPort: number; - readonly totalSocketCount: number; - readonly sockets: { [key: string]: unknown[] }; - readonly freeSockets: { [key: string]: unknown[] }; - readonly requests: { [key: string]: unknown[] }; - } - /* Node's keepAlive global agent for requests without an agent option. */ - export const globalAgent: Agent; - export interface ClientRequest { - readonly destroyed: boolean; - readonly aborted: boolean; - readonly socket: Socket; - readonly connection: Socket; - readonly writableCorked: number; - readonly method: string; - readonly path: string; - readonly host: string; - readonly protocol: string; - readonly headersSent: boolean; - readonly writableEnded: boolean; - readonly writableFinished: boolean; - readonly finished: boolean; - readonly reusedSocket: boolean; - setHeader(name: string, value: string): void; - getHeader(name: string): string | undefined; - hasHeader(name: string): boolean; - removeHeader(name: string): void; - getHeaderNames(): string[]; - getRawHeaderNames(): string[]; - getHeaders(): OutgoingHttpHeaders; - flushHeaders(): void; - addTrailers(headers: OutgoingHttpHeaders | ReadonlyArray<[string, string]>): void; - cork(): void; - uncork(): void; - setNoDelay(noDelay?: boolean): void; - setSocketKeepAlive(enable?: boolean, initialDelay?: number): void; - setTimeout(timeout: number, callback?: () => void): this; - write(data: string | Uint8Array): void; - end(data?: string | Uint8Array): void; - destroy(error?: Error): void; - abort(): void; - on(event: "response", listener: (res: IncomingMessage) => void): void; - on(event: "socket", listener: (socket: Socket) => void): void; - on( - event: "upgrade", - listener: (res: IncomingMessage, socket: Socket, head: Buffer) => void, - ): void; - on(event: "timeout" | "close" | "finish" | "abort" | "continue", listener: () => void): void; - on(event: "error", listener: (err: Error) => void): void; - /* addListener IS on (Node aliases them) — the suite spells both. */ - addListener(event: "response", listener: (res: IncomingMessage) => void): void; - addListener(event: "socket", listener: (socket: Socket) => void): void; - addListener( - event: "upgrade", - listener: (res: IncomingMessage, socket: Socket, head: Buffer) => void, - ): void; - addListener(event: "timeout" | "close" | "finish" | "abort", listener: () => void): void; - addListener(event: "error", listener: (err: Error) => void): void; - once(event: "response", listener: (res: IncomingMessage) => void): void; - once(event: "socket", listener: (socket: Socket) => void): void; - once( - event: "upgrade", - listener: (res: IncomingMessage, socket: Socket, head: Buffer) => void, - ): void; - once(event: "timeout" | "close" | "finish" | "abort" | "continue", listener: () => void): void; - once(event: "error", listener: (err: Error) => void): void; - } - export function createServer( - requestListener?: (req: IncomingMessage, res: ServerResponse) => void, - ): Server; - export function createServer( - options: ServerOptions, - requestListener?: (req: IncomingMessage, res: ServerResponse) => void, - ): Server; - /* One signature with a UNION first parameter rather than Node's two - * overloads: `const requestFn = tls ? https.request : http.request` - * (the portless isProxyRunning shape) unions two function types, and - * resolving a call on that union collapses each constituent to a - * single signature — with an overload SET on both sides the options - * literal ends up checked against URL, in either declaration order. - * A union parameter has nothing to collapse. The lowering dispatches - * on the argument's own type, so it reads the same either way. - * - * `target` is a URL string or URL object (the spelling a from-scratch - * client reaches for first — dialed directly, with a non-http scheme - * an ERR_INVALID_PROTOCOL at runtime rather than a type error), or - * the options record. */ - export function request( - target: RequestOptions | string | URL, - callback?: (res: IncomingMessage) => void, - ): ClientRequest; - export function get( - target: RequestOptions | string | URL, - callback?: (res: IncomingMessage) => void, - ): ClientRequest; - export function validateHeaderName(name: string, label?: string): void; - export function validateHeaderValue(name: string, value: unknown): void; -} -declare module "node:http" { - export * from "http"; -} - -/* node:tls — the TLS server slice (scr_tls.c over scr_net.c with the - * vendored mbedTLS; linked only into using binaries). - * createServer({ cert, key }, handler) returns a net.Server; the handler - * is Node's 'secureConnection' — it fires post-handshake with a socket - * that behaves exactly like a net socket (write/end/destroy/pipe and the - * data/end/close/error events are the same lowered surface). cert/key - * are PEM strings or Buffers. Handshake failures tear the socket down - * without emitting an event, Node's default for 'tlsClientError' with no listener. */ -declare module "tls" { - import { Server, Socket } from "net"; - export { Server }; - export interface TlsOptions { - /* Optional in the DECLARATION only (real Node programs build these - * literals with every TLS knob): the lowering still requires both — - * a literal without them fences at the call site. */ - cert?: string | Uint8Array; - key?: string | Uint8Array; - /* The options-record stance (see http.RequestOptions): every key - * typechecks; the option walk fences documented-but-unlowered keys - * by name and drops undocumented ones like Node. */ - [option: string]: unknown; - } - export function createServer( - options: TlsOptions, - secureConnectionListener?: (socket: TLSSocket) => void, - ): Server; - /** TLSSocket — post-handshake it IS a net socket to every lowered - * member (the mapping collapses it onto the socket kind; the - * secureConnect/session events ride the shared socket declarations). - * The TLS extras: authorized/authorizationError answer Node's verify - * split — authorizationError is the verify-failure CODE STRING - * (DEPTH_ZERO_SELF_SIGNED_CERT, ...), or null. */ - export interface TLSSocket extends Socket { - readonly encrypted: true; - authorized: boolean; - authorizationError: string | null; - /* null before the handshake, then the negotiated protocol or false */ - readonly alpnProtocol: string | false | null; - /* "TLSv1.2" or "TLSv1.3"; null before the handshake and after close */ - getProtocol(): string | null; - /* OpenSSL's suite name, the IANA name and the suite's version */ - getCipher(): CipherNameAndProtocol; - /* whether the handshake resumed a cached session */ - isSessionReused(): boolean; - } - /** new tls.TLSSocket(socket, options): the server-mode wrap of an open - * socket (a server connection or an http CONNECT socket). The options - * record reads at runtime; isServer, cert, key, secureContext, - * ALPNProtocols, minVersion and maxVersion are implemented. */ - export interface TLSSocketOptions { - isServer?: boolean; - cert?: string | Uint8Array; - key?: string | Uint8Array; - secureContext?: SecureContext; - ALPNProtocols?: string[] | Uint8Array; - minVersion?: string; - maxVersion?: string; - [option: string]: unknown; - } - export const TLSSocket: new (socket: Socket, options: TLSSocketOptions) => TLSSocket; - export interface CipherNameAndProtocol { - name: string; - standardName: string; - version: string; - } - export const DEFAULT_MIN_VERSION: string; - export const DEFAULT_MAX_VERSION: string; - /** tls.connect options — the RUNTIME options record (members read at - * runtime; port/host/socket/rejectUnauthorized/ca/servername are the - * honestly-implemented set, every other documented member throws the - * runtime fence — the options-record stance's divergence-66 shape). */ - export interface ConnectionOptions { - port?: number; - host?: string; - /* an open socket to run the client handshake over */ - socket?: Socket; - rejectUnauthorized?: boolean; - ca?: string | Uint8Array | ReadonlyArray; - servername?: string; - [option: string]: unknown; - } - export function connect( - options: ConnectionOptions, - secureConnectListener?: () => void, - ): TLSSocket; - export function connect( - port: number, - options?: ConnectionOptions, - secureConnectListener?: () => void, - ): TLSSocket; - export function connect( - port: number, - host?: string, - options?: ConnectionOptions, - secureConnectListener?: () => void, - ): TLSSocket; - /** An opaque parsed cert/key pair — what an SNI callback answers with - * (the value is a runtime handle; the members are runtime-internal). */ - export interface SecureContext {} - export function createSecureContext(options: TlsOptions): SecureContext; - /** The CA-store introspection surface (scr_tls_ca.c): per-type cached - * PEM string arrays. The host bundle stands in for Node's compiled-in - * Mozilla roots ('bundled', and rootCertificates) AND the platform - * store ('system') — the Windows certificate stores or POSIX bundle probe; - * 'extra' reads NODE_EXTRA_CA_CERTS. An unknown type string throws - * Node's ERR_INVALID_ARG_VALUE TypeError. */ - export function getCACertificates(type?: string): string[]; - export const rootCertificates: readonly string[]; - /** Replaces the 'default' set (deduped) and the trust anchors the TLS - * client verifies against; a certificate-free non-empty array throws - * Node's ERR_CRYPTO_OPERATION_FAILED Error. */ - export function setDefaultCACertificates(certs: readonly string[]): void; -} -declare module "node:tls" { - export * from "tls"; -} - -/* node:https — http over the TLS transport (scr_tls.c). - * createServer({ cert, key }, handler) is the http server behind a - * handshake; request/get are the http client with port 443 default and - * the local-CA knobs: `ca` (PEM string/Buffer) replaces the trust - * anchors, rejectUnauthorized: false disables verification (the - * self-signed probe shape). With neither, /etc/ssl/cert.pem stands in - * for Node's bundled roots. */ -declare module "https" { - import { Server } from "http"; - import { Agent, ClientRequest, IncomingMessage, ServerResponse } from "http"; - export { Agent, Server }; - /* Node's keepAlive global agent for requests without an agent option. */ - export const globalAgent: Agent; - export interface ServerOptions { - cert: string | Uint8Array; - key: string | Uint8Array; - /* The options-record stance (see http.RequestOptions). */ - [option: string]: unknown; - } - export interface RequestOptions { - hostname?: string; - host?: string; - port?: number; - path?: string; - method?: string; - timeout?: number; - headers?: { [name: string]: string }; - rejectUnauthorized?: boolean; - ca?: string | Uint8Array; - agent?: Agent | boolean | null; - /* Aborting destroys the request with an AbortError. */ - signal?: AbortSignal; - socketPath?: string; - /* The options-record stance (see http.RequestOptions). */ - [option: string]: unknown; - } - export function createServer( - options: ServerOptions, - requestListener: (req: IncomingMessage, res: ServerResponse) => void, - ): Server; - /* One signature with a union first parameter, for the request-fn - * ternary's sake (see the http block). A URL target takes no options, - * which means Node's defaults — the certificate is verified against - * the default trust anchors. */ - export function request( - target: RequestOptions | string | URL, - callback?: (res: IncomingMessage) => void, - ): ClientRequest; - export function get( - target: RequestOptions | string | URL, - callback?: (res: IncomingMessage) => void, - ): ClientRequest; -} -declare module "node:https" { - export * from "https"; -} - -/* node:http2 — the compatibility surface: allowHTTP1 servers negotiate - * h2 or HTTP/1.1 and expose the shared request/response API. HTTP/2 - * requests retain their backing stream; server session failures surface - * through sessionError. */ -declare module "http2" { - import { Socket } from "net"; - export interface Http2Stream { - on(event: "error" | "close", listener: (err: Error) => void): void; - once(event: "error" | "close", listener: (err: Error) => void): void; - addListener(event: "error" | "close", listener: (err: Error) => void): void; - destroy(): void; - } - export interface Http2ServerRequest { - readonly url: string; - readonly method: string; - readonly statusCode: number | undefined; - readonly socket: Socket; - readonly headers: { [name: string]: string | undefined }; - readonly stream: Http2Stream; - /* The h2 compat request's version triple ("2.0" over real h2 streams; - * the allowHTTP1 lowering's HTTP/1.1 connections answer 1.x). */ - readonly httpVersion: string; - readonly httpVersionMajor: number; - readonly httpVersionMinor: number; - readonly aborted: boolean; - readonly complete: boolean; - resume(): void; - destroy(): void; - setEncoding(encoding: string): void; - pipe(destination: Socket | import("http").ClientRequest): void; - on(event: "data", listener: (chunk: Buffer) => void): void; - on(event: "end" | "close" | "aborted", listener: () => void): void; - on(event: "error", listener: (err: Error) => void): void; - /* addListener IS on (Node aliases them) — and the compat req IS the - * http.IncomingMessage surface, so the alias mirrors here too. */ - addListener(event: "data", listener: (chunk: Buffer) => void): void; - addListener(event: "end" | "close" | "aborted", listener: () => void): void; - addListener(event: "error", listener: (err: Error) => void): void; - once(event: "data", listener: (chunk: Buffer) => void): void; - once(event: "end" | "close" | "aborted", listener: () => void): void; - once(event: "error", listener: (err: Error) => void): void; - } - export interface Http2ServerResponse { - readonly headersSent: boolean; - readonly writableEnded: boolean; - readonly writableCorked: number; - /* The same lowered surface as http.ServerResponse — the allowHTTP1 - * lowering serves every connection as HTTP/1.1, where the compat - * response IS this parser's response handle. */ - statusCode: number; - statusMessage: string; - setHeader(name: string, value: string | number): void; - getHeader(name: string): string | undefined; - getHeaderNames(): string[]; - getRawHeaderNames(): string[]; - getHeaders(): import("http").OutgoingHttpHeaders; - hasHeader(name: string): boolean; - removeHeader(name: string): void; - writeHead( - statusCode: number, - headers?: import("http").OutgoingHttpHeaders | string[], - ): Http2ServerResponse; - writeHead( - statusCode: number, - statusMessage: string, - headers?: import("http").OutgoingHttpHeaders | string[], - ): Http2ServerResponse; - writeContinue(): void; - writeEarlyHints(hints: Record): void; - write(data: string | Uint8Array): void; - flushHeaders(): void; - cork(): void; - uncork(): void; - addTrailers( - headers: import("http").OutgoingHttpHeaders | ReadonlyArray<[string, string]>, - ): void; - end(data?: string | Uint8Array, callback?: () => void): void; - end(callback: () => void): void; - destroy(): void; - on(event: "close", listener: () => void): void; - /* addListener IS on (Node aliases them) — and the compat res IS the - * http.ServerResponse surface (assignability into ServerResponse - * unions — IncomingMessage.pipe's destination — needs the alias). */ - addListener(event: "close", listener: () => void): void; - once(event: "close", listener: () => void): void; - } - export interface Http2SecureServer { - listen(port: number, callback?: () => void): void; - close(callback?: () => void): void; - /* The bound AddressInfo (Node answers null before listen — this - * surface answers the record with port 0 there, the serverPort - * stance). */ - address(): import("node:dgram").AddressInfo; - emit(event: "connection", socket: Socket): void; - on(event: "connection", listener: (socket: Socket) => void): void; - on( - event: "request", - listener: (req: Http2ServerRequest, res: Http2ServerResponse) => void, - ): void; - on(event: "error", listener: (err: Error) => void): void; - on(event: "close", listener: () => void): void; - on(event: "sessionError", listener: (err: Error, session: ServerHttp2Session) => void): void; - /* The ALPN=h2 server's surface (createSecureServer WITHOUT - * allowHTTP1 — the real h2-over-TLS stack): per-stream and - * per-session listeners, exactly the h2c server's. */ - on( - event: "stream", - listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void, - ): void; - on(event: "session", listener: (session: ServerHttp2Session) => void): void; - /* HTTP CONNECT (and, in Node, the h2 extended CONNECT — which the - * allowHTTP1 lowering never sees): the second argument is the raw - * socket on HTTP/1.1 connections; narrow with instanceof. */ - on( - event: "connect", - listener: ( - req: Http2ServerRequest | import("http").IncomingMessage, - resOrSocket: Http2ServerResponse | Socket, - ) => void, - ): void; - once(event: "connection", listener: (socket: Socket) => void): void; - once( - event: "request", - listener: (req: Http2ServerRequest, res: Http2ServerResponse) => void, - ): void; - once(event: "error", listener: (err: Error) => void): void; - once(event: "close", listener: () => void): void; - once(event: "sessionError", listener: (err: Error, session: ServerHttp2Session) => void): void; - once( - event: "stream", - listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void, - ): void; - once(event: "session", listener: (session: ServerHttp2Session) => void): void; - once( - event: "connect", - listener: ( - req: Http2ServerRequest | import("http").IncomingMessage, - resOrSocket: Http2ServerResponse | Socket, - ) => void, - ): void; - addListener(event: "connection", listener: (socket: Socket) => void): void; - addListener( - event: "request", - listener: (req: Http2ServerRequest, res: Http2ServerResponse) => void, - ): void; - addListener(event: "error", listener: (err: Error) => void): void; - addListener(event: "close", listener: () => void): void; - addListener( - event: "sessionError", - listener: (err: Error, session: ServerHttp2Session) => void, - ): void; - addListener( - event: "stream", - listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void, - ): void; - addListener(event: "session", listener: (session: ServerHttp2Session) => void): void; - addListener( - event: "connect", - listener: ( - req: Http2ServerRequest | import("http").IncomingMessage, - resOrSocket: Http2ServerResponse | Socket, - ) => void, - ): void; - } - export interface SecureServerOptions { - allowHTTP1?: boolean; - cert?: string | Uint8Array; - key?: string | Uint8Array; - SNICallback?: ( - servername: string, - cb: (err: Error | null, ctx?: import("tls").SecureContext) => void, - ) => void; - /* The options-record stance (see http.RequestOptions): h2 session- - * tuning knobs are accepted as literals and ignored (no h2 session - * ever exists under the allowHTTP1 lowering); other documented keys - * fence by name; undocumented keys drop like Node. */ - [option: string]: unknown; - } - /* The full Node v24 constants table (240 members), literal-typed so - * computed header keys ([HTTP2_HEADER_PATH]: value) stay literal keys. - * Every ACCESS bakes as its literal at lowering; the object itself - * never materializes (bare http2.constants value uses fence). */ - export interface Http2Constants { - readonly NGHTTP2_ERR_FRAME_SIZE_ERROR: -522; - readonly NGHTTP2_SESSION_SERVER: 0; - readonly NGHTTP2_SESSION_CLIENT: 1; - readonly NGHTTP2_STREAM_STATE_IDLE: 1; - readonly NGHTTP2_STREAM_STATE_OPEN: 2; - readonly NGHTTP2_STREAM_STATE_RESERVED_LOCAL: 3; - readonly NGHTTP2_STREAM_STATE_RESERVED_REMOTE: 4; - readonly NGHTTP2_STREAM_STATE_HALF_CLOSED_LOCAL: 5; - readonly NGHTTP2_STREAM_STATE_HALF_CLOSED_REMOTE: 6; - readonly NGHTTP2_STREAM_STATE_CLOSED: 7; - readonly NGHTTP2_FLAG_NONE: 0; - readonly NGHTTP2_FLAG_END_STREAM: 1; - readonly NGHTTP2_FLAG_END_HEADERS: 4; - readonly NGHTTP2_FLAG_ACK: 1; - readonly NGHTTP2_FLAG_PADDED: 8; - readonly NGHTTP2_FLAG_PRIORITY: 32; - readonly DEFAULT_SETTINGS_HEADER_TABLE_SIZE: 4096; - readonly DEFAULT_SETTINGS_ENABLE_PUSH: 1; - readonly DEFAULT_SETTINGS_MAX_CONCURRENT_STREAMS: 4294967295; - readonly DEFAULT_SETTINGS_INITIAL_WINDOW_SIZE: 65535; - readonly DEFAULT_SETTINGS_MAX_FRAME_SIZE: 16384; - readonly DEFAULT_SETTINGS_MAX_HEADER_LIST_SIZE: 65535; - readonly DEFAULT_SETTINGS_ENABLE_CONNECT_PROTOCOL: 0; - readonly MAX_MAX_FRAME_SIZE: 16777215; - readonly MIN_MAX_FRAME_SIZE: 16384; - readonly MAX_INITIAL_WINDOW_SIZE: 2147483647; - readonly NGHTTP2_SETTINGS_HEADER_TABLE_SIZE: 1; - readonly NGHTTP2_SETTINGS_ENABLE_PUSH: 2; - readonly NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS: 3; - readonly NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE: 4; - readonly NGHTTP2_SETTINGS_MAX_FRAME_SIZE: 5; - readonly NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE: 6; - readonly NGHTTP2_SETTINGS_ENABLE_CONNECT_PROTOCOL: 8; - readonly PADDING_STRATEGY_NONE: 0; - readonly PADDING_STRATEGY_ALIGNED: 1; - readonly PADDING_STRATEGY_MAX: 2; - readonly PADDING_STRATEGY_CALLBACK: 1; - readonly NGHTTP2_NO_ERROR: 0; - readonly NGHTTP2_PROTOCOL_ERROR: 1; - readonly NGHTTP2_INTERNAL_ERROR: 2; - readonly NGHTTP2_FLOW_CONTROL_ERROR: 3; - readonly NGHTTP2_SETTINGS_TIMEOUT: 4; - readonly NGHTTP2_STREAM_CLOSED: 5; - readonly NGHTTP2_FRAME_SIZE_ERROR: 6; - readonly NGHTTP2_REFUSED_STREAM: 7; - readonly NGHTTP2_CANCEL: 8; - readonly NGHTTP2_COMPRESSION_ERROR: 9; - readonly NGHTTP2_CONNECT_ERROR: 10; - readonly NGHTTP2_ENHANCE_YOUR_CALM: 11; - readonly NGHTTP2_INADEQUATE_SECURITY: 12; - readonly NGHTTP2_HTTP_1_1_REQUIRED: 13; - readonly NGHTTP2_DEFAULT_WEIGHT: 16; - readonly HTTP2_HEADER_STATUS: ":status"; - readonly HTTP2_HEADER_METHOD: ":method"; - readonly HTTP2_HEADER_AUTHORITY: ":authority"; - readonly HTTP2_HEADER_SCHEME: ":scheme"; - readonly HTTP2_HEADER_PATH: ":path"; - readonly HTTP2_HEADER_PROTOCOL: ":protocol"; - readonly HTTP2_HEADER_ACCEPT_ENCODING: "accept-encoding"; - readonly HTTP2_HEADER_ACCEPT_LANGUAGE: "accept-language"; - readonly HTTP2_HEADER_ACCEPT_RANGES: "accept-ranges"; - readonly HTTP2_HEADER_ACCEPT: "accept"; - readonly HTTP2_HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS: "access-control-allow-credentials"; - readonly HTTP2_HEADER_ACCESS_CONTROL_ALLOW_HEADERS: "access-control-allow-headers"; - readonly HTTP2_HEADER_ACCESS_CONTROL_ALLOW_METHODS: "access-control-allow-methods"; - readonly HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN: "access-control-allow-origin"; - readonly HTTP2_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS: "access-control-expose-headers"; - readonly HTTP2_HEADER_ACCESS_CONTROL_REQUEST_HEADERS: "access-control-request-headers"; - readonly HTTP2_HEADER_ACCESS_CONTROL_REQUEST_METHOD: "access-control-request-method"; - readonly HTTP2_HEADER_AGE: "age"; - readonly HTTP2_HEADER_AUTHORIZATION: "authorization"; - readonly HTTP2_HEADER_CACHE_CONTROL: "cache-control"; - readonly HTTP2_HEADER_CONNECTION: "connection"; - readonly HTTP2_HEADER_CONTENT_DISPOSITION: "content-disposition"; - readonly HTTP2_HEADER_CONTENT_ENCODING: "content-encoding"; - readonly HTTP2_HEADER_CONTENT_LENGTH: "content-length"; - readonly HTTP2_HEADER_CONTENT_TYPE: "content-type"; - readonly HTTP2_HEADER_COOKIE: "cookie"; - readonly HTTP2_HEADER_DATE: "date"; - readonly HTTP2_HEADER_ETAG: "etag"; - readonly HTTP2_HEADER_FORWARDED: "forwarded"; - readonly HTTP2_HEADER_HOST: "host"; - readonly HTTP2_HEADER_IF_MODIFIED_SINCE: "if-modified-since"; - readonly HTTP2_HEADER_IF_NONE_MATCH: "if-none-match"; - readonly HTTP2_HEADER_IF_RANGE: "if-range"; - readonly HTTP2_HEADER_LAST_MODIFIED: "last-modified"; - readonly HTTP2_HEADER_LINK: "link"; - readonly HTTP2_HEADER_LOCATION: "location"; - readonly HTTP2_HEADER_RANGE: "range"; - readonly HTTP2_HEADER_REFERER: "referer"; - readonly HTTP2_HEADER_SERVER: "server"; - readonly HTTP2_HEADER_SET_COOKIE: "set-cookie"; - readonly HTTP2_HEADER_STRICT_TRANSPORT_SECURITY: "strict-transport-security"; - readonly HTTP2_HEADER_TRANSFER_ENCODING: "transfer-encoding"; - readonly HTTP2_HEADER_TE: "te"; - readonly HTTP2_HEADER_UPGRADE_INSECURE_REQUESTS: "upgrade-insecure-requests"; - readonly HTTP2_HEADER_UPGRADE: "upgrade"; - readonly HTTP2_HEADER_USER_AGENT: "user-agent"; - readonly HTTP2_HEADER_VARY: "vary"; - readonly HTTP2_HEADER_X_CONTENT_TYPE_OPTIONS: "x-content-type-options"; - readonly HTTP2_HEADER_X_FRAME_OPTIONS: "x-frame-options"; - readonly HTTP2_HEADER_KEEP_ALIVE: "keep-alive"; - readonly HTTP2_HEADER_PROXY_CONNECTION: "proxy-connection"; - readonly HTTP2_HEADER_X_XSS_PROTECTION: "x-xss-protection"; - readonly HTTP2_HEADER_ALT_SVC: "alt-svc"; - readonly HTTP2_HEADER_CONTENT_SECURITY_POLICY: "content-security-policy"; - readonly HTTP2_HEADER_EARLY_DATA: "early-data"; - readonly HTTP2_HEADER_EXPECT_CT: "expect-ct"; - readonly HTTP2_HEADER_ORIGIN: "origin"; - readonly HTTP2_HEADER_PURPOSE: "purpose"; - readonly HTTP2_HEADER_TIMING_ALLOW_ORIGIN: "timing-allow-origin"; - readonly HTTP2_HEADER_X_FORWARDED_FOR: "x-forwarded-for"; - readonly HTTP2_HEADER_PRIORITY: "priority"; - readonly HTTP2_HEADER_ACCEPT_CHARSET: "accept-charset"; - readonly HTTP2_HEADER_ACCESS_CONTROL_MAX_AGE: "access-control-max-age"; - readonly HTTP2_HEADER_ALLOW: "allow"; - readonly HTTP2_HEADER_CONTENT_LANGUAGE: "content-language"; - readonly HTTP2_HEADER_CONTENT_LOCATION: "content-location"; - readonly HTTP2_HEADER_CONTENT_MD5: "content-md5"; - readonly HTTP2_HEADER_CONTENT_RANGE: "content-range"; - readonly HTTP2_HEADER_DNT: "dnt"; - readonly HTTP2_HEADER_EXPECT: "expect"; - readonly HTTP2_HEADER_EXPIRES: "expires"; - readonly HTTP2_HEADER_FROM: "from"; - readonly HTTP2_HEADER_IF_MATCH: "if-match"; - readonly HTTP2_HEADER_IF_UNMODIFIED_SINCE: "if-unmodified-since"; - readonly HTTP2_HEADER_MAX_FORWARDS: "max-forwards"; - readonly HTTP2_HEADER_PREFER: "prefer"; - readonly HTTP2_HEADER_PROXY_AUTHENTICATE: "proxy-authenticate"; - readonly HTTP2_HEADER_PROXY_AUTHORIZATION: "proxy-authorization"; - readonly HTTP2_HEADER_REFRESH: "refresh"; - readonly HTTP2_HEADER_RETRY_AFTER: "retry-after"; - readonly HTTP2_HEADER_TRAILER: "trailer"; - readonly HTTP2_HEADER_TK: "tk"; - readonly HTTP2_HEADER_VIA: "via"; - readonly HTTP2_HEADER_WARNING: "warning"; - readonly HTTP2_HEADER_WWW_AUTHENTICATE: "www-authenticate"; - readonly HTTP2_HEADER_HTTP2_SETTINGS: "http2-settings"; - readonly HTTP2_METHOD_ACL: "ACL"; - readonly HTTP2_METHOD_BASELINE_CONTROL: "BASELINE-CONTROL"; - readonly HTTP2_METHOD_BIND: "BIND"; - readonly HTTP2_METHOD_CHECKIN: "CHECKIN"; - readonly HTTP2_METHOD_CHECKOUT: "CHECKOUT"; - readonly HTTP2_METHOD_CONNECT: "CONNECT"; - readonly HTTP2_METHOD_COPY: "COPY"; - readonly HTTP2_METHOD_DELETE: "DELETE"; - readonly HTTP2_METHOD_GET: "GET"; - readonly HTTP2_METHOD_HEAD: "HEAD"; - readonly HTTP2_METHOD_LABEL: "LABEL"; - readonly HTTP2_METHOD_LINK: "LINK"; - readonly HTTP2_METHOD_LOCK: "LOCK"; - readonly HTTP2_METHOD_MERGE: "MERGE"; - readonly HTTP2_METHOD_MKACTIVITY: "MKACTIVITY"; - readonly HTTP2_METHOD_MKCALENDAR: "MKCALENDAR"; - readonly HTTP2_METHOD_MKCOL: "MKCOL"; - readonly HTTP2_METHOD_MKREDIRECTREF: "MKREDIRECTREF"; - readonly HTTP2_METHOD_MKWORKSPACE: "MKWORKSPACE"; - readonly HTTP2_METHOD_MOVE: "MOVE"; - readonly HTTP2_METHOD_OPTIONS: "OPTIONS"; - readonly HTTP2_METHOD_ORDERPATCH: "ORDERPATCH"; - readonly HTTP2_METHOD_PATCH: "PATCH"; - readonly HTTP2_METHOD_POST: "POST"; - readonly HTTP2_METHOD_PRI: "PRI"; - readonly HTTP2_METHOD_PROPFIND: "PROPFIND"; - readonly HTTP2_METHOD_PROPPATCH: "PROPPATCH"; - readonly HTTP2_METHOD_PUT: "PUT"; - readonly HTTP2_METHOD_REBIND: "REBIND"; - readonly HTTP2_METHOD_REPORT: "REPORT"; - readonly HTTP2_METHOD_SEARCH: "SEARCH"; - readonly HTTP2_METHOD_TRACE: "TRACE"; - readonly HTTP2_METHOD_UNBIND: "UNBIND"; - readonly HTTP2_METHOD_UNCHECKOUT: "UNCHECKOUT"; - readonly HTTP2_METHOD_UNLINK: "UNLINK"; - readonly HTTP2_METHOD_UNLOCK: "UNLOCK"; - readonly HTTP2_METHOD_UPDATE: "UPDATE"; - readonly HTTP2_METHOD_UPDATEREDIRECTREF: "UPDATEREDIRECTREF"; - readonly HTTP2_METHOD_VERSION_CONTROL: "VERSION-CONTROL"; - readonly HTTP_STATUS_CONTINUE: 100; - readonly HTTP_STATUS_SWITCHING_PROTOCOLS: 101; - readonly HTTP_STATUS_PROCESSING: 102; - readonly HTTP_STATUS_EARLY_HINTS: 103; - readonly HTTP_STATUS_OK: 200; - readonly HTTP_STATUS_CREATED: 201; - readonly HTTP_STATUS_ACCEPTED: 202; - readonly HTTP_STATUS_NON_AUTHORITATIVE_INFORMATION: 203; - readonly HTTP_STATUS_NO_CONTENT: 204; - readonly HTTP_STATUS_RESET_CONTENT: 205; - readonly HTTP_STATUS_PARTIAL_CONTENT: 206; - readonly HTTP_STATUS_MULTI_STATUS: 207; - readonly HTTP_STATUS_ALREADY_REPORTED: 208; - readonly HTTP_STATUS_IM_USED: 226; - readonly HTTP_STATUS_MULTIPLE_CHOICES: 300; - readonly HTTP_STATUS_MOVED_PERMANENTLY: 301; - readonly HTTP_STATUS_FOUND: 302; - readonly HTTP_STATUS_SEE_OTHER: 303; - readonly HTTP_STATUS_NOT_MODIFIED: 304; - readonly HTTP_STATUS_USE_PROXY: 305; - readonly HTTP_STATUS_TEMPORARY_REDIRECT: 307; - readonly HTTP_STATUS_PERMANENT_REDIRECT: 308; - readonly HTTP_STATUS_BAD_REQUEST: 400; - readonly HTTP_STATUS_UNAUTHORIZED: 401; - readonly HTTP_STATUS_PAYMENT_REQUIRED: 402; - readonly HTTP_STATUS_FORBIDDEN: 403; - readonly HTTP_STATUS_NOT_FOUND: 404; - readonly HTTP_STATUS_METHOD_NOT_ALLOWED: 405; - readonly HTTP_STATUS_NOT_ACCEPTABLE: 406; - readonly HTTP_STATUS_PROXY_AUTHENTICATION_REQUIRED: 407; - readonly HTTP_STATUS_REQUEST_TIMEOUT: 408; - readonly HTTP_STATUS_CONFLICT: 409; - readonly HTTP_STATUS_GONE: 410; - readonly HTTP_STATUS_LENGTH_REQUIRED: 411; - readonly HTTP_STATUS_PRECONDITION_FAILED: 412; - readonly HTTP_STATUS_PAYLOAD_TOO_LARGE: 413; - readonly HTTP_STATUS_URI_TOO_LONG: 414; - readonly HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE: 415; - readonly HTTP_STATUS_RANGE_NOT_SATISFIABLE: 416; - readonly HTTP_STATUS_EXPECTATION_FAILED: 417; - readonly HTTP_STATUS_TEAPOT: 418; - readonly HTTP_STATUS_MISDIRECTED_REQUEST: 421; - readonly HTTP_STATUS_UNPROCESSABLE_ENTITY: 422; - readonly HTTP_STATUS_LOCKED: 423; - readonly HTTP_STATUS_FAILED_DEPENDENCY: 424; - readonly HTTP_STATUS_TOO_EARLY: 425; - readonly HTTP_STATUS_UPGRADE_REQUIRED: 426; - readonly HTTP_STATUS_PRECONDITION_REQUIRED: 428; - readonly HTTP_STATUS_TOO_MANY_REQUESTS: 429; - readonly HTTP_STATUS_REQUEST_HEADER_FIELDS_TOO_LARGE: 431; - readonly HTTP_STATUS_UNAVAILABLE_FOR_LEGAL_REASONS: 451; - readonly HTTP_STATUS_INTERNAL_SERVER_ERROR: 500; - readonly HTTP_STATUS_NOT_IMPLEMENTED: 501; - readonly HTTP_STATUS_BAD_GATEWAY: 502; - readonly HTTP_STATUS_SERVICE_UNAVAILABLE: 503; - readonly HTTP_STATUS_GATEWAY_TIMEOUT: 504; - readonly HTTP_STATUS_HTTP_VERSION_NOT_SUPPORTED: 505; - readonly HTTP_STATUS_VARIANT_ALSO_NEGOTIATES: 506; - readonly HTTP_STATUS_INSUFFICIENT_STORAGE: 507; - readonly HTTP_STATUS_LOOP_DETECTED: 508; - readonly HTTP_STATUS_BANDWIDTH_LIMIT_EXCEEDED: 509; - readonly HTTP_STATUS_NOT_EXTENDED: 510; - readonly HTTP_STATUS_NETWORK_AUTHENTICATION_REQUIRED: 511; - } - export const constants: Http2Constants; - /* The eager handler is the first COMPAT 'request' listener — Node's - * route on either flavor (the allowHTTP1 server's HTTP/1.1 handles, - * the ALPN=h2 server's compat handles over streams). */ - export function createSecureServer( - options: SecureServerOptions, - onRequestHandler?: ( - req: import("http").IncomingMessage, - res: import("http").ServerResponse, - ) => void, - ): Http2SecureServer; - - /* ── the REAL h2c surface (scr_http2.c: frame codec + HPACK over the - * net loop) — createServer/connect and the session/stream handles the - * Node suite's http2 family exercises. Headers cross as the canonical - * header record (the IncomingHttpHeaders index-signature shape); the - * response :status reads back as a number. Members declared here - * without a lowering fence by name at their use sites. */ - - /** The http2 module's own header shapes: pure index-signature records - * (the HEADER-FAMILY canonicalization — string[] arm included). */ - export interface IncomingHttpHeaders { - [name: string]: number | string | string[] | undefined; - } - export interface OutgoingHttpHeaders { - [name: string]: number | string | string[] | undefined; - } - - export interface Http2Session { - close(callback?: () => void): void; - destroy(): void; - readonly closed: boolean; - readonly destroyed: boolean; - readonly encrypted: boolean; - readonly type: number; - readonly alpnProtocol: string; - readonly socket: import("net").Socket; - readonly pendingSettingsAck: boolean; - ref(): void; - unref(): void; - setTimeout(msecs: number, callback?: () => void): void; - ping(callback: (err: Error | null, duration: number, payload: Uint8Array) => void): boolean; - /* The settings surface rides the checked-dynamic machinery (records - * cross as dyn values — the suite's mustCall listeners are dyn). */ - settings(settings?: any, callback?: any): void; - readonly localSettings: any; - readonly remoteSettings: any; - goaway(code?: number, lastStreamID?: number, opaqueData?: Uint8Array): void; - on(event: "close" | "timeout", listener: () => void): void; - on(event: "error", listener: (err: Error) => void): void; - on( - event: "connect", - listener: (session: Http2Session, socket: import("net").Socket) => void, - ): void; - on( - event: "stream", - listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void, - ): void; - on( - event: "goaway", - listener: (errorCode: number, lastStreamID: number, opaqueData?: Uint8Array) => void, - ): void; - on(event: "localSettings" | "remoteSettings", listener: (settings: any) => void): void; - on(event: "frameError", listener: (type: number, code: number, id: number) => void): void; - on(event: "ping", listener: (payload: Uint8Array) => void): void; - once(event: "close" | "timeout", listener: () => void): void; - once(event: "error", listener: (err: Error) => void): void; - once( - event: "connect", - listener: (session: Http2Session, socket: import("net").Socket) => void, - ): void; - once( - event: "stream", - listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void, - ): void; - once( - event: "goaway", - listener: (errorCode: number, lastStreamID: number, opaqueData?: Uint8Array) => void, - ): void; - once(event: "localSettings" | "remoteSettings", listener: (settings: any) => void): void; - once(event: "frameError", listener: (type: number, code: number, id: number) => void): void; - once(event: "ping", listener: (payload: Uint8Array) => void): void; - addListener(event: "close" | "timeout", listener: () => void): void; - addListener(event: "error", listener: (err: Error) => void): void; - addListener( - event: "connect", - listener: (session: Http2Session, socket: import("net").Socket) => void, - ): void; - addListener( - event: "stream", - listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void, - ): void; - addListener( - event: "goaway", - listener: (errorCode: number, lastStreamID: number, opaqueData?: Uint8Array) => void, - ): void; - addListener(event: "localSettings" | "remoteSettings", listener: (settings: any) => void): void; - addListener( - event: "frameError", - listener: (type: number, code: number, id: number) => void, - ): void; - addListener(event: "ping", listener: (payload: Uint8Array) => void): void; - } - - export interface ClientHttp2Session extends Http2Session { - request( - headers?: OutgoingHttpHeaders, - options?: { - endStream?: boolean; - exclusive?: boolean; - parent?: number; - weight?: number; - waitForTrailers?: boolean; - }, - ): ClientHttp2Stream; - } - - export interface ServerHttp2Session extends Http2Session { - altsvc(alt: string, originOrStream: number | string): void; - origin(...origins: string[]): void; - } - - /** The shared Http2Stream core (both roles). 'data' chunks are Buffers - * (strings once setEncoding('utf8') is in force — the IncomingMessage - * chunk:any stance keeps both working). */ - interface Http2StreamCore { - readonly id: number | undefined; - readonly rstCode: number; - readonly closed: boolean; - readonly destroyed: boolean; - readonly pending: boolean; - readonly aborted: boolean; - readonly session: Http2Session; - readonly sentHeaders: OutgoingHttpHeaders; - readonly state: Record; - write(chunk: string | Uint8Array, callback?: (err?: Error | null) => void): boolean; - end(data?: string | Uint8Array, callback?: () => void): void; - end(callback: () => void): void; - close(code?: number, callback?: () => void): void; - destroy(error?: Error): void; - setEncoding(encoding: string): this; - resume(): void; - pause(): void; - setTimeout(msecs: number, callback?: () => void): void; - priority(options: Record): void; - sendTrailers(headers: OutgoingHttpHeaders): void; - } - - export interface ServerHttp2Stream extends Http2StreamCore { - on(event: "data", listener: (chunk: any) => void): void; - on( - event: - | "end" - | "close" - | "aborted" - | "ready" - | "timeout" - | "drain" - | "finish" - | "wantTrailers", - listener: () => void, - ): void; - on(event: "error", listener: (err: Error) => void): void; - on(event: "frameError", listener: (type: number, code: number, id: number) => void): void; - on(event: "trailers", listener: (trailers: IncomingHttpHeaders, flags: number) => void): void; - once(event: "data", listener: (chunk: any) => void): void; - once( - event: - | "end" - | "close" - | "aborted" - | "ready" - | "timeout" - | "drain" - | "finish" - | "wantTrailers", - listener: () => void, - ): void; - once(event: "error", listener: (err: Error) => void): void; - once(event: "frameError", listener: (type: number, code: number, id: number) => void): void; - once(event: "trailers", listener: (trailers: IncomingHttpHeaders, flags: number) => void): void; - addListener(event: "data", listener: (chunk: any) => void): void; - addListener( - event: - | "end" - | "close" - | "aborted" - | "ready" - | "timeout" - | "drain" - | "finish" - | "wantTrailers", - listener: () => void, - ): void; - addListener(event: "error", listener: (err: Error) => void): void; - addListener( - event: "frameError", - listener: (type: number, code: number, id: number) => void, - ): void; - addListener( - event: "trailers", - listener: (trailers: IncomingHttpHeaders, flags: number) => void, - ): void; - respond( - headers?: OutgoingHttpHeaders, - options?: { endStream?: boolean; waitForTrailers?: boolean }, - ): void; - respondWithFile( - path: string, - headers?: OutgoingHttpHeaders, - options?: Record, - ): void; - respondWithFD( - fd: number, - headers?: OutgoingHttpHeaders, - options?: Record, - ): void; - pushStream( - headers: OutgoingHttpHeaders, - callback: ( - err: Error | null, - pushStream: ServerHttp2Stream, - headers: OutgoingHttpHeaders, - ) => void, - ): void; - readonly headersSent: boolean; - readonly pushAllowed: boolean; - additionalHeaders(headers: OutgoingHttpHeaders): void; - } - - export interface ClientHttp2Stream extends Http2StreamCore { - on(event: "response", listener: (headers: IncomingHttpHeaders, flags: number) => void): void; - on(event: "push", listener: (headers: IncomingHttpHeaders, flags: number) => void): void; - on(event: "headers", listener: (headers: IncomingHttpHeaders, flags: number) => void): void; - on(event: "data", listener: (chunk: any) => void): void; - on( - event: - | "end" - | "close" - | "aborted" - | "ready" - | "timeout" - | "drain" - | "finish" - | "wantTrailers", - listener: () => void, - ): void; - on(event: "error", listener: (err: Error) => void): void; - on(event: "frameError", listener: (type: number, code: number, id: number) => void): void; - on(event: "trailers", listener: (trailers: IncomingHttpHeaders, flags: number) => void): void; - once(event: "response", listener: (headers: IncomingHttpHeaders, flags: number) => void): void; - once(event: "push", listener: (headers: IncomingHttpHeaders, flags: number) => void): void; - once(event: "headers", listener: (headers: IncomingHttpHeaders, flags: number) => void): void; - once(event: "data", listener: (chunk: any) => void): void; - once( - event: - | "end" - | "close" - | "aborted" - | "ready" - | "timeout" - | "drain" - | "finish" - | "wantTrailers", - listener: () => void, - ): void; - once(event: "error", listener: (err: Error) => void): void; - once(event: "frameError", listener: (type: number, code: number, id: number) => void): void; - once(event: "trailers", listener: (trailers: IncomingHttpHeaders, flags: number) => void): void; - addListener( - event: "response", - listener: (headers: IncomingHttpHeaders, flags: number) => void, - ): void; - addListener( - event: "push", - listener: (headers: IncomingHttpHeaders, flags: number) => void, - ): void; - addListener( - event: "headers", - listener: (headers: IncomingHttpHeaders, flags: number) => void, - ): void; - addListener(event: "data", listener: (chunk: any) => void): void; - addListener( - event: - | "end" - | "close" - | "aborted" - | "ready" - | "timeout" - | "drain" - | "finish" - | "wantTrailers", - listener: () => void, - ): void; - addListener(event: "error", listener: (err: Error) => void): void; - addListener( - event: "frameError", - listener: (type: number, code: number, id: number) => void, - ): void; - addListener( - event: "trailers", - listener: (trailers: IncomingHttpHeaders, flags: number) => void, - ): void; - } - - export interface Http2Server { - listen(port?: number, callback?: () => void): void; - listen(port: number, host: string, callback?: () => void): void; - close(callback?: () => void): void; - address(): import("node:dgram").AddressInfo; - setTimeout(msecs?: number, callback?: () => void): void; - ref(): void; - unref(): void; - on( - event: "stream", - listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void, - ): void; - on(event: "session", listener: (session: ServerHttp2Session) => void): void; - on( - event: "request", - listener: (req: Http2ServerRequest, res: Http2ServerResponse) => void, - ): void; - on(event: "error", listener: (err: Error) => void): void; - on(event: "close" | "listening" | "timeout", listener: () => void): void; - on(event: "connection", listener: (socket: import("net").Socket) => void): void; - on(event: "sessionError", listener: (err: Error, session: ServerHttp2Session) => void): void; - once( - event: "stream", - listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void, - ): void; - once(event: "session", listener: (session: ServerHttp2Session) => void): void; - once( - event: "request", - listener: (req: Http2ServerRequest, res: Http2ServerResponse) => void, - ): void; - once(event: "error", listener: (err: Error) => void): void; - once(event: "close" | "listening" | "timeout", listener: () => void): void; - once(event: "connection", listener: (socket: import("net").Socket) => void): void; - once(event: "sessionError", listener: (err: Error, session: ServerHttp2Session) => void): void; - addListener( - event: "stream", - listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void, - ): void; - addListener(event: "session", listener: (session: ServerHttp2Session) => void): void; - addListener( - event: "request", - listener: (req: Http2ServerRequest, res: Http2ServerResponse) => void, - ): void; - addListener(event: "error", listener: (err: Error) => void): void; - addListener(event: "close" | "listening" | "timeout", listener: () => void): void; - addListener(event: "connection", listener: (socket: import("net").Socket) => void): void; - addListener( - event: "sessionError", - listener: (err: Error, session: ServerHttp2Session) => void, - ): void; - } - - export interface ServerOptions { - [option: string]: unknown; - } - - export function createServer( - onRequestHandler?: (req: Http2ServerRequest, res: Http2ServerResponse) => void, - ): Http2Server; - export function createServer( - options: ServerOptions, - onRequestHandler?: (req: Http2ServerRequest, res: Http2ServerResponse) => void, - ): Http2Server; - - export function connect( - authority: string, - listener?: (session: ClientHttp2Session, socket: import("net").Socket) => void, - ): ClientHttp2Session; - export function connect( - authority: string, - options: Record, - listener?: (session: ClientHttp2Session, socket: import("net").Socket) => void, - ): ClientHttp2Session; - - export function getDefaultSettings(): any; - export function getPackedSettings(settings: Record): Uint8Array; - export const sensitiveHeaders: unique symbol; -} -declare module "node:http2" { - export * from "http2"; -} - -/* node:dgram — UDP sockets over the event loop (scr_dgram.c, linked only - * into using binaries). createSocket takes "udp4" or the - * { type: "udp4", reuseAddr? } literal; bind/connect bind NOW and defer - * 'listening'/'connect' to the next loop turn; send takes one string or - * Buffer datagram with an explicit port + address (the connected-send - * and callback forms have no lowering); address() returns the real - * {address, family, port} record and THROWS "Not running" before bind, - * like Node. The socket options throw Node's errors (EBADF before bind); - * add/dropMembership before bind is refused at run time (SC2020). 'error' - * with no listener exits 1 (the unhandled EventEmitter 'error' behavior). */ -declare module "dgram" { - export interface AddressInfo { - address: string; - family: string; - port: number; - } - export interface RemoteInfo { - address: string; - family: string; - port: number; - size: number; - } - export interface Socket { - bind(port: number, address?: string, callback?: () => void): void; - connect(port: number, address?: string, callback?: () => void): void; - send(msg: string | Uint8Array, port: number, address: string): void; - address(): AddressInfo; - close(callback?: () => void): void; - disconnect(): void; - unref(): void; - ref(): void; - setBroadcast(flag: boolean): void; - setTTL(ttl: number): number; - setMulticastTTL(ttl: number): number; - setMulticastLoopback(flag: boolean): boolean; - setMulticastInterface(multicastInterface: string): void; - addMembership(multicastAddress: string, multicastInterface?: string): void; - dropMembership(multicastAddress: string, multicastInterface?: string): void; - getRecvBufferSize(): number; - getSendBufferSize(): number; - setRecvBufferSize(size: number): void; - setSendBufferSize(size: number): void; - getSendQueueSize(): number; - getSendQueueCount(): number; - on(event: "message", listener: (msg: Buffer, rinfo: RemoteInfo) => void): void; - on(event: "listening" | "close" | "connect", listener: () => void): void; - on(event: "error", listener: (err: Error) => void): void; - once(event: "message", listener: (msg: Buffer, rinfo: RemoteInfo) => void): void; - once(event: "listening" | "close" | "connect", listener: () => void): void; - once(event: "error", listener: (err: Error) => void): void; - } - export function createSocket(type: "udp4"): Socket; - export function createSocket(options: { type: "udp4"; reuseAddr?: boolean }): Socket; -} -declare module "node:dgram" { - export * from "dgram"; -} - -/* node:dns — the lookup slice (scr_dgram.c hosts it; getaddrinfo runs AT - * CALL TIME and the callback defers to the next loop turn — SEMANTICS.md - * documents the blocking divergence). Only the { family: 4 } options - * form lowers; failures deliver Node's error shape ("getaddrinfo - * ENOTFOUND ") as the callback's Error argument, with "" for - * the address where Node passes undefined. */ -declare module "dns" { - export interface LookupAddress { - address: string; - family: number; - } - export interface LookupOptions { - family?: number | "IPv4" | "IPv6"; - hints?: number; - all?: boolean; - order?: "ipv4first" | "ipv6first" | "verbatim"; - verbatim?: boolean; - } - export interface LookupOneOptions extends LookupOptions { - all?: false; - } - export interface LookupAllOptions extends LookupOptions { - all: true; - } - /* The callback comes with the other lookup deliveries (literal - * addresses on the next tick); the options follow lib/dns.js. */ - export function lookup( - hostname: string, - callback: (err: NodeJS.ErrnoException | null, address: string, family: number) => void, - ): void; - export function lookup( - hostname: string, - family: number, - callback: (err: NodeJS.ErrnoException | null, address: string, family: number) => void, - ): void; - export function lookup( - hostname: string, - options: LookupOneOptions, - callback: (err: NodeJS.ErrnoException | null, address: string, family: number) => void, - ): void; - export function lookup( - hostname: string, - options: LookupAllOptions, - callback: (err: NodeJS.ErrnoException | null, addresses: LookupAddress[]) => void, - ): void; - /* getnameinfo with NI_NAMEREQD: the host's name and the port's service - * name (the number when it has none). */ - export function lookupService( - address: string, - port: number, - callback: (err: NodeJS.ErrnoException | null, hostname: string, service: string) => void, - ): void; - /* The nameservers /etc/resolv.conf lists. */ - export function getServers(): string[]; - export function getDefaultResultOrder(): "ipv4first" | "ipv6first" | "verbatim"; - export function setDefaultResultOrder(order: "ipv4first" | "ipv6first" | "verbatim"): void; - /* The promise forms (node:dns/promises). */ - export const promises: typeof import("dns/promises"); -} -declare module "node:dns" { - export * from "dns"; -} -/* node:dns/promises — lookup and lookupService as promises over the same - * resolution as the callback forms: argument errors throw synchronously, - * failures reject with the callbacks' errors. */ -declare module "dns/promises" { - import type { LookupAddress, LookupAllOptions, LookupOneOptions, LookupOptions } from "dns"; - export function lookup(hostname: string, family: number): Promise; - export function lookup(hostname: string, options: LookupOneOptions): Promise; - export function lookup(hostname: string, options: LookupAllOptions): Promise; - export function lookup( - hostname: string, - options: LookupOptions, - ): Promise; - export function lookup(hostname: string): Promise; - export function lookupService( - address: string, - port: number, - ): Promise<{ hostname: string; service: string }>; - export function getServers(): string[]; - export function getDefaultResultOrder(): "ipv4first" | "ipv6first" | "verbatim"; - export function setDefaultResultOrder(order: "ipv4first" | "ipv6first" | "verbatim"): void; -} -declare module "node:dns/promises" { - export * from "dns/promises"; -} - -/* node:v8 — importable for the packages that require it at top level. - * getHeapStatistics answers allocator-derived figures in Node's shape - * (approximate: the runtime heap's total and used bytes, the machine's - * memory as the limit; V8-only figures are 0). The other members observe - * V8 itself and fence at their use with the by-design reason; they are - * declared so programs that type-check under their own tsc reach that - * fence. */ -declare module "v8" { - type DoesZapCodeSpaceFlag = 0 | 1; - export interface HeapInfo { - total_heap_size: number; - total_heap_size_executable: number; - total_physical_size: number; - total_available_size: number; - used_heap_size: number; - heap_size_limit: number; - malloced_memory: number; - peak_malloced_memory: number; - does_zap_garbage: DoesZapCodeSpaceFlag; - number_of_native_contexts: number; - number_of_detached_contexts: number; - total_global_handles_size: number; - used_global_handles_size: number; - external_memory: number; - } - export function getHeapStatistics(): HeapInfo; - export function cachedDataVersionTag(): number; - export function getHeapSpaceStatistics(): unknown[]; - export function getHeapCodeStatistics(): unknown; - export function getHeapSnapshot(options?: unknown): unknown; - export function writeHeapSnapshot(fileName?: string, options?: unknown): string; - export function setFlagsFromString(flags: string): void; - export function setHeapSnapshotNearHeapLimit(limit: number): void; - export function serialize(value: unknown): Buffer; - export function deserialize(buffer: Uint8Array): unknown; - export function takeCoverage(): void; - export function stopCoverage(): void; - export function isStringOneByteRepresentation(content: string): boolean; -} -declare module "node:v8" { - export * from "v8"; -} - -/* node:worker_threads — native workers use statically compiled entry points. - * Lowering and runtime diagnostics define the supported options and methods. */ -declare module "worker_threads" { - export const isMainThread: boolean; - export const isInternalThread: boolean; - export const threadId: number; - export interface ResourceLimits { - maxYoungGenerationSizeMb?: number; - maxOldGenerationSizeMb?: number; - codeRangeSizeMb?: number; - stackSizeMb?: number; - } - /* {} in the main thread */ - export const resourceLimits: ResourceLimits; - /* the Worker's name option, trimmed ("WorkerThread" by default); "" in the main thread */ - export const threadName: string | null; - export interface WorkerOptions { - workerData?: unknown; - transferList?: readonly unknown[]; - argv?: readonly unknown[]; - name?: string; - resourceLimits?: ResourceLimits; - stdout?: boolean; - stderr?: boolean; - stdin?: boolean; - [option: string]: unknown; - } - export class Worker { - constructor(filename: string | URL, options?: WorkerOptions); - readonly threadId: number; - readonly resourceLimits?: ResourceLimits; - readonly threadName: string | null; - /* with the stdout/stderr options: the worker's writes to those streams */ - readonly stdout: import("stream").Readable; - readonly stderr: import("stream").Readable; - /* with the stdin option: what the worker reads from process.stdin */ - readonly stdin: import("stream").Writable | null; - /* terminate(), resolving with undefined once the worker exited */ - [Symbol.asyncDispose](): Promise; - on(event: "online", listener: () => void): this; - on(event: "message", listener: (value: any) => void): this; - on(event: "error", listener: (error: any) => void): this; - on(event: "exit", listener: (code: number) => void): this; - on(event: string, listener: (...args: any[]) => void): this; - once(event: string, listener: (...args: any[]) => void): this; - off(event: string, listener: (...args: any[]) => void): this; - addListener(event: string, listener: (...args: any[]) => void): this; - removeListener(event: string, listener: (...args: any[]) => void): this; - prependListener(event: string, listener: (...args: any[]) => void): this; - prependOnceListener(event: string, listener: (...args: any[]) => void): this; - postMessage(value: unknown, transferList?: readonly unknown[]): void; - ref(): void; - unref(): void; - terminate(): Promise; - removeAllListeners(event?: string): this; - listenerCount(event: string, listener?: (...args: any[]) => void): number; - } - export class MessagePort { - on(event: "message", listener: (value: any) => void): this; - on(event: "close", listener: () => void): this; - on(event: string, listener: (...args: any[]) => void): this; - once(event: string, listener: (...args: any[]) => void): this; - off(event: string, listener: (...args: any[]) => void): this; - addListener(event: string, listener: (...args: any[]) => void): this; - removeListener(event: string, listener: (...args: any[]) => void): this; - prependListener(event: string, listener: (...args: any[]) => void): this; - prependOnceListener(event: string, listener: (...args: any[]) => void): this; - postMessage(value: unknown, transferList?: readonly unknown[]): void; - start(): void; - close(callback?: () => void): void; - ref(): void; - unref(): void; - hasRef(): boolean; - removeAllListeners(event?: string): this; - listenerCount(event: string, listener?: (...args: any[]) => void): number; - } - /* Two entangled ports; a port moves to another thread through a - * postMessage or workerData transferList. */ - export class MessageChannel { - readonly port1: MessagePort; - readonly port2: MessagePort; - } - export function receiveMessageOnPort(port: MessagePort): { message: any } | undefined; - /* This thread's environment data; each new Worker starts from a clone. */ - export function setEnvironmentData(key: any, value?: any): void; - export function getEnvironmentData(key: any): any; - /* A marked object cannot be listed in a transfer list. */ - export function markAsUntransferable(object: any): void; - /* structuredClone and postMessage throw DataCloneError for a marked - * object, error or class instance */ - export function markAsUncloneable(object: any): void; - export function isMarkedAsUntransferable(object: any): boolean; - /* Delivers each message to every other open channel with the name, in - * any thread, as a 'message' MessageEvent. */ - export class BroadcastChannel extends EventTarget { - constructor(name: string); - readonly name: string; - onmessage: ((event: MessageEvent) => void) | null; - onmessageerror: ((event: MessageEvent) => void) | null; - postMessage(message: unknown): void; - close(): void; - ref(): this; - unref(): this; - } - export const parentPort: MessagePort | null; - export const workerData: any; -} -declare module "node:worker_threads" { - export * from "worker_threads"; -} - -/* node:buffer — the module spelling of the Buffer/atob/btoa globals. - * Buffer re-exports the GLOBAL declaration, so an imported binding - * resolves to the same symbol the lowerings already answer for (and - * third-party .d.ts files that `import { Buffer } from "node:buffer"` — - * file-entry-cache in a typed-JS source graph — typecheck). */ -declare module "buffer" { - export function isAscii(input: ArrayBuffer | ArrayBufferView): boolean; - export function isUtf8(input: ArrayBuffer | ArrayBufferView): boolean; - export function transcode(source: Uint8Array, fromEncoding: string, toEncoding: string): Buffer; - export function atob(data: string): string; - export function btoa(data: string): string; - export const kMaxLength: number; - export const kStringMaxLength: number; - export const Buffer: BufferConstructor; - export type Blob = globalThis.Blob; - export type File = globalThis.File; - export const Blob: typeof globalThis.Blob; - export const File: typeof globalThis.File; -} -declare module "node:buffer" { - export * from "buffer"; -} - -/* node:cluster — the primary-probe slice. A compiled binary never runs - * as a cluster worker (cluster forks the node binary itself — the - * mechanism is na for compiled programs), so isPrimary lowers to `true` - * and isWorker to `false`; everything else is undeclared. */ -declare module "cluster" { - const cluster: { - readonly isPrimary: boolean; - readonly isMaster: boolean; - readonly isWorker: boolean; - }; - export = cluster; -} -declare module "node:cluster" { - import cluster = require("cluster"); - export = cluster; -} - /* node:tty — the fd probe (the same isatty(3) behind process.*.isTTY) * and the supported structural view of a process output stream. */ declare module "tty" { @@ -7201,799 +1663,6 @@ declare module "node:tty" { export * from "tty"; } -/* node:async_hooks — declared surface without lowering: async-hook - * instrumentation is guarded behind env flags in harness code; a reached - * use fences at its site. */ -declare module "async_hooks" { - /* AsyncLocalStorage: fiber-carried context snapshots — run/getStore/ - * exit/enterWith/disable lower onto the runtime's active-slot machinery - * (als.* libCalls); values cross as dyn values. */ - export class AsyncLocalStorage { - constructor(); - run(store: T, callback: (...args: any[]) => R, ...args: any[]): R; - exit(callback: (...args: any[]) => R, ...args: any[]): R; - getStore(): T | undefined; - enterWith(store: T): void; - disable(): void; - } - export interface AsyncHook { - enable(): AsyncHook; - disable(): AsyncHook; - } - export function createHook(callbacks: { - init?: (asyncId: number, type: string, triggerAsyncId: number, resource: unknown) => void; - before?: (asyncId: number) => void; - after?: (asyncId: number) => void; - destroy?: (asyncId: number) => void; - }): AsyncHook; - export function executionAsyncId(): number; - export function triggerAsyncId(): number; -} -declare module "node:async_hooks" { - export * from "async_hooks"; -} -/* node:events — the EventEmitter class (the module object IS the class, - * like Node's `module.exports = EventEmitter`). Listener/emit signatures - * mirror @types/node's untyped `...args: any[]` surface; the compiler's - * lowering unifies each event name's argument tuple program-wide and - * checks annotated listener parameters against it (unannotated non-empty - * parameter lists have no static types and fence at the registration). */ -declare module "events" { - class EventEmitter { - constructor(); - static defaultMaxListeners: number; - /* Listeners registered under this symbol observe 'error' emits first - * (an emit with no 'error' listener still throws). */ - static readonly errorMonitor: unique symbol; - static addAbortListener(signal: AbortSignal, listener: (event: Event) => void): Disposable; - on(eventName: string | symbol, listener: (...args: any[]) => void): this; - addListener(eventName: string | symbol, listener: (...args: any[]) => void): this; - once(eventName: string | symbol, listener: (...args: any[]) => void): this; - prependListener(eventName: string | symbol, listener: (...args: any[]) => void): this; - prependOnceListener(eventName: string | symbol, listener: (...args: any[]) => void): this; - off(eventName: string | symbol, listener: (...args: any[]) => void): this; - removeListener(eventName: string | symbol, listener: (...args: any[]) => void): this; - removeAllListeners(eventName?: string | symbol): this; - emit(eventName: string | symbol, ...args: any[]): boolean; - listenerCount(eventName: string | symbol, listener?: (...args: any[]) => void): number; - /* Declared as zero-parameter closures so the RESULT is statically - * consumable (.length, indexing, identity): the lowering only admits - * these calls for events whose argument tuple is empty — a listener - * pulled from the array is then safely callable too. Events with - * arguments fence (their element type would lie). */ - listeners(eventName: string): Array<() => void>; - rawListeners(eventName: string): Array<() => void>; - eventNames(): string[]; - setMaxListeners(n: number): this; - getMaxListeners(): number; - } - import internal = require("node:events"); - namespace EventEmitter { - // The class under its own name, so `import { EventEmitter }` resolves - // (the @types/node namespace-alias pattern). - export { internal as EventEmitter }; - // The event's arguments, as checked-dynamic values. - export function once( - emitter: EventEmitter | NodeJS.ReadableStream | NodeJS.WritableStream, - eventName: string, - options?: { signal?: AbortSignal }, - ): Promise; - /* The emitter's own listenerCount/getMaxListeners/setMaxListeners/ - * listeners, over EventEmitter instances. */ - export function listenerCount(emitter: EventEmitter, eventName: string | symbol): number; - export function getMaxListeners(emitter: EventEmitter): number; - export function setMaxListeners(n?: number, ...eventTargets: EventEmitter[]): void; - export function getEventListeners(emitter: EventEmitter, name: string): Array<() => void>; - } - export = EventEmitter; -} -declare module "node:events" { - import events = require("events"); - export = events; -} - -/* node:stream — the static stream classes (runtime-provided, the - * EventEmitter-hierarchy precedent). Phase 1 is the OPTIONS-OBJECT - * constructor surface: `new Readable({ read() {} })`, `new Writable({ - * write(chunk, enc, cb) {} })`, Duplex/Transform/PassThrough, push/read, - * flowing vs paused, write/end backpressure + 'drain', pipe/unpipe, and - * destroy — with Node's event orderings. Chunks are BYTES (Buffer), with - * strings converted at push/write like Node, and any value on an - * object-mode side (objectMode, readableObjectMode, writableObjectMode — - * the object-mode constructor overloads come first). The runtime-fired - * events carry per-class FORCED argument tuples ('data' is one chunk, - * 'error' one Error, 'pipe'/'unpipe' one Readable, the rest empty) — the - * overloads below hand tsc the same contract so unannotated listener - * parameters contextually type in .js sources. */ -declare module "stream" { - import { EventEmitter } from "events"; - - interface ReadableOptions { - highWaterMark?: number; - encoding?: string; - objectMode?: boolean; - read?(this: Readable, size: number): void; - destroy?(this: Readable, error: Error | null, callback: (error?: Error | null) => void): void; - construct?(this: Readable, callback: (error?: Error | null) => void): void; - autoDestroy?: boolean; - emitClose?: boolean; - captureRejections?: boolean; - defaultEncoding?: string; - signal?: AbortSignal; - } - interface WritableOptions { - highWaterMark?: number; - decodeStrings?: boolean; - defaultEncoding?: string; - objectMode?: boolean; - write?( - this: Writable, - chunk: Buffer, - encoding: string, - callback: (error?: Error | null) => void, - ): void; - writev?( - this: Writable, - chunks: Array<{ chunk: any; encoding: string }>, - callback: (error?: Error | null) => void, - ): void; - final?(this: Writable, callback: (error?: Error | null) => void): void; - destroy?(this: Writable, error: Error | null, callback: (error?: Error | null) => void): void; - construct?(this: Writable, callback: (error?: Error | null) => void): void; - autoDestroy?: boolean; - emitClose?: boolean; - captureRejections?: boolean; - signal?: AbortSignal; - } - interface DuplexOptions { - highWaterMark?: number; - readableHighWaterMark?: number; - writableHighWaterMark?: number; - encoding?: string; - decodeStrings?: boolean; - objectMode?: boolean; - readableObjectMode?: boolean; - writableObjectMode?: boolean; - allowHalfOpen?: boolean; - read?(this: Duplex, size: number): void; - write?( - this: Duplex, - chunk: Buffer, - encoding: string, - callback: (error?: Error | null) => void, - ): void; - final?(this: Duplex, callback: (error?: Error | null) => void): void; - destroy?(this: Duplex, error: Error | null, callback: (error?: Error | null) => void): void; - construct?(this: Duplex, callback: (error?: Error | null) => void): void; - autoDestroy?: boolean; - emitClose?: boolean; - captureRejections?: boolean; - readable?: boolean; - writable?: boolean; - defaultEncoding?: string; - writev?( - this: Duplex, - chunks: Array<{ chunk: any; encoding: string }>, - callback: (error?: Error | null) => void, - ): void; - signal?: AbortSignal; - } - interface TransformOptions { - highWaterMark?: number; - readableHighWaterMark?: number; - writableHighWaterMark?: number; - encoding?: string; - decodeStrings?: boolean; - objectMode?: boolean; - readableObjectMode?: boolean; - writableObjectMode?: boolean; - allowHalfOpen?: boolean; - transform?( - this: Transform, - chunk: Buffer, - encoding: string, - callback: (error?: Error | null, data?: Buffer | string) => void, - ): void; - flush?(this: Transform, callback: (error?: Error | null, data?: Buffer | string) => void): void; - destroy?(this: Transform, error: Error | null, callback: (error?: Error | null) => void): void; - construct?(this: Transform, callback: (error?: Error | null) => void): void; - read?(this: Transform, size: number): void; - write?( - this: Transform, - chunk: Buffer, - encoding: string, - callback: (error?: Error | null) => void, - ): void; - final?(this: Transform, callback: (error?: Error | null) => void): void; - writev?(this: Transform, chunks: unknown[], callback: (error?: Error | null) => void): void; - autoDestroy?: boolean; - emitClose?: boolean; - captureRejections?: boolean; - readable?: boolean; - writable?: boolean; - defaultEncoding?: string; - signal?: AbortSignal; - } - - /* Object-mode options (objectMode: true, or a Duplex's or Transform's - * readableObjectMode / writableObjectMode): chunks and completion data - * are any values, and an object-mode write's encoding argument reaches - * write/transform as passed (undefined when absent, null when a - * callback took its place). The constructors take these before the - * byte-mode options, so a byte-mode stream's callbacks keep Buffer - * chunks. */ - type ObjectModeEncoding = string | null | undefined; - type ObjectModeFlag = - | { objectMode: true } - | { readableObjectMode: true } - | { writableObjectMode: true }; - interface ObjectWritableOptions extends Omit { - objectMode: true; - write?( - this: Writable, - chunk: any, - encoding: ObjectModeEncoding, - callback: (error?: Error | null) => void, - ): void; - } - type ObjectDuplexOptions = Omit & { - write?( - this: Duplex, - chunk: any, - encoding: ObjectModeEncoding, - callback: (error?: Error | null) => void, - ): void; - } & ObjectModeFlag; - type ObjectTransformOptions = Omit & { - transform?( - this: Transform, - chunk: any, - encoding: ObjectModeEncoding, - callback: (error?: Error | null, data?: any) => void, - ): void; - flush?(this: Transform, callback: (error?: Error | null, data?: any) => void): void; - write?( - this: Transform, - chunk: any, - encoding: ObjectModeEncoding, - callback: (error?: Error | null) => void, - ): void; - } & ObjectModeFlag; - - /* decodeStrings: false — a string write reaches write/transform as the - * string with its encoding (Buffers keep 'buffer'). */ - interface StringWritableOptions extends Omit { - decodeStrings: false; - write?( - this: Writable, - chunk: string | Buffer, - encoding: string, - callback: (error?: Error | null) => void, - ): void; - } - type StringDuplexOptions = Omit & { - decodeStrings: false; - write?( - this: Duplex, - chunk: string | Buffer, - encoding: string, - callback: (error?: Error | null) => void, - ): void; - }; - type StringTransformOptions = Omit & { - decodeStrings: false; - transform?( - this: Transform, - chunk: string | Buffer, - encoding: string, - callback: (error?: Error | null, data?: Buffer | string) => void, - ): void; - }; - - /* The operators' options: concurrency and highWaterMark bound map's - * in-flight callbacks and queued results. */ - interface ArrayOptions { - concurrency?: number; - highWaterMark?: number; - signal?: AbortSignal; - } - - class Readable extends EventEmitter { - constructor(opts?: ReadableOptions); - /* The underscore-method surface (@types/node declares these on the - * classes; assigning them post-construction shadows the prototype - * method and the machinery calls through the assignment — lowered - * onto the option-callback slots). */ - _read(size: number): void; - _destroy(error: Error | null, callback: (error?: Error | null) => void): void; - [Symbol.asyncIterator](): AsyncIterableIterator; - iterator(options?: { destroyOnReturn?: boolean }): AsyncIterableIterator; - /* compose(this, stage), destroyed when options.signal aborts. */ - compose(stage: ComposeStage, options?: { signal: AbortSignal }): Duplex; - push(chunk: Buffer | string | null, encoding?: string): boolean; - /* An object-mode stream's values. */ - push(chunk: any, encoding?: string): boolean; - unshift(chunk: Buffer | string, encoding?: string): void; - unshift(chunk: any, encoding?: string): void; - /* A Buffer, a string once encoded, an object-mode value, or null (as - * @types/node types it). */ - read(size?: number): any; - pause(): this; - resume(): this; - isPaused(): boolean; - setEncoding(encoding: string): this; - pipe(destination: T, options?: { end?: boolean }): T; - unpipe(destination?: NodeJS.WritableStream): this; - destroy(error?: Error): this; - readonly readable: boolean; - readonly readableEnded: boolean; - readonly readableFlowing: boolean | null; - readonly readableLength: number; - readonly readableHighWaterMark: number; - readonly readableObjectMode: boolean; - readonly closed: boolean; - readonly destroyed: boolean; - readonly errored: Error | null; - static from(iterable: unknown, options?: ReadableOptions): Readable; - /* A Readable over a web ReadableStream's reader. */ - static fromWeb( - readableStream: globalThis.ReadableStream, - options?: { - encoding?: string; - highWaterMark?: number; - objectMode?: boolean; - signal?: AbortSignal; - }, - ): Readable; - /* The Readable operators: chunks are strings or Buffers, map and - * flatMap callbacks return strings or Buffers (flatMap: arrays of - * them, or strings), and callbacks that declare the options argument - * are refused. */ - map( - fn: (data: any, options?: Pick) => any, - options?: ArrayOptions, - ): Readable; - filter( - fn: (data: any, options?: Pick) => boolean | Promise, - options?: ArrayOptions, - ): Readable; - forEach( - fn: (data: any, options?: Pick) => void | Promise, - options?: ArrayOptions, - ): Promise; - toArray(options?: Pick): Promise; - some( - fn: (data: any, options?: Pick) => boolean | Promise, - options?: ArrayOptions, - ): Promise; - find( - fn: (data: any, options?: Pick) => boolean | Promise, - options?: ArrayOptions, - ): Promise; - every( - fn: (data: any, options?: Pick) => boolean | Promise, - options?: ArrayOptions, - ): Promise; - flatMap( - fn: (data: any, options?: Pick) => any, - options?: ArrayOptions, - ): Readable; - drop(limit: number, options?: Pick): Readable; - take(limit: number, options?: Pick): Readable; - reduce( - fn: (previous: any, data: any, options?: Pick) => T, - initial?: undefined, - options?: Pick, - ): Promise; - reduce( - fn: (previous: T, data: any, options?: Pick) => T, - initial: T, - options?: Pick, - ): Promise; - on(event: "data", listener: (chunk: any) => void): this; - on(event: "end" | "close" | "readable" | "pause" | "resume", listener: () => void): this; - on(event: "error", listener: (err: Error) => void): this; - on(event: string, listener: (...args: any[]) => void): this; - once(event: "data", listener: (chunk: any) => void): this; - once(event: "end" | "close" | "readable" | "pause" | "resume", listener: () => void): this; - once(event: "error", listener: (err: Error) => void): this; - once(event: string, listener: (...args: any[]) => void): this; - addListener(event: "data", listener: (chunk: any) => void): this; - addListener( - event: "end" | "close" | "readable" | "pause" | "resume", - listener: () => void, - ): this; - addListener(event: "error", listener: (err: Error) => void): this; - addListener(event: string, listener: (...args: any[]) => void): this; - prependListener(event: "data", listener: (chunk: any) => void): this; - prependListener( - event: "end" | "close" | "readable" | "pause" | "resume", - listener: () => void, - ): this; - prependListener(event: "error", listener: (err: Error) => void): this; - prependListener(event: string, listener: (...args: any[]) => void): this; - prependOnceListener(event: "data", listener: (chunk: any) => void): this; - prependOnceListener( - event: "end" | "close" | "readable" | "pause" | "resume", - listener: () => void, - ): this; - prependOnceListener(event: "error", listener: (err: Error) => void): this; - prependOnceListener(event: string, listener: (...args: any[]) => void): this; - off(event: "data", listener: (chunk: any) => void): this; - off(event: "end" | "close" | "readable" | "pause" | "resume", listener: () => void): this; - off(event: "error", listener: (err: Error) => void): this; - off(event: string, listener: (...args: any[]) => void): this; - removeListener(event: "data", listener: (chunk: any) => void): this; - removeListener( - event: "end" | "close" | "readable" | "pause" | "resume", - listener: () => void, - ): this; - removeListener(event: "error", listener: (err: Error) => void): this; - removeListener(event: string, listener: (...args: any[]) => void): this; - } - - class Writable extends EventEmitter { - constructor(opts: ObjectWritableOptions); - constructor(opts: StringWritableOptions); - constructor(opts?: WritableOptions); - /* The underscore-method surface (see Readable). */ - _write(chunk: any, encoding: string, callback: (error?: Error | null) => void): void; - _writev( - chunks: Array<{ chunk: any; encoding: string }>, - callback: (error?: Error | null) => void, - ): void; - _final(callback: (error?: Error | null) => void): void; - _destroy(error: Error | null, callback: (error?: Error | null) => void): void; - write(chunk: Buffer | string, callback?: (error?: Error | null) => void): boolean; - write( - chunk: Buffer | string, - encoding: string, - callback?: (error?: Error | null) => void, - ): boolean; - /* An object-mode stream's values (the encoding argument reaches - * _write as passed). */ - write(chunk: any, callback?: (error?: Error | null) => void): boolean; - write( - chunk: any, - encoding: ObjectModeEncoding, - callback?: (error?: Error | null) => void, - ): boolean; - end(callback?: () => void): this; - end(chunk: Buffer | string, callback?: () => void): this; - end(chunk: Buffer | string, encoding: string, callback?: () => void): this; - end(chunk: any, callback?: () => void): this; - end(chunk: any, encoding: ObjectModeEncoding, callback?: () => void): this; - cork(): void; - uncork(): void; - setDefaultEncoding(encoding: string): this; - destroy(error?: Error): this; - readonly writable: boolean; - readonly writableEnded: boolean; - readonly writableFinished: boolean; - readonly writableLength: number; - readonly writableHighWaterMark: number; - readonly writableObjectMode: boolean; - readonly writableNeedDrain: boolean; - readonly writableCorked: number; - readonly closed: boolean; - readonly destroyed: boolean; - readonly errored: Error | null; - on(event: "drain" | "finish" | "close", listener: () => void): this; - on(event: "error", listener: (err: Error) => void): this; - on(event: "pipe" | "unpipe", listener: (src: Readable) => void): this; - on(event: string, listener: (...args: any[]) => void): this; - once(event: "drain" | "finish" | "close", listener: () => void): this; - once(event: "error", listener: (err: Error) => void): this; - once(event: "pipe" | "unpipe", listener: (src: Readable) => void): this; - once(event: string, listener: (...args: any[]) => void): this; - addListener(event: "drain" | "finish" | "close", listener: () => void): this; - addListener(event: "error", listener: (err: Error) => void): this; - addListener(event: "pipe" | "unpipe", listener: (src: Readable) => void): this; - addListener(event: string, listener: (...args: any[]) => void): this; - prependListener(event: "drain" | "finish" | "close", listener: () => void): this; - prependListener(event: "error", listener: (err: Error) => void): this; - prependListener(event: "pipe" | "unpipe", listener: (src: Readable) => void): this; - prependListener(event: string, listener: (...args: any[]) => void): this; - prependOnceListener(event: "drain" | "finish" | "close", listener: () => void): this; - prependOnceListener(event: "error", listener: (err: Error) => void): this; - prependOnceListener(event: "pipe" | "unpipe", listener: (src: Readable) => void): this; - prependOnceListener(event: string, listener: (...args: any[]) => void): this; - off(event: "drain" | "finish" | "close", listener: () => void): this; - off(event: "error", listener: (err: Error) => void): this; - off(event: "pipe" | "unpipe", listener: (src: Readable) => void): this; - off(event: string, listener: (...args: any[]) => void): this; - removeListener(event: "drain" | "finish" | "close", listener: () => void): this; - removeListener(event: "error", listener: (err: Error) => void): this; - removeListener(event: "pipe" | "unpipe", listener: (src: Readable) => void): this; - removeListener(event: string, listener: (...args: any[]) => void): this; - } - - /* Duplex is Readable + Writable in one object. TS single inheritance - * mirrors @types/node: extends Readable, the writable half declared - * again. The lowering treats both halves as first-class. */ - class Duplex extends Readable { - /* Duplex.from: an async generator function, an iterable, a string or - * a stream (duplexify.js). */ - static from(body: unknown): Duplex; - constructor(opts: ObjectDuplexOptions); - constructor(opts: StringDuplexOptions); - constructor(opts?: DuplexOptions); - /* The underscore-method surface (see Readable). */ - _write(chunk: any, encoding: string, callback: (error?: Error | null) => void): void; - _writev( - chunks: Array<{ chunk: any; encoding: string }>, - callback: (error?: Error | null) => void, - ): void; - _final(callback: (error?: Error | null) => void): void; - write(chunk: Buffer | string, callback?: (error?: Error | null) => void): boolean; - write( - chunk: Buffer | string, - encoding: string, - callback?: (error?: Error | null) => void, - ): boolean; - /* An object-mode stream's values (the encoding argument reaches - * _write as passed). */ - write(chunk: any, callback?: (error?: Error | null) => void): boolean; - write( - chunk: any, - encoding: ObjectModeEncoding, - callback?: (error?: Error | null) => void, - ): boolean; - end(callback?: () => void): this; - end(chunk: Buffer | string, callback?: () => void): this; - end(chunk: Buffer | string, encoding: string, callback?: () => void): this; - end(chunk: any, callback?: () => void): this; - end(chunk: any, encoding: ObjectModeEncoding, callback?: () => void): this; - cork(): void; - uncork(): void; - setDefaultEncoding(encoding: string): this; - readonly writable: boolean; - readonly writableEnded: boolean; - readonly writableFinished: boolean; - readonly writableLength: number; - readonly writableHighWaterMark: number; - readonly writableObjectMode: boolean; - readonly writableNeedDrain: boolean; - readonly writableCorked: number; - readonly allowHalfOpen: boolean; - on(event: "data", listener: (chunk: any) => void): this; - on( - event: "end" | "close" | "readable" | "pause" | "resume" | "drain" | "finish", - listener: () => void, - ): this; - on(event: "error", listener: (err: Error) => void): this; - on(event: "pipe" | "unpipe", listener: (src: Readable) => void): this; - on(event: string, listener: (...args: any[]) => void): this; - once(event: "data", listener: (chunk: any) => void): this; - once( - event: "end" | "close" | "readable" | "pause" | "resume" | "drain" | "finish", - listener: () => void, - ): this; - once(event: "error", listener: (err: Error) => void): this; - once(event: "pipe" | "unpipe", listener: (src: Readable) => void): this; - once(event: string, listener: (...args: any[]) => void): this; - addListener(event: "data", listener: (chunk: any) => void): this; - addListener( - event: "end" | "close" | "readable" | "pause" | "resume" | "drain" | "finish", - listener: () => void, - ): this; - addListener(event: "error", listener: (err: Error) => void): this; - addListener(event: "pipe" | "unpipe", listener: (src: Readable) => void): this; - addListener(event: string, listener: (...args: any[]) => void): this; - prependListener(event: "data", listener: (chunk: any) => void): this; - prependListener( - event: "end" | "close" | "readable" | "pause" | "resume" | "drain" | "finish", - listener: () => void, - ): this; - prependListener(event: "error", listener: (err: Error) => void): this; - prependListener(event: "pipe" | "unpipe", listener: (src: Readable) => void): this; - prependListener(event: string, listener: (...args: any[]) => void): this; - prependOnceListener(event: "data", listener: (chunk: any) => void): this; - prependOnceListener( - event: "end" | "close" | "readable" | "pause" | "resume" | "drain" | "finish", - listener: () => void, - ): this; - prependOnceListener(event: "error", listener: (err: Error) => void): this; - prependOnceListener(event: "pipe" | "unpipe", listener: (src: Readable) => void): this; - prependOnceListener(event: string, listener: (...args: any[]) => void): this; - off(event: "data", listener: (chunk: any) => void): this; - off( - event: "end" | "close" | "readable" | "pause" | "resume" | "drain" | "finish", - listener: () => void, - ): this; - off(event: "error", listener: (err: Error) => void): this; - off(event: "pipe" | "unpipe", listener: (src: Readable) => void): this; - off(event: string, listener: (...args: any[]) => void): this; - removeListener(event: "data", listener: (chunk: any) => void): this; - removeListener( - event: "end" | "close" | "readable" | "pause" | "resume" | "drain" | "finish", - listener: () => void, - ): this; - removeListener(event: "error", listener: (err: Error) => void): this; - removeListener(event: "pipe" | "unpipe", listener: (src: Readable) => void): this; - removeListener(event: string, listener: (...args: any[]) => void): this; - } - - class Transform extends Duplex { - constructor(opts: ObjectTransformOptions); - constructor(opts: StringTransformOptions); - constructor(opts?: TransformOptions); - /* The underscore-method surface (see Readable). */ - _transform( - chunk: any, - encoding: string, - callback: (error?: Error | null, data?: any) => void, - ): void; - _flush(callback: (error?: Error | null, data?: any) => void): void; - } - class PassThrough extends Transform { - constructor(opts: ObjectTransformOptions); - constructor(opts: StringTransformOptions); - constructor(opts?: TransformOptions); - } - - /* addAbortSignal(signal, stream): the stream, destroyed with an - * AbortError (cause: signal.reason) when the signal aborts. */ - function addAbortSignal(signal: AbortSignal, stream: T): T; - /* pipeline(...stages, callback): the last stream, or the object-mode - * PassThrough that a function destination feeds. */ - type PipelineResult = T extends [ - ...unknown[], - infer D, - (...args: any[]) => unknown, - ] - ? D extends (...args: any[]) => unknown - ? PassThrough - : D - : unknown; - function pipeline(...streams: T): PipelineResult; - /* util.promisify's pipeline overload matches only pipeline itself: a - * generic signature alone admits every function. */ - namespace pipeline { - const __promisify__: typeof promises.pipeline; - } - /* A compose stage: a stream, an async generator function over the - * previous stage, or (first) an iterable, async iterable or web - * ReadableStream. */ - type ComposeStage = - | Readable - | Writable - | Iterable - | AsyncIterable - | globalThis.ReadableStream - | ((source: any, options?: { signal: AbortSignal }) => unknown); - /* compose(...streams): a Duplex that writes to the first stage and - * reads from the last, with the stages running as a pipeline. */ - function compose(...streams: ComposeStage[]): Duplex; - /* finished's options: readable/writable choose which ends count, - * error: false ignores 'error' events, signal aborts the wait. */ - interface FinishedOptions { - readable?: boolean; - writable?: boolean; - error?: boolean; - signal?: AbortSignal; - } - function finished(stream: unknown, callback: (err?: Error | null) => void): () => void; - function finished( - stream: unknown, - options: FinishedOptions | null, - callback: (err?: Error | null) => void, - ): () => void; - /* Folds to the TARGET platform's default at compile time (win32 16384, - * else 65536 — Node's own state.js split); literal argument only. */ - function getDefaultHighWaterMark(objectMode: boolean): number; - /* The promise pipeline's result: a last stage that is an async - * function resolves the promise with its value (before the options - * argument, when one follows). */ - type PipelinePromise = T extends [ - ...unknown[], - (...args: any[]) => Promise, - ] - ? Promise

- : T extends [ - ...unknown[], - (...args: any[]) => Promise, - { signal?: AbortSignal; end?: boolean }, - ] - ? Promise

- : Promise; - namespace promises { - function pipeline(...streams: T): PipelinePromise; - function finished( - stream: unknown, - options?: FinishedOptions & { cleanup?: boolean }, - ): Promise; - } - class Stream extends EventEmitter {} - - export { - Readable, - Writable, - Duplex, - Transform, - PassThrough, - Stream, - addAbortSignal, - pipeline, - compose, - finished, - getDefaultHighWaterMark, - promises, - }; - export type { - ReadableOptions, - WritableOptions, - DuplexOptions, - TransformOptions, - FinishedOptions, - PipelinePromise, - }; -} -declare module "node:stream" { - import stream = require("stream"); - export = stream; -} -declare module "stream/promises" { - /* The promise forms of finished/pipeline: a void promise the stream's - * terminal point settles (rejected with the error, or - * ERR_STREAM_PREMATURE_CLOSE on an early close). Stream arguments are - * the lowered surface — iterables/generators/functions fence per site. - * pipeline's trailing { signal, end } object and finished's options - * (readable, writable, error, cleanup, signal) are supported. A last - * stage that is an async function resolves the promise with its value. */ - function pipeline(...streams: T): import("stream").PipelinePromise; - function finished( - stream: unknown, - options?: { - readable?: boolean; - writable?: boolean; - error?: boolean; - cleanup?: boolean; - signal?: AbortSignal; - } | null, - ): Promise; - export { pipeline, finished }; -} -declare module "node:stream/promises" { - import streamPromises = require("stream/promises"); - export = streamPromises; -} -declare module "stream/consumers" { - /* The promise consumers over a readable: accumulate every chunk and - * settle — text (the utf8 decode), json (the text through JSON.parse: - * `unknown`, validated with a checked cast; malformed input rejects - * with the parse's SyntaxError), buffer (the concatenated bytes; - * string chunks contribute their utf8 bytes, Node's Blob rule). - * Stream errors reject; an early close rejects with - * ERR_STREAM_PREMATURE_CLOSE. arrayBuffer answers an ArrayBuffer over - * the bytes; blob fences per site. */ - function text(stream: unknown): Promise; - function json(stream: unknown): Promise; - function buffer(stream: unknown): Promise; - function arrayBuffer(stream: unknown): Promise; - function blob(stream: unknown): Promise; - export { arrayBuffer, blob, buffer, json, text }; -} -declare module "node:stream/consumers" { - import streamConsumers = require("stream/consumers"); - export = streamConsumers; -} -/* node:stream/web — the module exports the web stream globals themselves - * (require("stream/web").ReadableStream === globalThis.ReadableStream). - * ReadableStream is the implemented class; its reader and controller are - * exported as types. */ -declare module "stream/web" { - export const ReadableStream: typeof globalThis.ReadableStream; - export type ReadableStream = globalThis.ReadableStream; - export type ReadableStreamDefaultReader = globalThis.ReadableStreamDefaultReader; - export type ReadableStreamDefaultController = - globalThis.ReadableStreamDefaultController; - export type UnderlyingSource = globalThis.UnderlyingSource; - export type QueuingStrategy = globalThis.QueuingStrategy; -} -declare module "node:stream/web" { - export * from "stream/web"; -} interface ErrorConstructor { stackTraceLimit: number; } diff --git a/packages/compiler/ambient/scriptc-overrides.d.ts b/packages/compiler/ambient/scriptc-overrides.d.ts index 2dafbcdf0..376eba9d5 100644 --- a/packages/compiler/ambient/scriptc-overrides.d.ts +++ b/packages/compiler/ambient/scriptc-overrides.d.ts @@ -70,3 +70,24 @@ interface PromiseConstructor { reject: (reason: Error) => void; }; } + +/* Carriers of the types the lowerer gives untyped JavaScript collections + * whose uses prove them (js-collection-types.ts). Nothing reads them at run + * time. */ +declare namespace __scriptcInferredCollections { + const map_string_string: Map; + const map_string_number: Map; + const map_string_boolean: Map; + const map_number_string: Map; + const map_number_number: Map; + const map_number_boolean: Map; + const map_boolean_string: Map; + const map_boolean_number: Map; + const map_boolean_boolean: Map; + const set_string: Set; + const set_number: Set; + const set_boolean: Set; + const array_string: string[]; + const array_number: number[]; + const array_boolean: boolean[]; +} diff --git a/packages/compiler/native/cli.layout.gz b/packages/compiler/native/cli.layout.gz new file mode 100644 index 000000000..4ede69568 Binary files /dev/null and b/packages/compiler/native/cli.layout.gz differ diff --git a/packages/compiler/native/cli.profdata.gz b/packages/compiler/native/cli.profdata.gz index b5f1e079f..aa9398b5f 100644 Binary files a/packages/compiler/native/cli.profdata.gz and b/packages/compiler/native/cli.profdata.gz differ diff --git a/packages/compiler/native/host.c b/packages/compiler/native/host.c index f54bdaa3f..bffdf933f 100644 --- a/packages/compiler/native/host.c +++ b/packages/compiler/native/host.c @@ -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 #include -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. */ @@ -135,3 +158,16 @@ void scr_cyc_set_growth_cap(long cap); void scriptc_native_bound_cycle_growth(void) { scr_cyc_set_growth_cap(2); } + +/* The runtime's nursery trigger (scr_rc.h). */ +void scr_cyc_set_nursery_threshold(long threshold); + +/* Runs nursery passes every 1024 new candidates instead of every 256. Much + * of a build's cyclic garbage outlives 256 releases: in the shorter window + * it survives a nursery pass, is promoted, and only full passes over the + * whole live heap find it. A constructor sets it before the compiler's + * modules initialize, so every pass of a build uses it. + * SCR_CYCLE_THRESHOLD still overrides it. */ +__attribute__((constructor)) static void scriptc_native_widen_cycle_nursery(void) { + scr_cyc_set_nursery_threshold(1024); +} diff --git a/packages/compiler/native/ts7-json.test.c b/packages/compiler/native/ts7-json.test.c new file mode 100644 index 000000000..b696d77c2 --- /dev/null +++ b/packages/compiler/native/ts7-json.test.c @@ -0,0 +1,33 @@ +/* Reads cases from standard input, each a little-endian u32 length, a u32 + * start and the bytes, and prints scriptc_ts7_json_value_end for each. */ +#include "ts7-process.h" +#include +#include +#ifdef _WIN32 +#include +#include +#endif + +static int read_u32(uint32_t *value) { + unsigned char bytes[4]; + if (fread(bytes, 1, 4, stdin) != 4) return 0; + *value = (uint32_t)bytes[0] | (uint32_t)bytes[1] << 8 | (uint32_t)bytes[2] << 16 | + (uint32_t)bytes[3] << 24; + return 1; +} + +int main(void) { +#ifdef _WIN32 + /* The cases are bytes: text mode would rewrite line endings. */ + _setmode(_fileno(stdin), _O_BINARY); +#endif + uint32_t length, start; + while (read_u32(&length)) { + if (!read_u32(&start)) return 1; + uint8_t *bytes = malloc(length == 0 ? 1 : length); + if (bytes == NULL || fread(bytes, 1, length, stdin) != length) return 1; + printf("%d\n", (int)scriptc_ts7_json_value_end(bytes, length, start)); + free(bytes); + } + return 0; +} diff --git a/packages/compiler/native/ts7-process-child.test.c b/packages/compiler/native/ts7-process-child.test.c index 7202cab51..4d1cbc081 100644 --- a/packages/compiler/native/ts7-process-child.test.c +++ b/packages/compiler/native/ts7-process-child.test.c @@ -12,7 +12,49 @@ #include #include #else +#include +#include +#include #include + +/* `--listen PATH --idle-exit SECONDS`: a stand-in for a shared server. In + * mode "listen-exit" it exits as an executable without --listen does. In + * mode "listen" it answers each client, in order, with its client number, + * its listen arguments and TSC_TS_LISTEN_MEMORY, then echoes until the + * client closes, and exits after SECONDS (at most 5) without a client. */ +static int listen_main(const char *mode, char **argv) { + if (strcmp(mode, "listen-exit") == 0) return 2; + if (strcmp(mode, "listen") != 0) return 73; + struct sockaddr_un address; + memset(&address, 0, sizeof(address)); + address.sun_family = AF_UNIX; + if (strlen(argv[0]) >= sizeof(address.sun_path)) return 77; + strcpy(address.sun_path, argv[0]); + int listener = socket(AF_UNIX, SOCK_STREAM, 0); + if (listener < 0 || bind(listener, (struct sockaddr *)&address, sizeof(address)) != 0 || + listen(listener, 4) != 0) + return 78; + const char *memory = getenv("TSC_TS_LISTEN_MEMORY"); + int idle = atoi(argv[2]); + if (idle > 5) idle = 5; + for (int client = 1;; client++) { + struct pollfd entry = {listener, POLLIN, 0}; + if (poll(&entry, 1, idle * 1000) <= 0) break; + int fd = accept(listener, NULL, NULL); + if (fd < 0) return 79; + char line[512]; + int size = snprintf(line, sizeof(line), "client %d\n%s %s\n%s\n", client, argv[0], argv[2], + memory == NULL ? "unset" : memory); + if (size <= 0 || write(fd, line, (size_t)size) != size) return 80; + uint8_t bytes[17]; + ssize_t count; + while ((count = read(fd, bytes, sizeof(bytes))) > 0) + if (write(fd, bytes, (size_t)count) != count) break; + close(fd); + } + unlink(argv[0]); + return 0; +} #endif static int child_main(int argc, char **argv) { @@ -20,15 +62,25 @@ static int child_main(int argc, char **argv) { _setmode(_fileno(stdin), _O_BINARY); _setmode(_fileno(stdout), _O_BINARY); #endif - if (argc < 5 || argc > 6 || strcmp(argv[1], "--api") != 0 || strcmp(argv[2], "--cwd") != 0 || - strncmp(argv[4], "--callbacks=", 12) != 0 || (argc == 6 && strcmp(argv[5], "--timing") != 0)) return 71; + int listening = argc >= 9 && strcmp(argv[5], "--listen") == 0 && strcmp(argv[7], "--idle-exit") == 0; + int fixed = listening ? 9 : 5; + int at = fixed, timing = 0, libraries = 0; + if (at < argc && strcmp(argv[at], "--timing") == 0) { timing = 1; at++; } + if (at < argc && strcmp(argv[at], "--default-libraries-from-disk") == 0) { libraries = 1; at++; } + if (argc < fixed || at != argc || strcmp(argv[1], "--api") != 0 || strcmp(argv[2], "--cwd") != 0 || + strncmp(argv[4], "--callbacks=", 12) != 0) + return 71; const char *mode = argv[3]; +#ifndef _WIN32 + if (listening) return listen_main(mode, argv + 6); +#endif if (strncmp(mode, "argv:", 5) == 0) { fwrite(mode + 5, 1, strlen(mode + 5), stdout); fputc('\n', stdout); fputs(argv[4], stdout); fputc('\n', stdout); - fputs(argc == 6 ? "timing\n" : "plain\n", stdout); + fputs(timing ? "timing\n" : "plain\n", stdout); + if (libraries) fputs("libraries from disk\n", stdout); return ferror(stdout) ? 72 : 0; } if (strcmp(mode, "exit") == 0) return 0; diff --git a/packages/compiler/native/ts7-process.c b/packages/compiler/native/ts7-process.c index ea451345d..8261c341b 100644 --- a/packages/compiler/native/ts7-process.c +++ b/packages/compiler/native/ts7-process.c @@ -1,5 +1,8 @@ #ifndef _WIN32 #define _POSIX_C_SOURCE 200809L +/* flock(2), for the checker server's lock file, and syscall(2). */ +#define _DEFAULT_SOURCE +#define _DARWIN_C_SOURCE #endif #include "ts7-process.h" #include @@ -16,7 +19,13 @@ #include #include #include +#include #include +#include +#ifdef __linux__ +#include +#endif +#include #include #include #include @@ -38,6 +47,9 @@ typedef struct Ts7Process { HANDLE output; #else int socket; + /* The checker server's lock file, held while connected to the server + * (scriptc_ts7_attach); -1 for a server this process started. */ + int lock; #endif } Ts7Process; @@ -149,7 +161,7 @@ static wchar_t *command_line(wchar_t **arguments, size_t count) { } static int start_process(Ts7Process *p, char **args, size_t argc) { - wchar_t *wide[6] = {0}; + wchar_t *wide[7] = {0}; wchar_t *line = NULL; HANDLE child_input = NULL, child_output = NULL, child_error = NULL; LPPROC_THREAD_ATTRIBUTE_LIST attributes = NULL; @@ -283,7 +295,7 @@ static int start_process(Ts7Process *p, char **args, size_t argc) { if (error == 0) error = posix_spawn(&pid, args[0], &actions, NULL, args, environment); posix_spawn_file_actions_destroy(&actions); if (error == 0) { - p->socket = sockets[0]; p->pid = (uint32_t)pid; + p->socket = sockets[0]; p->pid = (uint32_t)pid; p->lock = -1; } done: close(sockets[1]); @@ -293,15 +305,21 @@ done: static void stop_process(Ts7Process *p) { close(p->socket); + // A shared checker server keeps running; closing the connection ends this + // build's session, and closing the lock file lets another build connect. + if (p->lock >= 0) { close(p->lock); return; } int status; pid_t pid = (pid_t)p->pid; - for (int attempt = 0; attempt < 5; attempt++) { + // A server exits within a fraction of a millisecond of reading EOF, so the + // checks start 0.1 ms apart and double: 51.1 ms in all before the kill. + for (int attempt = 0; attempt < 10; attempt++) { pid_t result; do { result = waitpid(pid, &status, WNOHANG); } while (result < 0 && errno == EINTR); // ECHILD also means the PID no longer belongs to us. Never signal a // potentially recycled PID after somebody else has reaped the child. if (result == pid || (result < 0 && errno == ECHILD)) return; - struct timespec delay = {0, 10000000}; + if (attempt == 9) break; + struct timespec delay = {0, 100000L << attempt}; while (nanosleep(&delay, &delay) != 0 && errno == EINTR) {} } (void)kill(pid, SIGKILL); @@ -312,7 +330,7 @@ static void stop_process(Ts7Process *p) { uint32_t scriptc_ts7_open(const uint8_t *executable, size_t executable_size, const uint8_t *cwd, size_t cwd_size, const uint8_t *callbacks, size_t callbacks_size, - uint8_t timing) { + uint8_t timing, uint8_t libraries_from_disk) { if (next_id == 0) { error_text("server handle IDs are exhausted"); return 0; } char *exe = copy_argument(executable, executable_size, ""); if (exe == NULL) return 0; @@ -328,8 +346,12 @@ uint32_t scriptc_ts7_open(const uint8_t *executable, size_t executable_size, } exit_registered = 1; } - char *args[] = {exe, "--api", "--cwd", directory, callback_flag, timing ? "--timing" : NULL, NULL}; - int started = start_process(p, args, timing ? 6 : 5); + char *args[8] = {exe, "--api", "--cwd", directory, callback_flag}; + size_t argc = 5; + if (timing) args[argc++] = "--timing"; + if (libraries_from_disk) args[argc++] = "--default-libraries-from-disk"; + args[argc] = NULL; + int started = start_process(p, args, argc); free(exe); free(directory); free(callback_flag); if (!started) { free(p); return 0; } p->id = next_id++; @@ -338,6 +360,184 @@ uint32_t scriptc_ts7_open(const uint8_t *executable, size_t executable_size, return p->id; } +#ifdef _WIN32 +uint32_t scriptc_ts7_attach(const uint8_t *name, size_t name_size, + const uint8_t *executable, size_t executable_size, + const uint8_t *cwd, size_t cwd_size, + const uint8_t *callbacks, size_t callbacks_size, + uint8_t timing, uint8_t libraries_from_disk, uint32_t idle_exit, + uint32_t memory) { + (void)name; (void)name_size; (void)executable; (void)executable_size; (void)cwd; (void)cwd_size; + (void)callbacks; (void)callbacks_size; (void)timing; (void)libraries_from_disk; (void)idle_exit; (void)memory; + error_text("a shared checker server needs Unix sockets"); + return 0; +} +#else +/* This user's checker server directory: $XDG_RUNTIME_DIR/scriptc, or + * /tmp/scriptc-. It must be a directory this user owns with no access + * for others; one that exists otherwise is not used. */ +static int server_directory(char *path, size_t size) { + const char *runtime = getenv("XDG_RUNTIME_DIR"); + int count = runtime != NULL && runtime[0] == '/' + ? snprintf(path, size, "%s/scriptc", runtime) + : snprintf(path, size, "/tmp/scriptc-%lu", (unsigned long)geteuid()); + if (count < 0 || (size_t)count >= size) { error_text("checker server directory path is too long"); return 0; } + if (mkdir(path, 0700) != 0 && errno != EEXIST) { system_error(errno); return 0; } + struct stat info; + if (lstat(path, &info) != 0) { system_error(errno); return 0; } + if (!S_ISDIR(info.st_mode) || info.st_uid != geteuid() || (info.st_mode & 077) != 0) { + error_text("checker server directory is not private to this user"); + return 0; + } + return 1; +} + +/* A connection to the server listening at `path`, or -1. */ +static int connect_server(const char *path) { + struct sockaddr_un address; + memset(&address, 0, sizeof(address)); + address.sun_family = AF_UNIX; + memcpy(address.sun_path, path, strlen(path) + 1); + int fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (fd < 0) return -1; + if (fcntl(fd, F_SETFD, FD_CLOEXEC) < 0) { close(fd); return -1; } +#ifdef SO_NOSIGPIPE + int no_sigpipe = 1; + if (setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &no_sigpipe, sizeof(no_sigpipe)) != 0) { close(fd); return -1; } +#endif + int status; + do { status = connect(fd, (const struct sockaddr *)&address, sizeof(address)); } while (status != 0 && errno == EINTR); + if (status != 0) { close(fd); return -1; } + return fd; +} + +/* Starts the server in its own process group, so that a terminal's signals + * to the build do not reach it, with its standard streams on /dev/null and + * no other descriptor of this process, so that it holds no pipe a caller of + * the build waits on. `limit` (TSC_TS_LISTEN_MEMORY=...) is added to the + * environment unless it sets TSC_TS_LISTEN_MEMORY. */ +static pid_t start_server(char **args, char *limit) { +#ifdef __APPLE__ + char **inherited = *_NSGetEnviron(); +#else + char **inherited = environ; +#endif + size_t count = 0; + int has_limit = 0; + for (; inherited[count] != NULL; count++) + if (strncmp(inherited[count], "TSC_TS_LISTEN_MEMORY=", 21) == 0) has_limit = 1; + char **environment = malloc((count + 2) * sizeof(char *)); + if (environment == NULL) { error_text("out of memory"); return -1; } + memcpy(environment, inherited, count * sizeof(char *)); + environment[count] = has_limit ? NULL : limit; + environment[count + 1] = NULL; + long descriptors = sysconf(_SC_OPEN_MAX); + if (descriptors < 0 || descriptors > 65536) descriptors = 65536; + sigset_t signals; + sigemptyset(&signals); + pid_t pid = fork(); + if (pid == 0) { + // Only async-signal-safe calls until exec. + int null = open("/dev/null", O_RDWR); + if (setpgid(0, 0) != 0 || null < 0 || dup2(null, STDIN_FILENO) < 0 || dup2(null, STDOUT_FILENO) < 0 || + dup2(null, STDERR_FILENO) < 0) + _exit(127); +#if defined(__linux__) && defined(SYS_close_range) + if (syscall(SYS_close_range, 3U, ~0U, 0U) != 0) +#endif + for (long fd = 3; fd < descriptors; fd++) close((int)fd); + sigprocmask(SIG_SETMASK, &signals, NULL); + execve(args[0], args, environment); + _exit(127); + } + int error = errno; + free(environment); + if (pid < 0) { system_error(error); return -1; } + return pid; +} + +uint32_t scriptc_ts7_attach(const uint8_t *name, size_t name_size, + const uint8_t *executable, size_t executable_size, + const uint8_t *cwd, size_t cwd_size, + const uint8_t *callbacks, size_t callbacks_size, + uint8_t timing, uint8_t libraries_from_disk, uint32_t idle_exit, + uint32_t memory) { + if (next_id == 0) { error_text("server handle IDs are exhausted"); return 0; } + char *server = copy_argument(name, name_size, ""); + if (server == NULL) return 0; + char directory[256], socket_path[sizeof(((struct sockaddr_un *)0)->sun_path)], lock_path[512]; + int named = strchr(server, '/') == NULL; + if (!named) error_text("checker server names cannot contain '/'"); + if (named) named = server_directory(directory, sizeof(directory)); + if (named) { + int count = snprintf(socket_path, sizeof(socket_path), "%s/%s.sock", directory, server); + named = count > 0 && (size_t)count < sizeof(socket_path); + if (!named) error_text("checker server socket path is too long"); + else (void)snprintf(lock_path, sizeof(lock_path), "%s.lock", socket_path); + } + free(server); + if (!named) return 0; + // One build at a time uses the server; a build that finds it in use + // starts its own server instead of waiting. + int lock = open(lock_path, O_RDWR | O_CREAT | O_CLOEXEC | O_NOFOLLOW, 0600); + if (lock < 0) { system_error(errno); return 0; } + if (flock(lock, LOCK_EX | LOCK_NB) != 0) { + if (errno == EWOULDBLOCK) error_text("the checker server is in use"); + else system_error(errno); + close(lock); + return 0; + } + int fd = connect_server(socket_path); + if (fd < 0) { + char *exe = copy_argument(executable, executable_size, ""); + char *dir = exe == NULL ? NULL : copy_argument(cwd, cwd_size, ""); + char *callback_flag = dir == NULL ? NULL : copy_argument(callbacks, callbacks_size, "--callbacks="); + char idle[16], limit[48]; + (void)snprintf(idle, sizeof(idle), "%lu", (unsigned long)idle_exit); + (void)snprintf(limit, sizeof(limit), "TSC_TS_LISTEN_MEMORY=%lu", (unsigned long)memory); + pid_t pid = -1; + if (callback_flag != NULL) { + char *args[12] = {exe, "--api", "--cwd", dir, callback_flag, "--listen", socket_path, "--idle-exit", idle}; + size_t argc = 9; + if (timing) args[argc++] = "--timing"; + if (libraries_from_disk) args[argc++] = "--default-libraries-from-disk"; + args[argc] = NULL; + pid = start_server(args, limit); + } + free(exe); free(dir); free(callback_flag); + // The server listens as it starts. One that exits first, such as a + // tsc-ts without --listen, or one that is not listening within 2 s, + // leaves this build to start its own server. + for (int attempt = 0; pid > 0; attempt++) { + fd = connect_server(socket_path); + if (fd >= 0) break; + int status; + pid_t result; + do { result = waitpid(pid, &status, WNOHANG); } while (result < 0 && errno == EINTR); + if (result != 0) { error_text("the checker server exited as it started"); break; } + if (attempt == 1000) { error_text("the checker server did not start listening"); break; } + struct timespec delay = {0, 2000000}; + while (nanosleep(&delay, &delay) != 0 && errno == EINTR) {} + } + } + if (fd < 0) { close(lock); return 0; } + Ts7Process *p = calloc(1, sizeof(*p)); + if (p == NULL) { error_text("out of memory"); close(fd); close(lock); return 0; } + if (!exit_registered) { + if (atexit(scriptc_ts7_close_all) != 0) { + error_text("could not register server cleanup"); free(p); close(fd); close(lock); return 0; + } + exit_registered = 1; + } + p->socket = fd; + p->lock = lock; + p->id = next_id++; + p->next = processes; + processes = p; + return p->id; +} +#endif + uint32_t scriptc_ts7_pid(uint32_t handle) { Ts7Process *p = find_process(handle); return p != NULL ? p->pid : 0; @@ -405,3 +605,47 @@ void scriptc_ts7_close(uint32_t handle) { void scriptc_ts7_close_all(void) { while (processes != NULL) scriptc_ts7_close(processes->id); } + +static int json_space(uint8_t byte) { + return byte == ' ' || byte == '\n' || byte == '\r' || byte == '\t'; +} + +int32_t scriptc_ts7_json_value_end(const uint8_t *bytes, size_t size, uint32_t start) { + if (size > INT32_MAX) return -1; + size_t depth = 0; + size_t index = start; + while (index < size) { + uint8_t byte = bytes[index]; + if (byte == '"') { + index++; + // The quote found stays the candidate end until an escape skips it, so + // every byte of the string is scanned once by each search. + const uint8_t *quote = index < size ? memchr(bytes + index, '"', size - index) : NULL; + for (;;) { + if (quote == NULL) return -1; + const uint8_t *escape = memchr(bytes + index, '\\', (size_t)(quote - (bytes + index))); + if (escape == NULL) { + index = (size_t)(quote - bytes); + break; + } + index = (size_t)(escape - bytes) + 2; + if (bytes + index > quote) quote = index < size ? memchr(bytes + index, '"', size - index) : NULL; + } + index++; + if (depth == 0) return (int32_t)index; + continue; + } + if (byte == '{' || byte == '[') { + depth++; + } else if (byte == '}' || byte == ']') { + if (depth == 0) return (int32_t)index; + depth--; + if (depth == 0) return (int32_t)(index + 1); + } else if (depth == 0 && (byte == ',' || json_space(byte))) { + return (int32_t)index; + } + index++; + } + if (depth != 0) return -1; + return (int32_t)index; +} diff --git a/packages/compiler/native/ts7-process.ffi.json b/packages/compiler/native/ts7-process.ffi.json index 86f3cb4d2..963c81f15 100644 --- a/packages/compiler/native/ts7-process.ffi.json +++ b/packages/compiler/native/ts7-process.ffi.json @@ -1,12 +1,14 @@ { "ffi_format": 6, "functions": [ - { "name": "ts7NativeOpen", "symbol": "scriptc_ts7_open", "params": ["string", "string", "string", "bool"], "returns": "u32" }, + { "name": "ts7NativeOpen", "symbol": "scriptc_ts7_open", "params": ["string", "string", "string", "bool", "bool"], "returns": "u32" }, + { "name": "ts7NativeAttach", "symbol": "scriptc_ts7_attach", "params": ["string", "string", "string", "string", "bool", "bool", "u32", "u32"], "returns": "u32" }, { "name": "ts7NativeRead", "symbol": "scriptc_ts7_read", "params": ["u32", "mutable-bytes", "u32", "u32"], "returns": "i32" }, { "name": "ts7NativeWrite", "symbol": "scriptc_ts7_write", "params": ["u32", "bytes", "u32", "u32"], "returns": "i32" }, { "name": "ts7NativePid", "symbol": "scriptc_ts7_pid", "params": ["u32"], "returns": "u32" }, { "name": "ts7NativeClose", "symbol": "scriptc_ts7_close", "params": ["u32"], "returns": "void" }, { "name": "ts7NativeCloseAll", "symbol": "scriptc_ts7_close_all", "params": [], "returns": "void" }, - { "name": "ts7NativeError", "symbol": "scriptc_ts7_error", "params": ["mutable-bytes"], "returns": "i32" } + { "name": "ts7NativeError", "symbol": "scriptc_ts7_error", "params": ["mutable-bytes"], "returns": "i32" }, + { "name": "ts7NativeJsonValueEnd", "symbol": "scriptc_ts7_json_value_end", "params": ["bytes", "u32"], "returns": "i32" } ] } diff --git a/packages/compiler/native/ts7-process.h b/packages/compiler/native/ts7-process.h index 3d2a0ba05..d51f939a9 100644 --- a/packages/compiler/native/ts7-process.h +++ b/packages/compiler/native/ts7-process.h @@ -12,7 +12,21 @@ uint32_t scriptc_ts7_open(const uint8_t *executable, size_t executable_size, const uint8_t *cwd, size_t cwd_size, const uint8_t *callbacks, size_t callbacks_size, - uint8_t timing); + uint8_t timing, uint8_t libraries_from_disk); +/* Connects to the shared checker server `name` (`--api --listen`) in this + * user's private server directory, starting it in its own process group + * with `--idle-exit idle_exit` and a limit of `memory` megabytes + * (TSC_TS_LISTEN_MEMORY, unless the environment sets it) when none answers. Returns 0 with the error + * text when another build is connected to it, it cannot be started or + * reached, or the platform has no Unix sockets; the caller then starts its + * own server with scriptc_ts7_open. Closing the handle ends the connection + * and leaves the server running. */ +uint32_t scriptc_ts7_attach(const uint8_t *name, size_t name_size, + const uint8_t *executable, size_t executable_size, + const uint8_t *cwd, size_t cwd_size, + const uint8_t *callbacks, size_t callbacks_size, + uint8_t timing, uint8_t libraries_from_disk, uint32_t idle_exit, + uint32_t memory); int32_t scriptc_ts7_read(uint32_t handle, uint8_t *bytes, size_t size, uint32_t offset, uint32_t length); int32_t scriptc_ts7_write(uint32_t handle, const uint8_t *bytes, size_t size, @@ -21,5 +35,9 @@ uint32_t scriptc_ts7_pid(uint32_t handle); void scriptc_ts7_close(uint32_t handle); void scriptc_ts7_close_all(void); int32_t scriptc_ts7_error(uint8_t *bytes, size_t size); +/* The index after the JSON value that starts at `start` in a checker reply, + * as jsonValueEnd in semantic-json.ts computes it, or -1 when the reply is + * malformed there. */ +int32_t scriptc_ts7_json_value_end(const uint8_t *bytes, size_t size, uint32_t start); #endif diff --git a/packages/compiler/native/ts7-process.test.c b/packages/compiler/native/ts7-process.test.c index e17c367c7..85de63db1 100644 --- a/packages/compiler/native/ts7-process.test.c +++ b/packages/compiler/native/ts7-process.test.c @@ -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 #include @@ -11,7 +15,9 @@ #include #else #include +#include #include +#include #include #include #endif @@ -19,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)); @@ -81,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)); @@ -145,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); @@ -171,6 +181,89 @@ static void failure_cases(void) { scriptc_ts7_close(UINT32_MAX); } +#ifndef _WIN32 +static uint32_t attach(const char *name, const char *mode, uint32_t memory) { + return scriptc_ts7_attach((const uint8_t *)name, strlen(name), (const uint8_t *)executable, strlen(executable), + (const uint8_t *)mode, strlen(mode), (const uint8_t *)callbacks, strlen(callbacks), + 0, 0, 1, memory); +} + +static void expect_line(uint32_t id, const char *expected) { + char line[512]; + size_t at = 0; + do { + assert(at + 1 < sizeof(line)); + read_exact(id, (uint8_t *)line + at, 1); + } while (line[at++] != '\n'); + line[at] = '\0'; + assert(strcmp(line, expected) == 0); +} + +static void attach_cases(void) { + char runtime[] = "/tmp/scriptc-ts7-attach-XXXXXX"; + assert(mkdtemp(runtime) != NULL); + assert(setenv("XDG_RUNTIME_DIR", runtime, 1) == 0); + assert(unsetenv("TSC_TS_LISTEN_MEMORY") == 0); + char directory[256], socket_path[512], expected[600]; + uint8_t bytes[4]; + snprintf(directory, sizeof(directory), "%s/scriptc", runtime); + snprintf(socket_path, sizeof(socket_path), "%s/server.sock", directory); + // The first build starts the server with its idle time and memory limit. + // The server holds none of the build's descriptors, such as an output + // pipe its caller reads to the end. + int output[2]; + assert(pipe(output) == 0); + uint32_t first = attach("server", "listen", 123); + assert(first != 0 && scriptc_ts7_pid(first) == 0); + close(output[1]); + struct pollfd ended = {output[0], POLLIN, 0}; + assert(poll(&ended, 1, 5000) == 1); + assert(read(output[0], bytes, 1) == 0); + close(output[0]); + expect_line(first, "client 1\n"); + snprintf(expected, sizeof(expected), "%s 1\n", socket_path); + expect_line(first, expected); + expect_line(first, "123\n"); + write_exact(first, (const uint8_t *)"ping", 4, 0, 4); + read_exact(first, bytes, 4); + assert(memcmp(bytes, "ping", 4) == 0); + // A build that finds the server in use gets no handle. + for (int i = 0; i < 3; i++) { assert(attach("server", "listen", 123) == 0); fail_message(); } + scriptc_ts7_close(first); + scriptc_ts7_close(first); + // A later build connects to the same server. + uint32_t second = attach("server", "listen", 123); + assert(second != 0); + expect_line(second, "client 2\n"); + scriptc_ts7_close(second); + // An executable without --listen exits as it starts. + assert(attach("old", "listen-exit", 0) == 0); + fail_message(); + // A server directory others can access is not used. + assert(chmod(directory, 0755) == 0); + assert(attach("server", "listen", 123) == 0); + fail_message(); + assert(chmod(directory, 0700) == 0); + // A name that would leave the directory, or make the path too long. + assert(attach("../server", "listen", 123) == 0); + fail_message(); + char long_name[200]; + memset(long_name, 'x', sizeof(long_name) - 1); + long_name[sizeof(long_name) - 1] = '\0'; + assert(attach(long_name, "listen", 123) == 0); + fail_message(); + // The server exits after its idle second and removes its socket. + unlink(socket_path); + snprintf(expected, sizeof(expected), "%s.lock", socket_path); + unlink(expected); + snprintf(expected, sizeof(expected), "%s/old.sock.lock", directory); + unlink(expected); + rmdir(directory); + rmdir(runtime); + assert(unsetenv("XDG_RUNTIME_DIR") == 0); +} +#endif + static int process_main(int argc, char **argv) { assert(argc >= 2); executable = argv[1]; @@ -196,6 +289,9 @@ static int process_main(int argc, char **argv) { argument_cases(); exchange_cases(); failure_cases(); +#ifndef _WIN32 + attach_cases(); +#endif assert(resource_count() == before); #ifndef _WIN32 if (saved_input >= 0) { assert(dup2(saved_input, STDIN_FILENO) >= 0); close(saved_input); } diff --git a/packages/compiler/package.json b/packages/compiler/package.json index a358ae545..d1c39d8ac 100644 --- a/packages/compiler/package.json +++ b/packages/compiler/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/compiler", - "version": "0.4.2", + "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.8", + "tsc-ts": "0.0.10", "typescript": "7.0.2", "typescript5": "npm:typescript@5.9.3" }, diff --git a/packages/compiler/src/backend/cycle-analysis.ts b/packages/compiler/src/backend/cycle-analysis.ts index fbd96e854..b03979bd9 100644 --- a/packages/compiler/src/backend/cycle-analysis.ts +++ b/packages/compiler/src/backend/cycle-analysis.ts @@ -146,6 +146,10 @@ function analyzeCycles( ...(c.localCaptures !== undefined ? [{ name: "", type: { kind: "classval" as const, className: c.name } }] : []), + // An instance of a class that extends Array holds its elements. + ...(c.arrayElem !== undefined + ? [{ name: "", type: { kind: "array" as const, elem: c.arrayElem } }] + : []), ], })), ...(mod.records ?? []).map((r) => ({ diff --git a/packages/compiler/src/backend/executable-features.ts b/packages/compiler/src/backend/executable-features.ts index fbbf60e76..c33bab336 100644 --- a/packages/compiler/src/backend/executable-features.ts +++ b/packages/compiler/src/backend/executable-features.ts @@ -20,7 +20,9 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures { ...(features.warningReplace ? { warningReplace: true } : {}), ...(features.stdioExtended ? { stdioExtended: true } : {}), ...(features.nodeIsland ? { nodeIsland: true } : {}), + ...(features.moduleNamespace ? { moduleNamespace: true } : {}), zlib: features.zlib, + ...(features.brotli ? { brotli: true } : {}), assert: features.assert, inspect: features.inspect, dynInvoke: features.dynInvoke, @@ -30,10 +32,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 } : {}), // Island code's signal and exit listeners join the events unit's // registries (scope-B NI-3). events: features.processEvents || features.nodeIsland === true, @@ -53,6 +59,7 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures { dgram: features.dgram, dnsPromises: features.dnsPromises, dgramDyn: features.dgramDyn, + textCodecDyn: features.textCodecDyn, watch: features.fsWatch, foreignFfi: hasForeignFfiCallback(mod.ffiImports ?? []), nodeTest: features.nodeTest, @@ -63,6 +70,7 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures { dh: features.dh, pki: features.pki, ed25519: features.ed25519, + x509: features.x509, webcrypto: features.webcrypto, perfLoop: features.perfLoop, }; diff --git a/packages/compiler/src/backend/library-pack.ts b/packages/compiler/src/backend/library-pack.ts index e9f52f7a0..14ffdff28 100644 --- a/packages/compiler/src/backend/library-pack.ts +++ b/packages/compiler/src/backend/library-pack.ts @@ -54,6 +54,7 @@ export async function compilePackedLibrary( dgram: false, dnsPromises: false, dgramDyn: false, + textCodecDyn: false, watch: false, foreignFfi: false, nodeTest: false, @@ -63,6 +64,7 @@ export async function compilePackedLibrary( dh: false, pki: false, ed25519: false, + x509: options.x509 ?? false, webcrypto: false, perfLoop: false, tlsCa: false, @@ -151,7 +153,7 @@ export async function compilePackedLibrary( ...(await Promise.all( shards .slice(start, start + 4) - .map((shard, offset) => emit(shard.source, `shard-${start + offset}.o`)), + .map((shard, offset) => emit(shard.source(), `shard-${start + offset}.o`)), )), ); } @@ -164,6 +166,7 @@ export async function compilePackedLibrary( options.programPublicSymbols, stem, target.platform, + { inProcessElf: true }, ); } else { program = await emit( diff --git a/packages/compiler/src/backend/link-plan-core.test.ts b/packages/compiler/src/backend/link-plan-core.test.ts new file mode 100644 index 000000000..9c33e6fd0 --- /dev/null +++ b/packages/compiler/src/backend/link-plan-core.test.ts @@ -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"]); +}); diff --git a/packages/compiler/src/backend/link-plan-core.ts b/packages/compiler/src/backend/link-plan-core.ts index de5da1657..fc05d2961 100644 --- a/packages/compiler/src/backend/link-plan-core.ts +++ b/packages/compiler/src/backend/link-plan-core.ts @@ -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, + names: readonly string[], +): string[] { + return names.flatMap((name) => + name === "m" && target.platform === "linux" + ? ["-Wl,--as-needed", "-lm", "-Wl,--no-as-needed"] + : [`-l${name}`], + ); +} diff --git a/packages/compiler/src/backend/linker.ts b/packages/compiler/src/backend/linker.ts index af53f74df..c4f04133e 100644 --- a/packages/compiler/src/backend/linker.ts +++ b/packages/compiler/src/backend/linker.ts @@ -21,6 +21,8 @@ import { import { type NativeArtifactDependency } from "./native/contracts.js"; import { RuntimePackError, stageRuntimePackArtifacts } from "./runtime-pack.js"; import type { NativeLinkPlan } from "./link-plan.js"; +import { lldLinkerArgs } from "./lld.js"; +import { systemLibraryLinkerArgs } from "./link-plan-core.js"; import { createDarwinDebugSymbols, installDarwinDebugSymbols, @@ -240,11 +242,14 @@ export async function linkNativeExecutable( try { const staged = await stageRuntimePackArtifacts(plan.runtimePack); stagedRoot = staged.root; + // Linux links with lld when the default driver finds it. + const lld = explicitLinker === undefined ? lldLinkerArgs(plan.target.platform, linker) : []; const args = [ ...(explicitLinker === undefined ? plan.target.defaultLinkerArgs : []), + ...lld, ...plan.driverFlags, ...plan.inputs.map((input) => staged.replacements.get(input) ?? input), - ...plan.systemLibraries.map((name) => `-l${name}`), + ...systemLibraryLinkerArgs(plan.target, plan.systemLibraries), "-o", privateOut, ]; @@ -276,7 +281,15 @@ export async function linkNativeExecutable( ...(await snapshotDependencies([...toolchain, ...additionalDependencyPaths])), ]) .catch(() => null); - await execFileAsync(linker, args); + await execFileAsync(linker, args).catch(async (error: unknown) => { + // A failed link with lld runs again with the driver's default linker, + // whose error (if any) is the one reported. + if (lld.length === 0) throw error; + await execFileAsync( + linker, + args.filter((arg) => !lld.includes(arg)), + ); + }); const output = await stat(privateOut); if (!output.isFile() || output.size === 0) throw new Error("linker produced no executable"); if (plan.darwinDebugSymbols) { diff --git a/packages/compiler/src/backend/lld.test.ts b/packages/compiler/src/backend/lld.test.ts new file mode 100644 index 000000000..b0a466cf6 --- /dev/null +++ b/packages/compiler/src/backend/lld.test.ts @@ -0,0 +1,61 @@ +import { chmodSync, mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, expect, test } from "vitest"; +import { driverFindsLld, lldLinkerArgs } from "./lld.js"; + +const directories: string[] = []; +afterEach(() => { + for (const path of directories.splice(0)) rmSync(path, { recursive: true, force: true }); +}); + +function tool(path: string): string { + writeFileSync(path, "#!/bin/sh\n"); + chmodSync(path, 0o755); + return path; +} + +function fixture() { + const root = mkdtempSync(join(tmpdir(), "scriptc-lld-")); + directories.push(root); + const llvm = join(root, "llvm", "bin"); + const usr = join(root, "usr", "bin"); + const other = join(root, "other"); + for (const directory of [llvm, usr, other]) mkdirSync(directory, { recursive: true }); + return { root, llvm, usr, other }; +} + +test.skipIf(process.platform === "win32")( + "a driver finds ld.lld beside itself, beside its real executable, or on PATH", + () => { + const f = fixture(); + const clang = tool(join(f.llvm, "clang")); + // Without ld.lld anywhere. + expect(driverFindsLld(clang, { PATH: f.other })).toBe(false); + // Beside the driver. + tool(join(f.llvm, "ld.lld")); + expect(driverFindsLld(clang, { PATH: f.other })).toBe(true); + // A PATH entry that links to the driver: clang looks beside its real executable. + symlinkSync(clang, join(f.usr, "clang")); + expect(driverFindsLld("clang", { PATH: f.usr })).toBe(true); + // A driver elsewhere finds ld.lld on PATH, and not otherwise. + const gcc = tool(join(f.other, "gcc")); + expect(driverFindsLld(gcc, { PATH: f.other })).toBe(false); + expect(driverFindsLld(gcc, { PATH: [f.other, f.llvm].join(":") })).toBe(true); + expect(driverFindsLld("missing-driver", { PATH: f.llvm })).toBe(false); + }, +); + +test.skipIf(process.platform === "win32")( + "Linux links with the default driver use lld when the driver finds it", + () => { + const f = fixture(); + const clang = tool(join(f.llvm, "clang")); + tool(join(f.llvm, "ld.lld")); + const env = { PATH: f.other }; + expect(lldLinkerArgs("linux", clang, env)).toEqual(["-fuse-ld=lld"]); + expect(lldLinkerArgs("darwin", clang, env)).toEqual([]); + expect(lldLinkerArgs("windows", clang, env)).toEqual([]); + expect(lldLinkerArgs("linux", clang, { ...env, SCRIPTC_LINKER: clang })).toEqual([]); + }, +); diff --git a/packages/compiler/src/backend/lld.ts b/packages/compiler/src/backend/lld.ts new file mode 100644 index 000000000..790e07062 --- /dev/null +++ b/packages/compiler/src/backend/lld.ts @@ -0,0 +1,49 @@ +import { realpathSync, statSync } from "node:fs"; +import { delimiter, dirname, isAbsolute, join, resolve } from "node:path"; + +function isFile(path: string): boolean { + try { + return statSync(path).isFile(); + } catch { + return false; + } +} + +/** The executable `command` names: a path, or the first match on PATH. */ +function commandFile(command: string, env: NodeJS.ProcessEnv): string | null { + if (command.includes("/")) return isFile(command) ? resolve(command) : null; + for (const directory of (env["PATH"] ?? "/usr/bin:/bin").split(delimiter)) { + const candidate = join(directory || ".", command); + if (isFile(candidate)) return isAbsolute(candidate) ? candidate : resolve(candidate); + } + return null; +} + +/** + * Whether the compiler driver `driver` finds ld.lld for `-fuse-ld=lld`: in + * the directory of the driver's executable (where clang looks first) or on + * PATH. Linking still falls back to the driver's default linker when a link + * with lld fails. + */ +export function driverFindsLld(driver: string, env: NodeJS.ProcessEnv = process.env): boolean { + const file = commandFile(driver, env); + if (file === null) return false; + try { + for (const directory of new Set([dirname(file), dirname(realpathSync(file))])) + if (isFile(join(directory, "ld.lld"))) return true; + } catch { + return false; + } + return commandFile("ld.lld", env) !== null; +} + +/** `-fuse-ld=lld` for Linux links whose driver finds ld.lld. */ +export function lldLinkerArgs( + platform: string, + driver: string, + env: NodeJS.ProcessEnv = process.env, +): string[] { + return platform === "linux" && env["SCRIPTC_LINKER"] === undefined && driverFindsLld(driver, env) + ? ["-fuse-ld=lld"] + : []; +} diff --git a/packages/compiler/src/backend/llvm/alloc.ts b/packages/compiler/src/backend/llvm/alloc.ts index aa6345dab..2efeb96ac 100644 --- a/packages/compiler/src/backend/llvm/alloc.ts +++ b/packages/compiler/src/backend/llvm/alloc.ts @@ -459,18 +459,72 @@ export type FreeObservers = "weak" | "weak-or-source"; * type's disposals), and the block — `scr_cyc_free` for collector-headered * objects, `scr_rt_free` otherwise. Labels use the `sf.` prefix; the lines * end in an open block. + * + * With `keep` (an alloca of ptr holding NULL; inline allocation paths only), + * a block that would go onto the calling thread's free list is stored there + * instead, for a teardown segment (emitSegmentFree); a block the system or + * another arena owns is still freed. */ export function emitObjectFree( host: AllocHost, cyc: boolean, observers: FreeObservers = "weak", + keep: string | null = null, ): string[] { - const lines = threadLocalState(host, "sf", freeLines(host, cyc, observers)); + const lines = threadLocalState(host, "sf", freeLines(host, cyc, observers, keep)); if (host.objectAudit === false) return lines.filter((line) => line !== FREE_NOTE); host.declare(`declare void @scr_obj_free_note()`); return lines; } +/** Lines that put the blocks `first` to `last` (linked through their first + * words; one block when they are equal) of the class at reservation offset + * `off` on the calling thread's free list. */ +function freeListPushLines(prefix: string, off: string, first: string, last: string): string[] { + return [ + ` %${prefix}.shp = getelementptr inbounds i8, ptr @scr_sa, i64 ${SA_SHIFT}`, + ` %${prefix}.sh32 = load i32, ptr %${prefix}.shp`, + ` %${prefix}.sh = zext i32 %${prefix}.sh32 to i64`, + ` %${prefix}.c = lshr i64 ${off}, %${prefix}.sh`, + ` %${prefix}.c8 = shl i64 %${prefix}.c, 3`, + ` %${prefix}.fo = add i64 %${prefix}.c8, ${SA_FREE}`, + ` %${prefix}.fp = getelementptr inbounds i8, ptr @scr_sa, i64 %${prefix}.fo`, + ` %${prefix}.head = load ptr, ptr %${prefix}.fp`, + ` store ptr %${prefix}.head, ptr ${last}`, + ` store ptr ${first}, ptr %${prefix}.fp`, + ]; +} + +/** + * Lines that free a teardown segment: the blocks `first` to `last` (ptr + * values, both NULL for an empty segment), linked through their first words, + * which emitObjectFree kept. Every block of a segment is in the calling + * thread's arena and of one size class, so the segment goes onto that + * class's free list in one step, and the next allocations of the class take + * its blocks in segment order. Labels use `prefix`; the lines end in the + * open block `${prefix}.done`. + */ +export function emitSegmentFree( + host: AllocHost, + prefix: string, + first: string, + last: string, +): string[] { + declareState(host); + const lines = [ + ` %${prefix}.none = icmp eq ptr ${first}, null`, + ` br i1 %${prefix}.none, label %${prefix}.done, label %${prefix}.push`, + `${prefix}.push:`, + ` %${prefix}.base = load i64, ptr @scr_sa`, + ` %${prefix}.bi = ptrtoint ptr ${first} to i64`, + ` %${prefix}.off = sub i64 %${prefix}.bi, %${prefix}.base`, + ...freeListPushLines(prefix, `%${prefix}.off`, first, last), + ` br label %${prefix}.done`, + `${prefix}.done:`, + ]; + return threadLocalState(host, prefix, lines); +} + /** Lines that leave in `%.obs` whether the program can observe a * disposal of this type (folds to a constant). */ function observedLines(prefix: string, observers: FreeObservers): string[] { @@ -482,8 +536,14 @@ function observedLines(prefix: string, observers: FreeObservers): string[] { ]; } -function freeLines(host: AllocHost, cyc: boolean, observers: FreeObservers): string[] { +function freeLines( + host: AllocHost, + cyc: boolean, + observers: FreeObservers, + keep: string | null, +): string[] { if (host.inlineAlloc !== true) { + if (keep !== null) throw new Error("a kept block needs the inline allocation paths"); if (cyc) { host.declare(`declare void @scr_cyc_free(ptr)`); return [` call void @scr_obj_free_note()`, ` call void @scr_cyc_free(ptr %o)`]; @@ -548,16 +608,9 @@ function freeLines(host: AllocHost, cyc: boolean, observers: FreeObservers): str ` %sf.inx = call i1 @llvm.expect.i1(i1 %sf.in, i1 true)`, ` br i1 %sf.inx, label %sf.push, label %sf.sys`, `sf.push:`, - ` %sf.shp = getelementptr inbounds i8, ptr @scr_sa, i64 ${SA_SHIFT}`, - ` %sf.sh32 = load i32, ptr %sf.shp`, - ` %sf.sh = zext i32 %sf.sh32 to i64`, - ` %sf.c = lshr i64 %sf.off, %sf.sh`, - ` %sf.c8 = shl i64 %sf.c, 3`, - ` %sf.fo = add i64 %sf.c8, ${SA_FREE}`, - ` %sf.fp = getelementptr inbounds i8, ptr @scr_sa, i64 %sf.fo`, - ` %sf.head = load ptr, ptr %sf.fp`, - ` store ptr %sf.head, ptr ${blk}`, - ` store ptr ${blk}, ptr %sf.fp`, + ...(keep !== null + ? [` store ptr ${blk}, ptr ${keep}`] + : freeListPushLines("sf", "%sf.off", blk, blk)), ` br label %sf.done`, `sf.sys: ; outside the reservation, or the allocator is compiled out`, ` call void @scr_obj_free_note()`, diff --git a/packages/compiler/src/backend/llvm/appended-array-literals.ts b/packages/compiler/src/backend/llvm/appended-array-literals.ts index 3c09904e3..c65aded0c 100644 --- a/packages/compiler/src/backend/llvm/appended-array-literals.ts +++ b/packages/compiler/src/backend/llvm/appended-array-literals.ts @@ -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 = 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; + } +} diff --git a/packages/compiler/src/backend/llvm/blocks.ts b/packages/compiler/src/backend/llvm/blocks.ts index 354e631c6..3ae65cb0f 100644 --- a/packages/compiler/src/backend/llvm/blocks.ts +++ b/packages/compiler/src/backend/llvm/blocks.ts @@ -9,6 +9,7 @@ * code after it); here the lines are DROPPED instead, and emitStmts skips * whole statements once the current block is terminated, so no dropped * definition can leak into a later block. */ +import { UNWIND_RESUME, UNWIND_SLOT } from "./unwind-abi.js"; export class BlockBuilder { private readonly blocks: { label: string; lines: string[]; term: string | null }[] = []; @@ -86,13 +87,24 @@ export class BlockBuilder { terminate(s: string): void { if (this.cur.term === null) { - if (this.returnEpilogue && s.startsWith("ret ")) this.line(this.returnEpilogue); + if ( + this.returnEpilogue && + (s.startsWith("ret ") || + s.startsWith("call void @scr_unwind_raise()") || + s === UNWIND_RESUME) + ) + this.line(this.returnEpilogue); + if (s === UNWIND_RESUME) { + const exception = this.tmp(); + this.line(`${exception} = load { ptr, i32 }, ptr ${UNWIND_SLOT}`); + s = `resume { ptr, i32 } ${exception}`; + } this.cur.term = ` ${this.withDebugLocation(s)}`; } } - br(label: string): void { - this.terminate(`br label %${label}`); + br(label: string, attachment = ""): void { + this.terminate(`br label %${label}${attachment}`); } condBr(v: string, t: string, f: string, attachment = ""): void { diff --git a/packages/compiler/src/backend/llvm/bytes-alias.ts b/packages/compiler/src/backend/llvm/bytes-alias.ts index 9aaeaa6c7..2dff9f70b 100644 --- a/packages/compiler/src/backend/llvm/bytes-alias.ts +++ b/packages/compiler/src/backend/llvm/bytes-alias.ts @@ -7,13 +7,31 @@ import type { LlvmEmitterContext } from "./expr-context.js"; * buffer. Neither overlaps the other, a native field, array storage or a * module global's slot, so element stores leave header loads and global * receiver slots loop-invariant. Untagged accesses and calls still alias - * every category. */ + * every category. + * + * Header loads 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 { - return host.memoryAliasAttachment("bytes:elements"); + return host.memoryAliasAttachment("bytes:elements") + host.bytesAccessScope; } /** A module global's slot is its own category for whole-slot loads of a diff --git a/packages/compiler/src/backend/llvm/bytes-bounds.ts b/packages/compiler/src/backend/llvm/bytes-bounds.ts index 86a1d7ac8..29b1c8e15 100644 --- a/packages/compiler/src/backend/llvm/bytes-bounds.ts +++ b/packages/compiler/src/backend/llvm/bytes-bounds.ts @@ -14,11 +14,25 @@ interface Bound extends Extent { minimum: number; } type Node = IrExpr | IrStmt; +const FLIPPED: Readonly> = { + "<": ">", + "<=": ">=", + ">": "<", + ">=": "<=", + "===": "===", + "==": "==", + "!==": "!==", + "!=": "!=", +}; /** Relational proofs for fixed typed-array lengths. Cached lengths retain * their receiver identity only when that binding is never reassigned. * Loops may use offsets in either direction, but both index integrality - * and receiver stability are established separately from the relation. */ + * and receiver stability are established separately from the relation. + * For loops hold one bound over the body. While loops start from the + * condition's bound and follow the body in order: increments raise it and + * comparisons with the length in if conditions narrow it. Indexes are + * `counter + c` or `L + d - counter`, where L is the receiver's length. */ export function findBytesBounds(fn: IrFunction, ranges: IntegerRanges): ReadonlySet { const proved = new Map(); if (fn.async || fn.generator || !fn.locals.some((local) => local.type.kind === "bytes")) @@ -96,6 +110,20 @@ export function findBytesBounds(fn: IrFunction, ranges: IntegerRanges): Readonly const result = left + (e.op === "+" ? e.right.value : -e.right.value); return Number.isSafeInteger(result) ? result : null; } + /** For an index of the form `L + d - counter` (`len - 1 - i`, `right - i` + * with `right = len - 1`), where L is the receiver's length: d. */ + function reversed(e: IrExpr, bound: Bound): number | null { + if (e.kind !== "bin" || (e.op !== "+" && e.op !== "-")) return null; + if (e.op === "-" && e.right.kind === "varRef" && e.right.localId === bound.counter) { + const length = extent(e.left, false); + return length && length.receiver === bound.receiver ? length.offset : null; + } + if (e.right.kind !== "numLit" || !Number.isSafeInteger(e.right.value)) return null; + const left = reversed(e.left, bound); + if (left === null) return null; + const result = left + (e.op === "+" ? e.right.value : -e.right.value); + return Number.isSafeInteger(result) ? result : null; + } function affineRange(e: IrExpr, bound: Bound): IntegerRange | null { if (e.kind === "varRef" && e.localId === bound.counter) { const max = Number.MAX_SAFE_INTEGER + bound.offset - 1; @@ -159,7 +187,7 @@ export function findBytesBounds(fn: IrFunction, ranges: IntegerRanges): Readonly if (s.cond.left.kind !== "varRef" || s.cond.left.localId !== counter) { if (right.kind !== "varRef" || right.localId !== counter) return null; right = s.cond.left; - op = ({ "<": ">", "<=": ">=", ">": "<", ">=": "<=" } as Record)[op] ?? ""; + op = FLIPPED[op] ?? ""; } const update = s.update; let step = 0; @@ -212,6 +240,98 @@ export function findBytesBounds(fn: IrFunction, ranges: IntegerRanges): Readonly } return result && stable(s.body, result.receiver, counter) ? result : null; } + /** A comparison of the counter with the receiver's length: the operator + * with the counter on the left, and the length's offset. */ + function lengthComparison(e: IrExpr, bound: Bound): { op: string; offset: number } | null { + if (e.kind !== "bin") return null; + let other = e.right, + op: string = e.op; + if (e.left.kind !== "varRef" || e.left.localId !== bound.counter) { + if (other.kind !== "varRef" || other.localId !== bound.counter) return null; + other = e.left; + op = FLIPPED[op] ?? op; + } + const length = extent(other, false); + return length && length.receiver === bound.receiver ? { op, offset: length.offset } : null; + } + /** The step of `counter = counter + c` with a positive literal c, or null. */ + function increment(s: IrStmt, counter: string): number | null { + return s.kind === "assign" && + s.localId === counter && + s.value.kind === "bin" && + s.value.op === "+" && + s.value.left.kind === "varRef" && + s.value.left.localId === counter && + s.value.right.kind === "numLit" && + Number.isSafeInteger(s.value.right.value) && + s.value.right.value > 0 + ? s.value.right.value + : null; + } + function writes(s: IrStmt, id: string): boolean { + return !everyStmtList([s], { + expr: (e) => (e.kind !== "assignExpr" && e.kind !== "incDec") || e.localId !== id, + stmt: (child) => + !( + ((child.kind === "assign" || child.kind === "forOf" || child.kind === "varDecl") && + child.localId === id) || + (child.kind === "tryCatch" && child.catchLocalId === id) + ), + }); + } + /** A while loop whose condition bounds an integer counter by a typed + * array's length. The body may write the counter only by statements + * that add a positive literal, and may not write the receiver. Calls + * cannot write either binding (both are uncaptured locals), and a typed + * array's length is fixed at construction, so the body may hold calls. */ + function whileBound(s: IrStmt & { kind: "while" }): Bound | null { + const cond = s.cond; + if (cond.kind !== "bin") return null; + const counterRef = [cond.left, cond.right].find( + (side, i) => + side.kind === "varRef" && + eligible(side.localId) && + extent(i === 0 ? cond.right : cond.left, false) !== null, + ); + const minimum = counterRef ? ranges.get(counterRef)?.min : undefined; + if (counterRef?.kind !== "varRef" || minimum === undefined) return null; + const counter = counterRef.localId; + const length = extent(counterRef === cond.left ? cond.right : cond.left, false)!; + const receiver = length.receiver; + const comparison = lengthComparison(cond, { receiver, counter, offset: 0, minimum }); + if (comparison?.op !== "<" && comparison?.op !== "<=") return null; + // An increment is a statement of the body: one inside an expression + // (a comma expression's statements) would run where flow does not + // follow it. + const writesOk = everyStmtList(s.body, { + expr: (e) => + ((e.kind !== "assignExpr" && e.kind !== "incDec") || + (e.localId !== counter && e.localId !== receiver)) && + everyExprChild( + e, + () => true, + (child) => !writes(child, counter), + ), + stmt: (child) => { + if (child.kind === "assign" && child.localId === counter) + return increment(child, counter) !== null; + if (child.kind === "assign" || child.kind === "forOf" || child.kind === "varDecl") + return child.localId !== counter && child.localId !== receiver; + return ( + child.kind !== "tryCatch" || + (child.catchLocalId !== counter && child.catchLocalId !== receiver) + ); + }, + }); + return writesOk + ? { + receiver, + counter, + offset: comparison.offset + (comparison.op === "<=" ? 1 : 0), + minimum, + } + : null; + } function access(node: Node, array: IrExpr, index: IrExpr, active: Bound[], width: number): void { const integer = ranges.get(index); let safe = false; @@ -228,6 +348,13 @@ export function findBytesBounds(fn: IrFunction, ranges: IntegerRanges): Readonly safe = true; break; } + // counter < L + bound.offset and counter >= bound.minimum put + // L + d - counter in [0, L - width]. + const d = integer ? reversed(index, bound) : null; + if (d !== null && bound.offset <= d + 1 && bound.minimum >= d + width) { + safe = true; + break; + } } } proved.set(node, (proved.get(node) ?? true) && safe); @@ -244,8 +371,84 @@ export function findBytesBounds(fn: IrFunction, ranges: IntegerRanges): Readonly (child) => stmt(child, active), ); } + /** The bounds where `cond` is true and where it is false, from the + * bound before it. */ + function narrow(cond: IrExpr, bound: Bound): [Bound, Bound] { + const comparison = lengthComparison(cond, bound); + if (!comparison) return [bound, bound]; + const j = comparison.offset, + offset = bound.offset; + // counter < L + j, counter <= L + j, and counter !== L + j. + const below = { ...bound, offset: Math.min(offset, j) }, + atMost = { ...bound, offset: Math.min(offset, j + 1) }, + unequal = offset === j + 1 ? { ...bound, offset: j } : bound; + switch (comparison.op) { + case "<": + return [below, bound]; + case "<=": + return [atMost, bound]; + case ">": + return [bound, atMost]; + case ">=": + return [bound, below]; + case "===": + case "==": + return [atMost, unequal]; + case "!==": + case "!=": + return [unequal, atMost]; + default: + return [bound, bound]; + } + } + /** Visits a while body in order, from the bound that holds on entry. + * Returns the bound at the end of the list. Its offset is -Infinity (and + * its minimum Infinity) when the end is unreachable, and Infinity when + * nothing bounds the counter there. Every write of the counter adds a + * positive literal (whileBound), so the minimum survives any write. */ + function flow(list: IrStmt[], active: Bound[], bound: Bound): Bound { + for (const s of list) { + const current = Number.isFinite(bound.offset) ? [...active, bound] : active; + const step = increment(s, bound.counter); + if (step !== null) { + bound = { ...bound, offset: bound.offset + step, minimum: bound.minimum + step }; + } else if (s.kind === "if") { + expr(s.cond, current); + const [then, else_] = narrow(s.cond, bound); + const left = flow(s.then, active, then), + right = s.else_ ? flow(s.else_, active, else_) : else_; + bound = { + ...bound, + offset: Math.max(left.offset, right.offset), + minimum: Math.min(left.minimum, right.minimum), + }; + } else if (s.kind === "block" && !s.labels?.length) { + bound = flow(s.body, active, bound); + } else if (writes(s, bound.counter)) { + stmt(s, active); + bound = { ...bound, offset: Infinity }; + } else { + stmt(s, current); + if ( + s.kind === "break" || + s.kind === "continue" || + s.kind === "return" || + s.kind === "throw" + ) + bound = { ...bound, offset: -Infinity, minimum: Infinity }; + } + } + return bound; + } function stmt(s: IrStmt, active: Bound[]): boolean { if (s.kind === "bytesSet") access(s, s.arr, s.index, active, 1); + if (s.kind === "while") { + expr(s.cond, active); + const bound = whileBound(s); + if (!bound) return s.body.every((child) => stmt(child, active)); + flow(s.body, active, bound); + return true; + } if (s.kind === "for") { if (s.init) stmt(s.init, active); if (s.cond) expr(s.cond, active); diff --git a/packages/compiler/src/backend/llvm/bytes-headers.ts b/packages/compiler/src/backend/llvm/bytes-headers.ts new file mode 100644 index 000000000..f5b95cf75 --- /dev/null +++ b/packages/compiler/src/backend/llvm/bytes-headers.ts @@ -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; + /** The header of each receiver value read from one of `locals`. */ + values: Map; +} + +/** 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, +): BytesHeaderScope | null { + const receivers = new Set(); + const rebound = new Set(); + 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(); + 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() }; +} diff --git a/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts b/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts index cee91c66d..742d256b7 100644 --- a/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts +++ b/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts @@ -11,7 +11,7 @@ import { type IrType, } from "../../ir/ir.js"; import { analyzeIntegerRanges } from "../../ir/integer-ranges.js"; -import { matchIntegerCountedForLoop } from "../../ir/integer-loops.js"; +import { matchIntegerCountedForLoop, matchIntegerPairedForLoop } from "../../ir/integer-loops.js"; import { findInitializedByteLoopBindings } from "./byte-windows.js"; import { matchBytesLoopVersion, type BytesLoopVersion } from "./bytes-loop-versions.js"; import { emitLlvmModule } from "./emitter.js"; @@ -249,6 +249,11 @@ test("emitted loops version the body and tag typed-array memory", () => { const elements = /store double [^\n]*!tbaa !(\d+)/.exec(body)![1]; const header = /load ptr, ptr [^\n]*!tbaa !(\d+)/.exec(body)![1]; expect(elements).not.toBe(header); + // Header loads are invariant for the view's lifetime; element accesses + // are not. + expect(body).toMatch(/load ptr, ptr [^\n]*, !tbaa !\d+, !invariant\.load !\{\}$/m); + expect(body).toMatch(/load i64, ptr [^\n]*, !tbaa !\d+, !invariant\.load !\{\}$/m); + expect(body).not.toMatch(/(load double|store double)[^\n]*invariant\.load/); }); test("loops to a typed array's length version the other typed arrays they index", () => { @@ -312,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", () => { @@ -380,7 +392,10 @@ test("while loop counters below a bound version their accesses", () => { ]); const declared = (loop: IrStmt): IrFunction => fn([{ kind: "varDecl", localId: "i", init: ref("k"), loc }, loop]); - const versionOf = (loop: IrStmt & { kind: "while" }): BytesLoopVersion | null => { + const versionOf = ( + loop: IrStmt & { kind: "while" }, + workers = false, + ): BytesLoopVersion | null => { const f = declared(loop); const ranges = analyzeIntegerRanges(f); return matchBytesLoopVersion(loop, null, { @@ -391,7 +406,7 @@ test("while loop counters below a bound version their accesses", () => { initialized: findInitializedByteLoopBindings(f).get(loop) ?? new Set(), proven: new Set(), scalarStrings: new Set(), - workers: false, + workers, owned: new Set(), }); }; @@ -408,6 +423,11 @@ test("while loop counters below a bound version their accesses", () => { // An access after the counter advances in the same pass stays checked. const advanced = whileLoop([step, set(ref("out", f64s), ref("i"), num(0))]); expect(versionOf(advanced)).toBeNull(); + // In a worker program, a while loop versions for its counter's accesses, + // and not for thread-owned receivers alone. + expect(versionOf(copy, true)!.ownedReceivers).toHaveLength(2); + const job = whileLoop([set(ref("out", f64s), ref("k"), get(ref("src", f64s), ref("k"))), step]); + expect(versionOf(job, true)).toBeNull(); const mod: IrModule = { irVersion: 17, sourceFile: loc.file, @@ -567,7 +587,9 @@ test("the guard tests a string's ASCII bit and the fast copy reads bytes", () => const body = /^define internal [^\n]*@sc_bf_kernel[^]*?^}/m.exec(emitLlvmModule(mod))![0]; const guard = body.slice(0, body.search(/^bytes\.version\.slow\d*:/m)); expect(guard).toMatch(/load i32, ptr %\S+\n\s+%\S+ = icmp slt i32 %\S+, 0/); - const fast = body.slice(body.lastIndexOf("bytes.version.fast")); + // The byte copy, up to the units copy that follows it. + const units = body.search(/^bytes\.version\.fast\.units\d+:/m); + const fast = body.slice(body.search(/^bytes\.version\.fast\d+:/m), units < 0 ? undefined : units); expect(fast).toMatch(/load i8, ptr %\S+\n\s+%\S+ = uitofp i8/); expect(fast).not.toContain("@sc_str_char_code_at_int"); // The checked copy keeps the inline helper and its fallback. @@ -618,3 +640,79 @@ test("while loop counters below a string's length version its reads", () => { expect(version.bounded.map((c) => c.slack)).toEqual([0]); expect(version.fixed).toEqual([]); }); + +test("a two-counter loop versions the typed arrays it indexes at either counter", () => { + // for (let i = 0, j = k; i < j; i++, j--) { const t = out[i]; out[i] = out[j]; out[j] = t; } + const loop: IrStmt & { kind: "for" } = { + kind: "for", + init: { + kind: "block", + body: [ + { kind: "varDecl", localId: "i", init: num(0), loc }, + { kind: "varDecl", localId: "j", init: ref("k"), loc }, + ], + loc, + }, + cond: { kind: "bin", op: "<", left: ref("i"), right: ref("j"), type: BOOL, loc }, + update: { + kind: "block", + body: [ + { kind: "assign", localId: "i", value: bin("+", ref("i"), num(1)), loc }, + { kind: "assign", localId: "j", value: bin("-", ref("j"), num(1)), loc }, + ], + loc, + }, + body: [ + { kind: "varDecl", localId: "t", init: get(ref("out", f64s), ref("i")), loc }, + set(ref("out", f64s), ref("i"), get(ref("out", f64s), ref("j"))), + set(ref("out", f64s), ref("j"), ref("t")), + // Another index stays checked. + set(ref("src", f64s), bin("+", ref("i"), num(1)), num(0)), + ], + loc, + }; + const kernel = fn([loop], [local("t", F64)]); + const ranges = analyzeIntegerRanges(kernel); + const locals = new Map(kernel.locals.map((l) => [l.id, l])); + const paired = matchIntegerPairedForLoop(loop, locals, ranges)!; + expect(paired).not.toBeNull(); + const version = matchBytesLoopVersion( + loop, + null, + { + locals, + invariantGlobals: new Set(), + captured: new Set(), + ranges, + initialized: findInitializedByteLoopBindings(kernel).get(loop) ?? new Set(), + proven: new Set(), + workers: false, + owned: new Set(), + scalarStrings: new Set(), + }, + paired, + )!; + expect(version.paired?.loop).toBe(paired); + expect(version.paired?.receivers.map((r) => version.receivers[r]!.ref)).toEqual([ + ref("out", f64s), + ]); + expect(version.bounds.size).toBe(4); + const mod: IrModule = { + irVersion: 17, + sourceFile: loc.file, + entry: "__main", + functions: [ + { name: "__main", params: [], returnType: VOID, locals: [], body: [], loc }, + kernel, + ], + }; + const body = /^define internal [^\n]*@sc_bf_kernel[^]*?^}/m.exec(emitLlvmModule(mod))![0]; + const guard = body.slice(0, body.search(/^bytes\.version\.(fast|slow)\d+:$/m)); + // `i` starts at the literal 0, so its test compares the integer itself. + expect(guard).toMatch(/icmp sge i64 0, 0/); + expect(guard).toMatch(/icmp slt i64 %\S+, %\S+/); + const fast = /^bytes\.version\.fast\d+:$[^]*?^bytes\.version\.done\d+:$/m.exec(body)![0]; + // The fast copy keeps only the `src[i + 1]` check, and no second guard. + expect(fast.match(/bytes\.index\.invalid\d+:/g)).toHaveLength(1); + expect(fast).not.toContain("counted.fast"); +}); diff --git a/packages/compiler/src/backend/llvm/bytes-loop-versions.ts b/packages/compiler/src/backend/llvm/bytes-loop-versions.ts index 704b6723a..ccf217f9f 100644 --- a/packages/compiler/src/backend/llvm/bytes-loop-versions.ts +++ b/packages/compiler/src/backend/llvm/bytes-loop-versions.ts @@ -1,14 +1,34 @@ -import { F64, type IrExpr, type IrLocal, type IrStmt } from "../../ir/ir.js"; +import { + BYTES_ELEMENT_SIZE, + F64, + type IrExpr, + type IrLocal, + type IrStmt, + type IrType, +} from "../../ir/ir.js"; import type { IntegerRange, IntegerRanges } from "../../ir/integer-ranges.js"; -import type { IntegerCountedForLoop } from "../../ir/integer-loops.js"; +import type { IntegerCountedForLoop, IntegerPairedForLoop } from "../../ir/integer-loops.js"; import type { LlvmEmitterContext } from "./expr-context.js"; import { byteNumberAccess } from "../../ir/byte-numbers.js"; import { everyExprChild, everyStmtChild } from "../../ir/traverse.js"; import { exactInteger, widenInteger } from "./integer-values.js"; -import { emitCountedLoopLimit, emitCountedLoopSafety } from "./counted-loops.js"; -import { emitBytesHeaderLength, emitBytesSharedStorage } from "./expr-bytes.js"; +import { + emitCountedLoopLimit, + emitCountedLoopSafety, + emitStartIntegerTest, + emitValueIntegerTest, +} from "./counted-loops.js"; +import { integerOrNaNRange } from "./wrapping-sums.js"; +import { emitBytesData, emitBytesHeaderLength, emitBytesSharedStorage } from "./expr-bytes.js"; import { f64Lit } from "./common.js"; -import { emitStringAsciiFlag, emitStringByteLength } from "./string-reads.js"; +import { InternalCompilerError } from "../../errors.js"; +import { + emitStringAsciiFlag, + emitStringByteLength, + STRING_SCAN_UNITS_DECL, + stringScanType, +} from "./string-reads.js"; +import { unitSafeLoop } from "./string-unit-versions.js"; type Node = IrExpr | IrStmt; type VarRef = IrExpr & { kind: "varRef" }; @@ -67,8 +87,23 @@ 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; + /** The version's one string receiver, when the loop may also run over + * its UTF-16 units if it is not ASCII (string-unit-versions.ts). */ + unitsReceiver: number | null; + /** The units receiver's `length` reads in the loop. Both fast copies + * read the length the guard took (its UTF-16 length) in their place. */ + unitLengths: ReadonlySet; fixed: FixedCheck[]; affine: AffineCheck[]; /** While loops: accesses at a counter below a loop-invariant bound. */ @@ -79,11 +114,21 @@ export interface BytesLoopVersion { guarded: ReadonlySet; /** The counted loop whose iteration space the affine checks cover. */ counted: IntegerCountedForLoop | null; + /** A two-counter loop (`for (let i = lo, j = hi; i < j; i++, j--)`) and + * the receivers it indexes at either counter. Both counters stay between + * the two starts, so the guard requires `lo >= 0` and `hi < length`. */ + paired: { loop: IntegerPairedForLoop; receivers: number[] } | null; /** The guard also requires every counter value within [-bound, bound]. */ counterBound: number | null; /** Integer ranges of affine index operands that hold in the unchecked * loop (where the guard bounds the counter). */ ranges: ReadonlyMap; + /** Typed-array receivers whose element ranges the guard proves disjoint, + * and the receiver of each of their element reads and writes. The fast + * copy gives each receiver its own alias scope, so a store to one view + * leaves the elements of the others in registers. Null when the loop + * writes no element, or reads and writes a single view. */ + scopes: { receivers: number[]; nodes: ReadonlyMap } | null; } export interface BytesLoopFacts { @@ -106,11 +151,16 @@ export interface BytesLoopFacts { /** String bindings held as a slice of another string (string-slices.ts): * they have no string header for the guard to read. */ scalarStrings: ReadonlySet; + /** The functions a loop over a string's UTF-16 units may call; absent + * or null where emitted code does not read the runtime's scan receiver. */ + unitSafeFunctions?: ReadonlySet | null; } const MAX_COST = 400; const MAX_FIXED_END = 2 ** 31; const MAX_CHECKS = 8; +/** Alias-scoped views per loop: the guard tests every pair's ranges. */ +const MAX_SCOPED_RECEIVERS = 6; const MAX_OFFSET = 2 ** 31; const SAFE = Number.MAX_SAFE_INTEGER; /** The range the guard requires of an invariant index term without a @@ -153,6 +203,7 @@ export function matchBytesLoopVersion( loop: IrStmt & { kind: "for" | "while" }, counted: IntegerCountedForLoop | null, facts: BytesLoopFacts, + paired: IntegerPairedForLoop | null = null, ): BytesLoopVersion | null { const written = new Set(); const temps = new Map(); @@ -438,6 +489,9 @@ export function matchBytesLoopVersion( string, { check: BoundedCheck; nodes: { node: Node; access: WhileAccess }[] } >(); + const pairedCounters = + paired !== null && counted === null ? new Set([paired.low.localId, paired.high.localId]) : null; + const pairedNodes = new Map(); const access = (node: Node, array: IrExpr, index: IrExpr, width: number): void => { const ref = root(array); if (!ref) return; @@ -448,6 +502,20 @@ export function matchBytesLoopVersion( else owned.set(receiver, [node]); } if (facts.proven.has(node)) return; + // An element at either counter of a two-counter loop. + if ( + pairedCounters !== null && + width === 1 && + ref.type.kind === "bytes" && + index.kind === "varRef" && + pairedCounters.has(index.localId) + ) { + const receiver = receiverOf(ref); + const nodes = pairedNodes.get(receiver); + if (nodes) nodes.push(node); + else pairedNodes.set(receiver, [node]); + return; + } const shape = counter !== null ? affine(index) : null; if (shape && shape.counter !== 0) { const operands = new Set(); @@ -504,11 +572,24 @@ export function matchBytesLoopVersion( group.nodes.push(node); } else fixed.set(receiver, { check: { receiver, end }, nodes: [node] }); }; + // Element reads and writes of each typed-array receiver, for alias scopes. + const scoped = new Map(); + const writtenViews = new Set(); + const scope = (node: Node, array: IrExpr, write: boolean): void => { + const ref = root(array); + if (!ref || ref.type.kind !== "bytes") return; + const receiver = receiverOf(ref); + scoped.set(node, receiver); + if (write) writtenViews.add(receiver); + }; const visitExpr = (e: IrExpr): boolean => { if (e.kind === "bytesIntrinsic") { const numeric = byteNumberAccess(e); if (numeric) access(e, e.receiver, e.args[numeric.offsetArg]!, numeric.width); - else if (e.method === "get" && e.args[0]) access(e, e.receiver, e.args[0], 1); + else if (e.method === "get" && e.args[0]) { + access(e, e.receiver, e.args[0], 1); + scope(e, e.receiver, false); + } } else if ( e.kind === "strIntrinsic" && (e.method === "charCodeAt" || e.method === "charAt") && @@ -518,7 +599,10 @@ export function matchBytesLoopVersion( return everyExprChild(e, visitExpr, visitStmt); }; const visitStmt = (s: IrStmt): boolean => { - if (s.kind === "bytesSet") access(s, s.arr, s.index, 1); + if (s.kind === "bytesSet") { + access(s, s.arr, s.index, 1); + scope(s, s.arr, true); + } return everyStmtChild(s, visitExpr, visitStmt); }; loop.body.every(visitStmt); @@ -543,13 +627,23 @@ export function matchBytesLoopVersion( Math.max(0, MAX_CHECKS - fixedKept.length - affineKept.length), ); const ownedKept = [...owned.entries()].slice(0, MAX_CHECKS); + const pairedKept = [...pairedNodes.entries()].slice( + 0, + Math.max(0, MAX_CHECKS - fixedKept.length - affineKept.length - boundedKept.length), + ); if ( fixedKept.length === 0 && affineKept.length === 0 && boundedKept.length === 0 && - ownedKept.length === 0 + ownedKept.length === 0 && + pairedKept.length === 0 ) return null; + // A while loop versions only for accesses at its counters. Its other + // checks (thread-owned receivers, static indices) save a test or two per + // pass, while a second copy of its body doubles the calls it makes, which + // can keep a worker's job loop from inlining the function that runs a job. + if (loop.kind === "while" && boundedKept.length === 0) return null; const bounds = new Set(); const ownedNodes = new Set(); const used = new Set(); @@ -616,6 +710,10 @@ export function matchBytesLoopVersion( if (access.index !== access.ref) narrow(access.index, { min: 0, max: SAFE }); } } + for (const [receiver, nodes] of pairedKept) { + used.add(receiver); + for (const node of nodes) bounds.add(node); + } const counterChecks: CounterCheck[] = []; for (const [id, min] of counterMins) { const info = whileAccesses!.counters.get(id)!; @@ -627,9 +725,33 @@ export function matchBytesLoopVersion( : [], }); } + // Alias scopes: at least two views, one of them written, each read only + // through plain loads (in a worker program, a view the guard proves + // thread-owned). + const scopedReceivers = [...new Set(scoped.values())].sort((a, b) => a - b); + const ownedSet = new Set(ownedKept.map(([receiver]) => receiver)); + const scoping = + scopedReceivers.length >= 2 && + scopedReceivers.length <= MAX_SCOPED_RECEIVERS && + scopedReceivers.some((receiver) => writtenViews.has(receiver)) && + (!facts.workers || scopedReceivers.every((receiver) => ownedSet.has(receiver))); + if (scoping) for (const receiver of scopedReceivers) used.add(receiver); // Renumber the receivers the kept checks use. const order = [...used].sort((a, b) => a - b); const renumber = new Map(order.map((old, index) => [old, index])); + const stringReads = new Set(); + for (const group of [...fixedKept, ...affineKept]) + if (receivers[group.check.receiver]!.string) + for (const node of group.nodes) stringReads.add(node); + for (const group of boundedKept) + if (receivers[group.check.receiver]!.string) + for (const { node } of group.nodes) stringReads.add(node); + const strings = order.filter((index) => receivers[index]!.string); + const unitLengths = + facts.unitSafeFunctions != null && strings.length === 1 + ? unitSafeLoop(loop, receivers[strings[0]!]!.ref.localId, facts.unitSafeFunctions) + : null; + const unitsReceiver = unitLengths !== null ? renumber.get(strings[0]!)! : null; const needsBound = affineKept.length > 0 && counted !== null && @@ -639,6 +761,10 @@ 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(), fixed: fixedKept.map((g) => ({ receiver: renumber.get(g.check.receiver)!, end: g.check.end, @@ -658,8 +784,18 @@ export function matchBytesLoopVersion( counters: counterChecks, guarded, counted: affineKept.length > 0 ? counted : null, + paired: + paired !== null && pairedKept.length > 0 + ? { loop: paired, receivers: pairedKept.map(([receiver]) => renumber.get(receiver)!) } + : null, counterBound: needsBound ? bound : null, ranges, + scopes: scoping + ? { + receivers: scopedReceivers.map((receiver) => renumber.get(receiver)!), + nodes: new Map([...scoped].map(([node, receiver]) => [node, renumber.get(receiver)!])), + } + : null, }; } @@ -974,13 +1110,27 @@ function findWhileCounterAccesses( /** Branches to `fast` when every access the version proves is in bounds for * every iteration and every view it proves thread-owned has no shared * storage, and to `slow` otherwise (including a counted loop whose - * integer form is not established, or a module receiver not yet set). */ + * integer form is not established, or a module receiver not yet set). + * + * With `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, + * and branches to `unitsFast`. It returns the units pointer that copy + * reads, which stays valid while the loop runs (string-unit-versions.ts), + * and for a version with a units receiver, the receiver's UTF-16 length + * on both fast paths (an i64). */ export function emitBytesLoopVersionGuard( host: LlvmEmitterContext, version: BytesLoopVersion, fast: string, slow: string, -): void { + unitsFast?: string, + sharedFast?: string, +): { units: string | null; length: string | null } | null { const B = host.B; const next = (cond: string): void => { if (cond === "true") return; @@ -999,30 +1149,75 @@ export function emitBytesLoopVersionGuard( } return value; }); - const lengths = receivers.map((receiver, index) => { - const len = version.receivers[index]!.string - ? emitStringByteLength(host, receiver) - : emitBytesHeaderLength(host, receiver); + const unitsIndex = unitsFast !== undefined ? version.unitsReceiver : null; + const units: { read: { pointer: string; taken: string; published: string } | null } = { + read: null, + }; + const widen = (len: string): string => { if (host.sizeType === "i64") return len; const wide = B.tmp(); B.line(`${wide} = zext ${host.sizeType} ${len} to i64`); return wide; + }; + const lengths = receivers.map((receiver, index) => { + if (index === unitsIndex) { + const read = emitUnitsLength(host, receiver, () => emitUnitsSpan(host, version, index)); + units.read = read; + return read.length; + } + return widen( + version.receivers[index]!.string + ? emitStringByteLength(host, receiver) + : emitBytesHeaderLength(host, receiver), + ); }); const conditions: string[] = []; + // The published count is the receiver's UTF-16 length only while the + // runtime publishes the receiver itself. + if (units.read) conditions.push(units.read.published); // A string's byte length is its UTF-16 length when it is ASCII. version.receivers.forEach(({ string }, index) => { - if (string) conditions.push(emitStringAsciiFlag(host, receivers[index]!)); + if (string && index !== unitsIndex) + conditions.push(emitStringAsciiFlag(host, receivers[index]!)); }); + // 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(); B.line(`${ok} = icmp ugt i64 ${lengths[check.receiver]}, ${check.end}`); conditions.push(ok); } + if (version.scopes) { + // Every pair of scoped views covers disjoint bytes: one ends at or + // before the other starts. An empty view covers none. + const spans = version.scopes.receivers.map((index) => { + const type = version.receivers[index]!.ref.type as IrType & { kind: "bytes" }; + const start = B.tmp(), + size = B.tmp(), + end = B.tmp(); + B.line(`${start} = ptrtoint ptr ${emitBytesData(host, receivers[index]!)} to i64`); + B.line(`${size} = mul nuw i64 ${lengths[index]}, ${BYTES_ELEMENT_SIZE[type.elem]}`); + B.line(`${end} = add nuw i64 ${start}, ${size}`); + return { start, end }; + }); + spans.forEach((a, i) => { + for (const b of spans.slice(i + 1)) { + const before = B.tmp(), + after = B.tmp(), + apart = B.tmp(); + B.line(`${before} = icmp ule i64 ${a.end}, ${b.start}`); + B.line(`${after} = icmp ule i64 ${b.end}, ${a.start}`); + B.line(`${apart} = or i1 ${before}, ${after}`); + conditions.push(apart); + } + }); + } for (const counter of version.counters) { const value = host.emitExpr(counter.ref); // A counter held as an integer (an int32 slot) is exact already and @@ -1034,7 +1229,7 @@ export function emitBytesLoopVersionGuard( B.line(`${ok} = icmp sge i64 ${widenInteger(host, integer)}, ${counter.min}`); conditions.push(ok); } - } else conditions.push(emitExactIntegerTest(host, value.name, counter.min, SAFE).ok); + } else conditions.push(emitValueIntegerTest(host, value, counter.ref, counter.min, SAFE).ok); for (const wrap of counter.wrapBounds) { const bound = host.emitExpr(wrap.bound); const boundInteger = exactInteger(host, bound, wrap.bound); @@ -1064,6 +1259,9 @@ export function emitBytesLoopVersionGuard( const startInteger = exactInteger(host, startValue, counted.start); let start: string; if (startInteger) start = widenInteger(host, startInteger); + else if (startValue.nanInteger !== undefined && integerOrNaNRange(host, counted.start)) + // The counted-loop safety test required the ordered bit. + start = startValue.nanInteger; else { // The counted-loop safety test above proved an exact integer. start = B.tmp(); @@ -1102,7 +1300,13 @@ export function emitBytesLoopVersionGuard( for (const term of check.terms) { const value = host.emitExpr(term.expr); if (version.guarded.has(term.key)) { - const tested = emitExactIntegerTest(host, value.name, GUARDED_TERM.min, GUARDED_TERM.max); + const tested = emitValueIntegerTest( + host, + value, + term.expr, + GUARDED_TERM.min, + GUARDED_TERM.max, + ); conditions.push(tested.ok); const sum = B.tmp(); B.line(`${sum} = ${term.sign > 0 ? "add" : "sub"} i64 ${base}, ${tested.integer}`); @@ -1114,7 +1318,7 @@ export function emitBytesLoopVersionGuard( // Unreachable: every term has a proven range. Stay checked. B.br(slow); B.startBlock(B.newLabel("bytes.version.dead")); - return; + return null; } const wide = widenInteger(host, integer); const sum = B.tmp(); @@ -1143,6 +1347,25 @@ export function emitBytesLoopVersionGuard( conditions.push(both); } } + if (version.paired !== null) { + // Both counters stay within [lo, hi] (exact integers below 2^53): every + // access at either one is in bounds when `lo >= 0` and `hi < length`. + const { loop } = version.paired; + const [low, high] = [loop.low.start, loop.high.start].map((expr) => { + const tested = emitStartIntegerTest(host, expr, -SAFE, SAFE); + if (tested.ok !== "true") conditions.push(tested.ok); + return tested.integer; + }); + const nonNegative = B.tmp(); + B.line(`${nonNegative} = icmp sge i64 ${low}, 0`); + conditions.push(nonNegative); + for (const receiver of version.paired.receivers) { + // Lengths are at most 2^53 elements: a signed comparison is exact. + const below = B.tmp(); + B.line(`${below} = icmp slt i64 ${high}, ${lengths[receiver]}`); + conditions.push(below); + } + } let valid = "true"; for (const cond of conditions) { if (valid === "true") valid = cond; @@ -1152,41 +1375,134 @@ export function emitBytesLoopVersionGuard( valid = all; } } - if (valid === "true") B.br(fast); - else B.condBr(valid, fast, slow); + const length = version.unitsReceiver !== null ? lengths[version.unitsReceiver]! : null; + const taken = units.read; + const target = taken ? B.newLabel("bytes.version.units") : fast; + if (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); + B.condBr(taken.taken, unitsFast!, fast); + return { units: taken.pointer, length }; } -/** Tests that a double is an exact integer within [min, max] (not -0; NaN - * fails the ordered comparisons) and gives its i64. The integer is frozen, - * so it is a fixed value even when the test fails. */ -function emitExactIntegerTest( +/** The 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, - value: string, - min: number, - max: number, -): { ok: string; integer: string } { + 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 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 low = B.tmp(), - high = B.tmp(), - inRange = B.tmp(), - raw = B.tmp(), - integer = B.tmp(), - back = B.tmp(), - bits = B.tmp(), - backBits = B.tmp(), - exact = B.tmp(), - ok = B.tmp(); - B.line(`${low} = fcmp oge double ${value}, ${f64Lit(min)}`); - B.line(`${high} = fcmp ole double ${value}, ${f64Lit(max)}`); - B.line(`${inRange} = and i1 ${low}, ${high}`); - B.line(`${raw} = fptosi double ${value} to i64`); - B.line(`${integer} = freeze i64 ${raw}`); - B.line(`${back} = sitofp i64 ${integer} to double`); - B.line(`${bits} = bitcast double ${value} to i64`); - B.line(`${backBits} = bitcast double ${back} to i64`); - B.line(`${exact} = icmp eq i64 ${bits}, ${backBits}`); - B.line(`${ok} = and i1 ${inRange}, ${exact}`); - return { ok, integer }; + const sz = host.sizeType; + const type = stringScanType(sz); + const ascii = emitStringAsciiFlag(host, s); + const bytes = emitStringByteLength(host, s); + const from = B.currentLabel(); + const publish = B.newLabel("bytes.version.publish"), + join = B.newLabel("bytes.version.length"); + B.condBr(ascii, join, publish); + B.startBlock(publish); + // When `s` is already the scan receiver (an earlier loop over it published + // it), its units stay published until the runtime clears the scan: skip + // the call. 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, `, !prof !{!"branch_weights", i32 1000, i32 1}`); + B.startBlock(call); + host.declare(STRING_SCAN_UNITS_DECL); + const receiver = B.tmp(), + same = B.tmp(), + pointerSlot = B.tmp(), + pointer = B.tmp(), + countSlot = B.tmp(), + count = B.tmp(), + published = B.tmp(); + 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`); + B.line(`${same} = icmp eq ptr ${receiver}, ${s}`); + B.line(`${pointerSlot} = getelementptr inbounds ${type}, ptr @scr_str_scan, i32 0, i32 1`); + B.line(`${pointer} = load ptr, ptr ${pointerSlot}`); + B.line(`${countSlot} = getelementptr inbounds ${type}, ptr @scr_str_scan, i32 0, i32 2`); + B.line(`${count} = load ${sz}, ptr ${countSlot}`); + B.line(`${published} = select i1 ${same}, ${sz} ${count}, ${sz} 0`); + const end = B.currentLabel(); + B.br(join); + B.startBlock(join); + const length = B.tmp(), + units = B.tmp(), + taken = B.tmp(); + B.line(`${length} = phi ${sz} [ ${bytes}, %${from} ], [ ${published}, %${end} ]`); + B.line(`${units} = phi ptr [ null, %${from} ], [ ${pointer}, %${end} ]`); + B.line(`${taken} = phi i1 [ false, %${from} ], [ true, %${end} ]`); + const exact = B.tmp(); + B.line(`${exact} = phi i1 [ true, %${from} ], [ ${same}, %${end} ]`); + let wide = length; + if (sz !== "i64") { + wide = B.tmp(); + B.line(`${wide} = zext ${sz} ${length} to i64`); + } + return { length: wide, pointer: units, taken, published: exact }; } function scaleCounter(host: LlvmEmitterContext, value: string, scale: number): string { diff --git a/packages/compiler/src/backend/llvm/call-lifetimes.test.ts b/packages/compiler/src/backend/llvm/call-lifetimes.test.ts index 90909a024..5c40bb4ce 100644 --- a/packages/compiler/src/backend/llvm/call-lifetimes.test.ts +++ b/packages/compiler/src/backend/llvm/call-lifetimes.test.ts @@ -760,3 +760,154 @@ test("a parameter stored as its last use takes the owned convention", () => { const passed = ctor([{ kind: "exprStmt", expr: call("use", [ref("item", cell)]), loc }]); expect(analyze(passed).borrowed.get("%Box.constructor")).toEqual(new Set([0, 1])); }); + +test("a wrap into a pointer union forwards the wrapped binding to the callee's parameter", () => { + const wrap = (id: string): IrExpr => ({ + kind: "unionWrap", + unionId: "optional", + tag: 0, + value: ref(id, record), + type: optional, + loc, + }); + const leaf = helper("leaf"); + const outer = helper("outer"); + outer.params[0]!.type = outer.locals[0]!.type = record; + outer.locals.push(local("cell", record)); + outer.body = [ + { + kind: "varDecl", + localId: "cell", + init: { kind: "recordLit", fields: [], type: record, loc }, + loc, + }, + { kind: "exprStmt", expr: call("leaf", [wrap("value")]), loc }, + ret(call("leaf", [wrap("cell")])), + ]; + const functions = new Map([leaf, outer].map((fn) => [fn.name, fn])); + const analyzeWith = (pointer: boolean) => + analyzeCallLifetimes( + functions, + undefined, + undefined, + undefined, + undefined, + undefined, + () => pointer, + ); + // A boxed union builds a new box around the payload: a whole-value use. + expect(analyzeWith(false).parameters.get("outer")).toBeUndefined(); + expect(analyzeWith(false).locals.get("outer")).toEqual(new Set()); + // A pointer union passes the payload pointer itself. + expect(analyzeWith(true).parameters.get("outer")).toEqual(new Set([0])); + expect(analyzeWith(true).locals.get("outer")).toEqual(new Set(["cell"])); + // A callee that keeps its parameter keeps the wrapped binding too. + leaf.body.push(ret(ref("value"))); + expect(analyzeWith(true).parameters.get("outer")).toBeUndefined(); + expect(analyzeWith(true).locals.get("outer")).toEqual(new Set()); +}); + +test("a pointer-wrapped binding needs a callee that keeps the narrowed payload in place", () => { + const wrapCell: IrExpr = { + kind: "unionWrap", + unionId: "optional", + tag: 0, + value: ref("cell", record), + type: optional, + loc, + }; + const outer = helper("outer", []); + outer.locals.push(local("cell", record)); + outer.body = [ + { + kind: "varDecl", + localId: "cell", + init: { kind: "recordLit", fields: [], type: record, loc }, + loc, + }, + ret(call("leaf", [wrapCell])), + ]; + const read = (value: IrExpr): IrExpr => ({ + kind: "recordGet", + obj: value, + shapeId: "cell", + field: "x", + type: F64, + loc, + }); + const keeps = (leaf: IrFunction, ...more: IrFunction[]): boolean => { + const result = analyzeCallLifetimes( + new Map([outer, leaf, ...more].map((fn) => [fn.name, fn])), + (className) => className === "Node", + undefined, + undefined, + undefined, + undefined, + () => true, + ); + return result.locals.get("outer")?.has("cell") !== true; + }; + const leafWith = (body: IrStmt[]): IrFunction => ({ ...helper("leaf"), body }); + // Tag tests and field reads of the narrowed payload leave it in place. + expect(keeps(leafWith([{ kind: "exprStmt", expr: tag("value"), loc }]))).toBe(false); + expect(keeps(leafWith([ret(read(narrow("value")))]))).toBe(false); + // So do field reads through a checked narrowing, which throws on the + // other arm (`value.x` of a `T | undefined` parameter). + const checked: IrExpr = { + kind: "ternary", + cond: tag("value"), + then: { kind: "libCall", fn: "error.nodeThrow", args: [], type: record, loc }, + else_: narrow("value"), + type: record, + loc, + }; + expect(keeps(leafWith([ret(read(checked))]))).toBe(false); + // Returning it keeps the payload. + const returnsChecked = leafWith([ret(checked)]); + returnsChecked.returnType = record; + expect(keeps(returnsChecked)).toBe(true); + // Returning the narrowed payload, or a narrowed nullish read, keeps it, + // although the union itself is only projected. + const returned = leafWith([ret(narrow("value"))]); + returned.returnType = record; + expect(analyze(returned).parameters.get("leaf")).toEqual(new Set([0])); + expect(keeps(returned)).toBe(true); + const defaulted = leafWith([ + ret({ kind: "nullish", left: ref("value"), right: ref("other", record), type: record, loc }), + ]); + defaulted.locals.push(local("other", record)); + expect(keeps(defaulted)).toBe(true); + // So does a nullable-pointer field store of the union. + const self: IrType = { kind: "object", className: "Node" }; + const stored = leafWith([ + { + kind: "fieldSet", + obj: ref("this", self), + className: "Node", + field: "next", + value: ref("value"), + loc, + }, + ]); + stored.params = [{ localId: "value", name: "value", type: optional }]; + stored.locals.push(local("this", self)); + expect(keeps(stored)).toBe(true); + // A narrowed payload passed on, or the union passed on, is in place when + // the next callee keeps it in place. + const inner = (body: IrStmt[], type: IrType = record): IrFunction => { + const fn = helper("inner"); + fn.params[0]!.type = fn.locals[0]!.type = type; + fn.body = body; + return fn; + }; + const passNarrowed = leafWith([ret(call("inner", [narrow("value")]))]); + expect(keeps(passNarrowed, inner([ret(read(ref("value", record)))]))).toBe(false); + const returnsRecord = inner([ret(ref("value", record))]); + returnsRecord.returnType = record; + expect(keeps(passNarrowed, returnsRecord)).toBe(true); + const passUnion = leafWith([ret(call("inner", [ref("value")]))]); + expect(keeps(passUnion, inner([ret(read(narrow("value")))], optional))).toBe(false); + const innerReturned = inner([ret(narrow("value"))], optional); + innerReturned.returnType = record; + expect(keeps(passUnion, innerReturned)).toBe(true); +}); diff --git a/packages/compiler/src/backend/llvm/call-lifetimes.ts b/packages/compiler/src/backend/llvm/call-lifetimes.ts index db5bdb574..becd24805 100644 --- a/packages/compiler/src/backend/llvm/call-lifetimes.ts +++ b/packages/compiler/src/backend/llvm/call-lifetimes.ts @@ -14,6 +14,11 @@ import { storedLastUseRead } from "./local-extents.js"; interface ForwardedUse { callee: string; index: number; + /** The binding reached the call inside a nullable-pointer union + * (PointerWrapTest): the callee's parameter holds the binding itself as + * its payload, so the callee must keep the payload, not only the union, + * in place. */ + wrapped: boolean; } interface Uses { @@ -21,6 +26,13 @@ interface Uses { written: Set; declarations: Map; forwards: Map; + /** Union bindings whose payload a use may keep: a narrowed read that is + * not projected or passed on, a narrowed nullish read, a store into a + * nullable-pointer field. The union itself can still be only projected. */ + payloadKept: Set; + /** Narrowed payloads of union bindings passed directly as call arguments: + * the payload stays in place if the callee's parameter is proved. */ + payloadForwards: Map; } interface Parameter { @@ -99,19 +111,55 @@ const NO_INLINE_FIELDS: InlineFieldTest = { record: () => false, classField: () * reads the record and keeps no reference to it. */ export type RecordDynCopyTest = (shapeId: string) => boolean; +/** Wraps into a nullable-pointer union whose value is the payload pointer + * itself (LlEmitter.borrowableNullableWrap): the wrapped binding reaches the + * callee's parameter unchanged. */ +export type PointerWrapTest = (wrap: IrExpr & { kind: "unionWrap" }) => boolean; + function collectUses( fn: IrFunction, nullableField: NullableFieldTest, registerReturn: boolean, inlineField: InlineFieldTest, recordDynCopy: RecordDynCopyTest, + pointerWrap: PointerWrapTest, ): Uses { const uses: Uses = { invalid: new Set(), written: new Set(), declarations: new Map(), forwards: new Map(), + payloadKept: new Set(), + payloadForwards: new Map(), }; + const forward = ( + table: Map, + localId: string, + use: ForwardedUse, + ): void => { + let forwards = table.get(localId); + if (!forwards) table.set(localId, (forwards = [])); + forwards.push(use); + }; + /** The union binding `x` whose payload `value` is: `unionNarrow(x)`, or a + * checked narrowing `unionIsTag(x) ? : unionNarrow(x)` (either + * order), whose other branch is visited as any use. */ + function narrowedBinding(value: IrExpr): string | null { + if (value.kind === "unionNarrow") + return value.value.kind === "varRef" ? value.value.localId : null; + if ( + value.kind !== "ternary" || + value.cond.kind !== "unionIsTag" || + value.cond.value.kind !== "varRef" + ) + return null; + const id = value.cond.value.localId; + if (value.else_.kind === "libCall" && narrowedBinding(value.then) === id) + return expr(value.else_) ? id : null; + if (value.then.kind === "libCall" && narrowedBinding(value.else_) === id) + return expr(value.then) ? id : null; + return null; + } const aliases: { alias: string; source: string }[] = []; const stringInput = (value: IrExpr): boolean => { if (value.kind === "varRef" && value.type.kind === "string") return true; @@ -145,6 +193,12 @@ function collectUses( if (borrowsMapReadInputs(node)) return expr(node.receiver) && node.args.every(stringInput); break; case "unionNarrow": + // The payload may escape; the union itself does not. + if (node.value.kind === "varRef") { + uses.payloadKept.add(node.value.localId); + return true; + } + break; case "unionIsTag": if (node.value.kind === "varRef") return true; break; @@ -161,12 +215,16 @@ function collectUses( node.left.kind === "varRef" && node.left.type.kind === "union" && node.type.kind !== "union" - ) + ) { + uses.payloadKept.add(node.left.localId); return expr(node.right); + } break; case "fieldGet": case "recordGet": - if (node.obj.kind === "varRef") return true; + // A field read of a binding, or of a union binding's narrowed + // payload, keeps neither. + if (node.obj.kind === "varRef" || narrowedBinding(node.obj) !== null) return true; break; case "dynFrom": // A copied record converts field by field; a live capsule keeps it. @@ -200,13 +258,19 @@ function collectUses( return node.args.every(expr); case "call": node.args.forEach((arg, index) => { - if (arg.kind !== "varRef") { + const narrowed = narrowedBinding(arg); + if (narrowed !== null) { + forward(uses.payloadForwards, narrowed, { callee: node.callee, index, wrapped: false }); + return; + } + const wrapped = + arg.kind === "unionWrap" && arg.value.kind === "varRef" && pointerWrap(arg); + const value = wrapped ? arg.value : arg; + if (value.kind !== "varRef") { expr(arg); return; } - let forwards = uses.forwards.get(arg.localId); - if (!forwards) uses.forwards.set(arg.localId, (forwards = [])); - forwards.push({ callee: node.callee, index }); + forward(uses.forwards, value.localId, { callee: node.callee, index, wrapped }); }); return true; case "assignExpr": @@ -250,8 +314,11 @@ function collectUses( node.value.kind === "varRef" && node.value.type.kind === "union" && nullableField(node.className, node.field) - ) + ) { + // The field retains the payload. + uses.payloadKept.add(node.value.localId); return expr(node.obj); + } const stored = fieldStore( node.obj, node.value, @@ -345,10 +412,14 @@ export function analyzeCallLifetimes( registerReturns: ReadonlySet = new Set(), inlineField: InlineFieldTest = NO_INLINE_FIELDS, recordDynCopy: RecordDynCopyTest = () => false, + pointerWrap: PointerWrapTest = () => false, ): CallLifetimes { const usesByFunction = new Map(); const lazyCaptures = new LazyCaptures(functions); const nodes = new Map(); + /** Union parameters that keep their payload in place too, which a + * pointer-wrapped argument needs (ForwardedUse.wrapped). */ + const payloadNodes = new Map(); const unsafe: Parameter[] = []; const consumed: { fn: string; index: number }[] = []; const result: CallLifetimes = { @@ -361,7 +432,7 @@ export function analyzeCallLifetimes( }; for (const fn of functions.values()) { if (fn.generator !== undefined && fn.classCaptures === undefined) { - const uses = collectUses(fn, nullableField, false, inlineField, recordDynCopy); + const uses = collectUses(fn, nullableField, false, inlineField, recordDynCopy, pointerWrap); const locals = new Map(fn.locals.map((local) => [local.id, local])); const stable = new Set(); for (const param of fn.params) { @@ -386,6 +457,7 @@ export function analyzeCallLifetimes( registerReturns.has(fn.name), inlineField, recordDynCopy, + pointerWrap, ); usesByFunction.set(fn.name, uses); // Class-capturing bodies get only local facts (computed below from @@ -460,23 +532,50 @@ export function analyzeCallLifetimes( return node; }), ); + payloadNodes.set( + fn.name, + fn.params.map((param) => { + if (param.type.kind !== "union") return undefined; + const node: Parameter = { safe: !uses.payloadKept.has(param.localId), callers: new Set() }; + if (!node.safe) unsafe.push(node); + return node; + }), + ); } + /** The fact a forwarded use needs from the callee's parameter. */ + const target = (use: ForwardedUse): Parameter | undefined => + use.wrapped ? payloadNodes.get(use.callee)?.[use.index] : nodes.get(use.callee)?.[use.index]; for (const fn of functions.values()) { const params = nodes.get(fn.name); const uses = usesByFunction.get(fn.name); if (!params || !uses) continue; - fn.params.forEach((param, index) => { - const node = params[index]!; + const depend = (node: Parameter, callees: (Parameter | undefined)[]): void => { if (!node.safe) return; - for (const use of uses.forwards.get(param.localId) ?? []) { - const callee = nodes.get(use.callee)?.[use.index]; + for (const callee of callees) { if (!callee) { node.safe = false; unsafe.push(node); - break; + return; } callee.callers.add(node); } + }; + fn.params.forEach((param, index) => { + const node = params[index]!; + const forwards = uses.forwards.get(param.localId) ?? []; + depend(node, forwards.map(target)); + const payload = payloadNodes.get(fn.name)?.[index]; + if (!payload) return; + // The payload stays in place when the union does, when every callee + // the union reaches keeps its payload too, and when every callee its + // narrowed payload reaches proves that parameter. + depend(payload, [ + node, + ...forwards.map((use) => payloadNodes.get(use.callee)?.[use.index]), + ...(uses.payloadForwards.get(param.localId) ?? []).map( + (use) => nodes.get(use.callee)?.[use.index], + ), + ]); }); } for (let i = 0; i < unsafe.length; i++) { @@ -508,7 +607,7 @@ export function analyzeCallLifetimes( ) continue; const forwards = uses.forwards.get(local.id) ?? []; - if (forwards.every((use) => result.parameters.get(use.callee)?.has(use.index) === true)) { + if (forwards.every((use) => target(use)?.safe === true)) { projected.add(local.id); if (eligible(local) && !uses.written.has(local.id)) safe.add(local.id); } diff --git a/packages/compiler/src/backend/llvm/checked-narrows.ts b/packages/compiler/src/backend/llvm/checked-narrows.ts index c1514df1e..5fbaf6d08 100644 --- a/packages/compiler/src/backend/llvm/checked-narrows.ts +++ b/packages/compiler/src/backend/llvm/checked-narrows.ts @@ -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 diff --git a/packages/compiler/src/backend/llvm/classes.test.ts b/packages/compiler/src/backend/llvm/classes.test.ts index bee954afb..e7ba72784 100644 --- a/packages/compiler/src/backend/llvm/classes.test.ts +++ b/packages/compiler/src/backend/llvm/classes.test.ts @@ -77,6 +77,69 @@ test("leaf layouts keep direct teardown while recursive layouts share the depth } }); +test("teardowns of types with children of their own type free their blocks as one segment", () => { + const linked = { kind: "object" as const, className: "Linked" }; + const mod: IrModule = { + irVersion: 17, + sourceFile: loc.file, + entry: "main", + functions: [{ name: "main", params: [], returnType: VOID, locals: [], body: [], loc }], + classes: [ + { + name: "Linked", + // The array field keeps the recursive teardown: a type whose every + // reference field holds its own type tears down in one loop + // (iterative-teardown.test.ts). + fields: [ + { name: "next", type: linked }, + { name: "other", type: linked }, + { name: "tags", type: { kind: "array", elem: { kind: "string" } } }, + ], + methods: [], + loc, + }, + ], + records: [{ id: "branch", fields: [{ name: "next", type: linked }] }], + }; + const define = (llvm: string, name: string): string => + new RegExp(`^define internal [^@]*@${name}\\([^]*?^}`, "m").exec(llvm)?.[0] ?? ""; + const linkedDestroy = `${mangleClassRelease("Linked")}_destroy`; + const branchDestroy = `${mangleRecordRelease("branch")}_destroy`; + for (const workers of [false, true]) { + const llvm = emitLlvmModule({ ...mod, workers }, { pointerBits: 64 }); + // Linked's teardown keeps its block and its children's in a segment: it + // releases its own block first and places it by address against its + // first child, a child torn down directly appends to the segment, one + // past the direct chain appends through the runtime, and the chain's + // first teardown frees the segment onto the free list in one step. + const seg = define(llvm, `${linkedDestroy}_s`); + expect(seg.startsWith("define internal ptr"), "segment form").toBe(true); + expect(seg).toMatch( + /store ptr %(?:sf\.blk|o), ptr %sg\.own[^]*call ptr @sc_release_Linked_destroy_s\(/, + ); + // The children's releases have a copy for each place of the own block. + for (const copy of ["ta", "tb"]) + expect(seg).toContain( + `call ptr @scr_rc_destroy_seg(ptr %v0, ptr @${linkedDestroy}_sc, ptr @${linkedDestroy}, ptr %${copy}0.bt)`, + ); + expect(seg).not.toContain("call void @scr_rc_destroy("); + expect(seg).toContain("icmp ult ptr %sg.o, %sg.r1"); + const first = define(llvm, `${linkedDestroy}_d`); + expect(first).toContain(`call ptr @${linkedDestroy}_s(ptr %o, i32 %d, ptr %head)`); + expect(first).toContain("store ptr %first, ptr %sgf.fp"); + expect(first.includes("llvm.threadlocal.address"), "thread-local state").toBe(workers); + expect(define(llvm, linkedDestroy)).toContain(`call void @${linkedDestroy}_d(ptr %o, i32 0)`); + // A record whose child is another type frees its own block as before. + expect(define(llvm, `${branchDestroy}_s`)).toBe(""); + expect(define(llvm, branchDestroy)).toContain(`call void @${mangleClassRelease("Linked")}(`); + } + // Without the inline allocation paths (32-bit targets) a teardown frees + // through the runtime, block by block. + const llvm32 = emitLlvmModule(mod, { pointerBits: 32 }); + expect(define(llvm32, `${linkedDestroy}_s`)).toBe(""); + expect(define(llvm32, `${linkedDestroy}_d`)).toContain(`call void @${linkedDestroy}_d(`); +}); + test("releases split into an inlinable fast path and an out-of-line remainder", () => { // An array of the class itself makes the hierarchy cycle-capable. const kids = { kind: "array" as const, elem: { kind: "object" as const, className: "Node" } }; diff --git a/packages/compiler/src/backend/llvm/classes.ts b/packages/compiler/src/backend/llvm/classes.ts index 8f179828d..5941f5d5c 100644 --- a/packages/compiler/src/backend/llvm/classes.ts +++ b/packages/compiler/src/backend/llvm/classes.ts @@ -43,6 +43,10 @@ import { destroyDefinitions, directChildReleaseLines, directDestroyTarget, + segmentPrologueLines, + segmentTeardownLines, + iterativeTeardownDefinitions, + segmentedTeardown, usedDirectTargets, type DirectDestroy, type RegionHooks, @@ -55,6 +59,7 @@ import { traceAdapter, type ShapeHost, youngEntryLines, + arrElemInit, } from "./shapes.js"; /** One virtual method slot of a hierarchy: the ROOT-MOST declaring class @@ -368,11 +373,34 @@ function runtimePrefixOf(meta: LlClassMeta): RuntimePrefix | null { * on classes rooted at a runtime base, then the stream-state slot on * stream-rooted ones. */ function headerEnd(meta: LlClassMeta): number { + if (meta.def.arrayElem !== undefined) return ARRAY_PREFIX.length + 1; if (!meta.hierarchy || meta.idInRc) return 1; if (streamRooted(meta)) return 5; return runtimePrefixOf(meta) ? 4 : 2; } +/** The array header a class declared `extends Array` begins with, after + * the size_t reference count: %ScrArr's len, cap, elem, cyc, spare, filled + * and data (scr_array.h). */ +const ARRAY_PREFIX = ["i32", "i32", "i8", "i8", "i16", "i32", "ptr"]; + +/** LLVM `c"..."` text of a string's UTF-8 bytes and a terminating NUL. */ +function nulTerminatedBytes(text: string): string { + let out = ""; + for (const byte of Buffer.from(text, "utf8")) + out += + byte >= 0x20 && byte < 0x7f && byte !== 0x22 && byte !== 0x5c + ? String.fromCharCode(byte) + : `\\${byte.toString(16).padStart(2, "0").toUpperCase()}`; + return out + "\\00"; +} + +/** The class descriptor (ScrArrClass: name, destroy) of a class declared + * `extends Array`. */ +export function arrayClassDescriptor(className: string): string { + return `@${mangleClassStruct(className)}.arrcls`; +} + /** The GEP index of the ThreadSlots slot word (an i32) in a class whose * hierarchy holds ThreadSlots keys: right after the header words, so every * member of the hierarchy has it at the same offset. */ @@ -470,7 +498,11 @@ export interface ClassHost extends ShapeHost { * hierarchy folds away. The cycle analysis leaves the hierarchy untraced, so * no trace adapter ever visits an instance (as for every untraced class). */ export function programLifetimeClass(host: ClassHost, meta: LlClassMeta): boolean { - return host.programLifetime?.has(meta.root.def.name) === true && meta.root.def.runtime !== true; + return ( + host.programLifetime?.has(meta.root.def.name) === true && + meta.root.def.runtime !== true && + meta.root.def.arrayElem === undefined + ); } /** The reference count a program-lifetime instance is created with: the @@ -570,13 +602,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(); } @@ -722,7 +762,12 @@ export function assignCompactLayouts( ): void { const isCompact = (className: string): boolean => { const target = metaMap.get(className); - return target !== undefined && target.def.runtime !== true && compact(target.root); + return ( + target !== undefined && + target.def.runtime !== true && + target.root.def.arrayElem === undefined && + compact(target.root) + ); }; const byPre = [...metaMap.values()].sort((a, b) => a.pre - b.pre); for (const meta of byPre) { @@ -897,13 +942,15 @@ export function emitClassShapes( // usual pointer reinterprets. const runtimePrefix = runtimePrefixOf(meta); const prefix = - meta.hierarchy && !meta.idInRc - ? streamRooted(meta) - ? ["ptr", "ptr", "ptr", "ptr"] - : runtimePrefix - ? ["ptr", "ptr", "ptr"] - : ["ptr"] - : []; + cls.arrayElem !== undefined + ? ARRAY_PREFIX + : meta.hierarchy && !meta.idInRc + ? streamRooted(meta) + ? ["ptr", "ptr", "ptr", "ptr"] + : runtimePrefix + ? ["ptr", "ptr", "ptr"] + : ["ptr"] + : []; const members = [...prefix, ...(meta.threadSlot ? ["i32"] : []), ...fieldTys]; typeDefs.push( `%${mangleClassStruct(cls.name)} = type { ${host.sizeType}${members.length ? ", " + members.join(", ") : ""} } ` + @@ -990,6 +1037,7 @@ export function emitClassShapes( const types = ownedFields(cls).map(({ field }) => storedFieldType(cls, field)); for (const t of types) fieldTypes.push(t); if (programLifetimeClass(host, meta)) continue; + if (cls.arrayElem !== undefined) continue; if (meta.hierarchy && (meta.children.length !== 0 || meta.root.def.runtime === true)) continue; if (!types.some((t) => isRefCounted(t) && needsBoundedRelease(t))) continue; const release = mangleClassRelease(cls.name); @@ -1006,6 +1054,12 @@ export function emitClassShapes( } const directTargets = usedDirectTargets(host, candidates, fieldTypes); + // The descriptor entries of a class declared `extends Array`: a helper + // the module defines, or null (a class that is never constructed has + // neither). + const defined = new Set(mod.functions.map((fn) => fn.name)); + const arrayClassFn = (name: string | undefined): string => + name !== undefined && defined.has(name) ? `@${mangleFunction(name)}` : "null"; for (const cls of emitted) { const meta = metaMap.get(cls.name)!; const struct = mangleClassStruct(cls.name); @@ -1082,25 +1136,68 @@ export function emitClassShapes( bounded && (directTargets.has(`object:${cls.name}`) || refFields.some((f) => directDestroyTarget(host, f.type, directTargets) !== null)); + // The teardown's own symbol, and whether it keeps a segment (shapes.ts + // segmentedTeardown). + const destroy = `${meta.hierarchy ? mangleClassReleaseDirect(cls.name) : mangleClassRelease(cls.name)}_destroy`; + const segmented = + chained && + cls.arrayElem === undefined && + !meta.compact && + runtimePrefix === null && + !isStreamRooted && + !meta.threadSlot && + segmentedTeardown( + host, + destroy, + refFields.map((f) => f.type), + directTargets, + ); // The field-releasing teardown body shared by both release shapes // (the public one on standalone classes, the DIRECT one on hierarchy // members). Runs at rc == 0. const teardown = (lines: string[]): void => { - refFields.forEach((f, t) => { - lines.push( - ` %f${t} = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 ${f.index}`, - ` %v${t} = load ptr, ptr %f${t}`, - ); - const direct = bounded ? directDestroyTarget(host, f.type, directTargets) : null; - if (direct) - lines.push( - ...directChildReleaseLines(host, `td${t}`, `%v${t}`, direct, llvmCommentText(f.name)), - ); - else - lines.push( + const release = (prefix: string, f: (typeof refFields)[number], t: number): string[] => { + // An Array subclass instance's destroy releases its fields plainly + // (it takes no chain length). + const direct = + bounded && cls.arrayElem === undefined + ? directDestroyTarget(host, f.type, directTargets) + : null; + if (direct === null) + return [ ` call void ${releaseSym(host, f.type)}(ptr %v${t}) ; ${llvmCommentText(f.name)}`, - ); - }); + ]; + const own = segmented && direct.destroy === destroy; + return directChildReleaseLines( + host, + `${prefix}${t}`, + `%v${t}`, + direct, + llvmCommentText(f.name), + own, + ); + }; + const loads = refFields.map((f, t) => [ + ` %f${t} = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 ${f.index}`, + ` %v${t} = load ptr, ptr %f${t}`, + ]); + if (segmented) { + // A segmented teardown loads its fields, releases its own block, and + // then releases the fields (shapes.ts segmentTeardownLines). + const ownType = refFields.flatMap((f, t) => + directDestroyTarget(host, f.type, directTargets)?.destroy === destroy ? [`%v${t}`] : [], + ); + lines.push( + ...segmentPrologueLines(), + ...loads.flat(), + ...emitObjectFree(host, traced, "weak", "%sg.own"), + ...segmentTeardownLines(ownType, (prefix) => + refFields.flatMap((f, t) => release(prefix, f, t)), + ), + ); + return; + } + refFields.forEach((f, t) => lines.push(...loads[t]!, ...release("td", f, t))); if (runtimePrefix) lines.push(...regCall("td", runtimePrefix.drop, "")); if (isStreamRooted) lines.push(...stCall("tds", "scr_stream_st_release", "")); if (meta.threadSlot) lines.push(...slotForgetLines(host, struct, meta, "tdw")); @@ -1182,10 +1279,12 @@ export function emitClassShapes( ]; if (traced) reld.push(...cycleDeadLines(host, "teardown")); if (bounded) { - const destroy = `${mangleClassReleaseDirect(cls.name)}_destroy`; const destroyBody: string[] = []; teardown(destroyBody); - defs.push(...destroyDefinitions(destroy, destroyBody, chained), ``); + defs.push( + ...destroyDefinitions(destroy, destroyBody, chained, segmented ? host : null), + ``, + ); host.declare(`declare void @scr_rc_destroy(ptr, ptr)`); reld.push(` call void @scr_rc_destroy(ptr %o, ptr @${destroy})`); } else teardown(reld); @@ -1194,9 +1293,57 @@ export function emitClassShapes( reld.push(`done:`, ` ret void`, `}`, ``); defs.push(...reld); } + } else if (cls.arrayElem !== undefined) { + // An instance is an array: its release is the array's, which reaches + // the class's destroy (the descriptor's) when the count reaches zero. + host.declare(`declare void @scr_arr_release(ptr)`); + host.declare(`declare void @scr_arr_subclass_drop(ptr)`); + const destroy = `${mangleClassRelease(cls.name)}.arrdestroy`; + const destroyBody: string[] = [ + `define internal void @${destroy}(ptr %o) ${FN_ATTRS} { ; destroy ${cls.name} (an Array subclass instance)`, + `entry:`, + ]; + const fieldsTeardown: string[] = []; + teardown(fieldsTeardown); + const free = emitObjectFree(host, traced); + destroyBody.push( + ...fieldsTeardown.slice(0, fieldsTeardown.length - free.length), + ` call void @scr_arr_subclass_drop(ptr %o)`, + ...free, + ` ret void`, + `}`, + ``, + ); + const displayName = cls.name.includes(".") + ? cls.name.slice(cls.name.lastIndexOf(".") + 1) + : cls.name; + const jsName = cls.jsName ?? displayName; + defs.push( + ...destroyBody, + // The name is the class's own copy: its address identifies the + // class in dyn snapshots of an instance (scr_arr_copy_metadata). + `${arrayClassDescriptor(cls.name)}.name = private constant [${Buffer.byteLength(jsName, "utf8") + 1} x i8] c"${nulTerminatedBytes(jsName)}"`, + `${arrayClassDescriptor(cls.name)} = internal constant { ptr, ptr, ptr, ptr } { ptr ${arrayClassDescriptor(cls.name)}.name, ptr @${destroy}, ptr ${arrayClassFn(cls.arrayFields)}, ptr ${arrayClassFn(cls.arraySpecies)} }`, + ``, + `define internal void @${mangleClassRelease(cls.name)}(ptr %o) ${FN_ATTRS} { ; release ${cls.name} (the array's)`, + `entry:`, + ` call void @scr_arr_release(ptr %o)`, + ` ret void`, + `}`, + ``, + ); } else { const freeBody: string[] = []; teardown(freeBody); + // A segmented class whose every reference field holds its own type and + // that has no collector header tears down in one loop (shapes.ts + // iterativeTeardownDefinitions). + const iterative = + segmented && + !traced && + refFields.every( + (f) => directDestroyTarget(host, f.type, directTargets)?.destroy === destroy, + ); defs.push( ...releaseBody( host, @@ -1207,6 +1354,19 @@ export function emitClassShapes( bounded, lifetime, chained, + segmented, + false, + iterative + ? iterativeTeardownDefinitions( + host, + destroy, + struct, + refFields.map((f) => f.index), + `@${mangleClassRelease(cls.name)}`, + "weak", + count, + ) + : null, ), ``, ); @@ -1280,6 +1440,13 @@ export function emitClassShapes( nw.push( ` store ${host.sizeType} ${lifetime ? programLifetimeRc(host.sizeType) : 1}, ptr %o`, ); + if (cls.arrayElem !== undefined) { + const init = arrElemInit(host, cls.arrayElem); + host.declare(`declare void @scr_arr_subclass_init(ptr, i8, ptr, ptr, i1 zeroext)`); + nw.push( + ` call void @scr_arr_subclass_init(ptr %o, i8 ${init.kind}, ptr ${init.ops}, ptr ${arrayClassDescriptor(cls.name)}, i1 ${traced})`, + ); + } if (lifetime && audit) { // --sanitize builds: the RC audit skips its leak report when the // program keeps program-lifetime objects (scr_console.c), and @@ -1316,7 +1483,12 @@ export function emitClassShapes( // Young instances: program-owned traced classes. Classes rooted at a // runtime base (errors, emitters, events, streams) carry runtime state // and stay with trial deletion, like every runtime family. - if (traced && !lifetime && (!meta.hierarchy || meta.root.def.runtime !== true)) + if ( + traced && + !lifetime && + (!meta.hierarchy || meta.root.def.runtime !== true) && + cls.arrayElem === undefined + ) nw.push(...youngEntryLines(host)); nw.push(` ret ptr ${meta.hierarchy ? "%o.vt" : "%o"}`, `}`, ``); defs.push(...nw); @@ -1339,6 +1511,12 @@ export function emitClassShapes( tracedFields.forEach((f, i) => { tr.push(...traceFieldVisitLines(struct, f.index, i, llvmCommentText(f.name))); }); + // An Array subclass instance's elements, when its current element + // storage is cycle-capable (a species result can retype it). + if (cls.arrayElem !== undefined) { + host.declare(`declare void @scr_arr_subclass_trace(ptr, ptr, ptr)`); + tr.push(` call void @scr_arr_subclass_trace(ptr %o, ptr %visit, ptr %ctx) ; the elements`); + } tr.push(` ret void`, `}`, ``); defs.push(...tr); @@ -1356,6 +1534,12 @@ export function emitClassShapes( ); }); if (meta.threadSlot) gf.push(...slotForgetLines(host, struct, meta, "gfw")); + if (cls.arrayElem !== undefined) { + // The elements: traced ones are the collector's, the others are + // released with the storage (the same test as the trace's). + host.declare(`declare void @scr_arr_subclass_gc_drop(ptr)`); + gf.push(` call void @scr_arr_subclass_gc_drop(ptr %o)`); + } gf.push(...emitObjectFree(host, true), ` ret void`, `}`, ``); defs.push(...gf); } @@ -1367,18 +1551,18 @@ export function emitClassShapes( * statics plus their construct thunks — allocate, run the constructor over * a +1 `this`, and hand the remaining +1 out. No pending check is needed: * in-tier constructors are throw-free by construction. - * `classObjs` maps className → { nameSym } registered during body emission - * (the literal interned while the table was open). */ + * `classObjs` maps className → { nameSym, baseNameSym } registered during + * body emission (the literals interned while the table was open). */ export function emitClassObjDefs( host: ClassHost, metaMap: Map, - classObjs: Map, + classObjs: Map, fnByName: Map, llType: (t: IrType) => string, threadLocal = false, ): string[] { const out: string[] = []; - for (const [className, { nameSym }] of classObjs) { + for (const [className, { nameSym, baseNameSym }] of classObjs) { const meta = metaMap.get(className); if (!meta) throw new InternalCompilerError( @@ -1404,7 +1588,7 @@ export function emitClassObjDefs( const ctorArgs = params.map((p, i) => `${llType(p.type)} %a${i}`); const classObj = `@${mangleClassObj(className)} = internal ${threadLocal ? "thread_local " : ""}global %ScrClassObj ` + - `{ ${host.sizeType} -1, ${host.sizeType} ${intervalMeta.pre}, ${host.sizeType} ${intervalMeta.post}, ptr @${mangleCtorThunk(className)}, ptr ${nameSym}, ${host.sizeType} 0, ${host.sizeType} ${meta.def.jsLength ?? (meta.def.runtime ? 1 : 0)}, ptr null, ptr null } ; class ${className}`; + `{ ${host.sizeType} -1, ${host.sizeType} ${intervalMeta.pre}, ${host.sizeType} ${intervalMeta.post}, ptr @${mangleCtorThunk(className)}, ptr ${nameSym}, ${host.sizeType} 0, ${host.sizeType} ${meta.def.jsLength ?? (meta.def.runtime ? 1 : 0)}, ptr null, ptr null, ptr ${baseNameSym ?? "null"}, ${host.sizeType} ${meta.def.ownStaticFields ? 1 : 0} } ; class ${className}`; if (meta.def.runtime) { // A runtime-provided class: %C.construct allocates through the // runtime and returns the instance. diff --git a/packages/compiler/src/backend/llvm/common.ts b/packages/compiler/src/backend/llvm/common.ts index ac4e28708..f1bdeb557 100644 --- a/packages/compiler/src/backend/llvm/common.ts +++ b/packages/compiler/src/backend/llvm/common.ts @@ -29,6 +29,9 @@ export const SCR_ARR_FIELD = { * (SCR_ARR_STATES in scr_array.h). */ export const SCR_ARR_STATES = 16; +/** The `spare` flag of a frozen %ScrArr (SCR_ARR_FROZEN in scr_array.h). */ +export const SCR_ARR_FROZEN = 32; + /** Loads the i32 `len` or `cap` of a %ScrArr at `ptr` into `dest` as a * size-type count. */ export function loadArrCount( @@ -274,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. */ diff --git a/packages/compiler/src/backend/llvm/constant-callbacks.test.ts b/packages/compiler/src/backend/llvm/constant-callbacks.test.ts index e8ca5571f..8bdab0408 100644 --- a/packages/compiler/src/backend/llvm/constant-callbacks.test.ts +++ b/packages/compiler/src/backend/llvm/constant-callbacks.test.ts @@ -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", diff --git a/packages/compiler/src/backend/llvm/constant-choices.test.ts b/packages/compiler/src/backend/llvm/constant-choices.test.ts new file mode 100644 index 000000000..f19b5b19d --- /dev/null +++ b/packages/compiler/src/backend/llvm/constant-choices.test.ts @@ -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(); +}); diff --git a/packages/compiler/src/backend/llvm/constant-choices.ts b/packages/compiler/src/backend/llvm/constant-choices.ts new file mode 100644 index 000000000..6e7bb4c84 --- /dev/null +++ b/packages/compiler/src/backend/llvm/constant-choices.ts @@ -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; + +/** 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(); + 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] ?? "") : "", + ); +} diff --git a/packages/compiler/src/backend/llvm/counted-loops.ts b/packages/compiler/src/backend/llvm/counted-loops.ts index ca2dd6b92..88af25e47 100644 --- a/packages/compiler/src/backend/llvm/counted-loops.ts +++ b/packages/compiler/src/backend/llvm/counted-loops.ts @@ -5,8 +5,11 @@ import type { } from "../../ir/integer-loops.js"; import { FOLLOWER_GUARD_LIMIT } from "../../ir/integer-loops.js"; import type { IrExpr } from "../../ir/ir.js"; -import type { LlvmEmitterContext } from "./expr-context.js"; +import type { IntegerRange } from "../../ir/integer-ranges.js"; +import type { LlValue, LlvmEmitterContext } from "./expr-context.js"; import { f64Lit } from "./common.js"; +import { exactInteger, widenInteger } from "./integer-values.js"; +import { integerOrNaNRange } from "./wrapping-sums.js"; /** The caller has proved or guarded the direction's finite endpoint. Clamp * the opposite infinity before integer conversion. Fractional limits become @@ -50,8 +53,8 @@ export function emitCountedLoopLimit( } /** The run-time test of a guarded counted loop: an unproven limit is within - * the integer form's finite range and an unproven start is an exact safe - * integer. Returns `true` when nothing needs testing. */ + * the integer form's finite range and an unproven start is an exact integer + * within the loop's start bound. Returns `true` when nothing needs testing. */ export function emitCountedLoopSafety( host: LlvmEmitterContext, loop: IntegerCountedForLoop, @@ -65,7 +68,11 @@ export function emitCountedLoopSafety( `${safe} = fcmp ${loop.step > 0 ? "ole" : "oge"} double ${bound.name}, ${f64Lit(loop.guardLimit)}`, ); } - if (loop.startGuarded) safe = andSafe(host, safe, emitExactStart(host, loop.start)); + if (loop.startGuarded) { + const start = host.emitExpr(loop.start); + const bound = loop.startBound; + safe = andSafe(host, safe, emitValueIntegerTest(host, start, loop.start, -bound, bound).ok); + } for (const follower of loop.followers) { if (follower.startGuarded) safe = andSafe(host, safe, emitExactStart(host, follower.start)); if (follower.tripGuarded) safe = andSafe(host, safe, emitFollowerReach(host, loop, follower)); @@ -117,6 +124,7 @@ export function emitPairedLoopSafety(host: LlvmEmitterContext, loop: IntegerPair } function andSafe(host: LlvmEmitterContext, safe: string, ok: string): string { + if (ok === "true") return safe; if (safe === "true") return ok; const both = host.B.tmp(); host.B.line(`${both} = and i1 ${safe}, ${ok}`); @@ -125,10 +133,30 @@ function andSafe(host: LlvmEmitterContext, safe: string, ok: string): string { /** An exact integer of at most 2^53 - 1 in magnitude, and not -0 (the bit * patterns of the value and its round trip agree). NaN and infinities fail - * the ordered range tests. */ + * the ordered range tests. A start with an integer form (an int32 local, + * `p0 - 1` of one) is an exact integer already: its range answers the test, + * or two integer compares do. */ function emitExactStart(host: LlvmEmitterContext, expr: IrExpr): string { const B = host.B; const start = host.emitExpr(expr); + const integer = exactInteger(host, start, expr); + if (!integer) { + const limit = Number.MAX_SAFE_INTEGER; + const tested = emitNanIntegerTest(host, start, expr, -limit, limit); + if (tested) return tested.ok; + } + if (integer) { + const limit = Number.MAX_SAFE_INTEGER; + if (integer.range.min >= -limit && integer.range.max <= limit) return "true"; + const wide = widenInteger(host, integer), + low = B.tmp(), + high = B.tmp(), + ok = B.tmp(); + B.line(`${low} = icmp sge i64 ${wide}, ${-limit}`); + B.line(`${high} = icmp sle i64 ${wide}, ${limit}`); + B.line(`${ok} = and i1 ${low}, ${high}`); + return ok; + } const low = B.tmp(), high = B.tmp(), raw = B.tmp(), @@ -151,3 +179,118 @@ function emitExactStart(host: LlvmEmitterContext, expr: IrExpr): string { B.line(`${ok} = and i1 ${inRange}, ${exact}`); return ok; } + +/** Tests that a double is an exact integer within [min, max] (not -0; NaN + * fails the ordered comparisons) and gives its i64. The integer is frozen, + * so it is a fixed value even when the test fails. */ +export function emitExactIntegerTest( + host: LlvmEmitterContext, + value: string, + min: number, + max: number, +): { ok: string; integer: string } { + const B = host.B; + const low = B.tmp(), + high = B.tmp(), + inRange = B.tmp(), + raw = B.tmp(), + integer = B.tmp(), + back = B.tmp(), + bits = B.tmp(), + backBits = B.tmp(), + exact = B.tmp(), + ok = B.tmp(); + B.line(`${low} = fcmp oge double ${value}, ${f64Lit(min)}`); + B.line(`${high} = fcmp ole double ${value}, ${f64Lit(max)}`); + B.line(`${inRange} = and i1 ${low}, ${high}`); + B.line(`${raw} = fptosi double ${value} to i64`); + B.line(`${integer} = freeze i64 ${raw}`); + B.line(`${back} = sitofp i64 ${integer} to double`); + B.line(`${bits} = bitcast double ${value} to i64`); + B.line(`${backBits} = bitcast double ${back} to i64`); + B.line(`${exact} = icmp eq i64 ${bits}, ${backBits}`); + B.line(`${ok} = and i1 ${inRange}, ${exact}`); + return { ok, integer }; +} + +/** The i64 start of an integer counter: the integer form the start value + * carries (an int32 local, `p0 - 1` of one), or the double's conversion, + * which the loop's guard or proof makes exact. */ +export function emitCountedStart(host: LlvmEmitterContext, expr: IrExpr): string { + const value = host.emitExpr(expr); + const integer = exactInteger(host, value, expr); + if (integer) return widenInteger(host, integer); + // The guard's test of an exact integer or NaN required the ordered bit. + if (value.nanInteger !== undefined && integerOrNaNRange(host, expr)) return value.nanInteger; + const start = host.B.tmp(); + host.B.line(`${start} = fptosi double ${value.name} to i64`); + return start; +} + +/** emitExactIntegerTest of a start expression: when the value carries an + * integer form (an exact integer, never -0), its range answers the test or + * two integer compares do, and the integer is the form's. */ +export function emitStartIntegerTest( + host: LlvmEmitterContext, + expr: IrExpr, + min: number, + max: number, +): { ok: string; integer: string } { + return emitValueIntegerTest(host, host.emitExpr(expr), expr, min, max); +} + +/** emitExactIntegerTest of a value with the integer form it carries: an + * exact integer, whose range answers the test or two integer compares do; + * or the integer of an exact integer or NaN (`nanInteger`), which the test + * takes when the ordered bit is set and the range or compares agree. Any + * other value takes the double's test. */ +export function emitValueIntegerTest( + host: LlvmEmitterContext, + value: LlValue, + expr: IrExpr | undefined, + min: number, + max: number, +): { ok: string; integer: string } { + const integer = exactInteger(host, value, expr); + if (integer) return integerRangeTest(host, widenInteger(host, integer), integer.range, min, max); + return ( + emitNanIntegerTest(host, value, expr, min, max) ?? + emitExactIntegerTest(host, value.name, min, max) + ); +} + +/** The test of a value's `nanInteger`: its ordered bit, and the range or two + * integer compares. Null when the value carries none. */ +function emitNanIntegerTest( + host: LlvmEmitterContext, + value: LlValue, + expr: IrExpr | undefined, + min: number, + max: number, +): { ok: string; integer: string } | null { + if (value.nanInteger === undefined || value.ordered === undefined || !expr) return null; + const range = integerOrNaNRange(host, expr); + if (!range) return null; + const tested = integerRangeTest(host, value.nanInteger, range, min, max); + return { ok: andSafe(host, value.ordered, tested.ok), integer: value.nanInteger }; +} + +/** An integer within [min, max]: true when its range is, two compares + * otherwise. */ +function integerRangeTest( + host: LlvmEmitterContext, + wide: string, + range: IntegerRange, + min: number, + max: number, +): { ok: string; integer: string } { + if (range.min >= min && range.max <= max) return { ok: "true", integer: wide }; + const B = host.B; + const low = B.tmp(), + high = B.tmp(), + ok = B.tmp(); + B.line(`${low} = icmp sge i64 ${wide}, ${min}`); + B.line(`${high} = icmp sle i64 ${wide}, ${max}`); + B.line(`${ok} = and i1 ${low}, ${high}`); + return { ok, integer: wide }; +} diff --git a/packages/compiler/src/backend/llvm/cpu-dispatch.ts b/packages/compiler/src/backend/llvm/cpu-dispatch.ts new file mode 100644 index 000000000..f24fc8674 --- /dev/null +++ b/packages/compiler/src/backend/llvm/cpu-dispatch.ts @@ -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 = 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(); + 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, + retargeted: (body: string) => string, +): Set { + const named = new Set(); + 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; +} diff --git a/packages/compiler/src/backend/llvm/debug-info.ts b/packages/compiler/src/backend/llvm/debug-info.ts index 4e37ee5bc..aaa35d666 100644 --- a/packages/compiler/src/backend/llvm/debug-info.ts +++ b/packages/compiler/src/backend/llvm/debug-info.ts @@ -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(); private readonly locations = new Map(); private readonly scopes = new Map(); @@ -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, + ): 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); @@ -139,7 +213,6 @@ export class LlvmDebugInfo { let id: string; switch (type.kind) { case "f64": - case "date": case "procStream": id = this.add( `!DIBasicType(name: ${quoted(type.kind === "f64" ? "number" : type.kind)}, size: 64, encoding: DW_ATE_float)`, @@ -190,7 +263,7 @@ export class LlvmDebugInfo { ? [] : [ this.add( - `!DIDerivedType(tag: DW_TAG_member, name: "arm${i}", baseType: ${this.type(arm)}, size: ${arm.kind === "bool" ? 8 : arm.kind === "f64" || arm.kind === "date" ? 64 : this.pointerBits})`, + `!DIDerivedType(tag: DW_TAG_member, name: "arm${i}", baseType: ${this.type(arm)}, size: ${arm.kind === "bool" ? 8 : arm.kind === "f64" ? 64 : this.pointerBits})`, ), ], ); @@ -231,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); @@ -239,9 +332,7 @@ export class LlvmDebugInfo { // Scalar forward captures use an array-backed TDZ box. Expose that // exceptional representation opaquely rather than reporting a bad value. const opaqueBox = - local.boxed && - local.tdz && - (local.type.kind === "f64" || local.type.kind === "bool" || local.type.kind === "date"); + local.boxed && local.tdz && (local.type.kind === "f64" || local.type.kind === "bool"); const type = opaqueBox ? this.pointer( this.add( @@ -282,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; diff --git a/packages/compiler/src/backend/llvm/dense-array-access.ts b/packages/compiler/src/backend/llvm/dense-array-access.ts index 95612e57c..e002ecd8a 100644 --- a/packages/compiler/src/backend/llvm/dense-array-access.ts +++ b/packages/compiler/src/backend/llvm/dense-array-access.ts @@ -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) { @@ -505,8 +509,12 @@ export function emitDenseArrayState( indexExpr: IrExpr | undefined, kind: "state" | "has", width: number, + /** The index is a dense array loop counter its entry test proved below + * the hole-free prefix (provenDenseIndex): the slot is a present value. */ + proven = false, ): string { const B = host.B; + if (proven) return kind === "state" ? f64Lit(SCR_ARR_VALUE) : "true"; const fn = kind === "state" ? "scr_arr_state" : "scr_arr_has"; const ty = kind === "state" ? "double" : "i1"; host.declare(`declare ${kind === "state" ? "double" : "zeroext i1"} @${fn}(ptr, double)`); @@ -606,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"), diff --git a/packages/compiler/src/backend/llvm/dense-array-loops.test.ts b/packages/compiler/src/backend/llvm/dense-array-loops.test.ts index f7a4bc892..9ec2d5d39 100644 --- a/packages/compiler/src/backend/llvm/dense-array-loops.test.ts +++ b/packages/compiler/src/backend/llvm/dense-array-loops.test.ts @@ -1,9 +1,10 @@ import { expect, test } from "vitest"; import { BOOL, F64, arrayOf, type IrExpr, type IrLocal, type IrStmt } from "../../ir/ir.js"; -import type { IntegerCountedForLoop } from "../../ir/integer-loops.js"; +import type { IntegerCountedForLoop, IntegerPairedForLoop } from "../../ir/integer-loops.js"; import { countedDenseShape, matchDenseArrayLoop, + pairedDenseShape, type DenseLoopShape, } from "./dense-array-loops.js"; @@ -42,6 +43,8 @@ const locals = new Map([ ["a", { id: "a", name: "a", type: ARR, mutable: false }], ["b", { id: "b", name: "b", type: ARR, mutable: true }], ["c", { id: "c", name: "c", type: ARR, mutable: false }], + ["j", { id: "j", name: "j", type: F64, mutable: true }], + ["t", { id: "t", name: "t", type: F64, mutable: false }], ]); /** for (let i = 0; i < a.length; i++) body */ @@ -119,6 +122,7 @@ const counted = ( guarded: false, limitGuarded: false, startGuarded: false, + startBound: Number.MAX_SAFE_INTEGER, guardLimit: 0, followers: [], ...fields, @@ -193,3 +197,100 @@ test("a loop to another invariant limit covers each array whose length it keeps" expect([...matchLimited(resizing, new Set(["b"]))!.arrays.keys()]).toEqual(["a"]); expect(matchLimited(resizing)).toBeNull(); }); + +test("a two-counter loop covers the arrays indexed at either counter", () => { + // for (let i = 1, j = k - 1; i < j; i++, j--) { t = a[i]; a[i] = a[j]; a[j] = t; } + const paired: IntegerPairedForLoop = { + low: { localId: "i", start: num(1), step: 1, startGuarded: false }, + high: { localId: "j", start: ref("k"), step: -1, startGuarded: true }, + inclusive: false, + guarded: true, + range: { min: -Number.MAX_SAFE_INTEGER, max: Number.MAX_SAFE_INTEGER }, + }; + const pairedShape = pairedDenseShape(paired); + const matchPaired = (body: IrStmt[]) => + matchDenseArrayLoop(loop(body), pairedShape, locals, new Set(), new Set(["a", "b"])); + const swap: IrStmt[] = [ + { kind: "varDecl", localId: "t", init: read("a"), loc }, + write("a", read("a", ref("j"))), + write("a", ref("t"), ref("j")), + ]; + const dense = matchPaired(swap); + expect([...dense!.arrays.keys()]).toEqual(["a"]); + expect(dense!.pair).toEqual({ high: "j", lowStart: num(1), highStart: ref("k") }); + expect(dense!.range).toEqual({ min: 0, max: 2147483646 }); + // Stores at any other index may extend the array. + expect(matchPaired([...swap, write("a", num(0), ref("k"))])).toBeNull(); +}); + +test("a present-undefined store is covered only where it stores a covered read's missing arm", () => { + // const v = b[i] (as number | undefined); + // if (v is undefined) c[i] = undefined; else c[i] = v is number ? v : throw; + const unionType = { kind: "union" as const, unionId: "u" }; + const v: IrExpr = { kind: "varRef", localId: "v", type: unionType, loc }; + const isTag = (tag: number): IrExpr => ({ + kind: "unionIsTag", + unionId: "u", + tag, + negated: false, + value: v, + type: BOOL, + loc, + }); + const reader = (array: string, index = ref("i")): IrExpr => ({ + kind: "call", + callee: "%arr.idxOr.f64", + args: [arr(array), index], + type: unionType, + loc, + }); + const undefinedStore: IrStmt = { kind: "arraySetUndefined", arr: arr("c"), index: ref("i"), loc }; + const guarded = (array: string): IrStmt[] => [ + { kind: "varDecl", localId: "v", init: reader(array), loc }, + { + kind: "if", + cond: isTag(1), + then: [undefinedStore], + else_: [ + write("c", { + kind: "ternary", + cond: isTag(0), + then: { kind: "unionNarrow", unionId: "u", tag: 0, value: v, type: F64, loc }, + else_: { + kind: "libCall", + fn: "error.nodeThrow", + args: [num(1), { kind: "strLit", value: "", type: { kind: "string" }, loc }], + type: F64, + loc, + }, + type: F64, + loc, + }), + ], + loc, + }, + ]; + const withV = new Map([ + ...locals, + ["v", { id: "v", name: "v", type: unionType, mutable: false }], + ]); + const matchStores = (body: IrStmt[], initialized: string[]) => + matchDenseArrayLoop(loop(body), shape, withV, new Set(), new Set(initialized)); + const dense = matchStores(guarded("b"), ["a", "b", "c"]); + expect([...dense!.arrays.keys()].sort()).toEqual(["b", "c"]); + // A read of an array the entry test does not cover may miss in the fast + // copy, and its undefined store would run there. + expect(matchStores(guarded("b"), ["a", "c"])).toBeNull(); + // A present undefined stored on its own leaves out its array and every + // array the other name may hold. + const alone: IrStmt[] = [{ kind: "exprStmt", expr: read("b"), loc }, undefinedStore]; + expect(matchStores(alone, ["a", "b", "c"])).toBeNull(); + const separate = matchDenseArrayLoop( + loop(alone), + shape, + withV, + new Set(["c"]), + new Set(["a", "b", "c"]), + ); + expect([...separate!.arrays.keys()]).toEqual(["b"]); +}); diff --git a/packages/compiler/src/backend/llvm/dense-array-loops.ts b/packages/compiler/src/backend/llvm/dense-array-loops.ts index 184ff1353..020ea582a 100644 --- a/packages/compiler/src/backend/llvm/dense-array-loops.ts +++ b/packages/compiler/src/backend/llvm/dense-array-loops.ts @@ -20,16 +20,23 @@ * A counted loop with another invariant limit, such as `j < n` after * `const n = a.length` or `j < 5`, is covered the same way: the test bounds * the loop's exclusive limit by 2^31 - 1 and by each indexed array's - * `filled`, and the loop must provably never change any of their lengths. */ + * `filled`, and the loop must provably never change any of their lengths. + * + * A two-counter loop closing in on itself, such as + * `for (let i = 1, j = k - 1; i < j; i++, j--)`, indexes only between its + * low counter's start and its high counter's: the low counter only rises + * and stays below the high one, which only falls. Its test bounds the low + * start below by 0 and the high start by 2^31 - 2 and below each indexed + * array's `filled`, both starts being exact integers. */ import type { IrExpr, IrLocal, IrStmt } from "../../ir/ir.js"; import type { IntegerRange } from "../../ir/integer-ranges.js"; import { preservesArrayLength } from "../../ir/array-length-stability.js"; import { everyExprChild, everyStmtChild } from "../../ir/traverse.js"; -import type { IntegerCountedForLoop } from "../../ir/integer-loops.js"; +import type { IntegerCountedForLoop, IntegerPairedForLoop } from "../../ir/integer-loops.js"; import type { LlvmEmitterContext } from "./expr-context.js"; import { loadArrCount, SCR_ARR_FIELD } from "./common.js"; -import { emitCountedLoopLimit } from "./counted-loops.js"; +import { emitCountedLoopLimit, emitStartIntegerTest } from "./counted-loops.js"; export const INT32_MAX = 2147483647; @@ -47,6 +54,17 @@ export interface DenseArrayLoop { arrays: ReadonlyMap; /** The counter's range on every pass of the fast copy. */ range: IntegerRange; + /** A two-counter loop's high counter and both starts, when the counter + * is its low one (`length` and `limit` are then null). */ + pair: DensePair | null; +} + +/** The high counter of a two-counter dense loop, and the starts its entry + * test bounds. */ +export interface DensePair { + high: string; + lowStart: IrExpr; + highStart: IrExpr; } /** The fast copy duplicates the loop; keep the duplicated code small. */ @@ -62,6 +80,8 @@ export interface DenseLoopShape { /** The counter's proven range on every pass of the ordinary loop; the * fast copy keeps its minimum, which must be at least 0. */ range: IntegerRange; + /** A two-counter loop's high counter and starts. */ + pair?: DensePair; } /** `length.length - c` (c an integer literal, 0 when absent). */ @@ -107,6 +127,124 @@ export function countedDenseShape( return { counter: counted.localId, length: start.receiver, range: counted.range }; } +/** The shape of a two-counter loop whose low counter rises and high + * counter falls: both stay between the two starts, which the entry test + * bounds, so the fast copy's counters lie in [0, 2^31 - 2]. */ +export function pairedDenseShape(loop: IntegerPairedForLoop): DenseLoopShape { + return { + counter: loop.low.localId, + length: null, + range: { min: 0, max: INT32_MAX - 1 }, + pair: { high: loop.high.localId, lowStart: loop.low.start, highStart: loop.high.start }, + }; +} + +interface ElementRead { + arr: IrExpr; + index: IrExpr; +} + +/** The array and index of a call of an element-reader helper + * (`%arr.idxOr.*`, ir/int32-arrays.ts isElementReader): `a[i]` as + * `number | undefined`. */ +function elementReaderCall(e: IrExpr): ElementRead | null { + if (e.kind !== "call" || !e.callee.startsWith("%arr.idxOr.") || e.args.length !== 2) return null; + return { arr: e.args[0]!, index: e.args[1]! }; +} + +/** The array and index of an element read as `T | undefined`: an + * element-reader call, or its body inlined for an int32 array + * (`arrayState(a, i) === 1 ? wrap(a[i]) : wrap(undefined)`). */ +function elementRead(e: IrExpr): ElementRead | null { + const call = elementReaderCall(e); + if (call) return call; + if (e.kind !== "ternary") return null; + const cond = e.cond; + if (cond.kind !== "bin" || cond.op !== "===") return null; + const state = cond.left, + present = cond.right, + wrapped = e.then, + missing = e.else_; + if (state.kind !== "arrayState" || present.kind !== "numLit" || present.value !== 1) return null; + if (wrapped.kind !== "unionWrap" || missing.kind !== "unionWrap") return null; + const value = wrapped.value; + if (value.kind !== "arrayGet" || missing.value.kind !== "unitLit") return null; + const same = (x: IrExpr, y: IrExpr): boolean => + x.kind === "varRef" && y.kind === "varRef" && x.localId === y.localId; + return same(state.arr, value.arr) && same(state.index, value.index) + ? { arr: state.arr, index: state.index } + : null; +} + +/** The present-undefined stores of `a[i] = b[j]!` as lowering spells it, + * with the element read each one stores the missing arm of: + * + * const v = %arr.idxOr(b, j); + * if (v is undefined) a[i] = undefined; else a[i] = v is number ? v : throw; + * + * The read's local is declared once in the same list as the `if`, and only + * read after it. */ +function missingElementStores(body: IrStmt[]): Map { + const result = new Map(); + const writes = new Map(); + const count = (id: string): void => void writes.set(id, (writes.get(id) ?? 0) + 1); + const expr = (e: IrExpr): boolean => { + if (e.kind === "assignExpr" || e.kind === "incDec") count(e.localId); + return everyExprChild(e, expr, stmt); + }; + const stmt = (s: IrStmt): boolean => { + if (s.kind === "varDecl" || s.kind === "assign") count(s.localId); + return everyStmtChild(s, expr, stmt); + }; + body.every(stmt); + const list = (stmts: IrStmt[]): void => { + const reads = new Map(); + for (const s of stmts) { + if (s.kind === "varDecl" && s.init && writes.get(s.localId) === 1) { + const read = elementRead(s.init); + if (read) reads.set(s.localId, read); + } + if (s.kind === "if") { + const store = missingArmStore(s, reads); + if (store) result.set(store.stmt, store.read); + } + // Nested loops keep a loop out of the fast copy (matchDenseArrayLoop), + // and other statement lists leave their stores unmatched. + if (s.kind === "block") list(s.body); + if (s.kind === "if") { + list(s.then); + if (s.else_) list(s.else_); + } + } + }; + list(body); + return result; +} + +/** The present-undefined store of `if (v is undefined) a[i] = undefined; + * else a[i] = v is number ? v : throw;`, with the read `v` holds. */ +function missingArmStore( + s: IrStmt & { kind: "if" }, + reads: ReadonlyMap, +): { stmt: IrStmt; read: ElementRead } | null { + const cond = s.cond; + if (cond.kind !== "unionIsTag" || cond.negated || cond.value.kind !== "varRef") return null; + const local = cond.value.localId; + const read = reads.get(local); + if (!read || s.then.length !== 1 || s.else_?.length !== 1) return null; + const store = s.then[0]!, + other = s.else_[0]!; + if (store.kind !== "arraySetUndefined" || other.kind !== "arraySet") return null; + const value = other.value; + if (value.kind !== "ternary") return null; + const test = value.cond, + narrowed = value.then; + if (test.kind !== "unionIsTag" || test.negated || test.tag === cond.tag) return null; + if (test.value.kind !== "varRef" || test.value.localId !== local) return null; + if (narrowed.kind !== "unionNarrow" || narrowed.tag !== test.tag) return null; + return narrowed.type.kind === "f64" ? { stmt: store, read } : null; +} + /** Match the fast copy of a loop of the given shape. `initialized` holds the * bindings definitely assigned when the loop starts; `unaliased` the local * arrays no other binding holds. */ @@ -118,6 +256,8 @@ export function matchDenseArrayLoop( initialized: ReadonlySet, ): DenseArrayLoop | null { const { counter, length } = shape; + const high = shape.pair?.high ?? null; + const isCounter = (id: string): boolean => id === counter || id === high; if (shape.range.min < 0 || !loop.cond) return null; let nodes = 0, nested = false; @@ -126,13 +266,21 @@ export function matchDenseArrayLoop( // element write. const disturbed = new Set(); const access = (arr: IrExpr, index: IrExpr): void => { - if (arr.kind === "varRef" && index.kind === "varRef" && index.localId === counter) + if (arr.kind === "varRef" && index.kind === "varRef" && isCounter(index.localId)) candidates.set(arr.localId, arr); }; + const missing = missingElementStores(loop.body); + // The element reads behind the guarded present-undefined stores. + const guards: ElementRead[] = []; + // The arrays of the other present-undefined stores. The fast copy runs + // them, and the slot each one writes leaves the hole-free prefix. + const undefinedStores: IrExpr[] = []; const expr = (e: IrExpr): boolean => { if (++nodes > MAX_LOOP_NODES) return false; if (e.kind === "assignExpr" || e.kind === "incDec") disturbed.add(e.localId); if (e.kind === "arrayGet") access(e.arr, e.index); + const reader = elementReaderCall(e); + if (reader) access(reader.arr, reader.index); if (e.kind === "arrIntrinsic") { if (e.method === "getNumber" && e.args[0]) access(e.receiver, e.args[0]); else if (e.method !== "length" && e.receiver.kind === "varRef") @@ -147,9 +295,21 @@ export function matchDenseArrayLoop( if (s.kind === "assign" || s.kind === "varDecl") disturbed.add(s.localId); if (s.kind === "arraySet") { access(s.arr, s.index); - if (s.arr.kind === "varRef" && (s.index.kind !== "varRef" || s.index.localId !== counter)) + if (s.arr.kind === "varRef" && (s.index.kind !== "varRef" || !isCounter(s.index.localId))) disturbed.add(s.arr.localId); } + if (s.kind === "arraySetUndefined") { + // A present undefined at a counter only where it stores a missing + // element read (`a[i] = b[j]!`): the fast copy reads a present value + // there, so the store never runs in it. + const read = missing.get(s); + if (read === undefined || s.index.kind !== "varRef" || !isCounter(s.index.localId)) { + undefinedStores.push(s.arr); + } else { + access(s.arr, s.index); + guards.push(read); + } + } return everyStmtChild(s, expr, stmt); }; if (!loop.body.every(stmt) || !expr(loop.cond) || nested) return null; @@ -164,6 +324,15 @@ export function matchDenseArrayLoop( ) ) return null; + // A store through another name, such as `const b = a` or a field, lands + // in `a` unless one of the two bindings is a local array no other binding + // holds. + const mayStoreUndefined = (id: string): boolean => + undefinedStores.some((arr) => + arr.kind === "varRef" + ? arr.localId === id || (!unaliased.has(id) && !unaliased.has(arr.localId)) + : !unaliased.has(id), + ); const arrays = new Map(); for (const [id, ref] of candidates) { const local = locals.get(id); @@ -173,6 +342,7 @@ export function matchDenseArrayLoop( local.boxed || local.tdz || disturbed.has(id) || + mayStoreUndefined(id) || (id !== length?.localId && !initialized.has(id)) ) continue; @@ -180,18 +350,33 @@ export function matchDenseArrayLoop( // once: the loop must not change any of their lengths. if ( length === null && - !preservesArrayLength(nodesOfLoop, { receiver: id, inRange: counter }, locals, unaliased) + !preservesArrayLength( + nodesOfLoop, + { receiver: id, inRange: counter, inRangeHigh: high }, + locals, + unaliased, + ) ) continue; arrays.set(id, ref); } if (arrays.size === 0) return null; + // Each guarded store's read must be one the fast copy proves present. + for (const { arr, index } of guards) + if ( + arr.kind !== "varRef" || + !arrays.has(arr.localId) || + index.kind !== "varRef" || + !isCounter(index.localId) + ) + return null; return { counter, length, limit: length === null ? (shape.limit ?? null) : null, arrays, range: { min: shape.range.min, max: Math.min(shape.range.max, INT32_MAX - 1) }, + pair: shape.pair ?? null, }; } @@ -207,6 +392,7 @@ export function emitDenseLoopGuard(host: LlvmEmitterContext, loop: DenseArrayLoo host.markMemoryPointer(ptr, "array:header"); return ptr; }; + if (loop.pair !== null) return emitPairedDenseGuard(host, loop, loop.pair, header); if (loop.length === null) { // A limit at or below the start runs no pass, and passes the test. const limit = emitCountedLoopLimit(host, loop.limit!); @@ -254,3 +440,36 @@ export function emitDenseLoopGuard(host: LlvmEmitterContext, loop: DenseArrayLoo } return valid; } + +/** A two-counter loop's entry test: both starts are exact integers, the + * low one at least 0 and the high one at most 2^31 - 2 and below every + * indexed array's hole-free prefix bound. The starts are stable values + * (matchIntegerPairedForLoop), so evaluating them here again is unobservable. */ +function emitPairedDenseGuard( + host: LlvmEmitterContext, + loop: DenseArrayLoop, + pair: DensePair, + header: (array: string, field: number) => string, +): string { + const B = host.B; + const start = (expr: IrExpr, min: number, max: number) => + emitStartIntegerTest(host, expr, min, max); + const low = start(pair.lowStart, 0, INT32_MAX - 1); + const high = start(pair.highStart, -1, INT32_MAX - 1); + let valid = B.tmp(); + B.line(`${valid} = and i1 ${low.ok}, ${high.ok}`); + for (const ref of loop.arrays.values()) { + const array = host.emitStableReceiver(ref, []); + const filledPtr = header(array.name, SCR_ARR_FIELD.filled), + filled = B.tmp(), + wide = B.tmp(), + covered = B.tmp(), + both = B.tmp(); + B.line(`${filled} = load i32, ptr ${filledPtr}${host.fieldAliasAttachment(filledPtr)}`); + B.line(`${wide} = zext i32 ${filled} to i64`); + B.line(`${covered} = icmp slt i64 ${high.integer}, ${wide}`); + B.line(`${both} = and i1 ${valid}, ${covered}`); + valid = both; + } + return valid; +} diff --git a/packages/compiler/src/backend/llvm/dyn-handle-tags.test.ts b/packages/compiler/src/backend/llvm/dyn-handle-tags.test.ts index c015b85d0..8c3e5de80 100644 --- a/packages/compiler/src/backend/llvm/dyn-handle-tags.test.ts +++ b/packages/compiler/src/backend/llvm/dyn-handle-tags.test.ts @@ -1,7 +1,7 @@ import { readFileSync } from "node:fs"; import { expect, test } from "vitest"; import { DYN_HANDLE_KINDS } from "../../ir/ir.js"; -import { DYN_HANDLE_TAG_NUM } from "./dyn.js"; +import { DYN_HANDLE_TAG_NUM, TEXT_CODEC_TAG_NUM } from "./dyn.js"; // SCR_DYNH_TAGS in scr_dyn.h lists the ScrDynHandleTag values by name, each // with an explicit value, so changes that add tags edit different lines. The @@ -39,3 +39,9 @@ test("the backend's handle tag numbers match scr_dyn.h", () => { expect(values.get(tag!), `${kind} (${tag})`).toBe(num); } }); + +test("the backend's codec handle tag numbers match scr_dyn.h", () => { + const values = new Map(tags.map((tag) => [tag.name, tag.value])); + expect(values.get("SCR_DYNH_TEXT_DECODER")).toBe(TEXT_CODEC_TAG_NUM.TextDecoder); + expect(values.get("SCR_DYNH_TEXT_ENCODER")).toBe(TEXT_CODEC_TAG_NUM.TextEncoder); +}); diff --git a/packages/compiler/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts index 86626d304..12ea99688 100644 --- a/packages/compiler/src/backend/llvm/dyn.ts +++ b/packages/compiler/src/backend/llvm/dyn.ts @@ -43,6 +43,8 @@ import { isDynTypedRefType, isRefCounted, isUnitType, + recordHandleCodecClass, + textCodecRecordClass, typeKey, } from "../../ir/ir.js"; import { dynDesc, streamTypedRefEligible } from "../../ir/analysis.js"; @@ -64,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 @@ -94,6 +115,19 @@ export const DYN_HANDLE_TAG_NUM: Record = { cryptoHmac: 53, }; +/** The handle tags of TextEncoder and TextDecoder records boxed as dyn + * (scr_dyn.h; scr_text_codec_dyn.c boxes and unboxes them). */ +export const TEXT_CODEC_TAG_NUM: Record<"TextEncoder" | "TextDecoder" | "StringDecoder", number> = { + TextDecoder: 76, + TextEncoder: 77, + StringDecoder: 78, +}; + +/** The decoder record fields scr_text_decoder_to_dyn reads, in its + * parameter order. */ +const TEXT_DECODER_FIELDS = ["%TextDecoder", "%fatal", "%ignoreBOM", "%state"] as const; +const STRING_DECODER_FIELDS = ["%enc", "%pending"] as const; + /** Handle kinds whose runtime unit boxes them through its own function, * which installs the tag's ops on first use (scr_crypto.h). Programs that * never box one of these values do not link its dynamic members. */ @@ -138,6 +172,18 @@ export const DYN_KIND = { SYMBOL: 15, } as const; +/** Union arm kinds whose matchers reject every plain dyn object + * (trustedArmByLiteral). */ +const NON_OBJECT_ARM_KINDS: ReadonlySet = new Set([ + "f64", + "string", + "bool", + "nullT", + "undefinedT", + "bigint", + "symbol", +]); + /** What the dyn helpers need beyond the walker host: interned immortal * unit instances (undefined-armed dynCheck targets build them). */ export interface DynHost extends WalkerHost { @@ -161,6 +207,10 @@ export interface DynHost extends WalkerHost { dynPromiseAdapter(t: IrType): string; isErrorClass(className: string): boolean; classSubtypes(className: string): readonly string[]; + /** The strict subclasses whose class-value constructor takes the same + * arguments as `className`'s (classValueAbiEquals): the class values a + * checked `classval` slot accepts besides its own class. */ + classValueSubtypes(className: string): readonly string[]; /** emitter.ts's pendingTestLines: the inline pending-exception test. */ pendingTestLines(dest: string): string[]; } @@ -180,6 +230,11 @@ function builtinStreamKind(className: string): number | null { return kinds[className] ?? null; } +/** Unions whose discriminant has fewer string literals keep one literal test + * per arm (dispatchValues): one runtime lookup costs about what a few + * literal tests do. */ +const DISCRIMINANT_DISPATCH_VALUES = 4; + export class LlDyn { private readonly dynMatchers = new Map(); private readonly dynBuilders = new Map(); @@ -370,6 +425,51 @@ export class LlDyn { B.line(`call void @scr_dyn_build_add(ptr %d, ptr @${name}, ptr ${value})`); } + /** Whether a builder of `def` may pick a plain object's arm by its literal + * discriminator alone inside a trusted build (scr_dyn_trust_enter): the + * arms' literals are disjoint, and every arm without a literal has a kind + * no plain object matches. The value then matched exactly one arm with a + * literal, the arm whose literal it carries. */ + private trustedArmByLiteral(def: IrUnionDef): boolean { + const discriminant = def.discriminant; + if (!discriminant) return false; + const seen = new Set(); + for (const { values } of discriminant.cases) { + for (const value of values) { + const key = `${typeof value}:${String(value)}`; + if (seen.has(key)) return false; + seen.add(key); + } + } + return def.arms.every( + (arm, tag) => + discriminant.cases.some((candidate) => candidate.tag === tag) || + NON_OBJECT_ARM_KINDS.has(arm.kind), + ); + } + + /** Per arm of `def`, the dyn kind of the values a builder may convert into + * that arm without matching its structure inside a trusted build, or null. + * A value there already matched one of the union's arms, so the arm is + * known when it is the only arm that takes values of its kind: the arm + * whose literal a plain object carries (trustedArmByLiteral), or the one + * arm a plain object or an array can match at all. */ + private trustedArmKinds(def: IrUnionDef): (number | null)[] { + const byLiteral = this.trustedArmByLiteral(def); + const objectArms = def.arms.flatMap((arm, tag) => + NON_OBJECT_ARM_KINDS.has(arm.kind) ? [] : [tag], + ); + return def.arms.map((arm, tag) => { + if (def.discriminant?.cases.some((candidate) => candidate.tag === tag)) + return byLiteral ? DYN_KIND.OBJ : null; + if (objectArms.length !== 1 || objectArms[0] !== tag) return null; + if (arm.kind === "array") return DYN_KIND.ARR; + if (arm.kind === "record" && this.host.recordsById.get(arm.shapeId)?.tuple !== true) + return DYN_KIND.OBJ; + return null; + }); + } + /** The order a dyn value tries a union's arms in: the canonical order, * except that among the record arms, those with more required fields go * first, then those with more fields. Records are width-tolerant, so an @@ -400,8 +500,123 @@ export class LlDyn { return order; } + /** The string literals of `def`'s discriminant when its arms are chosen + * by reading the discriminant once (unionArmsByDiscriminant), or null + * when the union keeps one literal test per arm: some literal is not a + * string, or there are fewer than DISCRIMINANT_DISPATCH_VALUES. */ + private dispatchValues(def: IrUnionDef): string[] | null { + const values: string[] = []; + for (const guard of def.discriminant?.cases ?? []) { + if (guard.values.length === 0) return null; + for (const value of guard.values) { + if (typeof value !== "string") return null; + if (!values.includes(value)) values.push(value); + } + } + return values.length < DISCRIMINANT_DISPATCH_VALUES ? null : values; + } + + /** Branches to `hits[tag]` for the first arm in `order` that `%d` matches + * (unionArmMatch), or to `none` when no arm does, for + * a union whose discriminant literals are `values` (dispatchValues). The + * discriminant is read once (scr_dyn_discriminant_case, against the + * constant `table`), and only the arms whose literal it holds and the arms + * without a literal are tested, in `order`: every other arm's literal test + * fails. An arm's failed check can run a getter that changes the + * discriminant, so it is read again after each one; when its case + * changed, the tests continue arm by arm after the failed arm, as they + * would have run from the first arm. A value the runtime cannot classify + * that way (a typed capsule, an accessor, an inherited discriminant) takes + * the tests arm by arm. */ + private unionArmsByDiscriminant( + B: BlockBuilder, + def: IrUnionDef, + values: readonly string[], + order: readonly number[], + hits: readonly string[], + none: string, + preserveRefs: boolean, + table: string, + trustedKinds: readonly (number | null)[] = def.arms.map(() => null), + ): void { + const discriminant = def.discriminant!; + const literalsOf = (tag: number): readonly (string | number | boolean)[] | null => + discriminant.cases.find((candidate) => candidate.tag === tag)?.values ?? null; + const host = this.host; + this.defs.push( + `${table} = private unnamed_addr constant [${values.length} x ptr] [${values.map((value) => `ptr ${host.internLiteral(value)}`).join(", ")}]`, + ``, + ); + host.declare(`declare i32 @scr_dyn_discriminant_case(ptr, ptr, ptr, ${host.sizeType})`); + const classify = (): string => { + const index = B.tmp(); + B.line( + `${index} = call i32 @scr_dyn_discriminant_case(ptr %d, ptr ${host.internLiteral(discriminant.field)}, ptr ${table}, ${host.sizeType} ${values.length})`, + ); + return index; + }; + // The arm-by-arm tests, entered at any arm (`from[i]` tests order[i] + // and the arms after it). + const tests = B.newLabel("du.tests"); + const from = order.map((_, i) => (i === 0 ? tests : B.newLabel("du.t"))); + const after = (position: number): string => + position + 1 < order.length ? from[position + 1]! : none; + // Tests the arms a value in case `k` (-1: no literal) can match. + const tryCase = (k: number): void => { + const value = k < 0 ? null : values[k]!; + const positions = order + .map((_, position) => position) + .filter((position) => { + const literals = literalsOf(order[position]!); + return literals === null || (value !== null && literals.includes(value)); + }); + if (positions.length === 0) { + B.br(none); + return; + } + positions.forEach((position, index) => { + const tag = order[position]!; + const failed = B.newLabel("du.c.f"); + this.unionArmStructure(B, def, tag, hits[tag]!, failed, preserveRefs, trustedKinds[tag]!); + B.startBlock(failed); + const next = index === positions.length - 1 ? none : B.newLabel("du.c.n"); + const same = B.tmp(); + B.line(`${same} = icmp eq i32 ${classify()}, ${k}`); + B.condBr(same, next, after(position)); + if (next !== none) B.startBlock(next); + }); + }; + const index = classify(); + const unmatched = B.newLabel("du.none"); + const cases = values.map(() => B.newLabel("du.case")); + B.terminate( + `switch i32 ${index}, label %${tests} [ i32 -1, label %${unmatched} ${cases.map((label, k) => `i32 ${k}, label %${label}`).join(" ")} ]`, + ); + values.forEach((_, k) => { + B.startBlock(cases[k]!); + tryCase(k); + }); + B.startBlock(unmatched); + tryCase(-1); + order.forEach((tag, position) => { + B.startBlock(from[position]!); + this.unionArmMatch( + B, + def, + tag, + hits[tag]!, + after(position), + preserveRefs, + trustedKinds[tag]!, + ); + }); + } + /** Branch to a variant only after its literal discriminator and complete - * structure match. Use the same path for predicates and builders. */ + * structure match. Use the same path for predicates and builders. With a + * `trustedKind` (builders, trustedArmKinds), a value of that dyn kind + * inside a trusted build skips the structure match once its literal + * matched. */ private unionArmMatch( B: BlockBuilder, def: IrUnionDef, @@ -409,6 +624,7 @@ export class LlDyn { yes: string, no: string, preserveRefs = false, + trustedKind: number | null = null, ): void { const guard = def.discriminant?.cases.find((candidate) => candidate.tag === tag); if (guard && def.discriminant) { @@ -437,6 +653,34 @@ export class LlDyn { }); B.startBlock(structure); } + this.unionArmStructure(B, def, tag, yes, no, preserveRefs, trustedKind); + } + + /** The part of unionArmMatch past the arm's literal test. With a + * `trustedKind`, a value of that dyn kind inside a trusted build skips the + * structure match. */ + private unionArmStructure( + B: BlockBuilder, + def: IrUnionDef, + tag: number, + yes: string, + no: string, + preserveRefs: boolean, + trustedKind: number | null = null, + ): void { + if (trustedKind !== null) { + this.host.declare(`declare zeroext i1 @scr_dyn_trusted_build()`); + const kind = this.kindOf(B, "%d"); + const sameKind = B.tmp(); + const trusted = B.tmp(); + const skip = B.tmp(); + B.line(`${sameKind} = icmp eq i32 ${kind}, ${trustedKind}`); + B.line(`${trusted} = call zeroext i1 @scr_dyn_trusted_build()`); + B.line(`${skip} = and i1 ${sameKind}, ${trusted}`); + const full = B.newLabel("du.full"); + B.condBr(skip, yes, full); + B.startBlock(full); + } const matches = B.tmp(); B.line( `${matches} = call zeroext i1 @${this.dynMatchHelper(def.arms[tag]!, preserveRefs)}(ptr %d)`, @@ -456,7 +700,7 @@ export class LlDyn { /** The value LLVM type of a dynCheck result / toDyn operand for `t`. */ private valTy(t: IrType): string { - return t.kind === "f64" || t.kind === "date" || t.kind === "procStream" + return t.kind === "f64" || t.kind === "procStream" ? "double" : t.kind === "bool" ? "i1" @@ -534,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(); @@ -920,13 +1171,28 @@ export class LlDyn { /** `sc_dm_(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; const name = `sc_dm_${this.dynMatchers.size}`; this.dynMatchers.set(matcherKey, name); this.host.journalAllocated?.(name, "dyn.dynMatchHelper", [t, preserveRefs]); + const codec = textCodecRecordClass(t, (id) => this.host.recordsById.get(id)); + if (codec !== null) { + // A codec matches only its own handle box. + const B = new BlockBuilder(); + const matched = this.handleTagMatches(B, TEXT_CODEC_TAG_NUM[codec]); + B.terminate(`ret i1 ${matched}`); + this.defs.push( + `define internal zeroext i1 @${name}(ptr %d) ${FN_ATTRS} { ; matches ${key}`, + B.render(), + `}`, + ``, + ); + this.host.journalBuilt?.(); + return name; + } const recursive = this.isRecursiveShape(t); const wrapped = recursive || this.holdsRecursiveShape(t); const B = new BlockBuilder(); @@ -951,7 +1217,10 @@ export class LlDyn { const lPlain = B.newLabel("dm.tr.plain"); B.condBr(capsule, lCapsule, lPlain); B.startBlock(lCapsule); - if (preserveRefs) { + // A class target matches only capsules of that class (tested above): + // a materialized view is a plain object, which never carries a class + // brand, so materializing one cannot change the answer. + if (preserveRefs || (t.kind === "object" && t.className !== "%Error")) { B.terminate(`ret i1 false`); } else { const materialized = B.tmp(); @@ -1217,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})`); @@ -1228,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"); @@ -1247,11 +1525,26 @@ export class LlDyn { ); // Arms in canonical order; any full match answers true. const yes = B.newLabel("dm.y"); - for (let tag = 0; tag < def.arms.length; tag++) { - const ln = B.newLabel("dm.n"); - this.unionArmMatch(B, def, tag, yes, ln, preserveRefs); - B.startBlock(ln); - } + const values = this.dispatchValues(def); + if (values !== null) { + const no = B.newLabel("dm.n"); + this.unionArmsByDiscriminant( + B, + def, + values, + def.arms.map((_, tag) => tag), + def.arms.map(() => yes), + no, + preserveRefs, + `@${name}.cases`, + ); + B.startBlock(no); + } else + for (let tag = 0; tag < def.arms.length; tag++) { + const ln = B.newLabel("dm.n"); + this.unionArmMatch(B, def, tag, yes, ln, preserveRefs); + B.startBlock(ln); + } B.terminate(`ret i1 false`); B.startBlock(yes); B.terminate(`ret i1 true`); @@ -1308,8 +1601,9 @@ export class LlDyn { * fields, and at least one unless an index signature collects the * members), record members typed as unions of those leaves, records and * arrays, null and undefined (optional and nullable members; see - * jsonUnionSupported), and index signatures whose value type is not a - * union. Everything else keeps the checked-dynamic route alone. */ + * jsonUnionSupported), discriminated unions of records anywhere (see + * jsonDiscriminatedSupported), and index signatures whose value type is + * not a union. Everything else keeps the checked-dynamic route alone. */ private jsonSchemaSupported(t: IrType, visiting: Set, member = false): boolean { switch (t.kind) { case "f64": @@ -1336,13 +1630,42 @@ export class LlDyn { (!index || this.jsonSchemaSupported(index, visiting)) ); } - case "union": + case "union": { + const def = this.host.unionsById.get(t.unionId); + if (def?.discriminant) return this.jsonDiscriminatedSupported(def, visiting); return member && this.jsonUnionSupported(t, visiting); + } default: return false; } } + /** A discriminated union the one-pass parser writes directly, as a record + * member, an array element or the whole document: record arms that each + * name the string literals their discriminant member holds, every + * literal naming one arm, plus null and undefined arms. An object then + * selects the one arm whose literal its discriminant member holds, the + * only arm the checked route's literal tests leave to match + * (unionArmsByDiscriminant); an object holding no such literal declines. */ + private jsonDiscriminatedSupported(def: IrUnionDef, visiting: Set): boolean { + const discriminant = def.discriminant!; + const literals = new Set(); + let records = 0; + const supported = def.arms.every((arm, tag) => { + if (arm.kind === "nullT" || arm.kind === "undefinedT") return true; + if (arm.kind !== "record") return false; + records++; + const guard = discriminant.cases.find((candidate) => candidate.tag === tag); + if (!guard || guard.values.length === 0) return false; + for (const value of guard.values) { + if (typeof value !== "string" || literals.has(value)) return false; + literals.add(value); + } + return this.jsonSchemaSupported(arm, visiting); + }); + return supported && records >= 2; + } + /** A union record member the one-pass parser writes directly: every arm * a number, boolean, string, null, undefined, or a supported record or * array, with at most one record and one array arm and no discriminant. @@ -1447,9 +1770,15 @@ export class LlDyn { body = `${S} 3, ${S} ${entries.length}, ptr ${fields}, ptr ${overflow.elem}, ptr @${mangleRecordNew(t.shapeId)}, ptr null, ptr ${vAdapters(host, t).release}, ptr null, ${S} ${overflow.offset}`; break; } - case "union": - body = `${S} 5, ${S} 0, ptr null, ptr null, ptr null, ptr null, ptr null, ptr @${this.jsonUnionSchema(t, name)}, ${S} 0`; + case "union": { + // A discriminated union can be a whole document, whose value the + // parser releases when text follows it. + const release = host.unionsById.get(t.unionId)?.discriminant + ? vAdapters(host, t).release + : "null"; + body = `${S} 5, ${S} 0, ptr null, ptr null, ptr null, ptr null, ptr ${release}, ptr @${this.jsonUnionSchema(t, name)}, ${S} 0`; break; + } default: throw new InternalCompilerError(`llvm emitter bug: JSON schema of ${t.kind}`); } @@ -1464,7 +1793,11 @@ export class LlDyn { /** The ScrJsonUnion descriptor of a union member (scr_json.h layout): * its slot representation, the arm each JSON value kind selects, and the * values the runtime stores for null and for a missing member. Boxed - * unions build their boxes through `_box`. */ + * unions build their boxes through `_box`. A discriminated union + * also lists its object arms by discriminant literal (`_cases`), + * and, because it can be an array element or a whole document, whose + * values are boxes unless the union is a pointer union, it keeps its null + * value and box whatever its field representation. */ private jsonUnionSchema(t: IrType & { kind: "union" }, name: string): string { const host = this.host; const S = host.sizeType; @@ -1483,10 +1816,11 @@ export class LlDyn { ); const nullTag = tags[0]!; const absentTag = tags[6]!; - const nullValue = !inline && nullTag >= 0 ? host.unitInstanceRef(t.unionId, nullTag) : "null"; - const absentValue = !inline && absentTag >= 0 ? host.absentInstanceRef(t.unionId) : "null"; + const values = !inline || def.discriminant !== undefined; + const nullValue = values && nullTag >= 0 ? host.unitInstanceRef(t.unionId, nullTag) : "null"; + const absentValue = values && absentTag >= 0 ? host.absentInstanceRef(t.unionId) : "null"; let box = "null"; - if (repr === 2) { + if (repr === 2 || (def.discriminant && !nullable)) { box = `@${name}_box`; const lines = [`define internal ptr ${box}(i32 %tag, i64 %bits) ${FN_ATTRS} {`, `entry:`]; const cases: string[] = []; @@ -1527,14 +1861,45 @@ export class LlDyn { ``, ); } + let cases = "null"; + if (def.discriminant) { + const discriminant = def.discriminant; + const caseTy = `{ ptr, ${S}, ${S}, ptr }`; + const entries = discriminant.cases.flatMap((guard) => + guard.values.map( + (value) => + `${caseTy} { ptr ${host.cstr(String(value))}, ${S} ${Buffer.byteLength(String(value), "utf8")}, ${S} ${guard.tag}, ptr @${this.jsonSchema(def.arms[guard.tag]!)} }`, + ), + ); + const table = `@${name}_case_table`; + cases = `@${name}_cases`; + this.defs.push( + `${table} = internal constant [${entries.length} x ${caseTy}] [${entries.join(", ")}]`, + `${cases} = internal constant { ptr, ${S}, ${S}, ptr } { ptr ${host.cstr(discriminant.field)}, ${S} ${Buffer.byteLength(discriminant.field, "utf8")}, ${S} ${entries.length}, ptr ${table} }`, + ``, + ); + } const descriptor = `${name}_union`; this.defs.push( - `@${descriptor} = internal constant { ${S}, [7 x ${S}], [7 x ptr], ptr, ptr, ptr } { ${S} ${repr}, [7 x ${S}] [${tags.map((tag) => `${S} ${tag}`).join(", ")}], [7 x ptr] [${arms.join(", ")}], ptr ${nullValue}, ptr ${absentValue}, ptr ${box} }`, + `@${descriptor} = internal constant { ${S}, [7 x ${S}], [7 x ptr], ptr, ptr, ptr, ptr } { ${S} ${repr}, [7 x ${S}] [${tags.map((tag) => `${S} ${tag}`).join(", ")}], [7 x ptr] [${arms.join(", ")}], ptr ${nullValue}, ptr ${absentValue}, ptr ${box}, ptr ${cases} }`, ``, ); return descriptor; } + /** `record.sourceSpread(built, value)`: the built record keeps value's + * kept-source order over its own declared names. */ + emitRecordSourceSpread(B: BlockBuilder, built: string, value: string, shapeId: string): void { + const host = this.host; + const shape = host.recordsById.get(shapeId)!; + this.keepsRecordSources = true; + const names = this.recordNameTable(shapeId, shape); + host.declare(`declare void @scr_record_source_spread(ptr, ptr, ptr, ptr, ${host.sizeType})`); + B.line( + `call void @scr_record_source_spread(ptr ${built}, ptr ${value}, ptr ${names.names}, ptr ${names.lens}, ${host.sizeType} ${shape.fields.length})`, + ); + } + /** The declared-name tables scr_record_source_note scans: parallel * constant arrays of field-name bytes and byte lengths in declaration * order (the order the record serializes), interned per shape. Internal @@ -1566,7 +1931,12 @@ export class LlDyn { * dynCheck builder — which reports the exact error. Returns +1, or null * with the pending flag set. */ jsonParseHelper(t: IrType): string | null { - if (t.kind !== "record" && t.kind !== "array") return null; + if ( + t.kind !== "record" && + t.kind !== "array" && + !(t.kind === "union" && this.host.unionsById.get(t.unionId)?.discriminant) + ) + return null; const key = typeKey(t); const existing = this.jsonParsers.get(key); if (existing) return existing; @@ -1610,8 +1980,90 @@ export class LlDyn { /** `sc_dc_(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; @@ -1629,6 +2081,26 @@ export class LlDyn { const B = new BlockBuilder(); if (t.kind === "classval") { host.declare(`declare ptr @scr_dyn_class_check(ptr, ptr, ptr)`); + // A subclass's class value checks into the base class's slot when + // construction through the slot can call its thunk with the base + // constructor's arguments; any other subclass fails the check. + const subclasses = host.classValueSubtypes(t.className); + if (subclasses.length > 0) { + host.declare(`declare zeroext i1 @scr_dyn_class_is(ptr, ptr)`); + for (const className of subclasses) { + const subKey = host.cstr(typeKey({ kind: "classval", className })); + const matched = B.tmp(); + B.line(`${matched} = call zeroext i1 @scr_dyn_class_is(ptr %d, ptr ${subKey})`); + const yes = B.newLabel("sub.yes"); + const no = B.newLabel("sub.no"); + B.condBr(matched, yes, no); + B.startBlock(yes); + const sub = B.tmp(); + B.line(`${sub} = call ptr @scr_dyn_class_check(ptr %d, ptr ${subKey}, ptr %path)`); + B.terminate(`ret ptr ${sub}`); + B.startBlock(no); + } + } const checked = B.tmp(); B.line( `${checked} = call ptr @scr_dyn_class_check(ptr %d, ptr ${host.cstr(key)}, ptr %path)`, @@ -1643,6 +2115,31 @@ export class LlDyn { this.host.journalBuilt?.(); return name; } + const codec = textCodecRecordClass(t, (id) => host.recordsById.get(id)); + if (codec !== null) { + // The record the codec's handle box holds (+1), or the path TypeError. + const checked = B.tmp(); + if (codec === "StringDecoder") { + host.declare(`declare ptr @scr_string_decoder_from_dyn(ptr, ptr, ptr)`); + B.line( + `${checked} = call ptr @scr_string_decoder_from_dyn(ptr %d, ptr %path, ptr ${want})`, + ); + } else { + host.declare(`declare ptr @scr_text_codec_from_dyn(ptr, i1 zeroext, ptr, ptr)`); + B.line( + `${checked} = call ptr @scr_text_codec_from_dyn(ptr %d, i1 ${codec === "TextDecoder"}, ptr %path, ptr ${want})`, + ); + } + B.terminate(`ret ptr ${checked}`); + this.defs.push( + `define internal ptr @${name}(ptr %d, ptr %path) ${FN_ATTRS} {`, + B.render(), + `}`, + ``, + ); + this.host.journalBuilt?.(); + return name; + } if (isRefCounted(t) && t.kind !== "dyn") { host.declare(`declare ptr @scr_dyn_typed_ref_unbox(ptr)`); if (t.kind === "object" && t.className === "%Error") { @@ -1895,10 +2392,10 @@ export class LlDyn { }; switch (t.kind) { case "date": { - host.declare(`declare double @scr_dyn_native_date_value(ptr)`); + host.declare(`declare ptr @scr_dyn_native_date_check(ptr, ptr)`); const r = B.tmp(); - B.line(`${r} = call double @scr_dyn_native_date_value(ptr %d)`); - B.terminate(`ret double ${r}`); + B.line(`${r} = call ptr @scr_dyn_native_date_check(ptr %d, ptr %path)`); + B.terminate(`ret ptr ${r}`); break; } case "url": @@ -2373,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( @@ -2399,13 +2911,20 @@ export class LlDyn { `llvm emitter bug: dynCheck of unknown union ${t.unionId}`, ); // Arms in unionArmOrder, first FULL match wins. The matched arm's - // builder can no longer fail. - for (const i of this.unionArmOrder(def)) { + // builder can no longer fail, and it runs as a trusted build: a + // nested union inside it picks the arm trustedArmKinds names without + // matching the structure that this match already checked. + const trustedKinds = preserveRefs ? def.arms.map(() => null) : this.trustedArmKinds(def); + const trustedCall = (call: () => void): void => { + host.declare(`declare zeroext i1 @scr_dyn_trust_enter()`); + host.declare(`declare void @scr_dyn_trust_exit(i1 zeroext)`); + const previous = B.tmp(); + B.line(`${previous} = call zeroext i1 @scr_dyn_trust_enter()`); + call(); + B.line(`call void @scr_dyn_trust_exit(i1 zeroext ${previous})`); + }; + const build = (i: number): void => { const arm = def.arms[i]!; - const lHit = B.newLabel("dcu.h"); - const lNext = B.newLabel("dcu.n"); - this.unionArmMatch(B, def, i, lHit, lNext, preserveRefs); - B.startBlock(lHit); if (arm.kind === "undefinedT" || arm.kind === "nullT") { // A matched unit arm builds nothing: THE interned immortal // instance (rc == SIZE_MAX — no retain owed). @@ -2431,21 +2950,58 @@ export class LlDyn { } else if (host.nullableUnions.has(t.unionId)) { // A nullable union is its (+1) reference payload. const x = B.tmp(); - B.line(`${x} = call ptr @${this.dynCheckHelper(arm, preserveRefs)}(ptr %d, ptr %path)`); + trustedCall(() => + B.line( + `${x} = call ptr @${this.dynCheckHelper(arm, preserveRefs)}(ptr %d, ptr %path)`, + ), + ); B.terminate(`ret ptr ${x}`); } else { const rc = vAdapters(host, arm); host.declare(`declare ptr @scr_union_new_ref(i32, ptr, ptr, ptr, ptr)`); const x = B.tmp(); const u = B.tmp(); - B.line(`${x} = call ptr @${this.dynCheckHelper(arm, preserveRefs)}(ptr %d, ptr %path)`); + trustedCall(() => + B.line( + `${x} = call ptr @${this.dynCheckHelper(arm, preserveRefs)}(ptr %d, ptr %path)`, + ), + ); B.line( `${u} = call ptr @scr_union_new_ref(i32 ${i}, ptr ${x}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(host, arm)})`, ); B.terminate(`ret ptr ${u}`); } - B.startBlock(lNext); - } + }; + const order = this.unionArmOrder(def); + const values = this.dispatchValues(def); + if (values !== null) { + const hits = def.arms.map(() => B.newLabel("dcu.h")); + const unmatched = B.newLabel("dcu.n"); + this.unionArmsByDiscriminant( + B, + def, + values, + order, + hits, + unmatched, + preserveRefs, + `@${name}.cases`, + trustedKinds, + ); + for (const i of order) { + B.startBlock(hits[i]!); + build(i); + } + B.startBlock(unmatched); + } else + for (const i of order) { + const lHit = B.newLabel("dcu.h"); + const lNext = B.newLabel("dcu.n"); + this.unionArmMatch(B, def, i, lHit, lNext, preserveRefs, trustedKinds[i]!); + B.startBlock(lHit); + build(i); + B.startBlock(lNext); + } if (preserveRefs) { B.line( `call void @scr_dyn_check_fail(ptr %path, ptr ${host.cstr("reference with its original layout")}, ptr %d)`, @@ -2671,6 +3227,79 @@ export class LlDyn { return name; } + /** `i1`: %d is a handle box with tag `tag`. */ + private handleTagMatches(B: BlockBuilder, tag: number): string { + const kd = this.kindOf(B, "%d"); + const isHandle = B.tmp(); + B.line(`${isHandle} = icmp eq i32 ${kd}, ${DYN_KIND.HANDLE}`); + const lHandle = B.newLabel("dm.handle"); + const lDone = B.newLabel("dm.handle.done"); + const entry = B.currentLabel(); + B.condBr(isHandle, lHandle, lDone); + B.startBlock(lHandle); + const tagPtr = B.tmp(); + const loaded = B.tmp(); + B.line( + `${tagPtr} = getelementptr inbounds i8, ptr %d, i64 ${this.abiOffset(24, 20)} ; ->v.handle.tag`, + ); + B.line(`${loaded} = load i32, ptr ${tagPtr}`); + const same = B.tmp(); + B.line(`${same} = icmp eq i32 ${loaded}, ${tag}`); + B.br(lDone); + B.startBlock(lDone); + const matched = B.tmp(); + B.line(`${matched} = phi i1 [ false, %${entry} ], [ ${same}, %${lHandle} ]`); + return matched; + } + + /** A TextEncoder, TextDecoder or StringDecoder record boxes as its + * handle (+1), which holds the record by reference. The decoders pass + * the byte offsets of the fields their boxed methods read. */ + private textCodecToDyn( + B: BlockBuilder, + t: IrType & { kind: "record" }, + shape: IrRecordShape, + codec: "TextEncoder" | "TextDecoder" | "StringDecoder", + ): void { + const host = this.host; + const rc = vAdapters(host, t); + const r = B.tmp(); + if (codec === "TextEncoder") { + host.declare(`declare ptr @scr_text_encoder_to_dyn(ptr, ptr, ptr)`); + B.line( + `${r} = call ptr @scr_text_encoder_to_dyn(ptr %v, ptr ${rc.retain}, ptr ${rc.release})`, + ); + } else { + const struct = mangleRecordStruct(t.shapeId); + const base = host.nullableUnions.fieldBase(t.shapeId); + const start = B.tmp(); + B.line(`${start} = ptrtoint ptr %v to ${this.S}`); + const fields: readonly string[] = + codec === "StringDecoder" ? STRING_DECODER_FIELDS : TEXT_DECODER_FIELDS; + const offsets = fields.map((field) => { + const index = shape.fields.findIndex((f) => f.name === field); + if (index < 0) + throw new InternalCompilerError(`llvm emitter bug: ${codec} record lacks ${field}`); + const p = B.tmp(); + const at = B.tmp(); + const offset = B.tmp(); + B.line( + `${p} = getelementptr inbounds %${struct}, ptr %v, i64 0, i32 ${index + base} ; .${llvmCommentText(field)}`, + ); + B.line(`${at} = ptrtoint ptr ${p} to ${this.S}`); + B.line(`${offset} = sub ${this.S} ${at}, ${start}`); + return `${this.S} ${offset}`; + }); + const fn = + codec === "StringDecoder" ? "scr_string_decoder_to_dyn" : "scr_text_decoder_to_dyn"; + host.declare(`declare ptr @${fn}(ptr, ptr, ptr, ${fields.map(() => this.S).join(", ")})`); + B.line( + `${r} = call ptr @${fn}(ptr %v, ptr ${rc.retain}, ptr ${rc.release}, ${offsets.join(", ")})`, + ); + } + B.terminate(`ret ptr ${r}`); + } + /* ── toDynHelper (walkers.ts, ported) ─────────────────────────── */ /** `sc_td_( v) -> ptr` — build a fresh dyn value from a @@ -2688,11 +3317,10 @@ export class LlDyn { let sourceAccessor: { name: string; release: string } | null = null; switch (t.kind) { case "date": { - // A Date handle holding the same time value; native Dates are - // immutable values, and so is the handle. - host.declare(`declare ptr @scr_dyn_native_date_from_ms(double)`); + // A Date handle holding the same Date object. + host.declare(`declare ptr @scr_dyn_native_date(ptr)`); const r = B.tmp(); - B.line(`${r} = call ptr @scr_dyn_native_date_from_ms(double %v)`); + B.line(`${r} = call ptr @scr_dyn_native_date(ptr %v)`); B.terminate(`ret ptr ${r}`); break; } @@ -2846,6 +3474,11 @@ export class LlDyn { const shape = host.recordsById.get(t.shapeId); if (!shape) throw new InternalCompilerError(`llvm emitter bug: to-dyn of unknown shape ${t.shapeId}`); + const codec = recordHandleCodecClass(shape); + if (codec !== null) { + this.textCodecToDyn(B, t, shape, codec); + break; + } const struct = mangleRecordStruct(t.shapeId); const base = host.nullableUnions.fieldBase(t.shapeId); const fieldIndex = new Map(shape.fields.map((f, i) => [f.name, i + base])); @@ -3134,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})`); diff --git a/packages/compiler/src/backend/llvm/emit-function.ts b/packages/compiler/src/backend/llvm/emit-function.ts index b6df7bcca..24e1d06a3 100644 --- a/packages/compiler/src/backend/llvm/emit-function.ts +++ b/packages/compiler/src/backend/llvm/emit-function.ts @@ -20,7 +20,12 @@ import { constructorClass } from "../../ir/final-fields.js"; import { findLoopArrayBorrows } from "./loop-array-borrows.js"; import { findCallArrayReads, findLocalArrayReads } from "./local-array-reads.js"; import { findScalarStringSlices, findScratchStringSlices } from "./string-slices.js"; -import { findMapReadLifetimes, findReadNumberMapWrites } from "./map-read-lifetimes.js"; +import { + findMapReadBorrows, + findMapReadLifetimes, + findReadNumberMapWrites, +} from "./map-read-lifetimes.js"; +import { borrowableMapGet } from "./expr-containers.js"; import { findMapEntryFusions } from "./map-entry-fusion.js"; import { findLocalStackUnions } from "./stack-unions.js"; import { findLocalStackObjects, findReassignedStackObjects } from "./stack-objects.js"; @@ -56,12 +61,17 @@ import { emitIntegerClone, emitIntegerDispatcher } from "./integer-clones.js"; import { beginFunctionLoops, LOOPS } from "./loop-state.js"; import { runningExtremumUpdates } from "./running-extrema.js"; import { findIntegerOrNaNLocals } from "./wrapping-sums.js"; +import { UNWIND_PERSONALITY, UNWIND_SLOT } from "./unwind-abi.js"; export function emitFunction(emitter: LlEmitter, fn: IrFunction): string { const B = new BlockBuilder(); emitter.B = B; emitter.currentFunctionName = fn.name; + emitter.currentUnwindAbi = emitter.unwindAbi.has(fn.name); + emitter.usesLandingPads = false; + emitter.aliasScopedVersions = 0; emitter.findInt32Allocations(fn); + emitter.speculativeLeaves = 0; emitter.currentTier = fn.tier; // Fiber switches restore the active exception cell before control // returns to a synchronous frame (emitPendingCheck). @@ -73,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 @@ -136,6 +147,7 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string { emitter.integerTempsEnabled = emitter.debug === null && !fn.async && !fn.generator; beginFunctionLoops(emitter, fn, numericFn); emitter.speculativeLoopsEnabled = LOOPS.of(emitter).countedLoopsEnabled; + emitter.loopCopiesEnabled = true; emitter.speculativeFunction = fn.name; emitter.speculation = null; emitter.speculationResume = null; @@ -147,6 +159,7 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string { constructorClass(fn.name) === null && emitter.finalFields.size > 0; emitter.integerViews.clear(); + emitter.orderedViews.clear(); emitter.fieldPointerTags.clear(); emitter.compressedFieldPtrs.clear(); emitter.integerArrayBindings.clear(); @@ -159,6 +172,7 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string { emitter.optionalArrayReads, emitter.referenceWrites, emitter.fnByName, + emitter.nodeIndex, ) : new Set(); emitter.localArrayReads = findLocalArrayReads( @@ -186,15 +200,27 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string { fn, emitter.callLifetimes, emitter.scalarStringSlices, - emitter.lentStringParameters, + emitter.lendingFacts, ) : new Set(); emitter.mapReadLifetimes = findMapReadLifetimes( fn, emitter.boxedUnionsById, emitter.callLifetimes, + emitter.referenceEffects, + ); + const stable = emitter.callLifetimes.bindings.get(fn.name); + emitter.borrowedMapGets = findMapReadBorrows( + fn, + emitter.referenceEffects, + (decl) => + decl.init !== null && + stable?.has(decl.localId) === true && + !emitter.mapReadLifetimes.locals.has(decl.localId) && + borrowableMapGet(emitter, decl.init), ); emitter.readNumberMapWrites = findReadNumberMapWrites(fn); + emitter.numberMapKeyIntegers = new Map(); emitter.mapEntryFusion = emitter.debug === null ? findMapEntryFusions(fn) : null; emitter.localStackUnions = findLocalStackUnions( fn, @@ -304,11 +330,7 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string { // ScrGen owns the frame), so its OUT slot address is fixed. let out: string | null = null; const yieldKind = fn.generator?.yieldT.kind; - if ( - !emitter.wasi && - fn.async !== true && - (yieldKind === "f64" || yieldKind === "date" || yieldKind === "bool") - ) { + if (!emitter.wasi && fn.async !== true && (yieldKind === "f64" || yieldKind === "bool")) { emitter.declare(`declare ptr @scr_gen_of_fiber(ptr)`); const gen = B.tmp(); B.line(`${gen} = call ptr @scr_gen_of_fiber(ptr ${self})`); @@ -328,7 +350,7 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string { const paramIds = new Set(fn.params.map((p) => p.localId)); if (emitter.debug === null) { - for (const id of findIntegerViews(numericFn)) { + for (const id of findIntegerViews(numericFn, emitter.nodeIndex)) { // An i32 slot is its own view. if (emitter.int32Locals.has(id)) continue; const slot = B.slot(); @@ -336,6 +358,15 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string { `${slot} = alloca i32 ; integer view ${emitter.numericLocals.get(id)!.name}`, ); emitter.integerViews.set(id, slot); + // A local of exact integers or NaN also keeps whether it is NaN, so + // its integer is usable without converting the double. + if (emitter.integerOrNaNLocals.has(id)) { + const ordered = B.slot(); + B.entryAllocas.push( + `${ordered} = alloca i1 ; ordered view ${emitter.numericLocals.get(id)!.name}`, + ); + emitter.orderedViews.set(id, ordered); + } } } for (const global of initializerBindings) { @@ -599,6 +630,8 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string { B.terminate(cleanup.terminator); } + if (emitter.usesLandingPads && emitter.currentUnwindAbi) + B.entryAllocas.push(`${UNWIND_SLOT} = alloca { ptr, i32 }`); if (emitter.logArgSlots > 0) { B.entryAllocas.push(`%logargs = alloca [${emitter.logArgSlots} x %ScrLogArg]`); } @@ -639,7 +672,10 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string { const debug = emitter.debugScope === null ? "" : ` !dbg ${emitter.debugScope}`; const symbol = borrowed ? mangleBorrowedFunction(fn.name) : mangleFunction(fn.name); emitter.privateGlobalSlots.clear(); - let body = `define internal ${ret} @${symbol}${int32Abi ? INT32_ABI_SUFFIX : ""}(${params.join(", ")}) ${attrs}${debug} { ; ${fn.name}\n${B.render()}\n}`; + // Landing pads (emitDirectCall) need the personality. + const personality = emitter.usesLandingPads ? ` ${UNWIND_PERSONALITY}` : ""; + if (emitter.usesLandingPads) emitter.declare(`declare i32 @__gcc_personality_v0(...)`); + let body = `define internal ${ret} @${symbol}${int32Abi ? INT32_ABI_SUFFIX : ""}(${params.join(", ")}) ${attrs}${personality}${debug} { ; ${fn.name}\n${B.render()}\n}`; if (int32Abi) body += "\n" + emitter.emitInt32AbiWrapper(fn, symbol, int32Abi); if (emitter.integerClones.members.has(fn.name)) { body += "\n" + emitIntegerClone(fn, emitter, emitter.stackChecks?.has(fn.name) === true); @@ -704,7 +740,6 @@ export function emitGenScaffolding(emitter: LlEmitter): string[] { tr.push(` br label %done ; void body: the done value is undefined (NONE)`); break; case "f64": - case "date": emitter.declare(`declare void @scr_gen_out_f64(ptr, double)`); tr.push(` call void @scr_gen_out_f64(ptr %g, double %r)`, ` br label %done`); break; @@ -1053,7 +1088,6 @@ export function emitAsyncAwaitCall( emitter.declare(`declare void @scr_await_void(ptr)`); return ` call void @scr_await_void(ptr ${promise})`; case "f64": - case "date": emitter.declare(`declare double @scr_await_f64(ptr)`); return ` ${dest} = call double @scr_await_f64(ptr ${promise})`; case "bool": @@ -1108,7 +1142,7 @@ export function emitAsyncAwaitCall( const kind = ret.kind === "void" ? "void" - : ret.kind === "f64" || ret.kind === "date" + : ret.kind === "f64" ? "f64" : ret.kind === "bool" ? "bool" @@ -1187,7 +1221,6 @@ export function emitWasiFulfill(emitter: LlEmitter, v: LlValue | null): void { case "void": return; case "f64": - case "date": emitter.declare(`declare void @scr_gen_out_f64(ptr, double)`); emitter.B.line(`call void @scr_gen_out_f64(ptr ${gen}, double ${v!.name})`); return; @@ -1212,7 +1245,6 @@ export function emitWasiFulfill(emitter: LlEmitter, v: LlValue | null): void { emitter.B.line(`call void @scr_promise_fulfill_void(ptr ${pr})`); break; case "f64": - case "date": emitter.declare(`declare void @scr_promise_fulfill_f64(ptr, double)`); emitter.B.line(`call void @scr_promise_fulfill_f64(ptr ${pr}, double ${v!.name})`); break; @@ -1315,8 +1347,10 @@ export function emitArgPackAndTrampolinePrologue( * code. */ export function parkingFunctions(emitter: LlEmitter): Set | null { if (emitter.wasi || emitter.mod.workers === true) return null; - for (const decl of emitter.decls) if (/@scr_test_sub\b/.test(decl)) return null; - for (const text of emitter.fnDefText.values()) if (/@scr_test_sub\b/.test(text)) return null; + const testSub = (text: string): boolean => + text.includes("@scr_test_sub") && /@scr_test_sub\b/.test(text); + for (const decl of emitter.decls) if (testSub(decl)) return null; + for (const text of emitter.fnDefText.values()) if (testSub(text)) return null; // Reverse mention edges, then propagate "may park" to every mentioner. const mentionedBy = new Map>(); const parks = new Set(); diff --git a/packages/compiler/src/backend/llvm/emit-journal.ts b/packages/compiler/src/backend/llvm/emit-journal.ts index ad89d2671..b2b3fc4cc 100644 --- a/packages/compiler/src/backend/llvm/emit-journal.ts +++ b/packages/compiler/src/backend/llvm/emit-journal.ts @@ -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, tags: ReadonlyMap, + debugIds: ReadonlyMap = new Map(), ): (text: string) => string { const prefixes = new Set(); 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 { + 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, diff --git a/packages/compiler/src/backend/llvm/emit-module.ts b/packages/compiler/src/backend/llvm/emit-module.ts index c16834910..368d10ad3 100644 --- a/packages/compiler/src/backend/llvm/emit-module.ts +++ b/packages/compiler/src/backend/llvm/emit-module.ts @@ -1,4 +1,5 @@ /* LlEmitter's module-level output: module parts, helper definitions, library declarations and FFI callback definitions. */ +import { loopUnrollMetadata } from "./periodic-guards.js"; import { relieveHeapPressure } from "../../heap-pressure.js"; import { emitWasmModules } from "./wasm-modules.js"; import { type LlEmitter, ffiCallbackDummyLl, llStrBytes } from "./emitter.js"; @@ -17,6 +18,7 @@ import { } from "../mangle.js"; import { type IrGlobal, + type IrNodeIslandModules, type IrType, RUNTIME_ERROR_CLASSES, RUNTIME_EVENT_CLASS, @@ -57,13 +59,18 @@ import { ffiNativeTypeLl, } from "./common.js"; import { literalCapWord, literalCapWord32 } from "./string-key-hash.js"; -import { hasLoneSurrogate, wtf8Bytes } from "../literals.js"; +import { hasLoneSurrogate, llvmBytes, wtf8Bytes } from "../literals.js"; +import { nodeIslandManifest } from "../../node-islands/modules.js"; import { undefinedArmTag } from "../../ir/analysis.js"; import { NULLABLE_ABSENT, NULLABLE_NULL } from "./nullable-unions.js"; import { literalEqualityDefinition } from "./string-literal-equality.js"; -import { STRING_READ_HELPERS, stringScanDecl } from "./string-reads.js"; +import { STRING_READ_HELPERS, stringScanDecl, stringScanInline } from "./string-reads.js"; 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 @@ -100,10 +107,10 @@ function emitFunctionsInChunks(emitter: LlEmitter, chunks: number): string[] { return fnDefs; } -/** Modules with fewer functions emit in one process: forking and merging - * would cost more than the workers save (SCRIPTC_EMIT_WORKER_FUNCTIONS - * lowers it, for tests). */ -function workerMinFunctions(): number { +/** Modules with fewer functions emit in one process, and compute their + * analyses in it: forking and merging would cost more than the workers save + * (SCRIPTC_EMIT_WORKER_FUNCTIONS lowers it, for tests). */ +export function workerMinFunctions(): number { const value = Number(process.env["SCRIPTC_EMIT_WORKER_FUNCTIONS"] ?? "2000"); return Number.isFinite(value) && value > 0 ? value : 2000; } @@ -132,6 +139,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)); @@ -191,16 +199,106 @@ 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, 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 = {${base} }`, + ...(emitter.usesCoroutines ? [`attributes #1 = {${asan} presplitcoroutine${cpu} }`] : []), + ...(hasNoInlineRecordClone ? [`attributes #2 = { noinline${asan}${cpu} }`] : []), + ...(emitter.cpuDispatch ? cpuDispatchAttributes(asan) : []), + ]; +} + +/** The group line written before each function's text. Functions from one + * source file share a group, which the LLVM helper keeps in the same + * partitions so that their calls to each other stay inside a partition + * (native/llvm-codegen/src/partition.h). A group is numbered by the first + * function of its file in the module's function order, which does not depend + * on the host. Functions without a source file get no group. */ +function partitionGroupLines(functions: readonly IrFunction[]): string[] { + const groups = new Map(); + return functions.map((fn) => { + const file = fn.loc.file; + if (file === "") return PARTITION_GROUP_END; + let group = groups.get(file); + if (group === undefined) groups.set(file, (group = groups.size)); + return `${PARTITION_GROUP_END} ${group}`; + }); +} + +/** 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-`). 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(); + 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) { @@ -332,6 +430,8 @@ export function emitModuleParts(emitter: LlEmitter): string[] { // Fetch-referencing programs register native fetch before main. // Runtime selection compiles scr_fetch.c on the same predicate. const usesFetch = runtimeFeatures.fetch; + // URL programs give the host parser the IDNA unit's ToASCII. + const usesUrlIdna = runtimeFeatures.urlIdna; const snapshotsTlsCa = runtimeFeatures.tls || runtimeFeatures.tlsCa; // The process verdict has the same precedence as the C reference // emitter: node:test owns the final status when present; otherwise an @@ -381,6 +481,11 @@ export function emitModuleParts(emitter: LlEmitter): string[] { emitter.cstr(emitter.mod.nodeIsland?.nodeVersion ?? ""), ] : null; + // The island module tree: its manifest, and its files unless they ship + // beside the executable (node-islands/modules.ts). + const nodeIslandModules = usesNodeIsland ? emitter.mod.nodeIsland?.modules : undefined; + const nodeIslandModulesCall = + nodeIslandModules === undefined ? null : nodeIslandModulesInstall(emitter, nodeIslandModules); if (emitter.ffiHasForeignCallback) emitter.declare(`declare void @scr_ffi_install()`); if (usesStream) emitter.declare(`declare void @scr_stream_install()`); if (usesNetLookup) emitter.declare(`declare void @scr_net_lookup_install()`); @@ -392,6 +497,7 @@ export function emitModuleParts(emitter: LlEmitter): string[] { if (usesHttp2) emitter.declare(`declare void @scr_http2_dyn_install()`); if (usesHttp) emitter.declare(`declare void @scr_http_dyn_install()`); if (usesFetch) emitter.declare(`declare void @scr_fetch_install()`); + if (usesUrlIdna) emitter.declare(`declare void @scr_url_idna_install()`); if (usesNodeTest) { emitter.declare(`declare i32 @scr_test_exit_code()`); @@ -557,6 +663,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)`); @@ -574,6 +681,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.`, @@ -642,7 +766,7 @@ export function emitModuleParts(emitter: LlEmitter): string[] { // class-object shape { rc, pre, post, ctor, name } — field reads on // builtin errors and classval loads GEP through these. `%ScrError = type { ${emitter.sizeType}, ptr, ptr, ptr, ptr, ptr, i8, i8, i8, i8, i8, ptr, ptr }`, - `%ScrClassObj = type { ${emitter.sizeType}, ${emitter.sizeType}, ${emitter.sizeType}, ptr, ptr, ${emitter.sizeType}, ${emitter.sizeType}, ptr, ptr }`, + `%ScrClassObj = type { ${emitter.sizeType}, ${emitter.sizeType}, ${emitter.sizeType}, ptr, ptr, ${emitter.sizeType}, ${emitter.sizeType}, ptr, ptr, ptr, ${emitter.sizeType} }`, // The runtime emitter prefix { rc, vt, reg, cls } — user subclasses // embed it (classes.ts), and bare-emitter GEPs address through it. `%ScrEmitter = type { ${emitter.sizeType}, ptr, ptr, ptr }`, @@ -732,6 +856,7 @@ export function emitModuleParts(emitter: LlEmitter): string[] { for (let site = 0; site < emitter.jsIcSiteCount; site++) out.push(`@sc_js_ic_${site} = internal ${tl}global [${JS_IC_WORDS} x i64] zeroinitializer`); } + if (nodeIslandModulesCall !== null) out.push(...nodeIslandModulesCall.globals); for (const table of emitter.constantNumericTables.values()) { const n = table.values.length; let tableTmp = 0; @@ -856,6 +981,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 @@ -898,19 +1024,26 @@ 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); - out.push(fnDefs[0] ?? ""); - for (let i = 1; i < fnDefs.length; i++) out.push("", fnDefs[i]!); - out.push(""); + 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. for (const line of emitWasmModules(emitter.mod, flushedDecls, emitter.wasmBoundsChecks)) out.push(line); @@ -982,12 +1115,11 @@ export function emitModuleParts(emitter: LlEmitter): string[] { // LIBRARY mode: no @main — the profile-declared external // symbols specified by the library IR instead. for (const line of emitter.emitLibDefs(globals, globalReleaseLines, stamps)) out.push(line); - out.push(...emitter.inlineRcTail(flushedDecls)); - out.push(`attributes #0 = { sanitize_address }`); - if (emitter.usesCoroutines) out.push(`attributes #1 = { sanitize_address presplitcoroutine }`); - if (hasNoInlineRecordClone) out.push(`attributes #2 = { noinline sanitize_address }`); + for (const line of emitter.inlineRcTail(flushedDecls)) out.push(line); + out.push(...functionAttributes(emitter, hasNoInlineRecordClone)); out.push(``); out.push(...emitter.fieldAliasMetadata()); + out.push(...loopUnrollMetadata(out)); if (emitter.debug !== null) out.push(emitter.debug.render()); return out; } @@ -995,6 +1127,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 @@ -1027,9 +1160,11 @@ export function emitModuleParts(emitter: LlEmitter): string[] { ` call void @scr_node_island_install(ptr ${nodeIslandConfig[0]}, ptr ${nodeIslandConfig[1]})`, ] : []), + ...(nodeIslandModulesCall === null ? [] : [nodeIslandModulesCall.call]), ...(emitter.ffiHasForeignCallback ? [` call void @scr_ffi_install()`] : []), ...(snapshotsTlsCa ? [` call void @scr_tls_ca_install()`] : []), ...(usesFetch ? [` call void @scr_fetch_install()`] : []), + ...(usesUrlIdna ? [` call void @scr_url_idna_install()`] : []), ...(usesNet ? [` call void @scr_net_install()`, ` call void @scr_net_dyn_install()`] : []), ...(usesNetLookup ? [` call void @scr_net_lookup_install()`] : []), ...(usesHttp ? [` call void @scr_http_dyn_install()`] : []), @@ -1134,13 +1269,13 @@ export function emitModuleParts(emitter: LlEmitter): string[] { : [` %native_exit = call i32 @scr_exit_code_hint_get()`, ` ret i32 %native_exit`]), `}`, ``, - ...emitter.inlineRcTail(flushedDecls), - // 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). - `attributes #0 = { sanitize_address }`, - ...(emitter.usesCoroutines ? [`attributes #1 = { sanitize_address presplitcoroutine }`] : []), - ...(hasNoInlineRecordClone ? [`attributes #2 = { noinline sanitize_address }`] : []), + ); + for (const line of emitter.inlineRcTail(flushedDecls)) out.push(line); + out.push( + // The sanitized lane's -fsanitize=address link instruments the + // functions marked sanitize_address (the runtime TUs get theirs from + // clang). + ...functionAttributes(emitter, hasNoInlineRecordClone), ``, ); if (emitter.mod.workers) { @@ -1157,6 +1292,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`, @@ -1164,13 +1301,161 @@ export function emitModuleParts(emitter: LlEmitter): string[] { ); } out.push(...emitter.fieldAliasMetadata()); + out.push(...loopUnrollMetadata(out)); if (emitter.debug !== null) out.push(emitter.debug.render()); 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. */ +/** The island module tree's install call before %main, and the constant + * that carries its files when the executable ships them itself + * (scr_node_island.c: scr_node_island_install_modules). */ +function nodeIslandModulesInstall( + emitter: LlEmitter, + modules: IrNodeIslandModules, +): { call: string; globals: string[] } { + emitter.declare( + `declare void @scr_node_island_install_modules(ptr, ${emitter.sizeType}, ptr, ${emitter.sizeType}, ptr)`, + ); + const manifest = nodeIslandManifest(modules); + const manifestPtr = emitter.cstr(manifest); + const location = emitter.cstr(modules.location ?? ""); + const data = modules.location === undefined ? emitter.nodeIslandModuleData : null; + const globals = + data === null || data.length === 0 + ? [] + : [ + `@sc_node_island_modules = private unnamed_addr constant [${data.length} x i8] c"${llvmBytes(data, false)}", align 16`, + ]; + const st = emitter.sizeType; + return { + call: ` call void @scr_node_island_install_modules(ptr ${manifestPtr}, ${st} ${Buffer.byteLength(manifest, "utf8")}, ptr ${globals.length > 0 ? "@sc_node_island_modules" : "null"}, ${st} ${globals.length > 0 ? data!.length : 0}, ptr ${location})`, + globals, + }; +} + export function helperDefs(emitter: LlEmitter): string[] { const defs: string[] = []; const msgHelper = (fnName: string, msgSym: string, msg: string): void => { @@ -1275,114 +1560,66 @@ export function helperDefs(emitter: LlEmitter): string[] { ` 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} {`, + // The same lookup for a key the emitter knows is an integer (`%ki`, + // the double `%k`'s exact value): the direct index tests the integer + // itself, so the double is converted only by the hashed probe. A + // negative key is not below the index's size as an unsigned value. + `define internal ptr @sc_map_entry_f64_int(ptr %m, i64 %ki, double %k) ${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`, + ` %ndp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 16`, + ` %nd32 = load i32, ptr %ndp`, + ` %nd = zext i32 %nd32 to i64`, + ` %inside = icmp ult i64 %ki, %nd`, + ` br i1 %inside, label %slot, label %outside`, + `slot:`, + sz === "i64" ? ` %i = add i64 %ki, 0` : ` %i = trunc i64 %ki to ${sz}`, + ` %dp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 15`, + ` %dense = load ptr, ptr %dp`, + ` %sp = getelementptr inbounds i32, ptr %dense, ${sz} %i`, + ` %s = load i32, ptr %sp`, + ` %empty = icmp eq i32 %s, 0`, + ` br i1 %empty, label %miss, label %hit`, + `hit:`, + ` %e32 = sub i32 %s, 1`, + sz === "i32" ? ` %e = add i32 %e32, 0` : ` %e = zext i32 %e32 to ${sz}`, + ` %ep = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 11`, + ` %entries = load ptr, ptr %ep`, + ` %found = getelementptr inbounds %ScrMapNumEntry, ptr %entries, ${sz} %e`, + ` ret ptr %found`, `miss:`, ` ret ptr null`, - `runtime:`, - ` %r = call ptr @scr_map_entry_f64(ptr %m, double %k) memory(read) nounwind willreturn`, - ` ret ptr %r`, + `outside:`, + ` %dense.any = icmp ne i64 %nd, 0`, + ` br i1 %dense.any, label %miss, label %slow`, + `slow:`, + ` %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 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 @@ -1591,9 +1828,7 @@ export function helperDefs(emitter: LlEmitter): string[] { // A scan's receiver reads inline too (ScrStrScan): from ordinary global // storage, or, in worker executables, from the thread's static TLS block, // one thread-pointer-relative load (see executableTls). - const scanInline = - emitter.executableTls || - (emitter.mod.workers !== true && emitter.mod.lib?.threadInstances !== true); + const scanInline = stringScanInline(emitter); let scanDeclared = false; for (const helper of STRING_READ_HELPERS) { if (!emitter.decls.has(helper.decl)) continue; @@ -1617,8 +1852,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"; @@ -1748,7 +1983,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)`, diff --git a/packages/compiler/src/backend/llvm/emit-statements.ts b/packages/compiler/src/backend/llvm/emit-statements.ts index c2846a9d4..45d341853 100644 --- a/packages/compiler/src/backend/llvm/emit-statements.ts +++ b/packages/compiler/src/backend/llvm/emit-statements.ts @@ -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, @@ -32,7 +34,7 @@ import { allocateLocalUnion, storeLocalUnion } from "./local-union-storage.js"; import { boxAccess, boxNewCall, elemAccess } from "./shapes.js"; import { dynHoleRead } from "./loop-array-borrows.js"; import { endsWithJump, matchStringSelfConcat } from "../../ir/analysis.js"; -import { charAtUnitRead, emitDiscardedSplice } from "./expr-containers.js"; +import { charAtUnitRead, emitBorrowedMapGet, emitDiscardedSplice } from "./expr-containers.js"; import { InternalCompilerError } from "../../errors.js"; import { emitDenseArrayGet, @@ -50,15 +52,23 @@ import { matchIntegerBytesForLoop, matchIntegerCountedForLoop, matchIntegerPairedForLoop, + steppedLoopCounter, } from "../../ir/integer-loops.js"; import { emitByteWindowGuard, matchByteWindow } from "./byte-windows.js"; -import { countedDenseShape, emitDenseLoopGuard, matchDenseArrayLoop } from "./dense-array-loops.js"; +import { + countedDenseShape, + emitDenseLoopGuard, + matchDenseArrayLoop, + pairedDenseShape, +} from "./dense-array-loops.js"; import { emitCountedLoopLimit, emitCountedLoopSafety, + emitCountedStart, emitPairedLoopSafety, } from "./counted-loops.js"; import { emitBytesLoopVersionGuard, type BytesLoopVersion } from "./bytes-loop-versions.js"; +import { bytesHeaderAlias } from "./bytes-alias.js"; import { mangleLocal } from "../mangle.js"; import { LlvmUnsupportedError } from "./unsupported.js"; import type { LlValue } from "./expr-context.js"; @@ -73,6 +83,7 @@ import { planProvenIntegerLoop, planSpeculativeLoop, type ProvenLoopPlan, + type SpeculativeLoopPlan, speculativeReads, speculativeWriteTarget, } from "./speculative-integers.js"; @@ -84,6 +95,9 @@ import { } from "./string-construction.js"; import { emitStringInputs } from "./string-lifetimes.js"; import { LOOPS } from "./loop-state.js"; +import { loopUnrollAttachment, periodicGuardPeriod } from "./periodic-guards.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; @@ -98,6 +112,19 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { break; } const b = emitter.binding(s.localId); + if ( + emitter.borrowedMapGets.has(s.localId) && + b.kind === "local" && + s.init?.kind === "mapIntrinsic" + ) { + // The map entry's own reference: no retain now, no release at scope + // exit (findMapReadBorrows). + const borrowed = emitBorrowedMapGet(emitter, s.init); + if (borrowed) { + B.line(`store ptr ${borrowed.name}, ptr ${b.slot}`); + break; + } + } if (emitter.walkBorrows.has(s.localId) && b.kind === "local") { // A borrowed walk pointer: no retain now, no release at scope exit. const v = s.init === null ? "null" : emitter.emitReadReceiver(s.init).name; @@ -458,6 +485,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") @@ -551,6 +581,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") @@ -564,6 +595,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { s.value, emitter.bytesBounds.has(s), emitter.bytesOwned.has(s), + emitter.bytesAliasScopes.get(s), ); break; } @@ -811,6 +843,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"); @@ -966,7 +1005,9 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { } : countedLoop && !countedLoop.guarded && countedLoop.followers.length === 0 ? countedDenseShape(s, countedLoop) - : null; + : pairedLoop + ? pairedDenseShape(pairedLoop) + : null; const dense = denseShape && !window ? matchDenseArrayLoop( @@ -986,7 +1027,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { LOOPS.of(emitter).denseLoopsEnabled = false; try { B.startBlock(slow); - emitter.emitStmt(s); + emitSlowCopy(emitter, s); B.br(done); B.startBlock(fast); LOOPS.of(emitter).denseLoop = dense; @@ -1010,7 +1051,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { LOOPS.of(emitter).byteWindowsEnabled = false; try { B.startBlock(slow); - emitter.emitStmt(s); + emitSlowCopy(emitter, s); B.br(done); B.startBlock(fast); emitter.integerRanges = window.ranges; @@ -1043,15 +1084,17 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { LOOPS.of(emitter).unaliasedArrays, ) : countedLoop; - const version = versionable ? emitter.matchBytesLoopVersion(s, versionCounted) : null; + const version = versionable + ? emitter.matchBytesLoopVersion(s, versionCounted, versionCounted ? null : pairedLoop) + : null; if (version) { emitVersionedLoop(emitter, s, version); break; } let countedJoin: string | null = null; if ( - (countedLoop?.guarded && !LOOPS.of(emitter).provenCountedGuards.has(s)) || - pairedLoop?.guarded + (countedLoop?.guarded || pairedLoop?.guarded) && + !LOOPS.of(emitter).provenCountedGuards.has(s) ) { const safe = countedLoop ? emitCountedLoopSafety(emitter, countedLoop) @@ -1062,7 +1105,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { B.condBr(safe, fast, slow); B.startBlock(slow); LOOPS.of(emitter).countedLoopsEnabled = false; - emitter.emitStmt(s); + emitSlowCopy(emitter, s); LOOPS.of(emitter).countedLoopsEnabled = true; B.br(countedJoin); B.startBlock(fast); @@ -1102,9 +1145,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { B.entryAllocas.push( `${integerSlot} = alloca i64 ; integer induction ${emitter.currentLocals.get(countedLoop.localId)!.name}`, ); - const startNumber = emitter.emitExpr(countedLoop.start); - const start = B.tmp(); - B.line(`${start} = fptosi double ${startNumber.name} to i64`); + const start = emitCountedStart(emitter, countedLoop.start); B.line(`store i64 ${start}, ptr ${integerSlot}`); emitter.integerLoopBindings.set(countedLoop.localId, { slot: integerSlot, @@ -1120,9 +1161,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { B.entryAllocas.push( `${slot} = alloca i64 ; integer induction ${emitter.currentLocals.get(follower.localId)!.name}`, ); - const value = emitter.emitExpr(follower.start); - const followerStart = B.tmp(); - B.line(`${followerStart} = fptosi double ${value.name} to i64`); + const followerStart = emitCountedStart(emitter, follower.start); B.line(`store i64 ${followerStart}, ptr ${slot}`); followerSlots.push(slot); emitter.integerLoopBindings.set(follower.localId, { @@ -1139,16 +1178,18 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { B.entryAllocas.push( `${slot} = alloca i64 ; integer induction ${emitter.currentLocals.get(counter.localId)!.name}`, ); - const value = emitter.emitExpr(counter.start); - const start = B.tmp(); - B.line(`${start} = fptosi double ${value.name} to i64`); + const start = emitCountedStart(emitter, counter.start); B.line(`store i64 ${start}, ptr ${slot}`); pairedSlots.push(slot); + const dense = LOOPS.of(emitter).denseLoop; emitter.integerLoopBindings.set(counter.localId, { slot, type: "i64", signed: true, - range: pairedLoop.range, + range: + dense?.pair && dense.counter === pairedLoop.low.localId + ? dense.range + : pairedLoop.range, }); } } else if (s.init) { @@ -1185,13 +1226,103 @@ 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 = s.update || capturedLets.length > 0 || workerBudget !== null ? B.newLabel("loop.u") : lc; + // A periodic guard on the counter (periodic-guards.ts): the back + // edge asks LLVM to unroll the loop by the guard's period. + const period = + countedLoop && integerSlot + ? periodicGuardPeriod(s, countedLoop.localId, countedLoop.start, countedLoop.step) + : null; + const backEdge = period === null ? "" : loopUnrollAttachment(period); B.br(lc); B.startBlock(lc); enterLoopHeader(emitter, lc); @@ -1218,10 +1349,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { } B.line(`${inBounds} = icmp ult i32 ${narrow}, ${len}`); } else { - emitter.markMemoryPointer(lenPtr, "bytes:header"); - B.line( - `${len} = load ${emitter.sizeType}, ptr ${lenPtr}${emitter.fieldAliasAttachment(lenPtr)}`, - ); + B.line(`${len} = load ${emitter.sizeType}, ptr ${lenPtr}${bytesHeaderAlias(emitter)}`); B.line(`${inBounds} = icmp ult ${emitter.sizeType} ${index}, ${len}`); } B.condBr(inBounds, lb, exit); @@ -1255,7 +1383,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { }); emitter.emitBlock(s.body); emitter.jumpTargets.pop(); - B.br(lu); + B.br(lu, lu === lc ? backEdge : ""); if (lu !== lc) { B.startBlock(lu); emitter.emitWorkerLoopCheck(workerBudget); @@ -1288,7 +1416,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { B.line(`store i64 ${next}, ptr ${pairedSlots[i]}`); }); } else if (s.update) emitter.emitStmt(s.update); - B.br(lc); + B.br(lc, backEdge); } }); B.startBlock(le); @@ -2038,7 +2166,7 @@ function emitCallbackVersionedLoop( emitter.callbackVersioning.add(s); try { B.startBlock(slow); - emitter.emitStmt(s); + emitSlowCopy(emitter, s); B.br(done); B.startBlock(fast); for (const { ref } of bindings) emitter.knownCallbacks.add(ref.localId); @@ -2051,17 +2179,74 @@ function emitCallbackVersionedLoop( B.startBlock(done); } +/** The copy of a versioned loop that runs when its guard fails. Its loops + * get no speculative or int32 kind copies, which would emit each loop again + * for a path that rarely runs. Proven integer loops, which emit one copy, + * are kept. A typed-array version whose guard requires thread-owned + * storage does not use it (emitVersionedLoop). */ +function emitSlowCopy(emitter: LlEmitter, s: IrStmt): void { + const copies = emitter.loopCopiesEnabled; + emitter.loopCopiesEnabled = false; + try { + emitter.emitStmt(s); + } finally { + emitter.loopCopiesEnabled = copies; + } +} + /** Entries in a row whose fast copy bailed before a speculative loop site * stops trying it. */ const SPECULATION_BAILS = 8; +/** The attachment of each scoped access in a version's fast copy: the + * scope of its own view, and no alias with the scopes of the other views, + * whose bytes the guard proved disjoint. The scopes are named after the + * function and the version, so every copy of a loop has its own, and + * written inline (string-named scopes need no numbered metadata). Also + * the scopes, which each fast copy declares on entry (declareAliasScopes). */ +function bytesAliasScopes( + emitter: LlEmitter, + scopes: NonNullable, +): { attachments: Map; scopes: string[] } { + const version = emitter.aliasScopedVersions++; + const name = emitter.currentFunctionName.replace(/[^A-Za-z0-9_.$%]/g, "_"); + const domain = '!{!"scriptc typed-array views"}'; + const scope = (receiver: number): string => `!{!"${name}.${version}.${receiver}", ${domain}}`; + const attachments = new Map(); + for (const receiver of scopes.receivers) { + const others = scopes.receivers.filter((other) => other !== receiver).map(scope); + attachments.set( + receiver, + `, !alias.scope !{${scope(receiver)}}, !noalias !{${others.join(", ")}}`, + ); + } + const result = new Map(); + for (const [node, receiver] of scopes.nodes) result.set(node, attachments.get(receiver)!); + return { attachments: result, scopes: scopes.receivers.map(scope) }; +} + +/** Declares a version's scopes where a fast copy starts. The guard proves + * the views disjoint for this entry only: an enclosing loop may pass other + * views on its next pass (`[cur, next] = [next, cur]`). A declared scope + * holds from its declaration, and LLVM gives each copy of the declaration + * its own scopes when it unrolls or duplicates the enclosing code, so the + * no-alias facts of one entry never apply to the accesses of another. */ +function declareAliasScopes(emitter: LlEmitter, scopes: readonly string[]): void { + if (scopes.length === 0) return; + emitter.declare("declare void @llvm.experimental.noalias.scope.decl(metadata)"); + for (const scope of scopes) + emitter.B.line(`call void @llvm.experimental.noalias.scope.decl(metadata !{${scope}})`); +} + /** Emit a loop's condition, body and update through `emitCopy`, twice * when speculative-integers.ts plans a fast integer copy. Both copies * share the loop's setup and exit; the ordinary copy comes first so the * 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" }, @@ -2071,27 +2256,83 @@ function emitVersionedLoop( const fast = B.newLabel("bytes.version.fast"), slow = B.newLabel("bytes.version.slow"), done = B.newLabel("bytes.version.done"); - emitBytesLoopVersionGuard(emitter, version, fast, slow); + const unitsFast = + version.unitsReceiver !== null ? B.newLabel("bytes.version.fast.units") : undefined; + 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, - originalRanges = emitter.integerRanges; + originalRanges = emitter.integerRanges, + originalUnitReads = emitter.stringUnitReads, + originalLengthReads = emitter.stringLengthReads, + originalScopes = emitter.bytesAliasScopes; LOOPS.of(emitter).bytesLoopVersioning.add(s); try { B.startBlock(slow); - emitter.emitStmt(s); + // A guard that requires thread-owned storage fails for every view of + // shared memory, which worker programs pass routinely: that checked + // copy keeps its loops' speculative and int32 kind copies. + if (version.ownedReceivers.length > 0) emitter.emitStmt(s); + else emitSlowCopy(emitter, s); B.br(done); - B.startBlock(fast); - emitter.bytesBounds = new Set([...originalBounds, ...version.bounds]); if (version.owned.size > 0) emitter.bytesOwned = new Set([...originalOwned, ...version.owned]); if (version.ranges.size > 0) emitter.integerRanges = new Map([...originalRanges, ...version.ranges]); - if (version.counted?.guarded) LOOPS.of(emitter).provenCountedGuards.add(s); + // Both fast copies run under the guard's span test, so they share the + // version's scopes, and each declares them on entry. + const scoped = + version.scopes && emitter.debug === null ? bytesAliasScopes(emitter, version.scopes) : null; + if (scoped) emitter.bytesAliasScopes = new Map([...originalScopes, ...scoped.attachments]); + if (version.counted?.guarded || version.paired?.loop.guarded) + LOOPS.of(emitter).provenCountedGuards.add(s); + B.startBlock(fast); + declareAliasScopes(emitter, scoped?.scopes ?? []); + emitter.bytesBounds = new Set([...originalBounds, ...version.bounds]); + // Both fast copies read the string's length the guard took. + const length = guard?.length; + if (length != null && version.unitLengths.size > 0) + emitter.stringLengthReads = new Map([ + ...originalLengthReads, + ...[...version.unitLengths].map((node) => [node, length] as const), + ]); 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); + declareAliasScopes(emitter, scoped?.scopes ?? []); + emitter.bytesBounds = new Set([ + ...originalBounds, + ...[...version.bounds].filter((node) => !version.stringReads.has(node)), + ]); + emitter.stringUnitReads = new Map([ + ...originalUnitReads, + ...[...version.stringReads].map((node) => [node, units] as const), + ]); + emitter.emitStmt(s); + B.br(done); + } } finally { emitter.bytesBounds = originalBounds; emitter.bytesOwned = originalOwned; emitter.integerRanges = originalRanges; + emitter.stringUnitReads = originalUnitReads; + emitter.stringLengthReads = originalLengthReads; + emitter.bytesAliasScopes = originalScopes; LOOPS.of(emitter).provenCountedGuards.delete(s); LOOPS.of(emitter).bytesLoopVersioning.delete(s); } @@ -2108,6 +2349,7 @@ export function emitLoopCopies( const plan = allowed && emitter.speculativeLoopsEnabled && + emitter.loopCopiesEnabled && emitter.speculation === null && emitter.speculationResume === null ? planSpeculativeLoop( @@ -2124,6 +2366,7 @@ export function emitLoopCopies( ); }, (id) => emitter.speculativeLocal(id) || emitter.speculativeInvariantGlobal(id), + emitter.speculationContext(), ) : null; if (!plan) { @@ -2135,6 +2378,7 @@ export function emitLoopCopies( const kinds = allowed && emitter.speculativeLoopsEnabled && + emitter.loopCopiesEnabled && emitter.int32Slots.checkedInt32Arrays.size > 0 && emitter.int32Kinds === null && emitter.speculation === null && @@ -2149,6 +2393,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(); let ok = "true"; @@ -2181,6 +2441,20 @@ export function emitLoopCopies( B.line(`${both} = and i1 ${ok}, ${trying}`); ok = both; } + // Hoisted leaves live in locals of their own, which the resume copy of + // their write reads (see SpeculativeValue). + for (const { leaves } of plan.values.values()) + for (const leaf of leaves) { + emitter.currentLocals.set(leaf.localId, { + id: leaf.localId, + name: "speculated leaf", + type: leaf.expr.type, + mutable: true, + }); + B.entryAllocas.push( + `${emitter.binding(leaf.localId).slot} = alloca double ; speculated leaf`, + ); + } const fast = B.newLabel("spec.fast"), slow = B.newLabel("spec.slow"); B.condBr(ok, fast, slow); @@ -2206,6 +2480,7 @@ export function emitLoopCopies( throw new InternalCompilerError("llvm emitter bug: speculative loop copy without a header"); B.startBlock(B.newLabel("spec.tail")); emitter.speculationResume = resume; + emitter.speculationResumeValues = plan.values; emitter.speculationHeader = () => { B.br(header!); B.startBlock(B.newLabel("spec.unused")); @@ -2214,6 +2489,7 @@ export function emitLoopCopies( emitCopy(exit); } finally { emitter.speculationResume = null; + emitter.speculationResumeValues = null; emitter.speculationHeader = null; } B.startBlock(fast); @@ -2262,6 +2538,7 @@ export function emitLoopCopies( deferred, bails, ran, + values: plan.values, }; try { const leave = B.newLabel("spec.leave"); @@ -2409,6 +2686,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, + 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; @@ -2425,7 +2926,7 @@ function planProvenLoop( loop: Extract, ): ProvenLoopPlan | null { if ( - loop.kind !== "for" || + loop.kind === "doWhile" || !emitter.speculativeLoopsEnabled || emitter.speculation !== null || emitter.speculationResume !== null || @@ -2519,9 +3020,22 @@ export function emitSpeculativeWrite( // at once: a bailing path then owns nothing, and reads have no effect // whose reordering before the arithmetic could be observed. const reads = new Map(); - if (target.value !== null) { + const rewritten = emitter.speculation!.values?.get(s); + const value = rewritten?.value ?? target.value; + if (rewritten !== undefined) { + // Hoisted leaves run first, in the write's order, and keep their + // doubles for the resume copy. emitter.frames.push([]); - for (const read of speculativeReads(target.value)) reads.set(read, emitter.emitExpr(read)); + for (const leaf of rewritten.leaves) { + const evaluated = emitter.emitExpr(leaf.expr); + B.line(`store double ${evaluated.name}, ptr ${emitter.binding(leaf.localId).slot}`); + reads.set(leaf.ref, evaluated); + } + emitter.releaseTemps(emitter.frames.pop()!); + } + if (value !== null) { + emitter.frames.push([]); + for (const read of speculativeReads(value)) reads.set(read, emitter.emitExpr(read)); emitter.releaseTemps(emitter.frames.pop()!); } emitter.frames.push([]); @@ -2536,7 +3050,7 @@ export function emitSpeculativeWrite( B.line(`${biased} = add i64 ${result}, ${Number.MAX_SAFE_INTEGER}`); B.line(`${outside} = icmp ugt i64 ${biased}, ${2 * Number.MAX_SAFE_INTEGER}`); bail(outside); - } else result = emitSpeculativeInteger(emitter, target.value, bail, reads, resume === undefined); + } else result = emitSpeculativeInteger(emitter, value!, bail, reads, resume === undefined); // Speculable values read only numbers past this point. if (emitter.frames.pop()!.length > 0) throw new InternalCompilerError("llvm emitter bug: speculative write owns a temporary"); diff --git a/packages/compiler/src/backend/llvm/emitter.test.ts b/packages/compiler/src/backend/llvm/emitter.test.ts index 4793663aa..880685bf2 100644 --- a/packages/compiler/src/backend/llvm/emitter.test.ts +++ b/packages/compiler/src/backend/llvm/emitter.test.ts @@ -1,7 +1,6 @@ import { expect, test } from "vitest"; import { BOOL, - DATE_T, F64, PROCSTREAM_T, STRING, @@ -202,6 +201,29 @@ test("detached exit defines the runtime opt-in symbol only when requested", () = expect(detached.replace("@scr_detached_exit = constant i8 1\n", "")).toBe(plain); }); +test("a target CPU level reaches every function attribute group and nothing else", () => { + const module = moduleFor([call()]); + const plain = emitLlvmModule(module); + expect(plain).not.toContain("target-cpu"); + const v3 = emitLlvmModule(module, { targetCpu: "x86-64-v3" }); + expect(v3).toMatch(/^attributes #0 = \{ sanitize_address "target-cpu"="x86-64-v3" \}$/m); + expect(v3.replaceAll(' "target-cpu"="x86-64-v3"', "")).toBe(plain); + 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 }); @@ -287,7 +309,7 @@ test("shared exceptional exits preserve the function's scalar return ABI", () => }); test("exceptional exits use each scalar kind's LLVM return type", () => { - for (const type of [F64, DATE_T, BOOL, PROCSTREAM_T]) { + for (const type of [F64, BOOL, PROCSTREAM_T]) { for (const ownedLocals of [false, true]) { const module = moduleFor([call(), call(), { kind: "throw", value: literal("failed"), loc }]); const work = module.functions[1]!; @@ -499,6 +521,27 @@ test("class brand probes compare the program's key constant before the key text" const named = work(probe("dyn.typedRefIs"), dyn); expect(named).toContain("; ->v.typed_ref.type_key"); expect(named).toContain("@scr_dyn_typed_ref_is_key"); + // Another key pointer is rejected on its length and last eight bytes + // ("Example" plus the prefix's ':') before the out-of-line comparison. + expect(named).toContain("; ->v.typed_ref.type_key_len"); + expect(named).toContain("and i32 %t"); + expect(named).toMatch(/icmp eq i32 %t\d+, 14\b/); + const tail = Buffer.from("object:Example").readBigInt64LE(6); + expect(named).toContain(`icmp eq i64 %t`); + expect(named).toContain(`, ${tail}`); + const shortKey: IrExpr = { kind: "strLit", value: "o:E", type: STRING, loc: receiverLoc }; + const short = work( + { + kind: "libCall", + fn: "dyn.typedRefIs", + args: [ref(dyn), shortKey], + type: BOOL, + loc: receiverLoc, + }, + dyn, + ); + expect(short).toMatch(/icmp eq i32 %t\d+, 3\b/); + expect(short).not.toContain("load i64"); // The key-pointer test answers false for capsules boxed elsewhere. const exact = work(probe("dyn.typedRefKeyIs"), dyn); expect(exact).toContain("; ->v.typed_ref.type_key"); diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 050c3fb90..f34a9330c 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -1,3 +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"; @@ -16,6 +18,7 @@ import { import { youngNurseryRequested } from "../young-nursery.js"; import { llvmBytes as llBytes, wtf8Bytes } from "../literals.js"; import { InternalCompilerError } from "../../errors.js"; +import { serializeModuleParts } from "../../ir/serialize.js"; import { createHash } from "node:crypto"; import { endsWithJump, @@ -54,21 +57,23 @@ import { typeKey, VOID, classJsName, + classValueAbiEquals, } from "../../ir/ir.js"; import { moduleRuntimeFeatures, type RuntimeFeatures } from "../../ir/runtime-features.js"; 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 { IntegerCountedForLoop } from "../../ir/integer-loops.js"; +import type { BytesHeaderScope } from "./bytes-headers.js"; +import type { IntegerCountedForLoop, IntegerPairedForLoop } from "../../ir/integer-loops.js"; import { materializeSplitPiece, type SplitSpan, type StoredSplitSnapshot, type StringSplit, } from "./split-loops.js"; -import { scalarizeNumericRecords } from "../../ir/scalar-records.js"; -import { addRecordResultEntries } from "./record-results.js"; +import { scalarizeLiteralResults, scalarizeNumericRecords } from "../../ir/scalar-records.js"; +import { addRecordResultEntries, resultEntryName } from "./record-results.js"; import { specializeCallbackCalls } from "../../ir/callback-specialization.js"; import { type ChunkResult, @@ -79,10 +84,15 @@ import { } from "./emit-journal.js"; import { specializeNumericCalls } from "../../ir/numeric-call-specialization.js"; import { versionLoopBoundParameters } from "../../ir/parameter-versions.js"; +import { inlineExpressionFunctions } from "../../ir/expression-inlining.js"; +import { pairRowSums } from "../../ir/row-pairs.js"; import { allocatedLiteral, fillAllocationLength, + int32ArrayKey, selectInt32Arrays, + type Int32ArrayProof, + type Int32ArrayRevision, } from "../../ir/int32-arrays.js"; import { everyExpr, everyExprChild, everyStmtChild, everyStmtList } from "../../ir/traverse.js"; import { @@ -95,13 +105,21 @@ import { unshareNodes } from "../../ir/unshare.js"; import { runPairedWhileLoopsAsFor } from "../../ir/paired-while-loops.js"; import { copyStableCaptures } from "../../ir/capture-values.js"; import { resolveSelfCaptures } from "../../ir/self-captures.js"; -import { analyzeInt32Slots, dynamicFieldStores, type Int32Slots } from "../../ir/int32-slots.js"; +import { + analyzeInt32Slots, + dynamicFieldStores, + Int32RevisionConflict, + type DynamicFieldStores, + Int32Slots, +} from "../../ir/int32-slots.js"; import { analyzeFinalFields, type FinalFields } from "../../ir/final-fields.js"; import { foldConstantGlobals } from "../../ir/constant-globals.js"; import { dropUnwrittenPropertyBags } from "../../ir/property-bags.js"; import { freezeInitOnlyGlobals } from "../../ir/init-only-globals.js"; import { hoistFinalFieldReads } from "../../ir/field-values.js"; import { holdCharLocalsAsUnits } from "../../ir/char-unit-locals.js"; +import { unitSafeFunctions } from "./string-unit-versions.js"; +import { stringScanInline } from "./string-reads.js"; import { findInt32Abis, INT32_ABI_SUFFIX, @@ -111,7 +129,7 @@ import { } from "./int32-abi.js"; import { type Int32Local } from "./int32-locals.js"; import { findIntegerClones, NO_INTEGER_CLONES, type IntegerClones } from "./integer-clones.js"; -import { exactInteger, narrowInteger, widenInteger } from "./integer-values.js"; +import { exactInteger, narrowInteger, widenInteger, type LlInteger } from "./integer-values.js"; import { findInitializerBindings } from "../../ir/initializer-bindings.js"; import { findConstantNumericTables, type ConstantNumericTable } from "../../ir/constant-tables.js"; import { @@ -121,6 +139,17 @@ import { type FfiCallbackAdapter, } from "../ffi-callbacks.js"; import { computeMayThrow } from "../may-throw.js"; +import { + UNWIND_PERSONALITY, + UNWIND_RAISE, + UNWIND_RAISE_DECLARATION, + UNWIND_RESUME, + UNWIND_SLOT, + UnwindAbiViolation, + bridgePlainUnwindCalls, + unwindAbiFunctions, +} from "./unwind-abi.js"; +import { cloneForCpuDispatch } from "./cpu-dispatch.js"; import { mangleAsyncInlineAwaitCall, mangleAsyncSpawn, @@ -157,35 +186,43 @@ 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"; import { findWalkBorrows, type WalkBorrowHost } from "./walk-borrows.js"; -import { IrNodeIndex } from "../../ir/node-index.js"; +import { eachNodeOfKinds, IrNodeIndex, type IrNodeKind } from "../../ir/node-index.js"; import { lastUseRead } from "./local-extents.js"; import { findReboundParameters } from "./rebound-parameters.js"; import { ReferenceEffects } from "./reference-effects.js"; import { ReferenceWrites, type Writes } from "./reference-writes.js"; 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, findLentStringParameters, - type LentStringParameters, + findRecordFieldProducers, + type LendingFacts, type StringSliceSnapshot, } from "./string-slices.js"; import { LazyCaptures } from "./lazy-captures.js"; import { StackCaptures } from "./stack-captures.js"; -import { emitFieldAbsentTest, f64Lit, SCR_ARR_FIELD } 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, @@ -257,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, @@ -336,13 +378,22 @@ import { } from "./shapes.js"; import type { ExprOf, LibCallExpr, LlStreamTypedRefAdapter, LlValue } from "./expr-context.js"; import { INLINE_LITERAL_BYTES } from "./string-literal-equality.js"; +import { + inlinableNumberFunction, + withSpeculativeValue, + type InlinableFunction, + type SpeculationContext, + type SpeculativeValue, +} from "./speculative-integers.js"; import { emitFfiCallbackDefs, emitLibDefs, emitModuleParts, emitParts, helperDefs, + workerMinFunctions, } from "./emit-module.js"; +import { forkedAnalysis } from "./forked-analysis.js"; import { emitLoopCopies, emitSpeculativeWrite, @@ -432,19 +483,26 @@ export interface ForkPrimitives { export interface LlvmTargetOptions { /** C ABI target; omitted direct emission uses the host ABI. */ targetTriple?: string; + /** Linux (glibc and musl) executables: functions reached only by direct + * calls propagate exceptions by unwinding the native stack (unwind-abi.ts). */ + unwindAbi?: boolean; + /** Functions kept on the pending-flag ABI under `unwindAbi`: the ones an + * earlier emission found breaking its rules (UnwindAbiViolation). */ + unwindExcluded?: ReadonlySet; /** 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; /** Pointer width of the target C ABI. Native targets are 64-bit today. */ @@ -469,10 +527,19 @@ 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. */ detachedExit?: boolean; + /** The files of the program's Node island module tree, in manifest order + * (node-islands/modules.ts), when the executable carries the tree. */ + nodeIslandModuleData?: Uint8Array; /** Translated WebAssembly memories check bounds explicitly instead of * relying on guard pages (wasm-modules.ts wasmBoundsChecks). */ wasmBoundsChecks?: boolean; @@ -481,6 +548,15 @@ export interface LlvmTargetOptions { * store the instance's address over 8 in 32 bits (classes.ts * compactRefLoadLines). Elsewhere they keep 8-byte pointers. */ lowRegion?: boolean; + /** The x86-64 level the program's code may require (`--target-cpu`): + * 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 @@ -500,14 +576,78 @@ function versionParameters(mod: IrModule): IrModule { return versionLoopBoundParameters(mod, mangleFunction); } -/** The int32 arrays the int32 slot analysis keeps in `mod` (proven and - * checked), under the inputs LlEmitter gives it (selectInt32Arrays). */ -function provenInt32Arrays(mod: IrModule): ReadonlySet { +/** The int32 slot analysis of one module and the inputs it read: the + * constructor's own analyses of that module, which LlEmitter takes over + * instead of repeating them when it emits the same module. */ +export interface Int32SlotAnalysis { + readonly mod: IrModule; + readonly nodeIndex: IrNodeIndex; + readonly initializerBindings: ReturnType; + readonly dynamicStores: DynamicFieldStores; + readonly slots: Int32Slots; +} + +/** The int32 slot analysis of `mod`, with the inputs LlEmitter gives it. + * With `revise`, the analysis revises the functions of candidates that drop + * (Int32SlotInputs.revise), and `mod` is the module it started from. */ +function analyzeModuleInt32Slots( + mod: IrModule, + revise?: (fnName: string, dropped: ReadonlySet) => IrFunction, +): Int32SlotAnalysis { + const nodeIndex = new IrNodeIndex(mod); + const initializerBindings = findInitializerBindings(mod, nodeIndex); + const dynamicStores = dynamicFieldStores(mod, undefined, undefined, nodeIndex); const slots = analyzeInt32Slots(mod, { - dynamicStores: dynamicFieldStores(mod), - initializerBindings: findInitializerBindings(mod), + dynamicStores, + initializerBindings, + nodes: nodeIndex, + ...(revise === undefined ? {} : { revise }), }); - return new Set([...slots.int32Arrays, ...slots.checkedInt32Arrays]); + return { mod, nodeIndex, initializerBindings, dynamicStores, slots }; +} + +/** The candidates an analysis keeps: int32 arrays and checked ones. */ +function keptInt32Arrays(analysis: Int32SlotAnalysis): ReadonlySet { + return new Set([...analysis.slots.int32Arrays, ...analysis.slots.checkedInt32Arrays]); +} + +/** An analysis that revised functions (analyzeModuleInt32Slots' `revise`) + * as the analysis of `mod`, the module of the functions it ended with: the + * node index of `mod`, and its initializer bindings and dynamic stores, + * which must be the ones the analysis read. Null when they differ. */ +function revisedAnalysis(analysis: Int32SlotAnalysis, mod: IrModule): Int32SlotAnalysis | null { + const nodeIndex = new IrNodeIndex(mod); + const initializerBindings = findInitializerBindings(mod, nodeIndex); + const dynamicStores = dynamicFieldStores(mod, undefined, undefined, nodeIndex); + const read = analysis.initializerBindings; + if (initializerBindings.size !== read.size) return null; + for (const [name, globals] of initializerBindings) { + const other = read.get(name); + if (other?.length !== globals.length || globals.some((g, i) => other[i] !== g)) return null; + } + const stores = analysis.dynamicStores; + if ( + dynamicStores.all !== stores.all || + dynamicStores.names.size !== stores.names.size || + [...dynamicStores.names].some((name) => !stores.names.has(name)) + ) + return null; + return { + mod, + nodeIndex, + initializerBindings, + dynamicStores, + slots: analysis.slots, + }; +} + +/** The functions whose calls the backend may emit as calls of their + * result-slot entries (record-results.ts). */ +function resultEntryTargets(mod: IrModule): ReadonlySet { + const names = new Set(mod.functions.map((fn) => fn.name)); + const targets = new Set(); + for (const name of names) if (names.has(resultEntryName(name))) targets.add(name); + return targets; } /** The emitter for `input` after the module passes every emission runs. */ @@ -515,52 +655,157 @@ function moduleEmitter(input: IrModule, options: LlvmTargetOptions): LlEmitter { // Facts are keyed by node identity: emit the same module whether it was // lowered in memory or read back from its serialized form. const mod = unshareNodes(input); + // Each proof selectInt32Arrays asks for analyzes a marked module; the + // emitter of the module it returns reuses the last one. A proof that + // revised functions analyzed the module of the functions it ended with. + const rounds: { + last: Int32SlotAnalysis | null; + functions: readonly IrFunction[] | null; + revising: boolean; + } = { last: null, functions: null, revising: true }; + const proven = (marked: IrModule, revise: Int32ArrayRevision | null): Int32ArrayProof | null => { + rounds.functions = null; + if (revise === null || !rounds.revising) { + rounds.last = analyzeModuleInt32Slots(marked); + return { kept: keptInt32Arrays(rounds.last), functions: null }; + } + const latest = new Map(); + try { + rounds.last = analyzeModuleInt32Slots(marked, (fnName, dropped) => { + const fn = revise(fnName, dropped); + latest.set(fnName, fn); + return fn; + }); + } catch (error) { + if (!(error instanceof Int32RevisionConflict)) throw error; + rounds.last = null; + return null; + } + if (latest.size === 0) return { kept: keptInt32Arrays(rounds.last), functions: null }; + rounds.functions = marked.functions.map((fn) => latest.get(fn.name) ?? fn); + return { kept: keptInt32Arrays(rounds.last), functions: rounds.functions }; + }; // Keep source storage intact for debugger inspection in dev builds. - return new LlEmitter( - options.debugSources === undefined - ? selectInt32Arrays( - versionParameters( - addRecordResultEntries( - scalarizeNumericRecords( - specializeCallbackCalls( - specializeNumericCalls( - foldConstantGlobals( - copyStableCaptures( - resolveSelfCaptures( - hoistFinalFieldReads( - freezeInitOnlyGlobals( - holdCharLocalsAsUnits( - dropUnwrittenPropertyBags(runPairedWhileLoopsAsFor(mod)), - ), + if (options.debugSources !== undefined) return new LlEmitter(mod, options, null, null); + const passed = versionParameters( + addRecordResultEntries( + scalarizeLiteralResults( + scalarizeNumericRecords( + specializeCallbackCalls( + specializeNumericCalls( + pairRowSums( + inlineExpressionFunctions( + foldConstantGlobals( + copyStableCaptures( + resolveSelfCaptures( + hoistFinalFieldReads( + freezeInitOnlyGlobals( + holdCharLocalsAsUnits( + dropUnwrittenPropertyBags(runPairedWhileLoopsAsFor(mod)), ), ), ), ), ), - mangleFunction, ), - mangleFunction, ), ), + mangleFunction, ), + mangleFunction, ), - provenInt32Arrays, - ) - : mod, - options, + ), + ), + ), ); + let emitted = selectInt32Arrays(passed, proven, resultEntryTargets); + let prepared = rounds.last; + // A proof that revised functions analyzed the module of the functions it + // ended with, which is the selected module unless selection fell back to + // rounds: then the selected module was proved by its own round. + if (prepared !== null && rounds.functions !== null && prepared.mod !== emitted) { + const functions = rounds.functions; + prepared = + emitted !== passed && + emitted.functions.length === functions.length && + emitted.functions.every((fn, i) => fn === functions[i]) + ? revisedAnalysis(prepared, emitted) + : null; + if (prepared === null && emitted !== passed) { + rounds.revising = false; + emitted = selectInt32Arrays(passed, proven, resultEntryTargets); + prepared = rounds.last; + } + } + if (process.env["SCRIPTC_INT32_VERIFY"] === "1") + verifyInt32Selection(passed, emitted, prepared?.mod === emitted ? prepared : null); + return new LlEmitter(emitted, options, null, prepared?.mod === emitted ? prepared : null); +} + +/** SCRIPTC_INT32_VERIFY=1: throws unless the selection with revisions + * (`emitted`, and the analysis the emitter takes over) is the one rounds + * without revisions give: the same module, and the same facts. */ +function verifyInt32Selection( + passed: IrModule, + emitted: IrModule, + analysis: Int32SlotAnalysis | null, +): void { + let last: Int32SlotAnalysis | null = null; + const reference = selectInt32Arrays( + passed, + (marked) => { + last = analyzeModuleInt32Slots(marked); + return { kept: keptInt32Arrays(last), functions: null }; + }, + resultEntryTargets, + ); + const text = (mod: IrModule): string => serializeModuleParts(mod).join(""); + if (text(reference) !== text(emitted)) + throw new InternalCompilerError("int32 selection: the revised module differs from the rounds'"); + const facts = (mod: IrModule, prepared: Int32SlotAnalysis | null): string => + (prepared?.mod === mod ? prepared : analyzeModuleInt32Slots(mod)).slots.summary(mod); + const expected = facts(reference, last); + const actual = facts(emitted, analysis); + if (expected !== actual) { + const want = expected.split("\n"); + const got = actual.split("\n"); + const line = want.findIndex((entry, i) => entry !== got[i]); + throw new InternalCompilerError( + `int32 selection: the revised analysis differs from the rounds': ${want[line]?.slice(0, 400)} != ${got[line]?.slice(0, 400)}`, + ); + } +} + +/** Emits the module's parts, emitting it again with the functions an + * UnwindAbiViolation names on the pending-flag ABI. */ +function emitModuleText(input: IrModule, options: LlvmTargetOptions): string[] { + for (let excluded = options.unwindExcluded ?? new Set(); ;) { + const emitter = moduleEmitter(input, { ...options, unwindExcluded: excluded }); + try { + return emitter.emitParts(); + } catch (error) { + if (!(error instanceof UnwindAbiViolation)) throw error; + const names = new Set(excluded); + for (const name of emitter.unwindAbi) + for (const symbol of [mangleFunction(name), mangleBorrowedFunction(name)]) + if (error.symbols.has(symbol) || error.symbols.has(symbol + INT32_ABI_SUFFIX)) + names.add(name); + if (names.size === excluded.size) throw error; + excluded = names; + } + } } export function emitLlvmModule(input: IrModule, options: LlvmTargetOptions = {}): string { - return moduleEmitter(input, options).emit(); + return emitModuleText(input, options).join("\n"); } /** emitLlvmModule's text as parts that concatenate to it, without the copy * that joining them makes: a large program's text is 150 MB. */ export function emitLlvmModuleParts(input: IrModule, options: LlvmTargetOptions = {}): string[] { - return moduleEmitter(input, options) - .emitParts() - .flatMap((part, index) => (index === 0 ? [part] : ["\n", part])); + return emitModuleText(input, options).flatMap((part, index) => + index === 0 ? [part] : ["\n", part], + ); } /** Keep large translation units below the host's single-string limit. */ @@ -570,7 +815,7 @@ export function emitLlvmModuleSource( ): string | readonly string[] { // IR validation, contracts and the frontend's disposal run after lowering without checks. relieveHeapPressure(true); - const parts = moduleEmitter(input, options).emitParts(); + const parts = emitModuleText(input, options); const length = parts.reduce((total, part) => total + part.length, Math.max(0, parts.length - 1)); if (length <= 256 * 1024 * 1024) return parts.join("\n"); return parts.flatMap((part, index) => (index === 0 ? [part] : ["\n", part])); @@ -582,6 +827,21 @@ export function llStrBytes(text: string): string { const NO_BORROWED: ReadonlySet = new Set(); +/** The nodes that write a binding by its id (numberGlobalWriters). */ +const GLOBAL_WRITE_KINDS: readonly IrNodeKind[] = ["assign", "varDecl", "assignExpr", "incDec"]; +/** Runtime library calls (readsLoopMetrics). */ +const LIB_CALL_KINDS: readonly IrNodeKind[] = ["libCall"]; + +/** [A-Za-z0-9_] */ +function isIdentifierByte(code: number): boolean { + return ( + (code >= 48 && code <= 57) || + (code >= 65 && code <= 90) || + (code >= 97 && code <= 122) || + code === 95 + ); +} + /** Runtime entry points (and await-call entries) that may park the running * execution context, as they appear in emitted function text. */ export const SUSPENDS = @@ -694,8 +954,9 @@ export class LlEmitter { readonly stringSlices = new Map(); /** Slices written into stack buffers (findScratchStringSlices). */ scratchStringSlices: ReadonlySet = new Set(); - /** String parameters a caller may lend scratch slices to. */ - lentStringParameters: LentStringParameters = new Map(); + /** String parameters, record fields and record producers a caller may + * lend scratch slices to. */ + lendingFacts: LendingFacts = { strings: new Map(), records: new Map(), producers: new Map() }; localStackUnions = new Map(); localStackObjects = new Map(); reassignedStackObjects = new Set(); @@ -781,7 +1042,7 @@ export class LlEmitter { everyStmtList(fn.body, { stmt: (s) => { if (s.kind !== "varDecl" && s.kind !== "assign") return true; - const key = `${fn.name}\0${s.localId}`; + const key = int32ArrayKey(fn.name, s.localId); const checked = checkedInt32Arrays.has(key); if (!checked && !int32Arrays.has(key)) return true; const value = s.kind === "varDecl" ? s.init : s.value; @@ -799,15 +1060,16 @@ export class LlEmitter { }); } - /** Whether `arr` is one of the current function's int32 arrays - * (ir/int32-arrays.ts, kept by the int32 slot analysis). */ + /** Whether `arr` reads one of the int32 arrays (ir/int32-arrays.ts, kept + * by the int32 slot analysis): a local or parameter of the current + * function, or a module global. */ int32Array(arr: IrExpr): boolean { if (arr.kind !== "varRef") return false; if (this.int32KindArray(arr)) return true; if (this.int32Slots.int32Arrays.size === 0) return false; return ( this.int32ArrayTemps.has(arr.localId) || - this.int32Slots.int32Arrays.has(`${this.currentFunctionName}\0${arr.localId}`) + this.int32Slots.int32Arrays.has(int32ArrayKey(this.currentFunctionName, arr.localId)) ); } @@ -974,8 +1236,8 @@ export class LlEmitter { } /** Whether an element access at `index` of `arr` lies in the hole-free - * prefix: the counter of a dense array loop's fast copy indexing one of - * the arrays its entry test covered. */ + * prefix: a counter of a dense array loop's fast copy indexing one of the + * arrays its entry test covered. */ provenDenseIndex(arr: IrExpr, index: IrExpr): boolean { const loop = LOOPS.of(this).denseLoop; return ( @@ -983,8 +1245,8 @@ export class LlEmitter { arr.kind === "varRef" && loop.arrays.has(arr.localId) && index.kind === "varRef" && - index.localId === loop.counter && - this.integerLoopBindings.has(loop.counter) + (index.localId === loop.counter || index.localId === loop.pair?.high) && + this.integerLoopBindings.has(index.localId) ); } @@ -1036,6 +1298,12 @@ export class LlEmitter { readonly emitLibraryIdentity: boolean; readonly runtimeAbiMarker: boolean; readonly detachedExit: boolean; + readonly nodeIslandModuleData: Uint8Array | null; + 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. */ @@ -1070,6 +1338,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 @@ -1127,7 +1401,13 @@ export class LlEmitter { readonly checkedNarrows: CheckedNarrows; callArrayReads = new Map(); mapReadLifetimes: MapReadLifetimes = { locals: new Map(), arguments: new Map() }; + /** Locals of the current function that hold a Map.get result as the + * entry's own reference (findMapReadBorrows, borrowableMapGet). */ + borrowedMapGets: ReadonlySet = new Set(); readNumberMapWrites: ReadonlySet = new Set(); + /** The integer views of the current function's number Map/Set keys, by + * the key's double operand (noteNumberMapKey, map-number-lookup.ts). */ + numberMapKeyIntegers = new Map(); /** The current function's fused string-key Map get-then-set pairs (map-entry-fusion.ts). */ mapEntryFusion: MapEntryFusion | null = null; readonly callLifetimes: CallLifetimes; @@ -1197,6 +1477,10 @@ export class LlEmitter { /** May-throw analysis (computeMayThrow): pending * checks are emitted only after calls that can actually raise. */ readonly mayThrow: Set; + /** The functions that propagate exceptions by unwinding (unwind-abi.ts). */ + readonly unwindAbi: Set; + /** Whether the function being emitted is one of unwindAbi. */ + currentUnwindAbi = false; readonly indirectMayThrow: boolean; /** Worker programs: the functions that poll for termination on entry * (workerTerminationPolls); null outside worker programs. */ @@ -1235,6 +1519,11 @@ export class LlEmitter { /** Locals whose array literal emitStmts left for the `await Promise.all` * that reads them (deferredPromiseAllLocal). */ private readonly deferredPromiseAllEntries = new Map>(); + /** 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(); + readonly consumedReserves = new Set(); /** The promise a `return` emitted past its finally regions already * produced for its adopting await (borrowed from the return's parked * slot); that await takes it instead of evaluating its operand. */ @@ -1313,12 +1602,17 @@ export class LlEmitter { /** Class objects (classes as first-class values): className → the * interned .name literal ref — registered during body emission, the * statics and construct thunks assemble around the bodies. */ - readonly classObjs = new Map(); + readonly classObjs = new Map(); private readonly unionFieldReadGroups = new Map(); /** Preorder intervals of the runtime error classes under THIS module's * class-forest numbering (main() stamps scr_error_vts with them, exactly * like the runtime ABI’s errorVtStampLines). */ - readonly errorIntervals: { kind: number; pre: number; post: number; lib: string }[] = []; + readonly errorIntervals: { + kind: number; + pre: number; + post: number; + lib: string; + }[] = []; /** The runtime emitter vtable's preorder interval, when the program * touches node:events (the class def rides the module exactly then) — * main() stamps scr_emitter_vt with it (emitterVtStampLines, ported). */ @@ -1327,7 +1621,12 @@ export class LlEmitter { * ported): the defs ride every emitter-touching module (the frontend * collects the whole emitter-rooted tree), and scr_stream.c links on * the same predicate, so the stamps always have their globals. */ - readonly streamIntervals: { vt: string; pre: number; post: number; lib: string }[] = []; + readonly streamIntervals: { + vt: string; + pre: number; + post: number; + lib: string; + }[] = []; // ── per-function state (reset in emitFunction) ───────────────────────── B = new BlockBuilder(); @@ -1363,12 +1662,25 @@ export class LlEmitter { streamingSplitsEnabled = false; readonly initializerBindings: ReturnType; /** Whole-program int32 storage and parameter/result facts. */ - readonly int32Slots: Int32Slots; + get int32Slots(): Int32Slots { + return (this.int32SlotsValue ??= this.joinInt32Slots()); + } + private int32SlotsValue: Int32Slots | undefined; + /** The join of the int32 slot analysis a forked process computes + * (startInt32Slots). */ + private int32SlotsJoin: (() => Int32Slots) | undefined; /** The program has no runtime store into a checked-dynamic value * (dynamicFieldStores), so live views of classes and records never * 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; + /** 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. */ + readonly untypedIntegrity: boolean; /** Functions whose direct calls use i32 parameters or results. */ readonly int32Abis: ReadonlyMap; /** Pure number functions with speculative integer clones. */ @@ -1390,7 +1702,12 @@ export class LlEmitter { /** Active induction bindings retain their width independently of size_t. */ integerLoopBindings = new Map< string, - { slot: string; type: "i32" | "i64"; signed?: boolean; range?: { min: number; max: number } } + { + slot: string; + type: "i32" | "i64"; + signed?: boolean; + range?: { min: number; max: number }; + } >(); /** Constant number locals whose latest emitted declaration stored an * exact integer beside the double, such as `const j = i * 4` in a @@ -1405,6 +1722,9 @@ export class LlEmitter { integerTempsEnabled = false; /** Speculative integer loops (speculative-integers.ts) run in this function. */ speculativeLoopsEnabled = false; + /** False inside the copy of a versioned loop that runs when its guard + * fails: loops there get no speculative or int32 kind copies. */ + loopCopiesEnabled = true; /** The fast copy of a speculative integer loop being emitted: its * integer-computed writes and where each resumes in the ordinary copy. */ speculation: { @@ -1417,11 +1737,50 @@ export class LlEmitter { * integer loop has neither. */ bails?: string; ran?: string; + /** Writes that speculate a rewritten value (rewriteSpeculable). */ + values?: ReadonlyMap; } | null = null; /** Speculative loop sites numbered so far in the module. */ speculativeLoopSites = 0; /** The ordinary copy being emitted: resume labels still to place. */ speculationResume: Map | null = null; + /** The rewritten values of the writes `speculationResume` resumes at. */ + speculationResumeValues: ReadonlyMap | null = null; + /** Hoisted speculative leaves numbered so far in the function being + * emitted (emitFunction resets it): a function's local names must not + * depend on which functions the same emitter emitted before it, since + * function workers each emit a share of the module. */ + speculativeLeaves = 0; + private readonly inlinableFunctions = new Map(); + + /** What speculative loop planning may assume here (SpeculationContext). */ + speculationContext(): SpeculationContext { + const inlinable = (callee: string): InlinableFunction | undefined => { + if (!this.inlinableFunctions.has(callee)) { + // Unset while its body is checked: a recursive helper is not inlinable. + this.inlinableFunctions.set(callee, undefined); + const fn = this.fnByName.get(callee); + this.inlinableFunctions.set( + callee, + fn ? inlinableNumberFunction(fn, inlinable) : undefined, + ); + } + return this.inlinableFunctions.get(callee); + }; + return { + inlinable, + unaliased: (id) => { + const local = this.currentLocals.get(id); + return ( + local !== undefined && + !local.boxed && + !this.captureIds.has(id) && + !this.lazyCaptureBoxes.has(id) + ); + }, + leafId: () => `%spec.leaf.${this.speculativeLeaves++}`, + }; + } /** Run once at the top of the next loop copy's header * (`enterLoopHeader`): records the ordinary copy's header, or sends the * third copy's header there. */ @@ -1431,7 +1790,11 @@ export class LlEmitter { * (the fast copy) or as double arrays (an ordinary copy entered only * after they widened), as `function\0local` keys, and where each of the * fast copy's writes resumes in the third copy. */ - int32Kinds: { int32: Set; wide: Set; resume: Map } | null = null; + int32Kinds: { + int32: Set; + wide: Set; + resume: Map; + } | null = null; /** The resume label of the fast-copy write being emitted. */ int32KindResume: string | null = null; /** The function being emitted, for speculative globals. */ @@ -1459,10 +1822,42 @@ export class LlEmitter { * (one-unit tests and switches, charAtUnitRead), with the charAt each * stands for: a loop version's proof for the charAt covers its read. */ readonly charAtUnitReads = new Map(); + /** String reads a loop version proved in bounds of a string's UTF-16 + * units, with the units pointer to read (string-unit-versions.ts). */ + stringUnitReads: ReadonlyMap = new Map(); + /** `length` reads of a loop version's string, with the i64 UTF-16 + * length its guard took (bytes-loop-versions.ts). */ + stringLengthReads: ReadonlyMap = new Map(); /** Worker programs: typed-array accesses whose views an enclosing version * guard proved thread-owned. */ bytesOwned: ReadonlySet = 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 = 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 = new Set(); + /** Loop versions with alias scopes in the current function; it names + * their scopes. */ + aliasScopedVersions = 0; integerViews = new Map(); + /** The i1 slot beside an integer view of a local that only holds exact + * integers or NaN: true when the local is not NaN. */ + orderedViews = new Map(); /** Module globals the current function owns outright (initializer * bindings): their function-local slot, which stands in for the global. */ privateGlobalSlots = new Map(); @@ -1491,7 +1886,12 @@ export class LlEmitter { * releases frames/scopes down to the recorded depths and branches to * `label` (the catch, or the exception-path finally) instead of * returning out of the function. Entering a try emits no code. */ - tryStack: { label: string; used: boolean; frameDepth: number; scopeDepth: number }[] = []; + tryStack: { + label: string; + used: boolean; + frameDepth: number; + scopeDepth: number; + }[] = []; /** Return type of the function being emitted — the unwind path returns * a dummy of this type (never read: callers check the flag first). */ currentReturnType: IrType = VOID; @@ -1544,17 +1944,25 @@ export class LlEmitter { readonly mod: IrModule, options: LlvmTargetOptions, journal: EmitJournal | null = null, + int32Analysis: Int32SlotAnalysis | null = null, ) { this.targetOptions = options; 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 = new IrNodeIndex(mod); + this.nodeIndex = int32Analysis?.nodeIndex ?? new IrNodeIndex(mod); + this.initializerBindings = + int32Analysis?.initializerBindings ?? findInitializerBindings(mod, this.nodeIndex); + // One walk finds the dynamic field stores every analysis below consults. + const dynamicStores = + int32Analysis?.dynamicStores ?? dynamicFieldStores(mod, undefined, undefined, this.nodeIndex); + // Started before the other analyses, which may run while a forked + // process computes it (startInt32Slots). + if (int32Analysis !== null) this.int32SlotsValue = int32Analysis.slots; + else this.startInt32Slots(dynamicStores); this.stackTraces = observesStacks(mod, this.nodeIndex); this.nullableUnions = new NullableUnions( mod, @@ -1573,16 +1981,13 @@ export class LlEmitter { this.nullableUnions.armTypes(), ); this.constantNumericTables = findConstantNumericTables(mod); - this.initializerBindings = findInitializerBindings(mod, this.nodeIndex); - // One walk finds the dynamic field stores every analysis below consults. - const dynamicStores = dynamicFieldStores(mod, undefined, undefined, this.nodeIndex); - this.int32Slots = analyzeInt32Slots(mod, { - dynamicStores, - initializerBindings: this.initializerBindings, - nodes: this.nodeIndex, - }); + this.untypedIntegrity = mod.untypedIntegrity === true; this.liveViewsReadOnly = - mod.lib === undefined && !dynamicStores.all && dynamicStores.names.size === 0; + mod.lib === undefined && + !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 ?? []); @@ -1618,6 +2023,16 @@ export class LlEmitter { this.emitLibraryIdentity = options.emitLibraryIdentity !== false; this.runtimeAbiMarker = options.runtimeAbiMarker === true; this.detachedExit = options.detachedExit === true; + this.nodeIslandModuleData = options.nodeIslandModuleData ?? null; + 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({ @@ -1649,6 +2064,16 @@ export class LlEmitter { nodes: this.nodeIndex, }); this.mayThrow = mt.fns; + this.unwindAbi = + options.unwindAbi === true && + !this.wasi && + mod.lib === undefined && + options.debugSources === undefined && + (options.targetTriple + ? /-linux-(?:gnu|musl)/.test(options.targetTriple) + : process.platform === "linux") + ? unwindAbiFunctions(mod, this.mayThrow, options.unwindExcluded) + : new Set(); this.workerEntryPolls = mt.workerEntryPolls ?? null; this.stackChecks = mt.stackChecks ?? null; // A stackless call's eager prefix borrows its caller's stack; the @@ -1762,9 +2187,16 @@ export class LlEmitter { const shape = this.recordsById.get(shapeId); return shape !== undefined && !recordDynKeepsIdentity(shape); }, + (wrap) => this.borrowableNullableWrap(wrap) === "ref", ); - this.lentStringParameters = - this.debug === null ? findLentStringParameters(this.fnByName, this.callLifetimes) : new Map(); + if (this.debug === null) { + const strings = findLentStringParameters(this.fnByName, this.callLifetimes); + this.lendingFacts = { + strings, + records: findLentRecordFields(this.fnByName, this.callLifetimes, strings), + producers: findRecordFieldProducers(this.fnByName), + }; + } this.constantCallbacks = findConstantCallbacks(mod, this.callLifetimes, this.nodeIndex); // Integer clones signal a stack overflow through their result, which // needs the single-threaded guard; worker programs also poll for @@ -1783,10 +2215,13 @@ export class LlEmitter { : undefined, ) : NO_INTEGER_CLONES; + // Integer clones and their dispatchers call each other through their + // own lowering, which keeps plain calls and flag tests. + for (const name of this.integerClones.members) this.unwindAbi.delete(name); this.stackCallbacks = new StackCallbacks(this.fnByName); - this.receiverReaders = new AmbientReceiverReaders(this.fnByName); + this.receiverReaders = new AmbientReceiverReaders(this.fnByName, this.nodeIndex); this.stackCaptures = new StackCaptures(this.fnByName, this.callLifetimes, this.stackCallbacks); - this.lazyCaptures = new LazyCaptures(this.fnByName); + this.lazyCaptures = new LazyCaptures(this.fnByName, this.nodeIndex); const traced = computeTraced(mod, dynamicStores, cycleMutability); this.tracedShapes = traced.shapes; this.tracedUnions = traced.unions; @@ -1856,6 +2291,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), @@ -1875,14 +2311,14 @@ export class LlEmitter { const meta = this.classMeta.get(t.className); return meta !== undefined && programLifetimeClass(this.shapeHost, meta); }, - classSubtypes: (name) => { - const target = this.classMetaOf(name); - const intervals = classMembershipIntervals(this.classMeta, target.def.name); - return [...this.classMeta.values()] - .filter((meta) => - intervals.some((range) => range.pre <= meta.pre && meta.pre <= range.post), - ) - .map((meta) => meta.def.name); + classSubtypes: (name) => this.classSubtypes(name), + classValueSubtypes: (name) => { + const target = this.classMetaOf(name).def; + return this.classSubtypes(name).filter( + (sub) => + sub !== target.name && + classValueAbiEquals(this.classMetaOf(sub).def, target, this.fnByName), + ); }, pendingTestLines: (dest) => this.pendingTestLines(dest), virtualEntry: (implFn) => this.virtualEntry(implFn), @@ -2001,6 +2437,46 @@ export class LlEmitter { ); // After the vtable slots: borrowing slots exclude their implementations. if (this.debug === null) this.analyzeBorrowedReturns(); + this.int32SlotsValue ??= this.joinInt32Slots(); + } + + /** Computes the int32 slot analysis, or starts it on a forked process. + * Dev builds read its result only near the end of the constructor + * (int32Abis is empty with debug info), so for a large module the + * process computes it while the constructor runs the other analyses. */ + private startInt32Slots(dynamicStores: DynamicFieldStores): void { + const mod = this.mod; + const nodes = this.nodeIndex; + const analyze = (): Int32Slots => + analyzeInt32Slots(mod, { + dynamicStores, + initializerBindings: this.initializerBindings, + nodes, + }); + const fork = this.targetOptions.forkPrimitives; + if ( + fork === undefined || + this.targetOptions.debugSources === undefined || + (this.targetOptions.functionWorkers ?? 1) < 2 || + mod.functions.length < workerMinFunctions() + ) { + this.int32SlotsValue = analyze(); + return; + } + this.int32SlotsJoin = forkedAnalysis( + fork, + analyze, + (slots) => slots.encode(nodes), + (text) => Int32Slots.decode(text, mod, nodes), + ); + } + + private joinInt32Slots(): Int32Slots { + const join = this.int32SlotsJoin; + if (join === undefined) + throw new InternalCompilerError("llvm emitter bug: int32 slots read before analysis"); + this.int32SlotsJoin = undefined; + return join(); } /** The borrowed parameters of a hierarchy's vtable slot (empty: owned). */ @@ -2032,7 +2508,6 @@ export class LlEmitter { if (POINTER_KINDS.has(t.kind)) return "ptr"; switch (t.kind) { case "f64": - case "date": return "double"; case "bool": return "i1"; @@ -2063,7 +2538,9 @@ export class LlEmitter { return this.emitParts().join("\n"); } emitParts(): string[] { - return emitParts(this); + const parts = emitParts(this); + 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 @@ -2137,7 +2614,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) @@ -2165,7 +2642,6 @@ export class LlEmitter { this.declare(`declare void @scr_promise_fulfill_void(ptr)`); return [` call void @scr_promise_fulfill_void(ptr ${promise})`]; case "f64": - case "date": this.declare(`declare void @scr_promise_fulfill_f64(ptr, double)`); return [` call void @scr_promise_fulfill_f64(ptr ${promise}, double %r)`]; case "bool": @@ -2217,9 +2693,23 @@ export class LlEmitter { this.mangledFunctions.set(mangleBorrowedFunction(fn.name), fn.name); } } - for (const match of text.matchAll(/@(sc_b?f_[A-Za-z0-9_]+)/g)) { - const callee = this.mangledFunctions.get(match[1]!); + // The names `@(sc_b?f_[A-Za-z0-9_]+)` matches, found with indexOf. + for (let at = text.indexOf("@sc_"); at >= 0;) { + let start = at + 4; + if (text.charCodeAt(start) === 98 /* b */) start++; + if (text.charCodeAt(start) !== 102 /* f */ || text.charCodeAt(start + 1) !== 95 /* _ */) { + at = text.indexOf("@sc_", at + 1); + continue; + } + let end = start + 2; + while (end < text.length && isIdentifierByte(text.charCodeAt(end))) end++; + if (end === start + 2) { + at = text.indexOf("@sc_", at + 1); + continue; + } + const callee = this.mangledFunctions.get(text.slice(at + 1, end)); if (callee !== undefined) yield callee; + at = text.indexOf("@sc_", end); } } @@ -2395,7 +2885,7 @@ export class LlEmitter { return { name: "", type }; } const t = B.tmp(); - if (type.kind === "f64" || type.kind === "date") { + if (type.kind === "f64") { this.declare(`declare double @scr_coro_direct_f64(ptr)`); B.line(`${t} = call double @scr_coro_direct_f64(ptr ${record})`); } else if (type.kind === "bool") { @@ -2676,12 +3166,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}`) @@ -2807,7 +3298,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); @@ -3160,6 +3651,13 @@ export class LlEmitter { terminator = `br label %${target.label}`; } else if (this.currentCoro !== null) { terminator = `br label %${this.currentCoro.finalLabel}`; + } else if (this.currentUnwindAbi && this.inLandingPad) { + // The unwind that entered the landing pad goes on to the caller's. + terminator = UNWIND_RESUME; + } else if (this.currentUnwindAbi) { + // The caller's invoke takes the exception at its landing pad. + this.declare(UNWIND_RAISE_DECLARATION); + terminator = UNWIND_RAISE; } else { const t = this.llType(this.currentReturnType); terminator = @@ -3190,6 +3688,7 @@ export class LlEmitter { passThrough !== undefined && !target && this.currentCoro === null && + !this.currentUnwindAbi && !this.currentInt32Result ) { const t = this.llType(this.currentReturnType); @@ -3256,6 +3755,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 && @@ -3280,6 +3782,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) { @@ -3356,6 +3859,7 @@ export class LlEmitter { B.condBr(cold, lu, lk); } B.startBlock(lu); + onUnwind?.(); this.emitUnwind(through); B.startBlock(lk); if (skip !== undefined) { @@ -3376,6 +3880,46 @@ export class LlEmitter { B.startBlock(B.newLabel("exc.dead")); } + /** Emits `line`, a `call` of `callee`, and reports whether the call + * needs no pending check after it: a call of an unwinding-ABI function + * becomes an `invoke` whose landing pad runs this frame's unwind (the + * same releases and handler as emitPendingCheck's unwind path), and the + * normal path continues in a fresh block. Coroutine bodies keep plain + * calls (unwindAbiFunctions excludes their callees). */ + emitDirectCall(line: string, callee: string): boolean { + const B = this.B; + if (!this.unwindAbi.has(callee) || this.currentCoro !== null || B.isTerminated()) { + B.line(line); + return false; + } + const ok = B.newLabel("inv.ok"); + const pad = B.newLabel("inv.lp"); + this.usesLandingPads = true; + // A trailing comment goes after the invoke's labels. + const comment = line.indexOf(" ; "); + const text = comment < 0 ? line : line.slice(0, comment); + B.terminate( + `${text.replace(/^((?:%[\w.]+ = )?)call /, "$1invoke ")} to label %${ok} unwind label %${pad}`, + ); + B.startBlock(pad); + const exception = B.tmp(); + B.line(`${exception} = landingpad { ptr, i32 } cleanup`); + if (this.currentUnwindAbi) B.line(`store { ptr, i32 } ${exception}, ptr ${UNWIND_SLOT}`); + this.inLandingPad = true; + this.emitUnwind(); + this.inLandingPad = false; + B.startBlock(ok); + return true; + } + + /** Whether emitUnwind runs for a landing pad, whose unwind goes on with + * `resume` instead of raising again. */ + private inLandingPad = false; + + /** Whether the function being emitted has a landing pad (its definition + * needs the personality). */ + usesLandingPads = false; + /** A recursive function's entry check: below the running stack's guard, * throw Node's RangeError and unwind like any other pending exception. */ emitStackCheck(): void { @@ -3465,13 +4009,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): boolean { + return ( + this.mod.workers === true && !loopTestsEveryIteration(loop, (e) => this.callTestsPending(e)) + ); + } + workerLoopBudget( loop: Extract, ): 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}`); @@ -3551,15 +4101,14 @@ export class LlEmitter { set.add(fn.name); writers.set(localId, set); }; - everyStmtList(fn.body, { - stmt: (s) => { - if (s.kind === "assign" || s.kind === "varDecl") note(s.localId); - return true; - }, - expr: (e) => { - if (e.kind === "assignExpr" || e.kind === "incDec") note(e.localId); - return true; - }, + eachNodeOfKinds(fn, this.nodeIndex.scan(GLOBAL_WRITE_KINDS), (node) => { + if ( + node.kind === "assign" || + node.kind === "varDecl" || + node.kind === "assignExpr" || + node.kind === "incDec" + ) + note(node.localId); }); } this.globalWriters = writers; @@ -3590,14 +4139,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 @@ -3791,8 +4345,17 @@ export class LlEmitter { /** A union value's tag (i32): the box's tag word, or the NULL test of a * nullable union's pointer. */ + /** A box's tag and payload words are written only where the box is + * built (runtime constructors and untagged stack stores), so their loads + * carry their own alias tags: element and field stores cannot change + * them, and loops keep them hoisted. */ unionTag(uName: string, unionId: string): string { - return emitUnionTag(this.B, this.nullableUnions.get(unionId), uName); + return emitUnionTag( + this.B, + this.nullableUnions.get(unionId), + uName, + this.memoryAliasAttachment("union.tag"), + ); } /** Guard for helpers that build or read a union's `%ScrUnion` box @@ -3808,7 +4371,12 @@ export class LlEmitter { /** The BORROWED payload pointer of a ref arm (scr_union_peek inlined — * the runtime's is a static inline); a nullable union is its payload. */ unionPeek(uName: string, unionId: string): string { - return emitUnionPeek(this.B, this.nullableUnions.get(unionId), uName); + return emitUnionPeek( + this.B, + this.nullableUnions.get(unionId), + uName, + this.memoryAliasAttachment("union.payload"), + ); } /** Emits `switch` over a union's tag with one block per arm; each arm @@ -3933,6 +4501,15 @@ export class LlEmitter { // ── class plumbing ────────────────────────────────────────────────────── + /** `name` and every class whose membership interval falls in its own. */ + private classSubtypes(name: string): string[] { + const target = this.classMetaOf(name); + const intervals = classMembershipIntervals(this.classMeta, target.def.name); + return [...this.classMeta.values()] + .filter((meta) => intervals.some((range) => range.pre <= meta.pre && meta.pre <= range.post)) + .map((meta) => meta.def.name); + } + classMetaOf(className: string): LlClassMeta { const meta = this.classMeta.get(className); if (!meta) throw new InternalCompilerError(`llvm emitter bug: unknown class ${className}`); @@ -4037,7 +4614,12 @@ export class LlEmitter { classObjSym(className: string): string { if (!this.classObjs.has(className)) { const meta = this.classMetaOf(className); - this.classObjs.set(className, { nameSym: this.internLiteral(classJsName(meta.def)) }); + // util.inspect prints `[class C extends B]` with the base's name. + const baseName = meta.base ? classJsName(meta.base.def) : ""; + this.classObjs.set(className, { + nameSym: this.internLiteral(classJsName(meta.def)), + baseNameSym: baseName ? this.internLiteral(baseName) : null, + }); } this.journal?.entry("classObj", className, this.classObjs.get(className)); return mangleClassObj(className); @@ -4251,6 +4833,7 @@ export class LlEmitter { matchBytesLoopVersion( s: IrStmt & { kind: "for" | "while" }, counted: IntegerCountedForLoop | null, + paired: IntegerPairedForLoop | null = null, ): ReturnType { const fn = LOOPS.of(this).bytesLoopFunction; if (!fn) return null; @@ -4260,17 +4843,32 @@ export class LlEmitter { const invariantGlobals = new Set(this.stableGlobals); for (const global of this.initializerBindings.get(fn.name) ?? []) invariantGlobals.add(global.id); - return matchBytesLoopVersion(s, counted, { - locals: this.currentLocals, - invariantGlobals, - captured: this.captureIds, - ranges: this.integerRanges, - initialized, - proven: this.bytesBounds, - workers: this.mod.workers === true, - owned: this.bytesOwned, - scalarStrings: new Set(this.stringSlices.keys()), - }); + return matchBytesLoopVersion( + s, + counted, + { + locals: this.currentLocals, + invariantGlobals, + captured: this.captureIds, + ranges: this.integerRanges, + initialized, + proven: this.bytesBounds, + workers: this.mod.workers === true, + owned: this.bytesOwned, + scalarStrings: new Set(this.stringSlices.keys()), + unitSafeFunctions: this.unitSafeFunctions(), + }, + paired, + ); + } + + private unitSafe: ReadonlySet | null | undefined; + /** The functions a loop over a string's UTF-16 units may call, or null + * where emitted reads do not use the runtime's scan receiver. */ + private unitSafeFunctions(): ReadonlySet | null { + if (this.unitSafe === undefined) + this.unitSafe = stringScanInline(this) ? unitSafeFunctions(this.mod) : null; + return this.unitSafe; } /** The alias tag of a whole-slot load of a binding (module globals only). */ @@ -4754,6 +5352,7 @@ export class LlEmitter { chunkResult(): ChunkResult { return { entries: this.journal!.entries, + debugRequests: this.debug?.recorded() ?? [], flags: { usesCoroutines: this.usesCoroutines, usesTimers: this.usesTimers, @@ -4789,6 +5388,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(); + this.debug?.replay(chunk.debugRequests, (name) => this.functionNamed(name), debugIds); const flags = chunk.flags; this.usesCoroutines ||= flags.usesCoroutines; this.usesTimers ||= flags.usesTimers; @@ -4809,7 +5410,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 @@ -4897,13 +5504,20 @@ export class LlEmitter { return; } case "classObj": - addEntry(this.classObjs, key, value as { nameSym: string }); + addEntry(this.classObjs, key, value as { nameSym: string; baseNameSym: string | null }); return; 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": @@ -4911,7 +5525,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, @@ -4937,7 +5553,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 @@ -5115,7 +5737,22 @@ export class LlEmitter { if (fn.captures !== undefined) params.unshift("ptr %sc_env"); const ret = this.llType(fn.returnType); const call = `call ${ret} @${mangleBorrowedFunction(fn.name)}(${params.join(", ")})`; - B.line(ret === "void" ? call : `%result = ${call}`); + // The owned entry of an unwinding-ABI body releases the borrowed + // parameters on the unwind too, then resumes it to its caller. + const unwinding = this.unwindAbi.has(fn.name) && kept.size === 0; + if (unwinding) { + const ok = B.newLabel("adapt.ok"); + const pad = B.newLabel("adapt.lp"); + B.terminate( + `${ret === "void" ? "" : "%result = "}invoke ${call.slice(5)} to label %${ok} unwind label %${pad}`, + ); + B.startBlock(pad); + const exception = B.tmp(); + B.line(`${exception} = landingpad { ptr, i32 } cleanup`); + for (const index of borrowed) this.releaseFrame(`%p${index}`, fn.params[index]!.type); + B.terminate(`resume { ptr, i32 } ${exception}`); + B.startBlock(ok); + } else B.line(ret === "void" ? call : `%result = ${call}`); // A borrowed result is reachable from the parameters: own it first. A // throwing body returns a null dummy, which needs no owner (and string // or array retains do not accept NULL). @@ -5141,7 +5778,9 @@ export class LlEmitter { B.terminate(ret === "void" ? "ret void" : `ret ${ret} ${result}`); const symbol = kept.size > 0 ? `${mangleBorrowedFunction(fn.name)}.virtual` : mangleFunction(fn.name); - return `define internal ${ret} @${symbol}(${params.join(", ")}) ${FN_ATTRS} {\n${B.render()}\n}`; + if (unwinding) this.declare(`declare i32 @__gcc_personality_v0(...)`); + const personality = unwinding ? ` ${UNWIND_PERSONALITY}` : ""; + return `define internal ${ret} @${symbol}(${params.join(", ")}) ${FN_ATTRS}${personality} {\n${B.render()}\n}`; } canBorrowCallArgument(value: IrExpr): boolean { return canBorrowCallArgument(this, value); @@ -5165,7 +5804,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); } @@ -5314,13 +5956,21 @@ export class LlEmitter { private loopMetricsRead: boolean | undefined; readsLoopMetrics(): boolean { if (this.loopMetricsRead !== undefined) return this.loopMetricsRead; - const visitor = { - expr: (e: IrExpr): boolean => - !(e.kind === "libCall" && (e.fn === "perf.loopStart" || e.fn === "perf.idleTime")), - stmt: (): boolean => true, - }; - this.loopMetricsRead = !this.mod.functions.every((fn) => everyStmtList(fn.body, visitor)); - return this.loopMetricsRead; + let found = false; + const scan = this.nodeIndex.scan(LIB_CALL_KINDS); + for (const fn of this.mod.functions) { + eachNodeOfKinds(fn, scan, (node) => { + if ( + node.kind === "libCall" && + (node.fn === "perf.loopStart" || node.fn === "perf.idleTime") + ) + found = true; + return !found; + }); + if (found) break; + } + this.loopMetricsRead = found; + return found; } private lastUseMovesAllowed(): boolean { @@ -5434,7 +6084,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": @@ -5514,6 +6178,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); @@ -5546,6 +6212,22 @@ export class LlEmitter { if (resume && !resume.placed && !this.B.isTerminated()) { // The fast copy's failed checks re-execute this statement here. resume.placed = true; + // A write with hoisted leaves resumed from its own failed check + // re-executes its arithmetic over the leaves' stored doubles: each + // leaf ran once, in the fast copy. Reached from an earlier statement + // of this copy (a failed check before it), its leaves never ran, so + // it runs as written. + const rewritten = this.speculationResumeValues?.get(s); + if (rewritten !== undefined && rewritten.leaves.length > 0) { + const join = this.B.newLabel("spec.resume.join"); + this.emitStmt(s); + if (!this.B.isTerminated()) this.B.br(join); + this.B.startBlock(resume.label); + this.emitStmt(withSpeculativeValue(s, rewritten.value)); + if (!this.B.isTerminated()) this.B.br(join); + this.B.startBlock(join); + return; + } this.B.br(resume.label); this.B.startBlock(resume.label); } @@ -5688,6 +6370,10 @@ export class LlEmitter { }); return; } + if (e.kind === "dynInvoke" && e.calleeNameValue === undefined) { + emitDynamicExpr(this, e, true); + return; + } this.emitExpr(e); } @@ -6136,6 +6822,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; @@ -6279,6 +6971,41 @@ export class LlEmitter { return ok; } + /** Branches on "the array `arr` is frozen" (SCR_ARR_FROZEN in its storage + * flags, scr_array.h), or published too when `published`; like + * emitPublishedTest, leaves the builder in the cold block and answers the + * continuation label. */ + private emitFrozenArrayTest(arr: string, published: boolean): string { + const B = this.B; + const sparep = B.tmp(), + spare = B.tmp(), + bit = B.tmp(); + let frozen = B.tmp(); + B.line( + `${sparep} = getelementptr inbounds %ScrArr, ptr ${arr}, i32 0, i32 ${SCR_ARR_FIELD.spare}`, + ); + B.line(`${spare} = load i16, ptr ${sparep}`); + B.line(`${bit} = and i16 ${spare}, ${SCR_ARR_FROZEN}`); + B.line(`${frozen} = icmp ne i16 ${bit}, 0`); + if (published) { + const rc = B.tmp(), + immortal = B.tmp(), + either = B.tmp(); + B.line(`${rc} = load ${this.sizeType}, ptr ${arr}`); + B.line(`${immortal} = icmp eq ${this.sizeType} ${rc}, -1`); + B.line(`${either} = or i1 ${frozen}, ${immortal}`); + frozen = either; + } + this.declare(`declare i1 @llvm.expect.i1(i1, i1)`); + const expected = B.tmp(); + B.line(`${expected} = call i1 @llvm.expect.i1(i1 ${frozen}, i1 false)`); + const cold = B.newLabel("arr.frozen"), + ok = B.newLabel("arr.ok"); + B.condBr(expected, cold, ok); + B.startBlock(cold); + return ok; + } + private publishedNamesSym: string | null = null; /** The class-name table the hierarchy field guards index by preorder. */ @@ -6343,15 +7070,29 @@ export class LlEmitter { op: number, index = f64Lit(0), count = f64Lit(0), + third: string | null = null, ): void { - if (this.publishedTypes?.containerGuarded(arrType) !== true) return; + const published = this.publishedTypes; + if (published?.containerGuarded(arrType) !== true) return; const B = this.B; - const ok = this.emitPublishedTest(arr); - this.declare(`declare zeroext i1 @scr_throw_published_array(i32, ptr, double, double)`); + const guards = published.arrayGuards(arrType); + const ok = guards.frozen + ? this.emitFrozenArrayTest(arr, guards.published) + : this.emitPublishedTest(arr); const threw = B.tmp(); - B.line( - `${threw} = call zeroext i1 @scr_throw_published_array(i32 ${op}, ptr ${arr}, double ${index}, double ${count})`, - ); + if (third === null) { + this.declare(`declare zeroext i1 @scr_throw_published_array(i32, ptr, double, double)`); + B.line( + `${threw} = call zeroext i1 @scr_throw_published_array(i32 ${op}, ptr ${arr}, double ${index}, double ${count})`, + ); + } else { + this.declare( + `declare zeroext i1 @scr_throw_published_array_range(i32, ptr, double, double, double)`, + ); + B.line( + `${threw} = call zeroext i1 @scr_throw_published_array_range(i32 ${op}, ptr ${arr}, double ${index}, double ${count}, double ${third})`, + ); + } const unwind = B.newLabel("pub.throw"); B.condBr(threw, unwind, ok); B.startBlock(unwind); @@ -6615,6 +7356,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": @@ -6742,7 +7488,10 @@ export class LlEmitter { return childDataDynThunkFor(this, chunk); } - emitterFixedAdapter(cbT: IrType & { kind: "func" }): { fn: string; shim: string } { + emitterFixedAdapter(cbT: IrType & { kind: "func" }): { + fn: string; + shim: string; + } { return emitterFixedAdapter(this, cbT); } @@ -6757,8 +7506,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 { @@ -6889,8 +7650,9 @@ export class LlEmitter { inBounds = false, invalidNaN = false, owned = false, + scope = "", ): LlValue { - return emitBytesGet(this, elem, receiver, index, expr, inBounds, invalidNaN, owned); + return emitBytesGet(this, elem, receiver, index, expr, inBounds, invalidNaN, owned, scope); } emitToUint32(value: string, expr?: IrExpr, uint32?: string): string { @@ -6943,6 +7705,14 @@ export class LlEmitter { if (slot === undefined) return; const integer = this.emitToUint32(value.name, expr, value.uint32); this.B.line(`store i32 ${integer}, ptr ${slot}`); + const orderedSlot = this.orderedViews.get(localId); + if (orderedSlot === undefined) return; + let ordered = value.ordered; + if (ordered === undefined) { + ordered = this.B.tmp(); + this.B.line(`${ordered} = fcmp ord double ${value.name}, 0.0`); + } + this.B.line(`store i1 ${ordered}, ptr ${orderedSlot}`); } emitBytesSet( @@ -6954,8 +7724,20 @@ export class LlEmitter { expr?: IrExpr, inBounds = false, owned = false, + scope = "", ): void { - return emitBytesSet(this, elem, receiver, index, value, indexExpr, expr, inBounds, owned); + return emitBytesSet( + this, + elem, + receiver, + index, + value, + indexExpr, + expr, + inBounds, + owned, + scope, + ); } emitBytesIntrinsic(e: IrExpr & { kind: "bytesIntrinsic" }): LlValue { diff --git a/packages/compiler/src/backend/llvm/expr-async.ts b/packages/compiler/src/backend/llvm/expr-async.ts index b5d765b16..335946d9d 100644 --- a/packages/compiler/src/backend/llvm/expr-async.ts +++ b/packages/compiler/src/backend/llvm/expr-async.ts @@ -184,7 +184,7 @@ export function emitIntrinsicExpr(host: LlvmEmitterContext, e: ExprOf<"intrinsic } const v = host.emitExpr(e.args[0]!); const t = e.args[0]!.type; - if (t.kind === "f64" || t.kind === "date") { + if (t.kind === "f64") { host.declare(`declare void @scr_promise_fulfill_f64(ptr, double)`); B.line(`call void @scr_promise_fulfill_f64(ptr ${p}, double ${v.name})`); } else if (t.kind === "bool") { @@ -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; } } @@ -394,14 +409,26 @@ export function emitSerializationExpr( } const dynV = emitBorrowedInput(host, e.value); const helper = host.dyn.dynCheckHelper(e.type, e.preserveRefs); - if (isDynTypedRefType(e.type)) return emitClassCheck(host, e.type, dynV.name, helper); + // A check of an analysis-proven fact (shape inference's overlay) + // names its site, so a failure reports a compiler bug. + const site = e.inferredSite !== undefined ? host.cstr(e.inferredSite) : null; + if (site !== null) { + host.declare(`declare void @scr_dyn_inferred_site(ptr)`); + B.line(`call void @scr_dyn_inferred_site(ptr ${site})`); + } + if (isDynTypedRefType(e.type)) { + 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; + } const ty = host.llType(e.type); const t = B.tmp(); B.line( `${t} = call ${ty === "i1" ? "zeroext i1" : ty} @${helper}(ptr ${dynV.name}, ptr null)`, ); + 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: { @@ -420,6 +447,7 @@ function emitClassCheck( type: IrType & { kind: "object" }, value: string, helper: string, + note?: () => void, ): LlValue { const B = host.B; const slot = B.slot(); @@ -460,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(); @@ -500,7 +528,7 @@ export function emitAsyncExpr( host.declare(`declare void @scr_async_gen_hop_done()`); B.line(`call void @scr_async_gen_hop_done()`); } - if (yt.kind === "f64" || yt.kind === "date") { + if (yt.kind === "f64") { if (host.currentCoro !== null) { const coro = host.currentCoro; if (coro.out !== null) host.emitGenSlotStore(coro.out, "f64", v.name); @@ -582,7 +610,7 @@ export function emitAsyncExpr( return { name: "", type: e.type }; } const t = B.tmp(); - if (e.type.kind === "f64" || e.type.kind === "date") { + if (e.type.kind === "f64") { host.declare(`declare double @scr_gen_take_in_f64()`); B.line(`${t} = call double @scr_gen_take_in_f64()`); return { name: t, type: e.type }; @@ -710,12 +738,12 @@ export function emitAsyncExpr( throw new InternalCompilerError(`llvm emitter bug: genResume ${e.mode} on async`); const gb = emitBorrowedInput(host, e.gen); if (e.mode === "value") { - if (!host.wasi && (e.type.kind === "f64" || e.type.kind === "date")) + if (!host.wasi && e.type.kind === "f64") return { name: host.emitGenSlotTake(host.genOutSlot(gb.name), "f64"), type: e.type }; if (!host.wasi && e.type.kind === "bool") return { name: host.emitGenSlotTake(host.genOutSlot(gb.name), "bool"), type: e.type }; const t = B.tmp(); - if (e.type.kind === "f64" || e.type.kind === "date") { + if (e.type.kind === "f64") { host.declare(`declare double @scr_gen_take_out_f64(ptr)`); B.line(`${t} = call double @scr_gen_take_out_f64(ptr ${gb.name})`); return { name: t, type: e.type }; @@ -752,7 +780,7 @@ export function emitAsyncExpr( store(a.name, e.arg!.type); }; const parkIn = (name: string, t: IrType): void => { - if (t.kind === "f64" || t.kind === "date") { + if (t.kind === "f64") { host.declare(`declare void @scr_gen_in_f64(ptr, double)`); B.line(`call void @scr_gen_in_f64(ptr ${g.name}, double ${name})`); } else if (t.kind === "bool") { @@ -787,7 +815,7 @@ export function emitAsyncExpr( const a = host.emitExpr(e.arg); const t = e.arg.type; if (isRefCounted(t)) host.toHeap(a); - if (t.kind === "f64" || t.kind === "date") { + if (t.kind === "f64") { host.declare(`declare ptr @scr_async_gen_next_f64(ptr, double)`); call = `call ptr @scr_async_gen_next_f64(ptr ${g.name}, double ${a.name})`; } else if (t.kind === "bool") { @@ -806,7 +834,7 @@ export function emitAsyncExpr( const a = host.emitExpr(e.arg); const t = e.arg.type; if (isRefCounted(t)) host.toHeap(a); - if (t.kind === "f64" || t.kind === "date") { + if (t.kind === "f64") { host.declare(`declare ptr @scr_async_gen_return_f64(ptr, double)`); call = `call ptr @scr_async_gen_return_f64(ptr ${g.name}, double ${a.name})`; } else if (t.kind === "bool") { @@ -849,7 +877,7 @@ export function emitAsyncExpr( B.line(`call void @scr_gen_ret_none(ptr ${g.name})`); } else { sendArg((name, t) => { - if (t.kind === "f64" || t.kind === "date") { + if (t.kind === "f64") { host.declare(`declare void @scr_gen_ret_f64(ptr, double)`); B.line(`call void @scr_gen_ret_f64(ptr ${g.name}, double ${name})`); } else if (t.kind === "bool") { @@ -960,7 +988,7 @@ export function emitAsyncExpr( return { name: "", type: e.type }; } const t = B.tmp(); - if (e.type.kind === "f64" || e.type.kind === "date") { + if (e.type.kind === "f64") { const f64Await = host.awaitResultFn("f64"); host.declare(`declare double @${f64Await}(ptr)`); B.line(`${t} = call double @${f64Await}(ptr ${pr.name})`); @@ -1050,7 +1078,7 @@ export function emitAsyncExpr( const peek = host.unionPeek(u.name, def.id); if (host.coroAwait) host.emitCoroAwait(peek); let awaited: LlValue; - if (inner.kind === "f64" || inner.kind === "date") { + if (inner.kind === "f64") { const f64Await = host.awaitResultFn("f64"); host.declare(`declare double @${f64Await}(ptr)`); const x = B.tmp(); @@ -1136,7 +1164,6 @@ export function emitAsyncExpr( let resolve: string; const kindNums: Partial> = { f64: 0, - date: 0, bool: 1, string: 2, void: 3, @@ -1191,7 +1218,6 @@ export function emitAsyncExpr( B.line(`${p} = call ptr @scr_promise_new()`); const kindNums: Partial> = { f64: 0, - date: 0, bool: 1, string: 2, void: 3, diff --git a/packages/compiler/src/backend/llvm/expr-bytes.ts b/packages/compiler/src/backend/llvm/expr-bytes.ts index 41ddf2b9d..d5e87495a 100644 --- a/packages/compiler/src/backend/llvm/expr-bytes.ts +++ b/packages/compiler/src/backend/llvm/expr-bytes.ts @@ -1,14 +1,15 @@ /* 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"; -import { exactInteger, widenInteger } from "./integer-values.js"; +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 = { u8: { kind: 0, le: false }, @@ -86,17 +87,18 @@ export function emitBytesIndex( ): string { const B = host.B; const index = value.name; - const integer = exactInteger(host, value, expr); - if (inBounds && integer) { - const wide = widenInteger(host, integer); - if (host.sizeType === "i64") return wide; + // The exact integer, or a wide view that always equals the double (such + // as `i + 1` of a speculated counter). + const exact = alwaysExactInteger(host, value, exactInteger(host, value, expr)); + if (inBounds && exact !== null) { + if (host.sizeType === "i64") return exact; const narrow = B.tmp(); - B.line(`${narrow} = trunc i64 ${wide} to ${host.sizeType}`); + B.line(`${narrow} = trunc i64 ${exact} to ${host.sizeType}`); return narrow; } const len = emitBytesHeaderLength(host, receiver); - if (integer) { - const wideIndex = widenInteger(host, integer); + if (exact !== null) { + const wideIndex = exact; const wideLength = host.sizeType === "i64" ? len : B.tmp(); if (host.sizeType !== "i64") B.line(`${wideLength} = zext ${host.sizeType} ${len} to i64`); const inRange = B.tmp(); @@ -159,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`); @@ -183,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)}`); @@ -195,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(); @@ -234,6 +265,26 @@ export function emitBytesGet( inBounds = false, invalidNaN = false, owned = false, + scope = "", +): LlValue { + const outer = host.bytesAccessScope; + host.bytesAccessScope = scope; + try { + return emitScopedBytesGet(host, elem, receiver, index, expr, inBounds, invalidNaN, owned); + } finally { + host.bytesAccessScope = outer; + } +} + +function emitScopedBytesGet( + host: LlvmEmitterContext, + elem: IrBytesElem, + receiver: string, + index: LlValue, + expr: IrExpr | undefined, + inBounds: boolean, + invalidNaN: boolean, + owned: boolean, ): LlValue { const B = host.B; // Any invalid index traps (the array runtime's discipline). @@ -275,7 +326,9 @@ export function emitBytesGet( 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 }; + const integer = B.tmp(); + B.line(`${integer} = ${signed ? "sext" : "zext"} i32 ${u32} to i64`); + return { name: out, type: F64, uint32: u32, ordered: valid, nanInteger: integer }; } function emitBytesLoad( @@ -289,7 +342,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); } @@ -300,6 +353,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 @@ -546,6 +624,27 @@ export function emitBytesSet( expr?: IrExpr, inBounds = false, owned = false, + scope = "", +): void { + const outer = host.bytesAccessScope; + host.bytesAccessScope = scope; + try { + emitScopedBytesSet(host, elem, receiver, index, value, indexExpr, expr, inBounds, owned); + } finally { + host.bytesAccessScope = outer; + } +} + +function emitScopedBytesSet( + host: LlvmEmitterContext, + elem: IrBytesElem, + receiver: string, + index: LlValue, + value: LlValue, + indexExpr: IrExpr | undefined, + expr: IrExpr | undefined, + inBounds: boolean, + owned: boolean, ): void { const B = host.B; // Invalid integer-indexed writes are ignored after evaluating the RHS. @@ -617,7 +716,7 @@ export function emitBytesSet( 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(); @@ -739,6 +838,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)); @@ -767,6 +867,7 @@ export function emitBytesIntrinsic( host.bytesBounds.has(e), e.invalidNaN === true, host.bytesOwned.has(e), + host.bytesAliasScopes.get(e), ); case "slice": return call( @@ -936,6 +1037,23 @@ export function emitBytesIntrinsic( B.line(`${t} = fcmp one double ${idx.name}, ${f64Lit(-1)}`); return { name: t, type: e.type }; } + case "indexOfElem": + case "lastIndexOfElem": + return call( + "scr_bytes_index_of_elem", + "double (ptr, double, double, i1 zeroext)", + `ptr ${r.name}, double ${args[0]!.name}, double ${args[1]!.name}, i1 ${method === "lastIndexOfElem"}`, + false, + false, + ); + case "includesElem": + return call( + "scr_bytes_includes_elem", + "zeroext i1 (ptr, double, double)", + `ptr ${r.name}, double ${args[0]!.name}, double ${args[1]!.name}`, + false, + false, + ); case "fillElem": // Per-element TypedArray fill (non-u8): slice-style index // defaults, never throws; the receiver comes back +1. diff --git a/packages/compiler/src/backend/llvm/expr-callbacks.ts b/packages/compiler/src/backend/llvm/expr-callbacks.ts index 398aebb21..7a17f4fab 100644 --- a/packages/compiler/src/backend/llvm/expr-callbacks.ts +++ b/packages/compiler/src/backend/llvm/expr-callbacks.ts @@ -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); diff --git a/packages/compiler/src/backend/llvm/expr-calls.ts b/packages/compiler/src/backend/llvm/expr-calls.ts index a4e7cde2c..7faca5535 100644 --- a/packages/compiler/src/backend/llvm/expr-calls.ts +++ b/packages/compiler/src/backend/llvm/expr-calls.ts @@ -1,4 +1,5 @@ /* Focused LLVM expression emission extracted from emitter.ts. */ +import { arrayClassDescriptor } from "./classes.js"; import { InternalCompilerError } from "../../errors.js"; import { newValueMayThrow } from "../../ir/analysis.js"; import { @@ -42,8 +43,15 @@ 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"; import { emitScalarOptional, isScalarOptionalSource } from "./local-union-storage.js"; import { emitStackMapRead } from "./map-read-lifetimes.js"; @@ -166,6 +174,9 @@ export function emitCallExpr( return { name: host.emitOwnedNullableStack(a).box, type: a.type }; // An inline union field read copies its tag and bits. if (host.inlineFieldGet(a)) return host.emitReadReceiver(a); + // A scalar keyed record read writes its tag and bits. + if (a.kind === "recordKeyGet" && scalarRecordKeyGet(host, a)) + return { name: emitStackRecordKeyGet(host, a), type: a.type }; } if (inputs[index]) return host.emitReadReceiver(a); } @@ -240,38 +251,46 @@ export function emitCallExpr( ? INTEGER_DISPATCH_SUFFIX : ""; const target = `@${borrowed ? mangleBorrowedFunction(e.callee) : host.callTarget(e.callee)}${abi ? INT32_ABI_SUFFIX : ""}${dispatch}`; + // A call of an unwinding-ABI callee is an invoke (emitDirectCall): + // its landing pad unwinds, so no pending check follows it. if (e.type.kind === "void") { - B.line(`call void ${target}(${argList})`); - if (host.mayThrow.has(e.callee)) host.emitPendingCheck(); + const invoked = host.emitDirectCall(`call void ${target}(${argList})`, e.callee); + if (!invoked && host.mayThrow.has(e.callee)) host.emitPendingCheck(); if (borrowed) host.releaseTemps(host.frames.pop()!); return { name: "", type: e.type }; } const t = B.tmp(); if (abi?.union) { - B.line(`${t} = call ${SCALAR_UNION_RESULT} ${target}(${argList})`); - if (host.mayThrow.has(e.callee)) host.emitPendingCheck(); + const invoked = host.emitDirectCall( + `${t} = call ${SCALAR_UNION_RESULT} ${target}(${argList})`, + e.callee, + ); + if (!invoked && host.mayThrow.has(e.callee)) host.emitPendingCheck(); if (borrowed) host.releaseTemps(host.frames.pop()!); if (stackResult) return host.stackScalarUnion(t, e.type); return host.own({ name: host.boxScalarUnion(t, e.type), type: e.type }); } if (abi?.result) { - B.line(`${t} = call i32 ${target}(${argList})`); + const invoked = host.emitDirectCall(`${t} = call i32 ${target}(${argList})`, e.callee); const out = host.int32Result(t, e.type); - if (host.mayThrow.has(e.callee)) host.emitPendingCheck(out); + if (!invoked && host.mayThrow.has(e.callee)) host.emitPendingCheck(out); if (borrowed) host.releaseTemps(host.frames.pop()!); return out; } - B.line(`${t} = call ${host.llType(e.type)} ${target}(${argList})`); + const invoked = host.emitDirectCall( + `${t} = call ${host.llType(e.type)} ${target}(${argList})`, + e.callee, + ); let out: LlValue; if (borrowed && host.borrowedReturns.has(e.callee)) { // A borrowed-return body hands back +0 (a null dummy when it threw): // test the exception first, then own the result before the // argument snapshots that keep it reachable are released. - if (host.mayThrow.has(e.callee)) host.emitPendingCheck(); + if (!invoked && host.mayThrow.has(e.callee)) host.emitPendingCheck(); out = host.own({ name: host.retainFrame(t, e.type), type: e.type }); } else { out = host.own({ name: t, type: e.type }); - if (host.mayThrow.has(e.callee)) host.emitPendingCheck(out); + if (!invoked && host.mayThrow.has(e.callee)) host.emitPendingCheck(out); } if (borrowed) { host.toFrame(out); @@ -749,8 +768,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`); @@ -788,6 +806,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; @@ -801,7 +838,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 @@ -813,7 +861,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; @@ -853,7 +904,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(); ft.params.forEach((param, index) => { if (isRefCounted(param)) references.add(index); @@ -886,16 +937,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 = @@ -941,11 +1027,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); @@ -954,7 +1040,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 @@ -972,7 +1058,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); @@ -1150,6 +1236,17 @@ export function emitCallExpr( return host.own({ name: v.name, type: e.type }); } case "instanceOf": { + // An array is an instance of a class declared `extends Array` when + // its ext names that class (scr_arr_class_of). + if (e.value.type.kind === "array") { + const v = emitBorrowedInputs(host, [e.value])[0]!; + host.declare(`declare ptr @scr_arr_class_of(ptr)`); + const cls = host.B.tmp(); + const result = host.B.tmp(); + host.B.line(`${cls} = call ptr @scr_arr_class_of(ptr ${v.name})`); + host.B.line(`${result} = icmp eq ptr ${cls}, ${arrayClassDescriptor(e.className)}`); + return { name: result, type: e.type }; + } // O(1) preorder-interval test against the vtable the object // carries; the target's interval is a compile-time constant. if (e.value.type.kind !== "object") @@ -1346,6 +1443,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)) @@ -1362,10 +1475,11 @@ export function emitBorrowedResultCall(host: LlvmEmitterContext, e: ExprOf<"call ) .join(", "); const t = host.B.tmp(); - host.B.line( + const invoked = host.emitDirectCall( `${t} = call ${host.llType(e.type)} @${mangleBorrowedFunction(e.callee)}${abi ? INT32_ABI_SUFFIX : ""}(${argList}) ; borrowed result`, + e.callee, ); - if (host.mayThrow.has(e.callee)) host.emitPendingCheck(); + if (!invoked && host.mayThrow.has(e.callee)) host.emitPendingCheck(); return { name: t, type: e.type }; } diff --git a/packages/compiler/src/backend/llvm/expr-containers.ts b/packages/compiler/src/backend/llvm/expr-containers.ts index 1ba5da818..7eec14e4e 100644 --- a/packages/compiler/src/backend/llvm/expr-containers.ts +++ b/packages/compiler/src/backend/llvm/expr-containers.ts @@ -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, @@ -42,17 +42,21 @@ import { emitStringMapGetRef, emitStringMapGetScalar, emitStringMapHas, + noteNumberMapKey, } from "./map-number-lookup.js"; import { emitBorrowedInput } from "./borrowed-inputs.js"; 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"; import { exactInteger, widenInteger } from "./integer-values.js"; @@ -71,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); @@ -257,6 +261,16 @@ function emitDiscarded(host: LlvmEmitterContext, e: IrExpr): void { const start = host.emitExpr(e.args[0]!); const count = e.args[1] ? host.emitExpr(e.args[1]).name : F64_INF; const values = items.map((item) => host.emitExpr(item)); + // Before the values move into the buffer: a throw releases them with the + // frame. + host.emitPublishedArrayGuard( + r.name, + e.receiver.type, + 10, + start.name, + count, + f64Lit(values.length), + ); if (acc === "ref") values.forEach((value) => host.toHeap(value)); let buffer = "null"; if (values.length > 0) { @@ -312,6 +326,15 @@ export function charAtUnitRead( return read; } +/** The UTF-16 units an enclosing loop version proved the index of `e` + * below the count of (string-unit-versions.ts). */ +function provenUnitsRead(host: LlvmEmitterContext, e: IrExpr): string | null { + const units = host.stringUnitReads.get(e); + if (units !== undefined) return units; + const origin = host.charAtUnitReads.get(e); + return origin !== undefined ? (host.stringUnitReads.get(origin) ?? null) : null; +} + /** An enclosing loop version proved the string of `e` ASCII and its index * below the length (bytes-loop-versions.ts). */ function provenStringRead(host: LlvmEmitterContext, e: IrExpr): boolean { @@ -354,9 +377,14 @@ export function emitStrIntrinsic( const method = e.method; switch (method) { case "length": { + const t = B.tmp(); + const length = host.stringLengthReads.get(e); + if (length !== undefined) { + B.line(`${t} = sitofp i64 ${length} to double`); + return { name: t, type: e.type }; + } // sc_str_utf16_len (string-reads.ts) answers proven-ASCII strings inline. host.declare(STRING_UTF16_LEN_DECL); - const t = B.tmp(); B.line(`${t} = call double @sc_str_utf16_len(ptr ${r.name})`); return { name: t, type: e.type }; } @@ -366,6 +394,8 @@ export function emitStrIntrinsic( const index = e.args[0] ? exactInteger(host, args[0]!, e.args[0]) : null; if (index) { const wide = widenInteger(host, index); + const units = provenUnitsRead(host, e); + if (units !== null) return { name: emitUnitCharCode(host, units, wide), type: e.type }; if (provenStringRead(host, e)) return { name: emitAsciiCharCode(host, r.name, wide), type: e.type }; host.declare(STRING_CHAR_CODE_AT_INT_MARKER); @@ -536,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; @@ -762,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; @@ -804,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(); @@ -904,6 +969,12 @@ export function emitArrIntrinsic( B.line(`${t} = call ptr @${helper}(ptr ${r.name}, ptr ${sep.name})`); return host.own({ name: t, type: e.type }); } + if (elem.kind === "date") { + host.declare(`declare ptr @scr_date_arr_join(ptr, ptr)`); + const t = B.tmp(); + B.line(`${t} = call ptr @scr_date_arr_join(ptr ${r.name}, ptr ${sep.name})`); + return host.own({ name: t, type: e.type }); + } host.declare(`declare ptr @scr_arr_join(ptr, ptr)`); const t = B.tmp(); B.line(`${t} = call ptr @scr_arr_join(ptr ${r.name}, ptr ${sep.name})`); @@ -928,6 +999,16 @@ export function emitArrIntrinsic( case "sortPrimitive": case "toSortedPrimitive": { if (e.method === "sortPrimitive") host.emitPublishedArrayGuard(r.name, e.receiver.type, 7); + if (elem.kind === "date") { + // Dates order by their toString() text. + host.declare(`declare ptr @scr_arr_sort_by_text(ptr, i1 zeroext, ptr)`); + host.declare(`declare ptr @scr_date_object_to_string(ptr)`); + const sorted = B.tmp(); + B.line( + `${sorted} = call ptr @scr_arr_sort_by_text(ptr ${r.name}, i1 ${e.method === "toSortedPrimitive" ? 1 : 0}, ptr @scr_date_object_to_string)`, + ); + return host.own({ name: sorted, type: e.type }); + } host.declare(`declare ptr @scr_arr_sort_primitive(ptr, i1 zeroext)`); const result = B.tmp(); B.line( @@ -978,7 +1059,25 @@ export function emitArrIntrinsic( case "fill": case "fillUndefined": { const args = e.args.map((arg) => host.emitExpr(arg)); - host.emitPublishedArrayGuard(r.name, e.receiver.type, 7); + // fill's range is its last two arguments; copyWithin's target, + // start and end are its three. + if (e.method === "copyWithin") + host.emitPublishedArrayGuard( + r.name, + e.receiver.type, + 9, + args[0]!.name, + args[1]!.name, + args[2]!.name, + ); + else + host.emitPublishedArrayGuard( + r.name, + e.receiver.type, + 8, + args[args.length - 2]!.name, + args[args.length - 1]!.name, + ); if (host.checkedFills.has(e)) { // A new int32 array with checked stores, filled with a value that // may not be an int32: widen it first then. @@ -1042,7 +1141,7 @@ export function emitArrIntrinsic( // removes to the end (+Infinity, the slice convention). const start = host.emitExpr(e.args[0]!); const cnt = e.args[1] ? host.emitExpr(e.args[1]).name : F64_INF; - host.emitPublishedArrayGuard(r.name, e.receiver.type, 7); + host.emitPublishedArrayGuard(r.name, e.receiver.type, 10, start.name, cnt, f64Lit(0)); host.declare(`declare ptr @scr_arr_splice(ptr, double, double)`); const t = B.tmp(); B.line(`${t} = call ptr @scr_arr_splice(ptr ${r.name}, double ${start.name}, double ${cnt})`); @@ -1052,7 +1151,12 @@ export function emitArrIntrinsic( const start = host.emitExpr(e.args[0]!); const count = host.emitExpr(e.args[1]!); const items = emitBorrowedInput(host, e.args[2]!); - host.emitPublishedArrayGuard(r.name, e.receiver.type, 7); + if (host.publishedTypes?.containerGuarded(e.receiver.type) === true) { + host.declare(`declare double @scr_arr_len(ptr)`); + const n = B.tmp(); + B.line(`${n} = call double @scr_arr_len(ptr ${items.name})`); + host.emitPublishedArrayGuard(r.name, e.receiver.type, 10, start.name, count.name, n); + } host.declare(`declare ptr @scr_arr_splice_insert(ptr, double, double, ptr)`); const t = B.tmp(); B.line( @@ -1078,6 +1182,65 @@ export function emitArrIntrinsic( } } +/** Whether `e` is a Map.get that emitBorrowedMapGet can emit without a + * retain: a typed map with number or inline string keys, a reference value, + * and a nullable result (the value's pointer, or the absent arm's). */ +export function borrowableMapGet(host: LlvmEmitterContext, e: IrExpr): boolean { + if (e.kind !== "mapIntrinsic" || e.method !== "get" || e.args.length !== 1) return false; + if (!borrowsMapReadInputs(e) || e.receiver.type.kind !== "map") return false; + const { key, value } = e.receiver.type; + if (!isRefCounted(value) || value.kind === "union" || key.kind === "union") return false; + if (e.type.kind !== "union" || !host.nullableUnions.has(e.type.unionId)) return false; + const arg = e.args[0]!; + const access = mapKeyAccess(key); + return ( + (access === "f64" && arg.type.kind === "f64") || + (access === "str" && arg.type.kind === "string") + ); +} + +/** A borrowableMapGet read as the entry's own reference: no retain, and + * no owner. The caller proves that nothing can change the map while the + * value is used (findMapReadBorrows). Null, before emitting anything, for + * a key held as a split span, which takes the runtime's owned lookup. */ +export function emitBorrowedMapGet( + host: LlvmEmitterContext, + e: IrExpr & { kind: "mapIntrinsic" }, +): LlValue | null { + const arg = e.args[0]!; + if (arg.kind === "varRef" && host.splitSpans.get(arg.localId) !== undefined) return null; + const B = host.B; + const mapType = e.receiver.type as IrType & { kind: "map" }; + const r = host.emitStableReceiver(e.receiver, e.args); + const k = emitMapLookupKey(host, e.args[0]!, true); + const numberKey = inlineNumberKey(k); + const stringKey = entryStringKey(k); + const value = mapType.value; + let raw: string; + if (numberKey !== null) raw = emitNumberMapGetRef(host, r.name, numberKey, value, true); + else if (stringKey !== null) + raw = emitStringMapGetRef( + host, + r.name, + stringKey, + value, + compactMapValues(host, mapType.key, value), + true, + ); + else throw new InternalCompilerError("llvm emitter bug: borrowed Map.get without an inline key"); + const resultType = e.type as IrType & { kind: "union" }; + const absent = host.unitInstanceRef( + resultType.unionId, + undefinedArmTag(resultType, host.unionsById), + ); + if (absent === "null") return { name: raw, type: e.type }; + const isnull = B.tmp(), + t = B.tmp(); + B.line(`${isnull} = icmp eq ptr ${raw}, null`); + B.line(`${t} = select i1 ${isnull}, ptr ${absent}, ptr ${raw}`); + return { name: t, type: e.type }; +} + export function wrapNullable( host: LlvmEmitterContext, raw: string, @@ -1153,7 +1316,9 @@ function collectionOps(host: LlvmEmitterContext, key: IrType, value: IrType): st function newCollection(host: LlvmEmitterContext, key: IrType, value: IrType): string { const B = host.B; const arms = key.kind === "union" ? host.unionsById.get(key.unionId)?.arms : undefined; - const kind = mapKeyKindNum(key, arms); + // SCR_MAP_KEY_REF (2): shape-union keys are record pointers. + const kind = + key.kind === "union" && host.nullableUnions.shapes(key.unionId) ? 2 : mapKeyKindNum(key, arms); const m = B.tmp(); host.declare(`declare ptr @scr_map_new(i32, i32, ptr)`); // SCR_MAP_VAL_CREF (3): string keys and compact values (compactMapValues). @@ -1412,6 +1577,7 @@ export function emitMapLikeIntrinsic( : emitStringMapGetScalar(host, r.name, stringKey!); found = scalar.found; if (stringKey !== null) rememberMapEntry(host, e, r.name, stringKey, scalar.entry); + else rememberMapEntry(host, e, r.name, numberKey!, scalar.entry, "double"); readHit = () => { const hit = B.tmp(); B.line( @@ -1479,11 +1645,18 @@ export function emitMapLikeIntrinsic( // Key borrowed (the runtime retains stored string keys); the // value MOVES in (replacement releases the old value inside). const k = emitBorrowedInput(host, e.args[0]!); + if (kAcc === "f64") noteNumberMapKey(host, k, e.args[0]!); const v = host.emitExpr(e.args[1]!); const vAcc = elemAccess(receiverType.value); if (vAcc === "ref") host.toHeap(v); const ordinary = (): void => host.mapSet(r.name, kAcc, vAcc, k.name, v.name); - if (kAcc === "f64" && vAcc !== "ref" && host.readNumberMapWrites.has(e)) { + if ( + (kAcc === "str" || kAcc === "f64") && + vAcc !== "ref" && + emitFusedMapSet(host, e, r.name, k.name, { name: v.name, access: vAcc }, ordinary) + ) { + // Written through the read's entry, or inserted. + } else if (kAcc === "f64" && vAcc !== "ref" && host.readNumberMapWrites.has(e)) { const bits = B.tmp(); B.line( vAcc === "f64" @@ -1491,12 +1664,7 @@ export function emitMapLikeIntrinsic( : `${bits} = zext i1 ${v.name} to i64`, ); emitNumberMapSetScalar(host, r.name, k.name, bits, ordinary); - } else if ( - kAcc !== "str" || - vAcc === "ref" || - !emitFusedMapSet(host, e, r.name, k.name, { name: v.name, access: vAcc }, ordinary) - ) - ordinary(); + } else ordinary(); return { name: "", type: e.type }; } case "add": { @@ -1504,6 +1672,7 @@ export function emitMapLikeIntrinsic( // Element borrowed (the runtime retains stored strings); the unit // value is 0. Re-adding overwrites in place, preserving insertion. const k = emitBorrowedInput(host, e.args[0]!); + if (kAcc === "f64") noteNumberMapKey(host, k, e.args[0]!); if (kAcc === "f64" && host.readNumberMapWrites.has(e)) emitNumberMapSetScalar(host, r.name, k.name, null, () => host.mapSet(r.name, kAcc, "f64", k.name, f64Lit(0)), @@ -1513,7 +1682,10 @@ export function emitMapLikeIntrinsic( } case "has": { const k = borrowInputs ? host.emitReadReceiver(e.args[0]!) : host.emitExpr(e.args[0]!); - if (kAcc === "f64") return { name: emitNumberMapHas(host, r.name, k.name), type: e.type }; + if (kAcc === "f64") { + noteNumberMapKey(host, k, e.args[0]!); + return { name: emitNumberMapHas(host, r.name, k.name), type: e.type }; + } if (kAcc === "str") return { name: emitStringMapHas(host, r.name, k.name, compact), type: e.type }; host.declare(`declare zeroext i1 @scr_map_has_${kAcc}(ptr, ${kTy})`); diff --git a/packages/compiler/src/backend/llvm/expr-context.ts b/packages/compiler/src/backend/llvm/expr-context.ts index c6dc506f4..a5b7f61dd 100644 --- a/packages/compiler/src/backend/llvm/expr-context.ts +++ b/packages/compiler/src/backend/llvm/expr-context.ts @@ -21,6 +21,15 @@ 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; + /** An i64 equal to this number whenever `ordered` is true: the integer of + * a number that is an exact integer or NaN (a NaN-coded integer element + * read, a local that only holds those, or a sum, difference or product of + * them), so integer consumers need no conversion of the double. */ + nanInteger?: string; slot?: boolean; /** A sequence local held in a capture box; release the box itself. */ boxed?: boolean; @@ -30,6 +39,9 @@ export interface WideInteger { name: string; below: number; bound: number; + /** The double is the integer rounded once: the result of one operation on + * operands that always equal their doubles. */ + single?: true; } export type ExprOf = Extract; diff --git a/packages/compiler/src/backend/llvm/expr-control.ts b/packages/compiler/src/backend/llvm/expr-control.ts index 242f9997c..6e0fd4acb 100644 --- a/packages/compiler/src/backend/llvm/expr-control.ts +++ b/packages/compiler/src/backend/llvm/expr-control.ts @@ -2,11 +2,14 @@ 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"; export function emitControlExpr( host: LlvmEmitterContext, @@ -52,6 +55,8 @@ export function emitControlExpr( case "toBool": return { name: host.truthy(host.emitUnionProjection(e.operand)), type: e.type }; case "logical": { + const tagSet = e.op === "||" ? unionTagSet(e) : null; + if (tagSet) return { name: emitUnionTagSet(host, tagSet), type: e.type }; // JS value semantics: the result is the deciding operand itself. // Left evaluates once, ownership moves into the result slot; when // the branch takes the right operand the stale left releases first @@ -393,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" || @@ -487,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. @@ -550,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 @@ -576,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(); @@ -598,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); @@ -614,3 +641,97 @@ function literalChoice(e: IrExpr): boolean { if (e.kind === "strLit") return true; return e.kind === "ternary" && literalChoice(e.then) && literalChoice(e.else_); } + +type TagSet = { unionId: string; value: IrExpr; tags: Set }; + +/** An `||` chain of two or more tag tests of one local (`isTag(u, 1) || + * isTag(u, 4) || ...`, the shape of a union conversion's identity test). */ +function unionTagSet(e: IrExpr & { kind: "logical" }): TagSet | null { + const leaves: IrExpr[] = []; + const collect = (x: IrExpr): void => { + if (x.kind === "logical" && x.op === "||") { + collect(x.left); + collect(x.right); + } else leaves.push(x); + }; + collect(e); + const first = leaves[0]!; + if (first.kind !== "unionIsTag" || first.value.kind !== "varRef") return null; + const localId = first.value.localId; + const tags = new Set(); + for (const leaf of leaves) { + if ( + leaf.kind !== "unionIsTag" || + leaf.negated || + leaf.unionId !== first.unionId || + leaf.value.kind !== "varRef" || + leaf.value.localId !== localId + ) + return null; + tags.add(leaf.tag); + } + return { unionId: first.unionId, value: first.value, tags }; +} + +/** i1: the union value holds one of the tags. One read of the tag and one + * switch over the set; a shape union switches on the record's shape word + * after its unit encodings. */ +function emitUnionTagSet(host: LlvmEmitterContext, set: TagSet): string { + const B = host.B; + const u = host.emitUnionProjection(set.value).name; + const nullable = host.nullableUnions.get(set.unionId); + const yes = B.newLabel("tags.y"); + const no = B.newLabel("tags.n"); + const join = B.newLabel("tags.j"); + const target = (tag: number): string => (set.tags.has(tag) ? yes : no); + if (nullable?.repr === "shapes") { + if (nullable.unitTag >= 0) { + const isNull = B.tmp(); + const next = B.newLabel("tags.r"); + B.line(`${isNull} = icmp eq ptr ${u}, null`); + B.condBr(isNull, target(nullable.unitTag), next); + B.startBlock(next); + } + if (nullable.nullTag >= 0) { + const isNullArm = B.tmp(); + const next = B.newLabel("tags.r"); + B.line(`${isNullArm} = icmp eq ptr ${u}, ${NULLABLE_NULL}`); + B.condBr(isNullArm, target(nullable.nullTag), next); + B.startBlock(next); + } + const wp = B.tmp(); + const w = B.tmp(); + B.line( + `${wp} = getelementptr inbounds ${shapeWordHeader(host.sizeType)}, ptr ${u}, i32 0, i32 ${SHAPE_WORD_INDEX}`, + ); + B.line(`${w} = load i32, ptr ${wp}`); + const cases = nullable.arms + .filter((arm) => set.tags.has(arm.tag)) + .map((arm) => `i32 ${host.nullableUnions.shapeNumber(arm.shapeId)}, label %${yes}`); + B.terminate(`switch i32 ${w}, label %${no} [ ${cases.join(" ")} ]`); + } else { + const tag = nullable ? emitUnionTag(B, nullable, u) : host.unionTag(u, set.unionId); + const cases = [...set.tags].map((t) => `i32 ${t}, label %${yes}`); + B.terminate(`switch i32 ${tag}, label %${no} [ ${cases.join(" ")} ]`); + } + B.startBlock(yes); + B.br(join); + B.startBlock(no); + B.br(join); + B.startBlock(join); + const t = B.tmp(); + 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)); +} diff --git a/packages/compiler/src/backend/llvm/expr-dynamic.ts b/packages/compiler/src/backend/llvm/expr-dynamic.ts index 07a538184..7f4b7e48a 100644 --- a/packages/compiler/src/backend/llvm/expr-dynamic.ts +++ b/packages/compiler/src/backend/llvm/expr-dynamic.ts @@ -11,17 +11,19 @@ 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, type IrExpr, type IrType, isDynTypedRefType, + textCodecRecordClass, isRefCounted, isUnitType, typeEquals, typeKey, + VOID, } from "../../ir/ir.js"; import { BYTES_ELEM_NUM, closureIdentityEqual } from "./common.js"; import { DYN_KIND } from "./dyn.js"; @@ -29,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( @@ -56,6 +59,7 @@ export function emitDynamicExpr( | "caughtNarrow" | "caughtToDyn" >, + discarded = false, ): LlValue { const B = host.B; switch (e.kind) { @@ -86,7 +90,10 @@ export function emitDynamicExpr( // Bytes already box their shared mutable storage directly. A second // capsule would split identity between checked and native views. const errorRef = v.type.kind === "object" && host.shapeHost.isErrorClass(v.type.className); - if (!errorRef && ((e.liveRef && v.type.kind !== "bytes") || identityRef)) { + // A TextEncoder, TextDecoder or StringDecoder record boxes as its own handle, which + // keeps the record's identity (toDynHelper). + const codecRef = textCodecRecordClass(v.type, (id) => host.recordsById.get(id)) !== null; + if (!errorRef && !codecRef && ((e.liveRef && v.type.kind !== "bytes") || identityRef)) { if (v.type.kind === "union") { const adapter = host.liveDynUnionRefAdapter(v.type); const boxed = B.tmp(); @@ -115,6 +122,10 @@ export function emitDynamicExpr( const box = host.dyn.dynFuncBoxHelper(v.type); const t = B.tmp(); B.line(`${t} = call ptr @${box}(ptr ${v.name}, ptr ${name})`); + if (e.fnLength !== undefined) { + host.declare(`declare void @scr_dyn_function_set_length(ptr, i32)`); + B.line(`call void @scr_dyn_function_set_length(ptr ${t}, i32 ${e.fnLength})`); + } return host.own({ name: t, type: e.type }); } const conv = host.dyn.toDynHelper(v.type); @@ -253,6 +264,18 @@ export function emitDynamicExpr( // Each call passes its method name's id slot, so only the first call // looks the name up. const invoke = DYN_BUFFER_METHODS.get(e.method) ?? "scr_dyn_invoke_prepared_id"; + // A statement-position push drops the new length, so its entry + // need not box it. + if (discarded && e.method === "push" && invoke === "scr_dyn_invoke_prepared_id") { + host.declare( + `declare void @scr_dyn_invoke_discard_id(ptr, ptr, ptr, ptr, ptr, ${host.sizeType}, ptr)`, + ); + B.line( + `call void @scr_dyn_invoke_discard_id(ptr ${recv.name}, ptr ${prepared}, ptr ${host.cstr(e.method)}, ptr ${host.methodIdSlot(e.method)}, ptr ${argsPtr}, ${host.sizeType} ${args.length}, ptr ${host.cstr(e.calleeName)})`, + ); + host.emitPendingCheck(); + return { name: "", type: VOID }; + } host.declare(`declare ptr @${invoke}(ptr, ptr, ptr, ptr, ptr, ${host.sizeType}, ptr)`); let name = host.cstr(e.calleeName); if (e.calleeNameValue !== undefined) { @@ -294,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})`, ); @@ -880,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. diff --git a/packages/compiler/src/backend/llvm/expr-primitives.test.ts b/packages/compiler/src/backend/llvm/expr-primitives.test.ts index f343c956e..93555c101 100644 --- a/packages/compiler/src/backend/llvm/expr-primitives.test.ts +++ b/packages/compiler/src/backend/llvm/expr-primitives.test.ts @@ -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)( diff --git a/packages/compiler/src/backend/llvm/expr-primitives.ts b/packages/compiler/src/backend/llvm/expr-primitives.ts index 1eec6a965..1a4973a3a 100644 --- a/packages/compiler/src/backend/llvm/expr-primitives.ts +++ b/packages/compiler/src/backend/llvm/expr-primitives.ts @@ -13,12 +13,14 @@ import { emitStringInputs } from "./string-lifetimes.js"; import { emitStringParts, numberPart, stringParts } from "./string-construction.js"; import { emitBorrowedInputs } from "./borrowed-inputs.js"; import { + exactCompareOperands, exactInteger, int32Number, integerNumber, narrowInteger, wideArithmetic, widenInteger, + type LlInteger, } from "./integer-values.js"; import { bitwiseIdentityOperand, @@ -44,7 +46,80 @@ import { slotWidth, type ArrayAccess, } from "./dense-array-access.js"; -import { emitWrappingSum, markWrappingOperands } from "./wrapping-sums.js"; +import { + emitNanArithmetic, + emitWrappingSum, + integerOrNaNRange, + markWrappingOperands, +} from "./wrapping-sums.js"; +import { emitPowF64, emitPowerOfTwoQuotient, isPowerOfTwoCall } from "./lib-filesystem.js"; + +/** The signed integer predicates of the number compares. */ +const INTEGER_COMPARES: Record = { + "<": "slt", + "<=": "sle", + ">": "sgt", + ">=": "sge", + "===": "eq", + "!==": "ne", +}; + +const UNSIGNED_COMPARES: Record = { + "<": "ult", + "<=": "ule", + ">": "ugt", + ">=": "uge", + "===": "eq", + "!==": "ne", +}; + +/** A number compare of operands that are each an exact integer or an + * integer-or-NaN value (integerOrNaNRange: integer typed-array elements, + * `charCodeAt` results, sums of those), all within int32 or all within + * uint32: their 32-bit views compared, and NaN comparing false (unequal for + * `!==`). The 32-bit view is the value's ToUint32, which holds its bits + * whenever it is not NaN. Null when an operand has no such view. */ +function emitIntegerOrNaNCompare( + host: LlvmEmitterContext, + e: ExprOf<"bin">, + l: LlValue, + li: LlInteger | null, + r: LlValue, + ri: LlInteger | null, +): string | null { + const lr = li?.range ?? integerOrNaNRange(host, e.left), + rr = ri?.range ?? integerOrNaNRange(host, e.right); + if (!lr || !rr) return null; + const signed = withinInt32(lr) && withinInt32(rr); + if (!signed && !(withinUint32(lr) && withinUint32(rr))) return null; + const bits = (value: LlValue, integer: LlInteger | null): string | undefined => + integer ? narrowInteger(host, integer) : value.uint32; + const left = bits(l, li), + right = bits(r, ri); + if (left === undefined || right === undefined) return null; + const B = host.B; + const compare = B.tmp(); + B.line( + `${compare} = icmp ${(signed ? INTEGER_COMPARES : UNSIGNED_COMPARES)[e.op]} i32 ${left}, ${right}`, + ); + let result = compare; + for (const [value, integer] of [ + [l, li], + [r, ri], + ] as const) { + if (integer) continue; + const ordered = B.tmp(), + combined = B.tmp(); + B.line(`${ordered} = fcmp ord double ${value.name}, 0.0`); + if (e.op === "!==") { + const unordered = B.tmp(); + B.line(`${unordered} = xor i1 ${ordered}, true`); + B.line(`${combined} = or i1 ${result}, ${unordered}`); + } else B.line(`${combined} = and i1 ${result}, ${ordered}`); + result = combined; + } + return result; +} export function emitLiteralExpr( host: LlvmEmitterContext, @@ -135,7 +210,19 @@ export function emitLiteralExpr( // the binding. Consumers must not reload its current view. const uint32 = B.tmp(); B.line(`${uint32} = load i32, ptr ${integerView}`); - return { name: t, type: e.type, uint32 }; + const orderedView = host.orderedViews.get(e.localId); + const range = orderedView && host.integerOrNaNLocals.get(e.localId); + if (!orderedView || !range) return { name: t, type: e.type, uint32 }; + const ordered = B.tmp(); + B.line(`${ordered} = load i1, ptr ${orderedView}`); + // The view holds the integer's low 32 bits (0 for NaN): its integer + // when the range is within int32 or uint32. + const signed = range.min >= -2147483648 && range.max <= 2147483647; + if (!signed && !(range.min >= 0 && range.max <= 4294967295)) + return { name: t, type: e.type, uint32, ordered }; + const nanInteger = B.tmp(); + B.line(`${nanInteger} = ${signed ? "sext" : "zext"} i32 ${uint32} to i64`); + return { name: t, type: e.type, uint32, ordered, nanInteger }; } if (isRefCounted(e.type)) { // An immortal Immutable constant: nothing to count. @@ -235,7 +322,6 @@ export function emitOperatorExpr( "===": "oeq", "!==": "une", }; - const libm: Record = { "**": "pow" }; const bit: Record = { "&": "and", "|": "or", @@ -297,8 +383,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) }; @@ -318,15 +404,7 @@ export function emitOperatorExpr( if (integer) return integer; } if (cmp[e.op] !== undefined) { - const integerCmp: Record = { - "<": "slt", - "<=": "sle", - ">": "sgt", - ">=": "sge", - "===": "eq", - "!==": "ne", - }; - B.line(`${t} = icmp ${integerCmp[e.op]} i64 ${left}, ${right}`); + B.line(`${t} = icmp ${INTEGER_COMPARES[e.op]} i64 ${left}, ${right}`); return { name: t, type: e.type }; } } @@ -345,18 +423,35 @@ export function emitOperatorExpr( : emitCheckedRemainder(host, l, r, li, ri); if (checked) return checked; } + if (e.op === "/" && isPowerOfTwoCall(e.right)) + 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) { + // The integer of an exact integer or NaN, for index and loop guards. + const nan = emitNanArithmetic(host, e, l, r, li, ri); + if (wrapped !== undefined || wideInteger || nan) { const value: LlValue = { name: t, type: e.type }; - if (uint32 !== undefined) value.uint32 = uint32; + if (nan) Object.assign(value, nan); + if (wrapped !== undefined) Object.assign(value, wrapped); if (wideInteger) value.wideInteger = wideInteger; return value; } - } else B.line(`${t} = fcmp ${cmp[e.op]} double ${l.name}, ${r.name}`); + } else { + // An operand with only a wide view, such as `i + 1` or `2 * i + 1` + // of a speculated counter: compare the integers when that gives the + // doubles' result. + const operands = exactCompareOperands(host, l, li, r, ri); + if (operands) { + B.line(`${t} = icmp ${INTEGER_COMPARES[e.op]} i64 ${operands[0]}, ${operands[1]}`); + return { name: t, type: e.type }; + } + const nanCoded = emitIntegerOrNaNCompare(host, e, l, li, r, ri); + if (nanCoded) return { name: nanCoded, type: e.type }; + B.line(`${t} = fcmp ${cmp[e.op]} double ${l.name}, ${r.name}`); + } } else if (bit[e.op] !== undefined) { if (e.left.type.kind !== "f64" || e.right.type.kind !== "f64") { throw new LlvmUnsupportedError( @@ -395,32 +490,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 }; } @@ -573,7 +644,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, @@ -585,6 +656,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. */ @@ -870,6 +964,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< @@ -941,7 +1063,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[] = []; @@ -1076,9 +1225,10 @@ export function emitContainerExpr( const idx = host.emitExpr(e.index); if (e.arr.type.kind !== "array") throw new InternalCompilerError("llvm emitter bug: arrayState on non-array"); + const proven = host.provenDenseIndex(e.arr, e.index); const t = host.checkedInt32Array(e.arr) ? host.emitByElementKind(arr.name, "double", (kind) => - emitDenseArrayState(host, arr.name, idx, e.index, "state", slotWidth(kind)), + emitDenseArrayState(host, arr.name, idx, e.index, "state", slotWidth(kind), proven), )! : emitDenseArrayState( host, @@ -1087,6 +1237,7 @@ export function emitContainerExpr( e.index, "state", host.int32Array(e.arr) ? slotWidth("i32") : denseSlotWidth(host, e.arr.type.elem), + proven, ); return { name: t, type: e.type }; } @@ -1317,19 +1468,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; -} diff --git a/packages/compiler/src/backend/llvm/expr-records.ts b/packages/compiler/src/backend/llvm/expr-records.ts index bca728e9d..58f1297b3 100644 --- a/packages/compiler/src/backend/llvm/expr-records.ts +++ b/packages/compiler/src/backend/llvm/expr-records.ts @@ -6,8 +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 @@ -159,9 +162,64 @@ 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 }); +} + +/** Substitutions whose result owns its text: once the runtime call returns, + * no operand is read again. */ +function releasesInputsOnReturn(method: string): boolean { + return method === "replace" || method === "replaceAll"; +} + export function emitRegexIntrinsic( host: LlvmEmitterContext, e: IrExpr & { kind: "regexIntrinsic" }, +): LlValue { + if (!releasesInputsOnReturn(e.method)) return emitRegexOperation(host, e); + // The operands' temps (the subject of a chained substitution, a computed + // replacement) are released when the call returns rather than at the + // end of the statement, so a chain of substitutions holds at most its + // current subject and result. + host.frames.push([]); + const out = emitRegexOperation(host, e); + host.toFrame(out); + host.releaseTemps(host.frames.pop()!); + return host.own(out); +} + +function emitRegexOperation( + host: LlvmEmitterContext, + e: IrExpr & { kind: "regexIntrinsic" }, ): LlValue { const B = host.B; const method = e.method; @@ -233,11 +291,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)`); { @@ -322,6 +385,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( @@ -337,13 +402,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}`); @@ -365,6 +463,188 @@ 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`, at `number | undefined`) and whose every hit + * is a number or boolean arm of it, or an inline field of the same union. + * Projections (`??`, `!`, tag tests) read it as a private stack box, so + * the read allocates nothing. */ +export function scalarRecordKeyGet(host: LlvmEmitterContext, e: IrExpr): boolean { + if (e.kind !== "recordKeyGet" || e.missNaN === true || e.type.kind !== "union") return false; + const resultType = e.type; + const def = host.unionsById.get(resultType.unionId); + if (!isScalarUnionDef(def) || undefinedArmTag(resultType, host.unionsById) < 0) return false; + const isArm = (t: IrType): boolean => + (t.kind === "f64" || t.kind === "bool") && def!.arms.some((a) => typeEquals(a, t)); + const shape = host.recordShape(e.shapeId); + if (e.overflowOnly !== true) { + for (const f of shape.fields) { + const inline = host.recordFieldInline(e.shapeId, f.name); + if (!isArm(f.type) && !(inline && typeEquals(f.type, resultType))) return false; + } + } + return shape.indexValue === undefined || isArm(shape.indexValue); +} + +/** A scalarRecordKeyGet read as a private stack box: the tag and bits of + * the declared field or overflow entry the key names, or the undefined arm + * for a miss. The box must never reach an RC entry point. */ +export function emitStackRecordKeyGet( + host: LlvmEmitterContext, + e: IrExpr & { kind: "recordKeyGet" }, +): string { + const B = host.B; + if (e.type.kind !== "union") + throw new InternalCompilerError("llvm emitter bug: stack keyed read of a non-union"); + const resultType = e.type; + const def = host.unionsById.get(resultType.unionId)!; + const armTag = (t: IrType): number => def.arms.findIndex((a) => typeEquals(a, t)); + const obj = host.emitExpr(e.obj); + const key = host.emitExpr(e.key); + const box = B.slot(); + B.entryAllocas.push(`${box} = alloca %ScrUnion`); + const tagPtr = B.tmp(), + bitsPtr = B.tmp(); + B.line(`store ${host.sizeType} -1, ptr ${box}`); + B.line(`${tagPtr} = getelementptr inbounds %ScrUnion, ptr ${box}, i64 0, i32 1`); + B.line(`${bitsPtr} = getelementptr inbounds %ScrUnion, ptr ${box}, i64 0, i32 5`); + const join = B.newLabel("rks.j"); + const storeScalar = (t: IrType, value: string): void => { + const bits = B.tmp(); + if (t.kind === "f64") B.line(`${bits} = bitcast double ${value} to i64`); + else B.line(`${bits} = zext i1 ${value} to i64`); + B.line(`store i32 ${armTag(t)}, ptr ${tagPtr}`); + B.line(`store i64 ${bits}, ptr ${bitsPtr}`); + }; + 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("rks.h"); + const ln = B.newLabel("rks.n"); + B.condBr(hit, lh, ln); + B.startBlock(lh); + const { ptr, type, inline } = host.recordFieldPtr(obj.name, e.shapeId, f.name); + if (inline) { + const { tag, bits } = loadInlineUnion(B, ptr); + const present = presentBits(B, tag, bits, undefinedArmTag(type, host.unionsById)); + B.line(`store i32 ${tag}, ptr ${tagPtr}`); + B.line(`store i64 ${present}, ptr ${bitsPtr}`); + } else { + storeScalar(type, host.loadRecordField(ptr, type, false)); + } + B.br(join); + B.startBlock(ln); + } + const iv = shape.indexValue; + if (iv) { + const ovf = host.recordOvfPtr(obj.name, e.shapeId); + const outTy = iv.kind === "f64" ? "double" : "i8"; + const outSlot = B.slot(); + B.entryAllocas.push(`${outSlot} = alloca ${outTy}`); + B.line(`store ${outTy} ${iv.kind === "f64" ? f64Lit(0) : "0"}, ptr ${outSlot}`); + const getter = `scr_map_get_str_${iv.kind === "f64" ? "f64" : "bool"}`; + host.declare(`declare zeroext i1 @${getter}(ptr, ptr, ptr)`); + const found = B.tmp(); + B.line(`${found} = call zeroext i1 @${getter}(ptr ${ovf}, ptr ${key.name}, ptr ${outSlot})`); + const lh = B.newLabel("rks.h"); + const ln = B.newLabel("rks.n"); + B.condBr(found, lh, ln); + B.startBlock(lh); + const raw = B.tmp(); + B.line(`${raw} = load ${outTy}, ptr ${outSlot}`); + let value = raw; + if (iv.kind === "bool") { + value = B.tmp(); + B.line(`${value} = trunc i8 ${raw} to i1`); + } + storeScalar(iv, value); + B.br(join); + B.startBlock(ln); + } + // A miss is the undefined arm. + B.line(`store i32 ${undefinedArmTag(resultType, host.unionsById)}, ptr ${tagPtr}`); + B.line(`store i64 0, ptr ${bitsPtr}`); + B.br(join); + B.startBlock(join); + return box; +} + export function keyedRecordReadInto( host: LlvmEmitterContext, slot: string, @@ -376,6 +656,7 @@ export function keyedRecordReadInto( overflowOnly: boolean, loc?: SrcLoc, missNaN = false, + missInto: (() => void) | null = null, ): void { const B = host.B; const shape = host.recordShape(shapeId); @@ -551,35 +832,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; } diff --git a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts index aee7be180..f711795d3 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts @@ -1,7 +1,7 @@ import { typedRefConstructor } from "./shapes.js"; /* Focused LLVM expression emission extracted from emitter.ts. */ import { InternalCompilerError } from "../../errors.js"; -import { streamTypedRefEligible, undefinedArmTag } from "../../ir/analysis.js"; +import { streamRooted, streamTypedRefEligible, undefinedArmTag } from "../../ir/analysis.js"; import { type IrRecordShape, type IrType, @@ -16,9 +16,11 @@ import { KEY_ORDER_FIELD, shapeKeyOrders, isDynTypedRefType, + textCodecRecordClass, isRefCounted, shapeHasAccessorSlots, typeKey, + type IrClassDef, } from "../../ir/ir.js"; import { mangleFunction, mangleGlobal, mangleRecordStruct } from "../mangle.js"; import { BlockBuilder } from "./blocks.js"; @@ -121,12 +123,7 @@ export function dynPromiseAdapter(host: LlvmEmitterContext, inner: IrType): stri if (inner.kind === "void") { host.declare(`declare void @scr_promise_fulfill_void(ptr)`); B.line(`call void @scr_promise_fulfill_void(ptr %dst)`); - } else if ( - inner.kind === "f64" || - inner.kind === "date" || - inner.kind === "procStream" || - inner.kind === "bool" - ) { + } else if (inner.kind === "f64" || inner.kind === "procStream" || inner.kind === "bool") { const fn = inner.kind !== "bool" ? "scr_promise_fulfill_f64" : "scr_promise_fulfill_bool"; host.declare( `declare void @${fn}(ptr, ${host.llType(inner)}${inner.kind === "bool" ? " zeroext" : ""})`, @@ -187,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)`); @@ -276,9 +277,7 @@ export function streamTypedRefCommitAdapter( ); }); } - const symbols = new Map( - (meta.def.symbolFields ?? []).map((symbol) => [symbol.field, symbol.globalId]), - ); + const symbols = new Map((meta.def.symbolFields ?? []).map((symbol) => [symbol.field, symbol])); const fields = meta.def.fields.filter( (field) => (isClassOwnEnumerableFieldName(field.name) || symbols.has(field.name)) && @@ -298,13 +297,15 @@ export function streamTypedRefCommitAdapter( const inline = host.inlineUnions.classField(t.className, field.name); const fieldTy = int32 ? "i32" : llFieldType(field.type, inline); const checkTy = host.llType(field.type); - const symbolGlobal = symbols.get(field.name); - if (symbolGlobal) { + const symbolKey = symbols.get(field.name); + if (symbolKey) { host.declare(`declare ptr @scr_dyn_symbol_key_get(ptr, ptr, i1 zeroext)`); host.declare(`declare void @scr_dyn_release_v(ptr)`); + const keyLines = symbolKeyLines(host, symbolKey, `f${index}_keyraw`); lines.push( - ` %f${index}_keyraw = load ptr, ptr @${mangleGlobal(symbolGlobal)}`, + ...keyLines.load, ` %f${index}_key = call ptr @${host.dyn.toDynHelper(SYMBOL_T)}(ptr %f${index}_keyraw)`, + ...keyLines.release, ); if (field.type.kind === "symbol") { // A base constructor can mutate the instance before this @@ -332,7 +333,7 @@ export function streamTypedRefCommitAdapter( lines.push( ` %${raw} = call ptr @scr_dyn_obj_get(ptr %d, ptr ${host.cstr(field.name)}, ${host.sizeType} ${Buffer.byteLength(field.name, "utf8")})`, ); - if (meta.def.tracksOwnFields && !symbolGlobal) + if (meta.def.tracksOwnFields && !symbolKey) lines.push( ` %f${index}_present = icmp ne ptr %${raw}, null`, ` br i1 %f${index}_present, label %f${index}_read, label %${after}`, @@ -343,7 +344,7 @@ export function streamTypedRefCommitAdapter( ` %${undef} = call ptr @scr_dyn_undefined()`, ` %${input} = select i1 %${missing}, ptr %${undef}, ptr %${raw}`, ` %${next} = call ${field.type.kind === "bool" ? "zeroext " : ""}${checkTy} @${host.dyn.dynCheckHelper(field.type)}(ptr %${input}, ptr null)`, - ...(symbolGlobal ? [` call void @scr_dyn_release_v(ptr %${raw})`] : []), + ...(symbolKey ? [` call void @scr_dyn_release_v(ptr %${raw})`] : []), ...host.pendingTestLines(`%${pending}`), ` br i1 %${pending}, label %done, label %${store}`, `${store}:`, @@ -441,10 +442,14 @@ export function streamTypedRefCommitAdapter( host.declare(`declare ptr @scr_str_new(ptr, ${host.sizeType})`); host.declare(`declare void @scr_str_release(ptr)`); host.declare(`declare void @scr_dyn_key_delete(ptr, ptr, i1 zeroext)`); + // The view of a sealed or frozen instance holds its fields as + // non-configurable entries, which the bag copies. + const withAttributes = meta.def.fields.some((field) => field.name === CLASS_ATTRIBUTES); + const placeholderSet = withAttributes ? "scr_dyn_obj_set_field" : "scr_dyn_obj_set"; if (meta.def.tracksOwnFields) { host.declare(`declare ptr @scr_dyn_retain_v(ptr)`); - host.declare(`declare void @scr_dyn_obj_set(ptr, ptr, ${host.sizeType}, ptr)`); - } + host.declare(`declare void @${placeholderSet}(ptr, ptr, ${host.sizeType}, ptr)`); + } else if (withAttributes) host.declare(`declare void @scr_dyn_obj_delete_field(ptr, ptr)`); lines.push( ` %bag = call ptr @scr_dyn_new_obj()`, ` %bag_copy = call ptr @scr_dyn_copy_property_descriptors(ptr %bag, ptr %d)`, @@ -481,14 +486,16 @@ export function streamTypedRefCommitAdapter( `bag_field${index}_present:`, ` %bag_undefined${index} = call ptr @scr_dyn_undefined()`, ` %bag_placeholder${index} = call ptr @scr_dyn_retain_v(ptr %bag_undefined${index})`, - ` call void @scr_dyn_obj_set(ptr %bag, ptr ${host.cstr(field.name)}, ${host.sizeType} ${Buffer.byteLength(field.name, "utf8")}, ptr %bag_placeholder${index})`, + ` call void @${placeholderSet}(ptr %bag, ptr ${host.cstr(field.name)}, ${host.sizeType} ${Buffer.byteLength(field.name, "utf8")}, ptr %bag_placeholder${index})`, ` br label %bag_field${index}_after`, `bag_field${index}_after:`, ); else lines.push( ` %bag_key${index} = call ptr @scr_str_new(ptr ${host.cstr(field.name)}, ${host.sizeType} ${Buffer.byteLength(field.name, "utf8")})`, - ` call void @scr_dyn_key_delete(ptr %bag, ptr %bag_key${index}, i1 zeroext false)`, + withAttributes + ? ` call void @scr_dyn_obj_delete_field(ptr %bag, ptr %bag_key${index})` + : ` call void @scr_dyn_key_delete(ptr %bag, ptr %bag_key${index}, i1 zeroext false)`, ` call void @scr_str_release(ptr %bag_key${index})`, ); }); @@ -496,18 +503,24 @@ export function streamTypedRefCommitAdapter( host.declare( meta.def.tracksOwnFields ? `declare void @scr_dyn_symbol_key_placeholder(ptr, ptr)` - : `declare void @scr_dyn_key_delete_computed(ptr, ptr, i1 zeroext)`, + : withAttributes + ? `declare void @scr_dyn_obj_delete_symbol_field(ptr, ptr)` + : `declare void @scr_dyn_key_delete_computed(ptr, ptr, i1 zeroext)`, ); - [...symbols.values()].forEach((globalId, index) => + [...symbols.values()].forEach((symbol, index) => { + const keyLines = symbolKeyLines(host, symbol, `bag_symraw${index}`); lines.push( - ` %bag_symraw${index} = load ptr, ptr @${mangleGlobal(globalId)}`, + ...keyLines.load, ` %bag_sym${index} = call ptr @${host.dyn.toDynHelper(SYMBOL_T)}(ptr %bag_symraw${index})`, + ...keyLines.release, meta.def.tracksOwnFields ? ` call void @scr_dyn_symbol_key_placeholder(ptr %bag, ptr %bag_sym${index})` - : ` call void @scr_dyn_key_delete_computed(ptr %bag, ptr %bag_sym${index}, i1 zeroext false)`, + : withAttributes + ? ` call void @scr_dyn_obj_delete_symbol_field(ptr %bag, ptr %bag_sym${index})` + : ` call void @scr_dyn_key_delete_computed(ptr %bag, ptr %bag_sym${index}, i1 zeroext false)`, ` call void @scr_dyn_release_v(ptr %bag_sym${index})`, - ), - ); + ); + }); const { index } = classFieldIndex(meta, DYN_CLASS_PROPERTIES); lines.push( ` %bag_ptr = getelementptr inbounds %${classStructSym(t.className)}, ptr %target, i64 0, i32 ${index}`, @@ -520,6 +533,20 @@ export function streamTypedRefCommitAdapter( host.declare(`declare void @scr_error_commit_dyn(ptr, ptr)`); lines.push(` call void @scr_error_commit_dyn(ptr %target, ptr %d)`); } + // Untyped Object.freeze, seal and preventExtensions apply to the view; + // the instance's integrity word records them. + if (host.untypedIntegrity && meta.def.fields.some((field) => field.name === CLASS_ATTRIBUTES)) { + const bag = meta.def.fields.some((field) => field.name === DYN_CLASS_PROPERTIES) + ? "%bag" + : "null"; + host.declare(`declare double @scr_dyn_class_view_integrity(ptr, double, ptr)`); + lines.push( + ` %word_ptr = getelementptr inbounds %${classStructSym(t.className)}, ptr %target, i64 0, i32 ${classFieldIndex(meta, CLASS_ATTRIBUTES).index}`, + ` %word = load double, ptr %word_ptr`, + ` %word_next = call double @scr_dyn_class_view_integrity(ptr %d, double %word, ptr ${bag})`, + ` store double %word_next, ptr %word_ptr`, + ); + } lines.push(` br label %done`, `done:`, ` ret void`, `}`, ``); host.resolveThunkDefs.push(...lines); return `@${commit}`; @@ -597,12 +624,21 @@ export function liveDynUnionRefAdapter( if (!union) { throw new InternalCompilerError(`llvm emitter bug: live dyn ref of unknown union ${t.unionId}`); } + // Codec records box as their own handles, by reference, through toDynHelper. + const codecArm = (arm: IrType): boolean => + textCodecRecordClass(arm, (id) => host.recordsById.get(id)) !== null; const mutableArms = union.arms .map((arm, tag) => ({ arm, tag })) .filter( - ({ arm }) => arm.kind !== "bytes" && (streamTypedRefEligible(arm) || isDynTypedRefType(arm)), + ({ arm }) => + arm.kind !== "bytes" && + !codecArm(arm) && + (streamTypedRefEligible(arm) || isDynTypedRefType(arm)), ); - if (mutableArms.length === 0 && !union.arms.some((arm) => arm.kind === "bytes")) { + if ( + mutableArms.length === 0 && + !union.arms.some((arm) => arm.kind === "bytes" || codecArm(arm)) + ) { throw new InternalCompilerError(`llvm emitter bug: live dyn ref of immutable union ${key}`); } @@ -695,6 +731,27 @@ export function liveDynUnionRefAdapter( return sym; } +/** The raw ScrSym of a class's symbol field key into `%name`: a module + * key is borrowed from its global, a well-known one comes from the + * runtime's registry (+1, released by the `release` lines once the key + * value holds it). */ +function symbolKeyLines( + host: LlvmEmitterContext, + symbol: NonNullable[number], + name: string, +): { load: string[]; release: string[] } { + if ("globalId" in symbol) + return { load: [` %${name} = load ptr, ptr @${mangleGlobal(symbol.globalId)}`], release: [] }; + host.declare("declare ptr @scr_sym_well_known(ptr)"); + host.declare("declare void @scr_sym_release(ptr)"); + return { + load: [ + ` %${name} = call ptr @scr_sym_well_known(ptr ${host.internLiteral(symbol.wellKnown)})`, + ], + release: [` call void @scr_sym_release(ptr %${name})`], + }; +} + export function streamTypedRefBoxValue( host: LlvmEmitterContext, B: BlockBuilder, @@ -712,7 +769,13 @@ export function streamTypedRefBoxValue( B.line(`${boxed} = call ptr @${host.liveDynUnionRefAdapter(t)}(ptr ${value})`); return boxed; } - if (t.kind === "bytes" || (!streamTypedRefEligible(t) && !isDynTypedRefType(t))) { + // A codec record boxes as its own handle, by reference (toDynHelper), as + // it does outside a live view. + if ( + t.kind === "bytes" || + textCodecRecordClass(t, (id) => host.recordsById.get(id)) !== null || + (!streamTypedRefEligible(t) && !isDynTypedRefType(t)) + ) { const valueTy = host.llType(t); B.line(`${boxed} = call ptr @${host.dyn.toDynHelper(t)}(${valueTy} ${value})`); return boxed; @@ -733,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", @@ -808,8 +874,7 @@ export function streamTypedRefMaterializeAdapter( host.declare(`declare ptr @scr_error_dyn_fields(ptr)`); B.line(`${out} = call ptr @scr_error_dyn_fields(ptr %p)`); } else if ( - meta.def.runtime && - RUNTIME_STREAM_CLASSES.has(t.className) && + ((meta.def.runtime && RUNTIME_STREAM_CLASSES.has(t.className)) || streamRooted(meta)) && !meta.def.instancePrototypeHelper && !meta.def.prototypeDataHelper ) { @@ -913,8 +978,10 @@ export function streamTypedRefMaterializeAdapter( B.line(`${fieldValue} = sitofp i32 ${raw} to double`); } else if (inline) fieldValue = loadStructField(B, host.sizeType, fieldPtr, field.type, true); else B.line(`${fieldValue} = load ${llFieldType(field.type, false)}, ptr ${fieldPtr}`); - B.line(`${rawKey} = load ptr, ptr @${mangleGlobal(symbol.globalId)}`); + const keyLines = symbolKeyLines(host, symbol, rawKey.slice(1)); + for (const line of keyLines.load) B.line(line.trim()); const key = host.streamTypedRefBoxValue(B, SYMBOL_T, rawKey); + for (const line of keyLines.release) B.line(line.trim()); const afterSymbol = meta.def.tracksOwnFields || field.type.kind === "symbol" ? B.newLabel("symbol.after") @@ -943,9 +1010,15 @@ export function streamTypedRefMaterializeAdapter( host.declare(`declare void @scr_union_release(ptr)`); B.line(`call void @scr_union_release(ptr ${fieldValue})`); } - host.declare(`declare void @scr_dyn_symbol_key_set(ptr, ptr, ptr)`); + // As for string-keyed fields, attributes copied from the bag (a frozen + // instance's) do not stop the view taking the value. + const symbolSetter = + meta.def.tracksOwnFields || withAttributes + ? "scr_dyn_symbol_field_set" + : "scr_dyn_symbol_key_set"; + host.declare(`declare void @${symbolSetter}(ptr, ptr, ptr)`); host.declare(`declare void @scr_dyn_release_v(ptr)`); - B.line(`call void @scr_dyn_symbol_key_set(ptr ${out}, ptr ${key}, ptr ${value})`); + B.line(`call void @${symbolSetter}(ptr ${out}, ptr ${key}, ptr ${value})`); B.line(`call void @scr_dyn_release_v(ptr ${value})`); if (afterSymbol) { B.terminate(`br label %${afterSymbol}`); @@ -982,6 +1055,28 @@ export function streamTypedRefMaterializeAdapter( : `call void @scr_error_dyn_place_name(ptr ${out}, ptr ${host.cstr(nameAfter)}, ${host.sizeType} ${Buffer.byteLength(nameAfter, "utf8")})`, ); } + // A JavaScript Error subclass's property bag records, once a dynamic + // write has committed the view, where `name` and `message` were created + // (zod's error factory assigns `name` before it defines `message`); the + // slots hold their values. A `name` the class's construction sets is + // already where errorNameAfter places it among the fields. + if ( + meta.def.errorBagOrder && + meta.root.def.name === "%Error" && + meta.def.fields.some((field) => field.name === DYN_CLASS_PROPERTIES) + ) { + host.declare(`declare void @scr_error_dyn_follow_bag(ptr, ptr, ptr, i1)`); + const { index } = classFieldIndex(meta, DYN_CLASS_PROPERTIES); + const slot = B.tmp(), + bag = B.tmp(); + B.line( + `${slot} = getelementptr inbounds %${classStructSym(t.className)}, ptr %p, i64 0, i32 ${index}`, + ); + B.line(`${bag} = load ptr, ptr ${slot}`); + B.line( + `call void @scr_error_dyn_follow_bag(ptr ${out}, ptr ${bag}, ptr %p, i1 ${nameAfter === undefined ? "false" : "true"})`, + ); + } // A frozen, sealed or non-extensible instance's view says so. if (withAttributes) { host.declare(`declare void @scr_dyn_class_view_attributes(ptr, double)`); @@ -1144,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()`); @@ -1224,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( @@ -1457,9 +1588,26 @@ function liveClassOps(host: LlvmEmitterContext, snapshot: string, meta: LlClassM // The constructor's name, as util.inspect prints before the fields. const jsName = meta.def.jsName ?? meta.def.name.replace(/^%/, ""); const className = jsName === "" ? "null" : host.internLiteral(jsName); + // Its Symbol.toStringTag getter's string, for Object.prototype.toString + // and util.inspect. + let stringTag = "null"; + if (meta.def.stringTagHelper) { + // The runtime lends the instance; the helper takes its own reference. + stringTag = `@${snapshot}_ctag`; + const retain = vAdapters(host.shapeHost, { kind: "object", className: meta.def.name }).retain; + host.resolveThunkDefs.push( + `define internal ptr ${stringTag}(ptr %p) ${FN_ATTRS} { ; Symbol.toStringTag of ${meta.def.name}`, + `entry:`, + ` %owned = call ptr ${retain}(ptr %p)`, + ` %tag = call ptr @${mangleFunction(meta.def.stringTagHelper)}(ptr %owned)`, + ` ret ptr %tag`, + `}`, + ``, + ); + } host.resolveThunkDefs.push( `@${names} = internal constant [${members.length} x ptr] [${literals.join(", ")}]`, - `@${ops} = internal constant { ptr, ${host.sizeType}, ptr, ptr } { ptr ${exact}, ${host.sizeType} ${members.length}, ptr @${names}, ptr ${className} }`, + `@${ops} = internal constant { ptr, ${host.sizeType}, ptr, ptr, ptr } { ptr ${exact}, ${host.sizeType} ${members.length}, ptr @${names}, ptr ${className}, ptr ${stringTag} }`, ``, ); } diff --git a/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts b/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts index b368c91f4..3d9d8da76 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts @@ -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 = @@ -684,22 +684,15 @@ export function streamCbThunkFor( passed.push(`ptr %sr`); } // The stream-owning completion closure (a 1-cap closure boxing the - // retained stream) — shared by the typed and dyn done shapes. + // retained stream) — shared by the typed and dyn done shapes. Every + // write and writev call receives the same one, as Node passes one + // onwrite function; other calls receive a new one, which the stream + // reuses while it holds the only reference. + const doneEntry = + kind === "w" || kind === "v" ? "scr_stream_write_closure" : "scr_stream_done_closure"; const doneClosure = (fnRef: string, tag: string): string => { - host.declare(`declare ptr @scr_closure_new(ptr, ${host.sizeType})`); - host.declare(`declare ptr @scr_box_new_obj(ptr, ptr, ptr)`); - host.declare(`declare void @scr_box_set_ref(ptr, ptr)`); - host.declare(`declare ptr @scr_stream_retain_v(ptr)`); - host.declare(`declare void @scr_stream_release_v(ptr)`); - host.declare(`declare void @scr_stream_trace(ptr, ptr, ptr)`); - d.push( - ` %${tag}clo = call ptr @scr_closure_new(ptr ${fnRef}, ${host.sizeType} 1)`, - ` %${tag}bx = call ptr @scr_box_new_obj(ptr @scr_stream_retain_v, ptr @scr_stream_release_v, ptr @scr_stream_trace)`, - ` %${tag}cp = getelementptr inbounds %ScrClosure, ptr %${tag}clo, i64 1`, - ` store ptr %${tag}bx, ptr %${tag}cp`, - ` %${tag}sr = call ptr @scr_stream_retain_v(ptr %s)`, - ` call void @scr_box_set_ref(ptr %${tag}bx, ptr %${tag}sr)`, - ); + host.declare(`declare ptr @${doneEntry}(ptr, ptr)`); + d.push(` %${tag}clo = call ptr @${doneEntry}(ptr %s, ptr ${fnRef})`); return `%${tag}clo`; }; for (let i = off; i < declared.length; i++) { diff --git a/packages/compiler/src/backend/llvm/forked-analysis.ts b/packages/compiler/src/backend/llvm/forked-analysis.ts new file mode 100644 index 000000000..80ba35fac --- /dev/null +++ b/packages/compiler/src/backend/llvm/forked-analysis.ts @@ -0,0 +1,45 @@ +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import type { ForkPrimitives } from "./emitter.js"; + +/** Starts `compute` on a forked child process, which inherits this process + * as it is at the fork, and returns the join that answers its result. The + * child writes `encode`'s text to a file and exits; the join waits for it + * and decodes the text. When the fork fails, the child fails, or `encode` + * answers null, the join runs `compute` here instead, so the result does + * not depend on the child. Join once. */ +export function forkedAnalysis( + fork: ForkPrimitives, + compute: () => T, + encode: (value: T) => string | null, + decode: (text: string) => T, +): () => T { + const dir = mkdtempSync(join(tmpdir(), "scriptc-analysis-")); + const file = join(dir, "result.json"); + const pid = fork.fork(); + if (pid === 0) { + let code = 1; + try { + const text = encode(compute()); + if (text !== null) { + writeFileSync(file, text); + code = 0; + } + } catch { + // The parent computes the result itself. + } + fork.exit(code); + } + return () => { + let text: string | null = null; + try { + if (pid > 0 && fork.wait(pid) === 0) text = readFileSync(file, "utf8"); + } catch { + // The result is computed here. + } finally { + rmSync(dir, { recursive: true, force: true }); + } + return text === null ? compute() : decode(text); + }; +} diff --git a/packages/compiler/src/backend/llvm/function-layout.ts b/packages/compiler/src/backend/llvm/function-layout.ts new file mode 100644 index 000000000..00cf233db --- /dev/null +++ b/packages/compiler/src/backend/llvm/function-layout.ts @@ -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; + +/** 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 ` `. A function listed in two + * tiers keeps the first. */ +export function parseFunctionLayout(text: string): FunctionLayout { + const layout = new Map(); + 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; +} diff --git a/packages/compiler/src/backend/llvm/integer-values.ts b/packages/compiler/src/backend/llvm/integer-values.ts index ddbcdd8da..5b8935036 100644 --- a/packages/compiler/src/backend/llvm/integer-values.ts +++ b/packages/compiler/src/backend/llvm/integer-values.ts @@ -65,6 +65,22 @@ function magnitude(range: IntegerRange): number { return Math.max(Math.abs(range.min), Math.abs(range.max)); } +/** The bounds of a sum and a product of two magnitude bounds, as doubles. + * Below 2^53 the double result is the integer; above it the result is past + * every exactness limit either way, and rounding moves it far less than the + * margin from WIDE_BOUND to the i64 range. A result of exactly 2^53 may + * stand for 2^53 + 1 (2^53 - 1 + 2 rounds to 2^53), so it moves to the next + * double up unless the sum is exactly 2^53. */ +function sumBound(a: number, b: number): number { + const sum = a + b; + return sum === EXACT_DOUBLES && EXACT_DOUBLES - a !== b ? sum + 2 : sum; +} + +function productBound(a: number, b: number): number { + const product = a * b; + return product === EXACT_DOUBLES ? product + 2 : product; +} + /** The wide view of `l op r` for `+`, `-` and `*` when the exact operands * have no proven result range (their product or sum may pass 2^53). * @@ -95,17 +111,21 @@ export function wideArithmetic( rv = view(r, ri); if (!lv || !rv) return null; let below: number, bound: number; + let single = false; if (op === "*") { if (lv.below !== Infinity || rv.below !== Infinity) return null; - below = EXACT_DOUBLES; - bound = lv.bound * rv.bound; + bound = productBound(lv.bound, rv.bound); + // Scaling by a power of two never rounds a double that stays finite. + below = powerOfTwo(li) || powerOfTwo(ri) ? Infinity : EXACT_DOUBLES; + single = true; } else if (lv.below === Infinity && rv.below === Infinity) { below = EXACT_DOUBLES; - bound = lv.bound + rv.bound; + bound = sumBound(lv.bound, rv.bound); + single = true; } else if (lv.below === Infinity || rv.below === Infinity) { const [exact, wide] = lv.below === Infinity ? [lv, rv] : [rv, lv]; below = Math.min(wide.below - exact.bound, EXACT_DOUBLES); - bound = exact.bound + wide.bound; + bound = sumBound(exact.bound, wide.bound); } else return null; if (!(bound <= WIDE_BOUND) || below < INDEX_LIMIT) return null; const operand = (value: LlValue, integer: LlInteger | null): string => @@ -116,7 +136,50 @@ export function wideArithmetic( host.B.line( `${name} = ${op === "+" ? "add" : op === "-" ? "sub" : "mul"} nsw i64 ${left}, ${right}`, ); - return { name, below, bound }; + return single ? { name, below, bound, single } : { name, below, bound }; +} + +/** Whether an exact integer is the constant 2^k or -2^k. */ +function powerOfTwo(integer: LlInteger | null): boolean { + if (integer === null || integer.range.min !== integer.range.max) return false; + const value = Math.abs(integer.range.min); + // Math.log2 can round to an integer for a value that is not a power of + // two (2^52 - 1), so the candidate power is compared with the value. + return value >= 1 && value <= 2 ** 52 && 2 ** Math.round(Math.log2(value)) === value; +} + +/** The i64 operands of a number compare that gives the doubles' result: + * both equal their doubles wherever they can be, or one is an exact integer + * of magnitude at most 2^53 - 1 and the other's double is its integer + * rounded once. Rounding is monotonic, and b - 1, b and b + 1 are doubles + * for a safe integer b, so an integer rounded once compares with b as the + * integer does. Null for any other pair. */ +export function exactCompareOperands( + host: LlvmEmitterContext, + l: LlValue, + li: LlInteger | null, + r: LlValue, + ri: LlInteger | null, +): [string, string] | null { + const describe = (value: LlValue, integer: LlInteger | null) => { + if (integer) return { exact: true, single: true, bound: magnitude(integer.range) }; + const wide = value.wideInteger; + if (!wide) return null; + return { exact: wide.bound <= wide.below, single: wide.single === true, bound: wide.bound }; + }; + const a = describe(l, li), + b = describe(r, ri); + if (!a || !b) return null; + const safe = (d: { exact: boolean; bound: number }): boolean => + d.exact && d.bound <= Number.MAX_SAFE_INTEGER; + const ok = + (a.exact && b.exact) || + (safe(a) && (b.exact || b.single)) || + (safe(b) && (a.exact || a.single)); + if (!ok) return null; + const operand = (value: LlValue, integer: LlInteger | null): string => + integer ? widenInteger(host, integer) : value.wideInteger!.name; + return [operand(l, li), operand(r, ri)]; } /** An i64 index for a consumer that compares it, unsigned, with a length or @@ -134,6 +197,20 @@ export function indexInteger( return wide && wide.below >= INDEX_LIMIT ? wide.name : null; } +/** An i64 equal to a value's double wherever the value can be: its exact + * integer, or a wide view whose magnitude bound is within the magnitude below + * which the view is exact. Every integer of magnitude at most 2^53 is a + * double, so a bound equal to that limit is exact too. Null otherwise. */ +export function alwaysExactInteger( + host: LlvmEmitterContext, + value: LlValue, + integer: LlInteger | null, +): string | null { + if (integer) return widenInteger(host, integer); + const wide = value.wideInteger; + return wide && wide.bound <= wide.below ? wide.name : null; +} + export function widenInteger(host: LlvmEmitterContext, value: LlInteger): string { if (value.type === "i64") return value.name; const wide = host.B.tmp(); diff --git a/packages/compiler/src/backend/llvm/integer-views.ts b/packages/compiler/src/backend/llvm/integer-views.ts index c38770160..579f010a2 100644 --- a/packages/compiler/src/backend/llvm/integer-views.ts +++ b/packages/compiler/src/backend/llvm/integer-views.ts @@ -1,6 +1,9 @@ import type { IrExpr, IrFunction, IrStmt } from "../../ir/ir.js"; import { everyExprChild, everyStmtChild } from "../../ir/traverse.js"; import { byteNumberAccess } from "../../ir/byte-numbers.js"; +import { eachNodeOfKinds, type IrNodeIndex, type IrNodeKind } from "../../ir/node-index.js"; + +const LOOP_KINDS: readonly IrNodeKind[] = ["for", "forOf", "while", "doWhile"]; /** Keep a computed ToUint32 view beside an ordinary number when integer * consumers in loops can reuse it. The number remains authoritative for floating @@ -14,9 +17,16 @@ import { byteNumberAccess } from "../../ir/byte-numbers.js"; * compare converts the stored number back, and LLVM moves that number's * conversion into the join of a NaN-coded typed-array read (`phi [sitofp x, * 0.0]`), where nothing folds it back to the integer. */ -export function findIntegerViews(fn: IrFunction): Set { +export function findIntegerViews(fn: IrFunction, nodes?: IrNodeIndex): Set { const wanted = new Set(); if (fn.async || fn.generator) return wanted; + // Every view is wanted inside a loop: a body without one wants none. + let loops = false; + eachNodeOfKinds(fn, nodes?.scan(LOOP_KINDS), (node, statement) => { + if (statement && LOOP_KINDS.includes(node.kind)) loops = true; + return !loops; + }); + if (!loops) return wanted; const initialized = new Set(fn.params.map((p) => p.localId)); const excluded = new Set( [...(fn.captures ?? []), ...(fn.classCaptures ?? [])].map((c) => c.localId), diff --git a/packages/compiler/src/backend/llvm/iterative-teardown.test.ts b/packages/compiler/src/backend/llvm/iterative-teardown.test.ts new file mode 100644 index 000000000..befa258e8 --- /dev/null +++ b/packages/compiler/src/backend/llvm/iterative-teardown.test.ts @@ -0,0 +1,68 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile, deserializeModule } from "../../index.js"; +import { emitLlvmModule } from "./emitter.js"; + +async function moduleFor(source: string) { + const dir = await mkdtemp(join(tmpdir(), "scriptc-iterative-teardown-")); + try { + const entry = join(dir, "main.ts"), + output = join(dir, "main.ir.json"); + await writeFile(entry, source); + const result = await compile(entry, { outDir: dir, outPath: output, outputKind: "ir" }); + if (!result.ok) + throw new Error(result.diagnostics.map((item) => `${item.code}: ${item.message}`).join("\n")); + return deserializeModule(await readFile(output, "utf8")); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +const SOURCE = ` +interface TreeNode { left: TreeNode | null; right: TreeNode | null } +class ListNode { + next: ListNode | null; + value: number; + constructor(next: ListNode | null, value: number) { this.next = next; this.value = value; } +} +interface Labeled { child: Labeled | null; label: string[] } +function tree(d: number): TreeNode { return d > 0 ? { left: tree(d - 1), right: tree(d - 1) } : { left: null, right: null }; } +function size(n: TreeNode | null): number { return n === null ? 0 : 1 + size(n.left) + size(n.right); } +let list: ListNode | null = null; +for (let i = 0; i < 10; i++) list = new ListNode(list, i); +let labeled: Labeled | null = { child: { child: null, label: ["b"] }, label: ["a"] }; +console.log(size(tree(4)), list!.value, labeled!.label.length); +list = null; +labeled = null; +`; + +/** The definitions whose symbols end in `suffix`. */ +function definitions(ir: string, suffix: string): string[] { + return [ + ...ir.matchAll(new RegExp(`^define internal [^\\n]*@([\\w.$%-]+${suffix})\\(`, "gm")), + ].map((m) => m[1]!); +} + +test("self-recursive records and classes tear down in one loop", async () => { + const ir = emitLlvmModule(await moduleFor(SOURCE), { targetTriple: "x86_64-unknown-linux-gnu" }); + const loops = definitions(ir, "_destroy_it"); + // The tree record and the list class; the labeled record also holds an + // array, so it keeps the recursive teardown. + expect(loops.length).toBe(2); + expect(loops.some((name) => name.includes("ListNode"))).toBe(true); + for (const name of loops) { + const body = new RegExp( + `^define internal void @${name.replace(/[$.]/g, "\\$&")}\\(ptr %root\\)[^]*?^}`, + "m", + ).exec(ir)![0]; + // No call to a teardown: dead children go onto the pending list. + expect(body).not.toMatch(/call [^\n]*_destroy/); + expect(body).toContain("inttoptr"); + const base = name.slice(0, -"_it".length); + expect(ir).toContain(`define internal void @${base}(ptr %o)`); + expect(ir).not.toContain(`@${base}_s(`); + } + expect(definitions(ir, "_destroy_s").length).toBe(1); +}); diff --git a/packages/compiler/src/backend/llvm/js-ic.ts b/packages/compiler/src/backend/llvm/js-ic.ts index 7516673cd..d51b838bb 100644 --- a/packages/compiler/src/backend/llvm/js-ic.ts +++ b/packages/compiler/src/backend/llvm/js-ic.ts @@ -6,13 +6,16 @@ * * load ordinary object whose shape is entry 0's own-property shape: * read entry `slot` inline unless it is an accessor; otherwise - * scr_js_load_probe (polymorphic, prototype hits, misses) + * scr_js_get_probe (polymorphic, prototype hits, absent keys, + * misses) * store ordinary object whose shape is entry 0's replace shape, outside * an intact parse region, writable data entry: replace inline; * otherwise scr_js_store_probe (adds, polymorphic, misses) * 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 @@ -384,6 +387,15 @@ function emitGet(host: Host, e: ExprOf<"call">, callee: IrFunction, key: string) incoming.push(`[ ${numberResult(host, count, e)}, %${B.currentLabel()} ]`); B.br(join); B.startBlock(other); + // So is the length of a native array passed in from typed code. + const rest = B.newLabel("icg.rest"); + const arr = nativeArray(host, receiver.name, rest); + const native = load(host, "i32", arr, 8); + const nativeCount = B.tmp(); + B.line(`${nativeCount} = uitofp i32 ${native} to double`); + incoming.push(`[ ${numberResult(host, nativeCount, e)}, %${B.currentLabel()} ]`); + B.br(join); + B.startBlock(rest); } // Inline hits (borrowed values) and probe hits (owned ones) share one // conversion to the result type. @@ -393,9 +405,14 @@ function emitGet(host: Host, e: ExprOf<"call">, callee: IrFunction, key: string) B.startBlock(object); inlineLoad(host, receiver.name, ic, INLINE_WAYS, INLINE_PROTO, (cell) => cell, hits, hit, probe); B.startBlock(probe); - host.declare(`declare ptr @scr_js_load_probe(ptr, ptr, ptr)`); + // The read's probe also answers keys the receiver's plain chain lacks. + // 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_load_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(); @@ -623,6 +640,98 @@ function numberKey(host: Host, key: IrExpr): NumberKey | null { }; } +/** Branches to `yes` when `d` is a capsule of a native array passed in + * from typed code (a TYPED_REF bound to live array ops, not a record or + * class instance), with the ScrArr in `yes`; else to `no`. */ +function nativeArray(host: Host, d: string, no: string): string { + const B = host.B; + const capsule = B.newLabel("ica.cap"); + const bound = B.newLabel("ica.ops"); + const yes = B.newLabel("ica.arr"); + kindIs(host, d, DYN_KIND.TYPED_REF, capsule, no); + B.startBlock(capsule); + // The flag word after `casts`: record, klass and bag_class are its top + // three bits; any of them means `array` holds other ops. + const flags = load(host, "i32", d, 88); + const kinds = B.tmp(); + const plain = B.tmp(); + B.line(`${kinds} = lshr i32 ${flags}, 29`); + B.line(`${plain} = icmp eq i32 ${kinds}, 0`); + B.condBr(plain, bound, no); + B.startBlock(bound); + const ops = load(host, "ptr", d, 64); + const live = B.tmp(); + B.line(`${live} = icmp ne ptr ${ops}, null`); + B.condBr(live, yes, no); + B.startBlock(yes); + return load(host, "ptr", d, 16); +} + +/** `xs[i]` on a native number[] passed in from typed code: an integral + * index below the array's hole-free prefix reads the element in place. + * The other cases branch to `miss`, whose probe and helper answer them as + * before. The result joins `incoming` at `join`. */ +function emitNativeNumberElement( + host: Host, + receiver: string, + e: IrExpr, + indexIn: (next: string) => string, + miss: string, + join: string, + incoming: string[], +): void { + const B = host.B; + const arr = nativeArray(host, receiver, miss); + const numbers = B.newLabel("ica.f64"); + const elem = load(host, "i8", arr, 16); + const f64 = B.tmp(); + B.line(`${f64} = icmp eq i8 ${elem}, 0 ; SCR_ELEM_F64`); + B.condBr(f64, numbers, miss); + B.startBlock(numbers); + const index = indexIn(B.newLabel("ica.idx")); + const filled = load(host, "i32", arr, 20); + const filledD = B.tmp(); + const nonNegative = B.tmp(); + const below = B.tmp(); + const ok = B.tmp(); + B.line(`${filledD} = uitofp i32 ${filled} to double`); + B.line(`${nonNegative} = fcmp oge double ${index}, 0.0`); + B.line(`${below} = fcmp olt double ${index}, ${filledD}`); + B.line(`${ok} = and i1 ${nonNegative}, ${below}`); + const ranged = B.newLabel("ica.rng"); + B.condBr(ok, ranged, miss); + B.startBlock(ranged); + const i = B.tmp(); + const back = B.tmp(); + const integral = B.tmp(); + B.line(`${i} = fptoui double ${index} to i64`); + B.line(`${back} = uitofp i64 ${i} to double`); + B.line(`${integral} = fcmp oeq double ${back}, ${index}`); + const hit = B.newLabel("ica.hit"); + B.condBr(integral, hit, miss); + B.startBlock(hit); + const data = load(host, "ptr", arr, 24); + const slot = B.tmp(); + const value = B.tmp(); + B.line(`${slot} = getelementptr inbounds double, ptr ${data}, i64 ${i}`); + B.line(`${value} = load double, ptr ${slot}`); + let out: string; + if (e.type.kind === "jsval") { + const raw = B.tmp(); + const nan = B.tmp(); + const bits = B.tmp(); + out = B.tmp(); + B.line(`${raw} = bitcast double ${value} to i64`); + B.line(`${nan} = fcmp uno double ${value}, ${value}`); + B.line(`${bits} = select i1 ${nan}, i64 ${JS_BITS.canonicalNan}, i64 ${raw}`); + B.line(`${out} = inttoptr i64 ${bits} to ptr`); + } else { + out = numberResult(host, value, e); + } + incoming.push(`[ ${out}, %${B.currentLabel()} ]`); + B.br(join); +} + function emitKeyed(host: Host, e: ExprOf<"call">, callee: IrFunction): LlValue { const B = host.B; const receiver = emitBorrowedInput(host, e.args[0]!); @@ -645,18 +754,23 @@ function emitKeyed(host: Host, e: ExprOf<"call">, callee: IrFunction): LlValue { B.line(`store ptr null, ptr ${keySlot}`); host.ownSlot(keySlot, DYN); } - kindIs(host, receiver.name, DYN_KIND.ARR, array, probe); + /** The key as a double in block `next`; other keys go to the probe. */ + const indexIn = (next: string): string => { + if (numeric === null) { + kindIs(host, key!.name, DYN_KIND.NUM, next, probe); + B.startBlock(next); + return load(host, "double", key!.name, 16); + } + B.condBr(numeric.present, next, probe); + B.startBlock(next); + return numeric.value; + }; + const other = B.newLabel("ick.other"); + kindIs(host, receiver.name, DYN_KIND.ARR, array, other); + B.startBlock(other); + emitNativeNumberElement(host, receiver.name, e, indexIn, probe, join, incoming); B.startBlock(array); - let index: string; - if (numeric === null) { - kindIs(host, key!.name, DYN_KIND.NUM, number, probe); - B.startBlock(number); - index = load(host, "double", key!.name, 16); - } else { - B.condBr(numeric.present, number, probe); - B.startBlock(number); - index = numeric.value; - } + const index = indexIn(number); const len = load(host, "i64", receiver.name, 16); const i = B.tmp(); const back = B.tmp(); @@ -730,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). */ diff --git a/packages/compiler/src/backend/llvm/layouts.ts b/packages/compiler/src/backend/llvm/layouts.ts index 8e1f0a4c1..5c426e910 100644 --- a/packages/compiler/src/backend/llvm/layouts.ts +++ b/packages/compiler/src/backend/llvm/layouts.ts @@ -17,7 +17,7 @@ export function emitLlvmLayouts( host: ClassHost, mod: IrModule, classes: Map, - classObjects: Map, + classObjects: Map, functions: Map, llType: (type: IrType) => string, nullable?: NullableRefFields, diff --git a/packages/compiler/src/backend/llvm/lazy-captures.ts b/packages/compiler/src/backend/llvm/lazy-captures.ts index 2014869af..85acff2ef 100644 --- a/packages/compiler/src/backend/llvm/lazy-captures.ts +++ b/packages/compiler/src/backend/llvm/lazy-captures.ts @@ -1,5 +1,8 @@ import type { IrExpr, IrFunction, IrStmt } from "../../ir/ir.js"; import { everyStmtList } from "../../ir/traverse.js"; +import { eachNodeOfKinds, type IrNodeIndex, type IrNodeKind } from "../../ir/node-index.js"; + +const CLOSURE_KINDS: readonly IrNodeKind[] = ["closure"]; /** The local a node rebinds. Writes through the bound reference, such as a * field store or an array push, leave the binding unchanged. Catch bindings @@ -37,7 +40,11 @@ export class LazyCaptures { private readonly pending = new Set(); private sites: Map | null = null; - constructor(private readonly functions: ReadonlyMap) {} + constructor( + private readonly functions: ReadonlyMap, + /** The module's node index, read instead of walking the bodies it holds. */ + private readonly nodes?: IrNodeIndex, + ) {} /** The captures of closure body `fnName` that hold one value for the * closure's whole life: at every creation site the captured binding is @@ -103,16 +110,15 @@ export class LazyCaptures { private creationSites(): Map { if (this.sites !== null) return this.sites; const sites = new Map(); + const scan = this.nodes?.scan(CLOSURE_KINDS); for (const fn of this.functions.values()) { - const visit = (node: IrStmt | IrExpr): boolean => { + eachNodeOfKinds(fn, scan, (node) => { if (node.kind === "closure") { let list = sites.get(node.fnName); if (!list) sites.set(node.fnName, (list = [])); list.push({ owner: fn.name, captures: node.captures }); } - return true; - }; - everyStmtList(fn.body, { stmt: visit, expr: visit }); + }); } this.sites = sites; return sites; diff --git a/packages/compiler/src/backend/llvm/lib-abi.ts b/packages/compiler/src/backend/llvm/lib-abi.ts index 6917d8256..ea5ab82a2 100644 --- a/packages/compiler/src/backend/llvm/lib-abi.ts +++ b/packages/compiler/src/backend/llvm/lib-abi.ts @@ -73,6 +73,26 @@ export function rawBytes(host: LlvmEmitterContext, value: LlValue): AbiArgument[ return [ptr(data), { type: host.sizeType, name: length }]; } +/** A TLS client's `ca` as rawBytes, with a null pointer for none: an empty + * string, which Node reads as no ca (and which an omitted ca lowers to). + * A Buffer's pointer is never null, so an empty one is an empty trust + * store. */ +export function caBytes(host: LlvmEmitterContext, value: LlValue): AbiArgument[] { + const B = host.B; + const [data, length] = rawBytes(host, value); + const test = B.tmp(), + pointer = B.tmp(); + if (value.type.kind === "string") { + B.line(`${test} = icmp eq ${host.sizeType} ${length!.name}, 0`); + B.line(`${pointer} = select i1 ${test}, ptr null, ptr ${data!.name}`); + } else { + // An empty Buffer's data pointer may be null; its object stands in. + B.line(`${test} = icmp eq ptr ${data!.name}, null`); + B.line(`${pointer} = select i1 ${test}, ptr ${value.name}, ptr ${data!.name}`); + } + return [ptr(pointer), length!]; +} + export function callbackType(value: LlValue): IrType & { kind: "func" } { if (value.type.kind !== "func") throw new InternalCompilerError("runtime callback must be a function"); diff --git a/packages/compiler/src/backend/llvm/lib-connect.ts b/packages/compiler/src/backend/llvm/lib-connect.ts index 4d151fc7e..2ab72c188 100644 --- a/packages/compiler/src/backend/llvm/lib-connect.ts +++ b/packages/compiler/src/backend/llvm/lib-connect.ts @@ -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"; diff --git a/packages/compiler/src/backend/llvm/lib-datagram.ts b/packages/compiler/src/backend/llvm/lib-datagram.ts index 07de4e54e..7a83f1d30 100644 --- a/packages/compiler/src/backend/llvm/lib-datagram.ts +++ b/packages/compiler/src/backend/llvm/lib-datagram.ts @@ -19,7 +19,7 @@ function messageAdapter(host: LlvmEmitterContext, type: IrType): string { const key = "dgram:" + type.shapeId; const previous = host.resolveThunks.get(key); if (previous !== undefined) return previous; - const symbol = `sc_dgram_message_${host.resolveThunks.size}`; + const symbol = host.contentSymbol("sc_dgram_message_", key); host.resolveThunks.set(key, symbol); host.journal?.allocated(symbol, "messageAdapter", [type]); const shape = host.recordsById.get(type.shapeId); @@ -68,7 +68,7 @@ function lookupAdapter(host: LlvmEmitterContext, cb: LlValue): string { const key = `dns:${error.unionId}:${arity}`; const previous = host.resolveThunks.get(key); if (previous !== undefined) return previous; - const symbol = `sc_dns_lookup_${host.resolveThunks.size}`; + const symbol = host.contentSymbol("sc_dns_lookup_", key); host.resolveThunks.set(key, symbol); host.journal?.allocated(symbol, "lookupAdapter", [cb]); host.declare("declare ptr @scr_error_new(i32, ptr)"); diff --git a/packages/compiler/src/backend/llvm/lib-dispatch.ts b/packages/compiler/src/backend/llvm/lib-dispatch.ts index cd6010ab1..7432debb4 100644 --- a/packages/compiler/src/backend/llvm/lib-dispatch.ts +++ b/packages/compiler/src/backend/llvm/lib-dispatch.ts @@ -1,3 +1,4 @@ +import { arrayClassDescriptor } from "./classes.js"; import { emitBigIntWrap } from "./bigint-wrap.js"; import { emitBorrowedInput, emitBorrowedInputs } from "./borrowed-inputs.js"; import { borrowsIteratorInputs, borrowsJsonInputs } from "./checked-value-lifetimes.js"; @@ -5,25 +6,31 @@ import { emitTlsLibCall } from "./lib-tls.js"; import { emitHttp2LibCall } from "./lib-http2.js"; import { emitDatagramLibCall } from "./lib-datagram.js"; import { emitTestLibCall } from "./lib-test.js"; +import { FN_ATTRS, arrNewCall, traceArg, vAdapters } from "./shapes.js"; /* Focused LLVM library-call emission extracted from emitter.ts. */ import { InternalCompilerError } from "../../errors.js"; import { MAY_THROW_LIB_FNS } from "../../ir/builtin-effects.js"; -import type { IrExpr, IrType } from "../../ir/ir.js"; +import { BYTES_ELEMENT_SIZE, F64, type IrExpr, type IrType } from "../../ir/ir.js"; import { LlvmUnsupportedError } from "./unsupported.js"; import { emitJsLibCall } from "./jsval.js"; import type { LlvmEmitterContext, LibCallExpr, LibCallPrefix, LlValue } from "./expr-context.js"; import { ASSERT_VERDICT_LIB_FNS, LIB_FN_SYMS, + PURE_MATH_LIB_FNS, mockTimerSym, USES_TIMERS_LIB_FNS, } from "./lib-shared.js"; import { isDynUndefined } from "../../ir/runtime-features.js"; import { DYN_KIND } from "./dyn.js"; -import { rawBytes } from "./lib-abi.js"; +import { caBytes } from "./lib-abi.js"; import { emitInlineAtomicsOp } from "./expr-bytes.js"; import { regexLiteralCaptures } from "./regex-matchers.js"; +import { registerReplay } from "./emit-journal.js"; + +/** ScrDyn.v.typed_ref.type_key_len: the low 27 bits of its bit-field word. */ +const TYPE_KEY_LEN_MASK = (1 << 27) - 1; export function emitAssertInspectLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue { const B = host.B; @@ -236,6 +243,118 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll host.emitPendingCheck(); return output; } + if (e.fn === "promise.allTuple") return emitPromiseAllTuple(host, e); + if (e.fn === "promise.fulfilledValue") { + // The payload of a fulfilled promise (borrowed operand): the scalar + // kinds that lower to a double (numbers, Dates, process streams) and + // booleans by value, strings and references +1. + const promise = host.emitExpr(e.args[0]!); + const out = B.tmp(); + const ty = host.llType(e.type); + if (ty === "double") { + host.declare(`declare double @scr_promise_payload_f64(ptr)`); + B.line(`${out} = call double @scr_promise_payload_f64(ptr ${promise.name})`); + return { name: out, type: e.type }; + } + if (ty === "i1") { + host.declare(`declare zeroext i1 @scr_promise_payload_bool(ptr)`); + B.line(`${out} = call zeroext i1 @scr_promise_payload_bool(ptr ${promise.name})`); + return { name: out, type: e.type }; + } + if (e.type.kind === "string") { + host.declare(`declare ptr @scr_promise_payload_str(ptr)`); + B.line(`${out} = call ptr @scr_promise_payload_str(ptr ${promise.name}) ; +1`); + return host.own({ name: out, type: e.type }); + } + host.declare(`declare ptr @scr_promise_payload_ref(ptr)`); + B.line(`${out} = call ptr @scr_promise_payload_ref(ptr ${promise.name}) ; +1`); + return host.own({ name: out, type: e.type }); + } + if (e.fn === "record.sourceSpread") { + const built = host.emitReadReceiver(e.args[0]!); + const value = host.emitReadReceiver(e.args[1]!); + const type = e.args[0]!.type; + if (type.kind !== "record") + throw new InternalCompilerError("llvm emitter bug: record.sourceSpread of a non-record"); + host.dyn.emitRecordSourceSpread(B, built.name, value.name, type.shapeId); + return { name: "", type: e.type }; + } + if (e.fn === "array.speciesCreate") { + // The receiver is borrowed; the call answers a new reference. + const receiver = emitBorrowedInput(host, e.args[0]!); + const length = host.emitExpr(e.args[1]!); + host.declare(`declare ptr @scr_arr_species_create(ptr, double)`); + const out = B.tmp(); + B.line( + `${out} = call ptr @scr_arr_species_create(ptr ${receiver.name}, double ${length.name})`, + ); + const value = host.own({ name: out, type: e.type }); + host.emitPendingCheck(); + return value; + } + if (e.fn === "array.speciesFinish") { + // The instance is borrowed; the method's result moves into the call, + // which answers it or the instance that holds its elements. + const instance = emitBorrowedInput(host, e.args[0]!); + const result = host.emitExpr(e.args[1]!); + host.toHeap(result); + const setLength = host.emitExpr(e.args[2]!); + host.declare(`declare ptr @scr_arr_species_finish(ptr, ptr, i1 zeroext)`); + const out = B.tmp(); + B.line( + `${out} = call ptr @scr_arr_species_finish(ptr ${instance.name}, ptr ${result.name}, i1 zeroext ${setLength.name})`, + ); + const value = host.own({ name: out, type: e.type }); + host.emitPendingCheck(); + return value; + } + if (e.fn === "array.speciesAdopt") { + // The receiver is borrowed; the method's result moves into the call, + // which answers it or the species instance that replaces it. + const receiver = emitBorrowedInput(host, e.args[0]!); + const result = host.emitExpr(e.args[1]!); + host.toHeap(result); + const length = host.emitExpr(e.args[2]!); + const setLength = host.emitExpr(e.args[3]!); + host.declare(`declare ptr @scr_arr_species_adopt(ptr, ptr, double, i1 zeroext)`); + const out = B.tmp(); + B.line( + `${out} = call ptr @scr_arr_species_adopt(ptr ${receiver.name}, ptr ${result.name}, double ${length.name}, i1 zeroext ${setLength.name})`, + ); + const value = host.own({ name: out, type: e.type }); + host.emitPendingCheck(); + return value; + } + if (e.fn === "array.dynIsClass") { + const className = e.args[1]!.kind === "strLit" ? e.args[1]!.value : null; + if (className === null) + throw new InternalCompilerError("llvm emitter bug: array.dynIsClass needs a class name"); + const value = emitBorrowedInput(host, e.args[0]!); + host.declare(`declare zeroext i1 @scr_dyn_is_array_class(ptr, ptr)`); + const out = B.tmp(); + B.line( + `${out} = call zeroext i1 @scr_dyn_is_array_class(ptr ${value.name}, ptr ${arrayClassDescriptor(className)})`, + ); + return { name: out, type: e.type }; + } + if (e.fn === "array.classConstructAdopt") { + const className = e.args[0]!.kind === "strLit" ? e.args[0]!.value : null; + if (className === null) + throw new InternalCompilerError( + "llvm emitter bug: array.classConstructAdopt needs a class name", + ); + const result = host.emitExpr(e.args[1]!); + host.toHeap(result); + const length = host.emitExpr(e.args[2]!); + host.declare(`declare ptr @scr_arr_class_construct_adopt(ptr, ptr, double)`); + const out = B.tmp(); + B.line( + `${out} = call ptr @scr_arr_class_construct_adopt(ptr ${arrayClassDescriptor(className)}, ptr ${result.name}, double ${length.name})`, + ); + const value = host.own({ name: out, type: e.type }); + host.emitPendingCheck(); + return value; + } if (e.fn === "dyn.typedRefIs" || e.fn === "dyn.typedRefKeyIs") { const literal = e.args[1]!.kind === "strLit" ? e.args[1]!.value : null; if (e.fn === "dyn.typedRefKeyIs" && literal === null) @@ -281,6 +400,38 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll B.line(`store i1 true, ptr ${slot}`); B.condBr(same, done, compare); B.startBlock(compare); + // A capsule with another key pointer usually belongs to another + // class: reject it on its key length and last eight bytes before + // the out-of-line name comparison, so a class table's chain of + // probes costs a few instructions per class it skips. + B.line(`store i1 false, ptr ${slot}`); + const bytes = Buffer.from(literal, "utf8"); + const lenSlot = B.tmp(), + lenWord = B.tmp(), + len = B.tmp(), + lenEq = B.tmp(); + B.line( + `${lenSlot} = getelementptr inbounds i8, ptr ${value.name}, i64 ${host.abiOffset(88, 52)} ; ->v.typed_ref.type_key_len`, + ); + B.line(`${lenWord} = load i32, ptr ${lenSlot}`); + B.line(`${len} = and i32 ${lenWord}, ${TYPE_KEY_LEN_MASK}`); + B.line(`${lenEq} = icmp eq i32 ${len}, ${bytes.length}`); + const full = B.newLabel("brand.full"); + if (bytes.length >= 8) { + const tail = B.newLabel("brand.tail"); + B.condBr(lenEq, tail, done); + B.startBlock(tail); + const tailPtr = B.tmp(), + word = B.tmp(), + wordEq = B.tmp(); + B.line(`${tailPtr} = getelementptr inbounds i8, ptr ${keyPtr}, i64 ${bytes.length - 8}`); + B.line(`${word} = load i64, ptr ${tailPtr}, align 1`); + B.line(`${wordEq} = icmp eq i64 ${word}, ${bytes.readBigInt64LE(bytes.length - 8)}`); + B.condBr(wordEq, full, done); + } else { + B.condBr(lenEq, full, done); + } + B.startBlock(full); } } if (e.fn === "dyn.typedRefIs") { @@ -523,7 +674,8 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll // The https URL row is the http one with the TLS entry point — same // three arguments, same response-callback adapter. The https options // row is wider: rejectUnauthorized stays an i1, while its ScrStr or - // ScrBytes CA value expands to the runtime's raw pointer + length. + // ScrBytes CA value expands to the runtime's raw pointer + length + // (caBytes: a null pointer for none). const isTls = e.fn.startsWith("https."); const isUrl = e.fn.includes("requestUrl"); const isTlsOptions = isTls && !isUrl; @@ -557,7 +709,7 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll if (isTlsOptions) { callArgs = [ ...callArgs.slice(0, 8), - ...rawBytes(host, args[8]!).map((arg) => `${arg.type} ${arg.name}`), + ...caBytes(host, args[8]!).map((arg) => `${arg.type} ${arg.name}`), ]; host.declare( `declare ptr @scr_https_request(ptr, double, ptr, ptr, double, ptr, i1 zeroext, i1 zeroext, ptr, ${host.sizeType}, ptr, ptr)`, @@ -577,6 +729,15 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll if (MAY_THROW_LIB_FNS.has(e.fn) && LIB_FN_SYMS[e.fn] === undefined) { throw new LlvmUnsupportedError(`libCall:${e.fn}`, e.loc); } + // A setter's write: the time value into the Date object (ScrDate.ms). + if (e.fn === "date.store") { + const date = emitBorrowedInput(host, e.args[0]!).name; + const ms = host.emitExpr(e.args[1]!).name; + const field = B.tmp(); + B.line(`${field} = getelementptr inbounds i8, ptr ${date}, i64 8 ; ->ms`); + B.line(`store double ${ms}, ptr ${field}`); + return { name: ms, type: e.type }; + } // queueMicrotask's checked-dynamic form carries the AsyncLocalStorage // context like the static one (lib-process.ts). const sym = @@ -588,22 +749,74 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll // which also takes those streams. e.fn === "stream.unboxDyn" && host.usesDynBuiltinStream ? "scr_dyn_builtin_stream_unbox" - : // for await over a checked-dynamic value iterates a native - // stream behind it (scr_dyn_for_await_stream.c) - e.fn === "dyn.asyncIteratorOpen" && host.dynForAwaitStreams - ? "scr_stream_dyn_async_iterator_open" - : LIB_FN_SYMS[e.fn]); + : e.fn === "stream.unboxDynStage" && host.usesDynBuiltinStream + ? "scr_dyn_builtin_stream_unbox_stage" + : // for await over a checked-dynamic value iterates a native + // stream behind it (scr_dyn_for_await_stream.c) + e.fn === "dyn.asyncIteratorOpen" && host.dynForAwaitStreams + ? "scr_stream_dyn_async_iterator_open" + : LIB_FN_SYMS[e.fn]); if (sym === undefined) throw new LlvmUnsupportedError(`libCall:${e.fn}`, e.loc); + // The runtime's Date functions take and answer time values: a Date + // operand passes its time value, read at once from the borrowed object, + // and a Date result is a new Date object. const args = READ_ONLY_STRING_LIB_FNS.has(e.fn) ? emitBorrowedInputs(host, e.args) : e.args.map((a) => - borrowsJsonInputs(e.fn) || borrowsIteratorInputs(e.fn) - ? emitBorrowedInput(host, a) - : host.emitExpr(a), + a.type.kind === "date" + ? { name: emitDateTimeArg(host, a), type: F64 } + : borrowsJsonInputs(e.fn) || borrowsIteratorInputs(e.fn) + ? emitBorrowedInput(host, a) + : host.emitExpr(a), ); + // getTime and valueOf answer the time value the argument already is. + if ((e.fn === "date.getTime" || e.fn === "date.valueOf") && e.args[0]?.type.kind === "date") + return { name: args[0]!.name, type: e.type }; // The deep comparisons' number test: computed here, with no runtime call. if (e.fn === "assert.sameValue") return { name: emitSameValueF64(host, args[0]!.name, args[1]!.name), type: e.type }; + const callArgs = args; + if (e.type.kind === "date") { + const ms = emitSymbolLibCall(host, { ...e, type: F64 }, sym, callArgs); + host.declare(`declare ptr @scr_date_new(double)`); + const date = B.tmp(); + B.line(`${date} = call ptr @scr_date_new(double ${ms.name})`); + return host.own({ name: date, type: e.type }); + } + return emitSymbolLibCall(host, e, sym, callArgs); +} + +/** The time value of a Date argument. A Date that a runtime Date function + * produces for this argument alone (`new Date(ms).getTime()`, a + * time-value local's read) passes the function's time value and creates + * no Date object. */ +function emitDateTimeArg(host: LlvmEmitterContext, date: IrExpr): string { + // A time-value local's read is its time value itself. + if (date.kind === "libCall" && date.fn === "date.fromTime") + return host.emitExpr(date.args[0]!).name; + if (date.kind === "libCall" && date.fn.startsWith("date.")) + return emitGenericLibCall(host, { ...date, type: F64 }).name; + return emitDateTime(host, emitBorrowedInput(host, date).name); +} + +/** A Date object's time value (ScrDate.ms). */ +function emitDateTime(host: LlvmEmitterContext, date: string): string { + const B = host.B; + const field = B.tmp(); + const ms = B.tmp(); + B.line(`${field} = getelementptr inbounds i8, ptr ${date}, i64 8 ; ->ms`); + B.line(`${ms} = load double, ptr ${field}`); + return ms; +} + +/** The call of runtime function `sym` with evaluated arguments. */ +function emitSymbolLibCall( + host: LlvmEmitterContext, + e: LibCallExpr, + sym: string, + args: readonly LlValue[], +): LlValue { + const B = host.B; const argDecls = args.map((a) => { const ty = host.llType(a.type); return ty === "i1" ? "i1 zeroext" : ty; @@ -644,7 +857,8 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll return { name: "", type: e.type }; } const t = B.tmp(); - B.line(`${t} = call ${retDecl} @${sym}(${argList})`); + const attrs = PURE_MATH_LIB_FNS.has(e.fn) ? " nounwind willreturn memory(none)" : ""; + B.line(`${t} = call ${retDecl} @${sym}(${argList})${attrs}`); // The result joins its frame BEFORE the pending check so an unwind // releases the dummy (NULL for refcounted returns) harmlessly. const out = host.own({ name: t, type: e.type }); @@ -710,35 +924,68 @@ function emitAssertFailing( * `a.name.localeCompare(b.name)` reads both fields without retaining). */ const READ_ONLY_STRING_LIB_FNS: ReadonlySet = new Set(["str.localeCompare"]); -/** The parameter count of a replace callback the runtime can call through - * the closure ABI (scr_regex_replace_fn), or null when it needs the dyn - * box. Each declared parameter must receive a value of its declared kind - * in every match: the match string at 0; with more than one parameter the +/** The most parameters scr_regex_replace_fn calls a replacer with + * (SCR_REPLACER_MAX_ARITY), the first bit of its string-or-undefined + * parameters (SCR_REPLACER_OPTIONAL), and its flag for a checked-dynamic + * result (SCR_REPLACER_BOXED_RESULT). */ +const REPLACER_MAX_ARITY = 6; +const REPLACER_OPTIONAL = 2 ** 8; +const REPLACER_BOXED_RESULT = 2 ** 31; + +/** `string | undefined` held as the string pointer, NULL for undefined. */ +function optionalStringType(host: LlvmEmitterContext, type: IrType): boolean { + if (type.kind !== "union") return false; + const nullable = host.nullableUnions.get(type.unionId); + if (nullable?.repr !== "single" || nullable.arm.kind !== "string" || nullable.nullTag >= 0) + return false; + return host.unionsById.get(type.unionId)?.arms[nullable.unitTag]?.kind === "undefinedT"; +} + +/** The parameter count and kinds (scr_regex_replace_fn's `kinds`) of a + * replace callback the runtime can call through the closure ABI, or null + * when it needs the dyn box. A checked-dynamic parameter receives whatever + * Node passes at its position. Each typed parameter must receive a value of + * its declared kind in every match: the match string at 0; past 0 the * pattern must be a literal whose captures are known, so position i is a - * capture that every match sets (a string), the offset (a number) right - * after the captures, or the subject (a string) after that. */ -function typedReplacerArity(callback: IrType, pattern: IrExpr): number | null { + * capture (a string when every match sets it, otherwise `string | + * undefined`), the offset (a number) right after the captures, or the + * subject (a string) after that. The result is a string or a + * checked-dynamic value. */ +function typedReplacerShape( + host: LlvmEmitterContext, + callback: IrType, + pattern: IrExpr, +): { arity: number; kinds: number } | null { if (callback.kind !== "func" || callback.rest || callback.argumentsAll) return null; - if (callback.ret.kind !== "string") return null; + if (callback.ret.kind !== "string" && callback.ret.kind !== "dyn") return null; const params = callback.params; - if (params.length > 4) return null; - if (params.length === 0) return 0; - if (params[0]!.kind !== "string") return null; - if (params.length === 1) return 1; - if (pattern.kind !== "regexLit") return null; - const captures = regexLiteralCaptures(pattern.pattern, pattern.flags); - if (!captures) return null; - for (let i = 1; i < params.length; i++) { + if (params.length > REPLACER_MAX_ARITY) return null; + let kinds = callback.ret.kind === "dyn" ? REPLACER_BOXED_RESULT : 0; + let captures: { count: number; always: boolean[] } | null | undefined; + for (let i = 0; i < params.length; i++) { const kind = params[i]!.kind; + if (kind === "dyn") { + kinds += 2 ** i; + continue; + } + const optional = optionalStringType(host, params[i]!); + if (i === 0) { + if (kind !== "string") return null; + continue; + } + if (pattern.kind !== "regexLit") return null; + captures ??= regexLiteralCaptures(pattern.pattern, pattern.flags); + if (!captures) return null; const ok = i < captures.count - ? kind === "string" && captures.always[i] === true + ? (kind === "string" && captures.always[i] === true) || optional : i === captures.count ? kind === "f64" : i === captures.count + 1 && kind === "string"; if (!ok) return null; + if (optional) kinds += REPLACER_OPTIONAL * 2 ** i; } - return params.length; + return { arity: params.length, kinds }; } /** `s.replace(re, fn)` / `replaceAll` with a typed callback the runtime can @@ -748,16 +995,16 @@ function typedReplacerArity(callback: IrType, pattern: IrExpr): number | null { function emitTypedRegexReplace(host: LlvmEmitterContext, e: LibCallExpr): LlValue | null { const [subject, pattern, callback, all] = e.args; if (!subject || !pattern || callback?.kind !== "dynFrom" || all?.kind !== "boolLit") return null; - const arity = typedReplacerArity(callback.value.type, pattern); - if (arity === null) return null; + const shape = typedReplacerShape(host, callback.value.type, pattern); + if (shape === null) return null; const B = host.B; const s = host.emitExpr(subject); const re = host.emitExpr(pattern); const fn = host.emitExpr(callback.value); - host.declare(`declare ptr @scr_regex_replace_fn(ptr, ptr, ptr, i32, i1 zeroext)`); + host.declare(`declare ptr @scr_regex_replace_fn(ptr, ptr, ptr, i32, i32, i1 zeroext)`); const result = B.tmp(); B.line( - `${result} = call ptr @scr_regex_replace_fn(ptr ${s.name}, ptr ${re.name}, ptr ${fn.name}, i32 ${arity}, i1 zeroext ${all.value})`, + `${result} = call ptr @scr_regex_replace_fn(ptr ${s.name}, ptr ${re.name}, ptr ${fn.name}, i32 ${shape.arity}, i32 ${shape.kinds | 0}, i1 zeroext ${all.value})`, ); const output = host.own({ name: result, type: e.type }); host.emitPendingCheck(); @@ -805,6 +1052,70 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue { return { name: t, type: e.type }; } if (e.fn === "bytes.absent") return host.own({ name: "null", type: e.type }); + if (e.fn === "bytes.disjoint") { + // [data, data + len * size) of each view, compared as addresses. A view + // that is not there (a binding the nest never assigned, NULL) answers + // false without reading its header, so the caller keeps its loop. + const B = host.B; + const values = e.args.map((arg) => ({ arg, value: host.emitReadReceiver(arg) })); + const present = values.map(({ value }) => { + const t = B.tmp(); + B.line(`${t} = icmp ne ptr ${value.name}, null`); + return t; + }); + const both = B.tmp(); + B.line(`${both} = and i1 ${present[0]}, ${present[1]}`); + const compare = B.newLabel("disjoint.compare"), + done = B.newLabel("disjoint.done"); + const from = B.currentLabel(); + B.condBr(both, compare, done); + B.startBlock(compare); + const range = (arg: IrExpr, pointer: string): { start: string; end: string } => { + const size = arg.type.kind === "bytes" ? BYTES_ELEMENT_SIZE[arg.type.elem] : 1; + const dataSlot = B.tmp(), + data = B.tmp(), + lenSlot = B.tmp(), + len = B.tmp(), + bytes = B.tmp(), + start = B.tmp(), + end = B.tmp(); + B.line(`${dataSlot} = getelementptr inbounds %ScrBytes, ptr ${pointer}, i64 0, i32 3`); + B.line(`${data} = load ptr, ptr ${dataSlot}`); + B.line(`${lenSlot} = getelementptr inbounds %ScrBytes, ptr ${pointer}, i64 0, i32 1`); + B.line(`${len} = load ${host.sizeType}, ptr ${lenSlot}`); + B.line(`${bytes} = mul ${host.sizeType} ${len}, ${size}`); + B.line(`${start} = ptrtoint ptr ${data} to ${host.sizeType}`); + B.line(`${end} = add ${host.sizeType} ${start}, ${bytes}`); + return { start, end }; + }; + const left = range(values[0]!.arg, values[0]!.value.name), + right = range(values[1]!.arg, values[1]!.value.name); + const before = B.tmp(), + after = B.tmp(), + apart = B.tmp(), + t = B.tmp(); + B.line(`${before} = icmp ule ${host.sizeType} ${left.end}, ${right.start}`); + B.line(`${after} = icmp ule ${host.sizeType} ${right.end}, ${left.start}`); + B.line(`${apart} = or i1 ${before}, ${after}`); + const compared = B.currentLabel(); + B.br(done); + B.startBlock(done); + B.line(`${t} = phi i1 [ false, %${from} ], [ ${apart}, %${compared} ]`); + return { name: t, type: e.type }; + } + if (e.fn === "record.isFrozen") { + // The frozen bit of the record's rc word (scr_rc.h SCR_RC_FROZEN) is + // its second-highest bit; an immortal word sets the highest one too. + const value = host.emitReadReceiver(e.args[0]!); + const S = host.sizeType; + const rc = host.B.tmp(), + top = host.B.tmp(), + t = host.B.tmp(); + host.B.line(`${rc} = load ${S}, ptr ${value.name}`); + host.B.line(`${top} = lshr ${S} ${rc}, ${S === "i64" ? 62 : 30}`); + host.B.line(`${t} = icmp eq ${S} ${top}, 1`); + return { name: t, type: e.type }; + } const prefix = e.fn.slice(0, e.fn.indexOf(".")) as LibCallPrefix; // events.once builds typed listener adapters beside the emitter calls. if (e.fn === "events.once" || e.fn === "events.onceSignal") @@ -913,10 +1224,16 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue { case "blob": case "wasm": case "array": + if (e.fn === "array.foreignThrow") return emitArrayForeignThrow(host, e); + return host.emitGenericLibCall(e); + case "collection": case "record": case "disposable": case "punycode": case "constants": + case "promise": + case "navigator": + case "mime": return host.emitGenericLibCall(e); case "nodeIsland": return host.emitGenericLibCall(e); @@ -933,3 +1250,97 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue { } } } + +/** `array.foreignThrow`: the checked conversion's TypeError for a foreign + * element read into a typed result. Always throws; the result is a dummy + * of the replaced read's type (the error.argTypeThrow pattern). */ +function emitArrayForeignThrow(host: LlvmEmitterContext, e: LibCallExpr): LlValue { + const B = host.B; + const arr = host.emitExpr(e.args[0]!); + const index = host.emitExpr(e.args[1]!); + const want = host.emitExpr(e.args[2]!); + host.declare(`declare void @scr_arr_foreign_throw(ptr, double, ptr)`); + B.line( + `call void @scr_arr_foreign_throw(ptr ${arr.name}, double ${index.name}, ptr ${want.name})`, + ); + const ty = host.llType(e.type); + if (ty === "void") { + host.emitAlwaysPendingUnwind(); + return { name: "", type: e.type }; + } + const dummy = ty === "double" ? "0.0" : ty === "i1" ? "false" : "null"; + const out = host.own({ name: dummy, type: e.type }); + host.emitAlwaysPendingUnwind(); + return out; +} + +/** The settlement adapter of Promise.all's tuple form for the builder + * `fnName`, which fulfills the result with the builder's tuple. */ +function allTupleAdapterFor(host: LlvmEmitterContext, fnName: string, tuple: IrType): string { + const key = `allTuple:${fnName}`; + let adapter = host.resolveThunks.get(key); + if (adapter !== undefined) return adapter; + adapter = host.contentSymbol("sc_all_tuple_", key); + host.resolveThunks.set(key, adapter); + host.journal?.allocated(adapter, "allTupleAdapterFor", [fnName, tuple]); + const rc = vAdapters(host.shapeHost, tuple); + host.declare(`declare ptr @scr_promise_payload_ref(ptr)`); + host.declare(`declare void @scr_promise_fulfill_ref(ptr, ptr, ptr, ptr, ptr)`); + host.resolveThunkDefs.push( + `define internal void @${adapter}(ptr %dst, ptr %src) ${FN_ATTRS} { ; Promise.all tuple`, + `entry:`, + ` %entries = call ptr @scr_promise_payload_ref(ptr %src) ; +1, moves into the builder`, + ` %tuple = call ptr @${host.callTarget(fnName)}(ptr %entries)`, + ` call void @scr_promise_fulfill_ref(ptr %dst, ptr %tuple, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(host.shapeHost, tuple)})`, + ` ret void`, + `}`, + ``, + ); + host.journal?.built(); + return adapter; +} + +registerReplay("allTupleAdapterFor", (host, args) => + allTupleAdapterFor(host, args[0] as string, args[1] as IrType), +); + +/** Promise.all's tuple form (promise.allTuple): the combinator over the + * entries stores each fulfilled entry promise, and a settlement adapter on + * the combinator calls the builder with them and fulfills the result with + * the tuple inside the combinator's settlement; a rejection settles the + * result the same way. The combinator itself counts as handled. */ +function emitPromiseAllTuple(host: LlvmEmitterContext, e: LibCallExpr): LlValue { + const B = host.B; + const build = e.args[1]!; + if (build.kind !== "closure" || e.type.kind !== "promise") + throw new InternalCompilerError("llvm emitter bug: promise.allTuple shape"); + const entriesT = e.args[0]!.type; + const entries = host.emitExpr(e.args[0]!); + const adapter = allTupleAdapterFor(host, build.fnName, e.type.inner); + host.declare(`declare ptr @scr_promise_all(ptr, ptr, ptr)`); + host.declare(`declare void @scr_promise_all_store_promise(ptr, double, ptr)`); + host.declare(`declare ptr @scr_promise_new()`); + host.declare(`declare void @scr_promise_race_add(ptr, ptr, ptr)`); + host.declare(`declare void @scr_promise_mark_handled(ptr)`); + host.declare(`declare void @scr_promise_release(ptr)`); + host.declare(`declare double @scr_arr_len(ptr)`); + if (entriesT.kind !== "array") + throw new InternalCompilerError("llvm emitter bug: promise.allTuple entries"); + const len = B.tmp(); + const cap = B.tmp(); + B.line(`${len} = call double @scr_arr_len(ptr ${entries.name})`); + B.line(`${cap} = fptoui double ${len} to ${host.sizeType}`); + const values = B.tmp(); + B.line(`${values} = ${arrNewCall(host.shapeHost, entriesT.elem, cap)}`); + host.own({ name: values, type: entriesT }); + const all = B.tmp(); + B.line( + `${all} = call ptr @scr_promise_all(ptr ${entries.name}, ptr ${values}, ptr @scr_promise_all_store_promise)`, + ); + B.line(`call void @scr_promise_mark_handled(ptr ${all})`); + const result = B.tmp(); + B.line(`${result} = call ptr @scr_promise_new()`); + B.line(`call void @scr_promise_race_add(ptr ${result}, ptr ${all}, ptr @${adapter})`); + B.line(`call void @scr_promise_release(ptr ${all})`); + return host.own({ name: result, type: e.type }); +} diff --git a/packages/compiler/src/backend/llvm/lib-filesystem.ts b/packages/compiler/src/backend/llvm/lib-filesystem.ts index 7d9be632d..9174f4775 100644 --- a/packages/compiler/src/backend/llvm/lib-filesystem.ts +++ b/packages/compiler/src/backend/llvm/lib-filesystem.ts @@ -1,7 +1,7 @@ /* Focused LLVM library-call emission extracted from emitter.ts. */ import { InternalCompilerError } from "../../errors.js"; import { undefinedArmTag } from "../../ir/analysis.js"; -import { DYN, F64, STATS_T, STRING } from "../../ir/ir.js"; +import { DYN, F64, STATS_T, STRING, type IrExpr } from "../../ir/ir.js"; import { mangleRecordNew } from "../mangle.js"; import { arrNewCall, traceArg, vAdapters } from "./shapes.js"; import type { LlvmEmitterContext, LibCallExpr, LlValue } from "./expr-context.js"; @@ -1027,6 +1027,105 @@ function emitPowerOfTwo(host: LlvmEmitterContext, n: string): string { return out; } +/** `base ** exponent` and `Math.pow(base, exponent)` as V8's math::pow + * answers them: a literal exponent 2 is `base * base`, a literal 0.5 is + * `sqrt(base + 0)` (+Infinity for an infinite base), and every other + * exponent calls scr_math_pow, which applies V8's other special cases + * before the platform pow. The platform pow is never called by its libm + * name, which LLVM would rewrite (pow(2, x) to exp2(x), pow(x, -1) to 1/x) + * into answers that can differ from V8's. */ +export function emitPowF64( + host: LlvmEmitterContext, + base: string, + exponent: string, + exponentExpr: IrExpr, +): string { + const B = host.B; + const literal = + exponentExpr.kind === "numLit" + ? exponentExpr.value + : exponentExpr.kind === "unary" && + exponentExpr.op === "-" && + exponentExpr.operand.kind === "numLit" + ? -exponentExpr.operand.value + : null; + const out = B.tmp(); + if (literal === 2) { + B.line(`${out} = fmul double ${base}, ${base}`); + return out; + } + if (literal === 0.5) { + host.declare(`declare double @llvm.sqrt.f64(double)`); + host.declare(`declare double @llvm.fabs.f64(double)`); + const plus = B.tmp(), + root = B.tmp(), + magnitude = B.tmp(), + infinite = B.tmp(); + B.line(`${plus} = fadd double ${base}, ${f64Lit(0)}`); + B.line(`${root} = call double @llvm.sqrt.f64(double ${plus})`); + B.line(`${magnitude} = call double @llvm.fabs.f64(double ${base})`); + B.line(`${infinite} = fcmp oeq double ${magnitude}, 0x7FF0000000000000`); + B.line(`${out} = select i1 ${infinite}, double 0x7FF0000000000000, double ${root}`); + return out; + } + host.declare(`declare double @scr_math_pow(double, double)`); + B.line( + `${out} = call double @scr_math_pow(double ${base}, double ${exponent}) nounwind willreturn`, + ); + return out; +} + +/** `Math.pow(2, n)`, the divisor emitPowerOfTwoQuotient takes. */ +export function isPowerOfTwoCall(e: IrExpr): boolean { + return ( + e.kind === "libCall" && + e.fn === "math.pow" && + e.args.length === 2 && + e.args[0]!.kind === "numLit" && + e.args[0]!.value === 2 + ); +} + +/** `left / divisor` for a divisor that is usually a power of two + * (`x / Math.pow(2, n)`). A positive power of two from 2^-1022 to 2^1022 + * has a reciprocal that is a power of two too, whose bits are + * 0x7FE0000000000000 minus the divisor's, and the product with it is the + * quotient for every `left`: both are `left` scaled by the same power of + * two, rounded once. Any other divisor divides. */ +export function emitPowerOfTwoQuotient( + host: LlvmEmitterContext, + left: string, + divisor: string, +): string { + const B = host.B; + const bits = B.tmp(), + mantissa = B.tmp(), + plain = B.tmp(), + biased = B.tmp(), + offset = B.tmp(), + normal = B.tmp(), + power = B.tmp(), + inverseBits = B.tmp(), + inverse = B.tmp(), + scaled = B.tmp(), + divided = B.tmp(), + out = B.tmp(); + B.line(`${bits} = bitcast double ${divisor} to i64`); + B.line(`${mantissa} = and i64 ${bits}, ${2n ** 52n - 1n}`); + B.line(`${plain} = icmp eq i64 ${mantissa}, 0`); + // Biased exponents 1 to 2045 with a clear sign bit. + B.line(`${biased} = lshr i64 ${bits}, 52`); + B.line(`${offset} = sub i64 ${biased}, 1`); + B.line(`${normal} = icmp ult i64 ${offset}, 2045`); + B.line(`${power} = and i1 ${plain}, ${normal}`); + B.line(`${inverseBits} = sub i64 ${0x7fe0000000000000n}, ${bits}`); + B.line(`${inverse} = bitcast i64 ${inverseBits} to double`); + B.line(`${scaled} = fmul double ${left}, ${inverse}`); + B.line(`${divided} = fdiv double ${left}, ${divisor}`); + B.line(`${out} = select i1 ${power}, double ${scaled}, double ${divided}`); + return out; +} + export function emitPrimitiveLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue { const B = host.B; if (e.fn === "math.min" || e.fn === "math.max") { @@ -1048,6 +1147,11 @@ export function emitPrimitiveLibCall(host: LlvmEmitterContext, e: LibCallExpr): const v = host.emitExpr(e.args[1]!); return { name: emitPowerOfTwo(host, v.name), type: e.type }; } + if (e.fn === "math.pow") { + const base = host.emitExpr(e.args[0]!); + const exponent = host.emitExpr(e.args[1]!); + return { name: emitPowF64(host, base.name, exponent.name, e.args[1]!), type: e.type }; + } if (e.fn === "math.floor" || e.fn === "math.ceil") { const intr = e.fn === "math.floor" ? "floor" : "ceil"; const v = host.emitExpr(e.args[0]!); diff --git a/packages/compiler/src/backend/llvm/lib-http2.ts b/packages/compiler/src/backend/llvm/lib-http2.ts index 321f1d005..038e98dc4 100644 --- a/packages/compiler/src/backend/llvm/lib-http2.ts +++ b/packages/compiler/src/backend/llvm/lib-http2.ts @@ -9,6 +9,7 @@ import { callbackType, finishRuntimeCall, ptr, + caBytes, rawBytes, requestHandler, } from "./lib-abi.js"; @@ -21,7 +22,7 @@ function sniAnswer(host: LlvmEmitterContext, type: IrType): string { const key = "sni:" + typeKey(type); const previous = host.resolveThunks.get(key); if (previous !== undefined) return previous; - const symbol = `sc_sni_answer_${host.resolveThunks.size}`; + const symbol = host.contentSymbol("sc_sni_answer_", key); host.resolveThunks.set(key, symbol); host.journal?.allocated(symbol, "sniAnswer", [type]); const params = ["ptr %self"], @@ -94,11 +95,12 @@ export function sniCallbackArgs(host: LlvmEmitterContext, cb: LlValue): AbiArgum let type = cb.type, value = cb.name; if (type.kind === "union") { - const arms = host.unionsById.get(type.unionId)?.arms ?? []; + const unionId = type.unionId; + const arms = host.unionsById.get(unionId)?.arms ?? []; const tag = arms.findIndex((arm) => arm.kind === "func"); if (tag < 0) throw new InternalCompilerError("SNI callback union lacks its function arm"); type = arms[tag]!; - value = host.unwrapNullableClosure(value, tag); + value = host.unwrapNullableClosure(value, unionId, tag); } else host.toHeap(cb); if (type.kind !== "func" || type.params[1] === undefined) throw new InternalCompilerError("SNI callback lacks its answer function"); @@ -204,7 +206,7 @@ export function emitHttp2LibCall(host: LlvmEmitterContext, e: LibCallExpr): LlVa ); return finishRuntimeCall(host, e, "scr_http2_connect", [ ...a.slice(0, 2), - ...rawBytes(host, args[2]!), + ...caBytes(host, args[2]!), ...handler, ]); } diff --git a/packages/compiler/src/backend/llvm/lib-lookup.ts b/packages/compiler/src/backend/llvm/lib-lookup.ts index 56e94461f..620833819 100644 --- a/packages/compiler/src/backend/llvm/lib-lookup.ts +++ b/packages/compiler/src/backend/llvm/lib-lookup.ts @@ -37,7 +37,7 @@ function lookupAnswer(host: LlvmEmitterContext, answer: IrType & { kind: "func" const key = "lookup:" + typeKey(answer); let symbol = host.resolveThunks.get(key); if (symbol === undefined) { - symbol = `sc_lookup_answer_${host.resolveThunks.size}`; + symbol = host.contentSymbol("sc_lookup_answer_", key); host.resolveThunks.set(key, symbol); host.journal?.allocated(symbol, "lookupAnswer", [answer]); for (const declaration of [ diff --git a/packages/compiler/src/backend/llvm/lib-network.ts b/packages/compiler/src/backend/llvm/lib-network.ts index d49bbde14..5136d4aa0 100644 --- a/packages/compiler/src/backend/llvm/lib-network.ts +++ b/packages/compiler/src/backend/llvm/lib-network.ts @@ -165,7 +165,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr) const funcTag = def ? def.arms.findIndex((a) => a.kind === "func") : -1; if (funcTag < 0) throw new InternalCompilerError(`llvm emitter bug: ${e.fn} union lacks its func arm`); - cb = host.unwrapNullableClosure(args[cbIndex]!.name, funcTag); + cb = host.unwrapNullableClosure(args[cbIndex]!.name, cbT.unionId, funcTag); } } const valueCount = reusePort ? 5 : 4; @@ -309,6 +309,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr) e.fn === "net.sockOnClose" || e.fn === "net.sockOnConnect" || e.fn === "net.sockOnTimeout" || + e.fn === "net.sockOnDrain" || e.fn === "net.sockOnReadable" || e.fn === "net.sockOnReady" || e.fn === "http.reqOnEnd" || @@ -329,6 +330,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr) "net.sockOnClose": "scr_net_sock_on_close", "net.sockOnConnect": "scr_net_sock_on_connect", "net.sockOnTimeout": "scr_net_sock_on_timeout", + "net.sockOnDrain": "scr_net_sock_on_drain", "net.sockOnReadable": "scr_net_sock_on_readable", "net.sockOnReady": "scr_net_sock_on_ready", "http.reqOnEnd": "scr_http_req_on_end", @@ -399,6 +401,22 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr) host.emitPendingCheck(); return out; } + if (e.fn === "net.connectPath" || e.fn === "net.connectPathCb") { + const args = e.args.map((a) => host.emitExpr(a)); + let cb = "null"; + if (e.fn === "net.connectPathCb") { + host.toHeap(args[2]!); + cb = args[2]!.name; + } + host.declare(`declare ptr @scr_net_connect_path_checked(ptr, i1 zeroext, ptr)`); + const t = B.tmp(); + B.line( + `${t} = call ptr @scr_net_connect_path_checked(ptr ${args[0]!.name}, i1 ${args[1]!.name}, ptr ${cb})`, + ); + const out = host.own({ name: t, type: e.type }); + host.emitPendingCheck(); + return out; + } if (e.fn === "net.sockOnData" || e.fn === "http.reqOnData") { const cbT = e.args[1]!.type; if (cbT.kind !== "func") diff --git a/packages/compiler/src/backend/llvm/lib-process.ts b/packages/compiler/src/backend/llvm/lib-process.ts index 3e8a9bff5..bdb1fba27 100644 --- a/packages/compiler/src/backend/llvm/lib-process.ts +++ b/packages/compiler/src/backend/llvm/lib-process.ts @@ -1,10 +1,12 @@ /* Focused LLVM library-call emission extracted from emitter.ts. */ import { InternalCompilerError } from "../../errors.js"; import { undefinedArmTag } from "../../ir/analysis.js"; -import { F64, RUNTIME_ERROR_CLASSES, STRING } from "../../ir/ir.js"; +import { F64, type IrType, RUNTIME_ERROR_CLASSES, STRING, typeKey } from "../../ir/ir.js"; import type { LlvmEmitterContext, LibCallExpr, LlValue } from "./expr-context.js"; import { f64Lit } from "./common.js"; import { mockTimerSym } from "./lib-shared.js"; +import { FN_ATTRS } from "./shapes.js"; +import { registerReplay } from "./emit-journal.js"; export function emitChildProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue { const B = host.B; @@ -118,6 +120,13 @@ export function emitChildProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr B.line(`call void @scr_child_on_error(ptr ${child.name}, ptr ${cb.name}, ptr @${adapter})`); return { name: "", type: e.type }; } + if (e.fn === "child.hasChannel") { + const child = host.emitExpr(e.args[0]!); + host.declare(`declare zeroext i1 @scr_child_ipc_present(ptr)`); + const out = B.tmp(); + B.line(`${out} = call i1 @scr_child_ipc_present(ptr ${child.name})`); + return { name: out, type: e.type }; + } if (e.fn === "child.connected") { const child = host.emitExpr(e.args[0]!); host.declare(`declare zeroext i1 @scr_child_ipc_connected(ptr)`); @@ -460,9 +469,64 @@ export function emitAsyncContextLibCall(host: LlvmEmitterContext, e: LibCallExpr for (const a of e.args) host.emitExpr(a); return { name: "", type: e.type }; } + if (e.fn === "als.bindCallback") { + // The closure moves into the bound closure, whose entry is the + // per-signature thunk; with nothing to bind the runtime answers the + // closure itself. + if (e.type.kind !== "func") + throw new InternalCompilerError("llvm emitter bug: als.bindCallback on non-func"); + const cb = host.emitExpr(e.args[0]!); + const always = host.emitExpr(e.args[1]!); + host.toHeap(cb); + const thunk = boundCallbackThunk(host, e.type); + host.declare("declare ptr @scr_als_bind_closure(ptr, ptr, i1 zeroext)"); + const t = B.tmp(); + B.line( + `${t} = call ptr @scr_als_bind_closure(ptr ${cb.name}, ptr @${thunk}, i1 ${always.name})`, + ); + return host.own({ name: t, type: e.type }); + } return host.emitGenericLibCall(e); } +/** The entry of a callback bound to an AsyncLocalStorage context, one per + * callback signature: enters the captured context, calls the target with + * the same arguments (the callValue ABI) and restores the caller's + * context, also when the target leaves an exception pending. */ +function boundCallbackThunk(host: LlvmEmitterContext, cbT: IrType & { kind: "func" }): string { + const key = `alsbound:${typeKey(cbT)}`; + let sym = host.resolveThunks.get(key); + if (sym) return sym; + sym = host.contentSymbol("sc_alsbound_", key); + host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "boundCallbackThunk", [cbT]); + host.declare("declare ptr @scr_als_bound_target(ptr)"); + host.declare("declare ptr @scr_als_bound_enter(ptr)"); + host.declare("declare void @scr_als_bound_leave(ptr, ptr)"); + const ret = host.llType(cbT.ret); + const params = cbT.params.map((p, i) => `${host.llType(p)} %a${i}`); + const call = `call ${ret} %fn(${["ptr %cb", ...params].join(", ")})`; + host.resolveThunkDefs.push( + `define internal ${ret} @${sym}(${["ptr %self", ...params].join(", ")}) ${FN_ATTRS} { ; AsyncLocalStorage-bound callback ${typeKey(cbT)}`, + `entry:`, + ` %cb = call ptr @scr_als_bound_target(ptr %self)`, + ` %prev = call ptr @scr_als_bound_enter(ptr %self)`, + ` %fnp = getelementptr inbounds %ScrClosure, ptr %cb, i64 0, i32 1`, + ` %fn = load ptr, ptr %fnp`, + ret === "void" ? ` ${call}` : ` %r = ${call}`, + ` call void @scr_als_bound_leave(ptr %prev, ptr %cb)`, + ret === "void" ? ` ret void` : ` ret ${ret} %r`, + `}`, + ``, + ); + host.journal?.built(); + return sym; +} + +registerReplay("boundCallbackThunk", (host, args) => + boundCallbackThunk(host, args[0] as IrType & { kind: "func" }), +); + export function emitProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue { const B = host.B; if (e.fn === "process.forkTarget") { diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 2ad98ab7c..2fd4e49a9 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -35,11 +35,38 @@ export const ASSERT_VERDICT_LIB_FNS: ReadonlyMap = new Ma ["assert.partialDeepResult", null], ]); +/** The Math functions that call scr_ieee754.c. They read and write no + * memory, so calls with the same argument fold and loops may hoist them. */ +export const PURE_MATH_LIB_FNS: ReadonlySet = new Set([ + "math.sin", + "math.sinh", + "math.cos", + "math.cosh", + "math.tan", + "math.tanh", + "math.asin", + "math.asinh", + "math.acos", + "math.acosh", + "math.atan", + "math.atanh", + "math.cbrt", + "math.exp", + "math.expm1", + "math.log", + "math.log1p", + "math.log2", + "math.log10", + "math.atan2", +]); + export const LIB_FN_SYMS: Record = { "assert.expectsErrDyn": "scr_assert_expects_err_dyn", + "assert.throwsValidate": "scr_assert_throws_validate", "insp.jsonDyn": "scr_util_format_j", "http.reqH2StreamOrThrow": "scr_http_req_h2_stream_or_throw", "intl.segmenterNew": "scr_intl_segmenter_new", + "intl.segmenterRefusing": "scr_intl_segmenter_refusing", "weakMap.is": "scr_weak_map_is", "weakSet.is": "scr_weak_set_is", "weakMap.new": "scr_weak_map_new", @@ -82,7 +109,19 @@ export const LIB_FN_SYMS: Record = { "util.isDeepStrictEqual": "scr_util_is_deep_strict_equal", "util.styleText": "scr_util_style_text", "util.inherits": "scr_util_inherits", + "util.inheritsPrototype": "scr_util_inherits_prototype", "util.deprecate": "scr_util_deprecate", + "util.isArrayDeprecated": "scr_util_is_array_deprecated", + "util.extend": "scr_util_extend", + "util.signalExitCode": "scr_util_signal_exit_code", + "util.aborted": "scr_util_aborted", + "util.diff": "scr_util_diff", + "util.debounce": "scr_util_debounce", + "util.throttle": "scr_util_throttle", + "mime.typeNew": "scr_mime_type_new", + "mime.typeParse": "scr_mime_type_parse", + "mime.paramsNew": "scr_mime_params_new", + "mime.is": "scr_mime_is", "util.callbackify": "scr_util_callbackify", "util.promisifyDyn": "scr_util_promisify", "util.debuglog": "scr_util_debuglog", @@ -101,29 +140,32 @@ export const LIB_FN_SYMS: Record = { "str.localeCase": "scr_str_locale_case", "str.replaceLiteral": "scr_str_replace_literal", "str.replaceChars": "scr_str_replace_chars", + "str.codePoints": "scr_str_code_points", "num.fromString": "scr_string_to_number", "math.round": "scr_math_round", - "math.sin": "sin", - "math.sinh": "sinh", - "math.cos": "cos", - "math.cosh": "cosh", - "math.tan": "tan", - "math.tanh": "tanh", - "math.asin": "asin", - "math.asinh": "asinh", - "math.acos": "acos", - "math.acosh": "acosh", - "math.atan": "atan", - "math.atanh": "atanh", - "math.cbrt": "cbrt", - "math.exp": "exp", - "math.expm1": "expm1", + // Math's transcendental functions are V8's (scr_ieee754.c), so their + // results are the ones Node prints. + "math.sin": "scr_ieee754_sin", + "math.sinh": "scr_ieee754_sinh", + "math.cos": "scr_ieee754_cos", + "math.cosh": "scr_ieee754_cosh", + "math.tan": "scr_ieee754_tan", + "math.tanh": "scr_ieee754_tanh", + "math.asin": "scr_ieee754_asin", + "math.asinh": "scr_ieee754_asinh", + "math.acos": "scr_ieee754_acos", + "math.acosh": "scr_ieee754_acosh", + "math.atan": "scr_ieee754_atan", + "math.atanh": "scr_ieee754_atanh", + "math.cbrt": "scr_ieee754_cbrt", + "math.exp": "scr_ieee754_exp", + "math.expm1": "scr_ieee754_expm1", "math.sqrt": "sqrt", - "math.log": "log", - "math.log1p": "log1p", - "math.log2": "log2", - "math.log10": "log10", - "math.atan2": "atan2", + "math.log": "scr_ieee754_log", + "math.log1p": "scr_ieee754_log1p", + "math.log2": "scr_ieee754_log2", + "math.log10": "scr_ieee754_log10", + "math.atan2": "scr_ieee754_atan2", "math.pow": "scr_math_pow", // encodeURIComponent throws URIError on a lone surrogate // (MAY_THROW_LIB_FNS); decodeUriComponent is with the throwing tier below. @@ -252,8 +294,15 @@ export const LIB_FN_SYMS: Record = { "console.table": "scr_console_table", "console.dirDyn": "scr_console_dir_dyn", "global.native": "scr_global_native", + "global.installValues": "scr_global_values_install", + "global.forInObject": "scr_global_for_in_object", + "navigator.object": "scr_navigator_object", + "navigator.language": "scr_navigator_language", + "navigator.languages": "scr_navigator_languages", + "navigator.platform": "scr_navigator_platform", "process.builtinModule": "scr_process_builtin_module", "process.builtinUnsupported": "scr_process_builtin_unsupported", + "module.namespaceValue": "scr_module_namespace_value", "fs.callbackValue": "scr_fs_callback_value", "fs.callbackCall": "scr_fs_callback_call", "process.hrtimeValue": "scr_process_hrtime_value", @@ -270,6 +319,8 @@ export const LIB_FN_SYMS: Record = { "module.id": "scr_module_id", "module.path": "scr_module_path", "module.paths": "scr_module_paths", + "module.requireComputed": "scr_require_computed", + "module.resolveComputed": "scr_require_resolve", "module.children": "scr_module_children", "module.parent": "scr_module_parent", "module.loaded": "scr_module_loaded", @@ -333,6 +384,9 @@ export const LIB_FN_SYMS: Record = { "date.parse": "scr_date_parse", "date.getTime": "scr_date_get_time", "date.valueOf": "scr_date_get_time", + "date.fromTime": "scr_date_get_time", + "date.setFields": "scr_date_set_fields", + "date.timeClip": "scr_date_new_ms", "date.parseGetTime": "scr_date_parse", "date.utc": "scr_date_utc", "date.newLocal": "scr_date_new_local", @@ -375,6 +429,7 @@ export const LIB_FN_SYMS: Record = { "fs.rmRetrySync": "scr_fs_rm_opts_retry", "fs.rmdirSync": "scr_fs_rmdir", "fs.readdirSync": "scr_fs_readdir", + "fs.readdirRecursiveSync": "scr_fs_readdir_recursive", "fs.globSync": "scr_fs_glob_strings", "fs.callbackGlob": "scr_fs_glob_callback", "fsp.globStrings": "scr_fsp_glob_strings", @@ -398,6 +453,9 @@ export const LIB_FN_SYMS: Record = { "assert.eqBool": "scr_assert_eq_bool", "assert.eqSym": "scr_assert_eq_sym", "assert.eqDyn": "scr_assert_eq_dyn", + "assert.messageValue": "scr_assert_message_value", + "assert.methodValue": "scr_assert_method_value", + "assert.messageClear": "scr_assert_message_clear", "assert.looseResult": "scr_assert_loose_result", "assert.deepResult": "scr_assert_deep_result", "assert.looseDeepResult": "scr_assert_loose_deep_result", @@ -454,6 +512,7 @@ export const LIB_FN_SYMS: Record = { "buffer.poolFromDyn": "scr_buffer_from_dyn_pooled", "buffer.poolFromDynRef": "scr_buffer_from_dyn_ref_pooled", "buffer.poolFromDynOffset": "scr_buffer_from_dyn_offset_pooled", + "buffer.poolFromDynRange": "scr_buffer_from_dyn_range_pooled", "buffer.poolSize": "scr_buffer_pool_size", "buffer.setPoolSize": "scr_buffer_set_pool_size", "buffer.byteLenDyn": "scr_buffer_byte_length_dyn", @@ -479,6 +538,12 @@ export const LIB_FN_SYMS: Record = { "bytes.equalsChk": "scr_bytes_equals_chk", "bytes.compareChk": "scr_bytes_compare_chk", "buffer.newStringFail": "scr_buffer_new_string_fail", + "buffer.deprecation": "scr_buffer_deprecation", + "buffer.deprecationCount": "scr_buffer_deprecation_count", + "buffer.constructor": "scr_buffer_constructor_value", + "buffer.installValue": "scr_buffer_install_value", + "buffer.staticValue": "scr_buffer_static_value", + "buffer.prototypeMember": "scr_buffer_prototype_member", "fs.toUnixTimestamp": "scr_fs_to_unix_timestamp", "fs.existsChk": "scr_fs_exists_async", "fs.mkdtempSyncChk": "scr_fs_mkdtemp_sync_chk", @@ -515,6 +580,7 @@ export const LIB_FN_SYMS: Record = { "fs.openSync": "scr_fs_open", "fs.openNumericSync": "scr_fs_open_numeric", "fs.readSync": "scr_fs_read_sync", + "fs.readSyncDyn": "scr_fs_read_sync_dyn", "fs.writeSync": "scr_fs_write_sync", "fs.writeStrSync": "scr_fs_write_str_sync", "fs.closeSync": "scr_fs_close", @@ -560,12 +626,14 @@ export const LIB_FN_SYMS: Record = { "text.decoderEncoding": "scr_text_decoder_encoding", "text.decoderName": "scr_text_decoder_name", "text.encodeInto": "scr_text_encode_into", + "text.codecIs": "scr_text_codec_is", "buffer.isAscii": "scr_buffer_is_ascii", "buffer.isUtf8": "scr_buffer_is_utf8", "buffer.transcode": "scr_buffer_transcode", "zlib.deflateSync": "scr_zlib_deflate", "zlib.inflateSync": "scr_zlib_inflate", "zlib.crc32": "scr_zlib_crc32", + "zlib.brotliInstall": "scr_brotli_install", // The CA-store unit. get/set throw (an unknown type name, a // certificate-free set) and take the generic path's pending check. "tlsca.root": "scr_tls_ca_root", @@ -649,6 +717,7 @@ export const LIB_FN_SYMS: Record = { "process.stderrWriteBytes": "scr_process_stderr_write_bytes", "insp.buffer": "scr_insp_buffer", "insp.typedArray": "scr_insp_typed_array", + "insp.bytes": "scr_insp_bytes", // util.inspect (scr_inspect.c): all args borrowed; string results +1; // the begin/entry/key/moreItems/end accumulator drives the emitted // container walks. None of these throw. @@ -667,6 +736,8 @@ export const LIB_FN_SYMS: Record = { "insp.moreItems": "scr_insp_more_items", "insp.end": "scr_insp_end", "insp.nestedBegin": "scr_insp_nested_begin", + "insp.mark": "scr_insp_mark", + "insp.unwind": "scr_insp_unwind", "insp.nestedEnd": "scr_insp_nested_end", // Circular references over cycle-capable composites (Node's // seen/circular machinery — none of these throw; refWrap borrows its @@ -677,6 +748,7 @@ export const LIB_FN_SYMS: Record = { "insp.circular": "scr_insp_circular", // Undeclared source properties kept beside a checked record (scr_json.c). "record.sourceHas": "scr_record_source_has", + "record.sourceSpread": "scr_record_source_spread", // WHATWG URL + URLSearchParams (scr_url.c / scr_url_params.c): // constructions +1; url.new, the fileURLToPath pair, the win32 // pathToFileURL flavor, and sp.fromPairs throw catchably (may-throw). @@ -701,6 +773,9 @@ export const LIB_FN_SYMS: Record = { "url.hash": "scr_url_hash", "url.searchParams": "scr_url_search_params", "url.fileURLToPathUrl": "scr_url_to_path", + "url.fileURLToPathValue": "scr_url_to_path_value", + "fs.checkPath": "scr_fs_check_path", + "fs.checkReadEncoding": "scr_fs_check_read_encoding", "url.fileURLToPathChecked": "scr_url_checked_to_path", "url.fileURLToPathOptions": "scr_url_checked_to_path_options", "url.fileURLToPathBuffer": "scr_url_checked_to_path_buffer", @@ -718,6 +793,7 @@ export const LIB_FN_SYMS: Record = { "punycode.ucs2Encode": "scr_str_from_code_point", "punycode.warn": "scr_punycode_warn", "url.domainToUnicode": "scr_url_domain_to_unicode", + "url.toHttpOptions": "scr_url_to_http_options_dyn", "url.pathToFileURL": "scr_url_from_path", "url.pathToFileURLPlatform": "scr_url_from_path_platform", "url.pathToFileURLWin32": "scr_url_from_path", @@ -777,6 +853,10 @@ export const LIB_FN_SYMS: Record = { "procStream.write": "scr_proc_stream_write", "stream.stdioWritable": "scr_stream_stdio_writable", "stream.unboxDyn": "scr_stream_unbox_dyn", + "stream.dynIterableSource": "scr_stream_dyn_iterable_source", + "stream.dynSourceValue": "scr_stream_dyn_source_value", + "stream.unboxDynStage": "scr_stream_unbox_dyn_stage", + "stream.dynTransformResult": "scr_stream_dyn_transform_result", "stream.handleStream": "scr_stream_handle", "stream.pipeDyn": "scr_stream_pipe_dyn", "stream.unpipeDyn": "scr_stream_unpipe_dyn", @@ -958,6 +1038,7 @@ export const LIB_FN_SYMS: Record = { "fetch.streamIs": "scr_fetch_stream_is", "zlib.streamIs": "scr_zlib_stream_is", "fetch.streamNew": "scr_fetch_stream_new", + "fetch.transformNew": "scr_fetch_transform_new", "fetch.streamFrom": "scr_fetch_stream_from", "fetch.readerRead": "scr_fetch_reader_read", "json.parse": "scr_json_parse", @@ -970,6 +1051,7 @@ export const LIB_FN_SYMS: Record = { "dyn.keySetComputed": "scr_dyn_key_set_computed", "dyn.keyDelete": "scr_dyn_key_delete", "dyn.keyDeleteComputed": "scr_dyn_key_delete_computed", + "dyn.reflectDeleteProperty": "scr_dyn_reflect_delete_property", "dyn.hasKeyComputed": "scr_dyn_has_key_computed", "dyn.hasOwnComputed": "scr_dyn_has_own_computed", "dyn.propertyIsEnumerableComputed": "scr_dyn_property_is_enumerable_computed", @@ -1021,6 +1103,12 @@ export const LIB_FN_SYMS: Record = { "class.descriptorHasValue": "scr_class_descriptor_has_value", "class.throwReadOnly": "scr_class_throw_read_only", "class.throwNotExtensible": "scr_class_throw_not_extensible", + "record.freeze": "scr_record_freeze", + "record.throwFrozen": "scr_record_throw_frozen", + "array.freeze": "scr_arr_freeze", + "array.isFrozen": "scr_arr_is_frozen", + "collection.preventExtensions": "scr_map_prevent_extensions", + "collection.isExtensible": "scr_map_is_extensible", "dyn.nativeSetNew": "scr_dyn_native_set_new", "dyn.nativeSetIs": "scr_dyn_native_set_is", "dyn.nativeMapIs": "scr_dyn_native_map_is", @@ -1029,6 +1117,8 @@ export const LIB_FN_SYMS: Record = { "dyn.nativeDateHandleIs": "scr_dyn_native_date_is", "dyn.globalConstructor": "scr_dyn_global_constructor", "dyn.builtinFunction": "scr_dyn_builtin_function", + "dyn.namespaceNew": "scr_dyn_namespace_new", + "dyn.namespaceDefine": "scr_dyn_namespace_define", "date.nativeNew": "scr_dyn_native_date_new", "date.nativeNewFields": "scr_dyn_native_date_new_fields", "date.nativeNewValue": "scr_dyn_native_date_new_value", @@ -1044,6 +1134,27 @@ export const LIB_FN_SYMS: Record = { "dyn.generatorThis": "scr_gen_receiver", "insp.dyn": "scr_insp_dyn", "insp.dynS": "scr_insp_dyn_s", + "insp.format": "scr_insp_format_pack", + "insp.formatPrimitives": "scr_insp_format_primitives", + "insp.formatList": "scr_insp_format_list", + "insp.formatWithOptions": "scr_insp_format_with_options", + "insp.inspect": "scr_insp_inspect", + "insp.customEnabled": "scr_insp_custom_enabled", + "insp.registerCustom": "scr_insp_register_custom", + "insp.customInspect": "scr_insp_custom_inspect", + "insp.depthKind": "scr_insp_depth_kind", + "insp.customDepth": "scr_insp_custom_depth", + "insp.customOptions": "scr_insp_custom_options", + "insp.inspectFunction": "scr_insp_inspect_function", + "insp.customText": "scr_insp_custom_text", + "insp.customIsSelf": "scr_insp_custom_is_self", + "insp.customResult": "scr_insp_custom_result", + "insp.customCallBegin": "scr_insp_custom_call_begin", + "insp.customCallEnd": "scr_insp_custom_call_end", + "insp.formatSpecifies": "scr_insp_format_specifies", + "insp.formatSpecifiesDyn": "scr_insp_format_specifies_dyn", + "insp.dynFormatS": "scr_insp_dyn_format_s", + "insp.dynHidden": "scr_insp_dyn_hidden", "fs.readFileSyncDyn": "scr_fs_read_file_sync_dyn", "fs.readFileEncoded": "scr_fs_read_file_encoded", "fs.readFdEncoded": "scr_fs_read_fd_encoded", @@ -1063,9 +1174,32 @@ export const LIB_FN_SYMS: Record = { "crypto.generateKeyPairSync": "scr_crypto_generate_key_pair_sync", "crypto.generateKeyPairCb": "scr_crypto_generate_key_pair_async", "crypto.signOneshot": "scr_crypto_sign_oneshot", + "crypto.diffieHellman": "scr_crypto_diffie_hellman", "crypto.verifyOneshot": "scr_crypto_verify_oneshot", "crypto.createSign": "scr_crypto_create_sign", "crypto.createVerify": "scr_crypto_create_verify", + "crypto.x509Raw": "scr_crypto_x509_raw", + "crypto.x509RawDer": "scr_crypto_x509_raw_der", + "crypto.x509RawStr": "scr_crypto_x509_raw_str", + "crypto.x509Subject": "scr_crypto_x509_subject", + "crypto.x509Issuer": "scr_crypto_x509_issuer", + "crypto.x509SubjectAltName": "scr_crypto_x509_subject_alt_name", + "crypto.x509InfoAccess": "scr_crypto_x509_info_access", + "crypto.x509SerialNumber": "scr_crypto_x509_serial_number", + "crypto.x509Fingerprint256": "scr_crypto_x509_fingerprint256", + "crypto.x509Fingerprint512": "scr_crypto_x509_fingerprint512", + "crypto.x509Ca": "scr_crypto_x509_ca", + "crypto.x509SignatureAlgorithm": "scr_crypto_x509_signature_algorithm", + "crypto.x509SignatureAlgorithmOid": "scr_crypto_x509_signature_algorithm_oid", + "crypto.x509CheckHost": "scr_crypto_x509_check_host", + "crypto.x509CheckEmail": "scr_crypto_x509_check_email", + "crypto.x509CheckIP": "scr_crypto_x509_check_ip", + "crypto.x509CheckIssued": "scr_crypto_x509_check_issued", + "crypto.x509LegacyObject": "scr_crypto_x509_legacy_object", + "crypto.x509Pem": "scr_crypto_x509_pem", + "crypto.x509PublicKey": "scr_crypto_x509_public_key", + "crypto.x509Verify": "scr_crypto_x509_verify", + "crypto.x509CheckPrivateKey": "scr_crypto_x509_check_private_key", "fs.writeFileModeSync": "scr_fs_write_file_mode", "fs.chownSync": "scr_fs_chown", "fs.lchownSync": "scr_fs_lchown", @@ -1276,6 +1410,8 @@ export const LIB_FN_SYMS: Record = { "net.sockResetAndDestroy": "scr_net_sock_reset_and_destroy", "net.sockAddress": "scr_net_sock_address", "net.sockBytesWritten": "scr_net_sock_bytes_written", + "net.sockWritableLength": "scr_net_sock_writable_length", + "net.sockWritableNeedDrain": "scr_net_sock_writable_need_drain", "net.sockAddrField": "scr_net_sock_addr_field", "net.sockBytesRead": "scr_net_sock_bytes_read", "net.sockConnecting": "scr_net_sock_connecting", @@ -1329,6 +1465,13 @@ export const LIB_FN_SYMS: Record = { "http.reqPipeSock": "scr_http_req_pipe_sock", "http.reqResume": "scr_http_req_resume", "http.reqPause": "scr_http_req_pause", + "array.subclassConstruct": "scr_arr_subclass_construct", + "array.subclassConstructDyn": "scr_arr_subclass_construct_dyn", + "array.speciesAdopt": "scr_arr_species_adopt", + "array.speciesCreate": "scr_arr_species_create", + "array.speciesFinish": "scr_arr_species_finish", + "array.classConstructAdopt": "scr_arr_class_construct_adopt", + "array.dynIsClass": "scr_dyn_is_array_class", "http.reqDestroy": "scr_http_req_destroy", "http.reqSetEncoding": "scr_http_req_set_encoding", "http.resSetHeader": "scr_http_res_set_header", @@ -1480,6 +1623,10 @@ export const LIB_FN_SYMS: Record = { "als.exitRun": "scr_als_exit_run", "als.enterWith": "scr_als_enter_with", "als.disable": "scr_als_disable", + "als.newOptions": "scr_als_new_options", + "als.name": "scr_als_name", + "als.bindCallbackDyn": "scr_als_bind_dyn", + "als.snapshot": "scr_als_snapshot", "dc.chanBindStore": "scr_dc_chan_bind_store", "dc.chanUnbindStore": "scr_dc_chan_unbind_store", "dc.chanRunStores": "scr_dc_chan_run_stores", @@ -1584,6 +1731,7 @@ export const LIB_FN_SYMS: Record = { "dyn.objEntries": "scr_dyn_obj_entries", "dyn.structuredClone": "scr_structured_clone", "dyn.declAdopt": "scr_dyn_decl_adopt", + "dyn.adoptShared": "scr_dyn_adopt_shared", "dyn.cloneMissing": "scr_structured_clone_missing", "dyn.cloneTransferFail": "scr_structured_clone_transfer_fail", "dyn.cloneTransferFailWith": "scr_structured_clone_transfer_fail_with", @@ -1596,6 +1744,7 @@ export const LIB_FN_SYMS: Record = { "regex.groupParseInt": "scr_regex_group_parse_int", "regex.resultProp": "scr_regex_result_prop", "array.namedKeys": "scr_arr_named_keys", + "array.foreign": "scr_arr_get_foreign", "array.hasNamedKey": "scr_arr_has_named_key", "array.hasProperties": "scr_arr_has_properties", "array.undefinedKey": "scr_arr_undefined_key", @@ -1643,6 +1792,7 @@ export function mockTimerSym( export const USES_TIMERS_LIB_FNS = new Set([ "crypto.generateKeyPairCb", + "crypto.diffieHellman", "crypto.checkPrimeCb", "crypto.generatePrimeCb", "process.emitWarning", @@ -1668,11 +1818,18 @@ export const USES_TIMERS_LIB_FNS = new Set([ "url.legacyParse", "url.legacyFormat", "url.legacyResolve", + // The deprecated Buffer constructor reports DEP0005 on the next tick. + "buffer.deprecation", // The wrappers schedule warnings and callbacks on the tick queue. "util.deprecate", + "util.isArrayDeprecated", + "util.extend", "util.callbackify", "util.promisifyDyn", "util.debuglog", + // The wrapped calls arm timers and settle promises. + "util.debounce", + "util.throttle", "worker.new", "worker.parentPort", "worker.channel", @@ -1693,6 +1850,7 @@ export const USES_TIMERS_LIB_FNS = new Set([ "fetch.abortAny", "fetch.streamNew", "fetch.streamFrom", + "fetch.transformNew", "readable.new", // fs.ReadStream / fs.WriteStream open, read and write on the worker pool. "fs.createReadStream", @@ -1824,6 +1982,8 @@ export const USES_TIMERS_LIB_FNS = new Set([ "net.listenOptsReusePortCb", "net.connect", "net.connectCb", + "net.connectPath", + "net.connectPathCb", "net.connectLookup", "net.connectLookupDyn", "net.connectAttempt", diff --git a/packages/compiler/src/backend/llvm/lib-stream.ts b/packages/compiler/src/backend/llvm/lib-stream.ts index fdbf13d4f..53f455964 100644 --- a/packages/compiler/src/backend/llvm/lib-stream.ts +++ b/packages/compiler/src/backend/llvm/lib-stream.ts @@ -743,7 +743,7 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV const helpers = packed(5 + n + k, helperCount); let at = 5 + n + k + helperCount; const take = (present: boolean): string => (present ? args[at++]!.name : "null"); - const box = take(((flags >> 1) & 3) === 2); + const box = take(((flags >> 1) & 3) >= 2); const signal = take((flags & 32) !== 0); const abort = take((flags & 32) !== 0); const head = diff --git a/packages/compiler/src/backend/llvm/lib-tls.ts b/packages/compiler/src/backend/llvm/lib-tls.ts index 1d1cf659c..dbe492180 100644 --- a/packages/compiler/src/backend/llvm/lib-tls.ts +++ b/packages/compiler/src/backend/llvm/lib-tls.ts @@ -8,6 +8,7 @@ import { finishRuntimeCall, nullableReference, ptr, + caBytes, rawBytes, requestHandler, } from "./lib-abi.js"; @@ -110,6 +111,31 @@ export function emitTlsLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValu return finishRuntimeCall(host, e, "scr_tls_sock_get_cipher", a); case "tls.sockAlpnProtocol": return finishRuntimeCall(host, e, "scr_tls_sock_alpn_protocol", a); + case "tls.serverAddContext": + return finishRuntimeCall(host, e, "scr_tls_server_add_context", [...a, ptr()]); + case "tls.serverAddContextCtx": + return finishRuntimeCall(host, e, "scr_tls_server_add_context", [a[0]!, a[1]!, ptr(), a[2]!]); + case "tls.serverSetSecureContext": + return finishRuntimeCall(host, e, "scr_tls_server_set_secure_context", a); + case "tls.serverOnClientError": { + // (err, tlsSocket) listeners by arity: the adapter retains what the + // listener takes. + const cb = args[1]!; + const thunk = `scr_net_client_error_thunk${callbackType(cb).params.length}`; + return finishRuntimeCall(host, e, "scr_net_server_on_client_error", [ + a[0]!, + ...callbackAdapter(host, cb, thunk, ["ptr", "ptr", "ptr"]), + a[2]!, + ]); + } + case "tls.sockGetPeerCertificate": + return finishRuntimeCall(host, e, "scr_tls_sock_get_peer_certificate", a); + case "tls.sockGetCertificate": + return finishRuntimeCall(host, e, "scr_tls_sock_get_certificate", a); + case "tls.sockPeerCertDer": + return finishRuntimeCall(host, e, "scr_tls_sock_peer_cert_der", a); + case "tls.sockOwnCertDer": + return finishRuntimeCall(host, e, "scr_tls_sock_own_cert_der", a); case "tls.sockAuthError": { const result = callRuntime(host, "scr_tls_sock_auth_error", "ptr", a); return nullableReference(host, e.type, result, "string", "nullT"); @@ -159,7 +185,7 @@ export function emitTlsLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValu ); return finishRuntimeCall(host, e, "scr_https_request_agent", [ ...a.slice(0, 8), - ...rawBytes(host, args[8]!), + ...caBytes(host, args[8]!), a[9]!, ...handler, ]); @@ -179,7 +205,7 @@ export function emitTlsLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValu ["ptr", "ptr"], ); // Evaluate arguments once before selecting which transport consumes the callback. - const pem = rawBytes(host, args[9]!); + const pem = caBytes(host, args[9]!); const B = host.B, tls = B.newLabel("request.tls"), plain = B.newLabel("request.plain"), diff --git a/packages/compiler/src/backend/llvm/local-array-reads.ts b/packages/compiler/src/backend/llvm/local-array-reads.ts index 1c540295a..80ed2c440 100644 --- a/packages/compiler/src/backend/llvm/local-array-reads.ts +++ b/packages/compiler/src/backend/llvm/local-array-reads.ts @@ -797,7 +797,33 @@ export function emitLocalArrayRead( ); return null; }); - else if (inline) + else if ( + inline && + loopIndex !== null && + read.element.kind === "f64" && + host.provenDenseIndex(read.array, read.index) + ) { + // In a dense array loop's fast copy, the entry test proved the counter + // below the array's hole-free prefix: the slot holds a present value, + // and the read has no missing arm. + const dataPtr = B.tmp(), + data = B.tmp(), + valuePtr = B.tmp(), + value = B.tmp(), + width = int32 ? "i32" : "i64"; + B.line( + `${dataPtr} = getelementptr inbounds %ScrArr, ptr ${array.name}, i32 0, i32 ${SCR_ARR_FIELD.data}`, + ); + host.markMemoryPointer(dataPtr, "array:header"); + B.line(`${data} = load ptr, ptr ${dataPtr}${host.fieldAliasAttachment(dataPtr)}`); + B.line( + `${valuePtr} = getelementptr inbounds ${width}, ptr ${data}, ${host.sizeType} ${loopIndex}`, + ); + host.markMemoryPointer(valuePtr, "array:elements"); + B.line(`${value} = load ${width}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); + storeValue(value); + B.br(join); + } else if (inline) emitDenseReferenceArrayRead( host, array, diff --git a/packages/compiler/src/backend/llvm/local-union-storage.test.ts b/packages/compiler/src/backend/llvm/local-union-storage.test.ts index b8f44408d..81019cadc 100644 --- a/packages/compiler/src/backend/llvm/local-union-storage.test.ts +++ b/packages/compiler/src/backend/llvm/local-union-storage.test.ts @@ -113,7 +113,8 @@ test("conditional and reassigned locals own payload snapshots without heap wrapp expect(facts(mod).get("item")?.ownerType).toEqual(STRING); for (const bits of [32, 64] as const) { const ir = body(mod, bits); - expect(ir).toContain("call ptr @sc_map_entry_f64("); // inline number-key read + // The inline number-key read, on the integer view of the literal key. + expect(ir).toMatch(/call ptr @sc_map_entry_f64_int\(ptr %\S+, i64 \d+, double /); expect(ir).toContain("@scr_str_release"); expect(ir).not.toMatch(/@scr_union_(?:new|retain|release)/); expect(ir).toContain("store i64 0"); diff --git a/packages/compiler/src/backend/llvm/loop-array-borrows.ts b/packages/compiler/src/backend/llvm/loop-array-borrows.ts index 9335894d9..cc9e03a5c 100644 --- a/packages/compiler/src/backend/llvm/loop-array-borrows.ts +++ b/packages/compiler/src/backend/llvm/loop-array-borrows.ts @@ -1,5 +1,6 @@ import { F64, isRefCounted, type IrExpr, type IrFunction, type IrStmt } from "../../ir/ir.js"; import { everyStmtList, transformStmtList } from "../../ir/traverse.js"; +import { eachNodeOfKinds, type IrNodeIndex, type IrNodeKind } from "../../ir/node-index.js"; import type { CallLifetimes } from "./call-lifetimes.js"; import type { ReferenceEffects } from "./reference-effects.js"; import type { OptionalArrayReads } from "./local-array-reads.js"; @@ -57,6 +58,8 @@ export function dynHoleRead(value: IrExpr): (IrExpr & { kind: "arrayGet" }) | nu return same(cond.left.arr, then.arr) && same(cond.left.index, then.index) ? then : null; } +const LOOP_KINDS: readonly IrNodeKind[] = ["forOf", "for", "while", "doWhile"]; + /** Limit a borrow to a loop whose complete iteration preserves heap edges. * Initialization and unrelated work elsewhere in the function may mutate. * The array remains owned by its unchanged local or global binding; the @@ -72,6 +75,8 @@ export function findLoopArrayBorrows( optionalReads?: OptionalArrayReads, writes?: ReferenceWrites | null, functions?: ReadonlyMap, + /** The module's node index, read instead of walking for the loops. */ + nodes?: IrNodeIndex, ): ReadonlySet { const result = new Set(); if (fn.async || fn.generator) return result; @@ -88,9 +93,10 @@ export function findLoopArrayBorrows( (!local.boxed && !local.tdz && (bindings?.has(local.id) || parameters.has(local.id))) ); }; - everyStmtList(fn.body, { - expr: () => true, - stmt: (loop) => { + eachNodeOfKinds(fn, nodes?.scan(LOOP_KINDS), (node, statement) => { + if (!statement) return true; + const loop = node as IrStmt; + { if (loop.kind === "forOf") { const local = locals.get(loop.localId); if ( @@ -187,7 +193,7 @@ export function findLoopArrayBorrows( }, }); return true; - }, + } }); return result; } diff --git a/packages/compiler/src/backend/llvm/map-entry-fusion.test.ts b/packages/compiler/src/backend/llvm/map-entry-fusion.test.ts index 58d3a33d6..9c8ffb84c 100644 --- a/packages/compiler/src/backend/llvm/map-entry-fusion.test.ts +++ b/packages/compiler/src/backend/llvm/map-entry-fusion.test.ts @@ -137,12 +137,20 @@ test("a read followed by a write of the same map pairs, and the write reuses the } }); -test("number keys keep the write-after-read lookup instead of pairing", () => { - // findReadNumberMapWrites stores such writes into the found entry through - // a read-only probe that LLVM merges with the read's. - const mod = fixture((map, k) => [declare(map, k), set(map, k, increment(F64))], F64, F64); - expect(pairs(mod)).toBe(0); - expect(body(mod)).not.toContain("mapfuse.hit"); +test("number keys pair, and the write compares the key's bits with the read's", () => { + for (const value of [F64, BOOL]) { + const mod = fixture((map, k) => [declare(map, k), set(map, k, increment(value))], F64, value); + expect(pairs(mod)).toBe(1); + for (const bits of [32, 64] as const) { + const ir = body(mod, bits); + expect(ir).toContain("mapfuse.hit"); + expect(ir).toContain("alloca double ; fused map read: key"); + expect(ir).toMatch(/icmp eq i64 %t\d+, %t\d+/); + // The miss still inserts through the runtime. + expect(ir).toMatch(/call void @scr_map_set_f64_(?:f64|bool)\(/); + expect(ir).toContain("@sc_map_entry_f64("); + } + } }); test("the single-statement form pairs an unconditional nested read", () => { diff --git a/packages/compiler/src/backend/llvm/map-entry-fusion.ts b/packages/compiler/src/backend/llvm/map-entry-fusion.ts index bc2ca07f3..fe6f66d9c 100644 --- a/packages/compiler/src/backend/llvm/map-entry-fusion.ts +++ b/packages/compiler/src/backend/llvm/map-entry-fusion.ts @@ -14,10 +14,10 @@ * key match by identity implies the SameValueZero match the set would find. * * Only scalar values (number, boolean) fuse: overwriting them releases - * nothing, exactly like the runtime's own overwrite. Only string-keyed - * typed maps fuse here: a number-key write after a read of the same key - * already probes through the read-only entry lookup that LLVM merges with - * the read's (findReadNumberMapWrites, map-read-lifetimes.ts). + * nothing, exactly like the runtime's own overwrite. String-keyed and + * number-keyed typed maps fuse. A number key matches when its bits are the + * read's: such keys are SameValueZero-equal, and a key that is equal with + * other bits (-0 for 0) takes the ordinary set. */ import type { IrExpr, IrFunction, IrStmt } from "../../ir/ir.js"; import { everyExprChild, everyStmtChild } from "../../ir/traverse.js"; @@ -30,7 +30,7 @@ type MapOp = Extract; /** One fused read/write pair. The slots exist once the read emits through * an entry path; a write whose read took another path stays ordinary. */ export interface MapEntryPair { - slots: { map: string; key: string; entry: string } | null; + slots: { map: string; key: string; entry: string; keyType: "ptr" | "double" } | null; } export interface MapEntryFusion { @@ -38,13 +38,14 @@ export interface MapEntryFusion { writes: Map; } -/** A typed string-keyed Map read/write with a scalar value. */ +/** A typed string- or number-keyed Map read/write with a scalar value. */ function fusableOp(e: IrExpr, method: "get" | "set"): e is MapOp { if (e.kind !== "mapIntrinsic" || e.method !== method) return false; const receiver = e.receiver.type; - if (receiver.kind !== "map") return false; + if (receiver.kind !== "map" || receiver.key.kind === "union") return false; if (receiver.value.kind !== "f64" && receiver.value.kind !== "bool") return false; - if (mapKeyAccess(receiver.key) !== "str") return false; + const access = mapKeyAccess(receiver.key); + if (access !== "str" && access !== "f64") return false; return method === "get" ? borrowsMapReadInputs(e) : borrowsMapMutationReceiver(e); } @@ -219,23 +220,25 @@ export function findMapEntryFusions(fn: IrFunction): MapEntryFusion { return out; } -/** After a fused read resolved `entry` for the string `key` in `map`: park - * all three for the paired write. */ +/** After a fused read resolved `entry` for `key` in `map`: park all three + * for the paired write. `keyType` is the key's LLVM type: a string's ptr or + * a number's double. */ export function rememberMapEntry( host: LlvmEmitterContext, read: IrExpr, map: string, key: string, entry: string, + keyType: "ptr" | "double" = "ptr", ): void { const pair = host.mapEntryFusion?.reads.get(read); if (!pair) return; const B = host.B; if (pair.slots === null) { - const slots = { map: B.slot(), key: B.slot(), entry: B.slot() }; + const slots = { map: B.slot(), key: B.slot(), entry: B.slot(), keyType }; B.entryAllocas.push( `${slots.map} = alloca ptr ; fused map read: map`, - `${slots.key} = alloca ptr ; fused map read: key`, + `${slots.key} = alloca ${keyType} ; fused map read: key`, `${slots.entry} = alloca ptr ; fused map read: entry`, `store ptr null, ptr ${slots.entry}`, ); @@ -243,7 +246,7 @@ export function rememberMapEntry( } const slots = pair.slots; B.line(`store ptr ${map}, ptr ${slots.map}`); - B.line(`store ptr ${key}, ptr ${slots.key}`); + B.line(`store ${slots.keyType} ${key}, ptr ${slots.key}`); B.line(`store ptr ${entry}, ptr ${slots.entry}`); } @@ -270,13 +273,20 @@ export function emitFusedMapSet( both = B.tmp(), reuse = B.tmp(); B.line(`${parkedMap} = load ptr, ptr ${slots.map}`); - B.line(`${parkedKey} = load ptr, ptr ${slots.key}`); + B.line(`${parkedKey} = load ${slots.keyType}, ptr ${slots.key}`); B.line(`${entry} = load ptr, ptr ${slots.entry}`); // The entry is valid only between its read and this write. B.line(`store ptr null, ptr ${slots.entry}`); B.line(`${found} = icmp ne ptr ${entry}, null`); B.line(`${sameMap} = icmp eq ptr ${parkedMap}, ${map}`); - B.line(`${sameKey} = icmp eq ptr ${parkedKey}, ${key}`); + if (slots.keyType === "ptr") B.line(`${sameKey} = icmp eq ptr ${parkedKey}, ${key}`); + else { + const parkedBits = B.tmp(), + keyBits = B.tmp(); + B.line(`${parkedBits} = bitcast double ${parkedKey} to i64`); + B.line(`${keyBits} = bitcast double ${key} to i64`); + B.line(`${sameKey} = icmp eq i64 ${parkedBits}, ${keyBits}`); + } B.line(`${both} = and i1 ${sameMap}, ${sameKey}`); B.line(`${reuse} = select i1 ${found}, i1 ${both}, i1 false`); const hit = B.newLabel("mapfuse.hit"), diff --git a/packages/compiler/src/backend/llvm/map-number-lookup.test.ts b/packages/compiler/src/backend/llvm/map-number-lookup.test.ts index 989b8b3b0..f7bd87af1 100644 --- a/packages/compiler/src/backend/llvm/map-number-lookup.test.ts +++ b/packages/compiler/src/backend/llvm/map-number-lookup.test.ts @@ -118,7 +118,7 @@ test("string keys and reference values are retained inline, boolean slots compar }); /** work(map, k): map.has(k) on a number-keyed map. */ -function lookupFixture(): IrModule { +function lookupFixture(key: IrExpr = ref("k", F64)): IrModule { const map: IrType = { kind: "map", key: F64, value: F64 }; const params = [ { localId: "map", name: "map", type: map }, @@ -128,7 +128,7 @@ function lookupFixture(): IrModule { kind: "mapIntrinsic", method: "has", receiver: ref("map", map), - args: [ref("k", F64)], + args: [key], type: BOOL, loc, }; @@ -168,8 +168,10 @@ test("hashed number-key lookups probe inline and call the runtime only for wide expect(hashed).toContain("select i1 %nan, i64 9221120237041090560, i64 %kzb"); expect(hashed).toContain("mul i64 %x1, -4658895280553007687"); expect(hashed).toContain("mul i64 %x3, -7723592293110705685"); - // Filter (field 10), small-map scan of the entry count (field 8), and - // compact buckets (fields 13 and 14) tagged with the hash's top byte. + // Filter (field 10) before the small-map scan of the entry count (field + // 8) only, and compact buckets (fields 13 and 14) tagged with the hash's + // top byte. + expect(hashed).toContain("br i1 %linear, label %filter, label %buckets"); expect(hashed).toContain("getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 10"); expect(hashed).toContain(`load i32, ptr %nep`); expect(hashed).toContain(`load i32, ptr %nbp`); @@ -182,6 +184,91 @@ test("hashed number-key lookups probe inline and call the runtime only for wide } }); +test("a key with an integer view tests the direct index on the integer", () => { + for (const bits of [32, 64] as const) { + const sz = bits === 64 ? "i64" : "i32"; + const integral = emitted(lookupFixture({ kind: "numLit", value: 12, type: F64, loc }), bits); + expect(integral.work).toMatch(/call ptr @sc_map_entry_f64_int\(ptr %\S+, i64 12, double /); + const helper = + /^define internal ptr @sc_map_entry_f64_int\(ptr %m, i64 %ki, double %k\)[^]*?^}/m.exec( + integral.module, + )![0]; + // An unsigned compare also rejects negative keys; the hashed probe takes + // the integer. + expect(helper).toContain("icmp ult i64 %ki, %nd"); + expect(helper).toContain(`getelementptr inbounds i32, ptr %dense, ${sz} %i`); + expect(helper).toContain("call ptr @sc_map_entry_f64_hashed_int(ptr %m, i64 %ki)"); + expect(helper).not.toContain("fptosi"); + // Its key word is the integer's double, which needs no NaN or -0 + // normalization; the runtime lookup gets that double. + const hashed = + /^define internal ptr @sc_map_entry_f64_hashed_int\(ptr %m, i64 %ki\) noinline [^]*?^}/m.exec( + integral.module, + )![0]; + expect(hashed).toContain("sitofp i64 %ki to double"); + expect(hashed).not.toContain("fcmp"); + expect(hashed).toContain("mul i64 %x1, -4658895280553007687"); + expect(hashed).toContain( + "call ptr @scr_map_entry_f64(ptr %m, double %kf) memory(read) nounwind willreturn", + ); + // A fractional key and a key without a proof keep the double lookup. + for (const key of [{ kind: "numLit", value: 0.5, type: F64, loc } as IrExpr, ref("k", F64)]) { + const { work } = emitted(lookupFixture(key), bits); + expect(work).toContain("call ptr @sc_map_entry_f64("); + expect(work).not.toContain("@sc_map_entry_f64_int("); + } + } +}); + +test("a key view converted in a branch is not reused by a later lookup of the same constant", () => { + // work(map, k, taken): if (taken) return map.has((k = 2 ** 40)); return map.has(2 ** 40). + const wide = 2 ** 40; + const mod = lookupFixture(); + const work = mod.functions[1]!; + const map = work.params[0]!.type; + const has = (key: IrExpr): IrExpr => ({ + kind: "mapIntrinsic", + method: "has", + receiver: ref("map", map), + args: [key], + type: BOOL, + loc, + }); + const literal: IrExpr = { kind: "numLit", value: wide, type: F64, loc }; + const taken = { localId: "taken", name: "taken", type: BOOL }; + work.params.push(taken); + work.locals.push({ id: "taken", name: "taken", type: BOOL, mutable: true }); + work.body = [ + { + kind: "if", + cond: ref("taken", BOOL), + then: [ + { + kind: "return", + value: has({ + kind: "assignExpr", + localId: "k", + value: literal, + type: F64, + loc, + }), + loc, + }, + ], + else_: null, + loc, + }, + { kind: "return", value: has(literal), loc }, + ]; + for (const bits of [32, 64] as const) { + const { work: text } = emitted(mod, bits); + // The lookup after the branch takes the literal, not a conversion made + // inside the branch. + expect(text).toMatch(/call ptr @sc_map_entry_f64_int\(ptr %\S+, i64 1099511627776, double /); + expect(text).not.toMatch(/@sc_map_entry_f64_int\(ptr %\S+, i64 %/); + } +}); + /** work(map): map.size, and keep(map): map returned as an owned value. */ function sizeFixture(): IrModule { const map: IrType = { kind: "map", key: F64, value: F64 }; diff --git a/packages/compiler/src/backend/llvm/map-number-lookup.ts b/packages/compiler/src/backend/llvm/map-number-lookup.ts index 6d219b50a..17a1563c3 100644 --- a/packages/compiler/src/backend/llvm/map-number-lookup.ts +++ b/packages/compiler/src/backend/llvm/map-number-lookup.ts @@ -4,9 +4,10 @@ * direct index in the runtime (scr_map.c). Reads go through the internal * sc_map_entry_f64 helper (emitter.ts helperDefs), which LLVM inlines: it * resolves directly indexed maps without a call and other maps through - * the internal sc_map_entry_f64_hashed probe, which hashes the key as the - * runtime does and defers to scr_map_entry_f64 only for wide bucket - * tables. Both only read memory. The generated code then reads the entry's + * the internal sc_map_entry_f64_hashed probe (sc_map_entry_f64_hashed_int + * for a key with an integer view), which hashes the key as the runtime + * does and defers to scr_map_entry_f64 only for wide bucket tables. Both + * only read memory. The generated code then reads the entry's * value slot itself and retains a reference value with the value type's own * retain, which is the adapter the collection was constructed with. * String-keyed reads use the same entry contract through the internal @@ -28,16 +29,44 @@ * guard excludes. for-of starts at scr_map_iter_start, past a deleted * prefix. */ -import type { IrType } from "../../ir/ir.js"; -import type { LlvmEmitterContext } from "./expr-context.js"; +import type { IrExpr, IrType } from "../../ir/ir.js"; +import type { LlValue, LlvmEmitterContext } from "./expr-context.js"; +import { exactInteger, widenInteger } from "./integer-values.js"; import { mapKeyAccess } from "./shapes.js"; export const NUMBER_MAP_ENTRY_DECL = "declare ptr @scr_map_entry_f64(ptr, double)"; +/** Record the integer view of a number Map/Set key operand for the entry + * lookups of that key, when the view needs no instruction of its own: the + * value's integer or uint32 view, defined with the value, or an integral + * literal. A view exactInteger converts at this lookup is not recorded: the + * key cache spans the function, and a later lookup of the same operand (a + * constant, say) can be in a block the conversion does not dominate. */ +export function noteNumberMapKey(host: LlvmEmitterContext, key: LlValue, expr: IrExpr): void { + if (key.type.kind !== "f64" || host.numberMapKeyIntegers.has(key.name)) return; + if (!key.integer && key.uint32 === undefined && expr.kind !== "numLit") return; + const integer = exactInteger(host, key, expr); + if (!integer) return; + const defined = + integer.name === key.integer?.name || + integer.name === key.uint32 || + /^-?\d+$/.test(integer.name); + if (defined) host.numberMapKeyIntegers.set(key.name, integer); +} -/** The entry for `key`, or null when absent. Valid until the next mutation. */ +/** The entry for `key`, or null when absent. Valid until the next mutation. + * A key with an integer view (noteNumberMapKey) tests the direct index on + * that integer instead of converting the double back. */ export function emitNumberMapEntry(host: LlvmEmitterContext, map: string, key: string): string { host.declare(NUMBER_MAP_ENTRY_DECL); const entry = host.B.tmp(); + const integer = host.numberMapKeyIntegers.get(key); + if (integer) { + const wide = widenInteger(host, integer); + host.B.line( + `${entry} = call ptr @sc_map_entry_f64_int(ptr ${map}, i64 ${wide}, double ${key})`, + ); + return entry; + } host.B.line(`${entry} = call ptr @sc_map_entry_f64(ptr ${map}, double ${key})`); return entry; } @@ -122,14 +151,16 @@ export function emitStringMapGetScalar( } /** Map.get of a reference value: a +1 value, or null when absent — the - * contract of scr_map_get_f64_ref. */ + * contract of scr_map_get_f64_ref. `borrow`: the entry's own reference, + * without a retain, for a reader that finishes before the map can change. */ export function emitNumberMapGetRef( host: LlvmEmitterContext, map: string, key: string, value: IrType, + borrow = false, ): string { - return emitMapEntryGetRef(host, emitNumberMapEntry(host, map, key), value); + return emitMapEntryGetRef(host, emitNumberMapEntry(host, map, key), value, false, borrow); } /** Map.get of a reference value under a string key, with scr_map_get_str_ref's @@ -141,16 +172,25 @@ export function emitStringMapGetRef( key: string, value: IrType, compact = false, + borrow = false, ): string { - return emitMapEntryGetRef(host, emitStringMapEntry(host, map, key, compact), value, compact); + return emitMapEntryGetRef( + host, + emitStringMapEntry(host, map, key, compact), + value, + compact, + borrow, + ); } -/** The +1 value of a found entry, or null for a missing (null) entry. */ +/** The +1 value of a found entry (its own reference when `borrow`), or + * null for a missing (null) entry. */ function emitMapEntryGetRef( host: LlvmEmitterContext, entry: string, value: IrType, compact = false, + borrow = false, ): string { const B = host.B; const found = B.tmp(); @@ -160,10 +200,8 @@ function emitMapEntryGetRef( join = B.newLabel("mapn.join"); B.condBr(found, hit, miss); B.startBlock(hit); - const retained = host.retainFrame( - compact ? emitCompactMapSlot(host, entry) : emitNumberMapSlot(host, entry, "ptr"), - value, - ); + const loaded = compact ? emitCompactMapSlot(host, entry) : emitNumberMapSlot(host, entry, "ptr"); + const retained = borrow ? loaded : host.retainFrame(loaded, value); B.br(join); B.startBlock(miss); B.br(join); diff --git a/packages/compiler/src/backend/llvm/map-read-lifetimes.test.ts b/packages/compiler/src/backend/llvm/map-read-lifetimes.test.ts index 3725db060..d91e2b211 100644 --- a/packages/compiler/src/backend/llvm/map-read-lifetimes.test.ts +++ b/packages/compiler/src/backend/llvm/map-read-lifetimes.test.ts @@ -120,7 +120,12 @@ test("local map results keep scalar and owned reference payloads without heap wr expect(ir).toContain("call ptr @sc_map_entry_str("); expect(ir).not.toContain("@scr_map_get_"); } - if (value === STRING) expect(ir).toContain("@scr_str_release"); + // Nothing can change the map before the tag test, so a reference + // result is the entry's own (findMapReadBorrows): no retain or release. + if (value === STRING) { + expect(ir).not.toContain("@scr_str_retain"); + expect(ir).not.toContain("@scr_str_release"); + } } } }); @@ -132,7 +137,8 @@ test("nullable record results are the looked-up pointer itself on both ABIs", () expect(ir).not.toContain("%ScrUnion"); expect(ir).not.toContain("@scr_union_"); expect(ir).toMatch(/icmp (?:eq|ne) ptr %\w+, null/); - expect(ir).toContain("@sc_rrelease_"); + // The pointer is the entry's own reference until the tag test. + expect(ir).not.toContain("@sc_rrelease_"); } }); diff --git a/packages/compiler/src/backend/llvm/map-read-lifetimes.ts b/packages/compiler/src/backend/llvm/map-read-lifetimes.ts index 6b58d314a..fbb21dcd2 100644 --- a/packages/compiler/src/backend/llvm/map-read-lifetimes.ts +++ b/packages/compiler/src/backend/llvm/map-read-lifetimes.ts @@ -9,14 +9,21 @@ import { } from "../../ir/ir.js"; import { everyExpr, everyStmt, everyStmtList } from "../../ir/traverse.js"; import type { CallLifetimes } from "./call-lifetimes.js"; +import type { ReferenceEffects } from "./reference-effects.js"; import type { LlValue, LlvmEmitterContext } from "./expr-context.js"; import { + emitCompactMapSlot, emitNumberMapGetRef, emitNumberMapGetScalar, + emitNumberMapSlot, + emitStringMapEntry, emitStringMapGetScalar, emitStringMapGetRef, compactMapValues, + noteNumberMapKey, } from "./map-number-lookup.js"; +import { InternalCompilerError } from "../../errors.js"; +import { undefinedArmTag } from "../../ir/analysis.js"; import { mapKeyAccess, mapKeyParamType } from "./shapes.js"; import { rememberMapEntry } from "./map-entry-fusion.js"; @@ -68,6 +75,10 @@ export function borrowsMapMutationReceiver(expr: IrExpr): boolean { export interface LocalMapRead { /** The Map.get expression itself. */ expr: IrExpr; + /** A local's reference result may stay the entry's own reference: every + * use of the local runs before anything that can change a reference + * edge (findMapReadBorrows), so the entry still holds the value. */ + borrowed?: boolean; type: IrType; receiver: IrExpr; key: IrExpr; @@ -126,6 +137,7 @@ export function findMapReadLifetimes( fn: IrFunction, unions: ReadonlyMap, lifetimes: CallLifetimes, + effects?: ReferenceEffects, ): MapReadLifetimes { const result: MapReadLifetimes = { locals: new Map(), arguments: new Map() }; if (fn.async || fn.generator) return result; @@ -150,6 +162,125 @@ export function findMapReadLifetimes( return true; }, }); + if (effects) { + const borrowed = findMapReadBorrows(fn, effects, (decl) => { + const read = result.locals.get(decl.localId); + return read !== undefined && isRefCounted(read.value); + }); + for (const id of borrowed) result.locals.get(id)!.borrowed = true; + } + return result; +} + +/** Whether a map read's receiver keeps the map past its own statement: a + * local or a field of one. Changing either is an edge change the borrow + * proof sees. A temporary map (a call's result, `new Map()`) is released at + * the end of the declaration, and its entries with it. */ +function heldReceiver(expr: IrExpr): boolean { + if (expr.kind === "varRef") return true; + if (expr.kind === "fieldGet" || expr.kind === "recordGet") return heldReceiver(expr.obj); + if (expr.kind === "unionNarrow" || expr.kind === "downcast") return heldReceiver(expr.value); + return false; +} + +/** The locals, declared by statements `candidate` accepts, that can hold a + * map entry's own reference instead of a +1 copy. The statements after the + * declaration run in order, and every one that names the local must + * preserve reference edges (ReferenceEffects) and run before any statement + * that may not, so no write, deletion or unknown call can release the value + * while the local is read. An `if` passes its condition's state to both + * branches and joins them; any other statement that names the local is + * judged whole. A later pass of an enclosing loop declares the local again, + * so only the rest of its own statement list matters. The map must be held + * by a local or a field (heldReceiver), the read itself must preserve + * reference edges (a key whose evaluation replaces the receiver's field + * leaves the read on a map the statement releases), and locals declared + * from the local count as its uses. An update such as + * `const c = m.get(k); if (c === undefined) m.set(k, v); else c.n++` keeps + * the borrow: the write runs only on the branch that does not read `c`. */ +export function findMapReadBorrows( + fn: IrFunction, + effects: ReferenceEffects, + candidate: (decl: IrStmt & { kind: "varDecl" }) => boolean, +): Set { + const result = new Set(); + if (fn.async || fn.generator) return result; + // `ids` holds the local and the locals declared from it: a stable alias + // (`const d = c`) reads the same entry without a reference of its own. + const names = (node: IrStmt | IrExpr, ids: ReadonlySet, isStmt: boolean): boolean => { + let found = false; + const visitor = { + stmt: () => !found, + expr: (expr: IrExpr) => { + if (expr.kind === "varRef" && ids.has(expr.localId)) found = true; + return !found; + }, + }; + if (isStmt) everyStmt(node as IrStmt, visitor); + else everyExpr(node as IrExpr, visitor); + return found; + }; + // Adds the reference locals that `stmt` declares from one in `ids`, at any + // depth: a `var` inside a loop or a try stays in scope after it. + const alias = (stmt: IrStmt, ids: Set): void => { + everyStmt(stmt, { + stmt: (node) => { + if ( + node.kind === "varDecl" && + node.init !== null && + isRefCounted(node.init.type) && + names(node.init, ids, false) + ) + ids.add(node.localId); + return true; + }, + expr: () => true, + }); + }; + // Whether an edge may have changed after `list`, or null when a statement + // names the local after such a change (or changes edges itself). + const walk = (list: readonly IrStmt[], ids: Set, changed: boolean): boolean | null => { + for (const stmt of list) { + if (stmt.kind === "if") { + const preserving = effects.preserves(stmt.cond); + if (names(stmt.cond, ids, false) && (changed || !preserving)) return null; + const condition = changed || !preserving; + const then = walk(stmt.then, ids, condition); + const otherwise = stmt.else_ === null ? condition : walk(stmt.else_, ids, condition); + if (then === null || otherwise === null) return null; + changed = then || otherwise; + continue; + } + if (stmt.kind === "block") { + const inner = walk(stmt.body, ids, changed); + if (inner === null) return null; + changed = inner; + continue; + } + const preserving = effects.preservesScope([stmt]); + if (names(stmt, ids, true)) { + if (changed || !preserving) return null; + alias(stmt, ids); + } + changed ||= !preserving; + } + return changed; + }; + const visit = (list: readonly IrStmt[]): void => { + list.forEach((stmt, index) => { + if ( + stmt.kind === "varDecl" && + stmt.init?.kind === "mapIntrinsic" && + heldReceiver(stmt.init.receiver) && + effects.preserves(stmt.init) && + candidate(stmt) && + walk(list.slice(index + 1), new Set([stmt.localId]), false) !== null + ) + result.add(stmt.localId); + for (const nested of nestedStmtLists(stmt)) visit(nested); + }); + }; + visit(fn.body); return result; } @@ -265,6 +396,7 @@ export function emitMapLookupKey( const value = borrow ? host.emitReadReceiver(key) : host.emitExpr(key); const access = mapKeyAccess(key.type); const type = mapKeyParamType(access); + if (access === "f64") noteNumberMapKey(host, value, key); return { access, types: type, args: `${type} ${value.name}`, value: value.name }; } @@ -303,17 +435,19 @@ export function emitStackMapRead(host: LlvmEmitterContext, read: LocalMapRead): let owner: StackMapRead["owner"] = null; const number = inlineNumberKey(key); const string = entryStringKey(key); + const borrow = read.borrowed === true; if (number !== null) { if (isRefCounted(read.value)) { - const raw = emitNumberMapGetRef(host, receiver.name, number, read.value); + const raw = emitNumberMapGetRef(host, receiver.name, number, read.value, borrow); B.line(`${found} = icmp ne ptr ${raw}, null`); B.line(`store i64 0, ptr ${payload}`); B.line(`store ptr ${raw}, ptr ${payload}`); - owner = { slot: payload, type: read.value }; + if (!borrow) owner = { slot: payload, type: read.value }; } else { // Booleans are stored as 0/1, already the widened union payload. const scalar = emitNumberMapGetScalar(host, receiver.name, number); found = scalar.found; + rememberMapEntry(host, read.expr, receiver.name, number, scalar.entry, "double"); B.line(`store i64 ${scalar.bits}, ptr ${payload}`); } } else if (string !== null && !isRefCounted(read.value)) { @@ -326,8 +460,10 @@ export function emitStackMapRead(host: LlvmEmitterContext, read: LocalMapRead): let raw: string; const mapType = read.receiver.type; const compact = mapType.kind === "map" && compactMapValues(host, mapType.key, mapType.value); + // The runtime's other key kinds return +1, which the frame then owns. + const borrowed = borrow && stringKey !== null; if (stringKey !== null) - raw = emitStringMapGetRef(host, receiver.name, stringKey, read.value, compact); + raw = emitStringMapGetRef(host, receiver.name, stringKey, read.value, compact, borrowed); else { raw = B.tmp(); host.declare(`declare ptr @scr_map_get_${access}_ref(ptr, ${keyType})`); @@ -336,7 +472,7 @@ export function emitStackMapRead(host: LlvmEmitterContext, read: LocalMapRead): B.line(`${found} = icmp ne ptr ${raw}, null`); B.line(`store i64 0, ptr ${payload}`); B.line(`store ptr ${raw}, ptr ${payload}`); - owner = { slot: payload, type: read.value }; + if (!borrowed) owner = { slot: payload, type: read.value }; } else { const scalar = read.value.kind === "f64" ? "f64" : "bool"; B.line(`store i64 0, ptr ${payload}`); @@ -359,3 +495,62 @@ export function emitStackMapRead(host: LlvmEmitterContext, read: LocalMapRead): B.line(`store i32 ${selected}, ptr ${tag}`); return { value: { name: box, type: read.type }, owner }; } + +/** A string-keyed Map.get whose result is a nullable-pointer union over the + * map's own value type (`Map` read as `F | undefined`): a hit is + * the stored pointer itself and a miss the immortal undefined encoding, so + * the read can lend the stored value (emitBorrowedMapGet). */ +export function pointerMapGet(host: LlvmEmitterContext, e: IrExpr): boolean { + if ( + e.kind !== "mapIntrinsic" || + e.method !== "get" || + e.args.length !== 1 || + e.type.kind !== "union" || + e.receiver.type.kind !== "map" || + e.receiver.type.key.kind !== "string" || + !borrowsMapReadInputs(e) + ) + return false; + const nullable = host.nullableUnions.single(e.type.unionId); + return ( + nullable !== null && + typeEquals(nullable.arm, e.receiver.type.value) && + undefinedArmTag(e.type, host.unionsById) >= 0 + ); +} + +/** A pointerMapGet read without a reference: the map (and through it the + * stored value) must stay unchanged and alive until the consumer is done + * with the pointer (canBorrowReceiver). */ +export function emitBorrowedMapGet( + host: LlvmEmitterContext, + e: IrExpr & { kind: "mapIntrinsic" }, +): LlValue { + const B = host.B; + const mapType = e.receiver.type; + if (e.type.kind !== "union" || mapType.kind !== "map") + throw new InternalCompilerError("llvm emitter bug: borrowed Map.get shape"); + const receiver = host.emitReadReceiver(e.receiver); + const keyExpr = e.args[0]!; + const key = host.canBorrowReceiver(keyExpr) + ? host.emitReadReceiver(keyExpr) + : host.emitExpr(keyExpr); + const compact = compactMapValues(host, mapType.key, mapType.value); + const entry = emitStringMapEntry(host, receiver.name, key.name, compact); + const found = B.tmp(); + B.line(`${found} = icmp ne ptr ${entry}, null`); + const hit = B.newLabel("mapb.hit"), + miss = B.newLabel("mapb.miss"), + join = B.newLabel("mapb.join"); + B.condBr(found, hit, miss); + B.startBlock(hit); + const value = compact ? emitCompactMapSlot(host, entry) : emitNumberMapSlot(host, entry, "ptr"); + B.br(join); + B.startBlock(miss); + B.br(join); + B.startBlock(join); + const absent = host.unitInstanceRef(e.type.unionId, undefinedArmTag(e.type, host.unionsById)); + const result = B.tmp(); + B.line(`${result} = phi ptr [ ${value}, %${hit} ], [ ${absent}, %${miss} ]`); + return { name: result, type: e.type }; +} diff --git a/packages/compiler/src/backend/llvm/nullable-unions.test.ts b/packages/compiler/src/backend/llvm/nullable-unions.test.ts index 47278453e..f4e6a6866 100644 --- a/packages/compiler/src/backend/llvm/nullable-unions.test.ts +++ b/packages/compiler/src/backend/llvm/nullable-unions.test.ts @@ -7,7 +7,7 @@ import { type IrType, type IrUnionDef, } from "../../ir/ir.js"; -import { NullableUnions } from "./nullable-unions.js"; +import { isObjectArm, isPointerIdentityArm, NullableUnions } from "./nullable-unions.js"; const loc = { file: "nullable-unions.ts", start: 0, end: 0 }; const a: IrType = { kind: "record", shapeId: "a" }; @@ -56,10 +56,11 @@ test("unions of two or more records become shape unions", () => { }); test("records of runtime-inspected unions keep every union over them boxed", () => { - const set: IrType = { kind: "set", elem: { kind: "union", unionId: "ab" } }; + const set: IrType = { kind: "set", elem: { kind: "union", unionId: "abf" } }; const unions = new NullableUnions( module( [ + { id: "abf", arms: [a, b, F64] }, { id: "ab", arms: [a, b] }, { id: "abu", arms: [a, b, UNDEFINED_T] }, { id: "bc", arms: [b, c] }, @@ -68,6 +69,7 @@ test("records of runtime-inspected unions keep every union over them boxed", () { globals: [{ id: "%g.s", name: "s", type: set, mutable: false }] }, ), ); + expect(unions.get("abf")).toBeNull(); expect(unions.get("ab")).toBeNull(); expect(unions.get("abu")).toBeNull(); expect(unions.get("bc")).toBeNull(); @@ -75,9 +77,56 @@ test("records of runtime-inspected unions keep every union over them boxed", () expect(unions.shapeNumber("c")).toBe(0); }); +test("unions of records alone stay shape unions as Map keys and Set elements", () => { + const set: IrType = { kind: "set", elem: { kind: "union", unionId: "ab" } }; + const map: IrType = { kind: "map", key: { kind: "union", unionId: "abc" }, value: F64 }; + const unions = new NullableUnions( + module( + [ + { id: "ab", arms: [a, b] }, + { id: "abc", arms: [a, b, c] }, + { id: "ba", arms: [b, a] }, + { id: "abu", arms: [a, b, UNDEFINED_T] }, + ], + { + globals: [ + { id: "%g.s", name: "s", type: set, mutable: false }, + { id: "%g.m", name: "m", type: map, mutable: false }, + ], + }, + ), + ); + expect(unions.shapes("ab")).not.toBeNull(); + expect(unions.shapes("abc")).not.toBeNull(); + expect(unions.shapes("abu")).not.toBeNull(); + // Unions over the same records share one release and trace helper. + expect(unions.helperOwner("ba")).toBe("ab"); + expect(unions.helperOwner("abu")).toBe("ab"); + expect(unions.helperOwner("abc")).toBe("abc"); +}); + test("library, lifetime-region and debug builds keep record unions boxed", () => { const unions = [{ id: "ab", arms: [a, b] }]; expect(new NullableUnions(module(unions), false).get("ab")).toBeNull(); expect(new NullableUnions(module(unions, { lifetimeRegions: true })).get("ab")).toBeNull(); expect(new NullableUnions(module(unions)).shapes("ab")).not.toBeNull(); }); + +test("function arms are nullable pointers compared by identity root", () => { + const fn: IrType = { kind: "func", params: [F64], ret: F64 }; + const unions = new NullableUnions( + module([ + { id: "fu", arms: [fn, UNDEFINED_T] }, + { id: "fnu", arms: [NULL_T, fn, UNDEFINED_T] }, + { id: "ff", arms: [fn, F64] }, + ]), + ); + expect(unions.single("fu")).toMatchObject({ refTag: 0, unitTag: 1, nullTag: -1, arm: fn }); + expect(unions.single("fnu")).toMatchObject({ refTag: 1, unitTag: 2, nullTag: 0 }); + expect(unions.get("ff")).toBeNull(); + const single = unions.single("fu")!; + // Always truthy, but `===` compares adapters by the function they adapt. + expect(isObjectArm(single)).toBe(true); + expect(isPointerIdentityArm(single)).toBe(false); + expect(unions.armTypes().get("fu")).toEqual(fn); +}); diff --git a/packages/compiler/src/backend/llvm/nullable-unions.ts b/packages/compiler/src/backend/llvm/nullable-unions.ts index d9b9cdfcf..eedb922b3 100644 --- a/packages/compiler/src/backend/llvm/nullable-unions.ts +++ b/packages/compiler/src/backend/llvm/nullable-unions.ts @@ -89,21 +89,30 @@ export const NULLABLE_ABSENT = "@sc_nullable_absent"; /** The reference arms whose instances are never NULL. Class and record * entry points all tolerate NULL; strings, Stats snapshots, arrays, maps and - * sets retain through a NULL-skipping wrapper (shapes.ts). Object arms are - * always truthy and compare by identity; string arms are truthy when - * non-empty and compare bytes. */ + * sets retain through a NULL-skipping wrapper (shapes.ts), as do closures + * (function arms). Object arms are always truthy and compare by identity + * (function arms by identity root); string arms are truthy when non-empty + * and compare bytes. */ export type NullableRefArm = | { kind: "object"; className: string } | { kind: "record"; shapeId: string } | { kind: "string" } | { kind: "stats" } - | (IrType & { kind: "array" | "map" | "set" }); + | (IrType & { kind: "array" | "map" | "set" | "func" }); -/** The arm is a JS object: always truthy, compared by identity. */ +/** The arm is a JS object: always truthy. */ export function isObjectArm(nullable: PointerUnion): boolean { return nullable.repr === "shapes" || nullable.arm.kind !== "string"; } +/** The arm compares by pointer identity. Function arms compare identity + * roots instead: a signature adapter is the function it adapts. */ +export function isPointerIdentityArm(nullable: PointerUnion): boolean { + return ( + nullable.repr === "shapes" || (nullable.arm.kind !== "string" && nullable.arm.kind !== "func") + ); +} + /** The representation decision for every union of one module. It is a * pure function of the module, so serialized-IR and in-memory emission * agree. */ @@ -118,6 +127,9 @@ export class NullableUnions { readonly retainWrappers = new Map(); /** Some nullable union encodes `null` with NULLABLE_NULL. */ readonly usesNullSentinel: boolean = false; + /** The shape union whose release and trace helpers each shape union + * uses: the first one over the same record arms. */ + private readonly helperOwners = new Map(); /** `shapes`: allow shape unions (debug builds keep boxes, whose * debug-info layouts describe every arm). */ @@ -151,6 +163,24 @@ export class NullableUnions { if (!this.shapeNumbers.has(arm.shapeId)) this.shapeNumbers.set(arm.shapeId, this.shapeNumbers.size + 2); } + // Release and trace dispatch on the shape word, which names a record + // shape module-wide, so unions over the same records share them. + const owners = new Map(); + for (const nullable of this.byId.values()) { + if (nullable.repr !== "shapes") continue; + const key = nullable.arms + .map((arm) => arm.shapeId) + .sort() + .join(" "); + const owner = owners.get(key) ?? nullable.unionId; + owners.set(key, owner); + this.helperOwners.set(nullable.unionId, owner); + } + } + + /** The shape union whose release and trace helpers this one uses. */ + helperOwner(unionId: string): string { + return this.helperOwners.get(unionId) ?? unionId; } get(unionId: string): PointerUnion | null { @@ -243,7 +273,8 @@ function nullableShape( return { ...base, arm: { kind: "record", shapeId: arm.shapeId } }; if (arm.kind === "string") return { ...base, arm: { kind: "string" } }; if (arm.kind === "stats") return { ...base, arm: { kind: "stats" } }; - if (arm.kind === "array" || arm.kind === "map" || arm.kind === "set") return { ...base, arm }; + if (arm.kind === "array" || arm.kind === "map" || arm.kind === "set" || arm.kind === "func") + return { ...base, arm }; return null; } @@ -277,13 +308,19 @@ function shapeUnion( /** Unions the runtime itself inspects as tagged boxes keep that * representation: Map keys and Set elements hash and compare union keys by - * tag and payload inside the C map. */ + * tag and payload inside the C map. A union of records alone is the + * exception: its keys compare by record identity, which a shape union's + * record pointer carries as well (the map keys it by reference). */ function boxedUnionIds(mod: IrModule, nodes?: IrNodeIndex): Set { const boxed = new Set(); // Only the module's union definitions consult the result. if (!mod.unions?.length) return boxed; + const recordsOnly = new Set(); + for (const def of mod.unions) + if (def.arms.length >= 2 && def.arms.every((arm) => arm.kind === "record")) + recordsOnly.add(def.id); const mark = (t: IrType): void => { - if (t.kind === "union") boxed.add(t.unionId); + if (t.kind === "union" && !recordsOnly.has(t.unionId)) boxed.add(t.unionId); }; if (nodes !== undefined) { for (const t of nodes.types) { diff --git a/packages/compiler/src/backend/llvm/periodic-guards.test.ts b/packages/compiler/src/backend/llvm/periodic-guards.test.ts new file mode 100644 index 000000000..d1df4e7ab --- /dev/null +++ b/packages/compiler/src/backend/llvm/periodic-guards.test.ts @@ -0,0 +1,80 @@ +import { expect, test } from "vitest"; +import { countedFor } from "../../ir/build.js"; +import { BOOL, F64, type IrExpr, type IrStmt } from "../../ir/ir.js"; +import { + loopUnrollAttachment, + loopUnrollMetadata, + periodicGuardPeriod, +} from "./periodic-guards.js"; + +const loc = { file: "guards.ts", start: 0, end: 0 }; +const ref = (localId: string): IrExpr => ({ kind: "varRef", localId, type: F64, loc }); +const num = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc }); +const bin = (op: string, left: IrExpr, right: IrExpr, type = F64): IrExpr => + ({ kind: "bin", op, left, right, type, loc }) as IrExpr; +const and = (left: IrExpr, right: IrExpr): IrExpr => ({ + kind: "logical", + op: "&&", + left, + right, + type: BOOL, + loc, +}); +const guard = (cond: IrExpr): IrStmt => ({ + kind: "if", + cond, + then: [{ kind: "break", loc } as IrStmt], + else_: null, + loc, +}); +/** `for (i = 0; i < 50; i++) { ...body }` */ +const loop = (body: IrStmt[]) => countedFor(loc, num(50), () => body) as IrStmt & { kind: "for" }; +const period = (body: IrStmt[], start = 0, step = 1) => + periodicGuardPeriod(loop(body), "i.0", num(start), step); + +test("a guard on the counter's low bits gives the loop its period", () => { + // if ((i & 3) === 3 && x > 4) break; + const masked = bin("===", bin("&", ref("i.0"), num(3)), num(3), BOOL); + expect(period([guard(and(masked, bin(">", ref("x"), num(4), BOOL)))])).toBe(4); + // if (0 == (7 & i)) ... + expect(period([guard(bin("==", num(0), bin("&", num(7), ref("i.0")), BOOL))])).toBe(8); + expect(period([guard(bin("!==", bin("&", ref("i.0"), num(1)), num(0), BOOL))])).toBe(2); +}); + +test("other guards and loops get no period", () => { + const masked = (mask: number, value: number, counter = "i.0") => + guard(bin("===", bin("&", ref(counter), num(mask)), num(value), BOOL)); + // Masks that are not 1, 3 or 7, values outside the mask, another variable. + expect(period([masked(5, 1)])).toBeNull(); + expect(period([masked(15, 15)])).toBeNull(); + expect(period([masked(3, 4)])).toBeNull(); + expect(period([masked(3, 3, "j")])).toBeNull(); + // The masked test is not the condition's first conjunct. + const later = and( + bin(">", ref("x"), num(4), BOOL), + bin("===", bin("&", ref("i.0"), num(3)), num(3), BOOL), + ); + expect(period([guard(later)])).toBeNull(); + // A start that is not a multiple of the period, a step other than 1. + expect(period([masked(3, 3)], 1)).toBeNull(); + expect(period([masked(3, 3)], 0, 2)).toBeNull(); + // A nested loop. + expect(period([masked(3, 3), loop([])])).toBeNull(); +}); + +test("a large body is not unrolled", () => { + const work: IrStmt[] = []; + for (let k = 0; k < 400; k++) + work.push({ kind: "assign", localId: "x", value: bin("+", ref("x"), num(k)), loc } as IrStmt); + expect( + period([guard(bin("===", bin("&", ref("i.0"), num(3)), num(3), BOOL)), ...work]), + ).toBeNull(); +}); + +test("the module carries the loop ID nodes its back edges refer to", () => { + const lines = [` br label %loop.c1${loopUnrollAttachment(4)}`, " ret void"]; + expect(loopUnrollMetadata(lines)).toEqual([ + `!1000004 = distinct !{!1000004, !{!"llvm.loop.unroll.count", i32 4}}`, + ]); + expect(loopUnrollMetadata([" ret void"])).toEqual([]); +}); diff --git a/packages/compiler/src/backend/llvm/periodic-guards.ts b/packages/compiler/src/backend/llvm/periodic-guards.ts new file mode 100644 index 000000000..3a07126db --- /dev/null +++ b/packages/compiler/src/backend/llvm/periodic-guards.ts @@ -0,0 +1,97 @@ +/* Counted loops with a periodic guard: a branch on the counter's low bits, + * such as `if ((j & 3) === 3 && ...)` (a check every fourth iteration). + * LLVM checks such a guard on every iteration, and when the guarded + * condition is cheap it speculates the condition into the loop. The loop's + * back edge asks LLVM to unroll it by the guard's period + * (`llvm.loop.unroll.count`): in each unrolled copy the counter's low bits + * are known, so the guard folds to a constant, and only one copy in each + * period keeps the check. The metadata changes how LLVM compiles the loop, + * not what it computes. */ +import type { IrExpr, IrStmt } from "../../ir/ir.js"; +import { everyExprChild, everyStmtChild } from "../../ir/traverse.js"; + +/** The most IR nodes a loop body may have, times the period, for the loop + * to be unrolled: a larger body gains less than its unrolled copies cost. */ +const UNROLLED_BODY_NODES = 4096; + +/** The loop IDs, one per period (2, 4 and 8). A loop ID is a distinct + * node whose first operand is itself; loops with the same period share + * one, as the copies of an inlined loop do. */ +const loopIds = new Map([ + [2, 1_000_002], + [4, 1_000_004], + [8, 1_000_008], +]); + +/** The guard period of `loop`, a counted loop on `counter` that starts at + * `start` and steps by `step`, or null. The period is M + 1 where the body + * has an `if` whose condition is, or starts with an `&&` of, + * `(counter & M) === C` (or `==`, `!==`, `!=`, either operand order) with + * M one of 1, 3 and 7 and C an integer from 0 to M. The loop steps by 1 + * from a multiple of the period, so that the counter's low bits are known + * in each unrolled copy, and its body has no nested loop. */ +export function periodicGuardPeriod( + loop: IrStmt & { kind: "for" }, + counter: string, + start: IrExpr, + step: number, +): number | null { + if (step !== 1 || start.kind !== "numLit") return null; + let period: number | null = null; + let nodes = 0; + let plain = true; + const expr = (e: IrExpr): boolean => { + nodes++; + return everyExprChild(e, expr, stmt); + }; + const stmt = (s: IrStmt): boolean => { + nodes++; + if (s.kind === "for" || s.kind === "while" || s.kind === "doWhile" || s.kind === "forOf") { + plain = false; + return false; + } + if (s.kind === "if" && period === null) period = guardPeriod(s.cond, counter); + return everyStmtChild(s, expr, stmt); + }; + for (const s of loop.body) if (!stmt(s)) break; + if (!plain || period === null) return null; + if (start.value % period !== 0 || nodes * period > UNROLLED_BODY_NODES) return null; + return period; +} + +function guardPeriod(cond: IrExpr, counter: string): number | null { + let test = cond; + while (test.kind === "logical" && test.op === "&&") test = test.left; + if (test.kind !== "bin" || !["===", "==", "!==", "!="].includes(test.op)) return null; + const masked = (e: IrExpr, other: IrExpr): number | null => { + if (e.kind !== "bin" || e.op !== "&" || other.kind !== "numLit") return null; + const [ref, mask] = e.left.kind === "varRef" ? [e.left, e.right] : [e.right, e.left]; + if (ref.kind !== "varRef" || ref.localId !== counter || mask.kind !== "numLit") return null; + const m = mask.value; + if (m !== 1 && m !== 3 && m !== 7) return null; + const c = other.value; + return Number.isInteger(c) && c >= 0 && c <= m ? m + 1 : null; + }; + return masked(test.left, test.right) ?? masked(test.right, test.left); +} + +/** The attachment that asks LLVM to unroll a loop by `period`, for its + * back edge. */ +export function loopUnrollAttachment(period: number): string { + return `, !llvm.loop !${loopIds.get(period)!}`; +} + +/** The loop ID nodes that the module's text `lines` refer to. */ +export function loopUnrollMetadata(lines: readonly string[]): string[] { + const used = new Set(); + for (const line of lines) { + if (!line.includes("!llvm.loop !")) continue; + for (const [period, id] of loopIds) if (line.includes(`!llvm.loop !${id}`)) used.add(period); + } + return [...used] + .sort((a, b) => a - b) + .map((period) => { + const id = loopIds.get(period)!; + return `!${id} = distinct !{!${id}, !{!"llvm.loop.unroll.count", i32 ${period}}}`; + }); +} diff --git a/packages/compiler/src/backend/llvm/publish.ts b/packages/compiler/src/backend/llvm/publish.ts index 60270adda..62fdd75ec 100644 --- a/packages/compiler/src/backend/llvm/publish.ts +++ b/packages/compiler/src/backend/llvm/publish.ts @@ -596,10 +596,10 @@ export class PublishWalkers { } } -/** Unpublishable types stored as scalars (dates are time values, process - * streams file descriptors): no pointer to push, and always a value. */ +/** Unpublishable types stored as scalars (process streams are file + * descriptors): no pointer to push, and always a value. */ function scalarRefusal(t: IrType): boolean { - return t.kind === "date" || t.kind === "procStream"; + return t.kind === "procStream"; } /** The Node.js implementation's refusal for a value of type `t`. */ diff --git a/packages/compiler/src/backend/llvm/receivers.ts b/packages/compiler/src/backend/llvm/receivers.ts index 5e1077ee5..8524a51fb 100644 --- a/packages/compiler/src/backend/llvm/receivers.ts +++ b/packages/compiler/src/backend/llvm/receivers.ts @@ -11,13 +11,24 @@ import type { LlValue } from "./expr-context.js"; import { guardedValue } from "./borrowed-receivers.js"; import { InternalCompilerError } from "../../errors.js"; import { emitBorrowedArrayRead, emitProjectedArrayRead } from "./local-array-reads.js"; -import { emitStackMapRead, matchMapRead } from "./map-read-lifetimes.js"; +import { + emitBorrowedMapGet, + emitStackMapRead, + matchMapRead, + pointerMapGet, +} from "./map-read-lifetimes.js"; import { emitBorrowedResultCall, emitCallExpr } from "./expr-calls.js"; -import { emitCheckedNarrowReceiver } from "./checked-narrows.js"; +import { borrowsCheckedNarrow, emitCheckedNarrowReceiver } from "./checked-narrows.js"; import { canStackUnion, emitStackUnion } from "./stack-unions.js"; import { emitDowncastGuard, emitUnionNarrowGuard } from "./narrow-guards.js"; import { emitScalarOptional, isScalarOptionalSource } from "./local-union-storage.js"; import { borrowableInputs } from "./borrowed-inputs.js"; +import { + emitBorrowedRecordKeyGet, + emitStackRecordKeyGet, + pointerRecordKeyGet, + scalarRecordKeyGet, +} from "./expr-records.js"; import { type WalkBorrowHost, analyzeWalks } from "./walk-borrows.js"; /** Read a receiver whose value is consumed immediately by a field or tag @@ -91,10 +102,16 @@ export function emitReadReceiver(emitter: LlEmitter, e: IrExpr): LlValue { return emitBorrowedResultCall(emitter, e); const arrayRead = emitter.optionalArrayReads.get(e); if (arrayRead) return emitProjectedArrayRead(emitter, arrayRead); + if (e.kind === "recordKeyGet" && emitter.canBorrowReceiver(e)) + return emitBorrowedRecordKeyGet(emitter, e); + if (e.kind === "mapIntrinsic" && emitter.canBorrowReceiver(e)) + return emitBorrowedMapGet(emitter, e); const narrow = emitter.checkedNarrows.get(e); const narrowed = narrow ? emitCheckedNarrowReceiver(emitter, narrow) : null; if (narrowed) return narrowed; if (canStackUnion(e, emitter.boxedUnionsById)) return emitStackUnion(emitter, e).value; + if (e.kind === "recordKeyGet" && scalarRecordKeyGet(emitter, e)) + return { name: emitStackRecordKeyGet(emitter, e), type: e.type }; if (e.kind === "unionNarrow") { const union = emitter.emitUnionProjection(e.value); emitUnionNarrowGuard(emitter, union.name, e.unionId, e.tag); @@ -291,7 +308,27 @@ export function canBorrowReceiver(emitter: LlEmitter, e: IrExpr): boolean { // snapshot, so neither the callee nor a later argument may replace // what it was read from: a callee that preserves every edge cannot, // and any other one must leave the argument's locations unwritten. + // A keyed read lends the stored pointer while the record keeps it. + case "recordKeyGet": + return ( + pointerRecordKeyGet(emitter, e) && + emitter.canBorrowReceiver(e.obj) && + emitter.referenceEffects.preserves(e.key) + ); + // So does a Map.get while the map keeps it. + case "mapIntrinsic": + return ( + pointerMapGet(emitter, e) && + emitter.canBorrowReceiver(e.receiver) && + emitter.referenceEffects.preserves(e.args[0]!) + ); case "call": { + // A checked extraction lends the payload of the union it narrows + // (emitCheckedNarrowReceiver). + if (borrowsCheckedNarrow(emitter, e)) { + const narrow = emitter.checkedNarrows.get(e)!; + return emitter.canBorrowReceiver(narrow.value); + } if (!emitter.borrowedReturns.has(e.callee)) return false; if (!e.args.every((arg) => emitter.referenceEffects.preserves(arg))) return false; const preserving = emitter.referenceEffects.functions.has(e.callee); @@ -410,6 +447,7 @@ export function isStackUnionSource(emitter: LlEmitter, e: IrExpr): boolean { emitter.optionalArrayReads.get(e) !== null || isScalarOptionalSource(e, emitter.unionSources) || emitter.scalarResultCall(e) || - emitter.inlineFieldGet(e) + emitter.inlineFieldGet(e) || + scalarRecordKeyGet(emitter, e) ); } diff --git a/packages/compiler/src/backend/llvm/record-union-keys.test.ts b/packages/compiler/src/backend/llvm/record-union-keys.test.ts new file mode 100644 index 000000000..3c961cabb --- /dev/null +++ b/packages/compiler/src/backend/llvm/record-union-keys.test.ts @@ -0,0 +1,169 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile, deserializeModule } from "../../index.js"; +import { BOOL, F64, VOID, type IrExpr, type IrModule, type IrType } from "../../ir/ir.js"; +import { validateModule } from "../../ir/validate.js"; +import { emitLlvmModule } from "./emitter.js"; + +const loc = { file: "record-union-keys.ts", start: 0, end: 0 }; + +async function llvmFor(source: string): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-record-union-keys-")); + try { + const entry = join(dir, "main.ts"), + output = join(dir, "main.ir.json"); + await writeFile(entry, source); + const result = await compile(entry, { + outDir: dir, + outPath: output, + outputKind: "ir", + }); + if (!result.ok) + throw new Error(result.diagnostics.map((item) => `${item.code}: ${item.message}`).join("\n")); + const module = deserializeModule(await readFile(output, "utf8")); + expect(validateModule(module)).toEqual([]); + return emitLlvmModule(module); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +function body(ir: string, name: string): string { + const found = new RegExp(`^define internal [^\\n]*@${name}(?:\\.i32)?\\([^]*?^}`, "m").exec(ir); + expect(found, name).not.toBeNull(); + return found![0]; +} + +test("Sets and Maps of record unions key the record pointer", async () => { + const ir = await llvmFor(` +type Num = { kind: "num"; value: number }; +type Neg = { kind: "neg"; operand: Node }; +type Add = { kind: "add"; left: Node; right: Node }; +type Node = Num | Neg | Add; +const one: Node = { kind: "num", value: 1 }; +const nodes: Node[] = [one, { kind: "neg", operand: one }, { kind: "add", left: one, right: one }]; +const seen = new Set(); +const depth = new Map(); +for (const node of nodes) { + seen.add(node); + depth.set(node, node.kind === "num" ? 0 : 1); +} +console.log(seen.size, depth.get(nodes[1]!), seen.has({ kind: "num", value: 1 })); +`); + // SCR_MAP_KEY_REF (2), never SCR_MAP_KEY_UNION_REF (3), and no union box. + expect(ir).toMatch(/call ptr @scr_map_new\(i32 2,/); + expect(ir).not.toMatch(/call ptr @scr_map_new\(i32 3,/); + expect(ir).not.toMatch(/call ptr @scr_union_new_ref/); +}); + +const recordType = (shapeId: string): IrType => ({ kind: "record", shapeId }); +const wide: IrType = { kind: "union", unionId: "wide" }; + +/** `pick(u)` answers whether `u` holds arm 0, 2 or 3 of a five-record union + * through an `||` chain of tag tests; `same` is a second union over the same + * records in another order. */ +function tagChainModule(): IrModule { + const u: IrExpr = { kind: "varRef", localId: "u", type: wide, loc }; + const isTag = (tag: number): IrExpr => ({ + kind: "unionIsTag", + unionId: "wide", + tag, + negated: false, + value: u, + type: BOOL, + loc, + }); + const or = (left: IrExpr, right: IrExpr): IrExpr => ({ + kind: "logical", + op: "||", + left, + right, + type: BOOL, + loc, + }); + const shapes = ["a", "b", "c", "d", "e"]; + return { + irVersion: 17, + sourceFile: loc.file, + entry: "main", + records: shapes.map((id) => ({ id, fields: [{ name: id, type: F64 }] })), + unions: [ + { id: "wide", arms: shapes.map(recordType) }, + { id: "same", arms: [...shapes].reverse().map(recordType) }, + ], + functions: [ + { + name: "main", + params: [], + returnType: VOID, + locals: [], + body: [], + loc, + }, + { + name: "pick", + params: [{ localId: "u", name: "u", type: wide }], + returnType: BOOL, + locals: [{ id: "u", name: "u", type: wide, mutable: false }], + body: [{ kind: "return", value: or(or(isTag(0), isTag(2)), isTag(3)), loc }], + loc, + }, + { + name: "keep", + params: [{ localId: "s", name: "s", type: { kind: "union", unionId: "same" } }], + returnType: VOID, + locals: [ + { + id: "s", + name: "s", + type: { kind: "union", unionId: "same" }, + mutable: false, + }, + ], + body: [], + loc, + }, + ], + }; +} + +test("an || chain of tag tests is one switch on the shape word", () => { + const mod = tagChainModule(); + expect(validateModule(mod)).toEqual([]); + const ir = emitLlvmModule(mod, { pointerBits: 64 }); + const pick = body(ir, "sc_bf_pick"); + const switches = pick.match(/switch i32 [^\n]*/g) ?? []; + expect(switches).toHaveLength(1); + expect(switches[0]!.match(/label %tags\.y/g)).toHaveLength(3); + expect(pick).not.toMatch(/@sc_stag\./); +}); + +test("unions over the same records share one release", () => { + const ir = emitLlvmModule(tagChainModule(), { pointerBits: 64 }); + expect(ir.match(/^define internal void @sc_srelease\.[^\n]*noinline/gm)).toHaveLength(1); + expect(ir).toMatch(/^define internal void @sc_srelease\.wide\(/m); + expect(ir).not.toMatch(/^define internal void @sc_srelease\.same\(/m); +}); + +test("a union with a freezable arm compares its count without the frozen bit", async () => { + const ir = await llvmFor(` +type Name = { kind: "name"; text: string }; +type Count = { kind: "count"; value: number }; +type Key = Name | Count; +function name(text: string): Key { + const built: Name = { kind: "name", text }; + return Object.freeze(built); +} +const seen = new Set(); +seen.add(name("a")); +seen.add({ kind: "count", value: 1 }); +console.log(seen.size); +`); + const release = /^define internal void @sc_srelease\.[^\n]*\n[^]*?^}/m.exec(ir); + expect(release).not.toBeNull(); + // SCR_RC_FROZEN is bit 62: a frozen record's last reference has count 1. + expect(release![0]).toContain("and i64 %rc, -4611686018427387905"); + expect(release![0]).toContain("icmp ult i64 %rcm2, -4611686018427387907"); +}); diff --git a/packages/compiler/src/backend/llvm/reference-effects.ts b/packages/compiler/src/backend/llvm/reference-effects.ts index 6ebe1222b..43a9176d5 100644 --- a/packages/compiler/src/backend/llvm/reference-effects.ts +++ b/packages/compiler/src/backend/llvm/reference-effects.ts @@ -181,6 +181,9 @@ function expressionPreservesEdges( case "bytesIntrinsic": return ( e.method === "get" || + e.method === "indexOfElem" || + e.method === "lastIndexOfElem" || + e.method === "includesElem" || e.method === "length" || e.method === "byteLength" || e.method === "byteOffset" || diff --git a/packages/compiler/src/backend/llvm/reference-writes.test.ts b/packages/compiler/src/backend/llvm/reference-writes.test.ts index cbbe49d3b..db575babe 100644 --- a/packages/compiler/src/backend/llvm/reference-writes.test.ts +++ b/packages/compiler/src/backend/llvm/reference-writes.test.ts @@ -97,3 +97,46 @@ test("an array that is read back or escapes keeps its element writes", () => { const input = fill("input", [store("input", element("input"))]); expect(writes(input).conflicts(["a"], writes(input).summary("input"))).toBe(true); }); + +test("a keyed record read depends on every field and keyed entry of records", () => { + const handler: IrType = { kind: "func", params: [F64], ret: F64 }; + const table: IrType = { kind: "record", shapeId: "t" }; + const key: IrExpr = { kind: "strLit", value: "k", type: { kind: "string" }, loc }; + const read: IrExpr = { + kind: "recordKeyGet", + obj: ref("table", table), + shapeId: "t", + key, + type: { kind: "union", unionId: "hu" }, + loc, + }; + const keyedWrite: IrStmt = { + kind: "recordKeySet", + obj: ref("table", table), + shapeId: "t", + key, + value: ref("h", handler), + loc, + }; + const fn = (name: string, body: IrStmt[]): IrFunction => ({ + name, + loc, + returnType: F64, + params: [{ localId: "table", name: "table", type: table }], + locals: [ + { id: "table", name: "table", type: table, mutable: false }, + { id: "h", name: "h", type: handler, mutable: false }, + ], + body, + }); + const records = [{ id: "t", fields: [{ name: "add", type: handler }], indexValue: handler }]; + const quiet = fn("quiet", []); + const writer = fn("writer", [keyedWrite]); + const functions = new Map([quiet, writer].map((f) => [f.name, f])); + const rw = new ReferenceWrites(functions, [], () => false, new Set(), { records }); + rw.enter(quiet); + const keys = rw.readKeys(read); + expect(keys).toEqual(expect.arrayContaining(["r:*", "r:add"])); + expect(rw.conflicts(keys!, rw.summary("quiet"))).toBe(false); + expect(rw.conflicts(keys!, rw.summary("writer"))).toBe(true); +}); diff --git a/packages/compiler/src/backend/llvm/reference-writes.ts b/packages/compiler/src/backend/llvm/reference-writes.ts index 89132a715..79ac98a7d 100644 --- a/packages/compiler/src/backend/llvm/reference-writes.ts +++ b/packages/compiler/src/backend/llvm/reference-writes.ts @@ -92,6 +92,8 @@ const INERT_LIB_PREFIXES = [ ]; const INERT_LIB_CALLS = new Set([ "dyn.compare", + // Reads the ambient receiver (a fresh reference to the bound value). + "dyn.this", "error.new", "error.nodeThrow", "dyn.typedRefIs", @@ -107,6 +109,7 @@ const INERT_LIB_CALLS = new Set([ "regex.groupParseInt", "regex.resultProp", "array.namedKeys", + "array.foreign", "array.hasNamedKey", "array.hasProperties", "process.cwd", @@ -922,6 +925,17 @@ export class ReferenceWrites { case "arrayGet": out.push(ARRAY_KEY); return this.expr(node.index) === null && walk(node.arr); + case "recordKeyGet": + // Any declared field or overflow entry the key may name. + out.push(ANY_RECORD_KEY); + for (const f of this.records.get(node.shapeId)?.fields ?? []) out.push(`r:${f.name}`); + return this.expr(node.key) === null && walk(node.obj); + case "mapIntrinsic": + // A typed Map.get: any entry of any map. + if (node.method !== "get" || node.args.length !== 1 || !borrowsMapReadInputs(node)) + return false; + out.push(MAP_KEY); + return this.expr(node.args[0]!) === null && walk(node.receiver); case "unionNarrow": case "upcast": case "downcast": diff --git a/packages/compiler/src/backend/llvm/regex-matchers.test.ts b/packages/compiler/src/backend/llvm/regex-matchers.test.ts index d009de7c9..5f93f7295 100644 --- a/packages/compiler/src/backend/llvm/regex-matchers.test.ts +++ b/packages/compiler/src/backend/llvm/regex-matchers.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from "vitest"; import { + canReplaceWith, compileRegexMatcher, regexLiteralBailReason, regexLiteralCaptures, @@ -152,3 +153,46 @@ describe("regex literal matcher code", () => { expect(text).not.toContain("constant [128 x i8]"); }); }); + +describe("regex literal replacers", () => { + test("take global, non-sticky, non-plain-text patterns and ASCII replacements without $", () => { + expect(canReplaceWith("a[bc]", "g", "<2>")).toBe(true); + expect(canReplaceWith("a[bc]", "gi", "")).toBe(true); + expect(canReplaceWith("a[bc]", "", "x")).toBe(false); + expect(canReplaceWith("a[bc]", "gy", "x")).toBe(false); + expect(canReplaceWith("a[bc]", "g", "$1")).toBe(false); + expect(canReplaceWith("a[bc]", "g", "é")).toBe(false); + // Plain text is a string search at run time. + expect(canReplaceWith("&", "g", "&")).toBe(false); + expect(canReplaceWith("a\\.b", "g", "x")).toBe(false); + // Letters under i are not plain text. + expect(canReplaceWith("ab", "gi", "x")).toBe(true); + }); + + test("append the gap and the replacement and continue the search", () => { + const code = compileRegexMatcher("r", "<[^>]*>", "g", "i64", true, "|"); + const text = code!.lines.join("\n"); + expect(text).toContain('@r.rep = private unnamed_addr constant [1 x i8] c"|"'); + expect(text).toContain("call void @scr_re_replace_out_reserve(ptr %out, i64 %m.need)"); + expect(text).toContain("load <16 x i8>, ptr %m.src, align 1"); + expect(text).toMatch(/br i1 %m\.past, label %nomatch, label %scan/); + expect(text).not.toContain("m.ret:"); + expect(text).toContain("ret i32 %n.found"); + // The search after a match is a 16-byte scan, not a memchr call. + expect(text).not.toContain("@memchr"); + expect(code!.decls).toContain("declare void @scr_re_replace_out_reserve(ptr, i64)"); + // Without vectors the gap is a memcpy. + const scalar = compileRegexMatcher("r", "<[^>]*>", "g", "i64", false, "|")!.lines.join("\n"); + expect(scalar).not.toContain("<16 x i8>"); + expect(scalar).toContain( + "call void @llvm.memcpy.p0.p0.i64(ptr %m.dst, ptr %m.src, i64 %m.gapw, i1 false)", + ); + }); + + test("escape the replacement bytes and skip the copy of an empty one", () => { + const quoted = compileRegexMatcher("r", "a[bc]", "g", "i64", true, '"\\\n')!.lines.join("\n"); + expect(quoted).toContain('c"\\22\\5C\\0A"'); + const empty = compileRegexMatcher("r", "a[bc]", "g", "i64", true, "")!.lines.join("\n"); + expect(empty).not.toContain("@r.rep"); + }); +}); diff --git a/packages/compiler/src/backend/llvm/regex-matchers.ts b/packages/compiler/src/backend/llvm/regex-matchers.ts index d83d6bae5..0491a6446 100644 --- a/packages/compiler/src/backend/llvm/regex-matchers.ts +++ b/packages/compiler/src/backend/llvm/regex-matchers.ts @@ -24,6 +24,8 @@ * `len`, it returns the capture count (whole match included) so the runtime * can check it against the compiled bytecode. */ +import { InternalCompilerError } from "../../errors.js"; + const ASCII = 128; const INF = -1; /** Larger patterns keep the interpreter (binary size). */ @@ -1853,10 +1855,13 @@ export function compileRegexMatcher( flags: string, sizeType: string, vector: boolean, + replacement: string | null = null, ): RegexMatcherCode | null { const parsed = parseSubset(pattern, flags); if (parsed === null) return null; const sticky = flags.includes("y"); + if (replacement !== null && !canReplaceWith(pattern, flags, replacement)) + throw new InternalCompilerError(`llvm emitter bug: no replacer for /${pattern}/${flags}`); const { root, ncap } = parsed; const gen = new RxGen(sym, ncap, vector); const end: FirstInfo = { set: emptySet(), nullable: true, bol: false, uncond: true }; @@ -1873,6 +1878,12 @@ export function compileRegexMatcher( fn.push(` %stk = alloca ptr`); fn.push(` %sbuf = alloca [64 x i32]`); for (let r = 0; r < gen.regs; r++) fn.push(` %r${r} = alloca i32`); + if (replacement !== null) { + fn.push(` %next = alloca i32`); + fn.push(` %nfound = alloca i32`); + fn.push(` store i32 0, ptr %next`); + fn.push(` store i32 0, ptr %nfound`); + } fn.push(` store ptr %sbuf, ptr %stk`); fn.push(` store i32 64, ptr %scap`); fn.push(` store i32 %start, ptr %at`); @@ -1900,7 +1911,9 @@ export function compileRegexMatcher( } else { fn.push(` br label %seek`); } - if (firstCount === 1) { + // A replacer seeks again after every match, often a few bytes on: the + // 16-byte scan avoids a memchr call per match. + if (firstCount === 1 && replacement === null) { fn.push(`seek:`); const byte = intervals(first.set)[0]![0]!; fn.push(` %rest = sub i32 %len, %a`); @@ -1944,6 +1957,98 @@ export function compileRegexMatcher( fn.push(` br i1 %over, label %nomatch, label %scan`); } + if (replacement !== null) { + emitReplaceMatch(fn, sym, replacement, vector); + } else { + emitReturnMatch(fn, ncap); + } + + const result = replacement !== null ? "%n.found" : "0"; + fn.push(`nomatch:`); + if (replacement !== null) { + fn.push(` %n.next = load i32, ptr %next`); + fn.push(` %n.nextp = getelementptr inbounds i8, ptr %out, i64 16`); + fn.push(` %n.nextw = sext i32 %n.next to i64`); + fn.push(` store i64 %n.nextw, ptr %n.nextp`); + fn.push(` %n.found = load i32, ptr %nfound`); + } + fn.push(` %n.stk = load ptr, ptr %stk`); + fn.push(` %n.heap = icmp ne ptr %n.stk, %sbuf`); + fn.push(` br i1 %n.heap, label %n.free, label %n.ret`); + fn.push(`n.free:`); + fn.push(` call void @scr_regex_stack_free(ptr %n.stk)`); + fn.push(` br label %n.ret`); + fn.push(`n.ret:`); + fn.push(` ret i32 ${result}`); + + gen.emitTail(); + for (const l of gen.body) fn.push(l); + fn.push(`}`); + + const decls = REGEX_MATCHER_DECLS.slice(); + if (gen.body.some((l) => l.includes("@llvm.cttz.i16"))) + decls.push(`declare i16 @llvm.cttz.i16(i16, i1 immarg)`); + if (!sticky && !first.nullable && firstCount === 1 && replacement === null) + decls.push(`declare ptr @memchr(ptr, i32, ${sizeType})`); + if (replacement !== null) { + decls.push(`declare void @scr_re_replace_out_reserve(ptr, i64)`); + decls.push(`declare void @llvm.memcpy.p0.p0.i64(ptr, ptr, i64, i1 immarg)`); + } + const globals = [...gen.globals]; + if (replacement !== null && replacement.length > 0) + globals.push( + `@${sym}.rep = private unnamed_addr constant [${replacement.length} x i8] c"${llvmBytes(replacement)}"`, + ); + return { lines: [...globals, ...fn], decls }; +} + +/** Whether a substitution can run through a build-time replacer: a global, + * non-sticky pattern that is not plain text (the runtime searches those as + * strings) and an ASCII replacement without `$` patterns. */ +export function canReplaceWith(pattern: string, flags: string, replacement: string): boolean { + if (!flags.includes("g") || flags.includes("y") || isPlainTextPattern(pattern, flags)) + return false; + for (let i = 0; i < replacement.length; i++) { + const c = replacement.charCodeAt(i); + if (c >= ASCII || c === 0x24) return false; + } + return true; +} + +/** scr_regex_plain_text's rule: a pattern of literal characters and escaped + * metacharacters (no letters under `i`) that the runtime searches as text. */ +function isPlainTextPattern(pattern: string, flags: string): boolean { + const fold = flags.includes("i"); + if (pattern.length === 0 || pattern.length > 256) return false; + for (let i = 0; i < pattern.length; i++) { + let c = pattern[i]!; + if (c === "\\") { + if (i + 1 >= pattern.length) return false; + c = pattern[++i]!; + if (!"^$\\.*+?()[]{}|/".includes(c)) return false; + } else { + const code = c.charCodeAt(0); + if (code < 0x20 || code >= 0x7f || "^$.*+?()[]{}|".includes(c)) return false; + } + if (fold && /[a-z]/i.test(c)) return false; + } + return true; +} + +function llvmBytes(text: string): string { + let out = ""; + for (let i = 0; i < text.length; i++) { + const c = text.charCodeAt(i); + out += + c >= 0x20 && c < 0x7f && c !== 0x22 && c !== 0x5c + ? text[i] + : `\\${c.toString(16).toUpperCase().padStart(2, "0")}`; + } + return out; +} + +/** The match block of a matcher: store the capture pointers, return 1. */ +function emitReturnMatch(fn: string[], ncap: number): void { fn.push(`match:`); fn.push(` %m.end = load i32, ptr %pos`); fn.push(` %m.start = load i32, ptr %at`); @@ -1969,25 +2074,72 @@ export function compileRegexMatcher( fn.push(` br label %m.ret`); fn.push(`m.ret:`); fn.push(` ret i32 1`); +} - fn.push(`nomatch:`); - fn.push(` %n.stk = load ptr, ptr %stk`); - fn.push(` %n.heap = icmp ne ptr %n.stk, %sbuf`); - fn.push(` br i1 %n.heap, label %n.free, label %n.ret`); - fn.push(`n.free:`); - fn.push(` call void @scr_regex_stack_free(ptr %n.stk)`); - fn.push(` br label %n.ret`); - fn.push(`n.ret:`); - fn.push(` ret i32 0`); - - gen.emitTail(); - for (const l of gen.body) fn.push(l); - fn.push(`}`); - - const decls = REGEX_MATCHER_DECLS.slice(); - if (gen.body.some((l) => l.includes("@llvm.cttz.i16"))) - decls.push(`declare i16 @llvm.cttz.i16(i16, i1 immarg)`); - if (!sticky && !first.nullable && firstCount === 1) - decls.push(`declare ptr @memchr(ptr, i32, ${sizeType})`); - return { lines: [...gen.globals, ...fn], decls }; +/** The match block of a replacer: append the gap since the previous match + * and the replacement to the output (ScrReReplaceOut: len, cap, next at + * 0, 8 and 16, then data), and continue the search at the match end, or one past it for an + * empty match, as the runtime's replace loop does. */ +function emitReplaceMatch(fn: string[], sym: string, replacement: string, vector: boolean): void { + const n = replacement.length; + fn.push(`match:`); + fn.push(` %m.end = load i32, ptr %pos`); + fn.push(` %m.start = load i32, ptr %at`); + fn.push(` %m.next = load i32, ptr %next`); + fn.push(` %m.gap = sub i32 %m.start, %m.next`); + fn.push(` %m.gapw = zext i32 %m.gap to i64`); + fn.push(` %m.lenp = getelementptr inbounds i8, ptr %out, i64 0`); + fn.push(` %m.capp = getelementptr inbounds i8, ptr %out, i64 8`); + fn.push(` %m.datap = getelementptr inbounds i8, ptr %out, i64 24`); + fn.push(` %m.len0 = load i64, ptr %m.lenp`); + fn.push(` %m.cap = load i64, ptr %m.capp`); + fn.push(` %m.room = sub i64 %m.cap, %m.len0`); + // 16 bytes of slack past the gap and the replacement for the 16-byte copy. + fn.push(` %m.need = add i64 %m.gapw, ${n + 32}`); + fn.push(` %m.fits = icmp uge i64 %m.room, %m.need`); + fn.push(` br i1 %m.fits, label %m.put, label %m.grow`); + fn.push(`m.grow:`); + fn.push(` call void @scr_re_replace_out_reserve(ptr %out, i64 %m.need)`); + fn.push(` br label %m.put`); + fn.push(`m.put:`); + fn.push(` %m.data = load ptr, ptr %m.datap`); + fn.push(` %m.len = load i64, ptr %m.lenp`); + fn.push(` %m.dst = getelementptr inbounds i8, ptr %m.data, i64 %m.len`); + fn.push(` %m.nextw = sext i32 %m.next to i64`); + fn.push(` %m.src = getelementptr inbounds i8, ptr %s, i64 %m.nextw`); + if (vector) { + // A gap of at most 16 bytes with 16 readable subject bytes is one + // unaligned 16-byte copy. + fn.push(` %m.small = icmp ule i32 %m.gap, 16`); + fn.push(` %m.rest = sub i32 %len, %m.next`); + fn.push(` %m.readable = icmp sge i32 %m.rest, 16`); + fn.push(` %m.fast = and i1 %m.small, %m.readable`); + fn.push(` br i1 %m.fast, label %m.copy16, label %m.copy`); + fn.push(`m.copy16:`); + fn.push(` %m.v = load <16 x i8>, ptr %m.src, align 1`); + fn.push(` store <16 x i8> %m.v, ptr %m.dst, align 1`); + fn.push(` br label %m.rep`); + } else { + fn.push(` br label %m.copy`); + } + fn.push(`m.copy:`); + fn.push(` call void @llvm.memcpy.p0.p0.i64(ptr %m.dst, ptr %m.src, i64 %m.gapw, i1 false)`); + fn.push(` br label %m.rep`); + fn.push(`m.rep:`); + fn.push(` %m.rdst = getelementptr inbounds i8, ptr %m.dst, i64 %m.gapw`); + if (n > 0) + fn.push(` call void @llvm.memcpy.p0.p0.i64(ptr %m.rdst, ptr @${sym}.rep, i64 ${n}, i1 false)`); + fn.push(` %m.len1 = add i64 %m.len, %m.gapw`); + fn.push(` %m.len2 = add i64 %m.len1, ${n}`); + fn.push(` store i64 %m.len2, ptr %m.lenp`); + fn.push(` store i32 %m.end, ptr %next`); + fn.push(` %m.found0 = load i32, ptr %nfound`); + fn.push(` %m.found1 = add i32 %m.found0, 1`); + fn.push(` store i32 %m.found1, ptr %nfound`); + fn.push(` %m.empty = icmp eq i32 %m.end, %m.start`); + fn.push(` %m.after = add i32 %m.end, 1`); + fn.push(` %m.at = select i1 %m.empty, i32 %m.after, i32 %m.end`); + fn.push(` store i32 %m.at, ptr %at`); + fn.push(` %m.past = icmp sgt i32 %m.at, %len`); + fn.push(` br i1 %m.past, label %nomatch, label %scan`); } diff --git a/packages/compiler/src/backend/llvm/serialized-ir-emission.test.ts b/packages/compiler/src/backend/llvm/serialized-ir-emission.test.ts index cdbba146c..1d35caa05 100644 --- a/packages/compiler/src/backend/llvm/serialized-ir-emission.test.ts +++ b/packages/compiler/src/backend/llvm/serialized-ir-emission.test.ts @@ -41,7 +41,10 @@ test("sort-heavy programs emit equal LLVM from memory and from serialized IR", a pointerBits: 64, wasi: false, runtimeAbiMarker: false, + // compile() emits Linux textual LLVM with the unwinding ABI. + unwindAbi: process.platform === "linux", objectAudit: sanitize, + sanitizeAttributes: sanitize, ...inlineRcOption(options.optimization, sanitize), }); const text = typeof emitted === "string" ? emitted : emitted.join(""); diff --git a/packages/compiler/src/backend/llvm/shape-union-tags.test.ts b/packages/compiler/src/backend/llvm/shape-union-tags.test.ts new file mode 100644 index 000000000..e07626e77 --- /dev/null +++ b/packages/compiler/src/backend/llvm/shape-union-tags.test.ts @@ -0,0 +1,80 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile, deserializeModule } from "../../index.js"; +import { validateModule } from "../../ir/validate.js"; +import { emitLlvmModule } from "./emitter.js"; + +async function llvmFor(source: string): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-shape-union-tags-")); + try { + const entry = join(dir, "main.ts"), + output = join(dir, "main.ir.json"); + await writeFile(entry, source); + const result = await compile(entry, { outDir: dir, outPath: output, outputKind: "ir" }); + if (!result.ok) + throw new Error(result.diagnostics.map((item) => `${item.code}: ${item.message}`).join("\n")); + const module = deserializeModule(await readFile(output, "utf8")); + expect(validateModule(module)).toEqual([]); + return emitLlvmModule(module); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +function tagHelpers(ir: string): string[] { + return [...ir.matchAll(/^define internal i32 @sc_stag\.[^\n]*\{[^]*?^}/gm)].map( + (match) => match[0], + ); +} + +test("a union whose arms span few shape numbers switches on the shape word", async () => { + const ir = await llvmFor(` +type Num = { kind: "num"; value: number }; +type Neg = { kind: "neg"; operand: Node }; +type Add = { kind: "add"; left: Node; right: Node }; +type Node = Num | Neg | Add; +function evaluate(node: Node): number { + switch (node.kind) { + case "num": return node.value; + case "neg": return -evaluate(node.operand); + case "add": return evaluate(node.left) + evaluate(node.right); + } +} +const one: Node = { kind: "num", value: 1 }; +console.log(evaluate({ kind: "add", left: one, right: { kind: "neg", operand: one } })); +`); + expect(ir).not.toMatch(/@sc_stagtab\./); + const helpers = tagHelpers(ir); + expect(helpers.length).toBeGreaterThan(0); + for (const helper of helpers) expect(helper).toMatch(/switch i32 %w/); +}); + +test("a union whose arms span many shape numbers reads the arm from its table", async () => { + const arms = Array.from({ length: 40 }, (_, i) => i); + const ir = await llvmFor(` +${arms.map((i) => `type K${i} = { kind: "k${i}"; v${i}: number };`).join("\n")} +type Any = ${arms.map((i) => `K${i}`).join(" | ")}; +function value(item: Any): number { + switch (item.kind) { +${arms.map((i) => ` case "k${i}": return item.v${i};`).join("\n")} + } +} +const items: Any[] = [${arms.map((i) => `{ kind: "k${i}", v${i}: ${i} }`).join(", ")}]; +let total = 0; +for (const item of items) total += value(item); +console.log(total); +`); + // One byte per shape number from the arms' lowest to highest; 255 names no arm. + const table = + /^(@sc_stagtab\.\S+) = private unnamed_addr constant \[(\d+) x i8\] \[([^\]]*)\]$/m.exec(ir); + expect(table, "arm table").not.toBeNull(); + const entries = table![3]!.split(", ").map((entry) => Number(entry.replace("i8 ", ""))); + expect(entries).toHaveLength(Number(table![2])); + expect(entries.filter((entry) => entry !== 255).sort((a, b) => a - b)).toEqual(arms); + const helper = tagHelpers(ir).find((text) => text.includes(table![1]!)); + expect(helper, "tag helper").toBeDefined(); + expect(helper).toContain(`getelementptr inbounds [${table![2]} x i8], ptr ${table![1]}`); + expect(helper).not.toMatch(/switch i32 %w/); +}); diff --git a/packages/compiler/src/backend/llvm/shapes.test.ts b/packages/compiler/src/backend/llvm/shapes.test.ts index f7327a688..d9e5fa641 100644 --- a/packages/compiler/src/backend/llvm/shapes.test.ts +++ b/packages/compiler/src/backend/llvm/shapes.test.ts @@ -14,6 +14,7 @@ import { type ShapeHost, vAdapters, } from "./shapes.js"; +import { mangleRecordRelease } from "../mangle.js"; import { NullableUnions } from "./nullable-unions.js"; import { InlineUnionFields } from "./inline-unions.js"; @@ -109,10 +110,9 @@ describe("LLVM runtime RC symbols", () => { expect(releaseSym(host, { kind: "union", unionId: "u0" } as never)).toBe( "@sc_rc_release_union", ); - // Array retains inline; array releases stay calls. const array = { kind: "array", elem: { kind: "string" } } as never; expect(retainSym(host, array)).toBe("@sc_rc_retain_arr"); - expect(releaseSym(host, array)).toBe("@scr_arr_release"); + expect(releaseSym(host, array)).toBe("@sc_rc_release_arr"); expect(retainSym(host, { kind: "dyn" })).toBe("@sc_rc_retain_dyn"); expect(releaseSym(host, { kind: "dyn" })).toBe("@sc_rc_release_dyn"); // Unmirrored families and caught snapshots keep their runtime entry points. @@ -124,6 +124,7 @@ describe("LLVM runtime RC symbols", () => { expect(declarations).toContain("declare void @scr_str_release(ptr)"); expect(declarations).toContain("declare void @scr_cyc_on_release(ptr)"); expect(inlineRcDecls(host).sort()).toEqual([ + "declare void @scr_arr_release(ptr)", "declare void @scr_cyc_on_release(ptr)", "declare void @scr_dyn_release(ptr)", "declare void @scr_dyn_release_survivor(ptr)", @@ -145,11 +146,19 @@ describe("LLVM runtime RC symbols", () => { // The dyn fast path buffers surviving values by kind, as scr_dyn_release does. const ir = emitInlineRcHelpers(host).join("\n"); expect(ir).toContain("call void @scr_dyn_release_survivor(ptr %o)"); - expect(ir).toMatch( - /switch i32 %kind, label %done \[ i32 4, label %container i32 5, label %container i32 8, label %root/, + // Every kind that can be a candidate takes one path; scalars stay inline. + expect(ir).toContain( + "switch i32 %kind, label %done [ i32 4, label %candidate i32 5, label %candidate i32 8, label %candidate i32 9, label %candidate i32 12, label %candidate i32 13, label %candidate ]", ); - // Arrays and objects not yet buffered ask the runtime, which skips intact documents. + // Values not yet buffered ask the runtime, which decides per value. expect(ir).toMatch(/br i1 %cqueued, label %done, label %survivor/); + // The runtime buffers dyn candidates, so the dyn helper has no inline append. + const dynHelper = ir.slice( + ir.indexOf("@sc_rc_release_dyn("), + ir.indexOf("\n}", ir.indexOf("@sc_rc_release_dyn(")), + ); + expect(dynHelper).not.toContain("sc_cyc_append"); + expect(dynHelper).not.toContain("scr_cyc_on_release"); }); test("sanitized builds call the fast-path helpers out of line", () => { @@ -207,36 +216,42 @@ describe("LLVM runtime RC symbols", () => { }); test("optimized builds buffer candidates through the module's append helper", () => { - const emit = (inlineRc: true | "release", extra: Partial) => { + const emit = (inlineRc: true | "release", extra: Partial, kind: "union" | "dyn") => { const { host } = declarationHost(64, inlineRc); const configured: ShapeHost = { ...host, inlineAlloc: true, ...extra }; - releaseSym(configured, { kind: "dyn" }); + releaseSym(configured, kind === "union" ? ({ kind, unionId: "u0" } as never) : { kind }); return { ir: emitInlineRcHelpers(configured).join("\n"), decls: inlineRcDecls(configured), }; }; + const { ir, decls } = emit(true, {}, "union"); + expect(ir).toContain("call preserve_mostcc void @sc_cyc_append(ptr %o)"); + expect(ir).toContain("define internal preserve_mostcc void @sc_cyc_append(ptr %o) noinline"); + // The young test reads the generation byte at obj-5; the append fills + // scr_cyc_buffers' nursery buffer and the header's position at obj-4. + expect(ir).toContain("getelementptr i8, ptr %o, i64 -5"); + expect(ir).toContain("getelementptr inbounds i8, ptr @scr_cyc_buffers, i64 96"); + expect(ir).toContain("getelementptr inbounds i8, ptr @scr_cyc_buffers, i64 104"); + expect(ir).toContain("load i64, ptr @scr_cyc_live"); + expect(ir).toContain("or i32 %i, -2147483648"); + expect(ir).toContain("call void @scr_cyc_on_release(ptr %o)"); + expect(decls).toContain( + "@scr_cyc_buffers = external global { [3 x { ptr, i64, i64, i64 }], i64, i64 }", + ); + expect(decls).toContain("@scr_cyc_live = external global i64"); + // The dyn release buffers through scr_dyn_release_survivor, so a module + // whose only headered release is dyn's has no append helper. for (const mode of [true, "release"] as const) { - const { ir, decls } = emit(mode, {}); - expect(ir).toContain("call preserve_mostcc void @sc_cyc_append(ptr %o)"); - expect(ir).toContain("define internal preserve_mostcc void @sc_cyc_append(ptr %o) noinline"); - // The young test reads the generation byte at obj-5; the append fills - // scr_cyc_buffers' nursery buffer and the header's position at obj-4. - expect(ir).toContain("getelementptr i8, ptr %o, i64 -5"); - expect(ir).toContain("getelementptr inbounds i8, ptr @scr_cyc_buffers, i64 96"); - expect(ir).toContain("getelementptr inbounds i8, ptr @scr_cyc_buffers, i64 104"); - expect(ir).toContain("load i64, ptr @scr_cyc_live"); - expect(ir).toContain("or i32 %i, -2147483648"); - expect(ir).toContain("call void @scr_cyc_on_release(ptr %o)"); - expect(decls).toContain( - "@scr_cyc_buffers = external global { [3 x { ptr, i64, i64, i64 }], i64, i64 }", - ); - expect(decls).toContain("@scr_cyc_live = external global i64"); + const dyn = emit(mode, {}, "dyn"); + expect(dyn.ir).not.toContain("sc_cyc_append"); + expect(dyn.ir).not.toContain("scr_cyc_on_release"); + expect(dyn.decls.join("\n")).not.toContain("scr_cyc_buffers"); } // Worker executables and targets without inline allocation call // scr_cyc_on_release, which tries the same append first. for (const extra of [{ threadLocalAlloc: true }, { inlineAlloc: false }]) { - const { ir, decls } = emit(true, extra); + const { ir, decls } = emit(true, extra, "union"); expect(ir).not.toContain("sc_cyc_append"); expect(ir).toContain("call void @scr_cyc_on_release(ptr %o)"); expect(decls.join("\n")).not.toContain("scr_cyc_buffers"); @@ -279,6 +294,22 @@ describe("inline release budgets", () => { ); }); + test("each family's budget applies to its own sites in a function that has both", () => { + const strings = body("mixed", INLINE_STR_RELEASES_PER_FUNCTION + 1, "str"); + // One function: string sites over their budget, dyn sites within theirs. + const mixed = strings.replace( + " ret void", + ` call void @sc_rc_release_dyn(ptr %v)\n call void @sc_rc_release_other(ptr %v)\n ret void`, + ); + const other = body("other", 2, "str"); + const limited = limitInlineReleases([mixed, other], "release").join("\n"); + const out = limited.slice(0, limited.indexOf("@other(")); + expect(out).not.toContain("@sc_rc_release_str("); + expect(out).toContain("call void @sc_rc_release_dyn(ptr %v)"); + expect(out).toContain("call void @sc_rc_release_other(ptr %v)"); + expect(limited).toContain(other); + }); + test("a function over the string budget calls the runtime string release at every site", () => { const small = body("small", INLINE_STR_RELEASES_PER_FUNCTION, "str"); const large = body("large", INLINE_STR_RELEASES_PER_FUNCTION + 1, "str"); @@ -315,6 +346,42 @@ describe("emitted object releases", () => { root.indexOf("call void @scr_cyc_on_release(ptr %o)"), ); }); + + test("freezable records count their references without the frozen bit", () => { + const mod: IrModule = { + irVersion: 17, + sourceFile: "frozen.ts", + entry: "main", + functions: [], + records: [ + { id: "plain", fields: [{ name: "n", type: F64 }] }, + { id: "frozen", fields: [{ name: "n", type: F64 }], freezable: true }, + ], + }; + const body = (defs: string, name: string): string => + new RegExp(`^define internal void @${name}\\([^]*?^}`, "m").exec(defs)?.[0] ?? ""; + for (const [pointerBits, notFrozen, sharedBelow] of [ + [64, "-4611686018427387905", "-4611686018427387907"], + [32, "-1073741825", "-1073741827"], + ] as const) { + const { host } = declarationHost(pointerBits); + const defs = emitRecordShapes({ ...host, objectAudit: false }, mod).defs.join("\n"); + const S = pointerBits === 64 ? "i64" : "i32"; + const plain = body(defs, mangleRecordRelease("plain")); + expect(plain).toContain(`%shared = icmp ult ${S} %rcm2, -3`); + expect(plain).not.toContain("%refs"); + // The immortal word's count is ~FROZEN, which the compare sends to the + // immortal test, like counts 0 and 1. + const fast = body(defs, mangleRecordRelease("frozen")); + expect(fast).toContain(`%refs = and ${S} %rc, ${notFrozen}`); + expect(fast).toContain(`%rcm2 = sub ${S} %refs, 2`); + expect(fast).toContain(`%shared = icmp ult ${S} %rcm2, ${sharedBelow}`); + expect(fast).toContain(`%imm = icmp eq ${S} %rc, -1`); + const slow = body(defs, `${mangleRecordRelease("frozen")}_slow`); + expect(slow).toContain(`%refs = and ${S} %n, ${notFrozen}`); + expect(slow).toContain(`%dead = icmp eq ${S} %refs, 0`); + } + }); }); describe("live-object audit notes", () => { diff --git a/packages/compiler/src/backend/llvm/shapes.ts b/packages/compiler/src/backend/llvm/shapes.ts index d13f80a78..ea429d71a 100644 --- a/packages/compiler/src/backend/llvm/shapes.ts +++ b/packages/compiler/src/backend/llvm/shapes.ts @@ -34,6 +34,8 @@ import { cycTypeDescriptor, emitObjectAlloc, emitObjectFree, + emitSegmentFree, + type FreeObservers, CYC_APPEND, cycleAppendDecls, cycleAppendHelper, @@ -215,8 +217,9 @@ export interface RegionHooks { classDefinitions(cls: IrClassDef, sizeOf: string): string[]; } -/** Every emitted function/helper carries #0 = { sanitize_address } — see - * the emitter header. */ +/** Every emitted function/helper carries attribute group #0: the build's + * target CPU level, and `sanitize_address` in sanitized builds (see + * emit-module.ts functionAttributes). */ export const FN_ATTRS = "#0"; export { computeTraced } from "../cycle-analysis.js"; @@ -232,7 +235,7 @@ export { computeTraced } from "../cycle-analysis.js"; * `_v`-shaped. */ export function vAdapters(host: ShapeHost, t: IrType): { retain: string; release: string } { const nullable = host.nullableUnions.of(t); - if (nullable?.repr === "shapes") return shapeUnionRc(nullable); + if (nullable?.repr === "shapes") return shapeUnionRc(host, nullable); if (nullable) { const arm = vAdapters(host, nullable.arm); return { retain: nullableRetainSym(host, nullable.arm), release: arm.release }; @@ -289,7 +292,7 @@ export function vAdapters(host: ShapeHost, t: IrType): { retain: string; release export function retainSym(host: ShapeHost, t: IrType): string { if (t.kind === "jsval") return jsvalRcSym(host, "retain"); const nullable = host.nullableUnions.of(t); - if (nullable?.repr === "shapes") return shapeUnionRc(nullable).retain; + if (nullable?.repr === "shapes") return shapeUnionRc(host, nullable).retain; if (nullable) return nullableRetainSym(host, nullable.arm); const stem = runtimeRcStem(t); if (host.rcHelpers !== null && stem !== null && inlineRcFamily(host, stem, "retain") !== null) { @@ -304,26 +307,53 @@ export function retainSym(host: ShapeHost, t: IrType): string { /** A shape union's RC entry points: every record retain is the same * NULL- and immortal-skipping increment (the null sentinel is immortal), * so the first arm's serves; release dispatches on the arm. */ -function shapeUnionRc(union: ShapeUnion): { retain: string; release: string } { +function shapeUnionRc(host: ShapeHost, union: ShapeUnion): { retain: string; release: string } { return { retain: `@${mangleRecordRetain(union.arms[0]!.shapeId)}`, - release: shapeReleaseFn(union.unionId), + release: shapeReleaseFn(host.nullableUnions.helperOwner(union.unionId)), }; } +/** The widest span of shape numbers a union's arms may cover for its tag + * helper to switch on the shape word (emitShapeUnionHelpers). */ +const SHAPE_TAG_SWITCH_SPAN = 32; + /** The tag, release and trace helpers of every shape union * (nullable-unions.ts). The tag helper maps a value to its arm: NULL and * the null sentinel are the unit arms, anything else a tagged record whose - * shape word names its arm (the switch folds to a lookup table). */ + * shape word names its arm. + * - Arms within SHAPE_TAG_SWITCH_SPAN shape numbers: the helper switches on + * the shape word. Inlined, the switch folds into the caller's branch on + * the arm, one indirect jump with a jump table of at most that many + * entries per site. + * - Wider unions: the shape word indexes the union's arm table, one entry + * per shape number from the arms' lowest to highest, and callers branch + * on the small arm tag. Folded switches would give every site of such a + * union a jump table spanning all of its arms' shape numbers. */ export function emitShapeUnionHelpers(host: ShapeHost): string[] { const out: string[] = []; const S = host.sizeType; for (const union of host.nullableUnions.allShapeUnions()) { const header = shapeWordHeader(S); const tagFn = shapeTagFn(union.unionId); - const cases = union.arms - .map((arm) => `i32 ${host.nullableUnions.shapeNumber(arm.shapeId)}, label %a${arm.tag}`) - .join(" "); + const numbers = union.arms.map((arm) => host.nullableUnions.shapeNumber(arm.shapeId)); + const lo = Math.min(...numbers); + const span = Math.max(...numbers) - lo + 1; + const byTable = span > SHAPE_TAG_SWITCH_SPAN; + // Entries are bytes, or 16-bit words for a union of 255 arms or more. + // A shape number that names no arm maps to the all-ones entry, which is + // above every arm tag, so callers take their path for no arm as they do + // for the -1 of a shape number outside the table. + const wide = union.arms.some((arm) => arm.tag >= 255); + const entry = wide ? "i16" : "i8"; + const none = wide ? 65535 : 255; + const table = new Array(span).fill(none); + union.arms.forEach((arm, i) => (table[numbers[i]! - lo] = arm.tag)); + const tableName = tagFn.replace("@sc_stag.", "@sc_stagtab."); + if (byTable) + out.push( + `${tableName} = private unnamed_addr constant [${span} x ${entry}] [${table.map((t) => `${entry} ${t}`).join(", ")}]`, + ); out.push( `define internal i32 ${tagFn}(ptr %u) alwaysinline ${FN_ATTRS} { ; shape union tag`, `entry:`, @@ -341,48 +371,110 @@ export function emitShapeUnionHelpers(host: ShapeHost): string[] { ` ret i32 ${union.nullTag}`, `rec:`, ); - out.push( - ` %wp = getelementptr inbounds ${header}, ptr %u, i32 0, i32 ${SHAPE_WORD_INDEX}`, + const cases = union.arms.map((arm, i) => `i32 ${numbers[i]}, label %a${arm.tag}`).join(" "); + if (!byTable) + out.push( + ` %wp = getelementptr inbounds ${header}, ptr %u, i32 0, i32 ${SHAPE_WORD_INDEX}`, + ` %w = load i32, ptr %wp`, + ` switch i32 %w, label %bad [ ${cases} ]`, + ...union.arms.flatMap((arm) => [`a${arm.tag}:`, ` ret i32 ${arm.tag}`]), + `bad:`, + ` ret i32 -1`, + `}`, + ``, + ); + else + out.push( + ` %wp = getelementptr inbounds ${header}, ptr %u, i32 0, i32 ${SHAPE_WORD_INDEX}`, + ` %w = load i32, ptr %wp`, + ` %i = sub i32 %w, ${lo}`, + ` %inrange = icmp ult i32 %i, ${span}`, + ` br i1 %inrange, label %look, label %bad`, + `look:`, + ` %tp = getelementptr inbounds [${span} x ${entry}], ptr ${tableName}, i32 0, i32 %i`, + ` %t8 = load ${entry}, ptr %tp`, + ` %tag = zext ${entry} %t8 to i32`, + ` ret i32 %tag`, + `bad:`, + ` ret i32 -1`, + `}`, + ``, + ); + // Release and trace: one definition for the unions over the same + // record arms (helperOwner). Unit encodings are NULL or the immortal + // null sentinel; the shape word picks the arm. + if (host.nullableUnions.helperOwner(union.unionId) !== union.unionId) continue; + const units = [ + ` %isnull = icmp eq ptr %o, null`, + ` br i1 %isnull, label %done, label %${host.nullableUnions.usesNullSentinel ? "notnull" : "rec"}`, + ...(host.nullableUnions.usesNullSentinel + ? [ + `notnull:`, + ` %isnullarm = icmp eq ptr %o, ${NULLABLE_NULL}`, + ` br i1 %isnullarm, label %done, label %rec`, + ] + : []), + `rec:`, + ]; + const armSwitch = (call: (shapeId: string) => string): string[] => [ + ` %wp = getelementptr inbounds ${header}, ptr %o, i32 0, i32 ${SHAPE_WORD_INDEX}`, ` %w = load i32, ptr %wp`, - ` switch i32 %w, label %bad [ ${cases} ]`, - ...union.arms.flatMap((arm) => [`a${arm.tag}:`, ` ret i32 ${arm.tag}`]), - `bad:`, - ` ret i32 -1`, - `}`, - ``, - ); - // release: unit encodings are NULL or immortal; each arm releases - // through its own (NULL- and immortal-tolerant) helper. - out.push( - `define internal void ${shapeReleaseFn(union.unionId)}(ptr %u) ${FN_ATTRS} { ; shape union release`, - `entry:`, - ` %t = call i32 ${tagFn}(ptr %u)`, - ` switch i32 %t, label %done [ ${union.arms.map((arm) => `i32 ${arm.tag}, label %a${arm.tag}`).join(" ")} ]`, - ...union.arms.flatMap((arm) => [ - `a${arm.tag}:`, - ` call void @${mangleRecordRelease(arm.shapeId)}(ptr %u)`, - ` br label %done`, - ]), + ` switch i32 %w, label %done [ ${union.arms.map((arm) => `i32 ${host.nullableUnions.shapeNumber(arm.shapeId)}, label %a${arm.tag}`).join(" ")} ]`, + ...union.arms.flatMap((arm) => [`a${arm.tag}:`, call(arm.shapeId), ` br label %done`]), `done:`, ` ret void`, `}`, ``, - ); + ]; + // Every record arm shares the release fast path (the count at the same + // offset, one collector header choice for the whole union), so only a + // release that may free dispatches on the arm. The release stays a call: + // inlined at every release site, it grows code more than it saves. + const traced = union.arms.map((arm) => host.tracedShapes.has(`record:${arm.shapeId}`)); + // A freezable arm's count is the word without its frozen bit, as in + // releaseFastPath: a frozen record's last reference takes the slow path. + const frozen = union.arms.some((arm) => host.recordsById.get(arm.shapeId)?.freezable === true); + const notFrozen = ~recordFrozenBit(S); + const release = shapeReleaseFn(union.unionId); + if (traced.every((t) => t === traced[0])) + out.push( + `define internal void ${release}(ptr %o) noinline ${FN_ATTRS} { ; shape union release`, + `entry:`, + ...units, + ` %rc = load ${S}, ptr %o`, + ...(frozen + ? [ + ` %refs = and ${S} %rc, ${sizeLiteral(S, notFrozen)}`, + ` %rcm2 = sub ${S} %refs, 2`, + ` %shared = icmp ult ${S} %rcm2, ${sizeLiteral(S, notFrozen - 2n)}`, + ] + : [` %rcm2 = sub ${S} %rc, 2`, ` %shared = icmp ult ${S} %rcm2, -3`]), + ` br i1 %shared, label %dec, label %slow`, + `dec:`, + ` %n = sub ${S} %rc, 1`, + ` store ${S} %n, ptr %o`, + ...(traced[0] ? cycleRootLines(host, "done") : [` br label %done`]), + `slow:`, + ` %imm = icmp eq ${S} %rc, -1`, + ` br i1 %imm, label %done, label %free`, + `free:`, + ...armSwitch((shapeId) => ` call void @${mangleRecordRelease(shapeId)}_slow(ptr %o)`), + ); + else + out.push( + `define internal void ${release}(ptr %o) ${FN_ATTRS} { ; shape union release`, + `entry:`, + ...units, + ...armSwitch((shapeId) => ` call void @${mangleRecordRelease(shapeId)}(ptr %o)`), + ); if (shapeUnionTraced(host, union)) out.push( - `define internal void ${shapeTraceFn(union.unionId)}(ptr %u, ptr %visit, ptr %ctx) ${FN_ATTRS} { ; shape union trace`, + `define internal void ${shapeTraceFn(union.unionId)}(ptr %o, ptr %visit, ptr %ctx) ${FN_ATTRS} { ; shape union trace`, `entry:`, - ` %t = call i32 ${tagFn}(ptr %u)`, - ` switch i32 %t, label %done [ ${union.arms.map((arm) => `i32 ${arm.tag}, label %a${arm.tag}`).join(" ")} ]`, - ...union.arms.flatMap((arm) => [ - `a${arm.tag}:`, - ` call void @${mangleRecordTrace(arm.shapeId)}(ptr %u, ptr %visit, ptr %ctx)`, - ` br label %done`, - ]), - `done:`, - ` ret void`, - `}`, - ``, + ...units, + ...armSwitch( + (shapeId) => ` call void @${mangleRecordTrace(shapeId)}(ptr %o, ptr %visit, ptr %ctx)`, + ), ); } return out; @@ -434,7 +526,7 @@ export function emitNullableRetainWrappers(host: ShapeHost): string[] { export function releaseSym(host: ShapeHost, t: IrType): string { if (t.kind === "jsval") return jsvalRcSym(host, "release"); const nullable = host.nullableUnions.of(t); - if (nullable?.repr === "shapes") return shapeUnionRc(nullable).release; + if (nullable?.repr === "shapes") return shapeUnionRc(host, nullable).release; if (nullable) return releaseSym(host, nullable.arm); const stem = runtimeRcStem(t); if (host.rcHelpers !== null && stem !== null && inlineRcFamily(host, stem, "release") !== null) { @@ -545,21 +637,21 @@ interface InlineRcFamily { * repeats its own checks, so the fast path never duplicates it. * * The dyn release buffers a surviving value by kind (scr_dyn_release): - * the fast path decides the always and never cases and calls - * scr_dyn_release_survivor for kinds whose candidacy is per value, - * including arrays and objects of intact documents (ScrDynRegion). + * the fast path skips scalars and values already in a candidate buffer or + * never candidates, and calls scr_dyn_release_survivor for the rest, which + * decides per value (intact documents, ScrDynRegion; capsules and handles + * without edges). * - * Releases stay plain calls for: - * - arr: array releases are the most numerous release sites and the - * least hot (at most 1% self time across the benchmark suite), and - * inlining them roughly doubled the code growth for no measured speedup. - * Array retains are inlined. */ + * The array release inlines the decrement of an array that stays alive + * and is not headered (`a->cyc`): those are most array releases in + * type-checker workloads, where scr_arr_release's own prologue and checks + * took about half of its time. */ export const INLINE_RC_FAMILIES: Readonly> = { str: { cycle: "none", retainNull: false, release: "scr_str_release" }, bytes: { cycle: "none", retainNull: false, release: "scr_bytes_release" }, regex: { cycle: "none", retainNull: false, release: "scr_regex_release" }, bigint: { cycle: "none", retainNull: true, release: "scr_bigint_release" }, - arr: { cycle: "arr", retainNull: false, release: null }, + arr: { cycle: "arr", retainNull: false, release: "scr_arr_release" }, map: { cycle: "map", retainNull: false, release: "scr_map_release" }, union: { cycle: "always", retainNull: false, release: "scr_union_release" }, closure: { cycle: "always", retainNull: false, release: "scr_closure_release" }, @@ -655,9 +747,10 @@ function requestRcHelper(host: ShapeHost, family: string, op: RcOp): string { for (const decl of rcHelperDecls(family, op)) host.declare(decl); // Its body is written last (emitInlineRcHelpers), after the requests are // collected: a release that can buffer a candidate requests the append - // helper now. The jsval row (JS_RC_ROW) has no collector header; its - // cells release through dyn's helper, which makes its own request. - if (op === "release" && row.cycle === "always") requestCycleAppend(host); + // helper now. The dyn release buffers through scr_dyn_release_survivor + // instead. The jsval row (JS_RC_ROW) has no collector header; its cells + // release through dyn's helper. + if (op === "release" && row.cycle === "always" && family !== "dyn") requestCycleAppend(host); return `@${inlineRcName(family, op)}`; } @@ -691,34 +784,32 @@ export function limitInlineReleases( parts: readonly string[], mode: InlineRcMode | undefined, ): string[] { - const dyn = limitInlineReleaseFamily(parts, "dyn", INLINE_DYN_RELEASES_PER_FUNCTION); - return mode === "release" - ? limitInlineReleaseFamily(dyn, "str", INLINE_STR_RELEASES_PER_FUNCTION) - : dyn; -} - -function limitInlineReleaseFamily( - parts: readonly string[], - family: "dyn" | "str", - max: number, -): string[] { - const call = `call void @${inlineRcName(family, "release")}(`; - const runtime = `call void @${INLINE_RC_FAMILIES[family]!.release}(`; + const families: readonly ("dyn" | "str")[] = mode === "release" ? ["dyn", "str"] : ["dyn"]; + const limits = families.map((family) => + family === "dyn" ? INLINE_DYN_RELEASES_PER_FUNCTION : INLINE_STR_RELEASES_PER_FUNCTION, + ); + const calls = families.map((family) => `${inlineRcName(family, "release")}(`); + const runtimes = families.map((family) => `call void @${INLINE_RC_FAMILIES[family]!.release}(`); + const head = "call void @"; + const prefix = `${head}${inlineRcName("", "release")}`; // The function each release site belongs to: the last `define` line - // before it, counted over the joined text. + // before it, counted over the joined text. One walk over each part finds + // the sites of every family. const sites = ( part: string, - each: (offset: number, fn: number) => void, + each: (offset: number, family: number, fn: number) => void, first: number, ): number => { let fn = first; let define = part.startsWith("define ") ? 0 : part.indexOf("\ndefine "); - for (let at = part.indexOf(call); at >= 0; at = part.indexOf(call, at + call.length)) { + for (let at = part.indexOf(prefix); at >= 0; at = part.indexOf(prefix, at + prefix.length)) { + const family = calls.findIndex((call) => part.startsWith(call, at + head.length)); + if (family < 0) continue; while (define >= 0 && define < at) { fn++; define = part.indexOf("\ndefine ", define + 1); } - each(at, fn); + each(at, family, fn); } while (define >= 0) { fn++; @@ -726,29 +817,47 @@ function limitInlineReleaseFamily( } return fn; }; - const counts = new Map(); + const counts = families.map(() => new Map()); + // The functions each part's sites can belong to. + const spans: number[] = []; let fn = 0; - for (const part of parts) fn = sites(part, (_, f) => counts.set(f, (counts.get(f) ?? 0) + 1), fn); - const over = new Set([...counts].filter(([, n]) => n > max).map(([f]) => f)); - if (over.size === 0) return [...parts]; - fn = 0; - return parts.map((part) => { - const cuts: number[] = []; + for (const part of parts) { + spans.push(fn); fn = sites( part, - (at, f) => { - if (over.has(f)) cuts.push(at); - }, + (_, family, f) => counts[family]!.set(f, (counts[family]!.get(f) ?? 0) + 1), fn, ); + spans.push(fn); + } + const over = counts.map( + (count, family) => new Set([...count].filter(([, n]) => n > limits[family]!).map(([f]) => f)), + ); + if (over.every((set) => set.size === 0)) return [...parts]; + const anyOver = new Set(over.flatMap((set) => [...set])); + const touches = (from: number, to: number): boolean => { + for (let f = from; f <= to; f++) if (anyOver.has(f)) return true; + return false; + }; + return parts.map((part, index) => { + const from = spans[2 * index]!; + if (!touches(from, spans[2 * index + 1]!)) return part; + const cuts: { at: number; family: number }[] = []; + sites( + part, + (at, family, f) => { + if (over[family]!.has(f)) cuts.push({ at, family }); + }, + from, + ); if (cuts.length === 0) return part; let out = ""; - let from = 0; - for (const at of cuts) { - out += part.slice(from, at) + runtime; - from = at + call.length; + let next = 0; + for (const cut of cuts) { + out += part.slice(next, cut.at) + runtimes[cut.family]!; + next = cut.at + head.length + calls[cut.family]!.length; } - return out + part.slice(from); + return out + part.slice(next); }); } @@ -761,8 +870,11 @@ function rcHelperDecls(family: string, op: RcOp): string[] { if (op === "retain" || row.release === null) return []; return [ `declare void @${row.release}(ptr)`, - ...(row.cycle === "always" ? [`declare void @scr_cyc_on_release(ptr)`] : []), - ...(family === "dyn" ? [`declare void @scr_dyn_release_survivor(ptr)`] : []), + ...(family === "dyn" + ? [`declare void @scr_dyn_release_survivor(ptr)`] + : row.cycle === "always" + ? [`declare void @scr_cyc_on_release(ptr)`] + : []), ]; } @@ -872,21 +984,21 @@ function releaseHelper(host: ShapeHost, family: string, row: InlineRcFamily): st // scr_dyn_release buffers a surviving value by kind: always for // functions and proxies, per value for arrays and objects (not // while their document is intact, ScrDynRegion), capsules and - // handles (scr_dyn_release_survivor), never for scalars. An - // array or object already in a candidate buffer stays inline. + // handles, never for scalars. Every kind that can be buffered + // takes one path, which keeps each inlined site small: a value + // already in a candidate buffer (or never one) stays inline, and + // scr_dyn_release_survivor decides the rest. ` %kp = getelementptr i8, ptr %o, ${S} ${S === "i64" ? 8 : 4} ; ScrDyn.kind`, ` %kind = load i32, ptr %kp`, - // SCR_DYN_ARR 4, OBJ 5, FUNC 8, PROXY 13; HANDLE 9, TYPED_REF 12. - ` switch i32 %kind, label %done [ i32 4, label %container i32 5, label %container i32 8, label %root i32 13, label %root i32 9, label %survivor i32 12, label %survivor ]`, - `container:`, + // SCR_DYN_ARR 4, OBJ 5, FUNC 8, HANDLE 9, TYPED_REF 12, PROXY 13. + ` switch i32 %kind, label %done [ i32 4, label %candidate i32 5, label %candidate i32 8, label %candidate i32 9, label %candidate i32 12, label %candidate i32 13, label %candidate ]`, + `candidate:`, ` %cbufp = getelementptr i8, ptr %o, ${S} -${host.cycleBufOffset}`, ` %cbuf = load i32, ptr %cbufp`, ` %cqueued = icmp slt i32 %cbuf, 0`, ` br i1 %cqueued, label %done, label %survivor`, `survivor:`, ` call void @scr_dyn_release_survivor(ptr %o)`, - ` br label %done`, - `root:`, ] : family === "box" ? [ @@ -905,7 +1017,9 @@ function releaseHelper(host: ShapeHost, family: string, row: InlineRcFamily): st `root:`, ] : []), - ...(cycle === "always" ? cycleRootLines(host, "done") : [` br label %done`]), + ...(cycle === "always" && family !== "dyn" + ? cycleRootLines(host, "done") + : [` br label %done`]), `done:`, ` ret void`, `}`, @@ -969,7 +1083,9 @@ export function traceAdapter(host: ShapeHost, t: IrType): string | null { case "union": { const nullable = host.nullableUnions.get(t.unionId); if (nullable?.repr === "shapes") - return shapeUnionTraced(host, nullable) ? shapeTraceFn(nullable.unionId) : null; + return shapeUnionTraced(host, nullable) + ? shapeTraceFn(host.nullableUnions.helperOwner(nullable.unionId)) + : null; if (nullable) return traceAdapter(host, nullable.arm); if (!host.tracedUnions.has(t.unionId)) return null; host.declare(`declare void @scr_union_trace_v(ptr, ptr, ptr)`); @@ -1083,7 +1199,33 @@ export function arrNewCall( * live in its own block): capText must be "0". */ small = false, ): string { - const useRef = + const useRef = arrUsesRef(host, elem); + if (!useRef) { + if (small) { + host.declare(`declare ptr @scr_arr_new_small(i32)`); + return `call ptr @scr_arr_new_small(i32 ${elemKindNum(elem)})`; + } + host.declare(`declare ptr @scr_arr_new(i32, ${host.sizeType})`); + return `call ptr @scr_arr_new(i32 ${elemKindNum(elem)}, ${host.sizeType} ${capText})`; + } + if (small) { + host.declare(`declare ptr @scr_arr_new_ops_small(ptr)`); + return `call ptr @scr_arr_new_ops_small(ptr ${arrOps(host, elem)})`; + } + host.declare(`declare ptr @scr_arr_new_ops(ptr, ${host.sizeType})`); + return `call ptr @scr_arr_new_ops(ptr ${arrOps(host, elem)}, ${host.sizeType} ${capText})`; +} + +/** The ScrElemKind and ops descriptor (or `null`) an array of `elem` + * starts with: the arrays arrNewCall constructs, for the instances of a + * class that extends Array (scr_arr_subclass_init). */ +export function arrElemInit(host: ShapeHost, elem: IrType): { kind: number; ops: string } { + if (!arrUsesRef(host, elem)) return { kind: elemKindNum(elem), ops: "null" }; + return { kind: host.compactArrayElem?.(elem) === true ? 6 : 5, ops: arrOps(host, elem) }; +} + +function arrUsesRef(host: ShapeHost, elem: IrType): boolean { + return ( elem.kind === "record" || elem.kind === "object" || elem.kind === "union" || @@ -1099,21 +1241,9 @@ export function arrNewCall( elem.kind === "stats" || // fs.Stats snapshots: scr_stats_* adapters, no trace elem.kind === "dyn" || // native collection seeds/drains: traced REF elem.kind === "regex" || // RegExp values: scr_regex_* adapters, no trace (no refs inside) - (elem.kind === "array" && traceAdapter(host, elem) !== null); - if (!useRef) { - if (small) { - host.declare(`declare ptr @scr_arr_new_small(i32)`); - return `call ptr @scr_arr_new_small(i32 ${elemKindNum(elem)})`; - } - host.declare(`declare ptr @scr_arr_new(i32, ${host.sizeType})`); - return `call ptr @scr_arr_new(i32 ${elemKindNum(elem)}, ${host.sizeType} ${capText})`; - } - if (small) { - host.declare(`declare ptr @scr_arr_new_ops_small(ptr)`); - return `call ptr @scr_arr_new_ops_small(ptr ${arrOps(host, elem)})`; - } - host.declare(`declare ptr @scr_arr_new_ops(ptr, ${host.sizeType})`); - return `call ptr @scr_arr_new_ops(ptr ${arrOps(host, elem)}, ${host.sizeType} ${capText})`; + elem.kind === "date" || // Date objects: scr_date_* adapters, no trace (no refs inside) + (elem.kind === "array" && traceAdapter(host, elem) !== null) + ); } /** The %ScrArrOps descriptor (retain, release, trace, registry id) of a REF @@ -1162,7 +1292,6 @@ const SCR_ARR_OPS_RELEASE_NOTHING = 2; export function boxNewCall(host: ShapeHost, t: IrType): string { const plain: Partial> = { f64: 0, - date: 0, procStream: 0, bool: 1, string: 2, @@ -1187,11 +1316,7 @@ export function boxNewCall(host: ShapeHost, t: IrType): string { /** Box accessor suffix (boxAccess): scalars unboxed, ref kinds pointers. */ export function boxAccess(t: IrType): "f64" | "bool" | "ref" { - return t.kind === "f64" || t.kind === "date" || t.kind === "procStream" - ? "f64" - : t.kind === "bool" - ? "bool" - : "ref"; + return t.kind === "f64" || t.kind === "procStream" ? "f64" : t.kind === "bool" ? "bool" : "ref"; } /* ── record shapes ────────────────────────────────────────────────────── */ @@ -1207,7 +1332,6 @@ export function llFieldType( if (POINTER_KINDS.has(t.kind)) return "ptr"; switch (t.kind) { case "f64": - case "date": case "procStream": return "double"; case "bool": @@ -1405,17 +1529,23 @@ export interface DirectDestroy { /** The release of child `value` (a loaded field, possibly NULL) inside a * teardown whose chain length is `%d`: the child's own release for a shared * or immortal reference, its teardown for the last one. Labels use `tag`; - * the lines end in the open block `${tag}.next`. */ + * the lines end in the open block `${tag}.next`. + * + * With `segment` (the child has the teardown's own type, in a segmented + * teardown; segmentPrologueLines), the child's teardown appends its blocks + * to the teardown's segment. */ export function directChildReleaseLines( host: ShapeHost, tag: string, value: string, target: DirectDestroy, comment: string, + segment = false, ): string[] { const S = host.sizeType; const C = target.count ?? S; - host.declare(`declare void @scr_rc_destroy(ptr, ptr)`); + if (segment) host.declare(`declare ptr @scr_rc_destroy_seg(ptr, ptr, ptr, ptr)`); + else host.declare(`declare void @scr_rc_destroy(ptr, ptr)`); const lines = [ ` %${tag}.nn = icmp ne ptr ${value}, null`, ` br i1 %${tag}.nn, label %${tag}.chk, label %${tag}.next`, @@ -1447,16 +1577,139 @@ export function directChildReleaseLines( ` br i1 %${tag}.short, label %${tag}.direct, label %${tag}.bounded`, `${tag}.direct:`, ` %${tag}.d = add i32 %d, 1`, - ` call void @${target.destroy}_d(ptr ${value}, i32 %${tag}.d)`, + ); + if (!segment) { + lines.push( + ` call void @${target.destroy}_d(ptr ${value}, i32 %${tag}.d)`, + ` br label %${tag}.next`, + `${tag}.bounded:`, + ` call void @scr_rc_destroy(ptr ${value}, ptr @${target.destroy})`, + ` br label %${tag}.next`, + `${tag}.next:`, + ); + return lines; + } + lines.push( + ` %${tag}.t = load ptr, ptr %sg.t`, + ` %${tag}.dt = call ptr @${target.destroy}_s(ptr ${value}, i32 %${tag}.d, ptr %${tag}.t)`, + ` store ptr %${tag}.dt, ptr %sg.t`, ` br label %${tag}.next`, `${tag}.bounded:`, - ` call void @scr_rc_destroy(ptr ${value}, ptr @${target.destroy})`, + ` %${tag}.bt = load ptr, ptr %sg.t`, + ` %${tag}.rt = call ptr @scr_rc_destroy_seg(ptr ${value}, ptr @${target.destroy}_sc, ptr @${target.destroy}, ptr %${tag}.bt)`, + ` store ptr %${tag}.rt, ptr %sg.t`, ` br label %${tag}.next`, `${tag}.next:`, ); return lines; } +/* Teardown segments. A typed teardown that frees each block onto its free + * list as it goes frees a tree in walk order, and the free lists, which are + * LIFO, hand the blocks to the next allocations in the reverse order: a tree + * built again in the same order as the last (each generation of a structure + * a loop rebuilds) is laid out mirrored, and every walk over it, the next + * teardown included, runs through memory backwards, which defeats the + * hardware prefetchers. The runtime frees large dead sets of the cycle + * collector in address order for the same reason (scr_mem_order_for_free). + * + * So the teardown of a type with children of its own type (the `_s` form, + * segmentedTeardown) keeps the blocks it frees in a segment, linked through + * their first words, which it builds in order through a tail pointer: the + * `_s` form takes the segment's last link (the first word of its last block, + * or the head slot of an empty segment) and returns the new one. A node + * first releases its own block (emitObjectFree, keeping it), then appends it + * ahead of its children's blocks when its address is the lower, behind them + * otherwise (segmentTeardownLines), and the children append theirs in field + * order, so a structure + * built parent first (object literals allocate before their fields) or + * children first (constructor arguments) comes back in ascending order. The + * chain's first teardown (`_d`) frees the whole segment onto the class's + * free list in one step (emitSegmentFree), and the walks over the next + * generation run forward. A teardown past DIRECT_DESTROY_CHAIN appends + * through scr_rc_destroy_seg, under the same depth budget as + * scr_rc_destroy. Blocks outside the calling thread's arena are freed as + * before; one type's blocks are all of one class. */ + +/** Whether the teardown of a type whose reference fields have `fieldTypes` + * (with direct targets `targets`) and whose own teardown is `destroy` keeps + * a segment: a child field has its own type, and the inline allocation paths + * are in use. */ +export function segmentedTeardown( + host: ShapeHost, + destroy: string, + fieldTypes: readonly IrType[], + targets: ReadonlyMap, +): boolean { + if (host.inlineAlloc !== true) return false; + return fieldTypes.some((t) => directDestroyTarget(host, t, targets)?.destroy === destroy); +} + +/** The first lines of a segmented teardown, in its entry block: the + * segment's last link (`%sg.t`, from the `%tail` argument) and the kept own + * block (`%sg.own`, for emitObjectFree). */ +export function segmentPrologueLines(): string[] { + return [ + ` %sg.t = alloca ptr`, + ` %sg.own = alloca ptr`, + ` store ptr %tail, ptr %sg.t`, + ` store ptr null, ptr %sg.own`, + ]; +} + +/** The rest of a segmented teardown after emitObjectFree kept (or freed) + * its own block: the block goes ahead of its children's when its address is + * below the first non-NULL one of `children` (loaded fields of the + * teardown's own type, which stand for their blocks) or there is none, and + * behind them otherwise; then the children's releases (`releases`, given a + * label prefix), and the segment's last link is returned. Each order has its + * own copy of the releases, so neither keeps the decision, or the block once + * it is appended, across the children's teardowns. */ +export function segmentTeardownLines( + children: readonly string[], + releases: (prefix: string) => string[], +): string[] { + const lines = [` %sg.o = load ptr, ptr %sg.own`, ` %sg.kept = icmp ne ptr %sg.o, null`]; + let ref = "null"; + [...children].reverse().forEach((child, i) => { + lines.push( + ` %sg.nn${i} = icmp ne ptr ${child}, null`, + ` %sg.r${i} = select i1 %sg.nn${i}, ptr ${child}, ptr ${ref}`, + ); + ref = `%sg.r${i}`; + }); + lines.push( + ` %sg.leaf = icmp eq ptr ${ref}, null`, + ` %sg.lower = icmp ult ptr %sg.o, ${ref}`, + ` %sg.first = or i1 %sg.leaf, %sg.lower`, + ` %sg.ahead = and i1 %sg.kept, %sg.first`, + ` br i1 %sg.ahead, label %sg.front, label %sg.back`, + `sg.front:`, + ...segmentAppendLines("a"), + ...releases("ta"), + ` br label %sg.ret`, + `sg.back:`, + ...releases("tb"), + ` %sg.keep = icmp ne ptr %sg.o, null`, + ` br i1 %sg.keep, label %sg.last, label %sg.ret`, + `sg.last:`, + ...segmentAppendLines("l"), + ` br label %sg.ret`, + `sg.ret:`, + ` %sg.tr = load ptr, ptr %sg.t`, + ` ret ptr %sg.tr`, + ); + return lines; +} + +function segmentAppendLines(tag: string): string[] { + return [ + ` %sg.t${tag} = load ptr, ptr %sg.t`, + ` store ptr %sg.o, ptr %sg.t${tag}`, + ` store ptr %sg.o, ptr %sg.t`, + ]; +} + /** The direct-teardown target of a reference of `type`, if `targets` (keyed * `record:` / `object:`) has one. Nullable unions hold the arm's * pointer, NULL or an immortal sentinel, which the child release handles. */ @@ -1499,12 +1752,42 @@ export function usedDirectTargets( /** The teardown definitions for `destroy`: with `chained`, the body takes * the chain length `%d` (directChildReleaseLines) and the runtime's - * one-argument entry starts a chain. */ + * one-argument entry starts a chain. With `segment` (the host, for a + * segmented teardown), the body is a segment teardown's + * (segmentPrologueLines to segmentTeardownLines), defined as `_s`, with + * `_sc` for scr_rc_destroy_seg, and `_d` starts a segment and frees it. */ export function destroyDefinitions( destroy: string, body: readonly string[], chained: boolean, + segment: ShapeHost | null = null, ): string[] { + if (segment !== null) + return [ + `define internal ptr @${destroy}_s(ptr %o, i32 %d, ptr %tail) ${FN_ATTRS} {`, + `entry:`, + ...body, + `}`, + `define internal ptr @${destroy}_sc(ptr %o, ptr %tail) ${FN_ATTRS} {`, + `entry:`, + ` %t = call ptr @${destroy}_s(ptr %o, i32 0, ptr %tail)`, + ` ret ptr %t`, + `}`, + `define internal void @${destroy}_d(ptr %o, i32 %d) ${FN_ATTRS} {`, + `entry:`, + ` %head = alloca ptr`, + ` store ptr null, ptr %head`, + ` %last = call ptr @${destroy}_s(ptr %o, i32 %d, ptr %head)`, + ` %first = load ptr, ptr %head`, + ...emitSegmentFree(segment, "sgf", "%first", "%last"), + ` ret void`, + `}`, + `define internal void @${destroy}(ptr %o) ${FN_ATTRS} {`, + `entry:`, + ` call void @${destroy}_d(ptr %o, i32 0)`, + ` ret void`, + `}`, + ]; if (!chained) return [ `define internal void @${destroy}(ptr %o) ${FN_ATTRS} {`, @@ -1527,6 +1810,126 @@ export function destroyDefinitions( ]; } +/** The frozen bit of a freezable record's rc word (scr_rc.h SCR_RC_FROZEN): + * bit 62 on 64-bit targets, bit 30 on 32-bit ones. */ +function recordFrozenBit(sizeType: RcCount): bigint { + return 1n << (sizeType === "i64" ? 62n : 30n); +} + +/** `value` as a signed LLVM integer literal of `sizeType`. */ +function sizeLiteral(sizeType: RcCount, value: bigint): string { + return BigInt.asIntN(sizeType === "i64" ? 64 : 32, value).toString(); +} + +/** The teardown of a type whose reference fields all hold its own type + * (`fields`: their struct indices; `release`: the type's inlinable release; + * no collector header): one loop instead of a call per node. A child whose + * last reference the dead node held joins a pending list linked through its + * own count word, which it no longer needs; the loop takes the list's first + * node next, so nodes are visited parent first, fields in order. Each node + * releases its block as emitObjectFree does, keeping a block of the calling + * thread's arena in a segment linked through its first word, and the segment + * goes onto the free list in one step at the end (emitSegmentFree): the next + * structure built parent first takes the blocks in the order it allocates + * them, as segmentTeardownLines arranges for the recursive form. The loop + * needs no depth budget. `_d` (the direct-chain entry other teardowns call) + * and the plain entry both run it. A class that keeps its id in the count + * word counts in the low half (`count`); a dead node's whole word is free. */ +export function iterativeTeardownDefinitions( + host: ShapeHost, + destroy: string, + struct: string, + fields: readonly number[], + release: string, + observers: FreeObservers, + count: RcCount = host.sizeType, +): string[] { + const S = host.sizeType; + const lines = [ + `define internal void @${destroy}_it(ptr %root) ${FN_ATTRS} {`, + `entry:`, + ` %sg.own = alloca ptr`, + ` br label %it.node`, + `it.node:`, + ` %o = phi ptr [ %root, %entry ], [ %it.top, %it.next ]`, + ` %it.pend = phi ptr [ null, %entry ], [ %it.rest, %it.next ]`, + ` %it.first = phi ptr [ null, %entry ], [ %it.fa, %it.next ]`, + ` %it.last = phi ptr [ null, %entry ], [ %it.la, %it.next ]`, + ...fields.flatMap((index, i) => [ + ` %it.f${i} = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 ${index}`, + ` %it.v${i} = load ptr, ptr %it.f${i}`, + ]), + ` store ptr null, ptr %sg.own`, + ...emitObjectFree(host, false, observers, "%sg.own"), + ` br label %it.freed`, + `it.freed:`, + ` %it.k = load ptr, ptr %sg.own`, + ` %it.kept = icmp ne ptr %it.k, null`, + ` br i1 %it.kept, label %it.app, label %it.kids`, + `it.app:`, + ` %it.none = icmp eq ptr %it.first, null`, + ` br i1 %it.none, label %it.appd, label %it.link`, + `it.link:`, + ` store ptr %it.k, ptr %it.last`, + ` br label %it.appd`, + `it.appd:`, + ` %it.nf = select i1 %it.none, ptr %it.k, ptr %it.first`, + ` br label %it.kids`, + `it.kids:`, + ` %it.fa = phi ptr [ %it.nf, %it.appd ], [ %it.first, %it.freed ]`, + ` %it.la = phi ptr [ %it.k, %it.appd ], [ %it.last, %it.freed ]`, + ]; + // The fields go onto the list last first, so the first field is next. + let pend = "%it.pend"; + let from = "it.kids"; + for (let i = fields.length - 1; i >= 0; i--) { + const v = `%it.v${i}`; + lines.push( + ` %it.nn${i} = icmp ne ptr ${v}, null`, + ` br i1 %it.nn${i}, label %it.chk${i}, label %it.j${i}`, + `it.chk${i}:`, + ` %it.rc${i} = load ${count}, ptr ${v}`, + ` %it.last${i} = icmp eq ${count} %it.rc${i}, 1`, + ` br i1 %it.last${i}, label %it.push${i}, label %it.sh${i}`, + `it.push${i}:`, + ` %it.pl${i} = ptrtoint ptr ${pend} to ${S}`, + ` store ${S} %it.pl${i}, ptr ${v}`, + ` br label %it.j${i}`, + `it.sh${i}:`, + ` call void ${release}(ptr ${v}) ; a shared or immortal child`, + ` br label %it.j${i}`, + `it.j${i}:`, + ` %it.p${i} = phi ptr [ ${pend}, %${from} ], [ ${v}, %it.push${i} ], [ ${pend}, %it.sh${i} ]`, + ); + pend = `%it.p${i}`; + from = `it.j${i}`; + } + lines.push( + ` %it.empty = icmp eq ptr ${pend}, null`, + ` br i1 %it.empty, label %it.done, label %it.next`, + `it.next:`, + ` %it.top = getelementptr i8, ptr ${pend}, i64 0`, + ` %it.lw = load ${S}, ptr %it.top`, + ` %it.rest = inttoptr ${S} %it.lw to ptr`, + ` br label %it.node`, + `it.done:`, + ...emitSegmentFree(host, "sgf", "%it.fa", "%it.la"), + ` ret void`, + `}`, + `define internal void @${destroy}_d(ptr %o, i32 %d) ${FN_ATTRS} {`, + `entry:`, + ` call void @${destroy}_it(ptr %o)`, + ` ret void`, + `}`, + `define internal void @${destroy}(ptr %o) ${FN_ATTRS} {`, + `entry:`, + ` call void @${destroy}_it(ptr %o)`, + ` ret void`, + `}`, + ); + return lines; +} + /** Common NULL/immortal/decrement skeleton for ordinary object releases. * `freeBody` owns the zero-ref teardown and must leave the current block at * its end; traced objects also get the possible-cycle-root branch. @@ -1543,13 +1946,20 @@ export function releaseBody( bounded = true, immortal = false, chained = false, + segmented = false, + frozen = false, + destroyDefs: readonly string[] | null = null, ): string[] { const S = host.sizeType; if (bounded) host.declare(`declare void @scr_rc_destroy(ptr, ptr)`); const destroy = `${fnName}_destroy`; + // A freezable record's count is the word without its frozen bit. + const notFrozen = sizeLiteral(S, ~recordFrozenBit(S)); const lines = [ - ...(bounded ? destroyDefinitions(destroy, freeBody, chained) : []), - ...releaseFastPath(host, fnName, `${fnName}_slow`, traced, comment, immortal), + ...(bounded + ? (destroyDefs ?? destroyDefinitions(destroy, freeBody, chained, segmented ? host : null)) + : []), + ...releaseFastPath(host, fnName, `${fnName}_slow`, traced, comment, immortal, S, frozen), `define internal void @${fnName}_slow(ptr %o) noinline ${FN_ATTRS} {${comment ? ` ; ${comment}` : ""}`, `entry:`, ` %isnull = icmp eq ptr %o, null`, @@ -1561,7 +1971,9 @@ export function releaseBody( `dec:`, ` %n = sub ${S} %rc, 1`, ` store ${S} %n, ptr %o`, - ` %dead = icmp eq ${S} %n, 0`, + ...(frozen + ? [` %refs = and ${S} %n, ${notFrozen}`, ` %dead = icmp eq ${S} %refs, 0`] + : [` %dead = icmp eq ${S} %n, 0`]), ` br i1 %dead, label %free, label %${traced ? "root" : "done"}`, `free:`, ...(traced ? cycleDeadLines(host, "teardown") : []), @@ -1591,9 +2003,14 @@ export function releaseFastPath( comment = "", immortal = false, count: RcCount = host.sizeType, + frozen = false, ): string[] { const S = count; if (immortal) return immortalRcBody(host, fnName, "void", comment); + // A freezable record compares its count without the frozen bit: an + // immortal word's count is then ~FROZEN, which the same compare sends to + // `slow`. + const notFrozen = ~recordFrozenBit(S); return [ `define internal void @${fnName}(ptr %o) ${FN_ATTRS} {${comment ? ` ; ${comment} (fast path)` : ""}`, `entry:`, @@ -1601,8 +2018,13 @@ export function releaseFastPath( ` br i1 %isnull, label %done, label %check`, `check:`, ` %rc = load ${S}, ptr %o`, - ` %rcm2 = sub ${S} %rc, 2`, - ` %shared = icmp ult ${S} %rcm2, -3`, + ...(frozen + ? [ + ` %refs = and ${S} %rc, ${sizeLiteral(S, notFrozen)}`, + ` %rcm2 = sub ${S} %refs, 2`, + ` %shared = icmp ult ${S} %rcm2, ${sizeLiteral(S, notFrozen - 2n)}`, + ] + : [` %rcm2 = sub ${S} %rc, 2`, ` %shared = icmp ult ${S} %rcm2, -3`]), ` br i1 %shared, label %dec, label %slow`, `dec:`, ` %n = sub ${S} %rc, 1`, @@ -1780,26 +2202,63 @@ export function emitRecordShapes( const chained = directTargets.has(`record:${shape.id}`) || refMembers.some((m) => directDestroyTarget(host, m.type, directTargets) !== null); - for (const m of refMembers) { - freeBody.push( - ` %f${t} = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 ${m.index}`, - ` %v${t} = load ptr, ptr %f${t}`, + const destroy = `${mangleRecordRelease(shape.id)}_destroy`; + const bounded = refMembers.some((member) => needsBoundedRelease(member.type)); + const segmented = + bounded && + segmentedTeardown( + host, + destroy, + refMembers.map((m) => m.type), + directTargets, ); - const direct = directDestroyTarget(host, m.type, directTargets); - if (direct) { - freeBody.push( - ...directChildReleaseLines(host, `td${t}`, `%v${t}`, direct, llvmCommentText(m.name)), - ); - } else { - freeBody.push( - ` call void ${releaseSym(host, m.type)}(ptr %v${t}) ; ${llvmCommentText(m.name)}`, - ); - } - t++; - } // Closed layouts can carry a kept record source (record-sources.ts). const observers = recordMayKeepSource(shape) ? "weak-or-source" : "weak"; - freeBody.push(...emitObjectFree(host, traced, observers)); + // A segmented teardown loads its fields, releases its own block, and + // then releases the fields (segmentTeardownLines). + const loads: string[] = []; + const ownType: string[] = []; + const release = (prefix: string, m: (typeof refMembers)[number], i: number): string[] => { + const direct = directDestroyTarget(host, m.type, directTargets); + if (direct === null) + return [` call void ${releaseSym(host, m.type)}(ptr %v${i}) ; ${llvmCommentText(m.name)}`]; + const own = segmented && direct.destroy === destroy; + return directChildReleaseLines( + host, + `${prefix}${i}`, + `%v${i}`, + direct, + llvmCommentText(m.name), + own, + ); + }; + for (const m of refMembers) { + const fieldLines = [ + ` %f${t} = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 ${m.index}`, + ` %v${t} = load ptr, ptr %f${t}`, + ]; + if (segmented) { + loads.push(...fieldLines); + if (directDestroyTarget(host, m.type, directTargets)?.destroy === destroy) + ownType.push(`%v${t}`); + } else freeBody.push(...fieldLines, ...release("td", m, t)); + t++; + } + if (segmented) + freeBody.push( + ...segmentPrologueLines(), + ...loads, + ...emitObjectFree(host, traced, observers, "%sg.own"), + ...segmentTeardownLines(ownType, (prefix) => + refMembers.flatMap((m, i) => release(prefix, m, i)), + ), + ); + else freeBody.push(...emitObjectFree(host, traced, observers)); + // A segmented shape whose every reference field holds its own type, with + // no collector header and no frozen bit in its count, tears down in one + // loop (iterativeTeardownDefinitions). + const iterative = + segmented && !traced && shape.freezable !== true && ownType.length === refMembers.length; defs.push( ...releaseBody( host, @@ -1807,9 +2266,21 @@ export function emitRecordShapes( traced, freeBody, "", - refMembers.some((member) => needsBoundedRelease(member.type)), + bounded, false, chained, + segmented, + shape.freezable === true, + iterative + ? iterativeTeardownDefinitions( + host, + destroy, + struct, + refMembers.map((m) => m.index), + `@${mangleRecordRelease(shape.id)}`, + observers, + ) + : null, ), ``, ); diff --git a/packages/compiler/src/backend/llvm/speculative-integers.test.ts b/packages/compiler/src/backend/llvm/speculative-integers.test.ts index 3acfec0fb..1b82768d5 100644 --- a/packages/compiler/src/backend/llvm/speculative-integers.test.ts +++ b/packages/compiler/src/backend/llvm/speculative-integers.test.ts @@ -1,9 +1,11 @@ import { expect, test } from "vitest"; import { F64, type IrExpr, type IrStmt } from "../../ir/ir.js"; import { + inlinableNumberFunction, isSpeculableValue, planProvenIntegerLoop, planSpeculativeLoop, + rewriteSpeculable, speculativeReads, } from "./speculative-integers.js"; @@ -266,3 +268,136 @@ test("bounded cursors with proven writes are shadowed in a proven integer loop", planProvenIntegerLoop(forLoop([...body, jump]) as never, cursors, all, () => false, all), ).toBeNull(); }); + +const libCall = (fn: string, args: IrExpr[]): IrExpr => + ({ kind: "libCall", fn, args, type: F64, loc }) as never; +const call = (callee: string, args: IrExpr[]): IrExpr => ({ + kind: "call", + callee, + args, + type: F64, + loc, +}); +const pow2 = (n: IrExpr): IrExpr => libCall("math.pow", [num(2), n]); +/** `function trim(v, n) { return v - Math.floor(v / Math.pow(2, n)) * Math.pow(2, n); }` */ +const trim = { + name: "trim", + params: [ + { localId: "v", name: "v", type: F64 }, + { localId: "n", name: "n", type: F64 }, + ], + returnType: F64, + locals: [], + body: [ + { + kind: "return", + value: bin( + "-", + ref("v"), + bin("*", libCall("math.floor", [bin("/", ref("v"), pow2(ref("n")))]), pow2(ref("n"))), + ), + loc, + }, + ], + loc, +} as never; + +test("calls of small pure number functions expand into a speculated write", () => { + const inlinable = inlinableNumberFunction(trim)!; + expect(inlinable.params).toEqual(["v", "n"]); + const context = { inlinable: (callee: string) => (callee === "trim" ? inlinable : undefined) }; + // key = trim(key, m) * 4 + c + const value = bin("+", bin("*", call("trim", [ref("key"), ref("m")]), num(4)), ref("c")); + expect(isSpeculableValue(value)).toBe(false); + const rewritten = rewriteSpeculable(value, context)!; + expect(rewritten.leaves).toEqual([]); + expect(JSON.stringify(rewritten.value)).not.toContain('"call"'); + const write = assign("key", value); + const plan = planSpeculativeLoop( + loop([write, increment("i")]) as never, + all, + () => false, + () => false, + all, + context, + )!; + expect(plan.writes.has(write)).toBe(true); + expect(plan.values.get(write)).toEqual(rewritten); + // `m`, an input of the expanded body, is checked on entry with the others. + expect([...plan.locals.keys()].sort()).toEqual(["c", "i", "key", "m", "n"]); +}); + +test("Math.floor needs a power-of-two divisor, and expansion pure arguments", () => { + const context = { inlinable: () => inlinableNumberFunction(trim) }; + expect(rewriteSpeculable(libCall("math.floor", [bin("/", ref("a"), num(8))]), {})).not.toBeNull(); + expect(rewriteSpeculable(libCall("math.floor", [bin("/", ref("a"), num(6))]), {})).toBeNull(); + expect(rewriteSpeculable(libCall("math.floor", [bin("/", ref("a"), ref("b"))]), {})).toBeNull(); + // An argument that is itself a call is evaluated once: no expansion. + expect(rewriteSpeculable(call("trim", [call("other", []), ref("m")]), context)).toBeNull(); + const effectful = { + ...(trim as object), + body: [{ kind: "exprStmt", expr: call("log", []), loc }], + }; + expect(inlinableNumberFunction(effectful as never)).toBeUndefined(); + // A divisor next to a power of two is not one, though Math.log2 rounds it to 52. + const near = (d: number): IrExpr => libCall("math.floor", [bin("/", ref("a"), num(d))]); + expect(rewriteSpeculable(near(2 ** 52), {})).not.toBeNull(); + expect(rewriteSpeculable(near(2 ** 52 + 1), {})).toBeNull(); + expect(rewriteSpeculable(near(2 ** 53), {})).toBeNull(); + expect(rewriteSpeculable(near(0.5), {})).toBeNull(); +}); + +test("a helper's own calls must be of helpers that expand too", () => { + // `function outer(v, n) { return trim(v, n) + other(v); }` + const outer = { + ...(trim as object), + name: "outer", + body: [ + { + kind: "return", + value: bin("+", call("trim", [ref("v"), ref("n")]), call("other", [ref("v")])), + loc, + }, + ], + }; + const helpers = (callee: string) => + callee === "trim" ? inlinableNumberFunction(trim) : undefined; + expect(inlinableNumberFunction(outer as never)).toBeUndefined(); + expect(inlinableNumberFunction(outer as never, helpers)).toBeUndefined(); + const pure = { + ...(outer as object), + body: [{ kind: "return", value: bin("*", call("trim", [ref("v"), ref("n")]), num(2)), loc }], + }; + expect(inlinableNumberFunction(pure as never, helpers)).toBeDefined(); +}); + +test("other leaves are hoisted only when no call can change the write's locals", () => { + let next = 0; + const leafId = (): string => `%leaf.${next++}`; + const value = bin( + "%", + bin("+", bin("*", ref("acc"), num(7)), call("noisy", [ref("i")])), + num(1000003), + ); + const hoisted = rewriteSpeculable(value, { unaliased: () => true, leafId })!; + expect(hoisted.leaves.map((leaf) => leaf.expr)).toEqual([call("noisy", [ref("i")])]); + expect(rewriteSpeculable(value, { unaliased: (id) => id !== "acc", leafId })).toBeNull(); + // A call whose argument writes a local stays where it is. + const bump: IrExpr = { kind: "incDec", localId: "i", op: "+", prefix: false, type: F64, loc }; + expect( + rewriteSpeculable(bin("+", ref("i"), call("noisy", [bump])), { unaliased: () => true, leafId }), + ).toBeNull(); + // Without leaf ids nothing is hoisted. + expect(rewriteSpeculable(value, { unaliased: () => true })).toBeNull(); + const write = assign("acc", value); + const plan = planSpeculativeLoop( + loop([write, increment("i")]) as never, + (id) => !id.startsWith("%leaf"), + () => false, + () => false, + (id) => !id.startsWith("%leaf"), + { unaliased: () => true, leafId }, + )!; + expect(plan.values.get(write)!.leaves).toHaveLength(1); + expect([...plan.locals.keys()].some((id) => id.startsWith("%leaf"))).toBe(false); +}); diff --git a/packages/compiler/src/backend/llvm/speculative-integers.ts b/packages/compiler/src/backend/llvm/speculative-integers.ts index 00fabb788..971bd00cb 100644 --- a/packages/compiler/src/backend/llvm/speculative-integers.ts +++ b/packages/compiler/src/backend/llvm/speculative-integers.ts @@ -1,5 +1,6 @@ -import type { IrExpr, IrStmt } from "../../ir/ir.js"; -import { everyExprChild, everyStmtChild } from "../../ir/traverse.js"; +import type { IrExpr, IrFunction, IrStmt } from "../../ir/ir.js"; +import { everyExpr, everyExprChild, everyStmtChild } from "../../ir/traverse.js"; +import { InternalCompilerError } from "../../errors.js"; import { f64Lit } from "./common.js"; import type { LlValue, LlvmEmitterContext } from "./expr-context.js"; import { exactInteger, widenInteger } from "./integer-values.js"; @@ -30,6 +31,326 @@ export interface SpeculativeLoopPlan { * than through its condition, `break` or a failed check: no calls, no * throwing operations, no labeled jumps and no `return`. */ observerFree: boolean; + /** The writes that speculate a rewritten value (rewriteSpeculable). */ + values: Map; +} + +/** A write's value as speculation evaluates it: calls of small pure number + * functions expanded in place, and other number leaves (`leaves`) replaced + * by references to fresh locals. The fast copy evaluates each leaf once, + * stores it to its local and checks it as an integer; a failed check + * resumes at a copy of the write that reads the locals, so a leaf is never + * evaluated twice. */ +export interface SpeculativeValue { + value: IrExpr; + leaves: { localId: string; expr: IrExpr; ref: IrExpr }[]; +} + +/** What the planner may assume about the code around a loop. */ +export interface SpeculationContext { + /** The parameters and the returned expression of `callee` when it is a + * small pure number function (inlinableNumberFunction). */ + inlinable?: (callee: string) => InlinableFunction | undefined; + /** A local that no call can write: not captured, boxed or global. */ + unaliased?: (localId: string) => boolean; + /** A fresh local id for a hoisted leaf. */ + leafId?: () => string; +} + +export interface InlinableFunction { + params: readonly string[]; + body: IrExpr; +} + +/** `s` (a speculated write) writing `value` instead. */ +export function withSpeculativeValue(s: IrStmt, value: IrExpr): IrStmt { + switch (s.kind) { + case "assign": + return { ...s, value }; + case "varDecl": + return { ...s, init: value }; + case "exprStmt": + if (s.expr.kind === "assignExpr") return { ...s, expr: { ...s.expr, value } }; + break; + default: + break; + } + throw new InternalCompilerError(`llvm emitter bug: speculated ${s.kind} with a rewritten value`); +} + +/** Calls nest at most this deep in one expansion. */ +const MAX_INLINE_DEPTH = 4; +/** An expansion larger than this stays a call. */ +const MAX_INLINE_NODES = 64; + +/** A function that only returns arithmetic over its number parameters: + * `function trim(v, n) { return v - Math.floor(v / Math.pow(2, n)) * Math.pow(2, n); }`. + * Its call evaluates nothing but its arguments, so a speculated write may + * expand it in place. A call in its body must be of another such function + * (`resolve`): any other call could change what the expansion reads. */ +export function inlinableNumberFunction( + fn: IrFunction, + resolve?: (callee: string) => InlinableFunction | undefined, +): InlinableFunction | undefined { + if ( + fn.async || + fn.generator || + (fn.captures?.length ?? 0) > 0 || + (fn.classCaptures?.length ?? 0) > 0 || + fn.returnType.kind !== "f64" || + fn.params.some((p) => p.type.kind !== "f64" && p.type.kind !== "union") || + fn.body.length !== 1 || + fn.body[0]!.kind !== "return" || + fn.body[0]!.value === undefined + ) + return undefined; + const params = fn.params.map((p) => p.localId); + const known = new Set(params); + let nodes = 0; + // A parameter of a union type (`number | undefined`) is read only + // through its tag test and narrowing, which a wrapped argument folds. + const union = (e: IrExpr): boolean => e.kind === "varRef" && known.has(e.localId); + const pure = (e: IrExpr): boolean => { + if (++nodes > MAX_INLINE_NODES) return false; + if (e.kind === "boolLit") return true; + if (e.kind === "unionIsTag") return union(e.value); + if (e.kind === "unionNarrow") return e.type.kind === "f64" && union(e.value); + if (e.type.kind !== "f64") return false; + switch (e.kind) { + case "numLit": + return true; + case "varRef": + return known.has(e.localId); + case "ternary": + return pure(e.cond) && pure(e.then) && pure(e.else_); + case "bin": + return (INTEGER_OPS.has(e.op) || e.op === "/") && pure(e.left) && pure(e.right); + case "unary": + return e.op === "-" && pure(e.operand); + case "libCall": + return (e.fn === "math.floor" || e.fn === "math.pow") && e.args.every(pure); + case "call": + return resolve?.(e.callee) !== undefined && e.args.every(pure); + default: + return false; + } + }; + const body = fn.body[0]!.value!; + return pure(body) ? { params, body } : undefined; +} + +/** `e` with each parameter reference replaced by its argument. A tag test + * or narrowing of a wrapped argument folds, and so does a ternary over a + * folded test. */ +function substitute(e: IrExpr, args: ReadonlyMap): IrExpr { + switch (e.kind) { + case "varRef": + return args.get(e.localId) ?? e; + case "unionIsTag": { + const value = substitute(e.value, args); + if (value.kind !== "unionWrap" || value.unionId !== e.unionId) return { ...e, value }; + return { + kind: "boolLit", + value: (value.tag === e.tag) !== (e.negated === true), + type: e.type, + loc: e.loc, + }; + } + case "unionNarrow": { + const value = substitute(e.value, args); + return value.kind === "unionWrap" && value.unionId === e.unionId && value.tag === e.tag + ? value.value + : { ...e, value }; + } + case "ternary": { + const cond = substitute(e.cond, args); + if (cond.kind === "boolLit") return substitute(cond.value ? e.then : e.else_, args); + return { ...e, cond, then: substitute(e.then, args), else_: substitute(e.else_, args) }; + } + case "bin": + return { ...e, left: substitute(e.left, args), right: substitute(e.right, args) }; + case "unary": + return { ...e, operand: substitute(e.operand, args) }; + case "libCall": + case "call": + return { ...e, args: e.args.map((a) => substitute(a, args)) }; + default: + return e; + } +} + +/** k when `value` is exactly 2^k with k in [0, 52], else null. + * Math.log2 rounds: 2^52 + 1 gives 52, so the power is compared back. */ +function exactPowerOfTwo(value: number): number | null { + if (!(value >= 1)) return null; + const k = Math.round(Math.log2(value)); + return k >= 0 && k <= 52 && 2 ** k === value ? k : null; +} + +/** The same speculated operand written twice: a local, a literal, or a + * power of two of the same exponent. */ +function sameOperand(a: IrExpr, b: IrExpr): boolean { + if (a.kind === "varRef") return b.kind === "varRef" && a.localId === b.localId; + if (a.kind === "numLit") return b.kind === "numLit" && Object.is(a.value, b.value); + return ( + a.kind === "libCall" && + b.kind === "libCall" && + a.fn === "math.pow" && + b.fn === "math.pow" && + powerOfTwo(a) && + powerOfTwo(b) && + sameOperand(a.args[1]!, b.args[1]!) + ); +} + +/** `a - Math.floor(a / p) * p` (either factor order) with p a power of two + * and `a` a local or literal: the floor-mod of `a` by p. */ +function floorModPowerOfTwo(e: IrExpr): { value: IrExpr; power: IrExpr } | null { + if (e.kind !== "bin" || e.op !== "-" || e.right.kind !== "bin" || e.right.op !== "*") return null; + const { left, right } = e.right; + const [floor, power] = + left.kind === "libCall" && left.fn === "math.floor" ? [left, right] : [right, left]; + if (floor.kind !== "libCall" || floor.fn !== "math.floor" || floor.args.length !== 1) return null; + const quotient = floor.args[0]!; + if (quotient.kind !== "bin" || quotient.op !== "/") return null; + if (!powerOfTwo(power) || !sameOperand(quotient.right, power)) return null; + if (e.left.kind !== "varRef" && e.left.kind !== "numLit") return null; + return sameOperand(quotient.left, e.left) ? { value: e.left, power } : null; +} + +/** Whether `e` is a power of two speculation can shift by: + * `Math.pow(2, n)` or a literal 2^k with k in [0, 52]. */ +function powerOfTwo(e: IrExpr): boolean { + if (e.kind === "numLit") return exactPowerOfTwo(e.value) !== null; + return ( + e.kind === "libCall" && + e.fn === "math.pow" && + e.args.length === 2 && + e.args[0]!.kind === "numLit" && + e.args[0]!.value === 2 + ); +} + +/** Typed-array element kinds whose elements are integers. */ +const INTEGER_ELEMENTS = new Set(["u8", "u8c", "i8", "u16", "i16", "u32", "i32"]); + +/** A typed-array element read of an integer kind with no effect: an + * out-of-range read gives NaN, which fails its check. Float elements would + * only bail. */ +function isSpeculativeByteRead(e: IrExpr): boolean { + return ( + e.kind === "bytesIntrinsic" && + e.method === "get" && + e.type.kind === "f64" && + e.receiver.type.kind === "bytes" && + INTEGER_ELEMENTS.has(e.receiver.type.elem) && + e.args.length === 1 && + isEffectFree(e.receiver) && + isEffectFree(e.args[0]!) + ); +} + +/** Whether evaluating `e` assigns no binding: no `++`/`--`, no assignment + * expression, and no statement inside it. */ +function writesNoLocal(e: IrExpr): boolean { + return everyExpr(e, { + expr: (node) => node.kind !== "incDec" && node.kind !== "assignExpr", + stmt: () => false, + }); +} + +/** Rewrite a write's value that isSpeculableValue refuses into one it can + * speculate (SpeculativeValue), or null. Expansion and the new operations + * (`Math.pow(2, n)`, `Math.floor(a / 2^n)`, byte reads) evaluate nothing + * but numbers, so the fast copy may evaluate them in any order. Hoisted + * leaves run in the write's evaluation order before the arithmetic; that + * reorders nothing only when the write reads no heap and every local it + * reads is one no call can write. */ +export function rewriteSpeculable( + value: IrExpr, + context: SpeculationContext, +): SpeculativeValue | null { + const leaves: SpeculativeValue["leaves"] = []; + let reads = false; + const locals: string[] = []; + const rewrite = (e: IrExpr, depth: number): IrExpr | null => { + if (e.type.kind !== "f64") return null; + switch (e.kind) { + case "numLit": + return Number.isSafeInteger(e.value) && !Object.is(e.value, -0) ? e : null; + case "varRef": + locals.push(e.localId); + return e; + case "bin": { + if (!ARITHMETIC_OPS.has(e.op)) break; + const left = rewrite(e.left, depth); + const right = left && rewrite(e.right, depth); + return left && right ? { ...e, left, right } : null; + } + case "unary": { + if (e.op !== "-") break; + const operand = rewrite(e.operand, depth); + return operand ? { ...e, operand } : null; + } + case "libCall": { + if (powerOfTwo(e) && e.kind === "libCall") { + const exponent = rewrite(e.args[1]!, depth); + return exponent ? { ...e, args: [e.args[0]!, exponent] } : null; + } + const quotient = e.args[0]; + if ( + e.fn === "math.floor" && + e.args.length === 1 && + quotient?.kind === "bin" && + quotient.op === "/" && + powerOfTwo(quotient.right) + ) { + const left = rewrite(quotient.left, depth); + const right = left && rewrite(quotient.right, depth); + return left && right ? { ...e, args: [{ ...quotient, left, right }] } : null; + } + break; + } + case "call": { + const fn = depth < MAX_INLINE_DEPTH ? context.inlinable?.(e.callee) : undefined; + // Arguments are evaluated before the body: an expansion may read + // them in another order, or twice, only when they are pure. + const plain = (a: IrExpr): boolean => + isSpeculableValue(a, false) || + (a.kind === "unionWrap" && isSpeculableValue(a.value, false)); + if (fn !== undefined && fn.params.length === e.args.length && e.args.every(plain)) { + const args = new Map(fn.params.map((id, i) => [id, e.args[i]!])); + return rewrite(substitute(fn.body, args), depth + 1); + } + break; + } + default: + if (isSpeculativeRead(e) || isSpeculativeByteRead(e)) { + reads = true; + return e; + } + break; + } + // Only calls of program functions are hoisted: other leaves (a string's + // length, a Map read) are cheap where they stand, and would turn every + // loop that adds one into a second copy. + if (context.leafId === undefined || e.kind !== "call") return null; + // Hoisting runs the arguments before the rest of the write: one that + // assigns a local would move that write ahead of the write's earlier + // reads of it. + if (!e.args.every(writesNoLocal)) return null; + const ref: IrExpr = { kind: "varRef", localId: context.leafId(), type: e.type, loc: e.loc }; + leaves.push({ localId: (ref as { localId: string }).localId, expr: e, ref }); + return ref; + }; + const rewritten = rewrite(value, 0); + if (rewritten === null) return null; + if (leaves.length > 0) { + const leafIds = new Set(leaves.map((leaf) => leaf.localId)); + if (reads || !locals.every((id) => leafIds.has(id) || context.unaliased?.(id) === true)) + return null; + } + return { value: rewritten, leaves }; } type SpeculativeLoop = Extract; @@ -170,13 +491,16 @@ function isEffectFree(e: IrExpr): boolean { } } -/** The speculative reads of a speculable value in evaluation order. */ +/** The speculative reads of a speculable value in evaluation order, those + * of a rewritten value (rewriteSpeculable) included. */ export function speculativeReads(e: IrExpr, out: IrExpr[] = []): IrExpr[] { - if (e.kind === "bin" && ARITHMETIC_OPS.has(e.op)) { + if (e.kind === "bin" && (ARITHMETIC_OPS.has(e.op) || e.op === "/")) { speculativeReads(e.left, out); speculativeReads(e.right, out); } else if (e.kind === "unary" && e.op === "-") speculativeReads(e.operand, out); - else if (isSpeculativeRead(e)) out.push(e); + else if (e.kind === "libCall" && (e.fn === "math.floor" || e.fn === "math.pow")) + for (const arg of e.args) speculativeReads(arg, out); + else if (isSpeculativeRead(e) || isSpeculativeByteRead(e)) out.push(e); return out; } @@ -195,6 +519,8 @@ function hasProduct(e: IrExpr): boolean { if (e.kind === "bin") return e.op === "*" || e.op === "%" || hasProduct(e.left) || hasProduct(e.right); if (e.kind === "unary") return hasProduct(e.operand); + // Math.floor(a / 2^n) and Math.pow(2, n) replace a division and a call. + if (e.kind === "libCall") return e.fn === "math.floor" || e.fn === "math.pow"; return false; } @@ -202,6 +528,7 @@ function hasArithmetic(e: IrExpr): boolean { if (e.kind === "bin") return ARITHMETIC_OPS.has(e.op) || hasArithmetic(e.left) || hasArithmetic(e.right); if (e.kind === "unary") return e.op === "-" || hasArithmetic(e.operand); + if (e.kind === "libCall") return e.fn === "math.floor" || e.fn === "math.pow"; return false; } @@ -236,8 +563,11 @@ export function planSpeculativeLoop( integral: (localId: string) => boolean = () => false, proven: (e: IrExpr) => boolean = () => false, invariantEligible: (localId: string) => boolean = eligible, + context: SpeculationContext = {}, ): SpeculativeLoopPlan | null { const writes = new Map(); + const values = new Map(); + const leafIds = new Set(); const unspeculable = new Set(); const reads = new Set(); // Locals with at least one read the static analysis does not prove. @@ -263,11 +593,9 @@ export function planSpeculativeLoop( // make the body's work integer. let inCondition = false; - const expr = (e: IrExpr): boolean => { - if (rejected) return false; - nodes++; - if (!OBSERVER_FREE_EXPRESSIONS.has(e.kind) || (e.kind === "bin" && e.left.type.kind !== "f64")) - observerFree = false; + // What the integer writes read and consume. A rewritten value is noted + // here alone: its original, which `expr` walks, has its effects. + const note = (e: IrExpr): void => { if (e.kind === "varRef") { reads.add(e.localId); if (!proven(e)) unproven.add(e.localId); @@ -281,6 +609,20 @@ export function planSpeculativeLoop( } else if (e.kind === "arrayGet" || e.kind === "arrayHas" || e.kind === "arrayState") consume(e.index); else if (e.kind === "bytesIntrinsic" || e.kind === "strIntrinsic") e.args.forEach(consume); + }; + // An expanded call is emitted in the fast copy: it counts toward the + // duplicated code. + const noteAll = (e: IrExpr): boolean => { + nodes++; + note(e); + return everyExprChild(e, noteAll, () => true); + }; + const expr = (e: IrExpr): boolean => { + if (rejected) return false; + nodes++; + if (!OBSERVER_FREE_EXPRESSIONS.has(e.kind) || (e.kind === "bin" && e.left.type.kind !== "f64")) + observerFree = false; + note(e); return everyExprChild(e, expr, nested); }; // Statements inside expressions (sequence expressions) run mid-expression, @@ -307,7 +649,11 @@ export function planSpeculativeLoop( if (s.kind === "arraySet" || s.kind === "bytesSet") consume(s.index); const target = speculativeWriteTarget(s); if (target) { - if (target.value === null || isSpeculableValue(target.value)) { + const rewritten = + target.value === null || isSpeculableValue(target.value) + ? null + : rewriteSpeculable(target.value, context); + if (target.value === null || isSpeculableValue(target.value) || rewritten !== null) { const list = writes.get(target.localId) ?? []; list.push(s); writes.set(target.localId, list); @@ -315,6 +661,11 @@ export function planSpeculativeLoop( reads.add(target.localId); if (s.kind !== "exprStmt" || !proven(s.expr)) unproven.add(target.localId); } else expr(target.value); + if (rewritten !== null) { + values.set(s, rewritten); + for (const leaf of rewritten.leaves) leafIds.add(leaf.localId); + noteAll(rewritten.value); + } return !rejected; } unspeculable.add(target.localId); @@ -341,25 +692,28 @@ export function planSpeculativeLoop( // write runs: speculated itself, already an integer, or loop-invariant // (checked on entry). A value computed from doubles the loop writes // would only fail its check on every entry. + const valueOf = (s: IrStmt): IrExpr | null => + values.get(s)?.value ?? speculativeWriteTarget(s)!.value; const grounded = (e: IrExpr): boolean => { if (e.kind === "varRef") { const id = e.localId; return ( + leafIds.has(id) || proven(e) || locals.has(id) || integral(id) || (!writes.has(id) && !unspeculable.has(id) && invariantEligible(id)) ); } - if (isSpeculativeRead(e)) return true; + if (isSpeculativeRead(e) || isSpeculativeByteRead(e)) return true; return everyExprChild(e, grounded, () => false); }; for (let changed = true; changed;) { changed = false; for (const id of [...locals.keys()]) { for (const s of writes.get(id)!) { - const target = speculativeWriteTarget(s)!; - if (target.value !== null && !grounded(target.value)) { + const value = valueOf(s); + if (value !== null && !grounded(value)) { locals.delete(id); changed = true; break; @@ -376,6 +730,7 @@ export function planSpeculativeLoop( if (e.kind === "varRef") { const id = e.localId; if ( + !leafIds.has(id) && !proven(e) && !locals.has(id) && !writes.has(id) && @@ -388,11 +743,11 @@ export function planSpeculativeLoop( return everyExprChild(e, invariant, () => true); }; for (const s of speculated) { - const target = speculativeWriteTarget(s)!; - if (target.value === null) arithmetic = true; + const value = valueOf(s); + if (value === null) arithmetic = true; else { - if (hasArithmetic(target.value)) arithmetic = true; - invariant(target.value); + if (hasArithmetic(value)) arithmetic = true; + invariant(value); } } if (!arithmetic) return null; @@ -401,13 +756,12 @@ export function planSpeculativeLoop( [...locals.keys()].some((id) => multiplied.has(id)) || [...speculated].some((s) => { const target = speculativeWriteTarget(s)!; - return ( - target.value !== null && - (hasProduct(target.value) || isVariableStep(target.localId, target.value)) - ); + const value = valueOf(s); + return value !== null && (hasProduct(value) || isVariableStep(target.localId, value)); }); if (!heavy) return null; - return { locals, writes: speculated, observerFree }; + for (const s of values.keys()) if (!speculated.has(s)) values.delete(s); + return { locals, writes: speculated, observerFree, values }; } /** A proven integer loop. The bindings integer-ranges.ts bounds over a for @@ -627,6 +981,37 @@ export function emitSpeculativeInteger( } case "bin": { if (e.op !== "+" && e.op !== "-" && e.op !== "*" && e.op !== "%") break; + // The floor-mod of a safe integer by 2^n (n in [0, 52]) is its low n + // bits, also for a negative value: in [0, 2^n), and never -0. + const mod = floorModPowerOfTwo(e); + if (mod) { + const value = emitSpeculativeInteger(host, mod.value, bail, reads, integral); + const n = exponentOf(host, mod.power, bail, reads, integral); + const mask = B.tmp(), + low = B.tmp(); + B.line(`${mask} = sub i64 ${shiftOne(host, n)}, 1`); + B.line(`${low} = and i64 ${value}, ${mask}`); + return low; + } + // A product with 2^n shifts: it is safe iff the other factor's + // magnitude is at most (2^53 - 1) >> n, and a zero product of a safe + // integer and a positive power is +0. + if (e.op === "*" && (powerOfTwo(e.right) || powerOfTwo(e.left))) { + const [factor, power] = powerOfTwo(e.right) ? [e.left, e.right] : [e.right, e.left]; + const value = emitSpeculativeInteger(host, factor, bail, reads, integral); + const n = exponentOf(host, power, bail, reads, integral); + const limit = B.tmp(), + magnitude = B.tmp(), + outside = B.tmp(), + product = B.tmp(); + B.line(`${limit} = ashr i64 ${SAFE}, ${n}`); + host.declare("declare i64 @llvm.abs.i64(i64, i1)"); + B.line(`${magnitude} = call i64 @llvm.abs.i64(i64 ${value}, i1 true)`); + B.line(`${outside} = icmp ugt i64 ${magnitude}, ${limit}`); + bail(outside); + B.line(`${product} = shl nsw i64 ${value}, ${n}`); + return product; + } const a = emitSpeculativeInteger(host, e.left, bail, reads, integral); const b = emitSpeculativeInteger(host, e.right, bail, reads, integral); const result = B.tmp(); @@ -682,6 +1067,19 @@ export function emitSpeculativeInteger( bail(negativeZero); return result; } + case "libCall": { + // A rewritten value's Math.pow(2, n) and Math.floor(a / 2^n) + // (rewriteSpeculable): with n an integer in [0, 52], 2^n is exact and + // so is a / 2^n for a safe integer a, whose floor the arithmetic shift + // computes, also for negative a. No result is -0. + if (e.fn === "math.pow") return shiftOne(host, exponentOf(host, e, bail, reads, integral)); + if (e.fn !== "math.floor" || e.args[0]?.kind !== "bin") break; + const a = emitSpeculativeInteger(host, e.args[0].left, bail, reads, integral); + const n = exponentOf(host, e.args[0].right, bail, reads, integral); + const result = B.tmp(); + B.line(`${result} = ashr i64 ${a}, ${n}`); + return result; + } case "unary": { if (e.op !== "-") break; const a = emitSpeculativeInteger(host, e.operand, bail, reads, integral); @@ -699,6 +1097,35 @@ export function emitSpeculativeInteger( return speculativeLeaf(host, reads.get(e) ?? host.emitExpr(e), e, bail); } +/** The exponent of a power of two (Math.pow(2, n) or a literal 2^k) as an + * i64 in [0, 52]; other exponents bail. */ +function exponentOf( + host: LlvmEmitterContext, + e: IrExpr, + bail: (condition: string) => void, + reads: ReadonlyMap, + integral: boolean, +): string { + if (e.kind === "numLit") { + const k = exactPowerOfTwo(e.value); + if (k === null) throw new InternalCompilerError("llvm emitter bug: speculative power of two"); + return String(k); + } + if (e.kind !== "libCall") + throw new InternalCompilerError("llvm emitter bug: speculative power of two"); + const n = emitSpeculativeInteger(host, e.args[1]!, bail, reads, integral); + const outside = host.B.tmp(); + host.B.line(`${outside} = icmp ugt i64 ${n}, 52`); + bail(outside); + return n; +} + +function shiftOne(host: LlvmEmitterContext, n: string): string { + const result = host.B.tmp(); + host.B.line(`${result} = shl i64 1, ${n}`); + return result; +} + /** The safe i64 of an evaluated number: its proven integer, or a checked * conversion. */ function speculativeLeaf( diff --git a/packages/compiler/src/backend/llvm/split.test.ts b/packages/compiler/src/backend/llvm/split.test.ts index a49a11545..2e0cfa5a5 100644 --- a/packages/compiler/src/backend/llvm/split.test.ts +++ b/packages/compiler/src/backend/llvm/split.test.ts @@ -3,7 +3,7 @@ import { mkdtemp, readFile, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterAll, expect, test } from "vitest"; -import { splitLlvmLibraryProgram, splitLlvmProgram } from "./split.js"; +import { splitLlvmLibraryProgram, splitLlvmProgram, splitLlvmProgramFile } from "./split.js"; import { LlvmDebugInfo } from "./debug-info.js"; import { VOID } from "../../ir/ir.js"; @@ -61,17 +61,17 @@ test("splits generated LLVM functions and promotes only private cross-shard defi expect(actual!.promotedSymbols).not.toContain("public_entry"); expect(actual!.publicSymbols).toEqual(["public_value", "public_entry"]); expect(actual!.shards[0]!.name).toBe("program-globals.ll"); - expect(actual!.shards[0]!.source).toContain("@hidden_value = hidden global i64 7"); + expect(actual!.shards[0]!.source()).toContain("@hidden_value = hidden global i64 7"); expect( actual!.shards .slice(1) - .every((shard) => shard.source.includes("@hidden_value = external hidden global i64")), + .every((shard) => shard.source().includes("@hidden_value = external hidden global i64")), ).toBe(true); expect( actual!.shards.some( (shard) => - !shard.source.includes("define hidden i64 @right() #0") && - shard.source.includes("declare hidden i64 @right() #0"), + !shard.source().includes("define hidden i64 @right() #0") && + shard.source().includes("declare hidden i64 @right() #0"), ), ).toBe(true); }); @@ -82,9 +82,115 @@ test("thread-local definitions and declarations preserve their storage model acr "@hidden_value = internal thread_local global i64 7", ); const split = splitLlvmProgram(tls, { minimumBytes: 0, targetBytes: 64 * 1024 })!; - expect(split.shards[0]!.source).toContain("@hidden_value = hidden thread_local global i64 7"); + expect(split.shards[0]!.source()).toContain("@hidden_value = hidden thread_local global i64 7"); for (const shard of split.shards.slice(1)) { - expect(shard.source).toContain("@hidden_value = external hidden thread_local global i64"); + expect(shard.source()).toContain("@hidden_value = external hidden thread_local global i64"); + } +}); + +const PADDING = Array.from( + { length: 80 }, + (_, i) => + `define internal i64 @pad_${i}() #0 {\nentry:\n ; ${"x".repeat(2048)}\n ret i64 ${i}\n}\n`, +).join("\n"); + +/** SAMPLE with more globals after @public_value and enough functions to + * split into several shards. */ +function withGlobals(globals: string, tail = ""): string { + return `${SAMPLE.replace( + "@public_value = constant i64 11", + `@public_value = constant i64 11\n${globals}`, + ).replace("define i64 @public_entry", `${PADDING}\ndefine i64 @public_entry`)}${tail}`; +} + +/** Writes each shard and compiles it with clang. */ +async function compileShards( + shards: readonly { name: string; source: () => string }[], +): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-llvm-split-shards-")); + scratch.push(dir); + for (const shard of shards) { + const source = join(dir, shard.name); + await writeFile(source, shard.source()); + execFileSync("clang", ["-Wno-override-module", "-c", source, "-o", `${source}.o`]); + } +} + +test("private unnamed_addr lookup tables are promoted like internal globals", async () => { + const split = splitLlvmProgram( + withGlobals('@table = private unnamed_addr constant [2 x i8] c"\\01\\02"'), + { minimumBytes: 0, targetBytes: 64 * 1024 }, + )!; + expect(split).not.toBeNull(); + const [globals, ...functions] = split.shards; + expect(globals!.source()).toContain('@table = hidden unnamed_addr constant [2 x i8] c"\\01\\02"'); + for (const shard of functions) + expect(shard.source()).toContain("@table = external hidden constant [2 x i8]"); + expect(split.promotedSymbols).toContain("table"); + await compileShards(split.shards); +}); + +test("aggregate initializers over several lines stay whole in the globals shard", async () => { + // The regex global's comment holds unbalanced brackets. + const split = splitLlvmProgram( + withGlobals(`@regex = internal global { ptr, i64 } { ptr @left, i64 0 } ; /(a|[b/ +@vt = internal constant [2 x { i64, ptr }] [ + { i64, ptr } { i64 1, ptr @left }, + { i64, ptr } { i64 2, ptr @right } +]`), + { minimumBytes: 0, targetBytes: 64 * 1024 }, + )!; + expect(split).not.toBeNull(); + const [globals, ...functions] = split.shards; + expect(globals!.source()).toContain( + "@vt = hidden constant [2 x { i64, ptr }] [\n { i64, ptr } { i64 1, ptr @left },\n { i64, ptr } { i64 2, ptr @right }\n]", + ); + for (const shard of functions) { + expect(shard.source()).toContain("@vt = external hidden constant [2 x { i64, ptr }]"); + expect(shard.source()).not.toContain("{ i64, ptr } { i64 1"); + } + expect(split.promotedSymbols).toEqual(expect.arrayContaining(["regex", "vt"])); + await compileShards(split.shards); +}); + +test("globals after the attributes and metadata join the globals shard", async () => { + const split = splitLlvmProgram(withGlobals("", "@flag = internal constant i1 true\n"), { + minimumBytes: 0, + targetBytes: 64 * 1024, + })!; + expect(split).not.toBeNull(); + const [globals, ...functions] = split.shards; + expect(globals!.source()).toContain("@flag = hidden constant i1 true"); + for (const shard of functions) + expect(shard.source()).toContain("@flag = external hidden constant i1"); + await compileShards(split.shards); +}); + +test("a module split from its file has the shards of the same module split in memory", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-llvm-split-file-")); + scratch.push(dir); + const options = { minimumBytes: 0, targetBytes: 64 * 1024 }; + const storage = withGlobals( + `@table = private unnamed_addr constant [2 x i8] c"\\01\\02" +@vt = internal constant [2 x { i64, ptr }] [ + { i64, ptr } { i64 1, ptr @left }, + { i64, ptr } { i64 2, ptr @right } +]`, + "@flag = internal constant i1 true\n", + ); + for (const [name, source] of [ + ["sample.ll", withGlobals("")], + ["storage.ll", storage], + ] as const) { + const path = join(dir, name); + await writeFile(path, source); + const fromFile = splitLlvmProgramFile(path, options)!; + expect(fromFile).not.toBeNull(); + const materialize = (split: NonNullable) => ({ + ...split, + shards: split.shards.map((shard) => ({ name: shard.name, source: shard.source() })), + }); + expect(materialize(fromFile)).toEqual(materialize(splitLlvmProgram(source, options)!)); } }); @@ -127,7 +233,7 @@ entry: for (const shard of split.shards) { const path = join(dir, shard.name); const object = `${path}.o`; - await writeFile(path, shard.source); + await writeFile(path, shard.source()); execFileSync("clang", ["-Wno-override-module", "-O2", "-c", path, "-o", object]); objects.push(object); } @@ -169,10 +275,10 @@ test("shared metadata bounds shard growth while preserving every definition", () expect(split).not.toBeNull(); expect(split!.shards.length).toBeGreaterThan(1); expect( - split!.shards.reduce((bytes, shard) => bytes + Buffer.byteLength(shard.source), 0), + split!.shards.reduce((bytes, shard) => bytes + Buffer.byteLength(shard.source()), 0), ).toBeLessThanOrEqual(2 * Buffer.byteLength(source)); const definitions = split!.shards.flatMap((shard) => - [...shard.source.matchAll(/^define (?:hidden )?i64 @(\w+)\(/gm)].map((match) => match[1]), + [...shard.source().matchAll(/^define (?:hidden )?i64 @(\w+)\(/gm)].map((match) => match[1]), ); expect(definitions.sort()).toEqual([...names, "left", "right", "public_entry"].sort()); expect(split!.publicSymbols).toEqual(["public_value", "public_entry"]); @@ -207,9 +313,9 @@ test("debug metadata stays on definitions when splitting LLVM modules", async () const dir = await mkdtemp(join(tmpdir(), "scriptc-debug-split-")); scratch.push(dir); for (const shard of split!.shards) { - expect(shard.source).not.toMatch(/^declare.*!dbg/m); + expect(shard.source()).not.toMatch(/^declare.*!dbg/m); const path = join(dir, shard.name); - await writeFile(path, shard.source); + await writeFile(path, shard.source()); const result = spawnSync("clang", ["-Wno-override-module", "-c", path, "-o", `${path}.o`], { encoding: "utf8", }); @@ -250,7 +356,7 @@ test("every shard compiles and its merged object exposes only canonical public d for (const shard of split.shards) { const source = join(dir, shard.name); const object = `${source}.o`; - await writeFile(source, shard.source); + await writeFile(source, shard.source()); execFileSync("clang", ["-Wno-override-module", "-c", source, "-o", object]); objects.push(object); } diff --git a/packages/compiler/src/backend/llvm/split.ts b/packages/compiler/src/backend/llvm/split.ts index 3fb41ce6b..aecef8df8 100644 --- a/packages/compiler/src/backend/llvm/split.ts +++ b/packages/compiler/src/backend/llvm/split.ts @@ -19,10 +19,15 @@ * canonical TU normally — cache optimization must never become correctness. */ +import { closeSync, openSync, readSync, statSync } from "node:fs"; + export interface LlvmProgramShard { /** Stable source identity used in object cache keys and diagnostics. */ name: string; - source: string; + /** Assembles the shard's LLVM text. A module split from a file reads its + * function bodies back from there on each call, so only the shards being + * written are in memory. */ + source: () => string; } export interface LlvmProgramSplit { @@ -54,12 +59,20 @@ const LIBRARY_TARGET_BYTES = 768 * 1024; const LIBRARY_MINIMUM_BYTES = 2 * 1024 * 1024; interface FunctionDef { - source: string; + /** UTF-8 length of the definition's text. */ + bytes: number; + /** The definition's text: its header (promoted to hidden linkage when + * internal), body and closing brace. A definition left in a file reads + * its body through `read`. */ + text: (read: RangeReader) => string; declaration: string; symbol: string; promoted: boolean; } +/** Reads the text between two byte offsets of the module file. */ +type RangeReader = (start: number, end: number) => string; + interface GlobalDef { line: string; declaration: string | null; @@ -72,50 +85,68 @@ function symbolOf(spelling: string): string | null { return match?.[1] ?? null; } -function functionDefAt( - lines: readonly string[], - start: number, -): { def: FunctionDef; end: number } | null { - const header = lines[start]!; +/** A definition's header line: the line its shard compiles, the + * declaration every other shard carries, and its linkage. */ +function functionHeader( + header: string, +): { line: string; declaration: string; symbol: string; promoted: boolean } | null { const match = /^define\s+(internal\s+)?(.+?)\s+(@[-$._A-Za-z][-$.\w]*)\((.*)\)(.*)\{(?:\s*;.*)?$/.exec( header, ); if (!match) return null; - let end = start + 1; - while (end < lines.length && lines[end] !== "}") end++; - if (end >= lines.length) return null; const symbol = symbolOf(match[3]!); if (symbol === null) return null; const promoted = match[1] !== undefined; const linkage = promoted ? "hidden " : ""; - const definitionHeader = promoted - ? header.replace(/^define internal /, "define hidden ") - : header; - const source = [definitionHeader, ...lines.slice(start + 1, end + 1)].join("\n"); + const line = promoted ? header.replace(/^define internal /, "define hidden ") : header; // Parameter names are legal on declarations. Preserve the emitted text so // attributes, zeroext, and varargs stay byte-exact. // A definition's DISubprogram cannot be attached to a declaration in a // different shard. Its owning definition retains the original metadata. const attrs = match[5]!.replace(/\s+!dbg !\d+/g, ""); const declaration = `declare ${linkage}${match[2]} ${match[3]}(${match[4]})${attrs}`.trimEnd(); - return { def: { source, declaration, symbol, promoted }, end }; + return { line, declaration, symbol, promoted }; +} + +function functionDefAt( + lines: readonly string[], + start: number, +): { def: FunctionDef; end: number } | null { + const header = functionHeader(lines[start]!); + if (header === null) return null; + let end = start + 1; + while (end < lines.length && lines[end] !== "}") end++; + if (end >= lines.length) return null; + const source = [header.line, ...lines.slice(start + 1, end + 1)].join("\n"); + return { + def: { + bytes: Buffer.byteLength(source), + text: () => source, + declaration: header.declaration, + symbol: header.symbol, + promoted: header.promoted, + }, + end, + }; } function globalDef(line: string): GlobalDef | null { const match = - /^(@[-$._A-Za-z][-$.\w]*)\s+=\s+(internal\s+)?(thread_local(?:\([^)]*\))?\s+)?(global|constant)\s+(.+)$/.exec( + /^(@[-$._A-Za-z][-$.\w]*)\s+=\s+((?:internal|private)\s+)?(thread_local(?:\([^)]*\))?\s+)?((?:local_)?unnamed_addr\s+)?(global|constant)\s+([\s\S]+)$/.exec( line, ); if (!match) return null; const symbol = symbolOf(match[1]!); if (symbol === null) return null; + // Internal and private definitions (such as private unnamed_addr lookup + // tables) are local to their module: shards reach them as hidden ones. const promoted = match[2] !== undefined; const threadLocal = match[3] === undefined ? "" : `${match[3].trim()} `; // The declaration needs only the complete type. Emitted initializers begin // after it; the generated LLVM subset uses first-class scalars/pointers or // balanced aggregate types, so one small scanner is sufficient. - const rest = match[5]!; + const rest = match[6]!; let depth = 0; let quote = false; let typeEnd = -1; @@ -133,13 +164,30 @@ function globalDef(line: string): GlobalDef | null { if (typeEnd < 0) return null; const type = rest.slice(0, typeEnd); return { - line: promoted ? line.replace(`${match[1]} = internal `, `${match[1]} = hidden `) : line, - declaration: `${match[1]} = external ${promoted ? "hidden " : ""}${threadLocal}${match[4]} ${type}`, + line: promoted ? line.replace(`${match[1]} = ${match[2]}`, `${match[1]} = hidden `) : line, + declaration: `${match[1]} = external ${promoted ? "hidden " : ""}${threadLocal}${match[5]} ${type}`, symbol, promoted, }; } +/** The change in aggregate nesting depth over one line of LLVM text, + * outside string constants and comments (a regex global's comment spells + * its source). */ +function bracketDepth(line: string): number { + let depth = 0; + let quote = false; + for (let i = 0; i < line.length; i++) { + const ch = line[i]!; + if (ch === '"') quote = !quote; + if (quote) continue; + if (ch === ";") break; + if (ch === "{" || ch === "[" || ch === "<" || ch === "(") depth++; + else if (ch === "}" || ch === "]" || ch === ">" || ch === ")") depth--; + } + return depth; +} + function stableBucket(symbol: string, count: number): number { let hash = 0x811c9dc5; for (let i = 0; i < symbol.length; i++) { @@ -154,7 +202,7 @@ function chunksOf( targetBytes: number, maximumBuckets: number, ): { bucket: number; functions: FunctionDef[] }[] { - const totalBytes = functions.reduce((sum, fn) => sum + Buffer.byteLength(fn.source) + 2, 0); + const totalBytes = functions.reduce((sum, fn) => sum + fn.bytes + 2, 0); // A power-of-two bucket count changes only when the program crosses a wide // size band. Ordinary edits therefore keep every function in the same // symbol-hash bucket; ceil(total/target) would reshuffle the whole program @@ -168,6 +216,74 @@ function chunksOf( ); } +/** A module's top-level forms, as the splitter classifies them. */ +interface ModuleParts { + preamble: string[]; + globals: GlobalDef[]; + functions: FunctionDef[]; + trailer: string[]; +} + +/** Classifies a module one line at a time. `define` lines go to + * `onFunction`, which takes the following lines through the definition's + * closing brace; every other line is offered to `line`. */ +class ModuleReader { + readonly parts: ModuleParts = { preamble: [], globals: [], functions: [], trailer: [] }; + private sawDefinition = false; + private inTrailer = false; + /** A global definition whose aggregate initializer continues over the + * following lines (vtable arrays list one element per line). */ + private pendingGlobal: { text: string; depth: number } | null = null; + failed = false; + + /** Whether a definition may start here (none may follow the trailer). */ + startFunction(): boolean { + if (this.inTrailer || this.pendingGlobal !== null) this.failed = true; + this.sawDefinition = true; + return !this.failed; + } + + line(line: string): void { + if (this.pendingGlobal !== null) { + this.pendingGlobal.text += "\n" + line; + this.pendingGlobal.depth += bracketDepth(line); + if (this.pendingGlobal.depth <= 0) { + const { text } = this.pendingGlobal; + this.pendingGlobal = null; + this.global(text); + } + return; + } + if (line.startsWith("@") && line.includes(" = ")) { + const depth = bracketDepth(line); + if (depth > 0) this.pendingGlobal = { text: line, depth }; + else this.global(line); + return; + } + if (line.startsWith("attributes ") || line.startsWith("!")) this.inTrailer = true; + if (this.inTrailer) this.parts.trailer.push(line); + else if (!this.sawDefinition || line.trim() !== "") this.parts.preamble.push(line); + } + + /** Globals may follow the attributes and metadata (the feature flags the + * link-features step appends); they are module storage like any other. */ + private global(definition: string): void { + const parsed = globalDef(definition); + if (parsed === null) { + // External declarations are already valid in every shard. + if (/^@.+\s+=\s+external\s+/.test(definition)) this.parts.preamble.push(definition); + else this.failed = true; + } else { + this.parts.globals.push(parsed); + this.sawDefinition = true; + } + } + + finish(): ModuleParts | null { + return this.failed || this.pendingGlobal !== null ? null : this.parts; + } +} + export function splitLlvmProgram( source: string, options: LlvmProgramSplitOptions = {}, @@ -175,42 +291,129 @@ export function splitLlvmProgram( const minimumBytes = options.minimumBytes ?? DEFAULT_MINIMUM_BYTES; const sourceBytes = Buffer.byteLength(source); if (sourceBytes < minimumBytes) return null; - const targetBytes = Math.max(64 * 1024, options.targetBytes ?? DEFAULT_TARGET_BYTES); const lines = source.split("\n"); - const preamble: string[] = []; - const globals: GlobalDef[] = []; - const functions: FunctionDef[] = []; - const trailer: string[] = []; - let sawDefinition = false; - let inTrailer = false; - for (let i = 0; i < lines.length; i++) { + const reader = new ModuleReader(); + for (let i = 0; i < lines.length && !reader.failed; i++) { const line = lines[i]!; if (line.startsWith("define ")) { - if (inTrailer) return null; + if (!reader.startFunction()) break; const parsed = functionDefAt(lines, i); if (parsed === null) return null; - functions.push(parsed.def); - sawDefinition = true; + reader.parts.functions.push(parsed.def); i = parsed.end; - continue; - } - if (line.startsWith("@") && line.includes(" = ")) { - if (inTrailer) return null; - const parsed = globalDef(line); - if (parsed === null) { - // External declarations are already valid in every shard. - if (/^@.+\s+=\s+external\s+/.test(line)) preamble.push(line); - else return null; - } else { - globals.push(parsed); - sawDefinition = true; - } - continue; - } - if (line.startsWith("attributes ") || line.startsWith("!")) inTrailer = true; - if (inTrailer) trailer.push(line); - else if (!sawDefinition || line.trim() !== "") preamble.push(line); + } else reader.line(line); } + const parts = reader.finish(); + return parts === null ? null : shardModule(parts, sourceBytes, options, null); +} + +/** The read size of splitLlvmProgramFile's scan. */ +const FILE_CHUNK_BYTES = 16 * 1024 * 1024; + +/** splitLlvmProgram over LLVM text already written to `path`, for modules + * too large to hold in memory more than once (or past the longest string the + * engine allows). Function bodies stay in the file: each shard's source + * reads its bodies when it is read, so only the shards being written at a + * time are in memory. */ +export function splitLlvmProgramFile( + path: string, + options: LlvmProgramSplitOptions = {}, +): LlvmProgramSplit | null { + const minimumBytes = options.minimumBytes ?? DEFAULT_MINIMUM_BYTES; + const sourceBytes = statSync(path).size; + if (sourceBytes < minimumBytes) return null; + const reader = new ModuleReader(); + // The definition being read: its header and where its body starts. + let current: { header: NonNullable>; body: number } | null = + null; + const onLine = (bytes: Buffer, start: number, end: number, offset: number): void => { + if (current !== null) { + // Only the closing brace ends a definition; its body is not decoded. + if (end - start === 1 && bytes[start] === 0x7d) { + const { header, body } = current; + const bodyEnd = offset + 1; + reader.parts.functions.push({ + bytes: Buffer.byteLength(header.line) + 1 + (bodyEnd - body), + text: (read) => `${header.line}\n${read(body, bodyEnd)}`, + declaration: header.declaration, + symbol: header.symbol, + promoted: header.promoted, + }); + current = null; + } + return; + } + const line = bytes.toString("utf8", start, end); + if (line.startsWith("define ")) { + const header = functionHeader(line); + if (header === null || !reader.startFunction()) reader.failed = true; + else current = { header, body: offset + (end - start) + 1 }; + } else reader.line(line); + }; + const fd = openSync(path, "r"); + try { + let carry: Buffer = Buffer.alloc(0); + let carryOffset = 0; + for (let position = 0; position < sourceBytes && !reader.failed;) { + const chunk = Buffer.allocUnsafe(Math.min(FILE_CHUNK_BYTES, sourceBytes - position)); + const read = readSync(fd, chunk, 0, chunk.length, position); + if (read === 0) break; + const data = + carry.length === 0 + ? chunk.subarray(0, read) + : Buffer.concat([carry, chunk.subarray(0, read)]); + const dataOffset = carry.length === 0 ? position : carryOffset; + position += read; + let lineStart = 0; + for (let newline = data.indexOf(10); newline >= 0; newline = data.indexOf(10, lineStart)) { + onLine(data, lineStart, newline, dataOffset + lineStart); + lineStart = newline + 1; + } + carry = Buffer.from(data.subarray(lineStart)); + carryOffset = dataOffset + lineStart; + } + // The text after the last newline is a line too (empty when the file + // ends with one), as String.prototype.split answers it. + if (!reader.failed) onLine(carry, 0, carry.length, carryOffset); + } finally { + closeSync(fd); + } + if (current !== null) return null; + const parts = reader.finish(); + return parts === null ? null : shardModule(parts, sourceBytes, options, path); +} + +/** Runs `use` with a reader of `path`'s byte ranges (or of nothing, for a + * module held in memory). */ +function withRangeReader(path: string | null, use: (read: RangeReader) => T): T { + if (path === null) + return use(() => { + throw new Error("an in-memory LLVM module has no file to read"); + }); + const fd = openSync(path, "r"); + try { + return use((start, end) => { + const bytes = Buffer.allocUnsafe(end - start); + for (let done = 0; done < bytes.length;) { + const read = readSync(fd, bytes, done, bytes.length - done, start + done); + if (read === 0) throw new Error(`${path} ended before byte ${end}`); + done += read; + } + return bytes.toString("utf8"); + }); + } finally { + closeSync(fd); + } +} + +function shardModule( + parts: ModuleParts, + sourceBytes: number, + options: LlvmProgramSplitOptions, + path: string | null, +): LlvmProgramSplit | null { + const { preamble, globals, functions, trailer } = parts; + const targetBytes = Math.max(64 * 1024, options.targetBytes ?? DEFAULT_TARGET_BYTES); if (functions.length < 2) return null; const globalDecls = globals.flatMap((global) => global.declaration === null ? [] : [global.declaration], @@ -230,16 +433,22 @@ export function splitLlvmProgram( const chunks = chunksOf(functions, targetBytes, 2 ** Math.floor(Math.log2(bucketLimit))); if (chunks.length < 2) return null; const tail = trailer.length === 0 ? "" : `\n${trailer.join("\n")}`; - const functionShards = chunks.map(({ bucket, functions: chunk }) => { + // A shard's source is assembled when it is read; a module split from a + // file reads its function bodies from there each time. + const functionShards = chunks.map(({ bucket, functions: chunk }): LlvmProgramShard => { const owned = new Set(chunk); - const declarations = functions.filter((fn) => !owned.has(fn)).map((fn) => fn.declaration); return { name: `program-f${bucket.toString().padStart(3, "0")}.ll`, - source: [ - [...preamble, ...globalDecls, ...declarations, ""].join("\n"), - ...chunk.map((fn) => fn.source), - tail, - ].join("\n"), + source: () => { + const declarations = functions.filter((fn) => !owned.has(fn)).map((fn) => fn.declaration); + return withRangeReader(path, (read) => + [ + [...preamble, ...globalDecls, ...declarations, ""].join("\n"), + ...chunk.map((fn) => fn.text(read)), + tail, + ].join("\n"), + ); + }, }; }); // Global initializer changes are common (literal text, generated tables) @@ -247,15 +456,15 @@ export function splitLlvmProgram( // all module storage in one dedicated shard that declares every function. const globalShard: LlvmProgramShard = { name: "program-globals.ll", - source: [ - [...preamble, ...functions.map((fn) => fn.declaration), ""].join("\n"), - ...globals.map((global) => global.line), - tail, - ].join("\n"), + source: () => + [ + [...preamble, ...functions.map((fn) => fn.declaration), ""].join("\n"), + ...globals.map((global) => global.line), + tail, + ].join("\n"), }; - const shards = [globalShard, ...functionShards]; return { - shards, + shards: [globalShard, ...functionShards], promotedSymbols: [ ...globals.filter((global) => global.promoted).map((global) => global.symbol), ...functions.filter((fn) => fn.promoted).map((fn) => fn.symbol), diff --git a/packages/compiler/src/backend/llvm/stack-callbacks.ts b/packages/compiler/src/backend/llvm/stack-callbacks.ts index 5c404896a..20de50372 100644 --- a/packages/compiler/src/backend/llvm/stack-callbacks.ts +++ b/packages/compiler/src/backend/llvm/stack-callbacks.ts @@ -32,10 +32,12 @@ export class StackCallbacks { const count = B.tmp(); B.line(`${count} = getelementptr inbounds %ScrClosure, ptr ${closure}, i32 0, i32 2`); B.line(`store ${host.sizeType} ${value.captures.length}, ptr ${count}`); - if (this.functions.get(value.fnName)?.ownsPrototype) { + const target = this.functions.get(value.fnName); + const functionKind = target ? host.closureFunctionKind(target) : 0; + if (functionKind !== 0) { const kind = B.tmp(); B.line(`${kind} = getelementptr inbounds %ScrClosure, ptr ${closure}, i32 0, i32 4`); - B.line(`store i32 4, ptr ${kind}`); + B.line(`store i32 ${functionKind}, ptr ${kind}`); } value.captures.forEach((localId, index) => { const box = host.retainBox(host.captureBox(localId)); diff --git a/packages/compiler/src/backend/llvm/stack-captures.ts b/packages/compiler/src/backend/llvm/stack-captures.ts index f0812b7b0..40e1a0428 100644 --- a/packages/compiler/src/backend/llvm/stack-captures.ts +++ b/packages/compiler/src/backend/llvm/stack-captures.ts @@ -105,13 +105,7 @@ export class StackCaptures { // sites, including duplicated finally bodies and versioned loops. if (!this.locals.has(id) || this.remaining < 48) return null; const ref = isRefCounted(type); - if ( - !ref && - type.kind !== "f64" && - type.kind !== "date" && - type.kind !== "procStream" && - type.kind !== "bool" - ) + if (!ref && type.kind !== "f64" && type.kind !== "procStream" && type.kind !== "bool") return null; this.remaining -= 48; const B = host.B; diff --git a/packages/compiler/src/backend/llvm/stack-objects.test.ts b/packages/compiler/src/backend/llvm/stack-objects.test.ts index a74878080..c4f2bedc4 100644 --- a/packages/compiler/src/backend/llvm/stack-objects.test.ts +++ b/packages/compiler/src/backend/llvm/stack-objects.test.ts @@ -286,8 +286,9 @@ function bump(s: State): State { return { ...s, count: s.count + 1 }; } export function tokens(n: number, w: string): number { const t = { kind: "id", text: w, pos: n }; const u = make("num", w, n); - const [q, r] = divmod(n, 7); - return t.pos + width(u) + width({ kind: w, text: w, pos: 0 }) + q + r; + // Read by index: a destructured literal result is inlined instead. + const qr = divmod(n, 7); + return t.pos + width(u) + width({ kind: w, text: w, pos: 0 }) + qr[0] + qr[1]; } export function states(n: number, w: string): number { let s: State = { name: w, count: 0 }; diff --git a/packages/compiler/src/backend/llvm/stack-objects.ts b/packages/compiler/src/backend/llvm/stack-objects.ts index c7a58a7ea..ae2b904db 100644 --- a/packages/compiler/src/backend/llvm/stack-objects.ts +++ b/packages/compiler/src/backend/llvm/stack-objects.ts @@ -44,8 +44,9 @@ export type StackObjectFieldOwner = (fields: readonly StackObjectField[]) => voi * standalone scalar class whose constructor never lets `this` escape * (record-results.ts). The caller proves that every use of the value only * projects it. Record fields may own references; the frame releases them - * (stackObjectFields). Published shapes keep heap storage: their field - * guards treat the immortal header as a frozen object. */ + * (stackObjectFields). Published and freezable shapes keep heap storage: + * their field guards treat the immortal header as a frozen object, and a + * frozen record keeps its frozen bit in a counted header. */ export function canStackObject(host: LlvmEmitterContext, e: IrExpr): e is StackObjectSource { // A ThreadSlots key keeps its slot index for its lifetime and has its // values released when it is freed: it lives on the heap. @@ -53,6 +54,8 @@ export function canStackObject(host: LlvmEmitterContext, e: IrExpr): e is StackO return false; if (e.type.kind === "record" && host.publishedTypes?.recordGuarded(e.type.shapeId) === true) return false; + if (e.type.kind === "record" && host.recordsById.get(e.type.shapeId)?.freezable === true) + return false; // A producer with a result-slot entry builds its fresh result in the // caller's storage. if (e.kind === "call") return host.fnByName.has(resultEntryName(e.callee)); diff --git a/packages/compiler/src/backend/llvm/string-lifetimes.test.ts b/packages/compiler/src/backend/llvm/string-lifetimes.test.ts index f85addcf8..72fc57752 100644 --- a/packages/compiler/src/backend/llvm/string-lifetimes.test.ts +++ b/packages/compiler/src/backend/llvm/string-lifetimes.test.ts @@ -341,7 +341,7 @@ const methodCases: { { method: "normalize", args: [ref("argument")], result: STRING, target: "normalize" }, { method: "isWellFormed", args: [], result: BOOL, target: "is_well_formed" }, { method: "toWellFormed", args: [], result: STRING, target: "to_well_formed" }, - { method: "cpAt", args: [num(1)], result: STRING, target: "cp_at" }, + { method: "cpAtByte", args: [num(1)], result: STRING, target: "cp_at_byte", inline: true }, ]; test.each(methodCases)( @@ -478,7 +478,9 @@ test("string comparisons decide identity, lengths and leading bytes before the r expect(units).toContain("%below = icmp ult i8 %bx, %by"); expect(units).toContain("%tailable = and i1 %samelen, %short"); expect(units).toContain("%anyascii = icmp slt i32 %cws, 0"); - expect(units).toContain("call i32 @scr_str_cmp(ptr %a, ptr %b)"); + expect(units).toContain( + "call i32 @scr_str_cmp(ptr %a, ptr %b) nounwind willreturn memory(argmem: read)", + ); expect(units).toContain( "call i32 @scr_str_cmp_u16_bytes(ptr %a, ptr %b) nounwind willreturn memory(argmem: read)", ); diff --git a/packages/compiler/src/backend/llvm/string-lifetimes.ts b/packages/compiler/src/backend/llvm/string-lifetimes.ts index 6c3079cc1..5a4504813 100644 --- a/packages/compiler/src/backend/llvm/string-lifetimes.ts +++ b/packages/compiler/src/backend/llvm/string-lifetimes.ts @@ -30,7 +30,8 @@ export function borrowsStringInputs(method: IrStrIntrinsicMethod): boolean { case "normalize": case "isWellFormed": case "toWellFormed": - case "cpAt": + case "byteLength": + case "cpAtByte": return true; default: return false; diff --git a/packages/compiler/src/backend/llvm/string-reads.ts b/packages/compiler/src/backend/llvm/string-reads.ts index 441c925f3..1f77de2c2 100644 --- a/packages/compiler/src/backend/llvm/string-reads.ts +++ b/packages/compiler/src/backend/llvm/string-reads.ts @@ -24,6 +24,12 @@ import { literalCapWord, literalCapWord32 } from "./string-key-hash.js"; export const STRING_UTF16_LEN_DECL = "declare double @scr_str_utf16_len(ptr)"; export const STRING_CHAR_CODE_AT_DECL = "declare double @scr_str_char_code_at(ptr, double)"; +/** Converts a string to UTF-16 units and publishes it as the scan receiver + * when the loop about to read it can read enough of it (the other two + * operands are the range of indexes it reads; bytes-loop-versions.ts). + * Like the fallbacks below, it can evict an index entry, so it carries no + * memory restriction. */ +export const STRING_SCAN_UNITS_DECL = "declare void @scr_str_scan_units_for(ptr, double, double)"; /** The runtime fallbacks are cold: both common cases read inline. They * carry no * memory restriction: a fallback can evict an index entry, which resets @@ -37,6 +43,10 @@ export const STRING_CHAR_CODE_AT_INT_MARKER = "; uses @sc_str_char_code_at_int"; export const STRING_CHAR_AT_DECL = "declare ptr @scr_str_char_at(ptr, double)"; /** Requests the integer-index charAt helper, as the marker above does. */ export const STRING_CHAR_AT_INT_MARKER = "; uses @sc_str_char_at_int"; +/** The code-point character starting at a byte offset (the string + * iterator's step). Requested together with STRING_CHAR_AT_DECL, which + * defines the ASCII table. */ +export const STRING_CP_AT_BYTE_DECL = "declare ptr @scr_str_cp_at_byte(ptr, i64)"; /** The proven-ASCII flag of string `s`, as an i1. */ export function emitStringAsciiFlag(host: LlvmEmitterContext, s: string): string { @@ -75,6 +85,19 @@ export function emitAsciiCharCode(host: LlvmEmitterContext, s: string, index: st return code; } +/** Unit `index` (an i64, below their count) of a string's UTF-16 `units` + * (ScrStrScan): one 16-bit load, as a double. */ +export function emitUnitCharCode(host: LlvmEmitterContext, units: string, index: string): string { + const B = host.B; + const pointer = B.tmp(), + unit = B.tmp(), + code = B.tmp(); + B.line(`${pointer} = getelementptr inbounds i16, ptr ${units}, i64 ${index}`); + B.line(`${unit} = load i16, ptr ${pointer}`); + B.line(`${code} = uitofp i16 ${unit} to double`); + return code; +} + /** The one-character string of unit `index` (an i64) of a proven-ASCII * string `s` whose length covers it: the immortal `@sc_ascii_chars` entry * for its byte, as the inline charAt helpers answer. */ @@ -114,6 +137,15 @@ function loadLength(sz: string): string[] { ]; } +/** Whether emitted reads use the runtime's scan receiver: from ordinary + * global storage, or, in worker executables, from the thread's static TLS + * block (executableTls). */ +export function stringScanInline(host: LlvmEmitterContext): boolean { + return ( + host.executableTls || (host.mod.workers !== true && host.mod.lib?.threadInstances !== true) + ); +} + /** The runtime's ScrStrScan record and the declaration naming it. */ export function stringScanType(sz: string): string { return `{ ptr, ptr, ${sz} }`; @@ -323,6 +355,29 @@ export const STRING_READ_HELPERS: readonly StringReadHelper[] = [ ]; }, }, + { + // The string iterator's step at byte `%b`, a character start below the + // byte count: an ASCII byte is the table entry for it; any other lead + // byte takes the runtime, which copies the character's bytes. + decl: STRING_CP_AT_BYTE_DECL, + define: (sz, attrs) => [ + `define internal ptr @sc_str_cp_at_byte(ptr %s, i64 %b) ${attrs} {`, + `entry:`, + ` %data = getelementptr inbounds %ScrStr, ptr %s, i32 1`, + ` %p = getelementptr inbounds i8, ptr %data, i64 %b`, + ` %byte = load i8, ptr %p`, + ` %ascii = icmp sge i8 %byte, 0`, + ` br i1 %ascii, label %read, label %slow`, + `read:`, + ` %code = zext i8 %byte to i64`, + ` %char = getelementptr inbounds [128 x { ${sz}, ${sz}, ${sz}, [2 x i8] }], ptr @sc_ascii_chars, i64 0, i64 %code`, + ` ret ptr %char`, + `slow:`, + ` %r = call ptr @scr_str_cp_at_byte(ptr %s, i64 %b) nounwind willreturn`, + ` ret ptr %r`, + `}`, + ], + }, { // An exact integer index, as for sc_str_char_code_at_int. Requested // together with STRING_CHAR_AT_DECL, which defines the table. diff --git a/packages/compiler/src/backend/llvm/string-slices.test.ts b/packages/compiler/src/backend/llvm/string-slices.test.ts index b2656e560..e53bf38c6 100644 --- a/packages/compiler/src/backend/llvm/string-slices.test.ts +++ b/packages/compiler/src/backend/llvm/string-slices.test.ts @@ -1,8 +1,19 @@ import { expect, test } from "vitest"; -import { BOOL, F64, STRING, VOID, type IrExpr, type IrFunction, type IrStmt } from "../../ir/ir.js"; +import { + BOOL, + F64, + STRING, + VOID, + type IrExpr, + type IrFunction, + type IrStmt, + type IrType, +} from "../../ir/ir.js"; import { analyzeCallLifetimes } from "./call-lifetimes.js"; import { + findLentRecordFields, findLentStringParameters, + findRecordFieldProducers, findScalarStringSlices, findScratchStringSlices, } from "./string-slices.js"; @@ -142,12 +153,12 @@ function lending(body: IrStmt[]): IrFunction { function scratch(fn: IrFunction, extra: IrFunction[] = []): Set { const functions = new Map([fn, ...extra].map((f) => [f.name, f])); const lifetimes = analyzeCallLifetimes(functions); - return findScratchStringSlices( - fn, - lifetimes, - findScalarStringSlices(fn, lifetimes), - findLentStringParameters(functions, lifetimes), - ); + const strings = findLentStringParameters(functions, lifetimes); + return findScratchStringSlices(fn, lifetimes, findScalarStringSlices(fn, lifetimes), { + strings, + records: findLentRecordFields(functions, lifetimes, strings), + producers: findRecordFieldProducers(functions), + }); } test("slices read only by lending consumers use a stack buffer", () => { const inline = slice("source", 0, 3); @@ -242,3 +253,177 @@ test("helper parameters read only by lending consumers accept stack slices", () const all = new Map([fn, ...functions].map((f) => [f.name, f])); expect([...findLentStringParameters(all, analyzeCallLifetimes(all)).keys()]).toEqual(["isGet"]); }); + +const TOKEN: IrType = { kind: "record", shapeId: "token" }; +const record = (id: string): IrExpr => ({ kind: "varRef", localId: id, type: TOKEN, loc }); +const textOf = (obj: IrExpr): IrExpr => ({ + kind: "recordGet", + obj, + shapeId: "token", + field: "text", + type: STRING, + loc, +}); +const lengthOf = (receiver: IrExpr): IrExpr => ({ + kind: "strIntrinsic", + method: "length", + receiver, + args: [], + type: F64, + loc, +}); +const tokenLit = (text: IrExpr): IrExpr => ({ + kind: "recordLit", + fields: [ + { name: "text", value: text }, + { name: "pos", value: { kind: "numLit", value: 1, type: F64, loc } }, + ], + type: TOKEN, + loc, +}); +const keepIn = (value: IrExpr): IrStmt => ({ + kind: "exprStmt", + expr: { kind: "arrayLit", elems: [value], type: { kind: "array", elem: STRING }, loc }, + loc, +}); +/** A function of one record parameter `t`. */ +function recordHelper(name: string, body: IrStmt[]): IrFunction { + return { + name, + params: [{ localId: "t", name: "t", type: TOKEN }], + locals: [{ id: "t", name: "t", type: TOKEN, mutable: false }], + returnType: F64, + loc, + body, + }; +} +/** `make(text)` returns `{ text, pos: 1 }`; `twice` also reads its parameter elsewhere. */ +function producerHelper(name: string, extra: IrStmt[] = []): IrFunction { + return { + name, + params: [{ localId: "text", name: "text", type: STRING }], + locals: [{ id: "text", name: "text", type: STRING, mutable: false }], + returnType: TOKEN, + loc, + body: [...extra, { kind: "return", value: tokenLit(ref("text")), loc }], + }; +} +function containing(init: IrExpr, declare: IrExpr, uses: IrStmt[]): IrFunction { + const fn = lending([ + { kind: "varDecl", localId: "word", init, loc }, + { kind: "varDecl", localId: "tok", init: declare, loc }, + ...uses, + ]); + fn.locals.push({ id: "tok", name: "tok", type: TOKEN, mutable: false }); + return fn; +} +const reads = recordHelper("reads", [ + { kind: "return", value: lengthOf(textOf(record("t"))), loc }, +]); +const keepsText = recordHelper("keepsText", [ + keepIn(textOf(record("t"))), + { kind: "return", value: { kind: "numLit", value: 0, type: F64, loc }, loc }, +]); +const escapes = recordHelper("escapes", [ + { + kind: "exprStmt", + expr: { kind: "arrayLit", elems: [record("t")], type: { kind: "array", elem: TOKEN }, loc }, + loc, + }, + { kind: "return", value: { kind: "numLit", value: 0, type: F64, loc }, loc }, +]); +const passCall = (callee: string, arg: IrExpr): IrStmt => ({ + kind: "exprStmt", + expr: { kind: "call", callee, args: [arg], type: F64, loc }, + loc, +}); + +test("a slice stored in a record that is only projected uses a stack buffer", () => { + const helpers = [reads, keepsText, escapes, producerHelper("make")]; + for (const declare of [tokenLit(ref("word")), callOf("make", ref("word"))]) { + if (declare.kind === "call") declare.type = TOKEN; + const init = slice("source", 1, 5); + const fn = containing(init, declare, [ + { kind: "exprStmt", expr: equals(textOf(record("tok"))), loc }, + passCall("reads", record("tok")), + ]); + expect(scratch(fn, helpers).has(init)).toBe(true); + } + const all = new Map(helpers.map((f) => [f.name, f])); + const lifetimes = analyzeCallLifetimes(all); + const lent = findLentRecordFields(all, lifetimes, findLentStringParameters(all, lifetimes)); + expect(lent.get("reads")?.get(0)).toEqual(new Set()); + expect(lent.get("keepsText")?.get(0)).toEqual(new Set(["text"])); + expect(lent.has("escapes")).toBe(false); + expect(findRecordFieldProducers(all).get("make")).toEqual(new Map([[0, "text"]])); +}); + +test.each([ + "kept-field", + "returned", + "kept-by-callee", + "escapes-callee", + "captured", + "producer-reuse", + "producer-capture", +])("a slice stored in a record that may be kept (%s) stays owned", (reason) => { + // The factory also stores a closure over its text parameter. + const capturing = producerHelper("capturing"); + const literal = (capturing.body[0] as IrStmt & { kind: "return" }).value as IrExpr & { + kind: "recordLit"; + }; + literal.fields.push({ + name: "get", + value: { kind: "closure", fnName: "get", captures: ["text"], type: F64, loc }, + }); + const helpers = [ + reads, + keepsText, + escapes, + producerHelper("make"), + producerHelper("twice", [keepIn(ref("text"))]), + capturing, + ]; + const init = slice("source", 1, 5); + const declare = + reason === "producer-reuse" + ? callOf("twice", ref("word")) + : reason === "producer-capture" + ? callOf("capturing", ref("word")) + : tokenLit(ref("word")); + if (declare.kind === "call") declare.type = TOKEN; + const uses: IrStmt[] = [{ kind: "exprStmt", expr: equals(textOf(record("tok"))), loc }]; + if (reason === "kept-field") uses.push(keepIn(textOf(record("tok")))); + if (reason === "returned") uses.push({ kind: "return", value: record("tok"), loc }); + if (reason === "kept-by-callee") uses.push(passCall("keepsText", record("tok"))); + if (reason === "escapes-callee") uses.push(passCall("escapes", record("tok"))); + const fn = containing(init, declare, uses); + if (reason === "captured") fn.locals.find((l) => l.id === "tok")!.boxed = true; + expect(scratch(fn, helpers).has(init)).toBe(false); +}); + +test("searches of string arrays lend their needle", () => { + const keywords: IrExpr = { + kind: "varRef", + localId: "keywords", + type: { kind: "array", elem: STRING }, + loc, + }; + const classify = helper("classify", [ + { + kind: "return", + value: { + kind: "arrIntrinsic", + method: "includes", + receiver: keywords, + args: [ref("text")], + type: BOOL, + loc, + }, + loc, + }, + ]); + const init = slice("source", 0, 3); + const fn = lending([{ kind: "exprStmt", expr: callOf("classify", init), loc }]); + expect(scratch(fn, [classify]).has(init)).toBe(true); +}); diff --git a/packages/compiler/src/backend/llvm/string-slices.ts b/packages/compiler/src/backend/llvm/string-slices.ts index d00c681d6..c1e1e2593 100644 --- a/packages/compiler/src/backend/llvm/string-slices.ts +++ b/packages/compiler/src/backend/llvm/string-slices.ts @@ -8,6 +8,7 @@ import { everyExprChild, everyStmtChild, everyStmtList } from "../../ir/traverse import { F64_INF, f64Lit, sizeToDouble } from "./common.js"; import type { CallLifetimes } from "./call-lifetimes.js"; import type { LlValue, LlvmEmitterContext } from "./expr-context.js"; +import { emitStringAsciiFlag, emitStringByteLength } from "./string-reads.js"; type Slice = IrExpr & { kind: "strIntrinsic" }; @@ -66,6 +67,58 @@ export function findScalarStringSlices( return slices; } +/** scr_slice_unit: a slice()/substring() bound `v` over `length` units. A + * non-negative bound clamps to the length; a negative one counts from the + * end for slice (truncated toward zero) and is 0 for substring, as is NaN. */ +function emitSliceUnit( + host: LlvmEmitterContext, + v: string, + length: string, + substring: boolean, +): string { + const B = host.B; + const size = host.sizeType; + const lengthNumber = B.tmp(), + nonNegative = B.tmp(), + past = B.tmp(), + whole = B.tmp(), + clamped = B.tmp(), + result = B.tmp(); + B.line(`${lengthNumber} = ${sizeToDouble(size)} ${size} ${length} to double`); + B.line(`${nonNegative} = fcmp oge double ${v}, 0.0`); + B.line(`${past} = fcmp oge double ${v}, ${lengthNumber}`); + // Frozen: the conversion is only used where the bound is in range. + const raw = B.tmp(); + B.line(`${raw} = fptosi double ${v} to i64`); + B.line(`${whole} = freeze i64 ${raw}`); + let unit = whole; + if (size !== "i64") { + unit = B.tmp(); + B.line(`${unit} = trunc i64 ${whole} to ${size}`); + } + B.line(`${clamped} = select i1 ${past}, ${size} ${length}, ${size} ${unit}`); + if (substring) { + B.line(`${result} = select i1 ${nonNegative}, ${size} ${clamped}, ${size} 0`); + return result; + } + // Negative: (-length, -1] counts back from the end; (-1, 0) truncates to + // 0, and at or below -length, and NaN, give 0. + const negLength = B.tmp(), + inside = B.tmp(), + zero = B.tmp(), + back = B.tmp(), + counted = B.tmp(), + negative = B.tmp(); + B.line(`${negLength} = fneg double ${lengthNumber}`); + B.line(`${inside} = fcmp ogt double ${v}, ${negLength}`); + B.line(`${zero} = icmp eq ${size} ${unit}, 0`); + B.line(`${back} = add ${size} ${length}, ${unit}`); + B.line(`${counted} = select i1 ${zero}, ${size} 0, ${size} ${back}`); + B.line(`${negative} = select i1 ${inside}, ${size} ${counted}, ${size} 0`); + B.line(`${result} = select i1 ${nonNegative}, ${size} ${clamped}, ${size} ${negative}`); + return result; +} + export interface StringSliceSnapshot { source: string; range: string; @@ -87,10 +140,55 @@ export function emitStringSliceSnapshot( `${range} = alloca { ${host.sizeType}, ${host.sizeType}, i32 }`, ); B.line(`store ptr ${source.name}, ptr ${sourceSlot}`); + const start = args[0]?.name ?? f64Lit(0), + end = args[1]?.name ?? F64_INF; + const substring = slice.method === "substring"; + // A proven-ASCII receiver's units are its bytes, so the range is the + // runtime's clamped bounds over its byte length, and no boundary splits a + // surrogate pair (scr_str_slice_range); other strings ask the runtime. + const ascii = B.newLabel("slice.range.ascii"), + runtime = B.newLabel("slice.range.runtime"), + done = B.newLabel("slice.range.done"); + B.condBr(emitStringAsciiFlag(host, source.name), ascii, runtime); + B.startBlock(ascii); + const size = host.sizeType; + const length = emitStringByteLength(host, source.name); + let from = emitSliceUnit(host, start, length, substring), + to = emitSliceUnit(host, end, length, substring); + if (substring) { + const swap = B.tmp(), + low = B.tmp(), + high = B.tmp(); + B.line(`${swap} = icmp ugt ${size} ${from}, ${to}`); + B.line(`${low} = select i1 ${swap}, ${size} ${to}, ${size} ${from}`); + B.line(`${high} = select i1 ${swap}, ${size} ${from}, ${size} ${to}`); + from = low; + to = high; + } + const nonEmpty = B.tmp(), + difference = B.tmp(), + units = B.tmp(), + startPtr = B.tmp(), + lengthPtr = B.tmp(), + splitPtr = B.tmp(); + B.line(`${nonEmpty} = icmp ugt ${size} ${to}, ${from}`); + B.line(`${difference} = sub ${size} ${to}, ${from}`); + B.line(`${units} = select i1 ${nonEmpty}, ${size} ${difference}, ${size} 0`); + const rangeType = `{ ${size}, ${size}, i32 }`; + B.line(`${startPtr} = getelementptr inbounds ${rangeType}, ptr ${range}, i32 0, i32 0`); + B.line(`store ${size} ${from}, ptr ${startPtr}`); + B.line(`${lengthPtr} = getelementptr inbounds ${rangeType}, ptr ${range}, i32 0, i32 1`); + B.line(`store ${size} ${units}, ptr ${lengthPtr}`); + B.line(`${splitPtr} = getelementptr inbounds ${rangeType}, ptr ${range}, i32 0, i32 2`); + B.line(`store i32 0, ptr ${splitPtr}`); + B.br(done); + B.startBlock(runtime); host.declare("declare void @scr_str_slice_range(ptr, double, double, i1 zeroext, ptr)"); B.line( - `call void @scr_str_slice_range(ptr ${source.name}, double ${args[0]?.name ?? f64Lit(0)}, double ${args[1]?.name ?? F64_INF}, i1 ${slice.method === "substring" ? "true" : "false"}, ptr ${range})`, + `call void @scr_str_slice_range(ptr ${source.name}, double ${start}, double ${end}, i1 ${substring ? "true" : "false"}, ptr ${range})`, ); + B.br(done); + B.startBlock(done); host.toFrame(source); return { source: sourceSlot, range }; } @@ -145,6 +243,10 @@ const LENDING_LIB_FNS: ReadonlySet = new Set([ "num.parseFloat", ]); +/** Searches of string arrays compare the needle with the elements during + * the call. */ +const LENDING_ARRAY_SEARCHES: ReadonlySet = new Set(["includes", "indexOf", "lastIndexOf"]); + /** Typed string-keyed collection operations: reads probe with the key, and * an insertion stores scr_str_scratch_keep's copy (scr_map_append). */ const LENDING_KEY_METHODS: ReadonlySet = new Set(["get", "has", "set", "add", "delete"]); @@ -176,6 +278,12 @@ function lentOperands(value: IrExpr, parameters: LentStringParameters): readonly return LENDING_KEY_METHODS.has(value.method) && stringKeyed(value) ? value.args.slice(0, 1) : []; + case "arrIntrinsic": + return LENDING_ARRAY_SEARCHES.has(value.method) && + value.receiver.type.kind === "array" && + value.receiver.type.elem.kind === "string" + ? value.args.slice(0, 1) + : []; case "call": { const lent = parameters.get(value.callee); return lent ? value.args.filter((_, index) => lent.has(index)) : []; @@ -185,41 +293,123 @@ function lentOperands(value: IrExpr, parameters: LentStringParameters): readonly } } +/** Record parameters, by function and index, that the body only projects: + * it reads their fields and passes them only to such parameters. The set + * lists the string fields some read keeps (reads outside a lending + * position); every other string field is lent (findLentRecordFields). */ +export type LentRecordFields = ReadonlyMap>>; + +/** Functions whose body only returns a record literal that stores some + * parameters, each read once, directly as fields: by function, parameter + * index to field (findRecordFieldProducers). A call stores those arguments + * in the fields of its result and keeps them nowhere else. */ +export type RecordFieldProducers = ReadonlyMap>; + +/** What lends a string: lent string parameters, lent record fields and + * record field producers. */ +export interface LendingFacts { + strings: LentStringParameters; + records: LentRecordFields; + producers: RecordFieldProducers; +} + +const NO_LENDING_FACTS: LendingFacts = { + strings: new Map(), + records: new Map(), + producers: new Map(), +}; + interface LendingScan { /** Slice operands of lending consumers, excluding any node also reached * in an owning position. */ scratch: Set; /** Candidates read anywhere other than a lending position. */ invalid: Set; - /** Direct callees, for the parameter fixpoint. */ + /** Record candidates read other than by a field read or a lent record + * argument. */ + invalidRecords: Set; + /** String fields of record candidates read outside a lending position. */ + keptFields: Map>; + /** Direct callees, for the parameter fixpoints. */ callees: Set; } /** One pass over `fn`: which candidate bindings only lending consumers read * (a slice initializer of a candidate local is not itself a read), and * which slice operands those consumers lend. A switch discriminant is only - * compared with the case tests. */ + * compared with the case tests. Record candidates (`records`) may only be + * projected: their field reads are checked as reads of the field, and they + * may be passed to lent record parameters. A record `container` + * declaration stores candidates in fields of a record candidate; those + * stores are not reads. */ function scanLending( fn: IrFunction, candidates: ReadonlySet, initializers: ReadonlyMap, - parameters: LentStringParameters, + facts: LendingFacts, + records: ReadonlySet = new Set(), + containers: ReadonlyMap = new Map(), ): LendingScan { - const scan: LendingScan = { scratch: new Set(), invalid: new Set(), callees: new Set() }; + const scan: LendingScan = { + scratch: new Set(), + invalid: new Set(), + invalidRecords: new Set(), + keptFields: new Map(), + callees: new Set(), + }; const owned = new Set(); + const keep = (record: string, field: string): void => { + let fields = scan.keptFields.get(record); + if (!fields) scan.keptFields.set(record, (fields = new Set())); + fields.add(field); + }; + const projected = (value: IrExpr): value is IrExpr & { kind: "recordGet" } => + value.kind === "recordGet" && value.obj.kind === "varRef" && records.has(value.obj.localId); function lent(value: IrExpr): boolean { if (value.kind === "varRef" && candidates.has(value.localId)) return true; + if (projected(value)) return true; if (!isSlice(value)) return expr(value); scan.scratch.add(value); return everyExprChild(value, expr, stmt); } + /** A call's arguments: lent string parameters lend, lent record + * parameters project their record, and `stored` arguments (a producer's + * fields in a container declaration) are not reads. */ + function callArgs(value: IrExpr & { kind: "call" }, stored: ReadonlySet | null): void { + scan.callees.add(value.callee); + const strings = facts.strings.get(value.callee); + const recordParams = facts.records.get(value.callee); + value.args.forEach((arg, index) => { + if (stored !== null && stored.has(index)) return; + const kept = recordParams?.get(index); + if (kept && arg.kind === "varRef" && records.has(arg.localId)) { + for (const field of kept) keep(arg.localId, field); + return; + } + if (strings?.has(index)) lent(arg); + else expr(arg); + }); + } function expr(value: IrExpr): boolean { if (isSlice(value)) owned.add(value); + if (projected(value)) { + if (value.type.kind === "string") + keep((value.obj as { localId: string }).localId, value.field); + return true; + } if (value.kind === "varRef" && candidates.has(value.localId)) scan.invalid.add(value.localId); + if (value.kind === "varRef" && records.has(value.localId)) + scan.invalidRecords.add(value.localId); if (value.kind === "closure" || value.kind === "classRef") - for (const id of value.captures ?? []) scan.invalid.add(id); - if (value.kind === "call") scan.callees.add(value.callee); - const operands = lentOperands(value, parameters); + for (const id of value.captures ?? []) { + scan.invalid.add(id); + scan.invalidRecords.add(id); + } + if (value.kind === "call") { + callArgs(value, null); + return true; + } + const operands = lentOperands(value, facts.strings); if (operands.length === 0) return everyExprChild(value, expr, stmt); return everyExprChild( value, @@ -231,6 +421,20 @@ function scanLending( // A candidate's single declaration initializes it with its slice. if (value.kind === "varDecl" && value.init !== null && initializers.has(value.localId)) return everyExprChild(value.init, expr, stmt); + const container = value.kind === "varDecl" ? containers.get(value.localId) : undefined; + if (container && value.kind === "varDecl" && value.init !== null) { + const init = value.init; + if (init.kind === "recordLit") + for (const field of init.fields) { + if (!container.stored.has(field.value)) expr(field.value); + } + else if (init.kind === "call") + callArgs( + init, + new Set(init.args.flatMap((arg, i) => (container.stored.has(arg) ? [i] : []))), + ); + return true; + } if (value.kind === "switch" && value.disc.type.kind === "string") { lent(value.disc); for (const entry of value.cases) { @@ -276,7 +480,7 @@ export function findLentStringParameters( const fn = functions.get(name); if (!indexes || !fn) continue; const ids = new Set([...indexes].map((index) => fn.params[index]!.localId)); - const scan = scanLending(fn, ids, new Map(), lent); + const scan = scanLending(fn, ids, new Map(), { ...NO_LENDING_FACTS, strings: lent }); for (const callee of scan.callees) { let set = callers.get(callee); if (!set) callers.set(callee, (set = new Set())); @@ -296,6 +500,151 @@ export function findLentStringParameters( return lent; } +/** Functions whose whole body is `return { ... }`, a record literal storing + * some parameters, each read nowhere else, directly as fields (a factory + * such as `token(kind, text, pos)`). A synchronous body only: the result + * then holds those arguments, and nothing else does. */ +export function findRecordFieldProducers( + functions: ReadonlyMap, +): Map> { + const producers = new Map>(); + for (const fn of functions.values()) { + if (fn.async || fn.generator || fn.body.length !== 1) continue; + const only = fn.body[0]!; + if (only.kind !== "return" || only.value?.kind !== "recordLit") continue; + const literal = only.value; + const reads = new Map(); + everyStmtList(fn.body, { + expr: (value) => { + if (value.kind === "varRef") reads.set(value.localId, (reads.get(value.localId) ?? 0) + 1); + // A closure or class that captures a parameter keeps it too. + if (value.kind === "closure" || value.kind === "classRef") + for (const id of value.captures ?? []) reads.set(id, (reads.get(id) ?? 0) + 1); + return true; + }, + stmt: () => true, + }); + const fields = new Map(); + fn.params.forEach((param, index) => { + if (param.type.kind !== "string" || reads.get(param.localId) !== 1) return; + const field = literal.fields.find( + (entry) => + !entry.overflow && entry.value.kind === "varRef" && entry.value.localId === param.localId, + ); + if (field) fields.set(index, field.name); + }); + if (fields.size > 0) producers.set(fn.name, fields); + } + return producers; +} + +/** Record parameters that every body only projects (LentRecordFields), with + * the string fields some read keeps. Start from every borrowed record + * parameter of a synchronous function, keeping no field, and grow the kept + * fields and drop escaping parameters until nothing changes; a record passed + * to another lent record parameter keeps what that parameter keeps. */ +export function findLentRecordFields( + functions: ReadonlyMap, + lifetimes: CallLifetimes, + strings: LentStringParameters, +): Map>> { + const lent = new Map>>(); + for (const fn of functions.values()) { + if (fn.async || fn.generator) continue; + const stable = lifetimes.borrowed.get(fn.name); + const indexes = new Map>(); + fn.params.forEach((param, index) => { + if (param.type.kind === "record" && stable?.has(index)) indexes.set(index, new Set()); + }); + if (indexes.size > 0) lent.set(fn.name, indexes); + } + const callers = new Map>(); + const pending = new Set(lent.keys()); + while (pending.size > 0) { + const name = pending.values().next().value as string; + pending.delete(name); + const indexes = lent.get(name); + const fn = functions.get(name); + if (!indexes || !fn) continue; + const ids = new Set([...indexes.keys()].map((index) => fn.params[index]!.localId)); + const scan = scanLending( + fn, + new Set(), + new Map(), + { strings, records: lent, producers: new Map() }, + ids, + ); + for (const callee of scan.callees) { + let set = callers.get(callee); + if (!set) callers.set(callee, (set = new Set())); + set.add(name); + } + let changed = false; + for (const [index, kept] of [...indexes]) { + const id = fn.params[index]!.localId; + if (scan.invalidRecords.has(id)) { + indexes.delete(index); + changed = true; + continue; + } + const fields = scan.keptFields.get(id); + if (fields) + for (const field of fields) { + if (kept.has(field)) continue; + kept.add(field); + changed = true; + } + } + if (!changed) continue; + if (indexes.size === 0) lent.delete(name); + const affected = callers.get(name); + if (affected) for (const caller of affected) if (lent.has(caller)) pending.add(caller); + } + return lent; +} + +/** A record local whose declaration stores slice locals directly in some of + * its fields: a record literal, or a call to a record field producer. */ +interface RecordContainer { + /** The stored candidate reads of the declaration, by field. */ + stored: Map; +} + +/** Record locals declared with slice locals in their fields. */ +function findRecordContainers( + fn: IrFunction, + bindings: ReadonlySet | undefined, + slices: ReadonlySet, + producers: RecordFieldProducers, +): Map { + const containers = new Map(); + everyStmtList(fn.body, { + expr: () => true, + stmt: (stmt) => { + if (stmt.kind !== "varDecl" || stmt.init === null || !bindings?.has(stmt.localId)) + return true; + if (stmt.init.type.kind !== "record") return true; + const stored = new Map(); + const init = stmt.init; + if (init.kind === "recordLit") { + for (const field of init.fields) + if (!field.overflow && field.value.kind === "varRef" && slices.has(field.value.localId)) + stored.set(field.value, { field: field.name, slice: field.value.localId }); + } else if (init.kind === "call") { + const fields = producers.get(init.callee); + init.args.forEach((arg, index) => { + const field = fields?.get(index); + if (field !== undefined && arg.kind === "varRef" && slices.has(arg.localId)) + stored.set(arg, { field, slice: arg.localId }); + }); + } + if (stored.size > 0) containers.set(stmt.localId, { stored }); + return true; + }, + }); + return containers; +} + /** Slices whose every reader borrows them are written into a stack buffer * instead of a heap allocation (scr_str_slice_scratch): a slice operand of * a lending consumer, and the initializer of a stable local read only by @@ -304,12 +653,18 @@ export function findLentStringParameters( * non-ASCII results keep the ordinary owned string. Each site owns its * buffer: a site runs again only after its previous value is dead, since * nothing can keep it. Coroutine frames do not keep stack buffers across a - * suspension, so they are left alone. */ + * suspension, so they are left alone. + * + * A slice local may also be stored in a field of a stable record local + * declared with it (a record literal, or a record field producer's result): + * the record is then only projected, and every read of that field lends. + * The record is declared after the slice in its scope and is never kept, so + * it is dead before the slice's site runs again. */ export function findScratchStringSlices( fn: IrFunction, lifetimes: CallLifetimes, scalar: ReadonlyMap, - parameters: LentStringParameters = new Map(), + facts: LendingFacts = NO_LENDING_FACTS, ): Set { if (fn.async || fn.generator) return new Set(); const bindings = lifetimes.bindings.get(fn.name); @@ -328,7 +683,19 @@ export function findScratchStringSlices( return true; }, }); - const scan = scanLending(fn, new Set(locals.keys()), locals, parameters); + const containers = findRecordContainers(fn, bindings, new Set(locals.keys()), facts.producers); + const scan = scanLending( + fn, + new Set(locals.keys()), + locals, + facts, + new Set(containers.keys()), + containers, + ); + for (const [record, container] of containers) + for (const { field, slice } of container.stored.values()) + if (scan.invalidRecords.has(record) || scan.keptFields.get(record)?.has(field)) + scan.invalid.add(slice); for (const [id, init] of locals) if (!scan.invalid.has(id)) scan.scratch.add(init); return scan.scratch; } diff --git a/packages/compiler/src/backend/llvm/string-unit-versions.test.ts b/packages/compiler/src/backend/llvm/string-unit-versions.test.ts new file mode 100644 index 000000000..e21803948 --- /dev/null +++ b/packages/compiler/src/backend/llvm/string-unit-versions.test.ts @@ -0,0 +1,126 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile, deserializeModule } from "../../index.js"; +import type { IrModule, IrStmt } from "../../ir/ir.js"; +import { unitSafeFunctions, unitSafeLoop } from "./string-unit-versions.js"; + +async function lower(source: string): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-unit-versions-")); + try { + const entry = join(dir, "main.ts"); + const outPath = join(dir, "main.ir.json"); + await writeFile(entry, source); + const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" }); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + return deserializeModule(await readFile(outPath, "utf8")); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +const loopOf = (mod: IrModule, name: string): IrStmt & { kind: "for" } => + mod.functions.find((f) => f.name === name)!.body.find((s) => s.kind === "for") as IrStmt & { + kind: "for"; + }; + +test("loops over a string's units call only functions that read no string", async () => { + const mod = await lower(` +function lower(c: number): boolean { + return c >= 97 && c <= 122; +} +function word(c: number): boolean { + return lower(c) || (c >= 48 && c <= 57) || Math.abs(c - 200) < 50; +} +function codeOf(t: string, i: number): number { + return t.charCodeAt(i); +} +function viaCodeOf(t: string): number { + return codeOf(t, 0); +} +function words(s: string): number { + let n = 0; + const starts = [0]; + for (let i = 0; i < s.length; i++) { + if (word(s.charCodeAt(i))) n++; + else starts.push(i); + } + return n + starts.length; +} +function pairs(s: string, t: string): number { + let n = 0; + for (let i = 0; i < s.length; i++) n += s.charCodeAt(i) + codeOf(t, i); + return n; +} +function both(s: string, t: string): number { + let n = 0; + for (let i = 0; i < s.length; i++) n += s.charCodeAt(i) + t.charCodeAt(i); + return n; +} +function concatenates(s: string): string { + let out = ""; + for (let i = 0; i < s.length; i++) if (s.charCodeAt(i) === 32) out += "-"; + return out; +} +console.log(words("ab c"), pairs("ab", "cd"), both("ab", "cd"), concatenates("a b"), viaCodeOf("x")); +`); + const safe = unitSafeFunctions(mod); + expect(safe.has("lower")).toBe(true); + expect(safe.has("word")).toBe(true); + // A string read, directly or through a call, makes a function unsafe. + expect(safe.has("codeOf")).toBe(false); + expect(safe.has("viaCodeOf")).toBe(false); + + const param = (fn: string, index = 0): string => + mod.functions.find((f) => f.name === fn)!.params[index]!.localId; + // A unit-safe loop answers its receiver's length reads: here the + // condition's `s.length`. + const lengths = unitSafeLoop(loopOf(mod, "words"), param("words"), safe); + expect(lengths?.size).toBe(1); + expect([...lengths!].every((e) => e.kind === "strIntrinsic" && e.method === "length")).toBe(true); + expect(unitSafeLoop(loopOf(mod, "pairs"), param("pairs"), safe)).toBeNull(); + expect(unitSafeLoop(loopOf(mod, "both"), param("both"), safe)).toBeNull(); + expect(unitSafeLoop(loopOf(mod, "concatenates"), param("concatenates"), safe)).toBeNull(); +}); + +test("loops that sort with a comparator or copy an untyped source get no units copy", async () => { + // Either can run program code that indexes other strings, which evicts + // the receiver's units while the loop reads them. + const mod = await lower(` +function sorts(s: string, values: Float64Array, compare: (a: number, b: number) => number): number { + let n = 0; + for (let i = 0; i < s.length; i++) { + n += s.charCodeAt(i); + if (i === 3) values.sort(compare); + } + return n; +} +function copies(s: string, values: Float64Array, source: any): number { + let n = 0; + for (let i = 0; i < s.length; i++) { + n += s.charCodeAt(i); + if (i === 3) values.set(source); + } + return n; +} +function sortsNumbers(s: string, values: Float64Array): number { + let n = 0; + for (let i = 0; i < s.length; i++) { + n += s.charCodeAt(i); + if (i === 3) values.sort(); + } + return n; +} +const values = new Float64Array([2, 1]); +console.log(sorts("abcdef", values, (a, b) => a - b), copies("abcdef", values, [4, 3]), sortsNumbers("abcdef", values)); +`); + const safe = unitSafeFunctions(mod); + const param = (fn: string): string => + mod.functions.find((f) => f.name === fn)!.params[0]!.localId; + expect(unitSafeLoop(loopOf(mod, "sorts"), param("sorts"), safe)).toBeNull(); + expect(unitSafeLoop(loopOf(mod, "copies"), param("copies"), safe)).toBeNull(); + // A numeric sort calls no program code. + expect(unitSafeLoop(loopOf(mod, "sortsNumbers"), param("sortsNumbers"), safe)).not.toBeNull(); +}); diff --git a/packages/compiler/src/backend/llvm/string-unit-versions.ts b/packages/compiler/src/backend/llvm/string-unit-versions.ts new file mode 100644 index 000000000..368b8eae8 --- /dev/null +++ b/packages/compiler/src/backend/llvm/string-unit-versions.ts @@ -0,0 +1,208 @@ +import type { IrBytesIntrinsicMethod, IrExpr, IrModule, IrStmt } from "../../ir/ir.js"; +import { everyExprChild, everyStmtChild } from "../../ir/traverse.js"; + +/* Loop versions over the UTF-16 units of a string that is not ASCII. + * + * The runtime publishes the receiver of an indexed scan with its UTF-16 + * units (ScrStrScan, string-reads.ts), and drops them when the string is + * released or appended to in place, or when its index entry is evicted: + * the runtime indexes another non-ASCII string. A loop version's string + * receiver is a binding the loop does not write, which keeps it alive and + * unappended for the whole loop. Its fast copy over units therefore only + * needs a body that indexes no other string: the operations below, and + * calls of functions made of the same operations without string reads + * (`unitSafeFunctions`). */ + +const ARRAY_METHODS = new Set(["push", "pop", "length"]); + +/* The typed-array and Buffer methods a units copy may run: those that + * call no program code and read no checked-dynamic value or string. A + * comparator sort (sortCompare) calls back into the program, and a copy + * from a checked-dynamic source (setFromDyn) reads its elements through + * getters; either can index another string, which evicts the receiver's + * units while the loop reads them. */ +const UNIT_SAFE_BYTES_METHODS: ReadonlySet = new Set([ + "buffer", + "length", + "byteLength", + "byteOffset", + "get", + "slice", + "subarray", + "copyWithin", + "sortNumeric", + "toReversed", + "with", + "toArray", + "setFrom", + "readNum", + "writeNum", + "readNumVar", + "writeNumVar", + "equals", + "compareBuf", + "indexOfNum", + "lastIndexOfNum", + "includesNum", + "indexOfElem", + "lastIndexOfElem", + "includesElem", + "fill", + "fillNum", + "fillElem", + "copy", + "swap16", + "swap32", + "swap64", + "dataViewNew", + "dvGetUint8", + "dvGetInt8", + "dvGetUint16", + "dvGetInt16", + "dvGetUint32", + "dvGetInt32", + "dvGetFloat16", + "dvGetFloat32", + "dvGetFloat64", + "dvGetBigUint64Number", + "dvGetBigInt64Number", + "dvSetUint8", + "dvSetInt8", + "dvSetUint16", + "dvSetInt16", + "dvSetUint32", + "dvSetInt32", + "dvSetFloat16", + "dvSetFloat32", + "dvSetFloat64", +]); + +/** Whether `e` (or statement `s`) uses only operations that index no + * string: numbers, booleans, locals, records and class fields, typed + * arrays, number arrays and Math. `reads` admits the string reads a + * version covers; `calls` the callees. */ +function unitSafe( + node: { e: IrExpr } | { s: IrStmt }, + reads: (e: IrExpr & { kind: "strIntrinsic" }) => boolean, + calls: (callee: string) => boolean, +): boolean { + const expr = (e: IrExpr): boolean => { + switch (e.kind) { + case "varRef": + case "numLit": + case "boolLit": + case "bin": + case "unary": + case "logical": + case "ternary": + case "toBool": + case "seqExpr": + case "assignExpr": + case "incDec": + case "recordGet": + case "fieldGet": + break; + case "bytesIntrinsic": + if (!UNIT_SAFE_BYTES_METHODS.has(e.method)) return false; + break; + case "arrayGet": + if (e.type.kind !== "f64" && e.type.kind !== "bool") return false; + break; + case "arrIntrinsic": { + // Growing or shrinking an array of numbers allocates; it reads no + // string. + const array = e.receiver.type; + if ( + !ARRAY_METHODS.has(e.method) || + array.kind !== "array" || + (array.elem.kind !== "f64" && array.elem.kind !== "bool") + ) + return false; + break; + } + case "libCall": + if (!e.fn.startsWith("math.")) return false; + break; + case "call": + if (!calls(e.callee)) return false; + break; + case "strIntrinsic": + if (!reads(e)) return false; + break; + default: + return false; + } + return everyExprChild(e, expr, stmt); + }; + const stmt = (s: IrStmt): boolean => { + switch (s.kind) { + case "varDecl": + case "assign": + case "exprStmt": + case "if": + case "block": + case "while": + case "doWhile": + case "for": + case "break": + case "continue": + case "return": + case "recordSet": + case "fieldSet": + case "bytesSet": + break; + default: + return false; + } + return everyStmtChild(s, expr, stmt); + }; + return "e" in node ? expr(node.e) : stmt(node.s); +} + +/** The module's functions a units version may call: synchronous + * functions without captures whose bodies use only unit-safe operations, + * read no string, and call only such functions. */ +export function unitSafeFunctions(mod: IrModule): ReadonlySet { + const candidates = new Map( + mod.functions + .filter((fn) => !fn.async && !fn.generator && !(fn.captures?.length ?? 0)) + .map((fn) => [fn.name, fn]), + ); + const safe = new Set(candidates.keys()); + for (let settled = false; !settled;) { + settled = true; + for (const name of safe) { + const fn = candidates.get(name)!; + const ok = fn.body.every((s) => + unitSafe( + { s }, + () => false, + (callee) => safe.has(callee), + ), + ); + if (!ok) { + safe.delete(name); + settled = false; + } + } + } + return safe; +} + +/** The `length` reads of the binding `receiver` in a loop whose every + * part is unit-safe, reading only charCodeAt and length of `receiver` and + * calling only `safe` functions; null for any other loop. */ +export function unitSafeLoop( + loop: IrStmt & { kind: "for" | "while" }, + receiver: string, + safe: ReadonlySet, +): ReadonlySet | null { + const lengths = new Set(); + const reads = (e: IrExpr & { kind: "strIntrinsic" }): boolean => { + if (e.receiver.kind !== "varRef" || e.receiver.localId !== receiver) return false; + if (e.method === "length") lengths.add(e); + return e.method === "charCodeAt" || e.method === "length"; + }; + const calls = (callee: string): boolean => safe.has(callee); + return unitSafe({ s: loop }, reads, calls) ? lengths : null; +} diff --git a/packages/compiler/src/backend/llvm/test-boxed-unions.ts b/packages/compiler/src/backend/llvm/test-boxed-unions.ts index 15cf5aee9..8847d53a3 100644 --- a/packages/compiler/src/backend/llvm/test-boxed-unions.ts +++ b/packages/compiler/src/backend/llvm/test-boxed-unions.ts @@ -1,14 +1,29 @@ -import type { IrModule, IrType } from "../../ir/ir.js"; +import { F64, type IrModule, type IrType, type IrUnionDef } from "../../ir/ir.js"; /** Test helper: a Set of each union keeps it a tagged `%ScrUnion` box (Set * elements are runtime-inspected boxes), so stack-box tests exercise the - * paths that nullable-pointer unions (nullable-unions.ts) bypass. */ + * paths that nullable-pointer unions (nullable-unions.ts) bypass. A union of + * records alone is a Set element by record identity, so for those the Set + * holds a companion union with a number arm, which keeps the records out of + * every shape union. */ export function keepUnionsBoxed(mod: IrModule): IrModule { - const keep = (mod.unions ?? []).map((union) => ({ - id: `%g.keep.${union.id}`, - name: `keep_${union.id}`, - type: { kind: "set", elem: { kind: "union", unionId: union.id } } as IrType, - mutable: false, - })); - return { ...mod, globals: [...(mod.globals ?? []), ...keep] }; + const companions: IrUnionDef[] = []; + const keep = (mod.unions ?? []).map((union) => { + let elem = union.id; + if (union.arms.every((arm) => arm.kind === "record")) { + elem = `%keep.${union.id}`; + companions.push({ id: elem, arms: [...union.arms, F64] }); + } + return { + id: `%g.keep.${union.id}`, + name: `keep_${union.id}`, + type: { kind: "set", elem: { kind: "union", unionId: elem } } as IrType, + mutable: false, + }; + }); + return { + ...mod, + unions: [...(mod.unions ?? []), ...companions], + globals: [...(mod.globals ?? []), ...keep], + }; } diff --git a/packages/compiler/src/backend/llvm/union-repr.ts b/packages/compiler/src/backend/llvm/union-repr.ts index 959f9e51e..8c6fb4c85 100644 --- a/packages/compiler/src/backend/llvm/union-repr.ts +++ b/packages/compiler/src/backend/llvm/union-repr.ts @@ -8,8 +8,14 @@ import type { BlockBuilder } from "./blocks.js"; import { NULLABLE_NULL, shapeTagFn, type PointerUnion } from "./nullable-unions.js"; -/** The i32 tag of a union value. */ -export function emitUnionTag(B: BlockBuilder, nullable: PointerUnion | null, u: string): string { +/** The i32 tag of a union value. `alias` is the tag word's alias + * attachment (", !tbaa !N"), when the caller has one. */ +export function emitUnionTag( + B: BlockBuilder, + nullable: PointerUnion | null, + u: string, + alias = "", +): string { const t = B.tmp(); if (nullable?.repr === "shapes") { B.line(`${t} = call i32 ${shapeTagFn(nullable.unionId)}(ptr ${u})`); @@ -31,7 +37,7 @@ export function emitUnionTag(B: BlockBuilder, nullable: PointerUnion | null, u: } const p = B.tmp(); B.line(`${p} = getelementptr inbounds %ScrUnion, ptr ${u}, i64 0, i32 1`); - B.line(`${t} = load i32, ptr ${p}`); + B.line(`${t} = load i32, ptr ${p}${alias}`); return t; } @@ -81,12 +87,17 @@ export function emitNullableIsTag( } /** The BORROWED payload pointer of a reference arm. */ -export function emitUnionPeek(B: BlockBuilder, nullable: PointerUnion | null, u: string): string { +export function emitUnionPeek( + B: BlockBuilder, + nullable: PointerUnion | null, + u: string, + alias = "", +): string { if (nullable) return u; const p = B.tmp(); const t = B.tmp(); B.line(`${p} = getelementptr inbounds %ScrUnion, ptr ${u}, i64 0, i32 5`); - B.line(`${t} = load ptr, ptr ${p}`); + B.line(`${t} = load ptr, ptr ${p}${alias}`); return t; } diff --git a/packages/compiler/src/backend/llvm/unwind-abi.test.ts b/packages/compiler/src/backend/llvm/unwind-abi.test.ts new file mode 100644 index 000000000..bcef1c379 --- /dev/null +++ b/packages/compiler/src/backend/llvm/unwind-abi.test.ts @@ -0,0 +1,325 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile, deserializeModule } from "../../index.js"; +import { validateModule } from "../../ir/validate.js"; +import { emitLlvmModule } from "./emitter.js"; +import { UnwindAbiViolation, bridgePlainUnwindCalls } from "./unwind-abi.js"; + +async function moduleFor(source: string) { + const dir = await mkdtemp(join(tmpdir(), "scriptc-unwind-abi-")); + try { + const entry = join(dir, "main.ts"), + output = join(dir, "main.ir.json"); + await writeFile(entry, source); + const result = await compile(entry, { outDir: dir, outPath: output, outputKind: "ir" }); + if (!result.ok) + throw new Error(result.diagnostics.map((item) => `${item.code}: ${item.message}`).join("\n")); + const module = deserializeModule(await readFile(output, "utf8")); + expect(validateModule(module)).toEqual([]); + return module; + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +/** The definition of the source function `name` (its i32 or borrowing entry when it has one). */ +function body(ir: string, name: string): string { + return new RegExp( + `^define internal [^\\n]*@sc_b?f_[^\\n(]*${name}(?:\\.i32)?\\([^]*?^}`, + "m", + ).exec(ir)![0]; +} + +const SOURCE = ` +function leaf(n: number): number { + if (n === 3) throw new RangeError("three"); + return n * 2; +} +function first(n: number): number { + return leaf(n) + 1; +} +function second(n: number): number { + return first(n) * 3; +} +function third(n: number): number { + return second(n) - 2; +} +function mid(n: number): number { + const parts: string[] = ["a" + n]; + const r = third(n); + parts.push("b"); + return r + parts.length; +} +class Counter { + total = 0; + add(n: number): void { this.total += mid(n); } +} +function outer(n: number): string { + try { return "ok " + mid(n); } catch (e) { return "caught " + (e as Error).message; } +} +const counter = new Counter(); +for (let i = 0; i < 5; i++) { console.log(outer(i)); try { counter.add(i); } catch { console.log("add", i); } } +const f = (x: number) => mid(x); +console.log(f(1), counter.total); +`; + +const LINUX = { targetTriple: "x86_64-unknown-linux-gnu", unwindAbi: true } as const; + +test("directly called functions raise, and their callers invoke them without a pending check", async () => { + const ir = emitLlvmModule(await moduleFor(SOURCE), LINUX); + // leaf throws: the exception starts on the pending flag, and the three + // calls above it test the flag too; mid raises it on. + for (const name of ["leaf", "first", "second", "third"]) + expect(body(ir, name)).not.toContain("@scr_unwind_raise"); + const mid = body(ir, "mid"); + expect(mid).toMatch(/= call double @sc_b?f_[^(]*third\(/); + expect(mid).toContain("call void @scr_unwind_raise()"); + // outer invokes mid; the landing pad takes the exception to its handler. + const outer = body(ir, "outer"); + expect(outer).toMatch( + /= invoke double @sc_b?f_[^(]*mid[^\n]* to label %inv\.ok\d+ unwind label %inv\.lp\d+/, + ); + expect(outer).toMatch(/inv\.lp\d+:\n\s+%t\d+ = landingpad \{ ptr, i32 \} cleanup/); + expect(outer).toContain("personality ptr @__gcc_personality_v0"); + expect(ir).toContain("declare i32 @__gcc_personality_v0(...)"); + expect(ir).toContain("declare void @scr_unwind_raise() cold"); +}); + +test("a method of a class outside any hierarchy is on the unwinding ABI; closures keep the flag", async () => { + const ir = emitLlvmModule(await moduleFor(SOURCE), LINUX); + // Counter.add invokes mid and resumes the unwind from its landing pad; the + // closure over mid is a pointer target and returns with the flag. + const add = body(ir, "Counter_add"); + expect(add).toMatch(/invoke double @sc_b?f_[^(]*mid/); + expect(add).toMatch(/resume \{ ptr, i32 \} %[\w.]+/); + expect(add).not.toContain("@scr_unwind_raise"); + const closures = ir + .split(/\n(?=define )/) + .filter((def) => /^define internal [^\n]*@sc_b?f_[^\n(]*fn_/.test(def)); + expect(closures.length).toBeGreaterThan(0); + for (const def of closures) expect(def).not.toContain("call void @scr_unwind_raise()"); +}); + +test("recursive functions keep the pending flag, also when they recurse through a callback", async () => { + const source = ` +function leaf(n: number): number { + if (n < 0) throw new RangeError("negative"); + return n; +} +function one(n: number): number { return leaf(n) + 1; } +function two(n: number): number { return one(n) + 1; } +function three(n: number): number { return two(n) + 1; } +function four(n: number): number { + const parts: string[] = ["a" + n]; + const r = three(n); + return r + parts.length; +} +function walk(n: number, depth: number): number { + const parts: string[] = ["w" + n]; + const r = depth === 0 ? four(n) : walk(n, depth - 1); + return r + parts.length; +} +function visit(n: number, depth: number, each: (n: number, depth: number) => number): number { + const parts: string[] = ["v" + n]; + const r = depth === 0 ? four(n) : each(n, depth - 1); + return r + parts.length; +} +const each = (n: number, depth: number): number => visit(n, depth, each); +for (let i = -1; i < 2; i++) { + try { console.log(walk(i, 3), visit(i, 3, each)); } catch (e) { console.log((e as Error).message); } +} +`; + const ir = emitLlvmModule(await moduleFor(source), LINUX); + expect(body(ir, "four")).toContain("call void @scr_unwind_raise()"); + for (const name of ["walk", "visit"]) { + expect(body(ir, name)).not.toContain("@scr_unwind_raise"); + expect(body(ir, name)).toMatch(/invoke double @sc_b?f_[^(]*four/); + } +}); + +test("without the option, and off Linux, emission keeps the pending-flag ABI", async () => { + const mod = await moduleFor(SOURCE); + for (const options of [{}, { targetTriple: "arm64-apple-macosx14.0.0", unwindAbi: true }]) { + const ir = emitLlvmModule(mod, options); + expect(ir).not.toContain("invoke "); + expect(ir).not.toContain("@scr_unwind_raise"); + expect(ir).not.toContain("personality"); + } +}); + +test("bridgePlainUnwindCalls keeps the pending-flag contract at plain calls of raising functions", () => { + const raising = [ + "define internal ptr @sc_bf_raises(ptr %p) #0 { ; raises", + "entry:", + " call void @scr_unwind_raise()", + " unreachable", + "}", + ].join("\n"); + const caller = [ + "define internal ptr @sc_f_adapter(ptr %p0) #0 {", + "entry:", + " %result = call ptr @sc_bf_raises(ptr %p0) ; borrowed result", + " br label %next", + "next:", + " %v = phi ptr [ %result, %entry ]", + " ret ptr %v", + "}", + ].join("\n"); + const [out] = bridgePlainUnwindCalls([`${raising}\n${caller}`]); + expect(out).toContain( + "%result.ub = invoke ptr @sc_bf_raises(ptr %p0) to label %ub.n.0 unwind label %ub.lp.0", + ); + expect(out).toContain("%result = phi ptr [ %result.ub, %ub.n.0 ], [ null, %ub.lp.0 ]"); + // The split block's successor now comes from its last part. + expect(out).toContain("%v = phi ptr [ %result, %ub.ok.0 ]"); + expect(out).toMatch( + /define internal ptr @sc_f_adapter\(ptr %p0\) #0 personality ptr @__gcc_personality_v0 \{/, + ); + expect(out).toContain("declare i32 @__gcc_personality_v0(...)"); +}); + +test("an invoked wrapper of a raising function resumes the unwind; its address must not escape", () => { + const raising = [ + "define internal double @sc_bf_body.i32(i32 %a) #0 {", + "entry:", + " call void @scr_unwind_raise()", + " unreachable", + "}", + ].join("\n"); + const wrapper = [ + "define internal double @sc_bf_body(double %a) #0 {", + "entry:", + " %i = fptosi double %a to i32", + " %r = call double @sc_bf_body.i32(i32 %i)", + " ret double %r", + "}", + ].join("\n"); + const caller = [ + "define internal double @sc_bf_caller(double %a) #0 personality ptr @__gcc_personality_v0 {", + "entry:", + " %r = invoke double @sc_bf_body(double %a) to label %ok unwind label %lp", + "lp:", + " %x = landingpad { ptr, i32 } cleanup", + " call void @scr_unwind_raise()", + " unreachable", + "ok:", + " ret double %r", + "}", + ].join("\n"); + const [out] = bridgePlainUnwindCalls([[raising, wrapper, caller].join("\n")]); + expect(out).toContain( + "%r = invoke double @sc_bf_body.i32(i32 %i) to label %ub.ok.0 unwind label %ub.lp.0", + ); + expect(out).toContain("resume { ptr, i32 } %ub.v.0"); + expect(() => + bridgePlainUnwindCalls([ + `${raising}\n@table = internal constant [1 x ptr] [ptr @sc_bf_body.i32]`, + ]), + ).toThrow(UnwindAbiViolation); + try { + bridgePlainUnwindCalls([ + `${raising}\n@table = internal constant [1 x ptr] [ptr @sc_bf_body.i32]`, + ]); + } catch (error) { + expect([...(error as UnwindAbiViolation).symbols]).toEqual(["sc_bf_body.i32"]); + } +}); + +test("an invoked function that never tests the pending flag needs no raise", () => { + const raising = [ + "define internal void @sc_bf_raises() #0 {", + "entry:", + " call void @scr_unwind_raise()", + " unreachable", + "}", + ].join("\n"); + const leaf = [ + "define internal double @sc_bf_leaf() #0 {", + "entry:", + " ret double 1.0", + "}", + ].join("\n"); + const caller = (callee: string) => + [ + "define internal double @sc_bf_caller() #0 personality ptr @__gcc_personality_v0 {", + "entry:", + ` %r = invoke double @${callee}() to label %ok unwind label %lp`, + "lp:", + " %x = landingpad { ptr, i32 } cleanup", + " call void @scr_unwind_raise()", + " unreachable", + "ok:", + " ret double %r", + "}", + ].join("\n"); + expect(() => + bridgePlainUnwindCalls([[raising, leaf, caller("sc_bf_leaf")].join("\n")]), + ).not.toThrow(); + const testing = leaf.replace( + " ret double 1.0", + " %f = load i32, ptr @scr_exc_flag\n ret double 1.0", + ); + expect(() => + bridgePlainUnwindCalls([[raising, testing, caller("sc_bf_leaf")].join("\n")]), + ).toThrow(UnwindAbiViolation); +}); + +test("calls that cannot unwind are marked nounwind", () => { + const raising = [ + "define internal void @sc_bf_raises(ptr %b) #0 {", + "entry:", + " %n = call i64 @scr_bytes_length(ptr %b) ; length", + " call void @scr_bytes_release(ptr %b), !dbg !4", + " %r = call ptr %b(ptr %b)", + " call void @scr_unwind_raise()", + " unreachable", + "}", + ].join("\n"); + const [out] = bridgePlainUnwindCalls([raising]); + expect(out).toContain("%n = call i64 @scr_bytes_length(ptr %b) nounwind ; length"); + expect(out).toContain("call void @scr_bytes_release(ptr %b) nounwind, !dbg !4"); + expect(out).toContain("%r = call ptr %b(ptr %b) nounwind"); + expect(out).toContain(" call void @scr_unwind_raise()\n"); +}); + +test("functions without a plain call of a raising function change only their call lines", () => { + const raising = [ + "define internal void @sc_bf_raises() #0 {", + "entry:", + " call void @scr_unwind_raise()", + " unreachable", + "}", + ].join("\n"); + const other = [ + "define internal void @sc_bf_raises.copy() #0 {", + "entry:", + " ret void", + "}", + ].join("\n"); + const plain = [ + "define internal double @sc_f_plain(ptr %p) #0 {", + "entry: ; call sites below", + " %a = call double @sc_f_other(ptr %p)", + " %b = tail call double @sc_f_other(ptr %p) ; mentions @ and @sc_bf_raises.copy", + " store ptr @sc_bf_raises.copy, ptr %p", + " ret double %a", + "}", + ].join("\n"); + const parts = [`${raising}\n${other}`, plain, "@table = internal constant ptr @sc_f_plain"]; + const out = bridgePlainUnwindCalls(parts); + expect(out[0]).toBe(parts[0]); + expect(out[1]).toBe( + plain + .replace("@sc_f_other(ptr %p)\n", "@sc_f_other(ptr %p) nounwind\n") + .replace("@sc_f_other(ptr %p) ;", "@sc_f_other(ptr %p) nounwind ;"), + ); + expect(out[2]).toBe(parts[2]); + expect(() => + bridgePlainUnwindCalls([ + raising, + plain.replace("store ptr @sc_bf_raises.copy", "store ptr @sc_bf_raises"), + ]), + ).toThrow(UnwindAbiViolation); +}); diff --git a/packages/compiler/src/backend/llvm/unwind-abi.ts b/packages/compiler/src/backend/llvm/unwind-abi.ts new file mode 100644 index 000000000..821c5351b --- /dev/null +++ b/packages/compiler/src/backend/llvm/unwind-abi.ts @@ -0,0 +1,570 @@ +/* The functions that propagate exceptions by unwinding instead of the + * pending flag (see unwindAbiFunctions). */ +import type { IrExpr, IrModule } from "../../ir/ir.js"; +import { everyStmtList } from "../../ir/traverse.js"; +import { eachStringField } from "../../ir/fields.generated.js"; + +/** How many calls above a `throw` functions keep the pending flag. */ +const THROW_LEVELS = 3; +/** The call graph's node for calls through values. */ +const ANY_FUNCTION = " any function"; + +/** The nodes of `edges` on a cycle (a recursion), by Tarjan's algorithm + * without recursion of its own: a large module's call chains are deeper + * than the host stack. */ +function recursiveFunctions(edges: ReadonlyMap): Set { + const index = new Map(); + const low = new Map(); + const stack: string[] = []; + const onStack = new Set(); + const result = new Set(); + let counter = 0; + const enter = (name: string): { name: string; next: number } => { + index.set(name, counter); + low.set(name, counter++); + stack.push(name); + onStack.add(name); + return { name, next: 0 }; + }; + for (const root of edges.keys()) { + if (index.has(root)) continue; + const work = [enter(root)]; + while (work.length > 0) { + const top = work[work.length - 1]!; + const out = edges.get(top.name) ?? []; + if (top.next < out.length) { + const callee = out[top.next++]!; + if (!index.has(callee)) work.push(enter(callee)); + else if (onStack.has(callee)) + low.set(top.name, Math.min(low.get(top.name)!, index.get(callee)!)); + continue; + } + work.pop(); + const parent = work[work.length - 1]; + if (parent !== undefined) + low.set(parent.name, Math.min(low.get(parent.name)!, low.get(top.name)!)); + if (low.get(top.name) !== index.get(top.name)) continue; + const members: string[] = []; + let member: string; + do { + member = stack.pop()!; + onStack.delete(member); + members.push(member); + } while (member !== top.name); + if (members.length > 1 || out.includes(top.name)) for (const m of members) result.add(m); + } + } + return result; +} + +/** The may-throw functions whose exceptions reach their callers by + * unwinding the native stack (scr_unwind_raise) instead of returning with + * the pending flag set. Their callers call them with `invoke`, so the call + * needs no flag test; the landing pad runs the frame's unwind. + * + * A function qualifies only when every way it can be reached is a direct + * `call` from emitted code that can lower it as an `invoke`: it is not + * async or a generator, not a constructor, not a method a virtual call can + * reach or one a vtable holds (an overridden method), not a closure + * target, not the entry or a library export, not named by any node other + * than a `call`'s callee, and not called from an async or generator body + * (coroutine bodies keep plain calls). + * + * A native unwind costs about 2 µs a raise and 0.1 µs a frame, against a + * few nanoseconds for a flag test, so the functions an exception is likely to + * cross many of keep the flag: those with a `throw` or rethrow of their own + * and those up to THROW_LEVELS calls above one (a handler near the throw + * takes it without a native unwind), and the recursive ones (deep stacks + * are recursions). */ +export function unwindAbiFunctions( + mod: IrModule, + mayThrow: ReadonlySet, + excluded: ReadonlySet = new Set(), +): Set { + const names = new Set(mod.functions.map((fn) => fn.name)); + const blocked = new Set(excluded); + blocked.add(mod.entry); + // The runtime calls the error property readers through the pointers the + // module installs (emit-module.ts). + blocked.add("%error.name.read"); + blocked.add("%error.message.read"); + for (const entry of mod.lib?.exports ?? []) blocked.add(entry.fnName); + // Methods a virtual call can reach, and the overridden methods of class + // hierarchies (whose vtables list them), are called through pointers; the + // others are called directly, like functions. A virtual call reaches its listed + // targets, or else the method in its receiver class's hierarchy; a + // receiver that is no class of the module (an interface) may reach any + // class's method of that name. + const classes = new Map((mod.classes ?? []).map((cls) => [cls.name, cls])); + const anyClass = new Set(); + for (const fn of mod.functions) + everyStmtList(fn.body, { + expr: (e: IrExpr) => { + if (e.kind !== "virtualCall") return true; + if (e.targets !== undefined) for (const target of e.targets) blocked.add(target); + else if (classes.has(e.className)) blocked.add(`%${e.className}.${e.method}`); + else anyClass.add(e.method); + return true; + }, + stmt: () => true, + }); + // A hierarchy's vtables hold the methods declared by more than one of its + // classes (an override and what it overrides). + const rootOf = (name: string): string => { + let cls = classes.get(name); + while (cls?.base !== undefined && classes.has(cls.base)) cls = classes.get(cls.base); + return cls?.name ?? name; + }; + const declarers = new Map(); + for (const cls of mod.classes ?? []) + for (const m of cls.methods ?? []) { + const key = `${rootOf(cls.name)}\0${m}`; + declarers.set(key, (declarers.get(key) ?? 0) + 1); + } + for (const cls of mod.classes ?? []) { + blocked.add(`%${cls.name}.constructor`); + blocked.add(`%${cls.name}.construct`); + const root = rootOf(cls.name); + for (const m of cls.methods ?? []) + if (anyClass.has(m) || declarers.get(`${root}\0${m}`)! > 1) blocked.add(`%${cls.name}.${m}`); + } + // Any function a node names other than as a call's callee may be reached + // through a pointer. + // Every string field counts (eachStringField), as a key walk of the node + // would find them; a call's callee does not. + const referenced = new Set(); + const noteName = (value: string): void => { + if (names.has(value)) referenced.add(value); + }; + const noteCallName = (value: string, field: string): void => { + if (field !== "callee") noteName(value); + }; + // The call graph, for the functions near a throw and the recursive ones. + // Calls through values (closures, class values, dynamic calls, callbacks + // passed to the runtime) reach ANY_FUNCTION, which reaches every function + // a value can hold. + const throws = new Set(); + const callers = new Map(); + const callees = new Map(); + const edge = (from: string, to: string): void => { + let list = callers.get(to); + if (list === undefined) callers.set(to, (list = [])); + list.push(from); + let out = callees.get(from); + if (out === undefined) callees.set(from, (out = [])); + out.push(to); + }; + const methodsNamed = new Map(); + for (const cls of mod.classes ?? []) + for (const m of cls.methods ?? []) { + let list = methodsNamed.get(m); + if (list === undefined) methodsNamed.set(m, (list = [])); + list.push(`%${cls.name}.${m}`); + } + for (const fn of mod.functions) { + const coroutine = fn.async === true || fn.generator !== undefined; + everyStmtList(fn.body, { + expr: (e: IrExpr) => { + const kind = e.kind as string; + if (kind === "throw" || kind === "rethrow") throws.add(fn.name); + if (e.kind === "call") { + edge(fn.name, e.callee); + // The entry's callees (the module initializers) run once: on the + // flag they keep landing pads out of the top-level code inlined + // into the entry. + if (coroutine || fn.name === mod.entry) blocked.add(e.callee); + eachStringField(e, noteCallName); + return true; + } + eachStringField(e, noteName); + if (e.kind === "virtualCall") + for (const target of e.targets ?? methodsNamed.get(e.method) ?? []) edge(fn.name, target); + else if (e.kind === "new") { + edge(fn.name, `%${e.className}.constructor`); + edge(fn.name, `%${e.className}.construct`); + } else if (kind === "callValue" || kind === "dynCall" || kind === "newValue") + edge(fn.name, ANY_FUNCTION); + else if ("args" in e) { + const args: unknown = e.args; + if (Array.isArray(args)) + for (const arg of args as IrExpr[]) + if (arg.type.kind === "func") edge(fn.name, ANY_FUNCTION); + } + return true; + }, + stmt: (s) => { + const kind = s.kind as string; + if (kind === "throw" || kind === "rethrow") throws.add(fn.name); + eachStringField(s, noteName); + return true; + }, + }); + } + for (const name of referenced) { + blocked.add(name); + edge(ANY_FUNCTION, name); + } + // A throw starts on the pending flag, and so do the functions up to + // THROW_LEVELS calls above it. The union narrowing helpers' throws (a + // failed narrowing) don't count for their callers. + for (const name of throws) blocked.add(name); + let level = [...throws].filter((name) => !name.startsWith("%union.")); + const near = new Set(level); + for (let i = 0; i < THROW_LEVELS; i++) { + const next: string[] = []; + for (const name of level) + for (const caller of callers.get(name) ?? []) + if (!near.has(caller)) { + near.add(caller); + next.push(caller); + } + level = next; + } + for (const name of near) blocked.add(name); + for (const name of recursiveFunctions(callees)) blocked.add(name); + const result = new Set(); + for (const fn of mod.functions) { + if (!mayThrow.has(fn.name) || blocked.has(fn.name)) continue; + if (fn.async === true || fn.generator !== undefined) continue; + result.add(fn.name); + } + return result; +} + +/** What an unwinding-ABI function's unwind ends with when no handler of + * its own takes the exception. */ +export const UNWIND_RAISE = "call void @scr_unwind_raise()\n unreachable"; +/** What the unwind of a landing pad ends with when no handler of the + * function takes the exception: the unwind goes on (`resume`) with the + * exception the landing pad stored in UNWIND_SLOT. The block builder + * expands it (BlockBuilder.terminate). */ +export const UNWIND_RESUME = "resume { ptr, i32 } "; +/** The function's slot for the exception its landing pads take. */ +export const UNWIND_SLOT = "%unw.exc"; +/** scr_unwind_raise's declaration. It is not `noreturn`: sanitized builds + * would call the sanitizer's no-return hook before it, which fails on fiber + * stacks; scr_unwind_raise clears the unwound frames' shadow itself. */ +export const UNWIND_RAISE_DECLARATION = "declare void @scr_unwind_raise() cold"; + +/** The personality of functions with landing pads: the landing pads are + * cleanups that scr_unwind_raise's forced unwind runs. */ +export const UNWIND_PERSONALITY = "personality ptr @__gcc_personality_v0"; + +/** An unwind leaving the function: a raise, or a landing pad's `resume`. */ +const UNWINDS = /call void @scr_unwind_raise\(\)|\n\s+resume /; +/** A pending-flag test: the thread's exception word, the active cell, or the runtime query. */ +const PENDING_TEST = /@scr_exc_flag\b|%exc\.cell\b|@scr_exc_active\b|@scr_exc_pending\(/; + +/** Functions emission found breaking the unwinding ABI's rules + * (bridgePlainUnwindCalls), by symbol: the caller emits the module again + * with them on the pending-flag ABI. */ +export class UnwindAbiViolation extends Error { + readonly symbols: ReadonlySet; + constructor(symbols: ReadonlySet) { + super(`unwinding ABI: ${[...symbols].slice(0, 3).join(", ")} must keep the pending flag`); + this.symbols = symbols; + } +} +const DEFINE_NAME = /^define [^@\n]*@([\w.$%-]+)\(/; +const PLAIN_CALL = /^(\s*)(%[\w.$-]+) = call (.*?)@([\w.$%-]+)\(|^(\s*)call (.*?)@([\w.$%-]+)\(/; +const RETURN_ATTRIBUTES = + /\b(?:zeroext|signext|noalias|nonnull|noundef|inreg|dereferenceable(?:_or_null)?\(\d+\)|align \d+)\s+/g; + +/** A call instruction's line. */ +const CALL_LINE = /^\s*(?:%[\w.$-]+ = )?(?:(?:tail|musttail|notail) )?call /; +/** A call's callee (a function or a register) and the parenthesis that + * opens its arguments. */ +const CALLEE = /[@%][\w.$%-]+\(/; + +/** The index of the parenthesis that closes the argument list opened at `open`. */ +function argumentsEnd(line: string, open: number): number { + 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; + } + return close; +} + +/** A call that cannot unwind, marked so: only scr_unwind_raise and the + * raising functions propagate an unwind (runtime code and every function + * reached through a pointer return normally), so every other call is + * `nounwind`, and the inliner keeps it a call where it inlines a raising + * function at an invoke. */ +function markNounwind(line: string): string { + const call = line.indexOf("call "); + const callee = CALLEE.exec(line.slice(call)); + if (callee === null || callee[0] === "@scr_unwind_raise(") return line; + const close = argumentsEnd(line, call + callee.index + callee[0].length - 1); + return close >= line.length + ? line + : `${line.slice(0, close + 1)} nounwind${line.slice(close + 1)}`; +} + +/** The functions of `part`: its text split before each `define` line + * (`part.split(/\n(?=define )/)`), found with indexOf. */ +function splitFunctions(part: string): string[] { + const chunks: string[] = []; + let start = 0; + for (let at = part.indexOf("\ndefine "); at >= 0; at = part.indexOf("\ndefine ", at + 1)) { + chunks.push(part.slice(start, at)); + start = at + 1; + } + chunks.push(part.slice(start)); + return chunks; +} + +/** Whether `code` can be part of a symbol name (`[\w.$%-]`). */ +function isNameCode(code: number): boolean { + return ( + (code >= 97 && code <= 122) || + (code >= 65 && code <= 90) || + (code >= 48 && code <= 57) || + code === 95 /* _ */ || + code === 46 /* . */ || + code === 36 /* $ */ || + code === 37 /* % */ || + code === 45 /* - */ + ); +} + +/** Calls `each` with every line of `chunk` that holds `call `, by its start + * and end offsets, once per line and in order. Lines without `call ` match + * neither CALL_LINE nor PLAIN_CALL. */ +function eachCallLine(chunk: string, each: (start: number, end: number) => void): void { + for (let at = chunk.indexOf("call "); at >= 0;) { + const start = chunk.lastIndexOf("\n", at) + 1; + let end = chunk.indexOf("\n", at); + if (end < 0) end = chunk.length; + each(start, end); + at = chunk.indexOf("call ", end); + } +} + +/** The value a bridged call yields on its landing pad: the dummy a + * pending-flag callee returns with an exception pending. */ +function dummyValue(type: string): string { + if (type === "ptr") return "null"; + if (type === "double" || type === "float") return "0.0"; + if (/^i\d+$/.test(type)) return "0"; + return "zeroinitializer"; +} + +/** Rewrites every remaining plain call of an unwinding-ABI function (one + * that raises, scr_unwind_raise, or resumes an unwind) in `parts`, the module's text, into an + * invoke whose landing pad continues after the call with the exception + * pending and the call's dummy result: the pending-flag contract those + * call sites were emitted for, whichever lowering emitted them. Calls of + * these functions that emitDirectCall lowered are invokes already. */ +export function bridgePlainUnwindCalls(parts: string[]): string[] { + const raisers = new Set(); + const split = parts.map(splitFunctions); + for (const chunks of split) + for (const chunk of chunks) { + const m = DEFINE_NAME.exec(chunk); + if (m !== null && UNWINDS.test(chunk)) raisers.add(m[1]!); + } + if (raisers.size === 0) return parts; + // An invoke's callee must raise or never test the pending flag: one that + // returned with the flag pending would pass the exception by its invoking + // caller unseen. A wrapper an invoke reaches (an int32 or borrowed-ABI + // entry forwarding to the body) propagates its callee's unwind instead + // (`resume`), so it raises too. A raising function must be reached only by + // calls: a pointer to it (a vtable or table entry, a runtime callback) + // could be called without a landing pad. Functions that break either rule + // go back to the pending-flag ABI (UnwindAbiViolation). + const violations = new Set(); + const invoked = new Set(); + const testsFlag = new Set(); + for (const chunks of split) + for (const chunk of chunks) { + const m = DEFINE_NAME.exec(chunk); + if (m !== null && PENDING_TEST.test(chunk)) testsFlag.add(m[1]!); + for (const c of chunk.matchAll(/ invoke [^@\n]*@([\w.$%-]+)\(/g)) invoked.add(c[1]!); + } + const resumers = new Set(); + for (let grew = true; grew;) { + grew = false; + for (const chunks of split) + for (const chunk of chunks) { + const m = DEFINE_NAME.exec(chunk); + const name = m?.[1]; + if (name === undefined || raisers.has(name) || !invoked.has(name)) continue; + if (![...chunk.matchAll(/ call [^@\n]*@([\w.$%-]+)\(/g)].some((c) => raisers.has(c[1]!))) + continue; + if (testsFlag.has(name)) continue; + raisers.add(name); + resumers.add(name); + grew = true; + } + } + for (const name of invoked) if (!raisers.has(name) && testsFlag.has(name)) violations.add(name); + // Every mention of a raising function outside its `define` line, other + // than as the callee of a call or invoke, found with indexOf. + for (const chunks of split) + for (const chunk of chunks) + for (let at = chunk.indexOf("@"); at >= 0;) { + let end = at + 1; + while (end < chunk.length && isNameCode(chunk.charCodeAt(end))) end++; + const next = chunk.indexOf("@", end); + if (end === at + 1) { + at = next; + continue; + } + const name = chunk.slice(at + 1, end); + if (raisers.has(name)) { + const start = chunk.lastIndexOf("\n", at) + 1; + const line = chunk.slice(start, at); + if ( + !line.startsWith("define ") && + !(/(?:call|invoke) [^@]*$/.test(line) && chunk.charAt(end) === "(") + ) + violations.add(name); + } + at = next; + } + if (violations.size > 0) throw new UnwindAbiViolation(violations); + let personality = false; + let counter = 0; + const out = split.map((chunks) => + chunks + .map((chunk) => { + if (!DEFINE_NAME.test(chunk) || !chunk.includes("call ")) return chunk; + // A function without a plain call of a raising function only marks + // its calls nounwind: rewrite just its call lines. + let bridged = false; + const marks: { start: number; end: number; line: string }[] = []; + eachCallLine(chunk, (start, end) => { + if (bridged) return; + const line = chunk.slice(start, end); + const m = PLAIN_CALL.exec(line); + const callee = m === null ? undefined : (m[4] ?? m[7]); + if (m !== null && callee !== undefined && raisers.has(callee)) { + bridged = true; + return; + } + if (!CALL_LINE.test(line)) return; + const marked = markNounwind(line); + if (marked !== line) marks.push({ start, end, line: marked }); + }); + if (!bridged) { + if (marks.length === 0) return chunk; + const pieces: string[] = []; + let from = 0; + for (const mark of marks) { + pieces.push(chunk.slice(from, mark.start), mark.line); + from = mark.end; + } + pieces.push(chunk.slice(from)); + return pieces.join(""); + } + const lines = chunk.split("\n"); + const resume = resumers.has(DEFINE_NAME.exec(chunk)![1]!); + const result: string[] = []; + // The label a block's terminator now follows, for phi operands. + const moved = new Map(); + let block = ""; + let changed = false; + for (let i = 0; i < lines.length; i++) { + const line = lines[i]!; + const label = /^([\w.$-]+):/.exec(line); + if (label !== null) block = label[1]!; + const m = PLAIN_CALL.exec(line); + const callee = m === null ? undefined : (m[4] ?? m[7]); + if (m === null || callee === undefined || !raisers.has(callee)) { + const marked = CALL_LINE.test(line) ? markNounwind(line) : line; + changed ||= marked !== line; + result.push(marked); + continue; + } + // The call's text from its callee to the end of the argument list. + const close = argumentsEnd(line, line.indexOf(`@${callee}(`) + callee.length + 1); + const head = line.slice(line.indexOf("call ") + 5, close + 1); + const tail = line.slice(close + 1); + const meta = /,\s*!/.exec(tail); + const attrs = ( + meta === null ? tail.replace(/;.*$/, "") : tail.slice(0, meta.index) + ).trimEnd(); + const dbg = meta === null ? "" : tail.slice(meta.index).replace(/\s;.*$/, ""); + const n = counter++; + if (block === "") { + result.splice(1, 0, "entry.ub:"); + block = "entry.ub"; + } + const indent = m[1] ?? m[5] ?? " "; + const ok = `ub.ok.${n}`; + const pad = `ub.lp.${n}`; + if (resume) { + // The wrapper holds nothing to release: the unwind goes on to + // its caller's landing pad. + const assign = m[2] !== undefined ? `${m[2]} = ` : ""; + result.push( + `${indent}${assign}invoke ${head}${attrs} to label %${ok} unwind label %${pad}${dbg}`, + ); + result.push( + `${pad}:`, + `${indent}%ub.v.${n} = landingpad { ptr, i32 } cleanup`, + `${indent}resume { ptr, i32 } %ub.v.${n}`, + ); + result.push(`${ok}:`); + } else if (m[2] !== undefined) { + const type = (m[3] ?? "").replace(RETURN_ATTRIBUTES, "").trim(); + const normal = `ub.n.${n}`; + result.push( + `${indent}${m[2]}.ub = invoke ${head}${attrs} to label %${normal} unwind label %${pad}${dbg}`, + ); + result.push(`${normal}:`, `${indent}br label %${ok}`); + result.push( + `${pad}:`, + `${indent}%ub.v.${n} = landingpad { ptr, i32 } cleanup`, + `${indent}br label %${ok}`, + ); + result.push( + `${ok}:`, + `${indent}${m[2]} = phi ${type} [ ${m[2]}.ub, %${normal} ], [ ${dummyValue(type)}, %${pad} ]`, + ); + } else { + result.push( + `${indent}invoke ${head}${attrs} to label %${ok} unwind label %${pad}${dbg}`, + ); + result.push( + `${pad}:`, + `${indent}%ub.v.${n} = landingpad { ptr, i32 } cleanup`, + `${indent}br label %${ok}`, + ); + result.push(`${ok}:`); + } + moved.set(block, ok); + changed = true; + } + if (!changed) return chunk; + if (moved.size === 0) return result.join("\n"); + personality = true; + const header = result[0]!; + const brace = header.lastIndexOf(" {"); + if (!header.includes("personality") && brace >= 0) { + // Before the function's metadata attachments (`!dbg`), if any. + const dbg = header.slice(0, brace).search(/\s!dbg !\d+$/); + const at = dbg >= 0 ? dbg : brace; + result[0] = `${header.slice(0, at)} ${UNWIND_PERSONALITY}${header.slice(at)}`; + } + // A split block's successors now come from its last part. + if (moved.size > 0) + for (let i = 0; i < result.length; i++) { + const line = result[i]!; + if (!line.includes(" phi ")) continue; + result[i] = line.replace(/, %([\w.$-]+) \]/g, (all, name: string) => { + const to = moved.get(name); + return to === undefined ? all : `, %${to} ]`; + }); + } + return result.join("\n"); + }) + .join("\n"), + ); + if (personality && !out.some((part) => part.includes("@__gcc_personality_v0(...)"))) + out[0] = `declare i32 @__gcc_personality_v0(...)\n${out[0]}`; + return out; +} diff --git a/packages/compiler/src/backend/llvm/walkers.ts b/packages/compiler/src/backend/llvm/walkers.ts index 331ea2199..7c67e98ae 100644 --- a/packages/compiler/src/backend/llvm/walkers.ts +++ b/packages/compiler/src/backend/llvm/walkers.ts @@ -26,6 +26,7 @@ import { llFieldType, loadStructField, releaseSym, + retainSym, traceAdapter, type ShapeHost, } from "./shapes.js"; @@ -41,6 +42,8 @@ import { undefinedArmTag } from "../../ir/analysis.js"; * scr_error-style label texts). */ export interface WalkerHost extends ShapeHost { readonly unionsById: Map; + /** Arrays of this element type can hold SCR_ARR_FOREIGN elements. */ + mayHoldForeign(elem: IrType): boolean; /** `@`-ref of an interned immortal ScrStr literal. */ internLiteral(text: string): string; /** `@`-ref of an interned NUL-terminated byte-array constant. */ @@ -234,6 +237,10 @@ export class LlWalkers { this.host.declare(`declare void @scr_jb_put_json_str(ptr, ptr)`); B.line(`call void @scr_jb_put_json_str(ptr %b, ptr %v)`); break; + case "date": + this.host.declare(`declare void @scr_jb_put_date(ptr, ptr)`); + B.line(`call void @scr_jb_put_date(ptr %b, ptr %v) ; toJSON(): ISO text, null if invalid`); + break; case "dyn": // A checked-dynamic value serializes itself (runtime walker). // Bare stringify of dyn stays frontend-fenced, and overflow @@ -321,9 +328,7 @@ export class LlWalkers { if (i > 0) this.putc(B, "%b", "44"); // ',' if (edgeable(f.type)) this.jbEdgeIdx(B, String(i)); const v = this.loadField(B, "%v", shapeId, fieldIndex.get(f.name)!, f.type); - B.line( - `call void @${this.jsonWriteHelper(f.type)}(ptr %b, ${this.valTy(f.type)} ${v}) ; [${llvmCommentText(f.name)}]`, - ); + this.writeField(B, v, f.type, `[${llvmCommentText(f.name)}]`); }); this.putc(B, "%b", "93"); // ']' if (cyclic) this.jbLeave(B); @@ -355,9 +360,7 @@ export class LlWalkers { this.puts(B, "%b", `${i > 0 ? "," : ""}${jsonObjectKeyLabel(f.name)}`); if (edgeable(f.type)) this.jbEdgeProp(B, f.name); const v = this.loadField(B, "%v", shapeId, fieldIndex.get(f.name)!, f.type); - B.line( - `call void @${this.jsonWriteHelper(f.type)}(ptr %b, ${this.valTy(f.type)} ${v}) ; ${llvmCommentText(f.name)}`, - ); + this.writeField(B, v, f.type, `${llvmCommentText(f.name)}`); }); } else { const first = B.slot(); @@ -392,9 +395,7 @@ export class LlWalkers { comma(); this.puts(B, "%b", jsonObjectKeyLabel(f.name)); if (edgeable(f.type)) this.jbEdgeProp(B, f.name); - B.line( - `call void @${this.jsonWriteHelper(f.type)}(ptr %b, ${this.valTy(f.type)} ${v}) ; ${llvmCommentText(f.name)}`, - ); + this.writeField(B, v, f.type, `${llvmCommentText(f.name)}`); if (skip !== null) { B.br(skip); B.startBlock(skip); @@ -516,9 +517,7 @@ export class LlWalkers { B.startBlock(lKey); this.puts(B, "%b", jsonObjectKeyLabel(fieldName)); if (cyclic && traceAdapter(this.host, f.type) !== null) this.jbEdgeProp(B, fieldName); - B.line( - `call void @${this.jsonWriteHelper(f.type)}(ptr %b, ${this.valTy(f.type)} ${v}) ; ${llvmCommentText(fieldName)}`, - ); + this.writeField(B, v, f.type, `${llvmCommentText(fieldName)}`); B.terminate("ret i1 true"); }); B.startBlock(lSkip); @@ -535,14 +534,15 @@ export class LlWalkers { /** Whether the walker for `t` can leave an exception pending: cyclic * values, records serialized through a kept checked source, and - * checked-dynamic values (toJSON methods, getters, BigInts). */ + * checked-dynamic values (toJSON methods, getters, BigInts), including + * the foreign elements of arrays that can hold them. */ jsonWriteMayThrow(t: IrType, seen = new Set()): boolean { if (traceAdapter(this.host, t) !== null) return true; switch (t.kind) { case "dyn": return true; case "array": - return this.jsonWriteMayThrow(t.elem, seen); + return this.host.mayHoldForeign(t.elem) || this.jsonWriteMayThrow(t.elem, seen); case "union": return (this.host.unionsById.get(t.unionId)?.arms ?? []).some((arm) => this.jsonWriteMayThrow(arm, seen), @@ -674,6 +674,47 @@ export class LlWalkers { B.line(`call void @scr_arr_release(ptr ${ks})`); } + /** Serializing a value of `t` can call user code that changes the + * arrays or records holding it: it reaches an array that can hold + * foreign elements (their toJSON and getters), or a value whose own + * serialization may (a class instance, a checked-dynamic value), in a + * program that has foreign elements at all. */ + private serializationMayCallUserCode(t: IrType, seen = new Set()): boolean { + const host = this.host; + if (!host.mayHoldForeign({ kind: "f64" }) && !host.mayHoldForeign({ kind: "string" })) + return false; + const key = typeKey(t); + if (seen.has(key)) return false; + seen.add(key); + switch (t.kind) { + case "dyn": + case "object": + return true; + case "array": + return host.mayHoldForeign(t.elem) || this.serializationMayCallUserCode(t.elem, seen); + case "record": + return (host.recordsById.get(t.shapeId)?.fields ?? []).some((field) => + this.serializationMayCallUserCode(field.type, seen), + ); + case "union": + return (host.unionsById.get(t.unionId)?.arms ?? []).some((arm) => + this.serializationMayCallUserCode(arm, seen), + ); + default: + return false; + } + } + + /** Write a field value loaded borrowed from its record. When its + * serialization can call user code that replaces the field, the value + * is held across the write. */ + private writeField(B: BlockBuilder, v: string, t: IrType, comment: string): void { + const held = isRefCounted(t) && this.serializationMayCallUserCode(t); + if (held) B.line(`call ptr ${retainSym(this.host, t)}(ptr ${v})`); + B.line(`call void @${this.jsonWriteHelper(t)}(ptr %b, ${this.valTy(t)} ${v}) ; ${comment}`); + if (held) B.line(`call void ${releaseSym(this.host, t)}(ptr ${v})`); + } + private emitArrayWriter(B: BlockBuilder, elem: IrType, cyclic: boolean): void { const host = this.host; const w = this.jsonWriteHelper(elem); @@ -683,7 +724,12 @@ export class LlWalkers { if (cyclic) this.jbEnter(B, true); this.putc(B, "%b", "91"); // '[' const finish = B.newLabel("jwa.end"); - if (elem.kind !== "bool") this.emitDenseArrayElements(B, elem, w, cyclic, finish); + // The dense walk borrows the storage and each element. When an + // element's serialization can call user code, which can grow or + // shrink the array or replace the element, the general walk below + // reads each index afresh and holds its element instead. + if (elem.kind !== "bool" && !this.serializationMayCallUserCode(elem)) + this.emitDenseArrayElements(B, elem, w, cyclic, finish); const len = B.tmp(); B.line(`${len} = call double @scr_arr_len(ptr %v)`); B.countedLoop(len, (i) => { @@ -706,6 +752,25 @@ export class LlWalkers { B.line(`${present} = fcmp oeq double ${state}, ${f64Lit(1)}`); B.condBr(present, valueBlock, missingBlock); B.startBlock(missingBlock); + if (host.mayHoldForeign(elem)) { + // A foreign element (SCR_ARR_FOREIGN) serializes its own value, + // after its toJSON runs with the index as its key. + const foreign = B.tmp(); + const foreignBlock = B.newLabel("jwa.foreign"); + const nullBlock = B.newLabel("jwa.null"); + B.line(`${foreign} = fcmp oeq double ${state}, ${f64Lit(3)}`); + B.condBr(foreign, foreignBlock, nullBlock); + B.startBlock(foreignBlock); + host.declare(`declare ptr @scr_arr_get_foreign(ptr, double)`); + host.declare(`declare void @scr_dyn_release(ptr)`); + const payload = B.tmp(); + B.line(`${payload} = call ptr @scr_arr_get_foreign(ptr %v, double ${i}) ; +1`); + host.declare(`declare void @scr_jb_put_dyn_element(ptr, ptr, double)`); + B.line(`call void @scr_jb_put_dyn_element(ptr %b, ptr ${payload}, double ${i})`); + B.line(`call void @scr_dyn_release(ptr ${payload})`); + B.br(doneBlock); + B.startBlock(nullBlock); + } host.declare(`declare void @scr_jb_puts(ptr, ptr)`); B.line(`call void @scr_jb_puts(ptr %b, ptr ${host.cstr("null")})`); B.br(doneBlock); @@ -907,7 +972,28 @@ export class LlWalkers { const lvalue = B.newLabel("uj.p"); B.line(`${state} = call double @scr_arr_state(ptr %a, double ${i})`); B.line(`${isValue} = fcmp oeq double ${state}, ${f64Lit(1)}`); - B.condBr(isValue, lvalue, next); + if (host.mayHoldForeign({ kind: "union", unionId })) { + // A foreign element (SCR_ARR_FOREIGN) joins as its ToString, like + // every element but undefined and null. + const notValue = B.newLabel("uj.f"); + const foreignLabel = B.newLabel("uj.foreign"); + B.condBr(isValue, lvalue, notValue); + B.startBlock(notValue); + const foreign = B.tmp(); + B.line(`${foreign} = fcmp oeq double ${state}, ${f64Lit(3)}`); + B.condBr(foreign, foreignLabel, next); + B.startBlock(foreignLabel); + host.declare(`declare ptr @scr_arr_get_foreign(ptr, double)`); + host.declare(`declare void @scr_dyn_release(ptr)`); + const payload = B.tmp(); + B.line(`${payload} = call ptr @scr_arr_get_foreign(ptr %a, double ${i}) ; +1`); + host.declare(`declare void @scr_jb_join_dyn(ptr, ptr)`); + B.line(`call void @scr_jb_join_dyn(ptr ${buf}, ptr ${payload})`); + B.line(`call void @scr_dyn_release(ptr ${payload})`); + B.br(next); + } else { + B.condBr(isValue, lvalue, next); + } B.startBlock(lvalue); const u = B.tmp(); B.line(`${u} = call ptr @scr_arr_peek_ref(ptr %a, double ${i}) ; borrowed scalar union`); @@ -927,6 +1013,17 @@ export class LlWalkers { case "string": this.putScrStr(B, buf, this.unionPeek(B, u, unionId)); break; + case "date": { + host.declare(`declare ptr @scr_date_object_to_string(ptr)`); + host.declare(`declare void @scr_str_release(ptr)`); + const text = B.tmp(); + B.line( + `${text} = call ptr @scr_date_object_to_string(ptr ${this.unionPeek(B, u, unionId)})`, + ); + this.putScrStr(B, buf, text); + B.line(`call void @scr_str_release(ptr ${text})`); + break; + } case "f64": { host.declare(`declare double @scr_union_get_f64(ptr)`); host.declare(`declare void @scr_jb_put_number(ptr, double)`); diff --git a/packages/compiler/src/backend/llvm/wrapping-sums.ts b/packages/compiler/src/backend/llvm/wrapping-sums.ts index c736fcf72..7c421175c 100644 --- a/packages/compiler/src/backend/llvm/wrapping-sums.ts +++ b/packages/compiler/src/backend/llvm/wrapping-sums.ts @@ -2,15 +2,17 @@ import type { IrExpr, IrFunction, IrStmt } from "../../ir/ir.js"; import { everyExprChild, everyStmtChild } from "../../ir/traverse.js"; import { integerArithmeticRange, type IntegerRange } from "../../ir/integer-ranges.js"; import type { LlValue, LlvmEmitterContext } from "./expr-context.js"; -import type { LlInteger } from "./integer-values.js"; +import { widenInteger, type LlInteger } from "./integer-values.js"; -/** Marks the `+` and `-` operands whose number only reaches ToInt32/ToUint32 - * consumers (`| 0`, the other bitwise operators, `Math.imul`, `Math.clz32`), - * directly or through further sums, in the emitter's `wrappingSums`. Called - * by the consumer before its operands emit. */ +/** Marks the `+`, `-` and `*` operands whose number only reaches + * ToInt32/ToUint32 consumers (`| 0`, the other bitwise operators, + * `Math.imul`, `Math.clz32`), directly or through further sums and + * products, in the emitter's `wrappingSums`. Called by the consumer before + * its operands emit. */ export function markWrappingOperands(host: LlvmEmitterContext, ...operands: IrExpr[]): void { for (const operand of operands) { - if (operand.kind !== "bin" || (operand.op !== "+" && operand.op !== "-")) continue; + if (operand.kind !== "bin" || (operand.op !== "+" && operand.op !== "-" && operand.op !== "*")) + continue; if (operand.type.kind !== "f64" || host.wrappingSums.has(operand)) continue; host.wrappingSums.add(operand); markWrappingOperands(host, operand.left, operand.right); @@ -20,8 +22,9 @@ export function markWrappingOperands(host: LlvmEmitterContext, ...operands: IrEx /** The values an f64 expression can have when each one is an exact integer * (never -0) or NaN: proven ranges, safe integer literals, NaN-coded reads * of integer typed-array elements and of Int32[] elements, `charCodeAt`, - * and sums and differences of those whose every integer result is still - * exact in a double. */ + * and sums, differences and products of those whose every integer result + * is a safe integer, which the double holds exactly. A product that can be + * -0 (a zero times a negative) has no range. */ export function integerOrNaNRange(host: LlvmEmitterContext, e: IrExpr): IntegerRange | null { if (e.type.kind !== "f64") return null; const range = host.integerRanges.get(e); @@ -49,7 +52,7 @@ export function integerOrNaNRange(host: LlvmEmitterContext, e: IrExpr): IntegerR // A UTF-16 code unit, or NaN past either end. if (e.kind === "strIntrinsic" && e.method === "charCodeAt") return { min: 0, max: 65535 }; if (e.kind === "varRef") return host.integerOrNaNLocals.get(e.localId) ?? null; - if (e.kind === "bin" && (e.op === "+" || e.op === "-")) { + if (e.kind === "bin" && (e.op === "+" || e.op === "-" || e.op === "*")) { const left = integerOrNaNRange(host, e.left); const right = left && integerOrNaNRange(host, e.right); return right ? integerArithmeticRange(e.op, left!, right) : null; @@ -69,10 +72,16 @@ export function integerOrNaNIndex( ): { wide: string; ordered: string } | null { const range = expr ? integerOrNaNRange(host, expr) : null; if (!range) return null; + // A value that carries its integer and ordered bit needs no conversion. + if (value.nanInteger !== undefined && value.ordered !== undefined) + return { wide: value.nanInteger, ordered: value.ordered }; const B = host.B; - const ordered = B.tmp(); const wide = B.tmp(); - B.line(`${ordered} = fcmp ord double ${value.name}, 0.0`); + let ordered = value.ordered; + if (ordered === undefined) { + ordered = B.tmp(); + B.line(`${ordered} = fcmp ord double ${value.name}, 0.0`); + } if (value.uint32 !== undefined && range.min >= 0 && range.max <= 4294967295) { B.line(`${wide} = zext i32 ${value.uint32} to i64`); } else if (value.uint32 !== undefined && range.min >= -2147483648 && range.max <= 2147483647) { @@ -111,9 +120,14 @@ function wrappingOperand( return { low, valid: null }; } // The number is an exact integer or NaN, and its ToUint32 view is the - // integer's low 32 bits or 0. An ordered compare tells the two apart. - const valid = B.tmp(); - B.line(`${valid} = fcmp ord double ${value.name}, 0.0`); + // integer's low 32 bits or 0. Its ordered bit, or an ordered compare, + // tells the two apart. + let valid: string | null = value.ordered ?? null; + if (valid === "true") valid = null; + else if (valid === null) { + valid = B.tmp(); + B.line(`${valid} = fcmp ord double ${value.name}, 0.0`); + } if (value.uint32 !== undefined) return { low: value.uint32, valid }; // Without a view, convert once. The conversion of NaN is poison, but the // result only selects the sum when every operand is ordered. @@ -128,12 +142,16 @@ function wrappingOperand( return { low, valid }; } -/** ToUint32 of `left op right` (`+` or `-`) in 32-bit arithmetic when the - * expression only reaches ToInt32/ToUint32 consumers and both operands are - * exact integers or NaN with an exact result. Both conversions are - * arithmetic mod 2^32, so the wrapping i32 sum of the operands' low halves - * is the converted exact sum; a NaN operand makes the sum NaN, which - * converts to 0. Returns undefined, emitting nothing, for any other sum. */ +/** ToUint32 of `left op right` (`+`, `-` or `*`) in 32-bit arithmetic when + * the expression only reaches ToInt32/ToUint32 consumers and both operands + * are exact integers or NaN with an exact result. Both conversions are + * arithmetic mod 2^32, so the wrapping i32 sum or product of the operands' + * low halves is the converted exact result; a NaN operand makes the result + * NaN, which converts to 0. A product is exact when the operands' magnitude + * bounds multiply to at most 2^53; it may be -0 (`0 * -1`), which converts + * to 0 like the integer 0. Returns the converted result with its ordered + * bit (`true` when no operand can be NaN), or undefined, emitting nothing, + * for any other expression. */ export function emitWrappingSum( host: LlvmEmitterContext, e: IrExpr & { kind: "bin" }, @@ -141,25 +159,88 @@ export function emitWrappingSum( r: LlValue, li: LlInteger | null, ri: LlInteger | null, -): string | undefined { - if ((e.op !== "+" && e.op !== "-") || !host.wrappingSums.has(e)) return undefined; +): { uint32: string; ordered: string } | undefined { + if ((e.op !== "+" && e.op !== "-" && e.op !== "*") || !host.wrappingSums.has(e)) return undefined; const leftRange = li ? li.range : integerOrNaNRange(host, e.left); const rightRange = leftRange && (ri ? ri.range : integerOrNaNRange(host, e.right)); - if (!rightRange || !integerArithmeticRange(e.op, leftRange!, rightRange)) return undefined; + if (!rightRange) return undefined; + if ( + e.op === "*" + ? !exactProduct(leftRange!, rightRange) + : !integerArithmeticRange(e.op, leftRange!, rightRange) + ) + return undefined; const B = host.B; const left = wrappingOperand(host, l, li, leftRange!); const right = wrappingOperand(host, r, ri, rightRange); const sum = B.tmp(); - B.line(`${sum} = ${e.op === "+" ? "add" : "sub"} i32 ${left.low}, ${right.low}`); + B.line( + `${sum} = ${e.op === "+" ? "add" : e.op === "-" ? "sub" : "mul"} i32 ${left.low}, ${right.low}`, + ); let valid = left.valid ?? right.valid; - if (valid === null) return sum; + if (valid === null) return { uint32: sum, ordered: "true" }; if (left.valid && right.valid) { valid = B.tmp(); B.line(`${valid} = and i1 ${left.valid}, ${right.valid}`); } const out = B.tmp(); B.line(`${out} = select i1 ${valid}, i32 ${sum}, i32 0`); - return out; + return { uint32: out, ordered: valid }; +} + +/** The integer of `left op right` (`+`, `-` or `*`) and its ordered bit + * when the expression is an exact integer or NaN (`integerOrNaNRange`) and + * each operand carries an integer: its exact integer, or its `nanInteger` + * and ordered bit. The result is NaN exactly when an operand is, and the i64 + * arithmetic of the operands' integers is the result's integer otherwise: + * the range keeps it within the safe integers. While an operand is NaN its + * integer is 0 or another integer of these forms, so the i64 arithmetic + * stays within range either way. Undefined, emitting nothing, for any other + * expression. */ +export function emitNanArithmetic( + host: LlvmEmitterContext, + e: IrExpr & { kind: "bin" }, + l: LlValue, + r: LlValue, + li: LlInteger | null, + ri: LlInteger | null, +): { nanInteger: string; ordered: string } | undefined { + if (e.op !== "+" && e.op !== "-" && e.op !== "*") return undefined; + const operand = ( + value: LlValue, + integer: LlInteger | null, + ): { integer: string; ordered: string } | null => { + if (integer) return { integer: widenInteger(host, integer), ordered: "true" }; + if (value.nanInteger === undefined || value.ordered === undefined) return null; + return { integer: value.nanInteger, ordered: value.ordered }; + }; + if ((!li && l.nanInteger === undefined) || (!ri && r.nanInteger === undefined)) return undefined; + if (!integerOrNaNRange(host, e)) return undefined; + const left = operand(l, li); + const right = left && operand(r, ri); + if (!right) return undefined; + const B = host.B; + const nanInteger = B.tmp(); + B.line( + `${nanInteger} = ${e.op === "+" ? "add" : e.op === "-" ? "sub" : "mul"} i64 ${left!.integer}, ${right.integer}`, + ); + let ordered = left!.ordered === "true" ? right.ordered : left!.ordered; + if (left!.ordered !== "true" && right.ordered !== "true") { + ordered = B.tmp(); + B.line(`${ordered} = and i1 ${left!.ordered}, ${right.ordered}`); + } + return { nanInteger, ordered }; +} + +/** Whether every product of a value in `left` and one in `right` is an + * integer of magnitude below 2^53, which the double product holds exactly. + * The bound's own product is computed in a double: a bound of at least + * 2^53 never rounds below 2^53, while 2^53 + 1 (3 * 3002399751580331) + * rounds to 2^53, so the test is strict. */ +function exactProduct(left: IntegerRange, right: IntegerRange): boolean { + const magnitude = (range: IntegerRange): number => + Math.max(Math.abs(range.min), Math.abs(range.max)); + return magnitude(left) * magnitude(right) < 2 ** 53; } /** The number locals of `fn` that only hold exact integers (never -0) or NaN, with the range of diff --git a/packages/compiler/src/backend/loop-polls.ts b/packages/compiler/src/backend/loop-polls.ts index 14c2b049c..866711fb7 100644 --- a/packages/compiler/src/backend/loop-polls.ts +++ b/packages/compiler/src/backend/loop-polls.ts @@ -106,3 +106,34 @@ export function loopTestsEveryIteration(loop: Loop, tested: (call: IrExpr) => bo } return false; } + +/** True when the body of `loop`, outside its nested loops and closures, + * reads or writes a typed-array element: the loops whose cancellation poll + * the emitter moves between blocks of iterations, so that LLVM can + * vectorize them. Outside another loop, other loops keep the budget in + * their body, which costs less in a loop of a few iterations. */ +export function loopAccessesTypedArrays(loop: Loop): boolean { + const inExpr = (e: IrExpr): boolean => { + if (e.kind === "bytesIntrinsic" && e.method === "get") return true; + let found = false; + everyExprChild( + e, + (child) => !(found = inExpr(child)), + (child) => !(found = inStmt(child)), + ); + return found; + }; + const inStmt = (s: IrStmt): boolean => { + if (s.kind === "bytesSet") return true; + if (s.kind === "while" || s.kind === "doWhile" || s.kind === "for" || s.kind === "forOf") + return false; + let found = false; + everyStmtChild( + s, + (child) => !(found = inExpr(child)), + (child) => !(found = inStmt(child)), + ); + return found; + }; + return loop.body.some(inStmt); +} diff --git a/packages/compiler/src/backend/mangle.ts b/packages/compiler/src/backend/mangle.ts index 14736ead7..8542a1c64 100644 --- a/packages/compiler/src/backend/mangle.ts +++ b/packages/compiler/src/backend/mangle.ts @@ -221,15 +221,6 @@ export function mangleGenDrop(fnName: string): string { export function mangleGenSourceClose(fnName: string): string { return `sc_gc_${sanitize(fnName)}`; } -/** Emitted ref-kind resolve thunk, interned per inner-type key. */ -export function mangleResolveThunk(n: number): string { - return `sc_rslv_${n}`; -} -/** Emitted generator-resume result builder (the IteratorResult record), - * interned per generator-type key. */ -export function mangleGenResThunk(n: number): string { - return `sc_gres_${n}`; -} /** Emitted Promise.race fulfillment adapter (entry inner type → result * inner type), interned per type pair. */ export function mangleRaceThunk(n: number): string { diff --git a/packages/compiler/src/backend/may-throw.ts b/packages/compiler/src/backend/may-throw.ts index 0cfab9244..693d20b3b 100644 --- a/packages/compiler/src/backend/may-throw.ts +++ b/packages/compiler/src/backend/may-throw.ts @@ -93,7 +93,7 @@ export function computeMayThrow( const classes = new Map((mod.classes ?? []).map((cls) => [cls.name, cls])); const constructorClasses = new Map(); const classValueCallers = new Map>(); - const tdzGlobals = (mod.globals ?? []).filter((g) => g.tdz).map((g) => g.id); + const tdzGlobals = new Set((mod.globals ?? []).filter((g) => g.tdz).map((g) => g.id)); for (const cls of mod.classes ?? []) { constructorClasses.set(`%${cls.name}.constructor`, cls.name); for (const m of cls.methods ?? []) { @@ -113,11 +113,10 @@ export function computeMayThrow( if (explain) f.calleeLocs = new Map(); // TDZ reads and non-initializing writes can throw ReferenceError. // Capture locals carry the same flag as the declaring binding. - const tdzIds = new Set([...tdzGlobals, ...fn.locals.filter((l) => l.tdz).map((l) => l.id)]); - const mutableTdzIds = new Set([ - ...tdzGlobals, - ...fn.locals.filter((l) => l.tdz && l.mutable).map((l) => l.id), - ]); + // Every TDZ global counts for both reads and writes; the module's set is + // shared by every function. + const tdzLocals = new Set(fn.locals.filter((l) => l.tdz).map((l) => l.id)); + const mutableTdzLocals = new Set(fn.locals.filter((l) => l.tdz && l.mutable).map((l) => l.id)); // Traverse typed executable nodes without copying the IR into unknown. const visit = (rec: IrExpr | IrStmt): boolean => { const threw = f.throws; @@ -133,13 +132,17 @@ export function computeMayThrow( case "varRef": case "incDec": case "assignExpr": - if (tdzIds.has(rec.localId)) f.throws = true; + if (tdzGlobals.has(rec.localId) || tdzLocals.has(rec.localId)) f.throws = true; break; case "assign": // A declaration is allowed to fill an empty box. Legacy const // TDZ stores also initialize; only mutable subsequent stores // introduce the new write-side exception edge. - if (rec.initializes !== true && mutableTdzIds.has(rec.localId)) f.throws = true; + if ( + rec.initializes !== true && + (tdzGlobals.has(rec.localId) || mutableTdzLocals.has(rec.localId)) + ) + f.throws = true; break; case "dynCheck": case "caughtCheck": diff --git a/packages/compiler/src/backend/native-codegen-core.ts b/packages/compiler/src/backend/native-codegen-core.ts index 5b9437cde..dd17466eb 100644 --- a/packages/compiler/src/backend/native-codegen-core.ts +++ b/packages/compiler/src/backend/native-codegen-core.ts @@ -6,19 +6,29 @@ export const NATIVE_CODEGEN_LLVM_VERSION = "22.1.8"; export type NativeCodegenOutputKind = "asm" | "obj"; -/** LLVM text per partition, and the most partitions one executable uses. */ +/** LLVM text that stays one module, LLVM text per partition, the fewest + * partitions (MIN_PROGRAM_PARTITIONS, of at least MIN_PARTITION_BYTES each) + * a divided module gets, and the most one executable uses. */ +const SINGLE_PARTITION_BYTES = 1024 * 1024; const PROGRAM_PARTITION_BYTES = 4 * 1024 * 1024; +const MIN_PARTITION_BYTES = 512 * 1024; +const MIN_PROGRAM_PARTITIONS = 8; const MAX_PROGRAM_PARTITIONS = 64; -/** Executable objects this large are divided into partitions that the helper - * simplifies, optimizes and compiles concurrently, importing small functions - * across partitions for inlining, and reuses individually across builds. The +/** Executable objects larger than SINGLE_PARTITION_BYTES are divided into + * partitions that the helper simplifies, optimizes and compiles concurrently, + * importing small functions across partitions for inlining, and reuses + * individually across builds. A divided module gets at least + * MIN_PROGRAM_PARTITIONS so that mid-size programs use several cores. The * count is a power of two that depends only on the module's size, on every * target, so artifacts never vary with the host and ordinary edits keep every * function in its partition. */ export function nativeProgramPartitions(llvmBytes: number): number { - const required = Math.ceil(llvmBytes / PROGRAM_PARTITION_BYTES); - if (required < 2) return 1; + if (llvmBytes <= SINGLE_PARTITION_BYTES) return 1; + const required = Math.max( + Math.ceil(llvmBytes / PROGRAM_PARTITION_BYTES), + Math.min(MIN_PROGRAM_PARTITIONS, Math.ceil(llvmBytes / MIN_PARTITION_BYTES)), + ); return Math.min(MAX_PROGRAM_PARTITIONS, 2 ** Math.ceil(Math.log2(required))); } diff --git a/packages/compiler/src/backend/native-codegen.test.ts b/packages/compiler/src/backend/native-codegen.test.ts index 545711a60..64961bcaa 100644 --- a/packages/compiler/src/backend/native-codegen.test.ts +++ b/packages/compiler/src/backend/native-codegen.test.ts @@ -1,4 +1,4 @@ -import { chmod, mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises"; +import { chmod, mkdir, mkdtemp, readFile, readdir, rm, stat, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { afterEach, expect, test } from "vitest"; @@ -26,6 +26,7 @@ async function fakePackage( emptyOutput?: boolean; missingOutput?: boolean; changePackageDuringEmit?: boolean; + profileReport?: string; } = {}, ) { const root = await mkdtemp(join(tmpdir(), "scriptc-native-helper-test-")); @@ -72,6 +73,7 @@ ${ ? ":" : 'for each in $outputs; do cp "$input" "$each"; done' } +${options.profileReport === undefined ? "" : `printf '%s\\n' '${options.profileReport}'`} `, ); await chmod(bin, 0o755); @@ -196,6 +198,41 @@ test("profiles reach the helper and key the native cache by their bytes", async expect(calls[2]).toContain("--profile-use /profiles/a.profdata"); }); +test("a cached artifact keeps the profile match of the build that produced it", async () => { + const pkg = await fakePackage({ + profileReport: + '{"ok":true,"profile":{"annotated":120,"mismatched":7,"missing":40,"unknown_partitions":0}}', + }); + const used = { + ...request(pkg.root, pkg.packageJson, join(pkg.root, "used.o")), + profile: { use: { path: "/profiles/a.profdata", digest: "a".repeat(64) } }, + }; + const match = { annotated: 120, mismatched: 7, missing: 40, unknownPartitions: 0 }; + expect((await emitNativeArtifact(used)).profileMatch).toEqual(match); + const again = await emitNativeArtifact({ ...used, outputPath: join(pkg.root, "again.o") }); + expect(again.profileMatch).toEqual(match); + expect((await readFile(pkg.log, "utf8")).trim().split("\n")).toHaveLength(1); + // Without the cached match, the cached partition's counts are unknown. + const cache = join(pkg.root, "cache", "native-codegen-v1"); + const [shard] = await readdir(cache); + for (const name of await readdir(join(cache, shard!))) + if (name.includes(".profile-match")) await rm(join(cache, shard!, name)); + const unknown = await emitNativeArtifact({ ...used, outputPath: join(pkg.root, "unknown.o") }); + expect(unknown.profileMatch).toEqual({ + annotated: 0, + mismatched: 0, + missing: 0, + unknownPartitions: 1, + }); + // Builds without a profile carry no match, from the helper or the cache. + const plain = request(pkg.root, pkg.packageJson, join(pkg.root, "plain.o")); + expect((await emitNativeArtifact(plain)).profileMatch).toBeUndefined(); + expect( + (await emitNativeArtifact({ ...plain, outputPath: join(pkg.root, "plain-again.o") })) + .profileMatch, + ).toBeUndefined(); +}); + test("chunked LLVM input preserves emitted bytes and the native cache identity", async () => { const pkg = await fakePackage(); const first = request(pkg.root, pkg.packageJson, join(pkg.root, "chunks.o")); @@ -352,11 +389,13 @@ test("sanitized native artifacts fail before helper resolution", async () => { test("program partitions depend only on module size", () => { const megabyte = 1024 * 1024; - expect(nativeProgramPartitions(4 * megabyte)).toBe(1); - expect(nativeProgramPartitions(4 * megabyte + 1)).toBe(2); + expect(nativeProgramPartitions(megabyte)).toBe(1); + expect(nativeProgramPartitions(megabyte + 1)).toBe(4); + expect(nativeProgramPartitions(2 * megabyte)).toBe(4); + expect(nativeProgramPartitions(2 * megabyte + 1)).toBe(8); + expect(nativeProgramPartitions(32 * megabyte)).toBe(8); // Powers of two keep every function in its partition across small edits. - expect(nativeProgramPartitions(13 * megabyte)).toBe(4); - expect(nativeProgramPartitions(16 * megabyte)).toBe(4); + expect(nativeProgramPartitions(32 * megabyte + 1)).toBe(16); expect(nativeProgramPartitions(100 * megabyte)).toBe(32); expect(nativeProgramPartitions(1024 * megabyte)).toBe(64); expect(nativePartitionPaths("/build.d/program.o", 3)).toEqual([ diff --git a/packages/compiler/src/backend/native-codegen.ts b/packages/compiler/src/backend/native-codegen.ts index 5b91c60e7..84c76ce91 100644 --- a/packages/compiler/src/backend/native-codegen.ts +++ b/packages/compiler/src/backend/native-codegen.ts @@ -2,7 +2,17 @@ import { execFile } from "node:child_process"; import { createHash, type Hash } from "node:crypto"; import { closeSync, constants, openSync, readSync } from "node:fs"; import { createRequire } from "node:module"; -import { access, chmod, copyFile, mkdir, readFile, rename, rm, stat } from "node:fs/promises"; +import { + access, + chmod, + copyFile, + mkdir, + readFile, + rename, + rm, + stat, + writeFile, +} from "node:fs/promises"; import { dirname, join } from "node:path"; import { promisify } from "node:util"; import { @@ -30,6 +40,8 @@ import { compilerReleaseVersion } from "../library/sidecar.js"; import { nativeProfileHelperArgs, nativeProfileKey, + parseNativeProfileMatch, + type NativeProfileMatch, type NativeProfileOptions, } from "./native-profile.js"; import { writeTextParts } from "../text-parts.js"; @@ -69,6 +81,8 @@ export interface NativeCodegenArtifact { dependencies: NativeArtifactDependency[]; /** Every emitted file, starting with the requested output path. */ outputPaths: string[]; + /** How the build's profile matched, when it used one and the helper ran. */ + profileMatch?: NativeProfileMatch; } const resolvedHelperCache = new Map>(); @@ -432,7 +446,10 @@ export async function emitNativeArtifact( dependencies: helper.dependencies, outputPaths: outputs, } satisfies NativeCodegenArtifact; - if (await installCachedPartitions(cached, outputs)) return artifact; + if (await installCachedPartitions(cached, outputs)) { + if (options.profile?.use === undefined) return artifact; + return { ...artifact, profileMatch: await cachedProfileMatch(cached[0]!, outputs.length) }; + } // Partitioned programs also reuse individual partition artifacts, so an // edit recompiles only the partitions it affects. The directory names the // exact helper build and target; the helper keys entries by content. @@ -455,10 +472,11 @@ export async function emitNativeArtifact( // The helper reads a private copy: the caller's .ll may be shared with a // concurrent build of another entry that has the same output stem. const input = privateSiblingPath(options.outputPath, "native-input"); + const matchStage = privateSiblingPath(options.outputPath, "native-profile-match"); try { if (isLlvmFile(options.llvm)) await copyFile(options.llvm.file, input); else await writeLlvmSource(input, options.llvm, 0o600); - await invoke(helper.binaryPath, [ + const { stdout } = await invoke(helper.binaryPath, [ "emit", "--input", input, @@ -489,6 +507,8 @@ export async function emitNativeArtifact( } await chmod(stage, artifactMode()); } + const profileMatch = + options.profile?.use === undefined ? null : parseNativeProfileMatch(stdout); // Cache publication is an optimization boundary. The helper has already // produced a valid caller artifact, so a read-only/full cache must not // discard it or turn an otherwise successful build into an exception. @@ -496,21 +516,68 @@ export async function emitNativeArtifact( root !== null && (await nativeArtifactDependenciesStillMatch(helper.dependencies).catch(() => false)) ) { + // The match goes in first: a reader that finds every partition finds it too. + if (profileMatch !== null) { + await writeFile( + matchStage, + `${JSON.stringify({ profile: helperProfileMatch(profileMatch) })}\n`, + { + mode: 0o600, + }, + ) + .then(() => publishCachedFile(matchStage, cachedProfileMatchPath(cached[0]!))) + .catch(() => undefined); + } for (let index = 0; index < stages.length; index++) await publishCachedFile(stages[index]!, cached[index]!).catch(() => undefined); } for (let index = 0; index < stages.length; index++) await rename(stages[index]!, outputs[index]!); await pruneBuildCache(root); - return artifact; + return profileMatch === null ? artifact : { ...artifact, profileMatch }; } finally { await Promise.all([ ...stages.map((stage) => rm(stage, { force: true }).catch(() => undefined)), rm(input, { force: true }).catch(() => undefined), + rm(matchStage, { force: true }).catch(() => undefined), ]); } } +/** The cache entry beside a cached artifact's first partition that holds the + * profile match of the build that produced it. */ +function cachedProfileMatchPath(firstPartition: string): string { + return `${firstPartition}.profile-match`; +} + +/** The match in the helper's own JSON form, as parseNativeProfileMatch reads it. */ +function helperProfileMatch(match: NativeProfileMatch): Record { + return { + annotated: match.annotated, + mismatched: match.mismatched, + missing: match.missing, + unknown_partitions: match.unknownPartitions, + }; +} + +/** The profile match of a cached artifact. Without a valid entry, every + * partition's counts are unknown. */ +async function cachedProfileMatch( + firstPartition: string, + partitions: number, +): Promise { + const path = cachedProfileMatchPath(firstPartition); + const text = (await validCachedFile(path)) ? await readFile(path, "utf8").catch(() => "") : ""; + return ( + parseNativeProfileMatch(text) ?? { + annotated: 0, + mismatched: 0, + missing: 0, + unknownPartitions: partitions, + } + ); +} + /** Install a cached artifact only when every partition is present and valid. */ async function installCachedPartitions( cached: readonly (string | null)[], diff --git a/packages/compiler/src/backend/native-link-info-core.ts b/packages/compiler/src/backend/native-link-info-core.ts index 093256575..525698759 100644 --- a/packages/compiler/src/backend/native-link-info-core.ts +++ b/packages/compiler/src/backend/native-link-info-core.ts @@ -37,7 +37,12 @@ export interface NativeLinkFeatures { /** Calls into Node.js (@scriptc/node): scr_node_island.c and the * SCR_NODE_ISLANDS variant of the event loop unit. */ nodeIsland?: boolean; + /** Module namespace objects as checked-dynamic values link + * scr_module_namespace.c; absent (false) otherwise. */ + moduleNamespace?: boolean; zlib: boolean; + /** node:zlib's Brotli forms: scr_brotli.c and the vendored brotli archive. */ + brotli?: boolean; assert: boolean; inspect: boolean; dynInvoke: boolean; @@ -53,18 +58,26 @@ export interface NativeLinkFeatures { * Absent (false) for programs without them. */ handleProps?: boolean; declAdopt?: boolean; + typeOrigins?: boolean; /** for await over checked-dynamic values: scr_dyn_for_await.c. Absent * (false) for programs without it. */ dynForAwait?: boolean; /** events.once with options: scr_events_once_signal.c. Absent (not * false) for programs without it. */ eventsOnceSignal?: boolean; + /** util.MIMEType and util.MIMEParams: scr_mime.c. */ + mime?: boolean; + /** util.debounce and util.throttle: scr_util_timing.c. */ + utilTiming?: boolean; /** Buffer.prototype methods called on checked-dynamic values link * scr_bytes_dyn.c; absent (false) for programs without them. */ dynBuffer?: boolean; /** Builtin functions read as values (Buffer.byteLength, Buffer.alloc, * fs.realpathSync) link scr_builtin_values.c; absent (false) otherwise. */ builtinValues?: boolean; + /** createRequire's computed require and require.resolve link + * scr_require.c; absent (false) otherwise. */ + computedRequire?: boolean; events: boolean; emitter: boolean; symbol: boolean; @@ -84,6 +97,9 @@ export interface NativeLinkFeatures { dnsPromises: boolean; /** dgram sockets as untyped values: scr_dgram_dyn.c. */ dgramDyn: boolean; + /** TextEncoder and TextDecoder records as untyped values: + * scr_text_codec_dyn.c. */ + textCodecDyn: boolean; watch: boolean; foreignFfi: boolean; nodeTest: boolean; @@ -104,6 +120,9 @@ export interface NativeLinkFeatures { /** Ed25519 keys and signatures with pki: scr_ed25519.c (the vendored * libsodium Ed25519). */ ed25519: boolean; + /** X509Certificate fields beyond the validity window and fingerprint: + * scr_x509.c. */ + x509: boolean; /** perf_hooks event-loop metrics: scr_perf_loop.c. */ perfLoop: boolean; tlsCa: boolean; diff --git a/packages/compiler/src/backend/native-profile.test.ts b/packages/compiler/src/backend/native-profile.test.ts index 590c39eef..18f0c6f73 100644 --- a/packages/compiler/src/backend/native-profile.test.ts +++ b/packages/compiler/src/backend/native-profile.test.ts @@ -1,7 +1,9 @@ import { expect, test } from "vitest"; import { executableLinkInputs } from "./link-plan-core.js"; import { + parseNativeProfileMatch, nativeProfileHelperArgs, + renderNativeProfileMatch, nativeProfileKey, nativeProfileRefusal, profileGenerateLinkerArgs, @@ -71,3 +73,61 @@ test("only the instrumented link adds the profile runtime", () => { "-fprofile-generate", ); }); + +test("the helper's profile match is read from its output", () => { + expect( + parseNativeProfileMatch( + '{"ok":true,"profile":{"annotated":120,"mismatched":7,"missing":40,"unknown_partitions":1}}\n', + ), + ).toEqual({ annotated: 120, mismatched: 7, missing: 40, unknownPartitions: 1 }); + // A helper that predates the report prints nothing. + expect(parseNativeProfileMatch("")).toBeNull(); + expect(parseNativeProfileMatch('{"ok":true}\n')).toBeNull(); + expect(parseNativeProfileMatch('{"ok":true,"profile":{"annotated":-1}}\n')).toBeNull(); +}); + +test("the build reports the match, and warns when the profile is stale", () => { + expect( + renderNativeProfileMatch("app.profdata", { + annotated: 9170, + mismatched: 0, + missing: 28636, + unknownPartitions: 0, + }), + ).toBe( + "scriptc: profile app.profdata: 9170 functions annotated, 0 mismatched, 28636 without a record\n", + ); + // 10% of the described functions changed: at the threshold, no warning. + expect( + renderNativeProfileMatch("app.profdata", { + annotated: 90, + mismatched: 10, + missing: 5, + unknownPartitions: 1, + }), + ).toBe( + "scriptc: profile app.profdata: 90 functions annotated, 10 mismatched, 5 without a record, 1 partition without counts\n", + ); + expect( + renderNativeProfileMatch("app.profdata", { + annotated: 5208, + mismatched: 3739, + missing: 28859, + unknownPartitions: 2, + }), + ).toBe( + "scriptc: profile app.profdata: 5208 functions annotated, 3739 mismatched, 28859 without a record, 2 partitions without counts\n" + + "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\n", + ); + // Nothing described: no share to warn about. + expect( + renderNativeProfileMatch("app.profdata", { + annotated: 0, + mismatched: 0, + missing: 12, + unknownPartitions: 0, + }), + ).toBe( + "scriptc: profile app.profdata: 0 functions annotated, 0 mismatched, 12 without a record\n", + ); +}); diff --git a/packages/compiler/src/backend/native-profile.ts b/packages/compiler/src/backend/native-profile.ts index 4cd766319..85b1fdca4 100644 --- a/packages/compiler/src/backend/native-profile.ts +++ b/packages/compiler/src/backend/native-profile.ts @@ -44,6 +44,71 @@ export function nativeProfileHelperArgs(profile: NativeProfileOptions | undefine return []; } +/** How an optimized build's profile matched the program's functions, as the + * helper reports it: functions that took the profile's counts, functions + * whose control flow changed since the profile was collected, functions the + * profile has no record of (new, or never run while it was collected), and + * partitions reused from the cache without their counts. */ +export interface NativeProfileMatch { + annotated: number; + mismatched: number; + missing: number; + unknownPartitions: number; +} + +/** The profile match in a successful helper emit's output, or null when the + * helper reports none (a helper that predates the report). */ +export function parseNativeProfileMatch(stdout: string): NativeProfileMatch | null { + for (const line of stdout.split("\n")) { + let parsed: unknown; + try { + parsed = JSON.parse(line); + } catch { + continue; + } + const profile = (parsed as { profile?: Record } | null)?.profile; + if (typeof profile !== "object" || profile === null) continue; + const count = (key: string): number | null => { + const value = profile[key]; + return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : null; + }; + const annotated = count("annotated"); + const mismatched = count("mismatched"); + const missing = count("missing"); + const unknownPartitions = count("unknown_partitions"); + if (annotated === null || mismatched === null || missing === null || unknownPartitions === null) + continue; + return { annotated, mismatched, missing, unknownPartitions }; + } + return null; +} + +/** The share of the functions a profile describes whose control flow changed + * since it was collected, above which `scriptc build` warns that the profile + * is stale. A freshly collected profile has none. */ +export const STALE_PROFILE_SHARE = 0.1; + +/** `scriptc build --profile-use`'s report on stderr: one line with the match, + * and a warning line when more than STALE_PROFILE_SHARE of the functions the + * profile describes changed since it was collected. */ +export function renderNativeProfileMatch(profile: string, match: NativeProfileMatch): string { + const unknown = + match.unknownPartitions === 0 + ? "" + : `, ${match.unknownPartitions} partition${match.unknownPartitions === 1 ? "" : "s"} without counts`; + const lines = [ + `scriptc: profile ${profile}: ${match.annotated} functions annotated, ${match.mismatched} mismatched, ${match.missing} without a record${unknown}`, + ]; + const described = match.annotated + match.mismatched; + if (described > 0 && match.mismatched / described > STALE_PROFILE_SHARE) { + const percent = ((match.mismatched / described) * 100).toFixed(1); + lines.push( + `scriptc: warning: ${match.mismatched} of the ${described} functions ${profile} describes (${percent}%) changed since it was collected and are optimized without their counts; collect the profile again`, + ); + } + return `${lines.join("\n")}\n`; +} + /** Cache-key part; empty without a profile so other keys are unchanged. */ export function nativeProfileKey(profile: NativeProfileOptions | undefined): string { if (profile?.generate !== undefined) return `profile-generate:${profile.generate}`; diff --git a/packages/compiler/src/backend/native-toolchain.test.ts b/packages/compiler/src/backend/native-toolchain.test.ts index fd7a5325f..0ac21e853 100644 --- a/packages/compiler/src/backend/native-toolchain.test.ts +++ b/packages/compiler/src/backend/native-toolchain.test.ts @@ -24,6 +24,7 @@ import { compileLibArchive, executableSectionEliminationFlags, executableNativeEnvironmentFingerprint, + implicitDependencyIncludeDirective, implicitDependencyProbeIncludes, parseLinkTraceFiles, resolveBuildCacheRoot, @@ -624,6 +625,46 @@ test("implicit dependency seeds include separately compiled vendor system header expect(includes).toContain(""); }); +test.skipIf(clangExecutable === undefined)( + "the implicit toolchain probe guards target-specific compiler headers", + async () => { + // clang ships its x86 intrinsic headers (, ...) for every + // target, so __has_include finds them on arm64, where including them is + // an #error, and the same holds for the ARM headers on x86. The probe + // must guard them by target. Only the compiler's own freestanding + // headers are visible here, so the result does not depend on the host's + // C library. + const includes = (await implicitDependencyProbeIncludes(runtimeSrcDir())).filter( + (include) => + include !== "" && /intrin\.h>$|^$|^$/.test(include), + ); + expect(includes).toContain(""); + const resourceDir = execFileSync(clangExecutable!, ["-print-resource-dir"], { + encoding: "utf8", + }).trim(); + const source = includes.map(implicitDependencyIncludeDirective).join(""); + for (const target of ["aarch64-unknown-linux-gnu", "x86_64-unknown-linux-gnu"]) { + const result = spawnSync( + clangExecutable!, + [ + `--target=${target}`, + "-ffreestanding", + "-nostdinc", + "-isystem", + join(resourceDir, "include"), + "-fsyntax-only", + "-x", + "c", + "-", + ], + { input: source, encoding: "utf8" }, + ); + expect(result.stderr, target).toBe(""); + expect(result.status, target).toBe(0); + } + }, +); + test.skipIf( process.platform === "win32" || clangExecutable === undefined || @@ -3983,12 +4024,12 @@ test.skipIf( programShards: [ { name: "program-f000.ll", - source: + source: () => "define i32 @main() {\nentry:\n ret i32 7\n}\ndeclare hidden i64 @other_value()\n", }, { name: "program-f001.ll", - source: + source: () => "declare i32 @main()\ndefine hidden i64 @other_value() {\nentry:\n ret i64 9\n}\n", }, ], @@ -4038,11 +4079,11 @@ test.skipIf(!nativeShardMergeAvailable)( const programShards = [ { name: "program-f000.ll", - source: "define i64 @foo() {\nentry:\n ret i64 1\n}\ndeclare i64 @bar()\n", + source: () => "define i64 @foo() {\nentry:\n ret i64 1\n}\ndeclare i64 @bar()\n", }, { name: "program-f001.ll", - source: "declare i64 @foo()\ndefine i64 @bar() {\nentry:\n ret i64 2\n}\n", + source: () => "declare i64 @foo()\ndefine i64 @bar() {\nentry:\n ret i64 2\n}\n", }, ]; try { @@ -4082,7 +4123,7 @@ test.skipIf(!nativeShardMergeAvailable)( // The canonical TU and ABI stay fixed, but the exact shard projection // changes. The merged-object tier must not reuse the previous layout. await build("bar", join(dir, "bar-commented.lib.a"), [ - { ...programShards[0]!, source: `; projection edit\n${programShards[0]!.source}` }, + { ...programShards[0]!, source: () => `; projection edit\n${programShards[0]!.source()}` }, programShards[1]!, ]); expect(await completeArtifacts(cacheRoot, "lib")).toHaveLength(3); @@ -4131,11 +4172,11 @@ test.skipIf( const programShards = [ { name: "program-f000.ll", - source: "define i64 @foo() {\nentry:\n ret i64 1\n}\ndeclare i64 @bar()\n", + source: () => "define i64 @foo() {\nentry:\n ret i64 1\n}\ndeclare i64 @bar()\n", }, { name: "program-f001.ll", - source: "declare i64 @foo()\ndefine i64 @bar() {\nentry:\n ret i64 2\n}\n", + source: () => "declare i64 @foo()\ndefine i64 @bar() {\nentry:\n ret i64 2\n}\n", }, ]; const ldWrapper = (generation: string): string => `#!/bin/sh @@ -4227,8 +4268,8 @@ test.skipIf( // If compilation reaches either shard, this build fails. Missing ld // (and objcopy on Linux) must select the valid canonical TU instead. programShards: [ - { name: "program-f000.ll", source: "not valid LLVM IR\n" }, - { name: "program-f001.ll", source: "also not valid LLVM IR\n" }, + { name: "program-f000.ll", source: () => "not valid LLVM IR\n" }, + { name: "program-f001.ll", source: () => "also not valid LLVM IR\n" }, ], programPublicSymbols: ["public_value"], outPath, @@ -4293,12 +4334,12 @@ test.skipIf( programShards: [ { name: "program-f000.ll", - source: + source: () => "define i64 @public_value() {\nentry:\n ret i64 7\n}\ndeclare i64 @other_value()\n", }, { name: "program-f001.ll", - source: + source: () => "declare i64 @public_value()\ndefine i64 @other_value() {\nentry:\n ret i64 9\n}\n", }, ], diff --git a/packages/compiler/src/backend/native-toolchain.ts b/packages/compiler/src/backend/native-toolchain.ts index 05f1f7122..6c024c6a5 100644 --- a/packages/compiler/src/backend/native-toolchain.ts +++ b/packages/compiler/src/backend/native-toolchain.ts @@ -26,7 +26,10 @@ export { export { CcCompileError, subprocessFailureDetail } from "./native/process.js"; export { compileLibArchive } from "./native/library.js"; export { localizeLibraryObjects } from "./native/object-merge.js"; -export { implicitDependencyProbeIncludes } from "./native/compiler-fingerprint.js"; +export { + implicitDependencyIncludeDirective, + implicitDependencyProbeIncludes, +} from "./native/compiler-fingerprint.js"; export { parseLinkTraceFiles, nativeLinkerDependencyPaths } from "./native/linker-fingerprint.js"; export { clearCcCaches, stageRuntimeObjects } from "./native/runtime-objects.js"; export { diff --git a/packages/compiler/src/backend/native-tools.ts b/packages/compiler/src/backend/native-tools.ts index 2f1b88ed0..9d6f2cf94 100644 --- a/packages/compiler/src/backend/native-tools.ts +++ b/packages/compiler/src/backend/native-tools.ts @@ -8,7 +8,12 @@ import { validateNativeCodegenVersion, type NativeCodegenOutputKind, } from "./native-codegen-core.js"; -import { nativeProfileHelperArgs, type NativeProfileOptions } from "./native-profile.js"; +import { + nativeProfileHelperArgs, + parseNativeProfileMatch, + type NativeProfileMatch, + type NativeProfileOptions, +} from "./native-profile.js"; export function runNativeTool( executable: string, @@ -48,10 +53,10 @@ export function emitNativeObject(options: { partitionCache?: string; /** Profile-guided optimization: instrumentation or an indexed profile. */ profile?: NativeProfileOptions; -}): string[] { +}): { outputs: string[]; profileMatch: NativeProfileMatch | null } { verifyNativeHelper(options); const outputs = nativePartitionPaths(options.outputPath, options.partitions ?? 1); - runNativeTool(options.executable, [ + const stdout = runNativeTool(options.executable, [ "emit", "--input", options.inputPath, @@ -74,7 +79,10 @@ export function emitNativeObject(options: { ...nativeProfileHelperArgs(options.profile), ]); for (const output of outputs) requireNativeArtifact(output); - return outputs; + return { + outputs, + profileMatch: options.profile?.use === undefined ? null : parseNativeProfileMatch(stdout), + }; } /** Merges raw or indexed IR profiles into one indexed profile. */ diff --git a/packages/compiler/src/backend/native/compiler-fingerprint.ts b/packages/compiler/src/backend/native/compiler-fingerprint.ts index ec427ce0a..6f92707f6 100644 --- a/packages/compiler/src/backend/native/compiler-fingerprint.ts +++ b/packages/compiler/src/backend/native/compiler-fingerprint.ts @@ -195,15 +195,25 @@ export async function implicitDependencyProbeIncludes(rtDir: string): Promise" || include === "" - ? " && (defined(__i386__) || defined(__x86_64__) || defined(_M_IX86) || defined(_M_X64))" - : include === "" - ? " && defined(_WIN32)" - : include === "" || include === "" || include === "" - ? " && (defined(__arm__) || defined(__aarch64__) || defined(_M_ARM) || defined(_M_ARM64))" - : ""; +/** Compiler-provided x86 headers: clang ships them for every target, so + * __has_include finds them on arm64 too, where including them is an + * #error. Covers the SSE/AVX intrinsic families (, + * , ...), , and . */ +function x86OnlyHeader(include: string): boolean { + return include === "" || /^<(x86|x86gpr|[a-z]*mm)intrin\.h>$/.test(include); +} + +/** One guarded line of the implicit-toolchain probe source: the header is + * included only when the compiler can find it and it belongs to the target + * being probed. */ +export function implicitDependencyIncludeDirective(include: string): string { + const targetCondition = x86OnlyHeader(include) + ? " && (defined(__i386__) || defined(__x86_64__) || defined(_M_IX86) || defined(_M_X64))" + : include === "" + ? " && defined(_WIN32)" + : include === "" || include === "" || include === "" + ? " && (defined(__arm__) || defined(__aarch64__) || defined(_M_ARM) || defined(_M_ARM64))" + : ""; return `#if __has_include(${include})${targetCondition}\n#include ${include}\n#endif\n`; } diff --git a/packages/compiler/src/backend/native/contracts.ts b/packages/compiler/src/backend/native/contracts.ts index 2945f5508..252518ffb 100644 --- a/packages/compiler/src/backend/native/contracts.ts +++ b/packages/compiler/src/backend/native/contracts.ts @@ -29,7 +29,7 @@ export interface CcOptions { windowsSubsystem?: WindowsSubsystem; /** Optional equivalent LLVM modules for dev compilation. Unsupported * targets or merge failures fall back to the canonical cPath TU. */ - programShards?: readonly { name: string; source: string }[]; + programShards?: readonly { name: string; source: () => string }[]; /** Canonical externally visible definitions retained while shard merging * demotes generated cross-shard linkage back to local symbols. */ programPublicSymbols?: readonly string[]; @@ -85,6 +85,9 @@ export interface CcOptions { * compares round-trips and fixed-blob inflation, never raw deflate * output. */ zlib?: boolean; + /** The program uses node:zlib's Brotli forms (with zlib): compiles + * scr_brotli.c and links the vendored brotli archive. */ + brotli?: boolean; /** The program uses node:assert (index.ts detects assert.* libCalls on * the IR): compiles scr_assert.c — the zlib gating precedent, so * assert-free binaries keep their exact size class. scr_regex.c calls @@ -123,14 +126,22 @@ export interface CcOptions { /** Own properties JavaScript adds to children: compiles scr_dyn_handle_props.c. */ handleProps?: boolean; declAdopt?: boolean; + typeOrigins?: boolean; /** for await over checked-dynamic values: compiles scr_dyn_for_await.c. */ dynForAwait?: boolean; /** events.once with options (events.onceSignal): compiles * scr_events_once_signal.c. */ eventsOnceSignal?: boolean; + /** util.MIMEType and util.MIMEParams: compiles scr_mime.c. */ + mime?: boolean; + /** util.debounce and util.throttle: compiles scr_util_timing.c. */ + utilTiming?: boolean; /** Buffer.prototype methods called on checked-dynamic values: * compiles scr_bytes_dyn.c. */ dynBuffer?: boolean; + /** createRequire's computed require and require.resolve: compiles + * scr_require.c. */ + computedRequire?: boolean; /** Builtin functions read as values: compiles scr_builtin_values.c. */ builtinValues?: boolean; /** The program constructs EventTarget/Event/CustomEvent values or @@ -147,6 +158,9 @@ export interface CcOptions { /** Calls into Node.js (@scriptc/node): compiles scr_node_island.c and * the event loop with SCR_NODE_ISLANDS (its helper slot). */ nodeIsland?: boolean; + /** Module namespace objects as checked-dynamic values: compiles + * scr_module_namespace.c. */ + moduleNamespace?: boolean; /** The program uses the process-events surface (signal/exit listeners, * stdin events, for-await over stdin — moduleUsesProcessEvents on the * IR): compiles scr_events.c into the binary. Event-free binaries keep @@ -223,6 +237,10 @@ export interface CcOptions { * scr_dgram_dyn.c over the dgram unit, so typed dgram binaries keep * their exact link line. */ dgramDyn?: boolean; + /** A TextEncoder or TextDecoder record converts to or from an untyped + * value: compiles scr_text_codec_dyn.c, so typed codec binaries keep + * their exact link line. */ + textCodecDyn?: boolean; /** The program uses fs.watch (moduleUsesFsWatch on the IR): compiles * scr_watch.c into the binary — the net gating precedent, so watch-free @@ -268,6 +286,8 @@ export interface CcOptions { /** Ed25519 with pki: compiles scr_ed25519.c (the vendored libsodium * Ed25519). */ ed25519?: boolean; + /** X509Certificate fields: compiles scr_x509.c. */ + x509?: boolean; /** crypto.subtle importKey/sign/verify/digest: compiles scr_webcrypto.c. */ webcrypto?: boolean; /** perf_hooks event-loop metrics: compiles scr_perf_loop.c. */ @@ -297,7 +317,7 @@ export interface LibArchiveOptions { * `programSource` remains the canonical TU/cache identity; these stable * shards compile independently and are relocatably merged into one program * object before archive assembly. */ - programShards?: readonly { name: string; source: string }[]; + programShards?: readonly { name: string; source: () => string }[]; /** Canonical externally visible definitions retained while the shard merge * demotes generated cross-shard linkage back to local symbols. */ programPublicSymbols?: readonly string[]; @@ -353,6 +373,8 @@ export interface LibArchiveOptions { searchParams?: boolean; emitter?: boolean; zlib?: boolean; + /** X509Certificate fields: scr_x509.c. */ + x509?: boolean; copying?: boolean; textDecoderLegacy?: boolean; } diff --git a/packages/compiler/src/backend/native/executable.ts b/packages/compiler/src/backend/native/executable.ts index a60643508..b5072bf25 100644 --- a/packages/compiler/src/backend/native/executable.ts +++ b/packages/compiler/src/backend/native/executable.ts @@ -43,6 +43,7 @@ import { cacheTargetIdentity, isZigDriver, isMuslTarget, + type CcDriver, } from "./driver.js"; import { resolveProgramShardMergeIdentity, @@ -82,15 +83,18 @@ import { currentVendorCacheBuildIdentity, vendorBuildCacheRoot, tlsArchivePath, + brotliArchivePath, lreObjectPaths, zlibObjectPaths, stageVendorInputs, ensureTlsArchive, + ensureBrotliArchive, ensureLreObjects, ensureZlibObjects, vendorEngineDir, vendorZlibDir, vendorTlsDir, + vendorBrotliDir, } from "./vendor.js"; import { execFileAsync, subprocessFailureDetail, CcCompileError } from "./process.js"; import { implicitLinkerFingerprint } from "./linker-fingerprint.js"; @@ -119,6 +123,7 @@ interface RuntimeUnitContext { lreObjects: readonly string[]; zlibObjects: readonly string[]; tlsArchive: string | null; + brotliArchive: string | null; } /** An optional runtime input: runtime sources compiled when `when` holds, @@ -186,10 +191,14 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [ unit((u) => u.opts.dynBuffer === true && u.opts.dynInvoke, "scr_bytes_dyn.c"), // Builtin functions read as values in JavaScript dependencies. unit((u) => u.opts.builtinValues === true, "scr_builtin_values.c"), + // createRequire's computed require and require.resolve. + unit((u) => u.opts.computedRequire === true, "scr_require.c"), unit((u) => u.opts.dc, "scr_dc.c"), unit((u) => u.opts.dynTier, "scr_js_value.c", "scr_js_object.c"), unit((u) => u.opts.eventTarget || u.nativeFetch, "scr_event_target.c"), unit((u) => u.opts.wasm, "scr_wasm.c"), + // Module namespace objects as checked-dynamic values. + unit((u) => u.opts.moduleNamespace === true, "scr_module_namespace.c"), unit( (u) => u.opts.dynAsync || @@ -222,6 +231,18 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [ : [] : [], ), + // Brotli (scr_brotli.c) plugs into the zlib engine and links the + // vendored brotli archive. + unitArgs((u) => + u.brotliArchive + ? [ + "-I", + join(vendorBrotliDir(), "include"), + u.rt(join(u.rtDir, "scr_brotli.c")), + u.brotliArchive, + ] + : [], + ), unit((u) => u.opts.events, "scr_events.c", "scr_readline.c"), // readline's terminal mode: rl.terminal sets the regex unit it // measures with. @@ -230,6 +251,8 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [ // events.once with options registers through the emitter and // event-target units. unit((u) => u.opts.eventsOnceSignal === true, "scr_events_once_signal.c"), + unit((u) => u.opts.mime === true, "scr_mime.c"), + unit((u) => u.opts.utilTiming === true, "scr_util_timing.c"), // The checked-dynamic HANDLE support unit (listener gate + runtime // adapter closures): every referencing unit is one of the emitter or // net families (http implies net), so handle-free binaries keep @@ -281,6 +304,7 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [ unit((u) => u.opts.handleProps === true, "scr_dyn_handle_props.c"), // The declaration-typed boundary's adoption. unit((u) => u.opts.declAdopt === true, "scr_decl_adopt.c"), + unit((u) => u.opts.typeOrigins === true, "scr_type_origin.c"), // for await over checked-dynamic values. unit((u) => u.opts.dynForAwait === true, "scr_dyn_for_await.c"), // for await over checked-dynamic values that may hold native streams. @@ -302,12 +326,16 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [ unit((u) => u.opts.dgram, "scr_dgram.c"), unit((u) => u.opts.dgram && u.opts.dnsPromises, "scr_dns_promises.c"), unit((u) => u.opts.dgram && u.opts.dgramDyn, "scr_dgram_dyn.c"), + unit((u) => u.opts.textCodecDyn, "scr_text_codec_dyn.c"), unit((u) => u.opts.watch, "scr_watch.c"), unit((u) => u.opts.foreignFfi, "scr_ffi_queue.c"), unit((u) => u.opts.nodeTest, "scr_test.c"), unit((u) => u.opts.webcrypto, "scr_webcrypto.c"), unit((u) => u.opts.webcrypto === true && u.opts.pki === true, "scr_webcrypto_pki.c"), unit((u) => u.opts.pki === true && u.opts.ed25519 === true, "scr_ed25519.c"), + unit((u) => u.opts.x509 === true, "scr_x509.c"), + // The TLS socket certificate accessors read scr_tls.c and build with scr_x509.c. + unit((u) => u.tls && u.opts.x509 === true, "scr_tls_x509.c"), // The crypto global's object extends scr_async_dyn.c's. unit((u) => u.opts.webcrypto && u.opts.dynAsync, "scr_webcrypto_object.c"), // The CA-store unit rides its own gate OR the tls one: scr_tls.c @@ -399,6 +427,7 @@ export async function compileCInternal( const dh = opts.dh ?? false; const pki = opts.pki ?? false; const mbedtls = tls || cipher || ecdh || dh || pki; + const brotli = (opts.zlib ?? false) && opts.brotli === true; const driver = resolveCc(); const darwinDebugSymbols = needsDarwinDebugSymbols( targetPlatform(driver), @@ -414,7 +443,7 @@ export async function compileCInternal( ? ["-gline-tables-only", ...(opts.cPath.endsWith(".ll") ? [] : ["-gno-column-info"])] : []; const { programShards, programPublicSymbols, programShardMergeIdentity } = - await selectProgramShards(driver, opts, optimization === "dev" && !sanitize); + await selectProgramShards(driver, opts, optimization === "dev"); // Mobile triples produce library archives, never standalone executables: // the executable-lane runtime (event loop, sockets, child processes) is // not verified on those device classes. compile() reports the SC3002 @@ -638,6 +667,9 @@ export async function compileCInternal( let tlsArchive = mbedtls ? tlsArchivePath(sanitize, driver, vendorBuildIdentity, vendorCacheRoot) : null; + let brotliArchive = brotli + ? brotliArchivePath(sanitize, driver, vendorBuildIdentity, vendorCacheRoot) + : null; // ours would collide; see scr_regex.c) — the standalone objects are for // static builds only. @@ -675,6 +707,23 @@ export async function compileCInternal( : await stageVendorInputs(materialize, join(stageRoot, "tls")); tlsArchive = paths[0]!; } + if (brotli) { + const cachedArchive = brotliArchivePath( + sanitize, + driver, + vendorBuildIdentity, + materializeCacheRoot, + ); + protectCachedArtifact(cacheWarmPaths, cachedArchive); + const materialize = async (): Promise => [ + await ensureBrotliArchive(sanitize, driver, vendorBuildIdentity, materializeCacheRoot), + ]; + const paths = + stageRoot === undefined + ? await materialize() + : await stageVendorInputs(materialize, join(stageRoot, "brotli")); + brotliArchive = paths[0]!; + } if (regex) { for (const object of lreObjectPaths( sanitize, @@ -761,6 +810,7 @@ export async function compileCInternal( lreObjects, zlibObjects, tlsArchive, + brotliArchive, }), // A .ll program TU (the LLVM backend) deliberately carries no target // triple (byte-stable output; clang supplies the host/SCRIPTC_TARGET @@ -784,6 +834,7 @@ export async function compileCInternal( // generated/native input that references inflate symbols. Cross // and targetless Zig builds use vendored zlib objects in the input section above. ...(((opts.zlib ?? false) || nativeFetch) && !isZigDriver(driver) ? ["-lz"] : []), + ...unwinderLinkArgs(driver), // glibc keeps libm separate from libc. This must trail the generated // program and every native FFI input because GNU ld resolves archives // from left to right. @@ -846,9 +897,15 @@ export async function compileCInternal( }; const buildExecutable = async ( runtimeInput: (path: string) => string, - build: { programPath?: string; outPath?: string; compilerVisibleSource?: string } = {}, + build: { + programPath?: string; + outPath?: string; + compilerVisibleSource?: string; + /** programPath is an object already (the merged program shards). */ + programObject?: boolean; + } = {}, ): Promise => { - if (!separateSanitizedProgram) { + if (!separateSanitizedProgram || build.programObject === true) { await runClang(buildArgs(runtimeInput, build)); return; } @@ -1031,6 +1088,7 @@ export async function compileCInternal( ...(mbedtls && targetPlatform(driver) === "win32" ? ["-lbcrypt"] : []), ...(tlsCa && targetPlatform(driver) === "win32" ? ["-lcrypt32"] : []), ...(((opts.zlib ?? false) || nativeFetch) && !isZigDriver(driver) ? ["-lz"] : []), + ...unwinderLinkArgs(driver), ...driver.linkArgs, ...executableLinkFlags, ]; @@ -1043,6 +1101,7 @@ export async function compileCInternal( ...(mbedtls && targetPlatform(driver) === "win32" ? ["-lbcrypt"] : []), ...(tlsCa && targetPlatform(driver) === "win32" ? ["-lcrypt32"] : []), ...(((opts.zlib ?? false) || nativeFetch) && !isZigDriver(driver) ? ["-lz"] : []), + ...unwinderLinkArgs(driver), ...driver.linkArgs, ...executableLinkFlags, ]; @@ -1425,7 +1484,7 @@ export async function compileCInternal( .update("\0") .update(shard.name) .update("\0") - .update(shard.source) + .update(shard.source()) .digest("hex"); return { ...shard, @@ -1439,7 +1498,7 @@ export async function compileCInternal( for (let i = 0; i < entries.length; i += shardWidth) { await Promise.all( entries.slice(i, i + shardWidth).map(async (entry) => { - await writeFile(entry.sourcePath, entry.source); + await writeFile(entry.sourcePath, entry.source()); if (await copyValidCachedFile(entry.cachePath, entry.staged)) return; entry.missed = true; await runClang([ @@ -1462,6 +1521,8 @@ export async function compileCInternal( [], programPublicSymbols, `${stem}.program`, + undefined, + { inProcessElf: true }, ); const publishable = entries.filter((entry) => entry.missed); if (publishable.length > 0) { @@ -1526,7 +1587,7 @@ export async function compileCInternal( (p) => objects?.get(p) ?? p, shardedProgramObject === null ? { programPath, outPath: privateOut, compilerVisibleSource: opts.cPath } - : { programPath: shardedProgramObject, outPath: privateOut }, + : { programPath: shardedProgramObject, outPath: privateOut, programObject: true }, ); if (darwinDebugSymbols) { // clang invokes dsymutil when compiling a source TU while linking. @@ -1652,3 +1713,11 @@ export async function compileC(opts: CcOptions): Promise { await rm(`${opts.outPath}.dSYM`, { recursive: true, force: true }); } } + +/** Linux runtimes raise through the native unwinder (scr_unwind_raise): + * Zig's libunwind, or the toolchain's libgcc_eh. Both are static archives, + * so programs load no shared library for them. */ +function unwinderLinkArgs(driver: CcDriver): string[] { + if (targetPlatform(driver) !== "linux") return []; + return isZigDriver(driver) ? ["-lunwind"] : ["-lgcc_eh"]; +} diff --git a/packages/compiler/src/backend/native/library.ts b/packages/compiler/src/backend/native/library.ts index f04e9926c..06b4ce1df 100644 --- a/packages/compiler/src/backend/native/library.ts +++ b/packages/compiler/src/backend/native/library.ts @@ -73,7 +73,7 @@ import { CacheInputsChangedError } from "./session.js"; * async_free refusal already guarantees nothing references them — and * scr_library.c (sink, arena, reset registry, library funnel) joins. The gated * units a library may reach are the pure-data ones (regex + the vendored matcher, assert, - * inspect, symbol, searchParams, emitter+dyn_handle, zlib); every + * inspect, symbol, searchParams, emitter+dyn_handle, zlib, x509); every * loop-hooked or ambient unit was refused at SC4005 before emission. * External-symbol contract: undefined references only to the target's C/math * runtime and system APIs. Windows embedders additionally link advapi32, @@ -115,6 +115,7 @@ export async function compileLibArchive(opts: LibArchiveOptions): Promise ...(opts.dynInvoke && opts.dynBuffer ? ["scr_bytes_dyn.c"] : []), ...(opts.dynTier === true ? ["scr_js_value.c", "scr_js_object.c"] : []), ...(opts.zlib ? ["scr_zlib.c"] : []), + ...(opts.x509 ? ["scr_x509.c"] : []), ...(opts.copying ? ["scr_copying.c"] : []), ]; const cflags = [ @@ -484,7 +485,7 @@ export async function compileLibArchive(opts: LibArchiveOptions): Promise .update("\0") .update(shard.name) .update("\0") - .update(shard.source) + .update(shard.source()) .digest("hex"); cachePath = join(persistentCache.root, "program-shard", key); } @@ -494,7 +495,7 @@ export async function compileLibArchive(opts: LibArchiveOptions): Promise for (let i = 0; i < shardEntries.length; i += shardWidth) { await Promise.all( shardEntries.slice(i, i + shardWidth).map(async (entry) => { - await writeFile(entry.sourcePath, entry.source); + await writeFile(entry.sourcePath, entry.source()); if ( entry.cachePath !== null && (await copyValidCachedFile(entry.cachePath, entry.staged)) @@ -516,6 +517,8 @@ export async function compileLibArchive(opts: LibArchiveOptions): Promise [], programPublicSymbols!, `${stem}.program`, + undefined, + { inProcessElf: true }, ); // Keep the canonical archive member spelling. The fact that native // compilation used shards is an implementation detail; consumers and diff --git a/packages/compiler/src/backend/native/object-merge.ts b/packages/compiler/src/backend/native/object-merge.ts index 39a14785e..43f15fef8 100644 --- a/packages/compiler/src/backend/native/object-merge.ts +++ b/packages/compiler/src/backend/native/object-merge.ts @@ -5,6 +5,17 @@ import { localizeElfObject, mergeAndLocalizeCoffObjects } from "../object-locali import { type CcDriver, targetPlatform } from "./driver.js"; import { execFileAsync, subprocessFailureDetail } from "./process.js"; +/* Demotes in process (localizeElfObject), rewriting the object in place. */ +async function localizeInProcess(object: string, keepSymbols: readonly string[]): Promise { + try { + await writeFile(object, localizeElfObject(await readFile(object), new Set(keepSymbols))); + } catch (err) { + throw new Error( + `ELF symbol localization failed while localizing the library archive's runtime symbols (abi.localize_runtime).\n\n${(err as Error).message}`, + ); + } +} + /* Multi-instance library mode's localization step: combine the program * object with exactly the runtime/vendor members it reaches into ONE * relocatable object, then demote every scriptc external definition except @@ -46,7 +57,11 @@ import { execFileAsync, subprocessFailureDetail } from "./process.js"; * multi-archive link from discarding one archive's copies), * then binutils objcopy --keep-global-symbols localizes every * other DEFINED global (objcopy leaves undefined symbols - * global by its own rule). The exact historical recipe. + * global by its own rule). The exact historical recipe. With + * `inProcessElf` (program-shard merges), localizeElfObject + * demotes instead of objcopy: a sanitized dev program merges + * into an object with hundreds of thousands of sections, which + * objcopy takes minutes over and localizeElfObject seconds. * ELF, cross triples from any host — `zig cc -target -r` * merges (zig is already the cross driver's hard requirement), * then localizeElfObject demotes in process and resolves @@ -68,6 +83,7 @@ export async function localizeLibraryObjects( keepSymbols: readonly string[], stem: string, platform = targetPlatform(driver), + options: { inProcessElf?: boolean } = {}, ): Promise { const combined = join(buildDir, `${stem}.localized.o`); const staging = join(buildDir, `${stem}.localize-staging.a`); @@ -79,7 +95,7 @@ export async function localizeLibraryObjects( const stderr = subprocessFailureDetail(err); throw new Error( `${argv[0]} failed while localizing the library archive's runtime symbols (abi.localize_runtime).\n` + - `Runtime symbol localization needs the ${platform === "darwin" ? "host toolchain's ld" : driver.target === null ? "host toolchain's ld and objcopy" : "cross driver's relocatable link"} beside the C compiler.\n\n${stderr}`, + `Runtime symbol localization needs the ${platform === "darwin" || (driver.target === null && options.inProcessElf === true) ? "host toolchain's ld" : driver.target === null ? "host toolchain's ld and objcopy" : "cross driver's relocatable link"} beside the C compiler.\n\n${stderr}`, ); } }; @@ -122,7 +138,6 @@ export async function localizeLibraryObjects( keepFile, ]); } else if (platform === "linux" && driver.target === null) { - await writeFile(keepFile, keepSymbols.map((s) => `${s}\n`).join("")); await run([ "ld", "-r", @@ -132,7 +147,12 @@ export async function localizeLibraryObjects( "-o", combined, ]); - await run(["objcopy", `--keep-global-symbols=${keepFile}`, combined]); + if (options.inProcessElf === true) { + await localizeInProcess(combined, keepSymbols); + } else { + await writeFile(keepFile, keepSymbols.map((s) => `${s}\n`).join("")); + await run(["objcopy", `--keep-global-symbols=${keepFile}`, combined]); + } } else if (platform === "linux") { // Cross ELF: the cross driver's own lld performs the relocatable merge // (with the staging archive's member semantics); demotion and section- @@ -152,13 +172,7 @@ export async function localizeLibraryObjects( "-o", combined, ]); - try { - await writeFile(combined, localizeElfObject(await readFile(combined), new Set(keepSymbols))); - } catch (err) { - throw new Error( - `ELF symbol localization failed while localizing the library archive's runtime symbols (abi.localize_runtime).\n\n${(err as Error).message}`, - ); - } + await localizeInProcess(combined, keepSymbols); } else { throw new InternalCompilerError( `runtime symbol localization (abi.localize_runtime) has no ${platform} arm; compileLibrary admits darwin, linux, and win32 builds only`, diff --git a/packages/compiler/src/backend/native/program-shards.ts b/packages/compiler/src/backend/native/program-shards.ts index 978be3a97..2948299b2 100644 --- a/packages/compiler/src/backend/native/program-shards.ts +++ b/packages/compiler/src/backend/native/program-shards.ts @@ -7,7 +7,7 @@ import { rememberFingerprintDependencies } from "./dependency-files.js"; export function updateProgramShardCacheIdentity( hash: ReturnType, - shards: readonly { name: string; source: string }[] | undefined, + shards: readonly { name: string; source: () => string }[] | undefined, publicSymbols: readonly string[] | undefined, mergeIdentity: string | undefined, ): void { @@ -17,7 +17,7 @@ export function updateProgramShardCacheIdentity( } else { hash.update("\0"); for (const shard of shards) { - hash.update(shard.name).update("\0").update(shard.source).update("\0"); + hash.update(shard.name).update("\0").update(shard.source()).update("\0"); } } hash.update("\0program-public-symbols\0"); @@ -52,13 +52,13 @@ export async function resolveProgramShardMergeIdentity(driver: CcDriver): Promis } if (platform === "linux") { if (driver.target === null) { - const [ld, objcopy] = await Promise.all([resolvedTool("ld"), resolvedTool("objcopy")]); - return ld === null || objcopy === null + // ld -r merges; localizeElfObject demotes in process. + const ld = await resolvedTool("ld"); + return ld === null ? null - : rememberFingerprintDependencies( - `program-shard-merge-linux-v1\0${ld.cacheIdentity}\0${objcopy.cacheIdentity}`, - [ld.canonicalPath, objcopy.canonicalPath], - ); + : rememberFingerprintDependencies(`program-shard-merge-linux-v2\0${ld.cacheIdentity}`, [ + ld.canonicalPath, + ]); } const arch = driver.target.split("-", 1)[0]; if (arch !== "x86_64" && arch !== "aarch64") return null; @@ -78,7 +78,7 @@ export async function resolveProgramShardMergeIdentity(driver: CcDriver): Promis } /** Library builds admit shards in both optimization modes; executable builds - * only admit unsanitized dev LLVM. Both use the same validated names, required + * admit dev LLVM, sanitized or not. Both use the same validated names, required * public-symbol projection, target support, and canonical-TU fallback. */ export async function selectProgramShards( driver: CcDriver, diff --git a/packages/compiler/src/backend/native/runtime-inputs.ts b/packages/compiler/src/backend/native/runtime-inputs.ts index 6fb51fd32..891fa9312 100644 --- a/packages/compiler/src/backend/native/runtime-inputs.ts +++ b/packages/compiler/src/backend/native/runtime-inputs.ts @@ -3,7 +3,7 @@ import { createRequire } from "node:module"; import { readdir, readFile, realpath, stat } from "node:fs/promises"; import { basename, dirname, extname, join, relative, resolve } from "node:path"; import { fileURLToPath } from "node:url"; -import { MBEDTLS_VERSION, QJS_COMMIT, ZLIB_VERSION } from "../vendor-inputs.js"; +import { BROTLI_VERSION, MBEDTLS_VERSION, QJS_COMMIT, ZLIB_VERSION } from "../vendor-inputs.js"; import { stableTestMemo } from "./session.js"; const implementationPath = fileURLToPath(import.meta.url); @@ -53,6 +53,7 @@ export const EXECUTABLE_RUNTIME_SOURCES = [ "scr_object.c", "scr_union.c", "scr_exception.c", + "scr_unwind.c", "scr_error.c", "scr_console.c", "scr_lib.c", @@ -73,6 +74,8 @@ export const EXECUTABLE_RUNTIME_SOURCES = [ "scr_slots.c", "scr_alloc.c", "scr_cipher_info.c", + "scr_ieee754.c", + "scr_glibc_math.c", ] as const; export function runtimeSrcDir(): string { @@ -134,6 +137,7 @@ async function runtimeFingerprintFresh(rtDir: string): Promise { .update(QJS_COMMIT) .update(MBEDTLS_VERSION) .update(ZLIB_VERSION) + .update(BROTLI_VERSION) .update("\0backend-recipe\0"); for (const path of NATIVE_RECIPE_IMPLEMENTATION_PATHS) { h.update(basename(path)) @@ -164,6 +168,7 @@ async function runtimeFingerprintInputGroups( { label: "quickjs-ng", dir: join(rtDir, "..", "vendor", "quickjs-ng") }, { label: "mbedtls", dir: join(rtDir, "..", "vendor", "mbedtls") }, { label: "zlib", dir: join(rtDir, "..", "vendor", "zlib") }, + { label: "brotli", dir: join(rtDir, "..", "vendor", "brotli", "c") }, { label: "curl", dir: join(rtDir, "..", "vendor", "curl") }, ].map(async (group) => { const names = (await nativeSourceFiles(group.dir, true)) diff --git a/packages/compiler/src/backend/native/vendor.ts b/packages/compiler/src/backend/native/vendor.ts index cf7b3a68d..b6084d5c4 100644 --- a/packages/compiler/src/backend/native/vendor.ts +++ b/packages/compiler/src/backend/native/vendor.ts @@ -6,6 +6,9 @@ import { resolvedToolIdentity } from "./tool-identity.js"; export const { vendorEngineDir, vendorTlsDir, + vendorBrotliDir, + brotliArchivePath, + ensureBrotliArchive, vendorBuildCacheRoot, vendorCacheTargetFlavor, vendorCacheBuildIdentity, diff --git a/packages/compiler/src/backend/object-localize.test.ts b/packages/compiler/src/backend/object-localize.test.ts index 9e4e30285..1a1e9d4bd 100644 --- a/packages/compiler/src/backend/object-localize.test.ts +++ b/packages/compiler/src/backend/object-localize.test.ts @@ -28,7 +28,10 @@ const SHF_EXECINSTR = 0x4n; const SHF_GROUP = 0x200n; const STB_GLOBAL = 1; const STB_WEAK = 2; +const SHT_SYMTAB_SHNDX = 18; +const SHN_LORESERVE = 0xff00; const SHN_COMMON = 0xfff2; +const SHN_XINDEX = 0xffff; interface ElfSymbolSpec { name: string; @@ -53,7 +56,11 @@ function buildStrtab(names: string[]): { data: Uint8Array; offsets: Map s.name).filter((n) => n !== "")); const symData = new Uint8Array((symbols.length + 1) * 24); const symView = new DataView(symData.buffer); + const shndxData = new Uint8Array((symbols.length + 1) * 4); + let usesShndx = false; symbols.forEach((sym, i) => { const at = (i + 1) * 24; symView.setUint32(at, sym.name === "" ? 0 : strtab.offsets.get(sym.name)!, true); symView.setUint8(at + 4, (sym.binding << 4) | 0); - symView.setUint16(at + 6, sym.shndx, true); + if (sym.shndx >= SHN_LORESERVE && sym.shndx !== SHN_COMMON) { + symView.setUint16(at + 6, SHN_XINDEX, true); + new DataView(shndxData.buffer).setUint32((i + 1) * 4, sym.shndx, true); + usesShndx = true; + } else { + symView.setUint16(at + 6, sym.shndx, true); + } symView.setBigUint64(at + 8, sym.value ?? 0n, true); }); const all: ElfSectionSpec[] = [ @@ -82,9 +97,22 @@ function buildElf( }, { name: ".strtab", type: SHT_STRTAB, data: strtab.data }, { name: ".shstrtab", type: SHT_STRTAB }, + ...(usesShndx + ? [ + { + name: ".symtab_shndx", + type: SHT_SYMTAB_SHNDX, + data: shndxData, + link: sections.length + 1, + entsize: 4n, + }, + ] + : []), ]; + const shstrndx = sections.length + 3; + const extended = all.length >= SHN_LORESERVE; const shstr = buildStrtab(all.map((s) => s.name).filter((n) => n !== "")); - all[all.length - 1]!.data = shstr.data; + all[shstrndx]!.data = shstr.data; let offset = 64; const offsets = all.map((s) => { const data = s.data ?? new Uint8Array(0); @@ -106,8 +134,8 @@ function buildElf( view.setBigUint64(40, BigInt(shoff), true); view.setUint16(52, 64, true); view.setUint16(58, 64, true); - view.setUint16(60, all.length, true); - view.setUint16(62, all.length - 1, true); + view.setUint16(60, extended ? 0 : all.length, true); + view.setUint16(62, shstrndx >= SHN_LORESERVE ? SHN_XINDEX : shstrndx, true); all.forEach((s, i) => { const at = shoff + i * 64; const data = s.data ?? new Uint8Array(0); @@ -116,8 +144,8 @@ function buildElf( view.setUint32(at + 4, s.type, true); view.setBigUint64(at + 8, s.flags ?? 0n, true); view.setBigUint64(at + 24, BigInt(s.type === 0 ? 0 : offsets[i]!), true); - view.setBigUint64(at + 32, BigInt(data.length), true); - view.setUint32(at + 40, s.link ?? 0, true); + view.setBigUint64(at + 32, BigInt(i === 0 && extended ? all.length : data.length), true); + view.setUint32(at + 40, i === 0 && shstrndx >= SHN_LORESERVE ? shstrndx : (s.link ?? 0), true); view.setUint32(at + 44, s.info ?? 0, true); view.setBigUint64(at + 48, s.addralign ?? 1n, true); view.setBigUint64(at + 56, s.entsize ?? 0n, true); @@ -142,8 +170,10 @@ interface ReadElf { function readElf(bytes: Uint8Array): ReadElf { const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); const shoff = Number(view.getBigUint64(40, true)); - const shnum = view.getUint16(60, true); - const shstrndx = view.getUint16(62, true); + let shnum = view.getUint16(60, true); + let shstrndx = view.getUint16(62, true); + if (shnum === 0) shnum = Number(view.getBigUint64(shoff + 32, true)); + if (shstrndx === SHN_XINDEX) shstrndx = view.getUint32(shoff + 40, true); const raw = [] as { nameOff: number; type: number; @@ -182,12 +212,16 @@ function readElf(bytes: Uint8Array): ReadElf { const symtabIdx = raw.findIndex((s) => s.type === SHT_SYMTAB); const symtab = raw[symtabIdx]!; const strtab = raw[symtab.link]!; + const shndxTable = raw.find((s) => s.type === SHT_SYMTAB_SHNDX && s.link === symtabIdx); const symbols = [] as ReadElf["symbols"]; for (let at = symtab.offset; at < symtab.offset + symtab.size; at += 24) { + let shndx = view.getUint16(at + 6, true); + if (shndx === SHN_XINDEX) + shndx = view.getUint32(shndxTable!.offset + ((at - symtab.offset) / 24) * 4, true); symbols.push({ name: name(strtab, view.getUint32(at, true)), binding: view.getUint8(at + 4) >> 4, - shndx: view.getUint16(at + 6, true), + shndx, }); } return { sections, symbols, symtabInfo: symtab.info }; @@ -241,6 +275,65 @@ describe("localizeElfObject", () => { expect(info & 0xffffffffn).toBe(2n); }); + test("keeps extended section numbering when the object has SHN_LORESERVE or more sections", () => { + // A sanitized dev program merges into an object with more sections than + // e_shnum and st_shndx can index: section 0 carries the count and the + // section-name table's index, and .symtab_shndx the symbols' indices. + const count = SHN_LORESERVE + 400; + const texts: ElfSectionSpec[] = Array.from({ length: count }, (_, i) => ({ + name: `.text.f${i}`, + type: SHT_PROGBITS, + flags: SHF_ALLOC | SHF_EXECINSTR, + data: new Uint8Array(4), + })); + const rela = new Uint8Array(24); + new DataView(rela.buffer).setBigUint64(8, (2n << 32n) | 2n, true); // sym 2 (high) + const object = buildElf( + [ + ...texts, + { + name: ".rela.text.last", + type: SHT_RELA, + data: rela, + link: count + 2, + info: count, + entsize: 24n, + }, + ], + [ + { name: "low", binding: STB_GLOBAL, shndx: 1 }, + { name: "high", binding: STB_GLOBAL, shndx: count }, + { name: "main", binding: STB_GLOBAL, shndx: count - 1 }, + { name: "libc_ref", binding: STB_GLOBAL, shndx: 0 }, + { name: "shared_common", binding: STB_GLOBAL, shndx: SHN_COMMON, value: 8n }, + ], + 0, + ); + const output = localizeElfObject(object, new Set(["main"])); + const header = new DataView(output.buffer, output.byteOffset, output.byteLength); + expect(header.getUint16(60, true)).toBe(0); + expect(header.getUint16(62, true)).toBe(SHN_XINDEX); + const localized = readElf(output); + expect(localized.sections.length).toBe(count + 6); + expect(localized.sections.filter((s) => s.type === SHT_SYMTAB_SHNDX)).toHaveLength(1); + const byName = new Map(localized.symbols.map((s) => [s.name, s])); + expect(byName.get("low")!.binding).toBe(0); + expect(byName.get("high")!.binding).toBe(0); + expect(localized.sections[byName.get("high")!.shndx]!.name).toBe(`.text.f${count - 1}`); + expect(byName.get("main")!.binding).toBe(STB_GLOBAL); + expect(localized.sections[byName.get("main")!.shndx]!.name).toBe(`.text.f${count - 2}`); + expect(byName.get("libc_ref")!.shndx).toBe(0); + expect(byName.get("shared_common")!.shndx).toBe(SHN_COMMON); + const relaOut = localized.sections.find((s) => s.name === ".rela.text.last")!; + expect(localized.sections[relaOut.info]!.name).toBe(`.text.f${count - 1}`); + const info = new DataView(relaOut.data.buffer, relaOut.data.byteOffset).getBigUint64(8, true); + expect(localized.symbols[Number(info >> 32n)]!.name).toBe("high"); + // The output reads back as the same object. + expect(readElf(localizeElfObject(output, new Set(["main"]))).symbols).toEqual( + localized.symbols, + ); + }); + test("resolves section groups: SHT_GROUP dropped, SHF_GROUP cleared, members and indices intact", () => { const group = new Uint8Array(8); const groupView = new DataView(group.buffer); diff --git a/packages/compiler/src/backend/object-localize.ts b/packages/compiler/src/backend/object-localize.ts index 302027484..fb91ea85b 100644 --- a/packages/compiler/src/backend/object-localize.ts +++ b/packages/compiler/src/backend/object-localize.ts @@ -84,9 +84,13 @@ interface ElfSymbol { name: string; info: number; other: number; - /** Resolved section index (SHT_SYMTAB_SHNDX applied), or a reserved - * SHN_* value below/at SHN_LORESERVE semantics. */ + /** Section index (SHT_SYMTAB_SHNDX applied), or the reserved SHN_* value + * when `reserved`. */ shndx: number; + /** st_shndx holds a reserved value (SHN_ABS, SHN_COMMON, ...), not a + * section index. An index from SHT_SYMTAB_SHNDX can itself be at or above + * SHN_LORESERVE. */ + reserved: boolean; } /** Demote every defined global/weak symbol not named in `keep` to a local @@ -143,8 +147,8 @@ export function localizeElfObject(object: Uint8Array, keep: ReadonlySet) if (strtab === undefined) fail("ELF symbol table names an invalid string table"); // SHT_SYMTAB_SHNDX (only present when section indices overflow st_shndx): - // resolve the real indices up front and drop the section — the rebuilt - // object's section count only shrinks, so inline indices always fit. + // resolve the real indices up front and drop the section. The rebuilt + // object writes a new one when its own section count still overflows. const shndxSection = sections.find( (s) => s.type === SHT_SYMTAB_SHNDX && sections[s.link] === symtab, ); @@ -170,9 +174,11 @@ export function localizeElfObject(object: Uint8Array, keep: ReadonlySet) for (let i = 0; i < symCount; i++) { const at = i * 24; let shndx: number = symView.getUint16(at + 6, true); + let reserved = shndx >= SHN_LORESERVE; if (shndx === SHN_XINDEX) { if (shndxView === null) fail("ELF symbol uses SHN_XINDEX without a SHT_SYMTAB_SHNDX table"); shndx = shndxView.getUint32(i * 4, true); + reserved = false; } symbols.push({ nameOffset: symView.getUint32(at, true), @@ -180,6 +186,7 @@ export function localizeElfObject(object: Uint8Array, keep: ReadonlySet) info: symView.getUint8(at + 4), other: symView.getUint8(at + 5), shndx, + reserved, }); } @@ -212,11 +219,11 @@ export function localizeElfObject(object: Uint8Array, keep: ReadonlySet) const symbolKept: boolean[] = []; for (let i = 0; i < symCount; i++) { const sym = symbols[i]!; - const regular = sym.shndx > SHN_UNDEF && sym.shndx < SHN_LORESERVE; + const regular = !sym.reserved && sym.shndx !== SHN_UNDEF; symbolKept.push(!(regular && sectionKept[sym.shndx] !== true)); if (i === 0) continue; const binding = sym.info >> 4; - const defined = sym.shndx !== SHN_UNDEF && sym.shndx !== SHN_COMMON; + const defined = sym.shndx !== SHN_UNDEF && !(sym.reserved && sym.shndx === SHN_COMMON); if (binding !== STB_LOCAL && defined && !keep.has(sym.name)) { sym.info = (STB_LOCAL << 4) | (sym.info & 0xf); } @@ -236,6 +243,16 @@ export function localizeElfObject(object: Uint8Array, keep: ReadonlySet) symbolMap[oldIndex] = newIndex; }); + // Extended section numbering: with SHN_LORESERVE or more sections, an + // index from SHN_LORESERVE up does not fit st_shndx, e_shnum or + // e_shstrndx. Such a symbol's st_shndx is SHN_XINDEX and its index goes in + // a SHT_SYMTAB_SHNDX section (one entry per symbol, appended after the + // kept sections); the counts go in section 0 (sh_size, sh_link). + let keptCount = 0; + for (const keptSection of sectionKept) if (keptSection) keptCount++; + const extended = keptCount >= SHN_LORESERVE; + const shndxOut = extended ? new Uint8Array(symbolOrder.length * 4) : null; + const shndxOutView = shndxOut === null ? null : new DataView(shndxOut.buffer); const newSymtabData = new Uint8Array(symbolOrder.length * 24); const newSymtabView = new DataView(newSymtabData.buffer); symbolOrder.forEach((oldIndex, newIndex) => { @@ -244,9 +261,14 @@ export function localizeElfObject(object: Uint8Array, keep: ReadonlySet) newSymtabView.setUint32(at, sym.nameOffset, true); newSymtabView.setUint8(at + 4, sym.info); newSymtabView.setUint8(at + 5, sym.other); - const shndx = - sym.shndx > SHN_UNDEF && sym.shndx < SHN_LORESERVE ? sectionMap[sym.shndx]! : sym.shndx; - newSymtabView.setUint16(at + 6, shndx, true); + const regular = !sym.reserved && sym.shndx !== SHN_UNDEF; + const shndx = regular ? sectionMap[sym.shndx]! : sym.shndx; + if (regular && shndx >= SHN_LORESERVE) { + newSymtabView.setUint16(at + 6, SHN_XINDEX, true); + shndxOutView!.setUint32(newIndex * 4, shndx, true); + } else { + newSymtabView.setUint16(at + 6, shndx, true); + } newSymtabView.setBigUint64(at + 8, symView.getBigUint64(oldIndex * 24 + 8, true), true); newSymtabView.setBigUint64(at + 16, symView.getBigUint64(oldIndex * 24 + 16, true), true); }); @@ -278,6 +300,33 @@ export function localizeElfObject(object: Uint8Array, keep: ReadonlySet) // Rebuild. sh_link is a section index in every use this object can carry; // sh_info is one only on relocation sections. const kept = sections.filter((_, i) => sectionKept[i] === true); + const newSymtabIndex = sectionMap[symtabIndex]!; + if (shndxOut !== null) { + // The extended index table goes last, named in the section-name table. + const shstrtab = sections[shstrndx]!; + let nameOffset = shndxSection?.nameOffset; + if (nameOffset === undefined) { + const name = textEncoder.encode(".symtab_shndx\0"); + const grown = new Uint8Array(shstrtab.data.length + name.length); + grown.set(shstrtab.data); + grown.set(name, shstrtab.data.length); + nameOffset = shstrtab.data.length; + shstrtab.data = grown; + } + kept.push({ + nameOffset, + type: SHT_SYMTAB_SHNDX, + flags: 0n, + addr: 0n, + size: BigInt(shndxOut.length), + link: symtabIndex, + info: 0, + addralign: 4n, + entsize: 4n, + data: shndxOut, + }); + } + const newShstrndx = sectionMap[shstrndx]!; let offset = 64; const offsets: number[] = []; for (const section of kept) { @@ -301,8 +350,8 @@ export function localizeElfObject(object: Uint8Array, keep: ReadonlySet) outView.setBigUint64(40, BigInt(shoffOut), true); outView.setUint16(54, 0, true); // e_phentsize outView.setUint16(56, 0, true); // e_phnum - outView.setUint16(60, kept.length, true); - outView.setUint16(62, sectionMap[shstrndx]!, true); + outView.setUint16(60, extended ? 0 : kept.length, true); + outView.setUint16(62, newShstrndx >= SHN_LORESERVE ? SHN_XINDEX : newShstrndx, true); kept.forEach((section, i) => { if (section.type !== SHT_NULL && section.type !== SHT_NOBITS) { out.set(section.data, offsets[i]!); @@ -313,12 +362,27 @@ export function localizeElfObject(object: Uint8Array, keep: ReadonlySet) outView.setBigUint64(at + 8, section.flags, true); outView.setBigUint64(at + 16, section.addr, true); outView.setBigUint64(at + 24, BigInt(offsets[i]!), true); + if (i === 0) { + // Section 0 carries the counts that overflow the ELF header. + outView.setBigUint64(at + 32, extended ? BigInt(kept.length) : 0n, true); + outView.setUint32(at + 40, newShstrndx >= SHN_LORESERVE ? newShstrndx : 0, true); + outView.setUint32(at + 44, 0, true); + return; + } outView.setBigUint64( at + 32, section.type === SHT_NOBITS ? section.size : BigInt(section.data.length), true, ); - outView.setUint32(at + 40, section.link === 0 ? 0 : (sectionMap[section.link] ?? 0), true); + outView.setUint32( + at + 40, + section.type === SHT_SYMTAB_SHNDX + ? newSymtabIndex + : section.link === 0 + ? 0 + : (sectionMap[section.link] ?? 0), + true, + ); outView.setUint32( at + 44, section.type === SHT_RELA || section.type === SHT_REL diff --git a/packages/compiler/src/backend/publication.test.ts b/packages/compiler/src/backend/publication.test.ts index 149ea4bb9..7b3e5f5cc 100644 --- a/packages/compiler/src/backend/publication.test.ts +++ b/packages/compiler/src/backend/publication.test.ts @@ -85,3 +85,34 @@ test("published types close over fields, containers and whole hierarchies", () = computePublishedTypes({ ...withStores, functions: withStores.functions.slice(1) }), ).toBeNull(); }); + +test("freezable array types carry the array write guard without a publish", () => { + const numbers = arrayOf(F64); + const push: IrStmt = { + kind: "exprStmt", + expr: { + kind: "arrIntrinsic", + method: "push", + receiver: ref("a", numbers), + args: [{ kind: "numLit", value: 1, type: F64, loc }], + type: F64, + loc, + }, + loc, + }; + const mod: IrModule = { + irVersion: 17, + sourceFile: loc.file, + entry: "main", + functions: [fn("main", []), fn("pushNumber", [push])], + freezableArrays: ["array"], + }; + const guarded = computePublishedTypes(mod)!; + expect(guarded.containerGuarded(numbers)).toBe(true); + expect(guarded.arrayGuards(numbers)).toEqual({ published: false, frozen: true }); + expect(guarded.containerGuarded(arrayOf(STRING))).toBe(false); + // A frozen array's push throws, so the function may throw. + expect(computeMayThrow(mod).fns.has("pushNumber")).toBe(true); + const { freezableArrays: _frozen, ...plain } = mod; + expect(computePublishedTypes(plain)).toBeNull(); +}); diff --git a/packages/compiler/src/backend/publication.ts b/packages/compiler/src/backend/publication.ts index 473a0cba4..38e429a47 100644 --- a/packages/compiler/src/backend/publication.ts +++ b/packages/compiler/src/backend/publication.ts @@ -46,6 +46,10 @@ export class PublishedTypes { readonly records = new Set(); /** typeKeys of array, map and set types. */ readonly containers = new Set(); + /** typeKeys of the array types the program may freeze + * (IrModule.freezableArrays): a frozen array refuses writes like a + * published one, so their stores carry the same guard. */ + readonly frozenArrays = new Set(); /** Constructor stores into a `this` nothing else can reach yet * (immutability.ts): never a published target, so never guarded. */ privateStores: ReadonlySet = new Set(); @@ -64,10 +68,16 @@ export class PublishedTypes { } containerGuarded(t: IrType): boolean { - return ( - (t.kind === "array" || t.kind === "map" || t.kind === "set") && - this.containers.has(typeKey(t)) - ); + if (t.kind !== "array" && t.kind !== "map" && t.kind !== "set") return false; + const key = typeKey(t); + return this.containers.has(key) || this.frozenArrays.has(key); + } + + /** Whether an array of type `t` may be published, and whether it may be + * frozen: the two states its write guard tests. */ + arrayGuards(t: IrType): { published: boolean; frozen: boolean } { + const key = typeKey(t); + return { published: this.containers.has(key), frozen: this.frozenArrays.has(key) }; } } @@ -87,7 +97,13 @@ export function computePublishedTypes(mod: IrModule, nodes?: IrNodeIndex): Publi ) roots.push(node.type); }); - if (roots.length === 0) return null; + const frozenArrays = mod.freezableArrays ?? []; + if (roots.length === 0 && frozenArrays.length === 0) return null; + if (roots.length === 0) { + const frozen = new PublishedTypes(); + for (const key of frozenArrays) frozen.frozenArrays.add(key); + return frozen; + } const classes = new Map((mod.classes ?? []).map((c) => [c.name, c])); const children = new Map(); @@ -100,6 +116,7 @@ export function computePublishedTypes(mod: IrModule, nodes?: IrNodeIndex): Publi const records = new Map((mod.records ?? []).map((r) => [r.id, r])); const unions = new Map((mod.unions ?? []).map((u) => [u.id, u])); const out = new PublishedTypes(); + for (const key of frozenArrays) out.frozenArrays.add(key); out.privateStores = computeConstructionFacts(mod, nodes).privateStores; const seen = new Set(); const stack = [...roots]; diff --git a/packages/compiler/src/backend/runtime-abi.ts b/packages/compiler/src/backend/runtime-abi.ts index 11bde6c2b..985ba6203 100644 --- a/packages/compiler/src/backend/runtime-abi.ts +++ b/packages/compiler/src/backend/runtime-abi.ts @@ -2,8 +2,8 @@ * of the link contract: a program object keeps this symbol undefined and the * matching runtime defines it, so incompatible manual links fail before the * program can start. */ -export const RUNTIME_ABI_VERSION = 37 as const; -export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v37" as const; +export const RUNTIME_ABI_VERSION = 47 as const; +export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v47" as const; /** Object consumption is useful today, but the complete scr_* surface may * still change before 1.0. The versioned marker prevents accidental mixing; diff --git a/packages/compiler/src/backend/runtime-pack-core.ts b/packages/compiler/src/backend/runtime-pack-core.ts index 10d0b972d..489c153c5 100644 --- a/packages/compiler/src/backend/runtime-pack-core.ts +++ b/packages/compiler/src/backend/runtime-pack-core.ts @@ -34,7 +34,7 @@ interface RuntimePackUnit { } interface RuntimePackArchive extends RuntimePackArtifact { - id: "quickjs" | "libregexp" | "zlib" | "mbedtls"; + id: "quickjs" | "libregexp" | "zlib" | "brotli" | "mbedtls"; predicate: RuntimePredicate; } @@ -87,13 +87,19 @@ export interface RuntimeFeatureSet extends NativeLinkFeatures { streamDyn: boolean; handleProps: boolean; declAdopt: boolean; + typeOrigins: boolean; dynForAwait: boolean; eventsOnceSignal: boolean; + mime: boolean; + utilTiming: boolean; dynBuffer: boolean; builtinValues: boolean; + computedRequire: boolean; warningReplace: boolean; stdioExtended: boolean; nodeIsland: boolean; + brotli: boolean; + moduleNamespace: boolean; nativeFetch: boolean; netEffective: boolean; httpEffective: boolean; @@ -286,13 +292,19 @@ export function effectiveRuntimeFeatures( streamDyn: features.streamDyn === true, handleProps: features.handleProps === true, declAdopt: features.declAdopt === true, + typeOrigins: features.typeOrigins === true, dynForAwait: features.dynForAwait === true, eventsOnceSignal: features.eventsOnceSignal === true, + mime: features.mime === true, + utilTiming: features.utilTiming === true, dynBuffer: features.dynBuffer === true, builtinValues: features.builtinValues === true, + computedRequire: features.computedRequire === true, warningReplace: features.warningReplace === true, stdioExtended: features.stdioExtended === true, nodeIsland: features.nodeIsland === true, + brotli: features.brotli === true, + moduleNamespace: features.moduleNamespace === true, nativeFetch, netEffective, httpEffective, @@ -389,6 +401,19 @@ export function runtimePackFlavorKey( return optimized; } +/** The units a flavor links for `features`, each with its selected + * variant. Variants of one unit can share an artifact file (the pack stores + * identical objects once), so a variant is identified by its unit and id, + * not by its path. */ +export function selectedRuntimeUnits( + flavor: RuntimePackFlavorManifest, + features: RuntimeFeatureSet, +): { source: string; variant: RuntimePackVariant }[] { + return flavor.runtime_units + .filter((unit) => evaluateRuntimePredicate(unit.predicate, features)) + .map((unit) => ({ source: unit.source, variant: selectVariant(unit, features) })); +} + /** Selection is deterministic and independent of installation paths. Both * hosts verify and privately stage exactly these artifacts before linking. */ export function selectRuntimePackArtifacts( @@ -410,9 +435,7 @@ export function selectRuntimePackArtifacts( ); return { features, - runtime: selectedFlavor.runtime_units - .filter((unit) => evaluateRuntimePredicate(unit.predicate, features)) - .map((unit) => selectVariant(unit, features)), + runtime: selectedRuntimeUnits(selectedFlavor, features).map(({ variant }) => variant), archives: manifest.archives.filter((archive) => evaluateRuntimePredicate(archive.predicate, features), ), diff --git a/packages/compiler/src/backend/runtime-pack-native.test.ts b/packages/compiler/src/backend/runtime-pack-native.test.ts index 20214818f..7a8a2e6c4 100644 --- a/packages/compiler/src/backend/runtime-pack-native.test.ts +++ b/packages/compiler/src/backend/runtime-pack-native.test.ts @@ -1,12 +1,40 @@ +import { createHash } from "node:crypto"; +import { + mkdirSync, + readFileSync, + renameSync, + statSync, + symlinkSync, + unlinkSync, + utimesSync, + writeFileSync, +} from "node:fs"; import { mkdtemp, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { afterEach, expect, test } from "vitest"; +import { afterEach, expect, test, vi } from "vitest"; import { nativeCodegenDiag } from "../diagnostics/diagnostic.js"; +import { compilerReleaseVersion } from "../library/sidecar.js"; import type { NativeLinkFeatures } from "./native-link-info.js"; -import { RuntimePackError } from "./runtime-pack-core.js"; -import { selectNativeRuntimePack } from "./runtime-pack-native.js"; -import { WASM32_WASI_TARGET } from "./targets.js"; +import { RUNTIME_ABI_MARKER, RUNTIME_ABI_VERSION } from "./runtime-abi.js"; +import { RuntimePackError, type RuntimePackManifest } from "./runtime-pack-core.js"; +import { + runtimePackManifest, + selectNativeRuntimePack, + stageNativeRuntimeSelection, + type NativeRuntimeSelection, + type RuntimePackStore, +} from "./runtime-pack-native.js"; +import { MACOS_ARM64_TARGET, WASM32_WASI_TARGET } from "./targets.js"; + +vi.mock("node:crypto", async (original) => { + const crypto = await original(); + return { ...crypto, createHash: vi.fn(crypto.createHash) }; +}); +vi.mock("node:fs", async (original) => { + const fs = await original(); + return { ...fs, readFileSync: vi.fn(fs.readFileSync) }; +}); const dirs: string[] = []; afterEach(async () => @@ -45,3 +73,171 @@ test("a missing native helper keeps the reinstall hint", () => { "reinstall scriptc with optional dependencies enabled for this host", ); }); + +/** A runtime pack with one runtime object and a license, and a store that + * keeps entries in memory. */ +async function packFixture() { + const root = await mkdtemp(join(tmpdir(), "scriptc-runtime-pack-store-")); + dirs.push(root); + const target = MACOS_ARM64_TARGET; + const version = compilerReleaseVersion(); + writeFileSync( + join(root, "package.json"), + JSON.stringify({ name: target.runtimePackPackage, version }), + ); + mkdirSync(join(root, "artifacts")); + const object = join(root, "artifacts", "base.o"); + writeFileSync(object, "base object"); + writeFileSync(join(root, "license.txt"), "license"); + const unit = { + source: "scr_base.c", + predicate: true, + variants: [ + { + id: "default", + when: {}, + defines: [], + path: "artifacts/base.o", + sha256: createHash("sha256").update("base object").digest("hex"), + size: Buffer.byteLength("base object"), + }, + ], + }; + const manifest: RuntimePackManifest = { + schema: "scriptc.runtime-pack.v1", + format: 1, + package: target.runtimePackPackage, + version, + target: { + name: target.name, + llvm_triple: target.llvmTriple, + architecture: target.architecture, + object_format: target.objectFormat, + minimum_os: target.minimumOs, + }, + runtime_abi: { version: RUNTIME_ABI_VERSION, marker: RUNTIME_ABI_MARKER }, + compiler: { command: "clang", identity: "fixture clang", target: target.llvmTriple }, + macros: { + executable: [], + excluded: ["SCR_LIB", "SCR_THREAD_INSTANCES", "SCR_RC_AUDIT"], + sanitizer: "external-toolchain-required", + }, + flavors: { + release: { optimization: "-O2", runtime_units: [unit] }, + dev: { optimization: "-O0", runtime_units: [unit] }, + }, + archives: [], + system_libraries: [{ name: "System", predicate: true }], + licenses: [{ path: "license.txt", license: "fixture" }], + }; + const manifestText = JSON.stringify(manifest); + writeFileSync(join(root, "runtime-pack.json"), manifestText); + const entries = new Map(); + const store: RuntimePackStore = { + read: (key) => entries.get(key) ?? null, + write: (key, text) => void entries.set(key, text), + }; + const select = () => + selectNativeRuntimePack( + root, + target, + version, + {} as NativeLinkFeatures, + "release", + "executable", + store, + ); + let stages = 0; + const stage = (selection: NativeRuntimeSelection) => { + const directory = join(root, `stage-${stages++}`); + mkdirSync(directory); + return stageNativeRuntimeSelection(selection, directory, store); + }; + return { root, object, manifestText, entries, select, stage }; +} + +test("a stored selection answers while the pack's files keep their identities", async () => { + const f = await packFixture(); + const first = f.select(); + expect(first.selected.systemLibraries).toEqual(["System"]); + expect(f.entries.size).toBe(1); + // The next selection reads the stored answer instead of the manifest. + const [key, text] = [...f.entries][0]!; + const stored = JSON.parse(text); + stored.selected.systemLibraries = ["Stored"]; + f.entries.set(key, JSON.stringify(stored)); + const second = f.select(); + expect(second.selected.systemLibraries).toEqual(["Stored"]); + expect(second.licenses).toEqual(first.licenses); + expect(second.manifest).toBeUndefined(); + expect(runtimePackManifest(second).flavors.release).toEqual(first.manifest!.flavors.release); + // Writing the manifest again, with the same text, gives it a new identity. + writeFileSync(join(f.root, "runtime-pack.json"), f.manifestText); + expect(f.select().selected.systemLibraries).toEqual(["System"]); +}); + +test.each(["replaced", "rewritten in place"])( + "staging hashes an artifact again when it is %s, even at the same size and mtime", + async (change) => { + const f = await packFixture(); + const selection = f.select(); + f.stage(selection); + const before = statSync(f.object); + const changed = change === "replaced" ? f.object + ".new" : f.object; + writeFileSync(changed, "x".repeat(before.size)); + utimesSync(changed, before.atime, before.mtime); + if (changed !== f.object) renameSync(changed, f.object); + expect(() => f.stage(selection)).toThrow( + "runtime pack artifact hash mismatch: artifacts/base.o", + ); + }, +); + +test("staging does not hash an unchanged artifact again", async () => { + const f = await packFixture(); + const selection = f.select(); + const hashes = () => vi.mocked(createHash).mock.calls.length; + let start = hashes(); + f.stage(selection); + const firstStage = hashes() - start; + start = hashes(); + const staged = f.stage(selection); + // Only the store key is hashed; the staged copy has the verified bytes. + expect(hashes() - start).toBe(firstStage - 1); + expect(readFileSync(staged.runtimeObjects[0]!, "utf8")).toBe("base object"); +}); + +test.skipIf(process.platform === "win32")( + "staging reads an artifact's bytes from the file whose identity it checks", + async () => { + const f = await packFixture(); + // The artifact path is a symbolic link to the verified file. + const real = f.object + ".real"; + renameSync(f.object, real); + symlinkSync(real, f.object); + const selection = f.select(); + f.stage(selection); + // During the next read the link names another file of the same size, + // and it names the verified file again before the read ends. + const other = f.object + ".other"; + writeFileSync(other, "x".repeat(statSync(real).size)); + const read = vi.mocked(readFileSync); + const original = read.getMockImplementation()!; + read.mockImplementation(((source: Parameters[0], ...rest: unknown[]) => { + if (typeof source === "string" && !source.endsWith("base.o")) + return (original as (...args: unknown[]) => unknown)(source, ...rest); + read.mockImplementation(original); + unlinkSync(f.object); + symlinkSync(other, f.object); + try { + return (original as (...args: unknown[]) => unknown)(source, ...rest); + } finally { + unlinkSync(f.object); + symlinkSync(real, f.object); + } + }) as typeof readFileSync); + const staged = f.stage(selection); + expect(read.getMockImplementation()).toBe(original); + expect(readFileSync(staged.runtimeObjects[0]!, "utf8")).toBe("base object"); + }, +); diff --git a/packages/compiler/src/backend/runtime-pack-native.ts b/packages/compiler/src/backend/runtime-pack-native.ts index bd38e2ef0..0fff34455 100644 --- a/packages/compiler/src/backend/runtime-pack-native.ts +++ b/packages/compiler/src/backend/runtime-pack-native.ts @@ -1,16 +1,21 @@ /** Synchronous runtime-pack host for the statically compiled driver. Each - * selected artifact is hashed and copied from the same read into private - * storage. This host has no persistent cache or mutable installed link inputs. */ + * selected artifact is verified and copied from the same read into private + * storage. With a store, a pack's selections and the identities of the + * artifacts it verified are kept between builds: an artifact whose file + * identity is unchanged since it was hashed is not hashed again. */ import type { NativeOptimization } from "./optimization.js"; import { createHash } from "node:crypto"; import { closeSync, constants, existsSync, + fstatSync, mkdirSync, openSync, readFileSync, + statSync, writeSync, + type Stats, } from "node:fs"; import { dirname, join } from "node:path"; import type { NativeLinkFeatures } from "./native-link-info.js"; @@ -34,13 +39,66 @@ export interface NativeRuntimePack { export interface NativeRuntimeSelection { root: string; - manifest: RuntimePackManifest; + /** The parsed manifest; a selection read from a store has none until + * runtimePackManifest parses it. */ + manifest?: RuntimePackManifest; + licenses: RuntimePackManifest["licenses"]; packageText: string; manifestText: string; selected: RuntimePackArtifacts; flavor: NativeOptimization; } +/** Text entries kept between builds (the native compiler's cache). */ +export interface RuntimePackStore { + read(key: string): string | null; + write(key: string, text: string): void; +} + +interface FileIdentity { + dev: number; + ino: number; + size: number; + mtime: number; + ctime: number; +} + +function fileIdentity(path: string): FileIdentity { + return statsIdentity(statSync(path)); +} + +function statsIdentity(info: Stats): FileIdentity { + return { + dev: info.dev, + ino: info.ino, + size: info.size, + mtime: info.mtimeMs, + ctime: info.ctimeMs, + }; +} + +function sameIdentity(a: FileIdentity, b: FileIdentity): boolean { + return ( + a.dev === b.dev && + a.ino === b.ino && + a.size === b.size && + a.mtime === b.mtime && + a.ctime === b.ctime + ); +} + +function storeKey(text: string): string { + return createHash("sha256").update(text).digest("hex"); +} + +/** A stored selection: what selecting from the pack's files, with these + * identities, returned. */ +interface StoredSelection { + schema: 1; + licenses: RuntimePackManifest["licenses"]; + selected: RuntimePackArtifacts; +} + export function selectNativeRuntimePack( root: string, target: NativeTargetSpec, @@ -48,38 +106,204 @@ export function selectNativeRuntimePack( features: NativeLinkFeatures, flavor: NativeOptimization, mode: RuntimePackMode = "executable", + store?: RuntimePackStore, ): NativeRuntimeSelection { - if (!existsSync(join(root, "package.json"))) { + const packagePath = join(root, "package.json"); + const manifestPath = join(root, "runtime-pack.json"); + if (!existsSync(packagePath)) { throw new RuntimePackError( `runtime pack for ${target.name} is not installed; install ${target.runtimePackPackage}@${compilerVersion} in your project or set SCRIPTC_RUNTIME_PACK to its directory`, "missing", "scriptc includes the runtime pack for its own platform only; other targets need their runtime pack package in the project", ); } - const packageText = readFileSync(join(root, "package.json"), "utf8"); - const manifestText = readFileSync(join(root, "runtime-pack.json"), "utf8"); + // The pack's files by identity, with everything else selection reads. + let key: string | null = null; + let identities: FileIdentity[] = []; + if (store !== undefined) { + try { + identities = [fileIdentity(packagePath), fileIdentity(manifestPath)]; + key = storeKey( + JSON.stringify({ + schema: 1, + root, + identities, + target: target.name, + compilerVersion, + features, + flavor, + mode, + }), + ); + const stored = readStoredSelection(store.read(key)); + if (stored !== null) { + const packageText = readFileSync(packagePath, "utf8"); + const manifestText = readFileSync(manifestPath, "utf8"); + // The texts read are the ones with the stored identities. + if ( + sameIdentity(fileIdentity(packagePath), identities[0]!) && + sameIdentity(fileIdentity(manifestPath), identities[1]!) + ) { + return { + root, + licenses: stored.licenses, + packageText, + manifestText, + selected: stored.selected, + flavor, + }; + } + } + } catch { + key = null; + } + } + const packageText = readFileSync(packagePath, "utf8"); + const manifestText = readFileSync(manifestPath, "utf8"); const identity = JSON.parse(packageText) as { name?: string; version?: string }; const manifest = parseRuntimePackManifest(JSON.parse(manifestText)); validateRuntimePackIdentity(manifest, identity.name, identity.version, target, compilerVersion); const selected = selectRuntimePackArtifacts(manifest, features, flavor, process.env, mode); - return { root, manifest, packageText, manifestText, selected, flavor }; + if (store !== undefined && key !== null) { + try { + // Stored only when the files selected from still have the identities + // in the key. + if ( + sameIdentity(fileIdentity(packagePath), identities[0]!) && + sameIdentity(fileIdentity(manifestPath), identities[1]!) + ) { + const entry: StoredSelection = { schema: 1, licenses: manifest.licenses, selected }; + store.write(key, JSON.stringify(entry)); + } + } catch { + /* A failed write is a later miss. */ + } + } + return { + root, + manifest, + licenses: manifest.licenses, + packageText, + manifestText, + selected, + flavor, + }; } -function stageArtifact(root: string, stage: string, artifact: RuntimePackArtifact): string { +/** The selection's parsed manifest. */ +export function runtimePackManifest(selection: NativeRuntimeSelection): RuntimePackManifest { + return (selection.manifest ??= parseRuntimePackManifest(JSON.parse(selection.manifestText))); +} + +function readStoredSelection(text: string | null): StoredSelection | null { + if (text === null) return null; + const value: unknown = JSON.parse(text); + if (value === null || typeof value !== "object") return null; + const entry = value as Partial; + if ( + entry.schema !== 1 || + !Array.isArray(entry.licenses) || + entry.selected === undefined || + entry.selected === null || + typeof entry.selected !== "object" || + !Array.isArray(entry.selected.runtime) || + !Array.isArray(entry.selected.archives) || + !Array.isArray(entry.selected.systemLibraries) + ) + return null; + return entry as StoredSelection; +} + +/** An artifact path's identity when its bytes last matched the manifest's + * size and digest. */ +interface VerifiedArtifact { + path: string; + sha256: string; + dev: number; + ino: number; + size: number; + mtime: number; + ctime: number; +} + +interface StoredVerification { + schema: number; + artifacts: VerifiedArtifact[]; +} + +function readVerified( + store: RuntimePackStore | undefined, + key: string, +): Map { + const verified = new Map(); + try { + const text = store?.read(key) ?? null; + if (text === null) return verified; + const stored = JSON.parse(text) as StoredVerification; + if (stored.schema === 1) + for (const artifact of stored.artifacts) verified.set(artifact.path, artifact); + } catch { + /* A malformed entry verifies everything again. */ + } + return verified; +} + +function stageArtifact( + root: string, + stage: string, + artifact: RuntimePackArtifact, + verified: Map, + newlyVerified: VerifiedArtifact[], +): string { const source = join(root, artifact.path); const destination = join(stage, artifact.path); let bytes: Buffer; + let before: FileIdentity; + let after: FileIdentity; try { - bytes = readFileSync(source); + // The identities are those of the open file the bytes come from, also + // when the path is a symbolic link that changes during the read. + const input = openSync(source, constants.O_RDONLY); + try { + before = statsIdentity(fstatSync(input)); + bytes = readFileSync(input); + after = statsIdentity(fstatSync(input)); + } finally { + closeSync(input); + } } catch { throw new RuntimePackError(`runtime pack artifact is missing: ${artifact.path}`, "invalid"); } + // The bytes are those last hashed when the file kept the identity it had + // then for the whole read. + const known = verified.get(artifact.path); + const unchanged = sameIdentity(before, after); + const trusted = + known !== undefined && + known.sha256 === artifact.sha256 && + known.size === artifact.size && + unchanged && + known.dev === before.dev && + known.ino === before.ino && + known.size === before.size && + known.mtime === before.mtime && + known.ctime === before.ctime; if ( bytes.length !== artifact.size || - createHash("sha256").update(bytes).digest("hex") !== artifact.sha256 + (!trusted && createHash("sha256").update(bytes).digest("hex") !== artifact.sha256) ) { throw new RuntimePackError(`runtime pack artifact hash mismatch: ${artifact.path}`, "invalid"); } + if (!trusted && unchanged) + newlyVerified.push({ + path: artifact.path, + sha256: artifact.sha256, + dev: before.dev, + ino: before.ino, + size: before.size, + mtime: before.mtime, + ctime: before.ctime, + }); mkdirSync(dirname(destination), { recursive: true }); const fd = openSync( destination, @@ -123,15 +347,19 @@ export function stageNativeRuntimePack( export function stageNativeRuntimeSelection( selection: NativeRuntimeSelection, stageRoot: string, + store?: RuntimePackStore, ): NativeRuntimePack { - const { root, manifest, selected, packageText, manifestText } = selection; + const { root, licenses, selected, packageText, manifestText } = selection; const packagePath = join(root, "package.json"); const manifestPath = join(root, "runtime-pack.json"); - const runtimeObjects = selected.runtime.map((artifact) => - stageArtifact(root, stageRoot, artifact), - ); - const archives = selected.archives.map((artifact) => stageArtifact(root, stageRoot, artifact)); - for (const license of manifest.licenses) { + const key = storeKey(JSON.stringify({ schema: 1, verified: root })); + const verified = readVerified(store, key); + const newlyVerified: VerifiedArtifact[] = []; + const stage = (artifact: RuntimePackArtifact): string => + stageArtifact(root, stageRoot, artifact, verified, newlyVerified); + const runtimeObjects = selected.runtime.map(stage); + const archives = selected.archives.map(stage); + for (const license of licenses) { try { readFileSync(join(root, license.path)); } catch { @@ -144,5 +372,14 @@ export function stageNativeRuntimeSelection( ) { throw new RuntimePackError("runtime pack changed while staging verified artifacts", "invalid"); } + if (store !== undefined && newlyVerified.length > 0) { + for (const artifact of newlyVerified) verified.set(artifact.path, artifact); + const entry: StoredVerification = { schema: 1, artifacts: [...verified.values()] }; + try { + store.write(key, JSON.stringify(entry)); + } catch { + /* A failed write only means hashing again next time. */ + } + } return { runtimeObjects, archives, systemLibraries: selected.systemLibraries }; } diff --git a/packages/compiler/src/backend/runtime-pack-targets.test.ts b/packages/compiler/src/backend/runtime-pack-targets.test.ts index 44c3d6290..fcbe3f08e 100644 --- a/packages/compiler/src/backend/runtime-pack-targets.test.ts +++ b/packages/compiler/src/backend/runtime-pack-targets.test.ts @@ -52,7 +52,7 @@ async function fixture(target: NativeTargetSpec, workers = false) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 37, marker: "scr_runtime_abi_v37" }, + runtime_abi: { version: 47, marker: "scr_runtime_abi_v47" }, compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple }, macros: { executable: [], excluded: ["SCR_LIB"], sanitizer: "external-toolchain-required" }, flavors: { @@ -118,6 +118,7 @@ const features = { dgram: false, dnsPromises: false, dgramDyn: false, + textCodecDyn: false, watch: false, foreignFfi: false, nodeTest: false, @@ -128,6 +129,7 @@ const features = { dh: false, pki: false, ed25519: false, + x509: false, webcrypto: false, perfLoop: false, }; diff --git a/packages/compiler/src/backend/runtime-pack.test.ts b/packages/compiler/src/backend/runtime-pack.test.ts index e3bc999a7..b6e3f0ef7 100644 --- a/packages/compiler/src/backend/runtime-pack.test.ts +++ b/packages/compiler/src/backend/runtime-pack.test.ts @@ -54,6 +54,7 @@ const BASE: NativeLinkFeatures = { dgram: false, dnsPromises: false, dgramDyn: false, + textCodecDyn: false, watch: false, foreignFfi: false, nodeTest: false, @@ -64,6 +65,7 @@ const BASE: NativeLinkFeatures = { dh: false, pki: false, ed25519: false, + x509: false, webcrypto: false, perfLoop: false, }; @@ -127,7 +129,7 @@ async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 37, marker: "scr_runtime_abi_v37" }, + runtime_abi: { version: 47, marker: "scr_runtime_abi_v47" }, compiler: { command: "clang", identity: "fixture clang", diff --git a/packages/compiler/src/backend/target-cpu.test.ts b/packages/compiler/src/backend/target-cpu.test.ts new file mode 100644 index 000000000..428caee32 --- /dev/null +++ b/packages/compiler/src/backend/target-cpu.test.ts @@ -0,0 +1,51 @@ +import { expect, test } from "vitest"; +import { + isTargetCpuOption, + targetCpuAttribute, + targetCpuField, + targetCpuKeyParts, + targetCpuTargetRefusal, + x86Level, +} from "./target-cpu.js"; + +const V2 = ["cx16", "lahf_lm", "popcnt", "pni", "sse4_1", "sse4_2", "ssse3"]; +const V3 = ["avx", "avx2", "bmi1", "bmi2", "f16c", "fma", "abm", "movbe", "xsave"]; +const V4 = ["avx512f", "avx512bw", "avx512cd", "avx512dq", "avx512vl"]; +const level = (flags: string[]) => x86Level((flag) => flags.includes(flag)); + +test("a host's level is the highest one whose features and lower levels' features it has", () => { + expect(level([])).toBe("baseline"); + expect(level(V2)).toBe("x86-64-v2"); + expect(level([...V2, ...V3])).toBe("x86-64-v3"); + expect(level([...V2, ...V3, ...V4])).toBe("x86-64-v4"); + // AVX-512 without BMI2 is still v2. + expect(level([...V2, ...V3.filter((f) => f !== "bmi2"), ...V4])).toBe("x86-64-v2"); + expect(level([...V3, ...V4])).toBe("baseline"); +}); + +test("the baseline adds no key part, attribute or option field", () => { + for (const cpu of [undefined, "baseline"] as const) { + expect(targetCpuKeyParts(cpu)).toEqual([]); + expect(targetCpuAttribute(cpu)).toBe(""); + expect(targetCpuField(cpu)).toEqual({}); + expect(targetCpuTargetRefusal(cpu, "linux", "arm64")).toBeNull(); + } + expect(targetCpuKeyParts("x86-64-v3")).toEqual(["target-cpu-x86-64-v3"]); + expect(targetCpuAttribute("x86-64-v4")).toBe(' "target-cpu"="x86-64-v4"'); + expect(targetCpuField("x86-64-v2")).toEqual({ targetCpu: "x86-64-v2" }); +}); + +test("a level is refused for targets that are not x86-64", () => { + expect(targetCpuTargetRefusal("x86-64-v3", "linux", "x64")).toBeNull(); + expect(targetCpuTargetRefusal("x86-64-v3", "darwin", "arm64")).toBe( + "--target-cpu=x86-64-v3 requires an x86-64 target", + ); + expect(targetCpuTargetRefusal("x86-64-v3", "wasi", "x64")).not.toBeNull(); +}); + +test("the option accepts the levels and native", () => { + for (const value of ["baseline", "x86-64-v2", "x86-64-v3", "x86-64-v4", "native"]) + expect(isTargetCpuOption(value)).toBe(true); + for (const value of ["x86-64", "haswell", "", undefined]) + expect(isTargetCpuOption(value)).toBe(false); +}); diff --git a/packages/compiler/src/backend/target-cpu.ts b/packages/compiler/src/backend/target-cpu.ts new file mode 100644 index 000000000..d2967ca68 --- /dev/null +++ b/packages/compiler/src/backend/target-cpu.ts @@ -0,0 +1,123 @@ +import { execFileSync } from "node:child_process"; +import { readFileSync } from "node:fs"; + +/** The x86-64 microarchitecture levels an executable may require + * (`--target-cpu`). `baseline` is the x86-64 baseline every release build + * targets; the other levels are LLVM's `x86-64-v2` to `x86-64-v4`. */ +export type TargetCpu = "baseline" | "x86-64-v2" | "x86-64-v3" | "x86-64-v4"; + +/** The values `--target-cpu` accepts: a level, or `native` for the highest + * level the build host supports. */ +export const TARGET_CPU_OPTIONS = ["baseline", "x86-64-v2", "x86-64-v3", "x86-64-v4", "native"]; + +export function isTargetCpuOption(value: unknown): value is TargetCpu | "native" { + return typeof value === "string" && TARGET_CPU_OPTIONS.includes(value); +} + +/** The CPU features each level adds to the one below it, as Linux names + * them in /proc/cpuinfo (`pni` is SSE3, `abm` includes LZCNT). */ +const LEVEL_FLAGS: readonly [TargetCpu, readonly string[]][] = [ + ["x86-64-v2", ["cx16", "lahf_lm", "popcnt", "pni", "sse4_1", "sse4_2", "ssse3"]], + ["x86-64-v3", ["avx", "avx2", "bmi1", "bmi2", "f16c", "fma", "abm", "movbe", "xsave"]], + ["x86-64-v4", ["avx512f", "avx512bw", "avx512cd", "avx512dq", "avx512vl"]], +]; + +/** The macOS `machdep.cpu` spellings of the same features. */ +const DARWIN_FLAGS: Readonly> = { + cx16: "CX16", + lahf_lm: "LAHF", + popcnt: "POPCNT", + pni: "SSE3", + sse4_1: "SSE4.1", + sse4_2: "SSE4.2", + ssse3: "SSSE3", + avx: "AVX1.0", + avx2: "AVX2", + bmi1: "BMI1", + bmi2: "BMI2", + f16c: "F16C", + fma: "FMA", + abm: "LZCNT", + movbe: "MOVBE", + xsave: "XSAVE", + avx512f: "AVX512F", + avx512bw: "AVX512BW", + avx512cd: "AVX512CD", + avx512dq: "AVX512DQ", + avx512vl: "AVX512VL", +}; + +/** The highest level whose features (and every lower level's) are all in + * `has`. */ +export function x86Level(has: (flag: string) => boolean): TargetCpu { + let level: TargetCpu = "baseline"; + for (const [next, flags] of LEVEL_FLAGS) { + if (!flags.every(has)) break; + level = next; + } + return level; +} + +/** The highest level the build host supports, or null when the host is not + * an x86-64 Linux or macOS machine. */ +export function hostTargetCpu( + platform: string = process.platform, + arch: string = process.arch, +): TargetCpu | null { + if (arch !== "x64") return null; + try { + if (platform === "linux") { + const info = readFileSync("/proc/cpuinfo", "utf8"); + const line = /^flags\s*:(.*)$/m.exec(info); + if (!line) return null; + const flags = new Set(line[1]!.trim().split(/\s+/)); + return x86Level((flag) => flags.has(flag)); + } + if (platform === "darwin") { + const text = execFileSync( + "sysctl", + ["-n", "machdep.cpu.features", "machdep.cpu.leaf7_features", "machdep.cpu.extfeatures"], + { encoding: "utf8" }, + ); + const flags = new Set(text.trim().split(/\s+/)); + return x86Level((flag) => flags.has(DARWIN_FLAGS[flag]!)); + } + } catch { + return null; + } + return null; +} + +/** Cache-key parts for a target level; empty for the baseline so existing + * keys are unchanged. */ +export function targetCpuKeyParts(cpu: TargetCpu | undefined): string[] { + return cpu === undefined || cpu === "baseline" ? [] : [`target-cpu-${cpu}`]; +} + +/** The function attribute that makes LLVM generate code for the level, or + * "" for the baseline. */ +export function targetCpuAttribute(cpu: TargetCpu | undefined): string { + return cpu === undefined || cpu === "baseline" ? "" : ` "target-cpu"="${cpu}"`; +} + +/** The options field for a level: omitted for the baseline. */ +export function targetCpuField(cpu: TargetCpu | undefined): { targetCpu?: TargetCpu } { + return cpu === undefined || cpu === "baseline" ? {} : { targetCpu: cpu }; +} + +/** Cache-key parts for a dispatching build: the tiers it carries. */ +export function cpuDispatchKeyParts(cpuDispatch: boolean | undefined): string[] { + return cpuDispatch === true ? ["cpu-dispatch-x86-64-v3"] : []; +} + +/** The error for a level on a target that is not x86-64, or null. */ +export function targetCpuTargetRefusal( + cpu: TargetCpu | undefined, + platform: string, + architecture: string, +): string | null { + if (cpu === undefined || cpu === "baseline") return null; + if (architecture !== "x64" || platform === "wasi") + return `--target-cpu=${cpu} requires an x86-64 target`; + return null; +} diff --git a/packages/compiler/src/backend/vendor-archives.ts b/packages/compiler/src/backend/vendor-archives.ts index daa7e5eaa..ef63aa4e4 100644 --- a/packages/compiler/src/backend/vendor-archives.ts +++ b/packages/compiler/src/backend/vendor-archives.ts @@ -14,10 +14,15 @@ import type { CcDriver } from "./native/driver.js"; const execFileAsync = promisify(execFile); +/** The section flags the libregexp/libunicode objects build with (the + * runtime pack's executable_section_elimination flags). */ +const LRE_SECTION_FLAGS = ["-ffunction-sections", "-fdata-sections"]; + import { QJS_COMMIT, MBEDTLS_VERSION, ZLIB_VERSION, + BROTLI_VERSION, LRE_SOURCES, ZLIB_SOURCES, } from "./vendor-inputs.js"; @@ -25,6 +30,7 @@ export { QJS_COMMIT, MBEDTLS_VERSION, ZLIB_VERSION, + BROTLI_VERSION, LRE_SOURCES, ZLIB_SOURCES, } from "./vendor-inputs.js"; @@ -189,13 +195,13 @@ export function createVendorArchives(context: VendorArchiveContext) { (sanitize ? "asan" : "plain") + (driver.argv.length === 1 && driver.argv[0] === "clang" ? "" : "-zigcc") + `-${vendorCacheTargetFlavor(driver)}-${buildIdentity}`; - const cacheDir = join(cacheRoot, `${QJS_COMMIT.slice(0, 12)}-lre-${flavor}`); + const cacheDir = join(cacheRoot, `${QJS_COMMIT.slice(0, 12)}-lre-sections-${flavor}`); return LRE_SOURCES.map((f) => join(cacheDir, f.replace(/\.c$/, ".o"))); } /** The libregexp objects for one flavor, compiled lazily on the first * regex-using build (~1s) and cached under - * /vendor/-lre---/*.o — with the same atomic-rename + * /vendor/-lre-sections---/*.o — with the same atomic-rename * publish (parallel first builds race safely; losers discard their work). * Plain is -Os to keep regex-using binaries small; asan matches the final link so * the sanitized lane instruments the matcher too. Cross targets get their @@ -226,6 +232,10 @@ export function createVendorArchives(context: VendorArchiveContext) { vendor: vendorEngineDir(), sources: LRE_SOURCES, objects: lreObjectPaths(sanitize, driver, buildIdentity, cacheRoot), + // Section elimination keeps only the libunicode tables a program + // reaches: the URL host parser's NFC links the normalization tables + // without the regex property ones. + flags: LRE_SECTION_FLAGS, }); } @@ -294,6 +304,7 @@ export function createVendorArchives(context: VendorArchiveContext) { vendor: string; sources: readonly string[]; objects: string[]; + flags?: readonly string[]; }): Promise { const { sanitize, driver, cacheRoot, flavor, name, vendor, sources, objects } = options; if ((await Promise.all(objects.map(validVendorArtifact))).every(Boolean)) return objects; @@ -310,6 +321,7 @@ export function createVendorArchives(context: VendorArchiveContext) { "-std=c11", ...driver.targetArgs, ...(sanitize ? ["-O1", "-fsanitize=address"] : ["-Os"]), + ...(options.flags ?? []), "-I", vendor, "-c", @@ -418,9 +430,98 @@ export function createVendorArchives(context: VendorArchiveContext) { return archive; } + function vendorBrotliDir(): string { + return join(runtimeSrcDir(), "..", "vendor", "brotli", "c"); + } + + function brotliArchivePath( + sanitize: boolean, + driver: CcDriver, + buildIdentity: string, + cacheRoot: string = vendorBuildCacheRoot(), + ): string { + const flavor = `${sanitize ? "asan" : "plain"}-${vendorCacheTargetFlavor(driver)}-${buildIdentity}`; + return join(cacheRoot, `brotli-${BROTLI_VERSION}-${flavor}`, "libbrotli.a"); + } + + /** The brotli archive for one flavor, compiled lazily on the first + * Brotli-using build and cached under + * /vendor/brotli----/libbrotli.a, + * with the mbedTLS archive's recipe: per-TU compiles of c/common, c/dec + * and c/enc against c/include, then one `ar rcs`. dec/ and enc/ share + * file names, so each object is named after its path (dec-static_init.o). */ + async function ensureBrotliArchive( + sanitize: boolean, + driver: CcDriver, + buildIdentity: string, + cacheRoot: string = vendorBuildCacheRoot(), + ): Promise { + const flavor = `${sanitize ? "asan" : "plain"}-${vendorCacheTargetFlavor(driver)}-${buildIdentity}`; + const compileArgv = isZigDriver(driver) ? driver.argv : ["clang"]; + const arArgv = isZigDriver(driver) ? [...driver.argv.slice(0, 1), "ar"] : ["ar"]; + const vendor = vendorBrotliDir(); + const archive = brotliArchivePath(sanitize, driver, buildIdentity, cacheRoot); + if (await validVendorArtifact(archive)) return archive; + + await mkdir(cacheRoot, { recursive: true }); + const buildDir = await mkdtemp(join(tmpdir(), `scriptc-vendor-brotli-${flavor}-`)); + try { + const sources = ( + await Promise.all( + ["common", "dec", "enc"].map(async (dir) => + (await readdir(join(vendor, dir))) + .filter((name) => !name.startsWith(".") && name.endsWith(".c")) + .map((name) => `${dir}/${name}`), + ), + ) + ) + .flat() + .sort(); + const object = (src: string): string => + join(buildDir, `${src.replaceAll("/", "-").replace(/\.c$/, "")}.o`); + const cflags = [ + "-std=c11", + ...driver.targetArgs, + ...(sanitize ? ["-O1", "-fsanitize=address"] : ["-Os"]), + "-I", + join(vendor, "include"), + ]; + const width = availableParallelism(); + for (let i = 0; i < sources.length; i += width) { + await Promise.all( + sources + .slice(i, i + width) + .map((src) => + execFileAsync(compileArgv[0] ?? "clang", [ + ...compileArgv.slice(1), + ...cflags, + "-c", + join(vendor, src), + "-o", + object(src), + ]), + ), + ); + } + await execFileAsync(arArgv[0] ?? "ar", [ + ...arArgv.slice(1), + "rcs", + join(buildDir, "libbrotli.a"), + ...sources.map(object), + ]); + await publishVendorArtifact(join(buildDir, "libbrotli.a"), archive); + } finally { + await rm(buildDir, { recursive: true, force: true }); + } + return archive; + } + return { vendorEngineDir, vendorTlsDir, + vendorBrotliDir, + brotliArchivePath, + ensureBrotliArchive, vendorBuildCacheRoot, vendorCacheTargetFlavor, vendorCacheBuildIdentity, diff --git a/packages/compiler/src/backend/vendor-inputs.ts b/packages/compiler/src/backend/vendor-inputs.ts index 3d2710fa0..a9d13e2f0 100644 --- a/packages/compiler/src/backend/vendor-inputs.ts +++ b/packages/compiler/src/backend/vendor-inputs.ts @@ -1,7 +1,9 @@ -/** The pinned QuickJS, mbedTLS, and zlib inputs used by native recipes. */ +/** The pinned QuickJS, mbedTLS, zlib, and brotli inputs used by native recipes. */ export const QJS_COMMIT = "3c8f3d68953955950074c41c6e4d999562ae82a7"; export const MBEDTLS_VERSION = "3.6.7"; export const ZLIB_VERSION = "1.3.1"; +/** The brotli release Node v24.22.0 bundles (packages/runtime/vendor/brotli). */ +export const BROTLI_VERSION = "1.2.0"; /** Exact translation-unit membership shared by cache builds and external * source-pack recipes. */ export const LRE_SOURCES = ["libregexp.c", "libunicode.c"] as const; diff --git a/packages/compiler/src/backend/warm-cache.ts b/packages/compiler/src/backend/warm-cache.ts index b48cf6739..c42ceb79e 100644 --- a/packages/compiler/src/backend/warm-cache.ts +++ b/packages/compiler/src/backend/warm-cache.ts @@ -61,6 +61,7 @@ export async function warmNativeCaches( dgram: false, dnsPromises: false, dgramDyn: false, + textCodecDyn: false, watch: false, foreignFfi: false, nodeTest: false, @@ -70,6 +71,7 @@ export async function warmNativeCaches( dh: false, pki: false, ed25519: false, + x509: false, webcrypto: false, perfLoop: false, tlsCa: false, diff --git a/packages/compiler/src/cli/command.ts b/packages/compiler/src/cli/command.ts index 16cb6b4ca..020d3eb4b 100644 --- a/packages/compiler/src/cli/command.ts +++ b/packages/compiler/src/cli/command.ts @@ -1,3 +1,4 @@ +import type { TypePatchCommandRequest } from "../frontend/type-patches/command.js"; import { rmSync, statSync } from "node:fs"; import { dirname, join, resolve } from "node:path"; import { parseArgs } from "node:util"; @@ -19,9 +20,22 @@ import { } from "../node-islands/policy.js"; import { TIER_MODES, type TierMode } from "../frontend/tier-analysis.js"; import { buildEnvelope } from "../diagnostics/envelope.js"; -import { renderWarnings } from "../diagnostics/render.js"; +import { + renderConfigWarnings, + renderTypePatchWarnings, + renderWarnings, +} from "../diagnostics/render.js"; +import { isTypePatchCode } from "../diagnostics/diagnostic.js"; import { isPerformanceCode, renderPerformanceWarnings } from "../diagnostics/dynamic-tier.js"; import { isNativeOptimization } from "../backend/optimization.js"; +import { renderNativeProfileMatch } from "../backend/native-profile.js"; +import { + hostTargetCpu, + isTargetCpuOption, + TARGET_CPU_OPTIONS, + targetCpuField, + type TargetCpu, +} from "../backend/target-cpu.js"; import { setProvenanceSources } from "../frontend/provenance-registry.js"; import type { CliHost, NativeCacheWarmProfile } from "./host.js"; import { resolveOutputOptions } from "./output-options.js"; @@ -103,6 +117,8 @@ async function main(args: string[], host: CliHost): Promise { values.strip || values["windows-subsystem"] !== undefined || values["detached-exit"] || + values["target-cpu"] !== undefined || + values["cpu-dispatch"] !== undefined || values["profile-generate"] !== undefined || values["profile-use"] !== undefined || values.sparse || @@ -162,6 +178,8 @@ async function main(args: string[], host: CliHost): Promise { values.sanitize || values["windows-subsystem"] !== undefined || values["detached-exit"] || + values["target-cpu"] !== undefined || + values["cpu-dispatch"] !== undefined || values["profile-generate"] !== undefined || values["profile-use"] !== undefined || (values["npm-static"] ?? []).length > 0 || @@ -191,6 +209,39 @@ async function main(args: string[], host: CliHost): Promise { process.stdout.write(`${outputPath}\n`); return 0; } + if (command === "types") { + const sub = positionals[1]; + const rest = positionals.slice(2); + let request: TypePatchCommandRequest; + if (sub === "patch" && rest.length === 3) { + request = { + action: "patch", + cwd: process.cwd(), + pkg: rest[0]!, + selector: rest[1]!, + type: rest[2]!, + range: values.range ?? null, + dryRun: values["dry-run"], + }; + } else if ( + sub === "list" && + rest.length === 0 && + values.range === undefined && + !values["dry-run"] + ) { + request = { action: "list", cwd: process.cwd() }; + } else { + fail( + `scriptc types takes "patch " (with --range or --dry-run) or "list"\n\n${USAGE}`, + ); + } + const result = await host.typePatches(request); + process.stdout.write(result.stdout); + process.stderr.write(result.stderr); + return result.exitCode; + } + if (values.range !== undefined || values["dry-run"]) + fail(`--range and --dry-run are scriptc types patch options\n\n${USAGE}`); if (values.sparse) fail(`--sparse is a merge-profiles option\n\n${USAGE}`); if (command !== "build" && command !== "run" && command !== "coverage") { fail(`unknown command "${command}"\n\n${USAGE}`); @@ -219,6 +270,8 @@ async function main(args: string[], host: CliHost): Promise { values.ffi !== undefined || values["windows-subsystem"] !== undefined || values["detached-exit"] || + values["target-cpu"] !== undefined || + values["cpu-dispatch"] !== undefined || values["profile-generate"] !== undefined || values["profile-use"] !== undefined || (values["npm-static"] ?? []).length > 0 || @@ -228,7 +281,7 @@ async function main(args: string[], host: CliHost): Promise { hasNodeIslandFlags(values) ) { fail( - "scriptc build --lib takes no --backend/--emit/--print/--optimization/--strip/--windows-subsystem/--detached-exit/--profile-generate/--profile-use/--npm-static/--ffi/--external-types/--tier/--explain-dynamic: the profile pins the emission and optimization, npm imports are judged automatically, outbound FFI belongs to executable builds, external type mappings belong to coverage, and library artifacts are static-tier only", + "scriptc build --lib takes no --backend/--emit/--print/--optimization/--strip/--windows-subsystem/--detached-exit/--target-cpu/--cpu-dispatch/--profile-generate/--profile-use/--npm-static/--ffi/--external-types/--tier/--explain-dynamic: the profile pins the emission and optimization, npm imports are judged automatically, outbound FFI belongs to executable builds, external type mappings belong to coverage, and library artifacts are static-tier only", ); } const profilePath = resolve(profileArg); @@ -271,6 +324,12 @@ async function main(args: string[], host: CliHost): Promise { if (command === "coverage" && values.strip) { fail(`--strip is a build/run option\n\n${USAGE}`); } + if (command === "coverage" && values["target-cpu"] !== undefined) { + fail(`--target-cpu is a build/run option\n\n${USAGE}`); + } + if (command === "coverage" && values["cpu-dispatch"] !== undefined) { + fail(`--cpu-dispatch is a build/run option\n\n${USAGE}`); + } if (command === "coverage" && values["detached-exit"]) { fail(`--detached-exit is a build/run option\n\n${USAGE}`); } @@ -424,6 +483,19 @@ async function main(args: string[], host: CliHost): Promise { if (windowsSubsystem !== undefined && command === "coverage") { fail(`--windows-subsystem is only supported for executable builds\n\n${USAGE}`); } + const targetCpuOption = values["target-cpu"]; + if (targetCpuOption !== undefined && !isTargetCpuOption(targetCpuOption)) { + fail( + `unknown target CPU "${targetCpuOption}" (supported: ${TARGET_CPU_OPTIONS.join(", ")})\n\n${USAGE}`, + ); + } + let targetCpu: TargetCpu | undefined = targetCpuOption === "native" ? undefined : targetCpuOption; + if (targetCpuOption === "native") { + const level = hostTargetCpu(); + if (level === null) + fail(`--target-cpu=native needs an x86-64 Linux or macOS build host; name a level instead`); + targetCpu = level; + } const output = command === "coverage" ? null @@ -438,6 +510,7 @@ async function main(args: string[], host: CliHost): Promise { ...(values.optimization === undefined ? {} : { optimization: values.optimization }), strip: values.strip, detachedExit: values["detached-exit"], + ...(values["cpu-dispatch"] === undefined ? {} : { cpuDispatch: values["cpu-dispatch"] }), ...(values["profile-generate"] === undefined ? {} : { profileGenerate: values["profile-generate"] }), @@ -565,6 +638,8 @@ async function main(args: string[], host: CliHost): Promise { : resolve(values.libnode), } : {}), + ...targetCpuField(targetCpu), + ...(values["cpu-dispatch"] === undefined ? {} : { cpuDispatch: values["cpu-dispatch"] }), // The instrumented program resolves a relative pattern against its own // working directory; resolve it here, where the user wrote it. ...(values["profile-generate"] === undefined @@ -574,6 +649,7 @@ async function main(args: string[], host: CliHost): Promise { ? {} : { profileUse: resolve(values["profile-use"]) }), ...(npmStatic !== undefined ? { npmStatic } : {}), + ...(values["shape-overlay"] === false ? { shapeOverlay: false } : {}), ...(ffiProfilePath !== undefined ? { ffiProfilePath } : {}), ...(printNativeLinkInfo ? { nativeLinkInfo: true } : {}), ...(tier !== undefined ? { tier } : {}), @@ -617,18 +693,44 @@ async function main(args: string[], host: CliHost): Promise { // warning names likely-hot functions that use untyped values. `run` // keeps stderr for the program's own output. const color = process.stderr.isTTY ?? false; - const divergences = warnings.filter((d) => !isPerformanceCode(d.code)); + const config = warnings.filter((d) => d.code === "SC8004"); + const patches = warnings.filter((d) => isTypePatchCode(d.code)); + const divergences = warnings.filter( + (d) => !isPerformanceCode(d.code) && d.code !== "SC8004" && !isTypePatchCode(d.code), + ); const performance = warnings.filter((d) => isPerformanceCode(d.code)); - if (divergences.length > 0) + if (config.length > 0) + process.stderr.write( + renderConfigWarnings(config, result.sourceTexts ?? new Map(), { color }) + "\n", + ); + if (patches.length > 0) { + if (config.length > 0) process.stderr.write("\n"); + process.stderr.write( + renderTypePatchWarnings(patches, result.sourceTexts ?? new Map(), { color }) + "\n", + ); + } + if (divergences.length > 0) { + if (config.length > 0 || patches.length > 0) process.stderr.write("\n"); process.stderr.write( renderWarnings(divergences, result.sourceTexts ?? new Map(), { color }) + "\n", ); + } if (performance.length > 0) { - if (divergences.length > 0) process.stderr.write("\n"); + if (divergences.length > 0 || config.length > 0 || patches.length > 0) + process.stderr.write("\n"); process.stderr.write(renderPerformanceWarnings(performance) + "\n"); } } if (result.artifact.kind === "exe") { + // How the profile matched the program's functions; `run` keeps stderr + // for the program's own output. + const profileUse = values["profile-use"]; + if ( + command === "build" && + profileUse !== undefined && + result.artifact.profileMatch !== undefined + ) + process.stderr.write(renderNativeProfileMatch(profileUse, result.artifact.profileMatch)); if (!values["keep-llvm"]) rmSync(result.artifact.translationUnitPath, { force: true }); } else if (result.artifact.kind === "obj") { nativeLinkInfo = result.artifact.nativeLinkInfo; diff --git a/packages/compiler/src/cli/host.ts b/packages/compiler/src/cli/host.ts index 7a927ee39..eafe1882b 100644 --- a/packages/compiler/src/cli/host.ts +++ b/packages/compiler/src/cli/host.ts @@ -1,3 +1,7 @@ +import type { + TypePatchCommandRequest, + TypePatchCommandResult, +} from "../frontend/type-patches/command.js"; import type { AnalyzeOptions, AnalyzeResult, @@ -30,5 +34,7 @@ export interface CliHost { /** Merges raw or indexed profiles into one indexed profile; a sparse one * leaves out functions that never ran. */ mergeProfiles: (outputPath: string, inputPaths: string[], sparse: boolean) => Promise; + /** `scriptc types patch|list` (frontend/type-patches/command.ts). */ + typePatches: (request: TypePatchCommandRequest) => Promise; run: (binary: string) => Promise; } diff --git a/packages/compiler/src/cli/output-options.ts b/packages/compiler/src/cli/output-options.ts index eb8f9a8e6..fb68eb050 100644 --- a/packages/compiler/src/cli/output-options.ts +++ b/packages/compiler/src/cli/output-options.ts @@ -11,6 +11,7 @@ export interface OutputOptionValues { strip?: boolean; windowsSubsystem?: string; detachedExit?: boolean; + cpuDispatch?: boolean; profileGenerate?: string; profileUse?: string; ffi?: string; @@ -75,6 +76,9 @@ export function resolveOutputOptions( if (values.detachedExit && emit !== "exe") { return { ok: false, message: `--detached-exit is only supported with --emit=exe` }; } + if (values.cpuDispatch && emit !== "exe") { + return { ok: false, message: `--cpu-dispatch is only supported with --emit=exe` }; + } if (values.profileGenerate !== undefined && values.profileUse !== undefined) { return { ok: false, message: `--profile-generate and --profile-use cannot be combined` }; } diff --git a/packages/compiler/src/cli/usage.ts b/packages/compiler/src/cli/usage.ts index 0b7457ba0..84cd744b3 100644 --- a/packages/compiler/src/cli/usage.ts +++ b/packages/compiler/src/cli/usage.ts @@ -13,6 +13,13 @@ Usage: exporting the profile symbols; a profile with a sidecar section also gets the contract sidecar JSON beside the archive + scriptc types patch [--range ] [--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 [--sparse] @@ -31,7 +38,7 @@ Options: (native-link-info implies --emit=obj and never links; diagnostics prints the versioned JSON diagnostics envelope for build and coverage) - --fail-on + --fail-on coverage only: exit 1 when the entry path has blockers (a build would fail), or also when it has Node divergences, likely-hot functions that use untyped @@ -61,6 +68,18 @@ Options: load at run time, or beside to copy it next to the executable (default: SCRIPTC_LIBNODE at build time; SCRIPTC_LIBNODE overrides it at run time) + --target-cpu + x86-64: compile the program's code for a CPU level + (default: baseline, which runs on any x86-64 CPU). The + executable then requires that level: v3 adds AVX2, BMI2 + and FMA, v4 adds AVX-512. native uses the build host's + level. The runtime keeps its baseline code + --cpu-dispatch x86-64: also compile the program's code for x86-64-v3 + (AVX2), and run that copy on CPUs that support it. The + executable still runs on any x86-64 CPU. Not used with + --target-cpu above baseline. The SCRIPTC_CPU_LEVEL + environment variable (0 to 4, or a level name) caps the + level at run time (opt in; --emit=exe only) --profile-generate Linux: instrument the executable for profile-guided optimization. Each run writes a raw profile to the @@ -72,8 +91,10 @@ Options: 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 one (release or speed; --emit=exe - only) + optimized as without one. build prints how the profile + matched on stderr, and warns when more than 10% of the + functions it describes changed (release or speed; + --emit=exe only) --keep-llvm keep generated LLVM IR beside the executable (default) --no-keep-llvm delete generated LLVM IR after compiling --emit-ir also write IR beside an executable or library archive; @@ -88,6 +109,10 @@ Options: "pkg..." also names every package pkg depends on). Packages that fail preflight are refused with a diagnostic. Eligible dependencies compile automatically. + --no-shape-overlay + compile npm packages' JavaScript without the types shape + inference proves (per package: "scriptc": {"packages": + {"": {"shapes": false}}} in package.json) --provenance-sources EXPERIMENTAL: compile npm dependencies from their provenance-attested SOURCE (fetched at the attested @@ -153,14 +178,19 @@ export const CLI_OPTIONS = { "windows-subsystem": { type: "string" }, "detached-exit": { type: "boolean", default: false }, libnode: { type: "string" }, + "target-cpu": { type: "string" }, + "cpu-dispatch": { type: "boolean" }, "profile-generate": { type: "string" }, "profile-use": { type: "string" }, sparse: { type: "boolean", default: false }, + range: { type: "string" }, + "dry-run": { type: "boolean", default: false }, "keep-llvm": { type: "boolean", default: true }, "emit-ir": { type: "boolean", default: false }, sanitize: { type: "boolean", default: false }, ffi: { type: "string" }, "npm-static": { type: "string", multiple: true }, + "shape-overlay": { type: "boolean" }, "provenance-sources": { type: "boolean", default: false }, "external-types": { type: "string", multiple: true }, tier: { type: "string" }, diff --git a/packages/compiler/src/compat/fetch-profile.ts b/packages/compiler/src/compat/fetch-profile.ts index c4d1e3f50..03d261aff 100644 --- a/packages/compiler/src/compat/fetch-profile.ts +++ b/packages/compiler/src/compat/fetch-profile.ts @@ -155,494 +155,104 @@ const typedInterfaceUnsupported = "typed source has no compiler bridge for this const metadataExclusion = "WebIDL brand metadata is observable reflection, not an executable compatibility operation"; -export const NODE24_FETCH_COMPAT_PROFILE = { - schemaVersion: 1, - target: { - node: "24.22.0", - undici: "7.29.1", - }, - requestInit: [ - { - id: "stdlib.fetch.request-init.method", - name: "RequestInit.method", - conversion: "WebIDL ByteString after all call arguments evaluate", - evidence: [fixture("static-coercion")], +function buildFetchCompatProfile() { + return { + schemaVersion: 1, + target: { + node: "24.22.0", + undici: "7.29.1", }, - { - id: "stdlib.fetch.request-init.headers", - name: "RequestInit.headers", - conversion: "Headers, record, or sequence-of-pairs snapshot", - evidence: [fixture("static"), fixture("static-coercion")], - }, - { - id: "stdlib.fetch.request-init.body", - name: "RequestInit.body", - conversion: "string, Uint8Array, ReadableStream, or null", - evidence: [fixture("static"), fixture("static-stream"), fixture("static-coercion")], - }, - { - id: "stdlib.fetch.request-init.duplex", - name: "RequestInit.duplex", - conversion: "WebIDL enum; 'half' required for streaming bodies", - evidence: [fixture("static-stream"), fixture("static-coercion")], - }, - { - id: "stdlib.fetch.request-init.redirect", - name: "RequestInit.redirect", - conversion: "WebIDL enum: follow, error, or manual", - evidence: [fixture("static"), fixture("static-coercion")], - }, - { - id: "stdlib.fetch.request-init.signal", - name: "RequestInit.signal", - conversion: "native AbortSignal handle or absent", - evidence: [fixture("static"), fixture("static-abort-throw")], - }, - ], - responseInit: [ - { - id: "stdlib.response-init.headers", - name: "ResponseInit.headers", - conversion: "Headers, record, or sequence-of-pairs snapshot", - evidence: [fixture("static")], - }, - { - id: "stdlib.response-init.status", - name: "ResponseInit.status", - conversion: "WebIDL unsigned-short conversion followed by the 200–599 range check", - evidence: [fixture("static")], - }, - { - id: "stdlib.response-init.statusText", - name: "ResponseInit.statusText", - conversion: "WebIDL ByteString with HTTP reason-phrase validation", - evidence: [fixture("static")], - }, - ], - members: { - responseReads: [ - "ok", - "status", - "statusText", - "url", - "redirected", - "headers", - "body", - "bodyUsed", - ], - responseCalls: ["json", "text", "bytes"], - readableStreamReads: ["locked"], - readableStreamCalls: ["cancel", "getReader"], - }, - operations: [ - { - id: "stdlib.headers.constructor", - name: "Headers constructor", - kind: "constructor", - facets: ["webidl-conversion", "mutation", "identity"], - scope: "Record and iterable header pairs, including copying native Headers", - evidence: [fixture("static-request")], - }, - { - id: "stdlib.request.constructor", - name: "Request constructor", - kind: "constructor", - facets: ["webidl-conversion", "body-consumption", "error-shape"], - scope: - "Absolute URLs with method, headers, body, duplex, redirect, and signal; other RequestInit members retain explicit fences", - evidence: [fixture("static-request")], - }, - ...[ - "method", - "url", - "headers", - "destination", - "referrer", - "referrerPolicy", - "mode", - "credentials", - "cache", - "redirect", - "integrity", - "keepalive", - "isReloadNavigation", - "isHistoryNavigation", - "signal", - "body", - "bodyUsed", - "duplex", - ].map((member): FetchCompatOperation => ({ - id: `stdlib.request.${member}`, - name: `Request.${member}`, - kind: "property", - facets: ["property-read", "state-machine"], - scope: "Reads on native Requests within the constructor's supported RequestInit subset", - evidence: [fixture("static-request")], - })), - ...["arrayBuffer", "text", "json", "bytes"].map((member): FetchCompatOperation => ({ - id: `stdlib.request.${member}`, - name: `Request.${member}`, - kind: "method", - facets: ["body-consumption", "promise-settlement", "state-machine", "error-shape"], - evidence: [fixture("static-request")], - })), - { - id: "stdlib.response.constructor", - name: "Response constructor", - kind: "constructor", - facets: ["webidl-conversion", "body-consumption", "state-machine", "error-shape"], - scope: - "BodyInit is string, Uint8Array/Buffer, ReadableStream, null/undefined, or a checked-dynamic value that follows the supported WebIDL string conversion; ResponseInit is headers/status/statusText", - evidence: [fixture("static")], - }, - { - id: "stdlib.fetch", - name: "fetch", - kind: "function", - facets: [ - "argument-evaluation", - "webidl-conversion", - "surplus-arguments", - "promise-settlement", - "transport", - "error-shape", - ], - evidence: [fixture("static"), fixture("static-coercion"), fixture("static-network-error")], - }, - { - id: "stdlib.abort-controller.constructor", - name: "AbortController constructor", - kind: "constructor", - facets: ["argument-evaluation", "identity", "state-machine", "surplus-arguments"], - evidence: [fixture("static-controller")], - }, - { - id: "stdlib.abort-controller.signal", - name: "AbortController.signal", - kind: "property", - facets: ["identity", "property-read", "state-machine"], - evidence: [fixture("static-controller")], - }, - { - id: "stdlib.abort-controller.abort", - name: "AbortController.abort", - kind: "method", - facets: ["callback-order", "identity", "liveness", "state-machine", "surplus-arguments"], - evidence: [fixture("static-controller")], - }, - { - id: "stdlib.abort-signal.abort", - name: "AbortSignal.abort", - kind: "static-method", - facets: ["identity", "liveness", "surplus-arguments", "property-read"], - evidence: [generated("webidl-operations"), fixture("static-stream-this")], - }, - { - id: "stdlib.abort-signal.any", - name: "AbortSignal.any", - kind: "static-method", - facets: ["webidl-conversion", "missing-arguments", "surplus-arguments", "property-read"], - evidence: [generated("webidl-operations"), fixture("static-stream-this")], - }, - { - id: "stdlib.abort-signal.timeout", - name: "AbortSignal.timeout", - kind: "static-method", - facets: ["webidl-conversion", "missing-arguments", "surplus-arguments", "error-shape"], - evidence: [generated("webidl-operations"), fixture("static-stream-this")], - }, - { - id: "stdlib.abort-signal.aborted", - name: "AbortSignal.aborted", - kind: "property", - facets: ["property-read"], - evidence: [generated("webidl-operations")], - }, - { - id: "stdlib.abort-signal.reason", - name: "AbortSignal.reason", - kind: "property", - facets: ["identity", "liveness", "property-read"], - evidence: [generated("webidl-operations"), fixture("static-stream-this")], - }, - { - id: "stdlib.abort-signal.onabort", - name: "AbortSignal.onabort", - kind: "property", - facets: ["callback-order", "callback-this", "mutation", "property-read"], - evidence: [generated("abort-events"), fixture("static-listener-this")], - }, - { - id: "stdlib.abort-signal.throw-if-aborted", - name: "AbortSignal.throwIfAborted", - kind: "method", - facets: ["identity", "error-shape"], - evidence: [generated("webidl-operations"), fixture("static-abort-throw")], - }, - { - id: "stdlib.abort-signal.add-event-listener", - name: "AbortSignal.addEventListener", - kind: "method", - facets: ["webidl-conversion", "identity", "callback-order", "callback-this"], - evidence: [ - generated("abort-events"), - fixture("static-listener-this"), - fixture("static-listener-noncallable"), - ], - }, - { - id: "stdlib.abort-signal.remove-event-listener", - name: "AbortSignal.removeEventListener", - kind: "method", - facets: ["webidl-conversion", "identity", "callback-order"], - evidence: [generated("abort-events"), fixture("static-stream-this")], - }, - { - id: "stdlib.abort-signal.dispatch-event", - name: "AbortSignal.dispatchEvent", - kind: "method", - facets: ["callback-order", "callback-this", "error-shape"], - evidence: [fixture("static-dispatch-throw")], - }, - { - id: "stdlib.readable-stream.constructor", - name: "ReadableStream constructor", - kind: "constructor", - facets: ["webidl-conversion", "callback-this", "callback-order", "state-machine"], - evidence: [generated("stream-traces"), fixture("static-stream-this")], - }, - { - id: "stdlib.readable-stream.from", - name: "ReadableStream.from", - kind: "static-method", - facets: ["webidl-conversion", "missing-arguments", "surplus-arguments", "liveness"], - evidence: [generated("webidl-operations"), generated("stream-traces")], - }, - { - id: "stdlib.readable-stream.locked", - name: "ReadableStream.locked", - kind: "property", - facets: ["property-read", "state-machine"], - evidence: [generated("stream-traces")], - }, - { - id: "stdlib.readable-stream.cancel", - name: "ReadableStream.cancel", - kind: "method", - facets: ["identity", "promise-settlement", "state-machine"], - evidence: [generated("stream-traces"), fixture("static-stream")], - }, - { - id: "stdlib.readable-stream.get-reader", - name: "ReadableStream.getReader", - kind: "method", - facets: ["webidl-conversion", "identity", "state-machine", "error-shape"], - evidence: [generated("stream-traces"), fixture("static-stream")], - }, - { - id: "stdlib.readable-stream-default-reader.closed", - name: "ReadableStreamDefaultReader.closed", - kind: "property", - facets: ["promise-settlement", "property-read", "state-machine"], - evidence: [generated("stream-traces"), fixture("static-stream")], - }, - { - id: "stdlib.readable-stream-default-reader.read", - name: "ReadableStreamDefaultReader.read", - kind: "method", - facets: ["identity", "promise-settlement", "state-machine"], - evidence: [generated("stream-traces"), fixture("static-stream")], - }, - { - id: "stdlib.readable-stream-default-reader.cancel", - name: "ReadableStreamDefaultReader.cancel", - kind: "method", - facets: ["identity", "promise-settlement", "state-machine"], - evidence: [generated("stream-traces"), fixture("static-stream")], - }, - { - id: "stdlib.readable-stream-default-reader.release-lock", - name: "ReadableStreamDefaultReader.releaseLock", - kind: "method", - facets: ["promise-settlement", "state-machine", "error-shape"], - evidence: [generated("stream-traces"), fixture("static-stream")], - }, - { - id: "stdlib.readable-stream-default-controller.desired-size", - name: "ReadableStreamDefaultController.desiredSize", - kind: "property", - facets: ["property-read", "state-machine"], - evidence: [generated("stream-traces")], - }, - { - id: "stdlib.readable-stream-default-controller.enqueue", - name: "ReadableStreamDefaultController.enqueue", - kind: "method", - facets: ["identity", "liveness", "state-machine", "error-shape"], - evidence: [generated("stream-traces"), fixture("static-stream")], - }, - { - id: "stdlib.readable-stream-default-controller.close", - name: "ReadableStreamDefaultController.close", - kind: "method", - facets: ["state-machine", "error-shape"], - evidence: [generated("stream-traces"), fixture("static-stream")], - }, - { - id: "stdlib.readable-stream-default-controller.error", - name: "ReadableStreamDefaultController.error", - kind: "method", - facets: ["identity", "promise-settlement", "state-machine"], - evidence: [generated("stream-traces"), fixture("static-stream")], - }, - ...["append", "delete", "get", "getSetCookie", "has", "set", "forEach"].map( - (member): FetchCompatOperation => ({ - id: `stdlib.headers.${member}`, - name: `Headers.${member}`, - kind: "method", - facets: - member === "forEach" - ? ["callback-order", "callback-this", "mutation"] - : member === "get" || member === "has" - ? ["webidl-conversion", "missing-arguments", "property-read"] - : ["webidl-conversion", "mutation", "error-shape"], + requestInit: [ + { + id: "stdlib.fetch.request-init.method", + name: "RequestInit.method", + conversion: "WebIDL ByteString after all call arguments evaluate", + evidence: [fixture("static-coercion")], + }, + { + id: "stdlib.fetch.request-init.headers", + name: "RequestInit.headers", + conversion: "Headers, record, or sequence-of-pairs snapshot", evidence: [fixture("static"), fixture("static-coercion")], - }), - ), - ...["ok", "status", "statusText", "url", "redirected", "headers", "body", "bodyUsed"].map( - (member): FetchCompatOperation => ({ - id: `stdlib.response.${member}`, - name: `Response.${member}`, - kind: "property", - facets: ["property-read"], - evidence: [fixture("static")], - }), - ), - { - id: "stdlib.response.static.json", - name: "Response.json", - kind: "static-method", - facets: ["webidl-conversion", "error-shape"], - scope: - "data that crosses the checked-dynamic boundary, serialized with JSON.stringify (toJSON hooks included); ResponseInit as for the constructor", - evidence: [fixture("static-response-json")], - }, - ...["json", "text", "bytes", "arrayBuffer"].map((member): FetchCompatOperation => ({ - id: `stdlib.response.${member}`, - name: `Response.${member}`, - kind: "method", - facets: ["body-consumption", "promise-settlement", "state-machine", "error-shape"], - evidence: - member === "arrayBuffer" - ? [fixture("static")] - : [fixture("static"), fixture("static-stream")], - })), - ], - inventory: { - interfaces: [ - "AbortController", - "AbortSignal", - "Headers", - "Request", - "Response", - "ReadableStream", - "ReadableStreamDefaultReader", - "ReadableStreamDefaultController", + }, + { + id: "stdlib.fetch.request-init.body", + name: "RequestInit.body", + conversion: "string, Uint8Array, ReadableStream, or null", + evidence: [fixture("static"), fixture("static-stream"), fixture("static-coercion")], + }, + { + id: "stdlib.fetch.request-init.duplex", + name: "RequestInit.duplex", + conversion: "WebIDL enum; 'half' required for streaming bodies", + evidence: [fixture("static-stream"), fixture("static-coercion")], + }, + { + id: "stdlib.fetch.request-init.redirect", + name: "RequestInit.redirect", + conversion: "WebIDL enum: follow, error, or manual", + evidence: [fixture("static"), fixture("static-coercion")], + }, + { + id: "stdlib.fetch.request-init.signal", + name: "RequestInit.signal", + conversion: "native AbortSignal handle or absent", + evidence: [fixture("static"), fixture("static-abort-throw")], + }, ], - entries: [ - staticEntry("stdlib.fetch", "globalThis", "fetch", "global"), - - staticEntry( - "stdlib.abort-controller.constructor", - "AbortController", - "constructor", - "constructor", - ), - staticEntry("stdlib.abort-controller.signal", "AbortController", "signal", "prototype"), - staticEntry("stdlib.abort-controller.abort", "AbortController", "abort", "prototype"), - outOfScopeEntry( - "stdlib.abort-controller.symbol.toStringTag", - "AbortController", - "[Symbol.toStringTag]", - "prototype-symbol", - metadataExclusion, - ), - - unsupportedEntry( - "stdlib.abort-signal.constructor", - "AbortSignal", - "constructor", - "constructor", - "Node exposes the interface object but constructing it throws; the compiler does not preserve that constructor behavior", - ), - staticEntry("stdlib.abort-signal.abort", "AbortSignal", "abort", "static"), - staticEntry("stdlib.abort-signal.timeout", "AbortSignal", "timeout", "static"), - staticEntry("stdlib.abort-signal.any", "AbortSignal", "any", "static"), - staticEntry("stdlib.abort-signal.aborted", "AbortSignal", "aborted", "prototype"), - staticEntry("stdlib.abort-signal.reason", "AbortSignal", "reason", "prototype"), - staticEntry( - "stdlib.abort-signal.throw-if-aborted", - "AbortSignal", - "throwIfAborted", - "prototype", - ), - staticEntry("stdlib.abort-signal.onabort", "AbortSignal", "onabort", "prototype"), - staticEntry( - "stdlib.abort-signal.add-event-listener", - "AbortSignal", - "addEventListener", - "prototype-inherited", - ), - staticEntry( - "stdlib.abort-signal.remove-event-listener", - "AbortSignal", - "removeEventListener", - "prototype-inherited", - ), - staticEntry( - "stdlib.abort-signal.dispatch-event", - "AbortSignal", - "dispatchEvent", - "prototype-inherited", - ), - outOfScopeEntry( - "stdlib.abort-signal.symbol.toStringTag", - "AbortSignal", - "[Symbol.toStringTag]", - "prototype-symbol", - metadataExclusion, - ), - - staticEntry("stdlib.headers.constructor", "Headers", "constructor", "constructor"), - ...["append", "delete", "get", "has", "set", "getSetCookie"].map((member) => - staticEntry(`stdlib.headers.${member}`, "Headers", member, "prototype"), - ), - ...["keys", "values", "entries"].map((member) => - unsupportedEntry( - `stdlib.headers.${member}`, - "Headers", - member, - "prototype", - "native Headers iteration does not yet expose a static iterator handle", - ), - ), - staticEntry("stdlib.headers.forEach", "Headers", "forEach", "prototype"), - unsupportedEntry( - "stdlib.headers.symbol.iterator", - "Headers", - "[Symbol.iterator]", - "prototype-symbol", - "native Headers iteration does not expose an engine-free iterator handle", - ), - outOfScopeEntry( - "stdlib.headers.symbol.toStringTag", - "Headers", - "[Symbol.toStringTag]", - "prototype-symbol", - metadataExclusion, - ), - - staticEntry("stdlib.request.constructor", "Request", "constructor", "constructor"), + responseInit: [ + { + id: "stdlib.response-init.headers", + name: "ResponseInit.headers", + conversion: "Headers, record, or sequence-of-pairs snapshot", + evidence: [fixture("static")], + }, + { + id: "stdlib.response-init.status", + name: "ResponseInit.status", + conversion: "WebIDL unsigned-short conversion followed by the 200–599 range check", + evidence: [fixture("static")], + }, + { + id: "stdlib.response-init.statusText", + name: "ResponseInit.statusText", + conversion: "WebIDL ByteString with HTTP reason-phrase validation", + evidence: [fixture("static")], + }, + ], + members: { + responseReads: [ + "ok", + "status", + "statusText", + "url", + "redirected", + "headers", + "body", + "bodyUsed", + ], + responseCalls: ["json", "text", "bytes"], + readableStreamReads: ["locked"], + readableStreamCalls: ["cancel", "getReader"], + }, + operations: [ + { + id: "stdlib.headers.constructor", + name: "Headers constructor", + kind: "constructor", + facets: ["webidl-conversion", "mutation", "identity"], + scope: "Record and iterable header pairs, including copying native Headers", + evidence: [fixture("static-request")], + }, + { + id: "stdlib.request.constructor", + name: "Request constructor", + kind: "constructor", + facets: ["webidl-conversion", "body-consumption", "error-shape"], + scope: + "Absolute URLs with method, headers, body, duplex, redirect, and signal; other RequestInit members retain explicit fences", + evidence: [fixture("static-request")], + }, ...[ "method", "url", @@ -662,276 +272,695 @@ export const NODE24_FETCH_COMPAT_PROFILE = { "body", "bodyUsed", "duplex", - "clone", - "blob", - "arrayBuffer", - "text", - "json", - "formData", - "bytes", - "attribute", - ].map((member) => - ["clone", "blob", "formData", "attribute"].includes(member) - ? unsupportedEntry( - `stdlib.request.${member}`, - "Request", - member, - "prototype", - typedInterfaceUnsupported, - ) - : staticEntry(`stdlib.request.${member}`, "Request", member, "prototype"), + ].map((member): FetchCompatOperation => ({ + id: `stdlib.request.${member}`, + name: `Request.${member}`, + kind: "property", + facets: ["property-read", "state-machine"], + scope: "Reads on native Requests within the constructor's supported RequestInit subset", + evidence: [fixture("static-request")], + })), + ...["arrayBuffer", "text", "json", "bytes"].map((member): FetchCompatOperation => ({ + id: `stdlib.request.${member}`, + name: `Request.${member}`, + kind: "method", + facets: ["body-consumption", "promise-settlement", "state-machine", "error-shape"], + evidence: [fixture("static-request")], + })), + { + id: "stdlib.response.constructor", + name: "Response constructor", + kind: "constructor", + facets: ["webidl-conversion", "body-consumption", "state-machine", "error-shape"], + scope: + "BodyInit is string, Uint8Array/Buffer, ReadableStream, null/undefined, or a checked-dynamic value that follows the supported WebIDL string conversion; ResponseInit is headers/status/statusText", + evidence: [fixture("static")], + }, + { + id: "stdlib.fetch", + name: "fetch", + kind: "function", + facets: [ + "argument-evaluation", + "webidl-conversion", + "surplus-arguments", + "promise-settlement", + "transport", + "error-shape", + ], + evidence: [fixture("static"), fixture("static-coercion"), fixture("static-network-error")], + }, + { + id: "stdlib.abort-controller.constructor", + name: "AbortController constructor", + kind: "constructor", + facets: ["argument-evaluation", "identity", "state-machine", "surplus-arguments"], + evidence: [fixture("static-controller")], + }, + { + id: "stdlib.abort-controller.signal", + name: "AbortController.signal", + kind: "property", + facets: ["identity", "property-read", "state-machine"], + evidence: [fixture("static-controller")], + }, + { + id: "stdlib.abort-controller.abort", + name: "AbortController.abort", + kind: "method", + facets: ["callback-order", "identity", "liveness", "state-machine", "surplus-arguments"], + evidence: [fixture("static-controller")], + }, + { + id: "stdlib.abort-signal.abort", + name: "AbortSignal.abort", + kind: "static-method", + facets: ["identity", "liveness", "surplus-arguments", "property-read"], + evidence: [generated("webidl-operations"), fixture("static-stream-this")], + }, + { + id: "stdlib.abort-signal.any", + name: "AbortSignal.any", + kind: "static-method", + facets: ["webidl-conversion", "missing-arguments", "surplus-arguments", "property-read"], + evidence: [generated("webidl-operations"), fixture("static-stream-this")], + }, + { + id: "stdlib.abort-signal.timeout", + name: "AbortSignal.timeout", + kind: "static-method", + facets: ["webidl-conversion", "missing-arguments", "surplus-arguments", "error-shape"], + evidence: [generated("webidl-operations"), fixture("static-stream-this")], + }, + { + id: "stdlib.abort-signal.aborted", + name: "AbortSignal.aborted", + kind: "property", + facets: ["property-read"], + evidence: [generated("webidl-operations")], + }, + { + id: "stdlib.abort-signal.reason", + name: "AbortSignal.reason", + kind: "property", + facets: ["identity", "liveness", "property-read"], + evidence: [generated("webidl-operations"), fixture("static-stream-this")], + }, + { + id: "stdlib.abort-signal.onabort", + name: "AbortSignal.onabort", + kind: "property", + facets: ["callback-order", "callback-this", "mutation", "property-read"], + evidence: [generated("abort-events"), fixture("static-listener-this")], + }, + { + id: "stdlib.abort-signal.throw-if-aborted", + name: "AbortSignal.throwIfAborted", + kind: "method", + facets: ["identity", "error-shape"], + evidence: [generated("webidl-operations"), fixture("static-abort-throw")], + }, + { + id: "stdlib.abort-signal.add-event-listener", + name: "AbortSignal.addEventListener", + kind: "method", + facets: ["webidl-conversion", "identity", "callback-order", "callback-this"], + evidence: [ + generated("abort-events"), + fixture("static-listener-this"), + fixture("static-listener-noncallable"), + ], + }, + { + id: "stdlib.abort-signal.remove-event-listener", + name: "AbortSignal.removeEventListener", + kind: "method", + facets: ["webidl-conversion", "identity", "callback-order"], + evidence: [generated("abort-events"), fixture("static-stream-this")], + }, + { + id: "stdlib.abort-signal.dispatch-event", + name: "AbortSignal.dispatchEvent", + kind: "method", + facets: ["callback-order", "callback-this", "error-shape"], + evidence: [fixture("static-dispatch-throw")], + }, + { + id: "stdlib.readable-stream.constructor", + name: "ReadableStream constructor", + kind: "constructor", + facets: ["webidl-conversion", "callback-this", "callback-order", "state-machine"], + evidence: [generated("stream-traces"), fixture("static-stream-this")], + }, + { + id: "stdlib.readable-stream.from", + name: "ReadableStream.from", + kind: "static-method", + facets: ["webidl-conversion", "missing-arguments", "surplus-arguments", "liveness"], + evidence: [generated("webidl-operations"), generated("stream-traces")], + }, + { + id: "stdlib.readable-stream.locked", + name: "ReadableStream.locked", + kind: "property", + facets: ["property-read", "state-machine"], + evidence: [generated("stream-traces")], + }, + { + id: "stdlib.readable-stream.cancel", + name: "ReadableStream.cancel", + kind: "method", + facets: ["identity", "promise-settlement", "state-machine"], + evidence: [generated("stream-traces"), fixture("static-stream")], + }, + { + id: "stdlib.readable-stream.get-reader", + name: "ReadableStream.getReader", + kind: "method", + facets: ["webidl-conversion", "identity", "state-machine", "error-shape"], + evidence: [generated("stream-traces"), fixture("static-stream")], + }, + { + id: "stdlib.readable-stream-default-reader.closed", + name: "ReadableStreamDefaultReader.closed", + kind: "property", + facets: ["promise-settlement", "property-read", "state-machine"], + evidence: [generated("stream-traces"), fixture("static-stream")], + }, + { + id: "stdlib.readable-stream-default-reader.read", + name: "ReadableStreamDefaultReader.read", + kind: "method", + facets: ["identity", "promise-settlement", "state-machine"], + evidence: [generated("stream-traces"), fixture("static-stream")], + }, + { + id: "stdlib.readable-stream-default-reader.cancel", + name: "ReadableStreamDefaultReader.cancel", + kind: "method", + facets: ["identity", "promise-settlement", "state-machine"], + evidence: [generated("stream-traces"), fixture("static-stream")], + }, + { + id: "stdlib.readable-stream-default-reader.release-lock", + name: "ReadableStreamDefaultReader.releaseLock", + kind: "method", + facets: ["promise-settlement", "state-machine", "error-shape"], + evidence: [generated("stream-traces"), fixture("static-stream")], + }, + { + id: "stdlib.readable-stream-default-controller.desired-size", + name: "ReadableStreamDefaultController.desiredSize", + kind: "property", + facets: ["property-read", "state-machine"], + evidence: [generated("stream-traces")], + }, + { + id: "stdlib.readable-stream-default-controller.enqueue", + name: "ReadableStreamDefaultController.enqueue", + kind: "method", + facets: ["identity", "liveness", "state-machine", "error-shape"], + evidence: [generated("stream-traces"), fixture("static-stream")], + }, + { + id: "stdlib.readable-stream-default-controller.close", + name: "ReadableStreamDefaultController.close", + kind: "method", + facets: ["state-machine", "error-shape"], + evidence: [generated("stream-traces"), fixture("static-stream")], + }, + { + id: "stdlib.readable-stream-default-controller.error", + name: "ReadableStreamDefaultController.error", + kind: "method", + facets: ["identity", "promise-settlement", "state-machine"], + evidence: [generated("stream-traces"), fixture("static-stream")], + }, + ...["append", "delete", "get", "getSetCookie", "has", "set", "forEach"].map( + (member): FetchCompatOperation => ({ + id: `stdlib.headers.${member}`, + name: `Headers.${member}`, + kind: "method", + facets: + member === "forEach" + ? ["callback-order", "callback-this", "mutation"] + : member === "get" || member === "has" + ? ["webidl-conversion", "missing-arguments", "property-read"] + : ["webidl-conversion", "mutation", "error-shape"], + evidence: [fixture("static"), fixture("static-coercion")], + }), ), - outOfScopeEntry( - "stdlib.request.symbol.toStringTag", + ...["ok", "status", "statusText", "url", "redirected", "headers", "body", "bodyUsed"].map( + (member): FetchCompatOperation => ({ + id: `stdlib.response.${member}`, + name: `Response.${member}`, + kind: "property", + facets: ["property-read"], + evidence: [fixture("static")], + }), + ), + { + id: "stdlib.response.static.json", + name: "Response.json", + kind: "static-method", + facets: ["webidl-conversion", "error-shape"], + scope: + "data that crosses the checked-dynamic boundary, serialized with JSON.stringify (toJSON hooks included); ResponseInit as for the constructor", + evidence: [fixture("static-response-json")], + }, + ...["json", "text", "bytes", "arrayBuffer"].map((member): FetchCompatOperation => ({ + id: `stdlib.response.${member}`, + name: `Response.${member}`, + kind: "method", + facets: ["body-consumption", "promise-settlement", "state-machine", "error-shape"], + evidence: + member === "arrayBuffer" + ? [fixture("static")] + : [fixture("static"), fixture("static-stream")], + })), + ], + inventory: { + interfaces: [ + "AbortController", + "AbortSignal", + "Headers", "Request", - "[Symbol.toStringTag]", - "prototype-symbol", - metadataExclusion, - ), - - staticEntry("stdlib.response.constructor", "Response", "constructor", "constructor"), - staticEntry("stdlib.response.static.json", "Response", "json", "static"), - ...["error", "redirect"].map((member) => - unsupportedEntry( - `stdlib.response.static.${member}`, - "Response", - member, - "static", - "Response static constructor-object operations have no compiler lowering", - ), - ), - unsupportedEntry("stdlib.response.type", "Response", "type", "prototype", widerMemberFence), - ...["url", "redirected", "status", "ok", "statusText", "headers", "body", "bodyUsed"].map( - (member) => staticEntry(`stdlib.response.${member}`, "Response", member, "prototype"), - ), - staticEntry("stdlib.response.arrayBuffer", "Response", "arrayBuffer", "prototype"), - ...["clone", "blob", "formData"].map((member) => - unsupportedEntry( - `stdlib.response.${member}`, - "Response", - member, - "prototype", - "the native fetch runtime does not implement this Response operation", - ), - ), - ...["text", "json"].map((member) => - staticEntry(`stdlib.response.${member}`, "Response", member, "prototype"), - ), - staticEntry("stdlib.response.bytes", "Response", "bytes", "prototype"), - outOfScopeEntry( - "stdlib.response.symbol.toStringTag", "Response", - "[Symbol.toStringTag]", - "prototype-symbol", - metadataExclusion, - ), - - staticEntry( - "stdlib.readable-stream.constructor", "ReadableStream", - "constructor", - "constructor", - ), - staticEntry("stdlib.readable-stream.from", "ReadableStream", "from", "static"), - staticEntry("stdlib.readable-stream.locked", "ReadableStream", "locked", "prototype"), - staticEntry("stdlib.readable-stream.cancel", "ReadableStream", "cancel", "prototype"), - staticEntry("stdlib.readable-stream.get-reader", "ReadableStream", "getReader", "prototype"), - ...["pipeThrough", "values"].map((member) => - unsupportedEntry( - `stdlib.readable-stream.${member}`, - "ReadableStream", - member, - "prototype", - member === "values" - ? "direct for-await consumption compiles statically; storing or driving the first-class iterator handle is unsupported" - : "the wider Web Streams graph is outside the native readable-stream slice", + "ReadableStreamDefaultReader", + "ReadableStreamDefaultController", + ], + entries: [ + staticEntry("stdlib.fetch", "globalThis", "fetch", "global"), + + staticEntry( + "stdlib.abort-controller.constructor", + "AbortController", + "constructor", + "constructor", ), - ), - ...["pipeTo", "tee"].map((member) => - unsupportedEntry( - `stdlib.readable-stream.${member}`, - "ReadableStream", - member, - "prototype", - "this operation is outside the native readable-stream implementation", + staticEntry("stdlib.abort-controller.signal", "AbortController", "signal", "prototype"), + staticEntry("stdlib.abort-controller.abort", "AbortController", "abort", "prototype"), + outOfScopeEntry( + "stdlib.abort-controller.symbol.toStringTag", + "AbortController", + "[Symbol.toStringTag]", + "prototype-symbol", + metadataExclusion, ), - ), - unsupportedEntry( - "stdlib.readable-stream.symbol.asyncIterator", - "ReadableStream", - "[Symbol.asyncIterator]", - "prototype-symbol", - "direct for-await consumption compiles statically; explicit symbol access and stored iterator handles have no compiler lowering", - ), - outOfScopeEntry( - "stdlib.readable-stream.symbol.toStringTag", - "ReadableStream", - "[Symbol.toStringTag]", - "prototype-symbol", - metadataExclusion, - ), - unsupportedEntry( - "stdlib.readable-stream-default-reader.constructor", - "ReadableStreamDefaultReader", - "constructor", - "constructor", - "reader handles come from ReadableStream.getReader(); the constructor object is unsupported", - ), - staticEntry( - "stdlib.readable-stream-default-reader.read", - "ReadableStreamDefaultReader", - "read", - "prototype", - ), - staticEntry( - "stdlib.readable-stream-default-reader.release-lock", - "ReadableStreamDefaultReader", - "releaseLock", - "prototype", - ), - staticEntry( - "stdlib.readable-stream-default-reader.closed", - "ReadableStreamDefaultReader", - "closed", - "prototype", - ), - staticEntry( - "stdlib.readable-stream-default-reader.cancel", - "ReadableStreamDefaultReader", - "cancel", - "prototype", - ), - outOfScopeEntry( - "stdlib.readable-stream-default-reader.symbol.toStringTag", - "ReadableStreamDefaultReader", - "[Symbol.toStringTag]", - "prototype-symbol", - metadataExclusion, - ), + unsupportedEntry( + "stdlib.abort-signal.constructor", + "AbortSignal", + "constructor", + "constructor", + "Node exposes the interface object but constructing it throws; the compiler does not preserve that constructor behavior", + ), + staticEntry("stdlib.abort-signal.abort", "AbortSignal", "abort", "static"), + staticEntry("stdlib.abort-signal.timeout", "AbortSignal", "timeout", "static"), + staticEntry("stdlib.abort-signal.any", "AbortSignal", "any", "static"), + staticEntry("stdlib.abort-signal.aborted", "AbortSignal", "aborted", "prototype"), + staticEntry("stdlib.abort-signal.reason", "AbortSignal", "reason", "prototype"), + staticEntry( + "stdlib.abort-signal.throw-if-aborted", + "AbortSignal", + "throwIfAborted", + "prototype", + ), + staticEntry("stdlib.abort-signal.onabort", "AbortSignal", "onabort", "prototype"), + staticEntry( + "stdlib.abort-signal.add-event-listener", + "AbortSignal", + "addEventListener", + "prototype-inherited", + ), + staticEntry( + "stdlib.abort-signal.remove-event-listener", + "AbortSignal", + "removeEventListener", + "prototype-inherited", + ), + staticEntry( + "stdlib.abort-signal.dispatch-event", + "AbortSignal", + "dispatchEvent", + "prototype-inherited", + ), + outOfScopeEntry( + "stdlib.abort-signal.symbol.toStringTag", + "AbortSignal", + "[Symbol.toStringTag]", + "prototype-symbol", + metadataExclusion, + ), - unsupportedEntry( - "stdlib.readable-stream-default-controller.constructor", - "ReadableStreamDefaultController", - "constructor", - "constructor", - "controller handles come from underlying-source callbacks; the constructor object is unsupported", - ), - staticEntry( - "stdlib.readable-stream-default-controller.desired-size", - "ReadableStreamDefaultController", - "desiredSize", - "prototype", - ), - staticEntry( - "stdlib.readable-stream-default-controller.close", - "ReadableStreamDefaultController", - "close", - "prototype", - ), - staticEntry( - "stdlib.readable-stream-default-controller.enqueue", - "ReadableStreamDefaultController", - "enqueue", - "prototype", - ), - staticEntry( - "stdlib.readable-stream-default-controller.error", - "ReadableStreamDefaultController", - "error", - "prototype", - ), - outOfScopeEntry( - "stdlib.readable-stream-default-controller.symbol.toStringTag", - "ReadableStreamDefaultController", - "[Symbol.toStringTag]", - "prototype-symbol", - metadataExclusion, - ), + staticEntry("stdlib.headers.constructor", "Headers", "constructor", "constructor"), + ...["append", "delete", "get", "has", "set", "getSetCookie"].map((member) => + staticEntry(`stdlib.headers.${member}`, "Headers", member, "prototype"), + ), + ...["keys", "values", "entries"].map((member) => + unsupportedEntry( + `stdlib.headers.${member}`, + "Headers", + member, + "prototype", + "native Headers iteration does not yet expose a static iterator handle", + ), + ), + staticEntry("stdlib.headers.forEach", "Headers", "forEach", "prototype"), + unsupportedEntry( + "stdlib.headers.symbol.iterator", + "Headers", + "[Symbol.iterator]", + "prototype-symbol", + "native Headers iteration does not expose an engine-free iterator handle", + ), + outOfScopeEntry( + "stdlib.headers.symbol.toStringTag", + "Headers", + "[Symbol.toStringTag]", + "prototype-symbol", + metadataExclusion, + ), - ...[ - "body", - "cache", - "credentials", - "dispatcher", - "duplex", - "headers", - "integrity", - "keepalive", - "method", - "mode", - "priority", - "redirect", - "referrer", - "referrerPolicy", - "signal", - "window", - ].map((member) => { - const id = `stdlib.fetch.request-init.${member}`; - const supported = new Set(["body", "duplex", "headers", "method", "redirect", "signal"]); - return supported.has(member) - ? staticEntry(id, "RequestInit", member, "dictionary") - : member === "dispatcher" + staticEntry("stdlib.request.constructor", "Request", "constructor", "constructor"), + ...[ + "method", + "url", + "headers", + "destination", + "referrer", + "referrerPolicy", + "mode", + "credentials", + "cache", + "redirect", + "integrity", + "keepalive", + "isReloadNavigation", + "isHistoryNavigation", + "signal", + "body", + "bodyUsed", + "duplex", + "clone", + "blob", + "arrayBuffer", + "text", + "json", + "formData", + "bytes", + "attribute", + ].map((member) => + ["clone", "blob", "formData", "attribute"].includes(member) ? unsupportedEntry( - id, - "RequestInit", + `stdlib.request.${member}`, + "Request", member, - "dictionary", - "custom dispatchers are unsupported; native environment-proxy routing uses NODE_USE_ENV_PROXY", + "prototype", + typedInterfaceUnsupported, ) - : unsupportedEntry( - id, - "RequestInit", - member, - "dictionary", - "the compiler does not preserve this RequestInit member's conversion or transport behavior", - ); - }), - ...["headers", "status", "statusText"].map((member) => - staticEntry(`stdlib.response-init.${member}`, "ResponseInit", member, "dictionary"), - ), - ], - excludedInterfaces: [ - { - name: "Blob/File/FormData bodies", - reason: - "fetch request and response bodies do not accept or produce Blob, File, or FormData values", - }, - { - name: "WritableStream/TransformStream", - reason: - "native support currently covers readable fetch bodies; general Web Streams graphs are unsupported", - }, - { - name: "Readable byte/BYOB streams", - reason: - "the native stream projection currently supports default readers and controllers only", - }, - { - name: "EventSource/WebSocket", - reason: "these are separate protocol clients, not fetch request/response compatibility", - }, - ], - }, -} satisfies FetchCompatProfile; + : staticEntry(`stdlib.request.${member}`, "Request", member, "prototype"), + ), + outOfScopeEntry( + "stdlib.request.symbol.toStringTag", + "Request", + "[Symbol.toStringTag]", + "prototype-symbol", + metadataExclusion, + ), -export const STATIC_RESPONSE_READS = new Set(NODE24_FETCH_COMPAT_PROFILE.members.responseReads); + staticEntry("stdlib.response.constructor", "Response", "constructor", "constructor"), + staticEntry("stdlib.response.static.json", "Response", "json", "static"), + ...["error", "redirect"].map((member) => + unsupportedEntry( + `stdlib.response.static.${member}`, + "Response", + member, + "static", + "Response static constructor-object operations have no compiler lowering", + ), + ), + unsupportedEntry("stdlib.response.type", "Response", "type", "prototype", widerMemberFence), + ...["url", "redirected", "status", "ok", "statusText", "headers", "body", "bodyUsed"].map( + (member) => staticEntry(`stdlib.response.${member}`, "Response", member, "prototype"), + ), + staticEntry("stdlib.response.arrayBuffer", "Response", "arrayBuffer", "prototype"), + ...["clone", "blob", "formData"].map((member) => + unsupportedEntry( + `stdlib.response.${member}`, + "Response", + member, + "prototype", + "the native fetch runtime does not implement this Response operation", + ), + ), + ...["text", "json"].map((member) => + staticEntry(`stdlib.response.${member}`, "Response", member, "prototype"), + ), + staticEntry("stdlib.response.bytes", "Response", "bytes", "prototype"), + outOfScopeEntry( + "stdlib.response.symbol.toStringTag", + "Response", + "[Symbol.toStringTag]", + "prototype-symbol", + metadataExclusion, + ), -export const STATIC_RESPONSE_CALLS = new Set(NODE24_FETCH_COMPAT_PROFILE.members.responseCalls); + staticEntry( + "stdlib.readable-stream.constructor", + "ReadableStream", + "constructor", + "constructor", + ), + staticEntry("stdlib.readable-stream.from", "ReadableStream", "from", "static"), + staticEntry("stdlib.readable-stream.locked", "ReadableStream", "locked", "prototype"), + staticEntry("stdlib.readable-stream.cancel", "ReadableStream", "cancel", "prototype"), + staticEntry( + "stdlib.readable-stream.get-reader", + "ReadableStream", + "getReader", + "prototype", + ), + ...["pipeThrough", "values"].map((member) => + unsupportedEntry( + `stdlib.readable-stream.${member}`, + "ReadableStream", + member, + "prototype", + member === "values" + ? "direct for-await consumption compiles statically; storing or driving the first-class iterator handle is unsupported" + : "the wider Web Streams graph is outside the native readable-stream slice", + ), + ), + ...["pipeTo", "tee"].map((member) => + unsupportedEntry( + `stdlib.readable-stream.${member}`, + "ReadableStream", + member, + "prototype", + "this operation is outside the native readable-stream implementation", + ), + ), + unsupportedEntry( + "stdlib.readable-stream.symbol.asyncIterator", + "ReadableStream", + "[Symbol.asyncIterator]", + "prototype-symbol", + "direct for-await consumption compiles statically; explicit symbol access and stored iterator handles have no compiler lowering", + ), + outOfScopeEntry( + "stdlib.readable-stream.symbol.toStringTag", + "ReadableStream", + "[Symbol.toStringTag]", + "prototype-symbol", + metadataExclusion, + ), -export const STATIC_HEADERS_CALLS = new Set( - NODE24_FETCH_COMPAT_PROFILE.inventory.entries - .filter( - (entry) => - entry.owner === "Headers" && entry.placement === "prototype" && entry.status === "static", - ) - .map((entry) => entry.member), -); + unsupportedEntry( + "stdlib.readable-stream-default-reader.constructor", + "ReadableStreamDefaultReader", + "constructor", + "constructor", + "reader handles come from ReadableStream.getReader(); the constructor object is unsupported", + ), + staticEntry( + "stdlib.readable-stream-default-reader.read", + "ReadableStreamDefaultReader", + "read", + "prototype", + ), + staticEntry( + "stdlib.readable-stream-default-reader.release-lock", + "ReadableStreamDefaultReader", + "releaseLock", + "prototype", + ), + staticEntry( + "stdlib.readable-stream-default-reader.closed", + "ReadableStreamDefaultReader", + "closed", + "prototype", + ), + staticEntry( + "stdlib.readable-stream-default-reader.cancel", + "ReadableStreamDefaultReader", + "cancel", + "prototype", + ), + outOfScopeEntry( + "stdlib.readable-stream-default-reader.symbol.toStringTag", + "ReadableStreamDefaultReader", + "[Symbol.toStringTag]", + "prototype-symbol", + metadataExclusion, + ), -export const STATIC_READABLE_STREAM_READS = new Set( - NODE24_FETCH_COMPAT_PROFILE.members.readableStreamReads, -); + unsupportedEntry( + "stdlib.readable-stream-default-controller.constructor", + "ReadableStreamDefaultController", + "constructor", + "constructor", + "controller handles come from underlying-source callbacks; the constructor object is unsupported", + ), + staticEntry( + "stdlib.readable-stream-default-controller.desired-size", + "ReadableStreamDefaultController", + "desiredSize", + "prototype", + ), + staticEntry( + "stdlib.readable-stream-default-controller.close", + "ReadableStreamDefaultController", + "close", + "prototype", + ), + staticEntry( + "stdlib.readable-stream-default-controller.enqueue", + "ReadableStreamDefaultController", + "enqueue", + "prototype", + ), + staticEntry( + "stdlib.readable-stream-default-controller.error", + "ReadableStreamDefaultController", + "error", + "prototype", + ), + outOfScopeEntry( + "stdlib.readable-stream-default-controller.symbol.toStringTag", + "ReadableStreamDefaultController", + "[Symbol.toStringTag]", + "prototype-symbol", + metadataExclusion, + ), -export const STATIC_READABLE_STREAM_CALLS = new Set( - NODE24_FETCH_COMPAT_PROFILE.members.readableStreamCalls, -); + ...[ + "body", + "cache", + "credentials", + "dispatcher", + "duplex", + "headers", + "integrity", + "keepalive", + "method", + "mode", + "priority", + "redirect", + "referrer", + "referrerPolicy", + "signal", + "window", + ].map((member) => { + const id = `stdlib.fetch.request-init.${member}`; + const supported = new Set(["body", "duplex", "headers", "method", "redirect", "signal"]); + return supported.has(member) + ? staticEntry(id, "RequestInit", member, "dictionary") + : member === "dispatcher" + ? unsupportedEntry( + id, + "RequestInit", + member, + "dictionary", + "custom dispatchers are unsupported; native environment-proxy routing uses NODE_USE_ENV_PROXY", + ) + : unsupportedEntry( + id, + "RequestInit", + member, + "dictionary", + "the compiler does not preserve this RequestInit member's conversion or transport behavior", + ); + }), + ...["headers", "status", "statusText"].map((member) => + staticEntry(`stdlib.response-init.${member}`, "ResponseInit", member, "dictionary"), + ), + ], + excludedInterfaces: [ + { + name: "Blob/File/FormData bodies", + reason: + "fetch request and response bodies do not accept or produce Blob, File, or FormData values", + }, + { + name: "WritableStream/TransformStream", + reason: + "native support covers readable fetch bodies and the identity `new TransformStream()`; the WritableStream constructor, transformers, and general Web Streams graphs are unsupported", + }, + { + name: "Readable byte/BYOB streams", + reason: + "the native stream projection currently supports default readers and controllers only", + }, + { + name: "EventSource/WebSocket", + reason: "these are separate protocol clients, not fetch request/response compatibility", + }, + ], + }, + } satisfies FetchCompatProfile; +} + +let fetchCompatProfile: ReturnType | undefined; + +/** The Node 24 fetch compatibility profile. It is built on the first call, + * so runs that do not lower a module do not construct it. */ +export function node24FetchCompatProfile(): ReturnType { + fetchCompatProfile ??= buildFetchCompatProfile(); + return fetchCompatProfile; +} + +/** The members the native static fetch handles project. */ +export interface StaticFetchMembers { + responseReads: ReadonlySet; + responseCalls: ReadonlySet; + headersCalls: ReadonlySet; + readableStreamReads: ReadonlySet; + readableStreamCalls: ReadonlySet; +} + +let staticFetchMemberSets: StaticFetchMembers | undefined; + +/** The profile's static members as sets, built on the first call. */ +export function staticFetchMembers(): StaticFetchMembers { + if (staticFetchMemberSets !== undefined) return staticFetchMemberSets; + const profile = node24FetchCompatProfile(); + staticFetchMemberSets = { + responseReads: new Set(profile.members.responseReads), + responseCalls: new Set(profile.members.responseCalls), + headersCalls: new Set( + profile.inventory.entries + .filter( + (entry) => + entry.owner === "Headers" && + entry.placement === "prototype" && + entry.status === "static", + ) + .map((entry) => entry.member), + ), + readableStreamReads: new Set(profile.members.readableStreamReads), + readableStreamCalls: new Set(profile.members.readableStreamCalls), + }; + return staticFetchMemberSets; +} diff --git a/packages/compiler/src/compile-types.ts b/packages/compiler/src/compile-types.ts index 2e745f34e..b5a5ac265 100644 --- a/packages/compiler/src/compile-types.ts +++ b/packages/compiler/src/compile-types.ts @@ -1,6 +1,8 @@ +import type { TargetCpu } from "./backend/target-cpu.js"; import type { WindowsSubsystem } from "./backend/targets.js"; import type { NativeLinkInfo } from "./backend/native-link-info.js"; import type { NativeOptimization } from "./backend/optimization.js"; +import type { NativeProfileMatch } from "./backend/native-profile.js"; import type { ScrDiagnostic } from "./diagnostics/diagnostic.js"; import type { CoverageInput } from "./coverage/report.js"; import type { TierMode } from "./frontend/tier-analysis.js"; @@ -43,6 +45,17 @@ export interface CompileBaseOptions { * SCRIPTC_LIBNODE at build time. SCRIPTC_LIBNODE overrides the recorded * directory at run time (node-islands/libnode.ts). */ libnode?: string; + /** The x86-64 microarchitecture level the executable's code may require + * (`x86-64-v2` to `x86-64-v4`). The program's functions are compiled for + * that level, and the executable then runs only on CPUs that support it. + * Unset or `baseline`, the code runs on any x86-64 CPU. x86-64 targets + * only; the runtime pack keeps its baseline code. */ + targetCpu?: TargetCpu; + /** Give the executable an x86-64-v3 copy of each program function, which + * it runs instead of the baseline code on CPUs that support that level + * (`SCRIPTC_CPU_LEVEL` caps the level at run time). Executables for x86-64 + * targets without a `targetCpu` only; off by default. */ + cpuDispatch?: boolean; /** Instrument the executable for profile-guided optimization. Each run * writes a raw profile to this path pattern (`%p` expands to the process * id; `LLVM_PROFILE_FILE` overrides the pattern at run time). Linux @@ -52,9 +65,18 @@ export interface CompileBaseOptions { * merge-profiles`). Code the profile does not describe, or describes * from an older version of the program, is optimized as without one. */ profileUse?: string; + /** A function layout (scripts/refresh-cli-layout.mjs; gzip-compressed when + * the name ends in .gz): the listed functions are written first, by tier, + * so the code that runs together sits together in the executable. The + * native compiler's build uses it. Executables and LLVM output. */ + functionLayout?: string; /** Eligible packages compile automatically. Explicit names override automatic selection heuristics. */ npmStatic?: readonly string[] | "auto"; + /** Shape inference's typed overlay for opted-in packages' JavaScript. + * False disables it (`--no-shape-overlay`); a package opts out with + * `"scriptc": { "packages": { "": { "shapes": false } } }`. */ + shapeOverlay?: boolean; /** Outbound native FFI manifest. Its signature-only TypeScript bindings * lower to direct C ABI calls. Source outputs retain those declarations; * archive/system-library inputs join only an executable link. */ @@ -100,6 +122,8 @@ export type CompileArtifact = path: string; translationUnitPath: string; backend: "llvm"; + /** How a `profileUse` build's profile matched the program's functions. */ + profileMatch?: NativeProfileMatch; }; export type CompileFailure = { diff --git a/packages/compiler/src/contracts/int32-elements.ts b/packages/compiler/src/contracts/int32-elements.ts index ca61a8e49..67446f2d1 100644 --- a/packages/compiler/src/contracts/int32-elements.ts +++ b/packages/compiler/src/contracts/int32-elements.ts @@ -709,7 +709,8 @@ export class Int32ElementContracts { const findChainsIn = (st: IrStmt): boolean => everyStmtChild(st, findChains, findChainsIn); for (const st of fn.body) findChainsIn(st); // Parameters whose callers are not all visible hold opaque arrays. - if (this.valueEscaped.has(fn.name) || this.runtimeVirtual(fn.name)) + const runtimeVirtual = this.runtimeVirtual; + if (this.valueEscaped.has(fn.name) || runtimeVirtual(fn.name)) for (const p of fn.params) if (this.holds(p.type)) { const why = this.valueEscaped.has(fn.name) diff --git a/packages/compiler/src/coverage/report.ts b/packages/compiler/src/coverage/report.ts index 58a2cafa6..d78120f89 100644 --- a/packages/compiler/src/coverage/report.ts +++ b/packages/compiler/src/coverage/report.ts @@ -22,9 +22,18 @@ import { type TiersEnvelope, } from "../diagnostics/envelope.js"; import { renderDiagnostics } from "../diagnostics/render.js"; +import { + dynamicSlotLabel, + isDynamicSlotCode, + type DynamicSlotEntry, +} from "../diagnostics/dynamic-slots.js"; import type { LowerStats } from "../frontend/lowering/lowerer.js"; import type { ProvenanceSources } from "../frontend/provenance-registry.js"; -import type { DemotionUnit, TierReport } from "../frontend/tier-analysis.js"; +import type { + DemotionUnit, + LocalizedFailureReport, + TierReport, +} from "../frontend/tier-analysis.js"; import type { WasmModuleReport } from "../wasm/discover.js"; import { describe as describeReason, @@ -55,6 +64,9 @@ export interface CoverageInput { * leaves untyped. Never blockers; `--fail-on=dynamic` opts into failing * on it. */ performance?: ScrDiagnostic[]; + /** Localized failure: the checked dynamic slots (SC6104, also in + * `performance`). `--fail-on=dynamic-slots` fails on them. */ + dynamicSlots?: DynamicSlotEntry[]; /** Each dependency's native compilation outcome, with the first refusal * reason for a refused package. */ npmStatic?: NpmStaticStatus[]; @@ -234,6 +246,10 @@ export function renderCoverage(input: CoverageInput, opts: CoverageRenderOptions ` ${c(DIM, `(${skipped} function${skipped === 1 ? "" : "s"} not analyzed — signature blocked; their statements count as not static)`)}`, ); } + renderDynamicSlots(input.dynamicSlots ?? [], out, c, (loc) => { + const p = locator.position(loc.file, loc.start); + return `${displayPath(loc.file, opts.root)}:${p.line}:${p.column}`; + }); out.push(""); // Report native package compilation and the first reason for each refusal. @@ -391,7 +407,7 @@ export function renderCoverage(input: CoverageInput, opts: CoverageRenderOptions renderRows(groupBlockers(divergences), YELLOW); }; // The performance warning: its message names the functions. - const performance = input.performance ?? []; + const performance = (input.performance ?? []).filter((d) => !isDynamicSlotCode(d.code)); const renderPerformance = () => { for (const d of performance) { out.push(""); @@ -525,6 +541,7 @@ export type CoverageFailOn = | "blockers" | "divergences" | "dynamic" + | "dynamic-slots" | "node-islands" | "eager-node-islands" | "island-gaps"; @@ -533,6 +550,7 @@ export const COVERAGE_FAIL_ON: readonly CoverageFailOn[] = [ "blockers", "divergences", "dynamic", + "dynamic-slots", "node-islands", "eager-node-islands", "island-gaps", @@ -548,7 +566,9 @@ export function coveragePasses(input: CoverageInput, failOn?: CoverageFailOn): b // runtime fences build (the statement throws only if it executes). if (input.diagnostics.length > 0) return false; if (failOn === "divergences" && (input.divergences?.length ?? 0) > 0) return false; - if (failOn === "dynamic" && (input.performance?.length ?? 0) > 0) return false; + if (failOn === "dynamic" && (input.performance ?? []).some((d) => !isDynamicSlotCode(d.code))) + return false; + if (failOn === "dynamic-slots" && (input.dynamicSlots?.length ?? 0) > 0) return false; // The Node island gates (report-only islands count: they are what a // build would choose). Every island evaluates eagerly until deferred // evaluation lands, so eager-node-islands equals node-islands for now. @@ -620,6 +640,19 @@ export function coverageEnvelope( percentStatic: total === 0 ? 100 : Math.floor((ok / total) * 100), }, }), + ...(input.dynamicSlots !== undefined + ? { + dynamicSlots: input.dynamicSlots.map((slot) => ({ + file: slot.loc.file, + ...locator.position(slot.loc.file, slot.loc.start), + name: slot.name, + kind: slot.slot, + declaredType: slot.declaredType, + code: slot.code, + reason: slot.reason, + })), + } + : {}), ...(input.tiers !== undefined ? { tiers: tiersEnvelope(input.tiers, locator) } : {}), ...(input.wasmModules !== undefined ? { wasmModules: input.wasmModules } : {}), ...(input.nodeIslands !== undefined @@ -668,6 +701,26 @@ function tiersEnvelope(tiers: TierReport, locator: SourceLocator): TiersEnvelope ...at(r.loc), })), units: tiers.units.map((u) => ({ ...unit(u), requests: u.requests, sites: u.sites })), + localizedFailure: { + errors: tiers.localizedFailure.errors, + typeFailures: tiers.localizedFailure.typeFailures, + cascades: tiers.localizedFailure.cascades, + clearedThroughL2: tiers.localizedFailure.clearedThroughL2, + clearedThroughL4: tiers.localizedFailure.clearedThroughL4, + remainingByCode: tiers.localizedFailure.remainingByCode, + slots: tiers.localizedFailure.slots.map((slot) => ({ + ...at(slot.loc), + slot: slot.slot, + name: slot.name, + declaredType: slot.declaredType, + family: slot.family, + code: slot.code, + message: slot.message, + stage: slot.stage, + ...(slot.rejects !== null ? { rejects: slot.rejects } : {}), + cascade: slot.cascade, + })), + }, }; } @@ -695,6 +748,59 @@ function unitMatches(u: DemotionUnit, query: string): boolean { ); } +const DYNAMIC_SLOTS_SHOWN = 20; + +/** The dynamic slots under the statement counts: each slot's declared + * type, and why it has no static lowering. */ +function renderDynamicSlots( + slots: readonly DynamicSlotEntry[], + out: string[], + c: (code: string, s: string) => string, + where: (loc: SrcLoc) => string, +): void { + if (slots.length === 0) return; + out.push( + ` dynamic slots ${slots.length} ${c(DIM, "(declared types with no static lowering, held as checked-dynamic values; SC6104)")}`, + ); + for (const slot of slots.slice(0, DYNAMIC_SLOTS_SHOWN)) + out.push(` ${dynamicSlotLabel(slot)} ${c(DIM, where(slot.loc))}`); + if (slots.length > DYNAMIC_SLOTS_SHOWN) + out.push(` ${c(DIM, `and ${slots.length - DYNAMIC_SLOTS_SHOWN} more`)}`); +} + +const SLOTS_SHOWN = 40; + +/** The localized-failure section of --explain-dynamic (report-only): the + * committed type failures that would become checked dynamic slots, and + * the cascade each one causes today. */ +function renderLocalizedFailure( + report: LocalizedFailureReport, + out: string[], + c: (code: string, s: string) => string, + where: (loc: { file: string; start: number }) => string, +): void { + if (report.typeFailures.degradable === 0 && report.typeFailures.refused === 0) return; + out.push(""); + out.push( + ` ${report.slots.length} would-be dynamic slot${report.slots.length === 1 ? "" : "s"} ${c(DIM, "(localized failure, report-only: type failures a checked dynamic slot would hold)")}`, + ); + out.push( + ` ${c(DIM, `${report.errors} errors: ${report.clearedThroughL2} clear with declaration slots (L1-L2), ${report.clearedThroughL4} with member and expression slots too (L3-L4); ${report.cascades} cascade${report.cascades === 1 ? "" : "s"} attributed; ${report.typeFailures.refused} type failure${report.typeFailures.refused === 1 ? " stays" : "s stay"} refused`)}`, + ); + for (const slot of report.slots.slice(0, SLOTS_SHOWN)) { + const cascade = slot.cascade > 0 ? `, clears ${slot.cascade}` : ""; + out.push( + ` ${slot.stage} ${slot.slot} ${slot.name}: ${capMessage(slot.declaredType)} ${c(DIM, `${where(slot.loc)} (${slot.code} ${slot.family}${cascade})`)}`, + ); + if (slot.rejects !== null) + out.push(` ${c(DIM, `rejects ${slot.rejects.split(" @")[0]}`)}`); + } + if (report.slots.length > SLOTS_SHOWN) + out.push( + ` ${c(DIM, `${report.slots.length - SLOTS_SHOWN} more (--print=diagnostics lists all)`)}`, + ); +} + /** `scriptc coverage --explain-dynamic[=]`: the dynamic tier today * and the demotion chains the report-only analysis computed. With a query, * only units whose name matches it (`x`, `Point.x`, `parse:x`). */ @@ -735,6 +841,7 @@ export function renderExplainDynamic( (excluded > 0 ? c(DIM, `, ${excluded} excluded by policy`) : ""), ); } + if (query === undefined) renderLocalizedFailure(tiers.localizedFailure, out, c, where); const units = tiers.units.filter((u) => query === undefined || unitMatches(u, query)); if (query !== undefined && units.length === 0) { out.push(` no demoted unit matches '${query}'`); diff --git a/packages/compiler/src/coverage/surface-manifest.ts b/packages/compiler/src/coverage/surface-manifest.ts index 9867a5617..b87a8065d 100644 --- a/packages/compiler/src/coverage/surface-manifest.ts +++ b/packages/compiler/src/coverage/surface-manifest.ts @@ -39,14 +39,14 @@ import { InternalCompilerError } from "../errors.js"; * byte-deterministic: entries sort by id, keys are emitted in one fixed * order, and the output carries no timestamps or absolute paths. */ import { FENCE_CODES, UNSUPPORTED, webAssemblyDiag } from "../diagnostics/diagnostic.js"; -import { NODE24_FETCH_COMPAT_PROFILE } from "../compat/fetch-profile.js"; +import { node24FetchCompatProfile } from "../compat/fetch-profile.js"; import { SUPPORTED_BUILTIN_MODULES, SUPPORTED_NODE_MODULES } from "../frontend/builtin-modules.js"; import { - AMBIENT_SURFACE_FNS, + ambientSurfaceFns, ARRAY_METHODS, BUILTIN_MODULE_CONSTS, BUILTIN_MODULE_FENCE_HINTS, - BUILTIN_MODULE_FNS, + builtinModuleFns, COLLECTION_ITERATOR_METHODS, COMPOUND_ASSIGN_OPS, STDLIB_SURFACE, @@ -88,18 +88,21 @@ export const MANIFEST_SCHEMA_VERSION = 2; /** The manifest's own scope statement: what the projection covers and — * just as load-bearing — what it deliberately leaves out, so absence is * never read as a support claim in either direction. */ -const COVERAGE_NOTES: string[] = [ - "Entries are projected mechanically from the compiler's own decision tables: the diagnostics registry, the unsupported-syntax dispatch tables, the stdlib and node-builtin lowering tables, and the supported-builtin-module list. Nothing is hand-maintained; the manifest regenerates byte-identically from the source tree at this version.", - "Absence from this manifest means 'not projected', never 'unsupported'. Surfaces lowered through dedicated code paths rather than tables are not yet projected: console, JSON, Promise/async and the timer surface, the net/http/tls/https/dgram/dns/assert/test/stream/readline module member surfaces, template literals, the regex slice, global functions (parseInt, parseFloat, isNaN, isFinite), and the process surface outside its ambient slice.", - "The ambient-nondeterminism and ambient-authority surfaces the library sidecar's determinism attestation scans ARE projected even where they lower through dedicated code paths — the Date compositions (stdlib.date.*), perf_hooks' performance.now, and the process global's ambient reads and authority calls (node-builtin.process.*) — so a determinism fence can name every surface the attestation demotes on.", - "stdlib and node-builtin member entries name surface whose LOWERED call forms are constrained (arity, argument shapes); declared call forms outside the lowered set are refused per site, with code SC2020 for standard-library and node-builtin surface.", - "Entries with status 'unsupported' describe where the named code is raised: forms of the construct outside the supported subset are refused with that code — not that every form of the named feature is refused. Supported forms appear as their own static entries where a table projects them.", - `The engine-free fetch projection targets Node ${NODE24_FETCH_COMPAT_PROFILE.target.node} with bundled Undici ${NODE24_FETCH_COMPAT_PROFILE.target.undici}. Each projected row names the differential evidence that guards it; changing the pinned Node or Undici version is an explicit profile update.`, - "The fetch profile also contains a runtime-reflected census of the selected fetch, abort, Headers, and readable-stream interfaces plus RequestInit/ResponseInit dictionary reads. Static and unsupported census rows are projected here; its explicitly out-of-scope metadata rows and adjacent-interface exclusions remain in the profile so absence is deliberate rather than ambiguous.", - "Process-level diagnostic codes are not surface entries: SC0001-SC0004 are preflight gates, SC1110 is a comptime evaluation failure, SC3001/SC3002 are backend/target tier refusals, SC9001/SC9002 are internal errors.", - "Entry statuses are projected for the desktop targets. The mobile targets (aarch64-apple-ios, aarch64-apple-ios-simulator, aarch64-linux-android) compile library-mode archives only: the library-admissible surface (what SC4005's async_free requirement and the library link set admit) is supported there, the executable lane refuses those triples with SC3002, and no entry outside the library-admissible surface carries a mobile support claim. iOS archives build for iOS 15.0 on darwin hosts; Android archives build against NDK API level 26.", - "No scheduling metadata is published; entry ids are the stable diff keys across releases.", -]; +function coverageNotes(): string[] { + const fetchTarget = node24FetchCompatProfile().target; + return [ + "Entries are projected mechanically from the compiler's own decision tables: the diagnostics registry, the unsupported-syntax dispatch tables, the stdlib and node-builtin lowering tables, and the supported-builtin-module list. Nothing is hand-maintained; the manifest regenerates byte-identically from the source tree at this version.", + "Absence from this manifest means 'not projected', never 'unsupported'. Surfaces lowered through dedicated code paths rather than tables are not yet projected: console, JSON, Promise/async and the timer surface, the net/http/tls/https/dgram/dns/assert/test/stream/readline module member surfaces, template literals, the regex slice, global functions (parseInt, parseFloat, isNaN, isFinite), and the process surface outside its ambient slice.", + "The ambient-nondeterminism and ambient-authority surfaces the library sidecar's determinism attestation scans ARE projected even where they lower through dedicated code paths — the Date compositions (stdlib.date.*), perf_hooks' performance.now, and the process global's ambient reads and authority calls (node-builtin.process.*) — so a determinism fence can name every surface the attestation demotes on.", + "stdlib and node-builtin member entries name surface whose LOWERED call forms are constrained (arity, argument shapes); declared call forms outside the lowered set are refused per site, with code SC2020 for standard-library and node-builtin surface.", + "Entries with status 'unsupported' describe where the named code is raised: forms of the construct outside the supported subset are refused with that code — not that every form of the named feature is refused. Supported forms appear as their own static entries where a table projects them.", + `The engine-free fetch projection targets Node ${fetchTarget.node} with bundled Undici ${fetchTarget.undici}. Each projected row names the differential evidence that guards it; changing the pinned Node or Undici version is an explicit profile update.`, + "The fetch profile also contains a runtime-reflected census of the selected fetch, abort, Headers, and readable-stream interfaces plus RequestInit/ResponseInit dictionary reads. Static and unsupported census rows are projected here; its explicitly out-of-scope metadata rows and adjacent-interface exclusions remain in the profile so absence is deliberate rather than ambiguous.", + "Process-level diagnostic codes are not surface entries: SC0001-SC0004 are preflight gates, SC1110 is a comptime evaluation failure, SC3001/SC3002 are backend/target tier refusals, SC9001/SC9002 are internal errors.", + "Entry statuses are projected for the desktop targets. The mobile targets (aarch64-apple-ios, aarch64-apple-ios-simulator, aarch64-linux-android) compile library-mode archives only: the library-admissible surface (what SC4005's async_free requirement and the library link set admit) is supported there, the executable lane refuses those triples with SC3002, and no entry outside the library-admissible surface carries a mobile support claim. iOS archives build for iOS 15.0 on darwin hosts; Android archives build against NDK API level 26.", + "No scheduling metadata is published; entry ids are the stable diff keys across releases.", + ]; +} /** Stable kebab id segment from prose: quotes dropped, the parenthetical * tail dropped, non-alphanumeric runs collapsed to one dash. */ @@ -410,7 +413,7 @@ export function generateSurfaceManifest(compilerVersion: string): SurfaceManifes : "recognized module (bare and node:-prefixed specifiers)", }); } - for (const [mod, members] of Object.entries(BUILTIN_MODULE_FNS)) { + for (const [mod, members] of Object.entries(builtinModuleFns())) { for (const member of Object.keys(members!)) { add({ id: `${moduleId(mod)}.${member}`, @@ -459,10 +462,9 @@ export function generateSurfaceManifest(compilerVersion: string): SurfaceManifes } // ── fetch/Web platform: one explicit, versioned compatibility profile ─ - const fetchTarget = - `Node ${NODE24_FETCH_COMPAT_PROFILE.target.node} / ` + - `Undici ${NODE24_FETCH_COMPAT_PROFILE.target.undici}`; - for (const operation of NODE24_FETCH_COMPAT_PROFILE.operations) { + const fetchProfile = node24FetchCompatProfile(); + const fetchTarget = `Node ${fetchProfile.target.node} / Undici ${fetchProfile.target.undici}`; + for (const operation of fetchProfile.operations) { const evidence = operation.evidence.map((item) => item.generated !== undefined ? `generated:${item.generated}` : `fixture:${item.fixture!}`, ); @@ -478,10 +480,7 @@ export function generateSurfaceManifest(compilerVersion: string): SurfaceManifes `differential evidence: ${evidence.join(", ")}`, }); } - for (const options of [ - NODE24_FETCH_COMPAT_PROFILE.requestInit, - NODE24_FETCH_COMPAT_PROFILE.responseInit, - ]) { + for (const options of [fetchProfile.requestInit, fetchProfile.responseInit]) { for (const option of options) { const evidence = option.evidence.map((item) => item.generated !== undefined ? `generated:${item.generated}` : `fixture:${item.fixture!}`, @@ -497,7 +496,7 @@ export function generateSurfaceManifest(compilerVersion: string): SurfaceManifes }); } } - for (const row of NODE24_FETCH_COMPAT_PROFILE.inventory.entries) { + for (const row of fetchProfile.inventory.entries) { if (row.status !== "unsupported") continue; if (row.code === undefined || row.reason === undefined) { throw new InternalCompilerError(`non-static fetch inventory row '${row.id}' is incomplete`); @@ -524,7 +523,7 @@ export function generateSurfaceManifest(compilerVersion: string): SurfaceManifes // Date/perf_hooks/process rows from the one ambient table — each entry // is fenceable by construction (its row carries the reach-witnessing // libCall spellings the fence detector uses). - for (const row of AMBIENT_SURFACE_FNS) { + for (const row of ambientSurfaceFns()) { add({ id: row.id, kind: row.kind, @@ -578,7 +577,7 @@ export function generateSurfaceManifest(compilerVersion: string): SurfaceManifes return { schemaVersion: MANIFEST_SCHEMA_VERSION, compilerVersion, - coverage: COVERAGE_NOTES, + coverage: coverageNotes(), entries, }; } diff --git a/packages/compiler/src/coverage/verdict-cache.ts b/packages/compiler/src/coverage/verdict-cache.ts index c92cdc45e..dfaaa5c98 100644 --- a/packages/compiler/src/coverage/verdict-cache.ts +++ b/packages/compiler/src/coverage/verdict-cache.ts @@ -7,6 +7,7 @@ * still hold. With a semantic matcher (the Node frontend's), edits that * only touch TypeScript comments also reuse it, with every reported * location moved to the edited text. */ +import type { DynamicSlotEntry } from "../diagnostics/dynamic-slots.js"; import { createHash } from "node:crypto"; import { mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs"; import { join } from "node:path"; @@ -23,7 +24,7 @@ import { type FrontendInputSnapshot, } from "../frontend/input-tracker.js"; -const SCHEMA = "scriptc.coverage-verdict.v3"; +const SCHEMA = "scriptc.coverage-verdict.v4"; /** Key-value storage for verdict entries (best effort: failures miss). */ export interface VerdictStore { @@ -41,6 +42,7 @@ export interface CachedCoverage { unreached?: { stats: LowerStats; diagnostics: ScrDiagnostic[]; runtimeFences?: ScrDiagnostic[] }; divergences?: ScrDiagnostic[]; performance?: ScrDiagnostic[]; + dynamicSlots?: DynamicSlotEntry[]; npmStatic?: NpmStaticStatus[]; wasmModules?: WasmModuleReport[]; nodeIslands?: NodeIslandReport; @@ -158,6 +160,7 @@ function cacheable(coverage: AnalyzeResult["coverage"]): CachedCoverage | null { if (coverage.unreached !== undefined) out.unreached = coverage.unreached; if (coverage.divergences !== undefined) out.divergences = coverage.divergences; if (coverage.performance !== undefined) out.performance = coverage.performance; + if (coverage.dynamicSlots !== undefined) out.dynamicSlots = coverage.dynamicSlots; if (coverage.npmStatic !== undefined) out.npmStatic = coverage.npmStatic; if (coverage.wasmModules !== undefined) out.wasmModules = coverage.wasmModules; if (coverage.nodeIslands !== undefined) out.nodeIslands = coverage.nodeIslands; @@ -177,6 +180,7 @@ function restore(cached: CachedCoverage, sources: CachedSource[]): AnalyzeResult if (cached.unreached !== undefined) coverage.unreached = cached.unreached; if (cached.divergences !== undefined) coverage.divergences = cached.divergences; if (cached.performance !== undefined) coverage.performance = cached.performance; + if (cached.dynamicSlots !== undefined) coverage.dynamicSlots = cached.dynamicSlots; if (cached.npmStatic !== undefined) coverage.npmStatic = cached.npmStatic; if (cached.wasmModules !== undefined) coverage.wasmModules = cached.wasmModules; if (cached.nodeIslands !== undefined) coverage.nodeIslands = cached.nodeIslands; diff --git a/packages/compiler/src/diagnostics/diagnostic.ts b/packages/compiler/src/diagnostics/diagnostic.ts index 2736d5edd..51a08dc5d 100644 --- a/packages/compiler/src/diagnostics/diagnostic.ts +++ b/packages/compiler/src/diagnostics/diagnostic.ts @@ -64,15 +64,21 @@ * parses date strings with V8's grammar. SC61xx are performance * notes, also warnings (coverage --fail-on=dynamic opts in): * likely-hot functions that use untyped values (SC6101, - * diagnostics/dynamic-tier.ts) - * SC8xxx Node islands: the program calls into Node.js, and the build - * finds no usable libnode pack (SC8005); Node island policy: - * packages and modules outside the native subset that run (or, - * in report-only mode, would run) in a Node island — unknown - * project configuration (SC8004), a package required to be - * native (SC8003) or every island refused (SC8002) while outside - * the native subset, and the R2 object-model constructs in user - * JavaScript (SC8101–SC8105, behind --strict-user-js) + * diagnostics/dynamic-tier.ts), package files whose projected + * declaration types were dropped (SC6102), and shape inference's + * overlay report: what it typed and how many inferred facts the + * build checks (SC6103, frontend/shape-inference) + * SC8xxx Node islands: packages and modules outside the native subset + * that run (or, in report-only mode, would run) in a Node + * island — unknown project configuration (SC8004), a package + * required to be native (SC8003) or every island refused + * (SC8002) while outside the native subset, and the R2 + * object-model constructs in user JavaScript (SC8101–SC8105, + * behind --strict-user-js); programs that call into Node.js + * where the build finds no usable libnode pack (SC8005), or + * cannot collect the modules a call loads (SC8006); and type + * patches from `scriptc.typePatches` that did not apply as + * written (SC8030–SC8033, warnings) * SC9xxx internal compiler errors (still source-anchored) */ import type { SrcLoc } from "../ir/ir.js"; @@ -109,6 +115,64 @@ export interface ScrDiagnostic { * and class member body: it throws when the module initializes, which is * program startup for a module the entry imports. */ startup?: true; + /** Localized failure, report-only (stage L0): set where a type with no + * static lowering poisoned a slot (Lowerer.badType and the dyn class + * field policy). Never part of the rendered message. */ + typeFailure?: TypeFailure; + /** Localized failure, report-only: the declaration this diagnostic + * rejected, as a failure-group key. Set when a reference flushes a + * deferred declaration's diagnostics, and on the diagnostics of a + * declaration statement whose bindings were blocked. */ + rejects?: string; + /** Localized failure, report-only: the failure groups (rejected + * declarations, blocked statements) that the statement committing this + * diagnostic used. A cascade clears when every one of them would + * degrade instead of failing. */ + cause?: readonly string[]; +} + +/** Where a type failure sits (localized-failure design §3.6): a + * declaration slot, a member of a declared type, or an expression's own + * type. */ +export type TypeFailureSlot = + | "field" + | "param" + | "return" + | "local" + | "global" + | "loop" + | "member" + | "expression"; + +/** Which badType branch (or policy fence) refused the type. */ +export type TypeFailureFamily = + | "node-types" + | "fetch-ambient" + | "enum-object" + | "stdlib-index" + | "index-signature" + | "package" + | "generic-signature" + | "overloads" + | "intersection" + | "component" + | "stdlib-nominal" + | "record-member" + | "unsupported" + | "dyn-field-policy"; + +/** A type failure's structured payload: what the commit points of the + * localized-failure design inspect (PoisonError.failure carries the same + * object). `degradable` is the §3.7 classification: a checked-dynamic slot + * would keep the program's meaning. */ +export interface TypeFailure { + slot: TypeFailureSlot; + /** The slot's name (`_customTools`, `listAll`, `x`), or the expression + * text for expression slots. */ + name: string; + declaredType: string; + family: TypeFailureFamily; + degradable: boolean; } /** SC3002–SC3004 — native-helper target, installation, and execution @@ -144,6 +208,18 @@ export function nodeIslandNodeDiag(message: string, file: string): ScrDiagnostic }; } +/** SC8006 — a Node island call whose modules the build cannot collect into + * the executable: a computed specifier, or a module of the program that + * loads others through computed specifiers. */ +export function nodeIslandModuleDiag(message: string, loc: SrcLoc): ScrDiagnostic { + return { + code: "SC8006", + message, + loc, + hint: "name the module with a string literal (a relative path or a package name); the build ships the modules it can see with the executable", + }; +} + /* ── SC6xxx: divergences from Node (warnings) ─────────────────────────── */ /** SC6001 — a value passed to (or declared as) a narrower record type is @@ -261,6 +337,40 @@ export function nodeIslandsOffDiag(pkg: string, why: string, loc: SrcLoc): ScrDi }; } +/** SC8002 — a user module is marked to run in a Node island while + * Node islands are off. */ +export function nodeIslandsOffModuleDiag( + file: string, + source: "config" | "cli", + loc: SrcLoc, +): ScrDiagnostic { + return { + code: "SC8002", + message: `user module '${file}' is marked to run in Node (${source === "cli" ? "--node-island" : "scriptc.nodeIslands.modules"}) and Node islands are off`, + loc, + hint: "allow Node islands (--node-islands=auto or explicit), or remove the marking", + }; +} + +/** SC8030–SC8033 — a type patch (`scriptc.typePatches`) that did not + * apply as written: no range covers the installed copy (SC8030), its + * selector matches nothing (SC8031), the declaration already has the type + * (SC8032), or the selector is ambiguous or not patchable (SC8033). + * Warnings: the build goes on with the declarations it has. */ +export function typePatchDiag( + code: "SC8030" | "SC8031" | "SC8032" | "SC8033", + message: string, + hint: string, + loc: SrcLoc, +): ScrDiagnostic { + return { code, message, loc, hint }; +} + +/** True for the type patch warnings (SC8030–SC8033). */ +export function isTypePatchCode(code: string): boolean { + return /^SC803[0-3]$/.test(code); +} + /** SC8003 — a package marked "native" is outside the native subset. */ export function nativePackageDiag(pkg: string, why: string, loc: SrcLoc): ScrDiagnostic { return { @@ -700,7 +810,7 @@ export function unsupportedTypeDiag(typeText: string, loc: SrcLoc): ScrDiagnosti code: "SC2001", message: `values of type '${typeText}' cannot be compiled yet ` + - `(supported: number, string, boolean, arrays, Maps, Sets, RegExp, read-only Date values, functions, classes, records, unions of those, and 'unknown')`, + `(supported: number, string, boolean, arrays, Maps, Sets, RegExp, Dates, functions, classes, records, unions of those, and 'unknown')`, loc, milestone: "M2", }; @@ -734,7 +844,7 @@ export function genericSignatureTypeDiag(typeText: string, loc: SrcLoc): ScrDiag export function indexSignatureTypeDiag(typeText: string, loc: SrcLoc): ScrDiagnostic { return { code: "SC2006", - message: `values of type '${typeText}' cannot be compiled: this index signature is outside the supported shape (string or number keys; values limited to numbers, strings, booleans, records, classes, arrays, unions, functions, Maps, Sets, RegExps, Promises, or 'unknown')`, + message: `values of type '${typeText}' cannot be compiled: this index signature is outside the supported shape (string or number keys; values limited to numbers, strings, booleans, records, classes, arrays, unions, functions, Maps, Sets, RegExps, Dates, Promises, or 'unknown')`, loc, milestone: "M2", hint: "string- and number-keyed index signatures over the supported value types compile directly; symbol keys and dual signatures with unequal value types have no lowering", @@ -855,6 +965,21 @@ export function nativeAddonDiag(specifier: string, loc: SrcLoc): ScrDiagnostic { }; } +/** A createRequire require or require.resolve call whose specifier is + * computed at run time, where the runtime resolves it like Node + * (scr_require.c). Coverage lists the call as deferred: a require whose + * specifier resolves to a module ends the program with this code, because + * the module graph is fixed at build time. */ +export function computedRequireDiag(surface: string, loc: SrcLoc): ScrDiagnostic { + return { + code: "SC2020", + message: `${surface} with a computed specifier resolves at run time; a require of a specifier that resolves to a module, such as a native addon, ends the program`, + loc, + milestone: "later", + hint: 'require("") compiles the module into the program; native addons need a replacement through native FFI (https://scriptc.dev/ffi#replacing-a-node-api-addon)', + }; +} + /** Records are monomorphic structs, so a value's shape must match the * expected shape exactly OR narrow through the width-copy family * (recordWidthPlan / the overflow capture: target fields copied off the diff --git a/packages/compiler/src/diagnostics/dynamic-slots.ts b/packages/compiler/src/diagnostics/dynamic-slots.ts new file mode 100644 index 000000000..09b4cd670 --- /dev/null +++ b/packages/compiler/src/diagnostics/dynamic-slots.ts @@ -0,0 +1,49 @@ +/* SC6104, the dynamic-slot performance warning (localized-failure design + * §3.9): a slot whose declared TypeScript type has no static lowering holds + * a checked-dynamic value instead of rejecting the declaration around it. + * Its wording lives here; the slots come from the lowering's commit points + * (Lowerer.degradeAtCommit). Imports nothing heavy, like dynamic-tier.ts. */ +import type { SrcLoc } from "../ir/ir.js"; +import type { ScrDiagnostic, TypeFailureSlot } from "./diagnostic.js"; + +export const DYNAMIC_SLOT_CODE = "SC6104"; + +/** One dynamic slot, as coverage reports it. */ +export interface DynamicSlotEntry { + loc: SrcLoc; + name: string; + slot: TypeFailureSlot; + declaredType: string; + /** The diagnostic the slot's declaration would have failed with. */ + code: string; + reason: string; +} + +const SLOT_WORD: Record = { + field: "field", + param: "parameter", + return: "return value of", + local: "local", + global: "module global", + loop: "loop variable", + member: "member", + expression: "expression", +}; + +/** `'_customTools' (declared 'ToolDefinition[]')`, with the slot kind. */ +export function dynamicSlotLabel(entry: DynamicSlotEntry): string { + return `${SLOT_WORD[entry.slot]} '${entry.name}' (declared '${entry.declaredType}')`; +} + +export function dynamicSlotDiag(entry: DynamicSlotEntry): ScrDiagnostic { + return { + code: DYNAMIC_SLOT_CODE, + message: `${dynamicSlotLabel(entry)} is held as a checked-dynamic value: its type has no static lowering (${entry.reason}); reads into typed code are checked`, + loc: entry.loc, + hint: "restate the declared type inside the compilable set to keep this slot static; scriptc coverage lists dynamic slots, and --fail-on=dynamic-slots fails on them", + }; +} + +export function isDynamicSlotCode(code: string): boolean { + return code === DYNAMIC_SLOT_CODE; +} diff --git a/packages/compiler/src/diagnostics/envelope.ts b/packages/compiler/src/diagnostics/envelope.ts index ff08c3968..f6f8b9d3d 100644 --- a/packages/compiler/src/diagnostics/envelope.ts +++ b/packages/compiler/src/diagnostics/envelope.ts @@ -15,7 +15,7 @@ * version; renaming or removing a field, or changing a field's meaning, * bumps it. */ import { readFileSync } from "node:fs"; -import type { ScrDiagnostic } from "./diagnostic.js"; +import { isTypePatchCode, type ScrDiagnostic } from "./diagnostic.js"; import { isPerformanceCode } from "./dynamic-tier.js"; import type { WasmModuleReport } from "../wasm/discover.js"; @@ -114,6 +114,27 @@ export interface TiersEnvelope { message: string; })[]; units: (TierUnitPosition & { requests: number; sites: number })[]; + /** Localized failure (report-only): committed type failures that would + * become checked dynamic slots, and what they would clear. */ + localizedFailure: { + errors: number; + typeFailures: { degradable: number; refused: number }; + cascades: number; + clearedThroughL2: number; + clearedThroughL4: number; + remainingByCode: Record; + slots: (SourcePosition & { + slot: string; + name: string; + declaredType: string; + family: string; + code: string; + message: string; + stage: string; + rejects?: string; + cascade: number; + })[]; + }; } export interface DiagnosticsEnvelope { @@ -143,6 +164,15 @@ export interface DiagnosticsEnvelope { }; /** build only: the produced artifact path. */ artifact?: string; + /** coverage only, when there are any: the checked dynamic slots + * (localized failure, SC6104). */ + dynamicSlots?: (SourcePosition & { + name: string; + kind: string; + declaredType: string; + code: string; + reason: string; + })[]; /** coverage --explain-dynamic only: the report-only tier assignment. */ tiers?: TiersEnvelope; /** coverage only: the WebAssembly modules the build can see, with their @@ -217,6 +247,7 @@ export function categoryOf(diag: ScrDiagnostic): DiagnosticCategory { if (code === "SC0002" || code === "SC0004") return "environment"; if (/^SC[35]\d{3}$/.test(code)) return "environment"; if (code === "SC8004" || code === "SC8005" || code === "SC8012") return "environment"; + if (isTypePatchCode(code)) return "environment"; if (/^SC9\d{3}$/.test(code)) return "internal"; return "unsupported"; } @@ -334,7 +365,13 @@ export function locateDiagnostic( return { code: diag.code, category: categoryOf(diag), - severity: isDivergenceCode(diag.code) || isPerformanceCode(diag.code) ? "warning" : "error", + severity: + isDivergenceCode(diag.code) || + isPerformanceCode(diag.code) || + diag.code === "SC8004" || + isTypePatchCode(diag.code) + ? "warning" + : "error", scope, file: diag.loc.file, line: start.line, diff --git a/packages/compiler/src/diagnostics/render.ts b/packages/compiler/src/diagnostics/render.ts index e5a75c6bc..a370eb3b1 100644 --- a/packages/compiler/src/diagnostics/render.ts +++ b/packages/compiler/src/diagnostics/render.ts @@ -4,7 +4,7 @@ * longer than FRAME_MAX_WIDTH (minified sources) shows a window of that * width around the diagnostic's column, with "…" where text is cut. */ -import type { ScrDiagnostic } from "./diagnostic.js"; +import { isTypePatchCode, type ScrDiagnostic } from "./diagnostic.js"; export interface RenderOptions { color?: boolean; @@ -102,14 +102,12 @@ export function renderDiagnostic( frame.push( c(DIM, ` ${" ".repeat(gutterWidth)} | `) + " ".repeat(caretCol) + - c(/^SC6\d{3}$/.test(diag.code) ? YELLOW : RED, "^" + "~".repeat(spanOnLine - 1)), + c(isWarningCode(diag.code) ? YELLOW : RED, "^" + "~".repeat(spanOnLine - 1)), ); emitLine(lineNum + 1); } - // The divergence band (SC6xxx) is the one warning severity: the program - // compiles, and the site can behave differently from Node. - const warning = /^SC6\d{3}$/.test(diag.code); + const warning = isWarningCode(diag.code); out.push( `${c(BOLD, `${diag.loc.file}:${lineNum}:${colNum}`)} - ${warning ? c(YELLOW, "warning") : c(RED, "error")} ${c(BOLD, diag.code)}: ${diag.message}`, ); @@ -154,6 +152,40 @@ export function renderDiagnostics( /** Divergence warnings after a successful build: the rendered sites plus a * one-line count, on stderr. */ +/** The warning severities: the divergence band (SC6xxx), where the program + * compiles and the site can behave differently from Node, and unknown + * settings in the project's scriptc configuration (SC8004), which builds do + * not use. */ +export function isWarningCode(code: string): boolean { + return /^SC6\d{3}$/.test(code) || code === "SC8004" || isTypePatchCode(code); +} + +/** A build's type patch warnings (SC8030–SC8033). */ +export function renderTypePatchWarnings( + diags: readonly ScrDiagnostic[], + sourceTextByFile: Map, + opts: RenderOptions = {}, +): string { + const n = diags.length; + return ( + renderDiagnostics([...diags], sourceTextByFile, opts) + + `\n\n${n} warning${n === 1 ? "" : "s"}: ${n === 1 ? "this type patch" : "these type patches"} did not apply as written; the build uses the package's own declarations there.` + ); +} + +/** A build's SC8004 warnings: the unknown scriptc settings it ignored. */ +export function renderConfigWarnings( + diags: readonly ScrDiagnostic[], + sourceTextByFile: Map, + opts: RenderOptions = {}, +): string { + const n = diags.length; + return ( + renderDiagnostics([...diags], sourceTextByFile, opts) + + `\n\n${n} warning${n === 1 ? "" : "s"}: the build does not use ${n === 1 ? "this setting" : "these settings"}; scriptc coverage reports Node islands once they are valid.` + ); +} + export function renderWarnings( diags: readonly ScrDiagnostic[], sourceTextByFile: Map, diff --git a/packages/compiler/src/executable/executable-cache.test.ts b/packages/compiler/src/executable/executable-cache.test.ts index 5cf8c6588..3ba6a7c54 100644 --- a/packages/compiler/src/executable/executable-cache.test.ts +++ b/packages/compiler/src/executable/executable-cache.test.ts @@ -58,6 +58,7 @@ const native: EarlyExecutableNativeFeatures = { dgram: false, dnsPromises: false, dgramDyn: false, + textCodecDyn: false, watch: false, foreignFfi: false, nodeTest: false, @@ -68,6 +69,7 @@ const native: EarlyExecutableNativeFeatures = { dh: false, pki: false, ed25519: false, + x509: false, webcrypto: false, perfLoop: false, }; @@ -218,7 +220,7 @@ test("early executable cache misses on source and resolution changes", async () expect(await readEarlyExecutableCache(f.root, f.options)).toBeNull(); }); -test("early executable keys isolate compile modes, paths, implementation, checker, and FFI manifests", async () => { +test("early executable keys isolate compile modes, paths, implementation, checker, function layouts, and FFI manifests", async () => { const f = await fixture(); const tracker = new FrontendInputTracker(); tracker.run(() => trackedReadFile(f.source)); @@ -235,8 +237,12 @@ test("early executable keys isolate compile modes, paths, implementation, checke { ...f.options, optimization: "dev" }, { ...f.options, optimization: "speed" }, { ...f.options, detachedExit: true }, + { ...f.options, targetCpu: "x86-64-v3" }, + { ...f.options, targetCpu: "x86-64-v4" }, + { ...f.options, cpuDispatch: true }, { ...f.options, profile: "profile-generate:/tmp/%p.profraw" }, { ...f.options, profile: "profile-use:digest" }, + { ...f.options, functionLayout: "layout-digest" }, { ...f.options, checker: "tsc-ts-executable-identity" }, { ...f.options, npmStatic: "auto" }, { ...f.options, npmStatic: ["commander"] }, diff --git a/packages/compiler/src/executable/executable-cache.ts b/packages/compiler/src/executable/executable-cache.ts index b9be0724d..c5dca4297 100644 --- a/packages/compiler/src/executable/executable-cache.ts +++ b/packages/compiler/src/executable/executable-cache.ts @@ -12,12 +12,14 @@ import { } from "node:fs/promises"; import { basename, dirname, join, resolve } from "node:path"; import type { ScrDiagnostic } from "../diagnostics/diagnostic.js"; +import type { NativeProfileMatch } from "../backend/native-profile.js"; import type { FrontendInputSnapshot } from "../frontend/input-tracker.js"; import { frontendInputsStillMatch, validFrontendInputSnapshot } from "../frontend/input-tracker.js"; import { compilerReleaseVersion } from "../library/sidecar.js"; import { nativeArtifactDependenciesStillMatch } from "../backend/native/artifact-stamps.js"; import { type NativeArtifactDependency } from "../backend/native/contracts.js"; import { optimizationKeyParts } from "../backend/optimization.js"; +import { type TargetCpu, cpuDispatchKeyParts, targetCpuKeyParts } from "../backend/target-cpu.js"; import { privateSiblingPath } from "../backend/build-cache.js"; import type { CompilerImplementationDependency } from "../library/compiler-self-identity.js"; import { @@ -62,7 +64,9 @@ export interface EarlyExecutableNativeFeatures { warningReplace?: boolean; stdioExtended?: boolean; nodeIsland?: boolean; + moduleNamespace?: boolean; zlib: boolean; + brotli?: boolean; assert: boolean; inspect: boolean; dynInvoke: boolean; @@ -72,10 +76,14 @@ export interface EarlyExecutableNativeFeatures { streamDyn?: boolean; handleProps?: boolean; declAdopt?: boolean; + typeOrigins?: boolean; dynForAwait?: boolean; eventsOnceSignal?: boolean; + mime?: boolean; + utilTiming?: boolean; dynBuffer?: boolean; builtinValues?: boolean; + computedRequire?: boolean; events: boolean; emitter: boolean; symbol: boolean; @@ -93,6 +101,7 @@ export interface EarlyExecutableNativeFeatures { dgram: boolean; dnsPromises: boolean; dgramDyn: boolean; + textCodecDyn: boolean; watch: boolean; foreignFfi: boolean; nodeTest: boolean; @@ -102,6 +111,7 @@ export interface EarlyExecutableNativeFeatures { dh: boolean; pki: boolean; ed25519: boolean; + x509: boolean; perfLoop: boolean; tlsCa: boolean; webcrypto: boolean; @@ -121,6 +131,11 @@ interface EarlyExecutableCacheStamp { native: EarlyExecutableNativeFeatures; /** The build's performance warnings (SC61xx), which a cache hit repeats. */ performance?: ScrDiagnostic[]; + /** Programs that call into Node.js: the libnode pack identity the build + * recorded (EarlyExecutableCacheOptions.libnodeIdentity). */ + libnode?: string; + /** How a --profile-use build's profile matched, which a cache hit reports. */ + profileMatch?: NativeProfileMatch; integrity: string; } @@ -139,18 +154,30 @@ export interface EarlyExecutableCacheOptions { windowsSubsystem?: "gui"; /** Omitted unless --detached-exit is set. */ detachedExit?: true; - /** Omitted unless --libnode or SCRIPTC_LIBNODE is set (calls into Node.js - * record the pack). */ - libnode?: string; + /** The identity of the libnode pack a program that calls into Node.js + * records (node-islands/libnode.ts libnodeCacheKey), or undefined when the + * build names none. Not part of the key: an entry of a program that calls + * into Node records it, and a hit requires the same identity, so the pack + * settings never change the key of a program that does not. */ + libnodeIdentity?: () => string | undefined; + /** Omitted for the x86-64 baseline (`--target-cpu`). */ + targetCpu?: TargetCpu; + /** Omitted for builds without runtime CPU dispatch. */ + cpuDispatch?: true; /** Omitted without --profile-generate/--profile-use; otherwise the * pattern, or the digest of the profile's bytes. */ profile?: string; + /** Omitted without a function layout; otherwise the digest of its bytes. */ + functionLayout?: string; /** Omitted for the default tsgo checker; with SCRIPTC_CHECKER=tsc-ts, the * tsc-ts executable's file identity. */ checker?: string; /** Omitted unless SCRIPTC_YOUNG_NURSERY=1 (backend/young-nursery.ts). */ youngNursery?: true; npmStatic: readonly string[] | "auto" | null; + /** Omitted unless shape inference's overlay compiles the packages + * (frontend/shape-inference). */ + shapeOverlay?: true; /** Raw manifest identity: path and bytes. Native archives remain under the * stricter native cache's independent dependency validation. */ ffiProfile: { path: string; bytes: Uint8Array } | null; @@ -204,12 +231,23 @@ export interface EarlyExecutableCacheHit { frontend: FrontendInputSnapshot; /** The performance warnings the build reported (SC61xx), repeated on a hit. */ performance?: ScrDiagnostic[]; + /** How a --profile-use build's profile matched, reported again on a hit. */ + profileMatch?: NativeProfileMatch; } export interface EarlyExecutableCachePublish extends EarlyExecutableCacheHit { nativeDependencies?: NativeArtifactDependency[]; } +function isProfileMatch(value: unknown): value is NativeProfileMatch { + if (typeof value !== "object" || value === null) return false; + const match = value as Record; + return ["annotated", "mismatched", "missing", "unknownPartitions"].every((key) => { + const count = match[key]; + return typeof count === "number" && Number.isInteger(count) && count >= 0; + }); +} + const BOOLEAN_NATIVE_KEYS = [ "regex", "copying", @@ -239,6 +277,7 @@ const BOOLEAN_NATIVE_KEYS = [ "dgram", "dnsPromises", "dgramDyn", + "textCodecDyn", "watch", "foreignFfi", "nodeTest", @@ -249,6 +288,7 @@ const BOOLEAN_NATIVE_KEYS = [ "dh", "pki", "ed25519", + "x509", "webcrypto", "perfLoop", ] as const satisfies readonly (keyof EarlyExecutableNativeFeatures)[]; @@ -283,8 +323,10 @@ function cacheKey(options: EarlyExecutableCacheOptions): string { ...(options.strip ? ["strip"] : []), ...(options.windowsSubsystem === "gui" ? ["windows-subsystem-gui"] : []), ...(options.detachedExit ? ["detached-exit"] : []), - ...(options.libnode !== undefined ? [`libnode:${options.libnode}`] : []), + ...targetCpuKeyParts(options.targetCpu), + ...cpuDispatchKeyParts(options.cpuDispatch), ...(options.profile !== undefined ? [options.profile] : []), + ...(options.functionLayout !== undefined ? [`layout:${options.functionLayout}`] : []), ...(options.checker !== undefined ? [`checker:${options.checker}`] : []), ...(options.youngNursery ? ["young-nursery"] : []), options.npmStatic === null @@ -326,8 +368,10 @@ function routeKey(options: Omit 0 ? { performance: stamp.performance } : {}), + ...(isProfileMatch(stamp.profileMatch) ? { profileMatch: stamp.profileMatch } : {}), ...(ir === null ? {} : { irPath: paths.irPath }), ...(llvmSourcePath === undefined ? {} : { llvmSourcePath }), ...(irSourcePath === undefined ? {} : { irSourcePath }), @@ -848,6 +895,8 @@ export async function publishEarlyExecutableCache( ...(result.performance === undefined || result.performance.length === 0 ? {} : { performance: result.performance }), + ...(result.native.nodeIsland === true ? { libnode: options.libnodeIdentity?.() ?? "" } : {}), + ...(result.profileMatch === undefined ? {} : { profileMatch: result.profileMatch }), }; const stamp: EarlyExecutableCacheStamp = { ...unsigned, integrity: stampIntegrity(unsigned) }; await writeFile(join(stage, "stamp.json"), `${JSON.stringify(stamp)}\n`, { mode: 0o600 }); diff --git a/packages/compiler/src/executable/prepare.ts b/packages/compiler/src/executable/prepare.ts index 1771f98f5..027bd6ca6 100644 --- a/packages/compiler/src/executable/prepare.ts +++ b/packages/compiler/src/executable/prepare.ts @@ -122,6 +122,10 @@ export function prepareExecutableModule( mod: lowered.module!, sourceTexts, stats: lowered.stats, - warnings: [...(lowered.divergences ?? []), ...(lowered.performance ?? [])], + warnings: [ + ...(fe.warnings ?? []), + ...(lowered.divergences ?? []), + ...(lowered.performance ?? []), + ], }; } diff --git a/packages/compiler/src/frontend/analysis.ts b/packages/compiler/src/frontend/analysis.ts index 403cb70b7..d2e881473 100644 --- a/packages/compiler/src/frontend/analysis.ts +++ b/packages/compiler/src/frontend/analysis.ts @@ -38,7 +38,9 @@ export function analyzeWithFrontend( } ffi = loaded.profile; } - const fe = createFrontend(entryPath, opts.npmStatic, opts.externalTypes); + const fe = createFrontend(entryPath, opts.npmStatic, opts.externalTypes, undefined, { + nodeIslands: true, + }); try { const emptyStats = { statementsTotal: 0, @@ -130,6 +132,7 @@ export function analyzeWithFrontend( ...(lowered.runtimeFences.length > 0 ? { runtimeFences: lowered.runtimeFences } : {}), ...(lowered.divergences !== undefined ? { divergences: lowered.divergences } : {}), ...(lowered.performance !== undefined ? { performance: lowered.performance } : {}), + ...(lowered.dynamicSlots !== undefined ? { dynamicSlots: lowered.dynamicSlots } : {}), ...(lowered.unreached ? { unreached: lowered.unreached } : {}), ...(fe.npmStatic.length > 0 ? { npmStatic: fe.npmStatic } : {}), // --provenance-sources: the per-package attribution inputs (the diff --git a/packages/compiler/src/frontend/canon-map.ts b/packages/compiler/src/frontend/canon-map.ts new file mode 100644 index 000000000..2dadc31cb --- /dev/null +++ b/packages/compiler/src/frontend/canon-map.ts @@ -0,0 +1,171 @@ +/* Canonical mapping (localized-failure design §4.3), stage N1: the IR type + * of a canonical node, built from about ten structural rules plus the + * nominal table. Nominal objects (classes, standard-library interfaces, + * handles), callables, index-signature shapes, tuples, bigint and symbol + * keep their per-type recognitions: they map through mapType on the + * checker type the node came from. Everything structural (primitives, + * literals, units, arrays, flat unions, plain records) maps here, from the + * canonical form alone. + * + * Shadow mode (SCRIPTC_CANON_SHADOW=1) compares this mapping with mapType + * for every successful top-level mapping and appends one JSON line per + * comparison to SCRIPTC_CANON_SHADOW_OUT. It maps into throwaway + * registries, so the program's own IR is untouched. */ +import { appendFileSync } from "node:fs"; +import { BOOL, DYN, F64, NULL_T, STRING, UNDEFINED_T, typeKey, type IrType } from "../ir/ir.js"; +import { canonKey, Normalizer, type Canon, type CanonProp } from "./canon.js"; +import * as ts from "./ts7/adapter.js"; +import { + mapType, + ShapeRegistry, + UnionRegistry, + withUndefinedArm, + type TypeMapperCtx, +} from "./type-mapper.js"; + +const MAX_DEPTH = 24; + +/** The IR type of a canonical node, or null when no rule applies. */ +export function mapCanonical(c: Canon, ctx: TypeMapperCtx, depth = 0): IrType | null { + if (depth > MAX_DEPTH) return null; + const viaChecker = (): IrType | null => (c.src === undefined ? null : mapType(c.src, ctx)); + switch (c.k) { + case "prim": + return c.p === "number" + ? F64 + : c.p === "string" + ? STRING + : c.p === "boolean" + ? BOOL + : viaChecker(); + case "lit": + return typeof c.value === "number" ? F64 : typeof c.value === "string" ? STRING : BOOL; + case "unit": + return c.u === "undefined" ? UNDEFINED_T : c.u === "null" ? NULL_T : viaChecker(); + case "top": + return DYN; + case "param": + // A class's type parameters bind at IR level only (a generic class + // instance's bindings); the lowerer's resolver answers them. + // `this` in a class body is the class itself: the nominal table. + if (c.src === undefined) return null; + if ((c.src as { isThisType?: boolean }).isThisType === true) return viaChecker(); + return ctx.resolveTypeParam?.(c.src) ?? null; + case "opaque": + case "tuple": + return viaChecker(); + case "array": { + const elem = mapCanonical(c.elem, ctx, depth + 1); + return elem === null ? null : { kind: "array", elem }; + } + case "union": { + const byKey = new Map(); + for (const arm of c.arms) { + const mapped = mapCanonical(arm, ctx, depth + 1); + if (mapped === null || mapped.kind === "void") return null; + if (mapped.kind === "union") { + const def = ctx.unions.get(mapped.unionId); + if (def === undefined) return null; + for (const a of def.arms) byKey.set(typeKey(a), a); + } else byKey.set(typeKey(mapped), mapped); + } + const arms = [...byKey.values()]; + if (arms.length === 1) return arms[0]!; + if (arms.some((a) => a.kind === "dyn")) return DYN; + arms.sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1)); + return { kind: "union", unionId: ctx.unions.intern(arms) }; + } + case "obj": { + if (c.nominal !== undefined || c.call().length > 0 || c.construct().length > 0) + return viaChecker(); + if (c.index().length > 0) return viaChecker(); + const props = c.props(); + // `{}` holds any non-nullish value: the checked dynamic, as mapType. + if (props.length === 0) return DYN; + const structural = mapCanonicalRecord(props, ctx, depth); + // An object whose members do not map structurally (an interface over + // Map or Set, methods) keeps its per-type recognition. + return structural ?? viaChecker(); + } + } +} + +/** A plain record from an object's canonical members, or null when a + * member does not map. */ +function mapCanonicalRecord( + props: readonly CanonProp[], + ctx: TypeMapperCtx, + depth: number, +): IrType | null { + const fields: { name: string; type: IrType }[] = []; + for (const p of props) { + let t = mapCanonical(p.type(), ctx, depth + 1); + if (t === null) return null; + if (p.optional && t.kind !== "dyn") { + const armed = withUndefinedArm(t, ctx.unions); + if (armed === null) return null; + t = armed; + } + fields.push({ name: p.name, type: t }); + } + const declaredOrder = fields.map((f) => f.name); + fields.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)); + return { kind: "record", shapeId: ctx.shapes.intern(fields, false, undefined, declaredOrder) }; +} + +export type ShadowStatus = "equal" | "diverge" | "canon-fail"; + +/** Installs nothing unless SCRIPTC_CANON_SHADOW=1: the hook mapType calls + * after each successful top-level mapping. */ +export function canonShadowHook( + checker: ts.TypeChecker, +): ((type: ts.Type, mapped: IrType, ctx: TypeMapperCtx) => void) | undefined { + if (process.env["SCRIPTC_CANON_SHADOW"] !== "1") return undefined; + const out = process.env["SCRIPTC_CANON_SHADOW_OUT"]; + let busy = false; + return (type, mapped, ctx) => { + if (busy) return; + busy = true; + // The shadow records and must never break a compile. + try { + const resolve = ctx.resolveTypeParamTs; + const normalizer = new Normalizer(checker); + const c = normalizer.canon(type, resolve ? (t) => resolve(t) : new Map()); + const { typeMemo: _memo, canonShadow: _hook, ...rest } = ctx; + const shadowCtx: TypeMapperCtx = { + ...rest, + shapes: new ShapeRegistry(), + unions: new UnionRegistry(), + canMemoizeType: () => false, + }; + let canonical: IrType | null = null; + try { + canonical = mapCanonical(c, shadowCtx); + } catch { + canonical = null; + } + const status: ShadowStatus = + canonical === null + ? "canon-fail" + : typeKey(canonical) === typeKey(mapped) + ? "equal" + : "diverge"; + if (out !== undefined) + appendFileSync( + out, + `${JSON.stringify({ + status, + kind: c.k, + type: checker.typeToString(type).slice(0, 160), + canon: canonKey(c).slice(0, 160), + strict: typeKey(mapped), + ...(canonical !== null ? { canonical: typeKey(canonical) } : {}), + })}\n`, + ); + } catch { + // A census line lost, nothing else. + } finally { + busy = false; + } + }; +} diff --git a/packages/compiler/src/frontend/canon.ts b/packages/compiler/src/frontend/canon.ts new file mode 100644 index 000000000..3ec18a46b --- /dev/null +++ b/packages/compiler/src/frontend/canon.ts @@ -0,0 +1,532 @@ +/* Canonical type normalization (localized-failure design part 2, §4), + * stage N0: report-only. Nothing in the compiler consumes it yet. + * + * `canon(type, env)` resolves a checker type, under the current generic + * bindings, to a small set of canonical shapes: primitive, literal, unit, + * top, object (nominal or structural, with lazily resolved members), array, + * tuple, flat union, unbound parameter, and an opaque leaf for what it + * cannot reduce. The per-form rules of type-mapper.ts (generic aliases, + * equal-member intersections, narrowed parameters, recursive placeholders) + * are what this replaces family by family in later stages; N1 runs it in + * shadow mode beside mapType, N2 as mapType's fallback. + * + * Nodes intern by checker type identity and the bindings of the type + * parameters the walk consulted, so a recursive type closes on itself once + * (a back-reference returns the node under construction). */ +import * as ts from "./ts7/adapter.js"; + +export type CanonPrim = "number" | "string" | "boolean" | "bigint" | "symbol"; + +export interface CanonProp { + name: string; + optional: boolean; + readonly: boolean; + type: () => Canon; +} + +export interface CanonSig { + params: { name: string; optional: boolean; rest: boolean; type: () => Canon }[]; + ret: () => Canon; + /** The signature keeps type parameters of its own. */ + generic: boolean; +} + +export interface CanonIndex { + key: Canon; + value: () => Canon; +} + +export interface AliasRef { + name: string; + args: readonly Canon[]; + /** The checker type the alias reference came from (identity preserved + * for tables keyed by ts.Type, such as boundAliasInstances). */ + tsType: ts.Type; +} + +export type Canon = CanonNode & { + /** The checker type the node was normalized from (a type parameter's + * binding for substituted parameters). */ + src?: ts.Type; +}; + +type CanonNode = + | { k: "prim"; p: CanonPrim } + | { k: "lit"; value: string | number | boolean } + | { k: "unit"; u: "undefined" | "null" | "void" | "never" } + | { k: "top"; t: "unknown" | "any" | "object" } + | { + k: "obj"; + /** Class or standard-library declaration name, when nominal. */ + nominal?: string; + /** The type arguments of a nominal generic instantiation. */ + typeArgs: () => readonly Canon[]; + alias?: AliasRef; + props: () => readonly CanonProp[]; + index: () => readonly CanonIndex[]; + call: () => readonly CanonSig[]; + construct: () => readonly CanonSig[]; + } + | { k: "array"; elem: Canon; readonly: boolean } + | { k: "tuple"; elems: readonly { type: Canon; optional: boolean; rest: boolean }[] } + | { k: "union"; arms: readonly Canon[] } + | { k: "param"; name: string } + | { k: "opaque"; ts: ts.Type; reason: string }; + +/** Type-parameter bindings: the checker type a parameter is bound to, by + * the parameter's symbol, or a resolver over the parameter type (the + * lowerer's resolveTypeParamTs). */ +export type CanonEnv = ReadonlyMap | ((param: ts.Type) => ts.Type | null); + +const PRIM_FLAGS: readonly [number, CanonPrim][] = [ + [ts.TypeFlags.Number, "number"], + [ts.TypeFlags.String, "string"], + [ts.TypeFlags.Boolean, "boolean"], + [ts.TypeFlags.BigInt, "bigint"], + [ts.TypeFlags.ESSymbol | ts.TypeFlags.UniqueESSymbol, "symbol"], +]; + +/** A stable key for a canonical node, for deduplication and tests. Deep + * and wide types are cut off (depth 6, about 4,000 characters), so equal + * keys mean equal up to that horizon. */ +export function canonKey(c: Canon): string { + const budget = { left: 4000 }; + return canonKeyAt(c, 0, budget); +} + +function canonKeyAt(c: Canon, depth: number, budget: { left: number }): string { + const key = canonKeyInner(c, depth, budget); + budget.left -= key.length; + return key; +} + +function canonKeyInner(c: Canon, depth: number, budget: { left: number }): string { + if (depth > 6 || budget.left <= 0) return "…"; + const canonKey = (n: Canon, d: number) => canonKeyAt(n, d, budget); + switch (c.k) { + case "prim": + return c.p; + case "lit": + return JSON.stringify(c.value); + case "unit": + return c.u; + case "top": + return c.t; + case "param": + return `param ${c.name}`; + case "opaque": + return `opaque(${c.reason})`; + case "array": + return `${c.readonly ? "readonly " : ""}${canonKey(c.elem, depth + 1)}[]`; + case "tuple": + return `[${c.elems.map((e) => `${e.rest ? "..." : ""}${canonKey(e.type, depth + 1)}${e.optional ? "?" : ""}`).join(", ")}]`; + case "union": + return c.arms.map((a) => canonKey(a, depth + 1)).join(" | "); + case "obj": { + if (c.nominal !== undefined) { + const args = c.typeArgs(); + return args.length === 0 + ? c.nominal + : `${c.nominal}<${args.map((a) => canonKey(a, depth + 1)).join(", ")}>`; + } + const props = c + .props() + .map((p) => `${p.name}${p.optional ? "?" : ""}: ${canonKey(p.type(), depth + 1)}`); + const sig = (s: CanonSig): string => + `(${s.params + .map( + (p) => `${p.rest ? "..." : ""}${p.optional ? "?" : ""}${canonKey(p.type(), depth + 1)}`, + ) + .join(", ")}) => ${canonKey(s.ret(), depth + 1)}`; + const calls = c.call().map(sig); + const constructs = c.construct().map((s) => `new ${sig(s)}`); + const index = c + .index() + .map((i) => `[${canonKey(i.key, depth + 1)}]: ${canonKey(i.value(), depth + 1)}`); + return `{ ${[...calls, ...constructs, ...index, ...props].join("; ")} }`; + } + } +} + +/** The pairs a comparison takes as equal while it runs, and the order they + * were assumed in, so that a failed comparison withdraws what it assumed. */ +interface Assumptions { + pairs: Map>; + trail: [Canon, Canon][]; +} + +/** Whether two canonical nodes are the same type, structurally: unlike + * canonKey, every part is compared (signature parameters, index + * signatures, type arguments, any depth). A pair met again while it is + * being compared is taken as equal, which closes the comparison of + * recursive types; when the comparison fails, the pairs assumed during it + * are withdrawn. */ +export function canonEqual( + a: Canon, + b: Canon, + assumed: Assumptions = { pairs: new Map(), trail: [] }, +): boolean { + if (a === b) return true; + if (a.k !== b.k) return false; + let pairs = assumed.pairs.get(a); + if (pairs?.has(b)) return true; + if (pairs === undefined) assumed.pairs.set(a, (pairs = new Set())); + const mark = assumed.trail.length; + pairs.add(b); + assumed.trail.push([a, b]); + const equal = sameCanon(a, b, assumed); + if (!equal) + while (assumed.trail.length > mark) { + const [x, y] = assumed.trail.pop()!; + assumed.pairs.get(x)!.delete(y); + } + return equal; +} + +function sameCanon(a: Canon, b: Canon, assumed: Assumptions): boolean { + const eq = (x: Canon, y: Canon): boolean => canonEqual(x, y, assumed); + const sigsEqual = (xs: readonly CanonSig[], ys: readonly CanonSig[]): boolean => + xs.length === ys.length && + xs.every((x, i) => { + const y = ys[i]!; + return ( + x.generic === y.generic && + x.params.length === y.params.length && + x.params.every( + (p, j) => + p.optional === y.params[j]!.optional && + p.rest === y.params[j]!.rest && + eq(p.type(), y.params[j]!.type()), + ) && + eq(x.ret(), y.ret()) + ); + }); + switch (a.k) { + case "prim": + return a.p === (b as typeof a).p; + case "lit": + return a.value === (b as typeof a).value; + case "unit": + return a.u === (b as typeof a).u; + case "top": + return a.t === (b as typeof a).t; + case "param": + return a.name === (b as typeof a).name; + case "opaque": + return a.ts === (b as typeof a).ts; + case "array": { + const o = b as typeof a; + return a.readonly === o.readonly && eq(a.elem, o.elem); + } + case "tuple": { + const o = b as typeof a; + return ( + a.elems.length === o.elems.length && + a.elems.every( + (e, i) => + e.optional === o.elems[i]!.optional && + e.rest === o.elems[i]!.rest && + eq(e.type, o.elems[i]!.type), + ) + ); + } + case "union": { + const o = b as typeof a; + return ( + a.arms.length === o.arms.length && + a.arms.every((x) => o.arms.some((y) => eq(x, y))) && + o.arms.every((y) => a.arms.some((x) => eq(x, y))) + ); + } + case "obj": { + const o = b as typeof a; + if (a.nominal !== o.nominal) return false; + if (a.nominal !== undefined) { + const ax = a.typeArgs(); + const ox = o.typeArgs(); + return ax.length === ox.length && ax.every((x, i) => eq(x, ox[i]!)); + } + const ap = a.props(); + const op = o.props(); + if (ap.length !== op.length) return false; + for (const p of ap) { + const q = op.find((x) => x.name === p.name); + if (q === undefined || q.optional !== p.optional || !eq(p.type(), q.type())) return false; + } + const ai = a.index(); + const oi = o.index(); + return ( + ai.length === oi.length && + ai.every((x, i) => eq(x.key, oi[i]!.key) && eq(x.value(), oi[i]!.value())) && + sigsEqual(a.call(), o.call()) && + sigsEqual(a.construct(), o.construct()) + ); + } + } +} + +export class Normalizer { + private readonly interned = new Map(); + /** A number for each type parameter an environment binds: parameters + * with the same name are distinct keys. */ + private readonly paramIds = new Map(); + + constructor(private readonly checker: ts.TypeChecker) {} + + canon(type: ts.Type, env: CanonEnv = new Map()): Canon { + const id = (type as { id?: number }).id; + const key = id === undefined ? null : `${id}|${this.envKey(env)}`; + if (key !== null) { + const known = this.interned.get(key); + if (known !== undefined) return known; + } + const node = this.build(type, env, key); + if (node.src === undefined) node.src = type; + if (key !== null && !this.interned.has(key)) this.interned.set(key, node); + return node; + } + + private envKey(env: CanonEnv): string { + // A resolver's bindings are the caller's current instantiation: a + // normalizer is used for one instantiation at a time. + if (typeof env === "function") return "fn"; + if (env.size === 0) return ""; + const parts: string[] = []; + for (const [param, bound] of env) { + let id = this.paramIds.get(param); + if (id === undefined) this.paramIds.set(param, (id = this.paramIds.size)); + parts.push(`${id}=${(bound as { id?: number }).id ?? "?"}`); + } + return parts.sort().join(","); + } + + private build(type: ts.Type, env: CanonEnv, key: string | null): Canon { + const flags = type.flags; + if (flags & ts.TypeFlags.Any) return { k: "top", t: "any" }; + if (flags & ts.TypeFlags.Unknown) return { k: "top", t: "unknown" }; + if (flags & ts.TypeFlags.NonPrimitive) return { k: "top", t: "object" }; + if (flags & ts.TypeFlags.Never) return { k: "unit", u: "never" }; + if (flags & ts.TypeFlags.Void) return { k: "unit", u: "void" }; + if (flags & ts.TypeFlags.Undefined) return { k: "unit", u: "undefined" }; + if (flags & ts.TypeFlags.Null) return { k: "unit", u: "null" }; + if (flags & ts.TypeFlags.TemplateLiteral) return { k: "prim", p: "string" }; + if (flags & (ts.TypeFlags.StringLiteral | ts.TypeFlags.NumberLiteral)) { + const value = (type as { value?: string | number }).value; + if (value !== undefined) return { k: "lit", value }; + } + if (flags & ts.TypeFlags.BooleanLiteral) + return { k: "lit", value: this.checker.typeToString(type) === "true" }; + if (!type.isUnionType()) + for (const [flag, p] of PRIM_FLAGS) if (flags & flag) return { k: "prim", p }; + if (flags & ts.TypeFlags.TypeParameter) { + const symbol = type.getSymbol(); + const bound = + typeof env === "function" + ? (env(type) ?? undefined) + : symbol === undefined + ? undefined + : env.get(symbol); + if (bound !== undefined && bound !== type) return this.canon(bound, env); + return { k: "param", name: symbol === undefined ? "?" : ts.symbolName(symbol) }; + } + if (type.isUnionType()) return this.union(ts.constituentTypes(type), env); + if (type.isIntersectionType()) return this.intersection(type, env, key); + if (flags & (ts.TypeFlags.Conditional | ts.TypeFlags.IndexedAccess | ts.TypeFlags.Index)) + return { k: "opaque", ts: type, reason: "symbolic type operator" }; + if (flags & ts.TypeFlags.Object) return this.object(type, env, key); + return { k: "opaque", ts: type, reason: `flags ${flags}` }; + } + + /** Flat, deduplicated, never-free; `void` reads as `undefined`. */ + private union(parts: readonly ts.Type[], env: CanonEnv): Canon { + const arms: Canon[] = []; + const add = (c: Canon): void => { + if (c.k === "union") { + for (const a of c.arms) add(a); + return; + } + if (c.k === "unit" && c.u === "never") return; + const arm: Canon = c.k === "unit" && c.u === "void" ? { k: "unit", u: "undefined" } : c; + // Objects dedupe by identity (they intern by checker type): comparing + // their members would walk into a recursive type still being built. + if (arms.some((a) => (arm.k === "obj" ? a === arm : canonEqual(a, arm)))) return; + arms.push(arm); + }; + for (const part of parts) add(this.canon(part, env)); + // Units last, in a fixed order; the rest keep the checker's order. + const unitRank = (c: Canon): number => + c.k !== "unit" ? 0 : c.u === "null" ? 1 : c.u === "undefined" ? 2 : 3; + arms.sort((a, b) => unitRank(a) - unitRank(b)); + if (arms.length === 0) return { k: "unit", u: "never" }; + if (arms.length === 1) return arms[0]!; + // `true | false` is `boolean`. + const bools = arms.filter((a) => a.k === "lit" && typeof a.value === "boolean"); + if (bools.length === 2) { + const rest = arms.filter((a) => !(a.k === "lit" && typeof a.value === "boolean")); + const merged: Canon[] = [{ k: "prim", p: "boolean" }, ...rest]; + return merged.length === 1 ? merged[0]! : { k: "union", arms: merged }; + } + return { k: "union", arms }; + } + + /** Equal constituents collapse; object-like constituents without + * signatures merge through the checker's resolved properties; a + * primitive branded with an object is the primitive. */ + private intersection(type: ts.Type, env: CanonEnv, key: string | null): Canon { + const parts = ts.constituentTypes(type).map((t) => this.canon(t, env)); + if (parts.every((p) => canonEqual(p, parts[0]!))) return parts[0]!; + // Narrowed parameters: `T & {}` (and `T & ({} | null)`'s object part) + // keep T without its undefined and null arms. + const empty = (p: Canon): boolean => + p.k === "obj" && + p.nominal === undefined && + p.props().length === 0 && + p.index().length === 0 && + p.call().length === 0 && + p.construct().length === 0; + const rest = parts.filter((p) => !empty(p)); + if (rest.length === 1 && rest.length < parts.length) { + const only = rest[0]!; + if (only.k !== "union") return only.k === "unit" ? { k: "unit", u: "never" } : only; + const arms = only.arms.filter((a) => a.k !== "unit"); + return arms.length === 1 ? arms[0]! : { k: "union", arms }; + } + const prims = parts.filter((p) => p.k === "prim" || p.k === "lit"); + if (prims.length === 1 && parts.every((p) => p.k === "prim" || p.k === "lit" || p.k === "obj")) + return prims[0]!; + if (parts.every((p) => p.k === "obj" || (p.k === "top" && p.t === "object"))) { + const merged = this.object(type, env, key); + // A class (or mixin instance) refined by other object types is that + // class, with its type arguments: the nominal table answers it, not a + // merged record. + let refined: { nominal: string; typeArgs: () => readonly Canon[] } | undefined; + for (const p of parts) + if (p.k === "obj" && p.nominal !== undefined) + refined ??= { nominal: p.nominal, typeArgs: p.typeArgs }; + if (refined !== undefined && merged.k === "obj" && merged.nominal === undefined) { + merged.nominal = refined.nominal; + merged.typeArgs = refined.typeArgs; + } + return merged; + } + return { k: "opaque", ts: type, reason: "intersection" }; + } + + private object(type: ts.Type, env: CanonEnv, key: string | null): Canon { + const checker = this.checker; + // Arrays and tuples enter the intern table before their elements are + // normalized, so a recursive alias (`type Tree = Tree[] | number`) + // closes on the node under construction. + if (checker.isArrayType(type)) { + const [elem] = checker.getTypeArguments(type as ts.TypeReference); + const node: Canon & { k: "array" } = { + k: "array", + elem: { k: "top", t: "unknown" }, + readonly: + type.getSymbol() !== undefined && ts.symbolName(type.getSymbol()!) === "ReadonlyArray", + }; + if (key !== null) this.interned.set(key, node); + if (elem !== undefined) node.elem = this.canon(elem, env); + return node; + } + if (checker.isTupleType(type)) { + const ref = type as ts.TupleTypeReference; + // The element flags live on the tuple shape: the type itself, or the + // target of a reference to it. + const shape = + (ref.elementFlags as ts.ElementFlags[] | undefined) !== undefined + ? ref + : (ref.getTarget() as ts.TupleType | undefined); + const flags = shape?.elementFlags ?? []; + const elems: { type: Canon; optional: boolean; rest: boolean }[] = []; + const node: Canon = { k: "tuple", elems }; + if (key !== null) this.interned.set(key, node); + checker.getTypeArguments(ref).forEach((a, i) => { + const f = flags[i] ?? ts.ElementFlags.Required; + elems.push({ + type: this.canon(a, env), + optional: (f & ts.ElementFlags.Optional) !== 0, + rest: (f & (ts.ElementFlags.Rest | ts.ElementFlags.Variadic)) !== 0, + }); + }); + return node; + } + // A mapped type the checker could not resolve (over an unbound or + // symbolically bound parameter) shows no members: irreducible here. + if ( + ((type as ts.ObjectType).objectFlags & ts.ObjectFlags.Mapped) !== 0 && + checker.getPropertiesOfType(type).length === 0 && + checker.getIndexInfosOfType(type).length === 0 + ) + return { k: "opaque", ts: type, reason: "unresolved mapped type" }; + const symbol = type.getSymbol(); + const decl = symbol?.declarations[0]; + const nominal = + decl !== undefined && (ts.isClassDeclaration(decl) || ts.isClassExpression(decl)) + ? ts.symbolName(symbol!) + : decl !== undefined && + ts.isInterfaceDeclaration(decl) && + ts.getSourceFileOfNode(decl).isDeclarationFile && + /\/lib\.[^/]*\.d\.ts$/.test(ts.getSourceFileOfNode(decl).fileName) + ? ts.symbolName(symbol!) + : undefined; + const aliasSymbol = type.getAliasSymbol(); + const alias: AliasRef | undefined = + aliasSymbol === undefined + ? undefined + : { + name: ts.symbolName(aliasSymbol), + args: type.getAliasTypeArguments().map((a) => this.canon(a, env)), + tsType: type, + }; + let typeArgs: readonly Canon[] | undefined; + let props: readonly CanonProp[] | undefined; + let index: readonly CanonIndex[] | undefined; + let call: readonly CanonSig[] | undefined; + let construct: readonly CanonSig[] | undefined; + const sigs = (list: readonly ts.Signature[]): CanonSig[] => + list.map((s) => ({ + params: s.getParameters().map((p) => { + const pd = p.declarations[0]; + const param = pd !== undefined && ts.isParameter(pd) ? pd : undefined; + return { + name: ts.symbolName(p), + optional: param?.questionToken !== undefined || param?.initializer !== undefined, + rest: param?.dotDotDotToken !== undefined, + type: () => this.canon(checker.getTypeOfSymbol(p), env), + }; + }), + ret: () => this.canon(checker.getReturnTypeOfSignature(s), env), + generic: s.getTypeParameters().length > 0, + })); + const node: Canon = { + k: "obj", + ...(nominal !== undefined ? { nominal } : {}), + ...(alias !== undefined ? { alias } : {}), + typeArgs: () => + (typeArgs ??= + nominal !== undefined && + ((type as ts.ObjectType).objectFlags & ts.ObjectFlags.Reference) !== 0 + ? checker.getTypeArguments(type as ts.TypeReference).map((a) => this.canon(a, env)) + : []), + props: () => + (props ??= checker.getPropertiesOfType(type).map((p) => ({ + name: ts.symbolName(p), + optional: (p.flags & ts.SymbolFlags.Optional) !== 0, + readonly: false, + type: () => this.canon(checker.getTypeOfSymbol(p), env), + }))), + index: () => + (index ??= checker.getIndexInfosOfType(type).map((info) => ({ + key: this.canon(info.keyType, env), + value: () => this.canon(info.type, env), + }))), + call: () => (call ??= sigs(checker.getCallSignatures(type))), + construct: () => (construct ??= sigs(checker.getConstructSignatures(type))), + }; + // Recursion closes here: members resolve lazily, so a back-reference + // reaches this node through the intern table. + if (key !== null) this.interned.set(key, node); + return node; + } +} diff --git a/packages/compiler/src/frontend/decl-projection.ts b/packages/compiler/src/frontend/decl-projection.ts index eaf7486df..eebe1f773 100644 --- a/packages/compiler/src/frontend/decl-projection.ts +++ b/packages/compiler/src/frontend/decl-projection.ts @@ -27,6 +27,59 @@ * scriptc-perf/results/design-decl-boundary/DESIGN.md. */ import * as ts from "./ts7/adapter.js"; +import { declarationSelector, originAt } from "./lowering/decl-origin.js"; + +/** Per user file, the origin (lowering/decl-origin.ts) of each boundary + * cast in source order: the package declaration the cast's type comes + * from, so a failing cast names it. Set by planDeclProjection, reset per + * program load. */ +const boundaryOrigins = new Map(); + +export function resetDeclBoundaryOrigins(): void { + boundaryOrigins.clear(); +} + +/** The origin of the `index`-th boundary cast of a file, or null. */ +export function declBoundaryOrigin(fileName: string, index: number): string | null { + return boundaryOrigins.get(fileName)?.[index] ?? null; +} + +/** The package declaration a projected expression's type comes from, in + * the declaration world. */ +function castOrigin(checker: ts.TypeChecker, node: ts.Node, type: ts.Type): string | null { + let inner = node as ts.Expression; + while ( + ts.isParenthesizedExpression(inner) || + ts.isAwaitExpression(inner) || + ts.isNonNullExpression(inner) + ) + inner = inner.expression; + let decl: ts.Node | undefined; + let suffix = ""; + try { + if (ts.isCallExpression(inner) || ts.isNewExpression(inner)) { + decl = checker.getResolvedSignature(inner)?.declaration; + suffix = "()"; + } else { + const name = ts.isPropertyAccessExpression(inner) + ? inner.name + : ts.isIdentifier(inner) + ? inner + : null; + if (name === null) return null; + let symbol = checker.getSymbolAtLocation(name); + if (symbol !== undefined && symbol.flags & ts.SymbolFlags.Alias) + symbol = checker.getAliasedSymbol(symbol); + decl = symbol?.declarations[0]; + } + if (decl === undefined) return null; + const selector = declarationSelector(decl); + if (selector === null) return null; + return originAt(decl, `${selector}${suffix}`, checker.typeToString(type)); + } catch { + return null; + } +} /** Alias names of projected types start with this prefix; the lowering * recognizes boundary casts and parameters by it. */ @@ -185,8 +238,24 @@ class TypePrinter { readonly declarations: string[] = []; private readonly inProgress = new Map(); + /** While set, printing registers no recursive alias (projectable()). */ + private dry = false; + constructor(private readonly checker: ts.TypeChecker) {} + /** Whether a type projects, without naming it in the generated file. */ + projectable(type: ts.Type): boolean { + if (type.flags & ANYISH) return false; + const was = this.dry; + this.dry = true; + try { + const text = this.print(type, 0); + return text !== null && text !== "unknown"; + } finally { + this.dry = was; + } + } + /** The alias naming a projectable type, or null. Top-level any/unknown * never projects. */ project(type: ts.Type): string | null { @@ -212,7 +281,12 @@ class TypePrinter { if (f & ts.TypeFlags.Null) return "null"; if (f & ts.TypeFlags.Undefined) return "undefined"; if (f & ts.TypeFlags.EnumLiteral || f & ts.TypeFlags.Enum) return null; - if (f & ts.TypeFlags.BooleanLiteral) return type.intrinsicName === "true" ? "true" : "false"; + // A boolean literal type carries its value (`intrinsicName` is not + // set for it by every checker facade). + if (f & ts.TypeFlags.BooleanLiteral) + return (type as ts.BooleanLiteralType).value === true || type.intrinsicName === "true" + ? "true" + : "false"; if (f & ts.TypeFlags.StringLiteral) return JSON.stringify(String(type.value)); if (f & ts.TypeFlags.NumberLiteral) { const v = Number(type.value); @@ -341,7 +415,7 @@ class TypePrinter { if (props.length > 0 && readonlyCount === props.length) return null; const text = `{ ${members.join("; ")} }`; if (frame.name !== null) { - this.declarations.push(`type ${frame.name} = ${text};`); + if (!this.dry) this.declarations.push(`type ${frame.name} = ${text};`); return frame.name; } return text; @@ -498,6 +572,41 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection | return undefined; } }; + const stringIndex = (checker: ts.TypeChecker, type: ts.Type): boolean => { + try { + return checker + .getIndexInfosOfType(type) + .some((info) => (info.keyType.flags & ts.TypeFlags.String) !== 0); + } catch { + return false; + } + }; + /** Whether the JavaScript world types a package value as an object + * literal that lacks a member its declaration promises: a property (an + * expando `out.max = ...` on a non-empty literal, which the JavaScript + * checker does not add to the literal's type) or a string index + * signature (`Record` over a literal). Lowering the value + * at the narrower literal type would drop the member or fix the key set, + * so it crosses at its declared type instead. */ + const missesDeclaredMembers = (jsType: ts.Type, declType: ts.Type): boolean => { + if (!(jsType.flags & ts.TypeFlags.Object) || !(declType.flags & ts.TypeFlags.Object)) + return false; + const literal = jsType.getSymbol(); + if (literal === undefined || (literal.flags & ts.SymbolFlags.ObjectLiteral) === 0) return false; + try { + if (declChecker.isArrayType(declType) || declChecker.isTupleType(declType)) return false; + if (stringIndex(declChecker, declType) && !stringIndex(jsChecker, jsType)) return true; + return declChecker + .getPropertiesOfType(declType) + .some( + (prop) => + !String(prop.escapedName).startsWith("__@") && + jsChecker.getPropertyOfType(jsType, String(prop.escapedName)) === undefined, + ); + } catch { + return false; + } + }; const namesPackageSymbol = (node: ts.Node): boolean => { let found = false; const visit = (n: ts.Node): void => { @@ -517,9 +626,18 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection | const declSf = input.declProgram.getSourceFile(file); if (jsSf === undefined || declSf === undefined || jsSf.text !== declSf.text) continue; const fileEdits: Edit[] = []; - /** Candidate expressions (by js node) with their alias. */ - const exprCandidates = new Map(); + const fileOrigins: (string | null)[] = []; + /** Candidate expressions (by js node) with their declared type, named + * only for the casts that survive suppression. */ + const exprCandidates = new Map(); const exprOrder: ts.Node[] = []; + /** Reads whose projectable receiver the JavaScript world types `any`: + * kept only when nothing types the receiver. */ + const deferred = new Set(); + /** Package calls whose declared type does not project. */ + const unprojected = new Set(); + /** The declaration-world twin of every walked node. */ + const twins = new Map(); const considerType = (jsNode: ts.Node, declNode: ts.Node): boolean => { if ( @@ -581,7 +699,14 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection | return; } const jsType = typeAt(jsChecker, jsNode); - if (!isAnyish(jsType)) return; + if (jsType === null) return; + // A typed JavaScript value crosses only where its object type lacks a + // member the declaration promises (missesDeclaredMembers). + const typedJs = !isAnyish(jsType); + if (typedJs) { + const declType = typeAt(declChecker, declNode); + if (declType === null || !missesDeclaredMembers(jsType, declType)) return; + } // Package-produced? const d = declNode as ts.Expression; let produced = false; @@ -603,8 +728,18 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection | } else if (ts.isPropertyAccessExpression(d) || ts.isElementAccessExpression(d)) { const recvDecl = typeAt(declChecker, d.expression); const recvJs = typeAt(jsChecker, (jsNode as ts.PropertyAccessExpression).expression); - const recvProjectable = recvDecl !== null && printer.project(recvDecl) !== null; + const recvProjectable = recvDecl !== null && printer.projectable(recvDecl); produced = !recvProjectable || (recvJs !== null && !isAnyish(recvJs)); + // A projectable receiver the JavaScript world types `any` is + // usually typed by another candidate. One that is a package value + // nothing projects stays `any`, such as a safeParse result narrowed + // to its success arm (the whole result is not data: its error arm + // is a class): the read crosses here instead, if the suppression + // below confirms the receiver stays untyped. + if (!produced) { + produced = true; + deferred.add(jsNode); + } } else if (ts.isAwaitExpression(d)) { const inner = unwrapTransparent(d.expression); produced = @@ -633,6 +768,12 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection | declaredInPackage(resolveAlias(raw)); } if (!produced) return; + if (isCall && !deferred.has(jsNode)) { + // Recorded before the contextual-type test below: a package value + // whose type does not project stays `any` wherever it flows. + const t = typeAt(declChecker, d); + if (t === null || !printer.projectable(t)) unprojected.add(jsNode); + } // A consumer that takes anything needs no conversion. try { const ctx = declChecker.getContextualType(d); @@ -642,9 +783,18 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection | } const declType = typeAt(declChecker, d); if (declType === null) return; - const alias = printer.project(declType); - if (alias === null) return; - exprCandidates.set(jsNode, alias); + if (!printer.projectable(declType)) { + // An index signature over values that do not project (package + // interfaces): the literal's fixed key set would still answer for + // keys it lacks, so the value stays untyped and every read is the + // JavaScript read. + if (typedJs && stringIndex(declChecker, declType)) { + exprCandidates.set(jsNode, "any"); + exprOrder.push(jsNode); + } + return; + } + exprCandidates.set(jsNode, declType); exprOrder.push(jsNode); }; @@ -703,6 +853,7 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection | const walk = (jsNode: ts.Node, declNode: ts.Node): void => { if (jsNode.kind !== declNode.kind) return; + twins.set(jsNode, declNode); if (ts.isTypeNode(jsNode) || ts.isExpressionWithTypeArguments(jsNode)) { if (considerType(jsNode, declNode)) return; } else { @@ -749,21 +900,67 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection | result = willBeTyped(decl.initializer, depth + 1); else if (ts.isParameter(decl) && annotated.has(decl)) result = true; else if (ts.isBindingElement(decl)) result = false; + else if ( + (ts.isVariableDeclaration(decl) || ts.isParameter(decl)) && + decl.type !== undefined + ) + result = annotationWillBeTyped(decl.type); } symbolTyped.set(sym, result); return result; } return false; }; + /** An annotation in this file whose declaration-world type projects: + * after the rewrite it names the projected structure (a package type + * is replaced by its alias, a user alias of one has its right-hand + * side replaced). */ + const annotationWillBeTyped = (type: ts.TypeNode): boolean => { + const twin = twins.get(type); + if (twin === undefined) return false; + let declType: ts.Type | null; + try { + declType = declChecker.getTypeFromTypeNode(twin as ts.TypeNode); + } catch { + return false; + } + return declType !== null && printer.projectable(declType); + }; const receiverOf = (e: ts.Node): ts.Node | null => { if (ts.isPropertyAccessExpression(e) || ts.isElementAccessExpression(e)) return e.expression; if (ts.isIdentifier(e)) return null; return null; }; const kept: ts.Node[] = []; + /** A receiver that stays `any` after the rewrite: a package call whose + * type does not project, or a user const bound to one. */ + const staysUntyped = (e: ts.Node): boolean => { + const u = unwrapTransparent(e as ts.Expression); + if (unprojected.has(u)) return true; + if (!ts.isIdentifier(u)) return false; + let sym: ts.Symbol | undefined; + try { + sym = jsChecker.getSymbolAtLocation(u); + } catch { + return false; + } + const decls = sym?.declarations ?? []; + if (decls.length !== 1) return false; + const decl = decls[0]!; + return ( + ts.isVariableDeclaration(decl) && + decl.type === undefined && + decl.initializer !== undefined && + decl.parent !== undefined && + ts.isVariableDeclarationList(decl.parent) && + (decl.parent.flags & ts.NodeFlags.Const) !== 0 && + unprojected.has(unwrapTransparent(decl.initializer)) + ); + }; for (const e of exprOrder) { const recv = receiverOf(e); if (recv !== null && willBeTyped(recv)) continue; + if (deferred.has(e) && (recv === null || !staysUntyped(recv))) continue; // An identifier candidate that is a user binding never gets here // (only package imports are candidates). kept.push(e); @@ -800,7 +997,15 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection | ) continue; } - const alias = exprCandidates.get(e)!; + const candidate = exprCandidates.get(e)!; + const alias = candidate === "any" ? "any" : printer.project(candidate); + if (alias === null) continue; + // Only casts to a projected alias are boundary casts the lowering + // counts (an `as any` cast is not one), so only they take a slot. + if (candidate !== "any") { + const twin = twins.get(e); + fileOrigins.push(twin === undefined ? null : castOrigin(declChecker, twin, candidate)); + } const start = ts.tokenStartOfNode(e); const stmt = e.parent; let prefix = "("; @@ -835,6 +1040,7 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection | if (fileEdits.length === 0) continue; edits.set(file, fileEdits); + if (fileOrigins.some((o) => o !== null)) boundaryOrigins.set(file, fileOrigins); } if (edits.size === 0) return null; diff --git a/packages/compiler/src/frontend/dts-paths.ts b/packages/compiler/src/frontend/dts-paths.ts index 009d1c333..7e53a2a9b 100644 --- a/packages/compiler/src/frontend/dts-paths.ts +++ b/packages/compiler/src/frontend/dts-paths.ts @@ -7,20 +7,36 @@ export { tsgoPath } from "./ts7/session-path.js"; const require = createRequire(import.meta.url); let installedDeclarationRoot: string | null = null; +/** The paths `declarationPath` answered, by name, for the current root. + * Lowering asks for them once per mapped type (isFallbackDtsPath), and each + * answer without an installed root costs a package resolution. */ +const declarationPaths = new Map(); /** A native distribution supplies its relocatable declaration directory. */ export function setDeclarationRoot(root: string): void { installedDeclarationRoot = root; + declarationPaths.clear(); } function declarationPath(name: string): string { + let path = declarationPaths.get(name); + if (path === undefined) { + path = resolveDeclarationPath(name); + declarationPaths.set(name, path); + } + return path; +} + +function resolveDeclarationPath(name: string): string { return tsgoPath( installedDeclarationRoot === null ? name === "scriptc.d.ts" ? require.resolve("@scriptc/compiler/scriptc.d.ts") : name === "scriptc-overrides.d.ts" ? require.resolve("@scriptc/compiler/scriptc-overrides.d.ts") - : require.resolve("@scriptc/compiler/scriptc-node-fallback.d.ts") + : name === "scriptc-node-fallback.d.ts" + ? require.resolve("@scriptc/compiler/scriptc-node-fallback.d.ts") + : join(dirname(require.resolve("@scriptc/compiler/scriptc-node-fallback.d.ts")), name) : join(installedDeclarationRoot, name), ); } @@ -56,6 +72,9 @@ export function compilerOwnedFileName(file: string): string | null { for (const declaration of [ambientDtsPath(), overridesDtsPath(), fallbackDtsPath()]) { if (path === declaration) return `scriptc:${path.slice(path.lastIndexOf("/") + 1)}`; } + const modules = fallbackModuleRoot() + "/"; + if (path.startsWith(modules)) + return `scriptc:${FALLBACK_MODULE_DIRECTORY}/${path.slice(modules.length)}`; return null; } @@ -75,14 +94,138 @@ export function overridesDtsPath(): string { return declarationPath("scriptc-overrides.d.ts"); } -/** Path of the shipped FALLBACK declarations (console, process, node:fs) — - * part of the program only when the target project has no @types/node. - * With @types/node, the project's real Node types stand in and this file - * stands down (its declaration forms would collide). */ +/** Path of the shipped FALLBACK declarations (the console and process + * globals and the modules they name) — part of the program only when the + * target project has no @types/node. With @types/node, the project's real + * Node types stand in and this file stands down (its declaration forms + * would collide). */ export function fallbackDtsPath(): string { return declarationPath("scriptc-node-fallback.d.ts"); } +const FALLBACK_MODULE_DIRECTORY = "node-fallback"; + +/** The fallback declarations of the other Node modules, one file per module + * under node-fallback/, by specifier (both spellings). Each file references + * the files of the modules its declarations name. */ +const FALLBACK_MODULE_FILES: Readonly> = { + "assert": "assert.d.ts", + "assert/strict": "assert-strict.d.ts", + "async_hooks": "async_hooks.d.ts", + "buffer": "buffer.d.ts", + "child_process": "child_process.d.ts", + "cluster": "cluster.d.ts", + "constants": "constants.d.ts", + "crypto": "crypto.d.ts", + "dgram": "dgram.d.ts", + "diagnostics_channel": "diagnostics_channel.d.ts", + "dns": "dns.d.ts", + "dns/promises": "dns-promises.d.ts", + "events": "events.d.ts", + "fs": "fs.d.ts", + "fs/promises": "fs-promises.d.ts", + "graceful-fs": "graceful-fs.d.ts", + "http": "http.d.ts", + "http2": "http2.d.ts", + "https": "https.d.ts", + "module": "module.d.ts", + "net": "net.d.ts", + "node:assert": "assert.d.ts", + "node:assert/strict": "assert-strict.d.ts", + "node:async_hooks": "async_hooks.d.ts", + "node:buffer": "buffer.d.ts", + "node:child_process": "child_process.d.ts", + "node:cluster": "cluster.d.ts", + "node:constants": "constants.d.ts", + "node:crypto": "crypto.d.ts", + "node:dgram": "dgram.d.ts", + "node:diagnostics_channel": "diagnostics_channel.d.ts", + "node:dns": "dns.d.ts", + "node:dns/promises": "dns-promises.d.ts", + "node:events": "events.d.ts", + "node:fs": "fs.d.ts", + "node:fs/promises": "fs-promises.d.ts", + "node:http": "http.d.ts", + "node:http2": "http2.d.ts", + "node:https": "https.d.ts", + "node:module": "module.d.ts", + "node:net": "net.d.ts", + "node:os": "os.d.ts", + "node:path": "path.d.ts", + "node:path/posix": "path-posix.d.ts", + "node:path/win32": "path-win32.d.ts", + "node:process": "process.d.ts", + "node:punycode": "punycode.d.ts", + "node:querystring": "querystring.d.ts", + "node:stream": "stream.d.ts", + "node:stream/consumers": "stream-consumers.d.ts", + "node:stream/promises": "stream-promises.d.ts", + "node:stream/web": "stream-web.d.ts", + "node:string_decoder": "string_decoder.d.ts", + "node:test": "test.d.ts", + "node:timers": "timers.d.ts", + "node:timers/promises": "timers-promises.d.ts", + "node:tls": "tls.d.ts", + "node:url": "url.d.ts", + "node:util": "util.d.ts", + "node:util/types": "util-types.d.ts", + "node:v8": "v8.d.ts", + "node:worker_threads": "worker_threads.d.ts", + "node:zlib": "zlib.d.ts", + "os": "os.d.ts", + "path": "path.d.ts", + "path/posix": "path-posix.d.ts", + "path/win32": "path-win32.d.ts", + "process": "process.d.ts", + "punycode": "punycode.d.ts", + "querystring": "querystring.d.ts", + "stream": "stream.d.ts", + "stream/consumers": "stream-consumers.d.ts", + "stream/promises": "stream-promises.d.ts", + "stream/web": "stream-web.d.ts", + "string_decoder": "string_decoder.d.ts", + "timers": "timers.d.ts", + "timers/promises": "timers-promises.d.ts", + "tls": "tls.d.ts", + "url": "url.d.ts", + "util": "util.d.ts", + "util/types": "util-types.d.ts", + "v8": "v8.d.ts", + "worker_threads": "worker_threads.d.ts", + "zlib": "zlib.d.ts", +}; + +function fallbackModuleRoot(): string { + return declarationPath(FALLBACK_MODULE_DIRECTORY); +} + +/** The fallback declaration file of the Node module `specifier`, or null. */ +export function fallbackModulePath(specifier: string): string | null { + return Object.hasOwn(FALLBACK_MODULE_FILES, specifier) + ? declarationPath(`${FALLBACK_MODULE_DIRECTORY}/${FALLBACK_MODULE_FILES[specifier]}`) + : null; +} + +const QUOTED_SPECIFIER = /["'`]((?:node:)?[a-z][a-z0-9_]*(?:[-/][a-z0-9_]+)?)["'`]/g; + +/** The fallback declaration files of the Node modules `text` names in a + * string literal: an over-approximation of the modules its imports, requires, + * import types and JSDoc types can name. */ +export function fallbackModulesNamedIn(text: string): string[] { + const files = new Set(); + for (const match of text.matchAll(QUOTED_SPECIFIER)) { + const file = fallbackModulePath(match[1]!); + if (file !== null) files.add(file); + } + return [...files]; +} + +/** True for the fallback declarations: the core file and the module files. */ +export function isFallbackDtsPath(file: string): boolean { + const path = tsgoPath(file); + return path === fallbackDtsPath() || path.startsWith(fallbackModuleRoot() + "/"); +} + /** True for files belonging to the adopted Node type surface: the * @types/node package itself and undici-types (its dependency — the * web-platform globals: fetch/Response/AbortSignal/ReadableStream/...). diff --git a/packages/compiler/src/frontend/fork-target.test.ts b/packages/compiler/src/frontend/fork-target.test.ts index 5e56cea5d..23982976a 100644 --- a/packages/compiler/src/frontend/fork-target.test.ts +++ b/packages/compiler/src/frontend/fork-target.test.ts @@ -3,7 +3,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { expect, test, vi } from "vitest"; import { forkTargetPaths } from "./fork-target.js"; -import { fallbackDtsPath } from "./dts-paths.js"; +import { fallbackDtsPath, fallbackModulePath } from "./dts-paths.js"; import { Ts7Host } from "./ts7/program-host.js"; import { Ts7Api } from "./ts7/rpc-api.js"; import { ModuleKind, ModuleResolutionKind } from "./ts7/enums.js"; @@ -35,12 +35,15 @@ test("fork discovery batches only candidate bindings and preserves aliases and s `, ); const host = new Ts7Host((options) => new Ts7Api(options), { cwd: directory }); - const program = host.createProgram([entry, declarations], { - noLib: true, - types: [], - module: ModuleKind.ESNext, - moduleResolution: ModuleResolutionKind.Bundler, - }); + const program = host.createProgram( + [entry, declarations, fallbackModulePath("node:child_process")!], + { + noLib: true, + types: [], + module: ModuleKind.ESNext, + moduleResolution: ModuleResolutionKind.Bundler, + }, + ); const checker = program.getTypeChecker(); const batch = vi.spyOn(checker, "prefetchSymbolNodesExact"); const whole = vi.spyOn(checker.raw, "getTypeOfSymbol"); @@ -65,3 +68,59 @@ test("fork discovery batches only candidate bindings and preserves aliases and s rmSync(directory, { recursive: true, force: true }); } }); + +/** forkTargetPaths over the entry of a program made of `files`, and + * whether it asked the checker for any binding. */ +function discover(files: Record): { targets: string[]; queried: boolean } { + const directory = mkdtempSync(join(tmpdir(), "scriptc-fork-gate-")); + for (const [name, text] of Object.entries(files)) writeFileSync(join(directory, name), text); + const entry = join(directory, "main.ts"); + const host = new Ts7Host((options) => new Ts7Api(options), { cwd: directory }); + const program = host.createProgram( + [entry, fallbackDtsPath(), fallbackModulePath("node:child_process")!], + { + noLib: true, + types: [], + module: ModuleKind.ESNext, + moduleResolution: ModuleResolutionKind.Bundler, + }, + ); + const batch = vi.spyOn(program.getTypeChecker(), "prefetchSymbolNodesExact"); + try { + const targets = forkTargetPaths(program, [program.getSourceFile(entry)!]).map((path) => + path.slice(directory.length + 1), + ); + return { targets, queried: batch.mock.calls.length > 0 }; + } finally { + batch.mockRestore(); + program.dispose(); + host.close(); + rmSync(directory, { recursive: true, force: true }); + } +} + +test("fork discovery asks the checker nothing when no file names node:child_process", () => { + expect( + discover({ + "main.ts": + 'function fork(url: URL) { return url; }\nfork(new URL("./child.ts", import.meta.url));\n', + }), + ).toEqual({ targets: [], queried: false }); +}); + +test("fork discovery reads a module specifier spelled with escapes", () => { + expect( + discover({ + "main.ts": + 'import { fork } from "node:child\\u005fprocess";\nfork(new URL("./child.ts", import.meta.url));\n', + }), + ).toEqual({ targets: ["child.ts"], queried: true }); +}); + +test("fork discovery reads a package imports entry that maps node:child_process", () => { + const result = discover({ + "package.json": JSON.stringify({ type: "module", imports: { "#cp": "node:child_process" } }), + "main.ts": 'import { fork } from "#cp";\nfork(new URL("./child.ts", import.meta.url));\n', + }); + expect(result.queried).toBe(true); +}); diff --git a/packages/compiler/src/frontend/fork-target.ts b/packages/compiler/src/frontend/fork-target.ts index 38c8f5f07..398e1b0aa 100644 --- a/packages/compiler/src/frontend/fork-target.ts +++ b/packages/compiler/src/frontend/fork-target.ts @@ -1,7 +1,14 @@ import { resolve } from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; import * as ts from "./ts7/adapter.js"; -import { fallbackDtsPath, isNodeTypesPath, tsgoPath } from "./dts-paths.js"; +import { + ambientDtsPath, + isFallbackDtsPath, + isNodeTypesPath, + overridesDtsPath, + tsgoPath, +} from "./dts-paths.js"; +import { nearestPackageImports } from "./resolve.js"; import { canonicalBuiltinModule } from "./builtin-modules.js"; import { moduleSourceCandidates } from "./module-source-candidates.js"; @@ -15,7 +22,7 @@ export function isNodeGlobal(program: ts.Program, expression: ts.Identifier): bo const source = ts.getSourceFileOfNode(declaration); return ( source.isDeclarationFile && - (tsgoPath(source.fileName) === fallbackDtsPath() || isNodeTypesPath(source.fileName)) + (isFallbackDtsPath(source.fileName) || isNodeTypesPath(source.fileName)) ); }) ); @@ -292,9 +299,77 @@ export function forkCallModulePath(program: ts.Program, call: ts.CallExpression) return staticForkModulePath(program, call.arguments.nodes[0]); } +/** The ways a string or template literal can spell one character of a + * module name: the character, \xHH, \uHHHH, \u{H…}, a legacy octal escape, + * or the character after a backslash where that is not another escape. */ +function spelledCharacter(ch: string): string { + const code = ch.charCodeAt(0); + const hex = code.toString(16); + const forms = [ + ch, + "\\\\x" + hex.padStart(2, "0"), + "\\\\u" + hex.padStart(4, "0"), + "\\\\u\\{0*" + hex + "\\}", + "\\\\" + code.toString(8), + ]; + if (!"bfnrtvux0123456789".includes(ch)) forms.push("\\\\" + ch); + return "(?:" + forms.join("|") + ")"; +} + +/** "child_process" with escapes and line continuations in any of its + * characters (case-insensitive, a superset). */ +const SPELLED_CHILD_PROCESS = new RegExp( + [..."child_process"].map(spelledCharacter).join("(?:\\\\(?:\\r\\n|[\\n\\r\\u2028\\u2029]))*"), + "i", +); + +/** Whether a call in the program can reach node:child_process's fork. A + * fork binding is an import or alias whose chain names the module in a + * module specifier (a string), or a '#' specifier the nearest package.json + * "imports" field maps. Node's type declarations and scriptc's own declare + * the module, and alias it only as node:child_process, so they are not + * read; any other file that names the module in a string holds + * "child_process" in its text, or an escape that spells it. */ +function namesChildProcess(program: ts.Program): boolean { + const known = program.analysis.namesChildProcess; + if (known !== null) return known; + const own = new Set([ambientDtsPath(), overridesDtsPath()]); + // Implementation files are already fetched; any other file is read as + // the host serves it, without fetching its syntax tree. + const texts = new Map(); + for (const sourceFile of program.getImplementationSourceFiles()) + texts.set(tsgoPath(sourceFile.fileName), sourceFile.text); + const scopes = new Set(); + let names = false; + for (const fileName of program.getSourceFileNames()) { + const path = tsgoPath(fileName); + if (isNodeTypesPath(fileName) || isFallbackDtsPath(fileName) || own.has(path)) continue; + const text = texts.get(path) ?? program.getSourceText(fileName) ?? ""; + if ( + text.includes("child_process") || + (text.includes("\\") && SPELLED_CHILD_PROCESS.test(text)) + ) { + names = true; + break; + } + const imports = nearestPackageImports(fileName); + if (imports === undefined || scopes.has(imports)) continue; + scopes.add(imports); + if (JSON.stringify(imports).includes("child_process")) { + names = true; + break; + } + } + program.analysis.namesChildProcess = names; + return names; +} + /** Source-order static fork targets in the supplied files, deduplicated by * resolved path. */ export function forkTargetPaths(program: ts.Program, files: readonly ts.SourceFile[]): string[] { + // Without a binding of node:child_process no call is a fork, and the + // checker queries below are skipped. + if (!namesChildProcess(program)) return []; const targets: string[] = []; const seen = new Set(); for (const sourceFile of files) { diff --git a/packages/compiler/src/frontend/input-tracker.test.ts b/packages/compiler/src/frontend/input-tracker.test.ts index b4f3957f5..a320eb2bd 100644 --- a/packages/compiler/src/frontend/input-tracker.test.ts +++ b/packages/compiler/src/frontend/input-tracker.test.ts @@ -270,3 +270,87 @@ test("synchronous tracking restores parents after throws and propagates unstable trackedFileExists(join(dir, "outside.ts")); expect(parent.snapshot()).toEqual(before); }); + +test("a settled file is checked by its identity, and any write invalidates it", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-inputs-")); + scratch.push(dir); + const source = join(dir, "entry.ts"); + await writeFile(source, "export const answer = 1;\n"); + const written = fs.statSync(source); + // The file was last changed long enough before the read. + vi.useFakeTimers({ toFake: ["Date"] }); + vi.setSystemTime(Date.now() + 5000); + const tracker = new FrontendInputTracker(); + try { + tracker.run(() => trackedReadFile(source)); + } finally { + vi.useRealTimers(); + } + const snapshot = tracker.snapshot(); + const probe = snapshot.probes.find((entry) => entry.op === "file"); + expect(probe?.op === "file" && probe.identity !== undefined).toBe(true); + expect(validFrontendInputSnapshot(snapshot)).toBe(true); + const reads = vi.mocked(fs.readFileSync); + reads.mockClear(); + expect(frontendInputsStillMatch(snapshot)).toBe(true); + expect(reads.mock.calls.filter(([path]) => path === source)).toHaveLength(0); + // A same-size rewrite that restores the modification time still changes + // the change time. + await writeFile(source, "export const answer = 2;\n"); + fs.utimesSync(source, written.atime, written.mtime); + expect(frontendInputsStillMatch(snapshot)).toBe(false); +}); + +test("a file changed shortly before the read is hashed on every check", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-inputs-")); + scratch.push(dir); + const source = join(dir, "entry.ts"); + await writeFile(source, "export const answer = 1;\n"); + const tracker = new FrontendInputTracker(); + tracker.run(() => trackedReadFile(source)); + const snapshot = tracker.snapshot(); + const probe = snapshot.probes.find((entry) => entry.op === "file"); + expect(probe?.op === "file" && probe.identity).toBeUndefined(); + const reads = vi.mocked(fs.readFileSync); + reads.mockClear(); + expect(frontendInputsStillMatch(snapshot)).toBe(true); + expect(reads.mock.calls.filter(([path]) => path === source)).toHaveLength(1); +}); + +test("a read before and a read after a file settles record one probe", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-inputs-")); + scratch.push(dir); + const source = join(dir, "entry.ts"); + await writeFile(source, "export const answer = 1;\n"); + const tracker = new FrontendInputTracker(); + // The first read comes right after the write, the second once the file + // has been unchanged long enough for its identity to stand for it. + tracker.run(() => trackedReadFile(source)); + vi.useFakeTimers({ toFake: ["Date"] }); + vi.setSystemTime(Date.now() + 5000); + try { + tracker.run(() => trackedReadFile(source)); + } finally { + vi.useRealTimers(); + } + const snapshot = tracker.snapshot(); + expect(snapshot.stable).toBe(true); + const probe = snapshot.probes.find((entry) => entry.op === "file"); + expect(probe?.op === "file" && probe.identity !== undefined).toBe(true); + expect(frontendInputsStillMatch(snapshot)).toBe(true); + // The identity still invalidates the snapshot on any write. + await writeFile(source, "export const answer = 2;\n"); + expect(frontendInputsStillMatch(snapshot)).toBe(false); +}); + +test("two reads of a file that saw different content prevent cache publication", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-inputs-")); + scratch.push(dir); + const source = join(dir, "entry.ts"); + await writeFile(source, "export const answer = 1;\n"); + const tracker = new FrontendInputTracker(); + tracker.run(() => trackedReadFile(source)); + await writeFile(source, "export const answer = 2;\n"); + tracker.run(() => trackedReadFile(source)); + expect(tracker.snapshot().stable).toBe(false); +}); diff --git a/packages/compiler/src/frontend/input-tracker.ts b/packages/compiler/src/frontend/input-tracker.ts index d27ab6285..5dd80dde5 100644 --- a/packages/compiler/src/frontend/input-tracker.ts +++ b/packages/compiler/src/frontend/input-tracker.ts @@ -12,7 +12,10 @@ import { observeSourceText } from "./ts7/ast-node.js"; * behind an old resolution answer. */ export type FrontendInputProbe = - | { op: "file"; path: string; digest: string } + /** `identity` (fileIdentity) lets a later check skip hashing the file. */ + | { op: "file"; path: string; digest: string; identity?: string } + /** A read of the file's raw bytes (files that need not be text). */ + | { op: "bytes"; path: string; digest: string } | { op: "read-error"; path: string } | { op: "kind"; path: string; kind: "file" | "directory" | "other" | "missing" } | { op: "entries"; path: string; files: string[]; directories: string[] } @@ -53,6 +56,52 @@ function frontendSourceDigest(text: string): string { return createHash("sha256").update(text).digest("hex"); } +/** How long a file must have been unchanged before a read for its identity + * to stand for its content: a later write then gets a newer change time + * even on file systems whose timestamps tick once a second or every two. */ +const IDENTITY_SETTLE_MS = 2000; + +/** The device, inode, size, modification time and change time of `path`. A + * write changes the change time, which no tool can set back, so an equal + * identity means the content is the one hashed with it. */ +function fileIdentity(path: string): string | null { + try { + const info = statSync(path); + return `${info.dev}:${info.ino}:${info.size}:${info.mtimeMs}:${info.ctimeMs}`; + } catch { + return null; + } +} + +/** The text of `path` with its identity when the identity was the same before + * and after the read and the file had been unchanged for IDENTITY_SETTLE_MS. */ +function readWithIdentity(path: string): { text: string; identity: string | null } { + const readAt = Date.now(); + const before = statSync(path); + const text = readFileSync(path, "utf8"); + const after = fileIdentity(path); + const identity = `${before.dev}:${before.ino}:${before.size}:${before.mtimeMs}:${before.ctimeMs}`; + const settled = + before.mtimeMs <= readAt - IDENTITY_SETTLE_MS && before.ctimeMs <= readAt - IDENTITY_SETTLE_MS; + return { text, identity: settled && after === identity ? identity : null }; +} + +/** Two reads of one file that saw the same content, as one probe: a read + * before the file had been unchanged for IDENTITY_SETTLE_MS records no + * identity, and a later read of the same bytes does. The probe keeps the + * identity, which the settled read proved for that content. Null when the + * reads differ in anything else. */ +function sameFileContent( + previous: FrontendInputProbe, + probe: FrontendInputProbe, +): FrontendInputProbe | null { + if (previous.op !== "file" || probe.op !== "file" || previous.digest !== probe.digest) + return null; + if (previous.identity === undefined) return probe; + if (probe.identity === undefined || probe.identity === previous.identity) return previous; + return null; +} + function pathKind(path: string): Extract["kind"] { try { const info = statSync(path); @@ -111,12 +160,17 @@ export class FrontendInputTracker { const key = `${probe.op}\0${probe.path}`; if (probe.op === "text" && this.probes.has(key)) return; const previous = this.probes.get(key); - if (previous !== undefined && JSON.stringify(previous) !== JSON.stringify(probe)) { + const merged = previous === undefined ? null : sameFileContent(previous, probe); + if ( + merged === null && + previous !== undefined && + JSON.stringify(previous) !== JSON.stringify(probe) + ) { // An input changed while this frontend was running. The build may // still finish normally, but it is not safe to publish as a cache key. this.stable = false; } - this.probes.set(key, probe); + this.probes.set(key, merged ?? probe); if (probe.op === "file") { // A successful content read supersedes earlier existence probes for the // same path only when they also observed a file. A missing/non-file @@ -199,8 +253,11 @@ export function trackedReadFile(path: string): string | null { const virtual = virtualSources.get(path); if (virtual !== undefined) return virtual; try { - const text = readFileSync(path, "utf8"); - record({ op: "file", path, digest: frontendSourceDigest(text) }); + const { text, identity } = readWithIdentity(path); + const digest = frontendSourceDigest(text); + record( + identity === null ? { op: "file", path, digest } : { op: "file", path, digest, identity }, + ); return text; } catch { record({ op: "read-error", path }); @@ -208,6 +265,21 @@ export function trackedReadFile(path: string): string | null { } } +/** A file's raw bytes, recorded by their digest. */ +export function trackedReadBytes(path: string): Uint8Array | null { + path = resolve(path); + const virtual = virtualSources.get(path); + if (virtual !== undefined) return new TextEncoder().encode(virtual); + try { + const bytes = new Uint8Array(readFileSync(path)); + record({ op: "bytes", path, digest: createHash("sha256").update(bytes).digest("hex") }); + return bytes; + } catch { + record({ op: "read-error", path }); + return null; + } +} + export function trackedFileExists(path: string): boolean { path = resolve(path); if (virtualSources.has(path)) return true; @@ -337,12 +409,24 @@ export function frontendInputsStillMatch( } switch (probe.op) { case "file": { + // An unchanged identity stands for the content hashed with it. + if (probe.identity !== undefined && fileIdentity(probe.path) === probe.identity) + return true; try { return frontendSourceDigest(readFileSync(probe.path, "utf8")) === probe.digest; } catch { return false; } } + case "bytes": { + try { + return ( + createHash("sha256").update(readFileSync(probe.path)).digest("hex") === probe.digest + ); + } catch { + return false; + } + } case "read-error": { try { readFileSync(probe.path, "utf8"); @@ -443,6 +527,11 @@ export function frontendInputsSemanticallyMatch( probes.push(probe); continue; } + if (probe.identity !== undefined && fileIdentity(probe.path) === probe.identity) { + currentSources.set(probe.path, previous); + probes.push(probe); + continue; + } let current: string; try { current = readFileSync(probe.path, "utf8"); @@ -460,7 +549,7 @@ export function frontendInputsSemanticallyMatch( if (rawText.has(probe.path) || !isSemanticEquivalent(probe.path, previous, current)) return null; changed.push({ path: probe.path, previous, current }); - probes.push({ ...probe, digest: currentDigest }); + probes.push({ op: "file", path: probe.path, digest: currentDigest }); } const adjusted: FrontendInputSnapshot = { ...snapshot, probes }; if (!frontendInputsStillMatch(adjusted, exclusions)) return null; @@ -488,6 +577,12 @@ export function validFrontendInputSnapshot(snapshot: unknown): snapshot is Front if (typeof value.path !== "string" || typeof value.op !== "string") return false; switch (value.op) { case "file": + return ( + typeof value.digest === "string" && + /^[0-9a-f]{64}$/.test(value.digest) && + (value.identity === undefined || typeof value.identity === "string") + ); + case "bytes": return typeof value.digest === "string" && /^[0-9a-f]{64}$/.test(value.digest); case "read-error": return true; diff --git a/packages/compiler/src/frontend/lowering/abstract-equality.ts b/packages/compiler/src/frontend/lowering/abstract-equality.ts index 6c74b3546..634ff6f22 100644 --- a/packages/compiler/src/frontend/lowering/abstract-equality.ts +++ b/packages/compiler/src/frontend/lowering/abstract-equality.ts @@ -5,6 +5,7 @@ import { DYN, DYN_HANDLE_KINDS, F64, + STRING, type IrExpr, type IrStmt, type IrType, @@ -285,6 +286,43 @@ export function lowerAbstractEquality( }; return negated ? { kind: "unary", op: "!", operand: result, type: BOOL, loc } : result; } + // Two Dates are equal when they are the same object, as with ===. + if (left.type.kind === "date" && right.type.kind === "date") + return { kind: "bin", op: negated ? "!==" : "===", left, right, type: BOOL, loc }; + // A Date and a string or number compare through the Date's default + // primitive, its toString() text. + if (left.type.kind === "date" || right.type.kind === "date") { + const otherType = left.type.kind === "date" ? right.type : left.type; + if (otherType.kind === "string" || otherType.kind === "f64") { + // Both operands evaluate before the Date converts. + const l = lowerer.declareHiddenLocal("%looseA", left.type); + const r = lowerer.declareHiddenLocal("%looseB", right.type); + const stmts: IrStmt[] = [ + { kind: "varDecl", localId: l.id, init: left, loc }, + { kind: "varDecl", localId: r.id, init: right, loc }, + ]; + const primitive = (value: IrExpr): IrExpr => { + if (value.type.kind !== "date") return value; + const text: IrExpr = { + kind: "libCall", + fn: "date.toString", + args: [value], + type: STRING, + loc, + }; + return otherType.kind === "string" + ? text + : { kind: "libCall", fn: "num.fromString", args: [text], type: F64, loc }; + }; + const a = primitive(varRef(l.id, left.type, loc)); + const b = primitive(varRef(r.id, right.type, loc)); + const result: IrExpr = + otherType.kind === "string" + ? { kind: "strEq", negated, left: a, right: b, type: BOOL, loc } + : { kind: "bin", op: negated ? "!==" : "===", left: a, right: b, type: BOOL, loc }; + return { kind: "seqExpr", stmts, result, type: BOOL, loc }; + } + } if (!supportsPair(lowerer, left.type, right.type)) return null; if (left.kind === "strLit" && right.kind === "strLit") { return { kind: "boolLit", value: (left.value === right.value) !== negated, type: BOOL, loc }; diff --git a/packages/compiler/src/frontend/lowering/argument-counts.ts b/packages/compiler/src/frontend/lowering/argument-counts.ts new file mode 100644 index 000000000..d79e7b43f --- /dev/null +++ b/packages/compiler/src/frontend/lowering/argument-counts.ts @@ -0,0 +1,229 @@ +import * as ts from "../ts7/adapter.js"; +import { bodyReadsArguments } from "../arguments-usage.js"; +import { isJsSourceFile, locOf } from "../program.js"; +import { BOOL, F64, type IrExpr, type IrStmt, type IrType } from "../../ir/ir.js"; +import { varRef } from "../../ir/build.js"; +import { typeKey } from "../type-mapper.js"; +import type { Lowerer } from "./lowerer.js"; +import { isThisParameter } from "./call-signatures.js"; +import { buildFunctionAdapter } from "./coercions/builders.js"; +import { keyedHelperName } from "./stable-names.js"; + +/** `arguments.length` in a top-level function declaration that the program + * also uses as a value. A direct call passes the declared parameters (tsc + * checks the arity), while a call through the value passes the parameter + * count of the function type the value was passed as (`[1].map(f)` calls f + * with 3). The value is an adapter that stores that count in a global slot + * before forwarding; the function reads the slot on entry and clears it, + * so a direct call (the cleared slot) sees the declared count. */ + +/** The count slot of `decl`, or null when its `arguments.length` folds to + * the declared count (or the declaration does not qualify). */ +export function argumentCountSlot(lowerer: Lowerer, decl: ts.FunctionDeclaration): string | null { + const cached = lowerer.argumentCountSlots.get(decl); + if (cached !== undefined) return cached; + let slot: string | null = null; + if ( + decl.body !== undefined && + decl.name !== undefined && + decl.asteriskToken === undefined && + !decl.typeParameters?.nodes.length && + lowerer.isTopLevelFnSymbol(decl.name) && + decl.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) !== true && + !isJsSourceFile(ts.getSourceFileOfNode(decl)) && + fixedArity(decl) && + bodyReadsArguments(decl) && + usedAsValue(lowerer, decl) + ) { + const sig = lowerer.fnSigOf(decl.name); + if (sig) { + slot = `%g.argc.${sig.name}`; + lowerer.globalsList.push({ id: slot, name: "%argc", type: F64, mutable: true }); + } + } + lowerer.argumentCountSlots.set(decl, slot); + return slot; +} + +/** Reads `decl`'s count slot into a hidden local on entry and clears it; the + * local holds the declared count when the slot was clear. */ +export function declareArgumentCount( + lowerer: Lowerer, + decl: ts.FunctionDeclaration, + prologue: IrStmt[], +): void { + const slot = argumentCountSlot(lowerer, decl); + if (slot === null) return; + const loc = locOf(decl); + const local = lowerer.declareHiddenLocal("%argc", F64); + const stored = varRef(slot, F64, loc); + prologue.push( + { + kind: "varDecl", + localId: local.id, + init: { + kind: "ternary", + cond: { + kind: "bin", + op: "===", + left: stored, + right: { kind: "numLit", value: 0, type: F64, loc }, + type: BOOL, + loc, + }, + then: { kind: "numLit", value: declaredCount(decl), type: F64, loc }, + else_: stored, + type: F64, + loc, + }, + loc, + }, + { kind: "assign", localId: slot, value: { kind: "numLit", value: 0, type: F64, loc }, loc }, + ); + lowerer.ctx.argumentCount = local; +} + +/** `decl` as a value at `reference`: the plain closure when every call + * through the expected function type passes the declared count, otherwise + * an adapter that stores the expected type's count in the slot first. */ +export function argumentCountValue( + lowerer: Lowerer, + decl: ts.FunctionDeclaration, + reference: ts.Expression, + closure: IrExpr & { type: IrType & { kind: "func" } }, +): IrExpr { + const slot = argumentCountSlot(lowerer, decl); + if (slot === null) return closure; + const count = contextualCallCount(lowerer, reference); + if (count === null || count === "varies") { + lowerer.unsupported( + "SC1090", + reference, + count === null + ? "functions that read 'arguments.length' used as values where no function type is expected (the count each call passes is not known)" + : "functions that read 'arguments.length' used as values where the expected function type has optional or rest parameters (the count varies per call)", + ); + } + const declared = declaredCount(decl); + if (count <= declared) return closure; + const loc = locOf(reference); + const type = closure.type; + const key = `argc:${slot}:${count}:${typeKey(type)}`; + let name = lowerer.arrHofHelpers.get(key); + if (name === undefined) { + name = keyedHelperName(lowerer, "%argc.adapter", key); + lowerer.arrHofHelpers.set(key, name); + const params = type.params.map((t, i) => ({ localId: `p${i}.0`, name: `p${i}`, type: t })); + lowerer.liftedFns.push( + ...buildFunctionAdapter( + name, + type, + type, + params, + [], + [ + { + kind: "assign", + localId: slot, + value: { kind: "numLit", value: count, type: F64, loc }, + loc, + }, + { + kind: "return", + value: { + kind: "callValue", + callee: varRef("f.0", type, loc), + args: params.map((p) => varRef(p.localId, p.type, loc)), + type: type.ret, + loc, + }, + loc, + }, + ], + loc, + ), + ); + } + return { kind: "call", callee: name, args: [closure], type, loc }; +} + +/** The argument count every call through the function type `node` is + * expected as passes: that type's parameter count when it has one call + * signature with a fixed parameter list (a `this` parameter is type-only), + * "varies" when it has optional or rest parameters, null when there is no + * such type. */ +export function contextualCallCount( + lowerer: Lowerer, + node: ts.Expression, +): number | "varies" | null { + const contextual = lowerer.checker.getContextualType(node); + if (contextual === undefined) return null; + const signatures = lowerer.checker.getCallSignatures( + lowerer.checker.getNonNullableType(contextual), + ); + if (signatures.length !== 1) return null; + const declaration = signatures[0]!.declaration; + if (declaration === undefined) return null; + let count = 0; + for (const parameter of (declaration.parameters?.nodes ?? []) as ts.ParameterDeclaration[]) { + if (isThisParameter(parameter)) continue; + if (parameter.questionToken || parameter.initializer || parameter.dotDotDotToken) + return "varies"; + count++; + } + return count; +} + +/** Whether a program reference to the function declaration or method `fn` + * reads it as a value (`xs.map(f)`, `const g = f`, `xs.map(C.parse)`) rather + * than calling it. Export specifiers and default exports only name a + * function; the importing files' references resolve to it and are checked + * the same way. */ +export function usedAsValue( + lowerer: Lowerer, + fn: ts.FunctionDeclaration | ts.MethodDeclaration, +): boolean { + const name = fn.name; + if (!name || !(ts.isIdentifier(name) || ts.isPrivateIdentifier(name))) return false; + const method = ts.isMethodDeclaration(fn); + const symbolAt = (n: ts.Node): ts.Symbol | null | undefined => + method + ? lowerer.checker.getSymbolAtLocation(n) + : lowerer.resolveValueSymbol(n as ts.Identifier); + const symbol = symbolAt(name); + if (!symbol) return false; + let found = false; + for (const sf of lowerer.program.getImplementationSourceFiles()) { + ts.walkPreorder(sf, (n) => { + if (found) return "stop"; + if (ts.isTypeNode(n)) return "skip"; + if (n === name || !(ts.isIdentifier(n) || ts.isPrivateIdentifier(n))) return undefined; + if (n.text !== name.text || !n.parent) return undefined; + const parent = n.parent; + // A method is read through its member name (`C.parse`, `this.#m`). + const read = ts.isPropertyAccessExpression(parent) && parent.name === n ? parent : n; + if (method && read === n && !ts.isBindingElement(parent)) return undefined; + const use = read.parent; + if (use && ts.isCallExpression(use) && use.expression === read) return undefined; + if (ts.isExportSpecifier(parent) || ts.isExportAssignment(parent)) return undefined; + if (ts.isImportSpecifier(parent) || ts.isImportClause(parent)) return undefined; + if (symbolAt(n) === symbol) found = true; + return undefined; + }); + if (found) break; + } + return found; +} + +function fixedArity(fn: ts.FunctionLikeDeclaration): boolean { + return fn.parameters.nodes.every( + (p) => + p.questionToken === undefined && + p.initializer === undefined && + p.dotDotDotToken === undefined, + ); +} + +function declaredCount(fn: ts.FunctionLikeDeclaration): number { + return fn.parameters.nodes.filter((p) => !isThisParameter(p)).length; +} diff --git a/packages/compiler/src/frontend/lowering/array-values.ts b/packages/compiler/src/frontend/lowering/array-values.ts index 314cdfd24..a8c2d699b 100644 --- a/packages/compiler/src/frontend/lowering/array-values.ts +++ b/packages/compiler/src/frontend/lowering/array-values.ts @@ -3,8 +3,10 @@ import { InternalCompilerError } from "../../errors.js"; import { dynUndefinedExpr, helperLoc, varRef } from "../../ir/build.js"; import { BOOL, + DYN, F64, type IrExpr, + type IrLocal, type IrStmt, type IrType, isIntegerBytesElem, @@ -15,6 +17,7 @@ import { typeKey, unionContainerArmsOk, } from "../../ir/ir.js"; +import { everyExpr } from "../../ir/traverse.js"; import type { Lowerer } from "./lowerer.js"; import type * as ts from "../ts7/adapter.js"; @@ -57,7 +60,33 @@ export function arrayNumberRead(arr: IrExpr, index: IrExpr, loc: SrcLoc): IrExpr }; } -/** Read one stabilized receiver/index pair. Both a hole and present undefined yield undefined. */ +/** A foreign element read into a typed result: it never fits, so the read + * throws the checked conversion's TypeError (array.foreignThrow). */ +export function foreignElementThrow( + lowerer: Lowerer, + arr: IrExpr, + index: IrExpr, + type: IrType, + loc: SrcLoc, +): IrExpr { + return { + kind: "libCall", + fn: "array.foreignThrow", + args: [arr, index, { kind: "strLit", value: lowerer.fmt(type), type: STRING, loc }], + type, + loc, + }; +} + +/** Arrays of this element type can hold elements outside it, stored + * through dynamic views (Lowerer.foreignElementKeys). */ +export function elementMayBeForeign(lowerer: Lowerer, elem: IrType): boolean { + const keys = lowerer.foreignElementKeys; + return keys.size > 0 && elem.kind !== "dyn" && (keys.has("*") || keys.has(typeKey(elem))); +} + +/** Read one stabilized receiver/index pair. Both a hole and present undefined yield undefined; + * a foreign element (state 3) takes the checked conversion into the typed result. */ export function arrayValueRead( lowerer: Lowerer, arr: IrExpr, @@ -67,8 +96,25 @@ export function arrayValueRead( ): IrExpr { const type = arrayValueType(lowerer, elem); const read: IrExpr = { kind: "arrayGet", arr, index, type: elem, loc }; - const missing: IrExpr = + let missing: IrExpr = type.kind === "dyn" ? dynUndefinedExpr(loc) : lowerer.wrappedUndefined(type, loc)!; + if (elementMayBeForeign(lowerer, elem)) { + missing = { + kind: "ternary", + cond: { + kind: "bin", + op: "===", + left: { kind: "arrayState", arr, index, type: F64, loc }, + right: { kind: "numLit", value: 3, type: F64, loc }, + type: BOOL, + loc, + }, + then: foreignElementThrow(lowerer, arr, index, type, loc), + else_: missing, + type, + loc, + }; + } return { kind: "ternary", cond: { @@ -193,6 +239,376 @@ export function unionArrayValueRead( return result; } +/** Expression kinds that evaluate without running other code or writing + * a binding. */ +const PURE_KINDS = new Set([ + "varRef", + "numLit", + "strLit", + "boolLit", + "unitLit", + "bin", + "unary", + "logical", + "ternary", + "toBool", + "fieldGet", + "recordGet", + "arrayGet", + "arrayState", + "arrayHas", + "unionIsTag", + "unionNarrow", + "unionWrap", +]); + +/** The locals evaluating `value` may assign, and whether it may run other + * code (anything but the pure kinds and the read-only element helpers), + * which can assign locals and fields through closures. */ +function valueEffects(lowerer: Lowerer, value: IrExpr): { writes: Set; opaque: boolean } { + const readers = new Set(); + for (const [key, name] of lowerer.arrHofHelpers) if (key.startsWith("idxOr:")) readers.add(name); + const writes = new Set(); + let opaque = false; + everyExpr(value, { + expr: (e) => { + if (e.kind === "assignExpr" || e.kind === "incDec") writes.add(e.localId); + else if (e.kind === "call" ? !readers.has(e.callee) : !PURE_KINDS.has(e.kind)) opaque = true; + return true; + }, + stmt: (s) => { + if (s.kind === "varDecl" || s.kind === "assign") writes.add(s.localId); + else opaque = true; + return true; + }, + }); + return { writes, opaque }; +} + +/** Whether evaluating `e` after `value` gives what it gives before it: a + * literal, or a local and field read `value` cannot change. */ +function unaffectedBy(e: IrExpr, effects: { writes: Set; opaque: boolean }): boolean { + switch (e.kind) { + case "numLit": + case "strLit": + return true; + case "varRef": + return !effects.writes.has(e.localId); + case "bin": + return unaffectedBy(e.left, effects) && unaffectedBy(e.right, effects); + case "fieldGet": + case "recordGet": + return !effects.opaque && unaffectedBy(e.obj, effects); + default: + return false; + } +} + +/** Whether `e` reads a `let` or `var` binding, which code the value runs + * can assign through a closure. */ +function readsMutableLocal(lowerer: Lowerer, e: IrExpr): boolean { + return !everyExpr(e, { + expr: (x) => + x.kind !== "varRef" || lowerer.ctx.locals.find((l) => l.id === x.localId)?.mutable === false, + stmt: () => true, + }); +} + +/** A number, string or boolean element type. Arrays of it hold a present + * undefined in the element's state, so `(T | undefined)[]` stores T + * elements (the type mapper) and a T[] seen as one stays the same array. */ +export function primitiveElement(elem: IrType): boolean { + return elem.kind === "f64" || elem.kind === "string" || elem.kind === "bool"; +} + +/** `value` is the element type with undefined, stored into an array of a + * primitive element type: a present undefined or the element. */ +export function optionalElementValue(lowerer: Lowerer, value: IrExpr, elem: IrType): boolean { + if (!primitiveElement(elem)) return false; + if (value.type.kind === "undefinedT") return true; + return ( + value.type.kind === "union" && + lowerer.armTag(value.type.unionId, UNDEFINED_T) >= 0 && + typeEquals(lowerer.stripUndefinedArm(value.type), elem) + ); +} + +/** A value of a primitive element type that holds a slot until the value + * stored there replaces it (insertedValues), or stands for a slot that is + * not read. */ +export function elementPlaceholder(elem: IrType, loc: SrcLoc): IrExpr { + if (elem.kind === "f64") return { kind: "numLit", value: 0, type: F64, loc }; + if (elem.kind === "bool") return { kind: "boolLit", value: false, type: BOOL, loc }; + return { kind: "strLit", value: "", type: STRING, loc }; +} + +/** Values inserted into an array (push, unshift, splice, a literal), in + * source order. A value that can be undefined (optionalElementValue) is + * kept in a temporary: the insertion takes a placeholder for it, and + * `fill` then stores the value at the placeholder's index. */ +export function insertedValues( + lowerer: Lowerer, + values: readonly IrExpr[], + elem: IrType, + coerce: (value: IrExpr, index: number) => IrExpr, + loc: SrcLoc, +): { + stmts: IrStmt[]; + args: IrExpr[]; + optional: boolean; + fill: (arr: IrExpr, first: IrExpr) => IrStmt[]; +} { + if (!values.some((value) => optionalElementValue(lowerer, value, elem))) + return { stmts: [], args: values.map(coerce), optional: false, fill: () => [] }; + const stmts: IrStmt[] = []; + const args: IrExpr[] = []; + const held: { offset: number; value: IrExpr }[] = []; + values.forEach((value, offset) => { + // Every value is evaluated in order before the insertion. + if (value.type.kind === "undefinedT") { + if (value.kind !== "unitLit") stmts.push({ kind: "exprStmt", expr: value, loc }); + args.push(elementPlaceholder(elem, loc)); + held.push({ offset, value: { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc } }); + return; + } + const temp = lowerer.declareHiddenLocal("%inserted", value.type); + stmts.push({ kind: "varDecl", localId: temp.id, init: value, loc }); + const stable = varRef(temp.id, temp.type, loc); + if (optionalElementValue(lowerer, value, elem)) { + args.push(elementPlaceholder(elem, loc)); + held.push({ offset, value: stable }); + } else { + args.push(coerce(stable, offset)); + } + }); + const fill = (arr: IrExpr, first: IrExpr): IrStmt[] => + held.map(({ offset, value }) => + arrayValueStore( + lowerer, + arr, + offset === 0 + ? first + : { + kind: "bin", + op: "+", + left: first, + right: { kind: "numLit", value: offset, type: F64, loc }, + type: F64, + loc, + }, + value, + elem, + loc, + ), + ); + return { stmts, args, optional: true, fill }; +} + +/** Statements appending `value` to `arr` inside a lifted helper. A value + * that can be undefined in a primitive array (optionalElementValue) is held + * in the helper's local `slot` (which the caller declares with the value's + * type) and stored as a present undefined or the element; any other value + * converts to the element type. */ +export function appendElement( + lowerer: Lowerer, + arr: IrExpr, + value: IrExpr, + elem: IrType, + slot: string, + loc: SrcLoc, +): IrStmt[] { + if (optionalElementValue(lowerer, value, elem) && value.type.kind !== "undefinedT") { + const held = varRef(slot, value.type, loc); + const length: IrExpr = { + kind: "arrIntrinsic", + method: "length", + receiver: arr, + args: [], + type: F64, + loc, + }; + return [ + { kind: "varDecl", localId: slot, init: value, loc }, + arrayValueStore(lowerer, arr, length, held, elem, loc), + ]; + } + return [ + { + kind: "exprStmt", + expr: { + kind: "arrIntrinsic", + method: "push", + receiver: arr, + args: [lowerer.coerceToExpected(value, elem)], + type: F64, + loc, + }, + loc, + }, + ]; +} + +/** The argument a helper passes in place of an element its callback never + * reads (lowerArrayCallback's `unread`): a constant of the parameter's + * type, so the slot stays unread and an element the type cannot hold + * (stored through a dynamic view) passes. Null for a type without one. */ +export function unreadArgument(lowerer: Lowerer, type: IrType, loc: SrcLoc): IrExpr | null { + if (type.kind === "f64") return { kind: "numLit", value: NaN, type: F64, loc }; + if (type.kind === "bool") return { kind: "boolLit", value: false, type: BOOL, loc }; + if (type.kind === "string") return { kind: "strLit", value: "", type: STRING, loc }; + if (type.kind === "union" && lowerer.armTag(type.unionId, UNDEFINED_T) >= 0) + return lowerer.wrappedUndefined(type, loc) ?? null; + return null; +} + +/** The locals of an element snapshot (arraySlotSnapshot): whether the slot + * held an element outside the element type, the value read otherwise, and + * that element's dynamic value otherwise. */ +export interface SlotSnapshot { + isForeign: string; + value: string; + foreign: string; +} + +/** The locals a snapshot of an `elem` element uses. */ +export function slotSnapshotLocals(lowerer: Lowerer, elem: IrType, ids: SlotSnapshot): IrLocal[] { + return [ + { id: ids.isForeign, name: "foreign", type: BOOL, mutable: false }, + { id: ids.value, name: "value", type: arrayValueType(lowerer, elem), mutable: false }, + { id: ids.foreign, name: "element", type: DYN, mutable: false }, + ]; +} + +/** Read the element at `arr[index]` as it is stored, before a callback runs, + * as JavaScript reads it before calling a callback that ignores it: a value + * or a present undefined into `ids.value`, or an element outside the element + * type (stored through a dynamic view) as its dynamic value into + * `ids.foreign`, which `ids.isForeign` tells apart. */ +export function arraySlotSnapshot( + lowerer: Lowerer, + arr: IrExpr, + index: IrExpr, + elem: IrType, + ids: SlotSnapshot, + loc: SrcLoc, +): IrStmt[] { + const valueT = arrayValueType(lowerer, elem); + const isForeign: IrExpr = { kind: "varRef", localId: ids.isForeign, type: BOOL, loc }; + const missing = + valueT.kind === "dyn" ? dynUndefinedExpr(loc) : lowerer.wrappedUndefined(valueT, loc)!; + return [ + { + kind: "varDecl", + localId: ids.isForeign, + init: { + kind: "bin", + op: "===", + left: { kind: "arrayState", arr, index, type: F64, loc }, + right: { kind: "numLit", value: 3, type: F64, loc }, + type: BOOL, + loc, + }, + loc, + }, + { + kind: "varDecl", + localId: ids.value, + init: { + kind: "ternary", + cond: isForeign, + then: missing, + else_: arrayValueRead(lowerer, arr, index, elem, loc), + type: valueT, + loc, + }, + loc, + }, + { + kind: "varDecl", + localId: ids.foreign, + init: { + kind: "ternary", + cond: isForeign, + then: { kind: "libCall", fn: "array.foreign", args: [arr, index], type: DYN, loc }, + else_: dynUndefinedExpr(loc), + type: DYN, + loc, + }, + loc, + }, + ]; +} + +/** Append a snapshot (arraySlotSnapshot) of an element to `out`, an array of + * the same element type, as it was stored: an element outside the element + * type stays one, appended as the view's push appends it. `out` is a + * binding. */ +export function arraySnapshotAppend( + lowerer: Lowerer, + out: IrExpr, + elem: IrType, + ids: SlotSnapshot, + loc: SrcLoc, +): IrStmt { + const end: IrExpr = { + kind: "arrIntrinsic", + method: "length", + receiver: out, + args: [], + type: F64, + loc, + }; + const value: IrExpr = { + kind: "varRef", + localId: ids.value, + type: arrayValueType(lowerer, elem), + loc, + }; + return { + kind: "if", + cond: { kind: "varRef", localId: ids.isForeign, type: BOOL, loc }, + then: [ + { + kind: "exprStmt", + expr: { + kind: "dynInvoke", + recv: { kind: "dynFrom", value: out, liveRef: true, type: DYN, loc }, + method: "push", + calleeName: "push", + args: [{ kind: "varRef", localId: ids.foreign, type: DYN, loc }], + type: DYN, + loc, + }, + loc, + }, + ], + else_: [arrayValueStore(lowerer, out, end, value, elem, loc)], + loc, + }; +} + +/** A snapshot (arraySlotSnapshot) as a value of `type`: the value read, or + * the element outside the element type through the checked conversion, + * which such an element fails as a typed read does. `typed` converts the + * value read. */ +export function arraySnapshotValue( + lowerer: Lowerer, + elem: IrType, + ids: SlotSnapshot, + type: IrType, + typed: (value: IrExpr) => IrExpr, + loc: SrcLoc, +): IrExpr { + return { + kind: "ternary", + cond: { kind: "varRef", localId: ids.isForeign, type: BOOL, loc }, + then: lowerer.coerceToExpected({ kind: "varRef", localId: ids.foreign, type: DYN, loc }, type), + else_: typed({ kind: "varRef", localId: ids.value, type: arrayValueType(lowerer, elem), loc }), + type, + loc, + }; +} + /** Store a stabilized value without changing the array's element ABI. */ export function arrayValueStore( lowerer: Lowerer, @@ -203,6 +619,12 @@ export function arrayValueStore( loc: SrcLoc, ): IrStmt { if (typeEquals(value.type, elem)) return { kind: "arraySet", arr, index, value, loc }; + // An undefined value of a `(T | undefined)[]` (primitiveElement). + if (value.type.kind === "undefinedT" && primitiveElement(elem)) { + const write: IrStmt = { kind: "arraySetUndefined", arr, index, loc }; + if (value.kind === "unitLit") return write; + return { kind: "block", body: [{ kind: "exprStmt", expr: value, loc }, write], loc }; + } if (value.type.kind === "union" && lowerer.armTag(value.type.unionId, UNDEFINED_T) >= 0) { // The tag test and the VALUE arm extraction both consume the value. Keep // an arbitrary expression (especially an array read or call) single @@ -210,10 +632,31 @@ export function arrayValueStore( // owned union temporary to release on every path. const unionId = value.type.unionId; let stable = value; - let prefix: IrStmt[] = []; + const prefix: IrStmt[] = []; + // The stores below evaluate the receiver and the index after the value. + // JavaScript evaluates them first (`out[n++] = src[n]` reads src[1]), so + // either may need its value held before the value runs. + if (value.kind !== "varRef") { + const valueFx = valueEffects(lowerer, value); + const hold = (e: IrExpr, name: string, effects: typeof valueFx): IrExpr => { + if (unaffectedBy(e, effects) && !(effects.opaque && readsMutableLocal(lowerer, e))) + return e; + const temp = lowerer.declareHiddenLocal(name, e.type); + prefix.push({ kind: "varDecl", localId: temp.id, init: e, loc }); + return varRef(temp.id, temp.type, loc); + }; + // The receiver runs before the index and the value, so the index's + // effects can change it too (`a[(a = b, 0)] = c[0]!`). + const indexFx = valueEffects(lowerer, index); + arr = hold(arr, "%arrayTarget", { + writes: new Set([...valueFx.writes, ...indexFx.writes]), + opaque: valueFx.opaque || indexFx.opaque, + }); + index = hold(index, "%arrayIndex", valueFx); + } if (value.kind !== "varRef") { const temp = lowerer.declareHiddenLocal("%arrayValue", value.type); - prefix = [{ kind: "varDecl", localId: temp.id, init: value, loc }]; + prefix.push({ kind: "varDecl", localId: temp.id, init: value, loc }); stable = varRef(temp.id, temp.type, loc); } const write: IrStmt = { @@ -249,6 +692,9 @@ export function lowerSafeIndexRead( const elem = arr.type.elem; if (elem.kind === "void") return null; const resultT = arrayValueType(lowerer, elem); + // An element union that cannot take an undefined arm has no value for a + // missing element. + if (resultT.kind !== "dyn" && lowerer.wrappedUndefined(resultT, loc) === null) return null; const key = `idxOr:${typeKey(elem)}`; let name = lowerer.arrHofHelpers.get(key); if (!name) { @@ -447,6 +893,9 @@ export function tryLowerNumericIndexRead( operand.callee !== lowerer.arrHofHelpers.get(`idxOr:${typeKey(F64)}`) ) return null; + // ToNumber of a foreign element differs from its own value under `+`, + // comparisons and ToBoolean. + if (elementMayBeForeign(lowerer, F64)) return null; const [arr, index] = operand.args; if ( operand.args.length !== 2 || @@ -540,6 +989,8 @@ export function tryFuseIndexRead( if (arr?.type.kind !== "array" || index?.type.kind !== "f64") return null; const elem = arr.type.elem; if (elem.kind === "union" || elem.kind === "void") return null; + // A foreign element is neither the present value nor the missing one. + if (elementMayBeForeign(lowerer, elem)) return null; if (operand.callee !== lowerer.arrHofHelpers.get(`idxOr:${typeKey(elem)}`)) return null; if (arr.kind !== "varRef" || (index.kind !== "varRef" && index.kind !== "numLit")) return null; const loc = operand.loc; diff --git a/packages/compiler/src/frontend/lowering/binding-analysis.ts b/packages/compiler/src/frontend/lowering/binding-analysis.ts index 1987c86ba..c88c06c90 100644 --- a/packages/compiler/src/frontend/lowering/binding-analysis.ts +++ b/packages/compiler/src/frontend/lowering/binding-analysis.ts @@ -19,6 +19,18 @@ export function bindingNeverReassigned( ): boolean { const cached = lowerer.neverReassignedCache.get(sym); if (cached !== undefined) return cached; + const written = fileWritesBinding(sym, ts.getSourceFileOfNode(decl), resolve); + lowerer.neverReassignedCache.set(sym, !written); + return !written; +} + +/** True when something in `file` writes `sym` (the writes + * bindingNeverReassigned counts), resolving identifiers with `resolve`. */ +export function fileWritesBinding( + sym: ts.Symbol, + file: ts.SourceFile, + resolve: (ident: ts.Identifier) => ts.Symbol | null | undefined, +): boolean { let written = false; // Text pre-check keeps the file walk cheap: only same-named // identifiers pay a symbol resolution. @@ -62,9 +74,8 @@ export function bindingNeverReassigned( } ts.forEachChild(n, visit); }; - ts.forEachChild(ts.getSourceFileOfNode(decl), visit); - lowerer.neverReassignedCache.set(sym, !written); - return !written; + ts.forEachChild(file, visit); + return written; } /** Strips the value-preserving wrappers off an expression: parens, diff --git a/packages/compiler/src/frontend/lowering/builtin-namespaces.ts b/packages/compiler/src/frontend/lowering/builtin-namespaces.ts new file mode 100644 index 000000000..c1e40d2e6 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/builtin-namespaces.ts @@ -0,0 +1,286 @@ +/** The standard namespace objects (`Math`, `JSON`, `Reflect`) read as + * JavaScript values. Each is one object per program, built on first use, + * with Node's own properties in Node's order: the functions are writable, + * configurable and non-enumerable, the Math constants are read-only and + * non-configurable, and `Symbol.toStringTag` names the namespace. Function + * members hold the same function values the spelled `Math.abs` read gives; + * members without one hold a function that throws the named fence when + * called. Direct member calls (`Math.abs(x)`) keep their static lowerings. */ +import { + BOOL, + DYN, + F64, + STRING, + SYMBOL_T, + VOID, + type IrExpr, + type IrStmt, + type SrcLoc, + funcOf, +} from "../../ir/ir.js"; +import { helperLoc, varRef } from "../../ir/build.js"; +import { unsupportedDiag } from "../../diagnostics/diagnostic.js"; +import type { Lowerer } from "./lowerer.js"; +import { InternalCompilerError } from "../../errors.js"; +import { mathMemberValue } from "./lower-stdlib.js"; +import { jsonMemberValue } from "./builtins/json.js"; +import { reflectMemberValue } from "./builtins/reflection.js"; + +/** A function member with its `length`, or a constant (`length` -1). */ +interface NamespaceMember { + readonly key: string; + readonly length: number; +} + +/** Node 24's own string-keyed properties of the namespace `name`, in + * order, or null when `name` is not a namespace. */ +function namespaceMembers(name: string): readonly NamespaceMember[] | null { + switch (name) { + case "Math": + return [ + { key: "abs", length: 1 }, + { key: "acos", length: 1 }, + { key: "acosh", length: 1 }, + { key: "asin", length: 1 }, + { key: "asinh", length: 1 }, + { key: "atan", length: 1 }, + { key: "atanh", length: 1 }, + { key: "atan2", length: 2 }, + { key: "ceil", length: 1 }, + { key: "cbrt", length: 1 }, + { key: "expm1", length: 1 }, + { key: "clz32", length: 1 }, + { key: "cos", length: 1 }, + { key: "cosh", length: 1 }, + { key: "exp", length: 1 }, + { key: "floor", length: 1 }, + { key: "fround", length: 1 }, + { key: "hypot", length: 2 }, + { key: "imul", length: 2 }, + { key: "log", length: 1 }, + { key: "log1p", length: 1 }, + { key: "log2", length: 1 }, + { key: "log10", length: 1 }, + { key: "max", length: 2 }, + { key: "min", length: 2 }, + { key: "pow", length: 2 }, + { key: "random", length: 0 }, + { key: "round", length: 1 }, + { key: "sign", length: 1 }, + { key: "sin", length: 1 }, + { key: "sinh", length: 1 }, + { key: "sqrt", length: 1 }, + { key: "tan", length: 1 }, + { key: "tanh", length: 1 }, + { key: "trunc", length: 1 }, + { key: "E", length: -1 }, + { key: "LN10", length: -1 }, + { key: "LN2", length: -1 }, + { key: "LOG10E", length: -1 }, + { key: "LOG2E", length: -1 }, + { key: "PI", length: -1 }, + { key: "SQRT1_2", length: -1 }, + { key: "SQRT2", length: -1 }, + { key: "f16round", length: 1 }, + ]; + case "JSON": + return [ + { key: "parse", length: 2 }, + { key: "stringify", length: 3 }, + { key: "rawJSON", length: 1 }, + { key: "isRawJSON", length: 1 }, + ]; + case "Reflect": + return [ + { key: "defineProperty", length: 3 }, + { key: "deleteProperty", length: 2 }, + { key: "apply", length: 3 }, + { key: "construct", length: 2 }, + { key: "get", length: 2 }, + { key: "getOwnPropertyDescriptor", length: 2 }, + { key: "getPrototypeOf", length: 1 }, + { key: "has", length: 2 }, + { key: "isExtensible", length: 1 }, + { key: "ownKeys", length: 1 }, + { key: "preventExtensions", length: 1 }, + { key: "set", length: 3 }, + { key: "setPrototypeOf", length: 2 }, + ]; + default: + return null; + } +} + +/** Property attribute bits for dyn.namespaceDefine (the members are never + * enumerable). */ +const WRITABLE = 1; +const CONFIGURABLE = 4; + +/** The namespace object the stdlib global `canonical` reads as in + * JavaScript, or null when it is not a namespace. */ +export function builtinNamespaceValue( + lowerer: Lowerer, + canonical: string, + loc: SrcLoc, +): IrExpr | null { + const members = namespaceMembers(canonical); + if (members === null) return null; + const name = `%builtin.namespace.${canonical}`; + if (!lowerer.liftedFns.some((fn) => fn.name === name)) + defineNamespaceBuilder(lowerer, canonical, members, name, helperLoc(loc)); + lowerer.noteEdge(name); + return { kind: "call", callee: name, args: [], type: DYN, loc }; +} + +/** The lifted function that builds the namespace object once and answers + * the same object afterwards. */ +function defineNamespaceBuilder( + lowerer: Lowerer, + canonical: string, + members: readonly NamespaceMember[], + name: string, + loc: SrcLoc, +): void { + const storage = `%g.${name}`; + const ready = `${storage}%ready`; + lowerer.globalsList.push( + { id: storage, name: `${canonical} namespace`, type: DYN, mutable: true }, + { id: ready, name: "%ready", type: BOOL, mutable: true }, + ); + const object = varRef("namespace.0", DYN, loc); + const body: IrStmt[] = [ + { + kind: "if", + cond: varRef(ready, BOOL, loc), + then: [{ kind: "return", value: varRef(storage, DYN, loc), loc }], + else_: null, + loc, + }, + { + kind: "varDecl", + localId: "namespace.0", + init: { + kind: "libCall", + fn: "dyn.namespaceNew", + args: [ + { kind: "strLit", value: canonical, type: STRING, loc }, + { + kind: "dynFrom", + value: { + kind: "libCall", + fn: "sym.wellKnown", + args: [{ kind: "strLit", value: "toStringTag", type: STRING, loc }], + type: SYMBOL_T, + loc, + }, + type: DYN, + loc, + }, + ], + type: DYN, + loc, + }, + loc, + }, + ]; + for (const { key, length } of members) { + const value = + length < 0 + ? constantValue(lowerer, canonical, key, loc) + : functionValue(lowerer, canonical, key, loc); + const attributes = length < 0 ? 0 : WRITABLE | CONFIGURABLE; + body.push({ + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "dyn.namespaceDefine", + args: [ + object, + { kind: "strLit", value: key, type: STRING, loc }, + value, + { kind: "numLit", value: attributes, type: F64, loc }, + { kind: "numLit", value: length, type: F64, loc }, + ], + type: VOID, + loc, + }, + loc, + }); + } + body.push( + { kind: "assign", localId: storage, value: object, loc }, + { + kind: "assign", + localId: ready, + value: { kind: "boolLit", value: true, type: BOOL, loc }, + loc, + }, + { kind: "return", value: varRef(storage, DYN, loc), loc }, + ); + lowerer.liftedFns.push({ + name, + params: [], + locals: [{ id: "namespace.0", name: "namespace", type: DYN, mutable: false }], + returnType: DYN, + body, + loc, + }); +} + +/** A Math constant, boxed. */ +function constantValue(lowerer: Lowerer, canonical: string, key: string, loc: SrcLoc): IrExpr { + const value = canonical === "Math" ? mathMemberValue(lowerer, key, loc) : null; + if (value === null) + throw new InternalCompilerError( + `builtin namespace '${canonical}.${key}' has no constant value`, + ); + return { kind: "dynFrom", value, type: DYN, loc }; +} + +/** A function member: its function value under its own name, or a function + * that throws the named fence when called. */ +function functionValue(lowerer: Lowerer, canonical: string, key: string, loc: SrcLoc): IrExpr { + const value = + canonical === "Math" + ? mathMemberValue(lowerer, key, loc) + : canonical === "JSON" + ? jsonMemberValue(lowerer, key, loc) + : reflectMemberValue(lowerer, key, loc); + if (value !== null) + return value.type.kind === "dyn" + ? value + : { kind: "dynFrom", value, fnName: key, type: DYN, loc }; + return fencedMember(lowerer, `${canonical}.${key}`, key, loc); +} + +/** One function per member without a function value: calling it throws the + * SC1090 fence, while `typeof`, identity and the property's presence stay + * as in Node. */ +function fencedMember(lowerer: Lowerer, display: string, key: string, loc: SrcLoc): IrExpr { + const name = `%builtin.fence.${display}`; + if (!lowerer.liftedFns.some((fn) => fn.name === name)) { + const diag = unsupportedDiag( + "SC1090", + loc, + `'${display}' as a function value (call it directly)`, + ); + lowerer.runtimeFences.push(diag); + lowerer.liftedFns.push({ + name, + params: [], + locals: [], + returnType: DYN, + body: [ + { kind: "runtimeFence", code: diag.code, message: `${diag.message} [${diag.code}]`, loc }, + ], + loc, + }); + } + return { + kind: "dynFrom", + value: { kind: "closure", fnName: name, captures: [], type: funcOf([], DYN), loc }, + fnName: key, + type: DYN, + loc, + }; +} diff --git a/packages/compiler/src/frontend/lowering/builtins/array-classes.ts b/packages/compiler/src/frontend/lowering/builtins/array-classes.ts new file mode 100644 index 000000000..4ed92faa7 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/builtins/array-classes.ts @@ -0,0 +1,757 @@ +/* Classes declared `extends Array`. An instance is an array of the class's + * element type: its struct begins with the runtime's array header (the + * reference count is the array's) and the class's own fields follow it, so + * every array operation applies to the instance unchanged and passing it + * where a plain array is expected passes the same object. The class's + * TypeScript type maps to its element array type; the class's own members, + * construction and `instanceof` view the array as the class object + * (arrayClassView). */ +import * as ts from "../../ts7/adapter.js"; +import { type Lowerer } from "../lowerer.js"; +import { isJsSourceFile, locOf } from "../../program.js"; +import { stripTypeWrappers } from "../typed-arguments.js"; +import { dynUndefinedExpr, nodeThrowExpr } from "../../../ir/build.js"; +import { lowerArrayFromCall, lowerArrayOfCall } from "../containers/array-construction.js"; +import { boundedArrayHofHelper } from "../containers/array-callbacks.js"; +import { storedClassValueType, type ClassInfo } from "../lower-classes.js"; +import { + BOOL, + DYN, + F64, + STRING, + UNDEFINED_T, + arrayOf, + typeEquals, + type IrExpr, + type IrFunction, + type IrLocal, + type IrStmt, + type IrType, + type SrcLoc, +} from "../../../ir/ir.js"; + +/** The element type of `class X extends Array` (`extends Array` in + * JavaScript reads `any[]`, whose elements are checked-dynamic). */ +export function arrayClassBase( + lowerer: Lowerer, + heritage: ts.ExpressionWithTypeArguments, +): { elem: IrType } { + const decl = heritage.parent?.parent; + const self = + decl && ts.isClassLikeDeclaration(decl) + ? lowerer.checker.getSymbolAtLocation(decl.name ?? decl) + : undefined; + const element = heritage.typeArguments?.nodes[0]; + if ( + self && + element && + mentionsClass(lowerer, lowerer.checker.getTypeAtLocation(element), self, new Set()) + ) + lowerer.unsupported("SC1090", heritage, "classes that extend an Array of their own instances"); + const mapped = lowerer.mapTypeOf(lowerer.checker.getTypeAtLocation(heritage)); + // `extends Array` in JavaScript reads `any[]`: dyn elements. + if (isJsSourceFile(ts.getSourceFileOfNode(heritage)) && mapped?.kind !== "array") + return { elem: DYN }; + if (mapped?.kind !== "array") + lowerer.unsupported( + "SC1090", + heritage, + "extending Array with an element type that does not compile", + ); + return { elem: mapped!.kind === "array" ? mapped!.elem : DYN }; +} + +/** Whether `type` is the class `self`'s instance type, or holds it through + * unions, intersections or type arguments (`Group | null`, `Group[]`, + * `Map`): the class's array type would contain itself. + * Another class's members do not count, its instances are references. */ +function mentionsClass( + lowerer: Lowerer, + type: ts.Type, + self: ts.Symbol, + seen: Set, +): boolean { + if (seen.has(type)) return false; + seen.add(type); + if (type.getSymbol() === self) return true; + if (type.isUnionType() || type.isIntersectionType()) + return ts.constituentTypes(type).some((part) => mentionsClass(lowerer, part, self, seen)); + return lowerer.checker + .getTypeArguments(type as ts.TypeReference) + .some((argument) => mentionsClass(lowerer, argument, self, seen)); +} + +/** The class declared `extends Array` at the root of `info`'s chain, or null. */ +export function arrayRootOf(info: ClassInfo | null | undefined): ClassInfo | null { + for (let c = info ?? null; c; c = c.base) if (c.extendsArray) return c; + return null; +} + +/** The array a class instance is (a reinterpret: the same object). */ +export function arrayOfClassInstance(info: ClassInfo, value: IrExpr, loc: SrcLoc): IrExpr { + const root = arrayRootOf(info)!; + return { kind: "upcast", value, type: arrayOf(root.extendsArray!.elem), loc }; +} + +/** The class object an instance of `info` is, viewed from its array type + * (a reinterpret; the value is known to be an instance of the class). */ +export function arrayClassView(info: ClassInfo, value: IrExpr, loc: SrcLoc): IrExpr { + return { kind: "downcast", value, type: { kind: "object", className: info.def.name }, loc }; +} + +/** The receiver of a member that `info` declares, read on `value` (the + * lowered `node`): an array value is viewed as the class object. An array + * whose elements are not the class's (a species result of another element + * type, narrowed back to the class by `instanceof`) is refused. */ +export function arrayClassReceiver( + lowerer: Lowerer, + info: ClassInfo, + value: IrExpr, + node: ts.Node, +): IrExpr { + if (value.type.kind !== "array") return value; + const root = arrayRootOf(info); + if (root?.extendsArray === undefined || !typeEquals(value.type.elem, root.extendsArray.elem)) + lowerer.unsupported( + "SC1090", + node, + "members of an Array subclass on an array whose elements are another type (a species result narrowed back to the class)", + ); + return arrayClassView(info, value, locOf(node)); +} + +function thisArray(info: ClassInfo, thisLocal: IrLocal, loc: SrcLoc): IrExpr { + return arrayOfClassInstance( + info, + { + kind: "varRef", + localId: thisLocal.id, + type: { kind: "object", className: info.def.name }, + loc, + }, + loc, + ); +} + +/** The implicit constructor's super(...args): `pack` is its dyn argument + * array, applied with the Array constructor's length-or-elements rule. */ +export function arraySuperCall( + info: ClassInfo, + thisRef: IrExpr, + pack: IrExpr, + loc: SrcLoc, +): IrStmt { + const elem = arrayRootOf(info)!.extendsArray!.elem; + return { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "array.subclassConstructDyn", + args: [ + arrayOfClassInstance(info, thisRef, loc), + pack, + { kind: "boolLit", value: elem.kind === "dyn", type: BOOL, loc }, + ], + type: { kind: "void" }, + loc, + }, + loc, + }; +} + +/** An explicit super(...) in a class declared `extends Array`: one number + * argument sets the length; otherwise the arguments (spreads included) + * become the elements, with the same one-number rule at run time. */ +export function lowerArraySuperCall( + lowerer: Lowerer, + info: ClassInfo, + thisLocal: IrLocal, + superCall: ts.CallExpression, +): IrStmt[] { + const loc = locOf(superCall); + const elem = arrayRootOf(info)!.extendsArray!.elem; + const args = superCall.arguments.nodes; + if (args.length === 0) return []; + const only = args.length === 1 && !ts.isSpreadElement(args[0]!) ? args[0]! : null; + if (only !== null && (lowerer.typeOf(only).flags & ts.TypeFlags.NumberLike) !== 0) { + return [ + { + kind: "arraySetLength", + arr: thisArray(info, thisLocal, loc), + length: lowerer.lowerExprExpecting(only, { kind: "f64" }), + loc, + }, + ]; + } + const arrT = arrayOf(elem); + const elements: IrExpr[] = []; + const spreads: number[] = []; + for (const arg of args) { + if (ts.isSpreadElement(arg)) { + spreads.push(elements.length); + elements.push(lowerer.lowerExprExpecting(arg.expression, arrT)); + } else elements.push(lowerer.lowerExprExpecting(arg, elem)); + } + const pack: IrExpr = { + kind: "arrayLit", + elems: elements, + ...(spreads.length > 0 ? { spreads } : {}), + type: arrT, + loc, + } as IrExpr; + return [ + { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "array.subclassConstruct", + args: [ + thisArray(info, thisLocal, loc), + pack, + { kind: "boolLit", value: elem.kind === "dyn", type: BOOL, loc }, + ], + type: { kind: "void" }, + loc, + }, + loc, + }, + ]; +} + +/** The ClassInfo of a class declared `extends Array`, by its declaration. */ +export function arrayClassOfDecl( + lowerer: Lowerer, + decl: ts.ClassLikeDeclaration, +): ClassInfo | null { + const symbol = decl.name ? lowerer.checker.getSymbolAtLocation(decl.name) : undefined; + let info = symbol ? lowerer.classBySymbol.get(symbol) : undefined; + if (!info) + for (const candidate of lowerer.classes.values()) if (candidate.decl === decl) info = candidate; + return info && arrayRootOf(info) ? info : null; +} + +/** The class that declares the member `access` names when that class is + * declared `extends Array` (its own fields, methods and accessors), or null + * for the Array members it inherits and every other access. */ +export function arrayClassMemberOwner( + lowerer: Lowerer, + access: ts.PropertyAccessExpression, +): ClassInfo | null { + const symbol = lowerer.checker.getSymbolAtLocation(access.name); + const decl = symbol?.declarations?.[0]; + const owner = decl?.parent; + if (owner === undefined || !ts.isClassLikeDeclaration(owner)) return null; + const declarer = arrayClassOfDecl(lowerer, owner); + if (declarer === null) return null; + // The receiver's own class (a subclass member reached through a base + // spelling is the same instance). + const receiver = lowerer.typeOf(access.expression).getSymbol()?.valueDeclaration; + if (receiver !== undefined && ts.isClassLikeDeclaration(receiver)) + return arrayClassOfDecl(lowerer, receiver) ?? declarer; + return declarer; +} + +/** `x instanceof Array` on a static array (always true: every array value, + * subclass instances included, is an Array) and `x instanceof X` for a + * class X declared `extends Array`: the array's class, read from its ext + * at run time. Null for every other instanceof. */ +export function lowerArrayClassInstanceOf( + lowerer: Lowerer, + expr: ts.BinaryExpression, + loc: SrcLoc, +): IrExpr | null { + if (!ts.isIdentifier(expr.right)) return null; + const leftType = lowerer.mapTypeOf(lowerer.typeOf(expr.left)); + if (lowerer.isStdlibGlobal(expr.right, "Array")) { + // A checked-dynamic value: the Array.isArray test (one realm). + if (leftType?.kind === "dyn") + return { + kind: "dynTest", + test: "array", + value: lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN), + type: BOOL, + loc, + } as IrExpr; + if (leftType?.kind !== "array") return null; + return { + kind: "seqExpr", + stmts: [{ kind: "exprStmt", expr: lowerer.lowerExpr(expr.left), loc }], + result: { kind: "boolLit", value: true, type: BOOL, loc }, + type: BOOL, + loc, + }; + } + const symbol = lowerer.resolveValueSymbol(expr.right); + const decl = symbol?.valueDeclaration; + let info: ClassInfo | null = null; + if (decl !== undefined && ts.isClassLikeDeclaration(decl)) { + info = arrayClassOfDecl(lowerer, decl); + } else { + // A binding that holds a class value (`const C = class extends Array + // {}`): its static class, the only class it can hold while nothing + // extends that class. + const stored = storedClassValueType(lowerer, expr.right); + const held = stored?.kind === "classval" ? lowerer.classes.get(stored.className) : undefined; + if (held === undefined || arrayRootOf(held) === null) return null; + if (held.subclasses.length > 0) + lowerer.unsupported( + "SC1090", + expr, + "'instanceof' against a binding holding a class that extends Array and has subclasses", + ); + info = held; + } + if (info === null) return null; + const value = lowerer.lowerExpr(expr.left); + if (value.type.kind === "array") + return { kind: "instanceOf", value, className: info.def.name, type: BOOL, loc }; + if (value.type.kind === "dyn") + return { + kind: "libCall", + fn: "array.dynIsClass", + args: [value, { kind: "strLit", value: info.def.name, type: STRING, loc }], + type: BOOL, + loc, + }; + if (value.type.kind === "object" && value.type.className === info.def.name) + return { + kind: "seqExpr", + stmts: [{ kind: "exprStmt", expr: value, loc }], + result: { kind: "boolLit", value: true, type: BOOL, loc }, + type: BOOL, + loc, + }; + return null; +} + +/** The class declared `extends Array` that a value of `type` is an instance + * of, or null. */ +export function arrayClassOfType(lowerer: Lowerer, type: ts.Type): ClassInfo | null { + const decl = type.getSymbol()?.valueDeclaration; + return decl !== undefined && ts.isClassLikeDeclaration(decl) + ? arrayClassOfDecl(lowerer, decl) + : null; +} + +/** `x.constructor` on an instance of a class declared `extends Array`: the + * class (the class has no subclasses, so the static type names it). */ +export function lowerArrayClassConstructorRead( + lowerer: Lowerer, + access: ts.PropertyAccessExpression, + loc: SrcLoc, +): IrExpr | null { + if (access.name.text !== "constructor") return null; + const info = arrayClassOfType(lowerer, lowerer.typeOf(access.expression)); + if (info === null) return null; + const value = lowerer.lowerExpr(access.expression); + const cls = lowerer.classValueRef(info, access); + return { + kind: "seqExpr", + stmts: [{ kind: "exprStmt", expr: value, loc }], + result: cls, + type: cls.type, + loc, + }; +} + +/** `%C.arrayFields(a)`: the own fields of an instance of a class declared + * `extends Array` as a dyn object, in declaration order (the descriptor's + * `fields`, which util.inspect and Object.keys read). Fields whose type + * has no dyn form are left out. Null when the class has no such field. */ +export function arrayClassFieldsFn(lowerer: Lowerer, info: ClassInfo): IrFunction | null { + const elem = arrayRootOf(info)!.extendsArray!.elem; + const arrT = arrayOf(elem); + const loc = info.def.loc; + const fields = info.def.fields.filter( + (f) => !f.name.startsWith("%") && !f.name.startsWith("#") && lowerer.dynConvertible(f.type), + ); + if (fields.length === 0) return null; + const name = `%${info.def.name}.arrayFields`; + const self = (): IrExpr => + arrayClassView(info, { kind: "varRef", localId: "a.0", type: arrT, loc }, loc); + const value: IrExpr = { + kind: "dynObjLit", + fields: fields.map((f) => ({ + key: { kind: "strLit", value: f.name, type: STRING, loc }, + value: lowerer.coerceToExpected( + { + kind: "fieldGet", + obj: self(), + className: info.def.name, + field: f.name, + type: f.type, + loc, + }, + DYN, + ), + })), + type: DYN, + loc, + }; + return { + name, + params: [{ localId: "a.0", name: "a", type: arrT }], + returnType: DYN, + locals: [{ id: "a.0", name: "a", type: arrT, mutable: false }], + body: [{ kind: "return", value, loc }], + loc, + }; +} + +/** `%C.species(n, withLength)`: `new C(n)`, or `new C()` without + * `withLength`, as ArraySpeciesCreate, Array.of and Array.from call it (the + * descriptor's `species`). Null when the constructor's parameters cannot + * take those arguments: they must be none, a forwarding rest (dyn or + * number), or a number first parameter whose others may be omitted (whose + * no-argument form throws). */ +/** A constructor parameter the species length argument can fill: a + * number, a checked-dynamic value, or a union with a number arm + * (`length?: number`). */ +function takesLength(lowerer: Lowerer, type: IrType): boolean { + if (type.kind === "f64" || type.kind === "dyn") return true; + if (type.kind !== "union") return false; + return lowerer.unions.get(type.unionId)?.arms.some((arm) => arm.kind === "f64") === true; +} + +export function arrayClassSpeciesFn(lowerer: Lowerer, info: ClassInfo): IrFunction | null { + const elem = arrayRootOf(info)!.extendsArray!.elem; + const arrT = arrayOf(elem); + const loc = info.def.loc; + const n = (): IrExpr => ({ kind: "varRef", localId: "n.0", type: F64, loc }); + const params = info.ctorParams; + // The constructor arguments with the length and without it (null: that + // form throws a TypeError). + let withLength: IrExpr[] | null = null; + let without: IrExpr[] | null = null; + if (params.length === 0) { + withLength = []; + without = []; + } else if (params.length === 1 && params[0]!.mode === "dynRest") { + withLength = [ + { + kind: "dynArrLit", + elems: [{ kind: "dynFrom", value: n(), type: DYN, loc }], + type: DYN, + loc, + }, + ]; + without = [{ kind: "dynArrLit", elems: [], type: DYN, loc }]; + } else if ( + params.length === 1 && + params[0]!.mode === "rest" && + params[0]!.type.kind === "array" && + params[0]!.type.elem.kind === "f64" + ) { + withLength = [{ kind: "arrayLit", elems: [n()], type: params[0]!.type, loc } as IrExpr]; + without = [{ kind: "arrayLit", elems: [], type: params[0]!.type, loc } as IrExpr]; + } else if ( + (params[0]!.mode === "required" || params[0]!.mode === "omittable") && + takesLength(lowerer, params[0]!.type) && + params.slice(1).every((p) => p.mode === "omittable") + ) { + // An omitted argument is what a call without it passes: the + // parameter's default, or undefined. + const blame = info.decl ?? null; + const omitted = (p: (typeof params)[number]): IrExpr => + p.callDefault ?? + (blame + ? lowerer.undefinedArgFor(p.type, loc, blame) + : lowerer.coerceToExpected( + { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc } as IrExpr, + p.type, + )); + const rest = params.slice(1).map(omitted); + // A dyn first parameter (a JavaScript `constructor(size)`) and an + // optional or defaulted one take the length as a number, and their + // omitted value in the no-argument form. + if (params[0]!.type.kind === "dyn" || params[0]!.mode === "omittable") + without = [ + params[0]!.type.kind === "dyn" ? dynUndefinedExpr(loc) : omitted(params[0]!), + ...rest, + ]; + withLength = [ + params[0]!.type.kind === "dyn" + ? { kind: "dynFrom", value: n(), type: DYN, loc } + : lowerer.coerceToExpected(n(), params[0]!.type), + ...params.slice(1).map(omitted), + ]; + } + if (withLength === null) return null; + const construct = (args: IrExpr[]): IrStmt => ({ + kind: "return", + value: arrayOfClassInstance( + info, + { + kind: "new", + className: info.def.name, + args, + type: { kind: "object", className: info.def.name }, + loc, + }, + loc, + ), + loc, + }); + const unsupported: IrStmt = { + kind: "exprStmt", + expr: nodeThrowExpr( + 1, + "", + `constructing ${info.def.jsName ?? info.def.name} without arguments is not supported yet`, + DYN, + loc, + ), + loc, + }; + return { + name: `%${info.def.name}.species`, + params: [ + { localId: "n.0", name: "n", type: F64 }, + { localId: "w.0", name: "withLength", type: BOOL }, + ], + returnType: arrT, + locals: [ + { id: "n.0", name: "n", type: F64, mutable: false }, + { id: "w.0", name: "withLength", type: BOOL, mutable: false }, + ], + body: [ + { + kind: "if", + cond: { kind: "varRef", localId: "w.0", type: BOOL, loc }, + then: [construct(withLength)], + else_: [without !== null ? construct(without) : unsupported], + loc, + }, + ], + loc, + }; +} + +/** Whether the program declares a class that extends Array. Array methods + * consult species only then. */ +export function programDeclaresArrayClass(lowerer: Lowerer): boolean { + for (const info of lowerer.classes.values()) if (info.extendsArray) return true; + return false; +} + +/** The Array methods that create their result through ArraySpeciesCreate: + * whether the species constructor receives the result's length (map, + * slice, splice; the others pass 0), whether the length is set after the + * elements are defined (slice, splice, concat), and whether the method + * calls back into the program (map, filter, flatMap), which it does after + * the species constructor ran. */ +const SPECIES_METHODS: ReadonlyMap< + string, + { resultLength: boolean; setLength: boolean; callback: boolean } +> = new Map([ + ["map", { resultLength: true, setLength: false, callback: true }], + ["filter", { resultLength: false, setLength: false, callback: true }], + ["slice", { resultLength: true, setLength: true, callback: false }], + ["splice", { resultLength: true, setLength: true, callback: false }], + ["concat", { resultLength: false, setLength: true, callback: false }], + ["flat", { resultLength: false, setLength: false, callback: false }], + ["flatMap", { resultLength: false, setLength: false, callback: true }], +]); + +/** An Array method call whose result an Array subclass instance creates + * through its species (scr_arr_species_adopt): the receiver is evaluated + * once into a hidden local that the method's lowering reads, and the + * method's plain result passes through the species constructor when the + * receiver is a subclass instance. Programs that declare no Array subclass + * keep the plain lowering. */ +export function lowerWithArraySpecies( + lowerer: Lowerer, + call: ts.CallExpression, + access: ts.PropertyAccessExpression, + lower: () => IrExpr | null, +): IrExpr | null { + const mode = SPECIES_METHODS.get(access.name.text); + if (mode === undefined || !programDeclaresArrayClass(lowerer)) return lower(); + const receiverType = lowerer.mapTypeOf(lowerer.typeOf(access.expression)); + if (receiverType?.kind !== "array") return lower(); + const loc = locOf(call); + const receiver = lowerer.lowerExpr(access.expression); + if (receiver.type.kind !== "array") return lower(); + const local = lowerer.declareHiddenLocal("%speciesReceiver", receiver.type); + const key = stripTypeWrappers(access.expression); + lowerer.stagedArguments.set(key, { localId: local.id, type: receiver.type }); + let result: IrExpr | null; + try { + result = lower(); + } finally { + lowerer.stagedArguments.delete(key); + } + if (result === null || result.type.kind !== "array") return result; + const receiverRef: IrExpr = { kind: "varRef", localId: local.id, type: receiver.type, loc }; + if (mode.callback) { + // The species constructor runs before the first callback: the + // arguments are evaluated in order, the receiver's length is read, the + // instance is created (with that length for map, 0 otherwise), then + // the method runs to that length and its elements move into the + // instance. + const bounded = + result.kind === "call" && + result.args[0]?.kind === "varRef" && + result.args[0].localId === local.id + ? boundedArrayHofHelper(lowerer, result.callee) + : null; + if (bounded === null || result.kind !== "call") + lowerer.unsupported( + "SC1090", + call, + `'${access.name.text}' in this form when the program declares a class that extends Array`, + ); + const stmts: IrStmt[] = [{ kind: "varDecl", localId: local.id, init: receiver, loc }]; + const args: IrExpr[] = [receiverRef]; + for (const arg of result.args.slice(1)) { + const held = lowerer.declareHiddenLocal("%speciesArgument", arg.type); + stmts.push({ kind: "varDecl", localId: held.id, init: arg, loc }); + args.push({ kind: "varRef", localId: held.id, type: arg.type, loc }); + } + const bound = lowerer.declareHiddenLocal("%speciesLength", F64); + stmts.push({ + kind: "varDecl", + localId: bound.id, + init: { + kind: "arrIntrinsic", + method: "length", + receiver: receiverRef, + args: [], + type: F64, + loc, + }, + loc, + }); + const boundRef: IrExpr = { kind: "varRef", localId: bound.id, type: F64, loc }; + args.push(boundRef); + const instance = lowerer.declareHiddenLocal("%speciesInstance", receiver.type); + const length: IrExpr = mode.resultLength + ? boundRef + : { kind: "numLit", value: 0, type: F64, loc }; + stmts.push({ + kind: "varDecl", + localId: instance.id, + init: { + kind: "libCall", + fn: "array.speciesCreate", + args: [receiverRef, length], + type: receiver.type, + loc, + }, + loc, + }); + return { + kind: "seqExpr", + stmts, + result: { + kind: "libCall", + fn: "array.speciesFinish", + args: [ + { kind: "varRef", localId: instance.id, type: receiver.type, loc }, + { ...result, callee: bounded, args }, + { kind: "boolLit", value: mode.setLength, type: BOOL, loc }, + ], + type: result.type, + loc, + }, + type: result.type, + loc, + }; + } + return { + kind: "seqExpr", + stmts: [{ kind: "varDecl", localId: local.id, init: receiver, loc }], + result: { + kind: "libCall", + fn: "array.speciesAdopt", + args: [ + receiverRef, + result, + { kind: "numLit", value: mode.resultLength ? -1 : 0, type: F64, loc }, + { kind: "boolLit", value: mode.setLength, type: BOOL, loc }, + ], + type: result.type, + loc, + }, + type: result.type, + loc, + }; +} + +/** `C.of(...items)` and `C.from(iterable)` on a class declared `extends + * Array` that declares neither: Array.of / Array.from's elements, in an + * instance the class constructs (`new C(items.length)` for of, `new C()` + * for an iterable's from), as Node's inherited statics do. */ +export function lowerArrayClassStaticCall( + lowerer: Lowerer, + call: ts.CallExpression, + access: ts.PropertyAccessExpression, +): IrExpr | null { + const name = access.name.text; + if (name !== "of" && name !== "from") return null; + if (!ts.isIdentifier(access.expression)) return null; + const symbol = lowerer.resolveValueSymbol(access.expression); + const decl = symbol?.valueDeclaration; + if (decl === undefined || !ts.isClassLikeDeclaration(decl)) return null; + const info = arrayClassOfDecl(lowerer, decl); + if (info === null || lowerer.findStaticOn(info, name)) return null; + const loc = locOf(call); + const args = call.arguments.nodes; + let ctorLength: number; + if (name === "of") { + if (args.some(ts.isSpreadElement)) + lowerer.unsupported("SC1090", call, "spreading arguments into Array.of on an Array subclass"); + ctorLength = args.length; + } else { + const source = args[0] ? lowerer.mapTypeOf(lowerer.typeOf(args[0])) : null; + if ( + source?.kind !== "array" && + source?.kind !== "string" && + source?.kind !== "set" && + source?.kind !== "map" + ) + lowerer.unsupported( + "SC1090", + call, + "Array.from on an Array subclass over a value that is not an array, string, Set or Map", + ); + ctorLength = -1; + } + const plain = + name === "of" + ? lowerArrayOfCall(lowerer, call, access, true) + : lowerArrayFromCall(lowerer, call, access, true); + if (plain === null || plain.type.kind !== "array") return plain; + return { + kind: "libCall", + fn: "array.classConstructAdopt", + args: [ + { kind: "strLit", value: info.def.name, type: STRING, loc }, + plain, + { kind: "numLit", value: ctorLength, type: F64, loc }, + ], + type: plain.type, + loc, + }; +} + +/** A class declared `extends Array` that defines `static [Symbol.species]` + * (Node would construct method results through that constructor). */ +export function refuseArrayClassSpecies(lowerer: Lowerer, decl: ts.ClassLikeDeclaration): void { + for (const member of decl.members.nodes) { + const name = member.name; + if ( + name !== undefined && + ts.isComputedPropertyName(name) && + ts.isPropertyAccessExpression(name.expression) && + name.expression.name.text === "species" && + ts.isIdentifier(name.expression.expression) && + name.expression.expression.text === "Symbol" + ) + lowerer.unsupported( + "SC1090", + member, + "Symbol.species on a class that extends Array (its methods' results construct the class itself)", + ); + } +} diff --git a/packages/compiler/src/frontend/lowering/builtins/async-context.ts b/packages/compiler/src/frontend/lowering/builtins/async-context.ts index 18bd93a86..a130df59b 100644 --- a/packages/compiler/src/frontend/lowering/builtins/async-context.ts +++ b/packages/compiler/src/frontend/lowering/builtins/async-context.ts @@ -2,7 +2,7 @@ import { isBuiltinClassInstance } from "./receiver-types.js"; import { dynUndefinedExpr } from "../../../ir/build.js"; import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; -import { locOf } from "../../program.js"; +import { isJsSourceFile, locOf } from "../../program.js"; import { BOOL, DYN, @@ -184,16 +184,89 @@ export function lowerDiagnosticsChannelMethodCall( ); } +/** Whether `node` names the AsyncLocalStorage class itself. */ +function isAsyncLocalStorageClass(lowerer: Lowerer, node: ts.Expression): boolean { + if (!ts.isIdentifier(node)) return false; + const symbol = lowerer.resolveValueSymbol(node); + return ( + symbol !== null && + lowerer.isStdlibSymbol(symbol) && + ts.symbolName(symbol) === "AsyncLocalStorage" + ); +} + +/** The static AsyncLocalStorage members: bind(fn) answers fn bound to the + * active context (its own type for a typed function; a dyn function in + * JavaScript files, where the bound function's name and receiver are + * observable, and for dyn values); snapshot() answers a dyn function + * (cb, ...args) that runs cb in the context active at the snapshot. */ +function lowerAsyncLocalStorageStaticCall( + lowerer: Lowerer, + call: ts.CallExpression, + access: ts.PropertyAccessExpression, +): IrExpr { + const name = access.name.text; + const loc = locOf(call); + const always: IrExpr = { kind: "boolLit", value: true, type: BOOL, loc }; + if (name === "bind" && call.arguments.nodes.length === 1) { + const arg = call.arguments.nodes[0]!; + if (!isJsSourceFile(ts.getSourceFileOfNode(call))) { + const fn = lowerer.lowerExpr(arg); + if (fn.type.kind === "func") + return { kind: "libCall", fn: "als.bindCallback", args: [fn, always], type: fn.type, loc }; + } + return asyncLocalStorageResult(lowerer, call, { + kind: "libCall", + fn: "als.bindCallbackDyn", + args: [lowerDiagnosticsMessage(lowerer, arg), always], + type: DYN, + loc, + }); + } + if (name === "snapshot" && call.arguments.nodes.length === 0) + return { kind: "libCall", fn: "als.snapshot", args: [], type: DYN, loc }; + lowerer.noLowering( + `AsyncLocalStorage.${name}`, + call, + "AsyncLocalStorage.bind(fn) and AsyncLocalStorage.snapshot() are the supported static members", + lowerer.checker.getSymbolAtLocation(access.name), + ); +} + +/** new AsyncLocalStorage(options): the options object as a dyn value + * (plain property literals build it member by member). */ +export function lowerAsyncLocalStorageOptions(lowerer: Lowerer, node: ts.Expression): IrExpr { + return lowerTracingHandlers(lowerer, node); +} + +/** `.name` on AsyncLocalStorage receivers: the constructor's name option + * (als.name), converted to the property's type. Null otherwise. */ +export function lowerAsyncLocalStorageProperty( + lowerer: Lowerer, + access: ts.PropertyAccessExpression, +): IrExpr | null { + if (access.questionDotToken || access.name.text !== "name") return null; + if (!isBuiltinClassInstance(lowerer, access.expression, "AsyncLocalStorage")) return null; + if (!lowerer.isStdlibMember(access)) return null; + const loc = locOf(access); + const receiver = lowerer.lowerExprExpecting(access.expression, F64); + const value: IrExpr = { kind: "libCall", fn: "als.name", args: [receiver], type: DYN, loc }; + const expected = lowerer.mapTypeOf(lowerer.typeOf(access)); + return !expected || expected.kind === "dyn" ? value : lowerer.coerceInto(access, value, expected); +} + /** Method calls on AsyncLocalStorage receivers: run(store, fn, ...args), * exit(fn, ...args), getStore(), enterWith(store), disable() — over the - * f64 store handle (als.* libCalls; values cross as dyn values). Null - * for non-ALS receivers. */ + * f64 store handle (als.* libCalls; values cross as dyn values) — and the + * static bind/snapshot members. Null for other receivers. */ export function lowerAsyncLocalStorageMethodCall( lowerer: Lowerer, call: ts.CallExpression, access: ts.PropertyAccessExpression, ): IrExpr | null { if (call.questionDotToken || access.questionDotToken) return null; + if (isAsyncLocalStorageClass(lowerer, access.expression)) + return lowerAsyncLocalStorageStaticCall(lowerer, call, access); if (!isBuiltinClassInstance(lowerer, access.expression, "AsyncLocalStorage")) return null; if (!lowerer.isStdlibMember(access)) return null; const name = access.name.text; diff --git a/packages/compiler/src/frontend/lowering/builtins/compression.ts b/packages/compiler/src/frontend/lowering/builtins/compression.ts index 717e09933..1e4d7364c 100644 --- a/packages/compiler/src/frontend/lowering/builtins/compression.ts +++ b/packages/compiler/src/frontend/lowering/builtins/compression.ts @@ -13,6 +13,7 @@ import { funcOf, type IrExpr, type IrLibFn, + type IrStmt, type IrType, type SrcLoc, STRING, @@ -40,6 +41,36 @@ const ZLIB_CALLBACK_FNS: Readonly> = { unzip: "zlib.unzipCb", }; +/* Node's BROTLI_DECODE and BROTLI_ENCODE modes. */ +const BROTLI_DECODE = 8; +const BROTLI_ENCODE = 9; + +/** The Brotli one-shots: every form takes the options entries. */ +const BROTLI_SYNC_MEMBERS: ReadonlySet = new Set([ + "brotliCompressSync", + "brotliDecompressSync", +]); +const BROTLI_CALLBACK_MEMBERS: ReadonlySet = new Set([ + "brotliCompress", + "brotliDecompress", +]); + +/** A Brotli mode connects the codec (scr_brotli.c) to the zlib engine + * before any module initializer runs. */ +function noteBrotliMode(lowerer: Lowerer, mode: number, loc: SrcLoc): void { + if (mode !== BROTLI_DECODE && mode !== BROTLI_ENCODE) return; + const installs = (stmt: IrStmt): boolean => + stmt.kind === "exprStmt" && + stmt.expr.kind === "libCall" && + stmt.expr.fn === "zlib.brotliInstall"; + if (lowerer.mainPrelude.some(installs)) return; + lowerer.mainPrelude.push({ + kind: "exprStmt", + expr: { kind: "libCall", fn: "zlib.brotliInstall", args: [], type: VOID, loc }, + loc, + }); +} + const ZLIB_STREAM_CLASS = "%Transform"; const ZLIB_STREAM_T: IrType = { kind: "object", className: ZLIB_STREAM_CLASS }; @@ -115,6 +146,7 @@ export function lowerZlibStreamNew( } const options = args[0] !== undefined ? zlibOptionsValue(lowerer, args[0], what, loc) : dynUndefinedExpr(loc); + noteBrotliMode(lowerer, mode, loc); return { kind: "libCall", fn: "zlib.streamNew", @@ -154,8 +186,8 @@ function isZlibFallbackDecl(lowerer: Lowerer, d: ts.Node): boolean { return false; } -/** True when `access` names a member the zlib stream base class declares - * (bytesWritten, flush, params, close, reset). */ +/** True when `access` names a member the zlib stream base classes declare + * (bytesWritten, flush, close and reset on ZlibBase; params on Zlib). */ export function isZlibStreamMember(lowerer: Lowerer, access: ts.PropertyAccessExpression): boolean { const symbol = lowerer.checker.getSymbolAtLocation(access.name); if (!symbol) return false; @@ -163,7 +195,7 @@ export function isZlibStreamMember(lowerer: Lowerer, access: ts.PropertyAccessEx (d) => d.parent !== undefined && ts.isClassDeclaration(d.parent) && - d.parent.name?.text === "ZlibBase" && + (d.parent.name?.text === "ZlibBase" || d.parent.name?.text === "Zlib") && isZlibFallbackDecl(lowerer, d.parent), ); } @@ -281,12 +313,17 @@ const ZLIB_ONE_SHOT_MODES: Readonly> = { deflateRaw: 5, inflateRaw: 6, unzip: 7, + brotliDecompressSync: BROTLI_DECODE, + brotliCompressSync: BROTLI_ENCODE, + brotliDecompress: BROTLI_DECODE, + brotliCompress: BROTLI_ENCODE, }; /** True for a zlib callback one-shot util.promisify converts (deflate, - * inflate, deflateRaw, inflateRaw, gzip, gunzip and unzip). */ + * inflate, deflateRaw, inflateRaw, gzip, gunzip, unzip, brotliCompress and + * brotliDecompress). */ export function isPromisifiedZlibMember(member: string): boolean { - return ZLIB_CALLBACK_FNS[member] !== undefined; + return ZLIB_CALLBACK_FNS[member] !== undefined || BROTLI_CALLBACK_MEMBERS.has(member); } /** A call through `promisify(zlib.gzip)` and the other callback one-shots: @@ -311,6 +348,7 @@ export function lowerPromisifiedZlibCall( ); } const input = zlibInput(lowerer, expr.arguments.nodes[0]!, loc); + noteBrotliMode(lowerer, ZLIB_ONE_SHOT_MODES[member]!, loc); return { kind: "libCall", fn: input.dyn ? "zlib.promiseDyn" : "zlib.promise", @@ -334,6 +372,8 @@ const ZLIB_STREAM_FACTORIES: Readonly> = { createDeflateRaw: 5, createInflateRaw: 6, createUnzip: 7, + createBrotliDecompress: BROTLI_DECODE, + createBrotliCompress: BROTLI_ENCODE, }; export function lowerZlibModuleCall( @@ -356,6 +396,9 @@ export function lowerZlibModuleCall( loc, ); } + if (BROTLI_SYNC_MEMBERS.has(bi.member) || BROTLI_CALLBACK_MEMBERS.has(bi.member)) { + return lowerBrotliOneShot(lowerer, expr, bi.member, loc); + } const syncFn = ZLIB_SYNC_FNS[bi.member]; if (syncFn !== undefined) { if (expr.arguments.nodes.length === 0 || expr.arguments.nodes.length > 2) { @@ -502,6 +545,57 @@ export function lowerZlibModuleCall( throw new InternalCompilerError(`unhandled lowered zlib member ${bi.member}`); } +/** brotliCompressSync(data[, options]), brotliCompress(data[, options], + * callback) and the decompress pair: the options entries with Node's + * BROTLI_ENCODE or BROTLI_DECODE mode, an absent options value passing + * undefined. */ +function lowerBrotliOneShot( + lowerer: Lowerer, + expr: ts.CallExpression, + member: string, + loc: SrcLoc, +): IrExpr { + const args = expr.arguments.nodes; + const mode = ZLIB_ONE_SHOT_MODES[member]!; + const sync = BROTLI_SYNC_MEMBERS.has(member); + if (sync ? args.length === 0 || args.length > 2 : args.length !== 2 && args.length !== 3) { + lowerer.noLowering( + `zlib.${member} with ${args.length} arguments`, + expr, + sync + ? `${member}(data) and ${member}(data, options) are supported` + : `${member}(data, callback) and ${member}(data, options, callback) are supported`, + ); + } + const input = zlibInput(lowerer, args[0]!, loc); + const withOptions = sync ? args.length === 2 : args.length === 3; + const options = withOptions + ? zlibOptionsValue(lowerer, args[1]!, `zlib.${member}`, loc) + : dynUndefinedExpr(loc); + noteBrotliMode(lowerer, mode, loc); + if (sync) { + return { + kind: "libCall", + fn: input.dyn ? "zlib.syncOptsDyn" : "zlib.syncOpts", + args: [numLit(mode, loc), input.value, options], + type: BYTES_U8, + loc, + }; + } + return { + kind: "libCall", + fn: input.dyn ? "zlib.cbOptsDyn" : "zlib.cbOpts", + args: [ + numLit(mode, loc), + input.value, + options, + errorFirstBytesCallback(lowerer, args[withOptions ? 2 : 1]!, "zlib"), + ], + type: VOID, + loc, + }; +} + /** The class name of a zlib stream mode (Node's constructor name). */ export function zlibClassName(mode: number): string { for (const [name, m] of ZLIB_STREAM_MODES) if (m === mode) return name; diff --git a/packages/compiler/src/frontend/lowering/builtins/crypto.ts b/packages/compiler/src/frontend/lowering/builtins/crypto.ts index 051ec18c4..15c057462 100644 --- a/packages/compiler/src/frontend/lowering/builtins/crypto.ts +++ b/packages/compiler/src/frontend/lowering/builtins/crypto.ts @@ -1454,6 +1454,7 @@ function lowerPkiCall( ], sign: ["crypto.signOneshot", 3, 3, "sign(algorithm, data, key)"], verify: ["crypto.verifyOneshot", 4, 4, "verify(algorithm, data, key, signature)"], + diffieHellman: ["crypto.diffieHellman", 1, 2, "diffieHellman(options[, callback])"], createSign: ["crypto.createSign", 1, 1, "createSign(algorithm)"], createVerify: ["crypto.createVerify", 1, 1, "createVerify(algorithm)"], }; @@ -1474,6 +1475,7 @@ function lowerPkiCall( return { kind: "libCall", fn, args: values, type: VOID, loc }; const call: IrExpr = { kind: "libCall", fn, args: values, type: DYN, loc }; const result = lowerer.mapTypeOf(lowerer.typeOf(expr)); - if (result === null || result.kind === "dyn") return call; + // diffieHellman's callback form answers undefined. + if (result === null || result.kind === "dyn" || result.kind === "void") return call; return lowerer.coerceInto(expr, call, result); } diff --git a/packages/compiler/src/frontend/lowering/builtins/date-classes.ts b/packages/compiler/src/frontend/lowering/builtins/date-classes.ts index a2b0fc20a..9d152de56 100644 --- a/packages/compiler/src/frontend/lowering/builtins/date-classes.ts +++ b/packages/compiler/src/frontend/lowering/builtins/date-classes.ts @@ -2,7 +2,13 @@ import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; import { locOf } from "../../program.js"; import { type ClassInfo } from "../lower-classes.js"; -import { DATE_METHOD_HINT, dateJsonType, dateValueMethod } from "./dates.js"; +import { + DATE_METHOD_HINT, + dateJsonType, + dateSetterTime, + dateValueMethod, + isDateSetter, +} from "./dates.js"; import { dynUndefinedExpr, varRef } from "../../../ir/build.js"; import { BOOL, @@ -248,6 +254,22 @@ export function lowerInheritedDateMethodCall( type: result.type, loc, }); + if (isDateSetter(name) && name !== "setTime") { + // The field setters change the Date the class's time slot holds once + // the arguments are evaluated: an argument that calls setTime on the + // instance gives the slot a new Date. + const date = lowerer.declareHiddenLocal("%dateTime", DATE_T); + const current = varRef(date.id, DATE_T, loc); + const time: IrExpr = { kind: "libCall", fn: "date.getTime", args: [current], type: F64, loc }; + const next = dateSetterTime(lowerer, call, name, stmts, time); + stmts.push({ + kind: "varDecl", + localId: date.id, + init: timeOf(lowerer, value, root, loc), + loc, + }); + return done({ kind: "libCall", fn: "date.store", args: [current, next], type: F64, loc }); + } if (name === "setTime") { // Every argument is evaluated before setTime converts the first one. let ms: IrExpr = { kind: "numLit", value: Number.NaN, type: F64, loc }; diff --git a/packages/compiler/src/frontend/lowering/builtins/dates.ts b/packages/compiler/src/frontend/lowering/builtins/dates.ts index a598f4ee6..075047606 100644 --- a/packages/compiler/src/frontend/lowering/builtins/dates.ts +++ b/packages/compiler/src/frontend/lowering/builtins/dates.ts @@ -2,12 +2,15 @@ import * as ts from "../../ts7/adapter.js"; import { type Lowerer, own } from "../lowerer.js"; import { locOf } from "../../program.js"; import { tryLowerExpression } from "../expressions/try-lower-expression.js"; +import { lowerToNumberArguments } from "../lower-exprs.js"; import { programDeclaresDateClass } from "./date-classes.js"; import { BOOL, + DATE_T, F64, type IrExpr, type IrLibFn, + type IrLocal, type IrStmt, type IrType, NULL_T, @@ -36,6 +39,32 @@ const DATE_GETTER_FNS: Readonly> = { getTimezoneOffset: "date.getTimezoneOffset", }; +/** The setters that change calendar fields: the field code + * scr_date_set_fields takes (scr_number.h SCR_DATE_SET_*; 8 marks the UTC + * forms) and the most arguments each takes. */ +const DATE_FIELD_SETTERS: ReadonlyMap = new Map([ + ["setMilliseconds", { code: 0, max: 1 }], + ["setSeconds", { code: 1, max: 2 }], + ["setMinutes", { code: 2, max: 3 }], + ["setHours", { code: 3, max: 4 }], + ["setDate", { code: 4, max: 1 }], + ["setMonth", { code: 5, max: 2 }], + ["setFullYear", { code: 6, max: 3 }], + ["setUTCMilliseconds", { code: 8, max: 1 }], + ["setUTCSeconds", { code: 9, max: 2 }], + ["setUTCMinutes", { code: 10, max: 3 }], + ["setUTCHours", { code: 11, max: 4 }], + ["setUTCDate", { code: 12, max: 1 }], + ["setUTCMonth", { code: 13, max: 2 }], + ["setUTCFullYear", { code: 14, max: 3 }], +]); + +/** Whether `name` is a Date setter scriptc lowers: setTime or a field + * setter. */ +export function isDateSetter(name: string): boolean { + return name === "setTime" || DATE_FIELD_SETTERS.has(name); +} + const DATE_TEXT_FNS: ReadonlyMap = new Map([ ["toString", "date.toString"], ["toDateString", "date.toDateString"], @@ -43,17 +72,23 @@ const DATE_TEXT_FNS: ReadonlyMap = new Map([ ]); export const DATE_METHOD_HINT = - "getTime(), valueOf(), toISOString(), toUTCString(), toJSON(), toString(), toDateString(), toTimeString(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale formatters have no lowering"; + "getTime(), valueOf(), toISOString(), toUTCString(), toJSON(), toString(), toDateString(), toTimeString(), the local/UTC calendar getters and setters, setTime(), and getTimezoneOffset() are supported; locale formatters have no lowering"; -/** The Date slice: statics plus read-only Date values backed by one - * TimeClip'd millisecond scalar. Construction lives in lowerNew; - * identity and mutating methods remain fenced. */ +/** Date statics and methods on Date objects (a TimeClip'd millisecond + * time value with identity): getters, text forms and setters. + * Construction lives in lowerNew. */ export function lowerDateCall( lowerer: Lowerer, call: ts.CallExpression, access: ts.PropertyAccessExpression, ): IrExpr | null { - if (call.questionDotToken || access.questionDotToken) return null; + // An optional chain lowers the call again once it has narrowed the + // receiver (Lowerer.chainHandled). + if ( + (call.questionDotToken && !lowerer.chainHandled.has(call)) || + (access.questionDotToken && !lowerer.chainHandled.has(access)) + ) + return null; const loc = locOf(call); const member = lowerer.stdlibGlobalMember(access, "Date"); if (member === "now" || member === "parse" || member === "UTC") @@ -61,16 +96,18 @@ export function lowerDateCall( if (lowerer.mapTypeOf(lowerer.typeOf(access.expression))?.kind !== "date") return null; if (!lowerer.isStdlibMember(access)) return null; const name = access.name.text; - // A Date a JavaScript file constructs is a mutable object; setTime on - // it takes the dynamic method call. So does every Date method on a + // A Date a JavaScript file constructs is a checked-dynamic value; a + // setter on it takes the dynamic method call, which changes the Date the + // handle holds. So does every Date method on a // checked-dynamic receiver when the program declares a class that extends // Date: the value can be an instance, whose class may declare the method. if ( - (name === "setTime" || programDeclaresDateClass(lowerer)) && + (isDateSetter(name) || programDeclaresDateClass(lowerer)) && tryLowerExpression(lowerer, access.expression)?.type.kind === "dyn" ) return null; if (name === "toJSON") return lowerDateToJson(lowerer, call, access); + if (isDateSetter(name)) return lowerDateSetter(lowerer, call, access, name); if (call.arguments.nodes.length !== 0) { lowerer.noLowering(`Date.prototype.${name} with arguments`, call, DATE_METHOD_HINT); } @@ -247,3 +284,151 @@ export function isDateJsonSource(lowerer: Lowerer, node: ts.Node): boolean { lowerer.isStdlibMember(access) ); } + +/** `d.setTime(t)` and the field setters (`d.setHours(h, m?, s?, ms?)` ...) + * on a Date object: the receiver, then the arguments in order, then the + * new time value (scr_date_set_fields over the time value the Date holds + * once the arguments have run) stored into the Date and answered. Missing + * optional arguments keep their fields; arguments past a setter's last + * one are evaluated and ignored. A Date local that holds its time value + * (date-values.ts) takes the new time value itself. */ +function lowerDateSetter( + lowerer: Lowerer, + call: ts.CallExpression, + access: ts.PropertyAccessExpression, + name: string, +): IrExpr { + const loc = locOf(call); + const nodes = call.arguments.nodes; + if (nodes.some((node) => ts.isSpreadElement(node))) + lowerer.noLowering(`Date.prototype.${name} with spread arguments`, call, DATE_METHOD_HINT); + const timeLocal = timeValueLocalOf(lowerer, access.expression); + const stmts: IrStmt[] = []; + let receiver: IrExpr | null = null; + if (timeLocal === null) { + // A checked-dynamic receiver takes the dynamic method call + // (lowerDateCall), so the value here is a Date object. + const value = lowerer.lowerExpr(access.expression); + if (value.type.kind !== "date") + lowerer.badType(access.expression, lowerer.typeOf(access.expression)); + const date = lowerer.declareHiddenLocal("%date", DATE_T); + stmts.push({ kind: "varDecl", localId: date.id, init: value, loc }); + receiver = { kind: "varRef", localId: date.id, type: DATE_T, loc }; + } + const time: IrExpr = + receiver === null + ? { kind: "varRef", localId: timeLocal!.id, type: F64, loc } + : { kind: "libCall", fn: "date.getTime", args: [receiver], type: F64, loc }; + const next = dateSetterTime(lowerer, call, name, stmts, time); + if (receiver === null) { + stmts.push({ kind: "assign", localId: timeLocal!.id, value: next, loc }); + return { + kind: "seqExpr", + stmts, + result: { kind: "varRef", localId: timeLocal!.id, type: F64, loc }, + type: F64, + loc, + }; + } + return { + kind: "seqExpr", + stmts, + result: { kind: "libCall", fn: "date.store", args: [receiver, next], type: F64, loc }, + type: F64, + loc, + }; +} + +/** The new time value of setter `name` called with `call`'s arguments on a + * Date whose time value `time` reads: the arguments evaluate into + * temporaries appended to `stmts` (ToNumber of each, an undefined one is + * NaN), and the result reads `time` after them. A missing first argument + * is NaN; missing optional arguments keep their fields; arguments past a + * setter's last one are evaluated and ignored. */ +export function dateSetterTime( + lowerer: Lowerer, + call: ts.CallExpression, + name: string, + stmts: IrStmt[], + time: IrExpr, +): IrExpr { + const loc = locOf(call); + const converted = lowerToNumberArguments( + lowerer, + call.arguments.nodes as readonly ts.Expression[], + ); + stmts.push(...converted.stmts); + const args: IrExpr[] = converted.values.map((value) => { + if (value.kind === "numLit") return value; + const arg = lowerer.declareHiddenLocal("%dateArg", F64); + stmts.push({ kind: "varDecl", localId: arg.id, init: value, loc }); + return { kind: "varRef", localId: arg.id, type: F64, loc }; + }); + const number = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc }); + if (name === "setTime") + return { + kind: "libCall", + fn: "date.timeClip", + args: [args[0] ?? number(Number.NaN)], + type: F64, + loc, + }; + const setter = DATE_FIELD_SETTERS.get(name)!; + // The first argument is required: a missing one is undefined, so NaN. + const used = args.length === 0 ? [number(Number.NaN)] : args.slice(0, setter.max); + const operands = [...used]; + while (operands.length < 4) operands.push(number(0)); + return { + kind: "libCall", + fn: "date.setFields", + args: [time, number(setter.code), number(used.length), ...operands], + type: F64, + loc, + }; +} + +/** The local a Date binding that holds its time value lowers to, for an + * identifier `node` that names one (date-values.ts); null otherwise. */ +function timeValueLocalOf(lowerer: Lowerer, node: ts.Expression): IrLocal | null { + while (ts.isParenthesizedExpression(node)) node = node.expression; + if (!ts.isIdentifier(node)) return null; + const local = lowerer.resolveLocal(node); + return local !== undefined && local !== null && lowerer.timeValueDateLocals.has(local) + ? local + : null; +} + +/** `d[Symbol.toPrimitive](hint)` on a Date with a literal hint: "number" + * answers the time value, "string" and "default" the toString() text (a + * Date's default hint is a string). Null for other calls. */ +export function lowerDateToPrimitiveCall( + lowerer: Lowerer, + call: ts.CallExpression, + access: ts.ElementAccessExpression, +): IrExpr | null { + const key = access.argumentExpression; + if ( + !ts.isPropertyAccessExpression(key) || + key.name.text !== "toPrimitive" || + !ts.isIdentifier(key.expression) || + !lowerer.isStdlibGlobal(key.expression, "Symbol") || + lowerer.mapTypeOf(lowerer.typeOf(access.expression))?.kind !== "date" + ) + return null; + const loc = locOf(call); + const hint = call.arguments.nodes.length === 1 ? call.arguments.nodes[0]! : null; + const text = hint !== null && ts.isStringLiteralLike(hint) ? hint.text : null; + if (text !== "number" && text !== "string" && text !== "default") + return lowerer.noLowering( + 'Date.prototype[Symbol.toPrimitive] without a "number", "string" or "default" literal hint', + call, + ); + const raw = lowerer.lowerExpr(access.expression); + const date: IrExpr = + raw.type.kind === "dyn" + ? { kind: "libCall", fn: "date.checkedValue", args: [raw], type: DATE_T, loc } + : raw; + return text === "number" + ? { kind: "libCall", fn: "date.valueOf", args: [date], type: F64, loc } + : { kind: "libCall", fn: "date.toString", args: [date], type: STRING, loc }; +} diff --git a/packages/compiler/src/frontend/lowering/builtins/disposable-stack.ts b/packages/compiler/src/frontend/lowering/builtins/disposable-stack.ts index 375c3f1e7..866358b06 100644 --- a/packages/compiler/src/frontend/lowering/builtins/disposable-stack.ts +++ b/packages/compiler/src/frontend/lowering/builtins/disposable-stack.ts @@ -73,8 +73,7 @@ export function lowerDisposableStackUseCall( if (valueT?.kind !== "object") return null; const loc = locOf(call); const className = valueT.className; - const info = lowerer.classes.get(className); - if (!info) lowerer.flushDeferredClass(className); + const info = lowerer.classes.get(className) ?? lowerer.flushDeferredClass(className); const found = info ? lowerer.findMethodOn(info, "sym:dispose") : null; if (!found) return null; if (found.sig.params.length !== 0) { diff --git a/packages/compiler/src/frontend/lowering/builtins/filesystem-calls.ts b/packages/compiler/src/frontend/lowering/builtins/filesystem-calls.ts index d147a6208..6b5e2d8ae 100644 --- a/packages/compiler/src/frontend/lowering/builtins/filesystem-calls.ts +++ b/packages/compiler/src/frontend/lowering/builtins/filesystem-calls.ts @@ -2,7 +2,7 @@ import { dynUndefinedExpr, boolLit, numLit, varRef } from "../../../ir/build.js" import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; import { lowerFsPath } from "./fs-paths.js"; -import { locOf } from "../../program.js"; +import { isJsSourceFile, locOf } from "../../program.js"; import { type BuiltinModuleFn, FS_WRITE_FILE_DOCUMENTED_OPTIONS, @@ -265,7 +265,7 @@ export function lowerFsPromisesOpenCall( "use open(path[, stringFlags[, numericMode]])", ); } - const path = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); + const path = lowerFsPath(lowerer, expr.arguments.nodes[0]!); const defaultFlags: IrExpr = { kind: "strLit", value: "r", type: STRING, loc }; const defaultMode: IrExpr = { kind: "numLit", value: 0o666, type: F64, loc }; const flags = expr.arguments.nodes[1] @@ -403,6 +403,28 @@ export function lowerFsReadSyncCall( } lowerer.noLowering("readSync with a non-literal options argument", optionsNode, supported); } + // JavaScript arguments outside the typed signature (an untyped fd or + // buffer, a bigint or untyped position) take Node's argument checks at + // run time, then the same read. + if (isJsSourceFile(ts.getSourceFileOfNode(expr))) { + const nodes = expr.arguments.nodes; + const typed = [F64, BYTES_U8, F64, F64, F64].every((want, i) => { + const node = nodes[i]; + if (node === undefined) return true; + let value: ts.Expression = node; + while (ts.isParenthesizedExpression(value)) value = value.expression; + if (i === 4 && value.kind === ts.SyntaxKind.NullKeyword) return true; + const t = lowerer.mapTypeOf(lowerer.typeOf(node)); + return t !== null && (want.kind === "bytes" ? t.kind === "bytes" : t.kind === "f64"); + }); + if (!typed) { + const args = [0, 1, 2, 3, 4].map((i): IrExpr => { + const node = nodes[i]; + return node === undefined ? dynUndefinedExpr(loc) : lowerer.lowerExprExpecting(node, DYN); + }); + return { kind: "libCall", fn: "fs.readSyncDyn", args, type: F64, loc }; + } + } const args: IrExpr[] = [ lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, F64), lowerer.lowerExprExpecting(expr.arguments.nodes[1]!, BYTES_U8), @@ -705,7 +727,7 @@ export function lowerFsPromisesEncodedRead( ) return null; } - const path = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); + const path = lowerFsPath(lowerer, expr.arguments.nodes[0]!); const encoding: IrExpr = optionsType.kind === "string" ? lowerer.lowerExprExpecting(options, STRING) @@ -853,7 +875,7 @@ export function lowerFsPromisesMkdirCall( ); } const { recursive, modeNode } = options; - const path = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); + const path = lowerFsPath(lowerer, expr.arguments.nodes[0]!); const mode: IrExpr | null = modeNode ? lowerer.lowerExprExpecting(modeNode, F64) : null; const type: IrType = { kind: "promise", inner: VOID }; if (!recursive) { diff --git a/packages/compiler/src/frontend/lowering/builtins/filesystem.ts b/packages/compiler/src/frontend/lowering/builtins/filesystem.ts index 6b988c491..d8272c795 100644 --- a/packages/compiler/src/frontend/lowering/builtins/filesystem.ts +++ b/packages/compiler/src/frontend/lowering/builtins/filesystem.ts @@ -1306,6 +1306,7 @@ export function lowerFsReaddirTypesCall( ); } let sawWithFileTypes = false; + let recursive: ts.Expression | null = null; for (const p of optsNode.properties.nodes) { const m = optionMember(p); if (!m) { @@ -1334,9 +1335,11 @@ export function lowerFsReaddirTypesCall( "names decode as utf8 (the default); encoding: 'buffer' has no lowering", ); } + } else if (m.name === "recursive" && !promiseForm) { + recursive = m.value; } else { // The options-record stance: recursive (a documented knob with - // no lowering) fences by name; undocumented keys drop like Node. + // no lowering here) fences by name; undocumented keys drop like Node. fenceOrDropOptionKey( lowerer, p, @@ -1347,6 +1350,26 @@ export function lowerFsReaddirTypesCall( ); } } + if (recursive !== null) { + // Only the `true`/`false` keywords fold; any other expression, even one + // typed as a literal, evaluates at run time for its effects. + let flag = recursive; + while (ts.isParenthesizedExpression(flag)) flag = flag.expression; + const literal = + flag.kind === ts.SyntaxKind.TrueKeyword + ? "true" + : flag.kind === ts.SyntaxKind.FalseKeyword + ? "false" + : null; + if (sawWithFileTypes && literal !== "false") { + lowerer.noLowering( + `${operation} with withFileTypes and recursive`, + recursive, + "recursive: true lists relative names; withFileTypes: true lists one directory's Dirents", + ); + } + if (!sawWithFileTypes) return lowerFsReaddirRecursive(lowerer, call, recursive, literal, loc); + } if (!sawWithFileTypes) { lowerer.noLowering( `${operation} with 2 arguments`, @@ -1388,6 +1411,75 @@ export function lowerFsReaddirTypesCall( }; } +/** `fs.readdirSync(path, { recursive })` without withFileTypes: the + * relative names of every entry below the path when `recursive` is true + * (scr_fs_readdir_recursive, Node's queue order), the directory's own names + * when it is false. A recursive flag that is not a boolean literal selects + * between the two at run time, after the path is evaluated. */ +function lowerFsReaddirRecursive( + lowerer: Lowerer, + call: ts.CallExpression, + recursive: ts.Expression, + literal: string | null, + loc: SrcLoc, +): IrExpr { + const names = arrayOf(STRING); + // @types/node types the call `string[] | Buffer[]` when the options name + // no encoding; the names are strings, so the call is the string[] arm. + const callType = lowerer.mapTypeOf(lowerer.typeOf(call)); + if ( + !callType || + !( + typeEquals(callType, names) || + (callType.kind === "union" && lowerer.armTag(callType.unionId, names) >= 0) + ) + ) { + lowerer.noLowering( + "readdirSync(path, { recursive }) where the result is not a string array", + call, + "names decode as utf8 strings (the default encoding)", + ); + } + return lowerFsReaddirNames(lowerer, call, recursive, literal, names, loc); +} + +function lowerFsReaddirNames( + lowerer: Lowerer, + call: ts.CallExpression, + recursive: ts.Expression, + literal: string | null, + names: IrType, + loc: SrcLoc, +): IrExpr { + const path = lowerFsPath(lowerer, call.arguments.nodes[0]!); + const listing = (fn: IrLibFn, arg: IrExpr): IrExpr => ({ + kind: "libCall", + fn, + args: [arg], + type: names, + loc, + }); + if (literal === "true") return listing("fs.readdirRecursiveSync", path); + if (literal === "false") return listing("fs.readdirSync", path); + const pathLocal = lowerer.declareHiddenLocal("%readdirPath", STRING); + const pathRef = varRef(pathLocal.id, STRING, loc); + const flag = lowerer.lowerExprExpecting(recursive, BOOL); + return { + kind: "seqExpr", + stmts: [{ kind: "varDecl", localId: pathLocal.id, init: path, loc }], + result: { + kind: "ternary", + cond: flag, + then: listing("fs.readdirRecursiveSync", pathRef), + else_: listing("fs.readdirSync", pathRef), + type: names, + loc, + }, + type: names, + loc, + }; +} + /** fs.statSync / fs.lstatSync's documented option keys. */ const FS_STAT_DOCUMENTED_OPTIONS: ReadonlySet = new Set(["bigint", "throwIfNoEntry"]); diff --git a/packages/compiler/src/frontend/lowering/builtins/fs-glob.ts b/packages/compiler/src/frontend/lowering/builtins/fs-glob.ts index f988c204d..4d88cd96e 100644 --- a/packages/compiler/src/frontend/lowering/builtins/fs-glob.ts +++ b/packages/compiler/src/frontend/lowering/builtins/fs-glob.ts @@ -22,7 +22,10 @@ import { builtinImportOf } from "./module-bindings.js"; * lib/internal/fs/glob.js). The pattern and options cross as dyn values so * the runtime validates them in Node's order. The result form (strings or * Dirent rows) comes from the call's mapped result type, so withFileTypes - * must be statically known; the runtime checks that the option agrees. */ + * must be statically known; the runtime checks that the option agrees. + * Because of that check, the form follows the checker's resolved overload + * even where an untyped options argument leaves the overload undecided + * (dynamicOverloadChoice). */ /** The interned Dirent record array (type-mapper.ts), or null. */ function direntArray(lowerer: Lowerer, type: IrType | null | undefined): boolean { @@ -67,7 +70,7 @@ export function lowerFsGlobSyncCall( loc: SrcLoc, ): IrExpr { const args = globArgs(lowerer, call, "fs.globSync"); - const callType = lowerer.mapTypeOf(lowerer.typeOf(call)); + const callType = lowerer.mapTypeOf(lowerer.checker.getTypeAtLocation(call)); const types = direntArray(lowerer, callType); if (types) fenceDirentExclude(lowerer, call, "fs.globSync"); if (!types && !(callType?.kind === "array" && callType.elem.kind === "string")) { @@ -132,7 +135,9 @@ export function lowerFspGlobCall(lowerer: Lowerer, call: ts.CallExpression, loc: } const args = globArgs(lowerer, call, "fs/promises glob"); // The async iterator's yielded type (its first type argument). - const yielded = lowerer.checker.getTypeArguments(lowerer.typeOf(call) as ts.TypeReference)[0]; + const yielded = lowerer.checker.getTypeArguments( + lowerer.checker.getTypeAtLocation(call) as ts.TypeReference, + )[0]; const element = yielded ? lowerer.mapTypeOf(yielded) : null; const rows: IrType | null = element ? { kind: "array", elem: element } : null; const types = direntArray(lowerer, rows); diff --git a/packages/compiler/src/frontend/lowering/builtins/fs-paths.ts b/packages/compiler/src/frontend/lowering/builtins/fs-paths.ts index 38583025d..46a575388 100644 --- a/packages/compiler/src/frontend/lowering/builtins/fs-paths.ts +++ b/packages/compiler/src/frontend/lowering/builtins/fs-paths.ts @@ -5,30 +5,40 @@ * conversion (url.fileURLToPathUrl). The later arguments through the last * one with side effects are evaluated into hidden locals before the * conversion, in source order, so they run even when it throws and each - * reads what it reads in Node. The conversion keeps Node's error order when every - * earlier path is a URL too and each argument Node validates before the - * path is absent or a literal that passes that validation; other URL - * forms fence with SC2020, as do the fs.promises functions, whose - * conversion errors are rejections rather than throws, and existsSync, - * which answers false for a URL it cannot convert. */ + * reads what it reads in Node. Before the conversion, an earlier string + * path gets its null-byte check (copyFileSync, renameSync) and a runtime + * readFileSync encoding its name check, which Node runs first. existsSync + * and the fs.promises functions convert to a value instead + * (url.fileURLToPathValue) and answer false or a rejected promise for a + * conversion error. An argument Node validates before the path must + * otherwise be absent or a literal that passes that validation; other + * forms fence with SC2020. */ import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; import { locOf } from "../../program.js"; import { sideEffectFreeOptionValue } from "../surfaces.js"; import { stripTypeWrappers } from "../typed-arguments.js"; -import { type IrExpr, type IrStmt, STRING, URL_T } from "../../../ir/ir.js"; +import { boolLit, varRef } from "../../../ir/build.js"; +import { BOOL, DYN, type IrExpr, type IrStmt, STRING, URL_T, VOID } from "../../../ir/ir.js"; +import { InternalCompilerError } from "../../../errors.js"; -/** The fs functions whose path parameters accept a URL: the path - * parameter indexes, and whether Node validates the paths before every - * other argument (otherwise the options come first). */ -const FS_URL_PATHS: Readonly< - Record -> = { +interface FsPathRow { + /** The path parameter indexes. */ + readonly paths: readonly number[]; + /** Whether Node validates the paths before every other argument + * (otherwise the options come first). */ + readonly pathsFirst: boolean; + /** The path parameters' names in Node's errors, when there are two. */ + readonly names?: readonly string[]; +} + +/** The fs functions whose path parameters accept a URL. */ +const FS_URL_PATHS: Readonly> = { accessSync: { paths: [0], pathsFirst: true }, appendFileSync: { paths: [0], pathsFirst: false }, chmodSync: { paths: [0], pathsFirst: true }, chownSync: { paths: [0], pathsFirst: true }, - copyFileSync: { paths: [0, 1], pathsFirst: true }, + copyFileSync: { paths: [0, 1], pathsFirst: true, names: ["src", "dest"] }, lchownSync: { paths: [0], pathsFirst: true }, lstatSync: { paths: [0], pathsFirst: true }, mkdirSync: { paths: [0], pathsFirst: false }, @@ -39,7 +49,7 @@ const FS_URL_PATHS: Readonly< readdirSync: { paths: [0], pathsFirst: false }, realpathSync: { paths: [0], pathsFirst: false }, "realpathSync.native": { paths: [0], pathsFirst: false }, - renameSync: { paths: [0, 1], pathsFirst: true }, + renameSync: { paths: [0, 1], pathsFirst: true, names: ["oldPath", "newPath"] }, rmSync: { paths: [0], pathsFirst: false }, rmdirSync: { paths: [0], pathsFirst: true }, statSync: { paths: [0], pathsFirst: true }, @@ -48,43 +58,66 @@ const FS_URL_PATHS: Readonly< writeFileSync: { paths: [0], pathsFirst: false }, }; -/** The fs.promises functions with path parameters, by path indexes. */ -const FSP_PATHS: Readonly> = { - access: [0], - appendFile: [0], - chmod: [0], - chown: [0], - copyFile: [0, 1], - lchown: [0], - lstat: [0], - mkdir: [0], - mkdtemp: [0], - open: [0], - opendir: [0], - readFile: [0], - readdir: [0], - realpath: [0], - rename: [0, 1], - rm: [0], - rmdir: [0], - stat: [0], - truncate: [0], - unlink: [0], - writeFile: [0], +/** The fs.promises functions with path parameters. */ +const FSP_PATHS: Readonly> = { + access: { paths: [0], pathsFirst: true }, + appendFile: { paths: [0], pathsFirst: false }, + chmod: { paths: [0], pathsFirst: true }, + chown: { paths: [0], pathsFirst: true }, + copyFile: { paths: [0, 1], pathsFirst: true }, + lchown: { paths: [0], pathsFirst: true }, + lstat: { paths: [0], pathsFirst: true }, + mkdir: { paths: [0], pathsFirst: false }, + mkdtemp: { paths: [0], pathsFirst: false }, + open: { paths: [0], pathsFirst: true }, + opendir: { paths: [0], pathsFirst: true }, + readFile: { paths: [0], pathsFirst: false }, + readdir: { paths: [0], pathsFirst: false }, + realpath: { paths: [0], pathsFirst: false }, + rename: { paths: [0, 1], pathsFirst: true }, + rm: { paths: [0], pathsFirst: true }, + rmdir: { paths: [0], pathsFirst: true }, + stat: { paths: [0], pathsFirst: true }, + truncate: { paths: [0], pathsFirst: true }, + unlink: { paths: [0], pathsFirst: true }, + writeFile: { paths: [0], pathsFirst: false }, }; +/** What a path argument of an fs call lowers to (Lowerer.fsPathPlans). */ +export type FsPathPlan = + /** The first URL path: the later arguments with effects, the earlier + * string paths to check, and a runtime encoding to check, all before the + * conversion. */ + | { + kind: "convert"; + staged: ts.Expression[]; + checks: { path: ts.Expression; name: string }[]; + encoding: ts.Expression | null; + } + /** A string path that a later URL path checks: the local that holds it, + * once lowered. */ + | { kind: "save"; local: string | null } + /** A URL path whose conversion error becomes false or a rejection: the + * local that holds the conversion's value. */ + | { kind: "value"; local: string }; + function isUrlArgument(lowerer: Lowerer, node: ts.Expression | undefined): boolean { return node !== undefined && lowerer.mapTypeOf(lowerer.typeOf(node))?.kind === "url"; } +function stripParens(node: ts.Expression): ts.Expression { + let e = node; + while (ts.isParenthesizedExpression(e)) e = e.expression; + return e; +} + /** A literal that Node's option validation accepts before it reads the * path: a string (the checker admits only the declared encodings, flags * and type names), a boolean, an integer in [0, 2^31 - 1] (every numeric * option of these functions accepts that range), undefined or null, or an * object literal of those. */ function preValidatedLiteral(node: ts.Expression): boolean { - let e = node; - while (ts.isParenthesizedExpression(e)) e = e.expression; + const e = stripParens(node); if (ts.isStringLiteralLike(e)) return true; if ( e.kind === ts.SyntaxKind.TrueKeyword || @@ -108,39 +141,59 @@ function preValidatedLiteral(node: ts.Expression): boolean { return false; } -/** SC2020 when the call passes a URL path in a form whose conversion - * order would differ from Node's (see the file comment). */ -function checkUrlPaths( +const HINT = "convert the URL first: fileURLToPath(url) from node:url"; + +/** SC2020 for an argument Node validates before the paths that is neither + * absent nor a literal passing that validation, other than a string + * readFileSync encoding (returned: it is checked before the conversion). + * writeFileSync/appendFileSync's data is checked after the path. */ +function optionsBeforePaths( lowerer: Lowerer, - call: ts.CallExpression, - module: string, + args: readonly ts.Expression[], member: string, -): void { - const args = call.arguments.nodes; - const name = module === "fs/promises" ? `fs.promises.${member}` : member; - const hint = "convert the URL first: fileURLToPath(url) from node:url"; - if (module === "fs/promises") { - if ((FSP_PATHS[member] ?? []).some((i) => isUrlArgument(lowerer, args[i]))) - lowerer.noLowering(`${name} with a URL path`, call, hint); - return; + row: FsPathRow, + name: string, +): ts.Expression | null { + if (row.pathsFirst) return null; + const data = /^(writeFile|appendFile)(Sync)?$/.test(member) ? 1 : -1; + let encoding: ts.Expression | null = null; + for (let i = 0; i < args.length; i++) { + const arg = args[i]!; + if (row.paths.includes(i) || i === data || preValidatedLiteral(arg)) continue; + if ( + member === "readFileSync" && + i === 1 && + lowerer.mapTypeOf(lowerer.typeOf(arg))?.kind === "string" + ) { + encoding = stripTypeWrappers(arg); + continue; + } + lowerer.noLowering(`${name} with a URL path and options that are not literals`, arg, HINT); } - if (member === "existsSync" && isUrlArgument(lowerer, args[0])) - lowerer.noLowering(`${name} with a URL path`, call, hint); - const row = FS_URL_PATHS[member]; - if (!row) return; - const first = row.paths.find((i) => isUrlArgument(lowerer, args[i])); - if (first === undefined) return; - if (args.some(ts.isSpreadElement)) - lowerer.noLowering(`${name} with spread arguments and a URL path`, call); - // The runtime checks a string path after the conversion, so a string - // path Node checks first cannot precede the URL. - if (row.paths.some((i) => i < first && args[i] !== undefined)) - lowerer.noLowering(`${name} with a string path before a URL path`, call, hint); - // Every argument from the URL through the last one with effects is - // evaluated ahead of the conversion, in source order: a later argument's - // effect (an assignment, say) must not change what an earlier one read. + return encoding; +} + +/** The arguments after the URL path that are evaluated ahead of the + * conversion: every one through the last with effects or that `force` + * names, in source order. An object or array literal among them is + * SC2020: its members are lowered one by one, so it cannot be evaluated + * ahead of the conversion as one value. */ +function stagedAfter( + lowerer: Lowerer, + args: readonly ts.Expression[], + first: number, + name: string, + force: (e: ts.Expression, i: number) => boolean, +): ts.Expression[] { + // Every argument from the URL through the last one with effects (or that + // the caller forces) is evaluated ahead of the conversion, in source + // order: a later argument's effect (an assignment, say) must not change + // what an earlier one read. let last = first; - for (let i = first + 1; i < args.length; i++) if (!sideEffectFreeOptionValue(args[i]!)) last = i; + for (let i = first + 1; i < args.length; i++) { + const e = stripTypeWrappers(args[i]!); + if (!sideEffectFreeOptionValue(e) || force(e, i)) last = i; + } const staged: ts.Expression[] = []; for (let i = first + 1; i <= last; i++) { // Keyed without type-only wrappers (`satisfies`, `as`, `!`), so a @@ -154,56 +207,190 @@ function checkUrlPaths( ? `${name} with a URL path and a literal before a later argument that has effects` : `${name} with a URL path and a later literal that has effects`, e, - hint, + HINT, ); } staged.push(e); } - if (staged.length > 0) { - lowerer.stagedAfterPath.set(args[first]!, staged); - for (const e of staged) lowerer.stagedArguments.set(e, null); - } - if (row.pathsFirst) return; - // writeFileSync/appendFileSync's data is checked after the path. - const data = member === "writeFileSync" || member === "appendFileSync" ? 1 : -1; - for (let i = 0; i < args.length; i++) { - if (row.paths.includes(i) || i === data || preValidatedLiteral(args[i]!)) continue; - lowerer.noLowering(`${name} with a URL path and options that are not literals`, args[i]!, hint); - } + return staged; } -/** Fences URL paths that cannot convert in Node's order: the check every - * fs and fs.promises call runs before lowering its arguments. */ +/** Plans the URL paths of a synchronous fs call (see the file comment): + * the check every fs call runs before lowering its arguments. */ export function checkFsUrlPaths( lowerer: Lowerer, call: ts.CallExpression, bi: { module: string; member: string }, ): void { - if (bi.module === "fs" || bi.module === "fs/promises") - checkUrlPaths(lowerer, call, bi.module, bi.member); + if (bi.module !== "fs") return; + const row = FS_URL_PATHS[bi.member]; + if (!row) return; + const args = call.arguments.nodes; + const first = row.paths.find((i) => isUrlArgument(lowerer, args[i])); + if (first === undefined || lowerer.fsPathPlans.has(stripParens(args[first]!))) return; + const name = bi.member; + if (args.some(ts.isSpreadElement)) + lowerer.noLowering(`${name} with spread arguments and a URL path`, call); + const checks: { path: ts.Expression; name: string }[] = []; + for (const i of row.paths) { + if (i >= first || args[i] === undefined) continue; + const path = stripParens(args[i]!); + checks.push({ path, name: row.names?.[i] ?? "path" }); + lowerer.fsPathPlans.set(path, { kind: "save", local: null }); + } + const encoding = optionsBeforePaths(lowerer, args, bi.member, row, name); + const staged = stagedAfter(lowerer, args, first, name, (e) => e === encoding); + for (const e of staged) lowerer.stagedArguments.set(e, null); + lowerer.fsPathPlans.set(stripParens(args[first]!), { kind: "convert", staged, checks, encoding }); +} + +/** existsSync and the fs.promises functions with URL paths: each URL + * converts to a value (url.fileURLToPathValue) after the arguments are + * evaluated, and the call runs on the converted paths only when every + * conversion produced a string; otherwise existsSync answers false and an + * fs.promises function a promise rejected with the first conversion's + * error. `lowerCall` lowers the call itself, whose URL paths then read the + * converted values. Null for every other call. */ +export function lowerFallibleFsUrlCall( + lowerer: Lowerer, + call: ts.CallExpression, + bi: { module: string; member: string }, + lowerCall: () => IrExpr, +): IrExpr | null { + const row = + bi.module === "fs/promises" + ? FSP_PATHS[bi.member] + : bi.module === "fs" && bi.member === "existsSync" + ? ({ paths: [0], pathsFirst: true } satisfies FsPathRow) + : undefined; + if (!row) return null; + const args = call.arguments.nodes; + const urls = row.paths.filter((i) => isUrlArgument(lowerer, args[i])); + if (urls.length === 0 || lowerer.fallibleFsCalls.has(call)) return null; + const name = bi.module === "fs/promises" ? `fs.promises.${bi.member}` : bi.member; + if (args.some(ts.isSpreadElement)) + lowerer.noLowering(`${name} with spread arguments and a URL path`, call); + const first = urls[0]!; + // A string path before the URL would be checked in the call itself, + // after the conversion. + if (row.paths.some((i) => i < first && args[i] !== undefined)) + lowerer.noLowering(`${name} with a string path before a URL path`, call, HINT); + if (optionsBeforePaths(lowerer, args, bi.member, row, name) !== null) + lowerer.noLowering(`${name} with a URL path and options that are not literals`, args[1]!, HINT); + const loc = locOf(call); + const stmts: IrStmt[] = []; + const values = new Map(); + const save = (value: IrExpr, label: string, at: ts.Node): string => { + const slot = lowerer.declareHiddenLocal(label, value.type); + stmts.push({ kind: "varDecl", localId: slot.id, init: value, loc: locOf(at) }); + return slot.id; + }; + values.set(first, save(lowerer.lowerExprExpecting(args[first]!, URL_T), "%fsUrl", args[first]!)); + const staged = stagedAfter(lowerer, args, first, name, (_e, i) => urls.includes(i)); + for (const e of staged) { + // A call lowered again (an array-literal element) evaluates the + // argument anew; the slot an earlier lowering saved it in is gone. + lowerer.stagedArguments.delete(e); + const value = lowerer.lowerExpr(e); + const slot = save(value, "%fsArg", e); + lowerer.stagedArguments.set(e, { localId: slot, type: value.type }); + const index = args.findIndex((arg) => stripTypeWrappers(arg) === e); + if (urls.includes(index)) values.set(index, slot); + } + const results: string[] = []; + for (const i of urls) { + const url = varRef(values.get(i)!, URL_T, loc); + const converted: IrExpr = { + kind: "libCall", + fn: "url.fileURLToPathValue", + args: [url], + type: DYN, + loc, + }; + const local = save(converted, "%fsPathValue", args[i]!); + lowerer.fsPathPlans.set(stripParens(args[i]!), { kind: "value", local }); + results.push(local); + } + lowerer.fallibleFsCalls.add(call); + let inner: IrExpr; + try { + inner = lowerCall(); + } finally { + lowerer.fallibleFsCalls.delete(call); + } + let result = inner; + for (let k = results.length - 1; k >= 0; k--) { + const value = varRef(results[k]!, DYN, loc); + const failed: IrExpr = + bi.module === "fs/promises" + ? { kind: "intrinsic", name: "promise.reject", args: [value], type: inner.type, loc } + : boolLit(false, loc); + result = { + kind: "ternary", + cond: { kind: "dynTest", test: "string", value, type: BOOL, loc }, + then: result, + else_: failed, + type: inner.type, + loc, + }; + } + return { kind: "seqExpr", stmts, result, type: inner.type, loc }; } /** An fs path argument as a string: a URL converts with fileURLToPath's * conversion (url.fileURLToPathUrl), which throws Node's errors for other - * schemes, hosts and encoded separators. */ + * schemes, hosts and encoded separators, after the checks its plan names. */ export function lowerFsPath(lowerer: Lowerer, node: ts.Expression): IrExpr { - if (!isUrlArgument(lowerer, node)) return lowerer.lowerExprExpecting(node, STRING); const loc = locOf(node); + const plan = lowerer.fsPathPlans.get(stripParens(node)); + if (plan?.kind === "value") + return { kind: "dynCheck", value: varRef(plan.local, DYN, loc), type: STRING, loc }; + if (!isUrlArgument(lowerer, node)) { + const value = lowerer.lowerExprExpecting(node, STRING); + if (plan?.kind !== "save") return value; + const slot = lowerer.declareHiddenLocal("%fsPath", STRING); + plan.local = slot.id; + return { + kind: "seqExpr", + stmts: [{ kind: "varDecl", localId: slot.id, init: value, loc }], + result: varRef(slot.id, STRING, loc), + type: STRING, + loc, + }; + } const url = lowerer.lowerExprExpecting(node, URL_T); - const staged = lowerer.stagedAfterPath.get(node); - if (staged === undefined) + if (plan?.kind !== "convert") return { kind: "libCall", fn: "url.fileURLToPathUrl", args: [url], type: STRING, loc }; // The URL and the later arguments with effects, in source order, then - // the conversion. + // the checks Node runs first, then the conversion. const saved = lowerer.declareHiddenLocal("%fsUrl", URL_T); const stmts: IrStmt[] = [{ kind: "varDecl", localId: saved.id, init: url, loc }]; - for (const arg of staged) { + for (const arg of plan.staged) { lowerer.stagedArguments.delete(arg); const value = lowerer.lowerExpr(arg); const slot = lowerer.declareHiddenLocal("%fsArg", value.type); stmts.push({ kind: "varDecl", localId: slot.id, init: value, loc: locOf(arg) }); lowerer.stagedArguments.set(arg, { localId: slot.id, type: value.type }); } + const check = (fn: "fs.checkPath" | "fs.checkReadEncoding", args: IrExpr[]): void => { + stmts.push({ kind: "exprStmt", expr: { kind: "libCall", fn, args, type: VOID, loc }, loc }); + }; + for (const { path, name } of plan.checks) { + const saved = lowerer.fsPathPlans.get(path); + if (saved?.kind !== "save" || saved.local === null) + throw new InternalCompilerError("an fs path checked before it was lowered"); + check("fs.checkPath", [ + varRef(saved.local, STRING, loc), + { kind: "strLit", value: name, type: STRING, loc }, + ]); + } + if (plan.encoding !== null) { + const slot = lowerer.stagedArguments.get(plan.encoding); + if (!slot) throw new InternalCompilerError("an fs encoding checked before it was saved"); + check("fs.checkReadEncoding", [ + lowerer.coerceToExpected(varRef(slot.localId, slot.type, loc), STRING), + ]); + } return { kind: "seqExpr", stmts, @@ -219,7 +406,12 @@ export function lowerFsPath(lowerer: Lowerer, node: ts.Expression): IrExpr { }; } -/** Whether `index` is a path parameter of the fs function `member`. */ -export function isFsPathParam(member: string, index: number): boolean { - return FS_URL_PATHS[member]?.paths.includes(index) === true; +/** Whether `index` is a path parameter of the fs or fs.promises function + * `member`. */ +export function isFsPathParam(module: string, member: string, index: number): boolean { + if (module === "fs/promises") return FSP_PATHS[member]?.paths.includes(index) === true; + if (module !== "fs") return false; + return ( + (member === "existsSync" && index === 0) || FS_URL_PATHS[member]?.paths.includes(index) === true + ); } diff --git a/packages/compiler/src/frontend/lowering/builtins/fs-streams.ts b/packages/compiler/src/frontend/lowering/builtins/fs-streams.ts index 498f9b6d4..6c6f8fa79 100644 --- a/packages/compiler/src/frontend/lowering/builtins/fs-streams.ts +++ b/packages/compiler/src/frontend/lowering/builtins/fs-streams.ts @@ -4,7 +4,7 @@ * and everything inherited goes through the stream spoke. */ import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; -import { locOf } from "../../program.js"; +import { isJsSourceFile, locOf } from "../../program.js"; import { isNodeTypesPath } from "../../dts-paths.js"; import { dynUndefinedExpr } from "../../../ir/build.js"; import { @@ -178,3 +178,32 @@ export function lowerFsStreamNew( locOf(expr), ); } + +/** `fs.ReadStream(path, options)` / `fs.WriteStream(...)` called without + * `new` in JavaScript: Node's stream constructors construct when called as + * functions, so the call is the `new` form. Null for other calls. */ +export function lowerFsStreamCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr | null { + if (!isJsSourceFile(ts.getSourceFileOfNode(expr))) return null; + const callee = expr.expression; + const name = ts.isPropertyAccessExpression(callee) + ? callee.name + : ts.isIdentifier(callee) + ? callee + : null; + if (name === null || !ts.isIdentifier(name)) return null; + const symbol = lowerer.checker.getSymbolAtLocation(name); + const resolved = + symbol && symbol.flags & ts.SymbolFlags.Alias + ? lowerer.checker.getAliasedSymbol(symbol) + : symbol; + const cls = fsStreamClassOf(lowerer, resolved); + if (cls === null) return null; + return fsStreamCreateExpr( + lowerer, + expr.arguments.nodes, + cls === "ReadStream", + `fs.${cls}`, + expr, + locOf(expr), + ); +} diff --git a/packages/compiler/src/frontend/lowering/builtins/json.ts b/packages/compiler/src/frontend/lowering/builtins/json.ts index 64f829865..2528554ff 100644 --- a/packages/compiler/src/frontend/lowering/builtins/json.ts +++ b/packages/compiler/src/frontend/lowering/builtins/json.ts @@ -1,3 +1,4 @@ +import { widensReducedRecords } from "../function-returns.js"; import { keyedHelperName } from "../stable-names.js"; import { isDateJsonSource } from "./dates.js"; import { dynUndefinedExpr, varRef } from "../../../ir/build.js"; @@ -96,7 +97,14 @@ export function lowerJsonMethodCall( !isDateJsonSource(lowerer, argNode) ) { const narrowed = lowerer.mapTypeOf(lowerer.typeOf(argNode)); - if (narrowed && !isUnitType(narrowed) && narrowed.kind !== "void") { + // A union of record types the checker's subtype reduction spelled as + // one of them serializes each value by its own layout. + if ( + narrowed && + !isUnitType(narrowed) && + narrowed.kind !== "void" && + !widensReducedRecords(lowerer, narrowed, value.type) + ) { const helper = narrowed.kind === "union" ? lowerer.narrowedRetagHelper(argNode, value.type.unionId, narrowed.unionId, loc) @@ -606,8 +614,15 @@ export function lowerJsonProperty( ): IrExpr | null { const member = lowerer.stdlibGlobalMember(expr, "JSON"); if (member === null) return null; + const value = jsonMemberValue(lowerer, member, locOf(expr)); + if (value !== null) return value; + lowerer.unsupported("SC1090", expr, `JSON methods as values (call '${member}' directly)`); +} + +/** The function value of the JSON member `member` (stringify and parse), + * or null for members without one. */ +export function jsonMemberValue(lowerer: Lowerer, member: string, loc: SrcLoc): IrExpr | null { if (member === "stringify") { - const loc = locOf(expr); const name = "%builtin.JSON.stringify"; if (!lowerer.builtinCallableValueFns.has(name)) { lowerer.builtinCallableValueFns.set(name, name); @@ -654,7 +669,6 @@ export function lowerJsonProperty( if (member === "parse") { // JSON.parse as a value: ToString of the text, then the reviver form // (a reviver that is not a function is ignored, as in Node). - const loc = locOf(expr); const name = "%builtin.JSON.parse"; if (!lowerer.builtinCallableValueFns.has(name)) { lowerer.builtinCallableValueFns.set(name, name); @@ -703,5 +717,5 @@ export function lowerJsonProperty( loc, }; } - lowerer.unsupported("SC1090", expr, `JSON methods as values (call '${member}' directly)`); + return null; } diff --git a/packages/compiler/src/frontend/lowering/builtins/mime.ts b/packages/compiler/src/frontend/lowering/builtins/mime.ts new file mode 100644 index 000000000..4c36d6cc5 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/builtins/mime.ts @@ -0,0 +1,180 @@ +import * as ts from "../../ts7/adapter.js"; +import { type Lowerer } from "../lowerer.js"; +import { locOf } from "../../program.js"; +import { dynUndefinedExpr, numLit } from "../../../ir/build.js"; +import { BOOL, DYN, type IrExpr, type IrLibFn } from "../../../ir/ir.js"; +import { builtinImportOf } from "./module-bindings.js"; + +/* node:util's MIMEType and MIMEParams are native checked-dynamic handles + * (scr_mime.c): both types map to dyn, their getters and setters are + * handle property reads and writes, their methods dispatch through the + * handle, and these helpers lower the uses a dyn value cannot answer by + * itself (construction, MIMEType.parse and instanceof). */ + +type MimeClass = "MIMEType" | "MIMEParams"; + +/** The MIMEType or MIMEParams class in any node:util spelling. */ +export function mimeClassOf(lowerer: Lowerer, node: ts.Expression): MimeClass | null { + const builtin = ts.isIdentifier(node) + ? builtinImportOf(lowerer, node) + : ts.isPropertyAccessExpression(node) && !node.questionDotToken + ? lowerer.builtinMemberOf(node) + : null; + if (builtin?.module !== "util") return null; + return builtin.member === "MIMEType" || builtin.member === "MIMEParams" ? builtin.member : null; +} + +/** The class of a stdlib MIMEType- or MIMEParams-typed value. */ +function mimeValueClass(lowerer: Lowerer, node: ts.Expression): MimeClass | null { + const type = lowerer.typeOf(node); + const sym = type.getAliasSymbol() ?? type.getSymbol(); + if (!sym) return null; + const name = ts.symbolName(sym); + if (name !== "MIMEType" && name !== "MIMEParams") return null; + return lowerer.isStdlibSymbol(sym) ? name : null; +} + +/** The call of `fn` over the first `count` arguments, as checked-dynamic + * values (missing ones as undefined). Every argument evaluates in order + * before the call, as in JavaScript: with surplus arguments, the used ones + * are held in hidden locals while the others evaluate. */ +function mimeCall( + lowerer: Lowerer, + fn: IrLibFn, + nodes: readonly ts.Expression[], + count: number, + loc: ReturnType, +): IrExpr { + const values = nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)); + if (values.length <= count) { + const args = [...values]; + while (args.length < count) args.push(dynUndefinedExpr(loc)); + return { kind: "libCall", fn, args, type: DYN, loc }; + } + const locals = values.slice(0, count).map((value) => ({ + value, + local: lowerer.declareHiddenLocal("%mimeArg", DYN), + })); + return { + kind: "seqExpr", + stmts: [ + ...locals.map(({ value, local }) => ({ + kind: "varDecl" as const, + localId: local.id, + init: value, + loc, + })), + ...values.slice(count).map((arg) => ({ kind: "exprStmt" as const, expr: arg, loc: arg.loc })), + ], + result: { + kind: "libCall", + fn, + args: locals.map(({ local }): IrExpr => ({ + kind: "varRef", + localId: local.id, + type: DYN, + loc, + })), + type: DYN, + loc, + }, + type: DYN, + loc, + }; +} + +/** `new MIMEType(string[, noThrow])` and `new MIMEParams()`. */ +export function lowerMimeNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr | null { + const cls = mimeClassOf(lowerer, expr.expression); + if (cls === null) return null; + const nodes = expr.arguments?.nodes ?? []; + if (nodes.some(ts.isSpreadElement)) return null; + const loc = locOf(expr); + const fn: IrLibFn = cls === "MIMEType" ? "mime.typeNew" : "mime.paramsNew"; + return mimeCall(lowerer, fn, nodes, cls === "MIMEType" ? 2 : 0, loc); +} + +const INSTANCE_METHODS: Record> = { + MIMEType: new Set(["toString", "toJSON"]), + MIMEParams: new Set([ + "get", + "has", + "set", + "delete", + "entries", + "keys", + "values", + "toString", + "toJSON", + ]), +}; + +/** MIMEType.parse(), and the instance methods, which the handle answers + * as methods, so they dispatch through dynInvoke rather than as a member + * read and a call. */ +export function lowerMimeCall(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null { + if (call.questionDotToken || !ts.isPropertyAccessExpression(call.expression)) return null; + const access = call.expression; + if (access.questionDotToken || call.arguments.nodes.some(ts.isSpreadElement)) return null; + const name = access.name.text; + const loc = locOf(call); + if (name === "parse" && mimeClassOf(lowerer, access.expression) === "MIMEType") { + return mimeCall(lowerer, "mime.typeParse", call.arguments.nodes, 1, loc); + } + const cls = mimeValueClass(lowerer, access.expression); + if (cls === null || !INSTANCE_METHODS[cls].has(name)) return null; + return { + kind: "dynInvoke", + recv: lowerer.lowerExprExpecting(access.expression, DYN), + method: name, + calleeName: ts.getTextOfNode(access), + args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), + type: DYN, + loc, + }; +} + +/** Reports SC2020 when `access` reads a static member of MIMEType or + * MIMEParams (`MIMEType.prototype`, `MIMEType.name`); null otherwise. + * JavaScript sources hold either class as an identity token, which has no + * members, so the read would answer undefined. MIMEType.parse calls lower + * in lowerMimeCall and never reach the member read. */ +export function fenceMimeStaticRead(lowerer: Lowerer, access: ts.PropertyAccessExpression): null { + const cls = mimeClassOf(lowerer, access.expression); + if (cls !== null) lowerer.noLowering(`util.${cls}.${access.name.text}`, access); + return null; +} + +/** `v instanceof MIMEType` / `v instanceof MIMEParams`. */ +export function lowerMimeInstanceof(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr | null { + const cls = mimeClassOf(lowerer, expr.right); + if (cls === null) return null; + const loc = locOf(expr); + const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN); + return { + kind: "libCall", + fn: "mime.is", + args: [value, numLit(cls === "MIMEType" ? 0 : 1, loc)], + type: BOOL, + loc, + }; +} + +/** `params`, `params.keys()`, `params.values()` or `params.entries()` over a + * MIMEParams: its checked-dynamic view iterates the live entry list, like + * URLSearchParams (isSearchParamsIterable). */ +export function isMimeParamsIterable(lowerer: Lowerer, node: ts.Expression): boolean { + while (ts.isParenthesizedExpression(node)) node = node.expression; + if (mimeValueClass(lowerer, node) === "MIMEParams") return true; + if ( + !ts.isCallExpression(node) || + node.arguments.nodes.length !== 0 || + !ts.isPropertyAccessExpression(node.expression) + ) + return false; + const name = node.expression.name.text; + return ( + (name === "keys" || name === "values" || name === "entries") && + mimeValueClass(lowerer, node.expression.expression) === "MIMEParams" + ); +} diff --git a/packages/compiler/src/frontend/lowering/builtins/module-bindings.ts b/packages/compiler/src/frontend/lowering/builtins/module-bindings.ts index 5465aeafa..98cc188f8 100644 --- a/packages/compiler/src/frontend/lowering/builtins/module-bindings.ts +++ b/packages/compiler/src/frontend/lowering/builtins/module-bindings.ts @@ -185,6 +185,32 @@ export function builtinMemberRequireDecl( return spec !== null && canonicalBuiltinModule(spec) !== null; } +/** The member's own symbol for a variable bound to a builtin module's + * member (`const SD = require("string_decoder").StringDecoder`, + * builtinMemberRequireDecl) when the member is a class, so the variable + * resolves as a named import of the class does. The symbol itself + * otherwise. */ +export function builtinMemberBindingTarget( + lowerer: Lowerer, + symbol: ts.Symbol | null, +): ts.Symbol | null { + const decl = symbol?.valueDeclaration; + if ( + decl === undefined || + !ts.isVariableDeclaration(decl) || + !builtinMemberRequireDecl(decl.name, decl.initializer) + ) + return symbol; + const init = decl.initializer as ts.PropertyAccessExpression; + let member = lowerer.checker.getSymbolAtLocation(init.name); + if (member && member.flags & ts.SymbolFlags.Alias) + member = lowerer.checker.getAliasedSymbol(member); + // Only a class member: `require("events").EventEmitter` is a static + // property of the exported class, which builtinEmitterInfoOf follows + // from the variable itself. + return member?.declarations.some(ts.isClassDeclaration) ? member : symbol; +} + /** The supported builtin module when `decl` destructures a builtin * NAMESPACE binding (`const crypto = require('crypto'); const { * createSign, sign: mySign } = crypto;` — test/common/crypto.js's @@ -213,7 +239,9 @@ export function builtinNamespaceDestructureModuleOf( if (ts.isCallExpression(init)) { const cr = createRequireSpecOf(lowerer, init); module = cr !== null && cr.spec !== null ? canonicalBuiltinModule(cr.spec) : null; - } else if (ts.isIdentifier(init)) { + } else if (ts.isIdentifier(init) || ts.isPropertyAccessExpression(init)) { + // `const { open } = fs.promises` / `= require("fs").promises`: a member + // that is itself a supported module. module = lowerer.builtinNamespaceModuleOf(init); } if (module === null) return null; diff --git a/packages/compiler/src/frontend/lowering/builtins/module-calls.ts b/packages/compiler/src/frontend/lowering/builtins/module-calls.ts index e5446291f..b21c964f9 100644 --- a/packages/compiler/src/frontend/lowering/builtins/module-calls.ts +++ b/packages/compiler/src/frontend/lowering/builtins/module-calls.ts @@ -25,15 +25,17 @@ import { lowerFsWriteOptionsCall, lowerFsReadPathCall, } from "./filesystem-calls.js"; -import { checkFsUrlPaths, isFsPathParam, lowerFsPath } from "./fs-paths.js"; +import { checkFsUrlPaths, isFsPathParam, lowerFallibleFsUrlCall, lowerFsPath } from "./fs-paths.js"; import { lowerBufferEncodingCall } from "./text-codecs.js"; import { lowerUtilTypeCall } from "./util-types.js"; import { InternalCompilerError } from "../../../errors.js"; +import { dynUndefinedExpr, varRef } from "../../../ir/build.js"; import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; import { type BuiltinModuleFn } from "../surfaces.js"; import { type ParamShape } from "../call-signatures.js"; -import { BYTES_U8, F64, type IrExpr, STRING, type SrcLoc, arrayOf } from "../../../ir/ir.js"; +import { BYTES_U8, DYN, F64, type IrExpr, STRING, type SrcLoc, arrayOf } from "../../../ir/ir.js"; +import { typeKey } from "../../type-mapper.js"; import { lowerProcessLoadEnvFile } from "./environment.js"; import { lowerFsWatchCall, lowerFsReaddirTypesCall, lowerFsStatOptionsCall } from "./filesystem.js"; import { lowerZlibModuleCall } from "./compression.js"; @@ -59,6 +61,10 @@ export function lowerBuiltinModuleCall( loc: SrcLoc, ): IrExpr { const name = ts.getTextOfNode(expr.expression); + const fallible = lowerFallibleFsUrlCall(lowerer, expr, bi, () => + lowerBuiltinModuleCall(lowerer, expr, bi, fn, loc), + ); + if (fallible) return fallible; checkFsUrlPaths(lowerer, expr, bi); if (bi.module === "util/types") { return lowerUtilTypeCall(lowerer, expr, bi, loc, name); @@ -258,7 +264,7 @@ export function lowerBuiltinModuleCall( bi.member === "readFile" && expr.arguments.nodes.length === 1 ) { - const path = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); + const path = lowerFsPath(lowerer, expr.arguments.nodes[0]!); return { kind: "libCall", fn: "fsp.readFileBytes", @@ -382,7 +388,65 @@ export function lowerBuiltinModuleCall( const lowered = lowerFsReadPathCall(lowerer, expr, loc); if (lowered) return lowered; } - const required = fn.params.length - (fn.defaults?.length ?? 0); + // util._extend over a typed target: the writes go through its + // checked-dynamic view, and the call answers the target itself, as Node + // does, when the result type is the target's. + if ( + bi.module === "util" && + bi.member === "_extend" && + expr.arguments.nodes.length >= 1 && + !expr.arguments.nodes.some(ts.isSpreadElement) + ) { + const target = lowerer.lowerExpr(expr.arguments.nodes[0]!); + const resultType = lowerer.mapTypeOf(lowerer.typeOf(expr)); + if ( + target.type.kind !== "dyn" && + resultType !== null && + typeKey(resultType) === typeKey(target.type) + ) { + const held = lowerer.declareHiddenLocal("%extendTarget", target.type); + const ref = varRef(held.id, target.type, loc); + const source = expr.arguments.nodes[1] + ? lowerer.lowerExprExpecting(expr.arguments.nodes[1], DYN) + : dynUndefinedExpr(loc); + return { + kind: "seqExpr", + stmts: [ + { kind: "varDecl", localId: held.id, init: target, loc }, + { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "util.extend", + args: [lowerer.coerceToExpected(ref, DYN), source], + type: DYN, + loc, + }, + loc, + }, + ], + result: ref, + type: target.type, + loc, + }; + } + } + // Trailing checked-dynamic parameters whose value adapter defaults to + // undefined may be omitted: the call passes undefined, as Node sees it. + let undefinedTail = 0; + if (!fn.defaults && fn.valueParams) { + for (let i = fn.valueParams.length - 1; i >= 0; i--) { + const param = fn.valueParams[i]!; + if ( + param.mode !== "optional" || + param.defaultValue !== undefined || + fn.params[i]?.kind !== "dyn" + ) + break; + undefinedTail++; + } + } + const required = fn.params.length - (fn.defaults?.length ?? undefinedTail); const hasSpread = expr.arguments.nodes.some(ts.isSpreadElement); if ( bi.module === "url" && @@ -462,23 +526,24 @@ export function lowerBuiltinModuleCall( : { type, mode: "omittable", - callDefault: { - kind: "strLit", - value: fn.defaults![i - required]!, - type: STRING, - loc, - }, + callDefault: fn.defaults + ? { kind: "strLit", value: fn.defaults[i - required]!, type: STRING, loc } + : dynUndefinedExpr(loc), }, ); args = lowerer.completeArgs(expr.arguments.nodes, shapes, loc, expr); } else { args = expr.arguments.nodes.map((a, i) => - bi.module === "fs" && isFsPathParam(bi.member, i) + isFsPathParam(bi.module, bi.member, i) ? lowerFsPath(lowerer, a) : lowerer.lowerExprExpecting(a, fn.params[i]), ); for (let i = args.length; i < fn.params.length; i++) { - const dflt = fn.defaults![i - required]!; + if (!fn.defaults) { + args.push(dynUndefinedExpr(loc)); + continue; + } + const dflt = fn.defaults[i - required]!; args.push({ kind: "strLit", value: dflt, type: STRING, loc }); } } diff --git a/packages/compiler/src/frontend/lowering/builtins/module-resolution.ts b/packages/compiler/src/frontend/lowering/builtins/module-resolution.ts index bbaf15353..60dd8007f 100644 --- a/packages/compiler/src/frontend/lowering/builtins/module-resolution.ts +++ b/packages/compiler/src/frontend/lowering/builtins/module-resolution.ts @@ -1,8 +1,8 @@ -import { nodeThrowExpr, boolLit, strLit, varRef } from "../../../ir/build.js"; +import { dynUndefinedExpr, nodeThrowExpr, boolLit, strLit, varRef } from "../../../ir/build.js"; import { dirname, isAbsolute, resolve } from "node:path"; import * as ts from "../../ts7/adapter.js"; import { type Lowerer, PoisonError } from "../lowerer.js"; -import { canonicalBuiltinModule, isNodeEsmFile, locOf } from "../../program.js"; +import { canonicalBuiltinModule, isJsSourceFile, isNodeEsmFile, locOf } from "../../program.js"; import { isRelativeSpecifier } from "../../workspace-registry.js"; import { probeNodeRequireRefusal } from "../../npm.js"; import { isNpmStaticPackage } from "../../npm-static.js"; @@ -15,21 +15,26 @@ import { type RuntimeResolveResult, } from "../../runtime-resolve.js"; import { + computedRequireDiag, invalidJsonModuleDiag, nativeAddonDiag, + noLoweringDiag, unsupportedPackageImportDiag, } from "../../../diagnostics/diagnostic.js"; -import { NODE_BUILTIN_MODULES_V24 } from "../surfaces.js"; +import { NODE_BUILTIN_MODULES_V24, builtinModulesArrayLit } from "../surfaces.js"; +import { moduleFileName } from "../import-meta.js"; import { BOOL, DYN, type IrExpr, + type IrStmt, type IrType, NULL_T, STRING, type SrcLoc, UNDEFINED_T, arrayOf, + funcOf, } from "../../../ir/ir.js"; import { lowerFfiMemoryModule } from "../native-ffi.js"; import { @@ -38,6 +43,7 @@ import { createRequireCalleeFileOf, } from "./module-bindings.js"; import { stripTypeCasts } from "./arguments.js"; +import { keyedHelperName } from "../stable-names.js"; /** node:module's two compiler-only calls. isBuiltin compares one evaluated * string against the pinned Node 24 list (bare builtins also accept their @@ -134,6 +140,188 @@ export function lowerNodeModuleCall( }; } +/** The run-time arguments of scr_require.c's computed require and + * require.resolve after the specifier: the requiring file (Node's require + * stack names it), the call's "file:line" for the SC2020 refusal, and the + * builtin ids. The call joins the runtime-fence ledger, so coverage lists + * it as deferred to runtime. */ +function computedRequireArgs( + lowerer: Lowerer, + surface: string, + node: ts.Node, + baseFile: ts.SourceFile, +): IrExpr[] { + const loc = locOf(node); + lowerer.runtimeFences.push(computedRequireDiag(surface, loc)); + const line = ts.getLineAndCharacterOfPosition(ts.getSourceFileOfNode(node), loc.start).line + 1; + return [ + strLit(moduleFileName(lowerer, baseFile), loc), + strLit(`${loc.file}:${line}`, loc), + builtinModulesArrayLit(loc), + ]; +} + +/** A createRequire loader's require.resolve with a computed request, in + * JavaScript source: Node's resolution at run time (scr_require.c), with + * the options argument checked there. */ +function computedResolveCall( + lowerer: Lowerer, + node: ts.Node, + baseFile: ts.SourceFile, + request: IrExpr, + options: IrExpr, + loc: SrcLoc, +): IrExpr { + return { + kind: "libCall", + fn: "module.resolveComputed", + args: [ + request, + options, + ...computedRequireArgs(lowerer, "createRequire's require.resolve", node, baseFile), + ], + type: STRING, + loc, + }; +} + +/** The fence a createRequire use with no build-time lowering throws in + * JavaScript source, where preflight admits it inside a try block + * (createRequireUse7 in program.ts). The diagnostic joins the runtime-fence + * ledger, so coverage lists the use as deferred to runtime. */ +function createRequireFence( + lowerer: Lowerer, + surface: string, + node: ts.Node, + hint: string, +): IrStmt & { kind: "runtimeFence" } { + const loc = locOf(node); + const d = noLoweringDiag(surface, loc, hint); + lowerer.runtimeFences.push(d); + const line = ts.getLineAndCharacterOfPosition(ts.getSourceFileOfNode(node), loc.start).line + 1; + return { + kind: "runtimeFence", + code: d.code, + message: `${d.message} [${d.code} at ${loc.file}:${line}]`, + loc, + }; +} + +/** A createRequire loader's `resolve` read as a value in JavaScript source + * (passed on, or picked by `??`): a function that resolves its request at + * run time, so `(options.resolve ?? require.resolve)(id)` behaves as in + * Node. */ +export function createRequireResolveValue( + lowerer: Lowerer, + access: ts.PropertyAccessExpression, + loc: SrcLoc, +): IrExpr { + const baseFile = createRequireCalleeFileOf(lowerer, access.expression); + if (baseFile === null || !isJsSourceFile(ts.getSourceFileOfNode(access))) { + lowerer.noLowering( + "createRequire's require.resolve as a value", + access, + 'call it with a string literal (require.resolve("")), which resolves at build time', + ); + } + const name = keyedHelperName(lowerer, "%createRequire.resolve", `${loc.file}:${loc.start}`); + if (!lowerer.liftedFns.some((fn) => fn.name === name)) { + const call = computedResolveCall( + lowerer, + access, + baseFile, + varRef("request", DYN, loc), + varRef("options", DYN, loc), + loc, + ); + lowerer.liftedFns.push({ + name, + params: [ + { localId: "request", name: "request", type: DYN }, + { localId: "options", name: "options", type: DYN }, + ], + locals: [ + { id: "request", name: "request", type: DYN, mutable: false }, + { id: "options", name: "options", type: DYN, mutable: false }, + ], + returnType: STRING, + body: [{ kind: "return", value: call, loc }], + loc, + }); + } + return { + kind: "dynFrom", + value: { kind: "closure", fnName: name, captures: [], type: funcOf([DYN, DYN], STRING), loc }, + fnName: access.name.text, + type: DYN, + loc, + }; +} + +/** A createRequire loader's `resolve.paths` read as a value in JavaScript + * source: a function that throws the fence when called. */ +export function createRequireResolveFenceValue( + lowerer: Lowerer, + access: ts.PropertyAccessExpression, + loc: SrcLoc, +): IrExpr { + const surface = "createRequire's require.resolve.paths as a value"; + const hint = + 'call it with a string literal (require.resolve.paths("")), which resolves at build time'; + if (!isJsSourceFile(ts.getSourceFileOfNode(access))) lowerer.noLowering(surface, access, hint); + const name = keyedHelperName(lowerer, "%createRequire.fence", `${loc.file}:${loc.start}`); + if (!lowerer.liftedFns.some((fn) => fn.name === name)) { + lowerer.liftedFns.push({ + name, + params: [], + locals: [], + returnType: DYN, + body: [createRequireFence(lowerer, surface, access, hint)], + loc, + }); + } + return { + kind: "dynFrom", + value: { kind: "closure", fnName: name, captures: [], type: funcOf([], DYN), loc }, + fnName: access.name.text, + type: DYN, + loc, + }; +} + +/** A createRequire use deferred to run time in JavaScript source: as in + * Node, the call's arguments are evaluated in order, then the call throws + * (here, the fence). Code before the call in the same statement runs, and + * the expression diverges, so nothing after it does. */ +function deferredCreateRequireThrow( + lowerer: Lowerer, + call: ts.CallExpression, + surface: string, + hint: string, + loc: SrcLoc, +): IrExpr { + const stmts: IrStmt[] = call.arguments.nodes.map((arg) => ({ + kind: "exprStmt", + expr: lowerer.lowerExpr(arg), + loc: locOf(arg), + })); + const type = lowerer.irTypeOf(call); + const slot = lowerer.declareHiddenLocal("%never", type); + slot.mutable = true; + stmts.push( + { kind: "varDecl", localId: slot.id, init: null, loc }, + createRequireFence(lowerer, surface, call, hint), + ); + return { + kind: "seqExpr", + stmts, + result: varRef(slot.id, type, loc), + diverges: true, + type, + loc, + }; +} + export function lowerCreateRequireCall( lowerer: Lowerer, call: ts.CallExpression, @@ -142,12 +330,37 @@ export function lowerCreateRequireCall( const cr = createRequireSpecOf(lowerer, call); if (cr === null) return null; if (cr.spec === null) { - lowerer.noLowering( - "createRequire's require with this argument shape", + const args = call.arguments.nodes; + const surface = "createRequire's require with a computed specifier or this argument shape"; + const hint = + 'the compiled module graph is fixed at build time — the one lowered form is require(""); native addons are not loaded'; + if (!isJsSourceFile(ts.getSourceFileOfNode(call))) + lowerer.noLowering(surface, call, hint, undefined, "dynamic-require"); + // Spread arguments have no run-time resolution: they run, then the + // call throws the fence. + if (args.some((a) => ts.isSpreadElement(a))) + return deferredCreateRequireThrow(lowerer, call, surface, hint, loc); + // Node evaluates every argument, then requires the first. + const stmts: IrStmt[] = []; + let id = args.length > 0 ? lowerer.lowerExprExpecting(args[0]!, DYN) : dynUndefinedExpr(loc); + if (args.length > 1) { + const slot = lowerer.declareHiddenLocal("%require.id", DYN); + stmts.push({ kind: "varDecl", localId: slot.id, init: id, loc }); + for (const arg of args.slice(1)) + stmts.push({ kind: "exprStmt", expr: lowerer.lowerExpr(arg), loc: locOf(arg) }); + id = varRef(slot.id, DYN, loc); + } + const require: IrExpr = { + kind: "libCall", + fn: "module.requireComputed", + args: [id, ...computedRequireArgs(lowerer, "createRequire's require", call, cr.baseFile)], + type: DYN, + loc, + }; + return lowerer.coerceInto( call, - 'the compiled module graph is fixed at build time — the one lowered form is require("")', - undefined, - "dynamic-require", + stmts.length === 0 ? require : { kind: "seqExpr", stmts, result: require, type: DYN, loc }, + lowerer.irTypeOf(call), ); } const spec = cr.spec; @@ -369,11 +582,30 @@ export function lowerRequireResolveCall(lowerer: Lowerer, call: ts.CallExpressio } const baseFile = requireResolverBaseFile(lowerer, receiver); if (baseFile === null) return null; + const surface = pathsCall ? "require.resolve.paths" : "require.resolve"; + // A createRequire loader's query that resolves only at run time, in + // JavaScript source: preflight admits it inside a try block + // (createRequireUse7 in program.ts). Its arguments run, then it throws. + const deferred = (what: string, hint: string): IrExpr | null => + call.questionDotToken === undefined && + createRequireCalleeFileOf(lowerer, receiver) !== null && + isJsSourceFile(ts.getSourceFileOfNode(call)) + ? deferredCreateRequireThrow( + lowerer, + call, + `createRequire's ${surface} ${what}`, + hint, + locOf(call), + ) + : null; + const resolveHint = `call it with a string literal (${surface}("")), which resolves at build time`; if ( call.questionDotToken !== undefined || call.arguments.nodes.length < 1 || call.arguments.nodes.length > (pathsCall ? 1 : 2) ) { + const fenced = deferred("with this argument shape", resolveHint); + if (fenced) return fenced; lowerer.noLowering( pathsCall ? "require.resolve.paths with this argument shape" @@ -382,7 +614,29 @@ export function lowerRequireResolveCall(lowerer: Lowerer, call: ts.CallExpressio ); } const specifier = staticString(call.arguments.nodes[0]); + // A computed request (or options) through a createRequire loader in + // JavaScript source resolves at run time. + if ( + (specifier === null || staticResolveOptionPaths(call.arguments.nodes[1]) === null) && + !pathsCall && + createRequireCalleeFileOf(lowerer, receiver) !== null && + isJsSourceFile(ts.getSourceFileOfNode(call)) + ) { + const [request, options] = call.arguments.nodes; + return computedResolveCall( + lowerer, + call, + baseFile, + lowerer.lowerExprExpecting(request!, DYN), + options === undefined + ? dynUndefinedExpr(locOf(call)) + : lowerer.lowerExprExpecting(options, DYN), + locOf(call), + ); + } if (specifier === null) { + const fenced = deferred("with a computed request", resolveHint); + if (fenced) return fenced; lowerer.noLowering( `${pathsCall ? "require.resolve.paths" : "require.resolve"} with a runtime-computed request`, call.arguments.nodes[0]!, @@ -418,6 +672,11 @@ export function lowerRequireResolveCall(lowerer: Lowerer, call: ts.CallExpressio } const paths = staticResolveOptionPaths(call.arguments.nodes[1]); if (paths === null) { + const fenced = deferred( + "with runtime-computed options", + 'omit options or pass { paths: ["", ...] }', + ); + if (fenced) return fenced; lowerer.noLowering( "require.resolve with runtime-computed options", call.arguments.nodes[1]!, diff --git a/packages/compiler/src/frontend/lowering/builtins/navigator.ts b/packages/compiler/src/frontend/lowering/builtins/navigator.ts index f90448c1e..f4fbf2567 100644 --- a/packages/compiler/src/frontend/lowering/builtins/navigator.ts +++ b/packages/compiler/src/frontend/lowering/builtins/navigator.ts @@ -1,13 +1,13 @@ -/* Node 24's `navigator` global (lib/internal/navigator.js), the part - * libraries read to tell runtimes apart: `typeof navigator` is "object" - * and `navigator.userAgent` is `Node.js/`. jose reads exactly - * that at module load (jwks/remote.js). Both fold to string literals, so - * nothing links. The other members (hardwareConcurrency, language, - * languages, platform, locks) and the object as a value keep a named +/* Node 24's `navigator` global (lib/internal/navigator.js): `typeof + * navigator` is "object", `navigator.userAgent` is `Node.js/` (both + * fold to string literals; jose reads them at module load), and + * hardwareConcurrency, language, languages and platform read the runtime's + * answers (scr_node_builtin.c). The object as a value is the runtime's + * Navigator handle, whose getters answer the same. `locks` keeps a named * refusal. */ import * as ts from "../../ts7/adapter.js"; import { strLit } from "../../../ir/build.js"; -import type { IrExpr } from "../../../ir/ir.js"; +import { DYN, F64, STRING, type IrExpr, type SrcLoc } from "../../../ir/ir.js"; import { isJsSourceFile, locOf } from "../../program.js"; import type { Lowerer } from "../lowerer.js"; @@ -34,9 +34,18 @@ export function lowerNavigatorMember( nodeVersion: string, ): IrExpr | null { if (!isNavigatorGlobal(lowerer, access.expression)) return null; + const loc = locOf(access); if (access.name.text === "userAgent") { - return strLit(`Node.js/${nodeVersion.split(".")[0]}`, locOf(access)); + return strLit(`Node.js/${nodeVersion.split(".")[0]}`, loc); } + if (access.name.text === "hardwareConcurrency") + return { kind: "libCall", fn: "os.availableParallelism", args: [], type: F64, loc }; + if (access.name.text === "language" || access.name.text === "platform") { + const fn = access.name.text === "language" ? "navigator.language" : "navigator.platform"; + return { kind: "libCall", fn, args: [], type: STRING, loc }; + } + if (access.name.text === "languages") + return { kind: "libCall", fn: "navigator.languages", args: [], type: DYN, loc }; return refuseNavigator( lowerer, access, @@ -55,7 +64,12 @@ export function refuseNavigator( return lowerer.noLowering( what, node, - "of Node's navigator global only typeof navigator and navigator.userAgent compile", + "navigator.locks (the Web Locks API) has no lowering", symbol ?? null, ); } + +/** The navigator object as a value: the runtime's Navigator handle. */ +export function navigatorValue(loc: SrcLoc): IrExpr { + return { kind: "libCall", fn: "navigator.object", args: [], type: DYN, loc }; +} diff --git a/packages/compiler/src/frontend/lowering/builtins/promises.ts b/packages/compiler/src/frontend/lowering/builtins/promises.ts index 129ac73a3..c3b2cd479 100644 --- a/packages/compiler/src/frontend/lowering/builtins/promises.ts +++ b/packages/compiler/src/frontend/lowering/builtins/promises.ts @@ -2,8 +2,9 @@ import { keyedHelperName } from "../stable-names.js"; import { checkedPromiseAll, thenReader } from "../checked-promise-all.js"; import { InternalCompilerError } from "../../../errors.js"; import * as ts from "../../ts7/adapter.js"; -import { newFnCtx, type Lowerer } from "../lowerer.js"; -import { varRef } from "../../../ir/build.js"; +import type { Lowerer } from "../lowerer.js"; +import { countedFor, varRef } from "../../../ir/build.js"; +import { arrayValueStore, optionalElementValue, primitiveElement } from "../array-values.js"; import { maybePromiseResolve, maybePromiseUnionOf } from "../maybe-promise.js"; import { isJsSourceFile, locOf } from "../../program.js"; import { @@ -14,34 +15,27 @@ import { type IrFunction, type IrStmt, type IrType, + type SrcLoc, UNDEFINED_T, VOID, arrayOf, + funcOf, isUnitType, typeEquals, typeKey, } from "../../../ir/ir.js"; /** `Promise.all([p, q, r])` over DIFFERENT promise types — the checker's - * tuple overload, `Promise<[A, B, C]>`. One interned async helper per - * entry/result signature takes the entries as arguments, awaits the - * runtime combinator over the same promises (each widened to - * Promise, a type-only view), so settlement is Node's exactly: the - * first rejection in settlement order wins and every later one counts as - * handled; then it reads each (now fulfilled) entry and returns the - * tuple. Entries keep their identity — no value converts through the - * checked-dynamic tree. Non-promise entries (numbers, strings, booleans, - * unit values — never thenable) pass straight into their slot; a - * checked-dynamic entry (a JavaScript function's result) joins through - * PromiseResolve: the promise it holds, a promise that adopts a thenable, - * or a promise fulfilled with its plain value. The tuple - * fulfills one microtask turn after the combinator does. Null when the - * result type or an entry has no typed home here. */ -function lowerTuplePromiseAll( + * tuple overload, `Promise<[A, B, C]>`: the result type and tuple shape, + * or null when the result type or an entry has no typed home here. + * Entries are promises, checked-dynamic values (a JavaScript function's + * result) and plain numbers, strings, booleans and unit values, which are + * never thenable. */ +function tuplePromiseAllShape( lowerer: Lowerer, call: ts.CallExpression, - elems: IrExpr[], -): IrExpr | null { + elems: readonly IrExpr[], +): { resultT: IrType & { kind: "promise" }; tuple: IrType & { kind: "record" } } | null { const resultT = lowerer.mapTypeOf(lowerer.typeOf(call)); if (resultT?.kind !== "promise" || resultT.inner.kind !== "record") return null; const shape = lowerer.shapes.get(resultT.inner.shapeId); @@ -53,135 +47,159 @@ function lowerTuplePromiseAll( const settles = (t: IrType): boolean => t.kind === "promise" || t.kind === "dyn"; if (!elems.some((e) => settles(e.type))) return null; if (!elems.every((e) => settles(e.type) || plainValue(e.type))) return null; + return { resultT, tuple: resultT.inner }; +} + +/** `Promise.all([p, q, r])` over different promise types. Each entry + * becomes a promise in input order (a plain value through Promise.resolve, + * a checked-dynamic one through its PromiseResolve view: the promise it + * holds, a promise that adopts a thenable, or a promise fulfilled with the + * value), widened to Promise, a type-only view. The runtime + * combinator over them gives Node's settlement: the first rejection in + * settlement order wins and every later one counts as handled. The + * result settles inside the combinator's own settlement, so it and its + * awaits land on Node's microtask turns; a fulfillment runs one interned + * function per entry/result signature that reads the fulfilled entries + * into the tuple. Entries keep their identity: no value converts through + * the checked-dynamic tree. */ +function lowerTuplePromiseAll( + lowerer: Lowerer, + call: ts.CallExpression, + elems: IrExpr[], +): IrExpr | null { + const plan = tuplePromiseAllShape(lowerer, call, elems); + if (!plan) return null; + const { resultT, tuple } = plan; const loc = locOf(call); - const key = `${elems.map((e) => typeKey(e.type)).join(",")}->${typeKey(resultT.inner)}`; + const voidPromise: IrType & { kind: "promise" } = { kind: "promise", inner: VOID }; + const entriesT = arrayOf(voidPromise); + const dynPromise: IrType = { kind: "promise", inner: DYN }; + // A checked-dynamic entry's PromiseResolve can run a `then` getter, which + // Node runs after the whole array literal evaluated. With such an entry, + // every entry evaluates into a hidden local first. + const staged: IrStmt[] = []; + const values = !elems.some((e) => e.type.kind === "dyn") + ? elems + : elems.map((e): IrExpr => { + if (e.kind === "unitLit") return e; + if (isUnitType(e.type)) { + staged.push({ kind: "exprStmt", expr: e, loc }); + return { + kind: "unitLit", + unit: e.type.kind === "nullT" ? "null" : "undefined", + type: e.type, + loc, + }; + } + const local = lowerer.declareHiddenLocal("%allValue", e.type); + staged.push({ kind: "varDecl", localId: local.id, init: e, loc }); + return varRef(local.id, e.type, loc); + }); + const entries = values.map((e): IrExpr => { + let promise: IrExpr; + if (e.type.kind === "promise") promise = e; + else if (e.type.kind === "dyn") + promise = { + kind: "libCall", + fn: "dyn.promiseResolve", + args: [e, thenReader(lowerer, loc)], + type: dynPromise, + loc, + }; + else if (isUnitType(e.type)) { + // A unit entry's value is its type's one value; the promise only + // takes its place in the combinator. + const resolved: IrExpr = { + kind: "intrinsic", + name: "promise.resolve", + args: [], + type: voidPromise, + loc, + }; + promise = + e.kind === "unitLit" + ? resolved + : { + kind: "seqExpr", + stmts: [{ kind: "exprStmt", expr: e, loc }], + result: resolved, + type: voidPromise, + loc, + }; + } else + promise = { + kind: "intrinsic", + name: "promise.resolve", + args: [e], + type: { kind: "promise", inner: e.type }, + loc, + }; + return promise.type.kind === "promise" && promise.type.inner.kind === "void" + ? promise + : { kind: "promiseVoidWiden", value: promise, type: voidPromise, loc }; + }); + const key = `${elems.map((e) => typeKey(e.type)).join(",")}->${typeKey(tuple)}`; let name = lowerer.arrHofHelpers.get(`promise.allTuple:${key}`); if (name === undefined) { name = keyedHelperName(lowerer, "%promise.allTuple", `promise.allTuple:${key}`); lowerer.arrHofHelpers.set(`promise.allTuple:${key}`, name); - const ctx = newFnCtx(false, null, null, resultT.inner); - ctx.isAsync = true; - lowerer.fnStack.push(ctx); - try { - const params = elems.map((e, i) => ({ - localId: `entry.${i}`, - name: `entry${i}`, - type: e.type, - })); - const ref = (i: number): IrExpr => varRef(`entry.${i}`, elems[i]!.type, loc); - const dynPromise: IrType = { kind: "promise", inner: DYN }; - const body: IrStmt[] = []; - // Each dyn entry's promise view, in entry order: the promise it - // holds, a promise that adopts a thenable's settlement, or a promise - // fulfilled with the plain value (PromiseResolve's rule). - const views = new Map(); - elems.forEach((e, i) => { - if (e.type.kind !== "dyn") return; - const view = lowerer.declareHiddenLocal("%allEntry", dynPromise); - body.push({ - kind: "varDecl", - localId: view.id, - init: { - kind: "libCall", - fn: "dyn.promiseResolve", - args: [ref(i), thenReader(lowerer, loc)], - type: dynPromise, - loc, - }, - loc, - }); - views.set(i, varRef(view.id, dynPromise, loc)); - }); - const widened: IrExpr[] = []; - elems.forEach((e, i) => { - const view = views.get(i); - if (view !== undefined) { - widened.push({ - kind: "promiseVoidWiden", - value: view, - type: { kind: "promise", inner: VOID }, - loc, - }); - return; - } - if (e.type.kind !== "promise") return; - widened.push( - e.type.inner.kind === "void" - ? ref(i) - : { - kind: "promiseVoidWiden", - value: ref(i), - type: { kind: "promise", inner: VOID }, + const shape = lowerer.shapes.get(tuple.shapeId)!; + const entriesRef = varRef("entries.0", entriesT, loc); + // Shape fields are sorted by name ("10" before "2"); a tuple field's + // name is its position. + const fields = shape.fields.map((field) => { + const i = Number(field.name); + const type = elems[i]!.type; + const inner = type.kind === "promise" ? type.inner : type; + const value: IrExpr = + inner.kind === "void" + ? { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc } + : isUnitType(inner) + ? { + kind: "unitLit", + unit: inner.kind === "nullT" ? "null" : "undefined", + type: inner, loc, - }, - ); - }); - const settled: IrExpr = { - kind: "awaitExpr", - value: { - kind: "intrinsic", - name: "promise.all", - args: [ - { - kind: "arrayLit", - elems: widened, - type: arrayOf({ kind: "promise", inner: VOID }), - loc, - }, - ], - type: { kind: "promise", inner: VOID }, - loc, - }, - type: VOID, - loc, - }; - // Shape fields are sorted by name ("10" before "2"); a tuple field's - // name is its position. - const fields = shape.fields.map((field) => { - const i = Number(field.name); - const entry = elems[i]!; - let value: IrExpr; - const view = views.get(i); - if (view !== undefined) { - value = { kind: "awaitExpr", value: view, type: DYN, loc }; - } else if (entry.type.kind === "promise") { - value = - entry.type.inner.kind === "void" - ? { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc } - : { kind: "awaitExpr", value: ref(i), type: entry.type.inner, loc }; - } else { - value = ref(i); - } - return { name: field.name, value: lowerer.coerceToExpected(value, field.type) }; - }); - body.push( - { kind: "exprStmt", expr: settled, loc }, - { - kind: "return", - value: { kind: "recordLit", fields, type: resultT.inner, loc }, - loc, - }, - ); - lowerer.liftedFns.push({ - name, - params, - returnType: resultT.inner, - locals: [ - ...params.map((p) => ({ id: p.localId, name: p.name, type: p.type, mutable: false })), - ...lowerer.ctx.locals, - ], - body, - async: true, - loc, - }); - } catch (e) { - lowerer.arrHofHelpers.delete(`promise.allTuple:${key}`); - throw e; - } finally { - lowerer.fnStack.pop(); - } + } + : { + kind: "libCall", + fn: "promise.fulfilledValue", + args: [ + { + kind: "arrayGet", + arr: entriesRef, + index: { kind: "numLit", value: i, type: F64, loc }, + type: voidPromise, + loc, + }, + ], + type: inner, + loc, + }; + return { name: field.name, value: lowerer.coerceToExpected(value, field.type) }; + }); + lowerer.liftedFns.push({ + name, + params: [{ localId: "entries.0", name: "entries", type: entriesT }], + returnType: tuple, + locals: [{ id: "entries.0", name: "entries", type: entriesT, mutable: false }], + body: [{ kind: "return", value: { kind: "recordLit", fields, type: tuple, loc }, loc }], + loc, + }); } - return { kind: "call", callee: name, args: elems, type: resultT, loc }; + const all: IrExpr = { + kind: "libCall", + fn: "promise.allTuple", + args: [ + { kind: "arrayLit", elems: entries, type: entriesT, loc }, + { kind: "closure", fnName: name, captures: [], type: funcOf([entriesT], tuple), loc }, + ], + type: resultT, + loc, + }; + return staged.length === 0 + ? all + : { kind: "seqExpr", stmts: staged, result: all, type: resultT, loc }; } /** `Promise.race([...])` on THE Promise global: the entries lower @@ -419,6 +437,28 @@ export function lowerPromiseStaticCall( inner.kind === "void" ? { kind: "promise", inner: VOID } : lowerer.mapTypeOf(lowerer.typeOf(call)); + // Promise[] combines into a `(T | undefined)[]`, which + // stores T elements (primitiveElement): collect the values, then copy + // them in with their present undefined values. + if ( + resultT?.kind === "promise" && + resultT.inner.kind === "array" && + primitiveElement(resultT.inner.elem) && + optionalElementValue( + lowerer, + { kind: "unitLit", unit: "undefined", type: inner, loc }, + resultT.inner.elem, + ) + ) { + const joined = optionalPromiseAll( + lowerer, + entries, + inner, + resultT as IrType & { kind: "promise" }, + loc, + ); + if (joined) return joined; + } if ( resultT?.kind !== "promise" || (inner.kind !== "void" && @@ -938,3 +978,139 @@ export function lowerAwaitedUniformPromiseAll( loc, }; } + +/** `Promise.all(ps)` over `Promise[]` for a number, string or + * boolean T, whose result is a `(T | undefined)[]` (stored as T elements). + * The combinator joins the entries (promise.allTuple), and its builder + * reads each fulfilled entry into the result with its present undefined, + * inside the combinator's settlement, so the result settles on Node's + * turn. Entries of another type keep the general combinator. */ +function optionalPromiseAll( + lowerer: Lowerer, + entries: IrExpr, + inner: IrType, + resultT: IrType & { kind: "promise" }, + loc: SrcLoc, +): IrExpr | null { + const entriesT = entries.type; + if (entriesT.kind !== "array" || entriesT.elem.kind !== "promise") return null; + if (!typeEquals(entriesT.elem.inner, inner)) return null; + const outT = resultT.inner as IrType & { kind: "array" }; + const voidPromise: IrType & { kind: "promise" } = { kind: "promise", inner: VOID }; + const joinedT = arrayOf(voidPromise); + const key = `promise.allOptional:${typeKey(entriesT)}:${typeKey(outT)}`; + let helper = lowerer.arrHofHelpers.get(key); + if (!helper) { + helper = keyedHelperName(lowerer, "%promise.allOptional", key); + lowerer.arrHofHelpers.set(key, helper); + const builder = keyedHelperName(lowerer, "%promise.allOptionalValues", key); + const length = (arr: IrExpr): IrExpr => ({ + kind: "arrIntrinsic", + method: "length", + receiver: arr, + args: [], + type: F64, + loc, + }); + const joined = varRef("joined.0", joinedT, loc); + const out = varRef("out.0", outT, loc); + lowerer.liftedFns.push({ + name: builder, + params: [{ localId: "joined.0", name: "joined", type: joinedT }], + returnType: outT, + locals: [ + { id: "joined.0", name: "joined", type: joinedT, mutable: false }, + { id: "out.0", name: "out", type: outT, mutable: false }, + { id: "i.0", name: "i", type: F64, mutable: true }, + { id: "v.0", name: "v", type: inner, mutable: false }, + ], + body: [ + { + kind: "varDecl", + localId: "out.0", + init: { kind: "arrayLit", elems: [], type: outT, loc }, + loc, + }, + countedFor(loc, length(joined), (index) => [ + { + kind: "varDecl", + localId: "v.0", + init: { + kind: "libCall", + fn: "promise.fulfilledValue", + args: [{ kind: "arrayGet", arr: joined, index, type: voidPromise, loc }], + type: inner, + loc, + }, + loc, + }, + arrayValueStore(lowerer, out, index, varRef("v.0", inner, loc), outT.elem, loc), + ]), + { kind: "return", value: out, loc }, + ], + loc, + }); + const ps = varRef("ps.0", entriesT, loc); + lowerer.liftedFns.push({ + name: helper, + params: [{ localId: "ps.0", name: "ps", type: entriesT }], + returnType: resultT, + locals: [ + { id: "ps.0", name: "ps", type: entriesT, mutable: false }, + { id: "joined.0", name: "joined", type: joinedT, mutable: false }, + { id: "i.0", name: "i", type: F64, mutable: true }, + ], + body: [ + { + kind: "varDecl", + localId: "joined.0", + init: { kind: "arrayLit", elems: [], type: joinedT, loc }, + loc, + }, + countedFor(loc, length(ps), (index) => [ + { + kind: "exprStmt", + expr: { + kind: "arrIntrinsic", + method: "push", + receiver: joined, + args: [ + { + kind: "promiseVoidWiden", + value: { kind: "arrayGet", arr: ps, index, type: entriesT.elem, loc }, + type: voidPromise, + loc, + }, + ], + type: F64, + loc, + }, + loc, + }, + ]), + { + kind: "return", + value: { + kind: "libCall", + fn: "promise.allTuple", + args: [ + joined, + { + kind: "closure", + fnName: builder, + captures: [], + type: funcOf([joinedT], outT), + loc, + }, + ], + type: resultT, + loc, + }, + loc, + }, + ], + loc, + }); + } + return { kind: "call", callee: helper, args: [entries], type: resultT, loc }; +} diff --git a/packages/compiler/src/frontend/lowering/builtins/promisify-callback.ts b/packages/compiler/src/frontend/lowering/builtins/promisify-callback.ts index b4dbbf513..d3378c8ec 100644 --- a/packages/compiler/src/frontend/lowering/builtins/promisify-callback.ts +++ b/packages/compiler/src/frontend/lowering/builtins/promisify-callback.ts @@ -1,7 +1,8 @@ import { keyedHelperName } from "../stable-names.js"; import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; -import { varRef } from "../../../ir/build.js"; +import { dynUndefinedExpr, varRef } from "../../../ir/build.js"; +import { isJsSourceFile } from "../../program.js"; import { BOOL, DYN, @@ -92,12 +93,22 @@ function callbackShape( return { fnT, resultT, args, callbackT, callbackParamT: callbackParamT!, inner, error }; } -/** Whether `promisify(fn)`'s one argument is typed `any`: the function is - * only known at run time, so the promisified function is built there. */ +/** Whether `promisify(fn)`'s one argument is only known at run time, so + * the promisified function is built there: a value typed `any`, a function + * with untyped parameters (the declarations type that call `any`), or any + * argument in JavaScript, whose inferred types do not describe the + * function Node receives (it validates the argument and reads its own + * properties). */ export function untypedPromisifyArgument(lowerer: Lowerer, call: ts.CallExpression): boolean { - if (call.questionDotToken || call.arguments.nodes.length !== 1) return false; - const argNode = call.arguments.nodes[0]!; - return !ts.isSpreadElement(argNode) && (lowerer.typeOf(argNode).flags & ts.TypeFlags.Any) !== 0; + if (call.questionDotToken || call.arguments.nodes.length > 1) return false; + const argNode = call.arguments.nodes[0]; + if (argNode !== undefined && ts.isSpreadElement(argNode)) return false; + if (isJsSourceFile(ts.getSourceFileOfNode(call))) return true; + if (argNode === undefined) return false; + return ( + (lowerer.typeOf(argNode).flags & ts.TypeFlags.Any) !== 0 || + (lowerer.typeOf(call).flags & ts.TypeFlags.Any) !== 0 + ); } /** The callback shape of a `promisify(fn)` call, or null. */ @@ -144,10 +155,11 @@ export function lowerCallbackPromisify( if (shape === null) { // An untyped (`any`) function: Node's promisify at run time. if (!untypedPromisifyArgument(lowerer, call)) return null; + const argNode = call.arguments.nodes[0]; return { kind: "libCall", fn: "util.promisifyDyn", - args: [lowerer.lowerExprExpecting(call.arguments.nodes[0]!, DYN)], + args: [argNode ? lowerer.lowerExprExpecting(argNode, DYN) : dynUndefinedExpr(loc)], type: DYN, loc, }; diff --git a/packages/compiler/src/frontend/lowering/builtins/readline.ts b/packages/compiler/src/frontend/lowering/builtins/readline.ts index aca209017..c2b6984ce 100644 --- a/packages/compiler/src/frontend/lowering/builtins/readline.ts +++ b/packages/compiler/src/frontend/lowering/builtins/readline.ts @@ -302,6 +302,15 @@ function interfaceClassModule(lowerer: Lowerer, node: ts.Expression): string | n : null; } +/** `new readline.Interface(...)` and `new readlinePromises.Interface(...)`: + * the constructor createInterface calls, with the same arguments. Null for + * other constructors. */ +export function lowerReadlineInterfaceNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr | null { + const module = interfaceClassModule(lowerer, expr.expression); + if (module === null) return null; + return lowerReadlineCreateCall(lowerer, expr, locOf(expr), module === "readline/promises"); +} + /** `rl instanceof readline.Interface` and `rl instanceof * readlinePromises.Interface` over a readline Interface: an interface is * an instance of the class of the module that created it (Node's @@ -1059,10 +1068,12 @@ function lowerOutput(lowerer: Lowerer, node: ts.Expression): IrExpr | "stdout" | * goes through rl.terminal with the completer. */ export function lowerReadlineCreateCall( lowerer: Lowerer, - expr: ts.CallExpression, + expr: ts.CallExpression | ts.NewExpression, loc: SrcLoc, promises: boolean, ): IrExpr { + const argNodes = expr.arguments?.nodes ?? []; + const what = ts.isNewExpression(expr) ? "new Interface" : "createInterface"; let input: IrExpr | null = null; let output: IrExpr | "stdout" | "none" = "none"; let crlf: IrExpr = numLit(0, loc); @@ -1112,39 +1123,38 @@ export function lowerReadlineCreateCall( }); terminal = { kind: "varRef", localId: local.id, type: BOOL, loc }; }; - const optsNode = expr.arguments.nodes[0]; + const optsNode = argNodes[0]; if (optsNode === undefined) { lowerer.noLowering( - "createInterface without arguments", + `${what} without arguments`, expr, "pass { input: process.stdin, output: process.stdout } or createInterface(input, output)", ); } if (!ts.isObjectLiteralExpression(optsNode)) { // createInterface(input, output?, completer?, terminal?) - if (expr.arguments.nodes.length > 4) { + if (argNodes.length > 4) { lowerer.noLowering( - `createInterface with ${expr.arguments.nodes.length} arguments`, - expr.arguments.nodes[4]!, + `${what} with ${argNodes.length} arguments`, + argNodes[4]!, "pass createInterface(input, output, completer, terminal) or one options object", ); } input = lowerInput(lowerer, optsNode); - if (expr.arguments.nodes[1] !== undefined) - output = lowerOutput(lowerer, expr.arguments.nodes[1]); - const completerNode = expr.arguments.nodes[2]; + if (argNodes[1] !== undefined) output = lowerOutput(lowerer, argNodes[1]); + const completerNode = argNodes[2]; if ( completerNode !== undefined && !(ts.isIdentifier(completerNode) && completerNode.text === "undefined") ) { setCompleter(completerNode); } - if (expr.arguments.nodes[3] !== undefined) setTerminal(expr.arguments.nodes[3]); + if (argNodes[3] !== undefined) setTerminal(argNodes[3]); } else { - if (expr.arguments.nodes.length > 1) { + if (argNodes.length > 1) { lowerer.noLowering( - "createInterface with more than one argument", - expr.arguments.nodes[1]!, + `${what} with more than one argument`, + argNodes[1]!, "pass every option in the one options object", ); } diff --git a/packages/compiler/src/frontend/lowering/builtins/reflection.ts b/packages/compiler/src/frontend/lowering/builtins/reflection.ts index 8e1e6fd94..ef55fa7e5 100644 --- a/packages/compiler/src/frontend/lowering/builtins/reflection.ts +++ b/packages/compiler/src/frontend/lowering/builtins/reflection.ts @@ -3,7 +3,33 @@ import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; import { locOf } from "../../program.js"; import { builtinModuleFnOf } from "../surfaces.js"; -import { BOOL, DYN, type IrExpr, STRING, arrayOf } from "../../../ir/ir.js"; +import { hasRuntimeStatics } from "../class-runtime-statics.js"; +import { + BOOL, + DYN, + type IrExpr, + STRING, + type SrcLoc, + arrayOf, + isUnitType, +} from "../../../ir/ir.js"; + +/** The function value of the Reflect member `member` (ownKeys, on native + * computed-key reflection), or null for members without one. */ +export function reflectMemberValue(lowerer: Lowerer, member: string, loc: SrcLoc): IrExpr | null { + if (member !== "ownKeys") return null; + const value = lowerer.lowerNativeCallableValue( + { + fn: "dyn.ownKeys", + params: [DYN], + result: DYN, + valueParams: [{ mode: "required", type: DYN }], + }, + "Reflect.ownKeys", + loc, + )!; + return { kind: "dynFrom", value, fnName: "ownKeys", type: DYN, loc }; +} /** Reflect.ownKeys uses native computed-key reflection. Reflect.apply(target, * thisArg, argsList) where the TARGET is a builtin @@ -63,6 +89,37 @@ export function lowerReflectCall( loc, }; } + // Reflect.deleteProperty over a value with a live property table: a + // checked-dynamic value, or a JavaScript class whose statics are runtime + // properties of its constructor + // (`Reflect.deleteProperty(Response, "getResponseState")`). + if ( + reflectMember === "deleteProperty" && + call.arguments.nodes.length === 2 && + !call.arguments.nodes.some(ts.isSpreadElement) + ) { + const loc = locOf(call); + let target = lowerer.lowerExpr(call.arguments.nodes[0]!); + const info = + target.type.kind === "classval" ? lowerer.classes.get(target.type.className) : undefined; + // A primitive target throws the TypeError, so it boxes as is. + const primitive = + isUnitType(target.type) || + ["bool", "f64", "string", "bigint", "symbol"].includes(target.type.kind); + if ( + primitive || + (info !== undefined && hasRuntimeStatics(info) && lowerer.dynConvertible(target.type)) + ) + target = lowerer.coerceToExpected(target, DYN); + if (target.type.kind !== "dyn") return null; + return { + kind: "libCall", + fn: "dyn.reflectDeleteProperty", + args: [target, lowerer.lowerExprExpecting(call.arguments.nodes[1]!, DYN)], + type: BOOL, + loc, + }; + } if (reflectMember === "get" || reflectMember === "set") { const required = reflectMember === "get" ? 2 : 3; if ( diff --git a/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts b/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts index 8d47c8e3a..39156940e 100644 --- a/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts +++ b/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts @@ -24,6 +24,36 @@ import { } from "./arguments.js"; import { type BuiltinModuleFn } from "../surfaces.js"; +/** The input of `decoder.decode(input)`. An undefined input decodes no bytes, + * as `decode()` does in Node: a statically undefined argument (after its + * effects) reads as an empty Uint8Array, and the undefined arm of a typed + * array or view that may be undefined (`Uint8Array | undefined`, + * `Uint16Array | undefined`) as an empty view of the same type. */ +function lowerDecodeInput(lowerer: Lowerer, node: ts.Expression, loc: SrcLoc): IrExpr { + const undefinedArg = lowerStaticallyUndefinedArgument(lowerer, node); + if (undefinedArg) + return defaultAfterUndefined(undefinedArg, { + kind: "bytesNew", + source: null, + type: BYTES_U8, + loc, + }); + const value = lowerer.lowerExpr(node); + if (value.type.kind === "union") { + const def = lowerer.unions.get(value.type.unionId); + const view = def?.arms.find((arm) => arm.kind === "bytes"); + if (def?.arms.length === 2 && view && def.arms.some((arm) => arm.kind === "undefinedT")) + return { + kind: "nullish", + left: value, + right: { kind: "bytesNew", source: null, type: view, loc }, + type: view, + loc, + }; + } + return value; +} + /** StringDecoder writes preserve pending bytes; end(input) performs the * final write and flushes the state. String input passes through unchanged. */ export function lowerStringDecoderMethodCall( @@ -413,7 +443,7 @@ function lowerStoredTextCodecCall( : { kind: "bytesNew", source: null, type: BYTES_U8, loc } : cls === "TextEncoder" ? lowerer.lowerExprExpecting(call.arguments.nodes[0]!, STRING) - : lowerer.lowerExpr(call.arguments.nodes[0]!); + : lowerDecodeInput(lowerer, call.arguments.nodes[0]!, loc); if ( cls === "TextDecoder" && (arg.type.kind === "dyn" || (arg.type.kind === "bytes" && arg.type.elem !== "u8")) @@ -622,7 +652,7 @@ export function lowerTextCodecCall( ); } const argNode = call.arguments.nodes[0]!; - let arg = lowerer.lowerExpr(argNode); + let arg = lowerDecodeInput(lowerer, argNode, loc); if (arg.type.kind === "dyn" || (arg.type.kind === "bytes" && arg.type.elem !== "u8")) { arg = { kind: "libCall", diff --git a/packages/compiler/src/frontend/lowering/builtins/webassembly.ts b/packages/compiler/src/frontend/lowering/builtins/webassembly.ts index 8e5c3a048..f33ebe9fe 100644 --- a/packages/compiler/src/frontend/lowering/builtins/webassembly.ts +++ b/packages/compiler/src/frontend/lowering/builtins/webassembly.ts @@ -403,7 +403,10 @@ export function lowerWebAssemblyValueOf( recv: IrExpr, loc: IrExpr["loc"], ): IrExpr | null { - const ctx = { checker: lowerer.checker, isStdlibFile: lowerer.isStdlibFile }; + const ctx = { + checker: lowerer.checker, + isStdlibFile: (sf: ts.SourceFile) => lowerer.isStdlibFile(sf), + }; if (!isWebAssemblyType(lowerer.typeOf(receiver), ctx)) return null; lowerer.usesWebAssembly = true; return { kind: "libCall", fn: "wasm.valueOf", args: [recv], type: DYN, loc }; diff --git a/packages/compiler/src/frontend/lowering/builtins/x509.ts b/packages/compiler/src/frontend/lowering/builtins/x509.ts new file mode 100644 index 000000000..5d81070eb --- /dev/null +++ b/packages/compiler/src/frontend/lowering/builtins/x509.ts @@ -0,0 +1,488 @@ +import * as ts from "../../ts7/adapter.js"; +import { type Lowerer } from "../lowerer.js"; +import { locOf } from "../../program.js"; +import { + BOOL, + BYTES_U8, + DYN, + F64, + STRING, + UNDEFINED_T, + type IrExpr, + type IrFunction, + type IrLibFn, + type IrType, + type SrcLoc, +} from "../../../ir/ir.js"; +import { everyStmtList } from "../../../ir/traverse.js"; +import { unsupportedDiag } from "../../../diagnostics/diagnostic.js"; +import { X509_DER_FIELD, X509_PUBLIC_KEY_FIELD } from "../../type-mapper.js"; +import { keyedHelperName } from "../stable-names.js"; +import { x509RecordCall } from "../lower-classes.js"; + +/* node:crypto's X509Certificate members beyond its fields: the methods + * and the publicKey getter. A certificate is the Dirent-style record + * (type-mapper.ts), and each member reads the record's hidden copy of its + * DER, which writes into the `raw` Buffer leave alone, as in Node. checkHost, + * checkEmail, checkIP, checkIssued, toLegacyObject and the PEM of + * toString/toJSON run in scr_x509.c; + * publicKey, verify and checkPrivateKey read keys in scr_pki.c. The first + * publicKey read stores the KeyObject in the record's hidden slot, so + * every read answers the same object, as in Node. */ + +/** Whether `t` is the X509Certificate record (its hidden publicKey slot + * marks the shape; no program can name that field). */ +export function isX509Type(lowerer: Lowerer, t: IrType | null | undefined): boolean { + return ( + t?.kind === "record" && + lowerer.shapes.get(t.shapeId)?.fields.some((f) => f.name === X509_PUBLIC_KEY_FIELD) === true + ); +} + +/** Whether `node` is an X509Certificate record. */ +function isX509Record(lowerer: Lowerer, node: ts.Expression): boolean { + const type = lowerer.typeOf(node); + const sym = type.getSymbol(); + if (!sym || ts.symbolName(sym) !== "X509Certificate" || !lowerer.isStdlibSymbol(sym)) + return false; + return isX509Type(lowerer, lowerer.mapTypeOf(type)); +} + +/** A certificate in an `unknown` or `any` slot would cross as a plain + * object of its fields, which Node's certificate is not: its fields are + * prototype getters, and toString() and toJSON() give its PEM. Every such + * conversion in the lowered program, including a certificate inside an + * array or record that converts, is refused where it happens. */ +export function refuseX509DynValues(lowerer: Lowerer, functions: readonly IrFunction[]): void { + // Certificates exist only where `new X509Certificate` built one. + if ( + !lowerer.valueHelpers.has("x509.record:bytes") && + !lowerer.valueHelpers.has("x509.record:str") && + !lowerer.valueHelpers.has("x509.record:der") + ) + return; + const seen = new Map(); + const holds = (t: IrType): boolean => { + switch (t.kind) { + case "record": { + const known = seen.get(t.shapeId); + if (known !== undefined) return known; + seen.set(t.shapeId, false); + const shape = lowerer.shapes.get(t.shapeId); + const result = + isX509Type(lowerer, t) || + (shape !== undefined && + (shape.fields.some((f) => holds(f.type)) || + (shape.indexValue !== undefined && holds(shape.indexValue)))); + seen.set(t.shapeId, result); + return result; + } + case "array": + case "set": + return holds(t.elem); + case "map": + return holds(t.key) || holds(t.value); + case "promise": + return holds(t.inner); + case "union": + return lowerer.unions.get(t.unionId)?.arms.some(holds) === true; + default: + return false; + } + }; + const visitor = { + expr: (e: IrExpr): boolean => { + if (e.kind === "dynFrom" && holds(e.value.type)) { + lowerer.pushDiag( + unsupportedDiag( + "SC1090", + e.loc, + "X509Certificate values in 'unknown' or 'any' slots (Node keeps a certificate's fields as prototype getters, and its toString() and toJSON() give the PEM; keep the X509Certificate type)", + ), + ); + } + return true; + }, + stmt: (): boolean => true, + }; + for (const fn of functions) everyStmtList(fn.body, visitor); +} + +/** String(x509), `${x509}` and x509 + "": the certificate's toString(), + * its PEM. */ +export function x509ToString(record: IrExpr, loc: SrcLoc): IrExpr { + return { kind: "libCall", fn: "crypto.x509Pem", args: [derOf(record, loc)], type: STRING, loc }; +} + +/** Node keeps a certificate's fields as prototype getters, so own-key + * walks (Object.keys/values/entries, for-in, spread, hasOwnProperty) see + * none of them. Those walks over a certificate fence instead of listing + * the fields. */ +export function fenceX509KeyWalk( + lowerer: Lowerer, + t: IrType | null | undefined, + node: ts.Node, + what: string, +): void { + if (!isX509Type(lowerer, t)) return; + lowerer.unsupported( + "SC1090", + node, + `${what} over X509Certificate values (Node keeps their fields as prototype getters, so own-key walks see none; read the fields)`, + ); +} + +function derOf(record: IrExpr, loc: SrcLoc): IrExpr { + if (record.type.kind !== "record") return record; + return { + kind: "recordGet", + obj: record, + shapeId: record.type.shapeId, + field: X509_DER_FIELD, + type: BYTES_U8, + loc, + }; +} + +const dynUndefined = (loc: SrcLoc): IrExpr => ({ + kind: "dynFrom", + value: { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc }, + type: DYN, + loc, +}); + +/** The method's string answer as `string | undefined`: "" is undefined. + * A lifted helper takes the text once. */ +function optionalString(lowerer: Lowerer, text: IrExpr, type: IrType, loc: SrcLoc): IrExpr { + if (type.kind !== "union") return text; + const def = lowerer.unions.get(type.unionId); + const stringTag = def ? def.arms.findIndex((a) => a.kind === "string") : -1; + const undefinedTag = def ? def.arms.findIndex((a) => a.kind === "undefinedT") : -1; + if (stringTag < 0 || undefinedTag < 0) return text; + const key = `x509.optional:${type.unionId}`; + let helper = lowerer.valueHelpers.get(key); + if (!helper) { + helper = keyedHelperName(lowerer, "%x509.optional", key); + lowerer.valueHelpers.set(key, helper); + const s: IrExpr = { kind: "varRef", localId: "s.0", type: STRING, loc }; + lowerer.liftedFns.push({ + name: helper, + params: [{ localId: "s.0", name: "s", type: STRING }], + returnType: type, + locals: [{ id: "s.0", name: "s", type: STRING, mutable: false }], + body: [ + { + kind: "return", + value: { + kind: "ternary", + cond: { + kind: "strEq", + negated: false, + left: s, + right: { kind: "strLit", value: "", type: STRING, loc }, + type: BOOL, + loc, + }, + then: { + kind: "unionWrap", + unionId: type.unionId, + tag: undefinedTag, + value: { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc }, + type, + loc, + }, + else_: { + kind: "unionWrap", + unionId: type.unionId, + tag: stringTag, + value: s, + type, + loc, + }, + type, + loc, + }, + loc, + }, + ], + loc, + }); + } + return { kind: "call", callee: helper, args: [text], type, loc }; +} + +/** `x509.publicKey`: the cached KeyObject (a lifted helper fills the + * hidden slot on the first read). */ +export function lowerX509PublicKey( + lowerer: Lowerer, + access: ts.PropertyAccessExpression, +): IrExpr | null { + if (access.name.text !== "publicKey" || access.questionDotToken) return null; + if (!isX509Record(lowerer, access.expression)) return null; + const loc = locOf(access); + const record = lowerer.lowerExpr(access.expression); + if (record.type.kind !== "record") return null; + const shapeId = record.type.shapeId; + const key = `x509.publicKey:${shapeId}`; + let helper = lowerer.valueHelpers.get(key); + if (!helper) { + helper = keyedHelperName(lowerer, "%x509.publicKey", key); + lowerer.valueHelpers.set(key, helper); + const c: IrExpr = { kind: "varRef", localId: "c.0", type: record.type, loc }; + const slot: IrExpr = { + kind: "recordGet", + obj: c, + shapeId, + field: X509_PUBLIC_KEY_FIELD, + type: DYN, + loc, + }; + lowerer.liftedFns.push({ + name: helper, + params: [{ localId: "c.0", name: "c", type: record.type }], + returnType: DYN, + locals: [{ id: "c.0", name: "c", type: record.type, mutable: false }], + body: [ + { + kind: "if", + cond: { kind: "dynTest", test: "undefined", value: slot, type: BOOL, loc }, + then: [ + { + kind: "recordSet", + obj: c, + shapeId, + field: X509_PUBLIC_KEY_FIELD, + value: { + kind: "libCall", + fn: "crypto.x509PublicKey", + args: [derOf(c, loc)], + type: DYN, + loc, + }, + loc, + }, + ], + else_: null, + loc, + }, + { kind: "return", value: slot, loc }, + ], + loc, + }); + } + return { kind: "call", callee: helper, args: [record], type: DYN, loc }; +} + +type X509Method = { fn: IrLibFn; min: number; max: number; form: string }; + +const METHODS: ReadonlyMap = new Map([ + ["verify", { fn: "crypto.x509Verify", min: 1, max: 1, form: "verify(publicKey)" }], + [ + "checkPrivateKey", + { fn: "crypto.x509CheckPrivateKey", min: 1, max: 1, form: "checkPrivateKey(privateKey)" }, + ], + ["checkIssued", { fn: "crypto.x509CheckIssued", min: 1, max: 1, form: "checkIssued(otherCert)" }], + ["checkHost", { fn: "crypto.x509CheckHost", min: 1, max: 2, form: "checkHost(name[, options])" }], + [ + "checkEmail", + { fn: "crypto.x509CheckEmail", min: 1, max: 2, form: "checkEmail(email[, options])" }, + ], + ["checkIP", { fn: "crypto.x509CheckIP", min: 1, max: 2, form: "checkIP(ip[, options])" }], + ["toLegacyObject", { fn: "crypto.x509LegacyObject", min: 0, max: 0, form: "toLegacyObject()" }], + ["toString", { fn: "crypto.x509Pem", min: 0, max: 0, form: "toString()" }], + ["toJSON", { fn: "crypto.x509Pem", min: 0, max: 0, form: "toJSON()" }], +]); + +/** The X509Certificate methods: verify, checkPrivateKey, checkIssued, + * checkHost, checkEmail, checkIP, toLegacyObject, toString and toJSON. */ +export function lowerX509Call(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null { + if (call.questionDotToken || !ts.isPropertyAccessExpression(call.expression)) return null; + const access = call.expression; + if (access.questionDotToken) return null; + const method = METHODS.get(access.name.text); + if (!method) return null; + if (!isX509Record(lowerer, access.expression)) return null; + const loc = locOf(call); + const args = call.arguments.nodes; + if (args.length < method.min || args.length > method.max || args.some(ts.isSpreadElement)) { + lowerer.noLowering( + `X509Certificate.${access.name.text} with ${args.length} argument(s)`, + call, + `the supported form is ${method.form}`, + ); + } + const raw = derOf(lowerer.lowerExpr(access.expression), loc); + if (method.fn === "crypto.x509LegacyObject") { + return { kind: "libCall", fn: method.fn, args: [raw], type: DYN, loc }; + } + if (method.fn === "crypto.x509Pem") + return { kind: "libCall", fn: method.fn, args: [raw], type: STRING, loc }; + if (method.fn === "crypto.x509CheckIssued") { + if (!isX509Record(lowerer, args[0]!)) { + // Node's validation of otherCert, after both operands evaluate. + return { + kind: "seqExpr", + stmts: [{ kind: "exprStmt", expr: raw, loc }], + result: { + kind: "libCall", + fn: "error.argTypeThrow", + args: [ + { kind: "strLit", value: "otherCert", type: STRING, loc }, + { kind: "strLit", value: "an instance of X509Certificate", type: STRING, loc }, + lowerer.lowerExprExpecting(args[0]!, DYN), + ], + type: BOOL, + loc, + }, + type: BOOL, + loc, + }; + } + const other = derOf(lowerer.lowerExpr(args[0]!), loc); + return { kind: "libCall", fn: method.fn, args: [raw, other], type: BOOL, loc }; + } + const first = lowerer.lowerExprExpecting(args[0]!, DYN); + if (method.fn === "crypto.x509Verify" || method.fn === "crypto.x509CheckPrivateKey") { + return { kind: "libCall", fn: method.fn, args: [raw, first], type: BOOL, loc }; + } + const options = args.length > 1 ? lowerer.lowerExprExpecting(args[1]!, DYN) : dynUndefined(loc); + const text: IrExpr = { + kind: "libCall", + fn: method.fn, + args: [raw, first, options], + type: STRING, + loc, + }; + return optionalString(lowerer, text, lowerer.mapTypeOf(lowerer.typeOf(call)) ?? STRING, loc); +} + +/** The TLS socket certificate accessors. getPeerCertificate([detailed]) + * and getCertificate() answer Node's legacy object (scr_tls_x509.c): + * Node reads `detailed` only when it is true. getPeerX509Certificate() and + * getX509Certificate() build a new X509Certificate from the certificate's + * DER on each call, or answer undefined without one. Null for other + * members. */ +export function lowerTlsCertificateCall( + lowerer: Lowerer, + call: ts.CallExpression, + name: string, + receiver: () => IrExpr, +): IrExpr | null { + const loc = locOf(call); + const args = call.arguments.nodes; + if (name === "getPeerCertificate" || name === "getCertificate") { + const max = name === "getPeerCertificate" ? 1 : 0; + if (args.length > max || args.some(ts.isSpreadElement)) { + lowerer.noLowering( + `${name} with ${args.length} argument(s)`, + call, + name === "getPeerCertificate" + ? "the form is getPeerCertificate(detailed?)" + : "getCertificate() takes no arguments", + ); + } + const socket = receiver(); + const value: IrExpr = + name === "getCertificate" + ? { kind: "libCall", fn: "tls.sockGetCertificate", args: [socket], type: DYN, loc } + : { + kind: "libCall", + fn: "tls.sockGetPeerCertificate", + args: [ + socket, + args.length > 0 ? lowerer.lowerExprExpecting(args[0]!, DYN) : dynUndefined(loc), + ], + type: DYN, + loc, + }; + const declared = lowerer.mapTypeOf(lowerer.typeOf(call)); + return declared === null || declared.kind === "dyn" + ? value + : lowerer.coerceInto(call, value, declared); + } + if (name !== "getPeerX509Certificate" && name !== "getX509Certificate") return null; + if (args.length > 0) { + lowerer.noLowering( + `${name} with ${args.length} argument(s)`, + call, + `${name}() takes no arguments`, + ); + } + const type = lowerer.mapTypeOf(lowerer.typeOf(call)); + const def = type?.kind === "union" ? lowerer.unions.get(type.unionId) : undefined; + const recordTag = def ? def.arms.findIndex((arm) => isX509Type(lowerer, arm)) : -1; + const undefinedTag = def ? def.arms.findIndex((arm) => arm.kind === "undefinedT") : -1; + if (type?.kind !== "union" || !def || recordTag < 0 || undefinedTag < 0) { + lowerer.noLowering( + `tlsSocket.${name} with this result type`, + call, + "its type is X509Certificate | undefined", + ); + } + const record = def.arms[recordTag] as IrType & { kind: "record" }; + const der: IrExpr = { + kind: "libCall", + fn: name === "getPeerX509Certificate" ? "tls.sockPeerCertDer" : "tls.sockOwnCertDer", + args: [receiver()], + type: BYTES_U8, + loc, + }; + const key = `tls.x509:${type.unionId}`; + let helper = lowerer.valueHelpers.get(key); + if (!helper) { + helper = keyedHelperName(lowerer, "%tls.x509", key); + lowerer.valueHelpers.set(key, helper); + const d: IrExpr = { kind: "varRef", localId: "d.0", type: BYTES_U8, loc }; + lowerer.liftedFns.push({ + name: helper, + params: [{ localId: "d.0", name: "der", type: BYTES_U8 }], + returnType: type, + locals: [{ id: "d.0", name: "der", type: BYTES_U8, mutable: false }], + body: [ + { + kind: "return", + value: { + kind: "ternary", + cond: { + kind: "bin", + op: "===", + left: { + kind: "bytesIntrinsic", + method: "length", + receiver: d, + args: [], + type: F64, + loc, + }, + right: { kind: "numLit", value: 0, type: F64, loc }, + type: BOOL, + loc, + }, + then: { + kind: "unionWrap", + unionId: type.unionId, + tag: undefinedTag, + value: { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc }, + type, + loc, + }, + else_: { + kind: "unionWrap", + unionId: type.unionId, + tag: recordTag, + // The handshake's DER, read without PEM detection. + value: x509RecordCall(lowerer, d, record, loc, true), + type, + loc, + }, + type, + loc, + }, + loc, + }, + ], + loc, + }); + } + return { kind: "call", callee: helper, args: [der], type, loc }; +} diff --git a/packages/compiler/src/frontend/lowering/call-arguments.ts b/packages/compiler/src/frontend/lowering/call-arguments.ts index 060ffe9d2..797bb1775 100644 --- a/packages/compiler/src/frontend/lowering/call-arguments.ts +++ b/packages/compiler/src/frontend/lowering/call-arguments.ts @@ -28,6 +28,7 @@ import { checkedIterableSpread } from "./checked-iterable-spread.js"; import { type ParamShape } from "./call-signatures.js"; import { arrayValueRead, arrayValueType } from "./array-values.js"; import { buildArrayConversion } from "./coercions/builders.js"; +import { lowerArraySpreadItems, lowerArrayValueItems } from "./containers/array-construction.js"; import { restTupleElementTypes, typeKey } from "../type-mapper.js"; function fixedTupleSpreadInfo( @@ -56,6 +57,95 @@ function emptyTupleSpread(lowerer: Lowerer, node: ts.Expression): boolean { return restTupleElementTypes(lowerer.typeOf(operand), lowerer.typeCtx)?.length === 0; } +/** A call's arguments as one checked-dynamic array, built in source order + * into a fresh hidden local: each argument converts to dyn, and a spread + * contributes the values its iteration yields (holes as undefined). + * `lowerSpread` lowers a spread's operand; the default lowers it here. */ +export function dynArgumentPack( + lowerer: Lowerer, + argNodes: readonly ts.Expression[], + loc: SrcLoc, + options: { + /** Pre-lowered values pushed before the arguments. */ + leading?: readonly IrExpr[] | undefined; + blame?: ts.Node | undefined; + lowerSpread?: (arg: ts.SpreadElement) => IrExpr; + } = {}, +): { stmts: IrStmt[]; pack: () => IrExpr } { + const blame = options.blame ?? argNodes[0]!; + const lowerSpread = + options.lowerSpread ?? ((arg: ts.SpreadElement) => lowerer.lowerExpr(arg.expression)); + const pack = lowerer.declareHiddenLocal("%spreadArgs", DYN); + const ref = (): IrExpr => ({ kind: "varRef", localId: pack.id, type: DYN, loc }); + const stmts: IrStmt[] = [ + { + kind: "varDecl", + localId: pack.id, + init: { kind: "dynArrLit", elems: [], type: DYN, loc }, + loc, + }, + ]; + const push = (value: IrExpr, fn: "dyn.packPush" | "dyn.packPushSpreadIter") => { + stmts.push({ + kind: "exprStmt", + expr: { kind: "libCall", fn, args: [ref(), value], type: VOID, loc: value.loc }, + loc: value.loc, + }); + }; + for (const value of options.leading ?? []) + push(lowerer.coerceInto(blame, value, DYN), "dyn.packPush"); + const spreads = argNodes.filter(ts.isSpreadElement).length; + argNodes.forEach((arg, index) => { + if (!ts.isSpreadElement(arg)) { + push(lowerer.lowerExprExpecting(arg, DYN), "dyn.packPush"); + return; + } + let value = lowerSpread(arg); + if (value.type.kind === "set") { + value = { + kind: "setIntrinsic", + method: "toArray", + receiver: value, + args: [], + type: arrayOf(value.type.elem), + loc: value.loc, + }; + } else if (value.type.kind === "object") { + value = lowerer.classIteratorDrainCall(value, locOf(arg)) ?? value; + } + if (value.type.kind === "array") { + // Argument iteration materializes holes as present undefined + // before conversion to the dense checked-dynamic argument pack. + value = { + kind: "arrayLit", + elems: [value], + spreads: [0], + type: value.type, + loc: value.loc, + }; + } + value = checkedIterableSpread(lowerer, value, ts.getTextOfNode(arg.expression), loc); + if (spreads === 1 && index === argNodes.length - 1) { + stmts.push({ + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "dyn.packPushSpread", + args: [ + ref(), + value, + { kind: "strLit", value: ts.getTextOfNode(arg.expression), type: STRING, loc }, + ], + type: VOID, + loc, + }, + loc, + }); + } else push(value, "dyn.packPushSpreadIter"); + }); + return { stmts, pack: ref }; +} + export function completeArgs( lowerer: Lowerer, argNodes: readonly ts.Expression[], @@ -96,72 +186,10 @@ export function completeArgs( // Runtime-length spreads determine the complete argument list before // any parameter default runs. Build it once in source order, then // extract the fixed native ABI slots; missing elements are undefined. - const pack = lowerer.declareHiddenLocal("%spreadArgs", DYN); - const ref = (): IrExpr => ({ kind: "varRef", localId: pack.id, type: DYN, loc }); - const stmts: IrStmt[] = [ - { - kind: "varDecl", - localId: pack.id, - init: { kind: "dynArrLit", elems: [], type: DYN, loc }, - loc, - }, - ]; - const push = (value: IrExpr, fn: "dyn.packPush" | "dyn.packPushSpreadIter") => { - stmts.push({ - kind: "exprStmt", - expr: { kind: "libCall", fn, args: [ref(), value], type: VOID, loc: value.loc }, - loc: value.loc, - }); - }; - for (const value of leading ?? []) push(lowerer.coerceInto(blame, value, DYN), "dyn.packPush"); - const spreads = argNodes.filter(ts.isSpreadElement).length; - argNodes.forEach((arg, index) => { - if (!ts.isSpreadElement(arg)) { - push(lowerer.lowerExprExpecting(arg, DYN), "dyn.packPush"); - return; - } - let value = lowerSpread(arg); - if (value.type.kind === "set") { - value = { - kind: "setIntrinsic", - method: "toArray", - receiver: value, - args: [], - type: arrayOf(value.type.elem), - loc: value.loc, - }; - } else if (value.type.kind === "object") { - value = lowerer.classIteratorDrainCall(value, locOf(arg)) ?? value; - } - if (value.type.kind === "array") { - // Argument iteration materializes holes as present undefined - // before conversion to the dense checked-dynamic argument pack. - value = { - kind: "arrayLit", - elems: [value], - spreads: [0], - type: value.type, - loc: value.loc, - }; - } - value = checkedIterableSpread(lowerer, value, ts.getTextOfNode(arg.expression), loc); - if (spreads === 1 && index === argNodes.length - 1) { - stmts.push({ - kind: "exprStmt", - expr: { - kind: "libCall", - fn: "dyn.packPushSpread", - args: [ - ref(), - value, - { kind: "strLit", value: ts.getTextOfNode(arg.expression), type: STRING, loc }, - ], - type: VOID, - loc, - }, - loc, - }); - } else push(value, "dyn.packPushSpreadIter"); + const { stmts, pack: ref } = dynArgumentPack(lowerer, argNodes, loc, { + leading, + blame, + lowerSpread, }); const args = shapes.map((shape, index): IrExpr => { if (shape.mode === "arguments") return ref(); @@ -403,6 +431,51 @@ export function completeArgs( } } if (restType.kind !== "array") lowerer.unsupported("SC1090", blame, "this call form"); + // A `(T | undefined)[]` rest (stored as T elements) packs undefined + // arguments as present undefined elements. + const restNodes = sources.slice(restAt); + // Plain arguments and spreads, also together with spread tuple fields + // (pre-lowered): each item, in argument order. + if ( + restType.optionalElements === true && + (!restNodes.some(isIr) || restNodes.some((a) => !isIr(a) && ts.isSpreadElement(a))) + ) { + const items: (ts.Expression | IrExpr)[] = []; + for (const a of restNodes) { + if (isIr(a)) items.push(a.ir); + else items.push(a); + } + out.push(lowerArraySpreadItems(lowerer, items, restType.elem, restType, loc)); + return out; + } + // Tuple-spread values (pre-lowered) and plain arguments pack the same + // way. Each value evaluates once, in argument order. + if ( + restType.optionalElements === true && + !restNodes.some((a) => !isIr(a) && ts.isSpreadElement(a)) + ) { + const stmts: IrStmt[] = []; + const values = restNodes.map((a): IrExpr => { + // A tuple field is read where its spread appears: a later + // argument may write the tuple. + let value: IrExpr; + if (isIr(a)) { + value = a.ir; + if (value.kind === "seqExpr") { + stmts.push(...value.stmts); + value = value.result; + } + } else value = lowerer.lowerExpr(a as ts.Expression); + const saved = lowerer.declareHiddenLocal("%callArg", value.type); + stmts.push({ kind: "varDecl", localId: saved.id, init: value, loc: value.loc }); + return { kind: "varRef", localId: saved.id, type: value.type, loc: value.loc }; + }); + const items = lowerArrayValueItems(lowerer, values, restType.elem, restType, loc); + out.push( + stmts.length === 0 ? items : { kind: "seqExpr", stmts, result: items, type: restType, loc }, + ); + return out; + } // The rest pack is a fresh array per call; surplus SPREADS copy // their elements in (JS-exact — `f(a, ...xs, b, ...ys)` packs in // order, sources untouched). diff --git a/packages/compiler/src/frontend/lowering/call-signatures.ts b/packages/compiler/src/frontend/lowering/call-signatures.ts index 45e6b091c..cefe518dd 100644 --- a/packages/compiler/src/frontend/lowering/call-signatures.ts +++ b/packages/compiler/src/frontend/lowering/call-signatures.ts @@ -1,3 +1,4 @@ +import { shapeOverlayAnnotated } from "../shape-inference/index.js"; import { typedArgumentsCandidate, typedArgumentsShapes, @@ -181,6 +182,12 @@ export function checkedJsDocumentedParameter( lowerer: Lowerer, param: ts.ParameterDeclaration, ): boolean { + if ( + param.parent !== undefined && + shapeOverlayAnnotated(param.parent) && + documentedParameter(param) + ) + return true; return lowerer.checkedJsFile(ts.getSourceFileOfNode(param)) && documentedParameter(param); } @@ -611,7 +618,6 @@ export function checkDefaultParamBodyType( if ( bodyType.kind === "void" || bodyType.kind === "regex" || - bodyType.kind === "date" || bodyType.kind === "dyn" || isUnitType(bodyType) ) { diff --git a/packages/compiler/src/frontend/lowering/cjs-exports-object.ts b/packages/compiler/src/frontend/lowering/cjs-exports-object.ts index ae8cce4ca..5e5f24e32 100644 --- a/packages/compiler/src/frontend/lowering/cjs-exports-object.ts +++ b/packages/compiler/src/frontend/lowering/cjs-exports-object.ts @@ -381,7 +381,7 @@ function esModuleExportIgnoresThis( depth: number, ): boolean { const moduleSym = lowerer.checker.getSymbolAtLocation(sf); - const exported = moduleSym?.getExports().get(name as ts.__String); + const exported = moduleSym?.getExports().get(ts.escapedSymbolName(name)); if (exported !== undefined) { const target = exported.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(exported) : exported; diff --git a/packages/compiler/src/frontend/lowering/cjs-inline-require.ts b/packages/compiler/src/frontend/lowering/cjs-inline-require.ts index a4df8d910..d8893aa72 100644 --- a/packages/compiler/src/frontend/lowering/cjs-inline-require.ts +++ b/packages/compiler/src/frontend/lowering/cjs-inline-require.ts @@ -28,8 +28,10 @@ import { isModuleExportsAccess, locOf, npmStaticDepSf7, + requireBindingEarlyUse7, requireSpecOf, resolveImport, + splitRequireCallOf, } from "../program.js"; import { createRequireCalleeFileOf } from "./builtins/module-bindings.js"; import { isCjsRequireGlobal } from "./lower-node-module.js"; @@ -43,6 +45,332 @@ export interface InlineRequire { dep: ts.SourceFile; } +/** A top-level `var m; m = require("./x")` binding. Node evaluates the + * require at the assignment; the binding is alias plumbing exactly like + * `const m = require("./x")` for every later read. */ +export interface SplitRequireBinding { + decl: ts.VariableDeclaration; + assign: ts.BinaryExpression; + call: ts.CallExpression; + spec: string; + dep: ts.SourceFile; +} + +function symbolOfIdentifierValue(lowerer: Lowerer, ident: ts.Identifier): ts.Symbol | undefined { + const parent = ident.parent; + // A destructuring shorthand target resolves to the property symbol at + // the name, but reads/writes the variable through its value symbol. + if (ts.isShorthandPropertyAssignment(parent) && parent.name === ident) { + return ( + lowerer.checker.getShorthandAssignmentValueSymbol(parent) ?? + lowerer.checker.getSymbolAtLocation(ident) + ); + } + return lowerer.checker.getSymbolAtLocation(ident); +} + +function destructuringContainerOf(node: ts.Node): ts.Node | null { + const parent = node.parent; + if (ts.isShorthandPropertyAssignment(parent) && parent.name === node) return parent; + if (ts.isPropertyAssignment(parent) && parent.initializer === node) return parent; + if (ts.isSpreadElement(parent) && parent.expression === node) return parent; + if (ts.isSpreadAssignment(parent) && parent.expression === node) return parent; + return null; +} + +function writeTargetOf(node: ts.Node): ts.Node | null { + let target = node; + for (;;) { + const parent = target.parent; + if ( + ts.isParenthesizedExpression(parent) || + ts.isArrayLiteralExpression(parent) || + ts.isObjectLiteralExpression(parent) + ) { + target = parent; + continue; + } + const container = destructuringContainerOf(target); + if (container === null) break; + target = container; + } + const parent = target.parent; + if ( + ts.isBinaryExpression(parent) && + parent.left === target && + ts.isAssignmentOperator(parent.operatorToken.kind) + ) + return parent; + if ( + (ts.isPrefixUnaryExpression(parent) || ts.isPostfixUnaryExpression(parent)) && + parent.operand === target && + (parent.operator === ts.SyntaxKind.PlusPlusToken || + parent.operator === ts.SyntaxKind.MinusMinusToken) + ) + return parent; + if ((ts.isForInStatement(parent) || ts.isForOfStatement(parent)) && parent.initializer === target) + return parent; + return null; +} + +/** The split form of a top-level require binding: `var m; m = require("./x")`. + * Exactly one write, at a top-level `m = require("")` statement. + * Null for the ordinary `const m = require("./x")` declaration and for + * every form that needs runtime module selection. */ +export function splitRequireBindingOf( + lowerer: Lowerer, + decl: ts.VariableDeclaration, +): SplitRequireBinding | null { + // Cheap shape gate before the cache: ordinary declarations never enter the + // per-Lowerer map. + if (decl.initializer !== undefined || !ts.isIdentifier(decl.name)) return null; + const list = decl.parent as ts.VariableDeclarationList; + if ((list.flags & ts.NodeFlags.Const) !== 0) return null; + const stmt = list.parent; + if (!ts.isVariableStatement(stmt) || !ts.isSourceFile(stmt.parent)) return null; + const cached = lowerer.cjsSplitRequireBindings.get(decl); + if (cached !== undefined) return cached; + const result = computeSplitRequireBinding(lowerer, decl); + lowerer.cjsSplitRequireBindings.set(decl, result); + return result; +} + +function computeSplitRequireBinding( + lowerer: Lowerer, + decl: ts.VariableDeclaration, +): SplitRequireBinding | null { + const sf = ts.getSourceFileOfNode(decl); + if (!isCjsJsFile(sf, lowerer.program)) return null; + if (!sf.text.includes("require")) return null; + const symbol = lowerer.checker.getSymbolAtLocation(decl.name); + if (symbol === undefined) return null; + // Merged declarations make the binding's initialization order ambiguous; + // keep ordinary storage instead of guessing. + let declarations = 0; + for (const other of symbol.declarations) { + if (!ts.isVariableDeclaration(other) || ++declarations > 1) return null; + } + if (declarations !== 1) return null; + + // Find the one top-level `m = ...` statement first: most uninitialized + // variables never reach that shape, so they avoid the full-file write scan. + let assign: ts.BinaryExpression | null = null; + for (const stmt of sf.statements.nodes) { + if (!ts.isExpressionStatement(stmt)) continue; + const expr = stmt.expression; + if ( + !ts.isBinaryExpression(expr) || + expr.operatorToken.kind !== ts.SyntaxKind.EqualsToken || + !ts.isIdentifier(expr.left) || + expr.left.text !== decl.name.text || + lowerer.checker.getSymbolAtLocation(expr.left) !== symbol + ) + continue; + if (assign !== null) return null; + assign = expr; + } + if (assign === null) return null; + + const req = inlineProgramRequireOf(lowerer, assign.right); + if (req === null) return null; + // Preflight treats the same bindings as require declarations (module edge, + // cycle admission, early-read fence); a binding it does not know keeps + // ordinary storage. + if (splitRequireCallOf(lowerer.program, decl) !== req.call) return null; + + // Any other write makes the value depend on runtime control flow. + let writes = 0; + ts.walkPreorder(sf, (node) => { + if (!ts.isIdentifier(node) || node.text !== decl.name.text) return; + if (symbolOfIdentifierValue(lowerer, node) !== symbol) return; + if (writeTargetOf(node) === null) return; + if (++writes > 1) return "stop"; + }); + if (writes !== 1) return null; + + return { decl, assign, call: req.call, spec: req.spec, dep: req.dep }; +} + +/** The split binding an identifier names, or null. */ +export function splitRequireBindingOfIdent( + lowerer: Lowerer, + ident: ts.Identifier, +): SplitRequireBinding | null { + const symbol = symbolOfIdentifierValue(lowerer, ident); + return symbol === undefined ? null : splitBindingOfSymbol(lowerer, symbol); +} + +/** The exact top-level assignment statement that initializes `expr`, or null. */ +export function splitRequireAssignmentOf( + lowerer: Lowerer, + expr: ts.Expression, +): SplitRequireBinding | null { + if ( + !ts.isBinaryExpression(expr) || + expr.operatorToken.kind !== ts.SyntaxKind.EqualsToken || + !ts.isIdentifier(expr.left) + ) + return null; + const bindingOfIdent = splitRequireBindingOfIdent(lowerer, expr.left); + return bindingOfIdent !== null && bindingOfIdent.assign === expr ? bindingOfIdent : null; +} + +/** The whole-export symbol a module's `module.exports = ` names, + * or null when the module is a table. */ +export function wholeExportSymbolOfModule(lowerer: Lowerer, dep: ts.SourceFile): ts.Symbol | null { + const cached = lowerer.cjsWholeExportSymbols.get(dep); + if (cached !== undefined) return cached; + if (cjsNamespaceModule(lowerer, dep)) { + lowerer.cjsWholeExportSymbols.set(dep, null); + return null; + } + const moduleSymbol = lowerer.checker.getSymbolAtLocation(dep); + let symbol = moduleSymbol?.getExports()?.get("export=" as ts.__String); + if (symbol === undefined) { + lowerer.cjsWholeExportSymbols.set(dep, null); + return null; + } + if (symbol.flags & ts.SymbolFlags.Alias) symbol = lowerer.checker.getAliasedSymbol(symbol); + lowerer.cjsWholeExportSymbols.set(dep, symbol); + return symbol; +} + +function splitBindingOfSymbol(lowerer: Lowerer, symbol: ts.Symbol): SplitRequireBinding | null { + const decl = symbol.declarations.find(ts.isVariableDeclaration); + return decl === undefined ? null : splitRequireBindingOf(lowerer, decl); +} + +/** The target whole-export symbol behind a split-binding symbol. */ +function splitWholeExportSymbolOfSymbol(lowerer: Lowerer, symbol: ts.Symbol): ts.Symbol | null { + const binding = splitBindingOfSymbol(lowerer, symbol); + return binding === null ? null : wholeExportSymbolOfModule(lowerer, binding.dep); +} + +/** True when a CommonJS module has a live member export (`exports.x` or + * `module.exports.x`) after all `module.exports =` replacements. Those + * members need a mutable whole-export root. */ +export function cjsHasLiveMemberExports(lowerer: Lowerer, sf: ts.SourceFile): boolean { + const cached = lowerer.cjsLiveMemberExports.get(sf); + if (cached !== undefined) return cached; + const stmts = sf.statements.nodes; + let lastTable = -1; + for (let i = 0; i < stmts.length; i++) { + if (cjsExportAssignmentOf(stmts[i]!)?.kind === "table") lastTable = i; + } + let live = false; + // Only members attached after the last replacement survive in Node. + if (lastTable >= 0) { + for (let i = lastTable + 1; i < stmts.length; i++) { + const stmt = stmts[i]!; + if (cjsExportAssignmentOf(stmt)?.kind === "member" && cjsExportDiscardReason(stmt) === null) { + live = true; + break; + } + } + } + lowerer.cjsLiveMemberExports.set(sf, live); + return live; +} + +/** The wrapper's export= symbol when a split binding is re-exported whole + * (`module.exports = m`) and that assignment owns storage for later member + * writes. Importers must read the wrapper's storage, not the split + * binding's underlying value. */ +function splitWholeExportStorageSymbolOf(lowerer: Lowerer, symbol: ts.Symbol): ts.Symbol | null { + const binding = splitBindingOfSymbol(lowerer, symbol); + if (binding === null) return null; + if (wholeExportSymbolOfModule(lowerer, binding.dep) === null) return null; + const sf = ts.getSourceFileOfNode(binding.decl); + if (!cjsHasLiveMemberExports(lowerer, sf)) return null; + const whole = cjsWholeExportValueOf(sf); + if (whole === null) return null; + let rhs: ts.Expression = whole.rhs; + while (ts.isParenthesizedExpression(rhs)) rhs = rhs.expression; + if (!ts.isIdentifier(rhs) || lowerer.checker.getSymbolAtLocation(rhs) !== symbol) return null; + const moduleSymbol = lowerer.checker.getSymbolAtLocation(sf); + let exportSymbol = moduleSymbol?.getExports()?.get("export=" as ts.__String); + if (exportSymbol === undefined) return null; + if (exportSymbol.flags & ts.SymbolFlags.Alias) + exportSymbol = lowerer.checker.getAliasedSymbol(exportSymbol); + return exportSymbol; +} + +/** The source file whose `module.exports =` assignment declares `symbol`. */ +function moduleExportSourceFileOf(symbol: ts.Symbol): ts.SourceFile | null { + for (const decl of symbol.declarations) { + if ( + ts.isBinaryExpression(decl) && + isModuleExportsAccess(decl.left) && + ts.isExpressionStatement(decl.parent) && + ts.isSourceFile(decl.parent.parent) + ) + return decl.getSourceFile(); + } + return null; +} + +/** One hop from a wrapper's export= symbol to the value it re-exports + * (`module.exports = require(...)` or `= `), or null. A + * wrapper with live members owns storage for them, so it is not followed. */ +function wholeReexportTargetOf(lowerer: Lowerer, symbol: ts.Symbol): ts.Symbol | null { + const sf = moduleExportSourceFileOf(symbol); + if (sf === null) return null; + // A wrapper with live members owns storage for them; importers must keep + // reading that root rather than the inner value. + if (cjsHasLiveMemberExports(lowerer, sf)) return null; + const whole = cjsWholeExportValueOf(sf); + if (whole === null) return null; + const inner = inlineProgramRequireOf(lowerer, whole.rhs); + if (inner !== null) return wholeExportSymbolOfModule(lowerer, inner.dep); + let rhs: ts.Expression = whole.rhs; + while (ts.isParenthesizedExpression(rhs)) rhs = rhs.expression; + if (!ts.isIdentifier(rhs)) return null; + const rhsSymbol = lowerer.checker.getSymbolAtLocation(rhs); + return rhsSymbol === undefined ? null : splitWholeExportSymbolOfSymbol(lowerer, rhsSymbol); +} + +/** Follow split bindings and whole re-exports to the ultimate value symbol. */ +function followSplitWholeTarget(lowerer: Lowerer, candidate: ts.Symbol | null): ts.Symbol | null { + if (candidate === null) return null; + let symbol = candidate; + const seen = new Set(); + for (let depth = 0; depth < 32 && !seen.has(symbol); depth++) { + seen.add(symbol); + const next = + splitWholeExportSymbolOfSymbol(lowerer, symbol) ?? wholeReexportTargetOf(lowerer, symbol); + if (next === null) break; + symbol = next; + } + return symbol; +} + +/** The target whole-export symbol behind `module.exports = `, + * letting importers of that wrapper resolve to the real value symbol. */ +export function splitWholeExportAliasTargetOf( + lowerer: Lowerer, + symbol: ts.Symbol, +): ts.Symbol | null { + const storage = splitWholeExportStorageSymbolOf(lowerer, symbol); + if (storage !== null) return followSplitWholeTarget(lowerer, storage); + const direct = followSplitWholeTarget(lowerer, splitWholeExportSymbolOfSymbol(lowerer, symbol)); + if (direct !== null) return direct; + return followSplitWholeTarget(lowerer, wholeReexportTargetOf(lowerer, symbol)); +} + +/** The first read of a split binding that code before its assignment can + * reach, using the same reachability model as ordinary require bindings. */ +export function splitRequireEarlyReadOf( + lowerer: Lowerer, + binding: SplitRequireBinding, +): ts.Identifier | null { + const sf = ts.getSourceFileOfNode(binding.decl); + const stmt = binding.assign.parent; + // `binding` was recognized only from a top-level `m = require(...)` + // statement, so the statement index is always present. + const index = sf.statements.nodes.indexOf(stmt as ts.ExpressionStatement); + return requireBindingEarlyUse7(lowerer.program, sf, index, binding.decl, []); +} + /** A `require("")` call of the CommonJS global in a CommonJS JS * file whose target is a compiled program module (relative, aliased, or an * opted-in --npm-static package). Builtins, JSON documents and @@ -108,13 +436,6 @@ export function inlineNamespaceRequireOf( /** `value` evaluated after the required module's run-once %init (and its * module-registry link) at the require's own position. */ export function withRequireInit(lowerer: Lowerer, req: InlineRequire, value: IrExpr): IrExpr { - if (lowerer.inlineRequireCycleCalls.has(req.call)) { - lowerer.unsupported( - "SC1090", - req.call, - `require() of '${req.spec}' while it is still evaluating (a require cycle: Node answers its partial exports, which the static export table does not model)`, - ); - } // Every inline program require is a preflight module edge, so its module // has an initializer; never read its exports without evaluating it. if (!lowerer.initNameOf.has(req.dep)) { diff --git a/packages/compiler/src/frontend/lowering/class-attributes.ts b/packages/compiler/src/frontend/lowering/class-attributes.ts index 4e7bbe801..cdd53efa6 100644 --- a/packages/compiler/src/frontend/lowering/class-attributes.ts +++ b/packages/compiler/src/frontend/lowering/class-attributes.ts @@ -39,8 +39,14 @@ import { runtimeRooted } from "./class-descriptors.js"; type ClassObjectType = Extract; function attributeClass(lowerer: Lowerer, target: IrExpr): ClassInfo | null { - if (!isDynTypedRefType(target.type)) return null; - const info = lowerer.classes.get(target.type.className); + return isDynTypedRefType(target.type) + ? attributeClassNamed(lowerer, target.type.className) + : null; +} + +/** The compiled class `className`, when its instances can carry attributes. */ +function attributeClassNamed(lowerer: Lowerer, className: string): ClassInfo | null { + const info = lowerer.classes.get(className); if (!info || runtimeRooted(info) || info.decl == null) return null; return info; } @@ -62,6 +68,20 @@ function addHiddenField(info: ClassInfo, name: string): void { add(root); } +/** In a program that freezes, seals or prevents extensions of untyped + * values, gives every hierarchy whose instances can be held there the + * integrity word. The runtime applies the operation to the instance's + * checked view, whose commit stores the view's integrity state in the word + * (backend streamTypedRefCommitAdapter), and the write checks below read + * it. */ +export function addUntypedIntegrityWords(lowerer: Lowerer): void { + if (!lowerer.untypedIntegrity) return; + for (const [className, info] of lowerer.classes) { + if (isDynTypedRefType({ kind: "object", className }) && attributeClassNamed(lowerer, className)) + addHiddenField(info, CLASS_ATTRIBUTES); + } +} + function hiddenFieldGet(receiver: IrExpr, className: string, field: string, loc: SrcLoc): IrExpr { return { kind: "fieldGet", obj: receiver, className, field, type: F64, loc }; } @@ -283,6 +303,32 @@ export function lowerClassFieldDefine( }; } +/** The property key of `field` when it is the storage of a symbol-keyed + * field of `info`, as the read-only TypeError names it: the symbol's + * String() form ("Symbol(tag)"), read from the module global that holds + * the symbol. Null for other fields. */ +function symbolFieldKey( + lowerer: Lowerer, + info: ClassInfo, + field: string, + loc: SrcLoc, +): IrExpr | null { + if (info.symbolFields === undefined) return null; + for (const [symbol, name] of info.symbolFields) { + if (name !== field) continue; + const global = lowerer.globalsBySymbol.get(symbol); + if (global?.type.kind !== "symbol") return null; + return { + kind: "libCall", + fn: "sym.toString", + args: [varRef(global.id, global.type, loc)], + type: STRING, + loc, + }; + } + return null; +} + function classDisplayName(info: ClassInfo): string { return info.def.jsName ?? info.decl?.name?.text ?? info.def.name.replace(/^%/, ""); } @@ -347,12 +393,15 @@ export function guardClassAttributeWrites(lowerer: Lowerer, functions: IrFunctio if ( statement.kind !== "fieldSet" || statement.attributesChecked || - !guarded.has(statement.className) || - !isClassOwnEnumerableFieldName(statement.field) + !guarded.has(statement.className) ) return statement; const info = lowerer.classes.get(statement.className)!; const loc = statement.loc; + // A symbol-keyed field is an own property under its symbol. + const symbolKey = symbolFieldKey(lowerer, info, statement.field, loc); + if (!symbolKey && !isClassOwnEnumerableFieldName(statement.field)) return statement; + const key = (): IrExpr => symbolKey ?? strLit(statement.field, loc); const receiver = local(statement.obj.type, loc); const value = local(statement.value.type, loc); const masked = (field: string, bit: number): IrExpr => ({ @@ -385,7 +434,7 @@ export function guardClassAttributeWrites(lowerer: Lowerer, functions: IrFunctio expr: { kind: "libCall", fn: "class.throwNotExtensible", - args: [strLit(statement.field, loc)], + args: [key()], type: VOID, loc, }, @@ -400,10 +449,7 @@ export function guardClassAttributeWrites(lowerer: Lowerer, functions: IrFunctio expr: { kind: "libCall", fn: "class.throwReadOnly", - args: [ - strLit(statement.field, loc), - instanceClassName(lowerer, info, receiver, helpers, loc), - ], + args: [key(), instanceClassName(lowerer, info, receiver, helpers, loc)], type: VOID, loc, }, diff --git a/packages/compiler/src/frontend/lowering/class-callbacks.ts b/packages/compiler/src/frontend/lowering/class-callbacks.ts index 0bda0068e..3a1c7975d 100644 --- a/packages/compiler/src/frontend/lowering/class-callbacks.ts +++ b/packages/compiler/src/frontend/lowering/class-callbacks.ts @@ -156,6 +156,13 @@ export function classCallbackValue( }; } +/** Notes a checked conversion of a replaceable method's result for the + * key-order witness: the result is a value the method or its replacement + * returns, not a record built from a dynamic value. */ +function methodResult(lowerer: Lowerer, checked: IrExpr, name: string): IrExpr { + return checked.kind === "dynCheck" ? lowerer.keyOrder.methodResult(checked, name) : checked; +} + export function classCallbackCall( lowerer: Lowerer, receiver: IrExpr, @@ -197,7 +204,7 @@ export function classCallbackCall( // generator arm does not box) keeps its native type: the unchanged // method's result stays as it is, and a replacement's dynamic result // takes the checked conversion into that type. - const converted = lowerer.coerceToExpected(call, fallback.type); + const converted = methodResult(lowerer, lowerer.coerceToExpected(call, fallback.type), name); if (!typeEquals(converted.type, fallback.type)) { lowerer.pushDiag( unsupportedDiag( @@ -234,7 +241,9 @@ export function classCallbackCall( loc, }; answer = - fallback.type.kind === "void" ? result : lowerer.coerceToExpected(result, fallback.type); + fallback.type.kind === "void" + ? result + : methodResult(lowerer, lowerer.coerceToExpected(result, fallback.type), name); } // Read after observedSlot built the prototype data and its originals. const originalId = observed ? original?.originalId() : undefined; diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts index ab3ebb34e..1c6df3d5c 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts @@ -1,4 +1,5 @@ import { primitiveConstructorOr } from "./lower-exprs.js"; +import { TO_STRING_TAG_FIELD, wellKnownSymbolOfField } from "./symbol-fields.js"; import { InternalCompilerError } from "../../errors.js"; import { dynUndefinedExpr, helperLoc, nodeThrowExpr, varRef } from "../../ir/build.js"; import { constructedValue } from "./constructor-returns.js"; @@ -26,6 +27,7 @@ import { isUnitType, typeEquals, typeKey, + type IrClassDef, type IrExpr, type IrFunction, type IrParam, @@ -52,6 +54,7 @@ import { import { classPrototypeData, hasClassPrototypeData, + prototypeReflectedFlag, observedPrototypeMethod, reflectClassPrototype, } from "./class-prototypes.js"; @@ -78,6 +81,7 @@ import { isClassCallback } from "./class-callbacks.js"; import { CaughtValueDispatch } from "./caught-value-dispatch.js"; import { refreshDescriptorGuards } from "./class-descriptors.js"; import { ClassConstructionDispatch } from "./class-construction.js"; +import { keyedHelperName } from "./stable-names.js"; import { DispatchSites } from "./dispatch-sites.js"; import { emitterRooted, @@ -153,6 +157,41 @@ const STREAM_NUM_PROPERTIES = new Set([ const streamProperty = (name: string): boolean => STREAM_BOOL_PROPERTIES.has(name) || STREAM_NUM_PROPERTIES.has(name); +/** `(instance) => string`, the string a class's inherited + * Symbol.toStringTag getter answers, which the runtime reads for + * Object.prototype.toString and util.inspect of an instance seen as + * `unknown`. None for a class without a string getter, or whose own tag + * field shadows it. */ +function stringTagHelper(lowerer: Lowerer, info: ClassInfo, loc: SrcLoc): IrFunction | null { + if (info.fields.has(TO_STRING_TAG_FIELD)) return null; + const getter = findMethodOn(lowerer, info, "get:sym:toStringTag"); + if (getter?.sig.ret.kind !== "string") return null; + const type: IrType = { kind: "object", className: info.def.name }; + const owner = getter.declarer.def.name; + return { + name: `%class.stringTag:${info.def.name}`, + params: [{ localId: "this", name: "this", type }], + locals: [{ id: "this", name: "this", type, mutable: false }], + returnType: STRING, + body: [ + { + kind: "return", + value: accessorCall( + lowerer, + owner, + "get:sym:toStringTag", + lowerer.upcastTo(varRef("this", type, loc), owner), + [], + STRING, + loc, + ), + loc, + }, + ], + loc, + }; +} + function symbolMemberKey( lowerer: Lowerer, info: ClassInfo, @@ -225,6 +264,7 @@ const KEY_CALLS = new Map([ ["dyn.propertyIsEnumerableComputed", "any"], ["dyn.hasOwnComputed", "value"], ["dyn.keyDeleteComputed", "value"], + ["dyn.reflectDeleteProperty", "object"], ["dyn.keySetComputed", "value"], ]); @@ -282,6 +322,11 @@ interface PropertyDispatch { fn: IrFunction; classes: Set; reflect?: boolean; + /** The helper's fallthrough statements as created, and the same access + * applied to the receiver itself: what the fallthrough reduces to for a + * value that is neither a class capsule nor a function, whose bag is the + * value. */ + plain?: { tail: readonly IrStmt[]; access: IrStmt[] }; } export function classInstanceOf( @@ -712,6 +757,105 @@ export function classForInHasKey( return { kind: "call", callee: name, args: [value, key], type: BOOL, loc }; } +/** Box class instances at their exact class. `%dyn.class.normalize` turns a + * capsule of a class with subclasses into a capsule of the instance's own + * class before any member table reads it, so every such box was followed + * by a second capsule. Each conversion of a value whose static class has a + * normalization branch now selects the exact class at the conversion, with + * the same tests in the same order (%dyn.class.boxExact:C), and the + * normalization of the resulting capsule falls through. Only JavaScript + * class hierarchies, whose instances live in untyped code, take the exact + * box. */ +export function boxClassInstancesExactly(lowerer: Lowerer, functions: IrFunction[]): void { + const normalize = lowerer.liftedFnNamed("%dyn.class.normalize"); + if (!normalize) return; + // Static class → its constructible descendants in normalization order. + const branches = new Map(); + for (const stmt of normalize.body) { + if (stmt.kind !== "if" || stmt.cond.kind !== "libCall" || stmt.cond.fn !== "dyn.typedRefIs") + continue; + const key = stmt.cond.args[1]; + if (key?.kind !== "strLit" || !key.value.startsWith("object:")) continue; + const children: string[] = []; + for (const inner of stmt.then) { + if (inner.kind !== "if" || inner.cond.kind !== "instanceOf") continue; + children.push(inner.cond.className); + } + // TypeScript hierarchies keep their static-class capsules: their boxes + // are rarer, and an exact box would emit each subclass's checked view. + const className = key.value.slice("object:".length); + const decl = lowerer.classes.get(className)?.decl; + if (!decl || !isJsSourceFile(ts.getSourceFileOfNode(decl))) continue; + if (children.length) branches.set(className, children); + } + if (branches.size === 0) return; + const loc = normalize.loc; + const helpers = new Map(); + const helperFor = (className: string): string => { + const known = helpers.get(className); + if (known !== undefined) return known; + const name = `%dyn.class.boxExact:${className}`; + helpers.set(className, name); + const type: IrType = { kind: "object", className }; + const value = varRef("value", type, loc); + functions.push({ + name, + params: [{ localId: "value", name: "value", type }], + returnType: DYN, + locals: [{ id: "value", name: "value", type, mutable: false }], + body: [ + ...branches.get(className)!.map((child): IrStmt => ({ + kind: "if", + cond: { kind: "instanceOf", value, className: child, type: BOOL, loc }, + then: [ + { + kind: "return", + value: { + kind: "dynFrom", + value: { + kind: "downcast", + value, + type: { kind: "object", className: child }, + loc, + }, + type: DYN, + loc, + }, + loc, + }, + ], + else_: null, + loc, + })), + { kind: "return", value: { kind: "dynFrom", value, type: DYN, loc }, loc }, + ], + loc, + }); + return name; + }; + const rewrite = (fn: IrFunction): void => { + fn.body = transformStmtList(fn.body, { + stmt: (statement) => statement, + expr: (expression) => + expression.kind === "dynFrom" && + expression.value.type.kind === "object" && + branches.has(expression.value.type.className) + ? { + kind: "call", + callee: helperFor(expression.value.type.className), + args: [expression.value], + type: DYN, + loc: expression.loc, + } + : expression, + }); + }; + for (const fn of [...functions]) { + if (fn === normalize || fn.name.startsWith("%dyn.class.boxExact:")) continue; + rewrite(fn); + } +} + /** Record field creation after the dispatch worklist closes, including writes * in generated member tables. Native values stay in their slots; the bag * owns only their property presence, attributes, and insertion position. */ @@ -1271,10 +1415,27 @@ export class ClassDynamicDispatch { const info = lowerer.classes.get(className); if (!info || info.builtinEmitter || info.builtinStream || info.builtinError) continue; if (info.symbolFields && info.def.symbolFields === undefined) { - info.def.symbolFields = [...info.symbolFields].flatMap(([symbol, field]) => { - const global = lowerer.globalsBySymbol.get(symbol); - return global?.type.kind === "symbol" ? [{ field, globalId: global.id }] : []; - }); + info.def.symbolFields = [...info.symbolFields].flatMap( + ([symbol, field]): NonNullable => { + const global = lowerer.globalsBySymbol.get(symbol); + if (global?.type.kind === "symbol") return [{ field, globalId: global.id }]; + const wellKnown = wellKnownSymbolOfField(field); + return wellKnown === null ? [] : [{ field, wellKnown }]; + }, + ); + } + const tagged = (current: ClassInfo): ClassInfo[] => [ + current, + ...current.subclasses.flatMap(tagged), + ]; + for (const current of tagged(info)) { + if (current.def.stringTagHelper !== undefined) continue; + const fn = stringTagHelper(lowerer, current, functions[0]!.loc); + if (!fn) continue; + current.def.stringTagHelper = fn.name; + this.generated.add(fn); + lowerer.liftedFns.push(fn); + changed = true; } if (this.prototypeHelper && !this.prototypeClasses.has(className)) { this.prototypeClasses.add(className); @@ -1629,6 +1790,15 @@ export class ClassDynamicDispatch { if (this.deletedNames === undefined) this.deletedNames = deletedPropertyNames(lowerer.moduleOrder); if (!root.base?.def.runtime && !fixedFieldFamily(root, this.deletedNames)) track(root); + // An Error family's bag records where `name` and `message` were + // created, which the error slots do not. + if (root.base?.def.name === "%Error") { + const order = (current: ClassInfo): void => { + current.def.errorBagOrder = true; + current.subclasses.forEach(order); + }; + order(info); + } } const loc = this.propertyBag.loc; const { receiver, condition } = this.propertyReceiver(plan, loc); @@ -2724,7 +2894,28 @@ export class ClassDynamicDispatch { loc, }); } - dispatch = { name, write: !!write, fn: helper, classes: new Set() }; + const receiverAccess: IrStmt[] = read + ? [{ kind: "return", value: { ...read, value: varRef("p.0", DYN, loc) }, loc }] + : [ + { + kind: "exprStmt", + expr: { + ...write!, + args: [varRef("p.0", DYN, loc), write!.args[1]!, varRef("p.1", DYN, loc)], + }, + loc, + }, + { kind: "return", value: null, loc }, + ]; + dispatch = { + name, + write: !!write, + fn: helper, + classes: new Set(), + ...(name !== "constructor" && name !== "toString" + ? { plain: { tail: [...helper.body], access: receiverAccess } } + : {}), + }; this.properties.set(key, dispatch); this.generated.add(helper); lowerer.liftedFns.push(helper); @@ -2740,15 +2931,19 @@ export class ClassDynamicDispatch { } const iterator = expr.kind === "libCall" && - (expr.fn === "dyn.iterator" || expr.fn === "dyn.arrayFromIterator") + (expr.fn === "dyn.iterator" || + expr.fn === "dyn.arrayFromIterator" || + expr.fn === "dyn.asyncIteratorOpen") ? expr : null; + // `for await` opens a class instance's [Symbol.asyncIterator](). + const asyncOpen = iterator?.fn === "dyn.asyncIteratorOpen"; const invoke: Invoke | null = iterator ? { kind: "dynInvoke", recv: iterator.args[0]!, - method: "sym:iterator", - calleeName: "value[Symbol.iterator]", + method: asyncOpen ? "sym:asyncIterator" : "sym:iterator", + calleeName: asyncOpen ? "value[Symbol.asyncIterator]" : "value[Symbol.iterator]", args: [], type: DYN, loc: iterator.loc, @@ -3528,10 +3723,29 @@ export class ClassDynamicDispatch { name, lowerer.coerceToExpected(varRef(global.id, global.type, loc), DYN), ); + } else if (wellKnownSymbolOfField(name) !== null) { + // A field keyed by a well-known symbol (Symbol.toStringTag). + symbols.set(name, { + kind: "dynFrom", + type: DYN, + loc, + value: { + kind: "libCall", + fn: "sym.wellKnown", + args: [ + { kind: "strLit", value: wellKnownSymbolOfField(name)!, type: STRING, loc }, + ], + type: SYMBOL_T, + loc, + }, + }); } } for (let owner: ClassInfo | null = info; owner; owner = owner.base) { - for (const name of [...owner.methods.keys()]) + for (const method of [...owner.methods.keys()]) { + // Accessors key by the property they define (`get [Symbol.toStringTag]`). + const name = + method.startsWith("get:") || method.startsWith("set:") ? method.slice(4) : method; if (name.startsWith("sym:")) { symbols.set(name, { kind: "dynFrom", @@ -3546,6 +3760,7 @@ export class ClassDynamicDispatch { }, }); } + } } const branches: IrStmt[] = []; if (!dispatch.write && !dispatch.reflect && !info.def.tracksOwnFields) { @@ -3701,7 +3916,7 @@ export class ClassDynamicDispatch { if (!dispatch.fn.params.some((param) => param.localId === dispatch.keyLocal)) params.push({ localId: dispatch.keyLocal, name: "key", type: STRING }); const helper: IrFunction = { - name: `%dyn.class.keys.${lowerer.liftedFns.length}`, + name: keyedHelperName(lowerer, "%dyn.class.keys", helperKey), params, returnType: dispatch.fn.returnType, locals: params.map((p) => ({ @@ -3971,6 +4186,103 @@ export class ClassDynamicDispatch { return { kind: "call", callee: name, args: [bag, key], type: BOOL, loc }; } + /** `%dyn.class.bagHasOwn`: whether an instance's property bag holds `key` + * as its own property. False before the instance has a bag. */ + private readBagHasOwn(lowerer: Lowerer, bag: IrExpr, key: IrExpr, loc: SrcLoc): IrExpr { + const name = "%dyn.class.bagHasOwn"; + let helper = lowerer.liftedFnNamed(name); + if (!helper) { + const value = varRef("p.0", DYN, loc); + helper = { + name, + params: [ + { localId: "p.0", name: "bag", type: DYN }, + { localId: "p.1", name: "key", type: STRING }, + ], + returnType: BOOL, + locals: [ + { id: "p.0", name: "bag", type: DYN, mutable: false }, + { id: "p.1", name: "key", type: STRING, mutable: false }, + ], + body: [ + { + kind: "if", + cond: { kind: "dynTest", test: "undefined", value, type: BOOL, loc }, + then: [ + { kind: "return", value: { kind: "boolLit", value: false, type: BOOL, loc }, loc }, + ], + else_: null, + loc, + }, + { + kind: "return", + value: { + kind: "libCall", + fn: "dyn.hasOwn", + args: [value, varRef("p.1", STRING, loc)], + type: BOOL, + loc, + }, + loc, + }, + ], + loc, + }; + lowerer.liftedFns.push(helper); + } + this.generated.add(helper); + return { kind: "call", callee: name, args: [bag, key], type: BOOL, loc }; + } + + /** The `constructor` an instance of `info` inherits when it has no own + * one. The class's prototype view answers its own `constructor` (a + * replacement, or the class once reflection installed it). A view + * without one answers the class itself unless reflection had populated it + * (prototypeReflectedFlag, set when the view initializes): then the + * program deleted the property, and the read continues to the base + * class's prototype and past the last one to Object.prototype, as in + * Node. A prototype's own `constructor` is read with the instance + * `receiver` as `this`, so an accessor's getter runs on the instance. */ + private inheritedConstructor( + lowerer: Lowerer, + info: ClassInfo, + key: IrExpr, + receiver: IrExpr, + loc: SrcLoc, + ): IrExpr { + const cls = lowerer.coerceToExpected(classValueRef(lowerer, info, info.decl!), DYN); + const prototype = hasClassPrototypeData(info) ? classPrototypeData(lowerer, info, loc) : null; + if (!prototype) return cls; + const base = info.base; + const read = (): IrExpr => ({ + kind: "libCall", + fn: "dyn.reflectGet", + args: [prototype, lowerer.coerceToExpected(key, DYN), receiver], + type: DYN, + loc, + }); + // Past the last class prototype, the read continues to Object.prototype's data property. + const next: IrExpr = + base && !base.builtinError && base.decl && !base.generic && !base.localClass + ? this.inheritedConstructor(lowerer, base, key, receiver, loc) + : { kind: "dynKeyGet", value: prototype, key, type: DYN, loc }; + return { + kind: "ternary", + cond: { kind: "libCall", fn: "dyn.hasOwn", args: [prototype, key], type: BOOL, loc }, + then: read(), + else_: { + kind: "ternary", + cond: varRef(prototypeReflectedFlag(lowerer, info), BOOL, loc), + then: next, + else_: cls, + type: DYN, + loc, + }, + type: DYN, + loc, + }; + } + /** The hints for which some boxed class declares a conversion method. A * string conversion calls toString first, so without a declared toString * or Symbol.toPrimitive it answers Object.prototype.toString's @@ -4896,6 +5208,7 @@ export class ClassDynamicDispatch { ); } if (lowerer) this.guardClassTables(lowerer); + if (lowerer) this.guardPropertyHelpers(lowerer); } /** Each tracked class's bag initializer over a typed receiver (`this`), @@ -4985,6 +5298,87 @@ export class ClassDynamicDispatch { } } + /** Fast exits for the generated property helpers: when every class + * branch tests a capsule key or a class key and the fallthrough is still + * the bag access the helper was created with, a value of another kind + * (a plain object, array or primitive) normalizes to itself, has itself + * as its bag, and so gets the plain access directly, without the + * normalization, bag and static-bag calls. */ + private guardPropertyHelpers(lowerer: Lowerer): void { + // Every class table the fallthrough or normalization calls must answer + // a plain value with the value itself: its kind exit is in place. + const exits = (name: string): boolean => { + const helper = lowerer.liftedFnNamed(name); + const first = helper?.body[0]; + const param = helper?.params[0]?.localId; + return ( + param !== undefined && + first?.kind === "if" && + first.cond.kind === "dynTest" && + first.cond.negated === true && + first.then.length === 1 && + first.then[0]!.kind === "return" && + first.then[0]!.value !== null && + returnsParam(first.then[0]!.value, param) + ); + }; + const tables = new Set([ + "%dyn.class.properties", + READ_PROPERTIES, + READ_PROPERTIES_OR_EMPTY, + "%dyn.class.normalize", + "%dyn.class.staticProperties", + ]); + const callsOnlyGuardedTables = (statements: IrStmt[]): boolean => + everyStmtList(statements, { + stmt: () => true, + expr: (expression) => + expression.kind !== "call" || (tables.has(expression.callee) && exits(expression.callee)), + }); + for (const dispatch of this.properties.values()) { + const plain = dispatch.plain; + if (!plain) continue; + const fn = dispatch.fn; + const param = fn.params[0]?.localId; + if (param === undefined) continue; + const head = fn.body.length - plain.tail.length; + if (head < 0 || plain.tail.some((stmt, i) => fn.body[head + i] !== stmt)) continue; + let ok = true; + for (const stmt of fn.body.slice(0, head)) { + if ( + stmt.kind === "assign" && + stmt.localId === param && + stmt.value.kind === "call" && + stmt.value.callee === "%dyn.class.normalize" + ) + continue; + if (stmt.kind === "if" && stmt.else_ === null && branchKind(stmt.cond, param) !== null) + continue; + ok = false; + break; + } + // Class branches never run for a plain value; the rest always does. + if (!ok || !callsOnlyGuardedTables(fn.body.filter((stmt) => stmt.kind !== "if"))) continue; + const loc = fn.loc; + const not = (test: "classCapsule" | "funcKind"): IrExpr => ({ + kind: "dynTest", + test, + negated: true, + value: varRef(param, DYN, loc), + type: BOOL, + loc, + }); + fn.body.unshift({ + kind: "if", + cond: not("classCapsule"), + then: [{ kind: "if", cond: not("funcKind"), then: plain.access, else_: null, loc }], + else_: null, + loc, + }); + delete dispatch.plain; + } + } + private normalizedDispatches(): IrFunction[] { return [ ...(this.propertyBag ? [this.propertyBag] : []), @@ -5647,15 +6041,19 @@ export class ClassDynamicDispatch { !info.classDecorators ) { this.constructors.set(info.def.name, info); - const cls = lowerer.coerceToExpected(classValueRef(lowerer, info, info.decl), DYN); const bag = this.branchPropertyBag(lowerer, info, receiver, false, loc); const key: IrExpr = { kind: "strLit", value: name, type: STRING, loc }; + // `constructor` is an own property of every class prototype, so an + // ancestor's prototype answers for an instance of this class only + // after the program deletes the class's own: the instance's own + // property answers, then the class's prototype (inheritedConstructor). + const answered = this.readBagHasOwn(lowerer, bag, key, loc); return [ { kind: "return", value: { kind: "ternary", - cond: this.readBagHas(lowerer, "dyn.hasKey", bag, key, loc), + cond: answered, then: { kind: "libCall", fn: "dyn.bagGet", @@ -5663,7 +6061,13 @@ export class ClassDynamicDispatch { type: DYN, loc, }, - else_: cls, + else_: this.inheritedConstructor( + lowerer, + info, + key, + lowerer.coerceToExpected(receiver, DYN), + loc, + ), type: DYN, loc, }, @@ -6390,24 +6794,41 @@ export class ClassDynamicDispatch { type: result, loc, }; + // A void method dispatches its overrides with statements: a ternary + // has no void form. + let voidDispatch: IrStmt[] = [{ kind: "exprStmt", expr: call, loc }]; for (const override of overrides.toReversed()) { const subType: IrType = { kind: "object", className: override.cls.def.name }; + const isOverride: IrExpr = { + kind: "instanceOf", + value: receiver, + className: override.cls.def.name, + type: BOOL, + loc, + }; + const overrideCall: IrExpr = { + kind: "call", + callee: override.callee, + args: [{ kind: "downcast", value: receiver, type: subType, loc }, ...args], + type: result, + loc, + }; + if (result.kind === "void") { + voidDispatch = [ + { + kind: "if", + cond: isOverride, + then: [{ kind: "exprStmt", expr: overrideCall, loc }], + else_: voidDispatch, + loc, + }, + ]; + continue; + } call = { kind: "ternary", - cond: { - kind: "instanceOf", - value: receiver, - className: override.cls.def.name, - type: BOOL, - loc, - }, - then: { - kind: "call", - callee: override.callee, - args: [{ kind: "downcast", value: receiver, type: subType, loc }, ...args], - type: result, - loc, - }, + cond: isOverride, + then: overrideCall, else_: call, type: result, loc, @@ -6415,10 +6836,7 @@ export class ClassDynamicDispatch { } let body: IrStmt[]; if (result.kind === "void") - body = [ - { kind: "exprStmt", expr: call, loc }, - { kind: "return", value: dynUndefinedExpr(loc), loc }, - ]; + body = [...voidDispatch, { kind: "return", value: dynUndefinedExpr(loc), loc }]; else { const boxed = lowerer.coerceToExpected(call, DYN); if (boxed.type.kind !== "dyn") return fence(); diff --git a/packages/compiler/src/frontend/lowering/class-method-mutations.ts b/packages/compiler/src/frontend/lowering/class-method-mutations.ts index fd23b6db8..8f12d15de 100644 --- a/packages/compiler/src/frontend/lowering/class-method-mutations.ts +++ b/packages/compiler/src/frontend/lowering/class-method-mutations.ts @@ -36,9 +36,20 @@ export function collectClassMethodMutations( const computedWrites: { key: ts.Expression | undefined; receiver: ts.Expression | undefined; + /** The written element access, when the write has one. */ + target?: ts.ElementAccessExpression; site: ts.Node; }[] = []; const propertyWrites: { target: ts.PropertyAccessExpression; site: ts.Node }[] = []; + /** Writes and deletes of a method name through an element access with a + * known key (`o["m"] = f`, `o[k] = f` with `k: "m" | "n"`), judged like + * the named writes in propertyWrites. */ + const keyedWrites: { + name: string; + receiver: ts.Expression; + target: ts.ElementAccessExpression; + site: ts.Node; + }[] = []; const reflection: ts.PropertyAccessExpression[] = []; const globals: ts.Identifier[] = []; const jsAssignments: (ts.PropertyAccessExpression | ts.ElementAccessExpression)[] = []; @@ -46,6 +57,13 @@ export function collectClassMethodMutations( const typedClasses: (ts.ClassDeclaration | ts.ClassExpression)[] = []; const symbolMethods = new Set(); let unknownKey: ts.Node | undefined; + /** Every write with an unknown key: each may install a replacement of any + * method name, and the key-order witness asks what each one installs. */ + const unknownKeySites: ts.Node[] = []; + const markUnknownKey = (site: ts.Node): void => { + unknownKey = site; + unknownKeySites.push(site); + }; let numericKey: ts.Node | undefined; let unknownOwnField = false; // `sym:iterator` for `Symbol.iterator`: the name a class method declared @@ -83,8 +101,8 @@ export function collectClassMethodMutations( for (const key of computedMethods) for (const name of literalValues(lowerer.typeOf(key)) ?? []) methods.add(String(name)); const markName = (name: string, site: ts.Node, arrow = false): void => { - if (name === "prototype" || name === "__proto__") unknownKey = site; - if (methods.has(name)) lowerer.prototypeMethodAccesses.set(name, site); + if (name === "prototype" || name === "__proto__") markUnknownKey(site); + if (methods.has(name)) lowerer.noteMethodAccess(name, site); if (!arrow) receiverFields.add(name); }; const unwrap = (value: ts.Expression): ts.Expression => { @@ -125,11 +143,13 @@ export function collectClassMethodMutations( }; // Whether no function value can inhabit `type`. Primitives qualify, and so // do non-callable object types a function is not assignable to: one that - // requires a property functions lack, or a weak (all-optional) type that + // requires a property functions lack, a weak (all-optional) type that // shares no property with functions (TypeScript's weak-type rule rejects - // callable sources too). - const functionFree = (type: ts.Type): boolean => { - if (type.isUnionType()) return ts.constituentTypes(type).every(functionFree); + // callable sources too), or a dictionary with no properties whose index + // signatures hold function-free values (functions have no index + // signature). `seen` stops recursive dictionary types. + const functionFree = (type: ts.Type, seen: Set = new Set()): boolean => { + if (type.isUnionType()) return ts.constituentTypes(type).every((t) => functionFree(t, seen)); if ( (type.flags & (ts.TypeFlags.StringLike | @@ -149,7 +169,12 @@ export function collectClassMethodMutations( ) return false; const properties = lowerer.checker.getPropertiesOfType(type); - if (properties.length === 0) return false; + if (properties.length === 0) { + if (seen.has(type)) return true; + seen.add(type); + const indexes = lowerer.checker.getIndexInfosOfType(type); + return indexes.length > 0 && indexes.every((index) => functionFree(index.type, seen)); + } const required = properties.filter( (property) => (property.flags & ts.SymbolFlags.Optional) === 0, ); @@ -163,14 +188,32 @@ export function collectClassMethodMutations( // A string index whose values cannot be functions excludes callable // method slots. Class instances have no implicit index signature, so such // a dictionary is only a class view when the class declares that index - // signature, and then every method must be assignable to its value type. - const functionFreeDictionary = (value: ts.Expression): boolean => - lowerer.checker - .getIndexInfosOfType(lowerer.typeOf(value)) + // signature, and then every method must be assignable to its value type, + // or when the dictionary keeps checked identity: it then holds the + // existing objects it receives, class instances included. + // A string-keyed dictionary that keeps checked identity holds the + // existing objects it receives, class instances included, so any write + // through it can replace a method. + const dictionaryView = (value: ts.Expression): boolean => { + const type = lowerer.typeOf(value); + return ( + lowerer.checker.getPropertiesOfType(type).length === 0 && + lowerer.checker + .getIndexInfosOfType(type) + .some((index) => (index.keyType.flags & ts.TypeFlags.StringLike) !== 0) && + lowerer.classImplementedProtocol(type) + ); + }; + const functionFreeDictionary = (value: ts.Expression): boolean => { + const type = lowerer.typeOf(value); + if (lowerer.classImplementedProtocol(type)) return false; + return lowerer.checker + .getIndexInfosOfType(type) .some( (index) => (index.keyType.flags & ts.TypeFlags.StringLike) !== 0 && functionFree(index.type), ); + }; const literalLoopKeys = (key: ts.Expression): (string | number)[] | null => { if (!ts.isIdentifier(key)) return null; const symbol = lowerer.checker.getSymbolAtLocation(key); @@ -242,13 +285,32 @@ export function collectClassMethodMutations( sameBinding(receiver, keys.arguments.nodes[0]!) ); }; - const markKey = (key: ts.Expression, site: ts.Node, receiver?: ts.Expression): void => { + /** A write of the known key `name`: through an element access, a write + * to a method name is judged like a named write (keyedWrites). */ + const markKeyed = ( + name: string, + site: ts.Node, + receiver?: ts.Expression, + target?: ts.ElementAccessExpression, + ): void => { + if (receiver && target && methods.has(name) && name !== "prototype" && name !== "__proto__") { + receiverFields.add(name); + keyedWrites.push({ name, receiver, target, site }); + } else markName(name, site); + }; + const markKey = ( + key: ts.Expression, + site: ts.Node, + receiver?: ts.Expression, + target?: ts.ElementAccessExpression, + ): void => { const value = unwrap(key); - if (ts.isStringLiteralLike(value)) markName(value.text, site); - else if (ts.isNumericLiteral(value)) markName(String(Number(value.text)), site); + if (ts.isStringLiteralLike(value)) markKeyed(value.text, site, receiver, target); + else if (ts.isNumericLiteral(value)) + markKeyed(String(Number(value.text)), site, receiver, target); // The key keeps its assertions: `slots[at as number]` and `slots[at!]` // write a number key whatever the type of `at`. - else computedWrites.push({ key, receiver, site }); + else computedWrites.push(target ? { key, receiver, target, site } : { key, receiver, site }); }; const markFields = (value: ts.Expression, site: ts.Node, receiver?: ts.Expression): void => { value = unwrap(value); @@ -275,7 +337,7 @@ export function collectClassMethodMutations( propertyWrites.push({ target, site }); } else markName(name, site, arrow); } else if (ts.isElementAccessExpression(target)) - markKey(target.argumentExpression, site, target.expression); + markKey(target.argumentExpression, site, target.expression, target); else if (ts.isArrayLiteralExpression(target)) { for (const element of target.elements.nodes) { if (ts.isSpreadElement(element)) markTarget(element.expression, site); @@ -394,11 +456,22 @@ export function collectClassMethodMutations( (ts.isElementAccessExpression(base) && ts.isStringLiteralLike(base.argumentExpression) && base.argumentExpression.text === "prototype"); - if (prototype) { - if (ts.isPropertyAccessExpression(node)) - lowerer.prototypeMethodAccesses.set(node.name.text, node); + // `Object.prototype.toString.call(value)` calls the builtin and + // keeps no reference to it, so it replaces nothing. + const builtinCall = + ts.isPropertyAccessExpression(node) && + ts.isPropertyAccessExpression(base) && + ts.isIdentifier(unwrap(base.expression)) && + lowerer.isStdlibGlobal(unwrap(base.expression) as ts.Identifier, "Object") && + ts.isPropertyAccessExpression(node.parent) && + node.parent.expression === node && + (node.parent.name.text === "call" || node.parent.name.text === "apply") && + ts.isCallExpression(node.parent.parent) && + node.parent.parent.expression === node.parent; + if (prototype && !builtinCall) { + if (ts.isPropertyAccessExpression(node)) lowerer.noteMethodAccess(node.name.text, node); else if (ts.isStringLiteralLike(node.argumentExpression)) - lowerer.prototypeMethodAccesses.set(node.argumentExpression.text, node); + lowerer.noteMethodAccess(node.argumentExpression.text, node); else markKey(node.argumentExpression, node); } } @@ -435,7 +508,7 @@ export function collectClassMethodMutations( ? lowerer.checker.getSymbolAtLocation(access.name) : lowerer.checker.getPropertyOfType(lowerer.typeOf(access.expression), name); if (symbol && symbol.declarations.some(ts.isMethodDeclaration)) - lowerer.prototypeMethodAccesses.set(name, access); + lowerer.noteMethodAccess(name, access); } lowerer.checker.prefetchSymbolRoots([...globals, ...reflection]); for (const node of globals) @@ -443,11 +516,12 @@ export function collectClassMethodMutations( (lowerer.isStdlibGlobal(node, "Object") || lowerer.isStdlibGlobal(node, "Reflect")) && (!ts.isPropertyAccessExpression(node.parent) || node.parent.expression !== node) ) - unknownKey = node; + markUnknownKey(node); for (const node of reflection) { const objectMethod = lowerer.stdlibGlobalMember(node, "Object"); const reflectMethod = lowerer.stdlibGlobalMember(node, "Reflect"); - if (objectMethod === "setPrototypeOf" || reflectMethod === "setPrototypeOf") unknownKey = node; + if (objectMethod === "setPrototypeOf" || reflectMethod === "setPrototypeOf") + markUnknownKey(node); const single = objectMethod === "defineProperty" || reflectMethod === "defineProperty" || @@ -457,7 +531,7 @@ export function collectClassMethodMutations( if (!single && !multiple) continue; const call = node.parent; if (!ts.isCallExpression(call) || call.expression !== node) { - unknownKey = node; + markUnknownKey(node); continue; } const receiver = call.arguments.nodes[0]; @@ -571,11 +645,15 @@ export function collectClassMethodMutations( ); for (const { target, site } of propertyWrites) { const name = target.name.text; - if (lowerer.prototypeMethodAccesses.has(name) || functionFree(lowerer.typeOf(target))) continue; - if (!methodOwnerCanInhabit(target.expression, name)) continue; - lowerer.prototypeMethodAccesses.set(name, site); + if ( + !dictionaryView(target.expression) && + (functionFree(lowerer.typeOf(target)) || !methodOwnerCanInhabit(target.expression, name)) + ) + continue; + if (lowerer.prototypeMethodAccesses.has(name)) lowerer.noteMethodSite(name, site); + else lowerer.noteMethodAccess(name, site); } - for (const { key, receiver, site } of computedWrites) { + for (const { key, receiver, target, site } of computedWrites) { // A class method declared with a well-known symbol key is not a write; // any other write with one can replace that method. const symbolName = key ? wellKnownMethodName(unwrap(key)) : null; @@ -592,7 +670,7 @@ export function collectClassMethodMutations( const type = key ? lowerer.typeOf(key) : undefined; const names = type && (literalValues(type) ?? (key ? literalLoopKeys(unwrap(key)) : null)); if (names) { - for (const name of names) markName(String(name), site); + for (const name of names) markKeyed(String(name), site, receiver, target); continue; } if (key && receiver && preservesExistingMethod(key, receiver, site)) continue; @@ -611,16 +689,32 @@ export function collectClassMethodMutations( // Numeric writes can replace numeric names but not ordinary names. numericKey = site; } else if (!(flags & ts.TypeFlags.ESSymbolLike)) { - unknownKey = site; + markUnknownKey(site); } } + lowerer.checker.prefetchClassCollection( + keyedWrites.flatMap((write) => [write.target, write.receiver]), + [], + ); + for (const { name, receiver, target, site } of keyedWrites) { + if ( + !dictionaryView(receiver) && + (functionFree(lowerer.typeOf(target)) || !methodOwnerCanInhabit(receiver, name)) + ) + continue; + if (lowerer.prototypeMethodAccesses.has(name)) lowerer.noteMethodSite(name, site); + else lowerer.noteMethodAccess(name, site); + } if (numericKey) { for (const name of [...methods, ...arrowFields]) { if (String(Number(name)) === name) markName(name, numericKey); } } if (unknownKey) { - for (const method of methods) lowerer.prototypeMethodAccesses.set(method, unknownKey); + for (const method of methods) { + lowerer.prototypeMethodAccesses.set(method, unknownKey); + for (const site of unknownKeySites) lowerer.noteMethodSite(method, site); + } for (const method of symbolMethods) lowerer.replacedSymbolMethods.add(method); } if (!unknownKey && !unknownOwnField) diff --git a/packages/compiler/src/frontend/lowering/class-method-values.ts b/packages/compiler/src/frontend/lowering/class-method-values.ts index 6e8256513..ab9101cf7 100644 --- a/packages/compiler/src/frontend/lowering/class-method-values.ts +++ b/packages/compiler/src/frontend/lowering/class-method-values.ts @@ -378,8 +378,33 @@ function reflectedMethodValue( !isJsSourceFile(ts.getSourceFileOfNode(declaration)) || info.localClass !== undefined || lowerer.errorHierarchyClassOf(info.def.name); + // `super.m(...)` in a prototype method reads the base class's prototype, + // which lowerSuperMethodCall answers over the call-site receiver when the + // base has a prototype object. Other uses of `super` keep the native + // receiver. + const superCalls = !staticMethod && info.base !== null && prototypeChainViewable(info.base); + // Over a dynamic receiver, private fields are brand-checked and read, and private methods + // brand-checked and called; a private accessor, or a private method read as a value, would become a + // lookup of its name. + const privateAccessors = new Set(); + const privateMethods = new Set(); + for (const other of declaration.members.nodes) + if (other.name !== undefined && ts.isPrivateIdentifier(other.name)) { + if (ts.isAccessor(other)) privateAccessors.add(other.name.text); + else if (ts.isMethodDeclaration(other)) privateMethods.add(other.name.text); + } ts.walkPreorder(member, (node) => { - if (ts.isPrivateIdentifier(node) || node.kind === ts.SyntaxKind.SuperKeyword) + if (node.kind === ts.SyntaxKind.SuperKeyword && !(superCalls && isSuperMethodCallee(node))) + needsNativeReceiver = true; + if (!ts.isPrivateIdentifier(node)) return; + // Private members on a static method's receiver (the class value) keep + // the native receiver; prototype methods brand-check theirs, except + // private accessors and private methods used other than by a call. + if ( + staticMethod || + privateAccessors.has(node.text) || + (privateMethods.has(node.text) && !isPrivateMethodCallee(node)) + ) needsNativeReceiver = true; }); if (needsNativeReceiver) return null; @@ -403,7 +428,11 @@ function reflectedMethodValue( loc: locOf(member), }; } + // The method may be lowered on top of an unrelated caller's frames (a + // `super` call in another class's method reflects this class): its `this` + // is its own receiver, never an enclosing function's. const context = newFnCtx(false, null, null, DYN); + context.thisBoundary = true; const previousClass = lowerer.currentClass; lowerer.currentClass = info; lowerer.fnStack.push(context); @@ -441,6 +470,55 @@ function reflectedMethodValue( } } +/** Whether `base` and every class above it have a complete prototype object + * (reflectClassPrototype): module-level program classes that neither extend + * Date nor a runtime-provided class. */ +function prototypeChainViewable(base: ClassInfo): boolean { + for (let c: ClassInfo | null = base; c; c = c.base) + if ( + c.decl === null || + c.localClass !== undefined || + c.generic !== undefined || + c.genericInstance !== undefined || + c.mixinInstance !== undefined || + c.callableBase !== undefined || + c.extendsDate !== undefined || + c.def.runtime || + c.builtinEmitter || + c.builtinStream || + c.builtinError || + c.builtinEventTarget + ) + return false; + return true; +} + +/** `super` as the receiver of a called member: `super.m(...)`. */ +/** Whether `node`, a private name, is the method a call calls (`x.#m(...)`). */ +function isPrivateMethodCallee(node: ts.Node): boolean { + const access = node.parent; + return ( + access !== undefined && + ts.isPropertyAccessExpression(access) && + access.name === node && + access.parent !== undefined && + ts.isCallExpression(access.parent) && + access.parent.expression === access + ); +} + +function isSuperMethodCallee(node: ts.Node): boolean { + const access = node.parent; + return ( + access !== undefined && + ts.isPropertyAccessExpression(access) && + access.expression === node && + access.parent !== undefined && + ts.isCallExpression(access.parent) && + access.parent.expression === access + ); +} + /** Complete adapters after all native class bodies and instantiations exist. */ export function finalizeClassMethodValues(lowerer: Lowerer, functions: IrFunction[]): void { const byName = new Map(functions.map((fn) => [fn.name, fn])); diff --git a/packages/compiler/src/frontend/lowering/class-protocols.ts b/packages/compiler/src/frontend/lowering/class-protocols.ts index 76091b13e..00eedb6c0 100644 --- a/packages/compiler/src/frontend/lowering/class-protocols.ts +++ b/packages/compiler/src/frontend/lowering/class-protocols.ts @@ -10,6 +10,9 @@ export class ClassProtocols { private readonly accepted = new Map(); /** What markIdentityFlows learned about the program, while it runs. */ private facts: FlowFacts | null = null; + /** The value types of the string-keyed dictionaries that keep checked + * identity (see markDictionaryViews). */ + private readonly dictionaryViews = new Set(); constructor( private readonly checker: ts.TypeChecker, @@ -323,6 +326,7 @@ export class ClassProtocols { else if (names !== "all") names.add(name); } } + this.markDictionaryViews(sites, sourceNodes, observedNodes, writeNodes); for (let i = 0; i < looseArguments.length; i++) { const type = typeOf(looseNodes[i]!); if (type === undefined) continue; @@ -697,6 +701,163 @@ export class ClassProtocols { return source !== undefined && this.isProgramClassInstance(source); } + /** String-keyed dictionaries (`Record`, `{ [key: string]: V }`) + * that receive an existing record or class instance. Natively a + * dictionary is a key/value record, and converting another object into + * one copies its fields, so writes through the dictionary and identity + * checks would miss the original. When the program writes through, or + * observes, a value of such a dictionary type, every dictionary with that + * value type keeps checked identity instead (records with the same value + * type share one native layout, so values pass between them). A source + * counts when it is an existing object (a binding, member, element or + * `this`, not a fresh literal or call result) whose type is a program + * class instance or a record with declared properties, or a constant + * `{}`, `object` or `unknown` binding initialized with one. */ + private markDictionaryViews( + sites: readonly { + source: ts.Expression; + target: () => ts.Type | undefined; + contextual?: ts.Expression; + }[], + sourceNodes: readonly ts.Expression[], + observedNodes: readonly ts.Expression[], + writeNodes: readonly ts.Expression[], + ): void { + // The calls the sites' arguments belong to, by callee name, built on + // first use. + let calls: Map | null = null; + const callsNamed = (name: string): readonly (ts.CallExpression | ts.NewExpression)[] => { + if (calls === null) { + calls = new Map(); + for (const site of sites) { + const call = site.contextual?.parent; + if (call === undefined || (!ts.isCallExpression(call) && !ts.isNewExpression(call))) + continue; + const callee = call.expression; + const named = ts.isIdentifier(callee) + ? callee.text + : ts.isPropertyAccessExpression(callee) + ? callee.name.text + : undefined; + if (named === undefined) continue; + const list = calls.get(named) ?? []; + if (!list.includes(call)) list.push(call); + calls.set(named, list); + } + } + return calls.get(name) ?? []; + }; + const candidates: { index: number; sourceType: ts.Type }[] = []; + for (let i = 0; i < sites.length; i++) { + const source = sourceNodes[i]!; + if (!isExistingObject(source)) continue; + const sourceType = attempt(() => this.checker.getTypeAtLocation(source)); + if (sourceType !== undefined && this.dictionarySource(source, sourceType, callsNamed)) + candidates.push({ index: i, sourceType }); + } + if (candidates.length === 0) return; + // One batch for the contextual targets of the candidate sites. + attempt(() => + this.checker.prefetchContextualTypes( + candidates.flatMap(({ index }) => { + const contextual = sites[index]!.contextual; + return contextual === undefined ? [] : [contextual]; + }), + ), + ); + const received = new Set(); + for (const { index, sourceType } of candidates) { + const target = attempt(sites[index]!.target); + const parts = + target === undefined ? [] : target.isUnionType() ? ts.constituentTypes(target) : [target]; + for (const part of parts) { + const value = this.dictionaryValueType(part); + // A dictionary of `unknown` or `any` values is checked-dynamic already. + if ( + value !== undefined && + (value.flags & (ts.TypeFlags.Unknown | ts.TypeFlags.Any)) === 0 && + this.dictionaryValueType(sourceType) !== value + ) + received.add(value); + } + } + if (received.size === 0) return; + // The dictionary values the program writes through or observes. + const seen = new Set(); + for (const node of [...writeNodes, ...observedNodes]) { + const type = attempt(() => this.checker.getTypeAtLocation(node)); + const parts = + type === undefined ? [] : type.isUnionType() ? ts.constituentTypes(type) : [type]; + for (const part of parts) { + const value = this.dictionaryValueType(part); + if (value !== undefined) seen.add(value); + } + } + for (const value of received) if (seen.has(value)) this.dictionaryViews.add(value); + } + + /** Whether `source` holds an existing object that a dictionary + * conversion would copy. */ + private dictionarySource( + source: ts.Expression, + type: ts.Type, + callsNamed: (name: string) => readonly (ts.CallExpression | ts.NewExpression)[], + ): boolean { + if (this.isProgramClassInstance(type) || this.isRecordWithFields(type)) return true; + if (!isDynamicView(this.checker, type) || !ts.isIdentifier(source)) return false; + const decl = attempt(() => this.checker.getSymbolAtLocation(source))?.valueDeclaration; + if (decl === undefined) return false; + const holdsObject = (expr: ts.Expression): boolean => { + const held = attempt(() => this.checker.getTypeAtLocation(this.flowSourceNode(expr))); + return ( + held !== undefined && (this.isProgramClassInstance(held) || this.isRecordWithFields(held)) + ); + }; + if (ts.isParameter(decl)) { + // A parameter holds what its callers pass: the calls of the function + // by name whose signature resolves to it. + const fn = decl.parent; + if (fn === undefined || !ts.isFunctionLike(fn)) return false; + const index = fn.parameters.nodes.indexOf(decl); + const name = functionName(fn); + if (name === undefined || index < 0) return false; + return callsNamed(name).some((call) => { + const argument = call.arguments?.nodes[index]; + return ( + argument !== undefined && + !ts.isSpreadElement(argument) && + attempt(() => this.checker.getResolvedSignature(call)?.declaration) === fn && + holdsObject(argument) + ); + }); + } + return ( + ts.isVariableDeclaration(decl) && + decl.initializer !== undefined && + ts.isVariableDeclarationList(decl.parent) && + (decl.parent.flags & ts.NodeFlags.Const) !== 0 && + holdsObject(decl.initializer) + ); + } + + /** A program object type with declared properties that is not a class + * instance, array, tuple, function or library type. */ + private isRecordWithFields(type: ts.Type): boolean { + return this.isStructuralTarget(type) && this.checker.getPropertiesOfType(type).length > 0; + } + + /** The value type of a string-keyed dictionary with no declared + * properties, or undefined for any other type. */ + private dictionaryValueType(type: ts.Type): ts.Type | undefined { + if ((type.flags & ts.TypeFlags.Object) === 0) return undefined; + if (this.checker.isArrayType(type) || this.checker.isTupleType(type)) return undefined; + if (this.checker.getCallSignatures(type).length > 0) return undefined; + if (this.checker.getPropertiesOfType(type).length > 0) return undefined; + return this.checker + .getIndexInfosOfType(type) + .find((index) => (index.keyType.flags & ts.TypeFlags.String) !== 0)?.type; + } + private isProgramClassInstance(type: ts.Type): boolean { const symbol = type.getSymbol(); return ( @@ -822,7 +983,13 @@ export class ClassProtocols { if ((type.flags & ts.TypeFlags.Object) === 0) return false; const previous = this.answers.get(type); if (previous !== undefined) return previous; - const answer = this.protocol(type); + // A dictionary keeps checked identity when the protocols already mark + // it (a literal whose methods read `this`) or when it receives existing + // objects (markDictionaryViews). + const dictionaryValue = this.dictionaryValueType(type); + const answer = + this.protocol(type) || + (dictionaryValue !== undefined && this.dictionaryViews.has(dictionaryValue)); this.answers.set(type, answer); return answer; } @@ -922,6 +1089,44 @@ const IDENTITY_METHODS = new Set([ "includes", ]); +/** An expression that denotes an object that already exists, not a fresh + * literal or call result. */ +function isExistingObject(node: ts.Expression): boolean { + return ( + ts.isIdentifier(node) || + ts.isPropertyAccessExpression(node) || + ts.isElementAccessExpression(node) || + node.kind === ts.SyntaxKind.ThisKeyword + ); +} + +/** `{}`, `object` and `unknown`: types whose values are checked-dynamic + * and keep the original object. */ +function isDynamicView(checker: ts.TypeChecker, type: ts.Type): boolean { + if (type.flags & (ts.TypeFlags.Unknown | ts.TypeFlags.NonPrimitive)) return true; + return ( + (type.flags & ts.TypeFlags.Object) !== 0 && + checker.getPropertiesOfType(type).length === 0 && + checker.getIndexInfosOfType(type).length === 0 && + checker.getCallSignatures(type).length === 0 && + checker.getConstructSignatures(type).length === 0 + ); +} + +/** The name calls use for a function, method or function-valued binding. */ +function functionName(fn: ts.Node): string | undefined { + if ((ts.isFunctionDeclaration(fn) || ts.isMethodDeclaration(fn)) && fn.name !== undefined) + return ts.isIdentifier(fn.name) ? fn.name.text : undefined; + if ( + (ts.isArrowFunction(fn) || ts.isFunctionExpression(fn)) && + fn.parent !== undefined && + ts.isVariableDeclaration(fn.parent) && + ts.isIdentifier(fn.parent.name) + ) + return fn.parent.name.text; + return undefined; +} + function isNullishLiteral(node: ts.Expression): boolean { while (ts.isParenthesizedExpression(node)) node = node.expression; return ( diff --git a/packages/compiler/src/frontend/lowering/class-prototypes.ts b/packages/compiler/src/frontend/lowering/class-prototypes.ts index cb2ab2280..0c6741c63 100644 --- a/packages/compiler/src/frontend/lowering/class-prototypes.ts +++ b/packages/compiler/src/frontend/lowering/class-prototypes.ts @@ -13,7 +13,7 @@ import { type IrStmt, type SrcLoc, } from "../../ir/ir.js"; -import { locOf } from "../program.js"; +import { isJsSourceFile, locOf } from "../program.js"; import { classMemberNameOf, classValueRef, @@ -299,6 +299,23 @@ function declaresPrototypeMembers(decl: ts.ClassLikeDeclaration): boolean { ); } +/** The global that is true once the class's prototype view has been + * populated by reflection (reflectClassPrototype): the view then holds an + * own `constructor` unless the program deleted it. A view that was never + * reflected leaves `constructor` implicit. */ +export function prototypeReflectedFlag(lowerer: Lowerer, info: ClassInfo): string { + if (info.prototypeReflectedFlag) return info.prototypeReflectedFlag; + const id = `%g.%prototype.data.${info.def.name}.reflected`; + lowerer.globalsList.push({ + id, + name: `${info.def.jsName ?? info.def.name}.prototype.reflected`, + type: BOOL, + mutable: true, + }); + info.prototypeReflectedFlag = id; + return id; +} + /** Populate the shared prototype view before exposing it to reflection. * Methods and accessors are non-enumerable and retain declaration identity. */ export function reflectClassPrototype( @@ -399,8 +416,17 @@ export function reflectClassPrototype( { key: "value", value: classValueRef(lowerer, info, info.decl) }, writable, ]), + { + kind: "assign", + localId: prototypeReflectedFlag(lowerer, info), + value: { kind: "boolLit", value: true, type: BOOL, loc }, + loc, + }, ); + // The members lower on top of whatever frames requested the view (a + // `super` call in a subclass's method); their `this` is their own receiver. const context = newFnCtx(false, null, null, DYN); + context.thisBoundary = true; lowerer.fnStack.push(context); try { for (const member of info.decl.members.nodes) { @@ -503,6 +529,69 @@ export function isErrorPrototypeValue( ); } +/** Whether a bare `C.prototype` read becomes the prototype of another object + * or is only compared or inspected: the argument of `Object.create` or + * `Object.getPrototypeOf` / `Reflect.getPrototypeOf`, the prototype argument + * of `Object.setPrototypeOf` / `Reflect.setPrototypeOf`, or an operand of + * `===` / `!==`. In these positions the complete view of the + * prototype (reflectClassPrototype) answers. Objects that inherit from it + * run the class's methods with themselves as the receiver, which the + * methods' receiver-independent lowering (reflectedClassMethodValue) + * supports. Other positions, such as `Object.setPrototypeOf(C.prototype, X)`, + * which would relink the class, keep the diagnostic. */ +function prototypeViewPosition(lowerer: Lowerer, expr: ts.PropertyAccessExpression): boolean { + let node: ts.Node = expr; + let parent = node.parent; + while (parent && ts.isParenthesizedExpression(parent)) { + node = parent; + parent = parent.parent; + } + if (!parent) return false; + if (ts.isCallExpression(parent) && ts.isPropertyAccessExpression(parent.expression)) { + const args = parent.arguments.nodes; + const objectMethod = lowerer.stdlibGlobalMember(parent.expression, "Object"); + const reflectMethod = lowerer.stdlibGlobalMember(parent.expression, "Reflect"); + if ( + objectMethod === "create" || + objectMethod === "getPrototypeOf" || + reflectMethod === "getPrototypeOf" + ) + return args[0] === node; + if (objectMethod === "setPrototypeOf" || reflectMethod === "setPrototypeOf") + return args[1] === node; + return false; + } + return ( + ts.isBinaryExpression(parent) && + (parent.operatorToken.kind === ts.SyntaxKind.EqualsEqualsEqualsToken || + parent.operatorToken.kind === ts.SyntaxKind.ExclamationEqualsEqualsToken) + ); +} + +/** A module-level JavaScript class named directly (not a class value that + * may hold a subclass at run time): its prototype has a complete view whose + * methods accept any receiver. */ +function viewableClass(lowerer: Lowerer, info: ClassInfo, receiver: ts.Expression): boolean { + return ( + info.decl !== null && + info.localClass === undefined && + isJsSourceFile(ts.getSourceFileOfNode(info.decl)) && + exactClassOfReceiver(lowerer, receiver) === info + ); +} + +/** The complete prototype view of the class `node` names, when it is a + * viewable class (util.inherits's superCtor); null otherwise. */ +export function classPrototypeView( + lowerer: Lowerer, + node: ts.Expression, + loc: SrcLoc, +): IrExpr | null { + const info = exactClassOfReceiver(lowerer, node); + if (!info || !viewableClass(lowerer, info, node)) return null; + return reflectClassPrototype(lowerer, info, loc, classValueRef(lowerer, info, node)); +} + /** Named data and method slots have native descriptors. Bare reflection and * accessor replacement still require a complete view of the prototype. */ export function lowerClassPrototypeData( @@ -566,6 +655,12 @@ export function lowerClassPrototypeData( }); return reflectClassPrototype(lowerer, info, locOf(expr), receiver); } + if ( + name === null && + prototypeViewPosition(lowerer, expr) && + viewableClass(lowerer, info, expr.expression) + ) + return reflectClassPrototype(lowerer, info, locOf(expr), classValueRef(lowerer, info, expr)); if (name === null || (isCompiledPrototypeMember(lowerer, info, name) && !method)) { lowerer.unsupported( "SC1090", diff --git a/packages/compiler/src/frontend/lowering/class-to-primitive.ts b/packages/compiler/src/frontend/lowering/class-to-primitive.ts index 0b107ab4e..8807ac5a5 100644 --- a/packages/compiler/src/frontend/lowering/class-to-primitive.ts +++ b/packages/compiler/src/frontend/lowering/class-to-primitive.ts @@ -13,6 +13,7 @@ import * as ts from "../ts7/adapter.js"; import type { Lowerer } from "./lowerer.js"; import { accessorCall, findMethodOn, type ClassInfo } from "./lower-classes.js"; import { isClassCallback } from "./class-callbacks.js"; +import { TO_STRING_TAG_FIELD } from "./symbol-fields.js"; import { isSafeToRepeat } from "./expressions/evaluation-safety.js"; import { DATE_TIME_FIELD, dateRootOf } from "./builtins/date-classes.js"; import { dateValueMethod } from "./builtins/dates.js"; @@ -29,7 +30,15 @@ type ConversionMethod = "sym:toPrimitive" | "valueOf" | "toString"; * Date`, or through Object.prototype.toString ("[object Object]"). */ type Plan = | { method: ConversionMethod; ret: IrType } - | { method: null; date?: "toString" | "valueOf" }; + | { method: null; date?: "toString" | "valueOf"; tag?: ToStringTag }; + +/** Where Object.prototype.toString finds a class's Symbol.toStringTag: a + * string field (`[Symbol.toStringTag] = "LRUCache"`) or a getter that + * returns a string, declared on `owner`. */ +interface ToStringTag { + owner: ClassInfo; + getter: boolean; +} const PRIMITIVE_KINDS = new Set(["f64", "string", "bool", "bigint", "undefinedT", "nullT"]); @@ -52,6 +61,17 @@ export function classToStringConversion( ); } +/** Object.prototype.toString of a program class instance: "[object " + + * its Symbol.toStringTag + "]" (an own tag field, else an inherited + * getter), or "[object Object]". The class's own conversion methods do not + * take part. Null as for classToStringConversion, and for Date subclasses. */ +export function classObjectTag(lowerer: Lowerer, value: IrExpr, node: ts.Node): IrExpr | null { + if (value.type.kind !== "object") return null; + const info = lowerer.classes.get(value.type.className); + if (!info || dateRootOf(info) !== null) return null; + return classConversion(lowerer, value, "tag", node, STRING, (tag) => tag); +} + /** ToNumber(ToPrimitive(value, "number")) of a program class instance * (`Number(x)`): `[Symbol.toPrimitive]("number")`, then `valueOf` and then * `toString`, and NaN for "[object Object]". Null as for @@ -122,7 +142,7 @@ function mayBeBigint(lowerer: Lowerer, type: IrType): boolean { function classConversion( lowerer: Lowerer, value: IrExpr, - hint: ConversionHint, + hint: ConversionHint | "tag", node: ts.Node, resultType: IrType, finish: (primitive: IrExpr) => IrExpr, @@ -131,7 +151,7 @@ function classConversion( const info = lowerer.classes.get(value.type.className); if (!info || !plainProgramClass(info)) return null; const classes = subtree(lowerer, info); - for (const cls of classes) refuseReplacedConversions(lowerer, cls, node); + if (hint !== "tag") for (const cls of classes) refuseReplacedConversions(lowerer, cls, node); const plans = new Map(classes.map((cls) => [cls, planOf(lowerer, cls, hint, node)])); // A class whose plan differs from its base's starts a group; the deepest // groups are tested first, so each instance takes its own class's plan. @@ -164,12 +184,52 @@ function classConversion( }; return finish(dateValueMethod(plan.date, time, loc)!); } + if (plan.method === null && plan.tag) { + // Object.prototype.toString: "[object " + the tag + "]". + const owner = plan.tag.owner.def.name; + const tag: IrExpr = plan.tag.getter + ? accessorCall( + lowerer, + owner, + "get:sym:toStringTag", + lowerer.upcastTo(object, owner), + [], + STRING, + loc, + ) + : { + kind: "fieldGet", + obj: lowerer.upcastTo(object, owner), + className: owner, + field: TO_STRING_TAG_FIELD, + type: STRING, + loc, + }; + return finish({ + kind: "strConcat", + left: { + kind: "strConcat", + left: { kind: "strLit", value: "[object ", type: STRING, loc }, + right: tag, + type: STRING, + loc, + }, + right: { kind: "strLit", value: "]", type: STRING, loc }, + type: STRING, + loc, + }); + } if (plan.method === null) return finish({ kind: "strLit", value: "[object Object]", type: STRING, loc }); const args: IrExpr[] = plan.method === "sym:toPrimitive" ? [{ kind: "strLit", value: hint, type: STRING, loc }] : []; return finish(accessorCall(lowerer, cls.def.name, plan.method, object, args, plan.ret, loc)); }; + // "[object Object]" reads nothing of the instance, but the receiver + // expression still runs, once. + const own = plans.get(info)!; + if (tops.length === 0 && own.method === null && !own.date && !own.tag && !isSafeToRepeat(value)) + prefix.push({ kind: "exprStmt", expr: value, loc }); let result = convert(info, receiver); for (let i = tops.length - 1; i >= 0; i--) { const top = tops[i]!; @@ -228,10 +288,28 @@ function depth(info: ClassInfo): number { } function samePlan(a: Plan, b: Plan): boolean { - return ( - a.method === b.method && - (a.method !== null || b.method !== null || a.date === (b as { date?: string }).date) - ); + if (a.method !== null || b.method !== null) return a.method === b.method; + return a.date === b.date && a.tag?.owner === b.tag?.owner && a.tag?.getter === b.tag?.getter; +} + +/** The class's Symbol.toStringTag, when it has one: the class at the top + * of the chain whose layout holds the tag field (an own property, which + * shadows any getter), or its getter. Other tag types would make + * Object.prototype.toString answer "Object" or the value depending on the + * run, and are refused. */ +function toStringTagOf(lowerer: Lowerer, info: ClassInfo, node: ts.Node): ToStringTag | null { + const type = info.fields.get(TO_STRING_TAG_FIELD); + if (type !== undefined) { + if (type.kind !== "string") refuse(lowerer, node, info, "a non-string Symbol.toStringTag"); + let owner = info; + while (owner.base?.fields.has(TO_STRING_TAG_FIELD)) owner = owner.base; + return { owner, getter: false }; + } + const getter = findMethodOn(lowerer, info, "get:sym:toStringTag"); + if (!getter) return null; + if (getter.sig.ret.kind !== "string") + refuse(lowerer, node, info, "a non-string Symbol.toStringTag"); + return { owner: getter.declarer, getter: true }; } /** A conversion method the class declares or inherits (a field of that @@ -246,7 +324,16 @@ function declared( return found ? { ret: found.sig.ret } : null; } -function planOf(lowerer: Lowerer, info: ClassInfo, hint: ConversionHint, node: ts.Node): Plan { +function planOf( + lowerer: Lowerer, + info: ClassInfo, + hint: ConversionHint | "tag", + node: ts.Node, +): Plan { + if (hint === "tag") { + const tag = toStringTagOf(lowerer, info, node); + return tag ? { method: null, tag } : { method: null }; + } const exotic = declared(lowerer, info, "sym:toPrimitive"); if (exotic) { if (!primitive(lowerer, exotic.ret)) @@ -271,7 +358,8 @@ function planOf(lowerer: Lowerer, info: ClassInfo, hint: ConversionHint, node: t refuse(lowerer, node, info, `a '${method}' method that can return an object`); return { method, ret: found.ret }; } - return { method: null }; + const tag = toStringTagOf(lowerer, info, node); + return tag ? { method: null, tag } : { method: null }; } function primitive(lowerer: Lowerer, type: IrType): boolean { @@ -292,8 +380,6 @@ function refuseReplacedConversions(lowerer: Lowerer, info: ClassInfo, node: ts.N if (lowerer.prototypeMethodAccesses.has(method)) refuse(lowerer, node, info, `a '${method}' the program reaches through a prototype`); } - if (findMethodOn(lowerer, info, "get:sym:toStringTag") || info.fields.has("sym:toStringTag")) - refuse(lowerer, node, info, "a Symbol.toStringTag"); } function refuse(lowerer: Lowerer, node: ts.Node, info: ClassInfo, what: string): never { diff --git a/packages/compiler/src/frontend/lowering/coercions/asserted.ts b/packages/compiler/src/frontend/lowering/coercions/asserted.ts new file mode 100644 index 000000000..43c447cb0 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/coercions/asserted.ts @@ -0,0 +1,87 @@ +/* Slots reached only through what a type assertion claims. */ +import type { IrExpr, IrType, SrcLoc } from "../../../ir/ir.js"; +import { canDynCheckTo, DYN, isUnitType, typeEquals } from "../../../ir/ir.js"; +import { lowerRecordOvfCaptureHelper } from "../containers/indexed-objects.js"; +import type { Lowerer } from "../lowerer.js"; +import { unionRetagHelper } from "./unions.js"; + +/** A value an erased assertion produced (`x as T`, lowerer.assertedValues) + * flowing into a slot that no ordinary conversion reaches. Runs only where + * the slot would otherwise refuse the value (SC2002/SC2003), so every + * conversion that already lowered keeps its code. + * + * - A union whose arms only partly convert (`api as Partial>` on `S | Partial> | undefined`): the arms that convert + * into T convert into the slot, and an arm the assertion excludes (no + * conversion into T) throws the catchable TypeError of a lying assertion. + * - A record or array whose member types the assertion narrows (`models as + * Model<"one">[]` on a union-element array): the checked deep conversion + * a cast of an unknown value takes, which validates the value member by + * member and names the deepest failing path. */ +export function assertedConversion( + lowerer: Lowerer, + expr: IrExpr, + expected: IrType, +): IrExpr | null { + const asserted = lowerer.assertedValues.get(expr); + if (!asserted) return null; + const loc = expr.loc; + if (expr.type.kind === "union") { + const from = lowerer.unions.get(expr.type.unionId); + if (!from) return null; + const values = from.arms.filter((arm) => !isUnitType(arm)); + const kept = values.filter((arm) => convertsInto(lowerer, arm, asserted, loc)); + if (kept.length === 0 || kept.length === values.length) return null; + if (asserted.kind === "union" && expected.kind === "union") { + // The arms T excludes trap; the others (units included) re-tag. + const trappable = new Set(); + from.arms.forEach((arm, tag) => { + if (!isUnitType(arm) && !kept.includes(arm)) trappable.add(tag); + }); + const helper = unionRetagHelper(lowerer, from.id, expected.unionId, loc, trappable); + return helper ? { kind: "call", callee: helper, args: [expr], type: expected, loc } : null; + } + // A record or array T holds one arm's values: extract that arm (every + // other arm, a unit too, contradicts the assertion) and convert it. + if (asserted.kind === "union" || kept.length !== 1) return null; + const arm = kept[0]!; + const helper = lowerer.narrowedArmHelper(from.id, arm, loc); + if (!helper) return null; + const narrowed = lowerer.checkedNarrowCall(helper, expr, arm, loc); + if (typeEquals(arm, expected)) return narrowed; + const converted = lowerer.coerceToExpected(narrowed, expected); + return typeEquals(converted.type, expected) ? converted : null; + } + if (expected.kind !== "record" && expected.kind !== "array") return null; + if (expected.kind === "record" && lowerer.shapes.get(expected.shapeId)?.indexValue !== undefined) + return null; + if ( + !lowerer.dynConvertible(expr.type) || + !canDynCheckTo( + expected, + (id) => lowerer.shapes.get(id), + (id) => lowerer.unions.get(id), + ) + ) + return null; + return { kind: "dynCheck", value: lowerer.coerceToExpected(expr, DYN), type: expected, loc }; +} + +/** True when a value of `arm` has a structural conversion into `target`: + * the same type, one of its arms, a width lift, or (into an + * index-signature record) the overflow capture. */ +function convertsInto(lowerer: Lowerer, arm: IrType, target: IrType, loc: SrcLoc): boolean { + if (typeEquals(arm, target)) return true; + if ( + target.kind === "union" && + lowerer.unions.get(target.unionId)?.arms.some((candidate) => typeEquals(candidate, arm)) + ) + return true; + if (lowerer.widthLiftPlan(arm, target) !== null) return true; + return ( + arm.kind === "record" && + target.kind === "record" && + lowerer.shapes.get(target.shapeId)?.indexValue !== undefined && + lowerRecordOvfCaptureHelper(lowerer, arm.shapeId, target.shapeId, loc) !== null + ); +} diff --git a/packages/compiler/src/frontend/lowering/coercions/state.ts b/packages/compiler/src/frontend/lowering/coercions/state.ts index ad1d7c6ea..9fa815081 100644 --- a/packages/compiler/src/frontend/lowering/coercions/state.ts +++ b/packages/compiler/src/frontend/lowering/coercions/state.ts @@ -1,4 +1,6 @@ import type { IrType } from "../../../ir/ir.js"; +import type * as ts from "../../ts7/adapter.js"; +import type { Lowerer } from "../lowerer.js"; /** Per-compilation conversion state. Recursive plans are scoped to the * active planning stack; only fully validated copy routes are memoized. */ @@ -12,7 +14,37 @@ export class CoercionState { // Provenance for checked extraction; consumers must not infer it from names. readonly checkedNarrows = new Set(); // The extracted arm of each checked (non-defaulting) extraction helper. - readonly checkedNarrowArms = new Map(); + readonly checkedNarrowArms = new Map< + string, + { + unionId: string; + tag: number; + target: IrType; + widens?: true; + message?: string; + origin?: string; + } + >(); readonly retags = new Map(); readonly narrows = new Map(); + readonly widthNarrows = new Map(); + /** Unions widenReducedReturn produced: only these read through the + * width-converting extraction (widthNarrowHelper). */ + readonly widenedReturns = new Set(); + /** widenReducedReturn's results by declaration and inferred return type, + * for the results computed without cutting a widening cycle through + * another function. Each keeps the type-parameter bindings it was mapped + * under and answers only under the same bindings. */ + readonly widenedReturnTypes = new Map>(); +} + +/** A widened inferred return type and the lowering state that mapped it + * (Lowerer.typeParamBindings and its checker-type and implicit-any twins, + * and whether an erased generic body is lowering). */ +export interface WidenedReturn { + type: IrType; + bindings: Lowerer["typeParamBindings"]; + tsBindings: Lowerer["typeParamTsBindings"]; + implicit: Lowerer["implicitParamTypes"]; + erasing: boolean; } diff --git a/packages/compiler/src/frontend/lowering/coercions/structural-plans.ts b/packages/compiler/src/frontend/lowering/coercions/structural-plans.ts index f42663a13..2a394c4d1 100644 --- a/packages/compiler/src/frontend/lowering/coercions/structural-plans.ts +++ b/packages/compiler/src/frontend/lowering/coercions/structural-plans.ts @@ -1,6 +1,6 @@ import { planRecordUnionLift, planRecordUnionWrap } from "../union-retag.js"; import type { WidthLift } from "../width-lift.js"; -import type { IrType } from "../../../ir/ir.js"; +import type { IrRecordShape, IrType } from "../../../ir/ir.js"; import { canDynCheckTo, isUnitType, typeEquals, UNDEFINED_T } from "../../../ir/ir.js"; import { type ClassInfo, findMethodOn, findGenericMethodOn } from "../lower-classes.js"; import type { Lowerer } from "../lowerer.js"; @@ -104,7 +104,15 @@ export function widthLiftPlan(lowerer: Lowerer, src: IrType, dst: IrType): Width }) ); }); - if (absentOnly.length === 1) selected = absentOnly; + // That layout does not win when it holds the record only by dropping + // fields and another arm keeps every field (`{ kind; type }` beside + // `{ kind; name; adapts?: true; type }`): the value is not its variant. + const dropsFields = (arm: IrType): boolean => !keepsEveryField(lowerer, source, arm); + if ( + absentOnly.length === 1 && + !(dropsFields(absentOnly[0]!.arm) && selected.some(({ arm }) => !dropsFields(arm))) + ) + selected = absentOnly; } // A record that fits several arms of a discriminated union selects one // by its discriminant at run time. Without this, an arm that a nested @@ -128,6 +136,23 @@ export function widthLiftPlan(lowerer: Lowerer, src: IrType, dst: IrType): Width const route = presenceRoute(lowerer, src.shapeId, selected[0]!, selected[1]!); if (route) return route; } + // A record whose fields some arm keeps EVERY one of, each lifting + // (`{ id; limit; opts?: A | B }` into `{ id; limit; opts?: A | B | C } | + // { id; opts?: C }`): that arm is the value's own layout, widened; the + // others hold it only by dropping fields. Prefer the unique lossless arm + // with the record's own fields, then the unique one that also declares + // optional fields the record lacks, which complete as absent. + if (selected.length > 1 && src.kind === "record") { + const source = lowerer.shapes.get(src.shapeId); + const lossless = selected.filter(({ arm }) => keepsEveryField(lowerer, source, arm)); + const exact = lossless.filter( + ({ arm }) => + arm.kind === "record" && + lowerer.shapes.get(arm.shapeId)?.fields.length === source?.fields.length, + ); + if (exact.length === 1) selected = exact; + else if (exact.length === 0 && lossless.length === 1) selected = lossless; + } if (selected.length !== 1) return null; return { how: "liftWrap", tag: selected[0]!.tag, arm: selected[0]!.arm }; } @@ -222,6 +247,23 @@ export function widthLiftPlan(lowerer: Lowerer, src: IrType, dst: IrType): Width return null; } +/** True when the record arm `arm` has a field of every name the record + * `source` has. Tuples keep their own positional layouts. */ +function keepsEveryField( + lowerer: Lowerer, + source: IrRecordShape | undefined, + arm: IrType, +): boolean { + const target = arm.kind === "record" ? lowerer.shapes.get(arm.shapeId) : undefined; + return ( + source !== undefined && + target !== undefined && + !source.tuple && + !target.tuple && + source.fields.every((field) => target.fields.some((kept) => kept.name === field.name)) + ); +} + /** Two record arms a source record fits, where one arm declares a field * the other lacks and the source types that field `T | undefined` * (`{ count, errors: errs.length ? errs : undefined }` into `{ count; diff --git a/packages/compiler/src/frontend/lowering/coercions/structural.ts b/packages/compiler/src/frontend/lowering/coercions/structural.ts index 4321a5ead..863472910 100644 --- a/packages/compiler/src/frontend/lowering/coercions/structural.ts +++ b/packages/compiler/src/frontend/lowering/coercions/structural.ts @@ -1,12 +1,14 @@ +import { fieldOrigin } from "../decl-origin.js"; import { keyedHelperName } from "../stable-names.js"; import { constructedValue } from "../constructor-returns.js"; -import { dynUndefinedExpr, varRef } from "../../../ir/build.js"; +import { countedFor, dynUndefinedExpr, varRef } from "../../../ir/build.js"; import type { FieldLift } from "./structural-plans.js"; import { buildArrayConversion } from "./builders.js"; +import { arrayValueRead, arrayValueStore, primitiveElement } from "../array-values.js"; import type { WidthLift } from "../width-lift.js"; import { InternalCompilerError } from "../../../errors.js"; import type { IrExpr, IrFunction, IrRecordShape, IrStmt, IrType, SrcLoc } from "../../../ir/ir.js"; -import { BOOL, DYN, F64, STRING, UNDEFINED_T } from "../../../ir/ir.js"; +import { BOOL, DYN, F64, STRING, UNDEFINED_T, typeEquals } from "../../../ir/ir.js"; import { typeKey } from "../../type-mapper.js"; import { findStaticOn, findGenericStaticOn } from "../lower-classes.js"; import { lowerRecordOvfCaptureHelper } from "../containers/indexed-objects.js"; @@ -54,6 +56,9 @@ export function widthCoerce(lowerer: Lowerer, expr: IrExpr, expected: IrType): I return lowerer.classStaticsProjection(expr.type.className, expected.shapeId, expr.loc); } if (expected.kind === "array" && expr.type.kind === "array") { + const optional = optionalArrayHelper(lowerer, expr.type, expected, expr.loc); + if (optional) + return { kind: "call", callee: optional, args: [expr], type: expected, loc: expr.loc }; const helper = lowerer.arrayWidthHelper(expr.type, expected, expr.loc); if (helper) return { kind: "call", callee: helper, args: [expr], type: expected, loc: expr.loc }; @@ -390,7 +395,15 @@ export function recordWidthHelper( // orphan one). const plan = lowerer.recordWidthPlan(fromId, toId); if (!plan) return null; - const key = `rec:${fromId}:${toId}`; + // Package code converting into a package-declared record type (an + // object literal returned under `@returns {HelpContext}`): its field + // extractions name the declared fields (decl-origin.ts), so the helper + // is keyed by that type's declaration too (two packages may declare + // records of the same name and shape). Every other projection is shared + // as before. + const declared = lowerer.coercionRecordOrigin(); + const key = + declared === null ? `rec:${fromId}:${toId}` : `rec:${fromId}:${toId}:origin:${declared.key}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; const name = keyedHelperName(lowerer, "%rec.width", key); @@ -398,7 +411,21 @@ export function recordWidthHelper( // (self-referential shapes) resolves to this helper itself. lowerer.valueHelpers.set(key, name); const fromT: IrType = { kind: "record", shapeId: fromId }; - lowerer.liftedFns.push(buildRecordProjection(lowerer, name, fromT, to, plan, loc)); + const outerNode = lowerer.coercionOriginNode; + const outerField = lowerer.coercionFieldOrigin; + // Nested conversions see only their own field's origin. + lowerer.coercionOriginNode = null; + lowerer.coercionFieldOrigin = null; + try { + lowerer.liftedFns.push( + buildRecordProjection(lowerer, name, fromT, to, plan, loc, (field) => + declared === null ? null : fieldOrigin(lowerer.checker, declared, field), + ), + ); + } finally { + lowerer.coercionOriginNode = outerNode; + lowerer.coercionFieldOrigin = outerField; + } return name; } @@ -518,6 +545,69 @@ export function emptyArrayLiftHelper( return name; } +/** Copy an array of `T | undefined` unions (a collection's values, an + * iterator drain) into a `(T | undefined)[]` of a number, string or + * boolean T, which stores T elements: undefined values become present + * undefined elements. Null for other element types. */ +function optionalArrayHelper( + lowerer: Lowerer, + fromT: IrType & { kind: "array" }, + toT: IrType & { kind: "array" }, + loc: SrcLoc, +): string | null { + const from = fromT.elem; + const to = toT.elem; + if ( + !primitiveElement(to) || + from.kind !== "union" || + lowerer.armTag(from.unionId, UNDEFINED_T) < 0 || + !typeEquals(lowerer.stripUndefinedArm(from), to) + ) + return null; + const key = `arrOptional:${typeKey(from)}:${typeKey(to)}`; + const existing = lowerer.valueHelpers.get(key); + if (existing) return existing; + const name = keyedHelperName(lowerer, "%arr.width", key); + lowerer.valueHelpers.set(key, name); + const input = varRef("a.0", fromT, loc); + const out = varRef("out.0", toT, loc); + lowerer.liftedFns.push({ + name, + params: [{ localId: "a.0", name: "a", type: fromT }], + returnType: toT, + locals: [ + { id: "a.0", name: "a", type: fromT, mutable: true }, + { id: "out.0", name: "out", type: toT, mutable: false }, + { id: "i.0", name: "i", type: F64, mutable: true }, + { id: "v.0", name: "v", type: from, mutable: false }, + ], + body: [ + { + kind: "varDecl", + localId: "out.0", + init: { kind: "arrayLit", elems: [], type: toT, loc }, + loc, + }, + countedFor( + loc, + { kind: "arrIntrinsic", method: "length", receiver: input, args: [], type: F64, loc }, + (index) => [ + { + kind: "varDecl", + localId: "v.0", + init: arrayValueRead(lowerer, input, index, from, loc), + loc, + }, + arrayValueStore(lowerer, out, index, varRef("v.0", from, loc), to, loc), + ], + ), + { kind: "return", value: out, loc }, + ], + loc, + }); + return name; +} + /** Copy an array through its validated element conversion. Intern before * building the body so recursive element conversions can reuse this helper. */ export function arrayWidthHelper( @@ -542,14 +632,37 @@ export function arrayWidthHelper( name, arrT, { kind: "arrayLit", elems: [], type: outT, loc }, - (element, _index, result) => ({ - kind: "arrIntrinsic", - method: "push", - receiver: result, - args: [lowerer.applyWidthLift(elemLift, element, toElem, loc)], - type: F64, - loc, - }), + (element, index, result) => { + let value = lowerer.applyWidthLift(elemLift, element, toElem, loc); + // A number, string or boolean array can hold present undefined + // elements (`(T | undefined)[]` stores T): they, and holes, copy + // as the target's undefined. + const missing = primitiveElement(fromElem) ? lowerer.wrappedUndefined(toElem, loc) : null; + if (missing !== null) + value = { + kind: "ternary", + cond: { + kind: "bin", + op: "===", + left: { kind: "arrayState", arr: varRef("a.0", arrT, loc), index, type: F64, loc }, + right: { kind: "numLit", value: 1, type: F64, loc }, + type: BOOL, + loc, + }, + then: value, + else_: missing, + type: toElem, + loc, + }; + return { + kind: "arrIntrinsic", + method: "push", + receiver: result, + args: [value], + type: F64, + loc, + }; + }, loc, ), ); @@ -709,6 +822,7 @@ function buildRecordProjection( target: IrRecordShape, plan: Map, loc: SrcLoc, + originOf: (field: string) => string | null = () => null, ): IrFunction { const parameter = source.kind === "record" ? "r" : "o"; const localId = parameter + ".0"; @@ -765,7 +879,12 @@ function buildRecordProjection( type: conversion.src, loc, }; + // Only the field's own extraction (a plain narrow) carries the + // declared field's origin; nested conversions are shared. + lowerer.coercionFieldOrigin = + conversion.lift.how === "narrow" ? originOf(field.name) : null; const lifted = lowerer.applyWidthLift(conversion.lift, read, field.type, loc); + lowerer.coercionFieldOrigin = null; return { name: field.name, value: diff --git a/packages/compiler/src/frontend/lowering/coercions/unions.ts b/packages/compiler/src/frontend/lowering/coercions/unions.ts index 434e687a5..bc4ca4b0e 100644 --- a/packages/compiler/src/frontend/lowering/coercions/unions.ts +++ b/packages/compiler/src/frontend/lowering/coercions/unions.ts @@ -1,3 +1,4 @@ +import { renderOriginNote } from "../decl-origin.js"; import { keyedHelperName } from "../stable-names.js"; import { buildUnionNarrow, unionNarrowFailureMessage } from "../union-narrow.js"; import { isSafeToRepeat } from "../expressions/evaluation-safety.js"; @@ -394,18 +395,38 @@ export function unionRetagHelper( * the catchable TypeError — divergence 38's lying-assertion stance (an * unchecked unionNarrow would misread the payload where JS lets the * impossible value flow on). Null when the target isn't a non-unit arm - * of the union — those uses keep their erasure/fences. */ + * of the union — those uses keep their erasure/fences. `message` replaces + * the failure message (the TypeError of the operation consuming the + * value, such as a call's "f is not a function"). */ export function narrowedArmHelper( lowerer: Lowerer, fromId: string, target: IrType, loc: SrcLoc, + message?: string, ): string | null { const from = lowerer.unions.get(fromId); if (!from || isUnitType(target)) return null; const tag = lowerer.armTag(fromId, target); if (tag < 0) return null; - const key = `${fromId}:${tag}`; + // A slot in package code typed by a package declaration: the TypeError + // names the declaration (decl-origin.ts). Other extractions keep their + // shared helper. + const originNode = + message === undefined && lowerer.coercionOriginUnion === fromId + ? lowerer.coercionOriginNode + : null; + const origin = + message !== undefined + ? null + : (lowerer.coercionFieldOrigin ?? + (originNode === null ? null : lowerer.slotOriginOf(originNode))); + const key = + message !== undefined + ? `${fromId}:${tag}:${message}` + : origin !== null + ? `${fromId}:${tag}:origin:${origin}` + : `${fromId}:${tag}`; const existing = lowerer.coercions.narrows.get(key); if (existing) { lowerer.coercions.checkedNarrows.add(existing); @@ -414,9 +435,67 @@ export function narrowedArmHelper( const name = keyedHelperName(lowerer, "%union.narrow", key); lowerer.coercions.narrows.set(key, name); lowerer.coercions.checkedNarrows.add(name); - const fn = buildUnionNarrow(name, from, target, loc, (type) => lowerer.fmt(type)); + const fn = buildUnionNarrow( + name, + from, + target, + loc, + (type) => lowerer.fmt(type), + null, + message ?? null, + null, + origin, + lowerer.targetPlatform === "win32", + ); if (!fn) throw new InternalCompilerError("lowerer bug: invalid checked union extraction"); - lowerer.coercions.checkedNarrowArms.set(name, { unionId: fromId, tag, target: from.arms[tag]! }); + lowerer.coercions.checkedNarrowArms.set(name, { + unionId: fromId, + tag, + target: from.arms[tag]!, + ...(message === undefined ? {} : { message }), + ...(origin === null ? {} : { origin }), + }); + lowerer.liftedFns.push(fn); + return name; +} + +/** Interned `%union.narrow.width.(u)`: the checked extraction of a + * record arm from a union that also holds records whose fields strictly + * include the arm's (a function's widened inferred return, see + * widenReducedReturn). Those arms convert like any record width coercion; + * every other arm throws like narrowedArmHelper's. */ +export function widthNarrowHelper( + lowerer: Lowerer, + fromId: string, + target: IrType, + loc: SrcLoc, +): string | null { + const from = lowerer.unions.get(fromId); + if (!from || target.kind !== "record") return null; + const tag = lowerer.armTag(fromId, target); + if (tag < 0) return null; + const key = `${fromId}:${tag}`; + const existing = lowerer.coercions.widthNarrows.get(key); + if (existing) return existing; + const name = keyedHelperName(lowerer, "%union.narrow.width", key); + lowerer.coercions.widthNarrows.set(key, name); + const fn = buildUnionNarrow( + name, + from, + target, + loc, + (type) => lowerer.fmt(type), + null, + null, + (armTag, payload) => widthSubtypeConversion(lowerer, from.arms[armTag]!, target, payload, loc), + ); + if (!fn) throw new InternalCompilerError("lowerer bug: invalid checked union extraction"); + lowerer.coercions.checkedNarrowArms.set(name, { + unionId: fromId, + tag, + target: from.arms[tag]!, + widens: true, + }); lowerer.liftedFns.push(fn); return name; } @@ -442,7 +521,8 @@ export function checkedNarrowCall( value.type.kind !== "union" || value.type.unionId !== arm.unionId || !typeEquals(type, arm.target) || - !isSafeToRepeat(value) + !isSafeToRepeat(value) || + arm.widens === true ) return call; const unionId = arm.unionId; @@ -458,7 +538,20 @@ export function checkedNarrowCall( let failure: IrExpr | null = null; for (let tag = from.arms.length - 1; tag >= 0; tag--) { if (tag === arm.tag) continue; - const message = unionNarrowFailureMessage(from.arms[tag]!, (t) => lowerer.fmt(t)); + const failing = from.arms[tag]!; + const message = + (arm.message ?? unionNarrowFailureMessage(failing, (t) => lowerer.fmt(t))) + + (arm.origin === undefined + ? "" + : renderOriginNote( + arm.origin, + isUnitType(failing) + ? failing.kind === "undefinedT" + ? "undefined" + : "null" + : lowerer.fmt(failing), + lowerer.targetPlatform === "win32", + )); const thrown = nodeThrowExpr(1, "", message, type, loc); failure = failure === null @@ -599,3 +692,31 @@ export function deferredReadHelper( lowerer.liftedFns.push(fn); return name; } + +/** Whether `arm` is a record whose fields strictly include those of the + * record `target`: a value of the target type under TypeScript's structural + * typing, which the checker's subtype reduction may have spelled as the + * target (`{ status } | { status, value }` infers `{ status }`). */ +export function isWidthSubtypeRecord(lowerer: Lowerer, arm: IrType, target: IrType): boolean { + if (arm.kind !== "record" || target.kind !== "record") return false; + const own = lowerer.shapes.get(arm.shapeId); + const want = lowerer.shapes.get(target.shapeId); + if (!own || !want || own.tuple || want.tuple || own.indexValue || want.indexValue) return false; + if (want.fields.length >= own.fields.length) return false; + const names = new Set(own.fields.map((f) => f.name)); + return want.fields.every((f) => names.has(f.name)); +} + +/** A checked narrow that meets a width-subtype record arm converts it like + * any record width coercion instead of reporting a lying narrowing. */ +function widthSubtypeConversion( + lowerer: Lowerer, + arm: IrType, + target: IrType, + payload: IrExpr, + loc: SrcLoc, +): IrExpr | null { + if (!isWidthSubtypeRecord(lowerer, arm, target)) return null; + const lift = lowerer.widthLiftPlan(arm, target); + return lift === null ? null : lowerer.applyWidthLift(lift, payload, target, loc); +} diff --git a/packages/compiler/src/frontend/lowering/completion-callbacks.ts b/packages/compiler/src/frontend/lowering/completion-callbacks.ts new file mode 100644 index 000000000..a8ae99ec1 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/completion-callbacks.ts @@ -0,0 +1,156 @@ +/* Completion callbacks in programs that use AsyncLocalStorage. Node runs + * the callback of an operation a callback-style API starts (fs.readFile, + * crypto.randomBytes, zlib.gzip, dns.lookup, server.listen, net.connect, + * http.get, stream.finished, stream.pipeline, ...) with the stores active + * when the call started it. The runtime invokes these callbacks from + * the event loop, where the context is empty, so in programs whose + * scheduling carries contexts (RuntimeFeatures asyncContext) each such + * argument is bound to the active context at the call: als.bindCallback + * for typed closures, als.bindCallbackDyn for dyn functions. Other + * programs are left unchanged. */ +import { BOOL, type IrExpr, type IrModule, type IrStmt } from "../../ir/ir.js"; +import type { IrLibFn } from "../../ir/builtin-signatures.js"; +import { mapExprChildren, mapStmtChildren } from "../../ir/traverse.js"; +import { moduleUsesAsyncContext } from "../../ir/runtime-features.js"; + +/** The argument that is the completion callback: its index; "last" for + * calls whose callback is the final argument when one is passed; or + * "arguments" for the fs callback calls, whose argument is the call's + * dyn argument vector, where the runtime finds the callback. */ +type CallbackPosition = number | "last" | "arguments"; + +const COMPLETION_CALLBACKS: ReadonlyMap = new Map< + IrLibFn, + CallbackPosition +>([ + ["cp.execFile", 2], + ["crypto.generateKeyCb", 2], + ["crypto.hkdfCb", 5], + ["crypto.pbkdf2Cb", 5], + ["crypto.randomBytesCb", 1], + ["crypto.randomFillCb", 4], + ["crypto.randomFillCbDyn", 4], + ["crypto.randomIntCb", 3], + ["crypto.scryptCb", 4], + ["dgram.bindCb", 3], + ["dgram.closeCb", 1], + ["dgram.connectCb", 3], + ["dns.lookupDyn", 2], + ["fs.callbackCall", "arguments"], + ["fs.callbackGlob", "arguments"], + ["fs.renameCb", 2], + ["http.requestAgentCb", "last"], + ["http.requestCb", "last"], + ["http.requestConnCb", "last"], + ["http.requestDynCb", "last"], + ["http.requestUrlCb", "last"], + ["net.connectCb", 2], + ["net.connectPathCb", 2], + ["net.listenCb", 2], + ["net.listenOptsCb", 4], + ["net.listenOptsReusePortCb", 5], + ["net.listenPathCb", 2], + ["process.stderrWriteBytesCb", 2], + ["process.stdoutWriteBytesCb", 2], + // stream.finished and stream.pipeline callbacks, which Node binds to + // the context of the call (eos' and pipeline's AsyncResource.bind). + ["stream.finished", 1], + ["stream.finishedDyn", 1], + ["stream.finishedOpts", "last"], + ["stream.finishedOptsSignal", "last"], + ["stream.pipeline", "last"], + ["stream.pipelineDyn", "last"], + ["stream.pipelineStages", "last"], + ["stream.pipelineStagesDyn", "last"], + ["zlib.cbOpts", 3], + ["zlib.cbOptsDyn", 3], + ["zlib.deflateCb", 1], + ["zlib.deflateRawCb", 1], + ["zlib.gunzipCb", 1], + ["zlib.gzipCb", 1], + ["zlib.inflateCb", 1], + ["zlib.inflateRawCb", 1], + ["zlib.unzipCb", 1], +]); + +/** Completion callbacks that fire from other contexts' code (a stream event + * that another context's call emits), so an empty context at the call + * binds too: the callback then runs with no store, as in Node. */ +const BIND_EMPTY_CONTEXT: ReadonlySet = new Set([ + "stream.finished", + "stream.finishedDyn", + "stream.finishedOpts", + "stream.finishedOptsSignal", + "stream.pipeline", + "stream.pipelineDyn", + "stream.pipelineStages", + "stream.pipelineStagesDyn", +]); + +/** `cb` bound to the context active where it is evaluated. Unless + * `bindEmpty`, an empty context leaves it unbound (`always` false): the + * loop already runs those callbacks with an empty context. */ +function bindToActiveContext(cb: IrExpr, bindEmpty = false): IrExpr { + const always: IrExpr = { kind: "boolLit", value: bindEmpty, type: BOOL, loc: cb.loc }; + if (cb.type.kind === "func") + return { + kind: "libCall", + fn: "als.bindCallback", + args: [cb, always], + type: cb.type, + loc: cb.loc, + }; + if (cb.type.kind === "dyn") + return { + kind: "libCall", + fn: "als.bindCallbackDyn", + args: [cb, always], + type: cb.type, + loc: cb.loc, + }; + return cb; +} + +/** An element of a dyn argument vector that can be the callback: a boxed + * closure binds as a typed closure; any other dyn value that is not a + * boxed primitive binds as a dyn function (non-functions pass through). */ +function bindArgumentElement(e: IrExpr): IrExpr { + if (e.kind === "dynFrom") { + return e.value.type.kind === "func" ? { ...e, value: bindToActiveContext(e.value) } : e; + } + return e.type.kind === "dyn" ? bindToActiveContext(e) : e; +} + +function bindCall(e: IrExpr): IrExpr { + if (e.kind !== "libCall") return e; + const position = COMPLETION_CALLBACKS.get(e.fn); + if (position === undefined) return e; + if (position === "arguments") { + const index = e.fn === "fs.callbackCall" ? 1 : 0; + const vector = e.args[index]; + if (vector === undefined || vector.kind !== "dynArrLit") return e; + const args = e.args.slice(); + args[index] = { + kind: "dynArrLit", + elems: vector.elems.map(bindArgumentElement), + type: vector.type, + loc: vector.loc, + }; + return { ...e, args }; + } + const index = position === "last" ? e.args.length - 1 : position; + const cb = e.args[index]; + if (cb === undefined || (position === "last" && cb.type.kind !== "func")) return e; + const args = e.args.slice(); + args[index] = bindToActiveContext(cb, BIND_EMPTY_CONTEXT.has(e.fn)); + return { ...e, args }; +} + +/** Binds the completion callbacks of `mod` when its scheduling carries + * AsyncLocalStorage contexts. */ +export function bindCompletionCallbacks(mod: IrModule): void { + if (!moduleUsesAsyncContext(mod)) return; + const rewriteExpr = (e: IrExpr): IrExpr => bindCall(mapExprChildren(e, rewriteExpr, rewriteStmt)); + const rewriteStmt = (s: IrStmt): IrStmt => mapStmtChildren(s, rewriteExpr, rewriteStmt); + for (const fn of mod.functions) fn.body = fn.body.map(rewriteStmt); +} diff --git a/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts b/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts index 86f214bc1..5eee71231 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts @@ -38,10 +38,19 @@ import { } from "../lower-array-sort.js"; import { arrayIndexPresent, + arraySlotSnapshot, + arraySnapshotAppend, + arraySnapshotValue, arrayValueRead, arrayValueStore, arrayValueType, currentArrayIndexPresent, + elementMayBeForeign, + foreignElementThrow, + primitiveElement, + slotSnapshotLocals, + unreadArgument, + type SlotSnapshot, } from "../array-values.js"; import { typeKey } from "../../type-mapper.js"; import { @@ -98,9 +107,12 @@ export function lowerArrayHofCall( } } const callback = booleanFilter - ? { fnArg: truthyFilterCallback(lowerer, valueT, loc), arity: 1 } + ? { fnArg: truthyFilterCallback(lowerer, valueT, loc), arity: 1, unread: false } : lowerArrayCallback(lowerer, argNode, [valueT], arrayOf(elem)); const { fnArg, arity } = callback; + // Only an array that can hold elements outside its element type needs to + // leave an element its callback ignores unread. + const unread = callback.unread && !present && elementMayBeForeign(lowerer, elem); const fnRet = fnArg.type.ret; if (method === "map" && (fnRet.kind === "void" || fnRet.kind === "func")) { // The result array U[] is unrepresentable here (no void elements; @@ -130,8 +142,9 @@ export function lowerArrayHofCall( : null; const outElem = filtered !== null ? filtered.elem : callbackArrayElement(lowerer, call, fnRet); // A filter that re-tags its elements keeps the single union-typed visit. + // A callback that never reads its element has no use for a typed one. const fast = - present || booleanFilter || (method === "filter" && !typeEquals(outElem, elem)) + present || booleanFilter || unread || (method === "filter" && !typeEquals(outElem, elem)) ? null : lowerFastCallback(lowerer, argNode, [valueT], 0, elem, arrayOf(elem), callback); const helper = arrayHofHelper( @@ -145,6 +158,7 @@ export function lowerArrayHofCall( callbackSite(fast ?? fnArg), fast?.type ?? null, present, + unread, ); const resultType: IrType = method === "map" || method === "filter" ? arrayOf(outElem) : VOID; const result: IrExpr = { @@ -538,12 +552,21 @@ function callbackValueRead( elem: IrType, present: boolean, loc: SrcLoc, + unread = false, ): IrExpr { + if (unread) { + const placeholder = unreadArgument(lowerer, callbackValueType(lowerer, elem, present), loc); + if (placeholder) return placeholder; + } return present ? { kind: "arrayGet", arr, index, type: elem, loc } : arrayValueRead(lowerer, arr, index, elem, loc); } +/** The snapshot locals of the filter and find helpers; `value` is their + * element local `v.0`. */ +const FILTER_SNAPSHOT: SlotSnapshot = { isForeign: "vf.0", value: "v.0", foreign: "vd.0" }; + /** Interned synthetic loop function for one (method, elem, fnRet, arity) * combo. Named `%arr..` ('%' keeps it out of the user * namespace); rides `liftedFns` into the module like a lifted lambda (it @@ -559,13 +582,23 @@ function arrayHofHelper( site = "", fastT: CallbackType | null = null, present = false, + unread = false, ): string { - const key = `${method}:${typeKey(elem)}:${typeKey(fnRet)}:${arity}:${typeKey(outElem)}${site}${fastT ? ":split" : ""}${present ? ":present" : ""}`; + const key = `${method}:${typeKey(elem)}:${typeKey(fnRet)}:${arity}:${typeKey(outElem)}${site}${fastT ? ":split" : ""}${present ? ":present" : ""}${unread ? ":unread" : ""}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; const name = keyedHelperName(lowerer, `%arr.${method}`, key); lowerer.arrHofHelpers.set(key, name); - lowerer.arrHofInfo.set(name, { method, elem, fnRet, arity, outElem, present, split: !!fastT }); + lowerer.arrHofInfo.set(name, { + method, + elem, + fnRet, + arity, + outElem, + present, + split: !!fastT, + unread, + }); const fn = buildArrayHofFn( lowerer, name, @@ -577,6 +610,7 @@ function arrayHofHelper( outElem, fastT, present, + unread, ); lowerer.liftedFns.push(fastT ? withFastCallback(fn, fastT, elem) : fn); return name; @@ -791,6 +825,7 @@ function buildArrayHofFn( outElem: IrType, fastT: CallbackType | null = null, present = false, + unread = false, ): IrFunction { const arrT = arrayOf(elem); const valueT = callbackValueType(lowerer, elem, present); @@ -807,6 +842,19 @@ function buildArrayHofFn( present, loc, ); + // What the callback takes: the element, or a placeholder for one it + // ignores (callbackValueRead's `unread`). + const argElem = unread + ? callbackValueRead( + lowerer, + varRef("a.0", arrT, loc), + varRef("i.0", F64, loc), + elem, + present, + loc, + true, + ) + : getElem; const hasElem: IrExpr = { kind: "arrayHas", arr: varRef("a.0", arrT, loc), @@ -827,15 +875,16 @@ function buildArrayHofFn( ); returnType = outT; body = [ + readLen, { kind: "varDecl", localId: "out.0", init: { kind: "arrayLit", elems: [], type: outT, loc }, loc, }, - readLen, // map's result has the receiver's length, holes where the receiver - // has them. Storage for it is reserved first and the length is set + // has them. Storage for it is reserved first (with the result's + // construction, which directly precedes it) and the length is set // after the loop, so a result without holes is filled by appends and // gets no state bytes. ...(method === "map" @@ -854,7 +903,7 @@ function buildArrayHofFn( kind: "if", cond: hasElem, then: [ - { kind: "varDecl", localId: "mapped.0", init: callF(getElem), loc }, + { kind: "varDecl", localId: "mapped.0", init: callF(argElem), loc }, arrayValueStore( lowerer, varRef("out.0", outT, loc), @@ -990,6 +1039,19 @@ function buildArrayHofFn( ); returnType = outT; const retained = filterRetainedValue(lowerer, varRef("v.0", valueT, loc), elem, outElem, loc); + // A predicate that ignores the element of an array that can hold + // elements outside its element type keeps a snapshot of the slot, read + // before the predicate runs, as stored (arraySlotSnapshot). + const copied = + (arity === 0 || unread) && + !present && + typeEquals(outElem, elem) && + elementMayBeForeign(lowerer, elem); + // `v.0` is declared above; the snapshot adds its other two locals. + if (copied) + locals.push( + ...slotSnapshotLocals(lowerer, elem, FILTER_SNAPSHOT).filter((l) => l.id !== "v.0"), + ); body = [ { kind: "varDecl", @@ -1002,34 +1064,62 @@ function buildArrayHofFn( { kind: "if", cond: hasElem, - then: [ - { kind: "varDecl", localId: "v.0", init: getElem, loc }, - { - kind: "if", - // ToBoolean over the predicate's answer — inert when it already - // returned bool, the per-union helper when it returned a union. - cond: filterCond(callF(varRef("v.0", valueT, loc)), fnRet, loc), - then: [ - arrayValueStore( + // A kept element is appended as it was stored, so one the element + // type cannot hold stays in the result. + then: copied + ? [ + ...arraySlotSnapshot( lowerer, - varRef("out.0", outT, loc), - { - kind: "arrIntrinsic", - method: "length", - receiver: varRef("out.0", outT, loc), - args: [], - type: F64, - loc, - }, - retained, - outElem, + varRef("a.0", arrT, loc), + varRef("i.0", F64, loc), + elem, + FILTER_SNAPSHOT, loc, ), + { + kind: "if", + cond: filterCond(callF(argElem), fnRet, loc), + then: [ + arraySnapshotAppend( + lowerer, + varRef("out.0", outT, loc), + elem, + FILTER_SNAPSHOT, + loc, + ), + ], + else_: null, + loc, + }, + ] + : [ + { kind: "varDecl", localId: "v.0", init: getElem, loc }, + { + kind: "if", + // ToBoolean over the predicate's answer — inert when it already + // returned bool, the per-union helper when it returned a union. + cond: filterCond(callF(varRef("v.0", valueT, loc)), fnRet, loc), + then: [ + arrayValueStore( + lowerer, + varRef("out.0", outT, loc), + { + kind: "arrIntrinsic", + method: "length", + receiver: varRef("out.0", outT, loc), + args: [], + type: F64, + loc, + }, + retained, + outElem, + loc, + ), + ], + else_: null, + loc, + }, ], - else_: null, - loc, - }, - ], else_: null, loc, }, @@ -1044,7 +1134,7 @@ function buildArrayHofFn( { kind: "if", cond: hasElem, - then: [{ kind: "exprStmt", expr: callF(getElem), loc }], + then: [{ kind: "exprStmt", expr: callF(argElem), loc }], else_: null, loc, }, @@ -1084,13 +1174,17 @@ export function lowerArrayFindLikeCall( presentElements(lowerer, call, elem) && ((method !== "find" && method !== "findLast") || (!bindUntyped && lowerer.coercibleValue(elem, lowerer.irTypeOf(call)))); - const { fnArg, arity } = lowerArrayCallback( + const lowered = lowerArrayCallback( lowerer, argNode, [callbackValueType(lowerer, elem, present)], arrT, bindUntyped, ); + const { fnArg, arity } = lowered; + // Only an array that can hold elements outside its element type needs to + // leave an element its callback ignores unread. + const unread = lowered.unread && !present && elementMayBeForeign(lowerer, elem); // JS takes the ToBoolean of the predicate's result — allowed wherever // that ToBoolean has a static answer (bool passes through; f64/string // by value; a truthy-answerable union by its arm — the @@ -1105,18 +1199,19 @@ export function lowerArrayFindLikeCall( method === "findIndex" || method === "findLastIndex" ) { - const fast = present - ? null - : lowerFastCallback( - lowerer, - argNode, - [arrayValueType(lowerer, elem)], - 0, - elem, - arrT, - { fnArg, arity }, - bindUntyped, - ); + const fast = + present || unread + ? null + : lowerFastCallback( + lowerer, + argNode, + [arrayValueType(lowerer, elem)], + 0, + elem, + arrT, + { fnArg, arity }, + bindUntyped, + ); const fastT = fast?.type ?? null; const helper = method === "findIndex" || method === "findLastIndex" @@ -1130,6 +1225,7 @@ export function lowerArrayFindLikeCall( callbackSite(fast ?? fnArg), fastT, present, + unread, ) : someEveryHelper( lowerer, @@ -1141,6 +1237,7 @@ export function lowerArrayFindLikeCall( callbackSite(fast ?? fnArg), fastT, present, + unread, ); return { kind: "call", @@ -1178,6 +1275,7 @@ export function lowerArrayFindLikeCall( retag, callbackSite(fnArg), present, + unread, ); return { kind: "call", callee: helper, args: [receiver, fnArg], type: resultT, loc }; } @@ -1271,9 +1369,10 @@ function findHelper( retag: string | null, site = "", present = false, + unread = false, ): string { const method = last ? "findLast" : "find"; - const key = `${method}:${typeKey(elem)}:${typeKey(resultT)}:${typeKey(fnRet)}:${arity}${site}${present ? ":present" : ""}`; + const key = `${method}:${typeKey(elem)}:${typeKey(resultT)}:${typeKey(fnRet)}:${arity}${site}${present ? ":present" : ""}${unread ? ":unread" : ""}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; const name = keyedHelperName(lowerer, `%arr.${method}`, key); @@ -1292,28 +1391,62 @@ function findHelper( type: resultT, loc, }; - const visitBody: IrStmt[] = [ - { - kind: "varDecl", - localId: "v.0", - init: callbackValueRead( - lowerer, - varRef("a.0", arrT, loc), - varRef("i.0", F64, loc), - elem, - present, - loc, - ), + const read: IrStmt = { + kind: "varDecl", + localId: "v.0", + init: callbackValueRead( + lowerer, + varRef("a.0", arrT, loc), + varRef("i.0", F64, loc), + elem, + present, loc, - }, - { - kind: "if", - cond: predicateToBoolean(callArrayCallback(arrT, fnT, fnRet, arity, v, loc)), - then: [{ kind: "return", value: found, loc }], - else_: null, - loc, - }, - ]; + ), + loc, + }; + // A predicate that ignores the element of an array that can hold + // elements outside its element type keeps a snapshot of the slot, read + // before the predicate runs as JavaScript reads it (arraySlotSnapshot): + // the predicate is called without reading it as typed, and a found element + // outside the element type takes the checked conversion into the result. + const ignored = (arity === 0 || unread) && !present && elementMayBeForeign(lowerer, elem); + const placeholder = unread ? unreadArgument(lowerer, valueT, loc) : null; + const visitBody: IrStmt[] = ignored + ? [ + ...arraySlotSnapshot( + lowerer, + varRef("a.0", arrT, loc), + varRef("i.0", F64, loc), + elem, + FILTER_SNAPSHOT, + loc, + ), + { + kind: "if", + cond: predicateToBoolean( + callArrayCallback(arrT, fnT, fnRet, arity, placeholder ?? v, loc), + ), + then: [ + { + kind: "return", + value: arraySnapshotValue(lowerer, elem, FILTER_SNAPSHOT, resultT, () => found, loc), + loc, + }, + ], + else_: null, + loc, + }, + ] + : [ + read, + { + kind: "if", + cond: predicateToBoolean(callArrayCallback(arrT, fnT, fnRet, arity, v, loc)), + then: [{ kind: "return", value: found, loc }], + else_: null, + loc, + }, + ]; const visit: IrStmt[] = visitBody; const loop = last ? reverseArrayLoop(loc, visit) @@ -1330,6 +1463,9 @@ function findHelper( locals: [ ...arrayCallbackLocals(arrT, fnT), { id: "v.0", name: "v", type: valueT, mutable: false }, + ...(ignored + ? slotSnapshotLocals(lowerer, elem, FILTER_SNAPSHOT).filter((l) => l.id !== "v.0") + : []), ], body, loc, @@ -1371,9 +1507,10 @@ function findIndexHelper( site = "", fastT: CallbackType | null = null, present = false, + unread = false, ): string { const method = last ? "findLastIndex" : "findIndex"; - const key = `${method}:${typeKey(elem)}:${typeKey(fnRet)}:${arity}${site}${fastT ? ":split" : ""}${present ? ":present" : ""}`; + const key = `${method}:${typeKey(elem)}:${typeKey(fnRet)}:${arity}${site}${fastT ? ":split" : ""}${present ? ":present" : ""}${unread ? ":unread" : ""}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; const name = keyedHelperName(lowerer, `%arr.${method}`, key); @@ -1387,6 +1524,7 @@ function findIndexHelper( outElem: elem, present, split: !!fastT, + unread, }); const arrT = arrayOf(elem); const fnT = funcOf([callbackValueType(lowerer, elem, present), F64, arrT].slice(0, arity), fnRet); @@ -1407,6 +1545,7 @@ function findIndexHelper( elem, present, loc, + unread, ), loc, ), @@ -1456,8 +1595,9 @@ function someEveryHelper( site = "", fastT: CallbackType | null = null, present = false, + unread = false, ): string { - const key = `${method}:${typeKey(elem)}:${typeKey(fnRet)}:${arity}${site}${fastT ? ":split" : ""}${present ? ":present" : ""}`; + const key = `${method}:${typeKey(elem)}:${typeKey(fnRet)}:${arity}${site}${fastT ? ":split" : ""}${present ? ":present" : ""}${unread ? ":unread" : ""}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; const name = keyedHelperName(lowerer, `%arr.${method}`, key); @@ -1470,6 +1610,7 @@ function someEveryHelper( outElem: elem, present, split: !!fastT, + unread, }); const arrT = arrayOf(elem); const fnT = funcOf([callbackValueType(lowerer, elem, present), F64, arrT].slice(0, arity), fnRet); @@ -1486,6 +1627,7 @@ function someEveryHelper( elem, present, loc, + unread, ), loc, ), @@ -1549,12 +1691,16 @@ export function lowerArrayFlatMapCall( const argNode = call.arguments.nodes[0]; if (!argNode) lowerer.unsupported("SC1090", call, "this call form"); // tsc-guarded const present = presentElements(lowerer, call, elem); - const { fnArg, arity } = lowerArrayCallback( + const lowered = lowerArrayCallback( lowerer, argNode, [callbackValueType(lowerer, elem, present)], arrT, ); + const { fnArg, arity } = lowered; + // Only an array that can hold elements outside its element type needs to + // leave an element its callback ignores unread. + const unread = lowered.unread && !present && elementMayBeForeign(lowerer, elem); const fnRet = fnArg.type.ret; if ( fnRet.kind === "dyn" || @@ -1599,12 +1745,13 @@ export function lowerArrayFlatMapCall( if (fnRet.kind === "void" || fnRet.kind === "func") lowerer.badType(call, lowerer.typeOf(call)); requireProducedArrayElement(lowerer, call, "'.flatMap()'", fnRet); const outElem = callbackArrayElement(lowerer, call, fnRet); - const fast = present - ? null - : lowerFastCallback(lowerer, argNode, [arrayValueType(lowerer, elem)], 0, elem, arrT, { - fnArg, - arity, - }); + const fast = + present || unread + ? null + : lowerFastCallback(lowerer, argNode, [arrayValueType(lowerer, elem)], 0, elem, arrT, { + fnArg, + arity, + }); const helper = arrayHofHelper( lowerer, "flatMap", @@ -1616,6 +1763,7 @@ export function lowerArrayFlatMapCall( callbackSite(fast ?? fnArg), fast?.type ?? null, present, + unread, ); return { kind: "call", @@ -1625,12 +1773,13 @@ export function lowerArrayFlatMapCall( loc, }; } - const fast = present - ? null - : lowerFastCallback(lowerer, argNode, [arrayValueType(lowerer, elem)], 0, elem, arrT, { - fnArg, - arity, - }); + const fast = + present || unread + ? null + : lowerFastCallback(lowerer, argNode, [arrayValueType(lowerer, elem)], 0, elem, arrT, { + fnArg, + arity, + }); const helper = flatMapHelper( lowerer, elem, @@ -1640,6 +1789,7 @@ export function lowerArrayFlatMapCall( callbackSite(fast ?? fnArg), fast?.type ?? null, present, + unread, ); return { kind: "call", @@ -1689,6 +1839,8 @@ function flatMapUnionPlan( } const converts = (t: IrType): boolean => { if (typeEquals(t, outElem)) return true; + // A `(T | undefined)[]` result stores T elements (primitiveElement). + if (t.kind === "undefinedT" && primitiveElement(outElem)) return true; if (outElem.kind !== "union") return false; return lowerer.armTag(outElem.unionId, t) >= 0; }; @@ -1754,24 +1906,21 @@ function flatMapUnionHelper( }; if (plan.arms[tag] === "empty") return []; if (plan.arms[tag] === "value") { + // A unit arm's value is the unit itself, which has no narrowed + // representation. + const value: IrExpr = isUnitType(arm) + ? { kind: "unitLit", unit: arm.kind === "nullT" ? "null" : "undefined", type: arm, loc } + : narrowed; return [ arrayValueStore( lowerer, outRef(), outLength(), - lowerer.coerceToExpected( - // A unit arm's value is the unit itself, which has no - // narrowed representation. - isUnitType(arm) - ? { - kind: "unitLit", - unit: arm.kind === "nullT" ? "null" : "undefined", - type: arm, - loc, - } - : narrowed, - plan.outElem, - ), + // An undefined value is a present undefined element of a + // primitive array (arrayValueStore). + arm.kind === "undefinedT" && primitiveElement(plan.outElem) + ? value + : lowerer.coerceToExpected(value, plan.outElem), plan.outElem, loc, ), @@ -1940,8 +2089,9 @@ function flatMapHelper( site = "", fastT: CallbackType | null = null, present = false, + unread = false, ): string { - const key = `flatMap:${typeKey(elem)}:${typeKey(fnRet)}:${arity}${site}${fastT ? ":split" : ""}${present ? ":present" : ""}`; + const key = `flatMap:${typeKey(elem)}:${typeKey(fnRet)}:${arity}${site}${fastT ? ":split" : ""}${present ? ":present" : ""}${unread ? ":unread" : ""}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; const name = keyedHelperName(lowerer, "%arr.flatMap", key); @@ -1954,6 +2104,7 @@ function flatMapHelper( outElem: fnRet.elem, present, split: !!fastT, + unread, }); const arrT = arrayOf(elem); const inner = fnRet.elem; @@ -2050,6 +2201,7 @@ function flatMapHelper( elem, present, loc, + unread, ), loc, ), @@ -2122,7 +2274,7 @@ export function lowerArrayReduceCall( lowerer.runtimeOptionalReduceTypes.get(call) ?? (hasInit ? lowerer.irTypeOf(call) : arrayValueType(lowerer, elem)); if (accT.kind === "void" || accT.kind === "func") lowerer.badType(call, lowerer.typeOf(call)); - const { fnArg, arity } = lowerArrayCallback( + const lowered = lowerArrayCallback( lowerer, argNode, [accT, arrayValueType(lowerer, elem)], @@ -2130,17 +2282,23 @@ export function lowerArrayReduceCall( false, accT, ); - const fast = lowerFastCallback( - lowerer, - argNode, - [accT, arrayValueType(lowerer, elem)], - 1, - elem, - arrT, - { fnArg, arity }, - false, - accT, - ); + const { fnArg, arity } = lowered; + // Only an array that can hold elements outside its element type needs to + // leave an element its callback ignores unread. + const unread = lowered.unread && elementMayBeForeign(lowerer, elem); + const fast = unread + ? null + : lowerFastCallback( + lowerer, + argNode, + [accT, arrayValueType(lowerer, elem)], + 1, + elem, + arrT, + { fnArg, arity }, + false, + accT, + ); const helper = reduceHelper( lowerer, method, @@ -2152,6 +2310,7 @@ export function lowerArrayReduceCall( loc, callbackSite(fast ?? fnArg), fast?.type ?? null, + unread, ); const args: IrExpr[] = [receiver, fnArg]; if (hasInit) args.push(lowerer.lowerExprExpecting(call.arguments.nodes[1]!, accT)); @@ -2182,8 +2341,9 @@ function reduceHelper( loc: SrcLoc, site = "", fastT: CallbackType | null = null, + unread = false, ): string { - const key = `${method}:${typeKey(elem)}:${typeKey(accT)}:${typeKey(fnRet)}:${arity}:${hasInit ? "init" : "seed"}${site}${fastT ? ":split" : ""}`; + const key = `${method}:${typeKey(elem)}:${typeKey(accT)}:${typeKey(fnRet)}:${arity}:${hasInit ? "init" : "seed"}${site}${fastT ? ":split" : ""}${unread ? ":unread" : ""}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; const name = keyedHelperName(lowerer, `%arr.${method}`, key); @@ -2198,6 +2358,7 @@ function reduceHelper( present: false, split: !!fastT, hasInit, + unread, }); const arrT = arrayOf(elem); const fnT = funcOf([accT, arrayValueType(lowerer, elem), F64, arrT].slice(0, arity), fnRet); @@ -2207,6 +2368,9 @@ function reduceHelper( const right = method === "reduceRight"; const at = (index: IrExpr): IrExpr => arrayValueRead(lowerer, varRef("a.0", arrT, loc), index, elem, loc); + // The visited element, or a placeholder for a callback that never reads it. + const placeholder = unread ? unreadArgument(lowerer, arrayValueType(lowerer, elem), loc) : null; + const visited = (): IrExpr => placeholder ?? at(i); const locals: IrLocal[] = [ { id: "a.0", name: "a", type: arrT, mutable: true }, { id: "f.0", name: "f", type: fnT, mutable: true }, @@ -2317,7 +2481,7 @@ function reduceHelper( { kind: "callValue", callee: varRef("f.0", fnT, loc), - args: [varRef("acc.0", accT, loc), at(i), i, varRef("a.0", arrT, loc)].slice( + args: [varRef("acc.0", accT, loc), visited(), i, varRef("a.0", arrT, loc)].slice( 0, arity, ), @@ -2347,6 +2511,43 @@ function reduceHelper( return name; } +/** The comparator of a sort over a `(T | undefined)[]`, which stores T + * elements. The comparator only receives present elements (undefined + * elements sort last without a call), so the unannotated parameters of an + * inline comparator take the element type, and the runtime calls it + * without an adapter. */ +function lowerOptionalElementComparator( + lowerer: Lowerer, + argNode: ts.Expression, + comparator: ts.Expression, + elem: IrType, + arrT: IrType & { kind: "array" }, +): IrExpr { + if ( + arrT.optionalElements !== true || + !(ts.isArrowFunction(comparator) || ts.isFunctionExpression(comparator)) || + isJsSourceFile(ts.getSourceFileOfNode(comparator)) + ) + return lowerer.lowerExpr(argNode); + const rebound: { symbol: ts.Symbol; previous: IrType | undefined }[] = []; + for (const param of comparator.parameters.nodes.slice(0, 2)) { + if (param.type || param.initializer || param.dotDotDotToken || !ts.isIdentifier(param.name)) + continue; + const symbol = lowerer.checker.getSymbolAtLocation(param.name); + if (!symbol) continue; + rebound.push({ symbol, previous: lowerer.runtimeOptionalBindingTypes.get(symbol) }); + lowerer.runtimeOptionalBindingTypes.set(symbol, elem); + } + try { + return lowerer.lowerExpr(argNode); + } finally { + for (const { symbol, previous } of rebound) { + if (previous === undefined) lowerer.runtimeOptionalBindingTypes.delete(symbol); + else lowerer.runtimeOptionalBindingTypes.set(symbol, previous); + } + } +} + /** `a.sort(cmp)` / `a.toSorted(cmp)`, desugared to an interned synthetic * function like the other array HOFs. sort mutates and returns the receiver; * toSorted takes its shallow snapshot INSIDE the helper, after the receiver @@ -2358,10 +2559,11 @@ function reduceHelper( * the left element first. Undefined values sink without reaching the user * comparator. The callback sequence differs from V8's TimSort, so exact * order and count parity are not claimed; results agree for consistent - * comparators. Without a comparator, string, number and boolean arrays use - * native stable ordering: their String conversions cannot call user code. - * Strings compare UTF-16 units; numbers compare their decimal spellings - * ([10, 9, 1] → [1, 10, 9]). Other element types still need a comparator. */ + * comparators. Without a comparator, string, number, boolean and Date + * arrays use native stable ordering: their String conversions cannot call + * user code. Strings compare UTF-16 units; numbers compare their decimal + * spellings ([10, 9, 1] → [1, 10, 9]); Dates compare their toString() text. + * Other element types still need a comparator. */ export function lowerArraySortCall( lowerer: Lowerer, call: ts.CallExpression, @@ -2374,7 +2576,7 @@ export function lowerArraySortCall( const copyFirst = method === "toSorted"; if ( call.arguments.nodes.length === 0 && - (elem.kind === "string" || elem.kind === "f64" || elem.kind === "bool") + (elem.kind === "string" || elem.kind === "f64" || elem.kind === "bool" || elem.kind === "date") ) { return { kind: "arrIntrinsic", @@ -2389,7 +2591,7 @@ export function lowerArraySortCall( lowerer.noLowering( `.${method} with ${call.arguments.nodes.length} arguments`, call, - "default ordering supports string[], number[] and boolean[]; other element types need a comparator", + "default ordering supports string[], number[], boolean[] and Date[]; other element types need a comparator", ); } const receiver = lowerer.lowerExpr(access.expression); @@ -2434,7 +2636,8 @@ export function lowerArraySortCall( loc, }; } - let fnArg = undefinedArg ?? lowerer.lowerExpr(argNode); + let fnArg = + undefinedArg ?? lowerOptionalElementComparator(lowerer, argNode, unwrappedArg, elem, arrT); // Reusable JS comparators often accept checked values. Adapt the array's // element ABI just as other array callbacks do before sorting. if ( @@ -2491,6 +2694,9 @@ export function lowerArraySortCall( loc, native, keyed && { order, key: reads !== null, keys: keyed.length }, + elementMayBeForeign(lowerer, elem) + ? (arr, index) => foreignElementThrow(lowerer, arr, index, elem, loc) + : null, ), ); } @@ -3170,3 +3376,33 @@ export function lowerTuplePredicateCall( loc, }; } + +/** The `%arr.` helper `name` (map, flatMap or filter) taking its + * iteration bound as a last parameter: the loop runs to the bound instead + * of the receiver's length when it starts. ArraySpeciesCreate reads the + * receiver's length before the species constructor runs, so a constructor + * that appends to the receiver does not extend the loop + * (lowerWithArraySpecies). Null when `name` is no such helper. */ +export function boundedArrayHofHelper(lowerer: Lowerer, name: string): string | null { + const info = lowerer.arrHofInfo.get(name); + if (!info || info.method === "forEach") return null; + const bounded = `${name}.bounded`; + if (lowerer.liftedFnNamed(bounded)) return bounded; + const fn = lowerer.liftedFnNamed(name); + if (!fn) return null; + let replaced = false; + const body = fn.body.map((stmt): IrStmt => { + if (replaced || stmt.kind !== "varDecl" || stmt.localId !== "n.0") return stmt; + replaced = true; + return { ...stmt, init: varRef("bound.0", F64, stmt.loc) }; + }); + if (!replaced) return null; + lowerer.liftedFns.push({ + ...fn, + name: bounded, + params: [...fn.params, { localId: "bound.0", name: "bound", type: F64 }], + locals: [...fn.locals, { id: "bound.0", name: "bound", type: F64, mutable: false }], + body, + }); + return bounded; +} diff --git a/packages/compiler/src/frontend/lowering/containers/array-concat.ts b/packages/compiler/src/frontend/lowering/containers/array-concat.ts index a7ce0a106..440cbe9e2 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-concat.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-concat.ts @@ -1,5 +1,6 @@ import { keyedHelperName } from "../stable-names.js"; import { identityPreservingWidening } from "../coercions/identity.js"; +import { elementMayBeForeign, foreignElementThrow } from "../array-values.js"; import { countedFor, numLit, varRef } from "../../../ir/build.js"; import { BOOL, @@ -131,7 +132,33 @@ export function arrayConcatHelper( loc, }, then: [{ kind: "arraySetUndefined", arr: out, index: destination, loc }], - else_: null, + else_: elementMayBeForeign(lowerer, sourceElem) + ? [ + { + // A foreign element takes the checked conversion's TypeError. + kind: "if", + cond: { + kind: "bin", + op: "===", + left: { kind: "arrayState", arr: source, index, type: F64, loc }, + right: numLit(3, loc), + type: BOOL, + loc, + }, + then: [ + { + kind: "arraySet", + arr: out, + index: destination, + value: foreignElementThrow(lowerer, source, index, elem, loc), + loc, + }, + ], + else_: null, + loc, + }, + ] + : null, loc, }, ], diff --git a/packages/compiler/src/frontend/lowering/containers/array-construction.ts b/packages/compiler/src/frontend/lowering/containers/array-construction.ts index 1f10640c0..cadee368a 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-construction.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-construction.ts @@ -1,8 +1,10 @@ import { keyedHelperName } from "../stable-names.js"; +import { indexedUnionArms, lowerIndexedUnionToArray } from "../indexed-unions.js"; import { generatorDrain } from "../iterator-adapters.js"; import { identityPreservingWidening } from "../coercions/identity.js"; import { liftedFreshSpread } from "../coercions/fresh-spread.js"; import { checkedIterableSpread } from "../checked-iterable-spread.js"; +import { AstNode } from "../../ts7/ast-node.js"; import { InternalCompilerError } from "../../../errors.js"; import { dynUndefinedExpr, @@ -26,7 +28,9 @@ import { type IrType, STRING, type SrcLoc, + UNDEFINED_T, arrayOf, + isUnitType, canConvertToDyn, funcOf, isRefCounted, @@ -34,7 +38,13 @@ import { typeEquals, } from "../../../ir/ir.js"; import { isJsSourceFile, locOf } from "../../program.js"; -import { arrayValueRead, arrayValueStore, arrayValueType } from "../array-values.js"; +import { + arrayValueRead, + arrayValueStore, + arrayValueType, + optionalElementValue, + primitiveElement, +} from "../array-values.js"; import { typeKey } from "../../type-mapper.js"; import { newFnCtx } from "../lowerer.js"; import { iteratorCanStep, iteratorValue } from "../iterator-consumption.js"; @@ -50,6 +60,34 @@ import { } from "./callback-arguments.js"; import { classGeneratorIterable } from "../class-iterables.js"; +/** A value whose type admits only null and undefined, stored into an array + * that keeps undefined elements: the checker admits it there only when its + * type is `undefined` (a tuple element typed `undefined` maps to such a + * union), so it is evaluated and stored as undefined. */ +function undefinedForNullish( + lowerer: Lowerer, + value: IrExpr, + arrType: IrType & { kind: "array" }, +): IrExpr { + if (arrType.optionalElements !== true || value.type.kind !== "union") return value; + const arms = lowerer.unions.get(value.type.unionId)?.arms ?? []; + if (arms.length === 0 || !arms.every(isUnitType)) return value; + const loc = value.loc; + const optional: IrType = { + kind: "union", + unionId: lowerer.unions.intern([arrType.elem, UNDEFINED_T]), + }; + const missing = lowerer.wrappedUndefined(optional, loc); + if (!missing) return value; + return { + kind: "seqExpr", + stmts: [{ kind: "exprStmt", expr: value, loc }], + result: missing, + type: optional, + loc, + }; +} + /** Build the argument array for a mutating/copying operation while keeping * the receiver's scalar payload ABI. A missing read is represented as the * present UNDEFINED state in this temporary, so push/unshift and toSpliced @@ -85,9 +123,11 @@ export function lowerArrayValueItems( loc, }, ]; - for (const value of values) { + for (const item of values) { + const value = undefinedForNullish(lowerer, item, arrType); const stored = - lowerer.runtimeOptionalWidening(value.type, elem) !== null + lowerer.runtimeOptionalWidening(value.type, elem) !== null || + optionalElementValue(lowerer, value, elem) ? value : lowerer.coerceToExpected(value, elem); body.push(arrayValueStore(lowerer, outRef, length(), stored, elem, loc)); @@ -95,16 +135,21 @@ export function lowerArrayValueItems( return { kind: "seqExpr", stmts: body, result: outRef, type: arrType, loc }; } +/** The items of an array built from argument nodes (a spread copies its + * elements in) and values already lowered (a spread tuple's fields), in + * order. Each value is stored when its turn comes. */ export function lowerArraySpreadItems( lowerer: Lowerer, - nodes: readonly ts.Expression[], + nodes: readonly (ts.Expression | IrExpr)[], elem: IrType, arrType: IrType & { kind: "array" }, loc: SrcLoc, ): IrExpr { const out = lowerer.declareHiddenLocal("%arrayItems", arrType); const outRef = varRef(out.id, arrType, loc); - const plainItems = nodes.filter((node) => !ts.isSpreadElement(node)).length; + const plainItems = nodes.filter( + (node) => !(node instanceof AstNode) || !ts.isSpreadElement(node), + ).length; const length = (): IrExpr => ({ kind: "arrIntrinsic", method: "length", @@ -128,7 +173,23 @@ export function lowerArraySpreadItems( }, ]; for (const node of nodes) { - if (ts.isSpreadElement(node)) { + if (!(node instanceof AstNode)) { + // A spread tuple's first field declares the tuple's storage, which + // the later fields read: its statements join this sequence, whose + // frame lasts until every item is stored. + let item = node; + if (item.kind === "seqExpr") { + body.push(...item.stmts); + item = item.result; + } + const value = undefinedForNullish(lowerer, item, arrType); + const stored = + lowerer.runtimeOptionalWidening(value.type, elem) !== null || + optionalElementValue(lowerer, value, elem) + ? value + : lowerer.coerceToExpected(value, elem); + body.push(arrayValueStore(lowerer, outRef, length(), stored, elem, loc)); + } else if (ts.isSpreadElement(node)) { let source = lowerer.lowerExpr(node.expression); if ( source.type.kind === "set" && @@ -211,7 +272,8 @@ export function lowerArraySpreadItems( } else { const value = lowerer.lowerExpr(node); const stored = - lowerer.runtimeOptionalWidening(value.type, elem) !== null + lowerer.runtimeOptionalWidening(value.type, elem) !== null || + optionalElementValue(lowerer, value, elem) ? value : lowerer.coerceInto(node, value, elem); body.push(arrayValueStore(lowerer, outRef, length(), stored, elem, loc)); @@ -498,12 +560,15 @@ export function lowerArrayOfCall( lowerer: Lowerer, call: ts.CallExpression, access: ts.PropertyAccessExpression, + /** The receiver is a class declared `extends Array` (array-classes.ts + * constructs the result through it). */ + arrayClassReceiver = false, ): IrExpr | null { if ( call.questionDotToken || access.questionDotToken || access.name.text !== "of" || - !lowerer.isStdlibGlobal(access.expression, "Array") + (!arrayClassReceiver && !lowerer.isStdlibGlobal(access.expression, "Array")) ) return null; let result = lowerer.mapTypeOf(lowerer.typeOf(call)); @@ -517,6 +582,20 @@ export function lowerArrayOfCall( if (call.arguments.nodes.some(ts.isSpreadElement)) { return lowerArraySpreadItems(lowerer, call.arguments.nodes, result.elem, result, locOf(call)); } + // A `(T | undefined)[]` (primitiveElement) stores undefined arguments as + // present undefined elements. + if (result.optionalElements === true) { + const nodes = call.arguments.nodes; + const values = nodes.map((arg) => lowerer.lowerExpr(arg)); + if (values.some((value) => optionalElementValue(lowerer, value, result.elem))) + return lowerArrayValueItems(lowerer, values, result.elem, result, locOf(call)); + return { + kind: "arrayLit", + elems: values.map((value, i) => lowerer.coerceInto(nodes[i]!, value, result.elem)), + type: result, + loc: locOf(call), + }; + } return { kind: "arrayLit", elems: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, result.elem)), @@ -856,9 +935,11 @@ export function lowerArrayFromCall( lowerer: Lowerer, call: ts.CallExpression, access: ts.PropertyAccessExpression, + /** The receiver is a class declared `extends Array` (array-classes.ts). */ + arrayClassReceiver = false, ): IrExpr | null { if (call.questionDotToken || access.questionDotToken) return null; - if (!lowerer.isStdlibGlobal(access.expression, "Array")) return null; + if (!arrayClassReceiver && !lowerer.isStdlibGlobal(access.expression, "Array")) return null; if (access.name.text !== "from") return null; const loc = locOf(call); const args = call.arguments.nodes; @@ -958,6 +1039,31 @@ export function lowerArrayFromCall( const arrT = lowerer.mapTypeOf(lowerer.typeOf(call)); if (arrT?.kind !== "array") lowerer.badType(call, lowerer.typeOf(call)); const elem = arrT.elem; + // A `(T | undefined)[]` (primitiveElement): n present undefined + // elements. + const elemTs = lowerer.checker.isArrayType(lowerer.typeOf(call)) + ? lowerer.checker.getTypeArguments(lowerer.typeOf(call) as ts.TypeReference)[0] + : undefined; + if ( + primitiveElement(elem) && + elemTs !== undefined && + (elemTs.isUnionType() ? ts.constituentTypes(elemTs) : [elemTs]).some( + (part) => (part.flags & ts.TypeFlags.Undefined) !== 0, + ) + ) { + return { + kind: "arrIntrinsic", + method: "fillUndefined", + receiver: { kind: "arrayNewLen", length: n, type: arrT, loc }, + // To the end: fill clamps an infinite end to the length. + args: [ + numLit(0, loc), + { kind: "bin", op: "/", left: numLit(1, loc), right: numLit(0, loc), type: F64, loc }, + ], + type: arrT, + loc, + }; + } const absent = elem.kind === "union" ? lowerer.wrappedUndefined(elem, loc) !== null || @@ -980,9 +1086,7 @@ export function lowerArrayFromCall( if ((args.length === 1 || args.length === 2) && !ts.isObjectLiteralExpression(args[0]!)) { const source = lowerer.lowerExpr(args[0]!); let src = - source.type.kind === "string" && args.length === 2 - ? strCharsCall(lowerer, source, loc) - : source; + source.type.kind === "string" && args.length === 2 ? strCharsCall(source, loc) : source; if (src.type.kind === "array") { const arrT = src.type; if (args.length === 1) { @@ -1054,6 +1158,10 @@ export function lowerArrayFromCall( loc, }; } + // A union of typed arrays and number arrays (indexed-unions.ts). + const indexedArms = indexedUnionArms(lowerer, src.type); + if (indexedArms && args.length === 1) + return lowerIndexedUnionToArray(lowerer, src, indexedArms, loc, true); if (src.type.kind === "set" && args.length === 1) { return { kind: "setIntrinsic", @@ -1066,7 +1174,7 @@ export function lowerArrayFromCall( } // String iteration advances by code point; the same helper serves // `[...s]` and both Array.from(s) forms. - if (src.type.kind === "string" && args.length === 1) return strCharsCall(lowerer, src, loc); + if (src.type.kind === "string" && args.length === 1) return strCharsCall(src, loc); if (src.type.kind === "dyn" && args.length === 1) { return lowerCheckedArrayFrom(lowerer, src, loc); } @@ -1080,7 +1188,10 @@ export function lowerArrayFromCall( // A generator (or an iterator adapted into one) drains in order. if (src.type.kind === "generator" && args.length === 1) { const arrT = lowerer.mapTypeOf(lowerer.typeOf(call)); - const drained = arrT?.kind === "array" ? generatorDrain(lowerer, src, arrT.elem, loc) : null; + const drained = + arrT?.kind === "array" + ? generatorDrain(lowerer, src, arrT.elem, loc, arrT.optionalElements === true) + : null; if (drained) return drained; } lowerer.noLowering( @@ -1154,6 +1265,11 @@ function buildArrayFromArrayFn( type: fnRet, loc, }; + // A result of another type than the element (`T | undefined` into a + // `(T | undefined)[]`, which stores T elements) is held in a local, so + // the store tests and converts one value. + const held = !typeEquals(fnRet, outElem); + const value: IrExpr = held ? varRef("v.0", fnRet, loc) : mapped; return { name, params: [ @@ -1166,6 +1282,7 @@ function buildArrayFromArrayFn( { id: "f.0", name: "f", type: fnT, mutable: true }, { id: "out.0", name: "out", type: outT, mutable: false }, { id: "i.0", name: "i", type: F64, mutable: true }, + ...(held ? [{ id: "v.0", name: "v", type: fnRet, mutable: false }] : []), ], body: [ { @@ -1186,7 +1303,8 @@ function buildArrayFromArrayFn( loc, }, body: [ - arrayValueStore(lowerer, out, i, mapped, outElem, loc), + ...(held ? [{ kind: "varDecl", localId: "v.0", init: mapped, loc } as const] : []), + arrayValueStore(lowerer, out, i, value, outElem, loc), { kind: "assign", localId: "i.0", @@ -1203,112 +1321,9 @@ function buildArrayFromArrayFn( } /** `Array.from(s)` / `[...s]` on a STRING: the code-point split into a - * fresh string[], through one interned helper per module. */ -export function strCharsCall(lowerer: Lowerer, src: IrExpr, loc: SrcLoc): IrExpr { - const key = "strChars"; - let helper = lowerer.arrHofHelpers.get(key); - if (!helper) { - helper = "%str.chars"; - lowerer.arrHofHelpers.set(key, helper); - lowerer.liftedFns.push(buildStrCharsFn(helper, loc)); - } - return { kind: "call", callee: helper, args: [src], type: arrayOf(STRING), loc }; -} - -/** The code-point split, from existing IR nodes — the string for-of - * desugar's UTF-16 cursor as a function: - * - * out = []; - * i = 0; - * while (i < s.length) { ch = cpAt(s, i); i += ch.length; out.push(ch); } - * return out; - */ -function buildStrCharsFn(name: string, loc: SrcLoc): IrFunction { - const outT = arrayOf(STRING); - - const sLen = (recv: IrExpr): IrExpr => ({ - kind: "strIntrinsic", - method: "length", - receiver: recv, - args: [], - type: F64, - loc, - }); - const body: IrStmt[] = [ - { - kind: "varDecl", - localId: "out.0", - init: { kind: "arrayLit", elems: [], type: outT, loc }, - loc, - }, - { kind: "varDecl", localId: "i.0", init: { kind: "numLit", value: 0, type: F64, loc }, loc }, - { - kind: "while", - cond: { - kind: "bin", - op: "<", - left: varRef("i.0", F64, loc), - right: sLen(varRef("s.0", STRING, loc)), - type: BOOL, - loc, - }, - body: [ - { - kind: "varDecl", - localId: "ch.0", - init: { - kind: "strIntrinsic", - method: "cpAt", - receiver: varRef("s.0", STRING, loc), - args: [varRef("i.0", F64, loc)], - type: STRING, - loc, - }, - loc, - }, - { - kind: "assign", - localId: "i.0", - value: { - kind: "bin", - op: "+", - left: varRef("i.0", F64, loc), - right: sLen(varRef("ch.0", STRING, loc)), - type: F64, - loc, - }, - loc, - }, - { - kind: "exprStmt", - expr: { - kind: "arrIntrinsic", - method: "push", - receiver: varRef("out.0", outT, loc), - args: [varRef("ch.0", STRING, loc)], - type: F64, - loc, - }, - loc, - }, - ], - loc, - }, - { kind: "return", value: varRef("out.0", outT, loc), loc }, - ]; - return { - name, - params: [{ localId: "s.0", name: "s", type: STRING }], - returnType: outT, - locals: [ - { id: "s.0", name: "s", type: STRING, mutable: true }, - { id: "out.0", name: "out", type: outT, mutable: false }, - { id: "i.0", name: "i", type: F64, mutable: true }, - { id: "ch.0", name: "ch", type: STRING, mutable: false }, - ], - body, - loc, - }; + * fresh string[] (scr_str_code_points). */ +export function strCharsCall(src: IrExpr, loc: SrcLoc): IrExpr { + return { kind: "libCall", fn: "str.codePoints", args: [src], type: arrayOf(STRING), loc }; } /** The lowered `length` value of the one-property source literal, or null diff --git a/packages/compiler/src/frontend/lowering/containers/array-methods.ts b/packages/compiler/src/frontend/lowering/containers/array-methods.ts index 3ab2e84e2..3b16e714f 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-methods.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-methods.ts @@ -32,6 +32,10 @@ import { arrayValueRead, arrayValueStore, arrayValueType, + elementPlaceholder, + insertedValues, + optionalElementValue, + primitiveElement, } from "../array-values.js"; import { typeKey } from "../../type-mapper.js"; import { @@ -125,6 +129,14 @@ function lowerOptionalArrayPush( return { kind: "seqExpr", stmts, result: length(), type: F64, loc }; } +/** The checker type of `node` admits undefined. */ +function checkerTypeAdmitsUndefined(lowerer: Lowerer, node: ts.Expression): boolean { + const type = lowerer.typeOf(node); + return (type.isUnionType() ? ts.constituentTypes(type) : [type]).some( + (part) => (part.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Void)) !== 0, + ); +} + /** `a.splice(start, count, v)` where v may hold undefined at run time. * Receiver, start, count, and v evaluate once, in order; a present v is * inserted as a plain element literal (which a statement-position splice @@ -658,7 +670,10 @@ export function lowerArrayMethodCall( const hasSpread = itemNodes.some(ts.isSpreadElement); const itemProbes = hasSpread ? [] : itemNodes.map((arg) => tryLowerExpression(lowerer, arg)); const statefulItems = itemProbes.some( - (probe) => probe !== null && lowerer.runtimeOptionalWidening(probe.type, elem) !== null, + (probe) => + probe !== null && + (lowerer.runtimeOptionalWidening(probe.type, elem) !== null || + optionalElementValue(lowerer, probe, elem)), ); const items: IrExpr = hasSpread ? lowerArraySpreadItems(lowerer, itemNodes, elem, receiverIr, loc) @@ -875,6 +890,65 @@ export function lowerArrayMethodCall( loc, }; } + // A `(T | undefined)[]` (primitiveElement) takes undefined values as + // present undefined elements, stored after the insertion. + if ( + (name === "push" || name === "unshift") && + primitiveElement(elem) && + call.arguments.nodes.some((a) => checkerTypeAdmitsUndefined(lowerer, a)) + ) { + const nodes = call.arguments.nodes; + const values = nodes.map((a) => lowerer.lowerExpr(a)); + const inserted = insertedValues( + lowerer, + values, + elem, + (value, i) => lowerer.coerceInto(nodes[i]!, value, elem), + loc, + ); + if (inserted.optional) { + const array = lowerer.declareHiddenLocal("%insertInto", receiver.type); + const arr = varRef(array.id, array.type, loc); + const length = lowerer.declareHiddenLocal("%insertLength", F64); + const count: IrExpr = numLit(values.length, loc); + const first: IrExpr = + name === "push" + ? { + kind: "bin", + op: "-", + left: varRef(length.id, F64, loc), + right: count, + type: F64, + loc, + } + : numLit(0, loc); + return { + kind: "seqExpr", + stmts: [ + { kind: "varDecl", localId: array.id, init: receiver, loc }, + ...inserted.stmts, + { + kind: "varDecl", + localId: length.id, + init: { + kind: "arrIntrinsic", + method: name, + receiver: arr, + args: inserted.args, + type: F64, + loc, + }, + loc, + }, + ...inserted.fill(arr, first), + ], + result: varRef(length.id, F64, loc), + type: F64, + loc, + }; + } + return { kind: "arrIntrinsic", method: name, receiver, args: inserted.args, type: F64, loc }; + } // Inserted values flow into the element slot like an assignment would: // union-element arrays wrap plain arm values (coerceInto is inert // when the types already agree). @@ -996,9 +1070,12 @@ export function lowerArrayMethodCall( const hasSpread = itemNodes.some(ts.isSpreadElement); const itemProbes = hasSpread ? [] : itemNodes.map((arg) => tryLowerExpression(lowerer, arg)); const statefulItems = itemProbes.some( - (probe) => probe !== null && lowerer.runtimeOptionalWidening(probe.type, elem) !== null, + (probe) => + probe !== null && + (lowerer.runtimeOptionalWidening(probe.type, elem) !== null || + optionalElementValue(lowerer, probe, elem)), ); - if (statefulItems && itemNodes.length === 1) { + if (statefulItems && itemNodes.length === 1 && itemProbes[0]?.type.kind === "union") { return lowerOptionalSpliceItem( lowerer, receiver, @@ -1081,14 +1158,25 @@ export function lowerArrayMethodCall( (lowerer.unions .get(elem.unionId) ?.arms.every( - (a) => a.kind === "f64" || a.kind === "string" || a.kind === "bool" || isUnitType(a), + (a) => + a.kind === "f64" || + a.kind === "string" || + a.kind === "bool" || + a.kind === "date" || + isUnitType(a), ) ?? false); - if (elem.kind !== "f64" && elem.kind !== "string" && elem.kind !== "bool" && !joinableUnion) { + if ( + elem.kind !== "f64" && + elem.kind !== "string" && + elem.kind !== "bool" && + elem.kind !== "date" && + !joinableUnion + ) { lowerer.unsupported( "SC1090", call, - "'.join()' on arrays of this element type (number, string, and boolean arrays join — unions of those with undefined/null arms too, the units printing empty like JS)", + "'.join()' on arrays of this element type (number, string, boolean and Date arrays join — unions of those with undefined/null arms too, the units printing empty like JS)", ); } const receiver = lowerer.lowerExpr(access.expression); @@ -1723,7 +1811,28 @@ function arraySearchHelper( type: valueT, loc, }; - const value = directNeedle ? rawRead : optionalValue; + // A direct needle matches only a present value (`match` tests the state + // first); a present undefined of a `(T | undefined)[]` (primitiveElement) + // must not be read as T. + const value = !directNeedle + ? optionalValue + : primitiveElement(elem) + ? { + kind: "ternary" as const, + cond: { + kind: "bin" as const, + op: "===" as const, + left: stateRef, + right: numLit(1, loc), + type: BOOL, + loc, + }, + then: rawRead, + else_: elementPlaceholder(elem, loc), + type: elem, + loc, + } + : rawRead; const readT = directNeedle ? elem : valueT; const equal = arraySearchEquality( lowerer, @@ -1777,17 +1886,27 @@ function arraySearchHelper( }, loc, }; - const visitPresent: IrStmt[] = [ - { kind: "varDecl", localId: "value.0", init: value, loc }, - { - kind: "if", - cond: match, - then: [{ kind: "return", value: method === "includes" ? boolLit(true, loc) : indexRef, loc }], - else_: null, - loc, - }, - advance, - ]; + const found: IrStmt = { + kind: "if", + cond: directNeedle ? equal : match, + then: [{ kind: "return", value: method === "includes" ? boolLit(true, loc) : indexRef, loc }], + else_: null, + loc, + }; + // A direct needle reads only value slots: an undefined or foreign + // element written through a dynamic view has no element-typed payload. + const visitPresent: IrStmt[] = directNeedle + ? [ + { + kind: "if", + cond: { kind: "bin", op: "===", left: stateRef, right: numLit(1, loc), type: BOOL, loc }, + then: [{ kind: "varDecl", localId: "value.0", init: value, loc }, found], + else_: null, + loc, + }, + advance, + ] + : [{ kind: "varDecl", localId: "value.0", init: value, loc }, found, advance]; const skipHole: IrStmt[] = method === "lastIndexOf" ? [advance] diff --git a/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts b/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts index 2f839a20b..5cdbe4d31 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts @@ -1,9 +1,15 @@ +import * as ts from "../../ts7/adapter.js"; +import { CONTRACT_TYPES_MODULE } from "../../contract-types.js"; import { keyedHelperName } from "../stable-names.js"; +import { writtenLocals } from "../../../ir/callback-specialization.js"; +import { AmbientReceiverReaders } from "../../../ir/receiver-readers.js"; import { boolLit, countedFor, numLit, varRef } from "../../../ir/build.js"; import { BOOL, + DYN, F64, VOID, + type IrBindingSource, type IrExpr, type IrFunction, type IrGlobal, @@ -16,16 +22,25 @@ import { typeKey, } from "../../../ir/ir.js"; import { + everyExpr, everyExprChild, + everyStmt, everyStmtChild, everyStmtList, + transformExpr, transformStmtList, } from "../../../ir/traverse.js"; import { arrayIndexPresent, arrayValueRead, + arraySlotSnapshot, + arraySnapshotAppend, + arraySnapshotValue, arrayValueStore, arrayValueType, + elementMayBeForeign, + unreadArgument, + type SlotSnapshot, } from "../array-values.js"; import type { Lowerer } from "../lowerer.js"; @@ -53,6 +68,9 @@ export interface ArrayHofInfo { present: boolean; split: boolean; hasInit?: boolean; + /** The callback never reads the element it declares + * (lowerArrayCallback's `unread`). */ + unread?: boolean; } type FuncType = IrType & { kind: "func" }; @@ -148,7 +166,8 @@ const PURE_STRING = new Set([ "toUpperCase", "isWellFormed", "toWellFormed", - "cpAt", + "byteLength", + "cpAtByte", ]); function primitive(t: IrType): boolean { @@ -537,11 +556,76 @@ function matchChain(lowerer: Lowerer, e: IrExpr): Chain | null { }; } +/** The outermost array-HOF call of `e` on its own, over its receiver. */ +function loneChain(lowerer: Lowerer, e: IrExpr): Chain | null { + const top = stageOf(lowerer, e); + if (!top) return null; + const { stage, receiver } = top; + if (receiver.type.kind !== "array") return null; + const stages: Stage[] = []; + let terminal: Terminal; + if (TERMINALS.has(stage.info.method)) { + const init = + stage.info.method === "reduce" && stage.info.hasInit && e.kind === "call" ? e.args[2]! : null; + terminal = { kind: "call", stage, init }; + } else if (PRODUCERS.has(stage.info.method)) { + terminal = { kind: "collect" }; + stages.push(stage); + } else return null; + return { + source: receiver, + sourceType: receiver.type, + slice: null, + stages, + terminal, + type: e.type, + loc: e.loc, + }; +} + +/** The callbacks of `chain` in order, or null when its stages cannot run + * in one loop: element types that do not line up, an array argument past + * the first stage, reduce without a seed, or findIndex over holes. */ +function chainCallbacks(chain: Chain): Stage[] | null { + const { stages, terminal } = chain; + const terminalStage = terminal.kind === "call" ? terminal.stage : null; + const callbacks = terminalStage ? [...stages, terminalStage] : stages; + // Element types must line up stage to stage. + let elem = chain.sourceType.elem; + for (const stage of callbacks) { + if (!typeEquals(stage.info.elem, elem)) return null; + if (stage.info.method === "filter" && !typeEquals(stage.info.outElem, stage.info.elem)) + return null; + if ( + (stage.info.method === "map" || stage.info.method === "flatMap") && + !typeEquals(stage.info.outElem, stage.info.fnRet) + ) + return null; + elem = stage.info.outElem; + } + // Index arguments are reproduced; an array argument only names the source. + for (let k = 0; k < callbacks.length; k++) { + const stage = callbacks[k]!; + const lead = stage.info.method === "reduce" ? 1 : 0; + if (stage.info.arity > lead + 3) return null; + if (stage.info.arity === lead + 3 && (k !== 0 || chain.slice)) return null; + } + if (terminalStage?.info.method === "reduce" && !terminalStage.info.hasInit) return null; + if (terminalStage?.info.method === "findIndex" && holeCarrying(chain)) return null; + return callbacks; +} + /* ── the fusion pass ─────────────────────────────────────────────────── */ /** The values a fused loop hands from one stage to the next: a local of a * known type, or (before the first read) the source slot itself. */ -type Flow = { kind: "value"; id: string; type: IrType } | { kind: "slot" }; +/** What reaches a stage: a value, the source slot not yet read, or a + * snapshot of the slot read before an earlier callback that ignored it + * (arraySlotSnapshot). */ +type Flow = + | { kind: "value"; id: string; type: IrType } + | { kind: "slot" } + | { kind: "snapshot"; ids: SlotSnapshot }; interface Plan { chain: Chain; @@ -550,6 +634,10 @@ interface Plan { writes: Set; /** Some callback may throw or loop; at most one does. */ risky: boolean; + /** How the loop inlines callbacks, when not by their effect summaries. */ + inline?: InlineTarget; + /** Stack frame name of the loop: the inlined callback's, for a lone call. */ + frame?: string; } /** Fuse chains of array-HOF helper calls into one loop. @@ -596,12 +684,35 @@ class PipelineFusion { readonly added: IrFunction[] = []; private readonly effects: CallbackEffects; private readonly helpers = new Map(); + /** Bindings each function, or a closure it creates, writes after their + * declaration. */ + private readonly written: Map>; + /** classCaptured's answers by function name. */ + private readonly classCaptures = new Map>(); + /** Whether a module imports or re-exports the contract types. */ + private readonly contractTypes: boolean; + /** Bindings of the function being fused that inlined callbacks captured. */ + private inlinedCaptures = new Set(); + /** The helper calls each callback with an undefined receiver; a copied + * body runs under the receiver around the array call instead. */ + private readonly receivers: AmbientReceiverReaders; constructor( private readonly lowerer: Lowerer, functions: readonly IrFunction[], ) { this.effects = new CallbackEffects(functions, lowerer.globalsList); + this.written = writtenLocals({ functions }); + this.receivers = new AmbientReceiverReaders(new Map(functions.map((fn) => [fn.name, fn]))); + this.contractTypes = lowerer.moduleOrder.some((sf) => + sf.statements.nodes.some( + (st) => + (ts.isImportDeclaration(st) || ts.isExportDeclaration(st)) && + st.moduleSpecifier !== undefined && + ts.isStringLiteral(st.moduleSpecifier) && + st.moduleSpecifier.text === CONTRACT_TYPES_MODULE, + ), + ); } run(fn: IrFunction): void { @@ -612,6 +723,7 @@ class PipelineFusion { }); if (!candidate) return; const removed = new Set(); + this.inlinedCaptures = new Set(); let body = this.lists(fn.body, fn, removed); body = transformStmtList(body, { stmt: (s) => s, @@ -619,6 +731,7 @@ class PipelineFusion { }); fn.body = body; if (removed.size > 0) fn.locals = fn.locals.filter((l) => !removed.has(l.id)); + if (this.inlinedCaptures.size > 0) unboxUncaptured(fn, this.inlinedCaptures); } /* ── statement lists: fold single-use chain bindings ── */ @@ -733,40 +846,51 @@ class PipelineFusion { /* ── expressions ── */ private fuse(e: IrExpr, fn: IrFunction): IrExpr | null { - const plan = this.plan(e, fn); + const plan = this.plan(e, fn) ?? this.lonePlan(e, fn); return plan ? this.emit(plan) : null; } + /** One array-HOF call on its own (or the last call of a chain that does + * not fuse), when every callback is a closure literal whose body can be + * copied into the loop. A single stage reorders nothing, so the + * callbacks' effects need no model; their captured bindings must keep + * one value while the loop runs (stableTarget), since the loop reads + * them once, when it starts. The loop keeps the helper's order: the + * length read once, each element read when it is visited, holes + * skipped (and kept by map). */ + private lonePlan(e: IrExpr, fn: IrFunction): Plan | null { + const chain = loneChain(this.lowerer, e); + if (!chain) return null; + const callbacks = chainCallbacks(chain); + if (!callbacks) return null; + const inline = this.stableTarget(fn); + let frame: string | undefined; + for (const stage of callbacks) { + for (const arg of [stage.slow, stage.fast]) { + if (!arg) continue; + const target = inline(arg); + if (!target) return null; + frame ??= target.sourceName; + } + } + return { + chain, + callbacks, + writes: new Set(), + risky: true, + inline, + ...(frame === undefined ? {} : { frame }), + }; + } + /** Decide whether `e` is a fusable chain; null when it is not. */ private plan(e: IrExpr, fn: IrFunction): Plan | null { const chain = matchChain(this.lowerer, e); if (!chain) return null; + const callbacks = chainCallbacks(chain); + if (!callbacks) return null; const { stages, terminal } = chain; const terminalStage = terminal.kind === "call" ? terminal.stage : null; - const callbacks = terminalStage ? [...stages, terminalStage] : stages; - // Element types must line up stage to stage. - let elem = chain.sourceType.elem; - for (const stage of callbacks) { - if (!typeEquals(stage.info.elem, elem)) return null; - if (stage.info.method === "filter" && !typeEquals(stage.info.outElem, stage.info.elem)) - return null; - if ( - (stage.info.method === "map" || stage.info.method === "flatMap") && - !typeEquals(stage.info.outElem, stage.info.fnRet) - ) - return null; - elem = stage.info.outElem; - } - // Index arguments are reproduced; an array argument only names the source. - for (let k = 0; k < callbacks.length; k++) { - const stage = callbacks[k]!; - const lead = stage.info.method === "reduce" ? 1 : 0; - if (stage.info.arity > lead + 3) return null; - if (stage.info.arity === lead + 3 && (k !== 0 || chain.slice)) return null; - } - if (terminalStage?.info.method === "reduce" && !terminalStage.info.hasInit) return null; - const holes = holeCarrying(chain); - if (terminalStage?.info.method === "findIndex" && holes) return null; // Effects of each callback, in chain order. const scope = this.effects.scope(fn, true); const summaries: Effects[] = []; @@ -831,30 +955,89 @@ class PipelineFusion { * are passed in by value, read when the fused call starts; no callback * in the chain assigns a binding another one reads). */ private readonly inlineTarget: InlineTarget = (arg) => { + const fn = this.closureTarget(arg); + if (!fn) return null; + const captures = new Set((fn.captures ?? []).map((c) => c.localId)); + const effects = this.effects.ofFunction(fn); + if (!effects || intersects(effects.writes, captures)) return null; + return inlinableBody(fn, captures) ? fn : null; + }; + + /** inlineTarget for a callback created in `fn` whose captured bindings + * nothing assigns after their declaration: `fn`'s own locals and + * parameters, written neither by `fn` nor by any closure that shares + * them, and not by the callback body. A binding a local class captures + * counts as written, since its methods may assign it; so does a binding + * a method reads through its class's captures. The callback must + * not observe the ambient receiver (AmbientReceiverReaders). */ + private stableTarget(fn: IrFunction): InlineTarget { + const locals = new Map(fn.locals.map((l) => [l.id, l])); + const inherited = new Set( + [...(fn.captures ?? []), ...(fn.classCaptures ?? [])].map((c) => c.localId), + ); + const written = this.written.get(fn.name); + const classCaptured = this.classCaptured(fn); + return (arg) => { + const target = this.closureTarget(arg); + if (!target || !written || arg.kind !== "closure") return null; + if (!this.receivers.ignores(target)) return null; + for (const id of arg.captures) { + const local = locals.get(id); + if (!local || local.tdz || inherited.has(id) || written.has(id) || classCaptured.has(id)) + return null; + } + const captures = new Set((target.captures ?? []).map((c) => c.localId)); + const assigns = !everyStmtList(target.body, { + stmt: (st) => !((st.kind === "assign" || st.kind === "forOf") && captures.has(st.localId)), + expr: (ex) => + !((ex.kind === "assignExpr" || ex.kind === "incDec") && captures.has(ex.localId)), + }); + return !assigns && inlinableBody(target, captures) ? target : null; + }; + } + + /** The bindings of `fn` that a local class it creates captures. */ + private classCaptured(fn: IrFunction): ReadonlySet { + const known = this.classCaptures.get(fn.name); + if (known) return known; + const ids = new Set(); + everyStmtList(fn.body, { + expr: (e) => { + if (e.kind === "classRef") for (const id of e.captures ?? []) ids.add(id); + return true; + }, + stmt: () => true, + }); + this.classCaptures.set(fn.name, ids); + return ids; + } + + /** The function behind a closure-literal callback the loop can inline. */ + private closureTarget(arg: IrExpr): IrFunction | null { if (arg.kind !== "closure" || arg.adapts) return null; const fn = this.effects.fn(arg.fnName); if (!fn || fn.async || fn.generator || fn.classCaptures?.length) return null; - const captures = new Set((fn.captures ?? []).map((c) => c.localId)); - if (captures.size !== arg.captures.length) return null; - const effects = this.effects.ofFunction(fn); - if (!effects || intersects(effects.writes, captures)) return null; - if (fn.locals.some((l) => l.tdz || (l.boxed && !captures.has(l.id)))) return null; - let size = 0; - const small = everyStmtList(fn.body, { - expr: (e) => ++size <= MAX_INLINE_NODES && e.kind !== "closure" && e.kind !== "selfRef", - stmt: (s) => ++size <= MAX_INLINE_NODES && s.kind !== "tryCatch", - }); - return small ? fn : null; - }; + if ((fn.captures ?? []).length !== arg.captures.length) return null; + return fn; + } private emit(plan: Plan): IrExpr | null { - const built = new FusedLoop(this.lowerer, plan.chain, this.inlineTarget).build(); + const built = new FusedLoop( + this.lowerer, + plan.chain, + plan.inline ?? this.inlineTarget, + this.contractTypes, + ).build(); if (!built) return null; + for (const id of built.inlinedCaptures) this.inlinedCaptures.add(id); let name = this.helpers.get(built.key); if (!name) { - name = keyedHelperName(this.lowerer, "%arr.fused", built.key); + name = keyedHelperName(this.lowerer, plan.inline ? "%arr.loop" : "%arr.fused", built.key); this.helpers.set(built.key, name); - this.added.push({ ...built.fn, name }); + const helper: IrFunction = { ...built.fn, name }; + // A lone call's loop stands in the inlined callback's stack frame. + if (plan.frame !== undefined) helper.sourceName = plan.frame; + this.added.push(helper); } return { kind: "call", @@ -866,6 +1049,48 @@ class PipelineFusion { } } +/** Whether a statement inside `e` (a sequence expression's) returns. */ +function returns(e: IrExpr): boolean { + return !everyExpr(e, { expr: () => true, stmt: (s) => s.kind !== "return" }); +} + +/** Drops the box of each binding in `inlined` that no closure or class + * created in `fn` captures any more: the callbacks that captured it were + * copied into loops, which receive its value. A dead-zone binding keeps the + * box that represents its uninitialized state, and bindings `fn` itself + * captures keep theirs. */ +function unboxUncaptured(fn: IrFunction, inlined: ReadonlySet): void { + const captured = new Set(); + everyStmtList(fn.body, { + expr: (e) => { + if (e.kind === "closure" || e.kind === "classRef") + for (const id of e.captures ?? []) captured.add(id); + return true; + }, + stmt: () => true, + }); + const inherited = new Set( + [...(fn.captures ?? []), ...(fn.classCaptures ?? [])].map((c) => c.localId), + ); + fn.locals = fn.locals.map((local): IrLocal => { + if (local.boxed !== true || local.tdz === true || !inlined.has(local.id)) return local; + if (captured.has(local.id) || inherited.has(local.id)) return local; + const { boxed: _boxed, ...plain } = local; + return plain; + }); +} + +/** A callback body the loop can copy: small, no nested closures, recursion + * or try statements, and no boxed locals besides its captures. */ +function inlinableBody(fn: IrFunction, captures: ReadonlySet): boolean { + if (fn.locals.some((l) => l.tdz || (l.boxed && !captures.has(l.id)))) return false; + let size = 0; + return everyStmtList(fn.body, { + expr: (e) => ++size <= MAX_INLINE_NODES && e.kind !== "closure" && e.kind !== "selfRef", + stmt: (s) => ++size <= MAX_INLINE_NODES && s.kind !== "tryCatch", + }); +} + function intersects(a: ReadonlySet, b: ReadonlySet): boolean { for (const id of a) if (b.has(id)) return true; return false; @@ -988,10 +1213,13 @@ class FusedLoop { private next = 0; private failed = false; + /** `contractTypes`: the program writes contract types, which the + * contract checker binds to source bindings. */ constructor( private readonly lowerer: Lowerer, private readonly chain: Chain, private readonly inlineTarget: InlineTarget, + private readonly contractTypes: boolean, ) { this.loc = chain.loc; } @@ -1000,9 +1228,9 @@ class FusedLoop { return `${name}.${this.next++}`; } - private local(name: string, type: IrType, mutable = false): string { + private local(name: string, type: IrType, mutable = false, source?: IrBindingSource): string { const id = this.fresh(name); - this.locals.push({ id, name, type, mutable }); + this.locals.push(source ? { id, name, type, mutable, source } : { id, name, type, mutable }); return id; } @@ -1019,7 +1247,9 @@ class FusedLoop { private callee(arg: IrExpr & { type: FuncType }): Callee { const known = this.callees.find((c) => c.arg === arg); if (known) return known; - const target = this.inlineTarget(arg); + // Called as a plain function: the loop is not the target's receiver. + const inlineTarget = this.inlineTarget; + const target = inlineTarget(arg); let callee: Callee; if (target && arg.kind === "closure") { const captures = new Map(); @@ -1067,21 +1297,50 @@ class FusedLoop { return true; }, }); + // Copies keep their source bindings, so contract types written on + // them are checked against what the loop stores. In a program that + // writes contract types, a parameter with a source binding keeps its + // own local for the same reason. + const sources = new Map(fn.locals.map((l) => [l.id, l.source])); const aliased = new Set(); fn.params.forEach((p, index) => { const arg = args[index]!; - if (arg.kind === "varRef" && !assigned.has(p.localId) && typeEquals(arg.type, p.type)) { + if ( + arg.kind === "varRef" && + !assigned.has(p.localId) && + typeEquals(arg.type, p.type) && + !(this.contractTypes && sources.get(p.localId) !== undefined) + ) { renamed.set(p.localId, arg.localId); aliased.add(p.localId); } }); for (const l of fn.locals) { if (renamed.has(l.id)) continue; - const id = this.local(l.name, l.type, true); + const id = this.local(l.name, l.type, true, l.source); renamed.set(l.id, id); } + const rename = (id: string): string => renamed.get(id) ?? id; + const renameExpr = (e: IrExpr): IrExpr => { + switch (e.kind) { + case "varRef": + case "incDec": + case "assignExpr": + return { ...e, localId: rename(e.localId) }; + default: + return e; + } + }; + // A bound parameter's value is an element or index with no source text + // of its own; it is located at the parameter, without a span. + const bind: IrStmt[] = fn.params.flatMap((p, index): IrStmt[] => { + if (aliased.has(p.localId)) return []; + const source = sources.get(p.localId); + const arg = args[index]!; + const init = source ? { ...arg, loc: { ...source.loc, end: source.loc.start } } : arg; + return [{ kind: "varDecl", localId: rename(p.localId), init, loc }]; + }); const labels = new Map(); - const exit = this.fresh("%fused.exit"); const label = (name: string): string => { let id = labels.get(name); if (!id) { @@ -1090,19 +1349,53 @@ class FusedLoop { } return id; }; - const result = fn.returnType.kind === "void" ? null : this.local("ret", fn.returnType, true); - const rename = (id: string): string => renamed.get(id) ?? id; - const body = transformStmtList(fn.body, { - expr: (e) => { - switch (e.kind) { - case "varRef": - case "incDec": - case "assignExpr": - return { ...e, localId: rename(e.localId) }; - default: - return e; + /** Locals and labels of a copied statement; returns are the caller's. */ + const renameStmt = (st: IrStmt): IrStmt => { + switch (st.kind) { + case "varDecl": + case "assign": + return { ...st, localId: rename(st.localId) }; + case "break": + case "continue": + return st.label ? { ...st, label: label(st.label) } : st; + case "forOf": { + const renamedFor = { ...st, localId: rename(st.localId) }; + return st.labels ? { ...renamedFor, labels: st.labels.map(label) } : renamedFor; } - }, + case "while": + case "doWhile": + case "for": + case "switch": + case "block": + return st.labels ? { ...st, labels: st.labels.map(label) } : st; + default: + return st; + } + }; + // A body whose only return is its last statement (`(x) => x * k`, + // `{ const y = x * 2; return y > 4; }`) needs no exit block: its other + // statements run after the bindings, and the call's value is the + // returned expression. + const last = fn.body[fn.body.length - 1]; + const prefix = fn.body.slice(0, -1); + if ( + last !== undefined && + last.kind === "return" && + last.value !== null && + typeEquals(last.value.type, fn.returnType) && + !returns(last.value) && + prefix.every((st) => everyStmt(st, { expr: () => true, stmt: (x) => x.kind !== "return" })) + ) { + const copy = { expr: renameExpr, stmt: renameStmt }; + return { + pre: [...bind, ...transformStmtList(prefix, copy)], + value: transformExpr(last.value, copy), + }; + } + const exit = this.fresh("%fused.exit"); + const result = fn.returnType.kind === "void" ? null : this.local("ret", fn.returnType, true); + const body = transformStmtList(fn.body, { + expr: renameExpr, stmt: (s): IrStmt => { switch (s.kind) { case "return": { @@ -1122,27 +1415,14 @@ class FusedLoop { loc: s.loc, }; } - case "varDecl": - case "assign": - return { ...s, localId: rename(s.localId) }; case "break": case "continue": return s.label && s.label !== exit ? { ...s, label: label(s.label) } : s; - case "while": - case "doWhile": - case "for": - case "block": - return s.labels ? { ...s, labels: s.labels.map(label) } : s; default: - return s; + return renameStmt(s); } }, }); - const bind: IrStmt[] = fn.params.flatMap((p, index): IrStmt[] => - aliased.has(p.localId) - ? [] - : [{ kind: "varDecl", localId: rename(p.localId), init: args[index]!, loc }], - ); const pre: IrStmt[] = []; if (result !== null) pre.push({ kind: "varDecl", localId: result, init: null, loc }); pre.push({ kind: "block", labels: [exit], body: [...bind, ...body], loc }); @@ -1155,7 +1435,13 @@ class FusedLoop { }; } - build(): { fn: Omit; args: IrExpr[]; key: string } | null { + build(): { + fn: Omit; + args: IrExpr[]; + key: string; + /** Bindings the callbacks copied into the loop captured at this site. */ + inlinedCaptures: string[]; + } | null { const { chain, loc } = this; const arrT = chain.sourceType; const a = this.param("a", chain.source, "src"); @@ -1324,13 +1610,21 @@ class FusedLoop { stage: Stage, k: number, lead: IrExpr[], - flow: Flow & { kind: "value" }, + flow: Flow, ): { pre: IrStmt[]; value: IrExpr } | null => { const arity = stage.info.arity; - const value = varRef(flow.id, flow.type, loc); const at = lead.length; const pick = (): { arg: IrExpr & { type: FuncType }; value: IrExpr | null } | null => { if (arity <= at) return { arg: stage.slow, value: null }; + // A callback that reads the value reaches it read (`read`); one + // that never reads it takes a placeholder for the unread slot. + if (flow.kind !== "value") { + const placeholder = stage.info.unread + ? unreadArgument(this.lowerer, stage.slow.type.params[at]!, loc) + : null; + return placeholder ? { arg: stage.slow, value: placeholder } : null; + } + const value = varRef(flow.id, flow.type, loc); const fast = stage.fast; if (fast && typeEquals(fast.type.params[at]!, flow.type)) return { arg: fast, value }; const paramT = stage.slow.type.params[at]!; @@ -1359,11 +1653,62 @@ class FusedLoop { return { pre: [...before, ...called.pre], value: called.value }; }; + /** The stage's callback never reads the element at parameter `at`, and + * a placeholder of its type stands for it (unreadArgument). */ + const unreadSlot = (stage: Stage, at: number): boolean => + stage.info.unread === true && + unreadArgument(this.lowerer, stage.slow.type.params[at]!, loc) !== null; + const fail = (): IrStmt[] => { this.failed = true; return []; }; + /** Bring `flow` to the callback of `stage`, whose element parameter is + * at `at`. A slot is read for a callback that reads it. For one that + * ignores it, the slot of an array that can hold elements outside its + * element type is read as stored before the callback runs (a snapshot, + * as JavaScript reads the element first), so such an element passes the + * callback as it is; other arrays read the slot as usual. A snapshot + * reaching a callback that reads it becomes its value, and an element + * outside the element type takes the ordinary read's checked + * conversion there. */ + const reach = ( + stage: Stage, + at: number, + flow: Flow, + rest: (flow: Flow) => IrStmt[], + ): IrStmt[] => { + const reads = stage.info.arity > at && !unreadSlot(stage, at); + if (flow.kind === "slot") { + if (reads || !elementMayBeForeign(this.lowerer, arrT.elem)) + return read(stage, (value) => rest(value)); + const ids: SlotSnapshot = { + isForeign: this.local("sf", BOOL), + value: this.local("sv", arrayValueType(this.lowerer, arrT.elem)), + foreign: this.local("sd", DYN), + }; + return [ + ...arraySlotSnapshot(this.lowerer, a, i, arrT.elem, ids, loc), + ...rest({ kind: "snapshot", ids }), + ]; + } + if (flow.kind === "snapshot" && reads) { + const valueT = arrayValueType(this.lowerer, arrT.elem); + const u = this.local("u", valueT); + return [ + { + kind: "varDecl", + localId: u, + init: arraySnapshotValue(this.lowerer, arrT.elem, flow.ids, valueT, (v) => v, loc), + loc, + }, + ...rest({ kind: "value", id: u, type: valueT }), + ]; + } + return rest(flow); + }; + /** Turn the source slot into a value for the callback of `stage`. */ const read = (stage: Stage, rest: (flow: Flow & { kind: "value" }) => IrStmt[]): IrStmt[] => { if (stage.info.present) { @@ -1374,7 +1719,9 @@ class FusedLoop { ]; } const valueT = arrayValueType(this.lowerer, arrT.elem); - if (stage.fast) { + // A foreign element is neither the plain element nor undefined: the + // ordinary read below converts it. + if (stage.fast && !elementMayBeForeign(this.lowerer, arrT.elem)) { // The split visit: a value goes on as the plain element, a present // undefined as the undefined arm. const missing = this.lowerer.wrappedUndefined(valueT, loc); @@ -1417,11 +1764,12 @@ class FusedLoop { }; /** The statements that run one value through stages k.. and the terminal. */ - const visit = (k: number, flow: Flow): IrStmt[] => { + const visit = (k: number, flow: Flow, reached = false): IrStmt[] => { if (k === chain.stages.length) return finish(flow); const stage = chain.stages[k]!; const info = stage.info; - if (flow.kind === "slot") return read(stage, (value) => visit(k, value)); + if (flow.kind !== "value" && !reached) + return reach(stage, 0, flow, (next) => visit(k, next, true)); const called = deliver(stage, k, [], flow); if (!called) return fail(); const { pre, value: result } = called; @@ -1518,11 +1866,23 @@ class FusedLoop { } }; + /** Append a snapshot of the source slot to the collected result: a + * filter whose callback ignored the element keeps it as it was stored + * (arraySnapshotAppend). */ + const copySlot = (flow: Flow & { kind: "snapshot" }): IrStmt[] => { + const outT = resultT as IrType & { kind: "array" }; + if (holes || !typeEquals(outT.elem, arrT.elem)) return fail(); + return [ + arraySnapshotAppend(this.lowerer, varRef(out!, resultT, loc), arrT.elem, flow.ids, loc), + ]; + }; /** The terminal's action for one value. */ - const finish = (flow: Flow): IrStmt[] => { + const finish = (flow: Flow, reached = false): IrStmt[] => { if (terminal.kind === "length") return [increment(count!, loc)]; if (terminal.kind === "collect") { - if (flow.kind !== "value") return fail(); + if (flow.kind === "snapshot") return copySlot(flow); + if (flow.kind === "slot") + return read(chain.stages[chain.stages.length - 1]!, (value) => finish(value)); const last = chain.stages[chain.stages.length - 1]!; const outT = resultT as IrType & { kind: "array" }; if (!typeEquals(outT.elem, last.info.outElem)) return fail(); @@ -1543,7 +1903,9 @@ class FusedLoop { } const stage = terminal.stage; const method = stage.info.method; - if (flow.kind === "slot") return read(stage, (value) => finish(value)); + const lead = method === "reduce" ? 1 : 0; + if (flow.kind !== "value" && !reached) + return reach(stage, lead, flow, (next) => finish(next, true)); if (method === "reduce") { const accT = stage.info.outElem; const called = deliver(stage, chain.stages.length, [varRef(acc!, accT, loc)], flow); @@ -1640,7 +2002,10 @@ class FusedLoop { }; foldKnownUnions(fn); scalarizeRecordTemporaries(this.lowerer, fn); - return { fn, args: this.args, key: this.keyParts.join("|") }; + const inlinedCaptures = this.callees.flatMap((c) => + c.inline && c.arg.kind === "closure" ? c.arg.captures : [], + ); + return { fn, args: this.args, key: this.keyParts.join("|"), inlinedCaptures }; } } @@ -1739,8 +2104,11 @@ function foldKnownUnions(fn: Omit): void { case "for": case "while": case "doWhile": + case "forOf": case "block": return { ...s, body: prune(s.body) }; + case "switch": + return { ...s, cases: s.cases.map((c) => ({ ...c, body: prune(c.body) })) }; default: return s; } @@ -1928,6 +2296,19 @@ function scalarizeRecordTemporaries(lowerer: Lowerer, fn: Omit ({ + test: c.test === null ? null : rewriteExpr(c.test), + body: rewriteList(c.body), + })), + }, + ]; case "block": return [{ ...s, body: rewriteList(s.body) }]; default: { diff --git a/packages/compiler/src/frontend/lowering/containers/bytes.ts b/packages/compiler/src/frontend/lowering/containers/bytes.ts index 995cb177a..a8890c431 100644 --- a/packages/compiler/src/frontend/lowering/containers/bytes.ts +++ b/packages/compiler/src/frontend/lowering/containers/bytes.ts @@ -1,7 +1,9 @@ import { keyedHelperName } from "../stable-names.js"; -import { boolLit, dynUndefinedExpr, numLit, varRef } from "../../../ir/build.js"; +import { allBytesArms, indexedUnionArms, lowerIndexedUnionMethod } from "../indexed-unions.js"; +import { boolLit, dynUndefinedExpr, nodeThrowExpr, numLit, varRef } from "../../../ir/build.js"; import { BUF_NUM_METHODS } from "./buffer-numeric-methods.js"; import * as ts from "../../ts7/adapter.js"; +import { constituentTypes } from "../../ts7/checker.js"; import { BIGINT_T, BOOL, @@ -15,6 +17,7 @@ import { type IrLibFn, type IrLocal, type IrParam, + type IrFunction, type IrStmt, type IrType, STRING, @@ -30,7 +33,8 @@ import { import { isJsSourceFile, locOf } from "../../program.js"; import { stdlibGlobalNameOf } from "../surfaces.js"; import type { Lowerer } from "../lowerer.js"; -import { newFnCtx, own } from "../lowerer.js"; +import { ENTRY_NAME, newFnCtx, own } from "../lowerer.js"; +import { everyStmt } from "../../../ir/traverse.js"; import { buildBytesSortFn, closureKeyOrder } from "../lower-array-sort.js"; import { isSafeToDiscard } from "../expressions/evaluation-safety.js"; import { lowerDynObjectLiteral } from "../expressions/object-literals.js"; @@ -40,7 +44,7 @@ import { lowerStaticallyUndefinedArgument, } from "../optional-arguments.js"; import { lowerArraySpreadItems, lowerCheckedArrayFrom } from "./array-construction.js"; -import { lowerToNumberArgument } from "../lower-exprs.js"; +import { lowerOptionalNumber, lowerToNumberArgument } from "../lower-exprs.js"; import { lowerBytesIteratorValue } from "../iterator-adapters.js"; import { lowerArrayCallback } from "./callback-arguments.js"; import { callbackSite, findPredicateOk, predicateToBoolean } from "./array-callbacks.js"; @@ -903,6 +907,126 @@ function lowerDataViewNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { return lowerArrayBufferView(lowerer, args, "dv", locOf(expr)); } +/** `receiver.set(src, offset?)` on a lowered typed-array receiver. */ +function bytesSetIntrinsic( + lowerer: Lowerer, + receiver: IrExpr, + argNodes: readonly ts.Expression[], + loc: SrcLoc, +): IrExpr { + const src = lowerer.lowerExpr(argNodes[0]!); + const method = src.type.kind === "bytes" ? "setFrom" : "setFromDyn"; + const args = [src.type.kind === "bytes" ? src : lowerer.coerceInto(argNodes[0]!, src, DYN)]; + if (argNodes.length === 2) + args.push( + lowerOptionalArgument(lowerer, argNodes[1]!, F64, { + kind: "numLit", + value: 0, + type: F64, + loc, + }), + ); + return { kind: "bytesIntrinsic", method, receiver, args, type: VOID, loc }; +} + +/** `Uint8Array.prototype.set.call(target, src, offset?)`, through any typed + * array constructor's prototype (they share %TypedArray%.prototype.set). A + * target the checker types as a typed array or Buffer lowers like + * `target.set(src, offset)`. Any other target, a DataView included, is + * checked at run time: a typed array calls its `set`, and + * anything else throws Node's TypeError "this is not a typed array.", + * after every argument is evaluated. */ +export function lowerBytesPrototypeSetCall( + lowerer: Lowerer, + call: ts.CallExpression, + access: ts.PropertyAccessExpression, +): IrExpr | null { + if ( + call.questionDotToken || + access.questionDotToken || + access.name.text !== "call" || + !ts.isPropertyAccessExpression(access.expression) + ) + return null; + const method = access.expression; + const proto = method.expression; + if ( + method.questionDotToken || + method.name.text !== "set" || + !ts.isPropertyAccessExpression(proto) || + proto.questionDotToken || + proto.name.text !== "prototype" || + !ts.isIdentifier(proto.expression) || + !lowerer.isStdlibMember(method) + ) + return null; + const ctor = lowerer.resolveValueSymbol(proto.expression); + if (!ctor || !lowerer.isStdlibSymbol(ctor) || !own(BYTES_CTORS, ts.symbolName(ctor))) return null; + const argNodes = call.arguments.nodes; + if (argNodes.length < 2 || argNodes.length > 3 || argNodes.some(ts.isSpreadElement)) return null; + const loc = locOf(call); + const target = lowerer.lowerExpr(argNodes[0]!); + // A DataView lowers to the same bytes type, so the checker type decides: + // only a typed array (or a Buffer) takes the method's path. + const targetSymbol = lowerer.typeOf(argNodes[0]!).getSymbol(); + if ( + target.type.kind === "bytes" && + targetSymbol !== undefined && + lowerer.isStdlibSymbol(targetSymbol) && + (own(BYTES_CTORS, ts.symbolName(targetSymbol)) !== undefined || + ts.symbolName(targetSymbol) === "Buffer") + ) + return bytesSetIntrinsic(lowerer, target, argNodes.slice(1), loc); + const slot = (value: IrExpr): { decl: IrStmt; read: IrExpr } => { + const local = lowerer.declareHiddenLocal("%setArg", DYN); + return { + decl: { kind: "varDecl", localId: local.id, init: value, loc }, + read: varRef(local.id, DYN, loc), + }; + }; + const slots = [ + slot(lowerer.coerceInto(argNodes[0]!, target, DYN)), + ...argNodes.slice(1).map((arg) => slot(lowerer.lowerExprExpecting(arg, DYN))), + ]; + const isTypedArray: IrExpr = { + kind: "libCall", + fn: "util.typeIs", + args: [slots[0]!.read, { kind: "strLit", value: "isTypedArray", type: STRING, loc }], + type: BOOL, + loc, + }; + return { + kind: "seqExpr", + stmts: [ + ...slots.map((s) => s.decl), + { + kind: "if", + cond: { kind: "unary", op: "!", operand: isTypedArray, type: BOOL, loc }, + then: [ + { + kind: "exprStmt", + expr: nodeThrowExpr(1, "", "this is not a typed array.", VOID, loc), + loc, + }, + ], + else_: null, + loc, + }, + ], + result: { + kind: "dynInvoke", + recv: slots[0]!.read, + method: "set", + calleeName: ts.getTextOfNode(method), + args: slots.slice(1).map((s) => s.read), + type: DYN, + loc, + }, + type: DYN, + loc, + }; +} + /** Method calls on typed-array/Buffer receivers: slice (copy), subarray * (shared view), toReversed, copyWithin, set(src, * offset?), and the u8-only Buffer surface: toString(enc?) plus the @@ -910,6 +1034,119 @@ function lowerDataViewNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { * variable-width read/writeUIntLE quartet — BUF_NUM_METHODS). Everything * else the lib declares (fill, indexOf, reverse, ...) falls through to * the SC2020 member fence. Null when this isn't a bytes method call. */ +/** The [needle, fromIndex] arguments of TypedArray indexOf, lastIndexOf + * and includes. A present fromIndex converts even when it is undefined + * (NaN, which ToIntegerOrInfinity makes 0); an absent one is 0, or + * +Infinity for lastIndexOf, which starts at the last element. */ +function elementSearchArgs(lowerer: Lowerer, call: ts.CallExpression, name: string): IrExpr[] { + const nodes = call.arguments.nodes; + const loc = locOf(call); + const needle = lowerer.lowerExprExpecting(nodes[0]!, F64); + const from: IrExpr = nodes[1] + ? lowerOptionalNumber(lowerer, lowerer.lowerExpr(nodes[1]), locOf(nodes[1]), nodes[1]) + : ({ + kind: "numLit", + value: name === "lastIndexOf" ? Infinity : 0, + type: F64, + loc, + } satisfies IrExpr); + // An undefined fromIndex is NaN once converted, which starts at 0. + if (from.kind === "unitLit") return [needle, { kind: "numLit", value: NaN, type: F64, loc }]; + if (from.type.kind === "undefinedT" || from.type.kind === "void") + return [ + needle, + { + kind: "seqExpr", + stmts: [{ kind: "exprStmt", expr: from, loc }], + result: { kind: "numLit", value: NaN, type: F64, loc }, + type: F64, + loc, + }, + ]; + if (from.type.kind !== "f64") lowerer.noLowering(`.${name} with a non-number fromIndex`, call); + return [needle, from]; +} + +const ELEMENT_SEARCHES: Readonly> = { + indexOf: "indexOfElem", + lastIndexOf: "lastIndexOfElem", + includes: "includesElem", +}; + +/** Whether a union has a Buffer arm, under any alias name: each arm's own + * symbol names it. */ +function hasBufferArm(type: ts.Type): boolean { + const arms = type.flags & ts.TypeFlags.Union ? constituentTypes(type) : [type]; + return arms.some((arm) => { + const symbol = arm.getSymbol(); + return symbol !== undefined && ts.symbolName(symbol) === "Buffer"; + }); +} + +/** The element-kind-generic typed-array methods on a union of typed + * arrays (indexed-unions.ts): the arguments lower as for one typed array, + * and each arm runs the method on its own receiver. Null for the methods + * that need a narrowing to one kind first. */ +function lowerUnionBytesMethodCall( + lowerer: Lowerer, + call: ts.CallExpression, + access: ts.PropertyAccessExpression, + name: string, +): IrExpr | null { + if (isJsSourceFile(ts.getSourceFileOfNode(call))) return null; + // A Buffer arm's methods differ from a typed array's (slice aliases, fill + // checks its range, searches take strings and wrap numbers), so such a + // union narrows to its arms first. + if (hasBufferArm(lowerer.typeOf(access.expression))) return null; + const nodes = call.arguments.nodes; + if (nodes.some(ts.isSpreadElement)) return null; + const loc = locOf(call); + const num = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc }); + const optional = (i: number, fallback: IrExpr, type: IrType = F64): IrExpr => + nodes[i] ? lowerOptionalArgument(lowerer, nodes[i]!, type, fallback) : fallback; + let method: IrBytesIntrinsicMethod; + let args: () => IrExpr[]; + let result: "receiver" | IrType; + if ((name === "slice" || name === "subarray") && nodes.length <= 2) { + method = name; + args = () => [optional(0, num(0)), optional(1, num(Infinity))]; + result = "receiver"; + } else if (name === "fill" && nodes.length <= 3) { + method = "fillElem"; + args = () => [optional(0, num(0)), optional(1, num(0)), optional(2, num(Infinity))]; + result = "receiver"; + } else if (name === "copyWithin" && nodes.length <= 3) { + method = "copyWithin"; + args = () => [optional(0, num(0)), optional(1, num(0)), optional(2, num(Infinity))]; + result = "receiver"; + } else if (name === "toReversed" && nodes.length === 0) { + method = "toReversed"; + args = () => []; + result = "receiver"; + } else if (Object.hasOwn(ELEMENT_SEARCHES, name) && (nodes.length === 1 || nodes.length === 2)) { + method = ELEMENT_SEARCHES[name]!; + args = () => elementSearchArgs(lowerer, call, name); + result = name === "includes" ? BOOL : F64; + } else if (name === "join" && nodes.length <= 1) { + method = "join"; + args = () => [optional(0, { kind: "strLit", value: ",", type: STRING, loc }, STRING)]; + result = STRING; + } else if (name === "set" && (nodes.length === 1 || nodes.length === 2)) { + const source = lowerer.mapTypeOf(lowerer.typeOf(nodes[0]!)); + method = source?.kind === "bytes" ? "setFrom" : "setFromDyn"; + args = () => { + const src = lowerer.lowerExpr(nodes[0]!); + const first = src.type.kind === "bytes" ? src : lowerer.coerceInto(nodes[0]!, src, DYN); + return nodes.length === 2 ? [first, optional(1, num(0))] : [first]; + }; + result = VOID; + } else return null; + const receiver = lowerer.lowerExpr(access.expression); + const arms = indexedUnionArms(lowerer, receiver.type); + if (!arms || !allBytesArms(arms)) return null; + return lowerIndexedUnionMethod(lowerer, receiver, arms, method, args(), result, loc); +} + export function lowerBytesMethodCall( lowerer: Lowerer, call: ts.CallExpression, @@ -918,6 +1155,9 @@ export function lowerBytesMethodCall( if (lowerer.chainBlocked(access, call)) return null; const name = access.name.text; const receiverIr = lowerer.mapTypeOf(lowerer.typeOf(access.expression)); + const unionArms = receiverIr ? indexedUnionArms(lowerer, receiverIr) : null; + if (unionArms && allBytesArms(unionArms) && lowerer.isStdlibMember(access)) + return lowerUnionBytesMethodCall(lowerer, call, access, name); if (receiverIr?.kind !== "bytes") return null; if (!lowerer.isStdlibMember(access)) return null; const loc = locOf(call); @@ -964,6 +1204,29 @@ export function lowerBytesMethodCall( if (value) return value; } } + // TypedArray indexOf / lastIndexOf / includes (the Buffer-declared forms, + // which also take string and Buffer needles, lowered above). + if ( + Object.hasOwn(ELEMENT_SEARCHES, name) && + !lowerer.checker + .getSymbolAtLocation(access.name) + ?.declarations.some( + (d) => ts.isInterfaceDeclaration(d.parent) && d.parent.name.text === "Buffer", + ) + ) { + if (nArgs < 1 || nArgs > 2 || call.arguments.nodes.some(ts.isSpreadElement)) { + lowerer.noLowering(`.${name} with ${nArgs} arguments on typed arrays`, call); + } + const receiver = lowerer.lowerExprExpecting(access.expression, receiverIr); + return { + kind: "bytesIntrinsic", + method: ELEMENT_SEARCHES[name]!, + receiver, + args: elementSearchArgs(lowerer, call, name), + type: name === "includes" ? BOOL : F64, + loc, + }; + } if (name === "toReversed") { if (nArgs !== 0) { lowerer.noLowering(`.toReversed with ${nArgs} arguments on typed arrays`, call); @@ -1106,21 +1369,7 @@ export function lowerBytesMethodCall( lowerer.noLowering(`.set with ${nArgs} arguments on typed arrays`, call); } const receiver = lowerer.lowerExprExpecting(access.expression, receiverIr); - const src = lowerer.lowerExpr(call.arguments.nodes[0]!); - const method = src.type.kind === "bytes" ? "setFrom" : "setFromDyn"; - const args = [ - src.type.kind === "bytes" ? src : lowerer.coerceInto(call.arguments.nodes[0]!, src, DYN), - ]; - if (nArgs === 2) - args.push( - lowerOptionalArgument(lowerer, call.arguments.nodes[1]!, F64, { - kind: "numLit", - value: 0, - type: F64, - loc, - }), - ); - return { kind: "bytesIntrinsic", method, receiver, args, type: VOID, loc }; + return bytesSetIntrinsic(lowerer, receiver, call.arguments.nodes, loc); } if (name === "toString") { // Buffer's toString(encoding?) — utf8 by default. A 0-arg toString @@ -1271,6 +1520,8 @@ export function lowerBytesMethodCall( "the numeric families read/write Buffer bytes", ); } + if (!numericArguments(lowerer, call)) + return dynNumericCall(lowerer, call, access, name, receiverIr, loc); const write = name.startsWith("write"); const required = write ? 1 : 0; // value; offset defaults to 0 if (nArgs < required || nArgs > required + 1) { @@ -1448,6 +1699,39 @@ const BUF_BIGINT_METHODS: Record lowerer.mapTypeOf(lowerer.typeOf(a))?.kind === "f64"); +} + +/** A numeric read/write with an argument that is not a number (a string + * offset from untyped JavaScript): the checked-dynamic Buffer method runs + * Node's argument validation at runtime. */ +function dynNumericCall( + lowerer: Lowerer, + call: ts.CallExpression, + access: ts.PropertyAccessExpression, + name: string, + receiverIr: IrType, + loc: SrcLoc, +): IrExpr { + const receiver = lowerer.lowerExprExpecting(access.expression, receiverIr); + return { + kind: "dynCheck", + value: { + kind: "dynInvoke", + recv: { kind: "dynFrom", value: receiver, type: DYN, loc, liveRef: true }, + method: name, + calleeName: ts.getTextOfNode(access), + args: call.arguments.nodes.map((a) => lowerer.lowerExprExpecting(a, DYN)), + type: DYN, + loc, + }, + type: F64, + loc, + }; +} + /** The variable-width quartet (offset + byteLength) → sign/endian token. */ const BUF_NUM_VAR_METHODS: Record = { readUIntBE: "ube", @@ -1807,7 +2091,12 @@ function knownBufferProducer( * or empty encoding and throws ERR_UNKNOWN_ENCODING for an unknown name; an * ArrayBuffer takes the second argument as its byte offset; other sources * ignore it. */ -function dynBufferFromParts(source: IrExpr, encoding: IrExpr, loc: SrcLoc): IrExpr { +function dynBufferFromParts( + source: IrExpr, + encoding: IrExpr, + loc: SrcLoc, + length?: IrExpr, +): IrExpr { const utf8: IrExpr = { kind: "strLit", value: "utf8", type: STRING, loc }; const isString = (x: IrExpr): IrExpr => ({ kind: "dynTest", @@ -1848,20 +2137,48 @@ function dynBufferFromParts(source: IrExpr, encoding: IrExpr, loc: SrcLoc): IrEx type: BYTES_U8, loc, }, - // An ArrayBuffer takes the second argument as its byte offset - // (scr_buffer_from_dyn_offset_pooled); other values convert with utf8. - else_: { - kind: "libCall", - fn: "buffer.poolFromDynOffset", - args: [source, encoding], - type: BYTES_U8, - loc, - }, + // An ArrayBuffer takes the second argument as its byte offset and the + // third as its length (scr_buffer_from_dyn_range_pooled); other values + // convert with utf8. + else_: length + ? { + kind: "libCall", + fn: "buffer.poolFromDynRange", + args: [source, encoding, length], + type: BYTES_U8, + loc, + } + : { + kind: "libCall", + fn: "buffer.poolFromDynOffset", + args: [source, encoding], + type: BYTES_U8, + loc, + }, type: BYTES_U8, loc, }; } +/** Buffer.from(arrayBuffer, byteOffset[, length]): each argument in order + * as a checked-dynamic value, read by the runtime as Node's + * fromArrayBuffer reads them. */ +function bufferFromArrayBufferRange( + lowerer: Lowerer, + args: readonly ts.Expression[], + loc: SrcLoc, + first?: IrExpr, +): IrExpr { + const operands = [0, 1, 2].map((i) => + i === 0 && first + ? first + : args[i] + ? lowerer.lowerExprExpecting(args[i]!, DYN) + : dynUndefinedExpr(loc), + ); + return { kind: "libCall", fn: "buffer.poolFromDynRange", args: operands, type: BYTES_U8, loc }; +} + /** Lowers `source` and `encNode` (or undefined) into hidden dyn locals, in * source order, and builds the result from their references. */ function withDynBufferArguments( @@ -1909,52 +2226,81 @@ function dynBufferFromRuntimeEncoding( ); } -/** The deprecated `new Buffer(arg[, encoding])` in a dependency's - * JavaScript, through the global or an alias of it (buffer-crc32 and yazl - * keep it as the fallback for runtimes without Buffer.alloc/Buffer.from). - * Node never emits DEP0005 for calls from node_modules, so the construction - * is exactly Node's: a number allocates zero-filled bytes (and throws - * ERR_INVALID_ARG_TYPE beside a string second argument), anything else - * converts as Buffer.from does. Null elsewhere: other callers owe the - * deprecation warning. */ -export function lowerNodeModulesBufferConstruct( +/** The deprecated `Buffer(arg[, encodingOrOffset[, length]])`, called or + * constructed through the global or an alias of it, as Node's Buffer + * function runs it: the arguments evaluate in order, DEP0005 is reported + * on the next tick (once, and never for a call made from node_modules, + * which only counts toward Node's 10000-call check), then a number + * allocates zero-filled bytes (and throws ERR_INVALID_ARG_TYPE beside a + * string second argument) and anything else converts as Buffer.from does. + * Null for spread arguments and more than three. */ +export function lowerBufferConstruct( lowerer: Lowerer, - expr: ts.NewExpression, + expr: ts.NewExpression | ts.CallExpression, ): IrExpr | null { - const sf = expr.getSourceFile(); - if (!isJsSourceFile(sf) || !/[\\/]node_modules[\\/]/.test(sf.fileName)) return null; + if (ts.isCallExpression(expr) && expr.questionDotToken) return null; const args = expr.arguments?.nodes ?? []; - if (args.length < 1 || args.length > 2 || args.some(ts.isSpreadElement)) return null; + if (args.length > 3 || args.some(ts.isSpreadElement)) return null; if (stdlibGlobalNameOf(lowerer, expr.expression) !== "Buffer") return null; const loc = locOf(expr); - const value = withDynBufferArguments( - lowerer, - lowerer.lowerExpr(args[0]!), - args[1], - loc, - (source, encoding) => ({ + const locals = [0, 1, 2].slice(0, Math.max(args.length, 2)).map((i) => { + const local = lowerer.declareHiddenLocal("%bufferArgument", DYN); + const node = args[i]; + const init = node === undefined ? dynUndefinedExpr(loc) : lowerer.lowerExprExpecting(node, DYN); + return { local, init }; + }); + const [source, second, third] = locals.map(({ local }) => varRef(local.id, DYN, loc)) as [ + IrExpr, + IrExpr, + IrExpr | undefined, + ]; + const inNodeModules = /[\\/]node_modules[\\/]/.test(expr.getSourceFile().fileName); + const value: IrExpr = { + kind: "ternary", + cond: { kind: "dynTest", test: "number", value: source, type: BOOL, loc }, + then: { kind: "ternary", - cond: { kind: "dynTest", test: "number", value: source, type: BOOL, loc }, - then: { - kind: "ternary", - cond: { kind: "dynTest", test: "string", value: encoding, type: BOOL, loc }, - then: { kind: "libCall", fn: "buffer.newStringFail", args: [source], type: BYTES_U8, loc }, - else_: { - kind: "libCall", - fn: "buffer.alloc", - args: [{ kind: "dynCheck", value: source, type: F64, loc }], - type: BYTES_U8, - loc, - }, + cond: { kind: "dynTest", test: "string", value: second, type: BOOL, loc }, + then: { kind: "libCall", fn: "buffer.newStringFail", args: [source], type: BYTES_U8, loc }, + else_: { + kind: "libCall", + fn: "buffer.alloc", + args: [{ kind: "dynCheck", value: source, type: F64, loc }], type: BYTES_U8, loc, }, - else_: dynBufferFromParts(source, encoding, loc), type: BYTES_U8, loc, - }), - ); - return { kind: "libCall", fn: "buffer.brand", args: [value], type: BYTES_U8, loc }; + }, + else_: dynBufferFromParts(source, second, loc, third), + type: BYTES_U8, + loc, + }; + return { + kind: "seqExpr", + stmts: [ + ...locals.map(({ local, init }): IrStmt => ({ + kind: "varDecl", + localId: local.id, + init, + loc, + })), + { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: inNodeModules ? "buffer.deprecationCount" : "buffer.deprecation", + args: [], + type: VOID, + loc, + }, + loc, + }, + ], + result: { kind: "libCall", fn: "buffer.brand", args: [value], type: BYTES_U8, loc }, + type: BYTES_U8, + loc, + }; } /** Buffer.from(array or bytes, extra): Node copies an array-like source and @@ -2018,7 +2364,16 @@ function lowerBufferStaticValue( lowerer.typeOf(args[0]!).getSymbol(), ) ) { - return lowerArrayBufferView(lowerer, args, "u8", loc); + // A byte offset or length follows Node's fromArrayBuffer (its + // conversions and ERR_BUFFER_OUT_OF_BOUNDS), not the typed-array view. + if (args.length === 1) return lowerArrayBufferView(lowerer, args, "u8", loc); + return bufferFromArrayBufferRange(lowerer, args, loc); + } + // Buffer.from(value, byteOffset, length) over an untyped value: an + // ArrayBuffer's range, or the two-argument conversion of other values. + if (args.length === 3 && !args.some(ts.isSpreadElement)) { + const first = lowerer.lowerExpr(args[0]!); + if (first.type.kind === "dyn") return bufferFromArrayBufferRange(lowerer, args, loc, first); } // Buffer.from(x.buffer[, byteOffset[, length]]): a u8 VIEW sharing // x's storage (Node shares the ArrayBuffer; length is in BYTES). @@ -2445,11 +2800,101 @@ function lowerBufferStaticValue( const s = lowerer.lowerExprExpecting(args[0]!, STRING); return { kind: "libCall", fn: "buffer.isEncoding", args: [s], type: BOOL, loc }; } + // Any other single argument: Node answers false for non-strings. + if (args.length === 1 && !ts.isSpreadElement(args[0]!)) { + const value = lowerer.declareHiddenLocal("%bufferEncoding", DYN); + const ref = varRef(value.id, DYN, loc); + return { + kind: "seqExpr", + stmts: [ + { + kind: "varDecl", + localId: value.id, + init: lowerer.lowerExprExpecting(args[0]!, DYN), + loc, + }, + ], + result: { + kind: "logical", + op: "&&", + left: { kind: "dynTest", test: "string", value: ref, type: BOOL, loc }, + right: { + kind: "libCall", + fn: "buffer.isEncoding", + args: [{ kind: "dynCheck", value: ref, type: STRING, loc }], + type: BOOL, + loc, + }, + type: BOOL, + loc, + }, + type: BOOL, + loc, + }; + } lowerer.noLowering( "Buffer.isEncoding with this argument shape", call, - "one string-typed argument is the lowered form", + "one argument is the lowered form", ); } return null; // of, copyBytesFrom, ... → the SC2020 member fence } + +/** `Buffer.prototype.` or `Buffer.prototype[key]` read in JavaScript + * (smart-buffer's `typeof Buffer.prototype[method]` probe): the member as + * scr_buffer_prototype_member answers it. Null for other expressions. */ +export function lowerBufferPrototypeMember( + lowerer: Lowerer, + expr: ts.PropertyAccessExpression | ts.ElementAccessExpression, +): IrExpr | null { + if (expr.questionDotToken || !isJsSourceFile(expr.getSourceFile())) return null; + const receiver = expr.expression; + if ( + !ts.isPropertyAccessExpression(receiver) || + receiver.questionDotToken || + receiver.name.text !== "prototype" || + stdlibGlobalNameOf(lowerer, receiver.expression) !== "Buffer" + ) + return null; + const loc = locOf(expr); + const key: IrExpr = ts.isPropertyAccessExpression(expr) + ? { + kind: "dynFrom", + value: { kind: "strLit", value: expr.name.text, type: STRING, loc }, + type: DYN, + loc, + } + : lowerer.lowerExprExpecting(expr.argumentExpression, DYN); + return { kind: "libCall", fn: "buffer.prototypeMember", args: [key], type: DYN, loc }; +} + +const BUFFER_VALUE_FNS: ReadonlySet = new Set([ + "buffer.constructor", + "buffer.staticValue", + "buffer.prototypeMember", +]); + +/** A program that reads the Buffer function's value installs it in %main, + * so a Buffer's `.constructor` answers the same function. */ +export function installBufferValue(functions: IrFunction[]): void { + let found = false; + const visitor = { + expr: (e: IrExpr): boolean => { + if (e.kind === "libCall" && BUFFER_VALUE_FNS.has(e.fn)) found = true; + return !found; + }, + stmt: (): boolean => !found, + }; + for (const fn of functions) { + for (const s of fn.body) everyStmt(s, visitor); + if (found) break; + } + const main = functions.find((fn) => fn.name === ENTRY_NAME); + if (!found || !main) return; + main.body.unshift({ + kind: "exprStmt", + expr: { kind: "libCall", fn: "buffer.installValue", args: [], type: VOID, loc: main.loc }, + loc: main.loc, + }); +} diff --git a/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts b/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts index d95640978..3b504861a 100644 --- a/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts +++ b/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts @@ -2,6 +2,7 @@ import { keyedHelperName } from "../stable-names.js"; import * as ts from "../../ts7/adapter.js"; import type { Lowerer } from "../lowerer.js"; import { isJsSourceFile, locOf } from "../../program.js"; +import { primitiveElement } from "../array-values.js"; import { varRef } from "../../../ir/build.js"; import { typeKey } from "../../type-mapper.js"; import { @@ -52,7 +53,7 @@ export function lowerArrayCallback( arrT: IrType, bindUntyped = false, expectedReturn?: IrType, -): { fnArg: IrExpr & { type: IrType & { kind: "func" } }; arity: number } { +): { fnArg: IrExpr & { type: IrType & { kind: "func" } }; arity: number; unread: boolean } { // `xs.map(((x) => x))`: a parenthesized callback is the function itself. while (ts.isParenthesizedExpression(argNode)) argNode = argNode.expression; // The ambient `Boolean` as the callback (`flags.every(Boolean)`, @@ -71,7 +72,11 @@ export function lowerArrayCallback( if (valueT === undefined || valueT.kind === "void" || isUnitType(valueT)) lowerer.badType(argNode, lowerer.typeOf(argNode)); if (valueT.kind === "union") lowerer.requireTruthyUnion(valueT.unionId, argNode); - return { fnArg: truthyFilterCallback(lowerer, valueT, locOf(argNode)), arity: 1 }; + return { + fnArg: truthyFilterCallback(lowerer, valueT, locOf(argNode)), + arity: 1, + unread: false, + }; } // The ambient `String` (`values.map(String)`): String(value) converts its // first argument as a direct call does, whatever the element type; the @@ -85,7 +90,7 @@ export function lowerArrayCallback( const valueT = lead[0]; if (valueT === undefined || valueT.kind === "void") lowerer.badType(argNode, lowerer.typeOf(argNode)); - return { fnArg: stringConversionCallback(lowerer, valueT, argNode), arity: 1 }; + return { fnArg: stringConversionCallback(lowerer, valueT, argNode), arity: 1, unread: false }; } const full = [...lead, F64, arrT]; // A DYN-receiver HOF's callback (`parsed.flatMap((value) => ...)`): @@ -144,12 +149,22 @@ export function lowerArrayCallback( const untyped = (checkerType.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0; const raw = untyped ? null : lowerer.mapTypeOf(checkerType); const mapped = raw ? lowerer.runtimeOptionalBindingType(param.name, raw) : null; + // A helper that passes present number, string or boolean elements + // (a `(T | undefined)[]` stores T elements) binds a `T | undefined` + // parameter to T, so the callback takes them without an adapter. + const presentElement = + mapped !== null && + primitiveElement(expected) && + mapped.kind === "union" && + lowerer.armTag(mapped.unionId, UNDEFINED_T) >= 0 && + typeEquals(lowerer.stripUndefinedArm(mapped), expected); if ( untyped ? !bindUntyped : mapped === null || (lowerer.coercibleValue(expected, mapped) && - !runtimeOptionalPairVariant(lowerer, expected, mapped)) + !runtimeOptionalPairVariant(lowerer, expected, mapped) && + !presentElement) ) return; const symbol = lowerer.checker.getSymbolAtLocation(param.name); @@ -265,9 +280,32 @@ export function lowerArrayCallback( return { fnArg: fnArg as IrExpr & { type: IrType & { kind: "func" } }, arity: fnArg.type.params.length, + unread: !readsParameter(argNode, lead.length - 1), }; } +/** Whether a callback can observe its parameter at `index`: false for a + * function or arrow expression that declares no such parameter, or binds + * it to a plain name that appears nowhere else in it (any identifier of + * that name counts, including shorthand properties and shadowing + * bindings). Any other callback may. */ +function readsParameter(argNode: ts.Expression, index: number): boolean { + if (!ts.isArrowFunction(argNode) && !ts.isFunctionExpression(argNode)) return true; + const param = argNode.parameters.nodes[index]; + if (!param) return false; + if (!ts.isIdentifier(param.name) || param.initializer || param.dotDotDotToken) return true; + const name = param.name.text; + const mentions = (node: ts.Node): boolean => + (node !== param.name && + ts.isIdentifier(node) && + (node.text === name || node.text === "arguments")) || + (ts.forEachChild(node, (child) => (mentions(child) ? true : undefined)) ?? false); + return ( + argNode.parameters.nodes.some((other) => other !== param && mentions(other)) || + mentions(argNode.body) + ); +} + /** Preserve the checker-selected payload ABI when a callback's runtime * return gained only undefined. The array state byte carries that value. */ export function callbackArrayElement( diff --git a/packages/compiler/src/frontend/lowering/containers/collection-methods.ts b/packages/compiler/src/frontend/lowering/containers/collection-methods.ts index e043daf68..21cc36016 100644 --- a/packages/compiler/src/frontend/lowering/containers/collection-methods.ts +++ b/packages/compiler/src/frontend/lowering/containers/collection-methods.ts @@ -1,4 +1,5 @@ import { keyedHelperName } from "../stable-names.js"; +import { primitiveElement } from "../array-values.js"; import { nodeThrowExpr, boolLit, countedFor, numLit, strLit, varRef } from "../../../ir/build.js"; import { InternalCompilerError } from "../../../errors.js"; import * as ts from "../../ts7/adapter.js"; @@ -43,6 +44,7 @@ import { } from "../collection-ingestion.js"; import { strCharsCall } from "./array-construction.js"; import { arrayElementRead } from "./array-iteration.js"; +import { isMimeParamsIterable } from "../builtins/mime.js"; import { isSearchParamsIterable } from "../builtins/url.js"; import { classGeneratorIterable } from "../class-iterables.js"; import { generatorDrain, lowerCollectionIteratorValue } from "../iterator-adapters.js"; @@ -1134,9 +1136,20 @@ function setFromSeedValue( if (source.type.kind === "set" && typeEquals(source.type, setT)) { return { kind: "setIntrinsic", method: "clone", receiver: source, args: [], type: setT, loc }; } else if (source.type.kind === "string" && setT.elem.kind === "string") { - seed = strCharsCall(lowerer, source, loc); + seed = strCharsCall(source, loc); } else if (source.type.kind === "array" && typeEquals(source.type.elem, setT.elem)) { seed = source; + } else if ( + source.type.kind === "array" && + primitiveElement(source.type.elem) && + setT.elem.kind === "union" && + lowerer.armTag(setT.elem.unionId, UNDEFINED_T) >= 0 && + typeEquals(lowerer.stripUndefinedArm(setT.elem), source.type.elem) + ) { + // A `(T | undefined)[]` stores T elements: its present undefined + // elements copy as the Set's undefined. + seed = lowerer.coerceToExpected(source, arrayOf(setT.elem)); + if (!typeEquals(seed.type, arrayOf(setT.elem))) return null; } else if (source.type.kind === "record") { const shape = lowerer.shapes.get(source.type.shapeId); if (!shape?.tuple || !shape.fields.every((field) => typeEquals(field.type, setT.elem))) @@ -1259,8 +1272,8 @@ export function lowerMapSeedNew( ); } // new Map(sp) / new Map(sp.entries()): the checked-dynamic view's live - // pairs seed the map in order. - if (isSearchParamsIterable(lowerer, argNode)) { + // pairs seed the map in order, and so do a MIMEParams's entries. + if (isSearchParamsIterable(lowerer, argNode) || isMimeParamsIterable(lowerer, argNode)) { return mapFromSeedValue(lowerer, lowerer.lowerExprExpecting(argNode, DYN), mapT); } const input = collectionInput(lowerer, argNode); diff --git a/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts b/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts index 668603d47..1c07885ae 100644 --- a/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts +++ b/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts @@ -1,6 +1,7 @@ import { keyedHelperName } from "../stable-names.js"; import { nullProtoRecordShape, recordNullProtoTest } from "../null-prototype.js"; import { countedFor, numLit, varRef } from "../../../ir/build.js"; +import { arrayValueStore, optionalElementValue, primitiveElement } from "../array-values.js"; import { objectEnumerationReceiver } from "../object-enumeration-receiver.js"; import { InternalCompilerError } from "../../../errors.js"; import * as ts from "../../ts7/adapter.js"; @@ -156,7 +157,14 @@ export function objectIterOverIndexShape( // also holds numbers): each overflow value checks into the element type, // so a value of a dropped arm throws instead of mistyping. let ivNarrows = false; - if (valueT) { + // `T | undefined` values into a `(T | undefined)[]`, which stores T + // elements (primitiveElement): each value is stored with its present + // undefined. + const optionalValues = + member === "values" && + valueT !== null && + optionalElementValue(lowerer, { kind: "unitLit", unit: "undefined", type: iv, loc }, valueT); + if (valueT && !optionalValues) { const ivSurfaces = typeEquals(iv, valueT) || (valueT.kind === "union" && lowerer.armTag(valueT.unionId, iv) >= 0) || @@ -350,7 +358,25 @@ export function objectIterOverIndexShape( loc, }, then: [fieldStmt], - else_: undefinedElement ? [push(pushedOf(undefinedElement))] : null, + else_: undefinedElement + ? [push(pushedOf(undefinedElement))] + : member === "values" && valueT && primitiveElement(valueT) + ? [ + { + kind: "arraySetUndefined", + arr: outRef, + index: { + kind: "arrIntrinsic", + method: "length", + receiver: outRef, + args: [], + type: F64, + loc, + }, + loc, + }, + ] + : null, loc, }; } @@ -374,35 +400,45 @@ export function objectIterOverIndexShape( const kRef = varRef("k.0", STRING, loc); const readValue: IrExpr | null = !valueT ? null - : ivNarrows + : optionalValues ? { - kind: "dynCheck", - value: { - kind: "dynFrom", - value: { - kind: "recordKeyGet", - obj: rRef, - shapeId: argIr.shapeId, - key: kRef, - overflowOnly: true, - type: iv, - loc, - }, - type: DYN, - loc, - }, - type: valueT, - loc, - } - : { kind: "recordKeyGet", obj: rRef, shapeId: argIr.shapeId, key: kRef, overflowOnly: true, - type: valueT, + type: iv, loc, - }; + } + : ivNarrows + ? { + kind: "dynCheck", + value: { + kind: "dynFrom", + value: { + kind: "recordKeyGet", + obj: rRef, + shapeId: argIr.shapeId, + key: kRef, + overflowOnly: true, + type: iv, + loc, + }, + type: DYN, + loc, + }, + type: valueT, + loc, + } + : { + kind: "recordKeyGet", + obj: rRef, + shapeId: argIr.shapeId, + key: kRef, + overflowOnly: true, + type: valueT, + loc, + }; const loopPushed: IrExpr = member === "keys" ? kRef @@ -440,7 +476,26 @@ export function objectIterOverIndexShape( }, loc, }, - push(loopPushed), + ...(optionalValues + ? [ + { kind: "varDecl" as const, localId: "v.0", init: readValue!, loc }, + arrayValueStore( + lowerer, + outRef, + { + kind: "arrIntrinsic", + method: "length", + receiver: outRef, + args: [], + type: F64, + loc, + }, + varRef("v.0", iv, loc), + valueT!, + loc, + ), + ] + : [push(loopPushed)]), ], ), { kind: "return", value: outRef, loc }, @@ -457,6 +512,7 @@ export function objectIterOverIndexShape( { id: "ks.0", name: "ks", type: ksT, mutable: false }, { id: "i.0", name: "i", type: F64, mutable: true }, { id: "k.0", name: "k", type: STRING, mutable: false }, + ...(optionalValues ? [{ id: "v.0", name: "v", type: iv, mutable: false }] : []), ], body, loc, diff --git a/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts b/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts index ba5d0684d..f6580c70e 100644 --- a/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts +++ b/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts @@ -1182,10 +1182,10 @@ function bindSource( /** Where chain code allocates its locals: a lifted helper function's own * frame, or hidden locals of the function being lowered (a fused for-of). */ -interface ChainFrame { - local(base: string, type: IrType, mutable: boolean): string; +abstract class ChainFrame { + abstract local(base: string, type: IrType, mutable: boolean): string; /** `id = id + 1` over a number local. */ - bump(id: string): IrStmt; + abstract bump(id: string): IrStmt; } /** `id = id + 1` over a number local. */ @@ -1206,11 +1206,13 @@ function bumpLocal(id: string, loc: SrcLoc): IrStmt { } /** Hidden locals of the function being lowered. */ -class HostFrame implements ChainFrame { +class HostFrame extends ChainFrame { constructor( private readonly lowerer: Lowerer, private readonly loc: SrcLoc, - ) {} + ) { + super(); + } local(base: string, type: IrType, mutable: boolean): string { const local = this.lowerer.declareHiddenLocal(`%${base}`, type); local.mutable = mutable; @@ -1223,11 +1225,13 @@ class HostFrame implements ChainFrame { /** The locals and parameters of a lifted helper function under * construction. */ -export class LiftedFrame implements ChainFrame { +export class LiftedFrame extends ChainFrame { readonly locals: IrLocal[] = []; readonly params: IrParam[] = []; private n = 0; - constructor(private readonly loc: SrcLoc) {} + constructor(private readonly loc: SrcLoc) { + super(); + } local(base: string, type: IrType, mutable: boolean): string { const id = `${base}.${this.n++}`; this.locals.push({ id, name: base, type, mutable }); diff --git a/packages/compiler/src/frontend/lowering/containers/replacer-params.test.ts b/packages/compiler/src/frontend/lowering/containers/replacer-params.test.ts new file mode 100644 index 000000000..1d5d376e9 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/containers/replacer-params.test.ts @@ -0,0 +1,63 @@ +import { describe, expect, test } from "vitest"; +import { regexCaptureParticipation } from "./replacer-params.js"; + +describe("regex capture participation", () => { + test("counts captures and marks the ones every match sets", () => { + expect(regexCaptureParticipation("\\{(\\w+)\\}", "g")).toEqual({ + count: 2, + always: [true, true], + }); + expect(regexCaptureParticipation("(\\d{4})-(\\d{2})(?:-(\\d{2}))?", "g")).toEqual({ + count: 4, + always: [true, true, true, false], + }); + expect(regexCaptureParticipation("(a)|(b)", "")).toEqual({ + count: 3, + always: [true, false, false], + }); + expect(regexCaptureParticipation("((a)|b)", "")).toEqual({ + count: 3, + always: [true, true, false], + }); + expect(regexCaptureParticipation("(?:(a)|b)(c)", "")).toEqual({ + count: 3, + always: [true, false, true], + }); + expect(regexCaptureParticipation("(a)*(b)+(c){0,2}(d){1,3}(e){00}", "")).toEqual({ + count: 6, + always: [true, false, true, false, true, false], + }); + expect(regexCaptureParticipation("(?=(a))(?!(b))(?<=(c))(?e)", "")).toEqual({ + count: 6, + always: [true, false, false, false, false, true], + }); + }); + + test("skips escapes, classes and non-capturing groups", () => { + expect(regexCaptureParticipation("\\((a)\\)[(|)\\]](?:b)(?i:c)", "")).toEqual({ + count: 2, + always: [true, true], + }); + expect(regexCaptureParticipation("[](a)", "")).toEqual({ count: 2, always: [true, true] }); + expect(regexCaptureParticipation("(a)[", "")).toBeNull(); + expect(regexCaptureParticipation("(a", "")).toBeNull(); + expect(regexCaptureParticipation("a)", "")).toBeNull(); + expect(regexCaptureParticipation("(a)", "v")).toBeNull(); + }); + test("marks captures in alternations, optional groups and lookarounds", () => { + const always = (pattern: string): boolean[] | null => + regexCaptureParticipation(pattern, "")?.always ?? null; + expect(always("tag(\\d+)")).toEqual([true, true]); + expect(always("((a)b)+c")).toEqual([true, true, true]); + expect(always("(?\\d)(x)")).toEqual([true, true, true]); + expect(always("(a){2,3}")).toEqual([true, true]); + expect(always("(?:x|y)(a)")).toEqual([true, true]); + expect(always("(?:(a)|b)c")).toEqual([true, false]); + expect(always("(x)?(\\d)")).toEqual([true, false, true]); + expect(always("(?:(t)a)*g")).toEqual([true, false]); + expect(always("((a)?)")).toEqual([true, true, false]); + expect(always("(?<=(-))(\\w)")).toEqual([true, false, true]); + expect(always("[\\]|(](b)")).toEqual([true, true]); + expect(always("[^)](c)\\|")).toEqual([true, true]); + }); +}); diff --git a/packages/compiler/src/frontend/lowering/containers/replacer-params.ts b/packages/compiler/src/frontend/lowering/containers/replacer-params.ts new file mode 100644 index 000000000..3a3eff1cf --- /dev/null +++ b/packages/compiler/src/frontend/lowering/containers/replacer-params.ts @@ -0,0 +1,302 @@ +import * as ts from "../../ts7/adapter.js"; +import { F64, STRING, type IrExpr, type IrType } from "../../../ir/ir.js"; +import type { Lowerer } from "../lowerer.js"; +import { bindingNeverReassigned } from "../binding-analysis.js"; + +/** The captures of a regex literal's pattern (the whole match included) + * and, per capture, whether every successful match sets it. A capture may + * be unset when an enclosing group has alternatives, when an enclosing + * group or the capture itself is quantified with a minimum of zero, or + * when it sits inside a lookaround. Null for the `v` flag (nested + * classes) and for a pattern the scan does not finish. */ +export function regexCaptureParticipation( + pattern: string, + flags: string, +): { count: number; always: boolean[] } | null { + if (flags.includes("v")) return null; + const always: boolean[] = [true]; + /** Open groups: the group's own capture (-1 when it does not capture), + * the captures nested in it, whether it has alternatives, and whether it + * is a lookaround. */ + const frames: { own: number; nested: number[]; alternatives: boolean; lookaround: boolean }[] = [ + { own: -1, nested: [], alternatives: false, lookaround: false }, + ]; + let i = 0; + while (i < pattern.length) { + const c = pattern[i]!; + if (c === "\\") { + i += 2; + continue; + } + if (c === "[") { + // The first unescaped `]` ends the class (`[]` is an empty class). + i++; + while (i < pattern.length && pattern[i] !== "]") i += pattern[i] === "\\" ? 2 : 1; + if (i >= pattern.length) return null; + i++; + continue; + } + if (c === "|") { + frames[frames.length - 1]!.alternatives = true; + i++; + continue; + } + if (c === "(") { + let capture = false; + let lookaround = false; + if (pattern[i + 1] !== "?") { + capture = true; + i++; + } else if (pattern[i + 2] === "<" && pattern[i + 3] !== "=" && pattern[i + 3] !== "!") { + const gt = pattern.indexOf(">", i + 3); + if (gt < 0) return null; + capture = true; + i = gt + 1; + } else { + const kind = pattern[i + 2] === "<" ? pattern[i + 3] : pattern[i + 2]; + lookaround = kind === "=" || kind === "!"; + i += 2; + } + frames.push({ + own: capture ? always.length : -1, + nested: [], + alternatives: false, + lookaround, + }); + if (capture) always.push(true); + continue; + } + if (c === ")") { + if (frames.length === 1) return null; + const frame = frames.pop()!; + i++; + const next = pattern[i]; + const optional = + next === "?" || next === "*" || (next === "{" && /^\{0+[,}]/.test(pattern.slice(i))); + const parent = frames[frames.length - 1]!; + // Alternatives inside the group leave its nested captures unset, but + // not the group's own capture. + for (const index of frame.nested) { + if (optional || frame.lookaround || frame.alternatives) always[index] = false; + parent.nested.push(index); + } + if (frame.own >= 0) { + if (optional || frame.lookaround) always[frame.own] = false; + parent.nested.push(frame.own); + } + continue; + } + i++; + } + if (frames.length !== 1) return null; + if (frames[0]!.alternatives) for (const index of frames[0]!.nested) always[index] = false; + return { count: always.length, always }; +} + +/** String members a refined parameter may use: these lower from the + * receiver's type whatever the checker resolved for an `any` receiver. */ +const STRING_MEMBERS: ReadonlySet = new Set([ + "length", + "toUpperCase", + "toLowerCase", + "trim", + "charCodeAt", + "codePointAt", + "slice", + "substring", + "repeat", + "padStart", + "padEnd", + "includes", + "startsWith", + "endsWith", + "indexOf", +]); + +/** Whether every read of `symbol` in `body` is one that means the same + * for an `any` value and for a `kind` value, and lowers for both: a + * template span, the argument of Number or String, the text argument of + * parseInt or parseFloat for a string and the radix of parseInt for a + * number (each a parameter of that type, which an `any` value converts + * to), a listed + * string member, `+`, strict equality, arithmetic and comparisons of + * numbers, a returned value, and a string key of an element access on a + * string-indexed record or an `any` value. Anything else keeps the + * parameter `any`. */ +function readsFit( + lowerer: Lowerer, + body: ts.Node, + symbol: ts.Symbol, + kind: "string" | "f64", +): boolean { + const name = ts.symbolName(symbol); + const primitive = (e: ts.Expression): boolean => { + const flags = lowerer.checker.getTypeAtLocation(e).flags; + return (flags & (ts.TypeFlags.NumberLike | ts.TypeFlags.StringLike)) !== 0; + }; + const fits = (id: ts.Identifier): boolean => { + let node: ts.Node = id; + while (ts.isParenthesizedExpression(node.parent)) node = node.parent; + const parent = node.parent; + if (ts.isTemplateSpan(parent) || ts.isReturnStatement(parent)) return true; + if (ts.isElementAccessExpression(parent)) { + if (kind !== "string" || parent.argumentExpression !== node) return false; + const receiver = lowerer.mapTypeOf(lowerer.checker.getTypeAtLocation(parent.expression)); + if (receiver?.kind === "dyn") return true; + const shape = receiver?.kind === "record" ? lowerer.shapes.get(receiver.shapeId) : undefined; + return shape?.indexValue !== undefined && !shape.tuple; + } + if (ts.isArrowFunction(parent)) return parent.body === node; + if (ts.isCallExpression(parent)) { + if (parent.expression === node) return false; + const position = parent.arguments.nodes.indexOf(node as ts.Expression); + if ( + lowerer.isStdlibGlobal(parent.expression, "Number") || + lowerer.isStdlibGlobal(parent.expression, "String") + ) + return position === 0; + if (lowerer.isStdlibGlobal(parent.expression, "parseFloat")) + return position === 0 && kind === "string"; + if (lowerer.isStdlibGlobal(parent.expression, "parseInt")) + return position === 0 ? kind === "string" : position === 1 && kind === "f64"; + return false; + } + if (ts.isPropertyAccessExpression(parent)) { + const member = parent.name.text; + if (kind !== "string" || parent.expression !== node || !STRING_MEMBERS.has(member)) + return false; + return ( + member === "length" || + (ts.isCallExpression(parent.parent) && parent.parent.expression === parent) + ); + } + if (ts.isBinaryExpression(parent)) { + const op = parent.operatorToken.kind; + if ( + op === ts.SyntaxKind.EqualsEqualsEqualsToken || + op === ts.SyntaxKind.ExclamationEqualsEqualsToken + ) + return true; + const other = parent.left === node ? parent.right : parent.left; + if (op === ts.SyntaxKind.PlusToken) return kind === "string" || primitive(other); + return ( + kind === "f64" && + (op === ts.SyntaxKind.MinusToken || + op === ts.SyntaxKind.AsteriskToken || + op === ts.SyntaxKind.SlashToken || + op === ts.SyntaxKind.PercentToken || + op === ts.SyntaxKind.LessThanToken || + op === ts.SyntaxKind.GreaterThanToken || + op === ts.SyntaxKind.LessThanEqualsToken || + op === ts.SyntaxKind.GreaterThanEqualsToken) && + (lowerer.checker.getTypeAtLocation(other).flags & ts.TypeFlags.NumberLike) !== 0 + ); + } + return false; + }; + let ok = true; + const visit = (n: ts.Node): void => { + if (!ok) return; + if (ts.isIdentifier(n) && n.text === name && lowerer.resolveValueSymbol(n) === symbol) { + if (!fits(n)) ok = false; + return; + } + ts.forEachChild(n, visit); + }; + visit(body); + return ok; +} + +/** The capture layout of a string search: the match, then the offset and + * the subject. */ +export const PLAIN_SEARCH_CAPTURES: { count: number; always: boolean[] } = { + count: 1, + always: [true], +}; + +/** The captures of a regex literal argument (regexCaptureParticipation), + * or null for any other pattern expression. */ +export function regexLiteralCapturesOf( + pattern: ts.Expression, +): { count: number; always: boolean[] } | null { + let regex = pattern; + while (ts.isParenthesizedExpression(regex)) regex = regex.expression; + if (!ts.isRegularExpressionLiteral(regex)) return null; + const text = regex.text; + const slash = text.lastIndexOf("/"); + return regexCaptureParticipation(text.slice(1, slash), text.slice(slash + 1)); +} + +/** Lowers an inline replace callback. JavaScript passes it the match, the + * captures, the match offset and the subject (`captures` gives their + * layout; null lowers the callback as written). TypeScript types the + * parameters after the match `any`, so an unannotated parameter that is + * never reassigned, whose every read fits its value (readsFit), and that + * receives a capture every match sets, the offset or the subject takes + * that value's type: a string or a number. The closure can then be called + * directly (typed replacers in lib-dispatch.ts) with fewer boxed values. */ +export function lowerReplacerCallback( + lowerer: Lowerer, + callback: ts.Expression, + captures: { count: number; always: boolean[] } | null, +): IrExpr { + let node = callback; + while (ts.isParenthesizedExpression(node)) node = node.expression; + // A named function expression can call itself with other arguments. + const literal = + ts.isArrowFunction(node) || (ts.isFunctionExpression(node) && !node.name) ? node : null; + if (!literal || !captures) return lowerer.lowerExpr(callback); + const bound: { symbol: ts.Symbol; previous: IrType | undefined }[] = []; + const previousImplicit = lowerer.implicitParamTypes; + let implicit: Map | null = null; + const params = literal.parameters.nodes; + // A declared `this` parameter takes no argument position. + const first = + params[0] && ts.isIdentifier(params[0].name) && params[0].name.text === "this" ? 1 : 0; + params.slice(first).forEach((param, i) => { + const type = + i === 0 || i > captures.count + 1 + ? null + : i < captures.count + ? captures.always[i] === true + ? STRING + : null + : i === captures.count + ? F64 + : STRING; + if ( + type === null || + !ts.isIdentifier(param.name) || + param.type || + param.initializer || + param.dotDotDotToken || + param.questionToken + ) + return; + if ((lowerer.checker.getTypeAtLocation(param.name).flags & ts.TypeFlags.Any) === 0) return; + const symbol = lowerer.checker.getSymbolAtLocation(param.name); + if ( + !symbol || + !bindingNeverReassigned(lowerer, symbol, param) || + !readsFit(lowerer, literal.body, symbol, type === STRING ? "string" : "f64") + ) + return; + bound.push({ symbol, previous: lowerer.runtimeOptionalBindingTypes.get(symbol) }); + lowerer.runtimeOptionalBindingTypes.set(symbol, type); + implicit ??= new Map(previousImplicit ?? []); + implicit.set( + symbol, + type === STRING ? lowerer.checker.getStringType() : lowerer.checker.getNumberType(), + ); + }); + if (implicit !== null) lowerer.implicitParamTypes = implicit; + try { + return lowerer.lowerExpr(callback); + } finally { + for (const { symbol, previous } of bound) { + if (previous === undefined) lowerer.runtimeOptionalBindingTypes.delete(symbol); + else lowerer.runtimeOptionalBindingTypes.set(symbol, previous); + } + lowerer.implicitParamTypes = previousImplicit; + } +} diff --git a/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts b/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts index d55f71ff0..12885418b 100644 --- a/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts +++ b/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts @@ -33,6 +33,7 @@ import { isSafeToDiscard } from "../expressions/evaluation-safety.js"; import { STRING_INDEX_METHODS, STRING_REPLACE_METHODS, STR_METHODS } from "../surfaces.js"; import { execResultCaptureMask } from "./exec-result-uses.js"; import { lowerStringReplacement } from "./string-replacement.js"; +import { lowerReplacerCallback, regexLiteralCapturesOf } from "./replacer-params.js"; import { coerceStringSearchValue, defaultAfterUndefined, @@ -1025,7 +1026,11 @@ export function lowerRegexMethodCall( : [ lowerer.lowerExprExpecting(arg0, { kind: "regex" }), call.arguments.nodes[1] - ? lowerer.lowerExpr(call.arguments.nodes[1]) + ? lowerReplacerCallback( + lowerer, + call.arguments.nodes[1], + regexLiteralCapturesOf(arg0), + ) : dynUndefinedExpr(loc), ]; if (name !== "split") { diff --git a/packages/compiler/src/frontend/lowering/containers/string-replacement.ts b/packages/compiler/src/frontend/lowering/containers/string-replacement.ts index 156f74869..c21808464 100644 --- a/packages/compiler/src/frontend/lowering/containers/string-replacement.ts +++ b/packages/compiler/src/frontend/lowering/containers/string-replacement.ts @@ -18,6 +18,7 @@ import { import { dynUndefinedExpr, numLit, strLit, varRef } from "../../../ir/build.js"; import { InternalCompilerError } from "../../../errors.js"; import { locOf } from "../../program.js"; +import { PLAIN_SEARCH_CAPTURES, lowerReplacerCallback } from "./replacer-params.js"; import type { Lowerer } from "../lowerer.js"; import { coerceStringSearchValue, @@ -48,11 +49,18 @@ export function lowerStringReplacement( type.kind === "bool" || type.kind === "bigint" || isUnitType(type); - const argument = (node: ts.Expression | undefined, callback: boolean): IrExpr => { + const argument = ( + node: ts.Expression | undefined, + callback: boolean, + plainSearch = false, + ): IrExpr => { if (!node) return strLit("undefined", loc); const absent = lowerStaticallyUndefinedArgument(lowerer, node); if (absent) return defaultAfterUndefined(absent, strLit("undefined", loc)); - const value = lowerer.lowerExpr(node); + // A plain search passes (match, offset, subject). + const value = plainSearch + ? lowerReplacerCallback(lowerer, node, PLAIN_SEARCH_CAPTURES) + : lowerer.lowerExpr(node); if (value.type.kind === "dyn" || (callback && value.type.kind === "func")) return value; if (!primitive(value.type)) { lowerer.noLowering( @@ -64,7 +72,7 @@ export function lowerStringReplacement( return coerceStringSearchValue(lowerer, value, node, loc); }; const search = argument(args[0], false); - const replacement = argument(args[1], true); + const replacement = argument(args[1], true, search.type.kind !== "dyn"); if (search.type.kind === "dyn" || replacement.type.kind === "dyn") { // Evaluate all inputs once before choosing the search protocol. Regexes // retain their native pattern; plain values use the string-search helper. diff --git a/packages/compiler/src/frontend/lowering/cycle-initialization.ts b/packages/compiler/src/frontend/lowering/cycle-initialization.ts index 4b1e74fcf..30c52e382 100644 --- a/packages/compiler/src/frontend/lowering/cycle-initialization.ts +++ b/packages/compiler/src/frontend/lowering/cycle-initialization.ts @@ -233,6 +233,38 @@ function earlyHeritageReference(lowerer: Lowerer, binding: CycleEarlyBinding): t return null; } +/** The identifiers of `sf` by text, each with its position in the file's + * preorder walk: one walk per file serves every class's reference scan. */ +function fileIdentifiers( + lowerer: Lowerer, + sf: ts.SourceFile, +): ReadonlyMap { + const memo = lowerer.program.analysis.fileIdentifiers; + const known = memo.get(sf); + if (known !== undefined) return known; + const byText = new Map(); + let order = 0; + ts.walkPreorder(sf, (node) => { + order++; + if (!ts.isIdentifier(node)) return undefined; + const list = byText.get(node.text); + if (list === undefined) byText.set(node.text, [{ order, node }]); + else list.push({ order, node }); + return undefined; + }); + memo.set(sf, byText); + return byText; +} + +/** Whether `node` lies inside `ancestor` (or is it): the reference scans + * skip the class's own declaration. */ +function within(node: ts.Node, ancestor: ts.Node | undefined): boolean { + if (ancestor === undefined) return false; + for (let current: ts.Node | undefined = node; current !== undefined; current = current.parent) + if (current === ancestor) return true; + return false; +} + /** Whether `node` names `symbol`, through import aliases when `aliases`. */ function resolvesTo( lowerer: Lowerer, @@ -294,12 +326,14 @@ function classReferences( // the class's name or a local one. const names = classLocalNames(lowerer, sf, symbol, name.text, cycle); if (names.size === 0) continue; - ts.walkPreorder(sf, (node) => { - if (node === declaration) return "skip"; - if (ts.isIdentifier(node) && names.has(node.text) && resolvesTo(lowerer, node, symbol, cycle)) + // The file's identifiers under those names, in walk order. + const identifiers = fileIdentifiers(lowerer, sf); + const named: { order: number; node: ts.Identifier }[] = []; + for (const text of names) for (const entry of identifiers.get(text) ?? []) named.push(entry); + if (names.size > 1) named.sort((a, b) => a.order - b.order); + for (const { node } of named) + if (!within(node, declaration) && resolvesTo(lowerer, node, symbol, cycle)) out.push({ loc: locOf(node), name: node.text }); - return undefined; - }); } return out; } @@ -707,11 +741,17 @@ function rewriteFunction( * unchecked. */ export function refuseUncheckedCycleReads( lowerer: Lowerer, - classes: readonly { baseValueGlobal?: string; symbolFields?: { globalId: string }[] }[], + classes: readonly { + baseValueGlobal?: string; + symbolFields?: readonly ({ globalId: string } | { wellKnown: string })[]; + }[], ): void { if (lowerer.cycleInitFlags.size === 0) return; for (const cls of classes) { - const ids = [cls.baseValueGlobal, ...(cls.symbolFields ?? []).map((f) => f.globalId)]; + const ids = [ + cls.baseValueGlobal, + ...(cls.symbolFields ?? []).map((f) => ("globalId" in f ? f.globalId : undefined)), + ]; for (const id of ids) { const flag = id === undefined ? undefined : lowerer.cycleInitFlags.get(id); if (flag === undefined) continue; diff --git a/packages/compiler/src/frontend/lowering/date-values.ts b/packages/compiler/src/frontend/lowering/date-values.ts new file mode 100644 index 000000000..8931d0f80 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/date-values.ts @@ -0,0 +1,264 @@ +import * as ts from "../ts7/adapter.js"; +import type { Lowerer } from "./lowerer.js"; +import { isDateSetter } from "./builtins/dates.js"; + +/** Date methods whose receiver is only read for its time value: a Date + * local used as their receiver can store the time value instead of a Date + * object (builtins/dates.ts lowers them). */ +const TIME_VALUE_METHODS = new Set([ + "getTime", + "valueOf", + "toISOString", + "toJSON", + "toString", + "toDateString", + "toTimeString", + "toUTCString", + "getFullYear", + "getUTCFullYear", + "getMonth", + "getUTCMonth", + "getDate", + "getUTCDate", + "getDay", + "getUTCDay", + "getHours", + "getUTCHours", + "getMinutes", + "getUTCMinutes", + "getSeconds", + "getUTCSeconds", + "getMilliseconds", + "getUTCMilliseconds", + "getTimezoneOffset", +]); + +const RELATIONAL = new Set([ + ts.SyntaxKind.LessThanToken, + ts.SyntaxKind.LessThanEqualsToken, + ts.SyntaxKind.GreaterThanToken, + ts.SyntaxKind.GreaterThanEqualsToken, +]); + +/** True when a local `const` (or a `let` that is never assigned again) + * initialized with `new Date(...)` can keep the Date's time value instead + * of a Date object: no use observes the Date's identity. Every use reads + * the time value as a Date method receiver (a setter replaces it; the + * arguments of a method that reads it call no setter on it), a + * String(), Number() or template operand, a unary + or - operand, a + * relational or string `+` operand, or `new Date(d)`'s argument, inside + * the declaring function, or inside the block or loop that scopes a + * module-level binding (no closure captures it). Top-level module bindings + * keep the object. Other uses box the time value into a new Date at the + * use. */ +export function isTimeValueDateDeclaration( + lowerer: Lowerer, + decl: ts.VariableDeclaration, +): boolean { + const known = lowerer.timeValueDateDecisions.get(decl); + if (known !== undefined) return known; + const answer = decideTimeValueDate(lowerer, decl); + lowerer.timeValueDateDecisions.set(decl, answer); + return answer; +} + +function decideTimeValueDate(lowerer: Lowerer, decl: ts.VariableDeclaration): boolean { + if (!ts.isIdentifier(decl.name) || decl.initializer === undefined) return false; + // Only a Date the declaration creates: another reference to an existing + // Date can mutate it (`const d = a; a.setTime(0)`), and d must see that. + if (!createsDate(lowerer, decl.initializer)) return false; + const sf = decl.getSourceFile(); + if (sf.isDeclarationFile || /\.[cm]?jsx?$/.test(sf.fileName)) return false; + const list = decl.parent; + if (!ts.isVariableDeclarationList(list)) return false; + const isConst = (list.flags & ts.NodeFlags.Const) !== 0; + const isLet = (list.flags & ts.NodeFlags.Let) !== 0; + if (!isConst && !isLet) return false; + if (!ts.isVariableStatement(list.parent) && !ts.isForStatement(list.parent)) return false; + const fn = enclosingFunction(decl); + // The uses to check: the function body, or for a binding inside a block + // or loop at module level (a local of the module's initializer, not a + // module binding) the block or loop that scopes it. + let scope: ts.Node | undefined; + if (fn !== null) scope = fn.body; + else if (!inClassStaticBlock(decl)) scope = moduleBlockScope(decl); + if (scope === undefined) return false; + // The lib Date interface by its symbol first: mapping every candidate + // binding's type would intern its unions before the lowering does. + const typeSymbol = lowerer.typeOf(decl.name).getSymbol(); + if ( + typeSymbol === undefined || + ts.symbolName(typeSymbol) !== "Date" || + !typeSymbol.declarations.some( + (d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile()), + ) + ) + return false; + if (lowerer.mapTypeOf(lowerer.typeOf(decl.name))?.kind !== "date") return false; + const symbol = lowerer.checker.getSymbolAtLocation(decl.name); + if (!symbol) return false; + let ok = true; + let sets = false; + const visit = (node: ts.Node): void => { + if (!ok) return; + if (ts.isIdentifier(node) && node !== decl.name && node.text === decl.name.text) { + if (lowerer.checker.getSymbolAtLocation(node) === symbol) { + const use = enclosingFunction(node) === fn ? timeValueUse(lowerer, node, symbol) : null; + if (use === null) ok = false; + else if (use === "set") sets = true; + } + return; + } + ts.forEachChild(node, visit); + }; + visit(scope); + if (ok && sets) lowerer.timeValueDateWrites.add(decl); + return ok; +} + +/** True for `new Date(...)` of the standard Date, through parentheses and + * type-only wrappers: a Date no other reference can reach. */ +function createsDate(lowerer: Lowerer, init: ts.Expression): boolean { + let e = init; + while ( + ts.isParenthesizedExpression(e) || + ts.isAsExpression(e) || + ts.isSatisfiesExpression(e) || + ts.isTypeAssertion(e) || + ts.isNonNullExpression(e) + ) + e = e.expression; + return ts.isNewExpression(e) && lowerer.isStdlibGlobal(e.expression, "Date"); +} + +/** The block or for statement that scopes a module-level binding which is + * not a module binding: a declaration in a block or loop body, or in a + * for statement's initializer. Undefined for a top-level declaration. */ +function moduleBlockScope(decl: ts.VariableDeclaration): ts.Node | undefined { + const list = decl.parent; + if (!list || !ts.isVariableDeclarationList(list)) return undefined; + if (ts.isForStatement(list.parent)) return list.parent; + const statement = list.parent; + if (!ts.isVariableStatement(statement) || ts.isSourceFile(statement.parent)) return undefined; + for (let cur: ts.Node | undefined = statement.parent; cur; cur = cur.parent) { + if (ts.isBlock(cur) || ts.isCaseBlock(cur)) return cur; + if (ts.isSourceFile(cur)) return undefined; + } + return undefined; +} + +function inClassStaticBlock(node: ts.Node): boolean { + for (let cur = node.parent; cur; cur = cur.parent) { + if (ts.isClassStaticBlockDeclaration(cur)) return true; + if (ts.isSourceFile(cur)) return false; + } + return false; +} + +function enclosingFunction(node: ts.Node): ts.FunctionLikeDeclaration | null { + for (let cur = node.parent; cur; cur = cur.parent) { + if ( + ts.isFunctionDeclaration(cur) || + ts.isFunctionExpression(cur) || + ts.isArrowFunction(cur) || + ts.isMethodDeclaration(cur) || + ts.isConstructorDeclaration(cur) || + ts.isGetAccessorDeclaration(cur) || + ts.isSetAccessorDeclaration(cur) + ) + return cur; + if (ts.isClassStaticBlockDeclaration(cur) || ts.isSourceFile(cur)) return null; + } + return null; +} + +/** How a use of the binding uses its time value: "read" for a use that + * reads only the time value, "set" for a setter call that replaces it, or + * null for any other use. */ +function timeValueUse( + lowerer: Lowerer, + id: ts.Identifier, + symbol: ts.Symbol, +): "read" | "set" | null { + let node: ts.Node = id; + while (ts.isParenthesizedExpression(node.parent)) node = node.parent; + const parent = node.parent; + if (parent === undefined) return null; + if (ts.isPropertyAccessExpression(parent) && parent.expression === node) { + const call = parent.parent; + if ( + parent.questionDotToken || + !ts.isCallExpression(call) || + call.expression !== parent || + call.questionDotToken || + !lowerer.isStdlibMember(parent) + ) + return null; + if (isDateSetter(parent.name.text)) return "set"; + // The method reads the time value after its arguments are evaluated: + // a setter there (`d.toJSON(d.setTime(0))`) changes what it reads. + if (call.arguments.nodes.some((arg) => callsSetterOf(lowerer, arg, symbol))) return null; + return TIME_VALUE_METHODS.has(parent.name.text) ? "read" : null; + } + return readsTimeValue(lowerer, node, parent, symbol) ? "read" : null; +} + +/** True when `node` calls a setter on the binding `symbol` names. */ +function callsSetterOf(lowerer: Lowerer, node: ts.Node, symbol: ts.Symbol): boolean { + if ( + ts.isCallExpression(node) && + ts.isPropertyAccessExpression(node.expression) && + isDateSetter(node.expression.name.text) + ) { + let receiver = node.expression.expression; + while (ts.isParenthesizedExpression(receiver)) receiver = receiver.expression; + if (ts.isIdentifier(receiver) && lowerer.checker.getSymbolAtLocation(receiver) === symbol) + return true; + } + return ( + ts.forEachChild(node, (child) => callsSetterOf(lowerer, child, symbol) || undefined) ?? false + ); +} + +/** A use of the binding, other than a method call, that reads only its + * time value. */ +function readsTimeValue( + lowerer: Lowerer, + node: ts.Node, + parent: ts.Node, + symbol: ts.Symbol, +): boolean { + if (ts.isTemplateSpan(parent)) return parent.expression === node; + // Unary + and -, the number hint. + if (ts.isPrefixUnaryExpression(parent)) + return ( + parent.operator === ts.SyntaxKind.PlusToken || parent.operator === ts.SyntaxKind.MinusToken + ); + if (ts.isCallExpression(parent)) { + return ( + parent.arguments.nodes.length === 1 && + parent.arguments.nodes[0] === node && + ts.isIdentifier(parent.expression) && + (lowerer.isStdlibGlobal(parent.expression, "String") || + lowerer.isStdlibGlobal(parent.expression, "Number")) + ); + } + if (ts.isNewExpression(parent)) { + return ( + parent.arguments?.nodes.length === 1 && + parent.arguments.nodes[0] === node && + ts.isIdentifier(parent.expression) && + lowerer.isStdlibGlobal(parent.expression, "Date") + ); + } + if (ts.isBinaryExpression(parent)) { + // A left operand converts after the right one is evaluated: a setter + // there changes the time value the comparison or concatenation reads. + if (parent.left === node && callsSetterOf(lowerer, parent.right, symbol)) return false; + const other = parent.left === node ? parent.right : parent.left; + const otherKind = lowerer.mapTypeOf(lowerer.typeOf(other))?.kind; + if (RELATIONAL.has(parent.operatorToken.kind)) return otherKind === "date"; + if (parent.operatorToken.kind === ts.SyntaxKind.PlusToken) return otherKind === "string"; + } + return false; +} diff --git a/packages/compiler/src/frontend/lowering/decl-origin.ts b/packages/compiler/src/frontend/lowering/decl-origin.ts new file mode 100644 index 000000000..6eae57966 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/decl-origin.ts @@ -0,0 +1,436 @@ +/* Origins of package-declared types at checked conversions. + * + * scriptc enforces declared types: a value that contradicts the type an + * npm package declares (its .d.ts, @types package or JSDoc) throws a + * TypeError where Node runs on. When the declaration is what is wrong, the + * fix is a type patch (package.json scriptc.typePatches), so the error + * names the declaration and the patch that would admit the value: + * + * TypeError: expected number at $, got undefined + * termctx@1.2.0 declares Terminal.columns() as number (index.js:14) + * if that declaration is wrong, patch it in package.json: "scriptc": + * { "typePatches": { "termctx@^1.2.0": { "declarations": + * { "Terminal.columns()": "| undefined" } } } } + * + * The origin is found for three kinds of slots: a value a package function + * returns under a written return type, a property of an object literal + * typed by a package-declared type, and a declaration-boundary cast (a + * value user code receives from a package; decl-projection.ts records + * those with originAt). Generic declarations get none: their types come + * from how the program instantiates them. Checks in + * code that names no package declaration carry no origin, so programs + * without npm packages lower exactly as before. + * + * The origin string's fields (separated by \x1f) are read by the runtime + * (scr_type_origin.c) for checked conversions, whose path and actual kind + * are only known when they fail, and rendered here for union extractions, + * whose failing arm is known statically. */ + +import type { IrExpr, IrModule } from "../../ir/ir.js"; +import { everyModuleNode } from "../../ir/traverse.js"; +import { trackedReadFile } from "../input-tracker.js"; +import * as ts from "../ts7/adapter.js"; + +export const ORIGIN_SEPARATOR = "\x1f"; + +interface PackageFile { + pkg: string; + version: string; + /** The file relative to the package root. */ + rel: string; +} + +/** Installed package versions by package root, read once per program load + * (resetDeclOriginVersions): a dependency reinstalled at another version + * between two builds of one process is read again. */ +const versions = new Map(); + +export function resetDeclOriginVersions(): void { + versions.clear(); +} + +function packageFileOf(fileName: string): PackageFile | null { + const path = fileName.split("\\").join("/"); + const marker = "/node_modules/"; + const at = path.lastIndexOf(marker); + if (at < 0) return null; + const parts = path.slice(at + marker.length).split("/"); + const nameParts = parts[0]!.startsWith("@") ? 2 : 1; + if (parts.length <= nameParts) return null; + let pkg = parts.slice(0, nameParts).join("/"); + const root = path.slice(0, at + marker.length) + pkg; + let versionRoot = root; + if (pkg.startsWith("@types/")) { + // A twin describes the runtime package installed beside it. + const bare = pkg.slice("@types/".length); + const i = bare.indexOf("__"); + pkg = i > 0 ? `@${bare.slice(0, i)}/${bare.slice(i + 2)}` : bare; + versionRoot = path.slice(0, at + marker.length) + pkg; + } + let version = versions.get(versionRoot); + if (version === undefined) { + version = ""; + try { + const parsed = JSON.parse(trackedReadFile(`${versionRoot}/package.json`) ?? "{}") as { + version?: unknown; + }; + if (typeof parsed.version === "string") version = parsed.version; + } catch { + version = ""; + } + versions.set(versionRoot, version); + } + return { pkg, version, rel: parts.slice(nameParts).join("/") }; +} + +/** True for a node in an installed package's file. */ +export function inPackageFile(node: ts.Node): boolean { + return packageFileOf(ts.getSourceFileOfNode(node).fileName) !== null; +} + +function lineOf(node: ts.Node): number { + const sf = ts.getSourceFileOfNode(node); + return ts.getLineAndCharacterOfPosition(sf, ts.tokenStartOfNode(node)).line + 1; +} + +function encode(file: PackageFile, selector: string, where: string, declared: string): string { + const range = file.version === "" ? "*" : `^${file.version}`; + return [file.pkg, file.version, range, selector, where, declared.replace(/\s+/g, " ")].join( + ORIGIN_SEPARATOR, + ); +} + +function nameOf(name: ts.Node | undefined): string | null { + if (name === undefined) return null; + if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isPrivateIdentifier(name)) + return name.text; + return null; +} + +/** `module.exports` → "export="; `exports.x` / `module.exports.x` → "x". */ +function exportTarget(left: ts.Node): string | null { + if (!ts.isPropertyAccessExpression(left)) return null; + const obj = left.expression; + const prop = nameOf(left.name); + if (prop === null) return null; + if (ts.isIdentifier(obj) && obj.text === "module" && prop === "exports") return "export="; + if (ts.isIdentifier(obj) && obj.text === "exports") return prop; + if ( + ts.isPropertyAccessExpression(obj) && + ts.isIdentifier(obj.expression) && + obj.expression.text === "module" && + nameOf(obj.name) === "exports" + ) + return prop; + return null; +} + +/** The selector naming a function-like declaration (without `()`), or + * null when it has no name a selector can reach. */ +function functionSelector(fn: ts.Node): string | null { + if (ts.isFunctionDeclaration(fn)) return nameOf(fn.name); + if (ts.isMethodDeclaration(fn) || ts.isGetAccessor(fn)) { + const method = nameOf(fn.name); + const holder = fn.parent; + if (method === null || holder === undefined) return null; + if (ts.isClassDeclaration(holder) || ts.isClassExpression(holder)) { + const cls = nameOf(holder.name); + return cls === null ? null : `${cls}.${method}`; + } + if (ts.isObjectLiteralExpression(holder)) { + const owner = valueSelector(holder); + return owner === null ? null : `${owner}.${method}`; + } + return null; + } + if (ts.isFunctionExpression(fn) || ts.isArrowFunction(fn)) return valueSelector(fn); + return null; +} + +/** The selector of the declaration an expression is the value of. */ +function valueSelector(value: ts.Node): string | null { + let e = value; + while (e.parent !== undefined && ts.isParenthesizedExpression(e.parent)) e = e.parent; + const p = e.parent; + if (p === undefined) return null; + if (ts.isVariableDeclaration(p) && p.initializer === e) return nameOf(p.name); + if ( + ts.isBinaryExpression(p) && + p.right === e && + p.operatorToken.kind === ts.SyntaxKind.EqualsToken + ) + return exportTarget(p.left); + if (ts.isPropertyAssignment(p) && p.initializer === e && p.parent !== undefined) { + const name = nameOf(p.name); + const owner = ts.isObjectLiteralExpression(p.parent) ? valueSelector(p.parent) : null; + return name === null || owner === null ? null : `${owner}.${name}`; + } + return null; +} + +/** True when a function-like declaration writes its return type: a type + * annotation, or a JSDoc @returns tag. */ +function writesReturnType(fn: ts.Node): boolean { + if ((fn as ts.FunctionDeclaration).type !== undefined) return true; + const sf = ts.getSourceFileOfNode(fn); + let owner: ts.Node = fn; + // The JSDoc of `exports.f = function`, `const f = () =>` sits on the + // statement. + while ( + owner.parent !== undefined && + !ts.isSourceFile(owner.parent) && + !ts.isBlock(owner.parent) && + !ts.isClassDeclaration(owner.parent) && + !ts.isClassExpression(owner.parent) && + !ts.isObjectLiteralExpression(owner.parent) + ) + owner = owner.parent; + const leading = sf.text.slice(owner.pos, ts.tokenStartOfNode(owner)); + const doc = leading.lastIndexOf("/**"); + return doc >= 0 && /@returns?\s*\{/.test(leading.slice(doc)); +} + +function enclosingFunction(node: ts.Node): ts.Node | null { + for (let p = node.parent; p !== undefined; p = p.parent) { + if ( + ts.isFunctionDeclaration(p) || + ts.isFunctionExpression(p) || + ts.isArrowFunction(p) || + ts.isMethodDeclaration(p) || + ts.isGetAccessor(p) + ) + return p; + if (ts.isSourceFile(p) || ts.isClassDeclaration(p)) return null; + } + return null; +} + +function typeText(checker: ts.TypeChecker, type: ts.Type | undefined): string { + if (type === undefined) return ""; + try { + return checker.typeToString(type); + } catch { + return ""; + } +} + +/** The origin of the slot a value expression in a package file flows + * into, or null when no package declaration types that slot. */ +export function slotOrigin(checker: ts.TypeChecker, node: ts.Node): string | null { + let value = node; + while (value.parent !== undefined && ts.isParenthesizedExpression(value.parent)) + value = value.parent; + const parent = value.parent; + if (parent === undefined) return null; + // A property of an object literal typed by a declared type. + if ( + (ts.isPropertyAssignment(parent) && parent.initializer === value) || + ts.isShorthandPropertyAssignment(value) || + (ts.isShorthandPropertyAssignment(parent) && parent.name === value) + ) { + const property = ts.isShorthandPropertyAssignment(value) ? value : parent; + const literal = property.parent; + const name = nameOf((property as ts.PropertyAssignment).name); + if (literal === undefined || !ts.isObjectLiteralExpression(literal) || name === null) + return null; + const contextual = checker.getContextualType(literal); + if (contextual === undefined) return null; + const typeSymbol = contextual.getAliasSymbol() ?? contextual.getSymbol(); + const typeName = typeSymbol === undefined ? "" : ts.symbolName(typeSymbol); + if (!/^[A-Za-z_$][\w$]*$/.test(typeName)) return null; + const symbol = checker.getPropertyOfType(contextual, name); + const decl = symbol?.declarations[0]; + if (symbol === undefined || decl === undefined || genericDeclaration(decl)) return null; + const file = packageFileOf(ts.getSourceFileOfNode(decl).fileName); + if (file === null) return null; + return encode( + file, + `${typeName}.${name}`, + `${file.rel}:${lineOf(decl)}`, + typeText(checker, checker.getTypeOfSymbol(symbol)), + ); + } + // A value a package function returns under its written return type. + let fn: ts.Node | null = null; + if (ts.isReturnStatement(parent) && parent.expression === value) fn = enclosingFunction(parent); + else if (ts.isArrowFunction(parent) && parent.body === value) fn = parent; + if (fn === null || !writesReturnType(fn)) return null; + const file = packageFileOf(ts.getSourceFileOfNode(fn).fileName); + const selector = functionSelector(fn); + if (file === null || selector === null) return null; + let declared = ""; + try { + const signature = checker.getSignatureFromDeclaration(fn as ts.SignatureDeclaration); + declared = + signature === undefined ? "" : typeText(checker, checker.getReturnTypeOfSignature(signature)); + } catch { + declared = ""; + } + return encode(file, `${selector}()`, `${file.rel}:${lineOf(fn)}`, declared); +} + +/** A package-declared record type a value is converted to (the + * contextual type of a value in package code, such as an object literal + * returned under `@returns {HelpContext}`), or null. */ +export interface RecordOrigin { + type: ts.Type; + name: string; + /** The declaration's identity (package, version, file, line and name): + * records of the same shape and name declared by two packages differ. */ + key: string; +} + +export function recordOrigin(checker: ts.TypeChecker, node: ts.Node): RecordOrigin | null { + const contextual = checker.getContextualType(node); + if (contextual === undefined) return null; + const symbol = contextual.getAliasSymbol() ?? contextual.getSymbol(); + const name = symbol === undefined ? "" : ts.symbolName(symbol); + if (!/^[A-Za-z_$][\w$]*$/.test(name)) return null; + const decl = symbol?.declarations[0]; + if (decl === undefined || genericDeclaration(decl)) return null; + const file = packageFileOf(ts.getSourceFileOfNode(decl).fileName); + if (file === null) return null; + const key = [file.pkg, file.version, file.rel, lineOf(decl), name].join(ORIGIN_SEPARATOR); + return { type: contextual, name, key }; +} + +/** The origin of one field of a package-declared record type. */ +export function fieldOrigin( + checker: ts.TypeChecker, + record: RecordOrigin, + field: string, +): string | null { + const symbol = checker.getPropertyOfType(record.type, field); + const decl = symbol?.declarations[0]; + if (symbol === undefined || decl === undefined || genericDeclaration(decl)) return null; + const file = packageFileOf(ts.getSourceFileOfNode(decl).fileName); + if (file === null) return null; + return encode( + file, + `${record.name}.${field}`, + `${file.rel}:${lineOf(decl)}`, + typeText(checker, checker.getTypeOfSymbol(symbol)), + ); +} + +/** The origin of a declaration in a package file named by `selector`. */ +export function originAt(decl: ts.Node, selector: string, declared: string): string | null { + const file = packageFileOf(ts.getSourceFileOfNode(decl).fileName); + if (file === null) return null; + return encode(file, selector, `${file.rel}:${lineOf(decl)}`, declared); +} + +/** True when a declaration or the class, interface or type alias holding + * it has type parameters: its declared type then comes from how the + * program instantiates it (a lying `z.custom()`), not from the package. */ +function genericDeclaration(decl: ts.Node): boolean { + for (let n: ts.Node | undefined = decl; n !== undefined && !ts.isSourceFile(n); n = n.parent) { + const params = (n as ts.FunctionDeclaration).typeParameters; + if (params !== undefined && params.nodes.length > 0) return true; + } + return false; +} + +/** The selector of a declaration in a declaration file: a function, a + * member of a class, interface or named object type, or null (also for + * generic declarations, whose types the program supplies). */ +export function declarationSelector(decl: ts.Node): string | null { + if (genericDeclaration(decl)) return null; + const own = nameOf((decl as ts.FunctionDeclaration).name); + if (own === null) return null; + if (ts.isFunctionDeclaration(decl) || ts.isVariableDeclaration(decl)) return own; + let holder = decl.parent; + if (holder !== undefined && ts.isTypeLiteralNode(holder)) holder = holder.parent; + if ( + holder !== undefined && + (ts.isClassDeclaration(holder) || + ts.isInterfaceDeclaration(holder) || + ts.isTypeAliasDeclaration(holder)) + ) { + const owner = nameOf(holder.name); + return owner === null ? null : `${owner}.${own}`; + } + return null; +} + +/** One argument of the suggested `scriptc types patch` command, quoted so + * the shell passes it as written (scr_type_origin.c quotes the same way): + * bare when it has only characters no shell treats specially; otherwise, + * for a POSIX shell, in single quotes with each `'` as `'\''`, and for + * Windows (cmd and PowerShell), in double quotes when it has none of the + * characters either shell expands, escapes or ends a string with there. Null when no quoting + * is inert there: the note then names the command without arguments. + * Package names, selectors and types come from package declarations. */ +export function shellArgument(value: string, windows: boolean): string | null { + if (/^[A-Za-z0-9_@+=:,./-]+$/.test(value)) return value; + if (!windows) + return /^[A-Za-z0-9_@%+=:,./-]+$/.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`; + for (const ch of value) { + const code = ch.codePointAt(0)!; + // PowerShell also takes the typographic quotes U+2018-U+201F as quotes. + if (code < 0x20 || code === 0x7f || (code >= 0x2018 && code <= 0x201f)) return null; + if ('"%!^$`'.includes(ch)) return null; + } + return `"${value}"`; +} + +/** The suggested patch for a declaration: `scriptc types patch` with the + * package, the selector and the type, each quoted for the target's shell. */ +export function patchSuggestion( + pkg: string, + selector: string, + type: string, + windows: boolean, +): string { + const args = [pkg, selector, type].map((a) => shellArgument(a, windows)); + return args.every((a) => a !== null) + ? `if that declaration is wrong, patch it: scriptc types patch ${args.join(" ")}` + : "if that declaration is wrong, patch it with scriptc types patch"; +} + +/** The note a union extraction's TypeError carries for a failing `arm` + * (the same text the runtime renders for checked conversions). `windows`: + * the program targets Windows, whose shells quote differently. */ +export function renderOriginNote(origin: string, arm: string, windows = false): string { + const [pkg = "", version = "", , selector = "", where = "", declared = ""] = + origin.split(ORIGIN_SEPARATOR); + return ( + `\n ${pkg}${version === "" ? "" : `@${version}`} declares ${selector}` + + `${declared === "" ? "" : ` as ${declared}`} (${where})` + + `\n ${patchSuggestion(pkg, selector, `| ${arm}`, windows)}` + ); +} + +/** Keeps only the origins the module's checks still reference (a coercion + * whose check was later replaced leaves its origin behind), renumbered in + * first-use order; drops the table when none is left, so the runtime unit + * is not linked for nothing. */ +export function compactDeclOrigins(mod: IrModule): void { + const table = mod.declOrigins ?? []; + const remap = new Map(); + const used: string[] = []; + // A node the IR shares at several places is renumbered once. + const checks = new Set(); + everyModuleNode(mod, { + expr: (e) => { + if (e.kind === "dynCheck" && e.origin !== undefined) { + // An exact capsule check never throws: nothing to name. + if (e.exact === true) { + delete e.origin; + return true; + } + checks.add(e); + if (!remap.has(e.origin)) { + remap.set(e.origin, used.length); + used.push(table[e.origin]!); + } + } + return true; + }, + stmt: () => true, + type: () => true, + }); + for (const check of checks) check.origin = remap.get(check.origin!)!; + if (used.length === 0) delete mod.declOrigins; + else mod.declOrigins = used; +} diff --git a/packages/compiler/src/frontend/lowering/deferred-module-initializers.test.ts b/packages/compiler/src/frontend/lowering/deferred-module-initializers.test.ts index 4696659bc..4786585d9 100644 --- a/packages/compiler/src/frontend/lowering/deferred-module-initializers.test.ts +++ b/packages/compiler/src/frontend/lowering/deferred-module-initializers.test.ts @@ -168,17 +168,19 @@ test("reports the functions whose placeholders it fills", () => { expect(filled).toEqual([functions[0]]); }); -test("an update of the binding demands its initializer", () => { +test("an update or an assignment of the binding demands its initializer", () => { const { initializers, pending, lowerer } = fixture(); const updated = block(loc), - written = block({ ...loc, start: 2, end: 3 }); + written = block({ ...loc, start: 2, end: 3 }), + assigned = block({ ...loc, start: 4, end: 5 }), + unused = block({ ...loc, start: 6, end: 7 }); const update = (id: string, prefix: boolean): IrStmt => ({ kind: "exprStmt", expr: { kind: "incDec", op: "+", prefix, localId: id, type: F64, loc }, loc, }); const functions = [ - fn("init", [updated, written]), + fn("init", [updated, written, assigned, unused]), fn("use", [ update("updated", false), { @@ -187,13 +189,29 @@ test("an update of the binding demands its initializer", () => { value: { kind: "numLit", value: 1, type: F64, loc }, loc, }, + { + kind: "exprStmt", + expr: { + kind: "assignExpr", + localId: "assigned", + value: { kind: "numLit", value: 2, type: F64, loc }, + type: F64, + loc, + }, + loc, + }, ]), ]; pending.set("updated", [{ loc, lower: () => [value(2)] }]); pending.set("written", [{ loc: written.loc, lower: () => [value(3)] }]); + pending.set("assigned", [{ loc: assigned.loc, lower: () => [value(4)] }]); + pending.set("unused", [{ loc: unused.loc, lower: () => [value(5)] }]); expect(initializers.process(lowerer, functions)).toBe(true); expect(updated.body).toEqual([value(2)]); - // A plain write never observes the initial value. - expect(written.body).toEqual([]); - expect(pending.has("written")).toBe(true); + // An assignment checks that the binding is initialized and releases its + // value, so the declaration must have run. + expect(written.body).toEqual([value(3)]); + expect(assigned.body).toEqual([value(4)]); + expect(unused.body).toEqual([]); + expect(pending.has("unused")).toBe(true); }); diff --git a/packages/compiler/src/frontend/lowering/deferred-module-initializers.ts b/packages/compiler/src/frontend/lowering/deferred-module-initializers.ts index a44df8f75..3af054dd6 100644 --- a/packages/compiler/src/frontend/lowering/deferred-module-initializers.ts +++ b/packages/compiler/src/frontend/lowering/deferred-module-initializers.ts @@ -387,18 +387,22 @@ function scanStatements( const placeholders: PlaceholderBlock[] = []; const typeEvents = lowerer ? typeScanner(lowerer, body) : null; const reads = new Set(); + const observe = (id: string): void => { + if (deferred!.has(id) && !reads.has(id)) { + reads.add(id); + body.push({ kind: "read", id }); + } + }; everyStmtList(statements, { stmt: (statement) => { if (statement.kind === "block" && statement.body.length === 0) placeholders.push(statement); + if (typeEvents !== null && statement.kind === "assign") observe(statement.localId); return true; }, expr: (expr) => { if (typeEvents === null) return true; typeEvents(expr.type); - if (readsBinding(expr) && deferred!.has(expr.localId) && !reads.has(expr.localId)) { - reads.add(expr.localId); - body.push({ kind: "read", id: expr.localId }); - } + if (observesBinding(expr)) observe(expr.localId); // Every call, as the helpers are visited from a stack in call order. if ( expr.kind === "call" && @@ -411,10 +415,14 @@ function scanStatements( return { body, placeholders }; } -/** A read of a binding's current value: a plain read, or an update - * (`n++`, `--n`) that reads before it writes. */ -function readsBinding(expr: IrExpr): expr is IrExpr & { kind: "varRef" | "incDec" } { - return expr.kind === "varRef" || expr.kind === "incDec"; +/** A use that needs the binding initialized: a plain read, an update + * (`n++`, `--n`) that reads before it writes, or an assignment, which + * checks that the binding is initialized and releases its current value. + * Assignment statements count the same way (scanStatements). */ +function observesBinding( + expr: IrExpr, +): expr is IrExpr & { kind: "varRef" | "incDec" | "assignExpr" } { + return expr.kind === "varRef" || expr.kind === "incDec" || expr.kind === "assignExpr"; } function registryInitializer( diff --git a/packages/compiler/src/frontend/lowering/dispatch-sites.ts b/packages/compiler/src/frontend/lowering/dispatch-sites.ts index b9ab8f069..33890ab52 100644 --- a/packages/compiler/src/frontend/lowering/dispatch-sites.ts +++ b/packages/compiler/src/frontend/lowering/dispatch-sites.ts @@ -72,6 +72,7 @@ const DISPATCH_LIB_CALLS = new Set([ "dyn.keySetComputed", "dyn.iterator", "dyn.arrayFromIterator", + "dyn.asyncIteratorOpen", "dyn.toString", "dyn.stringConstructor", "dyn.forInKeys", diff --git a/packages/compiler/src/frontend/lowering/expressions/array-literals.ts b/packages/compiler/src/frontend/lowering/expressions/array-literals.ts index 6c7ce814e..5854a0ae8 100644 --- a/packages/compiler/src/frontend/lowering/expressions/array-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/array-literals.ts @@ -1,4 +1,5 @@ /* Array literals. */ +import { indexedUnionArms, lowerIndexedUnionToArray } from "../indexed-unions.js"; import * as ts from "../../ts7/adapter.js"; import { type Lowerer, neverTaintedJsType } from "../lowerer.js"; import { notIterableSpelling } from "../iterable-spelling.js"; @@ -28,10 +29,12 @@ import { lowerTernary } from "./narrowing.js"; import { lowerCollectionSpread } from "../containers/collection-methods.js"; import { strCharsCall } from "../containers/array-construction.js"; import { generatorDrain } from "../iterator-adapters.js"; +import { isMimeParamsIterable } from "../builtins/mime.js"; import { isSearchParamsIterable } from "../builtins/url.js"; -import { varRef } from "../../../ir/build.js"; -import { arrayValueStore } from "../array-values.js"; +import { dynUndefinedExpr, varRef } from "../../../ir/build.js"; +import { arrayValueStore, primitiveElement } from "../array-values.js"; import { classGeneratorIterable } from "../class-iterables.js"; +import { iteratorCanStep } from "../iterator-consumption.js"; /** JavaScript: whether `expr` is or holds an existing checked-dynamic * object — a binding stored as a dyn value whose checker type is an @@ -101,6 +104,13 @@ function emptyNeverLiteral(lowerer: Lowerer, expr: ts.ArrayLiteralExpression): b * tsc's API doesn't surface it (ternary arms under an array context — * tsc accepts the arm covariantly, but a tagged element representation * must be BUILT as the slot's element type). */ +/** The checker type of `node` admits undefined. */ +function mayHoldUndefined(lowerer: Lowerer, node: ts.Expression): boolean { + const type = lowerer.typeOf(node); + const parts = type.isUnionType() ? ts.constituentTypes(type) : [type]; + return parts.some((part) => (part.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Void)) !== 0); +} + export function lowerArrayLiteral( lowerer: Lowerer, expr: ts.ArrayLiteralExpression, @@ -481,13 +491,21 @@ export function lowerArrayLiteral( if (src.type.kind === "object") { const drained = lowerer.classIteratorDrainCall(src, locOf(el), type.elem); if (drained) src = drained; + // In JavaScript an iterable class whose inferred iterator shape has + // no static protocol spreads through the runtime's iterator walk. + else if ( + isJsSourceFile(ts.getSourceFileOfNode(el)) && + lowerer.classes.get(src.type.className) && + lowerer.findMethodOn(lowerer.classes.get(src.type.className)!, "sym:iterator") + ) + src = lowerer.coerceInto(el.expression, src, DYN); } // `[...s]` on a STRING spreads its code-point characters (the // string iterator's walk — astral chars whole) through the same // interned helper as Array.from(s); the result rides the array // machinery below like any string[] source. if (src.type.kind === "string") { - src = strCharsCall(lowerer, src, locOf(el)); + src = strCharsCall(src, locOf(el)); } // `[...typedArray]`: represented typed arrays are dense numeric // iterables, so drain their elements into the fresh number[] that @@ -504,19 +522,31 @@ export function lowerArrayLiteral( loc: locOf(el), }; } + // A union of typed arrays and number arrays (indexed-unions.ts): + // each arm's elements as a number[]. + const indexedArms = indexedUnionArms(lowerer, src.type); + if (indexedArms) src = lowerIndexedUnionToArray(lowerer, src, indexedArms, locOf(el)); // `[...obj]` over a class whose [Symbol.iterator] is a generator // method spreads the generator that method returns. src = classGeneratorIterable(lowerer, src) ?? src; // `[...it]` over a generator (or an iterator adapted into one): // drain it once, in order, into a fresh element array. if (src.type.kind === "generator") { - const drained = generatorDrain(lowerer, src, type.elem, locOf(el)); + const drained = generatorDrain( + lowerer, + src, + type.elem, + locOf(el), + type.optionalElements === true, + ); if (drained) src = drained; } // `[...sp]` and `[...sp.entries()]` over a URLSearchParams: its // checked-dynamic view iterates the live pair list, and the drained // [name, value] pairs convert to the literal's tuple elements. - const searchParamsSource = isSearchParamsIterable(lowerer, el.expression); + const searchParamsSource = + isSearchParamsIterable(lowerer, el.expression) || + isMimeParamsIterable(lowerer, el.expression); if (src.type.kind === "searchParams") src = lowerer.coerceInto(el.expression, src, DYN); // A native checked iterable can supply scalar elements even when // the checker inferred a typed array from its producer. Drain once @@ -601,6 +631,9 @@ export function lowerArrayLiteral( let lowered: IrExpr; const directOptionalCandidate = runtimeOptionalElements.has(i) || + // An element that can be undefined in a `(T | undefined)[]` + // (primitiveElement) is stored as a present undefined. + (primitiveElement(type.elem) && mayHoldUndefined(lowerer, el)) || ts.isElementAccessExpression(el) || (ts.isCallExpression(el) && ts.isPropertyAccessExpression(el.expression) && @@ -678,7 +711,8 @@ export function lowerArrayLiteral( continue; } let value = el; - if (!typeEquals(value.type, type.elem)) { + // An undefined element is stored directly (arrayValueStore). + if (!typeEquals(value.type, type.elem) && value.type.kind !== "undefinedT") { const temp = lowerer.declareHiddenLocal("%arrayValue", value.type); body.push({ kind: "varDecl", localId: temp.id, init: value, loc }); value = varRef(temp.id, temp.type, loc); @@ -765,13 +799,17 @@ function lowerTupleSpreadLiteral( return { kind: "seqExpr", stmts, result: { kind: "recordLit", fields, type, loc }, type, loc }; } -/** Drain using checked dispatch so native class and generator iterators keep their methods. - * `spell` is the TypeError message for a source that is not iterable - * (notIterableSpelling). */ +/** Drain a checked-dynamic iterable: `next` is read once when the iterator + * opens, as JS keeps it in the iterator record, so a `next` the iterator + * assigns while it runs is not called. Runtime iterators step natively, and + * other iterators call the `next` read (class methods keep their emitted + * dispatch). `spell` is the TypeError message for a source that is not + * iterable (notIterableSpelling). */ function checkedIteratorPack(lowerer: Lowerer, source: IrExpr, spell: string, loc: SrcLoc): IrExpr { const name = "%iterator.pack.checked"; if (!lowerer.liftedFns.some((fn) => fn.name === name)) { const ref = (id: string): IrExpr => ({ kind: "varRef", localId: id, type: DYN, loc }); + const flag = (id: string): IrExpr => ({ kind: "varRef", localId: id, type: BOOL, loc }); const property = (key: string): IrExpr => ({ kind: "dynKeyGet", value: ref("step"), @@ -779,6 +817,7 @@ function checkedIteratorPack(lowerer: Lowerer, source: IrExpr, spell: string, lo type: DYN, loc, }); + const finished: IrStmt = { kind: "return", value: ref("result"), loc }; lowerer.liftedFns.push({ name, params: [ @@ -787,12 +826,13 @@ function checkedIteratorPack(lowerer: Lowerer, source: IrExpr, spell: string, lo ], returnType: DYN, locals: [ - ...["source", "iterator", "step", "result"].map((id) => ({ + ...["source", "iterator", "next", "step", "value", "result"].map((id) => ({ id, name: id, type: DYN, mutable: id !== "source", })), + { id: "fast", name: "fast", type: BOOL, mutable: false }, { id: "spell", name: "spell", type: STRING, mutable: false }, ], body: [ @@ -808,6 +848,24 @@ function checkedIteratorPack(lowerer: Lowerer, source: IrExpr, spell: string, lo }, loc, }, + { + kind: "varDecl", + localId: "next", + init: { + kind: "dynKeyGet", + value: ref("iterator"), + key: { kind: "strLit", value: "next", type: STRING, loc }, + type: DYN, + loc, + }, + loc, + }, + { + kind: "varDecl", + localId: "fast", + init: iteratorCanStep(ref("iterator"), ref("next"), loc), + loc, + }, { kind: "varDecl", localId: "result", @@ -818,25 +876,77 @@ function checkedIteratorPack(lowerer: Lowerer, source: IrExpr, spell: string, lo kind: "while", cond: { kind: "boolLit", value: true, type: BOOL, loc }, body: [ - { - kind: "varDecl", - localId: "step", - init: { - kind: "dynInvoke", - recv: ref("iterator"), - method: "next", - calleeName: "iterator.next", - args: [], - type: DYN, - loc, - }, - loc, - }, + // Declared before the branches, which each assign it: a local + // declared in a branch is released when the branch ends. + { kind: "varDecl", localId: "value", init: dynUndefinedExpr(loc), loc }, { kind: "if", - cond: { kind: "dynTest", test: "truthy", value: property("done"), type: BOOL, loc }, - then: [{ kind: "return", value: ref("result"), loc }], - else_: null, + cond: flag("fast"), + then: [ + { + kind: "assign", + localId: "value", + value: { + kind: "libCall", + fn: "dyn.iteratorStep", + args: [ref("iterator")], + type: DYN, + loc, + }, + loc, + }, + { + kind: "if", + cond: { + kind: "libCall", + fn: "dyn.iteratorStepDone", + args: [ref("iterator")], + type: BOOL, + loc, + }, + then: [finished], + else_: null, + loc, + }, + ], + else_: [ + { + kind: "varDecl", + localId: "step", + init: { + kind: "libCall", + fn: "dyn.iteratorResult", + args: [ + { + kind: "dynCall", + callee: ref("next"), + receiver: ref("iterator"), + calleeName: "iterator.next", + args: [], + type: DYN, + loc, + }, + ], + type: DYN, + loc, + }, + loc, + }, + { + kind: "if", + cond: { + kind: "dynTest", + test: "truthy", + value: property("done"), + type: BOOL, + loc, + }, + then: [finished], + else_: null, + loc, + }, + { kind: "assign", localId: "value", value: property("value"), loc }, + ], loc, }, { @@ -844,7 +954,7 @@ function checkedIteratorPack(lowerer: Lowerer, source: IrExpr, spell: string, lo expr: { kind: "libCall", fn: "dyn.packPush", - args: [ref("result"), property("value")], + args: [ref("result"), ref("value")], type: VOID, loc, }, diff --git a/packages/compiler/src/frontend/lowering/expressions/binary.ts b/packages/compiler/src/frontend/lowering/expressions/binary.ts index 17aef7b85..0d24561de 100644 --- a/packages/compiler/src/frontend/lowering/expressions/binary.ts +++ b/packages/compiler/src/frontend/lowering/expressions/binary.ts @@ -58,6 +58,7 @@ import { fenceNodeModuleMutation, lowerRequireCacheHas } from "../lower-node-mod import { errorToStringCall } from "../error-methods.js"; import { classToStringConversion, classUnionToString } from "../class-to-primitive.js"; import { isPerfEntryType, perfEntryTag } from "../builtins/performance.js"; +import { isX509Type, x509ToString } from "../builtins/x509.js"; import { guardNullProto, nullProtoRecordShape } from "../null-prototype.js"; import { numLit, varRef } from "../../../ir/build.js"; import { lowerUrlAssignment } from "../builtins/url.js"; @@ -243,37 +244,38 @@ function lowerLooseNullCompare( // No unit arms: never null-ish (defensive — the fold below). return { kind: "boolLit", value: negated, type: BOOL, loc }; } - const isTag = (tag: number): IrExpr => ({ + const isTag = (tag: number, value: IrExpr): IrExpr => ({ kind: "unionIsTag", unionId: ut.unionId, tag, negated, - value: other, + value, type: BOOL, loc, }); - if (tags.length === 1) return isTag(tags[0]!); + if (tags.length === 1) return isTag(tags[0]!, other); // Both null AND undefined arms: tag-in-set as a short-circuit pair - // (De Morgan for `!=`). The operand IrExpr rides both tests, so the - // backend emits it per test — restricted to re-emittable pure reads; - // anything effectful gets its own actionable fence (the generic - // SC1040 hint would wrongly suggest `== null` as the fix). + // (De Morgan for `!=`). The operand rides both tests, so an operand + // that is not a plain read (`m.get(k)! == null`) is evaluated once + // into a hidden local first. + let value = other; + let prefix: IrStmt[] = []; if (!isSafeToRepeat(other)) { - lowerer.unsupported( - "SC1090", - expr, - `'== null' / '!= null' on a '${lowerer.fmt(ut)}' value that isn't a plain read ` + - `(both unit arms need the operand twice — bind it to a const first)`, - ); + const tmp = lowerer.declareHiddenLocal("%nullish", ut); + value = varRef(tmp.id, ut, loc); + prefix = [{ kind: "varDecl", localId: tmp.id, init: other, loc }]; } - return { + const test: IrExpr = { kind: "logical", op: negated ? "&&" : "||", - left: isTag(tags[0]!), - right: isTag(tags[1]!), + left: isTag(tags[0]!, value), + right: isTag(tags[1]!, value), type: BOOL, loc, }; + return prefix.length === 0 + ? test + : { kind: "seqExpr", stmts: prefix, result: test, type: BOOL, loc }; } if (other.type.kind === "void") { return null; // no static tag to test — keep the fence @@ -764,10 +766,27 @@ export function ensureString(lowerer: Lowerer, e: IrExpr, node: ts.Node): IrExpr (lowerer.unions .get(elem.unionId) ?.arms.every( - (a) => a.kind === "f64" || a.kind === "string" || a.kind === "bool" || isUnitType(a), + (a) => + a.kind === "f64" || + a.kind === "string" || + a.kind === "bool" || + a.kind === "date" || + isUnitType(a), ) ?? false); - if (elem.kind === "f64" || elem.kind === "string" || elem.kind === "bool" || joinableUnion) { + // Checked-dynamic elements: the runtime's String() over the array's + // dyn form (its join, element by element). + if (elem.kind === "dyn") { + const boxed: IrExpr = { kind: "dynFrom", value: e, type: DYN, loc: e.loc }; + return { kind: "toString", operand: boxed, type: STRING, loc: e.loc }; + } + if ( + elem.kind === "f64" || + elem.kind === "string" || + elem.kind === "bool" || + elem.kind === "date" || + joinableUnion + ) { const sep: IrExpr = { kind: "strLit", value: ",", type: STRING, loc: e.loc }; return { kind: "arrIntrinsic", @@ -787,6 +806,7 @@ export function ensureString(lowerer: Lowerer, e: IrExpr, node: ts.Node): IrExpr // other record is Object.prototype.toString's constant. const shape = lowerer.shapes.get(e.type.shapeId); if (isPerfEntryType(lowerer, e.type)) return perfEntryTag(lowerer, e, e.type, e.loc); + if (isX509Type(lowerer, e.type)) return x509ToString(e, e.loc); if (shape && !shape.tuple && !shape.fields.some((f) => f.name === "toString")) { // A null-prototype record (Object.groupBy) has no toString or // valueOf: ToPrimitive throws Node's TypeError. @@ -1537,6 +1557,9 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr reference.kind === "map" || reference.kind === "set" || reference.kind === "union" || + // A promise of dyn boxes its own promise, so identity holds; + // other promise types box a fresh adapter. + (reference.kind === "promise" && reference.inner.kind === "dyn") || DYN_HANDLE_KINDS.has(reference.kind)) && lowerer.dynConvertible(reference) ) { @@ -1588,6 +1611,19 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr loc, }; } + // A Date against an untyped value: the same Date object, whose + // checked-dynamic handle the untyped value would hold. + if (dynSide.type.kind === "dyn" && scalarSide.type.kind === "date") { + const boxed = lowerer.coerceToExpected(scalarSide, DYN); + return { + kind: "dynScalarEq", + left: scalarSide === left ? boxed : left, + right: scalarSide === right ? boxed : right, + ...(negated ? { negated: true as const } : {}), + type: BOOL, + loc, + }; + } if ( dynSide.type.kind === "dyn" && (scalarSide.type.kind === "f64" || @@ -2012,6 +2048,28 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr loc, }; if (left.type.kind === "string" || right.type.kind === "string") { + // A Date on the left converts after the right operand is + // evaluated, which can run its setters. + if (left.type.kind === "date" && !isSafeToRepeat(right)) { + const date = lowerer.declareHiddenLocal("%plus", left.type); + const other = lowerer.declareHiddenLocal("%plus", right.type); + return { + kind: "seqExpr", + stmts: [ + { kind: "varDecl", localId: date.id, init: left, loc }, + { kind: "varDecl", localId: other.id, init: right, loc }, + ], + result: { + kind: "strConcat", + left: concatOperand(lowerer, varRef(date.id, left.type, loc), expr.left), + right: concatOperand(lowerer, varRef(other.id, right.type, loc), expr.right), + type: STRING, + loc, + }, + type: STRING, + loc, + }; + } return { kind: "strConcat", left: concatOperand(lowerer, left, expr.left), @@ -2195,14 +2253,41 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr loc, }; if (bothStr) return { kind: "strCmp", op: cmpOp, left, right, type: BOOL, loc }; - // A Date converts to a new checked-dynamic Date on each crossing, so - // comparisons keep their Date fence instead of comparing copies. - if ( - left.type.kind !== "date" && - right.type.kind !== "date" && - lowerer.dynConvertible(left.type) && - lowerer.dynConvertible(right.type) - ) { + // Dates compare by time value, the number hint of ToPrimitive. Both + // operands evaluate before either converts, so a right operand that + // mutates the left Date (`a < (a.setTime(t), b)`) is seen: with such + // a right operand, both Dates bind first and their times read after. + if (left.type.kind === "date" && right.type.kind === "date") { + const time = (d: IrExpr): IrExpr => ({ + kind: "libCall", + fn: "date.getTime", + args: [d], + type: F64, + loc, + }); + if (isSafeToRepeat(right)) + return { kind: "bin", op: cmpOp, left: time(left), right: time(right), type: BOOL, loc }; + const leftDate = lowerer.declareHiddenLocal("%date", left.type); + const rightDate = lowerer.declareHiddenLocal("%date", right.type); + return { + kind: "seqExpr", + stmts: [ + { kind: "varDecl", localId: leftDate.id, init: left, loc }, + { kind: "varDecl", localId: rightDate.id, init: right, loc }, + ], + result: { + kind: "bin", + op: cmpOp, + left: time(varRef(leftDate.id, left.type, loc)), + right: time(varRef(rightDate.id, right.type, loc)), + type: BOOL, + loc, + }, + type: BOOL, + loc, + }; + } + if (lowerer.dynConvertible(left.type) && lowerer.dynConvertible(right.type)) { return { kind: "libCall", fn: "dyn.compare", @@ -2312,6 +2397,34 @@ export function lowerStrictEquality( loc, }; } + // A promise against a union with a promise arm, or two such unions: + // converting a promise into a union's promise arm of another payload + // (`chains.get(id) === tail` with a `Map>`, or + // `a.pending === b.pending` after both were set to one promise) builds a + // new promise, so either side may hold a conversion of the other's + // promise, and a value narrowed out of the union may be one too. + // Promises are objects to ===, so compare both sides as untyped values, + // where a converted promise answers with its origin's identity (the plain + // promise-vs-promise rule below). + const holdsPromise = (type: IrType): boolean => + type.kind === "promise" || + (type.kind === "union" && + (lowerer.unions.get(type.unionId)?.arms ?? []).some((arm) => arm.kind === "promise")); + if ( + holdsPromise(left.type) && + holdsPromise(right.type) && + lowerer.dynConvertible(left.type) && + lowerer.dynConvertible(right.type) + ) { + return { + kind: "dynScalarEq", + left: lowerer.coerceToExpected(left, DYN), + right: lowerer.coerceToExpected(right, DYN), + ...(negated ? { negated: true as const } : {}), + type: BOOL, + loc, + }; + } if ((sameUnion || !bothUnion) && lowerer.eqComparableUnion(ut.unionId)) { aliasRefusal(ut); return { @@ -2416,6 +2529,7 @@ export function lowerStrictEquality( idLeft.type.kind === "map" || idLeft.type.kind === "set" || idLeft.type.kind === "regex" || + idLeft.type.kind === "date" || idLeft.type.kind === "url" || idLeft.type.kind === "object" || idLeft.type.kind === "record" || @@ -2452,14 +2566,7 @@ export function lowerStrictEquality( loc, }; } - // A Date converts to a new checked-dynamic Date on each crossing; its - // identity has no representation, so equality keeps the fence. - if ( - idLeft.type.kind !== "date" && - idRight.type.kind !== "date" && - lowerer.dynConvertible(idLeft.type) && - lowerer.dynConvertible(idRight.type) - ) { + if (lowerer.dynConvertible(idLeft.type) && lowerer.dynConvertible(idRight.type)) { return { kind: "dynScalarEq", left: lowerer.coerceToExpected(idLeft, DYN), diff --git a/packages/compiler/src/frontend/lowering/expressions/element-access.ts b/packages/compiler/src/frontend/lowering/expressions/element-access.ts index 0ea94ec7a..bbeeb7891 100644 --- a/packages/compiler/src/frontend/lowering/expressions/element-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/element-access.ts @@ -1,4 +1,6 @@ /* Element reads and writes (`a[i]`, `a[i] = v`), compound element updates, and keyed record reads. */ +import { OBJECT_PROTOTYPE_MEMBERS } from "../../shape-inference/domain.js"; +import { lowerUnionTupleRead } from "./union-fields.js"; import * as ts from "../../ts7/adapter.js"; import { lowerOptionalIndex, @@ -45,6 +47,7 @@ import { lowerDynMemberAssignment, lowerOptionalArrayIndexRead, lowerOptionalNumber, + assertedReadObservesUndefined, recordKeyResultOk, representedClassFieldTarget, runtimeOptionalElementKey, @@ -58,19 +61,31 @@ import { arrayValueRead, arrayValueStore, arrayValueType, + elementMayBeForeign, lowerSafeBytesRead, lowerSafeIndexRead, + primitiveElement, + unionArrayValueRead, } from "../array-values.js"; import { lowerOptionalStringIndex, lowerUnionStringIndex } from "../string-index.js"; import { InternalCompilerError } from "../../../errors.js"; import { dynUndefinedExpr, nodeThrowExpr, numLit, strLit, varRef } from "../../../ir/build.js"; import { npmStaticPackageOfPath } from "../../npm-static.js"; import { isSafeToRepeat } from "./evaluation-safety.js"; +import { withInheritedMethods } from "../record-inherited.js"; import { lowerClassPrototypeComputedAssignment } from "../class-prototypes.js"; import { isClassCallback } from "../class-callbacks.js"; import { isKeyedFieldAccess } from "./keyed-field-targets.js"; import { lowerFieldCompoundValue } from "./updates.js"; import { readsHeldEvolvingArray } from "../lower-stmts.js"; +import { + type IndexedArm, + indexedUnionArms, + lowerIndexedUnionNumberRead, + lowerIndexedUnionRead, + lowerIndexedUnionWrite, +} from "../indexed-unions.js"; +import { lowerBufferPrototypeMember } from "../containers/bytes.js"; /** `a[i]` reads. Only f64 indices into array receivers are modeled; JS * string-key element access (`a["length"]`) and string indexing (`s[0]` @@ -104,6 +119,8 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre const en = lowerEnumAccess(lowerer, expr); if (en) return en; } + const bufferMember = lowerBufferPrototypeMember(lowerer, expr); + if (bufferMember) return bufferMember; const cachedModule = lowerRequireCacheElement(lowerer, expr); if (cachedModule) return cachedModule; const namespaceElement = lowerModuleNamespaceElement(lowerer, expr); @@ -299,6 +316,31 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre } } + // An element read of a union of typed arrays and number arrays: each + // arm's own read, dispatched on the tag (indexed-unions.ts). + const indexedArms = receiverIr ? indexedUnionArms(lowerer, receiverIr) : null; + if (indexedArms && receiverIr && !isJsSourceFile(ts.getSourceFileOfNode(expr))) { + let recv = lowerer.lowerExpr(expr.expression); + if (recv.type.kind === "dyn") recv = lowerer.coerceInto(expr.expression, recv, receiverIr); + const arms = indexedUnionArms(lowerer, recv.type); + if (arms) { + const index = lowerOptionalNumber( + lowerer, + lowerer.lowerExpr(expr.argumentExpression), + locOf(expr.argumentExpression), + expr.argumentExpression, + ); + if (index.type.kind !== "f64") + lowerer.unsupported("SC1090", expr.argumentExpression, "indexing with non-number keys"); + if ( + (ts.isNonNullExpression(expr.parent) && expr.parent.expression === expr) || + lowerer.nanCodedReads?.isNumericRead(expr) === true + ) + return lowerIndexedUnionNumberRead(lowerer, recv, arms, index, locOf(expr)); + return lowerIndexedUnionRead(lowerer, recv, arms, index, locOf(expr)); + } + } + // Typed-array element read `b[i]` — like arrayGet with a scalar // result; any invalid index traps (the array discipline; JS would // read undefined — divergence 4's policy). @@ -346,13 +388,18 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre if (index.type.kind !== "f64") { lowerer.unsupported("SC1090", expr.argumentExpression, "indexing with non-number keys"); } - // `t[i]!` asserts presence: the numeric element read (NaN when - // invalid). Every other read answers undefined for an invalid index, - // like Node. + // `t[i]!` performs no runtime check, but its static type is a number: + // it takes the numeric element read (NaN when invalid), except where + // the value reaches a use that tells undefined from NaN (an equality, a + // string conversion, `typeof`, an array element), which reads undefined + // for an invalid index like `t[i]`. Every other read answers undefined + // for an invalid index, like Node. // A read whose every consumer treats undefined like NaN (see // nan-coded-reads.ts) is the same plain numeric read. if ( - (ts.isNonNullExpression(expr.parent) && expr.parent.expression === expr) || + (ts.isNonNullExpression(expr.parent) && + expr.parent.expression === expr && + !assertedReadObservesUndefined(lowerer, expr.parent)) || lowerer.nanCodedReads?.isNumericRead(expr) === true ) { return { @@ -420,6 +467,12 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre "tuple indexing with a non-literal index (tuples are fixed-shape — use t[0], t[1], ...)", ); } + // A union of tuples asserted to one tuple view (`(r as readonly + // [boolean, T | E])[0]`) still holds the union. + if (obj.type.kind === "union") { + const element = lowerUnionTupleRead(lowerer, obj, idx, locOf(expr)); + if (element) return lowerer.maybeNarrow(element, expr); + } const shapeId = obj.type.kind === "record" ? obj.type.shapeId : receiverIr.shapeId; const actualShape = lowerer.shapes.get(shapeId) ?? shape; const fieldType = actualShape.fields.find((f) => f.name === String(idx))?.type; @@ -552,7 +605,18 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre } } } + if (key.type.kind === "f64" && ts.isNumericLiteral(expr.argumentExpression)) { + const element = lowerUnionTupleRead( + lowerer, + value, + Number(expr.argumentExpression.text), + locOf(expr), + ); + if (element) return lowerer.maybeNarrow(element, expr); + } if (key.type.kind === "f64") { + const optional = optionalArmIndexRead(lowerer, value, key, locOf(expr)); + if (optional) return lowerer.maybeNarrow(optional, expr); const safe = lowerOptionalArrayIndexRead(lowerer, expr, value, key); if (safe) return safe; } @@ -843,9 +907,14 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre // must be the element type (library ABI checks and the catchable // RangeError contract for an out-of-bounds assert). Where undefined is // representable, the assertion keeps JavaScript's unchecked read. + // A declared `(T | undefined)[]` element (stored as T) keeps its + // optional read: the assertion then narrows it with a catchable + // TypeError, as for a union element. + const declaredOptional = arr.type.optionalElements === true; const provenPresent = ts.isNonNullExpression(expr.parent) && expr.parent.expression === expr && + !declaredOptional && !assertedElementKeepsUndefined(lowerer, expr.parent); if (provenPresent) { return lowerer.maybeNarrow( @@ -1028,7 +1097,7 @@ function lowerGuardedRecordWrite( const value = stabilized(lowerer, lowerer.lowerExpr(expr.right), "%recordValue", receiver.stmts); const write = lowerer.withExpressionOverride(target.expression, receiver.present, () => lowerer.withExpressionOverride(target.argumentExpression, key, () => - lowerer.withExpressionOverride(expr.right, value, () => lowerElementWrite(lowerer, expr)), + lowerer.withExpressionOverride(expr.right, value, () => lowerer.lowerElementWrite(expr)), ), ); return { @@ -1267,6 +1336,21 @@ export function lowerRecordKeyRead( }; return lowerer.maybeNarrow(get, expr); } + // A literal key the shape does not declare, on a shape without an index + // signature, names no own property of any of its values: the read + // answers undefined, as Node's does. The checker's key check cannot vouch + // for it here, since the receiver can be typed from a package's + // JavaScript while the checker read its declarations. Object.prototype + // members keep the keyed read below. + if (shape.indexValue === undefined && !OBJECT_PROTOTYPE_MEMBERS.has(litKey)) { + return { + kind: "seqExpr", + stmts: [{ kind: "exprStmt", expr: obj, loc }], + result: dynUndefinedExpr(loc), + type: DYN, + loc, + }; + } } let key = litKey !== null @@ -1394,7 +1478,17 @@ export function lowerRecordKeyRead( loc, }; if (nanCoded) return read; - return includeUndefined || optionalRead ? read : lowerer.maybeNarrow(read, expr); + const answered = withInheritedMethods(lowerer, read, litKey, isCalledAccess(expr)); + return includeUndefined || optionalRead ? answered : lowerer.maybeNarrow(answered, expr); +} + +/** The access is the callee of a call (`o[k](…)`, `o[k]?.(…)`, `o[k]!(…)`), + * which passes the object as `this`. */ +function isCalledAccess(node: ts.Expression): boolean { + let child: ts.Expression = node; + while (ts.isParenthesizedExpression(child.parent) || ts.isNonNullExpression(child.parent)) + child = child.parent; + return ts.isCallExpression(child.parent) && child.parent.expression === child; } /** ToPropertyKey for represented record keys. Missing array-derived string @@ -1427,7 +1521,7 @@ function lowerRecordPropertyKey(lowerer: Lowerer, key: IrExpr, node: ts.Expressi * exactly the key JS derives; negative/computed keys stay runtime * expressions and canonicalize through ensureString instead). Null for * everything else. */ -function recordKeyLiteralText(node: ts.Expression): string | null { +export function recordKeyLiteralText(node: ts.Expression): string | null { if (ts.isStringLiteral(node)) return node.text; if (ts.isNumericLiteral(node)) return String(Number(node.text)); return null; @@ -1518,6 +1612,9 @@ export function lowerElementCompoundParts( }); return { expr: { kind: "seqExpr", stmts, result: value, type: DYN, loc }, stmts: null }; } + const indexedArms = indexedUnionArms(lowerer, checked.type); + if (indexedArms && !isJsSourceFile(ts.getSourceFileOfNode(expr))) + return lowerIndexedUnionCompound(lowerer, expr, op, checked, indexedArms); const receiverType = checked.type.kind === "array" || checked.type.kind === "bytes" ? checked.type @@ -1570,11 +1667,14 @@ export function lowerElementCompoundParts( const indexRef = (): IrExpr => varRef(indexLocal.id, F64, loc); const oldValue: IrExpr = receiver.type.kind === "array" - ? receiver.type.elem.kind === "f64" + ? receiver.type.elem.kind === "f64" && + !(op === "+" && elementMayBeForeign(lowerer, receiver.type.elem)) ? // Every numeric operator applies ToNumber to the old value, so a // missing element is NaN without materializing `number | undefined`. arrayNumberRead(receiverRef(), indexRef(), locOf(target)) - : receiver.type.elem.kind === "string" && op === "+" + : receiver.type.elem.kind === "string" && + op === "+" && + !elementMayBeForeign(lowerer, receiver.type.elem) ? arrayStringOperand(receiverRef(), indexRef(), locOf(target)) : arrayValueRead(lowerer, receiverRef(), indexRef(), receiver.type.elem, locOf(target)) : { @@ -1702,6 +1802,54 @@ export function lowerElementCompoundParts( }; } +/** `u[i] op= v` on a union of typed arrays and number arrays: the + * receiver and index evaluate once, the old element reads as a number (NaN + * for an invalid index, as in the typed-array form), and the result is + * stored through the union's element write. */ +function lowerIndexedUnionCompound( + lowerer: Lowerer, + expr: ts.BinaryExpression, + op: CompoundOp, + receiver: IrExpr, + arms: readonly IndexedArm[], +): { expr: IrExpr | null; stmts: IrStmt[] | null } { + const target = expr.left as ts.ElementAccessExpression; + const loc = locOf(expr); + const index = lowerOptionalIndex( + lowerer, + lowerer.lowerExpr(target.argumentExpression), + locOf(target.argumentExpression), + target.argumentExpression, + ); + if (index.type.kind !== "f64") + lowerer.unsupported("SC1090", target.argumentExpression, "indexing with non-number keys"); + const receiverLocal = lowerer.declareHiddenLocal("%compoundArray", receiver.type); + const indexLocal = lowerer.declareHiddenLocal("%compoundIndex", F64); + const receiverRef = (): IrExpr => varRef(receiverLocal.id, receiver.type, loc); + const indexRef = (): IrExpr => varRef(indexLocal.id, F64, loc); + const oldValue = lowerIndexedUnionNumberRead(lowerer, receiverRef(), arms, indexRef(), loc); + const rhs = lowerOptionalNumber(lowerer, lowerer.lowerExpr(expr.right), loc); + if (rhs.type.kind !== "f64") compoundUnsupported(lowerer, expr, op, F64, rhs.type); + const computed: IrExpr = { kind: "bin", op, left: oldValue, right: rhs, type: F64, loc }; + const resultLocal = lowerer.declareHiddenLocal("%compoundResult", F64); + const resultRef = (): IrExpr => varRef(resultLocal.id, F64, loc); + return { + expr: { + kind: "seqExpr", + stmts: [ + { kind: "varDecl", localId: receiverLocal.id, init: receiver, loc }, + { kind: "varDecl", localId: indexLocal.id, init: index, loc }, + { kind: "varDecl", localId: resultLocal.id, init: computed, loc }, + lowerIndexedUnionWrite(lowerer, receiverRef(), arms, indexRef(), resultRef(), loc), + ], + result: resultRef(), + type: F64, + loc, + }, + stmts: null, + }; +} + /** ToString of one stabilized `string[]` element read for `+=`: a missing * element concatenates as "undefined". */ function arrayStringOperand(arr: IrExpr, index: IrExpr, loc: SrcLoc): IrExpr { @@ -2243,6 +2391,26 @@ export function lowerElementWrite(lowerer: Lowerer, expr: ts.BinaryExpression): }; } } + // An element write into a union of typed arrays and number arrays + // (indexed-unions.ts). + const indexedArms = receiverIr ? indexedUnionArms(lowerer, receiverIr) : null; + if (indexedArms && receiverIr && !isJsSourceFile(ts.getSourceFileOfNode(expr))) { + let recv = lowerer.lowerExpr(target.expression); + if (recv.type.kind === "dyn") recv = lowerer.coerceInto(target.expression, recv, receiverIr); + const arms = indexedUnionArms(lowerer, recv.type); + if (arms) { + const index = lowerOptionalNumber( + lowerer, + lowerer.lowerExpr(target.argumentExpression), + locOf(target.argumentExpression), + target.argumentExpression, + ); + if (index.type.kind !== "f64") + lowerer.unsupported("SC1090", target.argumentExpression, "indexing with non-number keys"); + const value = lowerer.lowerExprExpecting(expr.right, F64); + return lowerIndexedUnionWrite(lowerer, recv, arms, index, value, locOf(expr)); + } + } if (receiverIr?.kind !== "array") { lowerer.unsupported("SC1090", target, "assignment to non-array elements"); } @@ -2325,3 +2493,46 @@ export function lowerEnvironmentKey(lowerer: Lowerer, node: ts.Expression): IrEx return { kind: "libCall", fn: "dyn.toStringCoerce", args: [key], type: STRING, loc: key.loc }; return lowerRecordPropertyKey(lowerer, key, node); } + +/** A number-keyed read of a union of arrays with a `(T | undefined)[]` arm + * (stored as T elements, primitiveElement) whose element facts admit + * missing values: each arm reads its element by its state + * (unionArrayValueRead), where the keyed read would trap on a present + * undefined. The receiver and key evaluate once. Null for other unions. */ +function optionalArmIndexRead( + lowerer: Lowerer, + value: IrExpr, + key: IrExpr, + loc: SrcLoc, +): IrExpr | null { + if (value.type.kind !== "union") return null; + const arms = lowerer.unions.get(value.type.unionId)?.arms ?? []; + if ( + !arms.some( + (arm) => + arm.kind === "array" && + primitiveElement(arm.elem) && + arrayValueType(lowerer, arm.elem).kind === "union", + ) + ) + return null; + if (unionArrayValueRead(lowerer, value, key, loc) === null) return null; + const stmts: IrStmt[] = []; + const stable = (e: IrExpr, name: string, keep: boolean): IrExpr => { + if (keep) return e; + const local = lowerer.declareHiddenLocal(name, e.type); + stmts.push({ kind: "varDecl", localId: local.id, init: e, loc }); + return varRef(local.id, e.type, loc); + }; + // A receiver local stays a direct read only when the key cannot assign + // it (`a[(a = other, 0)]` reads the array `a` held before the key). + const plainKey = key.kind === "varRef" || key.kind === "numLit"; + const receiver = stable( + value, + "%indexReceiver", + value.kind === "numLit" || (value.kind === "varRef" && plainKey), + ); + const index = stable(key, "%indexKey", plainKey); + const read = unionArrayValueRead(lowerer, receiver, index, loc)!; + return stmts.length === 0 ? read : { kind: "seqExpr", stmts, result: read, type: read.type, loc }; +} diff --git a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts index 732aab434..d7068ecb5 100644 --- a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts +++ b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts @@ -1,4 +1,5 @@ /* Identifier references in expression position: lowerExprInner's identifier branch and its helpers. */ +import { globalMemberValue } from "../global-values.js"; import { predeclareForwardClass } from "../lower-stmts.js"; import { cjsMemberAliasValueOf } from "../cjs-exports-object.js"; import { builtinClassValue, builtinMemberClassKind } from "../builtins/builtin-class-dyn.js"; @@ -9,6 +10,7 @@ import { type Lowerer, PoisonError } from "../lowerer.js"; import { BOOL, BYTES_ELEMENT_NAME, + DATE_T, DYN, F64, type IrExpr, @@ -24,8 +26,14 @@ import { } from "../../../ir/ir.js"; import { lowerNodeModuleIdentifier } from "../lower-node-module.js"; import { lowerCjsExportsObjectValue } from "../cjs-exports-object.js"; +import { + splitRequireBindingOfIdent, + wholeExportSymbolOfModule, + wholeExportValue, +} from "../cjs-inline-require.js"; import { captureContextArguments } from "../function-context.js"; import { argumentsOwner, typedArgumentsCandidate } from "../typed-arguments.js"; +import { argumentCountValue } from "../argument-counts.js"; import { isCjsExportTableLiteral, isCjsJsFile, @@ -48,9 +56,11 @@ import { varRef } from "../../../ir/build.js"; import { ambientUndefReadType, ambientUndefinedFnSymbolOf, + ambientUndefinedVarSymbolOf, contextualUndefReadType, fenceEarlyAliasUse, isTemporalGlobalRoot, + cjsNamespaceImportDep, lowerNsIdentifierValue, nsUndefRead, } from "../lower-namespaces.js"; @@ -82,8 +92,8 @@ import { unsupportedModuleFeatureOf } from "../../builtin-modules.js"; import { lowerGlobalValue } from "../lower-global-value.js"; import { fenceEnumObjectValue } from "../lower-enums.js"; import { mixinFnOfCallee } from "../lower-mixins.js"; -import { isNavigatorGlobal, refuseNavigator } from "../builtins/navigator.js"; -import { wasiGuestPath } from "../../../wasi-paths.js"; +import { isNavigatorGlobal, navigatorValue } from "../builtins/navigator.js"; +import { moduleFileName } from "../import-meta.js"; export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: SrcLoc): IrExpr { if (lowerer.isStdlibGlobal(expr, "console")) { @@ -172,6 +182,12 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: // when the checker types this USE as a single arm, maybeNarrow // bridges the tagged representation with a unionNarrow. const local = lowerer.resolveLocal(expr); + // A Date local holding its time value reads as that Date: only uses + // that read the time value reach here (date-values.ts). + if (local && lowerer.timeValueDateLocals.has(local)) { + const time: IrExpr = { kind: "varRef", localId: local.id, type: F64, loc }; + return { kind: "libCall", fn: "date.fromTime", args: [time], type: DATE_T, loc }; + } if (local) { // A switch can dispatch past a lexical declaration. Keep this // previously refused codec form out until switch-scope TDZ boxes @@ -305,10 +321,12 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: // that does not exist. The blocked-binding cascade names the use; // without this the validator's registration check ICEs on the live // reference (`@this {T}` inference — signature 07). - const unregistered = lowerer.unregisteredClassOf(g.type, new Set()); + let unregistered = lowerer.unregisteredClassOf(g.type, new Set()); + // The class's own rejection is the root cause the cascade names + // (unless the flush registers it with dynamic slots). + if (unregistered !== null && lowerer.flushDeferredClass(unregistered) !== undefined) + unregistered = lowerer.unregisteredClassOf(g.type, new Set()); if (unregistered !== null) { - // The class's own rejection is the root cause the cascade names. - lowerer.flushDeferredClass(unregistered); lowerer.pushDiag(blockedBindingUseDiag(expr.text, loc)); throw new PoisonError(); } @@ -437,7 +455,17 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: // semantics); the rest marker carries the trailing dyn-array ABI. const funcType: IrType = funcTypeFromParamShapes(sig.params, sig.returnType); lowerer.requireExactArityValue(expr, expr, sig.params, funcType); - return { kind: "closure", fnName: sig.name, captures: [], type: funcType, loc }; + const closure = { + kind: "closure" as const, + fnName: sig.name, + captures: [], + type: funcType, + loc, + }; + const decl = lowerer.resolveValueSymbol(expr)?.valueDeclaration; + return decl && ts.isFunctionDeclaration(decl) && funcType.kind === "func" + ? argumentCountValue(lowerer, decl, expr, { ...closure, type: funcType }) + : closure; } { // A generic function as a VALUE: monomorphized by flow — the @@ -587,6 +615,9 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: // the same identity-token story as stdlib globals above (the // harness adds worker_threads.Worker to its identity Set). if (isJsSourceFile(ts.getSourceFileOfNode(expr))) { + // A member that is a global object reads as the global does. + const global = globalMemberValue(lowerer, bi, loc); + if (global) return global; // EventEmitter and the stream classes are the runtime's untyped // constructors, so constructor functions can inherit from them. const classKind = builtinMemberClassKind(bi.module, bi.member); @@ -596,12 +627,7 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: return builtinFunctionFenceValue(lowerer, bi, expr, loc); return { kind: "strLit", - // node:stream/web exports the web stream globals themselves: - // the same identity token as the global spelling. - value: - bi.module === "stream/web" - ? `[builtin ${bi.member}]` - : `[builtin ${bi.module}.${bi.member}]`, + value: `[builtin ${bi.module}.${bi.member}]`, type: STRING, loc, }; @@ -719,45 +745,27 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: expr.text === "decodeURI") && lowerer.isStdlibGlobal(expr, expr.text) ) { - const name = `%builtin.${expr.text}`; - const type = funcOf([DYN], STRING); - if (!lowerer.liftedFns.some((fn) => fn.name === name)) { - const value: IrExpr = { - kind: "libCall", - fn: "dyn.toStringCoerce", - args: [varRef("value", DYN, loc)], - type: STRING, - loc, - }; - const fn = - expr.text === "encodeURIComponent" - ? "str.encodeUriComponent" - : expr.text === "encodeURI" - ? "str.encodeUri" - : expr.text === "decodeURI" - ? "str.decodeUri" - : "str.decodeUriComponent"; - lowerer.liftedFns.push({ - name, - params: [{ localId: "value", name: "value", type: DYN }], - locals: [{ id: "value", name: "value", type: DYN, mutable: false }], - returnType: STRING, - body: [ - { - kind: "return", - value: { kind: "libCall", fn, args: [value], type: STRING, loc }, - loc, - }, - ], - loc, - }); - } - return { kind: "closure", fnName: name, captures: [], type, loc }; + return uriFunctionValue(lowerer, expr.text, loc); } if (lowerer.isStdlibGlobal(expr, "crypto")) return { kind: "libCall", fn: "webcrypto.object", args: [], type: DYN, loc }; + if (isNavigatorGlobal(lowerer, expr)) return navigatorValue(loc); const nativeAlias = stdlibGlobalNameOf(lowerer, expr); if (nativeAlias === "Buffer" && expr.text !== "Buffer") { + // An alias through a cast of globalThis carries the cast's static type, + // which the identity token below does not have. + const aliasSymbol = identifierReadSymbol(lowerer, expr); + if (aliasSymbol && lowerer.castGlobalAliases.has(aliasSymbol)) { + lowerer.unsupported( + "SC1090", + expr, + "the Buffer constructor read through a cast of globalThis as a value", + "test it in a condition, read its typeof, or call its members", + ); + } + // JavaScript reads the Buffer function's value (builtinGlobalValue). + if (isJsSourceFile(ts.getSourceFileOfNode(expr))) + return { kind: "libCall", fn: "buffer.constructor", args: [], type: DYN, loc }; return { kind: "strLit", value: "[builtin Buffer]", type: STRING, loc }; } if (lowerer.isStdlibGlobal(expr, "fetch")) { @@ -836,14 +844,7 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: // Stdlib/@types/node ambients never reach here (their chokepoint // is above); only user-file declares do. { - const sym = lowerer.resolveValueSymbol(expr); - const decl = sym ? sym.declarations[0] : undefined; - if ( - decl !== undefined && - ts.isVariableDeclaration(decl) && - decl.initializer === undefined && - ts.getCombinedModifierFlags(decl) & ts.ModifierFlags.Ambient - ) { + if (ambientUndefinedVarSymbolOf(lowerer, expr)) { const declared = lowerer.mapTypeOf(lowerer.typeOf(expr)); if (declared && declared.kind !== "void") { return { @@ -956,6 +957,37 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: // representation (ambient namespaces compile to Node's // ReferenceError instead — the object never exists at runtime). { + // `ns.name` through a namespace import of a CommonJS module, where Node's + // namespace has no `name` property (its lexer does not detect it), or + // `ns.default` (module.exports itself). + const cjsNamespace = cjsNamespaceImportDep(lowerer, expr); + if ( + cjsNamespace !== null && + ts.isPropertyAccessExpression(expr.parent) && + expr.parent.expression === expr && + ts.isIdentifier(expr.parent.name) + ) { + const name = expr.parent.name.text; + const { names, assignedLater } = lowerer.cjsNamespaceOf(cjsNamespace); + if (name === "default") + lowerer.unsupported( + "SC1090", + expr.parent, + `reads of 'default' through namespace imports of CommonJS modules (module.exports itself: read it through a default import)`, + ); + if (!names.has(name)) + lowerer.unsupported( + "SC1090", + expr.parent, + `reads of '${name}' through namespace imports of CommonJS modules (Node's lexer does not detect '${name}' among the module's exports, so Node reads undefined)`, + ); + if (assignedLater === "all" || assignedLater.has(name)) + lowerer.unsupported( + "SC1090", + expr.parent, + `reads of '${name}' through namespace imports of CommonJS modules whose functions assign exports (Node's namespace keeps the values from the end of the module's evaluation)`, + ); + } const moduleNs = lowerer.mapTypeOf(lowerer.typeOf(expr)); if (moduleNs?.kind === "moduleNs") { return { kind: "moduleNsRef", moduleId: moduleNs.moduleId, type: moduleNs, loc }; @@ -969,19 +1001,32 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: const ns = lowerNsIdentifierValue(lowerer, expr); if (ns) return ns; } - if (isNavigatorGlobal(lowerer, expr)) { - refuseNavigator( - lowerer, - expr, - "the navigator object as a value", - lowerer.checker.getSymbolAtLocation(expr), - ); - } // An import of a CommonJS member export whose value is a binding of the // exporting module (`module.exports = Client; module.exports.StatsD = // Client`): the member holds that binding's value. const memberAlias = cjsMemberAliasValueOf(lowerer, expr); if (memberAlias !== null) return lowerer.lowerExpr(memberAlias); + // `var m; m = require("./x")` as a bare value: a whole-export target is + // the exported value; a table target has no first-class module object. + { + const split = splitRequireBindingOfIdent(lowerer, expr); + if (split !== null) { + if (wholeExportSymbolOfModule(lowerer, split.dep) !== null) { + const value = wholeExportValue(lowerer, split.dep, expr); + if (value !== null) return value; + lowerer.unsupported( + "SC1090", + expr, + `the whole export of '${split.spec}' as a value (its 'module.exports =' value has no static representation)`, + ); + } + lowerer.unsupported( + "SC1090", + expr, + `the exports object of '${split.spec}' as a value (its exports compile to a static table: read require(${JSON.stringify(split.spec)}).member directly)`, + ); + } + } // Preflight guarantees no unresolved identifiers; anything else here // is a blocked declaration's binding (the SC2004 cascade) or a // binding form we don't model yet. @@ -991,12 +1036,6 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: ); } -function moduleFileName(lowerer: Lowerer, sf: ts.SourceFile): string { - return lowerer.targetPlatform === "wasi" - ? (wasiGuestPath(sf.fileName) ?? sf.fileName.replace(/\\/g, "/")) - : sf.fileName; -} - /** A computed-key access (`value[key]` after `typeof value === "object"`) * of a runtime-optional binding the checker narrowed to one record arm of a * wider union. The binding re-tags to that record or undefined, so the keyed @@ -1091,3 +1130,42 @@ function cjsExportAccessorRead(lowerer: Lowerer, ident: ts.Identifier): IrExpr | } return cjsAccessorCall(lowerer, getter, locOf(ident)); } + +/** encodeURIComponent, encodeURI, decodeURI or decodeURIComponent as a + * function value: one interned closure per function. */ +export function uriFunctionValue(lowerer: Lowerer, which: string, loc: SrcLoc): IrExpr { + const name = `%builtin.${which}`; + const type = funcOf([DYN], STRING); + if (!lowerer.liftedFns.some((fn) => fn.name === name)) { + const value: IrExpr = { + kind: "libCall", + fn: "dyn.toStringCoerce", + args: [varRef("value", DYN, loc)], + type: STRING, + loc, + }; + const fn = + which === "encodeURIComponent" + ? "str.encodeUriComponent" + : which === "encodeURI" + ? "str.encodeUri" + : which === "decodeURI" + ? "str.decodeUri" + : "str.decodeUriComponent"; + lowerer.liftedFns.push({ + name, + params: [{ localId: "value", name: "value", type: DYN }], + locals: [{ id: "value", name: "value", type: DYN, mutable: false }], + returnType: STRING, + body: [ + { + kind: "return", + value: { kind: "libCall", fn, args: [value], type: STRING, loc }, + loc, + }, + ], + loc, + }); + } + return { kind: "closure", fnName: name, captures: [], type, loc }; +} diff --git a/packages/compiler/src/frontend/lowering/expressions/message-listeners.ts b/packages/compiler/src/frontend/lowering/expressions/message-listeners.ts new file mode 100644 index 000000000..698bf4dc3 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/expressions/message-listeners.ts @@ -0,0 +1,22 @@ +import * as ts from "../../ts7/adapter.js"; + +const LISTENER_METHODS = new Set([ + "on", + "once", + "addListener", + "prependListener", + "prependOnceListener", +]); + +/** `.on("message", …)` and the other listener registrations. */ +export function isMessageListenerCall(call: ts.CallExpression): boolean { + const callee = call.expression; + const event = call.arguments.nodes[0]; + return ( + ts.isPropertyAccessExpression(callee) && + LISTENER_METHODS.has(callee.name.text) && + event !== undefined && + ts.isStringLiteral(event) && + event.text === "message" + ); +} diff --git a/packages/compiler/src/frontend/lowering/expressions/narrowing.ts b/packages/compiler/src/frontend/lowering/expressions/narrowing.ts index 01cab9901..91b63cabc 100644 --- a/packages/compiler/src/frontend/lowering/expressions/narrowing.ts +++ b/packages/compiler/src/frontend/lowering/expressions/narrowing.ts @@ -1,4 +1,5 @@ /* Type narrowing of lowered values, conditional and as-expressions, and runtime-optional receiver reads. */ +import { declBoundaryOrigin } from "../../decl-projection.js"; import * as ts from "../../ts7/adapter.js"; import { type Lowerer, dynFallbackType } from "../lowerer.js"; import { @@ -50,6 +51,7 @@ import { isConstAssertionTypeNode, unitOnlyUnion } from "../../type-mapper.js"; import { assertsGlobalObject, extractionCopiesReference } from "../assertion-identity.js"; import { iterableSlotValue, iterableUnionOf } from "../iterable-values.js"; import { isDeclBoundaryTypeNode } from "../../decl-projection.js"; +import { isMessageListenerCall } from "./message-listeners.js"; /** `c ? a : b`. An explicit destination constructs each branch at that * layout before joining it. Without one, retain the expression's inferred @@ -875,8 +877,15 @@ function checkedStaleArm( /** V8's spelling of a callee in "is not a function": a string-literal * element key reads as a property name (`o["a"]` is `o.a`), other keys - * keep their brackets. */ + * keep their brackets, parentheses and non-null assertions disappear, and + * a call result reads `f(...)`. */ export function v8CalleeText(node: ts.Node): string { + // `!` is type-only syntax: the running JavaScript has no trace of it. + if (ts.isNonNullExpression(node) || ts.isParenthesizedExpression(node)) + return v8CalleeText(node.expression); + // A call result prints with its arguments elided. + if (ts.isCallExpression(node) && !node.questionDotToken) + return `${v8CalleeText(node.expression)}(...)`; if (ts.isElementAccessExpression(node)) { const key = node.argumentExpression; const object = v8CalleeText(node.expression); @@ -1474,6 +1483,7 @@ export function lowerAsExpression( target?.kind === "record" && inner.type.shapeId !== target.shapeId ) { + lowerer.assertedValues.set(inner, target); return lowerer.coerceInto(expr, inner, target); } // `u as Arm` on a UNION value is `u!`'s spelling with a named arm @@ -1508,6 +1518,15 @@ export function lowerAsExpression( const asserted = checkedClassUnionAssertion(lowerer, inner, target, locOf(expr)); if (asserted) return asserted; } + // The erased value still answers to the assertion: a slot it cannot + // reach by an ordinary conversion converts it as asserted + // (assertedConversion). + if ( + target && + (target.kind === "union" || target.kind === "record" || target.kind === "array") && + (inner.type.kind === "union" || inner.type.kind === "array") + ) + lowerer.assertedValues.set(inner, target); return inner; // erasure, unchanged } @@ -1524,7 +1543,7 @@ export function lowerAsExpression( // The declaration-typed boundary (decl-projection.ts): an identity- // preserving checked conversion of a value package code produced. if (isDeclBoundaryTypeNode(expr.type)) { - const adopted = declBoundaryCast(lowerer, inner, target, locOf(expr)); + const adopted = declBoundaryCast(lowerer, inner, target, locOf(expr), expr); if (adopted) return adopted; } // An all-`unknown`-fields record target (`err as { code?: unknown }` @@ -1568,7 +1587,7 @@ export function lowerAsExpression( // contains undefined, so casts over parse results keep failing on // non-string values with the usual path-annotated TypeError. if (target.kind === "union" && lowerer.dynConvertible(target)) { - return { kind: "dynCheck", value: inner, type: target, loc: locOf(expr) }; + return checkedDynCast(lowerer, expr, inner, target); } // Uint8Array targets: the checked-dynamic tree carries a bytes kind now (converted // stdin chunks) — the extraction validates the kind and copies out. @@ -1612,9 +1631,145 @@ export function lowerAsExpression( `number, string, boolean, records, arrays, and unions of those)`, ); } + return checkedDynCast(lowerer, expr, inner, target); +} + +/** The checked conversion of a dynamic value. A record or array built from + * a value another holder may still reference keeps one identity with it + * (adoptingCast); only a source nothing else can hold (freshDynSource) + * converts outright, which keeps the fused `JSON.parse(text) as T`. */ +function checkedDynCast( + lowerer: Lowerer, + expr: ts.AsExpression | ts.TypeAssertion, + inner: IrExpr, + target: IrType, +): IrExpr { + if ( + holdsReference(lowerer, target) && + !freshDynSource(lowerer, expr, inner) && + !resultOnlyRead(lowerer, expr) + ) { + return adoptingCast(lowerer, inner, target, locOf(expr), "dyn.adoptShared"); + } return { kind: "dynCheck", value: inner, type: target, loc: locOf(expr) }; } +/** A cast whose result only serves one read of a primitive member + * (`(value as T).name`, `(value as T[]).length`): the converted value is + * never stored, compared, written or called through, and the read yields no + * reference into it, so whether it is the source itself or a copy cannot be + * observed. */ +function resultOnlyRead(lowerer: Lowerer, expr: ts.AsExpression | ts.TypeAssertion): boolean { + let node: ts.Node = expr; + while (node.parent !== undefined && ts.isParenthesizedExpression(node.parent)) node = node.parent; + const access = node.parent; + if ( + access === undefined || + !( + (ts.isPropertyAccessExpression(access) || ts.isElementAccessExpression(access)) && + access.expression === node + ) + ) + return false; + const primitive = + ts.TypeFlags.StringLike | + ts.TypeFlags.NumberLike | + ts.TypeFlags.BooleanLike | + ts.TypeFlags.BigIntLike | + ts.TypeFlags.Undefined | + ts.TypeFlags.Null; + const read = lowerer.typeOf(access); + const parts = read.isUnionType() ? ts.constituentTypes(read) : [read]; + if (!parts.every((part) => (part.flags & primitive) !== 0)) return false; + let use: ts.Node = access; + while (use.parent !== undefined && ts.isParenthesizedExpression(use.parent)) use = use.parent; + const parent = use.parent; + if (parent === undefined) return true; + if (ts.isCallExpression(parent) && parent.expression === use) return false; + if ( + ts.isBinaryExpression(parent) && + parent.left === use && + ts.isAssignmentOperator(parent.operatorToken.kind) + ) + return false; + if ( + (ts.isPrefixUnaryExpression(parent) || ts.isPostfixUnaryExpression(parent)) && + (parent.operator === ts.SyntaxKind.PlusPlusToken || + parent.operator === ts.SyntaxKind.MinusMinusToken) + ) + return false; + if (ts.isDeleteExpression(parent)) return false; + return true; +} + +/** A cast operand nothing else can hold: a value this expression creates + * (`JSON.parse(text)`, `await response.json()`), or a structured clone the + * runtime delivers once and the program reads at this site only (`workerData` + * read nowhere else; the parameter of the program's one `"message"` + * listener, read nowhere else in it). Anything else is adopted. */ +function freshDynSource( + lowerer: Lowerer, + expr: ts.AsExpression | ts.TypeAssertion, + inner: IrExpr, +): boolean { + if (inner.kind === "libCall" && inner.fn === "json.parse") return true; + if ( + inner.kind === "awaitExpr" && + inner.value.kind === "libCall" && + inner.value.fn === "fetch.responseJson" + ) + return true; + const facts = lowerer.structuredCloneReads(); + if (inner.kind === "libCall" && inner.fn === "worker.data") return facts.workerData === 1; + let operand: ts.Expression = expr.expression; + while (ts.isParenthesizedExpression(operand)) operand = operand.expression; + if (!ts.isIdentifier(operand) || inner.kind !== "varRef" || facts.messageListeners !== 1) + return false; + const symbol = lowerer.checker.getSymbolAtLocation(operand); + const param = symbol?.valueDeclaration; + if (!param || !ts.isParameter(param) || !ts.isIdentifier(param.name)) return false; + const fn = param.parent; + if ( + !(ts.isArrowFunction(fn) || ts.isFunctionExpression(fn)) || + fn.parameters.nodes[0] !== param || + !isMessageListener(fn) + ) + return false; + let reads = 0; + const name = param.name.text; + const visit = (node: ts.Node): void => { + if (ts.isIdentifier(node) && node.text === name && node !== param.name) reads++; + ts.forEachChild(node, visit); + }; + ts.forEachChild(fn, visit); + return reads === 1; +} + +/** The listener argument of `.on("message", fn)` (also once, + * addListener, prependListener, prependOnceListener) registered once: at + * module level, outside any function, class or loop, so one emitter can + * never hand the same message to two registrations of it. */ +function isMessageListener(fn: ts.Node): boolean { + const call = fn.parent; + if (!call || !ts.isCallExpression(call) || call.arguments.nodes[1] !== fn) return false; + if (!isMessageListenerCall(call)) return false; + for (let p = call.parent; p !== undefined && !ts.isSourceFile(p); p = p.parent) { + if ( + ts.isFunctionLike(p) || + ts.isClassDeclaration(p) || + ts.isClassExpression(p) || + ts.isClassStaticBlockDeclaration(p) || + ts.isForStatement(p) || + ts.isForInStatement(p) || + ts.isForOfStatement(p) || + ts.isWhileStatement(p) || + ts.isDoStatement(p) + ) + return false; + } + return true; +} + /** True when a type holds a record or an array somewhere (values whose * identity can be observed). */ function holdsReference(lowerer: Lowerer, t: IrType, seen = new Set()): boolean { @@ -1632,11 +1787,42 @@ function holdsReference(lowerer: Lowerer, t: IrType, seen = new Set()): * becomes a live capsule of the value built from it * (scr_dyn_decl_adopt), so identity and mutation visibility hold on both * sides. Scalar targets are ordinary checked casts. */ +/** The origin the projection recorded for a boundary cast (the package + * declaration its type comes from, decl-projection.ts), found by the + * cast's position among the file's boundary casts in source order. */ +function recordedBoundaryOrigin(lowerer: Lowerer, cast: ts.Node): string | null { + const sf = ts.getSourceFileOfNode(cast); + let order = lowerer.boundaryCastOrder.get(sf); + if (order === undefined) { + const found = new Map(); + const visit = (node: ts.Node): void => { + if (ts.isAsExpression(node) && isDeclBoundaryTypeNode(node.type)) found.set(node, found.size); + ts.forEachChild( + node, + (child) => { + visit(child); + return undefined; + }, + (list) => { + for (const child of list.nodes) visit(child); + return undefined; + }, + ); + }; + visit(sf); + order = found; + lowerer.boundaryCastOrder.set(sf, order); + } + const index = order.get(cast); + return index === undefined ? null : declBoundaryOrigin(sf.fileName, index); +} + function declBoundaryCast( lowerer: Lowerer, inner: IrExpr, target: IrType, loc: SrcLoc, + cast: ts.Node, ): IrExpr | null { if ( !canDynCheckTo( @@ -1647,9 +1833,42 @@ function declBoundaryCast( !(target.kind === "union" && lowerer.dynConvertible(target)) ) return null; - if (!holdsReference(lowerer, target)) { - return { kind: "dynCheck", value: inner, type: target, loc }; + // The package call or read the value comes from (decl-origin.ts): a + // failing check names it and the patch admitting the value. + let originText: string | null = null; + try { + originText = recordedBoundaryOrigin(lowerer, cast); + } catch { + originText = null; } + const originId = originText === null ? undefined : lowerer.declOriginIndex(originText); + if (!holdsReference(lowerer, target)) { + return { + kind: "dynCheck", + value: inner, + type: target, + loc, + ...(originId === undefined ? {} : { origin: originId }), + }; + } + return adoptingCast(lowerer, inner, target, loc, "dyn.declAdopt", originId); +} + +/** The checked conversion of `inner` to `target` followed by adoption + * (`dyn.declAdopt` at the boundary, the lenient `dyn.adoptShared` + * elsewhere): every source object or array that something else still + * references becomes a live capsule of the native value built from it. + * `originId` names the package declaration a failing check reports + * (decl-origin.ts). */ +function adoptingCast( + lowerer: Lowerer, + inner: IrExpr, + target: IrType, + loc: SrcLoc, + fn: "dyn.declAdopt" | "dyn.adoptShared", + originId?: number, +): IrExpr { + const boundary = fn === "dyn.declAdopt"; const source = lowerer.declareHiddenLocal("%boundarySource", DYN); const value = lowerer.declareHiddenLocal("%boundaryValue", target); return { @@ -1662,9 +1881,10 @@ function declBoundaryCast( init: { kind: "dynCheck", value: varRef(source.id, DYN, loc), - adopt: true, + ...(boundary ? { adopt: true as const } : {}), type: target, loc, + ...(originId === undefined ? {} : { origin: originId }), }, loc, }, @@ -1672,7 +1892,7 @@ function declBoundaryCast( kind: "exprStmt", expr: { kind: "libCall", - fn: "dyn.declAdopt", + fn, args: [ varRef(source.id, DYN, loc), { diff --git a/packages/compiler/src/frontend/lowering/expressions/nullish-assignment.ts b/packages/compiler/src/frontend/lowering/expressions/nullish-assignment.ts index 9664f591d..8a25eb124 100644 --- a/packages/compiler/src/frontend/lowering/expressions/nullish-assignment.ts +++ b/packages/compiler/src/frontend/lowering/expressions/nullish-assignment.ts @@ -14,7 +14,12 @@ import { import { varRef } from "../../../ir/build.js"; import { locOf } from "../../program.js"; import type { Lowerer } from "../lowerer.js"; -import { arrayNumberRead, arrayValueRead, arrayValueStore } from "../array-values.js"; +import { + arrayNumberRead, + arrayValueRead, + arrayValueStore, + elementMayBeForeign, +} from "../array-values.js"; import { fenceNodeModuleMutation } from "../lower-node-module.js"; import { globalSymbolKey } from "./global-symbols.js"; import { tryLowerExpression } from "./try-lower-expression.js"; @@ -185,7 +190,12 @@ export function lowerShortCircuitAssignment(lowerer: Lowerer, expr: ts.BinaryExp const element = storage; write = (value) => arrayValueStore(lowerer, receiver, key, value, element, loc); const elementLoc = locOf(target); - if (nullish && NON_NULLISH_ELEMENTS.has(element.kind)) { + // A foreign element can be any value, nullish or not. + if ( + nullish && + NON_NULLISH_ELEMENTS.has(element.kind) && + !elementMayBeForeign(lowerer, element) + ) { // Only a missing element is nullish: keep the present value or // store the right side, without the optional element value. rhs = lowerer.lowerExprExpecting(expr.right, element); diff --git a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts index 4cc0bee1a..31479e4b0 100644 --- a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts @@ -28,16 +28,22 @@ import type { IndexMergeContributor } from "../containers/indexed-objects.js"; import type { KeyOrderSegment } from "../key-order-witness.js"; import { PERF_ENTRY_MARKER, + X509_PUBLIC_KEY_FIELD, isGenericCallableMemberType, jsOpenObjectType, } from "../../type-mapper.js"; import { isSafeToRepeat } from "./evaluation-safety.js"; import { tryLowerExpression } from "./try-lower-expression.js"; -import { fenceSymbolFieldCopy } from "../symbol-fields.js"; +import { + classSymbolKeyOf, + fenceSymbolFieldCopy, + symbolFieldDisplayName, +} from "../symbol-fields.js"; import { planUnionRetag } from "../union-retag.js"; import { jsHoldsDynObject, runtimeOptionalStorageOperand } from "../lower-exprs.js"; import { builtinGlobalValue } from "./property-access.js"; import { holdsEvolvingArray } from "../lower-stmts.js"; +import { builtinNamespaceValue } from "../builtin-namespaces.js"; /** `{ a: 1, b: "x" }` → recordLit. The record type comes from the * contextual type when tsc has one (annotated declarations, arguments, @@ -195,7 +201,19 @@ function propNameText(lowerer: Lowerer, name: ts.PropertyName): string { * (NamedEvaluation): an identifier, string or numeric key, or a computed * key that folds to one string. Null for other keys. */ function propertyFunctionName(lowerer: Lowerer, name: ts.PropertyName): string | null { - if (ts.isComputedPropertyName(name)) return literalComputedKey(lowerer, name); + if (ts.isComputedPropertyName(name)) { + const literal = literalComputedKey(lowerer, name); + if (literal !== null) return literal; + // A symbol key names the function `[description]`. + let key = name.expression; + while (ts.isParenthesizedExpression(key)) key = key.expression; + const symbol = classSymbolKeyOf(lowerer, key); + if (symbol) return `[${symbolFieldDisplayName(symbol.fieldName).slice("Symbol(".length, -1)}]`; + const wellKnown = ts.isPropertyAccessExpression(key) + ? lowerer.stdlibGlobalMember(key, "Symbol") + : null; + return wellKnown ? `[Symbol.${wellKnown}]` : null; + } if (ts.isNumericLiteral(name)) return String(Number(name.text)); if (ts.isIdentifier(name) || ts.isStringLiteral(name)) return name.text; return null; @@ -401,7 +419,12 @@ export function lowerDynObjectLiteral( : !boxValue && (ts.isObjectLiteralExpression(valueExpr) || (ts.isArrayLiteralExpression(valueExpr) && - holdsEvolvingArray(lowerer, valueExpr))) + // An empty literal takes whatever the object's users + // store into it (`{ [kChildren]: [] }`), so it is a + // checked-dynamic array rather than its `never[]` layout. + (valueExpr.elements.nodes.length === 0 || + holdsEvolvingArray(lowerer, valueExpr) || + lowerer.untypedLiteralContext(valueExpr)))) ? lowerer.lowerExprExpecting(valueExpr, DYN) : lowerer.lowerExpr(valueExpr as ts.Expression); raw = fenceClosureProbe(lowerer, valueExpr, undefined, lowerValue) ?? lowerValue(); @@ -501,6 +524,13 @@ function fenceAccessorSpreadSource( "object spread over performance entries (Node keeps their fields as prototype getters, so own-key walks see none; read the fields or call toJSON())", ); } + if (srcShape && srcShape.fields.some((f) => f.name === X509_PUBLIC_KEY_FIELD)) { + lowerer.unsupported( + "SC1090", + prop, + "object spread over X509Certificate values (Node keeps their fields as prototype getters, so own-key walks see none; read the fields)", + ); + } if (srcShape && shapeHasAccessorSlots(srcShape)) { lowerer.unsupported( "SC1090", @@ -2504,6 +2534,20 @@ export function lowerObjectLiteral( : { kind: "seqExpr", stmts: spreadStmts, result, type, loc }; } +/** The field type is an array, or a union with exactly one array arm. */ +function singleArrayArm(lowerer: Lowerer, type: IrType): boolean { + if (type.kind === "array") return true; + if (type.kind !== "union") return false; + const arms = lowerer.unions.get(type.unionId)?.arms ?? []; + return arms.filter((arm) => arm.kind === "array").length === 1; +} + +function isEmptyArrayLiteral(node: ts.Node): boolean { + let inner = node; + while (ts.isParenthesizedExpression(inner)) inner = inner.expression; + return ts.isArrayLiteralExpression(inner) && inner.elements.nodes.length === 0; +} + /** A union of plain records can preserve its existing variant while * replacing shared fields. Dispatch before constructing the copy: only * the selected arm evaluates overrides, and recordClone snapshots fields @@ -2552,8 +2596,11 @@ function lowerUnionRecordClone(lowerer: Lowerer, expr: ts.ObjectLiteralExpressio for (let index = 0; index < props.length; index++) { const prop = props[index]!; const node = ts.isPropertyAssignment(prop) ? prop.initializer : prop.name; - const valueType = lowerer.mapTypeOf(lowerer.typeOf(node)); const fieldType = shape.fields.find((field) => field.name === names[index])!.type; + // An empty array literal (`content: []`, typed `never[]`) builds a + // fresh array of each arm's own element type. + if (isEmptyArrayLiteral(node) && singleArrayArm(lowerer, fieldType)) continue; + const valueType = lowerer.mapTypeOf(lowerer.typeOf(node)); const lift = valueType ? lowerer.widthLiftPlan(valueType, fieldType) : null; // The inferred result must retain an override's own fields and // aliases. A structural copy into the source field's narrower shape @@ -3262,7 +3309,11 @@ function shorthandBuiltinValue( const symbol = lowerer.checker.getShorthandAssignmentValueSymbol(prop); if (!symbol || !lowerer.isStdlibSymbol(symbol)) return null; const js = isJsSourceFile(ts.getSourceFileOfNode(prop)); - return builtinGlobalValue(ts.symbolName(symbol), locOf(prop.name), js); + const name = ts.symbolName(symbol); + return ( + (js ? builtinNamespaceValue(lowerer, name, locOf(prop.name)) : null) ?? + builtinGlobalValue(name, locOf(prop.name), js) + ); } export function lowerShorthandValue( diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts index 7b12297a8..58f07f890 100644 --- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts @@ -1,11 +1,23 @@ +import { widensReducedRecords } from "../function-returns.js"; +import { widthNarrowHelper } from "../coercions/unions.js"; /* Property reads (`a.b`): lowerExprInner's property-access branch, intrinsic and union properties, field reads and targets, and match-group reads. */ +import { jsSpecialGlobalValue } from "../global-values.js"; +import { + arrayClassMemberOwner, + arrayClassReceiver, + lowerArrayClassConstructorRead, +} from "../builtins/array-classes.js"; +import { indexedUnionArms, lowerIndexedUnionMember } from "../indexed-unions.js"; import { keyedHelperName } from "../stable-names.js"; +import { builtinNamespaceValue } from "../builtin-namespaces.js"; import { staticallySelected } from "../static-typeof.js"; import * as ts from "../../ts7/adapter.js"; import { lowerHistogramProperty } from "../builtins/performance.js"; import { staticFunctionNameRead } from "../static-function-names.js"; import { typedArgumentsArity } from "../typed-arguments.js"; +import { INSPECT_CUSTOM_DESCRIPTION, isInspectCustomAccess } from "../symbol-fields.js"; import { isThisParameter } from "../call-signatures.js"; +import { argumentCountSlot, contextualCallCount, usedAsValue } from "../argument-counts.js"; import { checkedPrivateValue, localPrivateBrandClass, @@ -32,7 +44,6 @@ import { } from "../../type-mapper.js"; import { BOOL, - BYTES_ELEMENT_NAME, BYTES_U8, DYN, F64, @@ -57,7 +68,9 @@ import { unionContainerArmsOk, BYTES_ELEMENT_SIZE, } from "../../../ir/ir.js"; -import { lowerNavigatorMember } from "../builtins/navigator.js"; +import { lowerNavigatorMember, navigatorValue } from "../builtins/navigator.js"; +import { lowerX509PublicKey } from "../builtins/x509.js"; +import { fenceMimeStaticRead } from "../builtins/mime.js"; import { NODE_COMPAT_VERSION } from "../builtins/process.js"; import { inlineNamespaceRequireOf, @@ -111,6 +124,8 @@ import { dynUndefinedExpr, nodeThrowExpr, numLit, strLit, varRef } from "../../. import { typedArrayLacksMember } from "../builtins/typed-array-members.js"; import { TEMPORAL_GLOBAL } from "../../builtin-modules.js"; import { tryLowerExpression } from "./try-lower-expression.js"; +import { lowerRecordInheritedMember } from "../record-inherited.js"; +import { DATE_METHOD_HINT } from "../builtins/dates.js"; import { cjsClassExprWholeExportOf, cjsExportAssignmentOf, @@ -134,6 +149,7 @@ import { import { expandoMemberRead } from "../lower-expando.js"; import { lowerProtocolPropertyRead } from "../class-protocol-dispatch.js"; import { lowerReadlineProperty } from "../builtins/readline.js"; +import { lowerAsyncLocalStorageProperty } from "../builtins/async-context.js"; import { lowerEventStaticProperty } from "../builtins/numbers.js"; import { lowerStreamProperty, lowerStreamStateProperty, streamSidesOf } from "../lower-stream.js"; import { lowerValueStreamProperty } from "../builtins/hash-stream.js"; @@ -144,6 +160,11 @@ import { nullishGenericBindingUnitOf } from "../binding-analysis.js"; import { blockedBindingUseDiag } from "../../../diagnostics/diagnostic.js"; import { npmStaticPackageOfPath } from "../../npm-static.js"; import { importMetaField, isImportMeta } from "../import-meta.js"; +import { createRequireCalleeFileOf } from "../builtins/module-bindings.js"; +import { + createRequireResolveFenceValue, + createRequireResolveValue, +} from "../builtins/module-resolution.js"; import { builtinPrototypeMethod, lowerArrayIsArrayValue, @@ -153,10 +174,12 @@ import { lowerStringCodesValue, } from "../lower-builtin-values.js"; import { lowerArrayFromValue } from "../containers/array-construction.js"; +import { lowerBufferPrototypeMember } from "../containers/bytes.js"; import { lowerNetAddressStaticRead } from "../builtins/net-address.js"; import { STATS_TYPE_TESTS } from "../builtins/filesystem.js"; import { arrayValueRead, arrayValueType } from "../array-values.js"; import { lowerClassMethodValue } from "../class-method-values.js"; +import { reflectMemberValue } from "../builtins/reflection.js"; import { objLitGenericFnInfoOf, objLitGenericFnNodeOf, @@ -283,11 +306,29 @@ const DGRAM_METHOD_VALUES = new Set([ "unref", ]); +function isCreateRequireResolveValue(lowerer: Lowerer, expr: ts.PropertyAccessExpression): boolean { + let resolve: ts.Expression = expr; + if ( + expr.name.text === "paths" && + ts.isPropertyAccessExpression(expr.expression) && + !expr.expression.questionDotToken + ) + resolve = expr.expression; + return ( + !expr.questionDotToken && + ts.isPropertyAccessExpression(resolve) && + resolve.name.text === "resolve" && + createRequireCalleeFileOf(lowerer, resolve.expression) !== null + ); +} + export function lowerPropertyAccessExpr( lowerer: Lowerer, expr: ts.PropertyAccessExpression, loc: SrcLoc, ): IrExpr { + const arrayClassConstructor = lowerArrayClassConstructorRead(lowerer, expr, loc); + if (arrayClassConstructor !== null) return arrayClassConstructor; // `value.#x` on a checked-dynamic value, or on a checked extraction of // one: the field of the brand-checked receiver (checkedPrivateValue). if (ts.isPrivateIdentifier(expr.name)) { @@ -310,6 +351,20 @@ export function lowerPropertyAccessExpr( // Cloudflare probe) folds like `navigator.userAgent`. const navigator = lowerNavigatorMember(lowerer, expr, NODE_COMPAT_VERSION); if (navigator) return navigator; + // util.inspect.custom: Symbol.for('nodejs.util.inspect.custom'). + if (isInspectCustomAccess(lowerer, expr)) { + return { + kind: "libCall", + fn: "sym.for", + args: [{ kind: "strLit", value: INSPECT_CUSTOM_DESCRIPTION, type: STRING, loc }], + type: SYMBOL_T, + loc, + }; + } + { + const member = lowerBufferPrototypeMember(lowerer, expr); + if (member) return member; + } // A dgram socket's method read as a value in JavaScript (`emit: // socket.emit.bind(socket)`): the socket crosses as its checked-dynamic // handle, whose methods are function values that dispatch on their @@ -341,6 +396,11 @@ export function lowerPropertyAccessExpr( const reading = lowerHistogramProperty(lowerer, expr); if (reading) return reading; } + // An X509Certificate's publicKey KeyObject. + { + const publicKey = lowerX509PublicKey(lowerer, expr); + if (publicKey) return publicKey; + } // `.length` of a forwarded fixed tuple (a typed `arguments`, a tuple // rest parameter, an alias of either): the signature's arity. if (expr.name.text === "length" && !expr.questionDotToken) { @@ -492,6 +552,8 @@ export function lowerPropertyAccessExpr( }; } if (lowerer.stdlibGlobalMember(expr, "Object") === "freeze") { + // Object.freeze as a value can freeze any untyped value it is passed. + lowerer.untypedIntegrity = true; const name = "%builtin.Object.freeze"; if (!lowerer.liftedFns.some((fn) => fn.name === name)) lowerer.liftedFns.push({ @@ -557,7 +619,10 @@ export function lowerPropertyAccessExpr( // (`(_a = globalThis).__config ?? (_a.__config = {})`) reads back from // the global object's own properties. const globalType = lowerer.typeOf(staticallySelected(lowerer, expr.expression)); - if (globalThisWrittenNames(lowerer, globalType).has(expr.name.text)) { + if ( + globalThisWrittenNames(lowerer, globalType).has(expr.name.text) || + lowerer.globalThisComputedWrite + ) { return { kind: "dynKeyGet", value: lowerer.coerceToExpected(lowerer.lowerExpr(expr.expression), DYN), @@ -566,6 +631,10 @@ export function lowerPropertyAccessExpr( loc, }; } + // Node's navigator, which JavaScript reads as a free name when no + // declaration types it (isNavigatorGlobal). + if (expr.name.text === "navigator" && isJsSourceFile(ts.getSourceFileOfNode(expr))) + return navigatorValue(loc); return dynUndefinedExpr(loc); } // `Uint8Array.prototype.toBase64` and its siblings, the base64/hex @@ -682,7 +751,31 @@ export function lowerPropertyAccessExpr( const declared = (fn as ts.FunctionLikeDeclaration).parameters.nodes.filter( (p) => !isThisParameter(p), ).length; - const count = Math.max(declared, contextualArgumentCount(lowerer, fn) ?? 0); + const contextual = contextualArgumentCount(lowerer, fn); + if (contextual === "varies") { + lowerer.unsupported( + "SC1090", + expr, + "'arguments.length' in function expressions passed where the expected function type has optional or rest parameters (the count varies per call)", + ); + } + // A top-level declaration also used as a value reads the count its + // caller stored (argument-counts.ts). + if (ts.isFunctionDeclaration(fn) && lowerer.ctx.argumentCount !== undefined) + return varRef(lowerer.ctx.argumentCount.id, F64, loc); + if ( + (ts.isFunctionDeclaration(fn) || ts.isMethodDeclaration(fn)) && + usedAsValue(lowerer, fn) + ) { + lowerer.unsupported( + "SC1090", + expr, + ts.isFunctionDeclaration(fn) && argumentCountSlot(lowerer, fn) !== null + ? "'arguments.length' in an arrow function inside a function that is also used as a value" + : "'arguments.length' in methods, nested, async and generator functions used as values (a caller of the value can pass more arguments than the function declares)", + ); + } + const count = Math.max(declared, contextual ?? 0); // Calls through the expression's own name pass the declared count, // calls through the wider slot pass more: no constant answers both. if (count > declared && referencesOwnName(lowerer, fn)) { @@ -833,6 +926,8 @@ export function lowerPropertyAccessExpr( // diagnostics_channel Channel receivers — name/hasSubscribers // over the f64 channel handle. lowerer.lowerDiagnosticsChannelProperty(expr) ?? + // AsyncLocalStorage receivers — the name option. + lowerAsyncLocalStorageProperty(lowerer, expr) ?? // readline Interface receivers — the history and option members // over the f64 interface handle. lowerReadlineProperty(lowerer, expr) ?? @@ -869,6 +964,7 @@ export function lowerPropertyAccessExpr( // prototype that no lowering above claimed keeps the SC2020 fence // rather than reading undefined through the field and dynamic reads. fenceBuiltinConstructorStaticRead(lowerer, expr) ?? + fenceMimeStaticRead(lowerer, expr) ?? lowerer.lowerFieldRead(expr); // A union-typed field read narrows like an identifier when the // checker has narrowed this use to one arm. @@ -913,18 +1009,8 @@ export function lowerPropertyAccessExpr( if (canonical === "globalThis") return lowerGlobalValue(lowerer, expr); // Typed for the programs that name it, absent in Node 24. if (canonical === TEMPORAL_GLOBAL) return dynUndefinedExpr(loc); - if (canonical === "ArrayBuffer") - return { kind: "libCall", fn: "arrayBuffer.constructor", args: [], type: DYN, loc }; - if (canonical === "fetch") - return { kind: "libCall", fn: "fetch.function", args: [], type: DYN, loc }; - if (Object.values(BYTES_ELEMENT_NAME).includes(canonical)) - return { - kind: "libCall", - fn: "bytes.constructor", - args: [{ kind: "strLit", value: canonical, type: STRING, loc }], - type: DYN, - loc, - }; + const value = jsSpecialGlobalValue(lowerer, canonical, loc); + if (value) return value; return ( builtinGlobalValueAt(lowerer, expr, canonical, loc, true) ?? { kind: "strLit", @@ -1105,11 +1191,18 @@ export function lowerPropertyAccessExpr( ) ) { const fnName = jsFuncNameOf(expr.expression); + // A builtin function value whose length is not its adapter's + // parameter count (`Math.max.length` is 2). + const fnLength = + probed.kind === "closure" + ? lowerer.builtinCallableValueLengths.get(probed.fnName) + : undefined; const boxed: IrExpr = { kind: "dynFrom", value: probed, type: DYN, ...(fnName !== null ? { fnName } : {}), + ...(fnLength !== undefined ? { fnLength } : {}), loc, }; const key: IrExpr = { @@ -1266,6 +1359,14 @@ export function lowerPropertyAccessExpr( return declared && declared.kind !== "void" ? lowerer.coerceToExpected(read, declared) : read; } } + // A createRequire loader's resolve read as a value (passed on, or + // picked by `??`): a function that resolves its request at run time. + // resolve.paths read as a value has no run-time lowering: a function + // that throws the fence when called. + if (isCreateRequireResolveValue(lowerer, expr)) + return expr.name.text === "paths" + ? createRequireResolveFenceValue(lowerer, expr, loc) + : createRequireResolveValue(lowerer, expr, loc); // The lib fence's PROPERTY chokepoint: a stdlib-declared member that // no lowering above claimed ([1,2].entries, Math.SQRT2, Promise.all, // re.exec as a value, ...) reports SC2020 here. @@ -1290,7 +1391,23 @@ export function lowerPropertyAccessExpr( // honest one, not a generic recitation about the outer dot. A // receiver that lowers cleanly means the MEMBER is the gap — name // the property and the receiver's type instead of "property access". - const recvLowered = lowerer.lowerExpr(expr.expression); + let recvLowered = lowerer.lowerExpr(expr.expression); + // A receiver whose value union holds record types the checker's subtype + // reduction folded into its type (`{ status } | { status, value }` typed + // `{ status }`, a call of a function with an inferred return type): read + // through the checker's type, converting the wider arms. + if (recvLowered.type.kind === "union") { + const declared = lowerer.mapTypeOf(lowerer.typeOf(expr.expression)); + if ( + declared !== null && + declared.kind === "record" && + widensReducedRecords(lowerer, declared, recvLowered.type) + ) { + const helper = widthNarrowHelper(lowerer, recvLowered.type.unionId, declared, loc); + if (helper !== null) + recvLowered = lowerer.checkedNarrowCall(helper, recvLowered, declared, loc); + } + } // A dyn receiver — JSON.parse's `any`, or `unknown` the checker // narrowed to `object` — reads through the dyn keyed read: member // or undefined (JS's own-property answer), throwing JS's TypeError @@ -1499,6 +1616,25 @@ export function lowerPropertyAccessExpr( ) { return afterReceiver(recvLowered, dynUndefinedExpr(loc), loc); } + // A typed array under a checker `any` (a JS binding its own initializer's + // inference refers to: hono's etag digest `nextChunk`) reads through its + // box, as the method-call twin dispatches. The box shares the native + // storage, so it needs no fresh-result restriction. + if ( + recvLowered.type.kind === "bytes" && + (lowerer.typeOf(expr.expression).flags & ts.TypeFlags.Any) !== 0 + ) { + return lowerer.maybeNarrow( + { + kind: "dynKeyGet", + value: lowerer.coerceToExpected(recvLowered, DYN), + key: { kind: "strLit", value: expr.name.text, type: STRING, loc }, + type: DYN, + loc, + }, + expr, + ); + } lowerer.unsupported( "SC1090", expr, @@ -1655,19 +1791,8 @@ export function lowerIntrinsicProperty( ); } } - if (lowerer.stdlibGlobalMember(expr, "Reflect") === "ownKeys") { - const value = lowerer.lowerNativeCallableValue( - { - fn: "dyn.ownKeys", - params: [DYN], - result: DYN, - valueParams: [{ mode: "required", type: DYN }], - }, - "Reflect.ownKeys", - locOf(expr), - )!; - return { kind: "dynFrom", value, fnName: "ownKeys", type: DYN, loc: locOf(expr) }; - } + if (lowerer.stdlibGlobalMember(expr, "Reflect") === "ownKeys") + return reflectMemberValue(lowerer, "ownKeys", locOf(expr)); if (lowerer.chainBlocked(expr)) return null; // Object.prototype.constructor: the Object constructor value. if ( @@ -1769,6 +1894,28 @@ export function lowerIntrinsicProperty( return lowerNumberParserValue(lowerer, numberMember, locOf(expr)); if (lowerer.stdlibGlobalMember(expr, "Buffer") === "poolSize") return { kind: "libCall", fn: "buffer.poolSize", args: [], type: F64, loc: locOf(expr) }; + { + const member = lowerer.stdlibGlobalMember(expr, "Buffer"); + if (member === "name") + return { kind: "strLit", value: "Buffer", type: STRING, loc: locOf(expr) }; + if (member === "length") return numLit(3, locOf(expr)); + // JavaScript reads the statics from the Buffer function's value, so + // each has one identity and Node's name and length. + if ( + member !== null && + BUFFER_VALUE_STATICS.has(member) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) + ) { + const loc = locOf(expr); + return { + kind: "libCall", + fn: "buffer.staticValue", + args: [{ kind: "strLit", value: member, type: STRING, loc }], + type: DYN, + loc, + }; + } + } const netAddressStatic = lowerNetAddressStaticRead(lowerer, expr); if (netAddressStatic) return netAddressStatic; if (lowerer.stdlibGlobalMember(expr, "Buffer") === "isBuffer") @@ -2005,6 +2152,9 @@ export function lowerIntrinsicProperty( ); } if (kind === "date") { + // A Date held as a checked-dynamic value (a Date a JavaScript file + // constructs) answers its method values at run time. + if (tryLowerExpression(lowerer, expr.expression)?.type.kind === "dyn") return null; const methods = new Set([ "getTime", "valueOf", @@ -2033,7 +2183,7 @@ export function lowerIntrinsicProperty( lowerer.noLowering( `Date.prototype.${name}`, expr, - "getTime(), valueOf(), toISOString(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale/string formatters have no lowering", + DATE_METHOD_HINT, lowerer.checker.getSymbolAtLocation(expr.name), ); } @@ -3081,6 +3231,12 @@ export function lowerFieldRead(lowerer: Lowerer, expr: ts.PropertyAccessExpressi loc: locOf(expr), }; } + // Object.prototype's methods read as values: an own entry of the + // member's function type, else the built-in. + if (actual?.type.kind === "record") { + const inherited = lowerRecordInheritedMember(lowerer, actual, expr); + if (inherited) return inherited; + } // A member the checker declares on the receiver, stored in a pure // index-signature record (an object literal that spread one, see // dictSpreadLiteralShape): the key is always present, so the keyed @@ -3224,6 +3380,18 @@ export function lowerUnionProperty( expr, ); } + // An array the checker sees through a union of arrays (`readdirSync`'s + // `string[] | Buffer[]`, whose lowering answers the string[] arm). + if (value.type.kind === "array" && expr.name.text === "length") { + return { + kind: "arrIntrinsic", + method: "length", + receiver: value, + args: [], + type: F64, + loc: locOf(expr), + }; + } if (value.type.kind !== "union") { lowerer.unsupported( "SC1090", @@ -3345,6 +3513,13 @@ export function lowerUnionProperty( loc: locOf(expr), }; } + // A union of typed arrays and number arrays: each arm's own length + // (and, for typed arrays only, byteLength, byteOffset, BYTES_PER_ELEMENT). + const indexedArms = indexedUnionArms(lowerer, value.type); + if (indexedArms) { + const member = lowerIndexedUnionMember(lowerer, value, indexedArms, field, locOf(expr)); + if (member) return member; + } lowerer.unsupported( "SC1090", expr, @@ -3492,13 +3667,13 @@ export function classFieldTarget( ) { return { container: "dynamic", obj: represented, field: fieldName, fieldType: DYN }; } - const info = lowerer.classes.get(receiverType.className); - if (!info) { - // A receiver typed as a class whose collection deferred: the - // deferred diagnostics are what explains the miss. + // A receiver typed as a class whose collection deferred: the deferred + // diagnostics are what explains the miss (unless the flush registers + // the class with dynamic slots, retryDeferred). + const info = + lowerer.classes.get(receiverType.className) ?? lowerer.flushDeferredClass(receiverType.className); - return null; - } + if (!info) return null; const fieldType = info.fields.get(fieldName); const lowerObjectReceiver = (): IrExpr => { let obj = represented; @@ -3668,6 +3843,32 @@ export function fieldTarget( ); } } + // A field declared by a class that extends Array, on an instance (whose + // type maps to its array): the class object's field. + { + const owner = arrayClassMemberOwner(lowerer, access); + const name = access.name.text; + if ( + owner !== null && + (owner.fields.has(name) || + lowerer.findMethodOn(owner, `get:${name}`) || + lowerer.findMethodOn(owner, `set:${name}`)) + ) { + const receiver = arrayClassReceiver( + lowerer, + owner, + lowerer.lowerExpr(access.expression), + access.expression, + ); + return classFieldTarget( + lowerer, + access.expression, + { kind: "object", className: owner.def.name }, + access.name.text, + receiver, + ); + } + } const stored = ts.isIdentifier(access.expression) ? (lowerer.peekLocal(access.expression)?.type ?? lowerer.globalOf(access.expression)?.type) : undefined; @@ -4145,9 +4346,13 @@ const GLOBAL_CONSTRUCTOR_VALUES: ReadonlySet = new Set([ /** The builtin global functions JavaScript sources read as interned * function values (the `cond ? setImmediate : process.nextTick` feature - * probe of bcryptjs and async libraries): typeof answers "function", - * identity holds across spellings, and calls run the timer APIs. */ + * probe of bcryptjs and async libraries, and the known globals Node's test + * harness collects): typeof answers "function", identity holds across + * spellings, and calls run what the direct calls run. */ const GLOBAL_FUNCTION_VALUES: ReadonlySet = new Set([ + "atob", + "btoa", + "structuredClone", "setTimeout", "setInterval", "setImmediate", @@ -4173,6 +4378,8 @@ export function builtinFunctionValue(name: string, loc: SrcLoc): IrExpr { * null when it stays an identity token (JavaScript) or fenced. */ export function builtinGlobalValue(canonical: string, loc: SrcLoc, js = false): IrExpr | null { if (js && GLOBAL_FUNCTION_VALUES.has(canonical)) return builtinFunctionValue(canonical, loc); + if (js && canonical === "Buffer") + return { kind: "libCall", fn: "buffer.constructor", args: [], type: DYN, loc }; if (GLOBAL_CONSTRUCTOR_VALUES.has(canonical)) { return { kind: "libCall", @@ -4218,6 +4425,10 @@ export function builtinGlobalValueAt( loc: SrcLoc, js: boolean, ): IrExpr | null { + // A namespace object models its own properties, so member reads and + // reflection read it like any object. + const namespace = js ? builtinNamespaceValue(lowerer, canonical, loc) : null; + if (namespace !== null) return namespace; if (readsBuiltinGlobalMembers(expr)) return null; const value = builtinGlobalValue(canonical, loc, js); if (value === null) return null; @@ -4300,6 +4511,22 @@ function fenceBuiltinConstructorStaticRead( return null; } +/** The Buffer statics the Buffer function's value holds + * (scr_buffer_constructor_value). */ +const BUFFER_VALUE_STATICS: ReadonlySet = new Set([ + "from", + "copyBytesFrom", + "of", + "alloc", + "allocUnsafe", + "allocUnsafeSlow", + "isBuffer", + "compare", + "isEncoding", + "concat", + "byteLength", +]); + /** Object.prototype members a plain record inherits, read as values. */ const OBJECT_PROTOTYPE_VALUE_READS: ReadonlySet = new Set([ "constructor", @@ -4483,26 +4710,9 @@ function referencesOwnName(lowerer: Lowerer, fn: ts.Node): boolean { } /** The argument count every caller passes a function expression whose - * value flows into one fixed-arity function type, an array method's - * callback or an annotated slot: that type's parameter count. Null when - * there is no such type or its arity varies. */ -function contextualArgumentCount(lowerer: Lowerer, fn: ts.Node): number | null { - if (!ts.isFunctionExpression(fn)) return null; - const contextual = lowerer.checker.getContextualType(fn); - if (contextual === undefined) return null; - const signatures = lowerer.checker.getCallSignatures( - lowerer.checker.getNonNullableType(contextual), - ); - if (signatures.length !== 1) return null; - const declaration = signatures[0]!.declaration; - if (declaration === undefined) return null; - let count = 0; - for (const parameter of (declaration.parameters?.nodes ?? []) as ts.ParameterDeclaration[]) { - if (isThisParameter(parameter)) continue; - if (parameter.questionToken || parameter.initializer || parameter.dotDotDotToken) return null; - count++; - } - return count; + * value flows into a function type (argument-counts.ts contextualCallCount). */ +function contextualArgumentCount(lowerer: Lowerer, fn: ts.Node): number | "varies" | null { + return ts.isFunctionExpression(fn) ? contextualCallCount(lowerer, fn) : null; } /** The property names the program assigns on the global object, through @@ -4520,11 +4730,24 @@ function globalThisWrittenNames(lowerer: Lowerer, globalType: ts.Type): Set = new Set([ "nullT", ]); +const PRIMITIVE_WRAPPERS = ["Boolean", "Number", "String", "BigInt", "Symbol"] as const; + /** `x instanceof C` for a program-declared class C. When x's static * class and C are both in extends-hierarchies the test is dynamic — the * O(1) preorder-interval check against the vtable (`instanceOf` node). @@ -106,6 +110,8 @@ const NO_CLASS_INSTANCE_KINDS: ReadonlySet = new Set([ * Folding is limited to side-effect-free operands — dropping a computed * operand would skip its effects, so those are rejected instead. */ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc: SrcLoc): IrExpr { + const arrayClassTest = lowerArrayClassInstanceOf(lowerer, expr, loc); + if (arrayClassTest !== null) return arrayClassTest; const webAssembly = lowerWebAssemblyInstanceof(lowerer, expr); if (webAssembly !== null) return webAssembly; refuseWebAssemblyUse(lowerer, expr.right); @@ -187,6 +193,20 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc loc, }; } + // The primitive wrapper constructors: a compiled program never creates a + // boxed primitive (`new Boolean(...)` and friends have no lowering), and + // a primitive is never an instance, so the test is false for every value + // (typebox's IsBoolean/IsNumber/IsString guards). The operand still runs. + if (PRIMITIVE_WRAPPERS.some((name) => lowerer.isStdlibGlobal(expr.right, name))) { + const value = lowerer.lowerExpr(expr.left); + return { + kind: "seqExpr", + stmts: [{ kind: "exprStmt", expr: value, loc }], + result: { kind: "boolLit", value: false, type: BOOL, loc }, + type: BOOL, + loc, + }; + } if (lowerer.isStdlibGlobal(expr.right, "FinalizationRegistry")) { const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN); return { kind: "libCall", fn: "finalizationRegistry.is", args: [value], type: BOOL, loc }; @@ -230,6 +250,8 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc if (cryptoKey) return cryptoKey; const netAddress = lowerNetAddressInstanceof(lowerer, expr); if (netAddress) return netAddress; + const mime = lowerMimeInstanceof(lowerer, expr); + if (mime) return mime; const readlineInterface = lowerReadlineInstanceof(lowerer, expr); if (readlineInterface) return readlineInterface; } @@ -288,6 +310,16 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc if (!target) { const date = lowerer.isStdlibGlobal(expr.right, "Date"); const url = lowerer.isStdlibGlobal(expr.right, "URL"); + // A catch binding holds a thrown Date as its checked-dynamic handle. + const caughtDate = date ? lowerer.caughtLocalOf(expr.left) : null; + if (caughtDate) + return { + kind: "libCall", + fn: "dyn.nativeDateIs", + args: [{ kind: "caughtToDyn", value: varRef(caughtDate.id, CAUGHT, loc), type: DYN, loc }], + type: BOOL, + loc, + }; if ((date || url) && !lowerer.caughtLocalOf(expr.left)) { const value = lowerer.lowerExpr(expr.left); if (value.type.kind === "dyn") @@ -338,6 +370,22 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc type: BOOL, loc, }; + // A codec in untyped storage is its record's handle box. + const codec = (["TextEncoder", "TextDecoder"] as const).find((name) => + lowerer.isStdlibGlobal(expr.right, name), + ); + if (codec && lowerer.mapTypeOf(lowerer.typeOf(expr.left))?.kind === "dyn") { + return { + kind: "libCall", + fn: "text.codecIs", + args: [ + lowerer.lowerExprExpecting(expr.left, DYN), + { kind: "boolLit", value: codec === "TextDecoder", type: BOOL, loc }, + ], + type: BOOL, + loc, + }; + } if (lowerer.isStdlibGlobal(expr.right, "ReadableStream")) { const value = lowerer.lowerExprExpecting(expr.left, DYN); return { kind: "libCall", fn: "fetch.streamIs", args: [value], type: BOOL, loc }; @@ -388,6 +436,22 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc loc, }; } + // Plain data (records, arrays, strings, numbers, booleans, and unions + // of them) is never a promise: false after the operand's evaluation. + const neverPromise = (t: IrType): boolean => + NEVER_PROMISE_KINDS.has(t.kind) || + (t.kind === "union" && (lowerer.unions.get(t.unionId)?.arms ?? [t]).every(neverPromise)); + if (neverPromise(left.type)) { + const no: IrExpr = { kind: "boolLit", value: false, type: BOOL, loc }; + if (isSafeToDiscard(left)) return no; + return { + kind: "seqExpr", + stmts: [{ kind: "exprStmt", expr: left, loc }], + result: no, + type: BOOL, + loc, + }; + } lowerer.unsupported( "SC1090", expr, @@ -490,12 +554,13 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc }; } if (rhsClassval?.kind === "classval" && !caught) { - const targetInfo = lowerer.classes.get(rhsClassval.className); + const targetInfo = + lowerer.classes.get(rhsClassval.className) ?? + lowerer.flushDeferredClass(rhsClassval.className); if (!targetInfo) { // The type world names a class the lowering never registered - // (a fenced class expression, a deferred declaration): flush - // its diagnostics and poison the test site, never an ICE. - lowerer.flushDeferredClass(rhsClassval.className); + // (a fenced class expression, a deferred declaration): its + // diagnostics flushed above; poison the test site, never an ICE. lowerer.unsupported( "SC1090", expr, @@ -1886,6 +1951,30 @@ export function lowerInExpression( }; } } + // A child process has `send`, `disconnect` and `channel` only when it was + // created with an IPC channel (fork, or an "ipc" stdio entry), and keeps + // them after the channel closes: the runtime answers from the child. + if (recv.type.kind === "child" && CHILD_CHANNEL_MEMBERS.has(key)) + return { kind: "libCall", fn: "child.hasChannel", args: [recv], type: BOOL, loc }; + // A native Node handle (an http.Server, a socket, a child process...): + // a member Node's declarations give the class is on its prototype, so + // `"closeAllConnections" in server` answers true. Undeclared names can + // still be real (internal `_`-prefixed fields), so they keep the fence. + if ( + NATIVE_HANDLE_KINDS.has(recv.type.kind) && + nodeDeclaredMember(lowerer, lowerer.typeOf(expr.right), key) + ) { + const answer: IrExpr = { kind: "boolLit", value: true, type: BOOL, loc }; + return isSafeToDiscard(recv) + ? answer + : { + kind: "seqExpr", + stmts: [{ kind: "exprStmt", expr: recv, loc }], + result: answer, + type: BOOL, + loc, + }; + } if (recv.type.kind !== "record") { lowerer.unsupported( "SC1090", @@ -2162,24 +2251,60 @@ function lowerRuntimeKeyIn( return { kind: "call", callee: helper, args: [keyIr, recv], type: BOOL, loc }; } -/** Standard-library constructors the untyped instanceof helper has no - * instance test for: an untyped `x instanceof Object` always refuses. */ -const DYN_INSTANCEOF_REFUSED: ReadonlySet = new Set(["Object", "Function"]); - /** The SC1090 message `x instanceof C` throws, with the site's position, - * when `C` is a runtime constructor with no instance test. A right-hand - * side that names one of DYN_INSTANCEOF_REFUSED always reaches that - * refusal, so the site is listed with the runtime fences. */ + * when `C` is a runtime constructor with no instance test. */ function dynInstanceOfRefusal(lowerer: Lowerer, expr: ts.BinaryExpression): string { const diag = unsupportedDiag("SC1090", locOf(expr), "instanceof against this native constructor"); - const symbol = lowerer.checker.getSymbolAtLocation(expr.right); - if ( - symbol !== undefined && - lowerer.isStdlibSymbol(symbol) && - DYN_INSTANCEOF_REFUSED.has(ts.symbolName(symbol)) - ) - lowerer.runtimeFences.push(diag); const sf = ts.getSourceFileOfNode(expr); const line = ts.getLineAndCharacterOfPosition(sf, diag.loc.start).line + 1; return `${diag.message} [${diag.code} at ${diag.loc.file}:${line}]`; } + +/** IR kinds of native Node handles whose members come from Node's own + * declarations. */ +const NATIVE_HANDLE_KINDS: ReadonlySet = new Set([ + "netServer", + "netSocket", + "httpReq", + "httpRes", + "httpClientReq", + "child", + "dgramSocket", + "fsWatcher", + "http2Session", + "http2Stream", +]); + +/** Child process members that exist only on a child with an IPC channel. */ +const CHILD_CHANNEL_MEMBERS: ReadonlySet = new Set(["send", "disconnect", "channel"]); + +/** Whether every constituent of `t` declares a required member `key` in + * a declaration file (Node's @types/node). */ +function nodeDeclaredMember(lowerer: Lowerer, t: ts.Type, key: string): boolean { + const parts = t.isUnionType() ? ts.constituentTypes(t) : [t]; + return parts.every((part) => { + const prop = lowerer.checker.getPropertyOfType(part, key); + if (prop === undefined || (prop.flags & ts.SymbolFlags.Optional) !== 0) return false; + return ( + prop.declarations.length > 0 && + prop.declarations.every((d) => ts.getSourceFileOfNode(d).isDeclarationFile) + ); + }); +} + +const NEVER_PROMISE_KINDS: ReadonlySet = new Set([ + "record", + "array", + "string", + "f64", + "bool", + "bigint", + "date", + "map", + "set", + "regex", + "bytes", + "symbol", + "nullT", + "undefinedT", +]); diff --git a/packages/compiler/src/frontend/lowering/expressions/typeof.ts b/packages/compiler/src/frontend/lowering/expressions/typeof.ts index cc3ac06fb..5f1e4a830 100644 --- a/packages/compiler/src/frontend/lowering/expressions/typeof.ts +++ b/packages/compiler/src/frontend/lowering/expressions/typeof.ts @@ -34,7 +34,12 @@ import { varRef } from "../../../ir/build.js"; import { isExecExceptionType, refuseExecExceptionCode } from "../builtins/errors.js"; import { InternalCompilerError } from "../../../errors.js"; import { TEMPORAL_GLOBAL } from "../../builtin-modules.js"; -import { isTemporalGlobalRoot } from "../lower-namespaces.js"; +import { + ambientUndefinedFnSymbolOf, + ambientUndefinedVarSymbolOf, + isTemporalGlobalRoot, +} from "../lower-namespaces.js"; +import { isF64HandleTyped } from "../f64-handles.js"; export function lowerTypeofExpr(lowerer: Lowerer, expr: ts.TypeOfExpression, loc: SrcLoc): IrExpr { const webAssembly = lowerWebAssemblyTypeof(lowerer, expr.expression); @@ -88,6 +93,12 @@ export function lowerTypeofExpr(lowerer: Lowerer, expr: ts.TypeOfExpression, loc if (builtinClassRefOf(lowerer, expr.expression) !== null) { return { kind: "strLit", value: "function", type: STRING, loc }; } + // The Buffer constructor and its aliases (`const B = globalThis.Buffer`, + // `(globalThis as { Buffer?: B }).Buffer`): a value read of an alias is an + // identity token, so its typeof is decided here. + if (stdlibGlobalNameOf(lowerer, expr.expression) === "Buffer") { + return { kind: "strLit", value: "function", type: STRING, loc }; + } if (ts.isPropertyAccessExpression(expr.expression)) { const member = expr.expression; const prototype = member.expression; @@ -213,6 +224,16 @@ export function lowerTypeofExpr(lowerer: Lowerer, expr: ts.TypeOfExpression, loc if (isTemporalGlobalRoot(lowerer, tested, sym)) { return { kind: "strLit", value: "undefined", type: STRING, loc }; } + // A name the program declares ambiently but nothing defines + // (`declare const Bun`, `declare function f(): void`): Node erases the + // declaration, so typeof answers "undefined" without reading it, where + // a read throws ReferenceError. + if ( + ambientUndefinedVarSymbolOf(lowerer, tested) || + ambientUndefinedFnSymbolOf(lowerer, tested) + ) { + return { kind: "strLit", value: "undefined", type: STRING, loc }; + } // `const { inspect } = require("util")` / `import { inspect } from // "node:util"`: a binding of a builtin module's exported function, // class or object. JavaScript sources represent such a value without @@ -275,13 +296,18 @@ export function lowerTypeofExpr(lowerer: Lowerer, expr: ts.TypeOfExpression, loc promise: "object", bytes: "object", regex: "object", + date: "object", generator: "object", classval: "function", moduleNs: "object", undefinedT: "undefined", nullT: "object", }; - const folded = FOLD[operand.type.kind]; + // An f64 handle (a readline Interface, a Timeout) is an object. + const folded = + operand.type.kind === "f64" && isF64HandleTyped(lowerer, expr.expression) + ? "object" + : FOLD[operand.type.kind]; if (folded) { const result: IrExpr = { kind: "strLit", value: folded, type: STRING, loc }; return isSafeToDiscard(operand) diff --git a/packages/compiler/src/frontend/lowering/expressions/union-fields.ts b/packages/compiler/src/frontend/lowering/expressions/union-fields.ts index 6454bc1fd..01c841fdd 100644 --- a/packages/compiler/src/frontend/lowering/expressions/union-fields.ts +++ b/packages/compiler/src/frontend/lowering/expressions/union-fields.ts @@ -2,6 +2,7 @@ import { nodeThrowExpr, varRef } from "../../../ir/build.js"; import * as ts from "../../ts7/adapter.js"; import type { Lowerer } from "../lowerer.js"; import { narrowStoredClassValue } from "../class-unions.js"; +import { markSloppyRecordWrites } from "../frozen-records.js"; import { BOOL, type IrExpr, @@ -246,6 +247,8 @@ export function lowerUnionFieldWrite( value: slotValue, loc, }; + // A sloppy-mode write to a frozen record leaves it unchanged. + if (write.kind === "recordSet") markSloppyRecordWrites(lowerer, write, access); branch = { kind: "seqExpr", stmts: [write], result, type: result.type, loc }; } else { branch = nodeThrowExpr( @@ -278,3 +281,58 @@ export function lowerUnionFieldWrite( } return { kind: "seqExpr", stmts: body, result: dispatch, type: result.type, loc }; } + +/** Read the element at a literal index from a union of tuples whose element + * types differ (`readonly [true, number] | readonly [false, string]`, index + * 1): each tag reads its own tuple's slot and widens it into the union of + * the arms' element types; the caller narrows the result to the checker's + * flow type. Null when an arm is not a tuple with that position. */ +export function lowerUnionTupleRead( + lowerer: Lowerer, + receiver: IrExpr, + index: number, + loc: IrExpr["loc"], +): IrExpr | null { + if (receiver.type.kind !== "union" || !Number.isInteger(index) || index < 0) return null; + const unionId = receiver.type.unionId; + const arms = lowerer.unions.get(unionId)?.arms; + if (!arms || arms.length < 2) return null; + const name = String(index); + const fields: UnionFieldArm[] = []; + const parts: IrType[] = []; + for (const arm of arms) { + if (arm.kind !== "record" || lowerer.shapes.get(arm.shapeId)?.tuple !== true) return null; + const field = unionFieldArm(lowerer, arm, name); + if (!field || field.type.kind === "void") return null; + fields.push(field); + const own = + field.type.kind === "union" ? lowerer.unions.get(field.type.unionId)?.arms : [field.type]; + if (!own) return null; + for (const part of own) if (!parts.some((p) => typeEquals(p, part))) parts.push(part); + } + const type: IrType = + parts.length === 1 ? parts[0]! : { kind: "union", unionId: lowerer.unions.intern(parts) }; + const object = lowerer.declareHiddenLocal("%unionReadObject", receiver.type); + const objectRef = varRef(object.id, object.type, loc); + const read = (tag: number): IrExpr | null => { + const probe = unionFieldRead(unionId, tag, objectRef, fields[tag]!, name, loc); + const converted = lowerer.coerceToExpected(probe, type); + return typeEquals(converted.type, type) && isWidening(converted, probe) ? converted : null; + }; + let dispatch = read(fields.length - 1); + if (!dispatch) return null; + for (let tag = fields.length - 2; tag >= 0; tag--) { + const then = read(tag); + if (!then) return null; + dispatch = { + kind: "ternary", + cond: { kind: "unionIsTag", unionId, tag, value: objectRef, negated: false, type: BOOL, loc }, + then, + else_: dispatch, + type, + loc, + }; + } + const init: IrStmt = { kind: "varDecl", localId: object.id, init: receiver, loc }; + return { kind: "seqExpr", stmts: [init], result: dispatch, type, loc }; +} diff --git a/packages/compiler/src/frontend/lowering/expressions/updates.ts b/packages/compiler/src/frontend/lowering/expressions/updates.ts index 04ca23a96..35d5d1a2e 100644 --- a/packages/compiler/src/frontend/lowering/expressions/updates.ts +++ b/packages/compiler/src/frontend/lowering/expressions/updates.ts @@ -1,4 +1,9 @@ /* Increment, decrement and prefix unary operators, and compound field updates. */ +import { + indexedUnionArms, + lowerIndexedUnionNumberRead, + lowerIndexedUnionWrite, +} from "../indexed-unions.js"; import * as ts from "../../ts7/adapter.js"; import { lowerOptionalIndex, @@ -115,6 +120,11 @@ function coercedUnion(lowerer: Lowerer, type: IrType): boolean { return arms.some((arm) => arm.kind !== "f64" && !isUnitType(arm)); } +/** ToNumber of a Date: its time value (valueOf, the number hint). */ +function dateTimeValue(date: IrExpr, loc: SrcLoc): IrExpr { + return { kind: "libCall", fn: "date.valueOf", args: [date], type: F64, loc }; +} + /** ToNumber of a boolean: 1 for true, 0 for false. */ function booleanToNumber(value: IrExpr, loc: SrcLoc): IrExpr { return { @@ -140,23 +150,25 @@ export function lowerPrefixUnary(lowerer: Lowerer, expr: ts.PrefixUnaryExpressio const operand = raw.type.kind === "bool" ? booleanToNumber(raw, loc) - : coercedUnion(lowerer, raw.type) - ? ({ - kind: "libCall", - fn: "dyn.toNumberCoerce", - args: [{ kind: "dynFrom", value: raw, type: DYN, loc }], - type: F64, - loc, - } satisfies IrExpr) - : raw.type.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(expr)) + : raw.type.kind === "date" + ? dateTimeValue(raw, loc) + : coercedUnion(lowerer, raw.type) ? ({ kind: "libCall", fn: "dyn.toNumberCoerce", - args: [raw], + args: [{ kind: "dynFrom", value: raw, type: DYN, loc }], type: F64, loc, } satisfies IrExpr) - : lowerOptionalNumber(lowerer, raw, loc, expr.operand); + : raw.type.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(expr)) + ? ({ + kind: "libCall", + fn: "dyn.toNumberCoerce", + args: [raw], + type: F64, + loc, + } satisfies IrExpr) + : lowerOptionalNumber(lowerer, raw, loc, expr.operand); if (operand.type.kind !== "f64") operatorUnsupported(lowerer, expr, "-", [raw.type]); if (operand.kind === "numLit") return { ...operand, value: -operand.value, loc }; return { kind: "unary", op: "-", operand, type: F64, loc }; @@ -180,6 +192,8 @@ export function lowerPrefixUnary(lowerer: Lowerer, expr: ts.PrefixUnaryExpressio return { kind: "libCall", fn: "num.fromString", args: [raw], type: F64, loc }; } if (raw.type.kind === "bool") return booleanToNumber(raw, loc); + // A Date's number hint answers its time value. + if (raw.type.kind === "date") return dateTimeValue(raw, loc); const classNumber = classToNumberConversion(lowerer, raw, expr.operand, "unary plus"); if (classNumber) return classNumber; const operand = lowerOptionalNumber(lowerer, raw, loc, expr.operand); @@ -191,6 +205,10 @@ export function lowerPrefixUnary(lowerer: Lowerer, expr: ts.PrefixUnaryExpressio operatorUnsupported(lowerer, expr, "+", [raw.type]); } case ts.SyntaxKind.ExclamationToken: { + // `!runtimeBuffer` over an alias of the Buffer global: Node always + // defines Buffer (the condition twin is lowerCondition's). + if (lowerer.isStdlibGlobal(expr.operand, "Buffer")) + return { kind: "boolLit", value: false, type: BOOL, loc }; // `!x` is ToBoolean-then-negate: f64/string operands go through toBool. const operand = lowerer.ensureBool( deferredSlotOr( @@ -386,6 +404,39 @@ export function lowerIncDec( loc, })); } + // A union of typed arrays and number arrays (indexed-unions.ts): the + // element read as a number and the union's element write. + const indexedArms = indexedUnionArms(lowerer, raw.type); + if ( + ts.isElementAccessExpression(access) && + indexedArms && + !isJsSourceFile(ts.getSourceFileOfNode(expr)) + ) { + const receiver = save(raw, "%incrementArray"); + const index = save( + lowerOptionalIndex( + lowerer, + lowerer.lowerExpr(access.argumentExpression), + locOf(access.argumentExpression), + access.argumentExpression, + ), + "%incrementIndex", + ); + if (index.type.kind !== "f64") + lowerer.unsupported("SC1090", access, "incrementing non-number array keys"); + return finish( + lowerIndexedUnionNumberRead(lowerer, receiver, indexedArms, index, loc), + (value) => + lowerIndexedUnionWrite( + lowerer, + receiver, + indexedArms, + index, + lowerer.coerceInto(access, value, F64), + loc, + ), + ); + } // A TS member statically typed number on a checked-dynamic receiver (a // class-backed protocol view): the expanded form `o.v = o.v + 1`, its // read checked into number like any other read of the member. diff --git a/packages/compiler/src/frontend/lowering/f64-handles.ts b/packages/compiler/src/frontend/lowering/f64-handles.ts new file mode 100644 index 000000000..0d7da0ff9 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/f64-handles.ts @@ -0,0 +1,15 @@ +import * as ts from "../ts7/adapter.js"; +import type { Lowerer } from "./lowerer.js"; + +/** Whether `node`'s value is an f64 handle: an object whose class + * type-mapper.ts represents as an f64 id into a runtime registry (a + * readline Interface, a Timeout or Immediate, an AsyncLocalStorage, a + * diagnostics_channel Channel). Its checker type is an object type that + * maps to f64; the Number wrapper interface maps to f64 for primitives. */ +export function isF64HandleTyped(lowerer: Lowerer, node: ts.Node): boolean { + const type = lowerer.checker.getTypeAtLocation(node); + if ((type.flags & ts.TypeFlags.Object) === 0) return false; + const symbol = type.getSymbol(); + if (symbol !== undefined && ts.symbolName(symbol) === "Number") return false; + return lowerer.mapTypeOf(type)?.kind === "f64"; +} diff --git a/packages/compiler/src/frontend/lowering/frozen-records.ts b/packages/compiler/src/frontend/lowering/frozen-records.ts new file mode 100644 index 000000000..fc770a0cb --- /dev/null +++ b/packages/compiler/src/frontend/lowering/frozen-records.ts @@ -0,0 +1,364 @@ +import * as ts from "../ts7/adapter.js"; +import { + BOOL, + VOID, + shapeHasAccessorSlots, + typeKey, + type IrExpr, + type IrFunction, + type IrStmt, + type IrType, + type SrcLoc, +} from "../../ir/ir.js"; +import { numLit, strLit, varRef } from "../../ir/build.js"; +import { everyStmtList, transformStmtList } from "../../ir/traverse.js"; +import type { Lowerer } from "./lowerer.js"; +import { recordHasOwnHelper } from "./lower-calls.js"; +import { isStrictCode } from "./strict-mode.js"; + +/* Object.freeze of records and arrays that other references can still + * write. Only the types a program may freeze pay for it. A record keeps its + * frozen bit in the rc word (scr_rc.h SCR_RC_FROZEN), which its release + * ignores, and every write to it checks the bit first + * (guardFrozenWrites). An array keeps it in its storage flags + * (scr_array.h SCR_ARR_FROZEN), which the backend's array write guards test + * for the array types the module lists (IrModule.freezableArrays). A type is + * freezable when a static Object.freeze names it, or, in a program that + * freezes, seals or prevents extensions of untyped values + * (noteUntypedIntegrity), when a value of it or of a type reaching it can be + * held as a live untyped reference. */ + +type RecordType = Extract; +type RecordWrite = Extract; +type FreezeFn = "record.freeze" | "array.freeze" | "collection.preventExtensions"; + +/** Object.freeze of a record or an array, or Object.freeze, seal or + * preventExtensions of a Map or Set: `fn` sets the value's bit, and the + * call answers the value. */ +export function lowerNativeFreeze( + lowerer: Lowerer, + fn: FreezeFn, + target: IrExpr, + loc: SrcLoc, +): IrExpr { + const type = target.type; + const slot = lowerer.declareHiddenLocal("%freezeTarget", type); + const receiver = (): IrExpr => varRef(slot.id, type, loc); + return { + kind: "seqExpr", + stmts: [ + { kind: "varDecl", localId: slot.id, init: target, loc }, + { + kind: "exprStmt", + expr: { kind: "libCall", fn, args: [receiver()], type: VOID, loc }, + loc, + }, + ], + result: receiver(), + type, + loc, + }; +} + +/** Records that the program freezes, seals or prevents extensions of the + * untyped value `node` lowers to. Unless it is a fresh object or array + * literal, the value can be any record, array or class instance the program + * holds as an untyped reference, so those types become freezable. */ +export function noteUntypedIntegrity(lowerer: Lowerer, node: ts.Expression): void { + let inner = node; + while (ts.isParenthesizedExpression(inner) || ts.isAsExpression(inner)) inner = inner.expression; + if (!ts.isObjectLiteralExpression(inner) && !ts.isArrayLiteralExpression(inner)) + lowerer.untypedIntegrity = true; +} + +function isRecordWrite(s: IrStmt): s is RecordWrite { + return s.kind === "recordSet" || s.kind === "recordKeySet" || s.kind === "recordKeyDelete"; +} + +/** The key of a record write's source range: strict mode is lexical, so + * every write lowered from the same range shares its mode. */ +function writeKey(loc: SrcLoc): string { + return `${loc.file}\0${loc.start}\0${loc.end}`; +} + +/** Records the record writes of `stmt`, lowered from `node`, as sloppy-mode + * writes when `node` is sloppy-mode code: a write to a frozen record then + * leaves it unchanged instead of throwing. `deletes` says `stmt` is the + * absent-state write of `delete r.f`, which Node refuses only while the + * field is present. */ +export function markSloppyRecordWrites( + lowerer: Lowerer, + stmt: IrStmt, + node: ts.Node, + deletes = false, +): IrStmt { + if (deletes) lowerer.recordFieldDeletes.add(writeKey(stmt.loc)); + if (isStrictCode(node)) return stmt; + everyStmtList([stmt], { + stmt: (s) => { + if (s.kind === "recordSet" || s.kind === "recordKeySet" || s.kind === "recordKeyDelete") + lowerer.sloppyRecordWrites.add(writeKey(s.loc)); + return true; + }, + expr: () => true, + }); + return stmt; +} + +/** Whether a field write's value can be an optional field's absent state, + * which only a field write itself can hold. */ +function holdsAbsentState(e: IrExpr): boolean { + if (e.kind === "fieldAbsent") return true; + return e.kind === "ternary" && (holdsAbsentState(e.then) || holdsAbsentState(e.else_)); +} + +/** The record shapes and array types (typeKeys) whose values the program + * may freeze. */ +function freezableTypes( + lowerer: Lowerer, + functions: IrFunction[], +): { shapes: Set; arrays: Set } { + const shapes = new Set(); + const arrays = new Set(); + const live: IrType[] = []; + for (const fn of functions) + everyStmtList(fn.body, { + expr: (e) => { + if (e.kind === "libCall" && e.fn === "record.freeze") { + const target = e.args[0]; + if (target && target.type.kind === "record") shapes.add(target.type.shapeId); + } else if (e.kind === "libCall" && e.fn === "array.freeze") { + const target = e.args[0]; + if (target && target.type.kind === "array") arrays.add(typeKey(target.type)); + } else if (e.kind === "dynFrom") live.push(e.value.type); + return true; + }, + stmt: () => true, + }); + if (!lowerer.untypedIntegrity) return { shapes, arrays }; + // An untyped reference to a class instance, or to a record or array + // boxed live, reaches the values its fields and elements hold as further + // live references. Class instances also cross into untyped code through + // compiled callbacks, so every class is a root. + const stack = [...live]; + for (const info of lowerer.classes.values()) + for (const field of info.def.fields) stack.push(field.type); + const seen = new Set(); + while (stack.length > 0) { + const type = stack.pop()!; + switch (type.kind) { + case "record": { + if (seen.has(`record:${type.shapeId}`)) break; + seen.add(`record:${type.shapeId}`); + shapes.add(type.shapeId); + const shape = lowerer.shapes.get(type.shapeId); + for (const field of shape?.fields ?? []) stack.push(field.type); + if (shape?.indexValue) stack.push(shape.indexValue); + break; + } + case "union": + if (seen.has(`union:${type.unionId}`)) break; + seen.add(`union:${type.unionId}`); + for (const arm of lowerer.unions.get(type.unionId)?.arms ?? []) stack.push(arm); + break; + case "array": { + const key = typeKey(type); + if (seen.has(key)) break; + seen.add(key); + arrays.add(key); + stack.push(type.elem); + break; + } + case "set": + stack.push(type.elem); + break; + case "map": + stack.push(type.key, type.value); + break; + case "promise": + stack.push(type.inner); + break; + case "generator": + stack.push(type.yieldT, type.retT, type.nextT); + break; + case "func": + for (const param of type.params) stack.push(param); + stack.push(type.ret); + break; + default: + break; + } + } + return { shapes, arrays }; +} + +/** The guarded form of a write to a freezable record: the receiver and the + * operands are evaluated first, in source order, then the frozen bit decides + * between the write and `reject`. */ +function guardedWrite( + receiver: IrExpr, + decls: IrStmt[], + reject: IrStmt[], + store: IrStmt, + loc: SrcLoc, +): IrStmt { + return { + kind: "block", + body: [ + ...decls, + { + kind: "if", + cond: { kind: "libCall", fn: "record.isFrozen", args: [receiver], type: BOOL, loc }, + then: reject, + else_: [store], + loc, + }, + ], + loc, + }; +} + +/** Checks the frozen bit before every write to a record of a freezable + * shape, and lists the freezable array types, whose writes the backend + * guards (IrModule.freezableArrays). A write to a frozen record throws + * Node's TypeError in strict mode code; in sloppy-mode code it leaves the + * record unchanged. Deleting a key the record does not have succeeds either + * way. */ +export function guardFrozenWrites(lowerer: Lowerer, functions: IrFunction[]): void { + const { shapes: freezable, arrays } = freezableTypes(lowerer, functions); + for (const id of freezable) { + const shape = lowerer.shapes.get(id); + if (shape) shape.freezable = true; + } + for (const key of arrays) lowerer.freezableArrays.add(key); + if (freezable.size === 0) return; + const lifted = lowerer.liftedFns.length; + for (const fn of functions) { + const local = (type: IrType, loc: SrcLoc): Extract => { + const id = `%frozenWrite.${fn.locals.length}`; + fn.locals.push({ id, name: id, type, mutable: false }); + return { kind: "varRef", localId: id, type, loc }; + }; + const guarded = new Set(); + fn.body = transformStmtList(fn.body, { + stmt: (statement) => { + if (!isRecordWrite(statement) || guarded.has(statement)) return statement; + if (!freezable.has(statement.shapeId)) return statement; + const sloppy = lowerer.sloppyRecordWrites.has(writeKey(statement.loc)); + const deletes = + statement.kind === "recordSet" && lowerer.recordFieldDeletes.has(writeKey(statement.loc)); + const loc = statement.loc; + const shapeId = statement.shapeId; + const receiver = local(statement.obj.type, loc); + const decls: IrStmt[] = [ + { kind: "varDecl", localId: receiver.localId, init: statement.obj, loc }, + ]; + const owner = strLit( + lowerer.shapes.get(shapeId)?.tuple ? "[object Array]" : "#", + loc, + ); + const thrown = (key: IrExpr, operation: number): IrStmt => ({ + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "record.throwFrozen", + args: [key, owner, numLit(operation, loc)], + type: VOID, + loc, + }, + loc, + }); + if (statement.kind === "recordSet") { + let value = statement.value; + if (!holdsAbsentState(value)) { + const slot = local(value.type, loc); + decls.push({ kind: "varDecl", localId: slot.localId, init: value, loc }); + value = slot; + } + const store: IrStmt = { + kind: "recordSet", + obj: receiver, + shapeId, + field: statement.field, + value, + loc, + }; + guarded.add(store); + const field = strLit(statement.field, loc); + // `delete r.f` of an optional field writes its absent state: Node + // only refuses it when the field is present. + const reject: IrStmt[] = sloppy + ? [] + : deletes + ? [ + { + kind: "if", + cond: lowerer.recordFieldPresent(receiver, shapeId, statement.field, loc), + then: [thrown(field, 2)], + else_: null, + loc, + }, + ] + : [thrown(field, 0)]; + return guardedWrite(receiver, decls, reject, store, loc); + } + const key = local(statement.key.type, loc); + decls.push({ kind: "varDecl", localId: key.localId, init: statement.key, loc }); + const named = key.type.kind === "string"; + const present: IrExpr = named + ? { + kind: "call", + callee: recordHasOwnHelper(lowerer, shapeId, loc), + args: [receiver, key], + type: BOOL, + loc, + } + : { kind: "boolLit", value: true, type: BOOL, loc }; + const text: IrExpr = named ? key : strLit("[key]", loc); + if (statement.kind === "recordKeySet") { + const value = local(statement.value.type, loc); + decls.push({ kind: "varDecl", localId: value.localId, init: statement.value, loc }); + const store: IrStmt = { + kind: "recordKeySet", + obj: receiver, + shapeId, + key, + value, + ...(statement.overflowOnly ? { overflowOnly: true as const } : {}), + loc, + }; + guarded.add(store); + const reject: IrStmt[] = sloppy + ? [] + : [ + { + kind: "if", + cond: present, + then: [thrown(text, 0)], + else_: [thrown(text, 1)], + loc, + }, + ]; + return guardedWrite(receiver, decls, reject, store, loc); + } + const store: IrStmt = { kind: "recordKeyDelete", obj: receiver, shapeId, key, loc }; + guarded.add(store); + const reject: IrStmt[] = sloppy + ? [] + : [{ kind: "if", cond: present, then: [thrown(text, 2)], else_: null, loc }]; + return guardedWrite(receiver, decls, reject, store, loc); + }, + expr: (expression) => expression, + }); + } + // The key-presence helpers the guards call. + for (const helper of lowerer.liftedFns.slice(lifted)) functions.push(helper); +} + +/** Whether a statically typed Object.freeze can freeze a value of `type`: + * a record whose shape has no accessor properties. */ +export function freezableRecordType(lowerer: Lowerer, type: IrType): type is RecordType { + if (type.kind !== "record") return false; + const shape = lowerer.shapes.get(type.shapeId); + return shape !== undefined && !shapeHasAccessorSlots(shape); +} diff --git a/packages/compiler/src/frontend/lowering/function-context.ts b/packages/compiler/src/frontend/lowering/function-context.ts index 92042b7d7..702574b63 100644 --- a/packages/compiler/src/frontend/lowering/function-context.ts +++ b/packages/compiler/src/frontend/lowering/function-context.ts @@ -46,6 +46,9 @@ export interface FnCtx { * rest param): the synthetic trailing dyn-array param `arguments` * reads resolve to. */ argumentsLocal?: IrLocal | null; + /** The count `arguments.length` reads in a top-level function declaration + * that is also used as a value (argument-counts.ts). */ + argumentCount?: IrLocal; /** Arrows inherit the nearest ordinary function's arguments binding. */ inheritsArguments?: boolean; /** A function lowered on demand on top of an unrelated caller's frames diff --git a/packages/compiler/src/frontend/lowering/function-returns.ts b/packages/compiler/src/frontend/lowering/function-returns.ts index dde109ea7..c692742a8 100644 --- a/packages/compiler/src/frontend/lowering/function-returns.ts +++ b/packages/compiler/src/frontend/lowering/function-returns.ts @@ -11,12 +11,15 @@ import { VOID, isUnitType, typeEquals, + typeKey, } from "../../ir/ir.js"; import { isJsSourceFile } from "../program.js"; import { PoisonError, dynFallbackType } from "./lowerer.js"; import { functionCanReturnUndefined } from "../function-completion.js"; import { jsBindingAppendsOnly, jsBindingHasOpenWrites } from "./lower-stmts.js"; import type { FnCtx } from "./function-context.js"; +import { constituentTypes } from "../ts7/checker.js"; +import { returnsGuessedOverloadValue } from "./overload-choice.js"; /** Keep the conservative completion rule shared by return construction * and inference. Nested control flow may still make completion unreachable. */ @@ -174,7 +177,26 @@ export function declaredReturnType( // `() => void`), and throw-only callbacks are ordinary code // (`.action(() => { throw ... })`). `never` VALUES stay unmapped. if (retTsType.flags & ts.TypeFlags.Never) return VOID; + // A JS generator whose result the checker cannot infer (mutually + // recursive methods returning `yield*` of each other, as in yaml's + // Parser) has the return type `any`: a generator over untyped values. + if ( + isJsSourceFile(ts.getSourceFileOfNode(decl)) && + decl.asteriskToken && + retTsType.flags & ts.TypeFlags.Any + ) + return { + kind: "generator", + ...(decl.modifiers?.nodes.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword) + ? { async: true as const } + : {}), + yieldT: DYN, + retT: DYN, + nextT: DYN, + }; let mappedReturn = lowerer.mapTypeOf(retTsType); + if (mappedReturn !== null) + mappedReturn = widenReducedReturn(lowerer, decl, retTsType, mappedReturn); // A generator declared to return Iterable returns a generator over T. if (mappedReturn && decl.asteriskToken) mappedReturn = iterableGeneratorArm(lowerer, mappedReturn); @@ -256,6 +278,22 @@ export function declaredReturnType( return decl.modifiers?.nodes.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword) ? { kind: "promise", inner: DYN } : DYN; + // A JS function returning a value whose type rests on an undecided + // overload (guessedOverloadValue): the inferred result is the first + // overload's, which the returned value need not have. + if ( + isJsSourceFile(ts.getSourceFileOfNode(decl)) && + !decl.type && + !decl.asteriskToken && + !hasExplicitJsDocReturn(decl) && + mappedReturn !== null && + mappedReturn.kind !== "dyn" && + !(mappedReturn.kind === "promise" && mappedReturn.inner.kind === "dyn") && + returnsGuessedOverloadValue(lowerer, decl) + ) + return decl.modifiers?.nodes.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword) + ? { kind: "promise", inner: DYN } + : DYN; // JS inference assigns Number to arithmetic over any, although the // runtime operands can both be BigInts. Preserve that native result. if ( @@ -522,3 +560,194 @@ export function blockBodyOf(decl: ts.FunctionLikeDeclaration): ts.Block | null { if (ts.isBlock(body)) return body; return ts.forEachChild(decl, (c) => (ts.isBlock(c) ? c : undefined)) ?? null; } + +const widening = new Set(); +/** How many times widenReducedReturn answered a declaration it was still + * widening with its unwidened type. A result computed while that happened + * may lack arms, so it is not kept. */ +let wideningCuts = 0; + +/** An inferred return type is the checker's SUBTYPE-REDUCED union of the + * returned values' types: `return aborted` (`{ status }`) beside `return + * { status, value }` infers just `{ status }`, because the second is a + * subtype of the first. Records have fixed layouts, so that type cannot + * hold the second value: the return would drop `value` or fail the union + * wrap. A function whose return type is inferred therefore returns the + * union of the record types it actually returns. Declared return types keep + * their meaning. */ +function widenReducedReturn( + lowerer: Lowerer, + decl: ts.SignatureDeclaration, + retTsType: ts.Type, + mapped: IrType, +): IrType { + if (decl.type !== undefined || hasExplicitJsDocReturn(decl)) return mapped; + const body = (decl as { body?: ts.Node }).body; + if (body === undefined || decl.asteriskToken !== undefined) return mapped; + if (decl.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword)) return mapped; + // A function expression with a contextual signature returns that + // signature's type; only free-standing inference is widened. + if ( + (ts.isFunctionExpression(decl) || ts.isArrowFunction(decl)) && + lowerer.checker.getContextualType(decl) !== undefined + ) + return mapped; + const arms = + mapped.kind === "union" ? (lowerer.unions.get(mapped.unionId)?.arms ?? []) : [mapped]; + const recordArms = arms.filter((a) => a.kind === "record"); + if (recordArms.length === 0) return mapped; + if (widening.has(decl)) { + wideningCuts++; + return mapped; + } + // Functions that forward to the same callees along several return + // branches read each callee's widened type once. A nested function + // lowered under other type-parameter bindings maps its returns again. + const known = lowerer.coercions.widenedReturnTypes.get(decl)?.get(retTsType); + const erasing = lowerer.erasingDepth > 0; + if ( + known !== undefined && + known.bindings === lowerer.typeParamBindings && + known.tsBindings === lowerer.typeParamTsBindings && + known.implicit === lowerer.implicitParamTypes && + known.erasing === erasing + ) + return known.type; + const cutsBefore = wideningCuts; + const widened = widenReturnArms(lowerer, decl, retTsType, mapped, arms, recordArms); + if (wideningCuts === cutsBefore) { + let byType = lowerer.coercions.widenedReturnTypes.get(decl); + if (byType === undefined) lowerer.coercions.widenedReturnTypes.set(decl, (byType = new Map())); + byType.set(retTsType, { + type: widened, + bindings: lowerer.typeParamBindings, + tsBindings: lowerer.typeParamTsBindings, + implicit: lowerer.implicitParamTypes, + erasing, + }); + } + return widened; +} + +/** widenReducedReturn's scan of the returned values, for a declaration not + * already being widened. */ +function widenReturnArms( + lowerer: Lowerer, + decl: ts.SignatureDeclaration, + retTsType: ts.Type, + mapped: IrType, + arms: readonly IrType[], + recordArms: readonly IrType[], +): IrType { + const body = (decl as { body?: ts.Node }).body!; + const checkerArms = new Set( + retTsType.flags & ts.TypeFlags.Union ? constituentTypes(retTsType) : [retTsType], + ); + const fieldsOf = (t: IrType): readonly string[] | null => { + if (t.kind !== "record") return null; + const shape = lowerer.shapes.get(t.shapeId); + return shape === undefined || shape.tuple || shape.indexValue !== undefined + ? null + : shape.fields.map((f) => f.name); + }; + const narrower = (part: IrType): boolean => { + const own = fieldsOf(part); + if (own === null || arms.some((a) => typeKey(a) === typeKey(part))) return false; + return recordArms.some((arm) => { + const fields = fieldsOf(arm); + return fields !== null && fields.length < own.length && fields.every((f) => own.includes(f)); + }); + }; + const extra = new Map(); + const returned = (expr: ts.Expression): void => { + // A call of another inferred function returns ITS widened type. + const callee = ts.isCallExpression(expr) + ? lowerer.checker.getResolvedSignature(expr)?.declaration + : undefined; + if ( + callee !== undefined && + ts.isFunctionLike(callee) && + (callee as { body?: ts.Node }).body !== undefined && + callee.type === undefined + ) { + // A call of the function itself returns its own type, whose arms + // add nothing. Reading it would cut the widening and keep this + // result from being reused. + if (callee === decl) return; + let t: IrType | null = null; + widening.add(decl); + try { + t = declaredReturnType(lowerer, callee, expr); + } catch { + t = null; + } finally { + widening.delete(decl); + } + if (t === null) return; + const parts = t.kind === "union" ? (lowerer.unions.get(t.unionId)?.arms ?? []) : [t]; + for (const part of parts) if (narrower(part)) extra.set(typeKey(part), part); + return; + } + // Only a value type the checker's reduction dropped from the inferred + // union is a candidate; a constituent of the inferred type is already + // represented by it. + const valueType = lowerer.typeOf(expr); + const valueArms = + valueType.flags & ts.TypeFlags.Union ? constituentTypes(valueType) : [valueType]; + for (const part of valueArms) { + if (checkerArms.has(part) || !(part.flags & ts.TypeFlags.Object)) continue; + if (!lowerer.checker.isTypeAssignableTo(part, retTsType)) continue; + const t = lowerer.mapTypeOf(part); + if (t !== null && narrower(t)) extra.set(typeKey(t), t); + } + }; + const scan = (node: ts.Node): void => { + if (node !== decl && ts.isFunctionLike(node)) return; + if (ts.isReturnStatement(node) && node.expression !== undefined) returned(node.expression); + ts.forEachChild(node, scan); + }; + if (ts.isArrowFunction(decl) && !ts.isBlock(body)) returned(body as ts.Expression); + else scan(body); + if (extra.size === 0) return mapped; + const joined = new Map(); + for (const a of [...arms, ...extra.values()]) joined.set(typeKey(a), a); + const sorted = [...joined.values()].sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1)); + const unionId = lowerer.unions.intern(sorted); + lowerer.coercions.widenedReturns.add(unionId); + return { kind: "union", unionId }; +} + +/** Whether `wide` is a widened inferred return that holds `narrow`'s arms plus records that are width + * subtypes of `narrow`'s record arms: the shape a subtype-reduced inferred + * type takes next to the values it was inferred from. */ +export function widensReducedRecords(lowerer: Lowerer, narrow: IrType, wide: IrType): boolean { + if (wide.kind !== "union" || typeKey(narrow) === typeKey(wide)) return false; + if (!lowerer.coercions.widenedReturns.has(wide.unionId)) return false; + const narrowArms = + narrow.kind === "union" ? (lowerer.unions.get(narrow.unionId)?.arms ?? []) : [narrow]; + const wideArms = lowerer.unions.get(wide.unionId)?.arms ?? []; + const keys = new Set(wideArms.map(typeKey)); + if (!narrowArms.every((a) => keys.has(typeKey(a)))) return false; + const fieldsOf = (t: IrType): readonly string[] | null => { + if (t.kind !== "record") return null; + const shape = lowerer.shapes.get(t.shapeId); + return shape === undefined || shape.tuple || shape.indexValue !== undefined + ? null + : shape.fields.map((f) => f.name); + }; + const narrowKeys = new Set(narrowArms.map(typeKey)); + const extras = wideArms.filter((a) => !narrowKeys.has(typeKey(a))); + return ( + extras.length > 0 && + extras.every((extra) => { + const own = fieldsOf(extra); + return ( + own !== null && + narrowArms.some((arm) => { + const f = fieldsOf(arm); + return f !== null && f.length < own.length && f.every((x) => own.includes(x)); + }) + ); + }) + ); +} diff --git a/packages/compiler/src/frontend/lowering/generator-delegation.ts b/packages/compiler/src/frontend/lowering/generator-delegation.ts index a606c177e..81ecc8353 100644 --- a/packages/compiler/src/frontend/lowering/generator-delegation.ts +++ b/packages/compiler/src/frontend/lowering/generator-delegation.ts @@ -577,10 +577,11 @@ export function lowerBuiltinDelegation( loc, }); } else if (sourceT.kind === "string") { + // A byte cursor over the WTF-8 data, as for-of over a string walks. const ch = lowerer.declareHiddenLocal("%delegateElement", STRING); const length = (receiver: IrExpr): IrExpr => ({ kind: "strIntrinsic", - method: "length", + method: "byteLength", receiver, args: [], type: F64, @@ -595,7 +596,7 @@ export function lowerBuiltinDelegation( localId: ch.id, init: { kind: "strIntrinsic", - method: "cpAt", + method: "cpAtByte", receiver: src(), args: [at()], type: STRING, diff --git a/packages/compiler/src/frontend/lowering/generic-functions.ts b/packages/compiler/src/frontend/lowering/generic-functions.ts index 222cb89f9..647ca52a1 100644 --- a/packages/compiler/src/frontend/lowering/generic-functions.ts +++ b/packages/compiler/src/frontend/lowering/generic-functions.ts @@ -1,3 +1,5 @@ +import { shapeOverlayAnnotated } from "../shape-inference/index.js"; +import { documentedParameter } from "./call-signatures.js"; import { staticNameBindings } from "./static-function-names.js"; import { positionTag } from "./structural-positions.js"; import { keyedHelperName } from "./stable-names.js"; @@ -645,8 +647,7 @@ function extendsParameter( info: GenericFnInfo, ): boolean { const params = new Set(); - const fn = info.decl as { parameters?: { nodes: readonly ts.ParameterDeclaration[] } }; - for (const param of fn.parameters?.nodes ?? []) { + for (const param of info.decl.parameters.nodes) { const symbol = ts.isIdentifier(param.name) ? lowerer.checker.getSymbolAtLocation(param.name) : undefined; @@ -672,8 +673,7 @@ function declaredInside(node: ts.Node, owner: ts.Node): boolean { * return gives anything else (or nothing). Nested functions and classes keep * their own returns. */ function returnedLocalClass(lowerer: Lowerer, info: GenericFnInfo): ts.ClassLikeDeclaration | null { - const fn = info.decl as ts.Node & { body?: ts.Node }; - const body = fn.body; + const body = info.decl.body; if (!body) return null; const classOf = (node: ts.Expression): ts.ClassLikeDeclaration | null => { let expr = node; @@ -1042,6 +1042,14 @@ function unifySignatureBindings( dAliasArgs.length && iAliasArgs.length === dAliasArgs.length ) { + if (tsBindings && declared !== inst) { + let entries = lowerer.boundAliasInstances.get(declared); + if (!entries) lowerer.boundAliasInstances.set(declared, (entries = [])); + // A body forwarding its own alias (`inner(g)` returning `Catalog` inside an instance) resolves through the enclosing one. + if (!entries.some((entry) => entry.tsBindings === tsBindings)) + entries.push({ tsBindings, instance: lowerer.boundAliasTsResolver(inst) ?? inst }); + } dAliasArgs.forEach((da, i) => { const ia = iAliasArgs[i]; if (ia) unify(da, ia, depth + 1); @@ -1857,10 +1865,14 @@ export function implicitAnyParamSymbolsOf( // The variadic-`arguments` form keeps its dynRest story whole. if (bodyReadsArguments(decl)) return null; let hasImplicitParams = false; + const overlayTyped = shapeOverlayAnnotated(decl); const out = decl.parameters.nodes.map((param): ts.Symbol | null => { if (!ts.isIdentifier(param.name)) return null; if (param.dotDotDotToken || param.questionToken || param.initializer) return null; if (param.name.text === "this") return null; + // A parameter typed by shape inference's overlay is a proven type, + // not an implicit type parameter. + if (overlayTyped && documentedParameter(param)) return null; const t = lowerer.typeOf(param.name); const broadFunction = lowerer.checker.typeToString(t) === "Function"; const broadArray = lowerer.checkerAnyArrayType(t); @@ -2084,6 +2096,10 @@ function storedImplicitArgumentType(lowerer: Lowerer, arg: ts.Expression): IrTyp isJsSourceFile(ts.getSourceFileOfNode(arg)) && (ts.isArrowFunction(arg) || ts.isFunctionExpression(arg)) ) { + // A closure lowered here before (another call site's instance key, or + // a repeated demand) keeps its value for the argument's own lowering. + const known = lowerer.speculatedClosure(arg); + if (known !== null) return known.type; const saved = lowerer.diagSink; const diagnostics: typeof lowerer.diags = []; lowerer.diagSink = diagnostics; @@ -2094,8 +2110,17 @@ function storedImplicitArgumentType(lowerer: Lowerer, arg: ts.Expression): IrTyp lowerer.diagSink = saved; } // Direct inferred helpers can receive native callbacks even when their - // result is an opaque handle that cannot cross checked storage. - if (diagnostics.length === 0 && value?.type.kind === "func") return value.type; + // result is an opaque handle that cannot cross checked storage. The + // argument's own lowering then takes this value instead of lowering + // the closure a second time. + if (diagnostics.length === 0 && value?.type.kind === "func") { + lowerer.speculatedClosures.set(arg, { + ctx: lowerer.ctx, + bindings: lowerer.typeParamBindings, + value, + }); + return value.type; + } } if (arg.kind === ts.SyntaxKind.ThisKeyword) return lowerer.resolveThis()?.type ?? null; if (ts.isAwaitExpression(arg)) { diff --git a/packages/compiler/src/frontend/lowering/global-values.ts b/packages/compiler/src/frontend/lowering/global-values.ts new file mode 100644 index 000000000..264c99337 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/global-values.ts @@ -0,0 +1,295 @@ +/* Builtin globals read through the global object by a name the compiler + * does not see (`globalThis[name]`, `for (name in globalThis)` followed by + * `globalThis[name]`). Such a read answers what the name's static spelling + * reads in a JavaScript source: the interned function and constructor + * values, the WebCrypto object and Buffer, and the opaque + * identity token for the other declared globals. + * + * The values come from one lookup function per program + * (`%global.builtinValue`), which a JavaScript module with such a read + * installs on the global object when its body starts + * (scr_global_values_install). The runtime answers the global object, + * console, fetch and the constants itself and calls the lookup for the + * other declared names: a value, `undefined` for the token, or `null` for + * a name without one, whose read keeps its SC2020 refusal. */ +import * as ts from "../ts7/adapter.js"; +import { + BYTES_ELEMENT_NAME, + DYN, + STRING, + UNDEFINED_T, + type IrExpr, + type IrStmt, + type IrType, + type SrcLoc, +} from "../../ir/ir.js"; +import { isJsSourceFile } from "../program.js"; +import type { Lowerer } from "./lowerer.js"; +import { TEMPORAL_GLOBAL } from "../builtin-modules.js"; +import { stdlibGlobalNameOf } from "./surfaces.js"; +import { builtinGlobalValue } from "./expressions/property-access.js"; +import { builtinNamespaceValue } from "./builtin-namespaces.js"; +import { uriFunctionValue } from "./expressions/identifiers.js"; +import { navigatorValue } from "./builtins/navigator.js"; +import { primitiveCtorClosure } from "./lower-exprs.js"; + +const LOOKUP_FN = "%global.builtinValue"; + +/** Names the runtime answers before the lookup runs. */ +const RUNTIME_ANSWERED = new Set([ + "globalThis", + "global", + "console", + "fetch", + "undefined", + "NaN", + "Infinity", +]); + +/** Declared globals whose reads through the global object stay refused: + * WebAssembly's namespace links the translated modules a program that + * names it statically carries. */ +const REFUSED = new Set(["WebAssembly"]); + +/** The receiver of the first element read of the global object + * (`globalThis[name]`, `globalThis["name"]`), or of the first for-in over + * it, in the JavaScript source `sf`; null when it has none. */ +export function computedGlobalReadOf(lowerer: Lowerer, sf: ts.SourceFile): ts.Expression | null { + if (!isJsSourceFile(sf)) return null; + const cached = lowerer.computedGlobalReads.get(sf); + if (cached !== undefined) return cached; + let found: ts.Expression | null = null; + // An iterative walk: sources can nest expressions thousands deep. + ts.walkPreorder(sf, (node) => { + if ( + (ts.isElementAccessExpression(node) || ts.isForInStatement(node)) && + isGlobalObjectRead(lowerer, node.expression) && + (ts.isForInStatement(node) || readsBuiltinByName(lowerer, node)) + ) { + found = node.expression; + return "stop"; + } + return undefined; + }); + lowerer.computedGlobalReads.set(sf, found); + return found; +} + +/** Whether `expr` reads the global object: a spelling of it, or a variable + * whose only declaration initializes it with one (`const g = globalThis`). + * The scan runs before function bodies are lowered, so the alias of a + * variable declared in a function is not registered yet. */ +function isGlobalObjectRead(lowerer: Lowerer, expr: ts.Expression): boolean { + if (stdlibGlobalNameOf(lowerer, expr) === "globalThis") return true; + let inner = expr; + while (ts.isParenthesizedExpression(inner)) inner = inner.expression; + if (!ts.isIdentifier(inner)) return false; + const declarations = lowerer.checker.getSymbolAtLocation(inner)?.declarations ?? []; + const declaration = declarations[0]; + return ( + declarations.length === 1 && + declaration !== undefined && + ts.isVariableDeclaration(declaration) && + ts.isIdentifier(declaration.name) && + declaration.initializer !== undefined && + stdlibGlobalNameOf(lowerer, declaration.initializer) === "globalThis" + ); +} + +/** Whether the element access `node` of the global object can read a + * builtin global by name: its key is not a symbol (`globalThis[key]` with + * `key = Symbol.for(...)` reads the global object's symbol properties), + * and it is neither the target of `=` nor the operand of `delete`. */ +function readsBuiltinByName(lowerer: Lowerer, node: ts.ElementAccessExpression): boolean { + if (lowerer.typeOf(node.argumentExpression).flags & ts.TypeFlags.ESSymbolLike) return false; + let outer: ts.Node = node; + while (ts.isParenthesizedExpression(outer.parent)) outer = outer.parent; + const parent = outer.parent; + if (ts.isDeleteExpression(parent)) return false; + return !( + ts.isBinaryExpression(parent) && + parent.left === outer && + parent.operatorToken.kind === ts.SyntaxKind.EqualsToken + ); +} + +/** The value a JavaScript source reads for the declared global `name` + * taken as a value, whatever its spelling (`name`, `globalThis.name`, + * `const { name } = globalThis`, `globalThis[name]`), or null when it + * reads the opaque identity token. */ +export function jsGlobalValueByName(lowerer: Lowerer, name: string, loc: SrcLoc): IrExpr | null { + return ( + jsSpecialGlobalValue(lowerer, name, loc) ?? + builtinNamespaceValue(lowerer, name, loc) ?? + builtinGlobalValue(name, loc, true) + ); +} + +/** jsGlobalValueByName for the globals other than the interned builtin + * functions and constructors (builtinGlobalValue). */ +export function jsSpecialGlobalValue(lowerer: Lowerer, name: string, loc: SrcLoc): IrExpr | null { + if (name === "String" || name === "Number" || name === "Boolean" || name === "BigInt") + return primitiveCtorClosure(lowerer, name, loc, true); + if (name === "ArrayBuffer") + return { kind: "libCall", fn: "arrayBuffer.constructor", args: [], type: DYN, loc }; + if (name === "Array") + return { kind: "libCall", fn: "dyn.arrayConstructor", args: [], type: DYN, loc }; + if ((Object.values(BYTES_ELEMENT_NAME) as string[]).includes(name)) + return { + kind: "libCall", + fn: "bytes.constructor", + args: [{ kind: "strLit", value: name, type: STRING, loc }], + type: DYN, + loc, + }; + if (URI_FUNCTIONS.has(name)) return uriFunctionValue(lowerer, name, loc); + if (name === "crypto") + return { kind: "libCall", fn: "webcrypto.object", args: [], type: DYN, loc }; + if (name === "navigator") return navigatorValue(loc); + if (name === "Buffer") + return { kind: "libCall", fn: "buffer.constructor", args: [], type: DYN, loc }; + if (name === "fetch") return { kind: "libCall", fn: "fetch.function", args: [], type: DYN, loc }; + if (name === "console") + return { kind: "libCall", fn: "console.native", args: [], type: DYN, loc }; + return null; +} + +const URI_FUNCTIONS = new Set([ + "encodeURIComponent", + "encodeURI", + "decodeURI", + "decodeURIComponent", +]); + +/** The statement that installs the program's lookup, built on first use + * from the names the checker declares on `receiver` (a global object). */ +export function globalValuesInstall( + lowerer: Lowerer, + receiver: ts.Expression, + loc: SrcLoc, +): IrStmt { + const fnType: IrType & { kind: "func" } = { kind: "func", params: [STRING], ret: DYN }; + if (!lowerer.liftedFnNamed(LOOKUP_FN)) { + const names = lowerer.checker + .getPropertiesOfType(lowerer.typeOf(receiver)) + .map((property) => ts.symbolName(property)) + .filter((name) => name !== TEMPORAL_GLOBAL && !RUNTIME_ANSWERED.has(name)); + const name: IrExpr = { kind: "varRef", localId: "name.0", type: STRING, loc }; + const body: IrStmt[] = []; + for (const global of [...new Set(names)].sort()) { + const value: IrExpr | null = REFUSED.has(global) + ? { kind: "unitLit", unit: "null", type: { kind: "nullT" }, loc } + : jsGlobalValueByName(lowerer, global, loc); + if (value === null) continue; + body.push({ + kind: "if", + cond: { + kind: "strEq", + negated: false, + left: name, + right: { kind: "strLit", value: global, type: STRING, loc }, + type: { kind: "bool" }, + loc, + }, + then: [{ kind: "return", value: lowerer.coerceToExpected(value, DYN), loc }], + else_: null, + loc, + }); + } + body.push({ + kind: "return", + value: lowerer.coerceToExpected( + { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc }, + DYN, + ), + loc, + }); + lowerer.liftedFns.push({ + name: LOOKUP_FN, + params: [{ localId: "name.0", name: "name", type: STRING }], + returnType: DYN, + locals: [{ id: "name.0", name: "name", type: STRING, mutable: false }], + body, + loc, + }); + } + const lookup = lowerer.coerceToExpected( + { kind: "closure", fnName: LOOKUP_FN, captures: [], type: fnType, loc }, + DYN, + ); + return { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "global.installValues", + args: [lookup], + type: { kind: "void" }, + loc, + }, + loc, + }; +} + +/** Builtin module members that are the global objects of the same name in + * Node 24 (`require("buffer").atob === globalThis.atob`). */ +const GLOBAL_MEMBERS: Readonly>> = { + buffer: new Set(["atob", "btoa", "Blob", "File", "Buffer"]), + timers: new Set([ + "setTimeout", + "setInterval", + "setImmediate", + "clearTimeout", + "clearInterval", + "clearImmediate", + ]), + perf_hooks: new Set([ + "performance", + "PerformanceEntry", + "PerformanceMark", + "PerformanceMeasure", + "PerformanceObserver", + "PerformanceObserverEntryList", + "PerformanceResourceTiming", + ]), + util: new Set(["TextEncoder", "TextDecoder"]), + url: new Set(["URL", "URLSearchParams"]), + worker_threads: new Set(["BroadcastChannel", "MessageChannel", "MessagePort"]), + "stream/web": new Set([ + "ReadableStream", + "ReadableStreamDefaultReader", + "ReadableStreamBYOBReader", + "ReadableStreamBYOBRequest", + "ReadableByteStreamController", + "ReadableStreamDefaultController", + "TransformStream", + "TransformStreamDefaultController", + "WritableStream", + "WritableStreamDefaultWriter", + "WritableStreamDefaultController", + "ByteLengthQueuingStrategy", + "CountQueuingStrategy", + "TextEncoderStream", + "TextDecoderStream", + "CompressionStream", + "DecompressionStream", + ]), +}; + +/** In a JavaScript source, the value of a builtin module member that is a + * global object (GLOBAL_MEMBERS): what the global's own spelling reads, + * so identity holds across the spellings. Null for other members. */ +export function globalMemberValue( + lowerer: Lowerer, + bi: { module: string; member: string }, + loc: SrcLoc, +): IrExpr | null { + if (!GLOBAL_MEMBERS[bi.module]?.has(bi.member)) return null; + return ( + jsGlobalValueByName(lowerer, bi.member, loc) ?? { + kind: "strLit", + value: `[builtin ${bi.member}]`, + type: STRING, + loc, + } + ); +} diff --git a/packages/compiler/src/frontend/lowering/import-meta.ts b/packages/compiler/src/frontend/lowering/import-meta.ts index 796e64b69..ccc86bfba 100644 --- a/packages/compiler/src/frontend/lowering/import-meta.ts +++ b/packages/compiler/src/frontend/lowering/import-meta.ts @@ -17,14 +17,19 @@ export function isImportMeta(expr: ts.Expression): boolean { ); } +/** The module's file name as the compiled program reports it + * (import.meta.filename): the WASI guest path on wasi targets. */ +export function moduleFileName(lowerer: Lowerer, sf: ts.SourceFile): string { + return lowerer.targetPlatform === "wasi" + ? (wasiGuestPath(sf.fileName) ?? sf.fileName.replace(/\\/g, "/")) + : sf.fileName; +} + /** File-backed metadata is fixed by the compiled module graph. Reads share * one implementation across dot access, folded keys and selected bindings. */ export function importMetaField(lowerer: Lowerer, node: ts.Node, name: string): IrExpr { const sf = ts.getSourceFileOfNode(node); - const fileName = - lowerer.targetPlatform === "wasi" - ? (wasiGuestPath(sf.fileName) ?? sf.fileName.replace(/\\/g, "/")) - : sf.fileName; + const fileName = moduleFileName(lowerer, sf); const loc = locOf(node); switch (name) { case "env": diff --git a/packages/compiler/src/frontend/lowering/indexed-unions.ts b/packages/compiler/src/frontend/lowering/indexed-unions.ts new file mode 100644 index 000000000..774f3aef3 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/indexed-unions.ts @@ -0,0 +1,569 @@ +import { keyedHelperName } from "./stable-names.js"; +import { helperLoc, varRef } from "../../ir/build.js"; +import { + BOOL, + BYTES_ELEMENT_SIZE, + F64, + VOID, + type IrBytesIntrinsicMethod, + type IrExpr, + type IrFunction, + type IrStmt, + type IrType, + type SrcLoc, + typeKey, +} from "../../ir/ir.js"; +import { everyExprChild, everyStmtChild } from "../../ir/traverse.js"; +import { arrayNumberRead, lowerSafeBytesRead, lowerSafeIndexRead } from "./array-values.js"; +import type { Lowerer } from "./lowerer.js"; + +/** Unions of number-indexed containers: typed arrays of any element kind + * (`Uint8Array | Uint16Array | Uint32Array`, as lru-cache picks by size) + * and `number[]` arms. + * + * The union keeps its ordinary tagged representation. Each operation on it + * lowers to a lifted helper per union and operation that tests the tag, + * narrows to the arm and runs the arm's own operation: the inline load or + * store of a typed array's element kind, or the dense read or write of a + * number array. The helpers are small, so LLVM inlines them, and a loop + * over one union value tests a loop-invariant tag. */ + +/** One arm: its tag and its type (bytes, or an array of numbers). */ +export interface IndexedArm { + tag: number; + type: IrType & ({ kind: "bytes" } | { kind: "array" }); +} + +/** The arms of a union whose every arm is a typed array or a `number[]`, + * or null. Unions with any other arm (undefined included) are not + * indexed unions: their reads need a narrowing first. */ +export function indexedUnionArms(lowerer: Lowerer, type: IrType): IndexedArm[] | null { + if (type.kind !== "union") return null; + const arms = lowerer.unions.get(type.unionId)?.arms; + if (!arms || arms.length < 2) return null; + const result: IndexedArm[] = []; + for (const [tag, arm] of arms.entries()) { + if (arm.kind === "bytes") result.push({ tag, type: arm }); + else if (arm.kind === "array" && arm.elem.kind === "f64") result.push({ tag, type: arm }); + else return null; + } + return result; +} + +/** Whether every arm is a typed array (so typed-array-only members such + * as `byteLength` apply). */ +export function allBytesArms(arms: readonly IndexedArm[]): boolean { + return arms.every((arm) => arm.type.kind === "bytes"); +} + +type ArmBody = (receiver: IrExpr, arm: IndexedArm, args: readonly IrExpr[]) => IrStmt[]; + +/** The helper `name(u, ...args)`: for each arm but the last, a tag test + * that runs the arm's statements; the last arm's statements run without a + * test (the union holds one of its arms). */ +function dispatchHelper( + lowerer: Lowerer, + key: string, + prefix: string, + union: IrType, + arms: readonly IndexedArm[], + params: readonly { name: string; type: IrType }[], + returnType: IrType, + body: ArmBody, + loc: SrcLoc, +): string { + const existing = lowerer.arrHofHelpers.get(key); + if (existing) return existing; + const name = keyedHelperName(lowerer, prefix, key); + lowerer.arrHofHelpers.set(key, name); + const at = helperLoc(loc); + if (union.kind !== "union") throw new Error("indexed-union helper over a non-union"); + const u = varRef("u.0", union, at); + const args = params.map((p) => varRef(`${p.name}.0`, p.type, at)); + const stmts: IrStmt[] = []; + arms.forEach((arm, i) => { + const receiver: IrExpr = { + kind: "unionNarrow", + unionId: union.unionId, + tag: arm.tag, + value: u, + type: arm.type, + loc: at, + }; + const armStmts = body(receiver, arm, args); + if (i === arms.length - 1) { + stmts.push(...armStmts); + return; + } + stmts.push({ + kind: "if", + cond: { + kind: "unionIsTag", + unionId: union.unionId, + tag: arm.tag, + negated: false, + value: u, + type: BOOL, + loc: at, + }, + then: armStmts, + else_: null, + loc: at, + }); + }); + const fn: IrFunction = { + name, + params: [ + { localId: "u.0", name: "u", type: union }, + ...params.map((p) => ({ localId: `${p.name}.0`, name: p.name, type: p.type })), + ], + returnType, + locals: [ + { id: "u.0", name: "u", type: union, mutable: false }, + ...params.map((p) => ({ id: `${p.name}.0`, name: p.name, type: p.type, mutable: false })), + ], + body: stmts, + loc: at, + }; + lowerer.liftedFns.push(fn); + return name; +} + +const returning = (value: IrExpr, loc: SrcLoc): IrStmt[] => [{ kind: "return", value, loc }]; + +/** `u[i]` read as an element or undefined (an invalid index), as the + * typed-array and number-array reads answer. */ +export function lowerIndexedUnionRead( + lowerer: Lowerer, + receiver: IrExpr, + arms: readonly IndexedArm[], + index: IrExpr, + loc: SrcLoc, +): IrExpr { + const resultT = lowerer.withUndefinedArm(F64); + const key = `idxUnion:get:${unionKey(receiver.type)}`; + const name = dispatchHelper( + lowerer, + key, + "%union.idxOr", + receiver.type, + arms, + [{ name: "i", type: F64 }], + resultT, + (arm, info, [i]) => + returning( + info.type.kind === "bytes" + ? lowerSafeBytesRead(lowerer, arm, i!, loc) + : lowerSafeIndexRead(lowerer, arm, i!, loc)!, + loc, + ), + loc, + ); + const numeric = indexedUnionNumberHelper(lowerer, receiver.type, arms, loc); + lowerer.indexedUnionNumericTwins.set(name, numeric); + return { kind: "call", callee: name, args: [receiver, index], type: resultT, loc }; +} + +/** `u[i]` read as a number: an invalid index reads as NaN, which is + * ToNumber(undefined). */ +export function lowerIndexedUnionNumberRead( + lowerer: Lowerer, + receiver: IrExpr, + arms: readonly IndexedArm[], + index: IrExpr, + loc: SrcLoc, +): IrExpr { + if (inlineDispatch(receiver, [index])) + return inlineArms(receiver, arms, F64, (arm, info) => numberRead(arm, info, index, loc), loc); + const name = indexedUnionNumberHelper(lowerer, receiver.type, arms, loc); + return { kind: "call", callee: name, args: [receiver, index], type: F64, loc }; +} + +function numberRead(arm: IrExpr, info: IndexedArm, index: IrExpr, loc: SrcLoc): IrExpr { + return info.type.kind === "bytes" + ? { + kind: "bytesIntrinsic", + method: "get", + receiver: arm, + args: [index], + type: F64, + loc, + invalidNaN: true, + } + : arrayNumberRead(arm, index, loc); +} + +function indexedUnionNumberHelper( + lowerer: Lowerer, + union: IrType, + arms: readonly IndexedArm[], + loc: SrcLoc, +): string { + return dispatchHelper( + lowerer, + `idxUnion:num:${unionKey(union)}`, + "%union.getNumber", + union, + arms, + [{ name: "i", type: F64 }], + F64, + (arm, info, [i]) => returning(numberRead(arm, info, i!, loc), loc), + loc, + ); +} + +/** The numeric twin of an indexed-union read helper call, or null. */ +export function indexedUnionNumericTwin(lowerer: Lowerer, call: IrExpr): IrExpr | null { + if (call.kind !== "call") return null; + const twin = lowerer.indexedUnionNumericTwins.get(call.callee); + return twin === undefined ? null : { ...call, callee: twin, type: F64 }; +} + +/** `u[i] = v` for a number `v`: each arm's element store (a typed array + * converts the number to its element kind and ignores an invalid index; a + * number array stores at the index). */ +export function lowerIndexedUnionWrite( + lowerer: Lowerer, + receiver: IrExpr, + arms: readonly IndexedArm[], + index: IrExpr, + value: IrExpr, + loc: SrcLoc, +): IrStmt { + // In place, the value evaluates before the arms read the receiver and + // index: only when it cannot rebind either. + if (inlineDispatch(receiver, [index]) && !rebinds(value, [receiver, index])) { + const union = receiver.type; + const valueLocal = lowerer.declareHiddenLocal("%elementValue", F64); + const v: IrExpr = { kind: "varRef", localId: valueLocal.id, type: F64, loc }; + let chain: IrStmt[] | null = null; + for (let i = arms.length - 1; i >= 0; i--) { + const info = arms[i]!; + const arm: IrExpr = { + kind: "unionNarrow", + unionId: union.kind === "union" ? union.unionId : "", + tag: info.tag, + value: receiver, + type: info.type, + loc, + }; + const store: IrStmt = + info.type.kind === "bytes" + ? { kind: "bytesSet", arr: arm, index, value: v, loc } + : { kind: "arraySet", arr: arm, index, value: v, loc }; + chain = + chain === null + ? [store] + : [ + { + kind: "if", + cond: { + kind: "unionIsTag", + unionId: union.kind === "union" ? union.unionId : "", + tag: info.tag, + negated: false, + value: receiver, + type: BOOL, + loc, + }, + then: [store], + else_: chain, + loc, + }, + ]; + } + return { + kind: "block", + body: [{ kind: "varDecl", localId: valueLocal.id, init: value, loc }, ...chain!], + loc, + }; + } + const name = dispatchHelper( + lowerer, + `idxUnion:set:${unionKey(receiver.type)}`, + "%union.idxSet", + receiver.type, + arms, + [ + { name: "i", type: F64 }, + { name: "v", type: F64 }, + ], + VOID, + (arm, info, [i, v]) => [ + info.type.kind === "bytes" + ? { kind: "bytesSet", arr: arm, index: i!, value: v!, loc } + : { kind: "arraySet", arr: arm, index: i!, value: v!, loc }, + { kind: "return", value: null, loc }, + ], + loc, + ); + return { + kind: "exprStmt", + expr: { kind: "call", callee: name, args: [receiver, index, value], type: VOID, loc }, + loc, + }; +} + +/** The members every arm answers: `length` for all, and `byteLength`, + * `byteOffset` and `BYTES_PER_ELEMENT` when every arm is a typed array. */ +export function lowerIndexedUnionMember( + lowerer: Lowerer, + receiver: IrExpr, + arms: readonly IndexedArm[], + member: string, + loc: SrcLoc, +): IrExpr | null { + const bytesOnly = member !== "length"; + if ( + member !== "length" && + member !== "byteLength" && + member !== "byteOffset" && + member !== "BYTES_PER_ELEMENT" + ) + return null; + if (bytesOnly && !allBytesArms(arms)) return null; + const memberOf = (arm: IrExpr, info: IndexedArm): IrExpr => + info.type.kind === "array" + ? { kind: "arrIntrinsic", method: "length", receiver: arm, args: [], type: F64, loc } + : member === "BYTES_PER_ELEMENT" + ? { kind: "numLit", value: BYTES_ELEMENT_SIZE[info.type.elem], type: F64, loc } + : { kind: "bytesIntrinsic", method: member, receiver: arm, args: [], type: F64, loc }; + if (inlineDispatch(receiver, [])) return inlineArms(receiver, arms, F64, memberOf, loc); + const name = dispatchHelper( + lowerer, + `idxUnion:${member}:${unionKey(receiver.type)}`, + `%union.${member}`, + receiver.type, + arms, + [], + F64, + (arm, info) => returning(memberOf(arm, info), loc), + loc, + ); + return { kind: "call", callee: name, args: [receiver], type: F64, loc }; +} + +function unionKey(type: IrType): string { + return type.kind === "union" ? String(type.unionId) : "?"; +} + +/** A typed-array method on a union whose every arm is a typed array: the + * arguments evaluate once in the caller, and each arm runs the same + * `bytesIntrinsic` on its own receiver. `result` "receiver" is a method + * that answers a typed array of the receiver's kind (subarray, slice, + * fill, ...): each arm wraps its answer back into the union. */ +export function lowerIndexedUnionMethod( + lowerer: Lowerer, + receiver: IrExpr, + arms: readonly IndexedArm[], + method: IrBytesIntrinsicMethod, + args: readonly IrExpr[], + result: "receiver" | IrType, + loc: SrcLoc, +): IrExpr { + const union = receiver.type; + if (union.kind !== "union") throw new Error("indexed-union method over a non-union"); + const resultT = result === "receiver" ? union : result; + const params = args.map((arg, i) => ({ name: `a${i}`, type: arg.type })); + const signature = args.map((arg) => typeKey(arg.type)).join(","); + const name = dispatchHelper( + lowerer, + `idxUnion:${method}(${signature}):${unionKey(union)}`, + `%union.${method}`, + union, + arms, + params, + resultT, + (arm, info, refs) => { + const call: IrExpr = { + kind: "bytesIntrinsic", + method, + receiver: arm, + args: [...refs], + type: result === "receiver" ? info.type : result, + loc, + }; + if (result === "receiver") + return returning( + { + kind: "unionWrap", + unionId: union.unionId, + tag: info.tag, + value: call, + type: union, + loc, + }, + loc, + ); + if (result.kind === "void") + return [ + { kind: "exprStmt", expr: call, loc }, + { kind: "return", value: null, loc }, + ]; + return returning(call, loc); + }, + loc, + ); + return { kind: "call", callee: name, args: [receiver, ...args], type: resultT, loc }; +} + +/** The elements of `u` as a `number[]`: a typed-array arm drains its + * elements into a fresh array. A number-array arm is the array itself for + * consumers that copy it (spreads), or with `fresh` a dense copy whose + * holes read as undefined (Array.from). */ +export function lowerIndexedUnionToArray( + lowerer: Lowerer, + receiver: IrExpr, + arms: readonly IndexedArm[], + loc: SrcLoc, + fresh = false, +): IrExpr { + const arrayT: IrType = { kind: "array", elem: F64 }; + const name = dispatchHelper( + lowerer, + `idxUnion:${fresh ? "from" : "toArray"}:${unionKey(receiver.type)}`, + fresh ? "%union.arrayFrom" : "%union.toArray", + receiver.type, + arms, + [], + arrayT, + (arm, info) => + returning( + info.type.kind === "bytes" + ? { + kind: "bytesIntrinsic", + method: "toArray", + receiver: arm, + args: [], + type: arrayT, + loc, + } + : fresh + ? { + kind: "arrIntrinsic", + method: "reverse", + receiver: { + kind: "arrIntrinsic", + method: "toReversed", + receiver: arm, + args: [], + type: arrayT, + loc, + }, + args: [], + type: arrayT, + loc, + } + : arm, + loc, + ), + loc, + ); + return { kind: "call", callee: name, args: [receiver], type: arrayT, loc }; +} + +/** Whether a dispatch can run in place instead of through a helper call: + * the receiver is a plain read, so testing and narrowing it once per arm + * reads the same value, and the other operands are plain reads or + * literals. The arms' own reads then stay visible to the integer range + * analysis (a loop bounded by `u.length` keeps an integer counter). */ +function inlineDispatch(receiver: IrExpr, operands: readonly IrExpr[]): boolean { + return ( + receiver.kind === "varRef" && operands.every((e) => e.kind === "varRef" || e.kind === "numLit") + ); +} + +/** Expressions that run no program code: reads, literals and operators + * over primitive values. */ +const CODE_FREE_EXPRS = new Set([ + "numLit", + "boolLit", + "strLit", + "unitLit", + "varRef", + "unary", + "logical", + "ternary", + "toBool", + "strEq", + "strCmp", + "arrayGet", + "arrayHas", + "arrayState", + "bytesIntrinsic", + "fieldGet", + "recordGet", + "unionNarrow", + "unionIsTag", + "unionDisc", + "upcast", + "nullish", +]); +const PRIMITIVE_OPERANDS = new Set(["f64", "bool", "string", "bigint"]); + +/** Whether evaluating `e` can rebind one of the plain reads `reads`: by + * assigning it, or by running program code (a call, a getter, a + * conversion) that could assign a captured binding. */ +function rebinds(e: IrExpr, reads: readonly IrExpr[]): boolean { + const ids = new Set(reads.flatMap((r) => (r.kind === "varRef" ? [r.localId] : []))); + let found = false; + const expr = (x: IrExpr): boolean => { + if ((x.kind === "assignExpr" || x.kind === "incDec") && ids.has(x.localId)) found = true; + else if (x.kind === "bin") { + if (!PRIMITIVE_OPERANDS.has(x.left.type.kind) || !PRIMITIVE_OPERANDS.has(x.right.type.kind)) + found = true; + } else if (x.kind === "call") { + // The union dispatch helpers only read and store elements. + if (!x.callee.startsWith("%union.")) found = true; + } else if (!CODE_FREE_EXPRS.has(x.kind)) found = true; + return !found && everyExprChild(x, expr, stmt); + }; + const stmt = (x: IrStmt): boolean => { + if ((x.kind === "assign" || x.kind === "varDecl") && ids.has(x.localId)) found = true; + return !found && everyStmtChild(x, expr, stmt); + }; + expr(e); + return found; +} + +/** The in-place form of a dispatch: a ternary chain over the arms' tags, + * the last arm untested. */ +function inlineArms( + receiver: IrExpr, + arms: readonly IndexedArm[], + type: IrType, + arm: (narrowed: IrExpr, info: IndexedArm) => IrExpr, + loc: SrcLoc, +): IrExpr { + const union = receiver.type; + if (union.kind !== "union") throw new Error("indexed-union dispatch over a non-union"); + const narrowed = (info: IndexedArm): IrExpr => ({ + kind: "unionNarrow", + unionId: union.unionId, + tag: info.tag, + value: receiver, + type: info.type, + loc, + }); + let result = arm(narrowed(arms[arms.length - 1]!), arms[arms.length - 1]!); + for (let i = arms.length - 2; i >= 0; i--) { + const info = arms[i]!; + result = { + kind: "ternary", + cond: { + kind: "unionIsTag", + unionId: union.unionId, + tag: info.tag, + negated: false, + value: receiver, + type: BOOL, + loc, + }, + then: arm(narrowed(info), info), + else_: result, + type, + loc, + }; + } + return result; +} diff --git a/packages/compiler/src/frontend/lowering/instantiation-path.test.ts b/packages/compiler/src/frontend/lowering/instantiation-path.test.ts index a31617106..267b402bc 100644 --- a/packages/compiler/src/frontend/lowering/instantiation-path.test.ts +++ b/packages/compiler/src/frontend/lowering/instantiation-path.test.ts @@ -1,19 +1,23 @@ import { expect, test } from "vitest"; +import type * as ts from "../ts7/adapter.js"; import { extendInstantiationPath, MAX_INSTANTIATION_RECURSION } from "./instantiation-path.js"; +/** A stand-in declaration: paths compare declarations by identity only. */ +const node = (): ts.Node => ({}) as ts.Node; + test("independent declarations and roots do not consume a recursive budget", () => { - const declaration = {}; + const declaration = node(); for (let i = 0; i < MAX_INSTANTIATION_RECURSION * 2; i++) expect(extendInstantiationPath(null, declaration)).not.toBeNull(); let path = extendInstantiationPath(null, declaration)!; for (let i = 0; i < MAX_INSTANTIATION_RECURSION * 2; i++) - path = extendInstantiationPath(path, {})!; + path = extendInstantiationPath(path, node())!; expect(extendInstantiationPath(path, declaration)).not.toBeNull(); }); test("bounds fresh recursive demands across intervening declarations", () => { - const first = {}; - const second = {}; + const first = node(); + const second = node(); let path = extendInstantiationPath(null, first)!; for (let i = 1; i < MAX_INSTANTIATION_RECURSION; i++) { path = extendInstantiationPath(path, second)!; diff --git a/packages/compiler/src/frontend/lowering/instantiation-path.ts b/packages/compiler/src/frontend/lowering/instantiation-path.ts index d79760988..b4a8375f6 100644 --- a/packages/compiler/src/frontend/lowering/instantiation-path.ts +++ b/packages/compiler/src/frontend/lowering/instantiation-path.ts @@ -1,7 +1,9 @@ +import type * as ts from "../ts7/adapter.js"; + /** Queued bodies preserve their demand ancestry. Independent concrete uses * do not consume a recursion budget; only fresh instances on one path do. */ export interface InstantiationPath { - declaration: object; + declaration: ts.Node; parent: InstantiationPath | null; } @@ -9,7 +11,7 @@ export const MAX_INSTANTIATION_RECURSION = 100; export function extendInstantiationPath( parent: InstantiationPath | null, - declaration: object, + declaration: ts.Node, ): InstantiationPath | null { let repetitions = 0; for (let path = parent; path; path = path.parent) { diff --git a/packages/compiler/src/frontend/lowering/iterator-adapters.ts b/packages/compiler/src/frontend/lowering/iterator-adapters.ts index e50652182..78fcff84d 100644 --- a/packages/compiler/src/frontend/lowering/iterator-adapters.ts +++ b/packages/compiler/src/frontend/lowering/iterator-adapters.ts @@ -1,3 +1,4 @@ +import { appendElement, optionalElementValue, primitiveElement } from "./array-values.js"; import { keyedHelperName } from "./stable-names.js"; import { dynUndefinedExpr, varRef } from "../../ir/build.js"; import { @@ -28,11 +29,206 @@ export function iteratorSlotAdapter( expr: IrExpr, expected: IrType & { kind: "generator" }, ): IrExpr | null { - if (expr.type.kind === "dyn") return dynIteratorAdapter(lowerer, expr, expected); + if (expr.type.kind === "dyn") + return ( + collectionIteratorSlotValue(lowerer, expr, expected) ?? + dynIteratorAdapter(lowerer, expr, expected) + ); if (expr.type.kind === "generator") return generatorRetypeAdapter(lowerer, expr, expected); return null; } +/** `m.values()` (or keys/entries, or a Set's) on a typed Map or Set, which + * lowers to a dynamic invocation on the boxed collection, flowing into an + * iterator-typed slot (`values(): IterableIterator { return + * this.rows.values(); }`): an interned generator over the collection that + * reads each entry typed, instead of the checked adapter over the boxed + * native iterator (dynIteratorAdapter), which boxes every value and checks + * it back. It keeps that adapter's protocol: the walk is lazy and live, and + * since the native iterator has no return(), an early exit from a + * consumer's loop parks it on the current entry instead of completing it, + * so a later loop continues with the next entry. Null for any other source + * or a slot whose yield type is not the collection's entry type. */ +function collectionIteratorSlotValue( + lowerer: Lowerer, + expr: IrExpr, + expected: IrType & { kind: "generator" }, +): IrExpr | null { + if (expr.kind !== "dynInvoke" || expr.args.length > 0 || expr.calleeNameValue) return null; + const method = expr.method; + if (method !== "values" && method !== "keys" && method !== "entries") return null; + if (expr.recv.kind !== "dynFrom") return null; + const receiver = expr.recv.value; + const collT = receiver.type; + if (collT.kind !== "map" && collT.kind !== "set") return null; + if (expected.async || expected.yieldT.kind === "void") return null; + if (expected.retT.kind !== "void" && expected.retT.kind !== "dyn") return null; + if (expected.nextT.kind !== "undefinedT" && expected.nextT.kind !== "dyn") return null; + const resultType = genResultRecord( + expected.yieldT, + expected.retT, + lowerer.shapes, + lowerer.unions, + ); + if (!resultType) return null; + const loc = expr.loc; + const key = `collIterSlot:${method}:${typeKey(collT)}:${typeKey(expected)}`; + let name = lowerer.valueHelpers.get(key); + if (!name) { + const coll = varRef("coll.0", collT, loc); + const index = varRef("index.0", F64, loc); + const native = varRef("native.0", DYN, loc); + const resumedFlag = varRef("resumed.0", BOOL, loc); + const keyT = collT.kind === "map" ? collT.key : collT.elem; + const valueT = collT.kind === "map" ? collT.value : collT.elem; + const read = (m: "iterKey" | "iterValue", t: IrType): IrExpr => + collT.kind === "map" + ? { kind: "mapIntrinsic", method: m, receiver: coll, args: [index], type: t, loc } + : { kind: "setIntrinsic", method: "iterKey", receiver: coll, args: [index], type: t, loc }; + // entries() answers [key, value] pairs: only a slot whose yield type is + // that pair as a record of the two fields takes the typed walk; a tuple + // or other pair type keeps the checked adapter. + if (method === "entries") { + const pair = + expected.yieldT.kind === "record" ? lowerer.shapes.get(expected.yieldT.shapeId) : undefined; + const field = (name: string): IrType | undefined => + pair?.fields.find((f) => f.name === name)?.type; + const first = field("0"); + const second = field("1"); + if ( + pair === undefined || + pair.fields.length !== 2 || + first === undefined || + second === undefined || + !typeEquals(first, keyT) || + !typeEquals(second, valueT) + ) + return null; + } + const entry = (): IrExpr => + method === "keys" || (collT.kind === "set" && method === "values") + ? read("iterKey", keyT) + : method === "values" + ? read("iterValue", valueT) + : { + kind: "recordLit", + fields: [ + { name: "0", value: read("iterKey", keyT) }, + { name: "1", value: read("iterValue", valueT) }, + ], + type: expected.yieldT, + loc, + }; + if (!typeEquals(entry().type, expected.yieldT)) return null; + // The entry is read once per step and kept: the parking yield of an + // early exit answers it again rather than rereading a slot the loop may + // have deleted. + const current = varRef("current.0", expected.yieldT, loc); + const suspend = (): IrStmt => ({ + kind: "exprStmt", + expr: { kind: "yieldExpr", value: current, type: resumeType(expected.nextT), loc }, + loc, + }); + const finish: IrStmt = { + kind: "return", + value: expected.retT.kind === "dyn" ? dynUndefinedExpr(loc) : null, + loc, + }; + name = keyedHelperName(lowerer, "%coll.iterSlot", key); + lowerer.valueHelpers.set(key, name); + lowerer.liftedFns.push({ + name, + params: [{ localId: "coll.0", name: "coll", type: collT }], + returnType: expected.retT, + locals: [ + { id: "coll.0", name: "coll", type: collT, mutable: false }, + { id: "native.0", name: "native", type: DYN, mutable: false }, + { id: "index.0", name: "index", type: F64, mutable: true }, + { id: "resumed.0", name: "resumed", type: BOOL, mutable: true }, + { id: "current.0", name: "current", type: expected.yieldT, mutable: false }, + ], + body: [ + { + kind: "varDecl", + localId: "native.0", + init: { + kind: "dynInvoke", + recv: lowerer.coerceToExpected(coll, DYN), + method: "values", + calleeName: collT.kind === "map" ? "map.values" : "set.values", + args: [], + type: DYN, + loc, + }, + loc, + }, + { + kind: "varDecl", + localId: "resumed.0", + init: { kind: "boolLit", value: false, type: BOOL, loc }, + loc, + }, + { + kind: "while", + cond: { kind: "boolLit", value: true, type: BOOL, loc }, + body: [ + { + kind: "assign", + localId: "index.0", + value: { + kind: "libCall", + fn: "dyn.collectionIteratorIndex", + args: [native], + type: F64, + loc, + }, + loc, + }, + { + kind: "if", + cond: { + kind: "bin", + op: "<", + left: index, + right: { kind: "numLit", value: 0, type: F64, loc }, + type: BOOL, + loc, + }, + then: [finish], + else_: null, + loc, + }, + setResumed(false, loc), + { kind: "varDecl", localId: "current.0", init: entry(), loc }, + { + kind: "tryCatch", + tryBody: [suspend(), setResumed(true, loc)], + catchBody: null, + catchLocalId: null, + // An early exit parks on the current entry; the next step + // resumes the walk after it. + finallyBody: [ + { + kind: "if", + cond: { kind: "unary", op: "!", operand: resumedFlag, type: BOOL, loc }, + then: [suspend(), { kind: "continue", loc }], + else_: null, + loc, + }, + ], + loc, + }, + ], + loc, + }, + ], + generator: { yieldT: expected.yieldT, nextT: expected.nextT, resultType, tag: "!" }, + loc, + }); + } + return { kind: "call", callee: name, args: [receiver], type: expected, loc }; +} + /** `resumed.0 = value` — the adapters' early-exit marker around a yield. */ function setResumed(value: boolean, loc: SrcLoc): IrStmt { return { @@ -547,7 +743,7 @@ export function lowerStringIteratorValue( left: index, right: { kind: "strIntrinsic", - method: "length", + method: "byteLength", receiver: text, args: [], type: F64, @@ -562,7 +758,7 @@ export function lowerStringIteratorValue( localId: "point.0", init: { kind: "strIntrinsic", - method: "cpAt", + method: "cpAtByte", receiver: text, args: [index], type: STRING, @@ -579,7 +775,7 @@ export function lowerStringIteratorValue( left: index, right: { kind: "strIntrinsic", - method: "length", + method: "byteLength", receiver: point, args: [], type: F64, @@ -769,16 +965,27 @@ function generatorRetypeAdapter( return { kind: "call", callee: name, args: [expr], type: expected, loc }; } +/** A checked-dynamic yielded value for a `(T | undefined)[]` of a + * number, string or boolean T: converted to `T | undefined`, which the + * drain stores as a T element or a present undefined. */ +function optionalYield(lowerer: Lowerer, value: IrExpr, elem: IrType, optional: boolean): IrExpr { + if (!optional || value.type.kind !== "dyn" || !primitiveElement(elem)) return value; + return lowerer.coerceToExpected(value, lowerer.withUndefinedArm(elem)); +} + /** Drain a synchronous generator into a fresh `elem[]` — the eager * consumers of an iterator (`[...it]`, `Array.from(it)`). Each step resumes * the generator once and appends its value; completion stops the drain * without appending the return value, as in JS. Null when the yielded - * values cannot be extracted into the element type. */ + * values cannot be extracted into the element type. `optional`: the + * result is a `(T | undefined)[]`, which stores T elements, so a + * checked-dynamic value may also be undefined. */ export function generatorDrain( lowerer: Lowerer, source: IrExpr, elem: IrType, loc: SrcLoc, + optional = false, ): IrExpr | null { const gen = source.type; if (gen.kind !== "generator" || gen.async || gen.yieldT.kind === "void") return null; @@ -787,14 +994,16 @@ export function generatorDrain( const valueT = shape?.fields.find((field) => field.name === "value")?.type; if (!resultType || !valueT) return null; const arrT: IrType = { kind: "array", elem }; - const key = `genDrain:${typeKey(gen)}:${typeKey(elem)}`; + const key = `genDrain:${typeKey(gen)}:${typeKey(elem)}${optional ? "?" : ""}`; let name = lowerer.valueHelpers.get(key); if (!name && stepDrivable(gen)) { // The record-free drive: test done, then take the yielded value. const genRef = varRef("gen.0", gen, loc) as IrExpr & { type: typeof gen }; const outRef = varRef("out.0", arrT, loc); - const value = lowerer.coerceToExpected(genValue(genRef, loc), elem); - if (!typeEquals(value.type, elem)) return null; + const yielded = optionalYield(lowerer, genValue(genRef, loc), elem, optional); + const present = optionalElementValue(lowerer, yielded, elem); + const value = present ? yielded : lowerer.coerceToExpected(yielded, elem); + if (!present && !typeEquals(value.type, elem)) return null; name = keyedHelperName(lowerer, "%gen.drain", key); lowerer.valueHelpers.set(key, name); lowerer.liftedFns.push({ @@ -804,6 +1013,7 @@ export function generatorDrain( locals: [ { id: "gen.0", name: "gen", type: gen, mutable: false }, { id: "out.0", name: "out", type: arrT, mutable: false }, + ...(present ? [{ id: "v.0", name: "v", type: value.type, mutable: false }] : []), ], body: [ { @@ -815,20 +1025,7 @@ export function generatorDrain( { kind: "while", cond: { kind: "unary", op: "!", operand: genStep(genRef, loc), type: BOOL, loc }, - body: [ - { - kind: "exprStmt", - expr: { - kind: "arrIntrinsic", - method: "push", - receiver: outRef, - args: [value], - type: F64, - loc, - }, - loc, - }, - ], + body: appendElement(lowerer, outRef, value, elem, "v.0", loc), loc, }, { kind: "return", value: outRef, loc }, @@ -855,8 +1052,10 @@ export function generatorDrain( loc, ); if (!raw) return null; - const value = lowerer.coerceToExpected(raw, elem); - if (!typeEquals(value.type, elem)) return null; + const yielded = optionalYield(lowerer, raw, elem, optional); + const present = optionalElementValue(lowerer, yielded, elem); + const value = present ? yielded : lowerer.coerceToExpected(yielded, elem); + if (!present && !typeEquals(value.type, elem)) return null; name = keyedHelperName(lowerer, "%gen.drain", key); lowerer.valueHelpers.set(key, name); lowerer.liftedFns.push({ @@ -867,6 +1066,7 @@ export function generatorDrain( { id: "gen.0", name: "gen", type: gen, mutable: false }, { id: "out.0", name: "out", type: arrT, mutable: false }, { id: "step.0", name: "step", type: resultType, mutable: false }, + ...(present ? [{ id: "v.0", name: "v", type: value.type, mutable: false }] : []), ], body: [ { @@ -906,18 +1106,7 @@ export function generatorDrain( else_: null, loc, }, - { - kind: "exprStmt", - expr: { - kind: "arrIntrinsic", - method: "push", - receiver: outRef, - args: [value], - type: F64, - loc, - }, - loc, - }, + ...appendElement(lowerer, outRef, value, elem, "v.0", loc), ], loc, }, diff --git a/packages/compiler/src/frontend/lowering/js-class-fields.ts b/packages/compiler/src/frontend/lowering/js-class-fields.ts index 2f726ce97..28a7ceeda 100644 --- a/packages/compiler/src/frontend/lowering/js-class-fields.ts +++ b/packages/compiler/src/frontend/lowering/js-class-fields.ts @@ -2,11 +2,19 @@ import * as ts from "../ts7/adapter.js"; import { isClassOwnEnumerableFieldName } from "../../ir/ir.js"; import type { ClassInfo } from "./lower-classes.js"; -/** Reflection that can remove a property or change its attributes. */ -const PROPERTY_REFLECTION = new Set(["deleteProperty", "freeze", "seal", "preventExtensions"]); +/* Object.freeze, Object.seal and Object.preventExtensions do not appear + * here: they never create or remove a property, and a class instance keeps + * its integrity state outside the field layout. A statically typed call + * sets the instance's CLASS_ATTRIBUTES word (class-attributes.ts), which + * TypeScript classes use with the same fixed layouts; through an untyped + * value the runtime refuses the operation on any class instance + * (scr_dyn_class_integrity_fence). Lifting that fence must revisit this. */ -/** `Object.freeze({ ... })` and the like apply to a new literal, which is - * not a class instance. */ +/** Reflection that can remove any property. */ +const PROPERTY_REFLECTION = new Set(["deleteProperty"]); + +/** `Reflect.deleteProperty({ ... }, key)` applies to a new literal, which + * is not a class instance. */ function freshLiteralArgument(callee: ts.PropertyAccessExpression): boolean { const call = callee.parent; if (!ts.isCallExpression(call) || call.expression !== callee) return false; @@ -141,12 +149,35 @@ export function constructionPrefix( return { created, sealed }; } -/** Whether a class declares a getter or setter, which can intercept a - * base constructor's field assignment. */ -function declaresAccessor(info: ClassInfo): boolean { - return [...info.methods.keys()].some( - (name) => name.startsWith("get:") || name.startsWith("set:"), - ); +/** The names of the getters and setters among a class's own methods. */ +export function accessorNames(methods: Iterable): Set { + const names = new Set(); + for (const name of methods) + if (name.startsWith("get:") || name.startsWith("set:")) names.add(name.slice(4)); + return names; +} + +/** Whether one of `accessors` can intercept an assignment the base + * constructions make: a base constructor's `this.name = value` runs a + * subclass accessor of that name instead of creating the field. Only field + * names can reach the created prefix; any other assignment already ends + * it. */ +export function accessorInterceptsBase(accessors: ReadonlySet, base: ClassInfo): boolean { + for (const name of accessors) if (base.fields.has(name)) return true; + return false; +} + +/** Whether an accessor that instances of `info` inherit or declare shares + * a name with one of their fields. Such a field and accessor disagree on + * what the instance's property is; any other accessor lives on the + * prototype beside the fields and never touches the layout. */ +function accessorShadowsField(info: ClassInfo): boolean { + for (let current: ClassInfo | null = info; current; current = current.base) { + if (current.def.runtime) break; + for (const name of accessorNames(current.methods.keys())) + if (info.fields.has(name)) return true; + } + return false; } /** `constructionPrefix` for a collected class and its bases. */ @@ -156,7 +187,8 @@ export function construction(info: ClassInfo): Construction | null { if (info.base) { base = construction(info.base); if (!base) return null; - if (declaresAccessor(info)) base = { created: base.created, sealed: false }; + if (accessorInterceptsBase(accessorNames(info.methods.keys()), info.base)) + base = { created: base.created, sealed: false }; } return constructionPrefix(info.decl, base, (name) => info.fields.has(name)); } @@ -169,7 +201,7 @@ export function construction(info: ClassInfo): Construction | null { export function fixedFieldFamily(root: ClassInfo, deleted: Set | null): boolean { if (deleted === null) return false; const fixed = (info: ClassInfo): boolean => { - if (declaresAccessor(info)) return false; + if (accessorShadowsField(info)) return false; const built = construction(info); if (!built) return false; for (const field of info.fieldOrder) { diff --git a/packages/compiler/src/frontend/lowering/js-collection-types.ts b/packages/compiler/src/frontend/lowering/js-collection-types.ts new file mode 100644 index 000000000..1e3219276 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/js-collection-types.ts @@ -0,0 +1,539 @@ +/* Types of untyped JavaScript collections that their uses prove. + * + * In a JavaScript file, `const m = new Map()`, `new Set()` and `[]` type as + * Map, Set and any[] to the checker, so every use lowers to + * the checked-dynamic collection operations. When every reference of such a + * `const` is a direct use of the collection, the value never leaves the + * binding. If its keys, values or elements are then each one primitive type + * wherever the program writes and looks them up, the binding lowers as the + * typed Map, Set or T[], as a JSDoc @type would make it: + * + * - Map: `m.get(k)`, `m.has(k)`, `m.delete(k)`, `m.clear()`, `m.size`, and + * `m.set(k, v)` as a statement (its result is the map). + * - Set: `s.add(k)` as a statement, `s.has(k)`, `s.delete(k)`, `s.clear()`, + * `s.size`. + * - Array: `a.push(...)`, `a.pop()`, `a.shift()`, `a.at(i)`, `a.includes(v)`, + * `a.indexOf(v)`, `a.join(separator)`, `a.length`. + * + * Any other reference (passing, returning, storing, exporting, iterating, + * spreading, inspecting the collection, an optional call), a direct `eval` + * in the file, or any doubt about a type keeps the binding untyped. + * + * The types come from carrier declarations in scriptc-overrides.d.ts, which + * only the lowering program includes. */ + +import * as ts from "../ts7/adapter.js"; +import { isJsSourceFile } from "../program.js"; +import { overridesDtsPath } from "../dts-paths.js"; +import type { Lowerer } from "./lowerer.js"; + +type Primitive = "string" | "number" | "boolean"; +const PRIMITIVES: readonly Primitive[] = ["string", "number", "boolean"]; + +type Collection = "map" | "set" | "array"; + +interface Candidate { + readonly kind: Collection; + readonly declaration: ts.VariableDeclaration; + /** Every identifier that names the binding, its declaration's included. */ + readonly names: ts.Identifier[]; + /** Map keys or Set members written by set/add. */ + readonly keyWrites: ts.Expression[]; + /** Map values written by set. Array elements written by push. */ + readonly valueWrites: ts.Expression[]; + /** Arguments compared with keys or elements: get/has/delete, includes/indexOf. */ + readonly lookups: ts.Expression[]; + /** Array indexes passed to at(). */ + readonly indexes: ts.Expression[]; + /** Separators passed to join(). */ + readonly separators: ts.Expression[]; + key: Primitive | undefined; + value: Primitive | undefined; +} + +const MAP_LOOKUPS = new Set(["get", "has", "delete"]); +const SET_LOOKUPS = new Set(["has", "delete"]); +const ARRAY_READS = new Set(["pop", "shift"]); +const ARRAY_SEARCHES = new Set(["includes", "indexOf"]); +/** The expression lowering refuses deeper nesting (SC1090). */ +const MAX_DEPTH = 200; + +/** The inferred type of the binding a `new Map()` or `new Set()` initializes, + * which the construction adopts. */ +export function initializedCollectionType( + lowerer: Lowerer, + expr: ts.NewExpression, +): ts.Type | null { + const declaration = expr.parent; + return declaration !== undefined && + ts.isVariableDeclaration(declaration) && + declaration.initializer === expr && + ts.isIdentifier(declaration.name) + ? inferredCollectionType(lowerer, declaration.name) + : null; +} + +/** The inferred collection type of an identifier that names a proven + * binding, or null. Computed for every JavaScript file of the program on + * first use. The inference itself reads types (a candidate's constructor + * through a destructured require, for example); while it runs, those reads + * see no inferred types rather than starting it again. */ +export function inferredCollectionType(lowerer: Lowerer, node: ts.Identifier): ts.Type | null { + if (lowerer.inferredCollectionTypes === undefined) { + lowerer.inferredCollectionTypes = new Map(); + lowerer.inferredCollectionTypes = inferProgram(lowerer); + } + const types = lowerer.inferredCollectionTypes; + return types.size === 0 ? null : (types.get(node) ?? null); +} + +function inferProgram(lowerer: Lowerer): Map { + const out = new Map(); + const carriers = carrierTypes(lowerer); + if (carriers === null) return out; + for (const file of lowerer.program.getImplementationSourceFiles()) { + if (!isJsSourceFile(file)) continue; + for (const candidate of inferFile(lowerer, file)) { + const name = + candidate.kind === "map" + ? `map_${candidate.key}_${candidate.value}` + : candidate.kind === "set" + ? `set_${candidate.key}` + : `array_${candidate.value}`; + const type = carriers.get(name); + if (type === undefined) continue; + for (const id of candidate.names) out.set(id, type); + } + } + return out; +} + +/** The carrier declarations' types, by name (`map_string_number`, + * `set_number`, `array_string`, ...), or null when the lowering program does + * not include them. */ +function carrierTypes(lowerer: Lowerer): Map | null { + const file = lowerer.program.getSourceFile(overridesDtsPath()); + if (file === undefined) return null; + const out = new Map(); + for (const statement of file.statements.nodes) { + if ( + !ts.isModuleDeclaration(statement) || + !ts.isIdentifier(statement.name) || + statement.name.text !== "__scriptcInferredCollections" || + statement.body === undefined || + !ts.isModuleBlock(statement.body) + ) + continue; + for (const member of statement.body.statements.nodes) { + if (!ts.isVariableStatement(member)) continue; + for (const declaration of member.declarationList.declarations.nodes) + if (ts.isIdentifier(declaration.name)) + out.set(declaration.name.text, lowerer.checker.getTypeAtLocation(declaration.name)); + } + } + return out.size > 0 ? out : null; +} + +function inferFile(lowerer: Lowerer, file: ts.SourceFile): Candidate[] { + const candidates = new Map(); + let directEval = false; + const declarations = (node: ts.Node): void => { + if ( + ts.isCallExpression(node) && + ts.isIdentifier(node.expression) && + node.expression.text === "eval" + ) + directEval = true; + if (ts.isVariableDeclaration(node)) { + const kind = candidateKind(lowerer, node); + const symbol = kind !== null ? lowerer.checker.getSymbolAtLocation(node.name) : undefined; + if (kind !== null && symbol !== undefined) + candidates.set(symbol, { + kind, + declaration: node, + names: [node.name as ts.Identifier], + keyWrites: [], + valueWrites: [], + lookups: [], + indexes: [], + separators: [], + key: undefined, + value: undefined, + }); + } + }; + ts.walkPreorder(file, declarations); + if (directEval || candidates.size === 0) return []; + + const names = new Set( + [...candidates.values()].map((c) => (c.declaration.name as ts.Identifier).text), + ); + const refused = new Set(); + const references = (node: ts.Node): void => { + if (ts.isIdentifier(node) && names.has(node.text)) { + const symbol = referencedSymbol(lowerer, node); + const candidate = symbol !== undefined ? candidates.get(symbol) : undefined; + if (symbol !== undefined && candidate !== undefined && node !== candidate.declaration.name) { + if (recordUse(candidate, node)) candidate.names.push(node); + else refused.add(symbol); + } + } + }; + ts.walkPreorder(file, references); + for (const symbol of refused) candidates.delete(symbol); + const proven = [...candidates.values()].filter( + (c) => c.valueWrites.length > 0 || c.keyWrites.length > 0, + ); + solve(lowerer, proven); + return proven.filter((c) => + c.kind === "map" ? c.key && c.value : c.kind === "set" ? c.key : c.value, + ); +} + +/** The collection a `const` declaration starts empty, when the checker + * types it as an untyped Map, Set or array. */ +function candidateKind(lowerer: Lowerer, declaration: ts.VariableDeclaration): Collection | null { + if (!ts.isIdentifier(declaration.name) || declaration.initializer === undefined) return null; + const list = declaration.parent; + if (!ts.isVariableDeclarationList(list) || (list.flags & ts.NodeFlags.Const) === 0) return null; + const statement = list.parent; + if ( + ts.isVariableStatement(statement) && + ts.getModifiers(statement)?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) + ) + return null; + const init = declaration.initializer; + let kind: Collection | null = null; + if (ts.isArrayLiteralExpression(init) && init.elements.nodes.length === 0) kind = "array"; + else if ( + ts.isNewExpression(init) && + init.typeArguments === undefined && + (init.arguments?.nodes.length ?? 0) === 0 + ) { + if (lowerer.isStdlibGlobal(init.expression, "Map")) kind = "map"; + else if (lowerer.isStdlibGlobal(init.expression, "Set")) kind = "set"; + } + if (kind === null) return null; + // A JSDoc type, or anything else the checker already knows, stands. + const type = lowerer.checker.getTypeAtLocation(declaration.name); + const args = + kind === "array" + ? lowerer.checker.isArrayType(type) + ? lowerer.checker.getTypeArguments(type as ts.TypeReference) + : [] + : lowerer.checker.getTypeArguments(type as ts.TypeReference); + const arity = kind === "map" ? 2 : 1; + if (args.length !== arity || !args.every((a) => (a.flags & ts.TypeFlags.Any) !== 0)) return null; + return kind; +} + +function referencedSymbol(lowerer: Lowerer, id: ts.Identifier): ts.Symbol | undefined { + const parent = id.parent; + if (parent !== undefined && ts.isShorthandPropertyAssignment(parent) && parent.name === id) + return lowerer.checker.getShorthandAssignmentValueSymbol(parent); + const symbol = lowerer.checker.getSymbolAtLocation(id); + // `export { m }` names the export, an alias of the local binding. + if ( + symbol !== undefined && + parent !== undefined && + ts.isExportSpecifier(parent) && + parent.propertyName === undefined && + (symbol.flags & ts.SymbolFlags.Alias) !== 0 + ) + return lowerer.checker.getAliasedSymbol(symbol); + return symbol; +} + +/** Records a reference's use, or answers false when the reference is not + * one of the collection uses this inference proves. */ +function recordUse(candidate: Candidate, id: ts.Identifier): boolean { + const access = id.parent; + if ( + access === undefined || + !ts.isPropertyAccessExpression(access) || + access.expression !== id || + access.questionDotToken !== undefined + ) + return false; + const name = access.name.text; + const call = access.parent; + if (call !== undefined && ts.isCallExpression(call) && call.expression === access) { + if (call.questionDotToken !== undefined) return false; + const args = call.arguments.nodes; + if (args.some(ts.isSpreadElement)) return false; + switch (candidate.kind) { + case "map": + if (MAP_LOOKUPS.has(name) && args.length === 1) + return (candidate.lookups.push(args[0]!), true); + if (name === "clear") return args.length === 0; + if (name === "set" && args.length === 2 && isStatement(call)) { + candidate.keyWrites.push(args[0]!); + candidate.valueWrites.push(args[1]!); + return true; + } + return false; + case "set": + if (SET_LOOKUPS.has(name) && args.length === 1) + return (candidate.lookups.push(args[0]!), true); + if (name === "clear") return args.length === 0; + if (name === "add" && args.length === 1 && isStatement(call)) + return (candidate.keyWrites.push(args[0]!), true); + return false; + case "array": + if (name === "push") return (candidate.valueWrites.push(...args), true); + if (ARRAY_READS.has(name)) return args.length === 0; + if (name === "at" && args.length === 1) return (candidate.indexes.push(args[0]!), true); + if (ARRAY_SEARCHES.has(name) && args.length === 1) + return (candidate.lookups.push(args[0]!), true); + if (name === "join" && args.length <= 1) return (candidate.separators.push(...args), true); + return false; + } + } + return (candidate.kind === "array" ? name === "length" : name === "size") && isPlainRead(access); +} + +function isStatement(call: ts.CallExpression): boolean { + let node: ts.Node = call; + while (node.parent !== undefined && ts.isParenthesizedExpression(node.parent)) node = node.parent; + return node.parent !== undefined && ts.isExpressionStatement(node.parent); +} + +/** A read that neither writes nor deletes the property. */ +function isPlainRead(access: ts.PropertyAccessExpression): boolean { + let node: ts.Node = access; + while (node.parent !== undefined && ts.isParenthesizedExpression(node.parent)) node = node.parent; + const parent = node.parent; + if (parent === undefined) return false; + if ( + ts.isBinaryExpression(parent) && + parent.left === node && + isAssignment(parent.operatorToken.kind) + ) + return false; + if ( + (ts.isPrefixUnaryExpression(parent) || ts.isPostfixUnaryExpression(parent)) && + (parent.operator === ts.SyntaxKind.PlusPlusToken || + parent.operator === ts.SyntaxKind.MinusMinusToken) + ) + return false; + return !ts.isDeleteExpression(parent); +} + +function isAssignment(kind: ts.SyntaxKind): boolean { + return kind >= ts.SyntaxKind.FirstAssignment && kind <= ts.SyntaxKind.LastAssignment; +} + +/** Decides each candidate's key and value types: a fixed point in which a + * decided candidate's reads type the expressions built from them. */ +function solve(lowerer: Lowerer, candidates: Candidate[]): void { + const bySymbol = new Map(); + for (const c of candidates) { + const symbol = lowerer.checker.getSymbolAtLocation(c.declaration.name); + if (symbol) bySymbol.set(symbol, c); + } + for (let changed = true; changed;) { + changed = false; + for (const c of candidates) { + if (c.kind !== "array" && c.key === undefined) { + const key = uniform(lowerer, bySymbol, c.keyWrites); + if (key !== null) { + c.key = key; + changed = true; + } + } + if (c.kind !== "set" && c.value === undefined) { + // Each candidate type is assumed for this collection's own reads. + const accepted = PRIMITIVES.filter((assumed) => { + c.value = assumed; + const found = uniform(lowerer, bySymbol, c.valueWrites); + c.value = undefined; + return found === assumed; + }); + if (accepted.length === 1) { + c.value = accepted[0]; + changed = true; + } + } + } + } + for (const c of candidates) { + const element = c.kind === "array" ? c.value : c.key; + // Lookups compare with the stored type; an index is a number. + const lookupsAgree = + element !== undefined && + c.lookups.every((e) => primitiveOf(lowerer, bySymbol, e, new Set()) === element) && + c.indexes.every((e) => primitiveOf(lowerer, bySymbol, e, new Set()) === "number") && + c.separators.every((e) => primitiveOf(lowerer, bySymbol, e, new Set()) === "string"); + if (!lookupsAgree) { + c.key = undefined; + c.value = undefined; + } + } +} + +/** The one primitive type of every expression, or null. */ +function uniform( + lowerer: Lowerer, + bySymbol: Map, + exprs: readonly ts.Expression[], +): Primitive | null { + let found: Primitive | null = null; + for (const e of exprs) { + const p = primitiveOf(lowerer, bySymbol, e, new Set()); + if (p === null || (found !== null && p !== found)) return null; + found = p; + } + return found; +} + +function checkerPrimitive(type: ts.Type): Primitive | null { + const flags = type.flags; + if (flags & (ts.TypeFlags.String | ts.TypeFlags.StringLiteral)) return "string"; + if (flags & (ts.TypeFlags.Number | ts.TypeFlags.NumberLiteral)) return "number"; + if (flags & (ts.TypeFlags.Boolean | ts.TypeFlags.BooleanLiteral)) return "boolean"; + return null; +} + +/** The primitive type `e` always evaluates to, or null. The checker answers + * where it knows; where it types `e` as `any`, the value's structure does: + * a read of a decided candidate, arithmetic and comparisons over such + * values, and constants defined from them. An expression nested deeper than + * the lowering's nesting limit answers null. */ +function primitiveOf( + lowerer: Lowerer, + bySymbol: Map, + e: ts.Expression, + visiting: Set, + depth = 0, +): Primitive | null { + if (depth > MAX_DEPTH) return null; + const type = lowerer.checker.getTypeAtLocation(e); + if ((type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) === 0) return checkerPrimitive(type); + const of = (x: ts.Expression): Primitive | null => + primitiveOf(lowerer, bySymbol, x, visiting, depth + 1); + if (ts.isParenthesizedExpression(e)) return of(e.expression); + if (ts.isPrefixUnaryExpression(e)) { + if (e.operator === ts.SyntaxKind.ExclamationToken) return "boolean"; + return e.operator !== ts.SyntaxKind.PlusPlusToken && + e.operator !== ts.SyntaxKind.MinusMinusToken && + of(e.operand) === "number" + ? "number" + : null; + } + if (ts.isConditionalExpression(e)) { + const yes = of(e.whenTrue); + return yes !== null && yes === of(e.whenFalse) ? yes : null; + } + if (ts.isBinaryExpression(e)) { + const op = e.operatorToken.kind; + switch (op) { + case ts.SyntaxKind.LessThanToken: + case ts.SyntaxKind.GreaterThanToken: + case ts.SyntaxKind.LessThanEqualsToken: + case ts.SyntaxKind.GreaterThanEqualsToken: + case ts.SyntaxKind.EqualsEqualsEqualsToken: + case ts.SyntaxKind.ExclamationEqualsEqualsToken: + case ts.SyntaxKind.EqualsEqualsToken: + case ts.SyntaxKind.ExclamationEqualsToken: + return "boolean"; + case ts.SyntaxKind.QuestionQuestionToken: + case ts.SyntaxKind.BarBarToken: { + // `m.get(k) ?? d` and `a.pop() || d`: the stored type or a default of it. + const stored = storedRead(lowerer, bySymbol, e.left); + const fallback = of(e.right); + return stored !== null && stored === fallback ? stored : null; + } + case ts.SyntaxKind.PlusToken: { + const left = of(e.left); + const right = of(e.right); + if (left === "number" && right === "number") return "number"; + return left !== null && right !== null && (left === "string" || right === "string") + ? "string" + : null; + } + case ts.SyntaxKind.MinusToken: + case ts.SyntaxKind.AsteriskToken: + case ts.SyntaxKind.SlashToken: + case ts.SyntaxKind.PercentToken: + case ts.SyntaxKind.AsteriskAsteriskToken: + case ts.SyntaxKind.BarToken: + case ts.SyntaxKind.AmpersandToken: + case ts.SyntaxKind.CaretToken: + case ts.SyntaxKind.LessThanLessThanToken: + case ts.SyntaxKind.GreaterThanGreaterThanToken: + case ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken: + return of(e.left) === "number" && of(e.right) === "number" ? "number" : null; + default: + return null; + } + } + if (ts.isCallExpression(e) || ts.isPropertyAccessExpression(e)) + return candidateRead(lowerer, bySymbol, e); + // A constant defined from such an expression. + if (ts.isIdentifier(e) && !visiting.has(e)) { + const symbol = lowerer.checker.getSymbolAtLocation(e); + const declaration = symbol?.valueDeclaration; + if ( + declaration !== undefined && + ts.isVariableDeclaration(declaration) && + declaration.initializer !== undefined && + ts.isVariableDeclarationList(declaration.parent) && + (declaration.parent.flags & ts.NodeFlags.Const) !== 0 + ) { + visiting.add(e); + const p = primitiveOf(lowerer, bySymbol, declaration.initializer, visiting, depth + 1); + visiting.delete(e); + return p; + } + } + return null; +} + +/** The candidate a use reads, when `e` is `c.member` or `c.member(...)` of a + * candidate `c`. */ +function candidateOf( + lowerer: Lowerer, + bySymbol: Map, + e: ts.Expression, +): { candidate: Candidate; member: string } | null { + const access = ts.isCallExpression(e) ? e.expression : e; + if (!ts.isPropertyAccessExpression(access) || !ts.isIdentifier(access.expression)) return null; + const symbol = lowerer.checker.getSymbolAtLocation(access.expression); + const candidate = symbol !== undefined ? bySymbol.get(symbol) : undefined; + return candidate !== undefined ? { candidate, member: access.name.text } : null; +} + +/** The type of a candidate's read that may answer undefined: `m.get(k)`, + * `a.pop()`, `a.shift()`, `a.at(i)`. */ +function storedRead( + lowerer: Lowerer, + bySymbol: Map, + e: ts.Expression, +): Primitive | null { + while (ts.isParenthesizedExpression(e)) e = e.expression; + if (!ts.isCallExpression(e)) return null; + const read = candidateOf(lowerer, bySymbol, e); + if (read === null) return null; + const { candidate, member } = read; + if (candidate.kind === "map" && member === "get") return candidate.value ?? null; + if (candidate.kind === "array" && (ARRAY_READS.has(member) || member === "at")) + return candidate.value ?? null; + return null; +} + +/** The type of a candidate's read that always answers a value: size, + * length, has, includes, indexOf, push. */ +function candidateRead( + lowerer: Lowerer, + bySymbol: Map, + e: ts.Expression, +): Primitive | null { + const read = candidateOf(lowerer, bySymbol, e); + if (read === null) return null; + const { candidate, member } = read; + if (!ts.isCallExpression(e)) return member === "size" || member === "length" ? "number" : null; + if (member === "has" || member === "includes" || member === "delete") return "boolean"; + if (member === "indexOf" || (candidate.kind === "array" && member === "push")) return "number"; + if (candidate.kind === "array" && member === "join") return "string"; + return null; +} diff --git a/packages/compiler/src/frontend/lowering/key-order-witness.test.ts b/packages/compiler/src/frontend/lowering/key-order-witness.test.ts index c85902c96..21fe56815 100644 --- a/packages/compiler/src/frontend/lowering/key-order-witness.test.ts +++ b/packages/compiler/src/frontend/lowering/key-order-witness.test.ts @@ -15,6 +15,7 @@ import { type KeyOrderSegment, } from "./key-order-witness.js"; import type { Lowerer } from "./lowerer.js"; +import * as ts from "../ts7/adapter.js"; const loc = { file: "order.ts", start: 0, end: 1 }; /** `number | undefined`: the optional-field slot type. */ @@ -28,7 +29,7 @@ interface Fixture { notes: KeyOrderNotes; record(fields: [string, IrType][], order: string[]): IrType & { kind: "record" }; literal(type: IrType, keys: string[], segments?: KeyOrderSegment[]): IrExpr; - run(body: IrStmt[]): IrFunction; + run(body: IrStmt[], name?: string): IrFunction; order(type: IrType & { kind: "record" }): string[] | undefined; } @@ -40,6 +41,8 @@ function fixture(): Fixture { keyOrder: notes, unions: new Map([["u0", { id: "u0", arms: [F64, { kind: "undefinedT" }] }]]), classes: new Map(), + methodAccessSites: new Map(), + dynConvertible: () => true, armTag: (unionId: string, arm: IrType): number => unionId === "u0" && arm.kind === "undefinedT" ? 1 : -1, recordFieldPresent: (obj: IrExpr, shapeId: string, field: string): IrExpr => ({ @@ -70,8 +73,8 @@ function fixture(): Fixture { segments ?? keys.map((name) => ({ kind: "key", name, definite: true })), ); }; - const run = (body: IrStmt[]): IrFunction => { - const fn: IrFunction = { name: "main", params: [], returnType: F64, locals: [], body, loc }; + const run = (body: IrStmt[], name = "main"): IrFunction => { + const fn: IrFunction = { name, params: [], returnType: F64, locals: [], body, loc }; applyKeyOrderWitnesses(lowerer, [fn]); return fn; }; @@ -700,3 +703,294 @@ test("a spread reads its source's order right after the literal evaluates the so const snapshot = (prologue[2] as IrStmt & { kind: "varDecl" }).localId; expect(orderIndexOf(lit)).toMatchObject({ kind: "varRef", localId: snapshot }); }); + +/** `{ a, b }` with a declared table `["a", "b"]` and a literal `{ b, a }` + * plus `{ ...value, a }` of the same shape, beside `extra`. */ +function selfCopy( + extra: (f: Fixture, ab: IrType & { kind: "record" }) => IrStmt[], + fnName = "main", +): string[] { + const f = fixture(); + const ab = f.record( + [ + ["a", F64], + ["b", F64], + ], + ["a", "b"], + ); + const self: KeyOrderSegment = { kind: "spread", shapeId: ab.shapeId, maybeAbsent: false }; + f.run( + [ + decl("v", f.literal(ab, ["b", "a"])), + observe(f.literal(ab, ["a", "b"], [self, { kind: "key", name: "a", definite: true }])), + ...extra(f, ab), + ], + fnName, + ); + return f.order(ab)!; +} + +test("a literal that starts by spreading its own shape takes the other literals' order", () => { + expect(selfCopy(() => [])).toEqual(["b", "a"]); +}); + +test("a self copy of a shape a checked conversion builds cannot be ordered statically", () => { + const parsed = (ab: IrType): IrExpr => ({ + kind: "dynCheck", + value: ref("j", { kind: "dyn" }), + type: ab, + loc, + }); + expect(selfCopy((_, ab) => [observe(parsed(ab))])).toEqual(["a", "b"]); +}); + +test("a replaceable method's checked result builds no record", () => { + expect( + selfCopy((f, ab) => { + const result: IrExpr = { kind: "dynCheck", value: ref("r", { kind: "dyn" }), type: ab, loc }; + return [observe(f.notes.methodResult(result, "load"))]; + }), + ).toEqual(["b", "a"]); +}); + +test("a replaceable method's checked result builds records when a replacement may return one", () => { + expect( + selfCopy((f, ab) => { + // A site the census cannot see through (a computed write of some + // value, a reflection call) may install a replacement returning a + // record built in any order. + (f.lowerer.methodAccessSites as Map).set("load", [ + { kind: ts.SyntaxKind.CallExpression } as unknown as ts.Node, + ]); + const result: IrExpr = { kind: "dynCheck", value: ref("r", { kind: "dyn" }), type: ab, loc }; + return [observe(f.notes.methodResult(result, "load"))]; + }), + ).toEqual(["a", "b"]); +}); + +/** A native callback taking the shape, boxed for JavaScript. */ +const boxedCallback = (ab: IrType): IrExpr => ({ + kind: "dynFrom", + value: { + kind: "closure", + fnName: "callback", + captures: [], + type: { kind: "func", params: [ab], ret: { kind: "void" } }, + loc, + }, + type: { kind: "dyn" }, + loc, +}); + +test("a native callback boxed for JavaScript receives records JavaScript builds", () => { + expect(selfCopy((_, ab) => [observe(boxedCallback(ab))])).toEqual(["a", "b"]); +}); + +test("a class's prototype slots box its methods without building their parameters", () => { + expect(selfCopy((_, ab) => [observe(boxedCallback(ab))], "%prototype.data.Service")).toEqual([ + "b", + "a", + ]); +}); + +test("a replaceable method's result keeps its witness through an IR rewrite", () => { + expect( + selfCopy((f, ab) => { + const result = f.notes.methodResult( + { + kind: "dynCheck", + value: ref("r", { kind: "dyn" }), + type: ab, + loc, + }, + "load", + ); + const identity = { expr: (e: IrExpr): IrExpr => e, stmt: (st: IrStmt): IrStmt => st }; + observeRecordLiteralRebuilds((from, to) => f.notes.follow(from, to)); + try { + const rebuilt = transformExpr(result, identity); + expect(rebuilt).not.toBe(result); + return [observe(rebuilt)]; + } finally { + observeRecordLiteralRebuilds(null); + } + }), + ).toEqual(["b", "a"]); +}); + +test("a checked conversion of a boxed primitive builds no record", () => { + expect( + selfCopy((_, ab) => { + const missing: IrExpr = { + kind: "dynFrom", + value: { kind: "unitLit", unit: "undefined", type: { kind: "undefinedT" }, loc }, + type: { kind: "dyn" }, + loc, + }; + return [observe({ kind: "dynCheck", value: missing, type: ab, loc })]; + }), + ).toEqual(["b", "a"]); +}); + +test("a checked conversion into a class builds none of its fields' records", () => { + expect( + selfCopy((f, ab) => { + (f.lowerer.classes as Map).set("Holder", { + def: { fields: [{ name: "value", type: ab }] }, + }); + const holder: IrType = { kind: "object", className: "Holder" }; + const checked: IrExpr = { + kind: "dynCheck", + value: ref("h", { kind: "dyn" }), + type: holder, + loc, + }; + return [{ kind: "exprStmt", expr: checked, loc }]; + }), + ).toEqual(["b", "a"]); +}); + +test("a checked function passes its record parameters out, and its callbacks' back in", () => { + const callable = (params: IrType[]): IrType => ({ + kind: "func", + params, + ret: { kind: "string" }, + }); + const checkedAs = (type: IrType): IrStmt => ({ + kind: "exprStmt", + expr: { kind: "dynCheck", value: ref("fn", { kind: "dyn" }), type, loc }, + loc, + }); + expect(selfCopy((_, ab) => [checkedAs(callable([ab]))])).toEqual(["b", "a"]); + // A callback parameter receives values the JavaScript function builds. + expect(selfCopy((_, ab) => [checkedAs(callable([callable([ab])]))])).toEqual(["a", "b"]); + // So does a container parameter, which the function can write into. + expect(selfCopy((_, ab) => [checkedAs(callable([{ kind: "array", elem: ab }]))])).toEqual([ + "a", + "b", + ]); +}); + +test("a generator resume builds only its result record, never the value it carries", () => { + const f = fixture(); + const ab = f.record( + [ + ["a", F64], + ["b", F64], + ], + ["a", "b"], + ); + const result = f.record( + [ + ["done", { kind: "bool" }], + ["value", ab], + ], + ["value", "done"], + ); + const gen: IrType = { + kind: "generator", + yieldT: ab, + retT: { kind: "void" }, + nextT: { kind: "undefinedT" }, + }; + const resume = (mode: "next" | "value", type: IrType): IrStmt => + observe({ kind: "genResume", mode, gen: ref("g", gen), arg: null, type, loc }); + f.run([ + observe(f.literal(ab, ["b", "a"])), + observe(f.literal(result, ["done", "value"])), + resume("value", ab), + resume("next", result), + ]); + expect(f.order(ab)).toEqual(["b", "a"]); + // The result record itself is built in its fixed layout: today's table. + expect(f.order(result)).toEqual(["value", "done"]); +}); + +/** `{ a, b }` built by a checked conversion and by the literal `{ b, a }`, + * copied by `{ ...value, a }` (evaluating the value into a binding, as + * spread literals do), observed. Answers the table and the function. */ +function checkedSelfCopy(): { order: string[]; fn: IrFunction; copy: IrExpr } { + const f = fixture(); + const ab = f.record( + [ + ["a", F64], + ["b", F64], + ], + ["a", "b"], + ); + const value = ref("value", ab) as IrExpr & { kind: "varRef" }; + const self: KeyOrderSegment = { kind: "spread", shapeId: ab.shapeId, maybeAbsent: false, value }; + const literal = f.literal(ab, ["a", "b"], [self, { kind: "key", name: "a", definite: true }]); + const copy: IrExpr = { + kind: "seqExpr", + stmts: [decl("value", { kind: "dynCheck", value: ref("j", { kind: "dyn" }), type: ab, loc })], + result: literal, + type: ab, + loc, + }; + const fn = f.run([observe(f.literal(ab, ["b", "a"])), observe(copy)]); + return { order: f.order(ab)!, fn, copy }; +} + +test("a spread of a value a checked conversion built carries the value's order over", () => { + const { order, fn, copy } = checkedSelfCopy(); + // Values the literal builds enumerate in its order; copies of converted + // values keep the order their record source holds. + expect(order).toEqual(["b", "a"]); + expect(copy).toMatchObject({ + kind: "seqExpr", + stmts: [ + { kind: "varDecl", localId: "value" }, + { kind: "varDecl", localId: "%orderedSpread.0", init: { kind: "recordLit" } }, + { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "record.sourceSpread", + args: [ + { kind: "varRef", localId: "%orderedSpread.0" }, + { kind: "varRef", localId: "value" }, + ], + }, + }, + ], + result: { kind: "varRef", localId: "%orderedSpread.0" }, + }); + expect(fn.locals.map((local) => local.id)).toEqual(["%orderedSpread.0"]); +}); + +test("a spread of a converted value into a wider shape orders the wider shape", () => { + const f = fixture(); + const ab = f.record( + [ + ["a", F64], + ["b", F64], + ], + ["a", "b"], + ); + const abc = f.record( + [ + ["a", F64], + ["b", F64], + ["c", F64], + ], + ["c", "a", "b"], + ); + const value = ref("value", ab) as IrExpr & { kind: "varRef" }; + const spread: KeyOrderSegment = { + kind: "spread", + shapeId: ab.shapeId, + maybeAbsent: false, + value, + }; + const wide: IrExpr = { + kind: "seqExpr", + stmts: [decl("value", { kind: "dynCheck", value: ref("j", { kind: "dyn" }), type: ab, loc })], + result: f.literal(abc, ["a", "b", "c"], [spread, { kind: "key", name: "c", definite: true }]), + type: abc, + loc, + }; + f.run([observe(f.literal(ab, ["b", "a"])), observe(wide)]); + expect(f.order(ab)).toEqual(["b", "a"]); + expect(f.order(abc)).toEqual(["b", "a", "c"]); +}); diff --git a/packages/compiler/src/frontend/lowering/key-order-witness.ts b/packages/compiler/src/frontend/lowering/key-order-witness.ts index aa84c68c2..a5e635e04 100644 --- a/packages/compiler/src/frontend/lowering/key-order-witness.ts +++ b/packages/compiler/src/frontend/lowering/key-order-witness.ts @@ -28,7 +28,25 @@ * build keeps today's table, since another table would make values * parsed in today's order keep a source view; a shape object literals * also build takes the literals' order. Their order is unknown, so a - * copy out of such a shape cannot be ordered statically; + * copy out of such a shape cannot be ordered statically. A conversion + * builds only the records it can create from a JavaScript value: none + * for a boxed primitive or a class instance (a native instance whose + * fields its own writes fill), none for a checked function's record + * parameters, and none for the checked result of a typed call of a + * method the program may replace on its instance. Without a replacement + * that result is the method's own native value, boxed and checked back + * unchanged; a replacement's result keeps its own order in its record + * source, and only copies out of it follow the chosen table, as they + * followed today's; + * - a literal that starts by spreading a value of its own shape copies + * that value's order. While no checked conversion builds the shape, + * every value has the one chosen order, so the copy adds no witness; + * - a literal that starts by spreading a value of a shape checked + * conversions build (an ordered spread) gives the record it builds the + * value's record source, when the value has one (record.sourceSpread). + * Every value of a shape enumerates in its table or in the order its + * record source holds, so the copy has the value's order either way, + * and the literal resolves as if the value were in its table's order; * - a write to an optional key that may be absent appends it in Node. A * key no construction creates ("late-only") goes after every key a * construction can create; any other moving write goes after every key. @@ -49,6 +67,7 @@ import { shapeHasAccessorSlots, typeEquals, UNDEFINED_T, + VOID, } from "../../ir/ir.js"; import { everyExprChild, @@ -57,7 +76,9 @@ import { transformStmtList, } from "../../ir/traverse.js"; import type { Lowerer } from "./lowerer.js"; +import * as ts from "../ts7/adapter.js"; import { ALWAYS_THROW_LIB_FNS } from "../../ir/builtin-effects.js"; +import { recordMayKeepSource } from "./record-sources.js"; /** The statements a key-order helper runs for record `ref`: `walks[0]`, * its walk over the shape's table, or, for a shape with orderVariants, a @@ -112,6 +133,7 @@ export type KeyOrderSegment = export class KeyOrderNotes { private readonly literals = new Map(); private readonly copies = new Map(); + private readonly methodResults = new Map(); /** Shapes a frontend order surface (keys, values/entries, inspect, * index-signature merges) walks. */ readonly observed = new Set(); @@ -128,13 +150,22 @@ export class KeyOrderNotes { return node; } + /** The checked result of a typed call of method `name`, which the program + * may replace on its instance (class-callbacks.ts classCallbackCall). */ + methodResult(node: IrExpr, name: string): IrExpr { + this.methodResults.set(node, name); + return node; + } + /** A pass rebuilt `from` as `to` (traverse.ts mapExprChildren): the - * rebuilt literal is the same construction, so it keeps the witness. */ + * rebuilt node is the same construction, so it keeps the witness. */ follow(from: IrExpr, to: IrExpr): void { const segments = this.literals.get(from); if (segments !== undefined) this.literals.set(to, segments); const source = this.copies.get(from); if (source !== undefined) this.copies.set(to, source); + const method = this.methodResults.get(from); + if (method !== undefined) this.methodResults.set(to, method); } observe(type: IrType): void { @@ -148,6 +179,11 @@ export class KeyOrderNotes { copyOf(node: IrExpr): string | undefined { return this.copies.get(node); } + + /** The method whose checked result `node` is, if it is one. */ + methodResultOf(node: IrExpr): string | undefined { + return this.methodResults.get(node); + } } const isArrayIndex = (name: string): boolean => { @@ -190,6 +226,14 @@ interface ShapeFacts { type Resolved = { order: string[] } | "unknown"; +/** A literal that starts with `...value`: the sequence that evaluates the + * value into its binding and then builds the literal, and its function. */ +interface LeadingSpread { + wrapper: IrExpr & { kind: "seqExpr" }; + literal: IrExpr & { kind: "recordLit" }; + fn: IrFunction; +} + /** `structuredClone(v)` checked back into v's own type: the copy is built * from v's dyn view, so it creates exactly v's keys in v's order. */ function orderPreservingClone(node: IrExpr & { kind: "dynCheck" }): boolean { @@ -201,6 +245,82 @@ function orderPreservingClone(node: IrExpr & { kind: "dynCheck" }): boolean { ); } +const PRIMITIVE_KINDS = new Set([ + "f64", + "bigint", + "string", + "bool", + "undefinedT", + "nullT", + "symbol", +]); + +/** A checked conversion of a boxed primitive (the `undefined` a dynamic + * construction passes for a missing argument) answers that primitive or + * throws: it builds no record. */ +function primitiveSource(node: IrExpr & { kind: "dynCheck" }): boolean { + return node.value.kind === "dynFrom" && PRIMITIVE_KINDS.has(node.value.value.type.kind); +} + +/** Whether a census site for a method name can only read the method or + * install a replacement whose result holds no object: a read + * (`C.prototype.m`, `obj.m`) outside an assignment target, or an assignment + * of a function expression whose return type is primitive. Other sites (a + * computed write of another value, a reflection call, a property bag) may + * install a replacement that returns a record. */ +function replacesWithoutRecords(lowerer: Lowerer, site: ts.Node): boolean { + const parent = site.parent; + if (ts.isPropertyAccessExpression(site) || ts.isElementAccessExpression(site)) + return !( + parent !== undefined && + ts.isBinaryExpression(parent) && + parent.left === site && + parent.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && + parent.operatorToken.kind <= ts.SyntaxKind.LastAssignment + ); + if (!ts.isBinaryExpression(site) || site.operatorToken.kind !== ts.SyntaxKind.EqualsToken) + return false; + let value = site.right; + while (ts.isParenthesizedExpression(value)) value = value.expression; + if (!ts.isArrowFunction(value) && !ts.isFunctionExpression(value)) return false; + const signatures = lowerer.checker.getCallSignatures(lowerer.typeOf(value)); + return ( + signatures.length > 0 && + signatures.every((signature) => + primitiveOnly(lowerer.checker.getReturnTypeOfSignature(signature)), + ) + ); +} + +/** A compiler helper that boxes a class's own state for JavaScript's reads: + * its prototype data and the property and method dispatch over boxed + * instances. */ +function boxesOwnState(fnName: string): boolean { + return ( + fnName.startsWith("%prototype.data.") || + fnName.startsWith("%dyn.class.") || + fnName.startsWith("%class.") + ); +} + +/** A type whose values are primitives (or void): it holds no record. */ +function primitiveOnly(type: ts.Type): boolean { + if (type.isUnionType()) return ts.constituentTypes(type).every(primitiveOnly); + return ( + (type.flags & + (ts.TypeFlags.StringLike | + ts.TypeFlags.NumberLike | + ts.TypeFlags.BooleanLike | + ts.TypeFlags.BigIntLike | + ts.TypeFlags.ESSymbolLike | + ts.TypeFlags.Null | + ts.TypeFlags.Undefined | + ts.TypeFlags.Void | + ts.TypeFlags.Never)) !== + 0 + ); +} + /** Chooses each in-scope record shape's enumeration table from its * witnesses. Runs once retained reachability has settled the historical * tables and derived metadata, before the order-snapshotting helper bodies @@ -239,35 +359,70 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio /** Every record shape a type can hold, through fields, arms, elements, * callables, promises and class fields. */ /** Each walk shares one `seen` set per result set, so every type graph - * is walked once however many roots reach it. */ - const shapesIn = (type: IrType, out: Set, seen: Set): void => { - const visit = (t: IrType): boolean => { + * is walked once however many roots reach it. + * + * `produced` walks only what a checked conversion into the type can + * build. It answers an existing native instance for a class type (a plain + * object cannot acquire a class brand; only %Error rebuilds), so the + * records the instance's fields hold come from the class's own writes and + * its checked field setters, each a witness of its own. A checked + * function's parameters receive native values: a record parameter is + * passed out, never built, though a callback among them receives values + * built from JavaScript again. `built` false starts the walk at a value + * passed out (a native value boxed for JavaScript); `writes` false leaves + * out what JavaScript could write into the containers passed out, so the + * walk counts only what the functions among them receive. */ + const shapesIn = ( + type: IrType, + out: Set, + seen: Set, + produced = false, + built = true, + writes = true, + ): void => { + const visit = (t: IrType, built: boolean): boolean => { + const mark = built ? "" : "-"; + // A container JavaScript builds converts into fresh native storage, so + // its children are built too. One passed out stays shared: JavaScript + // writes into it (built children) and reads native values out of it, + // such as callbacks whose parameters it then builds (children passed + // out). Those children count in both directions. + const child = (type: IrType): boolean => { + if (built || writes) visit(type, true); + if (!built) visit(type, false); + return true; + }; if (t.kind === "record") { - if (seen.has("r:" + t.shapeId)) return true; - seen.add("r:" + t.shapeId); - out.add(t.shapeId); + if (seen.has(mark + "r:" + t.shapeId)) return true; + seen.add(mark + "r:" + t.shapeId); + if (built) out.add(t.shapeId); const shape = shapes.get(t.shapeId); if (shape) { - for (const f of shape.fields) visit(f.type); - if (shape.indexValue) visit(shape.indexValue); + for (const f of shape.fields) child(f.type); + if (shape.indexValue) child(shape.indexValue); } return true; } if (t.kind === "union") { - if (seen.has("u:" + t.unionId)) return true; - seen.add("u:" + t.unionId); - for (const arm of lowerer.unions.get(t.unionId)?.arms ?? []) visit(arm); + if (seen.has(mark + "u:" + t.unionId)) return true; + seen.add(mark + "u:" + t.unionId); + for (const arm of lowerer.unions.get(t.unionId)?.arms ?? []) visit(arm, built); return true; } if (t.kind === "object") { - if (seen.has("c:" + t.className)) return true; - seen.add("c:" + t.className); - for (const f of lowerer.classes.get(t.className)?.def.fields ?? []) visit(f.type); + if (produced && t.className !== "%Error") return true; + if (seen.has(mark + "c:" + t.className)) return true; + seen.add(mark + "c:" + t.className); + for (const f of lowerer.classes.get(t.className)?.def.fields ?? []) child(f.type); return true; } - return everyTypeChild(t, visit); + if (produced && t.kind === "func") { + for (const param of t.params) visit(param, !built); + return visit(t.ret, built); + } + return everyTypeChild(t, child); }; - visit(type); + visit(type, built); }; const opaqueShapes = new Set(); const opaqueSeen = new Set(); @@ -278,14 +433,29 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio * declaration's order, the order their table holds. */ const declaredShapes = new Set(); const declaredSeen = new Set(); + const boxedSeen = new Set(); + /** The checked result of a replaceable method call while every site that + * can replace the method installs functions that return no record. */ + const unchangedMethodResult = (node: IrExpr): boolean => { + const name = notes.methodResultOf(node); + if (name === undefined) return false; + const sites = lowerer.methodAccessSites.get(name) ?? []; + return sites.every((site) => replacesWithoutRecords(lowerer, site)); + }; const producedBy = (type: IrType, opaque: boolean): void => { if (opaque) shapesIn(type, opaqueShapes, opaqueSeen); - else shapesIn(type, runtimeShapes, runtimeSeen); + else shapesIn(type, runtimeShapes, runtimeSeen, true); }; // ── collection ────────────────────────────────────────────────────── let pos = 0; let fnName = ""; + let currentFn: IrFunction | null = null; + /** Literals that start by spreading a binding, by their witness: where + * each one is built. A literal's notes are shared by its rebuilt copies, + * so `literalSites` counts every literal of each witness. */ + const leadingSpreads = new Map(); + const literalSites = new Map(); const loops: number[] = []; let loopIds = 0; const loopKey = (): string => loops.join("/"); @@ -337,8 +507,10 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio } const segments = notes.literalOf(e); const copy = notes.copyOf(e); - if (segments !== undefined) f.literals.push(segments); - else if (copy !== undefined) f.copies.push({ source: copy, keys: presentKeys }); + if (segments !== undefined) { + f.literals.push(segments); + literalSites.set(segments, (literalSites.get(segments) ?? 0) + 1); + } else if (copy !== undefined) f.copies.push({ source: copy, keys: presentKeys }); else f.pinned = true; return; } @@ -353,9 +525,31 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio case "assignExpr": noteInit(e.localId, e.value); return; + case "seqExpr": { + // A spread literal evaluates its sources into bindings first. + const literal = e.result; + if (literal.kind !== "recordLit" || currentFn === null) return; + const segments = notes.literalOf(literal); + const first = segments?.[0]; + if (segments === undefined || first?.kind !== "spread" || first.value === undefined) return; + const list = leadingSpreads.get(segments) ?? []; + list.push({ wrapper: e, literal, fn: currentFn }); + leadingSpreads.set(segments, list); + return; + } case "jsonStringify": case "dynFrom": observedRoots.push(e.value.type); + // A native value the program hands to JavaScript: the callbacks it + // holds (itself, a field, an element) receive records JavaScript + // builds. The helpers that box the program's own state for + // JavaScript's reads (a class's prototype slots and property + // dispatch) hand out the class's methods, whose arguments the + // program's own calls pass as native values, as a replaceable + // method's result above. Writes through the boxed value are left + // out: reading a boxed value creates no record. + if (!boxesOwnState(fnName)) + shapesIn(e.value.type, runtimeShapes, boxedSeen, true, false, false); return; case "recordKeyGet": observedRoots.push(e.obj.type); @@ -380,15 +574,31 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio case "strIntrinsic": case "regexIntrinsic": case "bytesIntrinsic": - // Generator results ({ done, value }) and Promise.withResolvers - // records are built by the backend in a fixed layout. - case "genResume": + // Promise.withResolvers records are built by the backend in a fixed + // layout. case "promiseWithResolvers": producedBy(e.type, true); return; + case "genResume": { + // A resume builds at most its IteratorResult record ({ done, value } + // in a fixed layout; an async generator's settles a promise of it). + // The value it carries is the generator's own yielded or returned + // value, which the generator's body constructs. The record-free + // drives build nothing: `value` moves that value out, and step, + // close and a record-free delegate answer a boolean or nothing. + if (e.mode === "step" || e.mode === "value" || e.mode === "close") return; + const result = e.type.kind === "promise" ? e.type.inner : e.type; + if (result.kind === "record") opaqueShapes.add(result.shapeId); + return; + } case "dynCheck": if (e.adopt === true) shapesIn(e.type, declaredShapes, declaredSeen); - else if (!orderPreservingClone(e)) producedBy(e.type, false); + // Conversions that build no record: a boxed primitive, and a + // replaceable method's result while no replacement the program can + // install returns a record: then it is the method's own value, + // boxed and checked back unchanged. + else if (!orderPreservingClone(e) && !primitiveSource(e) && !unchangedMethodResult(e)) + producedBy(e.type, false); return; case "dynDestrCheck": case "caughtCheck": @@ -440,6 +650,7 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio }; for (const fn of fns) { fnName = fn.name; + currentFn = fn; for (const st of fn.body) stmt(st); } for (const id of opaqueShapes) { @@ -556,6 +767,61 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio return edges; }; + // ── ordered spreads ───────────────────────────────────────────────── + // A value a checked conversion builds keeps its source's key order in + // its record source when that order is not the table's (record- + // sources.ts), so every value of a shape enumerates in the table's order + // or in the order its record source holds. A literal that starts with + // `...value` creates the value's keys first, in the value's order. When + // the value's shape is one checked conversions build, the literal + // carries that order over: after it builds the record it gives the + // record a source holding the value's order (record.sourceSpread), and + // the keys the value lacks follow in the table's order, which every + // literal sequence of the shape agrees with. Such a literal resolves as + // if its source were in the source's table order, and its own shape + // counts as built at run time too, since its values may now carry a + // source. It qualifies when every value of the source has all of the + // source's keys, both shapes can keep a source, and every later spread + // whose values may carry a source only overwrites the source's keys + // (its own order cannot move them). + const orderedSpreads = new Set(); + const keepsSource = (shapeId: string): boolean => + recordMayKeepSource(lowerer, { kind: "record", shapeId }); + const userKeys = (shapeId: string): string[] => + (shapes.get(shapeId)?.fields ?? []).map((x) => x.name).filter((k) => !k.startsWith("%")); + const ordersSpread = (segments: KeyOrderSegment[], sites: readonly LeadingSpread[]): boolean => { + const first = segments[0]; + if (first?.kind !== "spread" || first.maybeAbsent || first.value === undefined) return false; + const sourceId = first.shapeId; + if (facts.get(sourceId)?.runtime !== true || sites.length !== literalSites.get(segments)) + return false; + const target = sites[0]!.literal.type; + if (target.kind !== "record" || !keepsSource(sourceId) || !keepsSource(target.shapeId)) + return false; + const localId = first.value.localId; + const bound = ({ wrapper, literal }: LeadingSpread): boolean => + typeEquals(literal.type, target) && + wrapper.stmts.some( + (st) => + st.kind === "varDecl" && + st.localId === localId && + st.init !== null && + st.init.type.kind === "record" && + st.init.type.shapeId === sourceId, + ); + if (!sites.every(bound)) return false; + const sourceKeys = new Set(userKeys(sourceId)); + if ([...sourceKeys].some((k) => maybeAbsent(sourceId, k))) return false; + return segments + .slice(1) + .every( + (s) => + s.kind === "key" || + (s.kind === "spread" && + (facts.get(s.shapeId)?.runtime !== true || + userKeys(s.shapeId).every((k) => sourceKeys.has(k)))), + ); + }; // ── witness sequences ─────────────────────────────────────────────── /** The key sequences one literal can create, or null when a spread * source's order is unknown. A key sits at its first occurrence that @@ -565,15 +831,19 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio segments: readonly KeyOrderSegment[], keySet: ReadonlySet, resolved: (id: string) => Resolved, + raw: (id: string) => Resolved, ): string[][] | null => { const occurrences: { key: string; definite: boolean }[] = []; + const ordered = orderedSpreads.has(segments); for (const s of segments) { if (s.kind === "opaque") return null; if (s.kind === "key") { if (keySet.has(s.name)) occurrences.push({ key: s.name, definite: s.definite }); continue; } - const source = resolved(s.shapeId); + // An ordered spread's values without a source enumerate in the + // source's table; the others carry their order over. + const source = ordered && s === segments[0] ? raw(s.shapeId) : resolved(s.shapeId); if (source === "unknown") return null; for (const k of source.order) { if (keySet.has(k)) @@ -649,8 +919,35 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio for (const c of f.copies) deps.add(c.source); return [...deps]; }; + /** `{ ...value, ...rest }` that starts with a spread of a value of the + * literal's own shape holding every key: it creates its keys in the + * value's order, and the later segments only overwrite them. With no + * checked conversion building the shape, every value enumerates in the + * one table this pass chooses, so the copy agrees with any choice and + * adds no edge. */ + const copiesItself = ( + id: string, + f: ShapeFacts, + table: readonly string[], + segments: readonly KeyOrderSegment[], + ): boolean => { + const first = segments[0]; + return ( + first !== undefined && + first.kind === "spread" && + first.shapeId === id && + !first.maybeAbsent && + (!f.runtime || orderedSpreads.has(segments)) && + segments.every((s) => s.kind !== "opaque") && + table.every((k) => !maybeAbsent(id, k)) + ); + }; /** One shape's order given its sources' current orders. */ - const resolveOne = (id: string, current: (source: string) => Resolved): Resolved => { + const resolveOne = ( + id: string, + current: (source: string) => Resolved, + raw: (source: string) => Resolved, + ): Resolved => { const shape = shapes.get(id); if (!inScope(shape)) return "unknown"; const table = tableOf(shape); @@ -666,7 +963,8 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio const edges: [string, string][] = []; if (f.pinned) chain(table, edges); for (const segments of f.literals) { - const sequences = literalSequences(segments, keySet, current); + if (copiesItself(id, f, table, segments)) continue; + const sequences = literalSequences(segments, keySet, current, raw); if (sequences === null) return "unknown"; for (const seq of sequences) chain(seq, edges); } @@ -725,32 +1023,57 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio const known = result.get(id); return known === undefined ? "unknown" : asSource(id, known); }; - for (const scc of sccs) { - const members = new Set(scc); - const guess = new Map(); - for (const id of scc) { - const shape = shapes.get(id); - guess.set(id, inScope(shape) ? { order: tableOf(shape) } : "unknown"); - } - const lookup = (id: string): Resolved => - members.has(id) ? asSource(id, guess.get(id)!) : external(id); - let stable = false; - for (let round = 0; round < scc.length + 3 && !stable; round++) { - stable = true; + const resolveAll = (): void => { + result.clear(); + late.clear(); + for (const scc of sccs) { + const members = new Set(scc); + const guess = new Map(); for (const id of scc) { - const next = resolveOne(id, lookup); - const prev = guess.get(id)!; - const same = - next === "unknown" - ? prev === "unknown" - : prev !== "unknown" && prev.order.join("\0") === next.order.join("\0"); - if (!same) { - stable = false; - guess.set(id, next); + const shape = shapes.get(id); + guess.set(id, inScope(shape) ? { order: tableOf(shape) } : "unknown"); + } + const lookup = (id: string): Resolved => + members.has(id) ? asSource(id, guess.get(id)!) : external(id); + const rawLookup = (id: string): Resolved => + members.has(id) ? guess.get(id)! : (result.get(id) ?? "unknown"); + let stable = false; + for (let round = 0; round < scc.length + 3 && !stable; round++) { + stable = true; + for (const id of scc) { + const next = resolveOne(id, lookup, rawLookup); + const prev = guess.get(id)!; + const same = + next === "unknown" + ? prev === "unknown" + : prev !== "unknown" && prev.order.join("\0") === next.order.join("\0"); + if (!same) { + stable = false; + guess.set(id, next); + } } } + for (const id of scc) result.set(id, stable ? guess.get(id)! : "unknown"); } - for (const id of scc) result.set(id, stable ? guess.get(id)! : "unknown"); + }; + // Ordered spreads join once their source resolves; each one can let its + // own shape resolve, which a later spread of it needs. + resolveAll(); + for (let grew = true; grew;) { + grew = false; + for (const [segments, sites] of leadingSpreads) { + const first = segments[0]; + if (first?.kind !== "spread" || orderedSpreads.has(segments)) continue; + // A self copy resolves with its own shape. + const target = sites[0]!.literal.type; + const source = result.get(first.shapeId) ?? "unknown"; + const self = target.kind === "record" && target.shapeId === first.shapeId; + if ((source === "unknown" && !self) || !ordersSpread(segments, sites)) continue; + orderedSpreads.add(segments); + if (target.kind === "record") factsOf(target.shapeId).runtime = true; + grew = true; + } + if (grew) resolveAll(); } // ── observation ───────────────────────────────────────────────────── @@ -759,6 +1082,19 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio for (const id of notes.observed) shapesIn({ kind: "record", shapeId: id }, observed, observedSeen); for (const root of observedRoots) shapesIn(root, observed, observedSeen); + // An ordered spread's copies of values without a source take the source's + // order from its table, which must then hold the resolved order. + const orderedTarget = (sites: readonly LeadingSpread[]): string | null => { + const target = sites[0]!.literal.type; + return target.kind === "record" && (result.get(target.shapeId) ?? "unknown") !== "unknown" + ? target.shapeId + : null; + }; + for (const [segments, sites] of leadingSpreads) { + const first = segments[0]; + if (orderedSpreads.has(segments) && first?.kind === "spread" && orderedTarget(sites) !== null) + observed.add(first.shapeId); + } // ── apply ─────────────────────────────────────────────────────────── for (const [id, resolved] of result) { @@ -771,6 +1107,46 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio notes.changed.add(id); } + // Ordered spreads carry their source's order over (see above). + let orderedResults = 0; + const rewritten = new Set(); + for (const [segments, sites] of leadingSpreads) { + const first = segments[0]; + if (!orderedSpreads.has(segments) || first?.kind !== "spread" || first.value === undefined) + continue; + // A shape that did not resolve keeps today's table for every value. + if (orderedTarget(sites) === null) continue; + for (const { wrapper, literal, fn } of sites) { + if (rewritten.has(wrapper)) continue; + rewritten.add(wrapper); + const loc = literal.loc; + const localId = `%orderedSpread.${orderedResults++}`; + fn.locals.push({ id: localId, name: "%orderedSpread", type: literal.type, mutable: false }); + const built: IrExpr = { kind: "varRef", localId, type: literal.type, loc }; + const source: IrExpr = { + kind: "varRef", + localId: first.value.localId, + type: { kind: "record", shapeId: first.shapeId }, + loc, + }; + wrapper.stmts.push( + { kind: "varDecl", localId, init: literal, loc }, + { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "record.sourceSpread", + args: [built, source], + type: VOID, + loc, + }, + loc, + }, + ); + wrapper.result = { kind: "varRef", localId, type: literal.type, loc }; + } + } + // ── per-instance orders ───────────────────────────────────────────── // A shape whose literals write its keys in more than one order has no // one table. Each value then records which of the orders it was written diff --git a/packages/compiler/src/frontend/lowering/lib-boundary.ts b/packages/compiler/src/frontend/lowering/lib-boundary.ts index 5be10e997..8a264d346 100644 --- a/packages/compiler/src/frontend/lowering/lib-boundary.ts +++ b/packages/compiler/src/frontend/lowering/lib-boundary.ts @@ -57,7 +57,7 @@ import { isUnitType, typeEquals, } from "../../ir/ir.js"; -import { LIB_FN_SIGS } from "../../ir/builtin-signatures.js"; +import { libFnSignatures } from "../../ir/builtin-signatures.js"; import { REGEX_INTRINSIC_SIGS, STR_INTRINSIC_SIGS } from "../../ir/intrinsic-signatures.js"; import { unionMismatchDiag, unsupportedDiag } from "../../diagnostics/diagnostic.js"; @@ -162,7 +162,7 @@ function enforceExprBoundary(lowerer: Lowerer, node: IrExpr): void { const kind = node.kind; if (kind === "libCall") { const e = node; - const sig = LIB_FN_SIGS[e.fn]; + const sig = libFnSignatures()[e.fn]; if (!sig) return; e.args.forEach((a, i) => { const want = sig.argTypes[i]; diff --git a/packages/compiler/src/frontend/lowering/lower-array-sort.ts b/packages/compiler/src/frontend/lowering/lower-array-sort.ts index 5521d30ef..62507ddcd 100644 --- a/packages/compiler/src/frontend/lowering/lower-array-sort.ts +++ b/packages/compiler/src/frontend/lowering/lower-array-sort.ts @@ -926,6 +926,10 @@ export function buildArraySortFn( loc: SrcLoc, native = false, keyed: { order: number; key: boolean; keys: number } | null = null, + /** The throw for a foreign element (SCR_ARR_FOREIGN) at `index` of + * `arr`, which the comparator's element type cannot hold; for element + * types whose arrays can hold one. */ + checkForeign: ((arr: IrExpr, index: IrExpr) => IrExpr) | null = null, ): IrFunction { const b = new SortIr(loc); const arrT = arrayOf(elem), @@ -1035,7 +1039,21 @@ export function buildArraySortFn( b.branch( b.eq(state, b.num(1)), [compactValue()], - [b.branch(b.eq(state, b.num(2)), [b.step(undefinedCount)])], + [ + b.branch( + b.eq(state, b.num(2)), + [b.step(undefinedCount)], + checkForeign + ? [ + // A foreign element does not fit the comparator's element + // type: the checked conversion's TypeError. + b.branch(b.eq(state, b.num(3)), [ + { kind: "exprStmt", expr: checkForeign(src, index), loc }, + ]), + ] + : null, + ), + ], ), ]), // Sort only the compacted prefix. The tail is overwritten with undefined diff --git a/packages/compiler/src/frontend/lowering/lower-assert.ts b/packages/compiler/src/frontend/lowering/lower-assert.ts index bc51a9382..02faa7ade 100644 --- a/packages/compiler/src/frontend/lowering/lower-assert.ts +++ b/packages/compiler/src/frontend/lowering/lower-assert.ts @@ -51,11 +51,15 @@ import { STRING, type SrcLoc, VOID, + isDynTypedRefType, isUnitType, typeEquals, typeKey, } from "../../ir/ir.js"; import { boolLit, countedFor, dynUndefinedExpr, numLit, strLit, varRef } from "../../ir/build.js"; +import { everyExpr } from "../../ir/traverse.js"; +import { checkedClassInstanceOf } from "./class-construction.js"; +import { isJsSourceFile } from "../program.js"; /** node:assert/strict binds the loose NAMES to the strict comparisons — * Node's own aliasing (`strict.equal === assert.strictEqual`). */ @@ -73,6 +77,63 @@ function canonicalAssertMember(module: string, member: string): string { return member; } +/** The assert methods that are function values (scr_assert_method_value). */ +const ASSERT_FUNCTION_VALUES: ReadonlySet = new Set([ + "ok", + "fail", + "equal", + "notEqual", + "strictEqual", + "notStrictEqual", + "deepEqual", + "notDeepEqual", + "deepStrictEqual", + "notDeepStrictEqual", + "partialDeepStrictEqual", + "ifError", +]); + +/** The assert method a `const` binding holds (`const ok = assert.ok`, + * `const eq = strictEqual`), or null. */ +export function assertFunctionAliasOf( + lowerer: Lowerer, + ident: ts.Identifier, +): { module: string; member: string } | null { + const decl = lowerer.checker.getSymbolAtLocation(ident)?.valueDeclaration; + if ( + !decl || + !ts.isVariableDeclaration(decl) || + !ts.isIdentifier(decl.name) || + !ts.isVariableDeclarationList(decl.parent) || + (decl.parent.flags & ts.NodeFlags.Const) === 0 || + !decl.initializer + ) + return null; + let init = decl.initializer; + while (ts.isParenthesizedExpression(init)) init = init.expression; + const bi = ts.isPropertyAccessExpression(init) + ? lowerer.builtinMemberOf(init) + : ts.isIdentifier(init) + ? lowerer.builtinImportOf(init) + : null; + if (!bi || (bi.module !== "assert" && bi.module !== "assert/strict")) return null; + return ASSERT_FUNCTION_VALUES.has(canonicalAssertMember(bi.module, bi.member)) ? bi : null; +} + +/** An assert method read as a value (`emitter.on("x", assert.fail)`, + * `const eq = assert.strictEqual`): an untyped function that asserts as + * the method does, the same value for every read. Null for other + * members. */ +export function assertFunctionValue( + bi: { module: string; member: string }, + loc: SrcLoc, +): IrExpr | null { + if (bi.module !== "assert" && bi.module !== "assert/strict") return null; + const member = canonicalAssertMember(bi.module, bi.member); + if (!ASSERT_FUNCTION_VALUES.has(member)) return null; + return { kind: "libCall", fn: "assert.methodValue", args: [strLit(member, loc)], type: DYN, loc }; +} + /* Node's AssertionError carries actual, expected and operator. Where the * verdict is computed in compiled code, the failure branch records them * (assert.record) before the throwing call, so a passing assertion boxes @@ -166,28 +227,156 @@ function requireStatementPosition(lowerer: Lowerer, call: ts.CallExpression, wha ); } +/** The message the runtime reads from a registered value + * (SCR_ASSERT_VALUE_MESSAGE in scr_assert.h). */ +const VALUE_MESSAGE = "\u0001scriptc:assert-message-value"; + +/** A message argument that is not a string, met while one assertion + * lowers (withValueMessage). `fail` marks assert.fail's message, which + * Node uses for every value but null and undefined. */ +let valueMessage: { value: IrExpr; has: string; text: string; fail: boolean } | null = null; + /** The optional trailing message argument: Node accepts a string or an * Error (throwing the Error itself) — the STRING form lowers; a literal * `undefined` counts as omitted (Node's `message == null` test). Returns * the libCall's (msg, hasMsg) pair — a "" dummy when omitted, so the - * runtime can distinguish an omitted message from an empty one. */ + * runtime can distinguish an omitted message from an empty one. Any other + * value that crosses into the checked-dynamic tree (an untyped + * `failures.join('')`, an Error) is registered with the runtime at the call + * instead (withValueMessage). */ function lowerMessageArg( lowerer: Lowerer, node: ts.Expression | undefined, loc: SrcLoc, + fail = false, ): { msg: IrExpr; hasMsg: IrExpr } { if (!node || (ts.isIdentifier(node) && node.text === "undefined")) { return { msg: { kind: "strLit", value: "", type: STRING, loc }, hasMsg: boolLit(false, loc) }; } const msg = lowerer.lowerExpr(node); - if (msg.type.kind !== "string") { + if (msg.type.kind === "string") return { msg, hasMsg: boolLit(true, loc) }; + const value = + valueMessage === null && (msg.type.kind === "dyn" || lowerer.dynConvertible(msg.type)) + ? lowerer.coerceToExpected(msg, DYN) + : null; + if (value === null || value.type.kind !== "dyn") { lowerer.noLowering( `assert messages of type '${lowerer.fmt(msg.type)}'`, node, "only string messages lower (Node also accepts an Error to throw — construct and throw it directly)", ); } - return { msg, hasMsg: boolLit(true, loc) }; + const has = lowerer.declareHiddenLocal("assert.hasMessage", BOOL); + const text = lowerer.declareHiddenLocal("assert.messageText", STRING); + valueMessage = { value, has: has.id, text: text.id, fail }; + return { msg: varRef(text.id, STRING, loc), hasMsg: varRef(has.id, BOOL, loc) }; +} + +/** Lowers one assertion; a message value met on the way is evaluated after + * the assertion's other arguments, as Node evaluates them. A null or + * undefined message is omitted (Node's `message == null` test); a truthy + * one is registered with the runtime, which the failure reads; any other + * value is the empty message, which Node's comparison diffs treat as no + * message too (createErrDiff tests the message's truthiness). */ +function withValueMessage( + lowerer: Lowerer, + expr: ts.CallExpression, + loc: SrcLoc, + lower: () => IrExpr | null, +): IrExpr | null { + const outer = valueMessage; + valueMessage = null; + try { + const lowered = lower(); + const message = valueMessage as { + value: IrExpr; + has: string; + text: string; + fail: boolean; + } | null; + if (lowered === null || message === null) return lowered; + // assert.ok's statements evaluate the value first; its call is last. + const before = lowered.kind === "seqExpr" ? lowered.stmts : []; + const call = lowered.kind === "seqExpr" ? lowered.result : lowered; + if (call.kind !== "libCall") { + lowerer.noLowering( + "assert messages that are not strings in this assertion form", + expr, + "pass a string message", + ); + } + const stmts: IrStmt[] = [...before]; + // An argument computed from the message (assert.fail's generated flag) + // reads it after the declarations below; the others are evaluated first. + const readsMessage = (arg: IrExpr): boolean => + !everyExpr(arg, { + stmt: () => true, + expr: (e) => + !(e.kind === "varRef" && (e.localId === message.has || e.localId === message.text)), + }); + const args = call.args.map((arg, index) => + readsMessage(arg) ? arg : stableValue(lowerer, stmts, arg, `assert.arg${index}`, loc), + ); + const local = lowerer.declareHiddenLocal("assert.message", DYN); + const value = varRef(local.id, DYN, loc); + // The runtime reports a registered value: a truthy one for the other + // methods, any value but null and undefined for assert.fail (Node's + // `actual == null` test), whose other values read "Failed". + const registered: IrExpr = message.fail + ? varRef(message.has, BOOL, loc) + : { kind: "dynTest", test: "truthy", value, type: BOOL, loc }; + stmts.push( + { kind: "varDecl", localId: local.id, init: message.value, loc }, + { + kind: "varDecl", + localId: message.has, + init: { kind: "dynTest", test: "nullish", negated: true, value, type: BOOL, loc }, + loc, + }, + { + kind: "varDecl", + localId: message.text, + init: { + kind: "ternary", + cond: registered, + then: strLit(VALUE_MESSAGE, loc), + else_: strLit(message.fail ? "Failed" : "", loc), + type: STRING, + loc, + }, + loc, + }, + { + kind: "if", + cond: registered, + then: [ + { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "assert.messageValue", + args: [value, { kind: "dynTest", test: "error", value, type: BOOL, loc }], + type: VOID, + loc, + }, + loc, + }, + ], + else_: null, + loc, + }, + { kind: "exprStmt", expr: { ...call, args }, loc }, + ); + return { + kind: "seqExpr", + stmts, + result: { kind: "libCall", fn: "assert.messageClear", args: [], type: VOID, loc }, + type: VOID, + loc, + }; + } finally { + valueMessage = outer; + } } /** The module-function dispatch — the spoke's entry, called from both the @@ -201,6 +390,15 @@ export function lowerAssertModuleCall( loc: SrcLoc, ): IrExpr | null { if (bi.module !== "assert" && bi.module !== "assert/strict") return null; + return withValueMessage(lowerer, expr, loc, () => lowerAssertMember(lowerer, expr, bi, loc)); +} + +function lowerAssertMember( + lowerer: Lowerer, + expr: ts.CallExpression, + bi: { module: string; member: string }, + loc: SrcLoc, +): IrExpr | null { switch (canonicalAssertMember(bi.module, bi.member)) { case "ok": return lowerAssertOk(lowerer, expr, loc); @@ -880,7 +1078,7 @@ export function lowerAssertDirectCall( 'use the default import: import assert from "node:assert")', ); } - return lowerAssertOk(lowerer, expr, loc); + return withValueMessage(lowerer, expr, loc, () => lowerAssertOk(lowerer, expr, loc)); } /** assert(value[, message]) / assert.ok(value[, message]): the truthiness @@ -889,7 +1087,24 @@ export function lowerAssertDirectCall( * the same bytes at compile time). */ function lowerAssertOk(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr { requireStatementPosition(lowerer, expr, ts.getTextOfNode(expr.expression)); - if (expr.arguments.nodes.length < 1 || expr.arguments.nodes.length > 2) { + if (expr.arguments.nodes.length === 0) { + // Node's message for a call without arguments. + const text = "No value argument passed to `assert.ok()`"; + return { + kind: "seqExpr", + stmts: [recordOnFailure(lowerer, boolLit(true, loc), null, boolLit(true, loc), "==", loc)], + result: { + kind: "libCall", + fn: "assert.ok", + args: [boolLit(false, loc), strLit(text, loc), boolLit(true, loc)], + type: VOID, + loc, + }, + type: VOID, + loc, + }; + } + if (expr.arguments.nodes.length > 2) { lowerer.noLowering( `${ts.getTextOfNode(expr.expression)} with ${expr.arguments.nodes.length} arguments`, expr, @@ -953,22 +1168,26 @@ function lowerAssertFail(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc) generated = boolLit(true, loc); } else { // Node's AssertionError sets generatedMessage to !message, so an - // empty custom message counts as generated. - msg = stableValue( - lowerer, - stmts, - lowerMessageArg(lowerer, msgNode, loc).msg, - "%assertFailMessage", - loc, - ); - generated = { - kind: "strEq", - negated: false, - left: msg, - right: strLit("", loc), - type: BOOL, - loc, - }; + // empty custom message counts as generated. A message that is not a + // string is read from the hidden local that withValueMessage declares + // after these statements, so it is used as is. + const { msg: lowered, hasMsg } = lowerMessageArg(lowerer, msgNode, loc, true); + if (lowered.kind === "varRef" && hasMsg.kind === "varRef") { + // A value message: generated when Node reads "Failed" for null or + // undefined; the runtime sets the flag for a registered value. + msg = lowered; + generated = { kind: "unary", op: "!", operand: hasMsg, type: BOOL, loc }; + } else { + msg = stableValue(lowerer, stmts, lowered, "%assertFailMessage", loc); + generated = { + kind: "strEq", + negated: false, + left: msg, + right: strLit("", loc), + type: BOOL, + loc, + }; + } } const falseLit: IrExpr = { kind: "boolLit", value: false, type: BOOL, loc }; stmts.push(recordOnFailure(lowerer, boolLit(true, loc), null, null, "fail", loc)); @@ -1056,9 +1275,22 @@ function lowerAssertEqual( // would answer false where Node can answer true (the value's own // earlier crossing) — a miscompare, never lowered. Bytes fence both // ways (the dyn copy cannot carry the Buffer/Uint8Array brand the - // checker type knows); class instances and everything non-convertible - // keep the fence too. - if (a.type.kind === "dyn" || b.type.kind === "dyn") { + // checker type knows); everything non-convertible keeps the fence too. + // A class instance crosses as a reference to the instance itself, so + // strict compares are honest reference equality, between two instances + // too (assert.strictEqual(e, e.setMaxListeners(1))). + const instanceType = (t: IrType): boolean => + isDynTypedRefType(t) && !lowerer.isSubclassOf(t.className, "%Error"); + const instance = (e: IrExpr): boolean => + !deep && + (instanceType(e.type) || + (e.type.kind === "union" && + (lowerer.unions + .get(e.type.unionId) + ?.arms.every((arm) => instanceType(arm) || isUnitType(arm)) ?? + false))) && + lowerer.dynConvertible(e.type); + if (a.type.kind === "dyn" || b.type.kind === "dyn" || (instance(a) && instance(b))) { const box = (e: IrExpr, node: ts.Expression): IrExpr => { if (e.type.kind === "dyn") return e; const k = e.type.kind; @@ -1067,6 +1299,18 @@ function lowerAssertEqual( k === "f64" || k === "string" || k === "bool" || + // A union of scalars and units crosses by value, like its arms. + (e.type.kind === "union" && + (lowerer.unions + .get(e.type.unionId) + ?.arms.every( + (arm) => + arm.kind === "f64" || + arm.kind === "string" || + arm.kind === "bool" || + isUnitType(arm), + ) ?? + false)) || (k === "func" && lowerer.dynConvertible(e.type)) || // Runtime handles cross by REFERENCE (identity preserved), so the // strict compare is honest: assert.strictEqual(this, server) — @@ -1082,6 +1326,9 @@ function lowerAssertEqual( // same). The deep walk compares the retained error data without // re-encoding and losing identity (SEMANTICS.md). (k === "object" && e.type.kind === "object" && e.type.className === "%Error") || + instance(e) || + // A symbol crosses as itself, so strict compares are identity. + (!deep && k === "symbol") || (deep && k !== "bytes" && k !== "object" && lowerer.dynConvertible(e.type)) || // Under skipPrototype the prototype a class instance or Buffer // brand would carry is not compared. @@ -1355,7 +1602,7 @@ function lowerAssertMatch( lowerer.noLowering(`${surface} with ${expr.arguments.nodes.length} arguments`, expr); } const s = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); - const re = lowerer.lowerExpr(expr.arguments.nodes[1]!); + const re = lowerExpectedValue(lowerer, expr.arguments.nodes[1]!); if (re.type.kind !== "regex") { lowerer.noLowering( `${surface} with a non-regex pattern`, @@ -1420,6 +1667,18 @@ function inErrorHierarchy(lowerer: Lowerer, className: string): boolean { return false; } +/** An expected string or regex: a value JavaScript stores checked-dynamic + * (an object literal's field, an untyped binding) converts to the string + * or regex its checker type names, so the comparison takes that arm. */ +function lowerExpectedValue(lowerer: Lowerer, node: ts.Expression): IrExpr { + const value = lowerer.lowerExpr(node); + if (value.type.kind !== "dyn") return value; + const expected = lowerer.mapTypeOf(lowerer.typeOf(node)); + return expected?.kind === "string" || expected?.kind === "regex" + ? lowerer.coerceToExpected(value, expected) + : value; +} + /** Classify the expected argument: string → message, regex value → the * regex test over String(error), an error-class identifier → instanceof, * an object LITERAL over name/message/code (string or regex values) → @@ -1436,7 +1695,7 @@ function classifyThrowsExpected( return { form: "message", msg: m.msg, hasMsg: m.hasMsg }; } if (expectedT?.kind === "regex") { - return { form: "regex", value: lowerer.lowerExpr(node) }; + return { form: "regex", value: lowerExpectedValue(lowerer, node) }; } const sym = ts.isIdentifier(node) ? lowerer.resolveValueSymbol(node) : null; const info = @@ -1458,17 +1717,29 @@ function classifyThrowsExpected( displayName: rec ? rec.lib : info.def.name.replace(/^%/, ""), }; } - if ( - (surface === "assert.doesNotThrow" || surface === "assert.doesNotReject") && - expectedT?.kind === "func" - ) { + if (expectedT?.kind === "func") { const value = lowerer.lowerExpr(node); + // An untyped value there (a dynamic-tier function's result) is checked + // at run time like any untyped expected value. + if (value.type.kind === "jsval" || value.type.kind === "dyn") { + const boxed = lowerer.coerceToExpected(value, DYN); + if (boxed.type.kind === "dyn") return { form: "errValue", value: boxed }; + } + // A JavaScript function accepts any argument; a typed one must take + // unknown values (the caught value need not be an Error). if ( value.type.kind === "func" && - value.type.params.every((param) => param.kind === "dyn") && + (value.type.params.every((param) => param.kind === "dyn") || + isJsSourceFile(ts.getSourceFileOfNode(node))) && lowerer.dynConvertible(value.type) ) { - return { form: "predicate", value: lowerer.coerceToExpected(value, DYN) }; + // The boxed function carries its source name (Node's message names + // a named validation function). + const fnName = jsFuncNameOf(node); + return { + form: "predicate", + value: { kind: "dynFrom", value, ...(fnName !== null ? { fnName } : {}), type: DYN, loc }, + }; } lowerer.noLowering( `${surface} with this validation callback`, @@ -1509,7 +1780,7 @@ function classifyThrowsExpected( ); } const valueNode = ts.isPropertyAssignment(named) ? named.initializer : named.name; - const value = lowerer.lowerExpr(valueNode); + const value = lowerExpectedValue(lowerer, valueNode); if (value.type.kind === "string") { byId.set(id, { id, kind: "str", value, enameBake: null }); } else if (value.type.kind === "regex") { @@ -1534,7 +1805,9 @@ function classifyThrowsExpected( // tracingChannel suite's shape): the value crosses as a dyn error and // the runtime compares per Node's expectsError key walk. A // checked-dynamic expected rides directly; a static %Error boxes. - if (expectedT?.kind === "dyn") { + // An untyped JavaScript value (a parameter) too: a function there is + // checked at run time (assertThrowsHelper). + if (expectedT?.kind === "dyn" || expectedT === null) { const value = lowerer.lowerExpr(node); if (value.type.kind === "dyn") return { form: "errValue", value }; } @@ -1931,6 +2204,46 @@ function assertThrowsHelper( return { e: strLit("", loc), has: false }; })(); + // expectedException's function branch: a function with a prototype that + // the caught value is an instance of passes; anything else is called as + // a validation function, whose result must be true. + const validateStmts = (fn: IrExpr): IrStmt[] => { + const actual = (): IrExpr => ({ kind: "caughtToDyn", value: caughtRef(), type: DYN, loc }); + const prototype: IrExpr = { + kind: "dynKeyGet", + value: fn, + key: strLit("prototype", loc), + optional: true, + type: DYN, + loc, + }; + return [ + ifStmt( + { kind: "dynTest", test: "undefined", negated: true, value: prototype, type: BOOL, loc }, + [ + ifStmt( + checkedClassInstanceOf( + lowerer, + actual(), + fn, + "assert.throws against this native constructor is not supported yet", + loc, + ), + [doReturn], + ), + ], + ), + lib("assert.throwsValidate", [ + actual(), + fn, + strLit(mode === "rejects" ? "rejects" : "throws", loc), + m(), + hm(), + ]), + doReturn, + ]; + }; + let catchBody: IrStmt[]; if (mode === "dnr" || mode === "dnt") { const actual = (): IrExpr => ({ kind: "caughtToDyn", value: caughtRef(), type: DYN, loc }); @@ -2104,7 +2417,13 @@ function assertThrowsHelper( // caught value crosses as a dyn value (identity-preserving) and // the runtime walks the expected's keys — a mismatch throws the // deep-equal AssertionError. + // A function there is a validation function (or an error class + // value) as in the predicate form. catchBody = [ + ifStmt( + { kind: "dynTest", test: "function", value: varRef("ev.0", DYN, loc), type: BOOL, loc }, + validateStmts(varRef("ev.0", DYN, loc)), + ), lib("assert.expectsErrDyn", [ { kind: "caughtToDyn", value: caughtRef(), type: DYN, loc }, varRef("ev.0", DYN, loc), @@ -2115,7 +2434,8 @@ function assertThrowsHelper( ]; break; case "predicate": - throw new InternalCompilerError("assert validation callback only enters a no-error helper"); + catchBody = validateStmts(varRef("predicate.0", DYN, loc)); + break; } } diff --git a/packages/compiler/src/frontend/lowering/lower-builtin-values.ts b/packages/compiler/src/frontend/lowering/lower-builtin-values.ts index e5ebc6d8e..d59b234d3 100644 --- a/packages/compiler/src/frontend/lowering/lower-builtin-values.ts +++ b/packages/compiler/src/frontend/lowering/lower-builtin-values.ts @@ -16,7 +16,7 @@ import { strLit, varRef } from "../../ir/build.js"; import type { Lowerer } from "./lowerer.js"; import { BUILTIN_MODULE_CONSTS, - BUILTIN_MODULE_FNS, + builtinModuleFns, OBJECT_CALLABLE_VALUES, builtinConstLit, builtinModuleConstOf, @@ -430,7 +430,7 @@ export function pathModuleValue(lowerer: Lowerer, module: string, loc: SrcLoc): const add = (member: string, value: IrExpr): void => { fields.push({ key: strLit(member, loc), value: box(value) }); }; - for (const member of Object.keys(BUILTIN_MODULE_FNS[module] ?? {})) { + for (const member of Object.keys(builtinModuleFns()[module] ?? {})) { const value = lowerer.lowerBuiltinCallableValue({ module, member }, loc); if (value) add(member, value); } @@ -512,7 +512,7 @@ function moduleValue(lowerer: Lowerer, module: string, loc: SrcLoc): IrExpr { loc, }); }; - for (const member of Object.keys(BUILTIN_MODULE_FNS[module] ?? {})) { + for (const member of Object.keys(builtinModuleFns()[module] ?? {})) { const callable = lowerer.lowerBuiltinCallableValue({ module, member }, loc); if (callable) add(member, callable); } diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 6363a1110..3a329c547 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -1,5 +1,13 @@ +import { + arrayClassMemberOwner, + arrayClassReceiver, + lowerArrayClassStaticCall, +} from "./builtins/array-classes.js"; +import { checkedPrivateMethodReceiver, privateNameClass } from "./private-brands.js"; +import { assertFunctionAliasOf } from "./lower-assert.js"; import { keyedHelperName } from "./stable-names.js"; import { perKeyOrder } from "./key-order-witness.js"; +import { lowerDateToPrimitiveCall } from "./builtins/dates.js"; import { isForwardedTupleSpread, lowerForwardingApply } from "./typed-arguments.js"; import { guardNullProto, nullProtoRecordShape } from "./null-prototype.js"; import { lowerDynStreamMemberCall } from "./dyn-streams.js"; @@ -26,7 +34,11 @@ import { import { lowerMockTimersElementCall } from "./lower-test.js"; import { objectEnumerationReceiver } from "./object-enumeration-receiver.js"; import { InternalCompilerError } from "../../errors.js"; -import { isFsStreamMember, lowerFsStreamMethodCall } from "./builtins/fs-streams.js"; +import { + isFsStreamMember, + lowerFsStreamCall, + lowerFsStreamMethodCall, +} from "./builtins/fs-streams.js"; import * as ts from "../ts7/adapter.js"; import { isZlibStreamMember, @@ -121,7 +133,11 @@ import { } from "./containers/indexed-objects.js"; import { lowerTupleReadMethodCall } from "./containers/array-methods.js"; import { tryFuseIndexRead } from "./array-values.js"; -import { lowerBytesStaticCall } from "./containers/bytes.js"; +import { + lowerBufferConstruct, + lowerBytesPrototypeSetCall, + lowerBytesStaticCall, +} from "./containers/bytes.js"; import { lowerRegexMethodCall, lowerStringIndexCall, @@ -165,6 +181,7 @@ import { lowerPerfHooksCall, perfEntryTag, } from "./builtins/performance.js"; +import { fenceX509KeyWalk } from "./builtins/x509.js"; import { lowerProcStreamMethodCall, lowerProcessBuiltinModuleNamespace, @@ -185,7 +202,7 @@ import { import { isSafeToDiscard } from "./expressions/evaluation-safety.js"; import { tryLowerExpression } from "./expressions/try-lower-expression.js"; import { lowerChildMethodValue, lowerChildPropertyCall } from "./handle-properties.js"; -import { classToNumberConversion } from "./class-to-primitive.js"; +import { classObjectTag, classToNumberConversion } from "./class-to-primitive.js"; import { hasRuntimeStatics } from "./class-runtime-statics.js"; import { structuralReadType } from "./class-unions.js"; import { @@ -247,6 +264,7 @@ import { import { fenceSymbolFieldCopy } from "./symbol-fields.js"; import { lowerClassDataDescriptor, lowerClassDescriptorRead } from "./class-descriptors.js"; import { lowerClassFieldDefine, lowerClassIntegrity } from "./class-attributes.js"; +import { freezableRecordType, lowerNativeFreeze, noteUntypedIntegrity } from "./frozen-records.js"; import { classStaticDataFor } from "./class-static-data.js"; import { lowerUrlFactory } from "./lower-url.js"; import { nullishValueUnitOf } from "./binding-analysis.js"; @@ -257,6 +275,7 @@ import { completeArgs, } from "./call-arguments.js"; import { isThisParameter, type ParamShape } from "./call-signatures.js"; +import { dynamicOverloadChoice } from "./overload-choice.js"; import { lowerStringIteratorValue } from "./iterator-adapters.js"; import { checkedArmExtraction } from "./coercions/unions.js"; import { maybePromiseReturnStmt, promiseArmedUnionOf } from "./maybe-promise.js"; @@ -268,9 +287,32 @@ import { } from "./generic-functions.js"; import { codeGenerationCalleeOf, lowerCodeGenerationCall } from "./code-generation.js"; +import { lowerRecordInheritedCall } from "./record-inherited.js"; const ZLIB_STREAM_METHODS: ReadonlySet = new Set(["flush", "params", "close", "reset"]); +/** The node:util member a `(0, member)` callee names, or null. */ +function indirectUtilCalleeOf( + lowerer: Lowerer, + callee: ts.Expression, +): { module: string; member: string } | null { + let e = callee; + while (ts.isParenthesizedExpression(e)) e = e.expression; + if ( + !ts.isBinaryExpression(e) || + e.operatorToken.kind !== ts.SyntaxKind.CommaToken || + !ts.isNumericLiteral(e.left) + ) + return null; + const target = e.right; + const bi = ts.isIdentifier(target) + ? lowerer.builtinImportOf(target) + : ts.isPropertyAccessExpression(target) && !target.questionDotToken + ? lowerer.builtinMemberOf(target) + : null; + return bi?.module === "util" ? bi : null; +} + /** `Object.defineProperty(target, ...)`, `Object.defineProperties`, * `Reflect.defineProperty`, and `target.__defineGetter__/__defineSetter__` * over a target that may hold an Error can install an accessor over an @@ -370,6 +412,25 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { const nodeApi = nodeApiCalleeOf(lowerer.program, expr.expression); if (nodeApi !== null) return lowerNodeApiCall(lowerer, expr, nodeApi); } + // `(0, util_1.inspect)(value)`: TypeScript's CommonJS output calls a + // named import through a comma expression, which leaves the call's + // receiver undefined. node:util's functions do not read their receiver, + // so the call lowers as the direct one. + { + const util = indirectUtilCalleeOf(lowerer, expr.expression); + if (util) return lowerBuiltinCall(lowerer, expr, util, loc); + } + // The deprecated Buffer(...) call (containers/bytes.ts). The checker + // types it `any`, so the bytes cross as a checked-dynamic value, which + // keeps the Buffer brand for inspection. + { + const buffer = lowerBufferConstruct(lowerer, expr); + if (buffer) return { kind: "dynFrom", value: buffer, type: DYN, loc }; + } + { + const stream = lowerFsStreamCall(lowerer, expr); + if (stream) return stream; + } // A package shim's runtime intrinsic (a bodiless `declare function` // named scriptcUnixDgram* in a shipped shim): a direct runtime call. if (ts.isIdentifier(expr.expression) && isShimIntrinsicName(expr.expression.text)) { @@ -582,6 +643,17 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { ) { return lowerer.lowerSuperMethodCall(expr, expr.expression); } + // `value.#m(...)` on a checked-dynamic value (`this` in a JavaScript + // class's receiver-independent method): the private method of the + // brand-checked receiver. + if ( + ts.isPropertyAccessExpression(expr.expression) && + ts.isPrivateIdentifier(expr.expression.name) && + !expr.expression.questionDotToken + ) { + const privateCall = lowerCheckedPrivateMethodCall(lowerer, expr, expr.expression); + if (privateCall) return privateCall; + } const consoleMember = lowerer.consoleCallMember(expr); const consoleMethod = consoleMember === null ? lowerer.consoleMethodMember(expr) : null; @@ -1047,6 +1119,13 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { } const bi = lowerer.builtinImportOf(expr.expression); if (bi) return lowerBuiltinCall(lowerer, expr, bi, loc); + // A constant bound to an assert method (`const ok = assert.ok`) called + // directly is that assertion, with the message Node quotes the call for. + { + const alias = assertFunctionAliasOf(lowerer, expr.expression); + const served = alias ? lowerer.lowerAssertModuleCall(expr, alias, loc) : null; + if (served) return served; + } // The assert module binding called DIRECTLY (`assert(x)` — a default // import or the CJS `const assert = require("assert")`): Node's // module object IS assert.ok; namespace-import bindings fence inside @@ -1082,7 +1161,10 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { if (!argNode || (ts.isIdentifier(argNode) && argNode.text === "undefined")) { return { kind: "libCall", fn: "sym.newAnon", args: [], type: SYMBOL_T, loc }; } - const desc = lowerer.lowerExpr(argNode); + let desc = lowerer.lowerExpr(argNode); + // A number or boolean description is its ToString. + if (desc.type.kind === "f64" || desc.type.kind === "bool") + desc = lowerer.ensureString(desc, argNode); if (desc.type.kind !== "string") { lowerer.noLowering( `Symbol with a '${lowerer.fmt(desc.type)}' description`, @@ -1586,6 +1668,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { lowerer.lowerTestMethodCall(expr, expr.expression) ?? lowerer.lowerTimeoutMethodCall(expr, expr.expression) ?? lowerArrayPrototypeBorrowCall(lowerer, expr, expr.expression) ?? + lowerBytesPrototypeSetCall(lowerer, expr, expr.expression) ?? lowerObjectOwnPrototypeCall(lowerer, expr, expr.expression) ?? lowerObjectPrototypeCall(lowerer, expr, expr.expression) ?? lowerStringPrototypeCall(lowerer, expr, expr.expression) ?? @@ -1604,6 +1687,9 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { // Union receivers whose every arm has a text — the ngrok // `(chunk: Buffer | string) => chunk.toString()` idiom. lowerUnionToStringCall(lowerer, expr, expr.expression) ?? + // Object.prototype's methods called by name on records: an own + // callable entry, else the built-in with the record as receiver. + lowerRecordInheritedCall(lowerer, expr, expr.expression) ?? // Object.prototype.toString's default answer on records and // override-free program classes — "[object Object]", folded. lowerDefaultToStringCall(lowerer, expr, expr.expression) ?? @@ -1664,6 +1750,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { lowerIteratorStaticFence(lowerer, expr, expr.expression) ?? lowerObjectStaticCall(lowerer, expr, expr.expression) ?? lowerObjectFromEntriesCall(lowerer, expr, expr.expression) ?? + lowerArrayClassStaticCall(lowerer, expr, expr.expression) ?? lowerArrayFromCall(lowerer, expr, expr.expression) ?? lowerArrayFromAsyncCall(lowerer, expr, expr.expression) ?? lowerArrayOfCall(lowerer, expr, expr.expression) ?? @@ -1810,6 +1897,8 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { // The element spelling of an unsupported native Web method must fence // before lowering the callee into a checked-dynamic keyed read. if (ts.isElementAccessExpression(expr.expression)) { + const datePrimitive = lowerDateToPrimitiveCall(lowerer, expr, expr.expression); + if (datePrimitive) return datePrimitive; const timersDispose = lowerMockTimersElementCall(lowerer, expr, expr.expression); if (timersDispose) return timersDispose; const symbolMethod = lowerClassSymbolMethodCall(lowerer, expr); @@ -2450,6 +2539,9 @@ function lowerNumberConstructorValue( }; const optionalNumber = lowerOptionalStringNumber(lowerer, arg, loc); if (optionalNumber) return optionalNumber; + // A Date's number hint answers its time value. + if (arg.type.kind === "date") + return { kind: "libCall", fn: "date.valueOf", args: [arg], type: F64, loc }; // A program class instance converts through its own methods. const classNumber = classToNumberConversion(lowerer, arg, argNode); if (classNumber) return classNumber; @@ -2952,6 +3044,7 @@ function lowerObjectOwnCall( } else if (type.kind === "record") { const shape = lowerer.shapes.get(type.shapeId); fencePerfEntryKeyWalk(lowerer, type, receiverNode ?? call, `Object.prototype.${method}`); + fenceX509KeyWalk(lowerer, type, receiverNode ?? call, `Object.prototype.${method}`); if (!shape || shape.tuple || shapeHasAccessorSlots(shape)) { lowerer.noLowering( "Object.prototype." + method + " over this record shape", @@ -3110,6 +3203,10 @@ function lowerObjectPrototypeCall( const immediate = immediateObjectTagReceiver(lowerer, receiverNode); const receiver = immediate?.effect ?? lowerer.lowerExpr(receiverNode); const tag = immediate?.tag ?? objectToStringTag(lowerer, receiver.type); + if (tag === null && immediate === null && call.arguments.nodes.length === 1) { + const classTag = classObjectTag(lowerer, receiver, receiverNode); + if (classTag) return classTag; + } // A generator object answers its tag through its checked capsule. if (tag === null && receiver.type.kind !== "dyn" && receiver.type.kind !== "generator") { lowerer.noLowering( @@ -3763,6 +3860,14 @@ function lowerDynReceiverMethodCall( // 'unknown' slot does. if (checkerUntyped && DYN_HANDLE_KINDS.has(recv.type.kind)) recv = lowerer.coerceToExpected(recv, DYN); + // A typed array whose checker type collapsed to `any` (a JS binding + // referenced in its own initializer's inference — hono's etag digest + // `const nextChunk = new Uint8Array(new ArrayBuffer(f(chunk)))` beside + // `chunk = nextChunk`) still lowered to its native bytes. The typed + // lowerings key on the checker type and decline it, so it boxes by + // reference (the box shares the native storage, so writes through the + // call stay visible) and the call dispatches on the runtime kind. + if (checkerUntyped && recv.type.kind === "bytes") recv = lowerer.coerceToExpected(recv, DYN); // The last-resort retry (boxTypedTemporary — every other lowering // already declined): a checker-`any` receiver whose value is a FRESH // typed result of an earlier step in the chain (`out.split("\n")` @@ -5493,6 +5598,9 @@ function reconcileOverloadReturn( expr: ts.CallExpression | ts.TaggedTemplateExpression, call: IrExpr, ): IrExpr { + // The checker's overload is a guess for a checked-dynamic argument + // another overload accepts: the implementation's value stands. + if (ts.isCallExpression(expr) && dynamicOverloadChoice(lowerer, expr)) return call; const rsig = lowerer.checker.getResolvedSignature(expr); const rdecl = rsig ? rsig.declaration : undefined; // Nominal covariant overrides share the base's return ABI, but retain @@ -7279,7 +7387,7 @@ export function recordKeysArrayCall( * (a key is own exactly when Object.keys would list it — the two share * the test), everything else true. Unmatched keys probe the overflow * map when present, so explicit undefined values remain own keys. */ -function recordHasOwnHelper(lowerer: Lowerer, shapeId: string, loc: SrcLoc): string { +export function recordHasOwnHelper(lowerer: Lowerer, shapeId: string, loc: SrcLoc): string { const key = `obj.hasOwn:${shapeId}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; @@ -8443,7 +8551,17 @@ function lowerObjectStaticCall( ...(fnName !== null ? { fnName } : {}), }; } - if (target && isUnitType(target.type)) { + // A class value whose statics are runtime properties of its constructor + // boxes to that constructor function, as for Object.defineProperty above. + const runtimeStaticClass = + target?.type.kind === "classval" ? lowerer.classes.get(target.type.className) : undefined; + if ( + target && + (isUnitType(target.type) || + (runtimeStaticClass !== undefined && + hasRuntimeStatics(runtimeStaticClass) && + lowerer.dynConvertible(target.type))) + ) { target = { kind: "dynFrom", value: target, type: DYN, loc: locOf(call.arguments.nodes[0]!) }; } if (target?.type.kind === "dyn") { @@ -8494,6 +8612,22 @@ function lowerObjectStaticCall( locOf(call), ); if (fnIntegrity) return fnIntegrity; + // A Map or Set has no own properties: freeze, seal and preventExtensions + // all only make it not extensible. + if (value.type.kind === "map" || value.type.kind === "set") { + if (member === "preventExtensions" || member === "seal") + return lowerNativeFreeze(lowerer, "collection.preventExtensions", value, locOf(call)); + const extensible: IrExpr = { + kind: "libCall", + fn: "collection.isExtensible", + args: [value], + type: BOOL, + loc: locOf(call), + }; + return member === "isExtensible" + ? extensible + : { kind: "unary", op: "!", operand: extensible, type: BOOL, loc: locOf(call) }; + } if (member === "preventExtensions" || member === "seal") { const integrity = lowerClassIntegrity(lowerer, call, member, value); if (integrity) return integrity; @@ -8515,6 +8649,7 @@ function lowerObjectStaticCall( isSealed: "dyn.isSealed", } as const; const fn = operation[member as keyof typeof operation]; + if (!member.startsWith("is")) noteUntypedIntegrity(lowerer, call.arguments.nodes[0]!); return { kind: "libCall", fn, @@ -8528,10 +8663,44 @@ function lowerObjectStaticCall( // the argument — no alias exists, so the frozen bit is unobservable // (writes through the Readonly result are compile errors, and no // other reference can write). Primitives pass through per ES2015. - // Aliased objects keep a fence: a later write through the original - // reference would need the runtime frozen bit (strict mode throws). + // Aliased records get the frozen bit of their rc word (frozen-records.ts); + // other aliased objects keep a fence: a later write through the original + // reference would need a runtime frozen bit (strict mode throws). if (member === "isFrozen" && call.arguments.nodes.length === 1) { - const value = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, DYN); + const argNode = call.arguments.nodes[0]!; + const probedKind = tryLowerExpression(lowerer, argNode)?.type.kind; + if ( + probedKind === "record" || + probedKind === "array" || + probedKind === "map" || + probedKind === "set" + ) { + const native = lowerer.lowerExpr(argNode); + const loc = locOf(call); + if (native.type.kind === "record" || native.type.kind === "array") + return { + kind: "libCall", + fn: native.type.kind === "record" ? "record.isFrozen" : "array.isFrozen", + args: [native], + type: BOOL, + loc, + }; + if (native.type.kind === "map" || native.type.kind === "set") + return { + kind: "unary", + op: "!", + operand: { + kind: "libCall", + fn: "collection.isExtensible", + args: [native], + type: BOOL, + loc, + }, + type: BOOL, + loc, + }; + } + const value = lowerer.lowerExprExpecting(argNode, DYN); return { kind: "libCall", fn: "dyn.isFrozen", args: [value], type: BOOL, loc: locOf(call) }; } if (member === "freeze") { @@ -8547,8 +8716,10 @@ function lowerObjectStaticCall( (ts.isObjectLiteralExpression(inner) || ts.isArrayLiteralExpression(inner)) ? lowerer.lowerExprExpecting(argNode, DYN) : lowerer.lowerExpr(argNode); - if (value.type.kind === "dyn") + if (value.type.kind === "dyn") { + noteUntypedIntegrity(lowerer, argNode); return { kind: "libCall", fn: "dyn.freeze", args: [value], type: DYN, loc: locOf(call) }; + } if (ts.isObjectLiteralExpression(inner) || ts.isArrayLiteralExpression(inner)) { return value; // fresh — freeze is identity here, honestly } @@ -8581,6 +8752,12 @@ function lowerObjectStaticCall( if (fnIntegrity) return fnIntegrity; const integrity = lowerClassIntegrity(lowerer, call, "freeze", value); if (integrity) return integrity; + if (freezableRecordType(lowerer, value.type)) + return lowerNativeFreeze(lowerer, "record.freeze", value, locOf(call)); + if (value.type.kind === "array") + return lowerNativeFreeze(lowerer, "array.freeze", value, locOf(call)); + if (value.type.kind === "map" || value.type.kind === "set") + return lowerNativeFreeze(lowerer, "collection.preventExtensions", value, locOf(call)); lowerer.noLowering( "Object.freeze of a possibly-aliased value", call, @@ -8626,7 +8803,12 @@ function lowerObjectStaticCall( (probed && isDynTypedRefType(probed.type)) || (probed?.type.kind === "object" && lowerer.errorHierarchyClassOf(probed.type.className)) || (probed && constructor && (constructor.callableBase || hasRuntimeStatics(constructor))) || - (probed?.type.kind === "func" && lowerer.dynConvertible(probed.type)) + (probed?.type.kind === "func" && lowerer.dynConvertible(probed.type)) || + // null, undefined and unions of untyped JavaScript values: the same + // probe, whose ToObject throws Node's TypeError on nullish. + (probed && + (isUnitType(probed.type) || probed.type.kind === "union") && + lowerer.dynConvertible(probed.type)) ) { const loc = locOf(call); const receiver = lowerer.coerceToExpected(probed, DYN); @@ -8720,11 +8902,13 @@ function lowerObjectStaticCall( const moduleSymbol = source ? lowerer.checker.getSymbolAtLocation(source) : undefined; const names: string[] = []; moduleSymbol?.getExports().forEach((symbol, key) => { + // Internal names (`__export`) start with `__`; an exported name that + // does is keyed with an extra `_` (escapedSymbolName). const name = String(key); - if (name.startsWith("__") || name === "export=") return; + if ((name.startsWith("__") && !name.startsWith("___")) || name === "export=") return; const target = symbol.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(symbol) : symbol; - if (target.flags & ts.SymbolFlags.Value) names.push(name); + if (target.flags & ts.SymbolFlags.Value) names.push(ts.symbolName(symbol)); }); names.sort(); const loc = locOf(call); @@ -8850,6 +9034,7 @@ function lowerObjectStaticCall( // RESULTS, invoked in key order. The static field walk models neither // (accessor slots live outside declaredOrder), so the surface fences. fencePerfEntryKeyWalk(lowerer, argIr, call, `Object.${member}`); + fenceX509KeyWalk(lowerer, argIr, call, `Object.${member}`); if (shapeHasAccessorSlots(shape)) { lowerer.unsupported( "SC1090", @@ -9936,7 +10121,13 @@ function lowerObjLitGenericMethodCall( type: DYN, loc, }; - return lowerer.coerceInto(call, invoked, lowerer.irTypeOf(call)); + const resultType = lowerer.irTypeOf(call); + // A void method's result stays the runtime's untyped value, as a + // call through a non-generic void member's dispatch keeps it: a class + // may implement the member with a method that returns a value, which + // JavaScript passes on (`console.log(c.publish(m))`). + if (resultType.kind === "void") return invoked; + return lowerer.coerceInto(call, invoked, resultType); } } if (onInterface) { @@ -10004,6 +10195,12 @@ export function lowerObjectMethodCall( ? probe : null; if (specializedReceiver) mappedReceiver = specializedReceiver.type; + // A member declared by a class that extends Array, called on an instance + // (whose type maps to its array): the class's own method. + const arrayClassOwner = + mappedReceiver?.kind === "array" ? arrayClassMemberOwner(lowerer, access) : null; + if (arrayClassOwner !== null) + mappedReceiver = { kind: "object", className: arrayClassOwner.def.name }; if (mappedReceiver?.kind === "union") { const dispatched = lowerUnionObjectMethodCall(lowerer, call, access, mappedReceiver) ?? @@ -10037,6 +10234,13 @@ export function lowerObjectMethodCall( : null; if (receiverIr === null) return null; const lowerReceiver = (): IrExpr => { + if (arrayClassOwner !== null) + return arrayClassReceiver( + lowerer, + arrayClassOwner, + lowerer.lowerExpr(access.expression), + access.expression, + ); const receiver = specializedReceiver ?? lowerer.lowerExpr(access.expression); const optional = lowerer.runtimeOptionalPropertyReceiver( access.expression, @@ -10058,8 +10262,8 @@ export function lowerObjectMethodCall( ? lowerer.coerceInto(access.expression, receiver, receiverIr) : receiver; }; - const info = lowerer.classes.get(receiverIr.className); - if (!info) lowerer.flushDeferredClass(receiverIr.className); + const info = + lowerer.classes.get(receiverIr.className) ?? lowerer.flushDeferredClass(receiverIr.className); const found = info ? lowerer.findMethodOn(info, access.name.text) : null; // The zlib stream members (flush/params/close/reset) are declared on // the zlib classes; their receivers are runtime Transform streams. @@ -10412,11 +10616,8 @@ function lowerUnionObjectMethodCall( found: NonNullable>; }[] = []; for (const arm of def.arms as (IrType & { kind: "object" })[]) { - const info = lowerer.classes.get(arm.className); - if (!info) { - lowerer.flushDeferredClass(arm.className); - return null; - } + const info = lowerer.classes.get(arm.className) ?? lowerer.flushDeferredClass(arm.className); + if (!info) return null; const found = lowerer.findMethodOn(info, method); if (!found) return null; if (found.sig.abstract === true && !lowerer.overrideBelow(info, method)) return null; @@ -11322,3 +11523,35 @@ function lowerShimIntrinsicCall( ); return { kind: "libCall", fn: intrinsic.fn, args, type: intrinsic.ret, loc }; } + +/** `value.#m(args)` where `value` lowers to a checked-dynamic value and the + * declaration-level class whose body declares `#m` declares it as a method: + * the method called on the brand-checked receiver + * (checkedPrivateMethodReceiver). Null for any other private member call. */ +function lowerCheckedPrivateMethodCall( + lowerer: Lowerer, + call: ts.CallExpression, + access: ts.PropertyAccessExpression, +): IrExpr | null { + if (!ts.isPrivateIdentifier(access.name)) return null; + const name = access.name.text; + const owner = privateNameClass(lowerer, access.name); + const found = owner ? lowerer.findMethodOn(owner, name) : null; + if (!owner || !found || found.declarer !== owner || found.sig.abstract === true) return null; + const probed = tryLowerExpression(lowerer, access.expression); + if (!probed || !checkedDynamicOperand(probed)) return null; + const value = checkedDynamicOperand(lowerer.lowerExpr(access.expression)); + if (!value) return null; + const loc = locOf(call); + const callee = `%${owner.def.name}.${name}`; + lowerer.noteEdge(callee); + const receiver = checkedPrivateMethodReceiver(lowerer, owner, name, value, loc); + const args = lowerer.completeArgs(call.arguments.nodes, found.sig.params, loc, call); + return { kind: "call", callee, args: [receiver, ...args], type: found.sig.ret, loc }; +} + +/** A checked-dynamic value, or the dynamic value under a checked extraction. */ +function checkedDynamicOperand(expr: IrExpr): IrExpr | null { + if (expr.type.kind === "dyn") return expr; + return expr.kind === "dynCheck" && expr.value.type.kind === "dyn" ? expr.value : null; +} diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 42c72c653..80919a251 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -1,3 +1,5 @@ +import { lowerAsyncLocalStorageOptions } from "./builtins/async-context.js"; +import { shapeOverlayAnnotated } from "../shape-inference/index.js"; import { DATE_TIME_FIELD, dateRootOf, @@ -6,19 +8,30 @@ import { lowerDateSuperMethodCall, } from "./builtins/date-classes.js"; import { predeclareForwardClass } from "./lower-stmts.js"; +import { + arrayClassBase, + arrayClassFieldsFn, + arrayClassSpeciesFn, + arraySuperCall, + refuseArrayClassSpecies, + lowerArraySuperCall, +} from "./builtins/array-classes.js"; import { lowerDateStaticCall } from "./builtins/dates.js"; import { keyedHelperName } from "./stable-names.js"; import { lowerValueReturningConstructorNew } from "./js-dynamic-objects.js"; import { adaptPromiseExecutor } from "./promise-executor-adapter.js"; -import { dynUndefinedExpr, nodeThrowExpr, varRef } from "../../ir/build.js"; +import { dynUndefinedExpr, nodeThrowExpr, strLit, varRef } from "../../ir/build.js"; import { constructedFromValue, constructedValue, superIntoResultSlot, } from "./constructor-returns.js"; import { checkedIterableSpread } from "./checked-iterable-spread.js"; -import { lowerReadlineUiNew } from "./builtins/readline.js"; -import { builtinMemberRequireDecl } from "./builtins/module-bindings.js"; +import { lowerReadlineInterfaceNew, lowerReadlineUiNew } from "./builtins/readline.js"; +import { + builtinMemberBindingTarget, + builtinMemberRequireDecl, +} from "./builtins/module-bindings.js"; import { checkedClassUnionAssertion } from "./class-assertions.js"; import { collectClassMethodMutations } from "./class-method-mutations.js"; import { InternalCompilerError } from "../../errors.js"; @@ -42,11 +55,11 @@ import type { FnCtx, Lowerer } from "./lowerer.js"; import { inlineNamespaceRequireOf, lowerWithInlineRequireInit } from "./cjs-inline-require.js"; import { BOOL, + BYTES_U8, canDynCheckTo, DATE_T, DYN, F64, - bytesOf, type IrClassDef, type IrExpr, type IrFunction, @@ -99,6 +112,8 @@ import { jsOpenObjectType, typeKey, withUnitArm, + X509_DER_FIELD, + X509_PUBLIC_KEY_FIELD, } from "../type-mapper.js"; import { canonicalBuiltinModule, @@ -110,13 +125,14 @@ import { requireSpecOf, } from "../program.js"; import { PoisonError, dynFallbackType, jsFuncNameOf, newFnCtx, own } from "./lowerer.js"; +import { declarationGroupKey } from "./type-failure.js"; import { isAssertClassNew } from "./assert-instances.js"; import { isCjsRequireGlobal } from "./lower-node-module.js"; import { lowerArrayConstructor } from "./containers/array-construction.js"; +import { initializedCollectionType } from "./js-collection-types.js"; import { lowerMapSeedNew, lowerSetSeedNew } from "./containers/collection-methods.js"; import { lowerObjectConstructor } from "./containers/indexed-objects.js"; -import { knownBufEncoding, lowerNodeModulesBufferConstruct } from "./containers/bytes.js"; -import { isSafeToRepeat } from "./expressions/evaluation-safety.js"; +import { knownBufEncoding, lowerBufferConstruct } from "./containers/bytes.js"; import { lowerSearchParamsNew } from "./builtins/url.js"; import { lowerTextCodecNew } from "./builtins/text-codecs.js"; import { @@ -125,6 +141,7 @@ import { lowerWorkerNew, } from "./builtins/workers.js"; import { + noLoweringDiag, unsupportedPackageValueDiag, unsupportedDiag, type ScrDiagnostic, @@ -156,6 +173,7 @@ import { } from "./lower-event-emitter.js"; import { declSymbolOf } from "./lower-modules.js"; import { + INSPECT_CUSTOM_MEMBER, classSymbolKeyOf, classSymbolKeyOfSymbol, symbolFieldDisplayName, @@ -177,15 +195,23 @@ import { hasLexicalPrivateReference, hasStaticThis, rejectStaticThis } from "./s import { lowerUrlNew } from "./lower-url.js"; import { isNativeProxyInitializer, lowerNativeProxy } from "./expressions/native-proxy.js"; import { classStaticDataFor } from "./class-static-data.js"; -import { construction, constructionPrefix } from "./js-class-fields.js"; +import { + accessorInterceptsBase, + accessorNames, + construction, + constructionPrefix, +} from "./js-class-fields.js"; import { lowerObjectFactoryNew } from "./object-factory-new.js"; import { tryLowerExpression } from "./expressions/try-lower-expression.js"; import { lowerInstanceConstructorNew } from "./class-instance-constructor.js"; -import { classPrototypeData } from "./class-prototypes.js"; +import { classPrototypeData, reflectClassPrototype } from "./class-prototypes.js"; import { constructionEscape } from "./construction-escapes.js"; import { checkedClassConstruction, checkedClassConstructionPacked } from "./class-construction.js"; import { initializeRuntimeStatics, hasRuntimeStatics } from "./class-runtime-statics.js"; -import { reflectedClassStaticMethodValue } from "./class-method-values.js"; +import { + reflectedClassMethodValue, + reflectedClassStaticMethodValue, +} from "./class-method-values.js"; import { identifierReadSymbol } from "./surfaces.js"; export function returnsOnlyThis(member: ts.MethodDeclaration): boolean { @@ -477,6 +503,10 @@ export interface ClassInfo { localPrototypeData?: true; prototypeMethodValues?: Map; prototypeReflectionReady?: true; + /** The global that records at run time that the prototype view was + * populated by reflection, so it holds its own `constructor` until the + * program deletes it (prototypeReflectedFlag). */ + prototypeReflectedFlag?: string; prototypeObservedMethodsReady?: true; /** The globals holding each observed method's original prototype value * (set when the prototype data is built), by method name. A typed call @@ -586,6 +616,10 @@ export interface ClassInfo { * constructor's rules, and the Date methods it does not declare lower * over that value. Subclasses inherit the field. */ extendsDate?: true; + /** A class declared `extends Array` (builtins/array-classes.ts): its + * instances are arrays of `elem` whose own fields follow the array + * header, and its TypeScript type maps to `elem[]`. */ + extendsArray?: { elem: IrType }; /** The synthetic LifetimeRegion class (@scriptc/experimental, * lifetime-regions.ts): its constructor is synthesized with the class, * and its methods lower at their call sites. */ @@ -1148,7 +1182,14 @@ function jsFieldInitializerType(lowerer: Lowerer, type: IrType, expression: ts.E ) return DYN; } - if (type.kind === "array" || type.kind === "record") return DYN; + // Shape inference's overlay types a field as a record or array only when + // the analysis proves its values never reach code that could observe a + // copy, so those fields keep their native layout. + if ( + (type.kind === "array" || type.kind === "record") && + !shapeOverlayAnnotated(expression.parent?.parent ?? expression) + ) + return DYN; // JavaScript documentation cannot guarantee that a call or an indexed // read returns a present number. Numeric fields receiving those values // must retain undefined, so later arithmetic performs JS coercion (NaN). @@ -1420,7 +1461,7 @@ function undefArmedFieldType(lowerer: Lowerer, p: ts.Symbol): IrType | null { const arms = mapped.kind === "union" ? (lowerer.unions.get(mapped.unionId)?.arms ?? []) : [mapped]; for (const a of arms) { - if (a.kind === "map" || a.kind === "regex" || a.kind === "date" || a.kind === "generator") { + if (a.kind === "map" || a.kind === "regex" || a.kind === "generator") { return null; } byKey.set(typeKey(a), a); @@ -1974,6 +2015,7 @@ export function collectClassShapeInner( let genericBase: ts.ExpressionWithTypeArguments | undefined; let callableBase: ClassInfo["callableBase"]; let extendsDate = false; + let extendsArray: { elem: IrType } | undefined; let factoryBaseExpression: ts.Expression | undefined; const adoptCallableBase = (expression: ts.Expression): boolean => { if (familyMode || inst || mixin) return false; @@ -2174,6 +2216,13 @@ export function collectClassShapeInner( if (t && adoptCallableBase(t.expression)) continue; lowerer.unsupported("SC1090", clause, "extending computed expressions"); } + if ((familyMode || inst) && lowerer.isStdlibGlobal(t.expression, "Array")) + lowerer.unsupported("SC1090", t, "a generic class that extends Array"); + if (!familyMode && !inst && !mixin && lowerer.isStdlibGlobal(t.expression, "Array")) { + extendsArray = arrayClassBase(lowerer, t); + refuseArrayClassSpecies(lowerer, decl); + continue; + } if (!familyMode && !inst && !mixin && lowerer.isStdlibGlobal(t.expression, "Date")) { if (t.typeArguments?.nodes) lowerer.unsupported("SC1090", t, "extending generic classes"); extendsDate = true; @@ -2274,14 +2323,30 @@ export function collectClassShapeInner( continue; } if (t.typeArguments?.nodes) lowerer.unsupported("SC1090", t, "extending generic classes"); - base = named; - if (!base) { - if (adoptCallableBase(t.expression)) continue; + if (named?.extendsArray) lowerer.unsupported( "SC1090", t, + `extending '${t.expression.text}', a class that extends Array (one level of Array subclass compiles)`, + ); + base = named; + if (!base) { + if (adoptCallableBase(t.expression)) continue; + // A base whose own collection deferred can still register later + // (a flush retry with dynamic slots): the derived class retries + // with it (Lowerer.retryDeferred). + const diag = unsupportedDiag( + "SC1090", + locOf(t), `extending classes not declared in the program ('${t.expression.text}')`, ); + lowerer.retriableDiags.add(diag); + // A base the program declares but rejected: this rejection is its + // cascade (localized failure, report-only). + const rejectedBase = rejectedProgramClassGroup(lowerer, symbol); + if (rejectedBase !== null) diag.cause = [rejectedBase]; + lowerer.pushDiag(diag); + throw new PoisonError(); } } @@ -2517,13 +2582,13 @@ export function collectClassShapeInner( if (type.kind === "void") lowerer.badType(member.name, lowerer.typeOf(member.name)); if ( type.kind === "dyn" && + !lowerer.liftDynFieldPolicy && !isJsSourceFile(ts.getSourceFileOfNode(member)) && !lowerer.classFieldStoresDyn(lowerer.typeOf(member.name)) && (member.type !== undefined || (lowerer.typeOf(member.name).flags & ts.TypeFlags.Any) === 0) ) { - lowerer.unsupported( - "SC1090", + lowerer.dynFieldPolicyFence( member.name, `${dynFieldTypeSpelling(lowerer, member.name)}-typed static fields`, ); @@ -3033,14 +3098,16 @@ export function collectClassShapeInner( if (type.kind === "void") lowerer.badType(member.name, lowerer.typeOf(member.name)); // Bundled JS declares fields without annotations before assigning // them in the constructor. Use the same native checked-dynamic - // storage as implicit constructor-assigned JS fields. + // storage as implicit constructor-assigned JS fields. In + // TypeScript the refusal is a policy fence that a flush retry lifts + // (Lowerer.liftDynFieldPolicy). if ( type.kind === "dyn" && + !lowerer.liftDynFieldPolicy && !isJsSourceFile(ts.getSourceFileOfNode(member)) && !lowerer.classFieldStoresDyn(lowerer.typeOf(member.name)) ) { - lowerer.unsupported( - "SC1090", + lowerer.dynFieldPolicyFence( member.name, `${dynFieldTypeSpelling(lowerer, member.name)}-typed class fields`, ); @@ -3176,9 +3243,12 @@ export function collectClassShapeInner( const type = shape.bodyType ?? shape.type; if (type.kind === "void") lowerer.badType(p.name, lowerer.typeOf(p.name)); // The class-field dyn rule verbatim (KEEP NARROW). - if (type.kind === "dyn" && !lowerer.classFieldStoresDyn(lowerer.typeOf(p))) { - lowerer.unsupported( - "SC1090", + if ( + type.kind === "dyn" && + !lowerer.liftDynFieldPolicy && + !lowerer.classFieldStoresDyn(lowerer.typeOf(p)) + ) { + lowerer.dynFieldPolicyFence( p.name, `${dynFieldTypeSpelling(lowerer, p)}-typed class fields`, ); @@ -3312,6 +3382,7 @@ export function collectClassShapeInner( !functionLocalClass(decl) && ts.isIdentifier(member.name) && !lowerer.virtualJsMethods.has(member) && + !lowerer.checkedJsOverrideMethods.has(member) && !lowerer.prototypeMethodAccesses.has(member.name.text) && inst === undefined && decl.typeParameters === undefined && @@ -3977,7 +4048,6 @@ export function collectClassShapeInner( declared.kind !== "union" && declared.kind !== "dyn" && declared.kind !== "map" && - declared.kind !== "date" && declared.kind !== "generator" && declared.kind !== "void" && declared.kind !== "caught"; @@ -4114,7 +4184,7 @@ export function collectClassShapeInner( // A subclass field created before anything can observe the instance // keeps its type: super() cannot run code that reads it earlier. const baseConstruction = - base && ![...methods.keys()].some((m) => m.startsWith("get:") || m.startsWith("set:")) + base && !accessorInterceptsBase(accessorNames(methods.keys()), base) ? construction(base) : null; const sealedPrefix = baseConstruction @@ -4147,7 +4217,7 @@ export function collectClassShapeInner( // An own JS callable shadows its prototype declaration. // Keep the method in the prototype and store the assigned // value in the instance property bag. - lowerer.prototypeMethodAccesses.set(name, assign); + lowerer.noteMethodAccess(name, assign); continue; } const sym = lowerer.checker.getSymbolAtLocation(assign); @@ -4229,7 +4299,7 @@ export function collectClassShapeInner( } if (!key && (fields.has(fieldName) || hasAccessor(fieldName))) continue; if (!key && (methods.has(fieldName) || lowerer.findMethodOn(base, fieldName))) { - lowerer.prototypeMethodAccesses.set(fieldName, lhs); + lowerer.noteMethodAccess(fieldName, lhs); continue; } // A key already declared (own or inherited) makes later @@ -4381,7 +4451,7 @@ export function collectClassShapeInner( ctor && !mixinForwarding ? lowerer.paramShapes(ctor.parameters.nodes) : (base?.ctorParams ?? - (callableBase || extendsDate ? [{ type: DYN, mode: "dynRest" }] : [])); + (callableBase || extendsDate || extendsArray ? [{ type: DYN, mode: "dynRest" }] : [])); if ( ctor && !mixinForwarding && @@ -4403,6 +4473,14 @@ export function collectClassShapeInner( // INSTANTIATION prints its family's name — JS has one `Box`. jsName: jsNameOverride ?? decl.name?.text ?? "", ...(base ? { base: base.def.name } : {}), + ...(decl.members.nodes.some( + (member) => + ts.isPropertyDeclaration(member) && + !ts.isPrivateIdentifier(member.name) && + member.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.StaticKeyword), + ) + ? { ownStaticFields: true as const } + : {}), jsLength: ctor?.parameters?.nodes.findIndex( (parameter) => parameter.initializer || parameter.dotDotDotToken, @@ -4428,6 +4506,9 @@ export function collectClassShapeInner( ...(methods.size > 0 ? { methods: [...methods.keys()] } : {}), ...((): { prototypeNames?: string[] } => { const names = classPrototypeNames(decl); + // A [util.inspect.custom] method, by its reserved slot name: the + // checked-dynamic inspect walk refuses instances that have one. + if (methods.has(INSPECT_CUSTOM_MEMBER)) names.push(INSPECT_CUSTOM_MEMBER); return names.length > 0 ? { prototypeNames: names } : {}; })(), ...(abstractClass ? { abstract: true as const } : {}), @@ -4436,6 +4517,13 @@ export function collectClassShapeInner( return am.length > 0 ? { abstractMethods: am } : {}; })(), ...(inst ? { genericOf: inst.family.def.name } : {}), + ...(extendsArray + ? { + arrayElem: extendsArray.elem, + arrayFields: `%${className}.arrayFields`, + arraySpecies: `%${className}.species`, + } + : {}), loc: locOf(decl), }, fields, @@ -4452,6 +4540,7 @@ export function collectClassShapeInner( ...(genericBase ? { genericBase } : {}), ...(callableBase ? { callableBase } : {}), ...(extendsDate ? { extendsDate: true as const } : {}), + ...(extendsArray ? { extendsArray } : {}), ...(factoryBaseExpression ? { factoryBaseExpression } : {}), subclasses: [], throwingSetters, @@ -5676,15 +5765,22 @@ export function lowerStaticFieldRead( ): IrExpr | null { if (expr.name.text === "prototype") return null; if (expr.questionDotToken) return null; + const info = staticReceiverClass(lowerer, expr.expression); + if (!info) return null; + return lowerClassStaticRead(lowerer, info, expr.name.text, expr.expression, expr); +} + +/** The class a static member read through `receiver` reads exactly: the + * class a name binds (directly, through a require binding, or through a + * const alias), or null. */ +export function staticReceiverClass(lowerer: Lowerer, receiver: ts.Expression): ClassInfo | null { if ( - !ts.isIdentifier(expr.expression) && - expr.expression.kind !== ts.SyntaxKind.ThisKeyword && - exactClassOfReceiver(lowerer, expr.expression) === null + !ts.isIdentifier(receiver) && + receiver.kind !== ts.SyntaxKind.ThisKeyword && + exactClassOfReceiver(lowerer, receiver) === null ) return null; - const symbol = ts.isIdentifier(expr.expression) - ? lowerer.resolveValueSymbol(expr.expression) - : null; + const symbol = ts.isIdentifier(receiver) ? lowerer.resolveValueSymbol(receiver) : null; const info = (symbol ? lowerer.classBySymbol.get(symbol) : undefined) ?? // A require binding over `module.exports = class {…}` (the alias @@ -5692,7 +5788,7 @@ export function lowerStaticFieldRead( // or `C.name` below would fall through to paths that answer for // stdlib globals instead of this class. propertyAssignedClassInfoOf(lowerer, symbol) ?? - exactClassOfReceiver(lowerer, expr.expression) ?? + exactClassOfReceiver(lowerer, receiver) ?? undefined; if (!info) return null; // A decorated name that can REBIND (a replacing decorator): the @@ -5701,22 +5797,36 @@ export function lowerStaticFieldRead( // whose devirtualization and .name rules answer for every legal // runtime value. if (info.classDecorators?.valueGlobalId !== undefined) return null; - const loc = locOf(expr); - const found = findStaticOn(lowerer, info, expr.name.text); + return info; +} + +/** The static member `name` of `info` read through `receiver` (an exact + * reference to the class, see staticReceiverClass): `C.name` for the + * property access `site`, or a destructuring element over `C` whose + * initializer is `site`. Null when the class has no such member. */ +export function lowerClassStaticRead( + lowerer: Lowerer, + info: ClassInfo, + name: string, + receiver: ts.Expression, + site: ts.Expression, +): IrExpr | null { + const loc = locOf(site); + const found = findStaticOn(lowerer, info, name); // A Date static read as a value through a class declared `extends Date`: // the statics lower only as calls. if ( !found && dateRootOf(info) && - ["now", "parse", "UTC"].includes(expr.name.text) && - !(ts.isCallExpression(expr.parent) && expr.parent.expression === expr) + ["now", "parse", "UTC"].includes(name) && + !(ts.isCallExpression(site.parent) && site.parent.expression === site) ) - lowerer.noLowering(`Date.${expr.name.text} read through a class that extends Date`, expr); + lowerer.noLowering(`Date.${name} read through a class that extends Date`, site); if (hasRuntimeStatics(info) && !found?.field) return { kind: "dynKeyGet", - value: lowerer.coerceToExpected(classValueRef(lowerer, info, expr.expression), DYN), - key: { kind: "strLit", value: expr.name.text, type: STRING, loc }, + value: lowerer.coerceToExpected(classValueRef(lowerer, info, receiver), DYN), + key: { kind: "strLit", value: name, type: STRING, loc }, type: DYN, loc, }; @@ -5724,11 +5834,11 @@ export function lowerStaticFieldRead( // name: in JS the brand lives on that one constructor object, so // `D.#s` (a subclass receiver) throws Node's TypeError instead of // reaching up the chain — reported as SC1090 instead of resolved. - if (found && expr.name.text.startsWith("#") && found.declarer !== info) { + if (found && name.startsWith("#") && found.declarer !== info) { lowerer.unsupported( "SC1090", - expr, - `reading the private static '${expr.name.text}' through the subclass '${info.def.name.replace(/^%|^%m\d+\./, "")}' (JS brands the declaring class object alone — Node throws a TypeError here; spell the declaring class's name)`, + site, + `reading the private static '${name}' through the subclass '${info.def.name.replace(/^%|^%m\d+\./, "")}' (JS brands the declaring class object alone — Node throws a TypeError here; spell the declaring class's name)`, ); } if (found?.field !== undefined) { @@ -5738,29 +5848,31 @@ export function lowerStaticFieldRead( type: found.field.type, loc, }; - return found.field.runtimeOptional ? value : lowerer.maybeNarrow(value, expr); + return found.field.runtimeOptional || !ts.isPropertyAccessExpression(site) + ? value + : lowerer.maybeNarrow(value, site); } if (found) { - return staticMethodValue(lowerer, found.declarer, expr.name.text, found.method, expr, loc); + return staticMethodValue(lowerer, found.declarer, name, found.method, site, loc); } // A GENERIC static method as a VALUE: the pinned-value rule verbatim // (lowerGenericFnValue) — a slot spelling one concrete signature names // an instance, an unpinned reference fences by name. { - const gfound = findGenericStaticOn(lowerer, info, expr.name.text); - if (gfound) return lowerer.lowerGenericFnValue(expr, gfound.info); + const gfound = findGenericStaticOn(lowerer, info, name); + if (gfound) return lowerer.lowerGenericFnValue(site, gfound.info); } // `C.name` — the JS-observable class name, a compile-time constant on // the direct spelling (tsc rejects user statics named `name`, so the // chain above can never shadow it in TypeScript sources). A class of an // import cycle reads it from its class object instead, whose reference // checks the class's initialization (checkCycleBindingAccesses). - if (expr.name.text === "name" && info.def.jsName !== undefined) { + if (name === "name" && info.def.jsName !== undefined) { if (lowerer.classInitFlags.get(info.def.name)?.cycle === true) return { kind: "libCall", fn: "class.name", - args: [classValueRef(lowerer, info, expr.expression)], + args: [classValueRef(lowerer, info, receiver)], type: STRING, loc, }; @@ -5769,26 +5881,26 @@ export function lowerStaticFieldRead( if (info.callableBase) return { kind: "dynKeyGet", - value: lowerer.coerceToExpected(classValueRef(lowerer, info, expr.expression), DYN), - key: { kind: "strLit", value: expr.name.text, type: STRING, loc }, + value: lowerer.coerceToExpected(classValueRef(lowerer, info, receiver), DYN), + key: { kind: "strLit", value: name, type: STRING, loc }, type: DYN, loc, }; - const data = classStaticDataFor(lowerer, info, expr.name.text, loc); + const data = classStaticDataFor(lowerer, info, name, loc); if (data) { const value: IrExpr = { kind: "dynKeyGet", value: data, - key: { kind: "strLit", value: expr.name.text, type: STRING, loc }, + key: { kind: "strLit", value: name, type: STRING, loc }, type: DYN, loc, }; // JavaScript often assigns public statics after the class declaration. // Their storage stays mutable and checked, while reads retain the // checker's concrete type instead of losing the method receiver. - const type = isJsSourceFile(ts.getSourceFileOfNode(expr)) + const type = isJsSourceFile(ts.getSourceFileOfNode(site)) ? null - : lowerer.mapTypeOf(lowerer.typeOf(expr)); + : lowerer.mapTypeOf(lowerer.typeOf(site)); return type ? lowerer.coerceToExpected(value, type) : value; } return null; @@ -6152,6 +6264,7 @@ function lowerClassValue( const context = newFnCtx(true, null, null, VOID); info.localClass.context = context; lowerer.fnStack.push(context); + const diagsBefore = lowerer.diags.length; try { const bodies = lowerClassMembers(lowerer, info); for (const body of bodies) info.localClass.bodies.set(body.name, body); @@ -6166,6 +6279,8 @@ function lowerClassValue( }); } info.localClass.ready = true; + // A retry of the enclosing statement reuses these bodies. + for (const d of lowerer.diags.slice(diagsBefore)) lowerer.loweredOnceDiags.add(d); lowerer.onExprClassCollected?.(info); } finally { lowerer.fnStack.pop(); @@ -6614,7 +6729,7 @@ function staticCallOn( kind: "dynInvoke", recv: lowerer.lowerExprExpecting(access.expression, DYN), method: access.name.text, - calleeName: access.name.text, + calleeName: ts.getTextOfNode(access), args: call.arguments.nodes.map((argument) => lowerer.lowerExprExpecting(argument, DYN)), type: DYN, loc, @@ -6970,8 +7085,8 @@ function capturesEnclosingScope( * reflection: the member a runtime lookup finds can then differ. */ function memberAssigned(lowerer: Lowerer, member: ts.MethodDeclaration, name: string): boolean { let found = false; - for (const sf of lowerer.program.getSourceFiles()) { - if (sf.isDeclarationFile || found) continue; + for (const sf of lowerer.program.getImplementationSourceFiles()) { + if (found) continue; if (!sf.text.includes(name)) continue; ts.walkPreorder(sf, (node) => { if (found) return "skip"; @@ -7586,6 +7701,16 @@ export function collectVirtualJsMethods(lowerer: Lowerer, files: readonly ts.Sou if (!ts.isIdentifier(member.name)) continue; const property = lowerer.checker.getPropertyOfType(base, member.name.text); for (const declaration of property ? property.declarations : []) { + if ( + ts.isMethodDeclaration(declaration) && + isJsSourceFile(ts.getSourceFileOfNode(declaration)) && + checkedJsOverride(lowerer, declaration, member) + ) { + // A checked override pair dispatches through the vtable, + // so neither side may become an implicit generic. + lowerer.checkedJsOverrideMethods.add(declaration); + lowerer.checkedJsOverrideMethods.add(member); + } if ( ts.isMethodDeclaration(declaration) && isJsSourceFile(ts.getSourceFileOfNode(declaration)) && @@ -7672,7 +7797,7 @@ function checkedJsOverride( ): boolean { return [base, member].every( (method) => - lowerer.checkedJsFile(ts.getSourceFileOfNode(method)) && + (lowerer.checkedJsFile(ts.getSourceFileOfNode(method)) || shapeOverlayAnnotated(method)) && method.parameters.nodes.every(documentedParameter), ); } @@ -7923,6 +8048,12 @@ export function lowerClassMembers(lowerer: Lowerer, info: ClassInfo): IrFunction const diagsBefore = lowerer.diags.length; try { out.push(lowerer.lowerClassCtor(info)); + if (info.extendsArray) { + const fields = arrayClassFieldsFn(lowerer, info); + if (fields) out.push(fields); + const species = arrayClassSpeciesFn(lowerer, info); + if (species) out.push(species); + } } catch (e) { if (!(e instanceof PoisonError)) throw e; if (info.decl && isJsSourceFile(ts.getSourceFileOfNode(info.decl))) { @@ -7971,6 +8102,10 @@ export function lowerClassMembers(lowerer: Lowerer, info: ClassInfo): IrFunction * the base's signature — forward every param to super(), then run own * field initializers. */ export function lowerClassCtor(lowerer: Lowerer, info: ClassInfo): IrFunction { + if (info.extendsArray) { + lowerer.noteEdge(`%${info.def.name}.arrayFields`); + lowerer.noteEdge(`%${info.def.name}.species`); + } const cached = info.localClass?.bodies.get(`%${info.def.name}.constructor`); if (cached) return cached; return withInstanceBindings(lowerer, info, () => lowerClassCtorInner(lowerer, info)); @@ -8038,7 +8173,7 @@ function lowerClassCtorInner(lowerer: Lowerer, info: ClassInfo): IrFunction { lowerer.ctx.argumentsLocal = local; } body.push(...declared.prologue); - if (!ctorBase && !info.callableBase && !info.extendsDate) { + if (!ctorBase && !info.callableBase && !info.extendsDate && !info.extendsArray) { // Node's base-class order: field initializers run at the start // of construction, the parameter-property assignments open the // constructor body (probed — a field initializer reading a @@ -8050,7 +8185,7 @@ function lowerClassCtorInner(lowerer: Lowerer, info: ClassInfo): IrFunction { body.push(...lowerer.lowerDerivedCtorBody(info, thisLocal)); } } else { - if (ctorBase || info.callableBase || info.extendsDate) { + if (ctorBase || info.callableBase || info.extendsDate || info.extendsArray) { // Synthetic forwarding params (the inherited ABI signature). // Nothing references them by symbol — only the super call below, // which forwards the already-completed values UNCHANGED (defaults @@ -8150,7 +8285,10 @@ export function classMemberNameOf(lowerer: Lowerer, name: ts.PropertyName): stri member === "asyncIterator" || member === "dispose" || member === "asyncDispose" || - member === "toPrimitive" + member === "toPrimitive" || + // A toStringTag field keeps its symbol field name; its accessor and + // method forms take the method slot name. + (member === "toStringTag" && !ts.isPropertyDeclaration(name.parent)) ) { return `sym:${member}`; } @@ -8177,31 +8315,83 @@ export function classMemberNameOf(lowerer: Lowerer, name: ts.PropertyName): stri export interface ClassIteratorInfo { /** The `[Symbol.iterator]()` call's receiver class + its declarer. */ className: string; - /** The iterator object's type (the method's return). */ - iterT: IrType & { kind: "object" }; + /** The iterator's type (the method's return): a class instance, or a + * record (an object literal) whose `next` field is a function. */ + iterT: IrType & ({ kind: "object" } | { kind: "record" }); /** next()'s result record type. */ resultT: IrType & { kind: "record" }; valueT: IrType; /** False: no done field — the protocol never terminates. */ hasDone: boolean; + /** The iterator declares a zero-parameter return(), which a consumer + * that stops before the end calls (IteratorClose). Only consumers that + * ask for `close` see such iterators. */ + hasReturn: boolean; } -export function classIteratorOf(lowerer: Lowerer, t: IrType): ClassIteratorInfo | null { +/** A return() result type that is always an object: IteratorClose after a + * break throws a TypeError for any other result, which these desugars do + * not model. */ +function objectResult(t: IrType): boolean { + return t.kind === "record" || t.kind === "object"; +} + +/** What a class iterable's consumer handles: `records` accepts an + * object-literal iterator; `close` accepts an iterator with return(), + * which the consumer calls when it stops early (or never stops early). */ +export interface ClassIteratorOptions { + records?: boolean; + close?: boolean; +} +export function classIteratorOf( + lowerer: Lowerer, + t: IrType, + options: ClassIteratorOptions = {}, +): ClassIteratorInfo | null { if (t.kind !== "object") return null; const info = lowerer.classes.get(t.className); if (!info) return null; const iter = findMethodOn(lowerer, info, "sym:iterator"); if (!iter || iter.sig.params.length !== 0 || iter.sig.abstract === true) return null; const iterT = iter.sig.ret; - if (iterT.kind !== "object") return null; - const itInfo = lowerer.classes.get(iterT.className); - if (!itInfo) return null; - // IteratorClose honesty: a declared return()/throw() would be called - // by JS on abrupt completion; these desugars never close. - if (findMethodOn(lowerer, itInfo, "return") || findMethodOn(lowerer, itInfo, "throw")) - return null; - const next = findMethodOn(lowerer, itInfo, "next"); - if (!next || next.sig.params.length !== 0 || next.sig.abstract === true) return null; - const resultT = next.sig.ret; + let resultT: IrType; + let hasReturn: boolean; + if (iterT.kind === "object") { + const itInfo = lowerer.classes.get(iterT.className); + if (!itInfo) return null; + // IteratorClose honesty: a declared return() would be called by JS on + // abrupt completion, so only a consumer that closes takes it; for-of + // never calls throw(), the other consumers refuse it. + const close = findMethodOn(lowerer, itInfo, "return"); + if ( + close && + (!options.close || + close.sig.abstract === true || + close.sig.params.length !== 0 || + !objectResult(close.sig.ret)) + ) + return null; + if (findMethodOn(lowerer, itInfo, "throw") && !options.close) return null; + const next = findMethodOn(lowerer, itInfo, "next"); + if (!next || next.sig.params.length !== 0 || next.sig.abstract === true) return null; + resultT = next.sig.ret; + hasReturn = !!close; + } else if (iterT.kind === "record" && options.records) { + const fields = lowerer.shapes.get(iterT.shapeId)?.fields; + const next = fields?.find((f) => f.name === "next")?.type; + if (!next || next.kind !== "func" || next.params.length !== 0 || next.rest) return null; + const close = fields?.find((f) => f.name === "return")?.type; + if ( + close && + (!options.close || + close.kind !== "func" || + close.params.length !== 0 || + close.rest || + !objectResult(close.ret)) + ) + return null; + resultT = next.ret; + hasReturn = close !== undefined; + } else return null; if (resultT.kind !== "record") return null; const shape = lowerer.shapes.get(resultT.shapeId); const value = shape?.fields.find((f) => f.name === "value"); @@ -8214,18 +8404,82 @@ export function classIteratorOf(lowerer: Lowerer, t: IrType): ClassIteratorInfo resultT, valueT: value.type, hasDone: done !== undefined, + hasReturn, }; } -/** The `it.next()` step of a class iterator as an ordinary (possibly - * virtual) method call. */ -export function classIteratorNextCall( +/** The `next` an object-literal iterator's protocol calls, read once when + * the iterator opens: JS keeps the `next` it reads then in the iterator + * record, so a `next` the iterator assigns while it runs is not called. + * Null for an iterator class, whose `next` is a method. */ +export function classIteratorNextRead( lowerer: Lowerer, cit: ClassIteratorInfo, itRef: IrExpr, loc: SrcLoc, +): (IrExpr & { type: IrType & { kind: "func" } }) | null { + const iterT = cit.iterT; + if (iterT.kind === "object") return null; + const field = lowerer.shapes.get(iterT.shapeId)!.fields.find((f) => f.name === "next")!; + return { + kind: "recordGet", + obj: itRef, + shapeId: iterT.shapeId, + field: "next", + type: field.type as IrType & { kind: "func" }, + loc, + }; +} + +/** The `it.next()` step of a class iterator: an ordinary (possibly + * virtual) method call, or the call of `next`, the object-literal + * iterator's `next` that classIteratorNextRead read when it opened. */ +export function classIteratorNextCall( + lowerer: Lowerer, + cit: ClassIteratorInfo, + itRef: IrExpr, + next: IrExpr | null, + loc: SrcLoc, ): IrExpr { - return accessorCall(lowerer, cit.iterT.className, "next", itRef, [], cit.resultT, loc); + if (next === null || next.type.kind !== "func") + return classIteratorMethodCall(lowerer, cit, "next", itRef, loc); + return { kind: "callValue", callee: next, args: [], type: next.type.ret, loc }; +} + +/** `it.next()` or `it.return()` of a class iterator: a (possibly virtual) + * method call of an iterator class, or the call of an object-literal + * iterator's function field. */ +export function classIteratorMethodCall( + lowerer: Lowerer, + cit: ClassIteratorInfo, + method: "next" | "return", + itRef: IrExpr, + loc: SrcLoc, +): IrExpr { + const iterT = cit.iterT; + if (iterT.kind === "object") { + const ret = + method === "next" + ? cit.resultT + : findMethodOn(lowerer, lowerer.classes.get(iterT.className)!, "return")!.sig.ret; + return accessorCall(lowerer, iterT.className, method, itRef, [], ret, loc); + } + const field = lowerer.shapes.get(iterT.shapeId)!.fields.find((f) => f.name === method)!; + const fnT = field.type as IrType & { kind: "func" }; + return { + kind: "callValue", + callee: { + kind: "recordGet", + obj: itRef, + shapeId: iterT.shapeId, + field: method, + type: fnT, + loc, + }, + args: [], + type: fnT.ret, + loc, + }; } /** `recv[Symbol.iterator]()` as an ordinary method call. */ @@ -8253,12 +8507,15 @@ export function classIteratorDrainCall( loc: SrcLoc, elemT?: IrType, ): IrExpr | null { - const cit = classIteratorOf(lowerer, src.type); + // A spread takes every value, so it never closes the iterator. + const cit = classIteratorOf(lowerer, src.type, { records: true, close: true }); if (!cit) return null; const outElem = elemT ?? cit.valueT; // Probe the element coercion purely: identical types, or an arm of a // union destination (the wrap coerceToExpected applies below). - if (!typeEquals(outElem, cit.valueT)) { + // A checked-dynamic value (`IteratorResult`'s value is `T | any`) + // converts to the destination element with a check. + if (!typeEquals(outElem, cit.valueT) && cit.valueT.kind !== "dyn") { if (outElem.kind !== "union" || lowerer.armTag(outElem.unionId, cit.valueT) < 0) return null; } const outT = arrayOf(outElem); @@ -8270,6 +8527,8 @@ export function classIteratorDrainCall( const recvT: IrType = { kind: "object", className: cit.className }; const recvRef: IrExpr = { kind: "varRef", localId: "r.0", type: recvT, loc }; const itRef: IrExpr = { kind: "varRef", localId: "it.0", type: cit.iterT, loc }; + const nextRead = classIteratorNextRead(lowerer, cit, itRef, loc); + const nextRef: IrExpr | null = nextRead ? varRef("next.0", nextRead.type, loc) : null; const outRef: IrExpr = { kind: "varRef", localId: "out.0", type: outT, loc }; const resRef: IrExpr = { kind: "varRef", localId: "res.0", type: cit.resultT, loc }; const valueRead: IrExpr = { @@ -8284,7 +8543,7 @@ export function classIteratorDrainCall( { kind: "varDecl", localId: "res.0", - init: classIteratorNextCall(lowerer, cit, itRef, loc), + init: classIteratorNextCall(lowerer, cit, itRef, nextRef, loc), loc, }, ...(cit.hasDone @@ -8325,6 +8584,7 @@ export function classIteratorDrainCall( locals: [ { id: "r.0", name: "r", type: recvT, mutable: false }, { id: "it.0", name: "it", type: cit.iterT, mutable: false }, + ...(nextRead ? [{ id: "next.0", name: "next", type: nextRead.type, mutable: false }] : []), { id: "out.0", name: "out", type: outT, mutable: false }, { id: "res.0", name: "res", type: cit.resultT, mutable: true }, ], @@ -8341,6 +8601,9 @@ export function classIteratorDrainCall( init: classIteratorOpenCall(lowerer, cit, recvRef, loc), loc, }, + ...(nextRead + ? [{ kind: "varDecl", localId: "next.0", init: nextRead, loc } satisfies IrStmt] + : []), { kind: "while", cond: { kind: "boolLit", value: true, type: BOOL, loc }, @@ -8376,7 +8639,7 @@ export function classIteratorRestDrainCall( loc: SrcLoc, ): IrExpr { const outT = arrayOf(cit.valueT); - const key = `it:${cit.iterT.className}`; + const key = `it:${cit.iterT.kind === "object" ? cit.iterT.className : typeKey(cit.iterT)}`; let name = lowerer.iterDrainHelpers.get(key); if (!name) { name = keyedHelperName(lowerer, "%iter.drain", key); @@ -8388,7 +8651,7 @@ export function classIteratorRestDrainCall( { kind: "varDecl", localId: "res.0", - init: classIteratorNextCall(lowerer, cit, itRef, loc), + init: classIteratorNextCall(lowerer, cit, itRef, null, loc), loc, }, ...(cit.hasDone @@ -8969,7 +9232,9 @@ export function lowerDerivedCtorBody( loc, }, ]; - if (info.callableBase || info.extendsDate) { + if (info.extendsArray) { + statements.push(...lowerArraySuperCall(lowerer, info, thisLocal, superCall)); + } else if (info.callableBase || info.extendsDate) { if (superCall.arguments.nodes.some(ts.isSpreadElement)) lowerer.unsupported( "SC1090", @@ -9162,6 +9427,7 @@ export function superCallStmt( loc, }; if (info.extendsDate) return dateSuperCall(thisRef, info, args[0]!, loc); + if (info.extendsArray) return arraySuperCall(info, thisRef, args[0]!, loc); if (info.callableBase) return { kind: "exprStmt", @@ -9384,22 +9650,51 @@ export function lowerSuperMethodCall( if (!thisLocal) lowerer.unsupported("SC1080", access); const loc = locOf(call); const thisRef: IrExpr = { kind: "varRef", localId: thisLocal.id, type: thisLocal.type, loc }; - if (lowerer.prototypeMethodAccesses.has(access.name.text)) { - const prototype = classPrototypeData(lowerer, cls.base, loc); - if (prototype) { - const callee: IrExpr = { - kind: "dynKeyGet", - value: prototype, - key: { kind: "strLit", value: access.name.text, type: STRING, loc }, - type: DYN, - loc, - }; + // A method whose `this` is the call-site receiver (a JavaScript class's + // receiver-independent method value, reflectedClassMethodValue) looks the + // method up on the base class's complete prototype, as `super` reads + // [[HomeObject]].[[Prototype]], and calls it with that receiver. + const dynamicThis = thisRef.type.kind === "dyn"; + // While no code reads a prototype method by name (which could replace it), + // that method is the declaring class's own: its receiver-independent value + // is called directly. Reflecting the base's whole prototype instead would + // put its members on the prototype chain of every subclass instance's + // property bag, ahead of the subclasses' own compiled overrides. + const declared = + dynamicThis && !lowerer.prototypeMethodAccesses.has(access.name.text) + ? reflectedClassMethodValue(lowerer, found.declarer, access.name.text) + : null; + if (dynamicThis || lowerer.prototypeMethodAccesses.has(access.name.text)) { + const prototype = declared + ? null + : dynamicThis && cls.base.decl + ? reflectClassPrototype(lowerer, cls.base, loc, classValueRef(lowerer, cls.base, access)) + : classPrototypeData(lowerer, cls.base, loc); + if (declared || prototype) { + const callee: IrExpr = declared + ? lowerer.coerceToExpected(declared, DYN) + : { + kind: "dynKeyGet", + value: prototype!, + key: { kind: "strLit", value: access.name.text, type: STRING, loc }, + type: DYN, + loc, + }; + // Spread arguments are spread into the dynamic call, as in a dynamic method call. + const spreads: { arg: number; what: string }[] = []; + const args = call.arguments.nodes.map((arg, index) => { + if (!ts.isSpreadElement(arg)) return lowerer.lowerExprExpecting(arg, DYN); + const spelling = ts.getTextOfNode(arg.expression); + spreads.push({ arg: index, what: spelling }); + return checkedIterableSpread(lowerer, lowerer.lowerExpr(arg.expression), spelling, loc); + }); const value: IrExpr = { kind: "dynCall", callee, receiver: lowerer.coerceToExpected(thisRef, DYN), calleeName: ts.getTextOfNode(access), - args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), + args, + ...(spreads.length > 0 ? { spreads } : {}), type: DYN, loc, }; @@ -9768,6 +10063,75 @@ function assignedThisFieldType(lowerer: Lowerer, expr: ts.NewExpression): IrType return lowerer.currentClass?.fields.get(parent.left.name.text) ?? null; } +/** An array element read that construction narrows to a class value with a + * check (`%new.callee`, which throws "is not a constructor"). */ +interface NarrowedConstructorRead { + read: IrExpr; + check: string; + unionT: IrType & { kind: "union" }; + calleeT: IrType & { kind: "classval" }; + message: IrExpr; +} + +/** `new arr[i](args)` through a narrowed element read: an interned lifted + * function takes the read and the completed arguments, which evaluate in + * that order as call arguments, then runs the check and constructs. JS + * evaluates the arguments before it checks that the callee constructs, and + * the arguments stay in the one call, where a spread's argument pack lives + * until every argument has read it. */ +function checkedElementConstruction( + lowerer: Lowerer, + info: ClassInfo, + narrowed: NarrowedConstructorRead, + args: IrExpr[], + loc: SrcLoc, +): IrExpr { + const type: IrType = { kind: "object", className: info.def.name }; + const key = `newChecked:${narrowed.check}:${info.def.name}:${args.map((a) => typeKey(a.type)).join(",")}`; + let name = lowerer.valueHelpers.get(key); + if (!name) { + name = keyedHelperName(lowerer, "%new.checked", key); + lowerer.valueHelpers.set(key, name); + const params: IrParam[] = [ + { localId: "value", name: "value", type: narrowed.unionT }, + { localId: "message", name: "message", type: STRING }, + ...args.map((arg, i) => ({ localId: `arg.${i}`, name: `arg${i}`, type: arg.type })), + ]; + lowerer.liftedFns.push({ + name, + params, + returnType: type, + locals: params.map((p) => ({ id: p.localId, name: p.name, type: p.type, mutable: false })), + body: [ + { + kind: "return", + value: { + kind: "newValue", + callee: { + kind: "call", + callee: narrowed.check, + args: [varRef("value", narrowed.unionT, loc), varRef("message", STRING, loc)], + type: narrowed.calleeT, + loc, + }, + args: params.slice(2).map((p) => varRef(p.localId, p.type, loc)), + type, + loc, + }, + loc, + }, + ], + loc, + }); + } + return { + kind: "call", + callee: name, + args: [narrowed.read, narrowed.message, ...args], + type, + loc, + }; +} /** Map, Set and the error classes construct in lowerNew's identifier * branch, which a module member alias of one (`new mod.M()`) also takes. */ function isCollectionOrErrorClass(ref: BuiltinClassRef | null): boolean { @@ -9776,9 +10140,9 @@ function isCollectionOrErrorClass(ref: BuiltinClassRef | null): boolean { export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { const loc = locOf(expr); - // A dependency's deprecated `new Buffer(...)` fallback (containers/bytes.ts). + // The deprecated `new Buffer(...)` (containers/bytes.ts). { - const buffer = lowerNodeModulesBufferConstruct(lowerer, expr); + const buffer = lowerBufferConstruct(lowerer, expr); if (buffer) return buffer; } // new Function(...): Node's EvalError under @@ -9805,7 +10169,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { } const worker = lowerWorkerNew(lowerer, expr); if (worker) return worker; - const readlineUi = lowerReadlineUiNew(lowerer, expr); + const readlineUi = lowerReadlineUiNew(lowerer, expr) ?? lowerReadlineInterfaceNew(lowerer, expr); if (readlineUi) return readlineUi; const channel = lowerMessageChannelNew(lowerer, expr) ?? lowerBroadcastChannelNew(lowerer, expr); if (channel) return channel; @@ -9888,34 +10252,54 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { const args = expr.arguments?.nodes ?? []; const undefinedArg = (arg: ts.Expression): boolean => (lowerer.typeOf(arg).flags & ts.TypeFlags.Undefined) !== 0; - if (args.length > 2 || (args[0] && !undefinedArg(args[0]))) { + // A literal language tag: Unicode grapheme clusters have no locale + // tailoring. Only tags of the form language[-script][-region] are + // accepted; each is well-formed, so Node constructs the segmenter + // without the RangeError a malformed tag throws. + const literalLocale = (arg: ts.Expression): boolean => + ts.isStringLiteralLike(arg) && + /^[A-Za-z]{2,3}(-[A-Za-z]{4})?(-([A-Za-z]{2}|[0-9]{3}))?$/.test(arg.text); + if (args.length > 2 || (args[0] && !undefinedArg(args[0]) && !literalLocale(args[0]))) { lowerer.noLowering( "Intl.Segmenter locale negotiation", expr, - "omit the locale or pass undefined for default Unicode grapheme segmentation", + "omit the locale, pass undefined, or pass one language tag as a string literal", ); } + // JavaScript dependencies may construct word and sentence segmenters + // that they use only on some paths (pi-tui's shared word segmenter): + // the construction compiles and their segment() throws SC2020. + // TypeScript sources keep the build-time refusal. + const granularities = isJsSourceFile(ts.getSourceFileOfNode(expr)) + ? ["grapheme", "word", "sentence"] + : ["grapheme"]; const options = args[1]; - if ( - options && - !undefinedArg(options) && - !( - ts.isObjectLiteralExpression(options) && - options.properties.nodes.every( - (property) => - ts.isPropertyAssignment(property) && - (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) && - property.name.text === "granularity" && - ts.isStringLiteral(property.initializer) && - property.initializer.text === "grapheme", - ) - ) - ) { - lowerer.noLowering( - "Intl.Segmenter options", - options, - 'the native segmenter supports default options and { granularity: "grapheme" }; word and sentence segmentation remain unsupported', - ); + let granularity = "grapheme"; + if (options && !undefinedArg(options)) { + // An object literal of `granularity` entries (none: the default; a + // repeated key: the last one, as in JavaScript). + if (!ts.isObjectLiteralExpression(options)) + lowerer.noLowering( + "Intl.Segmenter options", + options, + 'the native segmenter supports default options and { granularity: "grapheme" }; word and sentence segmentation remain unsupported', + ); + for (const property of options.properties.nodes) { + if ( + !ts.isPropertyAssignment(property) || + !(ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) || + property.name.text !== "granularity" || + !ts.isStringLiteral(property.initializer) || + !granularities.includes(property.initializer.text) + ) { + lowerer.noLowering( + "Intl.Segmenter options", + options, + 'the native segmenter supports default options and { granularity: "grapheme" }; word and sentence segmentation remain unsupported', + ); + } + granularity = property.initializer.text; + } } // Explicit undefined expressions can carry effects; retain argument order. const stmts: IrStmt[] = args.map((arg) => { @@ -9926,7 +10310,28 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { loc: locOf(arg), }; }); - const result: IrExpr = { kind: "libCall", fn: "intl.segmenterNew", args: [], type: DYN, loc }; + let result: IrExpr = { kind: "libCall", fn: "intl.segmenterNew", args: [], type: DYN, loc }; + // A word or sentence segmenter carries its segment() refusal, which + // coverage lists as deferred to runtime. + if (granularity !== "grapheme") { + const surface = `Intl.Segmenter segmentation with granularity '${granularity}'`; + lowerer.runtimeFences.push( + noLoweringDiag( + surface, + loc, + "only grapheme segmenters segment; word and sentence segmenters construct", + ), + ); + const line = ts.getLineAndCharacterOfPosition(ts.getSourceFileOfNode(expr), loc.start).line; + const refusal = `${surface} is not supported yet [SC2020 at ${loc.file}:${line + 1}]`; + result = { + kind: "libCall", + fn: "intl.segmenterRefusing", + args: [strLit(refusal, loc)], + type: DYN, + loc, + }; + } return stmts.length ? { kind: "seqExpr", stmts, result, type: DYN, loc } : result; } // A package declaration without a compiled constructor cannot provide @@ -10105,6 +10510,55 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { const agent = lowerHttpAgentNew(lowerer, expr); if (agent) return agent; } + // `new crypto.X509Certificate(data)` — the Dirent-style data record: + // the certificate's lowered members (fingerprint — the SHA-1 of the + // DER, uppercase colon-separated — plus the validFrom/validTo + // validity window in Node's ASN1_TIME_print shape) compute AT + // CONSTRUCTION, when Node parses too, so unparseable input throws + // Node's exact PEM error here (ERR_OSSL_PEM_NO_START_LINE) and the + // handle never exists. Both import forms (`crypto.X509Certificate` + // through the namespace, named `X509Certificate`); Buffer input only + // — the readFileSync idiom. mapType interns the matching record, so + // locals and the composed member reads all flow. The data argument + // feeds THREE field computations, so construction goes through an + // interned helper whose parameter evaluates it exactly once. + // It runs before the standard-library constructor fence below, which + // also sees `const X = require("crypto").X509Certificate` as the + // builtin class. + { + const callee = expr.expression; + const isX509 = + (ts.isPropertyAccessExpression(callee) && + callee.name.text === "X509Certificate" && + lowerer.builtinNamespaceModuleOf(callee.expression) === "crypto") || + (ts.isIdentifier(callee) && + (() => { + const bi = lowerer.builtinImportOf(callee); + return bi?.module === "crypto" && bi.member === "X509Certificate"; + })()); + if (isX509) { + const args: readonly ts.Expression[] = expr.arguments?.nodes ?? []; + if (args.length !== 1) { + lowerer.noLowering( + "X509Certificate with this argument shape", + expr, + "the supported form is new X509Certificate(readFileSync(path))", + ); + } + const data = lowerer.lowerExpr(args[0]!); + const isBytes = data.type.kind === "bytes" && data.type.elem === "u8"; + if (!isBytes && data.type.kind !== "string") { + lowerer.noLowering( + `X509Certificate over '${lowerer.fmt(data.type)}' data`, + args[0]!, + "pass the certificate Buffer or PEM string (an fs.readFileSync result)", + ); + } + const t = lowerer.mapTypeOf(lowerer.typeOf(expr)); + if (t?.kind !== "record") lowerer.badType(expr, lowerer.typeOf(expr)); + return x509RecordCall(lowerer, data, t, loc); + } + } if ( ts.isIdentifier(expr.expression) || (ts.isPropertyAccessExpression(expr.expression) && @@ -10118,7 +10572,12 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { // (a no-op for every non-import= binding). if (ts.isIdentifier(expr.expression)) fenceEarlyAliasUse(lowerer, expr.expression, expr); const identifier = ts.isIdentifier(expr.expression) ? expr.expression : expr.expression.name; - const symbol = ts.isIdentifier(identifier) ? lowerer.resolveValueSymbol(identifier) : null; + // `const SD = require("string_decoder").StringDecoder; new SD()` + // constructs the member, as a named import of it does. + const symbol = builtinMemberBindingTarget( + lowerer, + ts.isIdentifier(identifier) ? lowerer.resolveValueSymbol(identifier) : null, + ); if (ts.isIdentifier(identifier) && lowerer.isStdlibGlobal(identifier, "Array")) { return lowerArrayConstructor(lowerer, expr, expr.arguments?.nodes ?? []); } @@ -10418,8 +10877,33 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { if (arg.type.kind === "string") { return { kind: "libCall", fn: "date.newString", args: [arg], type: DATE_T, loc }; } + // new Date(date): a new Date object with the same time value. if (arg.type.kind === "date") { - return arg; + const time: IrExpr = { kind: "libCall", fn: "date.getTime", args: [arg], type: F64, loc }; + return { kind: "libCall", fn: "date.newMs", args: [time], type: DATE_T, loc }; + } + // A union (`Date | string | number`) or checked-dynamic value: the + // runtime's constructor reads a Date's time value, parses a string + // and converts anything else to a primitive and a number. + if ( + (arg.type.kind === "union" || arg.type.kind === "dyn") && + lowerer.dynConvertible(arg.type) + ) { + const value: IrExpr = { + kind: "libCall", + fn: "date.nativeNewValue", + args: [ + { + kind: "dynArrLit", + elems: [lowerer.coerceToExpected(arg, DYN)], + type: DYN, + loc, + }, + ], + type: DYN, + loc, + }; + return { kind: "libCall", fn: "date.checkedValue", args: [value], type: DATE_T, loc }; } lowerer.noLowering( `new Date of '${lowerer.fmt(arg.type)}' values`, @@ -10552,7 +11036,11 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { // expression). The CONTEXTUAL type carries the annotation — adopt // it when it is a supported map. TS type arguments keep winning: // their expression type already maps. - if (mapped?.kind !== "map" || contextualSeed) { + const inferred = initializedCollectionType(lowerer, expr); + if (inferred) { + tsType = inferred; + mapped = lowerer.mapTypeOf(inferred); + } else if (mapped?.kind !== "map" || contextualSeed) { const ctx = lowerer.checker.getContextualType(expr); const ctxMapped = ctx ? lowerer.mapTypeOf(ctx) : null; if (ctx && ctxMapped?.kind === "map") { @@ -10622,7 +11110,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { expr, `Map values of type '${lowerer.checker.typeToString(targs[1])}' ` + `(Map values must be number, string, boolean, records, class instances, functions, ` + - `typed arrays, arrays, promises, Maps, Sets, or supported unions and checked values)`, + `typed arrays, arrays, promises, Dates, Maps, Sets, or supported unions and checked values)`, ); } lowerer.badType(expr, tsType); @@ -10692,7 +11180,8 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { // `new Set()`: construct + bulk add from supported values, // including live collection cursors. Duplicates collapse in insertion order. if (ts.isIdentifier(identifier) && lowerer.isStdlibGlobal(identifier, "Set")) { - const tsType = lowerer.typeOf(expr); + const inferred = initializedCollectionType(lowerer, expr); + const tsType = inferred ?? lowerer.typeOf(expr); let mapped = lowerer.mapTypeOf(tsType); const fieldType = assignedThisFieldType(lowerer, expr); if (mapped?.kind !== "set" && fieldType?.kind === "set") mapped = fieldType; @@ -10766,21 +11255,23 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { lowerer.badType(expr, tsType); } // `new AsyncLocalStorage()` (node:async_hooks): a fresh store id — - // an f64 handle (type-mapper.ts), the Channel story. Construction options - // ({ defaultValue, name }) have no lowering yet. + // an f64 handle (type-mapper.ts), the Channel story. The options form + // ({ defaultValue, name }) passes the options object as a dyn value. if ( symbol !== null && ts.symbolName(symbol) === "AsyncLocalStorage" && lowerer.isStdlibSymbol(symbol) ) { - if ((expr.arguments?.nodes.length ?? 0) > 0) { - lowerer.noLowering( - "new AsyncLocalStorage(options)", - expr, - "the zero-argument constructor is the supported form (defaultValue/name options have no lowering yet)", - ); - } - return { kind: "libCall", fn: "als.new", args: [], type: F64, loc }; + const options = expr.arguments?.nodes[0]; + if (options === undefined) + return { kind: "libCall", fn: "als.new", args: [], type: F64, loc }; + return { + kind: "libCall", + fn: "als.newOptions", + args: [lowerAsyncLocalStorageOptions(lowerer, options)], + type: F64, + loc, + }; } // `new Promise((resolve) => ...)`: the ambient Promise constructor. if (symbol !== null && ts.symbolName(symbol) === "Promise" && lowerer.isStdlibSymbol(symbol)) { @@ -10855,43 +11346,6 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { // families carry pointed hints: each states WHY no honest static // lowering exists (or what to use instead). if (lowerer.isStdlibSymbol(symbol ?? undefined)) { - // The deprecated `new Buffer(string, encoding?)` ctor's string arm - // with a NON-STRING first argument and a string second: Node - // throws ERR_INVALID_ARG_TYPE synchronously (and DEP0005 never - // fires on this throwing path, so the compiled silence matches). - // Every other new Buffer form keeps the fence below — the - // constructing forms would owe the deprecation warning. - if ( - expr.expression.text === "Buffer" && - expr.arguments?.nodes.length === 2 && - !expr.arguments.nodes.some(ts.isSpreadElement) && - lowerer.mapTypeOf(lowerer.typeOf(expr.arguments.nodes[0]!))?.kind !== "string" && - lowerer.mapTypeOf(lowerer.typeOf(expr.arguments.nodes[1]!))?.kind === "string" - ) { - const first = lowerer.lowerExpr(expr.arguments.nodes[0]!); - if ( - first.kind === "unitLit" || - first.type.kind === "dyn" || - lowerer.dynConvertible(first.type) - ) { - const got: IrExpr = - first.type.kind === "dyn" ? first : { kind: "dynFrom", value: first, type: DYN, loc }; - // The encoding argument still evaluates in Node before the - // throw only via the ctor body's later reads — it does NOT - // observe it before throwing, so dropping it is exact for - // effect-free operands; effectful ones keep the fence. - const enc = lowerer.lowerExpr(expr.arguments.nodes[1]!); - if (enc.kind === "strLit" || isSafeToRepeat(enc)) { - return { - kind: "libCall", - fn: "buffer.newStringFail", - args: [got], - type: bytesOf("u8"), - loc, - }; - } - } - } const ctorHints: Record = { RegExp: "use a regex literal (/pattern/flags) — constructed regexes have no lowering", String: @@ -10918,99 +11372,6 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { ); } } - // `new crypto.X509Certificate(data)` — the Dirent-style data record: - // the certificate's lowered members (fingerprint — the SHA-1 of the - // DER, uppercase colon-separated — plus the validFrom/validTo - // validity window in Node's ASN1_TIME_print shape) compute AT - // CONSTRUCTION, when Node parses too, so unparseable input throws - // Node's exact PEM error here (ERR_OSSL_PEM_NO_START_LINE) and the - // handle never exists. Both import forms (`crypto.X509Certificate` - // through the namespace, named `X509Certificate`); Buffer input only - // — the readFileSync idiom. mapType interns the matching record, so - // locals and the composed member reads all flow. The data argument - // feeds THREE field computations, so construction goes through an - // interned helper whose parameter evaluates it exactly once. - { - const callee = expr.expression; - const isX509 = - (ts.isPropertyAccessExpression(callee) && - callee.name.text === "X509Certificate" && - lowerer.builtinNamespaceModuleOf(callee.expression) === "crypto") || - (ts.isIdentifier(callee) && - (() => { - const bi = lowerer.builtinImportOf(callee); - return bi?.module === "crypto" && bi.member === "X509Certificate"; - })()); - if (isX509) { - const args: readonly ts.Expression[] = expr.arguments?.nodes ?? []; - if (args.length !== 1) { - lowerer.noLowering( - "X509Certificate with this argument shape", - expr, - "the supported form is new X509Certificate(readFileSync(path))", - ); - } - const data = lowerer.lowerExpr(args[0]!); - const isBytes = data.type.kind === "bytes" && data.type.elem === "u8"; - if (!isBytes && data.type.kind !== "string") { - lowerer.noLowering( - `X509Certificate over '${lowerer.fmt(data.type)}' data`, - args[0]!, - "pass the certificate Buffer or PEM string (an fs.readFileSync result)", - ); - } - const t = lowerer.mapTypeOf(lowerer.typeOf(expr)); - if (t?.kind !== "record") lowerer.badType(expr, lowerer.typeOf(expr)); - const key = `x509.record:${isBytes ? "bytes" : "str"}`; - let helper = lowerer.valueHelpers.get(key); - if (!helper) { - helper = keyedHelperName(lowerer, "%x509.record", key); - lowerer.valueHelpers.set(key, helper); - const dataT = data.type; - const dRef: IrExpr = { kind: "varRef", localId: "d.0", type: dataT, loc }; - const field = ( - name: string, - fn: - | "crypto.x509Fingerprint" - | "crypto.x509FingerprintStr" - | "crypto.x509ValidFrom" - | "crypto.x509ValidFromStr" - | "crypto.x509ValidTo" - | "crypto.x509ValidToStr", - ): { name: string; value: IrExpr } => ({ - name, - value: { kind: "libCall", fn, args: [dRef], type: STRING, loc }, - }); - lowerer.liftedFns.push({ - name: helper, - params: [{ localId: "d.0", name: "d", type: dataT }], - returnType: t, - locals: [{ id: "d.0", name: "d", type: dataT, mutable: false }], - body: [ - { - kind: "return", - value: { - kind: "recordLit", - fields: [ - field( - "fingerprint", - isBytes ? "crypto.x509Fingerprint" : "crypto.x509FingerprintStr", - ), - field("validFrom", isBytes ? "crypto.x509ValidFrom" : "crypto.x509ValidFromStr"), - field("validTo", isBytes ? "crypto.x509ValidTo" : "crypto.x509ValidToStr"), - ], - type: t, - loc, - }, - loc, - }, - ], - loc, - }); - } - return { kind: "call", callee: helper, args: [data], type: t, loc }; - } - } // `new X(...)` through a class VALUE (a classval-typed binding, array // element, map read, param): the newValue dispatch through the class // object's construct thunk. Arguments complete against the STATIC @@ -11039,12 +11400,13 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { ); info = lowerer.classes.get(calleeT.className); } + // The TYPE world names a class the lowering never registered — a + // fenced class expression, an abstract/deferred declaration: flush + // its own diagnostics (they tell the real story, unless the flush + // registers it with dynamic slots) and poison this construction + // site, never an ICE. + if (!info) info = lowerer.flushDeferredClass(calleeT.className); if (!info) { - // The TYPE world names a class the lowering never registered — - // a fenced class expression, an abstract/deferred declaration: - // flush its own diagnostics (they tell the real story) and - // poison this construction site, never an ICE. - lowerer.flushDeferredClass(calleeT.className); lowerer.unsupported( "SC1090", expr, @@ -11062,8 +11424,109 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { "constructing through a class value of an uninstantiated generic type (annotate the slot with a concrete instantiation — e.g. 'new (v: number) => Box')", ); } - const callee = lowerer.lowerExpr(expr.expression); + let callee = lowerer.lowerExpr(expr.expression); if (callee.type.kind === "dyn") return lowerCheckedConstructor(lowerer, expr, callee); + /** The union-typed element read a narrowing check guards, and the + * check (`%new.callee`) that yields the class value or throws. */ + let narrowedCallee: NarrowedConstructorRead | null = null; + // An array element read (`new T[0]()` over `[Dog, Animal]`, whose + // checker type is the common base) keeps the undefined an index past + // the end reads: the class value is narrowed out with a check, which + // throws a TypeError for undefined. + if (callee.type.kind === "union") { + const arms = lowerer.unions.get(callee.type.unionId)?.arms ?? []; + const classArms = arms.filter((arm) => arm.kind === "classval"); + if ( + classArms.length === 1 && + classArms[0]!.kind === "classval" && + classArms[0]!.className === calleeT.className && + arms.every((arm) => arm.kind === "classval" || arm.kind === "undefinedT") + ) { + const unionT = callee.type; + const helper = lowerer.narrowedArmHelper(unionT.unionId, calleeT, callee.loc); + if (helper) { + const loc = callee.loc; + const name = `%new.callee:${unionT.unionId}`; + if (!lowerer.liftedFns.some((fn) => fn.name === name)) { + const undefinedTag = lowerer.armTag(unionT.unionId, UNDEFINED_T); + const value: IrExpr = { kind: "varRef", localId: "value", type: unionT, loc }; + lowerer.liftedFns.push({ + name, + params: [ + { localId: "value", name: "value", type: unionT }, + { localId: "message", name: "message", type: STRING }, + ], + locals: [ + { id: "value", name: "value", type: unionT, mutable: false }, + { id: "message", name: "message", type: STRING, mutable: false }, + ], + returnType: calleeT, + body: [ + ...(undefinedTag >= 0 + ? [ + { + kind: "if", + cond: { + kind: "unionIsTag", + unionId: unionT.unionId, + tag: undefinedTag, + negated: false, + value, + type: BOOL, + loc, + }, + then: [ + { + kind: "throw", + value: { + kind: "libCall", + fn: "error.new", + args: [{ kind: "varRef", localId: "message", type: STRING, loc }], + type: { kind: "object", className: "%TypeError" }, + loc, + }, + loc, + }, + ], + else_: null, + loc, + } satisfies IrStmt, + ] + : []), + { + kind: "return", + value: lowerer.checkedNarrowCall(helper, value, calleeT, loc), + loc, + }, + ], + loc, + }); + } + // JavaScript evaluates the arguments before it checks that the + // callee constructs, so the check runs at the construction + // (checkedElementConstruction). + narrowedCallee = { + read: callee, + check: name, + unionT, + calleeT, + message: { + kind: "strLit", + value: `${expr.expression.getText()} is not a constructor`, + type: STRING, + loc, + }, + }; + callee = { + kind: "call", + callee: name, + args: [callee, narrowedCallee.message], + type: calleeT, + loc, + }; + } + } + } if (callee.type.kind !== "classval") lowerer.badType(expr.expression, lowerer.typeOf(expr.expression)); lowerer.noteEdge(classValueConstructor(info.def).fn); @@ -11103,13 +11566,15 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { const built = constructedFromValue( lowerer, info, - { - kind: "newValue", - callee, - args, - type: { kind: "object", className: info.def.name }, - loc, - }, + narrowedCallee !== null + ? checkedElementConstruction(lowerer, info, narrowedCallee, args, loc) + : { + kind: "newValue", + callee, + args, + type: { kind: "object", className: info.def.name }, + loc, + }, expr, ); // A runtime class slot widened from a union (`[Readable, Writable]`'s @@ -11284,14 +11749,166 @@ function genericFieldFnNodeOf( return null; } +/** The lifted `new crypto.X509Certificate(d)` constructor: raw decodes the + * input once (Node's PEM error throws here), and every other field reads + * raw. subjectAltName, infoAccess and signatureAlgorithm hold undefined + * where the runtime answers "". */ +/** The X509Certificate record `t` built from `data`, a certificate Buffer + * (PEM or DER) or PEM string, through the interned constructor helper. + * `der` marks a Buffer known to hold a certificate's DER (a TLS socket's), + * which is read without looking for PEM armor inside it. */ +export function x509RecordCall( + lowerer: Lowerer, + data: IrExpr, + t: IrType & { kind: "record" }, + loc: SrcLoc, + der = false, +): IrExpr { + const isBytes = data.type.kind === "bytes" && data.type.elem === "u8"; + const key = `x509.record:${der ? "der" : isBytes ? "bytes" : "str"}`; + let helper = lowerer.valueHelpers.get(key); + if (!helper) { + helper = keyedHelperName(lowerer, "%x509.record", key); + lowerer.valueHelpers.set(key, helper); + lowerer.liftedFns.push(x509RecordHelper(lowerer, helper, data.type, t, isBytes, der, loc)); + } + return { kind: "call", callee: helper, args: [data], type: t, loc }; +} + +function x509RecordHelper( + lowerer: Lowerer, + name: string, + dataT: IrType, + t: IrType & { kind: "record" }, + isBytes: boolean, + der: boolean, + loc: SrcLoc, +): IrFunction { + const shape = lowerer.shapes.get(t.shapeId); + if (!shape) throw new InternalCompilerError("lowerer bug: X509Certificate shape unregistered"); + const raw: IrExpr = { kind: "varRef", localId: "raw.0", type: BYTES_U8, loc }; + const call = (fn: IrLibFn, type: IrType): IrExpr => ({ + kind: "libCall", + fn, + args: [raw], + type, + loc, + }); + const locals: IrLocal[] = [ + { id: "d.0", name: "d", type: dataT, mutable: false }, + { id: "raw.0", name: "raw", type: BYTES_U8, mutable: false }, + ]; + const decls: IrStmt[] = [ + { + kind: "varDecl", + localId: "raw.0", + init: { + kind: "libCall", + fn: der ? "crypto.x509RawDer" : isBytes ? "crypto.x509Raw" : "crypto.x509RawStr", + args: [{ kind: "varRef", localId: "d.0", type: dataT, loc }], + type: BYTES_U8, + loc, + }, + loc, + }, + ]; + const validTime = (fn: IrLibFn): IrExpr => ({ + kind: "libCall", + fn: "date.newString", + args: [call(fn, STRING)], + type: DATE_T, + loc, + }); + const values: Record IrExpr> = { + ca: () => call("crypto.x509Ca", BOOL), + fingerprint: () => call("crypto.x509Fingerprint", STRING), + fingerprint256: () => call("crypto.x509Fingerprint256", STRING), + fingerprint512: () => call("crypto.x509Fingerprint512", STRING), + subject: () => call("crypto.x509Subject", STRING), + subjectAltName: (type) => optionalText("san", "crypto.x509SubjectAltName", type), + infoAccess: (type) => optionalText("access", "crypto.x509InfoAccess", type), + issuer: () => call("crypto.x509Issuer", STRING), + raw: () => raw, + serialNumber: () => call("crypto.x509SerialNumber", STRING), + signatureAlgorithm: (type) => optionalText("sigalg", "crypto.x509SignatureAlgorithm", type), + signatureAlgorithmOid: () => call("crypto.x509SignatureAlgorithmOid", STRING), + validFrom: () => call("crypto.x509ValidFrom", STRING), + validFromDate: () => validTime("crypto.x509ValidFrom"), + validTo: () => call("crypto.x509ValidTo", STRING), + validToDate: () => validTime("crypto.x509ValidTo"), + [X509_PUBLIC_KEY_FIELD]: () => ({ + kind: "dynFrom", + value: { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc }, + type: DYN, + loc, + }), + // A copy: writes into `raw` must not reach the methods. + [X509_DER_FIELD]: () => call("crypto.x509RawDer", BYTES_U8), + }; + /** A string | undefined field: the runtime's text, or undefined for "". */ + function optionalText(local: string, fn: IrLibFn, type: IrType): IrExpr { + if (type.kind !== "union") + throw new InternalCompilerError("lowerer bug: X509Certificate optional field type"); + const def = lowerer.unions.get(type.unionId); + const stringTag = def ? def.arms.findIndex((a) => a.kind === "string") : -1; + const undefinedTag = def ? def.arms.findIndex((a) => a.kind === "undefinedT") : -1; + if (stringTag < 0 || undefinedTag < 0) + throw new InternalCompilerError("lowerer bug: X509Certificate optional field arms"); + const id = `${local}.0`; + locals.push({ id, name: local, type: STRING, mutable: false }); + decls.push({ kind: "varDecl", localId: id, init: call(fn, STRING), loc }); + const text: IrExpr = { kind: "varRef", localId: id, type: STRING, loc }; + return { + kind: "ternary", + cond: { + kind: "strEq", + negated: false, + left: text, + right: { kind: "strLit", value: "", type: STRING, loc }, + type: BOOL, + loc, + }, + then: { + kind: "unionWrap", + unionId: type.unionId, + tag: undefinedTag, + value: { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc }, + type, + loc, + }, + else_: { kind: "unionWrap", unionId: type.unionId, tag: stringTag, value: text, type, loc }, + type, + loc, + }; + } + const fields = shape.fields.map((f) => { + const value = values[f.name]; + if (!value) throw new InternalCompilerError(`lowerer bug: X509Certificate field ${f.name}`); + return { name: f.name, value: value(f.type) }; + }); + return { + name, + params: [{ localId: "d.0", name: "d", type: dataT }], + returnType: t, + locals, + body: [...decls, { kind: "return", value: { kind: "recordLit", fields, type: t, loc }, loc }], + loc, + }; +} + /** Whether a JavaScript class's own binding is the target of - * `Object.defineProperty`/`defineProperties` in its file - * (`Object.defineProperty(Definition, "name", { value })`): its constructor + * `Object.defineProperty`/`defineProperties` or `Reflect.deleteProperty` in + * its file (`Object.defineProperty(Definition, "name", { value })`, + * `Reflect.deleteProperty(Response, "getResponseState")`): its constructor * then needs a live own-property table. */ function definedThroughReflection(lowerer: Lowerer, decl: ts.ClassLikeDeclaration): boolean { const name = decl.name; const sf = ts.getSourceFileOfNode(decl); - if (name === undefined || !sf.text.includes("definePropert")) return false; + if ( + name === undefined || + (!sf.text.includes("definePropert") && !sf.text.includes("deleteProperty")) + ) + return false; const symbol = lowerer.checker.getSymbolAtLocation(name); if (symbol === undefined) return false; let found = false; @@ -11300,9 +11917,11 @@ function definedThroughReflection(lowerer: Lowerer, decl: ts.ClassLikeDeclaratio if ( ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && - (node.expression.name.text === "defineProperty" || + (((node.expression.name.text === "defineProperty" || node.expression.name.text === "defineProperties") && - lowerer.stdlibGlobalMember(node.expression, "Object") !== null + lowerer.stdlibGlobalMember(node.expression, "Object") !== null) || + (node.expression.name.text === "deleteProperty" && + lowerer.stdlibGlobalMember(node.expression, "Reflect") !== null)) ) { const target = node.arguments.nodes[0]; if ( @@ -11317,3 +11936,14 @@ function definedThroughReflection(lowerer: Lowerer, decl: ts.ClassLikeDeclaratio }); return found; } + +/** The failure group of a class the program declares but did not collect + * (its own declaration was rejected), or null for any other symbol. */ +function rejectedProgramClassGroup(lowerer: Lowerer, symbol: ts.Symbol | null): string | null { + if (symbol === null) return null; + const recorded = lowerer.rejectedGroups.get(symbol); + if (recorded !== undefined) return recorded; + const decl = symbol.declarations.find((d) => ts.isClassDeclaration(d) || ts.isClassExpression(d)); + if (decl === undefined || ts.getSourceFileOfNode(decl).isDeclarationFile) return null; + return declarationGroupKey(symbol); +} diff --git a/packages/compiler/src/frontend/lowering/lower-console.ts b/packages/compiler/src/frontend/lowering/lower-console.ts index eec308342..5632f7402 100644 --- a/packages/compiler/src/frontend/lowering/lower-console.ts +++ b/packages/compiler/src/frontend/lowering/lower-console.ts @@ -14,7 +14,15 @@ import { boolLit, dynUndefinedExpr, numLit, strLit, varRef } from "../../ir/buil import { locOf } from "../program.js"; import { type ConsoleMethodMember } from "./builtins/console.js"; import { jsFuncNameOf, type Lowerer } from "./lowerer.js"; -import { lowerConsoleInspectArg, lowerFormatCall, lowerInspectValue } from "./lower-inspect.js"; +import { + heldFormatArgs, + lowerConsoleInspectArg, + lowerFormatCall, + lowerInspectValue, + lowerRuntimeFormat, + runtimeFormatString, + formatStringConverts, +} from "./lower-inspect.js"; import { streamSidesOf } from "./lower-stream.js"; /** node:console Console instances: a native handle (the checked-dynamic @@ -249,7 +257,8 @@ export function lowerConsoleArg( // inspect at the rest-args depth 2 (formatWithOptions) — scalar // kinds byte-exactly, boxed functions as [Function: name] / // [Function (anonymous)], composites through the dyn walk - // (insp.dyn). Never throws — Node's console.log never does. + // (insp.dyn). A value the inspector does not render throws the + // 'not supported yet' Error before anything is written. if (lowered.type.kind === "dyn") { return { kind: "libCall", @@ -357,9 +366,32 @@ export function lowerConsoleOutput( ) { return consoleWrite(stdout, [lowerFormatCall(lowerer, expr, loc, false)], loc); } - const args = expr.arguments.nodes.map((a) => - lowerConsoleArg(lowerer, a, lowerer.lowerExpr(a), surface, loc), - ); + const nodes = expr.arguments.nodes; + // Spread arguments and format strings that may hold a substitution at + // run time go through the runtime formatter. A string first argument + // without a '%' keeps the space-joined write below. + if (nodes.some(ts.isSpreadElement)) + return consoleWrite(stdout, [lowerRuntimeFormat(lowerer, nodes, loc)], loc); + const runtime = + nodes.length > 1 && !isStringLiteralLike(first) ? runtimeFormatString(lowerer, first!) : null; + if (runtime === "other" && !formatStringConverts(lowerer, first!)) + return consoleWrite(stdout, [lowerRuntimeFormat(lowerer, nodes, loc)], loc); + if (runtime !== null) { + const held = heldFormatArgs(lowerer, nodes, loc); + if (!held.joinable) return seq(held.stmts, consoleWrite(stdout, [held.format()], loc), loc); + const joined = held.args.map((arg) => + lowerConsoleArg(lowerer, arg.node, arg.ref(), surface, loc), + ); + const write: IrStmt = { + kind: "if", + cond: held.specifies, + then: [{ kind: "exprStmt", expr: consoleWrite(stdout, [held.format()], loc), loc }], + else_: [{ kind: "exprStmt", expr: consoleWrite(stdout, joined, loc), loc }], + loc, + }; + return seq([...held.stmts, write], noop(loc), loc); + } + const args = nodes.map((a) => lowerConsoleArg(lowerer, a, lowerer.lowerExpr(a), surface, loc)); return consoleWrite(stdout, args, loc); } diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index 2bde5f2aa..c70826975 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -1,3 +1,8 @@ +import { + arrayClassMemberOwner, + arrayOfClassInstance, + arrayRootOf, +} from "./builtins/array-classes.js"; import { keyedHelperName } from "./stable-names.js"; import { isNewTarget, @@ -72,7 +77,7 @@ import { import { nsMemberIdentOf } from "./lower-namespaces.js"; import { bindingNeverReassigned } from "./binding-analysis.js"; import { implicitMonoFile } from "./generic-functions.js"; -import { lowerTaggedTemplate } from "./lower-calls.js"; +import { lowerPrimitiveUnionNumber, lowerTaggedTemplate } from "./lower-calls.js"; import { unitOnlyUnion } from "../type-mapper.js"; import { lowerYield } from "./lower-generators.js"; import { unionWideningTags } from "../../ir/analysis.js"; @@ -89,14 +94,15 @@ import { classSymbolKeyOf } from "./symbol-fields.js"; import { lowerTypeofExpr } from "./expressions/typeof.js"; import { lowerIdentifierExpr } from "./expressions/identifiers.js"; import { lowerFieldCompoundValue, lowerIncDec } from "./expressions/updates.js"; -import { lowerTernary } from "./expressions/narrowing.js"; +import { lowerTernary, v8CalleeText } from "./expressions/narrowing.js"; import { classFieldTarget, lowerPropertyAccessExpr, lowerUnionKeyedRead, } from "./expressions/property-access.js"; import { lowerNullishPair } from "./expressions/binary.js"; -import { lowerElementCompoundParts } from "./expressions/element-access.js"; +import { lowerElementCompoundParts, recordKeyLiteralText } from "./expressions/element-access.js"; +import { OBJECT_PROTOTYPE_MEMBERS } from "../shape-inference/domain.js"; export { DEFERRED_READ_MESSAGE, abstractPropertyDeclOf, @@ -426,8 +432,19 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { return inner; const use = runtimeOptionalUseOf(expr); if (runtimeOptionalAssertionErases(lowerer, expr, inner, use)) return inner; + // A destination that holds null or undefined keeps the value, as `!` + // does in JavaScript (`return cache.get(key)!` from a function that + // returns `T | null`); the destination's own coercion re-tags it. + if (assertionDestinationHoldsNullish(lowerer, expr, inner.type)) return inner; + const operand = assertionOperand(lowerer, expr, inner, target ?? null, loc); + if (operand) return operand; if (target && target.kind !== "union" && !typeEquals(target, inner.type)) { - const helper = lowerer.narrowedArmHelper(inner.type.unionId, target, loc); + const helper = lowerer.narrowedArmHelper( + inner.type.unionId, + target, + loc, + assertedCalleeMessage(expr, target), + ); if (helper) { return lowerer.checkedNarrowCall(helper, inner, target, loc); } @@ -497,7 +514,23 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { // noImplicitThis, so the generic scope walk here is preflight-safe. const local = lowerer.resolveThis(expr); if (local) { - const value: IrExpr = { kind: "varRef", localId: local.id, type: local.type, loc }; + let value: IrExpr = { kind: "varRef", localId: local.id, type: local.type, loc }; + // `this` in a class declared `extends Array` is the array, except as + // the receiver of a member the class declares. + if (local.type.kind === "object") { + const arrayClass = lowerer.classes.get(local.type.className); + const parent = expr.parent; + if ( + arrayClass && + arrayRootOf(arrayClass) && + !( + ts.isPropertyAccessExpression(parent) && + parent.expression === expr && + arrayClassMemberOwner(lowerer, parent) !== null + ) + ) + value = arrayOfClassInstance(arrayClass, value, loc); + } // `this.#x` in a function-local class's prototype method, whose `this` // is the call-site value: the field lives on the native instance of // this evaluation (checkedPrivateReceiver). @@ -573,7 +606,24 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { // any other super position (field reads, bare references) stays out. lowerer.unsupported("SC1090", expr, "'super' outside super() and super.method() calls"); } - if (ts.isNewExpression(expr)) return lowerer.lowerNew(expr); + if (ts.isNewExpression(expr)) { + const value = lowerer.lowerNew(expr); + // `new X()` for a class declared `extends Array` is the array, except + // as the receiver of a member the class declares (as `this` above). + if (value.type.kind !== "object") return value; + const arrayClass = lowerer.classes.get(value.type.className); + if (!arrayClass || !arrayRootOf(arrayClass)) return value; + let operand: ts.Expression = expr; + while (ts.isParenthesizedExpression(operand.parent)) operand = operand.parent; + const parent = operand.parent; + if ( + ts.isPropertyAccessExpression(parent) && + parent.expression === operand && + arrayClassMemberOwner(lowerer, parent) !== null + ) + return value; + return arrayOfClassInstance(arrayClass, value, loc); + } if (ts.isAwaitExpression(expr)) { if (!lowerer.ctx.isAsync) { // A tsc-clean occurrence here is outside both an async function @@ -933,6 +983,257 @@ export function assertedElementKeepsUndefined( ); } +/** Whether `node!` is a returned value, an arrow function's body or a call + * argument whose destination type (the return or parameter type) is `any` + * or `unknown` (as for `String(x!)` and `Number(x!)`) or holds null or + * undefined that the union `inner` holds and the assertion would remove. + * Bindings and assignments are left out: the checker narrows them to the + * asserted type for later reads. */ +function assertionDestinationHoldsNullish( + lowerer: Lowerer, + node: ts.NonNullExpression, + inner: IrType & { kind: "union" }, +): boolean { + const holdsNull = lowerer.armTag(inner.unionId, NULL_T) >= 0; + const holdsUndefined = lowerer.armTag(inner.unionId, UNDEFINED_T) >= 0; + if (!holdsNull && !holdsUndefined) return false; + let outer: ts.Expression = node; + while (ts.isParenthesizedExpression(outer.parent)) outer = outer.parent; + const parent = outer.parent; + const destination = + (ts.isReturnStatement(parent) && parent.expression === outer) || + (ts.isArrowFunction(parent) && parent.body === outer) || + ((ts.isCallExpression(parent) || ts.isNewExpression(parent)) && + parent.arguments?.nodes.includes(outer) === true); + if (!destination) return false; + const contextual = lowerer.checker.getContextualType(outer); + if (!contextual) return false; + const nullish = + ts.TypeFlags.Any | + ts.TypeFlags.Unknown | + (holdsNull ? ts.TypeFlags.Null : 0) | + (holdsUndefined && !optionalStdlibParameter(lowerer, parent, outer) + ? ts.TypeFlags.Undefined + : 0); + if ((contextual.flags & ts.TypeFlags.Union) === 0) return (contextual.flags & nullish) !== 0; + return constituentTypes(contextual).some((type) => (type.flags & nullish) !== 0); +} + +/** Whether `declaration` is TextDecoder's `decode` method. */ +function isTextDecoderDecode(declaration: ts.Node): boolean { + if (!ts.isMethodSignatureDeclaration(declaration) && !ts.isMethodDeclaration(declaration)) + return false; + if (!ts.isIdentifier(declaration.name) || declaration.name.text !== "decode") return false; + const owner = declaration.parent; + return ( + (ts.isInterfaceDeclaration(owner) || ts.isClassDeclaration(owner)) && + owner.name?.text === "TextDecoder" + ); +} + +/** Whether `argument` of `call` binds an optional parameter of a standard + * library declaration (`decoder.decode(input?)`). The undefined such a + * parameter's type holds comes from its optionality, and the builtin + * lowerings read the argument as the declared type, so `x!` keeps the + * checked narrowing there. */ +function optionalStdlibParameter( + lowerer: Lowerer, + call: ts.Node, + argument: ts.Expression, +): boolean { + if (!ts.isCallExpression(call) && !ts.isNewExpression(call)) return false; + const declaration = lowerer.checker.getResolvedSignature(call)?.declaration; + if (declaration === undefined || !lowerer.isStdlibFile(ts.getSourceFileOfNode(declaration))) + return false; + // TextDecoder.decode reads an undefined input as no bytes, as Node does + // (builtins/text-codecs.ts lowerDecodeInput). + if (isTextDecoderDecode(declaration)) return false; + const index = call.arguments?.nodes.indexOf(argument) ?? -1; + const parameter = declaration.parameters?.nodes[index]; + return ( + parameter !== undefined && + parameter.dotDotDotToken === undefined && + (parameter.questionToken !== undefined || parameter.initializer !== undefined) + ); +} + +/** `x!` as the operand of an operator that converts its operands: an + * arithmetic, bitwise, relational or equality operator, `+`, a compound + * assignment, a unary `-`, `+`, `~` or `!`, a template span, or an argument + * of a Math function. `!` performs no runtime check, + * so a value that still holds undefined or null reaches the operator, which + * converts it as JavaScript does: NaN or 0 in arithmetic, false in a + * comparison, "undefined" or "null" in a string. Returns the operand to + * use, or null where the assertion narrows as usual. A numeric use gets the + * value converted with ToNumber; other uses get the union itself. */ +function assertionOperand( + lowerer: Lowerer, + expr: ts.NonNullExpression, + inner: IrExpr, + target: IrType | null, + loc: SrcLoc, +): IrExpr | null { + if (inner.type.kind !== "union" || target === null || target.kind === "union") return null; + const arms = lowerer.unions.get(inner.type.unionId)?.arms; + if ( + !arms?.some(isUnitType) || + !arms.every( + (arm) => + arm.kind === "f64" || arm.kind === "string" || arm.kind === "bool" || isUnitType(arm), + ) + ) + return null; + // Array element reads keep their own handling of a missing element. A + // typed-array read is optional here only where its use tells undefined + // from NaN (assertedReadObservesUndefined). + let operand: ts.Expression = expr.expression; + while (ts.isParenthesizedExpression(operand)) operand = operand.expression; + if (ts.isElementAccessExpression(operand)) { + const kind = lowerer.mapTypeOf(lowerer.typeOf(operand.expression))?.kind; + if (kind === "array") return null; + } + const use = convertingUseOf(lowerer, expr); + if (use === null) return null; + if (use === "value") return inner; + return lowerPrimitiveUnionNumber(lowerer, inner, expr, loc); +} + +/** How the converting use of `x!` (see assertionOperand) reads it: + * "number" where the use applies ToNumber, "value" where the consumer + * lowers the union itself (strings, equality, truthiness), null for other + * uses. */ +function convertingUseOf(lowerer: Lowerer, expr: ts.NonNullExpression): "number" | "value" | null { + let outer: ts.Expression = expr; + while (ts.isParenthesizedExpression(outer.parent)) outer = outer.parent; + const parent = outer.parent; + if (ts.isTemplateSpan(parent)) return parent.expression === outer ? "value" : null; + if (ts.isTypeOfExpression(parent)) return "value"; + if (ts.isPrefixUnaryExpression(parent)) { + if (parent.operator === ts.SyntaxKind.ExclamationToken) return "value"; + return parent.operator === ts.SyntaxKind.MinusToken || + parent.operator === ts.SyntaxKind.PlusToken || + parent.operator === ts.SyntaxKind.TildeToken + ? "number" + : null; + } + if (ts.isCallExpression(parent) && parent.arguments.nodes.includes(outer)) { + const callee = parent.expression; + return ts.isPropertyAccessExpression(callee) && + stdlibGlobalNameOf(lowerer, callee.expression) === "Math" + ? "number" + : null; + } + if (!ts.isBinaryExpression(parent)) return null; + const op = parent.operatorToken.kind; + if (EQUALITY_OPERATORS.has(op)) return "value"; + const other = parent.left === outer ? parent.right : parent.left; + const isString = (node: ts.Expression): boolean => + (lowerer.typeOf(node).flags & ts.TypeFlags.StringLike) !== 0; + // With a string on either side, `+` concatenates and a comparison + // compares strings. `undefined + 1` and `undefined + undefined` are NaN, + // which a string-typed result cannot hold, so `+` takes the union only + // with a string partner that is not itself asserted. A partner that may + // hold a string or a number at run time (`any`, `unknown`) also gets the + // union, and `+` decides between concatenation and addition when it runs. + if (op === ts.SyntaxKind.PlusToken || op === ts.SyntaxKind.PlusEqualsToken) { + if (op === ts.SyntaxKind.PlusEqualsToken && parent.right !== outer) return null; + let partner = other; + while (ts.isParenthesizedExpression(partner)) partner = partner.expression; + if (isString(other)) return ts.isNonNullExpression(partner) ? null : "value"; + if (!numericPartner(lowerer.typeOf(other))) return "value"; + return isString(expr) ? null : "number"; + } + if (RELATIONAL_OPERATORS.has(op)) return isString(other) || isString(expr) ? "value" : "number"; + if (NUMERIC_OPERATORS.has(op)) return "number"; + return NUMERIC_COMPOUNDS.has(op) && parent.right === outer ? "number" : null; +} + +/** A `+` partner whose every value is a number, a boolean, null or + * undefined: `+` adds with it. */ +function numericPartner(type: ts.Type): boolean { + const numeric = + ts.TypeFlags.NumberLike | + ts.TypeFlags.BooleanLike | + ts.TypeFlags.Null | + ts.TypeFlags.Undefined | + ts.TypeFlags.Void; + return (type.flags & ts.TypeFlags.Union) !== 0 + ? constituentTypes(type).every((part) => (part.flags & numeric) !== 0) + : (type.flags & numeric) !== 0; +} + +const EQUALITY_OPERATORS = new Set([ + ts.SyntaxKind.EqualsEqualsEqualsToken, + ts.SyntaxKind.ExclamationEqualsEqualsToken, + ts.SyntaxKind.EqualsEqualsToken, + ts.SyntaxKind.ExclamationEqualsToken, +]); + +const RELATIONAL_OPERATORS = new Set([ + ts.SyntaxKind.LessThanToken, + ts.SyntaxKind.GreaterThanToken, + ts.SyntaxKind.LessThanEqualsToken, + ts.SyntaxKind.GreaterThanEqualsToken, +]); + +const NUMERIC_OPERATORS = new Set([ + ts.SyntaxKind.MinusToken, + ts.SyntaxKind.AsteriskToken, + ts.SyntaxKind.SlashToken, + ts.SyntaxKind.PercentToken, + ts.SyntaxKind.AsteriskAsteriskToken, + ts.SyntaxKind.AmpersandToken, + ts.SyntaxKind.BarToken, + ts.SyntaxKind.CaretToken, + ts.SyntaxKind.LessThanLessThanToken, + ts.SyntaxKind.GreaterThanGreaterThanToken, + ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken, +]); + +const NUMERIC_COMPOUNDS = new Set([ + ts.SyntaxKind.MinusEqualsToken, + ts.SyntaxKind.AsteriskEqualsToken, + ts.SyntaxKind.SlashEqualsToken, + ts.SyntaxKind.PercentEqualsToken, + ts.SyntaxKind.AsteriskAsteriskEqualsToken, + ts.SyntaxKind.AmpersandEqualsToken, + ts.SyntaxKind.BarEqualsToken, + ts.SyntaxKind.CaretEqualsToken, + ts.SyntaxKind.LessThanLessThanEqualsToken, + ts.SyntaxKind.GreaterThanGreaterThanEqualsToken, + ts.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken, +]); + +/** The TypeError Node raises when an asserted callee (`t[k]!(…)`, `o.f!()`) + * holds no function: `!` is erased, so the call itself throws " is + * not a function" in V8's spelling of the callee. */ +function assertedCalleeMessage(expr: ts.NonNullExpression, target: IrType): string | undefined { + if (target.kind !== "func") return undefined; + let child: ts.Expression = expr; + while (ts.isParenthesizedExpression(child.parent)) child = child.parent; + const call = child.parent; + if (!ts.isCallExpression(call) || call.expression !== child || call.questionDotToken) + return undefined; + return `${v8CalleeText(expr.expression)} is not a function`; +} + +/** Whether the use of `t[i]!` tells undefined from NaN: a converting use + * that takes the value itself (see convertingUseOf), or a destination that + * holds a missing element (see assertedElementKeepsUndefined) other than a + * return, whose slot has the function's number type. */ +export function assertedReadObservesUndefined( + lowerer: Lowerer, + node: ts.NonNullExpression, +): boolean { + if (convertingUseOf(lowerer, node) === "value") return true; + let outer: ts.Expression = node; + while (ts.isParenthesizedExpression(outer.parent)) outer = outer.parent; + const parent = outer.parent; + if (ts.isReturnStatement(parent) || (ts.isArrowFunction(parent) && parent.body === outer)) + return false; + return assertedElementKeepsUndefined(lowerer, node); +} + export function runtimeOptionalAssertionErases( lowerer: Lowerer, expr: ts.Expression, @@ -1087,6 +1388,10 @@ export function lowerCondition(lowerer: Lowerer, expr: ts.Expression): IrExpr { lowerer.isStdlibGlobal(e.expression, "Object") ) return { kind: "boolLit", value: true, type: BOOL, loc: locOf(e) }; + // `if (runtimeBuffer)` over an alias of the Buffer global (a + // capability probe): Node always defines Buffer. + if (lowerer.isStdlibGlobal(e, "Buffer")) + return { kind: "boolLit", value: true, type: BOOL, loc: locOf(e) }; if (ts.isPropertyAccessExpression(e)) { const presence = lowerPromiseThenPresence(lowerer, e, { kind: "boolLit", @@ -1698,6 +2003,23 @@ export function lowerAbsenceProbe(lowerer: Lowerer, node: ts.Expression): IrExpr if (shape && !shape.tuple && shape.indexValue && shape.indexValue.kind !== "dyn") { return lowerer.lowerRecordKeyRead(expr, receiverT.shapeId, shape, true); } + // A literal key the shape does not declare is absent from every value + // (lowerRecordKeyRead answers undefined), whatever type the checker gave + // the read. An optional chain keeps its own lowering, which reads a + // nullish receiver as undefined instead of converting it. + const literal = recordKeyLiteralText(expr.argumentExpression); + if ( + !expr.questionDotToken && + !hasOptionalChainGuard(expr.expression) && + shape && + !shape.tuple && + !shape.indexValue && + literal !== null && + !OBJECT_PROTOTYPE_MEMBERS.has(literal) && + !shape.fields.some((field) => field.name === literal) + ) { + return lowerer.lowerRecordKeyRead(expr, receiverT.shapeId, shape, true); + } return null; } // `m[2]` on a JavaScript `RegExpExecArray | null` that a previous @@ -2106,6 +2428,7 @@ export function typeofAnswer(arm: IrType): string | null { case "set": case "generator": case "regex": + case "date": case "bytes": case "url": case "searchParams": @@ -2869,11 +3192,9 @@ export function symbolFieldInfo( if (lowerer.chainBlocked(expr)) return null; const receiverIr = lowerer.mapTypeOf(lowerer.typeOf(expr.expression)); if (receiverIr?.kind !== "object") return null; - const info = lowerer.classes.get(receiverIr.className); - if (!info) { - lowerer.flushDeferredClass(receiverIr.className); - return null; - } + const info = + lowerer.classes.get(receiverIr.className) ?? lowerer.flushDeferredClass(receiverIr.className); + if (!info) return null; const key = classSymbolKeyOf(lowerer, expr.argumentExpression); if (!key) return null; const field = info.symbolFields?.get(key.identity); diff --git a/packages/compiler/src/frontend/lowering/lower-functions.ts b/packages/compiler/src/frontend/lowering/lower-functions.ts index c1cca9c64..7f4c8dfb7 100644 --- a/packages/compiler/src/frontend/lowering/lower-functions.ts +++ b/packages/compiler/src/frontend/lowering/lower-functions.ts @@ -1,5 +1,6 @@ import type { ScrDiagnostic } from "../../diagnostics/diagnostic.js"; import { declareTypedArguments } from "./typed-arguments.js"; +import { declareArgumentCount } from "./argument-counts.js"; import { dynUndefinedExpr, varRef } from "../../ir/build.js"; import { everyStmtList, transformStmtList } from "../../ir/traverse.js"; import { bindingNeverReassigned } from "./binding-analysis.js"; @@ -166,6 +167,23 @@ function declareFunctionArguments( } } +/** The type parameters a lambda without its own takes from a generic + * contextual signature, unbound in the current scope. */ +function contextualTypeParameterSymbols( + lowerer: Lowerer, + node: ts.ArrowFunction | ts.FunctionExpression, +): ts.Symbol[] { + const signatures = lowerer.checker.getCallSignatures(lowerer.typeOf(node)); + if (signatures.length !== 1) return []; + const symbols: ts.Symbol[] = []; + for (const parameter of signatures[0]!.getTypeParameters()) { + const symbol = parameter.getSymbol(); + if (!symbol || lowerer.typeParamBindings?.has(symbol)) return []; + symbols.push(symbol); + } + return symbols; +} + /** Lifts an arrow function / function expression / nested declaration / * object-literal shorthand method to a module-level function and yields * the `closure` expression creating it. */ @@ -201,6 +219,31 @@ export function lowerLambda( lowerer.typeParamBindings = previous; } } + // A lambda contextually typed by a GENERIC signature (`send: (m) => …` + // for `send(m: Msg): string`) takes that signature's type + // parameters as its own; it lowers once, erased, like a generic lambda. + if ( + (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) && + !node.typeParameters?.nodes.length && + !lowerer.erasingLambdas.has(node) + ) { + const contextual = contextualTypeParameterSymbols(lowerer, node); + if (contextual.length > 0) { + const previous = lowerer.typeParamBindings; + const bindings = new Map(previous ?? []); + for (const symbol of contextual) bindings.set(symbol, DYN); + lowerer.typeParamBindings = bindings; + lowerer.erasingLambdas.add(node); + lowerer.erasingDepth++; + try { + return lowerLambda(lowerer, node, checkedReceiver); + } finally { + lowerer.erasingDepth--; + lowerer.erasingLambdas.delete(node); + lowerer.typeParamBindings = previous; + } + } + } const prepared = ts.isArrowFunction(node) || ts.isFunctionExpression(node) ? lowerer.preparedClassFactories.get(node) @@ -534,12 +577,18 @@ export function lowerFunction(lowerer: Lowerer, decl: ts.FunctionDeclaration): I const diagsBefore = lowerer.diags.length; lowerer.fnStack.push(ctx); try { + // The count a call through a value stored is taken (and its slot + // cleared) before the parameters bind: a destructuring default that + // throws must not leave the slot set for the next direct call. + const argumentCount: IrStmt[] = []; + declareArgumentCount(lowerer, decl, argumentCount); const receiver = decl.parameters.nodes.some(isThisParameter) || isJsSourceFile(ts.getSourceFileOfNode(decl)) ? declareCallReceiver(lowerer, decl, sig.generator !== undefined) : null; const { params, prologue } = lowerer.declareParams(decl.parameters.nodes, sig.params); if (receiver) prologue.unshift(receiver); + prologue.unshift(...argumentCount); declareFunctionArguments( lowerer, decl, diff --git a/packages/compiler/src/frontend/lowering/lower-generators.ts b/packages/compiler/src/frontend/lowering/lower-generators.ts index 80463fa6c..ccfa4ad39 100644 --- a/packages/compiler/src/frontend/lowering/lower-generators.ts +++ b/packages/compiler/src/frontend/lowering/lower-generators.ts @@ -183,13 +183,9 @@ export function lowerGenMethodCall( ); } arg = lowerer.lowerExpr(argNode); - if (arg.type.kind === "date") { - lowerer.unsupported( - "SC1090", - argNode, - ".throw() with a Date value (the exception channel cannot preserve Date's object kind; throw an Error or primitive instead)", - ); - } + // A Date throws as its checked-dynamic handle, the same Date object + // (the throw statement's rule). + if (arg.type.kind === "date") arg = lowerer.coerceToExpected(arg, DYN); if (arg.type.kind === "void" || arg.type.kind === "caught" || isUnitType(arg.type)) { lowerer.unsupported( "SC1090", @@ -333,7 +329,7 @@ export function stepDrivable(genT: GenType): boolean { /** Whether a yield channel's values have an OUT-slot form `value` reads. */ function hasOutSlot(yt: IrType): boolean { - return yt.kind === "f64" || yt.kind === "date" || yt.kind === "bool" || isRefCounted(yt); + return yt.kind === "f64" || yt.kind === "bool" || isRefCounted(yt); } /** `step` and `value` resumes of a generator-typed borrowed read. */ diff --git a/packages/compiler/src/frontend/lowering/lower-inspect.ts b/packages/compiler/src/frontend/lowering/lower-inspect.ts index 5266d5c4e..e14c3efca 100644 --- a/packages/compiler/src/frontend/lowering/lower-inspect.ts +++ b/packages/compiler/src/frontend/lowering/lower-inspect.ts @@ -1,5 +1,7 @@ import { keyedHelperName } from "./stable-names.js"; import { perKeyOrder } from "./key-order-witness.js"; +import { elementMayBeForeign } from "./array-values.js"; +import { classPrototypeView } from "./class-prototypes.js"; import { dynUndefinedExpr, boolLit, @@ -14,14 +16,17 @@ import { InternalCompilerError } from "../../errors.js"; import * as ts from "../ts7/adapter.js"; import { unsignedHex } from "../../format-integer.js"; -import type { Lowerer } from "./lowerer.js"; -import { isJsSourceFile } from "../program.js"; +import { jsFuncNameOf, type Lowerer } from "./lowerer.js"; +import { isJsSourceFile, locOf } from "../program.js"; +import { noLoweringDiag } from "../../diagnostics/diagnostic.js"; import { BOOL, + CAUGHT, DYN, F64, type IrExpr, type IrFunction, + type IrLocal, type IrStmt, type IrStrIntrinsicMethod, type IrType, @@ -33,6 +38,8 @@ import { type SrcLoc, canConvertToDyn, canDynCheckTo, + funcOf, + arrayOf, CLASS_FIELD_ATTRIBUTES_PREFIX, classDynViewSupported, DYN_CLASS_PROPERTIES, @@ -44,15 +51,20 @@ import { shapeKeyOrders, typeKey, } from "../../ir/ir.js"; -import type { ClassInfo } from "./lower-classes.js"; +import { accessorCall, findMethodOn, type ClassInfo } from "./lower-classes.js"; import { isSafeToDiscard, isSafeToRepeat } from "./expressions/evaluation-safety.js"; -import { symbolFieldDisplayName } from "./symbol-fields.js"; +import { + INSPECT_CUSTOM_MEMBER, + symbolFieldDisplayName, + TO_STRING_TAG_FIELD, +} from "./symbol-fields.js"; import { eventTargetRootOf } from "./lower-event-target.js"; import { dateRootOf } from "./builtins/date-classes.js"; import { lowerSystemErrorMap } from "./lower-system-errors.js"; import { isPerfEntryType, perfEntryJson } from "./builtins/performance.js"; import { recordMayKeepSource, recordSourceTest } from "./record-sources.js"; import { nullProtoRecordShape, recordNullProtoTest } from "./null-prototype.js"; +import { dynArgumentPack } from "./call-arguments.js"; /* ── IR construction shorthand ───────────────────────────────────────── */ function concatAll(parts: IrExpr[], loc: SrcLoc): IrExpr { @@ -736,11 +748,7 @@ function inspectSupport( case "array": return inspectSupport(lowerer, t.elem, visiting, out); case "bytes": - // A Uint8Array and a Buffer share the u8 representation and print - // differently; the other element kinds are never Buffers. - return t.elem === "u8" - ? "Uint8Array and Buffer values inside composites have no inspect lowering yet (Buffer's form vs Uint8Array's is not recorded in the static type here)" - : null; + return null; case "record": { if (visiting.has(t.shapeId)) { out.recursive = true; @@ -807,6 +815,8 @@ function inspectSupport( case "object": { const info = lowerer.classes.get(t.className); if (!info) return `class '${t.className}' has no inspect lowering`; + const custom = customInspectReason(lowerer, info); + if (custom !== null) return custom; // EventTarget and Event subclasses render through Node's inspectors // for those classes (the runtime's capsule view), and so do classes // declared `extends Date`. @@ -861,7 +871,10 @@ function inspectSupport( } // Generator objects print through their checked capsule: Node's // `Object [Generator] {}`, or the tag of the iterator they implement. + // URLs and URLSearchParams print through their handles. case "generator": + case "url": + case "searchParams": return null; default: return `'${lowerer.fmt(t)}' values have no inspect lowering`; @@ -902,6 +915,120 @@ export function inspectExpr( recurse: IrExpr, depth: IrExpr, loc: SrcLoc, + /** The instance's own rendering, without its custom inspect method. */ + ownRendering = false, +): IrExpr { + const render = inspectExprBody(lowerer, t, value, recurse, depth, loc, ownRendering); + // A top-level render through emitted helpers that can reach a refusal + // (the 'not supported yet' Error of insp.dyn) unwinds the inspector to + // its state before the render on every exit: the Error propagates out of + // the helpers before they close their frames. + if ( + render.kind !== "call" || + recurse.kind !== "numLit" || + recurse.value !== 0 || + !inspectMayRefuse(lowerer, t) + ) + return render; + return withInspectUnwind(lowerer, render, t, loc); +} + +/** A top-level helper render (`call helper(v, r, d)`) through a lifted + * wrapper that takes insp.mark first and runs insp.unwind however the + * render ends: `try { return helper(v, r, d); } finally { insp.unwind(); }`. + * A seqExpr cannot hold the try. */ +function withInspectUnwind( + lowerer: Lowerer, + render: IrExpr & { kind: "call" }, + t: IrType, + loc: SrcLoc, +): IrExpr { + const key = `unwind:${render.callee}`; + let name = lowerer.inspectHelpers.get(key); + if (name === undefined) { + name = keyedHelperName(lowerer, "%util.inspUnwound", key); + lowerer.inspectHelpers.set(key, name); + const params = [ + { localId: "v.0", name: "v", type: t }, + { localId: "r.0", name: "r", type: F64 }, + { localId: "d.0", name: "d", type: F64 }, + ]; + const call = (fn: "insp.mark" | "insp.unwind"): IrStmt => ({ + kind: "exprStmt", + expr: { kind: "libCall", fn, args: [], type: VOID, loc }, + loc, + }); + lowerer.liftedFns.push({ + name, + params, + returnType: STRING, + locals: params.map((p) => ({ id: p.localId, name: p.name, type: p.type, mutable: false })), + body: [ + call("insp.mark"), + { + kind: "tryCatch", + tryBody: [ + { + kind: "return", + value: { + kind: "call", + callee: render.callee, + args: params.map((p) => varRef(p.localId, p.type, loc)), + type: STRING, + loc, + }, + loc, + }, + ], + catchBody: null, + catchLocalId: null, + finallyBody: [call("insp.unwind")], + loc, + }, + ], + loc, + }); + } + return { ...render, callee: name }; +} + +/** Whether inspecting a `t` value can end in an inspection refusal: `t` + * or a type reachable from it is a checked-dynamic value, a generator, a + * Dirent row or a class instance, or a record or array that can keep + * undeclared properties, which can render through a checked-dynamic view. */ +function inspectMayRefuse(lowerer: Lowerer, t: IrType): boolean { + const seen = new Set(); + const walk = (u: IrType): boolean => { + if (u.kind === "dyn" || u.kind === "generator") return true; + if (u.kind === "record") { + const shape = lowerer.shapes.get(u.shapeId); + if (shape && isDirentShape(shape)) return true; + // The kept source properties render through the checked view (the + // helper's source gate). + if (recordMayKeepSource(lowerer, u)) return true; + } + // So do an array's properties besides its elements. + if (u.kind === "array" && lowerer.dynConvertible(u)) return true; + // A class instance can render through its checked-dynamic view, which + // a helper built earlier may only learn at the end of lowering + // (classInspectsAddedProperties). + if (u.kind === "object") return true; + const k = typeKey(u); + if (seen.has(k)) return false; + seen.add(k); + return typeConstituents(lowerer, u).some(walk); + }; + return walk(t); +} + +function inspectExprBody( + lowerer: Lowerer, + t: IrType, + value: IrExpr, + recurse: IrExpr, + depth: IrExpr, + loc: SrcLoc, + ownRendering: boolean, ): IrExpr { // A Dirent row renders through its dyn view: Node's `Dirent { ... }` // with the Symbol(type) key. @@ -950,9 +1077,29 @@ export function inspectExpr( loc, }; } - return { kind: "libCall", fn: "insp.buffer", args: [value], type: STRING, loc }; + // A Uint8Array, a Buffer and a DataView share the u8 representation + // and print differently: the view's own flags pick. + return { + kind: "libCall", + fn: "insp.bytes", + args: [value, recurse, depth], + type: STRING, + loc, + }; case "dyn": return { kind: "libCall", fn: "insp.dyn", args: [value, recurse, depth], type: STRING, loc }; + // URLs and URLSearchParams render through their checked-dynamic + // handles: Node's `URL { href: ..., hash: ... }` and + // `URLSearchParams { 'a' => '1' }`. + case "url": + case "searchParams": + return { + kind: "libCall", + fn: "insp.dyn", + args: [lowerer.coerceToExpected(value, DYN), recurse, depth], + type: STRING, + loc, + }; case "generator": return { kind: "libCall", @@ -962,6 +1109,15 @@ export function inspectExpr( loc, }; case "object": + if (!ownRendering && customInspectMethodOf(lowerer, t.className)) { + return { + kind: "call", + callee: customInspectHelper(lowerer, t, loc), + args: [value, recurse, depth], + type: STRING, + loc, + }; + } if ( eventTargetRootOf(lowerer.classes.get(t.className)) !== null || dateRootOf(lowerer.classes.get(t.className)) !== null @@ -1141,6 +1297,297 @@ function classInspectsAddedProperties(lowerer: Lowerer, info: ClassInfo): boolea ); } +/* ── custom inspect methods ([util.inspect.custom]) ──────────────────── */ + +/** The `[util.inspect.custom]` method a class's instances have, declared + * on the class or a base; null when they have none. */ +function customInspectMethodOf(lowerer: Lowerer, className: string) { + const info = lowerer.classes.get(className); + return info ? (lowerer.findMethodOn(info, INSPECT_CUSTOM_MEMBER) ?? null) : null; +} + +/** Why instances of a class with a custom inspect method have no static + * rendering, or null. Node calls the method with the remaining depth, the + * options and util.inspect; the method's parameters receive them as + * checked-dynamic values, and its result renders in the instance's place. */ +function customInspectReason(lowerer: Lowerer, info: ClassInfo): string | null { + const found = lowerer.findMethodOn(info, INSPECT_CUSTOM_MEMBER); + if (!found) { + const below = (c: ClassInfo): boolean => + c.subclasses.some((s) => s.methods.has(INSPECT_CUSTOM_MEMBER) || below(s)); + return below(info) + ? "classes whose subclasses declare [util.inspect.custom] have no inspect lowering yet (declare the method on the class itself)" + : null; + } + if (found.sig.params.length > 3) + return "[util.inspect.custom] methods with more than three parameters have no inspect lowering"; + for (const param of found.sig.params) { + if ( + param.type.kind !== "dyn" && + !canDynCheckTo( + param.type, + (id) => lowerer.shapes.get(id), + (id) => lowerer.unions.get(id), + ) + ) + return `[util.inspect.custom] methods with a '${lowerer.fmt(param.type)}' parameter have no inspect lowering`; + } + const ret = found.sig.ret; + if (ret.kind === "object") + return "[util.inspect.custom] methods that return a class instance have no inspect lowering"; + if ( + ret.kind !== "string" && + ret.kind !== "dyn" && + ret.kind !== "void" && + inspectSupport(lowerer, ret, new Set(), { recursive: false }) !== null + ) + return `[util.inspect.custom] methods returning '${lowerer.fmt(ret)}' have no inspect lowering`; + return null; +} + +/** Whether values of `t` can hold a checked-dynamic value, whose own custom + * inspect functions the runtime walk calls. */ +function typeReachesDyn(lowerer: Lowerer, t: IrType): boolean { + const seen = new Set(); + const walk = (u: IrType): boolean => { + if (u.kind === "dyn") return true; + const k = typeKey(u); + if (seen.has(k)) return false; + seen.add(k); + return typeConstituents(lowerer, u).some(walk); + }; + return walk(t); +} + +/** Whether values of `t` can hold an instance of a class with a custom + * inspect method. */ +export function typeReachesCustomInspect(lowerer: Lowerer, t: IrType): boolean { + const seen = new Set(); + const walk = (u: IrType): boolean => { + const k = typeKey(u); + if (seen.has(k)) return false; + seen.add(k); + if (u.kind === "object" && customInspectMethodOf(lowerer, u.className)) return true; + return typeConstituents(lowerer, u).some(walk); + }; + return walk(t); +} + +/** `%util.inspCustom.(v, r, d): string` — formatValue's custom hook + * over a class whose instances have a `[util.inspect.custom]` method: + * with ctx.customInspect set, the method's result (a string re-indented + * at the current level, the instance itself rendered normally, any other + * value rendered in its place); otherwise the instance's own rendering. */ +function customInspectHelper( + lowerer: Lowerer, + t: IrType & { kind: "object" }, + loc: SrcLoc, +): string { + const key = `inspCustom:${typeKey(t)}`; + const existing = lowerer.inspectHelpers.get(key); + if (existing) return existing; + const name = keyedHelperName(lowerer, "%util.inspCustom", key); + lowerer.inspectHelpers.set(key, name); + const found = customInspectMethodOf(lowerer, t.className)!; + const v = (): IrExpr => varRef("v.0", t, loc); + const r = (): IrExpr => varRef("r.0", F64, loc); + const d = (): IrExpr => varRef("d.0", F64, loc); + const ret = (value: IrExpr): IrStmt => ({ kind: "return", value, loc }); + const normal = (): IrExpr => inspectExpr(lowerer, t, v(), r(), d(), loc, true); + const runtimeArgs: IrExpr[] = [ + { kind: "libCall", fn: "insp.customDepth", args: [r(), d()], type: DYN, loc }, + { kind: "libCall", fn: "insp.customOptions", args: [d()], type: DYN, loc }, + { kind: "libCall", fn: "insp.inspectFunction", args: [], type: DYN, loc }, + ]; + const locals: IrLocal[] = [ + { id: "v.0", name: "v", type: t, mutable: false }, + { id: "r.0", name: "r", type: F64, mutable: false }, + { id: "d.0", name: "d", type: F64, mutable: false }, + ]; + // The arguments read the walk's depth and options, so they evaluate + // before the method's own context begins. + const argDecls: IrStmt[] = found.sig.params.map((param, i) => { + locals.push({ id: `arg${i}.0`, name: `arg${i}`, type: param.type, mutable: false }); + const init = lowerer.coerceToExpected(runtimeArgs[i]!, param.type); + return { kind: "varDecl", localId: `arg${i}.0`, init, loc }; + }); + const args = found.sig.params.map((param, i) => varRef(`arg${i}.0`, param.type, loc)); + const resultT = found.sig.ret; + const call = accessorCall(lowerer, t.className, INSPECT_CUSTOM_MEMBER, v(), args, resultT, loc); + // The method runs in a fresh inspect context (insp.customCallBegin); the + // walk's own context returns before the result renders. The token lets + // an enclosing end restore this walk's context when the method throws. + locals.push({ id: "tok.0", name: "tok", type: F64, mutable: false }); + const end: IrStmt = { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "insp.customCallEnd", + args: [varRef("tok.0", F64, loc)], + type: VOID, + loc, + }, + loc, + }; + const render: IrStmt[] = [ + ...argDecls, + { + kind: "varDecl", + localId: "tok.0", + init: { kind: "libCall", fn: "insp.customCallBegin", args: [], type: F64, loc }, + loc, + }, + ]; + if (resultT.kind === "void") { + render.push({ kind: "exprStmt", expr: call, loc }, end); + } else { + locals.push({ id: "res.0", name: "res", type: resultT, mutable: false }); + render.push({ kind: "varDecl", localId: "res.0", init: call, loc }, end); + } + const res = (): IrExpr => varRef("res.0", resultT, loc); + if (resultT.kind === "string") { + render.push(ret({ kind: "libCall", fn: "insp.customText", args: [res()], type: STRING, loc })); + } else if (resultT.kind === "dyn") { + render.push( + { + kind: "if", + cond: { + kind: "libCall", + fn: "insp.customIsSelf", + args: [res(), { kind: "dynFrom", value: v(), liveRef: true, type: DYN, loc }], + type: BOOL, + loc, + }, + then: [ret(normal())], + else_: null, + loc, + }, + ret({ kind: "libCall", fn: "insp.customResult", args: [res(), r(), d()], type: STRING, loc }), + ); + } else if (resultT.kind === "void") { + render.push(ret(strLit("undefined", loc))); + } else { + render.push(ret(inspectExpr(lowerer, resultT, res(), r(), d(), loc))); + } + lowerer.liftedFns.push({ + name, + params: [ + { localId: "v.0", name: "v", type: t }, + { localId: "r.0", name: "r", type: F64 }, + { localId: "d.0", name: "d", type: F64 }, + ], + returnType: STRING, + locals, + body: [ + { + kind: "if", + cond: { kind: "libCall", fn: "insp.customEnabled", args: [], type: BOOL, loc }, + then: render, + else_: null, + loc, + }, + ret(normal()), + ], + loc, + }); + return name; +} + +/** `%util.inspCustomDyn.(self, depth, options, inspect): dyn` — a + * class's custom inspect method for its instances held as checked-dynamic + * values: the runtime's inspect walk calls it with the instance's typed + * ref and the method's three arguments. */ +function customInspectDynHelper( + lowerer: Lowerer, + t: IrType & { kind: "object" }, + loc: SrcLoc, +): string { + const name = keyedHelperName(lowerer, "%util.inspCustomDyn", typeKey(t)); + const found = customInspectMethodOf(lowerer, t.className)!; + const param = (id: string): IrExpr => varRef(id, DYN, loc); + const ids = ["depth.0", "options.0", "inspect.0"]; + const call = accessorCall( + lowerer, + t.className, + INSPECT_CUSTOM_MEMBER, + varRef("v.0", t, loc), + found.sig.params.map((p, i) => lowerer.coerceToExpected(param(ids[i]!), p.type)), + found.sig.ret, + loc, + ); + const body: IrStmt[] = [ + { kind: "varDecl", localId: "v.0", init: lowerer.coerceToExpected(param("self.0"), t), loc }, + ]; + if (found.sig.ret.kind === "void") { + body.push( + { kind: "exprStmt", expr: call, loc }, + { kind: "return", value: dynUndefinedExpr(loc), loc }, + ); + } else { + body.push({ kind: "return", value: lowerer.coerceToExpected(call, DYN), loc }); + } + const params = ["self.0", ...ids]; + lowerer.liftedFns.push({ + name, + params: params.map((id) => ({ localId: id, name: id.slice(0, -2), type: DYN })), + returnType: DYN, + locals: [ + ...params.map((id) => ({ id, name: id.slice(0, -2), type: DYN, mutable: false })), + { id: "v.0", name: "v", type: t, mutable: false }, + ], + body, + loc, + }); + return name; +} + +/** The statements %main runs first, in every thread: each class whose + * instances have a custom inspect method registers its compiled call + * under its instances' typed-ref type key (scr_insp_register_custom). */ +export function customInspectRegistrations(lowerer: Lowerer, loc: SrcLoc): IrStmt[] { + const stmts: IrStmt[] = []; + for (const info of lowerer.classes.values()) { + if (info.def.runtime || info.def.abstract) continue; + // Error classes register when their own properties render statically + // (fieldedErrorReason): the others keep no method to call. + if (isErrorClass(lowerer, info.def.name) && fieldedErrorReason(lowerer, info) !== null) + continue; + if (!lowerer.findMethodOn(info, INSPECT_CUSTOM_MEMBER)) continue; + // Only constructed classes have instances, and only their methods lower + // (the constructor edge reaches the method). + if (!lowerer.classCanBeConstructed(info)) continue; + if (customInspectReason(lowerer, info) !== null) continue; + const t = { kind: "object" as const, className: info.def.name }; + const fn = customInspectDynHelper(lowerer, t, loc); + stmts.push({ + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "insp.registerCustom", + args: [ + strLit(typeKey(t), loc), + { + kind: "dynFrom", + value: { + kind: "closure", + fnName: fn, + captures: [], + type: funcOf([DYN, DYN, DYN, DYN], DYN), + loc, + }, + type: DYN, + loc, + }, + ], + type: VOID, + loc, + }, + loc, + }); + } + return stmts; +} + /* ── the interned per-type helper ────────────────────────────────────── */ /** `%util.insp.(v, r, d): string` — formatValue over the static type: @@ -1289,11 +1736,29 @@ function inspectHelper(lowerer: Lowerer, t: IrType, loc: SrcLoc): string { const i = (): IrExpr => varRef("i.0", F64, loc); const next = (): IrExpr => varRef("next.0", F64, loc); const hasMore = (): IrExpr => lt(i(), n()); + // A foreign element (state 3) holds a value outside the element type. + const foreignElements = elementMayBeForeign(lowerer, t.elem); + const foreign = (): IrExpr => ({ + kind: "libCall", + fn: "array.foreign", + args: [v(), i()], + type: DYN, + loc, + }); const isNumber = (): IrExpr => ({ kind: "ternary", cond: eq(state(i()), numLit(1, loc)), then: isNumberFlag(lowerer, t.elem, () => at(i()), loc), - else_: boolLit(false, loc), + else_: foreignElements + ? { + kind: "ternary", + cond: eq(state(i()), numLit(3, loc)), + then: isNumberFlag(lowerer, DYN, foreign, loc), + else_: boolLit(false, loc), + type: BOOL, + loc, + } + : boolLit(false, loc), type: BOOL, loc, }); @@ -1372,7 +1837,16 @@ function inspectHelper(lowerer: Lowerer, t: IrType, loc: SrcLoc): string { kind: "ternary", cond: eq(state(i()), numLit(1, loc)), then: child(t.elem, at(i())), - else_: strLit("undefined", loc), + else_: foreignElements + ? { + kind: "ternary", + cond: eq(state(i()), numLit(3, loc)), + then: child(DYN, foreign()), + else_: strLit("undefined", loc), + type: STRING, + loc, + } + : strLit("undefined", loc), type: STRING, loc, }, @@ -1870,8 +2344,86 @@ function inspectHelper(lowerer: Lowerer, t: IrType, loc: SrcLoc): string { // observable way — Node's inspect omits them entirely (verified), // so a class whose only fields are private prints as `C {}`. const visible = info.def.fields.filter((f) => isClassOwnEnumerableFieldName(f.name)); + // A Symbol.toStringTag getter (an own tag field is listed instead) + // brackets the tag after the name, `Name [tag] {`, when it is a + // nonempty string other than the name. A getter that throws shows + // no tag, as in Node. + const tagGetter = info.fields.has(TO_STRING_TAG_FIELD) + ? null + : findMethodOn(lowerer, info, "get:sym:toStringTag"); + let named: (plain: string, make: (head: IrExpr) => IrExpr) => IrExpr = (plain) => + strLit(plain, loc); + if (tagGetter?.sig.ret.kind === "string") { + const owner = tagGetter.declarer.def.name; + locals.push( + { id: "tag.0", name: "tag", type: STRING, mutable: true }, + { id: "tagError.0", name: "tagError", type: CAUGHT, mutable: false }, + ); + body.push( + { kind: "varDecl", localId: "tag.0", init: strLit("", loc), loc }, + { + kind: "tryCatch", + tryBody: [ + { + kind: "assign", + localId: "tag.0", + value: accessorCall( + lowerer, + owner, + "get:sym:toStringTag", + lowerer.upcastTo(v(), owner), + [], + STRING, + loc, + ), + loc, + }, + ], + catchBody: [], + catchLocalId: "tagError.0", + finallyBody: null, + loc, + }, + ); + const tag = (): IrExpr => varRef("tag.0", STRING, loc); + const shown: IrExpr = { + kind: "logical", + op: "&&", + left: { + kind: "strEq", + negated: true, + left: tag(), + right: strLit("", loc), + type: BOOL, + loc, + }, + right: { + kind: "strEq", + negated: true, + left: tag(), + right: strLit(display, loc), + type: BOOL, + loc, + }, + type: BOOL, + loc, + }; + const head = concatAll( + [strLit(`${display === "" ? "Object" : display} [`, loc), tag(), strLit("]", loc)], + loc, + ); + named = (plain, make) => ({ + kind: "ternary", + cond: shown, + then: make(head), + else_: strLit(plain, loc), + type: STRING, + loc, + }); + } + const after = (text: string) => (head: IrExpr) => concatAll([head, strLit(text, loc)], loc); if (visible.length === 0) { - body = [ret(strLit(display === "" ? "{}" : `${display} {}`, loc))]; + body.push(ret(named(display === "" ? "{}" : `${display} {}`, after(" {}")))); break; } const get = (field: string, type: IrType): IrExpr => ({ @@ -1882,7 +2434,22 @@ function inspectHelper(lowerer: Lowerer, t: IrType, loc: SrcLoc): string { type, loc, }); - body = [depthGate(display === "" ? "[Object]" : `[${display}]`), ...begin()]; + body.push( + { + kind: "if", + cond: { kind: "bin", op: ">", left: r(), right: d(), type: BOOL, loc }, + then: [ + ret( + named(display === "" ? "[Object]" : `[${display}]`, (head) => + concatAll([strLit("[", loc), head, strLit("]", loc)], loc), + ), + ), + ], + else_: null, + loc, + }, + ...begin(), + ); // def.fields carries layout order: the base chain first, then own — // exactly the own-property insertion order of a constructor that // assigns in declaration order (SEMANTICS.md 36's stance). SYMBOL- @@ -1911,7 +2478,7 @@ function inspectHelper(lowerer: Lowerer, t: IrType, loc: SrcLoc): string { ret( end( strLit("", loc), - strLit(display === "" ? "{" : `${display} {`, loc), + named(display === "" ? "{" : `${display} {`, after(" {")), strLit("}", loc), false, boolLit(false, loc), @@ -2181,15 +2748,17 @@ export function lowerConsoleInspectArg( ); } if (!isBufferTypeName(tname)) { + // A u8 value the checker types as a Uint8Array may still hold a + // Buffer, which Node prints as one: the view's flags pick. return { kind: "libCall", - fn: "insp.typedArray", + fn: t.elem === "u8" ? "insp.bytes" : "insp.typedArray", args: [value, numLit(0, loc), numLit(2, loc)], type: STRING, loc, }; } - return inspectExpr(lowerer, t, value, numLit(0, loc), numLit(2, loc), loc); + return { kind: "libCall", fn: "insp.buffer", args: [value], type: STRING, loc }; } if (t.kind === "void") { // Node prints "undefined" for a void call's result; a void expression @@ -2229,13 +2798,22 @@ function isDataViewTypeName(tname: string): boolean { /* ── the callsite: options, direct function/class names ──────────────── */ /** The parsed options literal: the resolved depth (numeric; Infinity for - * `null`), breakLength (numeric; default 80) and compact (numeric, 0 for + * `null`), breakLength (numeric; default 80) and compact (numeric, -1 for * false; default 3), with every other non-default knob fenced by name. */ interface InspectOptions { depth: number; breakLength: number; compact: number; sorted: boolean; + /** Set by the `atRunTime` parse: colors or a computed knob value, which + * the runtime reads. */ + runtime: boolean; + /** How the depth option was spelled, for custom inspect methods: 0 a + * number (also when absent), 1 null, 2 undefined. */ + depthKind: number; + /** customInspect: false skips [util.inspect.custom] methods (console.dir's + * default). */ + customInspect: boolean; } function parseInspectOptions( @@ -2243,9 +2821,18 @@ function parseInspectOptions( node: ts.Expression | undefined, recursive: boolean, dir = false, + atRunTime = false, ): InspectOptions { const surface = dir ? "console.dir" : "util.inspect"; - const options: InspectOptions = { depth: 2, breakLength: 80, compact: 3, sorted: false }; + const options: InspectOptions = { + depth: 2, + breakLength: 80, + compact: 3, + sorted: false, + runtime: false, + depthKind: 0, + customInspect: !dir, + }; if (!node || (ts.isIdentifier(node) && node.text === "undefined")) return options; if (!ts.isObjectLiteralExpression(node)) { lowerer.noLowering( @@ -2256,6 +2843,11 @@ function parseInspectOptions( } for (const prop of node.properties.nodes) { if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name)) { + // `{ depth }` and `{ ...base }` hold values known at run time. + if (atRunTime && (ts.isShorthandPropertyAssignment(prop) || ts.isSpreadAssignment(prop))) { + options.runtime = true; + continue; + } lowerer.noLowering( `${surface} options in this form`, prop, @@ -2285,8 +2877,17 @@ function parseInspectOptions( // bounded by the (finite) object graph — exactly Node. void recursive; options.depth = Infinity; - } else if (!(ts.isIdentifier(value) && value.text === "undefined")) { + options.depthKind = value.kind === ts.SyntaxKind.NullKeyword ? 1 : 0; + } else if (ts.isIdentifier(value) && value.text === "undefined") { + // Node's ctx.depth is undefined: no depth limit. + options.depth = Infinity; + options.depthKind = 2; + } else { const n = numeric(); + if (n === null && atRunTime) { + options.runtime = true; + continue; + } if (n === null) { lowerer.noLowering( `${surface} with a non-literal depth option`, @@ -2297,7 +2898,8 @@ function parseInspectOptions( options.depth = n; } } else if (key === "colors") { - if (value.kind !== ts.SyntaxKind.FalseKeyword) { + if (value.kind !== ts.SyntaxKind.FalseKeyword && atRunTime) options.runtime = true; + else if (value.kind !== ts.SyntaxKind.FalseKeyword) { lowerer.noLowering( `${surface} with colors: true`, value, @@ -2309,6 +2911,10 @@ function parseInspectOptions( // compact of at least 1; false and smaller numbers break every // composite onto its own lines. compact: true is the older layout. const n = value.kind === ts.SyntaxKind.FalseKeyword ? 0 : numeric(); + if (n === null && atRunTime && value.kind !== ts.SyntaxKind.TrueKeyword) { + options.runtime = true; + continue; + } if (n === null || Number.isNaN(n)) { lowerer.noLowering( `${surface} with this compact option`, @@ -2316,9 +2922,22 @@ function parseInspectOptions( "compact must be false or a numeric literal (compact: true has no lowering)", ); } - options.compact = Math.max(n, 0); + // false is -1: the same layout as 0, and custom inspect functions + // read it back as false. + options.compact = value.kind === ts.SyntaxKind.FalseKeyword ? -1 : Math.max(n, 0); } else if (key === "sorted") { - if (value.kind !== ts.SyntaxKind.TrueKeyword && value.kind !== ts.SyntaxKind.FalseKeyword) { + const sortedLiteral = + value.kind === ts.SyntaxKind.TrueKeyword || value.kind === ts.SyntaxKind.FalseKeyword; + if ( + !sortedLiteral && + atRunTime && + !ts.isArrowFunction(value) && + !ts.isFunctionExpression(value) + ) { + options.runtime = true; + continue; + } + if (!sortedLiteral) { lowerer.noLowering( `${surface} with this sorted option`, value, @@ -2328,6 +2947,10 @@ function parseInspectOptions( options.sorted = value.kind === ts.SyntaxKind.TrueKeyword; } else if (key === "breakLength") { const n = ts.isIdentifier(value) && value.text === "Infinity" ? Infinity : numeric(); + if (n === null && atRunTime) { + options.runtime = true; + continue; + } if (n === null || Number.isNaN(n)) { lowerer.noLowering( `${surface} with a non-literal breakLength option`, @@ -2336,6 +2959,12 @@ function parseInspectOptions( ); } options.breakLength = n; + } else if ( + !dir && + key === "customInspect" && + (value.kind === ts.SyntaxKind.TrueKeyword || value.kind === ts.SyntaxKind.FalseKeyword) + ) { + options.customInspect = value.kind === ts.SyntaxKind.TrueKeyword; } else if (dir && key === "customInspect") { // console.dir's own default; only that value is lowered. if (value.kind !== ts.SyntaxKind.FalseKeyword) { @@ -2358,40 +2987,55 @@ function parseInspectOptions( /** A render under non-default breakLength/compact/sorted: the value is evaluated * first, the layout applies to this render only, and the defaults return - * afterwards. */ + * however the render ends, an inspection refusal included. The render runs + * in a lifted function, `(v) => { insp.layout(...); try { return render(v); } + * finally { insp.layout(80, 3, false); } }`, since a seqExpr cannot hold the + * try; `tag` names the render for the function's key. */ function withInspectLayout( lowerer: Lowerer, options: InspectOptions, value: IrExpr, + tag: string, render: (value: IrExpr) => IrExpr, loc: SrcLoc, ): IrExpr { if (options.breakLength === 80 && options.compact === 3 && !options.sorted) return render(value); - const input = lowerer.declareHiddenLocal("%inspectInput", value.type); - const text = lowerer.declareHiddenLocal("%inspectText", STRING); - const layout = (breakLength: number, compact: number, sorted: boolean): IrStmt => ({ - kind: "exprStmt", - expr: { - kind: "libCall", - fn: "insp.layout", - args: [numLit(breakLength, loc), numLit(compact, loc), boolLit(sorted, loc)], - type: VOID, + const key = `layout:${tag}:${typeKey(value.type)}:${options.breakLength}:${options.compact}:${options.sorted}:${options.depth}`; + let name = lowerer.inspectHelpers.get(key); + if (name === undefined) { + name = keyedHelperName(lowerer, "%util.inspLayout", key); + lowerer.inspectHelpers.set(key, name); + const layout = (breakLength: number, compact: number, sorted: boolean): IrStmt => ({ + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "insp.layout", + args: [numLit(breakLength, loc), numLit(compact, loc), boolLit(sorted, loc)], + type: VOID, + loc, + }, loc, - }, - loc, - }); - return { - kind: "seqExpr", - stmts: [ - { kind: "varDecl", localId: input.id, init: value, loc }, - layout(options.breakLength, options.compact, options.sorted), - { kind: "varDecl", localId: text.id, init: render(varRef(input.id, value.type, loc)), loc }, - layout(80, 3, false), - ], - result: varRef(text.id, STRING, loc), - type: STRING, - loc, - }; + }); + lowerer.liftedFns.push({ + name, + params: [{ localId: "v.0", name: "v", type: value.type }], + returnType: STRING, + locals: [{ id: "v.0", name: "v", type: value.type, mutable: false }], + body: [ + layout(options.breakLength, options.compact, options.sorted), + { + kind: "tryCatch", + tryBody: [{ kind: "return", value: render(varRef("v.0", value.type, loc)), loc }], + catchBody: null, + catchLocalId: null, + finallyBody: [layout(80, 3, false)], + loc, + }, + ], + loc, + }); + } + return { kind: "call", callee: name, args: [value], type: STRING, loc }; } /** `util.inspect(fn)` / `util.inspect(Cls)` over a DIRECT identifier @@ -2416,6 +3060,12 @@ function directCallableInspect(lowerer: Lowerer, node: ts.Expression, loc: SrcLo } const decl = sym.declarations[0]; if (!decl) return null; + // A JavaScript function can hold own properties (`f.x = 1`, computed + // keys), which the runtime walk prints after its name. + if (isJsSourceFile(node.getSourceFile())) { + const type = lowerer.mapTypeOf(lowerer.typeOf(node)); + if (type?.kind === "func" && lowerer.dynConvertible(type)) return null; + } if (ts.isFunctionDeclaration(decl) && decl.name) { return strLit(`[Function: ${decl.name.text}]`, loc); } @@ -2433,16 +3083,86 @@ function directCallableInspect(lowerer: Lowerer, node: ts.Expression, loc: SrcLo return null; } -/** util.inspect(value[, options]) — the spoke's inspect entry. */ +/** util.inspect(value[, options]) and inspect(value, showHidden, depth, + * colors) — the spoke's inspect entry. Options the compiler resolves + * render through the static helpers; colors, option values known only at + * run time and the legacy forms render the value's checked-dynamic view + * in the runtime (insp.inspect). */ function lowerInspectCall(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr { - if (expr.arguments.nodes.length < 1 || expr.arguments.nodes.length > 2) { + const args = expr.arguments.nodes; + if (args.length < 1 || args.length > 4 || args.some(ts.isSpreadElement)) { lowerer.noLowering( - `util.inspect with ${expr.arguments.nodes.length} arguments`, + `util.inspect with ${args.some(ts.isSpreadElement) ? "spread" : args.length} arguments`, expr, - "the supported forms are inspect(value) and inspect(value, options)", + "the supported forms are inspect(value), inspect(value, options) and inspect(value, showHidden, depth, colors)", ); } - return lowerInspectValue(lowerer, expr.arguments.nodes[0]!, expr.arguments.nodes[1], loc, false); + if (args.length <= 2 && !inspectOptionsAtRunTime(lowerer, args[1])) + return lowerInspectValue(lowerer, args[0]!, args[1], loc, false); + return lowerRuntimeInspect(lowerer, args, loc); +} + +/** Whether an options argument needs the runtime reader: an options value + * that is not an object literal, or a literal with colors or a computed + * depth, compact, breakLength or sorted value. Literal options otherwise + * keep the static parse, which also reports the literal options neither + * path lowers. */ +function inspectOptionsAtRunTime(lowerer: Lowerer, node: ts.Expression | undefined): boolean { + if (!node || (ts.isIdentifier(node) && node.text === "undefined")) return false; + if (!ts.isObjectLiteralExpression(node)) return true; + return parseInspectOptions(lowerer, node, false, false, true).runtime; +} + +/** util.inspect over the value's checked-dynamic view, with every argument + * evaluated in order and read in the runtime as Node's inspect reads it. */ +function lowerRuntimeInspect( + lowerer: Lowerer, + args: readonly ts.Expression[], + loc: SrcLoc, +): IrExpr { + const valueNode = args[0]!; + if ( + directCallableInspect(lowerer, valueNode, loc) !== null || + ts.isArrowFunction(valueNode) || + ts.isFunctionExpression(valueNode) + ) { + lowerer.noLowering( + "util.inspect of a function or class with these options", + valueNode, + "pass the options as a literal without colors", + ); + } + const value = lowerer.lowerExpr(valueNode); + if (value.type.kind !== "dyn") { + const reason = inspectSupport(lowerer, value.type, new Set(), { recursive: false }); + if (reason !== null) + lowerer.noLowering(`util.inspect of '${lowerer.fmt(value.type)}' values`, valueNode, reason); + if (!lowerer.dynConvertible(value.type)) + lowerer.noLowering( + `util.inspect with these options of '${lowerer.fmt(value.type)}' values`, + valueNode, + "the options are read at run time, where the value renders through its checked-dynamic view", + ); + } + const stmts: IrStmt[] = []; + const held = (init: IrExpr): IrExpr => { + const local = lowerer.declareHiddenLocal("%inspectArg", DYN); + stmts.push({ kind: "varDecl", localId: local.id, init, loc }); + return varRef(local.id, DYN, loc); + }; + const operands: IrExpr[] = [held(lowerer.coerceToExpected(value, DYN))]; + for (let i = 1; i < 4; i++) { + const node = args[i]; + operands.push(node ? held(lowerer.lowerExprExpecting(node, DYN)) : dynUndefinedExpr(loc)); + } + operands.push(numLit(args.length, loc)); + return { + kind: "seqExpr", + stmts, + result: { kind: "libCall", fn: "insp.inspect", args: operands, type: STRING, loc }, + type: STRING, + loc, + }; } /** `inspect(value, options)` over one value node: the shared body of @@ -2476,7 +3196,17 @@ export function lowerInspectValue( loc, ); } - const value = lowerer.lowerExpr(valueNode); + let value = lowerer.lowerExpr(valueNode); + // A JavaScript function renders through the runtime walk, which prints + // its own properties after its name. + if ( + value.type.kind === "func" && + isJsSourceFile(valueNode.getSourceFile()) && + lowerer.dynConvertible(value.type) + ) { + const name = jsFuncNameOf(valueNode); + value = { kind: "dynFrom", value, type: DYN, ...(name !== null ? { fnName: name } : {}), loc }; + } if (value.type.kind === "bytes") { // One runtime representation serves Buffer AND Uint8Array; their // renderings differ ( vs Uint8Array(1) [ 170 ]), so the @@ -2508,9 +3238,13 @@ export function lowerInspectValue( lowerer, options, value, + "typedArray", (input) => ({ kind: "libCall", - fn: "insp.typedArray", + fn: + value.type.kind === "bytes" && value.type.elem === "u8" + ? "insp.bytes" + : "insp.typedArray", args: [input, numLit(0, loc), numLit(options.depth, loc)], type: STRING, loc, @@ -2519,7 +3253,7 @@ export function lowerInspectValue( ); } // Buffers render fully at any depth and on one line (custom inspect). - return inspectExpr(lowerer, value.type, value, numLit(0, loc), numLit(2, loc), loc); + return { kind: "libCall", fn: "insp.buffer", args: [value], type: STRING, loc }; } const walk = { recursive: false }; const reason = inspectSupport(lowerer, value.type, new Set(), walk); @@ -2528,11 +3262,53 @@ export function lowerInspectValue( } const options = parseInspectOptions(lowerer, optionsNode, walk.recursive, dir); const depth = options.depth; + // Custom inspect functions read the options as Node spelled them: an + // unbounded depth given as Infinity or undefined, and customInspect: + // false (console.dir's default), apply around the render when the value + // can reach a custom method; the previous settings return afterwards. + const reachesCustom = + typeReachesCustomInspect(lowerer, value.type) || typeReachesDyn(lowerer, value.type); + const depthContext = reachesCustom && depth === Infinity && options.depthKind !== 1; + const customContext = reachesCustom && !options.customInspect; + if (depthContext || customContext) { + const stmts: IrStmt[] = []; + const restore: IrStmt[] = []; + const setting = (fn: "insp.customInspect" | "insp.depthKind", arg: IrExpr, type: IrType) => { + const previous = lowerer.declareHiddenLocal("%inspectContext", type); + const call = (a: IrExpr): IrExpr => ({ kind: "libCall", fn, args: [a], type, loc }); + stmts.push({ kind: "varDecl", localId: previous.id, init: call(arg), loc }); + restore.push({ kind: "exprStmt", expr: call(varRef(previous.id, type, loc)), loc }); + }; + if (customContext) setting("insp.customInspect", boolLit(false, loc), BOOL); + if (depthContext) setting("insp.depthKind", numLit(options.depthKind, loc), F64); + const text = lowerer.declareHiddenLocal("%inspectText", STRING); + stmts.push({ + kind: "varDecl", + localId: text.id, + init: withInspectLayout( + lowerer, + options, + value, + "inspect", + (input) => inspectExpr(lowerer, value.type, input, numLit(0, loc), numLit(depth, loc), loc), + loc, + ), + loc, + }); + return { + kind: "seqExpr", + stmts: [...stmts, ...restore.reverse()], + result: varRef(text.id, STRING, loc), + type: STRING, + loc, + }; + } if (dir && value.type.kind === "dyn") { return withInspectLayout( lowerer, options, value, + "dirDyn", (input) => ({ kind: "libCall", fn: "console.dirDyn", @@ -2547,6 +3323,7 @@ export function lowerInspectValue( lowerer, options, value, + "inspect", (input) => inspectExpr(lowerer, value.type, input, numLit(0, loc), numLit(depth, loc), loc), loc, ); @@ -2558,7 +3335,17 @@ export function lowerInspectValue( * formatNumber, strings verbatim, objects through inspect at the given * depth). Null = fence with the given reason. */ function formatSArg(lowerer: Lowerer, node: ts.Expression, depth: number, loc: SrcLoc): IrExpr { - const value = lowerer.lowerExpr(node); + return formatSValue(lowerer, node, lowerer.lowerExpr(node), depth, loc); +} + +/** formatSArg over the already-lowered value of `node`. */ +function formatSValue( + lowerer: Lowerer, + node: ts.Expression, + value: IrExpr, + depth: number, + loc: SrcLoc, +): IrExpr { const t = value.type; if (t.kind === "string") return value; if (t.kind === "f64") @@ -2575,7 +3362,7 @@ function formatSArg(lowerer: Lowerer, node: ts.Expression, depth: number, loc: S if (t.kind === "dyn") { return { kind: "libCall", - fn: "insp.dynS", + fn: "insp.dynFormatS", args: [value, numLit(depth, loc)], type: STRING, loc, @@ -2623,10 +3410,21 @@ function formatSArg(lowerer: Lowerer, node: ts.Expression, depth: number, loc: S } /** %O (depth 2) / %o (showHidden, depth 4) — inspect with the spec's - * option deltas; %o fences over arrays (showHidden adds their hidden - * [length] entry, which has no lowering). */ + * option deltas. A checked-dynamic argument renders through the runtime's + * showHidden walk; static %o fences over arrays and nested dynamic values + * (showHidden adds an array's hidden [length] entry, which the static + * rendering has no lowering for). */ function formatOArg(lowerer: Lowerer, node: ts.Expression, depth: number, loc: SrcLoc): IrExpr { const value = lowerer.lowerExpr(node); + if (depth === 4 && value.type.kind === "dyn") { + return { + kind: "libCall", + fn: "insp.dynHidden", + args: [value, numLit(4, loc)], + type: STRING, + loc, + }; + } if (value.type.kind === "bytes") { lowerer.noLowering( `util.format %${depth === 4 ? "o" : "O"} of typed-array values`, @@ -2642,11 +3440,18 @@ function formatOArg(lowerer: Lowerer, node: ts.Expression, depth: number, loc: S node, reason, ); - if (depth === 4 && typeTreeHasArray(lowerer, value.type, new Set())) { + if (depth === 4 && typeReachesCustomInspect(lowerer, value.type)) { + lowerer.noLowering( + "util.format %o over values with [util.inspect.custom] methods", + node, + "%o passes showHidden to the methods — use %O", + ); + } + if (depth === 4 && typeTreeHasHidden(lowerer, value.type, new Set())) { lowerer.noLowering( "util.format %o over arrays", node, - "%o renders with showHidden (arrays gain a hidden [length] entry) — use %O for the default rendering", + "%o renders with showHidden (arrays gain a hidden [length] entry) — use %O for the default rendering, or pass the value as unknown", ); } return inspectExpr(lowerer, value.type, value, numLit(0, loc), numLit(depth, loc), loc); @@ -2724,8 +3529,9 @@ function formatIntegerFixed(value: IrExpr, spec: "%d" | "%i", loc: SrcLoc): IrEx /** util.format(...) / util.formatWithOptions({}, ...): the compile-time * %-substitution over a literal format string (formatWithOptionsInternal * ported exactly — the arg cursor, the args-exhausted guard, %c's - * consume-and-drop), with per-static-type conversions. Non-literal - * format strings lower only in the substitution-free shapes. */ + * consume-and-drop), with per-static-type conversions. Format strings + * known only at run time, spread arguments and non-empty options lower to + * the runtime formatter (lowerRuntimeFormat, heldFormatArgs). */ export function lowerFormatCall( lowerer: Lowerer, expr: ts.CallExpression, @@ -2737,30 +3543,61 @@ export function lowerFormatCall( const argNodes = [...(argNodesOverride ?? expr.arguments.nodes)]; if (withOptions) { const opts = argNodes.shift(); + // The empty literal {} is the defaults: exactly util.format. if (!opts || !ts.isObjectLiteralExpression(opts) || opts.properties.nodes.length > 0) { - lowerer.noLowering( - "util.formatWithOptions with a non-empty options literal", - opts ?? expr, - "only the empty literal {} (the defaults — exactly util.format) is lowered", - ); + if (opts && ts.isSpreadElement(opts)) { + lowerer.noLowering( + "util.formatWithOptions with a spread options argument", + opts, + "pass the options object as the first argument", + ); + } + return lowerRuntimeFormat(lowerer, argNodes, loc, opts ?? null); } } if (argNodes.length === 0) return strLit("", loc); const first = argNodes[0]!; const firstIsLiteral = ts.isStringLiteral(first) || ts.isNoSubstitutionTemplateLiteral(first); + if (argNodes.some(ts.isSpreadElement)) { + if (literalPrefix !== "" && !firstIsLiteral) + throw new InternalCompilerError("a format prefix needs a literal format string"); + return lowerRuntimeFormat(lowerer, argNodes, loc, undefined, literalPrefix); + } /** The rest-args tail: ` ` + (strings verbatim, everything else * inspected at the default depth). */ const restArg = (node: ts.Expression): IrExpr => formatSArg(lowerer, node, 2, loc); if (!firstIsLiteral) { - const firstT = lowerer.mapTypeOf(lowerer.typeOf(first)); - if (firstT?.kind === "string" && argNodes.length > 1) { - lowerer.noLowering( - "util.format with a runtime format string and further arguments", - first, - "the %-substitution positions are runtime-dependent — use a string literal", - ); + const runtime = argNodes.length > 1 ? runtimeFormatString(lowerer, first) : null; + if (runtime === "other" && !formatStringConverts(lowerer, first)) + return lowerRuntimeFormat(lowerer, argNodes, loc); + if (runtime !== null) { + // A string that substitutes formats at run time; any other string + // joins with the remaining arguments, as below. + const held = heldFormatArgs(lowerer, argNodes, loc); + if (!held.joinable) { + return { kind: "seqExpr", stmts: held.stmts, result: held.format(), type: STRING, loc }; + } + const joined: IrExpr[] = []; + held.args.forEach((arg, i) => { + if (i > 0) joined.push(strLit(" ", loc)); + joined.push(formatRestValue(lowerer, arg.node, arg.ref(), loc)); + }); + return { + kind: "seqExpr", + stmts: held.stmts, + result: { + kind: "ternary", + cond: held.specifies, + then: held.format(), + else_: concatAll(joined, loc), + type: STRING, + loc, + }, + type: STRING, + loc, + }; } // No substitutions possible: every argument joins with spaces. const parts: IrExpr[] = []; @@ -3046,36 +3883,288 @@ export function lowerFormatCall( return concatAll(parts, loc); } +/** One space-joined argument of util.format, as Node's formatter renders + * it after the format string: strings verbatim, everything else through + * inspect at the default depth. */ +function formatRestValue( + lowerer: Lowerer, + node: ts.Expression, + value: IrExpr, + loc: SrcLoc, +): IrExpr { + switch (value.type.kind) { + case "string": + return value; + case "f64": + return { kind: "libCall", fn: "insp.f64", args: [value], type: STRING, loc }; + case "bool": + return { kind: "toString", operand: value, type: STRING, loc }; + case "dyn": + return { kind: "libCall", fn: "insp.dynS", args: [value, numLit(2, loc)], type: STRING, loc }; + default: + return lowerConsoleInspectArg(lowerer, node, value, "util.format", loc); + } +} + +const UNCONVERTIBLE_HINT = + "the runtime formatter reads its arguments as checked-dynamic values — use a string literal format"; + +/** A string-valued expression that throws the SC2020 refusal where it runs + * (a JavaScript construct fence deferred to the branch that needs it). */ +function runtimeFenceValue( + lowerer: Lowerer, + surface: string, + node: ts.Node, + hint: string, + loc: SrcLoc, +): IrExpr { + const diag = noLoweringDiag(surface, locOf(node), hint); + lowerer.runtimeFences.push(diag); + const sf = ts.getSourceFileOfNode(node); + const line = ts.getLineAndCharacterOfPosition(sf, diag.loc.start).line + 1; + const slot = lowerer.declareHiddenLocal("%never", STRING); + slot.mutable = true; + return { + kind: "seqExpr", + stmts: [ + { kind: "varDecl", localId: slot.id, init: null, loc }, + { + kind: "runtimeFence", + code: diag.code, + message: `${diag.message} [${diag.code} at ${diag.loc.file}:${line}]`, + loc, + }, + ], + result: varRef(slot.id, STRING, loc), + diverges: true, + type: STRING, + loc, + }; +} + +/** Whether the held-argument form can test a format string whose type + * only may hold a string: a union that converts to a checked-dynamic value, + * or a value whose checker type does not map (its lowered type decides). */ +export function formatStringConverts(lowerer: Lowerer, first: ts.Expression): boolean { + const t = lowerer.mapTypeOf(lowerer.typeOf(first)); + return t === null || t.kind === "dyn" || lowerer.dynConvertible(t); +} + +/** How a non-literal format string is known: "string" and "dyn" values + * test for a substitution at run time, "other" types (unions with a string + * arm) format at run time, and null types never hold a string. */ +export function runtimeFormatString( + lowerer: Lowerer, + first: ts.Expression, +): "string" | "dyn" | "other" | null { + const t = lowerer.mapTypeOf(lowerer.typeOf(first)); + if (t === null) return "other"; + if (t.kind === "string") return "string"; + if (t.kind === "dyn") return "dyn"; + if (t.kind === "union") { + const arms = lowerer.unions.get(t.unionId)?.arms ?? []; + return arms.some((arm) => arm.kind === "string" || arm.kind === "dyn") ? "other" : null; + } + return t.kind === "jsval" || t.kind === "caught" ? "other" : null; +} + +/** The runtime formatter over a call's arguments: one checked-dynamic + * pack in source order (spreads contribute their elements), after the + * options when `options` is given (null: an omitted options argument, + * which Node's validation rejects). A literal prefix joins a literal + * format string. */ +export function lowerRuntimeFormat( + lowerer: Lowerer, + argNodes: readonly ts.Expression[], + loc: SrcLoc, + options?: ts.Expression | null, + literalPrefix = "", +): IrExpr { + const stmts: IrStmt[] = []; + let optionsRef: IrExpr | null = null; + if (options !== undefined) { + const held = lowerer.declareHiddenLocal("%formatOptions", DYN); + stmts.push({ + kind: "varDecl", + localId: held.id, + init: options === null ? dynUndefinedExpr(loc) : lowerer.lowerExprExpecting(options, DYN), + loc, + }); + optionsRef = varRef(held.id, DYN, loc); + } + let nodes = argNodes; + const leading: IrExpr[] = []; + if (literalPrefix !== "") { + const first = argNodes[0] as ts.StringLiteralLike; + leading.push(strLit(literalPrefix + first.text, loc)); + nodes = argNodes.slice(1); + } + const pack = dynArgumentPack(lowerer, nodes, loc, { + leading, + blame: argNodes[0] ?? options ?? undefined, + }); + stmts.push(...pack.stmts); + const result: IrExpr = + optionsRef === null + ? { kind: "libCall", fn: "insp.format", args: [pack.pack()], type: STRING, loc } + : { + kind: "libCall", + fn: "insp.formatWithOptions", + args: [optionsRef, pack.pack()], + type: STRING, + loc, + }; + return { kind: "seqExpr", stmts, result, type: STRING, loc }; +} + +/** The arguments of a call whose first argument is a string or dyn value + * known only at run time, each evaluated once into a hidden local: + * `specifies` tests that string for a '%' before its last character, and + * `format()` is the runtime formatter over the held values. The caller + * renders the other branch (the space-joined arguments) from `args` when + * every argument is `joinable` there. */ +export function heldFormatArgs( + lowerer: Lowerer, + argNodes: readonly ts.Expression[], + loc: SrcLoc, +): { + stmts: IrStmt[]; + args: { node: ts.Expression; ref: () => IrExpr }[]; + specifies: IrExpr; + format: () => IrExpr; + joinable: boolean; +} { + const stmts: IrStmt[] = []; + let unconvertible: { node: ts.Expression; type: IrType } | null = null; + const unconvertibleSurface = (t: IrType): string => + `util.format with a runtime format string over '${lowerer.fmt(t)}' values`; + const args = argNodes.map((node) => { + const value = lowerer.lowerExpr(node); + if (value.type.kind === "void") { + lowerer.unsupported( + "SC1090", + node, + "util.format of a void call result (call it as its own statement, then pass undefined)", + ); + } + // The space-joined branch prints these values as before; only a string + // that substitutes refuses them, where it runs. + if (value.type.kind !== "dyn" && !lowerer.dynConvertible(value.type)) + unconvertible ??= { node, type: value.type }; + // Unit values have no local: their effects run here and a literal + // stands in. + const k = value.type.kind; + if (k === "undefinedT" || k === "nullT") { + if (value.kind !== "unitLit") stmts.push({ kind: "exprStmt", expr: value, loc }); + const unit = k === "nullT" ? ("null" as const) : ("undefined" as const); + return { node, ref: (): IrExpr => ({ kind: "unitLit", unit, type: value.type, loc }) }; + } + const held = lowerer.declareHiddenLocal("%formatArg", value.type); + stmts.push({ kind: "varDecl", localId: held.id, init: value, loc }); + return { node, ref: () => varRef(held.id, value.type, loc) }; + }); + const first = args[0]!.ref(); + // A lowered value that is neither a string nor convertible holds no + // string, so it never substitutes. + const specifies: IrExpr = + first.type.kind === "string" + ? { kind: "libCall", fn: "insp.formatSpecifies", args: [first], type: BOOL, loc } + : first.type.kind === "dyn" || lowerer.dynConvertible(first.type) + ? { + kind: "libCall", + fn: "insp.formatSpecifiesDyn", + args: [lowerer.coerceToExpected(first, DYN)], + type: BOOL, + loc, + } + : { kind: "boolLit", value: false, type: BOOL, loc }; + const format = (): IrExpr => { + if (unconvertible !== null) { + const fenced = unconvertible; + return runtimeFenceValue( + lowerer, + unconvertibleSurface(fenced.type), + fenced.node, + UNCONVERTIBLE_HINT, + loc, + ); + } + // Strings, numbers, booleans, null and undefined format without the + // checked-dynamic walk other values need, which keeps it out of the + // executable. + const primitive = (t: IrType): boolean => + t.kind === "string" || + t.kind === "f64" || + t.kind === "bool" || + t.kind === "undefinedT" || + t.kind === "nullT" || + (t.kind === "union" && (lowerer.unions.get(t.unionId)?.arms.every(primitive) ?? false)); + // A native list of the checked values: converting into it is a tier + // boundary, so the statement stays static. + return { + kind: "libCall", + fn: args.every((arg) => primitive(arg.ref().type)) + ? "insp.formatPrimitives" + : "insp.formatList", + args: [ + { + kind: "arrayLit", + elems: args.map((arg) => lowerer.coerceToExpected(arg.ref(), DYN)), + type: arrayOf(DYN), + loc, + }, + ], + type: STRING, + loc, + }; + }; + // The space-joined branch renders each argument statically; arguments + // the static rendering refuses take the runtime formatter in both cases. + const joinable = args.every(({ ref }) => { + const t = ref().type; + if (t.kind === "func") return false; + if (t.kind === "bytes") return true; + return inspectSupport(lowerer, t, new Set(), { recursive: false }) === null; + }); + // Without a joined branch every call formats: TypeScript refuses the + // value at compile time. + const refused = unconvertible as { node: ts.Expression; type: IrType } | null; + if (!joinable && refused !== null && !isJsSourceFile(ts.getSourceFileOfNode(refused.node))) + lowerer.noLowering(unconvertibleSurface(refused.type), refused.node, UNCONVERTIBLE_HINT); + return { stmts, args, specifies, format, joinable }; +} + /** Arrays anywhere in the tree — the %o (showHidden) gate. */ -function typeTreeHasArray(lowerer: Lowerer, t: IrType, visiting: Set): boolean { +function typeTreeHasHidden(lowerer: Lowerer, t: IrType, visiting: Set): boolean { switch (t.kind) { case "array": + case "dyn": return true; case "record": { if (visiting.has(t.shapeId)) return false; visiting.add(t.shapeId); const shape = lowerer.shapes.get(t.shapeId); if (shape?.tuple) return true; - return (shape?.fields ?? []).some((f) => typeTreeHasArray(lowerer, f.type, visiting)); + return (shape?.fields ?? []).some((f) => typeTreeHasHidden(lowerer, f.type, visiting)); } case "map": return ( - typeTreeHasArray(lowerer, t.key, visiting) || typeTreeHasArray(lowerer, t.value, visiting) + typeTreeHasHidden(lowerer, t.key, visiting) || typeTreeHasHidden(lowerer, t.value, visiting) ); case "set": - return typeTreeHasArray(lowerer, t.elem, visiting); + return typeTreeHasHidden(lowerer, t.elem, visiting); case "union": { if (visiting.has(t.unionId)) return false; visiting.add(t.unionId); return (lowerer.unions.get(t.unionId)?.arms ?? []).some((a) => - typeTreeHasArray(lowerer, a, visiting), + typeTreeHasHidden(lowerer, a, visiting), ); } case "object": { if (visiting.has(t.className)) return false; visiting.add(t.className); return (lowerer.classes.get(t.className)?.def.fields ?? []).some((f) => - typeTreeHasArray(lowerer, f.type, visiting), + typeTreeHasHidden(lowerer, f.type, visiting), ); } case "bytes": @@ -3252,8 +4341,14 @@ function lowerUtilCallableCall( member === "inherits" && expr.arguments.nodes[1] !== undefined ? builtinClassKindOf(lowerer, expr.arguments.nodes[1]) : null; + // A superCtor class whose prototype has a complete view (a module-level + // JavaScript class named directly) passes that view to the runtime. + const superView = + member === "inherits" && builtinBase === null && expr.arguments.nodes[1] !== undefined + ? classPrototypeView(lowerer, expr.arguments.nodes[1], loc) + : null; for (const [index, arg] of expr.arguments.nodes.slice(0, classArgs).entries()) { - if (index === 1 && builtinBase !== null) continue; + if (index === 1 && (builtinBase !== null || superView !== null)) continue; if (lowerer.mapTypeOf(lowerer.typeOf(arg))?.kind === "classval") lowerer.noLowering( `util.${member} over a class constructor`, @@ -3285,7 +4380,9 @@ function lowerUtilCallableCall( for (const arg of expr.arguments.nodes.slice(arity)) stmts.push({ kind: "exprStmt", expr: lowerer.lowerExpr(arg), loc }); const type = fn === "util.inherits" ? VOID : DYN; - const result: IrExpr = { kind: "libCall", fn, args, type, loc }; + const result: IrExpr = superView + ? { kind: "libCall", fn: "util.inheritsPrototype", args: [...args, superView], type, loc } + : { kind: "libCall", fn, args, type, loc }; return { kind: "seqExpr", stmts, result, type: result.type, loc }; } diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts index b9c9e32c7..bffee4ef6 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.ts @@ -1,3 +1,5 @@ +import { computedGlobalReadOf, globalValuesInstall } from "./global-values.js"; +import type { TypeFailureSlot } from "../../diagnostics/diagnostic.js"; import { THREADS_MODULE } from "../threads-import.js"; import { NODE_API_MODULE } from "../node-api-import.js"; import { positionTag } from "./structural-positions.js"; @@ -44,10 +46,14 @@ import { } from "./cycle-initialization.js"; import { markRequireEarlyBindings } from "./require-initialization.js"; import { + cjsHasLiveMemberExports, cjsTableMethodEntry, inlineProgramRequireOf, nestedCjsExportAssignmentsOf, requireNestedPatternsOf, + splitRequireBindingOf, + splitRequireBindingOfIdent, + wholeExportSymbolOfModule, } from "./cjs-inline-require.js"; import { isDeadTscHelperDecl, tscInteropBindingOfDecl } from "./cjs-tsc-interop.js"; import { collectJsonRequires, isJsonRequireDecl } from "./json-require.js"; @@ -94,6 +100,7 @@ import { createRequireProgramModuleDecl, } from "./builtins/module-bindings.js"; import { isPromisifyCall, registerBuiltinCallableAlias } from "./builtins/callable-aliases.js"; +import { customInspectRegistrations } from "./lower-inspect.js"; import { registerBuiltinClassAlias } from "./builtins/class-values.js"; import { stripTypeCasts } from "./builtins/arguments.js"; import { isWebCryptoMemberType } from "./builtins/webcrypto.js"; @@ -174,6 +181,17 @@ export interface AppendGraph { * the require() edges preflight resolved — a CommonJS file's only edges * (the compiled CommonJS package a workspace module loaded on demand * imports, and the sibling packages and files IT requires). */ +/** Whether a class declaration has a static field with an initializer, + * which Node runs where the class statement evaluates. */ +function hasStaticFieldInitializer(decl: ts.ClassDeclaration): boolean { + return decl.members.nodes.some( + (member) => + ts.isPropertyDeclaration(member) && + member.initializer !== undefined && + member.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.StaticKeyword) === true, + ); +} + function appendedEdgesOf( program: ts.Program, graph: AppendGraph | undefined, @@ -347,24 +365,28 @@ function dynamicImportProgramTargetOf( } /** Every distinct program module a file's `import("literal")` sites name - * (source order, deduped). */ + * (source order, deduped). The parser lists the specifier of every such call + * in the file's module references, in source order after the static imports, + * so the file's nodes need no walk. */ function dynamicProgramImportsOf(program: ts.Program, sf: ts.SourceFile): ts.SourceFile[] { const out: ts.SourceFile[] = []; const seen = new Set(); - ts.walkPreorder(sf, (node) => { + for (const specifier of sf.imports) { + const call = specifier.parent; if ( - ts.isCallExpression(node) && - node.expression.kind === ts.SyntaxKind.ImportKeyword && - node.arguments.nodes[0] !== undefined && - ts.isStringLiteralLike(node.arguments.nodes[0]) - ) { - const dep = dynamicImportProgramTargetOf(program, sf, node.arguments.nodes[0].text); - if (dep && !seen.has(dep)) { - seen.add(dep); - out.push(dep); - } + call === undefined || + !ts.isCallExpression(call) || + call.expression.kind !== ts.SyntaxKind.ImportKeyword || + call.arguments.nodes[0] !== specifier || + !ts.isStringLiteralLike(specifier) + ) + continue; + const dep = dynamicImportProgramTargetOf(program, sf, specifier.text); + if (dep && !seen.has(dep)) { + seen.add(dep); + out.push(dep); } - }); + } return out; } @@ -951,6 +973,12 @@ export function moduleArtifacts( visitClass("%Readable"); visitClass("%Writable"); } + // A class declared `extends Array` named by its instanceof test + // or by X.of / X.from. + if (node.fn === "array.dynIsClass" && node.args[1]?.kind === "strLit") + visitClass(node.args[1].value); + if (node.fn === "array.classConstructAdopt" && node.args[0]?.kind === "strLit") + visitClass(node.args[0].value); break; case "dynFrom": if (boxedChild(node.value.type)) { @@ -1032,13 +1060,11 @@ export function moduleArtifacts( } while (pendingClasses.length > 0) { const name = pendingClasses.pop()!; - const info = lowerer.classes.get(name); - if (!info) { - // Referenced by an emitted type but never registered: collection - // deferred its diagnostics — a reached reference makes them count. - lowerer.flushDeferredClass(name); - continue; - } + // Referenced by an emitted type but never registered: collection + // deferred its diagnostics — a reached reference makes them count + // (or registers the class with dynamic slots, retryDeferred). + const info = lowerer.classes.get(name) ?? lowerer.flushDeferredClass(name); + if (!info) continue; for (const field of info.def.fields) visitType(field.type); if (info.base) visitClass(info.base.def.name); if (info.def.genericOf) visitClass(info.def.genericOf); @@ -1185,19 +1211,22 @@ function jsDynHoldableInitializer(lowerer: Lowerer, init: ts.Expression | undefi } if (e.kind === ts.SyntaxKind.NullKeyword) return true; if (ts.isIdentifier(e) && e.text === "undefined") return true; - if (ts.isConditionalExpression(e)) { - return ( - jsDynHoldableInitializer(lowerer, e.whenTrue) && - jsDynHoldableInitializer(lowerer, e.whenFalse) - ); - } + // The arms of a conditional or logical expression: checked values, or + // strings, numbers and booleans, which box into the checked-dynamic + // result (`const APP_NAME = config.name || "pi"`). + const arm = (x: ts.Expression): boolean => { + if (jsDynHoldableInitializer(lowerer, x)) return true; + const kind = lowerer.mapTypeOf(lowerer.typeOf(x))?.kind; + return kind === "string" || kind === "f64" || kind === "bool"; + }; + if (ts.isConditionalExpression(e)) return arm(e.whenTrue) && arm(e.whenFalse); if ( ts.isBinaryExpression(e) && (e.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken || e.operatorToken.kind === ts.SyntaxKind.BarBarToken || e.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) ) { - return jsDynHoldableInitializer(lowerer, e.left) && jsDynHoldableInitializer(lowerer, e.right); + return arm(e.left) && arm(e.right); } // A member of a checked module value is itself checked, including a // callable whose inferred overloads cannot describe one native ABI. @@ -1603,14 +1632,26 @@ export function collectGlobals( // require() of a program module is not: the exporter re-exports // that module's exports, which requirers resolve through the // checker's alias (the statement lowers to the dep's %init). + // A split binding re-exported whole with later member writes needs + // a mutable root: `module.exports = m; module.exports.extra = 1` + // extends the SAME object Node exposes. Without later members the + // binder is pure alias plumbing (no storage). + const splitWholeBinding = ts.isIdentifier(rhs) + ? splitRequireBindingOfIdent(lowerer, rhs) + : null; + const hasMemberExports = cjsHasLiveMemberExports(lowerer, sf); + const splitWhole = + splitWholeBinding !== null && + wholeExportSymbolOfModule(lowerer, splitWholeBinding.dep) !== null && + hasMemberExports; const factoryValued = - !ts.isIdentifier(rhs) && + (!ts.isIdentifier(rhs) || splitWhole) && !ts.isClassExpression(rhs) && !ts.isObjectLiteralExpression(rhs) && !( requireSpecOf(rhs) !== null && inlineProgramRequireOf(lowerer, rhs) !== null && - !sf.statements.nodes.some((s) => cjsExportAssignmentOf(s)?.kind === "member") + !hasMemberExports ) && cjsExportTargetLiteral(rhs, sf) === null; if (cjsScalarLiteral(rhs) || fnValued || factoryValued) { @@ -1623,6 +1664,9 @@ export function collectGlobals( // A returned object remains the live CommonJS root; later // exports.member writes must update that same open object. if (factoryValued && t?.kind === "record" && lowerer.dynConvertible(t)) t = DYN; + // A split whole-export root is extended by later member writes; + // keep it checked-dynamic so those writes reach the same object. + if (splitWhole && t && t.kind !== "dyn") t = DYN; if (t && t.kind !== "void") { const g: IrGlobal = { id: `%g.${tag}exports`, @@ -1953,18 +1997,21 @@ export function collectGlobals( // symbol registers HERE, before any use site can lower, and the // statement lowering skips the declaration by the same check. A // bad promisify target reports here (recorded once; the statement - // lowering poisons too, like every collectGlobals failure). + // lowering poisons too, like every collectGlobals failure). Targets + // that promisify to a function value (typed callback-last functions + // and untyped functions) keep an ordinary global. if ( ts.isIdentifier(decl.name) && decl.initializer && isPromisifyCall(lowerer, decl.initializer) ) { + let registered = true; try { - lowerer.registerPromisifiedBuiltinDecl(decl.name, decl.initializer); + registered = lowerer.registerPromisifiedBuiltinDecl(decl.name, decl.initializer); } catch (e) { if (!(e instanceof PoisonError)) throw e; } - continue; + if (registered) continue; } // `const inspect = require("util").inspect` at file scope: a // named import in const clothing (builtinImportOf resolves uses) @@ -2024,7 +2071,15 @@ export function collectGlobals( // Destructuring declarations register EVERY bound identifier (the // desugar in the init function assigns the pre-registered globals, // exactly like plain declarations). - for (const nameNode of boundIdentifiersOf(decl.name)) { + const names = boundIdentifiersOf(decl.name); + // Localized failure: a degradable type failure at a module global + // degrades that type and the same name registers again + // (globalSlotRetry), at most MAX_GLOBAL_RETRIES times. + let retries = 0; + let retriedIndex = -1; + for (let nameIndex = 0; nameIndex < names.length; nameIndex++) { + const nameNode = names[nameIndex]!; + if (nameIndex !== retriedIndex) retries = 0; const diagsBefore = lowerer.diags.length; try { // Checked JavaScript factories can expose inferred callables by @@ -2367,7 +2422,6 @@ export function collectGlobals( // References from separately-declared functions cascade to // their own per-site runtime fences. const factoryType = - !isLet && isJsSourceFile(sf) && !decl.type && !hasJsTypeAnnotation(decl) && @@ -2677,6 +2731,12 @@ export function collectGlobals( } } catch (e) { if (!(e instanceof PoisonError)) throw e; + if (retries < MAX_GLOBAL_RETRIES && globalSlotRetry(lowerer, sf, e)) { + retries++; + retriedIndex = nameIndex; + nameIndex--; + continue; + } // diagnostic already recorded; lowering the statement poisons // too — and in a JS file BOTH defer to the statement's // runtime fence (deferJsCollectionDiags). @@ -2687,6 +2747,20 @@ export function collectGlobals( } } +/** Localized failure's commit point for module globals (design §3.3): + * collectGlobals commits a global's type failure directly, before any body + * lowers, so it degrades here and the registration runs again. */ +function globalSlotRetry(lowerer: Lowerer, sf: ts.SourceFile, e: PoisonError): boolean { + if (isJsSourceFile(sf)) return false; + const slot = lowerer.degradeAtCommit(e, GLOBAL_SLOTS); + if (slot === null) return false; + lowerer.noteDynamicSlot(slot); + return true; +} + +const MAX_GLOBAL_RETRIES = 16; +const GLOBAL_SLOTS: ReadonlySet = new Set(["global"]); + /** Registers the entry-init note for a `var` module global: JS hoists * module vars to `undefined` at module entry, so an undefined-armed slot * must hold the interned undefined arm BEFORE the body runs (a function @@ -2714,6 +2788,7 @@ function noteVarGlobalEntryInit(lowerer: Lowerer, sf: ts.SourceFile, g: IrGlobal * plumbing (lowerVarDeclarator): no storage, the reads resolve through to * the module. JSON documents and native FFI requires keep their storage. */ function requireAliasDecl(lowerer: Lowerer, decl: ts.VariableDeclaration): boolean { + if (splitRequireBindingOf(lowerer, decl) !== null) return true; return ( decl.initializer !== undefined && requireSpecOf(decl.initializer) !== null && @@ -3002,6 +3077,10 @@ export function lowerFileInit( }); } if (sf === lowerer.entry) header.push(...lowerer.punycodeLinkWarning()); + // A module that reads builtin globals through the global object by + // computed names installs the program's lookup before its body runs. + const computedGlobalRead = computedGlobalReadOf(lowerer, sf); + if (computedGlobalRead) header.push(globalValuesInstall(lowerer, computedGlobalRead, loc0)); // Top-level `this` of a CommonJS module: its object exists before the // module's first statement (lowerCjsTopLevelThis). const exportsThis = lowerer.cjsExportsThis.get(sf); @@ -3178,7 +3257,19 @@ export function lowerFileInit( // by position) — exactly when JS evaluates decorators, static // initializers, and blocks, so code reading an earlier module // binding sees its assigned value. - const statics = [...lowerer.classes.values()] + const runsStatics = (c: ClassInfo): boolean => + lowerer.classInitFlags.has(c.def.name) || + c.callableBase !== undefined || + c.def.baseValueGlobal !== undefined || + c.runtimeStatics !== undefined || + c.staticFields.length > 0 || + (c.staticBlocks?.length ?? 0) > 0 || + c.classDecorators !== undefined; + const statics: { + pos: number; + info: ClassInfo | null; + deferred?: { symbol: ts.Symbol; decl: ts.ClassDeclaration }; + }[] = [...lowerer.classes.values()] .filter( (c) => // Class DECLARATIONS only: expression classes run their static @@ -3190,15 +3281,30 @@ export function lowerFileInit( ts.isClassDeclaration(c.decl) && !c.mixinInstance && ts.getSourceFileOfNode(c.decl) === sf && - (lowerer.classInitFlags.has(c.def.name) || - c.callableBase !== undefined || - c.def.baseValueGlobal !== undefined || - c.runtimeStatics !== undefined || - c.staticFields.length > 0 || - (c.staticBlocks?.length ?? 0) > 0 || - c.classDecorators !== undefined), + runsStatics(c), ) .map((c) => ({ pos: ts.tokenStartOfNode(c.decl!), info: c })); + // Localized failure: a deferred class with static field initializers + // is reached at its statement, where Node runs them. Its place retries + // it (retryDeferredAtDeclaration), and once it has registered there or + // through an earlier flush, its initializers lower in place. + for (const symbol of lowerer.deferredCollectors.keys()) + for (const decl of symbol.declarations) + if ( + ts.isClassDeclaration(decl) && + ts.getSourceFileOfNode(decl) === sf && + hasStaticFieldInitializer(decl) + ) + statics.push({ pos: ts.tokenStartOfNode(decl), info: null, deferred: { symbol, decl } }); + const lowerStaticsAt = (entry: (typeof statics)[number]): IrStmt[] => { + let info = entry.info; + if (entry.deferred) { + if (!lowerer.retryDeferredAtDeclaration(entry.deferred.symbol)) return []; + info = lowerer.classes.get(lowerer.classNamer(entry.deferred.decl)) ?? null; + if (info === null || !runsStatics(info)) return []; + } + return withClassInitFlag(lowerer, info!, lowerer.lowerStaticFieldInits(info!)); + }; // Statics-bearing MIXIN instantiations whose call evaluates in THIS // file: their declaration-time code runs when the call does — the // top-level statement lower-mixins recorded. Insertion order is @@ -3219,13 +3325,7 @@ export function lowerFileInit( // declarations never share a kept statement's start, so `<=` // changes nothing for them. while (at < statics.length && statics[at]!.pos <= ts.tokenStartOfNode(stmt)) { - body.push( - ...withClassInitFlag( - lowerer, - statics[at]!.info, - lowerer.lowerStaticFieldInits(statics[at]!.info), - ), - ); + body.push(...lowerStaticsAt(statics[at]!)); at++; } // A cycle binding's declaration stays at its position: its @@ -3244,13 +3344,7 @@ export function lowerFileInit( body.push(...lowerer.pendingClassExprInits.splice(0), ...stmtIr); } while (at < statics.length) { - body.push( - ...withClassInitFlag( - lowerer, - statics[at]!.info, - lowerer.lowerStaticFieldInits(statics[at]!.info), - ), - ); + body.push(...lowerStaticsAt(statics[at]!)); at++; } for (const { at, g } of unionEntryInits) { @@ -3529,6 +3623,9 @@ export function buildMain(lowerer: Lowerer): IrFunction { loc, }); } + // Classes with custom inspect methods register their compiled calls for + // the runtime's inspect walk before any module runs, in every thread. + body.unshift(...customInspectRegistrations(lowerer, loc)); // Node's startup refusal (a resolution the graph carries that Node // rejects — preflight's Node-order resolution walk — or a module-LINK // SyntaxError from either named-import checker): the graph is refused diff --git a/packages/compiler/src/frontend/lowering/lower-namespaces.ts b/packages/compiler/src/frontend/lowering/lower-namespaces.ts index ea844ab1e..4ea102769 100644 --- a/packages/compiler/src/frontend/lowering/lower-namespaces.ts +++ b/packages/compiler/src/frontend/lowering/lower-namespaces.ts @@ -42,7 +42,7 @@ import { isPackageShimFile, isShimIntrinsicName, packageShimFor } from "../packa import type { Lowerer } from "./lowerer.js"; import { boundIdentifiersOf } from "./lowerer.js"; import type { FileParts } from "./lower-modules.js"; -import { locOf, resolveImport } from "../program.js"; +import { isCjsJsFile, isJsSourceFile, locOf, resolveImport } from "../program.js"; import { F64, type IrExpr, type IrStmt, type IrType, STRING } from "../../ir/ir.js"; import { TEMPORAL_GLOBAL } from "../builtin-modules.js"; @@ -311,6 +311,30 @@ function moduleNsSourceFileOf(lowerer: Lowerer, e: ts.Expression): ts.SourceFile return null; } +/** The CommonJS module a JavaScript namespace import binding names + * (`import * as ns from "./m.cjs"`, `ns`), or null. */ +export function cjsNamespaceImportDep(lowerer: Lowerer, e: ts.Expression): ts.SourceFile | null { + if (!ts.isIdentifier(e)) return null; + const decl = lowerer.checker.getSymbolAtLocation(e)?.declarations.find(ts.isNamespaceImport); + const stmt = decl?.parent?.parent; + if ( + decl === undefined || + stmt === undefined || + !ts.isImportDeclaration(stmt) || + !ts.isStringLiteral(stmt.moduleSpecifier) + ) + return null; + const from = ts.getSourceFileOfNode(decl); + if (!isJsSourceFile(from)) return null; + const dep = resolveImport(lowerer.program, from, stmt.moduleSpecifier.text); + return dep !== null && + !dep.isDeclarationFile && + lowerer.fileTag.has(dep) && + isCjsJsFile(dep, lowerer.program) + ? dep + : null; +} + /** The member identifier of a qualified namespace reference, when `access` * reads a member declared in a lowered (or type-only) namespace block — * or an EXPORT of a program module read through its namespace-import @@ -330,6 +354,21 @@ export function nsMemberIdentOf( if (!ts.isIdentifier(access.name)) return null; const memberSym = lowerer.checker.getSymbolAtLocation(access.name); if (!memberSym) return null; + // A CommonJS module's namespace has the names Node's lexer detects, with + // the values they had when the module finished evaluating; the member + // then reads like the named import `import { name }`. `default` is + // module.exports itself. + const cjs = cjsNamespaceImportDep(lowerer, access.expression); + if (cjs !== null) { + const { names, assignedLater } = lowerer.cjsNamespaceOf(cjs); + const name = access.name.text; + return name !== "default" && + names.has(name) && + assignedLater !== "all" && + !assignedLater.has(name) + ? access.name + : null; + } if (nsBlockKindOfSymbol(lowerer, memberSym) !== null) return access.name; if (moduleNsSourceFileOf(lowerer, access.expression) !== null) return access.name; return null; @@ -637,6 +676,30 @@ export function ambientUndefReadType(lowerer: Lowerer, node: ts.Node): IrType | return null; } +/** The symbol of a variable a program file declares ambiently without + * an initializer (`declare const Bun: ...`), which nothing defines: Node + * erases the declaration, so a read throws the catchable ReferenceError + * " is not defined" and typeof answers "undefined". Null for other + * identifiers, including the standard library's declarations, which + * describe real runtime values. */ +export function ambientUndefinedVarSymbolOf( + lowerer: Lowerer, + ident: ts.Identifier, +): ts.Symbol | null { + const sym = lowerer.resolveValueSymbol(ident); + const decl = sym ? sym.declarations[0] : undefined; + if ( + sym === null || + decl === undefined || + lowerer.isStdlibSymbol(sym) || + !ts.isVariableDeclaration(decl) || + decl.initializer !== undefined || + !(ts.getCombinedModifierFlags(decl) & ts.ModifierFlags.Ambient) + ) + return null; + return sym; +} + /** The symbol behind `ident` when it names an ambient `declare function` * NOTHING defines: every function declaration on the symbol is body-less * and ambient in a USER program file (merge partners — the fundule's @@ -857,6 +920,15 @@ export function nsWritableTarget( const nsMember = nsMemberIdentOf(lowerer, access); if (!nsMember) return null; const memberSym = lowerer.checker.getSymbolAtLocation(nsMember); + // An export read through a module namespace object (`import * as ns`): + // the object is read-only, and Node's assignment throws a TypeError. + if (memberSym && nsBlockKindOfSymbol(lowerer, memberSym) === null) { + lowerer.unsupported( + "SC1090", + access, + `assignment to the module namespace member '${nsMember.text}' (module namespace objects are read-only: Node throws a TypeError)`, + ); + } if (memberSym) fenceEarlyNsMemberRef(lowerer, access, memberSym); const resolved = lowerer.resolveValueSymbol(nsMember); const g = resolved ? lowerer.globalsBySymbol.get(resolved) : undefined; diff --git a/packages/compiler/src/frontend/lowering/lower-node-island.ts b/packages/compiler/src/frontend/lowering/lower-node-island.ts index 080a6862c..fae8aa219 100644 --- a/packages/compiler/src/frontend/lowering/lower-node-island.ts +++ b/packages/compiler/src/frontend/lowering/lower-node-island.ts @@ -8,15 +8,21 @@ * Semantics, shared with packages/node/index.js (the Node implementation): * the arguments are copied with JSON.stringify of the argument array; the * module loads with require() from the calling module, so relative - * specifiers resolve from it; the result crosses as its JSON copy and is + * specifiers resolve from it. The specifier is a string literal: the build + * collects the module and what it loads into the executable + * (node-islands/modules.ts), and the call names its caller by its path in + * that tree, never by a path on the build machine. The result crosses as + * its JSON copy and is * checked against the declared result type, like `JSON.parse(s) as T`; a * thrown Error crosses as its nearest intrinsic class with its message and * string `code`. */ import { realpathSync } from "node:fs"; +import { isAbsolute } from "node:path"; import * as ts from "../ts7/adapter.js"; import { locOf } from "../program.js"; import type { NodeApiMember } from "../node-api-import.js"; -import { type Lowerer } from "./lowerer.js"; +import { PoisonError, type Lowerer } from "./lowerer.js"; +import { nodeIslandModuleDiag } from "../../diagnostics/diagnostic.js"; import { DYN, STRING, type IrExpr, type IrType } from "../../ir/ir.js"; const JSON_HINT = @@ -89,7 +95,26 @@ export function lowerNodeApiCall( "pass the module specifier, the export name and each argument explicitly", ); } - const specifier = lowerer.lowerExprExpecting(argNodes[0]!, STRING); + const specifierNode = argNodes[0]!; + if (!ts.isStringLiteral(specifierNode) && !ts.isNoSubstitutionTemplateLiteral(specifierNode)) { + lowerer.pushDiag( + nodeIslandModuleDiag(`${surface} with a computed module specifier`, locOf(specifierNode)), + ); + throw new PoisonError(); + } + const caller = callerPath(call); + const problem = lowerer.nodeIslandModules.add(caller, specifierNode.text); + if (problem !== null) { + lowerer.pushDiag(nodeIslandModuleDiag(`${surface}: ${problem}`, locOf(specifierNode))); + throw new PoisonError(); + } + const specifier = lowerer.lowerExprExpecting(specifierNode, STRING); + // An absolute specifier names a file of the build machine: the call names + // the shipped copy, relative to the caller inside the tree. + if (isAbsolute(specifierNode.text) && specifier.kind === "strLit") { + const target = lowerer.nodeIslandModules.resolvedPath(caller, specifierNode.text); + if (target !== null) lowerer.nodeIslandSpecifiers.push({ literal: specifier, caller, target }); + } const name = lowerer.lowerExprExpecting(argNodes[1]!, STRING); const values = argNodes.slice(2).map((arg) => { const value = lowerer.lowerExpr(arg); @@ -109,15 +134,18 @@ export function lowerNodeApiCall( lowerer.noLowering(`'${lowerer.fmt(bad)}' results of ${surface}`, call, JSON_HINT); } lowerer.usesNodeIsland = true; + // The caller's tree path is known once every call is collected (Lowerer). + const callerLiteral: IrExpr & { kind: "strLit" } = { + kind: "strLit", + value: caller, + type: STRING, + loc, + }; + lowerer.nodeIslandCallers.push({ literal: callerLiteral, caller }); const raw: IrExpr = { kind: "libCall", fn: member === "call" ? "nodeIsland.call" : "nodeIsland.callSync", - args: [ - { kind: "strLit", value: callerPath(call), type: STRING, loc }, - specifier, - name, - { kind: "dynArrLit", elems: values, type: DYN, loc }, - ], + args: [callerLiteral, specifier, name, { kind: "dynArrLit", elems: values, type: DYN, loc }], type: member === "call" ? { kind: "promise", inner: DYN } : DYN, loc, }; diff --git a/packages/compiler/src/frontend/lowering/lower-server.ts b/packages/compiler/src/frontend/lowering/lower-server.ts index cb79accea..d3132d02f 100644 --- a/packages/compiler/src/frontend/lowering/lower-server.ts +++ b/packages/compiler/src/frontend/lowering/lower-server.ts @@ -1,4 +1,5 @@ import { keyedHelperName } from "./stable-names.js"; +import { lowerTlsCertificateCall } from "./builtins/x509.js"; import { dynUndefinedExpr, nodeThrowExpr, @@ -803,6 +804,19 @@ export function lowerNetModuleCall( `the supported form is ${bi.member}(port, host?, connectListener?)`, ); } + // connect(path[, connectListener]): a string first argument names a + // Unix socket, or a localhost port when it reads as a non-negative + // number (Node's isPipeName, decided at run time). + if (lowerer.mapTypeOf(lowerer.typeOf(args[0]!))?.kind === "string") { + return lowerNetConnectPath( + lowerer, + expr, + bi.member, + lowerer.lowerExpr(args[0]!), + true, + args[1], + ); + } const port = lowerer.lowerExprExpecting(args[0]!, F64); // The optional middle host: a 2-arg call's second argument is the // host when string-typed, the connect listener when func-typed. @@ -840,6 +854,47 @@ export function lowerNetModuleCall( ); } +/** net.connect / net.createConnection of a Unix socket path (a string + * argument or the `path` option) with an optional connect listener. Node + * ignores any other argument between them for a path, so only a listener + * may follow. */ +function lowerNetConnectPath( + lowerer: Lowerer, + expr: ts.CallExpression, + member: string, + path: IrExpr, + pipeNameRule: boolean, + listener: ts.Expression | undefined, +): IrExpr { + const loc = locOf(expr); + const rule: IrExpr = { kind: "boolLit", value: pipeNameRule, type: BOOL, loc }; + if (expr.arguments.nodes.length > 2) { + lowerer.noLowering( + `${member} of a path with ${expr.arguments.nodes.length} arguments`, + expr, + `the supported form is ${member}(path[, connectListener])`, + ); + } + if (listener === undefined) + return { kind: "libCall", fn: "net.connectPath", args: [path, rule], type: NETSOCKET_T, loc }; + const { cb } = lowerCallbackArg( + lowerer, + listener, + "connect listeners", + 0, + () => false, + "use ()", + [], + ); + return { + kind: "libCall", + fn: "net.connectPathCb", + args: [path, rule, cb], + type: NETSOCKET_T, + loc, + }; +} + /** True when `t` is the lookup-function shape the connect lowering * accepts: (hostname: string, options: unknown, callback) => void where * callback is (err: , addresses: { address: @@ -928,6 +983,29 @@ function lowerNetConnectOptions( } } } + // connect({ path }[, connectListener]): the Unix-socket form. + { + const keys = optsNode.properties.nodes.map((p) => + (ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) && + (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) + ? p.name.text + : null, + ); + if (keys.length === 1 && keys[0] === "path") { + const prop = optsNode.properties.nodes[0]!; + const path = ts.isPropertyAssignment(prop) + ? lowerer.lowerExpr(prop.initializer) + : lowerer.lowerShorthandValue(prop as ts.ShorthandPropertyAssignment); + if (path.type.kind !== "string") { + lowerer.noLowering( + `a ${member} 'path' option of '${lowerer.fmt(path.type)}' values`, + prop, + "the path is a string here", + ); + } + return lowerNetConnectPath(lowerer, expr, member, path, false, args[1]); + } + } let port: IrExpr | null = null; let host: IrExpr | null = null; let lookup: IrExpr | null = null; @@ -1765,12 +1843,82 @@ function lowerNetServerMethodCall( ); return { kind: "libCall", fn: "net.serverCloseCb", args: [receiver, cb], type: VOID, loc }; } + if (name === "addContext" || name === "setSecureContext") { + // tls.Server's SNI table and default context (scr_tls.c): addContext + // takes a SecureContext or createSecureContext's options; the runtime + // throws Node's errors and the fence on a non-TLS server. + requireStatementPosition(lowerer, call, `server.${name}(...)`); + const want = name === "addContext" ? 2 : 1; + if (args.length !== want || args.some(ts.isSpreadElement)) { + lowerer.noLowering( + `${name} with ${args.length} arguments`, + call, + name === "addContext" + ? "the form is addContext(servername, context)" + : "the form is setSecureContext(options)", + ); + } + const receiver = coerceToHandle(lowerer, access.expression, NETSERVER_T); + if (name === "setSecureContext") { + const options = lowerer.lowerExprExpecting(args[0]!, DYN); + return { + kind: "libCall", + fn: "tls.serverSetSecureContext", + args: [receiver, options], + type: VOID, + loc, + }; + } + const servername = lowerer.lowerExprExpecting(args[0]!, DYN); + const context = lowerer.lowerExpr(args[1]!); + if (context.type.kind === "secureCtx") { + return { + kind: "libCall", + fn: "tls.serverAddContextCtx", + args: [receiver, servername, context], + type: VOID, + loc, + }; + } + return { + kind: "libCall", + fn: "tls.serverAddContext", + args: [receiver, servername, lowerer.coerceInto(args[1]!, context, DYN)], + type: VOID, + loc, + }; + } if ((name === "on" || name === "once" || name === "addListener") && args.length === 2) { requireStatementPosition(lowerer, call, `server.${name}(...)`); const once = boolLit(name === "once", loc); const evT = lowerer.typeOf(args[0]!); const event = evT.isStringLiteralType() ? evT.value : null; const receiver = coerceToHandle(lowerer, access.expression, NETSERVER_T); + if (event === "tlsClientError") { + // A failed handshake on a tls or https server: (err, tlsSocket). + const listener = args[1]!; + const { cb } = lowerCallbackArgShared( + lowerer, + listener, + "tlsClientError listeners", + 2, + (p, i) => + i === 0 ? p.kind === "object" && p.className === "%Error" : p.kind === "netSocket", + "use (err, tlsSocket), (err) or ()", + { + dynTuple: [ERROR_T, NETSOCKET_T], + checkAllParams: true, + adapt: (c) => voidizedCallback(lowerer, c, locOf(listener)), + }, + ); + return { + kind: "libCall", + fn: "tls.serverOnClientError", + args: [receiver, cb, once], + type: VOID, + loc, + }; + } if (event === "connection" || event === "secureConnection") { // 'secureConnection' rides the same list: a TLS server's // 'connection' listeners are DEFERRED to handshake completion @@ -2246,8 +2394,8 @@ function lowerNetServerMethodCall( function socketEncodingReachable(lowerer: Lowerer): boolean { if (lowerer.socketEncodingReachable !== null) return lowerer.socketEncodingReachable; let reachable = false; - for (const file of lowerer.program.getSourceFiles()) { - if (file.isDeclarationFile || !file.text.includes("setEncoding")) continue; + for (const file of lowerer.program.getImplementationSourceFiles()) { + if (!file.text.includes("setEncoding")) continue; ts.walkPreorder(file, (node) => { if (reachable) return "skip"; if (!ts.isCallExpression(node)) return undefined; @@ -2278,8 +2426,8 @@ function socketEncodingReachable(lowerer: Lowerer): boolean { function unixListenReachable(lowerer: Lowerer): boolean { if (lowerer.unixListenReachable !== null) return lowerer.unixListenReachable; let reachable = false; - for (const file of lowerer.program.getSourceFiles()) { - if (file.isDeclarationFile || !file.text.includes("listen")) continue; + for (const file of lowerer.program.getImplementationSourceFiles()) { + if (!file.text.includes("listen")) continue; ts.walkPreorder(file, (node) => { if (reachable) return "skip"; if (!ts.isCallExpression(node) || node.arguments.nodes.length === 0) return undefined; @@ -2354,7 +2502,9 @@ function lowerNetSocketMethodCall( }; } if (name === "write" || name === "end") { - requireStatementPosition(lowerer, call, `socket.${name}(...)`); + // write() answers Node's backpressure boolean; end() stays a statement + if (name === "end") requireStatementPosition(lowerer, call, `socket.${name}(...)`); + const resultT = name === "write" ? BOOL : VOID; // write(chunk, encoding) — the two-argument encoding form: 'buffer' // beside a string chunk is Writable's encoding check (a synchronous // ERR_UNKNOWN_ENCODING TypeError, "Unknown encoding: buffer" — the @@ -2380,7 +2530,7 @@ function lowerNetSocketMethodCall( const socket = coerceToHandle(lowerer, access.expression, NETSOCKET_T); const chunk = lowerer.lowerExpr(args[0]!); const fn: IrLibFn = chunk.type.kind === "string" ? "net.sockWrite" : "net.sockWriteBytes"; - return { kind: "libCall", fn, args: [socket, chunk], type: VOID, loc }; + return { kind: "libCall", fn, args: [socket, chunk], type: BOOL, loc }; } } } @@ -2439,7 +2589,7 @@ function lowerNetSocketMethodCall( NARROW_DATA_HINT, ); } - main = { kind: "libCall", fn, args: [receiver(), data], type: VOID, loc }; + main = { kind: "libCall", fn, args: [receiver(), data], type: resultT, loc }; } const cb = lowerer.lowerExpr(last); let register: IrExpr; @@ -2460,8 +2610,24 @@ function lowerNetSocketMethodCall( loc, }; } - stmts.push({ kind: "exprStmt", expr: main, loc }); - return { kind: "seqExpr", stmts, result: register, type: VOID, loc }; + if (resultT.kind === "void") { + stmts.push({ kind: "exprStmt", expr: main, loc }); + return { kind: "seqExpr", stmts, result: register, type: VOID, loc }; + } + // write(data, cb): the data is written, the callback registered, and + // the write's answer is the value + const okLocal = lowerer.declareHiddenLocal("%sockWriteOk", BOOL); + stmts.push( + { kind: "varDecl", localId: okLocal.id, init: main, loc }, + { kind: "exprStmt", expr: register, loc }, + ); + return { + kind: "seqExpr", + stmts, + result: { kind: "varRef", localId: okLocal.id, type: BOOL, loc }, + type: BOOL, + loc, + }; } const maxArgs = name === "write" ? 1 : 1; const minArgs = name === "write" ? 1 : 0; @@ -2481,17 +2647,17 @@ function lowerNetSocketMethodCall( const data = lowerer.lowerExpr(args[0]!); if (data.type.kind === "string") { const fn: IrLibFn = name === "write" ? "net.sockWrite" : "net.sockEndStr"; - return { kind: "libCall", fn, args: [receiver, data], type: VOID, loc }; + return { kind: "libCall", fn, args: [receiver, data], type: resultT, loc }; } if (data.type.kind === "bytes" && data.type.elem === "u8") { const fn: IrLibFn = name === "write" ? "net.sockWriteBytes" : "net.sockEndBytes"; - return { kind: "libCall", fn, args: [receiver, data], type: VOID, loc }; + return { kind: "libCall", fn, args: [receiver, data], type: resultT, loc }; } if (data.type.kind === "dyn") { // An untyped JS payload (a helper's parameter): the runtime // dispatches STR/BYTES and throws Node's chunk TypeError otherwise. const fn: IrLibFn = name === "write" ? "net.sockWriteDyn" : "net.sockEndDyn"; - return { kind: "libCall", fn, args: [receiver, data], type: VOID, loc }; + return { kind: "libCall", fn, args: [receiver, data], type: resultT, loc }; } lowerer.noLowering( `${name} of '${lowerer.fmt(data.type)}' data`, @@ -2632,6 +2798,12 @@ function lowerNetSocketMethodCall( ? value : lowerer.coerceInto(call, value, declared); } + { + const certificate = lowerTlsCertificateCall(lowerer, call, name, () => + coerceToHandle(lowerer, access.expression, NETSOCKET_T), + ); + if (certificate) return certificate; + } if (name === "isSessionReused" && args.length === 0) { // TLSSocket.isSessionReused(): the client never offers a cached // session (no session resumption, a documented TLS divergence), so @@ -2867,6 +3039,7 @@ function lowerNetSocketMethodCall( event === "connect" || event === "ready" || event === "timeout" || + event === "drain" || event === "readable" ) { const { cb } = lowerCallbackArg( @@ -2885,11 +3058,13 @@ function lowerNetSocketMethodCall( ? "net.sockOnClose" : event === "timeout" ? "net.sockOnTimeout" - : event === "readable" - ? "net.sockOnReadable" - : event === "ready" - ? "net.sockOnReady" - : "net.sockOnConnect"; + : event === "drain" + ? "net.sockOnDrain" + : event === "readable" + ? "net.sockOnReadable" + : event === "ready" + ? "net.sockOnReady" + : "net.sockOnConnect"; return { kind: "libCall", fn, args: [receiver, cb, once], type: VOID, loc }; } if (event === "lookup") { @@ -3880,6 +4055,18 @@ export function lowerServerProperty( const receiver = coerceToHandle(lowerer, expr.expression, NETSOCKET_T); return { kind: "libCall", fn: "net.sockBytesWritten", args: [receiver], type: F64, loc }; } + if ( + recvKind === "netSocket" && + lowerer.isStdlibMember(expr) && + (expr.name.text === "writableLength" || expr.name.text === "writableNeedDrain") + ) { + // The write buffer's unsent bytes, and whether a write answered false + // and 'drain' has not fired yet. + const receiver = coerceToHandle(lowerer, expr.expression, NETSOCKET_T); + return expr.name.text === "writableLength" + ? { kind: "libCall", fn: "net.sockWritableLength", args: [receiver], type: F64, loc } + : { kind: "libCall", fn: "net.sockWritableNeedDrain", args: [receiver], type: BOOL, loc }; + } if (recvKind === "netSocket" && lowerer.isStdlibMember(expr) && expr.name.text === "readable") { // true until the read half is done (peer FIN / destroy). const receiver = coerceToHandle(lowerer, expr.expression, NETSOCKET_T); @@ -4941,19 +5128,12 @@ function lowerTlsServerOptions( } if (k !== "cert" && k !== "key") { const init = ts.isPropertyAssignment(prop) ? prop.initializer : null; - if ( - (k === "ca" || k === "rejectUnauthorized") && - (init === null || sideEffectFreeOptionValue(init)) - ) { - // Both are inert without requestCert (which fences): the server's - // `ca` verifies CLIENT certificates and rejectUnauthorized only - // gates that verification — Node-observably droppable, matching + if (k === "rejectUnauthorized" && (init === null || sideEffectFreeOptionValue(init))) { + // Without requestCert (a literal with requestCert or ca rides the + // runtime walk) rejectUnauthorized gates nothing — droppable, like // the runtime walk's split (effectful values keep the fence). continue; } - if (k === "requestCert" && (init === null || init.kind === ts.SyntaxKind.FalseKeyword)) { - continue; // Node's false default, spelled out - } fenceOrDropOptionKey( lowerer, prop, @@ -4964,8 +5144,6 @@ function lowerTlsServerOptions( { SNICallback: "SNICallback lowers on tls.createServer and http2.createSecureServer({ allowHTTP1: true, ... }) — serve one cert/key pair here", - requestCert: - "client-certificate handshakes are not modeled (SEMANTICS.md divergence 55) — requestCert: false is the lowered value", }, ); continue; // an undocumented key, dropped like Node drops it @@ -5063,12 +5241,19 @@ function tlsLiteralNeedsRuntimeWalk( ); } +/** The client-certificate options (requestCert, and ca, which also + * completes the server's own chain): the runtime walk implements them for + * tls.createServer and https.createServer, and the literal cert/key + * lowering does not. */ +const TLS_CLIENT_AUTH_OPTIONS: ReadonlySet = new Set(["requestCert", "ca"]); + /** tls.createServer options the runtime walk implements and the literal * cert/key lowering does not. */ const TLS_SERVER_RUNTIME_OPTIONS: ReadonlySet = new Set([ "ALPNProtocols", "minVersion", "maxVersion", + ...TLS_CLIENT_AUTH_OPTIONS, ]); function lowerTlsServerOptionsOrDyn( @@ -5092,7 +5277,7 @@ function lowerTlsServerOptionsOrDyn( lowerer.noLowering( `${what} with SNICallback and options the runtime walk reads`, sniOption, - "SNICallback lowers with cert and key; ALPNProtocols, minVersion, maxVersion and the validated options are not combined with it yet", + "SNICallback lowers with cert and key; ALPNProtocols, minVersion, maxVersion, requestCert, ca and the validated options are not combined with it yet", ); } } @@ -5124,14 +5309,14 @@ function lowerTlsServerOptionsOrDyn( * connect(options[, cb]) and connect(port[, host][, options][, cb]) with * a RUNTIME options record (the checked-dynamic JS lane's shape — a dyn * value, or a record that converts): the runtime walk implements - * port/host/socket/rejectUnauthorized/ca/servername/ALPNProtocols/minVersion/maxVersion/timeout and throws the catchable + * port/host/socket/rejectUnauthorized/ca/cert/key/servername/ALPNProtocols/minVersion/maxVersion/timeout and throws the catchable * fence for other documented members (the divergence-66 stance). The * callback (and 'secureConnect'/'connect' listeners) fires * post-handshake — Node's secureConnect timing. */ function lowerTlsConnectCall(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr { const args = expr.arguments.nodes; const FORMS_HINT = - "the supported forms are connect(options[, cb]) and connect(port[, host][, options][, cb]) — options implement port/host/socket/rejectUnauthorized/ca/servername/ALPNProtocols/minVersion/maxVersion/timeout"; + "the supported forms are connect(options[, cb]) and connect(port[, host][, options][, cb]) — options implement port/host/socket/rejectUnauthorized/ca/cert/key/servername/ALPNProtocols/minVersion/maxVersion/timeout"; if (args.length < 1 || args.length > 4 || args.some(ts.isSpreadElement)) { lowerer.noLowering(`tls.connect with ${args.length} arguments`, expr, FORMS_HINT); } @@ -5368,7 +5553,13 @@ function lowerTlsModuleCall( "the supported form is createSecureContext({ cert, key })", ); } - const opts = lowerTlsServerOptionsOrDyn(lowerer, args[0]!, "tls.createSecureContext"); + const opts = lowerTlsServerOptionsOrDyn( + lowerer, + args[0]!, + "tls.createSecureContext", + false, + TLS_CLIENT_AUTH_OPTIONS, + ); if (opts.dyn !== undefined) { return { kind: "libCall", @@ -5625,7 +5816,13 @@ function lowerHttpsModuleCall( "the supported form is createServer({ cert, key }, (req, res) => ...)", ); } - const opts = lowerTlsServerOptionsOrDyn(lowerer, args[0]!, "https.createServer", true); + const opts = lowerTlsServerOptionsOrDyn( + lowerer, + args[0]!, + "https.createServer", + true, + TLS_CLIENT_AUTH_OPTIONS, + ); if (args.length === 1) { // The handler-less form: 'request' listeners arrive later via // server.on("request", ...) — the createSecureServer emission's @@ -6281,7 +6478,9 @@ function runtimeRequestOptions( return ( (ts.isPropertyAssignment(prop) || ts.isShorthandPropertyAssignment(prop)) && (ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)) && - prop.name.text === "socketPath" + (prop.name.text === "socketPath" || + // a client certificate: the runtime request walk reads cert and key + (secureish && (prop.name.text === "cert" || prop.name.text === "key"))) ); }); if (!needsRuntime) return null; diff --git a/packages/compiler/src/frontend/lowering/lower-stdlib.ts b/packages/compiler/src/frontend/lowering/lower-stdlib.ts index 7e108fde3..fe748bb93 100644 --- a/packages/compiler/src/frontend/lowering/lower-stdlib.ts +++ b/packages/compiler/src/frontend/lowering/lower-stdlib.ts @@ -35,14 +35,7 @@ import { esmNamedImportLinkCrash, isCjsJsFile, locOf, npmPackageNameOf } from ". import { foldedStringKeyOf, lowerDynObjectLiteral } from "./expressions/object-literals.js"; import { noteEventListeners } from "./lower-event-target.js"; import { PoisonError, newFnCtx, own } from "./lowerer.js"; -import { - NODE24_FETCH_COMPAT_PROFILE, - STATIC_HEADERS_CALLS, - STATIC_READABLE_STREAM_CALLS, - STATIC_READABLE_STREAM_READS, - STATIC_RESPONSE_CALLS, - STATIC_RESPONSE_READS, -} from "../../compat/fetch-profile.js"; +import { node24FetchCompatProfile, staticFetchMembers } from "../../compat/fetch-profile.js"; import { classGeneratorIterable } from "./class-iterables.js"; import { generatorDrain } from "./iterator-adapters.js"; @@ -434,7 +427,7 @@ function fenceRequestInitObject( : contextual ? [contextual] : []; - const rows = NODE24_FETCH_COMPAT_PROFILE.inventory.entries.filter( + const rows = node24FetchCompatProfile().inventory.entries.filter( (entry) => entry.owner === "RequestInit" && entry.placement === "dictionary", ); const shadowed = new Set(); @@ -837,7 +830,7 @@ function fetchInventoryStatus( placement: "static" | "prototype" | "prototype-symbol", ): "static" | "unsupported" | "out-of-scope" | null { return ( - NODE24_FETCH_COMPAT_PROFILE.inventory.entries.find( + node24FetchCompatProfile().inventory.entries.find( (entry) => entry.owner === owner && entry.member === member && entry.placement === placement, )?.status ?? null ); @@ -848,7 +841,7 @@ function fetchInventoryReason( member: string, placement: "static" | "prototype" | "prototype-symbol", ): string | undefined { - return NODE24_FETCH_COMPAT_PROFILE.inventory.entries.find( + return node24FetchCompatProfile().inventory.entries.find( (entry) => entry.owner === owner && entry.member === member && entry.placement === placement, )?.reason; } @@ -951,8 +944,9 @@ export function fenceStaticResponseMember( member.startsWith("[Symbol.") ? "prototype-symbol" : "prototype", ); + const projected = staticFetchMembers(); const supported = (member: string) => - use === "call" ? STATIC_RESPONSE_CALLS.has(member) : STATIC_RESPONSE_READS.has(member); + use === "call" ? projected.responseCalls.has(member) : projected.responseReads.has(member); if (members.every(supported)) return null; const member = members.find((candidate) => !supported(candidate))!; if (status(member) === "unsupported") { @@ -993,12 +987,9 @@ export function fenceStaticHeadersMember( member.startsWith("[Symbol.") ? "prototype-symbol" : "prototype", ); - if (use === "call" && members.every((member) => STATIC_HEADERS_CALLS.has(member))) { - return null; - } - const member = members.find( - (candidate) => use !== "call" || !STATIC_HEADERS_CALLS.has(candidate), - )!; + const headersCalls = staticFetchMembers().headersCalls; + if (use === "call" && members.every((member) => headersCalls.has(member))) return null; + const member = members.find((candidate) => use !== "call" || !headersCalls.has(candidate))!; if (status(member) === "unsupported") { lowerer.noLowering( `Headers.${member}`, @@ -1108,7 +1099,7 @@ function fetchInterfaceInventoryOwner( const symbol = [type.getSymbol(), type.getAliasSymbol()].find( (candidate) => candidate !== undefined && - NODE24_FETCH_COMPAT_PROFILE.inventory.interfaces.includes(ts.symbolName(candidate)) && + node24FetchCompatProfile().inventory.interfaces.includes(ts.symbolName(candidate)) && lowerer.isStdlibSymbol(candidate), ); return symbol ? { owner: ts.symbolName(symbol), symbol } : null; @@ -1123,7 +1114,7 @@ function fenceFetchObjectMembers( if (!resolved) return; const { owner, symbol } = resolved; for (const { member, node } of members) { - const row = NODE24_FETCH_COMPAT_PROFILE.inventory.entries.find( + const row = node24FetchCompatProfile().inventory.entries.find( (entry) => entry.owner === owner && entry.member === member && @@ -1136,7 +1127,7 @@ function fenceFetchObjectMembers( // Static data properties retain the ordinary checked-dynamic keyed-read // lowering. Only a method extraction loses the receiver that its direct // call bridge would preserve; non-static rows keep their inventory fence. - const operation = NODE24_FETCH_COMPAT_PROFILE.operations.find( + const operation = node24FetchCompatProfile().operations.find( (candidate) => candidate.id === row.id, ); if (row.status === "static" && operation?.kind === "property") { @@ -1235,7 +1226,7 @@ export function lowerFetchElementMethodCall( return null; } const rows = members.map((member) => - NODE24_FETCH_COMPAT_PROFILE.inventory.entries.find( + node24FetchCompatProfile().inventory.entries.find( (entry) => entry.owner === resolved.owner && entry.member === member && @@ -1397,10 +1388,11 @@ export function fenceStaticReadableStreamMember( member.startsWith("[Symbol.") ? "prototype-symbol" : "prototype", ); + const projected = staticFetchMembers(); const supported = (member: string) => use === "call" - ? STATIC_READABLE_STREAM_CALLS.has(member) - : STATIC_READABLE_STREAM_READS.has(member); + ? projected.readableStreamCalls.has(member) + : projected.readableStreamReads.has(member); if (members.every(supported)) return null; const member = members.find((candidate) => !supported(candidate))!; if (status(member) === "unsupported") { @@ -1630,16 +1622,22 @@ export function lowerStaticAbortControllerCall( call: ts.CallExpression, ): IrExpr | null { const access = call.expression; + // An optional chain (`controller?.abort()`) lowers the call again once it + // has narrowed the receiver (Lowerer.chainHandled). if ( - call.questionDotToken || + (call.questionDotToken && !lowerer.chainHandled.has(call)) || call.arguments.nodes.some((argument) => ts.isSpreadElement(argument)) || (!ts.isPropertyAccessExpression(access) && !ts.isElementAccessExpression(access)) || - access.questionDotToken || + (access.questionDotToken && !lowerer.chainHandled.has(access)) || staticResponseMemberName(lowerer, access) !== "abort" ) { return null; } - const receiverTs = lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(access.expression)); + const receiverTs = lowerer.checker.getBaseTypeOfLiteralType( + access.questionDotToken + ? lowerer.checker.getNonNullableType(lowerer.typeOf(access.expression)) + : lowerer.typeOf(access.expression), + ); const symbol = receiverTs.getAliasSymbol() ?? receiverTs.getSymbol(); if ( symbol === undefined || @@ -1945,6 +1943,19 @@ export function lowerStaticReadableStreamNew( }; } +/** `new TransformStream()`: the identity transform, a native handle whose + * readable side is a native ReadableStream and whose writable side is the + * runtime's WritableStream. A transformer or queuing strategies have no + * native lowering yet and keep the SC2020 refusal. */ +export function lowerStaticTransformStreamNew( + lowerer: Lowerer, + expr: ts.NewExpression, +): IrExpr | null { + if (stdlibGlobalNameOf(lowerer, expr.expression) !== "TransformStream") return null; + if ((expr.arguments?.nodes.length ?? 0) > 0) return null; + return { kind: "libCall", fn: "fetch.transformNew", args: [], type: DYN, loc: locOf(expr) }; +} + /** Native Request and Headers values share the fetch runtime's handles. */ export function lowerFetchWebNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr | null { const name = stdlibGlobalNameOf(lowerer, expr.expression); @@ -2555,35 +2566,8 @@ export function lowerMathProperty( const member = lowerer.stdlibGlobalMember(expr, "Math"); if (member === null) return null; const loc = locOf(expr); - const staticValue = own(STATIC_MATH_PROPS, member); - if (staticValue !== undefined) { - return { kind: "numLit", value: staticValue, type: F64, loc }; - } - const native = own(STATIC_MATH_FNS, member); - if (native && (member === "min" || member === "max" || member === "hypot")) - return lowerer.lowerNativeCallableValue( - { - fn: member === "min" ? "math.minArr" : member === "max" ? "math.maxArr" : "math.hypotArr", - params: [arrayOf(F64)], - result: F64, - valueParams: [{ mode: "rest", type: F64 }], - }, - `Math.${member}`, - loc, - ); - if (native && member !== "min" && member !== "max" && member !== "hypot") { - const params = Array.from({ length: native.arity }, () => F64); - return lowerer.lowerNativeCallableValue( - { - fn: native.fn, - params, - result: F64, - valueParams: params.map((type) => ({ mode: "required", type })), - }, - `Math.${member}`, - loc, - ); - } + const value = mathMemberValue(lowerer, member, loc); + if (value !== null) return value; const propType = own(STDLIB_SURFACE.math.props, member); if (propType !== undefined) { lowerer.refuseUnsupportedApi(`'Math.${member}'`, expr); @@ -2597,6 +2581,40 @@ export function lowerMathProperty( return null; // declared-but-untabled members fall through generically } +/** The value of the Math member `member`: a constant, or the interned + * native function value of a Math function. Null for other members. */ +export function mathMemberValue(lowerer: Lowerer, member: string, loc: SrcLoc): IrExpr | null { + const staticValue = own(STATIC_MATH_PROPS, member); + if (staticValue !== undefined) { + return { kind: "numLit", value: staticValue, type: F64, loc }; + } + const native = own(STATIC_MATH_FNS, member); + if (!native) return null; + if (member === "min" || member === "max" || member === "hypot") + return lowerer.lowerNativeCallableValue( + { + fn: member === "min" ? "math.minArr" : member === "max" ? "math.maxArr" : "math.hypotArr", + params: [arrayOf(F64)], + result: F64, + valueParams: [{ mode: "rest", type: F64 }], + valueLength: 2, + }, + `Math.${member}`, + loc, + ); + const params = Array.from({ length: native.arity }, () => F64); + return lowerer.lowerNativeCallableValue( + { + fn: native.fn, + params, + result: F64, + valueParams: params.map((type) => ({ mode: "required", type })), + }, + `Math.${member}`, + loc, + ); +} + export function npmPackageOf(lowerer: Lowerer, type: ts.Type): string | null { const pkg = lowerer.npmPackageOfSymbol(type.getAliasSymbol() ?? type.getSymbol()); if (pkg) return pkg; diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 66ecea602..fc259bbd8 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -1,4 +1,12 @@ +import { jsGlobalValueByName } from "./global-values.js"; import { lowerTupleAliasDecl, stripTypeWrappers } from "./typed-arguments.js"; +import { + allBytesArms, + indexedUnionArms, + lowerIndexedUnionMember, + lowerIndexedUnionNumberRead, + lowerIndexedUnionRead, +} from "./indexed-unions.js"; import { declaredMemberOf, isPromisePrototypeRead } from "./expressions/property-access.js"; import { hostsAll } from "./expressions/hosted-narrowing.js"; import { requestGeneralStreamSurface } from "./general-streams.js"; @@ -29,15 +37,23 @@ import { lowerOptionalIndex } from "./undefined-index.js"; import { processDataMemberValue, programReadsExitCode } from "./builtins/process.js"; import { requireInitFlagStmt } from "./require-initialization.js"; import { isJsonRequireDecl } from "./json-require.js"; +import { isTimeValueDateDeclaration } from "./date-values.js"; import { bindingSource } from "../binding-source.js"; import { lowerConstructorReturn } from "./constructor-returns.js"; -import type { Lowerer } from "./lowerer.js"; +import type { DynamicSlot, Lowerer } from "./lowerer.js"; +import type { ScrDiagnostic, TypeFailureSlot } from "../../diagnostics/diagnostic.js"; +import { statementGroupKey } from "./type-failure.js"; import { cjsExportValueByName, cjsNamespaceDestructureOf, cjsTableMethodEntry, inlineProgramRequireOf, requireNestedPatternsOf, + splitRequireAssignmentOf, + splitRequireBindingOf, + splitRequireBindingOfIdent, + splitRequireEarlyReadOf, + wholeExportSymbolOfModule, } from "./cjs-inline-require.js"; import { cjsExportMemberWriteGlobalOf, @@ -64,6 +80,7 @@ import { BOOL, BYTES_U8, CAUGHT, + DATE_T, DYN, F64, type IrExpr, @@ -105,6 +122,7 @@ import { requireSpecOf, } from "../program.js"; import { isStrictCode } from "./strict-mode.js"; +import { markSloppyRecordWrites } from "./frozen-records.js"; import { COMPOUND_ASSIGN_OPS, type CompoundOp, @@ -153,6 +171,7 @@ import { import { bindingNeverReassigned, deadUnmappableBinding, + fileWritesBinding, nullishExprUnitOf, nullishGenericBindingUnitOf, } from "./binding-analysis.js"; @@ -166,7 +185,12 @@ import { classStaticDataFor } from "./class-static-data.js"; import { objectFactorySignature } from "./object-factory-new.js"; import { isClassCallback, lowerClassCallbackAssign } from "./class-callbacks.js"; import { classForInHasKey, classPropertiesHelper } from "./class-dynamic-dispatch.js"; -import { genericIfaceBindingKeepsClass, superConditionLead } from "./lower-classes.js"; +import { + genericIfaceBindingKeepsClass, + lowerClassStaticRead, + staticReceiverClass, + superConditionLead, +} from "./lower-classes.js"; import { lowerStreamUnderscoreAssign, streamClassAliasDecl, @@ -216,7 +240,6 @@ import { globalSymbolKey } from "./expressions/global-symbols.js"; import { identifiersNamed } from "./identifier-uses.js"; import { lowerShortCircuitAssignment } from "./expressions/nullish-assignment.js"; import { - builtinGlobalValue, jsHoldsDynObject, lowerEnvironmentKey, lowerNativeFunctionAssignment, @@ -245,6 +268,7 @@ import { constituentTypes } from "../ts7/checker.js"; import { npmStaticPackageOfPath } from "../npm-static.js"; import { fenceNodeModuleMutation } from "./lower-node-module.js"; import { fencePerfEntryKeyWalk } from "./builtins/performance.js"; +import { fenceX509KeyWalk } from "./builtins/x509.js"; import { classGeneratorIterable } from "./class-iterables.js"; import { iterableUnionOf, lowerForOfIterable } from "./iterable-values.js"; @@ -392,8 +416,8 @@ function cleanupCall( ): { call: IrExpr; async: boolean } | null { const loc = locOf(node); if (value.type.kind === "object") { - const info = lowerer.classes.get(value.type.className); - if (!info) lowerer.flushDeferredClass(value.type.className); + const info = + lowerer.classes.get(value.type.className) ?? lowerer.flushDeferredClass(value.type.className); const asyncFound = awaited && info ? lowerer.findMethodOn(info, "sym:asyncDispose") : null; const syncFound = info ? lowerer.findMethodOn(info, "sym:dispose") : null; const member = asyncFound ? "sym:asyncDispose" : syncFound ? "sym:dispose" : null; @@ -727,57 +751,38 @@ export function lowerStmts(lowerer: Lowerer, stmts: readonly ts.Statement[]): Ir if (!lowerer.suppressStats) lowerer.countStatement(stmt, sf?.fileName); const diagsBefore = lowerer.diags.length; const sinkMark = lowerer.diagSinkMark(); + const touched = new Set(); + lowerer.failureTouches.push(touched); + const pendingSlots: DynamicSlot[] = []; + // A declarator that initializes a forward-captured TDZ box takes it + // out of tdzPredeclared; a retry after a later declarator degrades + // must initialize the same box again. + const tdzBefore = new Map(lowerer.ctx.tdzPredeclared); try { - let lowered: IrStmt | IrStmt[] | null; - let appended = false; - if ( - ts.isVariableStatement(stmt) && - (stmt.declarationList.flags & ts.NodeFlags.Using) !== 0 - ) { - const registrations = lowerUsingStatement(lowerer, stmt); - const accounting: IrStmt[] = []; - for (const registration of registrations) { - out.push(...registration.acquire); - accounting.push(...registration.acquire, ...registration.cleanup); - if (registration.cleanup.length > 0) { - usingMarkers.push({ - start: out.length, - cleanup: registration.cleanup, - loc: registration.loc, - }); - } + for (let attempt = 0; ; attempt++) { + try { + lowerOneStatement(lowerer, stmt, out, usingMarkers, diagsBefore, deferFences); + break; + } catch (e) { + // Localized failure's statement commit point (design §3.3): a + // degradable type failure at a local, loop variable or module + // global degrades that type and the statement lowers again. + // A declaration this statement flushed inside a probe (where + // no retry runs) can retry here too. + if (attempt >= MAX_DEGRADE_RETRIES || deferFences) throw e; + const slot = lowerer.degradeAtCommit(e, STATEMENT_SLOTS); + if (slot === null && !lowerer.reopenFlushedAtCommit(diagsBefore)) throw e; + // A failed attempt adds nothing to `out` but the TDZ boxes + // forward captures declared (they stay declared), so the + // retry appends after them. + lowerer.dropAttemptDiags(diagsBefore); + if (slot !== null) pendingSlots.push(slot); + for (const [symbol, local] of tdzBefore) + if (!lowerer.ctx.tdzPredeclared.has(symbol)) + lowerer.ctx.tdzPredeclared.set(symbol, local); } - lowered = accounting; - appended = true; - } else { - lowered = lowerer.lowerStmt(stmt); - } - // The lib-boundary chokepoint (lib-boundary.ts): checked-dynamic - // arguments that reached builtin-call slots without the checked - // coercion get their dynCheck here, and the uncoercible ones - // fence — inside this statement's poison window, so a JS fence - // becomes the statement's runtimeFence exactly like every other - // deferred rejection. - if (lowered) { - const body: IrStmt[] = []; - if (Array.isArray(lowered)) body.push(...lowered); - else body.push(lowered); - const rewritten = lowerSurplusCalls(lowerer, body); - enforceLibBoundary(lowerer, rewritten); - if (!appended) out.push(...rewritten); - } - // A JS statement that lowered CLEAN can still have flushed - // DEFERRED collection diagnostics (a symbol lookup on an alias - // resolves through flushDeferred — informational here, not a - // blocker of THIS statement): those belong to the runtime-fence - // ledger, not the build — real uses of the broken declaration - // meet their own per-site fences. - if (deferFences && lowerer.diagSink === null && lowerer.diags.length > diagsBefore) { - const flushed = lowerer.diags.splice(diagsBefore); - const ice = flushed.filter((d) => d.code === "SC9001"); - lowerer.diags.push(...ice); - lowerer.runtimeFences.push(...flushed.filter((d) => d.code !== "SC9001")); } + for (const slot of pendingSlots) lowerer.noteDynamicSlot(slot); } catch (e) { // A Go panic inside tsgo, surfaced by the sync channel as a // thrown Error (server intact — the prefetch fence's finding): @@ -790,7 +795,17 @@ export function lowerStmts(lowerer: Lowerer, stmts: readonly ts.Statement[]): Ir throw e; } if (!lowerer.suppressStats) lowerer.countStatementFailed(stmt, sf?.fileName); - lowerer.noteBlockedBindings(stmt); + // Localized failure (report-only): the statement's committed + // diagnostics that are not themselves type failures or rejected + // declarations are cascades of the rejected declarations it used. + const committed = + lowerer.diagSink === null && !deferFences ? lowerer.diags.slice(diagsBefore) : []; + if (touched.size > 0) { + const cause = [...touched]; + for (const d of committed) + if (d.typeFailure === undefined && d.rejects === undefined) d.cause ??= cause; + } + lowerer.noteBlockedBindings(stmt, committed); // The runtime-fence conversion (JS sources, direct diagnostics // only — a capture wrapper in force means collection owns the // report). The statement's diagnostics move OFF the build and @@ -814,6 +829,8 @@ export function lowerStmts(lowerer: Lowerer, stmts: readonly ts.Statement[]): Ir const fence = lowerer.capturedRuntimeFence(sinkMark, stmt); if (fence) out.push(fence); } + } finally { + lowerer.failureTouches.pop(); } } // Each acquisition protects the remainder of its lexical statement @@ -839,6 +856,81 @@ export function lowerStmts(lowerer: Lowerer, stmts: readonly ts.Statement[]): Ir return out; } +/** Localized failure: retries a degradable statement-level type failure + * at most this many times (each retry degrades one more type). */ +const MAX_DEGRADE_RETRIES = 16; + +/** The slot kinds lowerStmts' commit point degrades: locals, loop + * variables and module globals (stage L1), and the parameters and returns + * of the function expressions, arrows and methods a statement holds + * (stage L2). */ +const STATEMENT_SLOTS: ReadonlySet = new Set([ + "local", + "global", + "loop", + "param", + "return", + "field", +]); + +/** One attempt at lowering a statement of lowerStmts into `out`. */ +function lowerOneStatement( + lowerer: Lowerer, + stmt: ts.Statement, + out: IrStmt[], + usingMarkers: UsingMarker[], + diagsBefore: number, + deferFences: boolean, +): void { + let lowered: IrStmt | IrStmt[] | null; + let appended = false; + if (ts.isVariableStatement(stmt) && (stmt.declarationList.flags & ts.NodeFlags.Using) !== 0) { + const registrations = lowerUsingStatement(lowerer, stmt); + const accounting: IrStmt[] = []; + for (const registration of registrations) { + out.push(...registration.acquire); + accounting.push(...registration.acquire, ...registration.cleanup); + if (registration.cleanup.length > 0) { + usingMarkers.push({ + start: out.length, + cleanup: registration.cleanup, + loc: registration.loc, + }); + } + } + lowered = accounting; + appended = true; + } else { + lowered = lowerer.lowerStmt(stmt); + } + // The lib-boundary chokepoint (lib-boundary.ts): checked-dynamic + // arguments that reached builtin-call slots without the checked + // coercion get their dynCheck here, and the uncoercible ones + // fence — inside this statement's poison window, so a JS fence + // becomes the statement's runtimeFence exactly like every other + // deferred rejection. + if (lowered) { + const body: IrStmt[] = []; + if (Array.isArray(lowered)) body.push(...lowered); + else body.push(lowered); + const rewritten = lowerSurplusCalls(lowerer, body); + enforceLibBoundary(lowerer, rewritten); + if (!appended) out.push(...rewritten); + } + // A JS statement that lowered CLEAN can still have flushed + // DEFERRED collection diagnostics (a symbol lookup on an alias + // resolves through flushDeferred — informational here, not a + // blocker of THIS statement): those belong to the runtime-fence + // ledger, not the build — real uses of the broken declaration + // meet their own per-site fences. + if (deferFences && lowerer.diagSink === null && lowerer.diags.length > diagsBefore) { + const flushed = lowerer.diags.splice(diagsBefore); + const ice = flushed.filter((d) => d.code === "SC9001"); + lowerer.diags.push(...ice); + lowerer.runtimeFences.push(...flushed.filter((d) => d.code !== "SC9001")); + } +} + /** Whether a statement list belongs to a function body (at any block * depth) rather than to a module's top level. */ function enclosedByFunction(stmt: ts.Statement): boolean { @@ -1279,6 +1371,7 @@ function jsDeclarationExpandsObject( inferred !== null && jsOpenObjectType(decl, inferred, lowerer.shapes, lowerer.unions).kind === "dyn") || arms.some((arm) => arm.kind === "record" && jsObjectBindingExpands(lowerer, decl, arm)) || + arms.some((arm) => arm.kind === "record" && jsObjectLiteralFilledLater(lowerer, decl, arm)) || (isJsSourceFile(ts.getSourceFileOfNode(decl)) && !decl.type && !hasJsTypeAnnotation(decl) && @@ -1499,7 +1592,11 @@ function noteEarlyReadVar(lowerer: Lowerer, symbol: ts.Symbol, local: IrLocal): * lowerVarDecl's salvage path declares the local when it can, and * resolution checks locals/globals before the fallthroughs consult * blockedBindings. */ -export function noteBlockedBindings(lowerer: Lowerer, stmt: ts.Statement): void { +export function noteBlockedBindings( + lowerer: Lowerer, + stmt: ts.Statement, + diags: readonly ScrDiagnostic[] = [], +): void { let list: ts.VariableDeclarationList | null = null; if (ts.isVariableStatement(stmt)) list = stmt.declarationList; else if ( @@ -1510,12 +1607,20 @@ export function noteBlockedBindings(lowerer: Lowerer, stmt: ts.Statement): void list = stmt.initializer; } if (!list) return; + // Localized failure (report-only): the bindings form one failure group, + // the statement, whose diagnostics are the group's. + const group = statementGroupKey(stmt); + let bound = false; for (const decl of list.declarations.nodes) { for (const nameNode of boundIdentifiersOf(decl.name)) { const symbol = lowerer.checker.getSymbolAtLocation(nameNode); - if (symbol) lowerer.blockedBindings.add(symbol); + if (!symbol) continue; + lowerer.blockedBindings.add(symbol); + if (!lowerer.rejectedGroups.has(symbol)) lowerer.rejectedGroups.set(symbol, group); + bound = true; } } + if (bound) for (const d of diags) d.rejects ??= group; } /** True when a reference/assignment fell through resolution because the @@ -1525,12 +1630,13 @@ export function noteBlockedBindings(lowerer: Lowerer, stmt: ts.Statement): void * flushed by the emit pass). */ export function isBlockedBinding(lowerer: Lowerer, symbol: ts.Symbol | null): boolean { if (!symbol) return false; - return ( + const blocked = lowerer.blockedBindings.has(symbol) || lowerer.deferredDiags.has(symbol) || lowerer.flushedSymbols.has(symbol) || - lowerer.alreadyFlushed.has(symbol) - ); + lowerer.alreadyFlushed.has(symbol); + if (blocked) lowerer.touchRejected(symbol); + return blocked; } /** Lowers a statement in a fresh lexical scope (if/while/for bodies). */ @@ -1609,8 +1715,47 @@ const THROWN_AS_DYN: ReadonlySet = new Set([ "bytes", "func", "regex", + "date", ]); +/** A `return` at the top level of a CommonJS module (preflight admits it + * there only): it ends the module's evaluation, and its value is + * evaluated and discarded, as Node's module wrapper does. Top-level + * bindings and export assignments after it would be left uninitialized + * where Node leaves them in their temporal dead zone or absent, so a + * module with any keeps a refusal. Null for a return inside a function. */ +function lowerModuleLevelReturn(lowerer: Lowerer, stmt: ts.ReturnStatement): IrStmt | null { + let top: ts.Node = stmt; + while (top.parent !== undefined && !ts.isSourceFile(top.parent)) { + if (ts.isFunctionLike(top.parent) || ts.isClassStaticBlockDeclaration(top.parent)) return null; + top = top.parent; + } + const sf = top.parent; + if (sf === undefined || !ts.isSourceFile(sf)) return null; + const stmts = sf.statements.nodes; + const later = stmts.slice(stmts.indexOf(top as ts.Statement) + 1); + if ( + later.some( + (s) => + ts.isVariableStatement(s) || ts.isClassDeclaration(s) || cjsExportAssignmentOf(s) !== null, + ) + ) { + lowerer.unsupported( + "SC1090", + stmt, + "a top-level return in a CommonJS module that declares variables or classes, or assigns exports, after it", + ); + } + const loc = locOf(stmt); + const ret: IrStmt = { kind: "return", value: null, loc }; + if (!stmt.expression) return ret; + return { + kind: "block", + body: [{ kind: "exprStmt", expr: lowerer.lowerExpr(stmt.expression), loc }, ret], + loc, + }; +} + export function lowerStmt(lowerer: Lowerer, stmt: ts.Statement): IrStmt | IrStmt[] | null { if (ownPrototypeResetStatement(lowerer, stmt)) return { kind: "block", body: [], loc: locOf(stmt) }; @@ -1720,6 +1865,8 @@ export function lowerStmt(lowerer: Lowerer, stmt: ts.Statement): IrStmt | IrStmt if (ts.isForOfStatement(stmt)) return lowerer.lowerForOf(stmt); if (ts.isLabeledStatement(stmt)) return lowerLabeled(lowerer, stmt); if (ts.isReturnStatement(stmt)) { + const moduleReturn = lowerModuleLevelReturn(lowerer, stmt); + if (moduleReturn) return moduleReturn; // Implicit-any instance RETURN INFERENCE (function-returns.ts // resolveInferredReturn): the value lowers BARE and the statement // records itself — the post-pass settles the instance's return type @@ -1813,10 +1960,17 @@ export function lowerStmt(lowerer: Lowerer, stmt: ts.Statement): IrStmt | IrStmt return { kind: "rethrow", localId: caughtLocal.id, loc: locOf(stmt) }; } // Exception-representable values move into the runtime's exception - // cell. `throw f()` of a void call has no value; Date has numeric - // storage internally, but cannot preserve its object kind through the - // untyped catch boundary. Reject both rather than changing JS identity. - let value = lowerer.lowerExpr(stmt.expression); + // cell. `throw f()` of a void call has no value. + // A thrown object or array literal has no declared type: it is built as + // a dynamic object, so the catch binding's checked casts adopt it + // (lowerer.untypedLiteralContext). + let literal: ts.Expression = stmt.expression; + while (ts.isParenthesizedExpression(literal)) literal = literal.expression; + let value = + (ts.isObjectLiteralExpression(literal) || ts.isArrayLiteralExpression(literal)) && + lowerer.untypedLiteralContext(literal) + ? lowerer.lowerExprExpecting(stmt.expression, DYN) + : lowerer.lowerExpr(stmt.expression); if (value.type.kind === "void") lowerer.badType(stmt.expression, lowerer.typeOf(stmt.expression)); // Preserve primitive reasons and readable record identity across the @@ -1827,19 +1981,12 @@ export function lowerStmt(lowerer: Lowerer, stmt: ts.Statement): IrStmt | IrStmt } else if (value.type.kind === "record" && lowerer.dynConvertible(value.type)) { value = { kind: "dynFrom", value, liveRef: true, type: DYN, loc: locOf(stmt.expression) }; } else if (THROWN_AS_DYN.has(value.type.kind) && lowerer.dynConvertible(value.type)) { - // Arrays, Maps, Sets, typed arrays, Buffers, functions, regexes and - // unions over them keep their identity and contents the same way: - // the catch binding, `typeof`, `instanceof`, `Array.isArray`, - // inspection and typed views read the thrown object. + // Arrays, Maps, Sets, typed arrays, Buffers, functions, regexes, + // Dates and unions over them keep their identity and contents the + // same way: the catch binding, `typeof`, `instanceof`, + // `Array.isArray`, inspection and typed views read the thrown object. value = lowerer.coerceInto(stmt.expression, value, DYN); } - if (value.type.kind === "date") { - lowerer.unsupported( - "SC1090", - stmt.expression, - "throwing Date values (catch bindings cannot preserve Date's object kind; throw an Error or primitive instead)", - ); - } // A dyn throw rides the REF cell arm (scr_throw_ref over the checked-dynamic tree // node — identity preserved through catch bindings, caughtToDyn // passes it back by reference). The JS-lane traced-call shape: @@ -1931,6 +2078,23 @@ function lowerVarDeclarator( isConst: boolean, isLet: boolean, ): IrStmt[] { + // `var m; m = require("./x")`: the declaration is alias plumbing; the + // assignment owns the module's run-once %init (cjs-inline-require.ts). + // A read that code before the assignment can reach keeps Node's hoisted + // `undefined` divergence visible at build time, like a var require + // declaration below runnable code. + const split = splitRequireBindingOf(lowerer, decl); + if (split !== null) { + const early = splitRequireEarlyReadOf(lowerer, split); + if (early !== null) { + lowerer.unsupported( + "SC1013", + early, + `var require() binding '${(decl.name as ts.Identifier).text}' below code that can reach it (Node reads undefined before the require runs, which is not modeled — assign the require above the code that can run first)`, + ); + } + return []; + } // The canonical ESM bridge to a compiled program module. Like a // top-level CommonJS require declaration, the binding is alias // plumbing while the dependency evaluates at this statement's @@ -2596,7 +2760,7 @@ export function lowerBindingPattern( lowerer.unsupported("SC1031", el, "rest elements over a union of array or tuple layouts"); const loc = locOf(el); let value = unionArrayValueRead(lowerer, srcRef(), numLit(i, loc), loc, (text) => - strCharsCall(lowerer, text, loc), + strCharsCall(text, loc), ); if (!value) lowerer.unsupported( @@ -2690,7 +2854,7 @@ export function lowerBindingPattern( const itRef = (): IrExpr => ({ kind: "varRef", localId: it.id, type: citPat.iterT, loc }); for (const el of pattern.elements.nodes) { const stepLoc = locOf(el); - const step = lowerer.classIteratorNextCall(citPat, itRef(), stepLoc); + const step = lowerer.classIteratorNextCall(citPat, itRef(), null, stepLoc); if (ts.isOmittedExpression(el) || el.name === undefined) { out.push({ kind: "exprStmt", expr: step, loc: stepLoc }); continue; @@ -2726,7 +2890,7 @@ export function lowerBindingPattern( } // STRING sources (`const [a, b] = str`): array destructuring walks // the STRING ITERATOR — code points, astral characters whole — - // which is Array.from(string)'s own machinery (%str.chars), so the + // which is Array.from(string)'s own machinery (str.codePoints), so the // source splits once into a hidden chars array and the pattern // lowers as an array pattern over string[]. Positions, holes, // defaults (with the bounds test), rest (the remaining code points @@ -2745,7 +2909,7 @@ export function lowerBindingPattern( out.push({ kind: "varDecl", localId: chars.id, - init: strCharsCall(lowerer, srcRef(), loc), + init: strCharsCall(srcRef(), loc), loc, }); lowerer.lowerBindingPattern( @@ -3021,6 +3185,18 @@ export function lowerBindingPattern( ); return; } + // A class in a JavaScript file, destructured through its own name + // (`const { ANY, parse } = Comparator`): each element is the static + // read `Comparator.ANY` — the static getter, field or method as Node + // reads it, with the element's default applied when the read answers + // undefined. Other class-valued sources keep the refusal below. + if ( + srcType.kind === "classval" && + ts.isObjectBindingPattern(pattern) && + isJsSourceFile(ts.getSourceFileOfNode(pattern)) && + lowerClassValuePattern(lowerer, pattern, isLet, out) + ) + return; if (srcType.kind === "dyn") { // A runtime computed first key evaluates after RequireObjectCoercible: // a nullish source throws before the key's side effects run. Later @@ -3138,8 +3314,8 @@ export function lowerBindingPattern( // chain declares keep named fences (a detached method would lose its // receiver without an error; JS binds the prototype function). if (srcType.kind === "object") { - const info = lowerer.classes.get(srcType.className); - if (!info) lowerer.flushDeferredClass(srcType.className); + const info = + lowerer.classes.get(srcType.className) ?? lowerer.flushDeferredClass(srcType.className); if (info) { for (const el of pattern.elements.nodes) { if (el.name === undefined) continue; @@ -4242,13 +4418,9 @@ function stdlibGlobalTokenDestructure( const canonical = ts.symbolName(member) === "global" ? "globalThis" : ts.symbolName(member); token = `[builtin ${canonical}]`; alias = canonical; - // Builtin constructors read as their interned function values, and - // crypto as the WebCrypto object: the same values the bare and - // `globalThis.X` spellings read. - value = - canonical === "crypto" - ? { kind: "libCall", fn: "webcrypto.object", args: [], type: DYN, loc: locOf(el) } - : builtinGlobalValue(canonical, locOf(el), true); + // The values the bare and `globalThis.X` spellings read + // (jsGlobalValueByName). + value = jsGlobalValueByName(lowerer, canonical, locOf(el)); } else if (globalName === "console" && !CONSOLE_CALL_MEMBERS.has(propName)) { token = `[builtin console.${propName}]`; } else if (TOKEN_OPAQUE_GLOBALS.has(globalName)) { @@ -4332,6 +4504,60 @@ function stdlibGlobalTokenDestructure( return out; } +/** `const { ANY, parse } = Comparator` in a JavaScript file, where the + * declaration's initializer names the class exactly (staticReceiverClass): + * one static read per element, left to right. False, with nothing + * emitted, for other sources (a parameter or nested pattern over a class + * value), which keep the class-value refusal. */ +function lowerClassValuePattern( + lowerer: Lowerer, + pattern: ts.ObjectBindingPattern, + isLet: boolean, + out: IrStmt[], +): boolean { + const decl = pattern.parent; + if (!ts.isVariableDeclaration(decl) || decl.initializer === undefined) return false; + let receiver: ts.Expression = decl.initializer; + while (ts.isParenthesizedExpression(receiver)) receiver = receiver.expression; + const info = staticReceiverClass(lowerer, receiver); + if (info === null) return false; + for (const el of pattern.elements.nodes) { + if (el.name === undefined) continue; + if (el.dotDotDotToken) + lowerer.unsupported( + "SC1031", + el, + "rest bindings over class values (the rest object copies the class's own enumerable statics)", + ); + const prop = el.propertyName ?? el.name; + const name = + ts.isIdentifier(prop) || + ts.isComputedPropertyName(prop) || + ts.isStringLiteralLike(prop) || + ts.isNumericLiteral(prop) + ? patternKeyNameOf(lowerer, prop as ts.PropertyName) + : null; + if (name === null) + lowerer.unsupported( + "SC1031", + el, + "destructuring with computed keys that do not fold to one property name", + ); + if (name === "prototype" || name.startsWith("#")) + lowerer.unsupported("SC1031", el, `destructuring '${name}' from a class value`); + let value = lowerClassStaticRead(lowerer, info, name, receiver, receiver); + if (value === null) + lowerer.unsupported( + "SC1031", + el, + `destructuring the property '${name}' the class '${info.def.jsName ?? info.def.name}' does not declare as a static`, + ); + if (el.initializer) value = applyBindingDefault(lowerer, el, value); + lowerer.bindPatternTarget(el.name, value, isLet, out); + } + return true; +} + /** `x = dflt` on a pattern position whose source value always EXISTS * (tuple positions, record fields): JS evaluates the default exactly * when the value is undefined — the undefined-arm ternary, lazy, so @@ -5283,6 +5509,12 @@ function inferredEvolvingLetType(lowerer: Lowerer, decl: ts.VariableDeclaration) const scope = decl.parent?.parent?.parent; if (symbol === undefined || (!ts.isBlock(scope) && !ts.isSourceFile(scope))) return null; + // A shorthand property's name (`{ width }`) resolves to the property + // symbol, but it reads the binding: the checker's shorthand-value symbol. + const refersToBinding = (node: ts.Identifier): boolean => + (ts.isShorthandPropertyAssignment(node.parent) && node.parent.name === node + ? lowerer.checker.getShorthandAssignmentValueSymbol(node.parent) + : lowerer.checker.getSymbolAtLocation(node)) === symbol; const isPlainAssignmentTarget = (node: ts.Identifier): boolean => { let target: ts.Expression = node; while (ts.isParenthesizedExpression(target.parent) && target.parent.expression === target) { @@ -5298,11 +5530,7 @@ function inferredEvolvingLetType(lowerer: Lowerer, decl: ts.VariableDeclaration) const captures = (fn: ts.Node): boolean => { let captured = false; ts.walkPreorder(fn, (node) => { - if ( - node !== fn && - ts.isIdentifier(node) && - lowerer.checker.getSymbolAtLocation(node) === symbol - ) { + if (node !== fn && ts.isIdentifier(node) && refersToBinding(node)) { captured = true; return "stop"; } @@ -5354,7 +5582,7 @@ function inferredEvolvingLetType(lowerer: Lowerer, decl: ts.VariableDeclaration) if ( ts.isIdentifier(node) && node !== decl.name && - lowerer.checker.getSymbolAtLocation(node) === symbol && + refersToBinding(node) && !isPlainAssignmentTarget(node) ) { reads.push(node); @@ -5414,6 +5642,21 @@ export function lowerVarDecl( if (alias) return alias; } + // A Date local whose identity no use observes stores its time value. + if (isTimeValueDateDeclaration(lowerer, decl)) { + const loc = locOf(decl); + const date = lowerer.lowerExprExpecting(decl.initializer!, DATE_T); + const local = lowerer.declareLocal( + decl.name, + decl.name.text, + F64, + isLet || lowerer.timeValueDateWrites.has(decl), + ); + lowerer.timeValueDateLocals.add(local); + const init: IrExpr = { kind: "libCall", fn: "date.getTime", args: [date], type: F64, loc }; + return { kind: "varDecl", localId: local.id, init, loc }; + } + // A NULLISH generic binding (`const i: I = null as any` — the // declared type has no mapping, the value is provably null/undefined // forever): no storage exists; reads know the value (member reads and @@ -6359,6 +6602,41 @@ function lowerVariableInitializer( } } +/** A JS object literal that leaves out a required field of the binding's + * inferred record: tsc types `var p = {}; p.id = id;` by the members later + * assignments add, so the object only gains them after its initializer, and + * until then it is not a value of that record. Keep it a checked-dynamic + * object, as an object that gains keys outside the record is. */ +function jsObjectLiteralFilledLater( + lowerer: Lowerer, + decl: ts.VariableDeclaration, + type: IrType & { kind: "record" }, +): boolean { + if (decl.type || !decl.initializer || !isJsSourceFile(ts.getSourceFileOfNode(decl))) return false; + let initializer = decl.initializer; + while (ts.isParenthesizedExpression(initializer)) initializer = initializer.expression; + if (!ts.isObjectLiteralExpression(initializer) || hasJsTypeAnnotation(decl)) return false; + const shape = lowerer.shapes.get(type.shapeId); + if (!shape || shape.tuple) return false; + const given = new Set(); + for (const property of initializer.properties.nodes) { + if (ts.isSpreadAssignment(property)) return false; + const name = property.name; + if (name === undefined) return false; + if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) + given.add(name.text); + else return false; + } + // A field whose type holds undefined reads as it would before the write. + const holdsUndefined = (t: IrType): boolean => + t.kind === "dyn" || + t.kind === "undefinedT" || + (t.kind === "union" && lowerer.armTag(t.unionId, UNDEFINED_T) >= 0); + return shape.fields.some( + (field) => !given.has(field.name) && !field.name.startsWith("%") && !holdsUndefined(field.type), + ); +} + /** A JS object literal's inferred fields do not close its later storage. * Keep the original native object when writes add keys or delete fields; * converting it to a fixed record would discard that behavior. */ @@ -6884,7 +7162,7 @@ function lowerDynSwitch( loc: locOf(expression), }; } - if (test.type.kind === "date" || !lowerer.dynConvertible(test.type)) { + if (!lowerer.dynConvertible(test.type)) { lowerer.unsupported( "SC1100", expression, @@ -7160,7 +7438,11 @@ function lowerDeleteStatement(lowerer: Lowerer, expr: ts.DeleteExpression): IrSt if (obj.type.kind === "record") { const shape = lowerer.shapes.get(obj.type.shapeId); if (shape?.indexValue && shape.fields.length === 0 && !shape.tuple) { - return { kind: "recordKeyDelete", obj, shapeId: obj.type.shapeId, key: lowerKey(), loc }; + return markSloppyRecordWrites( + lowerer, + { kind: "recordKeyDelete", obj, shapeId: obj.type.shapeId, key: lowerKey(), loc }, + expr, + ); } // `delete r.f` of a declared OPTIONAL field (undefined-armed slot) // writes the field's ABSENT state: a monomorphic shape cannot remove @@ -7181,14 +7463,19 @@ function lowerDeleteStatement(lowerer: Lowerer, expr: ts.DeleteExpression): IrSt if (field) { const absent = lowerer.absentFieldValue(field.type, loc); if (absent) { - return { - kind: "recordSet", - obj, - shapeId: obj.type.shapeId, - field: field.name, - value: absent, - loc, - }; + return markSloppyRecordWrites( + lowerer, + { + kind: "recordSet", + obj, + shapeId: obj.type.shapeId, + field: field.name, + value: absent, + loc, + }, + expr, + true, + ); } } if (shape?.indexValue) { @@ -7211,6 +7498,66 @@ function lowerDeleteStatement(lowerer: Lowerer, expr: ts.DeleteExpression): IrSt ); } +/** True when the `var`/`let` binding `nameNode` names cannot change after + * the top-level CommonJS export statement `exportNode` that exports it by + * reference, so importers read the value Node copies at the export: its + * module's top level declares it once, with an initializer that runs + * before the export, and nothing assigns it. The initializer runs earlier + * in the same module, or in a module the exporting module required before + * the export (`var hljs = require('./core'); module.exports = hljs;` over + * highlight.js's `var highlight = HLJS({}); module.exports = highlight;`). */ +function unchangedAfterExport( + lowerer: Lowerer, + nameNode: ts.Identifier, + exportNode: ts.Node, +): boolean { + const topLevelDeclaration = (symbol: ts.Symbol): ts.VariableDeclaration | null => { + const decl = symbol.declarations.length === 1 ? symbol.declarations[0]! : undefined; + return decl !== undefined && + ts.isVariableDeclaration(decl) && + ts.isIdentifier(decl.name) && + decl.initializer !== undefined && + ts.isVariableDeclarationList(decl.parent) && + ts.isVariableStatement(decl.parent.parent) && + ts.isSourceFile(decl.parent.parent.parent) + ? decl + : null; + }; + const exportFile = ts.getSourceFileOfNode(exportNode); + let exportStatement: ts.Node = exportNode; + for (let parent = exportNode.parent; parent !== undefined && !ts.isSourceFile(parent);) { + if (ts.isFunctionLike(parent) || ts.isClassDeclaration(parent) || ts.isClassExpression(parent)) + return false; + exportStatement = parent; + parent = parent.parent; + } + const runsBeforeExport = (decl: ts.VariableDeclaration): boolean => { + const statement = decl.parent?.parent; + return statement?.parent === exportFile && statement.end <= exportStatement.pos; + }; + const sym = lowerer.resolveValueSymbol(nameNode); + const decl = sym ? topLevelDeclaration(sym) : null; + if (!sym || !decl || !bindingNeverReassigned(lowerer, sym, decl)) return false; + if (ts.getSourceFileOfNode(decl) === exportFile) return runsBeforeExport(decl); + // A binding of another module: the exporting module's own binding + // `nameNode` names holds that module's require, evaluated before the + // export, and this file does not assign the binding either. + const local = lowerer.checker.getSymbolAtLocation(nameNode); + const localDecl = local ? topLevelDeclaration(local) : null; + if (!localDecl || !runsBeforeExport(localDecl)) return false; + let init: ts.Expression = localDecl.initializer!; + while (ts.isParenthesizedExpression(init)) init = init.expression; + if ( + !ts.isCallExpression(init) || + !ts.isIdentifier(init.expression) || + init.expression.text !== "require" || + init.arguments.nodes.length !== 1 || + !ts.isStringLiteralLike(init.arguments.nodes[0]!) + ) + return false; + return !fileWritesBinding(sym, exportFile, (ident) => lowerer.resolveValueSymbol(ident)); +} + /** A top-level CommonJS export statement (see cjsExportAssignmentOf): * `exports.f = ` and each expression-valued property of * `module.exports = { ... }` assign their pre-registered export globals @@ -7394,7 +7741,7 @@ function lowerCjsExportStatement( const fenceMutable = (nameNode: ts.Identifier): void => { const sym = lowerer.resolveValueSymbol(nameNode); const g = sym ? lowerer.globalsBySymbol.get(sym) : undefined; - if (g?.mutable) { + if (g?.mutable && !unchangedAfterExport(lowerer, nameNode, cjs.expr)) { lowerer.unsupported( "SC1090", nameNode, @@ -7515,6 +7862,26 @@ function lowerCjsSingleValueExport( return lowerer.requireInitStmt(req.spec, req.call) ?? { kind: "block", body: [], loc }; } if (ts.isIdentifier(rhs)) { + const split = splitRequireBindingOfIdent(lowerer, rhs); + if (split !== null) { + if (wholeExportSymbolOfModule(lowerer, split.dep) === null) { + lowerer.unsupported( + "SC1090", + rhs, + `the exports object of '${split.spec}' as a value (its exports compile to a static table: read require(${JSON.stringify(split.spec)}).member directly)`, + ); + } + const root = lowerer.globalsByDeclNode.get(expr); + if (root !== undefined) { + return { + kind: "assign", + localId: root.id, + value: lowerer.lowerExprExpecting(rhs, root.type), + loc: locOf(rhs), + }; + } + return { kind: "block", body: [], loc }; + } const sym = lowerer.resolveValueSymbol(rhs); if (!sym) return null; if ( @@ -7534,7 +7901,7 @@ function lowerCjsSingleValueExport( } const g = lowerer.globalsBySymbol.get(sym); if (g) { - if (g.mutable) { + if (g.mutable && !unchangedAfterExport(lowerer, rhs, expr)) { lowerer.unsupported( "SC1090", rhs, @@ -8425,6 +8792,16 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm if (!ts.isIdentifier(expr.left)) { lowerer.unsupported("SC1090", expr.left, "assignment to non-variables"); } + const splitAssign = splitRequireAssignmentOf(lowerer, expr); + if (splitAssign !== null) { + return ( + lowerer.requireInitStmt(splitAssign.spec, splitAssign.call) ?? { + kind: "block", + body: [], + loc: locOf(expr), + } + ); + } const target = lowerer.resolveWritable(expr.left); if (!target) lowerer.rejectUnresolved( @@ -8739,13 +9116,20 @@ function destrSpellingOf(rhs: ts.Expression): string | null { return base === null ? null : `${base}.${e.name.text}`; } // `table[key]` and `list[0]`; V8 prints a numeric key in its canonical - // number form (`o[1.50]` reads "o[1.5]"). + // number form (`o[1.50]` reads "o[1.5]"), and a string key as a property + // name with its text decoded (`o["a.b"]` and `o[\`a.b\`]` read "o.a.b"). + // The message is a C string, so a key holding NUL keeps the refusal. if (ts.isElementAccessExpression(e) && !e.questionDotToken) { const base = destrSpellingOf(e.expression); const key = e.argumentExpression; if (base === null) return null; if (ts.isIdentifier(key)) return `${base}[${key.text}]`; if (ts.isNumericLiteral(key)) return `${base}[${String(Number(key.text))}]`; + if ( + (ts.isStringLiteral(key) || ts.isNoSubstitutionTemplateLiteral(key)) && + !key.text.includes("\0") + ) + return `${base}.${key.text}`; return null; } if (ts.isCallExpression(e) && !e.questionDotToken) { @@ -9566,7 +9950,7 @@ function lowerArrayAssignInto( let srcType = tmpRef().type; // A STRING source (`[a, b] = s`): the declaration path's rule in // assignment position — the string iterator's code-point split - // (%str.chars, astral characters whole) into a hidden chars array, + // (str.codePoints, astral characters whole) into a hidden chars array, // then the pattern proceeds as an array pattern over string[] // (defaults with the bounds test, rest packing the remaining code // points, elisions; positions past the last code point inherit the @@ -9579,7 +9963,7 @@ function lowerArrayAssignInto( out.push({ kind: "varDecl", localId: chars.id, - init: strCharsCall(lowerer, tmpRef(), loc), + init: strCharsCall(tmpRef(), loc), loc, }); tmpRef = () => ({ kind: "varRef", localId: chars.id, type: charsT, loc }); @@ -9917,8 +10301,23 @@ export function lowerForOf(lowerer: Lowerer, stmt: ts.ForOfStatement): IrStmt { // iterator are awaited as the async-from-sync iterator awaits them, and // a value that is not iterable throws Node's TypeError. const headT = lowerer.mapTypeOf(lowerer.typeOf(stmt.expression)); + // A class instance whose [Symbol.asyncIterator]() (or + // [Symbol.iterator]()) returns an iterator object takes the same + // checked-dynamic protocol over the boxed instance. + const headClass = headT?.kind === "object" ? lowerer.classes.get(headT.className) : undefined; + const classAsync = + headClass !== undefined && lowerer.findMethodOn(headClass, "sym:asyncIterator") !== null; + // Without [Symbol.asyncIterator], the loop opens [Symbol.iterator]() and + // awaits its values, as the async-from-sync iterator does. + const classSync = + headClass !== undefined && + !classAsync && + lowerer.findMethodOn(headClass, "sym:iterator") !== null; + const classIterable = classAsync || classSync; const syncGenerator = - (headT?.kind === "generator" && headT.async !== true) || isJsSourceFile(stmt.getSourceFile()); + (headT?.kind === "generator" && headT.async !== true) || + classIterable || + isJsSourceFile(stmt.getSourceFile()); if (lowerer.ctx.isAsync && (syncGenerator || isUntypedIterable(lowerer, stmt.expression))) { const iterable = lowerer.lowerExpr(stmt.expression); if (iterable.type.kind === "generator" && iterable.type.async) @@ -9930,10 +10329,19 @@ export function lowerForOf(lowerer: Lowerer, stmt: ts.ForOfStatement): IrStmt { ); if (iterable.type.kind === "dyn") return lowerForOfDyn(lowerer, stmt, iterable, labels, true); if ( - (iterable.type.kind === "generator" || isJsSourceFile(stmt.getSourceFile())) && + (iterable.type.kind === "generator" || + classIterable || + isJsSourceFile(stmt.getSourceFile())) && lowerer.dynConvertible(iterable.type) ) - return lowerForOfDyn(lowerer, stmt, lowerer.coerceToExpected(iterable, DYN), labels, true); + return lowerForOfDyn( + lowerer, + stmt, + lowerer.coerceToExpected(iterable, DYN), + labels, + true, + classSync && iterable.type.kind === "object", + ); } // A NodeJS.ReadableStream iterates as the general Readable. requestGeneralStreamSurface(lowerer, stmt.expression); @@ -10132,13 +10540,8 @@ export function lowerForOf(lowerer: Lowerer, stmt: ts.ForOfStatement): IrStmt { loc: locOf(iterSrc), }; } - if (iterable.type.kind === "bytes") { - return lowerForOfBytes( - lowerer, - stmt, - iterable as IrExpr & { type: IrType & { kind: "bytes" } }, - labels, - ); + if (iterable.type.kind === "bytes" || indexedUnionArms(lowerer, iterable.type)) { + return lowerForOfBytes(lowerer, stmt, iterable, labels); } if (iterable.type.kind !== "array") { // The lib types strings as iterable too, so those for-ofs typecheck; @@ -10186,17 +10589,30 @@ export function lowerForOf(lowerer: Lowerer, stmt: ts.ForOfStatement): IrStmt { ); // Class iterables ([Symbol.iterator]() + next() — classIteratorOf) // drive the same protocol with ordinary method calls. - const cit = lowerer.classIteratorOf(iterable.type); + const cit = lowerer.classIteratorOf(iterable.type, { records: true, close: true }); if (cit) return lowerForOfClassIterator(lowerer, stmt, iterable, cit, labels); // A declared [Symbol.iterator] whose shape classIteratorOf refused // (an `any`-typed iterator or result, a declared return()/throw()): // name the protocol, not the class. const info = lowerer.classes.get(iterable.type.className); + // In JavaScript the inferred shape is often a union of result + // objects: the loop runs the iterator protocol over the boxed value. + if ( + info && + isJsSourceFile(ts.getSourceFileOfNode(stmt)) && + lowerer.findMethodOn(info, "sym:iterator") + ) + return lowerForOfDyn( + lowerer, + stmt, + lowerer.coerceInto(stmt.expression, iterable, DYN), + labels, + ); if (info && lowerer.findMethodOn(info, "sym:iterator")) { lowerer.unsupported( "SC1090", stmt.expression, - `for-of over this [Symbol.iterator] shape (the method must take no parameters and return an object whose zero-parameter next() returns a { value, done? } record with a boolean done; iterator classes declaring return()/throw() stay out — IteratorClose has no lowering)`, + `for-of over this [Symbol.iterator] shape (the method must take no parameters and return an object whose zero-parameter next() returns a { value, done? } record with a boolean done, and whose return(), if any, takes no parameters and returns an object)`, ); } } @@ -10696,7 +11112,7 @@ export function lowerForOf(lowerer: Lowerer, stmt: ts.ForOfStatement): IrStmt { function lowerForOfBytes( lowerer: Lowerer, stmt: ts.ForOfStatement, - iterable: IrExpr & { type: IrType & { kind: "bytes" } }, + iterable: IrExpr, labels?: string[], ): IrStmt { const loc = locOf(stmt); @@ -10790,7 +11206,16 @@ function lowerForOfStoredNumericIterator( const loc = locOf(stmt); const sourceT = state.sourceType; - if (!((sourceT.kind === "array" && sourceT.elem.kind === "f64") || sourceT.kind === "bytes")) { + // A union of typed arrays and number arrays (indexed-unions.ts) reads + // through its arms' own length and element reads. + const unionArms = indexedUnionArms(lowerer, sourceT); + if ( + !( + (sourceT.kind === "array" && sourceT.elem.kind === "f64") || + sourceT.kind === "bytes" || + unionArms + ) + ) { throw new InternalCompilerError("internal: retained numeric iterator has a non-numeric source"); } @@ -10798,35 +11223,44 @@ function lowerForOfStoredNumericIterator( const doneRef = (): IrExpr => varRef(state.doneLocalId, BOOL, loc); const sourceRef = (): IrExpr => varRef(state.sourceLocalId, sourceT, loc); const sourceLength = (): IrExpr => - sourceT.kind === "array" - ? { - kind: "arrIntrinsic", - method: "length", - receiver: sourceRef(), - args: [], - type: F64, - loc, - } - : { - kind: "bytesIntrinsic", - method: "length", - receiver: sourceRef(), - args: [], - type: F64, - loc, - }; - const valueT = sourceT.kind === "array" ? arrayValueType(lowerer, F64) : F64; + unionArms + ? lowerIndexedUnionMember(lowerer, sourceRef(), unionArms, "length", loc)! + : sourceT.kind === "array" + ? { + kind: "arrIntrinsic", + method: "length", + receiver: sourceRef(), + args: [], + type: F64, + loc, + } + : { + kind: "bytesIntrinsic", + method: "length", + receiver: sourceRef(), + args: [], + type: F64, + loc, + }; + const valueT = + sourceT.kind === "array" || (unionArms && !allBytesArms(unionArms)) + ? arrayValueType(lowerer, F64) + : F64; const sourceValue = (): IrExpr => - sourceT.kind === "array" - ? arrayValueRead(lowerer, sourceRef(), indexRef(), F64, loc) - : { - kind: "bytesIntrinsic", - method: "get", - receiver: sourceRef(), - args: [indexRef()], - type: F64, - loc, - }; + unionArms + ? allBytesArms(unionArms) + ? lowerIndexedUnionNumberRead(lowerer, sourceRef(), unionArms, indexRef(), loc) + : lowerIndexedUnionRead(lowerer, sourceRef(), unionArms, indexRef(), loc) + : sourceT.kind === "array" + ? arrayValueRead(lowerer, sourceRef(), indexRef(), F64, loc) + : { + kind: "bytesIntrinsic", + method: "get", + receiver: sourceRef(), + args: [indexRef()], + type: F64, + loc, + }; const one: IrExpr = { kind: "numLit", value: 1, type: F64, loc }; lowerer.scopes.push(new Map()); @@ -11073,16 +11507,18 @@ function lowerForOfMatchAll( /** `for (const ch of s)` over a STRING: JS's string iterator, which walks * code POINTS — each pass binds the whole character (a two-unit string * for astral chars, where charAt/slice would return one surrogate half). - * Desugars over the UTF-16 cursor machinery: a hidden unit - * index reads the character with the cpAt intrinsic and advances by ITS - * length before the body runs (so `continue` never skips the advance): + * Desugars over a byte cursor into the WTF-8 data: a hidden byte offset + * reads the character that starts there with the cpAtByte intrinsic and + * advances by ITS byte count before the body runs (so `continue` never + * skips the advance): * * { const %s = ; let %i = 0; - * while (%i < %s.length) { const ch = cpAt(%s, %i); %i += ch.length; } } + * while (%i < byteLength(%s)) { const ch = cpAtByte(%s, %i); %i += byteLength(ch); } } * - * Strings are immutable, so the length is stable and there is no + * Strings are immutable, so the byte count is stable and there is no * iterator state to unwind on throw; a lone surrogate is a one-unit - * character, so every step lands on a character start. */ + * character with its own three-byte encoding, so every step lands on a + * character start. */ /** for-of over a CLASS ITERABLE (classIteratorOf's shape) — the * iterator protocol as ordinary method calls: * @@ -11095,11 +11531,13 @@ function lowerForOfMatchAll( * } } * * A result record with NO done field never terminates — exactly JS - * (undefined is falsy forever; Node loops forever too). No - * IteratorClose: classIteratorOf already refused iterator classes - * declaring return()/throw(), so a break exits with nothing to close. - * `done: true` results bind nothing (the loop breaks before the value - * read), matching JS's ignore-the-final-value rule. */ + * (undefined is falsy forever; Node loops forever too). An iterator with + * return() is closed when the loop leaves after a value arrived (break, + * return, or a throw from the body), as IteratorClose does: a done flag + * set around each step guards the call in a finally, and after a throw + * from the body an error from return() is dropped. A throw from next() + * does not close. `done: true` results bind nothing (the loop breaks + * before the value read), matching JS's ignore-the-final-value rule. */ function lowerForOfClassIterator( lowerer: Lowerer, stmt: ts.ForOfStatement, @@ -11141,7 +11579,22 @@ function lowerForOfClassIterator( const r = lowerer.declareHiddenLocal("%cir", cit.resultT); const recvRef: IrExpr = { kind: "varRef", localId: recv.id, type: iterable.type, loc }; const itRef = (): IrExpr => ({ kind: "varRef", localId: it.id, type: cit.iterT, loc }); + const nextRead = lowerer.classIteratorNextRead(cit, itRef(), loc); + const next = nextRead ? lowerer.declareHiddenLocal("%cin", nextRead.type) : null; const rRef = (): IrExpr => ({ kind: "varRef", localId: r.id, type: cit.resultT, loc }); + const done = cit.hasReturn ? lowerer.declareHiddenLocal("%cid", BOOL) : null; + if (done) done.mutable = true; + const setDone = (value: boolean): IrStmt[] => + done + ? [ + { + kind: "assign", + localId: done.id, + value: { kind: "boolLit", value, type: BOOL, loc }, + loc, + }, + ] + : []; const varTarget = exprTarget ?? (decl ? forOfVarTarget(lowerer, decl) : null); const x = varTarget ? lowerer.declareHiddenLocal("%vof", cit.valueT) @@ -11154,10 +11607,16 @@ function lowerForOfClassIterator( const xRef: IrExpr = { kind: "varRef", localId: x.id, type: cit.valueT, loc }; const blame: ts.Node = decl?.name ?? exprTargetNode ?? stmt.initializer; const head: IrStmt[] = [ + ...setDone(true), { kind: "varDecl", localId: r.id, - init: lowerer.classIteratorNextCall(cit, itRef(), loc), + init: lowerer.classIteratorNextCall( + cit, + itRef(), + next ? varRef(next.id, next.type, loc) : null, + loc, + ), loc, }, ...(cit.hasDone @@ -11201,8 +11660,16 @@ function lowerForOfClassIterator( } satisfies IrStmt, ] : []), + ...setDone(false), ]; const body = lowerer.inCtl("loop", () => lowerer.lowerScopedBlock(stmt.statement), labels); + const loop: IrStmt = { + kind: "while", + cond: { kind: "boolLit", value: true, type: BOOL, loc }, + body: [...head, ...body], + ...(labels && { labels }), + loc, + }; return { kind: "block", body: [ @@ -11213,13 +11680,48 @@ function lowerForOfClassIterator( init: lowerer.classIteratorOpenCall(cit, recvRef, loc), loc, }, - { - kind: "while", - cond: { kind: "boolLit", value: true, type: BOOL, loc }, - body: [...head, ...body], - ...(labels && { labels }), - loc, - }, + ...(next && nextRead + ? [{ kind: "varDecl", localId: next.id, init: nextRead, loc } satisfies IrStmt] + : []), + ...(done + ? [ + { + kind: "varDecl", + localId: done.id, + init: { kind: "boolLit", value: true, type: BOOL, loc }, + loc, + } satisfies IrStmt, + { + kind: "tryCatch", + tryBody: [loop], + catchBody: null, + catchLocalId: null, + dropFinallyErrors: true, + finallyBody: [ + { + kind: "if", + cond: { + kind: "unary", + op: "!", + operand: { kind: "varRef", localId: done.id, type: BOOL, loc }, + type: BOOL, + loc, + }, + then: [ + { + kind: "exprStmt", + expr: lowerer.classIteratorReturnCall(cit, itRef(), loc), + loc, + }, + ], + else_: null, + loc, + }, + ], + loc, + } satisfies IrStmt, + ] + : [loop]), ], loc, }; @@ -11237,6 +11739,7 @@ function lowerForOfDyn( iterable: IrExpr, labels?: string[], asyncLoop = false, + syncOpen = false, ): IrStmt { const loc = locOf(stmt); const spell = notIterableSpelling(stmt.expression, asyncLoop ? "async iterable" : "iterable"); @@ -11247,6 +11750,7 @@ function lowerForOfDyn( done.mutable = true; const value = lowerer.declareHiddenLocal("%dofValue", DYN); const returned = lowerer.declareHiddenLocal("%dofReturn", DYN); + const closed = lowerer.declareHiddenLocal("%dofClosed", DYN); const needsClose = lowerer.declareHiddenLocal("%dofNeedsClose", BOOL); needsClose.mutable = true; const error = lowerer.declareHiddenLocal("%dofError", CAUGHT); @@ -11312,9 +11816,10 @@ function lowerForOfDyn( }, then: [ { - kind: "exprStmt", + kind: "varDecl", + localId: closed.id, // return()'s (awaited) result must be an object, as for next(). - expr: asyncLoop + init: asyncLoop ? check( awaitDyn({ kind: "dynCall", @@ -11329,6 +11834,27 @@ function lowerForOfDyn( : call(varRef(returned.id, DYN, loc)), loc, }, + // A synchronous iterator's return() result is read as the + // async-from-sync iterator reads it: done, then the value, which is + // awaited, so a rejected promise there rejects the loop. + ...(syncIterator + ? [ + { + kind: "if", + cond: varRef(syncIterator.id, BOOL, loc), + then: [ + { kind: "exprStmt", expr: get(varRef(closed.id, DYN, loc), "done"), loc }, + { + kind: "exprStmt", + expr: awaitDyn(get(varRef(closed.id, DYN, loc), "value")), + loc, + }, + ], + else_: null, + loc, + } satisfies IrStmt, + ] + : []), ], else_: null, loc, @@ -11464,7 +11990,7 @@ function lowerForOfDyn( localId: iterator.id, init: check({ kind: "libCall", - fn: asyncLoop ? "dyn.asyncIteratorOpen" : "dyn.iterator", + fn: asyncLoop && !syncOpen ? "dyn.asyncIteratorOpen" : "dyn.iterator", args: [iterable, { kind: "strLit", value: spell, type: STRING, loc }], type: DYN, loc, @@ -11476,13 +12002,15 @@ function lowerForOfDyn( { kind: "varDecl", localId: syncIterator.id, - init: { - kind: "libCall", - fn: "dyn.asyncIteratorWasSync", - args: [], - type: BOOL, - loc, - }, + init: syncOpen + ? { kind: "boolLit", value: true, type: BOOL, loc } + : { + kind: "libCall", + fn: "dyn.asyncIteratorWasSync", + args: [], + type: BOOL, + loc, + }, loc, } satisfies IrStmt, ] @@ -11685,7 +12213,7 @@ function lowerForOfString( try { const s = lowerer.declareHiddenLocal("%strof", STRING); const i = lowerer.declareHiddenLocal("%iterof", F64); - i.mutable = true; // the unit index reassigns (hidden locals default const) + i.mutable = true; // the byte cursor reassigns (hidden locals default const) const sRef = (): IrExpr => ({ kind: "varRef", localId: s.id, type: STRING, loc }); const iRef = (): IrExpr => ({ kind: "varRef", localId: i.id, type: F64, loc }); // `for (var c of str)`: the character mechanics stay on a hidden @@ -11716,7 +12244,7 @@ function lowerForOfString( localId: ch.id, init: { kind: "strIntrinsic", - method: "cpAt", + method: "cpAtByte", receiver: sRef(), args: [iRef()], type: STRING, @@ -11743,7 +12271,7 @@ function lowerForOfString( left: iRef(), right: { kind: "strIntrinsic", - method: "length", + method: "byteLength", receiver: chRef, args: [], type: F64, @@ -11775,7 +12303,7 @@ function lowerForOfString( left: iRef(), right: { kind: "strIntrinsic", - method: "length", + method: "byteLength", receiver: sRef(), args: [], type: F64, @@ -11821,11 +12349,8 @@ function lowerForOfString( * own-keys snapshot, verified against Node) while each visit re-checks * presence (i < live length), so pops during the body skip exactly * like Node. Sparse arrays cannot exist here (hole creation traps). - * - globalThis: the harness's leaked-globals sweep — a compiled binary's - * global bindings are compile-time names, never enumerable runtime - * properties, so the enumeration is honestly EMPTY (zero iterations; - * the body typechecked but never evaluates — SEMANTICS.md documents - * the divergence). + * - globalThis: Node 24's enumerable globals in Node's order, then the + * program's own global properties, as keys of a fresh object. * - JavaScript program classes use their live property-creation table. * TypeScript and runtime-owned layouts keep the class-instance fence. * Bindings mirror for-of's: const/let declare the per-iteration string @@ -11835,10 +12360,12 @@ function lowerForOfString( function lowerForIn(lowerer: Lowerer, stmt: ts.ForInStatement): IrStmt { const labels = lowerer.takeLabels(); const loc = locOf(stmt); - if (stdlibGlobalNameOf(lowerer, stmt.expression) === "globalThis") { - return { kind: "block", body: [], loc }; - } - let receiver = lowerer.lowerExpr(stmt.expression); + // The global object visits Node's enumerable globals, then the + // program's own global properties (scr_global_for_in_object). + let receiver: IrExpr = + stdlibGlobalNameOf(lowerer, stmt.expression) === "globalThis" + ? { kind: "libCall", fn: "global.forInObject", args: [], type: DYN, loc } + : lowerer.lowerExpr(stmt.expression); if (receiver.type.kind === "object") { const info = lowerer.classes.get(receiver.type.className); let root = info; @@ -11906,6 +12433,7 @@ function lowerForIn(lowerer: Lowerer, stmt: ts.ForInStatement): IrStmt { // them (accessor slots live outside declaredOrder), so the loop // fences rather than skip keys. fencePerfEntryKeyWalk(lowerer, receiver.type, stmt.expression, "for-in"); + fenceX509KeyWalk(lowerer, receiver.type, stmt.expression, "for-in"); if (shapeHasAccessorSlots(rShape)) { lowerer.unsupported( "SC1090", diff --git a/packages/compiler/src/frontend/lowering/lower-stream.ts b/packages/compiler/src/frontend/lowering/lower-stream.ts index 04c2fa043..2ae952aaa 100644 --- a/packages/compiler/src/frontend/lowering/lower-stream.ts +++ b/packages/compiler/src/frontend/lowering/lower-stream.ts @@ -2542,7 +2542,7 @@ function classIteratorObject( iter: IrExpr, loc: SrcLoc, ): IrExpr { - const key = `streamFromIter:${cit.iterT.className}`; + const key = `streamFromIter:${cit.iterT.kind === "object" ? cit.iterT.className : typeKey(cit.iterT)}`; let name = lowerer.valueHelpers.get(key); if (!name) { name = keyedHelperName(lowerer, "%stream.fromIter", key); @@ -2570,7 +2570,7 @@ function classIteratorObject( { kind: "varDecl", localId: r.id, - init: classIteratorNextCall(lowerer, cit, varRef(it.id, cit.iterT, loc), loc), + init: classIteratorNextCall(lowerer, cit, varRef(it.id, cit.iterT, loc), null, loc), loc, }, { @@ -2837,6 +2837,223 @@ export function streamClassAliasDecl( const PIPELINE_STAGE_EXPECTED = "of type function or an instance of Blob, ReadableStream, WritableStream, Stream, Iterable, AsyncIterable, or Promise or { readable, writable } pair"; +/** A checked-dynamic pipeline stage taken as a stream: a boxed native + * stream unwraps at run time, and a value that is not stream-like or a + * function (an iterable after the first stage) becomes Duplex.from of it, + * as pipelineImpl's last branch does. Stream-like objects, web streams and + * { readable, writable } pairs throw the not-supported Error there, and so + * do functions, which reach this lowering only past + * MAX_DYN_FUNCTION_STAGES. */ +function unboxDynStage(lowerer: Lowerer, v: IrExpr, node: ts.Expression): IrExpr { + noteUntypedStreamFlow(lowerer, null); + return { + kind: "libCall", + fn: "stream.unboxDynStage", + args: [ + v, + strLit( + lowerer.dynNodeStreams + ? "pipeline stages other than native streams, iterables and functions (JavaScript stream objects, web streams, { readable, writable } pairs after the first stage, and functions after the first three untyped stages) are not supported yet" + : "pipeline stages other than native streams, iterables and functions (web streams, { readable, writable } pairs after the first stage, and functions after the first three untyped stages) are not supported yet", + locOf(node), + ), + ], + type: { kind: "object", className: "%Duplex" }, + loc: locOf(node), + }; +} + +/** A pipeline whose first stage is checked-dynamic, lowered with that + * stage already evaluated: `expr` is the stream it unboxes to, or, when + * `iterable` is set, the value to pump as an iterable source. */ +interface DynPipelineSource { + expr: IrExpr; + iterable: boolean; +} + +/** pipeline() whose first stage is a stream argument (not a function + * stage, an iterable of a static type, or a cipher value), lowered here. + * When the value is checked-dynamic, Node keeps a stream there and pipes + * it, and keeps an iterable (a string, an array, a generator, any value + * with an iterator method) and pumps it into the next stage. The value is + * evaluated once, and stream.dynIterableSource picks the lowering at run + * time. Null when the first stage is not such an argument. */ +function lowerDynSourcePipeline( + lowerer: Lowerer, + node: ts.Expression, + loc: SrcLoc, + build: (source: DynPipelineSource) => IrExpr, +): IrExpr | null { + if (pipelineStageFunction(lowerer, node) !== null) return null; + if (iterableSourceType(lowerer, node) !== null || isValueStream(lowerer, node)) return null; + let value = lowerer.lowerExpr(node); + // A Set, a Map or a generator object is iterated through its + // checked-dynamic value. + if (BOXED_SOURCE_KINDS.has(value.type.kind) && lowerer.dynConvertible(value.type)) { + value = { kind: "dynFrom", value, type: DYN, loc: value.loc }; + } + if (value.type.kind !== "dyn") { + let stream: IrExpr; + try { + stream = streamArgOf(lowerer, node, value, "pipeline"); + } catch (e) { + if (e instanceof StreamArgTypeThrow) return e.expr; + throw e; + } + return build({ expr: stream, iterable: false }); + } + const local = lowerer.declareHiddenLocal("%pipelineSource", DYN); + const ref = varRef(local.id, DYN, loc); + const stream = build({ expr: unboxDynStage(lowerer, ref, node), iterable: false }); + const iterable = build({ expr: ref, iterable: true }); + const type = stream.type; + return { + kind: "seqExpr", + stmts: [{ kind: "varDecl", localId: local.id, init: value, loc }], + result: { + kind: "ternary", + cond: { kind: "libCall", fn: "stream.dynIterableSource", args: [ref], type: BOOL, loc }, + then: lowerer.coerceToExpected(iterable, type), + else_: stream, + type, + loc, + }, + type, + loc, + }; +} + +/** The static types of a pipeline source that lowerDynSourcePipeline boxes + * as checked-dynamic values. An array or a string reaches it when the + * checker types the argument as any while its value keeps a static type. */ +const BOXED_SOURCE_KINDS: ReadonlySet = new Set([ + "set", + "map", + "generator", + "array", + "string", +]); + +/** The most checked-dynamic stages after the first that a pipeline + * branches on as functions; each one doubles the lowered pipeline calls. */ +const MAX_DYN_FUNCTION_STAGES = 3; + +/** A pipeline's arguments for one of the run-time branches + * lowerDynStagePipeline lowers. */ +interface DynStagePlan { + /** the first stage when it is checked-dynamic */ + source: DynPipelineSource | null; + /** argument values evaluated before the branches, in argument order */ + bound: ReadonlyMap; + /** a typed iterable first stage's source, lowered before the branches */ + preSource?: { closure: IrExpr; out: SyncGenType } | undefined; + /** the checked-dynamic stages this branch takes as functions */ + fns: ReadonlySet; +} + +/** pipeline() whose stages after the first include untyped values: + * pipelineImpl calls a stage that is a function with the previous stage + * and { signal }, and pipes into or takes any other value, so the call is + * lowered once per combination of those stages being functions or not, + * up to MAX_DYN_FUNCTION_STAGES of them, and a typeof test on each + * picks the branch at run time. The arguments other than function stages + * are evaluated first, in argument order. */ +function lowerDynStagePipeline( + lowerer: Lowerer, + stageArgs: readonly ts.Expression[], + src: DynPipelineSource | null, + loc: SrcLoc, + build: (plan: DynStagePlan) => IrExpr, +): IrExpr { + const candidates: number[] = []; + for (let j = 1; j < stageArgs.length; j++) { + const arg = stageArgs[j]!; + if (pipelineStageFunction(lowerer, arg) !== null || isValueStream(lowerer, arg)) continue; + if ((lowerer.typeOf(arg).flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0) { + candidates.push(j); + } + } + if (candidates.length === 0) return build({ source: src, bound: new Map(), fns: new Set() }); + const stmts: IrStmt[] = []; + const bind = (init: IrExpr): IrExpr => { + const local = lowerer.declareHiddenLocal("%pipelineArg", init.type); + stmts.push({ kind: "varDecl", localId: local.id, init, loc: init.loc }); + return varRef(local.id, init.type, init.loc); + }; + const source = src !== null && !src.iterable ? { expr: bind(src.expr), iterable: false } : src; + const bound = new Map(); + let preSource: DynStagePlan["preSource"]; + for (let j = src !== null ? 1 : 0; j < stageArgs.length; j++) { + const arg = stageArgs[j]!; + if (pipelineStageFunction(lowerer, arg) !== null) continue; + if (j === 0 && iterableSourceType(lowerer, arg) !== null) { + const lowered = lowerIterableSource(lowerer, arg, null, loc); + if (lowered === null) lowerer.noLowering("pipeline over this source", arg); + preSource = { closure: bind(lowered!.closure), out: lowered!.out }; + continue; + } + let value = lowerer.lowerExpr(arg); + // an untyped array, string, Set, Map or generator object that keeps + // its static type is taken through its checked-dynamic value + if ( + candidates.includes(j) && + BOXED_SOURCE_KINDS.has(value.type.kind) && + lowerer.dynConvertible(value.type) + ) { + value = { kind: "dynFrom", value, type: DYN, loc: value.loc }; + } + bound.set(j, bind(value)); + } + const dynStages = candidates + .filter((j) => bound.get(j)!.type.kind === "dyn") + .slice(0, MAX_DYN_FUNCTION_STAGES); + const branch = (i: number, fns: ReadonlySet): IrExpr => { + if (i === dynStages.length) return build({ source, bound, preSource, fns }); + const j = dynStages[i]!; + const asStream = branch(i + 1, fns); + const asFunction = branch(i + 1, new Set([...fns, j])); + // a function destination's promise resolves to its value + const type = + asFunction.type.kind === "promise" && asFunction.type.inner.kind === "dyn" + ? asFunction.type + : asStream.type; + return { + kind: "ternary", + cond: { kind: "dynTest", test: "function", value: bound.get(j)!, type: BOOL, loc }, + then: lowerer.coerceToExpected(asFunction, type), + else_: lowerer.coerceToExpected(asStream, type), + type, + loc, + }; + }; + const result = branch(0, new Set()); + return { kind: "seqExpr", stmts, result, type: result.type, loc }; +} + +/** lowerPipelineStages' source arguments for a checked-dynamic first stage. */ +function stagesSource( + src: DynPipelineSource | null, +): [ReadonlyMap | undefined, IrExpr | undefined] { + if (src === null) return [undefined, undefined]; + return src.iterable ? [undefined, src.expr] : [new Map([[0, src.expr]]), undefined]; +} + +/** A pipeline stage taken as a stream under a DynStagePlan: the + * checked-dynamic first stage's stream, a value bound before the + * branches, or the argument lowered here. */ +function planStreamArg( + lowerer: Lowerer, + plan: DynStagePlan, + node: ts.Expression, + i: number, +): IrExpr { + if (i === 0 && plan.source !== null) return plan.source.expr; + const bound = plan.bound.get(i); + return bound !== undefined + ? streamArgOf(lowerer, node, bound, "pipeline") + : lowerStreamArg(lowerer, node, "pipeline"); +} + /** A stream-rooted argument, lowered — or the pointed fence (pipeline's * iterable/generator/function stages have no lowering yet). `what` picks * the validation ladder: finished() rejects EVERY non-stream through @@ -2848,7 +3065,16 @@ function lowerStreamArg( node: ts.Expression, what: "finished" | "pipeline" | "addAbortSignal" | "compose", ): IrExpr { - const lowered = lowerer.lowerExpr(node); + return streamArgOf(lowerer, node, lowerer.lowerExpr(node), what); +} + +/** lowerStreamArg over the argument's lowered value. */ +function streamArgOf( + lowerer: Lowerer, + node: ts.Expression, + lowered: IrExpr, + what: "finished" | "pipeline" | "addAbortSignal" | "compose", +): IrExpr { // A Hash, Hmac, Cipheriv or Decipheriv stage is its Transform; pipeline // and compose stages keep the value alive until the stream settles. const v = isValueStream(lowerer, node) @@ -2857,24 +3083,7 @@ function lowerStreamArg( // A checked-dynamic pipeline stage (an untyped source or destination): // a boxed native stream unwraps at run time; other values (iterables, // functions) throw the not-supported Error there. - if (v.type.kind === "dyn" && what === "pipeline") { - noteUntypedStreamFlow(lowerer, null); - return { - kind: "libCall", - fn: "stream.unboxDyn", - args: [ - v, - strLit( - lowerer.dynNodeStreams - ? "pipeline stages other than native streams (JavaScript stream objects, iterables, generators, plain functions, and { readable, writable } pairs) are not supported yet" - : "pipeline stages other than native streams (iterables, generators, plain functions, and { readable, writable } pairs) are not supported yet", - locOf(node), - ), - ], - type: { kind: "object", className: "%Duplex" }, - loc: locOf(node), - }; - } + if (v.type.kind === "dyn" && what === "pipeline") return unboxDynStage(lowerer, v, node); const info = v.type.kind === "object" ? lowerer.classes.get(v.type.className) : undefined; if (!streamSidesOf(lowerer, info)) { // A provably-non-stream value in a JS source (the invalid-input @@ -3210,57 +3419,88 @@ export function lowerStreamModuleCall( ? pipelineOptions(lowerer, lastArg) : null; const stageArgs = opts ? args.slice(0, -1) : args; - if (hasPipelineFunctionStage(lowerer, stageArgs)) { - const staged = lowerPipelineStages(lowerer, stageArgs, loc); - const type = staged.destPromise ?? { kind: "promise", inner: VOID }; - // A promise destination's value settles the result (same type). - if (!opts) { - return staged.wrap({ - kind: "libCall", - fn: "sp.pipelineStages", - args: staged.args, - type, - loc, - }); - } - // the options: end, and the signal (flags bit 6) - const flagsArg = staged.args[3]!; - if (flagsArg.kind !== "numLit") - throw new InternalCompilerError("pipeline flags not a literal"); - const args = [...staged.args, boolLit(opts.end, loc)]; - if (opts.signal) { - args[3] = numLit(flagsArg.value | 64, loc); - args.push(opts.signal); - } - return staged.wrap({ kind: "libCall", fn: "sp.pipelineStagesOpts", args, type, loc }); - } - let streams: IrExpr[]; - try { - streams = stageArgs.map((a) => lowerStreamArg(lowerer, a, "pipeline")); - } catch (e) { - if (e instanceof StreamArgTypeThrow) return e.expr; - throw e; - } - if (opts) { - const head = [numLit(streams.length, loc), ...streams, boolLit(opts.end, loc)]; - return { - kind: "libCall", - fn: opts.signal ? "sp.pipelineOptsSignal" : "sp.pipelineOpts", - args: opts.signal ? [...head, opts.signal] : head, - type: { kind: "promise", inner: VOID }, - loc, - }; - } - return { - kind: "libCall", - fn: "sp.pipeline", - args: [numLit(streams.length, loc), ...streams], - type: { kind: "promise", inner: VOID }, - loc, - }; + const build = (src: DynPipelineSource | null): IrExpr => + lowerDynStagePipeline(lowerer, stageArgs, src, loc, (plan) => + lowerPromisePipeline(lowerer, stageArgs, opts, plan, loc), + ); + return lowerDynSourcePipeline(lowerer, stageArgs[0]!, loc, build) ?? build(null); } return null; } + return lowerStreamModuleCallbackCall(lowerer, call, bi, loc); +} + +/** stream/promises pipeline(...stages[, options]), for one branch of + * lowerDynStagePipeline's plan. */ +function lowerPromisePipeline( + lowerer: Lowerer, + stageArgs: readonly ts.Expression[], + opts: ReturnType | null, + plan: DynStagePlan, + loc: SrcLoc, +): IrExpr { + const src = plan.source; + if ( + src?.iterable === true || + plan.fns.size > 0 || + plan.preSource !== undefined || + hasPipelineFunctionStage(lowerer, stageArgs) + ) { + const staged = lowerPipelineStages(lowerer, stageArgs, loc, ...stagesSource(src), plan); + const type = staged.destPromise ?? { kind: "promise", inner: VOID }; + // A promise destination's value settles the result (same type). + if (!opts) { + return staged.wrap({ + kind: "libCall", + fn: "sp.pipelineStages", + args: staged.args, + type, + loc, + }); + } + // the options: end, and the signal (flags bit 6) + const flagsArg = staged.args[3]!; + if (flagsArg.kind !== "numLit") throw new InternalCompilerError("pipeline flags not a literal"); + const args = [...staged.args, boolLit(opts.end, loc)]; + if (opts.signal) { + args[3] = numLit(flagsArg.value | 64, loc); + args.push(opts.signal); + } + return staged.wrap({ kind: "libCall", fn: "sp.pipelineStagesOpts", args, type, loc }); + } + let streams: IrExpr[]; + try { + streams = stageArgs.map((a, i) => planStreamArg(lowerer, plan, a, i)); + } catch (e) { + if (e instanceof StreamArgTypeThrow) return e.expr; + throw e; + } + if (opts) { + const head = [numLit(streams.length, loc), ...streams, boolLit(opts.end, loc)]; + return { + kind: "libCall", + fn: opts.signal ? "sp.pipelineOptsSignal" : "sp.pipelineOpts", + args: opts.signal ? [...head, opts.signal] : head, + type: { kind: "promise", inner: VOID }, + loc, + }; + } + return { + kind: "libCall", + fn: "sp.pipeline", + args: [numLit(streams.length, loc), ...streams], + type: { kind: "promise", inner: VOID }, + loc, + }; +} + +/** The node:stream and stream/consumers members of lowerStreamModuleCall. */ +function lowerStreamModuleCallbackCall( + lowerer: Lowerer, + call: ts.CallExpression, + bi: { module: string; member: string }, + loc: SrcLoc, +): IrExpr | null { if (bi.module === "stream/consumers") { const args = call.arguments.nodes; // text/json/buffer/arrayBuffer(stream) → a pending promise the @@ -3411,67 +3651,11 @@ export function lowerStreamModuleCall( "the supported form is pipeline(source, ...streams, callback)", ); } - if (hasPipelineFunctionStage(lowerer, args.slice(0, -1))) { - const staged = lowerPipelineStages(lowerer, args.slice(0, -1), loc); - const cbNode = args[args.length - 1]!; - const cbParams = - ts.isArrowFunction(cbNode) || ts.isFunctionExpression(cbNode) - ? cbNode.parameters.nodes.length - : 0; - if (staged.dest === 2 || (staged.dest === 1 && cbParams > 1)) { - // callback(err, value): the destination's value (undefined for - // an async generator) reaches the callback through the - // checked-dynamic path. - const cb = lowerer.lowerExprExpecting(args[args.length - 1]!, DYN); - // SCR_PIPE_TYPED_CB: a typed callback whose error parameter cannot - // hold undefined takes null for no error; others take undefined. - const stagedArgs = [...staged.args]; - const flagsArg = stagedArgs[3]!; - if (flagsArg.kind !== "numLit") - throw new InternalCompilerError("pipeline flags not a literal"); - if ( - !isJsSourceFile(ts.getSourceFileOfNode(call)) && - !callbackErrorAdmitsUndefined(lowerer, cb) - ) - stagedArgs[3] = numLit(flagsArg.value | 128, loc); - return staged.wrap({ - kind: "libCall", - fn: "stream.pipelineStagesDyn", - args: [...stagedArgs, cb], - type: staged.last, - loc, - }); - } - const { cb, dyn } = lowerEosCallback( - lowerer, - "pipeline", - args[args.length - 1]!, - staged.last, + const build = (src: DynPipelineSource | null): IrExpr => + lowerDynStagePipeline(lowerer, args.slice(0, -1), src, loc, (plan) => + lowerCallbackPipeline(lowerer, call, args, plan, loc), ); - return staged.wrap({ - kind: "libCall", - fn: dyn ? "stream.pipelineStagesDyn" : "stream.pipelineStages", - args: [...staged.args, cb], - type: staged.last, - loc, - }); - } - let streams: IrExpr[]; - try { - streams = args.slice(0, -1).map((a) => lowerStreamArg(lowerer, a, "pipeline")); - } catch (e) { - if (e instanceof StreamArgTypeThrow) return e.expr; - throw e; - } - const last = streams[streams.length - 1]!; - const { cb, dyn } = lowerEosCallback(lowerer, "pipeline", args[args.length - 1]!, last.type); - return { - kind: "libCall", - fn: dyn ? "stream.pipelineDyn" : "stream.pipeline", - args: [numLit(streams.length, loc), ...streams, cb], - type: last.type, - loc, - }; + return lowerDynSourcePipeline(lowerer, args[0]!, loc, build) ?? build(null); } if (bi.member === "compose") { @@ -3484,6 +3668,84 @@ export function lowerStreamModuleCall( return null; } +/** pipeline(...stages, callback), for one branch of lowerDynStagePipeline's + * plan. */ +function lowerCallbackPipeline( + lowerer: Lowerer, + call: ts.CallExpression, + args: readonly ts.Expression[], + plan: DynStagePlan, + loc: SrcLoc, +): IrExpr { + const src = plan.source; + const cbNode = args[args.length - 1]!; + const cbParams = + ts.isArrowFunction(cbNode) || ts.isFunctionExpression(cbNode) + ? cbNode.parameters.nodes.length + : 0; + // A branch over untyped stages whose destination is a stream: the + // callback still receives (err, value), value undefined, as in Node. + const dynCallback = plan.bound.size > 0 && cbParams > 1; + if ( + src?.iterable === true || + plan.fns.size > 0 || + plan.preSource !== undefined || + hasPipelineFunctionStage(lowerer, args.slice(0, -1)) + ) { + const staged = lowerPipelineStages(lowerer, args.slice(0, -1), loc, ...stagesSource(src), plan); + if (staged.dest >= 2 || (staged.dest === 1 && cbParams > 1) || dynCallback) { + // callback(err, value): the destination's value (undefined for + // an async generator) reaches the callback through the + // checked-dynamic path. + const cb = lowerer.lowerExprExpecting(args[args.length - 1]!, DYN); + // SCR_PIPE_TYPED_CB: a typed callback whose error parameter cannot + // hold undefined takes null for no error; others take undefined. + const stagedArgs = [...staged.args]; + const flagsArg = stagedArgs[3]!; + if (flagsArg.kind !== "numLit") + throw new InternalCompilerError("pipeline flags not a literal"); + if ( + !isJsSourceFile(ts.getSourceFileOfNode(call)) && + !callbackErrorAdmitsUndefined(lowerer, cb) + ) + stagedArgs[3] = numLit(flagsArg.value | 128, loc); + return staged.wrap({ + kind: "libCall", + fn: "stream.pipelineStagesDyn", + args: [...stagedArgs, cb], + type: staged.last, + loc, + }); + } + const { cb, dyn } = lowerEosCallback(lowerer, "pipeline", args[args.length - 1]!, staged.last); + return staged.wrap({ + kind: "libCall", + fn: dyn ? "stream.pipelineStagesDyn" : "stream.pipelineStages", + args: [...staged.args, cb], + type: staged.last, + loc, + }); + } + let streams: IrExpr[]; + try { + streams = args.slice(0, -1).map((a, i) => planStreamArg(lowerer, plan, a, i)); + } catch (e) { + if (e instanceof StreamArgTypeThrow) return e.expr; + throw e; + } + const last = streams[streams.length - 1]!; + const { cb, dyn } = dynCallback + ? { cb: lowerer.lowerExprExpecting(cbNode, DYN), dyn: true } + : lowerEosCallback(lowerer, "pipeline", cbNode, last.type); + return { + kind: "libCall", + fn: dyn ? "stream.pipelineDyn" : "stream.pipeline", + args: [numLit(streams.length, loc), ...streams, cb], + type: last.type, + loc, + }; +} + /** A pipeline() function stage: an inline function or arrow function, or * an identifier bound to a function declaration or to a `const` * initialized with one, which nothing reassigns. */ @@ -3788,6 +4050,693 @@ function lowerWebStreamSource( }; } +/** A checked-dynamic pipeline source that is an iterable at run time + * (stream.dynIterableSource), as an async generator closure that iterates + * it as Node's pipeline does with `for await`: the value's async iterator, + * or its sync iterator stepped like the typed iterable sources. Each value + * is yielded; leaving early closes the iterator through its return(). */ +function lowerDynIterableSource( + lowerer: Lowerer, + value: IrExpr, + loc: SrcLoc, +): { closure: IrExpr; out: SyncGenType } | null { + const out: SyncGenType = { + kind: "generator", + async: true, + yieldT: DYN, + retT: VOID, + nextT: UNDEFINED_T, + }; + const genResultT = genResultRecord(DYN, VOID, lowerer.shapes, lowerer.unions); + if (!genResultT) return null; + const key = "dynIterableSource"; + let maker = lowerer.valueHelpers.get(key); + if (!maker) { + maker = `%stream.dynIterableSource.${lowerer.valueHelpers.size}`; + const genName = `${maker}.gen`; + const frame = new LiftedFrame(loc); + const srcId = frame.local("src", DYN, false); + const itId = frame.local("it", DYN, false); + const syncId = frame.local("sync", BOOL, false); + const nextId = frame.local("next", DYN, false); + const stepId = frame.local("step", DYN, true); + const valueId = frame.local("value", DYN, true); + const activeId = frame.local("active", BOOL, true); + const closeId = frame.local("close", DYN, false); + const it = (): IrExpr => varRef(itId, DYN, loc); + const sync = (): IrExpr => varRef(syncId, BOOL, loc); + const step = (): IrExpr => varRef(stepId, DYN, loc); + const value = (): IrExpr => varRef(valueId, DYN, loc); + // for await over a sync iterator awaits each value, the final one + // included (CreateAsyncFromSyncIterator): an object (a promise or a + // thenable) passes on its result + const awaitsValue = (): IrExpr => ({ + kind: "logical", + op: "&&", + left: sync(), + right: { + kind: "logical", + op: "||", + left: { kind: "dynTest", test: "promise", value: value(), type: BOOL, loc }, + right: { + kind: "logical", + op: "&&", + left: { + kind: "dynTest", + test: "nullish", + negated: true, + value: value(), + type: BOOL, + loc, + }, + right: { kind: "dynTest", test: "object", value: value(), type: BOOL, loc }, + type: BOOL, + loc, + }, + type: BOOL, + loc, + }, + type: BOOL, + loc, + }); + const get = (obj: IrExpr, name: string): IrExpr => ({ + kind: "dynKeyGet", + value: obj, + key: strLit(name, loc), + type: DYN, + loc, + }); + const awaitDyn = (v: IrExpr): IrExpr => ({ + kind: "libCall", + fn: "async.awaitDyn", + args: [v], + type: DYN, + loc, + }); + const call = (callee: IrExpr, calleeName: string): IrExpr => ({ + kind: "dynCall", + callee, + receiver: it(), + calleeName, + args: [], + type: DYN, + loc, + }); + const setActive = (on: boolean): IrStmt => ({ + kind: "assign", + localId: activeId, + value: boolLit(on, loc), + loc, + }); + const nextCall = (): IrExpr => call(varRef(nextId, DYN, loc), "iterator method"); + const body: IrStmt[] = [ + { + kind: "varDecl", + localId: itId, + init: { + kind: "libCall", + fn: "dyn.iteratorResult", + args: [ + { + kind: "libCall", + fn: "dyn.asyncIteratorOpen", + args: [varRef(srcId, DYN, loc), strLit("", loc)], + type: DYN, + loc, + }, + ], + type: DYN, + loc, + }, + loc, + }, + { + kind: "varDecl", + localId: syncId, + init: { kind: "libCall", fn: "dyn.asyncIteratorWasSync", args: [], type: BOOL, loc }, + loc, + }, + { kind: "varDecl", localId: nextId, init: get(it(), "next"), loc }, + { kind: "varDecl", localId: stepId, init: dynUndefinedExpr(loc), loc }, + { kind: "varDecl", localId: valueId, init: dynUndefinedExpr(loc), loc }, + { kind: "varDecl", localId: activeId, init: boolLit(false, loc), loc }, + { + kind: "tryCatch", + tryBody: [ + { + kind: "while", + cond: boolLit(true, loc), + body: [ + setActive(false), + { + kind: "assign", + localId: stepId, + value: { + kind: "libCall", + fn: "dyn.iteratorResult", + args: [ + { + kind: "ternary", + cond: sync(), + then: nextCall(), + else_: awaitDyn(nextCall()), + type: DYN, + loc, + }, + ], + type: DYN, + loc, + }, + loc, + }, + { + kind: "if", + cond: { + kind: "dynTest", + test: "truthy", + value: get(step(), "done"), + type: BOOL, + loc, + }, + // a sync iterator's final value is awaited too: a rejection + // rejects the pump, and the finished iterator is not closed. + // An async iterator's final value is not read. + then: [ + { + kind: "if", + cond: sync(), + then: [ + { kind: "assign", localId: valueId, value: get(step(), "value"), loc }, + { + kind: "if", + cond: awaitsValue(), + then: [{ kind: "exprStmt", expr: awaitDyn(value()), loc }], + else_: null, + loc, + }, + ], + else_: null, + loc, + }, + { kind: "break", loc }, + ], + else_: null, + loc, + }, + setActive(true), + { kind: "assign", localId: valueId, value: get(step(), "value"), loc }, + // a rejection here closes the iterator + { + kind: "if", + cond: awaitsValue(), + then: [{ kind: "assign", localId: valueId, value: awaitDyn(value()), loc }], + else_: null, + loc, + }, + { + kind: "exprStmt", + expr: { kind: "yieldExpr", value: value(), type: VOID, loc }, + loc, + }, + ], + loc, + }, + ], + catchBody: null, + catchLocalId: null, + finallyBody: [ + { + kind: "if", + cond: varRef(activeId, BOOL, loc), + then: [ + { kind: "varDecl", localId: closeId, init: get(it(), "return"), loc }, + { + kind: "if", + cond: { + kind: "unary", + op: "!", + operand: { + kind: "dynTest", + test: "nullish", + value: varRef(closeId, DYN, loc), + type: BOOL, + loc, + }, + type: BOOL, + loc, + }, + then: [ + { + kind: "exprStmt", + expr: awaitDyn(call(varRef(closeId, DYN, loc), "iterator return")), + loc, + }, + ], + else_: null, + loc, + }, + ], + else_: null, + loc, + }, + ], + loc, + }, + { kind: "return", value: null, loc }, + ]; + const srcLocal = frame.locals.find((l) => l.id === srcId)!; + srcLocal.boxed = true; + lowerer.valueHelpers.set(key, maker); + lowerer.liftedFns.push({ + name: genName, + params: [], + returnType: VOID, + async: true, + captures: [{ localId: srcId, name: "src", type: DYN }], + locals: frame.locals, + body, + generator: { yieldT: DYN, nextT: UNDEFINED_T, resultType: genResultT }, + loc, + }); + const makerSrc: IrLocal = { id: "src.0", name: "src", type: DYN, mutable: false, boxed: true }; + lowerer.liftedFns.push({ + name: maker, + params: [{ localId: makerSrc.id, name: makerSrc.name, type: DYN }], + returnType: funcOf([], out), + locals: [makerSrc], + body: [ + { + kind: "return", + value: { + kind: "closure", + fnName: genName, + captures: [makerSrc.id], + type: funcOf([], out), + loc, + }, + loc, + }, + ], + loc, + }); + } + return { + closure: { kind: "call", callee: maker, args: [value], type: funcOf([], out), loc }, + out, + }; +} + +/** The async generator a checked-dynamic stage's iterable is pumped + * through. */ +function dynStageGenType(): SyncGenType { + return { kind: "generator", async: true, yieldT: DYN, retT: VOID, nextT: UNDEFINED_T }; +} + +/** A checked-dynamic pipeline stage after the first that is a function at + * run time, as the stage closure the runtime calls with the previous + * stage and { signal } (pipelineImpl's `typeof stream === 'function'` + * branch): it calls the function with the previous stream or generator + * as an untyped value. Before the last stage the result must be async + * iterable (ERR_INVALID_RETURN_VALUE otherwise) and is pumped on through + * lowerDynIterableSource's generator; the last stage answers the result, + * which the runtime classifies (SCR_PIPE_DEST_DYN). A previous stage + * whose type has no untyped form throws the not-supported Error. */ +function lowerDynFunctionStage( + lowerer: Lowerer, + fnValue: IrExpr, + srcT: IrType, + j: number, + isLast: boolean, + loc: SrcLoc, + handoff?: DynStageHandoff, +): { closure: IrExpr; out: IrType } { + const out: IrType = isLast ? DYN : dynStageGenType(); + const stageT = funcOf([srcT, DYN], out); + const take = handoff?.take === true; + const give = handoff?.give === true && !isLast; + const key = `dynFunctionStage:${isLast ? "dest" : "transform"}:${typeKey(srcT)}:${take ? 1 : 0}${give ? 1 : 0}`; + let maker = lowerer.valueHelpers.get(key); + if (!maker) { + maker = `%stream.dynFunctionStage.${lowerer.valueHelpers.size}`; + lowerer.valueHelpers.set(key, maker); + const stageName = `${maker}.stage`; + const frame = new LiftedFrame(loc); + const fnId = frame.local("fn", DYN, false); + const labelId = frame.local("label", STRING, false); + const cellId = frame.local("cell", DYN, false); + const srcId = frame.param("src", srcT); + const optsId = frame.param("opts", DYN); + const inputId = frame.local("input", DYN, false); + const resultId = frame.local("result", DYN, false); + const nextId = frame.local("next", DYN, false); + const cell = (): IrExpr => varRef(cellId, DYN, loc); + const body: IrStmt[] = []; + if (take) { + // the previous untyped stage's value, handed over as it is + // (makeAsyncIterable answers an iterable itself) + body.push( + { kind: "varDecl", localId: inputId, init: handoffTake(cell(), loc), loc }, + handoffPut(cell(), dynUndefinedExpr(loc), loc), + ); + } + if (!take && !lowerer.dynConvertible(srcT)) { + body.push({ + kind: "return", + value: nodeThrowExpr( + 0, + "SC2020", + `pipeline function stages after a stage of type '${lowerer.fmt(srcT)}' are not supported yet`, + out, + loc, + ), + loc, + }); + } else { + body.push({ + kind: "varDecl", + localId: resultId, + init: { + kind: "dynCall", + callee: varRef(fnId, DYN, loc), + receiver: dynUndefinedExpr(loc), + calleeName: "transform", + args: [ + take + ? varRef(inputId, DYN, loc) + : { kind: "dynFrom", value: varRef(srcId, srcT, loc), type: DYN, loc }, + varRef(optsId, DYN, loc), + ], + type: DYN, + loc, + }, + loc, + }); + if (isLast) { + body.push({ kind: "return", value: varRef(resultId, DYN, loc), loc }); + } else { + body.push({ + kind: "varDecl", + localId: nextId, + init: { + kind: "libCall", + fn: "stream.dynTransformResult", + args: [varRef(resultId, DYN, loc), varRef(labelId, STRING, loc)], + type: DYN, + loc, + }, + loc, + }); + // the next untyped function stage takes the result itself; the + // generator returned for the runtime is then never read + if (give) body.push(handoffPut(cell(), varRef(nextId, DYN, loc), loc)); + const iterable = lowerDynIterableSource(lowerer, varRef(nextId, DYN, loc), loc); + if (!iterable) + throw new InternalCompilerError("dyn iterable generator without a result record"); + body.push({ + kind: "return", + value: { kind: "callValue", callee: iterable.closure, args: [], type: out, loc }, + loc, + }); + } + } + const captured = take || give ? [fnId, labelId, cellId] : [fnId, labelId]; + for (const id of captured) frame.locals.find((l) => l.id === id)!.boxed = true; + lowerer.liftedFns.push({ + name: stageName, + params: frame.params, + returnType: out, + captures: [ + { localId: fnId, name: "fn", type: DYN }, + { localId: labelId, name: "label", type: STRING }, + ...(take || give ? [{ localId: cellId, name: "cell", type: DYN }] : []), + ], + locals: frame.locals, + body, + loc, + }); + const makerFn: IrLocal = { id: "fn.0", name: "fn", type: DYN, mutable: false, boxed: true }; + const makerLabel: IrLocal = { + id: "label.1", + name: "label", + type: STRING, + mutable: false, + boxed: true, + }; + const makerCell: IrLocal = { + id: "cell.2", + name: "cell", + type: DYN, + mutable: false, + boxed: true, + }; + const makerLocals = take || give ? [makerFn, makerLabel, makerCell] : [makerFn, makerLabel]; + lowerer.liftedFns.push({ + name: maker, + params: makerLocals.map((l) => ({ localId: l.id, name: l.name, type: l.type })), + returnType: stageT, + locals: makerLocals, + body: [ + { + kind: "return", + value: { + kind: "closure", + fnName: stageName, + captures: makerLocals.map((l) => l.id), + type: stageT, + loc, + }, + loc, + }, + ], + loc, + }); + } + return { + closure: { + kind: "call", + callee: maker, + args: [fnValue, strLit(`transform[${j - 1}]`, loc), ...(take || give ? [handoff!.cell] : [])], + type: stageT, + loc, + }, + out, + }; +} + +/** The cell that carries an untyped stage's value to the untyped function + * stage after it, created per pipeline call. A stage that gives stores its + * value; the next stage takes it instead of the generator the runtime + * passes, as Node's makeAsyncIterable answers an iterable itself. */ +interface DynStageHandoff { + cell: IrExpr; + take: boolean; + give: boolean; +} + +function handoffPut(cell: IrExpr, value: IrExpr, loc: SrcLoc): IrStmt { + return { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "dyn.keySet", + args: [cell, strLit("v", loc), value], + type: VOID, + loc, + }, + loc, + }; +} + +function handoffTake(cell: IrExpr, loc: SrcLoc): IrExpr { + return { kind: "dynKeyGet", value: cell, key: strLit("v", loc), type: DYN, loc }; +} + +/** `(value) => generator over value`: the async generator an untyped + * destination function's async iterable result is pumped through + * (SCR_PIPE_DEST_DYN's box), interned. */ +function dynStageIterHelper(lowerer: Lowerer, loc: SrcLoc): IrExpr { + const genT = dynStageGenType(); + const type = funcOf([DYN], genT); + const key = "dynStageIter"; + let name = lowerer.valueHelpers.get(key); + if (!name) { + name = `%stream.dynStageIter.${lowerer.valueHelpers.size}`; + lowerer.valueHelpers.set(key, name); + const v: IrLocal = { id: "value.0", name: "value", type: DYN, mutable: false }; + const iterable = lowerDynIterableSource(lowerer, varRef(v.id, DYN, loc), loc); + if (!iterable) + throw new InternalCompilerError("dyn iterable generator without a result record"); + lowerer.liftedFns.push({ + name, + params: [{ localId: v.id, name: v.name, type: DYN }], + returnType: genT, + locals: [v], + body: [ + { + kind: "return", + value: { kind: "callValue", callee: iterable.closure, args: [], type: genT, loc }, + loc, + }, + ], + loc, + }); + } + return { kind: "closure", fnName: name, captures: [], type, loc }; +} + +/** A checked-dynamic first pipeline stage pumped as an iterable + * (pipelineImpl's i === 0 branch), as the stage closure the runtime calls + * with { signal } while it sets the pipeline up: a function is called + * with { signal } and its result must be iterable, a { readable, writable } + * pair pumps its readable side, and any other value is pumped itself, + * through lowerDynIterableSource's generator. */ +function lowerDynSourceStage( + lowerer: Lowerer, + value: IrExpr, + loc: SrcLoc, + cell?: IrExpr, +): { closure: IrExpr; out: SyncGenType } | null { + const out: SyncGenType = { + kind: "generator", + async: true, + yieldT: DYN, + retT: VOID, + nextT: UNDEFINED_T, + }; + const stageT = funcOf([DYN], out); + const give = cell !== undefined; + const key = give ? "dynSourceStage:give" : "dynSourceStage"; + let maker = lowerer.valueHelpers.get(key); + if (!maker) { + maker = `%stream.dynSourceStage.${lowerer.valueHelpers.size}`; + const stageName = `${maker}.stage`; + const frame = new LiftedFrame(loc); + const srcId = frame.local("src", DYN, false); + const cellId = frame.local("cell", DYN, false); + const optsId = frame.param("opts", DYN); + const valueId = frame.local("value", DYN, false); + const genId = frame.local("gen", out, false); + const src = (): IrExpr => varRef(srcId, DYN, loc); + const sourceValue = (v: IrExpr, returned: boolean): IrExpr => ({ + kind: "libCall", + fn: "stream.dynSourceValue", + args: [v, boolLit(returned, loc)], + type: DYN, + loc, + }); + const iterable = lowerDynIterableSource(lowerer, varRef(valueId, DYN, loc), loc); + if (!iterable) return null; + lowerer.valueHelpers.set(key, maker); + const body: IrStmt[] = [ + { + kind: "varDecl", + localId: valueId, + init: { + kind: "ternary", + cond: { kind: "dynTest", test: "function", value: src(), type: BOOL, loc }, + then: sourceValue( + { + kind: "dynCall", + callee: src(), + receiver: dynUndefinedExpr(loc), + calleeName: "source", + args: [varRef(optsId, DYN, loc)], + type: DYN, + loc, + }, + true, + ), + else_: sourceValue(src(), false), + type: DYN, + loc, + }, + loc, + }, + { + kind: "varDecl", + localId: genId, + init: { kind: "callValue", callee: iterable.closure, args: [], type: out, loc }, + loc, + }, + ]; + if (give) { + // the untyped function stage after the source takes the iterable + // itself, as Node's makeAsyncIterable answers it; a stream (a pair's + // readable side) goes as the generator over it, as Node iterates a + // stream there + const v = varRef(valueId, DYN, loc); + body.push( + handoffPut( + varRef(cellId, DYN, loc), + { + kind: "ternary", + cond: { kind: "libCall", fn: "stream.dynIterableSource", args: [v], type: BOOL, loc }, + then: v, + else_: { kind: "dynFrom", value: varRef(genId, out, loc), type: DYN, loc }, + type: DYN, + loc, + }, + loc, + ), + ); + } + body.push({ kind: "return", value: varRef(genId, out, loc), loc }); + const captured = give ? [srcId, cellId] : [srcId]; + for (const id of captured) frame.locals.find((l) => l.id === id)!.boxed = true; + lowerer.liftedFns.push({ + name: stageName, + params: frame.params, + returnType: out, + captures: [ + { localId: srcId, name: "src", type: DYN }, + ...(give ? [{ localId: cellId, name: "cell", type: DYN }] : []), + ], + locals: frame.locals, + body, + loc, + }); + const makerSrc: IrLocal = { id: "src.0", name: "src", type: DYN, mutable: false, boxed: true }; + const makerCell: IrLocal = { + id: "cell.1", + name: "cell", + type: DYN, + mutable: false, + boxed: true, + }; + const makerLocals = give ? [makerSrc, makerCell] : [makerSrc]; + lowerer.liftedFns.push({ + name: maker, + params: makerLocals.map((l) => ({ localId: l.id, name: l.name, type: l.type })), + returnType: stageT, + locals: makerLocals, + body: [ + { + kind: "return", + value: { + kind: "closure", + fnName: stageName, + captures: makerLocals.map((l) => l.id), + type: stageT, + loc, + }, + loc, + }, + ], + loc, + }); + } + return { + closure: { + kind: "call", + callee: maker, + args: give ? [value, cell!] : [value], + type: stageT, + loc, + }, + out, + }; +} + function lowerIterableSource( lowerer: Lowerer, node: ts.Expression, @@ -3931,7 +4880,7 @@ function lowerIterableSource( interface PipelineStagesLowering { args: IrExpr[]; last: IrType; - dest: 0 | 1 | 2; + dest: 0 | 1 | 2 | 3; destPromise: (IrType & { kind: "promise" }) | null; wrap: (call: IrExpr) => IrExpr; } @@ -3951,6 +4900,8 @@ function lowerPipelineStages( stageArgs: readonly ts.Expression[], loc: SrcLoc, preLowered?: ReadonlyMap, + dynSource?: IrExpr, + plan?: DynStagePlan, ): PipelineStagesLowering { if (stageArgs.length > 52) { lowerer.noLowering( @@ -3964,7 +4915,7 @@ function lowerPipelineStages( const helperArgs: IrExpr[] = []; let positions = 0; let mask = 0; - let dest: 0 | 1 | 2 = 0; + let dest: 0 | 1 | 2 | 3 = 0; let destPromise: (IrType & { kind: "promise" }) | null = null; // What the next function stage receives: null before the first stage, // else the previous stream's type or the previous stage's generator. @@ -3973,8 +4924,69 @@ function lowerPipelineStages( // before all stages, so when a stage precedes a stream the arguments // are bound to locals first, keeping JavaScript's evaluation order. const argOrder: { list: IrExpr[]; index: number }[] = []; + // An untyped stage followed by an untyped function stage hands its value + // over through one cell per call (DynStageHandoff). + let handoffCell: IrLocal | null = null; + const cellRef = (): IrExpr => { + handoffCell ??= lowerer.declareHiddenLocal("%pipelineHandoff", DYN); + return varRef(handoffCell.id, DYN, loc); + }; + let takes = false; for (let j = 0; j < stageArgs.length; j++) { const arg = stageArgs[j]!; + const givesToNext = plan?.fns.has(j + 1) === true; + if (j === 0 && dynSource !== undefined) { + const source = lowerDynSourceStage( + lowerer, + dynSource, + loc, + givesToNext ? cellRef() : undefined, + ); + takes = givesToNext; + if (source === null) lowerer.noLowering("pipeline over this source", arg); + // a source function takes { signal } + mask |= 1 << stages.length; + positions += 1; + argOrder.push({ list: stages, index: stages.length }); + stages.push(source!.closure); + srcT = source!.out; + continue; + } + if (plan?.fns.has(j) === true) { + // a checked-dynamic stage that is a function at run time + const isLast = j === stageArgs.length - 1; + const take = takes; + const give = givesToNext && !isLast; + takes = give; + const lowered = lowerDynFunctionStage( + lowerer, + plan.bound.get(j)!, + srcT!, + j, + isLast, + loc, + take || give ? { cell: cellRef(), take, give } : undefined, + ); + mask |= 1 << stages.length; + positions += 2 ** j; + argOrder.push({ list: stages, index: stages.length }); + stages.push(lowered.closure); + if (isLast) { + // its result is classified at run time: a promise settles the + // pipeline's value, and an async iterable is pumped into the + // PassThrough + const genT = dynStageGenType(); + const helpers = fromGenHelpers(lowerer, genT, loc); + if (!helpers) lowerer.noLowering("a pipeline destination with this generator type", arg); + helperArgs.push(helpers![0]!, helpers![1]!, helpers![2]!); + dest = 3; + destPromise = { kind: "promise", inner: DYN }; + srcT = null; + } else { + srcT = lowered.out; + } + continue; + } // compose's head and tail functions arrive as Duplex.from streams const fn = preLowered?.has(j) ? null : pipelineStageFunction(lowerer, arg); const syncSourceFn = @@ -3988,7 +5000,9 @@ function lowerPipelineStages( !preLowered?.has(j) && (syncSourceFn || (fn === null && iterableSourceType(lowerer, arg) !== null)) ) { - const source = lowerIterableSource(lowerer, arg, syncSourceFn ? fn : null, loc); + const source = + (syncSourceFn ? undefined : plan?.preSource) ?? + lowerIterableSource(lowerer, arg, syncSourceFn ? fn : null, loc); if (source === null) lowerer.noLowering("pipeline over this source", arg); positions += 1; argOrder.push({ list: stages, index: stages.length }); @@ -4013,7 +5027,12 @@ function lowerPipelineStages( } let stream: IrExpr; try { - stream = preLowered?.get(j) ?? lowerStreamArg(lowerer, arg, "pipeline"); + const bound = plan?.bound.get(j); + stream = + preLowered?.get(j) ?? + (bound !== undefined + ? streamArgOf(lowerer, arg, bound, "pipeline") + : lowerStreamArg(lowerer, arg, "pipeline")); } catch (e) { if (e instanceof StreamArgTypeThrow) lowerer.noLowering("pipeline over this value", arg); throw e; @@ -4092,6 +5111,8 @@ function lowerPipelineStages( ); } helperArgs.push(pipelineBoxHelper(lowerer, destPromise!, loc)); + } else if (dest === 3) { + helperArgs.push(dynStageIterHelper(lowerer, loc)); } const argDecls: IrStmt[] = []; const firstStage = argOrder.findIndex((a) => a.list === stages); @@ -4103,6 +5124,15 @@ function lowerPipelineStages( a.list[a.index] = varRef(local.id, init.type, init.loc); } } + if (handoffCell !== null) { + const cell: IrLocal = handoffCell; + argDecls.unshift({ + kind: "varDecl", + localId: cell.id, + init: { kind: "dynObjLit", fields: [], type: DYN, loc }, + loc, + }); + } const bindArgs = (call: IrExpr): IrExpr => argDecls.length === 0 ? call diff --git a/packages/compiler/src/frontend/lowering/lowerer.test.ts b/packages/compiler/src/frontend/lowering/lowerer.test.ts index 7fbb9661f..f36695234 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.test.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.test.ts @@ -239,7 +239,7 @@ test("optional union widening keeps refused and missing contracts current", () = unionId: lowerer.unions.recursiveRef(recursive, () => "Recursive"), }; expect(lowerer.withUndefinedArmOf(pending)).not.toBeNull(); - lowerer.unions.finalizeRecursive(recursive, [{ kind: "date" }, STRING]); + lowerer.unions.finalizeRecursive(recursive, [{ kind: "promise", inner: F64 }, STRING]); expect(lowerer.withUndefinedArmOf(pending)).toBeNull(); expect(lowerer.withUndefinedArmOf(pending)).toBeNull(); diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 4710ba7d9..46631a27e 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -1,9 +1,28 @@ +import { markInferredFactChecks, shapeOverlayFileNames } from "../shape-inference/index.js"; +import { npmStaticPackageOfPath } from "../npm-static.js"; +import { globalMemberValue } from "./global-values.js"; +import { unionNarrowFailureMessage } from "./union-narrow.js"; +import { + arrayClassFieldsFn, + arrayClassSpeciesFn, + arrayOfClassInstance, + arrayRootOf, + lowerWithArraySpecies, +} from "./builtins/array-classes.js"; +import { canonShadowHook } from "../canon-map.js"; +import { + compactDeclOrigins, + inPackageFile, + recordOrigin, + slotOrigin, + type RecordOrigin, +} from "./decl-origin.js"; import { isDeclBoundaryFile } from "../decl-projection.js"; import { relieveHeapPressure } from "../../heap-pressure.js"; -import { moduleNamespaceObject } from "./module-namespace-elements.js"; +import { moduleNamespaceObject, moduleNamespaceValue } from "./module-namespace-elements.js"; import { keyedHelperName } from "./stable-names.js"; import { positionTag, StructuralPositions } from "./structural-positions.js"; -import { onlyForwarded, stripTypeWrappers } from "./typed-arguments.js"; +import { bindingSymbolOfRead, onlyForwarded, stripTypeWrappers } from "./typed-arguments.js"; import { needsGeneralNodeStreams, noteUntypedStreamFlow, @@ -31,6 +50,7 @@ import { refuseUncheckedCycleReads, type CycleInitFlag, } from "./cycle-initialization.js"; +import { bindCompletionCallbacks } from "./completion-callbacks.js"; import { isJsonRequireDecl, type JsonRequireModule } from "./json-require.js"; import { checkRequireBindingAccesses, @@ -62,11 +82,13 @@ import { discoverWasmModules, type WasmModuleReport } from "../../wasm/discover. import { encodeBase64 } from "../../wasm/base64.js"; import { resolveRequireRuntime } from "../runtime-resolve.js"; import { lowerKeyObjectCall } from "./builtins/key-object.js"; +import { lowerX509Call, refuseX509DynValues } from "./builtins/x509.js"; import { isEcdhType, lowerEcdhCall } from "./builtins/ecdh.js"; import { isDiffieHellmanType, lowerDiffieHellmanCall } from "./builtins/dh.js"; import { fenceWebCryptoMember, lowerWebCryptoCall } from "./builtins/webcrypto.js"; import { valueStreamOf } from "./builtins/hash-stream.js"; import { lowerNetAddressCall, lowerNetAddressNew } from "./builtins/net-address.js"; +import { lowerMimeCall, lowerMimeNew } from "./builtins/mime.js"; import { dynUndefinedExpr, nodeThrowExpr, numLit, varRef } from "../../ir/build.js"; import { iteratorSlotAdapter } from "./iterator-adapters.js"; import { iterableSlotValue } from "./iterable-values.js"; @@ -114,7 +136,9 @@ import { checkedNarrowCall, checkedNarrowOperand, deferredReadHelper, + widthNarrowHelper, } from "./coercions/unions.js"; +import { assertedConversion } from "./coercions/asserted.js"; import { lowerUtilTypeValue, pathModuleValue, @@ -124,7 +148,7 @@ import { devirtualizeVirtualCalls } from "./devirtualize.js"; import { foldRegexSpanReads } from "./regex-span-reads.js"; import { RuntimeOptionalProvenance } from "./runtime-optional-provenance.js"; import { presentAtUse, reassignedBindings } from "./runtime-optional-guards.js"; -import { NarrowingStability } from "./narrowing-stability.js"; +import { NarrowingStability, widenedReceiver } from "./narrowing-stability.js"; import { ArrayElementStates } from "./runtime-optional-elements.js"; import { indexReadInBounds } from "./runtime-optional-bounds.js"; import { ArrayOwnership } from "./runtime-optional-ownership.js"; @@ -146,11 +170,19 @@ import { } from "./function-context.js"; import type { FrontendServices } from "../services.js"; import { InternalCompilerError } from "../../errors.js"; +import type { FsPathPlan } from "./builtins/fs-paths.js"; import { defaultAfterUndefined, lowerStaticallyUndefinedArgument } from "./optional-arguments.js"; import { type ProtocolDispatch } from "./class-protocol-dispatch.js"; -import { ClassDynamicDispatch, trackClassFieldCreation } from "./class-dynamic-dispatch.js"; -import { guardClassAttributeWrites } from "./class-attributes.js"; +import { + ClassDynamicDispatch, + boxClassInstancesExactly, + trackClassFieldCreation, +} from "./class-dynamic-dispatch.js"; +import { addUntypedIntegrityWords, guardClassAttributeWrites } from "./class-attributes.js"; +import { inferredCollectionType } from "./js-collection-types.js"; +import { guardFrozenWrites, markSloppyRecordWrites } from "./frozen-records.js"; import { applyHandleProperties } from "./handle-properties.js"; +import { installBufferValue } from "./containers/bytes.js"; import { ClassProtocols } from "./class-protocols.js"; import { earlyReadVarSymbols } from "./var-hoisting.js"; import { recordTextCodecClass } from "../../ir/ir.js"; @@ -166,7 +198,7 @@ import { import { DeferredModuleInitializers } from "./deferred-module-initializers.js"; import { finalizeClassMethodValues } from "./class-method-values.js"; import { finalizePrivateBrands } from "./private-brands.js"; -import { type ClassSymbolKey, tableKeyFieldType } from "./symbol-fields.js"; +import { type ClassSymbolKey, INSPECT_CUSTOM_MEMBER, tableKeyFieldType } from "./symbol-fields.js"; import type { HttpClientFnBinding } from "./lower-server.js"; export type { WidthLift } from "./width-lift.js"; export { newFnCtx, type FnCtx } from "./function-context.js"; @@ -192,7 +224,14 @@ import { isBundledShimForwarder } from "../package-shims.js"; import { erasedTypeOnlyImport } from "../type-only-imports.js"; import { literalValues } from "../literal-values.js"; import { discriminantOwners, literalUnionArm, type UnionLiteral } from "../union-discriminants.js"; -import type { ScrDiagnostic } from "../../diagnostics/diagnostic.js"; +import type { + ScrDiagnostic, + TypeFailure, + TypeFailureFamily, + TypeFailureSlot, +} from "../../diagnostics/diagnostic.js"; +import { declarationGroupKey, typeFailureOf } from "./type-failure.js"; +import { dynamicSlotDiag, type DynamicSlotEntry } from "../../diagnostics/dynamic-slots.js"; import { unsupportedAnyOperationDiag, blockedBindingUseDiag, @@ -261,12 +300,15 @@ import { type StartupCrash, cjsExportAssignmentOf, cjsExportDiscardReason, - fallbackDtsPath, + cjsExportsAssignedInFunctions, + cjsReexportedModules, + cjsNamespaceExportNames, isCheckedJsSourceFile, isCjsExportTableLiteral, isCjsJsFile, isJsSourceFile, isNodeEsmFile, + isFallbackDtsPath, isNodeTypesPath, locOf, orderedImportsOf, @@ -340,10 +382,13 @@ import { } from "./lower-modules.js"; import { prepareCjsModuleGraph, usesCjsTopLevelThis } from "./lower-node-module.js"; import { + type SplitRequireBinding, cjsNamespaceModule, cjsWholeExportValueOf, inlineNamespaceRequireOf, inlineProgramRequireOf, + splitRequireBindingOf, + splitWholeExportAliasTargetOf, } from "./cjs-inline-require.js"; import { tscInteropModuleOf } from "./cjs-tsc-interop.js"; import { usesCjsExportsIdentity } from "./cjs-exports-object.js"; @@ -352,6 +397,7 @@ import { type ClassInfo, type ClassMethodSignature, type ClassIteratorInfo, + type ClassIteratorOptions, type GenericClassInfo, registerBuiltinErrorClasses, defineClassValueConstruct, @@ -363,7 +409,9 @@ import { builtinStreamInfoOf, analyzeClassDecoration, classIteratorDrainCall, + classIteratorMethodCall, classIteratorNextCall, + classIteratorNextRead, classIteratorOf, classIteratorOpenCall, classIteratorRestDrainCall, @@ -423,6 +471,7 @@ import { collectSignatureInner, lambdaSignature, } from "./call-signatures.js"; +import { dynamicOverloadChoice, guessedOverloadIdentifier } from "./overload-choice.js"; import { implicitAnyParamSymbolsOf, implicitLocalFnNodeOf, @@ -438,10 +487,11 @@ import { genericLocalClassBinding, } from "./generic-functions.js"; import { bindingNeverReassigned } from "./binding-analysis.js"; +import { isMessageListenerCall } from "./expressions/message-listeners.js"; import type { InstantiationPath } from "./instantiation-path.js"; import { bodyReadsArguments } from "../arguments-usage.js"; import { completeArgs, wrappedUndefined, undefinedArgFor } from "./call-arguments.js"; -import { bodyReturnType, declaredReturnType } from "./function-returns.js"; +import { bodyReturnType, declaredReturnType, widensReducedRecords } from "./function-returns.js"; import { lowerCall, lowerPromiseMethodCall, lowerFilterNarrowCall } from "./lower-calls.js"; import { lowerFfiCall, validateFfiImports } from "./native-ffi.js"; import { @@ -558,6 +608,7 @@ import { lowerStaticReadableStreamCancelCall, lowerStaticReadableStreamControllerCall, lowerStaticReadableStreamNew, + lowerStaticTransformStreamNew, lowerStaticReadableStreamReaderCall, lowerStaticResponseCall, lowerStdlibMethodCall, @@ -580,7 +631,11 @@ import { lowerTestMethodCall, lowerTestCtxProperty, } from "./lower-test.js"; -import { lowerAssertModuleCall, lowerAssertDirectCall } from "./lower-assert.js"; +import { + assertFunctionValue, + lowerAssertModuleCall, + lowerAssertDirectCall, +} from "./lower-assert.js"; import type { AssertInstanceOptions } from "./assert-instances.js"; import { lowerUtilModuleCall } from "./lower-inspect.js"; import { lowerSystemErrorMapValue } from "./lower-system-errors.js"; @@ -675,6 +730,7 @@ import { maybePromiseUnionOf, promiseArmedUnionOf, } from "./maybe-promise.js"; +import { NodeIslandModuleCollector } from "../../node-islands/modules.js"; /** Entry function name. '%' cannot appear in a TS identifier, so a user * function can never collide with it (mangling is injective per prefix). */ @@ -686,7 +742,42 @@ interface GenericDemandOwner { classDemands: ClassInfo[]; } -export class PoisonError extends Error {} +/** A slot the localized-failure commit points turned into a checked + * dynamic slot (SC6104): where it is, what its declared type is, and the + * diagnostic it would have failed with. */ +export interface DynamicSlot { + loc: SrcLoc; + failure: TypeFailure; + code: string; + reason: string; +} + +/** Localized failure: rounds of the declaration flush retry. */ +const MAX_DECLARATION_RETRIES = 16; + +/** The slot kinds the declaration flush retry degrades (stage L2). */ +const DECLARATION_SLOTS: ReadonlySet = new Set([ + "field", + "param", + "return", + "local", + "global", + "loop", +]); + +export class PoisonError extends Error { + /** Set when a type with no static lowering poisoned a slot (badType and + * the dyn class field policy): what failed, for the localized-failure + * commit points. Report-only in stage L0. */ + readonly failure: TypeFailure | undefined; + /** The checker type that failed (with `failure`). */ + readonly failedType: ts.Type | undefined; + constructor(failure?: TypeFailure, failedType?: ts.Type) { + super(); + this.failure = failure; + this.failedType = failedType; + } +} /** A function-shaped value that exists only in the compiler's static * projection graph. Declared-function aliases point at the original @@ -801,6 +892,9 @@ export interface LowerResult { /** Performance warnings (SC61xx): the likely-hot functions the source * leaves untyped (lowering/dynamic-tier.ts). Never failures. */ performance?: ScrDiagnostic[]; + /** Localized failure: the checked dynamic slots the commit points + * created (each also an SC6104 in `performance`), present when any. */ + dynamicSlots?: DynamicSlotEntry[]; /** Functions lowering placed in the dynamic tier (ir/tiers.ts), present * when there are any. */ dynamicFunctions?: DynamicFunction[]; @@ -912,6 +1006,13 @@ export interface LowererMode { /** Symbols whose deferred diagnostics reachable emit already flushed — * the remainder must not report them a second time. */ alreadyFlushed?: ReadonlySet; + /** Localized failure: the types reachable emit degraded (the coverage + * remainder maps them alike, so the declarations reachable emit + * registered with dynamic slots collect there too). */ + degradedTypes?: ReadonlyMap; + /** Localized failure: the declarations reachable emit registered with + * dynamic slots (the coverage remainder registers them too). */ + retriedSymbols?: ReadonlySet; /** LowerOptions.externalTypes, threaded through every lowering pass. */ externalTypes?: ReadonlyMap; /** LowerOptions.externalTypeSpecifiersByFile, shared by every pass. */ @@ -1216,6 +1317,22 @@ export function lowerToIr( timing("dynamic-tier-scan", { found: hotDynamic === null ? 0 : 1 }); if (hotDynamic !== null) result = { ...result, performance: [hotDynamic] }; } + // Localized failure (SC6104): one performance warning per dynamic slot. + if (resultLowerer.dynamicSlots.length > 0) { + const dynamicSlots: DynamicSlotEntry[] = resultLowerer.dynamicSlots.map((slot) => ({ + loc: slot.loc, + name: slot.failure.name, + slot: slot.failure.slot, + declaredType: slot.failure.declaredType, + code: slot.code, + reason: slot.reason, + })); + result = { + ...result, + performance: [...(result.performance ?? []), ...dynamicSlots.map(dynamicSlotDiag)], + dynamicSlots, + }; + } // Contract types (@scriptc/types): written annotations, checked against // the IR by the compile pipeline (contracts/check.ts). // Acyclic object types name the IR unit the program lowered them to; a @@ -1303,6 +1420,8 @@ export function lowerToIr( reachable, remainder: true, alreadyFlushed: resultLowerer.flushedSymbols, + degradedTypes: resultLowerer.degradedTypes, + retriedSymbols: resultLowerer.retriedSymbols, inlineRequireCycleCalls, targetPlatform, ffiImports, @@ -1892,6 +2011,9 @@ function byName(fns: readonly IrFunction[]): IrFunction[] { ); } +/** The program module a createRequire binding requires. */ +type CreateRequireTarget = NonNullable>; + export class Lowerer { readonly frontendServices: FrontendServices | undefined; readonly checker: ts.TypeChecker; @@ -2208,6 +2330,23 @@ export class Lowerer { * the body needs — indexed accesses (`T[K]` needs K's literal key) and * keyed record reads (`o[k]` where k's type is a literal-bound K). */ typeParamTsBindings: Map | null = null; + /** Generic alias references of call-keyed instances' declared signatures + * (`Catalog`), each with the same alias as every call's resolved + * signature instantiates it (`Catalog<{ g1: … }, "acme">`), together with + * that instance's ts-level bindings map (typeParamTsBindings), which every + * scope that lowers the instance installs. The checker keeps the alias + * symbolic inside the generic body and offers no way to instantiate it + * with the bound arguments, but alias instantiations are interned: the + * body's `x as Catalog` is the very type the signature declares, so + * the pair recorded during unification is that re-instantiation. */ + readonly boundAliasInstances = new Map< + ts.Type, + { tsBindings: Map; instance: ts.Type }[] + >(); + /** Values an erased type assertion produced, with the asserted type + * (`x as T` keeps x's representation): a slot that no ordinary + * conversion reaches converts them as asserted (assertedConversion). */ + readonly assertedValues = new Map(); /** Non-null while an IMPLICIT-ANY instance body lowers (npm-static JS; * see generic-functions.ts): bound param symbol → * the call site's checker type, consulted by typeOf for identifier @@ -2287,6 +2426,14 @@ export class Lowerer { /** Per CommonJS file using top-level `this`: the global holding the * object that stands for its original exports (lowerCjsTopLevelThis). */ readonly cjsExportsThis = new Map(); + /** `var m; m = require("./x")` alias bindings (cjs-inline-require.ts). + * Null entries cache declarations that keep ordinary storage. */ + readonly cjsSplitRequireBindings = new Map(); + /** Per-module whole-export symbol (cjs-inline-require.ts). Null means the + * module's exports are a table. */ + readonly cjsWholeExportSymbols = new Map(); + /** Per-module live member-export classification (cjs-inline-require.ts). */ + readonly cjsLiveMemberExports = new Map(); /** Inline program-module require calls whose %init the enclosing member * read or member call is already sequencing (cjs-inline-require.ts). */ readonly inlineRequireInitDone = new Set(); @@ -2337,6 +2484,13 @@ export class Lowerer { readonly runtimeOptionalFields = new Set(); /** Bindings initialized by a Date's `toJSON()` (analyzeDateJsonResults). */ readonly dateJsonBindings = new Set(); + /** isTimeValueDateDeclaration's answers, and the locals that hold a + * Date's time value instead of a Date object (date-values.ts). */ + readonly timeValueDateDecisions = new Map(); + readonly timeValueDateLocals = new Set(); + /** Declarations of those locals that a setter call writes + * (date-values.ts). */ + readonly timeValueDateWrites = new Set(); /** Record bindings whose flow class reaches storage outside the class: * they keep the declared layout, so an initializer from unchecked reads * converts once instead of the binding adopting the widened variant. */ @@ -2345,6 +2499,9 @@ export class Lowerer { * every name destructured from a present source optional. */ readonly runtimeOptionalPatternTypes = new Map(); readonly runtimeOptionalReduceTypes = new Map(); + /** Each function declaration's `arguments.length` count slot, or null + * (argument-counts.ts argumentCountSlot). */ + readonly argumentCountSlots = new Map(); /** Non-null assertions whose value is forwarded into a slot promoted to * hold undefined. `x!` has no runtime effect, so these keep the stored * undefined state instead of trapping before any dereference. */ @@ -2356,6 +2513,11 @@ export class Lowerer { /** for-of loops over arrays that can hold neither holes nor undefined: * each pass reads the element payload directly. */ readonly presentElementLoops = new Set(); + /** Element keys (typeKey of the element type) of native arrays that can + * hold elements outside their element type, stored through dynamic views + * (SCR_ARR_FOREIGN); "*" for every array. Empty unless the program stores + * values through a dynamic value that can view a native array. */ + foreignElementKeys: ReadonlySet = new Set(); /** The fs/promises glob() call lowerForOf is lowering as a for await * head; other glob() calls have no lowering. */ fspGlobHead: ts.Node | null = null; @@ -2414,6 +2576,69 @@ export class Lowerer { return fallback ?? null; } + /** An object or array literal in an untyped context: its contextual type + * is `unknown` or `any` from a slot the program declares (a variable, + * field, parameter, return type or `unknown[]` element; a library + * parameter typed `any` only through a type argument), it is thrown, or + * it is a member of such a literal, and it holds data only (no methods, + * accessors or function members). Its value is built as a dynamic + * object: no type the program wrote gives it a native layout, and a later + * checked cast adopts it (dyn.adoptShared), so every holder keeps one + * identity. A library parameter the declarations type `any` or `unknown` + * itself (`console.log`, `JSON.stringify`) only reads the value and keeps + * the native literal. */ + untypedLiteralContext(x: ts.Expression): boolean { + if (isJsSourceFile(ts.getSourceFileOfNode(x))) return false; + // Data only: a literal with methods, accessors or function members is an + // object with behavior, which keeps its native layout (and boxed form). + if ( + ts.isObjectLiteralExpression(x) && + x.properties.nodes.some((p) => { + if (!ts.isPropertyAssignment(p)) + return !ts.isShorthandPropertyAssignment(p) && !ts.isSpreadAssignment(p); + let init: ts.Expression = p.initializer; + while (ts.isParenthesizedExpression(init)) init = init.expression; + return ( + ts.isFunctionExpression(init) || ts.isArrowFunction(init) || ts.isClassExpression(init) + ); + }) + ) + return false; + let node: ts.Node = x; + while (node.parent !== undefined && ts.isParenthesizedExpression(node.parent)) + node = node.parent; + const parent = node.parent; + if (parent === undefined) return false; + if (ts.isThrowStatement(parent)) return true; + if (ts.isPropertyAssignment(parent) && parent.initializer === node) { + const literal = parent.parent; + if (ts.isObjectLiteralExpression(literal)) return this.untypedLiteralContext(literal); + } + if (ts.isArrayLiteralExpression(parent) && this.untypedLiteralContext(parent)) return true; + const contextual = this.checker.getContextualType(x); + if (!contextual || !(contextual.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown))) + return false; + if (ts.isCallExpression(parent) || ts.isNewExpression(parent)) { + const signature = this.checker.getResolvedSignature(parent); + const decl = signature?.declaration; + if (decl && this.isStdlibFile(ts.getSourceFileOfNode(decl))) { + const index = (parent.arguments?.nodes ?? []).indexOf(node as ts.Expression); + const params = (decl as { parameters?: { nodes: readonly ts.ParameterDeclaration[] } }) + .parameters?.nodes; + const param = params?.[Math.min(index, params.length - 1)]; + let annotation = param?.type; + if (annotation && ts.isArrayTypeNode(annotation)) annotation = annotation.elementType; + if ( + !annotation || + annotation.kind === ts.SyntaxKind.AnyKeyword || + annotation.kind === ts.SyntaxKind.UnknownKeyword + ) + return false; + } + } + return true; + } + /** A fresh TypeScript data literal (`{ message: "m" }`) passed to a * checked-dynamic parameter of a callee implemented in JavaScript, whose * contract lets it reshape the object: a call through an untyped value, an @@ -2821,6 +3046,11 @@ export class Lowerer { /** Parameters read as `any` in the error-path copy of an error-first * callback. */ readonly anySymbols = new Set(); + /** The collection types that uses prove for untyped JavaScript bindings, + * by every identifier naming one (js-collection-types.ts). */ + inferredCollectionTypes: Map | undefined = undefined; + /** overload-choice's answers per call, binding and function. */ + readonly dynamicOverloadAnswers = new Map(); /** Error parameters of an error-first callback with a known value in * the copy being lowered: true while it holds an Error, false while it * holds null (staticCondition folds conditions on them). */ @@ -2896,6 +3126,9 @@ export class Lowerer { /** util.inspect's per-type traversal helpers (%util.insp.N), interned * by typeKey — the assertHelpers pattern with its own namespace. */ readonly inspectHelpers = new Map(); + /** Indexed-union safe-read helpers mapped to their numeric twins, for the + * fusion of an immediate ToNumber (tryLowerNumericIndexRead). */ + readonly indexedUnionNumericTwins = new Map(); private readonly unionLiteralOwners = new Map< string, { owners: Map | null; shapes: number; unions: number } @@ -2935,6 +3168,10 @@ export class Lowerer { * checks resolve through this map (stdlibGlobalNameOf), declarations * emit nothing. */ readonly stdlibGlobalAliases = new Map(); + /** The stdlibGlobalAliases bound through a cast of the global object + * (`(globalThis as { Buffer?: B }).Buffer`). Their static type is the + * cast's, so their value reads are refused (identifiers.ts). */ + readonly castGlobalAliases = new Set(); /** CJS export-table ACCESSORS (`module.exports = { get path() {...} }`), * lifted lazily as module-level functions and interned per accessor * declaration: member reads call the getter (lower-exprs). */ @@ -2969,6 +3206,9 @@ export class Lowerer { readonly builtinCallableValueFns = new Map(); /** Each builtin function-value adapter's Node name, for boxed values. */ readonly builtinCallableValueNames = new Map(); + /** Builtin function values whose `length` differs from their adapter's + * parameter count (BuiltinModuleFn.valueLength), by adapter name. */ + readonly builtinCallableValueLengths = new Map(); /** Optional-chain lowering state. While a chain body lowers, the guarded * receiver NODE reads as a chainRecv (typed by the narrowed arm) instead * of re-lowering, its checker type reads non-nullish (typeOf), and the @@ -3021,6 +3261,23 @@ export class Lowerer { /** Named method slots observed before class collection need a callable * ABI and shared prototype descriptors instead of call-site specialization. */ readonly prototypeMethodAccesses = new Map(); + /** Every site the census found for a name in prototypeMethodAccesses (which + * keeps one): reads of the method and writes that may replace it. The + * key-order witness asks which replacements can return records. */ + readonly methodAccessSites = new Map(); + + /** Marks `name` as a method the program may read or replace at `site`. */ + noteMethodAccess(name: string, site: ts.Node): void { + this.prototypeMethodAccesses.set(name, site); + this.noteMethodSite(name, site); + } + + /** Records a site for `name` without changing its prototypeMethodAccesses entry. */ + noteMethodSite(name: string, site: ts.Node): void { + const sites = this.methodAccessSites.get(name); + if (sites === undefined) this.methodAccessSites.set(name, [site]); + else sites.push(site); + } /** Whether the program reads `Error.prototype`, the checked-dynamic * object an ES5 constructor function can inherit from, or has a class * whose heritage is evaluated at run time (`extends (Base ?? Error)`): @@ -3030,6 +3287,9 @@ export class Lowerer { /** The property names the program assigns on the global object * (property-access.ts), collected on first use. */ globalThisWrittenNames: Set | null = null; + /** Set with globalThisWrittenNames: the program writes the global object + * by a computed name somewhere. */ + globalThisComputedWrite = false; /** Well-known symbol methods (`sym:iterator`) the program may replace * through a symbol-keyed write: an instance or prototype property, or a * reflection call. */ @@ -3044,6 +3304,8 @@ export class Lowerer { /** Inferred JS methods participating in an override chain keep a vtable * ABI instead of call-site specialization. Filled before class collection. */ readonly virtualJsMethods = new Set(); + /** Methods of checked-JavaScript override pairs (vtable dispatch). */ + readonly checkedJsOverrideMethods = new Set(); /** Descendant writes must widen the original JS field before collecting * any class layout; base and derived instances share its storage ABI. */ readonly inheritedJsFieldWrites = new Map< @@ -3116,8 +3378,21 @@ export class Lowerer { * (builtins/fs-paths.ts): each lowers to the hidden local that holds its * value, which is null until the path's lowering has saved it. */ readonly stagedArguments = new Map(); - /** The staged arguments that follow each such URL path, in order. */ - readonly stagedAfterPath = new Map(); + /** Closure arguments an implicit-any call already lowered to learn their + * type (storedImplicitArgumentType), by node: the argument's own lowering + * in the same function and with the same type bindings takes that value + * once instead of lowering the closure again. */ + readonly speculatedClosures = new Map< + ts.Expression, + { ctx: FnCtx; bindings: Map | null; value: IrExpr } + >(); + /** What each fs path argument of a URL-path call lowers to (the plans + * of builtins/fs-paths.ts). */ + readonly fsPathPlans = new Map(); + /** The existsSync/fs.promises calls whose converted URL paths are being + * lowered into the call itself (lowerFallibleFsUrlCall). A call may be + * lowered more than once, and each lowering converts again. */ + readonly fallibleFsCalls = new Set(); readonly moduleNsParamOverrides = new Map< ts.ParameterDeclaration, IrType & { kind: "moduleNs" } @@ -3140,6 +3415,16 @@ export class Lowerer { /** The program calls into Node.js (@scriptc/node): the module carries * the nodeIsland record, which the executable build fills in. */ usesNodeIsland = false; + /** The modules Node island calls load (lower-node-island.ts). */ + readonly nodeIslandModules = new NodeIslandModuleCollector(); + /** Each call's caller literal, set to its tree path when lowering ends. */ + readonly nodeIslandCallers: { literal: IrExpr & { kind: "strLit" }; caller: string }[] = []; + /** Absolute specifiers, rewritten to the shipped file relative to their caller. */ + readonly nodeIslandSpecifiers: { + literal: IrExpr & { kind: "strLit" }; + caller: string; + target: string; + }[] = []; /** Per-directory answers of experimental-import.ts's declaration-file test. */ readonly experimentalFiles = new Map(); /** Interned LifetimeRegion.run helpers, by callback type key. */ @@ -3219,10 +3504,9 @@ export class Lowerer { * tag. */ readonly unions = new UnionRegistry(); /** Context-free successful type mappings. See TypeMapperCtx.typeMemo. */ - readonly typeMemo = new Map(); + readonly typeMemo = new Map(); readonly ambient = ambientDtsPath(); readonly overridesAmbient = overridesDtsPath(); - readonly fallbackAmbient = fallbackDtsPath(); /** The one mapType context: registries + hooks, assembled in the * constructor (typeParamResolver reads the CURRENT instantiation bindings * through `this`, so the same ctx serves generic bodies too). */ @@ -3244,7 +3528,19 @@ export class Lowerer { // Stack of function contexts (bottom = the function being declared at // top level, top = the innermost nested function currently lowering). fnStack: FnCtx[] = []; + /** Source ranges of record writes in sloppy-mode code, and of the + * absent-state writes of `delete r.f` (markSloppyRecordWrites). */ + readonly sloppyRecordWrites = new Set(); + readonly recordFieldDeletes = new Set(); + /** typeKeys of the array types the program may freeze (IrModule.freezableArrays). */ + readonly freezableArrays = new Set(); + /** Whether the program freezes, seals or prevents extensions of untyped + * values other than fresh literals (noteUntypedIntegrity). */ + untypedIntegrity = false; readonly liftedFns: IrFunction[] = []; + /** global-values.ts's answer per JavaScript source: the receiver of its + * first read of the global object by a computed name, or null. */ + readonly computedGlobalReads = new Map(); /** The first lifted function of each name (liftedFnNamed), for the * liftedFns indexed so far. liftedFns only grows. */ private readonly liftedFnsByName = new Map(); @@ -3441,6 +3737,7 @@ export class Lowerer { this.internTableHelpers, this.coercions.retags, this.coercions.narrows, + this.coercions.widthNarrows, ]) for (const name of table.values()) interned.add(name); for (const fn of [...this.liftedFns, ...this.implicitFns]) @@ -3582,6 +3879,53 @@ export class Lowerer { * the fallthroughs report the inherited-blocker cascade (SC2004) * instead of misattributing the reference. */ readonly blockedBindings = new Set(); + /** Localized failure, report-only: the failure group (type-failure.ts) + * of each declaration whose rejection reached the build — a flushed + * deferred declaration, or a binding of a poisoned declaration + * statement. */ + readonly rejectedGroups = new Map(); + /** One frame per statement lowerStmts is lowering: the failure groups + * its lowering touched. A statement that then poisons attributes its + * diagnostics to them (ScrDiagnostic.cause). */ + readonly failureTouches: Set[] = []; + /** Localized failure: checker types (by id) whose strict mapping failed + * at a committed slot, with their lenient mapping. Filled only at the + * commit points (degradeAtCommit), so a program that compiles today + * never fills it. */ + readonly degradedTypes = new Map(); + /** The dynamic slots the commit points created: one SC6104 each. */ + readonly dynamicSlots: DynamicSlot[] = []; + /** Declarations retryDeferred registered. */ + readonly retriedSymbols = new Set(); + /** LowererMode.retriedSymbols. */ + readonly priorRetriedSymbols: ReadonlySet; + /** Bodies run() skipped because their declaration had not registered. */ + readonly skippedBodies = new Set(); + /** Declarations retryDeferred registered, in order: run() lowers the + * bodies it had skipped. */ + readonly retriedDeclarations: ts.Node[] = []; + /** Diagnostics of lowering that a statement's retry does not repeat (a + * function-local class's member bodies, lowered once and kept): + * dropAttemptDiags keeps them. */ + readonly loweredOnceDiags = new Set(); + /** Deferred classes whose static field initializers' place in %init + * has been lowered (retryDeferredAtDeclaration): a later flush must not + * register them, since their initializers would never run. */ + readonly staticInitsPassed = new Set(); + /** Deferred declarations' collections, for the flush retry + * (retryDeferred). */ + readonly deferredCollectors = new Map< + ts.Symbol, + { symbolOf: () => ts.Symbol | undefined; collect: () => void } + >(); + /** True while a flush retry collects a declaration: the dyn class + * field policy (dynFieldPolicyFence) is lifted for reached classes. */ + liftDynFieldPolicy = false; + /** Collection diagnostics that a flush retry may simply run again (a + * derived class whose base had not registered yet). */ + readonly retriableDiags = new Set(); + /** The checker type behind each type failure badType committed. */ + readonly failedTypes = new Map(); /** True while collectProgram runs: resolution helpers must not flush * deferred diagnostics (collection itself resolves symbols — extends * clauses — and collection order must not decide what reports). */ @@ -3635,23 +3979,91 @@ export class Lowerer { /** Bumps the per-file attribution counter (no-op unless provenance is * active and this is the emit/discovery lane — mirror the CALLER's * suppressStats guard, this method only gates remainder). */ - bumpFileStat(file: string, kind: "total" | "failed"): void { + bumpFileStat(file: string, kind: "total" | "failed", by = 1): void { if (this.remainder || !provenanceActive()) return; let s = this.statsByFile.get(file); if (!s) this.statsByFile.set(file, (s = { total: 0, failed: 0 })); - if (kind === "total") s.total += 1; - else if (kind === "failed") s.failed += 1; + if (kind === "total") s.total += by; + else if (kind === "failed") s.failed += by; } /** The program holds the declaration-typed boundary's projection * (decl-projection.ts): user-file record and array values crossing into * dynamic slots (package parameters) box as live capsules, so a value * that comes back through a boundary cast keeps its identity. */ + /** Origins of package-declared types checked in this program + * (decl-origin.ts), by IrModule.declOrigins index. */ + readonly declOrigins: string[] = []; + private readonly declOriginIds = new Map(); + private readonly packageFileMemo = new Map(); + private readonly slotOriginMemo = new Map(); + /** The value whose slot coercion is being lowered, while that value is + * in a package file: union extractions built for it carry the slot's + * origin in their TypeError. */ + coercionOriginNode: ts.Node | null = null; + /** Per user file, each declaration-boundary cast's index in source + * order (narrowing.ts recordedBoundaryOrigin). */ + readonly boundaryCastOrder = new Map>(); + /** The union type of that value: only its own extraction is the slot's. */ + coercionOriginUnion: string | null = null; + /** While a record projection into a package-declared type builds, the + * origin of the field being converted. */ + coercionFieldOrigin: string | null = null; + private readonly recordOriginMemo = new Map(); + + /** The package-declared record type the current coercion converts to. */ + coercionRecordOrigin(): RecordOrigin | null { + const node = this.coercionOriginNode; + if (node === null) return null; + let origin = this.recordOriginMemo.get(node); + if (origin === undefined) { + try { + origin = recordOrigin(this.checker, node); + } catch { + origin = null; + } + this.recordOriginMemo.set(node, origin); + } + return origin; + } + + declOriginIndex(origin: string): number { + let id = this.declOriginIds.get(origin); + if (id === undefined) { + id = this.declOrigins.length; + this.declOrigins.push(origin); + this.declOriginIds.set(origin, id); + } + return id; + } + + /** The origin of the package-declared slot a value flows into. */ + slotOriginOf(node: ts.Node): string | null { + let origin = this.slotOriginMemo.get(node); + if (origin === undefined) { + try { + origin = slotOrigin(this.checker, node); + } catch { + origin = null; + } + this.slotOriginMemo.set(node, origin); + } + return origin; + } + + private inPackageFileOf(node: ts.Node): boolean { + const sf = ts.getSourceFileOfNode(node); + let inside = this.packageFileMemo.get(sf); + if (inside === undefined) { + inside = inPackageFile(node); + this.packageFileMemo.set(sf, inside); + } + return inside; + } + private declBoundaryMemo: boolean | null = null; get declBoundary(): boolean { - this.declBoundaryMemo ??= this.program - .getSourceFiles() - .some((sf) => isDeclBoundaryFile(sf.fileName)); + this.declBoundaryMemo ??= this.program.getSourceFileNames().some(isDeclBoundaryFile); return this.declBoundaryMemo; } /** Tracked classes' bag initializers over a typed `this` @@ -3666,6 +4078,9 @@ export class Lowerer { ) { // The mode's options, sorted by name; the values derived from them follow. this.alreadyFlushed = mode.alreadyFlushed ?? new Set(); + if (mode.degradedTypes) + for (const [id, degraded] of mode.degradedTypes) this.degradedTypes.set(id, degraded); + this.priorRetriedSymbols = mode.retriedSymbols ?? new Set(); this.externalTypes = mode.externalTypes ?? new Map(); this.ffiBindingSymbols = mode.ffiBindingSymbols ?? null; this.ffiImports = mode.ffiImports ?? []; @@ -3692,14 +4107,21 @@ export class Lowerer { this.externalTypeSpecifiersByFile = mode.externalTypeSpecifiersByFile ?? directExternalTypeSpecifiersByFile(this.externalTypes); this.checker = program.getTypeChecker(); + const shadow = canonShadowHook(this.checker); this.typeCtx = { checker: this.checker, positionOf: (node) => this.positions.of(node), shapes: this.shapes, unions: this.unions, - classNamer: this.classNamer, + classNamer: (decl) => this.classNamer(decl), resolveTypeParam: this.typeParamResolver, resolveTypeParamTs: this.typeParamTsResolver, + resolveBoundAliasTs: this.boundAliasTsResolver, + degradedType: (type) => { + const id = (type as { id?: number }).id; + return id === undefined ? null : (this.degradedTypes.get(id)?.lenient ?? null); + }, + ...(shadow !== undefined ? { canonShadow: shadow } : {}), isErasing: () => this.erasingDepth > 0, genericClassInstance: (decl, ref, mapArgument) => this.genericClassInstanceType(decl, ref, mapArgument), @@ -3778,8 +4200,8 @@ export class Lowerer { }, mixinIntersectionInstance: (widened) => mixinIntersectionInstanceType(this, widened), classImplementedProtocol: (type) => this.classImplementedProtocol(type), - isStdlibFile: this.isStdlibFile, - isNpmFile: this.isNpmFile, + isStdlibFile: (sf) => this.isStdlibFile(sf), + isNpmFile: (sf) => this.isNpmFile(sf), // The declaration-typed boundary's generated types (decl-projection.ts) // describe data, like an explicitly mapped external type surface. isExternalTypeFile: (sf) => @@ -3851,7 +4273,8 @@ export class Lowerer { /** Declared class fields may store a DYN value only when its static type * is `unknown` or `object` (also with `null` or `undefined`, or an array * of those), names checked protocol identity, is an owned runtime - * handle (a Worker or MessagePort, a fetch Headers/Request/Response, or + * handle (a Worker or MessagePort, a fetch Headers/Request/Response or + * AbortController/AbortSignal, or * a node:crypto ECDH, DiffieHellman or DiffieHellmanGroup), or is a * WeakMap/WeakSet (native weak collections live in checked-dynamic * storage, as locals of those types do); `any` fields stay refused. */ @@ -3939,6 +4362,42 @@ export class Lowerer { return null; } + /** For each CommonJS module read through a JavaScript namespace import: + * the property names of Node's namespace object + * (cjsNamespaceExportNames), and the exports the module assigns inside + * functions, which the namespace does not see + * (cjsExportsAssignedInFunctions). */ + private readonly cjsNamespaces = new Map< + ts.SourceFile, + { names: ReadonlySet; assignedLater: ReadonlySet | "all" } + >(); + + cjsNamespaceOf(dep: ts.SourceFile): { + names: ReadonlySet; + assignedLater: ReadonlySet | "all"; + } { + let namespace = this.cjsNamespaces.get(dep); + if (namespace === undefined) { + // A re-exported name reads the module that assigns it, so the writes + // after evaluation of every re-exported module count too. + let assignedLater: Set | "all" = new Set(); + for (const mod of cjsReexportedModules(this.program, dep)) { + const later = cjsExportsAssignedInFunctions(mod); + if (later === "all") { + assignedLater = "all"; + break; + } + for (const name of later) assignedLater.add(name); + } + namespace = { + names: cjsNamespaceExportNames(this.program, dep), + assignedLater, + }; + this.cjsNamespaces.set(dep, namespace); + } + return namespace; + } + sourceFileOfModuleNamespace(type: IrType): ts.SourceFile | null { if (type.kind !== "moduleNs" || !type.moduleId.startsWith("file:")) return null; const path = type.moduleId.slice("file:".length); @@ -4098,6 +4557,11 @@ export class Lowerer { // registry (globals, fn signatures, classes) applies unchanged. const cjsValue = this.cjsExportValueSymbol(symbol); if (cjsValue) symbol = cjsValue; + // `var m; m = require("./x"); module.exports = m` over a whole-export + // target: TS types the wrapper's export= as `any`, so reconnect it to + // the target's value symbol explicitly. + const splitAlias = splitWholeExportAliasTargetOf(this, symbol); + if (splitAlias) symbol = splitAlias; this.flushDeferred(symbol); return symbol; } @@ -4214,7 +4678,7 @@ export class Lowerer { // exact mappings, while star exports resolve to the mapped declaration // owner through externalTypeSpecifiersByFile. const moduleSymbol = this.checker.getSymbolAtLocation(sf); - const exported = moduleSymbol?.getExports().get(exportName as ts.__String); + const exported = moduleSymbol?.getExports().get(ts.escapedSymbolName(exportName)); return this.externalTypeSpecifierOfSymbol(exported, seenSymbols, seenExports); } @@ -4357,7 +4821,7 @@ export class Lowerer { cjsModuleExportSymbol(sf: ts.SourceFile, name: string): ts.Symbol | undefined { const moduleSym = this.checker.getSymbolAtLocation(sf); const exports = moduleSym?.getExports(); - const direct = exports?.get(name as ts.__String); + const direct = exports?.get(ts.escapedSymbolName(name)); if (direct) return direct; // tsgo represents `module.exports = { ... }` as one `export=` symbol // and does not duplicate the object literal's properties into the @@ -4491,8 +4955,8 @@ export class Lowerer { const decl = decls.find(ts.isImportClause) ?? decls[0]; if (!decl) return null; if (ts.isImportClause(decl)) return this.cjsDefaultImportDepOf(decl); - if (!ts.isVariableDeclaration(decl) || !ts.isIdentifier(decl.name) || !decl.initializer) - return null; + if (!ts.isVariableDeclaration(decl) || !ts.isIdentifier(decl.name)) return null; + if (decl.initializer === undefined) return splitRequireBindingOf(this, decl)?.dep ?? null; { const interop = tscInteropModuleOf(this, expr); if (interop !== null) return cjsNamespaceModule(this, interop.dep) ? interop.dep : null; @@ -4508,6 +4972,55 @@ export class Lowerer { return createRequireProgramModuleOf(this, decl.initializer)?.dep ?? null; } + private readonly createRequireBindingTargets = new Map(); + private readonly createRequireElementTargets = new Map< + ts.Symbol, + { element: ts.BindingElement; target: CreateRequireTarget } | null + >(); + + /** The program module a `const x = createRequire(...)("./m")` binding + * requires, by the binding's symbol; null for every other binding. Every + * reference resolves it (resolveValueSymbol), so it is cached. */ + private createRequireBindingTarget(binding: ts.Symbol): CreateRequireTarget | null { + const cached = this.createRequireBindingTargets.get(binding); + if (cached !== undefined) return cached; + const decl = binding.declarations.find(ts.isVariableDeclaration); + const target = + decl && ts.isIdentifier(decl.name) && decl.initializer + ? createRequireProgramModuleOf(this, decl.initializer) + : null; + this.createRequireBindingTargets.set(binding, target); + return target; + } + + /** The binding element and required program module of a + * `const { m } = createRequire(...)("./m")` binding, by the binding's + * symbol; null for every other binding. Cached like + * createRequireBindingTarget. */ + private createRequireElementTarget( + binding: ts.Symbol, + ): { element: ts.BindingElement; target: CreateRequireTarget } | null { + const cached = this.createRequireElementTargets.get(binding); + if (cached !== undefined) return cached; + let found: { element: ts.BindingElement; target: CreateRequireTarget } | null = null; + const element = binding.declarations.find(ts.isBindingElement); + if ( + element && + element.name !== undefined && + ts.isIdentifier(element.name) && + ts.isObjectBindingPattern(element.parent) + ) { + const declaration = element.parent.parent; + const target = + ts.isVariableDeclaration(declaration) && declaration.initializer + ? createRequireProgramModuleOf(this, declaration.initializer) + : null; + if (target !== null) found = { element, target }; + } + this.createRequireElementTargets.set(binding, found); + return found; + } + /** The represented single value behind a createRequire binding, or null * when the target is a namespace/table module. Collection has already * registered executable exports before entry-module uses lower, so the @@ -4515,9 +5028,7 @@ export class Lowerer { * static storage or callable/class metadata. */ private createRequireWholeExportSymbolOf(ident: ts.Identifier): ts.Symbol | null { const binding = this.checker.getSymbolAtLocation(ident); - const decl = binding ? binding.declarations.find(ts.isVariableDeclaration) : undefined; - if (!decl || !ts.isIdentifier(decl.name) || !decl.initializer) return null; - const target = createRequireProgramModuleOf(this, decl.initializer); + const target = binding ? this.createRequireBindingTarget(binding) : null; if (target === null) return null; const moduleSym = this.checker.getSymbolAtLocation(target.dep); let symbol = moduleSym?.getExports().get("export=" as ts.__String); @@ -4562,18 +5073,9 @@ export class Lowerer { * target module's real export symbol. */ private createRequireDestructuredExportSymbolOf(ident: ts.Identifier): ts.Symbol | null { const binding = this.checker.getSymbolAtLocation(ident); - const element = binding ? binding.declarations.find(ts.isBindingElement) : undefined; - if ( - !element || - element.name === undefined || - !ts.isIdentifier(element.name) || - !ts.isObjectBindingPattern(element.parent) - ) - return null; - const declaration = element.parent.parent; - if (!ts.isVariableDeclaration(declaration) || !declaration.initializer) return null; - const target = createRequireProgramModuleOf(this, declaration.initializer); - if (target === null) return null; + const found = binding ? this.createRequireElementTarget(binding) : null; + if (found === null) return null; + const { element, target } = found; const property = element.propertyName; const name = property && (ts.isIdentifier(property) || ts.isStringLiteralLike(property)) @@ -4597,6 +5099,9 @@ export class Lowerer { // a binding (cjs-inline-require.ts sequences the module's %init). if (!ts.isIdentifier(expr)) return this.cjsModuleRefDepOf(expr) !== null; const sym = this.checker.getSymbolAtLocation(expr); + const splitDecl = sym?.declarations.find(ts.isVariableDeclaration); + const split = splitDecl === undefined ? null : splitRequireBindingOf(this, splitDecl); + if (split !== null) return cjsNamespaceModule(this, split.dep); if (this.localModuleBindingDepOf(expr) === null) return false; if (this.createRequireWholeExportSymbolOf(expr) !== null) return false; // SINGLE-VALUE exporters (`module.exports = Countdown` / `= double` / @@ -4687,7 +5192,14 @@ export class Lowerer { } collectProgram(parts: FileParts[]): void { - return collectProgram(this, parts); + collectProgram(this, parts); + // The coverage remainder registers the declarations reachable emit + // registered with dynamic slots (retryDeferred), so it neither reports + // them as unreached nor blocks their uses. + for (const symbol of this.priorRetriedSymbols) { + const diags = this.deferredDiags.get(symbol); + if (diags !== undefined) this.retryDeferred(symbol, diags); + } } /** Names every file's %init and registers the run-once guard globals @@ -4904,6 +5416,13 @@ export class Lowerer { * their external dependencies by name). Null for everything else * (builtins load nothing; the rest kept its preflight fence). */ requireInitStmt(spec: string, node: ts.Node): IrStmt | null { + if (this.inlineRequireCycleCalls.has(node)) { + this.unsupported( + "SC1090", + node, + `require() of '${spec}' while it is still evaluating (a require cycle: Node answers its partial exports, which the static export table does not model)`, + ); + } if (canonicalBuiltinModule(spec) === "punycode") return this.punycodeRequireWarning(node); // Project requires resolve within the program; another bare require can // be a program-module edge when it names an opted-in --npm-static package @@ -5072,11 +5591,11 @@ export class Lowerer { const sourceFiles = parts.map((p) => p.sf); const fnDecls = parts.flatMap((p) => p.fnDecls); // This fixed-point scan intentionally sees deferred bodies before - // reachability is known. Batch their binding queries through the - // facade's panic fence, then keep lookup free of lowering side effects: - // resolveValueSymbol also flushes deferred diagnostics and applies - // merged-namespace fences, whose authority remains ordinary lowering. - this.checker.prefetchSymbolRoots(sourceFiles, true); + // reachability is known. Its binding queries were batched through the + // facade's panic fence before collection (prefetchProgramSymbols); keep + // lookup free of lowering side effects: resolveValueSymbol also flushes + // deferred diagnostics and applies merged-namespace fences, whose + // authority remains ordinary lowering. // The facts each fixed-point site reads, by the symbols, nodes and // element keys they belong to (created with the site list below). let siteDependencies: SiteDependencies | null = null; @@ -5673,6 +6192,22 @@ export class Lowerer { }; if (!stringRead(e.left) && !stringRead(e.right)) return null; if (!primitive(e.left) || !primitive(e.right)) return null; + // A missing read is undefined, and `+` yields a number only when + // neither operand is a string: an operand that is always a string + // (`"x-" + PAD[i]`) makes the result a string. Only literals, + // templates and sums with such an operand are always strings; a + // binding or call can hold a missing read. + const definitelyString = (part: ts.Expression): boolean => { + const p = peel(part); + if (ts.isStringLiteral(p) || ts.isNoSubstitutionTemplateLiteral(p)) return true; + if (ts.isTemplateExpression(p)) return true; + return ( + ts.isBinaryExpression(p) && + p.operatorToken.kind === ts.SyntaxKind.PlusToken && + (definitelyString(p.left) || definitelyString(p.right)) + ); + }; + if (definitelyString(e.left) || definitelyString(e.right)) return null; return { kind: "union", unionId: this.unions.intern([F64, STRING]) }; }; /** The union of primitive result types, or null when any is missing or @@ -6328,21 +6863,36 @@ export class Lowerer { let dynamicViews: Set | null = null; // Element ABIs of the native arrays that an `any` or `unknown` value is // converted to, including arrays nested in the converted value. + // Assignments, including the logical ones (`view ??= numbers`). + const assignmentOperators = new Set([ + ts.SyntaxKind.EqualsToken, + ts.SyntaxKind.QuestionQuestionEqualsToken, + ts.SyntaxKind.BarBarEqualsToken, + ts.SyntaxKind.AmpersandAmpersandEqualsToken, + ]); + // Initializers of class properties, parameter defaults and + // destructuring defaults, which convert to the binding's type. + const initializedBinding = ( + node: ts.Node, + ): node is ts.PropertyDeclaration | ts.ParameterDeclaration | ts.BindingElement => + (ts.isPropertyDeclaration(node) || ts.isParameter(node) || ts.isBindingElement(node)) && + node.initializer !== undefined; const collectDynamicViews = (): Set => { const keys = new Set(); - const dynamicValue = (node: ts.Expression): boolean => { - // A binding's declared type says whether it holds dynamic data; a - // flow type such as `Array.isArray(x)`'s `any[]` only narrows it. + // A binding's declared type says whether it holds dynamic data; a flow + // type such as `Array.isArray(x)`'s `any[]` only narrows it. + const typedNode = (node: ts.Expression): ts.Node => { const e = peel(node); const symbol = ts.isIdentifier(e) ? symbolOf(e) : null; const declaration = symbol ? symbol.valueDeclaration : undefined; - const declared = - declaration && + return declaration && (ts.isVariableDeclaration(declaration) || ts.isParameter(declaration)) && ts.isIdentifier(declaration.name) - ? declaration.name - : null; - const type = typeOrNull(declared ?? node); + ? declaration.name + : node; + }; + const dynamicValue = (node: ts.Expression): boolean => { + const type = typeOrNull(typedNode(node)); if (type === null) return true; if ((type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0) return true; if (!this.checker.isArrayType(type) || this.checker.isTupleType(type)) return false; @@ -6363,58 +6913,321 @@ export class Lowerer { collect(field.type, depth + 1); } else if (type.kind === "promise") collect(type.inner, depth + 1); }; - const target = (type: ts.Type | null | undefined): void => { + const convert = (type: ts.Type | null | undefined): void => { // An unanswerable target may be any array. if (type === null) keys.add("*"); else if (type) collect(this.mapTypeOf(type), 0); }; - const visit = (node: ts.Node): void => { - if ((ts.isAsExpression(node) || ts.isTypeAssertion(node)) && dynamicValue(node.expression)) - target(typeOrNull(node)); - else if ( - ts.isVariableDeclaration(node) && - node.type && - node.initializer && - dynamicValue(node.initializer) - ) - target(typeOrNull(node.name)); - else if ( - ts.isBinaryExpression(node) && - node.operatorToken.kind === ts.SyntaxKind.EqualsToken && - dynamicValue(node.right) - ) - target(typeOrNull(node.left)); - else if (ts.isReturnStatement(node) && node.expression && dynamicValue(node.expression)) - target(contextualOrNull(node.expression)); - else if (ts.isCallExpression(node) || ts.isNewExpression(node)) { + /** Each value `node` converts, with the type it converts to. */ + const conversions = ( + node: ts.Node, + judge: (value: ts.Expression, target: () => ts.Type | null | undefined) => void, + ): void => { + if (ts.isAsExpression(node) || ts.isTypeAssertion(node)) + judge(node.expression, () => typeOrNull(node)); + else if (ts.isVariableDeclaration(node) && node.type && node.initializer) + judge(node.initializer, () => typeOrNull(node.name)); + else if (initializedBinding(node)) + judge(node.initializer!, () => typeOrNull(node.name ?? node)); + else if (ts.isBinaryExpression(node) && assignmentOperators.has(node.operatorToken.kind)) + judge(node.right, () => typeOrNull(node.left)); + else if ((ts.isReturnStatement(node) || ts.isYieldExpression(node)) && node.expression) { + const value = node.expression; + judge(value, () => contextualOrNull(value)); + } else if (ts.isCallExpression(node) || ts.isNewExpression(node)) { for (const arg of node.arguments?.nodes ?? []) - if (!ts.isSpreadElement(arg) && dynamicValue(arg)) target(contextualOrNull(arg)); - } else if (ts.isPropertyAssignment(node) && dynamicValue(node.initializer)) - target(contextualOrNull(node.initializer)); - else if (ts.isArrayLiteralExpression(node)) { + if (!ts.isSpreadElement(arg)) judge(arg, () => contextualOrNull(arg)); + } else if (ts.isPropertyAssignment(node)) { + const value = node.initializer; + judge(value, () => contextualOrNull(value)); + } else if (ts.isArrayLiteralExpression(node)) { for (const element of node.elements.nodes) - if ( - !ts.isSpreadElement(element) && - !ts.isOmittedExpression(element) && - dynamicValue(element) - ) - target(contextualOrNull(element)); - } else if ( - ts.isArrowFunction(node) && - !ts.isBlock(node.body) && - dynamicValue(node.body as ts.Expression) - ) - target(contextualOrNull(node.body as ts.Expression)); + if (!ts.isSpreadElement(element) && !ts.isOmittedExpression(element)) + judge(element, () => contextualOrNull(element)); + } else if (ts.isArrowFunction(node) && !ts.isBlock(node.body)) { + const body = node.body as ts.Expression; + judge(body, () => contextualOrNull(body)); + } }; + // The values' types in one batch: these bodies' reachability is not + // known yet. + const typed: ts.Node[] = []; + for (const sf of sourceFiles) + ts.walkPreorder(sf, (node) => { + panicSafe(() => conversions(node, (value) => typed.push(typedNode(value))), undefined); + return undefined; + }); + this.checker.prefetchClassCollection(typed, []); for (const sf of sourceFiles) ts.walkPreorder(sf, (node) => { // A checker panic hides this conversion's target: any array. + const judged = panicSafe( + () => ( + conversions(node, (value, target) => { + if (dynamicValue(value)) convert(target()); + }), + true + ), + false, + ); + if (!judged) keys.add("*"); + return undefined; + }); + return keys; + }; + // Element ABIs of the native arrays a value converted to a dynamic type + // can reach: the conversion keeps each such array as a live view, so + // stores through the dynamic value write the native storage. + // Standard library functions that only read the arguments they take as + // `any` or `unknown`: the console methods, String, Number, Boolean, + // Array.isArray, Object.is, JSON.stringify without a replacer function, + // and util's inspect and format. Any other function can keep an + // argument (Promise.resolve, Map.prototype.set, push onto an + // `unknown[]`) or pass it to a callback (a JSON.stringify replacer, an + // emitted event), which can write through it. + const readerGlobals = new Map | null>([ + ["console", null], + ["JSON", new Set(["stringify"])], + ["Array", new Set(["isArray"])], + ["Object", new Set(["is"])], + ]); + const readerFunctions = new Set(["String", "Number", "Boolean"]); + const utilReaders = new Set(["inspect", "format"]); + const standardLibraryReader = (node: ts.CallExpression | ts.NewExpression): boolean => + panicSafe(() => { + if (!ts.isCallExpression(node)) return false; + const callee = peel(node.expression); + if (ts.isIdentifier(callee)) + return [...readerFunctions].some((name) => this.isStdlibGlobal(callee, name)); + if (!ts.isPropertyAccessExpression(callee)) return false; + const member = callee.name.text; + for (const [global, members] of readerGlobals) { + if (!this.isStdlibGlobal(callee.expression, global)) continue; + if (members !== null && !members.has(member)) return false; + if (global !== "JSON") return true; + const replacer = node.arguments.nodes[1]; + return ( + replacer === undefined || + ts.isArrayLiteralExpression(peel(replacer)) || + peel(replacer).kind === ts.SyntaxKind.NullKeyword + ); + } + if (!utilReaders.has(member)) return false; + const declaration = this.checker.getResolvedSignature(node)?.declaration; + if (declaration === undefined) return false; + const file = ts.getSourceFileOfNode(declaration); + return this.isStdlibFile(file) && /[\\/]util\.d\.ts$/.test(file.fileName); + }, false); + const collectNativeViews = (): Set => { + const keys = new Set(); + const seen = new Set(); + const collect = (type: IrType | null): void => { + if (!type) return; + const id = typeKey(type); + if (seen.has(id)) return; + seen.add(id); + if (type.kind === "array") { + if (type.elem.kind !== "dyn") keys.add(typeKey(type.elem)); + collect(type.elem); + } else if (type.kind === "union") { + for (const arm of this.unions.get(type.unionId)?.arms ?? []) collect(arm); + } else if (type.kind === "record") { + for (const field of this.shapes.get(type.shapeId)?.fields ?? []) collect(field.type); + } else if (type.kind === "object") { + for (const field of this.classes.get(type.className)?.fields.values() ?? []) + collect(field); + } else if (type.kind === "map") { + collect(type.key); + collect(type.value); + } else if (type.kind === "set") collect(type.elem); + else if (type.kind === "promise") collect(type.inner); + }; + const holdsDynamic = (type: IrType | null, depth: number): boolean => { + if (!type || depth > 4) return false; + if (type.kind === "dyn") return true; + if (type.kind === "array" || type.kind === "set") return holdsDynamic(type.elem, depth + 1); + if (type.kind === "map") return holdsDynamic(type.value, depth + 1); + if (type.kind === "promise") return holdsDynamic(type.inner, depth + 1); + if (type.kind === "union") + return (this.unions.get(type.unionId)?.arms ?? []).some((arm) => + holdsDynamic(arm, depth + 1), + ); + if (type.kind === "record") + return (this.shapes.get(type.shapeId)?.fields ?? []).some((field) => + holdsDynamic(field.type, depth + 1), + ); + return false; + }; + const mapped = (type: ts.Type | null | undefined): IrType | null => + type ? panicSafe(() => this.mapTypeOf(type), null) : null; + const flow = (source: ts.Expression, target: ts.Type | null | undefined): void => { + if (target === undefined) return; + const into = target === null ? null : mapped(target); + // An unanswerable target may be dynamic. + if (target !== null && into !== null && !holdsDynamic(into, 0)) return; + const from = typeOrNull(source); + if (from === null) keys.add("*"); + else collect(mapped(from)); + }; + const visit = (node: ts.Node): void => { + if (ts.isAsExpression(node) || ts.isTypeAssertion(node)) + flow(node.expression, typeOrNull(node)); + else if (ts.isVariableDeclaration(node) && node.type && node.initializer) + flow(node.initializer, typeOrNull(node.name)); + else if (initializedBinding(node)) flow(node.initializer!, typeOrNull(node.name ?? node)); + else if (ts.isBinaryExpression(node) && assignmentOperators.has(node.operatorToken.kind)) + flow(node.right, typeOrNull(node.left)); + else if (ts.isReturnStatement(node) && node.expression) + flow(node.expression, contextualOrNull(node.expression)); + else if (ts.isYieldExpression(node) && node.expression) + flow(node.expression, contextualOrNull(node.expression)); + // A thrown value reaches a catch binding, which is dynamic. + else if (ts.isThrowStatement(node)) flow(node.expression, null); + else if (ts.isCallExpression(node) || ts.isNewExpression(node)) { + if (!standardLibraryReader(node)) + for (const arg of node.arguments?.nodes ?? []) + flow(ts.isSpreadElement(arg) ? arg.expression : arg, contextualOrNull(arg)); + } else if (ts.isPropertyAssignment(node)) + flow(node.initializer, contextualOrNull(node.initializer)); + else if (ts.isShorthandPropertyAssignment(node)) { + const name = node.name as ts.Expression; + flow(name, contextualOrNull(name)); + } else if (ts.isArrayLiteralExpression(node)) { + for (const element of node.elements.nodes) + if (!ts.isSpreadElement(element) && !ts.isOmittedExpression(element)) + flow(element, contextualOrNull(element)); + } else if (ts.isArrowFunction(node) && !ts.isBlock(node.body)) + flow(node.body as ts.Expression, contextualOrNull(node.body as ts.Expression)); + }; + for (const sf of sourceFiles) + ts.walkPreorder(sf, (node) => { + // A checker panic hides this conversion: any array. if (!panicSafe(() => (visit(node), true), false)) keys.add("*"); return undefined; }); return keys; }; + // A receiver typed `any`, `unknown` or an array of either can be a live + // view of a native array. A receiver of another type (`Worker[]`, + // `Record`) is one only after a lying assertion, even + // when its values are dynamic. + const dynamicType = (type: ts.Type): boolean => { + if ((type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0) return true; + if (!this.checker.isArrayType(type) || this.checker.isTupleType(type)) return false; + const element = this.checker.getTypeArguments(type as ts.TypeReference)[0]; + return ( + element !== undefined && (element.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0 + ); + }; + // A binding that only ever holds the array its declaration creates (a + // const variable, or a readonly property with an initializer) is never + // a view of a native array, even when its elements are dynamic. + const freshDynamicStorage = (node: ts.Expression, depth: number): boolean => { + const e = peel(node); + if (ts.isArrayLiteralExpression(e)) return true; + if (ts.isNewExpression(e) || ts.isCallExpression(e)) { + const callee = peel(e.expression); + if (ts.isIdentifier(callee)) return this.isStdlibGlobal(callee, "Array"); + return ( + ts.isPropertyAccessExpression(callee) && + (callee.name.text === "of" || callee.name.text === "from") && + this.isStdlibGlobal(callee.expression, "Array") + ); + } + if (depth > 4 || (!ts.isIdentifier(e) && !ts.isPropertyAccessExpression(e))) return false; + // A typed array along the way is the array a view shows. + const type = typeOrNull(e); + if (type === null || !dynamicType(type)) return false; + const symbol = panicSafe( + () => this.checker.getSymbolAtLocation(ts.isIdentifier(e) ? e : e.name), + undefined, + ); + const declaration = symbol?.valueDeclaration; + if (!symbol || !declaration) return false; + // Variables and class properties only: parameters take any argument, + // and object literals fill interface properties. + if ( + !(ts.isVariableDeclaration(declaration) && ts.isIdentifier(declaration.name)) && + !ts.isPropertyDeclaration(declaration) + ) + return false; + const assigned = assignedValues(symbol); + if (assigned === null) return false; + const values = [ + ...(declaration.initializer ? [declaration.initializer] : []), + ...(assigned ?? []), + ]; + return values.length > 0 && values.every((value) => freshDynamicStorage(value, depth + 1)); + }; + // Every value `=` assigns to each variable or property of the program, + // by symbol; null for one also written another way (compound or + // destructuring assignment). Built on first use. + let assignments: Map | null = null; + const assignedValues = (symbol: ts.Symbol): ts.Expression[] | null | undefined => { + if (assignments === null) { + const found = new Map(); + const add = (target: ts.Expression, value: ts.Expression | null): void => { + const e = peel(target); + if (ts.isArrayLiteralExpression(e) || ts.isObjectLiteralExpression(e)) { + // Destructuring: every target is written with a part of the value. + ts.forEachChild(e, (child) => { + if (ts.isIdentifier(child) || ts.isPropertyAccessExpression(child)) add(child, null); + else if (ts.isSpreadElement(child)) add(child.expression, null); + else if (ts.isPropertyAssignment(child)) add(child.initializer, null); + else if (ts.isShorthandPropertyAssignment(child)) + add(child.name as ts.Expression, null); + else if (ts.isBinaryExpression(child)) add(child.left, null); + return undefined; + }); + return; + } + const name = ts.isIdentifier(e) ? e : ts.isPropertyAccessExpression(e) ? e.name : null; + const written = name + ? panicSafe(() => this.checker.getSymbolAtLocation(name), undefined) + : undefined; + if (!written) return; + const list = found.get(written); + if (list === null) return; + if (value === null) found.set(written, null); + else if (list) list.push(value); + else found.set(written, [value]); + }; + for (const sf of sourceFiles) + ts.walkPreorder(sf, (node) => { + if (ts.isBinaryExpression(node)) { + const op = node.operatorToken.kind; + if (op === ts.SyntaxKind.EqualsToken) add(node.left, node.right); + else if (op >= ts.SyntaxKind.FirstAssignment && op <= ts.SyntaxKind.LastAssignment) + add(node.left, null); + } + return undefined; + }); + assignments = found; + } + return assignments.get(symbol); + }; + // `t[i]!` on a typed array, stored into an array, keeps an invalid + // index's undefined (assertedReadObservesUndefined): `!` performs no + // runtime check. A read whose consumers treat undefined like NaN stores + // NaN instead. + const assertedReadMayBeUndefined = (node: ts.Expression): boolean => { + let e = node; + while (ts.isParenthesizedExpression(e)) e = e.expression; + if (!ts.isNonNullExpression(e)) return false; + let read = e.expression; + while (ts.isParenthesizedExpression(read)) read = read.expression; + return ( + ts.isElementAccessExpression(read) && + this.mapTypeOf(this.typeOf(read.expression))?.kind === "bytes" && + !nanCoded.isNumericRead(read) + ); + }; const elementStates = new ArrayElementStates({ + isDynamicArray: (node) => { + const type = typeOrNull(node); + return (type === null || dynamicType(type)) && !freshDynamicStorage(node, 0); + }, + nativeViewKeys: collectNativeViews, elementAdmitsUndefined: (key) => { siteDependencies?.read(key); return optionalElementKeys.has(key); @@ -6423,8 +7236,19 @@ export class Lowerer { allocationOwner: (node) => ownership.allocationOwner(node), arrayKeys: arrayKeysOf, mayBeUndefined: (node) => { - if (explicitlyNonNull(node)) return false; + if (explicitlyNonNull(node)) return assertedReadMayBeUndefined(node); if (mayBeOptional(node)) return true; + // A value the checker types as possibly undefined: `(T | undefined)[]` + // stores T elements (the type mapper), so its undefined values are + // facts of T's arrays. + const checked = typeOrNull(node); + if ( + checked !== null && + (checked.isUnionType() ? ts.constituentTypes(checked) : [checked]).some( + (part) => (part.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Void)) !== 0, + ) + ) + return true; // `undefined as unknown as T` keeps the undefined value. for ( let inner: ts.Expression = node; @@ -6443,7 +7267,49 @@ export class Lowerer { const symbol = symbolOf(e); return symbol !== null && iterationSymbols.has(symbol) && !provenPresent(e, symbol); }, - callbackReturnsUndefined: (node) => callbackReturnsOptional(node), + iterationMayYieldUndefined: (node) => { + const type = typeOrNull(node); + if (type === null || this.checker.isArrayType(type)) return false; + // A tuple yields its elements, in order. + if (this.checker.isTupleType(type)) + return panicSafe( + () => + this.checker + .getTypeArguments(type as ts.TypeReference) + .some((element) => + (element.isUnionType() ? ts.constituentTypes(element) : [element]).some( + (part) => (part.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Void)) !== 0, + ), + ), + true, + ); + // The first type argument of Set, Map and Set iterators, + // generators and iterables is the value they yield. + const element = + (type.flags & ts.TypeFlags.Object) !== 0 + ? panicSafe(() => this.checker.getTypeArguments(type as ts.TypeReference)[0], undefined) + : undefined; + if (element === undefined) return false; + return (element.isUnionType() ? ts.constituentTypes(element) : [element]).some( + (part) => (part.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Void)) !== 0, + ); + }, + callbackReturnsUndefined: (node) => { + if (callbackReturnsOptional(node)) return true; + // A callback the checker types as possibly returning undefined + // (`(m) => map.get(m)`): its results are present undefined + // elements of a `(T | undefined)[]`, which stores T elements. + const type = typeOrNull(node); + return ( + type !== null && + this.checker.getCallSignatures(type).some((signature) => { + const result = this.checker.getReturnTypeOfSignature(signature); + return (result.isUnionType() ? ts.constituentTypes(result) : [result]).some( + (part) => (part.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Void)) !== 0, + ); + }) + ); + }, isGlobal: (node, name) => this.isStdlibGlobal(node, name), isString: (node) => { const type = typeOrNull(node); @@ -6470,6 +7336,14 @@ export class Lowerer { const rest = sig.params.find((shape) => shape.mode === "rest"); return rest?.type.kind === "array" ? [typeKey(rest.type.elem)] : []; }, + restStart: (call) => { + const callee = peel(call.expression); + if (!ts.isIdentifier(callee) && !ts.isPropertyAccessExpression(callee)) return null; + const symbol = callableSymbolOf(callee); + const sig = symbol ? signatureBySymbol.get(symbol) : undefined; + const at = sig ? sig.params.findIndex((shape) => shape.mode === "rest") : -1; + return at >= 0 ? at : null; + }, markIterationBinding: (node, known) => { const symbol = symbolOf(node); if (!symbol) return false; @@ -6504,6 +7378,7 @@ export class Lowerer { const cached = scanSites.get(sf); if (cached !== undefined) return cached; const nodes: ts.Node[] = []; + const values: ts.Node[] = []; ts.walkPreorder(sf, (node) => { if ( ts.isVariableDeclaration(node) || @@ -6519,7 +7394,27 @@ export class Lowerer { ts.isPostfixUnaryExpression(node) ) nodes.push(node); + // The values these sites store, whose checker types mayBeUndefined + // reads: batched here, since dead bodies are scanned too. + if (ts.isArrayLiteralExpression(node)) + for (const element of node.elements.nodes) + values.push(ts.isSpreadElement(element) ? element.expression : element); + else if (ts.isCallExpression(node) || ts.isNewExpression(node)) + for (const arg of node.arguments?.nodes ?? []) + values.push(ts.isSpreadElement(arg) ? arg.expression : arg); + else if ( + ts.isBinaryExpression(node) && + node.operatorToken.kind === ts.SyntaxKind.EqualsToken + ) + values.push(node.right); + // A for-of loop reads its iterable's array keys. + if (ts.isForOfStatement(node)) values.push(node.expression); + // A property read in such a slot reads its declared type through + // its receiver's (widenedType). + const receiver = widenedReceiver(node); + if (receiver !== null) values.push(receiver); }); + this.checker.prefetchCollectionTypes(values); scanSites.set(sf, nodes); return nodes; }; @@ -7097,7 +7992,8 @@ export class Lowerer { siteBase.set(sf, siteCount); siteCount += sitesOf(sf).length; } - siteDependencies = new SiteDependencies(siteCount); + // Each function's returns are one more site, after the files' sites. + siteDependencies = new SiteDependencies(siteCount + functionDeclBySymbol.size); let fullPass = true; for (let changed = true, first = true; changed || !fullPass; first = false) { fullPass = first || !changed; @@ -7111,8 +8007,14 @@ export class Lowerer { } } for (const sf of sourceFiles) if (scanFile(sf)) changed = true; + let returnSite = siteCount; for (const [symbol, decl] of functionDeclBySymbol) { - if (!scanReturns(symbol, decl)) continue; + const site = returnSite++; + if (!fullPass && !siteDependencies.isStale(site)) continue; + siteDependencies.begin(site); + const returnsChanged = scanReturns(symbol, decl); + siteDependencies.end(returnsChanged); + if (!returnsChanged) continue; siteDependencies.changed(symbol); changed = true; } @@ -7259,6 +8161,7 @@ export class Lowerer { } for (const [site, present] of presentSites) if (present) this.presentElementCalls.add(site); for (const [loop, present] of presentLoops) if (present) this.presentElementLoops.add(loop); + this.foreignElementKeys = elementStates.foreignKeys(); const globalsById = new Map(this.globalsList.map((global) => [global.id, global])); for (const [symbol, field] of staticFieldsBySymbol) { if (!optionalSymbols.has(symbol)) continue; @@ -7531,6 +8434,7 @@ export class Lowerer { if (this.remainder) { for (const fp of parts) this.checker.prefetchSourceFile(fp.sf); } + this.prefetchProgramSymbols(parts); this.collectProgram(parts); this.speculativeTypeQueries(() => { extendAssertedRecordMembers( @@ -7569,9 +8473,12 @@ export class Lowerer { if (sig && !this.wantBody(sig.name)) continue; const fn = this.lowerFunction(decl); if (fn) functions.push(fn); - else if (this.countsSkips()) { - this.countSkippedBody(decl); - if (this.remainder) this.reportBlockedBody(decl); + else { + this.skippedBodies.add(decl); + if (this.countsSkips()) { + this.countSkippedBody(decl); + if (this.remainder) this.reportBlockedBody(decl); + } } } for (const decl of fp.classDecls) { @@ -7580,7 +8487,10 @@ export class Lowerer { functions.push( ...this.withClosureRoot(info.def.name, () => this.lowerClassMembers(info)), ); - else if (this.countsSkips()) this.countSkippedBody(decl); + else { + this.skippedBodies.add(decl); + if (this.countsSkips()) this.countSkippedBody(decl); + } } } @@ -7615,15 +8525,38 @@ export class Lowerer { let gc = 0; let gi = 0; let es = 0; + let rd = 0; const classDispatch = new ClassDynamicDispatch(); let dispatchChanged = !this.remainder; while ( dispatchChanged || + rd < this.retriedDeclarations.length || ec < this.exprClasses.length || gc < this.genericClassInstances.length || gi < this.instantiationQueue.length || es < this.emitSpecQueue.length ) { + // Localized failure: declarations a flush registered with dynamic + // slots (retryDeferred) after the loops above skipped their bodies. + while (rd < this.retriedDeclarations.length) { + const decl = this.retriedDeclarations[rd++]!; + if (!this.skippedBodies.delete(decl)) continue; + if (this.countsSkips()) this.uncountSkippedBody(decl); + if (ts.isFunctionDeclaration(decl)) { + const fn = this.lowerFunction(decl); + if (fn) functions.push(fn); + else { + this.skippedBodies.add(decl); + if (this.countsSkips()) this.countSkippedBody(decl); + } + } else if (ts.isClassDeclaration(decl)) { + const info = this.classes.get(this.classNamer(decl)); + if (info) + functions.push( + ...this.withClosureRoot(info.def.name, () => this.lowerClassMembers(info)), + ); + } + } while (ec < this.exprClasses.length) { functions.push(...this.lowerClassMembers(this.exprClasses[ec++]!)); } @@ -7636,13 +8569,10 @@ export class Lowerer { // generic method's this/super fence, a fenced parameter // default): the diagnostic is recorded — the instance skips // like a signature-blocked function (lowerFunction's rule). - try { - functions.push( - this.withClosureRoot(inst.name, () => this.lowerGenericInstance(info, inst)), - ); - } catch (e) { - if (!(e instanceof PoisonError)) throw e; - } + const fn = this.lowerUnitDegrading(() => + this.withClosureRoot(inst.name, () => this.lowerGenericInstance(info, inst)), + ); + if (fn !== null) functions.push(fn); } // Emit-override specializations queued by the emit sites above (a // body can queue more — the super-forward chain — and generic @@ -7700,8 +8630,12 @@ export class Lowerer { finalizeClassMethodValues(this, functions); finalizePrivateBrands(this, functions); trackClassFieldCreation(this, functions); + boxClassInstancesExactly(this, functions); + addUntypedIntegrityWords(this); guardClassAttributeWrites(this, functions); + guardFrozenWrites(this, functions); applyHandleProperties(this, functions); + installBufferValue(functions); // Globals typed by a class that never REGISTERED (a JS class whose // collection fenced — Symbol-keyed fields, an unsupported base): the @@ -7791,6 +8725,8 @@ export class Lowerer { // every producer and consumer agrees. Programs with no unregistered // reference are untouched — byte-stability holds. checkRequireBindingAccesses(this, functions); + refuseX509DynValues(this, functions); + markInferredFactChecks(functions, this.program); const module: IrModule | null = this.diags.length > 0 ? null @@ -7805,16 +8741,33 @@ export class Lowerer { entry: ENTRY_NAME, ...(this.usesWorkers ? { workers: true } : {}), ...(this.usesLifetimeRegions ? { lifetimeRegions: true as const } : {}), - ...(this.usesNodeIsland ? { nodeIsland: { libnode: "", nodeVersion: "" } } : {}), + ...(this.usesNodeIsland ? { nodeIsland: this.nodeIslandRecord() } : {}), ...(this.declBoundary ? { declBoundary: true as const } : {}), + ...(shapeOverlayFileNames().length > 0 + ? { + inferredFactFiles: shapeOverlayFileNames() + .map((file) => ({ file, pkg: npmStaticPackageOfPath(file) ?? "" })) + .sort((a, b) => (a.file < b.file ? -1 : a.file > b.file ? 1 : 0)), + } + : {}), + ...(this.foreignElementKeys.size > 0 + ? { foreignElementKeys: [...this.foreignElementKeys].sort() } + : {}), + ...(this.declOrigins.length > 0 ? { declOrigins: [...this.declOrigins] } : {}), ...(this.threadSlotKeys.size > 0 ? { threadSlotKeys: [...this.threadSlotKeys].sort() } : {}), + ...(this.freezableArrays.size > 0 + ? { freezableArrays: [...this.freezableArrays].sort() } + : {}), + ...(this.untypedIntegrity ? { untypedIntegrity: true as const } : {}), ...(isNodeEsmFile(this.entry, this.program) ? {} : { commonJsEntry: true }), ...(this.ffiImports.length > 0 ? { ffiImports: [...this.ffiImports] } : {}), }; if (module) sanitizeUnregisteredClassTypes(module, (name) => this.classes.has(name)); + if (module?.declOrigins !== undefined) compactDeclOrigins(module); if (module) checkCycleBindingAccesses(this, module); + if (module) bindCompletionCallbacks(module); // Execution tiers from the final bodies (ir/tiers.ts). Assigned with or // without a module: coverage reports the dynamic tier of programs that // still have blockers. @@ -7891,6 +8844,22 @@ export class Lowerer { * percentage would overstate how much of the program compiles. Count * every statement the body holds as attempted and failed, in addition to * the skipped-function tally. */ + /** countSkippedBody undone: a flush registered the declaration after + * its body was skipped, and the body lowers now. */ + private uncountSkippedBody(decl: ts.Node): void { + this.stats.functionsSkipped--; + if (this.suppressStats) return; + const n = countBodyStatements(decl); + if (n === 0) return; + this.stats.statementsTotal -= n; + this.stats.statementsFailed -= n; + const file = ts.getSourceFileOfNode(decl).fileName; + for (let i = 0; i < n; i++) { + this.bumpFileStat(file, "total", -1); + this.bumpFileStat(file, "failed", -1); + } + } + private countSkippedBody(decl: ts.Node): void { this.stats.functionsSkipped++; if (this.suppressStats) return; @@ -7978,6 +8947,7 @@ export class Lowerer { this.onEdge = (name) => { collectionEdges.add(name); }; + this.prefetchProgramSymbols(parts); this.collectProgram(parts); this.speculativeTypeQueries(() => { extendAssertedRecordMembers( @@ -8006,16 +8976,24 @@ export class Lowerer { lower: () => IrFunction | null; } >(); - const bodyRoots = (...roots: (ts.Node | undefined | null)[]): ts.Node[] => - roots.filter((root) => root !== undefined && root !== null); - const functionRoots = (decl: ts.FunctionLikeDeclaration): ts.Node[] => - bodyRoots(...decl.parameters.nodes.map((param) => param.initializer), decl.body); - const classCtorRoots = (info: ClassInfo): ts.Node[] => - bodyRoots( - ...(info.ctor?.parameters?.nodes ?? []).map((param) => param.initializer), - info.ctor?.body, - ...info.fieldOrder.map((field) => field.initializer), - ); + // Roots are collected with plain pushes: spread arguments into a rest + // parameter pass the nodes as dynamic values. + const addRoot = (roots: ts.Node[], root: ts.Node | undefined | null): void => { + if (root !== undefined && root !== null) roots.push(root); + }; + const functionRoots = (decl: ts.FunctionLikeDeclaration): ts.Node[] => { + const roots: ts.Node[] = []; + for (const param of decl.parameters.nodes) addRoot(roots, param.initializer); + addRoot(roots, decl.body); + return roots; + }; + const classCtorRoots = (info: ClassInfo): ts.Node[] => { + const roots: ts.Node[] = []; + for (const param of info.ctor?.parameters?.nodes ?? []) addRoot(roots, param.initializer); + addRoot(roots, info.ctor?.body); + for (const field of info.fieldOrder) addRoot(roots, field.initializer); + return roots; + }; const classMemberRoots = (info: ClassInfo): ts.Node[] => { const roots = classCtorRoots(info); for (const { member } of this.classMethodMembers(info)) roots.push(...functionRoots(member)); @@ -8060,6 +9038,20 @@ export class Lowerer { lower: () => info.localClass && !info.localClass.ready ? null : this.lowerClassCtor(info), }); + // The descriptor helpers of a class that extends Array, reached + // from its constructor. + if (info.extendsArray) { + units.set(`%${cName}.arrayFields`, { + order: unitOrder++, + roots: [], + lower: () => arrayClassFieldsFn(this, info), + }); + units.set(`%${cName}.species`, { + order: unitOrder++, + roots: [], + lower: () => arrayClassSpeciesFn(this, info), + }); + } for (const { mName, member } of this.classMethodMembers(info)) { units.set(`%${cName}.${mName}`, { order: unitOrder++, @@ -8156,6 +9148,19 @@ export class Lowerer { this.constructedClasses.add(name.slice(1, -".constructor".length)); for (const [owner, methods] of this.virtualEdges) for (const method of methods) this.noteVirtualEdge(owner, method); + // An instance's [util.inspect.custom] method runs from the runtime + // inspect walk (customInspectRegistrations), which no IR edge shows. + for ( + let c = this.classes.get(name.slice(1, -".constructor".length)) ?? null; + c; + c = c.base + ) { + const custom = `%${c.def.name}.${INSPECT_CUSTOM_MEMBER}`; + if (units.has(custom)) { + this.onEdge?.(custom); + break; + } + } } }; // Class EXPRESSIONS collect while init bodies lower (below): their @@ -8175,6 +9180,10 @@ export class Lowerer { register(`%${cName}.constructor`, classCtorRoots(info), () => info.localClass && !info.localClass.ready ? null : this.lowerClassCtor(info), ); + if (info.extendsArray) { + register(`%${cName}.arrayFields`, [], () => arrayClassFieldsFn(this, info)); + register(`%${cName}.species`, [], () => arrayClassSpeciesFn(this, info)); + } for (const { mName, member } of this.classMethodMembers(info)) { register(`%${cName}.${mName}`, functionRoots(member), () => info.localClass && !info.localClass.ready @@ -8192,7 +9201,46 @@ export class Lowerer { register(`%${cName}.set:${prop}`, [], () => this.throwingSetterFn(info, prop)); } }; - for (const name of collectionEdges) this.onEdge(name); + // Localized failure: a declaration a flush registered with dynamic + // slots (retryDeferred) registers its units like a class expression. + this.onDeclarationRegistered = (decl: ts.Node): void => { + if (ts.isFunctionDeclaration(decl)) { + if (!decl.body) return; + const symbol = declSymbolOf(this, decl); + const sig = symbol ? this.fnSigsBySymbol.get(symbol) : undefined; + if (!sig || units.has(sig.name)) return; + units.set(sig.name, { + order: unitOrder++, + roots: functionRoots(decl), + lower: () => this.lowerFunction(decl), + }); + if (reachable.has(sig.name)) queue.push(sig.name); + metadataPriority.set(sig.name, [3, expressionMetadataOrder++]); + } else if (ts.isClassDeclaration(decl)) { + const info = this.classes.get(this.classNamer(decl)); + if (!info) return; + if (!info.generic) { + if (!units.has(`%${info.def.name}.constructor`)) this.onExprClassCollected?.(info); + return; + } + // A generic class FAMILY's only units are its statics; its + // instantiations lower in the instance drain. + if (!info.staticMethods) return; + for (const [name, entry] of info.staticMethods) { + const unit = `%${info.def.name}.static:${name}`; + if (units.has(unit)) continue; + units.set(unit, { + order: unitOrder++, + roots: functionRoots(entry.member), + lower: () => lowerStaticMethod(this, info, name), + }); + if (reachable.has(unit)) queue.push(unit); + metadataPriority.set(unit, [3, expressionMetadataOrder++]); + } + } + }; + const onEdge = this.onEdge; + for (const name of collectionEdges) onEdge(name); // Generic instances queued by the bodies above lower here (an instance // body fires edges of its own and can queue further instances of // either kind — function instances and class instantiations drain to @@ -8246,17 +9294,16 @@ export class Lowerer { this.genericDemandPriority.set(inst, ref); // Body-level poisons skip the instance after retaining the // diagnostic and every edge fired before poisoning. - try { - instanceFunctions.push( - this.withGenericDemandOwner(owner, () => - this.shapes.withDeclaredOrderPriority(ref, () => - this.withClosureRoot(inst.name, () => this.lowerGenericInstance(info, inst)), - ), + const fn = this.lowerUnitDegrading(() => + this.withGenericDemandOwner(owner, () => + this.shapes.withDeclaredOrderPriority(ref, () => + this.withClosureRoot(inst.name, () => this.lowerGenericInstance(info, inst)), ), - ); + ), + ); + if (fn !== null) { + instanceFunctions.push(fn); relieveHeapPressure(); - } catch (e) { - if (!(e instanceof PoisonError)) throw e; } } } @@ -8327,7 +9374,8 @@ export class Lowerer { // profile-mapped exports are called from outside the graph, so they // seed the worklist beside the init bodies. Unknown names are inert // (the export-map resolution reports them as SC4002 later). - for (const root of extraRoots ?? []) this.onEdge?.(root); + const rootEdge = this.onEdge; + for (const root of extraRoots ?? []) rootEdge?.(root); const shuffle = lowerShuffle(); const drainUnits = (): void => { while (queue.length > 0) { @@ -8445,7 +9493,9 @@ export class Lowerer { /** Reachability hook (see emitReachable): fires when lowering resolves a * reference to a lowerable body. */ noteEdge(name: string): void { - if (this.onEdge) this.onEdge(name); + // Called as a plain function: the instance is not its receiver. + const onEdge = this.onEdge; + if (onEdge) onEdge(name); } /** A subclass constructor's super() edge: the base constructor runs, but @@ -8625,7 +9675,14 @@ export class Lowerer { this.diagSink.push(d); return; } - if (pending) this.commitFlush(pending.symbol, pending.source); + // A probe's copy of a declaration's flushed diagnostics, committed + // after a flush retry registered the declaration: nothing to report. + if (pending && this.retriedSymbols.has(pending.symbol)) return; + if (pending) { + this.commitFlush(pending.symbol, pending.source); + const group = this.rejectedGroups.get(pending.symbol); + if (group !== undefined) d.rejects ??= group; + } // One site, one report: some declarations map a type twice (module // globals pre-register before their initializers lower) — an exact // duplicate (code + span + message) adds noise, not information. @@ -8644,6 +9701,18 @@ export class Lowerer { this.diags.push(d); } + /** The module's Node island record: the collected module tree, with each + * call's caller literal set to its path in that tree. The executable + * build fills in the libnode pack (index.ts). */ + private nodeIslandRecord(): NonNullable { + const modules = this.nodeIslandModules.tree(); + for (const { literal, caller } of this.nodeIslandCallers) + literal.value = NodeIslandModuleCollector.callerPath(modules, caller); + for (const { literal, caller, target } of this.nodeIslandSpecifiers) + literal.value = NodeIslandModuleCollector.relativeSpecifier(caller, target); + return { libnode: "", nodeVersion: "", ...(modules === null ? {} : { modules }) }; + } + unsupported( code: keyof typeof UNSUPPORTED & `SC${number}`, node: ts.Node, @@ -8680,6 +9749,166 @@ export class Lowerer { return (this.typeOf(node).flags & ts.TypeFlags.Any) !== 0; } + /** badType's commit: the diagnostic carries the structured type + * failure (report-only in stage L0), and so does the poison. */ + private typeFail( + node: ts.Node, + type: ts.Type, + family: TypeFailureFamily, + diag: ScrDiagnostic, + ): never { + const failure = typeFailureOf(node, this.checker.typeToString(type), family); + diag.typeFailure = failure; + this.failedTypes.set(failure, type); + this.pushDiag(diag); + throw new PoisonError(failure, type); + } + + /** Localized failure's commit point (design §3.3): `e` poisoned a unit + * whose failure is about to enter the build. When it is a degradable type + * failure at a slot kind in `slots`, the failing type joins + * degradedTypes (its lenient mapping is the checked dynamic, whole slot), + * the failure's own diagnostic leaves the build, and the caller lowers + * the unit again. Returns the slot to record once that retry succeeds, + * or null to commit as today. Never inside a capture buffer (a probe or a + * deferred collection) and never under --tier=static-only. */ + degradeAtCommit(e: unknown, slots: ReadonlySet): DynamicSlot | null { + if (this.tierMode !== "auto" || this.diagSink !== null) return null; + if (!(e instanceof PoisonError)) return null; + const failure = e.failure; + const type = e.failedType; + if (failure === undefined || type === undefined) return null; + if (!failure.degradable || failure.family === "dyn-field-policy") return null; + if (!slots.has(failure.slot)) return null; + const id = (type as { id?: number }).id; + // Already degraded and still failing: no progress, commit as today. + if (id === undefined || this.degradedTypes.has(id)) return null; + let committed: ScrDiagnostic | undefined; + for (let i = this.diags.length - 1; i >= 0; i--) { + if (this.diags[i]!.typeFailure !== failure) continue; + committed = this.diags[i]; + this.diags.splice(i, 1); + } + if (committed === undefined) return null; + this.degradedTypes.set(id, { lenient: DYN, code: committed.code, reason: committed.message }); + return { loc: committed.loc, failure, code: committed.code, reason: committed.message }; + } + + /** A declaration slot whose type maps through degradedTypes is a + * dynamic slot too, though only the first such slot failed: record it + * (outside probes; the lookup is skipped while nothing is degraded). */ + noteDegradedSlot(node: ts.Node, type: ts.Type): void { + if (this.diagSink !== null) return; + const degraded = this.degradedTypes.get((type as { id?: number }).id ?? -1); + if (degraded === undefined) return; + const failure = typeFailureOf(node, this.checker.typeToString(type), "unsupported"); + if (failure.slot === "expression" || failure.slot === "member") return; + this.noteDynamicSlot({ + loc: locOf(node), + failure, + code: degraded.code, + reason: degraded.reason, + }); + } + + /** Localized failure, statement commit point for declarations a probe + * flushed: a statement committing the flushed diagnostics of a deferred + * declaration (inside a capture buffer the flush could not retry) reopens + * it here, outside every probe. When the declaration's diagnostics are + * all retriable, they leave the build, the declaration retries + * (retryDeferred), and on success the caller lowers the statement again. + * Returns whether some declaration registered. */ + reopenFlushedAtCommit(diagsBefore: number): boolean { + if (this.tierMode !== "auto" || this.diagSink !== null) return false; + const groups = new Set(); + for (let i = diagsBefore; i < this.diags.length; i++) { + const group = this.diags[i]!.rejects; + if (group !== undefined && !group.startsWith("statement @")) groups.add(group); + } + if (groups.size === 0) return false; + let registered = false; + for (const [symbol, group] of [...this.rejectedGroups]) { + if (!groups.has(group) || !this.deferredCollectors.has(symbol)) continue; + const committed = this.diags.filter((d) => d.rejects === group); + this.diags.splice(0, this.diags.length, ...this.diags.filter((d) => d.rejects !== group)); + this.flushedSymbols.delete(symbol); + this.rejectedGroups.delete(symbol); + if (this.retryDeferred(symbol, committed)) { + registered = true; + continue; + } + // Not retriable: the declaration's diagnostics commit as before. + const last = this.deferredDiags.get(symbol) ?? committed; + this.deferredDiags.delete(symbol); + this.flushedSymbols.add(symbol); + this.noteRejected(symbol, last); + for (const d of last) this.pushDiag(d); + } + return registered; + } + + /** A failed attempt's own diagnostics leave the build before its unit + * lowers again; flushed declaration diagnostics (`rejects`) stay, since + * the retry will not flush them a second time. */ + dropAttemptDiags(diagsBefore: number): void { + const kept = this.diags + .slice(diagsBefore) + .filter((d) => d.rejects !== undefined || this.loweredOnceDiags.has(d)); + this.diags.length = diagsBefore; + this.diags.push(...kept); + } + + /** Lowers one unit whose body-level poison skips it (a generic + * instance), with localized failure's commit point: a degradable type + * failure at one of its declaration slots degrades the type and the unit + * lowers again (at most MAX_DECLARATION_RETRIES times). Null when it + * still poisons; its diagnostics are then committed as before. */ + lowerUnitDegrading(lower: () => T): T | null { + const slots: DynamicSlot[] = []; + for (let attempt = 0; ; attempt++) { + const mark = this.diags.length; + try { + const result = lower(); + for (const slot of slots) this.noteDynamicSlot(slot); + return result; + } catch (e) { + if (!(e instanceof PoisonError)) throw e; + if (attempt >= MAX_DECLARATION_RETRIES) return null; + const slot = this.degradeAtCommit(e, DECLARATION_SLOTS); + if (slot === null) return null; + slots.push(slot); + this.dropAttemptDiags(mark); + } + } + } + + /** Records a dynamic slot whose unit lowered with it (one per site). */ + noteDynamicSlot(slot: DynamicSlot): void { + const seen = this.dynamicSlots.some( + (s) => + s.loc.file === slot.loc.file && + s.loc.start === slot.loc.start && + s.loc.end === slot.loc.end, + ); + if (!seen) this.dynamicSlots.push(slot); + } + + /** The SC1090 refusal of checked-dynamic class fields in TypeScript + * (classFieldStoresDyn): a policy fence, not a missing representation. + * A flush retry lifts it (liftDynFieldPolicy), so a reached class with + * an 'any' field registers. */ + dynFieldPolicyFence(node: ts.Node, feature: string): never { + const diag = unsupportedDiag("SC1090", locOf(node), feature); + const failure = typeFailureOf( + node, + this.checker.typeToString(this.typeOf(node)), + "dyn-field-policy", + ); + diag.typeFailure = failure; + this.pushDiag(diag); + throw new PoisonError(failure, this.typeOf(node)); + } + badType(node: ts.Node, type: ts.Type): never { const widened = this.checker.getBaseTypeOfLiteralType(type); // Types declared by the ADOPTED @types/node (Buffer, NodeJS.Timeout, @@ -8691,8 +9920,12 @@ export class Lowerer { // applies to node-typed values. const typeSym = widened.getAliasSymbol() ?? widened.getSymbol(); if (this.nodeTypesOnlySymbol(typeSym)) { - this.pushDiag(noLoweringDiag(this.checker.typeToString(type), locOf(node), undefined, true)); - throw new PoisonError(); + this.typeFail( + node, + type, + "node-types", + noLoweringDiag(this.checker.typeToString(type), locOf(node), undefined, true), + ); } // Engine-backed ambient TYPES in a STATIC build report the per-site // SC2012 rather than the generic supported-types recitation. Native @@ -8703,10 +9936,12 @@ export class Lowerer { (FETCH_AMBIENT_TYPES as readonly string[]).includes(ts.symbolName(typeSym)) && this.isStdlibSymbol(typeSym) ) { - this.pushDiag( + this.typeFail( + node, + type, + "fetch-ambient", unsupportedNativeApiDiag(`a value of type '${ts.symbolName(typeSym)}'`, locOf(node)), ); - throw new PoisonError(); } // ENUM OBJECTS (`typeof e` — a numeric enum's type carries the // reverse-map index signature, a string enum's just its members): @@ -8715,14 +9950,16 @@ export class Lowerer { // as the per-site identifier fence — never the index-signature // recitation. if (typeSym && (typeSym.flags & ts.SymbolFlags.Enum) !== 0) { - this.pushDiag( + this.typeFail( + node, + type, + "enum-object", unsupportedDiag( "SC1090", locOf(node), `enum objects as values ('${ts.symbolName(typeSym)}' — member reads like '${ts.symbolName(typeSym)}.X' compile to constants; the object itself has no runtime representation)`, ), ); - throw new PoisonError(); } // Would-be records with INDEX SIGNATURES (`Record`, // `{ [k: string]: T }`) get the index-signature fence (SC2006) naming @@ -8752,19 +9989,26 @@ export class Lowerer { this.isStdlibFile(ts.getSourceFileOfNode(d)), ) ) { - this.pushDiag(noLoweringDiag(this.checker.typeToString(type), locOf(node))); - throw new PoisonError(); + this.typeFail( + node, + type, + "stdlib-index", + noLoweringDiag(this.checker.typeToString(type), locOf(node)), + ); } - this.pushDiag(indexSignatureTypeDiag(this.checker.typeToString(type), locOf(node))); - throw new PoisonError(); + this.typeFail( + node, + type, + "index-signature", + indexSignatureTypeDiag(this.checker.typeToString(type), locOf(node)), + ); } // Attribute an unsupported package type to its package. { const pkg = this.npmPackageOf(widened); if (pkg) { - this.pushDiag(unsupportedPackageValueDiag(pkg, locOf(node))); - throw new PoisonError(); + this.typeFail(node, type, "package", unsupportedPackageValueDiag(pkg, locOf(node))); } } // A type that keeps a GENERIC call signature (`(x: T) => T` slots, @@ -8780,8 +10024,12 @@ export class Lowerer { this.checker.getCallSignatures(p).some((s) => s.getTypeParameters().length > 0), ) ) { - this.pushDiag(genericSignatureTypeDiag(this.checker.typeToString(type), locOf(node))); - throw new PoisonError(); + this.typeFail( + node, + type, + "generic-signature", + genericSignatureTypeDiag(this.checker.typeToString(type), locOf(node)), + ); } } // STANDARD-LIBRARY nominal provenance, decided once: interface/class @@ -8801,16 +10049,24 @@ export class Lowerer { // generic overload set keeps the monomorphization story: a value of a // multi-signature type has no single compiled signature to hold. if (!stdlibNominal && this.checker.getCallSignatures(widened).length > 1) { - this.pushDiag(overloadedSignatureTypeDiag(this.checker.typeToString(type), locOf(node))); - throw new PoisonError(); + this.typeFail( + node, + type, + "overloads", + overloadedSignatureTypeDiag(this.checker.typeToString(type), locOf(node)), + ); } // INTERSECTIONS that resolved to no lowering (SC2008). The ones that // compile never get here: member intersections intern through the // record path, callable hybrids map to '%call' records, and pinned // mixin instantiations resolve by chain structure. if (widened.isIntersectionType()) { - this.pushDiag(intersectionTypeDiag(this.checker.typeToString(type), locOf(node))); - throw new PoisonError(); + this.typeFail( + node, + type, + "intersection", + intersectionTypeDiag(this.checker.typeToString(type), locOf(node)), + ); } // SUPPORTED shapes over a component outside its slot (SC2009): the // Map/Set domains, array/tuple elements, union arms, function @@ -8822,8 +10078,12 @@ export class Lowerer { { const detail = describeComponentBlocker(widened, this.typeCtx); if (detail !== null) { - this.pushDiag(componentTypeDiag(this.checker.typeToString(type), detail, locOf(node))); - throw new PoisonError(); + this.typeFail( + node, + type, + "component", + componentTypeDiag(this.checker.typeToString(type), detail, locOf(node)), + ); } } // STANDARD-LIBRARY nominal types with no lowering at all: the SC2020 @@ -8853,26 +10113,36 @@ export class Lowerer { TextDecoder: "TextDecoder instances support recognized labels, fatal/ignoreBOM option literals, and streaming UTF-8/UTF-16/single-byte decode; runtime option objects remain unsupported", }; - this.pushDiag( + this.typeFail( + node, + type, + "stdlib-nominal", noLoweringDiag( this.checker.typeToString(type), locOf(node), typeHints[ts.symbolNameOf(stdlibOwnSym) ?? ""], ), ); - throw new PoisonError(); } // USER record shapes blocked by ONE member (SC2009's record arm): // name the member and its type. { const detail = describeRecordMemberBlocker(widened, this.typeCtx); if (detail !== null) { - this.pushDiag(componentTypeDiag(this.checker.typeToString(type), detail, locOf(node))); - throw new PoisonError(); + this.typeFail( + node, + type, + "record-member", + componentTypeDiag(this.checker.typeToString(type), detail, locOf(node)), + ); } } - this.pushDiag(unsupportedTypeDiag(this.checker.typeToString(type), locOf(node))); - throw new PoisonError(); + this.typeFail( + node, + type, + "unsupported", + unsupportedTypeDiag(this.checker.typeToString(type), locOf(node)), + ); } /** The lib fence (SC2020): a reached use of standard-library surface @@ -8970,7 +10240,25 @@ export class Lowerer { } typeOf(node: ts.Node): ts.Type { + if (ts.isIdentifier(node)) { + const inferred = inferredCollectionType(this, node); + if (inferred) return inferred; + } if (this.anySymbols.size > 0 && this.readsAnySymbol(node)) return this.checker.getAnyType(); + // A checked-dynamic argument that another overload could take at run + // time leaves the resolved overload's return a guess, and JS bindings + // initialized with that result hold whatever it was. + if ( + (ts.isCallExpression(node) || ts.isNewExpression(node)) && + dynamicOverloadChoice(this, node) + ) + return this.checker.getAnyType(); + if ( + ts.isIdentifier(node) && + isJsSourceFile(ts.getSourceFileOfNode(node)) && + guessedOverloadIdentifier(this, node) + ) + return this.checker.getAnyType(); if (ts.isPropertyAccessExpression(node) && isJsSourceFile(ts.getSourceFileOfNode(node))) { const receiver = node.expression.kind === ts.SyntaxKind.ThisKeyword && this.fnStack.length > 0 @@ -9275,6 +10563,51 @@ export class Lowerer { return (sym && this.typeParamTsBindings.get(sym)) ?? null; }; + /** The concrete instantiation of a generic alias reference (`Catalog`) the current call-keyed instance's signature declares + * (boundAliasInstances), or null when its signature never instantiated + * that reference. Inert outside generic function instantiation. */ + readonly boundAliasTsResolver = (t: ts.Type): ts.Type | null => { + if (!this.typeParamTsBindings || t.getAliasTypeArguments().length === 0) return null; + const tsBindings = this.typeParamTsBindings; + return ( + this.boundAliasInstances.get(t)?.find((entry) => entry.tsBindings === tsBindings)?.instance ?? + null + ); + }; + + private structuredCloneReadCounts: { workerData: number; messageListeners: number } | null = null; + + /** How often the program reads `workerData` (every expression spelling it, + * counted conservatively) and registers a `"message"` listener: a + * structured clone read at one site only has no other holder + * (narrowing.ts freshDynSource). Computed once per lowering pass. */ + structuredCloneReads(): { workerData: number; messageListeners: number } { + if (this.structuredCloneReadCounts) return this.structuredCloneReadCounts; + const counts = { workerData: 0, messageListeners: 0 }; + const visit = (node: ts.Node): void => { + if (ts.isIdentifier(node) && node.text === "workerData") { + const parent = node.parent; + const declaresName = + parent !== undefined && + (ts.isImportSpecifier(parent) || + ts.isExportSpecifier(parent) || + ((ts.isPropertyAssignment(parent) || + ts.isPropertyDeclaration(parent) || + ts.isPropertySignature(parent) || + ts.isMethodDeclaration(parent)) && + parent.name === node)); + if (!declaresName) counts.workerData++; + } else if (ts.isCallExpression(node) && isMessageListenerCall(node)) { + counts.messageListeners++; + } + ts.forEachChild(node, visit); + }; + for (const sf of this.moduleOrder) ts.forEachChild(sf, visit); + this.structuredCloneReadCounts = counts; + return counts; + } + irTypeOf(node: ts.Node): IrType { const t = this.typeOf(node); // JS inference can leave mutable fields, accessor parameters and @@ -9289,6 +10622,7 @@ export class Lowerer { // f64 — but must not: pre-empt the mapping so the JS fallback below // answers instead. const mapped = neverTaintedJsType(this, node, t) ? null : this.mapTypeOf(t); + if (mapped !== null && this.degradedTypes.size > 0) this.noteDegradedSlot(node, t); if (!mapped) { const argumentsType = jsArgumentsFunctionType(this, t); if (argumentsType !== null) return argumentsType; @@ -9550,6 +10884,18 @@ export class Lowerer { const result = this.coerceToExpected(expr.result, expected); return { ...expr, result, type: result.type }; } + // An instance of a class declared `extends Array` is its array (the + // same object); an array slot of another element type converts it as + // an array. + if (expr.type.kind === "object" && expected.kind !== "object") { + const arrayClass = this.classes.get(expr.type.className); + if (arrayClass && arrayRootOf(arrayClass)) { + const asArray = arrayOfClassInstance(arrayClass, expr, expr.loc); + return typeEquals(asArray.type, expected) + ? asArray + : this.coerceToExpected(asArray, expected); + } + } // A destination that holds undefined takes an unassigned deferred-init // field's slot as is (Node passes the undefined along). if (expected.kind === "union" && this.armTag(expected.unionId, UNDEFINED_T) >= 0) { @@ -9681,10 +11027,8 @@ export class Lowerer { // so its module binding is too (`ReleaseEmitter = EventEmitter` over // `const EventEmitter = require("events")`). if (expr.type.kind === "moduleNs" && isJsSourceFileName(expr.loc.file)) { - const object = moduleNamespaceObject( - this, - expr as IrExpr & { type: IrType & { kind: "moduleNs" } }, - ); + const ns = expr as IrExpr & { type: IrType & { kind: "moduleNs" } }; + const object = moduleNamespaceObject(this, ns) ?? moduleNamespaceValue(this, ns); if (object) return object; } if ( @@ -9734,6 +11078,8 @@ export class Lowerer { // name Node's function carries (`[Function: randomUUID]`). const builtinName = expr.kind === "closure" ? this.builtinCallableValueNames.get(expr.fnName) : undefined; + const builtinLength = + expr.kind === "closure" ? this.builtinCallableValueLengths.get(expr.fnName) : undefined; return { kind: "dynFrom", value: expr, @@ -9741,6 +11087,7 @@ export class Lowerer { loc: expr.loc, ...(liveRef ? { liveRef: true as const } : {}), ...(builtinName !== undefined ? { fnName: builtinName } : {}), + ...(builtinLength !== undefined ? { fnLength: builtinLength } : {}), }; } return expr; @@ -9997,7 +11344,11 @@ export class Lowerer { expected.kind !== "void" && this.armTag(expr.type.unionId, expected) >= 0 ) { - const helper = this.narrowedArmHelper(expr.type.unionId, expected, expr.loc); + // A function's widened inferred return (widenReducedReturn) also + // holds records with more fields; those convert instead of throwing. + const helper = widensReducedRecords(this, expected, expr.type) + ? widthNarrowHelper(this, expr.type.unionId, expected, expr.loc) + : this.narrowedArmHelper(expr.type.unionId, expected, expr.loc); if (helper) { return this.checkedNarrowCall(helper, expr, expected, expr.loc); } @@ -10025,6 +11376,8 @@ export class Lowerer { if (!typeEquals(expr.type, expected)) { const w = this.widthCoerce(expr, expected); if (w) return w; + const asserted = assertedConversion(this, expr, expected); + if (asserted) return asserted; // A UNIT (null/undefined) flowing into a plain non-nullable slot // the checker approved (`null!` casts, non-strict assignments): // the stranded-source stance (divergence 38) without a union in @@ -10043,7 +11396,7 @@ export class Lowerer { if (helper) { return { kind: "call", callee: helper, args: [expr], type: expected, loc: expr.loc }; } - return expr; + return assertedConversion(this, expr, expected) ?? expr; } if (expr.type.kind === "void") { // A void CALL RESULT flowing into a union with an undefined arm @@ -10121,6 +11474,31 @@ export class Lowerer { } } } + // A promise or function value against a union whose ONLY arm of + // that kind differs from it (`Promise` into `Promise | + // undefined`, `() => Promise` into an optional + // `() => Promise` member): convert into + // that arm with the arm-level conversion (the checked payload + // widening, the function adapter), then wrap. Several arms of the + // kind stay ambiguous and fall through to the union fence. + if (expr.type.kind === "promise" || expr.type.kind === "func") { + const arms = this.unions.get(expected.unionId)?.arms ?? []; + const candidates = arms.filter((arm) => arm.kind === expr.type.kind); + const arm = candidates.length === 1 ? candidates[0]! : null; + if (arm) { + const converted = this.coerceToExpected(expr, arm); + if (converted !== expr && typeEquals(converted.type, arm)) { + return { + kind: "unionWrap", + unionId: expected.unionId, + tag: arms.indexOf(arm), + value: converted, + type: expected, + loc: expr.loc, + }; + } + } + } // A width-coercible value against a union: coerce into the SINGLE // width-liftable arm, then wrap like any arm value (widthLiftPlan's // liftWrap — several candidate arms are ambiguous and decline). @@ -10341,8 +11719,8 @@ export class Lowerer { return unionRetagHelper(this, fromId, toId, loc, trappable); } - narrowedArmHelper(fromId: string, target: IrType, loc: SrcLoc): string | null { - return narrowedArmHelper(this, fromId, target, loc); + narrowedArmHelper(fromId: string, target: IrType, loc: SrcLoc, message?: string): string | null { + return narrowedArmHelper(this, fromId, target, loc, message); } checkedNarrowCall(helper: string, value: IrExpr, type: IrType, loc: SrcLoc): IrExpr { @@ -10357,9 +11735,51 @@ export class Lowerer { * wrap implicitly (coerceToExpected), then the exact-type fence runs * (SC2002 for record shapes, SC2003 for unions). Every slot-directed * lowering goes through here (via lowerExprExpecting) or calls this - * directly when the expression was already lowered. */ + * directly when the expression was already lowered. In package files, a + * check this coercion creates names the package declaration that types + * the slot (decl-origin.ts). */ coerceInto(node: ts.Node, expr: IrExpr, expected: IrType): IrExpr { + if (!this.inPackageFileOf(node)) return this.coerceIntoSlot(node, expr, expected); + const outer = this.coercionOriginNode; + const outerUnion = this.coercionOriginUnion; + this.coercionOriginNode = node; + this.coercionOriginUnion = expr.type.kind === "union" ? expr.type.unionId : null; + let out: IrExpr; + try { + out = this.coerceIntoSlot(node, expr, expected); + } finally { + this.coercionOriginNode = outer; + this.coercionOriginUnion = outerUnion; + } + if (out !== expr && out.kind === "dynCheck" && out.origin === undefined) { + const origin = this.slotOriginOf(node); + if (origin !== null) return { ...out, origin: this.declOriginIndex(origin) }; + } + return out; + } + + private coerceIntoSlot(node: ts.Node, expr: IrExpr, expected: IrType): IrExpr { refuseInferredCallbackDrops(this, node, expr.type, expected); + // A union into one of its record arms extracts that arm, trusting the + // checker to have excluded the others. A built-in object arm the checker + // still types the site with (`c ?? new Map()` into an interface the + // untyped Map satisfies only through `any`) holds a value at run time, + // and no conversion makes a record of it, so the flow is refused like + // the arm's value would be. + if ( + expr.type.kind === "union" && + expected.kind === "record" && + this.armTag(expr.type.unionId, expected) >= 0 + ) { + const site = this.mapTypeOf(this.typeOf(node)); + const siteArms = + site?.kind === "union" ? (this.unions.get(site.unionId)?.arms ?? []) : site ? [site] : []; + for (const arm of this.unions.get(expr.type.unionId)?.arms ?? []) { + if (arm.kind !== "map" && arm.kind !== "set") continue; + if (!siteArms.some((s) => typeEquals(s, arm))) continue; + this.requireExactShape(node, arm, expected); + } + } // An assertion's scalar check whose value goes straight back into a // checked-dynamic slot (`process.loadEnvFile({} as unknown as string)`) // keeps the original value: JavaScript erases assertions, so the check @@ -10522,6 +11942,25 @@ export class Lowerer { } } + // A value the checker types `never` at this site (a read narrowed past + // every member of its union) exists only when an assertion lied about + // its type, so its layout need not fit the slot: it is evaluated, then a + // TypeError is thrown, as checked extraction of an impossible arm does. + if (!typeEquals(e.type, expected) && (this.typeOf(node).flags & ts.TypeFlags.Never) !== 0) { + return { + kind: "seqExpr", + stmts: [{ kind: "exprStmt", expr: e, loc: e.loc }], + result: nodeThrowExpr( + 1, + "", + unionNarrowFailureMessage(e.type, (t) => this.fmt(t)), + expected, + e.loc, + ), + type: expected, + loc: e.loc, + }; + } this.requireExactShape(node, e.type, expected); return e; } @@ -10845,6 +12284,7 @@ export class Lowerer { !this.mapTypeOf(this.typeOf(x)) || this.excessJsContextualProperty(x, parts) || this.reshapeableContextualLiteral(x, parts) || + this.untypedLiteralContext(x) || holdsEvolvingArray(this, x) ) { return lowerDynObjectLiteral(this, x); @@ -11438,6 +12878,7 @@ export class Lowerer { const list = this.deferredDiags.get(symbol) ?? []; list.push(...sink); this.deferredDiags.set(symbol, list); + this.deferredCollectors.set(symbol, { symbolOf, collect }); return symbol; } finally { this.diagSink = previousSink; @@ -11454,20 +12895,48 @@ export class Lowerer { this.reportDeferred(symbol); } + /** A statement's lowering used a declaration: if that declaration was + * rejected, the statement's own failure (if any) is its cascade. */ + touchRejected(symbol: ts.Symbol): void { + const group = this.rejectedGroups.get(symbol); + if (group === undefined) return; + const frame = this.failureTouches[this.failureTouches.length - 1]; + if (frame !== undefined) frame.add(group); + } + + /** Records a declaration whose deferred diagnostics reached the build: + * its failure group, on the group's diagnostics too. */ + private noteRejected(symbol: ts.Symbol, diags: readonly ScrDiagnostic[]): void { + let group = this.rejectedGroups.get(symbol); + if (group === undefined) { + group = declarationGroupKey(symbol); + this.rejectedGroups.set(symbol, group); + } + for (const d of diags) d.rejects ??= group; + } + /** flushDeferred without the collection-pass guard: for declarations * whose diagnostics must report eagerly (class statements with * declaration-time code, extended bases — collectClassShapeDeferring). */ reportDeferred(symbol: ts.Symbol): void { const diags = this.deferredDiags.get(symbol); - if (!diags) return; + if (!diags) { + this.touchRejected(symbol); + return; + } if (this.alreadyFlushed.has(symbol)) { this.deferredDiags.delete(symbol); // the emit pass reported these return; } if (this.diagSink === null) { + if (this.retryDeferred(symbol, diags)) return; + // A failed retry leaves the last attempt's diagnostics deferred. + const committed = this.deferredDiags.get(symbol) ?? diags; this.deferredDiags.delete(symbol); this.flushedSymbols.add(symbol); - for (const d of diags) this.pushDiag(d); + this.noteRejected(symbol, committed); + for (const d of committed) this.pushDiag(d); + this.touchRejected(symbol); return; } // A capture buffer is active (a probe, or another declaration's @@ -11484,6 +12953,7 @@ export class Lowerer { * (the declaration counts as flushed for the coverage remainder). */ private commitFlush(symbol: ts.Symbol, source: ScrDiagnostic): void { this.flushedSymbols.add(symbol); + this.noteRejected(symbol, [source]); const list = this.deferredDiags.get(symbol); if (!list) return; const index = list.indexOf(source); @@ -11491,9 +12961,132 @@ export class Lowerer { if (list.length === 0) this.deferredDiags.delete(symbol); } - flushDeferredClass(className: string): void { + /** Flushes a deferred class known by its IR name, and answers the class + * when the flush registered it (retryDeferred). */ + flushDeferredClass(className: string): ClassInfo | undefined { const symbol = this.deferredClassByName.get(className); if (symbol) this.flushDeferred(symbol); + return this.classes.get(className); + } + + /** Localized failure's declaration commit point (design §3.3): a + * reference is flushing a deferred declaration into the build. When + * every deferred diagnostic is a degradable type failure at one of the + * declaration's slots, the failing types degrade and the collection + * runs again (up to MAX_DECLARATION_RETRIES rounds, each degrading the + * types the previous round still failed on). On success the + * declaration registers, each degraded slot is an SC6104, and nothing + * reports. Otherwise the last round's diagnostics stay deferred for the + * caller to commit. */ + private retryDeferred(symbol: ts.Symbol, diags: readonly ScrDiagnostic[]): boolean { + if (this.tierMode !== "auto") return false; + if (this.staticInitsPassed.has(symbol)) return false; + const collector = this.deferredCollectors.get(symbol); + if (collector === undefined) return false; + const slots: DynamicSlot[] = []; + // The types this retry degraded (not those of retries its collections + // ran), for retryDeferredAtDeclaration to restore when it fails. + const degraded: number[] = []; + try { + return this.retryDeferredRounds(symbol, collector, diags, slots, degraded); + } finally { + this.retryDegraded = degraded; + } + } + + private retryDeferredRounds( + symbol: ts.Symbol, + collector: { symbolOf: () => ts.Symbol | undefined; collect: () => void }, + diags: readonly ScrDiagnostic[], + slots: DynamicSlot[], + degraded: number[], + ): boolean { + let current = diags; + for (let round = 0; round < MAX_DECLARATION_RETRIES; round++) { + const failing: { diag: ScrDiagnostic; failure: TypeFailure; id: number }[] = []; + let retriable = false; + for (const diag of current) { + if (this.retriableDiags.has(diag)) { + retriable = true; + continue; + } + const failure = diag.typeFailure; + // The dyn class field policy lifts during the retry; nothing to + // degrade. + if (failure?.family === "dyn-field-policy") { + retriable = true; + continue; + } + const type = failure === undefined ? undefined : this.failedTypes.get(failure); + const id = (type as { id?: number } | undefined)?.id; + if ( + failure === undefined || + id === undefined || + !failure.degradable || + !DECLARATION_SLOTS.has(failure.slot) || + this.degradedTypes.has(id) + ) + return false; + failing.push({ diag, failure, id }); + } + // A round makes progress by degrading a type, or (once) by running + // a collection that only waited for another declaration. + if (failing.length === 0 && !(retriable && round === 0)) return false; + for (const { diag, failure, id } of failing) { + this.degradedTypes.set(id, { lenient: DYN, code: diag.code, reason: diag.message }); + degraded.push(id); + slots.push({ loc: diag.loc, failure, code: diag.code, reason: diag.message }); + } + this.deferredDiags.delete(symbol); + const lifted = this.liftDynFieldPolicy; + this.liftDynFieldPolicy = true; + let registered: boolean; + try { + registered = this.collectDeferring(collector.symbolOf, collector.collect) === null; + } finally { + this.liftDynFieldPolicy = lifted; + } + if (registered) { + this.retriedSymbols.add(symbol); + this.deferredCollectors.delete(symbol); + for (const [name, deferred] of this.deferredClassByName) + if (deferred === symbol) this.deferredClassByName.delete(name); + for (const slot of slots) this.noteDynamicSlot(slot); + for (const decl of symbol.declarations) + if (ts.isFunctionDeclaration(decl) || ts.isClassDeclaration(decl)) { + this.retriedDeclarations.push(decl); + this.onDeclarationRegistered?.(decl); + } + return true; + } + current = this.deferredDiags.get(symbol) ?? []; + } + return false; + } + + private retryDegraded: number[] = []; + + /** A deferred class with static field initializers is reached where its + * statement evaluates: Node runs the initializers there, referenced or + * not. Its file's %init calls this at that place, which retries the class + * as a flush would, so the initializers lower in place. A failed retry + * reports nothing and leaves the class deferred as it was, and from here + * on no flush registers it (staticInitsPassed): it reports instead. + * Returns whether the class has registered. */ + retryDeferredAtDeclaration(symbol: ts.Symbol): boolean { + const diags = this.deferredDiags.get(symbol); + let registered = !this.deferredCollectors.has(symbol); + if (!registered && diags !== undefined) { + this.retryDegraded = []; + registered = this.retryDeferred(symbol, diags); + if (!registered) { + for (const id of this.retryDegraded) this.degradedTypes.delete(id); + this.deferredDiags.set(symbol, [...diags]); + } + this.retryDegraded = []; + } + this.staticInitsPassed.add(symbol); + return registered; } collectSignature(decl: ts.FunctionDeclaration): void { @@ -11644,6 +13237,17 @@ export class Lowerer { return lowerClassExpressionInfo(this, expr); } + /** Batch-prefetches the symbols of every runtime identifier of the + * program's files. Collection's early-binding scan and the runtime- + * optional analysis look them up across every body, reached or not; + * one batch answers both, instead of one request per identifier. */ + private prefetchProgramSymbols(parts: FileParts[]): void { + this.checker.prefetchSymbolRoots( + parts.map((fp) => fp.sf), + true, + ); + } + /** Depth of the whole-program analyses that query types across every * file before reachability is known (speculativeTypeQueries). */ private speculativeTypeQueryDepth = 0; @@ -11761,6 +13365,9 @@ export class Lowerer { * bodies as worklist units (the units map is otherwise built before * lowering starts). Null in the emit pass. */ onExprClassCollected: ((info: ClassInfo) => void) | null = null; + /** emitReachable's unit registration for a declaration retryDeferred + * registered after collection. */ + onDeclarationRegistered: ((decl: ts.Node) => void) | null = null; /** Mixin functions (`(Base: T) => class extends Base {…}`) by their * function-like node: recognized shape, or null for checked @@ -11842,12 +13449,15 @@ export class Lowerer { /** Method-like members with their collected method-map names. Body * lowering omits abstract declarations; ABI analysis must include them - * because an abstract-typed call selects the same virtual slot. */ - *classMethodMembers( + * because an abstract-typed call selects the same virtual slot. An array, + * not a generator: a generator's yielded values are dynamic values, which + * would make every syntax node one. */ + classMethodMembers( info: ClassInfo, includeAbstract = false, - ): Generator<{ mName: string; member: ts.MethodDeclaration | ts.AccessorDeclaration }> { - if (!info.decl) return; // builtin error classes: runtime-provided bodies + ): { mName: string; member: ts.MethodDeclaration | ts.AccessorDeclaration }[] { + const members: { mName: string; member: ts.MethodDeclaration | ts.AccessorDeclaration }[] = []; + if (!info.decl) return members; // builtin error classes: runtime-provided bodies for (const member of info.decl.members.nodes) { const fnLike = ts.isMethodDeclaration(member) || ts.isGetAccessor(member) || ts.isSetAccessor(member) @@ -11871,8 +13481,9 @@ export class Lowerer { : `${ts.isGetAccessor(fnLike) ? "get" : "set"}:${baseName}`; const signature = info.methods.get(mName); if (!signature || (!fnLike.body && !(includeAbstract && signature.abstract))) continue; - yield { mName, member: fnLike }; + members.push({ mName, member: fnLike }); } + return members; } lowerClassCtor(info: ClassInfo): IrFunction { @@ -11935,6 +13546,8 @@ export class Lowerer { /* ── scoping and captures ─────────────────────────────────────────── */ declareLocal(nameNode: ts.Node, name: string, type: IrType, mutable: boolean): IrLocal { + if (type.kind === "dyn" && this.degradedTypes.size > 0) + this.noteDegradedSlot(nameNode, this.typeOf(nameNode)); const symbol = this.checker.getSymbolAtLocation(nameNode); const local = declareContextLocal( this.ctx, @@ -12012,7 +13625,7 @@ export class Lowerer { const fn = decl.parent; const symbol = this.checker.getSymbolAtLocation(nameNode); const isRead = (id: ts.Identifier): boolean => - id !== nameNode && this.checker.getSymbolAtLocation(id) === symbol; + id !== nameNode && bindingSymbolOfRead(this, id) === symbol; const pure = fn !== undefined && symbol !== undefined && onlyForwarded(this, fn, isRead); // A forwarded-only tuple is never built: a tuple with optional // positions (`Parameters<(n?: T) => R>`, whose length varies per call) @@ -12594,8 +14207,8 @@ export class Lowerer { return lowerStmts(this, stmts); } - noteBlockedBindings(stmt: ts.Statement): void { - return noteBlockedBindings(this, stmt); + noteBlockedBindings(stmt: ts.Statement, diags?: readonly ScrDiagnostic[]): void { + return noteBlockedBindings(this, stmt, diags); } isBlockedBinding(symbol: ts.Symbol | null): boolean { @@ -12815,12 +14428,30 @@ export class Lowerer { /* ── expressions ──────────────────────────────────────────────────── */ lowerExpr(expr: ts.Expression): IrExpr { + if (this.speculatedClosures.size > 0) { + const speculated = this.speculatedClosure(expr); + if (speculated !== null) { + this.speculatedClosures.delete(expr); + return speculated; + } + } const streamValue = this.valueStreamReceivers.get(expr); if (streamValue) return valueStreamOf(varRef(streamValue.localId, streamValue.type, locOf(expr))); return this.stagedArgument(expr) ?? lowerExpr(this, expr); } + /** The value storedImplicitArgumentType lowered for `expr` in the active + * function and type bindings (speculatedClosures), or null. */ + speculatedClosure(expr: ts.Expression): IrExpr | null { + const entry = this.speculatedClosures.get(expr); + return entry !== undefined && + entry.ctx === this.ctx && + entry.bindings === this.typeParamBindings + ? entry.value + : null; + } + /** The saved value of a staged fs argument (stagedArguments, keyed * without type-only wrappers), or null for every other expression. */ private stagedArgument(node: ts.Expression): IrExpr | null { @@ -12851,7 +14482,7 @@ export class Lowerer { readonly isStdlibFile = (sf: ts.SourceFile): boolean => sf.fileName === this.ambient || sf.fileName === this.overridesAmbient || - sf.fileName === this.fallbackAmbient || + isFallbackDtsPath(sf.fileName) || this.program.isSourceFileDefaultLibrary(sf) || (sf.isDeclarationFile && isNodeTypesPath(sf.fileName)); @@ -12932,6 +14563,10 @@ export class Lowerer { ) { return lowerElementAccess(this, expr); } + // `a?.[i]`: the optional chain tests the receiver's undefined arm and + // lowers this access again inside its guard. + if (expr.questionDotToken && !this.chainHandled.has(expr)) + return lowerElementAccess(this, expr); const receiver = this.runtimeOptionalIdentifierValue(expr.expression); const receiverValue: IrExpr | null = receiver && (receiver.present.kind === "array" || receiver.present.kind === "record") @@ -13002,7 +14637,7 @@ export class Lowerer { } lowerElementWrite(expr: ts.BinaryExpression): IrStmt { - return lowerElementWrite(this, expr); + return markSloppyRecordWrites(this, lowerElementWrite(this, expr), expr); } lowerLiveWebValue(node: ts.Expression): IrExpr { @@ -13211,9 +14846,11 @@ export class Lowerer { lowerStaticBlobCall(this, expr) ?? lowerWebCryptoCall(this, expr) ?? lowerKeyObjectCall(this, expr) ?? + lowerX509Call(this, expr) ?? lowerEcdhCall(this, expr) ?? lowerDiffieHellmanCall(this, expr) ?? lowerNetAddressCall(this, expr) ?? + lowerMimeCall(this, expr) ?? lowerFetchElementMethodCall(this, expr) ?? lowerDynamicImportCall(this, expr) ?? lowerCall(this, expr) @@ -13257,7 +14894,9 @@ export class Lowerer { call: ts.CallExpression, access: ts.PropertyAccessExpression, ): IrExpr | null { - return lowerArrayMethodCall(this, call, access); + return lowerWithArraySpecies(this, call, access, () => + lowerArrayMethodCall(this, call, access), + ); } lowerMapMethodCall(call: ts.CallExpression, access: ts.PropertyAccessExpression): IrExpr | null { @@ -13343,16 +14982,29 @@ export class Lowerer { return accessorCall(this, className, member, obj, extraArgs, ret, loc); } - classIteratorOf(t: IrType): ClassIteratorInfo | null { - return classIteratorOf(this, t); + classIteratorOf(t: IrType, options?: ClassIteratorOptions): ClassIteratorInfo | null { + return classIteratorOf(this, t, options); + } + + classIteratorReturnCall(cit: ClassIteratorInfo, it: IrExpr, loc: SrcLoc): IrExpr { + return classIteratorMethodCall(this, cit, "return", it, loc); } classIteratorOpenCall(cit: ClassIteratorInfo, recv: IrExpr, loc: SrcLoc): IrExpr { return classIteratorOpenCall(this, cit, recv, loc); } - classIteratorNextCall(cit: ClassIteratorInfo, itRef: IrExpr, loc: SrcLoc): IrExpr { - return classIteratorNextCall(this, cit, itRef, loc); + classIteratorNextRead(cit: ClassIteratorInfo, itRef: IrExpr, loc: SrcLoc): IrExpr | null { + return classIteratorNextRead(this, cit, itRef, loc); + } + + classIteratorNextCall( + cit: ClassIteratorInfo, + itRef: IrExpr, + next: IrExpr | null, + loc: SrcLoc, + ): IrExpr { + return classIteratorNextCall(this, cit, itRef, next, loc); } classIteratorDrainCall(src: IrExpr, loc: SrcLoc, elemT?: IrType): IrExpr | null { @@ -13399,7 +15051,7 @@ export class Lowerer { } fieldSetStmt(target: FieldTarget, value: IrExpr, loc: SrcLoc, blame: ts.Node): IrStmt { - return fieldSetStmt(this, target, value, loc, blame); + return markSloppyRecordWrites(this, fieldSetStmt(this, target, value, loc, blame), blame); } lowerFieldCompound( @@ -13438,7 +15090,9 @@ export class Lowerer { lowerResponseNew(this, expr) ?? lowerBlobNew(this, expr) ?? lowerNetAddressNew(this, expr) ?? + lowerMimeNew(this, expr) ?? lowerStaticReadableStreamNew(this, expr) ?? + lowerStaticTransformStreamNew(this, expr) ?? lowerConsoleNew(this, expr); return ( builtin ?? this.withRuntimeOptionalClassValue(expr.expression, () => lowerNew(this, expr)) @@ -13537,6 +15191,18 @@ export class Lowerer { * member access that IS a module in its own right (`fs.promises` — the * same object as node:fs/promises, Node's rule). Null otherwise. */ builtinNamespaceModuleOf(expr: ts.Expression): string | null { + // A function declaration is never a module namespace. Answering that + // from the symbol skips the type mapping below (every direct call asks). + if (ts.isIdentifier(expr)) { + const symbol = this.checker.getSymbolAtLocation(expr); + if ( + symbol !== undefined && + (symbol.flags & ts.SymbolFlags.Alias) === 0 && + symbol.declarations.length > 0 && + symbol.declarations.every(ts.isFunctionDeclaration) + ) + return null; + } // Default and CommonJS process bindings alias the global object, whose // methods and properties keep their existing process lowerings. if (stdlibGlobalNameOf(this, expr) === "process") return null; @@ -13650,7 +15316,7 @@ export class Lowerer { if ( ts.isPropertyAccessExpression(expr) && !expr.questionDotToken && - ts.isIdentifier(expr.expression) + (ts.isIdentifier(expr.expression) || requireSpecOf(expr.expression) !== null) ) { const outer = this.builtinNamespaceModuleOf(expr.expression); if (outer === null) return null; @@ -13834,11 +15500,12 @@ export class Lowerer { if (bi.module === "crypto" && bi.member === "webcrypto") { return { kind: "libCall", fn: "webcrypto.object", args: [], type: DYN, loc }; } - // node:stream/web's exports are the web stream globals themselves: in - // JavaScript sources the same identity token as the global spelling - // (`require("stream/web").ReadableStream === globalThis.ReadableStream`). - if (bi.module === "stream/web" && isJsSourceFile(ts.getSourceFileOfNode(expr))) { - return { kind: "strLit", value: `[builtin ${bi.member}]`, type: STRING, loc }; + // Members that are global objects (`require("stream/web").ReadableStream + // === globalThis.ReadableStream`) read in JavaScript sources as their + // global spelling does. + if (isJsSourceFile(ts.getSourceFileOfNode(expr))) { + const global = globalMemberValue(this, bi, loc); + if (global) return global; } // A bare `fs.constants` read (not one of the baked bits above). if (bi.member === "constants" && bi.module === "fs") return fsConstantsObject(loc); @@ -13855,6 +15522,15 @@ export class Lowerer { `library functions as values (call '${ts.getTextOfNode(expr)}' directly)`, ); } + // A member that is a supported module in its own right (`fs.promises` + // is node:fs/promises, as in Node) reads as that module's namespace. + { + const sub = this.builtinNamespaceModuleOf(expr); + if (sub !== null) { + const type: IrType = { kind: "moduleNs", moduleId: `builtin:${sub}` }; + return { kind: "moduleNsRef", moduleId: type.moduleId, type, loc }; + } + } this.noLowering( `${bi.module}.${bi.member}`, expr, @@ -13869,7 +15545,13 @@ export class Lowerer { * deliberately narrower than table membership because many table rows * are only dispatch sentinels for call-site-specific lowering. */ lowerBuiltinCallableValue(bi: { module: string; member: string }, loc: SrcLoc): IrExpr | null { + const assertion = assertFunctionValue(bi, loc); + if (assertion) return assertion; if (bi.module === "util" && bi.member === "types") return utilTypesModuleValue(this, loc); + // util.inspect: the function custom inspect methods receive, one + // identity per thread. + if (bi.module === "util" && bi.member === "inspect") + return { kind: "libCall", fn: "insp.inspectFunction", args: [], type: DYN, loc }; if (bi.module === "util/types" && builtinModuleFnOf(this, bi.module, bi.member)) return lowerUtilTypeValue(this, bi.member, loc); if (bi.module === "util" && bi.member === "getSystemErrorMap") @@ -13886,6 +15568,9 @@ export class Lowerer { loc, }; } + // The buffer module's Buffer is the global constructor's value. + if (bi.module === "buffer" && bi.member === "Buffer") + return { kind: "libCall", fn: "buffer.constructor", args: [], type: DYN, loc }; if (bi.module === "fs" && (bi.member === "realpathSync" || bi.member === "realpathSync.native")) return fsRealpathValue(this, bi.member === "realpathSync.native", loc); const fn = builtinModuleFnOf(this, bi.module, bi.member); @@ -13916,6 +15601,8 @@ export class Lowerer { fnName = keyedHelperName(this, `%builtin.value.${fn.valueFn ?? fn.fn}`, key); this.builtinCallableValueFns.set(key, fnName); this.builtinCallableValueNames.set(fnName, display.slice(display.lastIndexOf(".") + 1)); + if (fn.valueLength !== undefined) + this.builtinCallableValueLengths.set(fnName, fn.valueLength); const params = funcType.params.map((type, index) => ({ localId: `arg.${index}`, name: `arg${index}`, diff --git a/packages/compiler/src/frontend/lowering/module-namespace-elements.ts b/packages/compiler/src/frontend/lowering/module-namespace-elements.ts index cf828c6f0..2c8e6a475 100644 --- a/packages/compiler/src/frontend/lowering/module-namespace-elements.ts +++ b/packages/compiler/src/frontend/lowering/module-namespace-elements.ts @@ -1,4 +1,5 @@ import { dynUndefinedExpr, strLit, varRef } from "../../ir/build.js"; +import { arrayOf, funcOf } from "../../ir/ir.js"; import * as ts from "../ts7/adapter.js"; import { DYN, @@ -9,7 +10,7 @@ import { type IrType, } from "../../ir/ir.js"; import { locOf } from "../program.js"; -import { newFnCtx, type Lowerer } from "./lowerer.js"; +import { newFnCtx, PoisonError, type Lowerer } from "./lowerer.js"; /** Computed namespace reads select live exports instead of copying a * namespace into a registry snapshot. Constructor registries may contain @@ -171,3 +172,124 @@ export function moduleNamespaceObject( loc, }; } + +/** A module namespace taken as an untyped value whose exports a copy + * cannot represent: a binding that can change after initialization (`let`, + * `var`), or an export the copy cannot name. The value is the runtime's + * namespace object (scr_module_namespace_value): one per module, whose + * property reads call a compiled getter for the export's current value and + * whose writes throw Node's TypeError. An export whose value has no + * checked-dynamic form throws its fence when it is read, and listing the + * object's keys or inspecting it throws the "not supported yet" error. */ +export function moduleNamespaceValue( + lowerer: Lowerer, + ns: IrExpr & { type: IrType & { kind: "moduleNs" } }, +): IrExpr | null { + const source = lowerer.sourceFileOfModuleNamespace(ns.type); + if (!source) return null; + const symbol = lowerer.checker.getSymbolAtLocation(source); + if (!symbol) return null; + const exports: { key: string; declaration: ts.Node | undefined }[] = []; + for (const exported of lowerer.checker.getExportsOfModule(symbol)) { + const target = + exported.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(exported) : exported; + if (!(target.flags & ts.SymbolFlags.Value)) continue; + exports.push({ key: ts.symbolName(exported), declaration: target.valueDeclaration }); + } + exports.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0)); + const name = `%module.namespace:${ns.type.moduleId}`; + const loc = ns.loc; + if (!lowerer.liftedFns.some((fn) => fn.name === name)) { + const context = newFnCtx(false, null, null, DYN); + const previousClass = lowerer.currentClass; + lowerer.currentClass = null; + lowerer.fnStack.push(context); + try { + const body: IrStmt[] = []; + for (const { key, declaration } of exports) { + const node = declaration ?? source; + const diagsBefore = lowerer.diags.length; + let read: IrStmt[]; + try { + const identifier = declaration?.name as ts.Node | undefined; + if (!identifier || !ts.isIdentifier(identifier)) { + lowerer.unsupported( + "SC1090", + node, + `reading the unnamed export '${key}' through a module namespace object`, + ); + } + const value = lowerer.lowerExprExpecting(identifier, DYN); + if (value.type.kind !== "dyn") { + lowerer.unsupported( + "SC1101", + identifier, + `reading the export '${key}' ('${lowerer.fmt(value.type)}') through a module namespace object`, + ); + } + read = [{ kind: "return", value, loc }]; + } catch (err) { + if (!(err instanceof PoisonError)) throw err; + const fence = lowerer.deferToRuntimeFence(diagsBefore, node, { kind: "statement" }); + if (!fence) throw err; + read = [fence]; + } + body.push({ + kind: "if", + cond: { + kind: "strEq", + negated: false, + left: varRef("key", STRING, loc), + right: strLit(key, loc), + type: { kind: "bool" }, + loc, + }, + then: read, + else_: null, + loc, + }); + } + body.push({ kind: "return", value: dynUndefinedExpr(loc), loc }); + lowerer.liftedFns.push({ + name, + params: [{ localId: "key", name: "key", type: STRING }], + returnType: DYN, + locals: [{ id: "key", name: "key", type: STRING, mutable: false }, ...context.locals], + body, + loc, + }); + } finally { + lowerer.fnStack.pop(); + lowerer.currentClass = previousClass; + } + } + const value: IrExpr = { + kind: "libCall", + fn: "module.namespaceValue", + args: [ + strLit(ns.type.moduleId, loc), + { + kind: "arrayLit", + elems: exports.map(({ key }) => strLit(key, loc)), + type: arrayOf(STRING), + loc, + }, + { + kind: "dynFrom", + value: { kind: "closure", fnName: name, captures: [], type: funcOf([STRING], DYN), loc }, + type: DYN, + loc, + }, + ], + type: DYN, + loc, + }; + if (ns.kind === "moduleNsRef") return value; + return { + kind: "seqExpr", + stmts: [{ kind: "exprStmt", expr: ns, loc }], + result: value, + type: DYN, + loc, + }; +} diff --git a/packages/compiler/src/frontend/lowering/nan-coded-reads.ts b/packages/compiler/src/frontend/lowering/nan-coded-reads.ts index 65efe5b2a..24c981bd3 100644 --- a/packages/compiler/src/frontend/lowering/nan-coded-reads.ts +++ b/packages/compiler/src/frontend/lowering/nan-coded-reads.ts @@ -249,6 +249,10 @@ export class NanCodedReads { private readonly returns = new Map(); /** Identifiers by text across the analyzed files. */ private readonly names = new Map(); + /** The identifiers of each name, grouped by the symbol they resolve to + * (referencesOf), once `names` is complete. */ + private readonly references = new Map>(); + private named = false; private readonly symbolCache = new Map(); private readonly pneCache = new Map(); private readonly pneReturnCache = new Map(); @@ -326,6 +330,7 @@ export class NanCodedReads { } }); } + this.named = true; // A proven-present read is a plain number; only the rest may be // undefined. The proof needs every reference, so it runs after the walk. for (const [read, elem] of candidates) @@ -879,8 +884,8 @@ export class NanCodedReads { else if (!decl.initializer) slot.sources.push(null); } } else return null; - for (const id of this.names.get(nameNode.text) ?? []) { - if (id === nameNode || this.symbolOf(id) !== symbol) continue; + for (const id of this.referencesOf(symbol, nameNode.text)) { + if (id === nameNode) continue; const write = this.writeOf(id); if (write !== "read") slot.reassigned = true; if (write === "read") slot.uses.push(id); @@ -895,6 +900,26 @@ export class NanCodedReads { return slot; } + /** The identifiers named `text` that resolve to `symbol`, in walk order. + * Each name's identifiers are resolved once, not once per binding. */ + private referencesOf(symbol: ts.Symbol, text: string): readonly ts.Identifier[] { + const ids = this.names.get(text) ?? []; + if (!this.named) return ids.filter((id) => this.symbolOf(id) === symbol); + let bySymbol = this.references.get(text); + if (bySymbol === undefined) { + bySymbol = new Map(); + for (const id of ids) { + const resolved = this.symbolOf(id); + if (resolved === null) continue; + const list = bySymbol.get(resolved); + if (list === undefined) bySymbol.set(resolved, [id]); + else list.push(id); + } + this.references.set(text, bySymbol); + } + return bySymbol.get(symbol) ?? []; + } + /** How an identifier reference touches its binding. */ private writeOf(id: ts.Identifier): "read" | "arith" | "unknown" | ts.Expression { let n: ts.Node = id; diff --git a/packages/compiler/src/frontend/lowering/narrowing-stability.ts b/packages/compiler/src/frontend/lowering/narrowing-stability.ts index 63e503221..d0c9b04a6 100644 --- a/packages/compiler/src/frontend/lowering/narrowing-stability.ts +++ b/packages/compiler/src/frontend/lowering/narrowing-stability.ts @@ -67,13 +67,12 @@ export class NarrowingStability { // Program-level queries before the program's files are split concern // declarations, never narrowed reads in function bodies. if (this.files === null) return null; - const position = widePosition(node); - if (position === null) return null; const cached = this.widened.get(node); if (cached !== undefined) return cached; + const position = widePosition(node); let result: ts.Type | null = null; try { - result = this.computeWidened(node, position); + if (position !== null) result = this.computeWidened(node, position); } finally { this.widened.set(node, result); } @@ -916,6 +915,15 @@ const INERT_CONSTRUCTORS = new Set([ "BigUint64Array", ]); +/** The receiver whose type widenedType reads for `node`: a property read in + * a slot that accepts its declared type (an argument, a return, a typed + * binding or literal). Whole-program analyses batch these receivers. */ +export function widenedReceiver(node: ts.Node): ts.Expression | null { + return ts.isPropertyAccessExpression(node) && widePosition(node) === "context" + ? node.expression + : null; +} + /** Where a stale narrowed read can keep its declared representation: the * receiver of `?.`, an absence/type/truthiness test, or a slot whose * contextual type may accept the declared type. */ diff --git a/packages/compiler/src/frontend/lowering/object-factory-new.ts b/packages/compiler/src/frontend/lowering/object-factory-new.ts index bcb675692..c672f82e2 100644 --- a/packages/compiler/src/frontend/lowering/object-factory-new.ts +++ b/packages/compiler/src/frontend/lowering/object-factory-new.ts @@ -6,10 +6,14 @@ import { bindingNeverReassigned } from "./binding-analysis.js"; import { implicitDefaultInstance } from "./generic-functions.js"; import { type FnSig } from "./call-signatures.js"; -/** An ordinary constructor's explicit object return replaces its receiver. - * Without observing this or new.target, it can use the function's call ABI. */ -function isObjectFactoryNew(lowerer: Lowerer, expr: ts.NewExpression): boolean { - if (!ts.isIdentifier(expr.expression) || lowerer.peekLocal(expr.expression)) return false; +/** The JavaScript function declaration (or never-reassigned function + * expression binding) `new` calls, when it is an ordinary constructor: not + * a generator or async function. */ +function jsConstructorFunctionOf( + lowerer: Lowerer, + expr: ts.NewExpression, +): ts.FunctionDeclaration | ts.FunctionExpression | null { + if (!ts.isIdentifier(expr.expression) || lowerer.peekLocal(expr.expression)) return null; // This probe also sees ordinary classes during global collection. Leave // their deferred diagnostics for the class declaration's lowering. const wasCollecting = lowerer.collecting; @@ -20,7 +24,7 @@ function isObjectFactoryNew(lowerer: Lowerer, expr: ts.NewExpression): boolean { } finally { lowerer.collecting = wasCollecting; } - if (!symbol) return false; + if (!symbol) return null; const owner = symbol.declarations.find( (node) => (ts.isFunctionDeclaration(node) && node.body !== undefined) || @@ -45,7 +49,15 @@ function isObjectFactoryNew(lowerer: Lowerer, expr: ts.NewExpression): boolean { declaration.asteriskToken || declaration.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) ) - return false; + return null; + return declaration; +} + +/** An ordinary constructor's explicit object return replaces its receiver. + * Without observing this or new.target, it can use the function's call ABI. */ +function isObjectFactoryNew(lowerer: Lowerer, expr: ts.NewExpression): boolean { + const declaration = jsConstructorFunctionOf(lowerer, expr); + if (!declaration?.body) return false; const objectReturn = (expression: ts.Expression): boolean => ts.isObjectLiteralExpression(expression) || ts.isArrayLiteralExpression(expression) || @@ -81,8 +93,13 @@ function isObjectFactoryNew(lowerer: Lowerer, expr: ts.NewExpression): boolean { /** Collection must register the shared result before reaching factory * bodies: those bodies can reference later globals in the same module. */ +/** The storage of a JavaScript module binding initialized with `new` of an + * ordinary function: a factory's result and a constructor function's + * receiver (`const head = new Segment()`) are both checked-dynamic + * values, which functions declared elsewhere in the module read through + * the global. */ export function objectFactoryGlobalType(lowerer: Lowerer, expr: ts.NewExpression): IrType | null { - return isObjectFactoryNew(lowerer, expr) ? DYN : null; + return jsConstructorFunctionOf(lowerer, expr) !== null ? DYN : null; } export function objectFactorySignature(lowerer: Lowerer, expr: ts.NewExpression): FnSig | null { diff --git a/packages/compiler/src/frontend/lowering/overload-choice.ts b/packages/compiler/src/frontend/lowering/overload-choice.ts new file mode 100644 index 000000000..f973ccf2e --- /dev/null +++ b/packages/compiler/src/frontend/lowering/overload-choice.ts @@ -0,0 +1,197 @@ +import * as ts from "../ts7/adapter.js"; +import type { Lowerer } from "./lowerer.js"; +import { typeEquals, type IrType } from "../../ir/ir.js"; +import { isJsSourceFile } from "../program.js"; +import { isThisParameter } from "./call-signatures.js"; +import { hasExplicitJsDocReturn } from "./function-returns.js"; +import { hasJsTypeAnnotation } from "./lower-stmts.js"; + +/** Whether a checked-dynamic argument leaves a JS call's overload + * undecided. + * + * The checker resolves an overloaded call whose argument is `any` (an + * untyped parameter, a value read from one) to the FIRST overload that + * argument satisfies, but the value can match any overload at run time: + * `generateKeyPairSync("ec", options)` answers KeyObjects or encoded + * strings by the options' contents, `digest(encoding)` a Buffer when the + * encoding is undefined. When another overload the call could take at + * run time returns a different type, the resolved overload's return is a + * guess, and the result is checked-dynamic instead. + * + * An overload is reachable when the arguments fit its arity (trailing + * checked-dynamic or `undefined` arguments may be absent) and every + * statically typed argument is assignable to its parameter. Generic + * overloads whose return cannot be mapped without instantiation do not + * count, so the resolved instantiation stands for them. */ +export function dynamicOverloadChoice( + lowerer: Lowerer, + expr: ts.CallExpression | ts.NewExpression, +): boolean { + return cachedAnswer(lowerer, expr, () => computeDynamicOverloadChoice(lowerer, expr)); +} + +/** Whether a JavaScript value's checker type rests on an undecided + * overload (dynamicOverloadChoice): the call itself, an awaited one, a + * call to an unannotated JS function returning such a value, or a read of + * an unannotated JS binding initialized with one (destructured or not). + * Such a value is checked-dynamic wherever the checker's inference would + * otherwise carry the guess into a binding or a return. */ +export function guessedOverloadValue(lowerer: Lowerer, expr: ts.Expression): boolean { + while (ts.isParenthesizedExpression(expr) || ts.isAwaitExpression(expr)) expr = expr.expression; + if (ts.isCallExpression(expr) || ts.isNewExpression(expr)) { + if (!isJsSourceFile(ts.getSourceFileOfNode(expr))) return false; + if (dynamicOverloadChoice(lowerer, expr)) return true; + const decl = lowerer.checker.getResolvedSignature(expr)?.declaration; + return ( + decl !== undefined && + (ts.isFunctionDeclaration(decl) || + ts.isFunctionExpression(decl) || + ts.isArrowFunction(decl)) && + isJsSourceFile(ts.getSourceFileOfNode(decl)) && + returnsGuessedOverloadValue(lowerer, decl) + ); + } + return ts.isIdentifier(expr) && guessedOverloadIdentifier(lowerer, expr); +} + +/** Whether a JS identifier, as a binding's declared name or as a read, + * names a guessedOverloadBinding. */ +export function guessedOverloadIdentifier(lowerer: Lowerer, node: ts.Identifier): boolean { + return cachedAnswer(lowerer, node, () => { + const parent = node.parent; + if ( + parent !== undefined && + (ts.isVariableDeclaration(parent) || ts.isBindingElement(parent)) && + parent.name === node + ) + return guessedOverloadBinding(lowerer, parent); + const decl = lowerer.checker.getSymbolAtLocation(node)?.valueDeclaration; + return decl !== undefined && guessedOverloadBinding(lowerer, decl); + }); +} + +/** Whether `decl` is an unannotated JS variable or destructured binding + * whose declaration's initializer is a guessedOverloadValue. */ +function guessedOverloadBinding(lowerer: Lowerer, decl: ts.Node): boolean { + let root: ts.Node | undefined = decl; + while (root && ts.isBindingElement(root) && root.parent && ts.isBindingPattern(root.parent)) { + root = root.parent.parent; + } + if (!root) return false; + if (!ts.isVariableDeclaration(root) || !root.initializer || root.type) return false; + if (!isJsSourceFile(ts.getSourceFileOfNode(root)) || hasJsTypeAnnotation(root)) return false; + const init = root.initializer; + return cachedAnswer(lowerer, root, () => guessedOverloadValue(lowerer, init)); +} + +/** Whether a return of the unannotated JS function `decl` itself is a + * guessedOverloadValue. */ +export function returnsGuessedOverloadValue( + lowerer: Lowerer, + decl: ts.SignatureDeclaration, +): boolean { + if (decl.type || decl.asteriskToken || hasExplicitJsDocReturn(decl)) return false; + return cachedAnswer(lowerer, decl, () => { + if (ts.isArrowFunction(decl) && decl.body && !ts.isBlock(decl.body)) { + return guessedOverloadValue(lowerer, decl.body); + } + let found = false; + const scan = (node: ts.Node): void => { + if (found || (node !== decl && ts.isFunctionLike(node))) return; + if (ts.isReturnStatement(node) && node.expression) { + found = guessedOverloadValue(lowerer, node.expression); + } + ts.forEachChild(node, scan); + }; + scan(decl); + return found; + }); +} + +/** One answer per node; a node asked again while its answer is being + * computed (recursive functions, self-referencing bindings) answers + * false. */ +function cachedAnswer(lowerer: Lowerer, node: ts.Node, compute: () => boolean): boolean { + const cached = lowerer.dynamicOverloadAnswers.get(node); + if (cached !== undefined) return cached; + lowerer.dynamicOverloadAnswers.set(node, false); + const answer = compute(); + lowerer.dynamicOverloadAnswers.set(node, answer); + return answer; +} + +function computeDynamicOverloadChoice( + lowerer: Lowerer, + expr: ts.CallExpression | ts.NewExpression, +): boolean { + // TypeScript sources keep the resolved overload: their reads validate + // the value against it where it is used. + if (!isJsSourceFile(ts.getSourceFileOfNode(expr))) return false; + const args = expr.arguments?.nodes ?? []; + if (args.length === 0 || args.some(ts.isSpreadElement)) return false; + const dynamic = args.map((arg) => checkedDynamicArgument(lowerer, arg)); + if (!dynamic.includes(true)) return false; + const checker = lowerer.checker; + const calleeType = checker.getTypeAtLocation(expr.expression); + const signatures = ts.isNewExpression(expr) + ? checker.getConstructSignatures(calleeType) + : checker.getCallSignatures(calleeType); + if (signatures.length < 2) return false; + const resolved = checker.getResolvedSignature(expr); + if (!resolved) return false; + const resolvedReturn = lowerer.mapTypeOf(checker.getReturnTypeOfSignature(resolved)); + if (resolvedReturn === null) return false; + // Arguments that may be absent at run time: the trailing run of + // checked-dynamic and `undefined` arguments. + let present = args.length; + while (present > 0 && (dynamic[present - 1] || isUndefinedArgument(args[present - 1]!))) { + present--; + } + for (const signature of signatures) { + const decl = signature.declaration; + if (!decl || decl === resolved.declaration || !ts.isFunctionLike(decl)) continue; + const generic = signature.getTypeParameters().length > 0; + const params = decl.parameters.nodes.filter((param) => !isThisParameter(param)); + const required = params.filter( + (param) => !param.questionToken && !param.initializer && !param.dotDotDotToken, + ).length; + const maximum = params.some((param) => param.dotDotDotToken) ? Infinity : params.length; + if (required > args.length || present > maximum) continue; + const symbols = signature.getParameters(); + let fits = true; + for (let i = 0; i < args.length && i < params.length && fits; i++) { + if (dynamic[i] || generic || params[i]!.dotDotDotToken) continue; + const symbol = symbols[i]; + if (!symbol) continue; + fits = checker.isTypeAssignableTo( + checker.getTypeAtLocation(args[i]!), + checker.getTypeOfSymbol(symbol), + ); + } + if (!fits) continue; + const candidate = lowerer.mapTypeOf(checker.getReturnTypeOfSignature(signature)); + if (candidate !== null && !holdsResult(resolvedReturn, candidate)) return true; + } + return false; +} + +/** Whether a value of the resolved overload's mapped return also holds + * every value of `candidate`: the same type, or a checked-dynamic one (a + * promise of one fulfills with anything). */ +function holdsResult(resolved: IrType, candidate: IrType): boolean { + if (resolved.kind === "dyn" || typeEquals(resolved, candidate)) return true; + return ( + resolved.kind === "promise" && + candidate.kind === "promise" && + holdsResult(resolved.inner, candidate.inner) + ); +} + +function checkedDynamicArgument(lowerer: Lowerer, arg: ts.Expression): boolean { + if (lowerer.checker.getTypeAtLocation(arg).flags & ts.TypeFlags.Any) return true; + return (lowerer.typeOf(arg).flags & ts.TypeFlags.Any) !== 0; +} + +function isUndefinedArgument(arg: ts.Expression): boolean { + return (ts.isIdentifier(arg) && arg.text === "undefined") || ts.isVoidExpression(arg); +} diff --git a/packages/compiler/src/frontend/lowering/private-brands.ts b/packages/compiler/src/frontend/lowering/private-brands.ts index c7881bce1..92bee4fdd 100644 --- a/packages/compiler/src/frontend/lowering/private-brands.ts +++ b/packages/compiler/src/frontend/lowering/private-brands.ts @@ -17,8 +17,9 @@ import type { Lowerer } from "./lowerer.js"; import type { ClassInfo } from "./lower-classes.js"; /** How a brand helper uses its receiver: `in` answers `#x in v`, `read` and - * `write` return the receiver as the declaring class for a field access. */ -type BrandUse = "in" | "read" | "write"; + * `write` return the receiver as the declaring class for a field access, and + * `call` for a private method call. */ +type BrandUse = "in" | "read" | "write" | "call"; const PREFIX = "%private."; @@ -214,6 +215,35 @@ export function checkedPrivateValue( return { kind: "call", callee: helper, args: [receiver], type: resultType, loc }; } +/** `value.#m(...)` on a checked-dynamic `value` in a declaration-level class + * `info` that declares the private method `#m`: the receiver as an instance + * of `info` when the value is one, else Node's TypeError ("Receiver must be + * an instance of class C"). */ +export function checkedPrivateMethodReceiver( + lowerer: Lowerer, + info: ClassInfo, + name: string, + receiver: IrExpr, + loc: SrcLoc, +): IrExpr { + const className = info.def.name; + const resultType: IrType = { kind: "object", className }; + const helper = `${PREFIX}call:${className}:${name}`; + if (!lowerer.valueHelpers.has(helper)) { + lowerer.liftedFns.push({ + name: helper, + params: [{ localId: "value.0", name: "value", type: DYN }], + returnType: resultType, + locals: [{ id: "value.0", name: "value", type: DYN, mutable: false }], + // finalizePrivateBrands fills the body. + body: [], + loc: helperLoc(loc), + }); + lowerer.valueHelpers.set(helper, helper); + } + return { kind: "call", callee: helper, args: [receiver], type: resultType, loc }; +} + /** A plain assignment, a destructuring target, or a for-in/of target writes * the field. Compound assignments and updates read it first, and Node * reports the failed read. */ @@ -358,6 +388,22 @@ export function finalizePrivateBrands(lowerer: Lowerer, functions: IrFunction[]) }, { kind: "return", value: { kind: "boolLit", value: false, type: BOOL, loc }, loc }, ); + } else if (use === "call") { + // V8 names the class by its own identifier (not a name inferred from + // a binding), "anonymous" without one, also for a nullish receiver. + const owner = lowerer.classes.get(className); + const identifier = owner?.decl?.name; + const shown = identifier !== undefined ? identifier.text : "anonymous"; + for (const unit of ["undefined", "null"] as const) { + body.push({ + kind: "if", + cond: { kind: "dynTest", test: unit, value, type: BOOL, loc }, + then: [typeError(strLit(`Cannot read properties of ${unit} (reading '${shown}')`, loc))], + else_: null, + loc, + }); + } + body.push(typeError(strLit(`Receiver must be an instance of class ${shown}`, loc))); } else { const verb = use === "read" ? "reading" : "setting"; const action = use === "read" ? "Cannot read properties" : "Cannot set properties"; diff --git a/packages/compiler/src/frontend/lowering/record-inherited.ts b/packages/compiler/src/frontend/lowering/record-inherited.ts new file mode 100644 index 000000000..65d070f1d --- /dev/null +++ b/packages/compiler/src/frontend/lowering/record-inherited.ts @@ -0,0 +1,911 @@ +/* Object.prototype's methods on typed records. + * + * A record is an ordinary object, so `r.toString()`, `r.hasOwnProperty(k)` + * and the other Object.prototype methods answer from its prototype unless + * the record holds an own entry under that name. Method calls with a + * constant name call an own entry when the record's type lets one be + * callable and otherwise run the built-in natively with the record as its + * receiver. Keyed reads of records whose values are functions or checked + * values answer the built-in as a native function value on the miss path, + * so truthiness, typeof and calls behave as in Node; hits are untouched. + * Records of other value types keep reading undefined for these names + * (a documented limitation). No dynamic-tier unit is involved: every + * built-in is a small lifted function over the record. */ +import * as ts from "../ts7/adapter.js"; +import { dynUndefinedExpr, nodeThrowExpr, varRef } from "../../ir/build.js"; +import { + BOOL, + DYN, + STRING, + UNDEFINED_T, + isUnitType, + typeEquals, + typeKey, + type IrExpr, + type IrLocal, + type IrStmt, + type IrType, + type SrcLoc, +} from "../../ir/ir.js"; +import type { Lowerer } from "./lowerer.js"; +import { locOf } from "../program.js"; +import { keyedHelperName } from "./stable-names.js"; +import { isSafeToRepeat } from "./expressions/evaluation-safety.js"; +import { nullProtoRecordShape, recordNullProtoTest } from "./null-prototype.js"; +import { recordHasOwnHelper } from "./lower-calls.js"; +import { OTHER_ARM_FAILURE_MESSAGE } from "./coercions/unions.js"; + +/** Object.prototype's methods a record answers from its prototype. */ +export const RECORD_INHERITED_METHODS: ReadonlySet = new Set([ + "toString", + "toLocaleString", + "valueOf", + "hasOwnProperty", + "isPrototypeOf", + "propertyIsEnumerable", +]); + +type FuncType = IrType & { kind: "func" }; + +/** Declares a hidden local in the function being built: the lowerer's + * current function, or a lifted helper's own local list. */ +type DeclareLocal = (name: string, type: IrType) => string; + +function liftedLocals(): { declare: DeclareLocal; locals: IrLocal[] } { + const locals: IrLocal[] = []; + return { + locals, + declare: (name, type) => { + const id = `${name}.${locals.length}`; + locals.push({ id, name, type, mutable: false }); + return id; + }, + }; +} + +/** The form a read of `type` gives a built-in method value: the union's one + * function arm (beside an undefined arm), a checked value, or null when the + * type cannot hold a function. */ +function methodValueForm(lowerer: Lowerer, type: IrType): FuncType | "dyn" | null { + if (type.kind === "dyn") return "dyn"; + if (type.kind !== "union") return null; + const arms = lowerer.unions.get(type.unionId)?.arms ?? []; + if (!arms.some((arm) => arm.kind === "undefinedT")) return null; + const funcs = arms.filter((arm): arm is FuncType => arm.kind === "func"); + if (funcs.length !== 1 || funcs[0]!.restAbi !== undefined) return null; + return funcs[0]!; +} + +/** A value of `type` may be a function that a call can enter. */ +function callableType(lowerer: Lowerer, type: IrType): boolean { + if (type.kind === "func" || type.kind === "dyn") return true; + if (type.kind !== "union") return false; + return (lowerer.unions.get(type.unionId)?.arms ?? []).some( + (arm) => arm.kind === "func" || arm.kind === "dyn", + ); +} + +/** A keyed record read (`r[k]`, `r["toString"]`) whose miss answers the + * inherited Object.prototype method for those names. Hits keep the + * ordinary read; only the miss path tests the key. `bound`: the read is + * the callee of a call (`r[k](…)`, `r[k]?.(…)`), whose `this` is the + * record; any other read yields the method as a plain value, whose calls + * see an undefined `this` as in Node. */ +export function withInheritedMethods( + lowerer: Lowerer, + read: IrExpr & { kind: "recordKeyGet" }, + litKey: string | null, + bound: boolean, +): IrExpr { + if (litKey !== null && !RECORD_INHERITED_METHODS.has(litKey)) return read; + if (read.missNaN === true) return read; + const form = methodValueForm(lowerer, read.type); + const shape = lowerer.shapes.get(read.shapeId); + if (form === null || !shape || shape.tuple) return read; + const loc = read.loc; + const stmts: IrStmt[] = []; + const repeatable = (value: IrExpr): boolean => isSafeToRepeat(value) || value.kind === "strLit"; + const stable = (value: IrExpr, name: string, keep: boolean): IrExpr => { + if (keep) return value; + const local = lowerer.declareHiddenLocal(name, value.type); + stmts.push({ kind: "varDecl", localId: local.id, init: value, loc }); + return varRef(local.id, value.type, loc); + }; + // The receiver is evaluated before the key: when computing the key runs + // code (it may reassign the receiver's variable), the receiver is held + // first, even when reading it again would be safe on its own. + const keyRepeatable = repeatable(read.key); + const obj = stable(read.obj, "%inheritedRecv", repeatable(read.obj) && keyRepeatable); + const key = stable(read.key, "%inheritedKey", keyRepeatable); + const helper = inheritedReadHelper(lowerer, read.shapeId, read.type, form, bound, loc); + const fallback: IrExpr = { kind: "call", callee: helper, args: [obj, key], type: read.type, loc }; + if ( + form !== "dyn" && + read.type.kind === "union" && + !(lowerer.unions.get(read.type.unionId)?.arms ?? []).some((arm) => arm.kind === "nullT") + ) { + // `r[k] ?? inherited(r, k)`: the hit is the read's own value, untouched. + const coalesced: IrExpr = { + kind: "nullish", + left: { ...read, obj, key }, + right: fallback, + type: read.type, + loc, + }; + return stmts.length === 0 + ? coalesced + : { kind: "seqExpr", stmts, result: coalesced, type: read.type, loc }; + } + const value = lowerer.declareHiddenLocal("%inheritedRead", read.type); + stmts.push({ kind: "varDecl", localId: value.id, init: { ...read, obj, key }, loc }); + const found = varRef(value.id, read.type, loc); + let missing: IrExpr; + if (form === "dyn") { + missing = { kind: "dynTest", test: "undefined", value: found, type: BOOL, loc } as IrExpr; + } else { + const unionId = (read.type as IrType & { kind: "union" }).unionId; + const tag = (lowerer.unions.get(unionId)?.arms ?? []).findIndex( + (arm) => arm.kind === "undefinedT", + ); + missing = { kind: "unionIsTag", unionId, tag, negated: false, value: found, type: BOOL, loc }; + } + return { + kind: "seqExpr", + stmts, + result: { + kind: "ternary", + cond: missing, + then: fallback, + else_: found, + type: read.type, + loc, + }, + type: read.type, + loc, + }; +} + +/** `%rec.inherited.(o, k)`: the inherited method `k` names as a value + * of `readType` (bound to `o`), or undefined. */ +function inheritedReadHelper( + lowerer: Lowerer, + shapeId: string, + readType: IrType, + form: FuncType | "dyn", + bound: boolean, + loc: SrcLoc, +): string { + const key = `rec.inherited:${bound ? "bound" : "value"}:${shapeId}:${typeKey(readType)}`; + const existing = lowerer.arrHofHelpers.get(key); + if (existing) return existing; + const name = keyedHelperName(lowerer, "%rec.inherited", key); + lowerer.arrHofHelpers.set(key, name); + const recT: IrType = { kind: "record", shapeId }; + const o = varRef("o.0", recT, loc); + const k = varRef("k.0", STRING, loc); + const fnType: FuncType = + form === "dyn" ? ({ kind: "func", params: [DYN], ret: DYN } as FuncType) : form; + const undefinedValue: IrExpr = + form === "dyn" + ? dynUndefinedExpr(loc) + : lowerer.coerceToExpected( + { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc }, + readType, + ); + const named = (method: string): IrExpr => ({ + kind: "strEq", + negated: false, + left: k, + right: { kind: "strLit", value: method, type: STRING, loc }, + type: BOOL, + loc, + }); + const methods = [...RECORD_INHERITED_METHODS]; + let anyName: IrExpr = named(methods[0]!); + for (const method of methods.slice(1)) + anyName = { kind: "logical", op: "||", left: anyName, right: named(method), type: BOOL, loc }; + const body: IrStmt[] = [ + // Most misses name no inherited method: one comparison chain, nothing else. + { + kind: "if", + cond: { kind: "unary", op: "!", operand: anyName, type: BOOL, loc }, + then: [{ kind: "return", value: { ...undefinedValue }, loc }], + else_: null, + loc, + }, + // An own key holding undefined shadows the prototype like any own key. + { + kind: "if", + cond: { + kind: "call", + callee: recordHasOwnHelper(lowerer, shapeId, loc), + args: [o, k], + type: BOOL, + loc, + }, + then: [{ kind: "return", value: { ...undefinedValue }, loc }], + else_: null, + loc, + }, + ]; + if (nullProtoRecordShape(lowerer, recT)) + body.push({ + kind: "if", + cond: recordNullProtoTest(o, shapeId, loc), + then: [{ kind: "return", value: { ...undefinedValue }, loc }], + else_: null, + loc, + }); + for (const method of methods) { + const value: IrExpr = bound + ? { + kind: "call", + callee: boundMethodFactory(lowerer, shapeId, method, fnType, loc), + args: [o], + type: fnType, + loc, + } + : { + kind: "closure", + fnName: unboundMethodImpl(lowerer, method, fnType, loc), + captures: [], + type: fnType, + loc, + }; + body.push({ + kind: "if", + cond: named(method), + then: [ + { + kind: "return", + value: lowerer.coerceToExpected(value, form === "dyn" ? DYN : readType), + loc, + }, + ], + else_: null, + loc, + }); + } + body.push({ kind: "return", value: { ...undefinedValue }, loc }); + lowerer.liftedFns.push({ + name, + params: [ + { localId: "o.0", name: "o", type: recT }, + { localId: "k.0", name: "k", type: STRING }, + ], + returnType: readType, + locals: [ + { id: "o.0", name: "o", type: recT, mutable: false }, + { id: "k.0", name: "k", type: STRING, mutable: false }, + ], + body, + loc, + }); + return name; +} + +/** `%rec.bind.(o)`: the built-in `method` bound to `o` as a closure. */ +function boundMethodFactory( + lowerer: Lowerer, + shapeId: string, + method: string, + fnType: FuncType, + loc: SrcLoc, +): string { + const key = `rec.bind:${shapeId}:${method}:${typeKey(fnType)}`; + const existing = lowerer.arrHofHelpers.get(key); + if (existing) return existing; + const name = keyedHelperName(lowerer, "%rec.bind", key); + lowerer.arrHofHelpers.set(key, name); + const recT: IrType = { kind: "record", shapeId }; + lowerer.liftedFns.push({ + name, + params: [{ localId: "o.0", name: "o", type: recT }], + returnType: fnType, + locals: [{ id: "o.0", name: "o", type: recT, mutable: false, boxed: true }], + body: [ + { + kind: "return", + value: { + kind: "closure", + fnName: inheritedMethodImpl(lowerer, shapeId, method, fnType, loc), + captures: ["o.0"], + type: fnType, + loc, + }, + loc, + }, + ], + loc, + }); + return name; +} + +/** The lifted body of one built-in bound to its record, with the read's + * function signature: the built-in's answer converted to the signature's + * return type, or the checked-conversion TypeError where it cannot be. */ +function inheritedMethodImpl( + lowerer: Lowerer, + shapeId: string, + method: string, + fnType: FuncType, + loc: SrcLoc, +): string { + const key = `rec.inheritedImpl:${shapeId}:${method}:${typeKey(fnType)}`; + const existing = lowerer.arrHofHelpers.get(key); + if (existing) return existing; + const name = keyedHelperName(lowerer, `%rec.${method}`, key); + lowerer.arrHofHelpers.set(key, name); + const recT: IrType = { kind: "record", shapeId }; + const o = varRef("o.0", recT, loc); + const params = fnType.params.map((type, i) => ({ localId: `p${i}.0`, name: `p${i}`, type })); + const args = params.map((p) => varRef(p.localId, p.type, loc)); + const body: IrStmt[] = []; + const hidden = liftedLocals(); + const answer = builtinAnswer(lowerer, hidden.declare, o, shapeId, method, args, fnType.ret, loc); + body.push(...returnAs(lowerer, answer, fnType.ret, loc)); + lowerer.liftedFns.push({ + name, + params, + returnType: fnType.ret, + captures: [{ localId: "o.0", name: "o", type: recT }], + locals: [ + { id: "o.0", name: "o", type: recT, mutable: false, boxed: true }, + ...params.map((p) => ({ id: p.localId, name: p.name, type: p.type, mutable: false })), + ...hidden.locals, + ], + body, + loc, + }); + return name; +} + +/** One built-in as a plain function value (no receiver): its answer for an + * undefined `this`, converted like inheritedMethodImpl's. */ +function unboundMethodImpl( + lowerer: Lowerer, + method: string, + fnType: FuncType, + loc: SrcLoc, +): string { + const key = `rec.unboundImpl:${method}:${typeKey(fnType)}`; + const existing = lowerer.arrHofHelpers.get(key); + if (existing) return existing; + const name = keyedHelperName(lowerer, `%obj.${method}`, key); + lowerer.arrHofHelpers.set(key, name); + const params = fnType.params.map((type, i) => ({ localId: `p${i}.0`, name: `p${i}`, type })); + const arg: IrExpr | undefined = params[0] && varRef(params[0].localId, params[0].type, loc); + const coerce = "Cannot convert undefined or null to object"; + let answer: IrExpr; + switch (method) { + case "toString": + answer = { kind: "strLit", value: "[object Undefined]", type: STRING, loc }; + break; + case "toLocaleString": + answer = nodeThrowExpr( + 1, + "", + "Object.prototype.toLocaleString called on null or undefined", + STRING, + loc, + ); + break; + case "isPrototypeOf": { + // A primitive argument answers false before `this` converts. + const no: IrExpr = { kind: "boolLit", value: false, type: BOOL, loc }; + const kind = arg?.type.kind; + if ( + arg === undefined || + isUnitType(arg.type) || + kind === "string" || + kind === "f64" || + kind === "bool" || + kind === "bigint" + ) + answer = no; + else if (kind === "dyn") + answer = { + kind: "ternary", + cond: { + kind: "logical", + op: "||", + left: { kind: "dynTest", test: "object", value: arg, type: BOOL, loc }, + right: { kind: "dynTest", test: "function", value: arg, type: BOOL, loc }, + type: BOOL, + loc, + } as IrExpr, + then: nodeThrowExpr(1, "", coerce, BOOL, loc), + else_: no, + type: BOOL, + loc, + }; + else answer = nodeThrowExpr(1, "", coerce, BOOL, loc); + break; + } + default: + answer = nodeThrowExpr(1, "", coerce, method === "valueOf" ? DYN : BOOL, loc); + } + const body: IrStmt[] = + answer.kind === "libCall" && answer.fn === "error.nodeThrow" + ? [ + { + kind: "return", + value: nodeThrowExpr( + 1, + "", + (answer.args[2] as IrExpr & { kind: "strLit" }).value, + fnType.ret, + loc, + ), + loc, + }, + ] + : returnAs(lowerer, answer, fnType.ret, loc); + lowerer.liftedFns.push({ + name, + params, + returnType: fnType.ret, + locals: params.map((p) => ({ id: p.localId, name: p.name, type: p.type, mutable: false })), + body, + loc, + }); + return name; +} + +/** The built-in's answer with `o` as its receiver. `want` is the type the + * caller needs (valueOf answers the record itself). */ +function builtinAnswer( + lowerer: Lowerer, + declare: DeclareLocal, + o: IrExpr, + shapeId: string, + method: string, + args: readonly IrExpr[], + want: IrType, + loc: SrcLoc, +): IrExpr { + switch (method) { + case "toString": + return { kind: "strLit", value: "[object Object]", type: STRING, loc }; + case "toLocaleString": + // Object.prototype.toLocaleString calls this.toString(). + return callOwnOrBuiltin( + lowerer, + declare, + o, + shapeId, + "toString", + [], + STRING, + "toString", + loc, + ); + case "valueOf": + return want.kind === "dyn" ? lowerer.coerceToExpected(o, DYN) : o; + case "isPrototypeOf": + // No object is created with a typed record as its prototype. + return { kind: "boolLit", value: false, type: BOOL, loc }; + default: { + // hasOwnProperty and propertyIsEnumerable: ToPropertyKey keeps a + // symbol a symbol, and a record's own keys are strings, so a symbol + // key is no entry of it. + const no: IrExpr = { kind: "boolLit", value: false, type: BOOL, loc }; + const arg = args[0]; + if (arg?.type.kind === "symbol") return sequence(arg, no, loc); + const has = (key: IrExpr): IrExpr => ({ + kind: "call", + callee: recordHasOwnHelper(lowerer, shapeId, loc), + args: [o, key], + type: BOOL, + loc, + }); + if (arg === undefined || !mayBeSymbol(arg.type)) return has(propertyKeyOf(lowerer, arg, loc)); + const id = declare("%protoKey", DYN); + const held = varRef(id, DYN, loc); + return { + kind: "seqExpr", + stmts: [{ kind: "varDecl", localId: id, init: lowerer.coerceToExpected(arg, DYN), loc }], + result: { + kind: "ternary", + cond: { kind: "dynTest", test: "symbol", value: held, type: BOOL, loc } as IrExpr, + then: no, + else_: has(propertyKeyOf(lowerer, held, loc)), + type: BOOL, + loc, + }, + type: BOOL, + loc, + }; + } + } +} + +/** Whether a value of `type` may be a symbol at run time. */ +function mayBeSymbol(type: IrType): boolean { + return !( + isUnitType(type) || + type.kind === "string" || + type.kind === "f64" || + type.kind === "bool" + ); +} + +/** ToPropertyKey of a built-in's key argument (absent: "undefined"), for + * keys that are not symbols. */ +function propertyKeyOf(lowerer: Lowerer, arg: IrExpr | undefined, loc: SrcLoc): IrExpr { + if (arg === undefined || isUnitType(arg.type)) + return { + kind: "strLit", + value: arg?.type.kind === "nullT" ? "null" : "undefined", + type: STRING, + loc, + }; + if (arg.type.kind === "string") return arg; + if (arg.type.kind === "f64" || arg.type.kind === "bool") + return { kind: "toString", operand: arg, type: STRING, loc }; + return { + kind: "libCall", + fn: "dyn.toStringCoerce", + args: [lowerer.coerceToExpected(arg, DYN)], + type: STRING, + loc, + }; +} + +/** `value` returned as `ret`: as is, wrapped into a union arm or a checked + * value, discarded for void, or the checked-conversion TypeError. */ +function returnAs(lowerer: Lowerer, value: IrExpr, ret: IrType, loc: SrcLoc): IrStmt[] { + const converted = convertTo(lowerer, value, ret); + if (converted !== null) return [{ kind: "return", value: converted, loc }]; + if (ret.kind === "void") + return [ + { kind: "exprStmt", expr: value, loc }, + { kind: "return", value: null, loc }, + ]; + return [ + { kind: "exprStmt", expr: value, loc }, + { kind: "return", value: nodeThrowExpr(1, "", OTHER_ARM_FAILURE_MESSAGE, ret, loc), loc }, + ]; +} + +function convertTo(lowerer: Lowerer, value: IrExpr, want: IrType): IrExpr | null { + if (typeEquals(value.type, want)) return value; + if (want.kind === "dyn" && lowerer.dynConvertible(value.type)) + return lowerer.coerceToExpected(value, DYN); + if (want.kind === "union" && lowerer.armTag(want.unionId, value.type) >= 0) + return lowerer.coerceToExpected(value, want); + return null; +} + +/** `o.method(...args)` on a record: an own callable entry when the record + * holds one, else the built-in. `calleeText` names the callee for Node's + * "is not a function". */ +function callOwnOrBuiltin( + lowerer: Lowerer, + declare: DeclareLocal, + o: IrExpr, + shapeId: string, + method: string, + args: readonly IrExpr[], + want: IrType, + calleeText: string, + loc: SrcLoc, + resolved?: { has: IrExpr; own: IrExpr }, +): IrExpr { + const shape = lowerer.shapes.get(shapeId)!; + const field = shape.fields.find((f) => f.name === method); + const ownType = field ? field.type : shape.indexValue; + let builtin = builtinAnswer(lowerer, declare, o, shapeId, method, args, want, loc); + const recT: IrType = { kind: "record", shapeId }; + if (nullProtoRecordShape(lowerer, recT)) + builtin = { + kind: "ternary", + cond: recordNullProtoTest(o, shapeId, loc), + then: nodeThrowExpr(1, "", `${calleeText} is not a function`, builtin.type, loc), + else_: builtin, + type: builtin.type, + loc, + }; + const builtinAsWant = convertTo(lowerer, builtin, want) ?? builtin; + if (ownType === undefined || !callableType(lowerer, ownType)) return builtinAsWant; + const own = resolved?.own ?? ownEntryRead(lowerer, o, shapeId, method, loc); + const called = callOwn(lowerer, declare, own, args, builtinAsWant.type, calleeText, loc); + return { + kind: "ternary", + cond: resolved?.has ?? ownEntryTest(lowerer, o, shapeId, method, loc), + then: called, + else_: builtinAsWant, + type: builtinAsWant.type, + loc, + }; +} + +/** Whether the record holds an own entry named `method`. */ +function ownEntryTest( + lowerer: Lowerer, + o: IrExpr, + shapeId: string, + method: string, + loc: SrcLoc, +): IrExpr { + return { + kind: "call", + callee: recordHasOwnHelper(lowerer, shapeId, loc), + args: [o, { kind: "strLit", value: method, type: STRING, loc }], + type: BOOL, + loc, + }; +} + +/** The record's own entry named `method`: its field, or its overflow entry + * (undefined when absent). */ +function ownEntryRead( + lowerer: Lowerer, + o: IrExpr, + shapeId: string, + method: string, + loc: SrcLoc, +): IrExpr { + const shape = lowerer.shapes.get(shapeId)!; + const field = shape.fields.find((f) => f.name === method); + return field + ? { kind: "recordGet", obj: o, shapeId, field: method, type: field.type, loc } + : { + kind: "recordKeyGet", + obj: o, + shapeId, + key: { kind: "strLit", value: method, type: STRING, loc }, + overflowOnly: true, + type: lowerer.withUndefinedArmOf(shape.indexValue!) ?? shape.indexValue!, + loc, + }; +} + +/** Call the own entry `own` (present, of any stored type) with `args`. A + * non-function throws Node's "is not a function"; a function whose + * parameters or result cannot carry the call throws the checked-conversion + * TypeError (the program's types claimed otherwise). */ +function callOwn( + lowerer: Lowerer, + declare: DeclareLocal, + own: IrExpr, + args: readonly IrExpr[], + want: IrType, + calleeText: string, + loc: SrcLoc, +): IrExpr { + const notFunction = nodeThrowExpr(1, "", `${calleeText} is not a function`, want, loc); + const lie = nodeThrowExpr(1, "", OTHER_ARM_FAILURE_MESSAGE, want, loc); + const localId = declare("%ownMethod", own.type); + const value = varRef(localId, own.type, loc); + const enter = (fn: IrExpr): IrExpr => { + if (fn.type.kind === "dyn") { + const result: IrExpr = { + kind: "dynCall", + callee: fn, + calleeName: calleeText, + args: args.map((a) => lowerer.coerceToExpected(a, DYN)), + type: DYN, + loc, + } as IrExpr; + return convertTo(lowerer, result, want) ?? sequence(result, lie, loc); + } + const sig = fn.type as FuncType; + if (sig.restAbi !== undefined) return lie; + const passed: IrExpr[] = []; + for (let i = 0; i < sig.params.length; i++) { + const param = sig.params[i]!; + const arg = args[i]; + const given = + arg === undefined + ? convertTo( + lowerer, + { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc }, + param, + ) + : convertTo(lowerer, arg, param); + if (given === null) return lie; + passed.push(given); + } + const result: IrExpr = { kind: "callValue", callee: fn, args: passed, type: sig.ret, loc }; + if (sig.ret.kind === "void") return sequence(result, lie, loc); + return convertTo(lowerer, result, want) ?? sequence(result, lie, loc); + }; + let call: IrExpr; + if (own.type.kind === "func" || own.type.kind === "dyn") call = enter(value); + else if (own.type.kind === "union") { + const unionId = own.type.unionId; + const arms = lowerer.unions.get(unionId)?.arms ?? []; + call = notFunction; + for (let tag = arms.length - 1; tag >= 0; tag--) { + const arm = arms[tag]!; + if (arm.kind !== "func" && arm.kind !== "dyn") continue; + call = { + kind: "ternary", + cond: { kind: "unionIsTag", unionId, tag, negated: false, value, type: BOOL, loc }, + then: enter({ kind: "unionNarrow", unionId, tag, value, type: arm, loc }), + else_: call, + type: want, + loc, + }; + } + } else call = notFunction; + return { + kind: "seqExpr", + stmts: [{ kind: "varDecl", localId, init: own, loc }], + result: call, + type: want, + loc, + }; +} + +function sequence(first: IrExpr, result: IrExpr, loc: SrcLoc): IrExpr { + return { + kind: "seqExpr", + stmts: [{ kind: "exprStmt", expr: first, loc }], + result, + type: result.type, + loc, + }; +} + +/** `r.toString()`, `r.hasOwnProperty(k)` and the other Object.prototype + * methods called by name on a typed record (the checker resolved the + * member to Object.prototype's). Null for other calls. */ +export function lowerRecordInheritedCall( + lowerer: Lowerer, + call: ts.CallExpression, + access: ts.PropertyAccessExpression, +): IrExpr | null { + const method = access.name.text; + if ( + call.questionDotToken || + access.questionDotToken || + !RECORD_INHERITED_METHODS.has(method) || + call.arguments.nodes.some(ts.isSpreadElement) || + !lowerer.isStdlibMember(access) + ) + return null; + const recvT = lowerer.mapTypeOf(lowerer.typeOf(access.expression)); + if (recvT?.kind !== "record") return null; + const shape = lowerer.shapes.get(recvT.shapeId); + if (!shape || shape.tuple) return null; + const want = + method === "valueOf" + ? (lowerer.mapTypeOf(lowerer.typeOf(call)) ?? recvT) + : method === "toString" || method === "toLocaleString" + ? STRING + : BOOL; + // Without a possible own function, toString and the own-key tests keep + // their existing lowerings (with the special record views they know). + if ( + (method === "hasOwnProperty" || method === "propertyIsEnumerable" || method === "toString") && + !shape.fields.some((f) => f.name === method && callableType(lowerer, f.type)) && + !(shape.indexValue && callableType(lowerer, shape.indexValue)) + ) + return null; + const loc = locOf(call); + const stmts: IrStmt[] = []; + const receiver = lowerer.lowerExpr(access.expression); + if (receiver.type.kind !== "record") return null; + const recvLocal = lowerer.declareHiddenLocal("%protoRecv", receiver.type); + stmts.push({ kind: "varDecl", localId: recvLocal.id, init: receiver, loc }); + const o = varRef(recvLocal.id, receiver.type, loc); + // `o.m(...)` reads the member before its arguments run: an argument that + // adds, deletes or replaces the own method does not change the callee. + const field = shape.fields.find((f) => f.name === method); + const ownType = field ? field.type : shape.indexValue; + let resolved: { has: IrExpr; own: IrExpr } | undefined; + if (ownType !== undefined && callableType(lowerer, ownType)) { + const shapeId = receiver.type.shapeId; + const hasLocal = lowerer.declareHiddenLocal("%protoHas", BOOL); + stmts.push({ + kind: "varDecl", + localId: hasLocal.id, + init: ownEntryTest(lowerer, o, shapeId, method, loc), + loc, + }); + const read = ownEntryRead(lowerer, o, shapeId, method, loc); + const ownLocal = lowerer.declareHiddenLocal("%protoOwn", read.type); + stmts.push({ kind: "varDecl", localId: ownLocal.id, init: read, loc }); + resolved = { has: varRef(hasLocal.id, BOOL, loc), own: varRef(ownLocal.id, read.type, loc) }; + } + const args: IrExpr[] = []; + for (const node of call.arguments.nodes) { + const arg = lowerer.lowerExpr(node); + const local = lowerer.declareHiddenLocal("%protoArg", arg.type); + stmts.push({ kind: "varDecl", localId: local.id, init: arg, loc }); + args.push(varRef(local.id, arg.type, loc)); + } + const result = callOwnOrBuiltin( + lowerer, + (name, type) => lowerer.declareHiddenLocal(name, type).id, + o, + receiver.type.shapeId, + method, + args, + want, + ts.getTextOfNode(access), + loc, + resolved, + ); + return { kind: "seqExpr", stmts, result, type: result.type, loc }; +} + +/** `r.toString` and the other Object.prototype methods read by name (not + * called) on an index-signature record: an own entry of the member's + * function type when the record holds one, else the built-in as a plain + * function value. An own entry of another type than the member's (a + * function of another signature) throws the checked-conversion TypeError: + * the program's types claimed the member. Null when the checker's member + * type is not a function. */ +export function lowerRecordInheritedMember( + lowerer: Lowerer, + receiver: IrExpr, + expr: ts.PropertyAccessExpression, +): IrExpr | null { + const method = expr.name.text; + if (!RECORD_INHERITED_METHODS.has(method) || !lowerer.isStdlibMember(expr)) return null; + if (receiver.type.kind !== "record") return null; + const want = lowerer.mapTypeOf(lowerer.typeOf(expr)); + if (want?.kind !== "func" || want.restAbi !== undefined) return null; + const shapeId = receiver.type.shapeId; + const shape = lowerer.shapes.get(shapeId); + if (!shape || shape.tuple) return null; + const loc = locOf(expr); + const builtin: IrExpr = { + kind: "closure", + fnName: unboundMethodImpl(lowerer, method, want, loc), + captures: [], + type: want, + loc, + }; + const recvLocal = lowerer.declareHiddenLocal("%protoRecv", receiver.type); + const o = varRef(recvLocal.id, receiver.type, loc); + const stmts: IrStmt[] = [{ kind: "varDecl", localId: recvLocal.id, init: receiver, loc }]; + let result: IrExpr = builtin; + if (shape.indexValue && typeEquals(shape.indexValue, want)) { + const ownType = lowerer.withUndefinedArmOf(want); + if (ownType?.kind === "union") { + const read: IrExpr = { + kind: "recordKeyGet", + obj: o, + shapeId, + key: { kind: "strLit", value: method, type: STRING, loc }, + overflowOnly: true, + type: ownType, + loc, + }; + const tag = lowerer.armTag(ownType.unionId, want); + const ownLocal = lowerer.declareHiddenLocal("%ownMethod", ownType); + const own = varRef(ownLocal.id, ownType, loc); + stmts.push({ kind: "varDecl", localId: ownLocal.id, init: read, loc }); + result = { + kind: "ternary", + cond: { + kind: "unionIsTag", + unionId: ownType.unionId, + tag, + negated: false, + value: own, + type: BOOL, + loc, + }, + then: { kind: "unionNarrow", unionId: ownType.unionId, tag, value: own, type: want, loc }, + else_: builtin, + type: want, + loc, + }; + } + } else if (shape.indexValue && callableType(lowerer, shape.indexValue)) { + result = { + kind: "ternary", + cond: ownEntryTest(lowerer, o, shapeId, method, loc), + then: nodeThrowExpr(1, "", OTHER_ARM_FAILURE_MESSAGE, want, loc), + else_: builtin, + type: want, + loc, + }; + } + return { kind: "seqExpr", stmts, result, type: want, loc }; +} diff --git a/packages/compiler/src/frontend/lowering/runtime-optional-elements.ts b/packages/compiler/src/frontend/lowering/runtime-optional-elements.ts index 96d20d2c4..0840cef91 100644 --- a/packages/compiler/src/frontend/lowering/runtime-optional-elements.ts +++ b/packages/compiler/src/frontend/lowering/runtime-optional-elements.ts @@ -16,6 +16,9 @@ export interface ElementStateHost { elementAdmitsUndefined(key: string): boolean; /** The value stored by `node` can be `undefined` at runtime. */ mayBeUndefined(node: ts.Expression): boolean; + /** Iterating `node`, an iterable other than an array (a Set, a Map's + * values, a generator), can yield `undefined`. */ + iterationMayYieldUndefined?(node: ts.Expression): boolean; /** A callback argument can return `undefined` at runtime. */ callbackReturnsUndefined(node: ts.Expression): boolean; /** The value may be a plain array-like object, whose missing indices @@ -28,6 +31,9 @@ export interface ElementStateHost { /** Element keys of the rest parameter that the spread arguments of * `call` fill: `[]` when the callee has none, null when it is unknown. */ restKeys(call: ts.CallExpression | ts.NewExpression): readonly string[] | null; + /** The argument index where the callee's rest parameter starts, or null + * when the callee has none or is unknown. */ + restStart?(call: ts.CallExpression | ts.NewExpression): number | null; /** Record that iterating `node`'s binding can observe `undefined`; * `known` when the iterated array's own facts (not only those shared by * its element ABI) say so. Returns true when the binding's record grew. */ @@ -37,8 +43,17 @@ export interface ElementStateHost { ownerOf(node: ts.Expression): ts.Symbol | null; /** The owning slot a newly built array is stored into. */ allocationOwner(node: ts.Expression): ts.Symbol | null; + /** `node` is a dynamic value (`any`, `unknown`, `unknown[]`): when it is + * a live view of a native array, a store through it writes that array's + * storage. */ + isDynamicArray(node: ts.Expression): boolean; + /** Element keys of the native arrays a value converted to a dynamic type + * can reach, as live views ("*" for any array). */ + nativeViewKeys(): ReadonlySet; /** Debug provenance for a new fact. */ - note: ((fact: "undefined" | "holes", key: string, site: ts.Node | undefined) => void) | null; + note: + | ((fact: "undefined" | "holes" | "foreign", key: string, site: ts.Node | undefined) => void) + | null; /** The scan read, or added to, the facts of an element key, of an owning * slot, or of every array (ANY_ARRAY), for dependency tracking * (SiteDependencies). */ @@ -48,6 +63,16 @@ export interface ElementStateHost { /** The fact name of the facts shared by every array. */ const ANY_ARRAY = "*"; +/** The fact name of holes stored through dynamic views. */ +const VIEW_HOLES = "%view"; +/** The fact name of values stored through dynamic views. */ +const VIEW_VALUES = "%view-values"; + +/** Element keys whose arrays store a value written through a dynamic view + * that the element type cannot hold, as a foreign element: `number[]` and + * `string[]`. Arrays of other element types reject such a write with the + * checked conversion's TypeError. */ +const FOREIGN_ELEMENT_KEYS: ReadonlySet = new Set(["f64", "string"]); /** What an array value can contain besides present values. */ interface Contents { @@ -81,6 +106,13 @@ export class ArrayElementStates { * whose element ABI is decided per generic instantiation. */ private anyUndefined = false; private anyHoley = false; + /** A store through a dynamic value can leave holes; they land in every + * native array such a value views. */ + private viewHoles = false; + /** A store through a dynamic value can write any value, which the + * viewed native arrays then hold as undefined or foreign elements. */ + private viewValues = false; + private viewKeys: ReadonlySet | null = null; /** Facts of arrays owned by one storage slot. */ private readonly undefinedOwners = new Set(); private readonly holeyOwners = new Set(); @@ -330,14 +362,65 @@ export class ArrayElementStates { this.host.read?.(ANY_ARRAY); if (keys === null) return null; for (const key of keys) this.host.read?.(key); + this.host.read?.(VIEW_HOLES); + this.host.read?.(VIEW_VALUES); return { undefined: this.anyUndefined || - keys.some((key) => this.undefinedKeys.has(key) || this.host.elementAdmitsUndefined(key)), - holes: this.anyHoley || keys.some((key) => this.holeyKeys.has(key)), + keys.some( + (key) => + this.undefinedKeys.has(key) || + this.host.elementAdmitsUndefined(key) || + this.viewedValues(key), + ), + holes: this.anyHoley || keys.some((key) => this.holeyKeys.has(key) || this.viewed(key)), }; } + /** Holes stored through a dynamic view can be in arrays of this key. */ + private viewed(key: string): boolean { + return this.viewHoles && this.isViewed(key); + } + + /** Values of any type stored through a dynamic view can be in arrays of + * this key. */ + private viewedValues(key: string): boolean { + return this.viewValues && FOREIGN_ELEMENT_KEYS.has(key) && this.isViewed(key); + } + + private isViewed(key: string): boolean { + this.viewKeys ??= this.host.nativeViewKeys(); + return this.viewKeys.has(key) || this.viewKeys.has(ANY_ARRAY); + } + + /** Element keys whose arrays can hold elements outside their element + * type, stored through dynamic views (of FOREIGN_ELEMENT_KEYS). Final + * once the fixed point is reached. */ + foreignKeys(): ReadonlySet { + if (!this.viewValues) return new Set(); + return new Set([...FOREIGN_ELEMENT_KEYS].filter((key) => this.isViewed(key))); + } + + /** A store through `receiver`, which has no native element key, can + * write a value of any type into a viewed native array. */ + private addViewValues(receiver: ts.Expression): boolean { + if (this.viewValues || !this.host.isDynamicArray(receiver)) return false; + this.viewValues = true; + this.host.changed?.(VIEW_VALUES); + this.host.note?.("foreign", VIEW_VALUES, this.site); + return true; + } + + /** A store through `receiver`, which has no native element key, can + * leave holes. Only a dynamic value can be a view of a native array. */ + private addViewHoles(receiver: ts.Expression): boolean { + if (this.viewHoles || !this.host.isDynamicArray(receiver)) return false; + this.viewHoles = true; + this.host.changed?.(VIEW_HOLES); + this.host.note?.("holes", VIEW_HOLES, this.site); + return true; + } + /** Reading every index of `node` (a spread, an iteration, `Array.from`) * can produce `undefined`. Only native arrays can: other iterables reject * an undefined value when it is stored. */ @@ -353,7 +436,11 @@ export class ArrayElementStates { /** One stored value, or the elements a spread copies. */ private valueMayBeUndefined(value: ts.Expression): boolean { - if (ts.isSpreadElement(value)) return this.mayYieldUndefined(value.expression); + if (ts.isSpreadElement(value)) + return ( + this.mayYieldUndefined(value.expression) || + (this.host.iterationMayYieldUndefined?.(value.expression) ?? false) + ); return this.host.mayBeUndefined(value); } @@ -427,7 +514,11 @@ export class ArrayElementStates { /** A store through `receiver` can leave holes. */ private addHolesTo(receiver: ts.Expression): boolean { const owner = this.host.ownerOf(receiver); - if (owner === null) return this.addHoles(this.host.arrayKeys(receiver)); + if (owner === null) { + const keys = this.host.arrayKeys(receiver); + if (keys !== null && keys.length === 0) return this.addViewHoles(receiver); + return this.addHoles(keys); + } if (this.holeyOwners.has(owner)) return false; this.holeyOwners.add(owner); this.host.changed?.(owner); @@ -487,10 +578,18 @@ export class ArrayElementStates { op === ts.SyntaxKind.QuestionQuestionEqualsToken || op === ts.SyntaxKind.BarBarEqualsToken || op === ts.SyntaxKind.AmpersandAmpersandEqualsToken; + const keys = this.host.arrayKeys(target.expression); + if (keys !== null && keys.length === 0) { + // Through a dynamic value: any element write can store a value the + // viewed native array's element type does not hold. + if (namesProperty(target)) return false; + let changed = false; + if (holes && this.addViewHoles(target.expression)) changed = true; + if (this.addViewValues(target.expression)) changed = true; + return changed; + } const undefinedValue = storesValue && this.host.mayBeUndefined(node.right); if (!holes && !undefinedValue) return false; - const keys = this.host.arrayKeys(target.expression); - if (keys !== null && keys.length === 0) return false; let changed = false; if (holes && this.addHolesTo(target.expression)) changed = true; if (undefinedValue && this.addUndefinedTo(target.expression)) changed = true; @@ -501,8 +600,6 @@ export class ArrayElementStates { target.name.text === "length" && !isShrinkingLength(node, target) ) { - const keys = this.host.arrayKeys(target.expression); - if (keys !== null && keys.length === 0) return false; return this.addHolesTo(target.expression); } return false; @@ -511,14 +608,26 @@ export class ArrayElementStates { private scanCall(node: ts.CallExpression | ts.NewExpression): boolean { let changed = false; const args = node.arguments?.nodes ?? []; - // Spreading a sparse array into a rest parameter fills the holes. + // Spreading a sparse array, or a tuple with undefined elements, into a + // rest parameter fills the holes. if (args.some((arg) => ts.isSpreadElement(arg))) { for (const arg of args) { - if (!ts.isSpreadElement(arg) || !this.mayYieldUndefined(arg.expression)) continue; + if (!ts.isSpreadElement(arg) || !this.valueMayBeUndefined(arg)) continue; const rest = this.host.restKeys(node) ?? this.host.arrayKeys(arg.expression); if (this.addUndefined(rest)) changed = true; } } + // An argument in a rest position that may be undefined is a present + // undefined element of the rest array. + const restStart = this.host.restStart?.(node) ?? null; + if (restStart !== null) + for (const arg of args.slice(restStart)) + if ( + !ts.isSpreadElement(arg) && + this.host.mayBeUndefined(arg) && + this.addUndefined(this.host.restKeys(node)) + ) + changed = true; if (this.record(node)) changed = true; const callee = peel(node.expression); if (!ts.isPropertyAccessExpression(callee)) return changed; @@ -541,7 +650,14 @@ export class ArrayElementStates { return changed; } const receiver = this.host.arrayKeys(callee.expression); - if (receiver !== null && receiver.length === 0) return changed; + if (receiver !== null && receiver.length === 0) { + if ( + (stored.length > 0 || callee.name.text === "fill") && + this.addViewValues(callee.expression) + ) + changed = true; + return changed; + } for (const value of stored) if (this.valueMayBeUndefined(value) && this.addUndefinedTo(callee.expression)) changed = true; return changed; @@ -574,10 +690,17 @@ export class ArrayElementStates { )); if (!reflective) return false; let changed = false; - for (const arg of args) { + // The written object: `Reflect.apply`'s receiver argument, otherwise + // the first argument. + const written = this.host.isGlobal(owner, "Reflect") && method === "apply" ? 1 : 0; + for (const [index, arg] of args.entries()) { const target = ts.isSpreadElement(arg) ? arg.expression : arg; const keys = this.host.arrayKeys(target); - if (keys !== null && keys.length === 0) continue; + if (keys !== null && keys.length === 0) { + if (index === written && this.addViewHoles(target)) changed = true; + if (index === written && this.addViewValues(target)) changed = true; + continue; + } if (this.addUndefinedTo(target)) changed = true; if (this.addHolesTo(target)) changed = true; } @@ -761,6 +884,14 @@ function isAppendIndex(target: ts.ElementAccessExpression): boolean { ); } +/** `o["name"]`: a string literal key that is not an array index writes a + * named property, never an element. */ +function namesProperty(target: ts.ElementAccessExpression): boolean { + const key = peel(target.argumentExpression); + if (!ts.isStringLiteral(key) && !ts.isNoSubstitutionTemplateLiteral(key)) return false; + return !/^(0|[1-9][0-9]*)$/.test(key.text) || Number(key.text) >= 2 ** 32 - 1; +} + /** `xs.length = 0`, `xs.length = xs.length - k` and `xs.length -= k` * truncate; truncation never leaves holes. */ function isShrinkingLength( diff --git a/packages/compiler/src/frontend/lowering/sanitize-class-types.ts b/packages/compiler/src/frontend/lowering/sanitize-class-types.ts index 77478d76d..6b89b446a 100644 --- a/packages/compiler/src/frontend/lowering/sanitize-class-types.ts +++ b/packages/compiler/src/frontend/lowering/sanitize-class-types.ts @@ -16,6 +16,8 @@ export function sanitizeUnregisteredClassTypes( case "object": return hasClass(type.className) ? type : F64; case "array": + type.elem = rewrite(type.elem); + break; case "set": type.elem = rewrite(type.elem); break; diff --git a/packages/compiler/src/frontend/lowering/site-dependencies.test.ts b/packages/compiler/src/frontend/lowering/site-dependencies.test.ts index ca5affcbe..0580a52ee 100644 --- a/packages/compiler/src/frontend/lowering/site-dependencies.test.ts +++ b/packages/compiler/src/frontend/lowering/site-dependencies.test.ts @@ -1,9 +1,13 @@ import { expect, test } from "vitest"; +import type * as ts from "../ts7/adapter.js"; import { SiteDependencies } from "./site-dependencies.js"; +/** A stand-in node fact: facts are compared by identity. */ +const node = (): ts.Node => ({}) as ts.Node; + test("a changed fact makes the sites that read it stale", () => { const dependencies = new SiteDependencies(3); - const a = {}; + const a = node(); const b = "key"; dependencies.begin(0); dependencies.read(a); @@ -20,8 +24,8 @@ test("a changed fact makes the sites that read it stale", () => { test("a visit that changes a fact makes every reader of its facts stale", () => { const dependencies = new SiteDependencies(3); - const shared = {}; - const own = {}; + const shared = node(); + const own = node(); dependencies.begin(0); dependencies.read(shared); dependencies.end(false); @@ -37,7 +41,7 @@ test("a visit that changes a fact makes every reader of its facts stale", () => test("reads outside a visit are not recorded", () => { const dependencies = new SiteDependencies(1); - const fact = {}; + const fact = node(); dependencies.read(fact); dependencies.changed(fact); expect(dependencies.isStale(0)).toBe(false); @@ -45,7 +49,7 @@ test("reads outside a visit are not recorded", () => { test("a site visited again is listed once per fact", () => { const dependencies = new SiteDependencies(3); - const fact = {}; + const fact = node(); for (const site of [0, 2, 1, 2, 0]) { dependencies.begin(site); dependencies.read(fact); diff --git a/packages/compiler/src/frontend/lowering/site-dependencies.ts b/packages/compiler/src/frontend/lowering/site-dependencies.ts index da8531e48..868531353 100644 --- a/packages/compiler/src/frontend/lowering/site-dependencies.ts +++ b/packages/compiler/src/frontend/lowering/site-dependencies.ts @@ -1,3 +1,5 @@ +import type * as ts from "../ts7/adapter.js"; + /** The facts each site of a fixed-point scan read on its last visit, so that * a pass visits again only the sites whose facts changed since. * @@ -13,14 +15,19 @@ * that with a pass over every site once a pass changes nothing, and resumes * when that pass changes a fact: a fact this tracking misses costs passes, * not a different result. */ +/** A fact a site reads: a symbol, a syntax node, or the name of a derived + * fact. A typed union keeps nodes and symbols native values; as `unknown` + * every syntax node would become a dynamic value. */ +export type SiteFact = ts.Node | ts.Symbol | string; + export class SiteDependencies { /** Each fact's entry: the visit that last read it, so that a visit lists a * fact once, followed by the sites that read it, ascending. */ - private readonly readers = new Map(); + private readonly readers = new Map(); private readonly stale: Uint8Array; private site = -1; private visit = 0; - private facts: unknown[] = []; + private facts: SiteFact[] = []; constructor(siteCount: number) { this.stale = new Uint8Array(siteCount); @@ -46,7 +53,7 @@ export class SiteDependencies { } /** The site being visited read `fact`. */ - read(fact: unknown): void { + read(fact: SiteFact): void { if (this.site < 0) return; const entry = this.readers.get(fact); if (entry === undefined) { @@ -72,7 +79,7 @@ export class SiteDependencies { } /** `fact` changed: every site that read it is visited again. */ - changed(fact: unknown): void { + changed(fact: SiteFact): void { const entry = this.readers.get(fact); if (entry !== undefined) for (let i = 1; i < entry.length; i++) this.stale[entry[i]!] = 1; } diff --git a/packages/compiler/src/frontend/lowering/static-typeof.ts b/packages/compiler/src/frontend/lowering/static-typeof.ts index 852cc3117..cef2d21aa 100644 --- a/packages/compiler/src/frontend/lowering/static-typeof.ts +++ b/packages/compiler/src/frontend/lowering/static-typeof.ts @@ -1,12 +1,14 @@ import * as ts from "../ts7/adapter.js"; import type { Lowerer } from "./lowerer.js"; +import { ambientUndefinedFnSymbolOf, ambientUndefinedVarSymbolOf } from "./lower-namespaces.js"; /** The answer `typeof ` gives without evaluating anything, for * the global-detection idioms (`typeof window < "u"`, `typeof global !== - * "undefined"`): a name nothing declares answers "undefined" (Node's - * answer for an undeclared global such as window or self, and typeof never - * throws for one), and the Node global object and process answer - * "object". Null for every other operand. */ + * "undefined"`): a name nothing declares, or one the program declares + * ambiently and nothing defines, answers "undefined" (Node's answer for an + * undeclared global such as window or self, and typeof never throws for + * one), and the Node global object and process answer "object". Null for + * every other operand. */ export function staticTypeofAnswer(lowerer: Lowerer, operand: ts.Expression): string | null { let e = operand; while (ts.isParenthesizedExpression(e)) e = e.expression; @@ -14,6 +16,10 @@ export function staticTypeofAnswer(lowerer: Lowerer, operand: ts.Expression): st if (lowerer.checker.getSymbolAtLocation(e) === undefined) return "undefined"; for (const name of ["global", "globalThis", "process"]) if (lowerer.isStdlibGlobal(e, name)) return "object"; + // A name the program declares ambiently but nothing defines (`declare + // const Bun`): undeclared at run time. + if (ambientUndefinedVarSymbolOf(lowerer, e) || ambientUndefinedFnSymbolOf(lowerer, e)) + return "undefined"; return null; } diff --git a/packages/compiler/src/frontend/lowering/string-index-bounds.ts b/packages/compiler/src/frontend/lowering/string-index-bounds.ts index af1b00ec6..6418aed76 100644 --- a/packages/compiler/src/frontend/lowering/string-index-bounds.ts +++ b/packages/compiler/src/frontend/lowering/string-index-bounds.ts @@ -182,11 +182,28 @@ export class StringIndexBounds { private readonly identifiers = new Map>(); constructor( - private readonly symbolAt: (node: ts.Node) => ts.Symbol | null, - private readonly declarationOf: (symbol: ts.Symbol) => ts.Node | undefined, - private readonly isString: (node: ts.Expression) => boolean, + private readonly lookupSymbol: (node: ts.Node) => ts.Symbol | null, + private readonly lookupDeclaration: (symbol: ts.Symbol) => ts.Node | undefined, + private readonly stringTyped: (node: ts.Expression) => boolean, ) {} + // The lookups are called as plain functions: the instance is not their + // receiver. + private symbolAt(node: ts.Node): ts.Symbol | null { + const lookup = this.lookupSymbol; + return lookup(node); + } + + private declarationOf(symbol: ts.Symbol): ts.Node | undefined { + const lookup = this.lookupDeclaration; + return lookup(symbol); + } + + private isString(node: ts.Expression): boolean { + const test = this.stringTyped; + return test(node); + } + /** True when `read` (an element access on a string receiver) is proven * to name an existing code unit. */ inBounds(read: ts.ElementAccessExpression): boolean { diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index 44564f2f1..3520e3aea 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -6,6 +6,20 @@ function unparenthesized(e: ts.Expression): ts.Expression { while (ts.isParenthesizedExpression(e)) e = e.expression; return e; } + +/** `e` without parentheses and type-only wrappers (as, satisfies, type + * assertions, non-null assertions), which change no runtime value. */ +function withoutTypeCasts(e: ts.Expression): ts.Expression { + while ( + ts.isParenthesizedExpression(e) || + ts.isAsExpression(e) || + ts.isSatisfiesExpression(e) || + ts.isTypeAssertion(e) || + ts.isNonNullExpression(e) + ) + e = e.expression; + return e; +} import { UNSUPPORTED } from "../../diagnostics/diagnostic.js"; import { BOOL, @@ -805,6 +819,9 @@ export interface BuiltinModuleFn { * direct-call row's `fn` (a direct call picks an arity-specific entry * point; the value accepts every arity through one). */ valueFn?: IrLibFn; + /** The function value's `length` when it is not the count of its + * required value parameters (`Math.max`'s 2 over its rest pack). */ + valueLength?: number; } export type BuiltinValueParam = @@ -859,6 +876,12 @@ export const OBJECT_CALLABLE_VALUES: Record result: DYN, valueParams: exactValueParams(DYN, DYN), }, + hasOwn: { + fn: "dyn.hasOwnComputed", + params: [DYN, DYN], + result: BOOL, + valueParams: exactValueParams(DYN, DYN), + }, keys: { fn: "dyn.objKeys", params: [DYN], result: DYN, valueParams: exactValueParams(DYN) }, values: { fn: "dyn.objValues", params: [DYN], result: DYN, valueParams: exactValueParams(DYN) }, entries: { fn: "dyn.objEntries", params: [DYN], result: DYN, valueParams: exactValueParams(DYN) }, @@ -1106,911 +1129,1002 @@ export const UTIL_TYPES_KEYS: readonly string[] = [ "isCryptoKey", ]; -export const BUILTIN_MODULE_FNS: Record< - string, - Record | undefined -> = { - fs: { - readFileSync: { fn: "fs.readFileSync", params: [STRING, STRING], result: STRING }, - // The exact-API subset is first-class. A fixed native ABI is not enough: - // rows whose Node function has behavior-bearing trailing options remain - // call-only, or function-width adaptation could discard those options - // without a diagnostic before entering the fixed runtime call. - writeFileSync: { fn: "fs.writeFileSync", params: [STRING, STRING], result: VOID }, - appendFileSync: { fn: "fs.appendFileSync", params: [STRING, STRING], result: VOID }, - existsSync: { - fn: "fs.existsSync", - params: [STRING], - result: BOOL, - valueParams: exactValueParams(STRING), - }, - mkdirSync: { fn: "fs.mkdirSync", params: [STRING], result: VOID }, - rmSync: { fn: "fs.rmSync", params: [STRING], result: VOID }, - rmdirSync: { fn: "fs.rmdirSync", params: [STRING], result: VOID }, - readdirSync: { fn: "fs.readdirSync", params: [STRING], result: arrayOf(STRING) }, - // Pattern and options are checked by the runtime (lowerFsGlobSyncCall). - globSync: { fn: "fs.globSync", params: [DYN, DYN], result: arrayOf(STRING) }, - statSync: { fn: "fs.statSync", params: [STRING], result: STATS_T }, - mkdtempSync: { fn: "fs.mkdtempSync", params: [STRING], result: STRING }, - // accessSync's omitted mode completes to 0 (F_OK) in the special case - // in lowerBuiltinModuleCall — string `defaults` can't spell a number. - accessSync: { fn: "fs.accessSync", params: [STRING, F64], result: VOID }, - unlinkSync: { - fn: "fs.unlinkSync", - params: [STRING], - result: VOID, - valueParams: exactValueParams(STRING), - }, - chmodSync: { - fn: "fs.chmodSync", - params: [STRING, F64], - result: VOID, - valueParams: exactValueParams(STRING, F64), - }, - chownSync: { - fn: "fs.chownSync", - params: [STRING, F64, F64], - result: VOID, - valueParams: exactValueParams(STRING, F64, F64), - }, - lchownSync: { - fn: "fs.lchownSync", - params: [STRING, F64, F64], - result: VOID, - valueParams: exactValueParams(STRING, F64, F64), - }, - fchownSync: { - fn: "fs.fchownSync", - params: [F64, F64, F64], - result: VOID, - valueParams: exactValueParams(F64, F64, F64), - }, - // truncateSync's omitted len completes to 0 in lowerBuiltinModuleCall, - // like accessSync's mode. - truncateSync: { fn: "fs.truncateSync", params: [STRING, F64], result: VOID }, - // The 2-argument form only: Node's mode flags (COPYFILE_EXCL, ...) - // land on the arity fence. - copyFileSync: { fn: "fs.copyFileSync", params: [STRING, STRING], result: VOID }, - // The callback form is special-cased in lowerBuiltinModuleCall: its - // Error | null parameter is program-shaped and needs an emitted - // adapter. The row still projects the static surface and routes the - // dispatch. - rename: { fn: "fs.renameCb", params: [], result: VOID }, - renameSync: { - fn: "fs.renameSync", - params: [STRING, STRING], - result: VOID, - valueParams: exactValueParams(STRING, STRING), - }, - // statSync's no-follow sibling; stats.isSymbolicLink answers what the - // follow-free snapshot saw. - // opendirSync's options (a literal { bufferSize, encoding: "utf8" }) - // lower in lowerBuiltinModuleCall; the row routes the dispatch. - opendirSync: { fn: "fs.opendirSync", params: [STRING, F64], result: DIR_T }, - // createReadStream/createWriteStream lower in lowerBuiltinModuleCall - // (path and options cross as dyn values); the rows route the dispatch. - createReadStream: { - fn: "fs.createReadStream", - params: [DYN, DYN], - result: { kind: "object", className: "%Readable" }, - }, - createWriteStream: { - fn: "fs.createWriteStream", - params: [DYN, DYN], - result: { kind: "object", className: "%Writable" }, - }, - lstatSync: { fn: "fs.lstatSync", params: [STRING], result: STATS_T }, - fstatSync: { - fn: "fs.fstatSync", - params: [F64], - result: STATS_T, - valueParams: exactValueParams(F64), - }, - fchmodSync: { - fn: "fs.fchmodSync", - params: [F64, F64], - result: VOID, - valueParams: exactValueParams(F64, F64), - }, - fsyncSync: { - fn: "fs.fsyncSync", - params: [F64], - result: VOID, - valueParams: exactValueParams(F64), - }, - fdatasyncSync: { - fn: "fs.fdatasyncSync", - params: [F64], - result: VOID, - valueParams: exactValueParams(F64), - }, - ftruncateSync: { fn: "fs.ftruncateSync", params: [F64, F64], result: VOID }, - utimesSync: { fn: "fs.utimesSync", params: [DYN, DYN, DYN], result: VOID }, - futimesSync: { fn: "fs.futimesSync", params: [DYN, DYN, DYN], result: VOID }, - lutimesSync: { fn: "fs.lutimesSync", params: [DYN, DYN, DYN], result: VOID }, - readvSync: { fn: "fs.readvSync", params: [F64, arrayOf(BYTES_U8), F64], result: F64 }, - writevSync: { fn: "fs.writevSync", params: [F64, arrayOf(BYTES_U8), F64], result: F64 }, - linkSync: { - fn: "fs.linkSync", - params: [DYN, DYN], - result: VOID, - valueParams: exactValueParams(DYN, DYN), - }, - // Options and paths are checked by the runtime (scr_fs_cp_begin). - cpSync: { - fn: "fs.cpSync", - params: [DYN, DYN, DYN], - result: VOID, - valueParams: [ - ...exactValueParams(DYN, DYN), - { mode: "optional", type: DYN, defaultValue: undefined }, - ], - }, - symlinkSync: { - fn: "fs.symlinkSync", - params: [DYN, DYN, DYN], - result: VOID, - valueParams: [ - ...exactValueParams(DYN, DYN), - { mode: "optional", type: DYN, defaultValue: undefined }, - ], - }, - readlinkSync: { - fn: "fs.readlinkSync", - params: [DYN, DYN], - result: DYN, - valueParams: [ - ...exactValueParams(DYN), - { mode: "optional", type: DYN, defaultValue: undefined }, - ], - }, - statfsSync: { - fn: "fs.statfsSync", - params: [DYN, DYN], - result: DYN, - valueParams: [ - ...exactValueParams(DYN), - { mode: "optional", type: DYN, defaultValue: undefined }, - ], - }, - // realpath(3) — Node's realpathSync (failures spell syscall "lstat", - // Node's own message shape). - realpathSync: { fn: "fs.realpathSync", params: [STRING], result: STRING }, - "realpathSync.native": { fn: "fs.realpathNativeSync", params: [STRING], result: STRING }, - // The fd pair behind spawn's fd-stdio form (the daemon-log idiom: - // openSync(logPath, "a") → spawn stdio ["ignore", fd, fd] → - // closeSync). String flags use the established two-argument path; - // inline numeric O_* expressions route through openNumericSync, with - // an optional creation mode as the third argument. - openSync: { fn: "fs.openSync", params: [STRING, STRING], result: F64 }, - // The buffer forms (fd, buffer, offset, length[, position]). The - // lowering completes an omitted or literal-null position to -1 (the - // runtime's current-offset sentinel); numeric positions read without - // advancing the fd. The options-object and bigint forms still fence. - readSync: { fn: "fs.readSync", params: [F64, BYTES_U8, F64, F64, F64], result: F64 }, - // Entirely special-cased: the classic Buffer window and utf8 string - // overloads lower to separate fixed-width runtime ABIs. As with - // readSync, -1 is the current-offset sentinel. - writeSync: { fn: "fs.writeSync", params: [F64, BYTES_U8, F64, F64, F64], result: F64 }, - closeSync: { - fn: "fs.closeSync", - params: [F64], - result: VOID, - valueParams: exactValueParams(F64), - }, - // Entirely special-cased (lowerFsWatchCall — the callback needs an - // adapter per listener shape); this entry only routes the dispatch. - watch: { fn: "fs.watch", params: [], result: VOID }, - }, - "fs/promises": { - // The sync operations behind already-settled promises: failures - // REJECT (catchable at the await) instead of throwing. readFile is - // utf8-fenced exactly like readFileSync (same special case below). - readFile: { - fn: "fsp.readFile", - params: [STRING, STRING], - result: { kind: "promise", inner: STRING }, - }, - writeFile: { - fn: "fsp.writeFile", - params: [STRING, STRING], - result: { kind: "promise", inner: VOID }, - }, - mkdir: { fn: "fsp.mkdir", params: [STRING], result: { kind: "promise", inner: VOID } }, - readdir: { - fn: "fsp.readdir", - params: [STRING], - result: { kind: "promise", inner: arrayOf(STRING) }, - }, - rm: { fn: "fsp.rm", params: [STRING], result: { kind: "promise", inner: VOID } }, - // The async iterator lowers only as a for await head (lowerFspGlobCall). - glob: { fn: "fsp.globStrings", params: [DYN, DYN], result: arrayOf(STRING) }, - stat: { fn: "fsp.stat", params: [STRING], result: { kind: "promise", inner: STATS_T } }, - realpath: { fn: "fsp.realpath", params: [STRING], result: { kind: "promise", inner: STRING } }, - lstat: { fn: "fsp.lstat", params: [STRING], result: { kind: "promise", inner: STATS_T } }, - opendir: { - fn: "fsp.opendir", - params: [STRING, F64], - result: { kind: "promise", inner: DIR_T }, - }, - unlink: { - fn: "fsp.unlink", - params: [STRING], - result: { kind: "promise", inner: VOID }, - valueParams: exactValueParams(STRING), - }, - chmod: { - fn: "fsp.chmod", - params: [STRING, F64], - result: { kind: "promise", inner: VOID }, - valueParams: exactValueParams(STRING, F64), - }, - chown: { - fn: "fsp.chown", - params: [STRING, F64, F64], - result: { kind: "promise", inner: VOID }, - valueParams: exactValueParams(STRING, F64, F64), - }, - lchown: { - fn: "fsp.lchown", - params: [STRING, F64, F64], - result: { kind: "promise", inner: VOID }, - valueParams: exactValueParams(STRING, F64, F64), - }, - // access' omitted mode and truncate's omitted len complete to 0 in - // lowerBuiltinModuleCall, like their sync spellings. - access: { fn: "fsp.access", params: [STRING, F64], result: { kind: "promise", inner: VOID } }, - truncate: { - fn: "fsp.truncate", - params: [STRING, F64], - result: { kind: "promise", inner: VOID }, - }, - // String data; Uint8Array/Buffer data routes to fsp.appendFileBytes in - // lowerBuiltinModuleCall. - appendFile: { - fn: "fsp.appendFile", - params: [STRING, STRING], - result: { kind: "promise", inner: VOID }, - }, - // The 2-argument form only, like copyFileSync. - copyFile: { - fn: "fsp.copyFile", - params: [STRING, STRING], - result: { kind: "promise", inner: VOID }, - }, - mkdtemp: { fn: "fsp.mkdtemp", params: [STRING], result: { kind: "promise", inner: STRING } }, - rmdir: { fn: "fsp.rmdir", params: [STRING], result: { kind: "promise", inner: VOID } }, - link: { - fn: "fsp.link", - params: [DYN, DYN], - result: { kind: "promise", inner: VOID }, - valueParams: exactValueParams(DYN, DYN), - }, - cp: { - fn: "fsp.cp", - params: [DYN, DYN, DYN], - result: { kind: "promise", inner: VOID }, - valueParams: [ - ...exactValueParams(DYN, DYN), - { mode: "optional", type: DYN, defaultValue: undefined }, - ], - }, - symlink: { - fn: "fsp.symlink", - params: [DYN, DYN, DYN], - result: { kind: "promise", inner: VOID }, - valueParams: [ - ...exactValueParams(DYN, DYN), - { mode: "optional", type: DYN, defaultValue: undefined }, - ], - }, - readlink: { - fn: "fsp.readlink", - params: [DYN, DYN], - result: { kind: "promise", inner: DYN }, - valueParams: [ - ...exactValueParams(DYN), - { mode: "optional", type: DYN, defaultValue: undefined }, - ], - }, - statfs: { - fn: "fsp.statfs", - params: [DYN, DYN], - result: { kind: "promise", inner: DYN }, - valueParams: [ - ...exactValueParams(DYN), - { mode: "optional", type: DYN, defaultValue: undefined }, - ], - }, - utimes: { fn: "fsp.utimes", params: [DYN, DYN, DYN], result: { kind: "promise", inner: VOID } }, - lutimes: { - fn: "fsp.lutimes", - params: [DYN, DYN, DYN], - result: { kind: "promise", inner: VOID }, - }, - rename: { - fn: "fsp.rename", - params: [STRING, STRING], - result: { kind: "promise", inner: VOID }, - valueParams: exactValueParams(STRING, STRING), - }, - // open's optional flags/mode completion is special-cased in - // lowerBuiltinModuleCall; this row routes all import spellings and - // gives coverage the static member. - open: { - fn: "fsp.open", - params: [STRING, STRING, F64], - result: { kind: "promise", inner: FILEHANDLE_T }, - valueParams: [ - { mode: "required", type: STRING }, - { mode: "optional", type: STRING, defaultValue: "r" }, - { mode: "optional", type: F64, defaultValue: 0o666 }, - ], - }, - }, - // The bare module's POSIX-target binding; a win32 target rebinds it to - // the win32 table (builtinModuleFnsOf — Node on Windows IS path.win32). - path: PATH_MODULE_FNS, - // Real Node modules ("node:path/posix" resolves), and the receivers of - // `path.posix.` / `path.win32.` through the namespace - // chain (builtinNamespaceModuleOf composes "path" + ".posix" to these - // keys). Both namespaces carry the FULL surface on every platform, - // Node's own rule: each answers its own platform's semantics anywhere. - "path/posix": PATH_MODULE_FNS, - "path/win32": PATH_WIN32_MODULE_FNS, - os: { - // One platform implementation: os.platform() === process.platform. - platform: { fn: "process.platform", params: [], result: STRING, valueParams: [] }, - homedir: { fn: "os.homedir", params: [], result: STRING, valueParams: [] }, - tmpdir: { fn: "os.tmpdir", params: [], result: STRING, valueParams: [] }, - // uname(2)'s release field — Node's own implementation. - release: { fn: "os.release", params: [], result: STRING, valueParams: [] }, - // uname(2)'s sysname field ("Darwin", "Linux", "Windows_NT") — the - // libFn/runtime pair predates this entry (the portless surface used - // them); this row makes the MODULE call reach them (test/common's - // isAIX/isIBMi getters are the canonical callers). - type: { fn: "os.type", params: [], result: STRING, valueParams: [] }, - // Total physical memory in bytes — same predating-pair story. - totalmem: { fn: "os.totalmem", params: [], result: F64, valueParams: [] }, - // uv_available_parallelism: the CPUs this process may use (affinity, - // then cgroup quota on Linux), at least 1. - availableParallelism: { - fn: "os.availableParallelism", - params: [], - result: F64, - valueParams: [], - }, - // os.arch() IS process.arch, like platform(). - arch: { fn: "process.arch", params: [], result: STRING, valueParams: [] }, - hostname: { fn: "os.hostname", params: [], result: STRING, valueParams: [] }, - // uv_os_uname's machine and version fields, read once at module load - // in Node (they never change while a process runs). - machine: { fn: "os.machine", params: [], result: STRING, valueParams: [] }, - version: { fn: "os.version", params: [], result: STRING, valueParams: [] }, - endianness: { fn: "os.endianness", params: [], result: STRING, valueParams: [] }, - freemem: { fn: "os.freemem", params: [], result: F64, valueParams: [] }, - uptime: { fn: "os.uptime", params: [], result: F64, valueParams: [] }, - loadavg: { fn: "os.loadavg", params: [], result: arrayOf(F64), valueParams: [] }, - // An omitted (or undefined) pid is 0, the current process - // (lowerOsGetPriorityCall completes it). - getPriority: { - fn: "os.getPriority", - params: [F64], - result: F64, - valueParams: [{ mode: "optional", type: F64, defaultValue: 0 }], - }, - // A one-argument call is setPriority(0, priority) - // (lowerOsSetPriorityCall completes it). - setPriority: { fn: "os.setPriority", params: [F64, F64], result: VOID }, - // Special-cased (lowerOsCpusCall): the call site's CpuInfo[] assembles - // from one uv_cpu_info snapshot. - cpus: { fn: "os.cpusTake", params: [], result: VOID }, - // Entirely special-cased (lowerOsNetworkInterfacesCall): the result is - // the call site's mapped Dict shape, verified - // structurally there — this entry only routes the dispatch. - networkInterfaces: { fn: "os.networkInterfaces", params: [], result: VOID }, - // Special-cased too (lowerOsUserInfoCall): the call site's mapped - // UserInfo record assembles field-by-field from scalar libCalls. - userInfo: { fn: "os.userName", params: [], result: VOID }, - }, - crypto: { - randomUUID: { - fn: "crypto.randomUUID", - params: [], - result: STRING, - // As a value (uuid's `export default { randomUUID }`), the optional - // options argument goes through the validating entry point. - valueFn: "crypto.randomUUIDOpts", - valueParams: [{ mode: "optional", type: DYN, defaultValue: undefined }], - }, - // A real Buffer result. The composed randomBytes(n).toString(enc) - // form KEEPS its one-libCall lowering (lowerCryptoComposedCall runs - // first and the Buffer never materializes there); this entry covers - // the bare calls and non-composed uses. - randomBytes: { - fn: "crypto.randomBytes", - params: [F64], - result: BYTES_U8, - valueParams: exactValueParams(F64), - }, - // The crypto utility surface is special-cased for overloads and - // string/Buffer splits in lowerCryptoModuleCall. These rows carry the - // canonical signatures for manifest projection and fallback dispatch. - createHash: { fn: "crypto.hashNew", params: [STRING], result: CRYPTOHASH_T }, - createHmac: { fn: "crypto.hmacNewStr", params: [STRING, STRING], result: CRYPTOHMAC_T }, - hash: { fn: "crypto.hashDigestStr", params: [STRING, STRING, STRING], result: STRING }, - timingSafeEqual: { fn: "crypto.timingSafeEqual", params: [BYTES_U8, BYTES_U8], result: BOOL }, - randomFillSync: { fn: "crypto.randomFill", params: [BYTES_U8, F64, F64], result: BYTES_U8 }, - getRandomValues: { fn: "crypto.getRandomValues", params: [BYTES_U8], result: BYTES_U8 }, - randomInt: { fn: "crypto.randomInt", params: [F64, F64], result: F64 }, - randomFill: { fn: "crypto.randomFillCb", params: [], result: VOID }, - randomUUIDv7: { - fn: "crypto.randomUUIDv7", - params: [DYN], - result: STRING, - valueParams: [{ mode: "optional", type: DYN, defaultValue: undefined }], - }, - createSecretKey: { fn: "crypto.createSecretKey", params: [DYN, DYN], result: DYN }, - generateKeySync: { fn: "crypto.generateKeySync", params: [DYN, DYN], result: DYN }, - generateKey: { fn: "crypto.generateKeyCb", params: [], result: VOID }, - // Asymmetric keys and signatures (lowerPkiCall): checked-dynamic - // arguments, results converted to the call site's type. - createPrivateKey: { fn: "crypto.createPrivateKey", params: [DYN], result: DYN }, - createPublicKey: { fn: "crypto.createPublicKey", params: [DYN], result: DYN }, - createSign: { fn: "crypto.createSign", params: [DYN], result: DYN }, - createVerify: { fn: "crypto.createVerify", params: [DYN], result: DYN }, - generateKeyPairSync: { fn: "crypto.generateKeyPairSync", params: [], result: DYN }, - generateKeyPair: { fn: "crypto.generateKeyPairCb", params: [], result: VOID }, - sign: { fn: "crypto.signOneshot", params: [], result: DYN }, - verify: { fn: "crypto.verifyOneshot", params: [], result: DYN }, - pbkdf2Sync: { - fn: "crypto.pbkdf2", - params: [BYTES_U8, BYTES_U8, F64, F64, STRING], - result: BYTES_U8, - }, - pbkdf2: { fn: "crypto.pbkdf2Cb", params: [], result: VOID }, - hkdfSync: { - fn: "crypto.hkdf", - params: [STRING, BYTES_U8, BYTES_U8, BYTES_U8, F64], - result: DYN, - }, - hkdf: { fn: "crypto.hkdfCb", params: [], result: VOID }, - scryptSync: { fn: "crypto.scrypt", params: [BYTES_U8, BYTES_U8, F64, DYN], result: BYTES_U8 }, - scrypt: { fn: "crypto.scryptCb", params: [], result: VOID }, - // Checked-dynamic arguments; the result is a native Cipheriv or - // Decipheriv handle (lowerCryptoModuleCall). - createCipheriv: { fn: "crypto.cipherNew", params: [BOOL, DYN, DYN, DYN, DYN], result: DYN }, - createDecipheriv: { fn: "crypto.cipherNew", params: [BOOL, DYN, DYN, DYN, DYN], result: DYN }, - // A checked-dynamic curve name; the result is a native ECDH handle - // (lowerCreateEcdh). - createECDH: { fn: "crypto.ecdhNew", params: [DYN], result: DYN }, - // Checked-dynamic arguments; the result is a native DiffieHellman or - // DiffieHellmanGroup handle (lowerCreateDiffieHellman and - // lowerGetDiffieHellman). - createDiffieHellman: { fn: "crypto.dhNew", params: [DYN, DYN, DYN, DYN], result: DYN }, - getDiffieHellman: { fn: "crypto.dhGroupNew", params: [DYN], result: DYN }, - createDiffieHellmanGroup: { fn: "crypto.dhGroupNew", params: [DYN], result: DYN }, - // Prime checks and generation (lowerPrimeCall): checked-dynamic - // arguments, results converted to the call site's type. - checkPrimeSync: { fn: "crypto.checkPrimeSync", params: [], result: DYN }, - // Checked-dynamic arguments, the result converted to the call site's - // type (lowerPrimeCall). - getCipherInfo: { fn: "crypto.getCipherInfo", params: [], result: DYN }, - checkPrime: { fn: "crypto.checkPrimeCb", params: [], result: VOID }, - generatePrimeSync: { fn: "crypto.generatePrimeSync", params: [], result: DYN }, - generatePrime: { fn: "crypto.generatePrimeCb", params: [], result: VOID }, - }, - zlib: { - // The default-options convenience family is special-cased in - // lowerBuiltinModuleCall: string inputs UTF-8 encode, callback forms - // carry program-shaped closures, and explicit options fence per site. - // native-toolchain.ts links libz only when these appear on the IR. - deflate: { fn: "zlib.deflateCb", params: [], result: VOID }, - deflateSync: { fn: "zlib.deflateSync", params: [BYTES_U8], result: BYTES_U8 }, - inflate: { fn: "zlib.inflateCb", params: [], result: VOID }, - inflateSync: { fn: "zlib.inflateSync", params: [BYTES_U8], result: BYTES_U8 }, - deflateRaw: { fn: "zlib.deflateRawCb", params: [], result: VOID }, - deflateRawSync: { fn: "zlib.deflateRawSync", params: [BYTES_U8], result: BYTES_U8 }, - inflateRaw: { fn: "zlib.inflateRawCb", params: [], result: VOID }, - inflateRawSync: { fn: "zlib.inflateRawSync", params: [BYTES_U8], result: BYTES_U8 }, - gzip: { fn: "zlib.gzipCb", params: [], result: VOID }, - gzipSync: { fn: "zlib.gzipSync", params: [BYTES_U8], result: BYTES_U8 }, - gunzip: { fn: "zlib.gunzipCb", params: [], result: VOID }, - gunzipSync: { fn: "zlib.gunzipSync", params: [BYTES_U8], result: BYTES_U8 }, - unzip: { fn: "zlib.unzipCb", params: [], result: VOID }, - unzipSync: { fn: "zlib.unzipSync", params: [BYTES_U8], result: BYTES_U8 }, - crc32: { fn: "zlib.crc32", params: [], result: F64 }, - // The stream factories (special-cased in lowerZlibModuleCall): each - // answers a runtime Transform stream over the zlib engine. - createDeflate: { fn: "zlib.streamNew", params: [], result: ZLIB_STREAM }, - createInflate: { fn: "zlib.streamNew", params: [], result: ZLIB_STREAM }, - createGzip: { fn: "zlib.streamNew", params: [], result: ZLIB_STREAM }, - createGunzip: { fn: "zlib.streamNew", params: [], result: ZLIB_STREAM }, - createDeflateRaw: { fn: "zlib.streamNew", params: [], result: ZLIB_STREAM }, - createInflateRaw: { fn: "zlib.streamNew", params: [], result: ZLIB_STREAM }, - createUnzip: { fn: "zlib.streamNew", params: [], result: ZLIB_STREAM }, - }, - url: { - fileURLToPath: { - fn: "url.fileURLToPathOptions", - params: [DYN, DYN], - result: STRING, - valueParams: [ - { mode: "required", type: DYN }, - { mode: "optional", type: DYN, defaultValue: undefined }, - ], - }, - fileURLToPathBuffer: { - fn: "url.fileURLToPathBuffer", - params: [DYN, DYN], - result: BYTES_U8, - valueParams: [ - { mode: "required", type: DYN }, - { mode: "optional", type: DYN, defaultValue: undefined }, - ], - }, - pathToFileURL: { - fn: "url.pathToFileURLChecked", - params: [DYN, DYN], - result: URL_T, - valueParams: [ - { mode: "required", type: DYN }, - { mode: "optional", type: DYN, defaultValue: undefined }, - ], - }, - // The legacy API and the domain pair (lowerLegacyUrlCall): arguments - // cross as dyn values for Node's own validators; url.parse() answers - // the checked-dynamic Url instance. - parse: { fn: "url.legacyParse", params: [DYN, DYN, DYN], result: DYN }, - format: { fn: "url.legacyFormat", params: [DYN, DYN], result: STRING }, - resolve: { fn: "url.legacyResolve", params: [DYN, DYN], result: STRING }, - domainToASCII: { fn: "url.domainToASCII", params: [DYN], result: STRING }, - domainToUnicode: { fn: "url.domainToUnicode", params: [DYN], result: STRING }, - }, - child_process: { - // spawnSync's and spawn's call completions are entirely special-cased - // (an omitted args list completes to an empty string[]; spawnSync - // accepts its documented literal options, while spawn supports the - // stdio/detached/env/cwd/windowsHide/shell subset — see - // lowerBuiltinModuleCall); the - // entries carry the canonical shapes. - spawnSync: { fn: "cp.spawnSync", params: [STRING, arrayOf(STRING)], result: SPAWNRES_T }, - spawn: { fn: "cp.spawn", params: [STRING, arrayOf(STRING)], result: CHILD_T }, - fork: { fn: "cp.fork", params: [STRING, arrayOf(STRING)], result: CHILD_T }, - // execFileSync(file, args?, options?) and execSync(command, options?) - // share the cp.execSync runtime entry (execSync sets the shell flag); - // both call completions are special-cased in lowerBuiltinModuleCall. - // The entries carry canonical shapes for the coverage/fence machinery. - execFileSync: { fn: "cp.execSync", params: [STRING, arrayOf(STRING)], result: STRING }, - execSync: { fn: "cp.execSync", params: [STRING], result: STRING }, - }, - // node:util lowers through dedicated paths: promisify - // only in the const-binding-over-execFile shape — recognized in - // lowerVarDecl / collectGlobals BEFORE any call lowering runs — and - // inspect/format/formatWithOptions plus parseArgs through the util spoke - // (lower-inspect.ts: per-type synthesized traversal helpers, - // compile-time format strings, checked-dynamic config/result for - // parseArgs), which both dispatch paths try before the generic table. - // parseArgs is tabled as well so the generated surface manifest records - // the dedicated static member; the spoke owns its optional arity and - // call-site-specific result type before this canonical signature is used. - util: { - styleText: { - fn: "util.styleText", - params: [DYN, DYN, DYN], - result: STRING, - valueParams: [ - { mode: "required", type: DYN }, - { mode: "required", type: DYN }, - { mode: "optional", type: DYN, defaultValue: undefined }, - ], - }, - isDeepStrictEqual: { - fn: "util.isDeepStrictEqual", - params: [DYN, DYN, DYN], - result: BOOL, - valueParams: [ - { mode: "required", type: DYN }, - { mode: "required", type: DYN }, - { mode: "optional", type: DYN, defaultValue: undefined }, - ], - }, - parseArgs: { fn: "util.parseArgs", params: [DYN], result: DYN }, - parseEnv: { fn: "util.parseEnv", params: [DYN], result: DYN }, - getSystemErrorName: { - fn: "util.getSystemErrorName", - params: [DYN], - result: STRING, - valueParams: exactValueParams(DYN), - }, - getSystemErrorMessage: { - fn: "util.getSystemErrorMessage", - params: [DYN], - result: STRING, - valueParams: exactValueParams(DYN), - }, - getSystemErrorMap: { fn: "util.systemErrorEntries", params: [], result: DYN }, - stripVTControlCharacters: { - fn: "util.stripVTControlCharacters", - params: [DYN], - result: STRING, - valueParams: exactValueParams(DYN), - }, - toUSVString: { - fn: "util.toUSVString", - params: [DYN], - result: STRING, - valueParams: exactValueParams(DYN), - }, - // The function helpers take checked-dynamic arguments and answer native - // dyn functions, which a static function slot checks when it receives - // one (lowerUtilCallableCall in the util spoke). - inherits: { - fn: "util.inherits", - params: [DYN, DYN], - result: VOID, - valueParams: [ - { mode: "required", type: DYN }, - { mode: "required", type: DYN }, - ], - }, - deprecate: { - fn: "util.deprecate", - params: [DYN, DYN, DYN, DYN], - result: DYN, - valueParams: [ - { mode: "required", type: DYN }, - { mode: "required", type: DYN }, - { mode: "optional", type: DYN, defaultValue: undefined }, - { mode: "optional", type: DYN, defaultValue: undefined }, - ], - }, - callbackify: { - fn: "util.callbackify", - params: [DYN], - result: DYN, - valueParams: [{ mode: "required", type: DYN }], - }, - debuglog: { - fn: "util.debuglog", - params: [DYN, DYN], - result: DYN, - valueParams: [ - { mode: "required", type: DYN }, - { mode: "optional", type: DYN, defaultValue: undefined }, - ], - }, - debug: { - fn: "util.debuglog", - params: [DYN, DYN], - result: DYN, - valueParams: [ - { mode: "required", type: DYN }, - { mode: "optional", type: DYN, defaultValue: undefined }, - ], - }, - }, - // The util/types spoke supplies the probe name to the shared brand check. - "util/types": { - isAnyArrayBuffer: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isArrayBuffer: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isArrayBufferView: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isDataView: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isTypedArray: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isUint8Array: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isUint8ClampedArray: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isUint16Array: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isUint32Array: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isInt8Array: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isInt16Array: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isInt32Array: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isFloat32Array: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isFloat64Array: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isMap: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isSet: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isWeakMap: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isWeakSet: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isRegExp: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isNativeError: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isPromise: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isProxy: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isAsyncFunction: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isGeneratorFunction: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isGeneratorObject: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isDate: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isSharedArrayBuffer: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isMapIterator: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isSetIterator: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isKeyObject: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isNumberObject: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isStringObject: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isBooleanObject: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isBigIntObject: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isSymbolObject: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isBoxedPrimitive: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isBigInt64Array: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isBigUint64Array: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isFloat16Array: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isCryptoKey: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isExternal: { fn: "util.typeIs", params: [DYN], result: BOOL }, - }, - buffer: { - isAscii: { fn: "buffer.isAscii", params: [DYN], result: BOOL }, - isUtf8: { fn: "buffer.isUtf8", params: [DYN], result: BOOL }, - transcode: { fn: "buffer.transcode", params: [DYN, DYN, DYN], result: BYTES_U8 }, - }, - tty: { - isatty: { fn: "process.isTTY", params: [F64], result: BOOL }, - }, - process: { - loadEnvFile: { fn: "process.loadEnvFile", params: [DYN], result: VOID }, - }, - // node:string_decoder's surface is the StringDecoder CLASS — new/write/ - // end are special-cased (lowerNew + lowerStringDecoderMethodCall); no - // function members exist to table. - string_decoder: {}, - // node:querystring — the legacy query-string codec (NOT URLSearchParams; - // the escaping and '+' rules differ). escape/unescape ride the table - // path directly; parse and stringify are special-cased in - // lowerBuiltinModuleCall (parse's result is the call site's mapped - // ParsedUrlQuery dictionary — the networkInterfaces verification stance - // — and its sep/eq/options complete there; stringify's object argument - // crosses as a dyn value). decode/encode are Node's own aliases of - // parse/stringify (`const decode = parse` in lib/querystring.js) and - // route to the same special cases; the entries carry canonical shapes. - querystring: { - parse: { fn: "qs.parse", params: [STRING], result: VOID }, - decode: { fn: "qs.parse", params: [STRING], result: VOID }, - stringify: { fn: "qs.stringify", params: [DYN], result: STRING }, - encode: { fn: "qs.stringify", params: [DYN], result: STRING }, - escape: { - fn: "qs.escape", - params: [STRING], - result: STRING, - valueParams: [{ mode: "required", type: STRING }], - }, - unescape: { - fn: "qs.unescape", - params: [STRING], - result: STRING, - valueParams: [{ mode: "required", type: STRING }], - }, - }, - // node:punycode (deprecated): punycode.js 2.1.0's conversions over - // strings (scr_url.c). ucs2.decode/encode are members of the module's - // ucs2 object, dispatched as module members (builtinMemberOf). The - // DEP0040 report rides the module's load sites (punycodeLoadWarning). - punycode: { - decode: { fn: "punycode.decode", params: [STRING], result: STRING }, - encode: { fn: "punycode.encode", params: [STRING], result: STRING }, - toASCII: { fn: "punycode.toASCII", params: [STRING], result: STRING }, - toUnicode: { fn: "punycode.toUnicode", params: [STRING], result: STRING }, - "ucs2.decode": { fn: "punycode.ucs2Decode", params: [STRING], result: arrayOf(F64) }, - "ucs2.encode": { fn: "punycode.ucs2Encode", params: [arrayOf(F64)], result: STRING }, - }, - // node:constants (deprecated) is one frozen object (Object.assign of - // os.constants' groups, fs.constants and crypto.constants): bindings of - // the module read it as a checked-dynamic value (builtinConstantsObject). - constants: {}, - // node:readline: createInterface's options are entirely special-cased - // (exactly { input: process.stdin, output?: process.stdout } — see - // lowerBuiltinModuleCall); the entry carries the canonical shape. The - // interface's question/close/on lower through - // lowerReadlineMethodCall. - readline: { - createInterface: { fn: "rl.create", params: [], result: F64 }, - }, - // node:readline/promises: the same Interface with the promise question - // (lowerReadlineMethodCall tells the forms apart by the call's result). - "readline/promises": { - createInterface: { fn: "rl.create", params: [], result: F64 }, - }, - // node:net and node:http lower ENTIRELY through the server spoke - // (lower-server.ts — every call shape is special-cased: closures, the - // optional connect host, writeHead's literal headers); the member list - // is the lower-server dispatch there. The modules still need keys here so future - // table entries have a home, and so the "recognized module, unlowered - // member" fence wording applies. - net: { - // The process-wide happy-eyeballs attempt budget: one runtime double - // in the core unit (never links scr_net.c by itself), default 250ms - // like Node's. - getDefaultAutoSelectFamilyAttemptTimeout: { - fn: "net.getAutoSelTimeout", - params: [], - result: F64, - }, - setDefaultAutoSelectFamilyAttemptTimeout: { - fn: "net.setAutoSelTimeout", - params: [F64], - result: VOID, - }, - // Node's IPv4Reg / IPv6Reg tests over String(input). - isIP: { fn: "net.isIP", params: [DYN], result: F64 }, - isIPv4: { fn: "net.isIPv4", params: [DYN], result: BOOL }, - isIPv6: { fn: "net.isIPv6", params: [DYN], result: BOOL }, - }, - http: {}, - // node:tls and node:https ride the same spoke (tls.createServer's - // options + secureConnection handler, https.createServer, and the - // https client's rejectUnauthorized/ca options are all special-cased - // in lower-server.ts). - tls: {}, - https: {}, - // node:dgram and node:dns lower ENTIRELY through the dgram spoke - // (lower-dgram.ts — createSocket's option literal, dns.lookup's - // options + callback, and the Socket method surface are all - // special-cased); the keys exist for the same reason net/http's do. - dgram: {}, - dns: {}, - // node:dns/promises rides the same spoke (lookup's promise form). - "dns/promises": {}, - // node:v8: getHeapStatistics answers allocator-derived figures in - // Node's shape (builtins/v8.ts — the entry carries the canonical - // shape); every other member fences with the by-design reason - // (builtinFenceHintOf). - v8: { - getHeapStatistics: { fn: "process.memoryUsageTake", params: [], result: VOID }, - }, - // node:stream/web re-exports the web stream globals (the same objects); - // its members lower through the globals' own paths. - "stream/web": {}, - // node:assert lowers ENTIRELY through the assert spoke (lower-assert.ts - // — every call shape is special-cased: optional messages complete, the - // comparisons pick per-type libCalls, deep equality synthesizes - // helpers); the keys exist so unlowered members fence module-qualified - // with the hints below. - assert: {}, - "assert/strict": {}, - // node:test lowers ENTIRELY through the test spoke (lower-test.ts — - // registrations, suites, hooks, the TestContext surface); the key - // exists so unlowered members fence module-qualified. - test: {}, - // node:stream/promises lowers through the stream spoke too - // (lower-stream.ts — finished/pipeline's promise forms special-case - // their stream arguments exactly like the callback forms); the key - // exists so unlowered members fence module-qualified. - "stream/promises": {}, - // node:stream/consumers rides the stream spoke as well (text/json/ - // buffer/arrayBuffer special-case their stream argument); blob fences - // module-qualified with the pointed hint below. - "stream/consumers": {}, - // node:timers/promises — the delay-only setTimeout and bare setImmediate - // (void promises the shared timer heap settles), and scheduler.wait / - // scheduler.yield, which Node defines as setTimeout(delay, undefined, - // options) and setImmediate(). The arity completion (omitted delay = - // Node's 1ms) and the value/options fences are special-cased in - // lowerBuiltinModuleCall; these entries carry the canonical shapes and - // route the dispatch. - "timers/promises": { - setTimeout: { fn: "tp.setTimeout", params: [F64], result: { kind: "promise", inner: VOID } }, - setImmediate: { fn: "tp.setImmediate", params: [], result: { kind: "promise", inner: VOID } }, - "scheduler.wait": { - fn: "tp.setTimeout", - params: [F64], - result: { kind: "promise", inner: VOID }, - }, - "scheduler.yield": { - fn: "tp.setImmediate", - params: [], - result: { kind: "promise", inner: VOID }, - }, - }, - // node:diagnostics_channel — the pub/sub core. channel() answers an f64 - // handle (type-mapper.ts maps Channel to F64); the subscriber arguments box - // into the checked-dynamic tree (special-cased in lowerBuiltinModuleCall — the entries - // carry canonical shapes and route the dispatch). The Channel method - // surface lowers through lowerDiagnosticsChannelMethodCall/lowerDiagnosticsChannelProperty. - diagnostics_channel: { - channel: { fn: "dc.channel", params: [STRING], result: F64 }, - subscribe: { fn: "dc.subscribe", params: [STRING, DYN], result: VOID }, - unsubscribe: { fn: "dc.unsubscribe", params: [STRING, DYN], result: BOOL }, - hasSubscribers: { fn: "dc.hasSubscribers", params: [STRING], result: BOOL }, - // The string form's canonical shape; the collection form (an object - // literal of five Channels) is special-cased in lowerBuiltinModuleCall. - tracingChannel: { fn: "dc.tracingChannel", params: [STRING], result: F64 }, - }, -}; +type BuiltinModuleFnTable = Record | undefined>; + +let builtinModuleFnTable: BuiltinModuleFnTable | undefined; + +/** Every builtin module's lowered function members, keyed by module and + * member. The table is built on the first call, so runs that do not lower + * a module do not construct it. */ +export function builtinModuleFns(): BuiltinModuleFnTable { + builtinModuleFnTable ??= buildBuiltinModuleFns(); + return builtinModuleFnTable; +} + +function buildBuiltinModuleFns(): BuiltinModuleFnTable { + return { + fs: { + readFileSync: { fn: "fs.readFileSync", params: [STRING, STRING], result: STRING }, + // The exact-API subset is first-class. A fixed native ABI is not enough: + // rows whose Node function has behavior-bearing trailing options remain + // call-only, or function-width adaptation could discard those options + // without a diagnostic before entering the fixed runtime call. + writeFileSync: { fn: "fs.writeFileSync", params: [STRING, STRING], result: VOID }, + appendFileSync: { fn: "fs.appendFileSync", params: [STRING, STRING], result: VOID }, + existsSync: { + fn: "fs.existsSync", + params: [STRING], + result: BOOL, + valueParams: exactValueParams(STRING), + }, + mkdirSync: { fn: "fs.mkdirSync", params: [STRING], result: VOID }, + rmSync: { fn: "fs.rmSync", params: [STRING], result: VOID }, + rmdirSync: { fn: "fs.rmdirSync", params: [STRING], result: VOID }, + readdirSync: { fn: "fs.readdirSync", params: [STRING], result: arrayOf(STRING) }, + // Pattern and options are checked by the runtime (lowerFsGlobSyncCall). + globSync: { fn: "fs.globSync", params: [DYN, DYN], result: arrayOf(STRING) }, + statSync: { fn: "fs.statSync", params: [STRING], result: STATS_T }, + mkdtempSync: { fn: "fs.mkdtempSync", params: [STRING], result: STRING }, + // accessSync's omitted mode completes to 0 (F_OK) in the special case + // in lowerBuiltinModuleCall — string `defaults` can't spell a number. + accessSync: { fn: "fs.accessSync", params: [STRING, F64], result: VOID }, + unlinkSync: { + fn: "fs.unlinkSync", + params: [STRING], + result: VOID, + valueParams: exactValueParams(STRING), + }, + chmodSync: { + fn: "fs.chmodSync", + params: [STRING, F64], + result: VOID, + valueParams: exactValueParams(STRING, F64), + }, + chownSync: { + fn: "fs.chownSync", + params: [STRING, F64, F64], + result: VOID, + valueParams: exactValueParams(STRING, F64, F64), + }, + lchownSync: { + fn: "fs.lchownSync", + params: [STRING, F64, F64], + result: VOID, + valueParams: exactValueParams(STRING, F64, F64), + }, + fchownSync: { + fn: "fs.fchownSync", + params: [F64, F64, F64], + result: VOID, + valueParams: exactValueParams(F64, F64, F64), + }, + // truncateSync's omitted len completes to 0 in lowerBuiltinModuleCall, + // like accessSync's mode. + truncateSync: { fn: "fs.truncateSync", params: [STRING, F64], result: VOID }, + // The 2-argument form only: Node's mode flags (COPYFILE_EXCL, ...) + // land on the arity fence. + copyFileSync: { fn: "fs.copyFileSync", params: [STRING, STRING], result: VOID }, + // The callback form is special-cased in lowerBuiltinModuleCall: its + // Error | null parameter is program-shaped and needs an emitted + // adapter. The row still projects the static surface and routes the + // dispatch. + rename: { fn: "fs.renameCb", params: [], result: VOID }, + renameSync: { + fn: "fs.renameSync", + params: [STRING, STRING], + result: VOID, + valueParams: exactValueParams(STRING, STRING), + }, + // statSync's no-follow sibling; stats.isSymbolicLink answers what the + // follow-free snapshot saw. + // opendirSync's options (a literal { bufferSize, encoding: "utf8" }) + // lower in lowerBuiltinModuleCall; the row routes the dispatch. + opendirSync: { fn: "fs.opendirSync", params: [STRING, F64], result: DIR_T }, + // createReadStream/createWriteStream lower in lowerBuiltinModuleCall + // (path and options cross as dyn values); the rows route the dispatch. + createReadStream: { + fn: "fs.createReadStream", + params: [DYN, DYN], + result: { kind: "object", className: "%Readable" }, + }, + createWriteStream: { + fn: "fs.createWriteStream", + params: [DYN, DYN], + result: { kind: "object", className: "%Writable" }, + }, + lstatSync: { fn: "fs.lstatSync", params: [STRING], result: STATS_T }, + fstatSync: { + fn: "fs.fstatSync", + params: [F64], + result: STATS_T, + valueParams: exactValueParams(F64), + }, + fchmodSync: { + fn: "fs.fchmodSync", + params: [F64, F64], + result: VOID, + valueParams: exactValueParams(F64, F64), + }, + fsyncSync: { + fn: "fs.fsyncSync", + params: [F64], + result: VOID, + valueParams: exactValueParams(F64), + }, + fdatasyncSync: { + fn: "fs.fdatasyncSync", + params: [F64], + result: VOID, + valueParams: exactValueParams(F64), + }, + ftruncateSync: { fn: "fs.ftruncateSync", params: [F64, F64], result: VOID }, + utimesSync: { fn: "fs.utimesSync", params: [DYN, DYN, DYN], result: VOID }, + futimesSync: { fn: "fs.futimesSync", params: [DYN, DYN, DYN], result: VOID }, + lutimesSync: { fn: "fs.lutimesSync", params: [DYN, DYN, DYN], result: VOID }, + readvSync: { fn: "fs.readvSync", params: [F64, arrayOf(BYTES_U8), F64], result: F64 }, + writevSync: { fn: "fs.writevSync", params: [F64, arrayOf(BYTES_U8), F64], result: F64 }, + linkSync: { + fn: "fs.linkSync", + params: [DYN, DYN], + result: VOID, + valueParams: exactValueParams(DYN, DYN), + }, + // Options and paths are checked by the runtime (scr_fs_cp_begin). + cpSync: { + fn: "fs.cpSync", + params: [DYN, DYN, DYN], + result: VOID, + valueParams: [ + ...exactValueParams(DYN, DYN), + { mode: "optional", type: DYN, defaultValue: undefined }, + ], + }, + symlinkSync: { + fn: "fs.symlinkSync", + params: [DYN, DYN, DYN], + result: VOID, + valueParams: [ + ...exactValueParams(DYN, DYN), + { mode: "optional", type: DYN, defaultValue: undefined }, + ], + }, + readlinkSync: { + fn: "fs.readlinkSync", + params: [DYN, DYN], + result: DYN, + valueParams: [ + ...exactValueParams(DYN), + { mode: "optional", type: DYN, defaultValue: undefined }, + ], + }, + statfsSync: { + fn: "fs.statfsSync", + params: [DYN, DYN], + result: DYN, + valueParams: [ + ...exactValueParams(DYN), + { mode: "optional", type: DYN, defaultValue: undefined }, + ], + }, + // realpath(3) — Node's realpathSync (failures spell syscall "lstat", + // Node's own message shape). + realpathSync: { fn: "fs.realpathSync", params: [STRING], result: STRING }, + "realpathSync.native": { fn: "fs.realpathNativeSync", params: [STRING], result: STRING }, + // The fd pair behind spawn's fd-stdio form (the daemon-log idiom: + // openSync(logPath, "a") → spawn stdio ["ignore", fd, fd] → + // closeSync). String flags use the established two-argument path; + // inline numeric O_* expressions route through openNumericSync, with + // an optional creation mode as the third argument. + openSync: { fn: "fs.openSync", params: [STRING, STRING], result: F64 }, + // The buffer forms (fd, buffer, offset, length[, position]). The + // lowering completes an omitted or literal-null position to -1 (the + // runtime's current-offset sentinel); numeric positions read without + // advancing the fd. The options-object and bigint forms still fence. + readSync: { fn: "fs.readSync", params: [F64, BYTES_U8, F64, F64, F64], result: F64 }, + // Entirely special-cased: the classic Buffer window and utf8 string + // overloads lower to separate fixed-width runtime ABIs. As with + // readSync, -1 is the current-offset sentinel. + writeSync: { fn: "fs.writeSync", params: [F64, BYTES_U8, F64, F64, F64], result: F64 }, + closeSync: { + fn: "fs.closeSync", + params: [F64], + result: VOID, + valueParams: exactValueParams(F64), + }, + // Entirely special-cased (lowerFsWatchCall — the callback needs an + // adapter per listener shape); this entry only routes the dispatch. + watch: { fn: "fs.watch", params: [], result: VOID }, + }, + "fs/promises": { + // The sync operations behind already-settled promises: failures + // REJECT (catchable at the await) instead of throwing. readFile is + // utf8-fenced exactly like readFileSync (same special case below). + readFile: { + fn: "fsp.readFile", + params: [STRING, STRING], + result: { kind: "promise", inner: STRING }, + }, + writeFile: { + fn: "fsp.writeFile", + params: [STRING, STRING], + result: { kind: "promise", inner: VOID }, + }, + mkdir: { fn: "fsp.mkdir", params: [STRING], result: { kind: "promise", inner: VOID } }, + readdir: { + fn: "fsp.readdir", + params: [STRING], + result: { kind: "promise", inner: arrayOf(STRING) }, + }, + rm: { fn: "fsp.rm", params: [STRING], result: { kind: "promise", inner: VOID } }, + // The async iterator lowers only as a for await head (lowerFspGlobCall). + glob: { fn: "fsp.globStrings", params: [DYN, DYN], result: arrayOf(STRING) }, + stat: { fn: "fsp.stat", params: [STRING], result: { kind: "promise", inner: STATS_T } }, + realpath: { + fn: "fsp.realpath", + params: [STRING], + result: { kind: "promise", inner: STRING }, + }, + lstat: { fn: "fsp.lstat", params: [STRING], result: { kind: "promise", inner: STATS_T } }, + opendir: { + fn: "fsp.opendir", + params: [STRING, F64], + result: { kind: "promise", inner: DIR_T }, + }, + unlink: { + fn: "fsp.unlink", + params: [STRING], + result: { kind: "promise", inner: VOID }, + valueParams: exactValueParams(STRING), + }, + chmod: { + fn: "fsp.chmod", + params: [STRING, F64], + result: { kind: "promise", inner: VOID }, + valueParams: exactValueParams(STRING, F64), + }, + chown: { + fn: "fsp.chown", + params: [STRING, F64, F64], + result: { kind: "promise", inner: VOID }, + valueParams: exactValueParams(STRING, F64, F64), + }, + lchown: { + fn: "fsp.lchown", + params: [STRING, F64, F64], + result: { kind: "promise", inner: VOID }, + valueParams: exactValueParams(STRING, F64, F64), + }, + // access' omitted mode and truncate's omitted len complete to 0 in + // lowerBuiltinModuleCall, like their sync spellings. + access: { fn: "fsp.access", params: [STRING, F64], result: { kind: "promise", inner: VOID } }, + truncate: { + fn: "fsp.truncate", + params: [STRING, F64], + result: { kind: "promise", inner: VOID }, + }, + // String data; Uint8Array/Buffer data routes to fsp.appendFileBytes in + // lowerBuiltinModuleCall. + appendFile: { + fn: "fsp.appendFile", + params: [STRING, STRING], + result: { kind: "promise", inner: VOID }, + }, + // The 2-argument form only, like copyFileSync. + copyFile: { + fn: "fsp.copyFile", + params: [STRING, STRING], + result: { kind: "promise", inner: VOID }, + }, + mkdtemp: { fn: "fsp.mkdtemp", params: [STRING], result: { kind: "promise", inner: STRING } }, + rmdir: { fn: "fsp.rmdir", params: [STRING], result: { kind: "promise", inner: VOID } }, + link: { + fn: "fsp.link", + params: [DYN, DYN], + result: { kind: "promise", inner: VOID }, + valueParams: exactValueParams(DYN, DYN), + }, + cp: { + fn: "fsp.cp", + params: [DYN, DYN, DYN], + result: { kind: "promise", inner: VOID }, + valueParams: [ + ...exactValueParams(DYN, DYN), + { mode: "optional", type: DYN, defaultValue: undefined }, + ], + }, + symlink: { + fn: "fsp.symlink", + params: [DYN, DYN, DYN], + result: { kind: "promise", inner: VOID }, + valueParams: [ + ...exactValueParams(DYN, DYN), + { mode: "optional", type: DYN, defaultValue: undefined }, + ], + }, + readlink: { + fn: "fsp.readlink", + params: [DYN, DYN], + result: { kind: "promise", inner: DYN }, + valueParams: [ + ...exactValueParams(DYN), + { mode: "optional", type: DYN, defaultValue: undefined }, + ], + }, + statfs: { + fn: "fsp.statfs", + params: [DYN, DYN], + result: { kind: "promise", inner: DYN }, + valueParams: [ + ...exactValueParams(DYN), + { mode: "optional", type: DYN, defaultValue: undefined }, + ], + }, + utimes: { + fn: "fsp.utimes", + params: [DYN, DYN, DYN], + result: { kind: "promise", inner: VOID }, + }, + lutimes: { + fn: "fsp.lutimes", + params: [DYN, DYN, DYN], + result: { kind: "promise", inner: VOID }, + }, + rename: { + fn: "fsp.rename", + params: [STRING, STRING], + result: { kind: "promise", inner: VOID }, + valueParams: exactValueParams(STRING, STRING), + }, + // open's optional flags/mode completion is special-cased in + // lowerBuiltinModuleCall; this row routes all import spellings and + // gives coverage the static member. + open: { + fn: "fsp.open", + params: [STRING, STRING, F64], + result: { kind: "promise", inner: FILEHANDLE_T }, + valueParams: [ + { mode: "required", type: STRING }, + { mode: "optional", type: STRING, defaultValue: "r" }, + { mode: "optional", type: F64, defaultValue: 0o666 }, + ], + }, + }, + // The bare module's POSIX-target binding; a win32 target rebinds it to + // the win32 table (builtinModuleFnsOf — Node on Windows IS path.win32). + path: PATH_MODULE_FNS, + // Real Node modules ("node:path/posix" resolves), and the receivers of + // `path.posix.` / `path.win32.` through the namespace + // chain (builtinNamespaceModuleOf composes "path" + ".posix" to these + // keys). Both namespaces carry the FULL surface on every platform, + // Node's own rule: each answers its own platform's semantics anywhere. + "path/posix": PATH_MODULE_FNS, + "path/win32": PATH_WIN32_MODULE_FNS, + os: { + // One platform implementation: os.platform() === process.platform. + platform: { fn: "process.platform", params: [], result: STRING, valueParams: [] }, + homedir: { fn: "os.homedir", params: [], result: STRING, valueParams: [] }, + tmpdir: { fn: "os.tmpdir", params: [], result: STRING, valueParams: [] }, + // uname(2)'s release field — Node's own implementation. + release: { fn: "os.release", params: [], result: STRING, valueParams: [] }, + // uname(2)'s sysname field ("Darwin", "Linux", "Windows_NT") — the + // libFn/runtime pair predates this entry (the portless surface used + // them); this row makes the MODULE call reach them (test/common's + // isAIX/isIBMi getters are the canonical callers). + type: { fn: "os.type", params: [], result: STRING, valueParams: [] }, + // Total physical memory in bytes — same predating-pair story. + totalmem: { fn: "os.totalmem", params: [], result: F64, valueParams: [] }, + // uv_available_parallelism: the CPUs this process may use (affinity, + // then cgroup quota on Linux), at least 1. + availableParallelism: { + fn: "os.availableParallelism", + params: [], + result: F64, + valueParams: [], + }, + // os.arch() IS process.arch, like platform(). + arch: { fn: "process.arch", params: [], result: STRING, valueParams: [] }, + hostname: { fn: "os.hostname", params: [], result: STRING, valueParams: [] }, + // uv_os_uname's machine and version fields, read once at module load + // in Node (they never change while a process runs). + machine: { fn: "os.machine", params: [], result: STRING, valueParams: [] }, + version: { fn: "os.version", params: [], result: STRING, valueParams: [] }, + endianness: { fn: "os.endianness", params: [], result: STRING, valueParams: [] }, + freemem: { fn: "os.freemem", params: [], result: F64, valueParams: [] }, + uptime: { fn: "os.uptime", params: [], result: F64, valueParams: [] }, + loadavg: { fn: "os.loadavg", params: [], result: arrayOf(F64), valueParams: [] }, + // An omitted (or undefined) pid is 0, the current process + // (lowerOsGetPriorityCall completes it). + getPriority: { + fn: "os.getPriority", + params: [F64], + result: F64, + valueParams: [{ mode: "optional", type: F64, defaultValue: 0 }], + }, + // A one-argument call is setPriority(0, priority) + // (lowerOsSetPriorityCall completes it). + setPriority: { fn: "os.setPriority", params: [F64, F64], result: VOID }, + // Special-cased (lowerOsCpusCall): the call site's CpuInfo[] assembles + // from one uv_cpu_info snapshot. + cpus: { fn: "os.cpusTake", params: [], result: VOID }, + // Entirely special-cased (lowerOsNetworkInterfacesCall): the result is + // the call site's mapped Dict shape, verified + // structurally there — this entry only routes the dispatch. + networkInterfaces: { fn: "os.networkInterfaces", params: [], result: VOID }, + // Special-cased too (lowerOsUserInfoCall): the call site's mapped + // UserInfo record assembles field-by-field from scalar libCalls. + userInfo: { fn: "os.userName", params: [], result: VOID }, + }, + crypto: { + randomUUID: { + fn: "crypto.randomUUID", + params: [], + result: STRING, + // As a value (uuid's `export default { randomUUID }`), the optional + // options argument goes through the validating entry point. + valueFn: "crypto.randomUUIDOpts", + valueParams: [{ mode: "optional", type: DYN, defaultValue: undefined }], + }, + // A real Buffer result. The composed randomBytes(n).toString(enc) + // form KEEPS its one-libCall lowering (lowerCryptoComposedCall runs + // first and the Buffer never materializes there); this entry covers + // the bare calls and non-composed uses. + randomBytes: { + fn: "crypto.randomBytes", + params: [F64], + result: BYTES_U8, + valueParams: exactValueParams(F64), + }, + // The crypto utility surface is special-cased for overloads and + // string/Buffer splits in lowerCryptoModuleCall. These rows carry the + // canonical signatures for manifest projection and fallback dispatch. + createHash: { fn: "crypto.hashNew", params: [STRING], result: CRYPTOHASH_T }, + createHmac: { fn: "crypto.hmacNewStr", params: [STRING, STRING], result: CRYPTOHMAC_T }, + hash: { fn: "crypto.hashDigestStr", params: [STRING, STRING, STRING], result: STRING }, + timingSafeEqual: { fn: "crypto.timingSafeEqual", params: [BYTES_U8, BYTES_U8], result: BOOL }, + randomFillSync: { fn: "crypto.randomFill", params: [BYTES_U8, F64, F64], result: BYTES_U8 }, + getRandomValues: { fn: "crypto.getRandomValues", params: [BYTES_U8], result: BYTES_U8 }, + randomInt: { fn: "crypto.randomInt", params: [F64, F64], result: F64 }, + randomFill: { fn: "crypto.randomFillCb", params: [], result: VOID }, + randomUUIDv7: { + fn: "crypto.randomUUIDv7", + params: [DYN], + result: STRING, + valueParams: [{ mode: "optional", type: DYN, defaultValue: undefined }], + }, + createSecretKey: { fn: "crypto.createSecretKey", params: [DYN, DYN], result: DYN }, + generateKeySync: { fn: "crypto.generateKeySync", params: [DYN, DYN], result: DYN }, + generateKey: { fn: "crypto.generateKeyCb", params: [], result: VOID }, + // Asymmetric keys and signatures (lowerPkiCall): checked-dynamic + // arguments, results converted to the call site's type. + createPrivateKey: { fn: "crypto.createPrivateKey", params: [DYN], result: DYN }, + createPublicKey: { fn: "crypto.createPublicKey", params: [DYN], result: DYN }, + createSign: { fn: "crypto.createSign", params: [DYN], result: DYN }, + createVerify: { fn: "crypto.createVerify", params: [DYN], result: DYN }, + generateKeyPairSync: { fn: "crypto.generateKeyPairSync", params: [], result: DYN }, + generateKeyPair: { fn: "crypto.generateKeyPairCb", params: [], result: VOID }, + sign: { fn: "crypto.signOneshot", params: [], result: DYN }, + verify: { fn: "crypto.verifyOneshot", params: [], result: DYN }, + diffieHellman: { fn: "crypto.diffieHellman", params: [], result: DYN }, + pbkdf2Sync: { + fn: "crypto.pbkdf2", + params: [BYTES_U8, BYTES_U8, F64, F64, STRING], + result: BYTES_U8, + }, + pbkdf2: { fn: "crypto.pbkdf2Cb", params: [], result: VOID }, + hkdfSync: { + fn: "crypto.hkdf", + params: [STRING, BYTES_U8, BYTES_U8, BYTES_U8, F64], + result: DYN, + }, + hkdf: { fn: "crypto.hkdfCb", params: [], result: VOID }, + scryptSync: { fn: "crypto.scrypt", params: [BYTES_U8, BYTES_U8, F64, DYN], result: BYTES_U8 }, + scrypt: { fn: "crypto.scryptCb", params: [], result: VOID }, + // Checked-dynamic arguments; the result is a native Cipheriv or + // Decipheriv handle (lowerCryptoModuleCall). + createCipheriv: { fn: "crypto.cipherNew", params: [BOOL, DYN, DYN, DYN, DYN], result: DYN }, + createDecipheriv: { fn: "crypto.cipherNew", params: [BOOL, DYN, DYN, DYN, DYN], result: DYN }, + // A checked-dynamic curve name; the result is a native ECDH handle + // (lowerCreateEcdh). + createECDH: { fn: "crypto.ecdhNew", params: [DYN], result: DYN }, + // Checked-dynamic arguments; the result is a native DiffieHellman or + // DiffieHellmanGroup handle (lowerCreateDiffieHellman and + // lowerGetDiffieHellman). + createDiffieHellman: { fn: "crypto.dhNew", params: [DYN, DYN, DYN, DYN], result: DYN }, + getDiffieHellman: { fn: "crypto.dhGroupNew", params: [DYN], result: DYN }, + createDiffieHellmanGroup: { fn: "crypto.dhGroupNew", params: [DYN], result: DYN }, + // Prime checks and generation (lowerPrimeCall): checked-dynamic + // arguments, results converted to the call site's type. + checkPrimeSync: { fn: "crypto.checkPrimeSync", params: [], result: DYN }, + // Checked-dynamic arguments, the result converted to the call site's + // type (lowerPrimeCall). + getCipherInfo: { fn: "crypto.getCipherInfo", params: [], result: DYN }, + checkPrime: { fn: "crypto.checkPrimeCb", params: [], result: VOID }, + generatePrimeSync: { fn: "crypto.generatePrimeSync", params: [], result: DYN }, + generatePrime: { fn: "crypto.generatePrimeCb", params: [], result: VOID }, + }, + zlib: { + // The default-options convenience family is special-cased in + // lowerBuiltinModuleCall: string inputs UTF-8 encode, callback forms + // carry program-shaped closures, and explicit options fence per site. + // native-toolchain.ts links libz only when these appear on the IR. + deflate: { fn: "zlib.deflateCb", params: [], result: VOID }, + deflateSync: { fn: "zlib.deflateSync", params: [BYTES_U8], result: BYTES_U8 }, + inflate: { fn: "zlib.inflateCb", params: [], result: VOID }, + inflateSync: { fn: "zlib.inflateSync", params: [BYTES_U8], result: BYTES_U8 }, + deflateRaw: { fn: "zlib.deflateRawCb", params: [], result: VOID }, + deflateRawSync: { fn: "zlib.deflateRawSync", params: [BYTES_U8], result: BYTES_U8 }, + inflateRaw: { fn: "zlib.inflateRawCb", params: [], result: VOID }, + inflateRawSync: { fn: "zlib.inflateRawSync", params: [BYTES_U8], result: BYTES_U8 }, + gzip: { fn: "zlib.gzipCb", params: [], result: VOID }, + gzipSync: { fn: "zlib.gzipSync", params: [BYTES_U8], result: BYTES_U8 }, + gunzip: { fn: "zlib.gunzipCb", params: [], result: VOID }, + gunzipSync: { fn: "zlib.gunzipSync", params: [BYTES_U8], result: BYTES_U8 }, + unzip: { fn: "zlib.unzipCb", params: [], result: VOID }, + unzipSync: { fn: "zlib.unzipSync", params: [BYTES_U8], result: BYTES_U8 }, + crc32: { fn: "zlib.crc32", params: [], result: F64 }, + // Brotli (special-cased in lowerZlibModuleCall): every form takes the + // options entries with Node's BROTLI_ENCODE or BROTLI_DECODE mode. + brotliCompress: { fn: "zlib.cbOpts", params: [], result: VOID }, + brotliCompressSync: { fn: "zlib.syncOpts", params: [], result: BYTES_U8 }, + brotliDecompress: { fn: "zlib.cbOpts", params: [], result: VOID }, + brotliDecompressSync: { fn: "zlib.syncOpts", params: [], result: BYTES_U8 }, + // The stream factories (special-cased in lowerZlibModuleCall): each + // answers a runtime Transform stream over the zlib engine. + createDeflate: { fn: "zlib.streamNew", params: [], result: ZLIB_STREAM }, + createInflate: { fn: "zlib.streamNew", params: [], result: ZLIB_STREAM }, + createGzip: { fn: "zlib.streamNew", params: [], result: ZLIB_STREAM }, + createGunzip: { fn: "zlib.streamNew", params: [], result: ZLIB_STREAM }, + createDeflateRaw: { fn: "zlib.streamNew", params: [], result: ZLIB_STREAM }, + createInflateRaw: { fn: "zlib.streamNew", params: [], result: ZLIB_STREAM }, + createUnzip: { fn: "zlib.streamNew", params: [], result: ZLIB_STREAM }, + createBrotliCompress: { fn: "zlib.streamNew", params: [], result: ZLIB_STREAM }, + createBrotliDecompress: { fn: "zlib.streamNew", params: [], result: ZLIB_STREAM }, + }, + url: { + fileURLToPath: { + fn: "url.fileURLToPathOptions", + params: [DYN, DYN], + result: STRING, + valueParams: [ + { mode: "required", type: DYN }, + { mode: "optional", type: DYN, defaultValue: undefined }, + ], + }, + fileURLToPathBuffer: { + fn: "url.fileURLToPathBuffer", + params: [DYN, DYN], + result: BYTES_U8, + valueParams: [ + { mode: "required", type: DYN }, + { mode: "optional", type: DYN, defaultValue: undefined }, + ], + }, + pathToFileURL: { + fn: "url.pathToFileURLChecked", + params: [DYN, DYN], + result: URL_T, + valueParams: [ + { mode: "required", type: DYN }, + { mode: "optional", type: DYN, defaultValue: undefined }, + ], + }, + // The legacy API and the domain pair (lowerLegacyUrlCall): arguments + // cross as dyn values for Node's own validators; url.parse() answers + // the checked-dynamic Url instance. + parse: { fn: "url.legacyParse", params: [DYN, DYN, DYN], result: DYN }, + format: { fn: "url.legacyFormat", params: [DYN, DYN], result: STRING }, + resolve: { fn: "url.legacyResolve", params: [DYN, DYN], result: STRING }, + domainToASCII: { fn: "url.domainToASCII", params: [DYN], result: STRING }, + domainToUnicode: { fn: "url.domainToUnicode", params: [DYN], result: STRING }, + urlToHttpOptions: { fn: "url.toHttpOptions", params: [DYN], result: DYN }, + }, + child_process: { + // spawnSync's and spawn's call completions are entirely special-cased + // (an omitted args list completes to an empty string[]; spawnSync + // accepts its documented literal options, while spawn supports the + // stdio/detached/env/cwd/windowsHide/shell subset — see + // lowerBuiltinModuleCall); the + // entries carry the canonical shapes. + spawnSync: { fn: "cp.spawnSync", params: [STRING, arrayOf(STRING)], result: SPAWNRES_T }, + spawn: { fn: "cp.spawn", params: [STRING, arrayOf(STRING)], result: CHILD_T }, + fork: { fn: "cp.fork", params: [STRING, arrayOf(STRING)], result: CHILD_T }, + // execFileSync(file, args?, options?) and execSync(command, options?) + // share the cp.execSync runtime entry (execSync sets the shell flag); + // both call completions are special-cased in lowerBuiltinModuleCall. + // The entries carry canonical shapes for the coverage/fence machinery. + execFileSync: { fn: "cp.execSync", params: [STRING, arrayOf(STRING)], result: STRING }, + execSync: { fn: "cp.execSync", params: [STRING], result: STRING }, + }, + // node:util lowers through dedicated paths: promisify + // only in the const-binding-over-execFile shape — recognized in + // lowerVarDecl / collectGlobals BEFORE any call lowering runs — and + // inspect/format/formatWithOptions plus parseArgs through the util spoke + // (lower-inspect.ts: per-type synthesized traversal helpers, + // compile-time format strings and the runtime formatter for the others, + // checked-dynamic config/result for + // parseArgs), which both dispatch paths try before the generic table. + // parseArgs is tabled as well so the generated surface manifest records + // the dedicated static member; the spoke owns its optional arity and + // call-site-specific result type before this canonical signature is used. + util: { + styleText: { + fn: "util.styleText", + params: [DYN, DYN, DYN], + result: STRING, + valueParams: [ + { mode: "required", type: DYN }, + { mode: "required", type: DYN }, + { mode: "optional", type: DYN, defaultValue: undefined }, + ], + }, + isDeepStrictEqual: { + fn: "util.isDeepStrictEqual", + params: [DYN, DYN, DYN], + result: BOOL, + valueParams: [ + { mode: "required", type: DYN }, + { mode: "required", type: DYN }, + { mode: "optional", type: DYN, defaultValue: undefined }, + ], + }, + parseArgs: { fn: "util.parseArgs", params: [DYN], result: DYN }, + aborted: { + fn: "util.aborted", + params: [DYN, DYN], + result: DYN, + valueParams: [ + { mode: "optional", type: DYN, defaultValue: undefined }, + { mode: "optional", type: DYN, defaultValue: undefined }, + ], + }, + diff: { + fn: "util.diff", + params: [DYN, DYN], + result: DYN, + valueParams: [ + { mode: "optional", type: DYN, defaultValue: undefined }, + { mode: "optional", type: DYN, defaultValue: undefined }, + ], + }, + debounce: { + fn: "util.debounce", + params: [DYN, DYN, DYN], + result: DYN, + valueParams: [ + { mode: "optional", type: DYN, defaultValue: undefined }, + { mode: "optional", type: DYN, defaultValue: undefined }, + { mode: "optional", type: DYN, defaultValue: undefined }, + ], + }, + throttle: { + fn: "util.throttle", + params: [DYN, DYN, DYN, DYN], + result: DYN, + valueParams: [ + { mode: "optional", type: DYN, defaultValue: undefined }, + { mode: "optional", type: DYN, defaultValue: undefined }, + { mode: "optional", type: DYN, defaultValue: undefined }, + { mode: "optional", type: DYN, defaultValue: undefined }, + ], + }, + convertProcessSignalToExitCode: { + fn: "util.signalExitCode", + params: [DYN], + result: F64, + valueParams: [{ mode: "optional", type: DYN, defaultValue: undefined }], + }, + // The deprecated helpers report DEP0044 and DEP0060 on their first call. + isArray: { + fn: "util.isArrayDeprecated", + params: [DYN], + result: BOOL, + valueParams: [{ mode: "optional", type: DYN, defaultValue: undefined }], + }, + _extend: { + fn: "util.extend", + params: [DYN, DYN], + result: DYN, + valueParams: [ + { mode: "optional", type: DYN, defaultValue: undefined }, + { mode: "optional", type: DYN, defaultValue: undefined }, + ], + }, + parseEnv: { fn: "util.parseEnv", params: [DYN], result: DYN }, + getSystemErrorName: { + fn: "util.getSystemErrorName", + params: [DYN], + result: STRING, + valueParams: exactValueParams(DYN), + }, + getSystemErrorMessage: { + fn: "util.getSystemErrorMessage", + params: [DYN], + result: STRING, + valueParams: exactValueParams(DYN), + }, + getSystemErrorMap: { fn: "util.systemErrorEntries", params: [], result: DYN }, + stripVTControlCharacters: { + fn: "util.stripVTControlCharacters", + params: [DYN], + result: STRING, + valueParams: exactValueParams(DYN), + }, + toUSVString: { + fn: "util.toUSVString", + params: [DYN], + result: STRING, + valueParams: exactValueParams(DYN), + }, + // The function helpers take checked-dynamic arguments and answer native + // dyn functions, which a static function slot checks when it receives + // one (lowerUtilCallableCall in the util spoke). + inherits: { + fn: "util.inherits", + params: [DYN, DYN], + result: VOID, + valueParams: [ + { mode: "required", type: DYN }, + { mode: "required", type: DYN }, + ], + }, + deprecate: { + fn: "util.deprecate", + params: [DYN, DYN, DYN, DYN], + result: DYN, + valueParams: [ + { mode: "required", type: DYN }, + { mode: "required", type: DYN }, + { mode: "optional", type: DYN, defaultValue: undefined }, + { mode: "optional", type: DYN, defaultValue: undefined }, + ], + }, + callbackify: { + fn: "util.callbackify", + params: [DYN], + result: DYN, + valueParams: [{ mode: "required", type: DYN }], + }, + debuglog: { + fn: "util.debuglog", + params: [DYN, DYN], + result: DYN, + valueParams: [ + { mode: "required", type: DYN }, + { mode: "optional", type: DYN, defaultValue: undefined }, + ], + }, + debug: { + fn: "util.debuglog", + params: [DYN, DYN], + result: DYN, + valueParams: [ + { mode: "required", type: DYN }, + { mode: "optional", type: DYN, defaultValue: undefined }, + ], + }, + }, + // The util/types spoke supplies the probe name to the shared brand check. + "util/types": { + isAnyArrayBuffer: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isArrayBuffer: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isArrayBufferView: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isDataView: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isTypedArray: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isUint8Array: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isUint8ClampedArray: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isUint16Array: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isUint32Array: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isInt8Array: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isInt16Array: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isInt32Array: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isFloat32Array: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isFloat64Array: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isMap: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isSet: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isWeakMap: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isWeakSet: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isRegExp: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isNativeError: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isPromise: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isProxy: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isAsyncFunction: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isGeneratorFunction: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isGeneratorObject: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isDate: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isSharedArrayBuffer: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isMapIterator: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isSetIterator: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isKeyObject: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isNumberObject: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isStringObject: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isBooleanObject: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isBigIntObject: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isSymbolObject: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isBoxedPrimitive: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isBigInt64Array: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isBigUint64Array: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isFloat16Array: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isCryptoKey: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isExternal: { fn: "util.typeIs", params: [DYN], result: BOOL }, + }, + buffer: { + isAscii: { fn: "buffer.isAscii", params: [DYN], result: BOOL }, + isUtf8: { fn: "buffer.isUtf8", params: [DYN], result: BOOL }, + transcode: { fn: "buffer.transcode", params: [DYN, DYN, DYN], result: BYTES_U8 }, + }, + tty: { + isatty: { fn: "process.isTTY", params: [F64], result: BOOL }, + }, + process: { + loadEnvFile: { fn: "process.loadEnvFile", params: [DYN], result: VOID }, + }, + // node:string_decoder's surface is the StringDecoder CLASS — new/write/ + // end are special-cased (lowerNew + lowerStringDecoderMethodCall); no + // function members exist to table. + string_decoder: {}, + // node:querystring — the legacy query-string codec (NOT URLSearchParams; + // the escaping and '+' rules differ). escape/unescape ride the table + // path directly; parse and stringify are special-cased in + // lowerBuiltinModuleCall (parse's result is the call site's mapped + // ParsedUrlQuery dictionary — the networkInterfaces verification stance + // — and its sep/eq/options complete there; stringify's object argument + // crosses as a dyn value). decode/encode are Node's own aliases of + // parse/stringify (`const decode = parse` in lib/querystring.js) and + // route to the same special cases; the entries carry canonical shapes. + querystring: { + parse: { fn: "qs.parse", params: [STRING], result: VOID }, + decode: { fn: "qs.parse", params: [STRING], result: VOID }, + stringify: { fn: "qs.stringify", params: [DYN], result: STRING }, + encode: { fn: "qs.stringify", params: [DYN], result: STRING }, + escape: { + fn: "qs.escape", + params: [STRING], + result: STRING, + valueParams: [{ mode: "required", type: STRING }], + }, + unescape: { + fn: "qs.unescape", + params: [STRING], + result: STRING, + valueParams: [{ mode: "required", type: STRING }], + }, + }, + // node:punycode (deprecated): punycode.js 2.1.0's conversions over + // strings (scr_url.c). ucs2.decode/encode are members of the module's + // ucs2 object, dispatched as module members (builtinMemberOf). The + // DEP0040 report rides the module's load sites (punycodeLoadWarning). + punycode: { + decode: { fn: "punycode.decode", params: [STRING], result: STRING }, + encode: { fn: "punycode.encode", params: [STRING], result: STRING }, + toASCII: { fn: "punycode.toASCII", params: [STRING], result: STRING }, + toUnicode: { fn: "punycode.toUnicode", params: [STRING], result: STRING }, + "ucs2.decode": { fn: "punycode.ucs2Decode", params: [STRING], result: arrayOf(F64) }, + "ucs2.encode": { fn: "punycode.ucs2Encode", params: [arrayOf(F64)], result: STRING }, + }, + // node:constants (deprecated) is one frozen object (Object.assign of + // os.constants' groups, fs.constants and crypto.constants): bindings of + // the module read it as a checked-dynamic value (builtinConstantsObject). + constants: {}, + // node:readline: createInterface's options are entirely special-cased + // (exactly { input: process.stdin, output?: process.stdout } — see + // lowerBuiltinModuleCall); the entry carries the canonical shape. The + // interface's question/close/on lower through + // lowerReadlineMethodCall. + readline: { + createInterface: { fn: "rl.create", params: [], result: F64 }, + }, + // node:readline/promises: the same Interface with the promise question + // (lowerReadlineMethodCall tells the forms apart by the call's result). + "readline/promises": { + createInterface: { fn: "rl.create", params: [], result: F64 }, + }, + // node:net and node:http lower ENTIRELY through the server spoke + // (lower-server.ts — every call shape is special-cased: closures, the + // optional connect host, writeHead's literal headers); the member list + // is the lower-server dispatch there. The modules still need keys here so future + // table entries have a home, and so the "recognized module, unlowered + // member" fence wording applies. + net: { + // The process-wide happy-eyeballs attempt budget: one runtime double + // in the core unit (never links scr_net.c by itself), default 250ms + // like Node's. + getDefaultAutoSelectFamilyAttemptTimeout: { + fn: "net.getAutoSelTimeout", + params: [], + result: F64, + }, + setDefaultAutoSelectFamilyAttemptTimeout: { + fn: "net.setAutoSelTimeout", + params: [F64], + result: VOID, + }, + // Node's IPv4Reg / IPv6Reg tests over String(input). + isIP: { fn: "net.isIP", params: [DYN], result: F64 }, + isIPv4: { fn: "net.isIPv4", params: [DYN], result: BOOL }, + isIPv6: { fn: "net.isIPv6", params: [DYN], result: BOOL }, + }, + http: {}, + // node:tls and node:https ride the same spoke (tls.createServer's + // options + secureConnection handler, https.createServer, and the + // https client's rejectUnauthorized/ca options are all special-cased + // in lower-server.ts). + tls: {}, + https: {}, + // node:dgram and node:dns lower ENTIRELY through the dgram spoke + // (lower-dgram.ts — createSocket's option literal, dns.lookup's + // options + callback, and the Socket method surface are all + // special-cased); the keys exist for the same reason net/http's do. + dgram: {}, + dns: {}, + // node:dns/promises rides the same spoke (lookup's promise form). + "dns/promises": {}, + // node:v8: getHeapStatistics answers allocator-derived figures in + // Node's shape (builtins/v8.ts — the entry carries the canonical + // shape); every other member fences with the by-design reason + // (builtinFenceHintOf). + v8: { + getHeapStatistics: { fn: "process.memoryUsageTake", params: [], result: VOID }, + }, + // node:stream/web re-exports the web stream globals (the same objects); + // its members lower through the globals' own paths. + "stream/web": {}, + // node:assert lowers ENTIRELY through the assert spoke (lower-assert.ts + // — every call shape is special-cased: optional messages complete, the + // comparisons pick per-type libCalls, deep equality synthesizes + // helpers); the keys exist so unlowered members fence module-qualified + // with the hints below. + assert: {}, + "assert/strict": {}, + // node:test lowers ENTIRELY through the test spoke (lower-test.ts — + // registrations, suites, hooks, the TestContext surface); the key + // exists so unlowered members fence module-qualified. + test: {}, + // node:stream/promises lowers through the stream spoke too + // (lower-stream.ts — finished/pipeline's promise forms special-case + // their stream arguments exactly like the callback forms); the key + // exists so unlowered members fence module-qualified. + "stream/promises": {}, + // node:stream/consumers rides the stream spoke as well (text/json/ + // buffer/arrayBuffer special-case their stream argument); blob fences + // module-qualified with the pointed hint below. + "stream/consumers": {}, + // node:timers/promises — the delay-only setTimeout and bare setImmediate + // (void promises the shared timer heap settles), and scheduler.wait / + // scheduler.yield, which Node defines as setTimeout(delay, undefined, + // options) and setImmediate(). The arity completion (omitted delay = + // Node's 1ms) and the value/options fences are special-cased in + // lowerBuiltinModuleCall; these entries carry the canonical shapes and + // route the dispatch. + "timers/promises": { + setTimeout: { fn: "tp.setTimeout", params: [F64], result: { kind: "promise", inner: VOID } }, + setImmediate: { fn: "tp.setImmediate", params: [], result: { kind: "promise", inner: VOID } }, + "scheduler.wait": { + fn: "tp.setTimeout", + params: [F64], + result: { kind: "promise", inner: VOID }, + }, + "scheduler.yield": { + fn: "tp.setImmediate", + params: [], + result: { kind: "promise", inner: VOID }, + }, + }, + // node:diagnostics_channel — the pub/sub core. channel() answers an f64 + // handle (type-mapper.ts maps Channel to F64); the subscriber arguments box + // into the checked-dynamic tree (special-cased in lowerBuiltinModuleCall — the entries + // carry canonical shapes and route the dispatch). The Channel method + // surface lowers through lowerDiagnosticsChannelMethodCall/lowerDiagnosticsChannelProperty. + diagnostics_channel: { + channel: { fn: "dc.channel", params: [STRING], result: F64 }, + subscribe: { fn: "dc.subscribe", params: [STRING, DYN], result: VOID }, + unsubscribe: { fn: "dc.unsubscribe", params: [STRING, DYN], result: BOOL }, + hasSubscribers: { fn: "dc.hasSubscribers", params: [STRING], result: BOOL }, + // The string form's canonical shape; the collection form (an object + // literal of five Channels) is special-cased in lowerBuiltinModuleCall. + tracingChannel: { fn: "dc.tracingChannel", params: [STRING], result: F64 }, + }, + }; +} /** Builtin-module CONSTANTS (value reads of named imports): each lowers to * a literal of its value's kind. The bare-module values are the TARGET @@ -2047,7 +2161,7 @@ export const BUILTIN_MODULE_CONSTS: Record< * the sidecar's determinism attestation demotes on them by prefix, so the * member's fence detector must witness them too (fencing one spelling * fences the operation, the trimLeft/trimStart rule). Keyed like - * BUILTIN_MODULE_FNS; consumed by the fence taxonomy + * builtinModuleFns(); consumed by the fence taxonomy * (library/fence-eval.ts) and the attestation-parity test. */ export const BUILTIN_MODULE_FN_ALIASES: Record< string, @@ -2070,7 +2184,16 @@ export const BUILTIN_MODULE_FN_ALIASES: Record< "fs.readFdEncoded", "fs.readFdSync", "fs.readFdSyncBytes", + // The runtime encoding's check before a URL path converts + // (fs-paths.ts). + "fs.checkReadEncoding", ], + // The string source's check before a URL destination converts + // (fs-paths.ts). + copyFileSync: ["fs.checkPath"], + renameSync: ["fs.checkPath"], + copyFile: ["fs.checkPath"], + rename: ["fs.checkPath"], readlinkSync: ["fs.readlinkSyncStr", "fs.readlinkSyncBuffer"], // The bytes-data form and the { mode } options form. writeFileSync: ["fs.writeFileSyncBytes", "fs.writeFileModeSync"], @@ -2094,12 +2217,14 @@ export const BUILTIN_MODULE_FN_ALIASES: Record< ], // The utf8 string overload; the table row is the Buffer-window form. writeSync: ["fs.writeStrSync"], + // The form whose arguments Node checks at run time. + readSync: ["fs.readSyncDyn"], // The { recursive, mode } option lowerings. mkdirSync: ["fs.mkdirModeSync", "fs.mkdirRecursiveSync", "fs.mkdirRecursiveModeSync"], // The { recursive, force } and maxRetries/retryDelay option lowerings. rmSync: ["fs.rmOptsSync", "fs.rmRetrySync"], - // The { withFileTypes: true } Dirent form. - readdirSync: ["fs.readdirTypesSync"], + // The { withFileTypes: true } Dirent form and the { recursive: true } walk. + readdirSync: ["fs.readdirTypesSync", "fs.readdirRecursiveSync"], // The { throwIfNoEntry } probe form. statSync: ["fs.statProbeSync"], // realpathSync and realpathSync.native read as function values. @@ -2188,621 +2313,643 @@ export interface AmbientSurfaceRow { note?: string; } -export const AMBIENT_SURFACE_FNS: readonly AmbientSurfaceRow[] = [ - { - id: "node-builtin.worker_threads.Worker", - kind: "node-builtin", - name: "worker_threads.Worker", - fns: ["worker.new"], - note: "native threads with statically compiled entry points, workerData, argv and message events", - }, - { - id: "node-builtin.worker_threads.metadata", - kind: "node-builtin", - name: "worker_threads metadata", - fns: [ - "worker.isMainThread", - "worker.threadId", - "worker.data", - "worker.parentPort", - "worker.resourceLimits", - "worker.threadName", - ], - note: "current-thread metadata and the worker's parent message port", - }, - { - id: "stdlib.sharedArrayBuffer", - kind: "stdlib", - name: "SharedArrayBuffer", - fns: ["sharedArrayBuffer.new"], - note: "fixed shared storage with context-local typed-array and DataView wrappers", - }, - { - id: "stdlib.blob", - kind: "stdlib", - name: "Blob", - fns: ["blob.new", "blob.slice", "blob.text", "blob.bytes", "blob.arrayBuffer", "blob.is"], - note: "immutable bytes with size, type, slice(), text(), bytes(), arrayBuffer() and structured cloning", - }, - { - id: "stdlib.file", - kind: "stdlib", - name: "File", - fns: ["blob.fileNew"], - note: "a Blob with name and lastModified (Date.now() when absent)", - }, - { - id: "stdlib.webcrypto.subtle", - kind: "stdlib", - name: "globalThis.crypto.subtle", - fns: [ - "webcrypto.generateKey", - "webcrypto.importKey", - "webcrypto.exportKey", - "webcrypto.encrypt", - "webcrypto.decrypt", - "webcrypto.wrapKey", - "webcrypto.unwrapKey", - "webcrypto.deriveBits", - "webcrypto.deriveKey", - "webcrypto.sign", - "webcrypto.verify", - "webcrypto.digest", - ], - note: "importKey, generateKey and exportKey (raw and jwk) for HMAC and AES keys, importKey for PBKDF2 and HKDF keys, AES-GCM, AES-CBC and AES-CTR encrypt and decrypt, wrapKey and unwrapKey of secret keys with AES-KW or those ciphers, PBKDF2 and HKDF deriveBits and deriveKey, HMAC sign and verify, generateKey, importKey, exportKey, sign and verify for ECDSA, Ed25519, RSASSA-PKCS1-v1_5 and RSA-PSS, and SHA-1/SHA-2 digest; other methods, algorithms and key formats are refused", - }, - { - id: "stdlib.webcrypto.CryptoKey", - kind: "stdlib", - name: "CryptoKey", - fns: ["webcrypto.isCryptoKey"], - note: "secret HMAC, AES, PBKDF2 and HKDF keys with type, extractable, algorithm and usages, and instanceof CryptoKey", - }, - { - id: "stdlib.atomics", - kind: "stdlib", - name: "Atomics", - fns: ["atomics.op", "atomics.wait", "atomics.notify"], - note: "8-, 16- and 32-bit integer operations, plus Int32Array wait and notify", - }, - { - id: "node-builtin.fs.callbacks", - kind: "node-builtin", - name: "fs error-first callbacks", - fns: ["fs.callbackValue", "fs.callbackCall", "fs.callbackGlob"], - note: "native filesystem callbacks, including stored platform adapter functions", - }, - // ── the Date slice (lowerDateCall/lowerNew): statics, construction, - // stored read-only values, calendar getters, and ISO formatting. - { - id: "stdlib.date.now", - kind: "stdlib", - name: "Date.now", - fns: ["date.now"], - note: "the live clock", - }, - { - id: "stdlib.date.parse", - kind: "stdlib", - name: "Date.parse", - fns: ["date.parse"], - note: "one date string, using V8's date parser shared with new Date(dateString)", - }, - { - id: "stdlib.date.constructor", - kind: "stdlib", - name: "Date constructor", - fns: [ - "date.newNow", - "date.newMs", - "date.newString", - "date.nativeNew", - "date.newLocal", - "date.nativeNewFields", - "date.nativeNewValue", - "date.argumentsTime", - "date.classOperation", - ], - note: "zero arguments, one milliseconds/date-string argument, or local-time year/month fields; values are the read-only TimeClip scalar slice", - }, - { - id: "stdlib.date.UTC", - kind: "stdlib", - name: "Date.UTC", - fns: ["date.utc"], - note: "the lowered call form takes 1 to 7 number arguments", - }, - { - id: "stdlib.date.getTime", - kind: "stdlib", - name: "Date.prototype.getTime", - fns: ["date.getTime", "date.parseGetTime"], - note: "the millisecond value of a stored Date", - }, - { - id: "stdlib.date.valueOf", - kind: "stdlib", - name: "Date.prototype.valueOf", - fns: ["date.valueOf"], - note: "the same millisecond read as getTime()", - }, - { - id: "stdlib.date.toISOString", - kind: "stdlib", - name: "Date.prototype.toISOString", - fns: ["date.toISOString", "date.toISOStringValue"], - note: "UTC ISO formatting over constructed and stored Date values", - }, - { - id: "stdlib.date.toUTCString", - kind: "stdlib", - name: "Date.prototype.toUTCString", - fns: ["date.toUTCString"], - note: "RFC 7231 IMF-fixdate formatting over stored Date values", - }, - { - id: "stdlib.date.getFullYear", - kind: "stdlib", - name: "Date.prototype.getFullYear", - fns: ["date.getFullYear"], - }, - { - id: "stdlib.date.getUTCFullYear", - kind: "stdlib", - name: "Date.prototype.getUTCFullYear", - fns: ["date.getUTCFullYear"], - }, - { - id: "stdlib.date.getMonth", - kind: "stdlib", - name: "Date.prototype.getMonth", - fns: ["date.getMonth"], - }, - { - id: "stdlib.date.getUTCMonth", - kind: "stdlib", - name: "Date.prototype.getUTCMonth", - fns: ["date.getUTCMonth"], - }, - { - id: "stdlib.date.getDate", - kind: "stdlib", - name: "Date.prototype.getDate", - fns: ["date.getDate"], - }, - { - id: "stdlib.date.getUTCDate", - kind: "stdlib", - name: "Date.prototype.getUTCDate", - fns: ["date.getUTCDate"], - }, - { id: "stdlib.date.getDay", kind: "stdlib", name: "Date.prototype.getDay", fns: ["date.getDay"] }, - { - id: "stdlib.date.getUTCDay", - kind: "stdlib", - name: "Date.prototype.getUTCDay", - fns: ["date.getUTCDay"], - }, - { - id: "stdlib.date.getHours", - kind: "stdlib", - name: "Date.prototype.getHours", - fns: ["date.getHours"], - }, - { - id: "stdlib.date.getUTCHours", - kind: "stdlib", - name: "Date.prototype.getUTCHours", - fns: ["date.getUTCHours"], - }, - { - id: "stdlib.date.getMinutes", - kind: "stdlib", - name: "Date.prototype.getMinutes", - fns: ["date.getMinutes"], - }, - { - id: "stdlib.date.getUTCMinutes", - kind: "stdlib", - name: "Date.prototype.getUTCMinutes", - fns: ["date.getUTCMinutes"], - }, - { - id: "stdlib.date.getSeconds", - kind: "stdlib", - name: "Date.prototype.getSeconds", - fns: ["date.getSeconds"], - }, - { - id: "stdlib.date.getUTCSeconds", - kind: "stdlib", - name: "Date.prototype.getUTCSeconds", - fns: ["date.getUTCSeconds"], - }, - { - id: "stdlib.date.getMilliseconds", - kind: "stdlib", - name: "Date.prototype.getMilliseconds", - fns: ["date.getMilliseconds"], - }, - { - id: "stdlib.date.getUTCMilliseconds", - kind: "stdlib", - name: "Date.prototype.getUTCMilliseconds", - fns: ["date.getUTCMilliseconds"], - }, - { - id: "stdlib.date.getTimezoneOffset", - kind: "stdlib", - name: "Date.prototype.getTimezoneOffset", - fns: ["date.getTimezoneOffset"], - }, - { - id: "stdlib.date.toString", - kind: "stdlib", - name: "Date.prototype.toString", - fns: ["date.toString"], - note: "local time with the host zone's long English name; String(date) and template literals use it", - }, - { - id: "stdlib.date.toDateString", - kind: "stdlib", - name: "Date.prototype.toDateString", - fns: ["date.toDateString"], - }, - { - id: "stdlib.date.toTimeString", - kind: "stdlib", - name: "Date.prototype.toTimeString", - fns: ["date.toTimeString"], - }, - // ── perf_hooks (lowerPerfHooksCall): the monotonic clock. - { - id: "node-builtin.perf_hooks.performance.now", - kind: "node-builtin", - name: "perf_hooks.performance.now", - fns: ["perf.now"], - note: "the global performance object and the performance.now.bind(performance) function value reach the same clock", - }, - { - id: "node-builtin.perf_hooks.performance.mark", - kind: "node-builtin", - name: "perf_hooks.performance.mark", - fns: [ - "perf.mark", - "perf.entryName", - "perf.entryType", - "perf.entryStart", - "perf.entryDuration", - "perf.entryDetail", - "perf.entryKeep", - "perf.entryRef", - ], - note: "records a mark in the thread's performance timeline at the live clock or the given startTime", - }, - { - id: "node-builtin.perf_hooks.performance.measure", - kind: "node-builtin", - name: "perf_hooks.performance.measure", - fns: ["perf.measure"], - note: "the mark-name, timestamp, and options forms are one surface", - }, - { - id: "node-builtin.perf_hooks.performance.getEntries", - kind: "node-builtin", - name: "perf_hooks.performance.getEntries", - fns: ["perf.entries"], - note: "getEntries, getEntriesByName, and getEntriesByType read the same timeline", - }, - { - id: "node-builtin.perf_hooks.performance.clearMarks", - kind: "node-builtin", - name: "perf_hooks.performance.clearMarks", - fns: ["perf.clearMarks"], - }, - { - id: "node-builtin.perf_hooks.performance.clearMeasures", - kind: "node-builtin", - name: "perf_hooks.performance.clearMeasures", - fns: ["perf.clearMeasures"], - }, - { - id: "node-builtin.perf_hooks.performance.eventLoopUtilization", - kind: "node-builtin", - name: "perf_hooks.performance.eventLoopUtilization", - fns: ["perf.loopStart", "perf.idleTime"], - note: "active time is the main thread's CPU time since the loop started; the one- and two-argument difference forms are supported", - }, - { - id: "node-builtin.perf_hooks.monitorEventLoopDelay", - kind: "node-builtin", - name: "perf_hooks.monitorEventLoopDelay", - fns: [ - "perf.eldNew", - "perf.eldEnable", - "perf.eldDisable", - "perf.eldReset", - "perf.eldPercentile", - "perf.eldStat", - ], - note: "an unref'd sampling interval records the delay histogram; enable, disable, reset, percentile and the min, max, mean, stddev, count and exceeds readings are supported", - }, - // ── the process global's ambient reads and authority calls - // (lowerProcessProperty/lowerProcessMethodCall — process is a - // provenance-checked global here, not an importable module). - { - id: "node-builtin.process.env", - kind: "node-builtin", - name: "process.env", - fns: ["process.envGet", "process.envSet", "process.envUnset", "process.envPairs"], - note: "reads, writes, deletes, and enumeration of the process environment (the process global)", - }, - { - id: "node-builtin.process.argv", - kind: "node-builtin", - name: "process.argv", - fns: ["process.argv"], - }, - { - id: "node-builtin.process.hrtime", - kind: "node-builtin", - name: "process.hrtime", - fns: ["process.hrtimeValue"], - note: "native monotonic tuple clock and bigint member; direct calls and stored JavaScript callable values", - }, - { - id: "node-builtin.process.getBuiltinModule", - kind: "node-builtin", - name: "process.getBuiltinModule", - fns: ["process.builtinId", "process.builtinModule", "process.builtinUnsupported"], - note: "native path and os export subsets plus current-thread worker_threads metadata; other modules and exports throw SC2020", - }, - { - id: "node-builtin.process.versions", - kind: "node-builtin", - name: "process.versions", - fns: ["process.versions"], - note: "shared version dictionary containing node and openssl; other components are absent unless defined by the program", - }, - { - id: "node-builtin.process.release", - kind: "node-builtin", - name: "process.release", - fns: ["process.release"], - note: "the compatibility target's release metadata as one stable read-only object", - }, - { - id: "node-builtin.process.cwd", - kind: "node-builtin", - name: "process.cwd", - fns: ["process.cwd"], - }, - { - id: "node-builtin.process.chdir", - kind: "node-builtin", - name: "process.chdir", - fns: ["process.chdir"], - }, - { - id: "node-builtin.process.pid", - kind: "node-builtin", - name: "process.pid", - fns: ["process.pid"], - }, - { - id: "node-builtin.process.getuid", - kind: "node-builtin", - name: "process.getuid", - fns: ["process.getuid"], - }, - { - id: "node-builtin.process.getgid", - kind: "node-builtin", - name: "process.getgid", - fns: ["process.getgid"], - }, - { - id: "node-builtin.process.geteuid", - kind: "node-builtin", - name: "process.geteuid", - fns: ["process.geteuid"], - }, - { - id: "node-builtin.process.getegid", - kind: "node-builtin", - name: "process.getegid", - fns: ["process.getegid"], - }, - { - id: "node-builtin.process.getgroups", - kind: "node-builtin", - name: "process.getgroups", - fns: ["process.getgroups"], - }, - { - id: "node-builtin.process.ppid", - kind: "node-builtin", - name: "process.ppid", - fns: ["process.ppid"], - }, - { - id: "node-builtin.process.title", - kind: "node-builtin", - name: "process.title", - fns: ["process.title", "process.setTitle"], - note: "reads and writes of the process title are one surface", - }, - { - id: "node-builtin.process.argv0", - kind: "node-builtin", - name: "process.argv0", - fns: ["process.argv0"], - }, - { - id: "node-builtin.process.execArgv", - kind: "node-builtin", - name: "process.execArgv", - fns: ["process.execArgv"], - }, - { - id: "node-builtin.process.execPath", - kind: "node-builtin", - name: "process.execPath", - fns: ["process.execPath"], - }, - { - id: "node-builtin.process.uptime", - kind: "node-builtin", - name: "process.uptime", - fns: ["process.uptime"], - }, - { - id: "node-builtin.process.availableMemory", - kind: "node-builtin", - name: "process.availableMemory", - fns: ["process.availableMemory"], - }, - { - id: "node-builtin.process.constrainedMemory", - kind: "node-builtin", - name: "process.constrainedMemory", - fns: ["process.constrainedMemory"], - }, - { - id: "node-builtin.process.memoryUsage", - kind: "node-builtin", - name: "process.memoryUsage", - fns: ["process.memoryUsageTake", "process.memoryUsageField", "process.rss"], - note: "the record and memoryUsage.rss() read live memory state; external and arrayBuffers are 0", - }, - { - id: "node-builtin.process.resourceUsage", - kind: "node-builtin", - name: "process.resourceUsage", - fns: ["process.rusage"], - note: "getrusage's 16 fields — every field read samples live machine state", - }, - { - id: "node-builtin.process.cpuUsage", - kind: "node-builtin", - name: "process.cpuUsage", - fns: [ - "process.cpuUser", - "process.cpuSystem", - "process.cpuUserDiff", - "process.cpuSystemDiff", - "process.cpuPrevValidate", - ], - note: "the plain-sample and previous-value diff forms are one surface", - }, - { - id: "node-builtin.process.threadCpuUsage", - kind: "node-builtin", - name: "process.threadCpuUsage", - fns: [ - "process.threadCpuUser", - "process.threadCpuSystem", - "process.threadCpuUserDiff", - "process.threadCpuSystemDiff", - ], - note: "the plain-sample and previous-value diff forms are one surface", - }, - { - id: "node-builtin.process.isTTY", - kind: "node-builtin", - name: "process.isTTY", - fns: ["process.isTTY"], - note: "the isTTY read on process.stdin/stdout/stderr — one surface across the three streams", - }, - { - id: "node-builtin.process.columns", - kind: "node-builtin", - name: "process.columns", - fns: ["process.columns"], - note: "the columns read on the process stdio streams (terminal geometry)", - }, - { - id: "node-builtin.process.rows", - kind: "node-builtin", - name: "process.rows", - fns: ["process.rows"], - note: "the rows read on process.stdout/stderr (terminal geometry)", - }, - { - id: "node-builtin.process.kill", - kind: "node-builtin", - name: "process.kill", - fns: ["process.kill", "process.killNum"], - note: "the signal-name and signal-number forms are one surface", - }, - { - id: "node-builtin.process.umask", - kind: "node-builtin", - name: "process.umask", - fns: ["process.umask"], - }, - { - id: "node-builtin.process.exit", - kind: "node-builtin", - name: "process.exit", - fns: ["process.exit", "process.exiting"], - note: "process.exit and the process._exiting flag read are one surface", - }, - { - id: "node-builtin.process.exitCode", - kind: "node-builtin", - name: "process.exitCode", - fns: ["process.setExitCode", "process.setExitCodeTracked", "process.hasExitCode"], - note: "numeric writes in statement position set the implicit exit status; reads answer the assigned code or undefined; process.exit() reads it", - }, - // ── the tls CA store (lowerTlsCaCall / lowerTlsRootCertificates): the - // host's trust anchors, read and replaced. Dedicated paths, and - // rootCertificates is a VALUE read with no call form at all, so none of - // the three can hang off a lowering-table row. Split three ways rather - // than folded into one "CA store" surface: a library author who denies - // REPLACING the trust anchors is making a different decision from one - // who denies reading them, and each Node spelling is the name they will - // reach for when writing the fence. - { - id: "node-builtin.tls.getCACertificates", - kind: "node-builtin", - name: "tls.getCACertificates", - fns: ["tlsca.get"], - note: "the per-type cached PEM bundle: 'default' and 'extra' additionally read NODE_EXTRA_CA_CERTS, 'system' the platform store", - }, - { - id: "node-builtin.tls.rootCertificates", - kind: "node-builtin", - name: "tls.rootCertificates", - fns: ["tlsca.root"], - note: "the value read; answers the same bundled array as getCACertificates('bundled'), but fenced under its own id — the spelling an author writes", - }, - { - id: "node-builtin.tls.setDefaultCACertificates", - kind: "node-builtin", - name: "tls.setDefaultCACertificates", - fns: ["tlsca.set"], - note: "replaces the default set and the client trust anchors for the rest of the process", - }, - // os.constants is a VALUE read (lowerNamespaceBuiltinProperty and the - // named-import path): the platform's null-prototype constants object. - { - id: "node-builtin.os.constants", - kind: "node-builtin", - name: "os.constants", - fns: ["os.constants"], - note: "the platform's dlopen, errno, signals and priority groups; members a platform lacks read as undefined", - }, - // fs.constants as a value, and its members beyond the baked access-mode - // bits: the target libc's null-prototype constants object. - { - id: "node-builtin.fs.constants", - kind: "node-builtin", - name: "fs.constants", - fns: ["fs.constants"], - note: "node_constants.cc's fs group under the target libc's macros; members a platform lacks read as undefined", - }, -]; +let ambientSurfaceRows: readonly AmbientSurfaceRow[] | undefined; + +/** The ambient surface rows, built on the first call. */ +export function ambientSurfaceFns(): readonly AmbientSurfaceRow[] { + ambientSurfaceRows ??= buildAmbientSurfaceFns(); + return ambientSurfaceRows; +} + +function buildAmbientSurfaceFns(): readonly AmbientSurfaceRow[] { + return [ + { + id: "node-builtin.worker_threads.Worker", + kind: "node-builtin", + name: "worker_threads.Worker", + fns: ["worker.new"], + note: "native threads with statically compiled entry points, workerData, argv and message events", + }, + { + id: "node-builtin.worker_threads.metadata", + kind: "node-builtin", + name: "worker_threads metadata", + fns: [ + "worker.isMainThread", + "worker.threadId", + "worker.data", + "worker.parentPort", + "worker.resourceLimits", + "worker.threadName", + ], + note: "current-thread metadata and the worker's parent message port", + }, + { + id: "stdlib.sharedArrayBuffer", + kind: "stdlib", + name: "SharedArrayBuffer", + fns: ["sharedArrayBuffer.new"], + note: "fixed shared storage with context-local typed-array and DataView wrappers", + }, + { + id: "stdlib.blob", + kind: "stdlib", + name: "Blob", + fns: ["blob.new", "blob.slice", "blob.text", "blob.bytes", "blob.arrayBuffer", "blob.is"], + note: "immutable bytes with size, type, slice(), text(), bytes(), arrayBuffer() and structured cloning", + }, + { + id: "stdlib.file", + kind: "stdlib", + name: "File", + fns: ["blob.fileNew"], + note: "a Blob with name and lastModified (Date.now() when absent)", + }, + { + id: "stdlib.webcrypto.subtle", + kind: "stdlib", + name: "globalThis.crypto.subtle", + fns: [ + "webcrypto.generateKey", + "webcrypto.importKey", + "webcrypto.exportKey", + "webcrypto.encrypt", + "webcrypto.decrypt", + "webcrypto.wrapKey", + "webcrypto.unwrapKey", + "webcrypto.deriveBits", + "webcrypto.deriveKey", + "webcrypto.sign", + "webcrypto.verify", + "webcrypto.digest", + ], + note: "importKey, generateKey and exportKey (raw and jwk) for HMAC and AES keys, importKey for PBKDF2 and HKDF keys, AES-GCM, AES-CBC and AES-CTR encrypt and decrypt, wrapKey and unwrapKey of secret keys with AES-KW or those ciphers, PBKDF2 and HKDF deriveBits and deriveKey, HMAC sign and verify, generateKey, importKey, exportKey, sign and verify for ECDSA, Ed25519, RSASSA-PKCS1-v1_5 and RSA-PSS, and SHA-1/SHA-2 digest; other methods, algorithms and key formats are refused", + }, + { + id: "stdlib.webcrypto.CryptoKey", + kind: "stdlib", + name: "CryptoKey", + fns: ["webcrypto.isCryptoKey"], + note: "secret HMAC, AES, PBKDF2 and HKDF keys with type, extractable, algorithm and usages, and instanceof CryptoKey", + }, + { + id: "stdlib.atomics", + kind: "stdlib", + name: "Atomics", + fns: ["atomics.op", "atomics.wait", "atomics.notify"], + note: "8-, 16- and 32-bit integer operations, plus Int32Array wait and notify", + }, + { + id: "node-builtin.fs.callbacks", + kind: "node-builtin", + name: "fs error-first callbacks", + fns: ["fs.callbackValue", "fs.callbackCall", "fs.callbackGlob"], + note: "native filesystem callbacks, including stored platform adapter functions", + }, + // ── the Date slice (lowerDateCall/lowerNew): statics, construction, + // stored read-only values, calendar getters, and ISO formatting. + { + id: "stdlib.date.now", + kind: "stdlib", + name: "Date.now", + fns: ["date.now"], + note: "the live clock", + }, + { + id: "stdlib.date.parse", + kind: "stdlib", + name: "Date.parse", + fns: ["date.parse"], + note: "one date string, using V8's date parser shared with new Date(dateString)", + }, + { + id: "stdlib.date.constructor", + kind: "stdlib", + name: "Date constructor", + fns: [ + "date.newNow", + "date.newMs", + "date.newString", + "date.nativeNew", + "date.newLocal", + "date.nativeNewFields", + "date.nativeNewValue", + "date.argumentsTime", + "date.classOperation", + ], + note: "zero arguments, one milliseconds/date-string argument, or local-time year/month fields; values are the read-only TimeClip scalar slice", + }, + { + id: "stdlib.date.setters", + kind: "stdlib", + name: "Date.prototype setters", + fns: ["date.setFields", "date.timeClip", "date.store"], + note: "setTime and the local and UTC field setters, with their optional arguments", + }, + { + id: "stdlib.date.UTC", + kind: "stdlib", + name: "Date.UTC", + fns: ["date.utc"], + note: "the lowered call form takes 1 to 7 number arguments", + }, + { + id: "stdlib.date.getTime", + kind: "stdlib", + name: "Date.prototype.getTime", + fns: ["date.getTime", "date.parseGetTime"], + note: "the millisecond value of a stored Date", + }, + { + id: "stdlib.date.valueOf", + kind: "stdlib", + name: "Date.prototype.valueOf", + fns: ["date.valueOf"], + note: "the same millisecond read as getTime()", + }, + { + id: "stdlib.date.toISOString", + kind: "stdlib", + name: "Date.prototype.toISOString", + fns: ["date.toISOString", "date.toISOStringValue"], + note: "UTC ISO formatting over constructed and stored Date values", + }, + { + id: "stdlib.date.toUTCString", + kind: "stdlib", + name: "Date.prototype.toUTCString", + fns: ["date.toUTCString"], + note: "RFC 7231 IMF-fixdate formatting over stored Date values", + }, + { + id: "stdlib.date.getFullYear", + kind: "stdlib", + name: "Date.prototype.getFullYear", + fns: ["date.getFullYear"], + }, + { + id: "stdlib.date.getUTCFullYear", + kind: "stdlib", + name: "Date.prototype.getUTCFullYear", + fns: ["date.getUTCFullYear"], + }, + { + id: "stdlib.date.getMonth", + kind: "stdlib", + name: "Date.prototype.getMonth", + fns: ["date.getMonth"], + }, + { + id: "stdlib.date.getUTCMonth", + kind: "stdlib", + name: "Date.prototype.getUTCMonth", + fns: ["date.getUTCMonth"], + }, + { + id: "stdlib.date.getDate", + kind: "stdlib", + name: "Date.prototype.getDate", + fns: ["date.getDate"], + }, + { + id: "stdlib.date.getUTCDate", + kind: "stdlib", + name: "Date.prototype.getUTCDate", + fns: ["date.getUTCDate"], + }, + { + id: "stdlib.date.getDay", + kind: "stdlib", + name: "Date.prototype.getDay", + fns: ["date.getDay"], + }, + { + id: "stdlib.date.getUTCDay", + kind: "stdlib", + name: "Date.prototype.getUTCDay", + fns: ["date.getUTCDay"], + }, + { + id: "stdlib.date.getHours", + kind: "stdlib", + name: "Date.prototype.getHours", + fns: ["date.getHours"], + }, + { + id: "stdlib.date.getUTCHours", + kind: "stdlib", + name: "Date.prototype.getUTCHours", + fns: ["date.getUTCHours"], + }, + { + id: "stdlib.date.getMinutes", + kind: "stdlib", + name: "Date.prototype.getMinutes", + fns: ["date.getMinutes"], + }, + { + id: "stdlib.date.getUTCMinutes", + kind: "stdlib", + name: "Date.prototype.getUTCMinutes", + fns: ["date.getUTCMinutes"], + }, + { + id: "stdlib.date.getSeconds", + kind: "stdlib", + name: "Date.prototype.getSeconds", + fns: ["date.getSeconds"], + }, + { + id: "stdlib.date.getUTCSeconds", + kind: "stdlib", + name: "Date.prototype.getUTCSeconds", + fns: ["date.getUTCSeconds"], + }, + { + id: "stdlib.date.getMilliseconds", + kind: "stdlib", + name: "Date.prototype.getMilliseconds", + fns: ["date.getMilliseconds"], + }, + { + id: "stdlib.date.getUTCMilliseconds", + kind: "stdlib", + name: "Date.prototype.getUTCMilliseconds", + fns: ["date.getUTCMilliseconds"], + }, + { + id: "stdlib.date.getTimezoneOffset", + kind: "stdlib", + name: "Date.prototype.getTimezoneOffset", + fns: ["date.getTimezoneOffset"], + }, + { + id: "stdlib.date.toString", + kind: "stdlib", + name: "Date.prototype.toString", + fns: ["date.toString"], + note: "local time with the host zone's long English name; String(date) and template literals use it", + }, + { + id: "stdlib.date.toDateString", + kind: "stdlib", + name: "Date.prototype.toDateString", + fns: ["date.toDateString"], + }, + { + id: "stdlib.date.toTimeString", + kind: "stdlib", + name: "Date.prototype.toTimeString", + fns: ["date.toTimeString"], + }, + // ── perf_hooks (lowerPerfHooksCall): the monotonic clock. + { + id: "node-builtin.perf_hooks.performance.now", + kind: "node-builtin", + name: "perf_hooks.performance.now", + fns: ["perf.now"], + note: "the global performance object and the performance.now.bind(performance) function value reach the same clock", + }, + { + id: "node-builtin.perf_hooks.performance.mark", + kind: "node-builtin", + name: "perf_hooks.performance.mark", + fns: [ + "perf.mark", + "perf.entryName", + "perf.entryType", + "perf.entryStart", + "perf.entryDuration", + "perf.entryDetail", + "perf.entryKeep", + "perf.entryRef", + ], + note: "records a mark in the thread's performance timeline at the live clock or the given startTime", + }, + { + id: "node-builtin.perf_hooks.performance.measure", + kind: "node-builtin", + name: "perf_hooks.performance.measure", + fns: ["perf.measure"], + note: "the mark-name, timestamp, and options forms are one surface", + }, + { + id: "node-builtin.perf_hooks.performance.getEntries", + kind: "node-builtin", + name: "perf_hooks.performance.getEntries", + fns: ["perf.entries"], + note: "getEntries, getEntriesByName, and getEntriesByType read the same timeline", + }, + { + id: "node-builtin.perf_hooks.performance.clearMarks", + kind: "node-builtin", + name: "perf_hooks.performance.clearMarks", + fns: ["perf.clearMarks"], + }, + { + id: "node-builtin.perf_hooks.performance.clearMeasures", + kind: "node-builtin", + name: "perf_hooks.performance.clearMeasures", + fns: ["perf.clearMeasures"], + }, + { + id: "node-builtin.perf_hooks.performance.eventLoopUtilization", + kind: "node-builtin", + name: "perf_hooks.performance.eventLoopUtilization", + fns: ["perf.loopStart", "perf.idleTime"], + note: "active time is the main thread's CPU time since the loop started; the one- and two-argument difference forms are supported", + }, + { + id: "node-builtin.perf_hooks.monitorEventLoopDelay", + kind: "node-builtin", + name: "perf_hooks.monitorEventLoopDelay", + fns: [ + "perf.eldNew", + "perf.eldEnable", + "perf.eldDisable", + "perf.eldReset", + "perf.eldPercentile", + "perf.eldStat", + ], + note: "an unref'd sampling interval records the delay histogram; enable, disable, reset, percentile and the min, max, mean, stddev, count and exceeds readings are supported", + }, + // ── the process global's ambient reads and authority calls + // (lowerProcessProperty/lowerProcessMethodCall — process is a + // provenance-checked global here, not an importable module). + { + id: "node-builtin.process.env", + kind: "node-builtin", + name: "process.env", + fns: ["process.envGet", "process.envSet", "process.envUnset", "process.envPairs"], + note: "reads, writes, deletes, and enumeration of the process environment (the process global)", + }, + { + id: "node-builtin.process.argv", + kind: "node-builtin", + name: "process.argv", + fns: ["process.argv"], + }, + { + id: "node-builtin.process.hrtime", + kind: "node-builtin", + name: "process.hrtime", + fns: ["process.hrtimeValue"], + note: "native monotonic tuple clock and bigint member; direct calls and stored JavaScript callable values", + }, + { + id: "node-builtin.process.getBuiltinModule", + kind: "node-builtin", + name: "process.getBuiltinModule", + fns: ["process.builtinId", "process.builtinModule", "process.builtinUnsupported"], + note: "native path and os export subsets plus current-thread worker_threads metadata; other modules and exports throw SC2020", + }, + { + id: "node-builtin.process.versions", + kind: "node-builtin", + name: "process.versions", + fns: ["process.versions"], + note: "shared version dictionary containing node and openssl; other components are absent unless defined by the program", + }, + { + id: "node-builtin.process.release", + kind: "node-builtin", + name: "process.release", + fns: ["process.release"], + note: "the compatibility target's release metadata as one stable read-only object", + }, + { + id: "node-builtin.process.cwd", + kind: "node-builtin", + name: "process.cwd", + fns: ["process.cwd"], + }, + { + id: "node-builtin.process.chdir", + kind: "node-builtin", + name: "process.chdir", + fns: ["process.chdir"], + }, + { + id: "node-builtin.process.pid", + kind: "node-builtin", + name: "process.pid", + fns: ["process.pid"], + }, + { + id: "node-builtin.process.getuid", + kind: "node-builtin", + name: "process.getuid", + fns: ["process.getuid"], + }, + { + id: "node-builtin.process.getgid", + kind: "node-builtin", + name: "process.getgid", + fns: ["process.getgid"], + }, + { + id: "node-builtin.process.geteuid", + kind: "node-builtin", + name: "process.geteuid", + fns: ["process.geteuid"], + }, + { + id: "node-builtin.process.getegid", + kind: "node-builtin", + name: "process.getegid", + fns: ["process.getegid"], + }, + { + id: "node-builtin.process.getgroups", + kind: "node-builtin", + name: "process.getgroups", + fns: ["process.getgroups"], + }, + { + id: "node-builtin.process.ppid", + kind: "node-builtin", + name: "process.ppid", + fns: ["process.ppid"], + }, + { + id: "node-builtin.process.title", + kind: "node-builtin", + name: "process.title", + fns: ["process.title", "process.setTitle"], + note: "reads and writes of the process title are one surface", + }, + { + id: "node-builtin.process.argv0", + kind: "node-builtin", + name: "process.argv0", + fns: ["process.argv0"], + }, + { + id: "node-builtin.process.execArgv", + kind: "node-builtin", + name: "process.execArgv", + fns: ["process.execArgv"], + }, + { + id: "node-builtin.process.execPath", + kind: "node-builtin", + name: "process.execPath", + fns: ["process.execPath"], + }, + { + id: "node-builtin.process.uptime", + kind: "node-builtin", + name: "process.uptime", + fns: ["process.uptime"], + }, + { + id: "node-builtin.process.availableMemory", + kind: "node-builtin", + name: "process.availableMemory", + fns: ["process.availableMemory"], + }, + { + id: "node-builtin.process.constrainedMemory", + kind: "node-builtin", + name: "process.constrainedMemory", + fns: ["process.constrainedMemory"], + }, + { + id: "node-builtin.process.memoryUsage", + kind: "node-builtin", + name: "process.memoryUsage", + fns: ["process.memoryUsageTake", "process.memoryUsageField", "process.rss"], + note: "the record and memoryUsage.rss() read live memory state; external and arrayBuffers are 0", + }, + { + id: "node-builtin.process.resourceUsage", + kind: "node-builtin", + name: "process.resourceUsage", + fns: ["process.rusage"], + note: "getrusage's 16 fields — every field read samples live machine state", + }, + { + id: "node-builtin.process.cpuUsage", + kind: "node-builtin", + name: "process.cpuUsage", + fns: [ + "process.cpuUser", + "process.cpuSystem", + "process.cpuUserDiff", + "process.cpuSystemDiff", + "process.cpuPrevValidate", + ], + note: "the plain-sample and previous-value diff forms are one surface", + }, + { + id: "node-builtin.process.threadCpuUsage", + kind: "node-builtin", + name: "process.threadCpuUsage", + fns: [ + "process.threadCpuUser", + "process.threadCpuSystem", + "process.threadCpuUserDiff", + "process.threadCpuSystemDiff", + ], + note: "the plain-sample and previous-value diff forms are one surface", + }, + { + id: "node-builtin.process.isTTY", + kind: "node-builtin", + name: "process.isTTY", + fns: ["process.isTTY"], + note: "the isTTY read on process.stdin/stdout/stderr — one surface across the three streams", + }, + { + id: "node-builtin.process.columns", + kind: "node-builtin", + name: "process.columns", + fns: ["process.columns"], + note: "the columns read on the process stdio streams (terminal geometry)", + }, + { + id: "node-builtin.process.rows", + kind: "node-builtin", + name: "process.rows", + fns: ["process.rows"], + note: "the rows read on process.stdout/stderr (terminal geometry)", + }, + { + id: "node-builtin.process.kill", + kind: "node-builtin", + name: "process.kill", + fns: ["process.kill", "process.killNum"], + note: "the signal-name and signal-number forms are one surface", + }, + { + id: "node-builtin.process.umask", + kind: "node-builtin", + name: "process.umask", + fns: ["process.umask"], + }, + { + id: "node-builtin.process.exit", + kind: "node-builtin", + name: "process.exit", + fns: ["process.exit", "process.exiting"], + note: "process.exit and the process._exiting flag read are one surface", + }, + { + id: "node-builtin.process.exitCode", + kind: "node-builtin", + name: "process.exitCode", + fns: ["process.setExitCode", "process.setExitCodeTracked", "process.hasExitCode"], + note: "numeric writes in statement position set the implicit exit status; reads answer the assigned code or undefined; process.exit() reads it", + }, + // ── the tls CA store (lowerTlsCaCall / lowerTlsRootCertificates): the + // host's trust anchors, read and replaced. Dedicated paths, and + // rootCertificates is a VALUE read with no call form at all, so none of + // the three can hang off a lowering-table row. Split three ways rather + // than folded into one "CA store" surface: a library author who denies + // REPLACING the trust anchors is making a different decision from one + // who denies reading them, and each Node spelling is the name they will + // reach for when writing the fence. + { + id: "node-builtin.tls.getCACertificates", + kind: "node-builtin", + name: "tls.getCACertificates", + fns: ["tlsca.get"], + note: "the per-type cached PEM bundle: 'default' and 'extra' additionally read NODE_EXTRA_CA_CERTS, 'system' the platform store", + }, + { + id: "node-builtin.tls.rootCertificates", + kind: "node-builtin", + name: "tls.rootCertificates", + fns: ["tlsca.root"], + note: "the value read; answers the same bundled array as getCACertificates('bundled'), but fenced under its own id — the spelling an author writes", + }, + { + id: "node-builtin.tls.setDefaultCACertificates", + kind: "node-builtin", + name: "tls.setDefaultCACertificates", + fns: ["tlsca.set"], + note: "replaces the default set and the client trust anchors for the rest of the process", + }, + // os.constants is a VALUE read (lowerNamespaceBuiltinProperty and the + // named-import path): the platform's null-prototype constants object. + { + id: "node-builtin.os.constants", + kind: "node-builtin", + name: "os.constants", + fns: ["os.constants"], + note: "the platform's dlopen, errno, signals and priority groups; members a platform lacks read as undefined", + }, + // fs.constants as a value, and its members beyond the baked access-mode + // bits: the target libc's null-prototype constants object. + { + id: "node-builtin.fs.constants", + kind: "node-builtin", + name: "fs.constants", + fns: ["fs.constants"], + note: "node_constants.cc's fs group under the target libc's macros; members a platform lacks read as undefined", + }, + ]; +} /** The win32-target overrides of the bare modules' constants: path's * constants ARE the path/win32 namespace's, and os.EOL is CRLF (Node's @@ -2815,7 +2962,7 @@ const WIN32_TARGET_CONSTS: Record< os: { EOL: "\r\n", devNull: "\\\\.\\nul" }, }; -/** BUILTIN_MODULE_FNS with the platform-conditional modules bound per +/** builtinModuleFns() with the platform-conditional modules bound per * TARGET: a win32 triple compiles Node-on-Windows semantics — `path` is * path.win32 and url's bridge selects the Windows runtime behavior. Fence wording is * unaffected — callers keep naming the module the source spelled. */ @@ -2824,9 +2971,9 @@ function builtinModuleFnsOf( module: string, ): Record | undefined { if (lowerer.targetPlatform === "win32") { - if (module === "path") return BUILTIN_MODULE_FNS["path/win32"]; + if (module === "path") return builtinModuleFns()["path/win32"]; } - return ownEntry(BUILTIN_MODULE_FNS, module); + return ownEntry(builtinModuleFns(), module); } /** Own-property lookup — lowerer.ts's own(), duplicated locally because @@ -3033,17 +3180,6 @@ export function builtinModulesArrayLit(loc: { file: string; start: number; end: }; } -/** Member-specific hints for RECOGNIZED builtin modules whose member has - * no lowering. The default-options one-shot zlib/deflate-raw/gzip codecs - * lower for strings and Buffers; the remaining surface points at them. */ -const ZLIB_HINT = - "the deflate/inflate, raw, gzip/gunzip, and unzip sync/callback forms with Node's options, crc32, " + - "and the Deflate/Inflate/Gzip/Gunzip/DeflateRaw/InflateRaw/Unzip streams are the lowered zlib surface"; -/** Brotli has no codec in the static runtime (zlib is the only vendored - * compression library). */ -const BROTLI_HINT = - "Brotli needs a Brotli codec, which the static runtime does not vendor — " + ZLIB_HINT; - const ASSERT_MODULE_HINTS: Record = { ifError: "test explicitly instead: assert.strictEqual(err, null) / " + @@ -3101,23 +3237,10 @@ export const BUILTIN_MODULE_FENCE_HINTS: Record< "createSign, createVerify, createECDH, and createDiffieHellman", ]), ), - diffieHellman: - "crypto.diffieHellman takes DH, EC or X25519 KeyObjects and has no lowering; " + - "the objects of createDiffieHellman, getDiffieHellman and createECDH compute shared secrets", setFips: "a compiled binary has no FIPS provider to enable, and Node itself throws on setFips(true) " + "in a non-FIPS build — getFips() answers 0 here", }, - zlib: { - brotliCompressSync: BROTLI_HINT, - brotliDecompressSync: BROTLI_HINT, - brotliCompress: BROTLI_HINT, - brotliDecompress: BROTLI_HINT, - createBrotliCompress: BROTLI_HINT, - createBrotliDecompress: BROTLI_HINT, - BrotliCompress: BROTLI_HINT, - BrotliDecompress: BROTLI_HINT, - }, http2: { connect: "HTTP/2 client sessions have no lowering — the lowered http2 surface is the SERVER side: " + @@ -3440,6 +3563,20 @@ export function stdlibGlobalNameOf(lowerer: Lowerer, expr: ts.Expression): strin return ts.symbolName(resolved) === "global" ? "globalThis" : ts.symbolName(resolved); } if (ts.isPropertyAccessExpression(expr) && !expr.questionDotToken) { + // The buffer module's Buffer (`require("buffer").Buffer`, `buffer_1.Buffer`) + // is the global constructor. + if (bufferNamespaceMember(lowerer, expr)) return "Buffer"; + // A cast of the global object (`(globalThis as { Buffer?: B }).Buffer`, + // `(globalThis as any).Buffer`) changes only its static type: the + // member is the global object's own. + const uncast = withoutTypeCasts(expr.expression); + if (uncast !== unparenthesized(expr.expression)) { + if (!ts.isIdentifier(uncast) || stdlibGlobalNameOf(lowerer, uncast) !== "globalThis") + return null; + const member = lowerer.checker.getPropertyOfType(lowerer.typeOf(uncast), expr.name.text); + if (!member || !lowerer.isStdlibSymbol(member)) return null; + return ts.symbolName(member) === "global" ? "globalThis" : ts.symbolName(member); + } if (stdlibGlobalNameOf(lowerer, expr.expression) !== "globalThis") return null; // Through a detection chain the checker types the receiver as the // union of every arm; the selected arm (the global object) owns the @@ -3572,9 +3709,28 @@ export function stdlibGlobalAliasDecl( const symbol = lowerer.checker.getSymbolAtLocation(nameNode); if (!symbol) return false; lowerer.stdlibGlobalAliases.set(symbol, name); + if (init !== undefined && castGlobalAliasSource(lowerer, init)) + lowerer.castGlobalAliases.add(symbol); return true; } +/** True when an alias initializer reads its global through a cast of the + * global object (`(globalThis as { Buffer?: B }).Buffer`), directly or as + * the left operand of `||` (stdlibGlobalAliasNameOf's guard), or copies an + * alias made that way. Such an alias carries the cast's static type. */ +function castGlobalAliasSource(lowerer: Lowerer, init: ts.Expression): boolean { + let source = unparenthesized(init); + if (ts.isBinaryExpression(source) && source.operatorToken.kind === ts.SyntaxKind.BarBarToken) + source = unparenthesized(source.left); + if (ts.isPropertyAccessExpression(source)) + return withoutTypeCasts(source.expression) !== unparenthesized(source.expression); + if (ts.isIdentifier(source)) { + const symbol = lowerer.checker.getSymbolAtLocation(source); + return symbol !== undefined && lowerer.castGlobalAliases.has(symbol); + } + return false; +} + /** True iff the symbol is declared ONLY by the adopted @types/node * surface (no es-lib or shipped-ambient declaration merges in) — chooses * the SC2020 fence's wording: "typed by @types/node", not "standard diff --git a/packages/compiler/src/frontend/lowering/symbol-fields.ts b/packages/compiler/src/frontend/lowering/symbol-fields.ts index 56718615e..d017cdc7f 100644 --- a/packages/compiler/src/frontend/lowering/symbol-fields.ts +++ b/packages/compiler/src/frontend/lowering/symbol-fields.ts @@ -76,7 +76,38 @@ export function fenceSymbolFieldCopy( } } +/** The registry name util.inspect.custom denotes. */ +export const INSPECT_CUSTOM_DESCRIPTION = "nodejs.util.inspect.custom"; +/** The method slot of a class's `[util.inspect.custom]` member. */ +export const INSPECT_CUSTOM_MEMBER = `${FIELD_PREFIX}Symbol(${INSPECT_CUSTOM_DESCRIPTION})`; + +/** `inspect.custom` over node:util's inspect: a named import, `util.inspect` + * or the CommonJS `util_1.inspect`. */ +export function isInspectCustomAccess(lowerer: Lowerer, expr: ts.Expression): boolean { + while (ts.isParenthesizedExpression(expr)) expr = expr.expression; + if (!ts.isPropertyAccessExpression(expr) || expr.questionDotToken) return false; + if (!ts.isIdentifier(expr.name) || expr.name.text !== "custom") return false; + let target = expr.expression; + while (ts.isParenthesizedExpression(target)) target = target.expression; + const bi = ts.isIdentifier(target) + ? lowerer.builtinImportOf(target) + : ts.isPropertyAccessExpression(target) && !target.questionDotToken + ? lowerer.builtinMemberOf(target) + : null; + return bi?.module === "util" && bi.member === "inspect"; +} + export function classSymbolKeyOf(lowerer: Lowerer, key: ts.Expression): ClassSymbolKey | null { + // util.inspect.custom is the registered Symbol.for(INSPECT_CUSTOM_DESCRIPTION), + // one identity with the program's own Symbol.for calls of that name. + if (isInspectCustomAccess(lowerer, key)) { + let e = key; + while (ts.isParenthesizedExpression(e)) e = e.expression; + const sym = lowerer.checker.getSymbolAtLocation((e as ts.PropertyAccessExpression).name); + return sym ? symbolKeyOf(lowerer, sym, true, INSPECT_CUSTOM_DESCRIPTION) : null; + } + const wellKnown = wellKnownSymbolKeyOf(lowerer, key); + if (wellKnown !== undefined) return wellKnown; if ( !ts.isIdentifier(key) && (!ts.isPropertyAccessExpression(key) || key.questionDotToken || !ts.isIdentifier(key.name)) @@ -90,6 +121,53 @@ export function classSymbolKeyOf(lowerer: Lowerer, key: ts.Expression): ClassSym return sym ? classSymbolKeyOfSymbol(lowerer, sym) : null; } +/** Well-known symbols a class field may be keyed by: those no protocol + * consults on an instance, or whose protocol the lowering reads from the + * field (Symbol.toStringTag, by the class string conversions). A field + * keyed by any other well-known symbol (Symbol.iterator, toPrimitive, + * hasInstance, ...) would be skipped by its protocol, so it keeps the + * computed-key refusal. */ +const FIELD_WELL_KNOWN_SYMBOLS: ReadonlySet = new Set(["toStringTag"]); + +/** The key of `Symbol.` on the standard global: undefined when the + * expression is not one, null when it is a well-known symbol a field may + * not be keyed by. The identity is the lib's declaration of the property, + * one symbol for every program reference. */ +function wellKnownSymbolKeyOf( + lowerer: Lowerer, + key: ts.Expression, +): ClassSymbolKey | null | undefined { + while (ts.isParenthesizedExpression(key)) key = key.expression; + if ( + !ts.isPropertyAccessExpression(key) || + key.questionDotToken || + !ts.isIdentifier(key.expression) || + !ts.isIdentifier(key.name) || + !lowerer.isStdlibGlobal(key.expression, "Symbol") + ) + return undefined; + const name = key.name.text; + const sym = lowerer.checker.getSymbolAtLocation(key.name); + if (!sym || !(lowerer.typeOf(key).flags & ts.TypeFlags.UniqueESSymbol)) return undefined; + if (!FIELD_WELL_KNOWN_SYMBOLS.has(name)) return null; + return { sym, identity: sym, fieldName: wellKnownSymbolFieldName(name) }; +} + +/** The layout name of a field keyed by the well-known `Symbol.`: a + * reserved symbol-field name, printed as Node spells the key. */ +export function wellKnownSymbolFieldName(name: string): string { + return `${FIELD_PREFIX}Symbol(Symbol.${name})`; +} + +/** The well-known symbol a field name is keyed by (`toStringTag`), or null. */ +export function wellKnownSymbolOfField(field: string): string | null { + const match = /^%symbol:Symbol\(Symbol\.(\w+)\)$/.exec(field); + return match === null ? null : match[1]!; +} + +/** The layout name of a `Symbol.toStringTag` field. */ +export const TO_STRING_TAG_FIELD = wellKnownSymbolFieldName("toStringTag"); + /** A literal Symbol()/Symbol.for() initialized once at module scope. The * whole-file proof also covers var declarations emitted by bundlers. Keys * cannot be used before initialization: a computed class field evaluates @@ -345,9 +423,9 @@ function tablePropertyWritten( } return false; }; - for (const sf of lowerer.program.getSourceFiles()) { + for (const sf of lowerer.program.getImplementationSourceFiles()) { if (written) break; - if (sf.isDeclarationFile || !sf.text.includes(name)) continue; + if (!sf.text.includes(name)) continue; ts.walkPreorder(sf, (node) => { if (written) return "stop"; let target: ts.Node | undefined; diff --git a/packages/compiler/src/frontend/lowering/type-failure.ts b/packages/compiler/src/frontend/lowering/type-failure.ts new file mode 100644 index 000000000..3502a9bbf --- /dev/null +++ b/packages/compiler/src/frontend/lowering/type-failure.ts @@ -0,0 +1,195 @@ +/* Localized failure, stage L0 (report-only). + * + * A type with no static lowering poisons the slot that holds it (a field, + * a parameter, a return, a local), and today the poison rejects the whole + * declaration around the slot: a class, a signature, a declaration + * statement. Every later use of that declaration then fails on its own + * line. The localized-failure design turns such slots into checked + * dynamic slots at the points where the build would otherwise commit the + * error. + * + * This module holds the bookkeeping that stage measures without changing + * any lowering: where a failing type sits (its slot), whether a dynamic + * slot would keep the program's meaning (design §3.7), and the keys that + * tie a cascade diagnostic to the rejected declaration it inherits from. + * The data rides the committed diagnostics as report-only fields + * (ScrDiagnostic.typeFailure, .rejects, .cause); tier-analysis.ts turns it + * into the would-be dynamic slot report of `--explain-dynamic`. */ +import type { + TypeFailure, + TypeFailureFamily, + TypeFailureSlot, +} from "../../diagnostics/diagnostic.js"; +import * as ts from "../ts7/adapter.js"; + +/** Families a checked-dynamic slot can hold (design §3.7). The others + * name values the compiler cannot produce (opaque stdlib and @types/node + * types, engine-backed ambient types), package value types that scope B + * routes to Node islands, enum objects with no runtime value, and generic + * implementations without an erased instance. */ +const DEGRADABLE_FAMILIES: ReadonlySet = new Set([ + "index-signature", + "overloads", + "intersection", + "component", + "record-member", + "unsupported", + "dyn-field-policy", +]); + +function nameText(name: ts.Node | undefined): string | null { + if (name === undefined) return null; + if (ts.isIdentifier(name) || ts.isPrivateIdentifier(name)) return name.text; + if (ts.isStringLiteral(name) || ts.isNumericLiteral(name)) return name.text; + return ts.getTextOfNode(name); +} + +function isParameterProperty(param: ts.ParameterDeclaration): boolean { + return ( + ts.isConstructorDeclaration(param.parent) && + (param.modifiers?.nodes ?? []).some( + (m) => + m.kind === ts.SyntaxKind.PublicKeyword || + m.kind === ts.SyntaxKind.PrivateKeyword || + m.kind === ts.SyntaxKind.ProtectedKeyword || + m.kind === ts.SyntaxKind.ReadonlyKeyword, + ) + ); +} + +function variableSlot(decl: ts.VariableDeclaration): TypeFailureSlot { + const list = decl.parent; + const holder = list?.parent; + if ( + holder !== undefined && + (ts.isForOfStatement(holder) || ts.isForInStatement(holder) || ts.isForStatement(holder)) + ) + return "loop"; + if (holder !== undefined && ts.isVariableStatement(holder) && ts.isSourceFile(holder.parent)) + return "global"; + return "local"; +} + +function functionLabel(fn: ts.Node): string { + if (ts.isConstructorDeclaration(fn)) return "constructor"; + if (ts.isFunctionLikeDeclaration(fn)) { + const own = nameText(fn.name); + if (own !== null) return own; + const parent = fn.parent; + if ( + parent !== undefined && + (ts.isVariableDeclaration(parent) || + ts.isPropertyAssignment(parent) || + ts.isPropertyDeclaration(parent)) + ) { + const bound = nameText(parent.name); + if (bound !== null) return bound; + } + } + return ""; +} + +/** The slot a badType blame node denotes. A declaration's own node, its + * name and its type annotation name the declaration's slot; anything + * reached through an expression is an expression slot (the type of a + * cast, a literal, a temporary). */ +export function typeFailureSlot(node: ts.Node): { slot: TypeFailureSlot; name: string } { + let current: ts.Node = node; + for (let depth = 0; depth < 64; depth++) { + if (ts.isParameter(current)) { + const name = nameText(current.name) ?? ""; + return { slot: isParameterProperty(current) ? "field" : "param", name }; + } + if (ts.isPropertyDeclaration(current)) + return { slot: "field", name: nameText(current.name) ?? "" }; + if (ts.isVariableDeclaration(current)) + return { slot: variableSlot(current), name: nameText(current.name) ?? "" }; + if (ts.isBindingElement(current)) { + let owner: ts.Node | undefined = current.parent; + while (owner !== undefined && !ts.isVariableDeclaration(owner) && !ts.isParameter(owner)) + owner = owner.parent; + const slot: TypeFailureSlot = + owner === undefined + ? "local" + : ts.isParameter(owner) + ? "param" + : variableSlot(owner as ts.VariableDeclaration); + return { slot, name: nameText(current.name) ?? "" }; + } + if (ts.isPropertySignature(current) || ts.isMethodSignatureDeclaration(current)) + return { slot: "member", name: nameText(current.name) ?? "" }; + if (ts.isFunctionLikeDeclaration(current)) + return { slot: "return", name: functionLabel(current) }; + if (ts.isCatchClause(current)) return { slot: "local", name: "" }; + const parent: ts.Node | undefined = current.parent; + if (parent === undefined || ts.isSourceFile(parent)) break; + // Climb from a declaration's name or type annotation, or from inside a + // type node; any other position is an expression's own type. + const declared = + (ts.isParameter(parent) || + ts.isPropertyDeclaration(parent) || + ts.isVariableDeclaration(parent) || + ts.isBindingElement(parent) || + ts.isPropertySignature(parent) || + ts.isMethodSignatureDeclaration(parent) || + ts.isFunctionLikeDeclaration(parent)) && + ((parent as { name?: ts.Node }).name === current || + (parent as { type?: ts.Node }).type === current); + if (!declared && !ts.isTypeNode(current) && !ts.isTypeNode(parent)) break; + current = parent; + } + const text = ts.getTextOfNode(node); + return { slot: "expression", name: text.length > 60 ? `${text.slice(0, 57)}...` : text }; +} + +/** Program code outside node_modules, in TypeScript: the code holes apply + * to (design §3.3, §3.8). JavaScript keeps its dynamic fallback and its + * runtime fences; npm packages keep island routing. */ +function programTypeScript(node: ts.Node): boolean { + const sf = ts.getSourceFileOfNode(node); + const file = sf.fileName; + if (file.includes("/node_modules/")) return false; + if (sf.isDeclarationFile) return false; + return !/\.(?:[cm]?js|jsx)$/.test(file); +} + +export function typeFailureOf( + node: ts.Node, + declaredType: string, + family: TypeFailureFamily, +): TypeFailure { + const { slot, name } = typeFailureSlot(node); + return { + slot, + name, + declaredType, + family, + degradable: DEGRADABLE_FAMILIES.has(family) && programTypeScript(node), + }; +} + +function locKey(node: ts.Node): string { + const sf = ts.getSourceFileOfNode(node); + return `${sf.fileName}:${ts.tokenStartOfNode(node)}`; +} + +/** The failure-group key of a deferred declaration (a class shape or a + * function signature whose collection poisoned). */ +export function declarationGroupKey(symbol: ts.Symbol): string { + const decl = symbol.declarations[0]; + const name = ts.symbolName(symbol); + if (decl === undefined) return `declaration ${name}`; + const kind = + ts.isClassDeclaration(decl) || ts.isClassExpression(decl) + ? "class" + : ts.isFunctionLikeDeclaration(decl) + ? "function" + : "declaration"; + return `${kind} ${name} @${locKey(decl)}`; +} + +/** The failure-group key of a poisoned declaration statement whose + * bindings are blocked. */ +export function statementGroupKey(stmt: ts.Statement): string { + return `statement @${locKey(stmt)}`; +} diff --git a/packages/compiler/src/frontend/lowering/typed-arguments.ts b/packages/compiler/src/frontend/lowering/typed-arguments.ts index 2fbd83482..e837d59e6 100644 --- a/packages/compiler/src/frontend/lowering/typed-arguments.ts +++ b/packages/compiler/src/frontend/lowering/typed-arguments.ts @@ -2,7 +2,15 @@ import * as ts from "../ts7/adapter.js"; import { bodyReadsArguments } from "../arguments-usage.js"; import { isJsSourceFile, locOf } from "../program.js"; import { restTupleElementTypes, unitOnlyUnion } from "../type-mapper.js"; -import { DYN, type IrExpr, type IrLocal, type IrStmt, type IrType, arrayOf } from "../../ir/ir.js"; +import { + DYN, + type IrExpr, + type IrLocal, + type IrParam, + type IrStmt, + type IrType, + arrayOf, +} from "../../ir/ir.js"; import type { Lowerer } from "./lowerer.js"; import { captureContextArguments } from "./function-context.js"; import { isThisParameter, type ParamShape } from "./call-signatures.js"; @@ -156,7 +164,7 @@ export function declareTypedArguments( lowerer: Lowerer, node: ts.FunctionExpression, shapes: readonly ParamShape[], - params: { localId: string; name: string; type: IrType }[], + params: IrParam[], prologue: IrStmt[], ): void { const loc = locOf(node); @@ -187,6 +195,15 @@ export function declareTypedArguments( }); } +/** The binding an identifier reads: its symbol, or for the name of a + * shorthand property (`{ xs }`) the variable whose value the property takes. */ +export function bindingSymbolOfRead(lowerer: Lowerer, id: ts.Identifier): ts.Symbol | undefined { + const parent = id.parent; + if (parent && ts.isShorthandPropertyAssignment(parent) && parent.name === id) + return lowerer.checker.getShorthandAssignmentValueSymbol(parent); + return lowerer.checker.getSymbolAtLocation(id); +} + /** Whether every read of a tuple binding in `fn` (identified by `isRoot`) * only forwards it: `f.apply(t, x)`, `g(...x)` into a program function, * `x.length`, or a const alias whose own reads only forward. Reads inside @@ -254,7 +271,7 @@ export function onlyForwarded( const alias = lowerer.checker.getSymbolAtLocation(p.name); const aliasName = p.name; if (alias) { - roots.push((id) => id !== aliasName && lowerer.checker.getSymbolAtLocation(id) === alias); + roots.push((id) => id !== aliasName && bindingSymbolOfRead(lowerer, id) === alias); return undefined; } } diff --git a/packages/compiler/src/frontend/lowering/union-narrow.test.ts b/packages/compiler/src/frontend/lowering/union-narrow.test.ts index ed60fac8d..5c2a81b5e 100644 --- a/packages/compiler/src/frontend/lowering/union-narrow.test.ts +++ b/packages/compiler/src/frontend/lowering/union-narrow.test.ts @@ -97,3 +97,22 @@ test("a deferred read requires a real undefined tag", () => { const value: IrExpr = { kind: "boolLit", value: false, type: BOOL, loc }; expect(buildUnionNarrow("bad", from, BOOL, loc, () => "", value)).toBeNull(); }); + +test("a failure message replaces every other tag's TypeError", () => { + const fn = buildUnionNarrow( + "call", + union, + STRING, + loc, + (type) => type.kind, + null, + "f is not a function", + )!; + const thrown = branches(fn).flatMap((branch) => { + const statement = branch.then[0]!; + return statement.kind === "throw" ? [statement.value] : []; + }); + expect(thrown).toHaveLength(4); + for (const value of thrown) + expect(value).toMatchObject({ kind: "libCall", args: [{ value: "f is not a function" }] }); +}); diff --git a/packages/compiler/src/frontend/lowering/union-narrow.ts b/packages/compiler/src/frontend/lowering/union-narrow.ts index 3bb09cb26..f9a858fb6 100644 --- a/packages/compiler/src/frontend/lowering/union-narrow.ts +++ b/packages/compiler/src/frontend/lowering/union-narrow.ts @@ -1,4 +1,5 @@ import { BOOL, STRING, isUnitType, typeEquals } from "../../ir/ir.js"; +import { renderOriginNote } from "./decl-origin.js"; import type { IrExpr, IrFunction, IrStmt, IrType, IrUnionDef, SrcLoc } from "../../ir/ir.js"; /** The TypeError message for a checked extraction that finds `arm`. */ @@ -20,7 +21,10 @@ export function unionNarrowFailureMessage( * * Deferred scalar fields can supply the established undefined default: * false for booleans or NaN for numbers. No other tag receives that default, - * and reference-bearing targets always use checked extraction. */ + * and reference-bearing targets always use checked extraction. + * + * `failureMessage` replaces the TypeError message of every other tag (the + * error Node raises for the operation consuming the value). */ export function buildUnionNarrow( name: string, from: IrUnionDef, @@ -28,6 +32,15 @@ export function buildUnionNarrow( loc: SrcLoc, formatType: (type: IrType) => string, undefinedDefault: IrExpr | null = null, + failureMessage: string | null = null, + /** Converts another arm's payload into the target (a record arm that is + * a width subtype of a record target), or null to throw. */ + convert: ((tag: number, payload: IrExpr) => IrExpr | null) | null = null, + /** The target's package-declared origin (decl-origin.ts): each arm's + * message names the declaration and the patch admitting that arm. */ + origin: string | null = null, + /** The program targets Windows (the patch command's quoting). */ + windows = false, ): IrFunction | null { if (isUnitType(target) || target.kind === "void") return null; const targetTag = from.arms.findIndex((arm) => typeEquals(arm, target)); @@ -43,6 +56,7 @@ export function buildUnionNarrow( const inputType: IrType = { kind: "union", unionId: from.id }; const input: IrExpr = { kind: "varRef", localId: "u.0", type: inputType, loc }; const body: IrStmt[] = []; + let converted: IrExpr | null = null; for (let tag = 0; tag < from.arms.length; tag++) { const arm = from.arms[tag]!; let branch: IrStmt[]; @@ -56,6 +70,19 @@ export function buildUnionNarrow( ]; } else if (arm.kind === "undefinedT" && undefinedDefault !== null) { branch = [{ kind: "return", value: undefinedDefault, loc }]; + } else if ( + convert !== null && + !isUnitType(arm) && + (converted = convert(tag, { + kind: "unionNarrow", + unionId: from.id, + tag, + value: input, + type: arm, + loc, + })) !== null + ) { + branch = [{ kind: "return", value: converted, loc }]; } else { branch = [ { @@ -66,7 +93,19 @@ export function buildUnionNarrow( args: [ { kind: "strLit", - value: unionNarrowFailureMessage(arm, formatType), + value: + (failureMessage ?? unionNarrowFailureMessage(arm, formatType)) + + (origin === null + ? "" + : renderOriginNote( + origin, + isUnitType(arm) + ? arm.kind === "undefinedT" + ? "undefined" + : "null" + : formatType(arm), + windows, + )), type: STRING, loc, }, diff --git a/packages/compiler/src/frontend/npm-static-namespace-defaults.test.ts b/packages/compiler/src/frontend/npm-static-namespace-defaults.test.ts new file mode 100644 index 000000000..0fe7dc9a1 --- /dev/null +++ b/packages/compiler/src/frontend/npm-static-namespace-defaults.test.ts @@ -0,0 +1,80 @@ +import { spawnSync } from "node:child_process"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterAll, expect, test } from "vitest"; +import { nodeFrontendServices } from "./services-node.js"; + +const dirs: string[] = []; +afterAll(() => { + for (const dir of dirs) rmSync(dir, { recursive: true, force: true }); +}); + +function rewrite(source: string): string | null { + return nodeFrontendServices().namespaceDefaults(source, "/pkg/index.js"); +} + +/** Node prints the same for the original and the rewritten module. */ +function expectSameOutput(source: string): void { + const result = rewrite(source); + expect(typeof result).toBe("string"); + const dir = mkdtempSync(join(tmpdir(), "scriptc-namespace-defaults-")); + dirs.push(dir); + const outputs = [source, result!].map((text, i) => { + const file = join(dir, `m${i}.cjs`); + writeFileSync(file, text); + const r = spawnSync(process.execPath, [file], { encoding: "utf8" }); + expect(r.status).toBe(0); + return r.stdout; + }); + expect(outputs[1]).toBe(outputs[0]); +} + +test("a guarded namespace declaration becomes its default", () => { + const source = `var app = app || {}; +app.util = app.util || {}; +app.util.twice = function (x) { return 2 * x; }; +var cfg = cfg ?? { depth: 1 }; +console.log(app.util.twice(3), cfg.depth, Object.keys(app).join(",")); +`; + expect(rewrite(source)).toBe(`var app = {}; +app.util = app.util || {}; +app.util.twice = function (x) { return 2 * x; }; +var cfg = { depth: 1 }; +console.log(app.util.twice(3), cfg.depth, Object.keys(app).join(",")); +`); + expectSameOutput(source); +}); + +test("any other write, declaration or earlier mention keeps the text", () => { + // Redeclared: the second declaration reads the first one's object. + expect(rewrite("var ns = ns || {};\nvar ns = ns || {};\n")).toBeNull(); + // Assigned elsewhere, updated, or declared as a function. + expect(rewrite("var ns = ns || {};\nfunction reset() { ns = {}; }\n")).toBeNull(); + expect(rewrite("var n = n || 0;\nn++;\n")).toBeNull(); + // A local of the same name somewhere in the file. + expect(rewrite("var ns = ns || {};\nfunction f(ns) { return ns; }\n")).toBeNull(); + // Mentioned by an earlier statement. + expect(rewrite("console.log(typeof ns);\nvar ns = ns || {};\n")).toBeNull(); + // let and const read their own binding in its temporal dead zone. + expect(rewrite("let ns = ns || {};\n")).toBeNull(); + // Not the declared name on the left. + expect(rewrite("var ns = other || {};\n")).toBeNull(); + // Written through a destructuring assignment, here by a hoisted function + // that runs before the declaration. + expect( + rewrite("init();\nvar ns = ns || {};\nfunction init() { [ns] = [{ a: 1 }]; }\n"), + ).toBeNull(); + expect( + rewrite("init();\nvar ns = ns || {};\nfunction init() { ({ ns } = { ns: { a: 1 } }); }\n"), + ).toBeNull(); + expect(rewrite("var ns = ns || {};\nfor ([ns] of [[{}]]);\n")).toBeNull(); +}); + +test("a name the CommonJS wrapper defines keeps the text", () => { + // `exports` is the module's export object here, not undefined. + const source = "var exports = exports || {};\nexports.answer = 42;\n"; + expect(rewrite(source)).toBeNull(); + expect(rewrite("var module = module || {};\n")).toBeNull(); + expect(rewrite("var require = require || null;\n")).toBeNull(); +}); diff --git a/packages/compiler/src/frontend/npm-static-namespace-defaults.ts b/packages/compiler/src/frontend/npm-static-namespace-defaults.ts new file mode 100644 index 000000000..26a8e4cd0 --- /dev/null +++ b/packages/compiler/src/frontend/npm-static-namespace-defaults.ts @@ -0,0 +1,161 @@ +/* The guarded namespace declaration of pre-module JavaScript: + * + * var ns = ns || {}; + * + * In a module (CommonJS or ES), `var` is module-local and hoisted, so the + * read in its own initializer sees `undefined` unless something writes the + * variable first. When nothing else in the file ever writes it (no other + * declaration, assignment or update of that name anywhere, no function or + * class of that name), no earlier top-level statement mentions it, and it + * is not a name a CommonJS module's wrapper defines (`exports`, `module`, + * ...), the initializer is its right operand: `var ns = {};`. Inference then types + * the namespace from that literal and the members later statements add, + * instead of `any` (the self-reference makes the checker give up). `??` + * is the same. Anything else keeps the original text. */ +import * as ts from "./ts7/syntax.js"; +import { tokenStartOfNode } from "./ts7/node-functions.js"; +import { applyEdits, unparen, type Edit } from "./npm-static-bundled-chunks.js"; + +/** Names a CommonJS module's wrapper function defines before its first + * statement: its parameters and its arguments object. `var exports = + * exports || {}` keeps the module's export object, not a fresh one. */ +const COMMONJS_WRAPPER_NAMES = new Set([ + "exports", + "require", + "module", + "__filename", + "__dirname", + "arguments", +]); + +/** A cheap probe: some `var X = X ||` or `?? ` may be present. */ +export function mayHoldNamespaceDefault(source: string): boolean { + return /\bvar\s+([A-Za-z_$][\w$]*)\s*=\s*\(?\s*\1\s*(\|\||\?\?)/.test(source); +} + +/** The module text with each guarded namespace declaration respelled as a + * plain one, or null when there is none. */ +export function rewriteNamespaceDefaults(sf: ts.SourceFile): string | null { + const stmts = sf.statements.nodes; + const edits: Edit[] = []; + for (let i = 0; i < stmts.length; i++) { + const stmt = stmts[i]!; + if (!ts.isVariableStatement(stmt)) continue; + const list = stmt.declarationList; + if ((list.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const)) !== 0) continue; + for (const decl of list.declarations.nodes) { + if (!ts.isIdentifier(decl.name) || decl.initializer === undefined) continue; + const name = decl.name.text; + const init = unparen(decl.initializer); + if ( + !ts.isBinaryExpression(init) || + (init.operatorToken.kind !== ts.SyntaxKind.BarBarToken && + init.operatorToken.kind !== ts.SyntaxKind.QuestionQuestionToken) + ) + continue; + const left = unparen(init.left); + if (!ts.isIdentifier(left) || left.text !== name) continue; + if (COMMONJS_WRAPPER_NAMES.has(name)) continue; + if (mentionedBefore(stmts, i, name) || writtenElsewhere(sf, name, decl)) continue; + const right = init.right; + edits.push({ + start: tokenStartOfNode(decl.initializer), + end: decl.initializer.end, + // A literal stays bare: inference types `var ns = {}` from the + // members later statements add (a parenthesized one it does not). + text: + ts.isObjectLiteralExpression(unparen(right)) || + ts.isArrayLiteralExpression(unparen(right)) + ? sf.text.slice(tokenStartOfNode(right), right.end) + : `(${sf.text.slice(tokenStartOfNode(right), right.end)})`, + }); + } + } + if (edits.length === 0) return null; + return applyEdits(sf.text, 0, sf.text.length, edits); +} + +/** Whether a top-level statement before index `at` mentions `name`. */ +function mentionedBefore(stmts: readonly ts.Statement[], at: number, name: string): boolean { + for (let i = 0; i < at; i++) { + let found = false; + ts.walkPreorder(stmts[i]!, (n) => { + if (found) return "stop"; + if (ts.isIdentifier(n) && n.text === name) found = true; + return undefined; + }); + if (found) return true; + } + return false; +} + +/** Whether anything other than `decl` declares or writes `name`. */ +function writtenElsewhere(sf: ts.SourceFile, name: string, decl: ts.VariableDeclaration): boolean { + let written = false; + ts.walkPreorder(sf, (n) => { + if (written) return "stop"; + if (n === decl.name) return undefined; + if (ts.isBinaryExpression(n) && isAssignmentOperator(n.operatorToken.kind)) { + if (assignsName(n.left, name)) written = true; + } else if ( + (ts.isPrefixUnaryExpression(n) || ts.isPostfixUnaryExpression(n)) && + (n.operator === ts.SyntaxKind.PlusPlusToken || + n.operator === ts.SyntaxKind.MinusMinusToken) && + isName(n.operand, name) + ) { + written = true; + } else if ( + (ts.isFunctionDeclaration(n) || ts.isClassDeclaration(n)) && + n.name !== undefined && + n.name.text === name + ) { + written = true; + } else if ( + (ts.isVariableDeclaration(n) || ts.isParameter(n) || ts.isBindingElement(n)) && + n !== decl && + n.name !== undefined && + bindsName(n.name, name) + ) { + // Another declaration of the name (a redeclaration, or a local that + // shadows it) — keep the original text rather than reason about it. + written = true; + } else if ( + (ts.isForInStatement(n) || ts.isForOfStatement(n)) && + !ts.isVariableDeclarationList(n.initializer) && + assignsName(n.initializer, name) + ) { + written = true; + } + return undefined; + }); + return written; +} + +function bindsName(binding: ts.Node, name: string): boolean { + if (ts.isIdentifier(binding)) return binding.text === name; + let found = false; + ts.walkPreorder(binding, (n) => { + if (found) return "stop"; + if (ts.isIdentifier(n) && n.text === name) found = true; + return undefined; + }); + return found; +} + +/** Whether an assignment target may write `name`: the name itself, or a + * destructuring pattern that mentions it anywhere (over-approximated: a + * property key of that name counts too). */ +function assignsName(target: ts.Node, name: string): boolean { + const t = unparen(target as ts.Expression); + if (ts.isArrayLiteralExpression(t) || ts.isObjectLiteralExpression(t)) return bindsName(t, name); + return isName(t, name); +} + +function isName(node: ts.Node, name: string): boolean { + const n = unparen(node as ts.Expression); + return ts.isIdentifier(n) && n.text === name; +} + +function isAssignmentOperator(kind: ts.SyntaxKind): boolean { + return kind >= ts.SyntaxKind.FirstAssignment && kind <= ts.SyntaxKind.LastAssignment; +} diff --git a/packages/compiler/src/frontend/npm-static-prune.ts b/packages/compiler/src/frontend/npm-static-prune.ts index 84a58d561..f02a6e0f6 100644 --- a/packages/compiler/src/frontend/npm-static-prune.ts +++ b/packages/compiler/src/frontend/npm-static-prune.ts @@ -11,6 +11,7 @@ import { npmStaticPackageOfPath } from "./npm-static.js"; import { trackedReadFile } from "./input-tracker.js"; import { npmPackageNameOf } from "./workspace-registry.js"; import { typeOnlyNamedImport } from "./type-only-imports.js"; +import { moduleSourceCandidates } from "./module-source-candidates.js"; export function isPrunedNpmReexport(program: ts.Program, stmt: ts.ExportDeclaration): boolean { return program.analysis.prunedNpmReexports.has(stmt); @@ -117,6 +118,21 @@ function pureExternalPackage( type Demand = Set | null; // null requests the entire namespace +/** One module edge of a package file, resolved once per plan. */ +interface ModuleEdge { + spec: string; + dep: ts.SourceFile | null; +} + +/** A file's runtime module edges: its import declarations with the names + * they request (null: the whole module), its `export … from` declarations, + * and its `import()` and `require()` calls with one string argument. */ +interface ModuleEdges { + imports: (ModuleEdge & { names: readonly string[] | null })[]; + reexports: (ModuleEdge & { stmt: ts.ExportDeclaration })[]; + calls: ModuleEdge[]; +} + /** Package files outside the demanded module graph are absent from the * executable program. All ordinary project files retain their preflight * behavior, including files not imported by the entry. */ @@ -151,6 +167,70 @@ export function planNpmStaticReexports( } }; + // The demand walk visits a file again whenever another of its names is + // requested, and every purity check walks the files it reaches: both read + // these edges instead of resolving the file's syntax each time. + const edgeMemo = new Map(); + const edgesOf = (sf: ts.SourceFile): ModuleEdges => { + const cached = edgeMemo.get(sf); + if (cached !== undefined) return cached; + const edges: ModuleEdges = { imports: [], reexports: [], calls: [] }; + for (const stmt of sf.statements.nodes) { + if (ts.isImportDeclaration(stmt)) { + if (!ts.isStringLiteral(stmt.moduleSpecifier)) continue; + const clause = stmt.importClause; + if (clause?.phaseModifier === ts.SyntaxKind.TypeKeyword) continue; + const spec = stmt.moduleSpecifier.text; + let names: string[] | null = null; + if ( + clause !== undefined && + (clause.namedBindings === undefined || + !( + ts.isNamespaceImport(clause.namedBindings) || + (ts.isNamedImports(clause.namedBindings) && + clause.namedBindings.elements.nodes.length === 0) + )) && + // `import { type A }` still loads the module, like `import {}`. + !typeOnlyNamedImport(stmt) + ) { + names = []; + if (clause.name !== undefined) names.push("default"); + if (clause.namedBindings && ts.isNamedImports(clause.namedBindings)) { + for (const element of clause.namedBindings.elements.nodes) { + if (!element.isTypeOnly) names.push((element.propertyName ?? element.name).text); + } + } + } + edges.imports.push({ spec, dep: resolveEdge(sf, spec), names }); + } else if (ts.isExportDeclaration(stmt)) { + // `export { type A } from` still loads the module under Node's + // type stripping; only `export type … from` erases. + if ( + stmt.isTypeOnly || + stmt.moduleSpecifier === undefined || + !ts.isStringLiteral(stmt.moduleSpecifier) + ) + continue; + const spec = stmt.moduleSpecifier.text; + edges.reexports.push({ spec, dep: resolveEdge(sf, spec), stmt }); + } + } + for (const node of moduleSourceCandidates(program, sf).calls) { + if (node.arguments.nodes.length !== 1) continue; + const arg = node.arguments.nodes[0]; + if (arg === undefined || !ts.isStringLiteralLike(arg)) continue; + const kind = + node.expression.kind === ts.SyntaxKind.ImportKeyword + ? "import" + : ts.isIdentifier(node.expression) && node.expression.text === "require" + ? "require" + : null; + if (kind !== null) edges.calls.push({ spec: arg.text, dep: resolveEdge(sf, arg.text, kind) }); + } + edgeMemo.set(sf, edges); + return edges; + }; + for (const sf of files) { if (npmStaticPackageOfPath(sf.fileName) === null) request(sf, null); } @@ -178,57 +258,65 @@ export function planNpmStaticReexports( return false; } }; + // An edge this program does not resolve: the conservative + // whole-dependency promise of the external package it names. + const externalPure = (sf: ts.SourceFile, spec: string): boolean => { + const resolved = resolveBareModule(sf.fileName, spec, "js-only"); + const pkg = resolved === null ? null : npmPackageNameOf(resolved.typesFile); + const root = + pkg === null || resolved === null + ? null + : packageRootJsonPath(resolved.typesFile, pkg, packageJsons); + return root !== null && pureExternalPackage(root, packageJsons); + }; // Check the actual imported closure: metadata for one file never vouches // for its side-effectful descendants, even across a package or a cycle. - const pureModuleTree = (root: ts.SourceFile): boolean => { - const cached = pureMemo.get(root); - if (cached !== undefined) return cached; - const visited = new Set(); - const visit = (sf: ts.SourceFile): boolean => { - if (visited.has(sf)) return true; - if (pureMemo.get(sf) === false || !filePure(sf)) return false; - visited.add(sf); - const edges: { spec: string; kind?: "import" | "require" }[] = []; - for (const stmt of sf.statements.nodes) { - if ( - ts.isImportDeclaration(stmt) && - stmt.importClause?.phaseModifier !== ts.SyntaxKind.TypeKeyword && - ts.isStringLiteral(stmt.moduleSpecifier) - ) - edges.push({ spec: stmt.moduleSpecifier.text }); - else if ( - ts.isExportDeclaration(stmt) && - !stmt.isTypeOnly && - stmt.moduleSpecifier && - ts.isStringLiteral(stmt.moduleSpecifier) - ) - edges.push({ spec: stmt.moduleSpecifier.text }); + // A file is pure when every file it reaches is. The walk finds the + // strongly connected components of the module graph (Tarjan): a finished + // component is pure as a whole. On the first impure file the walk stops, + // and every file still on its stack reaches that file, so each is impure. + const order = new Map(); + const stack: ts.SourceFile[] = []; + /** The lowest order the file's walk reaches (Infinity for a finished + * component), or -1 once it reaches an impure file. */ + const visit = (sf: ts.SourceFile): number => { + const known = pureMemo.get(sf); + if (known !== undefined) return known ? Infinity : -1; + const seen = order.get(sf); + if (seen !== undefined) return seen; + if (!filePure(sf)) { + pureMemo.set(sf, false); + return -1; + } + const own = order.size; + order.set(sf, own); + stack.push(sf); + let low = own; + const edges = edgesOf(sf); + for (const list of [edges.imports, edges.reexports, edges.calls]) { + for (const edge of list) { + const reached = + edge.dep !== null ? visit(edge.dep) : externalPure(sf, edge.spec) ? low : -1; + if (reached < 0) return -1; + if (reached < low) low = reached; } - ts.walkPreorder(sf, (node) => { - if (!ts.isCallExpression(node) || node.arguments.nodes.length !== 1) return; - const arg = node.arguments.nodes[0]; - if (!arg || !ts.isStringLiteralLike(arg)) return; - if (node.expression.kind === ts.SyntaxKind.ImportKeyword) - edges.push({ spec: arg.text, kind: "import" }); - else if (ts.isIdentifier(node.expression) && node.expression.text === "require") - edges.push({ spec: arg.text, kind: "require" }); - }); - return edges.every((edge) => { - const dep = resolveEdge(sf, edge.spec, edge.kind); - if (dep !== null) return visit(dep); - const resolved = resolveBareModule(sf.fileName, edge.spec, "js-only"); - const pkg = resolved === null ? null : npmPackageNameOf(resolved.typesFile); - const root = - pkg === null || resolved === null - ? null - : packageRootJsonPath(resolved.typesFile, pkg, packageJsons); - return root !== null && pureExternalPackage(root, packageJsons); - }); - }; - const pure = visit(root); - if (pure) for (const sf of visited) pureMemo.set(sf, true); - else pureMemo.set(root, false); - return pure; + } + if (low === own) { + for (;;) { + const member = stack.pop()!; + order.delete(member); + pureMemo.set(member, true); + if (member === sf) break; + } + } + return low; + }; + const pureModuleTree = (root: ts.SourceFile): boolean => { + if (visit(root) >= 0) return true; + for (const sf of stack) pureMemo.set(sf, false); + stack.length = 0; + order.clear(); + return false; }; const canPrune = ( sf: ts.SourceFile, @@ -245,78 +333,32 @@ export function planNpmStaticReexports( return names !== undefined && names !== null && !names.has(stmt.exportClause.name.text); }; + // A file is queued again only when its demand grows. Its imports and + // calls request the same modules every time; only a namespace re-export + // the new demand names stops being prunable. + const walked = new Set(); while (queue.length > 0) { const sf = queue.shift()!; - for (const stmt of sf.statements.nodes) { - if (ts.isImportDeclaration(stmt)) { - if (!ts.isStringLiteral(stmt.moduleSpecifier)) continue; - const clause = stmt.importClause; - if (clause?.phaseModifier === ts.SyntaxKind.TypeKeyword) continue; - const dep = resolveEdge(sf, stmt.moduleSpecifier.text); - if ( - clause === undefined || - (clause.namedBindings !== undefined && - (ts.isNamespaceImport(clause.namedBindings) || - (ts.isNamedImports(clause.namedBindings) && - clause.namedBindings.elements.nodes.length === 0))) || - // `import { type A }` still loads the module, like `import {}`. - typeOnlyNamedImport(stmt) - ) { - request(dep, null); - } else { - if (clause.name !== undefined) request(dep, "default"); - if (clause.namedBindings && ts.isNamedImports(clause.namedBindings)) { - for (const element of clause.namedBindings.elements.nodes) { - if (!element.isTypeOnly) request(dep, (element.propertyName ?? element.name).text); - } - } - } - } else if (ts.isExportDeclaration(stmt)) { - // `export { type A } from` still loads the module under Node's - // type stripping; only `export type … from` erases. - if ( - stmt.isTypeOnly || - stmt.moduleSpecifier === undefined || - !ts.isStringLiteral(stmt.moduleSpecifier) - ) - continue; - const dep = resolveEdge(sf, stmt.moduleSpecifier.text); - if (canPrune(sf, stmt, dep)) continue; - request(dep, null); + const edges = edgesOf(sf); + const first = !walked.has(sf); + walked.add(sf); + if (first) { + for (const edge of edges.imports) { + if (edge.names === null) request(edge.dep, null); + else for (const name of edge.names) request(edge.dep, name); } } - ts.walkPreorder(sf, (node) => { - if (!ts.isCallExpression(node) || node.arguments.nodes.length !== 1) return undefined; - const arg = node.arguments.nodes[0]; - if (arg === undefined || !ts.isStringLiteralLike(arg)) return undefined; - if ( - node.expression.kind === ts.SyntaxKind.ImportKeyword || - (ts.isIdentifier(node.expression) && node.expression.text === "require") - ) { - request( - resolveEdge( - sf, - arg.text, - node.expression.kind === ts.SyntaxKind.ImportKeyword ? "import" : "require", - ), - null, - ); - } - return undefined; - }); + for (const edge of edges.reexports) { + if (canPrune(sf, edge.stmt, edge.dep)) continue; + request(edge.dep, null); + } + if (first) for (const edge of edges.calls) request(edge.dep, null); } const pruned = new Set(); for (const sf of demanded.keys()) { - for (const stmt of sf.statements.nodes) { - if ( - !ts.isExportDeclaration(stmt) || - stmt.moduleSpecifier === undefined || - !ts.isStringLiteral(stmt.moduleSpecifier) - ) - continue; - if (canPrune(sf, stmt, resolveEdge(sf, stmt.moduleSpecifier.text))) pruned.add(stmt); - } + for (const edge of edgesOf(sf).reexports) + if (canPrune(sf, edge.stmt, edge.dep)) pruned.add(edge.stmt); } program.analysis.prunedNpmReexports.clear(); for (const stmt of pruned) program.analysis.prunedNpmReexports.add(stmt); diff --git a/packages/compiler/src/frontend/npm-static-type-exports-host.ts b/packages/compiler/src/frontend/npm-static-type-exports-host.ts index 8a61ed62b..f8aaa20f9 100644 --- a/packages/compiler/src/frontend/npm-static-type-exports-host.ts +++ b/packages/compiler/src/frontend/npm-static-type-exports-host.ts @@ -2,7 +2,8 @@ * package's declaration entry and runtime entry, copies the declaration * files the entry reaches, and builds the runtime entry's typedefs. */ import { dirname, relative, resolve as resolvePath } from "node:path"; -import { trackedFileExists, trackedReadFile } from "./input-tracker.js"; +import { trackedFileExists } from "./input-tracker.js"; +import { packageSourceText } from "./type-patches/layer.js"; import { resolveBareModule } from "./resolve.js"; import { isJsSourceFileName } from "./tsc-codes.js"; import { NPM_STATIC_TYPES_SUFFIX } from "./npm-static.js"; @@ -78,7 +79,7 @@ export function npmStaticTypeExportPlan( const parse = (file: string): ts.SourceFile | null => { let sf = parsed.get(file); if (sf === undefined) { - const text = trackedReadFile(file); + const text = packageSourceText(file); sf = text === null ? null : services.parse(file, text, "ts"); parsed.set(file, sf); } @@ -212,7 +213,7 @@ export function npmStaticTypeExportPlan( continue; const runtime = resolveBareModule(entryPath, pkg, "js-only"); if (runtime === null || !isJsSourceFileName(runtime.typesFile)) continue; - const runtimeText = trackedReadFile(runtime.typesFile); + const runtimeText = packageSourceText(runtime.typesFile); if (runtimeText === null) continue; const { types, values } = typesOf(declared.typesFile, new Set()); const declaredByRuntime = runtimeTypeNames(services, runtime.typesFile, runtimeText); diff --git a/packages/compiler/src/frontend/npm-static.ts b/packages/compiler/src/frontend/npm-static.ts index eed02c95c..8e26e3058 100644 --- a/packages/compiler/src/frontend/npm-static.ts +++ b/packages/compiler/src/frontend/npm-static.ts @@ -23,6 +23,7 @@ import { trackedRealpath, } from "./input-tracker.js"; import { npmBuiltinAliasOf } from "./builtin-modules.js"; +import { packageSourceText, typePatchSuppliesTypes } from "./type-patches/layer.js"; let activePackages: ReadonlySet = new Set(); let declarationOverloads: ReadonlyMap = new Map(); @@ -82,7 +83,7 @@ export function setNpmStaticTypeExports( function withTypeExportTypedefs(path: string, text: string | null): string | null { const typedefs = typeExportTypedefs.get(path.split("\\").join("/")); if (typedefs === undefined) return text; - const source = text ?? trackedReadFile(path); + const source = text ?? packageSourceText(path); return source === null ? text : `${source}\n${typedefs}\n`; } @@ -254,13 +255,18 @@ function aliasedPackageName(pkgDir: string): string | null { export function npmStaticTransformPkgJson(pkg: Record): void { delete pkg["types"]; delete pkg["typings"]; - const transform = (v: unknown): void => { - if (v === null || typeof v !== "object") return; - if (Array.isArray(v)) { - for (const item of v) transform(item); - return; - } - const obj = v as Record; + const transform = (v: unknown): unknown => { + if (v === null || typeof v !== "object") return v; + if (Array.isArray(v)) return v.map(transform); + // The result is a new object, built in key order; the first entry of a + // key wins. + const entries: [string, unknown][] = []; + const seen = new Set(); + const add = (key: string, value: unknown): void => { + if (seen.has(key)) return; + seen.add(key); + entries.push([key, value]); + }; // 0. The "node" condition HOISTS: Node's runtime resolution matches it // (yaml's `{ types, node: "./dist/index.js", default: "./browser/ // index.js" }` runs the node dist; the browser dist is a DIFFERENT @@ -270,24 +276,18 @@ export function npmStaticTransformPkgJson(pkg: Record): void { // under import+require (either matcher finds it there), an object's // entries splice inline — earlier real keys keep winning, exactly // Node's first-match-in-key-order rule. - if (obj["node"] !== undefined) { - const entries = Object.entries(obj); - for (const k of Object.keys(obj)) delete obj[k]; - for (const [k, val] of entries) { - if (k !== "node") { - if (!(k in obj)) obj[k] = val; - continue; - } - const inner: Record = - typeof val === "string" - ? { import: val, require: val } - : val !== null && typeof val === "object" && !Array.isArray(val) - ? (val as Record) - : {}; - for (const [ik, iv] of Object.entries(inner)) { - if (!(ik in obj)) obj[ik] = iv; - } + for (const [key, value] of Object.entries(v as Record)) { + if (key !== "node") { + add(key, value); + continue; } + const inner: Record = + typeof value === "string" + ? { import: value, require: value } + : value !== null && typeof value === "object" && !Array.isArray(value) + ? (value as Record) + : {}; + for (const [innerKey, innerValue] of Object.entries(inner)) add(innerKey, innerValue); } // 1. The types claims go AFTER the hoist: a node object's own // "types" key (uuid's `node: { types: "./dist/index.d.ts", default: @@ -295,15 +295,17 @@ export function npmStaticTransformPkgJson(pkg: Record): void { // in place tsgo follows it, maps the hidden declaration to its // same-stem JS (dist/index.js, the browser build) and disagrees with // this resolver's runtime answer. - delete obj["types"]; - delete obj["typings"]; - for (const value of Object.values(obj)) transform(value); + return Object.fromEntries( + entries + .filter(([key]) => key !== "types" && key !== "typings") + .map(([key, value]): [string, unknown] => [key, transform(value)]), + ); }; - if (pkg["exports"] !== undefined) transform(pkg["exports"]); + if (pkg["exports"] !== undefined) pkg["exports"] = transform(pkg["exports"]); // The "imports" field maps "#" specifiers with the same condition objects // (chalk 5's `"#supports-color": { node, default }`), so Node's "node" // target hoists there too. - if (pkg["imports"] !== undefined) transform(pkg["imports"]); + if (pkg["imports"] !== undefined) pkg["imports"] = transform(pkg["imports"]); } /** npmStaticTransformPkgJson over a package.json TEXT (the fs shadow's @@ -372,6 +374,7 @@ function packageIsUntyped(path: string): boolean { /* no package.json — keep probing */ } if (untyped && trackedExists(`${pkgDir}/index.d.ts`)) untyped = false; + if (untyped && typePatchSuppliesTypes(pkgDir)) untyped = false; if (untyped) { // the @types twin, hoisted anywhere up the realm chain const mangled = mangledTypesName(dirName); @@ -466,7 +469,11 @@ export function npmStaticFsShadow(services: FrontendServices): NpmStaticFsShadow if (hit !== undefined) return hit ?? undefined; let rewritten: string | null = null; try { - let original = trackedReadFile(path); + // A project type patch (type-patches/layer.ts) edits the text + // as the package wrote it, before any rewrite reads its JSDoc + // (unrewritten files fall through to the host, which serves the + // patched text too). + let original = packageSourceText(path); // A bundle's CommonJS chunks move to their own modules first, so // every later pass (and this shadow, for each chunk) sees plain // CommonJS module text. @@ -504,6 +511,10 @@ export function npmStaticFsShadow(services: FrontendServices): NpmStaticFsShadow // literals they build. const enums = original === null ? null : services.enumIifes(original, path); if (enums !== null) original = enums; + // Guarded namespace declarations (`var ns = ns || {}`) read the + // hoisted `undefined`: the declaration is its default. + const namespaces = original === null ? null : services.namespaceDefaults(original, path); + if (namespaces !== null) original = namespaces; // A module that names its export object through a variable reaches // its members through `exports`. const exportsVar = original === null ? null : services.cjsExportsVar(original, path); @@ -516,6 +527,7 @@ export function npmStaticFsShadow(services: FrontendServices): NpmStaticFsShadow aliased !== null || fnExports !== null || enums !== null || + namespaces !== null || exportsVar !== null; if (original !== null) { const namepaths = services.jsDocNamepaths(path, original); diff --git a/packages/compiler/src/frontend/package-shims.ts b/packages/compiler/src/frontend/package-shims.ts index 1727d2619..34d8461dc 100644 --- a/packages/compiler/src/frontend/package-shims.ts +++ b/packages/compiler/src/frontend/package-shims.ts @@ -56,6 +56,10 @@ interface ImplementationShim { /** Supported major versions and their entry modules, relative to the * shim root. */ versions: ReadonlyMap; + /** Major versions without an entry module that `--npm-static` compiles + * from the package's own published JavaScript. An installed version that + * is in neither list stays out of the program. */ + sourceVersions?: ReadonlySet; /** The entry has a default export (a CommonJS package's whole export * object, which a default import reads). */ defaultExport?: boolean; @@ -99,6 +103,7 @@ const IMPLEMENTATION_SHIMS: ReadonlyMap = new Map< mainFile: "source/index.js", typesFile: "source/index.d.ts", versions: new Map([[5, "chalk/v5.ts"]]), + sourceVersions: new Set([6]), }, ], [ @@ -200,20 +205,39 @@ export function packageShimEntries(): string[] { let unloadedPackages: ReadonlySet = new Set(); /** Reads the project's package-shim configuration: the `scriptc.packages` - * entries marked "unloaded" in the entry's nearest package.json - * (project-config.ts validates the rest). Called once per program load. */ + * entries marked "unloaded", and those with `"shapes": false`, in the + * entry's nearest package.json (project-config.ts validates the rest). + * Called once per program load. */ export function configurePackageShims(entryPath: string): void { const packages = new Set(); + const noShapes = new Set(); for (const [name, setting] of readProjectConfig(entryPath).packages) { if (setting.unloaded) packages.add(name); + if (!setting.shapes) noShapes.add(name); } unloadedPackages = packages; + shapelessPackages = noShapes; } -/** True for a package name the registry implements or the project marks - * never loaded: such a package never compiles its published JavaScript. */ -export function isPackageShimName(name: string): boolean { - return IMPLEMENTATION_SHIMS.has(name) || unloadedPackages.has(name); +let shapelessPackages: ReadonlySet = new Set(); + +/** Packages the project compiles without shape inference's overlay + * (`"scriptc": { "packages": { "": { "shapes": false } } }`). */ +export function isShapelessPackage(name: string): boolean { + return shapelessPackages.has(name); +} + +/** True when `--npm-static` leaves the package `name` out of its package + * list: the project marks it never loaded, or the registry implements it + * and the version installed for `entryPath` has an entry module or is not + * one of the entry's `sourceVersions`. */ +export function npmStaticOmitsPackage(entryPath: string, name: string): boolean { + if (unloadedPackages.has(name)) return true; + const shim = IMPLEMENTATION_SHIMS.get(name); + if (shim === undefined) return false; + const dir = installedPackageDir(entryPath, name); + const major = dir === null ? null : installedMajorVersion(dir, name); + return major === null || shim.versions.has(major) || !shim.sourceVersions?.has(major); } /** True when `specifier` names a package the project marks never loaded. */ @@ -249,9 +273,9 @@ function installedPackageDir(fromFile: string, name: string): string | null { return found; } -/** The shim entry for an installed package directory, or null when the - * directory is not a registered package at a supported major version. */ -function shimEntryForPackageDir(pkgDir: string, expectedName: string | null): string | null { +/** The installed package's name and major version from its package.json, + * or null when either is missing. */ +function installedPackageVersion(pkgDir: string): { name: string; major: number } | null { const text = trackedReadFile(join(pkgDir, "package.json")); if (text === null) return null; let pkg: { name?: unknown; version?: unknown }; @@ -261,11 +285,24 @@ function shimEntryForPackageDir(pkgDir: string, expectedName: string | null): st return null; } if (typeof pkg.name !== "string" || typeof pkg.version !== "string") return null; - if (expectedName !== null && pkg.name !== expectedName) return null; - const shim = IMPLEMENTATION_SHIMS.get(pkg.name); + return { name: pkg.name, major: Number(/^(\d+)\./.exec(pkg.version)?.[1] ?? NaN) }; +} + +/** The major version of the package `name` installed at `pkgDir`, or null. */ +function installedMajorVersion(pkgDir: string, name: string): number | null { + const installed = installedPackageVersion(pkgDir); + return installed === null || installed.name !== name ? null : installed.major; +} + +/** The shim entry for an installed package directory, or null when the + * directory is not a registered package at a supported major version. */ +function shimEntryForPackageDir(pkgDir: string, expectedName: string | null): string | null { + const installed = installedPackageVersion(pkgDir); + if (installed === null) return null; + if (expectedName !== null && installed.name !== expectedName) return null; + const shim = IMPLEMENTATION_SHIMS.get(installed.name); if (shim === undefined) return null; - const major = Number(/^(\d+)\./.exec(pkg.version)?.[1] ?? NaN); - const entry = shim.versions.get(major); + const entry = shim.versions.get(installed.major); if (entry === undefined) return null; return join(packageShimRoot(), shim.variant?.(pkgDir) ?? entry); } diff --git a/packages/compiler/src/frontend/pipeline-native.ts b/packages/compiler/src/frontend/pipeline-native.ts index 511fd62f2..f2df52608 100644 --- a/packages/compiler/src/frontend/pipeline-native.ts +++ b/packages/compiler/src/frontend/pipeline-native.ts @@ -1,4 +1,4 @@ -import { runFrontend, type Frontend } from "./pipeline.js"; +import { runFrontend, type Frontend, type FrontendOptions } from "./pipeline.js"; import { loadProgram } from "./program.js"; import { FrontendServices, type ComptimeEvaluator } from "./services.js"; import { createNativeTs7Api } from "./ts7/native-api.js"; @@ -13,6 +13,7 @@ export function runNativeFrontend( externalTypes?: Readonly>, evaluateComptime?: ComptimeEvaluator, libraryNpmStatic: readonly string[] = [], + options: FrontendOptions = {}, ): Frontend { const services = new FrontendServices( (options) => createNativeTs7Api({ ...options, executable }), @@ -31,6 +32,7 @@ export function runNativeFrontend( npmStatic, externalTypes, libraryNpmStatic, + options, ); return { ...frontend, diff --git a/packages/compiler/src/frontend/pipeline.ts b/packages/compiler/src/frontend/pipeline.ts index 8f8d37eef..13407f499 100644 --- a/packages/compiler/src/frontend/pipeline.ts +++ b/packages/compiler/src/frontend/pipeline.ts @@ -1,3 +1,4 @@ +import { shapeOverlayReport } from "./shape-inference/index.js"; /** Shared frontend orchestration. Hosts supply a program loader so Node * and native callers use the same npm selection, preflight and lowering. */ import type { ScrDiagnostic } from "../diagnostics/diagnostic.js"; @@ -48,6 +49,7 @@ import { NODE_API_MODULE } from "./node-api-import.js"; import { erasedTypeOnlyImport } from "./type-only-imports.js"; import { EXPERIMENTAL_MODULE } from "./experimental-import.js"; import { readProjectConfig } from "../project-config.js"; +import { typePatchDiagnostics } from "./type-patches/layer.js"; import { packageDirOf } from "../node-islands/origin.js"; import { scanFiles, @@ -116,18 +118,30 @@ export interface Frontend { /** The Node island detector's input (report-only): the program's * modules with their pre-scan markers, package import edges, and how * each refused package was refused. Call before dispose. */ - nodeIslandScan?: (opts: { userJs: boolean }) => NodeIslandScan; + nodeIslandScan?: ((opts: { userJs: boolean }) => NodeIslandScan) | undefined; + /** Diagnostics that do not fail a build: unknown settings in the + * project's scriptc configuration (SC8004), which builds do not use. */ + warnings?: ScrDiagnostic[]; /** Releases the frontend's resources (the spawned tsgo server). Call * exactly once, after the last lower(). */ dispose: () => void; } /** Hosts own transport lifetimes; shared compiler stages own the frontend. */ +/** What a frontend computes beyond the build's own needs. */ +export interface FrontendOptions { + /** The Node island report (coverage): the project's scriptc settings and + * the detector's scan of the program and its npm packages. Builds do not + * read either. */ + nodeIslands?: boolean; +} + export type FrontendFactory = ( entryPath: string, npmStatic?: readonly string[] | "auto" | "lib", externalTypes?: Readonly>, libraryNpmStatic?: readonly string[], + options?: FrontendOptions, ) => Frontend; /** Adds the program modules a literal `import()` reaches to `scanned`, @@ -241,7 +255,8 @@ function detectAutoPackages( } else if (mode === "lib" && !viaTransitive) { // CJS packages spell their dep edges as top-level requires; the // import-statement scan alone would miss every one of them. - for (const req of requiresOf(stmt)) edges.push({ spec: req.spec, loc: locOf(req.node) }); + for (const req of requiresOf(stmt, load.program)) + edges.push({ spec: req.spec, loc: locOf(req.node) }); } } // Transitive selection counts every literal require() and import() @@ -432,6 +447,7 @@ export function runFrontend( npmStatic?: readonly string[] | "auto" | "lib", externalTypes?: Readonly>, libraryNpmStatic: readonly string[] = [], + options: FrontendOptions = {}, ): Frontend { // A preflight or package probe may throw after its host has opened. Track // only live loads, releasing each immediately on normal fallback; this @@ -471,6 +487,7 @@ export function runFrontend( externalTypes, [...new Set(names)], transitive, + options, ); } catch (error) { for (const load of active) { @@ -492,6 +509,7 @@ function loadFrontend( externalTypes?: Readonly>, libraryNpmStatic: readonly string[] = [], transitive: Set = new Set(), + options: FrontendOptions = {}, ): Frontend { // Resolver package/workspace metadata is intentionally shared across the // several load attempts of ONE auto-detection fixpoint, but never across @@ -638,7 +656,7 @@ function loadFrontend( } const dropping = [...reasons.keys()].filter((p) => effective.has(p)); if (dropping.length === 0) break; - scanDroppedPackages(load, dropping, droppedScan); + if (options.nodeIslands === true) scanDroppedPackages(load, dropping, droppedScan); for (const p of dropping) { effective.delete(p); statuses.push({ package: p, status: "refused", detail: reasons.get(p)! }); @@ -690,13 +708,15 @@ function loadFrontend( probe.dispose(); if (probeDiags.some((d) => d.code === "SC0001")) dropWithNote(p); } - scanDroppedPackages(load, lastResortDropped.splice(0), droppedScan); + if (options.nodeIslands === true) + scanDroppedPackages(load, lastResortDropped.splice(0), droppedScan); load.dispose(); load = loadProgram(entryPath, { npmStatic: [...effective], externalTypes }); preflight = checkPreflight(load); if (preflight.some((d) => d.code === "SC0001") && effective.size > 0) { for (const p of [...effective]) dropWithNote(p); - scanDroppedPackages(load, lastResortDropped.splice(0), droppedScan); + if (options.nodeIslands === true) + scanDroppedPackages(load, lastResortDropped.splice(0), droppedScan); load.dispose(); load = loadProgram(entryPath, { npmStatic: [...effective], externalTypes }); preflight = checkPreflight(load); @@ -717,12 +737,20 @@ function loadFrontend( } const finalLoad = load; - // An unknown key or value in the project's scriptc configuration fails - // the build before anything else (SC8004). + // An unknown key or value in the project's scriptc configuration makes + // the Node island report unavailable (SC8004). The settings configure + // only that report: a build warns about them and goes on. const projectConfig = readProjectConfig(entryPath); const configDiagnostics = projectConfig.diagnostics; + const report = options.nodeIslands === true; + // Type patches that did not apply as written (SC8030–SC8033) warn; the + // build goes on with the packages' own declarations there. + const patchDiagnostics = typePatchDiagnostics()?.diagnostics ?? []; + const buildWarnings = [...(report ? [] : configDiagnostics), ...patchDiagnostics]; return { - preflight: configDiagnostics.length > 0 ? [...configDiagnostics, ...preflight] : preflight, + preflight: + report && configDiagnostics.length > 0 ? [...configDiagnostics, ...preflight] : preflight, + ...(buildWarnings.length > 0 ? { warnings: buildWarnings } : {}), entryText: () => finalLoad.entry.text, entryExports: () => entryFunctionExports(finalLoad.entry), // The contract scans the PROGRAM's source files, not the runtime @@ -757,9 +785,10 @@ function loadFrontend( ...finalLoad.program.getImplementationSourceFiles().filter((sf) => !sf.isDeclarationFile), ].map((sf) => [sf.fileName, sf.text]), ); - // SC8004 diagnostics point into the project's package.json. + // SC8004 and type patch diagnostics point into the project's + // package.json. if ( - configDiagnostics.length > 0 && + (configDiagnostics.length > 0 || patchDiagnostics.length > 0) && projectConfig.manifest !== null && projectConfig.manifestText !== null ) @@ -779,32 +808,38 @@ function loadFrontend( }); // The declaration-typed boundary's notes (SC6102) join the // performance warnings; they never fail a build. - const notes = finalLoad.declBoundaryNotes; - if (notes === undefined) return lowered; + const notes = [ + ...(finalLoad.declBoundaryNotes ?? []), + ...shapeOverlayReport(finalLoad.entry.fileName), + ]; + if (notes.length === 0) return lowered; return { ...lowered, performance: [...notes, ...(lowered.performance ?? [])] }; }, npmStatic: statuses, npmImportSites: npmSites, - nodeIslandScan: ({ userJs }) => { - const files = new Set([finalLoad.entry, ...finalLoad.moduleOrder]); - addDynamicImportGraph(finalLoad, files); - // Package edges matter only when the program has npm packages: one - // that loads, one that was refused, or one judged and dropped. - const npm = - statuses.length > 0 || - droppedScan.modules.length > 0 || - [...files].some((sf) => sf.fileName.includes("/node_modules/")); - const scanned = scanFiles(finalLoad.program, [...files], { - userJs, - dropped: false, - edges: npm, - }); - return { - modules: [...droppedScan.modules, ...scanned.modules], - edges: [...droppedScan.edges, ...scanned.edges], - refusals, - }; - }, + nodeIslandScan: + options.nodeIslands !== true + ? undefined + : ({ userJs }) => { + const files = new Set([finalLoad.entry, ...finalLoad.moduleOrder]); + addDynamicImportGraph(finalLoad, files); + // Package edges matter only when the program has npm packages: one + // that loads, one that was refused, or one judged and dropped. + const npm = + statuses.length > 0 || + droppedScan.modules.length > 0 || + [...files].some((sf) => sf.fileName.includes("/node_modules/")); + const scanned = scanFiles(finalLoad.program, [...files], { + userJs, + dropped: false, + edges: npm, + }); + return { + modules: [...droppedScan.modules, ...scanned.modules], + edges: [...droppedScan.edges, ...scanned.edges], + refusals, + }; + }, dispose: finalLoad.dispose, }; } diff --git a/packages/compiler/src/frontend/program-analysis.ts b/packages/compiler/src/frontend/program-analysis.ts index e49b9307e..b20f5d5e5 100644 --- a/packages/compiler/src/frontend/program-analysis.ts +++ b/packages/compiler/src/frontend/program-analysis.ts @@ -1,4 +1,5 @@ import type { + CallExpression, ExportDeclaration, Identifier, SourceFile, @@ -24,12 +25,27 @@ export class ProgramAnalysis { /** The modules of every import cycle the exact initialization rule admitted. */ readonly exactCycleMembers = new Set(); cycleEarlyBindings: EarlyBindingsMemo | null = null; + /** Whether some program file can name node:child_process (namesChildProcess). */ + namesChildProcess: boolean | null = null; moduleEarlyBindings: EarlyBindingsMemo | null = null; /** Top-level require declarations: the first reference to their bindings * that code running before the require can reach (requireEarlyReference). */ readonly requireEarlyReferences = new Map(); /** Per file: the first top-level statement that can run user code. */ readonly requireFirstRunnable = new Map(); + /** Per file: split require bindings (`var m; m = require("./x")`), from + * declaration to the require call of their one assignment + * (splitRequireBindings7). */ + readonly splitRequireBindings = new Map< + SourceFile, + ReadonlyMap + >(); + /** Per file: its identifiers by text, each with its position in the + * file's preorder walk (fileIdentifiers). */ + readonly fileIdentifiers = new Map< + SourceFile, + ReadonlyMap + >(); clear(): void { this.createRequireReasons.clear(); @@ -38,8 +54,11 @@ export class ProgramAnalysis { this.moduleSourceCandidates.clear(); this.exactCycleMembers.clear(); this.cycleEarlyBindings = null; + this.namesChildProcess = null; this.moduleEarlyBindings = null; this.requireEarlyReferences.clear(); this.requireFirstRunnable.clear(); + this.splitRequireBindings.clear(); + this.fileIdentifiers.clear(); } } diff --git a/packages/compiler/src/frontend/program.test.ts b/packages/compiler/src/frontend/program.test.ts index bdd6260ed..6a068353a 100644 --- a/packages/compiler/src/frontend/program.test.ts +++ b/packages/compiler/src/frontend/program.test.ts @@ -371,15 +371,60 @@ test("repeated cycle edges reuse a file's binding index", () => { test.for([ [ - // A shared instance built by a user constructor: the reads of it in - // the partner's function can run early; the class cannot. + // A shared instance built by a user constructor that calls the + // partner's function: its read of the instance can run early. + { + "main.ts": 'import { shared } from "./a.ts"; console.log(shared.n);', + "a.ts": + 'import { f } from "./b.ts"; export class C { n = 1; constructor() { console.log(f()); } } export const shared = new C();', + "b.ts": 'import { shared } from "./a.ts"; export function f(): number { return shared.n; }', + }, + ["shared"], + ], + [ + // User code runs while the instance is built, but nothing it reaches + // calls the partner's function before the declaration completed. { "main.ts": 'import { shared } from "./a.ts"; console.log(shared.n);', "a.ts": 'import { f } from "./b.ts"; export class C { n = 1; } export const shared = new C(); console.log(f());', "b.ts": 'import { shared } from "./a.ts"; export function f(): number { return shared.n; }', }, - ["shared"], + [], + ], + [ + // The partner's top level runs first and calls a hoisted function of + // the importer that reads a const declared later; a second function + // reads another const but nothing calls it early. + { + "main.ts": 'import { late } from "./a.ts"; console.log(late());', + "a.ts": + 'import { probe } from "./b.ts"; export function read(): string { return name; } export function late(): string { return other + probe; } export const name = "n"; export const other = "o";', + "b.ts": + 'import { read } from "./a.ts"; function attempt(): string { try { return read(); } catch { return "tdz"; } } export const probe = attempt();', + }, + ["name"], + ], + [ + // The partner's class calls a method through `this` in a static + // initializer; the method reads a const of the importer declared later. + { + "main.ts": 'import { name } from "./a.ts"; console.log(name);', + "a.ts": 'import { Probe } from "./b.ts"; export const name = "n"; console.log(Probe.value);', + "b.ts": + 'import { name } from "./a.ts"; export class Probe { static read(): string { return name; } static value = this.read(); }', + }, + ["name"], + ], + [ + // The same through a static block of a class stored by a const. + { + "main.ts": 'import { name } from "./a.ts"; console.log(name);', + "a.ts": 'import { Box } from "./b.ts"; export const name = "n"; console.log(Box.value);', + "b.ts": + 'import { name } from "./a.ts"; export const Box = class { static value = ""; static read(): string { return name; } static { this.value = this.read(); } };', + }, + ["name"], ], [ // The partner's top level reads the importer's const before it ran. diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index 932ac7054..58cdf219a 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -96,14 +96,16 @@ import type { import { configurePackageShims, isPackageShimFile, - isPackageShimName, isShimmedImport, + npmStaticOmitsPackage, } from "./package-shims.js"; import { provenanceEntryFor, provenancePaths } from "./provenance-registry.js"; import { cjsLexedExportsOfFile, cjsVisibleNames } from "./cjs-syntax.js"; import { ambientDtsPath, fallbackDtsPath, + fallbackModulesNamedIn, + isFallbackDtsPath, isNodeTypesPath, overridesDtsPath, tsgoPath, @@ -139,7 +141,19 @@ import { typeOnlyReachedFiles, } from "./type-only-imports.js"; import { moduleSourceCandidates } from "./module-source-candidates.js"; -import { planDeclProjection, type DeclProjection } from "./decl-projection.js"; +import { + planDeclProjection, + resetDeclBoundaryOrigins, + type DeclProjection, +} from "./decl-projection.js"; +import { configureTypePatches, packageSourceText } from "./type-patches/layer.js"; +import { resetDeclOriginVersions } from "./lowering/decl-origin.js"; +import { + reportShapes, + resetShapeOverlay, + shapeOverlayEnabled, + shapeOverlayer, +} from "./shape-inference/index.js"; import { inferredJsDiagnosticSuppressed } from "./inferred-js-diagnostics.js"; import { containsJsx, @@ -476,7 +490,7 @@ function visitNpmStaticDeclarationClosure( if (normalized !== packageRoot && !normalized.startsWith(packageRoot + "/")) return; if (seen.has(path)) return; seen.add(path); - const source = trackedReadFile(path); + const source = packageSourceText(path); if (source === null) return; onSource(path, source); for (const specifier of npmStaticDeclarationReexports(services.parse(path, source, "ts"))) { @@ -487,43 +501,33 @@ function visitNpmStaticDeclarationClosure( visit(entryPath); } -function npmStaticDeclarationOverloadsOf( +/** The overloads and properties of the classes a package's declaration + * closure declares, read from one parse of each declaration file. */ +function npmStaticDeclarationMetadataOf( services: FrontendServices, entryPath: string, -): NpmStaticDeclarationOverloads { - const classes = new Map>(); +): { overloads: NpmStaticDeclarationOverloads; properties: NpmStaticDeclarationProperties } { + const overloads = new Map>(); + const properties = new Map>(); visitNpmStaticDeclarationClosure(services, entryPath, (path, source) => { - for (const [className, methods] of parseNpmStaticDeclarationOverloads( - services.parse(path, source, "ts"), - )) { + const sourceFile = services.parse(path, source, "ts"); + for (const [className, methods] of parseNpmStaticDeclarationOverloads(sourceFile)) { const target = - classes.get(className) ?? new Map(); - classes.set(className, target); + overloads.get(className) ?? new Map(); + overloads.set(className, target); for (const [methodName, signatures] of methods) { if (!target.has(methodName)) target.set(methodName, signatures); } } - }); - return classes; -} - -function npmStaticDeclarationPropertiesOf( - services: FrontendServices, - entryPath: string, -): NpmStaticDeclarationProperties { - const classes = new Map>(); - visitNpmStaticDeclarationClosure(services, entryPath, (path, source) => { - for (const [className, properties] of parseNpmStaticDeclarationProperties( - services.parse(path, source, "ts"), - )) { - const target = classes.get(className) ?? new Map(); - classes.set(className, target); - for (const [propertyName, type] of properties) { + for (const [className, fields] of parseNpmStaticDeclarationProperties(sourceFile)) { + const target = properties.get(className) ?? new Map(); + properties.set(className, target); + for (const [propertyName, type] of fields) { if (!target.has(propertyName)) target.set(propertyName, type); } } }); - return classes; + return { overloads, properties }; } /** See LoadResult.startupCrash: Node's exact error message, the IR error @@ -621,6 +625,27 @@ function externalTypeFileClosure7( return new Map([...ownersByFile].map(([file, owners]) => [file, [...owners]] as const)); } +/** The fallback declaration files of the Node modules the program's files + * name (fallbackModulesNamedIn) that the program does not hold yet. Each + * file is read as tsgo reads it; the TypeScript libraries and scriptc's own + * declarations name no module that needs a file. */ +function fallbackModuleRoots7(program: ts.Program, host: ts.Ts7Host): string[] { + const names = program.getSourceFileNames(); + const held = new Set(names.filter(isFallbackDtsPath).map((name) => tsgoPath(name))); + const ambient = ambientDtsPath(); + const overrides = overridesDtsPath(); + const roots = new Set(); + for (const name of names) { + const path = tsgoPath(name); + if (held.has(path) || path === ambient || path === overrides) continue; + if (/\/lib\.[^/]*\.d\.ts$/.test(path) && program.getSourceFileMetadata(name)?.isDefaultLibrary) + continue; + for (const file of fallbackModulesNamedIn(host.sourceText(name) ?? "")) + if (!held.has(file)) roots.add(file); + } + return [...roots]; +} + /** Program roots hidden behind canonical createRequire bindings. tsgo * discovers ordinary import/require edges itself, but a source binding * returned by createRequire is intentionally just a function to the type @@ -673,6 +698,14 @@ function createRequireProgramRoots7(program: ts.Program): string[] { return [...roots].sort(); } +/** Whether the tsc gate can keep a file's diagnostics when the program + * loads: it drops all of an opted-in package's files and of the package + * shims (keptErrors in preflight also drops unselected package JavaScript, + * which is not known yet here, so those files are still checked). */ +function tscGateFileKept(fileName: string): boolean { + return npmStaticPackageOfPath(fileName) === null && !isPackageShimFile(fileName); +} + /** Classify the explicitly selected installed package's own sources. */ export function entryPackageFilePredicate(entryPath: string): (file: string) => boolean { const entryFile = tsgoPath(entryPath); @@ -856,6 +889,9 @@ function loadProgram7( /** The declaration-typed boundary's projection (decl-projection.ts): * answers rewritten user files for the lowering world, or null. */ projector?: (program: ts.Program) => DeclProjection | null, + /** Shape inference's typed overlay (shape-inference/overlay.ts): + * rewritten package JavaScript for the lowering world, or null. */ + overlayer?: (program: ts.Program) => ReadonlyMap | null, ): LoadResult & { disposeAll: () => void } { const config = adoptProjectConfig7(host, entryPath); const configuredPaths = config.options["paths"]; @@ -897,12 +933,23 @@ function loadProgram7( } options = { ...options, paths }; } - const coreRoots = [entryPath, ambientDtsPath(), nodeTypes ?? fallbackDtsPath()]; + // Without @types/node the fallback declarations stand in: the core file, + // and a Node module's file once a program file names the module + // (fallbackModuleRoots7). The modules the entry names join the first + // program. + const coreRoots = [ + entryPath, + ambientDtsPath(), + ...(nodeTypes === null + ? [fallbackDtsPath(), ...fallbackModulesNamedIn(host.sourceText(entryPath) ?? "")] + : [nodeTypes]), + ]; const programRoots = [...coreRoots]; let program = host.createProgram([...programRoots, overridesDtsPath()], options); try { for (let pass = 0; pass < 32; pass++) { const candidates = [ + ...(nodeTypes === null ? fallbackModuleRoots7(program, host) : []), ...entryPackageProgramRoots7(program, entryPath, rootNpmStaticEdges), ...createRequireProgramRoots7(program), ...forkTargetPaths(program, program.getImplementationSourceFiles()), @@ -942,7 +989,7 @@ function loadProgram7( const original = program; // The project world must read the original text too, so it is built // before the rewritten files are served, and only when it is needed. - const diagnostics = ts.getPreEmitDiagnostics(original); + const diagnostics = ts.getPreEmitDiagnostics(original, tscGateFileKept); const errors = diagnostics.some((d) => d.category === ts.DiagnosticCategory.Error); tscWorld = { program: original, @@ -990,13 +1037,33 @@ function loadProgram7( } else { tscWorld = { program: original, - diagnostics: ts.getPreEmitDiagnostics(original), + diagnostics: ts.getPreEmitDiagnostics(original, tscGateFileKept), projectWorld: null, declWorldFallback: true, }; } } } + // Shape inference's overlay: package files typed from proven facts. The + // tsc gate keeps reading the files as written. + // A load without the overlay must not see an earlier load's overlay + // files (the registry is per process). + resetShapeOverlay(); + if (overlayer !== undefined && jsx.refusal === null) { + const overlay = overlayer(program); + if (overlay !== null && overlay.size > 0) { + const before = program; + for (const [file, text] of overlay) host.addVirtualFile(file, text); + program = host.createProgram([...programRoots, overridesDtsPath()], options); + if (tscWorld === undefined) + tscWorld = { + program: before, + diagnostics: ts.getPreEmitDiagnostics(before), + projectWorld: null, + }; + else if (tscWorld.program !== before) before.dispose(); + } + } const entry = program.getSourceFile(entryPath); if (!entry) throw new Error(`could not load ${entryPath}`); const externalTypeSpecifiersByFile = externalTypeFileClosure7(program, externalTypes); @@ -1072,12 +1139,18 @@ export function loadProgram( } externalTypes.set(specifier, declarationPath); } - // Packages scriptc implements itself or the project marks never loaded - // (package-shims.ts) never compile their published JavaScript, even when - // named. + // Packages scriptc implements itself, and packages the project marks + // never loaded (package-shims.ts), never compile their published + // JavaScript, even when named. A registry entry's `sourceVersions` + // (chalk 6) compile from their own source. configurePackageShims(entryPath); + resetDeclBoundaryOrigins(); + resetDeclOriginVersions(); + // The project's type patches (type-patches/layer.ts), read by every host + // this load creates; nothing at all without `scriptc.typePatches`. + configureTypePatches(entryPath, (path, text, kind) => services.parse(path, text, kind)); const npmStaticPackages = [...new Set(opts?.npmStatic ?? [])].filter( - (name) => !isPackageShimName(name), + (name) => !npmStaticOmitsPackage(entryPath, name), ); // Resolve authored declaration entries before enabling the JS-only // package resolver. Safe overload metadata binds through the runtime @@ -1095,8 +1168,7 @@ export function loadProgram( ) { continue; } - const overloads = npmStaticDeclarationOverloadsOf(services, resolved.typesFile); - const properties = npmStaticDeclarationPropertiesOf(services, resolved.typesFile); + const { overloads, properties } = npmStaticDeclarationMetadataOf(services, resolved.typesFile); const classNames = new Set([...overloads.keys(), ...properties.keys()]); if (classNames.size === 0) continue; const runtime = resolveBareModule(entryPath, pkg, "js-only"); @@ -1259,7 +1331,12 @@ export function loadProgram( externalTypes, npmStaticPackages.length > 0, projector, + npmStaticPackages.length > 0 && shapeOverlayEnabled() ? shapeOverlayer : undefined, ); + // Shape inference, stage S1: a report only (SCRIPTC_DEBUG_SHAPES). + const shapes = process.env["SCRIPTC_DEBUG_SHAPES"]; + if (shapes !== undefined && shapes !== "" && shapes !== "0" && npmStaticPackages.length > 0) + reportShapes(load.program, shapes); return { ...load, services, @@ -1296,10 +1373,9 @@ export function loadProgram( * packages whose declarations the boundary projects from. */ function declBoundaryUserFiles(program: ts.Program, packages: readonly string[]): string[] { const out: string[] = []; - for (const sf of program.getSourceFiles()) { + for (const sf of program.getImplementationSourceFiles()) { const name = sf.fileName.split("\\").join("/"); - if (sf.isDeclarationFile || name.includes("/node_modules/") || !/\.[mc]?tsx?$/.test(name)) - continue; + if (name.includes("/node_modules/") || !/\.[mc]?tsx?$/.test(name)) continue; if (name.endsWith(".tsx")) continue; // JSX files: not projected yet const text = sf.text; if ( @@ -1378,6 +1454,18 @@ function suppressedJsStrictness7(d: ts.Diagnostic): boolean { if (JS_ANY_OPERATOR_CODES.has(d.code)) { return ts.flattenDiagnosticMessageText(d, "\n").includes("'any'"); } + // The deprecated Buffer(...) call without `new`, which Node's Buffer + // function accepts (containers/bytes.ts lowers it), and fs.ReadStream(...) + // and fs.WriteStream(...), which construct their stream + // (builtins/fs-streams.ts). + if (d.code === 2348) { + const text = ts.flattenDiagnosticMessageText(d, "\n"); + return ( + text.includes("'BufferConstructor'") || + text.includes("'typeof ReadStream'") || + text.includes("'typeof WriteStream'") + ); + } return false; } @@ -1418,17 +1506,31 @@ function isRequireStatement7(stmt: ts.Statement): boolean { ); } +/** A require() occurrence of a top-level statement. `split` marks the + * assignment of a split require binding (splitRequireBindings7): `node` + * is its require call and `decl` the binding's declaration. */ +interface RequireOccurrence7 { + spec: string; + node: ts.Node; + decl: ts.VariableDeclaration | null; + split?: true; +} + /** The require() occurrences of one top-level statement: the declaration - * forms and the bare side-effect call. Empty for everything else. */ -function requiresOf7( - stmt: ts.Statement, -): { spec: string; node: ts.Node; decl: ts.VariableDeclaration | null }[] { + * forms, the bare side-effect call, and the assignment of a split require + * binding. Empty for everything else. */ +function requiresOf7(stmt: ts.Statement, program: ts.Program): RequireOccurrence7[] { if (ts.isExpressionStatement(stmt)) { + const split = splitRequireOfStatement7(program, stmt); + if (split !== null) + return [ + { spec: requireSpecOf7(split.call)!, node: split.call, decl: split.decl, split: true }, + ]; const spec = requireSpecOf7(stmt.expression); return spec !== null ? [{ spec, node: stmt.expression, decl: null }] : []; } if (!ts.isVariableStatement(stmt)) return []; - const out: { spec: string; node: ts.Node; decl: ts.VariableDeclaration | null }[] = []; + const out: RequireOccurrence7[] = []; for (const decl of stmt.declarationList.declarations.nodes) { const spec = decl.initializer !== undefined ? requireSpecOf7(decl.initializer) : null; if (spec !== null) out.push({ spec, node: decl, decl }); @@ -1436,6 +1538,172 @@ function requiresOf7( return out; } +/** CommonJS require bindings split into a declaration and one later + * assignment (`var m; m = require("./x")`): a top-level `var` or `let` + * declaration without initializer in a CommonJS file, assigned by exactly one + * top-level `m = require("")` statement of the require global over + * a compiled program module, and written nowhere else. The binding holds the + * module's exports from the assignment on, as `const m = require("./x")` + * does, so preflight treats the assignment as that require: its module edge, + * its cycle admission and its early-read fence. The lowering's recognizer + * (splitRequireBindingOf in cjs-inline-require.ts) accepts only these + * bindings. Keyed by declaration, with the assignment's require call. */ +function splitRequireBindings7( + program: ts.Program, + sf: ts.SourceFile, +): ReadonlyMap { + const memo = program.analysis.splitRequireBindings; + const cached = memo.get(sf); + if (cached !== undefined) return cached; + const out = new Map(); + memo.set(sf, out); + if (sf.isDeclarationFile || !isCjsJsFile7(sf, program) || !sf.text.includes("require")) + return out; + const checker = program.getTypeChecker(); + const stmts = sf.statements.nodes; + const candidates = new Map< + ts.Symbol, + { decl: ts.VariableDeclaration; call: ts.CallExpression | null; assigns: number } + >(); + for (const stmt of stmts) { + if (!ts.isVariableStatement(stmt)) continue; + if ((stmt.declarationList.flags & (ts.NodeFlags.Const | ts.NodeFlags.Using)) !== 0) continue; + for (const decl of stmt.declarationList.declarations.nodes) { + if (!ts.isIdentifier(decl.name) || decl.initializer !== undefined) continue; + const symbol = checker.getSymbolAtLocation(decl.name); + if (symbol === undefined || symbol.declarations.length !== 1) continue; + candidates.set(symbol, { decl, call: null, assigns: 0 }); + } + } + if (candidates.size === 0) return out; + // The one top-level `m = ...` statement, whose right side is the require. + for (const stmt of stmts) { + if (!ts.isExpressionStatement(stmt) || !ts.isBinaryExpression(stmt.expression)) continue; + const assign = stmt.expression; + if (assign.operatorToken.kind !== ts.SyntaxKind.EqualsToken || !ts.isIdentifier(assign.left)) + continue; + const symbol = checker.getSymbolAtLocation(assign.left); + const candidate = symbol !== undefined ? candidates.get(symbol) : undefined; + if (candidate === undefined) continue; + candidate.assigns++; + let right = assign.right; + while (ts.isParenthesizedExpression(right)) right = right.expression; + if (ts.isCallExpression(right) && splitRequireTarget7(program, sf, right)) + candidate.call = right; + } + const names = new Set(); + for (const candidate of candidates.values()) { + if (candidate.assigns === 1 && candidate.call !== null) + names.add((candidate.decl.name as ts.Identifier).text); + } + if (names.size === 0) return out; + // Exactly one write in the whole file: the assignment itself. + const writes = new Map(); + ts.walkPreorder(sf, (node) => { + if (!ts.isIdentifier(node) || !names.has(node.text)) return undefined; + const parent = node.parent; + const symbol = + parent !== undefined && ts.isShorthandPropertyAssignment(parent) && parent.name === node + ? (checker.getShorthandAssignmentValueSymbol(parent) ?? checker.getSymbolAtLocation(node)) + : checker.getSymbolAtLocation(node); + if (symbol === undefined || !candidates.has(symbol) || !isWriteReference7(node)) + return undefined; + writes.set(symbol, (writes.get(symbol) ?? 0) + 1); + return undefined; + }); + for (const [symbol, candidate] of candidates) { + if (candidate.assigns === 1 && candidate.call !== null && writes.get(symbol) === 1) + out.set(candidate.decl, candidate.call); + } + return out; +} + +/** True when `call` is a require of the CommonJS global over a compiled + * program module: not a builtin, a JSON document or a declaration file. */ +function splitRequireTarget7( + program: ts.Program, + sf: ts.SourceFile, + call: ts.CallExpression, +): boolean { + const spec = requireSpecOf7(call); + if (spec === null || canonicalBuiltinModule(spec) !== null) return false; + if (requireCallBindingKind7(program, call) !== "ambient") return false; + const dep = + resolveImport7(program, sf, spec, "require") ?? npmStaticDepSf7(program, sf, spec, "require"); + return dep !== null && !dep.isDeclarationFile && !dep.fileName.endsWith(".json"); +} + +/** The split require binding a top-level statement assigns, or null. */ +function splitRequireOfStatement7( + program: ts.Program, + stmt: ts.Statement, +): { decl: ts.VariableDeclaration; call: ts.CallExpression } | null { + if ( + !ts.isExpressionStatement(stmt) || + !ts.isBinaryExpression(stmt.expression) || + !ts.isSourceFile(stmt.parent) + ) + return null; + let right = stmt.expression.right; + while (ts.isParenthesizedExpression(right)) right = right.expression; + for (const [decl, call] of splitRequireBindings7(program, stmt.parent)) + if (call === right) return { decl, call }; + return null; +} + +/** The top-level statement of a split require binding's assignment, from + * its require call (`m = require("./x")` or `m = (require("./x"))`). */ +function splitRequireStatementOf7(call: ts.CallExpression): ts.Node | undefined { + let node: ts.Node = call; + while (node.parent !== undefined && ts.isParenthesizedExpression(node.parent)) node = node.parent; + return node.parent?.parent; +} + +/** The require call of a split require binding's declaration, or null. */ +function splitRequireCallOf7( + program: ts.Program, + decl: ts.VariableDeclaration, +): ts.CallExpression | null { + return splitRequireBindings7(program, ts.getSourceFileOfNode(decl)).get(decl) ?? null; +} + +/** True when `id` is written: an assignment target (compound and + * destructuring included), an update operand, or a for-in/of target. */ +function isWriteReference7(id: ts.Identifier): boolean { + let n: ts.Node = id; + for (;;) { + const p: ts.Node | undefined = n.parent; + if (p === undefined) return false; + if (ts.isBinaryExpression(p)) { + const k = p.operatorToken.kind; + return ( + p.left === n && k >= ts.SyntaxKind.FirstAssignment && k <= ts.SyntaxKind.LastAssignment + ); + } + if (ts.isPostfixUnaryExpression(p) || ts.isPrefixUnaryExpression(p)) { + return ( + (p.operator === ts.SyntaxKind.PlusPlusToken || + p.operator === ts.SyntaxKind.MinusMinusToken) && + p.operand === n + ); + } + if (ts.isForOfStatement(p) || ts.isForInStatement(p)) return p.initializer === n; + if ( + ts.isParenthesizedExpression(p) || + ts.isArrayLiteralExpression(p) || + ts.isSpreadElement(p) || + ts.isSpreadAssignment(p) || + ts.isShorthandPropertyAssignment(p) || + ts.isObjectLiteralExpression(p) || + (ts.isPropertyAssignment(p) && p.initializer === n) + ) { + n = p; + continue; + } + return false; + } +} + /** `true`, or `!0`, its minified spelling. */ function isTrueLiteral(expr: ts.Expression): boolean { return ( @@ -1575,7 +1843,7 @@ function purePrefixDecl7(decl: ts.VariableDeclaration): boolean { function purePrefixStmt7(program: ts.Program, s: ts.Statement): boolean { if (ts.isEmptyStatement(s) || ts.isFunctionDeclaration(s)) return true; if (ts.isExpressionStatement(s) && ts.isStringLiteral(s.expression)) return true; // directive - if (isRequireStatement7(s)) return true; + if (isRequireStatement7(s) || splitRequireOfStatement7(program, s) !== null) return true; if (isCjsJsFile7(ts.getSourceFileOfNode(s), program)) { if (ts.isExpressionStatement(s) && isEsModuleStamp(s.expression)) { const call = s.expression as ts.CallExpression; @@ -1702,12 +1970,20 @@ function requireEarlyReference7( const cached = memo.get(decl); if (cached !== undefined) return cached; let found: ts.Identifier | null = null; - const stmt = decl.parent?.parent; const sf = ts.getSourceFileOfNode(decl); - if (stmt !== undefined && ts.isVariableStatement(stmt) && ts.isSourceFile(stmt.parent)) { + // A split require binding is bound at its assignment statement. + const splitCall = splitRequireCallOf7(program, decl); + const stmt = splitCall !== null ? splitRequireStatementOf7(splitCall) : decl.parent?.parent; + if ( + stmt !== undefined && + (ts.isVariableStatement(stmt) || splitCall !== null) && + ts.isSourceFile(stmt.parent) + ) { const stmts = sf.statements.nodes; - const k = stmts.indexOf(stmt); - const declarations = stmt.declarationList.declarations.nodes; + const k = stmts.indexOf(stmt as ts.Statement); + const declarations = ts.isVariableStatement(stmt) + ? stmt.declarationList.declarations.nodes + : []; // Earlier declarators run too: `const x = read(), dep = require('./dep')`. const precedingDecls = declarations.slice(0, declarations.indexOf(decl)); let firstRunnable = program.analysis.requireFirstRunnable.get(sf); @@ -1760,6 +2036,12 @@ export function requireBindingEarlyUse7( if (sym) bindings.add(sym); } if (bindings.size === 0) return null; + // A split require binding's assignment writes the binding; it reads nothing. + let splitWrite: ts.Node | null = splitRequireCallOf7(program, decl); + while (splitWrite !== null && ts.isParenthesizedExpression(splitWrite.parent)) + splitWrite = splitWrite.parent; + splitWrite = + splitWrite !== null && ts.isBinaryExpression(splitWrite.parent) ? splitWrite.parent.left : null; const stmts = sf.statements.nodes; const cjs = isCjsJsFile7(sf, program); const symbolOf = (id: ts.Identifier): ts.Symbol | undefined => { @@ -1983,7 +2265,7 @@ export function requireBindingEarlyUse7( const sym = symbolOf(n); if (sym === undefined) return undefined; if (bindings.has(sym)) { - if (exactEarlyRead?.(n) === true) return undefined; + if (n === splitWrite || exactEarlyRead?.(n) === true) return undefined; hit = n; return "stop"; } @@ -2320,12 +2602,39 @@ export function isCreateRequireBinding7(program: ts.Program, callee: ts.Identifi ); } -/** Only literal calls and literal resolution queries can erase the loader - * value. In particular, assignments, exported aliases, shorthand properties, - * optional calls, and passing the loader to user code cannot pass this test. */ -function staticCreateRequireUse7(expr: ts.Expression): boolean { +/** How a use of a createRequire loader lowers: + * - "static": a literal call or a literal resolution query, which erases + * the loader value. + * - "runtime": in JavaScript source, a call with a computed specifier, a + * require.resolve call with a computed request, or `resolve` read as a + * value. The runtime resolves the specifier like Node + * (module-resolution.ts, scr_require.c). + * - "deferred": in JavaScript source, inside the try block of a try + * statement in the same function, the other shapes: spread arguments, + * require.resolve with another argument count, and `resolve.paths` with + * a computed request or read as a value. The call throws a runtime + * fence, and the resolve.paths value is a function that throws one when + * called (module-resolution.ts), so the statement's catch clause handles + * them (packages that look for optional native addons). + * - "computed": the "runtime" uses in TypeScript source, and the + * "deferred" shapes outside a try block. + * - "escaping": the loader itself as a value, writes, optional calls, and + * other members. */ +const COMPUTED_CREATE_REQUIRE_REASON = + "its createRequire result is used with a computed specifier in TypeScript, or with spread arguments or resolve.paths outside a try block (static requires need literal specifiers)"; + +type CreateRequireUse7 = "static" | "runtime" | "deferred" | "computed" | "escaping"; + +function createRequireUse7(expr: ts.Expression): CreateRequireUse7 { + const use = createRequireUseShape7(expr); + return use === "runtime" && !isJsSourceFile(ts.getSourceFileOfNode(expr)) ? "computed" : use; +} + +function createRequireUseShape7(expr: ts.Expression): CreateRequireUse7 { let use: ts.Node = expr; while (ts.isParenthesizedExpression(use.parent)) use = use.parent; + let member = false; + let paths = false; if ( ts.isPropertyAccessExpression(use.parent) && use.parent.expression === use && @@ -2333,24 +2642,62 @@ function staticCreateRequireUse7(expr: ts.Expression): boolean { !use.parent.questionDotToken ) { use = use.parent; + member = true; if ( ts.isPropertyAccessExpression(use.parent) && use.parent.expression === use && use.parent.name.text === "paths" && !use.parent.questionDotToken - ) + ) { use = use.parent; + paths = true; + } } const call = use.parent; + if (ts.isCallExpression(call) && call.expression === use) { + if (call.questionDotToken) return "escaping"; + const args = call.arguments.nodes; + if (args.length === 1 && ts.isStringLiteralLike(args[0]!)) return "static"; + if ( + !paths && + !args.some((arg) => ts.isSpreadElement(arg)) && + !(member && (args.length === 0 || args.length > 2)) + ) + return "runtime"; + } else if (!member || isAssignmentTarget7(use)) { + return "escaping"; + } else if (!paths) { + return "runtime"; + } + return insideTryBlock7(use) ? "deferred" : "computed"; +} + +function isAssignmentTarget7(node: ts.Node): boolean { + const parent = node.parent; return ( - ts.isCallExpression(call) && - call.expression === use && - !call.questionDotToken && - call.arguments.nodes.length === 1 && - ts.isStringLiteralLike(call.arguments.nodes[0]!) + ts.isBinaryExpression(parent) && + parent.left === node && + parent.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && + parent.operatorToken.kind <= ts.SyntaxKind.LastAssignment ); } +/** True when `node` is inside the try block of a try statement in the same + * function (a catch clause or finally block does not count). A class field + * initializer runs when the class is instantiated, so it is a boundary too. */ +function insideTryBlock7(node: ts.Node): boolean { + for (let child = node, parent = node.parent; parent; child = parent, parent = parent.parent) { + if (ts.isTryStatement(parent) && parent.tryBlock === child) return true; + if ( + ts.isFunctionLike(parent) || + ts.isClassStaticBlockDeclaration(parent) || + ts.isPropertyDeclaration(parent) + ) + return false; + } + return false; +} + function stableCreateRequireBindingReason7( program: ts.Program, decl: ts.VariableDeclaration, @@ -2383,9 +2730,10 @@ function stableCreateRequireBindingReason7( : checker.getSymbolAtLocation(ref); if (target && (target.flags & ts.SymbolFlags.Alias) !== 0) target = checker.getAliasedSymbol(target); - if (target === symbol && !staticCreateRequireUse7(ref)) { - return "its createRequire binding is reassigned, escapes, or uses a computed specifier"; - } + if (target !== symbol) continue; + const use = createRequireUse7(ref); + if (use === "computed") return COMPUTED_CREATE_REQUIRE_REASON; + if (use === "escaping") return "its createRequire binding is reassigned or escapes"; } const preceding = stmt.declarationList.declarations.nodes.slice( 0, @@ -2448,10 +2796,10 @@ function npmStaticModuleImportReason7( checker.prefetchSymbolNodesExact(identifierOccurrences7(sf, specifier.name.text)); let reason: string | null = null; const checkUse = (expr: ts.Expression): void => { - if (!staticCreateRequireUse7(expr)) { - reason = - "its createRequire result escapes or is used with a computed specifier (static requires need literal specifiers)"; - } + const use = createRequireUse7(expr); + if (use === "computed") reason = COMPUTED_CREATE_REQUIRE_REASON; + else if (use === "escaping") + reason = "its createRequire result escapes (static requires need literal specifiers)"; }; ts.walkPreorder(sf, (node) => { if ( @@ -3494,7 +3842,7 @@ function cjsCycleRefusal7( } // Bindings of other members must stay unreachable from top-level code. for (const st of stmts) { - for (const req of requiresOf7(st)) { + for (const req of requiresOf7(st, program)) { if (req.decl === null) continue; const dep = resolveImport7(program, m, req.spec, "require"); if (dep === null || !members.has(dep)) continue; @@ -3728,10 +4076,14 @@ function exactCycleRefusal7( if (ts.isExportAssignment(stmt) && !stmt.isExportEquals) { return `the default export expression at ${lineOf(stmt)} has no initialization state inside the cycle`; } + // `export * from` binds each name to the declaring member's own + // binding, like a named re-export, so only `export * as ns` (a + // namespace object of a member) has state of its own. if ( ts.isExportDeclaration(stmt) && !stmt.isTypeOnly && - (stmt.exportClause === undefined || ts.isNamespaceExport(stmt.exportClause)) + stmt.exportClause !== undefined && + ts.isNamespaceExport(stmt.exportClause) ) { const dep = targetOf(stmt); if (dep !== null && members.has(dep)) { @@ -3903,22 +4255,7 @@ function computeCycleEarlyBindings7( const evaluation = known ? comp.slice().sort((a, b) => position.get(a)! - position.get(b)!) : comp.slice(); - interface Candidate { - symbol: ts.Symbol; - name: ts.Identifier; - kind: CycleEarlyBinding["kind"]; - module: ts.SourceFile; - /** User code can run between the cycle's start and the end of the - * declaration (always true when the order is unknown). */ - exposed: boolean; - rank: number; - start: number; - end: number; - reference: ts.Identifier | null; - folded: ts.Identifier | null; - localNames: Map; - } - const candidates = new Map(); + const candidates = new Map(); const names = new Set(); let dirty = !known; evaluation.forEach((m, rank) => { @@ -3955,6 +4292,7 @@ function computeCycleEarlyBindings7( start: ts.tokenStartOfNode(stmt), end: stmt.end, reference: null, + deferredReference: null, folded: null, localNames: new Map(), }); @@ -4023,15 +4361,27 @@ function computeCycleEarlyBindings7( ref.end <= c.end ) continue; + if (inDeferredPosition7(ref)) { + if (c.exposed && c.deferredReference === null) c.deferredReference = ref; + continue; + } let early: boolean; - if (inDeferredPosition7(ref)) early = c.exposed; - else if (sf === c.module) early = ts.tokenStartOfNode(ref) < c.start; + if (sf === c.module) early = ts.tokenStartOfNode(ref) < c.start; else if (!memberSet.has(sf) || !rankOf.has(sf)) early = false; else early = !known || rankOf.get(sf)! < c.rank; if (early) c.reference = ref; } } + // With a static evaluation order, a deferred reference only runs early + // when the code that runs before the declaration completes can reach + // the function holding it. + const reached = + known && [...candidates.values()].some((c) => c.reference === null && c.deferredReference) + ? cycleReachedDeferredReferences7(program, evaluation, candidates) + : null; for (const c of candidates.values()) { + if (c.reference === null && c.deferredReference !== null) + c.reference = reached === null ? c.deferredReference : (reached.get(c.symbol) ?? null); if (c.reference === null) continue; const settled = new Set(order.filter((sf) => !rankOf.has(sf))); if (known) for (const m of evaluation.slice(c.rank + 1)) settled.add(m); @@ -4050,6 +4400,299 @@ function computeCycleEarlyBindings7( return out; } +/** Collects into `units` the callables an initializer expression stores + * (see moduleEarlyBindingsOf7), recording in `immediate` the parts of a + * stored node that run where it is evaluated. */ +function storedCallables7( + checker: ts.TypeChecker, + e: ts.Expression, + units: ts.Node[], + immediate: Map, +): void { + while ( + ts.isParenthesizedExpression(e) || + ts.isAsExpression(e) || + ts.isSatisfiesExpression(e) || + ts.isNonNullExpression(e) + ) + e = e.expression; + if (ts.isArrowFunction(e) || ts.isFunctionExpression(e)) { + units.push(e); + } else if (ts.isClassExpression(e)) { + units.push(e); + immediate.set(e, classSyncParts7(e)); + } else if (ts.isObjectLiteralExpression(e)) { + for (const p of e.properties.nodes) { + if ( + ts.isMethodDeclaration(p) || + ts.isGetAccessorDeclaration(p) || + ts.isSetAccessorDeclaration(p) + ) { + // A computed key evaluates where the literal does; a plain + // member-chain key (`[Equal.symbol]`) runs no user code. + if (!ts.isComputedPropertyName(p.name)) units.push(p); + else if (memberChainKey7(p.name.expression)) { + units.push(p); + immediate.set(p, [p.name]); + } + } else if (ts.isPropertyAssignment(p)) + storedCallables7(checker, p.initializer, units, immediate); + } + } else if (ts.isArrayLiteralExpression(e)) { + for (const el of e.elements.nodes) + if (!ts.isSpreadElement(el)) storedCallables7(checker, el, units, immediate); + } else if (ts.isCallExpression(e)) { + // `const f = wrap(callback)` where wrap only returns closures over + // the callback: it runs when the stored result is called. + e.arguments.nodes.forEach((arg, index) => { + let fn: ts.Expression = arg; + while (ts.isParenthesizedExpression(fn)) fn = fn.expression; + if ( + (ts.isArrowFunction(fn) || ts.isFunctionExpression(fn)) && + returnsOnlyClosuresOver7(checker, e.expression, index) + ) + units.push(fn); + }); + } +} + +/** A binding of an import cycle and what the reference scan found. */ +interface CycleCandidate7 { + symbol: ts.Symbol; + name: ts.Identifier; + kind: CycleEarlyBinding["kind"]; + module: ts.SourceFile; + /** User code can run between the cycle's start and the end of the + * declaration (always true when the order is unknown). */ + exposed: boolean; + rank: number; + start: number; + end: number; + /** A reference read at top level before the declaration ran. */ + reference: ts.Identifier | null; + /** A deferred reference (function body, parameter default, instance + * field) when user code may run before the declaration completes. */ + deferredReference: ts.Identifier | null; + folded: ts.Identifier | null; + localNames: Map; +} + +/** For one import cycle with a static evaluation order: the first deferred + * reference to each candidate that code running before the candidate's + * declaration completes can reach, or null when the walk cannot be precise + * (top-level await, a default-exported function or class declaration). + * + * The cycle's top-level code runs in evaluation order. A stored callable + * (a function or class declaration, or a function, class or object-literal + * method stored by a variable initializer) becomes reachable once + * reachable code names its binding, through an import alias or a + * namespace member too; every other function-like in reachable code counts + * as reachable, since it may be handed to a callee. A namespace import of + * a cycle member used other than for a member read makes every stored + * callable reachable. moduleEarlyBindingsOf7 is the single-module form. */ +function cycleReachedDeferredReferences7( + program: ts.Program, + evaluation: readonly ts.SourceFile[], + candidates: ReadonlyMap, +): Map | null { + const checker = program.getTypeChecker(); + const members = new Set(evaluation); + for (const sf of evaluation) { + if (hasTopLevelAwait7(sf)) return null; + for (const stmt of sf.statements.nodes) { + if ( + (ts.isFunctionDeclaration(stmt) || ts.isClassDeclaration(stmt)) && + (ts.getCombinedModifierFlags(stmt as unknown as ts.Declaration) & + ts.ModifierFlags.Default) !== + 0 + ) + return null; + } + } + const stored = new Set(); + const immediate = new Map(); + const storedNames: ts.Identifier[] = []; + const unitsOfName = new Map(); + const store = (name: ts.Identifier, units: ts.Node[]): void => { + if (units.length === 0) return; + for (const u of units) stored.add(u); + storedNames.push(name); + unitsOfName.set(name, units); + }; + for (const sf of evaluation) { + for (const stmt of sf.statements.nodes) { + if ( + (ts.getCombinedModifierFlags(stmt as unknown as ts.Declaration) & + ts.ModifierFlags.Ambient) !== + 0 + ) + continue; + if (ts.isFunctionDeclaration(stmt) && stmt.name !== undefined && stmt.body !== undefined) { + store(stmt.name, [stmt]); + } else if (ts.isClassDeclaration(stmt) && stmt.name !== undefined) { + store(stmt.name, [stmt]); + } else if (ts.isVariableStatement(stmt)) { + for (const d of stmt.declarationList.declarations.nodes) { + if (ts.isIdentifier(d.name) && d.initializer !== undefined) { + const units: ts.Node[] = []; + storedCallables7(checker, d.initializer, units, immediate); + store(d.name, units); + } + } + } + } + } + const unitsBySymbol = new Map(); + const allUnits: ts.Node[] = []; + for (const name of storedNames) { + const symbol = checker.getSymbolAtLocation(name); + if (symbol === undefined) continue; + const units = unitsOfName.get(name)!; + unitsBySymbol.set(symbol, [...(unitsBySymbol.get(symbol) ?? []), ...units]); + allUnits.push(...units); + } + // A named class expression's inner name (`class Impl { static x = + // Impl.m(); }`) names the class inside its own body, as the binding it + // is stored in names it outside. + const innerNames: ts.Identifier[] = []; + for (const unit of allUnits) { + if (!ts.isClassExpression(unit) || unit.name === undefined) continue; + const symbol = checker.getSymbolAtLocation(unit.name); + if (symbol === undefined) continue; + innerNames.push(unit.name); + unitsBySymbol.set(symbol, [...(unitsBySymbol.get(symbol) ?? []), unit]); + } + // The identifier texts worth resolving: declared names, and the local + // names of imports (named, default and namespace) in cycle members. + const names = new Set(); + for (const c of candidates.values()) names.add(c.name.text); + for (const name of storedNames) names.add(name.text); + for (const name of innerNames) names.add(name.text); + for (const sf of evaluation) { + for (const stmt of sf.statements.nodes) { + if (!ts.isImportDeclaration(stmt) || stmt.importClause === undefined) continue; + const clause = stmt.importClause; + if (clause.name !== undefined) names.add(clause.name.text); + const bindings = clause.namedBindings; + if (bindings === undefined) continue; + if (ts.isNamedImports(bindings)) + for (const element of bindings.elements.nodes) names.add(element.name.text); + else names.add(bindings.name.text); + } + } + const idents: ts.Identifier[] = []; + for (const sf of evaluation) { + ts.walkPreorder(sf, (node) => { + if (ts.isImportDeclaration(node)) return "skip"; + if (ts.isIdentifier(node) && names.has(node.text)) idents.push(node); + return undefined; + }); + } + checker.prefetchSymbolNodesExact(idents); + const symbolOf = (id: ts.Identifier): ts.Symbol | undefined => { + const parent = id.parent; + let symbol = checker.getSymbolAtLocation(id); + if (parent !== undefined && ts.isShorthandPropertyAssignment(parent) && parent.name === id) { + symbol = checker.getShorthandAssignmentValueSymbol(parent) ?? symbol; + } + if (symbol !== undefined && symbol.flags & ts.SymbolFlags.Alias) { + symbol = checker.getAliasedSymbol(symbol); + } + return symbol; + }; + const isMemberNamespace = (symbol: ts.Symbol): boolean => + (symbol.flags & ts.SymbolFlags.ValueModule) !== 0 && + symbol.declarations.some((d) => ts.isSourceFile(d) && members.has(d)); + const declared = new Set(); + const reachedUnits = new Set(); + const out = new Map(); + const work: ts.Node[] = []; + const reach = (unit: ts.Node): void => { + if (reachedUnits.has(unit)) return; + reachedUnits.add(unit); + work.push(unit); + }; + const visit = (root: ts.Node): void => { + ts.walkPreorder(root, (node) => { + if (node !== root && stored.has(node)) { + for (const part of immediate.get(node) ?? []) visit(part); + return "skip"; + } + if (ts.isImportDeclaration(node)) return "skip"; + if (node.kind === ts.SyntaxKind.ThisKeyword) { + // `this` in a static initializer or block names the class, so it + // reaches the class's methods as naming the class does. + const owner = staticThisClass7(node); + if (owner !== undefined && stored.has(owner)) reach(owner); + return undefined; + } + if (!ts.isIdentifier(node) || !names.has(node.text)) return undefined; + if (inTypePosition7(node)) return undefined; + const parent = node.parent; + if (parent !== undefined && ts.isExportSpecifier(parent)) return undefined; + if (parent !== undefined && declaredName7(parent) === node) return undefined; + const symbol = symbolOf(node); + if (symbol === undefined) return undefined; + if (isMemberNamespace(symbol)) { + // `ns.member` resolves through the member's own identifier; any + // other use can reach every export. + if ( + !( + parent !== undefined && + ts.isPropertyAccessExpression(parent) && + parent.expression === node + ) + ) + for (const unit of allUnits) reach(unit); + return undefined; + } + const c = candidates.get(symbol); + if ( + c !== undefined && + node !== c.name && + !declared.has(symbol) && + !out.has(symbol) && + inDeferredPosition7(node) && + !( + c.kind === "class" && + ts.getSourceFileOfNode(node) === c.module && + ts.tokenStartOfNode(node) >= c.start && + node.end <= c.end + ) + ) + out.set(symbol, node); + for (const unit of unitsBySymbol.get(symbol) ?? []) reach(unit); + return undefined; + }); + }; + const drain = (): void => { + while (work.length > 0) visit(work.pop()!); + }; + const declare = (name: ts.BindingName): void => { + for (const id of boundNames7(name)) { + const symbol = checker.getSymbolAtLocation(id); + if (symbol !== undefined) declared.add(symbol); + } + }; + for (const sf of evaluation) { + for (const stmt of sf.statements.nodes) { + if (ts.isClassDeclaration(stmt)) { + for (const part of classSyncParts7(stmt)) visit(part); + drain(); + if (stmt.name !== undefined) declare(stmt.name); + } else if (ts.isVariableStatement(stmt)) { + for (const d of stmt.declarationList.declarations.nodes) { + visit(d); + drain(); + declare(d.name); + } + } else if (!ts.isFunctionDeclaration(stmt)) visit(stmt); + drain(); + } + } + return out; +} + /** Top-level let/const and class bindings, in modules outside the * exactly-initialized cycles, that code running before their declaration * completes can read. @@ -4114,51 +4757,8 @@ function moduleEarlyBindingsOf7( storedBy.set(name.text, list); }; const checker = program.getTypeChecker(); - const storedIn = (e: ts.Expression, units: ts.Node[]): void => { - while ( - ts.isParenthesizedExpression(e) || - ts.isAsExpression(e) || - ts.isSatisfiesExpression(e) || - ts.isNonNullExpression(e) - ) - e = e.expression; - if (ts.isArrowFunction(e) || ts.isFunctionExpression(e)) { - units.push(e); - } else if (ts.isClassExpression(e)) { - units.push(e); - immediate.set(e, classSyncParts7(e)); - } else if (ts.isObjectLiteralExpression(e)) { - for (const p of e.properties.nodes) { - if ( - ts.isMethodDeclaration(p) || - ts.isGetAccessorDeclaration(p) || - ts.isSetAccessorDeclaration(p) - ) { - // A computed key evaluates where the literal does; a plain - // member-chain key (`[Equal.symbol]`) runs no user code. - if (!ts.isComputedPropertyName(p.name)) units.push(p); - else if (memberChainKey7(p.name.expression)) { - units.push(p); - immediate.set(p, [p.name]); - } - } else if (ts.isPropertyAssignment(p)) storedIn(p.initializer, units); - } - } else if (ts.isArrayLiteralExpression(e)) { - for (const el of e.elements.nodes) if (!ts.isSpreadElement(el)) storedIn(el, units); - } else if (ts.isCallExpression(e)) { - // `const f = wrap(callback)` where wrap only returns closures over - // the callback: it runs when the stored result is called. - e.arguments.nodes.forEach((arg, index) => { - let fn: ts.Expression = arg; - while (ts.isParenthesizedExpression(fn)) fn = fn.expression; - if ( - (ts.isArrowFunction(fn) || ts.isFunctionExpression(fn)) && - returnsOnlyClosuresOver7(checker, e.expression, index) - ) - units.push(fn); - }); - } - }; + const storedIn = (e: ts.Expression, units: ts.Node[]): void => + storedCallables7(checker, e, units, immediate); sf.statements.nodes.forEach((stmt, index) => { if (ambient(stmt)) return; if (ts.isFunctionDeclaration(stmt) && stmt.name !== undefined && stmt.body !== undefined) { @@ -4411,6 +5011,40 @@ function returnsOnlyClosuresOver7( * expressions, decorators, computed member keys, static field * initializers and static blocks. Methods and instance fields wait for a * call or a construction. */ +/** The class whose static field initializer or static block evaluates `this` + * (through arrow functions, which keep the enclosing `this`), or undefined + * when another function or a class's instance side binds it. */ +function staticThisClass7(node: ts.Node): ts.ClassLikeDeclaration | undefined { + let child = node; + for (let n = node.parent; n !== undefined; child = n, n = n.parent) { + if (ts.isArrowFunction(n)) continue; + if (ts.isClassStaticBlockDeclaration(n)) return n.parent as ts.ClassLikeDeclaration; + if (ts.isPropertyDeclaration(n)) { + if (n.initializer !== child) return undefined; + return (ts.getCombinedModifierFlags(n) & ts.ModifierFlags.Static) !== 0 + ? (n.parent as ts.ClassLikeDeclaration) + : undefined; + } + if (ts.isComputedPropertyName(n)) { + // A computed key evaluates in the scope around the class. + const member = n.parent; + const cls = member?.parent; + if (cls === undefined) return undefined; + child = cls; + n = cls; + continue; + } + if ( + ts.isFunctionLike(n) || + ts.isClassDeclaration(n) || + ts.isClassExpression(n) || + ts.isSourceFile(n) + ) + return undefined; + } + return undefined; +} + function classSyncParts7(c: ts.ClassLikeDeclaration): ts.Node[] { const decorators = (n: ts.Node): ts.Node[] => ((n as { modifiers?: ts.NodeArray }).modifiers?.nodes ?? []).filter( @@ -4480,7 +5114,7 @@ function laterJobCallee7(checker: ts.TypeChecker, callee: ts.Expression): boolea // package declarations describe user code. return decls.every((d) => { const file = ts.getSourceFileOfNode(d).fileName; - return isNodeTypesPath(file) || file === fallbackDtsPath(); + return isNodeTypesPath(file) || isFallbackDtsPath(file); }); } @@ -4873,6 +5507,16 @@ function preflight7(load: LoadResult): { return new RegExp(`\\btypeof\\s*\\(?\\s*${name.replace(/\$/g, "\\$")}(?![\\w$])`).test(sf.text); }; + // A `return` at the top level of a CommonJS module (TS1108): Node runs + // the module as a function body, so the return ends the module's + // evaluation. ES modules and TypeScript keep the error. + const cjsTopLevelReturnSuppressed = (p: ts.Program, d: ts.Diagnostic): boolean => { + if (d.code !== 1108 || d.fileName === undefined || !isJsSourceFileName(d.fileName)) + return false; + const sf = p.getSourceFile(d.fileName); + return sf !== undefined && isCjsJsFile7(sf, p); + }; + const workspaceImplicitAnySuppressed = (p: ts.Program, d: ts.Diagnostic): boolean => { if (d.code !== 7016 || d.fileName === undefined || d.pos === undefined || d.end === undefined) return false; @@ -4894,7 +5538,14 @@ function preflight7(load: LoadResult): { const sf = p.getSourceFile(d.fileName); return sf !== undefined && isCheckedJsSourceFile(sf, projectCheckJs); }; - const errorsOf = (p: ts.Program): ts.Diagnostic[] => keptErrors(p, ts.getPreEmitDiagnostics(p)); + // keptErrors drops every diagnostic of these files: the gate does not ask + // the checker for them. + const diagnosticsKept = (fileName: string): boolean => + npmStaticPackageOfPath(fileName) === null && + !unselectedPackageJsFile(fileName) && + !isPackageShimFile(fileName); + const errorsOf = (p: ts.Program): ts.Diagnostic[] => + keptErrors(p, ts.getPreEmitDiagnostics(p, diagnosticsKept)); const keptErrors = (p: ts.Program, all: readonly ts.Diagnostic[]): ts.Diagnostic[] => { // First pass: every comment-side 2300's (file, name) — the partners // the second pass forgives. @@ -4919,6 +5570,7 @@ function preflight7(load: LoadResult): { !isNpmStaticSubclassArgument(p, d) && !workspaceImplicitAnySuppressed(p, d) && !typeofTestedNameSuppressed(p, d) && + !cjsTopLevelReturnSuppressed(p, d) && !jsdocTypeSuppressed(p, d, commentDup), ); }; @@ -5456,9 +6108,15 @@ function preflight7(load: LoadResult): { // to Node's no-op, the module still evaluates through its // ordinary init edge, and no member is ever read through the // lexer facade (self-imports of a CJS module included — the - // partially-built exports are never observed). Any OTHER use - // keeps the fence. - if (!nsBindingUsesAreBareStatements7(program, sf, clause.namedBindings.name)) { + // partially-built exports are never observed). In JavaScript + // sources every use passes here: member reads lower through the + // names Node's lexer detects, and the other reads refuse at + // their site (lower-namespaces.ts, cjsNamespaceImportDep). Any + // OTHER use in TypeScript keeps the fence. + if ( + !isJsSourceFileName(sf.fileName) && + !nsBindingUsesAreBareStatements7(program, sf, clause.namedBindings.name) + ) { diags.push( unsupportedDiag( "SC1013", @@ -5488,7 +6146,7 @@ function preflight7(load: LoadResult): { // module aliases cannot represent; createRequire bindings have no // checked form either. const earlyRequireFence = ( - req: { decl: ts.VariableDeclaration | null }, + req: RequireOccurrence7, bindingKind: RequireCallBindingKind7, loc: { file: string; start: number; end: number }, ): boolean => { @@ -5496,6 +6154,7 @@ function preflight7(load: LoadResult): { if ( early === null || (bindingKind !== "createRequire" && + req.split === undefined && ((req.decl!.parent?.flags ?? 0) & (ts.NodeFlags.Const | ts.NodeFlags.Let)) !== 0) ) { return false; @@ -5506,14 +6165,16 @@ function preflight7(load: LoadResult): { loc, bindingKind === "createRequire" ? `createRequire() binding '${early.text}' below code that can reach it (Node throws ReferenceError on any earlier access, which is not modeled — move the require above the code that can run first)` - : `var require() binding '${early.text}' below code that can reach it (Node reads undefined before the require runs, which is not modeled — declare it with const or let, or move the require above the code that can run first)`, + : req.split !== undefined + ? `require() binding '${early.text}' assigned below code that can reach it (Node reads undefined before the assignment runs, which is not modeled — move the assignment above the code that can run first)` + : `var require() binding '${early.text}' below code that can reach it (Node reads undefined before the require runs, which is not modeled — declare it with const or let, or move the require above the code that can run first)`, ), ); return true; }; for (let k = 0; k < stmts.length; k++) { const stmt = stmts[k]!; - for (const req of requiresOf7(stmt)) { + for (const req of requiresOf7(stmt, program)) { const loc = { file: sf.fileName, start: ts.tokenStartOfNode(req.node), @@ -5540,8 +6201,8 @@ function preflight7(load: LoadResult): { const jsonDep = resolveImport7(program, sf, req.spec, "require"); if (jsonDep !== null && jsonDep.fileName.endsWith(".json")) { if (bindingKind === "createRequire") continue; - if (ts.isVariableStatement(stmt) && requiresOf7(stmt).length > 1) { - const others = requiresOf7(stmt).some((r) => { + if (ts.isVariableStatement(stmt) && requiresOf7(stmt, program).length > 1) { + const others = requiresOf7(stmt, program).some((r) => { const d = resolveImport7(program, sf, r.spec, "require"); return d === null || !d.fileName.endsWith(".json"); }); @@ -5713,8 +6374,12 @@ function preflight7(load: LoadResult): { // counts, and targets outside the compiled graph add nothing here: // their use sites keep their existing lowering fences. if (jsRequireFile && !isNodeEsmFile7(sf, program)) { + // A split require binding's assignment is a statement-level require + // edge (requiresOf7 above), parenthesized or not. + const splitCalls = new Set(splitRequireBindings7(program, sf).values()); for (const call of inlineRequiresOf7(sf)) { if (requireCallBindingKind7(program, call) !== "ambient") continue; + if (splitCalls.has(call)) continue; const spec = requireSpecOf7(call)!; if (load.externalTypes.has(spec) || canonicalBuiltinModule(spec) !== null) continue; const lazy = inDeferredPosition7(call); @@ -5880,6 +6545,108 @@ function preflight7(load: LoadResult): { * Node refuses the program). This walk asks Node's question at compile * time, in Node's exact ORDER, and answers with Node's exact message. */ +/** The CommonJS program file a re-export (`module.exports = require(spec)`) + * of `from` resolves to, or null: reexport targets union in only when they + * resolve to CommonJS program files (Node's cjsPreparseModuleExports rule). */ +function cjsReexportDep7(program: ts.Program) { + return (from: ts.SourceFile, spec: string): ts.SourceFile | null => { + const dep = + resolveImport7(program, from, spec, "require") ?? + npmStaticDepSf7(program, from, spec, "require"); + return dep !== null && isCjsJsFile7(dep, program) ? dep : null; + }; +} + +/** Node's lexer analysis of a CommonJS file. A bundler rewrite spells some + * lexer-visible names as accessors. */ +function lexWithAccessorNames(sf: ts.SourceFile): ReturnType { + const lexed = cjsLexedExportsOfFile(sf); + for (const name of npmStaticLexerVisibleNames(sf.fileName)) lexed.exports.add(name); + return lexed; +} + +/** The property names of Node's namespace object for the CommonJS module + * `dep`: the names its lexer detects, CommonJS re-exports included (the + * rule of the named-import link check), plus `default` and + * `module.exports`. */ +/** `dep` and every CommonJS module it re-exports, transitively (the modules + * whose exports its namespace object shows). */ +export function cjsReexportedModules(program: ts.Program, dep: ts.SourceFile): ts.SourceFile[] { + const resolveDep = cjsReexportDep7(program); + const seen = new Set([dep]); + const order = [dep]; + for (let i = 0; i < order.length; i++) { + const mod = order[i]!; + for (const spec of lexWithAccessorNames(mod).reexports) { + const next = resolveDep(mod, spec); + if (next !== null && !seen.has(next)) { + seen.add(next); + order.push(next); + } + } + } + return order; +} + +export function cjsNamespaceExportNames(program: ts.Program, dep: ts.SourceFile): Set { + const names = cjsVisibleNames(dep, lexWithAccessorNames, cjsReexportDep7(program), new Map()); + return new Set([...names, "default", "module.exports"]); +} + +/** The export names a CommonJS module assigns inside functions + * (`exports.current = value` in a function body), or "all" when such code + * writes a computed name, replaces module.exports or redefines exports + * through Object.defineProperty. Node's namespace object of the module + * holds the values the exports had when the module finished evaluating, + * so it does not see those writes. */ +export function cjsExportsAssignedInFunctions(sf: ts.SourceFile): ReadonlySet | "all" { + const names = new Set(); + let all = false; + const exportsObject = (e: ts.Expression): boolean => + (ts.isIdentifier(e) && e.text === "exports") || isModuleExportsAccess(e); + const target = (e: ts.Expression): void => { + while (ts.isParenthesizedExpression(e)) e = e.expression; + if (isModuleExportsAccess(e)) all = true; + else if (ts.isPropertyAccessExpression(e) && exportsObject(e.expression)) + names.add(e.name.text); + else if (ts.isElementAccessExpression(e) && exportsObject(e.expression)) { + if (ts.isStringLiteralLike(e.argumentExpression)) names.add(e.argumentExpression.text); + else all = true; + } + }; + const visit = (node: ts.Node, inFunction: boolean): void => { + if (all) return; + const nested = inFunction || ts.isFunctionLike(node); + if (nested) { + if ( + ts.isBinaryExpression(node) && + node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && + node.operatorToken.kind <= ts.SyntaxKind.LastAssignment + ) + target(node.left); + else if ( + (ts.isPrefixUnaryExpression(node) || ts.isPostfixUnaryExpression(node)) && + (node.operator === ts.SyntaxKind.PlusPlusToken || + node.operator === ts.SyntaxKind.MinusMinusToken) + ) + target(node.operand); + else if (ts.isDeleteExpression(node)) target(node.expression); + else if ( + ts.isCallExpression(node) && + ts.isPropertyAccessExpression(node.expression) && + ts.isIdentifier(node.expression.expression) && + node.expression.expression.text === "Object" && + node.arguments.nodes[0] !== undefined && + exportsObject(node.arguments.nodes[0]) + ) + all = true; + } + ts.forEachChild(node, (child) => visit(child, nested)); + }; + visit(sf, false); + return all ? "all" : names; +} + interface BadCjsImport { /** The export name requested from the CJS module. */ exportName: string; @@ -5920,25 +6687,14 @@ function cjsNamedImportLinkCheck( const npmStatic = npmStaticDepSf7(program, from, spec); return npmStatic; }; - // Reexport targets union in only when they resolve to CommonJS program - // files (Node's cjsPreparseModuleExports rule). - const resolveCjsDep = (from: ts.SourceFile, spec: string): ts.SourceFile | null => { - const dep = - resolveImport7(program, from, spec, "require") ?? - npmStaticDepSf7(program, from, spec, "require"); - return dep !== null && isCjsJsFile7(dep, program) ? dep : null; - }; - // A bundler rewrite spells some lexer-visible names as accessors. - const lexWithAccessorNames = (sf: ts.SourceFile): ReturnType => { - const lexed = cjsLexedExportsOfFile(sf); - for (const name of npmStaticLexerVisibleNames(sf.fileName)) lexed.exports.add(name); - return lexed; - }; + const resolveCjsDep = cjsReexportDep7(program); const visible = (dep: ts.SourceFile, name: string): boolean => - // `default` is the module.exports binding itself — always provided. - // The program already owns native source ASTs; reuse them without - // starting another syntax session or reparsing their source text. + // `default` and `module.exports` are the module.exports binding itself + // — always provided. The program already owns native source ASTs; + // reuse them without starting another syntax session or reparsing + // their source text. name === "default" || + name === "module.exports" || cjsVisibleNames(dep, lexWithAccessorNames, resolveCjsDep, lexMemo).has(name); /** The statement's resolved LOCAL CommonJS dependency, when it is an @@ -6155,7 +6911,7 @@ function analyzeEsmNamedImportLinks( const nextSeen = new Set(seenModules); nextSeen.add(dep); const module = checker.getSymbolAtLocation(dep); - const exported = module?.getExports().get(name as ts.__String); + const exported = module?.getExports().get(ts.escapedSymbolName(name)); if (exported !== undefined) { let resolved = exported; const seenSymbols = new Set(); @@ -6448,7 +7204,13 @@ const nodeBuiltinNames: ReadonlySet = new Set(builtinModules); * established (the phase-2 port kept every spelling); the world-neutral * helpers re-export from their focused modules. */ -export { ambientDtsPath, fallbackDtsPath, isNodeTypesPath, overridesDtsPath } from "./dts-paths.js"; +export { + ambientDtsPath, + fallbackDtsPath, + isFallbackDtsPath, + isNodeTypesPath, + overridesDtsPath, +} from "./dts-paths.js"; export { builtinDefaultImportModule, @@ -6463,6 +7225,7 @@ export { requireSpecOf7 as requireSpecOf, isRequireStatement7 as isRequireStatement, requiresOf7 as requiresOf, + splitRequireCallOf7 as splitRequireCallOf, isNodeEsmFile7 as isNodeEsmFile, resolveImport7 as resolveImport, resolveNpmImport7 as resolveNpmImport, diff --git a/packages/compiler/src/frontend/resolve.ts b/packages/compiler/src/frontend/resolve.ts index 3b32dad3a..86252499f 100644 --- a/packages/compiler/src/frontend/resolve.ts +++ b/packages/compiler/src/frontend/resolve.ts @@ -37,9 +37,11 @@ import { trackedRealpath, } from "./input-tracker.js"; import { isRelativeSpecifier, packageNameOfSpecifier } from "./workspace-registry.js"; +import { typePatchFileExists } from "./type-patches/layer.js"; function isFile(path: string): boolean { - return trackedFileExists(path); + // A declaration entry a project type patch supplies (type-patches/layer.ts). + return trackedFileExists(path) || typePatchFileExists(path); } function isDirectory(path: string): boolean { @@ -562,6 +564,12 @@ export function nearestPkgJsonPath(fromFile: string): string | null { return dir === null ? null : join(dir, "package.json"); } +/** The nearest package scope's "imports" field (undefined without one). */ +export function nearestPackageImports(fromFile: string): unknown { + const dir = nearestPkgDir(dirname(resolve(fromFile))); + return dir === null ? undefined : pkgJsonOf(dir)?.imports; +} + /** The target the nearest package scope's "imports" field maps a '#' * specifier to under Node's ESM runtime conditions: a './' path inside the * package or another package's specifier. Null when no entry maps it. */ diff --git a/packages/compiler/src/frontend/services.ts b/packages/compiler/src/frontend/services.ts index 19728d6ce..5e493f0ba 100644 --- a/packages/compiler/src/frontend/services.ts +++ b/packages/compiler/src/frontend/services.ts @@ -14,12 +14,22 @@ import { } from "./npm-static-builtin-aliases.js"; import { mayHoldUmdWrapper, unwrapUmdFactory } from "./npm-static-umd.js"; import { mayHoldEnumIife, rewriteEnumIifes } from "./npm-static-enum-iife.js"; +import { + mayHoldNamespaceDefault, + rewriteNamespaceDefaults, +} from "./npm-static-namespace-defaults.js"; import { mayHoldTscBuiltinImport, unwrapTscBuiltinImports } from "./npm-static-tsc-helpers.js"; import { builtinPackageAliasFor } from "./package-shims.js"; import * as syntax from "./npm-static-declaration-syntax.js"; import { normalizeCjsExportsVar } from "./npm-static-cjs-exports-var.js"; import type { SourceFile } from "./ts7/ast-types.js"; +/** Whether the source can contain `word` as a keyword or identifier: it + * holds the word itself or a Unicode escape that might spell it. */ +function spells(source: string, word: string): boolean { + return source.includes(word) || source.includes("\\u"); +} + /** Evaluate an isolated, self-contained TypeScript callback under a finite * budget. Hosts supply execution; shared lowering owns capture checks and * verifies the returned value before baking it into the program. */ @@ -52,8 +62,12 @@ export class FrontendServices { } private sourceParser(): Ts7SourceParser { this.ensureOpen(); + // Called as a plain function: the instance is not its receiver. + const createApi = this.createApi; return (this.parser ??= new Ts7SourceParser( - (options) => this.createApi({ ...options, collectTiming: false }), + // The parser reads no libraries, so it gains nothing from the shared + // checker server, and leaves it to the program's checker. + (options) => createApi({ ...options, collectTiming: false, shared: false }), this.cwd, )); } @@ -62,9 +76,10 @@ export class FrontendServices { } evaluateComptime(source: string, timeoutMs: number): unknown { this.ensureOpen(); - if (this.comptimeEvaluator === undefined) + const evaluate = this.comptimeEvaluator; + if (evaluate === undefined) throw new Error("this compiler host does not provide compile-time evaluation"); - return this.comptimeEvaluator(source, timeoutMs); + return evaluate(source, timeoutMs); } createProgramHost(options?: Ts7HostOptions): Ts7Host { this.ensureOpen(); @@ -129,6 +144,13 @@ export class FrontendServices { if (!mayHoldEnumIife(source)) return null; return rewriteEnumIifes(this.parse(path, source, "js")); } + /** A package module with its guarded namespace declarations + * (`var ns = ns || {}`) respelled as plain ones (null: none). */ + namespaceDefaults(source: string, path: string): string | null { + this.ensureOpen(); + if (!mayHoldNamespaceDefault(source)) return null; + return rewriteNamespaceDefaults(this.parse(path, source, "js")); + } /** A package module with its requires of builtin-aliased polyfill * packages respelled as the builtins (null: none). */ builtinAliasRequires(source: string, path: string): string | null { @@ -147,13 +169,18 @@ export class FrontendServices { return normalizeCjsExportsVar(this.parse(path, source, "js")); } nullableClassFields(path: string, source: string): syntax.NpmStaticOverloadRewrite | null { + // Rewrites `null`-initialized fields of class declarations. + if (!spells(source, "class") || !spells(source, "null")) return null; return syntax.applyNpmStaticNullableClassFields(this.parse(path, source, "js"), source); } jsDocNamepaths(path: string, source: string): string | null { - if (!source.includes("~") && !source.includes("#")) return null; + // A namepath segment joined by `#` or `~` (applyNpmStaticJsDocNamepaths). + if (!/[$\w][#~][$A-Z_a-z]/.test(source)) return null; return syntax.applyNpmStaticJsDocNamepaths(this.parse(path, source, "js"), source); } findReturnWidening(path: string, source: string): syntax.NpmStaticOverloadRewrite | null { + // Widens `find` calls returned by methods of class declarations. + if (!spells(source, "class") || !spells(source, "find")) return null; return syntax.applyNpmStaticFindReturnWidening(this.parse(path, source, "js"), source); } declarationProperties( diff --git a/packages/compiler/src/frontend/shape-inference/analyze.ts b/packages/compiler/src/frontend/shape-inference/analyze.ts new file mode 100644 index 000000000..2a0ee09a1 --- /dev/null +++ b/packages/compiler/src/frontend/shape-inference/analyze.ts @@ -0,0 +1,3439 @@ +/* Shape inference (results/design-shape-inference/DESIGN.md), stage S1: + * a whole-program allocation-site points-to analysis over the JavaScript of + * opted-in npm packages and the user files that call into them. + * + * Stage S1 is report-only: the analysis runs when SCRIPTC_DEBUG_SHAPES is + * set and prints its plan (report.ts). Nothing it computes reaches the + * lowering yet, so every program compiles exactly as before. + * + * The analysis (DESIGN.md §4): + * - Andersen-style inclusion constraints, solved with a worklist. Locals + * and parameters are one value node per binding (flow-insensitive in S1); + * heap fields are one node per (atom, name); arrays one element node. + * - Calls resolve by the callee's atoms: functions, class constructors, + * methods by the receiver's class chain (inherited members, getters and + * setters, `this.constructor`), closures stored in fields, `bind`, and + * builtin summaries for the common library surface. + * - Escape: an atom escapes when it reaches code the analysis cannot see + * (a ⊤ callee, a builtin without a summary, a store into a ⊤ object). + * Escaped functions run with ⊤ arguments; everything reachable from an + * escaped atom escapes. + * - Census: writes and reflection whose receiver may be ⊤ apply to every + * escaped atom (§5.4); the report uses them to decide which allocation + * sites could become native records (§5.5). */ +import * as ts from "../ts7/adapter.js"; +import { isJsSourceFile } from "../program.js"; +import { npmStaticPackageOfPath } from "../npm-static.js"; +import { AtomTable, MODELED_GLOBALS, STRING_METHOD_RESULTS, type Atom } from "./domain.js"; + +type Node = ts.Node; + +export interface ValueNode { + readonly id: number; + readonly atoms: Set; + readonly succ: Set; + /** Constraints to run for each atom that arrives. */ + readonly onAtom: ((atom: Atom) => void)[]; + /** Every atom that arrives escapes. */ + escapeSink: string | null; +} + +export interface FunctionInfo { + readonly node: Node; + /** The analysis context: "" or a method's receiver (`C`). */ + readonly ctx: string; + /** The function whose clone the context is (bindings inside it are per context). */ + readonly root: Node | null; + /** Bodies are walked on first call: code that never runs adds no facts. */ + walked: boolean; + readonly label: string; + readonly params: number[]; + /** Index of a rest parameter, or -1. */ + readonly rest: number; + readonly thisNode: number; + readonly ret: number; + readonly isArrow: boolean; + readonly isAsync: boolean; + readonly isGenerator: boolean; + /** Reads `arguments` other than through `...arguments` forwarding. */ + usesArguments: boolean; + /** `f(...arguments)` forwarding: one node per argument position. */ + argPos: number[] | null; + /** The class whose member this function is (method, accessor, constructor). */ + readonly ownerClass: Node | null; + readonly inPackage: boolean; +} + +export interface LiteralSite { + readonly atom: Atom; + readonly node: Node; + readonly kind: "object" | "array"; + /** Static keys in creation order; null when a spread or computed key makes them unknown. */ + readonly keys: string[] | null; + /** The literal's own static keys (without spread contributions). */ + readonly ownKeys: string[]; + readonly hasAccessor: boolean; + readonly hasProto: boolean; + readonly hasComputedKey: boolean; + readonly spreads: number[]; + readonly inPackage: boolean; +} + +export interface Op { + readonly kind: + | "read" + | "write" + | "computedRead" + | "computedWrite" + | "delete" + | "freeze" + | "seal" + | "defineProperty" + | "setPrototypeOf" + | "assign" + | "proxy" + | "lengthWrite"; + readonly recv: number; + readonly name: string | null; + readonly node: Node; +} + +export interface CallEdge { + readonly caller: FunctionInfo | null; + readonly callee: FunctionInfo; + readonly inLoop: boolean; + /** The call (or `new`, getter read, builtin callback) site. */ + readonly site: Node; +} + +export interface ShapeAnalysis { + readonly atoms: AtomTable; + readonly nodes: ValueNode[]; + readonly functions: Map; + readonly sites: LiteralSite[]; + readonly ops: Op[]; + readonly calls: CallEdge[]; + readonly escaped: Map; + readonly topWriteNames: Map; + readonly topComputedWrites: Node[]; + /** Fields per atom: name → node id. */ + readonly fields: Map>; + readonly elements: Map; + readonly top: Atom; + readonly files: readonly ts.SourceFile[]; + readonly packageFiles: number; + readonly checker: ts.TypeChecker; + readonly budgetExceeded: boolean; + readonly steps: number; + readonly topCause: ReadonlyMap; + readonly nodeLabel: ReadonlyMap; + /** Functions unknown code may call (their atoms escaped). */ + readonly escapedFunctions: ReadonlySet; + /** ES5 constructors: each function's default prototype object. */ + readonly protoOf: ReadonlyMap; + /** ES5 constructor instances: the function atom whose `new` made each. */ + readonly fnOfInstance: ReadonlyMap; + /** Every value a declaration (parameter, variable, binding element) holds, + * merged over the analysis contexts it was analyzed in. */ + declValues(decl: Node): Set; + /** The classes a class declaration's chain contains, base last. */ + classChain(decl: Node): Node[]; + /** `if` statements in walked code whose condition is known: the branch + * whose condition values never allowed it is absent from `walkedBranches`. */ + readonly branchIfs: ReadonlySet; + readonly walkedBranches: ReadonlySet; +} + +/** Array.prototype members: other names on an array are own properties. */ +const ARRAY_METHODS: ReadonlySet = new Set([ + "at", + "concat", + "copyWithin", + "entries", + "every", + "fill", + "filter", + "find", + "findIndex", + "findLast", + "findLastIndex", + "flat", + "flatMap", + "forEach", + "includes", + "indexOf", + "join", + "keys", + "lastIndexOf", + "map", + "pop", + "push", + "reduce", + "reduceRight", + "reverse", + "shift", + "slice", + "some", + "sort", + "splice", + "toLocaleString", + "toReversed", + "toSorted", + "toSpliced", + "toString", + "unshift", + "values", + "with", +]); +const MAX_STEPS = 20_000_000; + +function list(arr: { readonly nodes: readonly T[] } | undefined): readonly T[] { + return arr?.nodes ?? []; +} + +function isStatic(node: Node): boolean { + const mods = (node as { modifiers?: { nodes: readonly Node[] } }).modifiers; + return list(mods).some((m) => m.kind === ts.SyntaxKind.StaticKeyword); +} + +function unparen(node: ts.Expression): ts.Expression { + let n = node; + for (;;) { + if (ts.isParenthesizedExpression(n)) n = n.expression; + else if (ts.isNonNullExpression(n)) n = n.expression; + else if (ts.isAsExpression(n) || ts.isSatisfiesExpression(n)) n = n.expression; + else return n; + } +} + +function propertyNameText(name: Node): string | null { + if (ts.isIdentifier(name) || ts.isPrivateIdentifier(name)) return name.text; + if (ts.isStringLiteral(name) || ts.isNoSubstitutionTemplateLiteral(name)) return name.text; + if (ts.isNumericLiteral(name)) return String(Number(name.text)); + if (ts.isComputedPropertyName(name)) { + const e = unparen(name.expression); + if (ts.isStringLiteral(e) || ts.isNoSubstitutionTemplateLiteral(e)) return e.text; + if (ts.isNumericLiteral(e)) return String(Number(e.text)); + } + return null; +} + +function shortFile(fileName: string): string { + const norm = fileName.split("\\").join("/"); + const nm = norm.lastIndexOf("/node_modules/"); + if (nm >= 0) { + const rest = norm.slice(nm + "/node_modules/".length).split("/"); + const pkgLen = rest[0]?.startsWith("@") ? 2 : 1; + const pkg = rest.slice(0, pkgLen).join("/"); + return `${pkg}/${rest[rest.length - 1]}`; + } + return norm.slice(norm.lastIndexOf("/") + 1); +} + +export function locLabel(node: Node): string { + const sf = node.getSourceFile(); + const { line } = ts.getLineAndCharacterOfPosition(sf, node.getStart(sf)); + return `${shortFile(sf.fileName)}:${line + 1}`; +} + +/** Whether a source file belongs to an opted-in package's JavaScript. */ +export function isPackageJsFile(sf: ts.SourceFile): boolean { + return isJsSourceFile(sf) && npmStaticPackageOfPath(sf.fileName) !== null; +} + +function isLoop(node: Node): boolean { + return ( + ts.isForStatement(node) || + ts.isForOfStatement(node) || + ts.isForInStatement(node) || + ts.isWhileStatement(node) || + ts.isDoStatement(node) + ); +} + +/** Whether control can reach the end of `stmts`. False only when every + * path ends in a return or throw, or in a `while (true)`, `do … while + * (true)` or `for (;;)` loop with no break inside; anything else is + * assumed to complete. */ +function canComplete(stmts: readonly Node[]): boolean { + return stmts.every(canCompleteStatement); +} + +function canCompleteStatement(s: Node): boolean { + if (ts.isReturnStatement(s) || ts.isThrowStatement(s)) return false; + if (ts.isBlock(s)) return canComplete(list(s.statements)); + if (ts.isIfStatement(s)) + return ( + s.elseStatement === undefined || + canCompleteStatement(s.thenStatement) || + canCompleteStatement(s.elseStatement) + ); + if (ts.isTryStatement(s)) { + if (s.finallyBlock !== undefined && !canCompleteStatement(s.finallyBlock)) return false; + return ( + canCompleteStatement(s.tryBlock) || + (s.catchClause !== undefined && canCompleteStatement(s.catchClause.block)) + ); + } + const endless = + ((ts.isWhileStatement(s) || ts.isDoStatement(s)) && + s.expression.kind === ts.SyntaxKind.TrueKeyword) || + (ts.isForStatement(s) && s.condition === undefined); + return !endless || containsBreak(s); +} + +/** Whether a break statement occurs in `node`'s body, outside nested + * functions. */ +function containsBreak(node: Node): boolean { + return ( + ts.forEachChild(node, (c) => + ts.isBreakStatement(c) || (!ts.isFunctionLike(c) && containsBreak(c)) ? true : undefined, + ) === true + ); +} + +const LOOP_CALLBACK_METHODS: ReadonlySet = new Set([ + "map", + "forEach", + "filter", + "some", + "every", + "find", + "findIndex", + "findLast", + "findLastIndex", + "reduce", + "reduceRight", + "flatMap", + "sort", + "toSorted", + "from", + "replace", + "replaceAll", +]); + +export function analyzeShapes(program: ts.Program, checker: ts.TypeChecker): ShapeAnalysis { + return new Analyzer(program, checker).run(); +} + +class Analyzer { + readonly atoms = new AtomTable(); + readonly nodes: ValueNode[] = []; + readonly functions = new Map(); + readonly sites: LiteralSite[] = []; + private readonly siteAtoms = new Set(); + readonly ops: Op[] = []; + readonly calls: CallEdge[] = []; + readonly escaped = new Map(); + readonly topWriteNames = new Map(); + readonly topComputedWrites: Node[] = []; + readonly fields = new Map>(); + readonly elements = new Map(); + readonly top: Atom; + private readonly bindings = new Map(); + private readonly exprMemo = new Map(); + /** Current analysis context and the function whose clone it is. */ + private ctx = ""; + private ctxRoot: Node | null = null; + private readonly classInfo = new Map(); + private readonly boundCalls = new Set(); + private readonly escapedFunctionCalls = new Set(); + private readonly worklist: [number, Atom][] = []; + private steps = 0; + private budgetExceeded = false; + private currentFn: FunctionInfo | null = null; + private loopDepth = 0; + private files: ts.SourceFile[] = []; + private packageFiles = 0; + private readonly prims = new Map(); + /** Per call site: the calling function and whether the site is in a loop + * (constraints fire during solving, after the walk has moved on). */ + private readonly siteCtx = new Map(); + private forceLoop = false; + + noteSite(site: Node): void { + if (!this.siteCtx.has(site)) + this.siteCtx.set(site, { caller: this.currentFn, inLoop: this.loopDepth > 0 }); + } + + /** Builtin calls that invoke their callbacks once per value + * (JSON.parse revivers, JSON.stringify replacers and toJSON methods), + * which may run after the call's own handling. */ + private readonly repeatingSites = new Set(); + + edgeFor(site: Node, callee: FunctionInfo): void { + const ctx = this.siteCtx.get(site); + this.calls.push({ + caller: ctx?.caller ?? null, + callee, + inLoop: (ctx?.inLoop ?? false) || this.forceLoop || this.repeatingSites.has(site), + site, + }); + } + + constructor( + private readonly program: ts.Program, + private readonly checker: ts.TypeChecker, + ) { + this.top = this.atoms.intern("top", () => ({ kind: "top", label: "⊤" })); + } + + run(): ShapeAnalysis { + for (const sf of this.program.getImplementationSourceFiles()) { + const name = sf.fileName.split("\\").join("/"); + const inPackage = isPackageJsFile(sf); + if (!inPackage && name.includes("/node_modules/")) continue; + this.files.push(sf); + if (inPackage) this.packageFiles++; + } + for (const sf of this.files) this.checker.prefetchSourceFile(sf); + for (const sf of this.files) this.moduleBody(sf); + this.finish(); + return { + atoms: this.atoms, + nodes: this.nodes, + functions: this.functions, + sites: this.sites, + ops: this.ops, + calls: this.calls, + escaped: this.escaped, + topWriteNames: this.topWriteNames, + topComputedWrites: this.topComputedWrites, + fields: this.fields, + elements: this.elements, + top: this.top, + files: this.files, + packageFiles: this.packageFiles, + checker: this.checker, + budgetExceeded: this.budgetExceeded, + steps: this.steps, + topCause: this.topCause, + nodeLabel: this.nodeLabel, + escapedFunctions: this.escapedFunctionCalls, + protoOf: this.protoOf, + fnOfInstance: this.fnOfInstance, + declValues: (decl) => this.declValues(decl), + classChain: (decl) => this.classChain(decl), + branchIfs: this.branchIfs, + walkedBranches: this.walkedBranches, + }; + } + + // ---- value nodes ------------------------------------------------------- + + newNode(): number { + const id = this.nodes.length; + this.nodes.push({ id, atoms: new Set(), succ: new Set(), onAtom: [], escapeSink: null }); + return id; + } + + /** Debugging (SCRIPTC_DEBUG_SHAPES_WHY): where each node's ⊤ came from. */ + readonly topCause = new Map(); + readonly nodeLabel = new Map(); + private cause: number | null = null; + private readonly traceTop = process.env["SCRIPTC_DEBUG_SHAPES_WHY"] !== undefined; + + add(node: number, atom: Atom): void { + const n = this.nodes[node]!; + if (n.atoms.has(atom)) return; + n.atoms.add(atom); + if (this.traceTop && atom === this.top && !this.topCause.has(node)) + this.topCause.set( + node, + this.cause ?? + (new Error().stack ?? "") + .split("\n") + .slice(2, 4) + .map((l) => l.trim()) + .join(" < "), + ); + this.worklist.push([node, atom]); + } + + edge(from: number, to: number): void { + if (from === to) return; + const n = this.nodes[from]!; + if (n.succ.has(to)) return; + n.succ.add(to); + const saved = this.cause; + this.cause = from; + for (const a of n.atoms) this.add(to, a); + this.cause = saved; + } + + on(node: number, fn: (atom: Atom) => void): void { + const n = this.nodes[node]!; + n.onAtom.push(fn); + for (const a of [...n.atoms]) fn(a); + } + + sink(node: number, reason: string): void { + const n = this.nodes[node]!; + if (n.escapeSink !== null) return; + n.escapeSink = reason; + for (const a of [...n.atoms]) this.escape(a, reason); + } + + constNode(atom: Atom): number { + const n = this.newNode(); + this.add(n, atom); + return n; + } + + prim(name: string): Atom { + let a = this.prims.get(name); + if (a === undefined) { + a = this.atoms.intern(`prim:${name}`, () => ({ kind: "prim", label: name })); + this.prims.set(name, a); + } + return a; + } + + /** ES5 constructor functions: the function's default prototype object, + * and which function each `new F()` instance atom belongs to. */ + readonly protoOf = new Map(); + readonly fnOfInstance = new Map(); + protoAtom(fn: Atom): Atom { + let p = this.protoOf.get(fn); + if (p === undefined) { + p = this.atoms.intern(`proto:${this.nodeKey(fn.node!)}:${fn.ctx ?? ""}`, () => ({ + kind: "object", + label: `${fn.label}.prototype`, + node: fn.node, + })); + this.protoOf.set(fn, p); + this.add(this.field(p, "constructor"), fn); + if (this.escaped.has(fn)) this.escape(p, `via ${fn.label}`); + } + return p; + } + + /** A boolean literal: labeled `boolean` like any boolean, but its + * truthiness is known, so `if` statements on it walk one branch. */ + boolLiteral(value: boolean): Atom { + return this.atoms.intern(`prim:boolean:${value}`, () => ({ + kind: "prim", + label: "boolean", + literal: value, + })); + } + + /** The analysis environment a deferred walk restores. */ + private env(): WalkEnv { + return { + ctx: this.ctx, + ctxRoot: this.ctxRoot, + currentFn: this.currentFn, + loopDepth: this.loopDepth, + }; + } + + private inEnv(env: WalkEnv, walk: () => void): void { + const saved = this.env(); + this.ctx = env.ctx; + this.ctxRoot = env.ctxRoot; + this.currentFn = env.currentFn; + this.loopDepth = env.loopDepth; + try { + walk(); + } finally { + this.ctx = saved.ctx; + this.ctxRoot = saved.ctxRoot; + this.currentFn = saved.currentFn; + this.loopDepth = saved.loopDepth; + } + } + + readonly branchIfs = new Set(); + readonly walkedBranches = new Set(); + /** Ifs whose branches wait for condition values; at the fixpoint, an + * `if` whose condition node stayed empty walks every waiting branch. */ + private readonly pendingIfs: PendingIf[] = []; + + /** An `if` walks a branch once a condition value allows it: the then + * branch on a value that may be truthy, the else branch on one that may + * be falsy. A branch no value allows is never walked, so its effects + * (returns, calls, writes) stay out of the facts; the overlay turns it + * into a checked internal error. */ + private ifStatement(s: ts.IfStatement): void { + const cond = this.expr(s.expression); + this.branchIfs.add(s); + const pending: PendingIf = { + s, + cond, + env: this.env(), + thenDone: false, + elseDone: s.elseStatement === undefined, + }; + this.pendingIfs.push(pending); + this.on(cond, (a) => { + if (!pending.thenDone && mayBeTruthy(a)) this.walkBranch(pending, "then"); + if (!pending.elseDone && mayBeFalsy(a)) this.walkBranch(pending, "else"); + }); + } + + private walkBranch(pending: PendingIf, which: "then" | "else"): void { + const branch = which === "then" ? pending.s.thenStatement : pending.s.elseStatement!; + if (which === "then") pending.thenDone = true; + else pending.elseDone = true; + this.walkedBranches.add(branch); + this.inEnv(pending.env, () => this.stmt(branch)); + } + + builtin(path: string, receiver?: Atom): Atom { + const key = receiver === undefined ? `builtin:${path}` : `builtin:${path}@${receiver.id}`; + return this.atoms.intern(key, () => ({ kind: "builtin", label: path, path, receiver })); + } + + solve(): void { + while (this.worklist.length > 0) { + if (++this.steps > MAX_STEPS) { + this.budgetExceeded = true; + return; + } + const [id, atom] = this.worklist.pop()!; + const n = this.nodes[id]!; + this.cause = id; + for (const s of n.succ) this.add(s, atom); + this.cause = null; + for (const f of n.onAtom) f(atom); + if (n.escapeSink !== null) this.escape(atom, n.escapeSink); + } + } + + // ---- heap ---------------------------------------------------------------- + + field(atom: Atom, name: string): number { + let m = this.fields.get(atom); + if (m === undefined) { + m = new Map(); + this.fields.set(atom, m); + } + let id = m.get(name); + if (id === undefined) { + id = this.newNode(); + m.set(name, id); + if (this.traceTop) this.nodeLabel.set(id, `${atom.label}.${name}`); + const byName = this.fieldReaders.get(atom); + const readers = byName?.get(name); + if (readers !== undefined) { + for (const dst of readers) this.edge(id, dst); + byName!.delete(name); + } + const reason = this.escaped.get(atom); + if (reason !== undefined) { + this.sink(id, reason); + if (this.topComputedWrites.length > 0 || this.topWriteNames.has(name)) + this.add(id, this.top); + } + } + return id; + } + + element(atom: Atom): number { + let id = this.elements.get(atom); + if (id === undefined) { + id = this.newNode(); + this.elements.set(atom, id); + const reason = this.escaped.get(atom); + if (reason !== undefined) { + this.sink(id, reason); + if (this.topComputedWrites.length > 0) this.add(id, this.top); + } + } + return id; + } + + escape(atom: Atom, reason: string): void { + if (atom.kind === "prim" || atom.kind === "top") return; + if (atom.kind === "builtin" && atom.receiver === undefined) return; + if (this.escaped.has(atom)) return; + this.escaped.set(atom, reason); + const via = `via ${atom.label}`; + // Unknown code reaches a constructor's prototype through the function + // and through its instances. + const ctor = atom.kind === "function" ? atom : this.fnOfInstance.get(atom); + if (ctor !== undefined && ctor.node !== undefined && (atom !== ctor || this.protoOf.has(ctor))) + this.escape(this.protoAtom(ctor), via); + const fm = this.fields.get(atom); + if (fm !== undefined) { + for (const [name, id] of fm) { + this.sink(id, via); + if (this.topComputedWrites.length > 0 || this.topWriteNames.has(name)) + this.add(id, this.top); + } + } + const el = this.elements.get(atom); + if (el !== undefined) { + this.sink(el, via); + if (this.topComputedWrites.length > 0) this.add(el, this.top); + } + if (atom.receiver !== undefined) this.escape(atom.receiver, via); + if (atom.kind === "function") { + const info = this.infoOfAtom(atom); + if (info !== undefined) this.escapeFunction(info, via); + } + if ((atom.kind === "instance" || atom.kind === "class") && atom.node !== undefined) { + // Unknown code may call any member with any arguments. + for (const member of this.classMembers(atom.node, atom.kind === "class")) { + const info = this.functionInfo(member, this.ownerOf(member), this.receiverCtx(atom)); + this.add(info.thisNode, atom); + this.escapeFunction(info, via); + } + } + if (atom.kind === "promise") this.sink(this.field(atom, "value"), via); + } + + /** A function unknown code may call: ⊤ arguments and receiver; its + * results escape. */ + escapeFunction(info: FunctionInfo, reason: string): void { + if (this.escapedFunctionCalls.has(info)) return; + this.escapedFunctionCalls.add(info); + this.activate(info); + for (const p of info.params) this.add(p, this.top); + if (!info.isArrow && info.ownerClass === null) this.add(info.thisNode, this.top); + this.sink(info.ret, reason); + } + + /** Property names that untyped code writes on receivers that may be ⊤ (census). */ + noteTopWrite(name: string | null, node: Node): void { + if (name === null) { + const first = this.topComputedWrites.length === 0; + this.topComputedWrites.push(node); + if (first) { + for (const atom of [...this.escaped.keys()]) { + for (const id of (this.fields.get(atom) ?? new Map()).values()) + this.add(id, this.top); + const el = this.elements.get(atom); + if (el !== undefined) this.add(el, this.top); + } + } + return; + } + if (this.topWriteNames.has(name)) return; + this.topWriteNames.set(name, node); + for (const atom of [...this.escaped.keys()]) { + const id = this.fields.get(atom)?.get(name); + if (id !== undefined) this.add(id, this.top); + } + } + + // ---- classes --------------------------------------------------------------- + + /** The base class declaration of `decl`, via the checker; null at a + * builtin base or none; undefined when unresolved. */ + baseClass(decl: Node): Node | null | undefined { + const heritage = list((decl as ts.ClassLikeDeclaration).heritageClauses).find( + (c) => c.token === ts.SyntaxKind.ExtendsKeyword, + )?.types?.nodes[0]; + if (heritage === undefined) return null; + const e = unparen(heritage.expression); + if (!ts.isIdentifier(e) && !ts.isPropertyAccessExpression(e)) return undefined; + let d = this.resolve(ts.isIdentifier(e) ? e : e.name); + // `var C = class C { … }` (bundler output) + if ( + d !== undefined && + ts.isVariableDeclaration(d) && + d.initializer !== undefined && + ts.isClassExpression(unparen(d.initializer)) + ) + d = unparen(d.initializer); + if (d !== undefined && ts.isClassLikeDeclaration(d)) return d; + if (d !== undefined && d.getSourceFile().isDeclarationFile) return null; + return undefined; + } + + classChain(decl: Node): Node[] { + const out: Node[] = []; + let cur: Node | null | undefined = decl; + while (cur && out.length < 64 && !out.includes(cur)) { + out.push(cur); + cur = this.baseClass(cur); + } + return out; + } + + /** Method, accessor and constructor declarations of a class chain. */ + classMembers(decl: Node, statics: boolean): Node[] { + const out: Node[] = []; + for (const c of this.classChain(decl)) { + for (const m of list((c as ts.ClassLikeDeclaration).members)) { + if (isStatic(m) !== statics) continue; + if ( + ts.isMethodDeclaration(m) || + ts.isGetAccessorDeclaration(m) || + ts.isSetAccessorDeclaration(m) + ) + out.push(m); + else if (ts.isPropertyDeclaration(m) && m.initializer !== undefined) { + const init = unparen(m.initializer); + if (ts.isArrowFunction(init) || ts.isFunctionExpression(init)) out.push(init); + } + } + } + return out; + } + + lookupMember( + decl: Node, + name: string, + statics: boolean, + ): { + kind: "method" | "getter" | "setter" | "both"; + get?: Node; + set?: Node; + method?: Node; + } | null { + for (const c of this.classChain(decl)) { + let get: Node | undefined; + let set: Node | undefined; + for (const m of list((c as ts.ClassLikeDeclaration).members)) { + if (isStatic(m) !== statics) continue; + const mn = (m as { name?: Node }).name; + if (mn === undefined || propertyNameText(mn) !== name) continue; + if (ts.isMethodDeclaration(m)) return { kind: "method", method: m }; + if (ts.isGetAccessorDeclaration(m)) get = m; + if (ts.isSetAccessorDeclaration(m)) set = m; + if (ts.isPropertyDeclaration(m)) return null; // an own field: data + } + if (get || set) + return { + kind: get && set ? "both" : get ? "getter" : "setter", + ...(get ? { get } : {}), + ...(set ? { set } : {}), + }; + } + return null; + } + + // ---- symbols ----------------------------------------------------------------- + + resolve(id: Node): Node | undefined { + let sym = this.checker.getSymbolAtLocation(id); + if (sym === undefined) return undefined; + if (sym.flags & ts.SymbolFlags.Alias) { + try { + sym = this.checker.getAliasedSymbol(sym); + } catch { + return undefined; + } + } + return sym.valueDeclaration ?? sym.declarations[0]; + } + + binding(decl: Node): number { + const key = this.ctxKey(decl); + let id = this.bindings.get(key); + if (id === undefined) { + id = this.newNode(); + this.bindings.set(key, id); + if (this.traceTop) + this.nodeLabel.set( + id, + `binding ${(decl as { name?: { text?: string } }).name?.text ?? "?"}@${locLabel(decl)}${this.ctx !== "" ? ` [${this.ctx}]` : ""}`, + ); + } + return id; + } + + /** Bindings and expressions inside the function being cloned belong to + * the clone's context; everything else to the default context. */ + ctxKey(node: Node): string { + const root = this.ctxRoot; + const inside = + root !== null && + node.getSourceFile() === root.getSourceFile() && + node.pos >= root.pos && + node.end <= root.end; + return `${inside ? this.ctx : ""}|${this.nodeKey(node)}`; + } + + private declIndex: Map | null = null; + declValues(decl: Node): Set { + if (this.declIndex === null) { + this.declIndex = new Map(); + for (const [key, id] of this.bindings) { + const k = Number(key.slice(key.lastIndexOf("|") + 1)); + let l = this.declIndex.get(k); + if (l === undefined) this.declIndex.set(k, (l = [])); + l.push(id); + } + } + const out = new Set(); + const k = this.nodeKeys.get(decl); + if (k === undefined) return out; + for (const id of this.declIndex.get(k) ?? []) for (const a of this.nodes[id]!.atoms) out.add(a); + return out; + } + + receiverCtx(atom: Atom): string { + return atom.kind === "instance" || atom.kind === "class" ? `C${atom.id}` : ""; + } + + infoOfAtom(atom: Atom): FunctionInfo | undefined { + if (atom.node === undefined) return undefined; + return this.functions.get(`${atom.ctx ?? ""}|${this.nodeKey(atom.node)}`); + } + + // ---- functions --------------------------------------------------------------- + + functionInfo(node: Node, ownerClass: Node | null, ctxArg = ""): FunctionInfo { + // A nested closure walked inside a clone shares the clone's context. + const inherit = + ctxArg === "" && + this.ctxRoot !== null && + node.getSourceFile() === this.ctxRoot.getSourceFile() && + node.pos >= this.ctxRoot.pos && + node.end <= this.ctxRoot.end; + const ctx = ctxArg !== "" ? ctxArg : inherit ? this.ctx : ""; + const root = ctxArg !== "" ? node : inherit ? this.ctxRoot : null; + const fkey = `${ctx}|${this.nodeKey(node)}`; + const existing = this.functions.get(fkey); + if (existing !== undefined) return existing; + const savedCtx = this.ctx; + const savedRoot = this.ctxRoot; + this.ctx = ctx; + this.ctxRoot = root; + const fn = node as ts.FunctionLikeDeclaration; + const isArrow = ts.isArrowFunction(node); + const params: number[] = []; + let rest = -1; + for (const p of list(fn.parameters)) { + const id = this.binding(p); + if (p.dotDotDotToken !== undefined) rest = params.length; + params.push(id); + } + const mods = list((fn as { modifiers?: { nodes: readonly Node[] } }).modifiers); + const sf = node.getSourceFile(); + const nameNode = (fn as { name?: Node }).name; + const name = nameNode !== undefined ? (propertyNameText(nameNode) ?? "") : null; + const owner = ownerClass !== null ? this.className(ownerClass) : null; + const label = `${owner !== null ? `${owner}.` : ""}${name ?? (ts.isConstructorDeclaration(node) ? "constructor" : "")}@${locLabel(node)}`; + const info: FunctionInfo = { + node, + ctx, + root, + walked: false, + label: ctx === "" ? label : `${label} [${this.ctxLabel(ctx)}]`, + params, + rest, + thisNode: isArrow && this.currentFn !== null ? this.currentFn.thisNode : this.newNode(), + ret: this.newNode(), + isArrow, + isAsync: mods.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword), + isGenerator: (fn as { asteriskToken?: Node }).asteriskToken !== undefined, + usesArguments: false, + argPos: null, + ownerClass, + inPackage: isPackageJsFile(sf), + }; + this.functions.set(fkey, info); + this.ctx = savedCtx; + this.ctxRoot = savedRoot; + return info; + } + + /** Walks a function's body the first time it can run. */ + activate(info: FunctionInfo): void { + if (info.walked) return; + info.walked = true; + if (ts.isSourceFile(info.node) || ts.isClassLikeDeclaration(info.node)) return; + const fn = info.node as ts.FunctionLikeDeclaration; + const savedCtx = this.ctx; + const savedRoot = this.ctxRoot; + const saved = this.currentFn; + const savedLoop = this.loopDepth; + this.ctx = info.ctx; + this.ctxRoot = info.root; + this.currentFn = info; + this.loopDepth = 0; + for (const p of list(fn.parameters)) { + if (p.initializer !== undefined) this.edge(this.expr(p.initializer), this.binding(p)); + if (p.dotDotDotToken !== undefined) { + const arr = this.atoms.intern(`rest:${this.nodes.length}`, () => ({ + kind: "array", + label: `rest@${locLabel(p)}`, + node: p, + })); + this.add(this.binding(p), arr); + } + this.bindPattern(p.name, this.binding(p)); + } + const body = fn.body; + if (body !== undefined) { + if (ts.isBlock(body)) { + this.stmts(list(body.statements)); + // A call that runs off the end of the body returns undefined. + if (canComplete(list(body.statements))) this.add(info.ret, this.prim("undefined")); + } else this.edge(this.expr(body as ts.Expression), info.ret); + } + this.currentFn = saved; + this.loopDepth = savedLoop; + this.ctx = savedCtx; + this.ctxRoot = savedRoot; + if (info.usesArguments) { + for (const p of info.params) this.add(p, this.top); + } + } + + /** The atom of a function node (memoized by its info). */ + fnAtom(node: Node, ownerClass: Node | null, ctx = ""): Atom { + const info = this.functionInfo(node, ownerClass, ctx); + return this.atoms.intern(`fn:${info.ret}`, () => ({ + kind: "function", + label: info.label, + node, + ctx: info.ctx, + })); + } + + ctxLabel(ctx: string): string { + if (ctx.startsWith("C")) return `this=${this.atoms.atoms[Number(ctx.slice(1))]?.label ?? ctx}`; + return ctx; + } + + className(decl: Node): string { + const own = (decl as ts.ClassLikeDeclaration).name?.text; + if (own !== undefined) return own; + const p = decl.parent; + if (p !== undefined && ts.isVariableDeclaration(p) && ts.isIdentifier(p.name)) + return p.name.text; + return ``; + } + + classAtoms(decl: Node): { cls: Atom; inst: Atom } { + const name = this.className(decl); + const cls = this.atoms.intern(`class:${this.nodeKey(decl)}`, () => ({ + kind: "class", + label: `class ${name}`, + node: decl, + })); + const inst = this.atoms.intern(`inst:${this.nodeKey(decl)}`, () => ({ + kind: "instance", + label: name, + node: decl, + })); + this.ensureClass(decl); + return { cls, inst }; + } + + private readonly nodeKeys = new Map(); + nodeKey(node: Node): number { + let k = this.nodeKeys.get(node); + if (k === undefined) { + k = this.nodeKeys.size; + this.nodeKeys.set(node, k); + } + return k; + } + + /** Registers a class: member functions, field initializers (as a pseudo + * function run at construction) and static initializers. */ + ensureClass(decl: Node): { initFn: FunctionInfo; staticInit: FunctionInfo } { + const existing = this.classInfo.get(decl); + if (existing !== undefined) return existing; + const c = decl as ts.ClassLikeDeclaration; + const label = this.className(decl); + const mk = (suffix: string): FunctionInfo => ({ + node: decl, + ctx: "", + root: null, + walked: true, + label: `${label}.${suffix}@${locLabel(decl)}`, + params: [], + rest: -1, + thisNode: this.newNode(), + ret: this.newNode(), + isArrow: false, + isAsync: false, + isGenerator: false, + usesArguments: false, + argPos: null, + ownerClass: decl, + inPackage: isPackageJsFile(decl.getSourceFile()), + }); + const entry = { initFn: mk(""), staticInit: mk("") }; + this.classInfo.set(decl, entry); + const cls = this.atoms.intern(`class:${this.nodeKey(decl)}`, () => ({ + kind: "class", + label: `class ${label}`, + node: decl, + })); + this.add(entry.staticInit.thisNode, cls); + const saved = this.currentFn; + for (const m of list(c.members)) { + const statics = isStatic(m); + if ( + ts.isMethodDeclaration(m) || + ts.isGetAccessorDeclaration(m) || + ts.isSetAccessorDeclaration(m) || + ts.isConstructorDeclaration(m) + ) { + continue; + } + if (ts.isPropertyDeclaration(m)) { + const name = propertyNameText(m.name); + const init = statics ? entry.staticInit : entry.initFn; + this.currentFn = init; + const value = + m.initializer !== undefined + ? this.exprIn(m.initializer, decl) + : this.constNode(this.prim("undefined")); + if (name !== null) this.storeConstraint(init.thisNode, name, value, m); + continue; + } + if (ts.isClassStaticBlockDeclaration(m)) { + this.currentFn = entry.staticInit; + this.stmts(list(m.body.statements)); + } + } + this.currentFn = saved; + return entry; + } + + /** An initializer expression: arrow functions in field initializers take + * the instance as `this` and belong to the class. */ + exprIn(e: ts.Expression, ownerClass: Node): number { + const u = unparen(e); + if (ts.isArrowFunction(u) || ts.isFunctionExpression(u)) { + const n = this.newNode(); + this.add(n, this.fnAtom(u, ts.isArrowFunction(u) ? null : ownerClass)); + return n; + } + return this.expr(e); + } + + /** `new C(args)` on a class atom: the instance, the field initializers of + * every class in the chain, and the constructor (or the implicit one). */ + construct( + decl: Node, + args: number[], + spread: boolean, + dst: number, + site: Node, + inst?: Atom, + ): void { + const atoms = this.classAtoms(decl); + const instance = inst ?? atoms.inst; + this.add(dst, instance); + this.callConstructorChain(decl, instance, args, spread, site); + } + + callConstructorChain( + decl: Node, + instance: Atom, + args: number[], + spread: boolean, + site: Node, + ): void { + const key = `ctor:${this.nodeKey(decl)}:${instance.id}:${this.nodeKey(site)}`; + if (this.boundCalls.has(key)) return; + this.boundCalls.add(key); + const entry = this.ensureClass(decl); + this.add(entry.initFn.thisNode, instance); + const ctor = list((decl as ts.ClassLikeDeclaration).members).find((m) => + ts.isConstructorDeclaration(m), + ); + if (ctor !== undefined) { + const info = this.functionInfo(ctor, decl, this.receiverCtx(instance)); + this.activate(info); + this.add(info.thisNode, instance); + this.bindArgs(info, args, spread); + this.edgeFor(site, info); + return; + } + const base = this.baseClass(decl); + if (base) this.callConstructorChain(base, instance, args, spread, site); + } + + bindArgs(info: FunctionInfo, args: number[], spread: boolean): void { + if (info.argPos !== null) { + for (let i = 0; i < info.argPos.length; i++) { + if (spread || i >= info.argPos.length - 1) { + for (const a of args.slice(i)) this.edge(a, info.argPos[i]!); + if (spread) this.add(info.argPos[i]!, this.top); + } else if (i < args.length) this.edge(args[i]!, info.argPos[i]!); + else this.add(info.argPos[i]!, this.prim("undefined")); + } + } + if (spread) { + for (const p of info.params) this.add(p, this.top); + for (const a of args) this.sink(a, `spread into ${info.label}`); + return; + } + for (let i = 0; i < info.params.length; i++) { + if (i === info.rest) { + const restAtoms = [...this.nodes[info.params[i]!]!.atoms]; + for (const ra of restAtoms) + for (let j = i; j < args.length; j++) this.edge(args[j]!, this.element(ra)); + break; + } + if (i < args.length) this.edge(args[i]!, info.params[i]!); + else this.add(info.params[i]!, this.prim("undefined")); + } + } + + /** Call of a function atom with a receiver node. */ + callFunction( + info: FunctionInfo, + thisNode: number | null, + args: number[], + spread: boolean, + dst: number, + site: Node, + ): void { + // `() => Object.create(null)` helpers: each call site gets its own + // dictionary, as if the helper were inlined. + if (this.isFactory(info.node)) { + const d = this.atoms.intern(`dict:${this.nodeKey(site)}`, () => ({ + kind: "dictionary", + label: `Object.create@${locLabel(site)}`, + node: site, + })); + this.add(dst, d); + return; + } + this.activate(info); + const fnNode = info.node; + const key = `call:${info.ctx}|${this.nodeKey(fnNode)}:${this.nodeKey(site)}:${thisNode ?? -1}:${dst}`; + if (this.boundCalls.has(key)) return; + this.boundCalls.add(key); + if (thisNode !== null && !info.isArrow) this.edge(thisNode, info.thisNode); + this.bindArgs(info, args, spread); + if (info.isAsync) { + const p = this.promiseAtom(fnNode, `async ${info.label}`); + this.awaitInto(info.ret, this.field(p, "value")); + this.add(dst, p); + } else if (info.isGenerator) { + this.add(dst, this.top); + } else this.edge(info.ret, dst); + this.edgeFor(site, info); + } + + private readonly factories = new Map(); + isFactory(node: Node): boolean { + let r = this.factories.get(node); + if (r === undefined) { + r = false; + const fn = node as ts.FunctionLikeDeclaration; + let e: Node | undefined = fn.body; + if (e !== undefined && ts.isBlock(e)) { + const st = e.statements.nodes; + e = st.length === 1 && ts.isReturnStatement(st[0]!) ? st[0]!.expression : undefined; + } + if (e !== undefined && !ts.isSourceFile(node)) { + const u = unparen(e as ts.Expression); + r = + ts.isCallExpression(u) && + u.expression.getText() === "Object.create" && + u.arguments.nodes.length === 1 && + u.arguments.nodes[0]!.kind === ts.SyntaxKind.NullKeyword; + } + this.factories.set(node, r); + } + return r; + } + + promiseAtom(site: Node, label: string): Atom { + return this.atoms.intern(`promise:${this.nodeKey(site)}`, () => ({ + kind: "promise", + label: `Promise@${label}`, + node: site, + })); + } + + /** dst ⊇ awaited(src). */ + awaitInto(src: number, dst: number): void { + this.on(src, (a) => { + if (a.kind === "promise") this.edge(this.field(a, "value"), dst); + else this.add(dst, a); + }); + } + + syntheticArray(site: Node, tag: string): Atom { + return this.atoms.intern(`arr:${tag}:${this.nodeKey(site)}`, () => ({ + kind: "array", + label: `${tag}[]@${locLabel(site)}`, + node: site, + })); + } + + // ---- statements ---------------------------------------------------------------- + + moduleBody(sf: ts.SourceFile): void { + const info: FunctionInfo = { + node: sf, + ctx: "", + root: null, + walked: true, + label: ``, + params: [], + rest: -1, + thisNode: this.constNode(this.prim("undefined")), + ret: this.newNode(), + isArrow: false, + isAsync: false, + isGenerator: false, + usesArguments: false, + argPos: null, + ownerClass: null, + inPackage: isPackageJsFile(sf), + }; + this.functions.set(`|${this.nodeKey(sf)}`, info); + this.currentFn = info; + this.stmts(list(sf.statements)); + this.currentFn = null; + } + + stmts(stmts: readonly Node[]): void { + for (const s of stmts) this.stmt(s); + } + + stmt(s: Node): void { + if (ts.isVariableStatement(s)) { + for (const d of list(s.declarationList.declarations)) this.varDecl(d); + return; + } + if (ts.isExpressionStatement(s)) { + this.expr(s.expression); + return; + } + if (ts.isReturnStatement(s)) { + if (this.currentFn === null) return; + if (s.expression !== undefined) this.edge(this.expr(s.expression), this.currentFn.ret); + else this.add(this.currentFn.ret, this.prim("undefined")); + return; + } + if (ts.isFunctionDeclaration(s)) { + const b = this.binding(s); + const saved = this.currentFn; + this.currentFn = null; + this.add(b, this.fnAtom(s, null)); + this.currentFn = saved; + return; + } + if (ts.isClassDeclaration(s)) { + const b = this.binding(s); + this.add(b, this.classAtoms(s).cls); + return; + } + if (isLoop(s)) { + if (ts.isForOfStatement(s) || ts.isForInStatement(s)) { + const src = this.expr(s.expression); + const target = this.newNode(); + if (ts.isForInStatement(s)) this.add(target, this.prim("string")); + else this.iterateInto(src, target, s); + const init = s.initializer; + if (ts.isVariableDeclarationList(init)) { + for (const d of list(init.declarations)) { + this.edge(target, this.binding(d)); + this.bindPattern(d.name, this.binding(d)); + } + } else this.assignTo(init as ts.Expression, target); + } + this.loopDepth++; + if (ts.isForStatement(s)) { + if (s.initializer !== undefined) { + if (ts.isVariableDeclarationList(s.initializer)) + for (const d of list(s.initializer.declarations)) this.varDecl(d); + else this.expr(s.initializer as ts.Expression); + } + if (s.condition !== undefined) this.expr(s.condition); + if (s.incrementor !== undefined) this.expr(s.incrementor); + } + if (ts.isWhileStatement(s) || ts.isDoStatement(s)) this.expr(s.expression); + const body = (s as { statement?: Node }).statement; + if (body !== undefined) this.stmt(body); + this.loopDepth--; + return; + } + if (ts.isTryStatement(s)) { + this.stmts(list(s.tryBlock.statements)); + if (s.catchClause !== undefined) { + const v = s.catchClause.variableDeclaration; + if (v !== undefined) { + this.add(this.binding(v), this.top); + this.bindPattern(v.name, this.binding(v)); + } + this.stmts(list(s.catchClause.block.statements)); + } + if (s.finallyBlock !== undefined) this.stmts(list(s.finallyBlock.statements)); + return; + } + if (ts.isThrowStatement(s)) { + this.sink(this.expr(s.expression), `thrown at ${locLabel(s)}`); + return; + } + if ( + ts.isImportDeclaration(s) || + ts.isExportDeclaration(s) || + ts.isInterfaceDeclaration(s) || + ts.isTypeAliasDeclaration(s) + ) + return; + if (ts.isExportAssignment(s)) { + this.expr(s.expression); + return; + } + if (ts.isIfStatement(s) && this.currentFn !== null) { + this.ifStatement(s); + return; + } + // Blocks, ifs, switches, labels: walk children, evaluating expressions. + ts.forEachChild(s, (child) => { + if (ts.isExpression(child)) this.expr(child); + else this.stmt(child); + return undefined; + }); + } + + varDecl(d: ts.VariableDeclaration): void { + const b = this.binding(d); + if (d.initializer !== undefined) this.edge(this.expr(d.initializer), b); + this.bindPattern(d.name, b); + } + + /** Destructuring: binding elements read from the source node. */ + bindPattern(name: Node, src: number): void { + if (ts.isIdentifier(name)) return; + if (ts.isObjectBindingPattern(name)) { + for (const el of list(name.elements)) { + const b = this.binding(el); + if (el.dotDotDotToken !== undefined) { + this.add(b, this.top); + continue; + } + const keyNode = el.propertyName ?? el.name; + const key = keyNode !== undefined ? propertyNameText(keyNode) : null; + if (key !== null) this.loadConstraint(src, key, b, el); + else this.add(b, this.top); + if (el.initializer !== undefined) this.edge(this.expr(el.initializer), b); + if (el.name !== undefined) this.bindPattern(el.name, b); + } + return; + } + if (ts.isArrayBindingPattern(name)) { + let i = 0; + for (const el of list(name.elements)) { + if (ts.isBindingElement(el)) { + const b = this.binding(el); + if (el.dotDotDotToken !== undefined) this.add(b, this.top); + else this.iterateInto(src, b, el, i); + if (el.initializer !== undefined) this.edge(this.expr(el.initializer), b); + if (el.name !== undefined) this.bindPattern(el.name, b); + } + i++; + } + } + } + + /** Element values of an iterable. */ + iterateInto(src: number, dst: number, site: Node, _index?: number): void { + this.on(src, (a) => { + if (a.kind === "array" || a.kind === "collection") this.edge(this.element(a), dst); + else if (a.kind === "prim" && a.label === "string") this.add(dst, this.prim("string")); + else if (a.kind === "prim" || a.kind === "object" || a.kind === "dictionary") { + /* not iterable (a TypeError): no value. Literals with a + Symbol.iterator member have a computed key; they are G sites. */ + } else { + this.add(dst, this.top); + if (a.kind !== "top") this.escape(a, `iterated at ${locLabel(site)}`); + } + }); + } + + // ---- expressions --------------------------------------------------------------- + + expr(e: ts.Expression): number { + const key = this.ctxKey(e); + const memo = this.exprMemo.get(key); + if (memo !== undefined) return memo; + const id = this.exprUncached(e); + this.exprMemo.set(key, id); + if (this.traceTop && !this.nodeLabel.has(id)) + this.nodeLabel.set( + id, + `expr ${e.getText().slice(0, 40).replace(/\s+/g, " ")}@${locLabel(e)}`, + ); + return id; + } + + private exprUncached(e: ts.Expression): number { + const u = unparen(e); + if (u !== e) return this.expr(u); + switch (e.kind) { + case ts.SyntaxKind.StringLiteral: + case ts.SyntaxKind.NoSubstitutionTemplateLiteral: + case ts.SyntaxKind.TemplateExpression: + if (ts.isTemplateExpression(e)) + for (const span of list(e.templateSpans)) this.expr(span.expression); + return this.constNode(this.prim("string")); + case ts.SyntaxKind.NumericLiteral: + return this.constNode(this.prim("number")); + case ts.SyntaxKind.BigIntLiteral: + return this.constNode(this.prim("bigint")); + case ts.SyntaxKind.TrueKeyword: + return this.constNode(this.boolLiteral(true)); + case ts.SyntaxKind.FalseKeyword: + return this.constNode(this.boolLiteral(false)); + case ts.SyntaxKind.NullKeyword: + return this.constNode(this.prim("null")); + case ts.SyntaxKind.RegularExpressionLiteral: + return this.constNode(this.builtin("RegExp.instance")); + case ts.SyntaxKind.ThisKeyword: + return this.currentFn !== null + ? this.currentFn.thisNode + : this.constNode(this.prim("undefined")); + case ts.SyntaxKind.SuperKeyword: + return this.constNode(this.top); + } + if (ts.isIdentifier(e)) return this.identifier(e); + if (ts.isObjectLiteralExpression(e)) return this.objectLiteral(e); + if (ts.isArrayLiteralExpression(e)) return this.arrayLiteral(e); + if (ts.isArrowFunction(e) || ts.isFunctionExpression(e)) { + const n = this.newNode(); + this.add(n, this.fnAtom(e, null)); + if (ts.isFunctionExpression(e) && e.name !== undefined) + this.add(this.binding(e), this.fnAtom(e, null)); + return n; + } + if (ts.isClassExpression(e)) { + const n = this.newNode(); + this.add(n, this.classAtoms(e).cls); + return n; + } + if (ts.isPropertyAccessExpression(e)) { + this.noteSite(e); + // `globalThis.Proxy` and the like are the modeled globals. + const object = unparen(e.expression); + if ( + ts.isIdentifier(object) && + object.text === "globalThis" && + MODELED_GLOBALS.has(e.name.text) + ) { + const d = this.resolve(object); + if (d === undefined || d.getSourceFile().isDeclarationFile) + return this.globalNode(e.name.text); + } + const recv = this.expr(e.expression); + const name = e.name.text; + const dst = this.newNode(); + this.loadConstraint(recv, name, dst, e); + this.ops.push({ kind: "read", recv, name, node: e }); + return dst; + } + if (ts.isElementAccessExpression(e)) { + const recv = this.expr(e.expression); + const key = unparen(e.argumentExpression); + const dst = this.newNode(); + const name = + ts.isStringLiteral(key) || ts.isNoSubstitutionTemplateLiteral(key) ? key.text : null; + if (name !== null) { + this.loadConstraint(recv, name, dst, e); + this.ops.push({ kind: "read", recv, name, node: e }); + } else { + this.expr(e.argumentExpression); + this.computedLoad(recv, dst, e); + this.ops.push({ kind: "computedRead", recv, name: null, node: e }); + } + return dst; + } + if (ts.isCallExpression(e)) return this.call(e); + if (ts.isNewExpression(e)) return this.newExpr(e); + if (ts.isBinaryExpression(e)) return this.binary(e); + if (ts.isConditionalExpression(e)) { + this.expr(e.condition); + const n = this.newNode(); + this.edge(this.expr(e.whenTrue), n); + this.edge(this.expr(e.whenFalse), n); + return n; + } + if (ts.isAwaitExpression(e)) { + const n = this.newNode(); + this.awaitInto(this.expr(e.expression), n); + return n; + } + if (ts.isTypeOfExpression(e)) { + this.expr(e.expression); + return this.constNode(this.prim("string")); + } + if (ts.isVoidExpression(e)) { + this.expr(e.expression); + return this.constNode(this.prim("undefined")); + } + if (ts.isDeleteExpression(e)) { + const target = unparen(e.expression); + if (ts.isPropertyAccessExpression(target) || ts.isElementAccessExpression(target)) { + const recv = this.expr(target.expression); + const name = ts.isPropertyAccessExpression(target) ? target.name.text : null; + this.ops.push({ kind: "delete", recv, name, node: e }); + } + return this.constNode(this.prim("boolean")); + } + if (ts.isPrefixUnaryExpression(e) || ts.isPostfixUnaryExpression(e)) { + const operand = e.operand; + this.expr(operand); + if ( + e.operator === ts.SyntaxKind.PlusPlusToken || + e.operator === ts.SyntaxKind.MinusMinusToken + ) + this.assignTo(operand, this.constNode(this.prim("number"))); + if (ts.isPrefixUnaryExpression(e) && e.operator === ts.SyntaxKind.ExclamationToken) + return this.constNode(this.prim("boolean")); + return this.constNode(this.prim("number")); + } + if (ts.isSpreadElement(e)) return this.expr(e.expression); + if (ts.isTaggedTemplateExpression(e)) return this.taggedTemplate(e); + if (ts.isYieldExpression(e)) { + if (e.expression !== undefined) + this.sink(this.expr(e.expression), `yielded at ${locLabel(e)}`); + return this.constNode(this.top); + } + if (ts.isMetaProperty(e)) return this.constNode(this.top); + // Anything else: evaluate children, answer ⊤. + ts.forEachChild(e, (child) => { + if (ts.isExpression(child)) this.expr(child); + return undefined; + }); + return this.constNode(this.top); + } + + identifier(id: ts.Identifier): number { + if (id.text === "undefined") return this.constNode(this.prim("undefined")); + if (id.text === "arguments") { + const fn = this.enclosingNonArrow(); + if (fn !== null) { + const parent = id.parent; + const forwarding = parent !== undefined && ts.isSpreadElement(parent); + if (!forwarding) fn.usesArguments = true; + const arr = this.syntheticArray(id, "arguments"); + for (const p of fn.params) this.edge(p, this.element(arr)); + return this.constNode(arr); + } + } + const decl = this.resolve(id); + if (decl === undefined) return this.globalNode(id.text); + if (decl.getSourceFile().isDeclarationFile) return this.globalNode(id.text); + if (!this.files.includes(decl.getSourceFile())) return this.constNode(this.top); + if (ts.isFunctionDeclaration(decl)) { + const b = this.binding(decl); + const saved = this.currentFn; + this.currentFn = null; + this.add(b, this.fnAtom(decl, null)); + this.currentFn = saved; + return b; + } + if (ts.isClassDeclaration(decl) || ts.isClassExpression(decl)) { + const b = this.binding(decl); + this.add(b, this.classAtoms(decl).cls); + return b; + } + if (ts.isFunctionExpression(decl)) return this.binding(decl); + if ( + ts.isImportSpecifier(decl) || + ts.isImportClause(decl) || + ts.isNamespaceImport(decl) || + ts.isImportEqualsDeclaration(decl) + ) + return this.constNode(this.top); + if (ts.isSourceFile(decl) || ts.isModuleDeclaration(decl)) return this.namespaceNode(decl); + // A CommonJS export the checker resolved an import or require binding + // to (`module.exports = { name }`, `exports.name = v`): the member of the + // module's exports. + const exported = cjsExportName(decl); + if (exported !== null && decl.getSourceFile() !== id.getSourceFile()) { + const dst = this.newNode(); + this.loadConstraint(this.namespaceNode(decl.getSourceFile()), exported, dst, id); + return dst; + } + return this.narrowed(id, decl, this.binding(decl)); + } + + /** Flow narrowing at guarded references (S1 handles the guards hot code + * uses: `x instanceof C` and calls of predicate functions whose body is + * `… && x instanceof C`, in the true arm of an `if`, `?:` or `&&`). The + * narrowed value keeps the guard's class arms and ⊤. */ + narrowed(id: ts.Identifier, decl: Node, value: number): number { + let child: Node = id; + let p = id.parent; + while (p !== undefined && !ts.isFunctionLike(p) && !ts.isSourceFile(p)) { + let cond: ts.Expression | undefined; + if (ts.isIfStatement(p) && p.thenStatement === child) cond = p.expression; + else if (ts.isConditionalExpression(p) && p.whenTrue === child) cond = p.condition; + else if ( + ts.isBinaryExpression(p) && + p.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken && + p.right === child + ) + cond = p.left; + if (cond !== undefined) { + const cls = this.guardClass(cond, decl); + if (cls !== null) { + const out = this.newNode(); + this.on(value, (a) => { + if (a.kind === "top") this.add(out, a); + else if ( + cls === "Promise" + ? a.kind === "promise" + : a.kind === "instance" && + a.node !== undefined && + this.classChain(a.node).includes(cls) + ) + this.add(out, a); + }); + return out; + } + } + child = p; + p = p.parent; + } + return value; + } + + /** The class a condition proves `decl`'s value an instance of, if any. */ + guardClass(cond: ts.Expression, decl: Node): Node | "Promise" | null { + const e = unparen(cond); + if (ts.isBinaryExpression(e)) { + if (e.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) + return this.guardClass(e.left, decl) ?? this.guardClass(e.right, decl); + if (e.operatorToken.kind !== ts.SyntaxKind.InstanceOfKeyword) return null; + const l = unparen(e.left); + if (!ts.isIdentifier(l) || this.resolve(l) !== decl) return null; + return this.instanceofTarget(e.right); + } + if ( + ts.isCallExpression(e) && + ts.isIdentifier(unparen(e.expression)) && + e.arguments.nodes.length === 1 + ) { + const arg = unparen(e.arguments.nodes[0]!); + if (!ts.isIdentifier(arg) || this.resolve(arg) !== decl) return null; + const fnDecl = this.resolve(unparen(e.expression)); + let fn: Node | undefined = fnDecl; + if (fn !== undefined && ts.isVariableDeclaration(fn) && fn.initializer !== undefined) + fn = unparen(fn.initializer); + if ( + fn === undefined || + !(ts.isArrowFunction(fn) || ts.isFunctionDeclaration(fn) || ts.isFunctionExpression(fn)) + ) + return null; + const param = fn.parameters.nodes[0]; + if (param === undefined || !ts.isIdentifier(param.name)) return null; + let body: Node | undefined = fn.body; + if (body !== undefined && ts.isBlock(body)) { + const stmts = body.statements.nodes; + body = + stmts.length === 1 && ts.isReturnStatement(stmts[0]!) ? stmts[0]!.expression : undefined; + } + if (body === undefined) return null; + return this.predicateClass(body as ts.Expression, param); + } + return null; + } + + /** `… && p instanceof C` in a predicate body over parameter `p`. */ + predicateClass(e: ts.Expression, param: Node): Node | "Promise" | null { + const u = unparen(e); + if (!ts.isBinaryExpression(u)) return null; + if (u.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) + return this.predicateClass(u.right, param) ?? this.predicateClass(u.left, param); + if (u.operatorToken.kind !== ts.SyntaxKind.InstanceOfKeyword) return null; + const l = unparen(u.left); + if (!ts.isIdentifier(l) || this.resolve(l) !== param) return null; + return this.instanceofTarget(u.right); + } + + instanceofTarget(right: ts.Expression): Node | "Promise" | null { + const r = unparen(right); + if (!ts.isIdentifier(r)) return null; + const d = this.resolve(r); + if (d === undefined || d.getSourceFile().isDeclarationFile) + return r.text === "Promise" ? "Promise" : null; + return ts.isClassLikeDeclaration(d) ? d : null; + } + + private readonly namespaces = new Map(); + /** Module namespaces: a CommonJS module's `module.exports` value is the + * namespace's `exports` field, and member reads see its members too. */ + private readonly namespaceAtoms = new Set(); + /** `import * as ns`: an object whose fields are the module's exports. */ + namespaceNode(mod: Node): number { + const existing = this.namespaces.get(mod); + if (existing !== undefined) return existing; + const n = this.newNode(); + this.namespaces.set(mod, n); + const atom = this.atoms.intern(`ns:${this.nodeKey(mod)}`, () => ({ + kind: "object", + label: `namespace ${shortFile((mod as ts.SourceFile).fileName ?? "")}`, + node: mod, + })); + this.namespaceAtoms.add(atom); + this.add(n, atom); + const sym = this.checker.getSymbolAtLocation(mod); + if (sym !== undefined) { + for (const ex of this.checker.getExportsOfModule(sym)) { + let target = ex; + if (target.flags & ts.SymbolFlags.Alias) { + try { + target = this.checker.getAliasedSymbol(target); + } catch { + continue; + } + } + const d = target.valueDeclaration ?? target.declarations[0]; + if (d === undefined) continue; + const value = this.declValue(d); + if (value !== null) this.edge(value, this.field(atom, String(ex.escapedName))); + } + } + return n; + } + + declValue(d: Node): number | null { + if (d.getSourceFile().isDeclarationFile || !this.files.includes(d.getSourceFile())) return null; + if (ts.isFunctionDeclaration(d)) { + const b = this.binding(d); + const saved = this.currentFn; + this.currentFn = null; + this.add(b, this.fnAtom(d, null)); + this.currentFn = saved; + return b; + } + if (ts.isClassDeclaration(d)) { + const b = this.binding(d); + this.add(b, this.classAtoms(d).cls); + return b; + } + if (ts.isVariableDeclaration(d) || ts.isBindingElement(d) || ts.isParameter(d)) + return this.binding(d); + if (ts.isSourceFile(d)) return this.namespaceNode(d); + return null; + } + + globalNode(name: string): number { + if (MODELED_GLOBALS.has(name)) return this.constNode(this.builtin(name)); + if (name === "NaN" || name === "Infinity") return this.constNode(this.prim("number")); + return this.constNode(this.top); + } + + enclosingNonArrow(): FunctionInfo | null { + const fn = this.currentFn; + if (fn === null) return null; + if (!fn.isArrow) return fn; + // An arrow's `arguments` is its enclosing function's. + let p: Node | undefined = fn.node.parent; + while (p !== undefined) { + if (ts.isFunctionLike(p) && !ts.isArrowFunction(p)) + return ( + this.functions.get(this.ctxKey(p)) ?? this.functions.get(`|${this.nodeKey(p)}`) ?? null + ); + p = p.parent; + } + return null; + } + + objectLiteral(e: ts.ObjectLiteralExpression): number { + const keys: string[] = []; + let keysKnown = true; + let hasAccessor = false; + let hasProto = false; + let hasComputedKey = false; + const spreads: number[] = []; + const inPackage = isPackageJsFile(e.getSourceFile()); + const label = (): string => + `{${keys.slice(0, 6).join(",")}${keys.length > 6 ? ",…" : ""}${keysKnown ? "" : ",…"}}@${locLabel(e)}`; + // keys first, so the label is complete + for (const p of list(e.properties)) { + if (ts.isSpreadAssignment(p)) { + keysKnown = false; + continue; + } + const name = (p as { name?: Node }).name; + const key = name !== undefined ? propertyNameText(name) : null; + if (key === null) { + hasComputedKey = true; + keysKnown = false; + continue; + } + if (key === "__proto__" && ts.isPropertyAssignment(p) && !ts.isComputedPropertyName(p.name)) + hasProto = true; + if (!keys.includes(key)) keys.push(key); + } + const atom = this.atoms.intern(`obj:${this.nodeKey(e)}`, () => ({ + kind: "object", + label: label(), + node: e, + })); + const n = this.newNode(); + this.add(n, atom); + for (const p of list(e.properties)) { + if (ts.isSpreadAssignment(p)) { + const src = this.expr(p.expression); + spreads.push(src); + this.copyFields(src, atom, p); + continue; + } + if (ts.isGetAccessorDeclaration(p) || ts.isSetAccessorDeclaration(p)) { + hasAccessor = true; + const info = this.functionInfo(p, null); + this.activate(info); + this.add(info.thisNode, atom); + const key = propertyNameText(p.name); + if (key !== null && ts.isGetAccessorDeclaration(p)) + this.edge(info.ret, this.field(atom, key)); + continue; + } + const name = (p as { name?: Node }).name; + const key = name !== undefined ? propertyNameText(name) : null; + if (name !== undefined && ts.isComputedPropertyName(name)) this.expr(name.expression); + let value: number; + if (ts.isPropertyAssignment(p)) value = this.expr(p.initializer); + else if (ts.isShorthandPropertyAssignment(p)) { + const sym = this.checker.getShorthandAssignmentValueSymbol(p); + const d = sym?.valueDeclaration ?? sym?.declarations[0]; + const id = p.name as ts.Identifier; + value = d !== undefined ? (this.declValue(d) ?? this.identifier(id)) : this.identifier(id); + } else if (ts.isMethodDeclaration(p)) { + const fa = this.fnAtom(p, null); + this.add(this.infoOfAtom(fa)!.thisNode, atom); + value = this.constNode(fa); + } else continue; + if (key !== null) this.edge(value, this.field(atom, key)); + else this.edge(value, this.anyField(atom)); + } + if (!this.siteAtoms.has(atom) && this.siteAtoms.add(atom)) + this.sites.push({ + atom, + node: e, + kind: "object", + keys: keysKnown ? keys : null, + ownKeys: keys, + hasAccessor, + hasProto, + hasComputedKey, + spreads, + inPackage, + }); + return n; + } + + /** Values written under unknown keys. */ + anyField(atom: Atom): number { + return this.field(atom, "*"); + } + + /** Loads waiting for an atom's first write of a field, by field name. */ + private readonly fieldReaders = new Map>(); + + /** dst ⊇ the values written to field `name` of `atom`, now and later, + * without creating the field for atoms that have none. */ + fieldInto(atom: Atom, name: string, dst: number): void { + const id = this.fields.get(atom)?.get(name); + if (id !== undefined) { + this.edge(id, dst); + return; + } + let byName = this.fieldReaders.get(atom); + if (byName === undefined) { + byName = new Map(); + this.fieldReaders.set(atom, byName); + } + let readers = byName.get(name); + if (readers === undefined) { + readers = []; + byName.set(name, readers); + } + readers.push(dst); + } + + /** dst ⊇ the values written to `atom` under unknown keys, now and later. */ + unknownKeysInto(atom: Atom, dst: number): void { + this.fieldInto(atom, "*", dst); + } + + copyFields(src: number, dstAtom: Atom, site: Node): void { + this.on(src, (a) => { + if (a.kind === "top") { + this.edge(this.constNode(this.top), this.anyField(dstAtom)); + return; + } + if (a.kind === "prim" || a.kind === "builtin") return; + if (a.kind === "array") { + this.edge(this.element(a), this.anyField(dstAtom)); + return; + } + // Copy every own field the source has, now and later. + const fm = this.fields.get(a); + const copied = new Set(); + const copy = (): void => { + for (const [name, id] of this.fields.get(a) ?? new Map()) { + if (copied.has(name)) continue; + copied.add(name); + this.edge(id, this.field(dstAtom, name)); + } + }; + copy(); + if (fm === undefined) this.fields.set(a, new Map()); + this.lateCopies.push(copy); + if (a.kind === "instance" || a.kind === "dictionary" || a.kind === "collection") + this.edge(this.constNode(this.top), this.anyField(dstAtom)); + void site; + }); + } + + /** Field copies re-run after solving rounds (fields created later). */ + private readonly lateCopies: (() => void)[] = []; + + /** node ⊇ the own field and element values of every value in it, now and + * later: the values JSON.stringify serializes. */ + childrenInto(node: number): void { + this.on(node, (a) => { + if ( + a.kind !== "object" && + a.kind !== "array" && + a.kind !== "instance" && + a.kind !== "dictionary" + ) + return; + const seen = new Set(); + const copy = (): void => { + for (const [name, id] of this.fields.get(a) ?? new Map()) { + if (seen.has(name) || name.startsWith("#")) continue; + seen.add(name); + this.edge(id, node); + } + const el = this.elements.get(a); + if (el !== undefined && !seen.has("[]")) { + seen.add("[]"); + this.edge(el, node); + } + }; + copy(); + this.lateCopies.push(copy); + }); + } + + /** JSON.stringify's call of `value.toJSON(key)` on an object, array or function that may + * have one (a class member, an own field, or a write under an unknown key, once it exists); + * the result goes to `out`. */ + callToJSON( + value: Atom, + key: number, + out: number, + site: ts.CallExpression | ts.TaggedTemplateExpression, + ): void { + if ( + value.kind !== "object" && + value.kind !== "instance" && + value.kind !== "dictionary" && + value.kind !== "array" && + value.kind !== "function" && + value.kind !== "class" + ) + return; + let called = false; + const call = (): void => { + if (called) return; + const fields = this.fields.get(value); + const has = + fields?.has("toJSON") === true || + fields?.has("*") === true || + ((value.kind === "instance" || value.kind === "class") && + this.lookupMember(value.node!, "toJSON", value.kind === "class") !== null); + if (!has) return; + called = true; + const fn = this.newNode(); + this.loadAtom(value, "toJSON", fn, site); + this.invoke(fn, this.constNode(value), [key], false, out, site); + }; + call(); + if (!called) this.lateCopies.push(call); + } + + arrayLiteral(e: ts.ArrayLiteralExpression): number { + const atom = this.atoms.intern(`arr:${this.nodeKey(e)}`, () => ({ + kind: "array", + label: `[]@${locLabel(e)}`, + node: e, + })); + const n = this.newNode(); + this.add(n, atom); + const el = this.element(atom); + let hasHole = false; + for (const item of list(e.elements)) { + if (ts.isOmittedExpression(item)) { + hasHole = true; + continue; + } + if (ts.isSpreadElement(item)) this.iterateInto(this.expr(item.expression), el, item); + else this.edge(this.expr(item), el); + } + if (!this.siteAtoms.has(atom) && this.siteAtoms.add(atom)) + this.sites.push({ + atom, + node: e, + kind: "array", + keys: null, + ownKeys: [], + hasAccessor: false, + hasProto: hasHole, + hasComputedKey: false, + spreads: [], + inPackage: isPackageJsFile(e.getSourceFile()), + }); + return n; + } + + binary(e: ts.BinaryExpression): number { + const op = e.operatorToken.kind; + if (op === ts.SyntaxKind.EqualsToken) { + const value = this.expr(e.right); + this.assignTo(e.left, value); + return value; + } + if ( + op === ts.SyntaxKind.BarBarEqualsToken || + op === ts.SyntaxKind.QuestionQuestionEqualsToken || + op === ts.SyntaxKind.AmpersandAmpersandEqualsToken + ) { + const n = this.newNode(); + this.edge(this.expr(e.left), n); + const value = this.expr(e.right); + this.edge(value, n); + this.assignTo(e.left, value); + return n; + } + if (op >= ts.SyntaxKind.FirstCompoundAssignment && op <= ts.SyntaxKind.LastCompoundAssignment) { + this.expr(e.left); + this.expr(e.right); + const result = this.constNode( + op === ts.SyntaxKind.PlusEqualsToken ? this.top : this.prim("number"), + ); + if (op === ts.SyntaxKind.PlusEqualsToken) { + this.nodes[result]!.atoms.clear(); + this.add(result, this.prim("string")); + this.add(result, this.prim("number")); + } + this.assignTo(e.left, result); + return result; + } + if ( + op === ts.SyntaxKind.BarBarToken || + op === ts.SyntaxKind.QuestionQuestionToken || + op === ts.SyntaxKind.AmpersandAmpersandToken + ) { + const n = this.newNode(); + this.edge(this.expr(e.left), n); + this.edge(this.expr(e.right), n); + return n; + } + if (op === ts.SyntaxKind.CommaToken) { + this.expr(e.left); + return this.expr(e.right); + } + const l = this.expr(e.left); + const r = this.expr(e.right); + switch (op) { + case ts.SyntaxKind.EqualsEqualsEqualsToken: + case ts.SyntaxKind.ExclamationEqualsEqualsToken: + case ts.SyntaxKind.EqualsEqualsToken: + case ts.SyntaxKind.ExclamationEqualsToken: + case ts.SyntaxKind.LessThanToken: + case ts.SyntaxKind.LessThanEqualsToken: + case ts.SyntaxKind.GreaterThanToken: + case ts.SyntaxKind.GreaterThanEqualsToken: + case ts.SyntaxKind.InstanceOfKeyword: + case ts.SyntaxKind.InKeyword: + if (op === ts.SyntaxKind.InKeyword) + this.ops.push({ kind: "computedRead", recv: r, name: null, node: e }); + return this.constNode(this.prim("boolean")); + case ts.SyntaxKind.PlusToken: { + const n = this.newNode(); + const strOrNum = (a: Atom): void => { + this.add( + n, + a.kind === "prim" && a.label === "number" ? this.prim("number") : this.prim("string"), + ); + if ( + a.kind === "prim" && + (a.label === "boolean" || a.label === "null" || a.label === "undefined") + ) + this.add(n, this.prim("number")); + if (a.kind === "top") this.add(n, this.prim("number")); + }; + this.on(l, strOrNum); + this.on(r, strOrNum); + return n; + } + default: + return this.constNode(this.prim("number")); + } + } + + assignTo(target: ts.Expression, value: number): void { + const t = unparen(target); + if (ts.isIdentifier(t)) { + const decl = this.resolve(t); + if (decl === undefined || decl.getSourceFile().isDeclarationFile) { + this.sink(value, `global write at ${locLabel(t)}`); + return; + } + this.edge(value, this.binding(decl)); + return; + } + if (ts.isPropertyAccessExpression(t)) { + const recv = this.expr(t.expression); + this.storeConstraint(recv, t.name.text, value, t); + this.ops.push({ + kind: t.name.text === "length" ? "lengthWrite" : "write", + recv, + name: t.name.text, + node: t, + }); + return; + } + if (ts.isElementAccessExpression(t)) { + const recv = this.expr(t.expression); + const key = unparen(t.argumentExpression); + if (ts.isStringLiteral(key) || ts.isNoSubstitutionTemplateLiteral(key)) { + this.storeConstraint(recv, key.text, value, t); + this.ops.push({ kind: "write", recv, name: key.text, node: t }); + } else { + this.expr(t.argumentExpression); + this.computedStore(recv, value, t); + this.ops.push({ kind: "computedWrite", recv, name: null, node: t }); + } + return; + } + if (ts.isObjectLiteralExpression(t)) { + for (const p of list(t.properties)) { + if (ts.isPropertyAssignment(p)) { + const key = propertyNameText(p.name); + const n = this.newNode(); + if (key !== null) this.loadConstraint(value, key, n, p); + else this.add(n, this.top); + this.assignTo(p.initializer, n); + } else if (ts.isShorthandPropertyAssignment(p)) { + const n = this.newNode(); + const id = p.name as ts.Identifier; + this.loadConstraint(value, id.text, n, p); + this.assignTo(id, n); + } + } + return; + } + if (ts.isArrayLiteralExpression(t)) { + for (const el of list(t.elements)) { + if (ts.isOmittedExpression(el)) continue; + const n = this.newNode(); + this.iterateInto(value, n, el); + this.assignTo(ts.isSpreadElement(el) ? el.expression : el, n); + } + } + } + + // ---- loads and stores ------------------------------------------------------------ + + loadConstraint(recv: number, name: string, dst: number, site: Node): void { + this.on(recv, (a) => this.loadAtom(a, name, dst, site)); + } + + loadAtom(a: Atom, name: string, dst: number, site: Node): void { + switch (a.kind) { + case "top": + this.add(dst, this.top); + return; + case "prim": + if (name === "length" && a.label === "string") this.add(dst, this.prim("number")); + else if (a.label === "string" || a.label === "number" || a.label === "boolean") + this.add(dst, this.builtin(`${a.label}.${name}`, a)); + return; + case "instance": { + const decl = a.node!; + if (name === "constructor") { + // An own `constructor` field, written by name or under an unknown key, shadows the class. + this.add(dst, this.classAtoms(decl).cls); + this.fieldInto(a, name, dst); + this.unknownKeysInto(a, dst); + return; + } + const member = this.lookupMember(decl, name, false); + if (member?.kind === "method") + this.add( + dst, + this.fnAtom(member.method!, this.ownerOf(member.method!), this.receiverCtx(a)), + ); + if (member !== null && member.get !== undefined) { + const g = this.functionInfo(member.get, this.ownerOf(member.get), this.receiverCtx(a)); + this.activate(g); + this.add(g.thisNode, a); + this.edge(g.ret, dst); + this.edgeFor(site, g); + return; + } + if (this.classChain(decl).some((c) => this.baseClass(c) === undefined)) + this.add(dst, this.top); + this.edge(this.field(a, name), dst); + this.unknownKeysInto(a, dst); + return; + } + case "class": { + const decl = a.node!; + if (name === "prototype" || name === "name") { + this.add(dst, name === "prototype" ? this.top : this.prim("string")); + this.fieldInto(a, name, dst); + this.unknownKeysInto(a, dst); + return; + } + const member = this.lookupMember(decl, name, true); + if (member?.kind === "method") + this.add( + dst, + this.fnAtom(member.method!, this.ownerOf(member.method!), this.receiverCtx(a)), + ); + if (member?.get !== undefined) { + const g = this.functionInfo(member.get, this.ownerOf(member.get), this.receiverCtx(a)); + this.activate(g); + this.add(g.thisNode, a); + this.edge(g.ret, dst); + return; + } + // static fields of the class and its bases + for (const c of this.classChain(decl)) { + const cls = this.classAtoms(c).cls; + this.edge(this.field(cls, name), dst); + this.unknownKeysInto(cls, dst); + } + return; + } + case "function": + // An own field, written by name or under an unknown key, shadows a builtin member. + if (name === "call" || name === "apply" || name === "bind") + this.add(dst, this.builtin(`function.${name}`, a)); + else if (name === "prototype") { + // The default prototype object, or what the program assigns. + this.add(dst, this.protoAtom(a)); + this.edge(this.field(a, "prototype"), dst); + } else if (name === "name") this.add(dst, this.prim("string")); + else if (name === "length") this.add(dst, this.prim("number")); + else { + this.edge(this.field(a, name), dst); + this.unknownKeysInto(a, dst); + return; + } + this.fieldInto(a, name, dst); + this.unknownKeysInto(a, dst); + return; + case "array": + if (name === "length") this.add(dst, this.prim("number")); + else if (/^\d+$/.test(name)) this.edge(this.element(a), dst); + else { + // A named property, written by name or under an unknown key; an own field shadows a method. + if (ARRAY_METHODS.has(name)) { + this.add(dst, this.builtin(`array.${name}`, a)); + this.fieldInto(a, name, dst); + } else this.edge(this.field(a, name), dst); + this.unknownKeysInto(a, dst); + } + return; + case "object": { + this.edge(this.field(a, name), dst); + this.edge(this.anyField(a), dst); + if (this.namespaceAtoms.has(a) && name !== "exports") { + this.loadConstraint(this.field(a, "exports"), name, dst, site); + } + // `new F()` instances read through F's prototype objects. + const fn = this.fnOfInstance.get(a); + if (fn !== undefined) { + const protos = this.newNode(); + this.add(protos, this.protoAtom(fn)); + this.edge(this.field(fn, "prototype"), protos); + this.on(protos, (proto) => { + if (proto.kind === "object") { + // A method stored under a computed key (`F.prototype[k] = fn`) + // may be any name. + this.edge(this.field(proto, name), dst); + this.edge(this.anyField(proto), dst); + } else if (proto.kind === "top") this.add(dst, this.top); + }); + return; + } + if (name === "constructor" || name === "hasOwnProperty" || name === "toString") + this.add(dst, this.builtin(`object.${name}`, a)); + return; + } + case "dictionary": + this.edge(this.anyField(a), dst); + this.edge(this.field(a, name), dst); + return; + case "promise": + if (name === "then" || name === "catch" || name === "finally") + this.add(dst, this.builtin(`promise.${name}`, a)); + return; + case "collection": + if (name === "size") this.add(dst, this.prim("number")); + else this.add(dst, this.builtin(`collection.${name}`, a)); + return; + case "builtin": + this.loadBuiltin(a, name, dst); + return; + } + void site; + } + + ownerOf(member: Node): Node | null { + const p = member.parent; + return p !== undefined && ts.isClassLikeDeclaration(p) ? p : null; + } + + loadBuiltin(a: Atom, name: string, dst: number): void { + const path = a.path ?? ""; + if ( + path === "Number" && + (name === "MAX_SAFE_INTEGER" || + name === "MIN_SAFE_INTEGER" || + name === "EPSILON" || + name === "MAX_VALUE" || + name === "MIN_VALUE" || + name === "POSITIVE_INFINITY" || + name === "NEGATIVE_INFINITY" || + name === "NaN") + ) + this.add(dst, this.prim("number")); + else if (path === "Math" && /^[A-Z0-9_]+$/.test(name)) this.add(dst, this.prim("number")); + else if (path === "Symbol") this.add(dst, this.prim("symbol")); + else if (path === "Error.instance" || path === "error") { + if (name === "message" || name === "stack" || name === "name") + this.add(dst, this.prim("string")); + else this.add(dst, this.top); + } else if ( + a.receiver === undefined && + MODELED_GLOBALS.has(path.split(".")[0]!) && + path.split(".").length < 4 + ) + this.add(dst, this.builtin(`${path}.${name}`)); + else if (path === "RegExp.instance") this.add(dst, this.builtin(`regexp.${name}`, a)); + else this.add(dst, this.top); + } + + storeConstraint(recv: number, name: string, value: number, site: Node): void { + this.on(recv, (a) => { + switch (a.kind) { + case "top": + this.noteTopWrite(name, site); + this.sink(value, `stored into ⊤.${name} at ${locLabel(site)}`); + return; + case "instance": { + const member = this.lookupMember(a.node!, name, false); + if (member?.set !== undefined) { + const s = this.functionInfo(member.set, this.ownerOf(member.set), this.receiverCtx(a)); + this.activate(s); + this.add(s.thisNode, a); + if (s.params[0] !== undefined) this.edge(value, s.params[0]); + return; + } + this.edge(value, this.field(a, name)); + return; + } + case "class": + case "function": + case "object": + case "dictionary": + this.edge(value, this.field(a, name)); + return; + case "array": + if (/^\d+$/.test(name)) this.edge(value, this.element(a)); + else if (name !== "length") this.edge(value, this.field(a, name)); + return; + case "prim": + return; + default: + this.sink(value, `stored into ${a.label} at ${locLabel(site)}`); + } + }); + } + + computedLoad(recv: number, dst: number, site: Node): void { + this.on(recv, (a) => { + switch (a.kind) { + case "top": + this.add(dst, this.top); + return; + case "array": + case "collection": + this.edge(this.element(a), dst); + return; + case "prim": + if (a.label === "string") this.add(dst, this.prim("string")); + return; + case "object": + case "dictionary": + case "instance": + case "class": + case "function": { + // every field, now and later + const seen = new Set(); + const copy = (): void => { + for (const [name, id] of this.fields.get(a) ?? new Map()) { + if (seen.has(name)) continue; + seen.add(name); + this.edge(id, dst); + } + }; + copy(); + this.lateCopies.push(copy); + if (a.kind === "instance" || a.kind === "class") this.add(dst, this.top); + return; + } + default: + this.add(dst, this.top); + } + void site; + }); + } + + computedStore(recv: number, value: number, site: Node): void { + this.on(recv, (a) => { + switch (a.kind) { + case "top": + this.noteTopWrite(null, site); + this.sink(value, `stored into ⊤[k] at ${locLabel(site)}`); + return; + case "array": + // An index, or a named property under an unknown key. + this.edge(value, this.element(a)); + this.edge(value, this.anyField(a)); + return; + case "prim": + return; + case "object": + case "dictionary": + case "instance": + case "class": + case "function": + this.edge(value, this.anyField(a)); + return; + default: + // As a store under a name: a builtin, promise or collection keeps it where it isn't tracked. + this.sink(value, `stored into ${a.label}[k] at ${locLabel(site)}`); + } + }); + } + + // ---- calls --------------------------------------------------------------------------- + + /** tag`…${x}…`: a call of the tag with the template's strings array (an + * array of strings with a `raw` array of strings) and each substitution. */ + taggedTemplate(e: ts.TaggedTemplateExpression): number { + this.noteSite(e); + const tag = unparen(e.tag); + const recv = + ts.isPropertyAccessExpression(tag) && tag.expression.kind !== ts.SyntaxKind.SuperKeyword + ? this.expr(tag.expression) + : null; + const callee = recv === null ? this.expr(tag) : null; + const strings = this.syntheticArray(e, "template"); + const raw = this.syntheticArray(e.template, "raw"); + this.add(this.element(strings), this.prim("string")); + this.add(this.element(raw), this.prim("string")); + this.add(this.field(strings, "raw"), raw); + const args = [this.constNode(strings)]; + if (ts.isTemplateExpression(e.template)) + for (const span of list(e.template.templateSpans)) args.push(this.expr(span.expression)); + const dst = this.newNode(); + if (recv !== null && ts.isPropertyAccessExpression(tag)) { + const name = tag.name.text; + this.ops.push({ kind: "read", recv, name, node: tag }); + this.on(recv, (a) => { + const fn = this.newNode(); + this.loadAtom(a, name, fn, tag); + this.invoke(fn, this.constNode(a), args, false, dst, e); + }); + } else this.invoke(callee!, null, args, false, dst, e); + return dst; + } + + args(e: ts.CallExpression | ts.NewExpression): { args: number[]; spread: boolean } { + const out: number[] = []; + let spread = false; + for (const a of list(e.arguments)) { + if (ts.isSpreadElement(a)) { + const inner = unparen(a.expression); + if (ts.isIdentifier(inner) && inner.text === "arguments") { + // `f(...arguments)`: forward the enclosing function's parameters. + const fn = this.enclosingNonArrow(); + if (fn !== null && out.length === 0) { + if (fn.argPos === null) fn.argPos = [0, 1, 2, 3, 4, 5].map(() => this.newNode()); + out.push(...fn.argPos); + continue; + } + } + spread = true; + out.push(this.expr(a.expression)); + continue; + } + out.push(this.expr(a)); + } + return { args: out, spread }; + } + + call(e: ts.CallExpression): number { + this.noteSite(e); + const callee = unparen(e.expression); + const dst = this.newNode(); + if (callee.kind === ts.SyntaxKind.SuperKeyword) { + const { args, spread } = this.args(e); + const fn = this.enclosingNonArrow(); + const owner = fn?.ownerClass ?? null; + const base = owner !== null ? this.baseClass(owner) : undefined; + if (fn !== null && base) { + this.on(fn.thisNode, (inst) => { + if (inst.kind === "instance") this.callConstructorChain(base, inst, args, spread, e); + }); + } else if (base === undefined) + for (const a of args) this.sink(a, `super(...) at ${locLabel(e)}`); + return dst; + } + // CommonJS `require("")` of an analyzed module: its exports (the + // module's namespace, which holds `module.exports` as its `exports` + // field and the checker's view of its exports). + if (ts.isIdentifier(callee) && callee.text === "require" && e.arguments.nodes.length === 1) { + const spec = e.arguments.nodes[0]!; + if (ts.isStringLiteral(spec)) { + let target: Node | undefined; + try { + const sym = this.checker.getSymbolAtLocation(spec); + target = sym?.valueDeclaration ?? sym?.declarations[0]; + } catch { + target = undefined; + } + if (target !== undefined && ts.isSourceFile(target) && this.files.includes(target)) { + const ns = this.namespaceNode(target); + this.edge(ns, dst); + this.on(ns, (a) => this.edge(this.field(a, "exports"), dst)); + return dst; + } + } + } + if (ts.isPropertyAccessExpression(callee) || ts.isElementAccessExpression(callee)) { + const recvExpr = unparen(callee.expression); + const { args, spread } = this.args(e); + if (recvExpr.kind === ts.SyntaxKind.SuperKeyword && ts.isPropertyAccessExpression(callee)) { + // super.m(...): the base class member with the current receiver. + const fn = this.enclosingNonArrow(); + const owner = fn?.ownerClass ?? null; + const base = owner !== null ? this.baseClass(owner) : undefined; + if (fn !== null && base) { + const member = this.lookupMember(base, callee.name.text, isStatic(fn.node)); + if (member?.method !== undefined) { + this.callFunction( + this.functionInfo(member.method, this.ownerOf(member.method), fn.ctx), + fn.thisNode, + args, + spread, + dst, + e, + ); + } + } + return dst; + } + const recv = this.expr(recvExpr); + let name: string | null = null; + if (ts.isPropertyAccessExpression(callee)) name = callee.name.text; + else { + const k = unparen(callee.argumentExpression); + if (ts.isStringLiteral(k) || ts.isNoSubstitutionTemplateLiteral(k)) name = k.text; + else this.expr(callee.argumentExpression); + } + if (name !== null) this.ops.push({ kind: "read", recv, name, node: callee }); + // Per receiver atom: the member it resolves to runs with that atom as `this`. + const member = name; + this.on(recv, (a) => { + const fnNode = this.newNode(); + const self = this.constNode(a); + if (member !== null) this.loadAtom(a, member, fnNode, callee); + else this.computedLoad(self, fnNode, callee); + this.invoke(fnNode, self, args, spread, dst, e); + }); + return dst; + } + const calleeNode = this.expr(callee); + const { args, spread } = this.args(e); + this.invoke(calleeNode, null, args, spread, dst, e); + return dst; + } + + invoke( + callee: number, + recv: number | null, + args: number[], + spread: boolean, + dst: number, + site: ts.CallExpression | ts.TaggedTemplateExpression, + ): void { + const undefNode = recv === null ? this.constNode(this.prim("undefined")) : recv; + this.on(callee, (a) => { + switch (a.kind) { + case "function": + this.callFunction(this.infoOfAtom(a)!, undefNode, args, spread, dst, site); + return; + case "builtin": + this.callBuiltin(a, args, spread, dst, site, recv); + return; + case "class": + return; // calling a class throws + case "prim": + return; + default: + for (const x of args) this.sink(x, `argument of ⊤ call at ${locLabel(site)}`); + if (recv !== null && a.kind === "top") + this.sink(recv, `receiver of ⊤ call at ${locLabel(site)}`); + this.add(dst, this.top); + } + }); + } + + newExpr(e: ts.NewExpression): number { + this.noteSite(e); + const dst = this.newNode(); + const callee = this.expr(e.expression); + const { args, spread } = this.args(e); + this.on(callee, (a) => { + switch (a.kind) { + case "class": + this.construct(a.node!, args, spread, dst, e); + return; + case "function": { + // An ES5 constructor function: one instance atom per function. + const inst = this.atoms.intern(`fninst:${this.nodeKey(a.node!)}`, () => ({ + kind: "object", + label: `new ${a.label}`, + node: a.node, + })); + this.fnOfInstance.set(inst, a); + this.add(dst, inst); + const info = this.infoOfAtom(a); + if (info !== undefined) { + this.activate(info); + this.add(info.thisNode, inst); + this.bindArgs(info, args, spread); + // `new` ignores a primitive result (and the undefined of a + // body that runs off its end) and yields the instance. + this.on(info.ret, (r) => { + if (r.kind !== "prim") this.add(dst, r); + }); + this.edgeFor(e, info); + } + return; + } + case "builtin": + this.constructBuiltin(a.path ?? "", args, dst, e); + return; + default: + for (const x of args) this.sink(x, `argument of ⊤ constructor at ${locLabel(e)}`); + this.add(dst, this.top); + } + }); + return dst; + } + + constructBuiltin(path: string, args: number[], dst: number, site: Node): void { + switch (path) { + case "Map": + case "Set": + case "WeakMap": + case "WeakSet": { + const c = this.atoms.intern(`coll:${this.nodeKey(site)}`, () => ({ + kind: "collection", + label: `${path}@${locLabel(site)}`, + node: site, + })); + this.add(dst, c); + if (args[0] !== undefined) this.iterateInto(args[0], this.element(c), site); + return; + } + case "Error": + case "TypeError": + case "RangeError": + case "SyntaxError": + this.add(dst, this.builtin("Error.instance")); + return; + case "Promise": { + const p = this.promiseAtom(site, locLabel(site)); + this.add(this.field(p, "value"), this.top); + this.add(dst, p); + if (args[0] !== undefined) { + const resolver = this.constNode(this.builtin("promise.resolver")); + this.on(args[0], (f) => { + if (f.kind === "function") + this.callFunction( + this.infoOfAtom(f)!, + null, + [resolver, resolver], + false, + this.newNode(), + site, + ); + }); + } + return; + } + case "Date": + this.add(dst, this.builtin("Date.instance")); + return; + case "RegExp": + this.add(dst, this.builtin("RegExp.instance")); + return; + case "Array": { + this.add(dst, this.syntheticArray(site, "Array")); + return; + } + case "Proxy": + // The handler's traps run on every operation with the target. + if (args[0] !== undefined) + this.ops.push({ kind: "proxy", recv: args[0], name: null, node: site }); + for (const x of args) this.sink(x, `argument of new Proxy at ${locLabel(site)}`); + this.add(dst, this.top); + return; + default: + for (const x of args) this.sink(x, `argument of new ${path} at ${locLabel(site)}`); + this.add(dst, this.top); + } + } + + /** Builtin call summaries; anything unmodeled makes its arguments escape. */ + callBuiltin( + a: Atom, + args: number[], + spread: boolean, + dst: number, + site: ts.CallExpression | ts.TaggedTemplateExpression, + recvNode: number | null, + ): void { + const path = a.path ?? ""; + const recv = a.receiver; + const arg = (i: number): number => args[i] ?? this.constNode(this.prim("undefined")); + const prim = (p: string): void => this.add(dst, this.prim(p)); + const callback = (i: number, cbArgs: number[], ret: number | null): void => { + if (args[i] === undefined) return; + this.on(args[i]!, (f) => { + if (f.kind === "function") { + const saved = this.forceLoop; + this.forceLoop = LOOP_CALLBACK_METHODS.has(path.slice(path.indexOf(".") + 1)); + this.callFunction(this.infoOfAtom(f)!, null, cbArgs, false, ret ?? this.newNode(), site); + this.forceLoop = saved; + } else if (f.kind === "top" || f.kind === "builtin") { + for (const x of cbArgs) this.sink(x, `argument of ⊤ callback at ${locLabel(site)}`); + if (ret !== null) this.add(ret, this.top); + } + }); + }; + const escapeAll = (): void => { + for (const x of args) this.sink(x, `argument of ${path} at ${locLabel(site)}`); + this.add(dst, this.top); + }; + // Methods of builtin receivers. + if (recv !== undefined) { + const method = path.slice(path.indexOf(".") + 1); + if (path.startsWith("array.")) { + const el = this.element(recv); + const num = this.constNode(this.prim("number")); + const recvN = this.constNode(recv); + switch (method) { + case "push": + case "unshift": + for (const x of args) { + if (spread) this.iterateInto(x, el, site); + else this.edge(x, el); + } + prim("number"); + return; + case "pop": + case "shift": + case "at": + this.edge(el, dst); + this.add(dst, this.prim("undefined")); + return; + case "find": + case "findLast": + callback(0, [el, num, recvN], null); + this.edge(el, dst); + this.add(dst, this.prim("undefined")); + return; + case "forEach": + callback(0, [el, num, recvN], null); + prim("undefined"); + return; + case "some": + case "every": + case "includes": + callback(0, [el, num, recvN], null); + prim("boolean"); + return; + case "findIndex": + case "findLastIndex": + case "indexOf": + case "lastIndexOf": + callback(0, [el, num, recvN], null); + prim("number"); + return; + case "join": + case "toString": + prim("string"); + return; + case "map": + case "flatMap": { + const out = this.syntheticArray(site, method); + const outEl = this.element(out); + if (method === "map") callback(0, [el, num, recvN], outEl); + else { + const r = this.newNode(); + callback(0, [el, num, recvN], r); + this.iterateInto(r, outEl, site); + } + this.add(dst, out); + return; + } + case "filter": + case "slice": + case "concat": + case "reverse": + case "sort": + case "splice": + case "toSorted": + case "toReversed": + case "flat": { + if (method === "filter") callback(0, [el, num, recvN], null); + if (method === "sort" || method === "toSorted") callback(0, [el, el], null); + if (method === "reverse" || method === "sort") { + this.add(dst, recv); + return; + } + const out = this.syntheticArray(site, method); + this.edge(el, this.element(out)); + if (method === "concat") + for (const x of args) this.iterateInto(x, this.element(out), site); + if (method === "splice") for (const x of args.slice(2)) this.edge(x, el); + this.add(dst, out); + return; + } + case "reduce": + case "reduceRight": { + const acc = this.newNode(); + if (args[1] !== undefined) this.edge(args[1], acc); + else this.edge(el, acc); + callback(0, [acc, el, num, recvN], acc); + this.edge(acc, dst); + return; + } + case "keys": + case "values": + case "entries": { + const out = this.syntheticArray(site, method); + if (method === "values") this.edge(el, this.element(out)); + else if (method === "keys") this.add(this.element(out), this.prim("number")); + else { + const pair = this.syntheticArray(site, "entry"); + this.add(this.element(pair), this.prim("number")); + this.edge(el, this.element(pair)); + this.add(this.element(out), pair); + } + this.add(dst, out); + return; + } + case "fill": + for (const x of args) this.edge(x, el); + this.add(dst, recv); + return; + } + escapeAll(); + this.escape(recv, `array.${method} at ${locLabel(site)}`); + return; + } + if (path.startsWith("string.") || path.startsWith("number.") || path.startsWith("boolean.")) { + const r = STRING_METHOD_RESULTS.get(method); + if (r === "array") { + const out = this.syntheticArray(site, method); + this.add(this.element(out), this.prim("string")); + this.add(this.element(out), this.prim("undefined")); + this.add(dst, out); + this.add(dst, this.prim("null")); + } else if (r !== undefined) prim(r); + else if (method === "replace" || method === "replaceAll") prim("string"); + else escapeAll(); + if (method === "replace" || method === "replaceAll") + callback(1, [this.constNode(this.prim("string"))], null); + return; + } + if (path.startsWith("regexp.")) { + if (method === "test") prim("boolean"); + else if (method === "exec") { + const out = this.syntheticArray(site, "exec"); + this.add(this.element(out), this.prim("string")); + this.add(dst, out); + prim("null"); + } else escapeAll(); + return; + } + if (path.startsWith("promise.")) { + const out = this.promiseAtom(site, locLabel(site)); + const value = this.field(recv, "value"); + const ret = this.newNode(); + if (method === "then") { + callback(0, [value], ret); + callback(1, [this.constNode(this.top)], ret); + } else if (method === "catch") { + callback(0, [this.constNode(this.top)], ret); + this.edge(value, ret); + } else { + callback(0, [], null); + this.edge(value, ret); + } + this.awaitInto(ret, this.field(out, "value")); + this.add(dst, out); + return; + } + if (path.startsWith("collection.")) { + const el = this.element(recv); + switch (method) { + case "add": + case "set": + for (const x of args) this.edge(x, el); + this.add(dst, recv); + return; + case "get": + this.edge(el, dst); + prim("undefined"); + return; + case "has": + case "delete": + prim("boolean"); + return; + case "clear": + prim("undefined"); + return; + case "forEach": + callback(0, [el, el], null); + prim("undefined"); + return; + case "keys": + case "values": + case "entries": { + const out = this.syntheticArray(site, method); + this.edge(el, this.element(out)); + if (method === "entries") { + const pair = this.syntheticArray(site, "entry"); + this.edge(el, this.element(pair)); + this.nodes[this.element(out)]!.atoms.clear(); + this.add(this.element(out), pair); + } + this.add(dst, out); + return; + } + } + escapeAll(); + return; + } + if (path.startsWith("function.")) { + if (recv.kind !== "function" || recv.node === undefined) { + escapeAll(); + return; + } + if (method === "bind") { + const info = this.infoOfAtom(recv); + if (info !== undefined && args[0] !== undefined && !info.isArrow) + this.edge(args[0], info.thisNode); + if (info !== undefined && args.length > 1) { + this.activate(info); + for (const p of info.params) this.add(p, this.top); + for (const x of args.slice(1)) this.sink(x, `bound argument at ${locLabel(site)}`); + } + this.add(dst, recv); + return; + } + if (method === "call") { + this.callFunction( + this.infoOfAtom(recv)!, + args[0] ?? this.constNode(this.prim("undefined")), + args.slice(1), + spread, + dst, + site, + ); + return; + } + // apply: arguments from an array + const info = this.infoOfAtom(recv); + if (info !== undefined) { + this.activate(info); + if (args[0] !== undefined && !info.isArrow) this.edge(args[0], info.thisNode); + for (const p of info.params) + if (args[1] !== undefined) this.iterateInto(args[1], p, site); + this.edge(info.ret, dst); + this.edgeFor(site, info); + } + return; + } + if (path.startsWith("object.")) { + if (method === "hasOwnProperty") prim("boolean"); + else if (method === "toString") prim("string"); + else escapeAll(); + return; + } + escapeAll(); + return; + } + // `Object.prototype.m.call(o, …)`-style calls of builtin prototype methods. + if (/^(Object|Array|String|Function)\.prototype\.\w+\.(call|apply)$/.test(path)) { + const m = path.split(".")[2]!; + if (m === "hasOwnProperty" || m === "propertyIsEnumerable" || m === "isPrototypeOf") + prim("boolean"); + else if (m === "toString") prim("string"); + else escapeAll(); + return; + } + switch (path) { + case "Object.keys": + case "Object.getOwnPropertyNames": { + const out = this.syntheticArray(site, "keys"); + this.add(this.element(out), this.prim("string")); + this.add(dst, out); + return; + } + case "Object.values": + case "Object.entries": { + const out = this.syntheticArray(site, path === "Object.values" ? "values" : "entries"); + const vals = this.newNode(); + if (args[0] !== undefined) this.computedLoad(args[0], vals, site); + if (path === "Object.values") this.edge(vals, this.element(out)); + else { + const pair = this.syntheticArray(site, "entry"); + this.add(this.element(pair), this.prim("string")); + this.edge(vals, this.element(pair)); + this.add(this.element(out), pair); + } + this.add(dst, out); + return; + } + case "Object.freeze": + case "Object.seal": + case "Object.preventExtensions": + this.ops.push({ + kind: path === "Object.freeze" ? "freeze" : "seal", + recv: arg(0), + name: null, + node: site, + }); + this.edge(arg(0), dst); + return; + case "Object.isFrozen": + case "Object.isSealed": + case "Object.isExtensible": + case "Array.isArray": + case "Number.isInteger": + case "Number.isNaN": + case "Number.isFinite": + case "Number.isSafeInteger": + case "isNaN": + case "isFinite": + prim("boolean"); + return; + case "Object.assign": { + this.ops.push({ kind: "assign", recv: arg(0), name: null, node: site }); + this.on(arg(0), (t) => { + for (const s of args.slice(1)) this.copyFields(s, t, site); + if (t.kind === "top") + for (const s of args.slice(1)) + this.sink(s, `Object.assign into ⊤ at ${locLabel(site)}`); + }); + this.edge(arg(0), dst); + return; + } + case "Object.defineProperty": + case "Object.defineProperties": + case "Reflect.defineProperty": + this.ops.push({ kind: "defineProperty", recv: arg(0), name: null, node: site }); + this.sink(arg(2), `property descriptor at ${locLabel(site)}`); + this.sink(arg(1), `property descriptor at ${locLabel(site)}`); + this.edge(arg(0), dst); + return; + case "Object.setPrototypeOf": + case "Reflect.setPrototypeOf": + this.ops.push({ kind: "setPrototypeOf", recv: arg(0), name: null, node: site }); + this.sink(arg(1), `prototype at ${locLabel(site)}`); + this.edge(arg(0), dst); + return; + case "Object.create": { + const d = this.atoms.intern(`dict:${this.nodeKey(site)}`, () => ({ + kind: "dictionary", + label: `Object.create@${locLabel(site)}`, + node: site, + })); + this.add(dst, d); + return; + } + case "Object.fromEntries": { + const d = this.atoms.intern(`dict:${this.nodeKey(site)}`, () => ({ + kind: "dictionary", + label: `fromEntries@${locLabel(site)}`, + node: site, + })); + const pairs = this.newNode(); + if (args[0] !== undefined) this.iterateInto(args[0], pairs, site); + const vals = this.newNode(); + this.iterateInto(pairs, vals, site); + this.edge(vals, this.anyField(d)); + this.add(dst, d); + return; + } + case "Object.getPrototypeOf": + case "Reflect.getPrototypeOf": + this.add(dst, this.top); + return; + case "Object.is": + prim("boolean"); + return; + case "Array.from": + case "Array.of": { + const out = this.syntheticArray(site, path); + if (path === "Array.of") for (const x of args) this.edge(x, this.element(out)); + else if (args[1] !== undefined) { + const els = this.newNode(); + if (args[0] !== undefined) this.iterateInto(args[0], els, site); + callback(1, [els, this.constNode(this.prim("number"))], this.element(out)); + } else if (args[0] !== undefined) this.iterateInto(args[0], this.element(out), site); + this.add(dst, out); + return; + } + case "JSON.parse": { + // A reviver runs on every parsed value with its key, the holder as `this`, innermost first: + // what it returns replaces the value, in its holder (which later calls receive) and in the + // result. + const revived = this.newNode(); + this.add(revived, this.top); + this.repeatingSites.add(site); + if (args[1] !== undefined) + this.on(args[1], (f) => { + if (f.kind === "function") + this.callFunction( + this.infoOfAtom(f)!, + revived, + [this.constNode(this.prim("string")), revived], + false, + revived, + site, + ); + else if (f.kind === "top") + this.sink(revived, `JSON.parse reviver at ${locLabel(site)}`); + }); + this.edge(revived, dst); + return; + } + case "JSON.stringify": { + // Every value it serializes: toJSON methods run on them, and a + // replacer function with each key and value, the holder as `this`. + // Their results are serialized in turn. + const serialized = this.newNode(); + this.childrenInto(serialized); + this.repeatingSites.add(site); + this.edge(arg(0), serialized); + const key = this.constNode(this.prim("string")); + this.on(serialized, (v) => this.callToJSON(v, key, serialized, site)); + if (args[1] !== undefined) + this.on(args[1], (f) => { + if (f.kind === "function") + this.callFunction( + this.infoOfAtom(f)!, + serialized, + [key, serialized], + false, + serialized, + site, + ); + else if (f.kind === "top") + this.sink(serialized, `JSON.stringify replacer at ${locLabel(site)}`); + }); + prim("string"); + prim("undefined"); + return; + } + case "Reflect.deleteProperty": { + const key = ts.isCallExpression(site) ? unparen(list(site.arguments)[1]!) : undefined; + const name = + key !== undefined && (ts.isStringLiteral(key) || ts.isNoSubstitutionTemplateLiteral(key)) + ? key.text + : null; + this.ops.push({ kind: "delete", recv: arg(0), name, node: site }); + prim("boolean"); + return; + } + case "Proxy.revocable": + this.ops.push({ kind: "proxy", recv: arg(0), name: null, node: site }); + escapeAll(); + return; + case "Reflect.construct": { + // Reflect.construct(Proxy, [target, handler]) is `new Proxy(target, handler)`: the arguments + // are the list's elements. + const listed = this.newNode(); + if (args[1] !== undefined) this.iterateInto(args[1], listed, site); + this.on(arg(0), (c) => { + if (c.kind === "builtin" && c.path === "Proxy") + this.ops.push({ kind: "proxy", recv: listed, name: null, node: site }); + }); + this.sink(listed, `argument of ${path} at ${locLabel(site)}`); + escapeAll(); + return; + } + case "String": + case "encodeURIComponent": + case "decodeURIComponent": + case "encodeURI": + case "decodeURI": + prim("string"); + return; + case "Number": + case "parseInt": + case "parseFloat": + case "Number.parseInt": + case "Number.parseFloat": + prim("number"); + return; + case "Boolean": + prim("boolean"); + return; + case "Symbol": + case "Symbol.for": + prim("symbol"); + return; + case "BigInt": + prim("bigint"); + return; + case "Promise.resolve": { + const p = this.promiseAtom(site, locLabel(site)); + if (args[0] !== undefined) this.awaitInto(args[0], this.field(p, "value")); + else this.add(this.field(p, "value"), this.prim("undefined")); + this.add(dst, p); + return; + } + case "Promise.reject": { + const p = this.promiseAtom(site, locLabel(site)); + this.add(dst, p); + if (args[0] !== undefined) this.sink(args[0], `rejection at ${locLabel(site)}`); + return; + } + case "Promise.all": + case "Promise.allSettled": + case "Promise.race": + case "Promise.any": { + const p = this.promiseAtom(site, locLabel(site)); + const items = this.newNode(); + if (args[0] !== undefined) this.iterateInto(args[0], items, site); + if (path === "Promise.all") { + const out = this.syntheticArray(site, "all"); + this.awaitInto(items, this.element(out)); + this.add(this.field(p, "value"), out); + } else if (path === "Promise.race" || path === "Promise.any") + this.awaitInto(items, this.field(p, "value")); + else this.add(this.field(p, "value"), this.top); + this.add(dst, p); + return; + } + case "promise.resolver": + for (const x of args) this.sink(x, `promise resolution at ${locLabel(site)}`); + return; + } + if (path.startsWith("console.")) { + prim("undefined"); + return; + } + if (path.startsWith("Math.")) { + prim("number"); + return; + } + if (path === "Date.now") { + prim("number"); + return; + } + if (path.startsWith("Date.instance") || path === "Error.instance") { + escapeAll(); + return; + } + void recvNode; + escapeAll(); + } + + /** Re-run deferred field copies until nothing new flows. */ + finish(): void { + for (;;) { + const before = this.steps; + for (const c of this.lateCopies) c(); + this.solve(); + // A condition with no values at the fixpoint is one the analysis + // could not evaluate: walk both of its branches. + let walked = false; + for (const p of this.pendingIfs) { + if (this.nodes[p.cond]!.atoms.size > 0) continue; + if (!p.thenDone) { + this.walkBranch(p, "then"); + walked = true; + } + if (!p.elseDone) { + this.walkBranch(p, "else"); + walked = true; + } + } + if (walked) continue; + if (this.steps === before || this.budgetExceeded) return; + } + } +} + +/** The export name of a CommonJS export declaration the checker resolves + * bindings to: a member of the `module.exports` literal, or the target of + * `exports.name =` / `module.exports.name =`. */ +function cjsExportName(decl: Node): string | null { + if (ts.isShorthandPropertyAssignment(decl) || ts.isPropertyAssignment(decl)) { + const n = decl.name; + return ts.isIdentifier(n) || ts.isStringLiteral(n) ? n.text : null; + } + if (ts.isPropertyAccessExpression(decl) && ts.isIdentifier(decl.name)) return decl.name.text; + if ( + ts.isBinaryExpression(decl) && + ts.isPropertyAccessExpression(decl.left) && + ts.isIdentifier(decl.left.name) + ) + return decl.left.name.text; + return null; +} + +interface WalkEnv { + readonly ctx: string; + readonly ctxRoot: Node | null; + readonly currentFn: FunctionInfo | null; + readonly loopDepth: number; +} + +interface PendingIf { + readonly s: ts.IfStatement; + readonly cond: number; + readonly env: WalkEnv; + thenDone: boolean; + elseDone: boolean; +} + +/** Whether a value of the atom may be truthy (only `null`, `undefined` and + * the literal `false` never are; ⊤ may be anything). */ +function mayBeTruthy(a: Atom): boolean { + if (a.kind !== "prim") return true; + if (a.label === "null" || a.label === "undefined") return false; + return a.literal !== false; +} + +/** Whether a value of the atom may be falsy (objects, arrays, functions, + * classes, promises and collections never are). */ +function mayBeFalsy(a: Atom): boolean { + if (a.kind === "top") return true; + if (a.kind !== "prim") return false; + return a.literal !== true; +} diff --git a/packages/compiler/src/frontend/shape-inference/constant-graphs.ts b/packages/compiler/src/frontend/shape-inference/constant-graphs.ts new file mode 100644 index 000000000..dffa8d17d --- /dev/null +++ b/packages/compiler/src/frontend/shape-inference/constant-graphs.ts @@ -0,0 +1,421 @@ +/* Static-object specialization, stage 1 (DESIGN-constant-objects.md): + * constant object graphs. + * + * An object literal is a SINGLETON when the code that creates it runs at + * most once: at module top level, or in a function every call path to which + * starts at module evaluation through exactly one call site outside loops + * (IIFEs, revealing modules, init helpers). A singleton's field is CONSTANT + * when every store to it runs in such once-only code, no reflective or + * computed write reaches the object, and the object never escapes to code + * the analysis cannot see. A constant object has only constant fields and a + * fixed key set; constant objects whose constant fields point at each other + * form a constant graph (`ns`, `ns.util`, `ns.util.trim`). + * + * This stage computes the graphs and reports them, with the rule that + * demoted every near miss, and the reads and calls a later stage could fold. + * Nothing reaches code generation. */ +import * as ts from "../ts7/adapter.js"; +import type { Atom } from "./domain.js"; +import type { FunctionInfo, Op, ShapeAnalysis } from "./analyze.js"; +import { npmStaticPackageOfPath } from "../npm-static.js"; + +type Node = ts.Node; + +export interface ConstantField { + readonly key: string; + /** Distinct atoms the field may hold. */ + readonly values: readonly Atom[]; + /** Stores (the literal's own member included). */ + readonly stores: number; +} + +/** An object the analysis tracks as one allocation: an object literal, a + * function object (a namespace or an ES5 constructor), or a constructor's + * default prototype object. */ +export interface Candidate { + readonly atom: Atom; + /** The syntax that creates it (the literal or the function). */ + readonly node: Node; + readonly kind: "literal" | "function" | "prototype"; + /** Keys the creating syntax itself defines. */ + readonly keys: readonly string[]; + /** Whether the creating syntax is a plain data literal. */ + readonly plain: boolean; + readonly inPackage: boolean; +} + +export interface ConstantObject { + readonly site: Candidate; + readonly fields: readonly ConstantField[]; + readonly frozen: boolean; +} + +export interface NearMiss { + readonly site: Candidate; + /** Null when the whole object was demoted. */ + readonly key: string | null; + readonly rule: + | "init-factory" + | "allocated-repeatedly" + | "escapes" + | "reflective-write" + | "computed-write" + | "literal-form" + | "written-after-init" + | "lazy-cache"; + readonly detail: string; +} + +export interface ConstantGraphs { + readonly objects: readonly ConstantObject[]; + readonly nearMisses: readonly NearMiss[]; + /** Reads `e.k` whose receiver is exactly one constant object and whose + * field is constant, and the subset that are callees. */ + readonly foldableReads: number; + readonly foldableCalls: number; + /** Reads of a constant field through a receiver that may also be other + * values. */ + readonly mixedReads: number; + readonly once: (fn: Node) => boolean; +} + +function isLoop(n: Node): boolean { + return ( + ts.isForStatement(n) || + ts.isForInStatement(n) || + ts.isForOfStatement(n) || + ts.isWhileStatement(n) || + ts.isDoStatement(n) + ); +} + +/** The function (or source file) whose body contains `n`. */ +function ownerFunction(n: Node): Node { + for (let p = n.parent; p !== undefined; p = p.parent) { + if (ts.isSourceFile(p)) return p; + if (ts.isFunctionLike(p) && (p as { body?: Node }).body !== undefined) return p; + } + return n.getSourceFile(); +} + +/** Whether `n` sits in a loop of its own function's body. */ +function inLoop(n: Node, owner: Node): boolean { + for (let p = n.parent; p !== undefined && p !== owner; p = p.parent) if (isLoop(p)) return true; + return false; +} + +function loc(n: Node): string { + const sf = n.getSourceFile(); + const file = sf.fileName.split("/node_modules/").pop() ?? sf.fileName; + return `${file.split("/").slice(-2).join("/")}:${sf.getLineAndCharacterOfPosition(ts.tokenStartOfNode(n)).line + 1}`; +} + +/** `x.k || (x.k = v)`, `x.k ??= v`, `x.k ||= v`, `if (!x.k) x.k = v`. */ +function isCacheStore(write: Node): boolean { + let assign: Node | undefined = write.parent; + while (assign !== undefined && ts.isParenthesizedExpression(assign)) assign = assign.parent; + if (assign === undefined || !ts.isBinaryExpression(assign)) return false; + const op = assign.operatorToken.kind; + if (op === ts.SyntaxKind.BarBarEqualsToken || op === ts.SyntaxKind.QuestionQuestionEqualsToken) + return true; + if (op !== ts.SyntaxKind.EqualsToken) return false; + const text = write.getText(); + let up: Node | undefined = assign.parent; + while (up !== undefined && ts.isParenthesizedExpression(up)) up = up.parent; + if ( + up !== undefined && + ts.isBinaryExpression(up) && + (up.operatorToken.kind === ts.SyntaxKind.BarBarToken || + up.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken) && + up.left.getText() === text + ) + return true; + for (let p: Node | undefined = assign.parent; p !== undefined; p = p.parent) { + if (ts.isIfStatement(p)) { + const cond = p.expression.getText().replace(/\s+/g, ""); + return cond === `!${text}` || cond === `${text}===undefined` || cond === `${text}==null`; + } + if (ts.isFunctionLike(p) || ts.isSourceFile(p)) break; + } + return false; +} + +const OBJECT_DEMOTING: ReadonlySet = new Set([ + "computedWrite", + "delete", + "defineProperty", + "setPrototypeOf", + "assign", + "proxy", +]); + +export function computeConstantGraphs(analysis: ShapeAnalysis): ConstantGraphs { + const infosByNode = new Map(); + for (const info of analysis.functions.values()) { + let l = infosByNode.get(info.node); + if (l === undefined) infosByNode.set(info.node, (l = [])); + l.push(info); + } + const edgesTo = new Map(); + for (const e of analysis.calls) { + let l = edgesTo.get(e.callee.node); + if (l === undefined) edgesTo.set(e.callee.node, (l = [])); + l.push(e); + } + const escapedNodes = new Set([...analysis.escapedFunctions].map((f) => f.node)); + + const onceMemo = new Map(); + const once = (fn: Node): boolean => { + if (ts.isSourceFile(fn)) return true; + const known = onceMemo.get(fn); + if (known !== undefined) return known; + onceMemo.set(fn, false); // a cycle is not once-only + let result = false; + const edges = edgesTo.get(fn) ?? []; + if ( + infosByNode.has(fn) && + !escapedNodes.has(fn) && + edges.length > 0 && + new Set(edges.map((e) => e.site)).size === 1 + ) { + result = edges.every( + (e) => + !e.inLoop && e.caller !== null && !inLoop(e.site, e.caller.node) && once(e.caller.node), + ); + } + onceMemo.set(fn, result); + return result; + }; + + // Whether every call of a function comes from once-only code outside + // loops, through any number of call sites: an init factory, whose + // allocations are one object per call site (schema builders called at + // module initialization). + const initOnlyMemo = new Map(); + const initOnly = (fn: Node): boolean => { + if (ts.isSourceFile(fn)) return true; + const known = initOnlyMemo.get(fn); + if (known !== undefined) return known; + initOnlyMemo.set(fn, false); + const edges = edgesTo.get(fn) ?? []; + const result = + infosByNode.has(fn) && + !escapedNodes.has(fn) && + edges.length > 0 && + edges.every( + (e) => + !e.inLoop && + e.caller !== null && + !inLoop(e.site, e.caller.node) && + (once(e.caller.node) || initOnly(e.caller.node)), + ); + initOnlyMemo.set(fn, result); + return result; + }; + + const opsByAtom = new Map(); + for (const op of analysis.ops) { + for (const a of analysis.nodes[op.recv]!.atoms) { + let l = opsByAtom.get(a); + if (l === undefined) opsByAtom.set(a, (l = [])); + l.push(op); + } + } + + const objects: ConstantObject[] = []; + const nearMisses: NearMiss[] = []; + const constantKeys = new Map>(); + const candidates: Candidate[] = []; + for (const site of analysis.sites) { + if (site.kind !== "object") continue; + candidates.push({ + atom: site.atom, + node: site.node, + kind: "literal", + keys: site.keys ?? [], + plain: !( + site.hasAccessor || + site.hasComputedKey || + site.spreads.length > 0 || + site.keys === null + ), + inPackage: site.inPackage, + }); + } + const inPackage = (n: Node): boolean => + npmStaticPackageOfPath(n.getSourceFile().fileName) !== null; + for (const atom of analysis.atoms.atoms) { + if (atom.kind !== "function" || atom.node === undefined) continue; + const hasFields = (analysis.fields.get(atom)?.size ?? 0) > 0; + const proto = analysis.protoOf.get(atom); + if (!hasFields && proto === undefined) continue; + candidates.push({ + atom, + node: atom.node, + kind: "function", + keys: [], + plain: true, + inPackage: inPackage(atom.node), + }); + if (proto !== undefined) + candidates.push({ + atom: proto, + node: atom.node, + kind: "prototype", + keys: ["constructor"], + plain: true, + inPackage: inPackage(atom.node), + }); + } + for (const site of candidates) { + // A function object is created where its expression or declaration + // runs; its prototype with it. + const owner = ownerFunction(site.node); + if (!once(owner) || inLoop(site.node, owner)) { + const factory = !inLoop(site.node, owner) && initOnly(owner); + if (site.inPackage || factory) + nearMisses.push({ + site, + key: null, + rule: factory ? "init-factory" : "allocated-repeatedly", + detail: factory + ? `one object per call of ${loc(owner)} (${new Set((edgesTo.get(owner) ?? []).map((e) => e.site)).size} call sites)` + : `created in ${owner.kind === ts.SyntaxKind.SourceFile ? "a module loop" : loc(owner)}`, + }); + continue; + } + const escaped = analysis.escaped.get(site.atom); + if (escaped !== undefined) { + nearMisses.push({ site, key: null, rule: "escapes", detail: escaped }); + continue; + } + if (!site.plain) { + nearMisses.push({ + site, + key: null, + rule: "literal-form", + detail: "accessor, computed key or spread in the literal", + }); + continue; + } + const ops = opsByAtom.get(site.atom) ?? []; + const demoting = ops.find((op) => OBJECT_DEMOTING.has(op.kind)); + if (demoting !== undefined) { + nearMisses.push({ + site, + key: null, + rule: demoting.kind === "computedWrite" ? "computed-write" : "reflective-write", + detail: `${demoting.kind} at ${loc(demoting.node)}`, + }); + continue; + } + const frozen = ops.some((op) => op.kind === "freeze"); + const stores = new Map(); + for (const k of site.keys) stores.set(k, 1); + // A function's own data fields come only from stores. + const demotedKeys = new Map(); + for (const op of ops) { + // A `length` store on an object (not an array) is an ordinary write. + if ((op.kind !== "write" && op.kind !== "lengthWrite") || op.name === null) continue; + stores.set(op.name, (stores.get(op.name) ?? 0) + 1); + const fn = ownerFunction(op.node); + if (once(fn) && !inLoop(op.node, fn)) continue; + if (demotedKeys.has(op.name)) continue; + demotedKeys.set(op.name, { + site, + key: op.name, + rule: isCacheStore(op.node) ? "lazy-cache" : "written-after-init", + detail: loc(op.node), + }); + } + for (const miss of demotedKeys.values()) nearMisses.push(miss); + const fieldMap = analysis.fields.get(site.atom) ?? new Map(); + const fields: ConstantField[] = []; + const keys = new Set(); + for (const [key, count] of stores) { + if (demotedKeys.has(key)) continue; + const id = fieldMap.get(key); + const values = id === undefined ? [] : [...analysis.nodes[id]!.atoms]; + fields.push({ key, values, stores: count }); + keys.add(key); + } + constantKeys.set(site.atom, keys); + if (demotedKeys.size === 0) objects.push({ site, fields, frozen }); + } + + let foldableReads = 0, + foldableCalls = 0, + mixedReads = 0; + for (const op of analysis.ops) { + if (op.kind !== "read" || op.name === null) continue; + const atoms = analysis.nodes[op.recv]!.atoms; + const constant = [...atoms].filter((a) => constantKeys.get(a)?.has(op.name!) === true); + if (constant.length === 0) continue; + if (atoms.size === 1) { + foldableReads++; + const p = op.node.parent; + if (p !== undefined && ts.isCallExpression(p) && p.expression === op.node) foldableCalls++; + } else mixedReads++; + } + return { objects, nearMisses, foldableReads, foldableCalls, mixedReads, once }; +} + +/** Report lines (SCRIPTC_DEBUG_SHAPES=const): per package, the constant + * graphs with their roots, the near misses by rule, and the foldable reads. */ +export function constantGraphReport(analysis: ShapeAnalysis, graphs: ConstantGraphs): string[] { + const lines: string[] = []; + const pkgOf = (site: Candidate): string => + npmStaticPackageOfPath(site.node.getSourceFile().fileName) ?? ""; + const byPkg = new Map(); + const entry = (pkg: string) => { + let e = byPkg.get(pkg); + if (e === undefined) byPkg.set(pkg, (e = { objects: [], misses: [] })); + return e; + }; + for (const o of graphs.objects) entry(pkgOf(o.site)).objects.push(o); + for (const m of graphs.nearMisses) entry(pkgOf(m.site)).misses.push(m); + const constantAtoms = new Set(graphs.objects.map((o) => o.site.atom)); + const referenced = new Set(); + for (const o of graphs.objects) + for (const f of o.fields) + if (f.values.length === 1 && constantAtoms.has(f.values[0]!)) referenced.add(f.values[0]!); + const describe = (o: ConstantObject, depth: number, seen: Set): string => { + const parts = o.fields.map((f) => { + if (f.values.length !== 1) + return `${f.key}: ${f.values.length === 0 ? "?" : f.values.map((a) => a.label).join("|")}`; + const v = f.values[0]!; + const child = graphs.objects.find((x) => x.site.atom === v); + if (child !== undefined && depth < 3 && !seen.has(v)) { + seen.add(v); + return `${f.key}: ${describe(child, depth + 1, seen)}`; + } + return `${f.key}: ${v.kind === "function" && child === undefined ? "fn" : v.label}`; + }); + const shown = parts.length > 8 ? [...parts.slice(0, 8), `… ${parts.length - 8} more`] : parts; + return `{${shown.join(", ")}}${o.frozen ? " (frozen)" : ""}`; + }; + lines.push( + `analysis ${analysis.steps} steps${analysis.budgetExceeded ? " (BUDGET EXCEEDED: facts incomplete)" : ""}, ${analysis.files.length} files`, + ); + lines.push( + `constant objects ${graphs.objects.length}, near misses ${graphs.nearMisses.length}; foldable reads ${graphs.foldableReads} (calls ${graphs.foldableCalls}), mixed-receiver reads ${graphs.mixedReads}`, + ); + for (const [pkg, e] of [...byPkg].sort((a, b) => a[0].localeCompare(b[0]))) { + const fields = e.objects.reduce((n, o) => n + o.fields.length, 0); + const rules = new Map(); + for (const m of e.misses) rules.set(m.rule, (rules.get(m.rule) ?? 0) + 1); + lines.push( + `${pkg}: ${e.objects.length} constant objects (${fields} fields); near misses ${[...rules].map(([r, n]) => `${r} ${n}`).join(", ") || "none"}`, + ); + for (const o of e.objects) { + if (referenced.has(o.site.atom)) continue; + lines.push(` graph ${loc(o.site.node)} ${describe(o, 0, new Set([o.site.atom]))}`); + } + for (const m of e.misses.filter((x) => x.rule !== "allocated-repeatedly").slice(0, 40)) { + lines.push( + ` near miss ${loc(m.site.node)}${m.key === null ? "" : ` .${m.key}`}: ${m.rule} (${m.detail})`, + ); + } + } + return lines; +} diff --git a/packages/compiler/src/frontend/shape-inference/domain.ts b/packages/compiler/src/frontend/shape-inference/domain.ts new file mode 100644 index 000000000..21e52346c --- /dev/null +++ b/packages/compiler/src/frontend/shape-inference/domain.ts @@ -0,0 +1,156 @@ +/* Shape inference (results/design-shape-inference/DESIGN.md), stage S1: + * the abstract domain of the allocation-site points-to analysis. + * + * An abstract value is a set of atoms. An atom names a set of runtime + * values: a primitive kind, an allocation site (object or array literal, or + * a value a builtin creates at one call site), the instances of one class, + * one class value, one function (closure site), a builtin object, or ⊤ — + * every value the analysis does not track. Value nodes hold atom sets and + * flow into each other along inclusion edges; the solver (analyze.ts) + * attaches load, store and call constraints that fire as atoms arrive. */ +import type * as ts from "../ts7/adapter.js"; + +export type AtomKind = + | "prim" + | "top" + | "object" + | "array" + | "instance" + | "class" + | "function" + | "builtin" + | "promise" + | "collection" + | "dictionary"; + +export interface Atom { + readonly id: number; + readonly kind: AtomKind; + /** Short display name, e.g. `string`, `ZodString`, `{status,value}@types.js:804`. */ + readonly label: string; + /** The syntax that creates the atom: the literal, class or function + * declaration, or the builtin call. Undefined for prims, ⊤ and builtins. */ + readonly node?: ts.Node | undefined; + /** builtin atoms: the dotted path (`Object.keys`, `array.push`). */ + readonly path?: string | undefined; + /** method atoms of builtin receivers: the receiver atom. */ + readonly receiver?: Atom | undefined; + /** function atoms: the analysis context of the function's clone (a + * method's receiver class), "" for the context-insensitive default. */ + readonly ctx?: string | undefined; + /** prim `boolean` atoms of a literal: its value. */ + readonly literal?: boolean | undefined; +} + +export class AtomTable { + readonly atoms: Atom[] = []; + private readonly byKey = new Map(); + + intern(key: string, make: (id: number) => Omit): Atom { + let atom = this.byKey.get(key); + if (atom === undefined) { + const id = this.atoms.length; + atom = { id, ...make(id) }; + this.atoms.push(atom); + this.byKey.set(key, atom); + } + return atom; + } +} + +/** Object.prototype members: a read of one of these names on a plain + * object resolves on the prototype, never to an absent own key. */ +export const OBJECT_PROTOTYPE_MEMBERS: ReadonlySet = new Set([ + "constructor", + "hasOwnProperty", + "isPrototypeOf", + "propertyIsEnumerable", + "toLocaleString", + "toString", + "valueOf", + "__defineGetter__", + "__defineSetter__", + "__lookupGetter__", + "__lookupSetter__", + "__proto__", +]); + +/** Global names the analysis models as builtin objects. Any other global is ⊤. */ +export const MODELED_GLOBALS: ReadonlySet = new Set([ + "Object", + "Array", + "JSON", + "Math", + "Number", + "String", + "Boolean", + "Symbol", + "Promise", + "Map", + "Set", + "WeakMap", + "WeakSet", + "Error", + "TypeError", + "RangeError", + "SyntaxError", + "Date", + "RegExp", + "console", + "Reflect", + "parseInt", + "parseFloat", + "isNaN", + "isFinite", + "encodeURIComponent", + "decodeURIComponent", + "encodeURI", + "decodeURI", + "BigInt", + "Proxy", +]); + +/** Results of string (and number) methods, by name. Anything else is ⊤. */ +type MethodResult = "string" | "number" | "boolean" | "array"; +export const STRING_METHOD_RESULTS: ReadonlyMap = new Map< + string, + MethodResult +>([ + ["at", "string"], + ["charAt", "string"], + ["charCodeAt", "number"], + ["codePointAt", "number"], + ["concat", "string"], + ["endsWith", "boolean"], + ["includes", "boolean"], + ["indexOf", "number"], + ["lastIndexOf", "number"], + ["localeCompare", "number"], + ["match", "array"], + ["matchAll", "array"], + ["normalize", "string"], + ["padEnd", "string"], + ["padStart", "string"], + ["repeat", "string"], + ["replace", "string"], + ["replaceAll", "string"], + ["search", "number"], + ["slice", "string"], + ["split", "array"], + ["startsWith", "boolean"], + ["substring", "string"], + ["substr", "string"], + ["toLowerCase", "string"], + ["toUpperCase", "string"], + ["toLocaleLowerCase", "string"], + ["toLocaleUpperCase", "string"], + ["toString", "string"], + ["trim", "string"], + ["trimEnd", "string"], + ["trimStart", "string"], + ["valueOf", "string"], + ["test", "boolean"], + ["toFixed", "string"], + ["toPrecision", "string"], + ["toExponential", "string"], +]); diff --git a/packages/compiler/src/frontend/shape-inference/index.ts b/packages/compiler/src/frontend/shape-inference/index.ts new file mode 100644 index 000000000..e2f7add04 --- /dev/null +++ b/packages/compiler/src/frontend/shape-inference/index.ts @@ -0,0 +1,162 @@ +/* Shape inference for untyped npm JavaScript + * (results/design-shape-inference/DESIGN.md): the report (S1) and the typed + * overlay (overlay.ts). */ +import type * as ts from "../ts7/adapter.js"; +import type { IrFunction } from "../../ir/ir.js"; +import type { ScrDiagnostic } from "../../diagnostics/diagnostic.js"; +import { npmStaticPackageOfPath } from "../npm-static.js"; +import { isShapelessPackage } from "../package-shims.js"; +import { analyzeShapes, isPackageJsFile } from "./analyze.js"; +import { OVERLAY_MARK, planShapeOverlay } from "./overlay.js"; +import { buildShapeReport } from "./report.js"; +import { computeConstantGraphs, constantGraphReport } from "./constant-graphs.js"; + +export { analyzeShapes } from "./analyze.js"; +export { buildShapeReport } from "./report.js"; + +/** SCRIPTC_DEBUG_SHAPES: `1` reports package code in hot functions, + * `verbose` every package site, `all` user files too, any other value only + * files whose path contains it. Writes to stderr; changes nothing else. The + * build cache does not key on the variable: use SCRIPTC_NO_CACHE=1. */ +export function reportShapes(program: ts.Program, filter: string): void { + const start = performance.now(); + const analysis = analyzeShapes(program, program.getTypeChecker()); + // `const`: the constant object graphs (DESIGN-constant-objects.md, stage 1). + const lines = + filter === "const" + ? constantGraphReport(analysis, computeConstantGraphs(analysis)) + : buildShapeReport(analysis, filter).lines; + const ms = (performance.now() - start).toFixed(0); + process.stderr.write(`[shapes] ${ms} ms\n${lines.map((l) => `[shapes] ${l}`).join("\n")}\n`); +} + +/* The overlay's files for the current load: the lowering treats them as + * checked JavaScript (their JSDoc types are the analysis's proven facts). */ +let overlayFiles: ReadonlySet = new Set(); +let overlayAnnotations = 0; +let overlayDeadBranches = 0; + +export function isShapeOverlayFile(fileName: string): boolean { + return overlayFiles.has(fileName); +} + +/** Whether a statement or declaration carries an overlay annotation. */ +export function shapeOverlayAnnotated(node: ts.Node): boolean { + const sf = node.getSourceFile(); + if (!overlayFiles.has(sf.fileName)) return false; + return sf.text.slice(node.pos, node.getStart(sf)).includes(OVERLAY_MARK); +} + +export function shapeOverlayFileNames(): readonly string[] { + return [...overlayFiles]; +} + +export function shapeOverlayAnnotationCount(): number { + return overlayAnnotations; +} + +export function resetShapeOverlay(): void { + overlayFiles = new Set(); + overlayAnnotations = 0; + overlayDeadBranches = 0; + inferredFactChecks = 0; +} + +/** Whether the overlay runs. While it is new it is opt-in + * (SCRIPTC_SHAPE_OVERLAY=1); `--no-shape-overlay` (the `shapeOverlay: + * false` build option) sets the variable to 0 for the build. */ +export function shapeOverlayEnabled(): boolean { + return process.env["SCRIPTC_SHAPE_OVERLAY"] === "1"; +} + +/** loadProgram7's overlayer: analyze, plan, and register the overlay files. */ +export function shapeOverlayer(program: ts.Program): ReadonlyMap | null { + resetShapeOverlay(); + const analysis = analyzeShapes(program, program.getTypeChecker()); + if (analysis.budgetExceeded) return null; + const report = buildShapeReport(analysis, "1"); + const packageFiles = analysis.files.filter((sf) => { + if (!isPackageJsFile(sf)) return false; + const pkg = npmStaticPackageOfPath(sf.fileName); + return pkg === null || !isShapelessPackage(pkg); + }); + const overlay = planShapeOverlay(analysis, report.sites, packageFiles); + overlayFiles = new Set(overlay.files.keys()); + overlayAnnotations = overlay.annotations; + overlayDeadBranches = overlay.deadBranches; + if (process.env["SCRIPTC_DEBUG_SHAPE_OVERLAY"] !== undefined) { + for (const [file, text] of overlay.files) + process.stderr.write(`[shape-overlay] ${file}\n${text}\n`); + } + return overlay.files; +} + +/** Marks the checked conversions lowered in overlay files: each checks an + * analysis-proven fact, so its failure names the site as an analysis bug. */ +export function markInferredFactChecks( + functions: readonly IrFunction[], + program: ts.Program, +): number { + if (overlayFiles.size === 0) return 0; + let count = 0; + const lines = new Map number>(); + const lineOf = (file: string, pos: number): number => { + let f = lines.get(file); + if (f === undefined) { + const sf = program.getSourceFile(file); + f = sf === undefined ? () => 0 : (p: number) => sf.getLineAndCharacterOfPosition(p).line + 1; + lines.set(file, f); + } + return f(pos); + }; + const visit = (x: unknown): void => { + if (Array.isArray(x)) { + for (const v of x) visit(v); + return; + } + if (x === null || typeof x !== "object") return; + const node = x as { + kind?: unknown; + loc?: { file: string; start: number }; + inferredSite?: string; + }; + if (node.kind === "dynCheck" && node.loc !== undefined && overlayFiles.has(node.loc.file)) { + const pkg = npmStaticPackageOfPath(node.loc.file) ?? ""; + const rel = node.loc.file.split("/node_modules/").pop() ?? node.loc.file; + node.inferredSite = `${pkg} ${rel}:${lineOf(node.loc.file, node.loc.start)}`; + count++; + } + for (const v of Object.values(x)) if (v !== null && typeof v === "object") visit(v); + }; + for (const fn of functions) visit(fn.body); + inferredFactChecks = count; + return count; +} + +let inferredFactChecks = 0; + +/** Inferred-fact checks inserted by the last build (the build report). */ +export function shapeInferredFactCheckCount(): number { + return inferredFactChecks; +} + +/** SC6103, the overlay's build report: what the overlay typed and how many + * checks of inferred facts the build inserted. A note, never a failure. */ +export function shapeOverlayReport(entryFile: string): ScrDiagnostic[] { + if (overlayFiles.size === 0) return []; + const packages = new Set([...overlayFiles].map((f) => npmStaticPackageOfPath(f) ?? f)); + return [ + { + code: "SC6103", + message: + `shape inference typed ${overlayAnnotations} declarations in ${overlayFiles.size} files of ${packages.size} ` + + `package${packages.size === 1 ? "" : "s"} (${[...packages].sort().join(", ")}); the build checks ` + + `${inferredFactChecks} inferred fact${inferredFactChecks === 1 ? "" : "s"} at run time` + + (overlayDeadBranches > 0 + ? ` and replaces ${overlayDeadBranches} branch${overlayDeadBranches === 1 ? "" : "es"} it proved unreachable with checks` + : ""), + loc: { file: entryFile, start: 0, end: 0 }, + hint: "a failing check reports a scriptc analysis bug; --no-shape-overlay compiles without the overlay", + }, + ]; +} diff --git a/packages/compiler/src/frontend/shape-inference/overlay.test.ts b/packages/compiler/src/frontend/shape-inference/overlay.test.ts new file mode 100644 index 000000000..f0effae77 --- /dev/null +++ b/packages/compiler/src/frontend/shape-inference/overlay.test.ts @@ -0,0 +1,32 @@ +import { afterAll, expect, test } from "vitest"; +import { closeSourceParser, parseSourceFile } from "../ts7/source-parser-node.js"; +import { neutralizeDocTags, OVERLAY_MARK } from "./overlay.js"; + +afterAll(closeSourceParser); + +test("neutralizeDocTags rewrites JSDoc comments and keeps literals", () => { + const source = [ + "/** @param {number} a */", + "function f(a) {", + ' const s = "/** @type {number} */";', + " const t = `/** @returns {string} ${a} */`;", + " const r = /\\/\\*\\* @param \\*\\//;", + " // /** @type {string} */", + ` /** @type {string} ${OVERLAY_MARK} */ const u = s + t;`, + " return a / 2 /** @returns {number} */;", + "}", + ].join("\n"); + const out = neutralizeDocTags(parseSourceFile("overlay.js", source, "js")); + expect(out.length).toBe(source.length); + expect(out.split("\n")).toEqual([ + "/** -param {number} a */", + "function f(a) {", + ' const s = "/** @type {number} */";', + " const t = `/** @returns {string} ${a} */`;", + " const r = /\\/\\*\\* @param \\*\\//;", + " // /** @type {string} */", + ` /** @type {string} ${OVERLAY_MARK} */ const u = s + t;`, + " return a / 2 /** -returns {number} */;", + "}", + ]); +}); diff --git a/packages/compiler/src/frontend/shape-inference/overlay.ts b/packages/compiler/src/frontend/shape-inference/overlay.ts new file mode 100644 index 000000000..edaad7b80 --- /dev/null +++ b/packages/compiler/src/frontend/shape-inference/overlay.ts @@ -0,0 +1,774 @@ +/* Shape inference: the typed overlay (results/design-shape-inference, + * maintainer decision 2026-10-23). The analysis's proven facts become JSDoc + * types in a rewritten copy of each package JavaScript file, and the + * lowering treats the rewritten file like a checked JavaScript file, so the + * static tier compiles the package's hot code from those types. + * + * Only proven facts are written: a position whose value set contains ⊤ + * (anything from islands, JSON.parse, the network or the file system, + * reflection, globals, `process`, caught exceptions, or code the analysis + * cannot see) stays untyped. A check the lowering inserts where a value + * meets an inferred type therefore fails only when the analysis is wrong; + * such checks carry the inferred-fact failure message. + * + * The package's own JSDoc is a hint, not a declaration: its tags are + * neutralized in the rewritten file, so only the analysis's types remain. + * + * Every edit is inline: no line breaks are added or removed, so positions + * keep their lines. */ +import * as ts from "../ts7/adapter.js"; +import type { Atom } from "./domain.js"; +import type { ShapeAnalysis } from "./analyze.js"; +import type { SiteVerdict } from "./report.js"; + +export interface ShapeOverlay { + /** Rewritten text per package file. */ + readonly files: Map; + /** Annotations written, for the build report. */ + readonly annotations: number; + /** Branches the analysis proved unreachable, replaced by checks. */ + readonly deadBranches: number; +} + +interface Edit { + readonly pos: number; + readonly text: string; + /** Replaces pos..end instead of inserting at pos. */ + readonly end?: number; +} + +/** Marks the overlay's own comments (an unknown JSDoc tag, ignored). */ +export const OVERLAY_MARK = "@scriptcShape"; +const MARK = OVERLAY_MARK; + +/** Above 1, values of several record key sets type as one merged record + * with optional fields (SCRIPTC_SHAPE_RECORD_ARMS; experimental). */ +const MAX_RECORD_ARMS = Number(process.env.SCRIPTC_SHAPE_RECORD_ARMS ?? 1); + +/** The most class or record arms an inferred union may have. */ +const MAX_ARMS = 6; + +function list(arr: { readonly nodes: readonly T[] } | undefined): readonly T[] { + return arr?.nodes ?? []; +} + +let debugIndex = 0; + +export function planShapeOverlay( + analysis: ShapeAnalysis, + verdicts: readonly SiteVerdict[], + packageFiles: readonly ts.SourceFile[], +): ShapeOverlay { + const pSites = new Map(); + const siteVerdicts = new Set(verdicts.map((v) => v.site.atom)); + // Records and arrays print only for sites whose objects never escape: a + // native record that reached untyped code would be seen as a copy. + const siteKeys = new Map }>(); + for (const v of verdicts) { + if (v.verdict !== "P-local") continue; + if (v.site.kind === "array") { + pSites.set(v.site.atom, v); + continue; + } + const keys = keysOfSite(analysis, v); + if (keys === null) continue; + siteKeys.set(v.site.atom, keys); + pSites.set(v.site.atom, v); + } + const files = new Map(); + let annotations = 0; + let deadBranches = 0; + debugIndex = 0; + // `if` branches no condition value allowed, per file: the overlay + // replaces each with a throwing check, so the checker and the lowering + // see the code the analysis saw. + const deadByFile = new Map(); + if (process.env["SCRIPTC_SHAPE_DEAD_BRANCHES"] !== "0") + for (const s of analysis.branchIfs) { + for (const branch of [s.thenStatement, s.elseStatement]) { + if (branch === undefined || analysis.walkedBranches.has(branch)) continue; + const sf = s.getSourceFile(); + let list = deadByFile.get(sf); + if (list === undefined) deadByFile.set(sf, (list = [])); + list.push(branch); + } + } + for (const sf of packageFiles) { + const typedefs = new Map(); + const typedefText: string[] = []; + const edits: Edit[] = []; + + const className = (decl: ts.Node): string | null => { + if (decl.getSourceFile() !== sf) return null; + const own = (decl as ts.ClassLikeDeclaration).name; + if (own !== undefined && ts.isClassDeclaration(decl) && decl.parent === sf) return own.text; + const p = decl.parent; + if ( + p !== undefined && + ts.isVariableDeclaration(p) && + ts.isIdentifier(p.name) && + p.parent?.parent?.parent === sf + ) + return p.name.text; + return null; + }; + + // `inner`: inside a record field, where ⊤ and unprintable values are `any`. + const typeOf = (atoms: ReadonlySet, inner: boolean, depth: number): string | null => { + if (atoms.size === 0) return inner ? "any" : null; + const arms = new Set(); + let objects = 0; + // Every array arm becomes one Array<…> of the joined element values. + const elements = new Set(); + let hasArray = false; + const promised = new Set(); + let hasPromise = false; + const recordSites: Atom[] = []; + for (const a of atoms) { + let t: string | null = null; + switch (a.kind) { + case "prim": + t = + a.label === "null" || + a.label === "undefined" || + a.label === "string" || + a.label === "number" || + a.label === "boolean" || + a.label === "bigint" + ? a.label + : null; + break; + case "instance": + t = a.node !== undefined ? className(a.node) : null; + objects++; + break; + case "object": { + objects++; + const v = pSites.get(a); + if (v === undefined || depth >= 4 || !siteKeys.has(a)) return inner ? "any" : null; + recordSites.push(a); + continue; + } + case "array": { + objects++; + if (analysis.escaped.has(a) || (siteVerdicts.has(a) && !pSites.has(a))) + return inner ? "any" : null; + const el = analysis.elements.get(a); + if (el !== undefined) for (const x of analysis.nodes[el]!.atoms) elements.add(x); + hasArray = true; + continue; + } + case "promise": { + // One promise arm: the values it resolves to, joined. + const value = analysis.fields.get(a)?.get("value"); + if (value !== undefined) for (const x of analysis.nodes[value]!.atoms) promised.add(x); + hasPromise = true; + continue; + } + default: + t = null; + } + if (t === null) return inner ? "any" : null; + arms.add(t); + } + // Records of one key set print as their typedef; records of several + // key sets merge into one record whose fields missing from some sites + // are optional (unions of overlapping records would need the lowering + // to pick an arm per value). + if (recordSites.length > 0) { + const keySets = new Set(recordSites.map(keySetOf)); + if (keySets.size === 1) arms.add(recordType(recordSites[0]!, depth)); + else if (MAX_RECORD_ARMS > 1) arms.add(mergedRecordType(recordSites, depth)); + else return inner ? "any" : null; + } + if (hasArray) { + const inside = depth < 4 ? typeOf(elements, true, depth + 1) : "any"; + arms.add(`Array<${inside ?? "any"}>`); + } + // A promise beside other values cannot cross into untyped code + // (maybe-promise unions stay typed-only), so it stays untyped here. + if (hasPromise) return inner ? "any" : null; + if (objects > MAX_ARMS) return inner ? "any" : null; + if (arms.has("any")) return inner ? "any" : null; + return [...arms].join(" | "); + }; + + // One typedef per key set: sites with the same keys intern to the same + // structural record, so their field types are joined. + const keySetOf = (site: Atom): string => { + const k = siteKeys.get(site)!; + return k.keys + .map((key) => (k.optional.has(key) ? `${key}?` : key)) + .sort() + .join(","); + }; + const sitesByKeys = new Map(); + for (const site of pSites.keys()) { + if (pSites.get(site)!.site.kind !== "object") continue; + const k = keySetOf(site); + let l = sitesByKeys.get(k); + if (l === undefined) sitesByKeys.set(k, (l = [])); + l.push(site); + } + const recordType = (site: Atom, depth: number): string => { + const keySet = keySetOf(site); + const existing = typedefs.get(keySet); + if (existing !== undefined) return existing; + const name = `__shape_${typedefs.size}`; + typedefs.set(keySet, name); + const { keys, optional } = siteKeys.get(site)!; + const parts = keys.map((k) => { + const atoms = new Set(); + for (const other of sitesByKeys.get(keySet) ?? [site]) { + const id = analysis.fields.get(other)?.get(k); + if (id !== undefined) for (const a of analysis.nodes[id]!.atoms) atoms.add(a); + } + const t = typeOf(atoms, true, depth + 1); + return `${/^[A-Za-z_$][\w$]*$/.test(k) ? k : JSON.stringify(k)}${optional.has(k) ? "?" : ""}: ${t ?? "any"}`; + }); + typedefText.push(`/** @typedef {{${parts.join(", ")}}} ${name} ${MARK} */`); + return name; + }; + + const mergedRecordType = (sites: readonly Atom[], depth: number): string => { + const all = new Set(); + for (const site of sites) + for (const other of sitesByKeys.get(keySetOf(site)) ?? [site]) all.add(other); + const keys: string[] = []; + const optional = new Set(); + for (const site of all) { + const k = siteKeys.get(site)!; + for (const key of k.keys) { + if (!keys.includes(key)) keys.push(key); + if (k.optional.has(key)) optional.add(key); + } + } + for (const site of all) { + const own = siteKeys.get(site)!.keys; + for (const key of keys) if (!own.includes(key)) optional.add(key); + } + const keySet = `merged:${keys + .map((k) => (optional.has(k) ? `${k}?` : k)) + .sort() + .join(",")}`; + const existing = typedefs.get(keySet); + if (existing !== undefined) return existing; + const name = `__shape_${typedefs.size}`; + typedefs.set(keySet, name); + const parts = keys.map((k) => { + const atoms = new Set(); + for (const site of all) { + const id = analysis.fields.get(site)?.get(k); + if (id !== undefined) for (const a of analysis.nodes[id]!.atoms) atoms.add(a); + } + const t = typeOf(atoms, true, depth + 1); + return `${/^[A-Za-z_$][\w$]*$/.test(k) ? k : JSON.stringify(k)}${optional.has(k) ? "?" : ""}: ${t ?? "any"}`; + }); + typedefText.push(`/** @typedef {{${parts.join(", ")}}} ${name} ${MARK} */`); + return name; + }; + + // Functions called with `new` yield their instance, whatever they return. + const constructed = new Set( + [...analysis.fnOfInstance.values()].map((a) => a.node), + ); + // Return values merged over every analysis context of each function. + const returns = new Map>(); + for (const info of analysis.functions.values()) { + if (info.node.getSourceFile() !== sf || info.isAsync || info.isGenerator) continue; + let set = returns.get(info.node); + if (set === undefined) returns.set(info.node, (set = new Set())); + for (const a of analysis.nodes[info.ret]!.atoms) set.add(a); + } + + // Receivers each method runs on, merged over its analysis contexts. + const thisAtoms = new Map>(); + for (const info of analysis.functions.values()) { + if (info.node.getSourceFile() !== sf || !ts.isMethodDeclaration(info.node)) continue; + let set = thisAtoms.get(info.node); + if (set === undefined) thisAtoms.set(info.node, (set = new Set())); + for (const a of analysis.nodes[info.thisNode]!.atoms) set.add(a); + } + const declaresMember = (cls: ts.Node, name: string): boolean => + analysis.classChain(cls).some((c) => + (c as ts.ClassLikeDeclaration).members.nodes.some((m) => { + const n = (m as { name?: ts.Node }).name; + return ( + n !== undefined && (ts.isIdentifier(n) || ts.isPrivateIdentifier(n)) && n.text === name + ); + }), + ); + // `this.m(…)` in an inherited method whose class does not declare `m` + // (zod's ZodType._parseSync calls the subclasses' _parse): the receiver + // is cast to the classes the method runs on, so the call dispatches over + // them instead of through an untyped call. + const receiverCasts = (method: ts.MethodDeclaration): void => { + const cls = method.parent; + const receivers = thisAtoms.get(method); + if (cls === undefined || receivers === undefined || receivers.size === 0) return; + if (![...receivers].every((a) => a.kind === "instance")) return; + const t = typeOf(receivers, false, 0); + if (t === null) return; + const scan = (node: ts.Node): void => { + if (ts.isFunctionLike(node) && !ts.isArrowFunction(node)) return; + if ( + ts.isCallExpression(node) && + ts.isPropertyAccessExpression(node.expression) && + node.expression.expression.kind === ts.SyntaxKind.ThisKeyword && + ts.isIdentifier(node.expression.name) && + !declaresMember(cls, node.expression.name.text) + ) { + const self = node.expression.expression; + edits.push({ pos: ts.tokenStartOfNode(self), text: `/** @type {${t}} ${MARK} */ (` }); + edits.push({ pos: self.end, text: ")" }); + annotations++; + } + ts.forEachChild(node, (c) => { + scan(c); + return undefined; + }); + }; + if (method.body !== undefined) for (const st of method.body.statements.nodes) scan(st); + }; + + // Same-named instance methods of the file's classes form a family: a + // call on a union of those classes needs one signature, so each member's + // parameters and result join the whole family's values. + const families = new Map(); + const familyVisit = (node: ts.Node): void => { + if ( + ts.isMethodDeclaration(node) && + ts.isIdentifier(node.name) && + !list((node as { modifiers?: { nodes: readonly ts.Node[] } }).modifiers).some( + (m) => m.kind === ts.SyntaxKind.StaticKeyword, + ) + ) { + let l = families.get(node.name.text); + if (l === undefined) families.set(node.name.text, (l = [])); + l.push(node); + } + ts.forEachChild(node, (c) => { + familyVisit(c); + return undefined; + }); + }; + familyVisit(sf); + // Members the program runs (they have analysis contexts); members it + // never reaches would only widen the family's types. + const reached = new Set(); + for (const info of analysis.functions.values()) + if (info.node.getSourceFile() === sf) reached.add(info.node); + const familyOf = (fn: ts.FunctionLikeDeclaration): readonly ts.MethodDeclaration[] => { + if (!ts.isMethodDeclaration(fn) || !ts.isIdentifier(fn.name)) + return [fn as ts.MethodDeclaration]; + const members = (families.get(fn.name.text) ?? [fn]).filter((m) => reached.has(m)); + return members.length > 0 ? members : [fn]; + }; + const paramValues = (fn: ts.FunctionLikeDeclaration, index: number): Set => { + const out = new Set(); + for (const member of familyOf(fn)) { + const p = list(member.parameters)[index]; + if (p === undefined) continue; + for (const a of analysis.declValues(p)) out.add(a); + } + return out; + }; + const returnValues = (fn: ts.FunctionLikeDeclaration): Set | undefined => { + let out: Set | undefined; + for (const member of familyOf(fn)) { + const r = returns.get(member); + if (r === undefined) continue; + out ??= new Set(); + for (const a of r) out.add(a); + } + return out; + }; + + const paramTags = (fn: ts.FunctionLikeDeclaration): string | null => { + const tags: string[] = []; + if (!ts.isConstructorDeclaration(fn) && !constructed.has(fn)) { + const ret = returnValues(fn); + const t = ret !== undefined && ret.size > 0 ? typeOf(ret, false, 0) : null; + const why = process.env["SCRIPTC_DEBUG_SHAPE_OVERLAY_WHY"]; + if ( + why !== undefined && + t === null && + ret !== undefined && + ts.isMethodDeclaration(fn) && + ts.isIdentifier(fn.name) && + fn.name.text === why + ) + process.stderr.write( + `[shape-overlay-why] ${fn.name.text}: ${[...ret].map((a) => `${a.kind}:${a.label}${analysis.escaped.has(a) ? "(escaped)" : ""}`).join(" | ")}\n`, + ); + if (t !== null) tags.push(`@returns {${t}}`); + } + list(fn.parameters).forEach((p, index) => { + if ( + !ts.isIdentifier(p.name) || + p.dotDotDotToken !== undefined || + p.initializer !== undefined || + p.questionToken !== undefined + ) + return; + const t = typeOf(paramValues(fn, index), false, 0); + if (t !== null) tags.push(`@param {${t}} ${p.name.text}`); + }); + return tags.length > 0 ? `${tags.join(" ")} ${MARK}` : null; + }; + + const visit = (node: ts.Node): void => { + if (ts.isMethodDeclaration(node)) receiverCasts(node); + if ( + ts.isMethodDeclaration(node) || + ts.isConstructorDeclaration(node) || + ts.isFunctionDeclaration(node) + ) { + if (node.body !== undefined) { + const tags = paramTags(node); + if (tags !== null) { + edits.push({ pos: ts.tokenStartOfNode(node), text: `/** ${tags} */ ` }); + annotations++; + } + } + // `this.f = …` fields of a constructor's top-level statements. + if (ts.isConstructorDeclaration(node) && node.body !== undefined) { + const cls = node.parent; + const seen = new Set(); + for (const st of list(node.body.statements)) { + if (!ts.isExpressionStatement(st) || !ts.isBinaryExpression(st.expression)) continue; + const b = st.expression; + if ( + b.operatorToken.kind !== ts.SyntaxKind.EqualsToken || + !ts.isPropertyAccessExpression(b.left) + ) + continue; + if ( + b.left.expression.kind !== ts.SyntaxKind.ThisKeyword || + !ts.isIdentifier(b.left.name) + ) + continue; + const name = b.left.name.text; + if (seen.has(name)) continue; + seen.add(name); + const t = cls !== undefined ? fieldType(cls, name) : null; + if (t !== null) { + edits.push({ pos: ts.tokenStartOfNode(st), text: `/** @type {${t}} ${MARK} */ ` }); + annotations++; + } + } + } + } + // Locals only: module-level bindings are initialized once (and a + // record-typed module constant can turn a return type into a union of + // overlapping records, which the lowering width-coerces). + if ( + ts.isVariableStatement(node) && + (node.parent !== sf || process.env["SCRIPTC_SHAPE_OVERLAY_MODULE_VARS"] === "1") && + node.declarationList.declarations.nodes.length === 1 + ) { + const d = node.declarationList.declarations.nodes[0]!; + const init = d.initializer; + if (ts.isIdentifier(d.name) && init !== undefined && !literalInitializer(init)) { + const t = typeOf(analysis.declValues(d), false, 0); + if (t !== null) { + edits.push({ pos: ts.tokenStartOfNode(node), text: `/** @type {${t}} ${MARK} */ ` }); + annotations++; + } + } + } + ts.forEachChild(node, (c) => { + visit(c); + return undefined; + }); + }; + + const fieldType = (cls: ts.Node, name: string): string | null => { + const atoms = new Set(); + for (const a of analysis.atoms.atoms) { + if ( + a.kind !== "instance" || + a.node === undefined || + !analysis.classChain(a.node).includes(cls) + ) + continue; + const id = analysis.fields.get(a)?.get(name); + if (id !== undefined) for (const x of analysis.nodes[id]!.atoms) atoms.add(x); + } + return typeOf(atoms, false, 0); + }; + + visit(sf); + // Dead branches: outermost first; edits inside a replaced range drop. + const dead = (deadByFile.get(sf) ?? []).sort((a, b) => a.pos - b.pos); + const ranges: [number, number][] = []; + const removed = new Set(); + for (const branch of dead) { + const start = ts.tokenStartOfNode(branch); + if (ranges.some(([lo, hi]) => start >= lo && start < hi)) continue; + if (hoistedNames(branch, removed) === null) continue; + removed.add(branch); + ranges.push([start, branch.end]); + } + // A var that only removed branches declare stays declared in each. + const hoisted = new Map(); + for (const branch of removed) + hoisted.set(ts.tokenStartOfNode(branch), hoistedNames(branch, removed)!); + if (ranges.length > 0) { + const kept = edits.filter((e) => !ranges.some(([lo, hi]) => e.pos > lo && e.pos < hi)); + edits.length = 0; + edits.push(...kept); + const pkg = packageNameOf(sf.fileName); + const rel = sf.fileName.split("/node_modules/").pop() ?? sf.fileName; + for (const [lo, hi] of ranges) { + const line = sf.getLineAndCharacterOfPosition(lo).line + 1; + const message = + `scriptc internal error: shape inference proved the branch at ${rel}:${line} unreachable, but it ran. ` + + `This is a bug in scriptc's shape analysis, not in the program. Rebuild with --no-shape-overlay, ` + + `or set "scriptc": {"packages": {"${pkg}": {"shapes": false}}} in package.json.`; + // The branch's own var names stay declared in its scope, undefined. + const names = hoisted.get(lo)!; + const vars = names.length > 0 ? `var ${names.join(", ")}; ` : ""; + edits.push({ + pos: lo, + end: hi, + text: `{ ${vars}throw new Error(${JSON.stringify(message)}); }`, + }); + deadBranches++; + } + } + // Debugging: SCRIPTC_SHAPE_OVERLAY_ONLY=a-b keeps the annotations + // numbered a..b (program order across files), to bisect a miscompile. + const only = process.env["SCRIPTC_SHAPE_OVERLAY_ONLY"]; + if (only !== undefined) { + const [lo, hi] = only.split("-").map(Number) as [number, number]; + edits.sort((a, b) => a.pos - b.pos); + const kept = edits.filter(() => { + const n = debugIndex++; + return n >= lo && n <= hi; + }); + edits.length = 0; + edits.push(...kept); + } + if (edits.length === 0) continue; + files.set(sf.fileName, applyEdits(neutralizeDocTags(sf), edits) + typedefText.join("")); + } + return { files, annotations, deadBranches }; +} + +function packageNameOf(fileName: string): string { + const parts = (fileName.split("/node_modules/").pop() ?? "").split("/"); + return parts[0]!.startsWith("@") ? `${parts[0]}/${parts[1]}` : parts[0]!; +} + +function literalInitializer(e: ts.Expression): boolean { + return ( + ts.isObjectLiteralExpression(e) || + (ts.isArrayLiteralExpression(e) && e.elements.nodes.length > 0) || + ts.isStringLiteral(e) || + ts.isNumericLiteral(e) || + ts.isNoSubstitutionTemplateLiteral(e) || + ts.isArrowFunction(e) || + ts.isFunctionExpression(e) || + e.kind === ts.SyntaxKind.TrueKeyword || + e.kind === ts.SyntaxKind.FalseKeyword + ); +} + +function applyEdits(text: string, edits: readonly Edit[]): string { + const sorted = [...edits].sort((a, b) => a.pos - b.pos); + let out = ""; + let at = 0; + for (const e of sorted) { + out += text.slice(at, e.pos) + e.text; + at = e.end ?? e.pos; + } + return out + text.slice(at); +} + +/** The file's text with the JSDoc type tags of its comments made plain + * text: package JSDoc is a hint, not a declaration. String, template and + * regular expression literals and JSX text are copied unchanged, so + * comment-like text in them keeps its runtime value. Lengths are unchanged, + * so edit positions in the source text stay valid. */ +export function neutralizeDocTags(sf: ts.SourceFile): string { + const text = sf.text; + // Literal token ranges, in source order. + const literals: [number, number][] = []; + const visit = (node: ts.Node): void => { + if (ts.isJsxText(node)) { + literals.push([node.pos, node.end]); + return; + } + if ( + ts.isStringLiteral(node) || + ts.isNoSubstitutionTemplateLiteral(node) || + ts.isTemplateHead(node) || + ts.isTemplateMiddle(node) || + ts.isTemplateTail(node) || + ts.isRegularExpressionLiteral(node) + ) { + literals.push([ts.tokenStartOfNode(node), node.end]); + return; + } + ts.forEachChild(node, (c) => { + visit(c); + return undefined; + }); + }; + visit(sf); + literals.sort((a, b) => a[0] - b[0]); + let out = ""; + let copied = 0; + let next = 0; + let pos = 0; + // Outside literals, `//` and `/*` only start comments. + while (pos < text.length) { + while (next < literals.length && literals[next]![1] <= pos) next++; + const literal = literals[next]; + if (literal !== undefined && literal[0] <= pos) { + pos = literal[1]; + continue; + } + if (text.charCodeAt(pos) !== 47) { + pos++; + continue; + } + const second = text.charCodeAt(pos + 1); + if (second === 47) { + while (pos < text.length && !lineBreak(text.charCodeAt(pos))) pos++; + continue; + } + if (second !== 42) { + pos++; + continue; + } + const close = text.indexOf("*/", pos + 2); + const end = close < 0 ? text.length : close + 2; + const comment = text.slice(pos, end); + if (comment.startsWith("/**") && !comment.includes(MARK)) { + out += text.slice(copied, pos) + neutralizeComment(comment); + copied = end; + } + pos = end; + } + return out + text.slice(copied); +} + +function lineBreak(code: number): boolean { + return code === 10 || code === 13 || code === 0x2028 || code === 0x2029; +} + +function neutralizeComment(comment: string): string { + return comment.replace( + /@(param|type|typedef|returns?|template|this|callback|property|prop|arg|argument|enum|implements|extends|augments|satisfies|overload|import)\b/g, + "-$1", + ); +} + +/** The `var` names a dead branch declares that its function (or file) + * declares nowhere else outside the `removed` branches: replacing the + * branch must keep them bound, as undefined. Null when the branch declares + * a function in a block, whose binding outside the block depends on the + * file's strictness; such a branch is kept. */ +function hoistedNames(branch: ts.Node, removed: ReadonlySet): readonly string[] | null { + let scope: ts.Node | undefined = branch.parent; + while (scope !== undefined && !ts.isSourceFile(scope) && !functionScope(scope)) + scope = scope.parent; + if (scope === undefined) return null; + const own = new Set(); + let blockFunction = false; + ts.walkPreorder(branch, (n) => { + if (n !== branch && ts.isFunctionDeclaration(n)) { + blockFunction = true; + return "skip"; + } + if (n !== branch && (functionScope(n) || ts.isClassDeclaration(n) || ts.isClassExpression(n))) + return "skip"; + if (ts.isVariableDeclarationList(n) && (n.flags & ts.NodeFlags.BlockScoped) === 0) + for (const d of n.declarations.nodes) addBindingNames(d.name, own); + return undefined; + }); + if (blockFunction) return null; + if (own.size === 0) return []; + // Names the scope binds outside the branch: parameters, a function + // expression's name, other var declarations and function declarations. + const elsewhere = new Set(); + if (!ts.isSourceFile(scope)) { + for (const p of list( + (scope as { parameters?: { readonly nodes: readonly ts.ParameterDeclaration[] } }).parameters, + )) + addBindingNames(p.name, elsewhere); + const name = (scope as { name?: ts.Node }).name; + if (name !== undefined && ts.isIdentifier(name)) elsewhere.add(name.text); + } + const body = ts.isSourceFile(scope) ? scope : (scope as { body?: ts.Node }).body; + if (body !== undefined) + ts.walkPreorder(body, (n) => { + if (n === branch || removed.has(n)) return "skip"; + if (n !== body && ts.isFunctionDeclaration(n) && n.name !== undefined) + elsewhere.add(n.name.text); + if (n !== body && (functionScope(n) || ts.isClassDeclaration(n) || ts.isClassExpression(n))) + return "skip"; + if (ts.isVariableDeclarationList(n) && (n.flags & ts.NodeFlags.BlockScoped) === 0) + for (const d of n.declarations.nodes) addBindingNames(d.name, elsewhere); + return undefined; + }); + return [...own].filter((name) => !elsewhere.has(name)); +} + +function functionScope(n: ts.Node): boolean { + return ( + ts.isFunctionDeclaration(n) || + ts.isFunctionExpression(n) || + ts.isArrowFunction(n) || + ts.isMethodDeclaration(n) || + ts.isGetAccessorDeclaration(n) || + ts.isSetAccessorDeclaration(n) || + ts.isConstructorDeclaration(n) || + ts.isClassStaticBlockDeclaration(n) + ); +} + +function addBindingNames(name: ts.BindingName, out: Set): void { + if (ts.isIdentifier(name)) { + out.add(name.text); + return; + } + for (const element of name.elements.nodes) + if (!ts.isOmittedExpression(element) && element.name !== undefined) + addBindingNames(element.name, out); +} + +/** A record site's keys: its static keys, or for a literal with spreads its + * own keys plus every key a spread source contributes (optional unless + * every value of that source has it). Null when a computed key or an + * untracked spread source makes the key set unknown. */ +function keysOfSite( + analysis: ShapeAnalysis, + v: SiteVerdict, +): { keys: string[]; optional: Set } | null { + const site = v.site; + if (site.keys !== null) return { keys: site.keys, optional: new Set() }; + if (site.hasComputedKey) return null; + const fields = analysis.fields.get(site.atom); + const any = fields?.get("*"); + if (any !== undefined && analysis.nodes[any]!.atoms.size > 0) return null; + const required = new Set(site.ownKeys); + for (const spread of site.spreads) { + const sets: Set[] = []; + for (const a of analysis.nodes[spread]!.atoms) { + if (a.kind === "prim" && (a.label === "undefined" || a.label === "null")) continue; + if (a.kind !== "object") return null; + const own = new Set(); + for (const k of analysis.fields.get(a)?.keys() ?? []) if (k !== "*") own.add(k); + sets.push(own); + } + const first = sets[0]; + if (first === undefined) continue; + for (const k of first) if (sets.every((set) => set.has(k))) required.add(k); + } + const all = [...site.ownKeys]; + for (const k of fields?.keys() ?? []) if (k !== "*" && !all.includes(k)) all.push(k); + return { keys: all, optional: new Set(all.filter((k) => !required.has(k))) }; +} diff --git a/packages/compiler/src/frontend/shape-inference/report.ts b/packages/compiler/src/frontend/shape-inference/report.ts new file mode 100644 index 000000000..c18d9eb56 --- /dev/null +++ b/packages/compiler/src/frontend/shape-inference/report.ts @@ -0,0 +1,374 @@ +/* Shape inference stage S1: the plan the analysis would hand the lowering, + * as a report (DESIGN.md §4.5, §5.5). Nothing here changes compilation. + * + * Per allocation site of package code, the verdict: + * P local — a native record would be exact: no reflection, no added + * keys, no computed writes, and its objects never escape; + * P escaping — exact as a record behind identity-keeping capsules: it + * escapes, but the census leaves every later operation + * answerable (fields untyped code may write become dyn); + * G — keeps today's dynamic object, with the rule that decided. + * Plus class field types, the census, and the hot functions' value types. */ +import * as ts from "../ts7/adapter.js"; +import { OBJECT_PROTOTYPE_MEMBERS, type Atom } from "./domain.js"; +import { locLabel, type FunctionInfo, type LiteralSite, type ShapeAnalysis } from "./analyze.js"; + +export interface SiteVerdict { + readonly site: LiteralSite; + readonly label: string; + readonly verdict: "P-local" | "P-escaping" | "G"; + readonly reasons: string[]; + /** P-escaping: fields untyped code may write (they become dyn slots). */ + readonly writableByTop: string[]; + readonly freezable: boolean; + readonly brand: boolean; +} + +export interface ShapeReport { + readonly sites: SiteVerdict[]; + readonly hot: Set; + readonly lines: string[]; +} + +const MAX_ATOMS_SHOWN = 6; + +function fieldKeys(fields: ReadonlyMap | undefined): string[] { + return fields === undefined ? [] : [...fields.keys()]; +} + +function fieldEntries(fields: ReadonlyMap | undefined): [string, number][] { + return fields === undefined ? [] : [...fields.entries()]; +} + +export function describeValue(analysis: ShapeAnalysis, node: number): string { + const atoms = [...analysis.nodes[node]!.atoms]; + if (atoms.length === 0) return "∅"; + const labels = [...new Set(atoms.map((a) => a.label))]; + labels.sort((a, b) => (a === "⊤" ? 1 : b === "⊤" ? -1 : a.localeCompare(b))); + const shown = labels.slice(0, MAX_ATOMS_SHOWN).join(" | "); + if (labels.length <= MAX_ATOMS_SHOWN) return shown; + return `${shown} | +${labels.length - MAX_ATOMS_SHOWN}${labels.includes("⊤") ? " | ⊤" : ""}`; +} + +function atomsOf(analysis: ShapeAnalysis, node: number): Set { + return analysis.nodes[node]!.atoms; +} + +/** Functions reached from call sites in loops (or loop callbacks), and + * everything they call: the static hotness heuristic (DESIGN.md §8.1). */ +export function hotFunctions(analysis: ShapeAnalysis): Set { + const hot = new Set(); + const callees = new Map(); + const work: FunctionInfo[] = []; + for (const c of analysis.calls) { + let l = callees.get(c.caller); + if (l === undefined) callees.set(c.caller, (l = [])); + l.push(c.callee); + if (c.inLoop && !hot.has(c.callee)) { + hot.add(c.callee); + work.push(c.callee); + } + } + while (work.length > 0) { + const f = work.pop()!; + for (const g of callees.get(f) ?? []) { + if (!hot.has(g)) { + hot.add(g); + work.push(g); + } + } + } + return hot; +} + +export function buildShapeReport(analysis: ShapeAnalysis, filter: string): ShapeReport { + const lines: string[] = []; + const all = filter === "all" || filter === "verbose"; + const verbose = filter === "verbose"; + const fileFilter = filter !== "1" && !all ? filter : null; + const wanted = (n: ts.Node, inPackage: boolean): boolean => { + if (fileFilter !== null) return n.getSourceFile().fileName.includes(fileFilter); + return all || inPackage; + }; + + // Census facts: operations whose receiver may be ⊤ reach every escaped atom. + + // Index ops by the site atoms they can reach directly. + const byAtom = new Map(); + const topOps: typeof analysis.ops = []; + for (const o of analysis.ops) { + const atoms = atomsOf(analysis, o.recv); + for (const a of atoms) { + if (a.kind !== "object" && a.kind !== "array") continue; + let l = byAtom.get(a); + if (l === undefined) byAtom.set(a, (l = [])); + l.push(o); + } + if (atoms.has(analysis.top)) topOps.push(o); + } + + // Key-order groups: sites with the same key set and different orders need brands. + const orders = new Map>(); + for (const s of analysis.sites) { + if (s.kind !== "object" || s.keys === null) continue; + const set = [...s.keys].sort().join(","); + let o = orders.get(set); + if (o === undefined) orders.set(set, (o = new Set())); + o.add(s.keys.join(",")); + } + + const verdicts: SiteVerdict[] = []; + for (const s of analysis.sites) { + if (!s.inPackage && !all && fileFilter === null) continue; + const reasons: string[] = []; + const escapedWhy = analysis.escaped.get(s.atom); + const escapes = escapedWhy !== undefined; + if (s.hasAccessor) reasons.push("accessor in literal"); + if (s.hasProto) reasons.push(s.kind === "array" ? "hole in literal" : "__proto__ in literal"); + if (s.hasComputedKey) reasons.push("computed key in literal"); + for (const sp of s.spreads) + if (atomsOf(analysis, sp).has(analysis.top)) reasons.push("spread of ⊤"); + const direct = byAtom.get(s.atom) ?? []; + const ops = escapes ? [...direct, ...topOps] : direct; + const keys = new Set(s.keys ?? []); + const fieldsOf = analysis.fields.get(s.atom); + const knownKeys = + s.keys !== null ? keys : new Set([...fieldKeys(fieldsOf)].filter((k) => k !== "*")); + let freezable = false; + const seen = new Set(); + const note = (r: string): void => { + if (!seen.has(r)) { + seen.add(r); + reasons.push(r); + } + }; + for (const o of ops) { + const viaTop = !atomsOf(analysis, o.recv).has(s.atom); + const where = `${locLabel(o.node)}${viaTop ? " via ⊤" : ""}`; + switch (o.kind) { + case "delete": + case "defineProperty": + case "setPrototypeOf": + case "seal": + case "proxy": + note(`${o.kind} at ${where}`); + break; + case "assign": + note(`Object.assign target at ${where}`); + break; + case "freeze": + freezable = true; + break; + case "computedWrite": + if (!viaTop) + note(s.kind === "array" ? `index write at ${where}` : `computed write at ${where}`); + break; + case "lengthWrite": + if (s.kind === "array") note(`length write at ${where}`); + break; + case "write": + if (s.kind === "object" && !viaTop && o.name !== null && !knownKeys.has(o.name)) + note(`adds key '${o.name}' at ${where}`); + break; + case "read": + if ( + s.kind === "object" && + !underPromiseGuard(o.node) && + escapes && + o.name !== null && + !viaTop && + !knownKeys.has(o.name) && + !OBJECT_PROTOTYPE_MEMBERS.has(o.name) + ) + note(`reads absent key '${o.name}' at ${where} (escapes)`); + break; + default: + break; + } + } + let brand = false; + if (s.kind === "object" && s.keys !== null) { + const o = orders.get([...s.keys].sort().join(",")); + if (o !== undefined && o.size > 1) brand = true; + } + const writableByTop: string[] = []; + if (escapes) { + if (analysis.topComputedWrites.length > 0) writableByTop.push("*"); + else for (const k of knownKeys) if (analysis.topWriteNames.has(k)) writableByTop.push(k); + } + const verdict: SiteVerdict["verdict"] = + reasons.length > 0 ? "G" : escapes ? "P-escaping" : "P-local"; + const label = s.atom.label; + if (escapes) reasons.push(`escapes: ${escapedWhy}`); + verdicts.push({ site: s, label, verdict, reasons, writableByTop, freezable, brand }); + } + + const hot = hotFunctions(analysis); + const hotNodes = new Set([...hot].map((f) => f.node)); + + // ---- print ---- + const pkgFns = [...analysis.functions.values()].filter((f) => f.inPackage); + lines.push( + `analyzed ${analysis.files.length} files (${analysis.packageFiles} package JS), ${analysis.functions.size} functions (${pkgFns.length} in packages), ${analysis.atoms.atoms.length} atoms, ${analysis.nodes.length} value nodes, ${analysis.steps} steps${analysis.budgetExceeded ? " — BUDGET EXCEEDED, results incomplete" : ""}`, + ); + const topNames = [...analysis.topWriteNames.entries()].map( + ([n, node]) => `${n} (${locLabel(node)})`, + ); + lines.push( + `census: writes on ⊤ receivers: ${topNames.length === 0 ? "none" : topNames.slice(0, 20).join(", ")}${topNames.length > 20 ? ` +${topNames.length - 20}` : ""}`, + ); + lines.push( + `census: computed writes on ⊤ receivers: ${analysis.topComputedWrites.length === 0 ? "none" : analysis.topComputedWrites.slice(0, 8).map(locLabel).join(", ")}`, + ); + const topReflect = topOps.filter( + (o) => + o.kind !== "read" && + o.kind !== "computedRead" && + o.kind !== "write" && + o.kind !== "computedWrite", + ); + lines.push( + `census: reflection on ⊤ receivers: ${topReflect.length === 0 ? "none" : topReflect.map((o) => `${o.kind} (${locLabel(o.node)})`).join(", ")}`, + ); + const escapedPkg = [...analysis.escaped.keys()].filter( + (a) => + a.node !== undefined && + wanted(a.node, a.node.getSourceFile().fileName.includes("/node_modules/")), + ); + lines.push( + `escaped atoms: ${analysis.escaped.size} (${escapedPkg.length} shown below where relevant)`, + ); + const firstEscapes = [...analysis.escaped.entries()] + .filter(([, why]) => !why.startsWith("via ")) + .slice(0, 12); + for (const [atom, why] of firstEscapes) lines.push(` escape root: ${atom.label}: ${why}`); + + const counts = { "P-local": 0, "P-escaping": 0, G: 0 }; + const hotCounts = { "P-local": 0, "P-escaping": 0, G: 0 }; + const inHot = (n: ts.Node): boolean => { + let p: ts.Node | undefined = n.parent; + while (p !== undefined) { + if (hotNodes.has(p)) return true; + p = p.parent; + } + return false; + }; + for (const v of verdicts) { + counts[v.verdict]++; + if (inHot(v.site.node)) hotCounts[v.verdict]++; + } + lines.push( + `sites: ${verdicts.length} (P local ${counts["P-local"]}, P escaping ${counts["P-escaping"]}, G ${counts.G}); in hot functions: P local ${hotCounts["P-local"]}, P escaping ${hotCounts["P-escaping"]}, G ${hotCounts.G}`, + ); + for (const v of verdicts) { + if (!wanted(v.site.node, v.site.inPackage)) continue; + if (!verbose && !inHot(v.site.node)) continue; + const extra: string[] = []; + if (v.brand) extra.push("needs brand (key order)"); + if (v.freezable) extra.push("freezable"); + if (v.writableByTop.length > 0) extra.push(`⊤-writable: ${v.writableByTop.join(",")}`); + const fields = analysis.fields.get(v.site.atom); + const ftypes = + v.site.kind === "array" + ? `elem: ${analysis.elements.has(v.site.atom) ? describeValue(analysis, analysis.elements.get(v.site.atom)!) : "∅"}` + : fieldEntries(fields) + .filter(([k]) => k !== "*") + .map(([k, id]) => `${k}: ${describeValue(analysis, id)}`) + .join("; "); + lines.push( + `site ${v.label}${inHot(v.site.node) ? " [hot]" : ""}: ${v.verdict}${v.reasons.length > 0 ? ` — ${v.reasons.join("; ")}` : ""}${extra.length > 0 ? ` [${extra.join("; ")}]` : ""}`, + ); + if (ftypes.length > 0) lines.push(` ${ftypes}`); + } + + // Class fields. + for (const atom of analysis.atoms.atoms) { + if (atom.kind !== "instance" || atom.node === undefined) continue; + const inPackage = atom.node.getSourceFile().fileName.includes("/node_modules/"); + if (!wanted(atom.node, inPackage)) continue; + const fm = analysis.fields.get(atom); + if (fm === undefined || fm.size === 0) continue; + const esc = analysis.escaped.get(atom); + const parts = [...fm.entries()].map(([k, id]) => { + const w = + esc !== undefined && + !k.startsWith("#") && + (analysis.topComputedWrites.length > 0 || analysis.topWriteNames.has(k)) + ? " (⊤-writable)" + : ""; + return `${k}: ${describeValue(analysis, id)}${w}`; + }); + lines.push( + `class ${atom.label}${esc !== undefined ? ` (escapes: ${esc})` : ""}: ${parts.join("; ")}`, + ); + } + + // Hot functions. + const hotPkg = [...hot].filter((info) => wanted(info.node, info.inPackage)); + lines.push(`hot functions: ${hotPkg.length} (clones per receiver class counted separately)`); + for (const info of hotPkg) { + const n = info.node; + const params = info.params.map((p, i) => `p${i}=${describeValue(analysis, p)}`).join(", "); + const flags = [ + info.usesArguments ? "uses arguments" : "", + analysis.escaped.size > 0 && [...analysis.escaped.keys()].some((a) => a.node === n) + ? "escapes" + : "", + ].filter((x) => x !== ""); + lines.push( + `fn ${info.label}${flags.length > 0 ? ` [${flags.join(", ")}]` : ""}: this=${describeValue(analysis, info.thisNode)}${params.length > 0 ? `, ${params}` : ""}, ret=${describeValue(analysis, info.ret)}`, + ); + } + const why = process.env["SCRIPTC_DEBUG_SHAPES_WHY"]; + if (why !== undefined) { + for (const info of analysis.functions.values()) { + if (!info.label.startsWith(why)) continue; + for (const [what, node] of [ + ["ret", info.ret], + ...info.params.map((p, i) => [`p${i}`, p] as const), + ] as const) { + if (!analysis.nodes[node]!.atoms.has(analysis.top)) continue; + lines.push(`why ⊤ in ${info.label} ${what}:`); + let cur: number | string | undefined = node; + for (let i = 0; i < 40 && typeof cur === "number"; i++) { + lines.push(` ${analysis.nodeLabel.get(cur) ?? `node ${cur}`}`); + cur = analysis.topCause.get(cur); + } + if (typeof cur === "string") lines.push(` seed: ${cur}`); + } + } + } + return { sites: verdicts, hot, lines }; +} + +/** A read inside the true arm of a promise test (`isAsync(x)`, + * `x instanceof Promise`): flow-sensitive narrowing removes the record + * arms there. Stage S1's locals are flow-insensitive, so the report + * recognizes the guard syntactically. */ +function underPromiseGuard(node: ts.Node): boolean { + let child: ts.Node = node; + let p = node.parent; + while (p !== undefined && !ts.isFunctionLike(p)) { + let cond: ts.Expression | undefined; + if (ts.isIfStatement(p) && p.thenStatement === child) cond = p.expression; + if (ts.isConditionalExpression(p) && p.whenTrue === child) cond = p.condition; + if (cond !== undefined && isPromiseTest(cond)) return true; + child = p; + p = p.parent; + } + return false; +} + +function isPromiseTest(e: ts.Expression): boolean { + if (ts.isParenthesizedExpression(e)) return isPromiseTest(e.expression); + if (ts.isBinaryExpression(e)) { + if (e.operatorToken.kind === ts.SyntaxKind.InstanceOfKeyword) + return ts.isIdentifier(e.right) && e.right.text === "Promise"; + if (e.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) + return isPromiseTest(e.left) || isPromiseTest(e.right); + return false; + } + if (ts.isCallExpression(e) && ts.isIdentifier(e.expression)) + return /^is(Async|Promise|Thenable)$/.test(e.expression.text); + return false; +} diff --git a/packages/compiler/src/frontend/tier-analysis.ts b/packages/compiler/src/frontend/tier-analysis.ts index b75e491a8..0baedd7dd 100644 --- a/packages/compiler/src/frontend/tier-analysis.ts +++ b/packages/compiler/src/frontend/tier-analysis.ts @@ -29,7 +29,12 @@ * Attribution is syntactic and approximate: the site's innermost node, the * operand it acts on, and that operand's declaration. Stage S6 replaces it * with requests the failing lowering names exactly. */ -import type { ScrDiagnostic } from "../diagnostics/diagnostic.js"; +import type { + ScrDiagnostic, + TypeFailureFamily, + TypeFailureSlot, +} from "../diagnostics/diagnostic.js"; +import { isWarningCode } from "../diagnostics/render.js"; import type { IrFunction, SrcLoc } from "../ir/ir.js"; import { dynamicOperationSites, type DynamicFunction } from "../ir/tiers.js"; import * as ts from "./ts7/adapter.js"; @@ -85,8 +90,70 @@ export interface TierDynamicFunction extends DynamicFunction { statements: number; } +/** The localized-failure stage whose slot positions include a slot kind + * (design §3.6): locals and globals degrade first, declarations next, + * members of declared types with push-down, expression types last. */ +export type LocalizedFailureStage = "L1" | "L2" | "L3" | "L4"; + +export function slotStage(slot: TypeFailureSlot): LocalizedFailureStage { + switch (slot) { + case "local": + case "global": + case "loop": + return "L1"; + case "field": + case "param": + case "return": + return "L2"; + case "member": + return "L3"; + case "expression": + return "L4"; + } +} + +/** A committed type failure that would become a checked dynamic slot + * (SC6104) instead of an error. */ +export interface WouldBeSlot { + loc: SrcLoc; + slot: TypeFailureSlot; + name: string; + declaredType: string; + family: TypeFailureFamily; + code: string; + message: string; + stage: LocalizedFailureStage; + /** The declaration the failure rejects today (a failure-group key), if + * any: a class, a signature, or a declaration statement. */ + rejects: string | null; + /** Other committed diagnostics that inherit from this slot's group and + * clear once every group they inherit from degrades (with push-down and + * expression slots, the full design). */ + cascade: number; +} + +/** Localized failure, report-only (stage L0): what the committed errors + * would become. Two scenarios: through stage L2 (declaration slots) and + * through L4 (members and expressions too). */ +export interface LocalizedFailureReport { + /** Reached errors (warnings excluded). */ + errors: number; + /** Type failures, by whether a checked dynamic slot keeps meaning. */ + typeFailures: { degradable: number; refused: number }; + slots: WouldBeSlot[]; + /** Diagnostics attributed to a rejected declaration they used. */ + cascades: number; + /** Errors that disappear (degraded slots plus cleared cascades). */ + clearedThroughL2: number; + clearedThroughL4: number; + /** The errors that remain through L4, by code. */ + remainingByCode: Record; +} + export interface TierReport { mode: TierMode; + /** Localized failure (report-only). */ + localizedFailure: LocalizedFailureReport; /** Reached source statements that perform a dynamic operation. */ statementsDynamic: number; dynamicFunctions: TierDynamicFunction[]; @@ -576,6 +643,119 @@ function demotedUnits(requests: readonly DemotionRequest[]): DemotedUnit[] { ); } +/* ── localized failure ───────────────────────────────────────────────── */ + +/** A diagnostic's failure-group bookkeeping (lowerer: badType, deferred + * flushes, blocked statements) turned into the would-be report. A group + * (a rejected declaration or blocked statement) clears in a scenario when + * each of its diagnostics either degrades (a degradable type failure at a + * slot the scenario covers) or is a cascade whose every cause clears. */ +export function localizedFailureReport(reached: readonly ScrDiagnostic[]): LocalizedFailureReport { + const errors = reached.filter((d) => !isWarningCode(d.code)); + const groups = new Map(); + for (const d of errors) { + if (d.rejects === undefined) continue; + const list = groups.get(d.rejects); + if (list === undefined) groups.set(d.rejects, [d]); + else list.push(d); + } + const stages: Record<"L2" | "L4", ReadonlySet> = { + L2: new Set(["L1", "L2"]), + L4: new Set(["L1", "L2", "L3", "L4"]), + }; + const clears = (scenario: "L2" | "L4") => { + const covered = stages[scenario]; + const memo = new Map(); + const degrades = (d: ScrDiagnostic): boolean => + d.typeFailure !== undefined && + d.typeFailure.degradable && + covered.has(slotStage(d.typeFailure.slot)); + const causeClears = (d: ScrDiagnostic): boolean => + d.cause !== undefined && d.cause.length > 0 && d.cause.every((g) => groupClears(g)); + const groupClears = (group: string): boolean => { + const known = memo.get(group); + if (known !== undefined) return known; + memo.set(group, false); // a cycle never clears + const members = groups.get(group) ?? []; + const ok = members.length > 0 && members.every((d) => degrades(d) || causeClears(d)); + memo.set(group, ok); + return ok; + }; + return { degrades, causeClears, groupClears }; + }; + const l2 = clears("L2"); + const l4 = clears("L4"); + const cleared = (c: ReturnType, d: ScrDiagnostic) => + c.degrades(d) || c.causeClears(d); + // Each slot's cascade: diagnostics whose causes (transitively) include + // the slot's group and that clear in the full scenario. + const reachesGroup = (d: ScrDiagnostic, group: string, seen: Set): boolean => { + for (const g of d.cause ?? []) { + if (g === group) return true; + if (seen.has(g)) continue; + seen.add(g); + for (const m of groups.get(g) ?? []) if (reachesGroup(m, group, seen)) return true; + } + return false; + }; + const slots: WouldBeSlot[] = []; + let degradable = 0; + let refused = 0; + for (const d of errors) { + const f = d.typeFailure; + if (f === undefined) continue; + if (!f.degradable) { + refused++; + continue; + } + degradable++; + const group = d.rejects ?? null; + let cascade = 0; + if (group !== null) + for (const e of errors) + if ( + e !== d && + e.typeFailure === undefined && + cleared(l4, e) && + reachesGroup(e, group, new Set()) + ) + cascade++; + slots.push({ + loc: d.loc, + slot: f.slot, + name: f.name, + declaredType: f.declaredType, + family: f.family, + code: d.code, + message: d.message, + stage: slotStage(f.slot), + rejects: group, + cascade, + }); + } + slots.sort( + (a, b) => + b.cascade - a.cascade || a.loc.file.localeCompare(b.loc.file) || a.loc.start - b.loc.start, + ); + const remainingByCode: Record = {}; + let clearedThroughL2 = 0; + let clearedThroughL4 = 0; + for (const d of errors) { + if (cleared(l2, d)) clearedThroughL2++; + if (cleared(l4, d)) clearedThroughL4++; + else remainingByCode[d.code] = (remainingByCode[d.code] ?? 0) + 1; + } + return { + errors: errors.length, + typeFailures: { degradable, refused }, + slots, + cascades: errors.filter((d) => d.cause !== undefined && d.typeFailure === undefined).length, + clearedThroughL2, + clearedThroughL4, + remainingByCode, + }; +} + export interface TierAnalysisInput { mode: TierMode; /** The dynamic-tier IR functions of the reached program. */ @@ -624,6 +804,7 @@ export function analyzeTiers(program: ts.Program, input: TierAnalysisInput): Tie const dynamic = dynamicFunctionReports(program, input.dynamicFunctions); return { mode: input.mode, + localizedFailure: localizedFailureReport(input.reached), statementsDynamic: dynamic.statements, dynamicFunctions: dynamic.functions, requests, diff --git a/packages/compiler/src/frontend/token-equivalence.test.ts b/packages/compiler/src/frontend/token-equivalence.test.ts index 880a07221..7bbe08929 100644 --- a/packages/compiler/src/frontend/token-equivalence.test.ts +++ b/packages/compiler/src/frontend/token-equivalence.test.ts @@ -1,8 +1,10 @@ import { expect, test } from "vitest"; -import { tokenEquivalentSource } from "./token-equivalence.js"; +import { tokenEquivalentSource, tokenPositionEquivalentSource } from "./token-equivalence.js"; const equivalent = (previous: string, current: string, path = "/src/a.ts") => tokenEquivalentSource(path, previous, current); +const positionEquivalent = (previous: string, current: string, path = "/src/a.ts") => + tokenPositionEquivalentSource(path, previous, current); test("whitespace and plain comments are trivia", () => { expect(equivalent("const a = 1;\n", "const a = 1; // one\n")).toBe(true); @@ -105,3 +107,50 @@ test("text the scanner cannot classify is never equivalent", () => { expect(equivalent("const a = ; // a\n", "const a = ; // b\n", "/src/a.tsx")).toBe(false); expect(equivalent('const s = "open\n', 'const s = "open // \n')).toBe(false); }); + +test("a / after a non-null assertion divides", () => { + expect( + equivalent("const q = data[i]! / 7; // ratio\n", "const q = data[i]! / 7; // the ratio\n"), + ).toBe(true); + expect( + equivalent("const q = value! / 7; // ratio\n", "const q = value! / 7; // the ratio\n"), + ).toBe(true); + expect(equivalent("const q = this! / 7; // ratio\n", "const q = this! / 7; // the ratio\n")).toBe( + true, + ); + // A prefix `!` negates a regular expression, whose `//` is code. + expect( + equivalent("const t = !/[//]/.test(s); c = 1;\n", "const t = !/[//]/.test(s); c = 2;\n"), + ).toBe(false); + expect(equivalent("return !/[//]/.test(s); // one\n", "return !/[//]/.test(s); // two\n")).toBe( + true, + ); + // After a line break, `!` starts a new expression. + expect(equivalent("a\n!/[//]/.test(s); c = 1;\n", "a\n!/[//]/.test(s); c = 2;\n")).toBe(false); + // `f()!` and `if (a) !` read the same up to the `/`. + expect(equivalent("const q = f()! / 2; // one\n", "const q = f()! / 2; // two\n")).toBe(false); +}); + +test("position equivalence also keeps every token's line and column", () => { + const source = "const a = 1; // one\nconst b = a; /* two */ f(b);\n"; + // A comment's text within its line, after the line's last token. + expect( + positionEquivalent(source, "const a = 1; // the first\nconst b = a; /* two */ f(b);\n"), + ).toBe(true); + // Comments and whitespace after the last token, and trailing whitespace. + expect(positionEquivalent(source, source + "// appended\n\n")).toBe(true); + expect( + positionEquivalent(source, "const a = 1; // one\nconst b = a; /* two */ f(b); \n"), + ).toBe(true); + expect(positionEquivalent(source, source.replaceAll("\n", "\r\n"))).toBe(true); + // A longer comment before a token moves its column; a new line moves the + // lines below it. Both stay token-equivalent. + const column = "const a = 1; // one\nconst b = a; /* the second */ f(b);\n"; + const line = "// header\nconst a = 1; // one\nconst b = a; /* two */ f(b);\n"; + expect(positionEquivalent(source, column)).toBe(false); + expect(positionEquivalent(source, line)).toBe(false); + expect(equivalent(source, column)).toBe(true); + expect(equivalent(source, line)).toBe(true); + // A token edit is never equivalent. + expect(positionEquivalent(source, source.replace("1", "2"))).toBe(false); +}); diff --git a/packages/compiler/src/frontend/token-equivalence.ts b/packages/compiler/src/frontend/token-equivalence.ts index 7a0f6cc79..9f6fd4613 100644 --- a/packages/compiler/src/frontend/token-equivalence.ts +++ b/packages/compiler/src/frontend/token-equivalence.ts @@ -17,7 +17,10 @@ * before it. After `)` it divides unless the parenthesis closes an `if`, * `while`, `for`, `for await`, or `with` header. After `}`, `++` or * `--` the scanner cannot tell which, so it gives up, as it does for - * escaped identifiers, which can spell keywords. + * escaped identifiers, which can spell keywords. After `!` in + * TypeScript it divides when the `!` follows an operand on the same line + * (a non-null assertion, `x! / 2`) and gives up when that operand is a + * `)`, `}`, `++` or `--`. * - A `/` with a line break (or a comment that spans lines) since the * previous token gives up: whether automatic semicolon insertion ends the * statement before it decides how it reads. @@ -35,6 +38,8 @@ interface Token { text: string; comment: boolean; lineBreakBefore: boolean; + /** The offset of the token's first code unit. */ + start: number; } /** Keywords after which `/` starts a regular expression. */ @@ -135,14 +140,20 @@ function scan(path: string, source: string): Token[] | null { // A line break, or a comment spanning lines, since the last token other // than a comment. let breakSinceCode = false; - // The previous token and the one before it, and whether a `)` closed a - // statement header. - const last: { token: string | null; before: string | null } = { token: null, before: null }; + // The previous token and the one before it, whether a line break came + // before the previous token, and whether a `)` closed a statement header. + const last: { token: string | null; before: string | null; broke: boolean } = { + token: null, + before: null, + broke: false, + }; let closedHeader = false; + // Every token ends at `index` when it is pushed. const push = (text: string, comment = false): void => { - tokens.push({ text, comment, lineBreakBefore }); + tokens.push({ text, comment, lineBreakBefore, start: index - text.length }); lineBreakBefore = false; if (!comment) { + last.broke = breakSinceCode; breakSinceCode = false; last.before = last.token; last.token = text; @@ -291,7 +302,19 @@ function scan(path: string, source: string): Token[] | null { else if (before.startsWith("'") || before.startsWith('"') || before.endsWith("`")) regex = false; else if (before.startsWith("/") && before.length > 1 && before !== "/=") regex = false; - else regex = true; + else if (before === "!" && typeScript && !last.broke && last.before !== null) { + // `x! / 2` divides: a `!` right after an operand on the same line is + // a non-null assertion. `!/re/.test(s)` negates a regular expression. + const operand = last.before; + if (operand === ")" || operand === "}" || operand === "++" || operand === "--") return null; + regex = !( + operand === "]" || + operand.startsWith("'") || + operand.startsWith('"') || + operand.endsWith("`") || + (/^[A-Za-z0-9_$]/.test(operand) && !EXPRESSION_KEYWORDS.has(operand)) + ); + } else regex = true; if (regex) { const start = index; index++; @@ -373,9 +396,33 @@ function scan(path: string, source: string): Token[] | null { return tokens; } -/** Whether `current` differs from `previous` only in whitespace and in - * comments that cannot affect compilation. */ -export function tokenEquivalentSource(path: string, previous: string, current: string): boolean { +/** The line and column of each token's start, with the line terminators + * source locations count (backend/source-locations.ts). */ +function tokenPositions(source: string, tokens: readonly Token[]): number[] { + const positions: number[] = []; + let line = 0; + let lineStart = 0; + let offset = 0; + for (const token of tokens) { + for (; offset < token.start; offset++) { + const code = source.charCodeAt(offset); + if (code === 13 && source.charCodeAt(offset + 1) === 10) offset++; + if (code === 10 || code === 13 || code === 0x2028 || code === 0x2029) { + line++; + lineStart = offset + 1; + } + } + positions.push(line, token.start - lineStart); + } + return positions; +} + +function equivalentSources( + path: string, + previous: string, + current: string, + positions: boolean, +): boolean { if (previous === current) return true; const left = scan(path, previous); if (left === null) return false; @@ -387,5 +434,27 @@ export function tokenEquivalentSource(path: string, previous: string, current: s if (a.text !== b.text || a.comment !== b.comment) return false; if (index > 0 && a.lineBreakBefore !== b.lineBreakBefore) return false; } - return true; + if (!positions) return true; + const before = tokenPositions(previous, left); + const after = tokenPositions(current, right); + return before.every((value, index) => value === after[index]); +} + +/** Whether `current` differs from `previous` only in whitespace and in + * comments that cannot affect compilation. */ +export function tokenEquivalentSource(path: string, previous: string, current: string): boolean { + return equivalentSources(path, previous, current, false); +} + +/** Whether `current` is token-equivalent to `previous` (tokenEquivalentSource) + * and every token keeps its line and column: the edit leaves the source + * positions debug information records unchanged (a comment's text within + * its line, comments and whitespace after the last token, trailing + * whitespace). */ +export function tokenPositionEquivalentSource( + path: string, + previous: string, + current: string, +): boolean { + return equivalentSources(path, previous, current, true); } diff --git a/packages/compiler/src/frontend/ts7/ast-node.test.ts b/packages/compiler/src/frontend/ts7/ast-node.test.ts index 8b391e622..b650e7725 100644 --- a/packages/compiler/src/frontend/ts7/ast-node.test.ts +++ b/packages/compiler/src/frontend/ts7/ast-node.test.ts @@ -473,6 +473,29 @@ test("named child access shares node and list identities without materializing s expect(() => file.namedChild(another, "name")).toThrow("another source file"); }); +test("named child getters and text keep answering the first read", () => { + let materialized = 0; + const file = new AstFile(decoded.get("main.ts")!.bytes, () => { + materialized++; + }); + let index = 1; + while (file.wire.kind(index) !== AstKind.FunctionDeclaration) index++; + const node = file.node(index); + const name = node.name; + expect(name?.index).toBe(file.wire.namedChild(index, "name")); + const after = materialized; + expect(node.name).toBe(name); + expect(node.childNode("name")).toBe(name); + expect(file.node(name!.index)).toBe(name); + expect(node.typeParameters).toBeUndefined(); + expect(materialized).toBe(after); + const text = name!.text; + expect(text).toBe(file.wire.text(name!.index)); + expect(name!.text).toBe(text); + expect(node.text).toBeUndefined(); + expect(node.text).toBeUndefined(); +}); + test("warming a named child preserves lazy failures in later malformed sibling links", () => { const bytes = decoded.get("main.ts")!.bytes; const wire = new AstFile(bytes).wire; diff --git a/packages/compiler/src/frontend/ts7/ast-node.ts b/packages/compiler/src/frontend/ts7/ast-node.ts index 4c7726c00..7120b90d4 100644 --- a/packages/compiler/src/frontend/ts7/ast-node.ts +++ b/packages/compiler/src/frontend/ts7/ast-node.ts @@ -83,6 +83,9 @@ export interface AstFileOptions { writeTargets?: boolean; } +const NO_CHILD_INDICES = new Uint32Array(0); +const NO_CHILD_NODES: AstNode[] = []; + export class AstFile { readonly wire: AstWireFile; readonly root: AstNode; @@ -94,7 +97,9 @@ export class AstFile { private readonly structuredNodes = new Map(); private readonly strings = new Map(); private lines: number[] | undefined; - private childIndices: Uint32Array | undefined; + /** Resolved named children, eight slots per node; empty until the first + * named child is read. */ + private childIndices: Uint32Array = NO_CHILD_INDICES; /** The root source file's name (AstNode.fileName). */ private rootFileName: string | undefined; @@ -164,8 +169,8 @@ export class AstFile { const mask = node.data >>> 30 === 0 ? node.data & 0xff : 0xff; if ((mask & (1 << order)) === 0) return 0; const slot = node.index * 8 + order; - const indices = this.childIndices; - if (indices !== undefined) { + let indices = this.childIndices; + if (indices.length !== 0) { const cached = indices[slot]!; if (cached !== 0) return cached; } @@ -173,9 +178,11 @@ export class AstFile { // The pinned wire has at most eight named children per node. One packed // table avoids per-node arrays; allocate it only after successful access // so malformed siblings still fail lazily in the checked wire decoder. - if (this.childIndices === undefined) - this.childIndices = new Uint32Array(this.wire.nodeCount * 8); - this.childIndices[slot] = index; + if (indices.length === 0) { + indices = new Uint32Array(this.wire.nodeCount * 8); + this.childIndices = indices; + } + indices[slot] = index; return index; } @@ -187,7 +194,7 @@ export class AstFile { if (parsed.path !== this.root.path) throw new AstDecodeError("node handle belongs to another source file"); const node = this.node(parsed.index); - if (node.kind !== parsed.kind) + if (node.kind !== this.wire.kindFromWire(parsed.kind)) throw new AstDecodeError("node handle kind does not match the response"); return node; } @@ -320,6 +327,13 @@ export class AstNode { readonly data: number; private parentResolved = false; private parentCache: AstNode | undefined; + /** The node's text, read once: the frontend asks the same identifiers + * for their text many times. */ + private textResolved = false; + private textCache: string | undefined; + /** Named child nodes by wire slot, filled as they are read; empty until + * the first one is. Slots the child mask leaves empty are never stored. */ + private childNodes: AstNode[] = NO_CHILD_NODES; constructor( readonly file: AstFile, @@ -336,7 +350,7 @@ export class AstNode { } get id(): string { - return `${this.index}.${this.kind}.${this.file.root.path}`; + return `${this.index}.${this.file.wire.kindOnWire(this.kind)}.${this.file.root.path}`; } get parent(): AstNode | undefined { if (!this.parentResolved) { @@ -351,7 +365,11 @@ export class AstNode { return this.parentCache; } get text(): string | undefined { - return this.file.wire.text(this.index); + if (!this.textResolved) { + this.textCache = this.file.wire.text(this.index); + this.textResolved = true; + } + return this.textCache; } get rawText(): string | undefined { return this.file.wire.rawText(this.index); @@ -363,8 +381,21 @@ export class AstNode { } /** childNode by wire slot, as the generated getters select it. */ childNodeAt(order: number): AstNode | undefined { + let nodes = this.childNodes; + if (order >= 0 && order < nodes.length) { + const cached = nodes[order]; + if (cached !== undefined) return cached; + } const index = this.file.namedChildAt(this, order); - return index === 0 ? undefined : this.file.node(index); + if (index === 0) return undefined; + const child = this.file.node(index); + if (nodes.length === 0) { + const mask = this.data >>> 30 === 0 ? this.data & 0xff : 0xff; + nodes = new Array(32 - Math.clz32(mask)); + this.childNodes = nodes; + } + nodes[order] = child; + return child; } childListAt(order: number): AstNodeList | undefined { const index = this.file.namedChildAt(this, order); diff --git a/packages/compiler/src/frontend/ts7/ast-wire-protocols.test.ts b/packages/compiler/src/frontend/ts7/ast-wire-protocols.test.ts new file mode 100644 index 000000000..36aca02dd --- /dev/null +++ b/packages/compiler/src/frontend/ts7/ast-wire-protocols.test.ts @@ -0,0 +1,132 @@ +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { expect, test } from "vitest"; +import { AstFile } from "./ast-node.js"; +import { AstWireFile, PROTOCOL_VERSION_7_1 } from "./ast-wire.js"; +import { + AstKind, + HEADER_OFFSET_NODES, + KIND_NODE_LIST, + NODE_LEN, + NODE_OFFSET_DATA, + NODE_OFFSET_KIND, + PROTOCOL_VERSION, +} from "./ast-schema.generated.js"; + +/** The same sources' getSourceFile responses from tsc 7.0.2 (protocol 5) + * and tsc 7.1.0-beta (protocol 9). */ +const fixtures = JSON.parse( + readFileSync( + fileURLToPath(new URL("../../../test/fixtures/ts7-protocols.json", import.meta.url)), + "utf8", + ), +) as { + sources: Record; + protocol5: Record; + protocol9: Record; +}; +const names = Object.keys(fixtures.sources); +const bytes = (protocol: "protocol5" | "protocol9", name: string): Uint8Array => + new Uint8Array(Buffer.from(fixtures[protocol][name]!, "base64")); + +/** Every node's fields as TypeScript 7.0 sees them. A node list's flags word + * is left out: protocol 9 stores whether the list has a trailing comma there, + * which readers take from the source text. */ +function nodes(wire: AstWireFile): string[] { + const result: string[] = []; + for (let index = 1; index < wire.nodeCount; index++) { + const kind = wire.kind(index); + const list = kind === KIND_NODE_LIST; + result.push( + [ + kind, + wire.pos(index), + wire.end(index), + list ? 0 : wire.flags(index), + list || wire.data(index) >>> 30 !== 2 ? wire.data(index) : wire.data(index) >>> 24, + wire.parent(index), + wire.next(index), + list ? "" : (wire.text(index) ?? ""), + list ? "" : (wire.rawText(index) ?? ""), + ].join(" "), + ); + } + return result; +} + +test("protocol 5 and protocol 9 responses decode to the same trees", () => { + for (const name of names) { + const old = new AstWireFile(bytes("protocol5", name)); + const next = new AstWireFile(bytes("protocol9", name)); + expect(old.protocol).toBe(PROTOCOL_VERSION); + expect(next.protocol).toBe(PROTOCOL_VERSION_7_1); + expect(nodes(next), name).toEqual(nodes(old)); + expect(next.preorder(), name).toEqual(old.preorder()); + const oldFile = new AstFile(bytes("protocol5", name)).sourceFile; + const nextFile = new AstFile(bytes("protocol9", name)).sourceFile; + expect(nextFile.fileName).toBe(oldFile.fileName); + expect(nextFile.text).toBe(fixtures.sources[name]); + expect(nextFile.languageVariant).toBe(oldFile.languageVariant); + expect(nextFile.statements.nodes.length).toBe(oldFile.statements.nodes.length); + } +}); + +test("protocol 9 node handles name TypeScript 7.1's kinds", () => { + const file = new AstFile(bytes("protocol9", "main.ts")); + const wire = file.wire; + let index = 1; + while (wire.kind(index) !== AstKind.FunctionDeclaration) index++; + const node = file.node(index); + expect(node.kind).toBe(AstKind.FunctionDeclaration); + expect(node.id).toBe(`${index}.${AstKind.FunctionDeclaration + 1}.${file.root.path}`); + expect(file.resolve(node.id)).toBe(node); + expect(() => file.resolve(`${index}.${AstKind.FunctionDeclaration}.${file.root.path}`)).toThrow( + "node handle kind does not match the response", + ); + // Tokens below SourceKeyword keep their numbers. + expect(wire.kindOnWire(AstKind.Identifier)).toBe(AstKind.Identifier); + expect(wire.kindFromWire(167)).toBe(-1); + const old = new AstFile(bytes("protocol5", "main.ts")); + expect(old.node(index).id).toBe(`${index}.${AstKind.FunctionDeclaration}.${old.root.path}`); +}); + +/** A copy of `name`'s protocol 9 response with the first node of `kind` + * (TypeScript 7.0's) changed by `change`. */ +function changed( + name: string, + kind: number, + change: (view: DataView, at: number) => void, +): Uint8Array { + const data = bytes("protocol9", name); + const view = new DataView(data.buffer, data.byteOffset, data.byteLength); + const nodes = view.getUint32(HEADER_OFFSET_NODES, true); + const onWire = kind >= 167 ? kind + 1 : kind; + for (let at = nodes + NODE_LEN; at < data.length; at += NODE_LEN) { + if (view.getUint32(at + NODE_OFFSET_KIND, true) !== onWire) continue; + change(view, at); + return data; + } + throw new Error(`no node of kind ${kind}`); +} + +test("protocol 9 nodes that TypeScript 7.0 has no form for are rejected", () => { + const sourceKeyword = changed("main.ts", AstKind.Identifier, (view, at) => + view.setUint32(at + NODE_OFFSET_KIND, 167, true), + ); + expect(() => new AstWireFile(sourceKeyword)).toThrow("SourceKeyword"); + const attributes = changed("main.ts", AstKind.ModuleDeclaration, (view, at) => + view.setUint32(at + NODE_OFFSET_DATA, view.getUint32(at + NODE_OFFSET_DATA, true) | 4, true), + ); + expect(() => new AstWireFile(attributes)).toThrow("import attributes on a module declaration"); + const source = changed("main.ts", AstKind.ImportClause, (view, at) => + view.setUint32( + at + NODE_OFFSET_DATA, + (view.getUint32(at + NODE_OFFSET_DATA, true) | (3 << 24)) >>> 0, + true, + ), + ); + expect(() => new AstWireFile(source)).toThrow("`import source`"); + const version = bytes("protocol9", "main.ts"); + version[3] = 6; + expect(() => new AstWireFile(version)).toThrow("unsupported protocol version 6"); +}); diff --git a/packages/compiler/src/frontend/ts7/ast-wire.ts b/packages/compiler/src/frontend/ts7/ast-wire.ts index 6b2222eae..4cba5923a 100644 --- a/packages/compiler/src/frontend/ts7/ast-wire.ts +++ b/packages/compiler/src/frontend/ts7/ast-wire.ts @@ -123,6 +123,44 @@ function astWords(bytes: Uint8Array, offset: number, count: number): Uint32Array return words; } +/** TypeScript 7.1's encoding, which this reader accepts beside + * PROTOCOL_VERSION (TypeScript 7.0's). Its header is 64 bytes: after the + * section offsets come the source file ID and lease ID (u64 each) and the + * binder data offset. Its node kinds count 7.1's SourceKeyword (167), so each + * kind above it is one more than 7.0's; the reader returns 7.0's kinds. A + * module declaration's children gain `attributes` in slot 2, which moves + * `body` to slot 3, and an import clause's phase gains `source` (3). */ +export const PROTOCOL_VERSION_7_1 = 9; +const HEADER_SIZE_7_1 = 64; +/** SourceKeyword in TypeScript 7.1's kinds. */ +const SOURCE_KEYWORD_7_1 = 167; + +/** The node table of a protocol 7.1 response in TypeScript 7.0's terms: each + * kind above SourceKeyword one lower, and each module declaration's child + * mask without the attributes slot. Throws for the nodes 7.0 has no form + * for: a SourceKeyword node, a module declaration with import attributes, + * and an `import source` clause. */ +function wordsAs70(words: Uint32Array, count: number): Uint32Array { + const result = new Uint32Array(words); + for (let index = 1; index < count; index++) { + const word = index * NODE_WORDS; + let kind = result[word + WORD_KIND]!; + if (kind === KIND_NODE_LIST) continue; + if (kind === SOURCE_KEYWORD_7_1) + throw new AstDecodeError("TypeScript 7.1's SourceKeyword node is not supported"); + if (kind > SOURCE_KEYWORD_7_1) result[word + WORD_KIND] = --kind; + const data = result[word + WORD_DATA]!; + if (kind === AstKind.ModuleDeclaration && data >>> 30 === 0) { + if ((data & 4) !== 0) + throw new AstDecodeError("import attributes on a module declaration are not supported"); + result[word + WORD_DATA] = ((data & ~0xff) | (data & 3) | ((data & 8) >>> 1)) >>> 0; + } else if (kind === AstKind.ImportClause && ((data >>> 24) & 3) === 3) { + throw new AstDecodeError("`import source` is not supported"); + } + } + return result; +} + function hexWord(value: number): string { const digits = "0123456789abcdef"; let result = ""; @@ -139,6 +177,8 @@ export class AstWireFile { readonly stringCount: number; readonly contentHash: string; readonly parseOptionsKey: string; + /** The response's protocol: PROTOCOL_VERSION or PROTOCOL_VERSION_7_1. */ + readonly protocol: number; private readonly strings: number; private readonly extended: number; private readonly structured: number; @@ -155,15 +195,19 @@ export class AstWireFile { constructor(private readonly bytes: Uint8Array) { astBounds(bytes, 0, HEADER_SIZE); const metadata = astU32(bytes, HEADER_OFFSET_METADATA); - if (metadata >>> 24 !== PROTOCOL_VERSION) - throw new AstDecodeError(`unsupported protocol version ${metadata >>> 24}`); + const protocol = metadata >>> 24; + if (protocol !== PROTOCOL_VERSION && protocol !== PROTOCOL_VERSION_7_1) + throw new AstDecodeError(`unsupported protocol version ${protocol}`); + this.protocol = protocol; + const headerSize = protocol === PROTOCOL_VERSION_7_1 ? HEADER_SIZE_7_1 : HEADER_SIZE; + astBounds(bytes, 0, headerSize); const stringOffsets = astU32(bytes, HEADER_OFFSET_STRING_TABLE_OFFSETS); this.strings = astU32(bytes, HEADER_OFFSET_STRING_TABLE); this.extended = astU32(bytes, HEADER_OFFSET_EXTENDED_DATA); this.structured = astU32(bytes, HEADER_OFFSET_STRUCTURED_DATA); this.nodes = astU32(bytes, HEADER_OFFSET_NODES); if ( - stringOffsets < HEADER_SIZE || + stringOffsets < headerSize || this.strings < stringOffsets || this.extended < this.strings || this.structured < this.extended || @@ -177,7 +221,8 @@ export class AstWireFile { } this.nodeCount = (bytes.length - this.nodes) / NODE_LEN; this.stringCount = (this.strings - stringOffsets) / 8; - this.words = astWords(bytes, this.nodes, this.nodeCount * NODE_WORDS); + const words = astWords(bytes, this.nodes, this.nodeCount * NODE_WORDS); + this.words = protocol === PROTOCOL_VERSION_7_1 ? wordsAs70(words, this.nodeCount) : words; this.stringWords = astWords(bytes, stringOffsets, this.stringCount * 2); this.stringCache = new Array(this.stringCount).fill(undefined); if (this.nodeCount < 2) throw new AstDecodeError("missing root"); @@ -200,6 +245,19 @@ export class AstWireFile { } } + /** `kind` (TypeScript 7.0's) as this response's protocol numbers it, the + * kind a node handle names. */ + kindOnWire(kind: number): number { + return this.protocol === PROTOCOL_VERSION_7_1 && kind >= SOURCE_KEYWORD_7_1 ? kind + 1 : kind; + } + + /** A kind as this response's protocol numbers it (a node handle's) in + * TypeScript 7.0's numbering; -1 for 7.1's SourceKeyword. */ + kindFromWire(kind: number): number { + if (this.protocol !== PROTOCOL_VERSION_7_1 || kind < SOURCE_KEYWORD_7_1) return kind; + return kind === SOURCE_KEYWORD_7_1 ? -1 : kind - 1; + } + /** The first word of node `index`. `index >>> 0 === index` accepts exactly * the integers in [0, 2^32), so with the bound it accepts the same indices * as an integer and range check. */ diff --git a/packages/compiler/src/frontend/ts7/checker-features.test.ts b/packages/compiler/src/frontend/ts7/checker-features.test.ts new file mode 100644 index 000000000..fcd3576cb --- /dev/null +++ b/packages/compiler/src/frontend/ts7/checker-features.test.ts @@ -0,0 +1,45 @@ +import { chmodSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterAll, afterEach, expect, test } from "vitest"; +import { checkerReadsLibrariesFromDisk, setCheckerFeatureStore } from "./checker-features.js"; + +const dir = mkdtempSync(join(tmpdir(), "scriptc-checker-features-")); +afterAll(() => rmSync(dir, { recursive: true, force: true })); +afterEach(() => setCheckerFeatureStore(null)); + +/** A stand-in checker that records its arguments and exits with `status`. */ +function checker(name: string, status: number): { path: string; log: string } { + const path = join(dir, name); + const log = path + ".log"; + writeFileSync(path, `#!/bin/sh\necho "$@" >> '${log}'\nexit ${status}\n`); + chmodSync(path, 0o755); + return { path, log }; +} + +test.skipIf(process.platform === "win32")( + "the checker is asked once whether it reads its library files from the disk, and the answer is stored", + async () => { + const { readFileSync, existsSync } = await import("node:fs"); + const entries = new Map(); + setCheckerFeatureStore({ + read: (key) => entries.get(key) ?? null, + write: (key, text) => void entries.set(key, text), + }); + const current = checker("current", 0); + const released = checker("released", 2); + expect(checkerReadsLibrariesFromDisk(current.path, {})).toBe(true); + expect(checkerReadsLibrariesFromDisk(released.path, {})).toBe(false); + expect(readFileSync(current.log, "utf8")).toBe("--api --default-libraries-from-disk\n"); + expect([...entries.values()].sort()).toEqual(["0", "1"]); + // Later asks use the answers: the checkers do not run again. + expect(checkerReadsLibrariesFromDisk(current.path, {})).toBe(true); + expect(checkerReadsLibrariesFromDisk(released.path, {})).toBe(false); + expect(readFileSync(current.log, "utf8").split("\n").length).toBe(2); + expect(readFileSync(released.log, "utf8").split("\n").length).toBe(2); + // tsgo is never asked. + const tsgo = checker("tsgo", 0); + expect(checkerReadsLibrariesFromDisk(tsgo.path, { SCRIPTC_CHECKER: "tsgo" })).toBe(false); + expect(existsSync(tsgo.log)).toBe(false); + }, +); diff --git a/packages/compiler/src/frontend/ts7/checker-features.ts b/packages/compiler/src/frontend/ts7/checker-features.ts new file mode 100644 index 000000000..b80a54994 --- /dev/null +++ b/packages/compiler/src/frontend/ts7/checker-features.ts @@ -0,0 +1,53 @@ +import { spawnSync } from "node:child_process"; +import { createHash } from "node:crypto"; +import { checkerBackend } from "./checker-backend.js"; +import { checkerExecutableIdentity } from "./lib-diagnostics.js"; + +/** Text entries kept between builds (the native compiler's cache). */ +export interface CheckerFeatureStore { + read(key: string): string | null; + write(key: string, text: string): void; +} + +let featureStore: CheckerFeatureStore | null = null; +const answers = new Map(); + +/** Keeps the answers of checkerReadsLibrariesFromDisk between builds. */ +export function setCheckerFeatureStore(store: CheckerFeatureStore | null): void { + featureStore = store; +} + +/** + * Whether the tsc-ts at `executable` takes `--api --default-libraries-from-disk`, which has it read the files + * of its default library directory from the disk instead of through the file system callbacks. tsc-ts releases + * before it reject the flag, so it is asked once per executable identity: the stored answer, or else + * `tsc-ts --api --default-libraries-from-disk` with an empty input, which exits with 0 when the flag is known + * and with a usage error when it is not. tsgo is never asked. + */ +export function checkerReadsLibrariesFromDisk( + executable: string, + env: NodeJS.ProcessEnv = process.env, +): boolean { + if (checkerBackend(env) !== "tsc-ts") return false; + const key = createHash("sha256") + .update(JSON.stringify(["default-libraries-from-disk", checkerExecutableIdentity(executable)])) + .digest("hex"); + const known = answers.get(key); + if (known !== undefined) return known; + const stored = featureStore?.read(key) ?? null; + let answer: boolean; + if (stored === "1" || stored === "0") answer = stored === "1"; + else { + const probe = spawnSync(executable, ["--api", "--default-libraries-from-disk"], { + encoding: "utf8", + stdio: "pipe", + timeout: 10_000, + }); + answer = probe.error === undefined && probe.status === 0; + // A probe that could not run says nothing about the flag: ask again next time. + if (probe.error === undefined && probe.status !== null) + featureStore?.write(key, answer ? "1" : "0"); + } + answers.set(key, answer); + return answer; +} diff --git a/packages/compiler/src/frontend/ts7/checker-server.test.ts b/packages/compiler/src/frontend/ts7/checker-server.test.ts new file mode 100644 index 000000000..9a0371fe6 --- /dev/null +++ b/packages/compiler/src/frontend/ts7/checker-server.test.ts @@ -0,0 +1,47 @@ +import { mkdtempSync, rmSync, utimesSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterAll, expect, test } from "vitest"; +import { checkerServerName } from "./checker-server.js"; + +const dir = mkdtempSync(join(tmpdir(), "scriptc-checker-server-")); +const executable = join(dir, "tsc-ts"); +writeFileSync(executable, "server"); +const enabled = { SCRIPTC_CHECKER_SERVER: "1" }; +const posix = process.platform === "linux" || process.platform === "darwin"; + +afterAll(() => rmSync(dir, { recursive: true, force: true })); + +test("builds use the shared checker server only when they opt in, on Linux and macOS, with tsc-ts", () => { + expect(checkerServerName(executable, dir, false, {})).toBeNull(); + expect(checkerServerName(executable, dir, false, { SCRIPTC_CHECKER_SERVER: "0" })).toBeNull(); + expect( + checkerServerName(executable, dir, false, { ...enabled, SCRIPTC_CHECKER: "tsgo" }), + ).toBeNull(); + const name = checkerServerName(executable, dir, false, enabled); + if (!posix) expect(name).toBeNull(); + else expect(name).toMatch(/^[0-9a-f]{32}$/); +}); + +test.skipIf(!posix)( + "the server's name changes with what its answers depend on, and with nothing else", + () => { + const name = checkerServerName(executable, dir, false, enabled); + expect( + checkerServerName(executable, dir, false, { ...enabled, SCRIPTC_CHECKER: "tsc-ts" }), + ).toBe(name); + expect(checkerServerName(executable, dir, false, { ...enabled, PATH: "/elsewhere" })).toBe( + name, + ); + const others = [ + checkerServerName(executable, join(dir, "other"), false, enabled), + checkerServerName(executable, dir, true, enabled), + checkerServerName(executable, dir, false, { ...enabled, TSC_TS_LIB_DIR: dir }), + checkerServerName(executable, dir, false, { ...enabled, TSC_TS_LISTEN_MEMORY: "512" }), + ]; + for (const other of others) expect(other).not.toBe(name); + // An updated tsc-ts executable gets its own server. + utimesSync(executable, new Date(0), new Date(0)); + expect(checkerServerName(executable, dir, false, enabled)).not.toBe(name); + }, +); diff --git a/packages/compiler/src/frontend/ts7/checker-server.ts b/packages/compiler/src/frontend/ts7/checker-server.ts new file mode 100644 index 000000000..94c80f8db --- /dev/null +++ b/packages/compiler/src/frontend/ts7/checker-server.ts @@ -0,0 +1,53 @@ +import { createHash } from "node:crypto"; +import { checkerBackend } from "./checker-backend.js"; +import { checkerExecutableIdentity } from "./lib-diagnostics.js"; +import { TS7_FILE_SYSTEM_CALLBACKS } from "./rpc-filesystem.js"; + +/** How long a shared checker server waits for the next build before it + * exits, in seconds. */ +export const CHECKER_SERVER_IDLE_EXIT = 300; + +/** The resident memory, in megabytes, above which a shared checker server + * exits after a build. tsc-ts never frees a program's syntax trees, types and + * symbols, so its memory grows with each build it serves. */ +export const CHECKER_SERVER_MEMORY = 2048; + +/** + * With `SCRIPTC_CHECKER_SERVER=1` on Linux and macOS, the native compiler + * checks with a tsc-ts server shared by the builds that follow + * (`tsc-ts --api --listen`): a build connects to it, starting it when none + * answers, and the server keeps the files it parsed for the next build. + * Returns the server's name, a digest of everything a server's answers + * depend on besides the files it reads through the build: the tsc-ts + * executable, the working directory, the file system callbacks, timing, + * whether it reads its library files from the disk, and the `TSC_TS_*` + * environment. Returns null when the shared server is not used. + */ +export function checkerServerName( + executable: string, + cwd: string, + timing: boolean, + env: NodeJS.ProcessEnv = process.env, + librariesFromDisk = false, +): string | null { + if (env["SCRIPTC_CHECKER_SERVER"] !== "1") return null; + if (process.platform !== "linux" && process.platform !== "darwin") return null; + if (checkerBackend(env) !== "tsc-ts") return null; + const tscTsEnv = Object.keys(env) + .filter((name) => name.startsWith("TSC_TS_")) + .sort() + .map((name) => [name, env[name] ?? ""]); + return createHash("sha256") + .update( + JSON.stringify([ + checkerExecutableIdentity(executable), + cwd, + TS7_FILE_SYSTEM_CALLBACKS, + timing, + librariesFromDisk, + tscTsEnv, + ]), + ) + .digest("hex") + .slice(0, 32); +} diff --git a/packages/compiler/src/frontend/ts7/lib-diagnostics.test.ts b/packages/compiler/src/frontend/ts7/lib-diagnostics.test.ts new file mode 100644 index 000000000..6d0c0651d --- /dev/null +++ b/packages/compiler/src/frontend/ts7/lib-diagnostics.test.ts @@ -0,0 +1,384 @@ +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterAll, afterEach, expect, test } from "vitest"; +import type { SourceFile } from "./ast-types.js"; +import { + prebuiltLibDiagnosticsText, + readPrebuiltLibDiagnostics, + recordPrebuiltLibDiagnostics, + reusedKeptLibDiagnostics, + reusedLibDiagnostics, + setLibDiagnosticsStore, + type LibDiagnosticsStore, + type PrebuiltLibDiagnostics, +} from "./lib-diagnostics.js"; +import type { Ts7DiagnosticData } from "./session-schema.generated.js"; +import type { Ts7CheckedFileDiagnosticData, Ts7SessionProgram } from "./session.js"; +import { closeSourceParser, parseSourceFile } from "./source-parser-node.js"; + +const dir = mkdtempSync(join(tmpdir(), "scriptc-lib-diagnostics-")); +const lib = join(dir, "lib.es5.d.ts"); +const fallback = join(dir, "scriptc-node-fallback.d.ts"); +const libText = "declare var NaN: number;\ninterface Array { length: number }\n"; +const fallbackText = "export {};\ndeclare global {\n var process: { argv: string[] };\n}\n"; +writeFileSync(lib, libText); +writeFileSync(fallback, fallbackText); +const main = join(dir, "main.ts"); +const util = join(dir, "\u{1F600}.ts"); +const other = join(dir, "�.ts"); + +afterEach(() => { + setLibDiagnosticsStore(null); + recordPrebuiltLibDiagnostics(null); +}); +afterAll(() => { + closeSourceParser(); + rmSync(dir, { recursive: true, force: true }); +}); + +function memoryStore(): LibDiagnosticsStore & { entries: Map } { + const entries = new Map(); + return { + entries, + read: (key) => entries.get(key) ?? null, + write: (key, text) => void entries.set(key, text), + }; +} + +function diagnostic(fileName: string | undefined, pos: number, code = 2322): Ts7DiagnosticData { + return { fileName, pos, end: pos + 1, code, category: 1, text: `at ${pos}` }; +} + +/** A session program whose full-check and checked-file answers are given; it + * records which requests it was asked. */ +function sessionProgram(answers: { + semantic: Map; + global?: Ts7DiagnosticData[]; + checked?: boolean; +}): Ts7SessionProgram & { asked: string[] } { + const asked: string[] = []; + const all = [...answers.semantic.values()].flat(); + return { + asked, + getCompilerOptions: () => ({ strict: true }), + diagnosticData: (method: string, file?: string) => { + asked.push(file === undefined ? method : `${method} ${file}`); + if (method === "getGlobalDiagnostics") return answers.global ?? []; + return file === undefined ? all : (answers.semantic.get(file) ?? []); + }, + checkedFileDiagnosticData: (files: readonly string[]): Ts7CheckedFileDiagnosticData | null => { + asked.push(`checked ${files.join(",")}`); + if (answers.checked === false) return null; + return { files: files.map((file) => answers.semantic.get(file) ?? []), global: [] }; + }, + } as unknown as Ts7SessionProgram & { asked: string[] }; +} + +/** The program's files, parsed: the supplied two and the given others. */ +function files(texts: Record): (name: string) => SourceFile | undefined { + const parsed = new Map([ + [lib, parseSourceFile(lib, libText, "ts")], + [fallback, parseSourceFile(fallback, fallbackText, "ts")], + ]); + for (const [name, text] of Object.entries(texts)) + parsed.set(name, parseSourceFile(name, text, "ts")); + return (name) => parsed.get(name); +} + +const names = [lib, fallback, main]; +const supplied = (name: string): boolean => name === lib || name === fallback; +const moduleFile = files({ [main]: "export const n: number = 'x';\n" }); + +test("without a store the full check answers", () => { + const program = sessionProgram({ semantic: new Map() }); + expect(reusedLibDiagnostics(program, names, supplied, moduleFile)).toBeNull(); + expect(program.asked).toEqual([]); +}); + +test("a stored answer stands in for checking the supplied files, merged by file name", () => { + const store = memoryStore(); + setLibDiagnosticsStore(store, "checker"); + const semantic = new Map([ + [lib, [diagnostic(lib, 1)]], + [fallback, []], + [main, [diagnostic(main, 5), diagnostic(main, 9)]], + ]); + const first = sessionProgram({ semantic }); + const full = reusedLibDiagnostics(first, names, supplied, moduleFile); + // The first build runs the full check and stores the supplied files' answers. + expect(first.asked).toEqual([ + "getGlobalDiagnostics", + "getSemanticDiagnostics", + "checked ", + `getSemanticDiagnostics ${lib}`, + `getSemanticDiagnostics ${fallback}`, + ]); + expect(store.entries.size).toBe(1); + const second = sessionProgram({ semantic }); + const reused = reusedLibDiagnostics(second, names, supplied, moduleFile); + expect(second.asked).toEqual([`checked ${main}`]); + // The files' diagnostics in the order of their names (lib.es5.d.ts, main.ts), as the full answer. + expect(reused).toEqual({ + semantic: [diagnostic(lib, 1), diagnostic(main, 5), diagnostic(main, 9)], + global: [], + }); + expect(full?.semantic).toEqual(reused?.semantic); +}); + +test("with kept files, a stored answer needs checking only the kept other files", () => { + const store = memoryStore(); + setLibDiagnosticsStore(store, "checker"); + const pkg = join(dir, "node_modules", "pkg", "index.d.ts"); + const semantic = new Map([ + [lib, [diagnostic(lib, 1)]], + [fallback, []], + [main, [diagnostic(main, 5)]], + [pkg, [diagnostic(pkg, 3)]], + ]); + const withPackage = files({ + [main]: "export const n: number = 'x';\n", + [pkg]: "export declare const p: number;\n", + }); + const all = [lib, fallback, main, pkg]; + const keep = new Set([lib, fallback, main]); + // Without a stored answer nothing is checked or stored. + const cold = sessionProgram({ semantic }); + expect(reusedKeptLibDiagnostics(cold, all, supplied, withPackage, keep)).toBeNull(); + expect(cold.asked).toEqual([]); + expect(store.entries.size).toBe(0); + reusedLibDiagnostics(sessionProgram({ semantic }), all, supplied, withPackage); + expect(store.entries.size).toBe(1); + const warm = sessionProgram({ semantic }); + expect(reusedKeptLibDiagnostics(warm, all, supplied, withPackage, keep)).toEqual({ + semantic: [diagnostic(lib, 1), diagnostic(main, 5)], + global: [], + }); + expect(warm.asked).toEqual([`checked ${main}`]); + // A left-out file that can declare something global still prevents reuse. + const augmenting = files({ + [main]: "export const n: number = 'x';\n", + [pkg]: "export {};\ndeclare global {\n var NaN: number;\n}\n", + }); + const blocked = sessionProgram({ semantic }); + expect(reusedKeptLibDiagnostics(blocked, all, supplied, augmenting, keep)).toBeNull(); + expect(blocked.asked).toEqual([]); +}); + +test("files merge in code point order, which differs from UTF-16 order", () => { + const store = memoryStore(); + setLibDiagnosticsStore(store, "checker"); + const all = [lib, fallback, util, other]; + const sources = files({ [util]: "export {};\n", [other]: "export {};\n" }); + const semantic = new Map([ + [util, [diagnostic(util, 1)]], + [other, [diagnostic(other, 2)]], + ]); + reusedLibDiagnostics(sessionProgram({ semantic }), all, supplied, sources); + const reused = reusedLibDiagnostics(sessionProgram({ semantic }), all, supplied, sources); + // U+FFFD sorts before U+1F600 by code point (its surrogates sort after it by UTF-16 unit). + expect(reused?.semantic).toEqual([diagnostic(other, 2), diagnostic(util, 1)]); +}); + +test("a full check with global diagnostics is not stored", () => { + const store = memoryStore(); + setLibDiagnosticsStore(store, "checker"); + const program = sessionProgram({ + semantic: new Map([[main, [diagnostic(main, 1)]]]), + global: [diagnostic(undefined, 0, 2318)], + }); + expect(reusedLibDiagnostics(program, names, supplied, moduleFile)?.global).toEqual([ + diagnostic(undefined, 0, 2318), + ]); + expect(store.entries.size).toBe(0); +}); + +test("scripts reuse the answer unless they declare a global the supplied files declare", () => { + const store = memoryStore(); + setLibDiagnosticsStore(store, "checker"); + const semantic = new Map([[main, []]]); + // A script declaring only new names: it stores the answer, and a later one reuses it. + const fresh = files({ + [main]: "const total = 1;\nfunction run() {}\nif (total) { var later = 2; }\n", + }); + reusedLibDiagnostics(sessionProgram({ semantic }), names, supplied, fresh); + expect(store.entries.size).toBe(1); + const again = sessionProgram({ semantic }); + expect(reusedLibDiagnostics(again, names, supplied, fresh)).not.toBeNull(); + expect(again.asked).toEqual([`checked ${main}`]); + // Scripts declaring a supplied global, at the top level, in a block, in a + // `declare global` of a supplied module, or an ambient module: the full check. + for (const text of [ + "interface Array { extra: T }\n", + "if (true) { var NaN = 1; }\n", + "declare var process: unknown;\n", + 'declare module "fs" {}\n', + ]) { + const program = sessionProgram({ semantic }); + const result = reusedLibDiagnostics(program, names, supplied, files({ [main]: text })); + expect(program.asked[0], text).not.toBe(`checked ${main}`); + if (result !== null) expect(program.asked[0]).toBe("getGlobalDiagnostics"); + } + expect(store.entries.size).toBe(1); +}); + +test("a program whose script declares a supplied global does not store its answer", () => { + const store = memoryStore(); + setLibDiagnosticsStore(store, "checker"); + const program = sessionProgram({ semantic: new Map([[main, []]]) }); + reusedLibDiagnostics(program, names, supplied, files({ [main]: "declare var NaN: number;\n" })); + expect(store.entries.size).toBe(0); +}); + +test("modules that can declare something global, and servers without the method, ask as before", () => { + const store = memoryStore(); + setLibDiagnosticsStore(store, "checker"); + const semantic = new Map([[main, []]]); + reusedLibDiagnostics(sessionProgram({ semantic }), names, supplied, moduleFile); + expect(store.entries.size).toBe(1); + for (const text of [ + "export {};\ndeclare global { var extra: number }\n", + 'export {};\ndeclare module "./other" { const extra: number }\n', + "export as namespace Lib;\nexport const n = 1;\n", + ]) { + const program = sessionProgram({ semantic }); + expect(reusedLibDiagnostics(program, names, supplied, files({ [main]: text }))).toBeNull(); + expect(program.asked).toEqual([]); + } + const old = sessionProgram({ semantic, checked: false }); + expect(reusedLibDiagnostics(old, names, supplied, moduleFile)).toBeNull(); + // Nor does a server without the method store an answer it could not use. + const fresh = memoryStore(); + setLibDiagnosticsStore(fresh, "checker"); + reusedLibDiagnostics(sessionProgram({ semantic, checked: false }), names, supplied, moduleFile); + expect(fresh.entries.size).toBe(0); +}); + +test("a supplied file that changes is a different entry", () => { + const store = memoryStore(); + setLibDiagnosticsStore(store, "checker"); + const semantic = new Map([[main, []]]); + reusedLibDiagnostics(sessionProgram({ semantic }), names, supplied, moduleFile); + writeFileSync(fallback, fallbackText + "// edited\n"); + const program = sessionProgram({ semantic }); + reusedLibDiagnostics(program, names, supplied, moduleFile); + expect(program.asked[0]).toBe("getGlobalDiagnostics"); + expect(store.entries.size).toBe(2); + writeFileSync(fallback, fallbackText); +}); + +/** The program's files, recording which ones were read. */ +function recording(texts: Record): ((name: string) => SourceFile | undefined) & { + read: string[]; +} { + const inner = files(texts); + const read: string[] = []; + return Object.assign((name: string) => (read.push(name), inner(name)), { read }); +} + +test("a program of modules stores its answer without reading the supplied files", () => { + const store = memoryStore(); + setLibDiagnosticsStore(store, "checker"); + const sources = recording({ [main]: "export const n: number = 'x';\n" }); + reusedLibDiagnostics( + sessionProgram({ semantic: new Map([[main, []]]) }), + names, + supplied, + sources, + ); + expect(sources.read).toEqual([main]); + const [text] = [...store.entries.values()]; + expect(JSON.parse(text!).globals).toBeNull(); + // A later program of modules reuses it, again without reading them. + const again = recording({ [main]: "export const n: number = 'x';\n" }); + const program = sessionProgram({ semantic: new Map([[main, []]]) }); + expect(reusedLibDiagnostics(program, names, supplied, again)).not.toBeNull(); + expect(program.asked).toEqual([`checked ${main}`]); + expect(again.read).toEqual([main]); +}); + +test("a script that uses an answer stored without global names collects and stores them", () => { + const store = memoryStore(); + setLibDiagnosticsStore(store, "checker"); + const semantic = new Map([[main, []]]); + reusedLibDiagnostics(sessionProgram({ semantic }), names, supplied, moduleFile); + const script = recording({ [main]: "const total = 1;\n" }); + const program = sessionProgram({ semantic }); + expect(reusedLibDiagnostics(program, names, supplied, script)).not.toBeNull(); + expect(program.asked).toEqual([`checked ${main}`]); + expect(script.read).toEqual([main, lib, fallback]); + const [text] = [...store.entries.values()]; + expect(new Set(JSON.parse(text!).globals)).toEqual(new Set(["NaN", "Array", "process"])); + // A script that declares one of them runs the full check, and the entry keeps its names. + const clash = sessionProgram({ semantic }); + reusedLibDiagnostics(clash, names, supplied, files({ [main]: "declare var NaN: number;\n" })); + expect(clash.asked[0]).toBe("getGlobalDiagnostics"); + expect(store.entries.size).toBe(1); + expect(JSON.parse([...store.entries.values()][0]!).globals).not.toBeNull(); +}); + +/** The content keys a full check of the program records, with checker release `checker`. */ +function recorded( + checker: string, + semantic: Map, +): PrebuiltLibDiagnostics { + const keys = new Set(); + recordPrebuiltLibDiagnostics({ checker, record: (key) => keys.add(key) }); + setLibDiagnosticsStore(memoryStore(), "build-time checker"); + reusedLibDiagnostics(sessionProgram({ semantic }), names, supplied, moduleFile); + recordPrebuiltLibDiagnostics(null); + return { checker, keys }; +} + +test("a prebuilt answer stands in for checking the supplied files in an empty store", () => { + const semantic = new Map([[main, [diagnostic(main, 5)]]]); + const prebuilt = recorded("1.0.0", semantic); + expect(prebuilt.keys.size).toBe(1); + // Another installation: its own store, its own checker identity. + const store = memoryStore(); + setLibDiagnosticsStore(store, "installed checker", () => prebuilt); + const program = sessionProgram({ semantic }); + expect(reusedLibDiagnostics(program, names, supplied, moduleFile)).toEqual({ + semantic: [diagnostic(main, 5)], + global: [], + }); + expect(program.asked).toEqual([`checked ${main}`]); + // The answer is stored, so the next build reads it from the store. + expect(store.entries.size).toBe(1); + const next = sessionProgram({ semantic }); + setLibDiagnosticsStore(store, "installed checker", () => { + throw new Error("the store answers first"); + }); + reusedLibDiagnostics(next, names, supplied, moduleFile); + expect(next.asked).toEqual([`checked ${main}`]); +}); + +test("prebuilt answers apply only to the same checker release and file contents", () => { + const semantic = new Map([[main, []]]); + const prebuilt = recorded("1.0.0", semantic); + const other: PrebuiltLibDiagnostics = { checker: "1.0.1", keys: prebuilt.keys }; + setLibDiagnosticsStore(memoryStore(), "installed checker", () => other); + const release = sessionProgram({ semantic }); + reusedLibDiagnostics(release, names, supplied, moduleFile); + expect(release.asked[0]).toBe("getGlobalDiagnostics"); + writeFileSync(fallback, fallbackText + "// edited\n"); + setLibDiagnosticsStore(memoryStore(), "installed checker", () => prebuilt); + const edited = sessionProgram({ semantic }); + reusedLibDiagnostics(edited, names, supplied, moduleFile); + expect(edited.asked[0]).toBe("getGlobalDiagnostics"); + writeFileSync(fallback, fallbackText); +}); + +test("a full check that reports diagnostics in a supplied file records no prebuilt answer", () => { + const prebuilt = recorded("1.0.0", new Map([[lib, [diagnostic(lib, 1)]]])); + expect(prebuilt.keys.size).toBe(0); +}); + +test("prebuilt answer files round-trip, and a malformed one is no answer", () => { + const path = join(dir, "lib-diagnostics.json"); + writeFileSync(path, prebuiltLibDiagnosticsText("1.0.0", ["b", "a", "b"])); + expect(readPrebuiltLibDiagnostics(path)).toEqual({ checker: "1.0.0", keys: new Set(["a", "b"]) }); + writeFileSync(path, '{ "schema": "other", "checker": "1.0.0", "keys": [] }'); + expect(readPrebuiltLibDiagnostics(path)).toBeNull(); + expect(readPrebuiltLibDiagnostics(join(dir, "missing.json"))).toBeNull(); +}); diff --git a/packages/compiler/src/frontend/ts7/lib-diagnostics.ts b/packages/compiler/src/frontend/ts7/lib-diagnostics.ts new file mode 100644 index 000000000..ca1f71fc2 --- /dev/null +++ b/packages/compiler/src/frontend/ts7/lib-diagnostics.ts @@ -0,0 +1,530 @@ +/* The semantic diagnostics of the declaration files scriptc supplies (the + * TypeScript libs and scriptc's own declaration files), reused across + * builds. Checking them is most of the full check of a small program. When + * no other file of a program can change what checking those files reports + * (every one is a module without global or module augmentations, or a + * script declaring no global name the supplied files declare), the answer + * depends only on the checker, the compiler options and the files + * themselves, so a stored answer for the same three stands in for checking + * them again: the server checks only the program's other files + * (getDiagnosticsOfCheckedFiles), and the semantic diagnostics merge in the + * order the full check sorts them, by file name. An answer is stored only + * when the full check reports no global diagnostics: the global diagnostics + * of checking the other files are then the full check's. + * + * The global names the supplied files declare matter only to programs with + * scripts, and collecting them reads every supplied file's syntax tree, + * which a program of modules does not otherwise read (about 4 MB at the + * peak of a small build). An answer stored by such a program leaves them + * out; the first program with scripts that uses the answer collects them + * and stores them with it. + * + * A native distribution also ships prebuilt answers: the content keys of + * the supplied file sets the checker reports no diagnostics in, recorded when + * the distribution was built. A build whose store has no answer uses a + * prebuilt one when the checker is the distribution's own and the files' + * content key is among them, and stores it, so builds with an empty cache + * skip the check of the supplied files too. */ +import { createHash } from "node:crypto"; +import { readFileSync, statSync } from "node:fs"; +import { basename, dirname, join } from "node:path"; +import type { Node, SourceFile } from "./ast-types.js"; +import type { Ts7DiagnosticData } from "./session-schema.generated.js"; +import type { Ts7SessionProgram } from "./session.js"; +import { NodeFlags, SyntaxKind } from "./enums.js"; +import { walkPreorder } from "./ast.js"; + +/** Key-value storage for reused diagnostics (best effort: failures miss). */ +export interface LibDiagnosticsStore { + read(key: string): string | null; + write(key: string, text: string): void; +} + +/** The answers a distribution ships: the content keys (contentKey) of the + * supplied file sets whose full check reported no diagnostics with the + * checker release `checker`. */ +export interface PrebuiltLibDiagnostics { + checker: string; + keys: ReadonlySet; +} + +const SCHEMA = "scriptc.lib-diagnostics.v2"; +const PREBUILT_SCHEMA = "scriptc.lib-diagnostics-prebuilt.v1"; + +let configured: { + store: LibDiagnosticsStore; + checker: string; + /** Read on the first store miss; null when there are none. */ + prebuilt: (() => PrebuiltLibDiagnostics | null) | null; +} | null = null; + +/** Has later builds of this process reuse the diagnostics of the supplied + * declaration files through `store`, for the checker identified by + * `checker` (its executable's identity), or stop reusing them (null). + * `prebuilt` reads the answers the distribution ships for that checker. */ +export function setLibDiagnosticsStore( + store: LibDiagnosticsStore | null, + checker = "", + prebuilt: (() => PrebuiltLibDiagnostics | null) | null = null, +): void { + if (store === null) { + configured = null; + return; + } + let loaded: PrebuiltLibDiagnostics | null | undefined; + configured = { + store, + checker, + prebuilt: + prebuilt === null ? null : () => (loaded === undefined ? (loaded = prebuilt()) : loaded), + }; +} + +let prebuiltRecorder: { checker: string; record: (key: string) => void } | null = null; + +/** Has later full checks that report no diagnostics in the supplied files + * pass their content key (contentKey, for the checker release `checker`) to + * `record`, or stop (null). A distribution build collects its prebuilt + * answers this way. */ +export function recordPrebuiltLibDiagnostics( + recorder: { checker: string; record: (key: string) => void } | null, +): void { + prebuiltRecorder = recorder; +} + +/** Reads a prebuilt answers file (`{ schema, checker, keys }`); null when + * it is missing or malformed. */ +export function readPrebuiltLibDiagnostics(path: string): PrebuiltLibDiagnostics | null { + try { + const value = JSON.parse(readFileSync(path, "utf8")) as { + schema?: unknown; + checker?: unknown; + keys?: unknown; + }; + if ( + value.schema !== PREBUILT_SCHEMA || + typeof value.checker !== "string" || + !Array.isArray(value.keys) || + !value.keys.every((key) => typeof key === "string") + ) + return null; + return { checker: value.checker, keys: new Set(value.keys as string[]) }; + } catch { + return null; + } +} + +/** A distribution's prebuilt answers at `path`, for its tsc-ts at `checker` + * (`/lib/tsc-ts`): null unless their checker release is the + * version of the package the executable belongs to. */ +export function distributionPrebuiltLibDiagnostics( + path: string, + checker: string, +): PrebuiltLibDiagnostics | null { + const prebuilt = readPrebuiltLibDiagnostics(path); + return prebuilt !== null && checkerPackageVersion(checker) === prebuilt.checker ? prebuilt : null; +} + +/** The version of the tsc-ts package whose executable is `checker` + * (`/lib/tsc-ts`), or null when it cannot be read. */ +export function checkerPackageVersion(checker: string): string | null { + try { + const pkg = JSON.parse( + readFileSync(join(dirname(dirname(checker)), "package.json"), "utf8"), + ) as { version?: unknown }; + return typeof pkg.version === "string" ? pkg.version : null; + } catch { + return null; + } +} + +/** The text of a prebuilt answers file holding `keys` for the checker + * release `checker`. */ +export function prebuiltLibDiagnosticsText(checker: string, keys: Iterable): string { + return `${JSON.stringify({ schema: PREBUILT_SCHEMA, checker, keys: [...new Set(keys)].sort() }, null, 1)}\n`; +} + +/** A checker executable's identity for the stored diagnostics' key: its + * path and file identity, or the path alone when it cannot be read. */ +export function checkerExecutableIdentity(path: string): string { + try { + const info = statSync(path); + return JSON.stringify([path, info.size, info.mtimeMs, info.ino]); + } catch { + return JSON.stringify([path]); + } +} + +/** The program's semantic and global diagnostics as the full check reports + * them, using stored diagnostics for the supplied declaration files; null + * when they cannot be reused for this program (then the caller asks for + * them as before). `supplied` tells whether a program file is one of the + * supplied declaration files. */ +export function reusedLibDiagnostics( + program: Ts7SessionProgram, + fileNames: readonly string[], + supplied: (fileName: string) => boolean, + sourceFile: (fileName: string) => SourceFile | undefined, +): { semantic: Ts7DiagnosticData[]; global: Ts7DiagnosticData[] } | null { + return reuse(program, fileNames, supplied, sourceFile, () => true, false); +} + +/** reusedLibDiagnostics for a caller that keeps only the diagnostics of the + * `kept` files: a stored answer then needs checking only the kept other + * files, and without a stored answer the result is null (nothing is checked + * or stored). */ +export function reusedKeptLibDiagnostics( + program: Ts7SessionProgram, + fileNames: readonly string[], + supplied: (fileName: string) => boolean, + sourceFile: (fileName: string) => SourceFile | undefined, + kept: ReadonlySet, +): { semantic: Ts7DiagnosticData[]; global: Ts7DiagnosticData[] } | null { + return reuse(program, fileNames, supplied, sourceFile, (name) => kept.has(name), true); +} + +function reuse( + program: Ts7SessionProgram, + fileNames: readonly string[], + supplied: (fileName: string) => boolean, + sourceFile: (fileName: string) => SourceFile | undefined, + keep: (fileName: string) => boolean, + storedOnly: boolean, +): { semantic: Ts7DiagnosticData[]; global: Ts7DiagnosticData[] } | null { + if (configured === null || process.env["SCRIPTC_LIB_DIAGNOSTICS"] === "0") return null; + const { store, checker } = configured; + const libs: string[] = []; + const own: string[] = []; + for (const name of fileNames) (supplied(name) ? libs : own).push(name); + if (libs.length === 0) return null; + // The global names each of the program's scripts declares; a module that + // declares nothing global contributes none. + const scripts: Set[] = []; + for (const name of own) { + const file = sourceFile(name); + if (file === undefined) return null; + if (file.externalModuleIndicator !== undefined) { + if (!declaresNothingGlobal(file)) return null; + continue; + } + const names = isJsFileName(name) ? null : scriptGlobalNames(file); + if (names === null) return null; + scripts.push(names); + } + // A script's globals cannot change what the supplied files report unless + // one of them is also a global the supplied files declare. + const collides = (globals: ReadonlySet): boolean => + scripts.some((names) => [...names].some((name) => globals.has(name))); + // The options without the configuration file's path: it names where the + // project is, which checking the supplied files does not read (the files a + // project's location brings in are part of the key). + const options = { ...program.getCompilerOptions(), configFilePath: undefined }; + const key = entryKey(checker, options, libs); + if (key === null) return null; + const stored = readEntry(store, key, libs) ?? prebuiltEntry(store, key, options, libs); + // An answer stored without the supplied files' global names: a program + // with scripts collects them and stores them with the answer. + let globals = stored?.globals ?? null; + if (stored !== null && globals === null && scripts.length > 0) { + globals = suppliedGlobalNames(libs, sourceFile); + if (globals !== null) writeEntry(store, key, globals, Object.fromEntries(stored.files)); + } + if (stored !== null && (scripts.length === 0 || (globals !== null && !collides(globals)))) { + const checkedNames = storedOnly ? own.filter(keep) : own; + const checked = program.checkedFileDiagnosticData(checkedNames); + if (checked === null) return null; + const byFile = new Map( + storedOnly ? [...stored.files].filter(([name]) => keep(name)) : stored.files, + ); + checkedNames.forEach((name, index) => byFile.set(name, checked.files[index] ?? [])); + return { semantic: inFileOrder(byFile), global: checked.global }; + } + if (storedOnly) return null; + // The full check, as before. Its answers for the supplied files are stored + // when it reports no global diagnostics and the program's scripts declare + // none of the supplied files' globals (so nothing of the program reached + // what those files report). + const global = program.diagnosticData("getGlobalDiagnostics"); + const semantic = program.diagnosticData("getSemanticDiagnostics"); + // A server that cannot check only some files could never use the answer. + if (global.length === 0 && stored === null && program.checkedFileDiagnosticData([]) !== null) { + const names = scripts.length === 0 ? null : suppliedGlobalNames(libs, sourceFile); + if (scripts.length === 0 || (names !== null && !collides(names))) { + const files: Record = {}; + for (const name of libs) files[name] = program.diagnosticData("getSemanticDiagnostics", name); + writeEntry(store, key, names, files); + if (prebuiltRecorder !== null && Object.values(files).every((list) => list.length === 0)) { + const content = contentKey(prebuiltRecorder.checker, options, libs); + if (content !== null) prebuiltRecorder.record(content); + } + } + } + return { semantic, global }; +} + +/** Stores an answer: the supplied files' diagnostics, and their global names + * when they were collected (null otherwise). Best effort: a failed write is + * a later miss. */ +function writeEntry( + store: LibDiagnosticsStore, + key: string, + globals: ReadonlySet | null, + files: Record, +): void { + try { + store.write( + key, + JSON.stringify({ schema: SCHEMA, globals: globals === null ? null : [...globals], files }), + ); + } catch { + // A failed write is a later miss. + } +} + +/** Whether a file cannot add to or change the global declarations the + * supplied files declare: a module with no `declare global`, no module + * augmentation and no UMD `export as namespace` declaration. */ +function declaresNothingGlobal(file: SourceFile): boolean { + if (file.externalModuleIndicator === undefined) return false; + if (file.moduleAugmentations.length > 0) return false; + for (const statement of file.statements.nodes) { + if (statement.kind === SyntaxKind.NamespaceExportDeclaration) return false; + // `declare global` (a module declaration named by the identifier `global`). + if ( + statement.kind === SyntaxKind.ModuleDeclaration && + statement.name?.kind === SyntaxKind.Identifier && + statement.name.text === "global" + ) + return false; + } + return true; +} + +function isJsFileName(name: string): boolean { + return /\.[cm]?jsx?$/.test(name); +} + +/** The names a script file declares in the global scope: its top-level + * declarations, and the `var` and function declarations of its top-level + * blocks; null when it declares an ambient module (`declare module "x"`), + * which can merge with one the supplied files declare. Over-approximates: + * a name more only makes the stored answer apply less often. */ +function scriptGlobalNames(file: SourceFile): Set | null { + const names = new Set(); + let ambientModule = false; + walkPreorder(file, (node) => { + if (node === file) return undefined; + if (node.kind === SyntaxKind.ModuleDeclaration && node.name?.kind === SyntaxKind.StringLiteral) + ambientModule = true; + const topLevel = node.parent === file; + if (topLevel || node.kind === SyntaxKind.FunctionDeclaration) addDeclaredNames(node, names); + else if ( + node.kind === SyntaxKind.VariableDeclarationList && + (node.flags & NodeFlags.BlockScoped) === 0 + ) + for (const declaration of node.declarations?.nodes ?? []) + addBindingNames(declaration.name, names); + // Functions, classes and namespaces have scopes of their own. + return GLOBAL_SCOPE_BOUNDARIES.has(node.kind) ? "skip" : undefined; + }); + return ambientModule ? null : names; +} + +/** The global names the supplied files declare: the declarations of their + * scripts (as scriptGlobalNames finds them, with ambient modules as their + * quoted names) and those of the `declare global` blocks of their modules; + * null when one of them is not available. */ +function suppliedGlobalNames( + libs: readonly string[], + sourceFile: (fileName: string) => SourceFile | undefined, +): Set | null { + const names = new Set(); + for (const name of libs) { + const file = sourceFile(name); + if (file === undefined) return null; + if (file.externalModuleIndicator === undefined) { + walkPreorder(file, (node) => { + if (node === file) return undefined; + if ( + node.kind === SyntaxKind.ModuleDeclaration && + node.name?.kind === SyntaxKind.StringLiteral + ) + names.add(JSON.stringify(node.name.text ?? "")); + if (node.parent === file || node.kind === SyntaxKind.FunctionDeclaration) + addDeclaredNames(node, names); + else if ( + node.kind === SyntaxKind.VariableDeclarationList && + (node.flags & NodeFlags.BlockScoped) === 0 + ) + for (const declaration of node.declarations?.nodes ?? []) + addBindingNames(declaration.name, names); + return GLOBAL_SCOPE_BOUNDARIES.has(node.kind) ? "skip" : undefined; + }); + continue; + } + for (const statement of file.statements.nodes) { + if ( + statement.kind !== SyntaxKind.ModuleDeclaration || + statement.name?.kind !== SyntaxKind.Identifier || + statement.name.text !== "global" + ) + continue; + for (const inner of statement.body?.statements?.nodes ?? []) addDeclaredNames(inner, names); + } + } + return names; +} + +const GLOBAL_SCOPE_BOUNDARIES = new Set([ + SyntaxKind.FunctionDeclaration, + SyntaxKind.FunctionExpression, + SyntaxKind.ArrowFunction, + SyntaxKind.MethodDeclaration, + SyntaxKind.Constructor, + SyntaxKind.GetAccessor, + SyntaxKind.SetAccessor, + SyntaxKind.ClassDeclaration, + SyntaxKind.ClassExpression, + SyntaxKind.ModuleDeclaration, +]); + +/** The names a statement declares in the scope it is in. */ +function addDeclaredNames(node: Node, names: Set): void { + switch (node.kind) { + case SyntaxKind.VariableStatement: + for (const declaration of node.declarationList?.declarations?.nodes ?? []) + addBindingNames(declaration.name, names); + return; + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.ClassDeclaration: + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.TypeAliasDeclaration: + case SyntaxKind.EnumDeclaration: + case SyntaxKind.ModuleDeclaration: + case SyntaxKind.ImportEqualsDeclaration: + if (node.name?.kind === SyntaxKind.Identifier) names.add(node.name.text ?? ""); + return; + } +} + +function addBindingNames(name: Node | undefined, names: Set): void { + if (name === undefined) return; + if (name.kind === SyntaxKind.Identifier) { + names.add(name.text ?? ""); + return; + } + // Object and array binding patterns. + for (const element of name.elements?.nodes ?? []) addBindingNames(element.name, names); +} + +/** The stored diagnostics' key: the checker, the compiler options and the + * supplied files in program order with each one's file identity; null when + * one of them cannot be read. */ +function entryKey(checker: string, options: unknown, libs: readonly string[]): string | null { + const files: unknown[] = []; + for (const name of libs) { + try { + const info = statSync(name); + files.push([name, info.size, info.mtimeMs, info.ino]); + } catch { + return null; + } + } + return createHash("sha256") + .update(JSON.stringify({ schema: SCHEMA, checker, options, files })) + .digest("hex"); +} + +/** The supplied files' answer from the distribution's prebuilt answers, + * stored under `key` for later builds: every file without diagnostics, when + * they hold the files' content key; null otherwise. */ +function prebuiltEntry( + store: LibDiagnosticsStore, + key: string, + options: unknown, + libs: readonly string[], +): { files: Map; globals: Set | null } | null { + const prebuilt = configured?.prebuilt?.() ?? null; + if (prebuilt === null || prebuilt.keys.size === 0) return null; + const content = contentKey(prebuilt.checker, options, libs); + if (content === null || !prebuilt.keys.has(content)) return null; + const files: Record = {}; + for (const name of libs) files[name] = []; + writeEntry(store, key, null, files); + return { files: new Map(Object.entries(files)), globals: null }; +} + +/** A supplied file set's key independent of where the files are installed: + * the checker release, the compiler options and each file's name and + * content digest, in program order; null when a file cannot be read. */ +function contentKey(checker: string, options: unknown, libs: readonly string[]): string | null { + const files: [string, string][] = []; + for (const name of libs) { + try { + files.push([basename(name), createHash("sha256").update(readFileSync(name)).digest("hex")]); + } catch { + return null; + } + } + return createHash("sha256") + .update(JSON.stringify({ schema: PREBUILT_SCHEMA, checker, options, files })) + .digest("hex"); +} + +function readEntry( + store: LibDiagnosticsStore, + key: string, + libs: readonly string[], +): { files: Map; globals: Set | null } | null { + let text: string | null; + try { + text = store.read(key); + } catch { + return null; + } + if (text === null) return null; + let entry: { schema?: unknown; globals?: unknown; files?: Record }; + try { + entry = JSON.parse(text) as typeof entry; + } catch { + return null; + } + if ( + entry.schema !== SCHEMA || + entry.files === undefined || + (entry.globals !== null && !Array.isArray(entry.globals)) + ) + return null; + const files = new Map(); + for (const name of libs) { + const list = entry.files[name]; + if (!Array.isArray(list)) return null; + files.set(name, list); + } + const globals = Array.isArray(entry.globals) + ? new Set(entry.globals.filter((name): name is string => typeof name === "string")) + : null; + return { files, globals }; +} + +/** Each file's diagnostics (already in the checker's order) in the order the + * checker sorts files: by file name, compared by code point. */ +export function inFileOrder(byFile: ReadonlyMap): Ts7DiagnosticData[] { + const names = [...byFile.keys()].sort(compareCodePoints); + const out: Ts7DiagnosticData[] = []; + for (const name of names) for (const d of byFile.get(name)!) out.push(d); + return out; +} + +function compareCodePoints(a: string, b: string): number { + let i = 0; + let j = 0; + while (i < a.length && j < b.length) { + const x = a.codePointAt(i)!; + const y = b.codePointAt(j)!; + if (x !== y) return x < y ? -1 : 1; + i += x > 0xffff ? 2 : 1; + j += y > 0xffff ? 2 : 1; + } + return i < a.length ? 1 : j < b.length ? -1 : 0; +} diff --git a/packages/compiler/src/frontend/ts7/native-api.ts b/packages/compiler/src/frontend/ts7/native-api.ts index 259529541..fa0690e69 100644 --- a/packages/compiler/src/frontend/ts7/native-api.ts +++ b/packages/compiler/src/frontend/ts7/native-api.ts @@ -1,7 +1,10 @@ import { Ts7Api } from "./api.js"; -import { spawnNativeTs7Wire } from "./native-process.js"; +import { checkerReadsLibrariesFromDisk } from "./checker-features.js"; +import { checkerServerName } from "./checker-server.js"; +import { attachNativeTs7Process, spawnNativeTs7Wire } from "./native-process.js"; import { Ts7RpcClient } from "./rpc-client.js"; import type { Ts7FileSystem } from "./rpc-filesystem.js"; +import { Ts7Wire } from "./rpc-wire.js"; /** The native driver supplies its installed TypeScript executable path. * Package discovery is separate from starting and owning a server. */ @@ -10,10 +13,32 @@ export function createNativeTs7Api(options: { cwd: string; fs: Ts7FileSystem; collectTiming?: boolean; + shared?: boolean; }): Ts7Api { const timing = options.collectTiming ?? false; + // The checker reads its own library files when it can: they are part of the + // installed tsc-ts, so no build serves them differently. + const libraries = checkerReadsLibrariesFromDisk(options.executable); + const server = + options.shared === false + ? null + : checkerServerName(options.executable, options.cwd, timing, process.env, libraries); + const shared = + server === null + ? null + : attachNativeTs7Process(server, options.executable, options.cwd, timing, libraries); + if (shared !== null) { + const rpc = new Ts7RpcClient(new Ts7Wire(shared)); + try { + // A server that ended the connection leaves this build to start its own. + rpc.requestText("ping", "null"); + return new Ts7Api(rpc, options.fs, timing); + } catch { + rpc.close(); + } + } return new Ts7Api( - new Ts7RpcClient(spawnNativeTs7Wire(options.executable, options.cwd, timing)), + new Ts7RpcClient(spawnNativeTs7Wire(options.executable, options.cwd, timing, libraries)), options.fs, timing, ); diff --git a/packages/compiler/src/frontend/ts7/native-process.ts b/packages/compiler/src/frontend/ts7/native-process.ts index 7bb523d74..d59543f32 100644 --- a/packages/compiler/src/frontend/ts7/native-process.ts +++ b/packages/compiler/src/frontend/ts7/native-process.ts @@ -1,5 +1,7 @@ import { Ts7Wire, type Ts7WireIo } from "./rpc-wire.js"; import { TS7_FILE_SYSTEM_CALLBACKS } from "./rpc-filesystem.js"; +import { CHECKER_SERVER_IDLE_EXIT, CHECKER_SERVER_MEMORY } from "./checker-server.js"; +import { useNativeJsonValueEnd } from "./semantic-json.js"; // Bound by native/ts7-process.ffi.json. This module is a native entry point; // the Node host continues to use rpc-process.ts's public spawn boundary. @@ -8,6 +10,17 @@ declare function ts7NativeOpen( cwd: string, callbacks: string, timing: boolean, + librariesFromDisk: boolean, +): number; +declare function ts7NativeAttach( + name: string, + executable: string, + cwd: string, + callbacks: string, + timing: boolean, + librariesFromDisk: boolean, + idleExit: number, + memory: number, ): number; declare function ts7NativeRead( handle: number, @@ -25,6 +38,11 @@ declare function ts7NativePid(handle: number): number; declare function ts7NativeClose(handle: number): void; declare function ts7NativeCloseAll(): void; declare function ts7NativeError(bytes: Uint8Array): number; +declare function ts7NativeJsonValueEnd(bytes: Uint8Array, start: number): number; + +// Reply decoding finds element ends with the C scanner. FFI functions can +// only be called, not passed as values, so an arrow function calls it. +useNativeJsonValueEnd((bytes, start) => ts7NativeJsonValueEnd(bytes, start)); // process.exit skips C atexit handlers. Its ordinary exit listener closes // servers too, while the C atexit sweep covers normal native termination. @@ -47,8 +65,26 @@ export class NativeTs7Process implements Ts7WireIo { private handle: number; readonly pid: number; - constructor(executable: string, cwd: string, collectTiming = false) { - this.handle = ts7NativeOpen(executable, cwd, TS7_FILE_SYSTEM_CALLBACKS, collectTiming); + /** With `handle`, a connection to a shared checker server (attachNativeTs7Process). With + * `librariesFromDisk`, the server reads its default library files from the disk + * (checkerReadsLibrariesFromDisk). */ + constructor( + executable: string, + cwd: string, + collectTiming = false, + handle = 0, + librariesFromDisk = false, + ) { + this.handle = + handle !== 0 + ? handle + : ts7NativeOpen( + executable, + cwd, + TS7_FILE_SYSTEM_CALLBACKS, + collectTiming, + librariesFromDisk, + ); if (this.handle === 0) throw nativeError("startup"); this.pid = ts7NativePid(this.handle); } @@ -97,10 +133,34 @@ export class NativeTs7Process implements Ts7WireIo { }; } +/** A connection to the shared checker server `name`, started when none + * answers (`checkerServerName`), or null when this build cannot use it, such + * as while another build is connected to it. */ +export function attachNativeTs7Process( + name: string, + executable: string, + cwd: string, + collectTiming = false, + librariesFromDisk = false, +): NativeTs7Process | null { + const handle = ts7NativeAttach( + name, + executable, + cwd, + TS7_FILE_SYSTEM_CALLBACKS, + collectTiming, + librariesFromDisk, + CHECKER_SERVER_IDLE_EXIT, + CHECKER_SERVER_MEMORY, + ); + return handle === 0 ? null : new NativeTs7Process(executable, cwd, collectTiming, handle); +} + export function spawnNativeTs7Wire( executable: string, cwd: string, collectTiming = false, + librariesFromDisk = false, ): Ts7Wire { - return new Ts7Wire(new NativeTs7Process(executable, cwd, collectTiming)); + return new Ts7Wire(new NativeTs7Process(executable, cwd, collectTiming, 0, librariesFromDisk)); } diff --git a/packages/compiler/src/frontend/ts7/program-host.test.ts b/packages/compiler/src/frontend/ts7/program-host.test.ts index 80c4d2bcf..2ab6a0293 100644 --- a/packages/compiler/src/frontend/ts7/program-host.test.ts +++ b/packages/compiler/src/frontend/ts7/program-host.test.ts @@ -3,7 +3,12 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, expect, test, vi } from "vitest"; import { Ts7Api } from "./rpc-api.js"; -import { Ts7Host, type Ts7ApiOptions, type Ts7CompilerOptions } from "./program-host.js"; +import { + getPreEmitDiagnostics, + Ts7Host, + type Ts7ApiOptions, + type Ts7CompilerOptions, +} from "./program-host.js"; import { ModuleDetectionKind, ModuleKind, ModuleResolutionKind, ScriptTarget } from "./enums.js"; import { tsgoPath } from "./session-path.js"; @@ -70,6 +75,54 @@ test("implementation discovery leaves declaration trees lazy without skipping th expect(() => program.getSourceFileNames()).toThrow("disposed"); }); +test("pre-emit diagnostics for kept files check only those, unless a global diagnostic appears", () => { + const { directory, entry } = fixture(); + const other = join(directory, "other.ts"); + const later = join(directory, "later.ts"); + writeFileSync( + entry, + 'import "./other.js";\nimport "./later.js";\nexport const n: number = "x";\n', + ); + writeFileSync(other, "export const s: string = 1;\n"); + writeFileSync(later, "export const b: boolean = 0;\n"); + const { host } = makeHost(directory); + const program = host.createProgram([entry], options); + try { + const full = getPreEmitDiagnostics(program); + const keep = (name: string): boolean => name !== tsgoPath(other); + const checked = vi.spyOn(program.project.program, "checkedFileDiagnosticData"); + const kept = getPreEmitDiagnostics(program, keep); + // The full answer without the left-out file's diagnostics, in the same order. + expect(full.some((diagnostic) => diagnostic.fileName === tsgoPath(other))).toBe(true); + expect(kept.map((diagnostic) => [diagnostic.fileName, diagnostic.code])).toEqual( + full + .filter((diagnostic) => diagnostic.fileName === undefined || keep(diagnostic.fileName)) + .map((diagnostic) => [diagnostic.fileName, diagnostic.code]), + ); + expect(kept).toHaveLength(2); + expect(checked).toHaveBeenCalledTimes(1); + expect(checked.mock.calls[0]![0]).not.toContain(tsgoPath(other)); + // Keeping every file is the full check. + expect(getPreEmitDiagnostics(program, () => true)).toEqual(full); + expect(checked).toHaveBeenCalledTimes(1); + // A global diagnostic from the partial check, or a server without it: the full check. + for (const answer of [ + { + files: [], + global: [{ fileName: undefined, pos: 0, end: 0, code: 2318, category: 1, text: "global" }], + }, + null, + ]) { + checked.mockReturnValueOnce( + answer as ReturnType, + ); + expect(getPreEmitDiagnostics(program, keep)).toEqual(full); + } + } finally { + program.dispose(); + } +}); + test("the factory receives one connection with the host's filesystem and timing", () => { const { directory, entry } = fixture(); const factory = vi.fn((connection: Ts7ApiOptions) => new Ts7Api(connection)); diff --git a/packages/compiler/src/frontend/ts7/program-host.ts b/packages/compiler/src/frontend/ts7/program-host.ts index 639086e94..2c8ff873f 100644 --- a/packages/compiler/src/frontend/ts7/program-host.ts +++ b/packages/compiler/src/frontend/ts7/program-host.ts @@ -46,6 +46,11 @@ import { trackedRealpath, } from "../input-tracker.js"; import { packageShimDeclarationText } from "../package-shims.js"; +import { typePatchedText, typePatchFileExists } from "../type-patches/layer.js"; +import { ambientDtsPath, isFallbackDtsPath, overridesDtsPath } from "../dts-paths.js"; +import { inFileOrder, reusedKeptLibDiagnostics, reusedLibDiagnostics } from "./lib-diagnostics.js"; +import { ts7Diagnostic } from "./diagnostic.js"; +import type { Ts7DiagnosticData } from "./session-schema.generated.js"; /** The compiler options our createProgram accepts: TS7's CompilerOptions * shape (numeric enums for target/module/moduleResolution — the enums module @@ -118,6 +123,9 @@ export interface Ts7ApiOptions { cwd: string; fs: Ts7FileSystem; collectTiming: boolean; + /** False for a connection that should not use the shared checker server + * (`SCRIPTC_CHECKER_SERVER`), such as the syntax-only source parser's. */ + shared?: boolean; } export type Ts7ApiFactory = (options: Ts7ApiOptions) => Ts7Api; @@ -145,37 +153,44 @@ export class Ts7Host { private readonly virtualChanges = new Map(); private readonly closingProjects = new Set(); private readonly api: Ts7Api; + /** The text the server reads for a file (its readFile hook). */ + private readonly readSource: (fileName: string) => string | null; private closed = false; constructor(createApi: Ts7ApiFactory, options?: Ts7HostOptions) { const virtualFiles = this.virtualFiles; const shadow = options?.fsShadow ?? null; + // A virtual file, a shadow's replacement, a package shim's declaration + // entry, a type-patched package file or the file on disk; null when shadowed out of existence or + // missing. + this.readSource = (fileName) => { + const virtual = virtualFiles.get(tsgoPath(fileName)); + if (virtual !== undefined) return stripSourceBom(virtual); + if (shadow !== null) { + if (shadow.hideFile(fileName)) return null; + const replacement = shadow.readFile(fileName); + if (replacement !== undefined) return stripSourceBom(replacement); + } + // A package scriptc implements itself (package-shims.ts): its + // installed declaration entry re-exports the shim module, in every + // program the host creates. + const shim = packageShimDeclarationText(fileName); + if (shim !== undefined) return shim; + // A package file a project type patch edits (type-patches/layer.ts). + const patched = typePatchedText(fileName); + if (patched !== undefined) return stripSourceBom(patched); + const source = trackedReadFile(fileName); + return source === null ? null : stripSourceBom(source); + }; this.api = createApi({ cwd: options?.cwd ?? process.cwd(), collectTiming: options?.collectTiming ?? false, fs: { - // string => virtual (or shadowed) hit; null => shadowed out of - // existence; undefined => real-FS fallthrough. - readFile: (fileName) => { - const virtual = virtualFiles.get(tsgoPath(fileName)); - if (virtual !== undefined) return stripSourceBom(virtual); - if (shadow !== null) { - if (shadow.hideFile(fileName)) return null; - const replacement = shadow.readFile(fileName); - if (replacement !== undefined) return stripSourceBom(replacement); - } - // A package scriptc implements itself (package-shims.ts): its - // installed declaration entry re-exports the shim module, in every - // program the host creates. - const shim = packageShimDeclarationText(fileName); - if (shim !== undefined) return shim; - const source = trackedReadFile(fileName); - return source === null ? null : stripSourceBom(source); - }, + readFile: this.readSource, fileExists: (fileName) => { if (virtualFiles.has(tsgoPath(fileName))) return true; if (shadow !== null && shadow.hideFile(fileName)) return false; - return trackedFileExists(fileName); + return trackedFileExists(fileName) || typePatchFileExists(fileName); }, directoryExists: (path) => trackedDirectoryExists(path), realpath: (path) => @@ -186,6 +201,12 @@ export class Ts7Host { }); } + /** The text this host serves for a file (null when it serves none), + * without asking the server for its syntax tree. */ + sourceText(fileName: string): string | null { + return this.readSource(fileName); + } + /** Registers an in-memory file served to tsgo by the virtual-FS hooks. */ addVirtualFile(path: string, content: string): void { this.ensureOpen(); @@ -308,6 +329,12 @@ export class Ts7Program { return this.project.program.getSourceFile(resolve(fileName)); } + /** A file's text as the host serves it, without fetching its syntax + * tree (Ts7Host.sourceText). */ + getSourceText(fileName: string): string | null { + return this.host.sourceText(fileName); + } + getSourceFileNames(): readonly string[] { this.snapshot.ensureActive(); return (this.sourceFileNamesCache ??= this.project.program.getSourceFileNames()); @@ -393,6 +420,37 @@ export class Ts7Program { return this.project.program.getSourceFileMetadata(resolve(fileName)); } + /** The semantic and global diagnostics with the stored diagnostics of the + * declaration files scriptc supplies (lib-diagnostics.ts), or null. */ + reusedLibDiagnostics(): { semantic: Ts7DiagnosticData[]; global: Ts7DiagnosticData[] } | null { + return this.libDiagnostics(null); + } + + /** reusedLibDiagnostics for the `kept` files only: null without a stored + * answer. */ + reusedKeptLibDiagnostics( + kept: ReadonlySet, + ): { semantic: Ts7DiagnosticData[]; global: Ts7DiagnosticData[] } | null { + return this.libDiagnostics(kept); + } + + private libDiagnostics( + kept: ReadonlySet | null, + ): { semantic: Ts7DiagnosticData[]; global: Ts7DiagnosticData[] } | null { + this.snapshot.ensureActive(); + const program = this.project.program; + const declarations = new Set([ambientDtsPath(), overridesDtsPath()]); + const names = this.getSourceFileNames(); + const supplied = (name: string): boolean => + declarations.has(tsgoPath(name)) || + isFallbackDtsPath(name) || + program.getSourceFileMetadataByPath(name)?.isDefaultLibrary === true; + const sourceFile = (name: string): SourceFile | undefined => this.getSourceFile(name); + return kept === null + ? reusedLibDiagnostics(program, names, supplied, sourceFile) + : reusedKeptLibDiagnostics(program, names, supplied, sourceFile, kept); + } + isSourceFileDefaultLibrary(file: SourceFile): boolean { return this.project.program.isSourceFileDefaultLibrary(file); } @@ -421,17 +479,66 @@ export class Ts7Program { /** 5.9.3's getPreEmitDiagnostics, composed from 7.0.2's split surface (the * package dropped the aggregate). Order mirrors 5.9.3: config, options/ - * program, global, syntactic, semantic. */ -export function getPreEmitDiagnostics(program: Ts7Program): readonly Diagnostic[] { + * program, global, syntactic, semantic. + * + * With `keep`, a caller that discards every diagnostic of the other files + * asks for semantic diagnostics of the kept files only: the server checks + * only those (getDiagnosticsOfCheckedFiles), or only those not answered by + * stored diagnostics of the supplied declaration files, and their + * diagnostics come in the full check's order. A global diagnostic can come + * from checking any file, so when that partial check reports one, the full + * check answers. */ +export function getPreEmitDiagnostics( + program: Ts7Program, + keep?: (fileName: string) => boolean, +): readonly Diagnostic[] { + const config = program.getConfigFileParsingDiagnostics(); + const options = program.getProgramDiagnostics(); + if (keep !== undefined) { + const kept = program.getSourceFileNames().filter((name) => keep(name)); + if (kept.length < program.getSourceFileNames().length) { + const partial = + program.reusedKeptLibDiagnostics(new Set(kept)) ?? keptFileDiagnostics(program, kept); + if (partial !== null && partial.global.length === 0) + return [ + ...config, + ...options, + ...program.getSyntacticDiagnostics(), + ...partial.semantic.map(ts7Diagnostic), + ]; + } + } + const reused = program.reusedLibDiagnostics(); + if (reused !== null) + return [ + ...config, + ...options, + ...reused.global.map(ts7Diagnostic), + ...program.getSyntacticDiagnostics(), + ...reused.semantic.map(ts7Diagnostic), + ]; return [ - ...program.getConfigFileParsingDiagnostics(), - ...program.getProgramDiagnostics(), + ...config, + ...options, ...program.getGlobalDiagnostics(), ...program.getSyntacticDiagnostics(), ...program.getSemanticDiagnostics(), ]; } +/** The semantic diagnostics of the kept files, in the full check's order, + * and the global diagnostics of checking them; null when the server cannot + * check only some files. */ +function keptFileDiagnostics( + program: Ts7Program, + kept: readonly string[], +): { semantic: Ts7DiagnosticData[]; global: Ts7DiagnosticData[] } | null { + const checked = program.project.program.checkedFileDiagnosticData(kept); + if (checked === null) return null; + const byFile = new Map(kept.map((name, index) => [name, checked.files[index] ?? []])); + return { semantic: inFileOrder(byFile), global: checked.global }; +} + /** ts.findConfigFile: nearest configName walking up from searchPath. */ export function findConfigFile( searchPath: string, diff --git a/packages/compiler/src/frontend/ts7/semantic-json.ts b/packages/compiler/src/frontend/ts7/semantic-json.ts index 3a04c741f..8913007d3 100644 --- a/packages/compiler/src/frontend/ts7/semantic-json.ts +++ b/packages/compiler/src/frontend/ts7/semantic-json.ts @@ -76,8 +76,8 @@ function malformedReply(): never { /** The index after the JSON value that starts at `start`: an object, an * array, a string or a bare literal. Strings are skipped with their escapes, - * so brackets inside them do not count. */ -function jsonValueEnd(bytes: Uint8Array, start: number): number { + * so brackets inside them do not count. Throws when the value is unclosed. */ +export function jsonValueEnd(bytes: Uint8Array, start: number): number { let depth = 0; let index = start; while (index < bytes.length) { @@ -103,6 +103,27 @@ function jsonValueEnd(bytes: Uint8Array, start: number): number { return index; } +/** The scanner the decoders find value ends with: jsonValueEnd, or the + * native compiler's scanner (useNativeJsonValueEnd). It is a property of a + * constant object: a program that installs the scanner but decodes no + * reply must still initialize it, and an assigned module variable with no + * other reader would be left uninitialized by a native build. */ +const scanner: { valueEnd: (bytes: Uint8Array, start: number) => number } = { + valueEnd: jsonValueEnd, +}; + +/** Has the decoders find value ends with `scan`, which returns what + * jsonValueEnd returns, or -1 where jsonValueEnd throws. The native compiler + * installs its C scanner (native/ts7-process.c), which skips the strings that + * make up most reply bytes with memchr. */ +export function useNativeJsonValueEnd(scan: (bytes: Uint8Array, start: number) => number): void { + scanner.valueEnd = (bytes, start) => { + const end = scan(bytes, start); + if (end < 0) malformedReply(); + return end; + }; +} + /** The id an object reply starts with (`{"id":N`), or -1 when it starts * otherwise. */ function leadingId(bytes: Uint8Array, start: number, end: number): number { @@ -158,7 +179,7 @@ export function decodeSemanticObject( ): R | null { const start = skipJsonSpace(bytes, 0); if (start === bytes.length) return null; - const end = jsonValueEnd(bytes, start); + const end = scanner.valueEnd(bytes, start); if (skipJsonSpace(bytes, end) !== bytes.length) malformedReply(); return replyElement(bytes, start, end, cached, build); } @@ -174,7 +195,7 @@ export function decodeSemanticObjects( let index = skipJsonSpace(bytes, 0); if (index === bytes.length) return null; if (bytes[index] !== OPEN_BRACKET) { - const end = jsonValueEnd(bytes, index); + const end = scanner.valueEnd(bytes, index); if ( skipJsonSpace(bytes, end) !== bytes.length || parseJsonSlice(bytes, index, end) !== null @@ -189,7 +210,7 @@ export function decodeSemanticObjects( return result; } for (;;) { - const end = jsonValueEnd(bytes, index); + const end = scanner.valueEnd(bytes, index); if (end === index) malformedReply(); result.push(replyElement(bytes, index, end, cached, build)); index = skipJsonSpace(bytes, end); diff --git a/packages/compiler/src/frontend/ts7/semantic-model.test.ts b/packages/compiler/src/frontend/ts7/semantic-model.test.ts index 9d0f61a4e..bbcd53d8b 100644 --- a/packages/compiler/src/frontend/ts7/semantic-model.test.ts +++ b/packages/compiler/src/frontend/ts7/semantic-model.test.ts @@ -28,6 +28,7 @@ import { SemanticSnapshot, SemanticType, symbolNameOf, + escapedSymbolName, } from "./semantic-model.js"; import type { __String } from "./ast-types.js"; import { @@ -313,6 +314,11 @@ test("symbol tables keep escaped keys, display names, canonical context and cach const exports = parent.getExports(); expect(symbolNameOf(members.get("___x" as __String))).toBe("__x"); expect(symbolNameOf(exports.get("__x" as __String))).toBe("__x"); + // A name that starts with `__` is keyed with an extra `_`. + expect(symbolNameOf(members.get(escapedSymbolName("__x")))).toBe("__x"); + expect(escapedSymbolName("x")).toBe("x"); + expect(escapedSymbolName("_x")).toBe("_x"); + expect(escapedSymbolName("___y")).toBe("____y"); expect(parent.getMembers()).toBe(members); expect(parent.getExports()).toBe(exports); expect(parent.getParent()).toBe(first.symbol(symbol(2))); diff --git a/packages/compiler/src/frontend/ts7/semantic-model.ts b/packages/compiler/src/frontend/ts7/semantic-model.ts index d3d5fe61a..04fa8e637 100644 --- a/packages/compiler/src/frontend/ts7/semantic-model.ts +++ b/packages/compiler/src/frontend/ts7/semantic-model.ts @@ -64,7 +64,9 @@ export interface SemanticQuery { export class SemanticProgram { constructor(private readonly sourceFile: (path: string) => AstNode | undefined) {} getSourceFile(path: string): AstNode | undefined { - return this.sourceFile(path); + // Called as a plain function: the instance is not its receiver. + const sourceFile = this.sourceFile; + return sourceFile(path); } } @@ -454,6 +456,12 @@ export function symbolName(symbol: SemanticSymbol): string { return name.startsWith("___") ? name.slice(1) : name; } +/** The key of the name `name` in a symbol table (`getExports()`, `getMembers()`): the name, with + * an extra `_` when it starts with `__`. The inverse of `symbolName`. */ +export function escapedSymbolName(name: string): __String { + return (name.startsWith("__") ? `_${name}` : name) as __String; +} + /** `symbolName(symbol)`, or undefined without a symbol. */ export function symbolNameOf(symbol: SemanticSymbol | null | undefined): string | undefined { return symbol === undefined || symbol === null ? undefined : symbolName(symbol); diff --git a/packages/compiler/src/frontend/ts7/semantic-types.ts b/packages/compiler/src/frontend/ts7/semantic-types.ts index ea43e3885..5a597ae4e 100644 --- a/packages/compiler/src/frontend/ts7/semantic-types.ts +++ b/packages/compiler/src/frontend/ts7/semantic-types.ts @@ -30,7 +30,7 @@ export type { SemanticSignature as Signature, SemanticSymbol as Symbol, } from "./semantic-model.js"; -export { symbolName, symbolNameOf } from "./semantic-model.js"; +export { escapedSymbolName, symbolName, symbolNameOf } from "./semantic-model.js"; export type { SemanticIndexInfo as IndexInfo, SemanticTypePredicate as TypePredicate, diff --git a/packages/compiler/src/frontend/ts7/session.ts b/packages/compiler/src/frontend/ts7/session.ts index d3f2ae9fe..082bb79a6 100644 --- a/packages/compiler/src/frontend/ts7/session.ts +++ b/packages/compiler/src/frontend/ts7/session.ts @@ -55,6 +55,13 @@ interface ProgramQuery { snapshot?: number; project?: string; file?: SemanticDocument; + files?: string[]; +} + +/** The answer of getDiagnosticsOfCheckedFiles, as the server sends it. */ +export interface Ts7CheckedFileDiagnosticData { + files: Ts7DiagnosticData[][]; + global: Ts7DiagnosticData[]; } /** Platform-independent session over an already connected transport. The @@ -344,6 +351,29 @@ export class Ts7SessionProgram { result.push(ts7Diagnostic(data)); return result; } + /** The diagnostics of `method` for `file`, or for the whole program, as + * the server sends them. */ + diagnosticData(method: string, file?: SemanticDocument): Ts7DiagnosticData[] { + return this.session.request(method, this.query(file)) ?? []; + } + /** The semantic diagnostics of each of `files` and the global diagnostics, + * after the server checks only those files (getDiagnosticsOfCheckedFiles), + * or null when the server does not answer that method. */ + checkedFileDiagnosticData(files: readonly string[]): Ts7CheckedFileDiagnosticData | null { + try { + return this.session.request("getDiagnosticsOfCheckedFiles", { + ...this.query(), + files: [...files], + }); + } catch (error) { + if ( + error instanceof Error && + error.message.includes('unknown API method "getDiagnosticsOfCheckedFiles"') + ) + return null; + throw error; + } + } getSyntacticDiagnostics(file?: SemanticDocument): Ts7Diagnostic[] { return this.diagnostics("getSyntacticDiagnostics", file); } diff --git a/packages/compiler/src/frontend/ts7/source-parser.ts b/packages/compiler/src/frontend/ts7/source-parser.ts index f9029774c..bbb6cae7a 100644 --- a/packages/compiler/src/frontend/ts7/source-parser.ts +++ b/packages/compiler/src/frontend/ts7/source-parser.ts @@ -42,7 +42,9 @@ export class Ts7SourceParser { } private connect(): Ts7Api { - return this.createApi({ + // Called as a plain function: the instance is not its receiver. + const createApi = this.createApi; + return createApi({ cwd: this.cwd, fs: { readFile: (path) => this.files.get(tsgoPath(path)) ?? null, diff --git a/packages/compiler/src/frontend/tsc-codes.ts b/packages/compiler/src/frontend/tsc-codes.ts index e6c0be7cd..a30945be7 100644 --- a/packages/compiler/src/frontend/tsc-codes.ts +++ b/packages/compiler/src/frontend/tsc-codes.ts @@ -128,10 +128,20 @@ export const JS_RELAXED_TSC_CODES: ReadonlySet = new Set([ // JavaScript subclasses may widen fields or add method parameters. // Class lowering checks native storage and override ABIs at each use. 2415, + // a private or protected constructor (`new readline.Interface(...)`, + // whose declaration is protected): JavaScript has no constructor + // accessibility, and Node constructs the class. Each `new` meets the + // lowerer's per-site constructor fences. + 2673, + 2674, // unresolvable modules: Node throws WHEN the require executes — the // binding types as any and reached uses fence instead of passing unchecked 2307, 2792, + // `delete` of a property the inference typed required (2790): JS deletes + // any configurable property, and the lowering checks each delete's + // receiver itself + 2790, // useUnknownInCatchVariables' annotation demand ("'e' is of type // 'unknown'", "Object is of type 'unknown'"): a JS catch clause CANNOT // annotate its variable, so the demand is unsatisfiable in source — @@ -150,7 +160,12 @@ export const JS_ANY_OPERATOR_CODES: ReadonlySet = new Set([2362, 2363, 2 * whose types come entirely from inference (checkJs + JSDoc) and whose JS * relaxation stance applies. */ export function isJsSourceFileName(fileName: string): boolean { - // Only the final few code units can match. Keep the same expression so - // unusual filenames retain its classification. - return /\.(js|mjs|cjs|jsx)$/.test(fileName.slice(-6)); + // The same classification as /\.(js|mjs|cjs|jsx)$/, without a regular + // expression: this runs for most type queries. + return ( + fileName.endsWith(".js") || + fileName.endsWith(".mjs") || + fileName.endsWith(".cjs") || + fileName.endsWith(".jsx") + ); } diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index f1e86067e..b2fc774f2 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -10,6 +10,7 @@ import { isDynTypedRefType, BOOL, bytesOf, + canBoxFuncIntoDyn, canConvertToDyn, canDynCheckTo, CHILD_T, @@ -169,9 +170,10 @@ export const WEBCRYPTO_DYN_TYPES: ReadonlySet = new Set([ "AesDerivedKeyParams", ]); -/** node:crypto's key and signature option interfaces: the key functions, - * KeyObject.export and sign/verify take them as checked-dynamic arguments - * (scr_lib.c, scr_pki.c). */ +/** node:crypto's key, signature and X509Certificate check option + * interfaces: the key functions, KeyObject.export, sign/verify and the + * X509Certificate checks take them as checked-dynamic arguments + * (scr_lib.c, scr_pki.c, scr_x509.c). */ export const CRYPTO_KEY_OPTION_DYN_TYPES: ReadonlySet = new Set([ "AsymmetricKeyExportOptions", "ECKeyPairKeyObjectOptions", @@ -183,12 +185,14 @@ export const CRYPTO_KEY_OPTION_DYN_TYPES: ReadonlySet = new Set([ "PrivateKeyInput", "PublicKeyInput", "RSAKeyPairKeyObjectOptions", + "X25519KeyPairOptions", "RSAKeyPairOptions", "SignKeyObjectInput", "SignPrivateKeyInput", "SigningOptions", "VerifyKeyObjectInput", "VerifyPublicKeyInput", + "X509CheckOptions", ]); export const FETCH_AMBIENT_TYPES = [ @@ -213,6 +217,9 @@ export const FETCH_AMBIENT_TYPES = [ "ReadableStreamReadResult", "ReadableStreamReadValueResult", "ReadableStreamReadDoneResult", + "TransformStream", + "WritableStream", + "WritableStreamDefaultWriter", "Blob", "File", "BlobPropertyBag", @@ -711,6 +718,12 @@ export class UnionRegistry { return id !== undefined && this.pendingRec.has(id); } + /** True when `unionId` is a recursive placeholder whose arms are not + * known yet (its outer frame is still mapping, or failed). */ + isPendingPlaceholder(unionId: string): boolean { + return this.pendingRec.has(unionId); + } + /** Completes a recursive placeholder with its canonical arm list and * registers the structural key (first writer wins, like shapes). */ finalizeRecursive(t: ts.Type, arms: IrType[], discriminant?: IrUnionDiscriminant): string { @@ -1249,6 +1262,10 @@ export interface TypeMapperCtx { * (erasedGenericMemberType): the signature's own type parameters * resolve to `unknown` instead of refusing the mapping. */ erasingSignature?: ts.Signature; + /** Set with erasingSignature: the signature's own type parameters, and + * whether the erasure consulted an enclosing binding. An erasure that + * resolved only its own parameters answers the same in every context. */ + erasedMember?: { own: ReadonlySet; outerConsulted: boolean }; /** True while an erased generic body lowers (its type parameters are * bound to `unknown`). */ isErasing?: () => boolean; @@ -1260,6 +1277,20 @@ export interface TypeMapperCtx { * only where literal identity matters — indexed accesses (`T[K]`) inside * generic bodies whose K is bound to a literal key. */ resolveTypeParamTs?: TypeParamTsResolver; + /** A generic alias reference inside a generic body (`Catalog`) + * re-instantiated with the instance's bound type arguments: the concrete + * checker type its signature instantiates, or null. Consulted only when + * the symbolic form does not map. */ + resolveBoundAliasTs?: TypeParamTsResolver; + /** Localized failure: the lenient mapping of a type whose strict mapping + * failed at a committed slot (Lowerer.degradedTypes), or null. Consulted + * only after the strict mapping (and its bound-alias retry) returns null, + * at every depth, so every occurrence of a degraded type maps alike. */ + degradedType?: (type: ts.Type) => IrType | null; + /** Canonical normalization's shadow mode (canon-map.ts): called after + * each successful top-level mapping; installed only under + * SCRIPTC_CANON_SHADOW=1. It records, and never changes a result. */ + canonShadow?: (type: ts.Type, mapped: IrType, ctx: TypeMapperCtx) => void; /** GENERIC program classes (monomorphization by flow): the mapping of a * concrete instantiation reference (`Box`) — the Lowerer * registers/reuses the instantiation (`Box%0`) and answers its object @@ -1322,7 +1353,7 @@ export interface TypeMapperCtx { * member graph each time is quadratic work. Generic/mixin contexts opt out * through canMemoizeType because the same checker type can map differently * under different instantiation bindings. */ - typeMemo?: Map; + typeMemo?: Map; canMemoizeType?: () => boolean; /** True for files the Lowerer actually compiles (its module order). A * class type can reach the entry through the TYPE world alone — a jsdoc @@ -1390,9 +1421,17 @@ function genChannels( if (!nextT || nextT.kind === "void" || nextT.kind === "nullT") return null; const ownChannel = (t: IrType): boolean => t.kind === "f64" || t.kind === "bool" || t.kind === "string"; + // A record yield channel keeps its type beside a dyn return (an + // `IterableIterator`). A record return channel beside a dyn yield + // does not: the value a delegating `yield*` answers (Effect.gen's + // generators) is often an object whose methods a record would drop. const dynMix = (yieldT.kind === "dyn" && retT.kind !== "dyn" && retT.kind !== "void" && !ownChannel(retT)) || - (retT.kind === "dyn" && yieldT.kind !== "dyn" && yieldT.kind !== "void" && !ownChannel(yieldT)); + (retT.kind === "dyn" && + yieldT.kind !== "dyn" && + yieldT.kind !== "void" && + !ownChannel(yieldT) && + yieldT.kind !== "record"); if (dynMix) return { yieldT: DYN, retT: DYN, nextT }; return { yieldT, retT, nextT }; } @@ -1419,16 +1458,18 @@ let mapTypeDepth = 0; let contextResolutions = 0; /** node:url's legacy record interfaces: url.parse()'s Url instance (and - * its two query-typed refinements), url.format()'s UrlObject input, and the - * URL form's URLFormatOptions. The values are checked-dynamic objects — - * Node's Url instance carries its class name and null fields, and format() - * reads any object's fields with JS truthiness. */ + * its two query-typed refinements), url.format()'s UrlObject input, the + * URL form's URLFormatOptions, and urlToHttpOptions's result. The values + * are checked-dynamic objects — Node's Url instance carries its class name + * and null fields, format() reads any object's fields with JS truthiness, + * and urlToHttpOptions answers a null-prototype object. */ const LEGACY_URL_TYPES = new Set([ "Url", "UrlObject", "UrlWithParsedQuery", "UrlWithStringQuery", "URLFormatOptions", + "UrlHttpOptions", ]); export function isLegacyUrlType( @@ -1733,9 +1774,17 @@ export function mapAsyncGeneratorAnnotation(type: ts.Type, ctx: TypeMapperCtx): return { kind: "generator", async: true, ...channels }; } -/** The native fetch handles (Headers, Request, Response), optionally in - * arrays or with null/undefined: checked-dynamic handles wherever they are - * stored, like the worker handles above. */ +const FETCH_HANDLE_NAMES: ReadonlySet = new Set([ + "Headers", + "Request", + "Response", + "AbortController", + "AbortSignal", +]); + +/** The native fetch handles (Headers, Request, Response, AbortController, + * AbortSignal), optionally in arrays or with null/undefined: checked-dynamic + * handles wherever they are stored, like the worker handles above. */ export function isFetchHandleType( type: ts.Type, ctx: Pick, @@ -1751,9 +1800,7 @@ export function isFetchHandleType( const symbol = member.getSymbol(); if ( !symbol || - (ts.symbolName(symbol) !== "Headers" && - ts.symbolName(symbol) !== "Request" && - ts.symbolName(symbol) !== "Response") || + !FETCH_HANDLE_NAMES.has(ts.symbolName(symbol)) || !symbol.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && @@ -1767,7 +1814,48 @@ export function isFetchHandleType( return handle; } +/** The array type a class declared `extends Array` maps to (its base type, + * `T[]`), or null for every other class. */ +function arrayClassBaseType( + classDecl: ts.ClassLikeDeclaration, + instance: ts.Type, + ctx: TypeMapperCtx, +): IrType | null { + const heritage = classDecl.heritageClauses?.nodes.find( + (clause) => clause.token === ts.SyntaxKind.ExtendsKeyword, + )?.types?.nodes[0]; + if (heritage === undefined || !ts.isIdentifier(heritage.expression)) return null; + if (heritage.expression.text !== "Array") return null; + // The polymorphic `this` type of the class names the class's own type. + const symbol = instance.getSymbol(); + const declared = + (instance.flags & ts.TypeFlags.TypeParameter) !== 0 && symbol !== undefined + ? ctx.checker.getDeclaredTypeOfSymbol(symbol) + : instance; + if ((declared.flags & ts.TypeFlags.Object) === 0) return null; + const target = declared.isTypeReference() ? (declared.getTarget() ?? declared) : declared; + const bases = ctx.checker.getBaseTypes(target as ts.InterfaceType); + const base = bases[0]; + if (base === undefined || !ctx.checker.isArrayType(base)) return null; + const mapped = mapType(base, ctx); + if (mapped?.kind === "array") return mapped; + // `extends Array` in JavaScript reads `any[]`: dyn elements (the class's + // arrayClassBase). + return isJsSourceFile(ts.getSourceFileOfNode(classDecl)) ? { kind: "array", elem: DYN } : null; +} + export function mapType(type: ts.Type, ctx: TypeMapperCtx): IrType | null { + const shadow = ctx.canonShadow; + if (shadow === undefined || mapTypeDepth !== 0) return mapTypeEntry(type, ctx); + // Shadow mode observes every successful top-level mapping, including the + // intrinsic primitives, local class values and memo hits that return + // before the mapping proper. + const mapped = mapTypeEntry(type, ctx); + if (mapped !== null) shadow(type, mapped, ctx); + return mapped; +} + +function mapTypeEntry(type: ts.Type, ctx: TypeMapperCtx): IrType | null { if (mapTypeDepth >= MAP_TYPE_MAX_DEPTH) return null; // These intrinsic domains cannot depend on generic bindings, declaration // provenance, or recursive shape state. Enum literals retain the ordinary @@ -1789,8 +1877,8 @@ export function mapType(type: ts.Type, ctx: TypeMapperCtx): IrType | null { const memoizable = typeId !== undefined && (ctx.canMemoizeType?.() ?? true); // An empty literal's type maps to the empty record as an unreduced union's // arm (emptyLiteralArm) and to the checked-dynamic value elsewhere: the two - // answers keep separate memo entries. - const memoKey = memoizable ? `${ctx.emptyLiteralArm ? 1 : 0}:${typeId}` : null; + // answers keep separate memo entries. Type ids are non-negative integers. + const memoKey = memoizable ? typeId * 2 + (ctx.emptyLiteralArm ? 1 : 0) : null; if (memoKey !== null) { const memo = ctx.typeMemo?.get(memoKey); if (memo !== undefined) return memo; @@ -1808,11 +1896,32 @@ export function mapType(type: ts.Type, ctx: TypeMapperCtx): IrType | null { if (!topLevel || !ts.isUnreadableConstituentsPanic(e)) throw e; mapped = null; } + // A generic alias the body spells over its own type parameters (`x as + // Catalog`) stays symbolic in the checker; when that form does not + // map, the instance's concrete instantiation of the same alias does. + if (mapped === null && ctx.resolveBoundAliasTs) { + const concrete = ctx.resolveBoundAliasTs(type); + if (concrete !== null && concrete !== type) { + mapped = mapType(concrete, ctx); + if (mapped !== null) contextResolutions++; + } + } + // Localized failure: a type that failed at a committed slot maps to its + // lenient form. It counts as a context resolution so neither it nor a + // type containing it is memoized: the strict form may map elsewhere. + if (mapped === null && ctx.degradedType) { + const lenient = ctx.degradedType(type); + if (lenient !== null) { + mapped = lenient; + contextResolutions++; + } + } if ( mapped !== null && memoKey !== null && topLevel && contextResolutions === sensitivityAtEntry && + ctx.erasingSignature === undefined && (ctx.canMemoizeType?.() ?? true) ) { ctx.typeMemo?.set(memoKey, mapped); @@ -1969,7 +2078,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { !ERASURE_KEPT_CONTAINERS.has(ts.symbolNameOf(type.getSymbol()) ?? "") && mentionsErasedTypeParam(type, ctx, 0) ) { - contextResolutions++; + if (!ctx.erasedMember || ctx.erasedMember.outerConsulted) contextResolutions++; return DYN; } // Readonly over a program class (Object.freeze's result type) is the @@ -1979,7 +2088,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { if (resolveTypeParam && type.flags & ts.TypeFlags.TypeParameter) { const bound = resolveTypeParam(type); if (bound) { - contextResolutions++; + if (!ctx.erasedMember?.own.has(type)) contextResolutions++; return bound; } } @@ -2327,6 +2436,23 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { elem = unitOnlyUnion(unions); if (!elem) return null; + // `(T | undefined)[]` of a number, string or boolean T stores T + // elements: an array slot holds a present undefined by its own state, + // so a T[] seen as `(T | undefined)[]` stays the same array. + let optionalElements = false; + if (elem.kind === "union") { + const arms = unions.get(elem.unionId)?.arms; + const present = + arms?.length === 2 ? arms.find((arm) => arm.kind !== "undefinedT") : undefined; + if ( + present !== undefined && + arms!.some((arm) => arm.kind === "undefinedT") && + (present.kind === "f64" || present.kind === "string" || present.kind === "bool") + ) { + elem = present; + optionalElements = true; + } + } // A dyn ELEMENT makes the WHOLE array the checked-dynamic value: // `unknown[]`, `object[]`, and the collapsed `(string | object)[]` // (the plugins-slot shape) — the checked-dynamic tree has real arrays, so length/ @@ -2348,7 +2474,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // drain of the auxiliary-server registries): handles are ordinary // refcounted REF elements (their `_v` adapters, no trace — both drop // their closures at their terminal event). - return arrayOf(elem); + return optionalElements ? { kind: "array", elem, optionalElements: true } : arrayOf(elem); } // Tuples: `[string, string]` maps to an interned RECORD shape flagged // `tuple`, with one field per position named by its index ("0", "1", ...) @@ -2534,6 +2660,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { return viaMixin; } } + // A class declared `extends Array` (frontend/lowering/builtins/ + // array-classes.ts): its instances are arrays of the base's elements. + { + const arrayBase = arrayClassBaseType(classDecl, widened, ctx); + if (arrayBase !== null) return arrayBase; + } if (classExprNeedsContext(classDecl)) { const local = ctx.localClassInstance?.(classDecl, widened); if (local) contextResolutions++; @@ -2970,6 +3102,30 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // spawn consumes a presence-sensitive option bag, including optional // stdio and env values. Keep it checked-dynamic for native normalization. if (isStdlibInterface("SpawnOptions")) return DYN; + // util.inspect as a value (a custom inspect method's third parameter is + // `typeof inspect`): the runtime's inspect function, called through the + // checked-dynamic boundary. + if ( + psym !== undefined && + ts.symbolName(psym) === "inspect" && + psym.declarations.some( + (d) => + ts.isFunctionDeclaration(d) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && + isDeclaredInAmbientModule(d, "util"), + ) + ) + return DYN; + // util.inspect's options bag, which inspect and formatWithOptions read + // at run time (its sorted option can be a comparator function), and the + // options custom inspect functions receive. + if ( + (isStdlibInterface("InspectOptions") || isStdlibInterface("InspectOptionsStylized")) && + psym!.declarations.some( + (d) => ts.isInterfaceDeclaration(d) && isDeclaredInAmbientModule(d, "util"), + ) + ) + return DYN; // dns.LookupAllOptions (`all: true`, the overload whose callback takes // an address list): a lookup option bag dns.lookup reads at run time. // It rides checked-dynamic storage, so the lookup function type that @@ -3033,11 +3189,14 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { if (!elem) return null; return genResultRecord(elem, VOID, ctx.shapes, ctx.unions); } - // node:net's SocketAddress and BlockList: native handles (scr_net.c) - // whose getters and methods dispatch through the handle. + // node:net's SocketAddress and BlockList (scr_net.c) and node:util's + // MIMEType and MIMEParams (scr_mime.c): native handles whose getters and + // methods dispatch through the handle. if ( ((psym !== undefined && ts.symbolName(psym) === "SocketAddress") || - (psym !== undefined && ts.symbolName(psym) === "BlockList")) && + (psym !== undefined && ts.symbolName(psym) === "BlockList") || + (psym !== undefined && ts.symbolName(psym) === "MIMEType") || + (psym !== undefined && ts.symbolName(psym) === "MIMEParams")) && psym.declarations.some( (d) => (ts.isClassDeclaration(d) || ts.isInterfaceDeclaration(d)) && @@ -3046,6 +3205,18 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { ) { return DYN; } + // The functions node:util's debounce and throttle return + // (scr_util_timing.c): native functions with their own members and + // getters. + if ( + psym !== undefined && + (ts.symbolName(psym) === "DebouncedFunction" || ts.symbolName(psym) === "ThrottledFunction") && + psym.declarations.some( + (d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(ts.getSourceFileOfNode(d)), + ) + ) { + return DYN; + } // WebCrypto: the crypto global and its subtle object are dynamic // objects, CryptoKey values are native handles (scr_webcrypto.c), and // the algorithm dictionaries the methods take and keys report are plain @@ -3109,7 +3280,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { psym && (FETCH_AMBIENT_TYPES as readonly string[]).includes(ts.symbolName(psym)) && !( - ts.symbolName(psym) === "ReadableStream" && + (ts.symbolName(psym) === "ReadableStream" || ts.symbolName(psym) === "WritableStream") && psym.declarations.some( (d) => ts.isInterfaceDeclaration(d) && isDeclaredInAmbientNamespace(d, "NodeJS"), ) @@ -3219,10 +3390,9 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { } const collectionView = mapCollectionView(widened, ctx); if (collectionView) return collectionView; - // Date: a TimeClip'd epoch-millisecond scalar in the static runtime. - // This supports stored/passed values and the read-only getter slice; - // identity and mutation stay fenced because the scalar deliberately - // carries no object identity. + // Date: a refcounted object holding its TimeClip'd time value (ScrDate), + // shared with the checked-dynamic Date handle, so it keeps its identity + // through every slot and through `unknown`. Setters stay fenced. if (isStdlibInterface("Date")) return DATE_T; // RegExp: a reference to the lib RegExp interface — provenance, not the // name (a user's own `interface RegExp` maps as a record). Regex values @@ -3955,8 +4125,10 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // Mixed dyn/concrete value channels share checked storage, so either // branch of the result record has the same representation. A number, // boolean or string channel keeps its own type beside a dyn one (the - // `Generator` defaults): its values ride OUT typed and box only - // when the result record is built. + // `Generator` default), and so does a record yield channel beside + // a dyn return (`IterableIterator`): its values ride OUT typed and box + // only when the result record is built, so for-of and spread read them + // back unchecked. if ( isStdlibInterface("Generator") || isStdlibInterface("AsyncGenerator") || @@ -4444,13 +4616,25 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { ), }; } - // crypto.X509Certificate — the Dirent-style data record: THREE visible - // fields (fingerprint and the validFrom/validTo validity window, all - // computed at construction by the new-expression lowering). Module- - // checked like SecureContext; @types/node declares the class inside - // `declare module "crypto"`, the shipped fallback mirrors it. - // JSON.stringify of one shows the three fields where Node's - // internal-slot object shows {} — the Dirent %dtype edge's cousin. + // X509Certificate.toLegacyObject()'s PeerCertificate (and tls's detailed + // form) is a plain checked-dynamic object. + if ( + psym !== undefined && + (ts.symbolName(psym) === "PeerCertificate" || + ts.symbolName(psym) === "DetailedPeerCertificate") && + psym.declarations.some( + (d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(ts.getSourceFileOfNode(d)), + ) + ) { + return DYN; + } + // crypto.X509Certificate — the Dirent-style data record: its fields are + // the certificate's lowered members, all computed at construction by the + // new-expression lowering (raw decodes the input once, and the others + // read raw). Module-checked like SecureContext; @types/node declares the + // class inside `declare module "crypto"`, the shipped fallback mirrors + // it. JSON.stringify of one shows the fields where Node's internal-slot + // object shows {} — the Dirent %dtype edge's cousin. if ( psym !== undefined && ts.symbolName(psym) === "X509Certificate" && @@ -4461,17 +4645,19 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { isDeclaredInAmbientModule(d, "crypto"), ) ) { + const optionalString = withUndefinedArm(STRING, ctx.unions); + if (!optionalString) return null; + const fields = X509_FIELDS.map(([name, type]) => ({ + name, + type: type === "string?" ? optionalString : type, + })); return { kind: "record", shapeId: ctx.shapes.intern( - [ - { name: "fingerprint", type: STRING }, - { name: "validFrom", type: STRING }, - { name: "validTo", type: STRING }, - ], + [...fields].sort((a, b) => (a.name < b.name ? -1 : 1)), false, undefined, - ["fingerprint", "validFrom", "validTo"], + fields.map((f) => f.name), ), }; } @@ -4755,6 +4941,9 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { if (hybrid) return hybrid; } const callSigs = checker.getCallSignatures(widened); + // AsyncLocalStorage.snapshot()'s generic runner is a dyn function value + // (als.snapshot), called through the dyn call path. + if (callSigs.length === 1 && isAsyncLocalStorageSnapshot(callSigs[0]!, ctx)) return DYN; if (callSigs.length === 1) return mapCallSignature(callSigs[0]!, ctx); // Records: object types whose members are all data properties (shorthand // methods in type position count — they're func-typed fields) with @@ -4891,6 +5080,19 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { const record = mapRecordType(widened, ctx); if (record) return record; } + // An intersection whose constituents all map to ONE representation is + // that representation. An intersection that restates an optional member + // as required (`Opts & { files: Array<{ path: string }> }` over + // `files?: Array<{ path: string }>`) gives the member the type `A[] & B[]` + // with two separately written but structurally equal element literals; + // the checker keeps both, while their arrays (tuples, maps, ...) intern + // to one IR type. Reached only after every other intersection rule + // declined, so intersections that already mapped keep their mapping. A + // dynamic result stays unmapped: two opaque types are not one type. + if (widened.isIntersectionType()) { + const same = mapEqualConstituentsIntersection(widened, ctx); + if (same) return same; + } // Unions. `boolean` is internally `true | false`, and narrowing produces // literal unions like `"a" | "b"` — parts that map to the SAME IR type // collapse (deduplicated by typeKey), so a single surviving arm is just @@ -5051,7 +5253,6 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { (a) => a.kind === "void" || a.kind === "union" || - a.kind === "date" || a.kind === "dyn" || // A generator arm only comes from an Iterable (an optional // one keeps its unit arms beside the iterable union's). @@ -5087,6 +5288,26 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { /** One non-overloaded call signature's func type (mapTypeInner's * single-signature rule; erasedOverloadMemberType maps each overload of a * member through it). */ +/** Whether `sig` is the function type AsyncLocalStorage.snapshot() returns: + * declared as that static method's return type in the async_hooks module + * of the standard library declarations. */ +function isAsyncLocalStorageSnapshot(sig: ts.Signature, ctx: TypeMapperCtx): boolean { + const decl = sig.declaration; + if (decl === undefined || !ts.isFunctionTypeNode(decl)) return false; + const method = decl.parent; + return ( + method !== undefined && + ts.isMethodDeclaration(method) && + ts.isIdentifier(method.name) && + method.name.text === "snapshot" && + method.parent !== undefined && + ts.isClassDeclaration(method.parent) && + method.parent.name?.text === "AsyncLocalStorage" && + ctx.isStdlibFile(ts.getSourceFileOfNode(decl)) && + isDeclaredInAmbientModule(decl, "async_hooks") + ); +} + function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null { const { checker } = ctx; if (sig.getTypeParameters().length && ctx.erasingSignature !== sig) return null; @@ -5142,10 +5363,12 @@ function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null // A parameter a TypeScript program types `any` takes whatever the // caller passes: the checked-dynamic slot, as an `any`-typed binding // already is (dynFallbackType). Declaration files keep their paths. - // Only function declarations, spelled function types and lambda - // parameters whose default is the only source of their type (`(n = - // lookup() ?? 5) => ...` with an `any` lookup): other lambdas and - // methods in object literals keep their existing checked-dynamic story. + // Only function declarations, spelled function types, method and call + // signatures of interfaces and type literals (`execute(params: any): + // Promise`), and lambda parameters whose default is the only source + // of their type (`(n = lookup() ?? 5) => ...` with an `any` lookup): + // other lambdas and methods in object literals keep their existing + // checked-dynamic story. if ( !pt && !rest && @@ -5154,6 +5377,8 @@ function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null decl.parent !== undefined && (ts.isFunctionDeclaration(decl.parent) || ts.isFunctionTypeNode(decl.parent) || + ts.isMethodSignatureDeclaration(decl.parent) || + ts.isCallSignatureDeclaration(decl.parent) || ((ts.isArrowFunction(decl.parent) || ts.isFunctionExpression(decl.parent)) && decl.initializer !== undefined && decl.type === undefined && @@ -5226,8 +5451,19 @@ function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null if (pt) pt = jsOpenObjectType(decl, pt, ctx.shapes, ctx.unions); // Belt and braces for non-strict type worlds: an optional param's // ABI slot is always the undefined-armed union (strictNullChecks - // already spells it that way; arm it here if the world didn't). - if (optional && pt && pt.kind !== "void" && pt.kind !== "dyn") { + // already spells it that way; arm it here if the world didn't). A + // back-reference to a union still being mapped (`options?: { then?: + // (ctx: Sub) => void }` where Sub extends the interface declaring the + // method) is a recursive placeholder with no arms yet; when the checker + // spelled the parameter `X | undefined`, the placeholder receives that + // undefined arm when its frame finishes. + const pendingOptional = + pt?.kind === "union" && + ctx.unions.isPendingPlaceholder(pt.unionId) && + ts + .constituentTypes(checker.getTypeOfSymbol(p)) + .some((part) => (part.flags & ts.TypeFlags.Undefined) !== 0); + if (optional && pt && pt.kind !== "void" && pt.kind !== "dyn" && !pendingOptional) { const armed = withUndefinedArm(pt, ctx.unions); if (!armed) return null; pt = armed; @@ -6584,7 +6820,7 @@ export function genResultRecord( for (const a of unions.get(t.unionId)?.arms ?? []) byKey.set(typeKey(a), a); return true; } - if (t.kind === "regex" || t.kind === "date" || t.kind === "generator") { + if (t.kind === "regex" || t.kind === "generator") { return false; // no legal union arm exists for these kinds } byKey.set(typeKey(t), t); @@ -6658,6 +6894,39 @@ function boundVoidReturnType(p: ts.Type, resolveTypeParam?: TypeParamResolver): * result is exactly the nullable-callback shape mapType's union branch * admits, `(() => void) | undefined`) so the interned union is IDENTICAL * to what mapping the checker's own `T | undefined` produces. */ +/** The X509Certificate record's hidden slot for its publicKey KeyObject, + * filled on the first read (builtins/x509.ts). */ +export const X509_PUBLIC_KEY_FIELD = "%publicKey"; + +/** The X509Certificate record's hidden copy of its DER, which its methods + * read: a program can write into the Buffer `raw` answers, and in Node + * that changes neither the certificate's checks nor its PEM. */ +export const X509_DER_FIELD = "%der"; + +/** The X509Certificate record's fields in @types/node's declaration order + * ("string?" is string | undefined), then the hidden publicKey and DER + * slots. */ +export const X509_FIELDS: readonly (readonly [string, IrType | "string?"])[] = [ + ["ca", BOOL], + ["fingerprint", STRING], + ["fingerprint256", STRING], + ["fingerprint512", STRING], + ["subject", STRING], + ["subjectAltName", "string?"], + ["infoAccess", "string?"], + ["issuer", STRING], + ["raw", { kind: "bytes", elem: "u8" }], + ["serialNumber", STRING], + ["signatureAlgorithm", "string?"], + ["signatureAlgorithmOid", STRING], + ["validFrom", STRING], + ["validFromDate", DATE_T], + ["validTo", STRING], + ["validToDate", DATE_T], + [X509_PUBLIC_KEY_FIELD, DYN], + [X509_DER_FIELD, { kind: "bytes", elem: "u8" }], +]; + export function withUndefinedArm(t: IrType, unions: UnionRegistry): IrType | null { return withUnitArm(t, "undefinedT", unions); } @@ -6673,15 +6942,15 @@ export function withUnitArm( if (t.kind === "union") { const def = unions.get(t.unionId); if (!def) return null; - if (def.arms.some((a) => a.kind === "date")) return null; if (def.arms.some((a) => a.kind === kind)) return t; const arms = [...def.arms, unit]; + // Date and Promise arms beside other data arms have no union home. + if (!unionContainerArmsOk(arms)) return null; arms.sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1)); return { kind: "union", unionId: unions.transform(def, arms) }; } if ( t.kind === "void" || - t.kind === "date" || t.kind === "dyn" || // A bare unit field type cannot occur (units live only inside unions), // but guard against constructing a single-arm union from one. @@ -6704,6 +6973,19 @@ export function withUnitArm( * assigned keys, which is also Node's answer for Object.assign(fn, obj) * (a function's own length/name are non-enumerable). Null whenever the * shape doesn't fit — the caller falls through to the plain-func mapping. */ +/** The IR type every constituent of an intersection maps to, or null when a + * constituent is unmappable or dynamic, or two constituents differ. */ +function mapEqualConstituentsIntersection(widened: ts.Type, ctx: TypeMapperCtx): IrType | null { + let same: IrType | null = null; + for (const part of ts.constituentTypes(widened)) { + const mapped = mapType(part, ctx); + if (mapped === null || mapped.kind === "dyn") return null; + if (same === null) same = mapped; + else if (!typeEquals(same, mapped)) return null; + } + return same; +} + function mapHybridCallableIntersection(widened: ts.Type, ctx: TypeMapperCtx): IrType | null { const { checker, shapes } = ctx; if (!widened.isIntersectionType()) return null; @@ -6978,6 +7260,43 @@ function immutableSourceType(type: ts.Type, ctx: TypeMapperCtx): ts.Type | null return null; } +/** A mapped type reached through a type alias whose arguments mention no + * type parameter (`Record`, `Catalog`, + * a module-level alias without arguments): + * the checker computed its members from concrete inputs, so an empty + * result really is the empty object type, unlike `Partial` inside a + * generic body. */ +function resolvedMappedAlias(t: ts.Type, ctx: TypeMapperCtx): boolean { + if (!isMappedShape(t) || t.getAliasSymbol() === undefined) return false; + const args = t.getAliasTypeArguments(); + const seen = new Set(); + const concrete = (part: ts.Type): boolean => { + if (part.flags & (ts.TypeFlags.TypeParameter | ts.TypeFlags.Index)) return false; + if (part.flags & (ts.TypeFlags.IndexedAccess | ts.TypeFlags.Conditional)) return false; + if (seen.has(part)) return true; + seen.add(part); + if (part.isUnionType() || part.isIntersectionType()) + return ts.constituentTypes(part).every(concrete); + if (!part.getAliasTypeArguments().every(concrete)) return false; + return ( + (part.flags & ts.TypeFlags.Object) === 0 || + ((part as ts.ObjectType).objectFlags & ts.ObjectFlags.Reference) === 0 || + ctx.checker.getTypeArguments(part as ts.TypeReference).every(concrete) + ); + }; + // An alias without type arguments is concrete when it is declared at + // module level (a function body's alias may mention that function's type + // parameters). + if (args.length === 0) + return t + .getAliasSymbol()! + .declarations.every( + (d) => + ts.isTypeAliasDeclaration(d) && (ts.isSourceFile(d.parent) || ts.isModuleBlock(d.parent)), + ); + return args.every(concrete); +} + /** True for MAPPED-type results — `Partial`, `Record<"a", n>`, * whatever `Pick`/`Omit` reduce to. Their shape is computed by the checker, * not declared anywhere: the only declaration behind them is the utility @@ -7163,10 +7482,21 @@ export function erasedGenericMemberType(t: ts.Type, ctx: TypeMapperCtx): IrType const sig = sigs[0]!; const own = new Set(sig.getTypeParameters()); const outer = ctx.resolveTypeParam; + // Resolving only the member's own parameters does not make the erasure + // context-sensitive, so a recursive record holding a generic method + // (`interface Sub extends Base { send(m: Msg): void }` reached + // from Base) still closes its knot. Consulting an enclosing binding does. + const erasedMember = { own, outerConsulted: false }; const erased = mapType(t, { ...ctx, erasingSignature: sig, - resolveTypeParam: (param) => (own.has(param) ? DYN : (outer?.(param) ?? null)), + erasedMember, + resolveTypeParam: (param) => { + if (own.has(param)) return DYN; + if (outer === undefined) return null; + erasedMember.outerConsulted = true; + return outer(param); + }, }); return erased?.kind === "func" ? erased : undefined; } @@ -7932,8 +8262,12 @@ function mapRecordTypeInner( // still unknown there) — interning `{}` would give a wrong type. The // degenerate empties (`Partial<{}>`, `Omit`) go with it. // An INDEX-SIGNATURE shape is exempt: `Record` legitimately - // has zero declared members — the signature is the shape. - if (computed && props.length === 0 && !indexValue) return null; + // has zero declared members — the signature is the shape. So is a mapped + // alias instantiated over concrete arguments (`Record`, a + // catalog type whose key filter keeps no key): the checker resolved it, + // and it is the empty object type below. + if (computed && props.length === 0 && !indexValue && !resolvedMappedAlias(widened, ctx)) + return null; // An empty object type — `{}` (spelled, the checker's shared // intrinsic, or inferred from an empty object literal), `interface // Empty {}` — is tsc's TOP type over non-nullish values: every number, @@ -8191,6 +8525,16 @@ function dynSubsumableUnionArm(arm: IrType, ctx: TypeMapperCtx): boolean { // `string | URL | Request`, fetch's own input type under @types/node. case "url": return true; + // A function the checked-dynamic tree can box (each parameter checks + // in, the result converts out) is held by reference there, and + // `typeof x === "function"` narrows it back out: `Progress | AbortSignal` + // beside fetch's handles. + case "func": + return canBoxFuncIntoDyn( + arm, + (id) => ctx.shapes.get(id), + (id) => ctx.unions.get(id), + ); case "record": case "array": // `T | Promise` with T bound to `unknown`: the checked-dynamic value @@ -8271,7 +8615,6 @@ function armHasUnionHome(arm: IrType): boolean { case "void": case "union": case "regex": - case "date": case "generator": case "dyn": return false; diff --git a/packages/compiler/src/frontend/type-patches/command.ts b/packages/compiler/src/frontend/type-patches/command.ts new file mode 100644 index 000000000..dfa05ea1e --- /dev/null +++ b/packages/compiler/src/frontend/type-patches/command.ts @@ -0,0 +1,413 @@ +/* `scriptc types patch` and `scriptc types list`: write and inspect the + * project's type patches (package.json scriptc.typePatches). + * + * scriptc types patch [--range ] [--dry-run] + * scriptc types list + * + * `patch` finds the installed package from the working directory, shows + * every declaration the selector matches (in the package's declaration + * files, its JSDoc and its @types package) with its type before and after, + * and adds the patch to the nearest package.json under a range covering + * the installed version (`^` unless --range says otherwise, or an + * existing entry's range when one covers it). Nothing is written when the + * selector matches nothing. `list` shows each configured patch, the + * installed copies it applies to and what it matches. */ + +import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs"; +import { dirname, join, relative } from "node:path"; +import { parseProjectConfig, type TypePatchEntry } from "../../project-config.js"; +import type { TypePatchParse } from "./layer.js"; +import { patchFile, type PatchMatch } from "./locate.js"; +import { parseSelector, parseTypeText, type Selector, type TypePatchOp } from "./selector.js"; +import { isValidRange, satisfies } from "./semver.js"; + +export type TypePatchCommandRequest = + | { + action: "patch"; + cwd: string; + pkg: string; + selector: string; + type: string; + range: string | null; + dryRun: boolean; + } + | { action: "list"; cwd: string }; + +export interface TypePatchCommandResult { + stdout: string; + stderr: string; + exitCode: number; +} + +interface PackageCopy { + root: string; + version: string | null; + /** True for an `@types/` twin. */ + twin: boolean; +} + +const PATCHABLE = /\.(?:[cm]?js|d\.[cm]?ts|[cm]?ts)$/; + +function nearestManifest(cwd: string): string | null { + for (let dir = cwd; ;) { + const manifest = join(dir, "package.json"); + if (existsSync(manifest)) return manifest; + const parent = dirname(dir); + if (parent === dir) return null; + dir = parent; + } +} + +function versionOf(root: string): string | null { + try { + const parsed = JSON.parse(readFileSync(join(root, "package.json"), "utf8")) as { + version?: unknown; + }; + return typeof parsed.version === "string" ? parsed.version : null; + } catch { + return null; + } +} + +function typesName(pkg: string): string { + return pkg.startsWith("@") ? `@types/${pkg.slice(1).replace("/", "__")}` : `@types/${pkg}`; +} + +/** The copy of `pkg` (and of its @types twin) Node resolution finds from + * `cwd`. A twin is versioned by the runtime package beside it. */ +function installedCopies(cwd: string, pkg: string): PackageCopy[] { + const out: PackageCopy[] = []; + let runtimeVersion: string | null = null; + let twinFound = false; + for (let dir = cwd; ;) { + const modules = join(dir, "node_modules"); + if (runtimeVersion === null && existsSync(join(modules, pkg, "package.json"))) { + const root = join(modules, pkg); + runtimeVersion = versionOf(root); + out.push({ root, version: runtimeVersion, twin: false }); + } + if (!twinFound && existsSync(join(modules, typesName(pkg), "package.json"))) { + twinFound = true; + out.push({ root: join(modules, typesName(pkg)), version: null, twin: true }); + } + const parent = dirname(dir); + if (parent === dir) break; + dir = parent; + } + for (const copy of out) if (copy.twin) copy.version = runtimeVersion ?? versionOf(copy.root); + return out; +} + +/** The files of a package copy a patch can apply to (nested packages + * excluded), package-relative. */ +function packageFiles(root: string): string[] { + const out: string[] = []; + const walk = (dir: string): void => { + let names: string[]; + try { + names = readdirSync(dir).sort(); + } catch { + return; + } + for (const name of names) { + if (name === "node_modules" || name.startsWith(".")) continue; + const path = join(dir, name); + let isDir = false; + try { + isDir = statSync(path).isDirectory(); + } catch { + continue; + } + if (isDir) walk(path); + else if (PATCHABLE.test(name)) out.push(relative(root, path).split("\\").join("/")); + } + }; + walk(root); + return out; +} + +interface FileMatch extends PatchMatch { + file: string; +} + +/** Every declaration a selector matches in a copy, and the problems where + * it cannot apply (`always`: a problem even where the selector matched + * elsewhere, such as an ambiguous replacement, which leaves those + * declarations as they are). */ +function matchCopy( + copy: PackageCopy, + selector: Selector, + op: TypePatchOp, + parse: TypePatchParse, +): { matches: FileMatch[]; problems: { message: string; always: boolean }[] } { + const words = + selector.root === "export=" + ? ["exports", "export"] + : selector.root === "default" + ? ["default"] + : [selector.root]; + const matches: FileMatch[] = []; + const problems: { message: string; always: boolean }[] = []; + for (const rel of packageFiles(copy.root)) { + if (selector.file !== null && selector.file !== rel) continue; + const path = join(copy.root, rel); + let text: string; + try { + text = readFileSync(path, "utf8"); + } catch { + continue; + } + if (!words.some((w) => text.includes(w))) continue; + let result; + try { + result = patchFile(rel, text, parse(path, text, /\.[cm]?ts$/.test(rel) ? "ts" : "js"), [ + { id: 0, selector, op }, + ]); + } catch { + continue; + } + for (const m of result.matches) matches.push({ ...m, file: rel }); + for (const p of result.problems) + problems.push({ message: `${p.message} (${rel})`, always: p.always === true }); + } + return { matches, problems }; +} + +function shownCopy(copy: PackageCopy, base: string, pkg: string): string { + const name = copy.twin ? typesName(pkg) : pkg; + return `${name} ${copy.version ?? "(no version)"} (${relative(base, copy.root).split("\\").join("/")})`; +} + +function describe(m: FileMatch): string { + if (m.redundant) return ` ${m.file}:${m.line} ${m.before} (already declared so)`; + return ` ${m.file}:${m.line} ${m.before} -> ${m.after}`; +} + +/** The indentation a JSON text uses (two spaces when it cannot tell). */ +function indentOf(text: string): string { + const m = /^[{[]\s*\n([ \t]+)\S/.exec(text); + return m === null ? " " : m[1]!; +} + +function isObject(value: unknown): value is Record { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + +/** JSON text of `value` with `path` set to the string `leaf` (objects + * created along the way, the leaf replaced if present), keeping key order + * and indenting like JSON.stringify(value, null, indent). The parsed value + * itself is only read. */ +function renderJson( + value: unknown, + indent: string, + at: string, + path: readonly string[], + leaf: string, +): string { + const inner = at + indent; + if (path.length > 0 && !isObject(value)) return renderJson({}, indent, at, path, leaf); + if (isObject(value)) { + const keys = Object.keys(value); + const parts: string[] = []; + const head = path[0]; + for (const k of keys) { + const child = + head === k + ? path.length === 1 + ? JSON.stringify(leaf) + : renderJson(value[k], indent, inner, path.slice(1), leaf) + : renderJson(value[k], indent, inner, [], leaf); + parts.push(`${inner}${JSON.stringify(k)}: ${child}`); + } + if (head !== undefined && !keys.includes(head)) { + const child = + path.length === 1 + ? JSON.stringify(leaf) + : renderJson({}, indent, inner, path.slice(1), leaf); + parts.push(`${inner}${JSON.stringify(head)}: ${child}`); + } + return parts.length === 0 ? "{}" : `{\n${parts.join(",\n")}\n${at}}`; + } + if (Array.isArray(value)) { + const items: unknown[] = value; + if (items.length === 0) return "[]"; + return `[\n${items.map((item) => `${inner}${renderJson(item, indent, inner, [], leaf)}`).join(",\n")}\n${at}]`; + } + return JSON.stringify(value) ?? "null"; +} + +function patchCommand( + req: Extract, + parse: TypePatchParse, +): TypePatchCommandResult { + const fail = (message: string): TypePatchCommandResult => ({ + stdout: "", + stderr: `scriptc: ${message}\n`, + exitCode: 1, + }); + const selector = parseSelector(req.selector); + if ("error" in selector) return fail(`'${req.selector}' is not a selector: it ${selector.error}`); + const op = parseTypeText(req.type); + if ("error" in op) return fail(`the type '${req.type}' ${op.error}`); + if (req.range !== null && !isValidRange(req.range)) + return fail(`'${req.range}' is not a semver range`); + const manifest = nearestManifest(req.cwd); + if (manifest === null) return fail("no package.json found from the working directory"); + const base = dirname(manifest); + const copies = installedCopies(req.cwd, req.pkg); + const runtime = copies.find((c) => !c.twin) ?? copies[0]; + if (runtime === undefined) return fail(`${req.pkg} is not installed here`); + const version = runtime.version; + const lines: string[] = []; + let matched = 0; + let changed = 0; + const problems: { message: string; always: boolean }[] = []; + for (const copy of copies) { + const found = matchCopy(copy, selector, op, parse); + problems.push(...found.problems); + if (found.matches.length === 0) continue; + lines.push(shownCopy(copy, base, req.pkg)); + for (const m of found.matches) { + lines.push(describe(m)); + matched++; + if (!m.redundant) changed++; + } + } + if (matched === 0) { + const why = + problems.length > 0 + ? problems.map((p) => p.message).join("\n") + : `'${req.selector}' matches no declaration`; + return fail(`${why}\nnothing was written`); + } + // A declaration the patch cannot apply to where it also matched (an + // ambiguous replacement) would stay as it is. + const unapplied = problems.filter((p) => p.always); + if (unapplied.length > 0) + return fail(`${unapplied.map((p) => p.message).join("\n")}\nnothing was written`); + const text = readFileSync(manifest, "utf8"); + // The compiler applies every entry whose range covers the installed + // version, and a later entry wins on the same selector. Without a range, + // the patch goes to the last covering entry that patches the selector + // already, else to the first covering entry; with one, to the entry of + // that range. Otherwise a new entry, which comes last, takes it. + const entries = parseProjectConfig(manifest, text).typePatches.filter( + (e: TypePatchEntry) => e.pkg === req.pkg, + ); + const covers = (e: TypePatchEntry): boolean => version !== null && satisfies(version, e.range); + const declares = (e: TypePatchEntry): boolean => + e.declarations.some((d) => d.selector === req.selector); + // The declarations may say this already while a covering entry patches + // the selector to something else, which the build would keep using. + const overridden = entries.some( + (e) => + covers(e) && e.declarations.some((d) => d.selector === req.selector && d.type !== req.type), + ); + if (changed === 0 && !overridden) { + lines.push("the declarations already say this; nothing was written"); + return { stdout: `${lines.join("\n")}\n`, stderr: "", exitCode: 0 }; + } + let parsed: unknown; + try { + parsed = JSON.parse(text); + } catch { + return fail(`${relative(req.cwd, manifest) || "package.json"} is not valid JSON`); + } + const existing = + req.range === null + ? (entries.findLast((e) => covers(e) && declares(e)) ?? entries.find(covers)) + : entries.find((e) => e.range === req.range); + if (existing !== undefined && covers(existing)) { + const later = entries + .slice(entries.indexOf(existing) + 1) + .find((e) => covers(e) && declares(e)); + if (later !== undefined) + return fail( + `scriptc.typePatches["${later.key}"] also patches '${req.selector}' for ${req.pkg} ${version} and takes precedence over scriptc.typePatches["${existing.key}"]\nnothing was written`, + ); + } + const key = + existing?.key ?? `${req.pkg}@${req.range ?? (version === null ? "*" : `^${version}`)}`; + const where = `scriptc.typePatches["${key}"].declarations["${req.selector}"]`; + if (req.dryRun) { + lines.push(`would write ${relative(req.cwd, manifest) || "package.json"} ${where}`); + return { stdout: `${lines.join("\n")}\n`, stderr: "", exitCode: 0 }; + } + const trailing = text.endsWith("\n") ? "\n" : ""; + writeFileSync( + manifest, + renderJson( + parsed, + indentOf(text), + "", + ["scriptc", "typePatches", key, "declarations", req.selector], + req.type, + ) + trailing, + ); + lines.push(`wrote ${relative(req.cwd, manifest) || "package.json"} ${where}`); + return { stdout: `${lines.join("\n")}\n`, stderr: "", exitCode: 0 }; +} + +function listCommand(cwd: string, parse: TypePatchParse): TypePatchCommandResult { + const manifest = nearestManifest(cwd); + if (manifest === null) + return { + stdout: "", + stderr: "scriptc: no package.json found from the working directory\n", + exitCode: 1, + }; + const base = dirname(manifest); + const config = parseProjectConfig(manifest, readFileSync(manifest, "utf8")); + const lines: string[] = []; + for (const d of config.diagnostics) + if (d.message.includes("typePatches")) lines.push(`warning ${d.code}: ${d.message}`); + if (config.typePatches.length === 0) { + lines.push("no type patches"); + return { stdout: `${lines.join("\n")}\n`, stderr: "", exitCode: 0 }; + } + for (const entry of config.typePatches) { + lines.push(entry.key); + const copies = installedCopies(cwd, entry.pkg); + const covered = copies.filter((c) => c.version !== null && satisfies(c.version, entry.range)); + if (copies.length === 0) lines.push(` not installed`); + else if (covered.length === 0) + lines.push( + ` does not apply to the installed ${entry.pkg} ${copies[0]!.version ?? "(no version)"}`, + ); + for (const decl of entry.declarations) { + const selector = parseSelector(decl.selector) as Selector; + const op = parseTypeText(decl.type) as TypePatchOp; + const found: FileMatch[] = []; + for (const copy of covered) { + for (const m of matchCopy(copy, selector, op, parse).matches) + found.push({ + ...m, + file: `${relative(base, copy.root).split("\\").join("/")}/${m.file}`, + }); + } + const status = + covered.length === 0 + ? "" + : found.length === 0 + ? " (matches nothing)" + : found.every((m) => m.redundant) + ? " (already declared so)" + : ""; + lines.push(` ${decl.selector}: ${decl.type}${status}`); + for (const m of found) + if (!m.redundant) lines.push(` ${m.file}:${m.line} ${m.before} -> ${m.after}`); + } + for (const [target, local] of entry.files) + lines.push(` file ${target}: ${relative(base, local).split("\\").join("/")}`); + if (entry.types !== null) + lines.push(` types: ${relative(base, entry.types).split("\\").join("/")}`); + } + return { stdout: `${lines.join("\n")}\n`, stderr: "", exitCode: 0 }; +} + +export function runTypePatchCommand( + req: TypePatchCommandRequest, + parse: TypePatchParse, +): TypePatchCommandResult { + return req.action === "patch" ? patchCommand(req, parse) : listCommand(req.cwd, parse); +} diff --git a/packages/compiler/src/frontend/type-patches/layer.ts b/packages/compiler/src/frontend/type-patches/layer.ts new file mode 100644 index 000000000..bb3b3627a --- /dev/null +++ b/packages/compiler/src/frontend/type-patches/layer.ts @@ -0,0 +1,492 @@ +/* Type patches: project-owned fixes for wrong npm declarations. + * + * `scriptc.typePatches` in the entry's package.json maps + * `"@"` to declaration patches (project-config.ts): + * + * "commander@^15.0.0": { "declarations": { "HelpContext.helpWidth": "| undefined" } } + * + * The patches are applied to file TEXT, in one place every consumer reads + * through: the program host's source reads (ts7/program-host.ts), which + * serve both checker worlds (the lowering world that types package + * JavaScript from its JSDoc, and the declaration world that types user + * code at the declaration boundary), and the npm-static passes that read + * package files themselves (`packageSourceText`). Downstream, the patched + * declaration simply IS the declaration: scriptc enforces it exactly as it + * enforced the original, so a patch can never make a check weaker than the + * declaration it replaces. + * + * A patch applies to an installed copy whose version satisfies its range; + * an `@types/` twin is matched by the runtime package's name and + * installed version. Without patches nothing here runs: `typePatchedText` + * answers undefined from one null test, so every program reads the same + * bytes as before (the zero-cost rule). + * + * See scriptc-perf/results/design-type-patches/DESIGN.md. */ + +import { dirname, relative } from "node:path"; +import type { ScrDiagnostic } from "../../diagnostics/diagnostic.js"; +import { typePatchDiag } from "../../diagnostics/diagnostic.js"; +import { readProjectConfig, type TypePatchEntry } from "../../project-config.js"; +import { trackedFileExists, trackedReadFile } from "../input-tracker.js"; +import type { SourceFile } from "../ts7/syntax.js"; +import { applyPatchEdits, patchFile, type PatchMatch, type PatchRequest } from "./locate.js"; +import { parseSelector, parseTypeText, type Selector, type TypePatchOp } from "./selector.js"; +import { satisfies } from "./semver.js"; + +export type TypePatchParse = (path: string, text: string, kind: "ts" | "js") => SourceFile; + +interface Request extends PatchRequest { + entry: TypePatchEntry; + /** Offset of the selector in the manifest. */ + offset: number; +} + +interface CopyInfo { + /** The runtime package's name (for an `@types` twin, the package it + * types). */ + pkg: string; + version: string | null; + root: string; + /** Requests whose entry's range the version satisfies. */ + requests: Request[]; + /** Replaced declaration files: package-relative path → local file. */ + files: Map; + /** Declaration entries a `types` patch supplies: package-relative path + * → local file (existing entries are replaced, missing ones appear). */ + entries: Map; +} + +interface MatchRecord extends PatchMatch { + file: string; + pkg: string; + version: string | null; +} + +interface State { + manifest: string; + /** The directory of the project's package.json. */ + projectDir: string; + manifestText: string; + parse: TypePatchParse; + byPackage: Map; + copies: Map; + files: Map; + /** `files` targets that do not exist in a copy. */ + missingFiles: { entry: TypePatchEntry; root: string; rel: string }[]; + /** Package copies whose version no patch range covers. */ + unmatchedCopies: Map; + /** Requests applicable to a copy this load read files of. */ + touched: Set; + matches: MatchRecord[]; + problems: Map; + /** Patched spans per served file (patched-text coordinates). */ + spans: Map; +} + +let state: State | null = null; + +/** Registers the project's type patches for one program load (or clears + * them). Called once per load, like configurePackageShims. */ +export function configureTypePatches(entryPath: string, parse: TypePatchParse): void { + const config = readProjectConfig(entryPath); + if (config.typePatches.length === 0 || config.manifest === null) { + state = null; + return; + } + const byPackage = new Map(); + let id = 0; + for (const entry of config.typePatches) { + const pkg = byPackage.get(entry.pkg) ?? { entries: [], requests: [] }; + byPackage.set(entry.pkg, pkg); + pkg.entries.push(entry); + for (const decl of entry.declarations) { + const selector = parseSelector(decl.selector); + const op = parseTypeText(decl.type); + if ("error" in selector || "error" in op) continue; // reported as SC8004 + pkg.requests.push({ + id: id++, + selector: selector as Selector, + op: op as TypePatchOp, + entry, + offset: decl.offset, + }); + } + } + state = { + manifest: config.manifest, + projectDir: dirname(config.manifest), + manifestText: config.manifestText ?? "", + parse, + byPackage, + copies: new Map(), + files: new Map(), + missingFiles: [], + unmatchedCopies: new Map(), + touched: new Set(), + matches: [], + problems: new Map(), + spans: new Map(), + }; +} + +/** True while the current load has type patches. */ +export function typePatchesActive(): boolean { + return state !== null; +} + +const PATCHABLE = /\.(?:[cm]?js|d\.[cm]?ts|[cm]?ts)$/; + +/** `@types/foo__bar` types `@foo/bar`. */ +function unmangleTypesName(name: string): string { + const bare = name.slice("@types/".length); + const i = bare.indexOf("__"); + return i > 0 ? `@${bare.slice(0, i)}/${bare.slice(i + 2)}` : bare; +} + +function versionAt(dir: string): string | null { + const text = trackedReadFile(`${dir}/package.json`); + if (text === null) return null; + try { + const parsed = JSON.parse(text) as { version?: unknown }; + return typeof parsed.version === "string" ? parsed.version : null; + } catch { + return null; + } +} + +/** The version of the runtime package `pkg` an `@types` twin under + * `nodeModules` describes: the copy installed beside it, else the one the + * project resolves (pnpm keeps each package in its own directory, so a + * twin has no runtime package beside it), else null. */ +function twinRuntimeVersion(s: State, nodeModules: string, pkg: string): string | null { + const beside = versionAt(nodeModules + pkg); + if (beside !== null) return beside; + for (let dir = s.projectDir; ;) { + const version = versionAt(`${dir}/node_modules/${pkg}`); + if (version !== null) return version; + const parent = dirname(dir); + if (parent === dir) return null; + dir = parent; + } +} + +/** The installed package copy a file belongs to, with the requests that + * apply to it; null for files outside node_modules or of unpatched + * packages. */ +function copyOf(s: State, path: string, touch = true): { copy: CopyInfo; rel: string } | null { + const marker = "/node_modules/"; + const at = path.lastIndexOf(marker); + if (at < 0) return null; + const after = path.slice(at + marker.length); + const parts = after.split("/"); + const nameParts = parts[0]!.startsWith("@") ? 2 : 1; + if (parts.length <= nameParts) return null; + const name = parts.slice(0, nameParts).join("/"); + const root = path.slice(0, at + marker.length) + name; + const rel = parts.slice(nameParts).join("/"); + let copy = s.copies.get(root); + if (copy === undefined) { + copy = null; + const twin = name.startsWith("@types/"); + const pkg = twin ? unmangleTypesName(name) : name; + const patches = s.byPackage.get(pkg); + if (patches !== undefined) { + // A twin is matched by the runtime package's version, never its own. + const version = twin + ? twinRuntimeVersion(s, path.slice(0, at + marker.length), pkg) + : versionAt(root); + const applicable = patches.entries.filter( + (e) => version !== null && satisfies(version, e.range), + ); + if (applicable.length === 0) { + s.unmatchedCopies.set(root, { pkg, version, entries: patches.entries }); + } else { + // Later entries win on the same selector, file or entry. + const bySelector = new Map(); + for (const r of patches.requests) + if (applicable.includes(r.entry)) bySelector.set(r.selector.text, r); + const files = new Map(); + let types: string | null = null; + const prefix = `${name}/`; + for (const e of applicable) { + for (const [target, local] of e.files) { + // `@types//` names the twin's file; plain paths the + // package's own. + const forTwin = target.startsWith("@types/"); + if (forTwin !== twin) continue; + const rel = forTwin + ? target.startsWith(prefix) + ? target.slice(prefix.length) + : null + : target; + if (rel === null) continue; + if (!trackedFileExists(`${root}/${rel}`)) { + s.missingFiles.push({ entry: e, root, rel }); + continue; + } + files.set(rel, local); + } + if (e.types !== null && !twin) types = e.types; + } + copy = { + pkg, + version, + root, + requests: [...bySelector.values()], + files, + entries: types === null ? new Map() : declarationEntries(root, types), + }; + } + } + s.copies.set(root, copy); + } + if (copy === null) return null; + if (touch) for (const r of copy.requests) s.touched.add(r); + return { copy, rel }; +} + +/** The declaration entry paths of a package a `types` patch stands for: + * its `types`/`typings` file and every declaration TypeScript probes + * beside a JavaScript entry (`main`, the `exports` targets of "."). */ +function declarationEntries(root: string, local: string): Map { + const out = new Map(); + let pkg: Record = {}; + try { + pkg = JSON.parse(trackedReadFile(`${root}/package.json`) ?? "{}") as Record; + } catch { + pkg = {}; + } + const add = (target: unknown): void => { + if (typeof target !== "string") return; + const rel = target.replace(/^\.\//, ""); + if (/\.d\.[cm]?ts$/.test(rel)) out.set(rel, local); + else if (/\.[cm]?js$/.test(rel)) out.set(rel.replace(/\.([cm]?)js$/, ".d.$1ts"), local); + // TypeScript probes x.tsx and then x.d.ts for x.jsx. + else if (rel.endsWith(".jsx")) out.set(`${rel.slice(0, -".jsx".length)}.d.ts`, local); + else if (!/\.\w+$/.test(rel)) out.set(`${rel}.d.ts`, local); + }; + add(pkg["types"]); + add(pkg["typings"]); + add(typeof pkg["main"] === "string" ? pkg["main"] : "index.js"); + const exportsField = pkg["exports"]; + const dot = + exportsField !== null && typeof exportsField === "object" && "." in exportsField + ? (exportsField as Record)["."] + : exportsField; + const visit = (v: unknown): void => { + if (typeof v === "string") add(v); + else if (v !== null && typeof v === "object") for (const x of Object.values(v)) visit(x); + }; + // Subpath exports ("./x") are not the entry. + if (dot !== null && typeof dot === "object" && Object.keys(dot).some((k) => k.startsWith("."))) + return out; + visit(dot); + return out; +} + +/** True for a declaration entry a `types` patch supplies that does not + * exist on disk (a package without declarations gains one). */ +export function typePatchFileExists(fileName: string): boolean { + const s = state; + if (s === null) return false; + const path = fileName.split("\\").join("/"); + if (!/\.d\.[cm]?ts$/.test(path)) return false; + const located = copyOf(s, path, false); + return located !== null && located.copy.entries.has(located.rel); +} + +/** True when a `types` patch gives the package at `pkgDir` declarations. */ +export function typePatchSuppliesTypes(pkgDir: string): boolean { + const s = state; + if (s === null) return false; + const located = copyOf(s, `${pkgDir.split("\\").join("/")}/package.json`, false); + return located !== null && located.copy.entries.size > 0; +} + +/** Names a request's root can appear under in a file's text. */ +function rootWords(sel: Selector): string[] { + if (sel.root === "export=") return ["exports", "export"]; + if (sel.root === "default") return ["default"]; + return [sel.root]; +} + +/** The patched text of a package file, or undefined when no patch applies + * to it (callers then read the file as usual). */ +export function typePatchedText(fileName: string): string | undefined { + const s = state; + if (s === null) return undefined; + const path = fileName.split("\\").join("/"); + if (!PATCHABLE.test(path)) return undefined; + const cached = s.files.get(path); + if (cached !== undefined) return cached ?? undefined; + let result: string | null = null; + const located = copyOf(s, path); + if (located !== null) { + // A replaced file or a supplied entry reads as the local file; the + // declaration patches then apply to it like to the package's own. + const local = located.copy.files.get(located.rel) ?? located.copy.entries.get(located.rel); + const replaced = local === undefined ? null : trackedReadFile(local); + const text = replaced ?? trackedReadFile(path); + if (replaced !== null) result = replaced; + const requests = located.copy.requests.filter( + (r) => + (r.selector.file === null || r.selector.file === located.rel) && + rootWords(r.selector).some((w) => text?.includes(w)), + ); + if (text !== null && requests.length > 0) { + const kind = /\.[cm]?ts$/.test(path) ? "ts" : "js"; + let out: ReturnType | null = null; + try { + out = patchFile(located.rel, text, s.parse(path, text, kind), requests); + } catch { + out = null; // unparseable: served unpatched, like other npm-static passes + } + if (out !== null) { + const byId = new Map(requests.map((r) => [r.id, r])); + for (const m of out.matches) { + s.matches.push({ + ...m, + file: path, + pkg: located.copy.pkg, + version: located.copy.version, + }); + } + for (const p of out.problems) { + const r = byId.get(p.id); + const prior = r === undefined ? undefined : s.problems.get(r); + if (r !== undefined && (prior === undefined || (p.always === true && !prior.always))) + s.problems.set(r, { + message: `${p.message} (${located.rel})`, + always: p.always === true, + }); + } + if (out.edits.length > 0) { + result = applyPatchEdits(text, out.edits); + s.spans.set(path, patchedSpans(out.edits)); + } + } + } + } + s.files.set(path, result); + return result ?? undefined; +} + +/** Edit spans in the patched text's coordinates. */ +function patchedSpans( + edits: readonly { start: number; end: number; text: string }[], +): { start: number; end: number }[] { + const sorted = [...edits].sort((a, b) => a.start - b.start || a.end - b.end); + const out: { start: number; end: number }[] = []; + let shift = 0; + for (const e of sorted) { + const start = e.start + shift; + out.push({ start, end: start + e.text.length }); + shift += e.text.length - (e.end - e.start); + } + return out; +} + +/** Text a package file is read as by passes that read package files + * themselves (npm-static): the patched text, or the file. */ +export function packageSourceText(path: string): string | null { + return typePatchedText(path) ?? trackedReadFile(path); +} + +/** The spans a type patch edited in a served file (patched-text offsets), + * for passes that rewrite package JSDoc and must keep patched comments + * (the shape overlay). Empty when nothing was patched. */ +export function typePatchedSpans(fileName: string): readonly { start: number; end: number }[] { + return state?.spans.get(fileName.split("\\").join("/")) ?? []; +} + +/** Every applied match of the current load (for `scriptc types patch + * --list` and tests). */ +export function typePatchMatches(): readonly MatchRecord[] { + return state?.matches ?? []; +} + +function loc(s: State, offset: number): { file: string; start: number; end: number } { + return { file: s.manifest, start: offset, end: offset }; +} + +/** The current load's type patch warnings: SC8030 (no range covers an + * installed copy), SC8031 (a selector matched nothing), SC8032 (redundant: + * the declaration already has the type), SC8033 (ambiguous or not + * patchable as written). Only packages the program loads are reported. */ +export function typePatchDiagnostics(): { + diagnostics: ScrDiagnostic[]; + manifest: string; + manifestText: string; +} | null { + const s = state; + if (s === null) return null; + const out: ScrDiagnostic[] = []; + for (const [root, info] of s.unmatchedCopies) { + const shown = info.version ?? "an unknown version"; + for (const entry of info.entries) { + out.push( + typePatchDiag( + "SC8030", + `type patch '${entry.key}' does not apply to ${info.pkg} ${shown} at ${relativeToManifest(s, root)}; its declarations are used unpatched`, + info.version === null + ? "the installed package has no version" + : `check whether the declarations of ${info.pkg} ${info.version} still need the patch, then widen the range or remove it`, + loc(s, entry.offset), + ), + ); + } + } + for (const missing of s.missingFiles) { + out.push( + typePatchDiag( + "SC8031", + `type patch '${missing.entry.key}': ${missing.rel} is not a file of ${missing.entry.pkg} at ${relativeToManifest(s, missing.root)}`, + "name a declaration file the installed package ships; the package may have moved it", + loc(s, missing.entry.offset), + ), + ); + } + for (const r of s.touched) { + const mine = s.matches.filter((m) => m.id === r.id); + // A selector that matched somewhere (the .d.ts) is not a problem where + // it cannot apply (undocumented JavaScript), unless a declaration it + // names was left unpatched. + const problem = s.problems.get(r); + if (problem !== undefined && (mine.length === 0 || problem.always)) { + out.push( + typePatchDiag( + "SC8033", + `type patch '${r.entry.key}': ${problem.message}`, + "name one declaration, or patch the declared type it refers to", + loc(s, r.offset), + ), + ); + continue; + } + if (mine.length === 0) { + out.push( + typePatchDiag( + "SC8031", + `type patch '${r.entry.key}': '${r.selector.text}' matches no declaration in the files of ${r.entry.pkg} this program loads`, + "check the spelling against the package's declarations and JSDoc; the package may have renamed it", + loc(s, r.offset), + ), + ); + continue; + } + if (mine.every((m) => m.redundant)) + out.push( + typePatchDiag( + "SC8032", + `type patch '${r.entry.key}': '${r.selector.text}' is already declared as ${mine[0]!.before}`, + "the package's declarations already say this; remove the patch", + loc(s, r.offset), + ), + ); + } + return { diagnostics: out, manifest: s.manifest, manifestText: s.manifestText }; +} + +function relativeToManifest(s: State, path: string): string { + return relative(dirname(s.manifest), path).split("\\").join("/"); +} diff --git a/packages/compiler/src/frontend/type-patches/locate.test.ts b/packages/compiler/src/frontend/type-patches/locate.test.ts new file mode 100644 index 000000000..d43695d9a --- /dev/null +++ b/packages/compiler/src/frontend/type-patches/locate.test.ts @@ -0,0 +1,201 @@ +import { describe, expect, test } from "vitest"; +import { nodeFrontendServices } from "../services-node.js"; +import { applyPatchEdits, patchFile, type PatchRequest } from "./locate.js"; +import { parseSelector, parseTypeText, type Selector, type TypePatchOp } from "./selector.js"; + +function patch(file: string, text: string, patches: Record) { + const requests: PatchRequest[] = Object.entries(patches).map(([sel, type], id) => ({ + id, + selector: parseSelector(sel) as Selector, + op: parseTypeText(type) as TypePatchOp, + })); + const kind = /\.[cm]?ts$/.test(file) ? "ts" : "js"; + const sf = nodeFrontendServices().parse(`/pkg/${file}`, text, kind); + const result = patchFile(file, text, sf, requests); + const out = applyPatchEdits(text, result.edits); + expect(out.split("\n").length).toBe(text.split("\n").length); + return { ...result, text: out }; +} + +const COMMANDER_LIKE = `class Command { + /** + * @typedef HelpContext + * @type {object} + * @property {boolean} error + * @property {number} helpWidth + * @property {function} write - includes stripColor if needed + * + * @returns {HelpContext} + * @private + */ + _getOutputContext(contextOptions) { + let helpWidth; + return { error: false, write: () => {}, helpWidth }; + } +} +module.exports = { Command }; +`; + +const MS_LIKE = `/** + * @param {String|Number} val + * @param {Object} [options] + * @return {String|Number} + */ +module.exports = function (val, options) { + return parse(val); +}; + +/** + * @param {String} str + * @return {Number} + */ +function parse(str) { + if (str.length > 100) { + return; + } + return 1; +} +`; + +describe("JSDoc", () => { + test("a @typedef property widens", () => { + const r = patch("lib/command.js", COMMANDER_LIKE, { "HelpContext.helpWidth": "| undefined" }); + expect(r.text).toContain("@property {number | undefined} helpWidth"); + expect(r.text).toContain("@returns {HelpContext}"); + expect(r.matches).toMatchObject([ + { line: 6, before: "number", after: "number | undefined", redundant: false }, + ]); + }); + + test("returns of export= and of a file-local function widen", () => { + const r = patch("index.js", MS_LIKE, { + "export=()": "| undefined", + "index.js:parse()": "| undefined", + }); + expect(r.text).toContain("@return {String|Number | undefined}\n */\nmodule.exports"); + expect(r.text).toContain("@return {Number | undefined}\n */\nfunction parse"); + expect(r.problems).toEqual([]); + }); + + test("a file prefix restricts the root to that file", () => { + const r = patch("other.js", MS_LIKE, { "index.js:parse()": "| undefined" }); + expect(r.matches).toEqual([]); + expect(r.text).toBe(MS_LIKE); + }); + + test("parameters, optional parameters and redundancy", () => { + const r = patch("index.js", MS_LIKE, { + "export=(options)": "| undefined", + "parse(str)": "string | undefined", + }); + expect(r.matches[0]).toMatchObject({ redundant: true }); + expect(r.text).toContain("@param {string | undefined} str"); + }); + + test("a replacement on an undocumented declaration inserts a tag", () => { + const text = + "/** Adds. */\nfunction add(a, b) { return a + b; }\nfunction sub(a, b) { return a - b; }\n"; + const r = patch("index.js", text, { "add()": "number", "sub(a)": "number" }); + expect(r.text).toBe( + "/** Adds. @returns {number} */\nfunction add(a, b) { return a + b; }\n/** @param {number} a */ function sub(a, b) { return a - b; }\n", + ); + }); + + test("a widening needs a written type", () => { + const r = patch("index.js", "function f() { return 1; }\n", { "f()": "| undefined" }); + expect(r.problems[0]!.message).toContain("no written type to widen"); + }); + + test("members of exported objects, classes and constructor assignments", () => { + const text = `class Box { + constructor() { + /** @type {string} */ + this.label = ""; + } + /** @returns {number} */ + size() { return 1; } +} +module.exports = { + /** @returns {string} */ + name() { return ""; }, + Box, +}; +`; + const r = patch("index.js", text, { + "Box.label": "| undefined", + "Box.size()": "| null", + "export=.name()": "| undefined", + }); + expect(r.text).toContain("@type {string | undefined} */\n this.label"); + expect(r.text).toContain("@returns {number | null} */\n size()"); + expect(r.text).toContain("@returns {string | undefined} */\n name()"); + }); + + test("function types are parenthesized before widening", () => { + const r = patch("index.js", "/** @type {(a: string) => void} */\nvar cb = null;\n", { + cb: "| undefined", + }); + expect(r.text).toContain("@type {((a: string) => void) | undefined}"); + }); +}); + +describe("declaration files", () => { + const DTS = `export interface Options { + name: string; + retries?: number; + env: Env; +} +export interface Env { + [key: string]: string; +} +export declare function fetchUser(id: string): Promise<{ email: string; id: string }>; +export declare function load(): User; +interface User { name: string; tags: string[] } +declare function ms(value: string): number; +declare function ms(value: number): string; +declare namespace ms { const version: string; } +export = ms; +`; + + test("members, optional members, index signatures and navigation", () => { + const r = patch("index.d.ts", DTS, { + "Options.name": "| undefined", + "Options.retries": "| undefined", + "Env[string]": "| undefined", + "fetchUser().email": "string | null", + "load().tags[]": "| undefined", + "Options.env[string]": "| null", + }); + expect(r.text).toContain(" name: string | undefined;"); + expect(r.text).toContain("[key: string]: string | undefined | null;"); + expect(r.text).toContain("Promise<{ email: string | null; id: string }>"); + expect(r.text).toContain("tags: (string | undefined)[]"); + expect(r.matches.find((m) => m.id === 1)).toMatchObject({ redundant: true }); + }); + + test("export= widens every overload, and a replacement of several is refused", () => { + const r = patch("index.d.ts", DTS, { "export=()": "| undefined" }); + expect(r.text).toContain("declare function ms(value: string): number | undefined;"); + expect(r.text).toContain("declare function ms(value: number): string | undefined;"); + const refused = patch("index.d.ts", DTS, { "export=()": "number" }); + expect(refused.problems[0]!.message).toContain("matches 2 declarations"); + expect(refused.text).toBe(DTS); + }); + + test("namespace members and generic types", () => { + const r = patch("index.d.ts", DTS, { "ms.version": "| undefined" }); + expect(r.text).toContain("const version: string | undefined;"); + const generic = patch( + "index.d.ts", + "export declare function f(): Map;\n", + { "f().a": "| undefined" }, + ); + expect(generic.problems[0]!.message).toContain("cannot follow"); + }); + + test("multi-line types keep their line count when replaced", () => { + const text = "export interface A {\n b: {\n c: number;\n };\n}\n"; + const r = patch("index.d.ts", text, { "A.b": "string" }); + expect(r.text.startsWith("export interface A {\n b: string\n\n;")).toBe(true); + }); +}); diff --git a/packages/compiler/src/frontend/type-patches/locate.ts b/packages/compiler/src/frontend/type-patches/locate.ts new file mode 100644 index 000000000..6c762f5c3 --- /dev/null +++ b/packages/compiler/src/frontend/type-patches/locate.ts @@ -0,0 +1,970 @@ +/* Finds the declarations a type patch selector names in one package file + * and computes the text edits that apply the patch. + * + * Declaration files (and TypeScript sources) are matched on their syntax + * tree: the edit replaces or widens a written type node. JavaScript is + * matched on its JSDoc: the edit replaces or widens the `{...}` of a + * `@typedef`'s `@property`, a `@returns`, a `@param` or a `@type` tag. + * Executable code is never edited, and no edit adds or removes a line + * break, so every later pass and diagnostic keeps its line. */ + +import * as ts from "../ts7/syntax.js"; +import { tokenStartOfNode } from "../ts7/node-functions.js"; +import { + normalizeType, + splitUnion, + type Selector, + type SelectorSegment, + type TypePatchOp, +} from "./selector.js"; + +export interface PatchRequest { + /** Caller's id, echoed in matches and problems. */ + id: number; + selector: Selector; + op: TypePatchOp; +} + +export interface PatchMatch { + id: number; + /** 1-based line of the patched type. */ + line: number; + before: string; + after: string; + /** The declaration already has the patched type: nothing was edited. */ + redundant: boolean; +} + +export interface PatchProblem { + id: number; + message: string; + /** Reported even when the selector matched elsewhere (an ambiguous + * replacement left a declaration unpatched). */ + always?: true; +} + +export interface PatchEdit { + start: number; + end: number; + text: string; +} + +export interface FilePatchResult { + edits: PatchEdit[]; + matches: PatchMatch[]; + problems: PatchProblem[]; +} + +/** A place a type is (or can be) written. */ +interface Slot { + /** The written type's span, or null when there is none. */ + span: { start: number; end: number } | null; + /** The declaration is optional (`x?:`, `[x]`), so it already admits + * undefined. */ + optional: boolean; + /** Where a replacement goes when nothing is written: the inserted text + * is `prefix + type + suffix`. */ + insert: { at: number; prefix: string; suffix: string } | null; + /** A JSDoc slot inside a comment (one-line edits only). */ + jsdoc: boolean; + /** The type is an array element (`T[]`): a union must be wrapped. */ + wrap?: boolean; +} + +type Step = { slots: Slot[] } | { problem: string }; + +/** Type texts that need parentheses before `| T` is appended. */ +function needsParens(text: string): boolean { + return /=>|\?|\bextends\b|^\s*new\b|^\s*function\b|^\s*!/.test(text); +} + +function lineOf(text: string, pos: number): number { + let line = 1; + for (let i = 0; i < pos && i < text.length; i++) if (text.charCodeAt(i) === 10) line++; + return line; +} + +function editsFor( + text: string, + req: PatchRequest, + slot: Slot, +): { edits: PatchEdit[]; match: PatchMatch } | { problem: string } { + const op = req.op; + if (slot.span === null) { + if (op.kind === "widen" || slot.insert === null) + return { + problem: `'${req.selector.text}' has no written type to widen; give the whole type instead`, + }; + const ins = slot.insert; + return { + edits: [{ start: ins.at, end: ins.at, text: ins.prefix + op.text + ins.suffix }], + match: { + id: req.id, + line: lineOf(text, ins.at), + before: "(none)", + after: op.text, + redundant: false, + }, + }; + } + const { start, end } = slot.span; + const original = text.slice(start, end); + const line = lineOf(text, start); + if (op.kind === "widen") { + const present = new Set(splitUnion(original).map(normalizeType)); + if (slot.optional) present.add("undefined"); + const add = op.arms.filter((a) => !present.has(normalizeType(a))); + if (add.length === 0) + return { + edits: [], + match: { id: req.id, line, before: original, after: original, redundant: true }, + }; + // `(() => void) | undefined`; an array element wraps the whole union: + // `(string | undefined)[]`. + const inner = needsParens(original); + const outer = slot.wrap === true; + const suffix = ` | ${add.join(" | ")}`; + const open = (outer ? "(" : "") + (inner ? "(" : ""); + const close = (inner ? ")" : "") + suffix + (outer ? ")" : ""); + const edits: PatchEdit[] = + open === "" + ? [{ start: end, end, text: close }] + : [ + { start, end: start, text: open }, + { start: end, end, text: close }, + ]; + const after = `${open}${original}${close}`; + return { edits, match: { id: req.id, line, before: original, after, redundant: false } }; + } + if (normalizeType(original) === normalizeType(op.text)) + return { + edits: [], + match: { id: req.id, line, before: original, after: original, redundant: true }, + }; + const breaks = original.split("\n").length - 1; + if (breaks > 0 && slot.jsdoc) + return { + problem: `'${req.selector.text}' names a JSDoc type that spans lines; replace it in one line upstream or widen it`, + }; + const replacement = + slot.wrap === true && (splitUnion(op.text).length > 1 || needsParens(op.text)) + ? `(${op.text})` + : op.text; + return { + edits: [{ start, end, text: replacement + "\n".repeat(breaks) }], + match: { id: req.id, line, before: original, after: replacement, redundant: false }, + }; +} + +/** Applies a file's requests. `sf` is the file's syntax tree, parsed as + * TypeScript for declaration files and TypeScript sources and as + * JavaScript otherwise. */ +export function patchFile( + relPath: string, + text: string, + sf: ts.SourceFile, + requests: readonly PatchRequest[], +): FilePatchResult { + const js = !/\.(d\.)?[mc]?ts$/.test(relPath); + const resolver = js ? new JsResolver(text, sf) : new TsResolver(text, sf); + const result: FilePatchResult = { edits: [], matches: [], problems: [] }; + for (const req of requests) { + if (req.selector.file !== null && req.selector.file !== relPath) continue; + const step = resolver.resolve(req.selector); + if ("problem" in step) { + result.problems.push({ id: req.id, message: step.problem }); + continue; + } + if (step.slots.length === 0) continue; + if (req.op.kind === "replace" && step.slots.length > 1) { + result.problems.push({ + id: req.id, + message: `'${req.selector.text}' matches ${step.slots.length} declarations in ${relPath}; a replacement must name one (widen them, or replace the declaration file)`, + always: true, + }); + continue; + } + for (const slot of step.slots) { + const out = editsFor(text, req, slot); + if ("problem" in out) { + result.problems.push({ id: req.id, message: out.problem }); + continue; + } + result.edits.push(...out.edits); + result.matches.push(out.match); + } + } + return result; +} + +/** Applies edits (sorted by position; at one position, in request order). + * An edit overlapping an earlier replacement is dropped. */ +export function applyPatchEdits(text: string, edits: readonly PatchEdit[]): string { + const sorted = edits + .map((e, i) => ({ e, i })) + .sort((a, b) => a.e.start - b.e.start || a.e.end - b.e.end || a.i - b.i) + .map((x) => x.e); + let out = ""; + let at = 0; + for (const e of sorted) { + if (e.start < at) continue; + out += text.slice(at, e.start) + e.text; + at = e.end; + } + return out + text.slice(at); +} + +function nameText(name: ts.Node | undefined): string | null { + if (name === undefined) return null; + if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isPrivateIdentifier(name)) + return name.text; + if (ts.isNumericLiteral(name)) return name.text; + return null; +} + +function hasDefaultModifier(node: ts.Node): boolean { + return (ts.getModifiers(node) ?? []).some((m) => m.kind === ts.SyntaxKind.DefaultKeyword); +} + +/* ── declaration files ─────────────────────────────────────────────── */ + +type TsPos = { node: ts.Node; optional: boolean; element?: boolean }; + +class TsResolver { + /** Top-level declarations by name, ambient module bodies included. */ + private readonly roots = new Map(); + private exportEquals: string | null = null; + private exportDefault: ts.Node[] = []; + + constructor( + private readonly text: string, + sf: ts.SourceFile, + ) { + this.collect(sf.statements.nodes, true); + } + + private collect(statements: readonly ts.Statement[], top: boolean): void { + for (const s of statements) { + if (ts.isExportAssignment(s)) { + if (ts.isIdentifier(s.expression)) { + if (s.isExportEquals) this.exportEquals = s.expression.text; + else this.exportDefault.push(...this.rootsNamed(s.expression.text)); + } + continue; + } + if (ts.isModuleDeclaration(s) && ts.isStringLiteral(s.name)) { + // `declare module "pkg" { ... }`: its statements are the module's. + if (top && s.body !== undefined && ts.isModuleBlock(s.body)) + this.collect(s.body.statements.nodes, false); + continue; + } + if (ts.isVariableStatement(s)) { + for (const d of s.declarationList.declarations.nodes) this.add(nameText(d.name), d); + continue; + } + const named = + ts.isFunctionDeclaration(s) || + ts.isClassDeclaration(s) || + ts.isInterfaceDeclaration(s) || + ts.isTypeAliasDeclaration(s) || + ts.isModuleDeclaration(s) || + ts.isEnumDeclaration(s); + if (!named) continue; + this.add(nameText((s as ts.FunctionDeclaration).name), s); + if ((ts.isFunctionDeclaration(s) || ts.isClassDeclaration(s)) && hasDefaultModifier(s)) + this.exportDefault.push(s); + } + } + + private add(name: string | null, node: ts.Node): void { + if (name === null) return; + const list = this.roots.get(name); + if (list === undefined) this.roots.set(name, [node]); + else list.push(node); + } + + private rootsNamed(name: string): ts.Node[] { + return this.roots.get(name) ?? []; + } + + resolve(sel: Selector): Step { + let start: ts.Node[]; + if (sel.root === "export=") + start = this.exportEquals === null ? [] : this.rootsNamed(this.exportEquals); + else if (sel.root === "default") start = this.exportDefault; + else start = this.rootsNamed(sel.root); + let positions: TsPos[] = start.map((node) => ({ node, optional: false })); + // A position that cannot continue is a problem only when no other + // position of the same name matches (a function merged with a + // namespace, an export and its declaration). + let problem: string | null = null; + for (const seg of sel.segments) { + const next: TsPos[] = []; + for (const pos of positions) { + const step = this.step(pos, seg); + if (typeof step === "string") problem ??= step; + else next.push(...step); + } + positions = next; + if (positions.length === 0) + return problem === null ? { slots: [] } : { problem: `'${sel.text}' ${problem}` }; + } + const slots: Slot[] = []; + for (const pos of positions) { + const slot = this.slotOf(pos); + if (typeof slot === "string") problem ??= slot; + else slots.push(slot); + } + if (slots.length === 0 && problem !== null) return { problem: `'${sel.text}' ${problem}` }; + return { slots }; + } + + private typeSlot(type: ts.TypeNode | undefined, optional: boolean): Slot { + return { + span: type === undefined ? null : { start: tokenStartOfNode(type), end: type.end }, + optional, + insert: null, + jsdoc: false, + }; + } + + private slotOf(pos: TsPos): Slot | string { + const n = pos.node; + if (ts.isTypeNode(n)) return { ...this.typeSlot(n, pos.optional), wrap: pos.element === true }; + if (ts.isPropertySignature(n) || ts.isPropertyDeclaration(n)) + return this.typeSlot( + n.type, + pos.optional || n.postfixToken?.kind === ts.SyntaxKind.QuestionToken, + ); + if ( + ts.isVariableDeclaration(n) || + ts.isTypeAliasDeclaration(n) || + ts.isIndexSignatureDeclaration(n) + ) + return this.typeSlot(n.type, false); + if (ts.isParameter(n)) + return this.typeSlot(n.type, n.questionToken !== undefined || n.initializer !== undefined); + if (ts.isGetAccessor(n)) return this.typeSlot(n.type, false); + if (this.isFunctionLike(n)) + return "names a function; add '()' for its return type or '(name)' for a parameter"; + return "names a declaration without a type of its own; name one of its members"; + } + + private isFunctionLike(n: ts.Node): boolean { + return ( + ts.isFunctionDeclaration(n) || + ts.isMethodSignatureDeclaration(n) || + ts.isMethodDeclaration(n) || + ts.isCallSignatureDeclaration(n) || + ts.isFunctionTypeNode(n) + ); + } + + /** The type a position stands for when a segment navigates into it. */ + private typeOf(n: ts.Node): ts.TypeNode | undefined { + if ( + ts.isPropertySignature(n) || + ts.isPropertyDeclaration(n) || + ts.isVariableDeclaration(n) || + ts.isTypeAliasDeclaration(n) || + ts.isParameter(n) + ) + return n.type; + return undefined; + } + + /** Object-like members a type node holds, following parentheses, unions, + * intersections, `Promise` and references to declarations in this + * file. Null when it passes through a type it cannot follow. */ + private containers(type: ts.TypeNode, depth = 0): ts.Node[] | null { + if (depth > 8) return null; + if (ts.isTypeLiteralNode(type)) return [type]; + if (ts.isParenthesizedTypeNode(type)) return this.containers(type.type, depth + 1); + if (ts.isUnionTypeNode(type) || ts.isIntersectionTypeNode(type)) { + const out: ts.Node[] = []; + for (const arm of type.types.nodes) { + if (arm.kind === ts.SyntaxKind.LiteralType || this.isKeywordType(arm)) continue; + const c = this.containers(arm, depth + 1); + if (c === null) return null; + out.push(...c); + } + return out; + } + if (ts.isTypeReferenceNode(type) && ts.isIdentifier(type.typeName)) { + const name = type.typeName.text; + const args = type.typeArguments?.nodes ?? []; + if ((name === "Promise" || name === "PromiseLike") && args.length === 1) + return this.containers(args[0]!, depth + 1); + if (args.length > 0) return null; + const decls = this.rootsNamed(name); + if (decls.length === 0) return null; + const out: ts.Node[] = []; + for (const d of decls) { + if (ts.isTypeAliasDeclaration(d)) { + const c = this.containers(d.type, depth + 1); + if (c === null) return null; + out.push(...c); + } else out.push(d); + } + return out; + } + return null; + } + + private isKeywordType(n: ts.Node): boolean { + return ( + n.kind === ts.SyntaxKind.NullKeyword || + n.kind === ts.SyntaxKind.UndefinedKeyword || + n.kind === ts.SyntaxKind.StringKeyword || + n.kind === ts.SyntaxKind.NumberKeyword || + n.kind === ts.SyntaxKind.BooleanKeyword + ); + } + + private membersOf(n: ts.Node): readonly ts.Node[] | null { + if (ts.isInterfaceDeclaration(n) || ts.isTypeLiteralNode(n) || ts.isClassDeclaration(n)) + return n.members.nodes; + if (ts.isModuleDeclaration(n)) + return n.body !== undefined && ts.isModuleBlock(n.body) ? n.body.statements.nodes : []; + return null; + } + + /** Expands a position into the object-like nodes that hold members. */ + private holders(pos: TsPos): ts.Node[] | string { + const n = pos.node; + if (this.membersOf(n) !== null) return [n]; + if (ts.isTypeAliasDeclaration(n) || ts.isTypeNode(n) || this.typeOf(n) !== undefined) { + const type = ts.isTypeNode(n) ? n : this.typeOf(n)!; + const c = this.containers(type); + if (c === null) + return "passes through a type it cannot follow (a generic, mapped, conditional or imported type); name the declared type itself"; + return c; + } + if (ts.isFunctionDeclaration(n) || ts.isEnumDeclaration(n)) return []; + return "has a member segment after a declaration without members"; + } + + private signaturesOf(pos: TsPos): ts.Node[] | string { + const n = pos.node; + if (this.isFunctionLike(n)) return [n]; + if (ts.isInterfaceDeclaration(n) || ts.isTypeLiteralNode(n)) + return n.members.nodes.filter((m) => ts.isCallSignatureDeclaration(m)); + const type = ts.isTypeNode(n) ? n : this.typeOf(n); + if (type !== undefined) { + let t: ts.TypeNode = type; + while (ts.isParenthesizedTypeNode(t)) t = t.type; + if (ts.isFunctionTypeNode(t)) return [t]; + const c = this.containers(t); + if (c === null) + return "passes through a type it cannot follow; name the declared type itself"; + return c.flatMap((h) => + ts.isInterfaceDeclaration(h) || ts.isTypeLiteralNode(h) + ? h.members.nodes.filter((m) => ts.isCallSignatureDeclaration(m)) + : [], + ); + } + if (ts.isModuleDeclaration(n) || ts.isClassDeclaration(n)) return []; + return "has '()' after a declaration that is not a function"; + } + + private step(pos: TsPos, seg: SelectorSegment): TsPos[] | string { + switch (seg.kind) { + case "member": { + const holders = this.holders(pos); + if (typeof holders === "string") return holders; + const out: TsPos[] = []; + for (const h of holders) { + const members = this.membersOf(h) ?? []; + for (const m of members) { + if (ts.isVariableStatement(m)) { + for (const d of m.declarationList.declarations.nodes) + if (nameText(d.name) === seg.name) out.push({ node: d, optional: false }); + continue; + } + if (nameText((m as ts.PropertySignatureDeclaration).name) === seg.name) + out.push({ node: m, optional: false }); + } + } + return out; + } + case "return": + case "param": { + const sigs = this.signaturesOf(pos); + if (typeof sigs === "string") return sigs; + const out: TsPos[] = []; + for (const s of sigs) { + const sig = s as ts.FunctionDeclaration; + if (seg.kind === "return") { + if (sig.type !== undefined) out.push({ node: sig.type, optional: false }); + else return "has no written return type"; + } else { + for (const p of sig.parameters.nodes) + if (nameText(p.name) === seg.name) out.push({ node: p, optional: false }); + } + } + return out; + } + case "element": { + let t: ts.TypeNode | undefined = ts.isTypeNode(pos.node) ? pos.node : this.typeOf(pos.node); + while (t !== undefined && ts.isParenthesizedTypeNode(t)) t = t.type; + if (t !== undefined && ts.isArrayTypeNode(t)) + return [{ node: t.elementType, optional: false, element: true }]; + if ( + t !== undefined && + ts.isTypeReferenceNode(t) && + ts.isIdentifier(t.typeName) && + (t.typeName.text === "Array" || t.typeName.text === "ReadonlyArray") && + t.typeArguments?.nodes.length === 1 + ) + return [{ node: t.typeArguments.nodes[0]!, optional: false }]; + return "has '[]' after a type that is not an array"; + } + case "index": { + const holders = this.holders(pos); + if (typeof holders === "string") return holders; + const want = + seg.key === "string" ? ts.SyntaxKind.StringKeyword : ts.SyntaxKind.NumberKeyword; + const out: TsPos[] = []; + for (const h of holders) + for (const m of this.membersOf(h) ?? []) + if (ts.isIndexSignatureDeclaration(m) && m.parameters.nodes[0]?.type?.kind === want) + out.push({ node: m, optional: false }); + return out; + } + } + } +} + +/* ── JSDoc in JavaScript ───────────────────────────────────────────── */ + +interface JsDocTag { + tag: string; + /** Inner span of the `{...}` type, or null. */ + type: { start: number; end: number } | null; + /** Parameter or property name (brackets and defaults removed). */ + name: string | null; + optional: boolean; + /** Offset of the `@`. */ + at: number; +} + +interface JsDocComment { + start: number; + end: number; + tags: JsDocTag[]; +} + +function parseJsDoc(text: string, start: number, end: number): JsDocComment { + const tags: JsDocTag[] = []; + const re = /(^|[\s*])@([A-Za-z]+)/g; + re.lastIndex = start + 3; + const body = text.slice(0, end - 2); + for (let m = re.exec(body); m !== null; m = re.exec(body)) { + const at = m.index + m[1]!.length; + const tag = m[2]!; + let i = at + 1 + tag.length; + while (i < body.length && (body[i] === " " || body[i] === "\t")) i++; + let type: JsDocTag["type"] = null; + if (body[i] === "{") { + let depth = 0; + let j = i; + for (; j < body.length; j++) { + if (body[j] === "{") depth++; + else if (body[j] === "}") { + depth--; + if (depth === 0) break; + } + } + if (j >= body.length) continue; + type = { start: i + 1, end: j }; + // Trim the inner span to the type text. + while (type.start < type.end && /\s/.test(text[type.start]!)) type.start++; + while (type.end > type.start && /\s/.test(text[type.end - 1]!)) type.end--; + i = j + 1; + re.lastIndex = i; + } + let name: string | null = null; + let optional = false; + const rest = /^[ \t]*(\[)?[ \t]*([A-Za-z_$][\w$.]*)/.exec(body.slice(i, i + 200)); + if (rest !== null) { + name = rest[2]!; + optional = rest[1] !== undefined; + } + if (type !== null && text[type.end - 1] === "=") optional = true; + tags.push({ tag, type, name, optional, at }); + } + return { start, end, tags }; +} + +/** The spans of a file's string, template and regular expression literal + * tokens. */ +function literalSpans(sf: ts.SourceFile): { start: number; end: number }[] { + const out: { start: number; end: number }[] = []; + const kinds = new Set([ + ts.SyntaxKind.StringLiteral, + ts.SyntaxKind.NoSubstitutionTemplateLiteral, + ts.SyntaxKind.TemplateHead, + ts.SyntaxKind.TemplateMiddle, + ts.SyntaxKind.TemplateTail, + ts.SyntaxKind.RegularExpressionLiteral, + ]); + const visit = (n: ts.Node): void => { + if (kinds.has(n.kind)) out.push({ start: tokenStartOfNode(n), end: n.end }); + ts.forEachChild(n, visit); + }; + visit(sf); + return out.sort((a, b) => a.start - b.start); +} + +const RETURN_TAGS = new Set(["returns", "return"]); +const PROPERTY_TAGS = new Set(["property", "prop"]); + +/** A declaration in JavaScript a selector can stand on. */ +type JsPos = + | { kind: "decl"; node: ts.Node; owner: ts.Node; fn: ts.Node | null; members: ts.Node | null } + | { kind: "typedef"; comment: JsDocComment; tagIndex: number; prefix: string } + | { kind: "slot"; slot: Slot }; + +class JsResolver { + private readonly comments: JsDocComment[] = []; + private readonly roots = new Map(); + private readonly exportEquals: JsPos[] = []; + private readonly exportDefault: JsPos[] = []; + + constructor( + private readonly text: string, + sf: ts.SourceFile, + ) { + // JSDoc comments, not text inside string, template or regular + // expression literals that looks like one. + const literals = literalSpans(sf); + const re = /\/\*\*(?!\/)[\s\S]*?\*\//g; + for (let m = re.exec(text); m !== null; m = re.exec(text)) { + const inside = literals.find((l) => l.start <= m!.index && m!.index < l.end); + if (inside !== undefined) { + re.lastIndex = inside.end; + continue; + } + this.comments.push(parseJsDoc(text, m.index, m.index + m[0].length)); + } + for (const c of this.comments) + c.tags.forEach((t, i) => { + if ((t.tag === "typedef" || t.tag === "callback") && t.name !== null) + this.add(t.name, { kind: "typedef", comment: c, tagIndex: i, prefix: "" }); + }); + // Declarations first: `exports.name = name` may come before the + // function declaration it exports, which is hoisted. + for (const s of sf.statements.nodes) if (!ts.isExpressionStatement(s)) this.collect(s); + for (const s of sf.statements.nodes) if (ts.isExpressionStatement(s)) this.collect(s); + } + + private add(name: string, pos: JsPos): void { + const list = this.roots.get(name); + if (list === undefined) this.roots.set(name, [pos]); + else list.push(pos); + } + + /** The position of a declaration whose value is `value` (or none). */ + private declPos(node: ts.Node, owner: ts.Node, value: ts.Node | undefined): JsPos { + let fn: ts.Node | null = null; + let members: ts.Node | null = null; + if (ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node)) fn = node; + else if (ts.isClassDeclaration(node)) members = node; + if (value !== undefined) { + let v = value; + while (ts.isParenthesizedExpression(v)) v = v.expression; + if (ts.isFunctionExpression(v) || ts.isArrowFunction(v)) fn = v; + else if (ts.isClassExpression(v) || ts.isObjectLiteralExpression(v)) members = v; + } + return { kind: "decl", node, owner, fn, members }; + } + + private collect(s: ts.Statement): void { + if (ts.isFunctionDeclaration(s) || ts.isClassDeclaration(s)) { + const pos = this.declPos(s, s, undefined); + const name = nameText(s.name); + if (name !== null) this.add(name, pos); + if (hasDefaultModifier(s)) this.exportDefault.push(pos); + return; + } + if (ts.isVariableStatement(s)) { + const decls = s.declarationList.declarations.nodes; + for (const d of decls) { + const name = nameText(d.name); + if (name !== null) + this.add(name, this.declPos(d, decls.length === 1 ? s : d, d.initializer)); + } + return; + } + if (ts.isExportAssignment(s)) { + if (!s.isExportEquals) this.exportDefault.push(this.declPos(s, s, s.expression)); + return; + } + if (!ts.isExpressionStatement(s)) return; + const e = s.expression; + if (!ts.isBinaryExpression(e) || e.operatorToken.kind !== ts.SyntaxKind.EqualsToken) return; + const target = exportTarget(e.left); + if (target === null) return; + if (target === "export=") { + this.exportEquals.push(this.declPos(s, s, e.right)); + // `module.exports = name`: the named declaration is the export. + if (ts.isIdentifier(e.right)) { + const named = this.roots.get(e.right.text); + if (named !== undefined) this.exportEquals.push(...named); + } + } else if (ts.isIdentifier(e.right) && this.roots.has(e.right.text)) { + // `exports.name = local`: the named declaration is the export (and + // already is under its own name). + if (target !== e.right.text) + for (const pos of [...this.roots.get(e.right.text)!]) + if (pos.kind === "decl") this.add(target, pos); + } else this.add(target, this.declPos(s, s, e.right)); + } + + /** The last JSDoc comment directly before a node's first token. */ + private docOf(owner: ts.Node): JsDocComment | null { + const start = tokenStartOfNode(owner); + for (let i = this.comments.length - 1; i >= 0; i--) { + const c = this.comments[i]!; + if (c.end > start) continue; + return /^\s*$/.test(this.text.slice(c.end, start)) ? c : null; + } + return null; + } + + /** Tags of a comment that belong to the declaration it documents, not + * to a `@typedef` or `@callback` block inside it. */ + private ownTags(c: JsDocComment): JsDocTag[] { + const out: JsDocTag[] = []; + let inBlock = false; + for (const t of c.tags) { + if (t.tag === "typedef" || t.tag === "callback") { + inBlock = true; + continue; + } + if (inBlock && (PROPERTY_TAGS.has(t.tag) || t.tag === "type")) continue; + if (inBlock && t.tag !== "param" && !RETURN_TAGS.has(t.tag)) inBlock = false; + if (inBlock) continue; + out.push(t); + } + return out; + } + + private tagSlot(tag: JsDocTag): Slot { + return { span: tag.type, optional: tag.optional, insert: null, jsdoc: true }; + } + + /** A slot for a tag of the declaration `owner` documents; when it has no + * such tag, a replacement inserts one. */ + private docSlot(owner: ts.Node, tagNames: ReadonlySet, name: string | null): Slot { + const doc = this.docOf(owner); + if (doc !== null) { + const tag = this.ownTags(doc).find( + (t) => tagNames.has(t.tag) && (name === null || t.name === name), + ); + if (tag !== undefined && tag.type !== null) return this.tagSlot(tag); + } + const tagName = [...tagNames][0]!; + const named = name === null ? "" : ` ${name}`; + if (doc !== null) { + const at = doc.end - 2; + const pad = /\s/.test(this.text[at - 1] ?? "") ? "" : " "; + return { + span: null, + optional: false, + insert: { at, prefix: `${pad}@${tagName} {`, suffix: `}${named} ` }, + jsdoc: true, + }; + } + return { + span: null, + optional: false, + insert: { at: tokenStartOfNode(owner), prefix: `/** @${tagName} {`, suffix: `}${named} */ ` }, + jsdoc: true, + }; + } + + resolve(sel: Selector): Step { + let positions: JsPos[]; + if (sel.root === "export=") positions = this.exportEquals; + else if (sel.root === "default") positions = this.exportDefault; + else positions = this.roots.get(sel.root) ?? []; + let problem: string | null = null; + for (const seg of sel.segments) { + const next: JsPos[] = []; + for (const pos of positions) { + const step = this.step(pos, seg); + if (typeof step === "string") problem ??= step; + else next.push(...step); + } + positions = next; + if (positions.length === 0) + return problem === null ? { slots: [] } : { problem: `'${sel.text}' ${problem}` }; + } + const slots: Slot[] = []; + for (const pos of positions) { + const slot = this.slotOf(pos); + if (typeof slot === "string") problem ??= slot; + else if (slot !== null) slots.push(slot); + } + if (slots.length === 0 && problem !== null) return { problem: `'${sel.text}' ${problem}` }; + return { slots }; + } + + private slotOf(pos: JsPos): Slot | string | null { + if (pos.kind === "slot") return pos.slot; + if (pos.kind === "typedef") { + const own = pos.comment.tags[pos.tagIndex]!; + if (pos.prefix === "") { + // A typedef's own type: only when it is not the `object` that + // its @property tags describe. + const typeTag = this.typedefTypeTag(pos); + if (typeTag === null || /^object$/i.test(this.text.slice(typeTag.start, typeTag.end))) + return "names a @typedef of an object; name one of its properties"; + return { span: typeTag, optional: false, insert: null, jsdoc: true }; + } + void own; + return null; + } + if (pos.fn !== null && pos.members === null) + return "names a function; add '()' for its return type or '(name)' for a parameter"; + if (pos.members !== null && !ts.isObjectLiteralExpression(pos.members)) + return "names a class; name one of its members"; + return this.docSlot(pos.owner, new Set(["type"]), null); + } + + private typedefTypeTag(pos: { + comment: JsDocComment; + tagIndex: number; + }): { start: number; end: number } | null { + const tags = pos.comment.tags; + const own = tags[pos.tagIndex]!; + if (own.type !== null) return own.type; + for (let i = pos.tagIndex + 1; i < tags.length; i++) { + const t = tags[i]!; + if (t.tag === "type") return t.type; + if (!PROPERTY_TAGS.has(t.tag)) break; + } + return null; + } + + private step(pos: JsPos, seg: SelectorSegment): JsPos[] | string { + // A JSDoc type is not navigated (a nested @property path continues + // from its typedef position instead). + if (pos.kind === "slot") return []; + if (pos.kind === "typedef") { + const tags = pos.comment.tags; + const own = tags[pos.tagIndex]!; + if (own.tag === "callback") { + if (seg.kind !== "return" && seg.kind !== "param") + return "has a member segment after a @callback"; + for (let i = pos.tagIndex + 1; i < tags.length; i++) { + const t = tags[i]!; + if (t.tag === "typedef" || t.tag === "callback") break; + if ( + seg.kind === "return" + ? RETURN_TAGS.has(t.tag) + : t.tag === "param" && t.name === seg.name + ) + return t.type === null ? [] : [{ kind: "slot", slot: this.tagSlot(t) }]; + } + return []; + } + if (seg.kind !== "member") return "can only name properties of a @typedef"; + const name = pos.prefix === "" ? seg.name : `${pos.prefix}.${seg.name}`; + for (let i = pos.tagIndex + 1; i < tags.length; i++) { + const t = tags[i]!; + if (t.tag === "typedef" || t.tag === "callback") break; + if (!PROPERTY_TAGS.has(t.tag) && t.tag !== "type") break; + if (PROPERTY_TAGS.has(t.tag) && t.name === name && t.type !== null) { + // A nested property path continues from here. + return [ + { kind: "slot", slot: this.tagSlot(t) }, + { ...pos, prefix: name }, + ]; + } + } + return []; + } + switch (seg.kind) { + case "return": + if (pos.fn === null) return "has '()' after a declaration that is not a function"; + return [{ kind: "slot", slot: this.docSlot(this.docOwner(pos), RETURN_TAGS, null) }]; + case "param": { + if (pos.fn === null) return "has '(name)' after a declaration that is not a function"; + const fn = pos.fn as ts.FunctionExpression; + if (!fn.parameters.nodes.some((p) => nameText(p.name) === seg.name)) return []; + return [ + { kind: "slot", slot: this.docSlot(this.docOwner(pos), new Set(["param"]), seg.name) }, + ]; + } + case "member": { + if (pos.members === null) return []; + return this.membersNamed(pos.members, seg.name); + } + default: + return "navigates into a JSDoc type; name the declared type itself"; + } + } + + /** The node whose leading comment documents a declaration. */ + private docOwner(pos: { owner: ts.Node }): ts.Node { + return pos.owner; + } + + private membersNamed(holder: ts.Node, name: string): JsPos[] { + const out: JsPos[] = []; + if (ts.isObjectLiteralExpression(holder)) { + for (const p of holder.properties.nodes) { + if (nameText((p as ts.PropertyAssignment).name) !== name) continue; + if (ts.isPropertyAssignment(p)) out.push(this.declPos(p, p, p.initializer)); + else if (ts.isMethodDeclaration(p)) out.push(this.declPos(p, p, undefined)); + // `{ parse }`: the declaration of that name. + else if (ts.isShorthandPropertyAssignment(p)) out.push(...(this.roots.get(name) ?? [])); + } + return out; + } + const cls = holder as ts.ClassDeclaration; + for (const m of cls.members.nodes) { + if (ts.isConstructorDeclaration(m) && m.body !== undefined) { + // `this.name = value` in the constructor, documented by its comment. + for (const s of m.body.statements.nodes) { + if (!ts.isExpressionStatement(s) || !ts.isBinaryExpression(s.expression)) continue; + const e = s.expression; + if ( + e.operatorToken.kind === ts.SyntaxKind.EqualsToken && + ts.isPropertyAccessExpression(e.left) && + e.left.expression.kind === ts.SyntaxKind.ThisKeyword && + nameText(e.left.name) === name + ) + out.push(this.declPos(s, s, e.right)); + } + continue; + } + if (nameText((m as ts.MethodDeclaration).name) !== name) continue; + if (ts.isMethodDeclaration(m)) out.push(this.declPos(m, m, undefined)); + else if (ts.isPropertyDeclaration(m)) out.push(this.declPos(m, m, m.initializer)); + else if (ts.isGetAccessor(m)) + out.push({ kind: "decl", node: m, owner: m, fn: m, members: null }); + } + return out; + } +} + +/** `module.exports` → "export="; `exports.x` / `module.exports.x` → "x". */ +function exportTarget(left: ts.Expression): string | null { + if (!ts.isPropertyAccessExpression(left)) return null; + const obj = left.expression; + const prop = nameText(left.name); + if (prop === null) return null; + if (ts.isIdentifier(obj) && obj.text === "module" && prop === "exports") return "export="; + if (ts.isIdentifier(obj) && obj.text === "exports") return prop; + if ( + ts.isPropertyAccessExpression(obj) && + ts.isIdentifier(obj.expression) && + obj.expression.text === "module" && + nameText(obj.name) === "exports" + ) + return prop; + return null; +} diff --git a/packages/compiler/src/frontend/type-patches/selector.test.ts b/packages/compiler/src/frontend/type-patches/selector.test.ts new file mode 100644 index 000000000..716c994a8 --- /dev/null +++ b/packages/compiler/src/frontend/type-patches/selector.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, test } from "vitest"; +import { parseSelector, parseTypeText, splitUnion } from "./selector.js"; + +describe("parseSelector", () => { + test("members, returns, parameters, elements and index signatures", () => { + expect(parseSelector("HelpContext.helpWidth")).toEqual({ + file: null, + root: "HelpContext", + segments: [{ kind: "member", name: "helpWidth" }], + text: "HelpContext.helpWidth", + }); + expect(parseSelector("index.js:parse()")).toMatchObject({ + file: "index.js", + root: "parse", + segments: [{ kind: "return" }], + }); + expect(parseSelector("export=()")).toMatchObject({ + root: "export=", + segments: [{ kind: "return" }], + }); + expect(parseSelector("fetchUser(id)")).toMatchObject({ + segments: [{ kind: "param", name: "id" }], + }); + expect(parseSelector("list()[].name")).toMatchObject({ + segments: [{ kind: "return" }, { kind: "element" }, { kind: "member", name: "name" }], + }); + expect(parseSelector("Env[string]")).toMatchObject({ + segments: [{ kind: "index", key: "string" }], + }); + }); + + test("malformed selectors say why", () => { + expect(parseSelector("")).toEqual({ error: "is empty" }); + expect(parseSelector("../x.js:f()")).toEqual({ error: "names a file outside the package" }); + expect(parseSelector("1abc")).toHaveProperty("error"); + expect(parseSelector("a.")).toHaveProperty("error"); + expect(parseSelector("a(1)")).toHaveProperty("error"); + }); +}); + +describe("parseTypeText", () => { + test("a leading bar widens, anything else replaces", () => { + expect(parseTypeText("| undefined")).toEqual({ kind: "widen", arms: ["undefined"] }); + expect(parseTypeText("| undefined | null")).toEqual({ + kind: "widen", + arms: ["undefined", "null"], + }); + expect(parseTypeText("string | null")).toEqual({ kind: "replace", text: "string | null" }); + expect(parseTypeText("a\nb")).toEqual({ error: "must be one line" }); + }); + + test("splitUnion keeps nested unions and arrows together", () => { + expect(splitUnion("(a | b) | (() => c | d) | { x: e | f }")).toEqual([ + "(a | b)", + "(() => c | d)", + "{ x: e | f }", + ]); + }); +}); diff --git a/packages/compiler/src/frontend/type-patches/selector.ts b/packages/compiler/src/frontend/type-patches/selector.ts new file mode 100644 index 000000000..8fdd8b67c --- /dev/null +++ b/packages/compiler/src/frontend/type-patches/selector.ts @@ -0,0 +1,133 @@ +/* Type patch selectors: names of declarations in a package, written the + * same way for JSDoc in its JavaScript and for its declaration files. + * + * selector := [ file ":" ] root segment* + * root := identifier | "export=" | "default" + * segment := "." name member (interface, type alias, @typedef, + * class, namespace, object type) + * | "()" return type + * | "(" name ")" type of the named parameter + * | "[]" element type of an array type + * | "[string]" | "[number]" value type of an index signature + * + * Examples: `HelpContext.helpWidth`, `index.js:parse()`, `export=()`, + * `fetchUser().email`, `Env[string]`. */ + +export type SelectorSegment = + | { kind: "member"; name: string } + | { kind: "return" } + | { kind: "param"; name: string } + | { kind: "element" } + | { kind: "index"; key: "string" | "number" }; + +export interface Selector { + /** Package-relative file the root must be declared in, or null. */ + file: string | null; + /** A declared name, `export=` or `default`. */ + root: string; + segments: readonly SelectorSegment[]; + /** The selector as written. */ + text: string; +} + +const IDENT = /^[A-Za-z_$][\w$]*/; + +/** The parsed selector, or a message saying what is wrong with it. */ +export function parseSelector(text: string): Selector | { error: string } { + let rest = text.trim(); + if (rest === "") return { error: "is empty" }; + let file: string | null = null; + const colon = rest.indexOf(":"); + if (colon >= 0) { + file = rest.slice(0, colon).replace(/^\.\//, ""); + rest = rest.slice(colon + 1); + if (file === "" || file.startsWith("/") || file.split("/").includes("..")) + return { error: "names a file outside the package" }; + } + let root: string; + if (rest.startsWith("export=")) { + root = "export="; + rest = rest.slice("export=".length); + } else { + const m = IDENT.exec(rest); + if (m === null) return { error: "must start with a declared name, 'export=' or 'default'" }; + root = m[0]; + rest = rest.slice(root.length); + } + const segments: SelectorSegment[] = []; + while (rest !== "") { + if (rest.startsWith(".")) { + const m = IDENT.exec(rest.slice(1)); + if (m === null) return { error: `has no member name after '.' at '${rest}'` }; + segments.push({ kind: "member", name: m[0] }); + rest = rest.slice(1 + m[0].length); + } else if (rest.startsWith("()")) { + segments.push({ kind: "return" }); + rest = rest.slice(2); + } else if (rest.startsWith("(")) { + const m = /^\(\s*([A-Za-z_$][\w$]*)\s*\)/.exec(rest); + if (m === null) return { error: `has a malformed parameter at '${rest}'` }; + segments.push({ kind: "param", name: m[1]! }); + rest = rest.slice(m[0].length); + } else if (rest.startsWith("[]")) { + segments.push({ kind: "element" }); + rest = rest.slice(2); + } else if (rest.startsWith("[string]") || rest.startsWith("[number]")) { + segments.push({ kind: "index", key: rest.startsWith("[string]") ? "string" : "number" }); + rest = rest.slice("[string]".length); + } else { + return { error: `has an unexpected '${rest}'` }; + } + } + return { file, root, segments, text: text.trim() }; +} + +/** A widening (`| undefined`) or a replacement type. */ +export type TypePatchOp = + | { kind: "widen"; arms: readonly string[] } + | { kind: "replace"; text: string }; + +/** The operation a patch's type text stands for, or a message. */ +export function parseTypeText(text: string): TypePatchOp | { error: string } { + if (/[\r\n]/.test(text)) return { error: "must be one line" }; + const t = text.trim(); + if (t === "") return { error: "is empty" }; + if (t.startsWith("|")) { + const arms = splitUnion(t.slice(1)); + if (arms.some((a) => a === "")) return { error: "has an empty union member" }; + return { kind: "widen", arms }; + } + return { kind: "replace", text: t }; +} + +/** The top-level union members of a type text, trimmed. */ +export function splitUnion(text: string): string[] { + const out: string[] = []; + let depth = 0; + let quote: string | null = null; + let start = 0; + for (let i = 0; i < text.length; i++) { + const ch = text[i]!; + if (quote !== null) { + if (ch === "\\") i++; + else if (ch === quote) quote = null; + continue; + } + if (ch === '"' || ch === "'" || ch === "`") quote = ch; + else if (ch === "(" || ch === "[" || ch === "{" || ch === "<") depth++; + else if (ch === ")" || ch === "]" || ch === "}" || ch === ">") { + // `=>` is not a closing bracket. + if (!(ch === ">" && text[i - 1] === "=")) depth--; + } else if (ch === "|" && depth === 0) { + out.push(text.slice(start, i).trim()); + start = i + 1; + } + } + out.push(text.slice(start).trim()); + return out; +} + +/** Whitespace-insensitive text of a type for comparisons. */ +export function normalizeType(text: string): string { + return text.replace(/\s+/g, ""); +} diff --git a/packages/compiler/src/frontend/type-patches/semver.test.ts b/packages/compiler/src/frontend/type-patches/semver.test.ts new file mode 100644 index 000000000..83663e3a2 --- /dev/null +++ b/packages/compiler/src/frontend/type-patches/semver.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, test } from "vitest"; +import { isValidRange, satisfies } from "./semver.js"; + +describe("satisfies", () => { + test.each([ + ["15.0.0", "^15.0.0", true], + ["15.4.2", "^15.0.0", true], + ["16.0.0", "^15.0.0", false], + ["0.2.5", "^0.2.3", true], + ["0.3.0", "^0.2.3", false], + ["0.0.3", "^0.0.3", true], + ["0.0.4", "^0.0.3", false], + ["1.2.9", "~1.2.3", true], + ["1.3.0", "~1.2.3", false], + ["1.9.0", "~1", true], + ["2.1.3", "2.1.x", true], + ["2.2.0", "2.1.x", false], + ["2.1.3", "2", true], + ["3.0.0", "2", false], + ["2.1.3", "*", true], + ["1.2.3", "1.2.3", true], + ["1.2.4", "=1.2.3", false], + ["1.5.0", ">=1.2.0 <2.0.0", true], + ["2.0.0", ">=1.2.0 <2.0.0", false], + ["2.0.0", ">= 1.2.0", true], + ["1.3.0", ">1.2", true], + ["1.2.9", ">1.2", false], + ["1.2.9", "<=1.2", true], + ["1.3.0", "<=1.2", false], + ["2.3.9", "1.2.3 - 2.3", true], + ["2.4.0", "1.2.3 - 2.3", false], + ["3.1.0", "^1.0.0 || ^3.0.0", true], + ["2.1.0", "^1.0.0 || ^3.0.0", false], + ["2.0.0-beta.1", "^1.0.0", false], + ["2.0.0-beta.1", "^2.0.0", false], + ["2.0.0-beta.2", "^2.0.0-beta.1", true], + ["2.0.0-alpha", "^2.0.0-beta.1", false], + ["2.1.0-beta.2", "^2.0.0-beta.1", false], + ])("%s satisfies %s: %s", (version, range, expected) => { + expect(satisfies(version, range)).toBe(expected); + }); + + test("malformed input never satisfies", () => { + expect(satisfies("not-a-version", "^1.0.0")).toBe(false); + expect(isValidRange("^banana")).toBe(false); + expect(isValidRange(">=1 <2 || 3.x")).toBe(true); + }); +}); diff --git a/packages/compiler/src/frontend/type-patches/semver.ts b/packages/compiler/src/frontend/type-patches/semver.ts new file mode 100644 index 000000000..86e9d1183 --- /dev/null +++ b/packages/compiler/src/frontend/type-patches/semver.ts @@ -0,0 +1,229 @@ +/* The npm semver range subset type patches key on (`commander@^15.0.0`): + * exact versions, x-ranges, `^`, `~`, comparators, hyphen ranges and `||`. + * A prerelease version satisfies a comparator set only when one of the + * set's comparators names a prerelease of the same major.minor.patch, as + * npm does. */ + +interface Version { + major: number; + minor: number; + patch: number; + pre: readonly (string | number)[]; +} + +interface Comparator { + op: "<" | "<=" | ">" | ">=" | "="; + version: Version; +} + +/** A partial version: missing or wildcard parts are null. */ +interface Partial { + major: number | null; + minor: number | null; + patch: number | null; + pre: readonly (string | number)[]; +} + +const VERSION = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/; + +function preParts(text: string | undefined): (string | number)[] { + if (text === undefined || text === "") return []; + return text.split(".").map((p) => (/^\d+$/.test(p) ? Number(p) : p)); +} + +/** A full version, or null. */ +export function parseVersion(text: string): Version | null { + const m = VERSION.exec(text.trim()); + if (m === null) return null; + return { major: Number(m[1]), minor: Number(m[2]), patch: Number(m[3]), pre: preParts(m[4]) }; +} + +function parsePartial(text: string): Partial | null { + const m = + /^v?(\d+|[xX*])(?:\.(\d+|[xX*]))?(?:\.(\d+|[xX*]))?(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec( + text, + ); + if (m === null) return null; + const part = (s: string | undefined): number | null => + s === undefined || s === "x" || s === "X" || s === "*" ? null : Number(s); + const major = part(m[1]); + const minor = major === null ? null : part(m[2]); + const patch = minor === null ? null : part(m[3]); + return { major, minor, patch, pre: patch === null ? [] : preParts(m[4]) }; +} + +function v( + major: number, + minor: number, + patch: number, + pre: readonly (string | number)[] = [], +): Version { + return { major, minor, patch, pre }; +} + +/** `-0` marks the lowest prerelease, so `<2.0.0-0` excludes 2.0.0's + * prereleases as npm's desugaring does. */ +const LOWEST: readonly (string | number)[] = [0]; + +function comparePre(a: readonly (string | number)[], b: readonly (string | number)[]): number { + if (a.length === 0 && b.length === 0) return 0; + if (a.length === 0) return 1; + if (b.length === 0) return -1; + for (let i = 0; i < Math.max(a.length, b.length); i++) { + const x = a[i]; + const y = b[i]; + if (x === undefined) return -1; + if (y === undefined) return 1; + if (x === y) continue; + if (typeof x === "number" && typeof y === "number") return x < y ? -1 : 1; + if (typeof x === "number") return -1; + if (typeof y === "number") return 1; + return x < y ? -1 : 1; + } + return 0; +} + +export function compareVersions(a: Version, b: Version): number { + if (a.major !== b.major) return a.major < b.major ? -1 : 1; + if (a.minor !== b.minor) return a.minor < b.minor ? -1 : 1; + if (a.patch !== b.patch) return a.patch < b.patch ? -1 : 1; + return comparePre(a.pre, b.pre); +} + +/** The comparators a partial with an operator stands for. */ +function desugar(op: string, p: Partial): Comparator[] | null { + const { major, minor, patch } = p; + if (op === "^") { + if (major === null) return []; + const lo = v(major, minor ?? 0, patch ?? 0, p.pre); + let hi: Version; + if (major > 0 || minor === null) hi = v(major + 1, 0, 0, LOWEST); + else if (minor > 0 || patch === null) hi = v(0, minor + 1, 0, LOWEST); + else hi = v(0, 0, patch + 1, LOWEST); + return [ + { op: ">=", version: lo }, + { op: "<", version: hi }, + ]; + } + if (op === "~") { + if (major === null) return []; + const lo = v(major, minor ?? 0, patch ?? 0, p.pre); + const hi = minor === null ? v(major + 1, 0, 0, LOWEST) : v(major, minor + 1, 0, LOWEST); + return [ + { op: ">=", version: lo }, + { op: "<", version: hi }, + ]; + } + if (op === "" || op === "=") { + if (major === null) return []; + if (minor === null) + return [ + { op: ">=", version: v(major, 0, 0) }, + { op: "<", version: v(major + 1, 0, 0, LOWEST) }, + ]; + if (patch === null) + return [ + { op: ">=", version: v(major, minor, 0) }, + { op: "<", version: v(major, minor + 1, 0, LOWEST) }, + ]; + return [{ op: "=", version: v(major, minor, patch, p.pre) }]; + } + if (major === null) + return op === "<" || op === ">" ? [{ op: "<", version: v(0, 0, 0, LOWEST) }] : []; + if (op === ">") { + if (minor === null) return [{ op: ">=", version: v(major + 1, 0, 0) }]; + if (patch === null) return [{ op: ">=", version: v(major, minor + 1, 0) }]; + return [{ op: ">", version: v(major, minor, patch, p.pre) }]; + } + if (op === ">=") return [{ op: ">=", version: v(major, minor ?? 0, patch ?? 0, p.pre) }]; + if (op === "<") { + if (minor === null) return [{ op: "<", version: v(major, 0, 0, LOWEST) }]; + if (patch === null) return [{ op: "<", version: v(major, minor, 0, LOWEST) }]; + return [{ op: "<", version: v(major, minor, patch, p.pre) }]; + } + if (op === "<=") { + if (minor === null) return [{ op: "<", version: v(major + 1, 0, 0, LOWEST) }]; + if (patch === null) return [{ op: "<", version: v(major, minor + 1, 0, LOWEST) }]; + return [{ op: "<=", version: v(major, minor, patch, p.pre) }]; + } + return null; +} + +/** One `||` alternative: a list of comparators, or null when malformed. */ +function parseSet(text: string): Comparator[] | null { + const t = text.trim(); + const hyphen = /^(\S+)\s+-\s+(\S+)$/.exec(t); + if (hyphen !== null) { + const lo = parsePartial(hyphen[1]!); + const hi = parsePartial(hyphen[2]!); + if (lo === null || hi === null) return null; + return [...(desugar(">=", lo) ?? []), ...(desugar("<=", hi) ?? [])]; + } + if (t === "") return []; + const out: Comparator[] = []; + // `>= 1.2.3` may carry a space between the operator and the version. + const tokens = t.replace(/(<=|>=|<|>|=|\^|~)\s+/g, "$1").split(/\s+/); + for (const token of tokens) { + const m = /^(<=|>=|<|>|=|\^|~>?|)(.*)$/.exec(token); + if (m === null) return null; + const op = m[1] === "~>" ? "~" : m[1]!; + const p = parsePartial(m[2]!); + if (p === null) return null; + const cs = desugar(op, p); + if (cs === null) return null; + out.push(...cs); + } + return out; +} + +/** The parsed range, or null when it is not a semver range. */ +export function parseRange(range: string): Comparator[][] | null { + const sets: Comparator[][] = []; + for (const alt of range.split("||")) { + const set = parseSet(alt); + if (set === null) return null; + sets.push(set); + } + return sets; +} + +export function isValidRange(range: string): boolean { + return parseRange(range) !== null; +} + +function test(c: Comparator, version: Version): boolean { + const cmp = compareVersions(version, c.version); + switch (c.op) { + case "<": + return cmp < 0; + case "<=": + return cmp <= 0; + case ">": + return cmp > 0; + case ">=": + return cmp >= 0; + default: + return cmp === 0; + } +} + +/** True when `version` satisfies `range` (false for malformed input). */ +export function satisfies(version: string, range: string): boolean { + const ver = parseVersion(version); + const sets = parseRange(range); + if (ver === null || sets === null) return false; + return sets.some((set) => { + if (!set.every((c) => test(c, ver))) return false; + if (ver.pre.length === 0) return true; + // A prerelease only matches a set that names a prerelease of the same + // version tuple (the LOWEST markers of upper bounds do not count). + return set.some( + (c) => + c.version.pre.length > 0 && + c.version.pre !== LOWEST && + c.version.major === ver.major && + c.version.minor === ver.minor && + c.version.patch === ver.patch, + ); + }); +} diff --git a/packages/compiler/src/frontend/worker-target.test.ts b/packages/compiler/src/frontend/worker-target.test.ts index 7125bc309..e68fc798a 100644 --- a/packages/compiler/src/frontend/worker-target.test.ts +++ b/packages/compiler/src/frontend/worker-target.test.ts @@ -3,7 +3,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { expect, test } from "vitest"; import { workerTargetPaths } from "./worker-target.js"; -import { fallbackDtsPath } from "./dts-paths.js"; +import { fallbackDtsPath, fallbackModulePath } from "./dts-paths.js"; import { Ts7Host } from "./ts7/program-host.js"; import { Ts7Api } from "./ts7/rpc-api.js"; import { ModuleKind, ModuleResolutionKind } from "./ts7/enums.js"; @@ -17,12 +17,15 @@ function discover(source: string): string[] { 'export { Worker as Task } from "node:worker_threads";', ); const host = new Ts7Host((options) => new Ts7Api(options), { cwd: directory }); - const program = host.createProgram([entry, fallbackDtsPath()], { - noLib: true, - types: [], - module: ModuleKind.ESNext, - moduleResolution: ModuleResolutionKind.Bundler, - }); + const program = host.createProgram( + [entry, fallbackDtsPath(), fallbackModulePath("node:worker_threads")!], + { + noLib: true, + types: [], + module: ModuleKind.ESNext, + moduleResolution: ModuleResolutionKind.Bundler, + }, + ); try { return workerTargetPaths(program, [program.getSourceFile(entry)!]).map((file) => file.replace(directory + "/", ""), diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index 302d92886..b79b86f1e 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -1,4 +1,12 @@ +import { shapeOverlayEnabled } from "./frontend/shape-inference/index.js"; +import { nodeFrontendServices } from "./frontend/services-node.js"; +import { + runTypePatchCommand, + type TypePatchCommandRequest, + type TypePatchCommandResult, +} from "./frontend/type-patches/command.js"; import { optimizationField, type NativeOptimization } from "./backend/optimization.js"; +import { targetCpuField, targetCpuTargetRefusal } from "./backend/target-cpu.js"; import { compilationTiming, type CompilationTiming, type TimingClock } from "./timing.js"; import { prepareExecutableModule } from "./executable/prepare.js"; import { @@ -9,6 +17,7 @@ import { } from "./library/prepare.js"; import { analyzeWithFrontend } from "./frontend/analysis.js"; import { checkerCacheIdentity } from "./frontend/ts7/checker-backend.js"; +import { setLibDiagnosticsStore } from "./frontend/ts7/lib-diagnostics.js"; import { analyzeWithVerdictCache, fileVerdictStore } from "./coverage/verdict-cache.js"; import { coverageCompilerIdentity } from "./coverage/verdict-startup.js"; import { commentOnlyReplay } from "./coverage/verdict-semantic.js"; @@ -44,15 +53,19 @@ export type { AnalyzeResult, } from "./compile-types.js"; import { compilePackedLibrary } from "./backend/library-pack.js"; +import { node24FetchCompatProfile } from "./compat/fetch-profile.js"; +import { libFnSignatures } from "./ir/builtin-signatures.js"; +import { type FunctionLayout, parseFunctionLayout } from "./backend/llvm/function-layout.js"; import { InternalCompilerError } from "./errors.js"; import { frontendProcessExecArgv } from "./frontend-process-args.js"; import { type ChildProcess, spawn } from "node:child_process"; import { createHash } from "node:crypto"; -import { existsSync } from "node:fs"; +import { existsSync, readFileSync } from "node:fs"; import { copyFile, mkdir, mkdtemp, readFile, rename, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { basename, dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; +import { gunzipSync } from "node:zlib"; import { clearCcCaches, configuredTargetPlatform, @@ -92,6 +105,7 @@ import { createNativeLinkPlan } from "./backend/link-plan.js"; import { nativeProfileKey, nativeProfileRefusal, + type NativeProfileMatch, type NativeProfileOptions, } from "./backend/native-profile.js"; import { @@ -100,7 +114,11 @@ import { platformLinkerSupportsPersistentCache, resolvePlatformLinker, } from "./backend/linker.js"; -import { splitLlvmLibraryProgram, splitLlvmProgram } from "./backend/llvm/split.js"; +import { + splitLlvmLibraryProgram, + splitLlvmProgram, + splitLlvmProgramFile, +} from "./backend/llvm/split.js"; import { emitLibraryIdentityLines, replaceLibraryIdentity, @@ -111,9 +129,12 @@ import { iceDiag, nativeCodegenDiag, nodeIslandNodeDiag, + nodeIslandModuleDiag, + isTypePatchCode, type ScrDiagnostic, } from "./diagnostics/diagnostic.js"; import { libnodeCacheKey, libnodeRecord } from "./node-islands/libnode.js"; +import { placeNodeIslandModules, readNodeIslandModules } from "./node-islands/modules.js"; import { loadLibraryProfile, type LibraryProfile } from "./library/library-profile.js"; import { clearFenceEvalCaches, decorateLibraryRefusals } from "./library/fence-eval.js"; import { @@ -189,6 +210,15 @@ export { } from "./backend/external-c.js"; export { warmNativeCaches } from "./backend/warm-cache.js"; export { mergeNativeProfiles as mergeProfiles } from "./backend/native-codegen.js"; + +/** `scriptc types patch|list` with the Node frontend's parser. */ +export async function typePatches( + request: TypePatchCommandRequest, +): Promise { + return runTypePatchCommand(request, (path, text, kind) => + nodeFrontendServices().parse(path, text, kind), + ); +} export { collectGarbageUnderHeapPressure } from "./heap-collection.js"; export { ANDROID_MIN_API, @@ -244,7 +274,7 @@ export { type SurfaceManifestEntry, } from "./coverage/surface-manifest.js"; export { - NODE24_FETCH_COMPAT_PROFILE, + node24FetchCompatProfile, type FetchCompatEvidence, type FetchCompatFacet, type FetchCompatInventory, @@ -256,7 +286,13 @@ export { type FetchCompatOption, type FetchCompatProfile, } from "./compat/fetch-profile.js"; -export { LIB_FN_SIGS } from "./ir/builtin-signatures.js"; +export { libFnSignatures } from "./ir/builtin-signatures.js"; +/** @deprecated Use `libFnSignatures()`. Built when this module loads; the + * native compiler does not load this module. */ +export const LIB_FN_SIGS = libFnSignatures(); +/** @deprecated Use `node24FetchCompatProfile()`. Built when this module + * loads; the native compiler does not load this module. */ +export const NODE24_FETCH_COMPAT_PROFILE = node24FetchCompatProfile(); export { validateModule } from "./ir/validate.js"; export { assignLegacyTiers, @@ -414,8 +450,13 @@ export async function analyzeCached( return result; } -const nodeFrontend: FrontendFactory = (entry, npmStatic, externalTypes, libraryNpmStatic) => - runFrontend(entry, loadProgram, npmStatic, externalTypes, libraryNpmStatic); +const nodeFrontend: FrontendFactory = ( + entry, + npmStatic, + externalTypes, + libraryNpmStatic, + options, +) => runFrontend(entry, loadProgram, npmStatic, externalTypes, libraryNpmStatic, options); /** The checker's part of the executable cache key (`checkerCacheIdentity`), as the CLI's startup route computes it. */ function checkerField(): { checker?: string } { @@ -445,6 +486,10 @@ export async function compile( opts: CompileRequestOptions, ): Promise { clearCompileSessionCaches(); + // `shapeOverlay: false` reaches the frontend (and its child process) + // through the environment the overlay reads. + const overlayEnv = process.env["SCRIPTC_SHAPE_OVERLAY"]; + if (opts.shapeOverlay === false) process.env["SCRIPTC_SHAPE_OVERLAY"] = "0"; const frontendInputs = new FrontendInputTracker(); const warnings: CompileWarningSink = { warnings: [], sourceTexts: null }; // compileTracked starts it for a build with no earlier build of the same invocation. @@ -455,7 +500,12 @@ export async function compile( ); return withCompileWarnings(result, warnings); } finally { + setLibDiagnosticsStore(null); frontendProcess?.release(); + if (opts.shapeOverlay === false) { + if (overlayEnv === undefined) delete process.env["SCRIPTC_SHAPE_OVERLAY"]; + else process.env["SCRIPTC_SHAPE_OVERLAY"] = overlayEnv; + } } } @@ -466,15 +516,42 @@ interface CompileWarningSink { sourceTexts: Map | null; } +/** The package.json texts type patch warnings point into, re-read for a + * cache hit's code frames (performance warnings need none). */ +function cachedWarningSources(warnings: readonly ScrDiagnostic[]): Map { + const texts = new Map(); + for (const w of warnings) { + if (!isTypePatchCode(w.code) || texts.has(w.loc.file)) continue; + try { + texts.set(w.loc.file, readFileSync(w.loc.file, "utf8")); + } catch { + // The frame is omitted; the message stands alone. + } + } + return texts; +} + /** The build's performance warnings (SC61xx), stored with its cache entry * so a cache hit repeats them. */ function performanceWarnings(sink: CompileWarningSink | undefined): { performance?: ScrDiagnostic[]; } { - const performance = (sink?.warnings ?? []).filter((d) => isPerformanceCode(d.code)); + // Type patch warnings (SC8030–SC8033) describe the same unchanged inputs, + // so a hit repeats them too. + const performance = (sink?.warnings ?? []).filter( + (d) => isPerformanceCode(d.code) || isTypePatchCode(d.code), + ); return performance.length === 0 ? {} : { performance }; } +/** The profile match of a --profile-use build's program object, for its + * artifact and its executable cache entry, which a cache hit reports again. */ +function profileMatchField(object: NativeProgramObject | null): { + profileMatch?: NativeProfileMatch; +} { + return object?.profileMatch === undefined ? {} : { profileMatch: object.profileMatch }; +} + function withCompileWarnings( result: T, sink: CompileWarningSink, @@ -567,8 +644,8 @@ async function compileExecutableNative( } const effectiveProgramSplit = programSplit ?? - (!programIsObject && features.optimization === "dev" && features.backend === "llvm" && !sanitize - ? splitLlvmProgram(await readFile(llvmPath, "utf8")) + (!programIsObject && features.optimization === "dev" && features.backend === "llvm" + ? splitLlvmProgramFile(llvmPath) : null); if (programIsObject) throw new InternalCompilerError("program objects must link against a precompiled runtime pack"); @@ -598,7 +675,9 @@ async function compileExecutableNative( ...(features.warningReplace ? { warningReplace: true } : {}), ...(features.stdioExtended ? { stdioExtended: true } : {}), ...(features.nodeIsland ? { nodeIsland: true } : {}), + ...(features.moduleNamespace ? { moduleNamespace: true } : {}), zlib: features.zlib, + ...(features.brotli ? { brotli: true } : {}), assert: features.assert, inspect: features.inspect, dynInvoke: features.dynInvoke, @@ -608,10 +687,14 @@ async function compileExecutableNative( ...(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.events, emitter: features.emitter, symbol: features.symbol, @@ -629,6 +712,7 @@ async function compileExecutableNative( dgram: features.dgram, dnsPromises: features.dnsPromises, dgramDyn: features.dgramDyn, + textCodecDyn: features.textCodecDyn, watch: features.watch, foreignFfi: features.foreignFfi, ...(features.workers ? { workers: true } : {}), @@ -641,6 +725,7 @@ async function compileExecutableNative( dh: features.dh, pki: features.pki, ed25519: features.ed25519, + x509: features.x509, webcrypto: features.webcrypto, perfLoop: features.perfLoop, ...(onArtifactReady === undefined ? {} : { onArtifactReady }), @@ -660,6 +745,7 @@ interface NativeProgramObject { partitionPaths: string[]; artifactPath: string; dependencies: NativeArtifactDependency[]; + profileMatch?: NativeProfileMatch; } async function emitNativeProgramObject( @@ -695,6 +781,7 @@ async function emitNativeProgramObject( partitionPaths: artifact.outputPaths.slice(1), artifactPath, dependencies: artifact.dependencies, + ...(artifact.profileMatch === undefined ? {} : { profileMatch: artifact.profileMatch }), }; } catch (error) { await removeNativeProgramObject(linkPath, nativePartitionPaths(linkPath, partitions)); @@ -730,6 +817,37 @@ async function readNativeProfile( return { use: { path, digest: createHash("sha256").update(bytes).digest("hex") } }; } +/** The digest of the request's function layout file, which keys the + * executable cache: the layout changes the code's order, not its frontend + * input. */ +async function functionLayoutKey(opts: CompileRequestOptions): Promise { + if (opts.functionLayout === undefined) return undefined; + const path = resolve(opts.functionLayout); + let bytes: Buffer; + try { + bytes = await readFile(path); + } catch (err) { + throw new Error( + `cannot read function layout ${path}: ${err instanceof Error ? err.message : String(err)}`, + ); + } + return createHash("sha256").update(bytes).digest("hex"); +} + +/** The request's function layout. */ +async function readFunctionLayout( + opts: CompileRequestOptions, +): Promise<{ functionLayout?: FunctionLayout }> { + if (opts.functionLayout === undefined) return {}; + const path = resolve(opts.functionLayout); + const bytes = await readFile(path); + return { + functionLayout: parseFunctionLayout( + (path.endsWith(".gz") ? gunzipSync(bytes) : bytes).toString("utf8"), + ), + }; +} + function usesPrecompiledRuntimePack(opts: CompileRequestOptions, backend: "llvm"): boolean { if (backend !== "llvm" || opts.sanitize === true) return false; return nativeCodegenTarget() !== null; @@ -788,7 +906,32 @@ async function prepareExecutableInput( const recorded = libnodeRecord(opts.libnode, opts.outPath, process.env, dirname(entryPath)); if (typeof recorded === "string") return { ok: false, diagnostics: [nodeIslandNodeDiag(recorded, entryPath)], sourceTexts }; - mod.nodeIsland = recorded; + mod.nodeIsland = { ...mod.nodeIsland, ...recorded }; + } + } + // The island module tree ships with the program: beside the executable + // with --libnode=beside, else inside it (node-islands/modules.ts). + let nodeIslandModuleData: Uint8Array | undefined; + const islandModules = mod.nodeIsland?.modules; + if (islandModules !== undefined && outputKind !== "ir") { + if (outputKind === "exe" && opts.libnode === "beside") { + const problem = placeNodeIslandModules(islandModules, `${opts.outPath}.libnode/modules`); + if (problem !== null) + return { + ok: false, + diagnostics: [nodeIslandModuleDiag(problem, { file: entryPath, start: 0, end: 0 })], + sourceTexts, + }; + islandModules.location = `$ORIGIN/${basename(opts.outPath)}.libnode/modules`; + } else { + const data = readNodeIslandModules(islandModules); + if (typeof data === "string") + return { + ok: false, + diagnostics: [nodeIslandModuleDiag(data, { file: entryPath, start: 0, end: 0 })], + sourceTexts, + }; + nodeIslandModuleData = data; } } if (warningSink !== undefined && prepared.warnings.length > 0) { @@ -816,13 +959,20 @@ async function prepareExecutableInput( llvm = emitLlvmModuleSource(mod, { targetTriple: process.env["SCRIPTC_TARGET"] ?? "", ...debugOptions, + ...(await readFunctionLayout(opts)), pointerBits: buildPlatform === "wasi" ? 32 : 64, wasi: buildPlatform === "wasi", runtimeAbiMarker: outputKind === "obj", + // Textual output shows the code an executable gets; objects keep + // the pending flag, since their link may not add an unwinder. + unwindAbi: buildPlatform === "linux" && outputKind !== "obj", objectAudit: opts.sanitize === true, + sanitizeAttributes: opts.sanitize === true, lowRegion: lowRegionTarget(buildPlatform, opts.sanitize === true), ...(opts.sanitize === true ? { wasmBoundsChecks: true } : {}), ...inlineRcOption(opts.optimization, opts.sanitize === true), + ...(nodeIslandModuleData === undefined ? {} : { nodeIslandModuleData }), + ...targetCpuField(opts.targetCpu), }); } catch (err) { if (!(err instanceof LlvmUnsupportedError)) throw err; @@ -885,14 +1035,20 @@ async function prepareExecutableInput( llvmSource = emitLlvmModuleSource(mod, { targetTriple: process.env["SCRIPTC_TARGET"] ?? "", ...debugOptions, + ...(await readFunctionLayout(opts)), pointerBits: buildPlatform === "wasi" ? 32 : 64, wasi: buildPlatform === "wasi", runtimeAbiMarker: useRuntimePack, + unwindAbi: buildPlatform === "linux", objectAudit: opts.sanitize === true, + sanitizeAttributes: opts.sanitize === true, lowRegion: lowRegionTarget(buildPlatform, opts.sanitize === true), ...(opts.sanitize === true ? { wasmBoundsChecks: true } : {}), ...inlineRcOption(opts.optimization, opts.sanitize === true), ...(opts.detachedExit === true ? { detachedExit: true } : {}), + ...(nodeIslandModuleData === undefined ? {} : { nodeIslandModuleData }), + ...targetCpuField(opts.targetCpu), + ...(opts.cpuDispatch === true ? { cpuDispatch: true } : {}), }); } catch (err) { if (!(err instanceof LlvmUnsupportedError)) throw err; @@ -915,7 +1071,6 @@ async function prepareExecutableInput( !useRuntimePack && backend === "llvm" && (opts.optimization ?? "release") === "dev" && - !(opts.sanitize ?? false) && typeof llvmSource === "string" ? splitLlvmProgram(llvmSource) : null; @@ -1180,10 +1335,38 @@ async function prepareExecutableInputInChild( * `requestPath` as `compileTracked` would in-process, and writes what it prepared, without the LLVM * text, to `resultPath`. */ +/** Has the build reuse the diagnostics of the declaration files scriptc + * supplies (lib-diagnostics.ts) through the build cache at `root`, when the + * build uses one (executables; source outputs create no cache) and the + * checker is tsc-ts. */ +function configureLibDiagnostics(root: string | null): void { + let checker: string | undefined; + try { + checker = root === null ? undefined : checkerCacheIdentity(); + } catch { + // A missing checker fails where the build starts it. + checker = undefined; + } + setLibDiagnosticsStore( + root === null || checker === undefined + ? null + : fileVerdictStore(join(root, "lib-diagnostics-v1")), + checker ?? "", + ); +} + export async function runFrontendProcess(requestPath: string, resultPath: string): Promise { const request = JSON.parse(await readFile(requestPath, "utf8")) as FrontendProcessRequest; + // The process started before the build's options were known. + if (request.opts.shapeOverlay === false) process.env["SCRIPTC_SHAPE_OVERLAY"] = "0"; clearCompileSessionCaches(); setProvenanceSources(request.provenance); + // The compile that started this process prepared the cache root. + configureLibDiagnostics( + (request.opts.outputKind ?? "exe") === "exe" && provenanceSources() === null + ? await prepareBuildCacheRoot(buildCacheRoot()) + : null, + ); const frontendInputs = new FrontendInputTracker(); const warnings: CompileWarningSink = { warnings: [], sourceTexts: null }; let result: FrontendProcessResult = { kind: "failed" }; @@ -1231,6 +1414,8 @@ export async function runFrontendProcess(requestPath: string, resultPath: string } } catch { result = { kind: "failed" }; + } finally { + setLibDiagnosticsStore(null); } await writeFile(resultPath, JSON.stringify(result), { mode: 0o600 }); } @@ -1297,6 +1482,19 @@ async function compileTracked( sourceTexts: new Map(), }; } + if (opts.cpuDispatch === true && outputKind !== "exe") { + return { + ok: false, + diagnostics: [ + nativeCodegenDiag( + "SC3002", + "--cpu-dispatch is only supported for executable output", + entryPath, + ), + ], + sourceTexts: new Map(), + }; + } if (opts.nativeLinkInfo === true && outputKind !== "obj") { return { ok: false, @@ -1433,6 +1631,18 @@ async function compileTracked( } } } + const targetCpuRefusal = targetCpuTargetRefusal( + opts.targetCpu, + buildPlatform, + nativeCodegenTarget()?.architecture ?? process.arch, + ); + if (targetCpuRefusal !== null) { + return { + ok: false, + diagnostics: [nativeCodegenDiag("SC3002", targetCpuRefusal, entryPath)], + sourceTexts: new Map(), + }; + } try { windowsSubsystemLinkerArgs(buildPlatform, opts.windowsSubsystem); } catch (err) { @@ -1452,9 +1662,11 @@ async function compileTracked( platform: buildPlatform, }); let profile: NativeProfileOptions | undefined; + let functionLayoutDigest: string | undefined; try { if (profileRefusal !== null) throw new Error(profileRefusal); profile = await readNativeProfile(opts); + functionLayoutDigest = await functionLayoutKey(opts); } catch (err) { return { ok: false, @@ -1468,12 +1680,11 @@ async function compileTracked( outputKind === "exe" && provenanceSources() === null ? await prepareBuildCacheRoot(buildCacheRoot()) : null; + configureLibDiagnostics(cacheRoot); let earlyCacheOptions: EarlyExecutableCacheOptions | null = null; if (outputKind === "exe") { const helperObjectRoute = opts.nativeProgramObject === true || usesPrecompiledRuntimePack(opts, "llvm"); - // Calls into Node record the libnode pack, keyed by its manifest (node-islands/libnode.ts). - const libnodeKey = libnodeCacheKey(opts.libnode); const routeOptions: Omit< EarlyExecutableCacheOptions, "implementation" | "implementationDependencies" | "nativeEnvironment" @@ -1488,11 +1699,17 @@ async function compileTracked( ...(opts.strip ? { strip: true as const } : {}), ...(opts.windowsSubsystem === "gui" ? { windowsSubsystem: "gui" as const } : {}), ...(opts.detachedExit === true ? { detachedExit: true as const } : {}), - ...(libnodeKey === undefined ? {} : { libnode: libnodeKey }), + // Calls into Node record the libnode pack; their entries record its + // identity (node-islands/libnode.ts) and a hit requires it unchanged. + libnodeIdentity: () => libnodeCacheKey(opts.libnode), + ...targetCpuField(opts.targetCpu), + ...(opts.cpuDispatch === true ? { cpuDispatch: true as const } : {}), ...(profile === undefined ? {} : { profile: nativeProfileKey(profile) }), + ...(functionLayoutDigest === undefined ? {} : { functionLayout: functionLayoutDigest }), ...checkerField(), ...(youngNurseryRequested() ? { youngNursery: true as const } : {}), npmStatic: opts.npmStatic ?? null, + ...(shapeOverlayEnabled() ? { shapeOverlay: true as const } : {}), ffiProfile: opts.ffiProfilePath === undefined || ffiProfileBytes === null ? null @@ -1534,31 +1751,27 @@ async function compileTracked( implementationDependencies: implementation.dependencies, }; } - const earlyHit = + const cachedHit = earlyCacheOptions === null ? null : await readEarlyExecutableCache(cacheRoot, earlyCacheOptions); + // A --libnode=beside build that calls into Node places its island modules + // next to the executable from the lowered module, which a cache hit does + // not have: such builds take the full path. + const earlyHit = + cachedHit !== null && cachedHit.native.nodeIsland === true && opts.libnode === "beside" + ? null + : cachedHit; if (earlyHit !== null) { timing("executable-cache-hit"); if (earlyCacheOptions === null) { throw new InternalCompilerError("executable cache hit without executable cache options"); } - // The pack a --libnode=beside build places next to the executable is an output of the - // build, like the executable: a restored executable that calls into Node gets it again. - if (earlyHit.native.nodeIsland === true && opts.libnode === "beside") { - const recorded = libnodeRecord(opts.libnode, opts.outPath); - if (typeof recorded === "string") - return { - ok: false, - diagnostics: [nodeIslandNodeDiag(recorded, entryPath)], - sourceTexts: new Map(), - }; - } // A cached build repeats its performance warning (SC6101): the code it // names is just as untyped as when it was built. if (warningSink !== undefined && earlyHit.performance !== undefined) { warningSink.warnings = earlyHit.performance; - warningSink.sourceTexts = new Map(); + warningSink.sourceTexts = cachedWarningSources(earlyHit.performance); } const executableCacheOptions = earlyCacheOptions; // Route/proof metadata is independently evictable. A full-compiler @@ -1575,6 +1788,7 @@ async function compileTracked( path: opts.outPath, translationUnitPath: earlyHit.llvmPath, backend: earlyHit.native.backend, + ...(earlyHit.profileMatch === undefined ? {} : { profileMatch: earlyHit.profileMatch }), }, binaryPath: opts.outPath, llvmPath: earlyHit.llvmPath, @@ -1652,6 +1866,7 @@ async function compileTracked( executableRestored: true, nativeDependencies: dependencies, frontend: earlyHit.frontend, + ...profileMatchField(nativeProgramObject), }); }, nativeProgramObject?.partitionPaths, @@ -1697,6 +1912,7 @@ async function compileTracked( path: opts.outPath, translationUnitPath: earlyHit.llvmPath, backend: earlyHit.native.backend, + ...profileMatchField(nativeProgramObject), }, binaryPath: opts.outPath, llvmPath: earlyHit.llvmPath, @@ -1788,6 +2004,7 @@ async function compileTracked( frontend: frontendInputs.snapshot(), ...(irSourcePath === undefined ? {} : { irPath: irSourcePath }), ...performanceWarnings(warningSink), + ...profileMatchField(nativeProgramObject), }); publishedExecutable = true; }, @@ -1807,6 +2024,7 @@ async function compileTracked( frontend: frontendInputs.snapshot(), ...(irSourcePath === undefined ? {} : { irPath: irSourcePath }), ...performanceWarnings(warningSink), + ...profileMatchField(nativeProgramObject), }).catch(() => undefined); } } catch (err) { @@ -1842,6 +2060,7 @@ async function compileTracked( path: opts.outPath, translationUnitPath: llvmPath, backend, + ...profileMatchField(nativeProgramObject), }, binaryPath: opts.outPath, llvmPath, @@ -1870,6 +2089,7 @@ function libraryNativeFeatures(mod: IrModule, backend: "llvm"): EarlyLibraryNati searchParams: features.searchParams, emitter: features.emitter, zlib: features.zlib, + x509: features.x509, copying: features.copying, textDecoderLegacy: features.legacyTextDecoder, dynInvoke: features.dynInvoke, @@ -1965,6 +2185,7 @@ async function compileLibraryNative( searchParams: features.searchParams, emitter: features.emitter, zlib: features.zlib, + x509: features.x509, copying: features.copying, textDecoderLegacy: features.textDecoderLegacy, dynInvoke: features.dynInvoke, @@ -2289,6 +2510,7 @@ async function compileLibraryTracked( pointerBits: buildPlatform === "wasi" ? 32 : 64, wasi: buildPlatform === "wasi", objectAudit: opts.sanitize === true, + sanitizeAttributes: opts.sanitize === true, ...inlineRcOption(profile.optimization, opts.sanitize === true), }); timing("llvm-emit", { diff --git a/packages/compiler/src/ir/analysis.ts b/packages/compiler/src/ir/analysis.ts index 70ec381a1..ee1cee6be 100644 --- a/packages/compiler/src/ir/analysis.ts +++ b/packages/compiler/src/ir/analysis.ts @@ -4,6 +4,7 @@ import { BYTES_ELEMENT_NAME, DYN_HANDLE_KINDS, RUNTIME_STREAM_CLASSES, + recordTextCodecClass, typeEquals, type IrExpr, type IrLibFn, @@ -141,8 +142,10 @@ export function dynDesc( return "string"; case "bool": return "boolean"; - case "record": - return recordsById.get(t.shapeId)?.tuple ? "array" : "object"; + case "record": { + const shape = recordsById.get(t.shapeId); + return shape?.tuple ? "array" : ((shape && recordTextCodecClass(shape)) ?? "object"); + } case "array": return "array"; case "nullT": diff --git a/packages/compiler/src/ir/array-length-stability.test.ts b/packages/compiler/src/ir/array-length-stability.test.ts index f9b3cc9c2..97270e059 100644 --- a/packages/compiler/src/ir/array-length-stability.test.ts +++ b/packages/compiler/src/ir/array-length-stability.test.ts @@ -193,3 +193,34 @@ test("number and boolean writes to class fields run no user code", () => { // A reference stored into a field releases the old one. expect(preserves([fieldSet(arr("b"))], "i", new Set(), scope)).toBe(false); }); + +test("optional element reads, their assertion errors and present-undefined stores at an in-range index keep the length", () => { + const optional = (receiver: string, index: IrExpr): IrExpr => ({ + kind: "call", + callee: "%arr.idxOr.f64", + args: [arr(receiver), index], + type: F64, + loc, + }); + const undefinedAt = (receiver: string, index: IrExpr): IrStmt => ({ + kind: "arraySetUndefined", + arr: arr(receiver), + index, + loc, + }); + const thrown: IrExpr = { kind: "libCall", fn: "error.nodeThrow", args: [num(1)], type: F64, loc }; + expect(preserves([stmt(optional("a", ref("i"))), stmt(thrown)])).toBe(true); + expect(preserves([undefinedAt("a", ref("i"))])).toBe(true); + // Past the counter, a present undefined extends the array. + expect(preserves([undefinedAt("a", plus(ref("i"), num(1)))])).toBe(false); + // The high counter of a two-counter loop is in range as well. + const paired = (body: IrStmt[]): boolean => + preservesArrayLength( + [cond, ...body], + { receiver: "a", inRange: "i", inRangeHigh: "j" }, + locals, + new Set(), + ); + expect(paired([undefinedAt("a", ref("j")), set("a", ref("j"), num(0))])).toBe(true); + expect(preserves([set("a", ref("j"), num(0))])).toBe(false); +}); diff --git a/packages/compiler/src/ir/array-length-stability.ts b/packages/compiler/src/ir/array-length-stability.ts index ed57972b6..7715ea1e1 100644 --- a/packages/compiler/src/ir/array-length-stability.ts +++ b/packages/compiler/src/ir/array-length-stability.ts @@ -162,6 +162,10 @@ export interface ArrayLengthLoop { * long as the length itself has not changed). An element write at this * index replaces an existing element or sets a non-index property. */ inRange: string | null; + /** A second such binding: the high counter of a two-counter loop + * (`for (let i = lo, j = hi; i < j; i++, j--)`), whose values stay + * between the low counter's start and its own. */ + inRangeHigh?: string | null; } /** Whether running `nodes` (a loop's condition, body and update) can never @@ -191,7 +195,10 @@ export function preservesArrayLength( // escaping. const receiver = (e: IrExpr): boolean => (e.kind === "varRef" && e.localId === loop.receiver) || expr(e); - const written = (id: string): boolean => id === loop.receiver || id === loop.inRange; + const inRange = (id: string): boolean => + (loop.inRange !== null && id === loop.inRange) || + (loop.inRangeHigh != null && id === loop.inRangeHigh); + const written = (id: string): boolean => id === loop.receiver || inRange(id); const expr = (e: IrExpr): boolean => { switch (e.kind) { case "varRef": @@ -224,12 +231,21 @@ export function preservesArrayLength( case "call": // A checked union extraction (`x!`, coercions/unions.ts): the // asserted arm's payload or a TypeError, and no user code. - return e.callee.startsWith("%union.narrow.") && e.args.every(expr); + if (e.callee.startsWith("%union.narrow.")) return e.args.every(expr); + // An element read as `T | undefined` (int32-arrays.ts isElementReader). + return ( + e.callee.startsWith("%arr.idxOr.") && + e.args.length === 2 && + receiver(e.args[0]!) && + expr(e.args[1]!) + ); case "mapIntrinsic": return READING_MAP_METHODS.has(e.method) && expr(e.receiver) && e.args.every(expr); case "setIntrinsic": return READING_SET_METHODS.has(e.method) && expr(e.receiver) && e.args.every(expr); case "libCall": + // The TypeError of an assertion past a union arm leaves the loop. + if (e.fn === "error.nodeThrow") return e.args.every(expr); return ( isBorrowSafeMath(e.fn) && e.args.every((arg) => arg.type.kind === "f64") && @@ -254,14 +270,18 @@ export function preservesArrayLength( break; case "arraySet": return ( - (separate(s.arr) || - (s.index.kind === "varRef" && - loop.inRange !== null && - s.index.localId === loop.inRange)) && + (separate(s.arr) || (s.index.kind === "varRef" && inRange(s.index.localId))) && receiver(s.arr) && expr(s.index) && expr(s.value) ); + case "arraySetUndefined": + // A present undefined replacing an element below the length. + return ( + (separate(s.arr) || (s.index.kind === "varRef" && inRange(s.index.localId))) && + receiver(s.arr) && + expr(s.index) + ); case "return": return s.value === null || expr(s.value); case "fieldSet": diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index 6ac3c9068..deae5fb58 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -93,6 +93,10 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "procStream.write", "child.stdioStream", "stream.unboxDyn", + "stream.dynIterableSource", + "stream.dynSourceValue", + "stream.unboxDynStage", + "stream.dynTransformResult", "stream.handleStream", "stream.pipeDyn", "stream.unpipeDyn", @@ -186,6 +190,23 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "num.toStringRadix", "num.toStringRadixF64", "insp.jsonDyn", + // The runtime formatter calls toString, valueOf and toJSON, and refuses + // renderings it has no lowering for. + "insp.format", + "insp.formatList", + "insp.formatWithOptions", + "insp.inspect", + // The options object reads the caller's options (getters included), and + // the checked-dynamic walk calls custom inspect functions. + "insp.customOptions", + "insp.customResult", + // A checked-dynamic value the inspector does not render (a promise, a + // URL, a RegExp) throws the 'not supported yet' Error. + "insp.dyn", + "insp.dynS", + // %o's showHidden walk refuses the kinds whose hidden entries it cannot + // render. + "insp.dynHidden", // diagnostics_channel: publish runs subscribers synchronously (a throw // propagates — the documented divergence from triggerUncaughtException); // the subscribe/unsubscribe forms throw Node's ERR_INVALID_ARG_TYPE for @@ -209,6 +230,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "async.awaitDyn", "als.run", "als.exitRun", + "als.newOptions", + "als.bindCallbackDyn", "dc.chanBindStore", "dc.chanRunStores", "http.resWriteHeadDyn", @@ -247,6 +270,11 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "tls.connectCb", "tls.wrapSocket", "tls.wrapSocketCtx", + // addContext's ERR_TLS_REQUIRED_SERVER_NAME and the context's errors, + // setSecureContext's validateObject. + "tls.serverAddContext", + "tls.serverAddContextCtx", + "tls.serverSetSecureContext", // The CA-store surface: an unknown getCACertificates type throws // Node's ERR_INVALID_ARG_VALUE TypeError; setDefaultCACertificates // throws Node's ERR_CRYPTO_OPERATION_FAILED on a certificate-free @@ -584,7 +612,15 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "util.styleText", "util.toUSVString", "util.inherits", + "util.inheritsPrototype", "util.deprecate", + "util.extend", + "util.signalExitCode", + "util.diff", + "util.debounce", + "util.throttle", + "mime.typeNew", + "mime.typeParse", "util.callbackify", "util.promisifyDyn", "util.debuglog", @@ -672,10 +708,13 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "process.builtinId", "process.builtinModule", "process.builtinUnsupported", + "module.requireComputed", + "module.resolveComputed", "fs.callbackValue", "fs.callbackCall", "dyn.keyDelete", "dyn.keyDeleteComputed", + "dyn.reflectDeleteProperty", "dyn.hasKeyComputed", "dyn.hasOwnComputed", "dyn.propertyIsEnumerableComputed", @@ -696,6 +735,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "class.defineField", "class.throwReadOnly", "class.throwNotExtensible", + "record.throwFrozen", "dyn.toString", "dyn.valueOf", "string.fromCodePoint", @@ -727,6 +767,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "fs.rmSync", "fs.rmdirSync", "fs.readdirSync", + "fs.readdirRecursiveSync", "fs.readdirTypesSync", "fs.globSync", "fs.globTypesSync", @@ -738,6 +779,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "url.set", "url.setChecked", "url.fileURLToPathUrl", + "fs.checkPath", + "fs.checkReadEncoding", "url.fileURLToPathChecked", "url.fileURLToPathOptions", "url.fileURLToPathBuffer", @@ -750,6 +793,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "url.legacyResolve", "url.domainToASCII", "url.domainToUnicode", + // ERR_INVALID_ARG_TYPE and the credentials' URIError. + "url.toHttpOptions", // The win32-target flavor of pathToFileURL (same runtime entry point — // the bridge dispatches by the binary's platform): Node's win32 arm // raises ERR_INVALID_ARG_VALUE TypeErrors for malformed UNC inputs, so @@ -834,12 +879,15 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "buffer.poolFromDyn", "buffer.poolFromDynRef", "buffer.poolFromDynOffset", + "buffer.poolFromDynRange", // The checked-dynamic compare/equals validators: Node's argument // ladders throw ERR_INVALID_ARG_TYPE / ERR_OUT_OF_RANGE catchably. "buffer.compareChk", "bytes.equalsChk", "bytes.compareChk", "buffer.newStringFail", + // Inherited and symbol-keyed Buffer.prototype members throw SC2020. + "buffer.prototypeMember", "fs.toUnixTimestamp", "fs.existsChk", "fs.mkdtempChk", @@ -857,6 +905,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "net.setAutoSelTimeout", "error.argTypeThrow", "error.propTypeThrow", + "array.foreignThrow", "emitter.setMaxChk", "emitter.setDefaultMaxChk", "fs.readFileSyncBytes", @@ -897,6 +946,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "dns.setDefaultResultOrder", "net.connect", "net.connectCb", + "net.connectPath", + "net.connectPathCb", // resetAndDestroy on a TLS socket throws ERR_INVALID_HANDLE_TYPE. "net.sockResetAndDestroy", // SocketAddress's option checks and parse()'s input check. @@ -961,6 +1012,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "assert.noErrorPredicate", "assert.regexDynTest", "assert.expectsErrDyn", + "assert.throwsValidate", "assert.ifErrorErr", "assert.ifErrorF64", "assert.ifErrorStr", @@ -1013,6 +1065,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "fs.openSync", "fs.openNumericSync", "fs.readSync", + "fs.readSyncDyn", "fs.writeSync", "fs.writeStrSync", "fs.closeSync", @@ -1030,8 +1083,19 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "crypto.generateKeyPairCb", "crypto.signOneshot", "crypto.verifyOneshot", + "crypto.diffieHellman", "crypto.createSign", "crypto.createVerify", + "crypto.x509Raw", + "crypto.x509RawDer", + "crypto.x509RawStr", + // Node's argument errors (name, options, pkey) and the key parse. + "crypto.x509CheckHost", + "crypto.x509CheckEmail", + "crypto.x509CheckIP", + "crypto.x509PublicKey", + "crypto.x509Verify", + "crypto.x509CheckPrivateKey", ]); /** libCall members that always throw. Their result type is the replaced @@ -1045,6 +1109,7 @@ export const ALWAYS_THROW_LIB_FNS: ReadonlySet = new Set([ "error.nodeThrow", "error.argTypeThrow", "error.propTypeThrow", + "array.foreignThrow", "fs.mkdtempChk", "fs.readFileChk", "fs.opendirChk", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index afb06b737..b9c30779e 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -58,4896 +58,5283 @@ function defineLibFnSignatures( * together here; frontend coercion and IR validation consume the same table. * All arguments are required. Lowering supplies optional-source defaults * before this boundary and preserves their evaluation order. */ -export const LIB_FN_SIGS = defineLibFnSignatures({ - /** Native static fetch and its Web-platform companions. fetch.start - * answers once the response head arrives; the response body readers - * consume the native body stream. AbortSignal and ReadableStream values - * are opaque checked-dynamic handles. */ - "fetch.start": { argTypes: [STRING, DYN], result: { kind: "promise", inner: DYN } }, - "fetch.responseNew": { argTypes: [DYN, DYN], result: DYN }, - "fetch.responseJsonStatic": { argTypes: [DYN, DYN], result: DYN }, - "fetch.responseLight": { argTypes: [DYN], result: arrayOf(STRING) }, - "fetch.input": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, - "abort.controllerNew": { argTypes: [], result: DYN }, - /** EventTarget / Event / CustomEvent construction (scr_event_target.c): - * the Event constructors take the argument vector as one dyn array and - * throw Node's validation errors; `event.is` is `v instanceof Event` - * (kind 0), `CustomEvent` (1), `EventTarget` (2). addAbortListener - * answers the disposer (the Disposable's [Symbol.dispose]). */ - "event.targetNew": { argTypes: [], result: DYN }, - "event.new": { argTypes: [DYN], result: DYN }, - "event.customNew": { argTypes: [DYN], result: DYN }, - "event.is": { argTypes: [DYN, F64], result: BOOL }, - /** Compiled EventTarget and Event subclasses: the receiver is the - * instance (an object rooted at %EventTarget / %Event). targetInit and - * init are super() (init takes the argument vector as one dyn array - * and throws the constructor's validation errors; initType is the - * super(type) form with a string); targetCall runs an EventTarget method - * with the instance's own capsule as `self` (targetDispatch is its - * dispatchEvent(event) form, answering the bool); call, get and set - * reach the Event members. */ - "event.targetInit": { argTypes: [null], result: VOID }, - "event.init": { argTypes: [null, DYN], result: VOID }, - "event.initType": { argTypes: [null, STRING], result: VOID }, - /** %main hands the engine the compiled `listener.handleEvent` reader - * (lower-event-target.ts noteEventListeners). */ - "event.handleEventReader": { argTypes: [DYN], result: VOID }, - "event.targetDispatch": { argTypes: [null, DYN, DYN], result: BOOL }, - "event.targetCall": { argTypes: [null, DYN, STRING, DYN], result: DYN }, - "event.call": { argTypes: [null, STRING, DYN], result: DYN }, - "event.get": { argTypes: [null, STRING], result: DYN }, - "event.set": { argTypes: [null, STRING, DYN], result: VOID }, - "events.addAbortListener": { argTypes: [DYN, DYN], result: DYN }, - /** Blob / File (scr_blob.c): the constructors and slice() take their - * argument vector as one dyn array and throw Node's validation errors; - * the readers settle on Node's microtask turn for the Blob's parts. - * `blob.is` is `v instanceof Blob` (kind 0) or `File` (1). */ - "blob.new": { argTypes: [DYN], result: DYN }, - "blob.fileNew": { argTypes: [DYN], result: DYN }, - "blob.slice": { argTypes: [DYN, DYN], result: DYN }, - "blob.text": { argTypes: [DYN], result: { kind: "promise", inner: STRING } }, - "blob.bytes": { argTypes: [DYN], result: { kind: "promise", inner: BYTES_U8 } }, - "blob.arrayBuffer": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, - "blob.is": { argTypes: [DYN, F64], result: BOOL }, - /** WebAssembly (scr_wasm.c): modules translated at build time - * (wasm/translate.ts), found at run time by the SHA-256 of their bytes. - * compile and instantiate over bytes settle on a later loop turn; - * instantiate over a Module settles now, as in Node. `wasm.is` answers - * instanceof for Module, Instance, Memory, Table and Global (kinds 0-4) - * and CompileError, LinkError and RuntimeError (5-7); `wasm.errorNew` - * constructs one of the error classes (kind 5-7, message, options). */ - "wasm.compile": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, - "wasm.validate": { argTypes: [DYN], result: BOOL }, - "wasm.instantiate": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, - "wasm.moduleNew": { argTypes: [DYN], result: DYN }, - "wasm.instanceNew": { argTypes: [DYN, DYN], result: DYN }, - "wasm.is": { argTypes: [DYN, F64], result: BOOL }, - "wasm.errorNew": { argTypes: [F64, DYN, DYN], result: DYN }, - /** new WebAssembly.Memory(descriptor), Table(descriptor, value) and - * Global(descriptor, value) (an undefined value is the type's default); - * `wasm.withoutNew` throws the TypeError for Module, Instance, Memory, - * Table or Global (kinds 0-4) called without new, after its arguments - * evaluate. */ - "wasm.memoryNew": { argTypes: [DYN], result: DYN }, - "wasm.tableNew": { argTypes: [DYN, DYN], result: DYN }, - "wasm.globalNew": { argTypes: [DYN, DYN], result: DYN }, - "wasm.withoutNew": { argTypes: [F64, DYN, DYN], result: DYN }, - /** WebAssembly.Module.imports(module), exports(module) and - * customSections(module, name): checked-dynamic arrays of descriptor - * objects and ArrayBuffers. */ - "wasm.moduleImports": { argTypes: [DYN], result: DYN }, - "wasm.moduleExports": { argTypes: [DYN], result: DYN }, - "wasm.moduleCustomSections": { argTypes: [DYN, DYN], result: DYN }, - /** `v.valueOf()` on a WebAssembly-typed receiver: a Global's value, - * dyn.valueOf's answer for anything else. */ - "wasm.valueOf": { argTypes: [DYN], result: DYN }, - /** WebAssembly.compile (0), instantiate (1) and validate (2) as function - * values, and the namespace object holding them. */ - "wasm.function": { argTypes: [F64], result: DYN }, - "wasm.namespace": { argTypes: [], result: DYN }, - /** The global object viewed through a cast to a type with a WebAssembly - * member: an object holding the namespace. */ - "wasm.globalView": { argTypes: [], result: DYN }, - "abort.timeout": { argTypes: [DYN], result: DYN }, - "abort.now": { argTypes: [DYN], result: DYN }, - "abort.any": { argTypes: [DYN], result: DYN }, - "fetch.function": { argTypes: [], result: DYN }, - "fetch.requestNew": { argTypes: [DYN, DYN], result: DYN }, - "fetch.headersNew": { argTypes: [DYN], result: DYN }, - "fetch.responseArrayBuffer": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, - "fetch.responseJson": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, - "fetch.responseText": { argTypes: [DYN], result: { kind: "promise", inner: STRING } }, - "fetch.responseBytes": { argTypes: [DYN], result: { kind: "promise", inner: BYTES_U8 } }, - "fetch.abortControllerNew": { argTypes: [], result: DYN }, - "fetch.abortTimeout": { argTypes: [DYN], result: DYN }, - "fetch.abortNow": { argTypes: [DYN], result: DYN }, - "fetch.abortAny": { argTypes: [DYN], result: DYN }, - "fetch.streamNew": { argTypes: [DYN], result: DYN }, - // Program-dependent iterable: typed arrays/bytes/string stay intact so - // the native stream can pull lazily; checked-dynamic values are the - // fallback. The libCall validator in validate.ts checks the closed set. - "fetch.webIs": { argTypes: [DYN, STRING], result: BOOL }, - "fetch.streamIs": { argTypes: [DYN], result: BOOL }, - "fetch.streamFrom": { argTypes: [null], result: DYN }, - // The chunk/result record depends on ReadableStream; validated by validate.ts. - "fetch.readerRead": { argTypes: [DYN], result: VOID }, - /** Readable.fromWeb (scr_stream_from_web.c): the stream, objectMode, - * highWaterMark (< 0: the mode's default) and encoding ("": none). */ - "fetch.readableFromWeb": { argTypes: [DYN, BOOL, F64, STRING], result: VOID }, - "json.parse": { argTypes: [STRING], result: DYN }, - "json.parseReviver": { argTypes: [STRING, DYN], result: DYN }, - "json.stringifyReplacer": { argTypes: [DYN, DYN, STRING], result: DYN }, - "json.stringifyValue": { argTypes: [DYN, DYN, DYN], result: DYN }, - /** @scriptc/node calls into Node loaded into the program (scr_node_island.c): - * the calling module's path, the specifier, the export name and the - * boxed argument array, copied as JSON. callSync answers the result's - * JSON copy or throws the copied error; call answers a promise of it. */ - "nodeIsland.callSync": { argTypes: [STRING, STRING, STRING, DYN], result: DYN }, - "nodeIsland.call": { - argTypes: [STRING, STRING, STRING, DYN], - result: { kind: "promise", inner: DYN }, - }, - /** Keyed WRITE on a dyn value — `h.onDone = cb` / `h["k"] = v` on a - * checked-dynamic object (args: receiver, key string, value — all - * borrowed; the runtime copies the key and retains the value in). An - * OBJ receiver sets the member (later writes win, insertion order - * preserved — JS exactly); undefined/null throws Node's catchable - * "Cannot set properties of undefined (setting 'k')"; every other kind - * throws Node's STRICT-mode "Cannot create property 'k' on " — - * primitives quoting their rendering, V8's "on number '5'" - * (sloppy mode would ignore the write — suite tests are 'use strict'; - * SEMANTICS.md notes the sloppy divergence: the write throws). Void - * result; in the may-throw seed set. */ - "dyn.keySet": { argTypes: [DYN, STRING, DYN], result: VOID }, - "dyn.keySetComputed": { argTypes: [DYN, DYN, DYN], result: VOID }, - /** Delete an ordinary checked-native object's own key. Borrows both - * arguments; other receiver representations retain a runtime refusal. */ - "dyn.keyDelete": { argTypes: [DYN, STRING, BOOL], result: VOID }, - "dyn.keyDeleteComputed": { argTypes: [DYN, DYN, BOOL], result: VOID }, - "dyn.hasKeyComputed": { argTypes: [DYN, DYN], result: BOOL }, - "dyn.hasOwnComputed": { argTypes: [DYN, DYN], result: BOOL }, - "dyn.propertyIsEnumerableComputed": { argTypes: [DYN, DYN], result: BOOL }, - /** Native own properties on globalThis keyed by symbol identity. */ - "dyn.globalSymbolGet": { argTypes: [SYMBOL_T], result: DYN }, - "dyn.globalSymbolSet": { argTypes: [SYMBOL_T, DYN], result: VOID }, - "dyn.globalSymbolHas": { argTypes: [SYMBOL_T], result: BOOL }, - "dyn.globalSymbolDelete": { argTypes: [SYMBOL_T], result: VOID }, - /** Exact native class capsule identity, without materializing its fields. */ - "dyn.typedRefIs": { argTypes: [DYN, STRING], result: BOOL }, - /** A capsule boxed by this program from exactly this class: one key - * pointer test, false for equal keys boxed elsewhere. Guards fast paths - * whose fallback handles every other value. The key is a literal. */ - "dyn.typedRefKeyIs": { argTypes: [DYN, STRING], result: BOOL }, - "dyn.classIs": { argTypes: [DYN, STRING], result: BOOL }, - /** Destructuring pack over a dyn source — `const [a, b] = d`, a - * destructured dyn callback param (args: the source and the STATIC - * TypeError spelling, "" when the source has none — both borrowed; - * result: a fresh dyn array, +1). Iterable kinds collect like spread - * (arrays element-by-element, strings by code point, bytes by byte); - * every other kind throws V8's destructuring TypeError — the spelling - * verbatim when non-empty, else the runtime kind wording ("number 5 is - * not iterable (cannot read property Symbol(Symbol.iterator))"). In the - * may-throw seed set. */ - "dyn.iterPack": { argTypes: [DYN, STRING], result: DYN }, - "dyn.mathFold": { argTypes: [DYN, BOOL, STRING], result: F64 }, - "dyn.destructureRest": { argTypes: [DYN, STRING], result: DYN }, - "dyn.arrayFromIterator": { argTypes: [DYN], result: DYN }, - "dyn.iterator": { argTypes: [DYN, STRING], result: DYN }, - /** for await over a checked-dynamic value: the loop's iterator (a - * compiled async generator, Symbol.asyncIterator's result, or the sync - * iterator), and whether that iterator is synchronous. */ - "dyn.asyncIteratorOpen": { argTypes: [DYN, STRING], result: DYN }, - "dyn.asyncIteratorWasSync": { argTypes: [], result: BOOL }, - "dyn.iteratorResult": { argTypes: [DYN], result: DYN }, - "dyn.iteratorCanStep": { argTypes: [DYN, DYN], result: BOOL }, - "dyn.iteratorStep": { argTypes: [DYN], result: DYN }, - "dyn.iteratorStepDone": { argTypes: [DYN], result: BOOL }, - "dyn.setGeneratorMembers": { argTypes: [DYN, DYN], result: VOID }, - "dyn.iteratorHelperMethod": { argTypes: [STRING], result: DYN }, - "dyn.collectionIteratorIndex": { argTypes: [DYN], result: F64 }, - "dyn.mapSeedEntries": { argTypes: [DYN], result: DYN }, - "dyn.mapSeedEntry": { argTypes: [DYN], result: DYN }, - /** The for-of-over-dyn pack accessors — the emitted index loop drives - * them over a dyn.iterPack result (ARR by construction). arrLen: the - * ARR length as f64 (0 for non-ARR kinds; arg borrowed). arrAt: the - * element at index (+1; the undefined singleton past the end). Neither - * throws. */ - "dyn.arrLen": { argTypes: [DYN], result: F64 }, - "dyn.arrAt": { argTypes: [DYN, F64], result: DYN }, - /** `key in v` with a RUNTIME (string) key on a checked-dynamic - * receiver (args: value dyn, key string; result bool): OBJ answers - * own-member presence, ARR answers 'length'/a valid index — exactly - * the literal-key dynHasKey path. Proxy has traps may throw. */ - "dyn.hasKey": { argTypes: [DYN, STRING], result: BOOL }, - /** The `in` operator with a runtime string key: dyn.hasKey plus the - * members inherited from builtin and compiled class prototypes. */ - "dyn.in": { argTypes: [DYN, STRING], result: BOOL }, - "dyn.freeze": { argTypes: [DYN], result: DYN }, - "dyn.isFrozen": { argTypes: [DYN], result: BOOL }, - /** Object.defineProperty on a declared class field (scr_json.c): the - * field's current value (borrowed), its FIELD_* attribute word, the - * instance's CLASS_* integrity word, the field name and the descriptor. - * Validates as Node does (TypeError "Cannot redefine property: f") and - * answers a one-property holder of the defined value and attributes (+1), - * read back with class.definedBits and class.definedValue. */ - "class.defineField": { argTypes: [DYN, F64, F64, STRING, DYN], result: DYN }, - "class.definedBits": { argTypes: [DYN, STRING], result: F64 }, - "class.definedValue": { argTypes: [DYN, STRING], result: DYN }, - /** Whether a property descriptor has a `value` member. */ - "class.descriptorHasValue": { argTypes: [DYN], result: BOOL }, - /** Throws Node's TypeError for a write to a read-only class field: the - * field name and the instance's class name. */ - "class.throwReadOnly": { argTypes: [STRING, STRING], result: VOID }, - /** Throws Node's TypeError for a class field initializer on a - * non-extensible instance: the field name. */ - "class.throwNotExtensible": { argTypes: [STRING], result: VOID }, - "dyn.nativeSetNew": { argTypes: [DYN], result: DYN }, - "dyn.nativeSetIs": { argTypes: [DYN], result: BOOL }, - "dyn.nativeMapIs": { argTypes: [DYN], result: BOOL }, - "dyn.nativeUrlIs": { argTypes: [DYN], result: BOOL }, - "dyn.nativeDateIs": { argTypes: [DYN], result: BOOL }, - "dyn.nativeDateHandleIs": { argTypes: [DYN], result: BOOL }, - /** A builtin constructor (`Object`, `Date`, `RegExp`, `Map`, `Set`, - * `Promise`, `WeakMap`, `WeakSet`) read as a JavaScript value: one - * interned function per name, the same one dynamic values answer for - * `.constructor`. The name is a compiler-chosen literal. */ - "dyn.globalConstructor": { argTypes: [STRING], result: DYN }, - /** A builtin global function (setTimeout, setInterval, setImmediate, - * the clear functions, queueMicrotask, process.nextTick) read as a - * JavaScript value: one interned function per name whose calls run the - * timer APIs (it pulls in the event loop). The name is a compiler-chosen - * literal. */ - "dyn.builtinFunction": { argTypes: [STRING], result: DYN }, - "date.nativeNew": { argTypes: [DYN], result: DYN }, - /** `new Date(year, monthIndex, ...)` in JavaScript: the argument array's - * first seven values pass through ToNumber in order (later ones are - * evaluated, never converted), then the local-time field constructor. */ - "date.nativeNewFields": { argTypes: [DYN], result: DYN }, - /** `new Date(value)` in JavaScript: a string argument parses with V8's - * full date parser; every other value constructs as date.nativeNew. */ - "date.nativeNewValue": { argTypes: [DYN], result: DYN }, - "date.argumentsTime": { argTypes: [DYN], result: DATE_T }, - "date.classOperation": { argTypes: [DYN, DYN], result: VOID }, - "date.checkedValue": { argTypes: [DYN], result: DATE_T }, - "dyn.nativeRegexIs": { argTypes: [DYN], result: BOOL }, - /** toString() on a checked-dynamic receiver: runtime kind dispatch - * (bytes decode per the literal encoding — utf8 default; strings, - * numbers, booleans, arrays, objects answer JS-exactly; undefined and - * null throw the catchable TypeError). */ - "dyn.toString": { argTypes: [DYN, DYN, STRING], result: STRING }, - "dyn.valueOf": { argTypes: [DYN, STRING], result: DYN }, - /** Object.defineProperties over dyn values (args: target, descriptors — - * both borrowed dyn; result: the target, +1 — JS's return value). - * Value descriptors become own properties on OBJ and FUNC targets; - * OBJ targets preserve data attributes. get/set - * descriptors and non-object targets/descriptors throw catchably - * (Node's TypeError texts; accessors the "not supported yet" Error). In - * the may-throw seed set. */ - "dyn.defineProps": { argTypes: [DYN, DYN], result: DYN }, - "dyn.defineProperty": { argTypes: [DYN, DYN, DYN], result: DYN }, - "dyn.getOwnPropertyDescriptor": { argTypes: [DYN, DYN], result: DYN }, - "dyn.arrayProtoCall": { argTypes: [DYN, STRING, DYN], result: DYN }, - /** Promise.all / Promise.allSettled / Promise.any over a spread - * checked-dynamic array: the values, the settled-record array, or the - * first fulfillment (AggregateError once every entry rejects). - * Thenable elements adopt; the second argument reads a program class - * instance's `then` (the compiled property dispatch). */ - "dyn.promiseAll": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, - "dyn.promiseAllSettled": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, - "dyn.promiseAny": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, - /** PromiseResolve over one checked-dynamic value: the promise it holds, - * a promise that adopts a thenable's settlement, or a promise fulfilled - * with the value. The second argument reads `then` as above. */ - "dyn.promiseResolve": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, - /** Bare `typeof v` on a dyn value AS A STRING (arg: the dyn value, - * borrowed; result: an owned string) — the dyn kind's JS answer: - * undefined→"undefined", null/object/array/bytes→"object" (JS's oldest - * wart preserved), boolean/number/string by kind, function→"function". - * Never throws. */ - "dyn.typeof": { argTypes: [DYN], result: STRING }, - /** Object.prototype.toString.call on a checked-dynamic value. */ - "dyn.objectTag": { argTypes: [DYN], result: STRING }, - /** Engine-free CommonJS module graph. Module values are scalar f64 - * handles; generated startup defines the registry, init wrappers update - * loading/cache state, and the read surface answers Node's live metadata. */ - "module.registryInit": { argTypes: [F64], result: VOID }, - "module.define": { argTypes: [F64, STRING, STRING, STRING, arrayOf(STRING), BOOL], result: VOID }, - "module.enter": { argTypes: [F64], result: VOID }, - "module.link": { argTypes: [F64, F64], result: VOID }, - "module.finish": { argTypes: [F64], result: VOID }, - "module.fail": { argTypes: [F64], result: VOID }, - "module.filename": { argTypes: [F64], result: STRING }, - "module.id": { argTypes: [F64], result: STRING }, - "module.path": { argTypes: [F64], result: STRING }, - "module.paths": { argTypes: [F64], result: arrayOf(STRING) }, - "module.children": { argTypes: [F64], result: arrayOf(F64) }, - "module.parent": { argTypes: [F64], result: F64 }, - "module.loaded": { argTypes: [F64], result: BOOL }, - "module.cacheGet": { argTypes: [STRING], result: F64 }, - "module.cacheHas": { argTypes: [STRING], result: BOOL }, - "module.cacheKeys": { argTypes: [], result: arrayOf(STRING) }, - "timers.setTimeout": { argTypes: [{ kind: "func", params: [], ret: VOID }, F64], result: VOID }, - /** The repeating timer pair. setInterval takes (callback, ms) like - * setTimeout and RETURNS the f64 handle the fallback declarations - * promise (ids start at 1, so truthiness narrowing works); the loop - * owns the callback until clearInterval removes the entry (eagerly — a - * live interval keeps the loop alive, a cleared one releases it, like - * Node). Neither throws; a throw ESCAPING an interval callback ends the - * program like a setTimeout throw. */ - "timers.setInterval": { argTypes: [{ kind: "func", params: [], ret: VOID }, F64], result: F64 }, - "timers.clearInterval": { argTypes: [F64], result: VOID }, - /** setTimeout WITH a clear handle (the f64 id) — the clearable/unref-able - * one-shot; clearTimeout cancels it, .unref() drops it from loop - * liveness. The plain timers.setTimeout stays the handle-less - * fire-and-forget. */ - "timers.setTimeoutHandle": { - argTypes: [{ kind: "func", params: [], ret: VOID }, F64], - result: F64, - }, - "timers.clearTimeout": { argTypes: [F64], result: VOID }, - /** Timeout.unref()/ref()/hasRef() — loop-liveness bookkeeping over the - * handle id. unref/ref RETURN the handle (f64) for chaining; hasRef - * returns bool. */ - "timers.unref": { argTypes: [F64], result: F64 }, - "timers.ref": { argTypes: [F64], result: F64 }, - "timers.hasRef": { argTypes: [F64], result: BOOL }, - /** Timeout.refresh() — re-arm to now + the original delay (from the - * heap or from inside the firing callback; a one-shot that fired on an - * earlier turn is gone and no-ops — documented divergence). Returns - * the handle for chaining like unref/ref. */ - "timers.refresh": { argTypes: [F64], result: F64 }, - /** setImmediate/clearImmediate — Node's check phase: callbacks run - * once per loop turn AFTER due timers, FIFO, and immediates queued - * mid-phase wait for the next turn. setImmediate returns the f64 - * handle (its own id space — clearTimeout of an Immediate no-ops, - * like Node); the Immediate ref trio mirrors the Timeout one (an - * unref'd pending immediate neither keeps the loop alive nor fires - * once nothing reffed remains). */ - "timers.setImmediate": { argTypes: [{ kind: "func", params: [], ret: VOID }], result: F64 }, - /** queueMicrotask (scr_async.c): the callback enters the SAME FIFO - * promise continuations ride — one microtask order, like V8's queue — - * and a throw is an UNCAUGHT exception (never a rejection). - * timers.queueMicrotask takes an owned zero-param closure and never - * throws; the Dyn form (checked-dynamic arguments — the mustCall - * wrapper, the suite's invalid-input probes) throws Node's - * ERR_INVALID_ARG_TYPE synchronously on a non-function value and calls - * the function with zero arguments at drain. */ - "timers.queueMicrotask": { argTypes: [{ kind: "func", params: [], ret: VOID }], result: VOID }, - "timers.queueMicrotaskDyn": { argTypes: [DYN], result: VOID }, - "timers.clearImmediate": { argTypes: [F64], result: VOID }, - /** process.nextTick(cb, ...args) — the user tick queue. args: [cb - * (() => void; trailing call arguments ride the timer surface's - * interned dyn thunk)]. Ticks drain BEFORE promise jobs at every loop - * checkpoint, to joint exhaustion with them (Node's tick-then- - * microtask order); ticks enqueued by station listeners run at the - * NEXT checkpoint (the stream-tick station divergence, SEMANTICS.md). - * Pending ticks are always-ready work — the loop neither sleeps nor - * exits while any exist; ticks scheduled from 'exit' listeners never - * run (Node). The enqueue itself never throws. */ - "process.nextTick": { argTypes: [{ kind: "func", params: [], ret: VOID }], result: VOID }, - /** The process introspection statics. uptime: seconds since the - * binary's own start (fractional — a load-time monotonic anchor). - * availableMemory/constrainedMemory: libuv's numbers (free-ish bytes; - * the cgroup cap or 0). cpuUser/cpuSystem (+ threadCpu twins): the - * process/thread CPU clocks in microseconds — the frontend composes - * the {user, system} records. The *Diff forms answer current − prev - * for one field; cpuPrevValidate throws Node's ERR_INVALID_ARG_VALUE - * RangeError for negative/non-finite prev fields (user first, then - * system — Node's order; MAY THROW). rusage(idx): one - * process.resourceUsage() field by canonical index (Node's units). - * activeResources: the loop's own bookkeeping — 'Timeout' per armed - * (or firing, uncleared) timer, 'Immediate' per queued unfired - * immediate; unmodeled resource kinds are absent (SEMANTICS.md). */ - "process.uptime": { argTypes: [], result: F64 }, - /** perf_hooks performance.now(): fractional ms since process start. */ - "perf.now": { argTypes: [], result: F64 }, - /** perf_hooks user timing over the thread's performance timeline. The - * producing calls take the call's whole argument vector as one dyn - * array (Node's validation and errors run in the runtime, MAY THROW): - * mark/measure answer 1, entries(args, which) the entry count of - * getEntries (0), getEntriesByName (1) or getEntriesByType (2). The - * entryName/Type/Start/Duration/Detail accessors read entry i of that - * answer; the frontend builds a new entry's record from them and hands - * it to entryKeep (a typed reference capsule), and the getters read the - * kept records back through entryRef, so entries keep their identity. */ - "perf.mark": { argTypes: [DYN], result: F64 }, - "perf.measure": { argTypes: [DYN], result: F64 }, - "perf.entries": { argTypes: [DYN, F64], result: F64 }, - "perf.clearMarks": { argTypes: [DYN], result: VOID }, - "perf.clearMeasures": { argTypes: [DYN], result: VOID }, - "perf.entryName": { argTypes: [F64], result: STRING }, - "perf.entryType": { argTypes: [F64], result: STRING }, - "perf.entryStart": { argTypes: [F64], result: F64 }, - "perf.entryDuration": { argTypes: [F64], result: F64 }, - "perf.entryDetail": { argTypes: [F64], result: DYN }, - "perf.entryKeep": { argTypes: [DYN], result: VOID }, - "perf.entryRef": { argTypes: [F64], result: DYN }, - /** perf_hooks event-loop metrics (scr_perf_loop.c): loopStart is the - * loop's start in performance.now() ms (0 before it), idleTime the idle - * ms since. eldNew(resolution ms) creates a monitorEventLoopDelay - * histogram handle (MAY THROW Node's resolution RangeError); enable/disable answer whether the state changed; - * eldPercentile expects Node's validated range; eldStat(which) reads - * 0 min, 1 max, 2 mean, 3 stddev, 4 count, 5 exceeds. */ - "perf.loopStart": { argTypes: [], result: F64 }, - "perf.idleTime": { argTypes: [], result: F64 }, - "perf.eldNew": { argTypes: [F64], result: F64 }, - "perf.eldEnable": { argTypes: [F64], result: BOOL }, - "perf.eldDisable": { argTypes: [F64], result: BOOL }, - "perf.eldReset": { argTypes: [F64], result: VOID }, - "perf.eldPercentile": { argTypes: [F64, F64], result: F64 }, - "perf.eldStat": { argTypes: [F64, F64], result: F64 }, - /** process.memoryUsage(): memoryUsageTake samples the resident set and - * the runtime heap once (MAY THROW Node's uv_resident_set_memory error) - * and memoryUsageField(k) reads that sample (0 rss, 1 heapTotal, 2 - * heapUsed, 3 external, 4 arrayBuffers). rss is memoryUsage.rss(). */ - "process.memoryUsageTake": { argTypes: [], result: VOID }, - "process.memoryUsageField": { argTypes: [F64], result: F64 }, - "process.rss": { argTypes: [], result: F64 }, - "process.availableMemory": { argTypes: [], result: F64 }, - "process.constrainedMemory": { argTypes: [], result: F64 }, - "process.cpuUser": { argTypes: [], result: F64 }, - "process.cpuSystem": { argTypes: [], result: F64 }, - "process.cpuUserDiff": { argTypes: [F64], result: F64 }, - "process.cpuSystemDiff": { argTypes: [F64], result: F64 }, - "process.threadCpuUser": { argTypes: [], result: F64 }, - "process.threadCpuSystem": { argTypes: [], result: F64 }, - "process.threadCpuUserDiff": { argTypes: [F64], result: F64 }, - "process.threadCpuSystemDiff": { argTypes: [F64], result: F64 }, - "process.cpuPrevValidate": { argTypes: [F64, F64], result: VOID }, - "process.rusage": { argTypes: [F64], result: F64 }, - "process.activeResources": { argTypes: [], result: arrayOf(STRING) }, - "timers.immediateUnref": { argTypes: [F64], result: F64 }, - "timers.immediateRef": { argTypes: [F64], result: F64 }, - "timers.immediateHasRef": { argTypes: [F64], result: BOOL }, - /** The tolerated non-handle clear (`clearTimeout(null)`, - * `clearInterval({})`, zero-argument forms): Node ignores anything - * that is not a live handle without an error — a VOID no-op the emitter drops - * (only syntactically side-effect-free arguments take this path). */ - "timers.clearNoop": { argTypes: [], result: VOID }, - // Signal listeners are zero-param (the ambient shape); exit/stdin - // callbacks carry program-dependent one-param shapes — null slots, the - // libCall case checks them (child.onExit precedent). - /** Named process signal events: [name string, callback dyn, once bool] - * or [name, callback] for removal. The runtime resolves platform signal - * numbers and delivers (name, number). Listeners do not keep the loop - * alive. Invalid callbacks and uncatchable signals throw. */ - "process.onSignal": { argTypes: [STRING, DYN, BOOL], result: VOID }, - "process.offSignal": { argTypes: [STRING, DYN], result: VOID }, - /** The process 'exit' event — process.on/once/off("exit"). args: [cb, - * once bool] / [cb]. Listeners run SYNCHRONOUSLY at termination - * (normal exit, process.exit(), the uncaught/unhandled exit-1 paths) - * with the exit code; anything they schedule never runs, like Node. - * Callback shapes: () => void or (code: number) => void — the emitter - * picks the runtime adapter. Never throw. */ - "process.onExit": { argTypes: [null, BOOL], result: VOID }, - "process.offExit": { argTypes: [null], result: VOID }, - /** process.stdin listener registration — stdin.on/once("data" | "end" | - * "error", cb). args: [cb, once bool]. data callbacks: () => void or - * (chunk: Uint8Array) => void; end: () => void; error: () => void or - * (err: Error) => void (the child error adapters are reused). While a - * data listener (or a parked for-await chunk promise) exists, stdin - * keeps the loop alive — Node's flowing-stdin keep-alive. Never - * throw. */ - "stdin.onData": { argTypes: [null, BOOL], result: VOID }, - "stdin.onEnd": { argTypes: [{ kind: "func", params: [], ret: VOID }, BOOL], result: VOID }, - "stdin.onError": { argTypes: [null, BOOL], result: VOID }, - /** process.stdin.on/once('keypress', cb) (scr_readline_tty.c): cb is the - * listener as a checked-dynamic function, called with (str, key). */ - "stdin.onKeypress": { argTypes: [DYN, BOOL], result: VOID }, - /** The for-await chunk source over process.stdin: no args, result - * Promise (+1). Fulfills with the next arrived chunk; the - * EMPTY bytes value is the done sentinel (POSIX reads never deliver - * empty data chunks), which the for-await desugar turns into loop - * exit. Never throws itself; awaiting it re-throws nothing (stdin - * errors surface through 'error' listeners, not the iterator). */ - "stdin.nextChunk": { argTypes: [], result: { kind: "promise", inner: BYTES_U8 } }, - "fs.readFileSync": { argTypes: [STRING, STRING], result: STRING }, - "fs.readFileEncoded": { argTypes: [STRING, STRING], result: STRING }, - "fs.readFdEncoded": { argTypes: [F64, STRING], result: STRING }, - /** readFileSync(path) — the Buffer read (+1 bytes); throws catchably - * like the utf8 form. */ - "fs.readFileSyncBuf": { argTypes: [STRING], result: BYTES_U8 }, - /** readFileSync(path, enc) with a RUNTIME encoding (an untyped JS - * parameter): undefined/null answer Buffers, utf8 a string, other real - * encodings throw the "not supported yet" Error, unknown names throw - * ERR_UNKNOWN_ENCODING. */ - "fs.readFileSyncDyn": { argTypes: [STRING, DYN], result: DYN }, - "fs.writeFileSync": { argTypes: [STRING, STRING], result: VOID }, - "fs.appendFileSync": { argTypes: [STRING, STRING], result: VOID }, - "fs.existsSync": { argTypes: [STRING], result: BOOL }, - "fs.mkdirSync": { argTypes: [STRING], result: VOID }, - "fs.rmSync": { argTypes: [STRING], result: VOID }, - "fs.rmdirSync": { argTypes: [STRING], result: VOID }, - "fs.readdirSync": { argTypes: [STRING], result: arrayOf(STRING) }, - // Result is the call site's Dirent record array (VOID is the - // structure-checked sentinel, the os.networkInterfaces pattern). - /** `fs.readdirSync(path, { withFileTypes: true })` — Dirent rows over - * one readdir pass (scr_lib.c's scandir snapshot; DT_UNKNOWN falls back - * to lstat, Node's getDirents rule). The result type is the call site's - * interned Dirent record array (name, parentPath, hidden %dtype in - * libuv's UV_DIRENT encoding) — verified by the frontend; the emitter - * assembles the rows from the snapshot. Byte order on POSIX (libuv - * sorts), the directory's order on Windows; no "."/"..". Throws - * Node's scandir errno error (may-throw seed set); fresh +1 array. */ - "fs.readdirTypesSync": { argTypes: [STRING], result: VOID }, - /** fs.globSync / fs.glob / fs/promises glob (scr_glob.c, Node's - * lib/internal/fs/glob.js walk): pattern and options are dyn values the - * runtime validates in Node's order. The string forms answer string[]; - * the Dirent forms answer the call site's Dirent record array (validated - * in validate.ts) assembled from the snapshot; fs.callbackGlob takes the - * argument vector and calls back with string[]. */ - "fs.globSync": { argTypes: [DYN, DYN], result: arrayOf(STRING) }, - "fs.globTypesSync": { argTypes: [DYN, DYN], result: VOID }, - "fs.callbackGlob": { argTypes: [DYN], result: VOID }, - "fsp.globStrings": { argTypes: [DYN, DYN], result: arrayOf(STRING) }, - "fsp.globTypes": { argTypes: [DYN, DYN], result: VOID }, - /** The error an fs/promises glob walk ended with (undefined when none), - * read once right after fsp.globStrings / fsp.globTypes. */ - "fsp.globError": { argTypes: [], result: DYN }, - /** fs.Dir (scr_lib.c): opendirSync(path, bufferSize) throws Node's - * opendir error (or the options.bufferSize RangeError); dir.readSync - * answers the call site's `Dirent | null` (VOID here — the emitter builds - * the row like readdirTypesSync) and dir.read is its promise twin; - * readSync and closeSync throw ERR_DIR_CLOSED on a closed Dir, read and - * close reject with it. */ - "fs.opendirSync": { argTypes: [STRING, F64], result: DIR_T }, - "fsp.opendir": { argTypes: [STRING, F64], result: { kind: "promise", inner: DIR_T } }, - "dir.path": { argTypes: [DIR_T], result: STRING }, - "dir.readSync": { argTypes: [DIR_T], result: VOID }, - "dir.read": { argTypes: [DIR_T], result: { kind: "promise", inner: VOID } }, - "dir.closeSync": { argTypes: [DIR_T], result: VOID }, - "dir.close": { argTypes: [DIR_T], result: { kind: "promise", inner: VOID } }, - /** node:path (scr_path.c ports BOTH of Node's implementations - * function-by-function): the `path.*` family is posix, the - * `path.win32*` family is Node v24's path.win32 ported exactly — the - * frontend binds the bare module to the TARGET platform's family - * (Node on Windows IS path.win32) and the path.posix / path.win32 - * namespaces to their own family everywhere. join and resolve take ONE - * string[] arg — the frontend packs the variadic call's arguments into - * an array literal. basename always receives its suffix (the frontend - * completes an omitted one with "", a Node no-op). toNamespacedPath is - * the posix identity and the win32 \\?\-prefixer. None of these throw; - * the resolves consult the process cwd like Node's. */ - "path.join": { argTypes: [arrayOf(STRING)], result: STRING }, - "path.resolve": { argTypes: [arrayOf(STRING)], result: STRING }, - "path.normalize": { argTypes: [STRING], result: STRING }, - "path.dirname": { argTypes: [STRING], result: STRING }, - "path.basename": { argTypes: [STRING, STRING], result: STRING }, - "path.extname": { argTypes: [STRING], result: STRING }, - "path.isAbsolute": { argTypes: [STRING], result: BOOL }, - "path.relative": { argTypes: [STRING, STRING], result: STRING }, - "path.toNamespacedPath": { argTypes: [STRING], result: STRING }, - "path.win32Join": { argTypes: [arrayOf(STRING)], result: STRING }, - "path.win32Resolve": { argTypes: [arrayOf(STRING)], result: STRING }, - "path.win32Normalize": { argTypes: [STRING], result: STRING }, - "path.win32Dirname": { argTypes: [STRING], result: STRING }, - "path.win32Basename": { argTypes: [STRING, STRING], result: STRING }, - "path.win32Extname": { argTypes: [STRING], result: STRING }, - "path.win32IsAbsolute": { argTypes: [STRING], result: BOOL }, - "path.win32Relative": { argTypes: [STRING, STRING], result: STRING }, - "path.win32ToNamespacedPath": { argTypes: [STRING], result: STRING }, - /** path.parse: [root, dir, base, ext, name] as a fresh string[] (the - * frontend builds the ParsedPath record); path.format: the pathObject's - * root, dir, base, name and ext with undefined spelled "". */ - "path.parse": { argTypes: [STRING], result: arrayOf(STRING) }, - "path.win32Parse": { argTypes: [STRING], result: arrayOf(STRING) }, - "path.format": { argTypes: [STRING, STRING, STRING, STRING, STRING], result: STRING }, - "path.win32Format": { argTypes: [STRING, STRING, STRING, STRING, STRING], result: STRING }, - /** path.matchesGlob(path, pattern): Node's matchGlobPattern over its - * bundled minimatch (scr_glob.c). Both families match case-insensitively - * on macOS and Windows hosts like Node's; win32 also accepts either - * separator. Throws TypeError for a pattern over 64 Ki code units, - * RangeError for a brace sequence too long to expand, and SyntaxError - * when a segment's regex is invalid under the u flag. */ - "path.matchesGlob": { argTypes: [STRING, STRING], result: BOOL }, - "path.win32MatchesGlob": { argTypes: [STRING, STRING], result: BOOL }, - /** node:os: homedir is $HOME else getpwuid(3); tmpdir is Node's env - * cascade ($TMPDIR/$TMP/$TEMP else /tmp, one trailing slash trimmed). - * os.platform() lowers to process.platform — one implementation. */ - "os.homedir": { argTypes: [], result: STRING }, - /** os.release(): uname(2)'s release field — Node's own implementation - * (the kernel version string, e.g. "24.6.0" on macOS 15). Interned; +1 - * per read. Never throws. */ - "os.release": { argTypes: [], result: STRING }, - /** os.type(): uname(2)'s sysname ("Darwin", "Linux"; "Windows_NT" on - * win32) — Node's uv_os_uname answer. Interned per call; never throws. */ - "os.type": { argTypes: [], result: STRING }, - /** os.totalmem(): total physical memory in bytes. Never throws. */ - "os.totalmem": { argTypes: [], result: F64 }, - /** os.availableParallelism(): CPUs the process may use (libuv's answer), >= 1. Never throws. */ - "os.availableParallelism": { argTypes: [], result: F64 }, - /** os.hostname/machine/version/endianness, freemem, uptime and loadavg: - * libuv's answers (uname's machine and version fields, gethostname, - * "LE"/"BE", free memory in bytes, uptime in seconds, the three load - * averages). Fresh results; never throw. */ - "os.hostname": { argTypes: [], result: STRING }, - "os.machine": { argTypes: [], result: STRING }, - "os.version": { argTypes: [], result: STRING }, - "os.endianness": { argTypes: [], result: STRING }, - "os.freemem": { argTypes: [], result: F64 }, - "os.uptime": { argTypes: [], result: F64 }, - "os.loadavg": { argTypes: [], result: arrayOf(F64) }, - /** os.getPriority(pid): validateInt32 (RangeError ERR_OUT_OF_RANGE), - * then the priority; a failure throws Node's SystemError. */ - "os.getPriority": { argTypes: [F64], result: F64 }, - /** os.setPriority(pid, priority): validateInt32 for both (priority in - * -20..19, RangeError ERR_OUT_OF_RANGE), then the priority; a failure - * throws Node's SystemError. */ - "os.setPriority": { argTypes: [F64, F64], result: VOID }, - /** os.cpus(): cpusTake snapshots every CPU and answers the count; - * cpusModel(i) and cpusField(i, k) (k: 0 speed, 1 user, 2 nice, 3 sys, - * 4 idle, 5 irq) read the snapshot. Never throw. */ - "os.cpusTake": { argTypes: [], result: F64 }, - "os.cpusModel": { argTypes: [F64], result: STRING }, - "os.cpusField": { argTypes: [F64, F64], result: F64 }, - /** os.constants: the per-thread null-prototype constants object (dlopen, - * errno, signals, priority, UV_UDP_REUSEADDR). Never throws. */ - "os.constants": { argTypes: [], result: DYN }, - /** fs.constants: the target's cached null-prototype constants object. */ - "fs.constants": { argTypes: [], result: DYN }, - /** The deprecated node:constants object (scr_os.c): the os groups and - * fs.constants merged with the borrowed baked crypto.constants table, - * frozen, one per thread. */ - "constants.object": { argTypes: [DYN], result: DYN }, - /** umask(2): arg < 0 reads without setting (umask has no read-only form - * — set 0, restore); otherwise sets and answers the PREVIOUS mask. - * Never throws. */ - "process.umask": { argTypes: [F64], result: F64 }, - /** chdir(2) — throws Node's fs-shaped error (ENOENT/EACCES/ENOTDIR, - * syscall "chdir") on failure. */ - "process.chdir": { argTypes: [STRING], result: VOID }, - "process.loadEnvFile": { argTypes: [DYN], result: VOID }, - /** process._exiting: true once the exit sequence began (exit listeners - * running) — the runtime flag scr_run_exit_listeners/process.exit set. - * Never throws. */ - "process.exiting": { argTypes: [], result: BOOL }, - /** net's process-wide happy-eyeballs attempt budget (Node's default - * 250ms): one runtime double in the core unit, so reading/writing it - * never forces the net unit into the link. Never throw. */ - "net.getAutoSelTimeout": { argTypes: [], result: F64 }, - "net.setAutoSelTimeout": { argTypes: [F64], result: VOID }, - /** realpath(3) with Node's error shape (syscall "lstat" in the message, - * Node's own spelling for realpathSync failures). +1 fresh string. */ - "fs.realpathSync": { argTypes: [STRING], result: STRING }, - /** fs.realpathSync (or its .native when the flag is true) as a function - * value: path and options arrive unchecked; the result is a string or a - * Buffer by the options' encoding. */ - "fs.realpathValue": { argTypes: [DYN, DYN, BOOL], result: DYN }, - /** Native realpath with syscall "realpath" on failure. +1 fresh string. */ - "fs.realpathNativeSync": { argTypes: [STRING], result: STRING }, - /** The os.userInfo() field trio (uv_os_get_passwd's slices): pw_name, - * pw_shell, pw_dir — the PASSWD homedir, not os.homedir's $HOME-first - * cascade (Node's own split). The frontend assembles the UserInfo - * record from these plus getuid/getgid. +1 fresh strings; a passwd - * lookup failure aborts (Node throws a system error there — no - * compiled program path reaches it for the running uid). */ - "os.userName": { argTypes: [], result: STRING }, - "os.userShell": { argTypes: [], result: STRING }, - "os.userHomedir": { argTypes: [], result: STRING }, - "os.tmpdir": { argTypes: [], result: STRING }, - // Result is the call site's Dict record (VOID is - // the dgram.address sentinel — the libCall case checks the structure). - /** os.networkInterfaces(): getifaddrs(3) → the Dict - * record, built inline by the emitter from a runtime snapshot (scr_lib.c). - * The result type is the CALL SITE's mapped @types/node shape — a pure - * index-signature record whose value is `Info[] | undefined`, Info a - * two-record union (IPv4: scopeid `number | undefined` holding undefined; - * IPv6: scopeid number) — verified structurally by the frontend. Rows - * match libuv's filter (IFF_UP && IFF_RUNNING, AF_INET/AF_INET6; loopback - * = internal; MACs from the interface's link-level sibling entry, zeros - * when absent; cidr from the netmask's contiguous prefix, the null arm - * when it is missing or non-contiguous). Key/row order follows the OS's - * getifaddrs enumeration — Node itself does not guarantee an order. - * Fresh +1 record; never throws (a getifaddrs failure yields {}). */ - "os.networkInterfaces": { argTypes: [], result: VOID }, - /** `Math.max(...xs)` / `Math.min(...xs)` over one spread number[] - * (scr_number.c): the JS fold exactly — any NaN element poisons the - * result, ±0 order by the JS comparison (max prefers +0, min prefers - * -0), and the empty array yields -Infinity / +Infinity like the - * zero-argument calls. Borrows the array; never throws. */ - "math.maxArr": { argTypes: [arrayOf(F64)], result: F64 }, - "math.minArr": { argTypes: [arrayOf(F64)], result: F64 }, - "math.hypotArr": { argTypes: [arrayOf(F64)], result: F64 }, - /** `Math.floor(x)` — C floor() IS the JS operation (NaN/±0/±Infinity - * pass through bit-exactly). Never throws. */ - "math.floor": { argTypes: [F64], result: F64 }, - /** Math.abs (C fabs — IS the JS operation) and Math.round (scr_lib.c: - * ECMA half-toward-+Infinity with the exact-fraction comparison — C - * round() is half-away-from-zero and floor(x+0.5) drifts at the - * epsilon boundary). Borrow nothing; never throw. */ - "math.abs": { argTypes: [F64], result: F64 }, - "math.round": { argTypes: [F64], result: F64 }, +function buildLibFnSignatures() { + return defineLibFnSignatures({ + /** Native static fetch and its Web-platform companions. fetch.start + * answers once the response head arrives; the response body readers + * consume the native body stream. AbortSignal and ReadableStream values + * are opaque checked-dynamic handles. */ + "fetch.start": { argTypes: [STRING, DYN], result: { kind: "promise", inner: DYN } }, + "fetch.responseNew": { argTypes: [DYN, DYN], result: DYN }, + "fetch.responseJsonStatic": { argTypes: [DYN, DYN], result: DYN }, + "fetch.responseLight": { argTypes: [DYN], result: arrayOf(STRING) }, + "fetch.input": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, + "abort.controllerNew": { argTypes: [], result: DYN }, + /** EventTarget / Event / CustomEvent construction (scr_event_target.c): + * the Event constructors take the argument vector as one dyn array and + * throw Node's validation errors; `event.is` is `v instanceof Event` + * (kind 0), `CustomEvent` (1), `EventTarget` (2). addAbortListener + * answers the disposer (the Disposable's [Symbol.dispose]). */ + "event.targetNew": { argTypes: [], result: DYN }, + "event.new": { argTypes: [DYN], result: DYN }, + "event.customNew": { argTypes: [DYN], result: DYN }, + "event.is": { argTypes: [DYN, F64], result: BOOL }, + /** Compiled EventTarget and Event subclasses: the receiver is the + * instance (an object rooted at %EventTarget / %Event). targetInit and + * init are super() (init takes the argument vector as one dyn array + * and throws the constructor's validation errors; initType is the + * super(type) form with a string); targetCall runs an EventTarget method + * with the instance's own capsule as `self` (targetDispatch is its + * dispatchEvent(event) form, answering the bool); call, get and set + * reach the Event members. */ + "event.targetInit": { argTypes: [null], result: VOID }, + "event.init": { argTypes: [null, DYN], result: VOID }, + "event.initType": { argTypes: [null, STRING], result: VOID }, + /** %main hands the engine the compiled `listener.handleEvent` reader + * (lower-event-target.ts noteEventListeners). */ + "event.handleEventReader": { argTypes: [DYN], result: VOID }, + "event.targetDispatch": { argTypes: [null, DYN, DYN], result: BOOL }, + "event.targetCall": { argTypes: [null, DYN, STRING, DYN], result: DYN }, + "event.call": { argTypes: [null, STRING, DYN], result: DYN }, + "event.get": { argTypes: [null, STRING], result: DYN }, + "event.set": { argTypes: [null, STRING, DYN], result: VOID }, + "events.addAbortListener": { argTypes: [DYN, DYN], result: DYN }, + /** Blob / File (scr_blob.c): the constructors and slice() take their + * argument vector as one dyn array and throw Node's validation errors; + * the readers settle on Node's microtask turn for the Blob's parts. + * `blob.is` is `v instanceof Blob` (kind 0) or `File` (1). */ + "blob.new": { argTypes: [DYN], result: DYN }, + "blob.fileNew": { argTypes: [DYN], result: DYN }, + "blob.slice": { argTypes: [DYN, DYN], result: DYN }, + "blob.text": { argTypes: [DYN], result: { kind: "promise", inner: STRING } }, + "blob.bytes": { argTypes: [DYN], result: { kind: "promise", inner: BYTES_U8 } }, + "blob.arrayBuffer": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, + "blob.is": { argTypes: [DYN, F64], result: BOOL }, + /** WebAssembly (scr_wasm.c): modules translated at build time + * (wasm/translate.ts), found at run time by the SHA-256 of their bytes. + * compile and instantiate over bytes settle on a later loop turn; + * instantiate over a Module settles now, as in Node. `wasm.is` answers + * instanceof for Module, Instance, Memory, Table and Global (kinds 0-4) + * and CompileError, LinkError and RuntimeError (5-7); `wasm.errorNew` + * constructs one of the error classes (kind 5-7, message, options). */ + "wasm.compile": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, + "wasm.validate": { argTypes: [DYN], result: BOOL }, + "wasm.instantiate": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, + "wasm.moduleNew": { argTypes: [DYN], result: DYN }, + "wasm.instanceNew": { argTypes: [DYN, DYN], result: DYN }, + "wasm.is": { argTypes: [DYN, F64], result: BOOL }, + "wasm.errorNew": { argTypes: [F64, DYN, DYN], result: DYN }, + /** new WebAssembly.Memory(descriptor), Table(descriptor, value) and + * Global(descriptor, value) (an undefined value is the type's default); + * `wasm.withoutNew` throws the TypeError for Module, Instance, Memory, + * Table or Global (kinds 0-4) called without new, after its arguments + * evaluate. */ + "wasm.memoryNew": { argTypes: [DYN], result: DYN }, + "wasm.tableNew": { argTypes: [DYN, DYN], result: DYN }, + "wasm.globalNew": { argTypes: [DYN, DYN], result: DYN }, + "wasm.withoutNew": { argTypes: [F64, DYN, DYN], result: DYN }, + /** WebAssembly.Module.imports(module), exports(module) and + * customSections(module, name): checked-dynamic arrays of descriptor + * objects and ArrayBuffers. */ + "wasm.moduleImports": { argTypes: [DYN], result: DYN }, + "wasm.moduleExports": { argTypes: [DYN], result: DYN }, + "wasm.moduleCustomSections": { argTypes: [DYN, DYN], result: DYN }, + /** `v.valueOf()` on a WebAssembly-typed receiver: a Global's value, + * dyn.valueOf's answer for anything else. */ + "wasm.valueOf": { argTypes: [DYN], result: DYN }, + /** WebAssembly.compile (0), instantiate (1) and validate (2) as function + * values, and the namespace object holding them. */ + "wasm.function": { argTypes: [F64], result: DYN }, + "wasm.namespace": { argTypes: [], result: DYN }, + /** The global object viewed through a cast to a type with a WebAssembly + * member: an object holding the namespace. */ + "wasm.globalView": { argTypes: [], result: DYN }, + "abort.timeout": { argTypes: [DYN], result: DYN }, + "abort.now": { argTypes: [DYN], result: DYN }, + "abort.any": { argTypes: [DYN], result: DYN }, + "fetch.function": { argTypes: [], result: DYN }, + "fetch.requestNew": { argTypes: [DYN, DYN], result: DYN }, + "fetch.headersNew": { argTypes: [DYN], result: DYN }, + "fetch.responseArrayBuffer": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, + "fetch.responseJson": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, + "fetch.responseText": { argTypes: [DYN], result: { kind: "promise", inner: STRING } }, + "fetch.responseBytes": { argTypes: [DYN], result: { kind: "promise", inner: BYTES_U8 } }, + "fetch.abortControllerNew": { argTypes: [], result: DYN }, + "fetch.abortTimeout": { argTypes: [DYN], result: DYN }, + "fetch.abortNow": { argTypes: [DYN], result: DYN }, + "fetch.abortAny": { argTypes: [DYN], result: DYN }, + "fetch.streamNew": { argTypes: [DYN], result: DYN }, + "fetch.transformNew": { argTypes: [], result: DYN }, + // Program-dependent iterable: typed arrays/bytes/string stay intact so + // the native stream can pull lazily; checked-dynamic values are the + // fallback. The libCall validator in validate.ts checks the closed set. + "fetch.webIs": { argTypes: [DYN, STRING], result: BOOL }, + "fetch.streamIs": { argTypes: [DYN], result: BOOL }, + "fetch.streamFrom": { argTypes: [null], result: DYN }, + // The chunk/result record depends on ReadableStream; validated by validate.ts. + "fetch.readerRead": { argTypes: [DYN], result: VOID }, + /** Readable.fromWeb (scr_stream_from_web.c): the stream, objectMode, + * highWaterMark (< 0: the mode's default) and encoding ("": none). */ + "fetch.readableFromWeb": { argTypes: [DYN, BOOL, F64, STRING], result: VOID }, + "json.parse": { argTypes: [STRING], result: DYN }, + "json.parseReviver": { argTypes: [STRING, DYN], result: DYN }, + "json.stringifyReplacer": { argTypes: [DYN, DYN, STRING], result: DYN }, + "json.stringifyValue": { argTypes: [DYN, DYN, DYN], result: DYN }, + /** @scriptc/node calls into Node loaded into the program (scr_node_island.c): + * the calling module's path, the specifier, the export name and the + * boxed argument array, copied as JSON. callSync answers the result's + * JSON copy or throws the copied error; call answers a promise of it. */ + "nodeIsland.callSync": { argTypes: [STRING, STRING, STRING, DYN], result: DYN }, + "nodeIsland.call": { + argTypes: [STRING, STRING, STRING, DYN], + result: { kind: "promise", inner: DYN }, + }, + /** Keyed WRITE on a dyn value — `h.onDone = cb` / `h["k"] = v` on a + * checked-dynamic object (args: receiver, key string, value — all + * borrowed; the runtime copies the key and retains the value in). An + * OBJ receiver sets the member (later writes win, insertion order + * preserved — JS exactly); undefined/null throws Node's catchable + * "Cannot set properties of undefined (setting 'k')"; every other kind + * throws Node's STRICT-mode "Cannot create property 'k' on " — + * primitives quoting their rendering, V8's "on number '5'" + * (sloppy mode would ignore the write — suite tests are 'use strict'; + * SEMANTICS.md notes the sloppy divergence: the write throws). Void + * result; in the may-throw seed set. */ + "dyn.keySet": { argTypes: [DYN, STRING, DYN], result: VOID }, + "dyn.keySetComputed": { argTypes: [DYN, DYN, DYN], result: VOID }, + /** Delete an ordinary checked-native object's own key. Borrows both + * arguments; other receiver representations retain a runtime refusal. */ + "dyn.keyDelete": { argTypes: [DYN, STRING, BOOL], result: VOID }, + "dyn.keyDeleteComputed": { argTypes: [DYN, DYN, BOOL], result: VOID }, + "dyn.reflectDeleteProperty": { argTypes: [DYN, DYN], result: BOOL }, + "dyn.hasKeyComputed": { argTypes: [DYN, DYN], result: BOOL }, + "dyn.hasOwnComputed": { argTypes: [DYN, DYN], result: BOOL }, + "dyn.propertyIsEnumerableComputed": { argTypes: [DYN, DYN], result: BOOL }, + /** Native own properties on globalThis keyed by symbol identity. */ + "dyn.globalSymbolGet": { argTypes: [SYMBOL_T], result: DYN }, + "dyn.globalSymbolSet": { argTypes: [SYMBOL_T, DYN], result: VOID }, + "dyn.globalSymbolHas": { argTypes: [SYMBOL_T], result: BOOL }, + "dyn.globalSymbolDelete": { argTypes: [SYMBOL_T], result: VOID }, + /** Exact native class capsule identity, without materializing its fields. */ + "dyn.typedRefIs": { argTypes: [DYN, STRING], result: BOOL }, + /** A capsule boxed by this program from exactly this class: one key + * pointer test, false for equal keys boxed elsewhere. Guards fast paths + * whose fallback handles every other value. The key is a literal. */ + "dyn.typedRefKeyIs": { argTypes: [DYN, STRING], result: BOOL }, + "dyn.classIs": { argTypes: [DYN, STRING], result: BOOL }, + /** Destructuring pack over a dyn source — `const [a, b] = d`, a + * destructured dyn callback param (args: the source and the STATIC + * TypeError spelling, "" when the source has none — both borrowed; + * result: a fresh dyn array, +1). Iterable kinds collect like spread + * (arrays element-by-element, strings by code point, bytes by byte); + * every other kind throws V8's destructuring TypeError — the spelling + * verbatim when non-empty, else the runtime kind wording ("number 5 is + * not iterable (cannot read property Symbol(Symbol.iterator))"). In the + * may-throw seed set. */ + "dyn.iterPack": { argTypes: [DYN, STRING], result: DYN }, + "dyn.mathFold": { argTypes: [DYN, BOOL, STRING], result: F64 }, + "dyn.destructureRest": { argTypes: [DYN, STRING], result: DYN }, + "dyn.arrayFromIterator": { argTypes: [DYN], result: DYN }, + "dyn.iterator": { argTypes: [DYN, STRING], result: DYN }, + /** for await over a checked-dynamic value: the loop's iterator (a + * compiled async generator, Symbol.asyncIterator's result, or the sync + * iterator), and whether that iterator is synchronous. */ + "dyn.asyncIteratorOpen": { argTypes: [DYN, STRING], result: DYN }, + "dyn.asyncIteratorWasSync": { argTypes: [], result: BOOL }, + "dyn.iteratorResult": { argTypes: [DYN], result: DYN }, + "dyn.iteratorCanStep": { argTypes: [DYN, DYN], result: BOOL }, + "dyn.iteratorStep": { argTypes: [DYN], result: DYN }, + "dyn.iteratorStepDone": { argTypes: [DYN], result: BOOL }, + "dyn.setGeneratorMembers": { argTypes: [DYN, DYN], result: VOID }, + "dyn.iteratorHelperMethod": { argTypes: [STRING], result: DYN }, + "dyn.collectionIteratorIndex": { argTypes: [DYN], result: F64 }, + "dyn.mapSeedEntries": { argTypes: [DYN], result: DYN }, + "dyn.mapSeedEntry": { argTypes: [DYN], result: DYN }, + /** The for-of-over-dyn pack accessors — the emitted index loop drives + * them over a dyn.iterPack result (ARR by construction). arrLen: the + * ARR length as f64 (0 for non-ARR kinds; arg borrowed). arrAt: the + * element at index (+1; the undefined singleton past the end). Neither + * throws. */ + "dyn.arrLen": { argTypes: [DYN], result: F64 }, + "dyn.arrAt": { argTypes: [DYN, F64], result: DYN }, + /** `key in v` with a RUNTIME (string) key on a checked-dynamic + * receiver (args: value dyn, key string; result bool): OBJ answers + * own-member presence, ARR answers 'length'/a valid index — exactly + * the literal-key dynHasKey path. Proxy has traps may throw. */ + "dyn.hasKey": { argTypes: [DYN, STRING], result: BOOL }, + /** The `in` operator with a runtime string key: dyn.hasKey plus the + * members inherited from builtin and compiled class prototypes. */ + "dyn.in": { argTypes: [DYN, STRING], result: BOOL }, + "dyn.freeze": { argTypes: [DYN], result: DYN }, + "dyn.isFrozen": { argTypes: [DYN], result: BOOL }, + /** Object.defineProperty on a declared class field (scr_json.c): the + * field's current value (borrowed), its FIELD_* attribute word, the + * instance's CLASS_* integrity word, the field name and the descriptor. + * Validates as Node does (TypeError "Cannot redefine property: f") and + * answers a one-property holder of the defined value and attributes (+1), + * read back with class.definedBits and class.definedValue. */ + "class.defineField": { argTypes: [DYN, F64, F64, STRING, DYN], result: DYN }, + "class.definedBits": { argTypes: [DYN, STRING], result: F64 }, + "class.definedValue": { argTypes: [DYN, STRING], result: DYN }, + /** Whether a property descriptor has a `value` member. */ + "class.descriptorHasValue": { argTypes: [DYN], result: BOOL }, + /** Throws Node's TypeError for a write to a read-only class field: the + * field name and the instance's class name. */ + "class.throwReadOnly": { argTypes: [STRING, STRING], result: VOID }, + /** Throws Node's TypeError for a class field initializer on a + * non-extensible instance: the field name. */ + "class.throwNotExtensible": { argTypes: [STRING], result: VOID }, + /** Object.freeze of a record whose shape is freezable (frozen-records.ts): + * sets the frozen bit in its rc word (scr_record_freeze). The receiver is + * any record type. */ + "record.freeze": { argTypes: [null], result: VOID }, + /** Whether a record is frozen: its rc word's frozen bit, set by + * record.freeze and dyn.freeze of a capsule. Emitted inline. The receiver + * is any record type. */ + "record.isFrozen": { argTypes: [null], result: BOOL }, + /** Throws Node's TypeError for a write to a frozen record: the key, the + * object's text in the message ("#") and the operation (0 assign, + * 1 add, 2 delete). */ + "record.throwFrozen": { argTypes: [STRING, STRING, F64], result: VOID }, + /** Object.freeze of an array (scr_arr_freeze): sets its frozen bit, which + * the write guards of IrModule.freezableArrays test, and isFrozen reads. + * The receiver is any array type. */ + "array.freeze": { argTypes: [null], result: VOID }, + "array.isFrozen": { argTypes: [null], result: BOOL }, + /** Object.freeze, seal or preventExtensions of a Map or Set + * (scr_map_prevent_extensions), which only makes it not extensible: its + * entries stay writable. The receiver is any map or set type. */ + "collection.preventExtensions": { argTypes: [null], result: VOID }, + "collection.isExtensible": { argTypes: [null], result: BOOL }, + "dyn.nativeSetNew": { argTypes: [DYN], result: DYN }, + "dyn.nativeSetIs": { argTypes: [DYN], result: BOOL }, + "dyn.nativeMapIs": { argTypes: [DYN], result: BOOL }, + "dyn.nativeUrlIs": { argTypes: [DYN], result: BOOL }, + "dyn.nativeDateIs": { argTypes: [DYN], result: BOOL }, + "dyn.nativeDateHandleIs": { argTypes: [DYN], result: BOOL }, + /** A builtin constructor (`Object`, `Date`, `RegExp`, `Map`, `Set`, + * `Promise`, `WeakMap`, `WeakSet`) read as a JavaScript value: one + * interned function per name, the same one dynamic values answer for + * `.constructor`. The name is a compiler-chosen literal. */ + "dyn.globalConstructor": { argTypes: [STRING], result: DYN }, + /** A builtin global function (setTimeout, setInterval, setImmediate, + * the clear functions, queueMicrotask, process.nextTick) read as a + * JavaScript value: one interned function per name whose calls run the + * timer APIs (it pulls in the event loop). The name is a compiler-chosen + * literal. */ + "dyn.builtinFunction": { argTypes: [STRING], result: DYN }, + "dyn.namespaceNew": { argTypes: [STRING, DYN], result: DYN }, + "dyn.namespaceDefine": { argTypes: [DYN, STRING, DYN, F64, F64], result: VOID }, + "date.nativeNew": { argTypes: [DYN], result: DYN }, + /** `new Date(year, monthIndex, ...)` in JavaScript: the argument array's + * first seven values pass through ToNumber in order (later ones are + * evaluated, never converted), then the local-time field constructor. */ + "date.nativeNewFields": { argTypes: [DYN], result: DYN }, + /** `new Date(value)` in JavaScript: a string argument parses with V8's + * full date parser; every other value constructs as date.nativeNew. */ + "date.nativeNewValue": { argTypes: [DYN], result: DYN }, + "date.argumentsTime": { argTypes: [DYN], result: DATE_T }, + "date.classOperation": { argTypes: [DYN, DYN], result: VOID }, + "date.checkedValue": { argTypes: [DYN], result: DATE_T }, + "dyn.nativeRegexIs": { argTypes: [DYN], result: BOOL }, + /** toString() on a checked-dynamic receiver: runtime kind dispatch + * (bytes decode per the literal encoding — utf8 default; strings, + * numbers, booleans, arrays, objects answer JS-exactly; undefined and + * null throw the catchable TypeError). */ + "dyn.toString": { argTypes: [DYN, DYN, STRING], result: STRING }, + "dyn.valueOf": { argTypes: [DYN, STRING], result: DYN }, + /** Object.defineProperties over dyn values (args: target, descriptors — + * both borrowed dyn; result: the target, +1 — JS's return value). + * Value descriptors become own properties on OBJ and FUNC targets; + * OBJ targets preserve data attributes. get/set + * descriptors and non-object targets/descriptors throw catchably + * (Node's TypeError texts; accessors the "not supported yet" Error). In + * the may-throw seed set. */ + "dyn.defineProps": { argTypes: [DYN, DYN], result: DYN }, + "dyn.defineProperty": { argTypes: [DYN, DYN, DYN], result: DYN }, + "dyn.getOwnPropertyDescriptor": { argTypes: [DYN, DYN], result: DYN }, + "dyn.arrayProtoCall": { argTypes: [DYN, STRING, DYN], result: DYN }, + /** Promise.all / Promise.allSettled / Promise.any over a spread + * checked-dynamic array: the values, the settled-record array, or the + * first fulfillment (AggregateError once every entry rejects). + * Thenable elements adopt; the second argument reads a program class + * instance's `then` (the compiled property dispatch). */ + "dyn.promiseAll": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, + "dyn.promiseAllSettled": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, + "dyn.promiseAny": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, + /** PromiseResolve over one checked-dynamic value: the promise it holds, + * a promise that adopts a thenable's settlement, or a promise fulfilled + * with the value. The second argument reads `then` as above. */ + "dyn.promiseResolve": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, + /** Bare `typeof v` on a dyn value AS A STRING (arg: the dyn value, + * borrowed; result: an owned string) — the dyn kind's JS answer: + * undefined→"undefined", null/object/array/bytes→"object" (JS's oldest + * wart preserved), boolean/number/string by kind, function→"function". + * Never throws. */ + "dyn.typeof": { argTypes: [DYN], result: STRING }, + /** Object.prototype.toString.call on a checked-dynamic value. */ + "dyn.objectTag": { argTypes: [DYN], result: STRING }, + /** Engine-free CommonJS module graph. Module values are scalar f64 + * handles; generated startup defines the registry, init wrappers update + * loading/cache state, and the read surface answers Node's live metadata. */ + "module.registryInit": { argTypes: [F64], result: VOID }, + "module.define": { + argTypes: [F64, STRING, STRING, STRING, arrayOf(STRING), BOOL], + result: VOID, + }, + "module.enter": { argTypes: [F64], result: VOID }, + "module.link": { argTypes: [F64, F64], result: VOID }, + "module.finish": { argTypes: [F64], result: VOID }, + "module.fail": { argTypes: [F64], result: VOID }, + "module.filename": { argTypes: [F64], result: STRING }, + "module.id": { argTypes: [F64], result: STRING }, + "module.path": { argTypes: [F64], result: STRING }, + "module.paths": { argTypes: [F64], result: arrayOf(STRING) }, + "module.children": { argTypes: [F64], result: arrayOf(F64) }, + "module.parent": { argTypes: [F64], result: F64 }, + "module.loaded": { argTypes: [F64], result: BOOL }, + "module.cacheGet": { argTypes: [STRING], result: F64 }, + /** createRequire's require(id) and require.resolve(request, options) + * with a specifier computed at run time (scr_require.c): the arguments, + * then the requiring file, the call's "file:line" and the builtin ids. + * Both resolve like Node at run time. requireComputed always throws + * (Node's argument errors or MODULE_NOT_FOUND), or ends the program with + * an SC2020 refusal when the specifier resolves; resolveComputed answers + * the resolved id or real path. */ + "module.requireComputed": { argTypes: [DYN, STRING, STRING, arrayOf(STRING)], result: DYN }, + "module.resolveComputed": { + argTypes: [DYN, DYN, STRING, STRING, arrayOf(STRING)], + result: STRING, + }, + "module.cacheHas": { argTypes: [STRING], result: BOOL }, + "module.cacheKeys": { argTypes: [], result: arrayOf(STRING) }, + "timers.setTimeout": { argTypes: [{ kind: "func", params: [], ret: VOID }, F64], result: VOID }, + /** The repeating timer pair. setInterval takes (callback, ms) like + * setTimeout and RETURNS the f64 handle the fallback declarations + * promise (ids start at 1, so truthiness narrowing works); the loop + * owns the callback until clearInterval removes the entry (eagerly — a + * live interval keeps the loop alive, a cleared one releases it, like + * Node). Neither throws; a throw ESCAPING an interval callback ends the + * program like a setTimeout throw. */ + "timers.setInterval": { argTypes: [{ kind: "func", params: [], ret: VOID }, F64], result: F64 }, + "timers.clearInterval": { argTypes: [F64], result: VOID }, + /** setTimeout WITH a clear handle (the f64 id) — the clearable/unref-able + * one-shot; clearTimeout cancels it, .unref() drops it from loop + * liveness. The plain timers.setTimeout stays the handle-less + * fire-and-forget. */ + "timers.setTimeoutHandle": { + argTypes: [{ kind: "func", params: [], ret: VOID }, F64], + result: F64, + }, + "timers.clearTimeout": { argTypes: [F64], result: VOID }, + /** Timeout.unref()/ref()/hasRef() — loop-liveness bookkeeping over the + * handle id. unref/ref RETURN the handle (f64) for chaining; hasRef + * returns bool. */ + "timers.unref": { argTypes: [F64], result: F64 }, + "timers.ref": { argTypes: [F64], result: F64 }, + "timers.hasRef": { argTypes: [F64], result: BOOL }, + /** Timeout.refresh() — re-arm to now + the original delay (from the + * heap or from inside the firing callback; a one-shot that fired on an + * earlier turn is gone and no-ops — documented divergence). Returns + * the handle for chaining like unref/ref. */ + "timers.refresh": { argTypes: [F64], result: F64 }, + /** setImmediate/clearImmediate — Node's check phase: callbacks run + * once per loop turn AFTER due timers, FIFO, and immediates queued + * mid-phase wait for the next turn. setImmediate returns the f64 + * handle (its own id space — clearTimeout of an Immediate no-ops, + * like Node); the Immediate ref trio mirrors the Timeout one (an + * unref'd pending immediate neither keeps the loop alive nor fires + * once nothing reffed remains). */ + "timers.setImmediate": { argTypes: [{ kind: "func", params: [], ret: VOID }], result: F64 }, + /** queueMicrotask (scr_async.c): the callback enters the SAME FIFO + * promise continuations ride — one microtask order, like V8's queue — + * and a throw is an UNCAUGHT exception (never a rejection). + * timers.queueMicrotask takes an owned zero-param closure and never + * throws; the Dyn form (checked-dynamic arguments — the mustCall + * wrapper, the suite's invalid-input probes) throws Node's + * ERR_INVALID_ARG_TYPE synchronously on a non-function value and calls + * the function with zero arguments at drain. */ + "timers.queueMicrotask": { argTypes: [{ kind: "func", params: [], ret: VOID }], result: VOID }, + "timers.queueMicrotaskDyn": { argTypes: [DYN], result: VOID }, + "timers.clearImmediate": { argTypes: [F64], result: VOID }, + /** process.nextTick(cb, ...args) — the user tick queue. args: [cb + * (() => void; trailing call arguments ride the timer surface's + * interned dyn thunk)]. Ticks drain BEFORE promise jobs at every loop + * checkpoint, to joint exhaustion with them (Node's tick-then- + * microtask order); ticks enqueued by station listeners run at the + * NEXT checkpoint (the stream-tick station divergence, SEMANTICS.md). + * Pending ticks are always-ready work — the loop neither sleeps nor + * exits while any exist; ticks scheduled from 'exit' listeners never + * run (Node). The enqueue itself never throws. */ + "process.nextTick": { argTypes: [{ kind: "func", params: [], ret: VOID }], result: VOID }, + /** The process introspection statics. uptime: seconds since the + * binary's own start (fractional — a load-time monotonic anchor). + * availableMemory/constrainedMemory: libuv's numbers (free-ish bytes; + * the cgroup cap or 0). cpuUser/cpuSystem (+ threadCpu twins): the + * process/thread CPU clocks in microseconds — the frontend composes + * the {user, system} records. The *Diff forms answer current − prev + * for one field; cpuPrevValidate throws Node's ERR_INVALID_ARG_VALUE + * RangeError for negative/non-finite prev fields (user first, then + * system — Node's order; MAY THROW). rusage(idx): one + * process.resourceUsage() field by canonical index (Node's units). + * activeResources: the loop's own bookkeeping — 'Timeout' per armed + * (or firing, uncleared) timer, 'Immediate' per queued unfired + * immediate; unmodeled resource kinds are absent (SEMANTICS.md). */ + "process.uptime": { argTypes: [], result: F64 }, + /** perf_hooks performance.now(): fractional ms since process start. */ + "perf.now": { argTypes: [], result: F64 }, + /** perf_hooks user timing over the thread's performance timeline. The + * producing calls take the call's whole argument vector as one dyn + * array (Node's validation and errors run in the runtime, MAY THROW): + * mark/measure answer 1, entries(args, which) the entry count of + * getEntries (0), getEntriesByName (1) or getEntriesByType (2). The + * entryName/Type/Start/Duration/Detail accessors read entry i of that + * answer; the frontend builds a new entry's record from them and hands + * it to entryKeep (a typed reference capsule), and the getters read the + * kept records back through entryRef, so entries keep their identity. */ + "perf.mark": { argTypes: [DYN], result: F64 }, + "perf.measure": { argTypes: [DYN], result: F64 }, + "perf.entries": { argTypes: [DYN, F64], result: F64 }, + "perf.clearMarks": { argTypes: [DYN], result: VOID }, + "perf.clearMeasures": { argTypes: [DYN], result: VOID }, + "perf.entryName": { argTypes: [F64], result: STRING }, + "perf.entryType": { argTypes: [F64], result: STRING }, + "perf.entryStart": { argTypes: [F64], result: F64 }, + "perf.entryDuration": { argTypes: [F64], result: F64 }, + "perf.entryDetail": { argTypes: [F64], result: DYN }, + "perf.entryKeep": { argTypes: [DYN], result: VOID }, + "perf.entryRef": { argTypes: [F64], result: DYN }, + /** perf_hooks event-loop metrics (scr_perf_loop.c): loopStart is the + * loop's start in performance.now() ms (0 before it), idleTime the idle + * ms since. eldNew(resolution ms) creates a monitorEventLoopDelay + * histogram handle (MAY THROW Node's resolution RangeError); enable/disable answer whether the state changed; + * eldPercentile expects Node's validated range; eldStat(which) reads + * 0 min, 1 max, 2 mean, 3 stddev, 4 count, 5 exceeds. */ + "perf.loopStart": { argTypes: [], result: F64 }, + "perf.idleTime": { argTypes: [], result: F64 }, + "perf.eldNew": { argTypes: [F64], result: F64 }, + "perf.eldEnable": { argTypes: [F64], result: BOOL }, + "perf.eldDisable": { argTypes: [F64], result: BOOL }, + "perf.eldReset": { argTypes: [F64], result: VOID }, + "perf.eldPercentile": { argTypes: [F64, F64], result: F64 }, + "perf.eldStat": { argTypes: [F64, F64], result: F64 }, + /** process.memoryUsage(): memoryUsageTake samples the resident set and + * the runtime heap once (MAY THROW Node's uv_resident_set_memory error) + * and memoryUsageField(k) reads that sample (0 rss, 1 heapTotal, 2 + * heapUsed, 3 external, 4 arrayBuffers). rss is memoryUsage.rss(). */ + "process.memoryUsageTake": { argTypes: [], result: VOID }, + "process.memoryUsageField": { argTypes: [F64], result: F64 }, + "process.rss": { argTypes: [], result: F64 }, + "process.availableMemory": { argTypes: [], result: F64 }, + "process.constrainedMemory": { argTypes: [], result: F64 }, + "process.cpuUser": { argTypes: [], result: F64 }, + "process.cpuSystem": { argTypes: [], result: F64 }, + "process.cpuUserDiff": { argTypes: [F64], result: F64 }, + "process.cpuSystemDiff": { argTypes: [F64], result: F64 }, + "process.threadCpuUser": { argTypes: [], result: F64 }, + "process.threadCpuSystem": { argTypes: [], result: F64 }, + "process.threadCpuUserDiff": { argTypes: [F64], result: F64 }, + "process.threadCpuSystemDiff": { argTypes: [F64], result: F64 }, + "process.cpuPrevValidate": { argTypes: [F64, F64], result: VOID }, + "process.rusage": { argTypes: [F64], result: F64 }, + "process.activeResources": { argTypes: [], result: arrayOf(STRING) }, + "timers.immediateUnref": { argTypes: [F64], result: F64 }, + "timers.immediateRef": { argTypes: [F64], result: F64 }, + "timers.immediateHasRef": { argTypes: [F64], result: BOOL }, + /** The tolerated non-handle clear (`clearTimeout(null)`, + * `clearInterval({})`, zero-argument forms): Node ignores anything + * that is not a live handle without an error — a VOID no-op the emitter drops + * (only syntactically side-effect-free arguments take this path). */ + "timers.clearNoop": { argTypes: [], result: VOID }, + // Signal listeners are zero-param (the ambient shape); exit/stdin + // callbacks carry program-dependent one-param shapes — null slots, the + // libCall case checks them (child.onExit precedent). + /** Named process signal events: [name string, callback dyn, once bool] + * or [name, callback] for removal. The runtime resolves platform signal + * numbers and delivers (name, number). Listeners do not keep the loop + * alive. Invalid callbacks and uncatchable signals throw. */ + "process.onSignal": { argTypes: [STRING, DYN, BOOL], result: VOID }, + "process.offSignal": { argTypes: [STRING, DYN], result: VOID }, + /** The process 'exit' event — process.on/once/off("exit"). args: [cb, + * once bool] / [cb]. Listeners run SYNCHRONOUSLY at termination + * (normal exit, process.exit(), the uncaught/unhandled exit-1 paths) + * with the exit code; anything they schedule never runs, like Node. + * Callback shapes: () => void or (code: number) => void — the emitter + * picks the runtime adapter. Never throw. */ + "process.onExit": { argTypes: [null, BOOL], result: VOID }, + "process.offExit": { argTypes: [null], result: VOID }, + /** process.stdin listener registration — stdin.on/once("data" | "end" | + * "error", cb). args: [cb, once bool]. data callbacks: () => void or + * (chunk: Uint8Array) => void; end: () => void; error: () => void or + * (err: Error) => void (the child error adapters are reused). While a + * data listener (or a parked for-await chunk promise) exists, stdin + * keeps the loop alive — Node's flowing-stdin keep-alive. Never + * throw. */ + "stdin.onData": { argTypes: [null, BOOL], result: VOID }, + "stdin.onEnd": { argTypes: [{ kind: "func", params: [], ret: VOID }, BOOL], result: VOID }, + "stdin.onError": { argTypes: [null, BOOL], result: VOID }, + /** process.stdin.on/once('keypress', cb) (scr_readline_tty.c): cb is the + * listener as a checked-dynamic function, called with (str, key). */ + "stdin.onKeypress": { argTypes: [DYN, BOOL], result: VOID }, + /** The for-await chunk source over process.stdin: no args, result + * Promise (+1). Fulfills with the next arrived chunk; the + * EMPTY bytes value is the done sentinel (POSIX reads never deliver + * empty data chunks), which the for-await desugar turns into loop + * exit. Never throws itself; awaiting it re-throws nothing (stdin + * errors surface through 'error' listeners, not the iterator). */ + "stdin.nextChunk": { argTypes: [], result: { kind: "promise", inner: BYTES_U8 } }, + "fs.readFileSync": { argTypes: [STRING, STRING], result: STRING }, + "fs.readFileEncoded": { argTypes: [STRING, STRING], result: STRING }, + "fs.readFdEncoded": { argTypes: [F64, STRING], result: STRING }, + /** readFileSync(path) — the Buffer read (+1 bytes); throws catchably + * like the utf8 form. */ + "fs.readFileSyncBuf": { argTypes: [STRING], result: BYTES_U8 }, + /** readFileSync(path, enc) with a RUNTIME encoding (an untyped JS + * parameter): undefined/null answer Buffers, utf8 a string, other real + * encodings throw the "not supported yet" Error, unknown names throw + * ERR_UNKNOWN_ENCODING. */ + "fs.readFileSyncDyn": { argTypes: [STRING, DYN], result: DYN }, + "fs.writeFileSync": { argTypes: [STRING, STRING], result: VOID }, + "fs.appendFileSync": { argTypes: [STRING, STRING], result: VOID }, + "fs.existsSync": { argTypes: [STRING], result: BOOL }, + "fs.mkdirSync": { argTypes: [STRING], result: VOID }, + "fs.rmSync": { argTypes: [STRING], result: VOID }, + "fs.rmdirSync": { argTypes: [STRING], result: VOID }, + "fs.readdirSync": { argTypes: [STRING], result: arrayOf(STRING) }, + /** `fs.readdirSync(path, { recursive: true })`: every entry below the + * path, relative to it, in Node's queue order. */ + "fs.readdirRecursiveSync": { argTypes: [STRING], result: arrayOf(STRING) }, + // Result is the call site's Dirent record array (VOID is the + // structure-checked sentinel, the os.networkInterfaces pattern). + /** `fs.readdirSync(path, { withFileTypes: true })` — Dirent rows over + * one readdir pass (scr_lib.c's scandir snapshot; DT_UNKNOWN falls back + * to lstat, Node's getDirents rule). The result type is the call site's + * interned Dirent record array (name, parentPath, hidden %dtype in + * libuv's UV_DIRENT encoding) — verified by the frontend; the emitter + * assembles the rows from the snapshot. Byte order on POSIX (libuv + * sorts), the directory's order on Windows; no "."/"..". Throws + * Node's scandir errno error (may-throw seed set); fresh +1 array. */ + "fs.readdirTypesSync": { argTypes: [STRING], result: VOID }, + /** fs.globSync / fs.glob / fs/promises glob (scr_glob.c, Node's + * lib/internal/fs/glob.js walk): pattern and options are dyn values the + * runtime validates in Node's order. The string forms answer string[]; + * the Dirent forms answer the call site's Dirent record array (validated + * in validate.ts) assembled from the snapshot; fs.callbackGlob takes the + * argument vector and calls back with string[]. */ + "fs.globSync": { argTypes: [DYN, DYN], result: arrayOf(STRING) }, + "fs.globTypesSync": { argTypes: [DYN, DYN], result: VOID }, + "fs.callbackGlob": { argTypes: [DYN], result: VOID }, + "fsp.globStrings": { argTypes: [DYN, DYN], result: arrayOf(STRING) }, + "fsp.globTypes": { argTypes: [DYN, DYN], result: VOID }, + /** The error an fs/promises glob walk ended with (undefined when none), + * read once right after fsp.globStrings / fsp.globTypes. */ + "fsp.globError": { argTypes: [], result: DYN }, + /** fs.Dir (scr_lib.c): opendirSync(path, bufferSize) throws Node's + * opendir error (or the options.bufferSize RangeError); dir.readSync + * answers the call site's `Dirent | null` (VOID here — the emitter builds + * the row like readdirTypesSync) and dir.read is its promise twin; + * readSync and closeSync throw ERR_DIR_CLOSED on a closed Dir, read and + * close reject with it. */ + "fs.opendirSync": { argTypes: [STRING, F64], result: DIR_T }, + "fsp.opendir": { argTypes: [STRING, F64], result: { kind: "promise", inner: DIR_T } }, + "dir.path": { argTypes: [DIR_T], result: STRING }, + "dir.readSync": { argTypes: [DIR_T], result: VOID }, + "dir.read": { argTypes: [DIR_T], result: { kind: "promise", inner: VOID } }, + "dir.closeSync": { argTypes: [DIR_T], result: VOID }, + "dir.close": { argTypes: [DIR_T], result: { kind: "promise", inner: VOID } }, + /** node:path (scr_path.c ports BOTH of Node's implementations + * function-by-function): the `path.*` family is posix, the + * `path.win32*` family is Node v24's path.win32 ported exactly — the + * frontend binds the bare module to the TARGET platform's family + * (Node on Windows IS path.win32) and the path.posix / path.win32 + * namespaces to their own family everywhere. join and resolve take ONE + * string[] arg — the frontend packs the variadic call's arguments into + * an array literal. basename always receives its suffix (the frontend + * completes an omitted one with "", a Node no-op). toNamespacedPath is + * the posix identity and the win32 \\?\-prefixer. None of these throw; + * the resolves consult the process cwd like Node's. */ + "path.join": { argTypes: [arrayOf(STRING)], result: STRING }, + "path.resolve": { argTypes: [arrayOf(STRING)], result: STRING }, + "path.normalize": { argTypes: [STRING], result: STRING }, + "path.dirname": { argTypes: [STRING], result: STRING }, + "path.basename": { argTypes: [STRING, STRING], result: STRING }, + "path.extname": { argTypes: [STRING], result: STRING }, + "path.isAbsolute": { argTypes: [STRING], result: BOOL }, + "path.relative": { argTypes: [STRING, STRING], result: STRING }, + "path.toNamespacedPath": { argTypes: [STRING], result: STRING }, + "path.win32Join": { argTypes: [arrayOf(STRING)], result: STRING }, + "path.win32Resolve": { argTypes: [arrayOf(STRING)], result: STRING }, + "path.win32Normalize": { argTypes: [STRING], result: STRING }, + "path.win32Dirname": { argTypes: [STRING], result: STRING }, + "path.win32Basename": { argTypes: [STRING, STRING], result: STRING }, + "path.win32Extname": { argTypes: [STRING], result: STRING }, + "path.win32IsAbsolute": { argTypes: [STRING], result: BOOL }, + "path.win32Relative": { argTypes: [STRING, STRING], result: STRING }, + "path.win32ToNamespacedPath": { argTypes: [STRING], result: STRING }, + /** path.parse: [root, dir, base, ext, name] as a fresh string[] (the + * frontend builds the ParsedPath record); path.format: the pathObject's + * root, dir, base, name and ext with undefined spelled "". */ + "path.parse": { argTypes: [STRING], result: arrayOf(STRING) }, + "path.win32Parse": { argTypes: [STRING], result: arrayOf(STRING) }, + "path.format": { argTypes: [STRING, STRING, STRING, STRING, STRING], result: STRING }, + "path.win32Format": { argTypes: [STRING, STRING, STRING, STRING, STRING], result: STRING }, + /** path.matchesGlob(path, pattern): Node's matchGlobPattern over its + * bundled minimatch (scr_glob.c). Both families match case-insensitively + * on macOS and Windows hosts like Node's; win32 also accepts either + * separator. Throws TypeError for a pattern over 64 Ki code units, + * RangeError for a brace sequence too long to expand, and SyntaxError + * when a segment's regex is invalid under the u flag. */ + "path.matchesGlob": { argTypes: [STRING, STRING], result: BOOL }, + "path.win32MatchesGlob": { argTypes: [STRING, STRING], result: BOOL }, + /** node:os: homedir is $HOME else getpwuid(3); tmpdir is Node's env + * cascade ($TMPDIR/$TMP/$TEMP else /tmp, one trailing slash trimmed). + * os.platform() lowers to process.platform — one implementation. */ + "os.homedir": { argTypes: [], result: STRING }, + /** os.release(): uname(2)'s release field — Node's own implementation + * (the kernel version string, e.g. "24.6.0" on macOS 15). Interned; +1 + * per read. Never throws. */ + "os.release": { argTypes: [], result: STRING }, + /** os.type(): uname(2)'s sysname ("Darwin", "Linux"; "Windows_NT" on + * win32) — Node's uv_os_uname answer. Interned per call; never throws. */ + "os.type": { argTypes: [], result: STRING }, + /** os.totalmem(): total physical memory in bytes. Never throws. */ + "os.totalmem": { argTypes: [], result: F64 }, + /** os.availableParallelism(): CPUs the process may use (libuv's answer), >= 1. Never throws. */ + "os.availableParallelism": { argTypes: [], result: F64 }, + /** os.hostname/machine/version/endianness, freemem, uptime and loadavg: + * libuv's answers (uname's machine and version fields, gethostname, + * "LE"/"BE", free memory in bytes, uptime in seconds, the three load + * averages). Fresh results; never throw. */ + "os.hostname": { argTypes: [], result: STRING }, + "os.machine": { argTypes: [], result: STRING }, + "os.version": { argTypes: [], result: STRING }, + "os.endianness": { argTypes: [], result: STRING }, + "os.freemem": { argTypes: [], result: F64 }, + "os.uptime": { argTypes: [], result: F64 }, + "os.loadavg": { argTypes: [], result: arrayOf(F64) }, + /** os.getPriority(pid): validateInt32 (RangeError ERR_OUT_OF_RANGE), + * then the priority; a failure throws Node's SystemError. */ + "os.getPriority": { argTypes: [F64], result: F64 }, + /** os.setPriority(pid, priority): validateInt32 for both (priority in + * -20..19, RangeError ERR_OUT_OF_RANGE), then the priority; a failure + * throws Node's SystemError. */ + "os.setPriority": { argTypes: [F64, F64], result: VOID }, + /** os.cpus(): cpusTake snapshots every CPU and answers the count; + * cpusModel(i) and cpusField(i, k) (k: 0 speed, 1 user, 2 nice, 3 sys, + * 4 idle, 5 irq) read the snapshot. Never throw. */ + "os.cpusTake": { argTypes: [], result: F64 }, + "os.cpusModel": { argTypes: [F64], result: STRING }, + "os.cpusField": { argTypes: [F64, F64], result: F64 }, + /** os.constants: the per-thread null-prototype constants object (dlopen, + * errno, signals, priority, UV_UDP_REUSEADDR). Never throws. */ + "os.constants": { argTypes: [], result: DYN }, + /** fs.constants: the target's cached null-prototype constants object. */ + "fs.constants": { argTypes: [], result: DYN }, + /** The deprecated node:constants object (scr_os.c): the os groups and + * fs.constants merged with the borrowed baked crypto.constants table, + * frozen, one per thread. */ + "constants.object": { argTypes: [DYN], result: DYN }, + /** umask(2): arg < 0 reads without setting (umask has no read-only form + * — set 0, restore); otherwise sets and answers the PREVIOUS mask. + * Never throws. */ + "process.umask": { argTypes: [F64], result: F64 }, + /** chdir(2) — throws Node's fs-shaped error (ENOENT/EACCES/ENOTDIR, + * syscall "chdir") on failure. */ + "process.chdir": { argTypes: [STRING], result: VOID }, + "process.loadEnvFile": { argTypes: [DYN], result: VOID }, + /** process._exiting: true once the exit sequence began (exit listeners + * running) — the runtime flag scr_run_exit_listeners/process.exit set. + * Never throws. */ + "process.exiting": { argTypes: [], result: BOOL }, + /** net's process-wide happy-eyeballs attempt budget (Node's default + * 250ms): one runtime double in the core unit, so reading/writing it + * never forces the net unit into the link. Never throw. */ + "net.getAutoSelTimeout": { argTypes: [], result: F64 }, + "net.setAutoSelTimeout": { argTypes: [F64], result: VOID }, + /** realpath(3) with Node's error shape (syscall "lstat" in the message, + * Node's own spelling for realpathSync failures). +1 fresh string. */ + "fs.realpathSync": { argTypes: [STRING], result: STRING }, + /** fs.realpathSync (or its .native when the flag is true) as a function + * value: path and options arrive unchecked; the result is a string or a + * Buffer by the options' encoding. */ + "fs.realpathValue": { argTypes: [DYN, DYN, BOOL], result: DYN }, + /** Native realpath with syscall "realpath" on failure. +1 fresh string. */ + "fs.realpathNativeSync": { argTypes: [STRING], result: STRING }, + /** The os.userInfo() field trio (uv_os_get_passwd's slices): pw_name, + * pw_shell, pw_dir — the PASSWD homedir, not os.homedir's $HOME-first + * cascade (Node's own split). The frontend assembles the UserInfo + * record from these plus getuid/getgid. +1 fresh strings; a passwd + * lookup failure aborts (Node throws a system error there — no + * compiled program path reaches it for the running uid). */ + "os.userName": { argTypes: [], result: STRING }, + "os.userShell": { argTypes: [], result: STRING }, + "os.userHomedir": { argTypes: [], result: STRING }, + "os.tmpdir": { argTypes: [], result: STRING }, + // Result is the call site's Dict record (VOID is + // the dgram.address sentinel — the libCall case checks the structure). + /** os.networkInterfaces(): getifaddrs(3) → the Dict + * record, built inline by the emitter from a runtime snapshot (scr_lib.c). + * The result type is the CALL SITE's mapped @types/node shape — a pure + * index-signature record whose value is `Info[] | undefined`, Info a + * two-record union (IPv4: scopeid `number | undefined` holding undefined; + * IPv6: scopeid number) — verified structurally by the frontend. Rows + * match libuv's filter (IFF_UP && IFF_RUNNING, AF_INET/AF_INET6; loopback + * = internal; MACs from the interface's link-level sibling entry, zeros + * when absent; cidr from the netmask's contiguous prefix, the null arm + * when it is missing or non-contiguous). Key/row order follows the OS's + * getifaddrs enumeration — Node itself does not guarantee an order. + * Fresh +1 record; never throws (a getifaddrs failure yields {}). */ + "os.networkInterfaces": { argTypes: [], result: VOID }, + /** `Math.max(...xs)` / `Math.min(...xs)` over one spread number[] + * (scr_number.c): the JS fold exactly — any NaN element poisons the + * result, ±0 order by the JS comparison (max prefers +0, min prefers + * -0), and the empty array yields -Infinity / +Infinity like the + * zero-argument calls. Borrows the array; never throws. */ + "math.maxArr": { argTypes: [arrayOf(F64)], result: F64 }, + "math.minArr": { argTypes: [arrayOf(F64)], result: F64 }, + "math.hypotArr": { argTypes: [arrayOf(F64)], result: F64 }, + /** `Math.floor(x)` — C floor() IS the JS operation (NaN/±0/±Infinity + * pass through bit-exactly). Never throws. */ + "math.floor": { argTypes: [F64], result: F64 }, + /** Math.abs (C fabs — IS the JS operation) and Math.round (scr_lib.c: + * ECMA half-toward-+Infinity with the exact-fraction comparison — C + * round() is half-away-from-zero and floor(x+0.5) drifts at the + * epsilon boundary). Borrow nothing; never throw. */ + "math.abs": { argTypes: [F64], result: F64 }, + "math.round": { argTypes: [F64], result: F64 }, - "math.trunc": { argTypes: [F64], result: F64 }, - "math.ceil": { argTypes: [F64], result: F64 }, - "math.sin": { argTypes: [F64], result: F64 }, - "math.sinh": { argTypes: [F64], result: F64 }, - "math.cos": { argTypes: [F64], result: F64 }, - "math.cosh": { argTypes: [F64], result: F64 }, - "math.tan": { argTypes: [F64], result: F64 }, - "math.tanh": { argTypes: [F64], result: F64 }, - "math.asin": { argTypes: [F64], result: F64 }, - "math.asinh": { argTypes: [F64], result: F64 }, - "math.acos": { argTypes: [F64], result: F64 }, - "math.acosh": { argTypes: [F64], result: F64 }, - "math.atan": { argTypes: [F64], result: F64 }, - "math.atanh": { argTypes: [F64], result: F64 }, - "math.cbrt": { argTypes: [F64], result: F64 }, - "math.clz32": { argTypes: [F64], result: F64 }, - /** Return -1, +1, or the original NaN/zero. In particular, -0 remains -0. */ - "math.sign": { argTypes: [F64], result: F64 }, - "math.exp": { argTypes: [F64], result: F64 }, - "math.expm1": { argTypes: [F64], result: F64 }, - "math.f16round": { argTypes: [F64], result: F64 }, - "math.fround": { argTypes: [F64], result: F64 }, - "math.sqrt": { argTypes: [F64], result: F64 }, - "math.log": { argTypes: [F64], result: F64 }, - "math.log1p": { argTypes: [F64], result: F64 }, - "math.log2": { argTypes: [F64], result: F64 }, - "math.log10": { argTypes: [F64], result: F64 }, - "math.atan2": { argTypes: [F64, F64], result: F64 }, - "math.pow": { argTypes: [F64, F64], result: F64 }, - "math.imul": { argTypes: [F64, F64], result: F64 }, - /** `Math.min(a, b)` / `Math.max(a, b)` — the two-argument scalar forms - * (scr_lib.c), JS-exact like the Arr folds: NaN poisons, max prefers +0 - * over -0 (min the reverse). C fmin/fmax are NOT these (they drop NaN). - * Never throw. */ - "math.min": { argTypes: [F64, F64], result: F64 }, - "math.max": { argTypes: [F64, F64], result: F64 }, - /** `Math.random()` — a uniform double in [0,1) with the spec's 53-bit - * granularity, drawn from arc4random_buf (the CSPRNG behind the crypto - * lowerings). Same distribution as Node, NECESSARILY different sequence - * (SEMANTICS.md 62 — no seeded sequence exists to match). Never throws. */ - "math.random": { argTypes: [], result: F64 }, - /** The static global parsers/tests (scr_string.c). num.parseInt is - * ECMA-262 19.2.5 exactly — JS whitespace, sign, ToInt32 radix (the - * frontend completes an omitted radix to 0 = the spec's "undefined": - * base 10 with the 0x hex escape), longest digit prefix, and the exact - * mathematical value correctly rounded (u64 fast path, bignum beyond — - * overflow is ±Infinity). num.isNaN is the NaN self-test on an - * already-number argument (tsc pins the argument to number, so no - * ToNumber coercion exists to model). Borrow; never throw. */ - "num.parseInt": { argTypes: [STRING, F64], result: F64 }, - /** ES parseFloat (scr_string.c): the longest StrDecimalLiteral prefix - * of the trimmed input (no hex, "Infinity" exact-case), NaN when none — - * ECMA-262 19.2.4 over a string argument (non-string arguments keep the - * fence: Node would ToNumber-coerce). Borrows; never throws. */ - "num.parseFloat": { argTypes: [STRING], result: F64 }, - /** `a.localeCompare(b)`: -1/0/1 in Node's root-locale collation for - * Latin-script text (scr_str_locale_compare). Never throws. */ - "str.localeCompare": { argTypes: [STRING, STRING], result: F64 }, - /** Annex B substr(start, length) with start ToNumber'd and length's - * presence; never throws. */ - "str.substr": { argTypes: [STRING, F64, F64, BOOL], result: STRING }, - /** toLocaleLowerCase/toLocaleUpperCase (scr_regex.c): the locales value - * as written, upper=true for toLocaleUpperCase. Throws for null and for - * locale forms with no native rules. */ - "str.localeCase": { argTypes: [STRING, DYN, BOOL], result: STRING }, - "str.replaceLiteral": { argTypes: [STRING, STRING, STRING, BOOL], result: STRING }, - // (subject, characters, NUL-separated replacement texts): the fused - // chain of global one-character replacements. - "str.replaceChars": { argTypes: [STRING, STRING, STRING], result: STRING }, - /* ToNumber(string) — ECMA-262 7.1.4.1 StringToNumber (scr_string.c): - * trim the JS StrWhiteSpace set, empty/whitespace-only → +0, then the - * whole span must be one StrNumericLiteral — signed decimal (Infinity - * included, strtod-over-validated-span correct rounding) or unsigned - * 0x/0o/0b (exact value, nearest-even; signed forms are NaN) — with - * any trailing garbage answering NaN. Number(aString), unary + on - * strings, and util.format %d over strings lower here. Borrows; never - * throws. */ - "num.fromString": { argTypes: [STRING], result: F64 }, - "num.isNaN": { argTypes: [F64], result: BOOL }, - /** The static URI component codecs (scr_string.c), ECMA-262 Encode/ - * Decode with the component sets over the runtime's UTF-8 strings. - * str.encodeUriComponent percent-encodes every byte outside the - * unreserved component set (ALPHA/DIGIT/- _ . ! ~ * ' ( )) as uppercase - * %XX — the spec's per-code-point UTF-8 encoding IS a byte scan here — - * and THROWS the spec's URIError ("URI malformed") on a lone surrogate. - * str.decodeUriComponent - * decodes %XX escapes bytewise (raw non-escape bytes copy through) and - * requires the escaped bytes to form strictly valid UTF-8 (overlong - * forms, surrogate code points, and >U+10FFFF refused, per UTF8-decode - * without replacement); bad hex or an invalid sequence THROWS the - * spec's URIError ("URI malformed"), catchable. Borrow; results +1. */ - "str.encodeUriComponent": { argTypes: [STRING], result: STRING }, - // The base64 globals: the argument is a dyn value (WebIDL ToString - // runs in the runtime); the zero-argument form always throws. - /** The WHATWG base64 globals (scr_string.c), Node-global since v16. - * Arguments are borrowed dyn values — WebIDL ToString runs in the - * runtime over the dyn kind (String(null) is "null": the html spec's - * coercion, which Node's atob(null) exercises). str.atob decodes - * forgiving-base64 (ASCII whitespace stripped, %4==0 strips up to two - * '=', %4==1 refuses, leftover bits discarded) into the latin1 code - * points as a string; a malformed input THROWS the catchable - * DOMException InvalidCharacterError ("The string to be decoded is not - * correctly encoded."). str.btoa encodes the string's code points as - * base64; any code point over U+00FF THROWS InvalidCharacterError - * ("Invalid character"). str.b64Missing is the zero-argument call of - * either: always throws Node's TypeError [ERR_MISSING_ARGS] "The - * \"input\" argument must be specified". Results +1. */ - "str.atob": { argTypes: [DYN], result: STRING }, - "str.btoa": { argTypes: [DYN], result: STRING }, - "str.b64Missing": { argTypes: [], result: STRING }, - "str.decodeUriComponent": { argTypes: [STRING], result: STRING }, - "str.decodeUri": { argTypes: [STRING], result: STRING }, - /** encodeURI: the same Encode() keeping the reserved set and '#' - * unescaped — throwing like the component encoder. Borrow; result +1. */ - "str.encodeUri": { argTypes: [STRING], result: STRING }, - /** RegExp.escape (ES2025): per-code-point EncodeForRegExpEscape — - * leading ASCII alphanumeric hex-escapes, syntax characters and '/' - * take a backslash, other punctuators/whitespace/line terminators - * hex-escape, the rest passes through. Total; borrows; result +1. */ - "regexp.escape": { argTypes: [STRING], result: STRING }, - /** Number.prototype formatters (scr_lib.c), JS-exact: - * num.toExponential is toExponential() with the spec's "as many digits - * as necessary"; num.toFixed0 is the non-throwing omitted-argument - * toFixed() fast path; num.toFixed implements an explicit fractionDigits - * with exact binary-value rounding and THROWS RangeError outside 0..100. - * Successful results +1. */ - "num.toExponential": { argTypes: [F64], result: STRING }, - "num.toFixed0": { argTypes: [F64], result: STRING }, - "num.toFixed": { argTypes: [F64, F64], result: STRING }, - /** toPrecision(precision) with the argument ToNumber'd and its presence - * (undefined answers ToString); toExponential with a DEFINED - * fractionDigits. Both throw V8's RangeError for out-of-range digits. */ - "num.toPrecision": { argTypes: [F64, F64, BOOL], result: STRING }, - "num.toExponentialDigits": { argTypes: [F64, F64], result: STRING }, - "num.toStringRadix": { argTypes: [F64, DYN], result: STRING }, - /** toString(radix) with a number radix (scr_lib.c): truncates it as - * ToIntegerOrInfinity does, THROWS RangeError outside 2..36. */ - "num.toStringRadixF64": { argTypes: [F64, F64], result: STRING }, - /** Object.is over two numbers — the spec's SameValue on doubles: NaN - * equals NaN, +0 differs from -0, everything else is `===`. Plain bool - * result; never throws. (Union-armed operands take unionEq's sameValue - * flag instead — this is the both-f64 fast path.) */ - "num.sameValue": { argTypes: [F64, F64], result: BOOL }, - /** `new Intl.NumberFormat("en-US").format(x)` and - * `x.toLocaleString("en-US")` with DEFAULT options — the one locale - * whose data the runtime embeds (Node's default-build locale): decimal - * notation, 0–3 fraction digits rounded half-up on the SHORTEST - * round-tripping decimal (ICU's rounding input, probed vs Node — - * format(1.0005) is "1.001" though toFixed(3) answers "1.000"), "," - * grouping every three integer digits, "∞"/"NaN" texts, and "-0" for - * negative inputs rounding to zero. Result +1; never throws. */ - "intl.numFormatEnUs": { argTypes: [F64], result: STRING }, - /** ES Symbol values (scr_symbol.c — link-gated by moduleUsesSymbol). - * sym.new: `Symbol(desc)` — a fresh runtime-unique identity (+1) whose - * one arg is the description string (borrowed); sym.newAnon is the - * description-less `Symbol()` form. sym.for: the Symbol.for global - * registry — one interned symbol per key (borrowed), +1 on every call. - * sym.toString: "Symbol(desc)" (+1 string; "Symbol()" when absent). - * sym.desc / sym.keyFor answer the interned `string | undefined` union - * (the runtime returns +1-or-NULL; the backend builds the union arms — - * the child.stdout pattern). None of these throw. */ - "sym.new": { argTypes: [STRING], result: SYMBOL_T }, - "sym.newAnon": { argTypes: [], result: SYMBOL_T }, - "sym.for": { argTypes: [STRING], result: SYMBOL_T }, - "sym.wellKnown": { argTypes: [STRING], result: SYMBOL_T }, - // Result is the interned `string | undefined` union — the libCall case - // checks the arms (the spawnRes.error pattern). - "sym.keyFor": { argTypes: [SYMBOL_T], result: VOID }, - "sym.desc": { argTypes: [SYMBOL_T], result: VOID }, - "sym.toString": { argTypes: [SYMBOL_T], result: STRING }, - /** Engine-free arbitrary-precision bigint operations (scr_bigint.c). - * parse accepts the ECMAScript BigInt string grammar; fromF64 accepts - * finite integral Numbers. Arithmetic returns fresh immutable values. - * cmp returns -1/0/1 as f64; toString accepts a numeric radix. */ - "bigint.parse": { argTypes: [STRING], result: BIGINT_T }, - "bigint.fromF64": { argTypes: [F64], result: BIGINT_T }, - "bigint.neg": { argTypes: [BIGINT_T], result: BIGINT_T }, - "bigint.not": { argTypes: [BIGINT_T], result: BIGINT_T }, - "bigint.add": { argTypes: [BIGINT_T, BIGINT_T], result: BIGINT_T }, - "bigint.sub": { argTypes: [BIGINT_T, BIGINT_T], result: BIGINT_T }, - "bigint.mul": { argTypes: [BIGINT_T, BIGINT_T], result: BIGINT_T }, - "bigint.div": { argTypes: [BIGINT_T, BIGINT_T], result: BIGINT_T }, - "bigint.mod": { argTypes: [BIGINT_T, BIGINT_T], result: BIGINT_T }, - "bigint.pow": { argTypes: [BIGINT_T, BIGINT_T], result: BIGINT_T }, - "bigint.and": { argTypes: [BIGINT_T, BIGINT_T], result: BIGINT_T }, - "bigint.or": { argTypes: [BIGINT_T, BIGINT_T], result: BIGINT_T }, - "bigint.xor": { argTypes: [BIGINT_T, BIGINT_T], result: BIGINT_T }, - "bigint.shl": { argTypes: [BIGINT_T, BIGINT_T], result: BIGINT_T }, - "bigint.shr": { argTypes: [BIGINT_T, BIGINT_T], result: BIGINT_T }, - "bigint.eq": { argTypes: [BIGINT_T, BIGINT_T], result: BOOL }, - "bigint.eqString": { argTypes: [BIGINT_T, STRING], result: BOOL }, - "bigint.cmp": { argTypes: [BIGINT_T, BIGINT_T], result: F64 }, - "bigint.cmpNumber": { argTypes: [BIGINT_T, F64], result: F64 }, - "bigint.truthy": { argTypes: [BIGINT_T], result: BOOL }, - "bigint.toString": { argTypes: [BIGINT_T, F64], result: STRING }, - "bigint.inspect": { argTypes: [BIGINT_T], result: STRING }, - "bigint.toF64": { argTypes: [BIGINT_T], result: F64 }, - "bigint.asUintN": { argTypes: [F64, BIGINT_T], result: BIGINT_T }, - "bigint.asIntN": { argTypes: [F64, BIGINT_T], result: BIGINT_T }, - "bigint.bufferRead": { argTypes: [BYTES_U8, F64, BOOL, BOOL], result: BIGINT_T }, - "bigint.bufferWrite": { argTypes: [BYTES_U8, BIGINT_T, F64, BOOL, BOOL], result: F64 }, - "bigint.dataViewGet": { argTypes: [BYTES_U8, F64, BOOL, BOOL], result: BIGINT_T }, - "bigint.dataViewSet": { argTypes: [BYTES_U8, F64, BIGINT_T, BOOL], result: VOID }, - /** node:url + the URL class (scr_url.c). url.new parses one absolute - * URL string into a mutable URL value (+1) — invalid input THROWS a - * catchable TypeError ("Invalid URL"), like Node's constructor. The - * getters (borrowed receiver, +1 string) never throw; url.href doubles - * as toString(). fileURLToPath has one libFn per receiver form (URL - * value / string) — both THROW Node's TypeErrors on non-file schemes, - * encoded slashes, and non-empty hosts. url.pathToFileURL resolves the - * path (getcwd) and never throws. */ - "url.new": { argTypes: [STRING], result: URL_T }, - "url.newBase": { argTypes: [STRING, STRING], result: URL_T }, - /** Factories consume already-converted input/base strings. canParse - * answers a boolean; parse returns a checked native URL or null (+1). - * Parser failures are suppressed; conversion occurs before these calls. */ - "url.canParse": { argTypes: [STRING], result: BOOL }, - "url.canParseBase": { argTypes: [STRING, STRING], result: BOOL }, - "url.parse": { argTypes: [STRING], result: DYN }, - "url.parseBase": { argTypes: [STRING, STRING], result: DYN }, - "url.protocol": { argTypes: [URL_T], result: STRING }, - "url.origin": { argTypes: [URL_T], result: STRING }, - "url.username": { argTypes: [URL_T], result: STRING }, - "url.password": { argTypes: [URL_T], result: STRING }, - "url.host": { argTypes: [URL_T], result: STRING }, - "url.hostname": { argTypes: [URL_T], result: STRING }, - "url.port": { argTypes: [URL_T], result: STRING }, - "url.pathname": { argTypes: [URL_T], result: STRING }, - "url.href": { argTypes: [URL_T], result: STRING }, - /** Selected component writes borrow the receiver and converted string. - * href replaces its parsed fields while retaining cached params identity. */ - "url.set": { argTypes: [URL_T, STRING, STRING], result: VOID }, - "url.setChecked": { argTypes: [URL_T, STRING, DYN], result: VOID }, - "url.fileURLToPathUrl": { argTypes: [URL_T], result: STRING }, - "url.fileURLToPathChecked": { argTypes: [DYN], result: STRING }, - "url.fileURLToPathOptions": { argTypes: [DYN, DYN], result: STRING }, - /** node:url's legacy API (scr_url_legacy.c): parse(url, parseQueryString, - * slashesDenoteHost) answers the checked-dynamic Url instance; format - * takes the urlObject (a Url-shaped object, a string, or a URL) and the - * URL form's options; resolve takes (from, to). The trailing bool says - * whether the call site emits DEP0169. domainToASCII/domainToUnicode - * take the domain and whether it was omitted. Node's validation errors - * throw catchably. */ - "url.legacyParse": { argTypes: [DYN, DYN, DYN, BOOL], result: DYN }, - "url.legacyFormat": { argTypes: [DYN, DYN, BOOL], result: STRING }, - "url.legacyResolve": { argTypes: [DYN, DYN, BOOL], result: STRING }, - "url.domainToASCII": { argTypes: [DYN, BOOL], result: STRING }, - "url.domainToUnicode": { argTypes: [DYN, BOOL], result: STRING }, - /** node:punycode (scr_url.c): punycode.js 2.1.0's string conversions; - * decode/toUnicode (and, past 32-bit deltas, encode/toASCII) throw the - * module's RangeErrors, ucs2Encode String.fromCodePoint's. warn marks the - * module's first load (scr_async_dyn.c) and reports DEP0040 then unless - * the second argument is false: at once for a static ES import, on the - * next tick for a require() call. */ - "punycode.decode": { argTypes: [STRING], result: STRING }, - "punycode.encode": { argTypes: [STRING], result: STRING }, - "punycode.toASCII": { argTypes: [STRING], result: STRING }, - "punycode.toUnicode": { argTypes: [STRING], result: STRING }, - "punycode.ucs2Decode": { argTypes: [STRING], result: arrayOf(F64) }, - "punycode.ucs2Encode": { argTypes: [arrayOf(F64)], result: STRING }, - "punycode.warn": { argTypes: [BOOL, BOOL], result: VOID }, - "url.fileURLToPathBuffer": { argTypes: [DYN, DYN], result: BYTES_U8 }, - "url.pathToFileURLChecked": { argTypes: [DYN, DYN], result: URL_T }, - "url.fileURLToPathStr": { argTypes: [STRING], result: STRING }, - "url.pathToFileURL": { argTypes: [STRING], result: URL_T }, - /** Explicit path syntax from the options.windows argument, independent - * of the executable's host platform. Windows UNC input can throw. */ - "url.pathToFileURLPlatform": { argTypes: [STRING, BOOL], result: URL_T }, - /** pathToFileURL under a win32 TARGET: the same scr_url_from_path call - * (the runtime selects the win32 arm by _WIN32), but a distinct IR name - * because that arm THROWS for malformed UNC inputs — may-throw seeds on - * it while posix pathToFileURL emission stays byte-identical. */ - "url.pathToFileURLWin32": { argTypes: [STRING], result: URL_T }, - /** URLSearchParams (scr_url.c — always linked with the url unit). - * Construction: sp.new (empty), sp.parse (one borrowed init string — - * a single leading '?' strips, Node's constructor), sp.copy (snapshot - * of another list), sp.fromPairs (a string[][] value — THROWS Node's - * ERR_INVALID_TUPLE TypeError on a non-[name, value] row; the one - * throwing entry in the family), sp.with (the record-literal init - * desugar: append one pair, answer the same list +1 — chains fold - * `new URLSearchParams({...})` into nested calls). url.searchParams - * answers the URL's LIVE cached view (+1, one identity per URL); - * url.search is the WHATWG search getter ("" for no/empty query). - * Methods mirror the WHATWG surface: sp.get answers +1-or-NULL (the - * sym.desc union pattern, null arm), sp.getAll a fresh string[]; - * sp.append/sp.set/sp.delete/sp.deleteValue/sp.sort mutate and - * re-serialize a live view's URL query; sp.has/sp.hasValue answer - * bools; sp.size/sp.toString are pure reads. sp.keyAt/sp.valAt are - * the for-of/forEach desugar's index reads (live — the loop re-reads - * sp.size each pass). */ - "sp.new": { argTypes: [], result: SEARCH_PARAMS_T }, - "sp.newChecked": { argTypes: [DYN], result: SEARCH_PARAMS_T }, - "sp.parse": { argTypes: [STRING], result: SEARCH_PARAMS_T }, - "sp.copy": { argTypes: [SEARCH_PARAMS_T], result: SEARCH_PARAMS_T }, - // The pairs argument is string[][] — checked structurally in validate.ts (the - // generic array-of slot has no named constant here). - "sp.fromPairs": { argTypes: [null], result: SEARCH_PARAMS_T }, - "sp.with": { argTypes: [SEARCH_PARAMS_T, STRING, STRING], result: SEARCH_PARAMS_T }, - "url.searchParams": { argTypes: [URL_T], result: SEARCH_PARAMS_T }, - "url.search": { argTypes: [URL_T], result: STRING }, - "url.hash": { argTypes: [URL_T], result: STRING }, - // Result is the interned `string | null` union — the libCall case - // checks the arms (the spawnRes.signal pattern). - "sp.get": { argTypes: [SEARCH_PARAMS_T, STRING], result: VOID }, - "sp.getAll": { argTypes: [SEARCH_PARAMS_T, STRING], result: arrayOf(STRING) }, - "sp.append": { argTypes: [SEARCH_PARAMS_T, STRING, STRING], result: VOID }, - "sp.set": { argTypes: [SEARCH_PARAMS_T, STRING, STRING], result: VOID }, - "sp.delete": { argTypes: [SEARCH_PARAMS_T, STRING], result: VOID }, - "sp.deleteValue": { argTypes: [SEARCH_PARAMS_T, STRING, STRING], result: VOID }, - "sp.has": { argTypes: [SEARCH_PARAMS_T, STRING], result: BOOL }, - "sp.hasValue": { argTypes: [SEARCH_PARAMS_T, STRING, STRING], result: BOOL }, - "sp.sort": { argTypes: [SEARCH_PARAMS_T], result: VOID }, - "sp.size": { argTypes: [SEARCH_PARAMS_T], result: F64 }, - "sp.toString": { argTypes: [SEARCH_PARAMS_T], result: STRING }, - "sp.keyAt": { argTypes: [SEARCH_PARAMS_T, F64], result: STRING }, - "sp.valAt": { argTypes: [SEARCH_PARAMS_T, F64], result: STRING }, - // node:querystring. qs.parse's result is the call site's ParsedUrlQuery - // dictionary record (VOID is the networkInterfaces sentinel — the - // libCall case checks the structure); qs.stringify's object argument - // is a dyn value (the frontend dynFroms typed records). - /** node:querystring (scr_qs.c — link-gated by moduleUsesQs; NOT - * URLSearchParams: the legacy codec's escaping and '+' rules differ). - * qs.escape is Node's qsEscape, which encodes exactly the component - * unreserved set — it emits the always-linked scr_qs_escape, so - * escape-only programs never pull the unit. qs.unescape is Node's qsUnescape: strict decodeURIComponent - * first, the lenient legacy unescapeBuffer fallback on failure (never - * throws). qs.parse takes (str, sep, eq, maxKeys) — the frontend - * completes omitted/null sep/eq to "&"/"=" and the omitted maxKeys - * option to Node's 1000 (0 and negatives mean unlimited, Node's rule) — - * and its result type is the CALL SITE's mapped ParsedUrlQuery shape (a - * pure index-signature record over `string | string[]`, an undefined - * arm tolerated — @types/node's Dict), verified structurally by the - * frontend (lowerQuerystringParseCall); the emitters construct the - * record and hand its overflow map to scr_qs_parse_into with the two - * union tags. qs.stringify takes (obj, sep, eq) with obj a dyn value - * (the frontend dynFroms the typed record; JS-world dyn values pass - * through) — Node's encodeStringified rules run in the runtime, so - * arrays expand to repeated keys and null/undefined/nested objects are - * empty values. Custom encoder/decoder options fence at compile time. - * All borrow; string results +1; escape and stringify throw URIError - * [ERR_INVALID_URI] on a lone surrogate. */ - "qs.parse": { argTypes: [STRING, STRING, STRING, F64], result: VOID }, - "qs.stringify": { argTypes: [DYN, STRING, STRING], result: STRING }, - "qs.escape": { argTypes: [STRING], result: STRING }, - "qs.unescape": { argTypes: [STRING], result: STRING }, - /** node:util.parseArgs: one checked-dynamic config object in, one - * checked-dynamic ParsedResults tree out. Statically typed member reads - * validate their values when leaving that tree. The native parser may - * throw Node's coded validation/grammar TypeErrors. */ - "util.parseArgs": { argTypes: [DYN], result: DYN }, - "util.parseEnv": { argTypes: [DYN], result: DYN }, - "util.getSystemErrorName": { argTypes: [DYN], result: STRING }, - "util.getSystemErrorMessage": { argTypes: [DYN], result: STRING }, - "util.systemErrorEntries": { argTypes: [], result: DYN }, - "util.stripVTControlCharacters": { argTypes: [DYN], result: STRING }, - "util.toUSVString": { argTypes: [DYN], result: STRING }, - "util.isDeepStrictEqual": { argTypes: [DYN, DYN, DYN], result: BOOL }, - "util.styleText": { argTypes: [DYN, DYN, DYN], result: STRING }, - /** util.inherits/deprecate/callbackify/debuglog over checked-dynamic - * arguments (scr_util_callable.c). The function results are native dyn - * wrappers; call sites with a static function type check them back. */ - "util.inherits": { argTypes: [DYN, DYN], result: VOID }, - "util.deprecate": { argTypes: [DYN, DYN, DYN, DYN], result: DYN }, - "util.callbackify": { argTypes: [DYN], result: DYN }, - /** util.promisify over an untyped function value: a native dyn function - * whose calls answer a dyn promise the appended callback settles. */ - "util.promisifyDyn": { argTypes: [DYN], result: DYN }, - "util.debuglog": { argTypes: [DYN, DYN], result: DYN }, - "util.typeIs": { argTypes: [DYN, STRING], result: BOOL }, - "util.typeValue": { argTypes: [DYN, STRING], result: DYN }, - /** fs.statSync → a Stats value (may throw, like the other sync fs - * calls); the stats.* getters are pure reads on it. */ - "fs.statSync": { argTypes: [STRING], result: STATS_T }, - "fs.lstatSync": { argTypes: [STRING], result: STATS_T }, - // statSync / lstatSync(path, { throwIfNoEntry }): arg 1 is the flag. - // Result is the interned `Stats | undefined` union (the runtime answers - // +1-or-NULL; the backend builds the arms). - "fs.statProbeSync": { argTypes: [STRING, BOOL], result: VOID }, - "fs.lstatProbeSync": { argTypes: [STRING, BOOL], result: VOID }, - "fs.fstatSync": { argTypes: [F64], result: STATS_T }, - "fs.fchmodSync": { argTypes: [F64, F64], result: VOID }, - "fs.fsyncSync": { argTypes: [F64], result: VOID }, - "fs.fdatasyncSync": { argTypes: [F64], result: VOID }, - "fs.ftruncateSync": { argTypes: [F64, F64], result: VOID }, - "fs.utimesSync": { argTypes: [DYN, DYN, DYN], result: VOID }, - "fs.futimesSync": { argTypes: [DYN, DYN, DYN], result: VOID }, - "fs.lutimesSync": { argTypes: [DYN, DYN, DYN], result: VOID }, - "fs.readvSync": { argTypes: [F64, arrayOf(BYTES_U8), F64], result: F64 }, - "fs.writevSync": { argTypes: [F64, arrayOf(BYTES_U8), F64], result: F64 }, - "fs.linkSync": { argTypes: [DYN, DYN], result: VOID }, - "fs.symlinkSync": { argTypes: [DYN, DYN, DYN], result: VOID }, - /** fs.cpSync / fs/promises cp (scr_fs_cp.c): src, dest and options are - * dyn values the runtime validates in Node's order. */ - "fs.cpSync": { argTypes: [DYN, DYN, DYN], result: VOID }, - "fs.readlinkSync": { argTypes: [DYN, DYN], result: DYN }, - "fs.statfsSync": { argTypes: [DYN, DYN], result: DYN }, - "fs.readlinkSyncStr": { argTypes: [DYN, DYN], result: STRING }, - "fs.readlinkSyncBuffer": { argTypes: [DYN, DYN], result: BYTES_U8 }, - /** fs.openSync(path, flags) → the raw fd as f64; fs.readSync/fs.writeSync - * over Buffer windows perform sequential I/O when position is -1 and - * offset-preserving positioned I/O otherwise; fs.writeStrSync is the - * utf8 string twin; and fs.closeSync(fd) closes the descriptor. This is - * the fd slice behind spawn/log-processing forms. - * flags is Node's string - * grammar ("r", "w", "a" and the +/x variants; unknown flags throw - * Node's ERR_INVALID_ARG_VALUE TypeError text). All throw Node-shaped - * fs errors (openSync ENOENT/EACCES..., readSync EBADF/range errors, - * closeSync EBADF). */ - "fs.openSync": { argTypes: [STRING, STRING], result: F64 }, - "fs.openNumericSync": { argTypes: [STRING, F64, F64], result: F64 }, - "fs.readSync": { argTypes: [F64, BYTES_U8, F64, F64, F64], result: F64 }, - "fs.writeSync": { argTypes: [F64, BYTES_U8, F64, F64, F64], result: F64 }, - "fs.writeStrSync": { argTypes: [F64, STRING, F64, STRING], result: F64 }, - // fs.watch's callback func type is program-dependent (zero params, or - // the eventType string) — the slot pins arity and the path/receiver. - /** fs.watch(path, listener?) → an FSWatcher handle (scr_watch.c — - * linked, and scr_watch_install() emitted, only when these appear on - * the IR; moduleUsesFsWatch is the switch). The path opens NOW — - * failure throws Node's fs error synchronously ("ENOENT: ..., watch - * 'x'", the polling-fallback catch shape) — and the unit's kqueue - * (EVFILT_VNODE) delivers "rename"/"change" through the loop's watch - * hook. The callback (nullable) MOVES in with an adapter per listener - * shape (zero-param, or the (eventType: string) form — runtime- - * provided); an open watcher keeps the loop alive until watcher.close() - * (idempotent, receiver borrowed, statement position only). */ - "fs.watch": { argTypes: [STRING], result: FSWATCHER_T }, - "fs.watchCb": { argTypes: [STRING, null], result: FSWATCHER_T }, - "watcher.close": { argTypes: [FSWATCHER_T], result: VOID }, - /** The composed `new crypto.X509Certificate(data).fingerprint` read - * (scr_lib.c — no certificate handle exists): the SHA-1 of the DER, - * uppercase colon-separated, over PEM or raw-DER Buffer input; other - * inputs throw Node's ERR_OSSL_PEM_NO_START_LINE Error (may-throw). */ - "crypto.x509Fingerprint": { argTypes: [BYTES_U8], result: STRING }, - "crypto.x509FingerprintStr": { argTypes: [STRING], result: STRING }, - /** The certificate's Validity window (validFrom / validTo reads — - * scr_lib.c's minimal ASN.1 walk to the TBSCertificate validity - * SEQUENCE): UTCTime and GeneralizedTime render in Node's exact - * ASN1_TIME_print shape ("Jul 1 00:00:00 2026 GMT" — %2d space-padded - * day). Same input contract and PEM error as the fingerprint pair - * (may-throw). */ - "crypto.x509ValidFrom": { argTypes: [BYTES_U8], result: STRING }, - "crypto.x509ValidFromStr": { argTypes: [STRING], result: STRING }, - "crypto.x509ValidTo": { argTypes: [BYTES_U8], result: STRING }, - "crypto.x509ValidToStr": { argTypes: [STRING], result: STRING }, - "fs.closeSync": { argTypes: [F64], result: VOID }, - "stats.isFile": { argTypes: [STATS_T], result: BOOL }, - "stats.isDirectory": { argTypes: [STATS_T], result: BOOL }, - "stats.isSymbolicLink": { argTypes: [STATS_T], result: BOOL }, - "stats.size": { argTypes: [STATS_T], result: F64 }, - "stats.dev": { argTypes: [STATS_T], result: F64 }, - "stats.ino": { argTypes: [STATS_T], result: F64 }, - "stats.blocks": { argTypes: [STATS_T], result: F64 }, - "stats.nlink": { argTypes: [STATS_T], result: F64 }, - "stats.atimeMs": { argTypes: [STATS_T], result: F64 }, - "stats.mtimeMs": { argTypes: [STATS_T], result: F64 }, - "stats.ctimeMs": { argTypes: [STATS_T], result: F64 }, - "stats.birthtimeMs": { argTypes: [STATS_T], result: F64 }, - "stats.mode": { argTypes: [STATS_T], result: F64 }, - "stats.uid": { argTypes: [STATS_T], result: F64 }, - "stats.gid": { argTypes: [STATS_T], result: F64 }, - "stats.rdev": { argTypes: [STATS_T], result: F64 }, - "stats.blksize": { argTypes: [STATS_T], result: F64 }, - "stats.isBlockDevice": { argTypes: [STATS_T], result: BOOL }, - "stats.isCharacterDevice": { argTypes: [STATS_T], result: BOOL }, - "stats.isFIFO": { argTypes: [STATS_T], result: BOOL }, - "stats.isSocket": { argTypes: [STATS_T], result: BOOL }, - /** The Date fields: the *Ms value rounded (Node's dateFromMs). */ - "stats.atime": { argTypes: [STATS_T], result: DATE_T }, - "stats.mtime": { argTypes: [STATS_T], result: DATE_T }, - "stats.ctime": { argTypes: [STATS_T], result: DATE_T }, - "stats.birthtime": { argTypes: [STATS_T], result: DATE_T }, - // The wider sync fs slice (unlink/chmod/chown/copyfile and the - // mode-carrying write/mkdir forms). - /** The wider sync fs slice (scr_lib.c), all throwing catchably with - * Node's errno message shapes and `.code` stamped like the rest of - * sync fs. unlink/chmod/chown wrap the syscalls 1:1 (Node reports the - * syscall's own name). copyFileSync copies contents into a fresh (or - * truncated) destination carrying the SOURCE's mode (libuv's - * uv_fs_copyfile behavior); its errors carry BOTH paths — Node's - * "copyfile 'src' -> 'dest'". lstatSync is statSync without following - * a trailing symlink (Node reports lstat); stats.isSymbolicLink / - * stats.dev / ino / blocks / nlink / atimeMs / mtimeMs / ctimeMs are pure reads on the widened - * snapshot (blocks is allocated 512-byte units; the times are milliseconds - * with their sub-second fractions, Node's arithmetic). - * writeFileModeSync is writeFileSync(path, data, { mode }): the mode - * applies at CREATION only (open(2) with O_CREAT, umask applying), - * exactly Node — an existing file keeps its permissions. mkdirModeSync - * / mkdirRecursiveModeSync are the mkdirSync option forms with an - * explicit mode (the recursive walk passes it to every directory it - * creates, like Node's). */ - "fs.unlinkSync": { argTypes: [STRING], result: VOID }, - "fs.chmodSync": { argTypes: [STRING, F64], result: VOID }, - "fs.chownSync": { argTypes: [STRING, F64, F64], result: VOID }, - "fs.lchownSync": { argTypes: [STRING, F64, F64], result: VOID }, - "fs.fchownSync": { argTypes: [F64, F64, F64], result: VOID }, - /** truncateSync opens the path with 'r+', then truncates (len checked and - * clamped like ftruncateSync's) and closes it. */ - "fs.truncateSync": { argTypes: [STRING, F64], result: VOID }, - "fs.copyFileSync": { argTypes: [STRING, STRING], result: VOID }, - /** renameSync is the direct two-path syscall wrapper. renameCb defers - * the operation to the loop and fires a program-shaped Error | null - * callback through a backend-emitted adapter. */ - "fs.renameSync": { argTypes: [STRING, STRING], result: VOID }, - // Callback type is program-dependent (zero params or Error | null). - "fs.renameCb": { argTypes: [STRING, STRING, null], result: VOID }, - "fs.writeFileModeSync": { argTypes: [STRING, STRING, F64], result: VOID }, - "fs.mkdirModeSync": { argTypes: [STRING, F64], result: VOID }, - "fs.mkdirRecursiveModeSync": { argTypes: [STRING, F64], result: VOID }, - "atomics.wait": { argTypes: [bytesOf("i32"), F64, F64, F64], result: STRING }, - "atomics.notify": { argTypes: [bytesOf("i32"), F64, F64], result: F64 }, - "atomics.op": { argTypes: [null, F64, F64, F64, F64], result: F64 }, - "atomics.opDyn": { argTypes: [DYN, F64, F64, F64, F64], result: F64 }, - "atomics.waitDyn": { argTypes: [DYN, F64, F64, F64], result: STRING }, - "atomics.notifyDyn": { argTypes: [DYN, F64, F64], result: F64 }, - "worker.new": { argTypes: [F64, STRING, DYN], result: DYN }, - "worker.isMainThread": { argTypes: [], result: BOOL }, - "worker.threadId": { argTypes: [], result: F64 }, - "worker.root": { argTypes: [], result: F64 }, - "worker.data": { argTypes: [], result: DYN }, - "worker.parentPort": { argTypes: [], result: DYN }, - /** worker_threads.resourceLimits: {} in the main thread; a refusal in a - * worker (scr_worker_events.c). */ - "worker.resourceLimits": { argTypes: [], result: DYN }, - /** worker_threads.threadName: the Worker's name in a worker, "" in the - * main thread (scr_worker.c). */ - "worker.threadName": { argTypes: [], result: DYN }, - /** new MessageChannel() ({ port1, port2 }) and receiveMessageOnPort(port) - * (scr_worker_events.c; the latter may throw). */ - "worker.channel": { argTypes: [], result: DYN }, - "worker.receiveMessageOnPort": { argTypes: [DYN], result: DYN }, - /** setEnvironmentData(key, value) and getEnvironmentData(key) - * (scr_worker_events.c; arguments borrowed). */ - "worker.setEnvironmentData": { argTypes: [DYN, DYN], result: VOID }, - "worker.getEnvironmentData": { argTypes: [DYN], result: DYN }, - /** markAsUntransferable(value) and isMarkedAsUntransferable(value) - * (scr_worker_events.c; the value borrowed). */ - "worker.markUntransferable": { argTypes: [DYN], result: VOID }, - "worker.isMarkedUntransferable": { argTypes: [DYN], result: BOOL }, - /** markAsUncloneable(value) (scr_json.c; the value borrowed). */ - "dyn.markUncloneable": { argTypes: [DYN], result: VOID }, - /** before new Worker() with the stdout or stderr option: links the stream - * unit that worker.stdout and worker.stderr read through (scr_stream.c). */ - "stream.workerStdio": { argTypes: [], result: VOID }, - /** new BroadcastChannel(...args) (scr_worker_events.c; may throw), after - * event.broadcastInstall, which installs its EventTarget side. */ - "worker.broadcastChannel": { argTypes: [DYN], result: DYN }, - "event.broadcastInstall": { argTypes: [], result: VOID }, - /** child_process.spawnSync (scr_child.c): posix_spawn + waitpid + piped - * utf8 capture — cmd borrowed, args one borrowed string[] (the frontend - * completes an omitted list to an empty literal), result an owned (+1) - * spawnRes. NEVER throws: spawn failure (nonexistent binary, EACCES) is - * data, like Node's error property — status null and empty outputs - * (SEMANTICS.md documents the divergence from Node's null stdout). - * The getters are pure reads: status is the interned `number | null` - * union (null = spawn failure or signal death, type-directed - * construction in the backend like process.envGet); stdout/stderr are - * +1 strings. */ - "cp.spawnSync": { argTypes: [STRING, arrayOf(STRING)], result: SPAWNRES_T }, - // spawnSync's options form: timeout, killSignal name ("" = SIGTERM), - // and the three stdio modes. - /** spawnSync with options (scr_child.c): the cp.spawnSync core plus the - * option slice portless-class CLIs pass — timeout (killSignal fires at - * the deadline and the result carries error: ETIMEDOUT + the signal, - * Node's shape), killSignal (a signal NAME; "" = the SIGTERM default), - * and per-fd stdio modes (in: 0 /dev/null, 1 ignore, 2 inherit; out/ - * err: 0 capture, 1 ignore → "", 2 inherit → ""). NEVER throws — spawn - * failure and timeout are data on the result, like Node's error - * property. spawnRes.signal is the result's termination signal as the - * call site's `Signals | null` union (null = exited normally or spawn - * failure), constructed type-directedly like spawnRes.status. */ - "cp.spawnSyncOpts": { - argTypes: [STRING, arrayOf(STRING), F64, STRING, F64, F64, F64], - result: SPAWNRES_T, - }, - /** cp.spawnSyncOpts with the `env` option: the trailing [k,v,...] - * pairs replace the child's environment (recordToEnvPairs). */ - "cp.spawnSyncEnv": { - argTypes: [STRING, arrayOf(STRING), F64, STRING, F64, F64, F64, arrayOf(STRING)], - result: SPAWNRES_T, - }, - /** cp.spawnSyncOpts with the stdio carried as a RUNTIME string — - * "pipe" | "ignore" | "inherit", proven by the call site's TYPE (the - * defaultRunner idiom `stdio: options?.stdio ?? "pipe"`); the runtime - * maps the value to the three modes. Args (cmd, argv, timeout, - * killSignal, stdio). Never throws, like the other spawnSync forms. */ - "cp.spawnSyncStdioStr": { - argTypes: [STRING, arrayOf(STRING), F64, STRING, STRING], - result: SPAWNRES_T, - }, - // node:net (scr_net.c). Listener slots are program-dependent closures — - // null slots, checked in the libCall case (the child.onExit precedent); - // the zero-param callbacks pin the exact func type here. The trailing - // BOOL on registrations is the once flag. - /** node:net (scr_net.c — linked, and scr_net_install() emitted, only - * when one of these appears on the IR; moduleUsesNet is the switch). - * Receivers are borrowed; CALLBACKS MOVE into the handle's listener - * registry and are released at settlement (the child.onExit story). - * All listener registrations carry a trailing BOOL once-flag (`on` vs - * `once`) and are void — chaining is fenced. None of these throw: - * listen/connect failures are the async 'error' event, like Node. - * - * net.createServer's optional connection handler and the - * serverOnConnection listeners take the runtime-provided adapters - * (zero-param, or the one-param socket shape); sockOnData's adapters - * are the stdin pair's shapes (zero-param / bytes chunk); the error - * events reuse the child %Error adapters. net.listen/net.listenCb bind - * NOW and defer 'listening' to the next loop turn (Node's next-tick - * emit); net.serverPort is the composed `server.address().port` read. - * net.connect's host argument is a string ("localhost" pins to - * 127.0.0.1 — SEMANTICS.md); the connect callback is once('connect'). */ - "net.createServer": { argTypes: [], result: NETSERVER_T }, - "net.createServerCb": { argTypes: [null], result: NETSERVER_T }, - "net.listen": { argTypes: [NETSERVER_T, F64], result: VOID }, - /** listen(path[, cb]): a Unix-socket server; the socket file is removed - * when the server closes. Failures are the async 'error'. */ - "net.listenPath": { argTypes: [NETSERVER_T, STRING], result: VOID }, - "net.listenPathCb": { - argTypes: [NETSERVER_T, STRING, { kind: "func", params: [], ret: VOID }], - result: VOID, - }, - "net.listenCb": { - argTypes: [NETSERVER_T, F64, { kind: "func", params: [], ret: VOID }], - result: VOID, - }, - /** listen({ port, host, ipv6Only }[, cb]) — the explicit-interface bind - * (portless's listenOnProxyInterface): args [server, port, host, - * ipv6Only]. host is an IP literal string ("" = the host-less - * dual-stack any default); ipv6Only sets IPV6_V6ONLY before the bind. - * These are the old-shaped calls, retained for omitted reusePort so - * serialized IR remains compatible. Failures are the async 'error', - * message in Node's listen shape with the requested host. Never throws. */ - "net.listenOpts": { argTypes: [NETSERVER_T, F64, STRING, BOOL], result: VOID }, - // The callback slot is a zero-param void closure OR its - // `(() => void) | undefined` optional-binding union (checked specially). - "net.listenOptsCb": { argTypes: [NETSERVER_T, F64, STRING, BOOL, null], result: VOID }, - /** Additive ABI for listen({ ..., reusePort }) — args - * [server, port, host, ipv6Only, reusePort], with the callback following - * those values in the callback form. The final BOOL is the requested - * kernel connection-distribution option; old spellings remain - * four/five-argument calls so serialized IR stays compatible. */ - "net.listenOptsReusePort": { argTypes: [NETSERVER_T, F64, STRING, BOOL, BOOL], result: VOID }, - // The callback is a zero-param void closure OR its optional-binding union; - // the callback follows the reusePort BOOL in this additive ABI. - "net.listenOptsReusePortCb": { - argTypes: [NETSERVER_T, F64, STRING, BOOL, BOOL, null], - result: VOID, - }, - "net.serverPort": { argTypes: [NETSERVER_T], result: F64 }, - "net.serverListening": { argTypes: [NETSERVER_T], result: BOOL }, - /** socket.ref()/unref() and server.ref()/unref(): whether the handle - * keeps the event loop alive. */ - "net.sockSetRef": { argTypes: [NETSOCKET_T, BOOL], result: VOID }, - "net.serverSetRef": { argTypes: [NETSERVER_T, BOOL], result: VOID }, - // net.serverAddress's record result is shape-checked in the libCall case - // (the dgram.address sentinel pattern). - /** server.address() as the full AddressInfo record (the dgram.address - * materialization pattern: the emitter builds the record from the three - * runtime reads; the frontend pinned the shape). Never throws — before - * listen it answers the any-form defaults with port 0 where Node - * answers null (the serverPort stance). */ - "net.serverAddress": { argTypes: [NETSERVER_T], result: VOID }, - "net.serverClose": { argTypes: [NETSERVER_T], result: VOID }, - /** server.close((err) => ...): the callback (a dyn function) gets - * undefined once the server closes, or Node's ERR_SERVER_NOT_RUNNING - * when it was not listening. */ - "net.serverCloseDyn": { argTypes: [NETSERVER_T, DYN], result: VOID }, - /** server.address() in programs that may listen on a Unix socket: the - * path a listening server answers ("" for a TCP server), and the check - * a record-typed address() makes first (throws for a Unix server). */ - "net.serverUnixPath": { argTypes: [NETSERVER_T], result: STRING }, - "net.serverAddressCheck": { argTypes: [NETSERVER_T], result: VOID }, - "net.serverCloseCb": { - argTypes: [NETSERVER_T, { kind: "func", params: [], ret: VOID }], - result: VOID, - }, - "net.serverOnError": { argTypes: [NETSERVER_T, null, BOOL], result: VOID }, - "net.serverOnClose": { - argTypes: [NETSERVER_T, { kind: "func", params: [], ret: VOID }, BOOL], - result: VOID, - }, - "net.serverOnConnection": { argTypes: [NETSERVER_T, null, BOOL], result: VOID }, - /** 'secureConnection' — the TLS server's deferred 'connection' list - * (handshake-completion timing); on a server without deferred - * connections (a plain net server) the registration never fires, - * exactly Node's split. */ - "net.serverOnSecureConnection": { argTypes: [NETSERVER_T, null, BOOL], result: VOID }, - "net.connect": { argTypes: [F64, STRING], result: NETSOCKET_T }, - /** connect with a validated autoSelectFamilyAttemptTimeout option (the - * budget runs Node's validateInt32-from-1 ladder and is then inert — - * the single dial has nothing to time). May-throw. */ - "net.connectAttempt": { argTypes: [F64, STRING, DYN], result: NETSOCKET_T }, - /** net.connect/createConnection over a RUNTIME option bag (computed - * keys — the invalid-input probes): Node-order validation (the - * objectMode trio's ERR_INVALID_ARG_VALUE, validatePort, host string, - * autoSelectFamily boolean, the attempt budget), then the trailing - * compiler-rendered fence — ALWAYS THROWS (the error.nodeThrow - * polymorphic-result carve-out). May-throw seed. */ - "net.connectOptsChk": { argTypes: [DYN, STRING], result: VOID }, - "net.connectCb": { - argTypes: [F64, STRING, { kind: "func", params: [], ret: VOID }], - result: NETSOCKET_T, - }, - // The lookup's exact func shape is program data (its answer callback's - // union/record types) — checked specially in the libCall case. - /** connect({ port, host, autoSelectFamily: true, lookup }) — the - * caller-resolver dial (portless's createLoopbackConnection): args - * [port, host, lookup]. The runtime creates the (connecting) socket - * handle, invokes the lookup as Node does — lookup(hostname, options, - * callback), options crossing as the dyn undefined — and the answer - * closure (an emitter-synthesized per-shape thunk over a boxed socket, - * the SNI-answer pattern) dials the answered addresses IN ORDER: each - * connect failure tries the next, the LAST failure's message is the - * socket's 'error' (Node's autoSelectFamily aggregate is a documented - * divergence), and a lookup error surfaces as the deferred socket - * 'error'. A synchronous throw INSIDE the lookup propagates out of the - * connect call (may-throw seed). */ - "net.connectLookup": { argTypes: [F64, STRING, null], result: NETSOCKET_T }, - /** connect({ host, port, lookup }) with a lookup of Node's own type, as - * a checked-dynamic function: the runtime calls it with { hints, all: - * true } and a callback taking an address string or list, then dials - * the answers in order. MAY THROW (host validation, a lookup throw). */ - "net.connectLookupDyn": { argTypes: [F64, STRING, DYN], result: NETSOCKET_T }, - "net.sockWrite": { argTypes: [NETSOCKET_T, STRING], result: VOID }, - "net.sockWriteBytes": { argTypes: [NETSOCKET_T, BYTES_U8], result: VOID }, - "net.sockEnd": { argTypes: [NETSOCKET_T], result: VOID }, - "net.sockEndStr": { argTypes: [NETSOCKET_T, STRING], result: VOID }, - "net.sockEndBytes": { argTypes: [NETSOCKET_T, BYTES_U8], result: VOID }, - /** write/end with a CHECKED-DYNAMIC chunk (an untyped JS payload into a - * typed socket): the runtime dispatches STR/BYTES and throws Node's - * ERR_INVALID_ARG_TYPE chunk TypeError on any other kind. */ - "net.sockWriteDyn": { argTypes: [NETSOCKET_T, DYN], result: VOID }, - "net.sockEndDyn": { argTypes: [NETSOCKET_T, DYN], result: VOID }, - "net.sockDestroy": { argTypes: [NETSOCKET_T], result: VOID }, - "net.sockPipe": { argTypes: [NETSOCKET_T, NETSOCKET_T], result: VOID }, - /** socket.pipe(res) — raw socket chunks into a ServerResponse body - * (the extended-CONNECT bridge leg): each chunk is a body write (the - * response's own framing applies); source EOF end()s the response, - * pipe's default. Borrows both. Never throws. */ - "net.sockPipeRes": { argTypes: [NETSOCKET_T, HTTPRES_T], result: VOID }, - "net.sockOnData": { argTypes: [NETSOCKET_T, null, BOOL], result: VOID }, - "net.sockOnEnd": { - argTypes: [NETSOCKET_T, { kind: "func", params: [], ret: VOID }, BOOL], - result: VOID, - }, - "net.sockOnClose": { - argTypes: [NETSOCKET_T, { kind: "func", params: [], ret: VOID }, BOOL], - result: VOID, - }, - "net.sockOnError": { argTypes: [NETSOCKET_T, null, BOOL], result: VOID }, - "net.sockOnConnect": { - argTypes: [NETSOCKET_T, { kind: "func", params: [], ret: VOID }, BOOL], - result: VOID, - }, - // node:dgram + node:dns (scr_dgram.c). The message/error listeners and - // dns.lookup's callback are program-dependent closures (null slots, - // checked in the libCall case); dgram.address's record result is - // program-dependent too (VOID here is the envGet sentinel — the libCall - // case checks the {address, family, port} shape). - /** node:dgram + node:dns (scr_dgram.c — linked, and - * scr_dgram_install() emitted, only when one of these appears on the - * IR; moduleUsesDgram is the switch — dns.lookup rides the same unit). - * The net listener discipline verbatim: receivers borrowed, CALLBACKS - * MOVE into the handle's registry and release at settlement, `on` vs - * `once` is the trailing bool, registrations are void. bind/connect - * bind NOW and defer 'listening'/'connect' to the next loop turn (the - * net.listen story); their optional-host completions are "" - * (bind → 0.0.0.0) and "127.0.0.1" (connect — udp4's Node default). - * bind/connect/send/close/address THROW Node's state errors ("Socket - * is already bound", "Already connected", "Not running") — may-throw - * seeded. dgram.address returns the AddressInfo RECORD (the frontend - * pins the {address, family, port} shape; the emitter builds it from - * runtime parts). onMessage adapters are emitted per rinfo record - * shape (the child.onExit precedent); onError reuses the child %Error - * adapters. dns.lookup resolves via getaddrinfo AT CALL TIME and - * defers the callback to the next turn (SEMANTICS.md documents the - * blocking divergence); its per-union adapter builds the - * `Error | null` first argument. */ - "dgram.createSocket": { argTypes: [BOOL], result: DGRAMSOCK_T }, - "dgram.bind": { argTypes: [DGRAMSOCK_T, F64, STRING], result: VOID }, - "dgram.bindCb": { - argTypes: [DGRAMSOCK_T, F64, STRING, { kind: "func", params: [], ret: VOID }], - result: VOID, - }, - "dgram.connect": { argTypes: [DGRAMSOCK_T, F64, STRING], result: VOID }, - "dgram.connectCb": { - argTypes: [DGRAMSOCK_T, F64, STRING, { kind: "func", params: [], ret: VOID }], - result: VOID, - }, - "dgram.sendStr": { argTypes: [DGRAMSOCK_T, STRING, F64, STRING], result: VOID }, - "dgram.sendBytes": { argTypes: [DGRAMSOCK_T, BYTES_U8, F64, STRING], result: VOID }, - /** Connected AF_UNIX datagram sockets for the hot-shots package shim - * (the unix-dgram addon's surface): fd or result >= 0, -errno on - * failure; send answers 1 when the socket would block. */ - "dgram.unixSocket": { argTypes: [], result: F64 }, - "dgram.unixConnect": { argTypes: [F64, STRING], result: F64 }, - "dgram.unixSend": { argTypes: [F64, BYTES_U8], result: F64 }, - "dgram.unixClose": { argTypes: [F64], result: F64 }, - /** The send argument-validation ladder over dyn arguments (Node's - * signature shuffle: slice bounds, list/type contracts, port/address - * validation, connected-state errors), then the send: lists, connected - * sends and completion callbacks included. Up to six arguments, the - * missing ones undefined. May-throw. */ - "dgram.sendChk": { argTypes: [DGRAMSOCK_T, DYN, DYN, DYN, DYN, DYN, DYN], result: VOID }, - "dgram.address": { argTypes: [DGRAMSOCK_T], result: VOID }, - "dgram.close": { argTypes: [DGRAMSOCK_T], result: VOID }, - "dgram.closeCb": { - argTypes: [DGRAMSOCK_T, { kind: "func", params: [], ret: VOID }], - result: VOID, - }, - "dgram.unref": { argTypes: [DGRAMSOCK_T], result: VOID }, - "dgram.ref": { argTypes: [DGRAMSOCK_T], result: VOID }, - /** The socket options (scr_dgram.c): the membership calls take the - * interface and whether it was given. May-throw. */ - "dgram.setBroadcast": { argTypes: [DGRAMSOCK_T, BOOL], result: VOID }, - "dgram.setTTL": { argTypes: [DGRAMSOCK_T, F64], result: F64 }, - "dgram.setMulticastTTL": { argTypes: [DGRAMSOCK_T, F64], result: F64 }, - "dgram.setMulticastLoopback": { argTypes: [DGRAMSOCK_T, BOOL], result: BOOL }, - "dgram.setMulticastInterface": { argTypes: [DGRAMSOCK_T, STRING], result: VOID }, - "dgram.addMembership": { argTypes: [DGRAMSOCK_T, STRING, STRING, BOOL], result: VOID }, - "dgram.dropMembership": { argTypes: [DGRAMSOCK_T, STRING, STRING, BOOL], result: VOID }, - "dgram.getRecvBufferSize": { argTypes: [DGRAMSOCK_T], result: F64 }, - "dgram.getSendBufferSize": { argTypes: [DGRAMSOCK_T], result: F64 }, - "dgram.setRecvBufferSize": { argTypes: [DGRAMSOCK_T, F64], result: VOID }, - "dgram.setSendBufferSize": { argTypes: [DGRAMSOCK_T, F64], result: VOID }, - "dgram.getSendQueueSize": { argTypes: [DGRAMSOCK_T], result: F64 }, - "dgram.getSendQueueCount": { argTypes: [DGRAMSOCK_T], result: F64 }, - "dgram.disconnect": { argTypes: [DGRAMSOCK_T], result: VOID }, - "dgram.onMessage": { argTypes: [DGRAMSOCK_T, null, BOOL], result: VOID }, - "dgram.onError": { argTypes: [DGRAMSOCK_T, null, BOOL], result: VOID }, - "dgram.onListening": { - argTypes: [DGRAMSOCK_T, { kind: "func", params: [], ret: VOID }, BOOL], - result: VOID, - }, - "dgram.onClose": { - argTypes: [DGRAMSOCK_T, { kind: "func", params: [], ret: VOID }, BOOL], - result: VOID, - }, - "dgram.onConnect": { - argTypes: [DGRAMSOCK_T, { kind: "func", params: [], ret: VOID }, BOOL], - result: VOID, - }, - "dns.lookup": { argTypes: [STRING, F64, null], result: VOID }, - /** Node's argument forms with dyn callbacks: lookup(hostname, options, - * callback) and lookupService(address, port, callback). */ - "dns.lookupDyn": { argTypes: [DYN, DYN, DYN, BOOL], result: VOID }, - "dns.lookupService": { argTypes: [DYN, DYN, DYN, BOOL], result: VOID }, - /** node:dns/promises: lookup(hostname, options) and lookupService(address, - * port) over the callback forms; the promise fulfills with the dyn - * { address, family } (array for all: true) / { hostname, service }. - * Argument errors throw synchronously (may-throw), unlike the - * util.promisify forms below. */ - "dns.promisesLookup": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, - "dns.promisesLookupService": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, - "dns.getServers": { argTypes: [], result: DYN }, - "dns.getDefaultResultOrder": { argTypes: [], result: STRING }, - "dns.setDefaultResultOrder": { argTypes: [DYN], result: VOID }, - // node:test (scr_test.c). Bodies are program-dependent closures (0 or - // 1 testCtx param, void or Promise result — the spoke pinned the - // shape): null slots. sub's result is the settled Promise the - // await consumes. - /** node:test (scr_test.c — linked only when one of these appears on - * the IR; moduleUsesNodeTest is the switch, and the main epilogue asks - * scr_test_exit_code() for the process's exit status). Strings are - * BORROWED, callbacks MOVE. register/suite/hook attach to the runner - * tree (register: name, mode 0|1|2 run/skip/todo, directive message "" - * = none, cb or absent via registerEmpty, flags 1 async | 2 takes-ctx - * | 4 only, "file:line:col"); suite runs its body AT registration - * (Node's collection phase). sub is t.test — runs the subtest INLINE - * on the runner fiber and returns the settled promise the await - * consumes. ctxSkip/ctxTodo mark the running test; ctxDiagnostic - * queues an ℹ line; ctxName reads t.name. Every registration keeps - * the loop-run emitted (usesTimers) so the runner fiber drains. */ - // The trailing three are the { plan, timeout, concurrency } options (plan - // -1 without one; timeout read under flag 8; concurrency read under flag - // 16, -1 for true). An out-of-range timeout or concurrency throws. - "test.register": { - argTypes: [STRING, F64, STRING, null, F64, STRING, F64, F64, F64], - result: VOID, - }, - "test.registerEmpty": { - argTypes: [STRING, F64, STRING, F64, STRING, F64, F64, F64], - result: VOID, - }, - // The suite body takes no parameters or its SuiteContext (flag 2). - "test.suite": { argTypes: [STRING, F64, STRING, null, STRING, F64, F64], result: VOID }, - // The trailing string is the hook call's "file:line:col". - "test.hook": { argTypes: [F64, null, F64, STRING], result: VOID }, - "test.sub": { - argTypes: [TESTCTX_T, STRING, F64, STRING, null, F64, STRING, F64, F64, F64], - result: { kind: "promise", inner: VOID }, - }, - "test.subEmpty": { - argTypes: [TESTCTX_T, STRING, F64, STRING, F64, STRING, F64, F64, F64], - result: VOID, - }, - "test.ctxSkip": { argTypes: [TESTCTX_T, STRING], result: VOID }, - "test.ctxTodo": { argTypes: [TESTCTX_T, STRING], result: VOID }, - "test.ctxDiagnostic": { argTypes: [TESTCTX_T, STRING], result: VOID }, - "test.ctxName": { argTypes: [TESTCTX_T], result: STRING }, - "test.ctxFullName": { argTypes: [TESTCTX_T], result: STRING }, - // t.passed, and t.workerId's Number(NODE_TEST_WORKER_ID) with 0 for - // undefined. - "test.ctxPassed": { argTypes: [TESTCTX_T], result: BOOL }, - "test.workerId": { argTypes: [], result: F64 }, - // getTestContext(): whether a test, suite or hook is running in the - // current async context, and that context. - "test.hasContext": { argTypes: [], result: BOOL }, - "test.context": { argTypes: [], result: TESTCTX_T }, - // t.before/t.after/t.beforeEach/t.afterEach (which: 0 before, 1 after, - // 2 beforeEach, 3 afterEach); a throwing t.before is captured on the test. - "test.ctxHook": { argTypes: [TESTCTX_T, F64, null, F64], result: VOID }, - // t.plan(count), and the count of one t.assert call. - "test.ctxPlan": { argTypes: [TESTCTX_T, F64], result: VOID }, - "test.ctxCount": { argTypes: [TESTCTX_T], result: VOID }, - // The mock trackers: the module's `mock` and t.mock keep each mock's - // restore closure (moved) for restoreAll and reset. - "test.mockTrack": { argTypes: [{ kind: "func", params: [], ret: VOID }], result: VOID }, - "test.mockRestoreAll": { argTypes: [], result: VOID }, - "test.mockReset": { argTypes: [], result: VOID }, - "test.ctxMockTrack": { - argTypes: [TESTCTX_T, { kind: "func", params: [], ret: VOID }], - result: VOID, - }, - "test.ctxMockRestoreAll": { argTypes: [TESTCTX_T], result: VOID }, - "test.ctxMockReset": { argTypes: [TESTCTX_T], result: VOID }, - // mock.timers on the module's `mock` and on t.mock. enable takes one - // code per listed API ('T' setTimeout, 'I' setInterval), the `now` - // value and its flags (1 given, 2 from a Date). enable, tick, runAll and - // setTime throw Node's errors; tick and runAll run timer callbacks. - // t.assert.snapshot and fileSnapshot: the escaped, wrapped serialized - // value, and the stored value it is compared with (snapshot: the test, - // the value, the snapshot file; fileSnapshot: the value, the path). - // Both read or, in update mode, record; missing and unreadable - // snapshots throw Node's ERR_INVALID_STATE. - "test.snapshotValue": { argTypes: [STRING], result: STRING }, - "test.ctxSnapshot": { argTypes: [TESTCTX_T, STRING, STRING], result: STRING }, - "test.fileSnapshot": { argTypes: [STRING, STRING], result: STRING }, - // Node's serializeValue over a dyn array of serializers (undefined for - // the default JSON.stringify(value, null, 2)); a throw becomes Node's - // "The provided serializers did not generate a string." ERR_INVALID_STATE. - "test.snapshotSerialize": { argTypes: [DYN, DYN], result: DYN }, - "test.mockTimersEnable": { argTypes: [STRING, F64, F64], result: VOID }, - "test.mockTimersTick": { argTypes: [F64], result: VOID }, - "test.mockTimersRunAll": { argTypes: [], result: VOID }, - "test.mockTimersReset": { argTypes: [], result: VOID }, - "test.mockTimersSetTime": { argTypes: [F64], result: VOID }, - "test.ctxMockTimersEnable": { argTypes: [TESTCTX_T, STRING, F64, F64], result: VOID }, - "test.ctxMockTimersTick": { argTypes: [TESTCTX_T, F64], result: VOID }, - "test.ctxMockTimersRunAll": { argTypes: [TESTCTX_T], result: VOID }, - "test.ctxMockTimersReset": { argTypes: [TESTCTX_T], result: VOID }, - "test.ctxMockTimersSetTime": { argTypes: [TESTCTX_T, F64], result: VOID }, - // node:http (scr_http.c over scr_net.c). The handler and the data - // listeners are program-dependent closures (null slots, checked in the - // libCall case); reqHeader's result is the interned string|undefined - // union, checked like process.envGet. - /** node:http, the server slice (scr_http.c over scr_net.c — linked - * only when these appear on the IR; moduleUsesHttpServer is the - * switch, and any http.* libCall also counts as net use so scr_net.c - * links and installs). http.createServer's handler MOVES in and takes - * the runtime adapters for its (req, res) / (req) / () shapes; req - * body listeners follow the net.sockOnData story (bytes chunks, once - * flags); reqHeader answers the interned `string | undefined` union - * exactly like process.envGet; the res writers are borrowed-argument - * voids with Node's framing decided in the runtime (Content-Length - * for end-before-head, chunked after an explicit writeHead/write). */ - "http.createServer": { argTypes: [null], result: NETSERVER_T }, - /** http.createServer() / http.Server() with no handler — the - * on("request") route; option helpers set parser behavior on a fresh - * server before the constructor/factory result is returned. */ - "http.createServerEmpty": { argTypes: [], result: NETSERVER_T }, - "http.validateHeaderName": { argTypes: [STRING, STRING], result: VOID }, - "http.validateHeaderValue": { argTypes: [STRING, DYN], result: VOID }, - "http.serverJoinDupHeaders": { argTypes: [NETSERVER_T], result: VOID }, - "http.serverAllowMissingHostHeader": { argTypes: [NETSERVER_T], result: VOID }, - /** The five writable numeric http.Server timeout fields use one - * selector ABI: 0 timeout, 1 keepAliveTimeout, 2 headersTimeout, - * 3 requestTimeout, 4 keepAliveTimeoutBuffer. These calls store/read - * the property values; timer enforcement remains outside this surface. - * The constructor-option setter takes dyn so explicit undefined remains - * distinguishable from a numeric value until its Node validation ladder. */ - "http.serverTimeoutGet": { argTypes: [NETSERVER_T, F64], result: F64 }, - "http.serverSetTimeout": { argTypes: [NETSERVER_T, F64], result: VOID }, - "http.serverSetTimeoutCb": { argTypes: [NETSERVER_T, F64, null], result: VOID }, - "http.serverOnTimeout": { argTypes: [NETSERVER_T, null, BOOL], result: VOID }, - "http.serverCloseAllConnections": { argTypes: [NETSERVER_T], result: VOID }, - "http.serverCloseIdleConnections": { argTypes: [NETSERVER_T], result: VOID }, - "http.serverTimeoutSet": { argTypes: [NETSERVER_T, F64, F64], result: VOID }, - "http.serverTimeoutOptionSet": { argTypes: [NETSERVER_T, F64, DYN], result: VOID }, - /** server.on("listening", cb) — the deferred listen-callback list. */ - "net.serverOnListening": { - argTypes: [NETSERVER_T, { kind: "func", params: [], ret: VOID }, BOOL], - result: VOID, - }, - /** The ServerResponse member surface: statusCode/statusMessage reads - * and assignments (Node's writable properties), the header CRUD trio - * (getHeader answers `string | undefined` like reqHeader), and - * end(cb)'s callback slot (resOnEndCallback registers, the end call - * follows — the callback fires deferred, just before Node's 'finish' - * emit; resOnFinish is the 'finish' listener slot). writeHead's - * statusMessage forms compose in interned helpers: resStatusMsgSet - * then the ordinary writeHead entry. */ - "http.resStatusGet": { argTypes: [HTTPRES_T], result: F64 }, - "http.resStatusSet": { argTypes: [HTTPRES_T, F64], result: VOID }, - "http.resStatusMsgGet": { argTypes: [HTTPRES_T], result: VOID }, - "http.resStatusMsgSet": { argTypes: [HTTPRES_T, STRING], result: VOID }, - "http.resRequest": { argTypes: [HTTPRES_T], result: HTTPREQ_T }, - "http.resSocket": { argTypes: [HTTPRES_T], result: VOID }, - "http.resWritableFinished": { argTypes: [HTTPRES_T], result: BOOL }, - "http.resSendDateGet": { argTypes: [HTTPRES_T], result: BOOL }, - "http.resSendDateSet": { argTypes: [HTTPRES_T, BOOL], result: VOID }, - "http.resStrictContentLengthGet": { argTypes: [HTTPRES_T], result: BOOL }, - "http.resStrictContentLengthSet": { argTypes: [HTTPRES_T, BOOL], result: VOID }, - "http.resSetTimeout": { argTypes: [HTTPRES_T, F64], result: VOID }, - "http.resSetTimeoutCb": { - argTypes: [HTTPRES_T, F64, { kind: "func", params: [], ret: VOID }], - result: VOID, - }, - // resGetHeader answers the interned `string | undefined` union — the - // reqHeader/envGet sentinel pattern (VOID here, checked specially). - "http.resGetHeader": { argTypes: [HTTPRES_T, STRING], result: VOID }, - /** getHeader as Node answers it: the number, string or string[] that was - * set, or undefined, as a checked-dynamic value. */ - "http.resGetHeaderDyn": { argTypes: [HTTPRES_T, STRING], result: DYN }, - "http.resGetHeaderNames": { argTypes: [HTTPRES_T], result: arrayOf(STRING) }, - "http.resGetRawHeaderNames": { argTypes: [HTTPRES_T], result: arrayOf(STRING) }, - "http.resGetHeaders": { argTypes: [HTTPRES_T], result: DYN }, - "http.resHasHeader": { argTypes: [HTTPRES_T, STRING], result: BOOL }, - "http.resRemoveHeader": { argTypes: [HTTPRES_T, STRING], result: VOID }, - "http.resOnFinish": { - argTypes: [HTTPRES_T, { kind: "func", params: [], ret: VOID }], - result: VOID, - }, - "http.resOnEndCallback": { - argTypes: [HTTPRES_T, { kind: "func", params: [], ret: VOID }], - result: VOID, - }, - "http.reqUrl": { argTypes: [HTTPREQ_T], result: STRING }, - "http.reqMethod": { argTypes: [HTTPREQ_T], result: STRING }, - "http.reqHeader": { argTypes: [HTTPREQ_T, STRING], result: VOID }, - "http.reqTrailer": { argTypes: [HTTPREQ_T, STRING], result: VOID }, - "http.reqHeaderValues": { argTypes: [HTTPREQ_T, STRING], result: VOID }, - "http.reqTrailerValues": { argTypes: [HTTPREQ_T, STRING], result: VOID }, - "http.reqOnData": { argTypes: [HTTPREQ_T, null, BOOL], result: VOID }, - "http.reqOnEnd": { - argTypes: [HTTPREQ_T, { kind: "func", params: [], ret: VOID }, BOOL], - result: VOID, - }, - "http.resSetHeader": { argTypes: [HTTPRES_T, STRING, STRING], result: VOID }, - /** setHeader / appendHeader with a number, string or array value; the - * stored value keeps its type for getHeader/getHeaders. MAY THROW. */ - "http.resSetHeaderDyn": { argTypes: [HTTPRES_T, STRING, DYN], result: VOID }, - "http.resAppendHeader": { argTypes: [HTTPRES_T, STRING, DYN], result: VOID }, - "http.resWriteHead": { argTypes: [HTTPRES_T, F64], result: VOID }, - "http.resWriteHeadN": { - argTypes: [HTTPRES_T, F64, arrayOf(STRING), arrayOf(STRING)], - result: VOID, - }, - /** HTTP/1.1 informational heads; early hints takes flat string pairs, - * with a lowercase `link` key required to send anything. */ - "http.resWriteContinue": { argTypes: [HTTPRES_T], result: VOID }, - "http.resWriteProcessing": { argTypes: [HTTPRES_T], result: VOID }, - "http.resWriteEarlyHints": { argTypes: [HTTPRES_T, arrayOf(STRING)], result: VOID }, - "http.resWrite": { argTypes: [HTTPRES_T, STRING], result: VOID }, - "http.resWriteBytes": { argTypes: [HTTPRES_T, BYTES_U8], result: VOID }, - "http.resEnd": { argTypes: [HTTPRES_T], result: VOID }, - "http.resEndStr": { argTypes: [HTTPRES_T, STRING], result: VOID }, - "http.resEndBytes": { argTypes: [HTTPRES_T, BYTES_U8], result: VOID }, - /** The checked-dynamic chunk twins (the net.sockWriteDyn story). */ - "http.resWriteDyn": { argTypes: [HTTPRES_T, DYN], result: VOID }, - "http.resEndDyn": { argTypes: [HTTPRES_T, DYN], result: VOID }, - "http.resHeadersSent": { argTypes: [HTTPRES_T], result: BOOL }, - "http.resWritableEnded": { argTypes: [HTTPRES_T], result: BOOL }, - "http.resFlushHeaders": { argTypes: [HTTPRES_T], result: VOID }, - "http.resAddTrailers": { argTypes: [HTTPRES_T, arrayOf(STRING)], result: VOID }, - "http.resCork": { argTypes: [HTTPRES_T], result: VOID }, - "http.resUncork": { argTypes: [HTTPRES_T], result: VOID }, - "http.resWritableCorked": { argTypes: [HTTPRES_T], result: F64 }, - // The member follow-ups: reqStatusCode's `number | undefined` and - // sockRemoteAddress's `string | undefined` results are shape-checked in - // the special cases in validate.ts (like reqHeader/columns). - /** The server-surface member follow-ups: reqStatusCode answers the - * interned `number | undefined` union (negative = the undefined arm — - * a SERVER request, where Node's statusCode is undefined; every client - * response carries a real status); reqSocket is the underlying - * connection (+1, the same handle net.connect would give); - * sockRemoteAddress answers `string | undefined` (NULL after the fd - * closed, Node's destroyed-socket undefined; a dual-stack accept of an - * IPv4 peer reads "::ffff:a.b.c.d" like Node). reqResume/reqPause/reqDestroy and - * the req error/close listener slots complete the IncomingMessage - * surface portless's client responses use; resDestroy/resOnClose and - * resWriteHeadPairs ([k0,v0,k1,v1,...] — the env.pairs helper's flat - * shape) complete ServerResponse. sockSetTimeout arms the idle - * EVFILT_TIMER ('timeout' fires after ms of inactivity, once per idle - * period, never destroying the socket — Node's semantics). */ - "http.reqStatusCode": { argTypes: [HTTPREQ_T], result: VOID }, - // reqStatusMessage's `string | undefined` is program-interned like - // reqHeader's (the VOID here is the envGet sentinel). - "http.reqStatusMessage": { argTypes: [HTTPREQ_T], result: VOID }, - /** req.rawHeaders — [name, value, name, value, ...] in arrival order, - * names in their ORIGINAL case (Node's shape); a fresh string[] per - * read. reqStatusMessage answers the interned `string | undefined` - * union — the reason phrase on client responses ("" when the status - * line carried none), the undefined arm on server requests (the - * statusCode split). sockDestroyed is socket.destroyed — true once the - * fd is gone (destroy() or full close). */ - "http.reqRawHeaders": { argTypes: [HTTPREQ_T], result: arrayOf(STRING) }, - /** The `{ ...req.headers }` snapshot feed: [lowercased name, value, - * ...] pairs in arrival order — the interned %headers.snapshot helper - * builds the record over it, exactly the process.envPairs pattern. */ - "http.reqHeaderPairs": { argTypes: [HTTPREQ_T], result: arrayOf(STRING) }, - /** Every value of one header in arrival order (empty when absent): the - * set-cookie array of a `{ ...req.headers }` snapshot. */ - "http.reqHeaderList": { argTypes: [HTTPREQ_T, STRING], result: arrayOf(STRING) }, - "http.reqRawTrailers": { argTypes: [HTTPREQ_T], result: arrayOf(STRING) }, - "http.reqTrailerPairs": { argTypes: [HTTPREQ_T], result: arrayOf(STRING) }, - "net.sockDestroyed": { argTypes: [NETSOCKET_T], result: BOOL }, - /** socket.writable — the write half is open: no end() yet, no FIN sent, - * fd alive (connecting sockets answer true; writes queue). Node's - * stream flag. Borrows; never throws. */ - "net.sockWritable": { argTypes: [NETSOCKET_T], result: BOOL }, - // The 'upgrade' registrations: the callback shapes are program-typed - // ((req, socket, head) and shorter prefixes — the libCall case checks). - /** The 'upgrade' events, both sides (SEMANTICS.md — the WebSocket - * proxying surface): serverOnUpgrade registers (req, socket, head) - * listeners fired INSTEAD of 'request' for Connection: upgrade - * requests (the parser steps aside; the socket is raw; `head` carries - * bytes past the request head; no listener = the socket destroys, - * Node's default). clientOnUpgrade is the client twin: a 101 response - * fires (res, socket, head) INSTEAD of 'response'. */ - "http.serverOnUpgrade": { argTypes: [NETSERVER_T, null, BOOL], result: VOID }, - /** server.on("connect", ...) — HTTP CONNECT tunneling, the 'upgrade' - * machinery's twin: (req, socket, head) fired INSTEAD of 'request' for - * CONNECT-method requests (no listener = the socket destroys, Node's - * default). The h2 compat server's 'connect' (portless's RFC 8441 - * handler) only ever sees the HTTP/1.1 arm under the allowHTTP1 - * lowering, so a listener whose second parameter is a UNION with a - * netSocket arm takes the socket wrapped at that arm (an emitted - * per-shape adapter — the tags are program data). */ - "http.serverOnConnect": { argTypes: [NETSERVER_T, null, BOOL], result: VOID }, - "http.clientOnUpgrade": { argTypes: [HTTPCLIENTREQ_T, null, BOOL], result: VOID }, - "http.reqSocket": { argTypes: [HTTPREQ_T], result: NETSOCKET_T }, - // reqH2Stream's program-interned Http2Stream | undefined result is - // checked in the special libCall cases in validate.ts. - "http.reqH2Stream": { argTypes: [HTTPREQ_T], result: VOID }, - "http.reqH2StreamOrThrow": { argTypes: [HTTPREQ_T, STRING], result: HTTP2STREAM_T }, - /** req.pipe(dest) — the IncomingMessage body streaming into a - * ServerResponse (the proxy's response leg), a ClientRequest (the - * request-body forward), or a raw socket (the upgrade-rejection leg); - * chunk-for-chunk, natural end ends the destination (Node's pipe - * default; no backpressure — divergence 54's stream model). */ - "http.reqPipeRes": { argTypes: [HTTPREQ_T, HTTPRES_T], result: VOID }, - "http.reqPipeClient": { argTypes: [HTTPREQ_T, HTTPCLIENTREQ_T], result: VOID }, - "http.reqPipeSock": { argTypes: [HTTPREQ_T, NETSOCKET_T], result: VOID }, - "http.reqResume": { argTypes: [HTTPREQ_T], result: VOID }, - "http.reqPause": { argTypes: [HTTPREQ_T], result: VOID }, - "http.reqDestroy": { argTypes: [HTTPREQ_T], result: VOID }, - "http.reqOnError": { argTypes: [HTTPREQ_T, null, BOOL], result: VOID }, - "http.reqOnClose": { - argTypes: [HTTPREQ_T, { kind: "func", params: [], ret: VOID }, BOOL], - result: VOID, - }, - "http.reqOnAborted": { - argTypes: [HTTPREQ_T, { kind: "func", params: [], ret: VOID }, BOOL], - result: VOID, - }, - "http.reqHttpVersion": { argTypes: [HTTPREQ_T], result: STRING }, - "http.reqHttpVersionMajor": { argTypes: [HTTPREQ_T], result: F64 }, - "http.reqHttpVersionMinor": { argTypes: [HTTPREQ_T], result: F64 }, - "http.reqAborted": { argTypes: [HTTPREQ_T], result: BOOL }, - "http.reqComplete": { argTypes: [HTTPREQ_T], result: BOOL }, - "http.resDestroy": { argTypes: [HTTPRES_T], result: VOID }, - "http.resOnClose": { - argTypes: [HTTPRES_T, { kind: "func", params: [], ret: VOID }, BOOL], - result: VOID, - }, - "http.resWriteHeadPairs": { argTypes: [HTTPRES_T, F64, arrayOf(STRING)], result: VOID }, - /** writeHead(status, headers) with a checked-dynamic headers value — - * the runtime OBJ walk (string/number values; other values throw - * Node's invalid-argument TypeError; may throw). */ - "http.resWriteHeadDyn": { argTypes: [HTTPRES_T, F64, DYN], result: VOID }, - "net.sockSetTimeout": { argTypes: [NETSOCKET_T, F64], result: VOID }, - /** setEncoding('utf8') — 'data' delivers strings inside the chunk-encoding window; other real encodings throw the "not supported yet" Error, unknown names throw ERR_UNKNOWN_ENCODING (may throw). */ - "net.sockSetEncoding": { argTypes: [NETSOCKET_T, STRING], result: VOID }, - "http.reqSetEncoding": { argTypes: [HTTPREQ_T, STRING], result: VOID }, - "net.sockOnTimeout": { - argTypes: [NETSOCKET_T, { kind: "func", params: [], ret: VOID }, BOOL], - result: VOID, - }, - "net.sockRemoteAddress": { argTypes: [NETSOCKET_T], result: VOID }, - /** socket.encrypted — `boolean | undefined`: the true arm iff the - * socket carries a TLS transport (Node types `encrypted: true` on - * TLSSocket; plain sockets answer undefined — the proxy.ts isEncrypted - * idiom reads it through a cast). */ - "net.sockEncrypted": { argTypes: [NETSOCKET_T], result: VOID }, - // node:http, the client slice. The response callback shapes are - // program-dependent (checked by validate.ts); header pairs arrive flat. - /** node:http, the CLIENT slice (http.request/http.get over the net - * client machinery): request/requestCb take (host, port, path, method, - * timeoutMs, headerPairs, autoEnd[, responseCb]) — headerPairs is the - * flat [k0,v0,...] array (empty for none), autoEnd true is http.get's - * eager end(). The handle owns one dialed connection (NO pooling — the - * wire still carries Node's exact head: user headers, then Host, - * Connection: keep-alive, and the framing header; the socket closes - * when the response completes). The response delivered to responseCb / - * 'response' listeners IS an httpReq (IncomingMessage), status and - * headers parsed from the wire, body via reqOnData/reqOnEnd. Errors are - * Node-shaped ('connect ECONNREFUSED ip:port', 'socket hang up') and - * fire 'error' then 'close'; an unhandled 'error' exits 1 like every - * net handle. clientWrite before end commits to chunked framing unless - * the caller set content-length; clientEnd(data) before any write sends - * Content-Length exactly like Node. */ - "http.request": { - argTypes: [STRING, F64, STRING, STRING, F64, arrayOf(STRING), BOOL], - result: HTTPCLIENTREQ_T, - }, - "http.requestCb": { - argTypes: [STRING, F64, STRING, STRING, F64, arrayOf(STRING), BOOL, null], - result: HTTPCLIENTREQ_T, - }, - /** new http.Agent(opts) / new https.Agent(opts): (secure, keepAlive, - * keepAliveMsecs, maxSockets, maxFreeSockets, timeoutMs, port) — the - * numeric options arrive < 0 for "unset" (Infinity/256/none; port - * seeds the settable defaultPort, Node's option merge). Returns the - * Agent as a checked-dynamic HANDLE (getName/destroy and the - * sockets/requests/freeSockets counters dispatch through the dyn - * handle ops). keepAlive: true THROWS the named construction fence — - * socket POOLING is not modeled (one dial per request); maxSockets - * accounting is real: over-limit requests defer their dial and queue. - * MAY THROW. */ - "http.agentNew": { argTypes: [BOOL, BOOL, F64, F64, F64, F64, F64, DYN], result: DYN }, - /** http.globalAgent / https.globalAgent (true): Node's keepAlive global - * agent, the one requests without an agent option use. */ - "http.globalAgent": { argTypes: [BOOL], result: DYN }, - /** http.request's `signal` option over the fresh request: an aborted - * signal destroys it with Node's AbortError now, a later abort event - * does the same. A falsy value is ignored; a non-signal throws - * ERR_INVALID_ARG_TYPE. */ - "http.clientSignal": { argTypes: [HTTPCLIENTREQ_T, DYN], result: VOID }, - /** options.signal checked before the request is built (ERR_INVALID_ARG_TYPE). */ - "http.clientSignalCheck": { argTypes: [DYN], result: VOID }, - /** https.request's servername option, stored for the construction call - * that follows: the TLS name sent and verified, and part of the pooled - * socket key. The "https." prefix keeps the TLS unit linked. */ - "https.nextServername": { argTypes: [STRING], result: VOID }, - /** The maxHeaderSize option of createServer / http.request: Node's - * validateInteger (ERR_OUT_OF_RANGE) then the header size bound. - * MAY THROW. */ - "http.serverMaxHeaderSize": { argTypes: [NETSERVER_T, F64], result: VOID }, - "http.clientMaxHeaderSize": { argTypes: [HTTPCLIENTREQ_T, F64], result: VOID }, - /** req.destroy(err): err becomes the request's 'error'. */ - "http.clientDestroyError": { - argTypes: [HTTPCLIENTREQ_T, { kind: "object", className: "%Error" }], - result: VOID, - }, - /** The agent-threaded request rows: the http.request/https.request - * shape with a trailing `agent` dyn argument (an Agent handle, false — - * the one-shot Connection: close dial — or null/undefined for the - * default path). port < 0 means "no port option": the agent's settable - * defaultPort, then the scheme's. MAY THROW (a non-Agent value is - * Node's ERR_INVALID_ARG_TYPE). */ - "http.requestAgent": { - argTypes: [STRING, F64, STRING, STRING, F64, arrayOf(STRING), BOOL, DYN], - result: HTTPCLIENTREQ_T, - }, - "http.requestAgentCb": { - argTypes: [STRING, F64, STRING, STRING, F64, arrayOf(STRING), BOOL, DYN, null], - result: HTTPCLIENTREQ_T, - }, - /** request/get with a URL-STRING first argument: the runtime parses it - * (WHATWG) and dials — throws catchably on an unparsable input or a - * non-http scheme. */ - "http.requestUrl": { argTypes: [STRING, STRING, BOOL], result: HTTPCLIENTREQ_T }, - "http.requestUrlCb": { argTypes: [STRING, STRING, BOOL, null], result: HTTPCLIENTREQ_T }, - /** request/get with a runtime options record: a RequestOptions - * variable, or a literal that spreads one or sets socketPath. Args - * (options, autoEnd[, cb]). The runtime reads the record in the - * ClientRequest constructor's order (host/port or socketPath, path, - * method, headers, timeout, agent, signal, maxHeaderSize) with Node's - * errors, and documented options it does not implement throw the - * runtime fence. The "https." pair adds the TLS options. MAY THROW. */ - "http.requestDyn": { argTypes: [DYN, BOOL], result: HTTPCLIENTREQ_T }, - "http.requestDynCb": { argTypes: [DYN, BOOL, null], result: HTTPCLIENTREQ_T }, - "https.requestDyn": { argTypes: [DYN, BOOL], result: HTTPCLIENTREQ_T }, - "https.requestDynCb": { argTypes: [DYN, BOOL, null], result: HTTPCLIENTREQ_T }, - /** The paused-mode demux surface (portless's first-byte TLS peek: - * once('readable') + read(1) + unshift + emit('connection')). - * sockOnReadable registers a zero-param listener (a consumer: arrived - * bytes buffer instead of flowing, and EOF announces too); sockRead - * answers `Buffer | null` (exactly n buffered bytes, or null — Node's - * less-than-n answer; n <= 0 drains everything); sockUnshift returns - * bytes to the front of the stream; serverEmitConnection routes a - * socket into another server's protocol layer (a TLS target's - * 'connection' waits for its handshake). */ - "net.sockOnReadable": { argTypes: [NETSOCKET_T, null, BOOL], result: VOID }, - // sockRead's result is the interned `Buffer | null` union — checked - // specially in validate.ts (the reqHeader/envGet pattern; VOID here is a - // placeholder the special case overrides). - "net.sockRead": { argTypes: [NETSOCKET_T, F64], result: VOID }, - "net.sockUnshift": { argTypes: [NETSOCKET_T, BYTES_U8], result: VOID }, - /** Socket flow control and the compat surface: pause/resume (reads - * gate off/on — kernel backpressure holds paused bytes; resume flows - * and discards sans listeners) and setNoDelay answer the SOCKET (+1, - * Node's chaining); destroySoon ends now and destroys once the FIN - * flushed; bytesWritten counts accepted bytes; readable is true until - * the read half ends. */ - "net.sockPause": { argTypes: [NETSOCKET_T], result: NETSOCKET_T }, - "net.sockResume": { argTypes: [NETSOCKET_T], result: NETSOCKET_T }, - "net.sockSetNoDelay": { argTypes: [NETSOCKET_T, BOOL], result: NETSOCKET_T }, - "net.sockDestroySoon": { argTypes: [NETSOCKET_T], result: VOID }, - /** RST close, answers the socket; throws ERR_INVALID_HANDLE_TYPE on TLS. */ - "net.sockResetAndDestroy": { argTypes: [NETSOCKET_T], result: NETSOCKET_T }, - /** socket.address(): { address, family, port } or {} as a dyn. */ - "net.sockAddress": { argTypes: [NETSOCKET_T], result: DYN }, - "net.sockBytesWritten": { argTypes: [NETSOCKET_T], result: F64 }, - /** remotePort (0), remoteFamily (1), localAddress (2), localPort (3), - * localFamily (4): dyn values, undefined when unknown. */ - "net.sockAddrField": { argTypes: [NETSOCKET_T, F64], result: DYN }, - "net.sockBytesRead": { argTypes: [NETSOCKET_T], result: F64 }, - "net.sockConnecting": { argTypes: [NETSOCKET_T], result: BOOL }, - "net.sockPending": { argTypes: [NETSOCKET_T], result: BOOL }, - "net.sockReadyState": { argTypes: [NETSOCKET_T], result: STRING }, - "net.sockSetKeepAlive": { argTypes: [NETSOCKET_T, BOOL, F64], result: NETSOCKET_T }, - "net.sockOnReady": { - argTypes: [NETSOCKET_T, { kind: "func", params: [], ret: VOID }, BOOL], - result: VOID, - }, - /** Listeners with a parameter, as dyn functions: close(hadError), - * write's cb(err) and end's cb. */ - "net.sockOnCloseDyn": { argTypes: [NETSOCKET_T, DYN, BOOL], result: VOID }, - /** socket.on/once('lookup', listener): the listener (a dyn function) - * gets (err, address, family, host) once per resolved address, or once - * with the lookup error, before the connect. */ - "net.sockOnLookup": { argTypes: [NETSOCKET_T, DYN, BOOL], result: VOID }, - "net.sockOnWriteFlush": { - argTypes: [NETSOCKET_T, { kind: "func", params: [], ret: VOID }], - result: VOID, - }, - "net.sockOnWriteFlushDyn": { argTypes: [NETSOCKET_T, DYN], result: VOID }, - "net.sockOnFinishDyn": { argTypes: [NETSOCKET_T, DYN], result: VOID }, - "net.sockSetTimeoutCb": { - argTypes: [NETSOCKET_T, F64, { kind: "func", params: [], ret: VOID }], - result: VOID, - }, - "net.isIP": { argTypes: [DYN], result: F64 }, - "net.isIPv4": { argTypes: [DYN], result: BOOL }, - "net.isIPv6": { argTypes: [DYN], result: BOOL }, - "net.serverMaxConnectionsGet": { argTypes: [NETSERVER_T], result: DYN }, - "net.serverMaxConnectionsSet": { argTypes: [NETSERVER_T, DYN], result: VOID }, - "net.serverDropMaxConnectionGet": { argTypes: [NETSERVER_T], result: DYN }, - "net.serverDropMaxConnectionSet": { argTypes: [NETSERVER_T, DYN], result: VOID }, - /** net.SocketAddress and net.BlockList (scr_net.c): the constructors - * take the argument array; the handles are dyn values. */ - "net.socketAddressNew": { argTypes: [DYN], result: DYN }, - "net.socketAddressParse": { argTypes: [DYN], result: DYN }, - "net.isSocketAddress": { argTypes: [DYN], result: BOOL }, - "net.blockListNew": { argTypes: [DYN], result: DYN }, - "net.isBlockList": { argTypes: [DYN], result: BOOL }, - "net.blockListPrivateRanges": { argTypes: [], result: DYN }, - "net.serverOnDrop": { argTypes: [NETSERVER_T, DYN, BOOL], result: VOID }, - "net.serverGetConnections": { argTypes: [NETSERVER_T, DYN], result: VOID }, - "net.sockReadable": { argTypes: [NETSOCKET_T], result: BOOL }, - /** socket.on('finish', cb) / end(cb): fires once when the FIN goes out - * (sweep-deferred, never the registering stack). */ - "net.sockOnFinish": { - argTypes: [NETSOCKET_T, { kind: "func", params: [], ret: VOID }], - result: VOID, - }, - "net.serverEmitConnection": { argTypes: [NETSERVER_T, NETSOCKET_T], result: VOID }, - // tls/https: cert/key/ca PEM arguments are strings OR Buffers (null = - // both accepted; the emitter passes data+len either way). - /** node:tls + node:https (scr_tls.c over scr_net.c/scr_http.c, with - * the vendored mbedTLS archive — linked only when one of these appears - * on the IR; moduleUsesTls is the switch, and every tls/https libCall - * also counts as net AND http use so both units link and install). - * tls.createServer takes (cert, key[, handler]) — cert/key are PEM - * strings or Buffers (the emitter passes data+len for either); the - * handler is Node's 'secureConnection' (fires post-handshake with a - * socket that behaves exactly like a net socket, the same adapters as - * net.createServer). https.createServer is (cert, key, handler) with - * the http request-handler adapters. https.request/requestCb extend - * the http client row with (…, rejectUnauthorized: bool, ca: PEM - * string/Buffer — "" for none) and default port 443; everything else - * (write/end/destroy/events, the response surface) IS the http client - * surface — the handles are the same kinds. */ - "tls.createServer": { argTypes: [null, null], result: NETSERVER_T }, - "tls.createServerCb": { argTypes: [null, null, null], result: NETSERVER_T }, - /** The SNICallback forms: (cert, key, SNICallback[, handler]). The - * callback is http2.createSecureServerSni's shape, a function or a - * function | undefined union. */ - "tls.createServerSni": { argTypes: [null, null, null], result: NETSERVER_T }, - "tls.createServerSniCb": { argTypes: [null, null, null, null], result: NETSERVER_T }, - // The runtime options records (divergence 66's stance): a dyn options - // value whose members read at runtime; pemDyn extracts a runtime-valued - // cert/key member (arg 1 is the precomposed fence label). - /** RUNTIME options records (the divergence-66 stance): the *Dyn - * creators take a checked-dynamic (dyn) options value whose members - * read at runtime — cert/key extract like the literal path, members - * whose literal forms fence THROW the catchable fence at runtime, and - * undocumented keys drop like Node drops them. tls.pemDyn is the - * literal walk's runtime-valued cert/key extraction: (value, whatLit) - * → PEM bytes (strings/Buffers/one-element arrays of those) or the - * thrown fence. All of them may throw. */ - "tls.pemDyn": { argTypes: [DYN, STRING], result: BYTES_U8 }, - "tls.createServerDyn": { argTypes: [DYN], result: NETSERVER_T }, - "tls.createServerDynCb": { argTypes: [DYN, null], result: NETSERVER_T }, - "https.createServerDyn": { argTypes: [DYN], result: NETSERVER_T }, - "https.createServerDynCb": { argTypes: [DYN, null], result: NETSERVER_T }, - /** createSecureServer(options, handler) — the eager COMPAT handler as - * the first 'request' listener (Node's exact route), on both literal - * flavors: Req is the dual ALPN allowHTTP1 server, H2Req the ALPN=h2-only - * server (both use compat handles over h2 streams). Args are - * (cert, key, cb, enableConnectProtocol). */ - "http2.createSecureServerReq": { argTypes: [null, null, null, BOOL], result: NETSERVER_T }, - "http2.createSecureServerH2Req": { argTypes: [null, null, null, BOOL], result: NETSERVER_T }, - /** createSecureServer with a RUNTIME options record (the divergence-66 - * stance): allowHTTP1/cert/key read at runtime — allowHTTP1 picks the - * flavor, the TLS server walk fences its out-of-bounds members with - * the catchable runtime fence, and h2 session-tuning keys drop exactly - * like the literal walk ignores them. May throw. The Cb form carries - * the eager compat handler. */ - "http2.createSecureServerDyn": { argTypes: [DYN], result: NETSERVER_T }, - "http2.createSecureServerDynCb": { argTypes: [DYN, null], result: NETSERVER_T }, - // tls.connect(port, host, opts[, cb]) — port -1 / host "" read the - // options record; the callback fires post-handshake (secureConnect). - /** tls.connect — the TLS client socket: (port, host, opts[, cb]) where - * port -1 reads options.port, host "" reads options.host, and opts is - * the runtime (dyn) options record (rejectUnauthorized/ca/servername - * implemented; other documented members throw the runtime fence). The - * callback fires post-handshake — Node's secureConnect timing. */ - "tls.connect": { argTypes: [F64, STRING, DYN], result: NETSOCKET_T }, - "tls.connectCb": { argTypes: [F64, STRING, DYN, null], result: NETSOCKET_T }, - // The TLSSocket member surface (authError's `string | null` union is - // shape-checked in the special cases in validate.ts — the reqHeader pattern). - /** The TLSSocket member surface on the socket kind: authorized (bool — - * Node's verify verdict), authorizationError (the verify-failure CODE - * STRING or null), the 'secureConnect' registration (a TLS socket's - * conn list fires at establishment; plain sockets never fire it), and - * the 'session' registration (fires once with the serialized session — - * a Buffer; the received-ticket event). */ - "tls.sockAuthorized": { argTypes: [NETSOCKET_T], result: BOOL }, - /** getProtocol() (string | null), getCipher() (CipherNameAndProtocol, - * undefined before the handshake, null after close) and alpnProtocol - * (string | false | null), as dyn values. */ - "tls.sockGetProtocol": { argTypes: [NETSOCKET_T], result: DYN }, - "tls.sockGetCipher": { argTypes: [NETSOCKET_T], result: DYN }, - "tls.sockAlpnProtocol": { argTypes: [NETSOCKET_T], result: DYN }, - "tls.sockAuthError": { argTypes: [NETSOCKET_T], result: VOID }, - "tls.sockOnSecureConnect": { - argTypes: [NETSOCKET_T, { kind: "func", params: [], ret: VOID }, BOOL], - result: VOID, - }, - "tls.sockOnSession": { argTypes: [NETSOCKET_T, null, BOOL], result: VOID }, - /** The 'secure' event: the handshake completed (after secureConnect on - * a client). Plain sockets never fire it. */ - "tls.sockOnSecure": { - argTypes: [NETSOCKET_T, { kind: "func", params: [], ret: VOID }, BOOL], - result: VOID, - }, - /** new tls.TLSSocket(socket, options) in server mode: a new TLS socket - * takes over the socket's connection (wrapSocketCtx passes a literal's - * secureContext member apart from the runtime options record). The - * record reads at runtime: isServer, cert, key, ALPNProtocols, - * minVersion and maxVersion are implemented, the client mode and other - * documented members throw the runtime fence. MAY THROW. */ - "tls.wrapSocket": { argTypes: [NETSOCKET_T, DYN], result: NETSOCKET_T }, - "tls.wrapSocketCtx": { argTypes: [NETSOCKET_T, DYN, SECURECTX_T], result: NETSOCKET_T }, - // createSecureContext({ cert, key }) mints the opaque SNI-answer handle. - /** tls.createSecureContext({ cert, key }) — parses the PEM pair into an - * opaque SecureContext handle (secureCtx kind) for SNI callbacks to - * answer with; cert/key are PEM strings or Buffers like createServer's. */ - "tls.createSecureContext": { argTypes: [null, null], result: SECURECTX_T }, - /** createSecureContext over a RUNTIME options record (the checked- - * dynamic lane): Node's typed option validations first (the ciphers/ - * passphrase/engine/version/timeout/ticketKeys ladders), then the pem - * walk — a validated { cert, key } bag builds the real context. - * May-throw. */ - "tls.createSecureContextDyn": { argTypes: [DYN], result: SECURECTX_T }, - /** tls.getCACertificates(type): validateString + the documented name - * set, then the trailing compiler-rendered fence — ALWAYS THROWS (the - * error.nodeThrow polymorphic-result carve-out). May-throw seed. */ - "tls.caCertsChk": { argTypes: [DYN, STRING], result: VOID }, - // The CA-store introspection unit (scr_tls_ca.c): per-type cached PEM - // string arrays and the default-set replacement. - /** The CA-store introspection surface (scr_tls_ca.c — its own unit and - * link gate, "tlsca." NOT "tls.", so a getCACertificates-only binary - * never pulls mbedTLS): tlsca.get is tls.getCACertificates(type) — the - * cached per-type string[] of PEM blocks (identity-stable across calls, - * Node's caching; an unknown type throws Node's ERR_INVALID_ARG_VALUE - * TypeError); tlsca.root is the tls.rootCertificates value read; - * tlsca.set is tls.setDefaultCACertificates(certs) — replaces the - * default set (deduped) and the anchors the TLS client verifies - * against, throwing Node's ERR_CRYPTO_OPERATION_FAILED when no entry - * carries a certificate block. */ - "tlsca.get": { argTypes: [STRING], result: arrayOf(STRING) }, - "tlsca.root": { argTypes: [], result: arrayOf(STRING) }, - "tlsca.set": { argTypes: [arrayOf(STRING)], result: VOID }, - "https.createServer": { argTypes: [null, null, null], result: NETSERVER_T }, - // http2's allowHTTP1 compatibility server (divergence 57): cert/key - // like tls.createServer; the 'request' handler arrives separately via - // http.serverOnRequest (shape checked in the libCall case, like - // http.createServer's). sessionError callback shape is checked by validate.ts. - /** node:http2 allowHTTP1: one TLS server advertises h2 + http/1.1 and - * dispatches to the matching parser after ALPN. Request listeners mirror - * into both compatibility lists; the remaining session/req.stream rows - * below retain their explicitly limited behavior. */ - "http2.createSecureServer": { argTypes: [null, null, BOOL], result: NETSERVER_T }, - // The SNI-callback form: arg 2 is the JS SNICallback closure — a - // `(servername, cb) => void` func, or its `| undefined` union from the - // conditional-spread spelling (the libCall case checks the shape). - /** createSecureServer with an SNI callback: args are (cert, key, sniCb) - * where sniCb is the JS SNICallback — a `(servername, cb) => void` - * closure, or the `SNICallback | undefined` union from the conditional- - * spread spelling (`...(x ? { SNICallback: x } : {})`; the emitter - * unwraps the union — the undefined arm means "no callback", exactly - * the no-SNI server). The runtime parses each connection's ClientHello - * for the server_name extension BEFORE the TLS handshake begins, calls - * the callback with (servername, answer-closure), and resumes the - * handshake when the answer arrives — cb(err) tears the socket down - * without an error event (Node's 'tlsClientError' default), cb(null, ctx) serves - * ctx's cert/key, cb(null, undefined) serves the default pair. */ - "http2.createSecureServerSni": { argTypes: [null, null, null, BOOL], result: NETSERVER_T }, - // The ALPN=h2 server (createSecureServer without allowHTTP1): the real - // h2 session machinery behind the TLS handshake. - /** The REAL h2-over-TLS server (createSecureServer WITHOUT allowHTTP1): - * scr_http2_create_secure_server — the h2c session machinery behind an - * mbedTLS handshake whose ALPN advertises h2 alone (an http/1.1-only - * client fails the handshake with no_application_protocol, Node's - * h2-only split). args are (cert, key) — PEM strings or Buffers. */ - "http2.createSecureServerH2": { argTypes: [null, null, BOOL], result: NETSERVER_T }, - "http.serverOnRequest": { argTypes: [NETSERVER_T, null, BOOL], result: VOID }, - /** server.on('checkContinue' | 'checkExpectation', (req, res)): the - * 'request' listener shape; the last argument picks checkExpectation. */ - "http.serverOnCheck": { argTypes: [NETSERVER_T, null, BOOL, BOOL], result: VOID }, - "http2.serverOnSessionError": { argTypes: [NETSERVER_T, null, BOOL], result: VOID }, - /** The guarded absent-session compatibility call remains a no-op. */ - "http2.streamNoop": { argTypes: [], result: VOID }, - /** The UNGUARDED h2-only stream call (`req.stream.on(...)`): stream is - * undefined on every connection the allowHTTP1 lowering accepts — and - * on every HTTP/1.1 connection of Node's own allowHTTP1 server — so the - * call IS Node's member read on undefined: throws the exact catchable - * TypeError ("Cannot read properties of undefined (reading 'on')"). - * One arg: the read member's name (a string literal). Never returns. */ - "http2.streamUndefCall": { argTypes: [STRING], result: VOID }, - // The REAL h2c surface (scr_http2.c). Callback slots are null (the - // emitter picks the thunk); pairs slots are string arrays; the - // endStream tri-state and event flags ride as f64. - /** node:http2, the REAL h2c surface (scr_http2.c — frame codec + HPACK - * over the net loop; the design note atop that file has the story). - * Sessions and streams are first-class handle kinds; 'stream'/'response' - * payloads cross as flat [name, value, ...] pairs arrays and an EMITTED - * adapter closure builds the program-side headers record (the response - * :status rides separately as a number). */ - "http2.createServer": { argTypes: [], result: NETSERVER_T }, - "http2.createServerReq": { argTypes: [null], result: NETSERVER_T }, - "http2.serverOnStream": { argTypes: [NETSERVER_T, null, BOOL], result: VOID }, - "http2.serverOnSession": { argTypes: [NETSERVER_T, null, BOOL], result: VOID }, - // connect(authority, reject, ca[, cb]): the TLS client knobs an https - // authority reads (inert on http ones, exactly Node); ca is a PEM - // string/Buffer ("" = the system anchors). - /** connect(authority[, listener]) — h2c prior knowledge; the listener - * closure (if any) is the 'connect' once-listener. */ - "http2.connect": { argTypes: [STRING, BOOL, null], result: HTTP2SESSION_T }, - "http2.connectCb": { argTypes: [STRING, BOOL, null, null], result: HTTP2SESSION_T }, - /** session.request(pairs?, endStream) — endStream is a tri-state f64: - * -1 the method's payload-meaningless default, 0/1 explicit. */ - "http2.sessionRequest": { - argTypes: [HTTP2SESSION_T, arrayOf(STRING), F64], - result: HTTP2STREAM_T, - }, - "http2.sessionClose": { argTypes: [HTTP2SESSION_T], result: VOID }, - "http2.sessionCloseCb": { argTypes: [HTTP2SESSION_T, null], result: VOID }, - "http2.sessionDestroy": { argTypes: [HTTP2SESSION_T], result: VOID }, - "http2.sessionOnClose": { argTypes: [HTTP2SESSION_T, null, BOOL], result: VOID }, - "http2.sessionOnError": { argTypes: [HTTP2SESSION_T, null, BOOL], result: VOID }, - "http2.sessionOnConnect": { argTypes: [HTTP2SESSION_T, null, BOOL], result: VOID }, - "http2.sessionOnStream": { argTypes: [HTTP2SESSION_T, null, BOOL], result: VOID }, - "http2.sessionOnGoaway": { argTypes: [HTTP2SESSION_T, null, BOOL], result: VOID }, - "http2.sessionClosed": { argTypes: [HTTP2SESSION_T], result: BOOL }, - "http2.sessionDestroyed": { argTypes: [HTTP2SESSION_T], result: BOOL }, - "http2.sessionEncrypted": { argTypes: [HTTP2SESSION_T], result: BOOL }, - "http2.sessionType": { argTypes: [HTTP2SESSION_T], result: F64 }, - "http2.sessionAlpn": { argTypes: [HTTP2SESSION_T], result: STRING }, - "http2.sessionSocket": { argTypes: [HTTP2SESSION_T], result: NETSOCKET_T }, - /** stream.respond(pairs?, endStream) — the server answer. */ - "http2.streamRespond": { argTypes: [HTTP2STREAM_T, arrayOf(STRING), BOOL], result: VOID }, - "http2.streamWrite": { argTypes: [HTTP2STREAM_T, STRING], result: VOID }, - "http2.streamWriteBytes": { argTypes: [HTTP2STREAM_T, BYTES_U8], result: VOID }, - "http2.streamEnd": { argTypes: [HTTP2STREAM_T], result: VOID }, - "http2.streamEndStr": { argTypes: [HTTP2STREAM_T, STRING], result: VOID }, - "http2.streamEndBytes": { argTypes: [HTTP2STREAM_T, BYTES_U8], result: VOID }, - "http2.streamClose": { argTypes: [HTTP2STREAM_T, F64], result: VOID }, - "http2.streamCloseCb": { argTypes: [HTTP2STREAM_T, F64, null], result: VOID }, - "http2.streamDestroy": { argTypes: [HTTP2STREAM_T], result: VOID }, - "http2.sessionSettings0": { argTypes: [HTTP2SESSION_T], result: VOID }, - "http2.sessionSettings": { argTypes: [HTTP2SESSION_T, DYN], result: VOID }, - "http2.sessionSettingsDynCb": { argTypes: [HTTP2SESSION_T, DYN, DYN], result: VOID }, - "http2.sessionSettingsCb0": { - argTypes: [HTTP2SESSION_T, DYN, { kind: "func", params: [], ret: VOID }], - result: VOID, - }, - "http2.sessionOnSettingsDyn": { argTypes: [HTTP2SESSION_T, DYN, BOOL, BOOL], result: VOID }, - "http2.sessionOnSettings0": { - argTypes: [HTTP2SESSION_T, { kind: "func", params: [], ret: VOID }, BOOL, BOOL], - result: VOID, - }, - "http2.sessionSettingsGet": { argTypes: [HTTP2SESSION_T, BOOL], result: DYN }, - "http2.sessionPendingSettingsAck": { argTypes: [HTTP2SESSION_T], result: BOOL }, - "http2.getDefaultSettings": { argTypes: [], result: DYN }, - "http2.streamSetEncoding": { argTypes: [HTTP2STREAM_T, STRING], result: VOID }, - "http2.streamSetEncodingRet": { argTypes: [HTTP2STREAM_T, STRING], result: HTTP2STREAM_T }, - "http2.streamResume": { argTypes: [HTTP2STREAM_T], result: VOID }, - "http2.streamPause": { argTypes: [HTTP2STREAM_T], result: VOID }, - "http2.streamOnData": { argTypes: [HTTP2STREAM_T, null, BOOL], result: VOID }, - "http2.streamOnEnd": { argTypes: [HTTP2STREAM_T, null, BOOL], result: VOID }, - "http2.streamOnClose": { argTypes: [HTTP2STREAM_T, null, BOOL], result: VOID }, - "http2.streamOnAborted": { argTypes: [HTTP2STREAM_T, null, BOOL], result: VOID }, - "http2.streamOnError": { argTypes: [HTTP2STREAM_T, null, BOOL], result: VOID }, - "http2.streamOnResponse": { argTypes: [HTTP2STREAM_T, null, BOOL], result: VOID }, - "http2.streamId": { argTypes: [HTTP2STREAM_T], result: F64 }, - "http2.streamRstCode": { argTypes: [HTTP2STREAM_T], result: F64 }, - "http2.streamDestroyed": { argTypes: [HTTP2STREAM_T], result: BOOL }, - "http2.streamClosed": { argTypes: [HTTP2STREAM_T], result: BOOL }, - "http2.streamAborted": { argTypes: [HTTP2STREAM_T], result: BOOL }, - "http2.streamPending": { argTypes: [HTTP2STREAM_T], result: BOOL }, - "http2.streamSession": { argTypes: [HTTP2STREAM_T], result: HTTP2SESSION_T }, - // The createConnection forms: arg 0 is the dialer closure (() => - // Socket — the libCall case checks), then path/method/timeout/headers/ - // autoEnd like http.request. - /** The createConnection form (the proxy's own dialer): args are - * (connCb, path, method, timeout, headers, autoEnd[, cb]) — connCb is - * a `() => net.Socket` closure the runtime invokes ONCE, synchronously - * (Node's onSocket timing); everything else matches http.request. The - * Host header defaults to "localhost" — a headers.host entry wins - * verbatim, the proxy shape. */ - "http.requestConn": { - argTypes: [null, STRING, STRING, F64, arrayOf(STRING), BOOL], - result: HTTPCLIENTREQ_T, - }, - "http.requestConnCb": { - argTypes: [null, STRING, STRING, F64, arrayOf(STRING), BOOL, null], - result: HTTPCLIENTREQ_T, - }, - "https.request": { - argTypes: [STRING, F64, STRING, STRING, F64, arrayOf(STRING), BOOL, BOOL, null], - result: HTTPCLIENTREQ_T, - }, - "https.requestCb": { - argTypes: [STRING, F64, STRING, STRING, F64, arrayOf(STRING), BOOL, BOOL, null, null], - result: HTTPCLIENTREQ_T, - }, - /** The URL-string first argument, the http.requestUrl row over TLS: - * no options means Node's defaults (verification on, default trust - * anchors), and a non-https scheme is ERR_INVALID_PROTOCOL. */ - "https.requestUrl": { argTypes: [STRING, STRING, BOOL], result: HTTPCLIENTREQ_T }, - "https.requestUrlCb": { argTypes: [STRING, STRING, BOOL, null], result: HTTPCLIENTREQ_T }, - "https.requestAgent": { - argTypes: [STRING, F64, STRING, STRING, F64, arrayOf(STRING), BOOL, BOOL, null, DYN], - result: HTTPCLIENTREQ_T, - }, - "https.requestAgentCb": { - argTypes: [STRING, F64, STRING, STRING, F64, arrayOf(STRING), BOOL, BOOL, null, DYN, null], - result: HTTPCLIENTREQ_T, - }, - // The requestFn binding's runtime-secure rows: https.request's shape - // with the leading `secure` bool. - /** A call through a `const requestFn = tls ? https.request : - * http.request` binding — the module-function-as-value ternary between - * the two known clients: the https.request row with a leading `secure` - * bool that picks the dial at RUNTIME (true = the TLS client, exactly - * https.request; false = the plain client, exactly http.request — - * rejectUnauthorized/ca ignored there like Node ignores TLS options on - * http.request). The "https." prefix keeps the TLS unit linked. */ - "https.requestFn": { - argTypes: [BOOL, STRING, F64, STRING, STRING, F64, arrayOf(STRING), BOOL, BOOL, null], - result: HTTPCLIENTREQ_T, - }, - "https.requestFnCb": { - argTypes: [BOOL, STRING, F64, STRING, STRING, F64, arrayOf(STRING), BOOL, BOOL, null, null], - result: HTTPCLIENTREQ_T, - }, - "http.clientWrite": { argTypes: [HTTPCLIENTREQ_T, STRING], result: VOID }, - "http.clientSetHeader": { argTypes: [HTTPCLIENTREQ_T, STRING, STRING], result: VOID }, - "http.clientGetHeader": { argTypes: [HTTPCLIENTREQ_T, STRING], result: VOID }, - "http.clientHasHeader": { argTypes: [HTTPCLIENTREQ_T, STRING], result: BOOL }, - "http.clientRemoveHeader": { argTypes: [HTTPCLIENTREQ_T, STRING], result: VOID }, - "http.clientGetHeaderNames": { argTypes: [HTTPCLIENTREQ_T], result: arrayOf(STRING) }, - "http.clientGetRawHeaderNames": { argTypes: [HTTPCLIENTREQ_T], result: arrayOf(STRING) }, - "http.clientGetHeaders": { argTypes: [HTTPCLIENTREQ_T], result: DYN }, - "http.clientWriteBytes": { argTypes: [HTTPCLIENTREQ_T, BYTES_U8], result: VOID }, - "http.clientEnd": { argTypes: [HTTPCLIENTREQ_T], result: VOID }, - "http.clientEndStr": { argTypes: [HTTPCLIENTREQ_T, STRING], result: VOID }, - "http.clientEndBytes": { argTypes: [HTTPCLIENTREQ_T, BYTES_U8], result: VOID }, - /** The checked-dynamic chunk twins (the net.sockWriteDyn story). */ - "http.clientWriteDyn": { argTypes: [HTTPCLIENTREQ_T, DYN], result: VOID }, - "http.clientEndDyn": { argTypes: [HTTPCLIENTREQ_T, DYN], result: VOID }, - "http.clientFlushHeaders": { argTypes: [HTTPCLIENTREQ_T], result: VOID }, - "http.clientAddTrailers": { argTypes: [HTTPCLIENTREQ_T, arrayOf(STRING)], result: VOID }, - "http.clientCork": { argTypes: [HTTPCLIENTREQ_T], result: VOID }, - "http.clientUncork": { argTypes: [HTTPCLIENTREQ_T], result: VOID }, - "http.clientWritableCorked": { argTypes: [HTTPCLIENTREQ_T], result: F64 }, - "http.clientMethod": { argTypes: [HTTPCLIENTREQ_T], result: STRING }, - "http.clientPath": { argTypes: [HTTPCLIENTREQ_T], result: STRING }, - "http.clientHost": { argTypes: [HTTPCLIENTREQ_T], result: STRING }, - "http.clientProtocol": { argTypes: [HTTPCLIENTREQ_T], result: STRING }, - "http.clientHeadersSent": { argTypes: [HTTPCLIENTREQ_T], result: BOOL }, - "http.clientWritableEnded": { argTypes: [HTTPCLIENTREQ_T], result: BOOL }, - "http.clientWritableFinished": { argTypes: [HTTPCLIENTREQ_T], result: BOOL }, - "http.clientSocket": { argTypes: [HTTPCLIENTREQ_T], result: NETSOCKET_T }, - "http.clientReusedSocket": { argTypes: [HTTPCLIENTREQ_T], result: BOOL }, - "http.clientSetNoDelay": { argTypes: [HTTPCLIENTREQ_T, BOOL], result: VOID }, - "http.clientSetSocketKeepAlive": { argTypes: [HTTPCLIENTREQ_T, BOOL, F64], result: VOID }, - "http.clientSetTimeout": { argTypes: [HTTPCLIENTREQ_T, F64], result: VOID }, - "http.clientSetTimeoutCb": { - argTypes: [HTTPCLIENTREQ_T, F64, { kind: "func", params: [], ret: VOID }], - result: VOID, - }, - "http.statusCodes": { argTypes: [], result: DYN }, - "http.methods": { argTypes: [], result: arrayOf(STRING) }, - "http.reqSetTimeout": { argTypes: [HTTPREQ_T, F64], result: VOID }, - "http.reqSetTimeoutCb": { - argTypes: [HTTPREQ_T, F64, { kind: "func", params: [], ret: VOID }], - result: VOID, - }, - "http.clientDestroy": { argTypes: [HTTPCLIENTREQ_T], result: VOID }, - "http.clientAbort": { argTypes: [HTTPCLIENTREQ_T], result: VOID }, - "http.clientAborted": { argTypes: [HTTPCLIENTREQ_T], result: BOOL }, - "http.clientDestroyed": { argTypes: [HTTPCLIENTREQ_T], result: BOOL }, - "http.clientOnResponse": { argTypes: [HTTPCLIENTREQ_T, null, BOOL], result: VOID }, - "http.clientOnSocket": { argTypes: [HTTPCLIENTREQ_T, null, BOOL], result: VOID }, - "http.clientOnFinish": { - argTypes: [HTTPCLIENTREQ_T, { kind: "func", params: [], ret: VOID }, BOOL], - result: VOID, - }, - "http.clientOnError": { argTypes: [HTTPCLIENTREQ_T, null, BOOL], result: VOID }, - "http.clientOnTimeout": { - argTypes: [HTTPCLIENTREQ_T, { kind: "func", params: [], ret: VOID }, BOOL], - result: VOID, - }, - "http.clientOnClose": { - argTypes: [HTTPCLIENTREQ_T, { kind: "func", params: [], ret: VOID }, BOOL], - result: VOID, - }, - "http.clientOnAbort": { - argTypes: [HTTPCLIENTREQ_T, { kind: "func", params: [], ret: VOID }, BOOL], - result: VOID, - }, - "http.clientOnContinue": { - argTypes: [HTTPCLIENTREQ_T, { kind: "func", params: [], ret: VOID }, BOOL], - result: VOID, - }, - /** execFileSync/execSync as ONE entry (args: cmd, argv, shell, input, - * cwd, hasEnv, envPairs, timeoutMs, stdoutMode, stderrMode, buffers — - * see scr_child.h). Throws Node's exact errors: "Command failed: " - * (+ captured stderr) on non-zero exit or signal death, "spawnSync - * ENOENT" on spawn failure, "spawnSync ETIMEDOUT" after - * the SIGTERM timeout, with Node's status, signal, output, pid, stdout - * and stderr properties (Buffer outputs when buffers is set: no - * encoding option). Result is the captured utf8 stdout (+1). */ - "cp.execSync": { - argTypes: [ - STRING, - arrayOf(STRING), - BOOL, - STRING, - BOOL, - STRING, - BOOL, - arrayOf(STRING), - F64, - F64, - F64, - BOOL, - ], - result: STRING, - }, - /** The promisified-execFile/exec capture (args: cmd, argv, shell, cwd, - * hasEnv, envPairs, timeoutMs; shell marks exec's /bin/sh -c argv, as - * for execSync, so failures display the command string): the same exec - * core in the async shape — both streams captured, no echo, Node's ASYNC messages on the throw paths - * ("Command failed: \n" with the unconditional newline, - * "spawn ENOENT" with .code, timeouts reporting as ordinary - * SIGTERM command failures — never ETIMEDOUT). Result reuses the - * ScrSpawnRes container (+1; stdout/stderr strings, status unused). - * Called only from the frontend's interned %execFileAsync and - * %execAsync ASYNC helpers, whose fiber turns the throw into the - * rejection. */ - "cp.execCapture": { - argTypes: [STRING, arrayOf(STRING), BOOL, STRING, BOOL, arrayOf(STRING), F64], - result: SPAWNRES_T, - }, - /** child_process.spawn (scr_child.c + the scr_async.c loop): posix_spawnp - * with per-slot ignore/inherit/pipe modes (and number output fds), the - * all-piped Node default, and the child registered with the event loop, - * which polls waitpid(WNOHANG) at quiescence like timers. - * NEVER throws: spawn failure defers to the "error" event, Node-exact - * (the error message is Node's "spawn "; an "error" - * event with no listener prints it and exits 1 like an EventEmitter). - * cmd/args borrowed; result an owned (+1) child handle. The loop will - * not exhaust while any spawned child is unreaped — Node's keep-alive. - * - * child.onExit / child.onClose / child.onError — terminal listener - * registration through child.on/once. Close uses the exit adapter shape - * but fires only after every piped stdio handle reaches EOF. The - * receiver is borrowed, the CALLBACK MOVES into the child's listener - * registry (released after the terminal event fires, or at reap for - * the event that never fires). Both are void (chaining is fenced). - * onExit's third emitted ingredient is an ADAPTER the backend interns - * per callback shape: the runtime invokes adapter(cb, has_code, code) - * and the adapter builds the `number | null` union (tags are program- - * dependent) or ignores the code for a zero-param callback. onError's - * adapters are runtime-provided (zero-param, or the %Error one-param - * shape — scr_error_new needs no program types). "exit" fires once - * with the code (f64 arm) or null (signal death); "error" fires only - * for spawn failure, exactly Node's split. */ - "cp.spawn": { argTypes: [STRING, arrayOf(STRING)], result: CHILD_T }, - /** Static-native fork startup and IPC. process.forkTarget initializes a - * re-executed child's inherited channel and returns its embedded target id - * (-1 in the parent). The remaining entries mirror Node's JSON channel. */ - "process.forkTarget": { argTypes: [F64], result: F64 }, - "cp.fork": { - argTypes: [F64, arrayOf(STRING), F64, F64, F64, BOOL, arrayOf(STRING), STRING], - result: CHILD_T, - }, - "child.connected": { argTypes: [CHILD_T], result: BOOL }, - "child.send": { argTypes: [CHILD_T, STRING], result: BOOL }, - "child.sendCb": { argTypes: [CHILD_T, STRING, null], result: BOOL }, - "child.disconnect": { argTypes: [CHILD_T], result: VOID }, - "child.onMessage": { argTypes: [CHILD_T, null, BOOL], result: VOID }, - "child.onDisconnect": { argTypes: [CHILD_T, null, BOOL], result: VOID }, - "child.onSpawn": { argTypes: [CHILD_T, null], result: VOID }, - /** Rest listeners use the checked function bridge to pack event arguments. */ - "child.onDyn": { argTypes: [CHILD_T, STRING, DYN], result: VOID }, - // Own properties JavaScript adds to children (handle-properties.ts). - "child.propsInstall": { argTypes: [], result: VOID }, - "child.propsDefine": { argTypes: [DYN, DYN, DYN], result: DYN }, - "child.propsDescriptor": { argTypes: [DYN, DYN], result: DYN }, - "child.propsResolve": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, - "child.propsOnDyn": { argTypes: [CHILD_T, STRING, DYN], result: VOID }, - "process.connected": { argTypes: [], result: BOOL }, - /** Whether this fork child opened its channel; part of process.send's - * presence in programs with fork targets. Never throws. */ - "process.ipcOpen": { argTypes: [], result: BOOL }, - "process.send": { argTypes: [STRING], result: BOOL }, - "process.sendCb": { argTypes: [STRING, null], result: BOOL }, - "process.disconnect": { argTypes: [], result: VOID }, - "process.onMessage": { argTypes: [null, BOOL], result: VOID }, - "process.onDisconnect": { argTypes: [null, BOOL], result: VOID }, - /** child_process.execFile's callback slice: starts an all-piped child, - * captures stdout/stderr, and moves the error-first callback into the - * child registry. The callback shape is program-dependent and checked - * by the validator/backends. The last argument is exec's shell flag: - * the argv is ["-c", command] and failure messages show the command. */ - "cp.execFile": { argTypes: [STRING, arrayOf(STRING), null, BOOL], result: CHILD_T }, - // spawn's options form: per-slot stdio modes (0 ignore / 1 inherit / - // 2 fd) with the out/err fds for mode 2, detached, env replacement - // pairs, cwd ("" = inherit). - /** spawn with options (scr_child.c): the cp.spawn core plus PER-SLOT - * stdio — args are (cmd, argv, inMode, outMode, errMode, outFd, errFd, - * detached, hasEnv, envPairs, cwd); modes 0 ignore (/dev/null), 1 - * inherit, 2 fd (out/err only — the fd dup2s into the child's slot, - * Node's stdio fd form; the daemon-log idiom ["ignore", logFd, logFd]). - * detached is POSIX_SPAWN_SETSID (the child gets its own session and - * process group, Node's semantics), env is a REPLACEMENT ([k,v,...] - * pairs like cp.execSync's), cwd ""=inherit. Same event/loop story as - * cp.spawn. */ - "cp.spawnOpts": { - argTypes: [ - STRING, - arrayOf(STRING), - F64, - F64, - F64, - F64, - F64, - BOOL, - BOOL, - BOOL, - arrayOf(STRING), - STRING, - ], - result: CHILD_T, - }, - /** Runtime options normalized into the native spawn core; unsupported - * process features retain explicit refusals. (command, args, options). */ - "cp.spawnDynamic": { argTypes: [STRING, arrayOf(STRING), DYN], result: CHILD_T }, - // The callback's func type is program-dependent (zero params, or the - // `number | null` union / the %Error class) — the libCall case checks - // the shape; the slot here only pins arity and the child receiver. - "child.onExit": { argTypes: [CHILD_T, null], result: VOID }, - "child.onClose": { argTypes: [CHILD_T, null], result: VOID }, - "child.onError": { argTypes: [CHILD_T, null], result: VOID }, - // Like process.envGet, spawnRes.status's result type is program-dependent - // (the interned `number | null` union) — the libCall case checks the arms. - "spawnRes.status": { argTypes: [SPAWNRES_T], result: VOID }, - // spawnRes.error's result is the interned `Error | undefined` union — - // the libCall case checks the arms, the spawnRes.status pattern. - /** Node's spawn-failure carrier `error?: Error`: a fresh +1 %Error - * ("spawnSync ENOENT", `code` stamped) when the spawn itself - * failed, the interned undefined arm otherwise — the result type is the - * call site's `Error | undefined` union, constructed type-directedly in - * the backend (the envGet convention). Never throws. */ - "spawnRes.error": { argTypes: [SPAWNRES_T], result: VOID }, - // spawnRes.signal's result is the interned `string | null` union — - // same pattern. - "spawnRes.signal": { argTypes: [SPAWNRES_T], result: VOID }, - // The ChildProcess lifecycle members: pid/exitCode are program-dependent - // unions (`number | undefined` / `number | null` — the libCall case - // checks the arms, the spawnRes.status pattern). - /** The ChildProcess lifecycle members (scr_child.c), Node's shapes - * exactly (SEMANTICS.md has the pinned matrix). child.pid is the - * checker's `number | undefined` (undefined = spawn failure) and - * child.exitCode its `number | null` (null while running and after a - * signal death; -errno once a spawn failure settled) — both unions are - * type-directed constructions in the backend over a has/get runtime - * pair, the spawnRes.status pattern. child.killed is Node's - * sent-a-signal flag. child.kill sends while the child is un-reaped - * (false after — Node's null-handle answer) and THROWS the - * Unknown-signal TypeError on bad names (may-throw); killNum passes - * numbers through (0 probes; never throws). child.unref drops the - * child from the loop's keep-alive set — it is still reaped while the - * loop runs for other reasons, and one the loop never reaps is left to - * the OS at exit. All receivers borrowed. */ - "child.pid": { argTypes: [CHILD_T], result: VOID }, - "child.exitCode": { argTypes: [CHILD_T], result: VOID }, - // The close-override pair: the bound close's result func type and the - // override's func argument are program-dependent (the callback union's - // tags) — the slots pin the server receiver; the libCall case checks - // the bound value's shape. - /** The close-override pair (the portless close-proxy idiom). - * serverCloseBind is `wrapper.close.bind(wrapper)` as a VALUE: a - * compiler-emitted closure over the server whose invocation runs the - * REAL close (scr_net_server_close_direct — never the override), so - * the override body's `origClose(cb)` cannot recurse; its callback - * argument (the `((err?: Error) => void) | undefined` union) registers - * as a once-'close' listener, a one-param callback wrapped by an - * emitted zero-arg trampoline firing the undefined arm (a clean close - * carries no error). serverSetCloseOverride is `wrapper.close = fn`: - * the override MOVES in behind an emitted zero-arg wrapper (it invokes - * the user function with the undefined-arm callback — tags are program - * data), and server.close() consults it before closing. */ - "net.serverCloseBind": { argTypes: [NETSERVER_T], result: VOID }, - "net.serverSetCloseOverride": { argTypes: [NETSERVER_T, null], result: VOID }, - // The piped-output stream reads: `Readable | null` unions (the libCall - // case checks the arms — the child.pid pattern with a ref arm). - /** The piped-output streams (stdio mode 3 — scr_child.c's stream - * slice). child.stdout/child.stderr answer the checker's - * `Readable | null` union (type-directed construction in the backend - * over the +1-or-NULL runtime pair — the child.pid pattern with a ref - * arm). stream.onData/onEnd register 'data'/'end' listeners (receiver - * borrowed, CALLBACK MOVES, trailing once-flag, void — chaining - * fenced): 'data' fires one Buffer chunk per read (zero-param and - * Buffer/string adapters are runtime-provided; setEncoding threads - * split multibyte sequences through the shared StringDecoder core. A - * union-param listener — - * ngrok's `Buffer | string` — gets a compiler-emitted adapter wrapping - * the chunk at its Buffer arm), 'end' fires once at EOF, always BEFORE - * the child's 'exit' (the pinned ordering). A flowing stream keeps the - * loop alive: usesTimers. */ - "child.stdout": { argTypes: [CHILD_T], result: VOID }, - "child.stderr": { argTypes: [CHILD_T], result: VOID }, - /** The piped-input writer (stdio mode 3 on fd 0). child.stdin answers - * `Writable | null`; writes copy borrowed data into the nonblocking - * queue, end/destroy settle it, writable is a pure state read, and - * listener callbacks move into the writer registry. */ - "child.stdin": { argTypes: [CHILD_T], result: VOID }, - /** Under the general stream representation (general-streams.ts), a - * stdio read (slot 0, 1 or 2) answers the child's bridged runtime stream - * — `Writable | null` for stdin, `Readable | null` otherwise; the bridge - * throws when the program lacks the stream unit's boxing helpers. */ - "child.stdioStream": { argTypes: [CHILD_T, F64], result: VOID }, - // Listener registrations: callback func shapes are program-dependent - // (zero-param / Buffer / string / unknown / Buffer-armed union) — the slots pin arity, - // the stream receiver, and the once-flag. - "stream.onData": { argTypes: [CHILDSTREAM_T, null, BOOL], result: VOID }, - "stream.onDataStr": { argTypes: [CHILDSTREAM_T, null, BOOL], result: VOID }, - "stream.onEnd": { argTypes: [CHILDSTREAM_T, null, BOOL], result: VOID }, - "stream.childSetEncoding": { argTypes: [CHILDSTREAM_T, STRING], result: CHILDSTREAM_T }, - "writer.writeString": { argTypes: [CHILDWRITER_T, STRING], result: BOOL }, - "writer.writeBytes": { argTypes: [CHILDWRITER_T, BYTES_U8], result: BOOL }, - "writer.end": { argTypes: [CHILDWRITER_T], result: VOID }, - "writer.destroy": { argTypes: [CHILDWRITER_T], result: VOID }, - "writer.writable": { argTypes: [CHILDWRITER_T], result: BOOL }, - "writer.onDrain": { argTypes: [CHILDWRITER_T, null, BOOL], result: VOID }, - "writer.onFinish": { argTypes: [CHILDWRITER_T, null, BOOL], result: VOID }, - "writer.onError": { argTypes: [CHILDWRITER_T, null, BOOL], result: VOID }, - /** The first-class WritableStream write (`output.write(line)` — the - * prefixStream idiom): the receiver IS the fd scalar (process.stdout/ - * stderr reads mint 1/2), dispatched onto the exact stdoutWrite/ - * stderrWrite paths so buffering and ordering stay identical. Data - * borrowed; the bool is Node's always-true backpressure signal. */ - "procStream.write": { argTypes: [PROCSTREAM_T, STRING], result: BOOL }, - /** process.stdout/stderr flowing into a runtime Writable slot under the - * general stream representation: an fd-backed Writable (+1) whose - * writes take process.stdout.write's synchronous path. */ - /** A pipeline() stage that is a checked-dynamic value: a boxed native - * stream unwraps by identity (+1); anything else throws the given - * not-supported message. Typed as a Duplex so every stage position - * accepts it; the runtime pipeline checks the stream's real sides. */ - "stream.unboxDyn": { - argTypes: [DYN, STRING], - result: { kind: "object", className: "%Duplex" }, - }, - /** The Transform a Cipheriv or Decipheriv reads and writes as a stream - * (+1, created on first use and owned by the cipher). */ - "stream.handleStream": { - argTypes: [DYN], - result: { kind: "object", className: "%Transform" }, - }, - /** Checked-dynamic stream slots (dyn-streams.ts): the native stream a - * checked-dynamic value boxes (+1) — a runtime stream with the readable - * (1) or writable (2) side — or null for any other value. Never throws; - * the result union is `%Readable | null` or `%Writable | null`. */ - "stream.dynNative": { argTypes: [DYN, F64], result: VOID }, - /** stream.dynNative for a pipe or unpipe destination: also a - * constructor-function stream's runtime stream, in programs that link - * that unit, unless it overrides write() or end(). */ - "stream.dynNativePipe": { argTypes: [DYN, F64], result: VOID }, - /** readable.pipe(destination, end) into a checked-dynamic destination - * that is not a native stream: [source, the source boxed, destination, - * end]. Node's pipe listener protocol over the destination's members - * (scr_stream_dyn.c); throws what those members throw. */ - "stream.pipeDyn": { argTypes: [null, DYN, DYN, BOOL], result: VOID }, - /** readable.unpipe(destination) for a checked-dynamic destination that - * is not a native stream: [source, destination]. Unpipes the pipe - * stream.pipeDyn registered into it, if any (scr_stream_dyn.c). */ - "stream.unpipeDyn": { argTypes: [null, DYN], result: VOID }, - "stream.stdioWritable": { - argTypes: [PROCSTREAM_T], - result: { kind: "object", className: "%Writable" }, - }, - "child.killed": { argTypes: [CHILD_T], result: BOOL }, - "child.kill": { argTypes: [CHILD_T, STRING], result: BOOL }, - "child.killNum": { argTypes: [CHILD_T, F64], result: BOOL }, - "child.unref": { argTypes: [CHILD_T], result: VOID }, - "child.ref": { argTypes: [CHILD_T], result: VOID }, - "spawnRes.stdout": { argTypes: [SPAWNRES_T], result: STRING }, - "spawnRes.stderr": { argTypes: [SPAWNRES_T], result: STRING }, - /** fs/promises (scr_lib.c over scr_async.c's settled minting): the SAME - * sync syscalls, wrapped in an ALREADY-SETTLED promise — failure - * REJECTS (catchable at the await) instead of throwing, so none of - * these are in the may-throw seed. readFile is utf8-fenced like - * readFileSync. The non-interleaving divergence is documented in - * SEMANTICS.md. */ - /** node:crypto randomness: randomUUID plus randomBytes as either a real - * Buffer or the fused randomBytes(n).toString("hex"|"base64") path. - * The size-taking forms throw Node's RangeError on invalid values. */ - "crypto.randomUUID": { argTypes: [], result: STRING }, - "crypto.randomBytesToString": { argTypes: [F64, STRING], result: STRING }, - /** crypto.randomBytes(n) → a real u8 Buffer (+1). THROWS Node's - * RangeError on out-of-range sizes, exactly like the composed - * randomBytesToString (which keeps its one-libCall lowering — the two - * coexist: the composed form never materializes the Buffer). */ - "crypto.randomBytes": { argTypes: [F64], result: BYTES_U8 }, - /** The fused createHash(alg).update(data).digest(enc) fast path and the - * one-shot crypto.hash implementation. First-class Hash/Hmac handles use - * the entries below; all paths share the incremental runtime core. */ - "crypto.hashDigestStr": { argTypes: [STRING, STRING, STRING], result: STRING }, - "crypto.hashDigestBytes": { argTypes: [STRING, BYTES_U8, STRING], result: STRING }, - /** First-class static Hash/Hmac handles. Constructors validate the - * runtime algorithm string (every digest Node 24 lists); update returns the same - * handle by retained identity, copy snapshots Hash state, and digest - * finalizes the handle and returns either a Buffer or encoded string. */ - "crypto.native": { argTypes: [], result: DYN }, - "crypto.hashNew": { argTypes: [STRING], result: CRYPTOHASH_T }, - /** The Transform a Hash or Hmac reads and writes as a stream (+1, - * created on first use and owned by the value; scr_stream.c). The Piped - * form, for pipe destinations and pipeline stages, also keeps the value - * alive until the stream flushes or is destroyed. */ - "crypto.hashStream": { argTypes: [null], result: { kind: "object", className: "%Transform" } }, - "crypto.hashStreamPiped": { - argTypes: [null], - result: { kind: "object", className: "%Transform" }, - }, - /** createCipheriv (false) / createDecipheriv (true) over checked-dynamic - * cipher, key, iv and options arguments (scr_cipher.c): a native - * Cipheriv/Decipheriv handle whose methods dispatch through dynInvoke. */ - "crypto.cipherNew": { argTypes: [BOOL, DYN, DYN, DYN, DYN], result: DYN }, - /** createECDH(curveName) over a checked-dynamic curve (scr_ecdh.c): a - * native ECDH handle whose methods dispatch through dynInvoke. */ - "crypto.ecdhNew": { argTypes: [DYN], result: DYN }, - /** ECDH.convertKey(key, curve, inputEncoding, outputEncoding, format): - * the converted point as a Buffer or string. */ - "crypto.ecdhConvertKey": { argTypes: [DYN, DYN, DYN, DYN, DYN], result: DYN }, - /** createDiffieHellman(sizeOrKey, keyEncoding, generator, - * generatorEncoding) over checked-dynamic arguments (scr_dh.c): a native - * DiffieHellman handle whose methods dispatch through dynInvoke. */ - "crypto.dhNew": { argTypes: [DYN, DYN, DYN, DYN], result: DYN }, - /** getDiffieHellman(groupName) and createDiffieHellmanGroup(name): a - * native DiffieHellmanGroup handle. */ - "crypto.dhGroupNew": { argTypes: [DYN], result: DYN }, - /** checkPrimeSync(candidate, options) and generatePrimeSync(size, - * options) over checked-dynamic arguments (scr_dh.c): a boolean, and an - * ArrayBuffer or bigint, as checked-dynamic results. The Cb forms take - * the callback and whether its error parameter takes null for Node's - * undefined, and call it in a later macrotask; Node's argument errors - * throw (may-throw). */ - "crypto.checkPrimeSync": { argTypes: [DYN, DYN], result: DYN }, - /** getCipherInfo(nameOrNid, options) over checked-dynamic arguments - * (scr_cipher_info.c): the info object or undefined. */ - "crypto.getCipherInfo": { argTypes: [DYN, DYN], result: DYN }, - "crypto.checkPrimeCb": { argTypes: [DYN, DYN, DYN, BOOL], result: VOID }, - "crypto.generatePrimeSync": { argTypes: [DYN, DYN], result: DYN }, - "crypto.generatePrimeCb": { argTypes: [DYN, DYN, DYN, BOOL], result: VOID }, - "crypto.hmacNewStr": { argTypes: [STRING, STRING], result: CRYPTOHMAC_T }, - "crypto.hmacNewBytes": { argTypes: [STRING, BYTES_U8], result: CRYPTOHMAC_T }, - "crypto.hashUpdateStr": { argTypes: [CRYPTOHASH_T, STRING], result: CRYPTOHASH_T }, - "crypto.hashUpdateBytes": { argTypes: [CRYPTOHASH_T, BYTES_U8], result: CRYPTOHASH_T }, - "crypto.hmacUpdateStr": { argTypes: [CRYPTOHMAC_T, STRING], result: CRYPTOHMAC_T }, - "crypto.hmacUpdateBytes": { argTypes: [CRYPTOHMAC_T, BYTES_U8], result: CRYPTOHMAC_T }, - "crypto.hashCopy": { argTypes: [CRYPTOHASH_T], result: CRYPTOHASH_T }, - "crypto.hashDigestString": { argTypes: [CRYPTOHASH_T, STRING], result: STRING }, - "crypto.hashDigestBuffer": { argTypes: [CRYPTOHASH_T], result: BYTES_U8 }, - "crypto.hmacDigestString": { argTypes: [CRYPTOHMAC_T, STRING], result: STRING }, - "crypto.hmacDigestBuffer": { argTypes: [CRYPTOHMAC_T], result: BYTES_U8 }, - /** The option and any-value forms, with Node's checks at run time. The - * Dyn digests return a string or a Buffer, and crypto.hash's Dyn form - * either one by its outputEncoding. */ - "crypto.hashNewOpts": { argTypes: [STRING, DYN], result: CRYPTOHASH_T }, - "crypto.hmacNewDyn": { argTypes: [STRING, DYN, DYN], result: CRYPTOHMAC_T }, - "crypto.hashCopyOpts": { argTypes: [CRYPTOHASH_T, DYN], result: CRYPTOHASH_T }, - "crypto.hashUpdateEnc": { argTypes: [CRYPTOHASH_T, STRING, STRING], result: CRYPTOHASH_T }, - "crypto.hmacUpdateEnc": { argTypes: [CRYPTOHMAC_T, STRING, STRING], result: CRYPTOHMAC_T }, - "crypto.hashUpdateDyn": { argTypes: [CRYPTOHASH_T, DYN, DYN], result: CRYPTOHASH_T }, - "crypto.hmacUpdateDyn": { argTypes: [CRYPTOHMAC_T, DYN, DYN], result: CRYPTOHMAC_T }, - "crypto.hashDigestDyn": { argTypes: [CRYPTOHASH_T, DYN], result: DYN }, - "crypto.hmacDigestDyn": { argTypes: [CRYPTOHMAC_T, DYN], result: DYN }, - "crypto.hashOneshot": { argTypes: [STRING, DYN, DYN], result: DYN }, - /** Views of any element kind (the validator checks both are views). */ - "crypto.timingSafeEqual": { argTypes: [null, null], result: BOOL }, - "crypto.timingSafeEqualDyn": { argTypes: [DYN, DYN], result: BOOL }, - "crypto.randomFill": { argTypes: [BYTES_U8, F64, F64], result: BYTES_U8 }, - "crypto.randomFillRest": { argTypes: [BYTES_U8, F64], result: BYTES_U8 }, - "crypto.randomInt": { argTypes: [F64, F64], result: F64 }, - "crypto.randomIntMax": { argTypes: [F64], result: F64 }, - /** randomFillSync over any view returns that view at its own type (the - * validator checks the result against the first argument). The Dyn form - * takes ArrayBuffers and other values. The callback forms carry - * randomFill's argument count before the callback; randomIntCb carries - * whether min was given. */ - "crypto.randomFillView": { argTypes: [null, F64, F64], result: BYTES_U8 }, - "crypto.randomFillViewRest": { argTypes: [null, F64], result: BYTES_U8 }, - /** crypto.getRandomValues: a view returns itself at its own type (the - * float/size DOMExceptions are thrown at run time); any other value - * takes the checked form. */ - "crypto.getRandomValues": { argTypes: [null], result: BYTES_U8 }, - "crypto.getRandomValuesDyn": { argTypes: [DYN], result: DYN }, - "crypto.randomFillSyncDyn": { argTypes: [DYN, DYN, DYN], result: DYN }, - "crypto.randomFillCb": { argTypes: [null, F64, F64, F64, null], result: VOID }, - "crypto.randomFillCbDyn": { argTypes: [DYN, DYN, DYN, F64, null], result: VOID }, - "crypto.randomIntCb": { argTypes: [F64, F64, F64, null], result: VOID }, - "crypto.randomUUIDOpts": { argTypes: [DYN], result: STRING }, - /** Secret KeyObjects: native handles whose getters and methods dispatch - * through the handle. keyObjectInput reads a key's bytes for hkdf. */ - "crypto.createSecretKey": { argTypes: [DYN, DYN], result: DYN }, - "crypto.generateKeySync": { argTypes: [DYN, DYN], result: DYN }, - "crypto.generateKeyCb": { argTypes: [DYN, DYN, null], result: VOID }, - "crypto.keyObjectInput": { argTypes: [DYN], result: BYTES_U8 }, - "crypto.isKeyObject": { argTypes: [DYN], result: BOOL }, - /** WebCrypto (scr_webcrypto.c): each method takes its argument vector as - * one dyn array and answers a promise that argument and validation - * failures reject. CryptoKey values are native handles. */ - "webcrypto.generateKey": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, - "webcrypto.importKey": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, - "webcrypto.exportKey": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, - "webcrypto.encrypt": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, - "webcrypto.decrypt": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, - "webcrypto.wrapKey": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, - "webcrypto.unwrapKey": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, - "webcrypto.deriveBits": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, - "webcrypto.deriveKey": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, - "webcrypto.sign": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, - "webcrypto.verify": { argTypes: [DYN], result: { kind: "promise", inner: BOOL } }, - "webcrypto.digest": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, - "webcrypto.isCryptoKey": { argTypes: [DYN], result: BOOL }, - "webcrypto.object": { argTypes: [], result: DYN }, - "crypto.randomUUIDv7": { argTypes: [DYN], result: STRING }, - "crypto.pbkdf2": { argTypes: [BYTES_U8, BYTES_U8, F64, F64, STRING], result: BYTES_U8 }, - "crypto.randomBytesCb": { argTypes: [F64, null], result: VOID }, - "crypto.pbkdf2Cb": { argTypes: [BYTES_U8, BYTES_U8, F64, F64, STRING, null], result: VOID }, - "crypto.hkdf": { argTypes: [STRING, BYTES_U8, BYTES_U8, BYTES_U8, F64], result: DYN }, - "crypto.hkdfCb": { argTypes: [STRING, BYTES_U8, BYTES_U8, BYTES_U8, F64, null], result: VOID }, - "crypto.scrypt": { argTypes: [BYTES_U8, BYTES_U8, F64, DYN], result: BYTES_U8 }, - "crypto.scryptCb": { argTypes: [BYTES_U8, BYTES_U8, F64, DYN, null], result: VOID }, - /** util.promisify over the callback forms: the callback form's - * arguments without the callback → a promise the callback form settles, - * or rejected with an argument error; never throws. The result's value - * type is the declared one (a Buffer, the filled view, a number, or a - * checked-dynamic value); the validator checks it. */ - "crypto.randomBytesPromise": { argTypes: [F64], result: VOID }, - "crypto.pbkdf2Promise": { argTypes: [BYTES_U8, BYTES_U8, F64, F64, STRING], result: VOID }, - "crypto.scryptPromise": { argTypes: [BYTES_U8, BYTES_U8, F64, DYN], result: VOID }, - "crypto.hkdfPromise": { argTypes: [STRING, BYTES_U8, BYTES_U8, BYTES_U8, F64], result: VOID }, - "crypto.randomFillPromise": { argTypes: [null, F64, F64, F64], result: VOID }, - "crypto.randomFillPromiseDyn": { argTypes: [DYN, DYN, DYN, F64], result: VOID }, - "crypto.randomIntPromise": { argTypes: [F64, F64, F64], result: VOID }, - "crypto.generateKeyPromise": { argTypes: [DYN, DYN], result: VOID }, - /** util.promisify(dns.lookup) and util.promisify(dns.lookupService): - * [hostname, options] and [address, port] → a promise of the - * checked-dynamic result object (the address array with options.all), - * or rejected with the error; never throws. */ - "dns.lookupPromise": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, - "dns.lookupServicePromise": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, - // The Buffer statics and the fs/zlib Buffer forms: fixed always-u8 - // signatures (Buffer IS a Uint8Array — one bytes kind). - /** The Buffer statics with fixed (always-u8) signatures. fromStr is - * `Buffer.from(string, enc)` — the frontend completes an omitted - * encoding to "utf8" and fences non-literal/unsupported ones; hex and - * base64 decode Node-leniently, so it never throws. concat takes ONE - * bytes[] arg (the list) and returns a fresh copy. `Buffer.from(u8)` - * and `Buffer.alloc(n)` need no libFn — they lower to bytesNew. */ - "buffer.fromStr": { argTypes: [STRING, STRING], result: BYTES_U8 }, - "buffer.fromStrChecked": { argTypes: [STRING, STRING], result: BYTES_U8 }, - "buffer.brand": { argTypes: [BYTES_U8], result: BYTES_U8 }, - /** Buffer.isBuffer over a typed view: whether the view carries the - * Buffer brand (any element kind; only Buffers do). Never throws. */ - "buffer.isBranded": { argTypes: [null], result: BOOL }, - /** Buffer.from on checked-native strings, bytes, arrays and data-only - * array-like/Buffer-JSON objects. The encoding is a normalized literal; - * non-string inputs ignore it. Copies the input and may throw during - * element coercion or argument validation. Custom object valueOf and - * opaque reference inputs retain explicit runtime refusals. */ - "buffer.fromDyn": { argTypes: [DYN, STRING], result: BYTES_U8 }, - "buffer.concat": { argTypes: [arrayOf(BYTES_U8)], result: BYTES_U8 }, - "buffer.concatDyn": { argTypes: [DYN], result: BYTES_U8 }, - /** The pooled Buffer factories (Node's allocate/fromStringFast/ - * fromArrayLike): allocUnsafe, Buffer.from of strings, Uint8Arrays, - * number[] and checked values, and concat return views into the - * per-thread pool slab below Buffer.poolSize >>> 1 bytes. alloc and - * allocUnsafeSlow never pool. The size forms validate like Node - * (ERR_OUT_OF_RANGE). poolSize reads and writes Buffer.poolSize. */ - "buffer.alloc": { argTypes: [F64], result: BYTES_U8 }, - /** Buffer.alloc as a function value: the size, fill and encoding arrive - * unchecked, and the runtime fills as Node's alloc does. */ - "buffer.allocValue": { argTypes: [DYN, DYN, DYN], result: BYTES_U8 }, - "buffer.allocUnsafe": { argTypes: [F64], result: BYTES_U8 }, - "buffer.allocUnsafeSlow": { argTypes: [F64], result: BYTES_U8 }, - "buffer.poolFromStr": { argTypes: [STRING, STRING], result: BYTES_U8 }, - "buffer.poolFromStrChecked": { argTypes: [STRING, STRING], result: BYTES_U8 }, - "buffer.poolFromBytes": { argTypes: [BYTES_U8], result: BYTES_U8 }, - "buffer.poolFromArray": { argTypes: [arrayOf(F64)], result: BYTES_U8 }, - "buffer.poolFromDyn": { argTypes: [DYN, STRING], result: BYTES_U8 }, - /** poolFromDyn that also copies an array crossing by reference. */ - "buffer.poolFromDynRef": { argTypes: [DYN, STRING], result: BYTES_U8 }, - "buffer.poolFromDynOffset": { argTypes: [DYN, DYN], result: BYTES_U8 }, - "buffer.poolSize": { argTypes: [], result: F64 }, - "buffer.setPoolSize": { argTypes: [F64], result: VOID }, - /** Buffer.byteLength(string, enc) — enc a NORMALIZED literal like - * fromStr's — and Buffer.isEncoding(name) over a runtime string - * (case-insensitive against Node's alias set). Pure; never throw. */ - "buffer.byteLenDyn": { argTypes: [DYN, STRING], result: F64 }, - /** Buffer.byteLength as a function value: the encoding arrives at run time. */ - "buffer.byteLenValue": { argTypes: [DYN, DYN], result: F64 }, - "buffer.byteLenStr": { argTypes: [STRING, STRING], result: F64 }, - "buffer.isEncoding": { argTypes: [STRING], result: BOOL }, - /** Buffer.concat(list, totalLength): the concatenation truncated or - * zero-padded to the total. THROWS Node's 'length' RangeError on a - * negative/non-integer total (may-throw seed). */ - "buffer.concatLen": { argTypes: [arrayOf(BYTES_U8), F64], result: BYTES_U8 }, - "buffer.concatLenDyn": { argTypes: [DYN, F64], result: BYTES_U8 }, - // The checked-dynamic compare/equals validators (Node's argument - // ladders over dyn-boxed invalid-input probes). - /** The checked-dynamic compare/equals validators (scr_bytes_io.c) — - * the lowered form when an argument is NOT statically bytes (the - * invalid-input probes; a dyn from an untyped JS helper): Node's own - * argument ladders run at runtime — ERR_INVALID_ARG_TYPE with the - * API's argument name ("buf1"/"buf2", "otherBuffer", "target"; offsets - * "of type number"), validateOffset's ERR_OUT_OF_RANGE for bad - * numbers, undefined offsets taking their Node defaults — and a - * well-typed value still computes the real answer. All args borrowed - * dyn; compareChk's four offset slots pass the undefined dyn when - * syntactically absent. May-throw seeds. */ - "buffer.compareChk": { argTypes: [DYN, DYN], result: F64 }, - "bytes.equalsChk": { argTypes: [BYTES_U8, DYN], result: BOOL }, - "bytes.compareChk": { argTypes: [BYTES_U8, DYN, DYN, DYN, DYN, DYN], result: F64 }, - /** The deprecated `new Buffer(number, 'enc')` string-arm rejection: - * always throws Node's ERR_INVALID_ARG_TYPE ("The \"string\" argument - * must be of type string. Received ..."). Borrowed dyn; may-throw. */ - "buffer.newStringFail": { argTypes: [DYN], result: BYTES_U8 }, - /** fs._toUnixTimestamp(time) over a dyn value: numeric strings and - * finite numbers coerce (negatives answer now/1000, Node's shape); - * everything else throws Node's ERR_INVALID_ARG_TYPE. May-throw. */ - "fs.toUnixTimestamp": { argTypes: [DYN], result: F64 }, - /** The fs argument-validation ladders (checked-dynamic lane): each Chk - * replicates its API's Node-order validation over dyn values (Node's - * exact typed errors — ERR_INVALID_ARG_TYPE/VALUE, ERR_OUT_OF_RANGE), - * and a full pass meets the trailing compiler-rendered SC2020 fence - * string — so the ALWAYS-THROW forms take the error.nodeThrow - * polymorphic-result carve-out. mkdtempSyncChk and the lchmod sync/ - * promise pair run the REAL operation on a validated pass instead - * (macOS lchmod(2); non-APPLE answers Node's not-a-function / - * ERR_METHOD_NOT_IMPLEMENTED shapes). May-throw seeds, all of them. */ - "fs.existsChk": { argTypes: [DYN, DYN], result: DYN }, - "fs.mkdtempChk": { argTypes: [DYN, DYN, STRING], result: VOID }, - "fs.mkdtempSyncChk": { argTypes: [DYN, DYN, STRING], result: STRING }, - "fs.readFileChk": { argTypes: [DYN, DYN, DYN, STRING], result: VOID }, - "fs.opendirChk": { argTypes: [DYN, DYN, STRING], result: VOID }, - "fs.watchFileChk": { argTypes: [DYN, DYN, STRING], result: VOID }, - "fs.lchmodChk": { argTypes: [DYN, DYN, DYN, STRING], result: VOID }, - "fs.lchmodSyncChk": { argTypes: [DYN, DYN], result: DYN }, - "fsp.lchmodChk": { argTypes: [DYN, DYN], result: { kind: "promise", inner: VOID } }, - "fs.readChk": { argTypes: [DYN, DYN, DYN, DYN, DYN, STRING], result: VOID }, - "fs.streamOptsChk": { argTypes: [DYN, DYN, STRING], result: VOID }, - /** fs.ReadStream / fs.WriteStream (scr_fs_stream.c): the constructors - * take the path and options as dyn values and throw Node's option - * errors; the members read bytesRead/bytesWritten, pending and the path - * value, and close() / close(cb) (a `() => void` callback). */ - "fs.createReadStream": { - argTypes: [DYN, DYN], - result: { kind: "object", className: "%Readable" }, - }, - "fs.createWriteStream": { - argTypes: [DYN, DYN], - result: { kind: "object", className: "%Writable" }, - }, - "fs.fsStreamBytes": { argTypes: [null], result: F64 }, - "fs.fsStreamPending": { argTypes: [null], result: BOOL }, - "fs.fsStreamPath": { argTypes: [null], result: DYN }, - "fs.fsStreamPropDyn": { argTypes: [null, STRING], result: DYN }, - "fileHandle.createReadStream": { - argTypes: [FILEHANDLE_T, DYN], - result: { kind: "object", className: "%Readable" }, - }, - "fileHandle.createWriteStream": { - argTypes: [FILEHANDLE_T, DYN], - result: { kind: "object", className: "%Writable" }, - }, - "fs.fsStreamClose": { argTypes: [null], result: VOID }, - "fs.fsStreamCloseCb": { argTypes: [null, funcOf([], VOID)], result: VOID }, - /** The compiler-resolved ERR_INVALID_ARG_TYPE throw with a RUNTIME- - * rendered Received tail: args [argname, "of type ..." clause, the - * offending dyn value]. ALWAYS THROWS; polymorphic result (the - * error.nodeThrow pattern). May-throw seed. */ - "error.argTypeThrow": { argTypes: [STRING, STRING, DYN], result: VOID }, - /** The property flavor of argTypeThrow ("The \"options.x\" property - * must be ..."): the option-bag ladders' provably-invalid arms. ALWAYS - * THROWS; polymorphic result. May-throw seed. */ - "error.propTypeThrow": { argTypes: [STRING, STRING, DYN], result: VOID }, - /** The checked-dynamic max-listeners ladders: setMaxChk is the - * instance form over a dyn n (non-numbers ERR_INVALID_ARG_TYPE, - * negatives/NaN ERR_OUT_OF_RANGE; +1 receiver back — chaining); - * setDefaultMaxChk is the static/property form, its second argument - * naming the message slot ("setMaxListeners" for the static call, - * "defaultMaxListeners" for the module-property assignment). */ - "emitter.setMaxChk": { argTypes: [null, DYN], result: VOID }, - "emitter.setDefaultMaxChk": { argTypes: [DYN, STRING], result: VOID }, - /** The Buffer forms of fs: readFileSync(path) with NO - * encoding → bytes (+1), writeFileSync/appendFileSync(path, bytes), and the - * fs/promises readFile(path) no-encoding form (an already-settled - * promise, rejecting on failure like the other fsp members). The sync - * forms THROW catchably on failure exactly like the utf8 forms. */ - "fs.readFileSyncBytes": { argTypes: [STRING], result: BYTES_U8 }, - "fs.writeFileSyncBytes": { argTypes: [STRING, BYTES_U8], result: VOID }, - "fs.appendFileSyncBytes": { argTypes: [STRING, BYTES_U8], result: VOID }, - "fsp.readFileBytes": { argTypes: [STRING], result: { kind: "promise", inner: BYTES_U8 } }, - /** node:zlib (scr_zlib.c — native-toolchain.ts compiles/links it ONLY when these - * appear on the IR, the regex/libcurl gating precedent): one-shot zlib, - * raw-DEFLATE, gzip, and auto-detect codecs over u8 bytes with Node's - * default options, or a validated literal compression level from -1 to 9. - * Compression never throws (OOM aborts); decompression - * of corrupt input THROWS Node's error catchably. */ - "zlib.deflateSync": { argTypes: [BYTES_U8], result: BYTES_U8 }, - "zlib.inflateSync": { argTypes: [BYTES_U8], result: BYTES_U8 }, - "zlib.deflateRawSync": { argTypes: [BYTES_U8], result: BYTES_U8 }, - /** bytes, mode (0 zlib / 1 raw / 2 gzip), validated compression level. */ - "zlib.deflateLevelSync": { argTypes: [BYTES_U8, F64, F64], result: BYTES_U8 }, - "zlib.inflateRawSync": { argTypes: [BYTES_U8], result: BYTES_U8 }, - "zlib.gzipSync": { argTypes: [BYTES_U8], result: BYTES_U8 }, - "zlib.gunzipSync": { argTypes: [BYTES_U8], result: BYTES_U8 }, - "zlib.unzipSync": { argTypes: [BYTES_U8], result: BYTES_U8 }, - /** The default-options callback twins run on the executable's shared - * native work pool and deliver (Error | null, Buffer) on a later loop - * turn. crc32 validates its optional uint32 seed synchronously. */ - "zlib.deflateCb": { argTypes: [BYTES_U8, null], result: VOID }, - "zlib.inflateCb": { argTypes: [BYTES_U8, null], result: VOID }, - "zlib.deflateRawCb": { argTypes: [BYTES_U8, null], result: VOID }, - "zlib.inflateRawCb": { argTypes: [BYTES_U8, null], result: VOID }, - "zlib.gzipCb": { argTypes: [BYTES_U8, null], result: VOID }, - "zlib.gunzipCb": { argTypes: [BYTES_U8, null], result: VOID }, - "zlib.unzipCb": { argTypes: [BYTES_U8, null], result: VOID }, - "zlib.crc32": { argTypes: [BYTES_U8, F64], result: F64 }, - /** The one-shot forms with an options value: node_zlib_mode, the input, - * the options (validated at runtime with Node's ladder). The Cb form - * takes the program-shaped (Error | null, Buffer) callback, which moves. */ - "zlib.syncOpts": { argTypes: [F64, BYTES_U8, DYN], result: BYTES_U8 }, - "zlib.cbOpts": { argTypes: [F64, BYTES_U8, DYN, null], result: VOID }, - /** The checked-dynamic input twins (the JS lane's untyped data). */ - "zlib.syncOptsDyn": { argTypes: [F64, DYN, DYN], result: BYTES_U8 }, - "zlib.cbOptsDyn": { argTypes: [F64, DYN, DYN, null], result: VOID }, - /** util.promisify over the callback one-shots: [mode, data, options] - * like zlib.cbOpts → a promise the worker-pool run settles with the - * Buffer or the callback form's Error. An options or input error rejects - * it; never throws. The Dyn twin takes the JS lane's untyped data. */ - "zlib.promise": { argTypes: [F64, BYTES_U8, DYN], result: { kind: "promise", inner: BYTES_U8 } }, - "zlib.promiseDyn": { argTypes: [F64, DYN, DYN], result: { kind: "promise", inner: BYTES_U8 } }, - /** The stream classes (scr_zlib_stream.c, linked with the stream unit): - * streamNew takes Node's node_zlib_mode number and the options value - * (undefined when absent), validates it with Node's ladder, and answers - * the +1 %Transform stream. flush takes the kind value, params the level - * and strategy values; the Cb forms move their () => void callback. The - * receivers are borrowed. */ - "zlib.streamNew": { argTypes: [F64, DYN], result: VOID }, - "zlib.streamBytesWritten": { argTypes: [null], result: F64 }, - /** `stream instanceof zlib.Gzip`: the borrowed stream and the class's - * node_zlib_mode; true when the stream is a zlib stream of that mode. */ - "zlib.streamIs": { argTypes: [null, F64], result: BOOL }, - "zlib.streamFlush": { argTypes: [null, DYN], result: VOID }, - "zlib.streamFlushCb": { argTypes: [null, DYN, null], result: VOID }, - "zlib.streamParams": { argTypes: [null, DYN, DYN], result: VOID }, - "zlib.streamParamsCb": { argTypes: [null, DYN, DYN, null], result: VOID }, - "zlib.streamClose": { argTypes: [null], result: VOID }, - "zlib.streamCloseCb": { argTypes: [null, null], result: VOID }, - "zlib.streamReset": { argTypes: [null], result: VOID }, - /** The Buffer overloads of the raw stream writes — same promptly - * submitted streams as process.stdoutWrite/stderrWrite, constantly true. - * The encoding arg is evaluated but ignored for bytes, like Node. The Cb - * forms move their program-shaped completion callback to the tick queue. */ - "process.stdoutWriteBytes": { argTypes: [BYTES_U8, STRING], result: BOOL }, - "process.stderrWriteBytes": { argTypes: [BYTES_U8, STRING], result: BOOL }, - // Completion callback is program-dependent: zero params, checked-dynamic, - // or an optional Error | null slot (same success shape as fs.rename). - "process.stdoutWriteBytesCb": { argTypes: [BYTES_U8, STRING, null], result: BOOL }, - "process.stderrWriteBytesCb": { argTypes: [BYTES_U8, STRING, null], result: BOOL }, - "fsp.readFile": { argTypes: [STRING, STRING], result: { kind: "promise", inner: STRING } }, - "fsp.writeFile": { argTypes: [STRING, STRING], result: { kind: "promise", inner: VOID } }, - /** fs.promises.writeFile(path, data, { mode }): the settled-promise - * twin of fs.writeFileModeSync. */ - "fsp.writeFileMode": { - argTypes: [STRING, STRING, F64], - result: { kind: "promise", inner: VOID }, - }, - "fsp.mkdir": { argTypes: [STRING], result: { kind: "promise", inner: VOID } }, - /** The fs/promises option/member tail the certs pipeline uses: mkdir's - * literal { recursive?, mode? } options (the mkdirSync matrix behind - * settled promises), unlink, chmod. Failures REJECT (catchable at the - * await), like the rest of the fsp family. */ - "fsp.mkdirMode": { argTypes: [STRING, F64], result: { kind: "promise", inner: VOID } }, - "fsp.mkdirRecursive": { argTypes: [STRING], result: { kind: "promise", inner: VOID } }, - "fsp.mkdirRecursiveMode": { argTypes: [STRING, F64], result: { kind: "promise", inner: VOID } }, - "fsp.unlink": { argTypes: [STRING], result: { kind: "promise", inner: VOID } }, - "fsp.chmod": { argTypes: [STRING, F64], result: { kind: "promise", inner: VOID } }, - "fsp.chown": { argTypes: [STRING, F64, F64], result: { kind: "promise", inner: VOID } }, - "fsp.lchown": { argTypes: [STRING, F64, F64], result: { kind: "promise", inner: VOID } }, - "fsp.access": { argTypes: [STRING, F64], result: { kind: "promise", inner: VOID } }, - "fsp.truncate": { argTypes: [STRING, F64], result: { kind: "promise", inner: VOID } }, - "fsp.appendFile": { argTypes: [STRING, STRING], result: { kind: "promise", inner: VOID } }, - "fsp.appendFileBytes": { argTypes: [STRING, BYTES_U8], result: { kind: "promise", inner: VOID } }, - "fsp.copyFile": { argTypes: [STRING, STRING], result: { kind: "promise", inner: VOID } }, - "fsp.mkdtemp": { argTypes: [STRING], result: { kind: "promise", inner: STRING } }, - /** readFile(path, encoding | { encoding }) for any runtime encoding name. */ - "fsp.readFileEncoded": { argTypes: [STRING, STRING], result: { kind: "promise", inner: STRING } }, - "fsp.rmdir": { argTypes: [STRING], result: { kind: "promise", inner: VOID } }, - "fsp.link": { argTypes: [DYN, DYN], result: { kind: "promise", inner: VOID } }, - "fsp.symlink": { argTypes: [DYN, DYN, DYN], result: { kind: "promise", inner: VOID } }, - "fsp.cp": { argTypes: [DYN, DYN, DYN], result: { kind: "promise", inner: VOID } }, - "fsp.readlink": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, - "fsp.statfs": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, - "fsp.readlinkStr": { argTypes: [DYN, DYN], result: { kind: "promise", inner: STRING } }, - "fsp.readlinkBuffer": { argTypes: [DYN, DYN], result: { kind: "promise", inner: BYTES_U8 } }, - "fsp.utimes": { argTypes: [DYN, DYN, DYN], result: { kind: "promise", inner: VOID } }, - "fsp.lutimes": { argTypes: [DYN, DYN, DYN], result: { kind: "promise", inner: VOID } }, - "fsp.rename": { argTypes: [STRING, STRING], result: { kind: "promise", inner: VOID } }, - "fsp.readdir": { argTypes: [STRING], result: { kind: "promise", inner: arrayOf(STRING) } }, - // Result is promise; the libCall case - // validates the program-dependent record shape in validate.ts. - /** `fs.promises.readdir(path, { withFileTypes: true })` — the settled- - * promise twin of fs.readdirTypesSync. The backends assemble the same - * call-site-shaped Dirent rows, then move the array into a promise; - * scandir failure becomes its rejection. */ - "fsp.readdirTypes": { argTypes: [STRING], result: { kind: "promise", inner: VOID } }, - "fsp.rm": { argTypes: [STRING], result: { kind: "promise", inner: VOID } }, - /** `fs.promises.rm(path, options)`: fs.rmOptsSync and fs.rmRetrySync - * behind a settled promise; their errors become its rejection. */ - "fsp.rmOpts": { argTypes: [STRING, BOOL, BOOL], result: { kind: "promise", inner: VOID } }, - "fsp.rmRetry": { - argTypes: [STRING, BOOL, BOOL, F64, F64], - result: { kind: "promise", inner: VOID }, - }, - "fsp.stat": { argTypes: [STRING], result: { kind: "promise", inner: STATS_T } }, - "fsp.realpath": { argTypes: [STRING], result: { kind: "promise", inner: STRING } }, - "fsp.lstat": { argTypes: [STRING], result: { kind: "promise", inner: STATS_T } }, - /** fs/promises.open and the statically represented FileHandle surface. - * Every operation returns an already-settled promise; syscall failures - * become rejections rather than escaping synchronously. read/write - * results are call-site record shapes ({ bytesRead/bytesWritten, - * buffer }) assembled by the backends around the fixed runtime ABI. */ - "fsp.open": { argTypes: [STRING, STRING, F64], result: { kind: "promise", inner: FILEHANDLE_T } }, - "fileHandle.fd": { argTypes: [FILEHANDLE_T], result: F64 }, - "fileHandle.close": { argTypes: [FILEHANDLE_T], result: { kind: "promise", inner: VOID } }, - "fileHandle.sync": { argTypes: [FILEHANDLE_T], result: { kind: "promise", inner: VOID } }, - "fileHandle.datasync": { argTypes: [FILEHANDLE_T], result: { kind: "promise", inner: VOID } }, - "fileHandle.truncate": { - argTypes: [FILEHANDLE_T, F64], - result: { kind: "promise", inner: VOID }, - }, - "fileHandle.chmod": { argTypes: [FILEHANDLE_T, F64], result: { kind: "promise", inner: VOID } }, - "fileHandle.utimes": { - argTypes: [FILEHANDLE_T, DYN, DYN], - result: { kind: "promise", inner: VOID }, - }, - "fileHandle.readv": { - argTypes: [FILEHANDLE_T, arrayOf(BYTES_U8), F64], - result: { kind: "promise", inner: VOID }, - }, - "fileHandle.writev": { - argTypes: [FILEHANDLE_T, arrayOf(BYTES_U8), F64], - result: { kind: "promise", inner: VOID }, - }, - // read/write carry call-site result record shapes; the validator checks - // those in validate.ts, so promise is only a table sentinel. - "fileHandle.read": { - argTypes: [FILEHANDLE_T, BYTES_U8, F64, F64, F64, BOOL], - result: { kind: "promise", inner: VOID }, - }, - "fileHandle.writeBytes": { - argTypes: [FILEHANDLE_T, BYTES_U8, F64, F64, F64, BOOL], - result: { kind: "promise", inner: VOID }, - }, - "fileHandle.writeStr": { - argTypes: [FILEHANDLE_T, STRING, F64, STRING], - result: { kind: "promise", inner: VOID }, - }, - "fileHandle.readFile": { - argTypes: [FILEHANDLE_T, STRING], - result: { kind: "promise", inner: STRING }, - }, - "fileHandle.readFileBytes": { - argTypes: [FILEHANDLE_T, STRING], - result: { kind: "promise", inner: BYTES_U8 }, - }, - "fileHandle.writeFile": { - argTypes: [FILEHANDLE_T, STRING, STRING], - result: { kind: "promise", inner: VOID }, - }, - "fileHandle.writeFileBytes": { - argTypes: [FILEHANDLE_T, BYTES_U8, STRING], - result: { kind: "promise", inner: VOID }, - }, - "fileHandle.stat": { argTypes: [FILEHANDLE_T], result: { kind: "promise", inner: STATS_T } }, - "process.argv": { argTypes: [], result: arrayOf(STRING) }, - // The Node runtime flag a node:test binary took out of process.argv - // (--test-update-snapshots), or [] — a fresh array per read. - "process.execArgv": { argTypes: [], result: arrayOf(STRING) }, - "process.platform": { argTypes: [], result: STRING }, - // The one libCall whose result type is program-dependent (union ids are - // per-module): the `result` here is a placeholder — the libCall case - // checks the union's ARMS ([string, undefinedT] in canonical order) - // against the module's registry instead. - /** getenv(3): one string key arg → the interned `string | undefined` - * union (present: +1 string wrapped into the string arm; absent: the - * interned undefined-arm instance). BOTH source forms — `process.env.FOO` - * and `process.env[expr]` — lower here. Purely static; never throws. */ - "process.envGet": { argTypes: [STRING], result: VOID }, - /** setenv(3): (name, value) string args → void. Later envGet reads and - * spawned children observe the write, like Node (values are strings — - * the frontend fences non-string RHS). Never throws. */ - "process.envSet": { argTypes: [STRING, STRING], result: VOID }, - /** `delete process.env[NAME]` — unsetenv(3): the mutation is visible to - * every later read (process.envGet asks getenv fresh) and inherited by - * spawned children, exactly Node. Statement position only (JS's boolean - * result is constant true there). Borrows the name; never throws. */ - "process.envUnset": { argTypes: [STRING], result: VOID }, - /** The whole environment as alternating [k0, v0, k1, v1, ...] strings in - * environ order — the raw material of the process.env SNAPSHOT record - * (the frontend's interned %env.snapshot helper keyed-writes the pairs - * into a fresh `{ [k: string]: string | undefined }` record). Fresh +1 - * array; never throws. */ - "process.envPairs": { argTypes: [], result: arrayOf(STRING) }, - "process.exit": { argTypes: [F64], result: VOID }, - /** Numeric process.exitCode write (integer validation, implicit exit status). */ - "process.setExitCode": { argTypes: [F64], result: VOID }, - /** The process.exitCode write of a program that also reads the - * property: the same validation, and the code counts as assigned. */ - "process.setExitCodeTracked": { argTypes: [F64], result: VOID }, - /** The code for process.exit() with no argument, or zero when unset. */ - "process.currentExitCode": { argTypes: [], result: F64 }, - /** Whether process.exitCode reads a code rather than undefined. Never throws. */ - "process.hasExitCode": { argTypes: [], result: BOOL }, - "process.cwd": { argTypes: [], result: STRING }, - /** getpid(2) / getuid(2): zero args → f64. POSIX-only target, so both - * always answer (the checker's `getuid?` optionality covers Windows — - * `process.getuid?.()` lowers as the plain call). Never throw. */ - "process.pid": { argTypes: [], result: F64 }, - "dyn.this": { argTypes: [], result: DYN }, - "dyn.generatorThis": { argTypes: [], result: DYN }, - "process.getuid": { argTypes: [], result: F64 }, - "process.getgid": { argTypes: [], result: F64 }, - /** geteuid(2) / getegid(2) / getgroups(2) (the effective gid appended - * when the list lacks it, like Node). Windows has no such members: the - * calls throw the TypeError a missing member does (MAY THROW). */ - "process.geteuid": { argTypes: [], result: F64 }, - "process.getegid": { argTypes: [], result: F64 }, - "process.getgroups": { argTypes: [], result: arrayOf(F64) }, - /** process.ppid: the parent's pid at the read. Never throws. */ - "process.ppid": { argTypes: [], result: F64 }, - /** process.title: libuv's process title (argv[0] until set); setTitle - * writes it (truncated to the original argument area on POSIX, the - * console title on Windows). Never throw. */ - "process.title": { argTypes: [], result: STRING }, - "process.setTitle": { argTypes: [STRING], result: VOID }, - /** process.argv0: the original argv[0]. Never throws. */ - "process.argv0": { argTypes: [], result: STRING }, - /** process.release: the compatibility target's stable release object - * (name, lts, sourceUrl, headersUrl, and libUrl on Windows). +1. */ - "process.release": { argTypes: [], result: DYN }, - /** process.execPath: the compiled binary's own resolved absolute path - * (one interned string, +1 per read) — the honest answer where Node's - * is the node executable's (SEMANTICS.md divergence 12). Never throws. */ - "process.execPath": { argTypes: [], result: STRING }, - /** process.arch: the compiled binary's OWN architecture ("arm64", - * "x64") — Node's answer for its own build on the same machine. - * Interned; +1 per read. Never throws. */ - "process.arch": { argTypes: [], result: STRING }, - /** Stable native version dictionary. node/openssl identify compatibility - * targets; components absent from the native runtime are omitted. */ - "process.versions": { argTypes: [], result: DYN }, - "process.builtinId": { argTypes: [DYN, arrayOf(STRING)], result: STRING }, - "process.builtinModule": { argTypes: [STRING, DYN], result: DYN }, - "process.builtinUnsupported": { argTypes: [STRING, STRING], result: DYN }, - "fs.callbackValue": { argTypes: [STRING], result: DYN }, - "fs.callbackCall": { argTypes: [STRING, DYN], result: DYN }, - "process.hrtimeValue": { argTypes: [], result: DYN }, - /** process.versions.node: the runtime's Node COMPATIBILITY TARGET — - * there is no Node under the binary, so this reports the version whose - * semantics the runtime implements (SEMANTICS.md divergence 60, the - * execPath stance). Interned; +1 per read. Never throws. */ - "process.versionsNode": { argTypes: [], result: STRING }, - "process.versionsOpenssl": { argTypes: [], result: STRING }, - /** kill(2) with Node's exact semantics and error shapes: the pid must be - * an int32 (else the ERR_INVALID_ARG_TYPE TypeError text), the named - * form resolves Node's signal-name table (unknown names throw the - * ERR_UNKNOWN_SIGNAL TypeError), an omitted signal completes to - * "SIGTERM" in the frontend, signal 0 probes, and a kill(2) failure - * throws Node's `kill ESRCH`/`kill EPERM` Error. Result is Node's - * constant true. */ - "process.kill": { argTypes: [F64, STRING], result: BOOL }, - "process.killNum": { argTypes: [F64, F64], result: BOOL }, - /** The raw byte writes: one borrowed string arg → bool (constantly true - * — this synchronous runtime never queues backpressure). stdoutWrite - * shares console.log's stream, each call is submitted promptly, and - * interleaved output keeps source order. No newline, no formatting, - * never throws. */ - "process.stdoutWrite": { argTypes: [STRING], result: BOOL }, - "process.stdio": { argTypes: [F64], result: DYN }, - "process.stderrWrite": { argTypes: [STRING], result: BOOL }, - // error.new's result and the receiver slots are builtin-error classes — - // program-dependent object types, checked in the libCall case. - /** The runtime-provided Error hierarchy's entry points (scr_error.c). - * error.new: one borrowed string arg (the message), result an owned (+1) - * builtin error instance — the result TYPE names which builtin class - * (backends derive the runtime kind from it). error.ctor: the - * super(message) call of an `extends Error` constructor — borrowed - * receiver (already allocated by the derived class's new) + borrowed - * message, void; the receiver's type names the builtin class whose name - * field to stamp. error.toString: borrowed `%Error`-typed receiver, +1 - * string in Node's "name: message" shape. Formatting can invoke a user - * message accessor, which can throw. */ - "error.new": { argTypes: [STRING], result: VOID }, - /** ECMAScript constructors with raw checked-dynamic message/options. - * Options retain cause presence; constructor calls borrow their args. - * Message coercion may throw before the cause is installed. */ - "error.newOptions": { argTypes: [DYN, DYN], result: VOID }, - "error.ctorOptions": { argTypes: [null, DYN, DYN], result: VOID }, - /** Borrowed Error receiver. cause returns an owned dyn value (undefined - * when absent); hasCause distinguishes absence from present undefined. */ - "error.cause": { argTypes: [null], result: DYN }, - "error.hasCause": { argTypes: [null], result: BOOL }, - /** Assignment borrows both operands and retains the new cause. */ - "error.setCause": { argTypes: [null, DYN], result: VOID }, - /** `err.code = s` on an error-hierarchy receiver: the code slot that - * error.code reads and coded throws stamp. Borrows both; never throws. */ - "error.setCode": { argTypes: [null, STRING], result: VOID }, - /** `err.code = v` with a value that is not statically a string: strings - * take the code slot, anything else clears it and sets the dynamic own - * property. Borrows both; never throws. */ - "error.setCodeDyn": { argTypes: [null, DYN], result: VOID }, - /** `Object.assign(err, ...sources)` onto an error-hierarchy receiver - * (args: the error, a dyn array of the sources): the own-member copy - * onto its checked-dynamic node, then the typed slots (name, message, - * stack, cause, code) re-read from it. Throws what the copy throws. */ - "error.assign": { argTypes: [null, DYN], result: VOID }, - /** `key in err` for a key no compiled class of the receiver declares - * (args: the error, the key): a runtime-set own property (Object.assign, - * throw-site fields, dynamic writes) or an Object.prototype member. */ - "error.hasKey": { argTypes: [null, STRING], result: BOOL }, - "error.hasCode": { argTypes: [null], result: BOOL }, - /** Whether the error's own `name` or `message` (the key) is an accessor - * a reflective definition installed. Borrows both; never throws. */ - "error.ownAccessor": { argTypes: [null, STRING], result: BOOL }, - /** That accessor's getter called on the error (owned result), and its - * setter called with the value (V8's TypeError without one). Borrow - * their arguments; throw what the accessor throws. */ - "error.accessorGet": { argTypes: [null, STRING], result: DYN }, - "error.accessorSet": { argTypes: [null, STRING, DYN], result: VOID }, - "error.defineCause": { argTypes: [null, DYN], result: VOID }, - "error.deleteCause": { argTypes: [null], result: VOID }, - /** The compiler-resolved Node-parity throw for always-throwing lowered - * arms (ERR_INVALID_THIS receivers, ERR_MISSING_ARGS arity ladders, - * the symbol-to-string TypeError): args are [error-kind f64 (the - * SCR_ERR_* index: 0 Error, 1 TypeError, 2 RangeError), code (empty = - * no code slot), message]. ALWAYS THROWS catchably; the result type is - * the replaced expression's own (never materialized — the - * global.undefRead pattern). May-throw seed. */ - "error.nodeThrow": { argTypes: [F64, STRING, STRING], result: VOID }, - /** JS ToString over a dyn value WITH the object protocol (a user - * toString/valueOf member is CALLED and its throw propagates; - * exhaustion throws "Cannot convert object to primitive value"; units - * render "null"/"undefined") — the WHATWG USVString conversions - * (URLSearchParams names/values). Borrowed dyn; +1 string. May-throw. */ - "dyn.toStringCoerce": { argTypes: [DYN], result: STRING }, - "dyn.stringConstructor": { argTypes: [DYN], result: STRING }, - "dyn.propertyKey": { argTypes: [DYN], result: DYN }, - "dyn.toNumeric": { argTypes: [DYN], result: DYN }, - "dyn.increment": { argTypes: [DYN, BOOL], result: DYN }, - /** JS ToNumber over a dyn value WITH the object protocol (number-hint - * valueOf/toString ordering; user throws propagate). Borrowed dyn; - * f64 result, or a throw. Used by statically lowered numeric coercions - * whose checker type remained any. */ - "dyn.numberConstructor": { argTypes: [DYN], result: F64 }, - "dyn.bigintConstructor": { argTypes: [DYN], result: BIGINT_T }, - "dyn.toNumberCoerce": { argTypes: [DYN], result: F64 }, - "dyn.add": { argTypes: [DYN, DYN], result: DYN }, - "dyn.arithmetic": { argTypes: [DYN, DYN, STRING], result: DYN }, - "dyn.compare": { argTypes: [DYN, DYN, STRING], result: BOOL }, - "dyn.bitwise": { argTypes: [DYN, DYN, STRING], result: DYN }, - "dyn.proxyNew": { argTypes: [DYN, DYN], result: DYN }, - /* Dynamic-tier values (jsval; two-tier design stage S3). The jsval pass - * (ir/jsval.ts) introduces these in dynamic-tier functions only; the - * runtime is scr_js_value.c. Arguments are borrowed and results owned, - * like every libCall. Boundary conversions: js.fromDyn (a cell keeps its - * identity), js.toDyn (an immediate is boxed), js.fromF64/fromBool/ - * fromStr/fromBigInt (the design's toJs: immediates, and strings and - * BigInts under a tag without a copy) and js.toF64 (fromJs to number: dynCheck's - * number check and TypeError). The operators are the dyn.* operators - * over jsvals, with identical JS semantics. */ - "js.fromDyn": { argTypes: [DYN], result: JSVAL }, - "js.toDyn": { argTypes: [JSVAL], result: DYN }, - "js.fromF64": { argTypes: [F64], result: JSVAL }, - "js.fromBool": { argTypes: [BOOL], result: JSVAL }, - "js.fromStr": { argTypes: [STRING], result: JSVAL }, - "js.fromBigInt": { argTypes: [BIGINT_T], result: JSVAL }, - "js.toF64": { argTypes: [JSVAL], result: F64 }, - "js.add": { argTypes: [JSVAL, JSVAL], result: JSVAL }, - "js.arithmetic": { argTypes: [JSVAL, JSVAL, STRING], result: JSVAL }, - "js.bitwise": { argTypes: [JSVAL, JSVAL, STRING], result: JSVAL }, - "js.compare": { argTypes: [JSVAL, JSVAL, STRING], result: BOOL }, - "js.toNumeric": { argTypes: [JSVAL], result: JSVAL }, - "js.increment": { argTypes: [JSVAL, BOOL], result: JSVAL }, - "js.toNumberCoerce": { argTypes: [JSVAL], result: F64 }, - // Always throws; the result is the READ's declared type (a typed dummy - // the unwind abandons) — the libCall case skips the result check. - /** A read of a `declare`d const NOTHING defines (the bundler-define - * pattern — __VERSION__): always throws the catchable ReferenceError - * Node raises at the access (" is not defined"). args[0] is the - * name; the result type is the read's declared type (a typed dummy the - * unwind abandons — the value never exists). */ - "global.undefRead": { argTypes: [STRING], result: VOID }, - "console.native": { argTypes: [], result: DYN }, - /** node:console Console instances (scr_console_native.c): new mints the - * handle over the stdout/stderr fds (1/2; 0 = a stream binds next) and - * the ignoreErrors flag; bind attaches a borrowed writable stream to - * target 0 (stdout) or 1 (stderr); write prints `text\n` to the - * selected target of an instance or the global console handle. */ - "console.new": { argTypes: [F64, F64, BOOL], result: DYN }, - "console.bind": { argTypes: [DYN, F64, null], result: VOID }, - "console.write": { argTypes: [DYN, STRING, BOOL], result: VOID }, - /** The global console's remaining members (scr_console_native.c; the - * group indentation itself lives with the writers in scr_console.c). - * Arguments are borrowed; string results are owned. noop is the - * emitter-dropped tail of a lowering whose effects sit in statements. - * label is `${label}` over a defaulted label (undefined -> "default"; - * a Symbol throws Node's TypeError). count prints "label: n". - * countReset answers the warning text for an unknown (or non-string) - * label, "" otherwise. time answers the duplicate-label warning text - * or ""; timeEnd/timeLog answer Node's formatted elapsed time, or "" - * for an unknown label (timeEnd drops the timer). group adds (+1) or - * removes (-1) one two-space indentation step. clear writes the - * cursor-home and erase-down sequences when stdout is a terminal. - * table renders and prints Node's table for a dyn value (undefined - * properties = all). dirDyn is console.dir's inspect of a dyn value - * (custom inspection off: a Buffer anywhere inside throws the fence). */ - "console.noop": { argTypes: [], result: VOID }, - "console.label": { argTypes: [DYN], result: STRING }, - "console.count": { argTypes: [STRING], result: VOID }, - "console.countReset": { argTypes: [DYN], result: STRING }, - "console.time": { argTypes: [STRING], result: STRING }, - "console.timeEnd": { argTypes: [STRING], result: STRING }, - "console.timeLog": { argTypes: [STRING], result: STRING }, - "console.group": { argTypes: [F64], result: VOID }, - "console.clear": { argTypes: [], result: VOID }, - "console.table": { argTypes: [DYN, DYN], result: VOID }, - "console.dirDyn": { argTypes: [DYN, F64], result: STRING }, - /** A native reference to the global object; known names retain value fences. */ - "global.native": { argTypes: [arrayOf(STRING)], result: DYN }, - // `X.name` through a class value: the arg is a program-dependent - // classval (a null slot; the libCall case checks the kind). - /** `X.name` through a class VALUE (scr_object.c): args[0] is a borrowed - * classval; the result is the class object's stored .name string, - * retained (+1 — the string is an interned immortal, so the retain is a - * no-op, kept for ownership uniformity). Never throws. A direct - * `C.name` on the class name itself folds to a strLit instead. */ - "class.name": { argTypes: [null], result: STRING }, - "error.ctor": { argTypes: [null, STRING], result: VOID }, - "error.toString": { argTypes: [null], result: STRING }, - /** SameValue over checked values, including NaN, signed zero and reference identity. */ - "dyn.sameValue": { argTypes: [DYN, DYN], result: BOOL }, - "error.stack": { argTypes: [null], result: STRING }, - "error.setStack": { argTypes: [null, STRING], result: VOID }, - "error.stackLimitGet": { argTypes: [], result: F64 }, - "error.stackLimitSet": { argTypes: [F64], result: VOID }, - // Receiver (any error-hierarchy object) and the program-dependent - // `string | undefined` result are checked in the libCall case. - /** NodeJS.ErrnoException's `.code` read: borrowed error-hierarchy - * receiver → the interned `string | undefined` union (type-directed - * construction in the backend, the process.envGet pattern) — the errno - * name where a throw site stamped one (fs, exec spawn/timeout, - * process.kill, the spawn 'error' event), the undefined arm everywhere - * else. Never throws. */ - "error.code": { argTypes: [null], result: VOID }, - // DOMException: construction takes the two dyn args (WebIDL's - // resolution runs in the runtime); the %DOMException result is a - // program-dependent object type, checked in the libCall case. The - // read surface takes the %DOMException receiver (a null slot — the - // libCall case checks the class name). - /** `new DOMException(message?, nameOrOptions?)` (scr_error.c): both args - * are borrowed dyn values (the lowering passes the dyn undefined for an - * absent argument, so WebIDL's optionality lives in one place). The - * runtime ToStrings the message ("" for absent/undefined), resolves the - * name — absent/undefined → "Error", a non-null object → ToString of its - * `name` member plus the `cause` own-property record, anything else → - * ToString — and stamps the legacy code from the WebIDL name table (0 - * off-table). Result is an owned (+1) %DOMException. Never throws (dyn - * ToString is total). */ - "error.newDom": { argTypes: [DYN, DYN], result: VOID }, - /** DOMException's own read surface (scr_error.c; %DOMException receivers - * only — borrowed). domCode: the legacy numeric code. domHasCause: the - * options form's own-property record (`'cause' in e`). domCause: the - * cause value, +1 (the dyn undefined when absent — matching Node's - * undefined read). None throws. */ - "error.domCode": { argTypes: [null], result: F64 }, - "error.domHasCause": { argTypes: [null], result: BOOL }, - "error.domCause": { argTypes: [null], result: DYN }, - /** structuredClone of a %DOMException receiver (scr_error.c): WebIDL - * serialization — name/message copy, the legacy code re-derives, cause - * does not serialize. args are the borrowed receiver and the borrowed - * options dyn value (the dyn undefined when absent — the shared - * validation throws Node's exact option errors; any non-empty transfer - * list throws DataCloneError, nothing static is transferable). Result - * +1 %DOMException. */ - "error.domClone": { argTypes: [null, DYN], result: VOID }, - /** `new AggregateError(errors, message?, options?)` (scr_json.c), in - * spec order: newAggregate installs the message and cause (both dyn, - * borrowed — the message conversion can throw) and returns an owned - * %AggregateError; aggregateSetErrors then stores the iterated list - * (borrowed receiver, the dyn array borrowed and retained). - * aggregateErrors reads the list (+1, the same array every read). */ - "error.newAggregate": { - argTypes: [DYN, DYN], - result: { kind: "object", className: "%AggregateError" }, - }, - "error.aggregateSetErrors": { argTypes: [null, DYN], result: VOID }, - "error.aggregateErrors": { argTypes: [null], result: DYN }, - /** `new SuppressedError(error, suppressed, message?)` (scr_json.c): all - * three dyn and borrowed; the message conversion can throw. Returns an - * owned %SuppressedError. */ - "error.newSuppressed": { - argTypes: [DYN, DYN, DYN], - result: { kind: "object", className: "%SuppressedError" }, - }, - /** `d instanceof TypeError` (and the other BUILTIN error classes) on a - * checked-dynamic value (scr_json.c): the from_error cache holds the - * dyn↔error identity edge, so the test resolves the runtime error and - * asks its vtable's stamped preorder interval — exact for every error - * that crossed the boundary. A dyn object that never came from an - * error (a hand-built {%error} literal) answers false: subclass - * identity is unknowable there (the root keeps dynTest's marker - * answer). args are the borrowed dyn and the SCR_ERR_* kind literal. - * Never throws. */ - "dyn.errInstanceof": { argTypes: [DYN, F64], result: BOOL }, - "weakMap.is": { argTypes: [DYN], result: BOOL }, - "weakSet.is": { argTypes: [DYN], result: BOOL }, - "weakMap.new": { argTypes: [DYN], result: DYN }, - "weakSet.new": { argTypes: [DYN], result: DYN }, - "finalizationRegistry.new": { argTypes: [DYN], result: DYN }, - "finalizationRegistry.is": { argTypes: [DYN], result: BOOL }, - "dyn.fromEntries": { argTypes: [DYN], result: DYN }, - "bytes.constructor": { argTypes: [STRING], result: DYN }, - "bytes.instanceOf": { argTypes: [DYN, DYN, STRING], result: BOOL }, - "bytes.construct": { argTypes: [DYN, DYN, STRING], result: DYN }, - /** The ambient receiver — JS `this` in a plain (non-method) function - * body: the innermost binding the current firing/dispatch window - * pushed (Node's listener receiver, a dyn OBJ method's object, an - * apply/call thisArg), or the undefined dyn singleton with none bound - * (the strict-mode plain-call answer, the old constant). Zero args → - * dyn (+1). Never throws. */ - "dyn.dataViewIs": { argTypes: [DYN], result: BOOL }, - "dyn.construct": { argTypes: [DYN, DYN, STRING], result: DYN }, - "arrayBuffer.constructor": { argTypes: [], result: DYN }, - "sharedArrayBuffer.new": { argTypes: [DYN], result: DYN }, - "sharedArrayBuffer.is": { argTypes: [DYN], result: BOOL }, - "arrayBuffer.new": { argTypes: [DYN], result: DYN }, - "ffi.argument": { argTypes: [DYN, STRING], result: DYN }, - "ffi.memoryModule": { argTypes: [DYN], result: DYN }, - "intl.segmenterNew": { argTypes: [], result: DYN }, - "arrayBuffer.is": { argTypes: [DYN], result: BOOL }, - "arrayBuffer.isView": { argTypes: [DYN], result: BOOL }, - "arrayBuffer.byteLengthGetter": { argTypes: [], result: F64 }, - "arrayBuffer.byteLengthDescriptor": { argTypes: [DYN], result: DYN }, - "arrayBuffer.viewU8C": { argTypes: [DYN, DYN, DYN], result: bytesOf("u8c") }, - "arrayBuffer.viewI8": { argTypes: [DYN, DYN, DYN], result: bytesOf("i8") }, - "arrayBuffer.viewU16": { argTypes: [DYN, DYN, DYN], result: bytesOf("u16") }, - "arrayBuffer.viewI16": { argTypes: [DYN, DYN, DYN], result: bytesOf("i16") }, - "arrayBuffer.viewU8": { argTypes: [DYN, DYN, DYN], result: BYTES_U8 }, - "arrayBuffer.viewU32": { argTypes: [DYN, DYN, DYN], result: bytesOf("u32") }, - "arrayBuffer.viewI32": { argTypes: [DYN, DYN, DYN], result: bytesOf("i32") }, - "arrayBuffer.viewF16": { argTypes: [DYN, DYN, DYN], result: bytesOf("f16") }, - "arrayBuffer.viewF32": { argTypes: [DYN, DYN, DYN], result: bytesOf("f32") }, - "arrayBuffer.viewF64": { argTypes: [DYN, DYN, DYN], result: bytesOf("f64") }, - "arrayBuffer.viewDV": { argTypes: [DYN, DYN, DYN], result: BYTES_U8 }, - /** Object.keys/values/entries over a CHECKED-DYNAMIC receiver - * (scr_json.c): the runtime walks the dyn node's own members in JS - * own-key order (array-index keys ascending first, then insertion - * order) and answers a dyn array (entries: an array of [key, value] - * pairs; values RETAIN the member nodes — reference semantics, like - * JS). Strings/arrays/bytes answer their index keys; other scalars an - * empty array; null/undefined throw Node's catchable TypeError - * ("Cannot convert undefined or null to object"). */ - "dyn.objKeys": { argTypes: [DYN], result: DYN }, - "dyn.forInKeys": { argTypes: [DYN], result: DYN }, - "dyn.forInKeyStrings": { argTypes: [DYN], result: arrayOf(STRING) }, - "dyn.forInStable": { argTypes: [DYN], result: BOOL }, - "dyn.objKeyStrings": { argTypes: [DYN], result: arrayOf(STRING) }, - /** Object.entries as a native [string, any][] (VOID: the tuple row's - * shape is program-dependent; the validator checks it). */ - "dyn.objEntryPairs": { argTypes: [DYN], result: VOID }, - "dyn.hasOwn": { argTypes: [DYN, STRING], result: BOOL }, - "dyn.propertyIsEnumerable": { argTypes: [DYN, STRING], result: BOOL }, - "dyn.assign": { argTypes: [DYN, DYN], result: DYN }, - "dyn.copyDataProperties": { argTypes: [DYN, DYN], result: DYN }, - "dyn.objectRest": { argTypes: [DYN, DYN], result: DYN }, - /** Variadic Object.assign over CHECKED-DYNAMIC targets (`Object.assign( - * {}, ...arr.map(f), tail)` — the option-table merge): the lowering - * builds one fresh dyn pack of sources (packPush retains a plain source - * in; packPushSpread flattens a spread source through the spread-call - * walk — V8's exact TypeError texts, the string arg spelling the spread - * expression for the nullish form), so every source evaluates and - * flattens BEFORE any copying (JS's ArgumentListEvaluation), then - * assignAll copies each pack element's own enumerable keys onto the - * target left to right and answers the TARGET (+1) — identity, like JS. - * assignAll throws Node's ToObject TypeError on a nullish target. */ - "dyn.packPush": { argTypes: [DYN, DYN], result: VOID }, - "dyn.packPushSpread": { argTypes: [DYN, DYN, STRING], result: VOID }, - /** The iterated-path spread twin: V8 spells the optimized apply-path - * texts (packPushSpread's — the expression named for nullish sources) - * only for the SINGLE LAST argument's spread; any other spread position - * drives the real iterator protocol, whose failure describes the VALUE - * ("object null is not iterable (cannot read property - * Symbol(Symbol.iterator))"). The frontend picks by position. */ - "dyn.packPushSpreadIter": { argTypes: [DYN, DYN], result: VOID }, - "dyn.reflectApply": { argTypes: [DYN, DYN, DYN], result: DYN }, - "dyn.reflectGet": { argTypes: [DYN, DYN, DYN], result: DYN }, - /** `using` / `await using` with a dyn value (scr_json.c): usingMethod - * runs the declaration's checks and answers what usingDispose calls at - * scope exit (undefined for a null or undefined value). The bool selects - * `await using`. Both throw Node's TypeErrors or the method's error. */ - "dyn.usingMethod": { argTypes: [DYN, BOOL], result: DYN }, - "dyn.usingDispose": { argTypes: [DYN, DYN], result: DYN }, - /** DisposableStack (scr_symbol.c): a new stack handle; whether a value is - * one; and `stack.use(value)` for a class instance, registering the - * compiler's disposal function for it. The last throws Node's - * ReferenceError on a disposed stack. */ - "disposable.stackNew": { argTypes: [], result: DYN }, - "disposable.stackIs": { argTypes: [DYN], result: BOOL }, - "disposable.stackUseCallback": { argTypes: [DYN, DYN, DYN], result: BOOL }, - "dyn.reflectSet": { argTypes: [DYN, DYN, DYN, DYN], result: BOOL }, - "dyn.reflectDefine": { argTypes: [DYN, DYN, DYN], result: BOOL }, - "dyn.abstractEq": { argTypes: [DYN, DYN], result: BOOL }, - "dyn.assignAll": { argTypes: [DYN, DYN], result: DYN }, + "math.trunc": { argTypes: [F64], result: F64 }, + "math.ceil": { argTypes: [F64], result: F64 }, + "math.sin": { argTypes: [F64], result: F64 }, + "math.sinh": { argTypes: [F64], result: F64 }, + "math.cos": { argTypes: [F64], result: F64 }, + "math.cosh": { argTypes: [F64], result: F64 }, + "math.tan": { argTypes: [F64], result: F64 }, + "math.tanh": { argTypes: [F64], result: F64 }, + "math.asin": { argTypes: [F64], result: F64 }, + "math.asinh": { argTypes: [F64], result: F64 }, + "math.acos": { argTypes: [F64], result: F64 }, + "math.acosh": { argTypes: [F64], result: F64 }, + "math.atan": { argTypes: [F64], result: F64 }, + "math.atanh": { argTypes: [F64], result: F64 }, + "math.cbrt": { argTypes: [F64], result: F64 }, + "math.clz32": { argTypes: [F64], result: F64 }, + /** Return -1, +1, or the original NaN/zero. In particular, -0 remains -0. */ + "math.sign": { argTypes: [F64], result: F64 }, + "math.exp": { argTypes: [F64], result: F64 }, + "math.expm1": { argTypes: [F64], result: F64 }, + "math.f16round": { argTypes: [F64], result: F64 }, + "math.fround": { argTypes: [F64], result: F64 }, + "math.sqrt": { argTypes: [F64], result: F64 }, + "math.log": { argTypes: [F64], result: F64 }, + "math.log1p": { argTypes: [F64], result: F64 }, + "math.log2": { argTypes: [F64], result: F64 }, + "math.log10": { argTypes: [F64], result: F64 }, + "math.atan2": { argTypes: [F64, F64], result: F64 }, + "math.pow": { argTypes: [F64, F64], result: F64 }, + "math.imul": { argTypes: [F64, F64], result: F64 }, + /** `Math.min(a, b)` / `Math.max(a, b)` — the two-argument scalar forms + * (scr_lib.c), JS-exact like the Arr folds: NaN poisons, max prefers +0 + * over -0 (min the reverse). C fmin/fmax are NOT these (they drop NaN). + * Never throw. */ + "math.min": { argTypes: [F64, F64], result: F64 }, + "math.max": { argTypes: [F64, F64], result: F64 }, + /** `Math.random()` — a uniform double in [0,1) with the spec's 53-bit + * granularity, drawn from arc4random_buf (the CSPRNG behind the crypto + * lowerings). Same distribution as Node, NECESSARILY different sequence + * (SEMANTICS.md 62 — no seeded sequence exists to match). Never throws. */ + "math.random": { argTypes: [], result: F64 }, + /** The static global parsers/tests (scr_string.c). num.parseInt is + * ECMA-262 19.2.5 exactly — JS whitespace, sign, ToInt32 radix (the + * frontend completes an omitted radix to 0 = the spec's "undefined": + * base 10 with the 0x hex escape), longest digit prefix, and the exact + * mathematical value correctly rounded (u64 fast path, bignum beyond — + * overflow is ±Infinity). num.isNaN is the NaN self-test on an + * already-number argument (tsc pins the argument to number, so no + * ToNumber coercion exists to model). Borrow; never throw. */ + "num.parseInt": { argTypes: [STRING, F64], result: F64 }, + /** ES parseFloat (scr_string.c): the longest StrDecimalLiteral prefix + * of the trimmed input (no hex, "Infinity" exact-case), NaN when none — + * ECMA-262 19.2.4 over a string argument (non-string arguments keep the + * fence: Node would ToNumber-coerce). Borrows; never throws. */ + "num.parseFloat": { argTypes: [STRING], result: F64 }, + /** `a.localeCompare(b)`: -1/0/1 in Node's root-locale collation for + * Latin-script text (scr_str_locale_compare). Never throws. */ + "str.localeCompare": { argTypes: [STRING, STRING], result: F64 }, + /** Annex B substr(start, length) with start ToNumber'd and length's + * presence; never throws. */ + "str.substr": { argTypes: [STRING, F64, F64, BOOL], result: STRING }, + /** toLocaleLowerCase/toLocaleUpperCase (scr_regex.c): the locales value + * as written, upper=true for toLocaleUpperCase. Throws for null and for + * locale forms with no native rules. */ + "str.localeCase": { argTypes: [STRING, DYN, BOOL], result: STRING }, + "str.replaceLiteral": { argTypes: [STRING, STRING, STRING, BOOL], result: STRING }, + // (subject, characters, NUL-separated replacement texts): the fused + // chain of global one-character replacements. + "str.replaceChars": { argTypes: [STRING, STRING, STRING], result: STRING }, + "str.codePoints": { argTypes: [STRING], result: arrayOf(STRING) }, + /* ToNumber(string) — ECMA-262 7.1.4.1 StringToNumber (scr_string.c): + * trim the JS StrWhiteSpace set, empty/whitespace-only → +0, then the + * whole span must be one StrNumericLiteral — signed decimal (Infinity + * included, strtod-over-validated-span correct rounding) or unsigned + * 0x/0o/0b (exact value, nearest-even; signed forms are NaN) — with + * any trailing garbage answering NaN. Number(aString), unary + on + * strings, and util.format %d over strings lower here. Borrows; never + * throws. */ + "num.fromString": { argTypes: [STRING], result: F64 }, + "num.isNaN": { argTypes: [F64], result: BOOL }, + /** The static URI component codecs (scr_string.c), ECMA-262 Encode/ + * Decode with the component sets over the runtime's UTF-8 strings. + * str.encodeUriComponent percent-encodes every byte outside the + * unreserved component set (ALPHA/DIGIT/- _ . ! ~ * ' ( )) as uppercase + * %XX — the spec's per-code-point UTF-8 encoding IS a byte scan here — + * and THROWS the spec's URIError ("URI malformed") on a lone surrogate. + * str.decodeUriComponent + * decodes %XX escapes bytewise (raw non-escape bytes copy through) and + * requires the escaped bytes to form strictly valid UTF-8 (overlong + * forms, surrogate code points, and >U+10FFFF refused, per UTF8-decode + * without replacement); bad hex or an invalid sequence THROWS the + * spec's URIError ("URI malformed"), catchable. Borrow; results +1. */ + "str.encodeUriComponent": { argTypes: [STRING], result: STRING }, + // The base64 globals: the argument is a dyn value (WebIDL ToString + // runs in the runtime); the zero-argument form always throws. + /** The WHATWG base64 globals (scr_string.c), Node-global since v16. + * Arguments are borrowed dyn values — WebIDL ToString runs in the + * runtime over the dyn kind (String(null) is "null": the html spec's + * coercion, which Node's atob(null) exercises). str.atob decodes + * forgiving-base64 (ASCII whitespace stripped, %4==0 strips up to two + * '=', %4==1 refuses, leftover bits discarded) into the latin1 code + * points as a string; a malformed input THROWS the catchable + * DOMException InvalidCharacterError ("The string to be decoded is not + * correctly encoded."). str.btoa encodes the string's code points as + * base64; any code point over U+00FF THROWS InvalidCharacterError + * ("Invalid character"). str.b64Missing is the zero-argument call of + * either: always throws Node's TypeError [ERR_MISSING_ARGS] "The + * \"input\" argument must be specified". Results +1. */ + "str.atob": { argTypes: [DYN], result: STRING }, + "str.btoa": { argTypes: [DYN], result: STRING }, + "str.b64Missing": { argTypes: [], result: STRING }, + "str.decodeUriComponent": { argTypes: [STRING], result: STRING }, + "str.decodeUri": { argTypes: [STRING], result: STRING }, + /** encodeURI: the same Encode() keeping the reserved set and '#' + * unescaped — throwing like the component encoder. Borrow; result +1. */ + "str.encodeUri": { argTypes: [STRING], result: STRING }, + /** RegExp.escape (ES2025): per-code-point EncodeForRegExpEscape — + * leading ASCII alphanumeric hex-escapes, syntax characters and '/' + * take a backslash, other punctuators/whitespace/line terminators + * hex-escape, the rest passes through. Total; borrows; result +1. */ + "regexp.escape": { argTypes: [STRING], result: STRING }, + /** Number.prototype formatters (scr_lib.c), JS-exact: + * num.toExponential is toExponential() with the spec's "as many digits + * as necessary"; num.toFixed0 is the non-throwing omitted-argument + * toFixed() fast path; num.toFixed implements an explicit fractionDigits + * with exact binary-value rounding and THROWS RangeError outside 0..100. + * Successful results +1. */ + "num.toExponential": { argTypes: [F64], result: STRING }, + "num.toFixed0": { argTypes: [F64], result: STRING }, + "num.toFixed": { argTypes: [F64, F64], result: STRING }, + /** toPrecision(precision) with the argument ToNumber'd and its presence + * (undefined answers ToString); toExponential with a DEFINED + * fractionDigits. Both throw V8's RangeError for out-of-range digits. */ + "num.toPrecision": { argTypes: [F64, F64, BOOL], result: STRING }, + "num.toExponentialDigits": { argTypes: [F64, F64], result: STRING }, + "num.toStringRadix": { argTypes: [F64, DYN], result: STRING }, + /** toString(radix) with a number radix (scr_lib.c): truncates it as + * ToIntegerOrInfinity does, THROWS RangeError outside 2..36. */ + "num.toStringRadixF64": { argTypes: [F64, F64], result: STRING }, + /** Object.is over two numbers — the spec's SameValue on doubles: NaN + * equals NaN, +0 differs from -0, everything else is `===`. Plain bool + * result; never throws. (Union-armed operands take unionEq's sameValue + * flag instead — this is the both-f64 fast path.) */ + "num.sameValue": { argTypes: [F64, F64], result: BOOL }, + /** `new Intl.NumberFormat("en-US").format(x)` and + * `x.toLocaleString("en-US")` with DEFAULT options — the one locale + * whose data the runtime embeds (Node's default-build locale): decimal + * notation, 0–3 fraction digits rounded half-up on the SHORTEST + * round-tripping decimal (ICU's rounding input, probed vs Node — + * format(1.0005) is "1.001" though toFixed(3) answers "1.000"), "," + * grouping every three integer digits, "∞"/"NaN" texts, and "-0" for + * negative inputs rounding to zero. Result +1; never throws. */ + "intl.numFormatEnUs": { argTypes: [F64], result: STRING }, + /** ES Symbol values (scr_symbol.c — link-gated by moduleUsesSymbol). + * sym.new: `Symbol(desc)` — a fresh runtime-unique identity (+1) whose + * one arg is the description string (borrowed); sym.newAnon is the + * description-less `Symbol()` form. sym.for: the Symbol.for global + * registry — one interned symbol per key (borrowed), +1 on every call. + * sym.toString: "Symbol(desc)" (+1 string; "Symbol()" when absent). + * sym.desc / sym.keyFor answer the interned `string | undefined` union + * (the runtime returns +1-or-NULL; the backend builds the union arms — + * the child.stdout pattern). None of these throw. */ + "sym.new": { argTypes: [STRING], result: SYMBOL_T }, + "sym.newAnon": { argTypes: [], result: SYMBOL_T }, + "sym.for": { argTypes: [STRING], result: SYMBOL_T }, + "sym.wellKnown": { argTypes: [STRING], result: SYMBOL_T }, + // Result is the interned `string | undefined` union — the libCall case + // checks the arms (the spawnRes.error pattern). + "sym.keyFor": { argTypes: [SYMBOL_T], result: VOID }, + "sym.desc": { argTypes: [SYMBOL_T], result: VOID }, + "sym.toString": { argTypes: [SYMBOL_T], result: STRING }, + /** Engine-free arbitrary-precision bigint operations (scr_bigint.c). + * parse accepts the ECMAScript BigInt string grammar; fromF64 accepts + * finite integral Numbers. Arithmetic returns fresh immutable values. + * cmp returns -1/0/1 as f64; toString accepts a numeric radix. */ + "bigint.parse": { argTypes: [STRING], result: BIGINT_T }, + "bigint.fromF64": { argTypes: [F64], result: BIGINT_T }, + "bigint.neg": { argTypes: [BIGINT_T], result: BIGINT_T }, + "bigint.not": { argTypes: [BIGINT_T], result: BIGINT_T }, + "bigint.add": { argTypes: [BIGINT_T, BIGINT_T], result: BIGINT_T }, + "bigint.sub": { argTypes: [BIGINT_T, BIGINT_T], result: BIGINT_T }, + "bigint.mul": { argTypes: [BIGINT_T, BIGINT_T], result: BIGINT_T }, + "bigint.div": { argTypes: [BIGINT_T, BIGINT_T], result: BIGINT_T }, + "bigint.mod": { argTypes: [BIGINT_T, BIGINT_T], result: BIGINT_T }, + "bigint.pow": { argTypes: [BIGINT_T, BIGINT_T], result: BIGINT_T }, + "bigint.and": { argTypes: [BIGINT_T, BIGINT_T], result: BIGINT_T }, + "bigint.or": { argTypes: [BIGINT_T, BIGINT_T], result: BIGINT_T }, + "bigint.xor": { argTypes: [BIGINT_T, BIGINT_T], result: BIGINT_T }, + "bigint.shl": { argTypes: [BIGINT_T, BIGINT_T], result: BIGINT_T }, + "bigint.shr": { argTypes: [BIGINT_T, BIGINT_T], result: BIGINT_T }, + "bigint.eq": { argTypes: [BIGINT_T, BIGINT_T], result: BOOL }, + "bigint.eqString": { argTypes: [BIGINT_T, STRING], result: BOOL }, + "bigint.cmp": { argTypes: [BIGINT_T, BIGINT_T], result: F64 }, + "bigint.cmpNumber": { argTypes: [BIGINT_T, F64], result: F64 }, + "bigint.truthy": { argTypes: [BIGINT_T], result: BOOL }, + "bigint.toString": { argTypes: [BIGINT_T, F64], result: STRING }, + "bigint.inspect": { argTypes: [BIGINT_T], result: STRING }, + "bigint.toF64": { argTypes: [BIGINT_T], result: F64 }, + "bigint.asUintN": { argTypes: [F64, BIGINT_T], result: BIGINT_T }, + "bigint.asIntN": { argTypes: [F64, BIGINT_T], result: BIGINT_T }, + "bigint.bufferRead": { argTypes: [BYTES_U8, F64, BOOL, BOOL], result: BIGINT_T }, + "bigint.bufferWrite": { argTypes: [BYTES_U8, BIGINT_T, F64, BOOL, BOOL], result: F64 }, + "bigint.dataViewGet": { argTypes: [BYTES_U8, F64, BOOL, BOOL], result: BIGINT_T }, + "bigint.dataViewSet": { argTypes: [BYTES_U8, F64, BIGINT_T, BOOL], result: VOID }, + /** node:url + the URL class (scr_url.c). url.new parses one absolute + * URL string into a mutable URL value (+1) — invalid input THROWS a + * catchable TypeError ("Invalid URL"), like Node's constructor. The + * getters (borrowed receiver, +1 string) never throw; url.href doubles + * as toString(). fileURLToPath has one libFn per receiver form (URL + * value / string) — both THROW Node's TypeErrors on non-file schemes, + * encoded slashes, and non-empty hosts. url.pathToFileURL resolves the + * path (getcwd) and never throws. */ + "url.new": { argTypes: [STRING], result: URL_T }, + "url.newBase": { argTypes: [STRING, STRING], result: URL_T }, + /** Factories consume already-converted input/base strings. canParse + * answers a boolean; parse returns a checked native URL or null (+1). + * Parser failures are suppressed; conversion occurs before these calls. */ + "url.canParse": { argTypes: [STRING], result: BOOL }, + "url.canParseBase": { argTypes: [STRING, STRING], result: BOOL }, + "url.parse": { argTypes: [STRING], result: DYN }, + "url.parseBase": { argTypes: [STRING, STRING], result: DYN }, + "url.protocol": { argTypes: [URL_T], result: STRING }, + "url.origin": { argTypes: [URL_T], result: STRING }, + "url.username": { argTypes: [URL_T], result: STRING }, + "url.password": { argTypes: [URL_T], result: STRING }, + "url.host": { argTypes: [URL_T], result: STRING }, + "url.hostname": { argTypes: [URL_T], result: STRING }, + "url.port": { argTypes: [URL_T], result: STRING }, + "url.pathname": { argTypes: [URL_T], result: STRING }, + "url.href": { argTypes: [URL_T], result: STRING }, + /** Selected component writes borrow the receiver and converted string. + * href replaces its parsed fields while retaining cached params identity. */ + "url.set": { argTypes: [URL_T, STRING, STRING], result: VOID }, + "url.setChecked": { argTypes: [URL_T, STRING, DYN], result: VOID }, + "url.fileURLToPathUrl": { argTypes: [URL_T], result: STRING }, + /** fileURLToPathUrl as a value: the path string, or the error it would + * throw (fs.existsSync and fs.promises URL paths). Never throws. */ + "url.fileURLToPathValue": { argTypes: [URL_T], result: DYN }, + /** getValidatedPath's null-byte check on a string path, and + * readFileSync's encoding check: what Node validates before it converts + * a later URL path. (path, argument name) and (encoding). */ + "fs.checkPath": { argTypes: [STRING, STRING], result: VOID }, + "fs.checkReadEncoding": { argTypes: [STRING], result: VOID }, + "url.fileURLToPathChecked": { argTypes: [DYN], result: STRING }, + "url.fileURLToPathOptions": { argTypes: [DYN, DYN], result: STRING }, + /** node:url's legacy API (scr_url_legacy.c): parse(url, parseQueryString, + * slashesDenoteHost) answers the checked-dynamic Url instance; format + * takes the urlObject (a Url-shaped object, a string, or a URL) and the + * URL form's options; resolve takes (from, to). The trailing bool says + * whether the call site emits DEP0169. domainToASCII/domainToUnicode + * take the domain and whether it was omitted. Node's validation errors + * throw catchably. */ + "url.legacyParse": { argTypes: [DYN, DYN, DYN, BOOL], result: DYN }, + "url.legacyFormat": { argTypes: [DYN, DYN, BOOL], result: STRING }, + "url.legacyResolve": { argTypes: [DYN, DYN, BOOL], result: STRING }, + "url.domainToASCII": { argTypes: [DYN, BOOL], result: STRING }, + "url.domainToUnicode": { argTypes: [DYN, BOOL], result: STRING }, + /** url.urlToHttpOptions over a checked-dynamic argument: a URL answers + * Node's null-prototype options object; a primitive or null throws + * ERR_INVALID_ARG_TYPE. */ + "url.toHttpOptions": { argTypes: [DYN], result: DYN }, + /** node:punycode (scr_url.c): punycode.js 2.1.0's string conversions; + * decode/toUnicode (and, past 32-bit deltas, encode/toASCII) throw the + * module's RangeErrors, ucs2Encode String.fromCodePoint's. warn marks the + * module's first load (scr_async_dyn.c) and reports DEP0040 then unless + * the second argument is false: at once for a static ES import, on the + * next tick for a require() call. */ + "punycode.decode": { argTypes: [STRING], result: STRING }, + "punycode.encode": { argTypes: [STRING], result: STRING }, + "punycode.toASCII": { argTypes: [STRING], result: STRING }, + "punycode.toUnicode": { argTypes: [STRING], result: STRING }, + "punycode.ucs2Decode": { argTypes: [STRING], result: arrayOf(F64) }, + "punycode.ucs2Encode": { argTypes: [arrayOf(F64)], result: STRING }, + "punycode.warn": { argTypes: [BOOL, BOOL], result: VOID }, + "url.fileURLToPathBuffer": { argTypes: [DYN, DYN], result: BYTES_U8 }, + "url.pathToFileURLChecked": { argTypes: [DYN, DYN], result: URL_T }, + "url.fileURLToPathStr": { argTypes: [STRING], result: STRING }, + "url.pathToFileURL": { argTypes: [STRING], result: URL_T }, + /** Explicit path syntax from the options.windows argument, independent + * of the executable's host platform. Windows UNC input can throw. */ + "url.pathToFileURLPlatform": { argTypes: [STRING, BOOL], result: URL_T }, + /** pathToFileURL under a win32 TARGET: the same scr_url_from_path call + * (the runtime selects the win32 arm by _WIN32), but a distinct IR name + * because that arm THROWS for malformed UNC inputs — may-throw seeds on + * it while posix pathToFileURL emission stays byte-identical. */ + "url.pathToFileURLWin32": { argTypes: [STRING], result: URL_T }, + /** URLSearchParams (scr_url.c — always linked with the url unit). + * Construction: sp.new (empty), sp.parse (one borrowed init string — + * a single leading '?' strips, Node's constructor), sp.copy (snapshot + * of another list), sp.fromPairs (a string[][] value — THROWS Node's + * ERR_INVALID_TUPLE TypeError on a non-[name, value] row; the one + * throwing entry in the family), sp.with (the record-literal init + * desugar: append one pair, answer the same list +1 — chains fold + * `new URLSearchParams({...})` into nested calls). url.searchParams + * answers the URL's LIVE cached view (+1, one identity per URL); + * url.search is the WHATWG search getter ("" for no/empty query). + * Methods mirror the WHATWG surface: sp.get answers +1-or-NULL (the + * sym.desc union pattern, null arm), sp.getAll a fresh string[]; + * sp.append/sp.set/sp.delete/sp.deleteValue/sp.sort mutate and + * re-serialize a live view's URL query; sp.has/sp.hasValue answer + * bools; sp.size/sp.toString are pure reads. sp.keyAt/sp.valAt are + * the for-of/forEach desugar's index reads (live — the loop re-reads + * sp.size each pass). */ + "sp.new": { argTypes: [], result: SEARCH_PARAMS_T }, + "sp.newChecked": { argTypes: [DYN], result: SEARCH_PARAMS_T }, + "sp.parse": { argTypes: [STRING], result: SEARCH_PARAMS_T }, + "sp.copy": { argTypes: [SEARCH_PARAMS_T], result: SEARCH_PARAMS_T }, + // The pairs argument is string[][] — checked structurally in validate.ts (the + // generic array-of slot has no named constant here). + "sp.fromPairs": { argTypes: [null], result: SEARCH_PARAMS_T }, + "sp.with": { argTypes: [SEARCH_PARAMS_T, STRING, STRING], result: SEARCH_PARAMS_T }, + "url.searchParams": { argTypes: [URL_T], result: SEARCH_PARAMS_T }, + "url.search": { argTypes: [URL_T], result: STRING }, + "url.hash": { argTypes: [URL_T], result: STRING }, + // Result is the interned `string | null` union — the libCall case + // checks the arms (the spawnRes.signal pattern). + "sp.get": { argTypes: [SEARCH_PARAMS_T, STRING], result: VOID }, + "sp.getAll": { argTypes: [SEARCH_PARAMS_T, STRING], result: arrayOf(STRING) }, + "sp.append": { argTypes: [SEARCH_PARAMS_T, STRING, STRING], result: VOID }, + "sp.set": { argTypes: [SEARCH_PARAMS_T, STRING, STRING], result: VOID }, + "sp.delete": { argTypes: [SEARCH_PARAMS_T, STRING], result: VOID }, + "sp.deleteValue": { argTypes: [SEARCH_PARAMS_T, STRING, STRING], result: VOID }, + "sp.has": { argTypes: [SEARCH_PARAMS_T, STRING], result: BOOL }, + "sp.hasValue": { argTypes: [SEARCH_PARAMS_T, STRING, STRING], result: BOOL }, + "sp.sort": { argTypes: [SEARCH_PARAMS_T], result: VOID }, + "sp.size": { argTypes: [SEARCH_PARAMS_T], result: F64 }, + "sp.toString": { argTypes: [SEARCH_PARAMS_T], result: STRING }, + "sp.keyAt": { argTypes: [SEARCH_PARAMS_T, F64], result: STRING }, + "sp.valAt": { argTypes: [SEARCH_PARAMS_T, F64], result: STRING }, + // node:querystring. qs.parse's result is the call site's ParsedUrlQuery + // dictionary record (VOID is the networkInterfaces sentinel — the + // libCall case checks the structure); qs.stringify's object argument + // is a dyn value (the frontend dynFroms typed records). + /** node:querystring (scr_qs.c — link-gated by moduleUsesQs; NOT + * URLSearchParams: the legacy codec's escaping and '+' rules differ). + * qs.escape is Node's qsEscape, which encodes exactly the component + * unreserved set — it emits the always-linked scr_qs_escape, so + * escape-only programs never pull the unit. qs.unescape is Node's qsUnescape: strict decodeURIComponent + * first, the lenient legacy unescapeBuffer fallback on failure (never + * throws). qs.parse takes (str, sep, eq, maxKeys) — the frontend + * completes omitted/null sep/eq to "&"/"=" and the omitted maxKeys + * option to Node's 1000 (0 and negatives mean unlimited, Node's rule) — + * and its result type is the CALL SITE's mapped ParsedUrlQuery shape (a + * pure index-signature record over `string | string[]`, an undefined + * arm tolerated — @types/node's Dict), verified structurally by the + * frontend (lowerQuerystringParseCall); the emitters construct the + * record and hand its overflow map to scr_qs_parse_into with the two + * union tags. qs.stringify takes (obj, sep, eq) with obj a dyn value + * (the frontend dynFroms the typed record; JS-world dyn values pass + * through) — Node's encodeStringified rules run in the runtime, so + * arrays expand to repeated keys and null/undefined/nested objects are + * empty values. Custom encoder/decoder options fence at compile time. + * All borrow; string results +1; escape and stringify throw URIError + * [ERR_INVALID_URI] on a lone surrogate. */ + "qs.parse": { argTypes: [STRING, STRING, STRING, F64], result: VOID }, + "qs.stringify": { argTypes: [DYN, STRING, STRING], result: STRING }, + "qs.escape": { argTypes: [STRING], result: STRING }, + "qs.unescape": { argTypes: [STRING], result: STRING }, + /** node:util.parseArgs: one checked-dynamic config object in, one + * checked-dynamic ParsedResults tree out. Statically typed member reads + * validate their values when leaving that tree. The native parser may + * throw Node's coded validation/grammar TypeErrors. */ + "util.parseArgs": { argTypes: [DYN], result: DYN }, + "util.parseEnv": { argTypes: [DYN], result: DYN }, + "util.getSystemErrorName": { argTypes: [DYN], result: STRING }, + "util.getSystemErrorMessage": { argTypes: [DYN], result: STRING }, + "util.systemErrorEntries": { argTypes: [], result: DYN }, + "util.stripVTControlCharacters": { argTypes: [DYN], result: STRING }, + "util.toUSVString": { argTypes: [DYN], result: STRING }, + "util.isDeepStrictEqual": { argTypes: [DYN, DYN, DYN], result: BOOL }, + "util.styleText": { argTypes: [DYN, DYN, DYN], result: STRING }, + /** util.inherits/deprecate/callbackify/debuglog over checked-dynamic + * arguments (scr_util_callable.c). The function results are native dyn + * wrappers; call sites with a static function type check them back. */ + "util.inherits": { argTypes: [DYN, DYN], result: VOID }, + "util.inheritsPrototype": { argTypes: [DYN, DYN, DYN], result: VOID }, + "util.deprecate": { argTypes: [DYN, DYN, DYN, DYN], result: DYN }, + "util.isArrayDeprecated": { argTypes: [DYN], result: BOOL }, + "util.extend": { argTypes: [DYN, DYN], result: DYN }, + "util.signalExitCode": { argTypes: [DYN], result: F64 }, + "util.aborted": { argTypes: [DYN, DYN], result: DYN }, + "util.diff": { argTypes: [DYN, DYN], result: DYN }, + "util.debounce": { argTypes: [DYN, DYN, DYN], result: DYN }, + "util.throttle": { argTypes: [DYN, DYN, DYN, DYN], result: DYN }, + "mime.typeNew": { argTypes: [DYN, DYN], result: DYN }, + "mime.typeParse": { argTypes: [DYN], result: DYN }, + "mime.paramsNew": { argTypes: [], result: DYN }, + "mime.is": { argTypes: [DYN, F64], result: BOOL }, + "util.callbackify": { argTypes: [DYN], result: DYN }, + /** util.promisify over an untyped function value: a native dyn function + * whose calls answer a dyn promise the appended callback settles. */ + "util.promisifyDyn": { argTypes: [DYN], result: DYN }, + "util.debuglog": { argTypes: [DYN, DYN], result: DYN }, + "util.typeIs": { argTypes: [DYN, STRING], result: BOOL }, + "util.typeValue": { argTypes: [DYN, STRING], result: DYN }, + /** fs.statSync → a Stats value (may throw, like the other sync fs + * calls); the stats.* getters are pure reads on it. */ + "fs.statSync": { argTypes: [STRING], result: STATS_T }, + "fs.lstatSync": { argTypes: [STRING], result: STATS_T }, + // statSync / lstatSync(path, { throwIfNoEntry }): arg 1 is the flag. + // Result is the interned `Stats | undefined` union (the runtime answers + // +1-or-NULL; the backend builds the arms). + "fs.statProbeSync": { argTypes: [STRING, BOOL], result: VOID }, + "fs.lstatProbeSync": { argTypes: [STRING, BOOL], result: VOID }, + "fs.fstatSync": { argTypes: [F64], result: STATS_T }, + "fs.fchmodSync": { argTypes: [F64, F64], result: VOID }, + "fs.fsyncSync": { argTypes: [F64], result: VOID }, + "fs.fdatasyncSync": { argTypes: [F64], result: VOID }, + "fs.ftruncateSync": { argTypes: [F64, F64], result: VOID }, + "fs.utimesSync": { argTypes: [DYN, DYN, DYN], result: VOID }, + "fs.futimesSync": { argTypes: [DYN, DYN, DYN], result: VOID }, + "fs.lutimesSync": { argTypes: [DYN, DYN, DYN], result: VOID }, + "fs.readvSync": { argTypes: [F64, arrayOf(BYTES_U8), F64], result: F64 }, + "fs.writevSync": { argTypes: [F64, arrayOf(BYTES_U8), F64], result: F64 }, + "fs.linkSync": { argTypes: [DYN, DYN], result: VOID }, + "fs.symlinkSync": { argTypes: [DYN, DYN, DYN], result: VOID }, + /** fs.cpSync / fs/promises cp (scr_fs_cp.c): src, dest and options are + * dyn values the runtime validates in Node's order. */ + "fs.cpSync": { argTypes: [DYN, DYN, DYN], result: VOID }, + "fs.readlinkSync": { argTypes: [DYN, DYN], result: DYN }, + "fs.statfsSync": { argTypes: [DYN, DYN], result: DYN }, + "fs.readlinkSyncStr": { argTypes: [DYN, DYN], result: STRING }, + "fs.readlinkSyncBuffer": { argTypes: [DYN, DYN], result: BYTES_U8 }, + /** fs.openSync(path, flags) → the raw fd as f64; fs.readSync/fs.writeSync + * over Buffer windows perform sequential I/O when position is -1 and + * offset-preserving positioned I/O otherwise; fs.writeStrSync is the + * utf8 string twin; and fs.closeSync(fd) closes the descriptor. This is + * the fd slice behind spawn/log-processing forms. + * flags is Node's string + * grammar ("r", "w", "a" and the +/x variants; unknown flags throw + * Node's ERR_INVALID_ARG_VALUE TypeError text). All throw Node-shaped + * fs errors (openSync ENOENT/EACCES..., readSync EBADF/range errors, + * closeSync EBADF). */ + "fs.openSync": { argTypes: [STRING, STRING], result: F64 }, + "fs.openNumericSync": { argTypes: [STRING, F64, F64], result: F64 }, + "fs.readSync": { argTypes: [F64, BYTES_U8, F64, F64, F64], result: F64 }, + "fs.readSyncDyn": { argTypes: [DYN, DYN, DYN, DYN, DYN], result: F64 }, + "fs.writeSync": { argTypes: [F64, BYTES_U8, F64, F64, F64], result: F64 }, + "fs.writeStrSync": { argTypes: [F64, STRING, F64, STRING], result: F64 }, + // fs.watch's callback func type is program-dependent (zero params, or + // the eventType string) — the slot pins arity and the path/receiver. + /** fs.watch(path, listener?) → an FSWatcher handle (scr_watch.c — + * linked, and scr_watch_install() emitted, only when these appear on + * the IR; moduleUsesFsWatch is the switch). The path opens NOW — + * failure throws Node's fs error synchronously ("ENOENT: ..., watch + * 'x'", the polling-fallback catch shape) — and the unit's kqueue + * (EVFILT_VNODE) delivers "rename"/"change" through the loop's watch + * hook. The callback (nullable) MOVES in with an adapter per listener + * shape (zero-param, or the (eventType: string) form — runtime- + * provided); an open watcher keeps the loop alive until watcher.close() + * (idempotent, receiver borrowed, statement position only). */ + "fs.watch": { argTypes: [STRING], result: FSWATCHER_T }, + "fs.watchCb": { argTypes: [STRING, null], result: FSWATCHER_T }, + "watcher.close": { argTypes: [FSWATCHER_T], result: VOID }, + /** The composed `new crypto.X509Certificate(data).fingerprint` read + * (scr_lib.c — no certificate handle exists): the SHA-1 of the DER, + * uppercase colon-separated, over PEM or raw-DER Buffer input; other + * inputs throw Node's ERR_OSSL_PEM_NO_START_LINE Error (may-throw). */ + "crypto.x509Fingerprint": { argTypes: [BYTES_U8], result: STRING }, + "crypto.x509FingerprintStr": { argTypes: [STRING], result: STRING }, + /** The certificate's Validity window (validFrom / validTo reads — + * scr_lib.c's minimal ASN.1 walk to the TBSCertificate validity + * SEQUENCE): UTCTime and GeneralizedTime render in Node's exact + * ASN1_TIME_print shape ("Jul 1 00:00:00 2026 GMT" — %2d space-padded + * day). Same input contract and PEM error as the fingerprint pair + * (may-throw). */ + "crypto.x509ValidFrom": { argTypes: [BYTES_U8], result: STRING }, + "crypto.x509ValidFromStr": { argTypes: [STRING], result: STRING }, + "crypto.x509ValidTo": { argTypes: [BYTES_U8], result: STRING }, + "crypto.x509ValidToStr": { argTypes: [STRING], result: STRING }, + /** The certificate's DER as a fresh Buffer (the raw field), decoded + * once from the constructor's input; the same PEM error (may-throw). */ + "crypto.x509Raw": { argTypes: [BYTES_U8], result: BYTES_U8 }, + "crypto.x509RawDer": { argTypes: [BYTES_U8], result: BYTES_U8 }, + "crypto.x509RawStr": { argTypes: [STRING], result: BYTES_U8 }, + /** The remaining X509Certificate fields over that DER, in Node's + * renderings (scr_lib.c). subjectAltName, infoAccess and + * signatureAlgorithm answer "" where Node answers undefined. Borrowed; + * never throw. */ + "crypto.x509Subject": { argTypes: [BYTES_U8], result: STRING }, + "crypto.x509Issuer": { argTypes: [BYTES_U8], result: STRING }, + "crypto.x509SubjectAltName": { argTypes: [BYTES_U8], result: STRING }, + "crypto.x509InfoAccess": { argTypes: [BYTES_U8], result: STRING }, + "crypto.x509SerialNumber": { argTypes: [BYTES_U8], result: STRING }, + "crypto.x509Fingerprint256": { argTypes: [BYTES_U8], result: STRING }, + "crypto.x509Fingerprint512": { argTypes: [BYTES_U8], result: STRING }, + "crypto.x509Ca": { argTypes: [BYTES_U8], result: BOOL }, + "crypto.x509SignatureAlgorithm": { argTypes: [BYTES_U8], result: STRING }, + "crypto.x509SignatureAlgorithmOid": { argTypes: [BYTES_U8], result: STRING }, + /** X509Certificate methods over its raw DER. checkHost/checkEmail/checkIP + * answer the matching name or "" (undefined), checkIssued compares two + * certificates, x509Pem is toString's PEM (scr_x509.c); publicKey, + * verify and checkPrivateKey read keys (scr_pki.c). */ + "crypto.x509CheckHost": { argTypes: [BYTES_U8, DYN, DYN], result: STRING }, + "crypto.x509CheckEmail": { argTypes: [BYTES_U8, DYN, DYN], result: STRING }, + "crypto.x509CheckIP": { argTypes: [BYTES_U8, DYN, DYN], result: STRING }, + "crypto.x509CheckIssued": { argTypes: [BYTES_U8, BYTES_U8], result: BOOL }, + "crypto.x509LegacyObject": { argTypes: [BYTES_U8], result: DYN }, + "crypto.x509Pem": { argTypes: [BYTES_U8], result: STRING }, + "crypto.x509PublicKey": { argTypes: [BYTES_U8], result: DYN }, + "crypto.x509Verify": { argTypes: [BYTES_U8, DYN], result: BOOL }, + "crypto.x509CheckPrivateKey": { argTypes: [BYTES_U8, DYN], result: BOOL }, + "fs.closeSync": { argTypes: [F64], result: VOID }, + "stats.isFile": { argTypes: [STATS_T], result: BOOL }, + "stats.isDirectory": { argTypes: [STATS_T], result: BOOL }, + "stats.isSymbolicLink": { argTypes: [STATS_T], result: BOOL }, + "stats.size": { argTypes: [STATS_T], result: F64 }, + "stats.dev": { argTypes: [STATS_T], result: F64 }, + "stats.ino": { argTypes: [STATS_T], result: F64 }, + "stats.blocks": { argTypes: [STATS_T], result: F64 }, + "stats.nlink": { argTypes: [STATS_T], result: F64 }, + "stats.atimeMs": { argTypes: [STATS_T], result: F64 }, + "stats.mtimeMs": { argTypes: [STATS_T], result: F64 }, + "stats.ctimeMs": { argTypes: [STATS_T], result: F64 }, + "stats.birthtimeMs": { argTypes: [STATS_T], result: F64 }, + "stats.mode": { argTypes: [STATS_T], result: F64 }, + "stats.uid": { argTypes: [STATS_T], result: F64 }, + "stats.gid": { argTypes: [STATS_T], result: F64 }, + "stats.rdev": { argTypes: [STATS_T], result: F64 }, + "stats.blksize": { argTypes: [STATS_T], result: F64 }, + "stats.isBlockDevice": { argTypes: [STATS_T], result: BOOL }, + "stats.isCharacterDevice": { argTypes: [STATS_T], result: BOOL }, + "stats.isFIFO": { argTypes: [STATS_T], result: BOOL }, + "stats.isSocket": { argTypes: [STATS_T], result: BOOL }, + /** The Date fields: the *Ms value rounded (Node's dateFromMs). */ + "stats.atime": { argTypes: [STATS_T], result: DATE_T }, + "stats.mtime": { argTypes: [STATS_T], result: DATE_T }, + "stats.ctime": { argTypes: [STATS_T], result: DATE_T }, + "stats.birthtime": { argTypes: [STATS_T], result: DATE_T }, + // The wider sync fs slice (unlink/chmod/chown/copyfile and the + // mode-carrying write/mkdir forms). + /** The wider sync fs slice (scr_lib.c), all throwing catchably with + * Node's errno message shapes and `.code` stamped like the rest of + * sync fs. unlink/chmod/chown wrap the syscalls 1:1 (Node reports the + * syscall's own name). copyFileSync copies contents into a fresh (or + * truncated) destination carrying the SOURCE's mode (libuv's + * uv_fs_copyfile behavior); its errors carry BOTH paths — Node's + * "copyfile 'src' -> 'dest'". lstatSync is statSync without following + * a trailing symlink (Node reports lstat); stats.isSymbolicLink / + * stats.dev / ino / blocks / nlink / atimeMs / mtimeMs / ctimeMs are pure reads on the widened + * snapshot (blocks is allocated 512-byte units; the times are milliseconds + * with their sub-second fractions, Node's arithmetic). + * writeFileModeSync is writeFileSync(path, data, { mode }): the mode + * applies at CREATION only (open(2) with O_CREAT, umask applying), + * exactly Node — an existing file keeps its permissions. mkdirModeSync + * / mkdirRecursiveModeSync are the mkdirSync option forms with an + * explicit mode (the recursive walk passes it to every directory it + * creates, like Node's). */ + "fs.unlinkSync": { argTypes: [STRING], result: VOID }, + "fs.chmodSync": { argTypes: [STRING, F64], result: VOID }, + "fs.chownSync": { argTypes: [STRING, F64, F64], result: VOID }, + "fs.lchownSync": { argTypes: [STRING, F64, F64], result: VOID }, + "fs.fchownSync": { argTypes: [F64, F64, F64], result: VOID }, + /** truncateSync opens the path with 'r+', then truncates (len checked and + * clamped like ftruncateSync's) and closes it. */ + "fs.truncateSync": { argTypes: [STRING, F64], result: VOID }, + "fs.copyFileSync": { argTypes: [STRING, STRING], result: VOID }, + /** renameSync is the direct two-path syscall wrapper. renameCb defers + * the operation to the loop and fires a program-shaped Error | null + * callback through a backend-emitted adapter. */ + "fs.renameSync": { argTypes: [STRING, STRING], result: VOID }, + // Callback type is program-dependent (zero params or Error | null). + "fs.renameCb": { argTypes: [STRING, STRING, null], result: VOID }, + "fs.writeFileModeSync": { argTypes: [STRING, STRING, F64], result: VOID }, + "fs.mkdirModeSync": { argTypes: [STRING, F64], result: VOID }, + "fs.mkdirRecursiveModeSync": { argTypes: [STRING, F64], result: VOID }, + "atomics.wait": { argTypes: [bytesOf("i32"), F64, F64, F64], result: STRING }, + "atomics.notify": { argTypes: [bytesOf("i32"), F64, F64], result: F64 }, + "atomics.op": { argTypes: [null, F64, F64, F64, F64], result: F64 }, + "atomics.opDyn": { argTypes: [DYN, F64, F64, F64, F64], result: F64 }, + "atomics.waitDyn": { argTypes: [DYN, F64, F64, F64], result: STRING }, + "atomics.notifyDyn": { argTypes: [DYN, F64, F64], result: F64 }, + "worker.new": { argTypes: [F64, STRING, DYN], result: DYN }, + "worker.isMainThread": { argTypes: [], result: BOOL }, + "worker.threadId": { argTypes: [], result: F64 }, + "worker.root": { argTypes: [], result: F64 }, + "worker.data": { argTypes: [], result: DYN }, + "worker.parentPort": { argTypes: [], result: DYN }, + /** worker_threads.resourceLimits: {} in the main thread; a refusal in a + * worker (scr_worker_events.c). */ + "worker.resourceLimits": { argTypes: [], result: DYN }, + /** worker_threads.threadName: the Worker's name in a worker, "" in the + * main thread (scr_worker.c). */ + "worker.threadName": { argTypes: [], result: DYN }, + /** new MessageChannel() ({ port1, port2 }) and receiveMessageOnPort(port) + * (scr_worker_events.c; the latter may throw). */ + "worker.channel": { argTypes: [], result: DYN }, + "worker.receiveMessageOnPort": { argTypes: [DYN], result: DYN }, + /** setEnvironmentData(key, value) and getEnvironmentData(key) + * (scr_worker_events.c; arguments borrowed). */ + "worker.setEnvironmentData": { argTypes: [DYN, DYN], result: VOID }, + "worker.getEnvironmentData": { argTypes: [DYN], result: DYN }, + /** markAsUntransferable(value) and isMarkedAsUntransferable(value) + * (scr_worker_events.c; the value borrowed). */ + "worker.markUntransferable": { argTypes: [DYN], result: VOID }, + "worker.isMarkedUntransferable": { argTypes: [DYN], result: BOOL }, + /** markAsUncloneable(value) (scr_json.c; the value borrowed). */ + "dyn.markUncloneable": { argTypes: [DYN], result: VOID }, + /** before new Worker() with the stdout or stderr option: links the stream + * unit that worker.stdout and worker.stderr read through (scr_stream.c). */ + "stream.workerStdio": { argTypes: [], result: VOID }, + /** new BroadcastChannel(...args) (scr_worker_events.c; may throw), after + * event.broadcastInstall, which installs its EventTarget side. */ + "worker.broadcastChannel": { argTypes: [DYN], result: DYN }, + "event.broadcastInstall": { argTypes: [], result: VOID }, + /** child_process.spawnSync (scr_child.c): posix_spawn + waitpid + piped + * utf8 capture — cmd borrowed, args one borrowed string[] (the frontend + * completes an omitted list to an empty literal), result an owned (+1) + * spawnRes. NEVER throws: spawn failure (nonexistent binary, EACCES) is + * data, like Node's error property — status null and empty outputs + * (SEMANTICS.md documents the divergence from Node's null stdout). + * The getters are pure reads: status is the interned `number | null` + * union (null = spawn failure or signal death, type-directed + * construction in the backend like process.envGet); stdout/stderr are + * +1 strings. */ + "cp.spawnSync": { argTypes: [STRING, arrayOf(STRING)], result: SPAWNRES_T }, + // spawnSync's options form: timeout, killSignal name ("" = SIGTERM), + // and the three stdio modes. + /** spawnSync with options (scr_child.c): the cp.spawnSync core plus the + * option slice portless-class CLIs pass — timeout (killSignal fires at + * the deadline and the result carries error: ETIMEDOUT + the signal, + * Node's shape), killSignal (a signal NAME; "" = the SIGTERM default), + * and per-fd stdio modes (in: 0 /dev/null, 1 ignore, 2 inherit; out/ + * err: 0 capture, 1 ignore → "", 2 inherit → ""). NEVER throws — spawn + * failure and timeout are data on the result, like Node's error + * property. spawnRes.signal is the result's termination signal as the + * call site's `Signals | null` union (null = exited normally or spawn + * failure), constructed type-directedly like spawnRes.status. */ + "cp.spawnSyncOpts": { + argTypes: [STRING, arrayOf(STRING), F64, STRING, F64, F64, F64], + result: SPAWNRES_T, + }, + /** cp.spawnSyncOpts with the `env` option: the trailing [k,v,...] + * pairs replace the child's environment (recordToEnvPairs). */ + "cp.spawnSyncEnv": { + argTypes: [STRING, arrayOf(STRING), F64, STRING, F64, F64, F64, arrayOf(STRING)], + result: SPAWNRES_T, + }, + /** cp.spawnSyncOpts with the stdio carried as a RUNTIME string — + * "pipe" | "ignore" | "inherit", proven by the call site's TYPE (the + * defaultRunner idiom `stdio: options?.stdio ?? "pipe"`); the runtime + * maps the value to the three modes. Args (cmd, argv, timeout, + * killSignal, stdio). Never throws, like the other spawnSync forms. */ + "cp.spawnSyncStdioStr": { + argTypes: [STRING, arrayOf(STRING), F64, STRING, STRING], + result: SPAWNRES_T, + }, + // node:net (scr_net.c). Listener slots are program-dependent closures — + // null slots, checked in the libCall case (the child.onExit precedent); + // the zero-param callbacks pin the exact func type here. The trailing + // BOOL on registrations is the once flag. + /** node:net (scr_net.c — linked, and scr_net_install() emitted, only + * when one of these appears on the IR; moduleUsesNet is the switch). + * Receivers are borrowed; CALLBACKS MOVE into the handle's listener + * registry and are released at settlement (the child.onExit story). + * All listener registrations carry a trailing BOOL once-flag (`on` vs + * `once`) and are void — chaining is fenced. None of these throw: + * listen/connect failures are the async 'error' event, like Node. + * + * net.createServer's optional connection handler and the + * serverOnConnection listeners take the runtime-provided adapters + * (zero-param, or the one-param socket shape); sockOnData's adapters + * are the stdin pair's shapes (zero-param / bytes chunk); the error + * events reuse the child %Error adapters. net.listen/net.listenCb bind + * NOW and defer 'listening' to the next loop turn (Node's next-tick + * emit); net.serverPort is the composed `server.address().port` read. + * net.connect's host argument is a string ("localhost" pins to + * 127.0.0.1 — SEMANTICS.md); the connect callback is once('connect'). */ + "net.createServer": { argTypes: [], result: NETSERVER_T }, + "net.createServerCb": { argTypes: [null], result: NETSERVER_T }, + "net.listen": { argTypes: [NETSERVER_T, F64], result: VOID }, + /** listen(path[, cb]): a Unix-socket server; the socket file is removed + * when the server closes. Failures are the async 'error'. */ + "net.listenPath": { argTypes: [NETSERVER_T, STRING], result: VOID }, + "net.listenPathCb": { + argTypes: [NETSERVER_T, STRING, { kind: "func", params: [], ret: VOID }], + result: VOID, + }, + "net.listenCb": { + argTypes: [NETSERVER_T, F64, { kind: "func", params: [], ret: VOID }], + result: VOID, + }, + /** listen({ port, host, ipv6Only }[, cb]) — the explicit-interface bind + * (portless's listenOnProxyInterface): args [server, port, host, + * ipv6Only]. host is an IP literal string ("" = the host-less + * dual-stack any default); ipv6Only sets IPV6_V6ONLY before the bind. + * These are the old-shaped calls, retained for omitted reusePort so + * serialized IR remains compatible. Failures are the async 'error', + * message in Node's listen shape with the requested host. Never throws. */ + "net.listenOpts": { argTypes: [NETSERVER_T, F64, STRING, BOOL], result: VOID }, + // The callback slot is a zero-param void closure OR its + // `(() => void) | undefined` optional-binding union (checked specially). + "net.listenOptsCb": { argTypes: [NETSERVER_T, F64, STRING, BOOL, null], result: VOID }, + /** Additive ABI for listen({ ..., reusePort }) — args + * [server, port, host, ipv6Only, reusePort], with the callback following + * those values in the callback form. The final BOOL is the requested + * kernel connection-distribution option; old spellings remain + * four/five-argument calls so serialized IR stays compatible. */ + "net.listenOptsReusePort": { argTypes: [NETSERVER_T, F64, STRING, BOOL, BOOL], result: VOID }, + // The callback is a zero-param void closure OR its optional-binding union; + // the callback follows the reusePort BOOL in this additive ABI. + "net.listenOptsReusePortCb": { + argTypes: [NETSERVER_T, F64, STRING, BOOL, BOOL, null], + result: VOID, + }, + "net.serverPort": { argTypes: [NETSERVER_T], result: F64 }, + "net.serverListening": { argTypes: [NETSERVER_T], result: BOOL }, + /** socket.ref()/unref() and server.ref()/unref(): whether the handle + * keeps the event loop alive. */ + "net.sockSetRef": { argTypes: [NETSOCKET_T, BOOL], result: VOID }, + "net.serverSetRef": { argTypes: [NETSERVER_T, BOOL], result: VOID }, + // net.serverAddress's record result is shape-checked in the libCall case + // (the dgram.address sentinel pattern). + /** server.address() as the full AddressInfo record (the dgram.address + * materialization pattern: the emitter builds the record from the three + * runtime reads; the frontend pinned the shape). Never throws — before + * listen it answers the any-form defaults with port 0 where Node + * answers null (the serverPort stance). */ + "net.serverAddress": { argTypes: [NETSERVER_T], result: VOID }, + "net.serverClose": { argTypes: [NETSERVER_T], result: VOID }, + /** server.close((err) => ...): the callback (a dyn function) gets + * undefined once the server closes, or Node's ERR_SERVER_NOT_RUNNING + * when it was not listening. */ + "net.serverCloseDyn": { argTypes: [NETSERVER_T, DYN], result: VOID }, + /** server.address() in programs that may listen on a Unix socket: the + * path a listening server answers ("" for a TCP server), and the check + * a record-typed address() makes first (throws for a Unix server). */ + "net.serverUnixPath": { argTypes: [NETSERVER_T], result: STRING }, + "net.serverAddressCheck": { argTypes: [NETSERVER_T], result: VOID }, + "net.serverCloseCb": { + argTypes: [NETSERVER_T, { kind: "func", params: [], ret: VOID }], + result: VOID, + }, + "net.serverOnError": { argTypes: [NETSERVER_T, null, BOOL], result: VOID }, + "net.serverOnClose": { + argTypes: [NETSERVER_T, { kind: "func", params: [], ret: VOID }, BOOL], + result: VOID, + }, + "net.serverOnConnection": { argTypes: [NETSERVER_T, null, BOOL], result: VOID }, + /** 'secureConnection' — the TLS server's deferred 'connection' list + * (handshake-completion timing); on a server without deferred + * connections (a plain net server) the registration never fires, + * exactly Node's split. */ + "net.serverOnSecureConnection": { argTypes: [NETSERVER_T, null, BOOL], result: VOID }, + "net.connect": { argTypes: [F64, STRING], result: NETSOCKET_T }, + /** connect with a validated autoSelectFamilyAttemptTimeout option (the + * budget runs Node's validateInt32-from-1 ladder and is then inert — + * the single dial has nothing to time). May-throw. */ + "net.connectAttempt": { argTypes: [F64, STRING, DYN], result: NETSOCKET_T }, + /** net.connect/createConnection over a RUNTIME option bag (computed + * keys — the invalid-input probes): Node-order validation (the + * objectMode trio's ERR_INVALID_ARG_VALUE, validatePort, host string, + * autoSelectFamily boolean, the attempt budget), then the trailing + * compiler-rendered fence — ALWAYS THROWS (the error.nodeThrow + * polymorphic-result carve-out). May-throw seed. */ + "net.connectOptsChk": { argTypes: [DYN, STRING], result: VOID }, + "net.connectCb": { + argTypes: [F64, STRING, { kind: "func", params: [], ret: VOID }], + result: NETSOCKET_T, + }, + /** net.connect/createConnection(path[, connectListener]) and the + * `path` option: a client of the Unix socket at `path`. With the flag + * (a positional string), Node's isPipeName applies first: a string that + * reads as a non-negative number is a localhost port. */ + "net.connectPath": { argTypes: [STRING, BOOL], result: NETSOCKET_T }, + "net.connectPathCb": { + argTypes: [STRING, BOOL, { kind: "func", params: [], ret: VOID }], + result: NETSOCKET_T, + }, + // The lookup's exact func shape is program data (its answer callback's + // union/record types) — checked specially in the libCall case. + /** connect({ port, host, autoSelectFamily: true, lookup }) — the + * caller-resolver dial (portless's createLoopbackConnection): args + * [port, host, lookup]. The runtime creates the (connecting) socket + * handle, invokes the lookup as Node does — lookup(hostname, options, + * callback), options crossing as the dyn undefined — and the answer + * closure (an emitter-synthesized per-shape thunk over a boxed socket, + * the SNI-answer pattern) dials the answered addresses IN ORDER: each + * connect failure tries the next, the LAST failure's message is the + * socket's 'error' (Node's autoSelectFamily aggregate is a documented + * divergence), and a lookup error surfaces as the deferred socket + * 'error'. A synchronous throw INSIDE the lookup propagates out of the + * connect call (may-throw seed). */ + "net.connectLookup": { argTypes: [F64, STRING, null], result: NETSOCKET_T }, + /** connect({ host, port, lookup }) with a lookup of Node's own type, as + * a checked-dynamic function: the runtime calls it with { hints, all: + * true } and a callback taking an address string or list, then dials + * the answers in order. MAY THROW (host validation, a lookup throw). */ + "net.connectLookupDyn": { argTypes: [F64, STRING, DYN], result: NETSOCKET_T }, + /** socket.write: Node's Writable answer — false when the writes not yet + * completely sent, each counted in full, reach the 65536-byte + * high-water mark ('drain' follows), and on an ended or closed socket. */ + "net.sockWrite": { argTypes: [NETSOCKET_T, STRING], result: BOOL }, + "net.sockWriteBytes": { argTypes: [NETSOCKET_T, BYTES_U8], result: BOOL }, + "net.sockEnd": { argTypes: [NETSOCKET_T], result: VOID }, + "net.sockEndStr": { argTypes: [NETSOCKET_T, STRING], result: VOID }, + "net.sockEndBytes": { argTypes: [NETSOCKET_T, BYTES_U8], result: VOID }, + /** write/end with a CHECKED-DYNAMIC chunk (an untyped JS payload into a + * typed socket): the runtime dispatches STR/BYTES and throws Node's + * ERR_INVALID_ARG_TYPE chunk TypeError on any other kind. */ + "net.sockWriteDyn": { argTypes: [NETSOCKET_T, DYN], result: BOOL }, + "net.sockEndDyn": { argTypes: [NETSOCKET_T, DYN], result: VOID }, + "net.sockDestroy": { argTypes: [NETSOCKET_T], result: VOID }, + "net.sockPipe": { argTypes: [NETSOCKET_T, NETSOCKET_T], result: VOID }, + /** socket.pipe(res) — raw socket chunks into a ServerResponse body + * (the extended-CONNECT bridge leg): each chunk is a body write (the + * response's own framing applies); source EOF end()s the response, + * pipe's default. Borrows both. Never throws. */ + "net.sockPipeRes": { argTypes: [NETSOCKET_T, HTTPRES_T], result: VOID }, + "net.sockOnData": { argTypes: [NETSOCKET_T, null, BOOL], result: VOID }, + "net.sockOnEnd": { + argTypes: [NETSOCKET_T, { kind: "func", params: [], ret: VOID }, BOOL], + result: VOID, + }, + "net.sockOnClose": { + argTypes: [NETSOCKET_T, { kind: "func", params: [], ret: VOID }, BOOL], + result: VOID, + }, + "net.sockOnError": { argTypes: [NETSOCKET_T, null, BOOL], result: VOID }, + "net.sockOnConnect": { + argTypes: [NETSOCKET_T, { kind: "func", params: [], ret: VOID }, BOOL], + result: VOID, + }, + // node:dgram + node:dns (scr_dgram.c). The message/error listeners and + // dns.lookup's callback are program-dependent closures (null slots, + // checked in the libCall case); dgram.address's record result is + // program-dependent too (VOID here is the envGet sentinel — the libCall + // case checks the {address, family, port} shape). + /** node:dgram + node:dns (scr_dgram.c — linked, and + * scr_dgram_install() emitted, only when one of these appears on the + * IR; moduleUsesDgram is the switch — dns.lookup rides the same unit). + * The net listener discipline verbatim: receivers borrowed, CALLBACKS + * MOVE into the handle's registry and release at settlement, `on` vs + * `once` is the trailing bool, registrations are void. bind/connect + * bind NOW and defer 'listening'/'connect' to the next loop turn (the + * net.listen story); their optional-host completions are "" + * (bind → 0.0.0.0) and "127.0.0.1" (connect — udp4's Node default). + * bind/connect/send/close/address THROW Node's state errors ("Socket + * is already bound", "Already connected", "Not running") — may-throw + * seeded. dgram.address returns the AddressInfo RECORD (the frontend + * pins the {address, family, port} shape; the emitter builds it from + * runtime parts). onMessage adapters are emitted per rinfo record + * shape (the child.onExit precedent); onError reuses the child %Error + * adapters. dns.lookup resolves via getaddrinfo AT CALL TIME and + * defers the callback to the next turn (SEMANTICS.md documents the + * blocking divergence); its per-union adapter builds the + * `Error | null` first argument. */ + "dgram.createSocket": { argTypes: [BOOL], result: DGRAMSOCK_T }, + "dgram.bind": { argTypes: [DGRAMSOCK_T, F64, STRING], result: VOID }, + "dgram.bindCb": { + argTypes: [DGRAMSOCK_T, F64, STRING, { kind: "func", params: [], ret: VOID }], + result: VOID, + }, + "dgram.connect": { argTypes: [DGRAMSOCK_T, F64, STRING], result: VOID }, + "dgram.connectCb": { + argTypes: [DGRAMSOCK_T, F64, STRING, { kind: "func", params: [], ret: VOID }], + result: VOID, + }, + "dgram.sendStr": { argTypes: [DGRAMSOCK_T, STRING, F64, STRING], result: VOID }, + "dgram.sendBytes": { argTypes: [DGRAMSOCK_T, BYTES_U8, F64, STRING], result: VOID }, + /** Connected AF_UNIX datagram sockets for the hot-shots package shim + * (the unix-dgram addon's surface): fd or result >= 0, -errno on + * failure; send answers 1 when the socket would block. */ + "dgram.unixSocket": { argTypes: [], result: F64 }, + "dgram.unixConnect": { argTypes: [F64, STRING], result: F64 }, + "dgram.unixSend": { argTypes: [F64, BYTES_U8], result: F64 }, + "dgram.unixClose": { argTypes: [F64], result: F64 }, + /** The send argument-validation ladder over dyn arguments (Node's + * signature shuffle: slice bounds, list/type contracts, port/address + * validation, connected-state errors), then the send: lists, connected + * sends and completion callbacks included. Up to six arguments, the + * missing ones undefined. May-throw. */ + "dgram.sendChk": { argTypes: [DGRAMSOCK_T, DYN, DYN, DYN, DYN, DYN, DYN], result: VOID }, + "dgram.address": { argTypes: [DGRAMSOCK_T], result: VOID }, + "dgram.close": { argTypes: [DGRAMSOCK_T], result: VOID }, + "dgram.closeCb": { + argTypes: [DGRAMSOCK_T, { kind: "func", params: [], ret: VOID }], + result: VOID, + }, + "dgram.unref": { argTypes: [DGRAMSOCK_T], result: VOID }, + "dgram.ref": { argTypes: [DGRAMSOCK_T], result: VOID }, + /** The socket options (scr_dgram.c): the membership calls take the + * interface and whether it was given. May-throw. */ + "dgram.setBroadcast": { argTypes: [DGRAMSOCK_T, BOOL], result: VOID }, + "dgram.setTTL": { argTypes: [DGRAMSOCK_T, F64], result: F64 }, + "dgram.setMulticastTTL": { argTypes: [DGRAMSOCK_T, F64], result: F64 }, + "dgram.setMulticastLoopback": { argTypes: [DGRAMSOCK_T, BOOL], result: BOOL }, + "dgram.setMulticastInterface": { argTypes: [DGRAMSOCK_T, STRING], result: VOID }, + "dgram.addMembership": { argTypes: [DGRAMSOCK_T, STRING, STRING, BOOL], result: VOID }, + "dgram.dropMembership": { argTypes: [DGRAMSOCK_T, STRING, STRING, BOOL], result: VOID }, + "dgram.getRecvBufferSize": { argTypes: [DGRAMSOCK_T], result: F64 }, + "dgram.getSendBufferSize": { argTypes: [DGRAMSOCK_T], result: F64 }, + "dgram.setRecvBufferSize": { argTypes: [DGRAMSOCK_T, F64], result: VOID }, + "dgram.setSendBufferSize": { argTypes: [DGRAMSOCK_T, F64], result: VOID }, + "dgram.getSendQueueSize": { argTypes: [DGRAMSOCK_T], result: F64 }, + "dgram.getSendQueueCount": { argTypes: [DGRAMSOCK_T], result: F64 }, + "dgram.disconnect": { argTypes: [DGRAMSOCK_T], result: VOID }, + "dgram.onMessage": { argTypes: [DGRAMSOCK_T, null, BOOL], result: VOID }, + "dgram.onError": { argTypes: [DGRAMSOCK_T, null, BOOL], result: VOID }, + "dgram.onListening": { + argTypes: [DGRAMSOCK_T, { kind: "func", params: [], ret: VOID }, BOOL], + result: VOID, + }, + "dgram.onClose": { + argTypes: [DGRAMSOCK_T, { kind: "func", params: [], ret: VOID }, BOOL], + result: VOID, + }, + "dgram.onConnect": { + argTypes: [DGRAMSOCK_T, { kind: "func", params: [], ret: VOID }, BOOL], + result: VOID, + }, + "dns.lookup": { argTypes: [STRING, F64, null], result: VOID }, + /** Node's argument forms with dyn callbacks: lookup(hostname, options, + * callback) and lookupService(address, port, callback). */ + "dns.lookupDyn": { argTypes: [DYN, DYN, DYN, BOOL], result: VOID }, + "dns.lookupService": { argTypes: [DYN, DYN, DYN, BOOL], result: VOID }, + /** node:dns/promises: lookup(hostname, options) and lookupService(address, + * port) over the callback forms; the promise fulfills with the dyn + * { address, family } (array for all: true) / { hostname, service }. + * Argument errors throw synchronously (may-throw), unlike the + * util.promisify forms below. */ + "dns.promisesLookup": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, + "dns.promisesLookupService": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, + "dns.getServers": { argTypes: [], result: DYN }, + "dns.getDefaultResultOrder": { argTypes: [], result: STRING }, + "dns.setDefaultResultOrder": { argTypes: [DYN], result: VOID }, + // node:test (scr_test.c). Bodies are program-dependent closures (0 or + // 1 testCtx param, void or Promise result — the spoke pinned the + // shape): null slots. sub's result is the settled Promise the + // await consumes. + /** node:test (scr_test.c — linked only when one of these appears on + * the IR; moduleUsesNodeTest is the switch, and the main epilogue asks + * scr_test_exit_code() for the process's exit status). Strings are + * BORROWED, callbacks MOVE. register/suite/hook attach to the runner + * tree (register: name, mode 0|1|2 run/skip/todo, directive message "" + * = none, cb or absent via registerEmpty, flags 1 async | 2 takes-ctx + * | 4 only, "file:line:col"); suite runs its body AT registration + * (Node's collection phase). sub is t.test — runs the subtest INLINE + * on the runner fiber and returns the settled promise the await + * consumes. ctxSkip/ctxTodo mark the running test; ctxDiagnostic + * queues an ℹ line; ctxName reads t.name. Every registration keeps + * the loop-run emitted (usesTimers) so the runner fiber drains. */ + // The trailing three are the { plan, timeout, concurrency } options (plan + // -1 without one; timeout read under flag 8; concurrency read under flag + // 16, -1 for true). An out-of-range timeout or concurrency throws. + "test.register": { + argTypes: [STRING, F64, STRING, null, F64, STRING, F64, F64, F64], + result: VOID, + }, + "test.registerEmpty": { + argTypes: [STRING, F64, STRING, F64, STRING, F64, F64, F64], + result: VOID, + }, + // The suite body takes no parameters or its SuiteContext (flag 2). + "test.suite": { argTypes: [STRING, F64, STRING, null, STRING, F64, F64], result: VOID }, + // The trailing string is the hook call's "file:line:col". + "test.hook": { argTypes: [F64, null, F64, STRING], result: VOID }, + "test.sub": { + argTypes: [TESTCTX_T, STRING, F64, STRING, null, F64, STRING, F64, F64, F64], + result: { kind: "promise", inner: VOID }, + }, + "test.subEmpty": { + argTypes: [TESTCTX_T, STRING, F64, STRING, F64, STRING, F64, F64, F64], + result: VOID, + }, + "test.ctxSkip": { argTypes: [TESTCTX_T, STRING], result: VOID }, + "test.ctxTodo": { argTypes: [TESTCTX_T, STRING], result: VOID }, + "test.ctxDiagnostic": { argTypes: [TESTCTX_T, STRING], result: VOID }, + "test.ctxName": { argTypes: [TESTCTX_T], result: STRING }, + "test.ctxFullName": { argTypes: [TESTCTX_T], result: STRING }, + // t.passed, and t.workerId's Number(NODE_TEST_WORKER_ID) with 0 for + // undefined. + "test.ctxPassed": { argTypes: [TESTCTX_T], result: BOOL }, + "test.workerId": { argTypes: [], result: F64 }, + // getTestContext(): whether a test, suite or hook is running in the + // current async context, and that context. + "test.hasContext": { argTypes: [], result: BOOL }, + "test.context": { argTypes: [], result: TESTCTX_T }, + // t.before/t.after/t.beforeEach/t.afterEach (which: 0 before, 1 after, + // 2 beforeEach, 3 afterEach); a throwing t.before is captured on the test. + "test.ctxHook": { argTypes: [TESTCTX_T, F64, null, F64], result: VOID }, + // t.plan(count), and the count of one t.assert call. + "test.ctxPlan": { argTypes: [TESTCTX_T, F64], result: VOID }, + "test.ctxCount": { argTypes: [TESTCTX_T], result: VOID }, + // The mock trackers: the module's `mock` and t.mock keep each mock's + // restore closure (moved) for restoreAll and reset. + "test.mockTrack": { argTypes: [{ kind: "func", params: [], ret: VOID }], result: VOID }, + "test.mockRestoreAll": { argTypes: [], result: VOID }, + "test.mockReset": { argTypes: [], result: VOID }, + "test.ctxMockTrack": { + argTypes: [TESTCTX_T, { kind: "func", params: [], ret: VOID }], + result: VOID, + }, + "test.ctxMockRestoreAll": { argTypes: [TESTCTX_T], result: VOID }, + "test.ctxMockReset": { argTypes: [TESTCTX_T], result: VOID }, + // mock.timers on the module's `mock` and on t.mock. enable takes one + // code per listed API ('T' setTimeout, 'I' setInterval), the `now` + // value and its flags (1 given, 2 from a Date). enable, tick, runAll and + // setTime throw Node's errors; tick and runAll run timer callbacks. + // t.assert.snapshot and fileSnapshot: the escaped, wrapped serialized + // value, and the stored value it is compared with (snapshot: the test, + // the value, the snapshot file; fileSnapshot: the value, the path). + // Both read or, in update mode, record; missing and unreadable + // snapshots throw Node's ERR_INVALID_STATE. + "test.snapshotValue": { argTypes: [STRING], result: STRING }, + "test.ctxSnapshot": { argTypes: [TESTCTX_T, STRING, STRING], result: STRING }, + "test.fileSnapshot": { argTypes: [STRING, STRING], result: STRING }, + // Node's serializeValue over a dyn array of serializers (undefined for + // the default JSON.stringify(value, null, 2)); a throw becomes Node's + // "The provided serializers did not generate a string." ERR_INVALID_STATE. + "test.snapshotSerialize": { argTypes: [DYN, DYN], result: DYN }, + "test.mockTimersEnable": { argTypes: [STRING, F64, F64], result: VOID }, + "test.mockTimersTick": { argTypes: [F64], result: VOID }, + "test.mockTimersRunAll": { argTypes: [], result: VOID }, + "test.mockTimersReset": { argTypes: [], result: VOID }, + "test.mockTimersSetTime": { argTypes: [F64], result: VOID }, + "test.ctxMockTimersEnable": { argTypes: [TESTCTX_T, STRING, F64, F64], result: VOID }, + "test.ctxMockTimersTick": { argTypes: [TESTCTX_T, F64], result: VOID }, + "test.ctxMockTimersRunAll": { argTypes: [TESTCTX_T], result: VOID }, + "test.ctxMockTimersReset": { argTypes: [TESTCTX_T], result: VOID }, + "test.ctxMockTimersSetTime": { argTypes: [TESTCTX_T, F64], result: VOID }, + // node:http (scr_http.c over scr_net.c). The handler and the data + // listeners are program-dependent closures (null slots, checked in the + // libCall case); reqHeader's result is the interned string|undefined + // union, checked like process.envGet. + /** node:http, the server slice (scr_http.c over scr_net.c — linked + * only when these appear on the IR; moduleUsesHttpServer is the + * switch, and any http.* libCall also counts as net use so scr_net.c + * links and installs). http.createServer's handler MOVES in and takes + * the runtime adapters for its (req, res) / (req) / () shapes; req + * body listeners follow the net.sockOnData story (bytes chunks, once + * flags); reqHeader answers the interned `string | undefined` union + * exactly like process.envGet; the res writers are borrowed-argument + * voids with Node's framing decided in the runtime (Content-Length + * for end-before-head, chunked after an explicit writeHead/write). */ + "http.createServer": { argTypes: [null], result: NETSERVER_T }, + /** http.createServer() / http.Server() with no handler — the + * on("request") route; option helpers set parser behavior on a fresh + * server before the constructor/factory result is returned. */ + "http.createServerEmpty": { argTypes: [], result: NETSERVER_T }, + "http.validateHeaderName": { argTypes: [STRING, STRING], result: VOID }, + "http.validateHeaderValue": { argTypes: [STRING, DYN], result: VOID }, + "http.serverJoinDupHeaders": { argTypes: [NETSERVER_T], result: VOID }, + "http.serverAllowMissingHostHeader": { argTypes: [NETSERVER_T], result: VOID }, + /** The five writable numeric http.Server timeout fields use one + * selector ABI: 0 timeout, 1 keepAliveTimeout, 2 headersTimeout, + * 3 requestTimeout, 4 keepAliveTimeoutBuffer. These calls store/read + * the property values; timer enforcement remains outside this surface. + * The constructor-option setter takes dyn so explicit undefined remains + * distinguishable from a numeric value until its Node validation ladder. */ + "http.serverTimeoutGet": { argTypes: [NETSERVER_T, F64], result: F64 }, + "http.serverSetTimeout": { argTypes: [NETSERVER_T, F64], result: VOID }, + "http.serverSetTimeoutCb": { argTypes: [NETSERVER_T, F64, null], result: VOID }, + "http.serverOnTimeout": { argTypes: [NETSERVER_T, null, BOOL], result: VOID }, + "http.serverCloseAllConnections": { argTypes: [NETSERVER_T], result: VOID }, + "http.serverCloseIdleConnections": { argTypes: [NETSERVER_T], result: VOID }, + "http.serverTimeoutSet": { argTypes: [NETSERVER_T, F64, F64], result: VOID }, + "http.serverTimeoutOptionSet": { argTypes: [NETSERVER_T, F64, DYN], result: VOID }, + /** server.on("listening", cb) — the deferred listen-callback list. */ + "net.serverOnListening": { + argTypes: [NETSERVER_T, { kind: "func", params: [], ret: VOID }, BOOL], + result: VOID, + }, + /** The ServerResponse member surface: statusCode/statusMessage reads + * and assignments (Node's writable properties), the header CRUD trio + * (getHeader answers `string | undefined` like reqHeader), and + * end(cb)'s callback slot (resOnEndCallback registers, the end call + * follows — the callback fires deferred, just before Node's 'finish' + * emit; resOnFinish is the 'finish' listener slot). writeHead's + * statusMessage forms compose in interned helpers: resStatusMsgSet + * then the ordinary writeHead entry. */ + "http.resStatusGet": { argTypes: [HTTPRES_T], result: F64 }, + "http.resStatusSet": { argTypes: [HTTPRES_T, F64], result: VOID }, + "http.resStatusMsgGet": { argTypes: [HTTPRES_T], result: VOID }, + "http.resStatusMsgSet": { argTypes: [HTTPRES_T, STRING], result: VOID }, + "http.resRequest": { argTypes: [HTTPRES_T], result: HTTPREQ_T }, + "http.resSocket": { argTypes: [HTTPRES_T], result: VOID }, + "http.resWritableFinished": { argTypes: [HTTPRES_T], result: BOOL }, + "http.resSendDateGet": { argTypes: [HTTPRES_T], result: BOOL }, + "http.resSendDateSet": { argTypes: [HTTPRES_T, BOOL], result: VOID }, + "http.resStrictContentLengthGet": { argTypes: [HTTPRES_T], result: BOOL }, + "http.resStrictContentLengthSet": { argTypes: [HTTPRES_T, BOOL], result: VOID }, + "http.resSetTimeout": { argTypes: [HTTPRES_T, F64], result: VOID }, + "http.resSetTimeoutCb": { + argTypes: [HTTPRES_T, F64, { kind: "func", params: [], ret: VOID }], + result: VOID, + }, + // resGetHeader answers the interned `string | undefined` union — the + // reqHeader/envGet sentinel pattern (VOID here, checked specially). + "http.resGetHeader": { argTypes: [HTTPRES_T, STRING], result: VOID }, + /** getHeader as Node answers it: the number, string or string[] that was + * set, or undefined, as a checked-dynamic value. */ + "http.resGetHeaderDyn": { argTypes: [HTTPRES_T, STRING], result: DYN }, + "http.resGetHeaderNames": { argTypes: [HTTPRES_T], result: arrayOf(STRING) }, + "http.resGetRawHeaderNames": { argTypes: [HTTPRES_T], result: arrayOf(STRING) }, + "http.resGetHeaders": { argTypes: [HTTPRES_T], result: DYN }, + "http.resHasHeader": { argTypes: [HTTPRES_T, STRING], result: BOOL }, + "http.resRemoveHeader": { argTypes: [HTTPRES_T, STRING], result: VOID }, + "http.resOnFinish": { + argTypes: [HTTPRES_T, { kind: "func", params: [], ret: VOID }], + result: VOID, + }, + "http.resOnEndCallback": { + argTypes: [HTTPRES_T, { kind: "func", params: [], ret: VOID }], + result: VOID, + }, + "http.reqUrl": { argTypes: [HTTPREQ_T], result: STRING }, + "http.reqMethod": { argTypes: [HTTPREQ_T], result: STRING }, + "http.reqHeader": { argTypes: [HTTPREQ_T, STRING], result: VOID }, + "http.reqTrailer": { argTypes: [HTTPREQ_T, STRING], result: VOID }, + "http.reqHeaderValues": { argTypes: [HTTPREQ_T, STRING], result: VOID }, + "http.reqTrailerValues": { argTypes: [HTTPREQ_T, STRING], result: VOID }, + "http.reqOnData": { argTypes: [HTTPREQ_T, null, BOOL], result: VOID }, + "http.reqOnEnd": { + argTypes: [HTTPREQ_T, { kind: "func", params: [], ret: VOID }, BOOL], + result: VOID, + }, + "http.resSetHeader": { argTypes: [HTTPRES_T, STRING, STRING], result: VOID }, + /** setHeader / appendHeader with a number, string or array value; the + * stored value keeps its type for getHeader/getHeaders. MAY THROW. */ + "http.resSetHeaderDyn": { argTypes: [HTTPRES_T, STRING, DYN], result: VOID }, + "http.resAppendHeader": { argTypes: [HTTPRES_T, STRING, DYN], result: VOID }, + "http.resWriteHead": { argTypes: [HTTPRES_T, F64], result: VOID }, + "http.resWriteHeadN": { + argTypes: [HTTPRES_T, F64, arrayOf(STRING), arrayOf(STRING)], + result: VOID, + }, + /** HTTP/1.1 informational heads; early hints takes flat string pairs, + * with a lowercase `link` key required to send anything. */ + "http.resWriteContinue": { argTypes: [HTTPRES_T], result: VOID }, + "http.resWriteProcessing": { argTypes: [HTTPRES_T], result: VOID }, + "http.resWriteEarlyHints": { argTypes: [HTTPRES_T, arrayOf(STRING)], result: VOID }, + "http.resWrite": { argTypes: [HTTPRES_T, STRING], result: VOID }, + "http.resWriteBytes": { argTypes: [HTTPRES_T, BYTES_U8], result: VOID }, + "http.resEnd": { argTypes: [HTTPRES_T], result: VOID }, + "http.resEndStr": { argTypes: [HTTPRES_T, STRING], result: VOID }, + "http.resEndBytes": { argTypes: [HTTPRES_T, BYTES_U8], result: VOID }, + /** The checked-dynamic chunk twins (the net.sockWriteDyn story). */ + "http.resWriteDyn": { argTypes: [HTTPRES_T, DYN], result: VOID }, + "http.resEndDyn": { argTypes: [HTTPRES_T, DYN], result: VOID }, + "http.resHeadersSent": { argTypes: [HTTPRES_T], result: BOOL }, + "http.resWritableEnded": { argTypes: [HTTPRES_T], result: BOOL }, + "http.resFlushHeaders": { argTypes: [HTTPRES_T], result: VOID }, + "http.resAddTrailers": { argTypes: [HTTPRES_T, arrayOf(STRING)], result: VOID }, + "http.resCork": { argTypes: [HTTPRES_T], result: VOID }, + "http.resUncork": { argTypes: [HTTPRES_T], result: VOID }, + "http.resWritableCorked": { argTypes: [HTTPRES_T], result: F64 }, + // The member follow-ups: reqStatusCode's `number | undefined` and + // sockRemoteAddress's `string | undefined` results are shape-checked in + // the special cases in validate.ts (like reqHeader/columns). + /** The server-surface member follow-ups: reqStatusCode answers the + * interned `number | undefined` union (negative = the undefined arm — + * a SERVER request, where Node's statusCode is undefined; every client + * response carries a real status); reqSocket is the underlying + * connection (+1, the same handle net.connect would give); + * sockRemoteAddress answers `string | undefined` (NULL after the fd + * closed, Node's destroyed-socket undefined; a dual-stack accept of an + * IPv4 peer reads "::ffff:a.b.c.d" like Node). reqResume/reqPause/reqDestroy and + * the req error/close listener slots complete the IncomingMessage + * surface portless's client responses use; resDestroy/resOnClose and + * resWriteHeadPairs ([k0,v0,k1,v1,...] — the env.pairs helper's flat + * shape) complete ServerResponse. sockSetTimeout arms the idle + * EVFILT_TIMER ('timeout' fires after ms of inactivity, once per idle + * period, never destroying the socket — Node's semantics). */ + "http.reqStatusCode": { argTypes: [HTTPREQ_T], result: VOID }, + // reqStatusMessage's `string | undefined` is program-interned like + // reqHeader's (the VOID here is the envGet sentinel). + "http.reqStatusMessage": { argTypes: [HTTPREQ_T], result: VOID }, + /** req.rawHeaders — [name, value, name, value, ...] in arrival order, + * names in their ORIGINAL case (Node's shape); a fresh string[] per + * read. reqStatusMessage answers the interned `string | undefined` + * union — the reason phrase on client responses ("" when the status + * line carried none), the undefined arm on server requests (the + * statusCode split). sockDestroyed is socket.destroyed — true once the + * fd is gone (destroy() or full close). */ + "http.reqRawHeaders": { argTypes: [HTTPREQ_T], result: arrayOf(STRING) }, + /** The `{ ...req.headers }` snapshot feed: [lowercased name, value, + * ...] pairs in arrival order — the interned %headers.snapshot helper + * builds the record over it, exactly the process.envPairs pattern. */ + "http.reqHeaderPairs": { argTypes: [HTTPREQ_T], result: arrayOf(STRING) }, + /** Every value of one header in arrival order (empty when absent): the + * set-cookie array of a `{ ...req.headers }` snapshot. */ + "http.reqHeaderList": { argTypes: [HTTPREQ_T, STRING], result: arrayOf(STRING) }, + "http.reqRawTrailers": { argTypes: [HTTPREQ_T], result: arrayOf(STRING) }, + "http.reqTrailerPairs": { argTypes: [HTTPREQ_T], result: arrayOf(STRING) }, + "net.sockDestroyed": { argTypes: [NETSOCKET_T], result: BOOL }, + /** socket.writable — the write half is open: no end() yet, no FIN sent, + * fd alive (connecting sockets answer true; writes queue). Node's + * stream flag. Borrows; never throws. */ + "net.sockWritable": { argTypes: [NETSOCKET_T], result: BOOL }, + // The 'upgrade' registrations: the callback shapes are program-typed + // ((req, socket, head) and shorter prefixes — the libCall case checks). + /** The 'upgrade' events, both sides (SEMANTICS.md — the WebSocket + * proxying surface): serverOnUpgrade registers (req, socket, head) + * listeners fired INSTEAD of 'request' for Connection: upgrade + * requests (the parser steps aside; the socket is raw; `head` carries + * bytes past the request head; no listener = the socket destroys, + * Node's default). clientOnUpgrade is the client twin: a 101 response + * fires (res, socket, head) INSTEAD of 'response'. */ + "http.serverOnUpgrade": { argTypes: [NETSERVER_T, null, BOOL], result: VOID }, + /** server.on("connect", ...) — HTTP CONNECT tunneling, the 'upgrade' + * machinery's twin: (req, socket, head) fired INSTEAD of 'request' for + * CONNECT-method requests (no listener = the socket destroys, Node's + * default). The h2 compat server's 'connect' (portless's RFC 8441 + * handler) only ever sees the HTTP/1.1 arm under the allowHTTP1 + * lowering, so a listener whose second parameter is a UNION with a + * netSocket arm takes the socket wrapped at that arm (an emitted + * per-shape adapter — the tags are program data). */ + "http.serverOnConnect": { argTypes: [NETSERVER_T, null, BOOL], result: VOID }, + "http.clientOnUpgrade": { argTypes: [HTTPCLIENTREQ_T, null, BOOL], result: VOID }, + "http.reqSocket": { argTypes: [HTTPREQ_T], result: NETSOCKET_T }, + // reqH2Stream's program-interned Http2Stream | undefined result is + // checked in the special libCall cases in validate.ts. + "http.reqH2Stream": { argTypes: [HTTPREQ_T], result: VOID }, + "http.reqH2StreamOrThrow": { argTypes: [HTTPREQ_T, STRING], result: HTTP2STREAM_T }, + /** req.pipe(dest) — the IncomingMessage body streaming into a + * ServerResponse (the proxy's response leg), a ClientRequest (the + * request-body forward), or a raw socket (the upgrade-rejection leg); + * chunk-for-chunk, natural end ends the destination (Node's pipe + * default; no backpressure — divergence 54's stream model). */ + "http.reqPipeRes": { argTypes: [HTTPREQ_T, HTTPRES_T], result: VOID }, + "http.reqPipeClient": { argTypes: [HTTPREQ_T, HTTPCLIENTREQ_T], result: VOID }, + "http.reqPipeSock": { argTypes: [HTTPREQ_T, NETSOCKET_T], result: VOID }, + "http.reqResume": { argTypes: [HTTPREQ_T], result: VOID }, + "http.reqPause": { argTypes: [HTTPREQ_T], result: VOID }, + "http.reqDestroy": { argTypes: [HTTPREQ_T], result: VOID }, + "http.reqOnError": { argTypes: [HTTPREQ_T, null, BOOL], result: VOID }, + "http.reqOnClose": { + argTypes: [HTTPREQ_T, { kind: "func", params: [], ret: VOID }, BOOL], + result: VOID, + }, + "http.reqOnAborted": { + argTypes: [HTTPREQ_T, { kind: "func", params: [], ret: VOID }, BOOL], + result: VOID, + }, + "http.reqHttpVersion": { argTypes: [HTTPREQ_T], result: STRING }, + "http.reqHttpVersionMajor": { argTypes: [HTTPREQ_T], result: F64 }, + "http.reqHttpVersionMinor": { argTypes: [HTTPREQ_T], result: F64 }, + "http.reqAborted": { argTypes: [HTTPREQ_T], result: BOOL }, + "http.reqComplete": { argTypes: [HTTPREQ_T], result: BOOL }, + "http.resDestroy": { argTypes: [HTTPRES_T], result: VOID }, + "http.resOnClose": { + argTypes: [HTTPRES_T, { kind: "func", params: [], ret: VOID }, BOOL], + result: VOID, + }, + "http.resWriteHeadPairs": { argTypes: [HTTPRES_T, F64, arrayOf(STRING)], result: VOID }, + /** writeHead(status, headers) with a checked-dynamic headers value — + * the runtime OBJ walk (string/number values; other values throw + * Node's invalid-argument TypeError; may throw). */ + "http.resWriteHeadDyn": { argTypes: [HTTPRES_T, F64, DYN], result: VOID }, + "net.sockSetTimeout": { argTypes: [NETSOCKET_T, F64], result: VOID }, + /** setEncoding('utf8') — 'data' delivers strings inside the chunk-encoding window; other real encodings throw the "not supported yet" Error, unknown names throw ERR_UNKNOWN_ENCODING (may throw). */ + "net.sockSetEncoding": { argTypes: [NETSOCKET_T, STRING], result: VOID }, + "http.reqSetEncoding": { argTypes: [HTTPREQ_T, STRING], result: VOID }, + "net.sockOnTimeout": { + argTypes: [NETSOCKET_T, { kind: "func", params: [], ret: VOID }, BOOL], + result: VOID, + }, + /** on('drain') / once('drain'): fires after a write answered false, once + * the write buffer empties. */ + "net.sockOnDrain": { + argTypes: [NETSOCKET_T, { kind: "func", params: [], ret: VOID }, BOOL], + result: VOID, + }, + "net.sockRemoteAddress": { argTypes: [NETSOCKET_T], result: VOID }, + /** socket.encrypted — `boolean | undefined`: the true arm iff the + * socket carries a TLS transport (Node types `encrypted: true` on + * TLSSocket; plain sockets answer undefined — the proxy.ts isEncrypted + * idiom reads it through a cast). */ + "net.sockEncrypted": { argTypes: [NETSOCKET_T], result: VOID }, + // node:http, the client slice. The response callback shapes are + // program-dependent (checked by validate.ts); header pairs arrive flat. + /** node:http, the CLIENT slice (http.request/http.get over the net + * client machinery): request/requestCb take (host, port, path, method, + * timeoutMs, headerPairs, autoEnd[, responseCb]) — headerPairs is the + * flat [k0,v0,...] array (empty for none), autoEnd true is http.get's + * eager end(). The handle owns one dialed connection (NO pooling — the + * wire still carries Node's exact head: user headers, then Host, + * Connection: keep-alive, and the framing header; the socket closes + * when the response completes). The response delivered to responseCb / + * 'response' listeners IS an httpReq (IncomingMessage), status and + * headers parsed from the wire, body via reqOnData/reqOnEnd. Errors are + * Node-shaped ('connect ECONNREFUSED ip:port', 'socket hang up') and + * fire 'error' then 'close'; an unhandled 'error' exits 1 like every + * net handle. clientWrite before end commits to chunked framing unless + * the caller set content-length; clientEnd(data) before any write sends + * Content-Length exactly like Node. */ + "http.request": { + argTypes: [STRING, F64, STRING, STRING, F64, arrayOf(STRING), BOOL], + result: HTTPCLIENTREQ_T, + }, + "http.requestCb": { + argTypes: [STRING, F64, STRING, STRING, F64, arrayOf(STRING), BOOL, null], + result: HTTPCLIENTREQ_T, + }, + /** new http.Agent(opts) / new https.Agent(opts): (secure, keepAlive, + * keepAliveMsecs, maxSockets, maxFreeSockets, timeoutMs, port) — the + * numeric options arrive < 0 for "unset" (Infinity/256/none; port + * seeds the settable defaultPort, Node's option merge). Returns the + * Agent as a checked-dynamic HANDLE (getName/destroy and the + * sockets/requests/freeSockets counters dispatch through the dyn + * handle ops). keepAlive: true THROWS the named construction fence — + * socket POOLING is not modeled (one dial per request); maxSockets + * accounting is real: over-limit requests defer their dial and queue. + * MAY THROW. */ + "http.agentNew": { argTypes: [BOOL, BOOL, F64, F64, F64, F64, F64, DYN], result: DYN }, + /** http.globalAgent / https.globalAgent (true): Node's keepAlive global + * agent, the one requests without an agent option use. */ + "http.globalAgent": { argTypes: [BOOL], result: DYN }, + /** http.request's `signal` option over the fresh request: an aborted + * signal destroys it with Node's AbortError now, a later abort event + * does the same. A falsy value is ignored; a non-signal throws + * ERR_INVALID_ARG_TYPE. */ + "http.clientSignal": { argTypes: [HTTPCLIENTREQ_T, DYN], result: VOID }, + /** options.signal checked before the request is built (ERR_INVALID_ARG_TYPE). */ + "http.clientSignalCheck": { argTypes: [DYN], result: VOID }, + /** https.request's servername option, stored for the construction call + * that follows: the TLS name sent and verified, and part of the pooled + * socket key. The "https." prefix keeps the TLS unit linked. */ + "https.nextServername": { argTypes: [STRING], result: VOID }, + /** The maxHeaderSize option of createServer / http.request: Node's + * validateInteger (ERR_OUT_OF_RANGE) then the header size bound. + * MAY THROW. */ + "http.serverMaxHeaderSize": { argTypes: [NETSERVER_T, F64], result: VOID }, + "http.clientMaxHeaderSize": { argTypes: [HTTPCLIENTREQ_T, F64], result: VOID }, + /** req.destroy(err): err becomes the request's 'error'. */ + "http.clientDestroyError": { + argTypes: [HTTPCLIENTREQ_T, { kind: "object", className: "%Error" }], + result: VOID, + }, + /** The agent-threaded request rows: the http.request/https.request + * shape with a trailing `agent` dyn argument (an Agent handle, false — + * the one-shot Connection: close dial — or null/undefined for the + * default path). port < 0 means "no port option": the agent's settable + * defaultPort, then the scheme's. MAY THROW (a non-Agent value is + * Node's ERR_INVALID_ARG_TYPE). */ + "http.requestAgent": { + argTypes: [STRING, F64, STRING, STRING, F64, arrayOf(STRING), BOOL, DYN], + result: HTTPCLIENTREQ_T, + }, + "http.requestAgentCb": { + argTypes: [STRING, F64, STRING, STRING, F64, arrayOf(STRING), BOOL, DYN, null], + result: HTTPCLIENTREQ_T, + }, + /** request/get with a URL-STRING first argument: the runtime parses it + * (WHATWG) and dials — throws catchably on an unparsable input or a + * non-http scheme. */ + "http.requestUrl": { argTypes: [STRING, STRING, BOOL], result: HTTPCLIENTREQ_T }, + "http.requestUrlCb": { argTypes: [STRING, STRING, BOOL, null], result: HTTPCLIENTREQ_T }, + /** request/get with a runtime options record: a RequestOptions + * variable, or a literal that spreads one or sets socketPath. Args + * (options, autoEnd[, cb]). The runtime reads the record in the + * ClientRequest constructor's order (host/port or socketPath, path, + * method, headers, timeout, agent, signal, maxHeaderSize) with Node's + * errors, and documented options it does not implement throw the + * runtime fence. The "https." pair adds the TLS options. MAY THROW. */ + "http.requestDyn": { argTypes: [DYN, BOOL], result: HTTPCLIENTREQ_T }, + "http.requestDynCb": { argTypes: [DYN, BOOL, null], result: HTTPCLIENTREQ_T }, + "https.requestDyn": { argTypes: [DYN, BOOL], result: HTTPCLIENTREQ_T }, + "https.requestDynCb": { argTypes: [DYN, BOOL, null], result: HTTPCLIENTREQ_T }, + /** The paused-mode demux surface (portless's first-byte TLS peek: + * once('readable') + read(1) + unshift + emit('connection')). + * sockOnReadable registers a zero-param listener (a consumer: arrived + * bytes buffer instead of flowing, and EOF announces too); sockRead + * answers `Buffer | null` (exactly n buffered bytes, or null — Node's + * less-than-n answer; n <= 0 drains everything); sockUnshift returns + * bytes to the front of the stream; serverEmitConnection routes a + * socket into another server's protocol layer (a TLS target's + * 'connection' waits for its handshake). */ + "net.sockOnReadable": { argTypes: [NETSOCKET_T, null, BOOL], result: VOID }, + // sockRead's result is the interned `Buffer | null` union — checked + // specially in validate.ts (the reqHeader/envGet pattern; VOID here is a + // placeholder the special case overrides). + "net.sockRead": { argTypes: [NETSOCKET_T, F64], result: VOID }, + "net.sockUnshift": { argTypes: [NETSOCKET_T, BYTES_U8], result: VOID }, + /** Socket flow control and the compat surface: pause/resume (reads + * gate off/on — kernel backpressure holds paused bytes; resume flows + * and discards sans listeners) and setNoDelay answer the SOCKET (+1, + * Node's chaining); destroySoon ends now and destroys once the FIN + * flushed; bytesWritten counts accepted bytes; readable is true until + * the read half ends. */ + "net.sockPause": { argTypes: [NETSOCKET_T], result: NETSOCKET_T }, + "net.sockResume": { argTypes: [NETSOCKET_T], result: NETSOCKET_T }, + "net.sockSetNoDelay": { argTypes: [NETSOCKET_T, BOOL], result: NETSOCKET_T }, + "net.sockDestroySoon": { argTypes: [NETSOCKET_T], result: VOID }, + /** RST close, answers the socket; throws ERR_INVALID_HANDLE_TYPE on TLS. */ + "net.sockResetAndDestroy": { argTypes: [NETSOCKET_T], result: NETSOCKET_T }, + /** socket.address(): { address, family, port } or {} as a dyn. */ + "net.sockAddress": { argTypes: [NETSOCKET_T], result: DYN }, + "net.sockBytesWritten": { argTypes: [NETSOCKET_T], result: F64 }, + /** socket.writableLength: Node's state.length, the length of every + * write not yet completely sent. */ + "net.sockWritableLength": { argTypes: [NETSOCKET_T], result: F64 }, + /** socket.writableNeedDrain: a write answered false and 'drain' has not + * fired, with the write half open. */ + "net.sockWritableNeedDrain": { argTypes: [NETSOCKET_T], result: BOOL }, + /** remotePort (0), remoteFamily (1), localAddress (2), localPort (3), + * localFamily (4): dyn values, undefined when unknown. */ + "net.sockAddrField": { argTypes: [NETSOCKET_T, F64], result: DYN }, + "net.sockBytesRead": { argTypes: [NETSOCKET_T], result: F64 }, + "net.sockConnecting": { argTypes: [NETSOCKET_T], result: BOOL }, + "net.sockPending": { argTypes: [NETSOCKET_T], result: BOOL }, + "net.sockReadyState": { argTypes: [NETSOCKET_T], result: STRING }, + "net.sockSetKeepAlive": { argTypes: [NETSOCKET_T, BOOL, F64], result: NETSOCKET_T }, + "net.sockOnReady": { + argTypes: [NETSOCKET_T, { kind: "func", params: [], ret: VOID }, BOOL], + result: VOID, + }, + /** Listeners with a parameter, as dyn functions: close(hadError), + * write's cb(err) and end's cb. */ + "net.sockOnCloseDyn": { argTypes: [NETSOCKET_T, DYN, BOOL], result: VOID }, + /** socket.on/once('lookup', listener): the listener (a dyn function) + * gets (err, address, family, host) once per resolved address, or once + * with the lookup error, before the connect. */ + "net.sockOnLookup": { argTypes: [NETSOCKET_T, DYN, BOOL], result: VOID }, + "net.sockOnWriteFlush": { + argTypes: [NETSOCKET_T, { kind: "func", params: [], ret: VOID }], + result: VOID, + }, + "net.sockOnWriteFlushDyn": { argTypes: [NETSOCKET_T, DYN], result: VOID }, + "net.sockOnFinishDyn": { argTypes: [NETSOCKET_T, DYN], result: VOID }, + "net.sockSetTimeoutCb": { + argTypes: [NETSOCKET_T, F64, { kind: "func", params: [], ret: VOID }], + result: VOID, + }, + "net.isIP": { argTypes: [DYN], result: F64 }, + "net.isIPv4": { argTypes: [DYN], result: BOOL }, + "net.isIPv6": { argTypes: [DYN], result: BOOL }, + "net.serverMaxConnectionsGet": { argTypes: [NETSERVER_T], result: DYN }, + "net.serverMaxConnectionsSet": { argTypes: [NETSERVER_T, DYN], result: VOID }, + "net.serverDropMaxConnectionGet": { argTypes: [NETSERVER_T], result: DYN }, + "net.serverDropMaxConnectionSet": { argTypes: [NETSERVER_T, DYN], result: VOID }, + /** net.SocketAddress and net.BlockList (scr_net.c): the constructors + * take the argument array; the handles are dyn values. */ + "net.socketAddressNew": { argTypes: [DYN], result: DYN }, + "net.socketAddressParse": { argTypes: [DYN], result: DYN }, + "net.isSocketAddress": { argTypes: [DYN], result: BOOL }, + "net.blockListNew": { argTypes: [DYN], result: DYN }, + "net.isBlockList": { argTypes: [DYN], result: BOOL }, + "net.blockListPrivateRanges": { argTypes: [], result: DYN }, + "net.serverOnDrop": { argTypes: [NETSERVER_T, DYN, BOOL], result: VOID }, + "net.serverGetConnections": { argTypes: [NETSERVER_T, DYN], result: VOID }, + "net.sockReadable": { argTypes: [NETSOCKET_T], result: BOOL }, + /** socket.on('finish', cb) / end(cb): fires once when the FIN goes out + * (sweep-deferred, never the registering stack). */ + "net.sockOnFinish": { + argTypes: [NETSOCKET_T, { kind: "func", params: [], ret: VOID }], + result: VOID, + }, + "net.serverEmitConnection": { argTypes: [NETSERVER_T, NETSOCKET_T], result: VOID }, + // tls/https: cert/key/ca PEM arguments are strings OR Buffers (null = + // both accepted; the emitter passes data+len either way). + /** node:tls + node:https (scr_tls.c over scr_net.c/scr_http.c, with + * the vendored mbedTLS archive — linked only when one of these appears + * on the IR; moduleUsesTls is the switch, and every tls/https libCall + * also counts as net AND http use so both units link and install). + * tls.createServer takes (cert, key[, handler]) — cert/key are PEM + * strings or Buffers (the emitter passes data+len for either); the + * handler is Node's 'secureConnection' (fires post-handshake with a + * socket that behaves exactly like a net socket, the same adapters as + * net.createServer). https.createServer is (cert, key, handler) with + * the http request-handler adapters. https.request/requestCb extend + * the http client row with (…, rejectUnauthorized: bool, ca: PEM + * string/Buffer — "" for none) and default port 443; everything else + * (write/end/destroy/events, the response surface) IS the http client + * surface — the handles are the same kinds. */ + "tls.createServer": { argTypes: [null, null], result: NETSERVER_T }, + "tls.createServerCb": { argTypes: [null, null, null], result: NETSERVER_T }, + /** The SNICallback forms: (cert, key, SNICallback[, handler]). The + * callback is http2.createSecureServerSni's shape, a function or a + * function | undefined union. */ + "tls.createServerSni": { argTypes: [null, null, null], result: NETSERVER_T }, + "tls.createServerSniCb": { argTypes: [null, null, null, null], result: NETSERVER_T }, + // The runtime options records (divergence 66's stance): a dyn options + // value whose members read at runtime; pemDyn extracts a runtime-valued + // cert/key member (arg 1 is the precomposed fence label). + /** RUNTIME options records (the divergence-66 stance): the *Dyn + * creators take a checked-dynamic (dyn) options value whose members + * read at runtime — cert/key extract like the literal path, members + * whose literal forms fence THROW the catchable fence at runtime, and + * undocumented keys drop like Node drops them. tls.pemDyn is the + * literal walk's runtime-valued cert/key extraction: (value, whatLit) + * → PEM bytes (strings/Buffers/one-element arrays of those) or the + * thrown fence. All of them may throw. */ + "tls.pemDyn": { argTypes: [DYN, STRING], result: BYTES_U8 }, + "tls.createServerDyn": { argTypes: [DYN], result: NETSERVER_T }, + "tls.createServerDynCb": { argTypes: [DYN, null], result: NETSERVER_T }, + "https.createServerDyn": { argTypes: [DYN], result: NETSERVER_T }, + "https.createServerDynCb": { argTypes: [DYN, null], result: NETSERVER_T }, + /** createSecureServer(options, handler) — the eager COMPAT handler as + * the first 'request' listener (Node's exact route), on both literal + * flavors: Req is the dual ALPN allowHTTP1 server, H2Req the ALPN=h2-only + * server (both use compat handles over h2 streams). Args are + * (cert, key, cb, enableConnectProtocol). */ + "http2.createSecureServerReq": { argTypes: [null, null, null, BOOL], result: NETSERVER_T }, + "http2.createSecureServerH2Req": { argTypes: [null, null, null, BOOL], result: NETSERVER_T }, + /** createSecureServer with a RUNTIME options record (the divergence-66 + * stance): allowHTTP1/cert/key read at runtime — allowHTTP1 picks the + * flavor, the TLS server walk fences its out-of-bounds members with + * the catchable runtime fence, and h2 session-tuning keys drop exactly + * like the literal walk ignores them. May throw. The Cb form carries + * the eager compat handler. */ + "http2.createSecureServerDyn": { argTypes: [DYN], result: NETSERVER_T }, + "http2.createSecureServerDynCb": { argTypes: [DYN, null], result: NETSERVER_T }, + // tls.connect(port, host, opts[, cb]) — port -1 / host "" read the + // options record; the callback fires post-handshake (secureConnect). + /** tls.connect — the TLS client socket: (port, host, opts[, cb]) where + * port -1 reads options.port, host "" reads options.host, and opts is + * the runtime (dyn) options record (rejectUnauthorized/ca/servername + * implemented; other documented members throw the runtime fence). The + * callback fires post-handshake — Node's secureConnect timing. */ + "tls.connect": { argTypes: [F64, STRING, DYN], result: NETSOCKET_T }, + "tls.connectCb": { argTypes: [F64, STRING, DYN, null], result: NETSOCKET_T }, + // The TLSSocket member surface (authError's `string | null` union is + // shape-checked in the special cases in validate.ts — the reqHeader pattern). + /** The TLSSocket member surface on the socket kind: authorized (bool — + * Node's verify verdict), authorizationError (the verify-failure CODE + * STRING or null), the 'secureConnect' registration (a TLS socket's + * conn list fires at establishment; plain sockets never fire it), and + * the 'session' registration (fires once with the serialized session — + * a Buffer; the received-ticket event). */ + "tls.sockAuthorized": { argTypes: [NETSOCKET_T], result: BOOL }, + /** getProtocol() (string | null), getCipher() (CipherNameAndProtocol, + * undefined before the handshake, null after close) and alpnProtocol + * (string | false | null), as dyn values. */ + "tls.sockGetProtocol": { argTypes: [NETSOCKET_T], result: DYN }, + "tls.sockGetCipher": { argTypes: [NETSOCKET_T], result: DYN }, + "tls.sockAlpnProtocol": { argTypes: [NETSOCKET_T], result: DYN }, + /** getPeerCertificate(detailed) and getCertificate() (the legacy object, + * {} or null; scr_tls_x509.c), and the DER behind getPeerX509Certificate() + * and getX509Certificate() (a Buffer, empty without a certificate). */ + "tls.sockGetPeerCertificate": { argTypes: [NETSOCKET_T, DYN], result: DYN }, + "tls.sockGetCertificate": { argTypes: [NETSOCKET_T], result: DYN }, + "tls.sockPeerCertDer": { argTypes: [NETSOCKET_T], result: BYTES_U8 }, + "tls.sockOwnCertDer": { argTypes: [NETSOCKET_T], result: BYTES_U8 }, + "tls.sockAuthError": { argTypes: [NETSOCKET_T], result: VOID }, + "tls.sockOnSecureConnect": { + argTypes: [NETSOCKET_T, { kind: "func", params: [], ret: VOID }, BOOL], + result: VOID, + }, + "tls.sockOnSession": { argTypes: [NETSOCKET_T, null, BOOL], result: VOID }, + /** The 'secure' event: the handshake completed (after secureConnect on + * a client). Plain sockets never fire it. */ + "tls.sockOnSecure": { + argTypes: [NETSOCKET_T, { kind: "func", params: [], ret: VOID }, BOOL], + result: VOID, + }, + /** new tls.TLSSocket(socket, options) in server mode: a new TLS socket + * takes over the socket's connection (wrapSocketCtx passes a literal's + * secureContext member apart from the runtime options record). The + * record reads at runtime: isServer, cert, key, ALPNProtocols, + * minVersion and maxVersion are implemented, the client mode and other + * documented members throw the runtime fence. MAY THROW. */ + "tls.wrapSocket": { argTypes: [NETSOCKET_T, DYN], result: NETSOCKET_T }, + "tls.wrapSocketCtx": { argTypes: [NETSOCKET_T, DYN, SECURECTX_T], result: NETSOCKET_T }, + // createSecureContext({ cert, key }) mints the opaque SNI-answer handle. + /** tls.createSecureContext({ cert, key }) — parses the PEM pair into an + * opaque SecureContext handle (secureCtx kind) for SNI callbacks to + * answer with; cert/key are PEM strings or Buffers like createServer's. */ + "tls.createSecureContext": { argTypes: [null, null], result: SECURECTX_T }, + /** createSecureContext over a RUNTIME options record (the checked- + * dynamic lane): Node's typed option validations first (the ciphers/ + * passphrase/engine/version/timeout/ticketKeys ladders), then the pem + * walk — a validated { cert, key } bag builds the real context. + * May-throw. */ + "tls.createSecureContextDyn": { argTypes: [DYN], result: SECURECTX_T }, + /** server.addContext(servername, options | SecureContext), + * server.setSecureContext(options) and the 'tlsClientError' listener of + * a tls or https server (scr_tls.c). */ + "tls.serverAddContext": { argTypes: [NETSERVER_T, DYN, DYN], result: VOID }, + "tls.serverAddContextCtx": { argTypes: [NETSERVER_T, DYN, SECURECTX_T], result: VOID }, + "tls.serverSetSecureContext": { argTypes: [NETSERVER_T, DYN], result: VOID }, + "tls.serverOnClientError": { argTypes: [NETSERVER_T, null, BOOL], result: VOID }, + /** tls.getCACertificates(type): validateString + the documented name + * set, then the trailing compiler-rendered fence — ALWAYS THROWS (the + * error.nodeThrow polymorphic-result carve-out). May-throw seed. */ + "tls.caCertsChk": { argTypes: [DYN, STRING], result: VOID }, + // The CA-store introspection unit (scr_tls_ca.c): per-type cached PEM + // string arrays and the default-set replacement. + /** The CA-store introspection surface (scr_tls_ca.c — its own unit and + * link gate, "tlsca." NOT "tls.", so a getCACertificates-only binary + * never pulls mbedTLS): tlsca.get is tls.getCACertificates(type) — the + * cached per-type string[] of PEM blocks (identity-stable across calls, + * Node's caching; an unknown type throws Node's ERR_INVALID_ARG_VALUE + * TypeError); tlsca.root is the tls.rootCertificates value read; + * tlsca.set is tls.setDefaultCACertificates(certs) — replaces the + * default set (deduped) and the anchors the TLS client verifies + * against, throwing Node's ERR_CRYPTO_OPERATION_FAILED when no entry + * carries a certificate block. */ + "tlsca.get": { argTypes: [STRING], result: arrayOf(STRING) }, + "tlsca.root": { argTypes: [], result: arrayOf(STRING) }, + "tlsca.set": { argTypes: [arrayOf(STRING)], result: VOID }, + "https.createServer": { argTypes: [null, null, null], result: NETSERVER_T }, + // http2's allowHTTP1 compatibility server (divergence 57): cert/key + // like tls.createServer; the 'request' handler arrives separately via + // http.serverOnRequest (shape checked in the libCall case, like + // http.createServer's). sessionError callback shape is checked by validate.ts. + /** node:http2 allowHTTP1: one TLS server advertises h2 + http/1.1 and + * dispatches to the matching parser after ALPN. Request listeners mirror + * into both compatibility lists; the remaining session/req.stream rows + * below retain their explicitly limited behavior. */ + "http2.createSecureServer": { argTypes: [null, null, BOOL], result: NETSERVER_T }, + // The SNI-callback form: arg 2 is the JS SNICallback closure — a + // `(servername, cb) => void` func, or its `| undefined` union from the + // conditional-spread spelling (the libCall case checks the shape). + /** createSecureServer with an SNI callback: args are (cert, key, sniCb) + * where sniCb is the JS SNICallback — a `(servername, cb) => void` + * closure, or the `SNICallback | undefined` union from the conditional- + * spread spelling (`...(x ? { SNICallback: x } : {})`; the emitter + * unwraps the union — the undefined arm means "no callback", exactly + * the no-SNI server). The runtime parses each connection's ClientHello + * for the server_name extension BEFORE the TLS handshake begins, calls + * the callback with (servername, answer-closure), and resumes the + * handshake when the answer arrives — cb(err) tears the socket down + * without an error event (Node's 'tlsClientError' default), cb(null, ctx) serves + * ctx's cert/key, cb(null, undefined) serves the default pair. */ + "http2.createSecureServerSni": { argTypes: [null, null, null, BOOL], result: NETSERVER_T }, + // The ALPN=h2 server (createSecureServer without allowHTTP1): the real + // h2 session machinery behind the TLS handshake. + /** The REAL h2-over-TLS server (createSecureServer WITHOUT allowHTTP1): + * scr_http2_create_secure_server — the h2c session machinery behind an + * mbedTLS handshake whose ALPN advertises h2 alone (an http/1.1-only + * client fails the handshake with no_application_protocol, Node's + * h2-only split). args are (cert, key) — PEM strings or Buffers. */ + "http2.createSecureServerH2": { argTypes: [null, null, BOOL], result: NETSERVER_T }, + "http.serverOnRequest": { argTypes: [NETSERVER_T, null, BOOL], result: VOID }, + /** server.on('checkContinue' | 'checkExpectation', (req, res)): the + * 'request' listener shape; the last argument picks checkExpectation. */ + "http.serverOnCheck": { argTypes: [NETSERVER_T, null, BOOL, BOOL], result: VOID }, + "http2.serverOnSessionError": { argTypes: [NETSERVER_T, null, BOOL], result: VOID }, + /** The guarded absent-session compatibility call remains a no-op. */ + "http2.streamNoop": { argTypes: [], result: VOID }, + /** The UNGUARDED h2-only stream call (`req.stream.on(...)`): stream is + * undefined on every connection the allowHTTP1 lowering accepts — and + * on every HTTP/1.1 connection of Node's own allowHTTP1 server — so the + * call IS Node's member read on undefined: throws the exact catchable + * TypeError ("Cannot read properties of undefined (reading 'on')"). + * One arg: the read member's name (a string literal). Never returns. */ + "http2.streamUndefCall": { argTypes: [STRING], result: VOID }, + // The REAL h2c surface (scr_http2.c). Callback slots are null (the + // emitter picks the thunk); pairs slots are string arrays; the + // endStream tri-state and event flags ride as f64. + /** node:http2, the REAL h2c surface (scr_http2.c — frame codec + HPACK + * over the net loop; the design note atop that file has the story). + * Sessions and streams are first-class handle kinds; 'stream'/'response' + * payloads cross as flat [name, value, ...] pairs arrays and an EMITTED + * adapter closure builds the program-side headers record (the response + * :status rides separately as a number). */ + "http2.createServer": { argTypes: [], result: NETSERVER_T }, + "http2.createServerReq": { argTypes: [null], result: NETSERVER_T }, + "http2.serverOnStream": { argTypes: [NETSERVER_T, null, BOOL], result: VOID }, + "http2.serverOnSession": { argTypes: [NETSERVER_T, null, BOOL], result: VOID }, + // connect(authority, reject, ca[, cb]): the TLS client knobs an https + // authority reads (inert on http ones, exactly Node); ca is a PEM + // string/Buffer ("" = the system anchors). + /** connect(authority[, listener]) — h2c prior knowledge; the listener + * closure (if any) is the 'connect' once-listener. */ + "http2.connect": { argTypes: [STRING, BOOL, null], result: HTTP2SESSION_T }, + "http2.connectCb": { argTypes: [STRING, BOOL, null, null], result: HTTP2SESSION_T }, + /** session.request(pairs?, endStream) — endStream is a tri-state f64: + * -1 the method's payload-meaningless default, 0/1 explicit. */ + "http2.sessionRequest": { + argTypes: [HTTP2SESSION_T, arrayOf(STRING), F64], + result: HTTP2STREAM_T, + }, + "http2.sessionClose": { argTypes: [HTTP2SESSION_T], result: VOID }, + "http2.sessionCloseCb": { argTypes: [HTTP2SESSION_T, null], result: VOID }, + "http2.sessionDestroy": { argTypes: [HTTP2SESSION_T], result: VOID }, + "http2.sessionOnClose": { argTypes: [HTTP2SESSION_T, null, BOOL], result: VOID }, + "http2.sessionOnError": { argTypes: [HTTP2SESSION_T, null, BOOL], result: VOID }, + "http2.sessionOnConnect": { argTypes: [HTTP2SESSION_T, null, BOOL], result: VOID }, + "http2.sessionOnStream": { argTypes: [HTTP2SESSION_T, null, BOOL], result: VOID }, + "http2.sessionOnGoaway": { argTypes: [HTTP2SESSION_T, null, BOOL], result: VOID }, + "http2.sessionClosed": { argTypes: [HTTP2SESSION_T], result: BOOL }, + "http2.sessionDestroyed": { argTypes: [HTTP2SESSION_T], result: BOOL }, + "http2.sessionEncrypted": { argTypes: [HTTP2SESSION_T], result: BOOL }, + "http2.sessionType": { argTypes: [HTTP2SESSION_T], result: F64 }, + "http2.sessionAlpn": { argTypes: [HTTP2SESSION_T], result: STRING }, + "http2.sessionSocket": { argTypes: [HTTP2SESSION_T], result: NETSOCKET_T }, + /** stream.respond(pairs?, endStream) — the server answer. */ + "http2.streamRespond": { argTypes: [HTTP2STREAM_T, arrayOf(STRING), BOOL], result: VOID }, + "http2.streamWrite": { argTypes: [HTTP2STREAM_T, STRING], result: VOID }, + "http2.streamWriteBytes": { argTypes: [HTTP2STREAM_T, BYTES_U8], result: VOID }, + "http2.streamEnd": { argTypes: [HTTP2STREAM_T], result: VOID }, + "http2.streamEndStr": { argTypes: [HTTP2STREAM_T, STRING], result: VOID }, + "http2.streamEndBytes": { argTypes: [HTTP2STREAM_T, BYTES_U8], result: VOID }, + "http2.streamClose": { argTypes: [HTTP2STREAM_T, F64], result: VOID }, + "http2.streamCloseCb": { argTypes: [HTTP2STREAM_T, F64, null], result: VOID }, + "http2.streamDestroy": { argTypes: [HTTP2STREAM_T], result: VOID }, + "http2.sessionSettings0": { argTypes: [HTTP2SESSION_T], result: VOID }, + "http2.sessionSettings": { argTypes: [HTTP2SESSION_T, DYN], result: VOID }, + "http2.sessionSettingsDynCb": { argTypes: [HTTP2SESSION_T, DYN, DYN], result: VOID }, + "http2.sessionSettingsCb0": { + argTypes: [HTTP2SESSION_T, DYN, { kind: "func", params: [], ret: VOID }], + result: VOID, + }, + "http2.sessionOnSettingsDyn": { argTypes: [HTTP2SESSION_T, DYN, BOOL, BOOL], result: VOID }, + "http2.sessionOnSettings0": { + argTypes: [HTTP2SESSION_T, { kind: "func", params: [], ret: VOID }, BOOL, BOOL], + result: VOID, + }, + "http2.sessionSettingsGet": { argTypes: [HTTP2SESSION_T, BOOL], result: DYN }, + "http2.sessionPendingSettingsAck": { argTypes: [HTTP2SESSION_T], result: BOOL }, + "http2.getDefaultSettings": { argTypes: [], result: DYN }, + "http2.streamSetEncoding": { argTypes: [HTTP2STREAM_T, STRING], result: VOID }, + "http2.streamSetEncodingRet": { argTypes: [HTTP2STREAM_T, STRING], result: HTTP2STREAM_T }, + "http2.streamResume": { argTypes: [HTTP2STREAM_T], result: VOID }, + "http2.streamPause": { argTypes: [HTTP2STREAM_T], result: VOID }, + "http2.streamOnData": { argTypes: [HTTP2STREAM_T, null, BOOL], result: VOID }, + "http2.streamOnEnd": { argTypes: [HTTP2STREAM_T, null, BOOL], result: VOID }, + "http2.streamOnClose": { argTypes: [HTTP2STREAM_T, null, BOOL], result: VOID }, + "http2.streamOnAborted": { argTypes: [HTTP2STREAM_T, null, BOOL], result: VOID }, + "http2.streamOnError": { argTypes: [HTTP2STREAM_T, null, BOOL], result: VOID }, + "http2.streamOnResponse": { argTypes: [HTTP2STREAM_T, null, BOOL], result: VOID }, + "http2.streamId": { argTypes: [HTTP2STREAM_T], result: F64 }, + "http2.streamRstCode": { argTypes: [HTTP2STREAM_T], result: F64 }, + "http2.streamDestroyed": { argTypes: [HTTP2STREAM_T], result: BOOL }, + "http2.streamClosed": { argTypes: [HTTP2STREAM_T], result: BOOL }, + "http2.streamAborted": { argTypes: [HTTP2STREAM_T], result: BOOL }, + "http2.streamPending": { argTypes: [HTTP2STREAM_T], result: BOOL }, + "http2.streamSession": { argTypes: [HTTP2STREAM_T], result: HTTP2SESSION_T }, + // The createConnection forms: arg 0 is the dialer closure (() => + // Socket — the libCall case checks), then path/method/timeout/headers/ + // autoEnd like http.request. + /** The createConnection form (the proxy's own dialer): args are + * (connCb, path, method, timeout, headers, autoEnd[, cb]) — connCb is + * a `() => net.Socket` closure the runtime invokes ONCE, synchronously + * (Node's onSocket timing); everything else matches http.request. The + * Host header defaults to "localhost" — a headers.host entry wins + * verbatim, the proxy shape. */ + "http.requestConn": { + argTypes: [null, STRING, STRING, F64, arrayOf(STRING), BOOL], + result: HTTPCLIENTREQ_T, + }, + "http.requestConnCb": { + argTypes: [null, STRING, STRING, F64, arrayOf(STRING), BOOL, null], + result: HTTPCLIENTREQ_T, + }, + "https.request": { + argTypes: [STRING, F64, STRING, STRING, F64, arrayOf(STRING), BOOL, BOOL, null], + result: HTTPCLIENTREQ_T, + }, + "https.requestCb": { + argTypes: [STRING, F64, STRING, STRING, F64, arrayOf(STRING), BOOL, BOOL, null, null], + result: HTTPCLIENTREQ_T, + }, + /** The URL-string first argument, the http.requestUrl row over TLS: + * no options means Node's defaults (verification on, default trust + * anchors), and a non-https scheme is ERR_INVALID_PROTOCOL. */ + "https.requestUrl": { argTypes: [STRING, STRING, BOOL], result: HTTPCLIENTREQ_T }, + "https.requestUrlCb": { argTypes: [STRING, STRING, BOOL, null], result: HTTPCLIENTREQ_T }, + "https.requestAgent": { + argTypes: [STRING, F64, STRING, STRING, F64, arrayOf(STRING), BOOL, BOOL, null, DYN], + result: HTTPCLIENTREQ_T, + }, + "https.requestAgentCb": { + argTypes: [STRING, F64, STRING, STRING, F64, arrayOf(STRING), BOOL, BOOL, null, DYN, null], + result: HTTPCLIENTREQ_T, + }, + // The requestFn binding's runtime-secure rows: https.request's shape + // with the leading `secure` bool. + /** A call through a `const requestFn = tls ? https.request : + * http.request` binding — the module-function-as-value ternary between + * the two known clients: the https.request row with a leading `secure` + * bool that picks the dial at RUNTIME (true = the TLS client, exactly + * https.request; false = the plain client, exactly http.request — + * rejectUnauthorized/ca ignored there like Node ignores TLS options on + * http.request). The "https." prefix keeps the TLS unit linked. */ + "https.requestFn": { + argTypes: [BOOL, STRING, F64, STRING, STRING, F64, arrayOf(STRING), BOOL, BOOL, null], + result: HTTPCLIENTREQ_T, + }, + "https.requestFnCb": { + argTypes: [BOOL, STRING, F64, STRING, STRING, F64, arrayOf(STRING), BOOL, BOOL, null, null], + result: HTTPCLIENTREQ_T, + }, + "http.clientWrite": { argTypes: [HTTPCLIENTREQ_T, STRING], result: VOID }, + "http.clientSetHeader": { argTypes: [HTTPCLIENTREQ_T, STRING, STRING], result: VOID }, + "http.clientGetHeader": { argTypes: [HTTPCLIENTREQ_T, STRING], result: VOID }, + "http.clientHasHeader": { argTypes: [HTTPCLIENTREQ_T, STRING], result: BOOL }, + "http.clientRemoveHeader": { argTypes: [HTTPCLIENTREQ_T, STRING], result: VOID }, + "http.clientGetHeaderNames": { argTypes: [HTTPCLIENTREQ_T], result: arrayOf(STRING) }, + "http.clientGetRawHeaderNames": { argTypes: [HTTPCLIENTREQ_T], result: arrayOf(STRING) }, + "http.clientGetHeaders": { argTypes: [HTTPCLIENTREQ_T], result: DYN }, + "http.clientWriteBytes": { argTypes: [HTTPCLIENTREQ_T, BYTES_U8], result: VOID }, + "http.clientEnd": { argTypes: [HTTPCLIENTREQ_T], result: VOID }, + "http.clientEndStr": { argTypes: [HTTPCLIENTREQ_T, STRING], result: VOID }, + "http.clientEndBytes": { argTypes: [HTTPCLIENTREQ_T, BYTES_U8], result: VOID }, + /** The checked-dynamic chunk twins (the net.sockWriteDyn story). */ + "http.clientWriteDyn": { argTypes: [HTTPCLIENTREQ_T, DYN], result: VOID }, + "http.clientEndDyn": { argTypes: [HTTPCLIENTREQ_T, DYN], result: VOID }, + "http.clientFlushHeaders": { argTypes: [HTTPCLIENTREQ_T], result: VOID }, + "http.clientAddTrailers": { argTypes: [HTTPCLIENTREQ_T, arrayOf(STRING)], result: VOID }, + "http.clientCork": { argTypes: [HTTPCLIENTREQ_T], result: VOID }, + "http.clientUncork": { argTypes: [HTTPCLIENTREQ_T], result: VOID }, + "http.clientWritableCorked": { argTypes: [HTTPCLIENTREQ_T], result: F64 }, + "http.clientMethod": { argTypes: [HTTPCLIENTREQ_T], result: STRING }, + "http.clientPath": { argTypes: [HTTPCLIENTREQ_T], result: STRING }, + "http.clientHost": { argTypes: [HTTPCLIENTREQ_T], result: STRING }, + "http.clientProtocol": { argTypes: [HTTPCLIENTREQ_T], result: STRING }, + "http.clientHeadersSent": { argTypes: [HTTPCLIENTREQ_T], result: BOOL }, + "http.clientWritableEnded": { argTypes: [HTTPCLIENTREQ_T], result: BOOL }, + "http.clientWritableFinished": { argTypes: [HTTPCLIENTREQ_T], result: BOOL }, + "http.clientSocket": { argTypes: [HTTPCLIENTREQ_T], result: NETSOCKET_T }, + "http.clientReusedSocket": { argTypes: [HTTPCLIENTREQ_T], result: BOOL }, + "http.clientSetNoDelay": { argTypes: [HTTPCLIENTREQ_T, BOOL], result: VOID }, + "http.clientSetSocketKeepAlive": { argTypes: [HTTPCLIENTREQ_T, BOOL, F64], result: VOID }, + "http.clientSetTimeout": { argTypes: [HTTPCLIENTREQ_T, F64], result: VOID }, + "http.clientSetTimeoutCb": { + argTypes: [HTTPCLIENTREQ_T, F64, { kind: "func", params: [], ret: VOID }], + result: VOID, + }, + "http.statusCodes": { argTypes: [], result: DYN }, + "http.methods": { argTypes: [], result: arrayOf(STRING) }, + "http.reqSetTimeout": { argTypes: [HTTPREQ_T, F64], result: VOID }, + "http.reqSetTimeoutCb": { + argTypes: [HTTPREQ_T, F64, { kind: "func", params: [], ret: VOID }], + result: VOID, + }, + "http.clientDestroy": { argTypes: [HTTPCLIENTREQ_T], result: VOID }, + "http.clientAbort": { argTypes: [HTTPCLIENTREQ_T], result: VOID }, + "http.clientAborted": { argTypes: [HTTPCLIENTREQ_T], result: BOOL }, + "http.clientDestroyed": { argTypes: [HTTPCLIENTREQ_T], result: BOOL }, + "http.clientOnResponse": { argTypes: [HTTPCLIENTREQ_T, null, BOOL], result: VOID }, + "http.clientOnSocket": { argTypes: [HTTPCLIENTREQ_T, null, BOOL], result: VOID }, + "http.clientOnFinish": { + argTypes: [HTTPCLIENTREQ_T, { kind: "func", params: [], ret: VOID }, BOOL], + result: VOID, + }, + "http.clientOnError": { argTypes: [HTTPCLIENTREQ_T, null, BOOL], result: VOID }, + "http.clientOnTimeout": { + argTypes: [HTTPCLIENTREQ_T, { kind: "func", params: [], ret: VOID }, BOOL], + result: VOID, + }, + "http.clientOnClose": { + argTypes: [HTTPCLIENTREQ_T, { kind: "func", params: [], ret: VOID }, BOOL], + result: VOID, + }, + "http.clientOnAbort": { + argTypes: [HTTPCLIENTREQ_T, { kind: "func", params: [], ret: VOID }, BOOL], + result: VOID, + }, + "http.clientOnContinue": { + argTypes: [HTTPCLIENTREQ_T, { kind: "func", params: [], ret: VOID }, BOOL], + result: VOID, + }, + /** execFileSync/execSync as ONE entry (args: cmd, argv, shell, input, + * cwd, hasEnv, envPairs, timeoutMs, stdoutMode, stderrMode, buffers — + * see scr_child.h). Throws Node's exact errors: "Command failed: " + * (+ captured stderr) on non-zero exit or signal death, "spawnSync + * ENOENT" on spawn failure, "spawnSync ETIMEDOUT" after + * the SIGTERM timeout, with Node's status, signal, output, pid, stdout + * and stderr properties (Buffer outputs when buffers is set: no + * encoding option). Result is the captured utf8 stdout (+1). */ + "cp.execSync": { + argTypes: [ + STRING, + arrayOf(STRING), + BOOL, + STRING, + BOOL, + STRING, + BOOL, + arrayOf(STRING), + F64, + F64, + F64, + BOOL, + ], + result: STRING, + }, + /** The promisified-execFile/exec capture (args: cmd, argv, shell, cwd, + * hasEnv, envPairs, timeoutMs; shell marks exec's /bin/sh -c argv, as + * for execSync, so failures display the command string): the same exec + * core in the async shape — both streams captured, no echo, Node's ASYNC messages on the throw paths + * ("Command failed: \n" with the unconditional newline, + * "spawn ENOENT" with .code, timeouts reporting as ordinary + * SIGTERM command failures — never ETIMEDOUT). Result reuses the + * ScrSpawnRes container (+1; stdout/stderr strings, status unused). + * Called only from the frontend's interned %execFileAsync and + * %execAsync ASYNC helpers, whose fiber turns the throw into the + * rejection. */ + "cp.execCapture": { + argTypes: [STRING, arrayOf(STRING), BOOL, STRING, BOOL, arrayOf(STRING), F64], + result: SPAWNRES_T, + }, + /** child_process.spawn (scr_child.c + the scr_async.c loop): posix_spawnp + * with per-slot ignore/inherit/pipe modes (and number output fds), the + * all-piped Node default, and the child registered with the event loop, + * which polls waitpid(WNOHANG) at quiescence like timers. + * NEVER throws: spawn failure defers to the "error" event, Node-exact + * (the error message is Node's "spawn "; an "error" + * event with no listener prints it and exits 1 like an EventEmitter). + * cmd/args borrowed; result an owned (+1) child handle. The loop will + * not exhaust while any spawned child is unreaped — Node's keep-alive. + * + * child.onExit / child.onClose / child.onError — terminal listener + * registration through child.on/once. Close uses the exit adapter shape + * but fires only after every piped stdio handle reaches EOF. The + * receiver is borrowed, the CALLBACK MOVES into the child's listener + * registry (released after the terminal event fires, or at reap for + * the event that never fires). Both are void (chaining is fenced). + * onExit's third emitted ingredient is an ADAPTER the backend interns + * per callback shape: the runtime invokes adapter(cb, has_code, code) + * and the adapter builds the `number | null` union (tags are program- + * dependent) or ignores the code for a zero-param callback. onError's + * adapters are runtime-provided (zero-param, or the %Error one-param + * shape — scr_error_new needs no program types). "exit" fires once + * with the code (f64 arm) or null (signal death); "error" fires only + * for spawn failure, exactly Node's split. */ + "cp.spawn": { argTypes: [STRING, arrayOf(STRING)], result: CHILD_T }, + /** Static-native fork startup and IPC. process.forkTarget initializes a + * re-executed child's inherited channel and returns its embedded target id + * (-1 in the parent). The remaining entries mirror Node's JSON channel. */ + "process.forkTarget": { argTypes: [F64], result: F64 }, + "cp.fork": { + argTypes: [F64, arrayOf(STRING), F64, F64, F64, BOOL, arrayOf(STRING), STRING], + result: CHILD_T, + }, + "child.connected": { argTypes: [CHILD_T], result: BOOL }, + "child.hasChannel": { argTypes: [CHILD_T], result: BOOL }, + "child.send": { argTypes: [CHILD_T, STRING], result: BOOL }, + "child.sendCb": { argTypes: [CHILD_T, STRING, null], result: BOOL }, + "child.disconnect": { argTypes: [CHILD_T], result: VOID }, + "child.onMessage": { argTypes: [CHILD_T, null, BOOL], result: VOID }, + "child.onDisconnect": { argTypes: [CHILD_T, null, BOOL], result: VOID }, + "child.onSpawn": { argTypes: [CHILD_T, null], result: VOID }, + /** Rest listeners use the checked function bridge to pack event arguments. */ + "child.onDyn": { argTypes: [CHILD_T, STRING, DYN], result: VOID }, + // Own properties JavaScript adds to children (handle-properties.ts). + "child.propsInstall": { argTypes: [], result: VOID }, + "child.propsDefine": { argTypes: [DYN, DYN, DYN], result: DYN }, + "child.propsDescriptor": { argTypes: [DYN, DYN], result: DYN }, + "child.propsResolve": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, + "child.propsOnDyn": { argTypes: [CHILD_T, STRING, DYN], result: VOID }, + "process.connected": { argTypes: [], result: BOOL }, + /** Whether this fork child opened its channel; part of process.send's + * presence in programs with fork targets. Never throws. */ + "process.ipcOpen": { argTypes: [], result: BOOL }, + "process.send": { argTypes: [STRING], result: BOOL }, + "process.sendCb": { argTypes: [STRING, null], result: BOOL }, + "process.disconnect": { argTypes: [], result: VOID }, + "process.onMessage": { argTypes: [null, BOOL], result: VOID }, + "process.onDisconnect": { argTypes: [null, BOOL], result: VOID }, + /** child_process.execFile's callback slice: starts an all-piped child, + * captures stdout/stderr, and moves the error-first callback into the + * child registry. The callback shape is program-dependent and checked + * by the validator/backends. The last argument is exec's shell flag: + * the argv is ["-c", command] and failure messages show the command. */ + "cp.execFile": { argTypes: [STRING, arrayOf(STRING), null, BOOL], result: CHILD_T }, + // spawn's options form: per-slot stdio modes (0 ignore / 1 inherit / + // 2 fd) with the out/err fds for mode 2, detached, env replacement + // pairs, cwd ("" = inherit). + /** spawn with options (scr_child.c): the cp.spawn core plus PER-SLOT + * stdio — args are (cmd, argv, inMode, outMode, errMode, outFd, errFd, + * detached, hasEnv, envPairs, cwd); modes 0 ignore (/dev/null), 1 + * inherit, 2 fd (out/err only — the fd dup2s into the child's slot, + * Node's stdio fd form; the daemon-log idiom ["ignore", logFd, logFd]). + * detached is POSIX_SPAWN_SETSID (the child gets its own session and + * process group, Node's semantics), env is a REPLACEMENT ([k,v,...] + * pairs like cp.execSync's), cwd ""=inherit. Same event/loop story as + * cp.spawn. */ + "cp.spawnOpts": { + argTypes: [ + STRING, + arrayOf(STRING), + F64, + F64, + F64, + F64, + F64, + BOOL, + BOOL, + BOOL, + arrayOf(STRING), + STRING, + ], + result: CHILD_T, + }, + /** Runtime options normalized into the native spawn core; unsupported + * process features retain explicit refusals. (command, args, options). */ + "cp.spawnDynamic": { argTypes: [STRING, arrayOf(STRING), DYN], result: CHILD_T }, + // The callback's func type is program-dependent (zero params, or the + // `number | null` union / the %Error class) — the libCall case checks + // the shape; the slot here only pins arity and the child receiver. + "child.onExit": { argTypes: [CHILD_T, null], result: VOID }, + "child.onClose": { argTypes: [CHILD_T, null], result: VOID }, + "child.onError": { argTypes: [CHILD_T, null], result: VOID }, + // Like process.envGet, spawnRes.status's result type is program-dependent + // (the interned `number | null` union) — the libCall case checks the arms. + "spawnRes.status": { argTypes: [SPAWNRES_T], result: VOID }, + // spawnRes.error's result is the interned `Error | undefined` union — + // the libCall case checks the arms, the spawnRes.status pattern. + /** Node's spawn-failure carrier `error?: Error`: a fresh +1 %Error + * ("spawnSync ENOENT", `code` stamped) when the spawn itself + * failed, the interned undefined arm otherwise — the result type is the + * call site's `Error | undefined` union, constructed type-directedly in + * the backend (the envGet convention). Never throws. */ + "spawnRes.error": { argTypes: [SPAWNRES_T], result: VOID }, + // spawnRes.signal's result is the interned `string | null` union — + // same pattern. + "spawnRes.signal": { argTypes: [SPAWNRES_T], result: VOID }, + // The ChildProcess lifecycle members: pid/exitCode are program-dependent + // unions (`number | undefined` / `number | null` — the libCall case + // checks the arms, the spawnRes.status pattern). + /** The ChildProcess lifecycle members (scr_child.c), Node's shapes + * exactly (SEMANTICS.md has the pinned matrix). child.pid is the + * checker's `number | undefined` (undefined = spawn failure) and + * child.exitCode its `number | null` (null while running and after a + * signal death; -errno once a spawn failure settled) — both unions are + * type-directed constructions in the backend over a has/get runtime + * pair, the spawnRes.status pattern. child.killed is Node's + * sent-a-signal flag. child.kill sends while the child is un-reaped + * (false after — Node's null-handle answer) and THROWS the + * Unknown-signal TypeError on bad names (may-throw); killNum passes + * numbers through (0 probes; never throws). child.unref drops the + * child from the loop's keep-alive set — it is still reaped while the + * loop runs for other reasons, and one the loop never reaps is left to + * the OS at exit. All receivers borrowed. */ + "child.pid": { argTypes: [CHILD_T], result: VOID }, + "child.exitCode": { argTypes: [CHILD_T], result: VOID }, + // The close-override pair: the bound close's result func type and the + // override's func argument are program-dependent (the callback union's + // tags) — the slots pin the server receiver; the libCall case checks + // the bound value's shape. + /** The close-override pair (the portless close-proxy idiom). + * serverCloseBind is `wrapper.close.bind(wrapper)` as a VALUE: a + * compiler-emitted closure over the server whose invocation runs the + * REAL close (scr_net_server_close_direct — never the override), so + * the override body's `origClose(cb)` cannot recurse; its callback + * argument (the `((err?: Error) => void) | undefined` union) registers + * as a once-'close' listener, a one-param callback wrapped by an + * emitted zero-arg trampoline firing the undefined arm (a clean close + * carries no error). serverSetCloseOverride is `wrapper.close = fn`: + * the override MOVES in behind an emitted zero-arg wrapper (it invokes + * the user function with the undefined-arm callback — tags are program + * data), and server.close() consults it before closing. */ + "net.serverCloseBind": { argTypes: [NETSERVER_T], result: VOID }, + "net.serverSetCloseOverride": { argTypes: [NETSERVER_T, null], result: VOID }, + // The piped-output stream reads: `Readable | null` unions (the libCall + // case checks the arms — the child.pid pattern with a ref arm). + /** The piped-output streams (stdio mode 3 — scr_child.c's stream + * slice). child.stdout/child.stderr answer the checker's + * `Readable | null` union (type-directed construction in the backend + * over the +1-or-NULL runtime pair — the child.pid pattern with a ref + * arm). stream.onData/onEnd register 'data'/'end' listeners (receiver + * borrowed, CALLBACK MOVES, trailing once-flag, void — chaining + * fenced): 'data' fires one Buffer chunk per read (zero-param and + * Buffer/string adapters are runtime-provided; setEncoding threads + * split multibyte sequences through the shared StringDecoder core. A + * union-param listener — + * ngrok's `Buffer | string` — gets a compiler-emitted adapter wrapping + * the chunk at its Buffer arm), 'end' fires once at EOF, always BEFORE + * the child's 'exit' (the pinned ordering). A flowing stream keeps the + * loop alive: usesTimers. */ + "child.stdout": { argTypes: [CHILD_T], result: VOID }, + "child.stderr": { argTypes: [CHILD_T], result: VOID }, + /** The piped-input writer (stdio mode 3 on fd 0). child.stdin answers + * `Writable | null`; writes copy borrowed data into the nonblocking + * queue, end/destroy settle it, writable is a pure state read, and + * listener callbacks move into the writer registry. */ + "child.stdin": { argTypes: [CHILD_T], result: VOID }, + /** Under the general stream representation (general-streams.ts), a + * stdio read (slot 0, 1 or 2) answers the child's bridged runtime stream + * — `Writable | null` for stdin, `Readable | null` otherwise; the bridge + * throws when the program lacks the stream unit's boxing helpers. */ + "child.stdioStream": { argTypes: [CHILD_T, F64], result: VOID }, + // Listener registrations: callback func shapes are program-dependent + // (zero-param / Buffer / string / unknown / Buffer-armed union) — the slots pin arity, + // the stream receiver, and the once-flag. + "stream.onData": { argTypes: [CHILDSTREAM_T, null, BOOL], result: VOID }, + "stream.onDataStr": { argTypes: [CHILDSTREAM_T, null, BOOL], result: VOID }, + "stream.onEnd": { argTypes: [CHILDSTREAM_T, null, BOOL], result: VOID }, + "stream.childSetEncoding": { argTypes: [CHILDSTREAM_T, STRING], result: CHILDSTREAM_T }, + "writer.writeString": { argTypes: [CHILDWRITER_T, STRING], result: BOOL }, + "writer.writeBytes": { argTypes: [CHILDWRITER_T, BYTES_U8], result: BOOL }, + "writer.end": { argTypes: [CHILDWRITER_T], result: VOID }, + "writer.destroy": { argTypes: [CHILDWRITER_T], result: VOID }, + "writer.writable": { argTypes: [CHILDWRITER_T], result: BOOL }, + "writer.onDrain": { argTypes: [CHILDWRITER_T, null, BOOL], result: VOID }, + "writer.onFinish": { argTypes: [CHILDWRITER_T, null, BOOL], result: VOID }, + "writer.onError": { argTypes: [CHILDWRITER_T, null, BOOL], result: VOID }, + /** The first-class WritableStream write (`output.write(line)` — the + * prefixStream idiom): the receiver IS the fd scalar (process.stdout/ + * stderr reads mint 1/2), dispatched onto the exact stdoutWrite/ + * stderrWrite paths so buffering and ordering stay identical. Data + * borrowed; the bool is Node's always-true backpressure signal. */ + "procStream.write": { argTypes: [PROCSTREAM_T, STRING], result: BOOL }, + /** process.stdout/stderr flowing into a runtime Writable slot under the + * general stream representation: an fd-backed Writable (+1) whose + * writes take process.stdout.write's synchronous path. */ + /** A pipeline() stage that is a checked-dynamic value: a boxed native + * stream unwraps by identity (+1); anything else throws the given + * not-supported message. Typed as a Duplex so every stage position + * accepts it; the runtime pipeline checks the stream's real sides. */ + "stream.unboxDyn": { + argTypes: [DYN, STRING], + result: { kind: "object", className: "%Duplex" }, + }, + /** Whether pipeline() pumps a checked-dynamic first stage as an + * iterable (a string, a value with an iterator method that is not a + * stream with pipe() and on(), a { readable, writable } pair, or a + * source function's result) instead of piping it as a stream. */ + "stream.dynIterableSource": { argTypes: [DYN], result: BOOL }, + /** The value pipeline() pumps for a checked-dynamic first stage: a + * source function's result (the flag set), which must be iterable, else + * a { readable, writable } pair's readable side or the value itself. */ + "stream.dynSourceValue": { argTypes: [DYN, BOOL], result: DYN }, + /** An untyped pipeline function stage's result before the last stage, + * which must be async iterable: ERR_INVALID_RETURN_VALUE naming the + * stage ("transform[i]") otherwise. Answers the value (+1). */ + "stream.dynTransformResult": { argTypes: [DYN, STRING], result: DYN }, + /** A checked-dynamic pipeline() stage taken as a stream: a native stream + * (+1), or Duplex.from of a value that is not stream-like or a function + * (an iterable), which the pipeline uses without piping into it. Other + * values throw: Duplex.from's ERR_INVALID_ARG_TYPE, or the given + * not-supported message. */ + "stream.unboxDynStage": { + argTypes: [DYN, STRING], + result: { kind: "object", className: "%Duplex" }, + }, + /** The Transform a Cipheriv or Decipheriv reads and writes as a stream + * (+1, created on first use and owned by the cipher). */ + "stream.handleStream": { + argTypes: [DYN], + result: { kind: "object", className: "%Transform" }, + }, + /** Checked-dynamic stream slots (dyn-streams.ts): the native stream a + * checked-dynamic value boxes (+1) — a runtime stream with the readable + * (1) or writable (2) side — or null for any other value. Never throws; + * the result union is `%Readable | null` or `%Writable | null`. */ + "stream.dynNative": { argTypes: [DYN, F64], result: VOID }, + /** stream.dynNative for a pipe or unpipe destination: also a + * constructor-function stream's runtime stream, in programs that link + * that unit, unless it overrides write() or end(). */ + "stream.dynNativePipe": { argTypes: [DYN, F64], result: VOID }, + /** readable.pipe(destination, end) into a checked-dynamic destination + * that is not a native stream: [source, the source boxed, destination, + * end]. Node's pipe listener protocol over the destination's members + * (scr_stream_dyn.c); throws what those members throw. */ + "stream.pipeDyn": { argTypes: [null, DYN, DYN, BOOL], result: VOID }, + /** readable.unpipe(destination) for a checked-dynamic destination that + * is not a native stream: [source, destination]. Unpipes the pipe + * stream.pipeDyn registered into it, if any (scr_stream_dyn.c). */ + "stream.unpipeDyn": { argTypes: [null, DYN], result: VOID }, + "stream.stdioWritable": { + argTypes: [PROCSTREAM_T], + result: { kind: "object", className: "%Writable" }, + }, + "child.killed": { argTypes: [CHILD_T], result: BOOL }, + "child.kill": { argTypes: [CHILD_T, STRING], result: BOOL }, + "child.killNum": { argTypes: [CHILD_T, F64], result: BOOL }, + "child.unref": { argTypes: [CHILD_T], result: VOID }, + "child.ref": { argTypes: [CHILD_T], result: VOID }, + "spawnRes.stdout": { argTypes: [SPAWNRES_T], result: STRING }, + "spawnRes.stderr": { argTypes: [SPAWNRES_T], result: STRING }, + /** fs/promises (scr_lib.c over scr_async.c's settled minting): the SAME + * sync syscalls, wrapped in an ALREADY-SETTLED promise — failure + * REJECTS (catchable at the await) instead of throwing, so none of + * these are in the may-throw seed. readFile is utf8-fenced like + * readFileSync. The non-interleaving divergence is documented in + * SEMANTICS.md. */ + /** node:crypto randomness: randomUUID plus randomBytes as either a real + * Buffer or the fused randomBytes(n).toString("hex"|"base64") path. + * The size-taking forms throw Node's RangeError on invalid values. */ + "crypto.randomUUID": { argTypes: [], result: STRING }, + "crypto.randomBytesToString": { argTypes: [F64, STRING], result: STRING }, + /** crypto.randomBytes(n) → a real u8 Buffer (+1). THROWS Node's + * RangeError on out-of-range sizes, exactly like the composed + * randomBytesToString (which keeps its one-libCall lowering — the two + * coexist: the composed form never materializes the Buffer). */ + "crypto.randomBytes": { argTypes: [F64], result: BYTES_U8 }, + /** The fused createHash(alg).update(data).digest(enc) fast path and the + * one-shot crypto.hash implementation. First-class Hash/Hmac handles use + * the entries below; all paths share the incremental runtime core. */ + "crypto.hashDigestStr": { argTypes: [STRING, STRING, STRING], result: STRING }, + "crypto.hashDigestBytes": { argTypes: [STRING, BYTES_U8, STRING], result: STRING }, + /** First-class static Hash/Hmac handles. Constructors validate the + * runtime algorithm string (every digest Node 24 lists); update returns the same + * handle by retained identity, copy snapshots Hash state, and digest + * finalizes the handle and returns either a Buffer or encoded string. */ + "crypto.native": { argTypes: [], result: DYN }, + "crypto.hashNew": { argTypes: [STRING], result: CRYPTOHASH_T }, + /** The Transform a Hash or Hmac reads and writes as a stream (+1, + * created on first use and owned by the value; scr_stream.c). The Piped + * form, for pipe destinations and pipeline stages, also keeps the value + * alive until the stream flushes or is destroyed. */ + "crypto.hashStream": { argTypes: [null], result: { kind: "object", className: "%Transform" } }, + "crypto.hashStreamPiped": { + argTypes: [null], + result: { kind: "object", className: "%Transform" }, + }, + /** createCipheriv (false) / createDecipheriv (true) over checked-dynamic + * cipher, key, iv and options arguments (scr_cipher.c): a native + * Cipheriv/Decipheriv handle whose methods dispatch through dynInvoke. */ + "crypto.cipherNew": { argTypes: [BOOL, DYN, DYN, DYN, DYN], result: DYN }, + /** createECDH(curveName) over a checked-dynamic curve (scr_ecdh.c): a + * native ECDH handle whose methods dispatch through dynInvoke. */ + "crypto.ecdhNew": { argTypes: [DYN], result: DYN }, + /** ECDH.convertKey(key, curve, inputEncoding, outputEncoding, format): + * the converted point as a Buffer or string. */ + "crypto.ecdhConvertKey": { argTypes: [DYN, DYN, DYN, DYN, DYN], result: DYN }, + /** createDiffieHellman(sizeOrKey, keyEncoding, generator, + * generatorEncoding) over checked-dynamic arguments (scr_dh.c): a native + * DiffieHellman handle whose methods dispatch through dynInvoke. */ + "crypto.dhNew": { argTypes: [DYN, DYN, DYN, DYN], result: DYN }, + /** getDiffieHellman(groupName) and createDiffieHellmanGroup(name): a + * native DiffieHellmanGroup handle. */ + "crypto.dhGroupNew": { argTypes: [DYN], result: DYN }, + /** checkPrimeSync(candidate, options) and generatePrimeSync(size, + * options) over checked-dynamic arguments (scr_dh.c): a boolean, and an + * ArrayBuffer or bigint, as checked-dynamic results. The Cb forms take + * the callback and whether its error parameter takes null for Node's + * undefined, and call it in a later macrotask; Node's argument errors + * throw (may-throw). */ + "crypto.checkPrimeSync": { argTypes: [DYN, DYN], result: DYN }, + /** getCipherInfo(nameOrNid, options) over checked-dynamic arguments + * (scr_cipher_info.c): the info object or undefined. */ + "crypto.getCipherInfo": { argTypes: [DYN, DYN], result: DYN }, + "crypto.checkPrimeCb": { argTypes: [DYN, DYN, DYN, BOOL], result: VOID }, + "crypto.generatePrimeSync": { argTypes: [DYN, DYN], result: DYN }, + "crypto.generatePrimeCb": { argTypes: [DYN, DYN, DYN, BOOL], result: VOID }, + "crypto.hmacNewStr": { argTypes: [STRING, STRING], result: CRYPTOHMAC_T }, + "crypto.hmacNewBytes": { argTypes: [STRING, BYTES_U8], result: CRYPTOHMAC_T }, + "crypto.hashUpdateStr": { argTypes: [CRYPTOHASH_T, STRING], result: CRYPTOHASH_T }, + "crypto.hashUpdateBytes": { argTypes: [CRYPTOHASH_T, BYTES_U8], result: CRYPTOHASH_T }, + "crypto.hmacUpdateStr": { argTypes: [CRYPTOHMAC_T, STRING], result: CRYPTOHMAC_T }, + "crypto.hmacUpdateBytes": { argTypes: [CRYPTOHMAC_T, BYTES_U8], result: CRYPTOHMAC_T }, + "crypto.hashCopy": { argTypes: [CRYPTOHASH_T], result: CRYPTOHASH_T }, + "crypto.hashDigestString": { argTypes: [CRYPTOHASH_T, STRING], result: STRING }, + "crypto.hashDigestBuffer": { argTypes: [CRYPTOHASH_T], result: BYTES_U8 }, + "crypto.hmacDigestString": { argTypes: [CRYPTOHMAC_T, STRING], result: STRING }, + "crypto.hmacDigestBuffer": { argTypes: [CRYPTOHMAC_T], result: BYTES_U8 }, + /** The option and any-value forms, with Node's checks at run time. The + * Dyn digests return a string or a Buffer, and crypto.hash's Dyn form + * either one by its outputEncoding. */ + "crypto.hashNewOpts": { argTypes: [STRING, DYN], result: CRYPTOHASH_T }, + "crypto.hmacNewDyn": { argTypes: [STRING, DYN, DYN], result: CRYPTOHMAC_T }, + "crypto.hashCopyOpts": { argTypes: [CRYPTOHASH_T, DYN], result: CRYPTOHASH_T }, + "crypto.hashUpdateEnc": { argTypes: [CRYPTOHASH_T, STRING, STRING], result: CRYPTOHASH_T }, + "crypto.hmacUpdateEnc": { argTypes: [CRYPTOHMAC_T, STRING, STRING], result: CRYPTOHMAC_T }, + "crypto.hashUpdateDyn": { argTypes: [CRYPTOHASH_T, DYN, DYN], result: CRYPTOHASH_T }, + "crypto.hmacUpdateDyn": { argTypes: [CRYPTOHMAC_T, DYN, DYN], result: CRYPTOHMAC_T }, + "crypto.hashDigestDyn": { argTypes: [CRYPTOHASH_T, DYN], result: DYN }, + "crypto.hmacDigestDyn": { argTypes: [CRYPTOHMAC_T, DYN], result: DYN }, + "crypto.hashOneshot": { argTypes: [STRING, DYN, DYN], result: DYN }, + /** Views of any element kind (the validator checks both are views). */ + "crypto.timingSafeEqual": { argTypes: [null, null], result: BOOL }, + "crypto.timingSafeEqualDyn": { argTypes: [DYN, DYN], result: BOOL }, + "crypto.randomFill": { argTypes: [BYTES_U8, F64, F64], result: BYTES_U8 }, + "crypto.randomFillRest": { argTypes: [BYTES_U8, F64], result: BYTES_U8 }, + "crypto.randomInt": { argTypes: [F64, F64], result: F64 }, + "crypto.randomIntMax": { argTypes: [F64], result: F64 }, + /** randomFillSync over any view returns that view at its own type (the + * validator checks the result against the first argument). The Dyn form + * takes ArrayBuffers and other values. The callback forms carry + * randomFill's argument count before the callback; randomIntCb carries + * whether min was given. */ + "crypto.randomFillView": { argTypes: [null, F64, F64], result: BYTES_U8 }, + "crypto.randomFillViewRest": { argTypes: [null, F64], result: BYTES_U8 }, + /** crypto.getRandomValues: a view returns itself at its own type (the + * float/size DOMExceptions are thrown at run time); any other value + * takes the checked form. */ + "crypto.getRandomValues": { argTypes: [null], result: BYTES_U8 }, + "crypto.getRandomValuesDyn": { argTypes: [DYN], result: DYN }, + "crypto.randomFillSyncDyn": { argTypes: [DYN, DYN, DYN], result: DYN }, + "crypto.randomFillCb": { argTypes: [null, F64, F64, F64, null], result: VOID }, + "crypto.randomFillCbDyn": { argTypes: [DYN, DYN, DYN, F64, null], result: VOID }, + "crypto.randomIntCb": { argTypes: [F64, F64, F64, null], result: VOID }, + "crypto.randomUUIDOpts": { argTypes: [DYN], result: STRING }, + /** Secret KeyObjects: native handles whose getters and methods dispatch + * through the handle. keyObjectInput reads a key's bytes for hkdf. */ + "crypto.createSecretKey": { argTypes: [DYN, DYN], result: DYN }, + "crypto.generateKeySync": { argTypes: [DYN, DYN], result: DYN }, + "crypto.generateKeyCb": { argTypes: [DYN, DYN, null], result: VOID }, + "crypto.keyObjectInput": { argTypes: [DYN], result: BYTES_U8 }, + "crypto.isKeyObject": { argTypes: [DYN], result: BOOL }, + /** WebCrypto (scr_webcrypto.c): each method takes its argument vector as + * one dyn array and answers a promise that argument and validation + * failures reject. CryptoKey values are native handles. */ + "webcrypto.generateKey": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, + "webcrypto.importKey": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, + "webcrypto.exportKey": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, + "webcrypto.encrypt": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, + "webcrypto.decrypt": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, + "webcrypto.wrapKey": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, + "webcrypto.unwrapKey": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, + "webcrypto.deriveBits": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, + "webcrypto.deriveKey": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, + "webcrypto.sign": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, + "webcrypto.verify": { argTypes: [DYN], result: { kind: "promise", inner: BOOL } }, + "webcrypto.digest": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, + "webcrypto.isCryptoKey": { argTypes: [DYN], result: BOOL }, + "webcrypto.object": { argTypes: [], result: DYN }, + "crypto.randomUUIDv7": { argTypes: [DYN], result: STRING }, + "crypto.pbkdf2": { argTypes: [BYTES_U8, BYTES_U8, F64, F64, STRING], result: BYTES_U8 }, + "crypto.randomBytesCb": { argTypes: [F64, null], result: VOID }, + "crypto.pbkdf2Cb": { argTypes: [BYTES_U8, BYTES_U8, F64, F64, STRING, null], result: VOID }, + "crypto.hkdf": { argTypes: [STRING, BYTES_U8, BYTES_U8, BYTES_U8, F64], result: DYN }, + "crypto.hkdfCb": { argTypes: [STRING, BYTES_U8, BYTES_U8, BYTES_U8, F64, null], result: VOID }, + "crypto.scrypt": { argTypes: [BYTES_U8, BYTES_U8, F64, DYN], result: BYTES_U8 }, + "crypto.scryptCb": { argTypes: [BYTES_U8, BYTES_U8, F64, DYN, null], result: VOID }, + /** util.promisify over the callback forms: the callback form's + * arguments without the callback → a promise the callback form settles, + * or rejected with an argument error; never throws. The result's value + * type is the declared one (a Buffer, the filled view, a number, or a + * checked-dynamic value); the validator checks it. */ + "crypto.randomBytesPromise": { argTypes: [F64], result: VOID }, + "crypto.pbkdf2Promise": { argTypes: [BYTES_U8, BYTES_U8, F64, F64, STRING], result: VOID }, + "crypto.scryptPromise": { argTypes: [BYTES_U8, BYTES_U8, F64, DYN], result: VOID }, + "crypto.hkdfPromise": { argTypes: [STRING, BYTES_U8, BYTES_U8, BYTES_U8, F64], result: VOID }, + "crypto.randomFillPromise": { argTypes: [null, F64, F64, F64], result: VOID }, + "crypto.randomFillPromiseDyn": { argTypes: [DYN, DYN, DYN, F64], result: VOID }, + "crypto.randomIntPromise": { argTypes: [F64, F64, F64], result: VOID }, + "crypto.generateKeyPromise": { argTypes: [DYN, DYN], result: VOID }, + /** util.promisify(dns.lookup) and util.promisify(dns.lookupService): + * [hostname, options] and [address, port] → a promise of the + * checked-dynamic result object (the address array with options.all), + * or rejected with the error; never throws. */ + "dns.lookupPromise": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, + "dns.lookupServicePromise": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, + // The Buffer statics and the fs/zlib Buffer forms: fixed always-u8 + // signatures (Buffer IS a Uint8Array — one bytes kind). + /** The Buffer statics with fixed (always-u8) signatures. fromStr is + * `Buffer.from(string, enc)` — the frontend completes an omitted + * encoding to "utf8" and fences non-literal/unsupported ones; hex and + * base64 decode Node-leniently, so it never throws. concat takes ONE + * bytes[] arg (the list) and returns a fresh copy. `Buffer.from(u8)` + * and `Buffer.alloc(n)` need no libFn — they lower to bytesNew. */ + "buffer.fromStr": { argTypes: [STRING, STRING], result: BYTES_U8 }, + "buffer.fromStrChecked": { argTypes: [STRING, STRING], result: BYTES_U8 }, + "buffer.brand": { argTypes: [BYTES_U8], result: BYTES_U8 }, + /** Buffer.isBuffer over a typed view: whether the view carries the + * Buffer brand (any element kind; only Buffers do). Never throws. */ + "buffer.isBranded": { argTypes: [null], result: BOOL }, + /** Buffer.from on checked-native strings, bytes, arrays and data-only + * array-like/Buffer-JSON objects. The encoding is a normalized literal; + * non-string inputs ignore it. Copies the input and may throw during + * element coercion or argument validation. Custom object valueOf and + * opaque reference inputs retain explicit runtime refusals. */ + "buffer.fromDyn": { argTypes: [DYN, STRING], result: BYTES_U8 }, + "buffer.concat": { argTypes: [arrayOf(BYTES_U8)], result: BYTES_U8 }, + "buffer.concatDyn": { argTypes: [DYN], result: BYTES_U8 }, + /** The pooled Buffer factories (Node's allocate/fromStringFast/ + * fromArrayLike): allocUnsafe, Buffer.from of strings, Uint8Arrays, + * number[] and checked values, and concat return views into the + * per-thread pool slab below Buffer.poolSize >>> 1 bytes. alloc and + * allocUnsafeSlow never pool. The size forms validate like Node + * (ERR_OUT_OF_RANGE). poolSize reads and writes Buffer.poolSize. */ + "buffer.alloc": { argTypes: [F64], result: BYTES_U8 }, + /** Buffer.alloc as a function value: the size, fill and encoding arrive + * unchecked, and the runtime fills as Node's alloc does. */ + "buffer.allocValue": { argTypes: [DYN, DYN, DYN], result: BYTES_U8 }, + "buffer.allocUnsafe": { argTypes: [F64], result: BYTES_U8 }, + "buffer.allocUnsafeSlow": { argTypes: [F64], result: BYTES_U8 }, + "buffer.poolFromStr": { argTypes: [STRING, STRING], result: BYTES_U8 }, + "buffer.poolFromStrChecked": { argTypes: [STRING, STRING], result: BYTES_U8 }, + "buffer.poolFromBytes": { argTypes: [BYTES_U8], result: BYTES_U8 }, + "buffer.poolFromArray": { argTypes: [arrayOf(F64)], result: BYTES_U8 }, + "buffer.poolFromDyn": { argTypes: [DYN, STRING], result: BYTES_U8 }, + /** poolFromDyn that also copies an array crossing by reference. */ + "buffer.poolFromDynRef": { argTypes: [DYN, STRING], result: BYTES_U8 }, + "buffer.poolFromDynOffset": { argTypes: [DYN, DYN], result: BYTES_U8 }, + /** Buffer.from(value, byteOffset, length): an ArrayBuffer's range with + * Node's fromArrayBuffer checks; other values as poolFromDynOffset. */ + "buffer.poolFromDynRange": { argTypes: [DYN, DYN, DYN], result: BYTES_U8 }, + "buffer.poolSize": { argTypes: [], result: F64 }, + "buffer.setPoolSize": { argTypes: [F64], result: VOID }, + /** Buffer.byteLength(string, enc) — enc a NORMALIZED literal like + * fromStr's — and Buffer.isEncoding(name) over a runtime string + * (case-insensitive against Node's alias set). Pure; never throw. */ + "buffer.byteLenDyn": { argTypes: [DYN, STRING], result: F64 }, + /** Buffer.byteLength as a function value: the encoding arrives at run time. */ + "buffer.byteLenValue": { argTypes: [DYN, DYN], result: F64 }, + "buffer.byteLenStr": { argTypes: [STRING, STRING], result: F64 }, + "buffer.isEncoding": { argTypes: [STRING], result: BOOL }, + /** Buffer.concat(list, totalLength): the concatenation truncated or + * zero-padded to the total. THROWS Node's 'length' RangeError on a + * negative/non-integer total (may-throw seed). */ + "buffer.concatLen": { argTypes: [arrayOf(BYTES_U8), F64], result: BYTES_U8 }, + "buffer.concatLenDyn": { argTypes: [DYN, F64], result: BYTES_U8 }, + // The checked-dynamic compare/equals validators (Node's argument + // ladders over dyn-boxed invalid-input probes). + /** The checked-dynamic compare/equals validators (scr_bytes_io.c) — + * the lowered form when an argument is NOT statically bytes (the + * invalid-input probes; a dyn from an untyped JS helper): Node's own + * argument ladders run at runtime — ERR_INVALID_ARG_TYPE with the + * API's argument name ("buf1"/"buf2", "otherBuffer", "target"; offsets + * "of type number"), validateOffset's ERR_OUT_OF_RANGE for bad + * numbers, undefined offsets taking their Node defaults — and a + * well-typed value still computes the real answer. All args borrowed + * dyn; compareChk's four offset slots pass the undefined dyn when + * syntactically absent. May-throw seeds. */ + "buffer.compareChk": { argTypes: [DYN, DYN], result: F64 }, + "bytes.equalsChk": { argTypes: [BYTES_U8, DYN], result: BOOL }, + "bytes.compareChk": { argTypes: [BYTES_U8, DYN, DYN, DYN, DYN, DYN], result: F64 }, + /** The deprecated `new Buffer(number, 'enc')` string-arm rejection: + * always throws Node's ERR_INVALID_ARG_TYPE ("The \"string\" argument + * must be of type string. Received ..."). Borrowed dyn; may-throw. */ + "buffer.newStringFail": { argTypes: [DYN], result: BYTES_U8 }, + "buffer.deprecation": { argTypes: [], result: VOID }, + "buffer.deprecationCount": { argTypes: [], result: VOID }, + "buffer.constructor": { argTypes: [], result: DYN }, + "buffer.installValue": { argTypes: [], result: VOID }, + "buffer.staticValue": { argTypes: [STRING], result: DYN }, + "buffer.prototypeMember": { argTypes: [DYN], result: DYN }, + /** fs._toUnixTimestamp(time) over a dyn value: numeric strings and + * finite numbers coerce (negatives answer now/1000, Node's shape); + * everything else throws Node's ERR_INVALID_ARG_TYPE. May-throw. */ + "fs.toUnixTimestamp": { argTypes: [DYN], result: F64 }, + /** The fs argument-validation ladders (checked-dynamic lane): each Chk + * replicates its API's Node-order validation over dyn values (Node's + * exact typed errors — ERR_INVALID_ARG_TYPE/VALUE, ERR_OUT_OF_RANGE), + * and a full pass meets the trailing compiler-rendered SC2020 fence + * string — so the ALWAYS-THROW forms take the error.nodeThrow + * polymorphic-result carve-out. mkdtempSyncChk and the lchmod sync/ + * promise pair run the REAL operation on a validated pass instead + * (macOS lchmod(2); non-APPLE answers Node's not-a-function / + * ERR_METHOD_NOT_IMPLEMENTED shapes). May-throw seeds, all of them. */ + "fs.existsChk": { argTypes: [DYN, DYN], result: DYN }, + "fs.mkdtempChk": { argTypes: [DYN, DYN, STRING], result: VOID }, + "fs.mkdtempSyncChk": { argTypes: [DYN, DYN, STRING], result: STRING }, + "fs.readFileChk": { argTypes: [DYN, DYN, DYN, STRING], result: VOID }, + "fs.opendirChk": { argTypes: [DYN, DYN, STRING], result: VOID }, + "fs.watchFileChk": { argTypes: [DYN, DYN, STRING], result: VOID }, + "fs.lchmodChk": { argTypes: [DYN, DYN, DYN, STRING], result: VOID }, + "fs.lchmodSyncChk": { argTypes: [DYN, DYN], result: DYN }, + "fsp.lchmodChk": { argTypes: [DYN, DYN], result: { kind: "promise", inner: VOID } }, + "fs.readChk": { argTypes: [DYN, DYN, DYN, DYN, DYN, STRING], result: VOID }, + "fs.streamOptsChk": { argTypes: [DYN, DYN, STRING], result: VOID }, + /** fs.ReadStream / fs.WriteStream (scr_fs_stream.c): the constructors + * take the path and options as dyn values and throw Node's option + * errors; the members read bytesRead/bytesWritten, pending and the path + * value, and close() / close(cb) (a `() => void` callback). */ + "fs.createReadStream": { + argTypes: [DYN, DYN], + result: { kind: "object", className: "%Readable" }, + }, + "fs.createWriteStream": { + argTypes: [DYN, DYN], + result: { kind: "object", className: "%Writable" }, + }, + "fs.fsStreamBytes": { argTypes: [null], result: F64 }, + "fs.fsStreamPending": { argTypes: [null], result: BOOL }, + "fs.fsStreamPath": { argTypes: [null], result: DYN }, + "fs.fsStreamPropDyn": { argTypes: [null, STRING], result: DYN }, + "fileHandle.createReadStream": { + argTypes: [FILEHANDLE_T, DYN], + result: { kind: "object", className: "%Readable" }, + }, + "fileHandle.createWriteStream": { + argTypes: [FILEHANDLE_T, DYN], + result: { kind: "object", className: "%Writable" }, + }, + "fs.fsStreamClose": { argTypes: [null], result: VOID }, + "fs.fsStreamCloseCb": { argTypes: [null, funcOf([], VOID)], result: VOID }, + /** The compiler-resolved ERR_INVALID_ARG_TYPE throw with a RUNTIME- + * rendered Received tail: args [argname, "of type ..." clause, the + * offending dyn value]. ALWAYS THROWS; polymorphic result (the + * error.nodeThrow pattern). May-throw seed. */ + "error.argTypeThrow": { argTypes: [STRING, STRING, DYN], result: VOID }, + /** The property flavor of argTypeThrow ("The \"options.x\" property + * must be ..."): the option-bag ladders' provably-invalid arms. ALWAYS + * THROWS; polymorphic result. May-throw seed. */ + "error.propTypeThrow": { argTypes: [STRING, STRING, DYN], result: VOID }, + /** The checked-dynamic max-listeners ladders: setMaxChk is the + * instance form over a dyn n (non-numbers ERR_INVALID_ARG_TYPE, + * negatives/NaN ERR_OUT_OF_RANGE; +1 receiver back — chaining); + * setDefaultMaxChk is the static/property form, its second argument + * naming the message slot ("setMaxListeners" for the static call, + * "defaultMaxListeners" for the module-property assignment). */ + "emitter.setMaxChk": { argTypes: [null, DYN], result: VOID }, + "emitter.setDefaultMaxChk": { argTypes: [DYN, STRING], result: VOID }, + /** The Buffer forms of fs: readFileSync(path) with NO + * encoding → bytes (+1), writeFileSync/appendFileSync(path, bytes), and the + * fs/promises readFile(path) no-encoding form (an already-settled + * promise, rejecting on failure like the other fsp members). The sync + * forms THROW catchably on failure exactly like the utf8 forms. */ + "fs.readFileSyncBytes": { argTypes: [STRING], result: BYTES_U8 }, + "fs.writeFileSyncBytes": { argTypes: [STRING, BYTES_U8], result: VOID }, + "fs.appendFileSyncBytes": { argTypes: [STRING, BYTES_U8], result: VOID }, + "fsp.readFileBytes": { argTypes: [STRING], result: { kind: "promise", inner: BYTES_U8 } }, + /** node:zlib (scr_zlib.c — native-toolchain.ts compiles/links it ONLY when these + * appear on the IR, the regex/libcurl gating precedent): one-shot zlib, + * raw-DEFLATE, gzip, and auto-detect codecs over u8 bytes with Node's + * default options, or a validated literal compression level from -1 to 9. + * Compression never throws (OOM aborts); decompression + * of corrupt input THROWS Node's error catchably. */ + "zlib.deflateSync": { argTypes: [BYTES_U8], result: BYTES_U8 }, + "zlib.inflateSync": { argTypes: [BYTES_U8], result: BYTES_U8 }, + "zlib.deflateRawSync": { argTypes: [BYTES_U8], result: BYTES_U8 }, + /** bytes, mode (0 zlib / 1 raw / 2 gzip), validated compression level. */ + "zlib.deflateLevelSync": { argTypes: [BYTES_U8, F64, F64], result: BYTES_U8 }, + "zlib.inflateRawSync": { argTypes: [BYTES_U8], result: BYTES_U8 }, + "zlib.gzipSync": { argTypes: [BYTES_U8], result: BYTES_U8 }, + "zlib.gunzipSync": { argTypes: [BYTES_U8], result: BYTES_U8 }, + "zlib.unzipSync": { argTypes: [BYTES_U8], result: BYTES_U8 }, + /** The default-options callback twins run on the executable's shared + * native work pool and deliver (Error | null, Buffer) on a later loop + * turn. crc32 validates its optional uint32 seed synchronously. */ + "zlib.deflateCb": { argTypes: [BYTES_U8, null], result: VOID }, + "zlib.inflateCb": { argTypes: [BYTES_U8, null], result: VOID }, + "zlib.deflateRawCb": { argTypes: [BYTES_U8, null], result: VOID }, + "zlib.inflateRawCb": { argTypes: [BYTES_U8, null], result: VOID }, + "zlib.gzipCb": { argTypes: [BYTES_U8, null], result: VOID }, + "zlib.gunzipCb": { argTypes: [BYTES_U8, null], result: VOID }, + "zlib.unzipCb": { argTypes: [BYTES_U8, null], result: VOID }, + "zlib.crc32": { argTypes: [BYTES_U8, F64], result: F64 }, + /** The one-shot forms with an options value: node_zlib_mode, the input, + * the options (validated at runtime with Node's ladder). The Cb form + * takes the program-shaped (Error | null, Buffer) callback, which moves. */ + "zlib.syncOpts": { argTypes: [F64, BYTES_U8, DYN], result: BYTES_U8 }, + "zlib.cbOpts": { argTypes: [F64, BYTES_U8, DYN, null], result: VOID }, + /** The checked-dynamic input twins (the JS lane's untyped data). */ + "zlib.syncOptsDyn": { argTypes: [F64, DYN, DYN], result: BYTES_U8 }, + "zlib.cbOptsDyn": { argTypes: [F64, DYN, DYN, null], result: VOID }, + /** util.promisify over the callback one-shots: [mode, data, options] + * like zlib.cbOpts → a promise the worker-pool run settles with the + * Buffer or the callback form's Error. An options or input error rejects + * it; never throws. The Dyn twin takes the JS lane's untyped data. */ + "zlib.promise": { + argTypes: [F64, BYTES_U8, DYN], + result: { kind: "promise", inner: BYTES_U8 }, + }, + "zlib.promiseDyn": { argTypes: [F64, DYN, DYN], result: { kind: "promise", inner: BYTES_U8 } }, + /** The stream classes (scr_zlib_stream.c, linked with the stream unit): + * streamNew takes Node's node_zlib_mode number and the options value + * (undefined when absent), validates it with Node's ladder, and answers + * the +1 %Transform stream. flush takes the kind value, params the level + * and strategy values; the Cb forms move their () => void callback. The + * receivers are borrowed. */ + /** Connects scr_brotli.c to the zlib engine (%main, before any module). */ + "zlib.brotliInstall": { argTypes: [], result: VOID }, + "zlib.streamNew": { argTypes: [F64, DYN], result: VOID }, + "zlib.streamBytesWritten": { argTypes: [null], result: F64 }, + /** `stream instanceof zlib.Gzip`: the borrowed stream and the class's + * node_zlib_mode; true when the stream is a zlib stream of that mode. */ + "zlib.streamIs": { argTypes: [null, F64], result: BOOL }, + "zlib.streamFlush": { argTypes: [null, DYN], result: VOID }, + "zlib.streamFlushCb": { argTypes: [null, DYN, null], result: VOID }, + "zlib.streamParams": { argTypes: [null, DYN, DYN], result: VOID }, + "zlib.streamParamsCb": { argTypes: [null, DYN, DYN, null], result: VOID }, + "zlib.streamClose": { argTypes: [null], result: VOID }, + "zlib.streamCloseCb": { argTypes: [null, null], result: VOID }, + "zlib.streamReset": { argTypes: [null], result: VOID }, + /** The Buffer overloads of the raw stream writes — same promptly + * submitted streams as process.stdoutWrite/stderrWrite, constantly true. + * The encoding arg is evaluated but ignored for bytes, like Node. The Cb + * forms move their program-shaped completion callback to the tick queue. */ + "process.stdoutWriteBytes": { argTypes: [BYTES_U8, STRING], result: BOOL }, + "process.stderrWriteBytes": { argTypes: [BYTES_U8, STRING], result: BOOL }, + // Completion callback is program-dependent: zero params, checked-dynamic, + // or an optional Error | null slot (same success shape as fs.rename). + "process.stdoutWriteBytesCb": { argTypes: [BYTES_U8, STRING, null], result: BOOL }, + "process.stderrWriteBytesCb": { argTypes: [BYTES_U8, STRING, null], result: BOOL }, + /** The value of a FULFILLED promise, read without a microtask turn (+1 + * for references): the tuple form of Promise.all reads its entries this + * way once the combinator fulfills, each entry widened to Promise + * and read at the type it was built with (program-dependent). */ + "promise.fulfilledValue": { argTypes: [null], result: VOID }, + /** Promise.all's tuple form as a value: the entries (each widened to + * Promise) join the runtime combinator, and when it fulfills the + * function reads the fulfilled entries into the tuple inside that + * settlement, so the result settles on the combinator's turn; a + * rejection settles it the same way. The result is the tuple's promise + * (program-dependent). */ + "promise.allTuple": { argTypes: [null, null], result: VOID }, + "fsp.readFile": { argTypes: [STRING, STRING], result: { kind: "promise", inner: STRING } }, + "fsp.writeFile": { argTypes: [STRING, STRING], result: { kind: "promise", inner: VOID } }, + /** fs.promises.writeFile(path, data, { mode }): the settled-promise + * twin of fs.writeFileModeSync. */ + "fsp.writeFileMode": { + argTypes: [STRING, STRING, F64], + result: { kind: "promise", inner: VOID }, + }, + "fsp.mkdir": { argTypes: [STRING], result: { kind: "promise", inner: VOID } }, + /** The fs/promises option/member tail the certs pipeline uses: mkdir's + * literal { recursive?, mode? } options (the mkdirSync matrix behind + * settled promises), unlink, chmod. Failures REJECT (catchable at the + * await), like the rest of the fsp family. */ + "fsp.mkdirMode": { argTypes: [STRING, F64], result: { kind: "promise", inner: VOID } }, + "fsp.mkdirRecursive": { argTypes: [STRING], result: { kind: "promise", inner: VOID } }, + "fsp.mkdirRecursiveMode": { argTypes: [STRING, F64], result: { kind: "promise", inner: VOID } }, + "fsp.unlink": { argTypes: [STRING], result: { kind: "promise", inner: VOID } }, + "fsp.chmod": { argTypes: [STRING, F64], result: { kind: "promise", inner: VOID } }, + "fsp.chown": { argTypes: [STRING, F64, F64], result: { kind: "promise", inner: VOID } }, + "fsp.lchown": { argTypes: [STRING, F64, F64], result: { kind: "promise", inner: VOID } }, + "fsp.access": { argTypes: [STRING, F64], result: { kind: "promise", inner: VOID } }, + "fsp.truncate": { argTypes: [STRING, F64], result: { kind: "promise", inner: VOID } }, + "fsp.appendFile": { argTypes: [STRING, STRING], result: { kind: "promise", inner: VOID } }, + "fsp.appendFileBytes": { + argTypes: [STRING, BYTES_U8], + result: { kind: "promise", inner: VOID }, + }, + "fsp.copyFile": { argTypes: [STRING, STRING], result: { kind: "promise", inner: VOID } }, + "fsp.mkdtemp": { argTypes: [STRING], result: { kind: "promise", inner: STRING } }, + /** readFile(path, encoding | { encoding }) for any runtime encoding name. */ + "fsp.readFileEncoded": { + argTypes: [STRING, STRING], + result: { kind: "promise", inner: STRING }, + }, + "fsp.rmdir": { argTypes: [STRING], result: { kind: "promise", inner: VOID } }, + "fsp.link": { argTypes: [DYN, DYN], result: { kind: "promise", inner: VOID } }, + "fsp.symlink": { argTypes: [DYN, DYN, DYN], result: { kind: "promise", inner: VOID } }, + "fsp.cp": { argTypes: [DYN, DYN, DYN], result: { kind: "promise", inner: VOID } }, + "fsp.readlink": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, + "fsp.statfs": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, + "fsp.readlinkStr": { argTypes: [DYN, DYN], result: { kind: "promise", inner: STRING } }, + "fsp.readlinkBuffer": { argTypes: [DYN, DYN], result: { kind: "promise", inner: BYTES_U8 } }, + "fsp.utimes": { argTypes: [DYN, DYN, DYN], result: { kind: "promise", inner: VOID } }, + "fsp.lutimes": { argTypes: [DYN, DYN, DYN], result: { kind: "promise", inner: VOID } }, + "fsp.rename": { argTypes: [STRING, STRING], result: { kind: "promise", inner: VOID } }, + "fsp.readdir": { argTypes: [STRING], result: { kind: "promise", inner: arrayOf(STRING) } }, + // Result is promise; the libCall case + // validates the program-dependent record shape in validate.ts. + /** `fs.promises.readdir(path, { withFileTypes: true })` — the settled- + * promise twin of fs.readdirTypesSync. The backends assemble the same + * call-site-shaped Dirent rows, then move the array into a promise; + * scandir failure becomes its rejection. */ + "fsp.readdirTypes": { argTypes: [STRING], result: { kind: "promise", inner: VOID } }, + "fsp.rm": { argTypes: [STRING], result: { kind: "promise", inner: VOID } }, + /** `fs.promises.rm(path, options)`: fs.rmOptsSync and fs.rmRetrySync + * behind a settled promise; their errors become its rejection. */ + "fsp.rmOpts": { argTypes: [STRING, BOOL, BOOL], result: { kind: "promise", inner: VOID } }, + "fsp.rmRetry": { + argTypes: [STRING, BOOL, BOOL, F64, F64], + result: { kind: "promise", inner: VOID }, + }, + "fsp.stat": { argTypes: [STRING], result: { kind: "promise", inner: STATS_T } }, + "fsp.realpath": { argTypes: [STRING], result: { kind: "promise", inner: STRING } }, + "fsp.lstat": { argTypes: [STRING], result: { kind: "promise", inner: STATS_T } }, + /** fs/promises.open and the statically represented FileHandle surface. + * Every operation returns an already-settled promise; syscall failures + * become rejections rather than escaping synchronously. read/write + * results are call-site record shapes ({ bytesRead/bytesWritten, + * buffer }) assembled by the backends around the fixed runtime ABI. */ + "fsp.open": { + argTypes: [STRING, STRING, F64], + result: { kind: "promise", inner: FILEHANDLE_T }, + }, + "fileHandle.fd": { argTypes: [FILEHANDLE_T], result: F64 }, + "fileHandle.close": { argTypes: [FILEHANDLE_T], result: { kind: "promise", inner: VOID } }, + "fileHandle.sync": { argTypes: [FILEHANDLE_T], result: { kind: "promise", inner: VOID } }, + "fileHandle.datasync": { argTypes: [FILEHANDLE_T], result: { kind: "promise", inner: VOID } }, + "fileHandle.truncate": { + argTypes: [FILEHANDLE_T, F64], + result: { kind: "promise", inner: VOID }, + }, + "fileHandle.chmod": { argTypes: [FILEHANDLE_T, F64], result: { kind: "promise", inner: VOID } }, + "fileHandle.utimes": { + argTypes: [FILEHANDLE_T, DYN, DYN], + result: { kind: "promise", inner: VOID }, + }, + "fileHandle.readv": { + argTypes: [FILEHANDLE_T, arrayOf(BYTES_U8), F64], + result: { kind: "promise", inner: VOID }, + }, + "fileHandle.writev": { + argTypes: [FILEHANDLE_T, arrayOf(BYTES_U8), F64], + result: { kind: "promise", inner: VOID }, + }, + // read/write carry call-site result record shapes; the validator checks + // those in validate.ts, so promise is only a table sentinel. + "fileHandle.read": { + argTypes: [FILEHANDLE_T, BYTES_U8, F64, F64, F64, BOOL], + result: { kind: "promise", inner: VOID }, + }, + "fileHandle.writeBytes": { + argTypes: [FILEHANDLE_T, BYTES_U8, F64, F64, F64, BOOL], + result: { kind: "promise", inner: VOID }, + }, + "fileHandle.writeStr": { + argTypes: [FILEHANDLE_T, STRING, F64, STRING], + result: { kind: "promise", inner: VOID }, + }, + "fileHandle.readFile": { + argTypes: [FILEHANDLE_T, STRING], + result: { kind: "promise", inner: STRING }, + }, + "fileHandle.readFileBytes": { + argTypes: [FILEHANDLE_T, STRING], + result: { kind: "promise", inner: BYTES_U8 }, + }, + "fileHandle.writeFile": { + argTypes: [FILEHANDLE_T, STRING, STRING], + result: { kind: "promise", inner: VOID }, + }, + "fileHandle.writeFileBytes": { + argTypes: [FILEHANDLE_T, BYTES_U8, STRING], + result: { kind: "promise", inner: VOID }, + }, + "fileHandle.stat": { argTypes: [FILEHANDLE_T], result: { kind: "promise", inner: STATS_T } }, + "process.argv": { argTypes: [], result: arrayOf(STRING) }, + // The Node runtime flag a node:test binary took out of process.argv + // (--test-update-snapshots), or [] — a fresh array per read. + "process.execArgv": { argTypes: [], result: arrayOf(STRING) }, + "process.platform": { argTypes: [], result: STRING }, + // The one libCall whose result type is program-dependent (union ids are + // per-module): the `result` here is a placeholder — the libCall case + // checks the union's ARMS ([string, undefinedT] in canonical order) + // against the module's registry instead. + /** getenv(3): one string key arg → the interned `string | undefined` + * union (present: +1 string wrapped into the string arm; absent: the + * interned undefined-arm instance). BOTH source forms — `process.env.FOO` + * and `process.env[expr]` — lower here. Purely static; never throws. */ + "process.envGet": { argTypes: [STRING], result: VOID }, + /** setenv(3): (name, value) string args → void. Later envGet reads and + * spawned children observe the write, like Node (values are strings — + * the frontend fences non-string RHS). Never throws. */ + "process.envSet": { argTypes: [STRING, STRING], result: VOID }, + /** `delete process.env[NAME]` — unsetenv(3): the mutation is visible to + * every later read (process.envGet asks getenv fresh) and inherited by + * spawned children, exactly Node. Statement position only (JS's boolean + * result is constant true there). Borrows the name; never throws. */ + "process.envUnset": { argTypes: [STRING], result: VOID }, + /** The whole environment as alternating [k0, v0, k1, v1, ...] strings in + * environ order — the raw material of the process.env SNAPSHOT record + * (the frontend's interned %env.snapshot helper keyed-writes the pairs + * into a fresh `{ [k: string]: string | undefined }` record). Fresh +1 + * array; never throws. */ + "process.envPairs": { argTypes: [], result: arrayOf(STRING) }, + "process.exit": { argTypes: [F64], result: VOID }, + /** Numeric process.exitCode write (integer validation, implicit exit status). */ + "process.setExitCode": { argTypes: [F64], result: VOID }, + /** The process.exitCode write of a program that also reads the + * property: the same validation, and the code counts as assigned. */ + "process.setExitCodeTracked": { argTypes: [F64], result: VOID }, + /** The code for process.exit() with no argument, or zero when unset. */ + "process.currentExitCode": { argTypes: [], result: F64 }, + /** Whether process.exitCode reads a code rather than undefined. Never throws. */ + "process.hasExitCode": { argTypes: [], result: BOOL }, + "process.cwd": { argTypes: [], result: STRING }, + /** getpid(2) / getuid(2): zero args → f64. POSIX-only target, so both + * always answer (the checker's `getuid?` optionality covers Windows — + * `process.getuid?.()` lowers as the plain call). Never throw. */ + "process.pid": { argTypes: [], result: F64 }, + "dyn.this": { argTypes: [], result: DYN }, + "dyn.generatorThis": { argTypes: [], result: DYN }, + "process.getuid": { argTypes: [], result: F64 }, + "process.getgid": { argTypes: [], result: F64 }, + /** geteuid(2) / getegid(2) / getgroups(2) (the effective gid appended + * when the list lacks it, like Node). Windows has no such members: the + * calls throw the TypeError a missing member does (MAY THROW). */ + "process.geteuid": { argTypes: [], result: F64 }, + "process.getegid": { argTypes: [], result: F64 }, + "process.getgroups": { argTypes: [], result: arrayOf(F64) }, + /** process.ppid: the parent's pid at the read. Never throws. */ + "process.ppid": { argTypes: [], result: F64 }, + /** process.title: libuv's process title (argv[0] until set); setTitle + * writes it (truncated to the original argument area on POSIX, the + * console title on Windows). Never throw. */ + "process.title": { argTypes: [], result: STRING }, + "process.setTitle": { argTypes: [STRING], result: VOID }, + /** process.argv0: the original argv[0]. Never throws. */ + "process.argv0": { argTypes: [], result: STRING }, + /** process.release: the compatibility target's stable release object + * (name, lts, sourceUrl, headersUrl, and libUrl on Windows). +1. */ + "process.release": { argTypes: [], result: DYN }, + /** process.execPath: the compiled binary's own resolved absolute path + * (one interned string, +1 per read) — the honest answer where Node's + * is the node executable's (SEMANTICS.md divergence 12). Never throws. */ + "process.execPath": { argTypes: [], result: STRING }, + /** process.arch: the compiled binary's OWN architecture ("arm64", + * "x64") — Node's answer for its own build on the same machine. + * Interned; +1 per read. Never throws. */ + "process.arch": { argTypes: [], result: STRING }, + /** Stable native version dictionary. node/openssl identify compatibility + * targets; components absent from the native runtime are omitted. */ + "process.versions": { argTypes: [], result: DYN }, + "process.builtinId": { argTypes: [DYN, arrayOf(STRING)], result: STRING }, + "process.builtinModule": { argTypes: [STRING, DYN], result: DYN }, + "process.builtinUnsupported": { argTypes: [STRING, STRING], result: DYN }, + /** A module namespace object as a checked-dynamic value + * (scr_module_namespace_value): the module id, its sorted export names + * and the compiled getter. One object per module. Never throws. */ + "module.namespaceValue": { argTypes: [STRING, arrayOf(STRING), DYN], result: DYN }, + "fs.callbackValue": { argTypes: [STRING], result: DYN }, + "fs.callbackCall": { argTypes: [STRING, DYN], result: DYN }, + "process.hrtimeValue": { argTypes: [], result: DYN }, + /** process.versions.node: the runtime's Node COMPATIBILITY TARGET — + * there is no Node under the binary, so this reports the version whose + * semantics the runtime implements (SEMANTICS.md divergence 60, the + * execPath stance). Interned; +1 per read. Never throws. */ + "process.versionsNode": { argTypes: [], result: STRING }, + "process.versionsOpenssl": { argTypes: [], result: STRING }, + /** kill(2) with Node's exact semantics and error shapes: the pid must be + * an int32 (else the ERR_INVALID_ARG_TYPE TypeError text), the named + * form resolves Node's signal-name table (unknown names throw the + * ERR_UNKNOWN_SIGNAL TypeError), an omitted signal completes to + * "SIGTERM" in the frontend, signal 0 probes, and a kill(2) failure + * throws Node's `kill ESRCH`/`kill EPERM` Error. Result is Node's + * constant true. */ + "process.kill": { argTypes: [F64, STRING], result: BOOL }, + "process.killNum": { argTypes: [F64, F64], result: BOOL }, + /** The raw byte writes: one borrowed string arg → bool (constantly true + * — this synchronous runtime never queues backpressure). stdoutWrite + * shares console.log's stream, each call is submitted promptly, and + * interleaved output keeps source order. No newline, no formatting, + * never throws. */ + "process.stdoutWrite": { argTypes: [STRING], result: BOOL }, + "process.stdio": { argTypes: [F64], result: DYN }, + "process.stderrWrite": { argTypes: [STRING], result: BOOL }, + // error.new's result and the receiver slots are builtin-error classes — + // program-dependent object types, checked in the libCall case. + /** The runtime-provided Error hierarchy's entry points (scr_error.c). + * error.new: one borrowed string arg (the message), result an owned (+1) + * builtin error instance — the result TYPE names which builtin class + * (backends derive the runtime kind from it). error.ctor: the + * super(message) call of an `extends Error` constructor — borrowed + * receiver (already allocated by the derived class's new) + borrowed + * message, void; the receiver's type names the builtin class whose name + * field to stamp. error.toString: borrowed `%Error`-typed receiver, +1 + * string in Node's "name: message" shape. Formatting can invoke a user + * message accessor, which can throw. */ + "error.new": { argTypes: [STRING], result: VOID }, + /** ECMAScript constructors with raw checked-dynamic message/options. + * Options retain cause presence; constructor calls borrow their args. + * Message coercion may throw before the cause is installed. */ + "error.newOptions": { argTypes: [DYN, DYN], result: VOID }, + "error.ctorOptions": { argTypes: [null, DYN, DYN], result: VOID }, + /** Borrowed Error receiver. cause returns an owned dyn value (undefined + * when absent); hasCause distinguishes absence from present undefined. */ + "error.cause": { argTypes: [null], result: DYN }, + "error.hasCause": { argTypes: [null], result: BOOL }, + /** Assignment borrows both operands and retains the new cause. */ + "error.setCause": { argTypes: [null, DYN], result: VOID }, + /** `err.code = s` on an error-hierarchy receiver: the code slot that + * error.code reads and coded throws stamp. Borrows both; never throws. */ + "error.setCode": { argTypes: [null, STRING], result: VOID }, + /** `err.code = v` with a value that is not statically a string: strings + * take the code slot, anything else clears it and sets the dynamic own + * property. Borrows both; never throws. */ + "error.setCodeDyn": { argTypes: [null, DYN], result: VOID }, + /** `Object.assign(err, ...sources)` onto an error-hierarchy receiver + * (args: the error, a dyn array of the sources): the own-member copy + * onto its checked-dynamic node, then the typed slots (name, message, + * stack, cause, code) re-read from it. Throws what the copy throws. */ + "error.assign": { argTypes: [null, DYN], result: VOID }, + /** `key in err` for a key no compiled class of the receiver declares + * (args: the error, the key): a runtime-set own property (Object.assign, + * throw-site fields, dynamic writes) or an Object.prototype member. */ + "error.hasKey": { argTypes: [null, STRING], result: BOOL }, + "error.hasCode": { argTypes: [null], result: BOOL }, + /** Whether the error's own `name` or `message` (the key) is an accessor + * a reflective definition installed. Borrows both; never throws. */ + "error.ownAccessor": { argTypes: [null, STRING], result: BOOL }, + /** That accessor's getter called on the error (owned result), and its + * setter called with the value (V8's TypeError without one). Borrow + * their arguments; throw what the accessor throws. */ + "error.accessorGet": { argTypes: [null, STRING], result: DYN }, + "error.accessorSet": { argTypes: [null, STRING, DYN], result: VOID }, + "error.defineCause": { argTypes: [null, DYN], result: VOID }, + "error.deleteCause": { argTypes: [null], result: VOID }, + /** The compiler-resolved Node-parity throw for always-throwing lowered + * arms (ERR_INVALID_THIS receivers, ERR_MISSING_ARGS arity ladders, + * the symbol-to-string TypeError): args are [error-kind f64 (the + * SCR_ERR_* index: 0 Error, 1 TypeError, 2 RangeError), code (empty = + * no code slot), message]. ALWAYS THROWS catchably; the result type is + * the replaced expression's own (never materialized — the + * global.undefRead pattern). May-throw seed. */ + "error.nodeThrow": { argTypes: [F64, STRING, STRING], result: VOID }, + /** JS ToString over a dyn value WITH the object protocol (a user + * toString/valueOf member is CALLED and its throw propagates; + * exhaustion throws "Cannot convert object to primitive value"; units + * render "null"/"undefined") — the WHATWG USVString conversions + * (URLSearchParams names/values). Borrowed dyn; +1 string. May-throw. */ + "dyn.toStringCoerce": { argTypes: [DYN], result: STRING }, + "dyn.stringConstructor": { argTypes: [DYN], result: STRING }, + "dyn.propertyKey": { argTypes: [DYN], result: DYN }, + "dyn.toNumeric": { argTypes: [DYN], result: DYN }, + "dyn.increment": { argTypes: [DYN, BOOL], result: DYN }, + /** JS ToNumber over a dyn value WITH the object protocol (number-hint + * valueOf/toString ordering; user throws propagate). Borrowed dyn; + * f64 result, or a throw. Used by statically lowered numeric coercions + * whose checker type remained any. */ + "dyn.numberConstructor": { argTypes: [DYN], result: F64 }, + "dyn.bigintConstructor": { argTypes: [DYN], result: BIGINT_T }, + "dyn.toNumberCoerce": { argTypes: [DYN], result: F64 }, + "dyn.add": { argTypes: [DYN, DYN], result: DYN }, + "dyn.arithmetic": { argTypes: [DYN, DYN, STRING], result: DYN }, + "dyn.compare": { argTypes: [DYN, DYN, STRING], result: BOOL }, + "dyn.bitwise": { argTypes: [DYN, DYN, STRING], result: DYN }, + "dyn.proxyNew": { argTypes: [DYN, DYN], result: DYN }, + /* Dynamic-tier values (jsval; two-tier design stage S3). The jsval pass + * (ir/jsval.ts) introduces these in dynamic-tier functions only; the + * runtime is scr_js_value.c. Arguments are borrowed and results owned, + * like every libCall. Boundary conversions: js.fromDyn (a cell keeps its + * identity), js.toDyn (an immediate is boxed), js.fromF64/fromBool/ + * fromStr/fromBigInt (the design's toJs: immediates, and strings and + * BigInts under a tag without a copy) and js.toF64 (fromJs to number: dynCheck's + * number check and TypeError). The operators are the dyn.* operators + * over jsvals, with identical JS semantics. */ + "js.fromDyn": { argTypes: [DYN], result: JSVAL }, + "js.toDyn": { argTypes: [JSVAL], result: DYN }, + "js.fromF64": { argTypes: [F64], result: JSVAL }, + "js.fromBool": { argTypes: [BOOL], result: JSVAL }, + "js.fromStr": { argTypes: [STRING], result: JSVAL }, + "js.fromBigInt": { argTypes: [BIGINT_T], result: JSVAL }, + "js.toF64": { argTypes: [JSVAL], result: F64 }, + "js.add": { argTypes: [JSVAL, JSVAL], result: JSVAL }, + "js.arithmetic": { argTypes: [JSVAL, JSVAL, STRING], result: JSVAL }, + "js.bitwise": { argTypes: [JSVAL, JSVAL, STRING], result: JSVAL }, + "js.compare": { argTypes: [JSVAL, JSVAL, STRING], result: BOOL }, + "js.toNumeric": { argTypes: [JSVAL], result: JSVAL }, + "js.increment": { argTypes: [JSVAL, BOOL], result: JSVAL }, + "js.toNumberCoerce": { argTypes: [JSVAL], result: F64 }, + // Always throws; the result is the READ's declared type (a typed dummy + // the unwind abandons) — the libCall case skips the result check. + /** A read of a `declare`d const NOTHING defines (the bundler-define + * pattern — __VERSION__): always throws the catchable ReferenceError + * Node raises at the access (" is not defined"). args[0] is the + * name; the result type is the read's declared type (a typed dummy the + * unwind abandons — the value never exists). */ + "global.undefRead": { argTypes: [STRING], result: VOID }, + "console.native": { argTypes: [], result: DYN }, + /** node:console Console instances (scr_console_native.c): new mints the + * handle over the stdout/stderr fds (1/2; 0 = a stream binds next) and + * the ignoreErrors flag; bind attaches a borrowed writable stream to + * target 0 (stdout) or 1 (stderr); write prints `text\n` to the + * selected target of an instance or the global console handle. */ + "console.new": { argTypes: [F64, F64, BOOL], result: DYN }, + "console.bind": { argTypes: [DYN, F64, null], result: VOID }, + "console.write": { argTypes: [DYN, STRING, BOOL], result: VOID }, + /** The global console's remaining members (scr_console_native.c; the + * group indentation itself lives with the writers in scr_console.c). + * Arguments are borrowed; string results are owned. noop is the + * emitter-dropped tail of a lowering whose effects sit in statements. + * label is `${label}` over a defaulted label (undefined -> "default"; + * a Symbol throws Node's TypeError). count prints "label: n". + * countReset answers the warning text for an unknown (or non-string) + * label, "" otherwise. time answers the duplicate-label warning text + * or ""; timeEnd/timeLog answer Node's formatted elapsed time, or "" + * for an unknown label (timeEnd drops the timer). group adds (+1) or + * removes (-1) one two-space indentation step. clear writes the + * cursor-home and erase-down sequences when stdout is a terminal. + * table renders and prints Node's table for a dyn value (undefined + * properties = all). dirDyn is console.dir's inspect of a dyn value + * (custom inspection off: a Buffer anywhere inside throws the fence). */ + "console.noop": { argTypes: [], result: VOID }, + "console.label": { argTypes: [DYN], result: STRING }, + "console.count": { argTypes: [STRING], result: VOID }, + "console.countReset": { argTypes: [DYN], result: STRING }, + "console.time": { argTypes: [STRING], result: STRING }, + "console.timeEnd": { argTypes: [STRING], result: STRING }, + "console.timeLog": { argTypes: [STRING], result: STRING }, + "console.group": { argTypes: [F64], result: VOID }, + "console.clear": { argTypes: [], result: VOID }, + "console.table": { argTypes: [DYN, DYN], result: VOID }, + "console.dirDyn": { argTypes: [DYN, F64], result: STRING }, + /** A native reference to the global object; known names retain value fences. */ + "global.native": { argTypes: [arrayOf(STRING)], result: DYN }, + /** Installs the program's builtin-global lookup (global-values.ts). */ + "global.installValues": { argTypes: [DYN], result: VOID }, + /** The keys `for (key in globalThis)` visits, as a fresh object. */ + "global.forInObject": { argTypes: [], result: DYN }, + /** Node's navigator global (scr_node_builtin.c). */ + "navigator.object": { argTypes: [], result: DYN }, + "navigator.language": { argTypes: [], result: STRING }, + "navigator.languages": { argTypes: [], result: DYN }, + "navigator.platform": { argTypes: [], result: STRING }, + // `X.name` through a class value: the arg is a program-dependent + // classval (a null slot; the libCall case checks the kind). + /** `X.name` through a class VALUE (scr_object.c): args[0] is a borrowed + * classval; the result is the class object's stored .name string, + * retained (+1 — the string is an interned immortal, so the retain is a + * no-op, kept for ownership uniformity). Never throws. A direct + * `C.name` on the class name itself folds to a strLit instead. */ + "class.name": { argTypes: [null], result: STRING }, + "error.ctor": { argTypes: [null, STRING], result: VOID }, + "error.toString": { argTypes: [null], result: STRING }, + /** SameValue over checked values, including NaN, signed zero and reference identity. */ + "dyn.sameValue": { argTypes: [DYN, DYN], result: BOOL }, + "error.stack": { argTypes: [null], result: STRING }, + "error.setStack": { argTypes: [null, STRING], result: VOID }, + "error.stackLimitGet": { argTypes: [], result: F64 }, + "error.stackLimitSet": { argTypes: [F64], result: VOID }, + // Receiver (any error-hierarchy object) and the program-dependent + // `string | undefined` result are checked in the libCall case. + /** NodeJS.ErrnoException's `.code` read: borrowed error-hierarchy + * receiver → the interned `string | undefined` union (type-directed + * construction in the backend, the process.envGet pattern) — the errno + * name where a throw site stamped one (fs, exec spawn/timeout, + * process.kill, the spawn 'error' event), the undefined arm everywhere + * else. Never throws. */ + "error.code": { argTypes: [null], result: VOID }, + // DOMException: construction takes the two dyn args (WebIDL's + // resolution runs in the runtime); the %DOMException result is a + // program-dependent object type, checked in the libCall case. The + // read surface takes the %DOMException receiver (a null slot — the + // libCall case checks the class name). + /** `new DOMException(message?, nameOrOptions?)` (scr_error.c): both args + * are borrowed dyn values (the lowering passes the dyn undefined for an + * absent argument, so WebIDL's optionality lives in one place). The + * runtime ToStrings the message ("" for absent/undefined), resolves the + * name — absent/undefined → "Error", a non-null object → ToString of its + * `name` member plus the `cause` own-property record, anything else → + * ToString — and stamps the legacy code from the WebIDL name table (0 + * off-table). Result is an owned (+1) %DOMException. Never throws (dyn + * ToString is total). */ + "error.newDom": { argTypes: [DYN, DYN], result: VOID }, + /** DOMException's own read surface (scr_error.c; %DOMException receivers + * only — borrowed). domCode: the legacy numeric code. domHasCause: the + * options form's own-property record (`'cause' in e`). domCause: the + * cause value, +1 (the dyn undefined when absent — matching Node's + * undefined read). None throws. */ + "error.domCode": { argTypes: [null], result: F64 }, + "error.domHasCause": { argTypes: [null], result: BOOL }, + "error.domCause": { argTypes: [null], result: DYN }, + /** structuredClone of a %DOMException receiver (scr_error.c): WebIDL + * serialization — name/message copy, the legacy code re-derives, cause + * does not serialize. args are the borrowed receiver and the borrowed + * options dyn value (the dyn undefined when absent — the shared + * validation throws Node's exact option errors; any non-empty transfer + * list throws DataCloneError, nothing static is transferable). Result + * +1 %DOMException. */ + "error.domClone": { argTypes: [null, DYN], result: VOID }, + /** `new AggregateError(errors, message?, options?)` (scr_json.c), in + * spec order: newAggregate installs the message and cause (both dyn, + * borrowed — the message conversion can throw) and returns an owned + * %AggregateError; aggregateSetErrors then stores the iterated list + * (borrowed receiver, the dyn array borrowed and retained). + * aggregateErrors reads the list (+1, the same array every read). */ + "error.newAggregate": { + argTypes: [DYN, DYN], + result: { kind: "object", className: "%AggregateError" }, + }, + "error.aggregateSetErrors": { argTypes: [null, DYN], result: VOID }, + "error.aggregateErrors": { argTypes: [null], result: DYN }, + /** `new SuppressedError(error, suppressed, message?)` (scr_json.c): all + * three dyn and borrowed; the message conversion can throw. Returns an + * owned %SuppressedError. */ + "error.newSuppressed": { + argTypes: [DYN, DYN, DYN], + result: { kind: "object", className: "%SuppressedError" }, + }, + /** `d instanceof TypeError` (and the other BUILTIN error classes) on a + * checked-dynamic value (scr_json.c): the from_error cache holds the + * dyn↔error identity edge, so the test resolves the runtime error and + * asks its vtable's stamped preorder interval — exact for every error + * that crossed the boundary. A dyn object that never came from an + * error (a hand-built {%error} literal) answers false: subclass + * identity is unknowable there (the root keeps dynTest's marker + * answer). args are the borrowed dyn and the SCR_ERR_* kind literal. + * Never throws. */ + "dyn.errInstanceof": { argTypes: [DYN, F64], result: BOOL }, + "weakMap.is": { argTypes: [DYN], result: BOOL }, + "weakSet.is": { argTypes: [DYN], result: BOOL }, + "weakMap.new": { argTypes: [DYN], result: DYN }, + "weakSet.new": { argTypes: [DYN], result: DYN }, + "finalizationRegistry.new": { argTypes: [DYN], result: DYN }, + "finalizationRegistry.is": { argTypes: [DYN], result: BOOL }, + "dyn.fromEntries": { argTypes: [DYN], result: DYN }, + "bytes.constructor": { argTypes: [STRING], result: DYN }, + "bytes.instanceOf": { argTypes: [DYN, DYN, STRING], result: BOOL }, + "bytes.construct": { argTypes: [DYN, DYN, STRING], result: DYN }, + /** The ambient receiver — JS `this` in a plain (non-method) function + * body: the innermost binding the current firing/dispatch window + * pushed (Node's listener receiver, a dyn OBJ method's object, an + * apply/call thisArg), or the undefined dyn singleton with none bound + * (the strict-mode plain-call answer, the old constant). Zero args → + * dyn (+1). Never throws. */ + "dyn.dataViewIs": { argTypes: [DYN], result: BOOL }, + "dyn.construct": { argTypes: [DYN, DYN, STRING], result: DYN }, + "arrayBuffer.constructor": { argTypes: [], result: DYN }, + "sharedArrayBuffer.new": { argTypes: [DYN], result: DYN }, + "sharedArrayBuffer.is": { argTypes: [DYN], result: BOOL }, + "arrayBuffer.new": { argTypes: [DYN], result: DYN }, + "ffi.argument": { argTypes: [DYN, STRING], result: DYN }, + "ffi.memoryModule": { argTypes: [DYN], result: DYN }, + "intl.segmenterNew": { argTypes: [], result: DYN }, + "intl.segmenterRefusing": { argTypes: [STRING], result: DYN }, + "arrayBuffer.is": { argTypes: [DYN], result: BOOL }, + "arrayBuffer.isView": { argTypes: [DYN], result: BOOL }, + "arrayBuffer.byteLengthGetter": { argTypes: [], result: F64 }, + "arrayBuffer.byteLengthDescriptor": { argTypes: [DYN], result: DYN }, + "arrayBuffer.viewU8C": { argTypes: [DYN, DYN, DYN], result: bytesOf("u8c") }, + "arrayBuffer.viewI8": { argTypes: [DYN, DYN, DYN], result: bytesOf("i8") }, + "arrayBuffer.viewU16": { argTypes: [DYN, DYN, DYN], result: bytesOf("u16") }, + "arrayBuffer.viewI16": { argTypes: [DYN, DYN, DYN], result: bytesOf("i16") }, + "arrayBuffer.viewU8": { argTypes: [DYN, DYN, DYN], result: BYTES_U8 }, + "arrayBuffer.viewU32": { argTypes: [DYN, DYN, DYN], result: bytesOf("u32") }, + "arrayBuffer.viewI32": { argTypes: [DYN, DYN, DYN], result: bytesOf("i32") }, + "arrayBuffer.viewF16": { argTypes: [DYN, DYN, DYN], result: bytesOf("f16") }, + "arrayBuffer.viewF32": { argTypes: [DYN, DYN, DYN], result: bytesOf("f32") }, + "arrayBuffer.viewF64": { argTypes: [DYN, DYN, DYN], result: bytesOf("f64") }, + "arrayBuffer.viewDV": { argTypes: [DYN, DYN, DYN], result: BYTES_U8 }, + /** Object.keys/values/entries over a CHECKED-DYNAMIC receiver + * (scr_json.c): the runtime walks the dyn node's own members in JS + * own-key order (array-index keys ascending first, then insertion + * order) and answers a dyn array (entries: an array of [key, value] + * pairs; values RETAIN the member nodes — reference semantics, like + * JS). Strings/arrays/bytes answer their index keys; other scalars an + * empty array; null/undefined throw Node's catchable TypeError + * ("Cannot convert undefined or null to object"). */ + "dyn.objKeys": { argTypes: [DYN], result: DYN }, + "dyn.forInKeys": { argTypes: [DYN], result: DYN }, + "dyn.forInKeyStrings": { argTypes: [DYN], result: arrayOf(STRING) }, + "dyn.forInStable": { argTypes: [DYN], result: BOOL }, + "dyn.objKeyStrings": { argTypes: [DYN], result: arrayOf(STRING) }, + /** Object.entries as a native [string, any][] (VOID: the tuple row's + * shape is program-dependent; the validator checks it). */ + "dyn.objEntryPairs": { argTypes: [DYN], result: VOID }, + "dyn.hasOwn": { argTypes: [DYN, STRING], result: BOOL }, + "dyn.propertyIsEnumerable": { argTypes: [DYN, STRING], result: BOOL }, + "dyn.assign": { argTypes: [DYN, DYN], result: DYN }, + "dyn.copyDataProperties": { argTypes: [DYN, DYN], result: DYN }, + "dyn.objectRest": { argTypes: [DYN, DYN], result: DYN }, + /** Variadic Object.assign over CHECKED-DYNAMIC targets (`Object.assign( + * {}, ...arr.map(f), tail)` — the option-table merge): the lowering + * builds one fresh dyn pack of sources (packPush retains a plain source + * in; packPushSpread flattens a spread source through the spread-call + * walk — V8's exact TypeError texts, the string arg spelling the spread + * expression for the nullish form), so every source evaluates and + * flattens BEFORE any copying (JS's ArgumentListEvaluation), then + * assignAll copies each pack element's own enumerable keys onto the + * target left to right and answers the TARGET (+1) — identity, like JS. + * assignAll throws Node's ToObject TypeError on a nullish target. */ + "dyn.packPush": { argTypes: [DYN, DYN], result: VOID }, + "dyn.packPushSpread": { argTypes: [DYN, DYN, STRING], result: VOID }, + /** The iterated-path spread twin: V8 spells the optimized apply-path + * texts (packPushSpread's — the expression named for nullish sources) + * only for the SINGLE LAST argument's spread; any other spread position + * drives the real iterator protocol, whose failure describes the VALUE + * ("object null is not iterable (cannot read property + * Symbol(Symbol.iterator))"). The frontend picks by position. */ + "dyn.packPushSpreadIter": { argTypes: [DYN, DYN], result: VOID }, + "dyn.reflectApply": { argTypes: [DYN, DYN, DYN], result: DYN }, + "dyn.reflectGet": { argTypes: [DYN, DYN, DYN], result: DYN }, + /** `using` / `await using` with a dyn value (scr_json.c): usingMethod + * runs the declaration's checks and answers what usingDispose calls at + * scope exit (undefined for a null or undefined value). The bool selects + * `await using`. Both throw Node's TypeErrors or the method's error. */ + "dyn.usingMethod": { argTypes: [DYN, BOOL], result: DYN }, + "dyn.usingDispose": { argTypes: [DYN, DYN], result: DYN }, + /** DisposableStack (scr_symbol.c): a new stack handle; whether a value is + * one; and `stack.use(value)` for a class instance, registering the + * compiler's disposal function for it. The last throws Node's + * ReferenceError on a disposed stack. */ + "disposable.stackNew": { argTypes: [], result: DYN }, + "disposable.stackIs": { argTypes: [DYN], result: BOOL }, + "disposable.stackUseCallback": { argTypes: [DYN, DYN, DYN], result: BOOL }, + "dyn.reflectSet": { argTypes: [DYN, DYN, DYN, DYN], result: BOOL }, + "dyn.reflectDefine": { argTypes: [DYN, DYN, DYN], result: BOOL }, + "dyn.abstractEq": { argTypes: [DYN, DYN], result: BOOL }, + "dyn.assignAll": { argTypes: [DYN, DYN], result: DYN }, - "dyn.objCreateNullProto": { argTypes: [], result: DYN }, - "dyn.arrayPrototype": { argTypes: [], result: DYN }, - "dyn.arrayConstructor": { argTypes: [], result: DYN }, - "dyn.objectPrototype": { argTypes: [], result: DYN }, - "dyn.errorPrototype": { argTypes: [], result: DYN }, - "dyn.inheritsErrorPrototype": { argTypes: [DYN], result: BOOL }, - "dyn.errorCaptureStackTrace": { argTypes: [DYN], result: DYN }, - "dyn.errorCaptureStackTraceFunction": { argTypes: [], result: DYN }, - "dyn.functionApply": { argTypes: [], result: DYN }, - "dyn.builtinMethod": { argTypes: [STRING, STRING], result: DYN }, - "dyn.classPrototype": { argTypes: [DYN, DYN], result: DYN }, - "dyn.classBasePrototype": { argTypes: [DYN], result: DYN }, - "dyn.classInherit": { argTypes: [DYN, DYN], result: DYN }, - /** EventEmitter (0), Stream (1), Readable (2), Writable (3), Duplex (4), - * Transform (5) and PassThrough (6) as untyped constructor values, one - * per thread (scr_dyn_builtin_class.c). */ - "dyn.builtinClass": { argTypes: [F64], result: DYN }, - /** The stream classes (2-6 above) with their stream half: constructor - * functions over them get native stream state - * (scr_dyn_builtin_stream.c). */ - "dyn.builtinStreamClass": { argTypes: [F64], result: DYN }, - /** `value instanceof C` for those constructors, over the value's - * prototype chain. */ - "dyn.builtinInstanceOf": { argTypes: [DYN, F64], result: BOOL }, - /** `value instanceof f` for an untyped constructor function: the - * prototype chain against f.prototype. */ - "dyn.functionInstanceOf": { argTypes: [DYN, DYN], result: BOOL }, - "dyn.classSuper": { argTypes: [DYN, DYN, DYN], result: VOID }, - "dyn.assignPrototype": { argTypes: [DYN, DYN, DYN], result: DYN }, - "dyn.definePrototypeProps": { argTypes: [DYN, DYN, DYN], result: DYN }, - "dyn.bagGet": { argTypes: [DYN, STRING, DYN], result: DYN }, - /** A class field's creation in its instance's property bag: adds the - * string key as a default data property when absent, answering false - * (nothing changed) when the bag needs the general defineProperty path. */ - "dyn.bagCreateField": { argTypes: [DYN, STRING], result: BOOL }, - "dyn.bagSet": { argTypes: [DYN, STRING, DYN, DYN], result: VOID }, - "dyn.objCreate": { argTypes: [DYN], result: DYN }, - // Names a compiled class's prototype object (args[0]) after the class - // (args[1]) for util.inspect. - "dyn.nameClassPrototype": { argTypes: [DYN, STRING], result: VOID }, - "dyn.objCreateWithProperties": { argTypes: [DYN, DYN], result: DYN }, - "dyn.getPrototype": { argTypes: [DYN], result: DYN }, - // dyn.getPrototype for walks that compare each prototype with a program - // class's prototype: native builtin values end the walk (null). - "dyn.getPrototypeForIdentity": { argTypes: [DYN], result: DYN }, - "dyn.setPrototype": { argTypes: [DYN, DYN], result: DYN }, - "dyn.getOwnPropertyNames": { argTypes: [DYN], result: DYN }, - "dyn.ownKeys": { argTypes: [DYN], result: DYN }, - "dyn.getOwnPropertySymbols": { argTypes: [DYN], result: DYN }, - "dyn.getOwnPropertyDescriptors": { argTypes: [DYN], result: DYN }, - "dyn.preventExtensions": { argTypes: [DYN], result: DYN }, - "dyn.isExtensible": { argTypes: [DYN], result: BOOL }, - "dyn.seal": { argTypes: [DYN], result: DYN }, - "dyn.isSealed": { argTypes: [DYN], result: BOOL }, - "dyn.objValues": { argTypes: [DYN], result: DYN }, - /** Object.values as a native vector of checked values, for results the - * program consumes directly (for-of, index reads, read-only methods). */ - "dyn.objValueList": { argTypes: [DYN], result: arrayOf(DYN) }, - "dyn.objEntries": { argTypes: [DYN], result: DYN }, - /** structuredClone over the checked-dynamic tree (scr_json.c): the JSON-safe subset plus - * bytes (a fresh copy — a Buffer clones as a plain Uint8Array, like - * Node), deep. Functions and handle kinds throw the spec's catchable - * DataCloneError; CYCLES throw the scriptc fence (the checked-dynamic tree cannot - * represent them — Node clones cycles; documented divergence). The - * options dyn value validates with Node's exact errors (dictionary - * conversion, the transfer-sequence member; any non-empty transfer - * list throws DataCloneError). dyn.cloneMissing is the zero-argument - * call: always throws Node's TypeError [ERR_MISSING_ARGS] with Node's - * own (verbatim, doubly-wrapped) message. */ - "dyn.structuredClone": { argTypes: [DYN, DYN], result: DYN }, - /** The declaration-typed boundary's adoption (decl-projection.ts): - * `source` is the dynamic value a boundary cast just converted, and - * `capsule` a live capsule of the native value built from it. Every - * object or array of `source` that something else still references - * becomes, in place, a live capsule of the native value built from it. - * Throws the path TypeError, changing nothing, when a converted object - * is not plain data (a prototype, accessors, symbols, frozen state). */ - "dyn.declAdopt": { argTypes: [DYN, DYN], result: VOID }, - "dyn.cloneMissing": { argTypes: [], result: DYN }, - /** structuredClone with a NON-EMPTY transfer array of static values: - * nothing static is transferable, so the call always throws Node's - * catchable DataCloneError ("Found invalid value in transferList.") — - * lowered directly (the list's values need no dyn representation to - * fail). */ - "dyn.cloneTransferFail": { argTypes: [], result: DYN }, - /** The same refusal given the transfer list's first member, an - * ArrayBuffer: a Buffer pool slab gets Node's untransferable message. */ - "dyn.cloneTransferFailWith": { argTypes: [DYN], result: DYN }, - /** `new RegExp(pattern, flags?)` (scr_regex.c): a heap regex over the - * same libregexp engine the literals use. The pattern compiles EAGERLY - * so an invalid pattern (or an unknown flag letter) throws Node's - * catchable SyntaxError at construction — Node's message shape with - * libregexp's detail text (approximate fidelity; e.name exact). An - * empty pattern stores the spec's "(?:)" source. Both args borrowed - * strings (the lowering completes an absent flags to ""); result +1. - * The result TYPE is the regex kind, so the link switch pulls the - * engine exactly like a literal. */ - "regex.new": { argTypes: [STRING, STRING], result: REGEX }, - "regex.resetLastIndex": { argTypes: [REGEX, F64], result: VOID }, - "regex.newChecked": { argTypes: [DYN, DYN], result: REGEX }, - "regex.replaceCallback": { argTypes: [STRING, REGEX, DYN, BOOL], result: STRING }, - // Reads of capture args[1] of an execSpans/matchGroups result: - // `capture === args[2]`, Number(capture) and parseInt(capture, - // args[2]). Borrowed; never throw. - "regex.groupEquals": { argTypes: [arrayOf(STRING), F64, STRING], result: BOOL }, - "regex.groupNumber": { argTypes: [arrayOf(STRING), F64], result: F64 }, - "regex.groupParseInt": { argTypes: [arrayOf(STRING), F64, F64], result: F64 }, - // `.index` / `.input` (args[1]) of a match result. Borrowed; never throws. - "regex.resultProp": { argTypes: [arrayOf(STRING), STRING], result: DYN }, - // The string keys `for...in` visits on an array (args[0], any element - // type) after its indices. Borrowed; never throws. - "array.namedKeys": { argTypes: [null], result: arrayOf(STRING) }, - // Whether the array (args[0]) still has the key (args[1]) from its - // namedKeys list. Borrowed; never throws. - "array.hasNamedKey": { argTypes: [null, STRING], result: BOOL }, - // Whether an array (args[0], any element type) has properties besides its - // elements: noncanonical numeric keys, match metadata or named - // properties. Borrowed; never throws. - "array.hasProperties": { argTypes: [null], result: BOOL }, - // The key of an element access whose index is undefined (the property - // named "undefined"; frontend/lowering/undefined-index.ts). - "array.undefinedKey": { argTypes: [], result: F64 }, - // node:events EventEmitter: receivers are emitter-hierarchy objects and - // the chaining forms (on/off/removeAll/setMax) return the receiver's - // own class — program-dependent object types, checked in the libCall - // case. emitter.emit is the one VARIADIC libCall: args beyond - // (recv, name) are the event's frontend-unified tuple, any borrowable - // kind — the count check admits a longer list for it alone. - /** node:events EventEmitter (scr_events_emitter.c, link-gated by - * moduleUsesEmitter). The receiver of every instance form is a borrowed - * emitter-hierarchy object (`%EventEmitter` or a user subclass — the - * backend reinterprets to ScrEmitter*, the identical prefix); event - * names are borrowed strings (compile-time literals — the frontend - * fences non-literals and unifies each event's argument tuple program- - * wide). The chaining forms (on/off/removeAll/setMax) return the - * receiver +1 typed as its static class, Node's `return this`. - * - * emitter.new: `new EventEmitter()` → a +1 bare emitter. emitter.ctor: - * super() into the prefix of an emitted subclass (borrowed receiver, - * void — allocation already initialized the prefix). emitter.on: - * (recv, name, cb /moves/, once, prepend) — the backend synthesizes the - * per-signature va_list invoke adapter from the cb's func type. - * emitter.emit: (recv, name, ...tuple) — VARIADIC, the one libCall - * whose arg count exceeds its signature; args are borrowed, result is - * the had-listeners bool. emitter.emitError: emit('error', err) — - * throws err when unhandled. count/countFn/names/listeners/getMax/ - * setMax/setDefaultMax/getDefaultMax are the introspection surface. */ - "emitter.new": { argTypes: [], result: VOID }, - /** `x.constructor.name` of an emitter or stream (a borrowed object of - * EventEmitter's hierarchy): the +1 class name the instance records. */ - "emitter.className": { argTypes: [null], result: STRING }, - "emitter.ctor": { argTypes: [null], result: VOID }, - "emitter.on": { argTypes: [null, STRING, null, BOOL, BOOL], result: VOID }, - "emitter.off": { argTypes: [null, STRING, null], result: VOID }, - // The dyn-adapted registration family (JS-lane checked-dynamic - // listeners): checkListener has NO receiver (it validates the listener - // argument alone); onDyn carries (recv, name, cb dyn, adapter func, - // once, prepend), offDyn (recv, name, cb dyn). - "emitter.checkListener": { argTypes: [DYN], result: VOID }, - "emitter.onDyn": { argTypes: [null, STRING, DYN, null, BOOL, BOOL], result: VOID }, - "emitter.onFlex": { argTypes: [null, STRING, DYN, BOOL, BOOL], result: VOID }, - /** on/once/prepend* and emit through an untyped value (the class - * dispatch of compiled emitters): onFlex/emitFlex plus runtime typed - * adapters under 'error' and the meta events, and Node's unhandled-'error' - * contract. emitAny is VARIADIC like emitFlex. */ - "emitter.onAny": { argTypes: [null, STRING, DYN, BOOL, BOOL], result: VOID }, - "emitter.offDyn": { argTypes: [null, STRING, DYN], result: VOID }, - "emitter.removeAll": { argTypes: [null, STRING, BOOL], result: VOID }, - "emitter.emit": { argTypes: [null, STRING], result: BOOL }, - "emitter.emitFlex": { argTypes: [null, STRING], result: BOOL }, - "emitter.emitAny": { argTypes: [null, STRING], result: BOOL }, - /** An untyped emit of an event with a fixed tuple: (recv, name, k, then - * k checked-dynamic arguments as given, then the event's converted - * tuple) — VARIADIC like emitter.emit. hasTyped answers whether a - * listener of the name needs the converted tuple. */ - "emitter.emitMixed": { argTypes: [null, STRING, F64], result: BOOL }, - "emitter.hasTyped": { argTypes: [null, STRING], result: BOOL }, - "emitter.emitError": { argTypes: [null, STRING, null], result: BOOL }, - "emitter.count": { argTypes: [null, STRING], result: F64 }, - "emitter.countFn": { argTypes: [null, STRING, null], result: F64 }, - "emitter.countDyn": { argTypes: [null, STRING, DYN], result: F64 }, - "emitter.names": { argTypes: [null], result: VOID }, - "emitter.listeners": { argTypes: [null, STRING], result: VOID }, - /** listeners()/rawListeners() through an untyped value: a checked- - * dynamic array (throws when a listener has typed parameters). */ - "emitter.listenersDyn": { argTypes: [null, STRING], result: DYN }, - /** Stream-'data' registration twins of emitter.on/onDyn (same args): - * chosen at registration sites whose receiver is stream-rooted, so the - * backend emits DATA thunks — the runtime's 'data' emission carries - * BOTH payload slots (bytes, string; exactly one non-NULL — encoded - * streams deliver strings), and the thunk unwraps the listener's - * declared side (typed) or boxes by tag (dyn). emitter.emitData is the - * user-emit form: (recv, name, chunk) with a bytes OR string chunk. */ - "emitter.onData": { argTypes: [null, STRING, null, BOOL, BOOL], result: VOID }, - "emitter.onDataDyn": { argTypes: [null, STRING, DYN, null, BOOL, BOOL], result: VOID }, - "emitter.emitData": { argTypes: [null, STRING, null], result: BOOL }, - "emitter.setMax": { argTypes: [null, F64], result: VOID }, - "emitter.getMax": { argTypes: [null], result: F64 }, - "emitter.setDefaultMax": { argTypes: [F64], result: VOID }, - "emitter.getDefaultMax": { argTypes: [], result: F64 }, - // The ES module namespace's copy of defaultMaxListeners, and - // module.syncBuiltinESMExports() refreshing it. - "emitter.esmDefaultMax": { argTypes: [], result: F64 }, - "emitter.esmSync": { argTypes: [], result: VOID }, - /** The events.errorMonitor event key: an immortal string the runtime - * registry matches by identity, so no string event name reaches it. */ - "emitter.errorMonitor": { argTypes: [], result: STRING }, - // node:stream: receivers are stream-hierarchy objects and several - // results are program-dependent (the receiver's class, unions) — the - // libCall case owns those checks. The constructors and a few methods - // are VARIADIC (trailing option callbacks / the optional chunk+cb tail): - // argTypes here is the fixed prefix, VARIADIC_LIB_FNS admits the rest. - /** node:stream (scr_stream.c, link-gated by moduleUsesStream — which - * implies the emitter unit: stream events dispatch through the embedded - * ScrEmitter registry). Receivers are borrowed stream-class objects - * (`%Readable`/`%Writable`/`%Duplex`/`%Transform`/`%PassThrough` — one - * runtime layout, reinterpreted by side). - * - * Constructors (`readable.new` et al): args are [hwmR, hwmW, flags] - * followed by the PRESENT user callbacks in canonical order (read, - * write, final, destroy, transform, flush — the flags f64 is a bitmask - * naming which follow; absent ones emit NULL). Every callback closure - * MOVES and carries a leading `this` param (the stream), invoked - * through compiler-emitted adapters. Results are +1. - * - * readable.push / readable.pushStr / readable.pushNull: Node's push — - * buffers or delivers (bytes chunk borrowed; string converted utf8); - * returns the below-hwm answer. readable.read: (recv, size — -1 for - * absent) → Buffer|null union. pause/resume return recv +1 (`this`); - * isPaused answers the flag. readable.pipe: (recv, dst, end) → dst +1, - * fires 'pipe' on dst; readable.pipeGeneral: the same, with `end` off - * when dst is the converted process.stdout / process.stderr; - * readable.unpipe (recv[, dst]) → recv +1. - * writable.write/writeStr: (recv, chunk[, cb]) → below-hwm bool (cb - * MOVES when present, called after the user write completes). - * writable.writeEnc: (recv, flags, chunk[, encoding][, cb]) — flags 1 - * an encoding argument, 2 a callback (the encoding is then null, as - * Node's write shifts it). - * writable.end: (recv, flags[, chunk][, encoding][, cb]) → recv +1; - * flags 16: an encoding argument follows the dyn chunk. cork/uncork are - * void. stream.destroy/destroyErr: (recv[, err]) → recv +1. - * stream.prop: (recv, name-literal) → the flag/number the name asks - * for; stream.errored → Error|null union; readable.flowing → - * bool|null union. All may leave a listener's exception pending - * (dispatch runs user code synchronously, like emit). */ - "readable.new": { argTypes: [F64, BOOL, BOOL, F64], result: VOID }, - "writable.new": { argTypes: [F64, BOOL, BOOL, F64], result: VOID }, - "duplex.new": { argTypes: [F64, F64, BOOL, BOOL, BOOL, BOOL, BOOL, F64], result: VOID }, - "transform.new": { argTypes: [F64, F64, BOOL, BOOL, BOOL, BOOL, BOOL, F64], result: VOID }, - "passthrough.new": { argTypes: [F64, F64, BOOL, BOOL, BOOL, BOOL, BOOL, F64], result: VOID }, - // The subclass-initialization twins: the borrowed receiver leads, then - // the .new tail (variadic callbacks likewise). - // The dyn-options twins: newDyn takes the record, initDyn the borrowed - // receiver + record + fallback flags (trailing wrapper closures ride - // the variadic tail like the static forms). - /** The underscore-method assignment surface (`r._read = fn` after - * construction — Node's own-property shadow of the prototype method): - * args [stream receiver (borrowed), callback closure (+1 moves)]. The - * runtime slot the matching option callback fills swaps its closure - * and invoke thunk; the next dispatch uses it (Node's timing). The - * setters themselves never throw. */ - "stream.setRead": { argTypes: [null, null], result: VOID }, - "stream.setWrite": { argTypes: [null, null], result: VOID }, - "stream.setFinal": { argTypes: [null, null], result: VOID }, - "stream.setDestroy": { argTypes: [null, null], result: VOID }, - "stream.setTransform": { argTypes: [null, null], result: VOID }, - "stream.setFlush": { argTypes: [null, null], result: VOID }, - "stream.setConstruct": { argTypes: [null, null], result: VOID }, - "stream.replaceConstruct": { argTypes: [null, null], result: VOID }, - "stream.setWritev": { argTypes: [null, null], result: VOID }, - "stream.setWritevDefault": { argTypes: [null, null], result: VOID }, - /** The dyn-options twins (a checked-dynamic options record — the JS - * lane's `super(options)` forwarding and `new Readable(dynVar)`): the - * option walk runs at RUNTIME with Node's reading rules. newDyn: - * (optsDyn) → the fresh stream; initDyn: (recv, optsDyn, flags, - * ...fallback wrapper closures in canonical order — the flags literal - * names which ride, exactly the .init callback ABI). MAY THROW (a - * consumed-but-unlowered option is the compile fence's runtime twin). */ - /** stream.finished(s, cb) — the callback form: the watcher fires once - * at the terminal point with the finish status; the result is the +1 - * cleanup closure. stream.pipeline(count, s1..sn, cb): chains pipes, - * propagates the first error by destroying the rest, calls cb after the - * last 'close'; answers the destination +1. The Dyn twins take the - * callback as a checked-dynamic VALUE (mustCall wrappers). */ - /** stream.addAbortSignal: [stream, signal] — the constructors' signal - * option, chained over the fresh stream (falsy signals are ignored); - * stream.addAbortSignalFn: [signal, stream] — addAbortSignal(signal, - * stream). Both answer the stream (+1) and throw ERR_INVALID_ARG_TYPE - * for a value that is not an AbortSignal; they link the event-target - * unit (addAbortListener). */ - "stream.addAbortSignal": { argTypes: [null, DYN], result: VOID }, - "stream.addAbortSignalFn": { argTypes: [DYN, null], result: VOID }, - "stream.finished": { argTypes: [null, null], result: VOID }, - "stream.finishedDyn": { argTypes: [null, DYN], result: VOID }, - "stream.pipeline": { argTypes: [F64], result: VOID }, - "stream.pipelineDyn": { argTypes: [F64], result: VOID }, - "stream.pipelineStages": { argTypes: [F64, F64, F64, F64, F64], result: VOID }, - "stream.composeStages": { argTypes: [F64, F64, F64, F64, F64], result: VOID }, - "stream.pipelineStagesDyn": { argTypes: [F64, F64, F64, F64, F64], result: VOID }, - /** stream.compose(count, s1..sn): the Duplex over a pipeline of the - * streams (+1). MAY THROW (ERR_INVALID_ARG_VALUE for a stream that is - * no longer readable or writable). */ - "stream.compose": { argTypes: [F64], result: VOID }, - /** node:stream/promises — the promise forms over the same machinery: - * sp.finished: (s) → a pending void promise the terminal watcher - * settles; sp.pipeline: (count, s1..sn) → the callback pipeline's - * chaining/destroyer semantics settling a void promise (fulfilled on a - * clean finish, rejected with the finish status otherwise). */ - "sp.finished": { argTypes: [null], result: { kind: "promise", inner: VOID } }, - /** The options forms: finished(s, { readable, writable, error, cleanup, - * signal }) as [s, readable, writable, error, cleanup, signal?] — - * readable/writable -1 (absent), 0 or 1 — and pipeline(...streams, - * { signal, end }) as [count, s1..sn, end, signal?]. The Signal forms - * link the event-target unit (addAbortListener). The callback form - * stream.finishedOpts: [s, readable, writable, error, signal?, cb] → - * the cleanup closure. */ - "sp.finishedOpts": { - argTypes: [null, F64, F64, BOOL, BOOL], - result: { kind: "promise", inner: VOID }, - }, - "sp.finishedOptsSignal": { - argTypes: [null, F64, F64, BOOL, BOOL, DYN], - result: { kind: "promise", inner: VOID }, - }, - "sp.pipelineOpts": { argTypes: [F64], result: { kind: "promise", inner: VOID } }, - "sp.pipelineOptsSignal": { argTypes: [F64], result: { kind: "promise", inner: VOID } }, - "stream.finishedOpts": { argTypes: [null, F64, F64, BOOL, null], result: VOID }, - "stream.finishedOptsSignal": { argTypes: [null, F64, F64, BOOL, DYN, null], result: VOID }, - "sp.pipeline": { argTypes: [F64], result: { kind: "promise", inner: VOID } }, - /** pipeline with function stages: [n, k, positions, flags, mask, - * s1..sn, stage1..stagek, helpers..., box?, (signal, abort)?] (+ the - * callback for the stream.* forms): the user's stage functions at the - * positions the bitmask names, the frontend's (next, read, return) - * helpers for each pumped generator (pipelinePumpedRuns) and a promise - * destination's box, and the pipeline's AbortSignal with its abort - * closure; flags bits 1-2 give the destination kind and bit 5 the - * signal group (scr_stream_pipeline_stages). A promise destination's - * promise type is sp.pipelineStages' result. */ - "sp.pipelineStages": { - argTypes: [F64, F64, F64, F64, F64], - result: { kind: "promise", inner: VOID }, - }, - /** sp.pipelineStages with the options argument: the same head, then - * end (a bool) and, with flags bit 6, the options' signal (dyn). */ - "sp.pipelineStagesOpts": { - argTypes: [F64, F64, F64, F64, F64], - result: { kind: "promise", inner: VOID }, - }, - /** node:stream/consumers — the promise consumers over the readable - * machinery: (s) → a pending promise settled at the terminal point - * with the accumulated result (sc.text: the utf8 decode, sc.json: the - * parsed dyn — malformed input rejects with the parse's SyntaxError, - * sc.buffer: the concatenated bytes, sc.arrayBuffer: an ArrayBuffer - * dyn over them) or rejected with the stream's - * error / ERR_STREAM_PREMATURE_CLOSE. */ - "sc.text": { argTypes: [null], result: { kind: "promise", inner: STRING } }, - "sc.json": { argTypes: [null], result: { kind: "promise", inner: DYN } }, - "sc.buffer": { argTypes: [null], result: { kind: "promise", inner: BYTES_U8 } }, - "sc.arrayBuffer": { argTypes: [null], result: { kind: "promise", inner: DYN } }, - "readable.newDyn": { argTypes: [DYN], result: VOID }, - "writable.newDyn": { argTypes: [DYN], result: VOID }, - "duplex.newDyn": { argTypes: [DYN], result: VOID }, - "transform.newDyn": { argTypes: [DYN], result: VOID }, - "passthrough.newDyn": { argTypes: [DYN], result: VOID }, - "readable.initDyn": { argTypes: [null, DYN, F64], result: VOID }, - "writable.initDyn": { argTypes: [null, DYN, F64], result: VOID }, - "duplex.initDyn": { argTypes: [null, DYN, F64], result: VOID }, - "transform.initDyn": { argTypes: [null, DYN, F64], result: VOID }, - "passthrough.initDyn": { argTypes: [null, DYN, F64], result: VOID }, - /** Subclass initialization (`super(options?)` in a user `extends - * Readable` constructor): same tail as the `.new` forms, prefixed with - * the BORROWED receiver (the emitted subclass allocation — vtable and - * display name stamped, state NULL until here). Overridden underscore - * methods arrive as synthesized wrapper closures dispatching through - * the vtable. Void result. */ - "readable.init": { argTypes: [null, F64, BOOL, BOOL, F64], result: VOID }, - "writable.init": { argTypes: [null, F64, BOOL, BOOL, F64], result: VOID }, - "duplex.init": { argTypes: [null, F64, F64, BOOL, BOOL, BOOL, BOOL, BOOL, F64], result: VOID }, - "transform.init": { argTypes: [null, F64, F64, BOOL, BOOL, BOOL, BOOL, BOOL, F64], result: VOID }, - "passthrough.init": { - argTypes: [null, F64, F64, BOOL, BOOL, BOOL, BOOL, BOOL, F64], - result: VOID, - }, - "readable.push": { argTypes: [null, BYTES_U8], result: BOOL }, - "readable.pushStr": { argTypes: [null, STRING], result: BOOL }, - "readable.pushNull": { argTypes: [null], result: BOOL }, - "readable.pushU": { argTypes: [null, null], result: BOOL }, - "readable.pushDyn": { argTypes: [null, DYN], result: BOOL }, - "readable.unshift": { argTypes: [null, BYTES_U8], result: VOID }, - "readable.unshiftDyn": { argTypes: [null, DYN], result: VOID }, - "readable.unshiftStr": { argTypes: [null, STRING], result: VOID }, - "stream.onDyn": { argTypes: [null, STRING, DYN, BOOL, BOOL], result: VOID }, - "readable.readDyn": { argTypes: [null, DYN], result: DYN }, - "readable.read": { argTypes: [null, F64], result: VOID }, - "readable.pause": { argTypes: [null], result: VOID }, - "readable.setEncoding": { argTypes: [null, STRING], result: VOID }, - /** push(chunk, enc) with a literal non-utf8 encoding, and the - * defaultEncoding option's push side (how push(string) decodes — - * Buffer.from(chunk, enc)); both carry the CANONICAL literal. */ - "readable.pushStrEnc": { argTypes: [null, STRING, STRING], result: BOOL }, - "readable.pushEncoding": { argTypes: [null, STRING], result: VOID }, - /** The objectMode option's sides (readable, writable), applied right - * after construction; answers the receiver (+1, the chaining shape). */ - "stream.objectMode": { argTypes: [null, BOOL, BOOL], result: VOID }, - "stream.decodeStrings": { argTypes: [null, BOOL], result: VOID }, - /** Runtime decodeStrings and defaultEncoding option values, applied - * after construction (answers the receiver +1); the encoding was - * validated by stream.checkEncodingOption before construction. */ - "stream.optionValues": { argTypes: [null, DYN, DYN], result: VOID }, - "stream.checkEncodingOption": { argTypes: [DYN], result: VOID }, - "stream.duplexFromGen": { argTypes: [null, null, null, null, null, null], result: VOID }, - "stream.duplexFromDyn": { argTypes: [DYN], result: VOID }, - /** Duplex.from over a lone Readable or Writable, or a { readable, - * writable } pair of streams (duplexify.js's _duplexify). */ - "stream.duplexifyReadable": { argTypes: [null], result: VOID }, - "stream.duplexifyWritable": { argTypes: [null], result: VOID }, - "stream.duplexifyPair": { argTypes: [null, null], result: VOID }, - /** Duplex.from(promise): the promise and its box helper. */ - "stream.duplexFromPromise": { argTypes: [null, null], result: VOID }, - /** Duplex.from(asyncFunction): the stage, its source helper and the - * promise's box helper. */ - "stream.duplexFromAsyncFn": { argTypes: [null, null, null], result: VOID }, - /** The same for a body that declares the options argument: then the - * AbortController's abort() and its signal. */ - "stream.duplexFromGenSignal": { - argTypes: [null, null, null, null, null, null, null, DYN], - result: VOID, - }, - "stream.duplexFromAsyncFnSignal": { argTypes: [null, null, null, null, DYN], result: VOID }, - "writable.writeEncoding": { argTypes: [null, STRING], result: VOID }, - "writable.setDefaultEncoding": { argTypes: [null, DYN], result: VOID }, - /** Whether a readable.nextChunkDyn step answered the end (the done - * marker; an object-mode stream can yield undefined). */ - "stream.chunkDone": { argTypes: [DYN], result: BOOL }, - /** for-await over a readable (the desugared loop's per-pass promise): - * +1 promise of the next chunk — buffered content, the EOF sentinel - * (empty Buffer / dyn undefined), or a rejection with the stream's - * error. The Dyn twin boxes chunks by runtime tag (the JS lane). - * readable.fromArr is Readable.from over a typed array, string or - * Buffer: [array, SCR_FROM_* flags, highWaterMark]. readable.fromDyn is - * Readable.from over a checked-dynamic source: [source, options]. - * readable.fromIter is Readable.from over a class iterator, already - * opened: [checked-dynamic { next } object, flags, highWaterMark]; - * readable.fromAsyncIter is the async class iterator's twin. */ - "readable.nextChunk": { argTypes: [null], result: VOID }, - "readable.nextChunkDyn": { argTypes: [null], result: VOID }, - "readable.fromArr": { argTypes: [null, F64, F64], result: VOID }, - "readable.fromDyn": { argTypes: [DYN, DYN], result: VOID }, - "readable.fromIter": { argTypes: [DYN, F64, F64], result: VOID }, - "readable.fromAsyncIter": { argTypes: [DYN, F64, F64], result: VOID }, - /** Readable.from over a compiled generator: [generator, next, return, - * throw, flags, highWaterMark] (sync) or [generator, next, read, - * return, throw, flags, highWaterMark] (async), the helpers being the - * frontend's interned closures. readable.fromDone is the done marker - * the helpers answer. */ - "readable.fromGen": { argTypes: [null, null, null, null, F64, F64], result: VOID }, - "readable.fromAsyncGen": { argTypes: [null, null, null, null, null, F64, F64], result: VOID }, - "readable.fromDone": { argTypes: [], result: DYN }, - /** The Readable operators (scr_stream_ops.c): [stream, ...] with every - * other argument a checked-dynamic value (the callback, the count, the - * options). map/filter/flatMap/drop/take answer the object-mode output - * stream (validated as a stream class; THROW Node's validation errors); - * toArray settles a dyn array, find a dyn value, some/every a boolean, - * forEach nothing; reduce: [stream, fn, init, hasInit, options, kind] - * with the payload kind (0 dyn, 1 number, 2 boolean, 3 string). The - * promise-returning ones reject instead of throwing. */ - "readable.opMap": { argTypes: [null, DYN, DYN, DYN], result: VOID }, - "readable.opFilter": { argTypes: [null, DYN, DYN, DYN], result: VOID }, - "readable.opFlatMap": { argTypes: [null, DYN, DYN, DYN], result: VOID }, - /** flatMap whose callback returns a compiled generator: the helpers - * over its type follow (async: next, read, return, throw; sync: next, - * return, throw). */ - "readable.opFlatMapAsyncGen": { - argTypes: [null, DYN, DYN, DYN, null, null, null, null], - result: VOID, - }, - "readable.opFlatMapSyncGen": { argTypes: [null, DYN, DYN, DYN, null, null, null], result: VOID }, - "readable.opDrop": { argTypes: [null, DYN, DYN], result: VOID }, - "readable.opTake": { argTypes: [null, DYN, DYN], result: VOID }, - "readable.opToArray": { argTypes: [null, DYN], result: { kind: "promise", inner: DYN } }, - "readable.opForEach": { - argTypes: [null, DYN, DYN, DYN], - result: { kind: "promise", inner: VOID }, - }, - "readable.opSome": { argTypes: [null, DYN, DYN, DYN], result: { kind: "promise", inner: BOOL } }, - "readable.opEvery": { argTypes: [null, DYN, DYN, DYN], result: { kind: "promise", inner: BOOL } }, - "readable.opFind": { argTypes: [null, DYN, DYN, DYN], result: { kind: "promise", inner: DYN } }, - /** The map family's last argument is the callback's { signal } (or - * undefined). reduceSignal adds ac.abort() (a closure) and - * { signal: ac.signal } for a reducer that reads them. */ - "readable.opReduce": { argTypes: [null, DYN, DYN, BOOL, DYN, F64], result: VOID }, - "readable.opReduceSignal": { - argTypes: [null, DYN, DYN, BOOL, DYN, F64, null, DYN], - result: VOID, - }, - "readable.opSignalList": { argTypes: [DYN], result: DYN }, - "readable.opSignalOpts": { argTypes: [DYN], result: DYN }, - "readable.opSignalOf": { argTypes: [DYN], result: DYN }, - "readable.opOnceOptions": { argTypes: [], result: DYN }, - "readable.resume": { argTypes: [null], result: VOID }, - "readable.isPaused": { argTypes: [null], result: BOOL }, - "readable.pipe": { argTypes: [null, null, BOOL], result: VOID }, - "readable.pipeGeneral": { argTypes: [null, null, BOOL], result: VOID }, - "readable.unpipe": { argTypes: [null], result: VOID }, - "readable.flowing": { argTypes: [null], result: VOID }, - "writable.write": { argTypes: [null, BYTES_U8], result: BOOL }, - "writable.writeStr": { argTypes: [null, STRING], result: BOOL }, - "writable.writeU": { argTypes: [null, null], result: BOOL }, - "writable.writeDyn": { argTypes: [null, DYN], result: BOOL }, - "writable.writeEnc": { argTypes: [null, F64, DYN], result: BOOL }, - "writable.end": { argTypes: [null, F64], result: VOID }, - "writable.cork": { argTypes: [null], result: VOID }, - "writable.uncork": { argTypes: [null], result: VOID }, - "stream.destroy": { argTypes: [null], result: VOID }, - "stream.destroyErr": { argTypes: [null, null], result: VOID }, - /** AsyncIteratorClose for Readable[Symbol.asyncIterator](): destroy with - * Node's AbortError/ABORT_ERR payload while the iterator's internal error - * consumer prevents an unhandled-error crash. */ - "stream.iteratorClose": { argTypes: [null], result: VOID }, - "stream.prop": { argTypes: [null, STRING], result: VOID }, - "stream.errored": { argTypes: [null], result: VOID }, - // node:assert: pass/negated/deep/hasMsg are frontend-computed bools; the - // message slot always carries a string ("" when hasMsg is false). - /** node:assert (scr_assert.c; assert.match in scr_regex.c — every call - * site carries a regex value, so the regex link switch is already on). - * Failures throw a catchable AssertionError — a runtime %Error whose - * name is "AssertionError" and whose code slot is "ERR_ASSERTION" — so - * `instanceof Error`, `.name`, `.message`, and `.code` all answer like - * Node's. Generated messages are Node's assertion_error.js scalar forms - * byte-exactly (the short `a !== b` form, the stacked `+ actual - * - expected` diff with the string `^` indicator, the inline-vs-block - * not-equal split); composite deep failures carry the header line - * without the rendered inspect diff (documented divergence). - * - * assert.ok: (pass, message) — the frontend computed the truthiness AND - * the full message (the user's, or the compile-time source-text form — - * assert.fail lowers here too with pass=false). assert.eqF64/eqBigInt/ - * eqStr/eqBool: (a, b, negated, deep, msg, hasMsg) — Object.is comparison, - * covering strictEqual/notStrictEqual and the scalar deepStrictEqual - * pair; msg is a typed dummy ("" literal) when hasMsg is false (Node - * distinguishes an omitted message from an empty one per operator). - * assert.deepResult: (equal, negated, msg, hasMsg) — the verdict of a - * frontend-synthesized structural comparison, turned into Node's throw. - * assert.sameValue: Object.is over doubles (the deep-equal helpers' - * number leaf; never throws). assert.match: (s, regex, negated, msg, - * hasMsg) — a fresh exec from index 0. assert.throwsNone: (rejection, - * ename, hasEname, msg, hasMsg) — the "Missing expected - * exception|rejection" throw of assert.throws/rejects whose callback - * returned (fulfilled) normally, with Node's ` (${expected.name})` - * detail when the expected class or shape carries a name. - * assert.throwsMismatch: (expectedName, error) — the wrong-class throw - * of the assert.throws(fn, ErrorClass) form. assert.eqSym: - * (a, b, negated, deep, msg, hasMsg) — strict equality over symbol - * values, pointer identity with v24's "Symbol(desc)" stacked-diff - * messages (scr_symbol.c, the assert.match pattern — symbol-typed - * slots already flip the symbol link switch). assert.eqDyn: - * (a, b, negated, deep, msg, hasMsg) — the whole quartet over - * checked-dynamic operands (both slots dyn; the frontend boxes a - * static side with dynFrom first): SameValue for the strict pair over - * the dyn kinds (boxed-closure identity for functions), the structural - * dyn walk for the deep pair, with assertion_error.js's messages — - * scalar forms byte-exact, composites rendered compact:false/sorted - * through the checked-dynamic tree and diffed with the real myers line printer. - * - * The assert.throws(fn, {name/code/message}) shape check - * (expectedException over the static error surface): shapeBegin - * stashes the caught error, shapeStr/shapeRe add one expected key each - * (key ids 0 code / 1 message / 2 name; shapeRe lives in scr_regex.c — - * its regex argument flips the regex link switch — and tests eagerly - * so scr_assert.c stays libregexp-free), then shapeEnd throws Node's - * deep-equal Comparison diff BYTE-EXACTLY (the bounded key set makes - * the rendering enumerable) or the custom message. - * assert.throwsRegex: (regex, error, msg, hasMsg) — the - * assert.throws(fn, /re/) check over String(error), Node's - * regex-mismatch message. assert.regexErrTest: doesNotReject's - * regex predicate over String(error) (never throws). - * assert.unwantedRejection: (error, msg, hasMsg) — doesNotReject's - * "Got unwanted rejection" throw. - * assert.ifErrorErr/F64/Str/Bool: assert.ifError's per-type throws - * ("ifError got unwanted exception: " + the error's message/name or - * the value's inspection) — the frontend routes null/undefined to a - * no-op and everything else here (Node throws for falsy values too). - * All arguments are borrowed. */ - "assert.ok": { argTypes: [BOOL, STRING, BOOL], result: VOID }, - /** The next AssertionError's actual, expected and operator, recorded on - * a failure branch. */ - "assert.record": { argTypes: [DYN, DYN, STRING], result: VOID }, - "assert.errorNew": { argTypes: [DYN], result: { kind: "object", className: "%AssertionError" } }, - "assert.eqF64": { argTypes: [F64, F64, BOOL, BOOL, STRING, BOOL], result: VOID }, - "assert.eqBigInt": { argTypes: [BIGINT_T, BIGINT_T, BOOL, BOOL, STRING, BOOL], result: VOID }, - "assert.eqStr": { argTypes: [STRING, STRING, BOOL, BOOL, STRING, BOOL], result: VOID }, - "assert.eqBool": { argTypes: [BOOL, BOOL, BOOL, BOOL, STRING, BOOL], result: VOID }, - "assert.deepResult": { argTypes: [BOOL, BOOL, STRING, BOOL], result: VOID }, - "assert.looseDeepResult": { argTypes: [BOOL, BOOL, STRING, BOOL], result: VOID }, - "assert.looseDeepDyn": { argTypes: [DYN, DYN, BOOL, STRING, BOOL], result: VOID }, - "assert.partialDeepDyn": { argTypes: [DYN, DYN, STRING, BOOL], result: VOID }, - "assert.partialDeepResult": { argTypes: [BOOL, BOOL, STRING, BOOL], result: VOID }, - "assert.partialEqDyn": { argTypes: [DYN, DYN], result: BOOL }, - "assert.looseF64Eq": { argTypes: [F64, F64], result: BOOL }, - "assert.sameValue": { argTypes: [F64, F64], result: BOOL }, - // The deep-equality pair memo: both slots are program-dependent - // (any cycle-capable record/array/map type). - /* deepStrictEqual's pair memo over cycle-capable types: enter answers - * true for a pair already being compared (Node's memo — equal cyclic - * structures compare true); leave pops. */ - "assert.deqEnter": { argTypes: [null, null], result: BOOL }, - "assert.deqLeave": { argTypes: [], result: VOID }, - "assert.match": { argTypes: [STRING, REGEX, BOOL, STRING, BOOL], result: VOID }, - "assert.throwsNone": { argTypes: [BOOL, STRING, BOOL, STRING, BOOL], result: VOID }, - "assert.throwsMismatch": { - argTypes: [STRING, { kind: "object", className: "%Error" }, STRING, BOOL, BOOL], - result: VOID, - }, - "assert.throwsRegex": { - argTypes: [REGEX, { kind: "object", className: "%Error" }, STRING, BOOL, BOOL], - result: VOID, - }, - // Symbol strict equality (pointer identity; scr_symbol.c). - "assert.eqSym": { - argTypes: [{ kind: "symbol" }, { kind: "symbol" }, BOOL, BOOL, STRING, BOOL], - result: VOID, - }, - // The equality quartet over checked-dynamic operands (the frontend - // boxes a static side into the checked-dynamic tree first). - "assert.eqDyn": { argTypes: [DYN, DYN, BOOL, BOOL, STRING, BOOL], result: VOID }, - "assert.looseResult": { argTypes: [BOOL, BOOL, STRING, STRING, STRING, BOOL], result: VOID }, - // The throws(fn, {shape}) accumulator: begin/slot calls never throw; - // shapeEnd throws the Comparison diff. The error slot is the - // %Error-narrowed caught value. - "assert.shapeBegin": { argTypes: [{ kind: "object", className: "%Error" }], result: VOID }, - "assert.shapeStr": { argTypes: [F64, STRING], result: VOID }, - "assert.shapeRe": { argTypes: [F64, REGEX], result: VOID }, - "assert.shapeEnd": { argTypes: [STRING, BOOL, BOOL], result: VOID }, - "assert.regexErrTest": { - argTypes: [REGEX, { kind: "object", className: "%Error" }], - result: BOOL, - }, - "assert.unwantedRejection": { - argTypes: [{ kind: "object", className: "%Error" }, STRING, BOOL], - result: VOID, - }, - "assert.unwantedError": { argTypes: [STRING, BOOL, STRING, BOOL], result: VOID }, - "assert.noErrorPredicate": { argTypes: [DYN, DYN], result: BOOL }, - "assert.regexDynTest": { argTypes: [REGEX, DYN], result: BOOL }, - /** Node's expectsError over an error-INSTANCE expected (assert.throws/ - * rejects second argument): walk the expected dyn error's keys (name, - * message, code — the %error marker skipped) and deep-compare each - * against the caught value's; a mismatch throws the deep-equal - * AssertionError (scr_assert.c). MAY THROW by design. */ - "assert.expectsErrDyn": { argTypes: [DYN, DYN, STRING, BOOL], result: VOID }, - // assert.ifError's typed entries (always throw; unit args never - // lower). The error slot is program-dependent — any %Error-hierarchy - // class, root or subclass (the insp.error precedent, null slot). - "assert.ifErrorErr": { argTypes: [null], result: VOID }, - "assert.ifErrorF64": { argTypes: [F64], result: VOID }, - "assert.ifErrorStr": { argTypes: [STRING], result: VOID }, - "assert.ifErrorBool": { argTypes: [BOOL], result: VOID }, - "assert.ifErrorDyn": { argTypes: [DYN], result: VOID }, - // Bytes equality: the two value slots are any bytes kind (the frontend - // gates both sides to ONE static bytes type; the libCall case checks - // the kind and the pair below). - "assert.refEqBytes": { argTypes: [null, null, BOOL, BOOL, STRING, BOOL], result: VOID }, - "assert.refEqFn": { argTypes: [null, null, BOOL, STRING, BOOL], result: VOID }, - "assert.bytesDeepEq": { argTypes: [null, null, BOOL], result: BOOL }, - "assert.looseBytesEq": { argTypes: [null, null], result: BOOL }, - // util.inspect: the error receiver is program-dependent (a builtin or - // user error class — the error.toString precedent, null slot). + "dyn.objCreateNullProto": { argTypes: [], result: DYN }, + "dyn.arrayPrototype": { argTypes: [], result: DYN }, + "dyn.arrayConstructor": { argTypes: [], result: DYN }, + "dyn.objectPrototype": { argTypes: [], result: DYN }, + "dyn.errorPrototype": { argTypes: [], result: DYN }, + "dyn.inheritsErrorPrototype": { argTypes: [DYN], result: BOOL }, + "dyn.errorCaptureStackTrace": { argTypes: [DYN], result: DYN }, + "dyn.errorCaptureStackTraceFunction": { argTypes: [], result: DYN }, + "dyn.functionApply": { argTypes: [], result: DYN }, + "dyn.builtinMethod": { argTypes: [STRING, STRING], result: DYN }, + "dyn.classPrototype": { argTypes: [DYN, DYN], result: DYN }, + "dyn.classBasePrototype": { argTypes: [DYN], result: DYN }, + "dyn.classInherit": { argTypes: [DYN, DYN], result: DYN }, + /** EventEmitter (0), Stream (1), Readable (2), Writable (3), Duplex (4), + * Transform (5) and PassThrough (6) as untyped constructor values, one + * per thread (scr_dyn_builtin_class.c). */ + "dyn.builtinClass": { argTypes: [F64], result: DYN }, + /** The stream classes (2-6 above) with their stream half: constructor + * functions over them get native stream state + * (scr_dyn_builtin_stream.c). */ + "dyn.builtinStreamClass": { argTypes: [F64], result: DYN }, + /** `value instanceof C` for those constructors, over the value's + * prototype chain. */ + "dyn.builtinInstanceOf": { argTypes: [DYN, F64], result: BOOL }, + /** `value instanceof f` for an untyped constructor function: the + * prototype chain against f.prototype. */ + "dyn.functionInstanceOf": { argTypes: [DYN, DYN], result: BOOL }, + "dyn.classSuper": { argTypes: [DYN, DYN, DYN], result: VOID }, + "dyn.assignPrototype": { argTypes: [DYN, DYN, DYN], result: DYN }, + "dyn.definePrototypeProps": { argTypes: [DYN, DYN, DYN], result: DYN }, + "dyn.bagGet": { argTypes: [DYN, STRING, DYN], result: DYN }, + /** A class field's creation in its instance's property bag: adds the + * string key as a default data property when absent, answering false + * (nothing changed) when the bag needs the general defineProperty path. */ + "dyn.bagCreateField": { argTypes: [DYN, STRING], result: BOOL }, + "dyn.bagSet": { argTypes: [DYN, STRING, DYN, DYN], result: VOID }, + "dyn.objCreate": { argTypes: [DYN], result: DYN }, + // Names a compiled class's prototype object (args[0]) after the class + // (args[1]) for util.inspect. + "dyn.nameClassPrototype": { argTypes: [DYN, STRING], result: VOID }, + "dyn.objCreateWithProperties": { argTypes: [DYN, DYN], result: DYN }, + "dyn.getPrototype": { argTypes: [DYN], result: DYN }, + // dyn.getPrototype for walks that compare each prototype with a program + // class's prototype: native builtin values end the walk (null). + "dyn.getPrototypeForIdentity": { argTypes: [DYN], result: DYN }, + "dyn.setPrototype": { argTypes: [DYN, DYN], result: DYN }, + "dyn.getOwnPropertyNames": { argTypes: [DYN], result: DYN }, + "dyn.ownKeys": { argTypes: [DYN], result: DYN }, + "dyn.getOwnPropertySymbols": { argTypes: [DYN], result: DYN }, + "dyn.getOwnPropertyDescriptors": { argTypes: [DYN], result: DYN }, + "dyn.preventExtensions": { argTypes: [DYN], result: DYN }, + "dyn.isExtensible": { argTypes: [DYN], result: BOOL }, + "dyn.seal": { argTypes: [DYN], result: DYN }, + "dyn.isSealed": { argTypes: [DYN], result: BOOL }, + "dyn.objValues": { argTypes: [DYN], result: DYN }, + /** Object.values as a native vector of checked values, for results the + * program consumes directly (for-of, index reads, read-only methods). */ + "dyn.objValueList": { argTypes: [DYN], result: arrayOf(DYN) }, + "dyn.objEntries": { argTypes: [DYN], result: DYN }, + /** structuredClone over the checked-dynamic tree (scr_json.c): the JSON-safe subset plus + * bytes (a fresh copy — a Buffer clones as a plain Uint8Array, like + * Node), deep. Functions and handle kinds throw the spec's catchable + * DataCloneError; CYCLES throw the scriptc fence (the checked-dynamic tree cannot + * represent them — Node clones cycles; documented divergence). The + * options dyn value validates with Node's exact errors (dictionary + * conversion, the transfer-sequence member; any non-empty transfer + * list throws DataCloneError). dyn.cloneMissing is the zero-argument + * call: always throws Node's TypeError [ERR_MISSING_ARGS] with Node's + * own (verbatim, doubly-wrapped) message. */ + "dyn.structuredClone": { argTypes: [DYN, DYN], result: DYN }, + /** The declaration-typed boundary's adoption (decl-projection.ts): + * `source` is the dynamic value a boundary cast just converted, and + * `capsule` a live capsule of the native value built from it. Every + * object or array of `source` that something else still references + * becomes, in place, a live capsule of the native value built from it. + * Throws the path TypeError, changing nothing, when a converted object + * is not plain data (a prototype, accessors, symbols, frozen state). */ + "dyn.declAdopt": { argTypes: [DYN, DYN], result: VOID }, + /** dyn.declAdopt for every other checked cast of a dynamic value to a + * record or array (`value as T`): a shared source object or array + * becomes a live capsule of the value built from it. A node that is not + * plain data, or that the value holds twice, keeps its separate copy. + * Never throws: the conversion already succeeded. */ + "dyn.adoptShared": { argTypes: [DYN, DYN], result: VOID }, + "dyn.cloneMissing": { argTypes: [], result: DYN }, + /** structuredClone with a NON-EMPTY transfer array of static values: + * nothing static is transferable, so the call always throws Node's + * catchable DataCloneError ("Found invalid value in transferList.") — + * lowered directly (the list's values need no dyn representation to + * fail). */ + "dyn.cloneTransferFail": { argTypes: [], result: DYN }, + /** The same refusal given the transfer list's first member, an + * ArrayBuffer: a Buffer pool slab gets Node's untransferable message. */ + "dyn.cloneTransferFailWith": { argTypes: [DYN], result: DYN }, + /** `new RegExp(pattern, flags?)` (scr_regex.c): a heap regex over the + * same libregexp engine the literals use. The pattern compiles EAGERLY + * so an invalid pattern (or an unknown flag letter) throws Node's + * catchable SyntaxError at construction — Node's message shape with + * libregexp's detail text (approximate fidelity; e.name exact). An + * empty pattern stores the spec's "(?:)" source. Both args borrowed + * strings (the lowering completes an absent flags to ""); result +1. + * The result TYPE is the regex kind, so the link switch pulls the + * engine exactly like a literal. */ + "regex.new": { argTypes: [STRING, STRING], result: REGEX }, + "regex.resetLastIndex": { argTypes: [REGEX, F64], result: VOID }, + "regex.newChecked": { argTypes: [DYN, DYN], result: REGEX }, + "regex.replaceCallback": { argTypes: [STRING, REGEX, DYN, BOOL], result: STRING }, + // Reads of capture args[1] of an execSpans/matchGroups result: + // `capture === args[2]`, Number(capture) and parseInt(capture, + // args[2]). Borrowed; never throw. + "regex.groupEquals": { argTypes: [arrayOf(STRING), F64, STRING], result: BOOL }, + "regex.groupNumber": { argTypes: [arrayOf(STRING), F64], result: F64 }, + "regex.groupParseInt": { argTypes: [arrayOf(STRING), F64, F64], result: F64 }, + // `.index` / `.input` (args[1]) of a match result. Borrowed; never throws. + "regex.resultProp": { argTypes: [arrayOf(STRING), STRING], result: DYN }, + // The string keys `for...in` visits on an array (args[0], any element + // type) after its indices. Borrowed; never throws. + "array.namedKeys": { argTypes: [null], result: arrayOf(STRING) }, + /** super(...) in a class declared `extends Array`: the instance, the + * arguments (an array of its element type, or the implicit constructor's + * dyn argument array) and whether the elements are dyn values. */ + "array.subclassConstruct": { argTypes: [null, null, BOOL], result: VOID }, + "array.subclassConstructDyn": { argTypes: [null, DYN, BOOL], result: VOID }, + /** ArraySpeciesCreate over an Array method's plain result: the receiver + * (borrowed), the result (moved), the species constructor's length + * argument (-1: the result's length) and whether to set the length. The + * result has the second argument's type. */ + "array.speciesAdopt": { argTypes: [null, null, F64, BOOL], result: VOID }, + /** ArraySpeciesCreate ahead of a method that calls back into the program + * (map, filter, flatMap): the receiver (borrowed) and the species + * constructor's length argument. The result has the receiver's type: the + * instance, or the receiver itself when it is a plain array. */ + "array.speciesCreate": { argTypes: [null, F64], result: VOID }, + /** The method's plain result (moved) defined on the instance + * array.speciesCreate answered (borrowed), and whether to set the length. + * The result has the second argument's type. */ + "array.speciesFinish": { argTypes: [null, null, BOOL], result: VOID }, + /** Array.of / Array.from on a class declared `extends Array`: the class + * (its name, a literal), the plain result (moved) and the constructor's + * length argument (negative: no argument). */ + "array.classConstructAdopt": { argTypes: [STRING, null, F64], result: VOID }, + /** `value instanceof C` for a dyn value and a class that extends Array + * (its name, a literal). */ + "array.dynIsClass": { argTypes: [DYN, STRING], result: BOOL }, + // The value of the foreign element (SCR_ARR_FOREIGN) at args[1] of an + // array (args[0], any element type); callers test the element state + // first. Borrowed; never throws. + "array.foreign": { argTypes: [null, F64], result: DYN }, + /** The checked conversion's TypeError for the foreign element at args[1] + * of an array (args[0]) read where a value described by args[2] is + * expected. ALWAYS THROWS; polymorphic result (the error.nodeThrow + * pattern). May-throw seed. */ + "array.foreignThrow": { argTypes: [null, F64, STRING], result: VOID }, + // Whether the array (args[0]) still has the key (args[1]) from its + // namedKeys list. Borrowed; never throws. + "array.hasNamedKey": { argTypes: [null, STRING], result: BOOL }, + // Whether an array (args[0], any element type) has properties besides its + // elements: noncanonical numeric keys, match metadata or named + // properties. Borrowed; never throws. + "array.hasProperties": { argTypes: [null], result: BOOL }, + // The key of an element access whose index is undefined (the property + // named "undefined"; frontend/lowering/undefined-index.ts). + "array.undefinedKey": { argTypes: [], result: F64 }, + // node:events EventEmitter: receivers are emitter-hierarchy objects and + // the chaining forms (on/off/removeAll/setMax) return the receiver's + // own class — program-dependent object types, checked in the libCall + // case. emitter.emit is the one VARIADIC libCall: args beyond + // (recv, name) are the event's frontend-unified tuple, any borrowable + // kind — the count check admits a longer list for it alone. + /** node:events EventEmitter (scr_events_emitter.c, link-gated by + * moduleUsesEmitter). The receiver of every instance form is a borrowed + * emitter-hierarchy object (`%EventEmitter` or a user subclass — the + * backend reinterprets to ScrEmitter*, the identical prefix); event + * names are borrowed strings (compile-time literals — the frontend + * fences non-literals and unifies each event's argument tuple program- + * wide). The chaining forms (on/off/removeAll/setMax) return the + * receiver +1 typed as its static class, Node's `return this`. + * + * emitter.new: `new EventEmitter()` → a +1 bare emitter. emitter.ctor: + * super() into the prefix of an emitted subclass (borrowed receiver, + * void — allocation already initialized the prefix). emitter.on: + * (recv, name, cb /moves/, once, prepend) — the backend synthesizes the + * per-signature va_list invoke adapter from the cb's func type. + * emitter.emit: (recv, name, ...tuple) — VARIADIC, the one libCall + * whose arg count exceeds its signature; args are borrowed, result is + * the had-listeners bool. emitter.emitError: emit('error', err) — + * throws err when unhandled. count/countFn/names/listeners/getMax/ + * setMax/setDefaultMax/getDefaultMax are the introspection surface. */ + "emitter.new": { argTypes: [], result: VOID }, + /** `x.constructor.name` of an emitter or stream (a borrowed object of + * EventEmitter's hierarchy): the +1 class name the instance records. */ + "emitter.className": { argTypes: [null], result: STRING }, + "emitter.ctor": { argTypes: [null], result: VOID }, + "emitter.on": { argTypes: [null, STRING, null, BOOL, BOOL], result: VOID }, + "emitter.off": { argTypes: [null, STRING, null], result: VOID }, + // The dyn-adapted registration family (JS-lane checked-dynamic + // listeners): checkListener has NO receiver (it validates the listener + // argument alone); onDyn carries (recv, name, cb dyn, adapter func, + // once, prepend), offDyn (recv, name, cb dyn). + "emitter.checkListener": { argTypes: [DYN], result: VOID }, + "emitter.onDyn": { argTypes: [null, STRING, DYN, null, BOOL, BOOL], result: VOID }, + "emitter.onFlex": { argTypes: [null, STRING, DYN, BOOL, BOOL], result: VOID }, + /** on/once/prepend* and emit through an untyped value (the class + * dispatch of compiled emitters): onFlex/emitFlex plus runtime typed + * adapters under 'error' and the meta events, and Node's unhandled-'error' + * contract. emitAny is VARIADIC like emitFlex. */ + "emitter.onAny": { argTypes: [null, STRING, DYN, BOOL, BOOL], result: VOID }, + "emitter.offDyn": { argTypes: [null, STRING, DYN], result: VOID }, + "emitter.removeAll": { argTypes: [null, STRING, BOOL], result: VOID }, + "emitter.emit": { argTypes: [null, STRING], result: BOOL }, + "emitter.emitFlex": { argTypes: [null, STRING], result: BOOL }, + "emitter.emitAny": { argTypes: [null, STRING], result: BOOL }, + /** An untyped emit of an event with a fixed tuple: (recv, name, k, then + * k checked-dynamic arguments as given, then the event's converted + * tuple) — VARIADIC like emitter.emit. hasTyped answers whether a + * listener of the name needs the converted tuple. */ + "emitter.emitMixed": { argTypes: [null, STRING, F64], result: BOOL }, + "emitter.hasTyped": { argTypes: [null, STRING], result: BOOL }, + "emitter.emitError": { argTypes: [null, STRING, null], result: BOOL }, + "emitter.count": { argTypes: [null, STRING], result: F64 }, + "emitter.countFn": { argTypes: [null, STRING, null], result: F64 }, + "emitter.countDyn": { argTypes: [null, STRING, DYN], result: F64 }, + "emitter.names": { argTypes: [null], result: VOID }, + "emitter.listeners": { argTypes: [null, STRING], result: VOID }, + /** listeners()/rawListeners() through an untyped value: a checked- + * dynamic array (throws when a listener has typed parameters). */ + "emitter.listenersDyn": { argTypes: [null, STRING], result: DYN }, + /** Stream-'data' registration twins of emitter.on/onDyn (same args): + * chosen at registration sites whose receiver is stream-rooted, so the + * backend emits DATA thunks — the runtime's 'data' emission carries + * BOTH payload slots (bytes, string; exactly one non-NULL — encoded + * streams deliver strings), and the thunk unwraps the listener's + * declared side (typed) or boxes by tag (dyn). emitter.emitData is the + * user-emit form: (recv, name, chunk) with a bytes OR string chunk. */ + "emitter.onData": { argTypes: [null, STRING, null, BOOL, BOOL], result: VOID }, + "emitter.onDataDyn": { argTypes: [null, STRING, DYN, null, BOOL, BOOL], result: VOID }, + "emitter.emitData": { argTypes: [null, STRING, null], result: BOOL }, + "emitter.setMax": { argTypes: [null, F64], result: VOID }, + "emitter.getMax": { argTypes: [null], result: F64 }, + "emitter.setDefaultMax": { argTypes: [F64], result: VOID }, + "emitter.getDefaultMax": { argTypes: [], result: F64 }, + // The ES module namespace's copy of defaultMaxListeners, and + // module.syncBuiltinESMExports() refreshing it. + "emitter.esmDefaultMax": { argTypes: [], result: F64 }, + "emitter.esmSync": { argTypes: [], result: VOID }, + /** The events.errorMonitor event key: an immortal string the runtime + * registry matches by identity, so no string event name reaches it. */ + "emitter.errorMonitor": { argTypes: [], result: STRING }, + // node:stream: receivers are stream-hierarchy objects and several + // results are program-dependent (the receiver's class, unions) — the + // libCall case owns those checks. The constructors and a few methods + // are VARIADIC (trailing option callbacks / the optional chunk+cb tail): + // argTypes here is the fixed prefix, VARIADIC_LIB_FNS admits the rest. + /** node:stream (scr_stream.c, link-gated by moduleUsesStream — which + * implies the emitter unit: stream events dispatch through the embedded + * ScrEmitter registry). Receivers are borrowed stream-class objects + * (`%Readable`/`%Writable`/`%Duplex`/`%Transform`/`%PassThrough` — one + * runtime layout, reinterpreted by side). + * + * Constructors (`readable.new` et al): args are [hwmR, hwmW, flags] + * followed by the PRESENT user callbacks in canonical order (read, + * write, final, destroy, transform, flush — the flags f64 is a bitmask + * naming which follow; absent ones emit NULL). Every callback closure + * MOVES and carries a leading `this` param (the stream), invoked + * through compiler-emitted adapters. Results are +1. + * + * readable.push / readable.pushStr / readable.pushNull: Node's push — + * buffers or delivers (bytes chunk borrowed; string converted utf8); + * returns the below-hwm answer. readable.read: (recv, size — -1 for + * absent) → Buffer|null union. pause/resume return recv +1 (`this`); + * isPaused answers the flag. readable.pipe: (recv, dst, end) → dst +1, + * fires 'pipe' on dst; readable.pipeGeneral: the same, with `end` off + * when dst is the converted process.stdout / process.stderr; + * readable.unpipe (recv[, dst]) → recv +1. + * writable.write/writeStr: (recv, chunk[, cb]) → below-hwm bool (cb + * MOVES when present, called after the user write completes). + * writable.writeEnc: (recv, flags, chunk[, encoding][, cb]) — flags 1 + * an encoding argument, 2 a callback (the encoding is then null, as + * Node's write shifts it). + * writable.end: (recv, flags[, chunk][, encoding][, cb]) → recv +1; + * flags 16: an encoding argument follows the dyn chunk. cork/uncork are + * void. stream.destroy/destroyErr: (recv[, err]) → recv +1. + * stream.prop: (recv, name-literal) → the flag/number the name asks + * for; stream.errored → Error|null union; readable.flowing → + * bool|null union. All may leave a listener's exception pending + * (dispatch runs user code synchronously, like emit). */ + "readable.new": { argTypes: [F64, BOOL, BOOL, F64], result: VOID }, + "writable.new": { argTypes: [F64, BOOL, BOOL, F64], result: VOID }, + "duplex.new": { argTypes: [F64, F64, BOOL, BOOL, BOOL, BOOL, BOOL, F64], result: VOID }, + "transform.new": { argTypes: [F64, F64, BOOL, BOOL, BOOL, BOOL, BOOL, F64], result: VOID }, + "passthrough.new": { argTypes: [F64, F64, BOOL, BOOL, BOOL, BOOL, BOOL, F64], result: VOID }, + // The subclass-initialization twins: the borrowed receiver leads, then + // the .new tail (variadic callbacks likewise). + // The dyn-options twins: newDyn takes the record, initDyn the borrowed + // receiver + record + fallback flags (trailing wrapper closures ride + // the variadic tail like the static forms). + /** The underscore-method assignment surface (`r._read = fn` after + * construction — Node's own-property shadow of the prototype method): + * args [stream receiver (borrowed), callback closure (+1 moves)]. The + * runtime slot the matching option callback fills swaps its closure + * and invoke thunk; the next dispatch uses it (Node's timing). The + * setters themselves never throw. */ + "stream.setRead": { argTypes: [null, null], result: VOID }, + "stream.setWrite": { argTypes: [null, null], result: VOID }, + "stream.setFinal": { argTypes: [null, null], result: VOID }, + "stream.setDestroy": { argTypes: [null, null], result: VOID }, + "stream.setTransform": { argTypes: [null, null], result: VOID }, + "stream.setFlush": { argTypes: [null, null], result: VOID }, + "stream.setConstruct": { argTypes: [null, null], result: VOID }, + "stream.replaceConstruct": { argTypes: [null, null], result: VOID }, + "stream.setWritev": { argTypes: [null, null], result: VOID }, + "stream.setWritevDefault": { argTypes: [null, null], result: VOID }, + /** The dyn-options twins (a checked-dynamic options record — the JS + * lane's `super(options)` forwarding and `new Readable(dynVar)`): the + * option walk runs at RUNTIME with Node's reading rules. newDyn: + * (optsDyn) → the fresh stream; initDyn: (recv, optsDyn, flags, + * ...fallback wrapper closures in canonical order — the flags literal + * names which ride, exactly the .init callback ABI). MAY THROW (a + * consumed-but-unlowered option is the compile fence's runtime twin). */ + /** stream.finished(s, cb) — the callback form: the watcher fires once + * at the terminal point with the finish status; the result is the +1 + * cleanup closure. stream.pipeline(count, s1..sn, cb): chains pipes, + * propagates the first error by destroying the rest, calls cb after the + * last 'close'; answers the destination +1. The Dyn twins take the + * callback as a checked-dynamic VALUE (mustCall wrappers). */ + /** stream.addAbortSignal: [stream, signal] — the constructors' signal + * option, chained over the fresh stream (falsy signals are ignored); + * stream.addAbortSignalFn: [signal, stream] — addAbortSignal(signal, + * stream). Both answer the stream (+1) and throw ERR_INVALID_ARG_TYPE + * for a value that is not an AbortSignal; they link the event-target + * unit (addAbortListener). */ + "stream.addAbortSignal": { argTypes: [null, DYN], result: VOID }, + "stream.addAbortSignalFn": { argTypes: [DYN, null], result: VOID }, + "stream.finished": { argTypes: [null, null], result: VOID }, + "stream.finishedDyn": { argTypes: [null, DYN], result: VOID }, + "stream.pipeline": { argTypes: [F64], result: VOID }, + "stream.pipelineDyn": { argTypes: [F64], result: VOID }, + "stream.pipelineStages": { argTypes: [F64, F64, F64, F64, F64], result: VOID }, + "stream.composeStages": { argTypes: [F64, F64, F64, F64, F64], result: VOID }, + "stream.pipelineStagesDyn": { argTypes: [F64, F64, F64, F64, F64], result: VOID }, + /** stream.compose(count, s1..sn): the Duplex over a pipeline of the + * streams (+1). MAY THROW (ERR_INVALID_ARG_VALUE for a stream that is + * no longer readable or writable). */ + "stream.compose": { argTypes: [F64], result: VOID }, + /** node:stream/promises — the promise forms over the same machinery: + * sp.finished: (s) → a pending void promise the terminal watcher + * settles; sp.pipeline: (count, s1..sn) → the callback pipeline's + * chaining/destroyer semantics settling a void promise (fulfilled on a + * clean finish, rejected with the finish status otherwise). */ + "sp.finished": { argTypes: [null], result: { kind: "promise", inner: VOID } }, + /** The options forms: finished(s, { readable, writable, error, cleanup, + * signal }) as [s, readable, writable, error, cleanup, signal?] — + * readable/writable -1 (absent), 0 or 1 — and pipeline(...streams, + * { signal, end }) as [count, s1..sn, end, signal?]. The Signal forms + * link the event-target unit (addAbortListener). The callback form + * stream.finishedOpts: [s, readable, writable, error, signal?, cb] → + * the cleanup closure. */ + "sp.finishedOpts": { + argTypes: [null, F64, F64, BOOL, BOOL], + result: { kind: "promise", inner: VOID }, + }, + "sp.finishedOptsSignal": { + argTypes: [null, F64, F64, BOOL, BOOL, DYN], + result: { kind: "promise", inner: VOID }, + }, + "sp.pipelineOpts": { argTypes: [F64], result: { kind: "promise", inner: VOID } }, + "sp.pipelineOptsSignal": { argTypes: [F64], result: { kind: "promise", inner: VOID } }, + "stream.finishedOpts": { argTypes: [null, F64, F64, BOOL, null], result: VOID }, + "stream.finishedOptsSignal": { argTypes: [null, F64, F64, BOOL, DYN, null], result: VOID }, + "sp.pipeline": { argTypes: [F64], result: { kind: "promise", inner: VOID } }, + /** pipeline with function stages: [n, k, positions, flags, mask, + * s1..sn, stage1..stagek, helpers..., box?, (signal, abort)?] (+ the + * callback for the stream.* forms): the user's stage functions at the + * positions the bitmask names, the frontend's (next, read, return) + * helpers for each pumped generator (pipelinePumpedRuns) and a promise + * destination's box, and the pipeline's AbortSignal with its abort + * closure; flags bits 1-2 give the destination kind and bit 5 the + * signal group (scr_stream_pipeline_stages). A promise destination's + * promise type is sp.pipelineStages' result. */ + "sp.pipelineStages": { + argTypes: [F64, F64, F64, F64, F64], + result: { kind: "promise", inner: VOID }, + }, + /** sp.pipelineStages with the options argument: the same head, then + * end (a bool) and, with flags bit 6, the options' signal (dyn). */ + "sp.pipelineStagesOpts": { + argTypes: [F64, F64, F64, F64, F64], + result: { kind: "promise", inner: VOID }, + }, + /** node:stream/consumers — the promise consumers over the readable + * machinery: (s) → a pending promise settled at the terminal point + * with the accumulated result (sc.text: the utf8 decode, sc.json: the + * parsed dyn — malformed input rejects with the parse's SyntaxError, + * sc.buffer: the concatenated bytes, sc.arrayBuffer: an ArrayBuffer + * dyn over them) or rejected with the stream's + * error / ERR_STREAM_PREMATURE_CLOSE. */ + "sc.text": { argTypes: [null], result: { kind: "promise", inner: STRING } }, + "sc.json": { argTypes: [null], result: { kind: "promise", inner: DYN } }, + "sc.buffer": { argTypes: [null], result: { kind: "promise", inner: BYTES_U8 } }, + "sc.arrayBuffer": { argTypes: [null], result: { kind: "promise", inner: DYN } }, + "readable.newDyn": { argTypes: [DYN], result: VOID }, + "writable.newDyn": { argTypes: [DYN], result: VOID }, + "duplex.newDyn": { argTypes: [DYN], result: VOID }, + "transform.newDyn": { argTypes: [DYN], result: VOID }, + "passthrough.newDyn": { argTypes: [DYN], result: VOID }, + "readable.initDyn": { argTypes: [null, DYN, F64], result: VOID }, + "writable.initDyn": { argTypes: [null, DYN, F64], result: VOID }, + "duplex.initDyn": { argTypes: [null, DYN, F64], result: VOID }, + "transform.initDyn": { argTypes: [null, DYN, F64], result: VOID }, + "passthrough.initDyn": { argTypes: [null, DYN, F64], result: VOID }, + /** Subclass initialization (`super(options?)` in a user `extends + * Readable` constructor): same tail as the `.new` forms, prefixed with + * the BORROWED receiver (the emitted subclass allocation — vtable and + * display name stamped, state NULL until here). Overridden underscore + * methods arrive as synthesized wrapper closures dispatching through + * the vtable. Void result. */ + "readable.init": { argTypes: [null, F64, BOOL, BOOL, F64], result: VOID }, + "writable.init": { argTypes: [null, F64, BOOL, BOOL, F64], result: VOID }, + "duplex.init": { argTypes: [null, F64, F64, BOOL, BOOL, BOOL, BOOL, BOOL, F64], result: VOID }, + "transform.init": { + argTypes: [null, F64, F64, BOOL, BOOL, BOOL, BOOL, BOOL, F64], + result: VOID, + }, + "passthrough.init": { + argTypes: [null, F64, F64, BOOL, BOOL, BOOL, BOOL, BOOL, F64], + result: VOID, + }, + "readable.push": { argTypes: [null, BYTES_U8], result: BOOL }, + "readable.pushStr": { argTypes: [null, STRING], result: BOOL }, + "readable.pushNull": { argTypes: [null], result: BOOL }, + "readable.pushU": { argTypes: [null, null], result: BOOL }, + "readable.pushDyn": { argTypes: [null, DYN], result: BOOL }, + "readable.unshift": { argTypes: [null, BYTES_U8], result: VOID }, + "readable.unshiftDyn": { argTypes: [null, DYN], result: VOID }, + "readable.unshiftStr": { argTypes: [null, STRING], result: VOID }, + "stream.onDyn": { argTypes: [null, STRING, DYN, BOOL, BOOL], result: VOID }, + "readable.readDyn": { argTypes: [null, DYN], result: DYN }, + "readable.read": { argTypes: [null, F64], result: VOID }, + "readable.pause": { argTypes: [null], result: VOID }, + "readable.setEncoding": { argTypes: [null, STRING], result: VOID }, + /** push(chunk, enc) with a literal non-utf8 encoding, and the + * defaultEncoding option's push side (how push(string) decodes — + * Buffer.from(chunk, enc)); both carry the CANONICAL literal. */ + "readable.pushStrEnc": { argTypes: [null, STRING, STRING], result: BOOL }, + "readable.pushEncoding": { argTypes: [null, STRING], result: VOID }, + /** The objectMode option's sides (readable, writable), applied right + * after construction; answers the receiver (+1, the chaining shape). */ + "stream.objectMode": { argTypes: [null, BOOL, BOOL], result: VOID }, + "stream.decodeStrings": { argTypes: [null, BOOL], result: VOID }, + /** Runtime decodeStrings and defaultEncoding option values, applied + * after construction (answers the receiver +1); the encoding was + * validated by stream.checkEncodingOption before construction. */ + "stream.optionValues": { argTypes: [null, DYN, DYN], result: VOID }, + "stream.checkEncodingOption": { argTypes: [DYN], result: VOID }, + "stream.duplexFromGen": { argTypes: [null, null, null, null, null, null], result: VOID }, + "stream.duplexFromDyn": { argTypes: [DYN], result: VOID }, + /** Duplex.from over a lone Readable or Writable, or a { readable, + * writable } pair of streams (duplexify.js's _duplexify). */ + "stream.duplexifyReadable": { argTypes: [null], result: VOID }, + "stream.duplexifyWritable": { argTypes: [null], result: VOID }, + "stream.duplexifyPair": { argTypes: [null, null], result: VOID }, + /** Duplex.from(promise): the promise and its box helper. */ + "stream.duplexFromPromise": { argTypes: [null, null], result: VOID }, + /** Duplex.from(asyncFunction): the stage, its source helper and the + * promise's box helper. */ + "stream.duplexFromAsyncFn": { argTypes: [null, null, null], result: VOID }, + /** The same for a body that declares the options argument: then the + * AbortController's abort() and its signal. */ + "stream.duplexFromGenSignal": { + argTypes: [null, null, null, null, null, null, null, DYN], + result: VOID, + }, + "stream.duplexFromAsyncFnSignal": { argTypes: [null, null, null, null, DYN], result: VOID }, + "writable.writeEncoding": { argTypes: [null, STRING], result: VOID }, + "writable.setDefaultEncoding": { argTypes: [null, DYN], result: VOID }, + /** Whether a readable.nextChunkDyn step answered the end (the done + * marker; an object-mode stream can yield undefined). */ + "stream.chunkDone": { argTypes: [DYN], result: BOOL }, + /** for-await over a readable (the desugared loop's per-pass promise): + * +1 promise of the next chunk — buffered content, the EOF sentinel + * (empty Buffer / dyn undefined), or a rejection with the stream's + * error. The Dyn twin boxes chunks by runtime tag (the JS lane). + * readable.fromArr is Readable.from over a typed array, string or + * Buffer: [array, SCR_FROM_* flags, highWaterMark]. readable.fromDyn is + * Readable.from over a checked-dynamic source: [source, options]. + * readable.fromIter is Readable.from over a class iterator, already + * opened: [checked-dynamic { next } object, flags, highWaterMark]; + * readable.fromAsyncIter is the async class iterator's twin. */ + "readable.nextChunk": { argTypes: [null], result: VOID }, + "readable.nextChunkDyn": { argTypes: [null], result: VOID }, + "readable.fromArr": { argTypes: [null, F64, F64], result: VOID }, + "readable.fromDyn": { argTypes: [DYN, DYN], result: VOID }, + "readable.fromIter": { argTypes: [DYN, F64, F64], result: VOID }, + "readable.fromAsyncIter": { argTypes: [DYN, F64, F64], result: VOID }, + /** Readable.from over a compiled generator: [generator, next, return, + * throw, flags, highWaterMark] (sync) or [generator, next, read, + * return, throw, flags, highWaterMark] (async), the helpers being the + * frontend's interned closures. readable.fromDone is the done marker + * the helpers answer. */ + "readable.fromGen": { argTypes: [null, null, null, null, F64, F64], result: VOID }, + "readable.fromAsyncGen": { argTypes: [null, null, null, null, null, F64, F64], result: VOID }, + "readable.fromDone": { argTypes: [], result: DYN }, + /** The Readable operators (scr_stream_ops.c): [stream, ...] with every + * other argument a checked-dynamic value (the callback, the count, the + * options). map/filter/flatMap/drop/take answer the object-mode output + * stream (validated as a stream class; THROW Node's validation errors); + * toArray settles a dyn array, find a dyn value, some/every a boolean, + * forEach nothing; reduce: [stream, fn, init, hasInit, options, kind] + * with the payload kind (0 dyn, 1 number, 2 boolean, 3 string). The + * promise-returning ones reject instead of throwing. */ + "readable.opMap": { argTypes: [null, DYN, DYN, DYN], result: VOID }, + "readable.opFilter": { argTypes: [null, DYN, DYN, DYN], result: VOID }, + "readable.opFlatMap": { argTypes: [null, DYN, DYN, DYN], result: VOID }, + /** flatMap whose callback returns a compiled generator: the helpers + * over its type follow (async: next, read, return, throw; sync: next, + * return, throw). */ + "readable.opFlatMapAsyncGen": { + argTypes: [null, DYN, DYN, DYN, null, null, null, null], + result: VOID, + }, + "readable.opFlatMapSyncGen": { + argTypes: [null, DYN, DYN, DYN, null, null, null], + result: VOID, + }, + "readable.opDrop": { argTypes: [null, DYN, DYN], result: VOID }, + "readable.opTake": { argTypes: [null, DYN, DYN], result: VOID }, + "readable.opToArray": { argTypes: [null, DYN], result: { kind: "promise", inner: DYN } }, + "readable.opForEach": { + argTypes: [null, DYN, DYN, DYN], + result: { kind: "promise", inner: VOID }, + }, + "readable.opSome": { + argTypes: [null, DYN, DYN, DYN], + result: { kind: "promise", inner: BOOL }, + }, + "readable.opEvery": { + argTypes: [null, DYN, DYN, DYN], + result: { kind: "promise", inner: BOOL }, + }, + "readable.opFind": { argTypes: [null, DYN, DYN, DYN], result: { kind: "promise", inner: DYN } }, + /** The map family's last argument is the callback's { signal } (or + * undefined). reduceSignal adds ac.abort() (a closure) and + * { signal: ac.signal } for a reducer that reads them. */ + "readable.opReduce": { argTypes: [null, DYN, DYN, BOOL, DYN, F64], result: VOID }, + "readable.opReduceSignal": { + argTypes: [null, DYN, DYN, BOOL, DYN, F64, null, DYN], + result: VOID, + }, + "readable.opSignalList": { argTypes: [DYN], result: DYN }, + "readable.opSignalOpts": { argTypes: [DYN], result: DYN }, + "readable.opSignalOf": { argTypes: [DYN], result: DYN }, + "readable.opOnceOptions": { argTypes: [], result: DYN }, + "readable.resume": { argTypes: [null], result: VOID }, + "readable.isPaused": { argTypes: [null], result: BOOL }, + "readable.pipe": { argTypes: [null, null, BOOL], result: VOID }, + "readable.pipeGeneral": { argTypes: [null, null, BOOL], result: VOID }, + "readable.unpipe": { argTypes: [null], result: VOID }, + "readable.flowing": { argTypes: [null], result: VOID }, + "writable.write": { argTypes: [null, BYTES_U8], result: BOOL }, + "writable.writeStr": { argTypes: [null, STRING], result: BOOL }, + "writable.writeU": { argTypes: [null, null], result: BOOL }, + "writable.writeDyn": { argTypes: [null, DYN], result: BOOL }, + "writable.writeEnc": { argTypes: [null, F64, DYN], result: BOOL }, + "writable.end": { argTypes: [null, F64], result: VOID }, + "writable.cork": { argTypes: [null], result: VOID }, + "writable.uncork": { argTypes: [null], result: VOID }, + "stream.destroy": { argTypes: [null], result: VOID }, + "stream.destroyErr": { argTypes: [null, null], result: VOID }, + /** AsyncIteratorClose for Readable[Symbol.asyncIterator](): destroy with + * Node's AbortError/ABORT_ERR payload while the iterator's internal error + * consumer prevents an unhandled-error crash. */ + "stream.iteratorClose": { argTypes: [null], result: VOID }, + "stream.prop": { argTypes: [null, STRING], result: VOID }, + "stream.errored": { argTypes: [null], result: VOID }, + // node:assert: pass/negated/deep/hasMsg are frontend-computed bools; the + // message slot always carries a string ("" when hasMsg is false). + /** node:assert (scr_assert.c; assert.match in scr_regex.c — every call + * site carries a regex value, so the regex link switch is already on). + * Failures throw a catchable AssertionError — a runtime %Error whose + * name is "AssertionError" and whose code slot is "ERR_ASSERTION" — so + * `instanceof Error`, `.name`, `.message`, and `.code` all answer like + * Node's. Generated messages are Node's assertion_error.js scalar forms + * byte-exactly (the short `a !== b` form, the stacked `+ actual + * - expected` diff with the string `^` indicator, the inline-vs-block + * not-equal split); composite deep failures carry the header line + * without the rendered inspect diff (documented divergence). + * + * assert.ok: (pass, message) — the frontend computed the truthiness AND + * the full message (the user's, or the compile-time source-text form — + * assert.fail lowers here too with pass=false). assert.eqF64/eqBigInt/ + * eqStr/eqBool: (a, b, negated, deep, msg, hasMsg) — Object.is comparison, + * covering strictEqual/notStrictEqual and the scalar deepStrictEqual + * pair; msg is a typed dummy ("" literal) when hasMsg is false (Node + * distinguishes an omitted message from an empty one per operator). + * assert.deepResult: (equal, negated, msg, hasMsg) — the verdict of a + * frontend-synthesized structural comparison, turned into Node's throw. + * assert.sameValue: Object.is over doubles (the deep-equal helpers' + * number leaf; never throws). assert.match: (s, regex, negated, msg, + * hasMsg) — a fresh exec from index 0. assert.throwsNone: (rejection, + * ename, hasEname, msg, hasMsg) — the "Missing expected + * exception|rejection" throw of assert.throws/rejects whose callback + * returned (fulfilled) normally, with Node's ` (${expected.name})` + * detail when the expected class or shape carries a name. + * assert.throwsMismatch: (expectedName, error) — the wrong-class throw + * of the assert.throws(fn, ErrorClass) form. assert.eqSym: + * (a, b, negated, deep, msg, hasMsg) — strict equality over symbol + * values, pointer identity with v24's "Symbol(desc)" stacked-diff + * messages (scr_symbol.c, the assert.match pattern — symbol-typed + * slots already flip the symbol link switch). assert.eqDyn: + * (a, b, negated, deep, msg, hasMsg) — the whole quartet over + * checked-dynamic operands (both slots dyn; the frontend boxes a + * static side with dynFrom first): SameValue for the strict pair over + * the dyn kinds (boxed-closure identity for functions), the structural + * dyn walk for the deep pair, with assertion_error.js's messages — + * scalar forms byte-exact, composites rendered compact:false/sorted + * through the checked-dynamic tree and diffed with the real myers line printer. + * + * The assert.throws(fn, {name/code/message}) shape check + * (expectedException over the static error surface): shapeBegin + * stashes the caught error, shapeStr/shapeRe add one expected key each + * (key ids 0 code / 1 message / 2 name; shapeRe lives in scr_regex.c — + * its regex argument flips the regex link switch — and tests eagerly + * so scr_assert.c stays libregexp-free), then shapeEnd throws Node's + * deep-equal Comparison diff BYTE-EXACTLY (the bounded key set makes + * the rendering enumerable) or the custom message. + * assert.throwsRegex: (regex, error, msg, hasMsg) — the + * assert.throws(fn, /re/) check over String(error), Node's + * regex-mismatch message. assert.regexErrTest: doesNotReject's + * regex predicate over String(error) (never throws). + * assert.unwantedRejection: (error, msg, hasMsg) — doesNotReject's + * "Got unwanted rejection" throw. + * assert.ifErrorErr/F64/Str/Bool: assert.ifError's per-type throws + * ("ifError got unwanted exception: " + the error's message/name or + * the value's inspection) — the frontend routes null/undefined to a + * no-op and everything else here (Node throws for falsy values too). + * All arguments are borrowed. */ + "assert.ok": { argTypes: [BOOL, STRING, BOOL], result: VOID }, + /** The next AssertionError's actual, expected and operator, recorded on + * a failure branch. */ + "assert.record": { argTypes: [DYN, DYN, STRING], result: VOID }, + "assert.errorNew": { + argTypes: [DYN], + result: { kind: "object", className: "%AssertionError" }, + }, + "assert.eqF64": { argTypes: [F64, F64, BOOL, BOOL, STRING, BOOL], result: VOID }, + "assert.eqBigInt": { argTypes: [BIGINT_T, BIGINT_T, BOOL, BOOL, STRING, BOOL], result: VOID }, + "assert.eqStr": { argTypes: [STRING, STRING, BOOL, BOOL, STRING, BOOL], result: VOID }, + "assert.eqBool": { argTypes: [BOOL, BOOL, BOOL, BOOL, STRING, BOOL], result: VOID }, + "assert.deepResult": { argTypes: [BOOL, BOOL, STRING, BOOL], result: VOID }, + "assert.looseDeepResult": { argTypes: [BOOL, BOOL, STRING, BOOL], result: VOID }, + "assert.looseDeepDyn": { argTypes: [DYN, DYN, BOOL, STRING, BOOL], result: VOID }, + "assert.partialDeepDyn": { argTypes: [DYN, DYN, STRING, BOOL], result: VOID }, + "assert.partialDeepResult": { argTypes: [BOOL, BOOL, STRING, BOOL], result: VOID }, + "assert.partialEqDyn": { argTypes: [DYN, DYN], result: BOOL }, + "assert.looseF64Eq": { argTypes: [F64, F64], result: BOOL }, + "assert.sameValue": { argTypes: [F64, F64], result: BOOL }, + // The deep-equality pair memo: both slots are program-dependent + // (any cycle-capable record/array/map type). + /* deepStrictEqual's pair memo over cycle-capable types: enter answers + * true for a pair already being compared (Node's memo — equal cyclic + * structures compare true); leave pops. */ + "assert.deqEnter": { argTypes: [null, null], result: BOOL }, + "assert.deqLeave": { argTypes: [], result: VOID }, + "assert.match": { argTypes: [STRING, REGEX, BOOL, STRING, BOOL], result: VOID }, + "assert.throwsNone": { argTypes: [BOOL, STRING, BOOL, STRING, BOOL], result: VOID }, + "assert.throwsMismatch": { + argTypes: [STRING, { kind: "object", className: "%Error" }, STRING, BOOL, BOOL], + result: VOID, + }, + "assert.throwsRegex": { + argTypes: [REGEX, { kind: "object", className: "%Error" }, STRING, BOOL, BOOL], + result: VOID, + }, + // Symbol strict equality (pointer identity; scr_symbol.c). + "assert.eqSym": { + argTypes: [{ kind: "symbol" }, { kind: "symbol" }, BOOL, BOOL, STRING, BOOL], + result: VOID, + }, + // The equality quartet over checked-dynamic operands (the frontend + // boxes a static side into the checked-dynamic tree first). + "assert.eqDyn": { argTypes: [DYN, DYN, BOOL, BOOL, STRING, BOOL], result: VOID }, + /** A message argument that is not a string, registered for the next + * failing assertion (scr_assert_message_value). */ + "assert.messageValue": { argTypes: [DYN, BOOL], result: VOID }, + /** An assert method as a function value, by canonical name. */ + "assert.methodValue": { argTypes: [STRING], result: DYN }, + "assert.messageClear": { argTypes: [], result: VOID }, + "assert.looseResult": { argTypes: [BOOL, BOOL, STRING, STRING, STRING, BOOL], result: VOID }, + // The throws(fn, {shape}) accumulator: begin/slot calls never throw; + // shapeEnd throws the Comparison diff. The error slot is the + // %Error-narrowed caught value. + "assert.shapeBegin": { argTypes: [{ kind: "object", className: "%Error" }], result: VOID }, + "assert.shapeStr": { argTypes: [F64, STRING], result: VOID }, + "assert.shapeRe": { argTypes: [F64, REGEX], result: VOID }, + "assert.shapeEnd": { argTypes: [STRING, BOOL, BOOL], result: VOID }, + "assert.regexErrTest": { + argTypes: [REGEX, { kind: "object", className: "%Error" }], + result: BOOL, + }, + "assert.unwantedRejection": { + argTypes: [{ kind: "object", className: "%Error" }, STRING, BOOL], + result: VOID, + }, + "assert.unwantedError": { argTypes: [STRING, BOOL, STRING, BOOL], result: VOID }, + "assert.noErrorPredicate": { argTypes: [DYN, DYN], result: BOOL }, + "assert.regexDynTest": { argTypes: [REGEX, DYN], result: BOOL }, + /** Node's expectsError over an error-INSTANCE expected (assert.throws/ + * rejects second argument): walk the expected dyn error's keys (name, + * message, code — the %error marker skipped) and deep-compare each + * against the caught value's; a mismatch throws the deep-equal + * AssertionError (scr_assert.c). MAY THROW by design. */ + "assert.expectsErrDyn": { argTypes: [DYN, DYN, STRING, BOOL], result: VOID }, + /** A validation function against the caught value (Node's expectedException). */ + "assert.throwsValidate": { argTypes: [DYN, DYN, STRING, STRING, BOOL], result: VOID }, + // assert.ifError's typed entries (always throw; unit args never + // lower). The error slot is program-dependent — any %Error-hierarchy + // class, root or subclass (the insp.error precedent, null slot). + "assert.ifErrorErr": { argTypes: [null], result: VOID }, + "assert.ifErrorF64": { argTypes: [F64], result: VOID }, + "assert.ifErrorStr": { argTypes: [STRING], result: VOID }, + "assert.ifErrorBool": { argTypes: [BOOL], result: VOID }, + "assert.ifErrorDyn": { argTypes: [DYN], result: VOID }, + // Bytes equality: the two value slots are any bytes kind (the frontend + // gates both sides to ONE static bytes type; the libCall case checks + // the kind and the pair below). + "assert.refEqBytes": { argTypes: [null, null, BOOL, BOOL, STRING, BOOL], result: VOID }, + "assert.refEqFn": { argTypes: [null, null, BOOL, STRING, BOOL], result: VOID }, + "assert.bytesDeepEq": { argTypes: [null, null, BOOL], result: BOOL }, + "assert.looseBytesEq": { argTypes: [null, null], result: BOOL }, + // util.inspect: the error receiver is program-dependent (a builtin or + // user error class — the error.toString precedent, null slot). - "insp.f64": { argTypes: [F64], result: STRING }, - /** util.format %j over a checked-dynamic argument: the runtime dyn - * walk (JS-exact stringify; root undefined/function prints - * "undefined"; a handle in the tree throws the "not supported yet" - * Error). */ - "insp.jsonDyn": { argTypes: [DYN], result: STRING }, - "insp.str": { argTypes: [STRING], result: STRING }, - "insp.regex": { argTypes: [REGEX], result: STRING }, - /** A Date's inspect text: toISOString(), or "Invalid Date". */ - "insp.date": { argTypes: [DATE_T], result: STRING }, - "insp.buffer": { argTypes: [BYTES_U8], result: STRING }, - /** A numeric typed array that is not a Buffer (any element kind). */ - "insp.typedArray": { argTypes: [null, F64, F64], result: STRING }, - "insp.error": { argTypes: [null, F64, F64], result: STRING }, - "insp.errorHasCause": { argTypes: [null], result: BOOL }, - "insp.errorBagHasProperties": { argTypes: [DYN], result: BOOL }, - "insp.errorCause": { argTypes: [null, F64, F64], result: STRING }, - "insp.dyn": { argTypes: [DYN, F64, F64], result: STRING }, - "insp.dynS": { argTypes: [DYN, F64], result: STRING }, - "insp.begin": { argTypes: [F64], result: VOID }, - /** util.inspect's breakLength, compact (0 for false) and sorted for the - * renders that follow; the call site restores 80, 3 and false afterwards. */ - "insp.layout": { argTypes: [F64, F64, BOOL], result: VOID }, - "insp.entry": { argTypes: [STRING, BOOL], result: VOID }, - // Circular references: the receiver slot is program-dependent (any - // cycle-capable record/array/map/class type — the insp.error precedent). - /* Circular references over cycle-capable composites (recursive record/ - * class types): circCheck answers a value's circular id when it is - * already on the traversal stack (0 otherwise), seenPush/refWrap - * bracket the frame (insp.end adds Node's " " prefix to values - * the walk found circular), circular renders "[Circular *N]". */ - "insp.circCheck": { argTypes: [null], result: F64 }, - "insp.seenPush": { argTypes: [null], result: VOID }, - "insp.refWrap": { argTypes: [null, STRING], result: STRING }, - "insp.circular": { argTypes: [F64], result: STRING }, - /* Whether a closed declared-field record kept the undeclared properties - * of its checked source (scr_record_source_note): reflective surfaces - * (inspect, Object.keys/values/entries) then answer through its live - * checked view. The receiver is any such record type. Never throws. */ - "record.sourceHas": { argTypes: [null], result: BOOL }, - "insp.key": { argTypes: [STRING], result: STRING }, - "insp.moreItems": { argTypes: [F64], result: STRING }, - "insp.end": { argTypes: [STRING, STRING, STRING, F64, BOOL, BOOL], result: STRING }, - /** A nested inspect inside a custom rendering (PerformanceEntry's - * `${name} ${inspect(toJSON)}`): nestedBegin starts a fresh inspect - * context, nestedEnd(prefix, inner) restores the caller's and answers - * `prefix inner` re-indented at the caller's level (+1). */ - "insp.nestedBegin": { argTypes: [], result: VOID }, - "insp.nestedEnd": { argTypes: [STRING, STRING], result: STRING }, - /** node:string_decoder's utf8 StringDecoder (scr_string.c): the decoder - * value is a one-field record whose f64 PACKS the pending partial - * sequence (count + up to 3 raw bytes — Node buffers at most 3 for - * every encoding); the frontend's interned %strdec helpers thread it, - * with the decoder's CANONICAL encoding name first, through these pure - * functions. write: (enc, pending, chunk) → the decoded complete - * prefix of pending+chunk (+1); next: (enc, pending, chunk) → the - * packed NEW pending; end: (enc, pending) → the buffered partial's - * flush (+1). Node-exact per encoding (oracle-pinned); none throws. */ - "strdec.write": { argTypes: [STRING, F64, BYTES_U8], result: STRING }, - "strdec.next": { argTypes: [STRING, F64, BYTES_U8], result: F64 }, - "strdec.end": { argTypes: [STRING, F64], result: STRING }, - /** A runtime encoding argument's canonical name (Node's normalizeEncoding: - * nullish and "" are utf8); an unknown name throws ERR_UNKNOWN_ENCODING. */ - "strdec.encoding": { argTypes: [DYN], result: STRING }, - // node:readline: the interface handle is f64; the callbacks' func types - // are program-dependent (the emitter picks the adapter by parameter - // count), so those slots are null like child.onExit's. - /** node:readline and node:readline/promises (scr_readline.c, linked - * under the events gate — these fns imply moduleUsesProcessEvents; a - * stream input or output adds scr_stream.c's bridge). The interface - * value is an f64 handle (the Timeout-id precedent); an open interface - * on stdin keeps the loop alive until close/EOF. - * rl.create: [mode, input?, output?, crlfDelay, prompt?] → f64, mode - * bits 1 stream input, 2 stream output, 4 stdout output, 16 prompt - * (lowerReadlineCreateCall); rl.createSignal: the same plus the - * trailing AbortSignal (mode bit 8), which links the event-target unit - * for addAbortListener. rl.question: [handle, query, cb] — the callback - * form; THROWS ERR_USE_AFTER_CLOSE on a closed interface (may-throw). - * rl.questionAsync(Signal): [handle, query, signal?] → promise, - * rejecting instead of throwing. rl.close/pause/resume/prompt: - * [handle] (pause/resume/prompt throw ERR_USE_AFTER_CLOSE when - * closed); rl.setPrompt/getPrompt; rl.write: [handle, data] (throws - * when closed). rl.on: [handle, event, cb, once] — the event number - * (0 'line', 1 'close', 2 'pause', 3 'resume', 4 'error') and the - * listener (moves). rl.iterOpen/iterNext/iterReturn: the async - * iterator — iterNext answers promise of the line or undefined. */ - "rl.create": { argTypes: [F64], result: F64 }, - "rl.createSignal": { argTypes: [F64], result: F64 }, - "rl.question": { argTypes: [F64, STRING, null], result: VOID }, - "rl.questionAsync": { argTypes: [F64, STRING], result: { kind: "promise", inner: STRING } }, - "rl.questionAsyncSignal": { - argTypes: [F64, STRING, DYN], - result: { kind: "promise", inner: STRING }, - }, - /** The question forms with a checked-dynamic query (a JavaScript value - * that need not be a string). */ - "rl.questionDyn": { argTypes: [F64, DYN, null], result: VOID }, - "rl.questionAsyncDyn": { argTypes: [F64, DYN], result: { kind: "promise", inner: STRING } }, - "rl.questionAsyncSignalDyn": { - argTypes: [F64, DYN, DYN], - result: { kind: "promise", inner: STRING }, - }, - "rl.close": { argTypes: [F64], result: VOID }, - "rl.pause": { argTypes: [F64], result: VOID }, - "rl.resume": { argTypes: [F64], result: VOID }, - "rl.prompt": { argTypes: [F64], result: VOID }, - /** prompt(preserveCursor): [handle, preserveCursor] */ - "rl.promptWith": { argTypes: [F64, BOOL], result: VOID }, - "rl.setPrompt": { argTypes: [F64, STRING], result: VOID }, - "rl.setPromptDyn": { argTypes: [F64, DYN], result: VOID }, - "rl.getPrompt": { argTypes: [F64], result: STRING }, - "rl.write": { argTypes: [F64, STRING], result: VOID }, - /** write(data, key): [handle, hasData, data, hasKey, hasName, name, - * ctrl, meta, shift] */ - "rl.writeKey": { - argTypes: [F64, BOOL, STRING, BOOL, BOOL, STRING, BOOL, BOOL, BOOL], - result: VOID, - }, - "rl.on": { argTypes: [F64, F64, null, BOOL], result: VOID }, - "rl.iterOpen": { argTypes: [F64], result: VOID }, - "rl.iterNext": { argTypes: [F64], result: { kind: "promise", inner: DYN } }, - "rl.iterReturn": { argTypes: [F64], result: VOID }, - /** createInterface's history and editing options (scr_rl_check_options: - * [flags, tabSize, escapeCodeTimeout, historySize], may throw; - * rl.setOptions: [handle, flags, historySize, removeHistoryDuplicates, - * tabSize, escapeCodeTimeout]; rl.setHistory: [handle, history]) and - * the member reads (rl.history answers the array, rl.numProp the - * numeric member by number). */ - "rl.checkOptions": { argTypes: [F64, F64, F64, F64], result: VOID }, - "rl.checkCompleter": { argTypes: [DYN], result: VOID }, - "rl.setOptions": { argTypes: [F64, F64, F64, BOOL, F64, F64], result: VOID }, - "rl.setHistory": { argTypes: [F64, arrayOf(STRING)], result: VOID }, - "rl.history": { argTypes: [F64], result: arrayOf(STRING) }, - "rl.numProp": { argTypes: [F64, F64], result: F64 }, - /** Terminal mode (scr_readline_tty.c): rl.terminal: [handle, mode (0 - * the option's value, 2 output.isTTY of process.stdout), terminal, - * completer (undefined when absent), readline/promises], emitted with - * scr_insp_dyn when there is a completer; the line, terminal and cursor - * members. */ - "rl.terminal": { argTypes: [F64, F64, BOOL, DYN, BOOL], result: VOID }, - "rl.line": { argTypes: [F64], result: STRING }, - "rl.isTerminal": { argTypes: [F64], result: BOOL }, - "rl.isPromises": { argTypes: [F64], result: BOOL }, - /** createInterface's input or output as a checked-dynamic value: - * [value, output] — the stream; refused when it is not one. */ - "rl.inputFromDyn": { argTypes: [DYN, BOOL], result: { kind: "object", className: "%Readable" } }, - "rl.outputFromDyn": { - argTypes: [DYN, BOOL], - result: { kind: "object", className: "%Writable" }, - }, - "rl.cursor": { argTypes: [F64], result: F64 }, - /** readline.cursorTo / moveCursor / clearLine / clearScreenDown: rl.seq - * [kind (0-3), the first and second numbers, whether the second is - * given] answers the data to write after the argument checks (may - * throw); rl.moves [dx, dy] says whether moveCursor writes. */ - "rl.seq": { argTypes: [F64, F64, F64, BOOL], result: STRING }, - "rl.moves": { argTypes: [F64, F64], result: BOOL }, - /** getCursorPos() (scr_readline_tty.c, linked through the regex unit - * like rl.terminal): [handle, 0 rows | 1 cols] */ - "rl.cursorPos": { argTypes: [F64, F64], result: F64 }, - /** readline.emitKeypressEvents (scr_readline_tty.c): rl.keypressStdin - * [interface (0: none), escapeCodeTimeout (NaN: the interface's or - * 500)], rl.keypressStream [stream, interface, timeout]. */ - "rl.keypressStdin": { argTypes: [F64, F64], result: VOID }, - "rl.keypressStream": { argTypes: [null, F64, F64], result: VOID }, - /** readline/promises Readline over an f64 handle: rl.uiOpen [fd (1 - * stdout, 2 stderr), autoCommit], rl.uiOpenStream [stream, autoCommit] - * (isWritable, may throw); the actions take [handle, ...numbers] (the - * checks may throw; cursorTo: [handle, x, whether y is given, y]); - * rl.uiCommit answers a promise. */ - "rl.uiOpen": { argTypes: [F64, BOOL], result: F64 }, - "rl.uiOpenStream": { argTypes: [null, BOOL], result: F64 }, - "rl.uiCursorTo": { argTypes: [F64, F64, BOOL, F64], result: VOID }, - "rl.uiMoveCursor": { argTypes: [F64, F64, F64], result: VOID }, - "rl.uiClearLine": { argTypes: [F64, F64], result: VOID }, - "rl.uiClearScreenDown": { argTypes: [F64], result: VOID }, - "rl.uiRollback": { argTypes: [F64], result: VOID }, - "rl.uiCommit": { argTypes: [F64], result: { kind: "promise", inner: VOID } }, - /** node:timers/promises — the promisified pair (scr_async.c, beside - * the timer heap they ride): tp.setTimeout: [ms] → a pending void - * promise a one-shot heap timer fulfills (the loop's timer phase, FIFO - * against equal deadlines like Node); tp.setImmediate: [] → the same - * through the immediate queue (fires before due timers of later loop - * turns, Node's check phase). Neither throws. */ - "tp.setTimeout": { argTypes: [F64], result: { kind: "promise", inner: VOID } }, - // scheduler.wait(delay): tp.setTimeout's promise. It throws Node's - // ERR_INVALID_THIS once mock.timers has restored the real scheduler.wait. - "tp.schedulerWait": { argTypes: [F64], result: { kind: "promise", inner: VOID } }, - "tp.setImmediate": { argTypes: [], result: { kind: "promise", inner: VOID } }, - /** events.once (scr_events_emitter.c): [emitter, name, boxer, - * errorBoxer, listenForError] → a pending promise of any[]; the boxers - * turn the event's and the 'error' tuple into that array. */ - "events.once": { argTypes: [null, STRING, null, null, BOOL], result: VOID }, - /** events.once(emitter, name, { signal }) (scr_events_once_signal.c): - * events.once's arguments plus the checked-dynamic signal (an - * AbortSignal or undefined). */ - "events.onceSignal": { argTypes: [null, STRING, null, null, BOOL, DYN], result: VOID }, - /** The resolve-value forms: [ms?, value, ref] → a pending promise of the - * value's type that copies the borrowed already-resolved `value` promise - * when the timer (or immediate) fires; ref false unrefs it. The result - * type follows the value promise. */ - "tp.setTimeoutValue": { argTypes: [F64, null, BOOL], result: VOID }, - "tp.setImmediateValue": { argTypes: [null, BOOL], result: VOID }, - /** The same with options.signal (a checked-dynamic AbortSignal or - * undefined) last: an abort clears the timer and rejects with an - * AbortError (scr_event_target.c). */ - "tp.setTimeoutSignal": { argTypes: [F64, null, BOOL, DYN], result: VOID }, - "tp.setImmediateSignal": { argTypes: [null, BOOL, DYN], result: VOID }, - /** scheduler.wait(delay, options): tp.setTimeoutValue's and - * tp.setTimeoutSignal's shapes, with tp.schedulerWait's throws. */ - "tp.schedulerWaitValue": { argTypes: [F64, null, BOOL], result: VOID }, - "tp.schedulerWaitSignal": { argTypes: [F64, null, BOOL, DYN], result: VOID }, - /** node:diagnostics_channel (scr_dc.c, linked when any dc.* appears — - * the zlib gating precedent): a process-global name→channel registry; - * channel values are f64 handles (type-mapper.ts maps Channel to F64, the - * readline.Interface pattern). Subscribers are dyn function values — - * identity-compared by unsubscribe, called (message, name) by publish - * over a SNAPSHOT of the list (a subscriber unsubscribing itself - * mid-publish still lets its siblings fire, Node's behavior). dc.publish - * MAY THROW: a subscriber's throw propagates out of publish (catchable - * there) where Node routes it to triggerUncaughtException — the - * documented divergence. subscribe/unsubscribe throw Node's - * ERR_INVALID_ARG_TYPE TypeError for non-function subscribers. */ - "dc.channel": { argTypes: [STRING], result: F64 }, - "dc.subscribe": { argTypes: [STRING, DYN], result: VOID }, - "dc.unsubscribe": { argTypes: [STRING, DYN], result: BOOL }, - "dc.hasSubscribers": { argTypes: [STRING], result: BOOL }, - "dc.publish": { argTypes: [F64, DYN], result: VOID }, - "dc.chanSubscribe": { argTypes: [F64, DYN], result: VOID }, - "dc.chanUnsubscribe": { argTypes: [F64, DYN], result: BOOL }, - "dc.chanHasSubscribers": { argTypes: [F64], result: BOOL }, - "dc.chanName": { argTypes: [F64], result: STRING }, - /** TracingChannel (dc.tracingChannel): a registry entry of the five - * event channels, an f64 handle like Channel (type-mapper.ts). tcSubscribe/ - * tcUnsubscribe walk a dyn handlers object's five event keys (truthy - * non-function slots throw the per-channel ERR_INVALID_ARG_TYPE); - * tcTraceSync/tcTraceCallback/tcTracePromise run Node's publish choreography in C over - * dyn values (fn, ctx, thisArg, args-array) with thisArg bound as the - * ambient receiver — the traced call's throw and any subscriber throw - * both propagate (MAY THROW). tcTraceCallback wraps args[position] in a - * native error/result + asyncStart/asyncEnd publisher and throws Node's - * TypeError when that slot is not callable. tracingChannelOf is the - * five-Channel collection form of the constructor. */ - /** setImmediate as a first-class dyn value (scr_async.c): a minted dyn - * callable scheduling args[0](args[1..]) on the immediate queue — the - * Node-suite traceCallback shape (`traceCallback(setImmediate, ...)`). - * Calling it validates the callback (the dyn call machinery); minting - * never throws. */ - "timers.setImmediateFnValue": { argTypes: [], result: DYN }, - /** `new Promise(setImmediate)` (the Node-suite early-exit shape): a - * fresh promise an immediate fulfills with the undefined dyn value — - * the executor IS setImmediate, so resolve rides the immediate queue - * (scr_async.c). Never throws. */ - "timers.immediatePromise": { argTypes: [], result: { kind: "promise", inner: DYN } }, - "dc.tracingChannel": { argTypes: [STRING], result: F64 }, - "dc.tracingChannelOf": { argTypes: [F64, F64, F64, F64, F64], result: F64 }, - "dc.tcChannel": { argTypes: [F64, F64], result: F64 }, - "dc.tcHasSubscribers": { argTypes: [F64], result: BOOL }, - "dc.tcSubscribe": { argTypes: [F64, DYN], result: VOID }, - "dc.tcUnsubscribe": { argTypes: [F64, DYN], result: BOOL }, - "dc.tcTraceSync": { argTypes: [F64, DYN, DYN, DYN, DYN], result: DYN }, - "dc.tcTraceCallback": { argTypes: [F64, DYN, F64, DYN, DYN, DYN], result: DYN }, - /** tracePromise (scr_dc.c): start publish, the traced call, a wrap of - * non-promise results, the end publish, and a REACTION FIBER that - * awaits the traced promise and publishes asyncStart/asyncEnd (error - * first on rejection) before settling the returned promise with - * the passed-through outcome. MAY THROW (the traced call and the - * synchronous publishes). */ - "dc.tcTracePromise": { - argTypes: [F64, DYN, DYN, DYN, DYN], - result: { kind: "promise", inner: DYN }, - }, - /** process.on/once('unhandledRejection', fn): registers a dyn listener - * the checkpoint report dispatches per never-observed rejection — - * (reason, promise) — instead of printing and exiting 1 (scr_async.c). - * The bool second arg is `once` (auto-removed after one delivery, - * Node's once); off/removeListener remove by closure identity, the - * offWarning stance. Throws Node's ERR_INVALID_ARG_TYPE on a - * non-function. */ - /** Uncaught exception handlers and monitors: [callback dyn, once, monitor]. */ - "process.onUncaughtException": { argTypes: [DYN, BOOL, BOOL], result: VOID }, - "process.offUncaughtException": { argTypes: [DYN, BOOL], result: VOID }, - "process.onUnhandledRejection": { argTypes: [DYN, BOOL], result: VOID }, - "process.offUnhandledRejection": { argTypes: [DYN], result: VOID }, - /** process.on/once/off('rejectionHandled', fn): the sibling registry. - * A handler attached after unhandledRejection delivery fires the event - * once. Dispatch is synchronous at the attach, with the promise, - * Node's payload. */ - "process.onRejectionHandled": { argTypes: [DYN, BOOL], result: VOID }, - "process.offRejectionHandled": { argTypes: [DYN], result: VOID }, - /** process warnings (scr_async_dyn.c): onWarning/offWarning register - * dyn listeners; emitWarning applies Node's full argument grammar over - * the call's dyn argument vector (ERR_INVALID_ARG_TYPE TypeErrors — MAY - * THROW) and emits on the next tick, like Node: Node's stderr report, - * then the listeners. */ - "process.onWarning": { argTypes: [DYN], result: VOID }, - "process.offWarning": { argTypes: [DYN], result: VOID }, - "process.emitWarning": { argTypes: [DYN], result: VOID }, - /** A Warning-typed process warning with this message (the console - * members' warnings), emitted on the next tick as Node's emitWarning - * does (so the loop runs); an empty message emits nothing. */ - "process.emitWarningText": { argTypes: [STRING], result: VOID }, - /** process.noDeprecation (0) / process.throwDeprecation (1): the stored - * value (undefined until assigned) and its assignment. util.deprecate's - * warnings and DeprecationWarning emissions read their truthiness. */ - "process.deprecationFlag": { argTypes: [F64], result: DYN }, - "process.setDeprecationFlag": { argTypes: [F64, DYN], result: VOID }, - "process.setEmitWarning": { argTypes: [DYN], result: VOID }, - /** AsyncLocalStorage (node:async_hooks — scr_async.c): stores are f64 - * handles; contexts are immutable fiber-carried snapshots (spawned - * fibers inherit the spawner's — Node's init-time capture). run/exit - * enter (or clear) the store, call the dyn function with forwarded - * arguments, and restore (the finally); getStore answers the current - * dyn value or undefined; enterWith installs with no restore point. - * run/exit MAY THROW (the callback's own throws propagate). */ - /** `await v` over a checked-dynamic VALUE (scr_async.c): a dyn promise - * adopts (rejections re-throw — MAY THROW), anything else takes JS's - * one-microtask non-thenable await and answers itself. Only emitted - * inside async bodies (the frontend's isAsync gate). */ - "async.awaitDyn": { argTypes: [DYN], result: DYN }, - /** The bare one-microtask hop (scr_await_hop): `await v` over a typed - * NON-promise value — JS awaits non-thenables through exactly one - * microtask turn and yields the value itself. Never throws. */ - "async.hop": { argTypes: [], result: VOID }, - "als.new": { argTypes: [], result: F64 }, - "als.get": { argTypes: [F64], result: DYN }, - "als.run": { argTypes: [F64, DYN, DYN, DYN], result: DYN }, - "als.exitRun": { argTypes: [F64, DYN, DYN], result: DYN }, - "als.enterWith": { argTypes: [F64, DYN], result: VOID }, - "als.disable": { argTypes: [F64], result: VOID }, - /** Channel.bindStore/unbindStore/runStores (scr_dc.c): the - * AsyncLocalStorage integration — set-semantics bindings per store, - * runStores entering every bound store with transform(data) around the - * publish and the callback (MAY THROW: transforms, subscribers, and - * the callback all run). */ - "dc.chanBindStore": { argTypes: [F64, F64, DYN], result: VOID }, - "dc.chanUnbindStore": { argTypes: [F64, F64], result: BOOL }, - "dc.chanRunStores": { argTypes: [F64, DYN, DYN, DYN, DYN], result: DYN }, - /** The Number statics with a static C implementation (scr_lib.c): one - * f64 arg → bool, JS-exact BY CONSTRUCTION — Number.isFinite/isNaN/ - * isInteger/isSafeInteger never coerce, and the frontend routes only - * f64-typed arguments here (other static types fence). None throws. */ - "number.isFinite": { argTypes: [F64], result: BOOL }, - "number.isNaN": { argTypes: [F64], result: BOOL }, - "number.isInteger": { argTypes: [F64], result: BOOL }, - "number.isSafeInteger": { argTypes: [F64], result: BOOL }, - /** Date (scr_lib.c). Values are TimeClip'd epoch-millisecond scalars: - * construction/store/pass/getters are exact while identity and mutation - * remain fenced. date.now is Node's integer milliseconds since epoch; - * date.toISOString formats one raw f64 millisecond time value and - * date.toISOStringValue formats a stored Date, both with Node's rules (UTC, - * YYYY-MM-DDTHH:mm:ss.sssZ, expanded ±YYYYYY years outside 0–9999, - * ToInteger truncation of fractional ms) and THROWS Node's "Invalid - * time value" RangeError on NaN / out-of-range input (may-throw seed). - * Results: f64 / owned (+1) string. */ - "date.now": { argTypes: [], result: F64 }, - "date.newNow": { argTypes: [], result: DATE_T }, - "date.newMs": { argTypes: [F64], result: DATE_T }, - "date.newString": { argTypes: [STRING], result: DATE_T }, - /** Date.parse(dateString), with its own reach witness for exact fences. */ - "date.parse": { argTypes: [STRING], result: F64 }, - "date.getTime": { argTypes: [DATE_T], result: F64 }, - "date.valueOf": { argTypes: [DATE_T], result: F64 }, - "date.toISOString": { argTypes: [F64], result: STRING }, - "date.toISOStringValue": { argTypes: [DATE_T], result: STRING }, - /** Date.prototype.toUTCString over a stored Date ("Invalid Date" for - * NaN; never throws). */ - "date.toUTCString": { argTypes: [DATE_T], result: STRING }, - "date.getFullYear": { argTypes: [DATE_T], result: F64 }, - "date.getUTCFullYear": { argTypes: [DATE_T], result: F64 }, - "date.getMonth": { argTypes: [DATE_T], result: F64 }, - "date.getUTCMonth": { argTypes: [DATE_T], result: F64 }, - "date.getDate": { argTypes: [DATE_T], result: F64 }, - "date.getUTCDate": { argTypes: [DATE_T], result: F64 }, - "date.getDay": { argTypes: [DATE_T], result: F64 }, - "date.getUTCDay": { argTypes: [DATE_T], result: F64 }, - "date.getHours": { argTypes: [DATE_T], result: F64 }, - "date.getUTCHours": { argTypes: [DATE_T], result: F64 }, - "date.getMinutes": { argTypes: [DATE_T], result: F64 }, - "date.getUTCMinutes": { argTypes: [DATE_T], result: F64 }, - "date.getSeconds": { argTypes: [DATE_T], result: F64 }, - "date.getUTCSeconds": { argTypes: [DATE_T], result: F64 }, - "date.getMilliseconds": { argTypes: [DATE_T], result: F64 }, - "date.getUTCMilliseconds": { argTypes: [DATE_T], result: F64 }, - "date.getTimezoneOffset": { argTypes: [DATE_T], result: F64 }, - /** Date.prototype.toString / toDateString / toTimeString and String(date): - * V8's local-time formats with the host zone's long name, "Invalid Date" - * for an invalid date. Owned (+1) string; never throws. */ - "date.toString": { argTypes: [DATE_T], result: STRING }, - "date.toDateString": { argTypes: [DATE_T], result: STRING }, - "date.toTimeString": { argTypes: [DATE_T], result: STRING }, - /** The composed `new Date(dateString).getTime()` read: one borrowed - * string, f64 milliseconds since epoch, with V8's date-string grammar - * (the ES5 format, then the legacy forms: RFC 1123/2822, Date#toString, - * "Jan 2, 2024", ...). Zone-less dates are local time; anything V8 - * rejects is NaN, Node's invalid-date getTime. Never throws. */ - "date.parseGetTime": { argTypes: [STRING], result: F64 }, - /** `Date.UTC(...)` — seven f64 arguments (the frontend completes the - * spec's defaults for omitted trailing parts: month 0, date 1, time - * parts 0), the spec's MakeDay/MakeTime/TimeClip exactly: 0–99 years - * map to 1900+year, out-of-range months/dates roll over, non-finite - * parts and out-of-range results answer NaN. Never throws. */ - "date.utc": { argTypes: [F64, F64, F64, F64, F64, F64, F64], result: F64 }, - /** `new Date(year, monthIndex[, date, hours, minutes, seconds, ms])` — - * the same seven-f64 completion as date.utc, but the fields are LOCAL - * time: V8's MakeDay/MakeTime, then UTC(t) in the host zone (DST gaps - * read with the offset before the transition, overlaps answer the - * earlier instant), then TimeClip. Never throws. */ - "date.newLocal": { argTypes: [F64, F64, F64, F64, F64, F64, F64], result: DATE_T }, - /** WHATWG TextDecoder.decode over u8 bytes (scr_bytes.c): utf-8 with - * default options — the same maximal-subpart replacement decode as - * Buffer.toString("utf8"), with the leading BOM stripped (the one - * behavioral difference; ignoreBOM defaults to false). Inline calls - * lower directly; stored decoder records dispatch by encoding id. - * TextEncoder.encode needs no libFn: its matching forms - * lower to buffer.fromStr(s, "utf8") (identical bytes — both write each - * lone surrogate as U+FFFD). Borrowed arg; owned (+1) string; never throws. */ - "text.decode": { argTypes: [BYTES_U8], result: STRING }, - "bytes.bufferSource": { argTypes: [DYN], result: BYTES_U8 }, - /** Whether an error-first callback's Buffer result is absent: the zlib - * and crypto callback adapters pass NULL after an error. Borrowed arg; - * never throws. */ - "bytes.isAbsent": { argTypes: [BYTES_U8], result: BOOL }, - /** The absent Buffer result itself (NULL), where an error-path callback - * body passes its undefined result on into a Buffer slot. Owned (+1); - * never throws. */ - "bytes.absent": { argTypes: [], result: BYTES_U8 }, - /** node:crypto asymmetric keys and signatures (scr_pki.c): checked- - * dynamic arguments, borrowed; KeyObject, Sign and Verify handles, - * Buffers and booleans as checked-dynamic results (+1). Node's argument - * and decoder errors throw (may-throw); generateKeyPairCb's callback - * receives (null, publicKey, privateKey) in a later macrotask. */ - "crypto.createPrivateKey": { argTypes: [DYN], result: DYN }, - "crypto.createPublicKey": { argTypes: [DYN], result: DYN }, - "crypto.generateKeyPairSync": { argTypes: [DYN, DYN], result: DYN }, - "crypto.generateKeyPairCb": { argTypes: [DYN, DYN, DYN], result: VOID }, - "crypto.signOneshot": { argTypes: [DYN, DYN, DYN], result: DYN }, - "crypto.verifyOneshot": { argTypes: [DYN, DYN, DYN, DYN], result: DYN }, - "crypto.createSign": { argTypes: [DYN], result: DYN }, - "crypto.createVerify": { argTypes: [DYN], result: DYN }, - /** The Buffer brand of a Uint8Array source onto a fresh Uint8Array - * result (map and filter of a Buffer return a Buffer). */ - "bytes.copyBrand": { argTypes: [BYTES_U8, BYTES_U8], result: VOID }, - "text.decodeBufferSource": { argTypes: [DYN], result: STRING }, - "text.decodeOptions": { argTypes: [BYTES_U8, BOOL, BOOL], result: STRING }, - "text.decodeStream": { argTypes: [DYN, BYTES_U8, F64, BOOL, BOOL, BOOL], result: STRING }, - "text.decodeLegacyOptions": { argTypes: [BYTES_U8, F64, BOOL, BOOL], result: STRING }, - /** TextDecoder.decode for a compile-time non-UTF-8 WHATWG label. The - * second f64 is the frontend-owned encoding id consumed by scr_bytes.c; - * label aliases/case/ASCII whitespace are canonicalized before IR. The - * mapping tables are generated from the pinned Node 24 oracle, keeping - * output portable without an ICU/iconv dependency. Borrowed args; owned - * (+1) string; never throws. */ - "text.decodeLegacy": { argTypes: [BYTES_U8, F64], result: STRING }, - "text.decoderEncoding": { argTypes: [DYN], result: F64 }, - "text.decoderName": { argTypes: [F64], result: STRING }, - "text.encodeInto": { argTypes: [DYN, DYN], result: DYN }, - "buffer.isAscii": { argTypes: [DYN], result: BOOL }, - "buffer.isUtf8": { argTypes: [DYN], result: BOOL }, - "buffer.transcode": { argTypes: [DYN, DYN, DYN], result: BYTES_U8 }, - /** The fs option forms and friends (scr_lib.c), all throwing catchably - * like the rest of sync fs. mkdirRecursiveSync is Node's recursive - * algorithm (try mkdir, EEXIST-dir is fine, ENOENT creates the parent - * first — errors report Node's errno at Node's path, EEXIST at a file - * target, ENOTDIR at the full path past a file); the first-created-dir - * return value has no lowering (statement position only, frontend- - * fenced). rmOptsSync is rmSync with (recursive, force) bools: force - * swallows ENOENT, recursive removes trees post-order, a directory - * without recursive throws Node's ERR_FS_EISDIR SystemError. mkdtempSync appends the six X's and returns the - * created path (+1). accessSync takes the F_OK/R_OK/W_OK/X_OK bits as - * one f64 (the frontend bakes fs.constants.* as literals and completes - * an omitted mode to 0). readFdSync/readFdSyncBytes are the - * readFileSync(fd[, "utf8"]) forms — a read(2) loop to EOF on the fd - * (the stdin path); errors carry Node's no-path message shape. */ - "fs.mkdirRecursiveSync": { argTypes: [STRING], result: VOID }, - "fs.rmOptsSync": { argTypes: [STRING, BOOL, BOOL], result: VOID }, - /** rmOptsSync's maxRetries/retryDelay form (path, recursive, force, - * maxRetries, retryDelay): Node's linear-backoff retry on - * EBUSY/EMFILE/ENFILE/ENOTEMPTY/EPERM — the tmpdir-harness shape - * rmSync(p, { maxRetries: 3, recursive: true, force: true }). Throws - * ERR_OUT_OF_RANGE for a retryDelay outside int32 >= 0 or a maxRetries - * outside uint32, before checking the path. */ - "fs.rmRetrySync": { argTypes: [STRING, BOOL, BOOL, F64, F64], result: VOID }, - "fs.mkdtempSync": { argTypes: [STRING], result: STRING }, - "fs.accessSync": { argTypes: [STRING, F64], result: VOID }, - "fs.readFdSync": { argTypes: [F64, STRING], result: STRING }, - "fs.readFdSyncBytes": { argTypes: [F64], result: BYTES_U8 }, - /** Validated isatty(3) probe. Stream property lowering maps false to - * undefined; tty.isatty exposes the boolean result directly. */ - "process.isTTY": { argTypes: [F64], result: BOOL }, - // Like process.envGet: the result is the module's interned - // `number | undefined` union — checked by arms in the libCall case. - /** Terminal width over an fd literal (1/2 — process.stdout/stderr - * .columns): ioctl(TIOCGWINSZ). The result is the module's interned - * `number | undefined` union — a non-TTY stream (or an ioctl refusal) - * yields the undefined arm, exactly Node's missing `.columns`. Never - * throws. */ - "process.columns": { argTypes: [F64], result: VOID }, - /** Terminal height, with the same fd and optional-number contract. */ - "process.rows": { argTypes: [F64], result: VOID }, - /** process.stdin.destroy(): a deliberate no-op (no stream machinery - * exists to tear down; documented in SEMANTICS.md). */ - "process.stdinDestroy": { argTypes: [], result: VOID }, - /** process.stdin.setRawMode(mode) — one borrowed bool arg, void. On a - * TTY stdin: termios raw mode on/off (libuv's UV_TTY_MODE_RAW flag set, - * the mode Node's setRawMode(true) applies; false restores the entry - * state). On a NON-TTY stdin Node's process.stdin has no such method at - * all, so the call throws Node's exact catchable TypeError - * ("process.stdin.setRawMode is not a function") — the may-throw seed - * carries it. */ - "process.stdinSetRawMode": { argTypes: [BOOL], result: VOID }, - // Arg 0 is a packed f64[] OR a bytes value (the spread-typed-array - // form) — checked in the libCall case. - /** fromCharCode takes one packed f64[] or bytes arg and builds a string - * from UTF-16 code units. Adjacent surrogate pairs combine; lone - * surrogates follow the runtime's replacement policy. */ - "string.fromCharCode": { argTypes: [null], result: STRING }, - /** Numeric code points, with catchable RangeError for non-integers or - * values outside 0..0x10ffff. Uses the same UTF-8 surrogate policy. */ - "string.fromCodePoint": { argTypes: [null], result: STRING }, - /** lastIndexOf returns the last UTF-16 start index, or -1. The two-arg - * form searches at or before its numeric position; NaN starts at the end. - * String arguments are borrowed and neither form throws. */ - "string.lastIndexOf": { argTypes: [STRING, STRING], result: F64 }, - "string.lastIndexOfFrom": { argTypes: [STRING, STRING, F64], result: F64 }, - /** String.raw(template, ...subs): the raw literals array (a string[] - * read off the template record) interleaved with the PRE-STRINGIFIED - * substitutions (the frontend applies the static ToString and packs - * them into one string[] literal) — extra substitutions drop, missing - * ones skip, the spec's loop exactly (scr_array.c). Borrowed args; - * +1 string; never throws. */ - "string.raw": { argTypes: [arrayOf(STRING), arrayOf(STRING)], result: STRING }, -}); + "insp.f64": { argTypes: [F64], result: STRING }, + /** util.format %j over a checked-dynamic argument: the runtime dyn + * walk (JS-exact stringify; root undefined/function prints + * "undefined"; a handle in the tree throws the "not supported yet" + * Error). */ + "insp.jsonDyn": { argTypes: [DYN], result: STRING }, + "insp.str": { argTypes: [STRING], result: STRING }, + "insp.regex": { argTypes: [REGEX], result: STRING }, + /** A Date's inspect text: toISOString(), or "Invalid Date". */ + "insp.date": { argTypes: [DATE_T], result: STRING }, + "insp.buffer": { argTypes: [BYTES_U8], result: STRING }, + /** A numeric typed array that is not a Buffer (any element kind). */ + "insp.typedArray": { argTypes: [null, F64, F64], result: STRING }, + /** A u8 byte view in a union arm, element or field: a DataView, Buffer + * or Uint8Array by its own flags. */ + "insp.bytes": { argTypes: [null, F64, F64], result: STRING }, + "insp.error": { argTypes: [null, F64, F64], result: STRING }, + "insp.errorHasCause": { argTypes: [null], result: BOOL }, + "insp.errorBagHasProperties": { argTypes: [DYN], result: BOOL }, + "insp.errorCause": { argTypes: [null, F64, F64], result: STRING }, + "insp.dyn": { argTypes: [DYN, F64, F64], result: STRING }, + "insp.dynS": { argTypes: [DYN, F64], result: STRING }, + /** util.format over a dyn array of its arguments, and + * util.formatWithOptions(options, pack): the runtime formatter for format + * strings known at run time and spread arguments. */ + "insp.format": { argTypes: [DYN], result: STRING }, + /** insp.format over arguments that are all strings, numbers, booleans, + * null or undefined, without the checked-dynamic walk. */ + "insp.formatPrimitives": { argTypes: [arrayOf(DYN)], result: STRING }, + "insp.formatList": { argTypes: [arrayOf(DYN)], result: STRING }, + "insp.formatWithOptions": { argTypes: [DYN, DYN], result: STRING }, + /** util.inspect(value, options, depth, colors) with the call's argument + * count: the runtime reads the options and renders the checked-dynamic + * view. */ + "insp.inspect": { argTypes: [DYN, DYN, DYN, DYN, F64], result: STRING }, + /** A [util.inspect.custom] method's call: whether ctx.customInspect is + * set, its depth argument (the remaining depth at recursion r of depth + * d), its options object and util.inspect, then its result rendered: a + * string re-indented at the current level, or a checked-dynamic value + * (customIsSelf tests for the instance itself). customInspect sets the + * flag for the renders that follow and answers the previous value, and + * depthKind does the same for how an unbounded depth was given (0 + * Infinity, 1 null, 2 undefined). */ + "insp.customEnabled": { argTypes: [], result: BOOL }, + /** Registers a class's compiled custom inspect function under its + * instances' typed-ref type key, for the checked-dynamic walk. */ + "insp.registerCustom": { argTypes: [STRING, DYN], result: VOID }, + "insp.customInspect": { argTypes: [BOOL], result: BOOL }, + "insp.depthKind": { argTypes: [F64], result: F64 }, + "insp.customDepth": { argTypes: [F64, F64], result: DYN }, + "insp.customOptions": { argTypes: [F64], result: DYN }, + "insp.inspectFunction": { argTypes: [], result: DYN }, + "insp.customText": { argTypes: [STRING], result: STRING }, + "insp.customIsSelf": { argTypes: [DYN, DYN], result: BOOL }, + "insp.customResult": { argTypes: [DYN, F64, F64], result: STRING }, + /** Around the method's call: the inspect and format calls it makes run + * in a fresh inspect context with the default options. begin answers the + * token end takes; end also discards what a call that threw left. */ + "insp.customCallBegin": { argTypes: [], result: F64 }, + "insp.customCallEnd": { argTypes: [F64], result: VOID }, + /** Whether a runtime format string has a '%' before its last character. */ + "insp.formatSpecifies": { argTypes: [STRING], result: BOOL }, + "insp.formatSpecifiesDyn": { argTypes: [DYN], result: BOOL }, + "insp.dynFormatS": { argTypes: [DYN, F64], result: STRING }, + /** util.format's %o over a checked-dynamic value: inspect with + * showHidden. Kinds whose hidden entries have no rendering throw the + * 'not supported yet' Error. */ + "insp.dynHidden": { argTypes: [DYN, F64], result: STRING }, + "insp.begin": { argTypes: [F64], result: VOID }, + /** util.inspect's breakLength, compact (-1 for false) and sorted for the + * renders that follow; the call site restores 80, 3 and false afterwards. */ + "insp.layout": { argTypes: [F64, F64, BOOL], result: VOID }, + "insp.entry": { argTypes: [STRING, BOOL], result: VOID }, + // Circular references: the receiver slot is program-dependent (any + // cycle-capable record/array/map/class type — the insp.error precedent). + /* Circular references over cycle-capable composites (recursive record/ + * class types): circCheck answers a value's circular id when it is + * already on the traversal stack (0 otherwise), seenPush/refWrap + * bracket the frame (insp.end adds Node's " " prefix to values + * the walk found circular), circular renders "[Circular *N]". */ + "insp.circCheck": { argTypes: [null], result: F64 }, + "insp.seenPush": { argTypes: [null], result: VOID }, + "insp.refWrap": { argTypes: [null, STRING], result: STRING }, + "insp.circular": { argTypes: [F64], result: STRING }, + /* Whether a closed declared-field record kept the undeclared properties + * of its checked source (scr_record_source_note): reflective surfaces + * (inspect, Object.keys/values/entries) then answer through its live + * checked view. The receiver is any such record type. Never throws. */ + "record.sourceHas": { argTypes: [null], result: BOOL }, + /** `record.sourceSpread(built, value)`: the record a literal starting + * with `...value` just built keeps the order of value's kept source + * (key-order-witness.ts ordered spreads). Both are record types. Never + * throws. */ + "record.sourceSpread": { argTypes: [null, null], result: VOID }, + "insp.key": { argTypes: [STRING], result: STRING }, + "insp.moreItems": { argTypes: [F64], result: STRING }, + "insp.end": { argTypes: [STRING, STRING, STRING, F64, BOOL, BOOL], result: STRING }, + /** A nested inspect inside a custom rendering (PerformanceEntry's + * `${name} ${inspect(toJSON)}`): nestedBegin starts a fresh inspect + * context, nestedEnd(prefix, inner) restores the caller's and answers + * `prefix inner` re-indented at the caller's level (+1). */ + "insp.nestedBegin": { argTypes: [], result: VOID }, + "insp.nestedEnd": { argTypes: [STRING, STRING], result: STRING }, + /** Around a top-level render that an inspection refusal can end: mark + * records the inspector's frame and nesting depth, unwind (in the + * render's finally) closes what the render left open above it. */ + "insp.mark": { argTypes: [], result: VOID }, + "insp.unwind": { argTypes: [], result: VOID }, + /** node:string_decoder's utf8 StringDecoder (scr_string.c): the decoder + * value is a one-field record whose f64 PACKS the pending partial + * sequence (count + up to 3 raw bytes — Node buffers at most 3 for + * every encoding); the frontend's interned %strdec helpers thread it, + * with the decoder's CANONICAL encoding name first, through these pure + * functions. write: (enc, pending, chunk) → the decoded complete + * prefix of pending+chunk (+1); next: (enc, pending, chunk) → the + * packed NEW pending; end: (enc, pending) → the buffered partial's + * flush (+1). Node-exact per encoding (oracle-pinned); none throws. */ + "strdec.write": { argTypes: [STRING, F64, BYTES_U8], result: STRING }, + "strdec.next": { argTypes: [STRING, F64, BYTES_U8], result: F64 }, + "strdec.end": { argTypes: [STRING, F64], result: STRING }, + /** A runtime encoding argument's canonical name (Node's normalizeEncoding: + * nullish and "" are utf8); an unknown name throws ERR_UNKNOWN_ENCODING. */ + "strdec.encoding": { argTypes: [DYN], result: STRING }, + // node:readline: the interface handle is f64; the callbacks' func types + // are program-dependent (the emitter picks the adapter by parameter + // count), so those slots are null like child.onExit's. + /** node:readline and node:readline/promises (scr_readline.c, linked + * under the events gate — these fns imply moduleUsesProcessEvents; a + * stream input or output adds scr_stream.c's bridge). The interface + * value is an f64 handle (the Timeout-id precedent); an open interface + * on stdin keeps the loop alive until close/EOF. + * rl.create: [mode, input?, output?, crlfDelay, prompt?] → f64, mode + * bits 1 stream input, 2 stream output, 4 stdout output, 16 prompt + * (lowerReadlineCreateCall); rl.createSignal: the same plus the + * trailing AbortSignal (mode bit 8), which links the event-target unit + * for addAbortListener. rl.question: [handle, query, cb] — the callback + * form; THROWS ERR_USE_AFTER_CLOSE on a closed interface (may-throw). + * rl.questionAsync(Signal): [handle, query, signal?] → promise, + * rejecting instead of throwing. rl.close/pause/resume/prompt: + * [handle] (pause/resume/prompt throw ERR_USE_AFTER_CLOSE when + * closed); rl.setPrompt/getPrompt; rl.write: [handle, data] (throws + * when closed). rl.on: [handle, event, cb, once] — the event number + * (0 'line', 1 'close', 2 'pause', 3 'resume', 4 'error') and the + * listener (moves). rl.iterOpen/iterNext/iterReturn: the async + * iterator — iterNext answers promise of the line or undefined. */ + "rl.create": { argTypes: [F64], result: F64 }, + "rl.createSignal": { argTypes: [F64], result: F64 }, + "rl.question": { argTypes: [F64, STRING, null], result: VOID }, + "rl.questionAsync": { argTypes: [F64, STRING], result: { kind: "promise", inner: STRING } }, + "rl.questionAsyncSignal": { + argTypes: [F64, STRING, DYN], + result: { kind: "promise", inner: STRING }, + }, + /** The question forms with a checked-dynamic query (a JavaScript value + * that need not be a string). */ + "rl.questionDyn": { argTypes: [F64, DYN, null], result: VOID }, + "rl.questionAsyncDyn": { argTypes: [F64, DYN], result: { kind: "promise", inner: STRING } }, + "rl.questionAsyncSignalDyn": { + argTypes: [F64, DYN, DYN], + result: { kind: "promise", inner: STRING }, + }, + "rl.close": { argTypes: [F64], result: VOID }, + "rl.pause": { argTypes: [F64], result: VOID }, + "rl.resume": { argTypes: [F64], result: VOID }, + "rl.prompt": { argTypes: [F64], result: VOID }, + /** prompt(preserveCursor): [handle, preserveCursor] */ + "rl.promptWith": { argTypes: [F64, BOOL], result: VOID }, + "rl.setPrompt": { argTypes: [F64, STRING], result: VOID }, + "rl.setPromptDyn": { argTypes: [F64, DYN], result: VOID }, + "rl.getPrompt": { argTypes: [F64], result: STRING }, + "rl.write": { argTypes: [F64, STRING], result: VOID }, + /** write(data, key): [handle, hasData, data, hasKey, hasName, name, + * ctrl, meta, shift] */ + "rl.writeKey": { + argTypes: [F64, BOOL, STRING, BOOL, BOOL, STRING, BOOL, BOOL, BOOL], + result: VOID, + }, + "rl.on": { argTypes: [F64, F64, null, BOOL], result: VOID }, + "rl.iterOpen": { argTypes: [F64], result: VOID }, + "rl.iterNext": { argTypes: [F64], result: { kind: "promise", inner: DYN } }, + "rl.iterReturn": { argTypes: [F64], result: VOID }, + /** createInterface's history and editing options (scr_rl_check_options: + * [flags, tabSize, escapeCodeTimeout, historySize], may throw; + * rl.setOptions: [handle, flags, historySize, removeHistoryDuplicates, + * tabSize, escapeCodeTimeout]; rl.setHistory: [handle, history]) and + * the member reads (rl.history answers the array, rl.numProp the + * numeric member by number). */ + "rl.checkOptions": { argTypes: [F64, F64, F64, F64], result: VOID }, + "rl.checkCompleter": { argTypes: [DYN], result: VOID }, + "rl.setOptions": { argTypes: [F64, F64, F64, BOOL, F64, F64], result: VOID }, + "rl.setHistory": { argTypes: [F64, arrayOf(STRING)], result: VOID }, + "rl.history": { argTypes: [F64], result: arrayOf(STRING) }, + "rl.numProp": { argTypes: [F64, F64], result: F64 }, + /** Terminal mode (scr_readline_tty.c): rl.terminal: [handle, mode (0 + * the option's value, 2 output.isTTY of process.stdout), terminal, + * completer (undefined when absent), readline/promises], emitted with + * scr_insp_dyn when there is a completer; the line, terminal and cursor + * members. */ + "rl.terminal": { argTypes: [F64, F64, BOOL, DYN, BOOL], result: VOID }, + "rl.line": { argTypes: [F64], result: STRING }, + "rl.isTerminal": { argTypes: [F64], result: BOOL }, + "rl.isPromises": { argTypes: [F64], result: BOOL }, + /** createInterface's input or output as a checked-dynamic value: + * [value, output] — the stream; refused when it is not one. */ + "rl.inputFromDyn": { + argTypes: [DYN, BOOL], + result: { kind: "object", className: "%Readable" }, + }, + "rl.outputFromDyn": { + argTypes: [DYN, BOOL], + result: { kind: "object", className: "%Writable" }, + }, + "rl.cursor": { argTypes: [F64], result: F64 }, + /** readline.cursorTo / moveCursor / clearLine / clearScreenDown: rl.seq + * [kind (0-3), the first and second numbers, whether the second is + * given] answers the data to write after the argument checks (may + * throw); rl.moves [dx, dy] says whether moveCursor writes. */ + "rl.seq": { argTypes: [F64, F64, F64, BOOL], result: STRING }, + "rl.moves": { argTypes: [F64, F64], result: BOOL }, + /** getCursorPos() (scr_readline_tty.c, linked through the regex unit + * like rl.terminal): [handle, 0 rows | 1 cols] */ + "rl.cursorPos": { argTypes: [F64, F64], result: F64 }, + /** readline.emitKeypressEvents (scr_readline_tty.c): rl.keypressStdin + * [interface (0: none), escapeCodeTimeout (NaN: the interface's or + * 500)], rl.keypressStream [stream, interface, timeout]. */ + "rl.keypressStdin": { argTypes: [F64, F64], result: VOID }, + "rl.keypressStream": { argTypes: [null, F64, F64], result: VOID }, + /** readline/promises Readline over an f64 handle: rl.uiOpen [fd (1 + * stdout, 2 stderr), autoCommit], rl.uiOpenStream [stream, autoCommit] + * (isWritable, may throw); the actions take [handle, ...numbers] (the + * checks may throw; cursorTo: [handle, x, whether y is given, y]); + * rl.uiCommit answers a promise. */ + "rl.uiOpen": { argTypes: [F64, BOOL], result: F64 }, + "rl.uiOpenStream": { argTypes: [null, BOOL], result: F64 }, + "rl.uiCursorTo": { argTypes: [F64, F64, BOOL, F64], result: VOID }, + "rl.uiMoveCursor": { argTypes: [F64, F64, F64], result: VOID }, + "rl.uiClearLine": { argTypes: [F64, F64], result: VOID }, + "rl.uiClearScreenDown": { argTypes: [F64], result: VOID }, + "rl.uiRollback": { argTypes: [F64], result: VOID }, + "rl.uiCommit": { argTypes: [F64], result: { kind: "promise", inner: VOID } }, + /** node:timers/promises — the promisified pair (scr_async.c, beside + * the timer heap they ride): tp.setTimeout: [ms] → a pending void + * promise a one-shot heap timer fulfills (the loop's timer phase, FIFO + * against equal deadlines like Node); tp.setImmediate: [] → the same + * through the immediate queue (fires before due timers of later loop + * turns, Node's check phase). Neither throws. */ + "tp.setTimeout": { argTypes: [F64], result: { kind: "promise", inner: VOID } }, + // scheduler.wait(delay): tp.setTimeout's promise. It throws Node's + // ERR_INVALID_THIS once mock.timers has restored the real scheduler.wait. + "tp.schedulerWait": { argTypes: [F64], result: { kind: "promise", inner: VOID } }, + "tp.setImmediate": { argTypes: [], result: { kind: "promise", inner: VOID } }, + /** events.once (scr_events_emitter.c): [emitter, name, boxer, + * errorBoxer, listenForError] → a pending promise of any[]; the boxers + * turn the event's and the 'error' tuple into that array. */ + "events.once": { argTypes: [null, STRING, null, null, BOOL], result: VOID }, + /** events.once(emitter, name, { signal }) (scr_events_once_signal.c): + * events.once's arguments plus the checked-dynamic signal (an + * AbortSignal or undefined). */ + "events.onceSignal": { argTypes: [null, STRING, null, null, BOOL, DYN], result: VOID }, + /** The resolve-value forms: [ms?, value, ref] → a pending promise of the + * value's type that copies the borrowed already-resolved `value` promise + * when the timer (or immediate) fires; ref false unrefs it. The result + * type follows the value promise. */ + "tp.setTimeoutValue": { argTypes: [F64, null, BOOL], result: VOID }, + "tp.setImmediateValue": { argTypes: [null, BOOL], result: VOID }, + /** The same with options.signal (a checked-dynamic AbortSignal or + * undefined) last: an abort clears the timer and rejects with an + * AbortError (scr_event_target.c). */ + "tp.setTimeoutSignal": { argTypes: [F64, null, BOOL, DYN], result: VOID }, + "tp.setImmediateSignal": { argTypes: [null, BOOL, DYN], result: VOID }, + /** scheduler.wait(delay, options): tp.setTimeoutValue's and + * tp.setTimeoutSignal's shapes, with tp.schedulerWait's throws. */ + "tp.schedulerWaitValue": { argTypes: [F64, null, BOOL], result: VOID }, + "tp.schedulerWaitSignal": { argTypes: [F64, null, BOOL, DYN], result: VOID }, + /** node:diagnostics_channel (scr_dc.c, linked when any dc.* appears — + * the zlib gating precedent): a process-global name→channel registry; + * channel values are f64 handles (type-mapper.ts maps Channel to F64, the + * readline.Interface pattern). Subscribers are dyn function values — + * identity-compared by unsubscribe, called (message, name) by publish + * over a SNAPSHOT of the list (a subscriber unsubscribing itself + * mid-publish still lets its siblings fire, Node's behavior). dc.publish + * MAY THROW: a subscriber's throw propagates out of publish (catchable + * there) where Node routes it to triggerUncaughtException — the + * documented divergence. subscribe/unsubscribe throw Node's + * ERR_INVALID_ARG_TYPE TypeError for non-function subscribers. */ + "dc.channel": { argTypes: [STRING], result: F64 }, + "dc.subscribe": { argTypes: [STRING, DYN], result: VOID }, + "dc.unsubscribe": { argTypes: [STRING, DYN], result: BOOL }, + "dc.hasSubscribers": { argTypes: [STRING], result: BOOL }, + "dc.publish": { argTypes: [F64, DYN], result: VOID }, + "dc.chanSubscribe": { argTypes: [F64, DYN], result: VOID }, + "dc.chanUnsubscribe": { argTypes: [F64, DYN], result: BOOL }, + "dc.chanHasSubscribers": { argTypes: [F64], result: BOOL }, + "dc.chanName": { argTypes: [F64], result: STRING }, + /** TracingChannel (dc.tracingChannel): a registry entry of the five + * event channels, an f64 handle like Channel (type-mapper.ts). tcSubscribe/ + * tcUnsubscribe walk a dyn handlers object's five event keys (truthy + * non-function slots throw the per-channel ERR_INVALID_ARG_TYPE); + * tcTraceSync/tcTraceCallback/tcTracePromise run Node's publish choreography in C over + * dyn values (fn, ctx, thisArg, args-array) with thisArg bound as the + * ambient receiver — the traced call's throw and any subscriber throw + * both propagate (MAY THROW). tcTraceCallback wraps args[position] in a + * native error/result + asyncStart/asyncEnd publisher and throws Node's + * TypeError when that slot is not callable. tracingChannelOf is the + * five-Channel collection form of the constructor. */ + /** setImmediate as a first-class dyn value (scr_async.c): a minted dyn + * callable scheduling args[0](args[1..]) on the immediate queue — the + * Node-suite traceCallback shape (`traceCallback(setImmediate, ...)`). + * Calling it validates the callback (the dyn call machinery); minting + * never throws. */ + "timers.setImmediateFnValue": { argTypes: [], result: DYN }, + /** `new Promise(setImmediate)` (the Node-suite early-exit shape): a + * fresh promise an immediate fulfills with the undefined dyn value — + * the executor IS setImmediate, so resolve rides the immediate queue + * (scr_async.c). Never throws. */ + "timers.immediatePromise": { argTypes: [], result: { kind: "promise", inner: DYN } }, + "dc.tracingChannel": { argTypes: [STRING], result: F64 }, + "dc.tracingChannelOf": { argTypes: [F64, F64, F64, F64, F64], result: F64 }, + "dc.tcChannel": { argTypes: [F64, F64], result: F64 }, + "dc.tcHasSubscribers": { argTypes: [F64], result: BOOL }, + "dc.tcSubscribe": { argTypes: [F64, DYN], result: VOID }, + "dc.tcUnsubscribe": { argTypes: [F64, DYN], result: BOOL }, + "dc.tcTraceSync": { argTypes: [F64, DYN, DYN, DYN, DYN], result: DYN }, + "dc.tcTraceCallback": { argTypes: [F64, DYN, F64, DYN, DYN, DYN], result: DYN }, + /** tracePromise (scr_dc.c): start publish, the traced call, a wrap of + * non-promise results, the end publish, and a REACTION FIBER that + * awaits the traced promise and publishes asyncStart/asyncEnd (error + * first on rejection) before settling the returned promise with + * the passed-through outcome. MAY THROW (the traced call and the + * synchronous publishes). */ + "dc.tcTracePromise": { + argTypes: [F64, DYN, DYN, DYN, DYN], + result: { kind: "promise", inner: DYN }, + }, + /** process.on/once('unhandledRejection', fn): registers a dyn listener + * the checkpoint report dispatches per never-observed rejection — + * (reason, promise) — instead of printing and exiting 1 (scr_async.c). + * The bool second arg is `once` (auto-removed after one delivery, + * Node's once); off/removeListener remove by closure identity, the + * offWarning stance. Throws Node's ERR_INVALID_ARG_TYPE on a + * non-function. */ + /** Uncaught exception handlers and monitors: [callback dyn, once, monitor]. */ + "process.onUncaughtException": { argTypes: [DYN, BOOL, BOOL], result: VOID }, + "process.offUncaughtException": { argTypes: [DYN, BOOL], result: VOID }, + "process.onUnhandledRejection": { argTypes: [DYN, BOOL], result: VOID }, + "process.offUnhandledRejection": { argTypes: [DYN], result: VOID }, + /** process.on/once/off('rejectionHandled', fn): the sibling registry. + * A handler attached after unhandledRejection delivery fires the event + * once. Dispatch is synchronous at the attach, with the promise, + * Node's payload. */ + "process.onRejectionHandled": { argTypes: [DYN, BOOL], result: VOID }, + "process.offRejectionHandled": { argTypes: [DYN], result: VOID }, + /** process warnings (scr_async_dyn.c): onWarning/offWarning register + * dyn listeners; emitWarning applies Node's full argument grammar over + * the call's dyn argument vector (ERR_INVALID_ARG_TYPE TypeErrors — MAY + * THROW) and emits on the next tick, like Node: Node's stderr report, + * then the listeners. */ + "process.onWarning": { argTypes: [DYN], result: VOID }, + "process.offWarning": { argTypes: [DYN], result: VOID }, + "process.emitWarning": { argTypes: [DYN], result: VOID }, + /** A Warning-typed process warning with this message (the console + * members' warnings), emitted on the next tick as Node's emitWarning + * does (so the loop runs); an empty message emits nothing. */ + "process.emitWarningText": { argTypes: [STRING], result: VOID }, + /** process.noDeprecation (0) / process.throwDeprecation (1): the stored + * value (undefined until assigned) and its assignment. util.deprecate's + * warnings and DeprecationWarning emissions read their truthiness. */ + "process.deprecationFlag": { argTypes: [F64], result: DYN }, + "process.setDeprecationFlag": { argTypes: [F64, DYN], result: VOID }, + "process.setEmitWarning": { argTypes: [DYN], result: VOID }, + /** AsyncLocalStorage (node:async_hooks — scr_async.c): stores are f64 + * handles; contexts are immutable fiber-carried snapshots (spawned + * fibers inherit the spawner's — Node's init-time capture). run/exit + * enter (or clear) the store, call the dyn function with forwarded + * arguments, and restore (the finally); getStore answers the current + * dyn value or undefined; enterWith installs with no restore point. + * run/exit MAY THROW (the callback's own throws propagate). */ + /** `await v` over a checked-dynamic VALUE (scr_async.c): a dyn promise + * adopts (rejections re-throw — MAY THROW), anything else takes JS's + * one-microtask non-thenable await and answers itself. Only emitted + * inside async bodies (the frontend's isAsync gate). */ + "async.awaitDyn": { argTypes: [DYN], result: DYN }, + /** The bare one-microtask hop (scr_await_hop): `await v` over a typed + * NON-promise value — JS awaits non-thenables through exactly one + * microtask turn and yields the value itself. Never throws. */ + "async.hop": { argTypes: [], result: VOID }, + "als.new": { argTypes: [], result: F64 }, + "als.get": { argTypes: [F64], result: DYN }, + "als.run": { argTypes: [F64, DYN, DYN, DYN], result: DYN }, + "als.exitRun": { argTypes: [F64, DYN, DYN], result: DYN }, + "als.enterWith": { argTypes: [F64, DYN], result: VOID }, + "als.disable": { argTypes: [F64], result: VOID }, + /** new AsyncLocalStorage(options) (Node's object check throws; MAY + * THROW) and the store's name option (dyn; "" without one). */ + "als.newOptions": { argTypes: [DYN], result: F64 }, + "als.name": { argTypes: [F64], result: DYN }, + /** A function bound to the active context: the completion callbacks of + * callback-style APIs (always false: an empty context leaves the + * callback unbound) and AsyncLocalStorage.bind (always true). The typed + * form answers its closure argument's type; the dyn form throws Node's + * ERR_INVALID_ARG_TYPE for a non-function when always (MAY THROW). */ + "als.bindCallback": { argTypes: [null, BOOL], result: VOID }, + "als.bindCallbackDyn": { argTypes: [DYN, BOOL], result: DYN }, + /** AsyncLocalStorage.snapshot(): a dyn function (cb, ...args) that runs + * cb in the context active at the snapshot. */ + "als.snapshot": { argTypes: [], result: DYN }, + /** Channel.bindStore/unbindStore/runStores (scr_dc.c): the + * AsyncLocalStorage integration — set-semantics bindings per store, + * runStores entering every bound store with transform(data) around the + * publish and the callback (MAY THROW: transforms, subscribers, and + * the callback all run). */ + "dc.chanBindStore": { argTypes: [F64, F64, DYN], result: VOID }, + "dc.chanUnbindStore": { argTypes: [F64, F64], result: BOOL }, + "dc.chanRunStores": { argTypes: [F64, DYN, DYN, DYN, DYN], result: DYN }, + /** The Number statics with a static C implementation (scr_lib.c): one + * f64 arg → bool, JS-exact BY CONSTRUCTION — Number.isFinite/isNaN/ + * isInteger/isSafeInteger never coerce, and the frontend routes only + * f64-typed arguments here (other static types fence). None throws. */ + "number.isFinite": { argTypes: [F64], result: BOOL }, + "number.isNaN": { argTypes: [F64], result: BOOL }, + "number.isInteger": { argTypes: [F64], result: BOOL }, + "number.isSafeInteger": { argTypes: [F64], result: BOOL }, + /** Date (scr_lib.c). Values are TimeClip'd epoch-millisecond scalars: + * construction/store/pass/getters are exact while identity and mutation + * remain fenced. date.now is Node's integer milliseconds since epoch; + * date.toISOString formats one raw f64 millisecond time value and + * date.toISOStringValue formats a stored Date, both with Node's rules (UTC, + * YYYY-MM-DDTHH:mm:ss.sssZ, expanded ±YYYYYY years outside 0–9999, + * ToInteger truncation of fractional ms) and THROWS Node's "Invalid + * time value" RangeError on NaN / out-of-range input (may-throw seed). + * Results: f64 / owned (+1) string. */ + "date.now": { argTypes: [], result: F64 }, + "date.newNow": { argTypes: [], result: DATE_T }, + "date.newMs": { argTypes: [F64], result: DATE_T }, + "date.newString": { argTypes: [STRING], result: DATE_T }, + /** Date.parse(dateString), with its own reach witness for exact fences. */ + "date.parse": { argTypes: [STRING], result: F64 }, + "date.getTime": { argTypes: [DATE_T], result: F64 }, + "date.valueOf": { argTypes: [DATE_T], result: F64 }, + "date.toISOString": { argTypes: [F64], result: STRING }, + "date.toISOStringValue": { argTypes: [DATE_T], result: STRING }, + /** Date.prototype.toUTCString over a stored Date ("Invalid Date" for + * NaN; never throws). */ + "date.toUTCString": { argTypes: [DATE_T], result: STRING }, + "date.getFullYear": { argTypes: [DATE_T], result: F64 }, + "date.getUTCFullYear": { argTypes: [DATE_T], result: F64 }, + "date.getMonth": { argTypes: [DATE_T], result: F64 }, + "date.getUTCMonth": { argTypes: [DATE_T], result: F64 }, + "date.getDate": { argTypes: [DATE_T], result: F64 }, + "date.getUTCDate": { argTypes: [DATE_T], result: F64 }, + "date.getDay": { argTypes: [DATE_T], result: F64 }, + "date.getUTCDay": { argTypes: [DATE_T], result: F64 }, + "date.getHours": { argTypes: [DATE_T], result: F64 }, + "date.getUTCHours": { argTypes: [DATE_T], result: F64 }, + "date.getMinutes": { argTypes: [DATE_T], result: F64 }, + "date.getUTCMinutes": { argTypes: [DATE_T], result: F64 }, + "date.getSeconds": { argTypes: [DATE_T], result: F64 }, + "date.getUTCSeconds": { argTypes: [DATE_T], result: F64 }, + "date.getMilliseconds": { argTypes: [DATE_T], result: F64 }, + "date.getUTCMilliseconds": { argTypes: [DATE_T], result: F64 }, + "date.getTimezoneOffset": { argTypes: [DATE_T], result: F64 }, + /** A Date object for a time value a local holds (date-values.ts): a + * lib-call argument passes the time value itself, and any other use + * creates the Date. */ + "date.fromTime": { argTypes: [F64], result: DATE_T }, + /** Date.prototype's field setters over a time value + * (scr_date_set_fields): time value, setter code, argument count and four + * arguments; answers the new time value. */ + "date.setFields": { argTypes: [F64, F64, F64, F64, F64, F64, F64], result: F64 }, + /** TimeClip of a number (setTime's argument). */ + "date.timeClip": { argTypes: [F64], result: F64 }, + /** Stores a time value into a Date object and answers it (a setter's + * write). */ + "date.store": { argTypes: [DATE_T, F64], result: F64 }, + /** Date.prototype.toString / toDateString / toTimeString and String(date): + * V8's local-time formats with the host zone's long name, "Invalid Date" + * for an invalid date. Owned (+1) string; never throws. */ + "date.toString": { argTypes: [DATE_T], result: STRING }, + "date.toDateString": { argTypes: [DATE_T], result: STRING }, + "date.toTimeString": { argTypes: [DATE_T], result: STRING }, + /** The composed `new Date(dateString).getTime()` read: one borrowed + * string, f64 milliseconds since epoch, with V8's date-string grammar + * (the ES5 format, then the legacy forms: RFC 1123/2822, Date#toString, + * "Jan 2, 2024", ...). Zone-less dates are local time; anything V8 + * rejects is NaN, Node's invalid-date getTime. Never throws. */ + "date.parseGetTime": { argTypes: [STRING], result: F64 }, + /** `Date.UTC(...)` — seven f64 arguments (the frontend completes the + * spec's defaults for omitted trailing parts: month 0, date 1, time + * parts 0), the spec's MakeDay/MakeTime/TimeClip exactly: 0–99 years + * map to 1900+year, out-of-range months/dates roll over, non-finite + * parts and out-of-range results answer NaN. Never throws. */ + "date.utc": { argTypes: [F64, F64, F64, F64, F64, F64, F64], result: F64 }, + /** `new Date(year, monthIndex[, date, hours, minutes, seconds, ms])` — + * the same seven-f64 completion as date.utc, but the fields are LOCAL + * time: V8's MakeDay/MakeTime, then UTC(t) in the host zone (DST gaps + * read with the offset before the transition, overlaps answer the + * earlier instant), then TimeClip. Never throws. */ + "date.newLocal": { argTypes: [F64, F64, F64, F64, F64, F64, F64], result: DATE_T }, + /** WHATWG TextDecoder.decode over u8 bytes (scr_bytes.c): utf-8 with + * default options — the same maximal-subpart replacement decode as + * Buffer.toString("utf8"), with the leading BOM stripped (the one + * behavioral difference; ignoreBOM defaults to false). Inline calls + * lower directly; stored decoder records dispatch by encoding id. + * TextEncoder.encode needs no libFn: its matching forms + * lower to buffer.fromStr(s, "utf8") (identical bytes — both write each + * lone surrogate as U+FFFD). Borrowed arg; owned (+1) string; never throws. */ + "text.decode": { argTypes: [BYTES_U8], result: STRING }, + "bytes.bufferSource": { argTypes: [DYN], result: BYTES_U8 }, + /** Whether an error-first callback's Buffer result is absent: the zlib + * and crypto callback adapters pass NULL after an error. Borrowed arg; + * never throws. */ + "bytes.isAbsent": { argTypes: [BYTES_U8], result: BOOL }, + /** Whether two typed arrays share no byte: their element ranges in + * memory do not overlap. Borrowed args of any element kind; never throws. */ + "bytes.disjoint": { argTypes: [null, null], result: BOOL }, + /** The absent Buffer result itself (NULL), where an error-path callback + * body passes its undefined result on into a Buffer slot. Owned (+1); + * never throws. */ + "bytes.absent": { argTypes: [], result: BYTES_U8 }, + /** node:crypto asymmetric keys and signatures (scr_pki.c): checked- + * dynamic arguments, borrowed; KeyObject, Sign and Verify handles, + * Buffers and booleans as checked-dynamic results (+1). Node's argument + * and decoder errors throw (may-throw); generateKeyPairCb's callback + * receives (null, publicKey, privateKey) in a later macrotask. */ + "crypto.createPrivateKey": { argTypes: [DYN], result: DYN }, + "crypto.createPublicKey": { argTypes: [DYN], result: DYN }, + "crypto.generateKeyPairSync": { argTypes: [DYN, DYN], result: DYN }, + "crypto.generateKeyPairCb": { argTypes: [DYN, DYN, DYN], result: VOID }, + "crypto.signOneshot": { argTypes: [DYN, DYN, DYN], result: DYN }, + "crypto.diffieHellman": { argTypes: [DYN, DYN], result: DYN }, + "crypto.verifyOneshot": { argTypes: [DYN, DYN, DYN, DYN], result: DYN }, + "crypto.createSign": { argTypes: [DYN], result: DYN }, + "crypto.createVerify": { argTypes: [DYN], result: DYN }, + /** The Buffer brand of a Uint8Array source onto a fresh Uint8Array + * result (map and filter of a Buffer return a Buffer). */ + "bytes.copyBrand": { argTypes: [BYTES_U8, BYTES_U8], result: VOID }, + "text.decodeBufferSource": { argTypes: [DYN], result: STRING }, + "text.decodeOptions": { argTypes: [BYTES_U8, BOOL, BOOL], result: STRING }, + "text.decodeStream": { argTypes: [DYN, BYTES_U8, F64, BOOL, BOOL, BOOL], result: STRING }, + "text.decodeLegacyOptions": { argTypes: [BYTES_U8, F64, BOOL, BOOL], result: STRING }, + /** TextDecoder.decode for a compile-time non-UTF-8 WHATWG label. The + * second f64 is the frontend-owned encoding id consumed by scr_bytes.c; + * label aliases/case/ASCII whitespace are canonicalized before IR. The + * mapping tables are generated from the pinned Node 24 oracle, keeping + * output portable without an ICU/iconv dependency. Borrowed args; owned + * (+1) string; never throws. */ + "text.decodeLegacy": { argTypes: [BYTES_U8, F64], result: STRING }, + "text.decoderEncoding": { argTypes: [DYN], result: F64 }, + "text.decoderName": { argTypes: [F64], result: STRING }, + "text.encodeInto": { argTypes: [DYN, DYN], result: DYN }, + /** `value instanceof TextDecoder` (true) or `TextEncoder` (false) on a + * checked-dynamic value: a codec record's handle box (scr_text_codec_dyn.c). + * Borrowed; never throws. */ + "text.codecIs": { argTypes: [DYN, BOOL], result: BOOL }, + "buffer.isAscii": { argTypes: [DYN], result: BOOL }, + "buffer.isUtf8": { argTypes: [DYN], result: BOOL }, + "buffer.transcode": { argTypes: [DYN, DYN, DYN], result: BYTES_U8 }, + /** The fs option forms and friends (scr_lib.c), all throwing catchably + * like the rest of sync fs. mkdirRecursiveSync is Node's recursive + * algorithm (try mkdir, EEXIST-dir is fine, ENOENT creates the parent + * first — errors report Node's errno at Node's path, EEXIST at a file + * target, ENOTDIR at the full path past a file); the first-created-dir + * return value has no lowering (statement position only, frontend- + * fenced). rmOptsSync is rmSync with (recursive, force) bools: force + * swallows ENOENT, recursive removes trees post-order, a directory + * without recursive throws Node's ERR_FS_EISDIR SystemError. mkdtempSync appends the six X's and returns the + * created path (+1). accessSync takes the F_OK/R_OK/W_OK/X_OK bits as + * one f64 (the frontend bakes fs.constants.* as literals and completes + * an omitted mode to 0). readFdSync/readFdSyncBytes are the + * readFileSync(fd[, "utf8"]) forms — a read(2) loop to EOF on the fd + * (the stdin path); errors carry Node's no-path message shape. */ + "fs.mkdirRecursiveSync": { argTypes: [STRING], result: VOID }, + "fs.rmOptsSync": { argTypes: [STRING, BOOL, BOOL], result: VOID }, + /** rmOptsSync's maxRetries/retryDelay form (path, recursive, force, + * maxRetries, retryDelay): Node's linear-backoff retry on + * EBUSY/EMFILE/ENFILE/ENOTEMPTY/EPERM — the tmpdir-harness shape + * rmSync(p, { maxRetries: 3, recursive: true, force: true }). Throws + * ERR_OUT_OF_RANGE for a retryDelay outside int32 >= 0 or a maxRetries + * outside uint32, before checking the path. */ + "fs.rmRetrySync": { argTypes: [STRING, BOOL, BOOL, F64, F64], result: VOID }, + "fs.mkdtempSync": { argTypes: [STRING], result: STRING }, + "fs.accessSync": { argTypes: [STRING, F64], result: VOID }, + "fs.readFdSync": { argTypes: [F64, STRING], result: STRING }, + "fs.readFdSyncBytes": { argTypes: [F64], result: BYTES_U8 }, + /** Validated isatty(3) probe. Stream property lowering maps false to + * undefined; tty.isatty exposes the boolean result directly. */ + "process.isTTY": { argTypes: [F64], result: BOOL }, + // Like process.envGet: the result is the module's interned + // `number | undefined` union — checked by arms in the libCall case. + /** Terminal width over an fd literal (1/2 — process.stdout/stderr + * .columns): ioctl(TIOCGWINSZ). The result is the module's interned + * `number | undefined` union — a non-TTY stream (or an ioctl refusal) + * yields the undefined arm, exactly Node's missing `.columns`. Never + * throws. */ + "process.columns": { argTypes: [F64], result: VOID }, + /** Terminal height, with the same fd and optional-number contract. */ + "process.rows": { argTypes: [F64], result: VOID }, + /** process.stdin.destroy(): a deliberate no-op (no stream machinery + * exists to tear down; documented in SEMANTICS.md). */ + "process.stdinDestroy": { argTypes: [], result: VOID }, + /** process.stdin.setRawMode(mode) — one borrowed bool arg, void. On a + * TTY stdin: termios raw mode on/off (libuv's UV_TTY_MODE_RAW flag set, + * the mode Node's setRawMode(true) applies; false restores the entry + * state). On a NON-TTY stdin Node's process.stdin has no such method at + * all, so the call throws Node's exact catchable TypeError + * ("process.stdin.setRawMode is not a function") — the may-throw seed + * carries it. */ + "process.stdinSetRawMode": { argTypes: [BOOL], result: VOID }, + // Arg 0 is a packed f64[] OR a bytes value (the spread-typed-array + // form) — checked in the libCall case. + /** fromCharCode takes one packed f64[] or bytes arg and builds a string + * from UTF-16 code units. Adjacent surrogate pairs combine; lone + * surrogates follow the runtime's replacement policy. */ + "string.fromCharCode": { argTypes: [null], result: STRING }, + /** Numeric code points, with catchable RangeError for non-integers or + * values outside 0..0x10ffff. Uses the same UTF-8 surrogate policy. */ + "string.fromCodePoint": { argTypes: [null], result: STRING }, + /** lastIndexOf returns the last UTF-16 start index, or -1. The two-arg + * form searches at or before its numeric position; NaN starts at the end. + * String arguments are borrowed and neither form throws. */ + "string.lastIndexOf": { argTypes: [STRING, STRING], result: F64 }, + "string.lastIndexOfFrom": { argTypes: [STRING, STRING, F64], result: F64 }, + /** String.raw(template, ...subs): the raw literals array (a string[] + * read off the template record) interleaved with the PRE-STRINGIFIED + * substitutions (the frontend applies the static ToString and packs + * them into one string[] literal) — extra substitutions drop, missing + * ones skip, the spec's loop exactly (scr_array.c). Borrowed args; + * +1 string; never throws. */ + "string.raw": { argTypes: [arrayOf(STRING), arrayOf(STRING)], result: STRING }, + }); +} + +let libFnSignatureTable: ReturnType | undefined; + +/** The runtime call slots of every native builtin, keyed by name. The table + * is built on the first call, so runs that neither lower nor validate a + * module do not construct it. */ +export function libFnSignatures(): ReturnType { + libFnSignatureTable ??= buildLibFnSignatures(); + return libFnSignatureTable; +} /** A builtin name exists exactly when its runtime signature exists. */ -export type IrLibFn = keyof typeof LIB_FN_SIGS; +export type IrLibFn = keyof ReturnType; /** The function-stage runs of a pipeline of `total` stages that a stream * follows (positions bit j: stage j is a function), and an async * generator destination's run (flags bits 1-2 = 1, pumped into the - * PassThrough pipeline() answers): each pumps its last generator through - * one (next, read, return) triple. */ + * PassThrough pipeline() answers, or 3, an untyped destination whose + * result may be pumped there): each pumps its last generator through one + * (next, read, return) triple. */ export function pipelinePumpedRuns(total: number, positions: number, flags: number): number { - let runs = ((flags >> 1) & 3) === 1 ? 1 : 0; + const dest = (flags >> 1) & 3; + let runs = dest === 1 || dest === 3 ? 1 : 0; for (let j = 0; j + 1 < total; j++) { if (Math.floor(positions / 2 ** j) % 2 === 1 && Math.floor(positions / 2 ** (j + 1)) % 2 === 0) runs++; diff --git a/packages/compiler/src/ir/callback-specialization.ts b/packages/compiler/src/ir/callback-specialization.ts index 6038ccaa5..ba579061c 100644 --- a/packages/compiler/src/ir/callback-specialization.ts +++ b/packages/compiler/src/ir/callback-specialization.ts @@ -16,7 +16,10 @@ const COPIED_NODE_FRACTION = 0.25; * closure it creates writes through its environment (and transitively the * closures those create). A parameter or capture absent from its function's * set holds the value it entered with for the whole call. */ -export function writtenLocals(mod: IrModule, nodes?: IrNodeIndex): Map> { +export function writtenLocals( + mod: { readonly functions: readonly IrFunction[] }, + nodes?: IrNodeIndex, +): Map> { return rebindings(mod, nodes).written; } @@ -30,29 +33,55 @@ const REBINDING_KINDS: readonly IrNodeKind[] = [ "closure", ]; +/** The rebindings one function's body makes: locals it writes after + * declaring them, and the closures it creates over captured bindings. */ +interface FunctionRebindings { + ids: Set; + sites: { owner: string; fnName: string; captures: string[] }[]; +} + +/** Records `node`'s rebinding into `into` (owner `fnName`). */ +function noteRebinding(fnName: string, node: IrExpr | IrStmt, into: FunctionRebindings): void { + if (node.kind === "assign" || node.kind === "forOf") into.ids.add(node.localId); + else if (node.kind === "tryCatch" && node.catchLocalId !== null) into.ids.add(node.catchLocalId); + else if (node.kind === "assignExpr" || node.kind === "incDec") into.ids.add(node.localId); + else if (node.kind === "closure" && node.captures.length) + into.sites.push({ owner: fnName, fnName: node.fnName, captures: node.captures }); +} + function rebindings( - mod: IrModule, + mod: { readonly functions: readonly IrFunction[] }, nodes?: IrNodeIndex, ): { written: Map>; /** Bindings of each function that a closure it creates writes. */ closureWritten: Map>; } { + const scan = nodes?.scan(REBINDING_KINDS); + const each = mod.functions.map((fn) => { + const own: FunctionRebindings = { ids: new Set(), sites: [] }; + eachNodeOfKinds(fn, scan, (node) => noteRebinding(fn.name, node, own)); + return own; + }); + return closeRebindings(mod, each); +} + +/** The module's rebindings from each function's own (`each`, in module + * order): + * a binding a closure writes through its environment is written by the + * function that created the closure, transitively. */ +function closeRebindings( + mod: { readonly functions: readonly IrFunction[] }, + each: readonly FunctionRebindings[], +): { written: Map>; closureWritten: Map> } { const written = new Map>(); const closureWritten = new Map>(); const sites: { owner: string; fnName: string; captures: string[] }[] = []; - const scan = nodes?.scan(REBINDING_KINDS); - for (const fn of mod.functions) { - const ids = new Set(); - eachNodeOfKinds(fn, scan, (node) => { - if (node.kind === "assign" || node.kind === "forOf") ids.add(node.localId); - else if (node.kind === "tryCatch" && node.catchLocalId !== null) ids.add(node.catchLocalId); - else if (node.kind === "assignExpr" || node.kind === "incDec") ids.add(node.localId); - else if (node.kind === "closure" && node.captures.length) - sites.push({ owner: fn.name, fnName: node.fnName, captures: node.captures }); - }); - written.set(fn.name, ids); - } + mod.functions.forEach((fn, index) => { + const own = each[index]!; + written.set(fn.name, own.ids); + for (const site of own.sites) sites.push(site); + }); const functions = new Map(mod.functions.map((fn) => [fn.name, fn])); for (let changed = true; changed;) { changed = false; @@ -127,13 +156,11 @@ export function specializeCallbackCalls( symbolName: (name: string) => string = (name) => name, ): IrModule { const functions = new Map(mod.functions.map((fn) => [fn.name, fn])); - const { written, closureWritten } = rebindings(mod); const copies = new Map(); - const candidates = new Map(); - let totalNodes = 0; + // Helpers that may take callbacks: their callback parameters are final + // once the walk below finds which of them the body rebinds. + const helpers = new Map(); for (const fn of mod.functions) { - const nodes = countNodes(fn); - totalNodes += nodes; if ( !(fn.ownsPrototype || fn.name.startsWith("%arr.")) || fn.name === mod.entry || @@ -143,9 +170,80 @@ export function specializeCallbackCalls( fn.classCaptures?.length || fn.fastRead !== undefined || fn.speculativeDispatch || - nodes > MAX_FUNCTION_NODES + !fn.params.some((param) => param.type.kind === "func") ) continue; + helpers.set(fn.name, fn); + } + if (!helpers.size) return mod; + + // Function values, exports, entry points and methods: an original + // reachable this way stays even when every direct call moves to a copy. + const referenced = new Set(moduleFunctionReferences(mod)); + for (const cls of mod.classes ?? []) + for (const method of ["constructor", ...(cls.methods ?? [])]) + referenced.add(`%${cls.name}.${method}`); + const globalIds = new Set(mod.globals?.map((global) => global.id)); + const globalWrites = new Map(); + // One walk of every body records what the steps below read: its node + // count (countNodes), its rebindings, the values written into each of its + // bindings, and its direct calls of helpers, in walk order. + const each = mod.functions.map((fn) => { + let nodes = 0; + const own: FunctionRebindings = { ids: new Set(), sites: [] }; + const values = new Map(); + const counts = new Map(); + const calls: (IrExpr & { kind: "call" })[] = []; + const record = (id: string, value: IrExpr | null, weight: number): void => { + counts.set(id, (counts.get(id) ?? 0) + weight); + if (value) { + const list = values.get(id); + if (list === undefined) values.set(id, [value]); + else list.push(value); + } + }; + everyStmtList(fn.body, { + stmt: (stmt) => { + nodes++; + noteRebinding(fn.name, stmt, own); + if (stmt.kind === "varDecl") record(stmt.localId, stmt.init, stmt.init ? 1 : 0); + if (stmt.kind === "assign") { + if (globalIds.has(stmt.localId)) { + const list = globalWrites.get(stmt.localId); + if (list === undefined) globalWrites.set(stmt.localId, [stmt.value]); + else list.push(stmt.value); + } + record(stmt.localId, stmt.value, 1); + } + if (stmt.kind === "forOf") record(stmt.localId, null, 2); + if (stmt.kind === "tryCatch" && stmt.catchLocalId !== null) + record(stmt.catchLocalId, null, 2); + return true; + }, + expr: (expr) => { + nodes++; + noteRebinding(fn.name, expr, own); + if (expr.kind === "assignExpr" || expr.kind === "incDec") { + record(expr.localId, null, 2); + if (globalIds.has(expr.localId)) globalWrites.set(expr.localId, []); + } + if (expr.kind === "call" && helpers.has(expr.callee)) calls.push(expr); + const name = exprFunctionReference(expr); + if (name !== undefined) referenced.add(name); + return true; + }, + }); + return { fn, nodes: Math.min(nodes, MAX_FUNCTION_NODES + 1), own, values, counts, calls }; + }); + const { written, closureWritten } = closeRebindings( + mod, + each.map((entry) => entry.own), + ); + const nodesOf = new Map(each.map((entry) => [entry.fn.name, entry.nodes])); + const candidates = new Map(); + for (const fn of helpers.values()) { + const nodes = nodesOf.get(fn.name)!; + if (nodes > MAX_FUNCTION_NODES) continue; const rebound = written.get(fn.name)!; const slots: number[] = []; fn.params.forEach((param, index) => { @@ -155,46 +253,11 @@ export function specializeCallbackCalls( } if (!candidates.size) return mod; - // Function values, exports, entry points and methods: an original - // reachable this way stays even when every direct call moves to a copy. - const referenced = new Set(moduleFunctionReferences(mod)); - for (const cls of mod.classes ?? []) - for (const method of ["constructor", ...(cls.methods ?? [])]) - referenced.add(`%${cls.name}.${method}`); // Single definitions of function-typed locals and globals. const definitions = new Map>(); - const globalIds = new Set(mod.globals?.map((global) => global.id)); - const globalWrites = new Map(); - for (const fn of mod.functions) { - const values = new Map(); - const counts = new Map(); - const record = (id: string, value: IrExpr | null, weight: number): void => { - counts.set(id, (counts.get(id) ?? 0) + weight); - if (value) values.set(id, [...(values.get(id) ?? []), value]); - }; - everyStmtList(fn.body, { - stmt: (stmt) => { - if (stmt.kind === "varDecl") record(stmt.localId, stmt.init, stmt.init ? 1 : 0); - if (stmt.kind === "assign") { - if (globalIds.has(stmt.localId)) - globalWrites.set(stmt.localId, [...(globalWrites.get(stmt.localId) ?? []), stmt.value]); - record(stmt.localId, stmt.value, 1); - } - if (stmt.kind === "forOf") record(stmt.localId, null, 2); - if (stmt.kind === "tryCatch" && stmt.catchLocalId !== null) - record(stmt.catchLocalId, null, 2); - return true; - }, - expr: (expr) => { - if (expr.kind === "assignExpr" || expr.kind === "incDec") { - record(expr.localId, null, 2); - if (globalIds.has(expr.localId)) globalWrites.set(expr.localId, []); - } - const name = exprFunctionReference(expr); - if (name !== undefined) referenced.add(name); - return true; - }, - }); + // The sum of countNodes over every function. + let totalNodes = 0; + for (const { fn, nodes, values, counts } of each) { const single = new Map(); const locals = new Set(fn.locals.map((local) => local.id)); const shared = closureWritten.get(fn.name); @@ -208,6 +271,7 @@ export function specializeCallbackCalls( ) single.set(id, list[0]!); definitions.set(fn.name, single); + totalNodes += nodes; } const globalDefinitions = new Map(); for (const global of mod.globals ?? []) @@ -274,23 +338,18 @@ export function specializeCallbackCalls( // unknown callbacks or function-value entries. const keysOf = new Map>(); const unknown = new Set(); - for (const fn of mod.functions) - everyStmtList(fn.body, { - stmt: () => true, - expr: (expr) => { - if (expr.kind !== "call") return true; - const candidate = candidates.get(expr.callee); - if (!candidate) return true; - const key = callKey(fn, new Map(), expr, candidate); - if (key === "") unknown.add(expr.callee); - else if (key !== null) { - const keys = keysOf.get(expr.callee) ?? new Set(); - keys.add(key); - keysOf.set(expr.callee, keys); - } - return true; - }, - }); + for (const { fn, calls } of each) + for (const call of calls) { + const candidate = candidates.get(call.callee); + if (!candidate) continue; + const key = callKey(fn, new Map(), call, candidate); + if (key === "") unknown.add(call.callee); + else if (key !== null) { + const keys = keysOf.get(call.callee) ?? new Set(); + keys.add(key); + keysOf.set(call.callee, keys); + } + } const specialized = new Set(); for (const [name, keys] of keysOf) if (keys.size > 1 || unknown.has(name) || referenced.has(name)) specialized.add(name); @@ -405,12 +464,9 @@ export function specializeCallbackCalls( return owner.body.map(stmt); } - const functionsOut = mod.functions.map((fn) => { + const functionsOut = each.map(({ fn, calls }) => { // Most bodies call no specialized original; keep their nodes. - const untouched = everyStmtList(fn.body, { - stmt: () => true, - expr: (expr) => expr.kind !== "call" || !specialized.has(expr.callee), - }); + const untouched = calls.every((call) => !specialized.has(call.callee)); return untouched ? fn : { ...fn, body: rewriteBody(fn, new Map(), new Set(), false) }; }); for (let i = 0; i < pending.length; i++) { @@ -423,19 +479,29 @@ export function specializeCallbackCalls( // Drop originals no remaining code reaches: every direct call moved to a // copy and nothing names them as a value. const callers = new Map>(); - for (const fn of functionsOut) + const call = (callee: string, caller: string): void => { + if (!specialized.has(callee)) return; + const set = callers.get(callee) ?? new Set(); + set.add(caller); + callers.set(callee, set); + }; + functionsOut.forEach((fn, index) => { + // A body the rewrite kept has the calls the walk recorded, and its + // closures are already in `referenced`. + const kept = each[index]; + if (kept !== undefined && kept.fn === fn) { + for (const site of kept.calls) call(site.callee, fn.name); + return; + } everyStmtList(fn.body, { stmt: () => true, expr: (expr) => { - if (expr.kind === "call" && specialized.has(expr.callee)) { - const set = callers.get(expr.callee) ?? new Set(); - set.add(fn.name); - callers.set(expr.callee, set); - } + if (expr.kind === "call") call(expr.callee, fn.name); if (expr.kind === "closure" && specialized.has(expr.fnName)) referenced.add(expr.fnName); return true; }, }); + }); const live = new Set(); const work: string[] = []; for (const fn of functionsOut) diff --git a/packages/compiler/src/ir/capture-values.ts b/packages/compiler/src/ir/capture-values.ts index 779ca1b76..19ec41363 100644 --- a/packages/compiler/src/ir/capture-values.ts +++ b/packages/compiler/src/ir/capture-values.ts @@ -86,7 +86,12 @@ export function copyStableCaptures(mod: IrModule): IrModule { // A closure body whose every use became a direct call is only reachable // through its copy now. const referenced = new Set(moduleFunctionReferences(mod)); - for (const fn of functions) + functions.forEach((fn, index) => { + // A body the rewrite kept names what the flow recorded of it. + if (fn === mod.functions[index]) { + for (const name of flow.references(fn)) referenced.add(name); + return; + } everyStmtList(fn.body, { stmt: () => true, expr: (e) => { @@ -95,6 +100,7 @@ export function copyStableCaptures(mod: IrModule): IrModule { return true; }, }); + }); const clones = flow.directClones(); return { ...mod, @@ -251,6 +257,9 @@ interface FunctionFacts { captureIndex: Map; classCaptures: Set; params: Set; + /** Functions the body names: direct call callees and function values + * (exprFunctionReference). */ + references: Set; } interface CreationSite { @@ -273,17 +282,26 @@ export class CaptureFlow { /** Environment-free bodies called directly, and their copies' names. */ private readonly clones = new Map(); - constructor(functions: readonly IrFunction[]) { + /** With `scan` false, each body's facts are collected when a question + * first needs them, and only unchanged() may be asked: the other + * questions read creation sites, which come from scanning every body. */ + constructor(functions: readonly IrFunction[], scan = true) { for (const fn of functions) this.functions.set(fn.name, fn); this.receivers = new AmbientReceiverReaders(this.functions); // Creation sites are collected while scanning each body. - for (const fn of functions) this.factsOf(fn); + if (scan) for (const fn of functions) this.factsOf(fn); } directClones(): ReadonlyMap { return this.clones; } + /** The functions the body of `fn`, one of the functions this flow was + * made from, names (FunctionFacts.references). */ + references(fn: IrFunction): ReadonlySet { + return this.factsOf(fn).references; + } + /** A lifted body that captures nothing and can be entered like a declared * function: synchronous, never naming its own closure, and unable to * observe the ambient receiver a closure call would bracket. */ @@ -459,6 +477,7 @@ export class CaptureFlow { captureIndex: new Map(), classCaptures: new Set((fn.classCaptures ?? []).map((c) => c.localId)), params: new Set(fn.params.map((p) => p.localId)), + references: new Set(), }; (fn.captures ?? []).forEach((c, index) => { if (!facts.captureIndex.has(c.localId)) facts.captureIndex.set(c.localId, index); @@ -511,6 +530,8 @@ export class CaptureFlow { facts.writes.set(id, (facts.writes.get(id) ?? 0) + 1); }; const expr = (e: IrExpr): boolean => { + const referenced = e.kind === "call" ? e.callee : exprFunctionReference(e); + if (referenced !== undefined) facts.references.add(referenced); switch (e.kind) { case "varRef": facts.reads.set(e.localId, (facts.reads.get(e.localId) ?? 0) + 1); @@ -538,7 +559,7 @@ export class CaptureFlow { facts.callees.add(e.callee.localId); break; } - return everyExprChild(e, expr, (s) => stmt(s, false)); + return everyExprChild(e, expr, nested); }; // `split` mirrors rewrite(): only statement lists reached through // ordinary nesting from the body are expanded. Switch cases share one @@ -599,8 +620,10 @@ export class CaptureFlow { list(s.body, split) ); } - return everyStmtChild(s, expr, (child) => stmt(child, false)); + return everyStmtChild(s, expr, nested); }; + // Statements nested in expressions and in other statements' heads. + const nested = (s: IrStmt): boolean => stmt(s, false); list(fn.body, true); return facts; } @@ -705,8 +728,11 @@ export class CaptureFlow { const splits = new Map(); const entry: IrStmt[] = []; const locals: IrLocal[] = []; - const used = new Set(fn.locals.map((l) => l.id)); + // The local ids a copy must avoid, collected for the first copy: most + // functions make none. + let used: Set | null = null; const fresh = (local: IrLocal): IrLocal => { + used ??= new Set(fn.locals.map((l) => l.id)); let k = 0; while (used.has(`${local.name}.${k}`)) k++; const id = `${local.name}.${k}`; diff --git a/packages/compiler/src/ir/char-unit-locals.test.ts b/packages/compiler/src/ir/char-unit-locals.test.ts index ad16ecebd..a075f5a25 100644 --- a/packages/compiler/src/ir/char-unit-locals.test.ts +++ b/packages/compiler/src/ir/char-unit-locals.test.ts @@ -4,6 +4,7 @@ import { join } from "node:path"; import { expect, test } from "vitest"; import { compile, deserializeModule } from "../index.js"; import { holdCharLocalsAsUnits } from "./char-unit-locals.js"; +import { validateModule } from "./validate.js"; import type { IrFunction, IrModule } from "./ir.js"; import { everyStmtList } from "./traverse.js"; @@ -96,3 +97,121 @@ console.log(escapes("a b", 1), rebound(" x", 0), wide("ab", 0), against("a", "b expect(fn(after, name)).toBe(fn(before, name)); } }); + +/** The union and dynamic comparisons left in the function. */ +function unionTests(f: IrFunction): string[] { + const out: string[] = []; + everyStmtList(f.body, { + stmt: () => true, + expr: (e) => { + if (e.kind === "unionEq" || e.kind === "unionIsTag") out.push(e.kind); + if (e.kind === "libCall" && e.fn === "js.compare") out.push(e.fn); + if (e.kind === "strIntrinsic") out.push(e.method); + return true; + }, + }); + return out; +} + +test("optional character reads compare their code unit", async () => { + const mod = holdCharLocalsAsUnits( + await lower(` +function scan(src: string, pos: number): number { + let n = 0; + if (src[pos] === " ") n += 1; + if (src[pos] !== "x") n += 2; + if (src[pos] < "a" || "z" <= src[pos]) n += 4; + if (src[pos + 1] === "=") n += 8; + const c = src[pos]; + if (c === undefined) n += 16; + if (c !== undefined && c > "m") n += 32; + switch (c) { + case "q": + n += 64; + break; + case "r": + n += 128; + } + return n; +} +console.log(scan("a b", 1), scan("q", 0), scan("ab", 5), scan("r=", 0.5)); +`), + ); + const scan = fn(mod, "scan"); + // Only charCodeAt reads remain: no union tests, dynamic compares, charAt + // or length reads. + expect(new Set(unionTests(scan))).toEqual(new Set(["charCodeAt"])); + expect(scan.locals.find((l) => l.name === "c")!.type).toEqual({ kind: "f64" }); +}); + +test("optional character reads with other uses stay reads", async () => { + const before = await lower(` +function kept(src: string, pos: number): string { + const c = src[pos]; + if (c === " ") return "space"; + return c ?? "end"; +} +function longer(src: string, pos: number): boolean { + return src[pos] === "ab"; +} +function twice(src: string, pos: number): boolean { + return src[pos * 2] === undefined; +} +console.log(kept("a b", 1), kept("ab", 0), kept("", 3), longer("ab", 0), twice("ab", 1)); +`); + const after = holdCharLocalsAsUnits(before); + for (const name of ["kept", "longer", "twice"]) { + expect(fn(before, name)).toBeDefined(); + expect(unionTests(fn(after, name))).toEqual(unionTests(fn(before, name))); + } + // The local's own test still reads it as a union. + expect(unionTests(fn(after, "kept"))).toContain("unionEq"); +}); + +test("a held local tested inside another read's index or receiver becomes its unit", async () => { + const mod = holdCharLocalsAsUnits( + await lower(` +function count(src: string, other: string): number { + let n = 0; + for (let pos = 0; pos <= src.length; pos += 1.5) { + const c = src[pos]; + if (src[c === "a" ? pos + 1 : pos] === "b") n++; + if ((c === "x" ? other : src)[pos] === "y") n += 2; + const d = src[c === "c" ? pos - 1 : pos]; + if (d === "c") n += 10; + } + return n; +} +console.log(count("abcab", "xyz"), count("cab", "yy"), count("", "")); +`), + ); + const count = fn(mod, "count"); + expect(validateModule(mod)).toEqual([]); + expect(count.locals.find((l) => l.name === "c")!.type).toEqual({ kind: "f64" }); + expect(count.locals.find((l) => l.name === "d")!.type).toEqual({ kind: "f64" }); + // No test of either local reads it as a union. + expect(unionTests(count)).not.toContain("unionEq"); +}); + +test("charAt tests in place compare charCodeAt", async () => { + const mod = holdCharLocalsAsUnits( + await lower(` +function tests(s: string, i: number): number { + let n = 0; + if (s.charAt(i) === " ") n += 1; + if ("a" <= s.charAt(i) && s.charAt(i) < "n") n += 2; + if (s.charAt(i) === "ab") n += 4; + return n; +} +console.log(tests("a b", 1), tests("abc", 0), tests("", 2)); +`), + ); + // The two-unit literal keeps its string comparison. + expect(reads(fn(mod, "tests"))).toEqual([ + "charCodeAt", + "charCodeAt", + "charCodeAt", + "strEq", + "charAt", + ]); +}); diff --git a/packages/compiler/src/ir/char-unit-locals.ts b/packages/compiler/src/ir/char-unit-locals.ts index beeeffaf3..be1eae2c3 100644 --- a/packages/compiler/src/ir/char-unit-locals.ts +++ b/packages/compiler/src/ir/char-unit-locals.ts @@ -8,11 +8,15 @@ import { type IrStmt, } from "./ir.js"; import { mapExprChildren, mapStmtChildren } from "./traverse.js"; +import { type IrFieldWalker, walkStmtFields } from "./fields.generated.js"; type CharAt = IrExpr & { kind: "strIntrinsic" }; type UnitUse = IrExpr & { kind: "strEq" | "strCmp" }; +type CompareOp = "<" | "<=" | ">" | ">="; -/** Character locals held as their code unit. +const FLIPPED = { "<": ">", "<=": ">=", ">": "<", ">=": "<=" } as const; + +/** Character reads and locals held as their code unit. * * `const c = s[i]` (or `s.charAt(i)`) followed only by tests such as * `c === " "`, `c >= "a" && c <= "z"` or `switch (c) { case "x": ... }` @@ -28,7 +32,17 @@ type UnitUse = IrExpr & { kind: "strEq" | "strCmp" }; * * Only immutable unboxed string locals qualify, with one declaration whose * initializer is a one-argument charAt, and with no other mention of the - * local. */ + * local. The same tests of `s.charAt(i)` in place compare + * `s.charCodeAt(i)`, so a loop version sees one kind of read. + * + * `s[i]` with an index not proven in bounds reads the character or + * undefined (an optional read, see `optionalCharRead`). Its tests against + * one-unit literals compare `i === Math.floor(i) ? s.charCodeAt(i) : NaN` + * instead: undefined, like NaN, equals no literal and fails every ordered + * comparison, and charCodeAt answers NaN out of range. That holds both for + * the read in place and for an immutable local initialized from it and + * used only in such tests and in tests for undefined, which become NaN + * tests. */ export function holdCharLocalsAsUnits(mod: IrModule): IrModule { let changed = false; const functions = mod.functions.map((fn) => { @@ -39,11 +53,277 @@ export function holdCharLocalsAsUnits(mod: IrModule): IrModule { return changed ? { ...mod, functions } : mod; } +/** The read of `s[i]` for an index not proven in bounds: temps for the + * string and the index, then the string's charAt when the index is an + * integer in [0, length) and undefined otherwise. */ +interface OptionalCharRead { + /** The temps' declarations, and whether both operands are plain (local + * reads and literals), which the unit read may evaluate in place. */ + temps: [IrStmt & { kind: "varDecl" }, IrStmt & { kind: "varDecl" }]; + plain: boolean; + receiver: IrExpr; + index: IrExpr; + unionId: string; + stringTag: number; + undefinedTag: number; +} + +function optionalCharRead(e: IrExpr): OptionalCharRead | null { + if (e.kind !== "seqExpr" || e.stmts.length !== 2 || e.diverges) return null; + const [str, idx] = e.stmts; + if (str?.kind !== "varDecl" || idx?.kind !== "varDecl" || !str.init || !idx.init) return null; + const isStr = (x: IrExpr): boolean => x.kind === "varRef" && x.localId === str.localId; + const isIdx = (x: IrExpr): boolean => x.kind === "varRef" && x.localId === idx.localId; + const r = e.result; + if (r.kind !== "ternary") return null; + const c = r.cond; + if (c.kind !== "logical" || c.op !== "&&" || c.left.kind !== "logical" || c.left.op !== "&&") + return null; + const [low, high, whole] = [c.left.left, c.left.right, c.right]; + if ( + !( + low.kind === "bin" && + low.op === ">=" && + isIdx(low.left) && + low.right.kind === "numLit" && + low.right.value === 0 + ) || + !( + high.kind === "bin" && + high.op === "<" && + isIdx(high.left) && + high.right.kind === "strIntrinsic" && + high.right.method === "length" && + isStr(high.right.receiver) + ) || + !( + whole.kind === "bin" && + whole.op === "===" && + isIdx(whole.left) && + whole.right.kind === "libCall" && + whole.right.fn === "math.floor" && + whole.right.args.length === 1 && + isIdx(whole.right.args[0]!) + ) + ) + return null; + const [present, absent] = [r.then, r.else_]; + if (present.kind !== "unionWrap" || absent.kind !== "unionWrap") return null; + if (present.unionId !== absent.unionId) return null; + const read = present.value; + if ( + read.kind !== "strIntrinsic" || + read.method !== "charAt" || + !isStr(read.receiver) || + read.args.length !== 1 || + !isIdx(read.args[0]!) + ) + return null; + if (absent.value.kind !== "unitLit" || absent.value.unit !== "undefined") return null; + return { + temps: [str, idx], + plain: plainOperand(str.init) && plainOperand(idx.init), + receiver: str.init, + index: idx.init, + unionId: present.unionId, + stringTag: present.tag, + undefinedTag: absent.tag, + }; +} + +function plainOperand(e: IrExpr): boolean { + return e.kind === "varRef" || e.kind === "numLit" || e.kind === "strLit"; +} + +/** `i === Math.floor(i) ? s.charCodeAt(i) : NaN`: the code unit of an + * optional read, or NaN where it reads undefined. Plain operands are read + * in place; others keep the read's temps, so each is evaluated once. */ +function unitRead(read: OptionalCharRead, loc: IrExpr["loc"]): IrExpr { + if (!read.plain) { + const [str, idx] = read.temps; + const temp = (decl: IrStmt & { kind: "varDecl" }, type: IrExpr["type"]): IrExpr => ({ + kind: "varRef", + localId: decl.localId, + type, + loc, + }); + return { + kind: "seqExpr", + stmts: [{ ...str }, { ...idx }], + result: unitOf(temp(str, read.receiver.type), () => temp(idx, F64), loc), + type: F64, + loc, + }; + } + return unitOf({ ...read.receiver }, () => ({ ...read.index }), loc); +} + +function unitOf(receiver: IrExpr, index: () => IrExpr, loc: IrExpr["loc"]): IrExpr { + return { + kind: "ternary", + cond: { + kind: "bin", + op: "===", + left: index(), + right: { kind: "libCall", fn: "math.floor", args: [index()], type: F64, loc }, + type: BOOL, + loc, + }, + then: { + kind: "strIntrinsic", + method: "charCodeAt", + receiver, + args: [index()], + type: F64, + loc, + }, + else_: { kind: "numLit", value: NaN, type: F64, loc }, + type: F64, + loc, + }; +} + +/** A test of an operand that reads one character or undefined, against a + * literal of one code unit or for undefined, and the same test on the + * operand's code unit. `tagsOf` gives the operand's union tags. */ +interface UnitTest { + operand: IrExpr; + /** The test reads the unit twice. */ + twice?: boolean; + test: (unit: () => IrExpr) => IrExpr; +} + +function optionalUnitTest( + e: IrExpr, + tagsOf: (operand: IrExpr) => OptionalCharRead | null, +): UnitTest | null { + const loc = e.loc; + const compare = (op: "===" | "!==" | CompareOp, u: IrExpr, right: IrExpr): IrExpr => ({ + kind: "bin", + op, + left: u, + right, + type: BOOL, + loc, + }); + const num = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc }); + if (e.kind === "unionEq" && !e.sameValue) { + for (const [operand, other] of [ + [e.left, e.right], + [e.right, e.left], + ] as const) { + const tags = tagsOf(operand); + if ( + tags && + tags.unionId === e.unionId && + other.kind === "unionWrap" && + other.tag === tags.stringTag && + other.value.kind === "strLit" && + other.value.value.length === 1 + ) { + const code = other.value.value.charCodeAt(0); + return { + operand, + test: (unit) => compare(e.negated ? "!==" : "===", unit(), num(code)), + }; + } + // `case undefined:` of a switch: undefined is a NaN unit. + if ( + tags && + tags.unionId === e.unionId && + other.kind === "unionWrap" && + other.tag === tags.undefinedTag && + other.value.kind === "unitLit" && + other.value.unit === "undefined" + ) + return { + operand, + twice: true, + test: (unit) => compare(e.negated ? "===" : "!==", unit(), unit()), + }; + } + return null; + } + if (e.kind === "libCall" && e.fn === "js.compare" && e.args.length === 3) { + const [a, b, opArg] = e.args as [IrExpr, IrExpr, IrExpr]; + if (opArg.kind !== "strLit" || !(opArg.value in FLIPPED)) return null; + const op = opArg.value as CompareOp; + const literalOf = (x: IrExpr): number | null => + x.kind === "libCall" && + x.fn === "js.fromStr" && + x.args.length === 1 && + x.args[0]!.kind === "strLit" && + x.args[0]!.value.length === 1 + ? x.args[0]!.value.charCodeAt(0) + : null; + const operandOf = (x: IrExpr): IrExpr | null => + x.kind === "libCall" && + x.fn === "js.fromDyn" && + x.args.length === 1 && + x.args[0]!.kind === "dynFrom" && + tagsOf(x.args[0]!.value) + ? x.args[0]!.value + : null; + // The comparison with the operand on the left. + const left = operandOf(a); + const right = operandOf(b); + const code = left ? literalOf(b) : right ? literalOf(a) : null; + const operand = left ?? right; + if (operand === null || code === null) return null; + const ordered = left ? op : FLIPPED[op]; + return { operand, test: (unit) => compare(ordered, unit(), num(code)) }; + } + if (e.kind === "unionIsTag") { + const tags = tagsOf(e.value); + if (!tags || tags.unionId !== e.unionId) return null; + if (e.tag !== tags.undefinedTag && e.tag !== tags.stringTag) return null; + // Undefined is a NaN unit; a character is any other. + const present = (e.tag === tags.stringTag) !== e.negated; + return { + operand: e.value, + twice: true, + test: (unit) => compare(present ? "===" : "!==", unit(), unit()), + }; + } + return null; +} + function rewriteFunction(fn: IrFunction): IrFunction { + const locals = new Map(fn.locals.map((l) => [l.id, l])); const candidates = new Set( - fn.locals.filter((l) => !l.mutable && !l.boxed && l.type.kind === "string").map((l) => l.id), + fn.locals + .filter( + (l) => !l.mutable && !l.boxed && (l.type.kind === "string" || l.type.kind === "union"), + ) + .map((l) => l.id), ); - if (candidates.size === 0) return fn; + // The optional reads candidate union locals are declared from, directly + // or as copies of another such local (a switch's discriminant temp). + const reads = new Map(); + const copyOf = new Map(); + // Whether a candidate string is declared from a charAt. + let charAtDeclared = false; + // The declarations, in the order a walk of every field finds them. + const collector: IrFieldWalker = { + node: (node) => { + if (node.kind !== "varDecl" || !candidates.has(node.localId) || !node.init) return; + const id = node.localId; + const init = node.init; + if (locals.get(id)?.type.kind === "string" && isCharAt(init)) charAtDeclared = true; + const read = optionalCharRead(init); + if (read) reads.set(id, read); + else if (init.kind === "varRef" && reads.has(init.localId)) { + reads.set(id, reads.get(init.localId)!); + copyOf.set(id, init.localId); + } + }, + string: () => {}, + }; + // Both walks only record candidates: a function without one holds none. + if (candidates.size > 0) for (const stmt of fn.body) walkStmtFields(stmt, collector, false); + const localTags = (operand: IrExpr): OptionalCharRead | null => + operand.kind === "varRef" ? (reads.get(operand.localId) ?? null) : null; + // Every mention of a candidate, in any field, against the mentions the // rewrite understands. const mentions = new Map(); @@ -52,31 +332,42 @@ function rewriteFunction(fn: IrFunction): IrFunction { const note = (map: Map, id: string): void => { map.set(id, (map.get(id) ?? 0) + 1); }; - const count = (value: unknown): void => { - if (typeof value === "string") { - if (candidates.has(value)) note(mentions, value); - } else if (Array.isArray(value)) { - for (const item of value) count(item); - } else if (value !== null && typeof value === "object") { - const node = value as Record; - const id = understoodMention(node); + const counter: IrFieldWalker = { + node: (node) => { + const id = understoodMention(node, locals, reads, localTags); if (id !== null && candidates.has(id)) { note(understood, id); if (node.kind === "varDecl") note(declarations, id); } - for (const key in node) if (key !== "loc") count(node[key]); - } + }, + string: (value) => { + if (candidates.has(value)) note(mentions, value); + }, }; - count(fn.body); + // Only candidates declared from a charAt or an optional read can be held + // (understoodMention), and the count only decides which are. + if (reads.size > 0 || charAtDeclared) + for (const stmt of fn.body) walkStmtFields(stmt, counter, false); + // A copy is held only with its source, and the source's mention in the + // copy's initializer is understood only while the copy is held. const held = new Set(); for (const id of candidates) - if ( - declarations.get(id) === 1 && - (mentions.get(id) ?? 0) > 0 && - mentions.get(id) === understood.get(id) - ) - held.add(id); - if (held.size === 0) return fn; + if (declarations.get(id) === 1 && (mentions.get(id) ?? 0) > 0) held.add(id); + for (let settled = false; !settled;) { + settled = true; + for (const id of held) { + let copies = 0; + for (const [copy, source] of copyOf) if (source === id && held.has(copy)) copies++; + const source = copyOf.get(id); + if ( + mentions.get(id) !== (understood.get(id) ?? 0) + copies || + (source !== undefined && !held.has(source)) + ) { + held.delete(id); + settled = false; + } + } + } const unitRef = (id: string, loc: IrExpr["loc"]): IrExpr => ({ kind: "varRef", @@ -84,53 +375,86 @@ function rewriteFunction(fn: IrFunction): IrFunction { type: F64, loc, }); + // Operands of optional unit tests: held union locals and reads in place. + const operandTags = (operand: IrExpr): OptionalCharRead | null => + operand.kind === "varRef" + ? held.has(operand.localId) + ? localTags(operand) + : null + : optionalCharRead(operand); + let changed = false; const rewriteExpr = (e: IrExpr): IrExpr => { if (e.kind === "strEq" || e.kind === "strCmp") { const use = unitUse(e); if (use && held.has(use.id)) { - const code: IrExpr = { kind: "numLit", value: use.code, type: F64, loc: e.loc }; - const c = unitRef(use.id, e.loc); - if (e.kind === "strEq") - return { - kind: "bin", - op: e.negated ? "!==" : "===", - left: c, - right: code, - type: BOOL, - loc: e.loc, - }; - // The comparison with the local on the left. - const op = use.left - ? e.op - : ({ "<": ">", "<=": ">=", ">": "<", ">=": "<=" } as const)[e.op]; - if (op === ">" || op === ">=") - return { kind: "bin", op, left: c, right: code, type: BOOL, loc: e.loc }; - const negation: IrExpr = { - kind: "bin", - op: op === "<" ? ">=" : ">", - left: c, - right: code, - type: BOOL, - loc: e.loc, + changed = true; + return charUnitTest(e, unitRef(use.id, e.loc), use.code, use.left); + } + // `s.charAt(i)` in place: its unit read, `s.charCodeAt(i)`. + const direct = charAtTest(e); + if (direct) { + changed = true; + const read: IrExpr = { + ...direct.charAt, + method: "charCodeAt", + receiver: rewriteExpr(direct.charAt.receiver), + args: direct.charAt.args.map(rewriteExpr), + type: F64, }; - return { kind: "unary", op: "!", operand: negation, type: BOOL, loc: e.loc }; + return charUnitTest(e, read, direct.code, direct.left); + } + } + const optional = optionalUnitTest(e, operandTags); + if (optional) { + const operand = optional.operand; + if (operand.kind === "varRef") { + changed = true; + return optional.test(() => unitRef(operand.localId, e.loc)); + } + // A read that keeps its temps declares them once: one unit read. + const read = optionalCharRead(operand)!; + if (read.plain || !optional.twice) { + changed = true; + const rewritten = rewriteRead(read); + return optional.test(() => unitRead(rewritten, operand.loc)); } } return mapExprChildren(e, rewriteExpr, rewriteStmt); }; + // A read's receiver and index may test other held locals. + const rewriteRead = (read: OptionalCharRead): OptionalCharRead => { + const [str, idx] = read.temps; + return { + ...read, + temps: [ + { ...str, init: rewriteExpr(str.init!) }, + { ...idx, init: rewriteExpr(idx.init!) }, + ], + receiver: rewriteExpr(read.receiver), + index: rewriteExpr(read.index), + }; + }; const rewriteStmt = (s: IrStmt): IrStmt => { - if (s.kind === "varDecl" && held.has(s.localId) && s.init?.kind === "strIntrinsic") - return { - ...s, - init: { - ...s.init, - method: "charCodeAt", - receiver: rewriteExpr(s.init.receiver), - args: s.init.args.map(rewriteExpr), - type: F64, - }, - }; - if (s.kind === "switch" && s.disc.kind === "varRef" && held.has(s.disc.localId)) + if (s.kind === "varDecl" && held.has(s.localId) && s.init) { + changed = true; + const source = copyOf.get(s.localId); + if (source !== undefined) return { ...s, init: unitRef(source, s.init.loc) }; + const read = reads.get(s.localId); + if (read) return { ...s, init: unitRead(rewriteRead(read), s.init.loc) }; + if (s.init.kind === "strIntrinsic") + return { + ...s, + init: { + ...s.init, + method: "charCodeAt", + receiver: rewriteExpr(s.init.receiver), + args: s.init.args.map(rewriteExpr), + type: F64, + }, + }; + } + if (s.kind === "switch" && s.disc.kind === "varRef" && held.has(s.disc.localId)) { + changed = true; return { ...s, disc: unitRef(s.disc.localId, s.disc.loc), @@ -143,11 +467,14 @@ function rewriteFunction(fn: IrFunction): IrFunction { body: c.body.map(rewriteStmt), })), }; + } return mapStmtChildren(s, rewriteExpr, rewriteStmt); }; + const body = fn.body.map(rewriteStmt); + if (!changed) return fn; return { ...fn, - body: fn.body.map(rewriteStmt), + body, locals: fn.locals.map((l) => (held.has(l.id) ? unitLocal(l) : l)), }; } @@ -157,26 +484,86 @@ function unitLocal(local: IrLocal): IrLocal { return { ...local, type: F64 }; } -/** The local a node mentions in a form the rewrite handles: the - * declaration from a one-argument charAt, a one-unit test, or a switch - * over one-unit cases. */ -function understoodMention(node: Record): string | null { - // count() visits every object in the body (types, locations, cases), so - // only the node kinds handled here are read as IR nodes - const kind = node.kind; - if (kind !== "varDecl" && kind !== "strEq" && kind !== "strCmp" && kind !== "switch") return null; - const n = node as unknown as IrExpr | IrStmt; - if (n.kind === "varDecl") return n.init && isCharAt(n.init) ? n.localId : null; +/** The local a node mentions in a form the rewrite handles: a string + * local's declaration from a one-argument charAt, one-unit test, or switch + * over one-unit cases; a union local's declaration from an optional read, + * or its test against a one-unit literal or for undefined. */ +function understoodMention( + n: IrExpr | IrStmt, + locals: ReadonlyMap, + reads: ReadonlyMap, + localTags: (operand: IrExpr) => OptionalCharRead | null, +): string | null { + const kind = n.kind; + if ( + kind !== "varDecl" && + kind !== "strEq" && + kind !== "strCmp" && + kind !== "switch" && + kind !== "unionEq" && + kind !== "libCall" && + kind !== "unionIsTag" + ) + return null; + if (n.kind === "varDecl") { + if (!n.init) return null; + const local = locals.get(n.localId); + if (local?.type.kind === "string") return isCharAt(n.init) ? n.localId : null; + const read = reads.get(n.localId); + return read && local?.type.kind === "union" && local.type.unionId === read.unionId + ? n.localId + : null; + } if (n.kind === "strEq" || n.kind === "strCmp") return unitUse(n)?.id ?? null; if (n.kind === "switch") { - if (n.disc.kind !== "varRef") return null; + if (n.disc.kind !== "varRef" || n.disc.type.kind !== "string") return null; if (!n.cases.some((c) => c.test !== null)) return null; const units = n.cases.every( (c) => c.test === null || (c.test.kind === "strLit" && c.test.value.length === 1), ); return units ? n.disc.localId : null; } - return null; + const optional = optionalUnitTest(n as IrExpr, localTags); + return optional?.operand.kind === "varRef" ? optional.operand.localId : null; +} + +/** A one-unit test of a string that reads one character or "" (charAt), + * on its unit `unit`, which reads NaN for "": an equality, or an ordered + * comparison with the unit on the left when `unitLeft`. "" is below every + * one-unit literal, and NaN fails every ordered comparison, so `c < "x"` + * becomes `!(c >= 120)`. */ +function charUnitTest(e: UnitUse, unit: IrExpr, code: number, unitLeft: boolean): IrExpr { + const literal: IrExpr = { kind: "numLit", value: code, type: F64, loc: e.loc }; + if (e.kind === "strEq") + return { + kind: "bin", + op: e.negated ? "!==" : "===", + left: unit, + right: literal, + type: BOOL, + loc: e.loc, + }; + const op = unitLeft ? e.op : FLIPPED[e.op]; + if (op === ">" || op === ">=") + return { kind: "bin", op, left: unit, right: literal, type: BOOL, loc: e.loc }; + const negation: IrExpr = { + kind: "bin", + op: op === "<" ? ">=" : ">", + left: unit, + right: literal, + type: BOOL, + loc: e.loc, + }; + return { kind: "unary", op: "!", operand: negation, type: BOOL, loc: e.loc }; +} + +/** A test between `s.charAt(i)` and a literal of one code unit. */ +function charAtTest(e: UnitUse): { charAt: CharAt; code: number; left: boolean } | null { + const literalRight = e.right.kind === "strLit"; + const literal = literalRight ? e.right : e.left; + const other = literalRight ? e.left : e.right; + if (literal.kind !== "strLit" || literal.value.length !== 1 || !isCharAt(other)) return null; + return { charAt: other, code: literal.value.charCodeAt(0), left: literalRight }; } function isCharAt(e: IrExpr): e is CharAt { diff --git a/packages/compiler/src/ir/constant-globals.ts b/packages/compiler/src/ir/constant-globals.ts index f18764223..76b086f40 100644 --- a/packages/compiler/src/ir/constant-globals.ts +++ b/packages/compiler/src/ir/constant-globals.ts @@ -36,7 +36,11 @@ export function foldConstantGlobals(mod: IrModule): IrModule { if (value && (value.kind === "numLit" || value.kind === "boolLit")) values.set(id, value); else values.delete(id); }; - for (const fn of mod.functions) + // Per function, the candidates it reads: the rewrite below visits only the + // functions that read a constant. + const readsByFunction = new Map>(); + for (const fn of mod.functions) { + let read: Set | undefined; everyStmtList(fn.body, { // Every node other than a read that names the binding is a write // (assignments, loop and catch bindings, compound updates); only a @@ -48,16 +52,21 @@ export function foldConstantGlobals(mod: IrModule): IrModule { return true; }, expr: (e) => { - if (e.kind !== "varRef" && "localId" in e && typeof e.localId === "string") - write(e.localId, null); + if (e.kind === "varRef") { + if (candidates.has(e.localId)) { + if (read === undefined) read = new Set(); + read.add(e.localId); + } + } else if ("localId" in e && typeof e.localId === "string") write(e.localId, null); return true; }, }); + if (read !== undefined) readsByFunction.set(fn, read); + } const constants = new Map(); for (const [id, value] of values) if (writes.get(id) === 1) constants.set(id, value); if (constants.size === 0) return mod; - const reads = (e: IrExpr): boolean => !(e.kind === "varRef" && constants.has(e.localId)); const rewriteExpr = (e: IrExpr): IrExpr => { if (e.kind === "varRef") { const value = constants.get(e.localId); @@ -68,7 +77,8 @@ export function foldConstantGlobals(mod: IrModule): IrModule { const rewriteStmt = (s: IrStmt): IrStmt => mapStmtChildren(s, rewriteExpr, rewriteStmt); let changed = false; const functions = mod.functions.map((fn): IrFunction => { - if (everyStmtList(fn.body, { stmt: () => true, expr: reads })) return fn; + const read = readsByFunction.get(fn); + if (read === undefined || ![...read].some((id) => constants.has(id))) return fn; changed = true; return { ...fn, body: fn.body.map(rewriteStmt) }; }); diff --git a/packages/compiler/src/ir/expression-inlining.test.ts b/packages/compiler/src/ir/expression-inlining.test.ts new file mode 100644 index 000000000..ae68c44ea --- /dev/null +++ b/packages/compiler/src/ir/expression-inlining.test.ts @@ -0,0 +1,145 @@ +import { expect, test } from "vitest"; +import { inlineExpressionFunctions } from "./expression-inlining.js"; +import { BOOL, F64, VOID, type IrExpr, type IrFunction, type IrModule, type IrStmt } from "./ir.js"; + +const loc = { file: "expression-inlining.ts", start: 0, end: 0 }; +const num = (value: number): IrExpr => ({ loc, kind: "numLit", value, type: F64 }); +const ref = (localId: string): IrExpr => ({ loc, kind: "varRef", localId, type: F64 }); +const bin = (op: "+" | "*" | ">>>", left: IrExpr, right: IrExpr): IrExpr => ({ + loc, + kind: "bin", + op, + left, + right, + type: F64, +}); +const call = (callee: string, args: IrExpr[]): IrExpr => ({ + loc, + kind: "call", + callee, + args, + type: F64, +}); +const local = (id: string, extra: object = {}) => ({ + id, + name: id, + type: F64, + mutable: true, + ...extra, +}); + +// a(i, j) = ((i + j) * (i + j + 1)) >>> 1 +const a: IrFunction = { + loc, + name: "a", + params: [ + { localId: "i", name: "i", type: F64 }, + { localId: "j", name: "j", type: F64 }, + ], + locals: [local("i"), local("j")], + returnType: F64, + body: [ + { + loc, + kind: "return", + value: bin( + ">>>", + bin("*", bin("+", ref("i"), ref("j")), bin("+", bin("+", ref("i"), ref("j")), num(1))), + num(1), + ), + }, + ], +}; + +/** `caller` running `body` in a loop. */ +function caller(body: IrStmt[], extra: object = {}): IrFunction { + return { + loc, + name: "caller", + params: [], + locals: [local("x", extra), local("y"), local("out")], + returnType: VOID, + body: [{ loc, kind: "while", cond: { loc, kind: "boolLit", value: true, type: BOOL }, body }], + }; +} + +const loopBody = (fn: IrFunction): IrStmt[] => (fn.body[0] as IrStmt & { kind: "while" }).body; + +const mod = (fn: IrFunction): IrModule => ({ + irVersion: 17, + sourceFile: "expression-inlining.ts", + entry: "caller", + functions: [a, fn], +}); + +test("calls passing literals and locals take the callee's expression over them", () => { + const result = inlineExpressionFunctions( + mod(caller([{ loc, kind: "assign", localId: "out", value: call("a", [ref("x"), num(3)]) }])), + ); + const assign = loopBody(result.functions[1]!)[0] as IrStmt & { kind: "assign" }; + expect(assign.value).toMatchObject({ + kind: "bin", + op: ">>>", + left: { + kind: "bin", + op: "*", + left: { + kind: "bin", + op: "+", + left: { kind: "varRef", localId: "x" }, + right: { kind: "numLit", value: 3 }, + }, + }, + }); + // Every substituted read is a fresh node. + const left = (assign.value as IrExpr & { kind: "bin" }).left as IrExpr & { kind: "bin" }; + const factors = [left.left, (left.right as IrExpr & { kind: "bin" }).left] as (IrExpr & { + kind: "bin"; + })[]; + expect(factors[0]!.left).not.toBe(factors[1]!.left); + expect(result.functions[0]).toBe(a); +}); + +test("calls with other arguments, boxed locals or callees with other bodies keep their call", () => { + const nested = call("a", [call("a", [ref("x"), ref("y")]), num(1)]); + const kept = inlineExpressionFunctions( + mod(caller([{ loc, kind: "assign", localId: "out", value: nested }])), + ); + const outer = (loopBody(kept.functions[1]!)[0] as IrStmt & { kind: "assign" }).value; + // The inner call inlines; the outer one passes a computed argument. + expect(outer).toMatchObject({ + kind: "call", + callee: "a", + args: [{ kind: "bin", op: ">>>" }, { kind: "numLit" }], + }); + const boxed = caller( + [{ loc, kind: "assign", localId: "out", value: call("a", [ref("x"), ref("y")]) }], + { + boxed: true, + }, + ); + expect(inlineExpressionFunctions(mod(boxed)).functions[1]).toBe(boxed); + const twoStatements: IrFunction = { + ...a, + body: [{ loc, kind: "exprStmt", expr: ref("i") }, ...a.body], + }; + const unchanged: IrModule = { + ...mod( + caller([{ loc, kind: "assign", localId: "out", value: call("a", [ref("x"), ref("y")]) }]), + ), + functions: [ + twoStatements, + caller([{ loc, kind: "assign", localId: "out", value: call("a", [ref("x"), ref("y")]) }]), + ], + }; + expect(inlineExpressionFunctions(unchanged)).toBe(unchanged); +}); + +test("calls outside loops keep their call", () => { + const outside: IrFunction = { + ...caller([]), + body: [{ loc, kind: "assign", localId: "out", value: call("a", [ref("x"), num(3)]) }], + }; + const m = mod(outside); + expect(inlineExpressionFunctions(m)).toBe(m); +}); diff --git a/packages/compiler/src/ir/expression-inlining.ts b/packages/compiler/src/ir/expression-inlining.ts new file mode 100644 index 000000000..1282cdb79 --- /dev/null +++ b/packages/compiler/src/ir/expression-inlining.ts @@ -0,0 +1,166 @@ +import type { IrExpr, IrFunction, IrModule, IrStmt } from "./ir.js"; +import { everyExprChild, everyStmtChild, mapExprChildren, transformStmtList } from "./traverse.js"; + +/** Callee bodies above this many nodes keep their call. */ +const MAX_EXPRESSION_NODES = 32; + +/** Number operators that run no other code and cannot throw. */ +const PURE_BINARY = new Set([ + "+", + "-", + "*", + "/", + "%", + "**", + "&", + "|", + "^", + "<<", + ">>", + ">>>", +]); + +/** The returned expression of a function whose body is `return ` over + * its number parameters: literals, parameter reads, and the number + * operators of PURE_BINARY, unary `-` and `~`. Null for any other function. */ +function inlinableExpression(fn: IrFunction): IrExpr | null { + if (fn.async || fn.generator || fn.captures !== undefined || fn.classCaptures?.length) + return null; + if (fn.tier === "dynamic" || fn.returnType.kind !== "f64") return null; + if (fn.body.length !== 1) return null; + const only = fn.body[0]!; + if (only.kind !== "return" || only.value === null) return null; + const params = new Set(); + for (const param of fn.params) { + if (param.type.kind !== "f64") return null; + params.add(param.localId); + } + const locals = new Map(fn.locals.map((local) => [local.id, local])); + for (const id of params) { + const local = locals.get(id); + if (local === undefined || local.boxed || local.tdz) return null; + } + let nodes = 0; + const pure = (e: IrExpr): boolean => { + if (++nodes > MAX_EXPRESSION_NODES || e.type.kind !== "f64") return false; + switch (e.kind) { + case "numLit": + return true; + case "varRef": + return params.has(e.localId); + case "bin": + return PURE_BINARY.has(e.op) && pure(e.left) && pure(e.right); + case "unary": + return (e.op === "-" || e.op === "~") && pure(e.operand); + default: + return false; + } + }; + return pure(only.value) ? only.value : null; +} + +/** A call argument that reads the same value wherever the inlined body + * reads it: a number literal or a read of one of the caller's own unboxed + * locals outside the temporal dead zone. The body writes nothing, so every + * read sees the value the call would have passed. */ +function duplicableArgument(e: IrExpr, caller: ReadonlyMap) { + if (e.type.kind !== "f64") return false; + // A literal whose source spelling does not round-trip keeps its call. + if (e.kind === "numLit") return e.spelling === undefined; + if (e.kind !== "varRef") return false; + const local = caller.get(e.localId); + return local !== undefined && !local.boxed && !local.tdz; +} + +/** A fresh copy of `e`: facts downstream are keyed by node identity. */ +function copy(e: IrExpr, substitute: (ref: IrExpr & { kind: "varRef" }) => IrExpr): IrExpr { + if (e.kind === "varRef") return substitute(e); + return mapExprChildren( + { ...e }, + (child) => copy(child, substitute), + (stmt) => stmt, + ); +} + +/** The direct calls of `body` that run inside a loop body or condition. */ +function callsInLoops(body: IrStmt[]): Set { + const found = new Set(); + let depth = 0; + const expr = (e: IrExpr): boolean => { + if (depth > 0 && e.kind === "call") found.add(e); + return everyExprChild(e, expr, stmt); + }; + const stmt = (s: IrStmt): boolean => { + const loop = + s.kind === "for" || s.kind === "while" || s.kind === "doWhile" || s.kind === "forOf"; + if (loop) depth++; + everyStmtChild(s, expr, stmt); + if (loop) depth--; + return true; + }; + body.forEach(stmt); + return found; +} + +/** Inlines functions whose body is one number expression over their + * parameters (`function a(i, j) { return (((i + j) * (i + j + 1)) >>> 1) + + * i + 1; }`) at direct call sites that pass literals or reads of the + * caller's locals. + * + * The callee runs no other code, allocates nothing and cannot throw, so + * the call's only effect is its result. Its arguments are reads that + * nothing between them and the callee's reads can change, so substituting + * them gives the same value. The caller's loop and integer analyses then + * see the arithmetic on its own counters (`a(i, j)` inside a counted loop + * over `i` and `j`). Only calls inside loops are inlined: elsewhere the + * integer analyses gain nothing. The function itself stays for its other + * uses. */ +export function inlineExpressionFunctions(mod: IrModule): IrModule { + const members = new Set((mod.classes ?? []).flatMap((cls) => cls.methods ?? [])); + const expressions = new Map(); + for (const fn of mod.functions) { + if (fn.name === mod.entry || members.has(fn.name)) continue; + const value = inlinableExpression(fn); + if (value !== null) expressions.set(fn.name, { fn, value }); + } + if (expressions.size === 0) return mod; + let changed = false; + const functions = mod.functions.map((fn) => { + const locals = new Map(fn.locals.map((local) => [local.id, local])); + const inLoops = callsInLoops(fn.body); + // Each call the rewrite inlines: of an expression function, in a loop, + // with arguments the copy may repeat. + const inlinable = (e: IrExpr & { kind: "call" }): boolean => { + if (e.callee === fn.name) return false; + const target = expressions.get(e.callee); + return ( + target !== undefined && + target.fn.params.length === e.args.length && + e.args.every((arg) => duplicableArgument(arg, locals)) + ); + }; + if (![...inLoops].some((e) => e.kind === "call" && inlinable(e))) return fn; + let inlined = false; + const body = transformStmtList(fn.body, { + stmt: (s) => s, + expr: (e) => { + if (e.kind !== "call" || !inLoops.has(e) || !inlinable(e)) return e; + const target = expressions.get(e.callee)!; + const args = new Map(target.fn.params.map((param, i) => [param.localId, e.args[i]!])); + inlined = true; + return copy(target.value, (ref) => { + const arg = args.get(ref.localId); + if (arg?.kind === "numLit") + return { kind: "numLit", value: arg.value, type: arg.type, loc: ref.loc }; + if (arg?.kind === "varRef") + return { kind: "varRef", localId: arg.localId, type: arg.type, loc: ref.loc }; + return ref; + }); + }, + }); + if (!inlined) return fn; + changed = true; + return { ...fn, body }; + }); + return changed ? { ...mod, functions } : mod; +} diff --git a/packages/compiler/src/ir/fields.generated.ts b/packages/compiler/src/ir/fields.generated.ts new file mode 100644 index 000000000..e9f5efaa3 --- /dev/null +++ b/packages/compiler/src/ir/fields.generated.ts @@ -0,0 +1,2071 @@ +// Generated from packages/compiler/src/ir/ir.ts by scripts/generate-ir-fields.mjs. +// Run `pnpm generate:ir-fields` after changing the fields of an IR node. +import type { IrExpr, IrStmt, IrType } from "./ir.js"; + +/** Calls `visit` with each string field of `node` (including `kind`) and its + * name: the string values `for (const key in node)` finds on a node, read + * by name in the order ir.ts declares them. */ +export function eachStringField( + node: IrExpr | IrStmt, + visit: (value: string, field: string) => void, +): void { + switch (node.kind) { + case "arrIntrinsic": { + visit(node.kind, "kind"); + visit(node.method, "method"); + return; + } + case "arrayDelete": { + visit(node.kind, "kind"); + return; + } + case "arrayGet": { + visit(node.kind, "kind"); + return; + } + case "arrayHas": { + visit(node.kind, "kind"); + return; + } + case "arrayLit": { + visit(node.kind, "kind"); + return; + } + case "arrayNewLen": { + visit(node.kind, "kind"); + return; + } + case "arraySet": { + visit(node.kind, "kind"); + return; + } + case "arraySetLength": { + visit(node.kind, "kind"); + return; + } + case "arraySetUndefined": { + visit(node.kind, "kind"); + return; + } + case "arrayState": { + visit(node.kind, "kind"); + return; + } + case "assign": { + visit(node.kind, "kind"); + visit(node.localId, "localId"); + return; + } + case "assignExpr": { + visit(node.kind, "kind"); + visit(node.localId, "localId"); + return; + } + case "awaitExpr": { + visit(node.kind, "kind"); + return; + } + case "awaitUnionExpr": { + visit(node.kind, "kind"); + return; + } + case "bin": { + visit(node.kind, "kind"); + visit(node.op, "op"); + return; + } + case "block": { + visit(node.kind, "kind"); + return; + } + case "boolLit": { + visit(node.kind, "kind"); + return; + } + case "break": { + visit(node.kind, "kind"); + if (typeof node.label === "string") visit(node.label, "label"); + return; + } + case "bytesIntrinsic": { + visit(node.kind, "kind"); + visit(node.method, "method"); + return; + } + case "bytesNew": { + visit(node.kind, "kind"); + return; + } + case "bytesSet": { + visit(node.kind, "kind"); + return; + } + case "call": { + visit(node.kind, "kind"); + visit(node.callee, "callee"); + return; + } + case "callValue": { + visit(node.kind, "kind"); + return; + } + case "caughtCheck": { + visit(node.kind, "kind"); + visit(node.className, "className"); + return; + } + case "caughtNarrow": { + visit(node.kind, "kind"); + return; + } + case "caughtTest": { + visit(node.kind, "kind"); + visit(node.test, "test"); + if (typeof node.className === "string") visit(node.className, "className"); + return; + } + case "caughtToDyn": { + visit(node.kind, "kind"); + return; + } + case "chainRecv": { + visit(node.kind, "kind"); + visit(node.id, "id"); + return; + } + case "classRef": { + visit(node.kind, "kind"); + visit(node.className, "className"); + return; + } + case "closure": { + visit(node.kind, "kind"); + visit(node.fnName, "fnName"); + return; + } + case "continue": { + visit(node.kind, "kind"); + if (typeof node.label === "string") visit(node.label, "label"); + return; + } + case "doWhile": { + visit(node.kind, "kind"); + return; + } + case "downcast": { + visit(node.kind, "kind"); + return; + } + case "dynArrLit": { + visit(node.kind, "kind"); + return; + } + case "dynCall": { + visit(node.kind, "kind"); + visit(node.calleeName, "calleeName"); + return; + } + case "dynCheck": { + visit(node.kind, "kind"); + if (typeof node.inferredSite === "string") visit(node.inferredSite, "inferredSite"); + return; + } + case "dynDestrCheck": { + visit(node.kind, "kind"); + visit(node.spelling, "spelling"); + if (typeof node.firstProp === "string") visit(node.firstProp, "firstProp"); + return; + } + case "dynFrom": { + visit(node.kind, "kind"); + if (typeof node.fnName === "string") visit(node.fnName, "fnName"); + return; + } + case "dynHasKey": { + visit(node.kind, "kind"); + visit(node.key, "key"); + return; + } + case "dynInvoke": { + visit(node.kind, "kind"); + visit(node.method, "method"); + visit(node.calleeName, "calleeName"); + return; + } + case "dynIterN": { + visit(node.kind, "kind"); + if (typeof node.notIterableMessage === "string") visit(node.notIterableMessage, "notIterableMessage"); + return; + } + case "dynKeyGet": { + visit(node.kind, "kind"); + return; + } + case "dynObjLit": { + visit(node.kind, "kind"); + return; + } + case "dynScalarEq": { + visit(node.kind, "kind"); + return; + } + case "dynTest": { + visit(node.kind, "kind"); + visit(node.test, "test"); + if (typeof node.bytesElem === "string") visit(node.bytesElem, "bytesElem"); + return; + } + case "exprStmt": { + visit(node.kind, "kind"); + return; + } + case "ffiCall": { + visit(node.kind, "kind"); + visit(node.import, "import"); + return; + } + case "fieldAbsent": { + visit(node.kind, "kind"); + visit(node.unionId, "unionId"); + return; + } + case "fieldGet": { + visit(node.kind, "kind"); + visit(node.className, "className"); + visit(node.field, "field"); + return; + } + case "fieldIncDec": { + visit(node.kind, "kind"); + visit(node.op, "op"); + visit(node.className, "className"); + visit(node.field, "field"); + return; + } + case "fieldSet": { + visit(node.kind, "kind"); + visit(node.className, "className"); + visit(node.field, "field"); + return; + } + case "for": { + visit(node.kind, "kind"); + return; + } + case "forOf": { + visit(node.kind, "kind"); + visit(node.localId, "localId"); + return; + } + case "genResume": { + visit(node.kind, "kind"); + visit(node.mode, "mode"); + return; + } + case "if": { + visit(node.kind, "kind"); + return; + } + case "incDec": { + visit(node.kind, "kind"); + visit(node.op, "op"); + visit(node.localId, "localId"); + return; + } + case "instanceOf": { + visit(node.kind, "kind"); + visit(node.className, "className"); + return; + } + case "instanceOfValue": { + visit(node.kind, "kind"); + return; + } + case "intrinsic": { + visit(node.kind, "kind"); + visit(node.name, "name"); + return; + } + case "jsonStringify": { + visit(node.kind, "kind"); + if (typeof node.indent === "string") visit(node.indent, "indent"); + return; + } + case "libCall": { + visit(node.kind, "kind"); + visit(node.fn, "fn"); + return; + } + case "logical": { + visit(node.kind, "kind"); + visit(node.op, "op"); + return; + } + case "mapIntrinsic": { + visit(node.kind, "kind"); + visit(node.method, "method"); + return; + } + case "mapNew": { + visit(node.kind, "kind"); + return; + } + case "moduleNsRef": { + visit(node.kind, "kind"); + visit(node.moduleId, "moduleId"); + return; + } + case "new": { + visit(node.kind, "kind"); + visit(node.className, "className"); + return; + } + case "newPromise": { + visit(node.kind, "kind"); + return; + } + case "newValue": { + visit(node.kind, "kind"); + return; + } + case "nullish": { + visit(node.kind, "kind"); + return; + } + case "numLit": { + visit(node.kind, "kind"); + if (typeof node.spelling === "string") visit(node.spelling, "spelling"); + return; + } + case "optChain": { + visit(node.kind, "kind"); + visit(node.id, "id"); + return; + } + case "orDefault": { + visit(node.kind, "kind"); + if (typeof node.retag === "string") visit(node.retag, "retag"); + return; + } + case "promiseVoidWiden": { + visit(node.kind, "kind"); + return; + } + case "promiseWithResolvers": { + visit(node.kind, "kind"); + return; + } + case "recordClone": { + visit(node.kind, "kind"); + return; + } + case "recordGet": { + visit(node.kind, "kind"); + visit(node.shapeId, "shapeId"); + visit(node.field, "field"); + return; + } + case "recordHas": { + visit(node.kind, "kind"); + visit(node.shapeId, "shapeId"); + visit(node.field, "field"); + return; + } + case "recordKeyDelete": { + visit(node.kind, "kind"); + visit(node.shapeId, "shapeId"); + return; + } + case "recordKeyGet": { + visit(node.kind, "kind"); + visit(node.shapeId, "shapeId"); + return; + } + case "recordKeySet": { + visit(node.kind, "kind"); + visit(node.shapeId, "shapeId"); + return; + } + case "recordLit": { + visit(node.kind, "kind"); + return; + } + case "recordNullProto": { + visit(node.kind, "kind"); + visit(node.shapeId, "shapeId"); + return; + } + case "recordOvfHas": { + visit(node.kind, "kind"); + visit(node.shapeId, "shapeId"); + return; + } + case "recordOvfKeys": { + visit(node.kind, "kind"); + visit(node.shapeId, "shapeId"); + return; + } + case "recordSet": { + visit(node.kind, "kind"); + visit(node.shapeId, "shapeId"); + visit(node.field, "field"); + return; + } + case "regexIntrinsic": { + visit(node.kind, "kind"); + visit(node.method, "method"); + return; + } + case "regexLit": { + visit(node.kind, "kind"); + visit(node.pattern, "pattern"); + visit(node.flags, "flags"); + return; + } + case "rethrow": { + visit(node.kind, "kind"); + visit(node.localId, "localId"); + return; + } + case "return": { + visit(node.kind, "kind"); + return; + } + case "runtimeFence": { + visit(node.kind, "kind"); + visit(node.code, "code"); + visit(node.message, "message"); + return; + } + case "selfRef": { + visit(node.kind, "kind"); + return; + } + case "seqExpr": { + visit(node.kind, "kind"); + return; + } + case "setIntrinsic": { + visit(node.kind, "kind"); + visit(node.method, "method"); + return; + } + case "setNew": { + visit(node.kind, "kind"); + return; + } + case "strCmp": { + visit(node.kind, "kind"); + visit(node.op, "op"); + return; + } + case "strConcat": { + visit(node.kind, "kind"); + return; + } + case "strEq": { + visit(node.kind, "kind"); + return; + } + case "strIntrinsic": { + visit(node.kind, "kind"); + visit(node.method, "method"); + return; + } + case "strLit": { + visit(node.kind, "kind"); + visit(node.value, "value"); + return; + } + case "switch": { + visit(node.kind, "kind"); + return; + } + case "templateStrings": { + visit(node.kind, "kind"); + visit(node.key, "key"); + return; + } + case "ternary": { + visit(node.kind, "kind"); + return; + } + case "throw": { + visit(node.kind, "kind"); + return; + } + case "toBool": { + visit(node.kind, "kind"); + return; + } + case "toString": { + visit(node.kind, "kind"); + return; + } + case "tryCatch": { + visit(node.kind, "kind"); + if (typeof node.catchLocalId === "string") visit(node.catchLocalId, "catchLocalId"); + return; + } + case "unary": { + visit(node.kind, "kind"); + visit(node.op, "op"); + return; + } + case "unionDisc": { + visit(node.kind, "kind"); + visit(node.unionId, "unionId"); + visit(node.field, "field"); + return; + } + case "unionEq": { + visit(node.kind, "kind"); + visit(node.unionId, "unionId"); + return; + } + case "unionFuncEq": { + visit(node.kind, "kind"); + visit(node.unionId, "unionId"); + return; + } + case "unionIsTag": { + visit(node.kind, "kind"); + visit(node.unionId, "unionId"); + return; + } + case "unionKeyGet": { + visit(node.kind, "kind"); + visit(node.unionId, "unionId"); + return; + } + case "unionNarrow": { + visit(node.kind, "kind"); + visit(node.unionId, "unionId"); + return; + } + case "unionRetype": { + visit(node.kind, "kind"); + visit(node.fromId, "fromId"); + visit(node.unionId, "unionId"); + return; + } + case "unionWrap": { + visit(node.kind, "kind"); + visit(node.unionId, "unionId"); + return; + } + case "unitLit": { + visit(node.kind, "kind"); + visit(node.unit, "unit"); + return; + } + case "upcast": { + visit(node.kind, "kind"); + return; + } + case "varDecl": { + visit(node.kind, "kind"); + visit(node.localId, "localId"); + return; + } + case "varRef": { + visit(node.kind, "kind"); + visit(node.localId, "localId"); + return; + } + case "virtualCall": { + visit(node.kind, "kind"); + visit(node.className, "className"); + visit(node.method, "method"); + return; + } + case "while": { + visit(node.kind, "kind"); + return; + } + case "yieldExpr": { + visit(node.kind, "kind"); + return; + } + } +} + +/** What a field walk reports (walkExprFields): each expression and + * statement node before its fields, and each string a field holds. */ +export interface IrFieldWalker { + node(node: IrExpr | IrStmt): void; + string(value: string): void; +} + +/** Walks every field of `node` but `loc` (and `type` with `skipType`), and + * every field of each object under it: the strings and nodes a recursive + * `for (const key in value)` walk with the same skips finds, read by name in + * the order ir.ts declares the fields. */ +export function walkExprFields(node: IrExpr, walker: IrFieldWalker, skipType: boolean): void { + walker.node(node); + switch (node.kind) { + case "arrIntrinsic": { + walker.string(node.kind); + walker.string(node.method); + walkExprFields(node.receiver, walker, skipType); + for (const item0 of node.args) { + walkExprFields(item0, walker, skipType); + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "arrayGet": { + walker.string(node.kind); + walkExprFields(node.arr, walker, skipType); + walkExprFields(node.index, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "arrayHas": { + walker.string(node.kind); + walkExprFields(node.arr, walker, skipType); + walkExprFields(node.index, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "arrayLit": { + walker.string(node.kind); + for (const item1 of node.elems) { + walkExprFields(item1, walker, skipType); + } + if (node.spreads !== undefined && node.spreads !== null) { + for (const item2 of node.spreads) { + } + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "arrayNewLen": { + walker.string(node.kind); + walkExprFields(node.length, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "arrayState": { + walker.string(node.kind); + walkExprFields(node.arr, walker, skipType); + walkExprFields(node.index, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "assignExpr": { + walker.string(node.kind); + walker.string(node.localId); + walkExprFields(node.value, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "awaitExpr": { + walker.string(node.kind); + walkExprFields(node.value, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "awaitUnionExpr": { + walker.string(node.kind); + walkExprFields(node.value, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "bin": { + walker.string(node.kind); + walker.string(node.op); + walkExprFields(node.left, walker, skipType); + walkExprFields(node.right, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "boolLit": { + walker.string(node.kind); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "bytesIntrinsic": { + walker.string(node.kind); + walker.string(node.method); + walkExprFields(node.receiver, walker, skipType); + for (const item3 of node.args) { + walkExprFields(item3, walker, skipType); + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "bytesNew": { + walker.string(node.kind); + if (node.source !== undefined && node.source !== null) { + walkExprFields(node.source, walker, skipType); + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "call": { + walker.string(node.kind); + walker.string(node.callee); + for (const item4 of node.args) { + walkExprFields(item4, walker, skipType); + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "callValue": { + walker.string(node.kind); + walkExprFields(node.callee, walker, skipType); + if (node.receiver !== undefined && node.receiver !== null) { + walkExprFields(node.receiver, walker, skipType); + } + for (const item5 of node.args) { + walkExprFields(item5, walker, skipType); + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "caughtCheck": { + walker.string(node.kind); + walkExprFields(node.value, walker, skipType); + walker.string(node.className); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "caughtNarrow": { + walker.string(node.kind); + walkExprFields(node.value, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "caughtTest": { + walker.string(node.kind); + walkExprFields(node.value, walker, skipType); + walker.string(node.test); + if (node.className !== undefined && node.className !== null) { + walker.string(node.className); + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "caughtToDyn": { + walker.string(node.kind); + walkExprFields(node.value, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "chainRecv": { + walker.string(node.kind); + walker.string(node.id); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "classRef": { + walker.string(node.kind); + walker.string(node.className); + if (node.captures !== undefined && node.captures !== null) { + for (const item6 of node.captures) { + walker.string(item6); + } + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "closure": { + walker.string(node.kind); + walker.string(node.fnName); + for (const item7 of node.captures) { + walker.string(item7); + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "downcast": { + walker.string(node.kind); + walkExprFields(node.value, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "dynArrLit": { + walker.string(node.kind); + for (const item8 of node.elems) { + walkExprFields(item8, walker, skipType); + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "dynCall": { + walker.string(node.kind); + walkExprFields(node.callee, walker, skipType); + if (node.receiver !== undefined && node.receiver !== null) { + walkExprFields(node.receiver, walker, skipType); + } + walker.string(node.calleeName); + if (node.calleeNameValue !== undefined && node.calleeNameValue !== null) { + walkExprFields(node.calleeNameValue, walker, skipType); + } + for (const item9 of node.args) { + walkExprFields(item9, walker, skipType); + } + if (node.spreads !== undefined && node.spreads !== null) { + for (const item10 of node.spreads) { + walker.string(item10.what); + } + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "dynCheck": { + walker.string(node.kind); + walkExprFields(node.value, walker, skipType); + if (node.inferredSite !== undefined && node.inferredSite !== null) { + walker.string(node.inferredSite); + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "dynDestrCheck": { + walker.string(node.kind); + walkExprFields(node.value, walker, skipType); + walker.string(node.spelling); + if (node.firstProp !== undefined && node.firstProp !== null) { + walker.string(node.firstProp); + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "dynFrom": { + walker.string(node.kind); + walkExprFields(node.value, walker, skipType); + if (node.fnName !== undefined && node.fnName !== null) { + walker.string(node.fnName); + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "dynHasKey": { + walker.string(node.kind); + walker.string(node.key); + walkExprFields(node.value, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "dynInvoke": { + walker.string(node.kind); + walkExprFields(node.recv, walker, skipType); + walker.string(node.method); + walker.string(node.calleeName); + if (node.calleeNameValue !== undefined && node.calleeNameValue !== null) { + walkExprFields(node.calleeNameValue, walker, skipType); + } + for (const item11 of node.args) { + walkExprFields(item11, walker, skipType); + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "dynIterN": { + walker.string(node.kind); + walkExprFields(node.value, walker, skipType); + if (node.notIterableMessage !== undefined && node.notIterableMessage !== null) { + walker.string(node.notIterableMessage); + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "dynKeyGet": { + walker.string(node.kind); + walkExprFields(node.key, walker, skipType); + walkExprFields(node.value, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "dynObjLit": { + walker.string(node.kind); + if (node.fields !== undefined && node.fields !== null) { + for (const item12 of node.fields) { + walkExprFields(item12.key, walker, skipType); + walkExprFields(item12.value, walker, skipType); + } + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "dynScalarEq": { + walker.string(node.kind); + walkExprFields(node.left, walker, skipType); + walkExprFields(node.right, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "dynTest": { + walker.string(node.kind); + walker.string(node.test); + if (node.bytesElem !== undefined && node.bytesElem !== null) { + walker.string(node.bytesElem); + } + walkExprFields(node.value, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "ffiCall": { + walker.string(node.kind); + walker.string(node.import); + for (const item13 of node.args) { + walkExprFields(item13, walker, skipType); + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "fieldAbsent": { + walker.string(node.kind); + walker.string(node.unionId); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "fieldGet": { + walker.string(node.kind); + walkExprFields(node.obj, walker, skipType); + walker.string(node.className); + walker.string(node.field); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "fieldIncDec": { + walker.string(node.kind); + walker.string(node.op); + walkExprFields(node.obj, walker, skipType); + walker.string(node.className); + walker.string(node.field); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "genResume": { + walker.string(node.kind); + walker.string(node.mode); + walkExprFields(node.gen, walker, skipType); + if (node.arg !== undefined && node.arg !== null) { + walkExprFields(node.arg, walker, skipType); + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "incDec": { + walker.string(node.kind); + walker.string(node.op); + walker.string(node.localId); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "instanceOf": { + walker.string(node.kind); + walkExprFields(node.value, walker, skipType); + walker.string(node.className); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "instanceOfValue": { + walker.string(node.kind); + walkExprFields(node.value, walker, skipType); + walkExprFields(node.classValue, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "intrinsic": { + walker.string(node.kind); + walker.string(node.name); + for (const item14 of node.args) { + walkExprFields(item14, walker, skipType); + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "jsonStringify": { + walker.string(node.kind); + walkExprFields(node.value, walker, skipType); + if (node.indent !== undefined && node.indent !== null) { + walker.string(node.indent); + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "libCall": { + walker.string(node.kind); + walker.string(node.fn); + for (const item15 of node.args) { + walkExprFields(item15, walker, skipType); + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + if (node.assertOptions !== undefined && node.assertOptions !== null) { + if (node.assertOptions.diff !== undefined && node.assertOptions.diff !== null) { + walker.string(node.assertOptions.diff); + } + } + return; + } + case "logical": { + walker.string(node.kind); + walker.string(node.op); + walkExprFields(node.left, walker, skipType); + walkExprFields(node.right, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "mapIntrinsic": { + walker.string(node.kind); + walker.string(node.method); + walkExprFields(node.receiver, walker, skipType); + for (const item16 of node.args) { + walkExprFields(item16, walker, skipType); + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "mapNew": { + walker.string(node.kind); + if (node.seed !== undefined && node.seed !== null) { + for (const item17 of node.seed) { + walkExprFields(item17.key, walker, skipType); + walkExprFields(item17.value, walker, skipType); + } + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "moduleNsRef": { + walker.string(node.kind); + walker.string(node.moduleId); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "new": { + walker.string(node.kind); + walker.string(node.className); + for (const item18 of node.args) { + walkExprFields(item18, walker, skipType); + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "newPromise": { + walker.string(node.kind); + walkExprFields(node.executor, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "newValue": { + walker.string(node.kind); + walkExprFields(node.callee, walker, skipType); + for (const item19 of node.args) { + walkExprFields(item19, walker, skipType); + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "nullish": { + walker.string(node.kind); + walkExprFields(node.left, walker, skipType); + walkExprFields(node.right, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "numLit": { + walker.string(node.kind); + if (node.spelling !== undefined && node.spelling !== null) { + walker.string(node.spelling); + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "optChain": { + walker.string(node.kind); + walker.string(node.id); + walkExprFields(node.receiver, walker, skipType); + walkExprFields(node.body, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "orDefault": { + walker.string(node.kind); + walkExprFields(node.left, walker, skipType); + walkExprFields(node.right, walker, skipType); + if (node.retag !== undefined && node.retag !== null) { + walker.string(node.retag); + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "promiseVoidWiden": { + walker.string(node.kind); + walkExprFields(node.value, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "promiseWithResolvers": { + walker.string(node.kind); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "recordClone": { + walker.string(node.kind); + walkExprFields(node.source, walker, skipType); + for (const item20 of node.overrides) { + walker.string(item20.name); + walkExprFields(item20.value, walker, skipType); + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "recordGet": { + walker.string(node.kind); + walkExprFields(node.obj, walker, skipType); + walker.string(node.shapeId); + walker.string(node.field); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "recordHas": { + walker.string(node.kind); + walkExprFields(node.obj, walker, skipType); + walker.string(node.shapeId); + walker.string(node.field); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "recordKeyGet": { + walker.string(node.kind); + walkExprFields(node.obj, walker, skipType); + walker.string(node.shapeId); + walkExprFields(node.key, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "recordLit": { + walker.string(node.kind); + for (const item21 of node.fields) { + walker.string(item21.name); + walkExprFields(item21.value, walker, skipType); + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "recordNullProto": { + walker.string(node.kind); + walkExprFields(node.obj, walker, skipType); + walker.string(node.shapeId); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "recordOvfHas": { + walker.string(node.kind); + walkExprFields(node.obj, walker, skipType); + walker.string(node.shapeId); + walkExprFields(node.key, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "recordOvfKeys": { + walker.string(node.kind); + walkExprFields(node.obj, walker, skipType); + walker.string(node.shapeId); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "regexIntrinsic": { + walker.string(node.kind); + walker.string(node.method); + walkExprFields(node.receiver, walker, skipType); + for (const item22 of node.args) { + walkExprFields(item22, walker, skipType); + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "regexLit": { + walker.string(node.kind); + walker.string(node.pattern); + walker.string(node.flags); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "selfRef": { + walker.string(node.kind); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "seqExpr": { + walker.string(node.kind); + for (const item23 of node.stmts) { + walkStmtFields(item23, walker, skipType); + } + walkExprFields(node.result, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "setIntrinsic": { + walker.string(node.kind); + walker.string(node.method); + walkExprFields(node.receiver, walker, skipType); + for (const item24 of node.args) { + walkExprFields(item24, walker, skipType); + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "setNew": { + walker.string(node.kind); + if (node.seed !== undefined && node.seed !== null) { + walkExprFields(node.seed, walker, skipType); + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "strCmp": { + walker.string(node.kind); + walker.string(node.op); + walkExprFields(node.left, walker, skipType); + walkExprFields(node.right, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "strConcat": { + walker.string(node.kind); + walkExprFields(node.left, walker, skipType); + walkExprFields(node.right, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "strEq": { + walker.string(node.kind); + walkExprFields(node.left, walker, skipType); + walkExprFields(node.right, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "strIntrinsic": { + walker.string(node.kind); + walker.string(node.method); + walkExprFields(node.receiver, walker, skipType); + for (const item25 of node.args) { + walkExprFields(item25, walker, skipType); + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "strLit": { + walker.string(node.kind); + walker.string(node.value); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "templateStrings": { + walker.string(node.kind); + walker.string(node.key); + for (const item26 of node.cooked) { + walker.string(item26); + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "ternary": { + walker.string(node.kind); + walkExprFields(node.cond, walker, skipType); + walkExprFields(node.then, walker, skipType); + walkExprFields(node.else_, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "toBool": { + walker.string(node.kind); + walkExprFields(node.operand, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "toString": { + walker.string(node.kind); + walkExprFields(node.operand, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "unary": { + walker.string(node.kind); + walker.string(node.op); + walkExprFields(node.operand, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "unionDisc": { + walker.string(node.kind); + walker.string(node.unionId); + walker.string(node.field); + walkExprFields(node.value, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "unionEq": { + walker.string(node.kind); + walker.string(node.unionId); + walkExprFields(node.left, walker, skipType); + walkExprFields(node.right, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "unionFuncEq": { + walker.string(node.kind); + walker.string(node.unionId); + walkExprFields(node.union, walker, skipType); + walkExprFields(node.func, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "unionIsTag": { + walker.string(node.kind); + walker.string(node.unionId); + walkExprFields(node.value, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "unionKeyGet": { + walker.string(node.kind); + walker.string(node.unionId); + walkExprFields(node.key, walker, skipType); + walkExprFields(node.value, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "unionNarrow": { + walker.string(node.kind); + walker.string(node.unionId); + walkExprFields(node.value, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "unionRetype": { + walker.string(node.kind); + walker.string(node.fromId); + walker.string(node.unionId); + for (const item27 of node.tags) { + } + walkExprFields(node.value, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "unionWrap": { + walker.string(node.kind); + walker.string(node.unionId); + walkExprFields(node.value, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "unitLit": { + walker.string(node.kind); + walker.string(node.unit); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "upcast": { + walker.string(node.kind); + walkExprFields(node.value, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "varRef": { + walker.string(node.kind); + walker.string(node.localId); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "virtualCall": { + walker.string(node.kind); + walker.string(node.className); + walker.string(node.method); + for (const item28 of node.args) { + walkExprFields(item28, walker, skipType); + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + if (node.targets !== undefined && node.targets !== null) { + for (const item29 of node.targets) { + walker.string(item29); + } + } + return; + } + case "yieldExpr": { + walker.string(node.kind); + if (node.value !== undefined && node.value !== null) { + walkExprFields(node.value, walker, skipType); + } + if (node.captureCompletion !== undefined && node.captureCompletion !== null) { + walkTypeFields(node.captureCompletion.returnType, walker, skipType); + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + } +} + +/** Walks every field of `node` but `loc` (and `type` with `skipType`), and + * every field of each object under it: the strings and nodes a recursive + * `for (const key in value)` walk with the same skips finds, read by name in + * the order ir.ts declares the fields. */ +export function walkStmtFields(node: IrStmt, walker: IrFieldWalker, skipType: boolean): void { + walker.node(node); + switch (node.kind) { + case "arrayDelete": { + walker.string(node.kind); + walkExprFields(node.arr, walker, skipType); + walkExprFields(node.index, walker, skipType); + return; + } + case "arraySet": { + walker.string(node.kind); + walkExprFields(node.arr, walker, skipType); + walkExprFields(node.index, walker, skipType); + walkExprFields(node.value, walker, skipType); + return; + } + case "arraySetLength": { + walker.string(node.kind); + walkExprFields(node.arr, walker, skipType); + walkExprFields(node.length, walker, skipType); + return; + } + case "arraySetUndefined": { + walker.string(node.kind); + walkExprFields(node.arr, walker, skipType); + walkExprFields(node.index, walker, skipType); + return; + } + case "assign": { + walker.string(node.kind); + walker.string(node.localId); + walkExprFields(node.value, walker, skipType); + return; + } + case "block": { + walker.string(node.kind); + for (const item30 of node.body) { + walkStmtFields(item30, walker, skipType); + } + if (node.labels !== undefined && node.labels !== null) { + for (const item31 of node.labels) { + walker.string(item31); + } + } + return; + } + case "break": { + walker.string(node.kind); + if (node.label !== undefined && node.label !== null) { + walker.string(node.label); + } + return; + } + case "bytesSet": { + walker.string(node.kind); + walkExprFields(node.arr, walker, skipType); + walkExprFields(node.index, walker, skipType); + walkExprFields(node.value, walker, skipType); + return; + } + case "continue": { + walker.string(node.kind); + if (node.label !== undefined && node.label !== null) { + walker.string(node.label); + } + return; + } + case "doWhile": { + walker.string(node.kind); + for (const item32 of node.body) { + walkStmtFields(item32, walker, skipType); + } + walkExprFields(node.cond, walker, skipType); + if (node.labels !== undefined && node.labels !== null) { + for (const item33 of node.labels) { + walker.string(item33); + } + } + return; + } + case "exprStmt": { + walker.string(node.kind); + walkExprFields(node.expr, walker, skipType); + return; + } + case "fieldSet": { + walker.string(node.kind); + walkExprFields(node.obj, walker, skipType); + walker.string(node.className); + walker.string(node.field); + walkExprFields(node.value, walker, skipType); + return; + } + case "for": { + walker.string(node.kind); + if (node.init !== undefined && node.init !== null) { + walkStmtFields(node.init, walker, skipType); + } + if (node.cond !== undefined && node.cond !== null) { + walkExprFields(node.cond, walker, skipType); + } + if (node.update !== undefined && node.update !== null) { + walkStmtFields(node.update, walker, skipType); + } + for (const item34 of node.body) { + walkStmtFields(item34, walker, skipType); + } + if (node.labels !== undefined && node.labels !== null) { + for (const item35 of node.labels) { + walker.string(item35); + } + } + return; + } + case "forOf": { + walker.string(node.kind); + walker.string(node.localId); + walkExprFields(node.iterable, walker, skipType); + for (const item36 of node.body) { + walkStmtFields(item36, walker, skipType); + } + if (node.labels !== undefined && node.labels !== null) { + for (const item37 of node.labels) { + walker.string(item37); + } + } + return; + } + case "if": { + walker.string(node.kind); + walkExprFields(node.cond, walker, skipType); + for (const item38 of node.then) { + walkStmtFields(item38, walker, skipType); + } + if (node.else_ !== undefined && node.else_ !== null) { + for (const item39 of node.else_) { + walkStmtFields(item39, walker, skipType); + } + } + return; + } + case "recordKeyDelete": { + walker.string(node.kind); + walkExprFields(node.obj, walker, skipType); + walker.string(node.shapeId); + walkExprFields(node.key, walker, skipType); + return; + } + case "recordKeySet": { + walker.string(node.kind); + walkExprFields(node.obj, walker, skipType); + walker.string(node.shapeId); + walkExprFields(node.key, walker, skipType); + walkExprFields(node.value, walker, skipType); + return; + } + case "recordSet": { + walker.string(node.kind); + walkExprFields(node.obj, walker, skipType); + walker.string(node.shapeId); + walker.string(node.field); + walkExprFields(node.value, walker, skipType); + return; + } + case "rethrow": { + walker.string(node.kind); + walker.string(node.localId); + return; + } + case "return": { + walker.string(node.kind); + if (node.value !== undefined && node.value !== null) { + walkExprFields(node.value, walker, skipType); + } + return; + } + case "runtimeFence": { + walker.string(node.kind); + walker.string(node.code); + walker.string(node.message); + return; + } + case "switch": { + walker.string(node.kind); + walkExprFields(node.disc, walker, skipType); + for (const item40 of node.cases) { + if (item40.test !== undefined && item40.test !== null) { + walkExprFields(item40.test, walker, skipType); + } + for (const item41 of item40.body) { + walkStmtFields(item41, walker, skipType); + } + } + if (node.labels !== undefined && node.labels !== null) { + for (const item42 of node.labels) { + walker.string(item42); + } + } + return; + } + case "throw": { + walker.string(node.kind); + walkExprFields(node.value, walker, skipType); + return; + } + case "tryCatch": { + walker.string(node.kind); + for (const item43 of node.tryBody) { + walkStmtFields(item43, walker, skipType); + } + if (node.catchBody !== undefined && node.catchBody !== null) { + for (const item44 of node.catchBody) { + walkStmtFields(item44, walker, skipType); + } + } + if (node.catchLocalId !== undefined && node.catchLocalId !== null) { + walker.string(node.catchLocalId); + } + if (node.finallyBody !== undefined && node.finallyBody !== null) { + for (const item45 of node.finallyBody) { + walkStmtFields(item45, walker, skipType); + } + } + return; + } + case "varDecl": { + walker.string(node.kind); + walker.string(node.localId); + if (node.init !== undefined && node.init !== null) { + walkExprFields(node.init, walker, skipType); + } + return; + } + case "while": { + walker.string(node.kind); + walkExprFields(node.cond, walker, skipType); + for (const item46 of node.body) { + walkStmtFields(item46, walker, skipType); + } + if (node.labels !== undefined && node.labels !== null) { + for (const item47 of node.labels) { + walker.string(item47); + } + } + return; + } + } +} + +/** Walks every field of `node` but `loc` (and `type` with `skipType`), and + * every field of each object under it: the strings and nodes a recursive + * `for (const key in value)` walk with the same skips finds, read by name in + * the order ir.ts declares the fields. */ +export function walkTypeFields(node: IrType, walker: IrFieldWalker, skipType: boolean): void { + switch (node.kind) { + case "array": { + walker.string(node.kind); + walkTypeFields(node.elem, walker, skipType); + return; + } + case "bigint": { + walker.string(node.kind); + return; + } + case "bool": { + walker.string(node.kind); + return; + } + case "bytes": { + walker.string(node.kind); + walker.string(node.elem); + return; + } + case "caught": { + walker.string(node.kind); + return; + } + case "child": { + walker.string(node.kind); + return; + } + case "childStream": { + walker.string(node.kind); + return; + } + case "childWriter": { + walker.string(node.kind); + return; + } + case "classval": { + walker.string(node.kind); + walker.string(node.className); + return; + } + case "cryptoHash": { + walker.string(node.kind); + return; + } + case "cryptoHmac": { + walker.string(node.kind); + return; + } + case "date": { + walker.string(node.kind); + return; + } + case "dgramSocket": { + walker.string(node.kind); + return; + } + case "dir": { + walker.string(node.kind); + return; + } + case "dyn": { + walker.string(node.kind); + return; + } + case "f64": { + walker.string(node.kind); + return; + } + case "fileHandle": { + walker.string(node.kind); + return; + } + case "fsWatcher": { + walker.string(node.kind); + return; + } + case "func": { + walker.string(node.kind); + for (const item48 of node.params) { + walkTypeFields(item48, walker, skipType); + } + walkTypeFields(node.ret, walker, skipType); + if (node.restAbi !== undefined && node.restAbi !== null) { + walker.string(node.restAbi); + } + return; + } + case "generator": { + walker.string(node.kind); + walkTypeFields(node.yieldT, walker, skipType); + walkTypeFields(node.retT, walker, skipType); + walkTypeFields(node.nextT, walker, skipType); + return; + } + case "http2Session": { + walker.string(node.kind); + return; + } + case "http2Stream": { + walker.string(node.kind); + return; + } + case "httpClientReq": { + walker.string(node.kind); + return; + } + case "httpReq": { + walker.string(node.kind); + return; + } + case "httpRes": { + walker.string(node.kind); + return; + } + case "jsval": { + walker.string(node.kind); + return; + } + case "map": { + walker.string(node.kind); + walkTypeFields(node.key, walker, skipType); + walkTypeFields(node.value, walker, skipType); + return; + } + case "moduleNs": { + walker.string(node.kind); + walker.string(node.moduleId); + return; + } + case "netServer": { + walker.string(node.kind); + return; + } + case "netSocket": { + walker.string(node.kind); + return; + } + case "nullT": { + walker.string(node.kind); + return; + } + case "object": { + walker.string(node.kind); + walker.string(node.className); + return; + } + case "procStream": { + walker.string(node.kind); + return; + } + case "promise": { + walker.string(node.kind); + walkTypeFields(node.inner, walker, skipType); + return; + } + case "record": { + walker.string(node.kind); + walker.string(node.shapeId); + return; + } + case "regex": { + walker.string(node.kind); + return; + } + case "searchParams": { + walker.string(node.kind); + return; + } + case "secureCtx": { + walker.string(node.kind); + return; + } + case "set": { + walker.string(node.kind); + walkTypeFields(node.elem, walker, skipType); + return; + } + case "spawnRes": { + walker.string(node.kind); + return; + } + case "stats": { + walker.string(node.kind); + return; + } + case "string": { + walker.string(node.kind); + return; + } + case "symbol": { + walker.string(node.kind); + return; + } + case "testCtx": { + walker.string(node.kind); + return; + } + case "undefinedT": { + walker.string(node.kind); + return; + } + case "union": { + walker.string(node.kind); + walker.string(node.unionId); + return; + } + case "url": { + walker.string(node.kind); + return; + } + case "void": { + walker.string(node.kind); + return; + } + } +} diff --git a/packages/compiler/src/ir/fields.test.ts b/packages/compiler/src/ir/fields.test.ts new file mode 100644 index 000000000..8d1aaa433 --- /dev/null +++ b/packages/compiler/src/ir/fields.test.ts @@ -0,0 +1,111 @@ +import { mkdtemp, readFile, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile, deserializeModule } from "../index.js"; +import type { IrExpr, IrStmt } from "./ir.js"; +import { everyStmtList } from "./traverse.js"; +import { eachStringField, walkStmtFields } from "./fields.generated.js"; + +// Programs with many node kinds: classes, closures, async, generators, +// dynamic values, streams and records. +const PROGRAMS = [ + "1003-json-parse-unions.ts", + "114-live-record-delete.ts", + "1200-regex-test-basics.ts", + "1631-inspect-arrays.ts", + "4900-console-instances.ts", + "5612-readable-from-iterables.cjs", + "7110-als-completion-callbacks.ts", + "7321-speculative-loop-bail-sites.ts", +]; + +/** The string fields a key walk of `node` finds, as `field=value`. */ +function keyWalk(node: IrExpr | IrStmt): string[] { + const found: string[] = []; + for (const key in node) { + const value = (node as unknown as Record)[key]; + if (typeof value === "string") found.push(`${key}=${value}`); + } + return found.sort(); +} + +/** The events of a recursive key walk of `value` that skips `loc` (and + * `type` with `skipType`): `n:` for an IR node (an object with a kind + * and a location; `n:varDecl:` for a declaration), `s:` for + * each string. */ +function keyWalkDeep(value: unknown, events: string[], skipType: boolean): void { + if (typeof value === "string") events.push(`s:${value}`); + else if (Array.isArray(value)) for (const item of value) keyWalkDeep(item, events, skipType); + else if (value !== null && typeof value === "object") { + const record = value as Record; + if ("kind" in record && "loc" in record) events.push(nodeEvent(record as unknown as IrExpr)); + for (const key in record) + if (key !== "loc" && !(skipType && key === "type")) + keyWalkDeep(record[key], events, skipType); + } +} + +function nodeEvent(node: IrExpr | IrStmt): string { + return node.kind === "varDecl" ? `n:varDecl:${node.localId}` : `n:${node.kind}`; +} + +async function lowered(program: string, dir: string) { + const outPath = join(dir, "main.ir.json"); + const entry = join(import.meta.dirname, "../../../../tests/corpus", program); + const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" }); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + return deserializeModule(await readFile(outPath, "utf8")); +} + +// A key walk visits a node's fields in the order the lowering created them, +// which can differ from ir.ts's order between sibling fields (a dynKeyGet is +// built key first). The walk's callers count what they find, except that +// declarations must come in the same order. +test("walkStmtFields reports what a recursive key walk finds", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-ir-walk-")); + try { + for (const program of PROGRAMS) { + const mod = await lowered(program, dir); + for (const skipType of [false, true]) + for (const fn of mod.functions) { + const expected: string[] = []; + keyWalkDeep(fn.body, expected, skipType); + const events: string[] = []; + const walker = { + node: (node: IrExpr | IrStmt) => events.push(nodeEvent(node)), + string: (value: string) => events.push(`s:${value}`), + }; + for (const stmt of fn.body) walkStmtFields(stmt, walker, skipType); + const where = `${program}: ${fn.name}, skipType ${skipType}`; + expect([...events].sort(), where).toEqual([...expected].sort()); + const declarations = (list: string[]) => list.filter((e) => e.startsWith("n:varDecl:")); + expect(declarations(events), where).toEqual(declarations(expected)); + } + } + } finally { + await rm(dir, { recursive: true, force: true }); + } +}, 300_000); + +test("eachStringField reads every string field a key walk of a node finds", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-ir-fields-")); + try { + for (const program of PROGRAMS) { + const mod = await lowered(program, dir); + let nodes = 0; + const check = (node: IrExpr | IrStmt): boolean => { + const read: string[] = []; + eachStringField(node, (value, field) => read.push(`${field}=${value}`)); + expect(read.sort(), `${program}: ${node.kind}`).toEqual(keyWalk(node)); + nodes++; + return true; + }; + for (const fn of mod.functions) everyStmtList(fn.body, { expr: check, stmt: check }); + expect(nodes, program).toBeGreaterThan(0); + } + } finally { + await rm(dir, { recursive: true, force: true }); + } +}, 300_000); diff --git a/packages/compiler/src/ir/function-references.test.ts b/packages/compiler/src/ir/function-references.test.ts index 0e09a7567..48fa8a841 100644 --- a/packages/compiler/src/ir/function-references.test.ts +++ b/packages/compiler/src/ir/function-references.test.ts @@ -100,6 +100,7 @@ const EXPR_FIELDS: Classified = { unionIsTag: { unionId: "other" }, unionEq: { unionId: "other" }, jsonStringify: { indent: "other" }, + dynCheck: { inferredSite: "other" }, }; const STMT_FIELDS: Classified = { @@ -157,10 +158,14 @@ const MODULE_FIELDS: Record>, Role> = { "classes[].errorNameAfter": "other", "classes[].prototypeDataHelper": "reference", "classes[].instancePrototypeHelper": "reference", + "classes[].arrayFields": "reference", + "classes[].arraySpecies": "reference", + "classes[].stringTagHelper": "reference", "classes[].localCaptures[].name": "other", "classes[].localCaptures[].localId": "other", "classes[].symbolFields[].field": "other", "classes[].symbolFields[].globalId": "other", + "classes[].symbolFields[].wellKnown": "other", "classes[].fields[].name": "other", "classes[].methods[]": "method", "classes[].prototypeNames[]": "other", @@ -219,8 +224,19 @@ const MODULE_FIELDS: Record>, Role> = { "wasmUntranslated[].sha256": "other", "wasmUntranslated[].reason": "other", "threadSlotKeys[]": "other", + "foreignElementKeys[]": "other", + "freezableArrays[]": "other", "nodeIsland.libnode": "other", "nodeIsland.nodeVersion": "other", + "nodeIsland.modules.root": "other", + "nodeIsland.modules.location": "other", + "nodeIsland.modules.files[].path": "other", + "nodeIsland.modules.files[].sha256": "other", + "nodeIsland.modules.links[].path": "other", + "nodeIsland.modules.links[].target": "other", + "inferredFactFiles[].file": "other", + "inferredFactFiles[].pkg": "other", + "declOrigins[]": "other", }; const loc = { file: "references.ts", start: 0, end: 0 }; @@ -282,6 +298,18 @@ const REFERENCE_FIXTURES: Record IrModule> = { ...module(), classes: [{ name: "%m0.Cell", fields: [], instancePrototypeHelper: TARGET, loc }], }), + "classes[].arrayFields": () => ({ + ...module(), + classes: [{ name: "%m0.Row", fields: [], arrayElem: F64, arrayFields: TARGET, loc }], + }), + "classes[].arraySpecies": () => ({ + ...module(), + classes: [{ name: "%m0.Row", fields: [], arrayElem: F64, arraySpecies: TARGET, loc }], + }), + "classes[].stringTagHelper": () => ({ + ...module(), + classes: [{ name: "%m0.Cell", fields: [], stringTagHelper: TARGET, loc }], + }), "lib.exports[].fnName": () => ({ ...module(), lib: { diff --git a/packages/compiler/src/ir/function-references.ts b/packages/compiler/src/ir/function-references.ts index db474e9bb..61e6e2797 100644 --- a/packages/compiler/src/ir/function-references.ts +++ b/packages/compiler/src/ir/function-references.ts @@ -24,6 +24,9 @@ export function moduleFunctionReferences(mod: IrModule): string[] { for (const cls of mod.classes ?? []) { if (cls.prototypeDataHelper !== undefined) names.push(cls.prototypeDataHelper); if (cls.instancePrototypeHelper !== undefined) names.push(cls.instancePrototypeHelper); + if (cls.arrayFields !== undefined) names.push(cls.arrayFields); + if (cls.arraySpecies !== undefined) names.push(cls.arraySpecies); + if (cls.stringTagHelper !== undefined) names.push(cls.stringTagHelper); } return names; } diff --git a/packages/compiler/src/ir/int32-arrays.ts b/packages/compiler/src/ir/int32-arrays.ts index 02de7268f..0677b2ad1 100644 --- a/packages/compiler/src/ir/int32-arrays.ts +++ b/packages/compiler/src/ir/int32-arrays.ts @@ -1,3 +1,4 @@ +import { exprFunctionReference, moduleFunctionReferences } from "./function-references.js"; import { F64, type IrExpr, type IrFunction, type IrModule, type IrStmt } from "./ir.js"; type SwitchCase = Extract["cases"][number]; @@ -6,22 +7,29 @@ import { everyExprChild, everyStmtChild, everyStmtList, + mapExprChildren, + mapStmtChildren, transformExpr, transformStmt, transformStmtList, type IrTransform, } from "./traverse.js"; -/** Local number arrays the backend may store as int32 slots. +/** Number arrays the backend may store as int32 slots. * - * A candidate is a local `number[]` of a synchronous function that no - * other binding can hold and only compiled element operations reach: - * every write of the local stores a fresh array (an array literal, or - * `new Array(n).fill(v)`), and every read of it is the receiver of an - * element read, write, state test, `length`, `push` or `pop` (a discarded - * `pop` included), or the array of an element-reader helper call. The - * array never reaches a runtime function that reads its slots, a library - * call, a closure, a return or untyped code. + * A candidate is a `number[]` binding that only compiled element + * operations reach: a local of a synchronous function, a module global, or + * a parameter. Every write of a local or a global stores a fresh array (an + * array literal, or `new Array(n).fill(v)`); a parameter is never written. + * Every read of a candidate is the receiver of an element read, write, + * state test, `length`, `push` or `pop` (a discarded `pop` included), the + * array of an element-reader helper call, or the argument of a direct call + * whose parameter at that position is a candidate itself. A parameter is a + * candidate only when its function is reached by direct calls alone and + * every call passes a candidate there, so every binding that can hold one + * of these arrays is a candidate: the array never reaches a runtime + * function that reads its slots, a library call, a closure, a return or + * untyped code. * * Candidates are only proposed here. The int32 slot analysis * (int32-slots.ts) keeps the ones whose every stored element is a proven @@ -33,6 +41,18 @@ import { * calls are inlined first (rewriteCandidates), so no shared helper ever * receives an int32 array. */ +/** The candidate key of the binding `localId` as `fnName` reads it: + * `function\0local` for a local or parameter, `\0global` for a module + * global (`%g.` ids), which every function reads alike. */ +export function int32ArrayKey(fnName: string, localId: string): string { + return localId.startsWith("%g.") ? `\0${localId}` : `${fnName}\0${localId}`; +} + +/** The int32 element slot (int32-slots.ts) of a candidate key. */ +export function int32ArraySlot(key: string): string { + return key.startsWith("\0") ? `g:${key.slice(1)}` : `l:${key}`; +} + /** Array methods a candidate may be the receiver of: the runtime paths * behind them read and write int32 slots through the double-valued slot * accessors. */ @@ -202,10 +222,70 @@ function writesLocal(e: IrExpr, id: string): boolean { return !expr(e); } -/** The candidate locals of `fn` (see the module comment). */ -export function int32ArrayCandidates(fn: IrFunction, readers: ReadonlySet): Set { +/** Expression kinds an index can be built from without running any code. */ +const PURE_INDEX_KINDS: ReadonlySet = new Set([ + "varRef", + "numLit", + "boolLit", + "bin", + "unary", + "ternary", + "logical", + "arrayGet", + "arrayState", +]); + +/** Whether evaluating an index runs no code (no call, accessor or + * conversion): plain reads, literals and their operators only. */ +function pureIndex(e: IrExpr): boolean { + return PURE_INDEX_KINDS.has(e.kind) && everyExprChild(e, pureIndex, () => false); +} + +/** The parameters and globals a module's functions share as candidates, + * still assumed to qualify. */ +export interface SharedCandidates { + /** Candidate parameters, as `function\0local` keys. */ + params: ReadonlySet; + /** Candidate globals, as `%g.` ids. */ + globals: ReadonlySet; + /** Whether the parameter at `index` of `callee` is a candidate. */ + paramAt: (callee: string, index: number) => boolean; +} + +const NO_SHARED: SharedCandidates = { + params: new Set(), + globals: new Set(), + paramAt: () => false, +}; + +/** The candidates of `fn` (see the module comment): its own locals, its + * parameters in `shared.params`, and the globals of `shared.globals` that + * it reads only in candidate positions. `written` receives the globals it + * assigns fresh arrays. */ +export function int32ArrayCandidates( + fn: IrFunction, + readers: ReadonlySet, + shared: SharedCandidates = NO_SHARED, + written: Set = new Set(), +): Set { const result = new Set(); - if (fn.async || fn.generator) return result; + if (fn.async || fn.generator) { + // Globals read here leave the candidates. + for (const id of shared.globals) result.add(id); + everyStmtList(fn.body, { + expr: (e) => { + if ((e.kind === "varRef" || e.kind === "assignExpr") && result.has(e.localId)) + result.delete(e.localId); + return true; + }, + stmt: (s) => { + if ((s.kind === "varDecl" || s.kind === "assign") && result.has(s.localId)) + result.delete(s.localId); + return true; + }, + }); + return result; + } const captured = new Set( [...(fn.captures ?? []), ...(fn.classCaptures ?? [])].map((c) => c.localId), ); @@ -217,11 +297,12 @@ export function int32ArrayCandidates(fn: IrFunction, readers: ReadonlySet(); + const allocated = new Set(); // A receiver position: the candidate is read there without escaping. const receiver = (e: IrExpr): void => { if (e.kind !== "varRef") expr(e); @@ -255,13 +336,28 @@ export function int32ArrayCandidates(fn: IrFunction, readers: ReadonlySet { + if (shared.paramAt(e.callee, index)) receiver(arg); + else expr(arg); + }); return true; } } @@ -272,7 +368,8 @@ export function int32ArrayCandidates(fn: IrFunction, readers: ReadonlySet | undefined, live: ReadonlySet): Set { + const result = new Set(); + if (ids) for (const id of ids) if (live.has(id)) result.add(id); + return result; +} + +/** The candidates of every function of `mod` that has any, only those + * whose keys are in `keep` when given. Parameters and globals are shared: + * starting from every one that may qualify, each pass drops the ones a + * function reads outside candidate positions, the parameters some call + * passes anything but a candidate, and the globals never assigned a fresh + * array, until a pass drops nothing. Functions in `redirected` keep their + * parameters: some of their calls reach another function. */ +export function moduleInt32ArrayCandidates( + mod: IrModule, + readers: ReadonlySet, + keep: ReadonlySet | null = null, + redirected: ReadonlySet = new Set(), +): Map> { + const kept = (key: string): boolean => keep === null || keep.has(key); + const arrayParams = new Map(); + for (const fn of mod.functions) { + // Class methods, constructors and runtime helpers (`%` names) keep + // their parameters. + if (fn.name.startsWith("%") || fn.async || fn.generator || redirected.has(fn.name)) continue; + const indexes: number[] = []; + fn.params.forEach((p, i) => { + if (p.type.kind === "array" && p.type.elem.kind === "f64") indexes.push(i); + }); + if (indexes.length > 0) arrayParams.set(fn.name, indexes); + } + let globals = new Set(); + for (const g of mod.globals ?? []) + if ( + g.type.kind === "array" && + g.type.elem.kind === "f64" && + g.initFlag === undefined && + kept(`\0${g.id}`) + ) + globals.add(g.id); + const result = new Map>(); + if (arrayParams.size === 0 && globals.size === 0) { + for (const fn of mod.functions) { + const candidates = int32ArrayCandidates(fn, readers); + for (const id of candidates) if (!kept(`${fn.name}\0${id}`)) candidates.delete(id); + if (candidates.size > 0) result.set(fn.name, candidates); + } + return result; + } + // One walk: the functions reached other than by direct calls, the direct + // calls of functions with number[] parameters, the globals each function + // reads or writes, and whether a module namespace object exists (its + // member reads can reach any global by name). + const reached = new Set(moduleFunctionReferences(mod)); + const calls: { caller: string; call: IrExpr & { kind: "call" } }[] = []; + const globalsIn = new Map>(); + let namespaces = false; + for (const fn of mod.functions) { + const used = new Set(); + const note = (id: string): void => { + if (globals.has(id)) used.add(id); + }; + everyStmtList(fn.body, { + expr: (e) => { + const name = exprFunctionReference(e); + if (name !== undefined) reached.add(name); + if (e.kind === "moduleNsRef") namespaces = true; + if (e.kind === "call" && arrayParams.has(e.callee)) + calls.push({ caller: fn.name, call: e }); + if (e.kind === "varRef" || e.kind === "assignExpr") note(e.localId); + return true; + }, + stmt: (s) => { + if (s.kind === "varDecl" || s.kind === "assign") note(s.localId); + return true; + }, + }); + if (used.size > 0) globalsIn.set(fn.name, used); + } + if (namespaces) { + globals = new Set(); + globalsIn.clear(); + } + const paramIds = new Map(); + let params = new Set(); + for (const fn of mod.functions) { + const indexes = arrayParams.get(fn.name); + if (!indexes || reached.has(fn.name)) continue; + paramIds.set( + fn.name, + fn.params.map((p) => p.localId), + ); + for (const i of indexes) { + const key = `${fn.name}\0${fn.params[i]!.localId}`; + if (kept(key)) params.add(key); + } + } + for (;;) { + const current = params; + const written = new Set(); + const liveGlobals = new Set(globals); + result.clear(); + for (const fn of mod.functions) { + const shared: SharedCandidates = { + params: current, + globals: liveIn(globalsIn.get(fn.name), globals), + paramAt: (callee, index) => { + const id = paramIds.get(callee)?.[index]; + return id !== undefined && current.has(`${callee}\0${id}`); + }, + }; + const candidates = int32ArrayCandidates(fn, readers, shared, written); + for (const id of shared.globals) if (!candidates.has(id)) liveGlobals.delete(id); + for (const id of candidates) + if (!shared.globals.has(id) && !kept(`${fn.name}\0${id}`)) candidates.delete(id); + if (candidates.size > 0) result.set(fn.name, candidates); + } + for (const id of liveGlobals) if (!written.has(id)) liveGlobals.delete(id); + const live = new Set( + [...current].filter((key) => { + const cut = key.indexOf("\0"); + return result.get(key.slice(0, cut))?.has(key.slice(cut + 1)) === true; + }), + ); + // A parameter needs a candidate at that position of every call; a call + // with fewer arguments passes undefined. + for (const { caller, call } of calls) { + const ids = paramIds.get(call.callee); + if (!ids) continue; + ids.forEach((id, i) => { + const key = `${call.callee}\0${id}`; + if (!live.has(key)) return; + const arg = call.args[i]; + const ok = + arg?.kind === "varRef" && + (arg.localId.startsWith("%g.") + ? liveGlobals.has(arg.localId) + : result.get(caller)?.has(arg.localId) === true); + if (!ok) live.delete(key); + }); + } + if (live.size === current.size && liveGlobals.size === globals.size) break; + params = live; + globals = liveGlobals; + } + return result; +} + +/** The candidates one array can reach together: a direct call's argument + * and the parameter it binds hold the same array, so their element facts + * stand or fall together. `group` maps each of `keys` to its group's keys + * (itself alone for most), and `globalReaders` each candidate global's + * key to the functions that read or write it. */ +export function int32ArrayGroups( + mod: IrModule, + keys: ReadonlySet, +): { group: Map; globalReaders: Map> } { + const parent = new Map(); + const root = (key: string): string => { + let r = key; + while (parent.has(r)) r = parent.get(r)!; + if (r !== key) parent.set(key, r); + return r; + }; + const globalReaders = new Map>(); + const params = new Map(mod.functions.map((fn) => [fn.name, fn.params])); + const paramKeys = new Set( + mod.functions.flatMap((fn) => fn.params.map((p) => `${fn.name}\0${p.localId}`)), + ); + if ([...keys].some((key) => key.startsWith("\0") || paramKeys.has(key))) { + for (const fn of mod.functions) { + const touch = (id: string): void => { + const key = int32ArrayKey(fn.name, id); + if (!key.startsWith("\0") || !keys.has(key)) return; + const set = globalReaders.get(key) ?? new Set(); + set.add(fn.name); + globalReaders.set(key, set); + }; + everyStmtList(fn.body, { + expr: (e) => { + if (e.kind === "varRef" || e.kind === "assignExpr") touch(e.localId); + if (e.kind === "call") + e.args.forEach((arg, i) => { + const param = params.get(e.callee)?.[i]; + if (arg.kind !== "varRef" || !param) return; + const a = int32ArrayKey(fn.name, arg.localId), + p = `${e.callee}\0${param.localId}`; + if (!keys.has(a) || !keys.has(p)) return; + const ra = root(a), + rp = root(p); + if (ra !== rp) parent.set(ra, rp); + }); + return true; + }, + stmt: (s) => { + if (s.kind === "varDecl" || s.kind === "assign") touch(s.localId); + return true; + }, + }); + } + } + const members = new Map(); + for (const key of keys) { + const r = root(key); + const list = members.get(r); + if (list) list.push(key); + else members.set(r, [key]); + } + const group = new Map(); + for (const key of keys) group.set(key, members.get(root(key))!); + return { group, globalReaders }; +} + /** The element values stored into each candidate: element writes, pushed * values, literal elements and fill values. */ export function int32ArrayStores( @@ -341,16 +656,34 @@ export function int32ArrayStores( return stores; } +const REPEATED_INDEX_OPS = new Set(["+", "-", "*", "&", "|", "^", "<<", ">>", ">>>"]); + +/** An index the inlined read may evaluate at each of its uses: a plain read, + * a literal, or integer arithmetic over them. Repeating it keeps the forms + * the backend derives from its operands at every use, such as the exact i64 + * of an int32 product, where a bound local holds only the double. */ +function repeatableIndex(e: IrExpr): boolean { + if (e.kind === "varRef" || e.kind === "numLit") return true; + if (e.kind === "unary") return e.op === "-" && repeatableIndex(e.operand); + return ( + e.kind === "bin" && + REPEATED_INDEX_OPS.has(e.op) && + repeatableIndex(e.left) && + repeatableIndex(e.right) + ); +} + /** Rewrite a function's candidates for the backend. A discarded pop uses * the candidate in place of its hidden local. An element-reader helper call * whose array is a candidate is inlined: the helper's returned ternary, with * the candidate for its array and the index for its index, so the read means - * the same. An index that is not a plain read or literal is bound to a fresh - * local first: in its own declaration just before the statement when the - * call is the whole value of a declaration or assignment (the form lowering - * gives `a[i]!`, and the one the backend reads without a box), otherwise in a - * sequence around the ternary. Selection keeps out indexes that write the - * array, so evaluating the index before the array read is unobservable. */ + * the same. An index that is not repeatable (repeatableIndex) is bound to a + * fresh local first: in its own declaration just before the statement when + * the call is the whole value of a declaration or assignment (the form + * lowering gives `a[i]!`, and the one the backend reads without a box), + * otherwise in a sequence around the ternary. Selection keeps out indexes + * that write the array, so evaluating the index before the array read is + * unobservable. */ function rewriteCandidates( fn: IrFunction, candidates: ReadonlySet, @@ -374,14 +707,13 @@ function rewriteCandidates( candidates.has(e.args[0]!.localId) ? e : null; - const simple = (e: IrExpr): boolean => e.kind === "varRef" || e.kind === "numLit"; // The statements of a list, with complex read indexes declared first. const hoist = (list: IrStmt[]): IrStmt[] => list.flatMap((s): IrStmt[] => { if (s.kind !== "varDecl" && s.kind !== "assign") return [s]; const value = s.kind === "varDecl" ? s.init : s.value; const call = readerCall(value); - if (!call || simple(call.args[1]!)) return [s]; + if (!call || repeatableIndex(call.args[1]!)) return [s]; const { decl, ref } = bind(call.args[1]!); const read: IrExpr = { ...call, args: [call.args[0]!, ref] }; if (s.kind === "varDecl") return [decl, { ...s, init: read }]; @@ -444,21 +776,23 @@ function rewriteCandidates( if (ret.kind !== "return" || !ret.value) return e; let index = call.args[1]!; const stmts: IrStmt[] = []; - if (!simple(index)) { + if (!repeatableIndex(index)) { const { decl, ref } = bind(index); stmts.push(decl); index = ref; } const operand = index; - const body = transformExpr(reparseExpr(ret.value), { - expr: (x) => { - if (x.kind !== "varRef") return x; - if (x.localId === arrayParam!.localId) return reparseExpr(array); - if (x.localId === indexParam!.localId) return reparseExpr(operand); - return x; - }, - stmt: (x) => x, - }); + // The arguments' copies are not searched again: an index may read a + // local that has the id of one of the helper's parameters. + const substituteExpr = (x: IrExpr): IrExpr => { + if (x.kind !== "varRef") return mapExprChildren(x, substituteExpr, substituteStmt); + if (x.localId === arrayParam!.localId) return reparseExpr(array); + if (x.localId === indexParam!.localId) return reparseExpr(operand); + return x; + }; + const substituteStmt = (x: IrStmt): IrStmt => + mapStmtChildren(x, substituteExpr, substituteStmt); + const body = substituteExpr(reparseExpr(ret.value)); // The ternary is a fresh copy (reparseExpr): it takes the call's location. const typed = body; typed.loc = e.loc; @@ -471,45 +805,169 @@ function rewriteCandidates( return { ...fn, body, locals: locals.filter((local) => !popLocals.has(local.id)) }; } -/** Propose the module's int32 array candidates (`function\0local` keys in +/** A module marked for its int32 array candidates: the candidates of + * each function (int32ArrayCandidates) and the element readers the marking + * inlines. */ +interface Int32ArrayMarking { + readonly mod: IrModule; + readonly readers: ReadonlyMap; + readonly candidates: ReadonlyMap>; +} + +/** markInt32ArrayCandidates with the candidates it marked; null when it + * marks none. */ +function markCandidates( + mod: IrModule, + keep: ReadonlySet | null, + redirected: ReadonlySet, +): Int32ArrayMarking | null { + if (mod.lib !== undefined) return null; + const readers = new Map(); + for (const fn of mod.functions) if (isElementReader(fn)) readers.set(fn.name, fn); + const all = moduleInt32ArrayCandidates(mod, new Set(readers.keys()), keep, redirected); + if (all.size === 0) return null; + const keys = new Set(); + const functions = mod.functions.map((fn) => { + const candidates = all.get(fn.name); + if (!candidates) return fn; + for (const id of candidates) keys.add(int32ArrayKey(fn.name, id)); + return rewriteCandidates(fn, candidates, readers); + }); + return { + mod: { ...mod, functions, int32ArrayCandidates: [...keys] }, + readers, + candidates: all, + }; +} + +/** Propose the module's int32 array candidates (int32ArrayKey keys in * `int32ArrayCandidates`), only those in `keep` when given, and rewrite * their functions. */ export function markInt32ArrayCandidates( mod: IrModule, keep: ReadonlySet | null = null, + redirected: ReadonlySet = new Set(), ): IrModule { - if (mod.lib !== undefined) return mod; - const readers = new Map(); - for (const fn of mod.functions) if (isElementReader(fn)) readers.set(fn.name, fn); - const names = new Set(readers.keys()); - const keys: string[] = []; - const functions = mod.functions.map((fn) => { - const candidates = int32ArrayCandidates(fn, names); - for (const id of candidates) - if (keep === null || keep.has(`${fn.name}\0${id}`)) keys.push(`${fn.name}\0${id}`); - else candidates.delete(id); - return candidates.size === 0 ? fn : rewriteCandidates(fn, candidates, readers); - }); - return keys.length === 0 ? mod : { ...mod, functions, int32ArrayCandidates: keys }; + return markCandidates(mod, keep, redirected)?.mod ?? mod; +} + +/** Revises a function of a module marked for int32 array candidates when + * some of them drop: the function marked for its candidates outside + * `dropped` (int32ArrayKey keys), which is the unmarked function when none + * is left. */ +export type Int32ArrayRevision = (fnName: string, dropped: ReadonlySet) => IrFunction; + +/** What a proof of a marked module (selectInt32Arrays' `prove`) keeps. */ +export interface Int32ArrayProof { + /** The candidates it keeps. */ + readonly kept: ReadonlySet; + /** The functions of the module it proved, in module order, when it + * revised some through the Int32ArrayRevision it was given; otherwise + * null, for the marked module's own. */ + readonly functions: readonly IrFunction[] | null; } /** The module the backend emits: rewritten for exactly the candidates the - * int32 slot analysis (`prove`, returning its int32Arrays) keeps, so a - * module without int32 arrays is `mod` itself. A dropped candidate's - * rewrite is undone by marking again without it, until every candidate - * marked is kept. */ + * int32 slot analysis (`prove`) keeps, so a module without int32 arrays is + * `mod` itself. `redirected` names the functions some of whose calls the + * backend emits as calls of another function (a result-slot entry): their + * parameters keep doubles. + * + * The proof of the marked module gets an Int32ArrayRevision: as candidates + * drop, it revises their functions and goes on proving the revised module, + * so one analysis ends with the module marked for the candidates it keeps. + * That module is the answer when marking for those candidates again proposes + * exactly them, in the functions the proof revised. A proof that cannot + * follow a revision answers null; then each round marks the module, proves + * it without revisions, and marks again without the dropped candidates, + * until a round keeps every candidate it marks (four rounds at most). */ export function selectInt32Arrays( mod: IrModule, - prove: (marked: IrModule) => ReadonlySet, + prove: (marked: IrModule, revise: Int32ArrayRevision | null) => Int32ArrayProof | null, + redirected: (mod: IrModule) => ReadonlySet = () => new Set(), ): IrModule { + const others = redirected(mod); + const marking = markCandidates(mod, null, others); + if (marking === null) return mod; + const original = new Map(mod.functions.map((fn) => [fn.name, fn])); + const markedFunctions = new Map(marking.mod.functions.map((fn) => [fn.name, fn])); + // Each function's latest revision and the candidates it is marked for. + const revisions = new Map }>(); + const revise: Int32ArrayRevision = (fnName, dropped) => { + const candidates = marking.candidates.get(fnName) ?? new Set(); + const ids = new Set(); + for (const id of candidates) if (!dropped.has(int32ArrayKey(fnName, id))) ids.add(id); + const latest = revisions.get(fnName); + if (latest !== undefined && latest.ids.size === ids.size) return latest.fn; + if (latest === undefined && ids.size === candidates.size) return markedFunctions.get(fnName)!; + const fn = + ids.size === 0 + ? original.get(fnName)! + : rewriteCandidates(original.get(fnName)!, ids, marking.readers); + revisions.set(fnName, { fn, ids }); + return fn; + }; + const proof = prove(marking.mod, revise); + const proposed = marking.mod.int32ArrayCandidates ?? []; let keep: ReadonlySet | null = null; - for (let round = 0; round < 4; round++) { - const marked = markInt32ArrayCandidates(mod, keep); + let round = 0; + if (proof !== null && proof.functions === null) { + // The marked module itself was proved: the first round. + if (proposed.every((key) => proof.kept.has(key))) return marking.mod; + keep = new Set(proposed.filter((key) => proof.kept.has(key))); + round = 1; + } else if (proof !== null) { + const settled = settleRevisions(mod, marking, proof, revisions, others); + if (settled !== null) return settled; + } + for (; round < 4; round++) { + const marked = markInt32ArrayCandidates(mod, keep, others); if (marked === mod) return mod; - const kept = prove(marked); - const proposed = marked.int32ArrayCandidates ?? []; - if (proposed.every((key) => kept.has(key))) return marked; - keep = new Set(proposed.filter((key) => kept.has(key))); + const kept = prove(marked, null)!.kept; + const roundProposed = marked.int32ArrayCandidates ?? []; + if (roundProposed.every((key) => kept.has(key))) return marked; + keep = new Set(roundProposed.filter((key) => kept.has(key))); } return mod; } + +/** The module marked for the candidates a revising proof kept, built from + * the functions it proved: null unless marking `mod` for those candidates + * proposes, in each function, exactly the candidates its proved function is + * marked for. `mod` itself when it proposes none. */ +function settleRevisions( + mod: IrModule, + marking: Int32ArrayMarking, + proof: Int32ArrayProof, + revisions: ReadonlyMap }>, + redirected: ReadonlySet, +): IrModule | null { + const kept = proof.kept; + const functions = proof.functions!; + if (functions.length !== mod.functions.length) return null; + const all = moduleInt32ArrayCandidates( + mod, + new Set(marking.readers.keys()), + new Set([...(marking.mod.int32ArrayCandidates ?? [])].filter((key) => kept.has(key))), + redirected, + ); + const keys = new Set(); + for (let i = 0; i < mod.functions.length; i++) { + const fn = mod.functions[i]!; + const proved = functions[i]!; + const candidates = all.get(fn.name); + const revision = revisions.get(fn.name); + const marked = revision?.ids ?? marking.candidates.get(fn.name) ?? new Set(); + if ((candidates?.size ?? 0) !== marked.size) return null; + if (candidates !== undefined) + for (const id of candidates) { + if (!marked.has(id)) return null; + keys.add(int32ArrayKey(fn.name, id)); + } + const expected = + revision?.fn ?? (marking.candidates.has(fn.name) ? marking.mod.functions[i]! : fn); + if (proved !== expected) return null; + } + if (all.size === 0) return mod; + return { ...mod, functions: [...functions], int32ArrayCandidates: [...keys] }; +} diff --git a/packages/compiler/src/ir/int32-slots.test.ts b/packages/compiler/src/ir/int32-slots.test.ts index be933ab38..b94dee341 100644 --- a/packages/compiler/src/ir/int32-slots.test.ts +++ b/packages/compiler/src/ir/int32-slots.test.ts @@ -281,6 +281,7 @@ test("dynamic stores disqualify the field names they can reach", () => { "dyn.typeof", "insp.dyn", "insp.dynS", + "insp.dynHidden", "dyn.objCreate", "dyn.getPrototype", "dyn.nameClassPrototype", @@ -357,6 +358,52 @@ test("an Error subclass constructor forwarding dynamic arguments stores no class expect(slots.isField("C", "a")).toBe(true); }); +test("Buffer.from over an ArrayBuffer range and Buffer as a value keep class fields int32", () => { + // `Buffer.from(view.buffer, offset, length)` passes its operands as + // checked values, and a program that reads the Buffer function's value + // installs it in %main; neither stores into a class instance. + const classes = [cls("C", ["a"])]; + const view: IrExpr = { loc, kind: "dynFrom", value: ref("o", obj("C")), type: DYN }; + const libCall = (fnName: string, args: IrExpr[], type: IrType = VOID): IrStmt => + stmt({ loc, kind: "libCall", fn: fnName, args, type } as IrExpr); + const slots = analyzeInt32Slots( + mod(classes, [ + fn( + "load", + [ + { id: "o", type: obj("C") }, + { id: "storage", type: DYN }, + ], + [ + set(ref("o", obj("C")), "C", "a", num(1)), + libCall("dyn.typedRefIs", [ + view, + { loc, kind: "strLit", value: "object:C", type: STRING }, + ]), + libCall("buffer.installValue", []), + libCall("buffer.constructor", [], DYN), + libCall( + "buffer.poolFromDynRange", + [ref("storage", DYN), num(2), num(4)].map((arg): IrExpr => + arg.type.kind === "dyn" ? arg : { loc, kind: "dynFrom", value: arg, type: DYN }, + ), + ), + libCall("buffer.poolFromDynOffset", [ + ref("storage", DYN), + { + loc, + kind: "dynFrom", + value: num(2), + type: DYN, + }, + ]), + ], + ), + ]), + ); + expect(slots.isField("C", "a")).toBe(true); +}); + test("captured bindings share one proof across the declaring function and its closures", () => { const closure: IrExpr = { loc, kind: "closure", fnName: "inner", captures: ["s"], type: DYN }; const outer = (): IrFunction => ({ @@ -476,6 +523,12 @@ test("counting-down recursion proves its parameter from the call-site ranges", ( ); const rising = analyzeInt32Slots(mod([], [up, fn("user", [], [stmt(call("up", [num(-18)]))])])); expect(rising.facts("up").params?.get("d")).toEqual({ min: -18, max: 0 }); + // The same with the caller first, so the fixpoint analyzes it after the + // parameter dropped: the narrowing still sees its argument. + const callerFirst = analyzeInt32Slots( + mod([], [fn("user", [], [stmt(call("up", [num(-18)]))]), up]), + ); + expect(callerFirst.facts("up").params?.get("d")).toEqual({ min: -18, max: 0 }); // A negative start counts down past the smallest int32. const negative = analyzeInt32Slots(mod([], [count, even, odd, user([18, -1])])); diff --git a/packages/compiler/src/ir/int32-slots.ts b/packages/compiler/src/ir/int32-slots.ts index 8b992e6a8..1334b9b99 100644 --- a/packages/compiler/src/ir/int32-slots.ts +++ b/packages/compiler/src/ir/int32-slots.ts @@ -20,8 +20,14 @@ import { type IrNodeKind, } from "./node-index.js"; import { findInitializerBindings, withInitializerBindings } from "./initializer-bindings.js"; -import { checkableInt32Allocations, int32ArrayStores } from "./int32-arrays.js"; -import { everyExprChild, everyStmtChild, everyTypeChild } from "./traverse.js"; +import { + checkableInt32Allocations, + int32ArrayGroups, + int32ArrayKey, + int32ArraySlot, + int32ArrayStores, +} from "./int32-arrays.js"; +import { everyExprChild, everyStmtChild, everyStmtList, everyTypeChild } from "./traverse.js"; /** The storage slot an array-valued expression reads, as an * IrContracts.int32Elements key: a local or parameter @@ -257,9 +263,10 @@ export class Int32Slots { /** Whether the module has int32 array candidates (the narrowedReads * range fact). */ narrowedReads = false; - /** Local number arrays (`function\0local`) stored as int32 slots: the - * module's int32 array candidates whose every stored element the - * fixpoint proved an int32. */ + /** Number arrays stored as int32 slots (int32ArrayKey keys: locals and + * parameters as `function\0local`, globals as `\0global`): the module's + * int32 array candidates whose every stored element, through every + * binding that can hold the same array, the fixpoint proved an int32. */ readonly int32Arrays = new Set(); /** Local number arrays that may hold int32 slots with checked stores: the * candidates whose element stores the fixpoint proved exact integers but @@ -345,6 +352,123 @@ export class Int32Slots { for (const params of this.paramFacts.values()) count += params.size; return count; } + + /** The analysis as JSON, for a process that shares this module and its + * node index (a forked one). Proven writes are numbered by their position + * in `nodes`. Null when a proven write is not in the index. */ + encode(nodes: IrNodeIndex): string | null { + const proven = nodes.expressionPositions(this.provenWrites); + if (proven === null) return null; + const data: Int32SlotsData = { + fields: [...this.fields], + familyOf: [...this.familyOf], + paramFacts: [...this.paramFacts].map(([fn, params]) => [fn, [...params]]), + returns: [...this.returns], + returnRanges: [...this.returnRanges], + boxes: [...this.boxes], + boxGroupOf: [...this.boxGroupOf], + provenWrites: proven, + called: [...this.called], + reasons: [...this.reasons], + pinned: [...this.pinned], + int32Locals: [...this.int32Locals], + int32Elements: [...this.int32Elements], + pinViolations: this.pinViolations, + nonNegative: [...this.nonNegative], + narrowedReads: this.narrowedReads, + int32Arrays: [...this.int32Arrays], + checkedInt32Arrays: [...this.checkedInt32Arrays], + }; + return JSON.stringify(data); + } + + /** The analysis `encode` wrote for `mod`, whose node index is `nodes`. */ + static decode(text: string, mod: IrModule, nodes: IrNodeIndex): Int32Slots { + const data = JSON.parse(text) as Int32SlotsData; + const slots = new Int32Slots(new Map((mod.classes ?? []).map((c) => [c.name, c]))); + for (const family of data.fields) slots.fields.add(family); + for (const [key, family] of data.familyOf) slots.familyOf.set(key, family); + for (const [fn, params] of data.paramFacts) slots.paramFacts.set(fn, new Map(params)); + for (const fn of data.returns) slots.returns.add(fn); + for (const [fn, range] of data.returnRanges) slots.returnRanges.set(fn, range); + for (const group of data.boxes) slots.boxes.add(group); + for (const [member, group] of data.boxGroupOf) slots.boxGroupOf.set(member, group); + for (const node of nodes.expressionsAt(data.provenWrites)) slots.provenWrites.add(node); + for (const fn of data.called) slots.called.add(fn); + for (const [key, why] of data.reasons) slots.reasons.set(key, why); + for (const key of data.pinned) slots.pinned.add(key); + for (const key of data.int32Locals) slots.int32Locals.add(key); + for (const key of data.int32Elements) slots.int32Elements.add(key); + slots.pinViolations.push(...data.pinViolations); + for (const key of data.nonNegative) slots.nonNegative.add(key); + slots.narrowedReads = data.narrowedReads; + for (const key of data.int32Arrays) slots.int32Arrays.add(key); + for (const key of data.checkedInt32Arrays) slots.checkedInt32Arrays.add(key); + return slots; + } + + /** Every fact, in a canonical order, with each proven write as its + * position in `mod` (function index, then walk order): two analyses of + * equal modules hold the same facts when their summaries are equal. */ + summary(mod: IrModule): string { + const sorted = (values: Iterable): string => [...values].sort().join(","); + const lines = [ + `fields ${sorted(this.fields)}`, + `params ${sorted([...this.paramFacts].flatMap(([fn, params]) => [...params].map(([id, r]) => `${fn}\0${id}=${r.min}:${r.max}`)))}`, + `returns ${sorted(this.returns)}`, + `returnRanges ${sorted([...this.returnRanges].map(([fn, r]) => `${fn}=${r.min}:${r.max}`))}`, + `boxes ${sorted(this.boxes)}`, + `boxGroups ${sorted([...this.boxGroupOf].map(([member, group]) => `${member}=${group}`))}`, + `called ${sorted(this.called)}`, + `pinned ${sorted(this.pinned)}`, + `int32Locals ${sorted(this.int32Locals)}`, + `int32Elements ${sorted(this.int32Elements)}`, + `nonNegative ${sorted(this.nonNegative)}`, + `pinViolations ${sorted(this.pinViolations)}`, + `narrowedReads ${this.narrowedReads}`, + `int32Arrays ${sorted(this.int32Arrays)}`, + `checkedInt32Arrays ${sorted(this.checkedInt32Arrays)}`, + ]; + const positions: string[] = []; + mod.functions.forEach((fn, f) => { + let at = 0; + everyStmtList(fn.body, { + expr: (e) => { + if (this.provenWrites.has(e)) positions.push(`${f}:${at}`); + at++; + return true; + }, + stmt: () => { + at++; + return true; + }, + }); + }); + lines.push(`provenWrites ${this.provenWrites.size} ${positions.join(",")}`); + return lines.join("\n"); + } +} + +/** Int32Slots as plain data (Int32Slots.encode). */ +interface Int32SlotsData { + fields: string[]; + familyOf: [string, string | null][]; + paramFacts: [string, [string, IntegerRange][]][]; + returns: string[]; + returnRanges: [string, IntegerRange][]; + boxes: string[]; + boxGroupOf: [string, string][]; + provenWrites: number[]; + called: string[]; + reasons: [string, string][]; + pinned: string[]; + int32Locals: string[]; + int32Elements: string[]; + pinViolations: string[]; + nonNegative: string[]; + narrowedReads: boolean; + int32Arrays: string[]; + checkedInt32Arrays: string[]; } const DYN_KINDS = new Set(["dyn", "jsval", "caught"]); @@ -396,6 +520,10 @@ const DYN_READ_ONLY = new Set([ "dyn.bagGet", "dyn.bitwise", "dyn.compare", + // Adoption reads the native value it was built into and turns shared + // source nodes into capsules of it; it stores no property into a capsule. + "dyn.declAdopt", + "dyn.adoptShared", "dyn.forInKeyStrings", "dyn.forInKeys", "dyn.forInStable", @@ -442,8 +570,29 @@ const DYN_READ_ONLY = new Set([ "error.new", "error.newOptions", "fs.utimesSync", + // Buffer.prototype members read as values: a lookup by key. + "buffer.prototypeMember", + // Buffer.from(value, offset[, length]) over checked values: an + // ArrayBuffer's view after Node's offset and length conversions, or a copy + // of a string, array-like or typed array's values. + "buffer.poolFromDynOffset", + "buffer.poolFromDynRange", "insp.dyn", "insp.dynS", + // util.format and util.inspect over checked values: the inspect walk and + // the format conversions read them; insp.registerCustom keeps a class's + // custom inspect thunk in the runtime's registry. + "insp.customIsSelf", + "insp.customResult", + "insp.format", + "insp.formatList", + "insp.formatPrimitives", + "insp.formatSpecifiesDyn", + "insp.formatWithOptions", + "insp.inspect", + "insp.registerCustom", + "insp.dynFormatS", + "insp.dynHidden", // The dynamic-tier operators run their dyn twins (scr_js_value.c); the // conversions move a value between dyn and jsval, and js.toF64 is // dynCheck's number check. @@ -818,6 +967,95 @@ export interface Int32SlotInputs { initializerBindings?: ReadonlyMap; /** The module's node index, read instead of walking the bodies. */ nodes?: IrNodeIndex; + /** Revises a function of a module marked for int32 array candidates when + * candidates drop (int32-arrays.ts Int32ArrayRevision): the function + * marked without the `dropped` candidates. With it, a candidate that drops + * (and keeps no checked stores) takes its sites back to the element-reader + * calls the marking inlined, and the fixpoint goes on over the revised + * functions, so its result is the analysis of the module marked for the + * candidates it keeps. A revision that changes more of a function than + * those sites throws Int32RevisionConflict. */ + revise?: (fnName: string, dropped: ReadonlySet) => IrFunction; +} + +/** A revision (Int32SlotInputs.revise) changed a function in a way the + * fixpoint cannot follow: a call of a function no other call reaches, or + * any change besides element-reader calls taking the place of their + * inlined reads. The module needs an analysis of its own. */ +export class Int32RevisionConflict extends Error {} + +/** What the analysis's setup reads from a function body, besides direct + * calls: a revision must leave all of it unchanged. */ +function revisionStructure(fn: IrFunction): { + calls: { callee: string; args: number }[]; + rest: string; +} { + const calls: { callee: string; args: number }[] = []; + const rest: string[] = []; + const boxed = new Set(fn.locals.filter((local) => local.boxed).map((local) => local.id)); + const expr = (e: IrExpr): boolean => { + const referenced = exprFunctionReference(e); + if (referenced !== undefined) rest.push(`ref:${referenced}`); + switch (e.kind) { + case "call": + calls.push({ callee: e.callee, args: e.args.length }); + break; + case "new": + rest.push(`new:${e.className}:${e.args.length}`); + break; + case "varRef": + if (boxed.has(e.localId)) rest.push(`read:${e.localId}`); + break; + case "assignExpr": + case "incDec": + rest.push(`${e.kind}:${e.localId}`); + break; + case "fieldGet": + case "fieldIncDec": + rest.push(`${e.kind}:${e.className}\0${e.field}`); + break; + case "closure": + rest.push(`closure:${e.fnName}:${e.captures.join(",")}`); + break; + case "classRef": + rest.push(`classRef:${e.className}:${(e.captures ?? []).join(",")}`); + break; + case "virtualCall": + rest.push(`virtualCall:${e.className}.${e.method}`); + break; + case "instanceOfValue": + rest.push(`instanceOf:${e.classValue.kind}`); + break; + } + if (BOUNDARY_KINDS.has(e.kind)) rest.push(`boundary:${e.kind}`); + return everyExprChild(e, expr, stmt); + }; + const stmt = (s: IrStmt): boolean => { + switch (s.kind) { + case "forOf": + rest.push(`forOf:${s.localId}`); + break; + case "tryCatch": + if (s.catchLocalId) rest.push(`catch:${s.catchLocalId}`); + break; + case "assign": + case "varDecl": + if (boxed.has(s.localId)) rest.push(`${s.kind}:${s.localId}`); + break; + case "throw": + rest.push("throw"); + break; + } + return everyStmtChild(s, expr, stmt); + }; + for (const s of fn.body) stmt(s); + rest.sort(); + for (const local of fn.locals) + if (local.boxed || local.tdz) + rest.push(`local:${local.id}:${local.boxed === true}:${local.tdz === true}`); + for (const capture of fn.captures ?? []) rest.push(`capture:${capture.localId}`); + for (const capture of fn.classCaptures ?? []) rest.push(`classCapture:${capture.localId}`); + return { calls, rest: rest.join("\n") }; } export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}): Int32Slots { @@ -847,17 +1085,29 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}): for (const key of contracts?.int32Locals ?? []) slots.addInt32Local(key); for (const key of contracts?.int32Elements ?? []) slots.addInt32Elements(key); // Int32 array candidates (ir/int32-arrays.ts): assumed to hold int32 - // elements until a store the fixpoint cannot prove drops one. + // elements until a store the fixpoint cannot prove drops one, with every + // candidate the same array can reach (its group). const arrayCandidates = new Map>(); - slots.narrowedReads = (mod.int32ArrayCandidates?.length ?? 0) > 0; - for (const key of mod.int32ArrayCandidates ?? []) { + const globalArrays = new Set(); + const arrayKeys = new Set(mod.int32ArrayCandidates ?? []); + slots.narrowedReads = arrayKeys.size > 0; + for (const key of arrayKeys) { const cut = key.indexOf("\0"); - const fnName = key.slice(0, cut); - const set = arrayCandidates.get(fnName) ?? new Set(); - set.add(key.slice(cut + 1)); - arrayCandidates.set(fnName, set); - slots.addInt32Elements(`l:${key}`); + if (cut === 0) globalArrays.add(key.slice(1)); + else { + const fnName = key.slice(0, cut); + const set = arrayCandidates.get(fnName) ?? new Set(); + set.add(key.slice(cut + 1)); + arrayCandidates.set(fnName, set); + } + slots.addInt32Elements(int32ArraySlot(key)); } + const arrayGroups = arrayKeys.size > 0 ? int32ArrayGroups(mod, arrayKeys) : null; + // Functions that read or write a candidate global. + const globalArrayFunctions = new Set(); + if (arrayGroups) + for (const names of arrayGroups.globalReaders.values()) + for (const name of names) globalArrayFunctions.add(name); /* ── candidate fields ── */ const children = new Map(); @@ -1139,6 +1389,7 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}): const candidateWrites = nodes?.scan(CANDIDATE_WRITE_KINDS); const hasCandidateWrite = (fn: IrFunction): boolean => { if (arrayCandidates.has(fn.name)) return true; + if (globalArrayFunctions.has(fn.name)) return true; const box = (localId: string): boolean => slots.hasBox(slots.boxGroup(fn.name, localId)); const returns = slots.hasReturn(fn.name); let found = false; @@ -1176,12 +1427,45 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}): }); return found; }; + // Candidates dropped without checked stores, and the revision of the + // functions that hold them (Int32SlotInputs.revise). + const dropped = new Set(); + const revise = (names: ReadonlySet): void => { + for (const name of names) { + const previous = functions.get(name)!; + const next = inputs.revise!(name, dropped); + if (next === previous) continue; + const before = revisionStructure(previous); + const after = revisionStructure(next); + if (before.rest !== after.rest) throw new Int32RevisionConflict(name); + // The revision adds the element-reader calls back: each must be of a + // function other calls already reach, with its arity. + const counts = new Map(); + for (const { callee } of before.calls) counts.set(callee, (counts.get(callee) ?? 0) + 1); + for (const { callee, args } of after.calls) { + const left = counts.get(callee) ?? 0; + if (left > 0) { + counts.set(callee, left - 1); + continue; + } + const target = functions.get(callee); + if (!called.has(callee) || target === undefined || target.params.length !== args) + throw new Int32RevisionConflict(name); + reads(`ret:${callee}`, name); + } + for (const left of counts.values()) if (left > 0) throw new Int32RevisionConflict(name); + functions.set(name, next); + } + }; const provenByFunction = new Map(); // Each function's parameter and result writes under its latest analysis: // at the fixpoint, the ranges of every write under the final facts. const lastWrites = new Map>(); - while (queue.length) { - const fn = functions.get(queue.pop()!)!; + // First in, first out: a function whose slots change while others wait + // goes after them, so a caller of many callees is analyzed again once + // for all of their changes, not once for each. + for (let head = 0; head < queue.length; head++) { + const fn = functions.get(queue[head]!)!; queued.delete(fn.name); const params = slots.facts(fn.name).params; if (params) for (const id of params.keys()) reads(`param:${fn.name}\0${id}`, fn.name); @@ -1209,27 +1493,83 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}): // (checkableInt32Allocations), it keeps int32 slots with checked stores // (checkedInt32Arrays), which widen it to doubles in place at the first // store that is not an int32. - const arrays = arrayCandidates.get(fn.name); - if (arrays) { - let dropped = false; + const locals = arrayCandidates.get(fn.name); + let arrays = locals; + if (globalArrayFunctions.has(fn.name)) { + const both = new Set(); + if (locals) for (const id of locals) both.add(id); + for (const id of globalArrays) both.add(id); + arrays = both; + } + // Functions whose candidates this visit drops, revised after it. + const revised = new Set(); + if (arrays && arrayGroups) { + const requeue = (name: string): void => { + if (queued.has(name)) return; + queued.add(name); + queue.push(name); + }; + const params = new Set(fn.params.map((param) => param.localId)); for (const [id, values] of int32ArrayStores(fn, arrays)) { - const key = `l:${fn.name}\0${id}`; - if (!slots.hasInt32ElementSlot(key) || values.every(int32)) continue; - slots.dropInt32Elements(key); + const key = int32ArrayKey(fn.name, id); + if (!slots.hasInt32ElementSlot(int32ArraySlot(key)) || values.every(int32)) continue; + // The array's group drops together: every function reading a + // member is analyzed again without its facts. + const group = arrayGroups.group.get(key) ?? [key]; + for (const member of group) { + slots.dropInt32Elements(int32ArraySlot(member)); + if (member.startsWith("\0")) { + const names = arrayGroups.globalReaders.get(member); + if (names) for (const name of names) requeue(name); + } else requeue(member.slice(0, member.indexOf("\0"))); + } + // Checked stores serve a lone local: its allocations are the only + // arrays it holds. const checkable = (value: IrExpr): boolean => { const range = ranges.get(value); return int32(value) || range != null; }; - if (values.every(checkable) && checkableInt32Allocations(fn, id)) - slots.checkedInt32Arrays.add(`${fn.name}\0${id}`); - else reason(`array:${fn.name}\0${id}`, "an element store is not int32"); - dropped = true; - } - if (dropped && !queued.has(fn.name)) { - queued.add(fn.name); - queue.push(fn.name); + if ( + group.length === 1 && + !key.startsWith("\0") && + !params.has(id) && + values.every(checkable) && + checkableInt32Allocations(fn, id) + ) + slots.checkedInt32Arrays.add(key); + else { + for (const member of group) reason(`array:${member}`, "an element store is not int32"); + if (inputs.revise !== undefined) { + // The revised module no longer marks the group: its functions + // leave the candidate sets the visits below consult. + for (const member of group) { + dropped.add(member); + if (member.startsWith("\0")) { + globalArrays.delete(member.slice(1)); + const names = arrayGroups.globalReaders.get(member); + if (names) for (const name of names) revised.add(name); + } else { + const cut = member.indexOf("\0"); + const owner = member.slice(0, cut); + const ids = arrayCandidates.get(owner); + ids?.delete(member.slice(cut + 1)); + if (ids?.size === 0) arrayCandidates.delete(owner); + revised.add(owner); + } + } + globalArrayFunctions.clear(); + for (const [member, names] of arrayGroups.globalReaders) + if (!dropped.has(member)) for (const name of names) globalArrayFunctions.add(name); + } + } } } + if (revised.size > 0) { + revise(revised); + // A revised body is analyzed again (it was queued with its readers); + // the writes of the body it replaces no longer count. + if (functions.get(fn.name) !== fn) continue; + } // An int32 written into a slot still assumed non-negative: widen the // assumption when the value may be negative, and reanalyze its readers. const settle = (slot: string, e: IrExpr): void => { @@ -1298,10 +1638,8 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}): } for (const proven of provenByFunction.values()) for (const value of proven) slots.provenWrites.add(value); - for (const [fnName, ids] of arrayCandidates) - for (const id of ids) - if (slots.hasInt32ElementSlot(`l:${fnName}\0${id}`)) - slots.int32Arrays.add(`${fnName}\0${id}`); + for (const key of arrayKeys) + if (slots.hasInt32ElementSlot(int32ArraySlot(key))) slots.int32Arrays.add(key); const ranged = rangedParameters( slots, functions, @@ -1613,8 +1951,14 @@ function narrowSlotRanges( // The ranges every function was last analyzed under. let analyzed: Map = new Map(proven); let verified: Map = new Map(proven); - // Functions whose recorded writes predate their current facts. + // Functions whose recorded writes predate their current facts: those + // whose parameters rangedParameters proved, and their callers, which + // wrote nothing into those parameters while they had no fact. const dirty = new Set(changed); + for (const name of changed) { + const callers = readersOf(`ret:${name}`); + if (callers) for (const caller of callers) dirty.add(caller); + } // The narrowing operator: a bound moves at most once from its proven // value (a revived result's from int32), so recursion that shifts its // argument (`f(d - 1)`) cannot walk a bound down one step per round. diff --git a/packages/compiler/src/ir/integer-loop-facts.ts b/packages/compiler/src/ir/integer-loop-facts.ts index 8807212dc..d66153b19 100644 --- a/packages/compiler/src/ir/integer-loop-facts.ts +++ b/packages/compiler/src/ir/integer-loop-facts.ts @@ -152,7 +152,7 @@ export function boundedIntegerLoopFacts( e.receiver.kind === "varRef" && stable(e.receiver.localId) ) - return { min: 0, max: Number.MAX_SAFE_INTEGER }; + return BYTES_LENGTH_RANGE; // Every evaluation of an array length lies in its range, so the // condition bounds each pass even when the body resizes the array. if (isArrayLength(e)) return ARRAY_LENGTH_RANGE; @@ -225,6 +225,12 @@ export function boundedIntegerLoopFacts( return result; } +/** Every length, byteLength and byteOffset of a typed array, DataView or + * Buffer: each counts elements or bytes of one allocation, and every + * supported target addresses less than 2^48 bytes. Counters and offsets + * derived from them then stay exact integers far below 2^53. */ +export const BYTES_LENGTH_RANGE: IntegerRange = { min: 0, max: 2 ** 48 }; + /** Every length an array can have: SCR_ARR_MAX_LENGTH (scr_array.c). */ export const ARRAY_LENGTH_RANGE: IntegerRange = { min: 0, max: 4294967295 }; @@ -353,6 +359,13 @@ export function guardedLoopCounterFacts( stable(e.receiver.localId) ) return { min: 0, max: STRING_MAX_UNITS }; + if ( + e.kind === "bytesIntrinsic" && + (e.method === "length" || e.method === "byteLength") && + e.receiver.kind === "varRef" && + stable(e.receiver.localId) + ) + return BYTES_LENGTH_RANGE; // An array length can change on any pass, but never leaves its range, // which is all a ceiling needs. if (isArrayLength(e)) return ARRAY_LENGTH_RANGE; @@ -402,9 +415,84 @@ export function guardedLoopCounterFacts( if (Number.isSafeInteger(start.min) && Number.isSafeInteger(max)) result.set(id, { min: start.min, max }); } + // Companion counters: a binding the loop changes only by positive literal + // increments that no condition bounds (`out[k++] = ...` beside + // `while (i < n)`). A bounded counter that every pass advances before it + // can continue limits the number of passes, and each pass adds at most + // the companion's increments in the body. Increments inside nested loops + // can repeat within a pass, so those companions are not bounded. + let passes = Infinity; + for (const [id, range] of result) { + const hi = ceiling.get(id)!; + const step = guaranteedStep(loop.body, id); + if (step <= 0) continue; + passes = Math.min(passes, Math.floor((hi - range.min) / step) + 1); + } + if (!Number.isSafeInteger(passes) || passes < 0) return result; + const repeated = new Set(); + for (const inner of nested) for (const id of writtenLocals(inner.body)) repeated.add(id); + for (const [id, total] of budget) { + const local = locals.get(id), + start = initial.get(id); + if (result.has(id) || refused.has(id) || repeated.has(id) || ceiling.has(id)) continue; + if (!local || local.boxed || local.tdz || !start) continue; + const max = start.max + passes * total; + if (Number.isSafeInteger(start.min) && Number.isSafeInteger(max)) + result.set(id, { min: start.min, max }); + } return result; } +/** The increments of counter `id` that every pass of `body` makes before it + * can start another pass: those of the leading statements up to the first + * one that can continue the loop or holds a nested region (0 when there is + * none). A pass that leaves the loop earlier starts no further pass, so the + * count bounds the passes that continue. */ +function guaranteedStep(body: readonly IrStmt[], id: string): number { + let step = 0; + for (const s of body) { + if (s.kind === "assign" && s.localId === id) { + const delta = increment(id, s.value); + if (delta === null || delta <= 0) return 0; + step += delta; + continue; + } + if (s.kind === "exprStmt" && s.expr.kind === "incDec" && s.expr.localId === id) { + if (s.expr.op !== "+") return 0; + step += 1; + continue; + } + if (mayContinue(s)) break; + } + return step; +} + +/** Whether `s` can start another pass of the enclosing loop before its + * following statements run: it holds a `continue`, a loop or another + * region whose control flow is not walked here. */ +function mayContinue(s: IrStmt): boolean { + let found = false; + const stmt = (x: IrStmt): boolean => { + if ( + x.kind === "continue" || + x.kind === "for" || + x.kind === "while" || + x.kind === "doWhile" || + x.kind === "forOf" || + x.kind === "tryCatch" || + x.kind === "switch" || + (x.kind === "block" && !!x.labels?.length) + ) { + found = true; + return false; + } + return everyStmtChild(x, expr, stmt); + }; + const expr = (e: IrExpr): boolean => everyExprChild(e, expr, stmt); + stmt(s); + return found; +} + /** The largest value counter `id` takes in one pass of `body`, which starts * at most at `entry`. The caller has checked that the body changes the * counter only by positive literal increments outside switch, exception and diff --git a/packages/compiler/src/ir/integer-loops.test.ts b/packages/compiler/src/ir/integer-loops.test.ts index 2cca43a00..48be67fbf 100644 --- a/packages/compiler/src/ir/integer-loops.test.ts +++ b/packages/compiler/src/ir/integer-loops.test.ts @@ -552,3 +552,71 @@ test("counters stepped beside the tested one run as integers", () => { { startGuarded: true, tripGuarded: true }, ]); }); + +/** `counted(limit)` with the body `x = ((i + k) * (i + k + 1)) >>> 1` + * inside a nested `for (let j = 0; j < 4; j++)` loop. */ +function productLoop(limit: IrExpr, start: IrExpr = number(0)): IrStmt & { kind: "for" } { + const s = counted(limit); + if (s.init?.kind === "varDecl") s.init.init = start; + const sum = (extra: number): IrExpr => { + const base: IrExpr = { kind: "bin", op: "+", left: ref("i"), right: ref("k"), type: F64, loc }; + return extra === 0 + ? base + : { kind: "bin", op: "+", left: base, right: number(extra), type: F64, loc }; + }; + const product: IrExpr = { kind: "bin", op: "*", left: sum(0), right: sum(1), type: F64, loc }; + const inner = counted(number(4)); + if (inner.init?.kind === "varDecl") inner.init.localId = "j"; + if (inner.cond?.kind === "bin") inner.cond.left = ref("j"); + if (inner.update?.kind === "assign") { + inner.update.localId = "j"; + if (inner.update.value.kind === "bin") inner.update.value.left = ref("j"); + } + inner.body = [ + { + kind: "assign", + localId: "x", + value: { kind: "bin", op: ">>>", left: product, right: number(1), type: F64, loc }, + loc, + }, + ]; + s.body = [inner]; + return s; +} + +test("guarded counted loops whose products the counter bound proves exact may hold nested loops", () => { + const scope = new Map(locals); + for (const id of ["limit", "start", "k", "x", "j"]) + scope.set(id, { id, name: id, type: F64, mutable: true }); + const tight = matchIntegerCountedForLoop(productLoop(ref("limit"), ref("start")), scope); + expect(tight).toMatchObject({ + guarded: true, + limitGuarded: true, + startGuarded: true, + startBound: 2 ** 25, + guardLimit: 2 ** 25, + range: { min: -(2 ** 25), max: 2 ** 25 - 1 }, + }); + // A literal trip count past the bound keeps the safe-integer bound, and + // a guarded loop under that bound holds no nested loop. + expect(matchIntegerCountedForLoop(productLoop(number(1e9), ref("start")), scope)).toBeNull(); + const plain = counted(ref("limit")); + plain.body = [productLoop(number(4)).body[0]!]; + const inner = plain.body[0] as IrStmt & { kind: "for" }; + inner.body = [ + { + kind: "assign", + localId: "x", + value: { kind: "bin", op: "|", left: ref("i"), right: number(0), type: F64, loc }, + loc, + }, + ]; + expect(matchIntegerCountedForLoop(plain, scope)).toBeNull(); + // Without a nested loop the product bound still applies to the limit. + const flat = productLoop(ref("limit"), ref("start")); + flat.body = (flat.body[0] as IrStmt & { kind: "for" }).body; + expect(matchIntegerCountedForLoop(flat, scope)).toMatchObject({ + startBound: 2 ** 25, + guardLimit: 2 ** 25, + }); +}); diff --git a/packages/compiler/src/ir/integer-loops.ts b/packages/compiler/src/ir/integer-loops.ts index d6942718a..427f6879c 100644 --- a/packages/compiler/src/ir/integer-loops.ts +++ b/packages/compiler/src/ir/integer-loops.ts @@ -12,6 +12,9 @@ import { preservesArrayLength } from "./array-length-stability.js"; export interface IntegerBytesForLoop { localId: string; limitReceiver: IrExpr; + /** The receiver is a local of the function that the body never assigns + * (a typed array's length is then the same on every iteration). */ + stableReceiver: boolean; } export interface IntegerCountedForLoop { @@ -24,8 +27,12 @@ export interface IntegerCountedForLoop { /** The limit needs the run-time `guardLimit` test (part of `guarded`). */ limitGuarded: boolean; /** The start has no proven integer range: the versioned loop also tests - * that it is an exact integer (not -0) of at most 2^53 in magnitude. */ + * that it is an exact integer (not -0) of at most `startBound` in + * magnitude. */ startGuarded: boolean; + /** The magnitude the guarded start is tested against: 2^53 - 1, or + * PRODUCT_COUNTER_BOUND when the body multiplies sums of the counter. */ + startBound: number; guardLimit: number; range: IntegerRange; /** Counters declared and stepped beside this one that the condition does @@ -50,6 +57,64 @@ export interface IntegerLoopFollower { range: IntegerRange; } +/** The counter magnitude below which a product of two sums of counters and + * small literals stays exact in a double: `(i + j) * (i + j + 1)` with both + * counters within 2^25 is below 2^52. A guarded loop whose body multiplies + * such sums for a bitwise operator tests its counter against this bound, so + * the product's integer form is proven exact. */ +export const PRODUCT_COUNTER_BOUND = 2 ** 25; + +/** Whether `e` is a sum or difference of locals and safe integer literals + * of magnitude at most 2^20, and whether it reads `counter`. */ +function localSum(e: IrExpr, counter: string): { sum: boolean; reads: boolean } { + let reads = false; + const sum = (x: IrExpr): boolean => { + if (x.kind === "varRef") { + if (x.localId === counter) reads = true; + return true; + } + if (x.kind === "numLit") return Number.isSafeInteger(x.value) && Math.abs(x.value) <= 2 ** 20; + return x.kind === "bin" && (x.op === "+" || x.op === "-") && sum(x.left) && sum(x.right); + }; + return { sum: sum(e), reads }; +} + +/** Whether a bitwise operand `e` multiplies two sums of locals and + * literals, one of which reads `counter` (`((i + j) * (i + j + 1)) >>> 1`). */ +function multipliesCounterSums(e: IrExpr, counter: string): boolean { + if (e.kind !== "bin") return false; + if (e.op === "*") { + const left = localSum(e.left, counter), + right = localSum(e.right, counter); + return left.sum && right.sum && (left.reads || right.reads); + } + return ( + (e.op === "+" || e.op === "-") && + (multipliesCounterSums(e.left, counter) || multipliesCounterSums(e.right, counter)) + ); +} + +const BITWISE = new Set(["&", "|", "^", "<<", ">>", ">>>"]); + +/** Whether `body`, nested loops included, takes a bitwise operator of a + * product that multipliesCounterSums matches. */ +function bodyMultipliesCounter(body: IrStmt[], counter: string): boolean { + let found = false; + everyStmtList(body, { + stmt: () => !found, + expr: (e) => { + if ( + e.kind === "bin" && + BITWISE.has(e.op) && + (multipliesCounterSums(e.left, counter) || multipliesCounterSums(e.right, counter)) + ) + found = true; + return !found; + }, + }); + return found; +} + /** The bound of the run-time follower test. */ export const FOLLOWER_GUARD_LIMIT = 2 ** 52; /** The magnitude a follower that passed the test can reach. The test's four @@ -119,13 +184,26 @@ function stableLoopValue( stableLoopValue(value.receiver, locals, body, invariant, counter) ); case "bytesIntrinsic": - case "arrIntrinsic": + case "arrIntrinsic": { + // The receiver may be one arm of a union binding (indexed-unions.ts). + const receiver = + value.receiver.kind === "unionNarrow" ? value.receiver.value : value.receiver; return ( (value.method === "length" || (value.kind === "bytesIntrinsic" && value.method === "byteLength")) && - value.receiver.kind === "varRef" && - (value.kind === "bytesIntrinsic" || !invariant || stableLength(value.receiver.localId)) && - stableLoopValue(value.receiver, locals, body, invariant, counter) + receiver.kind === "varRef" && + (value.kind === "bytesIntrinsic" || !invariant || stableLength(receiver.localId)) && + stableLoopValue(receiver, locals, body, invariant, counter) + ); + } + // A union binding dispatched on its tag (indexed-unions.ts): stable + // when the binding is and every arm is. + case "ternary": + return ( + value.cond.kind === "unionIsTag" && + stableLoopValue(value.cond.value, locals, body, invariant, counter) && + stableLoopValue(value.then, locals, body, invariant, counter, stableLength) && + stableLoopValue(value.else_, locals, body, invariant, counter, stableLength) ); default: return false; @@ -158,6 +236,23 @@ export function matchIntegerCountedForLoop( // The condition tests the driving counter; every other declaration // follows it. const driver = declared(cond.left) ?? declared(cond.right); + // A body that multiplies sums of the driver tests it against the bound + // under which those products are exact. A limit proven past that bound + // (a literal trip count) keeps the safe-integer bound instead. + const limitSide = driver !== null && declared(cond.left) === driver ? cond.right : cond.left; + const knownLimit = + limitSide.kind === "numLit" + ? { min: limitSide.value, max: limitSide.value } + : ranges.get(limitSide); + const driverBound = + driver !== null && + !( + knownLimit && + Math.max(Math.abs(knownLimit.min), Math.abs(knownLimit.max)) > PRODUCT_COUNTER_BOUND + ) && + bodyMultipliesCounter(stmt.body, driver) + ? PRODUCT_COUNTER_BOUND + : Number.MAX_SAFE_INTEGER; const counterIds = decls.map((d) => (d.kind === "varDecl" ? d.localId : "")); const stepOf = (id: string): number | null => { const steps = updates.map((u) => constantStep(u, id)).filter((v) => v !== null); @@ -178,7 +273,8 @@ export function matchIntegerCountedForLoop( ? { min: start.value, max: start.value } : ranges.get(start); const guarded = !known && start.kind !== "numLit" && start.type.kind === "f64"; - const range = guarded ? { min: -Number.MAX_SAFE_INTEGER, max: Number.MAX_SAFE_INTEGER } : known; + const bound = id === driver ? driverBound : Number.MAX_SAFE_INTEGER; + const range = guarded ? { min: -bound, max: bound } : known; const step = stepOf(id); if ( !range || @@ -235,7 +331,9 @@ export function matchIntegerCountedForLoop( ); if (!stableLoopValue(limit, locals, stmt.body, true, local.id, stableLength)) return null; const inclusive = op === "<=" || op === ">="; - const guardLimit = step > 0 ? 2 ** 53 - (inclusive ? 1 : 0) : -(2 ** 53) + (inclusive ? 1 : 0); + const limitBound = driverBound === Number.MAX_SAFE_INTEGER ? 2 ** 53 : driverBound; + const guardLimit = + step > 0 ? limitBound - (inclusive ? 1 : 0) : -limitBound + (inclusive ? 1 : 0); const boundRange = limit.kind === "numLit" ? { min: limit.value, max: limit.value } : ranges.get(limit); const limitGuarded = @@ -251,7 +349,9 @@ export function matchIntegerCountedForLoop( Number.MAX_SAFE_INTEGER, boundRange ? Math.ceil(boundRange.max) - (inclusive ? 0 : 1) - : Number.MAX_SAFE_INTEGER, + : limitGuarded + ? Math.ceil(guardLimit) - (inclusive ? 0 : 1) + : Number.MAX_SAFE_INTEGER, ), ), } @@ -262,7 +362,9 @@ export function matchIntegerCountedForLoop( -Number.MAX_SAFE_INTEGER, boundRange ? Math.floor(boundRange.min) + (inclusive ? 0 : 1) - : -Number.MAX_SAFE_INTEGER, + : limitGuarded + ? Math.floor(guardLimit) + (inclusive ? 0 : 1) + : -Number.MAX_SAFE_INTEGER, ), ), max: startRange.max, @@ -353,9 +455,15 @@ export function matchIntegerCountedForLoop( const bodyStmt = (s: IrStmt): boolean => { if (s.kind === "bytesSet" && counter(s.index)) useful = true; if (!guarded && s.kind === "arraySet" && counter(s.index)) useful = true; + // A guarded loop's copy duplicates nested loops too, so only a body + // whose counter products the bound proves exact may hold a nested + // `for` loop; the node budget counts its body. if ( guarded && - (s.kind === "for" || s.kind === "forOf" || s.kind === "while" || s.kind === "doWhile") + (s.kind === "forOf" || + s.kind === "while" || + s.kind === "doWhile" || + (s.kind === "for" && driverBound === Number.MAX_SAFE_INTEGER)) ) return false; return (!guarded || ++cost <= 80) && everyStmtChild(s, expr, bodyStmt); @@ -370,6 +478,7 @@ export function matchIntegerCountedForLoop( guarded, limitGuarded, startGuarded, + startBound: driverBound, guardLimit, range, followers, @@ -509,6 +618,28 @@ function writesLocal(body: IrStmt[], localId: string): boolean { }); } +/** The counter of a `for` loop that its condition reads and its update + * steps by a constant, when nothing else in the loop writes it: each test + * of the condition then sees a different value. The local must not be + * boxed (a closure could write it). */ +export function steppedLoopCounter( + stmt: IrStmt & { kind: "for" }, + locals: ReadonlyMap, +): { ref: IrExpr & { kind: "varRef" }; step: number } | null { + const cond = stmt.cond; + if (cond?.kind !== "bin") return null; + for (const side of [cond.left, cond.right]) { + if (side.kind !== "varRef") continue; + const local = locals.get(side.localId); + if (local === undefined || local.boxed) continue; + const step = constantStep(stmt.update, side.localId); + if (step === null || writesLocal(stmt.body, side.localId)) continue; + if (writesLocal([{ kind: "exprStmt", expr: cond, loc: cond.loc }], side.localId)) continue; + return { ref: side, step }; + } + return null; +} + function isUnitIncrement(update: IrStmt | null, localId: string): boolean { // The frontend normally lowers `i++` to `i = i + 1` before backend // emission. Accept the incDec form too so this analysis remains valid for @@ -576,7 +707,12 @@ export function matchIntegerBytesForLoop( if (!isUnitIncrement(stmt.update, init.localId)) return null; if (writesLocal(stmt.body, init.localId)) return null; - return { localId: init.localId, limitReceiver: cond.right.receiver }; + return { + localId: init.localId, + limitReceiver: cond.right.receiver, + stableReceiver: + receiverLocal !== undefined && !writesLocal(stmt.body, cond.right.receiver.localId), + }; } /** Array lengths are uint32, so zero-based loops and nested `j = i + 1` @@ -623,5 +759,11 @@ export function matchIntegerArrayForLoop( writesLocal(stmt.body, local.id) ) return null; - return { localId: local.id, limitReceiver: cond.right.receiver }; + return { + localId: local.id, + limitReceiver: cond.right.receiver, + stableReceiver: + locals.has(cond.right.receiver.localId) && + !writesLocal(stmt.body, cond.right.receiver.localId), + }; } diff --git a/packages/compiler/src/ir/integer-ranges.test.ts b/packages/compiler/src/ir/integer-ranges.test.ts index fcb040275..318ff2c09 100644 --- a/packages/compiler/src/ir/integer-ranges.test.ts +++ b/packages/compiler/src/ir/integer-ranges.test.ts @@ -440,6 +440,37 @@ test("int32-closed locals keep the int32 range across loops and switches", () => expect(switched.get(afterSwitch)).toEqual(INT32); }); +test("loop-carried locals written from Int32[] element reads keep the int32 range", () => { + const arrType = { kind: "array", elem: F64 } as const; + const loop = (localId: string): { after: IrExpr; f: IrFunction } => { + const after = ref(); + // `x = a[x]!` in a loop: a cursor that follows indices stored in the array. + const read: IrExpr = { + loc, + kind: "arrayGet", + arr: { loc, kind: "varRef", localId, type: arrType }, + index: ref(), + type: F64, + }; + const f = fn([ + assign(num(0)), + { + loc, + kind: "while", + cond: condition("<", ref("unknown"), num(1)), + body: [assign(read)], + }, + observe(after), + ]); + return { after, f }; + }; + const elements = { elements: (arr: IrExpr) => arr.kind === "varRef" && arr.localId === "ints" }; + const ints = loop("ints"); + expect(analyzeIntegerRanges(ints.f, elements).get(ints.after)).toEqual(INT32); + const floats = loop("floats"); + expect(analyzeIntegerRanges(floats.f, elements).get(floats.after) ?? null).toBeNull(); +}); + test("uint32-closed locals keep the uint32 range across loop back edges", () => { // for (let i = 0; i < n; i++) { v = unknown >>> 0; acc = (acc + v) >>> 0; ... } const loop = (start: IrExpr | null, writes: IrExpr[]) => { @@ -797,6 +828,72 @@ test("a while condition on a string's length bounds a counter, and guarded incre expect(run((inside) => [observe(inside), bump, rebound]).after).toBeNull(); }); +test("a while counter every pass advances bounds the companions the loop increments", () => { + // pos = 0; k = 0; while (pos < text.length) { pos++; observe(k); if (unknown === 1) k++; k++; } observe(k) + const text: IrExpr = { loc, kind: "varRef", localId: "text", type: STRING }; + const length: IrExpr = { + loc, + kind: "strIntrinsic", + method: "length", + receiver: text, + args: [], + type: F64, + }; + const set = (localId: string, value: IrExpr): IrStmt => ({ loc, kind: "assign", localId, value }); + const bump = (localId: string): IrStmt => ({ + loc, + kind: "exprStmt", + expr: { loc, kind: "incDec", op: "+", prefix: false, localId, type: F64 }, + }); + const maybe = (then: IrStmt[]): IrStmt => ({ + loc, + kind: "if", + cond: condition("===", ref("unknown"), num(1)), + then, + else_: null, + }); + const run = (body: (inside: IrExpr) => IrStmt[]) => { + const inside = ref("k"), + after = ref("k"); + const loop: IrStmt = { + loc, + kind: "while", + cond: condition("<", ref("pos"), length), + body: body(inside), + }; + const f = fn([set("pos", num(0)), set("k", num(0)), loop, observe(after)]); + f.locals.push( + { id: "pos", name: "pos", type: F64, mutable: true }, + { id: "k", name: "k", type: F64, mutable: true }, + { id: "text", name: "text", type: STRING, mutable: false }, + ); + const facts = analyzeIntegerRangeFacts(f); + return { + inside: facts.ranges.get(inside), + after: facts.ranges.get(after), + loop: facts.loopRanges.get(loop)?.get("k"), + }; + }; + // At most 2^31 - 1 passes, each adding at most 2. + const bounded = { min: 0, max: 4294967294 }; + expect(run((inside) => [bump("pos"), observe(inside), maybe([bump("k")]), bump("k")])).toEqual({ + inside: bounded, + after: bounded, + loop: bounded, + }); + // A pass that can continue before advancing the counter, or an increment a + // nested loop repeats, bounds nothing. + const skip: IrStmt = maybe([{ loc, kind: "continue" }]); + expect(run((inside) => [skip, bump("pos"), observe(inside), bump("k")]).after).toBeNull(); + const repeat: IrStmt = { + loc, + kind: "while", + cond: condition("===", ref("unknown"), num(1)), + body: [bump("k")], + }; + expect(run((inside) => [bump("pos"), observe(inside), repeat]).after).toBeNull(); +}); + test("a nested while loop may advance a counter its own condition bounds", () => { // while (pos < text.length) { observe(pos); while (pos < text.length && unknown === 1) pos++; pos += 2; } const text: IrExpr = { loc, kind: "varRef", localId: "text", type: STRING }; @@ -1292,7 +1389,7 @@ test("a for loop bounds cursors it resets and copies, and their writes keep the } }); -test("a cursor of a loop over a typed array keeps its writes exact up to 2^53 - 1", () => { +test("a cursor of a loop over a typed array keeps its writes within the largest length", () => { // for (x = 0; x < bytes.length; x++) changes = changes + 1; const bytes: IrExpr = { loc, @@ -1324,7 +1421,7 @@ test("a cursor of a loop over a typed array keeps its writes exact up to 2^53 - { id: "changes", name: "changes", type: F64, mutable: true }, { id: "bytes", name: "bytes", type: { kind: "bytes", elem: "u8" }, mutable: false }, ); - // The sum of the header range passes 2^53 - 1; the write takes the loop - // range instead. - expect(analyzeIntegerRanges(f).get(counted)).toEqual({ min: 0, max: Number.MAX_SAFE_INTEGER }); + // The sum of the header range passes the largest typed array length; the + // write takes the loop range instead. + expect(analyzeIntegerRanges(f).get(counted)).toEqual({ min: 1, max: 2 ** 48 }); }); diff --git a/packages/compiler/src/ir/integer-ranges.ts b/packages/compiler/src/ir/integer-ranges.ts index 0e5ca426d..51c0a929b 100644 --- a/packages/compiler/src/ir/integer-ranges.ts +++ b/packages/compiler/src/ir/integer-ranges.ts @@ -2,6 +2,7 @@ import type { IrExpr, IrFunction, IrStmt } from "./ir.js"; import { everyExprChild, everyStmtChild, everyStmtList } from "./traverse.js"; import { byteNumberAccess, byteNumberRange } from "./byte-numbers.js"; import { + BYTES_LENGTH_RANGE, boundedIntegerLoopFacts, guardedLoopCounterFacts, integerPathFallsThrough, @@ -480,6 +481,7 @@ export function analyzeIntegerRangeFacts( return boxedRange(e.localId); } case "fieldGet": + case "arrayGet": case "call": return slotRange(e); default: @@ -820,7 +822,7 @@ export function analyzeIntegerRangeFacts( const numeric = byteNumberAccess(e); if (numeric && !numeric.write) range = byteNumberRange(numeric); else if (e.method === "length" || e.method === "byteLength" || e.method === "byteOffset") - range = { min: 0, max: Number.MAX_SAFE_INTEGER }; + range = BYTES_LENGTH_RANGE; else if (e.method === "get" && e.invalidNaN !== true && e.receiver.type.kind === "bytes") { const elem = e.receiver.type.elem; if (elem === "u8" || elem === "u8c") range = { min: 0, max: 255 }; @@ -865,6 +867,8 @@ export function analyzeIntegerRangeFacts( ); if (e.type.kind === "f64") { if (e.method === "length") range = { min: 0, max: STRING_MAX_UNITS }; + // A string holds at most SCR_STR_MAX_CAP (0x7fffffff) bytes. + else if (e.method === "byteLength") range = { min: 0, max: STRING_MAX_UNITS }; // An empty needle is found at the clamped position, up to the length. else if (e.method === "indexOf") range = { min: -1, max: STRING_MAX_UNITS }; } @@ -1074,6 +1078,19 @@ export function analyzeIntegerRangeFacts( // inside and after the loop. const guarded = s.kind === "while" ? guardedLoopCounterFacts(s, loopLocals, facts) : noInvariants; + // The same bounds hold on entry and at every write in the loop, + // which is what a proven integer loop needs (loopRanges). A loop + // analyzed again keeps the bindings every analysis bounds. + if (s.kind === "while") { + const previous = loopRanges.get(s); + if (!previous) loopRanges.set(s, new Map(guarded)); + else + for (const [id, range] of previous) { + const other = guarded.get(id); + if (other) previous.set(id, union(range, other)); + else previous.delete(id); + } + } const loop = new Map(facts); havocWith(loop, [s], loop, guarded); for (const [id, range] of guarded) set(loop, id, range); diff --git a/packages/compiler/src/ir/intrinsic-signatures.ts b/packages/compiler/src/ir/intrinsic-signatures.ts index eda569883..3e26e77f2 100644 --- a/packages/compiler/src/ir/intrinsic-signatures.ts +++ b/packages/compiler/src/ir/intrinsic-signatures.ts @@ -31,7 +31,8 @@ export const STR_INTRINSIC_SIGS: Record< normalize: { argTypes: [STRING], minArgs: 1, result: STRING }, isWellFormed: { argTypes: [], minArgs: 0, result: BOOL }, toWellFormed: { argTypes: [], minArgs: 0, result: STRING }, - cpAt: { argTypes: [F64], minArgs: 1, result: STRING }, + byteLength: { argTypes: [], minArgs: 0, result: F64 }, + cpAtByte: { argTypes: [F64], minArgs: 1, result: STRING }, }; /** Per-method signature for regexIntrinsic. test/source/flags take the diff --git a/packages/compiler/src/ir/ir.test.ts b/packages/compiler/src/ir/ir.test.ts index d62c9c83c..30fe6f50e 100644 --- a/packages/compiler/src/ir/ir.test.ts +++ b/packages/compiler/src/ir/ir.test.ts @@ -57,7 +57,8 @@ describe("IR kind sets", () => { test("distinguishes pointer values from object-like scalars", () => { expect(POINTER_KINDS.has("record")).toBe(true); - expect(POINTER_KINDS.has("date")).toBe(false); + expect(POINTER_KINDS.has("date")).toBe(true); + expect(POINTER_KINDS.has("procStream")).toBe(false); }); test("distinguishes typed-rest closure ABIs", () => { diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 6ed304d73..4663e2736 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -87,7 +87,16 @@ export type IrType = | { kind: "date" } | { kind: "string" } // heap, refcounted, UTF-8 | { kind: "bool" } - | { kind: "array"; elem: IrType } // heap, refcounted, monomorphic elements + | { + kind: "array"; + elem: IrType; + /** A `(T | undefined)[]` of a number, string or boolean T, stored as + * T elements (which hold a present undefined by their state). Not + * part of the type's identity (typeEquals, typeKey): it only lets a + * checked conversion from a dynamic value accept undefined + * elements. */ + optionalElements?: true; + } // heap, refcounted, monomorphic elements /** ES `Map` — heap, refcounted, insertion-ordered hash map with ONE * runtime representation (ScrMap) and type-directed key/value handling, * exactly the array pattern (never per-instantiation structs). Keys use @@ -440,6 +449,7 @@ const POINTER_KIND_LIST = [ "url", "searchParams", "symbol", + "date", ...POINTER_HANDLE_KINDS, "func", "object", @@ -463,7 +473,7 @@ export const POINTER_KINDS = irKindSet(POINTER_KIND_LIST); export const RUNTIME_RC_STEMS: Record = { f64: "", bigint: "scr_bigint", - date: "", + date: "scr_date", string: "scr_str", bool: "", array: "scr_arr", @@ -645,6 +655,7 @@ export function isSupportedArrayElem(t: IrType): boolean { case "func": case "promise": case "regex": + case "date": case "child": case "netServer": // fs.Stats snapshots (`await Promise.all(paths.map(stat))`): immutable @@ -689,7 +700,8 @@ export function isIdentityCollectionKey(t: IrType): boolean { // functions intern one immortal closure, inner closures allocate once // per evaluation), so pointer identity is JS function identity. // Closures are cycle-headered and traced like records. - t.kind === "func" + t.kind === "func" || + t.kind === "date" ); } @@ -797,6 +809,9 @@ export function isSupportedMapValue(t: IrType): boolean { // the pre-settle closures. case "secureCtx": return true; + // A Date object: immutable, holds no references. + case "date": + return true; // A promise (Map> — the SNI callback's // in-flight dedupe map): refcounted, with trace/cycle support already // in place (scr_promise_trace_v — the race machinery's refcounting). @@ -1009,6 +1024,18 @@ export function isRefCounted(t: IrType): boolean { /* ── module ────────────────────────────────────────────────────────────── */ +/** The island module tree of a program (node-islands/modules.ts): paths + * relative to `root`, the directory on the build machine that holds them. + * Only the relative paths enter an executable. */ +export interface IrNodeIslandModules { + root: string; + files: { path: string; size: number; sha256: string }[]; + links: { path: string; target: string }[]; + /** Where the executable finds the tree: absent when it carries the tree + * itself, else a `$ORIGIN/` path (`--libnode=beside`). */ + location?: string; +} + export interface IrModule { /** Bumped on any breaking IR change; serialize.ts refuses mismatches. */ irVersion: 17; @@ -1048,14 +1075,37 @@ export interface IrModule { * closures boxed for package code return records and arrays as live * capsules, so a value that comes back keeps its identity. */ declBoundary?: true; + /** Package files compiled from shape inference's typed overlay + * (frontend/shape-inference/overlay.ts), each with its package name: a + * checked conversion in them checks an analysis-proven fact, so its + * failure reports a compiler analysis bug instead of a type error. */ + inferredFactFiles?: { file: string; pkg: string }[]; + /** Origins of package-declared types checked by `dynCheck.origin` + * (frontend/lowering/decl-origin.ts): fields separated by \x1f — + * package, version, suggested range, selector, where it is declared, + * declared type. */ + declOrigins?: string[]; /** The program constructs LifetimeRegion (@scriptc/experimental): every * ProgramLifetime instance lives in a counted region (the region.* * intrinsics; backend/llvm/regions.ts). */ lifetimeRegions?: true; + /** Element keys (typeKey of the element type) of native arrays that can + * hold elements outside their element type (SCR_ARR_FOREIGN), stored + * through dynamic views; "*" for every array. Absent when none can. */ + foreignElementKeys?: string[]; /** Classes used as ThreadSlots keys (@scriptc/experimental): the root of * each one's hierarchy carries a slot word (the slots.* intrinsics; * backend/llvm/thread-slots.ts). */ threadSlotKeys?: string[]; + /** typeKeys of the array types whose values the program may freeze + * (frontend frozen-records.ts): their writes test the array's frozen bit + * (SCR_ARR_FROZEN) and throw Node's TypeError when it is set. */ + freezableArrays?: string[]; + /** The program freezes, seals or prevents extensions of untyped values + * (frontend frozen-records.ts noteUntypedIntegrity). Class hierarchies + * that can be held there carry an integrity word, which the runtime sets + * through the commit of the instance's checked view. */ + untypedIntegrity?: true; /** The entry module is CommonJS under Node's classification. Node runs a * CommonJS main synchronously and drains process.nextTick callbacks * before promise jobs at the first checkpoint; an ES-module main's @@ -1067,7 +1117,13 @@ export interface IrModule { * executable) and the exact Node version its bridge links. Set by the * executable build (node-islands/libnode.ts); the emitted main records * both before %main. */ - nodeIsland?: { libnode: string; nodeVersion: string }; + nodeIsland?: { + libnode: string; + nodeVersion: string; + /** The modules the calls load (node-islands/modules.ts), when any call + * loads a file. */ + modules?: IrNodeIslandModules; + }; /** Outbound native FFI declarations used by `ffiCall` expressions. * These are link-time C ABI imports, not runtime dynamic-library handles: * executable builds resolve their symbols from the manifest's archive @@ -1474,11 +1530,18 @@ export interface IrClassDef { jsName?: string; /** JavaScript constructor arity, ending before the first default or rest parameter. */ jsLength?: number; + /** The class declares public static fields. util.inspect prints them + * after the class tag; a class object reached through `unknown` does + * not carry them unless the runtime holds its static table. */ + ownStaticFields?: true; /** Zero-argument native helper returning the shared prototype data object. * Used when materializing an instance's own-property view. */ prototypeDataHelper?: string; /** Native instance to its actual prototype, including factory captures and subclasses. */ instancePrototypeHelper?: string; + /** Native instance to the string its inherited Symbol.toStringTag getter + * answers, read by the runtime for an instance seen as `unknown`. */ + stringTagHelper?: string; /** Public JavaScript fields record their creation in the shared property * bag. Its descriptors preserve presence and insertion order independently * of native layout order; field values remain in their native slots. */ @@ -1488,8 +1551,14 @@ export interface IrClassDef { * class of the chain sets `name` during construction, and then an own * `name` follows every other own property. */ errorNameAfter?: string | null; - /** Stable module symbol identities for public instance layout fields. */ - symbolFields?: { field: string; globalId: string }[]; + /** JavaScript Error subclasses with a property bag: a view committed by a + * dynamic write keeps `name` and `message` where the bag recorded them + * (scr_error_dyn_follow_bag). */ + errorBagOrder?: boolean; + /** Stable symbol identities for public instance layout fields: the + * module global holding the key, or the name of the well-known symbol + * (`toStringTag` for `Symbol.toStringTag`). */ + symbolFields?: ({ field: string } & ({ globalId: string } | { wellKnown: string }))[]; /** RUNTIME-PROVIDED class (the builtin Error hierarchy): the struct, RC * helpers, and vtable live in the runtime (ScrError / scr_error_*), so * backends emit no definitions for it — only the preorder-interval @@ -1497,6 +1566,17 @@ export interface IrClassDef { * RUNTIME_ERROR_CLASSES). User subclasses are ordinary emitted classes * whose layout prefix embeds ScrError's fields. */ runtime?: true; + /** A class declared `extends Array`: its instances are arrays of this + * element type. The struct begins with the runtime's array header (the + * reference count is the array's, field 0) and the class's fields follow + * it; the runtime finds the class through the array's ext. */ + arrayElem?: IrType; + /** The function (array) => dyn listing such a class's own fields + * (ScrArrClass.fields); absent when it declares none. */ + arrayFields?: string; + /** The function (length) => array constructing such a class for + * ArraySpeciesCreate (ScrArrClass.species). */ + arraySpecies?: string; /** Base class name (single inheritance, `extends`). A class is IN A * HIERARCHY when it has a base or is some class's base; hierarchy classes * carry a vtable word after `rc` (backends), standalone classes are laid @@ -1606,6 +1686,28 @@ export function classValueConstructor(def: { name: string; runtime?: true }): { : { fn: `%${def.name}.constructor`, firstArg: 1 }; } +/** Whether construction through a class value typed as `sup` may call + * `sub`'s construct thunk: both class-value constructors exist and take the + * same argument types after the receiver. The classval upcast rule and the + * checked class-value boundary both rest on it. */ +export function classValueAbiEquals( + sub: { name: string; runtime?: true }, + sup: { name: string; runtime?: true }, + functions: ReadonlyMap, +): boolean { + const subValueCtor = classValueConstructor(sub); + const supValueCtor = classValueConstructor(sup); + const subCtor = functions.get(subValueCtor.fn); + const supCtor = functions.get(supValueCtor.fn); + if (!subCtor || !supCtor) return false; + const subArgs = subCtor.params.slice(subValueCtor.firstArg); + const supArgs = supCtor.params.slice(supValueCtor.firstArg); + return ( + subArgs.length === supArgs.length && + subArgs.every((p, i) => typeEquals(p.type, supArgs[i]!.type)) + ); +} + /** The JavaScript `.name` of a class: its jsName, or the runtime-provided * class's own name (`%TypeError` is TypeError). */ export function classJsName(def: { name: string; runtime?: true; jsName?: string }): string { @@ -1662,6 +1764,8 @@ export const ZLIB_STREAM_MODES: ReadonlyMap = new Map([ ["DeflateRaw", 5], ["InflateRaw", 6], ["Unzip", 7], + ["BrotliDecompress", 8], + ["BrotliCompress", 9], ]); export interface IrRecordShape { @@ -1691,6 +1795,10 @@ export interface IrRecordShape { * dynCheck), and JSON serialization appends overflow entries after the * declared fields in insertion order. */ indexValue?: IrType; + /** Values of this shape may be frozen (frozen-records.ts): their rc word + * can carry the frozen bit, which their release ignores, and every write + * to them checks it. */ + freezable?: true; /** Field names in FIRST-SEEN declaration order (the checker's property * order of the first ts.Type interned to this shape) — metadata, NOT * part of the interned identity: a later structurally-equal type with a @@ -1751,6 +1859,34 @@ export function recordTextCodecClass(shape: IrRecordShape): "TextEncoder" | "Tex return null; } +/** The class of a record that crosses checked-dynamic slots as a native + * handle: the codecs, and a StringDecoder (type-mapper.ts: its canonical + * encoding and the packing of its pending bytes). */ +export function recordHandleCodecClass( + shape: IrRecordShape, +): "TextEncoder" | "TextDecoder" | "StringDecoder" | null { + const codec = recordTextCodecClass(shape); + if (codec !== null) return codec; + return shape.fields.length === 2 && + shape.fields[0]!.name === "%enc" && + shape.fields[1]!.name === "%pending" + ? "StringDecoder" + : null; +} + +/** The codec class of a TextEncoder, TextDecoder or StringDecoder record + * type, else null. Such a record crosses an `unknown` slot as a native + * handle that holds the record itself (scr_text_codec_dyn.c), never as a + * copy or a field view. */ +export function textCodecRecordClass( + t: IrType, + getRecord: (shapeId: string) => IrRecordShape | undefined, +): "TextEncoder" | "TextDecoder" | "StringDecoder" | null { + if (t.kind !== "record") return null; + const shape = getRecord(t.shapeId); + return shape ? recordHandleCodecClass(shape) : null; +} + /** Object-literal ACCESSOR properties (`{ get x() {...}, set x(v) {...} }`) * live on the shape as reserved '%'-fields holding closures: `%get:x` a * `() => T` invoked once per property READ (side effects and all — JS's @@ -2360,10 +2496,13 @@ export type IrStrIntrinsicMethod = // surrogate, and the copy with each one replaced by U+FFFD. | "isWellFormed" | "toWellFormed" - // The string iterator's step: the full code-POINT character at a UTF-16 - // index (astral chars come back whole where charAt truncates) — for-of - // over strings desugars onto it, advancing by the result's length. - | "cpAt"; + // The string iterator over the WTF-8 bytes: the byte count of a string, + // and the code-point character that starts at a byte offset (a lone + // surrogate is a one-unit character). for-of, spread and Array.from of a + // string walk a byte cursor with them, advancing by the character's + // byte count; the cursor is always a character start below the count. + | "byteLength" + | "cpAtByte"; /** The typed-array/Buffer method surface (bytesIntrinsic). Receiver/arg * conventions (validated): `length`/`byteLength` are property reads → f64; @@ -2480,6 +2619,14 @@ export type IrBytesIntrinsicMethod = | "indexOfNum" | "lastIndexOfNum" | "includesNum" + /** TypedArray.prototype.indexOf / lastIndexOf / includes on any element + * kind: [f64 needle, f64 fromIndex]. indexOf and lastIndexOf compare + * with strict equality (NaN is never found, -0 matches 0), includes with + * SameValueZero (NaN is found). An omitted fromIndex is 0, or +Infinity + * for lastIndexOf (which clamps to the last index). */ + | "indexOfElem" + | "lastIndexOfElem" + | "includesElem" | "fill" | "fillNum" | "fillStr" @@ -3331,7 +3478,17 @@ export type IrExpr = * from a union) whose Web API contract exposes the reference again, such * as stream chunks and abort reasons; ordinary JSON-shaped values retain * the documented deep-copy boundary. */ - | { kind: "dynFrom"; value: IrExpr; fnName?: string; liveRef?: true; type: IrType; loc: SrcLoc } + | { + kind: "dynFrom"; + value: IrExpr; + fnName?: string; + /** A boxed function's `length` when it differs from its parameter + * count (builtin function values). */ + fnLength?: number; + liveRef?: true; + type: IrType; + loc: SrcLoc; + } /** CALLING a dyn value — `fn(a, b)` where fn is checked-dynamic (an * implicit-any JS binding, a dyn record member, a dynKeyGet result). * Arguments are ALREADY dyn-typed (typed values box through dynFrom at @@ -3844,7 +4001,7 @@ export type IrExpr = } /** Standard-library call (`process` members, node:fs functions). `fn` is a * closed union; arg/result types are fixed per member (validated against - * LIB_FN_SIGS). Property READS (`process.argv`, `process.platform`) are + * libFnSignatures()). Property READS (`process.argv`, `process.platform`) are * zero-arg libCalls. Args are BORROWED by the operation (frame temps * release at statement end); refcounted results come back owned (+1) — * `process.argv` returns +1 on ONE interned array (JS identity: @@ -3928,12 +4085,21 @@ export type IrExpr = * unshared source is simply consumed. Values must be plain data * (ordinary objects and arrays without accessors, prototypes, symbols * or frozen state), otherwise the cast throws the path TypeError. */ + /** `origin`: the target type comes from an npm package's declaration; + * an index into `IrModule.declOrigins`. A failing check's TypeError then + * names the declaration and the type patch that would admit the value + * (scr_type_origin.c). */ | { kind: "dynCheck"; value: IrExpr; preserveRefs?: true; adopt?: true; exact?: true; + /** Shape inference's overlay: this conversion checks an + * analysis-proven fact (" :"); a failure is a + * compiler analysis bug and reports itself as one. */ + inferredSite?: string; + origin?: number; type: IrType; loc: SrcLoc; }; @@ -4025,6 +4191,9 @@ function isJsonSafeAt( case "string": case "bool": return true; + // JSON.stringify writes a Date's toJSON(); JSON text never parses to one. + case "date": + return stringify; case "array": return isJsonSafeAt( t.elem, @@ -4123,7 +4292,6 @@ function isJsonSafeAt( // Maps are not JSON (JSON.stringify(new Map()) is "{}" in Node — an // empty-object husk nobody wants; stringify/dynCheck reject instead). case "map": - case "date": // URLSearchParams stringifies as the same "{}" husk — rejected; use // sp.toString() instead. case "searchParams": @@ -4279,6 +4447,7 @@ export function canConvertToDyn( visiting: Set = new Set(), ): boolean { if (isJsonSafeType(t, getRecord, getUnion)) return true; + if (textCodecRecordClass(t, getRecord) !== null) return true; // numeric typed arrays and boxable functions are dyn kinds the walker boxes // ANYWHERE (bytes and functions held by identity), including nested // in records/arrays/unions. isJsonSafeType rejects them, but dynFrom @@ -4325,6 +4494,7 @@ export function canConvertToDyn( // either its result or a promise of it. canBoxDynComposite(a, getRecord, getUnion, visiting) || isDynTypedRefType(a) || + textCodecRecordClass(a, getRecord) !== null || a.kind === "classval" || DYN_HANDLE_KINDS.has(a.kind) || (a.kind === "func" && canBoxFuncIntoDyn(a, getRecord, getUnion, visiting)) || @@ -4404,7 +4574,8 @@ function canBoxDynCompositeAt( case "record": { const shape = getRecord(t.shapeId); if (!shape) return false; - if (recordTextCodecClass(shape) !== null) return false; + // A codec boxes as its own handle wherever it sits (toDynHelper). + if (recordTextCodecClass(shape) !== null) return true; // Recursive shapes answer coinductively, like isJsonSafeType. if (visiting.has(t.shapeId)) return true; visiting.add(t.shapeId); @@ -4505,6 +4676,8 @@ function canDynCheckToAt( visiting.delete(key); return result; } + // A codec handle unboxes to the record it holds. + if (textCodecRecordClass(t, getRecord) !== null) return true; if (t.kind === "record") { const key = typeKey(t); if (visiting.has(key)) return false; diff --git a/packages/compiler/src/ir/js-ic.test.ts b/packages/compiler/src/ir/js-ic.test.ts index f71a1e9db..35de18085 100644 --- a/packages/compiler/src/ir/js-ic.test.ts +++ b/packages/compiler/src/ir/js-ic.test.ts @@ -126,6 +126,140 @@ test("property helpers whose class branches only test capsules are cache sites", expect(sites.site("%dyn.class.property.1")).toEqual({ kind: "set", key: "y" }); }); +test("bags converted between dyn and jsval are still the receiver", () => { + // A program that declares a class returns its bags as jsvals, and the + // helpers read them through js.toDyn. + const toDyn = (value: IrExpr): IrExpr => ({ + kind: "libCall", + fn: "js.toDyn", + args: [value], + type: DYN, + loc, + }); + const fromDyn = (value: IrExpr): IrExpr => ({ + kind: "libCall", + fn: "js.fromDyn", + args: [value], + type: { kind: "jsval" }, + loc, + }); + const jsvalBag = helper( + "%dyn.class.readPropertiesOrEmpty.js", + ["v"], + [capsuleBranch("v"), ret(fromDyn(ref("v")))], + ); + const staticBag = helper("%dyn.class.staticProperties", ["v"], [ret(ref("v"))]); + const getter = getHelper( + "%dyn.class.property.0", + "x", + [capsuleBranch("p.0")], + call(staticBag.name, toDyn(call(jsvalBag.name, ref("p.0")))), + ); + const setter = setHelper("%dyn.class.property.1", "y"); + const write = setter.body[0] as Extract; + const keySet = write.expr as Extract; + keySet.args[0] = toDyn(call(jsvalBag.name, ref("p.0"))); + // A converted value that is not the receiver stays a helper call. + const foreign = getHelper( + "%dyn.class.property.2", + "x", + [], + toDyn(fromDyn({ kind: "dynObjLit", type: DYN, loc } as IrExpr)), + ); + const sites = sitesOf(jsvalBag, staticBag, getter, setter, foreign); + expect(sites.site("%dyn.class.property.0")).toEqual({ kind: "get", key: "x" }); + expect(sites.site("%dyn.class.property.1")).toEqual({ kind: "set", key: "y" }); + expect(sites.site("%dyn.class.property.2")).toBeNull(); +}); + +test("a bag helper may first rebind its parameter to a bag of it", () => { + // Programs with subclasses normalize the value before the class branches. + const normalize = helper("%dyn.class.normalize", ["v"], [capsuleBranch("v"), ret(ref("v"))]); + const rebind = (value: IrExpr): IrStmt => ({ kind: "assign", localId: "v", value, loc }); + const normalized = helper( + "%dyn.class.readPropertiesOrEmpty.normalized", + ["v"], + [rebind(call(normalize.name, ref("v"))), capsuleBranch("v"), ret(ref("v"))], + ); + const replaced = helper( + "%dyn.class.readPropertiesOrEmpty.replaced", + ["v"], + [rebind({ kind: "dynObjLit", type: DYN, loc } as IrExpr), ret(ref("v"))], + ); + const sites = sitesOf( + normalize, + normalized, + replaced, + getHelper("%dyn.class.property.0", "x", [], call(normalized.name, ref("p.0"))), + getHelper("%dyn.class.property.1", "x", [], call(replaced.name, ref("p.0"))), + // The property helper itself normalizes its receiver first, too. + getHelper("%dyn.class.property.2", "x", [ + { kind: "assign", localId: "p.0", value: call(normalize.name, ref("p.0")), loc }, + capsuleBranch("p.0"), + ]), + ); + expect(sites.site("%dyn.class.property.0")).toEqual({ kind: "get", key: "x" }); + expect(sites.site("%dyn.class.property.1")).toBeNull(); + expect(sites.site("%dyn.class.property.2")).toEqual({ kind: "get", key: "x" }); +}); + +test("keyed helpers may test capsules between the key's coercion and the read", () => { + const read = (object: IrExpr, key: string): IrExpr => ({ + kind: "libCall", + fn: "js.fromDyn", + args: [{ kind: "dynKeyGet", value: object, key: ref(key), type: DYN, loc }], + type: { kind: "jsval" }, + loc, + }); + const guard = (test: "symbol" | "nullish", tested: string, object: IrExpr): IrStmt => ({ + kind: "if", + cond: { kind: "dynTest", test, value: ref(tested), type: BOOL, loc }, + then: [ret(read(object, "p.key"))], + else_: null, + loc, + }); + const keyed = (name: string, between: IrStmt[]): IrFunction => + helper( + name, + ["p.0", "p.key"], + [ + capsuleBranch("p.0"), + guard("symbol", "p.key", call(identityBag.name, ref("p.0"))), + guard("nullish", "p.0", ref("p.0")), + { + kind: "varDecl", + localId: "key.string", + init: { + kind: "libCall", + fn: "dyn.toStringCoerce", + args: [ref("p.key")], + type: STRING, + loc, + }, + loc, + }, + ...between, + ret(read(call(identityBag.name, ref("p.0")), "key.string")), + ], + ["key.string"], + ); + const errorBranch: IrStmt = { + kind: "if", + cond: { kind: "dynTest", test: "error", value: ref("p.0"), type: BOOL, loc }, + then: [ret(ref("p.0"))], + else_: null, + loc, + }; + const sites = sitesOf( + keyed("%dyn.class.computed.0", []), + keyed("%dyn.class.computed.1", [capsuleBranch("p.0"), capsuleBranch("p.0", "dyn.classIs")]), + keyed("%dyn.class.computed.2", [errorBranch]), + ); + expect(sites.site("%dyn.class.computed.0")).toEqual({ kind: "keyed" }); + expect(sites.site("%dyn.class.computed.1")).toEqual({ kind: "keyed" }); + expect(sites.site("%dyn.class.computed.2")).toBeNull(); +}); + test("any other prefix or tail keeps the helper call", () => { const errorBranch: IrStmt = { kind: "if", @@ -177,6 +311,23 @@ test("a function holds a site when it calls a classified helper or dispatches a expect(hasJsIcSite({ ...caller, name: "%init.0" }, sites)).toBe(true); }); +test("a function holds a site when an object literal has a constant-key member", () => { + const sites = sitesOf(); + const literal = (key: IrExpr): IrFunction => + helper( + "f", + ["o"], + [ret({ kind: "dynObjLit", fields: [{ key, value: ref("o") }], type: DYN, loc })], + ); + expect(hasJsIcSite(literal(str("x")), sites)).toBe(true); + // `__proto__` sets the prototype, and a computed key has no constant + // name: both keep the generic member store. + expect(hasJsIcSite(literal(str("__proto__")), sites)).toBe(false); + expect(hasJsIcSite(literal(ref("k")), sites)).toBe(false); + // Module initializers build their literals once. + expect(hasJsIcSite({ ...literal(str("x")), name: "%init.0" }, sites)).toBe(false); +}); + test("bag helpers that exit early for values no class branch matches are still the receiver", () => { /** `if (!(v)) return `: the class tables' early exit. */ const exit = (test: "classCapsule" | "funcKind", value: IrExpr, negated = true): IrStmt => ({ @@ -237,3 +388,57 @@ test("bag helpers that exit early for values no class branch matches are still t expect(sites.site("%dyn.class.property.2")).toBeNull(); expect(sites.site("%dyn.class.property.3")).toBeNull(); }); + +test("helpers that answer plain values first with the same operation are cache sites", () => { + const negated = (test: "classCapsule" | "funcKind"): IrExpr => ({ + kind: "dynTest", + test, + negated: true, + value: ref("p.0"), + type: BOOL, + loc, + }); + const exit = (body: IrStmt[], test: IrExpr = negated("classCapsule")): IrStmt => ({ + kind: "if", + cond: test, + then: [{ kind: "if", cond: negated("funcKind"), then: body, else_: null, loc }], + else_: null, + loc, + }); + const store = (key: string): IrStmt[] => [ + { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "dyn.keySet", + args: [ref("p.0"), str(key), ref("p.1")], + type: VOID, + loc, + }, + loc, + }, + ret(null), + ]; + const load = (key: string): IrStmt[] => [ + ret({ kind: "dynKeyGet", value: ref("p.0"), key: str(key), type: DYN, loc }), + ]; + const sites = sitesOf( + setHelper("%dyn.class.property.0", "x", [exit(store("x")), capsuleBranch("p.0")]), + getHelper("%dyn.class.property.1", "x", [exit(load("x")), capsuleBranch("p.0")]), + // An exit for another key, or one plain values do not take. + setHelper("%dyn.class.property.2", "x", [exit(store("y"))]), + setHelper("%dyn.class.property.3", "x", [ + exit(store("x"), { + kind: "dynTest", + test: "classCapsule", + value: ref("p.0"), + type: BOOL, + loc, + }), + ]), + ); + expect(sites.site("%dyn.class.property.0")).toEqual({ kind: "set", key: "x" }); + expect(sites.site("%dyn.class.property.1")).toEqual({ kind: "get", key: "x" }); + expect(sites.site("%dyn.class.property.2")).toBeNull(); + expect(sites.site("%dyn.class.property.3")).toBeNull(); +}); diff --git a/packages/compiler/src/ir/js-ic.ts b/packages/compiler/src/ir/js-ic.ts index 752316db3..5a483c585 100644 --- a/packages/compiler/src/ir/js-ic.ts +++ b/packages/compiler/src/ir/js-ic.ts @@ -61,13 +61,68 @@ export class JsIcSites { if (fn.params.some((p) => p.type.kind !== "dyn")) return null; const receiver = fn.params[0]?.localId; if (receiver === undefined) return null; + const second = fn.params[1]?.localId; + const exits: JsIcSite[] = []; let i = 0; - while (i < fn.body.length && this.capsuleBranch(fn.body[i]!, receiver)) i++; + for (; i < fn.body.length; i++) { + const s = fn.body[i]!; + if (this.capsuleBranch(s, receiver) || this.rebinding(s, receiver)) continue; + const exit = this.plainExit(s, receiver, second); + if (exit === null) break; + exits.push(exit); + } const tail = fn.body.slice(i); - if (fn.params.length === 1) return this.getTail(tail, receiver); - if (fn.params.length !== 2) return null; - const second = fn.params[1]!.localId; - return this.setTail(tail, receiver, second) ?? this.keyedTail(tail, receiver, second); + let site: JsIcSite | null; + if (fn.params.length === 1) site = this.getTail(tail, receiver); + else if (fn.params.length !== 2 || second === undefined) return null; + else site = this.setTail(tail, receiver, second) ?? this.keyedTail(tail, receiver, second); + if (site === null || site.kind === "keyed") return exits.length === 0 ? site : null; + // An exit answers plain values with the operation the site caches. + return exits.every((exit) => exit.kind === site.kind && exit.key === site.key) ? site : null; + } + + /** `if (!classCapsule(receiver)) { [if (!funcKind(receiver))] OP }`, no + * else: an exit that runs the helper's generic operation directly on a + * value no class table matches. OP is `return [js.fromDyn](dynKeyGet(R, + * K))` or `dyn.keySet(R, K, value); return`, with R the receiver (or a + * bag of it). */ + private plainExit(s: IrStmt, receiver: string, value: string | undefined): JsIcSite | null { + const negated = (e: IrExpr, test: "classCapsule" | "funcKind"): boolean => + e.kind === "dynTest" && e.test === test && e.negated === true && isVar(e.value, receiver); + if (s.kind !== "if" || s.else_ !== null || !negated(s.cond, "classCapsule")) return null; + let body = s.then; + const inner = body[0]; + if ( + body.length === 1 && + inner?.kind === "if" && + inner.else_ === null && + negated(inner.cond, "funcKind") + ) + body = inner.then; + const [first, ret] = body; + if (body.length === 1 && first?.kind === "return") { + const read = unwrapFromDyn(first.value); + if ( + read?.kind !== "dynKeyGet" || + read.key.kind !== "strLit" || + !this.bagOf(read.value, receiver) + ) + return null; + return { kind: "get", key: read.key.value }; + } + if ( + body.length !== 2 || + first?.kind !== "exprStmt" || + ret?.kind !== "return" || + ret.value !== null + ) + return null; + const call = first.expr; + if (call.kind !== "libCall" || call.fn !== "dyn.keySet" || call.args.length !== 3) return null; + const [bag, key, stored] = call.args; + if (!bag || !this.bagOf(bag, receiver) || key?.kind !== "strLit") return null; + if (value === undefined || !isVar(stored, value)) return null; + return { kind: "set", key: key.value }; } /** `varDecl bag = BAG(receiver); return [js.fromDyn](bag === receiver ? @@ -109,10 +164,14 @@ export class JsIcSites { /** `if (typeof key === "symbol") return dynKeyGet(BAG(receiver), key); * if (receiver == null) return dynKeyGet(receiver, key); * const s = String(key); return dynKeyGet(BAG(receiver), s)`, each read - * possibly under js.fromDyn. */ + * possibly under js.fromDyn. Capsule branches may precede the final read: + * a program that declares classes looks the string key up in each class's + * key table there. */ private keyedTail(tail: IrStmt[], receiver: string, key: string): JsIcSite | null { - const [symbol, nullish, decl, ret] = tail; - if (tail.length !== 4) return null; + const [symbol, nullish, decl] = tail; + const ret = tail[tail.length - 1]; + if (tail.length < 4) return null; + if (!tail.slice(3, -1).every((s) => this.capsuleBranch(s, receiver))) return null; if (!this.guard(symbol, "symbol", key, (read) => this.bagOf(read, receiver), key)) return null; if (!this.guard(nullish, "nullish", receiver, (read) => isVar(read, receiver), key)) return null; @@ -159,9 +218,12 @@ export class JsIcSites { } /** Whether `e` is the receiver itself for every ordinary object or array: - * the receiver, or a chain of property-bag helpers applied to it. */ + * the receiver, or a chain of property-bag helpers applied to it. A + * conversion between the dyn and jsval forms keeps the object it holds. */ private bagOf(e: IrExpr, receiver: string): boolean { if (isVar(e, receiver)) return true; + if (e.kind === "libCall" && (e.fn === "js.toDyn" || e.fn === "js.fromDyn")) + return e.args.length === 1 && this.bagOf(e.args[0]!, receiver); return ( e.kind === "call" && e.args.length === 1 && @@ -182,6 +244,13 @@ export class JsIcSites { return ret.kind === "return" && ret.value !== null && this.bagOf(ret.value, param); } + /** `param = `, such as the subclass normalization + * `value = %dyn.class.normalize(value)`, which leaves an ordinary object + * or array in `param`. */ + private rebinding(s: IrStmt, param: string): boolean { + return s.kind === "assign" && s.localId === param && this.bagOf(s.value, param); + } + /** A property-bag helper that returns its argument unless a capsule * branch is taken. */ private identityHelper(name: string): boolean { @@ -204,7 +273,10 @@ export class JsIcSites { identity = fn.body .slice(0, -1) - .every((s) => this.capsuleBranch(s, param) || this.kindExit(s, param)) && + .every( + (s) => + this.capsuleBranch(s, param) || this.kindExit(s, param) || this.rebinding(s, param), + ) && last?.kind === "return" && last.value !== null && this.bagOf(last.value, param); @@ -224,20 +296,33 @@ function unwrapFromDyn(e: IrExpr | null | undefined): IrExpr | null { return e; } -/** Whether a function holds a cache site: a call to a classified helper or - * a method dispatch. */ +/** Whether a function holds a cache site: a call to a classified helper, + * a method dispatch, or an object literal member with a constant key. */ export function hasJsIcSite(fn: IrFunction, sites: JsIcSites): boolean { const methods = jsIcMethodSites(fn.name); return !everyStmtList(fn.body, { expr: (e) => !( (methods && e.kind === "dynInvoke") || + (methods && + e.kind === "dynObjLit" && + (e.fields ?? []).some((f) => jsIcLiteralKey(f.key) !== null)) || (e.kind === "call" && sites.site(e.callee) !== null) ), stmt: () => true, }); } +/** The key of an object literal member that its site adds through the + * store cache: a constant string other than `__proto__`, or null. The + * literal starts as an empty object with the default prototype, so each + * such member appends one data property, and the site caches the shape + * transition and shares the key bytes the shape holds. Module + * initializers have no literal sites (jsIcMethodSites). */ +export function jsIcLiteralKey(key: IrExpr): string | null { + return key.kind === "strLit" && key.value !== "__proto__" ? key.value : null; +} + /** Whether method dispatch in a function goes through caches. Module * initializers (`%init.N`) run once, and the methods they call at top level * are overwhelmingly registrations on native handles (`worker.on(...)`, diff --git a/packages/compiler/src/ir/node-index.ts b/packages/compiler/src/ir/node-index.ts index 2aac7df92..be1fa506a 100644 --- a/packages/compiler/src/ir/node-index.ts +++ b/packages/compiler/src/ir/node-index.ts @@ -122,6 +122,36 @@ export class IrNodeIndex { if (statementKinds.has(kind)) this.statements[code] = 1; } + /** The positions of the expressions `wanted` in the index, which an + * index of the same module in another process resolves with + * expressionsAt. Null when an expression is not recorded, or is recorded + * more than once. */ + expressionPositions(wanted: ReadonlySet): number[] | null { + const positions: number[] = []; + if (wanted.size === 0) return positions; + const found = new Set(); + const { nodes, codes, statements } = this; + for (let i = 0; i < nodes.length; i++) { + if (statements[codes[i]!] !== 0) continue; + const node = nodes[i]! as IrExpr; + if (!wanted.has(node)) continue; + if (found.has(node)) return null; + found.add(node); + positions.push(i); + } + return found.size === wanted.size ? positions : null; + } + + /** The expressions at `positions` (expressionPositions). */ + expressionsAt(positions: readonly number[]): IrExpr[] { + return positions.map((position) => { + const code = this.codes[position]; + if (code === undefined || this.statements[code] !== 0) + throw new InternalCompilerError(`no IR expression at index position ${position}`); + return this.nodes[position]! as IrExpr; + }); + } + /** A scan for the nodes of `kinds`, for eachNodeOfKinds. Pass a * constant list: scans are kept by the list's identity. */ scan(kinds: readonly IrNodeKind[]): IrKindScan { diff --git a/packages/compiler/src/ir/paired-while-loops.ts b/packages/compiler/src/ir/paired-while-loops.ts index c096c34fe..030b255f2 100644 --- a/packages/compiler/src/ir/paired-while-loops.ts +++ b/packages/compiler/src/ir/paired-while-loops.ts @@ -6,8 +6,10 @@ import { type IrFunction, type IrLocal, type IrModule, + type IrParam, type IrStmt, } from "./ir.js"; +import { type IrFieldWalker, walkStmtFields } from "./fields.generated.js"; import { everyExprChild, everyStmtChild, mapExprChildren, mapStmtChildren } from "./traverse.js"; type WhileLoop = IrStmt & { kind: "while" }; @@ -99,22 +101,20 @@ function continuesLoop(loop: WhileLoop): boolean { return found; } -/** Mentions of each id in `node`, in any field (a string literal equal to - * an id counts too, which only refuses more loops). */ +/** Mentions of each id in `stmts`, in any field but types (a string + * literal equal to an id counts too, which only refuses more loops). */ function countMentions( - node: unknown, + stmts: readonly IrStmt[], ids: ReadonlySet, mentions: Map, ): void { - if (typeof node === "string") { - if (ids.has(node)) mentions.set(node, (mentions.get(node) ?? 0) + 1); - } else if (Array.isArray(node)) { - for (const item of node) countMentions(item, ids, mentions); - } else if (node !== null && typeof node === "object") { - const record = node as Record; - for (const key in record) - if (key !== "loc" && key !== "type") countMentions(record[key], ids, mentions); - } + const walker: IrFieldWalker = { + node: () => {}, + string: (value) => { + if (ids.has(value)) mentions.set(value, (mentions.get(value) ?? 0) + 1); + }, + }; + for (const stmt of stmts) walkStmtFields(stmt, walker, true); } /** A loop with the qualifying shape, the statements before it in its @@ -184,7 +184,12 @@ function rewriteFunction(fn: IrFunction): IrFunction { const params = new Set(fn.params.map((p) => p.localId)); // A counter this function reaches through its environment. const outer = new Map(); - countMentions([fn.captures ?? [], fn.classCaptures ?? []], ids, outer); + const noteOuter = (param: IrParam): void => { + for (const value of [param.localId, param.name]) + if (ids.has(value)) outer.set(value, (outer.get(value) ?? 0) + 1); + }; + for (const param of fn.captures ?? []) noteOuter(param); + for (const param of fn.classCaptures ?? []) noteOuter(param); let ranges: ReadonlyMap | null = null; const used = new Set(fn.locals.map((l) => l.id)); @@ -192,7 +197,7 @@ function rewriteFunction(fn: IrFunction): IrFunction { const rewritten = new Map>(); for (const { loop, counters, before, nested } of candidates) { const inside = new Map(); - countMentions(loop, new Set(counters), inside); + countMentions([loop], new Set(counters), inside); // Each counter's start: its declaration's initializer, or null for a // parameter. const starts = new Map(); diff --git a/packages/compiler/src/ir/parameter-versions.ts b/packages/compiler/src/ir/parameter-versions.ts index 88232251c..5156d4bff 100644 --- a/packages/compiler/src/ir/parameter-versions.ts +++ b/packages/compiler/src/ir/parameter-versions.ts @@ -184,7 +184,8 @@ export function versionLoopBoundParameters( symbolName: (name: string) => string = (name) => name, ): IrModule { const candidates: { fn: IrFunction; params: Set }[] = []; - const sites = callSites(mod); + // Call sites are read only for functions with loop-bound parameters. + let sites: ReturnType | null = null; const functions = new Map(mod.functions.map((fn) => [fn.name, fn])); const members = new Set((mod.classes ?? []).flatMap((cls) => cls.methods ?? [])); for (const fn of mod.functions) { @@ -207,7 +208,11 @@ export function versionLoopBoundParameters( const local = locals.get(id); if (!local || local.boxed || local.tdz) params.delete(id); } - if (params.size === 0 || size(fn) > MAX_FUNCTION_NODES || keepsSingleBody(fn, sites, functions)) + if ( + params.size === 0 || + size(fn) > MAX_FUNCTION_NODES || + keepsSingleBody(fn, (sites ??= callSites(mod)), functions) + ) continue; candidates.push({ fn, params }); } diff --git a/packages/compiler/src/ir/receiver-readers.ts b/packages/compiler/src/ir/receiver-readers.ts index 3d0e6f35d..067c474ca 100644 --- a/packages/compiler/src/ir/receiver-readers.ts +++ b/packages/compiler/src/ir/receiver-readers.ts @@ -1,6 +1,7 @@ import { isBorrowSafeMath } from "./analysis.js"; import type { IrExpr, IrFunction, IrLibFn, IrStmt } from "./ir.js"; -import { everyStmtList } from "./traverse.js"; +import { eachNodeOfKinds, type IrNodeIndex } from "./node-index.js"; +import { everyExprChild, everyStmtChild } from "./traverse.js"; /** Expression kinds that run no user code of their own and never consult * the ambient receiver stack. Calls are admitted here and resolved through @@ -85,6 +86,25 @@ const RECEIVER_FREE_STMTS = new Set([ "runtimeFence", ]); +/** The kinds whose nodes the readers walk answers true for without looking + * at them: every receiver-free statement, and the receiver-free expressions + * other than calls, library calls, string conversions and data intrinsics. */ +const TRIVIALLY_RECEIVER_FREE = new Set([ + ...RECEIVER_FREE_STMTS, + ...[...RECEIVER_FREE_EXPRS].filter( + (kind) => + kind !== "call" && + kind !== "new" && + kind !== "virtualCall" && + kind !== "libCall" && + kind !== "toString" && + kind !== "arrIntrinsic" && + kind !== "strIntrinsic" && + kind !== "mapIntrinsic" && + kind !== "setIntrinsic", + ), +]); + /** Runtime library calls beyond scalar Math that only compute or build and * park a native error: the f64[] folds behind Math.max/min(...xs), * `new Error(message)` (scr_error_new), and the checked-read TypeError @@ -132,16 +152,76 @@ function opaqueOperand(e: IrExpr): boolean { * this set needs no receiver bracket. */ export class AmbientReceiverReaders { private readers: Set | null = null; + private byMethod: Map | null = null; - constructor(private readonly functions: ReadonlyMap) {} + constructor( + private readonly functions: ReadonlyMap, + /** The module's node index, read instead of walking the bodies it holds. */ + private readonly nodes?: IrNodeIndex, + ) {} ignores(fn: IrFunction): boolean { if (fn.async || fn.generator) return false; return !this.compute().has(fn.name); } - private compute(): Set { - if (this.readers) return this.readers; + /** A signature adapter body that observes the ambient receiver only by + * forwarding it, unchanged, to the original it captures (a callValue on + * that capture whose receiver is the plain `this` read). Such an adapter + * ignores the receiver exactly when its original does. */ + forwardsOnly(fn: IrFunction): boolean { + if (fn.async || fn.generator) return false; + const original = fn.captures?.[0]?.localId; + if (original === undefined) return false; + const readers = this.compute(); + const edge = (name: string): boolean => this.functions.has(name) && !readers.has(name); + const stmt = (node: IrStmt): boolean => + RECEIVER_FREE_STMTS.has(node.kind) && everyStmtChild(node, expr, stmt); + const expr = (node: IrExpr): boolean => { + if (node.kind === "callValue" && node.receiver !== undefined) { + const receiver = node.receiver; + return ( + receiver.kind === "libCall" && + receiver.fn === "dyn.this" && + receiver.args.length === 0 && + node.callee.kind === "varRef" && + node.callee.localId === original && + node.args.every(expr) + ); + } + return this.admits(node, edge) && everyExprChild(node, expr, stmt); + }; + return fn.body.every(stmt); + } + + /** Whether one expression node, apart from its children, leaves the + * ambient receiver alone; `edge` admits a direct callee. */ + private admits(expr: IrExpr, edge: (name: string) => boolean): boolean { + if (!RECEIVER_FREE_EXPRS.has(expr.kind)) return false; + switch (expr.kind) { + case "call": + return edge(expr.callee); + case "new": + return edge(`%${expr.className}.constructor`); + case "virtualCall": { + // Any class's override may run: admit only when every + // same-named method is itself receiver-free. + const targets = this.methods().get(expr.method) ?? []; + return targets.length > 0 && targets.every(edge); + } + case "libCall": + return isBorrowSafeMath(expr.fn) || RECEIVER_FREE_LIB_FNS.has(expr.fn); + case "toString": + return !opaqueOperand(expr.operand); + default: { + const operands = dataIntrinsicOperands(expr); + return operands === null || !operands.some(opaqueOperand); + } + } + } + + private methods(): Map { + if (this.byMethod) return this.byMethod; const byMethod = new Map(); for (const name of this.functions.keys()) { const dot = name.lastIndexOf("."); @@ -151,8 +231,15 @@ export class AmbientReceiverReaders { list.push(name); byMethod.set(method, list); } + this.byMethod = byMethod; + return byMethod; + } + + private compute(): Set { + if (this.readers) return this.readers; const readers = new Set(); const callers = new Map(); + const scan = this.nodes?.scanExcept(TRIVIALLY_RECEIVER_FREE); for (const fn of this.functions.values()) { const callees = new Set(); let reads = fn.async === true || fn.generator !== undefined; @@ -162,31 +249,12 @@ export class AmbientReceiverReaders { return true; }; if (!reads) - reads = !everyStmtList(fn.body, { - stmt: (stmt) => RECEIVER_FREE_STMTS.has(stmt.kind), - expr: (expr) => { - if (!RECEIVER_FREE_EXPRS.has(expr.kind)) return false; - switch (expr.kind) { - case "call": - return edge(expr.callee); - case "new": - return edge(`%${expr.className}.constructor`); - case "virtualCall": { - // Any class's override may run: admit only when every - // same-named method is itself receiver-free. - const targets = byMethod.get(expr.method) ?? []; - return targets.length > 0 && targets.every(edge); - } - case "libCall": - return isBorrowSafeMath(expr.fn) || RECEIVER_FREE_LIB_FNS.has(expr.fn); - case "toString": - return !opaqueOperand(expr.operand); - default: { - const operands = dataIntrinsicOperands(expr); - return operands === null || !operands.some(opaqueOperand); - } - } - }, + eachNodeOfKinds(fn, scan, (node, statement) => { + const free = statement + ? RECEIVER_FREE_STMTS.has((node as IrStmt).kind) + : this.admits(node as IrExpr, edge); + if (!free) reads = true; + return free; }); if (reads) readers.add(fn.name); for (const callee of callees) { diff --git a/packages/compiler/src/ir/row-pairs.test.ts b/packages/compiler/src/ir/row-pairs.test.ts new file mode 100644 index 000000000..8ef01c665 --- /dev/null +++ b/packages/compiler/src/ir/row-pairs.test.ts @@ -0,0 +1,174 @@ +import { expect, test } from "vitest"; +import { BOOL, F64, VOID, type IrExpr, type IrFunction, type IrModule, type IrStmt } from "./ir.js"; +import { pairRowSums } from "./row-pairs.js"; + +const loc = { file: "row-pairs.ts", start: 0, end: 0 }; +const BYTES = { kind: "bytes", elem: "f64" } as const; +const num = (value: number): IrExpr => ({ loc, kind: "numLit", value, type: F64 }); +const ref = (localId: string): IrExpr => ({ loc, kind: "varRef", localId, type: F64 }); +const bytes = (localId: string): IrExpr & { kind: "varRef" } => ({ + loc, + kind: "varRef", + localId, + type: BYTES, +}); +const bin = (op: "+" | "/" | "<", left: IrExpr, right: IrExpr): IrExpr => ({ + loc, + kind: "bin", + op, + left, + right, + type: op === "<" ? BOOL : F64, +}); +const step = (id: string): IrStmt => ({ + loc, + kind: "assign", + localId: id, + value: bin("+", ref(id), num(1)), +}); +const counted = ( + id: string, + start: IrExpr, + end: IrExpr, + body: IrStmt[], +): IrStmt & { kind: "for" } => ({ + loc, + kind: "for", + init: { loc, kind: "varDecl", localId: id, init: start }, + cond: bin("<", ref(id), end), + update: step(id), + body, +}); +const get = (array: string, index: IrExpr): IrExpr => ({ + loc, + kind: "bytesIntrinsic", + method: "get", + receiver: bytes(array), + args: [index], + type: F64, + invalidNaN: true, +}); + +/** for (i = start; i < end; i++) { t = 0; for (j = 0; j < n; j++) t += u[j] / (i + j + 1); v[i] = t } */ +function rowSum( + term: IrExpr = bin("/", get("u", ref("j")), bin("+", bin("+", ref("i"), ref("j")), num(1))), +) { + return counted("i", ref("start"), ref("end"), [ + { loc, kind: "varDecl", localId: "t", init: num(0) }, + counted("j", num(0), ref("n"), [ + { loc, kind: "assign", localId: "t", value: bin("+", ref("t"), term) }, + ]), + { loc, kind: "bytesSet", arr: bytes("v"), index: ref("i"), value: ref("t") }, + ]); +} + +const local = (id: string, type: IrExpr["type"] = F64) => ({ id, name: id, type, mutable: true }); +function mod(body: IrStmt[]): IrModule { + const fn: IrFunction = { + loc, + name: "rows", + params: [], + locals: [ + ...["i", "j", "t", "start", "end", "n"].map((id) => local(id)), + local("u", BYTES), + local("v", BYTES), + ], + returnType: VOID, + body, + }; + return { irVersion: 17, sourceFile: "row-pairs.ts", entry: "rows", functions: [fn] }; +} + +test("a row-sum nest runs two rows per pass behind its bounds and overlap test", () => { + const result = pairRowSums(mod([rowSum()])); + const top = result.functions[0]!.body[0] as IrStmt & { kind: "block" }; + expect(top.kind).toBe("block"); + const [go, from, branch, rest] = top.body as [ + IrStmt & { kind: "varDecl" }, + IrStmt & { kind: "varDecl" }, + IrStmt & { kind: "if" }, + IrStmt & { kind: "for" }, + ]; + // The test: int32 bounds, start <= end, and the stored array disjoint + // from the array the term reads. + expect(JSON.stringify(go.init)).toContain('"fn":"bytes.disjoint"'); + expect(branch.else_).toBeNull(); + // The four bounds, masked to the range the test proved, then the pairs. + expect(branch.then.slice(0, 4)).toMatchObject( + Array(4).fill({ kind: "varDecl", init: { op: "&", right: { value: 2 ** 25 - 1 } } }), + ); + const pairs = branch.then[4] as IrStmt & { kind: "for" }; + expect(pairs.body.filter((s) => s.kind === "bytesSet")).toHaveLength(2); + const [, , inner, last] = pairs.body as [ + IrStmt, + IrStmt, + IrStmt & { kind: "for" }, + IrStmt & { kind: "if" }, + ]; + const column = inner.init as IrStmt & { kind: "varDecl" }; + // Two columns per step: for each, one read of u[j] for both rows, then + // the two sums; the second column reads `column + 1`. + expect(inner.body).toHaveLength(6); + expect(inner.body[0]).toMatchObject({ kind: "varDecl", init: { kind: "bytesIntrinsic" } }); + expect(inner.body[3]).toMatchObject({ + kind: "varDecl", + init: { + kind: "bytesIntrinsic", + args: [{ op: "+", left: { localId: column.localId }, right: { value: 1 } }], + }, + }); + expect(inner.cond).toMatchObject({ op: "<", right: { op: "-", right: { value: 1 } } }); + expect(inner.update).toMatchObject({ value: { op: "+", right: { value: 2 } } }); + // The last column of an odd count, `to - 1`. + expect(last.then).toHaveLength(3); + expect(last.then[0]).toMatchObject({ + kind: "varDecl", + init: { args: [{ op: "-", right: { value: 1 } }] }, + }); + expect(last.else_).toBeNull(); + // The second row's term reads `row + 1`. + const row = pairs.init?.kind === "varDecl" ? pairs.init.localId : ""; + expect((inner.body[2] as IrStmt & { kind: "assign" }).value).toMatchObject({ + op: "+", + right: { + op: "/", + left: { kind: "varRef" }, + right: { + op: "+", + left: { + op: "+", + left: { op: "+", left: { kind: "varRef", localId: row }, right: { value: 1 } }, + }, + }, + }, + }); + // The loop as written runs from `end - ((end - start) & 1)` after the + // pairs, or from the start when the test failed: the nest's one other copy. + expect(from.init).toMatchObject({ + kind: "ternary", + then: { op: "-" }, + else_: { localId: "start" }, + }); + expect(rest.init).toMatchObject({ + kind: "varDecl", + localId: "i", + init: { localId: from.localId }, + }); + expect(top.body).toHaveLength(4); + expect( + result.functions[0]!.locals.map((l) => l.id).filter((id) => id.startsWith("%pair.")), + ).toHaveLength(13); +}); + +test("nests whose term calls, writes or reads the sum keep their loops", () => { + const calls: IrExpr = { loc, kind: "call", callee: "f", args: [ref("j")], type: F64 }; + const readsSum = bin("+", ref("t"), get("u", ref("j"))); + for (const term of [calls, readsSum]) { + const m = mod([rowSum(term)]); + expect(pairRowSums(m)).toBe(m); + } + const stored = rowSum(); + stored.body[2] = { loc, kind: "bytesSet", arr: bytes("v"), index: ref("j"), value: ref("t") }; + const m = mod([stored]); + expect(pairRowSums(m)).toBe(m); +}); diff --git a/packages/compiler/src/ir/row-pairs.ts b/packages/compiler/src/ir/row-pairs.ts new file mode 100644 index 000000000..a3ef789a3 --- /dev/null +++ b/packages/compiler/src/ir/row-pairs.ts @@ -0,0 +1,550 @@ +import { + BOOL, + F64, + type IrExpr, + type IrFunction, + type IrLocal, + type IrModule, + type IrStmt, +} from "./ir.js"; +import { + everyExpr, + everyExprChild, + everyStmtList, + mapExprChildren, + transformStmtList, +} from "./traverse.js"; +import { PRODUCT_COUNTER_BOUND } from "./integer-loops.js"; + +/** Inner-loop expressions above this many nodes keep the loop as written. */ +const MAX_TERM_NODES = 64; + +/** The largest bound a paired nest runs with. Counters below + * PRODUCT_COUNTER_BOUND keep products of their sums exact, so the nest's + * counted loops take integer counters without a run-time guard (and its + * copy of the body). */ +const SMALL_BOUND = PRODUCT_COUNTER_BOUND - 1; + +/** The number operators a term may use: they run no other code and cannot + * throw. */ +const PURE_BINARY = new Set([ + "+", + "-", + "*", + "/", + "%", + "**", + "&", + "|", + "^", + "<<", + ">>", + ">>>", +]); + +/** A loop nest that sums one term per row: + * + * for (let i = start; i < end; i++) { + * let t = ; + * for (let j = from; j < to; j++) t = t + ; + * out[i] = t; + * } + */ +interface RowSum { + row: string; + start: IrExpr; + end: IrExpr; + sum: string; + init: IrExpr & { kind: "numLit" }; + inner: IrStmt & { kind: "for" }; + column: string; + term: IrExpr; + out: IrExpr & { kind: "varRef" }; + /** The typed arrays the term reads. */ + reads: (IrExpr & { kind: "varRef" })[]; +} + +function isRef(e: IrExpr | null | undefined, id: string): boolean { + return e?.kind === "varRef" && e.localId === id; +} + +/** `id = id + 1`, or `id++` as a statement. */ +function unitStep(update: IrStmt | null, id: string): boolean { + if (update?.kind === "exprStmt") + return update.expr.kind === "incDec" && update.expr.localId === id && update.expr.op === "+"; + return ( + update?.kind === "assign" && + update.localId === id && + update.value.kind === "bin" && + update.value.op === "+" && + isRef(update.value.left, id) && + update.value.right.kind === "numLit" && + update.value.right.value === 1 + ); +} + +/** A plain number local: unboxed and outside the temporal dead zone. */ +function plainNumber(locals: ReadonlyMap, id: string): boolean { + const local = locals.get(id); + return local !== undefined && local.type.kind === "f64" && !local.boxed && !local.tdz; +} + +function readsLocal(e: IrExpr, id: string): boolean { + return !everyExpr(e, { stmt: () => true, expr: (x) => !isRef(x, id) }); +} + +/** The row-sum shape of a `for` statement, or null. Bounds and receivers + * must be literals or locals the nest never writes. */ +function matchRowSum( + s: IrStmt & { kind: "for" }, + locals: ReadonlyMap, +): RowSum | null { + if (s.labels?.length || s.init?.kind !== "varDecl" || !s.init.init) return null; + const row = s.init.localId; + if (!plainNumber(locals, row) || !unitStep(s.update, row)) return null; + const cond = s.cond; + if (cond?.kind !== "bin" || cond.op !== "<" || !isRef(cond.left, row)) return null; + const start = s.init.init, + end = cond.right; + if (s.body.length !== 3) return null; + const [decl, inner, store] = s.body as [IrStmt, IrStmt, IrStmt]; + if (decl.kind !== "varDecl" || decl.init?.kind !== "numLit") return null; + const sum = decl.localId; + if (!plainNumber(locals, sum)) return null; + if (inner.kind !== "for" || inner.labels?.length) return null; + if (inner.init?.kind !== "varDecl" || !inner.init.init) return null; + const column = inner.init.localId; + if (!plainNumber(locals, column) || !unitStep(inner.update, column)) return null; + const innerCond = inner.cond; + if (innerCond?.kind !== "bin" || innerCond.op !== "<" || !isRef(innerCond.left, column)) + return null; + if (inner.body.length !== 1) return null; + const add = inner.body[0]!; + if ( + add.kind !== "assign" || + add.localId !== sum || + add.value.kind !== "bin" || + add.value.op !== "+" || + !isRef(add.value.left, sum) + ) + return null; + const term = add.value.right; + if ( + store.kind !== "bytesSet" || + store.arr.kind !== "varRef" || + store.arr.type.kind !== "bytes" || + !isRef(store.index, row) || + !isRef(store.value, sum) + ) + return null; + const out = store.arr; + // Values the nest reads but never writes: the row counter, the column + // counter and the sum are its only writes. + const written = new Set([row, sum, column]); + // A typed array's length is fixed, so a stable receiver's length is + // stable too. + const stable = (e: IrExpr): boolean => + e.kind === "numLit" || + (e.kind === "varRef" && + !written.has(e.localId) && + locals.get(e.localId) !== undefined && + !locals.get(e.localId)!.boxed && + !locals.get(e.localId)!.tdz) || + (e.kind === "bytesIntrinsic" && + e.method === "length" && + e.args.length === 0 && + e.receiver.kind === "varRef" && + e.receiver.type.kind === "bytes" && + stable(e.receiver)); + if (!stable(start) || !stable(end) || !stable(out)) return null; + if (!stable(inner.init.init) || !stable(innerCond.right)) return null; + const reads: (IrExpr & { kind: "varRef" })[] = []; + let nodes = 0; + const pure = (e: IrExpr): boolean => { + if (++nodes > MAX_TERM_NODES) return false; + switch (e.kind) { + case "numLit": + return e.type.kind === "f64"; + case "varRef": + return e.type.kind === "f64" && (e.localId === row || e.localId === column || stable(e)); + case "bin": + return e.type.kind === "f64" && PURE_BINARY.has(e.op) && pure(e.left) && pure(e.right); + case "unary": + return e.type.kind === "f64" && (e.op === "-" || e.op === "~") && pure(e.operand); + case "bytesIntrinsic": + if (stable(e)) return true; + if ( + e.method !== "get" || + e.args.length !== 1 || + e.receiver.kind !== "varRef" || + e.receiver.type.kind !== "bytes" || + !stable(e.receiver) + ) + return false; + reads.push(e.receiver); + return pure(e.args[0]!); + default: + return false; + } + }; + if (!pure(term) || readsLocal(term, sum)) return null; + return { row, start, end, sum, init: decl.init, inner, column, term, out, reads }; +} + +/** A fresh copy of `e` with `substitute` applied to its local reads and + * `replace` to the nodes it answers for. */ +function copy( + e: IrExpr, + substitute: (ref: IrExpr & { kind: "varRef" }) => IrExpr, + replace: (node: IrExpr) => IrExpr | null = () => null, +): IrExpr { + const replaced = replace(e); + if (replaced !== null) return replaced; + if (e.kind === "varRef") return substitute(e); + return mapExprChildren( + { ...e }, + (child) => copy(child, substitute, replace), + (stmt) => stmt, + ); +} + +const bin = ( + op: "+" | "-" | "/" | "&" | "|", + left: IrExpr, + right: IrExpr, + loc: IrExpr["loc"], +): IrExpr => ({ + kind: "bin", + op, + left, + right, + type: F64, + loc, +}); + +/** Runs the row sums of `match` two rows per pass when the test holds and + * as written otherwise. Each pass takes two columns per step, then the + * last column when their count is odd. */ +function pairRows( + s: IrStmt & { kind: "for" }, + match: RowSum, + fresh: (name: string) => string, + done: Set, +): IrStmt { + const loc = s.loc; + const ref = (id: string): IrExpr => ({ kind: "varRef", localId: id, type: F64, loc }); + const num = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc }); + const again = (e: IrExpr): IrExpr => copy(e, (r) => ({ ...r })); + const row = fresh("row"), + sum = fresh("sum"), + next = fresh("next"), + column = fresh("column"); + // Element reads that do not depend on the row are read once per column + // and shared by both rows. + const sharedReads: IrExpr[] = []; + const collect = (e: IrExpr): boolean => { + if (e.kind === "bytesIntrinsic" && e.method === "get" && !readsLocal(e, match.row)) { + sharedReads.push(e); + return true; + } + return everyExprChild(e, collect, () => true); + }; + collect(match.term); + /** Both rows' additions at the column `columnAt()`: its shared element + * reads, then each row's sum. The second row's term reads `row + 1`, and + * the second column's `column + 1`, so their counter sums stay visible to + * the counted-loop analysis. */ + const step = (columnAt: () => IrExpr): IrStmt[] => { + const at = (r: IrExpr & { kind: "varRef" }): IrExpr => + r.localId === match.column ? columnAt() : { ...r }; + const shared = new Map(); + const elements: IrStmt[] = sharedReads.map((read) => { + const id = fresh("element"); + shared.set(read, id); + return { kind: "varDecl", localId: id, init: copy(read, at), loc }; + }); + const termAt = (rowExpr: () => IrExpr): IrExpr => + copy( + match.term, + (r) => (r.localId === match.row ? rowExpr() : at(r)), + (node) => { + const id = shared.get(node); + return id === undefined ? null : { kind: "varRef", localId: id, type: node.type, loc }; + }, + ); + return [ + ...elements, + { + kind: "assign", + localId: sum, + value: bin( + "+", + ref(sum), + termAt(() => ref(row)), + loc, + ), + loc, + }, + { + kind: "assign", + localId: next, + value: bin( + "+", + ref(next), + termAt(() => bin("+", ref(row), num(1), loc)), + loc, + ), + loc, + }, + ]; + }; + const innerCond = match.inner.cond as IrExpr & { kind: "bin" }; + const columnStart = (match.inner.init as IrStmt & { kind: "varDecl" }).init!; + // The bounds, as the paired nest reads them (see `small` below). + const low = fresh("low"), + high = fresh("high"), + first = fresh("first"), + last = fresh("last"); + const bounds: IrStmt[] = ( + [ + [low, match.start], + [high, match.end], + [first, columnStart], + [last, innerCond.right], + ] as const + ).map(([id, bound]) => ({ + kind: "varDecl", + localId: id, + init: bin("&", again(bound), num(SMALL_BOUND), loc), + loc, + })); + const pairs: IrStmt = { + kind: "for", + init: { kind: "varDecl", localId: row, init: ref(low), loc }, + cond: { + kind: "bin", + op: "<", + left: ref(row), + right: bin("-", ref(high), num(1), loc), + type: BOOL, + loc, + }, + update: { kind: "assign", localId: row, value: bin("+", ref(row), num(2), loc), loc }, + body: [ + { kind: "varDecl", localId: sum, init: { ...match.init }, loc }, + { kind: "varDecl", localId: next, init: { ...match.init }, loc }, + // Both column bounds are int32, so `column < to - 1` is + // `column + 1 < to`. Each row adds its columns in order. + { + kind: "for", + init: { kind: "varDecl", localId: column, init: ref(first), loc }, + cond: { + ...innerCond, + left: ref(column), + right: bin("-", ref(last), num(1), loc), + }, + update: { kind: "assign", localId: column, value: bin("+", ref(column), num(2), loc), loc }, + body: [...step(() => ref(column)), ...step(() => bin("+", ref(column), num(1), loc))], + loc, + }, + // An odd column count leaves the last column, `to - 1`. + { + kind: "if", + cond: { + kind: "logical", + op: "&&", + left: { ...innerCond, left: ref(first), right: ref(last) }, + right: { + kind: "bin", + op: "===", + left: bin("&", bin("-", ref(last), ref(first), loc), num(1), loc), + right: num(1), + type: BOOL, + loc, + }, + type: BOOL, + loc, + }, + then: step(() => bin("-", ref(last), num(1), loc)), + else_: null, + loc, + }, + { kind: "bytesSet", arr: { ...match.out }, index: ref(row), value: ref(sum), loc }, + { + kind: "bytesSet", + arr: { ...match.out }, + index: bin("+", ref(row), num(1), loc), + value: ref(next), + loc, + }, + ], + loc, + }; + // Every bound is an integer in [0, SMALL_BOUND] and not -0, so `row < + // end - 1` is `row + 1 < end` (and the same for the columns), and the + // masks in `bounds` leave the bounds unchanged while proving both + // counters' ranges. + // The nest has at least two rows. The column bounds' tests and the + // overlap tests run only then, so they never read an array the program + // assigns only when there are rows. Rows are + // independent when the written array shares no byte with an array the + // term reads; an array that is not there answers false. + const int32 = (e: IrExpr): IrExpr => ({ + kind: "bin", + op: "===", + left: bin("|", again(e), num(0), loc), + right: again(e), + type: BOOL, + loc, + }); + const compare = (op: "<=" | ">=", e: IrExpr, value: number): IrExpr => ({ + kind: "bin", + op, + left: again(e), + right: num(value), + type: BOOL, + loc, + }); + // `1 / e > 0` holds for +0 and every positive bound, but not for -0: + // masking would turn a -0 start into +0, which a term can tell apart. + const small = (e: IrExpr): IrExpr[] => [ + int32(e), + { kind: "bin", op: ">", left: bin("/", num(1), again(e), loc), right: num(0), type: BOOL, loc }, + compare("<=", e, SMALL_BOUND), + ]; + const tests: IrExpr[] = [ + ...small(match.start), + ...small(match.end), + { + kind: "bin", + op: "<", + left: bin("+", again(match.start), num(1), loc), + right: again(match.end), + type: BOOL, + loc, + }, + ...small(columnStart), + ...small(innerCond.right), + ...match.reads.map((read): IrExpr => ({ + kind: "libCall", + fn: "bytes.disjoint", + args: [{ ...match.out }, { ...read }], + type: BOOL, + loc, + })), + ]; + const test = tests.reduce((left, right) => ({ + kind: "logical", + op: "&&", + left, + right, + type: BOOL, + loc, + })); + // The loop as written then runs the rows the pairs left: the last one when + // the count is odd (from `end - ((end - start) & 1)`), or all of them when + // the test failed. It is the nest's only other copy. + const go = fresh("go"), + from = fresh("from"); + const paired: IrExpr = { kind: "bin", op: "===", left: ref(go), right: num(1), type: BOOL, loc }; + const rest: IrStmt = { + ...s, + init: { kind: "varDecl", localId: match.row, init: ref(from), loc }, + }; + done.add(rest); + return { + kind: "block", + body: [ + { + kind: "varDecl", + localId: go, + init: { kind: "ternary", cond: test, then: num(1), else_: num(0), type: F64, loc }, + loc, + }, + { + kind: "varDecl", + localId: from, + init: { + kind: "ternary", + cond: paired, + then: bin( + "-", + again(match.end), + bin("&", bin("-", again(match.end), again(match.start), loc), num(1), loc), + loc, + ), + else_: again(match.start), + type: F64, + loc, + }, + loc, + }, + { kind: "if", cond: { ...paired }, then: [...bounds, pairs], else_: null, loc }, + rest, + ], + loc, + }; +} + +/** Runs loop nests that sum one term per row (spectral-norm's `au`) two rows + * per pass. + * + * Each row's sum keeps its order of additions, so every sum has the bits + * the nest computes as written: the second row's term is the same + * expression of `row + 1`. The term only reads numbers, the counters and + * typed-array elements, so it runs no other code and cannot throw. The + * nest runs paired when both row bounds are int32 (so `row < end - 1` is + * `row + 1 < end`), there are at least two rows, and the array the rows + * store into shares no byte with any array the term reads, so no row reads + * another row's result. An odd row count runs its last row as written. + * Otherwise the nest runs as written. Two independent sums take turns in + * the inner loop, so neither waits on the other's addition. + * + * The inner loop takes two columns per step, each row adding the first + * column's term and then the second's, and the last column on its own + * when the count is odd, so the additions keep their order. Both column + * bounds must also be int32 (so `column < to - 1` is `column + 1 < to`). + * Neighboring rows and columns share counter sums (`row + 1 + column` and + * `row + (column + 1)`), and the step's divisions have no dependence + * between them. */ +export function pairRowSums(mod: IrModule): IrModule { + let changed = false; + const functions = mod.functions.map((fn): IrFunction => { + if (fn.async || fn.generator) return fn; + const locals = new Map(fn.locals.map((local) => [local.id, local])); + // Most bodies hold no row sum: find one before copying the body. + let found = false; + everyStmtList(fn.body, { + expr: () => !found, + stmt: (s) => { + if (s.kind === "for" && matchRowSum(s, locals) !== null) found = true; + return !found; + }, + }); + if (!found) return fn; + const added: IrLocal[] = []; + const ids = new Set(fn.locals.map((local) => local.id)); + const fresh = (name: string): string => { + let id = `%pair.${name}.${ids.size}`; + while (ids.has(id)) id += "_"; + ids.add(id); + added.push({ id, name: `%pair.${name}`, type: F64, mutable: true }); + return id; + }; + const paired = new Set(); + const body = transformStmtList(fn.body, { + expr: (e) => e, + stmt: (s) => { + if (s.kind !== "for" || paired.has(s)) return s; + const match = matchRowSum(s, locals); + if (match === null) return s; + paired.add(s); + return pairRows(s, match, fresh, paired); + }, + }); + if (added.length === 0) return fn; + changed = true; + return { ...fn, locals: [...fn.locals, ...added], body }; + }); + return changed ? { ...mod, functions } : mod; +} diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 9c4c493be..a1d3ea990 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -2,12 +2,14 @@ import type { IrClassDef, IrExpr, IrModule, IrRecordShape, IrType, IrUnionDef } import type { IrLibFn } from "./builtin-signatures.js"; import { isDirentShape, + recordHandleCodecClass, + recordTextCodecClass, RUNTIME_EMITTER_CLASS, RUNTIME_EVENT_CLASS, RUNTIME_EVENT_TARGET_CLASS, RUNTIME_STREAM_CLASSES, } from "./ir.js"; -import { everyModuleNode } from "./traverse.js"; +import { everyModuleNode, everyStmtList } from "./traverse.js"; import { eachNodeOfKinds, type IrNodeIndex } from "./node-index.js"; import { JsIcSites, hasJsIcSite } from "./js-ic.js"; @@ -99,6 +101,13 @@ const DYN_ASYNC_LIB_FNS: ReadonlySet = new Set([ "process.setDeprecationFlag", "process.setEmitWarning", "punycode.warn", + // DEP0005 for the deprecated Buffer constructor. + "buffer.deprecation", + // util.aborted answers a checked-dynamic promise, and so do the + // functions util.debounce and util.throttle return. + "util.aborted", + "util.debounce", + "util.throttle", "timers.immediatePromise", // The performance timeline (scr_perf.c, linked with this unit for its // MaxPerformanceEntryBufferExceededWarning). @@ -116,12 +125,16 @@ const DYN_ASYNC_LIB_FNS: ReadonlySet = new Set([ "perf.measure", // Blob and File (scr_blob.c, linked with this unit, which boxes their // reads as dyn promises). + "als.bindCallbackDyn", "als.disable", "als.enterWith", "als.exitRun", "als.get", + "als.name", "als.new", + "als.newOptions", "als.run", + "als.snapshot", "blob.arrayBuffer", "blob.bytes", "blob.fileNew", @@ -136,7 +149,11 @@ const DYN_ASYNC_LIB_FNS: ReadonlySet = new Set([ "util.promisifyDyn", "util.debuglog", "util.deprecate", + "util.diff", + "util.extend", "util.inherits", + "util.inheritsPrototype", + "util.isArrayDeprecated", "util.styleText", ]); @@ -165,7 +182,11 @@ const UTIL_CALLABLE_LIB_FNS: ReadonlySet = new Set([ "util.callbackify", "util.debuglog", "util.deprecate", + "util.diff", + "util.extend", "util.inherits", + "util.inheritsPrototype", + "util.isArrayDeprecated", ]); /** Entry points that link the regex unit: the regex and glob matchers (fs.glob's walk compiles its patterns with the glob matcher), and readline's terminal unit (scr_readline_tty.c), which measures with the regex unit's stripVTControlCharacters. */ @@ -256,6 +277,10 @@ const STREAM_LIB_FNS: ReadonlySet = new Set([ "stream.prop", "stream.stdioWritable", "stream.unboxDyn", + "stream.dynIterableSource", + "stream.dynSourceValue", + "stream.dynTransformResult", + "stream.unboxDynStage", "stream.handleStream", "stream.unpipeDyn", "stream.workerStdio", @@ -292,6 +317,7 @@ const READABLE_DYN_ASYNC_LIB_FNS: ReadonlySet = new Set([ "stream.duplexFromAsyncFn", "stream.duplexFromGenSignal", "stream.duplexFromAsyncFnSignal", + "stream.unboxDynStage", ]); /** libCall name prefixes of the Readable entry points that await through the dyn-async unit. */ @@ -309,6 +335,8 @@ const DYN_INVOKE_LIB_FNS: ReadonlySet = new Set([ "dyn.definePrototypeProps", "dyn.functionApply", "dyn.globalConstructor", + "dyn.namespaceDefine", + "dyn.namespaceNew", "dyn.objCreateWithProperties", "dyn.objectTag", "stream.dynNative", @@ -361,6 +389,10 @@ const SYMBOL_LIB_FNS: ReadonlySet = new Set([ "dyn.usingDispose", "dyn.usingMethod", "util.isDeepStrictEqual", + // The Buffer function value's Symbol(kIsEncodingSymbol) member + "buffer.constructor", + "buffer.staticValue", + "buffer.prototypeMember", ]); /** Entry points that link the querystring unit. */ @@ -415,7 +447,10 @@ interface TypeTables { unions: Map; classes: Map; /** mentionsDgramSocket's carriers, computed on first use. */ - dgramCarriers?: DgramCarriers; + dgramCarriers?: TypeCarriers; + /** mentionsTextCodec's and mentionsTextDecoder's carriers. */ + textCodecCarriers?: TypeCarriers; + textDecoderCarriers?: TypeCarriers; } /** Whether boxing a value of type `t` as dyn boxes a promise, which goes @@ -479,30 +514,42 @@ function boxesPromise(t: IrType, tables: TypeTables, seen: Set): boolean } } -/** The record shapes, unions and classes whose values carry a dgram socket - * (directly or through other members), computed once per module. */ -interface DgramCarriers { +/** The record shapes, unions and classes whose values carry a leaf type + * (directly or through other members), computed once per module and leaf. */ +interface TypeCarriers { records: Set; unions: Set; anyClass: boolean; } -/** Whether `t` carries a dgram socket, given the carrier sets so far: - * structural through functions (parameters and result), promises and - * collections; named records, unions and class instances by the sets. */ -function carriesDgram(t: IrType, carriers: DgramCarriers): boolean { +/** The leaf a carrier set tracks: a dgram socket, a codec record. */ +type CarrierLeaf = (t: IrType, tables: TypeTables) => boolean; + +/** Whether `t` carries a leaf, given the carrier sets so far: structural + * through functions (parameters and result), promises and collections; + * named records, unions and class instances by the sets. */ +function carriesLeaf( + t: IrType, + carriers: TypeCarriers, + leaf: CarrierLeaf, + tables: TypeTables, +): boolean { + if (leaf(t, tables)) return true; switch (t.kind) { - case "dgramSocket": - return true; case "func": - return t.params.some((p) => carriesDgram(p, carriers)) || carriesDgram(t.ret, carriers); + return ( + t.params.some((p) => carriesLeaf(p, carriers, leaf, tables)) || + carriesLeaf(t.ret, carriers, leaf, tables) + ); case "promise": - return carriesDgram(t.inner, carriers); + return carriesLeaf(t.inner, carriers, leaf, tables); case "array": case "set": - return carriesDgram(t.elem, carriers); + return carriesLeaf(t.elem, carriers, leaf, tables); case "map": - return carriesDgram(t.key, carriers) || carriesDgram(t.value, carriers); + return ( + carriesLeaf(t.key, carriers, leaf, tables) || carriesLeaf(t.value, carriers, leaf, tables) + ); case "record": return carriers.records.has(t.shapeId); case "union": @@ -515,25 +562,29 @@ function carriesDgram(t: IrType, carriers: DgramCarriers): boolean { } } -/** The module's dgram carriers: a fixpoint over its records, unions and +/** The module's carriers of `leaf`: a fixpoint over its records, unions and * class fields. */ -function dgramCarriers(tables: TypeTables): DgramCarriers { - const carriers: DgramCarriers = { records: new Set(), unions: new Set(), anyClass: false }; +function leafCarriers(tables: TypeTables, leaf: CarrierLeaf): TypeCarriers { + const carriers: TypeCarriers = { records: new Set(), unions: new Set(), anyClass: false }; + const carries = (t: IrType): boolean => carriesLeaf(t, carriers, leaf, tables); for (let changed = true; changed;) { changed = false; for (const [id, shape] of tables.records) - if (!carriers.records.has(id) && shape.fields.some((f) => carriesDgram(f.type, carriers))) { + if ( + !carriers.records.has(id) && + (leaf({ kind: "record", shapeId: id }, tables) || shape.fields.some((f) => carries(f.type))) + ) { carriers.records.add(id); changed = true; } for (const [id, def] of tables.unions) - if (!carriers.unions.has(id) && def.arms.some((a) => carriesDgram(a, carriers))) { + if (!carriers.unions.has(id) && def.arms.some(carries)) { carriers.unions.add(id); changed = true; } if ( !carriers.anyClass && - [...tables.classes.values()].some((c) => c.fields.some((f) => carriesDgram(f.type, carriers))) + [...tables.classes.values()].some((c) => c.fields.some((f) => carries(f.type))) ) { carriers.anyClass = true; changed = true; @@ -542,13 +593,42 @@ function dgramCarriers(tables: TypeTables): DgramCarriers { return carriers; } +const dgramLeaf: CarrierLeaf = (t) => t.kind === "dgramSocket"; + /** Whether converting a value of type `t` to or from dyn converts a dgram * socket, which goes through scr_dgram_dyn.c: the socket itself, or a * function, collection, record, union, promise or class instance that * carries one (boxed functions convert their parameters too). */ function mentionsDgramSocket(t: IrType, tables: TypeTables): boolean { - tables.dgramCarriers ??= dgramCarriers(tables); - return carriesDgram(t, tables.dgramCarriers); + tables.dgramCarriers ??= leafCarriers(tables, dgramLeaf); + return carriesLeaf(t, tables.dgramCarriers, dgramLeaf, tables); +} + +const textCodecLeaf: CarrierLeaf = (t, tables) => { + if (t.kind !== "record") return false; + const shape = tables.records.get(t.shapeId); + return shape !== undefined && recordHandleCodecClass(shape) !== null; +}; + +const textDecoderLeaf: CarrierLeaf = (t, tables) => { + if (t.kind !== "record") return false; + const shape = tables.records.get(t.shapeId); + return shape !== undefined && recordTextCodecClass(shape) === "TextDecoder"; +}; + +/** Whether converting a value of type `t` to or from dyn converts a + * TextEncoder, TextDecoder or StringDecoder record, which boxes it as a handle of + * scr_text_codec_dyn.c (the same carrier walk as dgram sockets). */ +function mentionsTextCodec(t: IrType, tables: TypeTables): boolean { + tables.textCodecCarriers ??= leafCarriers(tables, textCodecLeaf); + return carriesLeaf(t, tables.textCodecCarriers, textCodecLeaf, tables); +} + +/** The TextDecoder half: its boxed decode runs scr_text_decode_stream, + * which reaches the legacy decoders. */ +function mentionsTextDecoder(t: IrType, tables: TypeTables): boolean { + tables.textDecoderCarriers ??= leafCarriers(tables, textDecoderLeaf); + return carriesLeaf(t, tables.textDecoderCarriers, textDecoderLeaf, tables); } /** The libCall fns served by the OPTIONAL events unit (scr_events.c): @@ -656,6 +736,10 @@ interface RuntimeFeatureRule { /** Gives every rule in the table one type. */ const rule = (r: RuntimeFeatureRule): RuntimeFeatureRule => r; +/* The node:url and WHATWG URL entry points: every URL value comes from + * one, and parsing one can reach the UTS #46 host processing. */ +const URL_PREFIXES: readonly string[] = ["url."]; + /* The HTTP, HTTPS, HTTP/2 and TLS surfaces, which link the net unit. */ const NET_PREFIXES: readonly string[] = ["net.", "http.", "tls.", "https.", "http2."]; @@ -679,6 +763,10 @@ export const PKI_LIB_FNS: ReadonlySet = new Set([ "crypto.generateKeyPairSync", "crypto.signOneshot", "crypto.verifyOneshot", + "crypto.x509PublicKey", + "crypto.x509Verify", + "crypto.x509CheckPrivateKey", + "crypto.diffieHellman", ]); /** The WebCrypto algorithms scr_webcrypto_pki.c implements over the pki @@ -794,6 +882,15 @@ const RUNTIME_FEATURE_RULES = { (node.kind === "dynFrom" && mentionsDgramSocket(node.value.type, tables())) || (node.kind === "dynCheck" && mentionsDgramSocket(node.type, tables())), }), + /** A TextEncoder, TextDecoder or StringDecoder record converts to or from an untyped + * value (its handle box, or a boxed function, record or collection that + * carries one): scr_text_codec_dyn.c. */ + textCodecDyn: rule({ + fns: ["text.codecIs"], + expr: (node, tables) => + (node.kind === "dynFrom" && mentionsTextCodec(node.value.type, tables())) || + (node.kind === "dynCheck" && mentionsTextCodec(node.type, tables())), + }), /** node:dns/promises: scr_dns_promises.c over the dgram unit. */ dnsPromises: rule({ fns: ["dns.promisesLookup", "dns.promisesLookupService"] }), dynAsync: rule({ @@ -802,10 +899,11 @@ const RUNTIME_FEATURE_RULES = { mod.functions.some( (fn) => fn.async === true && fn.generator === undefined && fn.returnType.kind === "dyn", ), - // The legacy url functions, crypto.createCipheriv and - // ecdh.setPublicKey emit their deprecation warnings through the - // dyn-async unit (WebCrypto's encrypt and decrypt link scr_cipher.c - // too); scr_wasm.c boxes its handles and promises through it. + // The legacy url functions, crypto.createCipheriv, + // ecdh.setPublicKey and generateKeyPair's rsa-pss options emit their + // deprecation warnings through the dyn-async unit (WebCrypto's + // encrypt and decrypt link scr_cipher.c too); scr_wasm.c boxes its + // handles and promises through it. fns: [ ...DYN_ASYNC_LIB_FNS, ...READABLE_DYN_ASYNC_LIB_FNS, @@ -816,6 +914,8 @@ const RUNTIME_FEATURE_RULES = { "webcrypto.wrapKey", "webcrypto.unwrapKey", "crypto.ecdhNew", + "crypto.generateKeyPairCb", + "crypto.generateKeyPairSync", ], prefixes: [...READABLE_DYN_ASYNC_PREFIXES, "wasm."], expr: (node, tables) => @@ -844,6 +944,7 @@ const RUNTIME_FEATURE_RULES = { ...UTIL_CALLABLE_LIB_FNS, ...BUILTIN_CLASS_LIB_FNS, "util.promisifyDyn", + "buffer.prototypeMember", ], // Constant-key reads of Object.prototype member names answer the // inherited member through scr_dyn_invoke.c. @@ -866,16 +967,37 @@ const RUNTIME_FEATURE_RULES = { /** The declaration-typed boundary's adoption (scr_decl_adopt.c): its * casts, and closures boxed in a boundary program, which adopt their * arguments. */ - declAdopt: rule({ fns: ["dyn.declAdopt"], initial: (mod) => mod.declBoundary === true }), + declAdopt: rule({ + fns: ["dyn.declAdopt", "dyn.adoptShared"], + initial: (mod) => mod.declBoundary === true, + }), + // Checked conversions into package-declared types name the declaration + // in their TypeError (scr_type_origin.c). + typeOrigins: rule({ initial: (mod) => (mod.declOrigins?.length ?? 0) > 0 }), /** for await over checked-dynamic values (scr_dyn_for_await.c). */ dynForAwait: rule({ fns: ["dyn.asyncIteratorOpen", "dyn.asyncIteratorWasSync"] }), /** Buffer.prototype methods called on checked-dynamic values * (scr_bytes_dyn.c). */ dynBuffer: rule({ + // Buffer.prototype members read as values dispatch to the same entries. + fns: ["buffer.prototypeMember"], expr: (node, _tables) => node.kind === "dynInvoke" && DYN_BUFFER_METHODS.has(node.method), }), + /** createRequire's require and require.resolve with specifiers computed + * at run time (scr_require.c). */ + computedRequire: rule({ fns: ["module.requireComputed", "module.resolveComputed"] }), /** Builtin functions read as values (scr_builtin_values.c). */ - builtinValues: rule({ fns: ["buffer.byteLenValue", "buffer.allocValue", "fs.realpathValue"] }), + builtinValues: rule({ + fns: [ + "buffer.byteLenValue", + "buffer.allocValue", + "fs.realpathValue", + "buffer.constructor", + "buffer.installValue", + "buffer.staticValue", + "buffer.prototypeMember", + ], + }), /** The stream unit emits through the emitter unit, also when the * program constructs no stream (a pipeline of functions). */ /** scr_ecdh.c over the mbedTLS archive; DEP0031 reports through the @@ -899,9 +1021,20 @@ const RUNTIME_FEATURE_RULES = { (mod.classes ?? []).some( (c) => c.name === RUNTIME_EVENT_TARGET_CLASS || c.name === RUNTIME_EVENT_CLASS, ), - fns: ["events.addAbortListener", ...ABORT_SIGNAL_LIB_FNS], + // util.debounce and util.throttle register options.signal's listener. + fns: [ + "events.addAbortListener", + "util.aborted", + "util.debounce", + "util.throttle", + ...ABORT_SIGNAL_LIB_FNS, + ], prefixes: ["event."], }), + /** util.MIMEType and util.MIMEParams (scr_mime.c). */ + mime: rule({ prefixes: ["mime."] }), + /** util.debounce and util.throttle (scr_util_timing.c). */ + utilTiming: rule({ fns: ["util.debounce", "util.throttle"] }), /** events.once(emitter, name, { signal }) (scr_events_once_signal.c). */ eventsOnceSignal: rule({ fns: ["events.onceSignal"] }), fetch: rule({ prefixes: ["fetch."] }), @@ -942,7 +1075,12 @@ const RUNTIME_FEATURE_RULES = { prefixes: ["insp.", "console.", "test."], libCall: (node) => node.fn === "rl.terminal" && !isDynUndefined(node.args[3]!), }), - legacyTextDecoder: rule({ fns: [...LEGACY_TEXT_DECODER_LIB_FNS] }), + legacyTextDecoder: rule({ + fns: [...LEGACY_TEXT_DECODER_LIB_FNS], + expr: (node, tables) => + (node.kind === "dynFrom" && mentionsTextDecoder(node.value.type, tables())) || + (node.kind === "dynCheck" && mentionsTextDecoder(node.type, tables())), + }), /** The program calls mock.timers members: its timer calls go through * scr_test.c's mock-aware entry points. */ mockTimers: rule({ prefixes: ["test.mockTimers", "test.ctxMockTimers"] }), @@ -979,9 +1117,10 @@ const RUNTIME_FEATURE_RULES = { /** scr_url_legacy.c parses and stringifies queries. */ qs: rule({ fns: [...QS_LIB_FNS, ...LEGACY_URL_LIB_FNS] }), /** scr_url_legacy.c lowercases and normalizes hostnames with the regex - * unit's libunicode, NODE_DEBUG matching in scr_util_callable.c and - * readline's terminal unit use the regex unit, and locale case mapping - * runs its default conversion. */ + * unit's libunicode, the WHATWG URL host parser runs UTS #46 through + * scr_idna.c (which links with the regex unit), NODE_DEBUG matching in + * scr_util_callable.c and readline's terminal unit use the regex unit, + * and locale case mapping runs its default conversion. */ regex: rule({ fns: [ ...REGEX_LIB_FNS, @@ -990,13 +1129,17 @@ const RUNTIME_FEATURE_RULES = { "rl.terminal", "str.localeCase", ], + prefixes: URL_PREFIXES, expr: (node, _tables) => node.kind === "regexLit" || node.kind === "regexIntrinsic" || (node.kind === "strIntrinsic" && CASE_MAPPING_STR_METHODS.has(node.method)), - types: ["regex"], + types: ["regex", "url"], }), searchParams: rule({ fns: ["url.searchParams"], prefixes: ["sp."], types: ["searchParams"] }), + /** URL programs install the IDNA unit's ToASCII for the WHATWG host + * parser (scr_url_idna_install). */ + urlIdna: rule({ prefixes: URL_PREFIXES, types: ["url"] }), /** stdin setEncoding / string data listeners / stdio 'resize' events. A * typed string 'data' listener takes setEncoding-decoded chunks; the * process stream object's setEncoding and 'resize' members are reached @@ -1031,12 +1174,46 @@ const RUNTIME_FEATURE_RULES = { utilPromisify: rule({ fns: ["util.promisifyDyn"] }), /** process.emitWarning is assigned (process.setEmitWarning). */ warningReplace: rule({ fns: ["process.setEmitWarning"] }), + /** Module namespace objects as checked-dynamic values + * (scr_module_namespace.c). */ + moduleNamespace: rule({ fns: ["module.namespaceValue"] }), /** The WebAssembly API (scr_wasm.c). */ wasm: rule({ prefixes: ["wasm."] }), /** crypto.subtle importKey/sign/verify/digest and CryptoKey: * scr_webcrypto.c. */ webcrypto: rule({ prefixes: ["webcrypto."] }), + /** scr_x509.c: the X509Certificate fields beyond the validity window and + * fingerprint, which stay in scr_lib.c. */ + x509: rule({ + fns: [ + "crypto.x509Ca", + "crypto.x509Fingerprint256", + "crypto.x509Fingerprint512", + "crypto.x509InfoAccess", + "crypto.x509Issuer", + "crypto.x509Raw", + "crypto.x509RawDer", + "crypto.x509RawStr", + "crypto.x509SerialNumber", + "crypto.x509SignatureAlgorithm", + "crypto.x509SignatureAlgorithmOid", + "crypto.x509Subject", + "crypto.x509SubjectAltName", + "crypto.x509CheckHost", + "crypto.x509CheckEmail", + "crypto.x509CheckIP", + "crypto.x509CheckIssued", + "crypto.x509LegacyObject", + "crypto.x509Pem", + // getPeerCertificate and getCertificate build with scr_x509.c too. + "tls.sockGetPeerCertificate", + "tls.sockGetCertificate", + ], + }), zlib: rule({ prefixes: ["zlib."] }), + /** node:zlib's Brotli forms: zlib.brotliInstall, which %main calls when + * the program uses Brotli (scr_brotli.c). */ + brotli: rule({ fns: ["zlib.brotliInstall"] }), }; type RuntimeFeatureName = keyof typeof RUNTIME_FEATURE_RULES; @@ -1059,6 +1236,7 @@ function noRuntimeFeatures(): RuntimeFeatures { dc: false, dgram: false, dgramDyn: false, + textCodecDyn: false, dnsPromises: false, dynAsync: false, dynBuiltinClass: false, @@ -1066,6 +1244,7 @@ function noRuntimeFeatures(): RuntimeFeatures { dynInvoke: false, dynTier: false, ecdh: false, + x509: false, dh: false, emitter: false, eventTarget: false, @@ -1077,6 +1256,8 @@ function noRuntimeFeatures(): RuntimeFeatures { http: false, inspect: false, legacyTextDecoder: false, + mime: false, + utilTiming: false, mockTimers: false, net: false, netLookup: false, @@ -1090,14 +1271,18 @@ function noRuntimeFeatures(): RuntimeFeatures { qs: false, regex: false, searchParams: false, + urlIdna: false, stdioExtended: false, + moduleNamespace: false, stream: false, streamDyn: false, handleProps: false, declAdopt: false, + typeOrigins: false, dynForAwait: false, dynBuffer: false, builtinValues: false, + computedRequire: false, symbol: false, tls: false, tlsCa: false, @@ -1106,6 +1291,7 @@ function noRuntimeFeatures(): RuntimeFeatures { wasm: false, webcrypto: false, zlib: false, + brotli: false, }; } @@ -1201,6 +1387,17 @@ function scanRuntimeFeatures( * switch that pulls scr_regex.c + the vendored libregexp into the binary * (native-toolchain.ts); regex-free programs keep the historical command line. The * typed traversal visits type slots as well as executable nodes. */ +/** True when the module enters AsyncLocalStorage stores (RuntimeFeatures + * asyncContext): a walk for those libCalls alone, without the other + * features' rules. */ +export function moduleUsesAsyncContext(mod: IrModule): boolean { + const visitor = { + expr: (e: IrExpr): boolean => e.kind !== "libCall" || !ASYNC_CONTEXT_LIB_FNS.has(e.fn), + stmt: (): boolean => true, + }; + return !mod.functions.every((fn) => everyStmtList(fn.body, visitor)); +} + export function moduleUsesRegex(mod: IrModule): boolean { return scanRuntimeFeatures(mod, "regex").regex; } diff --git a/packages/compiler/src/ir/scalar-records.test.ts b/packages/compiler/src/ir/scalar-records.test.ts index e3132088d..2392562a3 100644 --- a/packages/compiler/src/ir/scalar-records.test.ts +++ b/packages/compiler/src/ir/scalar-records.test.ts @@ -10,7 +10,7 @@ import { type IrStmt, type IrType, } from "./ir.js"; -import { scalarizeNumericRecords } from "./scalar-records.js"; +import { scalarizeLiteralResults, scalarizeNumericRecords } from "./scalar-records.js"; import { validateModule } from "./validate.js"; const loc = { file: "scalar-records.ts", start: 0, end: 0 }; @@ -284,3 +284,189 @@ test("bounds expansion in callers with many eligible calls", () => { expect(remaining).toBeGreaterThan(0); expect(remaining).toBeLessThan(300); }); + +const tuple: IrType = { kind: "record", shapeId: "t0" }; +const strRef = (localId: string): IrExpr => ref(localId, STRING); +const element = (localId: string, field: string, type: IrType): IrStmt => ({ + kind: "varDecl", + localId, + init: { kind: "recordGet", obj: ref("result", tuple), shapeId: "t0", field, type, loc }, + loc, +}); + +/** `split(text, at)` returning `[text.slice(0, at), text.slice(at), at > 0]`, + * and a `main` destructuring it. */ +function literalFixture(after: IrStmt[]): IrModule { + const slice = (args: IrExpr[]): IrExpr => ({ + kind: "strIntrinsic", + method: "slice", + receiver: strRef("text"), + args, + type: STRING, + loc, + }); + return { + irVersion: 17, + sourceFile: loc.file, + entry: "main", + records: [ + { + id: "t0", + fields: [ + { name: "0", type: STRING }, + { name: "1", type: STRING }, + { name: "2", type: BOOL }, + ], + tuple: true, + }, + ], + functions: [ + { + name: "split", + params: [ + { localId: "text", name: "text", type: STRING }, + { localId: "at", name: "at", type: F64 }, + ], + returnType: tuple, + locals: [ + { id: "text", name: "text", type: STRING, mutable: false }, + { id: "at", name: "at", type: F64, mutable: false }, + ], + body: [ + { + kind: "return", + value: { + kind: "recordLit", + type: tuple, + fields: [ + { name: "0", value: slice([num(0), ref("at")]) }, + { name: "1", value: slice([ref("at")]) }, + { + name: "2", + value: { kind: "bin", op: ">", left: ref("at"), right: num(0), type: BOOL, loc }, + }, + ], + loc, + }, + loc, + }, + ], + loc, + }, + { + name: "main", + params: [{ localId: "source", name: "source", type: STRING }], + returnType: VOID, + locals: [ + { id: "source", name: "source", type: STRING, mutable: false }, + { id: "result", name: "result", type: tuple, mutable: false }, + { id: "head", name: "head", type: STRING, mutable: false }, + { id: "tail", name: "tail", type: STRING, mutable: true }, + ], + body: [ + { + kind: "varDecl", + localId: "result", + init: call("split", [strRef("source"), call("effect", [num(1)])], tuple), + loc, + }, + ...after, + ], + loc, + }, + { + name: "effect", + params: [{ localId: "x", name: "x", type: F64 }], + returnType: F64, + locals: [{ id: "x", name: "x", type: F64, mutable: true }], + body: [{ kind: "return", value: ref("x"), loc }], + loc, + }, + ], + }; +} + +function literalTransformed(mod: IrModule): IrModule { + expect(validateModule(mod)).toEqual([]); + const out = scalarizeLiteralResults(mod); + expect(validateModule(out)).toEqual([]); + return out; +} + +test("declares destructured elements from a literal producer's fields in evaluation order", () => { + const mod = literalFixture([element("head", "0", STRING), element("tail", "1", STRING)]); + const snapshot = structuredClone(mod); + const out = literalTransformed(mod); + expect(mod).toEqual(snapshot); + const caller = main(out); + expect(nodes(caller.body, "recordGet")).toEqual([]); + expect(nodes(caller.body, "call").map((c) => c["callee"])).toEqual(["effect"]); + const declared = caller.body.map((s) => (s.kind === "varDecl" ? s.localId : s.kind)); + // Parameters, then every field in the literal's order: the unread third + // field still evaluates, into a binding of its own. + expect(declared.slice(2)).toEqual(["head", "tail", declared[4]]); + expect(caller.locals.find((l) => l.id === declared[4])?.type).toEqual(BOOL); + expect(caller.locals.some((l) => l.id === "result")).toBe(false); + // Slices stay slices of the renamed parameter. + expect(nodes(caller.body, "strIntrinsic").map((n) => (n["receiver"] as IrExpr).kind)).toEqual([ + "varRef", + "varRef", + ]); +}); + +test("keeps the call when the result is read other than by adjacent element declarations", () => { + const escaped = literalFixture([ + element("head", "0", STRING), + { kind: "exprStmt", expr: ref("result", tuple), loc }, + ]); + expect(literalTransformed(escaped)).toBe(escaped); + const apart = literalFixture([ + element("head", "0", STRING), + { kind: "exprStmt", expr: num(0), loc }, + element("tail", "1", STRING), + ]); + expect(literalTransformed(apart)).toBe(apart); +}); + +test("keeps the call when an argument holds a counted temporary of its own", () => { + const mod = literalFixture([element("head", "0", STRING)]); + const decl = main(mod).body[0] as IrStmt & { kind: "varDecl" }; + const length: IrExpr = { + kind: "strIntrinsic", + method: "length", + receiver: { kind: "strLit", value: "abc", type: STRING, loc }, + args: [], + type: F64, + loc, + }; + (decl.init as IrExpr & { kind: "call" }).args[1] = length; + expect(literalTransformed(mod)).toBe(mod); +}); + +test("renames a producer's parameter where its literal writes it", () => { + const mod = literalFixture([element("head", "0", STRING), element("tail", "1", STRING)]); + const split = mod.functions.find((fn) => fn.name === "split")!; + // Lowering declares every parameter mutable. + for (const l of split.locals) l.mutable = true; + const ret = split.body[0] as IrStmt & { kind: "return" }; + const literal = ret.value as IrExpr & { kind: "recordLit" }; + // `at++ > (at = 0)` + literal.fields[2]!.value = { + kind: "bin", + op: ">", + left: { kind: "incDec", op: "+", prefix: false, localId: "at", type: F64, loc }, + right: { kind: "assignExpr", localId: "at", value: num(0), type: F64, loc }, + type: BOOL, + loc, + }; + const caller = main(literalTransformed(mod)); + const [text, at] = caller.body + .slice(0, 2) + .map((s) => (s as IrStmt & { kind: "varDecl" }).localId); + expect(at).not.toBe("at"); + const writes = [...nodes(caller.body, "incDec"), ...nodes(caller.body, "assignExpr")]; + expect(writes.map((w) => w["localId"])).toEqual([at, at]); + // Only the written parameter's binding is mutable. + expect(caller.locals.find((l) => l.id === at)?.mutable).toBe(true); + expect(caller.locals.find((l) => l.id === text)?.mutable).toBe(false); +}); diff --git a/packages/compiler/src/ir/scalar-records.ts b/packages/compiler/src/ir/scalar-records.ts index 0a5b0c80f..088fd8461 100644 --- a/packages/compiler/src/ir/scalar-records.ts +++ b/packages/compiler/src/ir/scalar-records.ts @@ -10,6 +10,7 @@ import { } from "./ir.js"; import { everyExpr, + transformExpr, everyExprChild, everyStmtChild, everyStmtList, @@ -362,3 +363,338 @@ export function scalarizeNumericRecords(mod: IrModule): IrModule { }); return changed ? { ...mod, functions } : mod; } + +/** A producer whose whole body is `return `, with number, boolean + * or string fields, inlined where its result is destructured. */ +interface LiteralProducer { + fn: IrFunction; + shape: IrRecordShape; + literal: IrExpr & { kind: "recordLit" }; + size: number; + /** The parameters the literal writes (`a++`, `a = b`). */ + written: ReadonlySet; +} + +/** Whether `value` holds no counted temporary of its own: every counted + * node in it only views a binding, so evaluating it in its own statement + * shortens no lifetime. A message argument of a throw counts as nothing: it + * lives until the throw, which ends the evaluation anyway. */ +function viewsOnly(value: IrExpr): boolean { + if (!isRefCounted(value.type)) return everyExprChild(value, viewsOnly, () => false); + switch (value.kind) { + case "varRef": + return true; + case "unionNarrow": + return viewsOnly(value.value); + case "ternary": + return viewsOnly(value.cond) && viewsOnly(value.then) && viewsOnly(value.else_); + case "libCall": + return value.fn === "error.nodeThrow"; + default: + return false; + } +} + +/** The node's own value may be counted (it moves into the binding that + * takes it); everything under it must hold no temporary (viewsOnly), as + * scalarTemporaries requires for the numeric pass. */ +function ownedTemporaries(value: IrExpr): boolean { + return everyExprChild(value, viewsOnly, () => false); +} + +const LITERAL_FIELD_KINDS: ReadonlySet = new Set(["f64", "bool", "string"]); + +function literalProducer( + fn: IrFunction, + shapes: ReadonlyMap, +): LiteralProducer | null { + if ( + fn.async || + fn.generator || + fn.captures?.length || + fn.classCaptures?.length || + fn.returnType.kind !== "record" || + fn.body.length !== 1 + ) + return null; + const shape = shapes.get(fn.returnType.shapeId); + if ( + !shape || + shape.indexValue || + shape.fields.length === 0 || + shape.fields.length > MAX_FIELDS || + shape.fields.some((f) => !LITERAL_FIELD_KINDS.has(f.type.kind)) + ) + return null; + // Every local is a parameter: the body declares nothing. + const params = new Set(fn.params.map((p) => p.localId)); + if (fn.locals.some((l) => l.boxed || l.tdz || !params.has(l.id))) return null; + const ret = fn.body[0]!; + const literal = ret.kind === "return" ? ret.value : null; + if ( + literal?.kind !== "recordLit" || + literal.type.kind !== "record" || + literal.type.shapeId !== shape.id || + literal.fields.length !== shape.fields.length || + !literal.fields.every( + (f) => + !f.overflow && + shape.fields.some((sf) => sf.name === f.name && sf.type.kind === f.value.type.kind) && + ownedTemporaries(f.value), + ) + ) + return null; + let size = 0; + const written = new Set(); + const small = everyExpr(literal, { + expr: (node) => { + if (node.kind === "incDec" || node.kind === "assignExpr") written.add(node.localId); + return ( + ++size <= MAX_CALLEE_NODES && + node.kind !== "closure" && + node.kind !== "selfRef" && + node.kind !== "awaitExpr" && + node.kind !== "awaitUnionExpr" && + node.kind !== "yieldExpr" + ); + }, + stmt: () => false, + }); + return small ? { fn, shape, literal, size, written } : null; +} + +/** Rewrite every statement list under `body`, innermost first. */ +function mapStatementLists(body: IrStmt[], f: (list: IrStmt[]) => IrStmt[]): IrStmt[] { + const lists = (s: IrStmt): IrStmt => { + switch (s.kind) { + case "if": + return { + ...s, + then: mapStatementLists(s.then, f), + else_: s.else_ === null ? null : mapStatementLists(s.else_, f), + }; + case "block": + case "while": + case "doWhile": + case "for": + case "forOf": + return { ...s, body: mapStatementLists(s.body, f) }; + case "switch": + return { + ...s, + cases: s.cases.map((c) => ({ + ...c, + body: mapStatementLists(c.body, f), + })), + }; + case "tryCatch": + return { + ...s, + tryBody: mapStatementLists(s.tryBody, f), + catchBody: s.catchBody === null ? null : mapStatementLists(s.catchBody, f), + finallyBody: s.finallyBody === null ? null : mapStatementLists(s.finallyBody, f), + }; + default: + return s; + } + }; + return f(body.map(lists)); +} + +/** How often each local is read, anywhere in `fn`. */ +function localReads(fn: IrFunction): Map { + const reads = new Map(); + const count = (id: string): void => { + reads.set(id, (reads.get(id) ?? 0) + 1); + }; + everyStmtList(fn.body, { + expr: (node) => { + switch (node.kind) { + case "varRef": + case "incDec": + case "assignExpr": + count(node.localId); + break; + case "closure": + case "classRef": + for (const id of node.captures ?? []) count(id); + break; + } + return true; + }, + stmt: (node) => { + if (node.kind === "assign" || node.kind === "forOf" || node.kind === "rethrow") + count(node.localId); + return true; + }, + }); + return reads; +} + +/** Inline small literal producers where the result is only destructured. + * + * `const [head, tail] = split(text, at)`, where `split` is + * `return [text.slice(0, at), text.slice(at)]`, lowers to a result binding + * and one declaration per element read from it, right after the call. Each + * such declaration now takes its field's value directly: the parameters are + * declared from the arguments in order, then the fields in the literal's + * order (unread fields go to fresh bindings), then the destructured + * bindings, so evaluation order is unchanged and no record is built. A + * slice element read only through its length then needs no copy at all + * (findScalarStringSlices). The declarations stay in the caller's own + * statement list, so every binding keeps its scope; parameters live to the + * end of it, which only lengthens the arguments' lifetimes. Every other use + * of the result binding keeps the call. */ +export function scalarizeLiteralResults(mod: IrModule): IrModule { + const shapes = new Map((mod.records ?? []).map((s) => [s.id, s])); + const producers = new Map(); + for (const fn of mod.functions) { + const p = literalProducer(fn, shapes); + if (p) producers.set(fn.name, p); + } + if (producers.size === 0) return mod; + let changed = false; + const globalIds = new Set((mod.globals ?? []).map((g) => g.id)); + const functions = mod.functions.map((fn): IrFunction => { + if (fn.async || fn.generator) return fn; + const locals = new Map(fn.locals.map((l) => [l.id, l])); + // The names a fresh binding must avoid, collected when the first one is + // made: most functions inline no producer. + let used: Set | null = null; + let next = 0; + const fresh = (): string => { + if (used === null) { + used = new Set(locals.keys()); + collectLabels(fn.body, used); + } + let id: string; + do { + id = `%literal.${next++}`; + } while (used.has(id) || globalIds.has(id)); + used.add(id); + return id; + }; + let reads: Map | null = null; + const added: IrLocal[] = []; + const removed = new Set(); + let budget = MAX_INLINE_NODES; + const rewrite = (list: IrStmt[]): IrStmt[] => { + const out: IrStmt[] = []; + for (let i = 0; i < list.length; i++) { + const decl = list[i]!; + const call = decl.kind === "varDecl" ? decl.init : null; + const p = call?.kind === "call" ? producers.get(call.callee) : undefined; + const local = decl.kind === "varDecl" ? locals.get(decl.localId) : undefined; + if ( + decl.kind !== "varDecl" || + call?.kind !== "call" || + !p || + !local || + p.fn.name === fn.name || + p.size > budget || + local.mutable || + local.boxed || + local.tdz || + local.type.kind !== "record" || + local.type.shapeId !== p.shape.id || + call.args.length !== p.fn.params.length || + !call.args.every(ownedTemporaries) + ) { + out.push(decl); + continue; + } + // The element declarations right after the call, one per field. + const elements = new Map(); + let j = i + 1; + for (; j < list.length; j++) { + const element = list[j]!; + const get = element.kind === "varDecl" ? element.init : null; + const target = element.kind === "varDecl" ? locals.get(element.localId) : undefined; + if ( + element.kind !== "varDecl" || + get?.kind !== "recordGet" || + get.obj.kind !== "varRef" || + get.obj.localId !== local.id || + get.shapeId !== p.shape.id || + elements.has(get.field) || + !target || + target.boxed || + target.tdz || + target.type.kind !== get.type.kind + ) + break; + elements.set(get.field, element); + } + reads ??= localReads(fn); + if (elements.size === 0 || reads.get(local.id) !== elements.size) { + out.push(decl); + continue; + } + budget -= p.size; + changed = true; + removed.add(local.id); + const renamed = new Map(p.fn.params.map((param) => [param.localId, fresh()])); + for (const l of p.fn.locals) { + // Copied field by field: the native compiler does not spread a + // record whose type has a union. + const copy: IrLocal = { + id: renamed.get(l.id)!, + name: l.name, + type: l.type, + mutable: p.written.has(l.id), + }; + if (l.source !== undefined) copy.source = l.source; + added.push(copy); + } + p.fn.params.forEach((param, index) => { + out.push({ + kind: "varDecl", + localId: renamed.get(param.localId)!, + init: call.args[index]!, + loc: decl.loc, + }); + }); + // Reads and writes of a parameter (`a++`, `a = b`) both name it. + const rename = (value: IrExpr): IrExpr => + transformExpr(value, { + expr: (node) => + (node.kind === "varRef" || node.kind === "incDec" || node.kind === "assignExpr") && + renamed.has(node.localId) + ? { ...node, localId: renamed.get(node.localId)! } + : node, + stmt: (node) => node, + }); + for (const field of p.literal.fields) { + const element = elements.get(field.name); + let id = element?.localId; + if (id === undefined) { + id = fresh(); + const type = p.shape.fields.find((f) => f.name === field.name)!.type; + added.push({ + id, + name: `${local.name}.${field.name}`, + type, + mutable: false, + }); + } + out.push({ + kind: "varDecl", + localId: id, + init: rename(field.value), + loc: element?.loc ?? decl.loc, + }); + } + i = j - 1; + } + return out; + }; + const body = mapStatementLists(fn.body, rewrite); + if (!changed || removed.size === 0) return fn; + return { + ...fn, + locals: [...fn.locals.filter((l) => !removed.has(l.id)), ...added], + body, + }; + }); + return changed ? { ...mod, functions } : mod; +} diff --git a/packages/compiler/src/ir/self-captures.ts b/packages/compiler/src/ir/self-captures.ts index e009e9a11..70242cb8b 100644 --- a/packages/compiler/src/ir/self-captures.ts +++ b/packages/compiler/src/ir/self-captures.ts @@ -107,7 +107,8 @@ export function resolveSelfCaptures(mod: IrModule): IrModule { if (creator.captures?.some((c) => c.localId === source)) return false; if (creator.classCaptures?.some((c) => c.localId === source)) return false; if (declarations.get(owner)!.get(source) !== 1) return false; - flow ??= new CaptureFlow(mod.functions); + // Only unchanged() is asked: the flow reads just the bodies it needs. + flow ??= new CaptureFlow(mod.functions, false); return flow.unchanged(owner, source, definition.writes); }); if (qualifies) { @@ -118,7 +119,12 @@ export function resolveSelfCaptures(mod: IrModule): IrModule { } if (resolved.size === 0) return mod; + // Only the resolved bodies and the functions that create their closures + // change. + const touched = new Set(resolved.keys()); + for (const name of resolved.keys()) for (const site of sites.get(name)!) touched.add(site.owner); const rewritten = mod.functions.map((fn) => { + if (!touched.has(fn.name)) return fn; const own = resolved.get(fn.name); const self = own === undefined ? null : fn.captures![own]!; let changed = false; diff --git a/packages/compiler/src/ir/traverse.ts b/packages/compiler/src/ir/traverse.ts index 251904351..e83bf5c42 100644 --- a/packages/compiler/src/ir/traverse.ts +++ b/packages/compiler/src/ir/traverse.ts @@ -140,6 +140,7 @@ export function everyModuleNode(mod: IrModule, visitor: IrModuleVisitor): boolea for (const cls of mod.classes ?? []) { for (const capture of cls.localCaptures ?? []) if (!type(capture.type, cls.loc)) return false; for (const field of cls.fields) if (!type(field.type, cls.loc)) return false; + if (cls.arrayElem !== undefined && !type(cls.arrayElem, cls.loc)) return false; } for (const global of mod.globals ?? []) if (!type(global.type, global.source?.loc ?? entryLoc)) return false; @@ -473,9 +474,10 @@ export function everyStmtChild( throw new Error("unhandled IR node"); } -/** Called with each record literal mapExprChildren rebuilds and its - * replacement, so a side table keyed by node identity (the lowering's - * key-order witness notes) follows the literal into the rewritten tree. */ +/** Called with each record literal and checked conversion mapExprChildren + * rebuilds and its replacement, so a side table keyed by node identity (the + * lowering's key-order witness notes) follows the node into the rewritten + * tree. */ let recordLiteralRebuilt: ((from: IrExpr, to: IrExpr) => void) | null = null; /** Installs (or with null removes) the record-literal rebuild observer. */ @@ -504,8 +506,11 @@ export function mapExprChildren( return node; case "libCall": return { ...node, args: mapOf(node.args, expr) }; - case "dynCheck": - return { ...node, value: expr(node.value) }; + case "dynCheck": { + const rebuilt: IrExpr = { ...node, value: expr(node.value) }; + if (recordLiteralRebuilt !== null) recordLiteralRebuilt(node, rebuilt); + return rebuilt; + } case "strEq": return { ...node, left: expr(node.left), right: expr(node.right) }; case "fieldGet": diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index a404c918a..a1ca8f9c7 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -57,11 +57,16 @@ import { maybePromiseUnionArmsOk, VOID, } from "./ir.js"; -import { DYN_CLASS_PROPERTIES, callSiteReturnType, classValueConstructor } from "./ir.js"; +import { + DYN_CLASS_PROPERTIES, + callSiteReturnType, + classValueAbiEquals, + classValueConstructor, +} from "./ir.js"; import { unionWideningTags } from "./analysis.js"; import { alwaysReturns } from "./control-flow.js"; import { dynamicOperationOf, functionTier } from "./tiers.js"; -import { LIB_FN_SIGS, pipelinePumpedRuns } from "./builtin-signatures.js"; +import { libFnSignatures, pipelinePumpedRuns } from "./builtin-signatures.js"; import { ALWAYS_THROW_LIB_FNS } from "./builtin-effects.js"; /** The promisified crypto calls' value kinds besides checked-dynamic. */ @@ -122,6 +127,14 @@ export function validateModule(mod: IrModule): IrValidationError[] { errors.push({ message: "worker execution mode must be a boolean", loc: moduleLoc }); if (mod.declBoundary !== undefined && mod.declBoundary !== true) errors.push({ message: "declBoundary must be true when present", loc: moduleLoc }); + const declOrigins = mod.declOrigins; + if ( + declOrigins !== undefined && + (!Array.isArray(declOrigins) || declOrigins.some((o) => typeof o !== "string")) + ) + errors.push({ message: "declOrigins must be an array of strings", loc: moduleLoc }); + if (mod.untypedIntegrity !== undefined && mod.untypedIntegrity !== true) + errors.push({ message: "untypedIntegrity must be true when present", loc: moduleLoc }); if (mod.lifetimeRegions !== undefined && mod.lifetimeRegions !== true) errors.push({ message: "lifetimeRegions must be true when present", loc: moduleLoc }); if (mod.workers && mod.lib !== undefined) @@ -501,10 +514,29 @@ export function validateModule(mod: IrModule): IrValidationError[] { }); } } - for (const symbol of cls.symbolFields ?? []) { - const global = mod.globals?.find((global) => global.id === symbol.globalId); + if (cls.stringTagHelper !== undefined) { + const helper = functionsByName.get(cls.stringTagHelper); + const receiver = helper?.params[0]?.type; if ( - global?.type.kind !== "symbol" || + !helper || + helper.params.length !== 1 || + receiver?.kind !== "object" || + receiver.className !== cls.name || + helper.returnType.kind !== "string" || + helper.captures !== undefined || + helper.classCaptures !== undefined + ) { + errors.push({ + message: `class ${cls.name}: string tag helper must be a noncapturing (instance) => string function`, + loc: cls.loc, + }); + } + } + for (const symbol of cls.symbolFields ?? []) { + const global = + "globalId" in symbol ? mod.globals?.find((global) => global.id === symbol.globalId) : null; + if ( + ("globalId" in symbol ? global?.type.kind !== "symbol" : symbol.wellKnown === "") || !cls.fields.some((field) => field.name === symbol.field) ) { errors.push({ message: `class ${cls.name}: invalid symbol field metadata`, loc: cls.loc }); @@ -803,7 +835,6 @@ export function validateModule(mod: IrModule): IrValidationError[] { arm.kind === "void" || arm.kind === "union" || arm.kind === "dyn" || - arm.kind === "date" || (arm.kind === "generator" && iterableArmsElem(u.arms) === null) ) { errors.push({ message: `union ${u.id}: arm ${i} is ${arm.kind}`, loc: noLoc }); @@ -976,6 +1007,7 @@ export function validateModule(mod: IrModule): IrValidationError[] { mod.workers === true, mod.lifetimeRegions === true, new Set(mod.threadSlotKeys ?? []), + mod.declOrigins?.length ?? 0, ); } return errors; @@ -995,6 +1027,7 @@ function validateFunction( workers: boolean, lifetimeRegions: boolean, threadSlotKeys: ReadonlySet, + declOriginCount: number, ): void { const locals = new Map(fn.locals.map((l) => [l.id, l])); const err = (message: string, loc: SrcLoc) => @@ -1507,6 +1540,7 @@ function validateFunction( e.left.type.kind === "map" || e.left.type.kind === "set" || e.left.type.kind === "regex" || + e.left.type.kind === "date" || e.left.type.kind === "url" || e.left.type.kind === "object" || e.left.type.kind === "record" || @@ -2046,12 +2080,18 @@ function validateFunction( } case "arrayNewLen": { // Mapper-less Array.from({length: n}): absent-slot fill exists - // only for refcounted element kinds (frontend fences scalars). + // only for refcounted element kinds. Number and boolean slots are + // filled with present undefined values right after + // (`(T | undefined)[]`); the frontend fences other scalar uses. if (e.type.kind !== "array") { err(`arrayNewLen must be array-typed, got ${e.type.kind}`, e.loc); break; } - if (!isRefCounted(e.type.elem)) { + if ( + !isRefCounted(e.type.elem) && + e.type.elem.kind !== "f64" && + e.type.elem.kind !== "bool" + ) { err( `arrayNewLen with non-refcounted ${e.type.elem.kind} elements (no absent value)`, e.loc, @@ -2192,6 +2232,9 @@ function validateFunction( indexOfNum: { argTypes: [F64, F64], minArgs: 1, result: F64 }, lastIndexOfNum: { argTypes: [F64, F64], minArgs: 1, result: F64 }, includesNum: { argTypes: [F64, F64], minArgs: 1, result: BOOL }, + indexOfElem: { argTypes: [F64, F64], minArgs: 2, result: F64 }, + lastIndexOfElem: { argTypes: [F64, F64], minArgs: 2, result: F64 }, + includesElem: { argTypes: [F64, F64], minArgs: 2, result: BOOL }, fill: { argTypes: [BYTES_U8, F64, F64], minArgs: 1, result: BYTES_U8 }, fillNum: { argTypes: [F64, F64, F64], minArgs: 1, result: BYTES_U8 }, // Per-element TypedArray fill (any elem — the non-u8 fill path). @@ -2372,9 +2415,13 @@ function validateFunction( (e.method === "sortPrimitive" || e.method === "toSortedPrimitive") && elem.kind !== "string" && elem.kind !== "f64" && - elem.kind !== "bool" + elem.kind !== "bool" && + elem.kind !== "date" ) { - err(`arrIntrinsic ${e.method} requires primitive elements, got ${elem.kind}`, e.loc); + err( + `arrIntrinsic ${e.method} requires primitive or Date elements, got ${elem.kind}`, + e.loc, + ); } if (e.method === "sortValues" || e.method === "sortDense") { const arms = elem.kind === "union" ? unions.get(elem.unionId)?.arms : undefined; @@ -2456,13 +2503,18 @@ function validateFunction( elem.kind !== "f64" && elem.kind !== "string" && elem.kind !== "bool" && + elem.kind !== "date" && !( elem.kind === "union" && (unions .get(elem.unionId) ?.arms.every( (a) => - a.kind === "f64" || a.kind === "string" || a.kind === "bool" || isUnitType(a), + a.kind === "f64" || + a.kind === "string" || + a.kind === "bool" || + a.kind === "date" || + isUnitType(a), ) ?? false) ) @@ -2962,24 +3014,30 @@ function validateFunction( } const subClass = classes.get(sub); const supClass = classes.get(sup); - const subValueCtor = subClass ? classValueConstructor(subClass) : null; - const supValueCtor = supClass ? classValueConstructor(supClass) : null; - const subCtor = subValueCtor ? functions.get(subValueCtor.fn) : undefined; - const supCtor = supValueCtor ? functions.get(supValueCtor.fn) : undefined; - if (!subCtor || !supCtor || !subValueCtor || !supValueCtor) { + const subCtor = subClass ? functions.get(classValueConstructor(subClass).fn) : undefined; + const supCtor = supClass ? functions.get(classValueConstructor(supClass).fn) : undefined; + if (!subClass || !supClass || !subCtor || !supCtor) { err(`classval upcast: "${sub}"/"${sup}" lack constructor functions`, e.loc); break; } - const subArgs = subCtor.params.slice(subValueCtor.firstArg); - const supArgs = supCtor.params.slice(supValueCtor.firstArg); - const abiEqual = - subArgs.length === supArgs.length && - subArgs.every((p, i) => typeEquals(p.type, supArgs[i]!.type)); + const abiEqual = classValueAbiEquals(subClass, supClass, functions); if (!abiEqual) { err(`classval upcast: "${sub}" and "${sup}" constructor ABIs differ`, e.loc); } break; } + // An instance of a class declared `extends Array` and its array + // are the same object: upcast to the array, downcast back. + { + const instance = e.kind === "upcast" ? e.value.type : e.type; + const array = e.kind === "upcast" ? e.type : e.value.type; + if (instance.kind === "object" && array.kind === "array") { + const elem = classes.get(instance.className)?.arrayElem; + if (elem === undefined || !typeEquals(elem, array.elem)) + err(`${e.kind}: "${instance.className}" is not an array of that element type`, e.loc); + break; + } + } if (e.type.kind !== "object" || e.value.type.kind !== "object") { err(`${e.kind} between non-class types`, e.loc); break; @@ -3087,6 +3145,15 @@ function validateFunction( case "instanceOf": { checkExpr(e.value); if (e.type.kind !== "bool") err("instanceOf must be bool", e.loc); + // An array against a class declared `extends Array` (its ext). + if (e.value.type.kind === "array") { + if (classes.get(e.className)?.arrayElem === undefined) + err( + `instanceOf of an array against "${e.className}", which does not extend Array`, + e.loc, + ); + break; + } if (!hierarchy.has(e.className)) { err(`instanceOf against non-hierarchy class "${e.className}"`, e.loc); } @@ -4072,7 +4139,7 @@ function validateFunction( } if (e.fn === "worker.new" && !workers) errors.push({ message: "Worker construction requires worker execution mode", loc: e.loc }); - const sig = LIB_FN_SIGS[e.fn]; + const sig = libFnSignatures()[e.fn]; if (!sig) { err(`libCall of unknown library function "${e.fn as string}"`, e.loc); return; @@ -4177,6 +4244,41 @@ function validateFunction( } function checkLibValueCall(e: IrExpr & { kind: "libCall" }): boolean { + // ArraySpeciesCreate over an Array method's result, and Array.of / + // Array.from on a class that extends Array: the result has the plain + // result's array type. + if (e.fn === "array.dynIsClass") { + const cls = e.args[1]?.kind === "strLit" ? classes.get(e.args[1].value) : undefined; + if (cls?.arrayElem === undefined) + err("libCall array.dynIsClass must name a class that extends Array", e.loc); + return false; + } + if (e.fn === "array.speciesCreate") { + if (e.args[0]?.type.kind !== "array" || !typeEquals(e.type, e.args[0].type)) + err("libCall array.speciesCreate must have its receiver's array type", e.loc); + return true; + } + if (e.fn === "array.speciesFinish") { + const plain = e.args[1]; + if (plain?.type.kind !== "array" || !typeEquals(e.type, plain.type)) + err("libCall array.speciesFinish must have its array argument's type", e.loc); + if (e.args[0]?.type.kind !== "array") + err("libCall array.speciesFinish instance must be an array", e.loc); + return true; + } + if (e.fn === "array.speciesAdopt" || e.fn === "array.classConstructAdopt") { + const plain = e.args[1]; + if (plain?.type.kind !== "array" || !typeEquals(e.type, plain.type)) + err(`libCall ${e.fn} must have its array argument's type`, e.loc); + if (e.fn === "array.speciesAdopt" && e.args[0]?.type.kind !== "array") + err("libCall array.speciesAdopt receiver must be an array", e.loc); + if (e.fn === "array.classConstructAdopt") { + const cls = e.args[0]?.kind === "strLit" ? classes.get(e.args[0].value) : undefined; + if (cls?.arrayElem === undefined) + err("libCall array.classConstructAdopt must name a class that extends Array", e.loc); + } + return true; + } const cryptoPromise = CRYPTO_PROMISE_VALUES[e.fn]; if (cryptoPromise) { const inner = e.type.kind === "promise" ? e.type.inner : undefined; @@ -4275,7 +4377,9 @@ function validateFunction( if ( e.fn === "array.namedKeys" || e.fn === "array.hasNamedKey" || - e.fn === "array.hasProperties" + e.fn === "array.hasProperties" || + e.fn === "array.foreign" || + e.fn === "array.foreignThrow" ) { if (e.args[0]?.type.kind !== "array") { err(`libCall ${e.fn} arg 0: expected an array, got ${e.args[0]?.type.kind}`, e.loc); @@ -5339,6 +5443,39 @@ function validateFunction( } function checkLibSpecialCall(e: IrExpr & { kind: "libCall" }): boolean { + if (e.fn === "promise.fulfilledValue") { + const promise = e.args[0]?.type; + if ( + promise?.kind !== "promise" || + e.type.kind === "void" || + isUnitType(e.type) || + promise.inner.kind !== "void" + ) + err(`libCall ${e.fn} must read a Promise entry at a non-void type`, e.loc); + return true; + } + if (e.fn === "promise.allTuple") { + const [entries, build] = e.args; + if ( + entries?.type.kind !== "array" || + entries.type.elem.kind !== "promise" || + entries.type.elem.inner.kind !== "void" || + build?.kind !== "closure" || + build.captures.length > 0 || + build.type.kind !== "func" || + e.type.kind !== "promise" || + !typeEquals(build.type.ret, e.type.inner) + ) + err(`libCall ${e.fn} must join Promise entries into its builder's tuple`, e.loc); + return true; + } + if (e.fn === "als.bindCallback") { + // The bound function, at its closure argument's own type. + const fn = e.args[0]?.type; + if (fn?.kind !== "func" || !typeEquals(e.type, fn)) + err(`libCall ${e.fn} must return its function argument's type`, e.loc); + return true; + } if ( e.fn === "crypto.randomFillView" || e.fn === "crypto.randomFillViewRest" || @@ -5688,7 +5825,7 @@ function validateFunction( } const lastIsFunction = Math.floor(positions! / 2 ** (total - 1)) % 2 === 1; const helperCount = 3 * pipelinePumpedRuns(total, positions!, flags!); - const groups = helperCount + (destKind === 2 ? 1 : 0) + (flags! & 32 ? 2 : 0); + const groups = helperCount + (destKind >= 2 ? 1 : 0) + (flags! & 32 ? 2 : 0); const cbArgs = e.fn === "sp.pipelineStagesOpts" ? 1 + (flags! & 64 ? 1 : 0) @@ -5703,7 +5840,7 @@ function validateFunction( total > 52 || positions! >= 2 ** total || functions !== k || - destKind === 3 || + (destKind === 3 && e.fn === "stream.composeStages") || (destKind !== 0) !== lastIsFunction || total < 2 || e.args.length !== 5 + n! + k! + groups + cbArgs @@ -5722,7 +5859,7 @@ function validateFunction( err(`libCall ${e.fn} stage function ${i} must be a func`, e.loc); } at2 += k!; - const helpers = helperCount + (destKind === 2 ? 1 : 0); + const helpers = helperCount + (destKind >= 2 ? 1 : 0); for (let i = at2; i < at2 + helpers; i++) { if (e.args[i]?.type.kind !== "func") err(`libCall ${e.fn} helper ${i} must be a func`, e.loc); @@ -6081,6 +6218,11 @@ function validateFunction( case "dynCheck": { checkExpr(e.value); expectType(e.value, DYN, "dynCheck operand"); + if ( + e.origin !== undefined && + (!Number.isInteger(e.origin) || e.origin < 0 || e.origin >= declOriginCount) + ) + err(`dynCheck origin ${e.origin} is not an index of declOrigins`, e.loc); // The target drives the emitted validator/builder: non-dyn, // non-void, JSON-representable (closures/class instances can never // be found inside a JSON dyn — the frontend rejects those casts). @@ -6285,15 +6427,10 @@ function validateFunction( } } else { // throw: any exception-representable payload (the throw - // statement's rule). Date cannot retain its object kind in the - // untyped exception cell. + // statement's rule). if (e.arg === null) { err("genResume throw with no payload", e.loc); - } else if ( - e.arg.type.kind === "void" || - e.arg.type.kind === "caught" || - e.arg.type.kind === "date" - ) { + } else if (e.arg.type.kind === "void" || e.arg.type.kind === "caught") { err(`genResume throw of a ${e.arg.type.kind} value`, e.loc); } } @@ -6734,11 +6871,7 @@ function validateFunction( checkExpr(s.value); // dyn throws are allowed: the dyn node rides the REF cell arm by // reference (the JS-lane `throw err` of a dyn argument). - if ( - s.value.type.kind === "void" || - s.value.type.kind === "caught" || - s.value.type.kind === "date" - ) { + if (s.value.type.kind === "void" || s.value.type.kind === "caught") { err(`throw of a ${s.value.type.kind} value`, s.loc); } break; diff --git a/packages/compiler/src/library/fence-eval.ts b/packages/compiler/src/library/fence-eval.ts index 8b0450857..a950b3990 100644 --- a/packages/compiler/src/library/fence-eval.ts +++ b/packages/compiler/src/library/fence-eval.ts @@ -56,9 +56,9 @@ import { } from "../coverage/surface-manifest.js"; import { libFenceDiag, type ScrDiagnostic } from "../diagnostics/diagnostic.js"; import { - AMBIENT_SURFACE_FNS, + ambientSurfaceFns, BUILTIN_MODULE_CONSTS, - BUILTIN_MODULE_FNS, + builtinModuleFns, BUILTIN_MODULE_FN_ALIASES, STATIC_MATH_FNS, STATIC_MATH_PROPS, @@ -125,7 +125,7 @@ interface FenceTaxonomy { /** node-builtin constant ids: reads fold to literals at compile time. */ foldedIds: Set; /** Ambient dedicated-path surfaces (Date/perf_hooks/process): manifest - * id → the libCall spellings witnessing reach (AMBIENT_SURFACE_FNS — + * id → the libCall spellings witnessing reach (ambientSurfaceFns() — * the same rows the manifest projects the entries from). */ ambientFns: Map; } @@ -148,7 +148,7 @@ function fenceTaxonomy(): FenceTaxonomy { const builtinFns = new Map(); const builtinRoots = new Map(); const foldedIds = new Set(); - for (const [mod, members] of Object.entries(BUILTIN_MODULE_FNS)) { + for (const [mod, members] of Object.entries(builtinModuleFns())) { const root = new Set(); for (const [member, entry] of Object.entries(members!)) { if (entry === undefined) continue; @@ -156,7 +156,7 @@ function fenceTaxonomy(): FenceTaxonomy { // The bare-path and url tables rebind per TARGET platform // (builtinModuleFnsOf): a fence on the bare id covers both flavors. if (mod === "path") { - const w = BUILTIN_MODULE_FNS["path/win32"]?.[member]; + const w = builtinModuleFns()["path/win32"]?.[member]; if (w !== undefined) fns.add(w.fn); } if (mod === "url" && member === "pathToFileURL") fns.add("url.pathToFileURLWin32"); @@ -173,7 +173,7 @@ function fenceTaxonomy(): FenceTaxonomy { } for (const member of Object.keys(STATIC_MATH_PROPS)) foldedIds.add(`stdlib.math.${member}`); const ambientFns = new Map( - AMBIENT_SURFACE_FNS.map((row) => [row.id, row.fns as readonly string[]]), + ambientSurfaceFns().map((row) => [row.id, row.fns as readonly string[]]), ); taxonomy = { byId, diff --git a/packages/compiler/src/library/ir-policy.ts b/packages/compiler/src/library/ir-policy.ts index 6d5de2cf1..2337c41ad 100644 --- a/packages/compiler/src/library/ir-policy.ts +++ b/packages/compiler/src/library/ir-policy.ts @@ -31,6 +31,7 @@ const LIB_MODE_REFUSED_PREFIXES: readonly [string, string][] = [ ["zlib.cbOpts", "the async node:zlib callback surface"], ["zlib.promise", "the async node:zlib callback surface (util.promisify)"], ["zlib.stream", "the node:zlib stream classes"], + ["zlib.brotliInstall", "node:zlib's Brotli forms"], ["process.stdoutWriteBytesCb", "process.stdout.write completion callbacks"], ["process.stderrWriteBytesCb", "process.stderr.write completion callbacks"], ["timers.", "the timers surface (setTimeout family)"], @@ -63,6 +64,10 @@ const LIB_MODE_REFUSED_PREFIXES: readonly [string, string][] = [ ["crypto.generateKeyPair", "asymmetric KeyObjects, generateKeyPair, sign and verify"], ["crypto.signOneshot", "asymmetric KeyObjects, generateKeyPair, sign and verify"], ["crypto.verifyOneshot", "asymmetric KeyObjects, generateKeyPair, sign and verify"], + ["crypto.x509PublicKey", "X509Certificate publicKey, verify and checkPrivateKey"], + ["crypto.x509Verify", "X509Certificate publicKey, verify and checkPrivateKey"], + ["crypto.x509CheckPrivateKey", "X509Certificate publicKey, verify and checkPrivateKey"], + ["crypto.diffieHellman", "crypto.diffieHellman"], ["webcrypto.", "WebCrypto (crypto.subtle)"], ["fs.watch", "fs.watch"], ["watcher.", "fs.watch"], diff --git a/packages/compiler/src/library/library-cache.test.ts b/packages/compiler/src/library/library-cache.test.ts index 846d90272..c19275733 100644 --- a/packages/compiler/src/library/library-cache.test.ts +++ b/packages/compiler/src/library/library-cache.test.ts @@ -85,6 +85,7 @@ test("early library cache restores generated artifacts and metadata", async () = searchParams: false, emitter: false, zlib: false, + x509: false, copying: false, textDecoderLegacy: false, dynInvoke: false, @@ -138,6 +139,7 @@ test("early library cache publishes after creating a fresh output directory", as searchParams: false, emitter: false, zlib: false, + x509: false, copying: false, textDecoderLegacy: false, dynInvoke: false, @@ -168,6 +170,7 @@ test("early library cache hits refresh every payload's LRU time", async () => { searchParams: false, emitter: false, zlib: false, + x509: false, copying: false, textDecoderLegacy: false, dynInvoke: false, @@ -213,6 +216,7 @@ test("early library cache misses on source edits and newly-resolved candidates", searchParams: false, emitter: false, zlib: false, + x509: false, copying: false, textDecoderLegacy: false, dynInvoke: false, @@ -242,6 +246,7 @@ test("early library cache misses on source edits and newly-resolved candidates", searchParams: false, emitter: false, zlib: false, + x509: false, copying: false, textDecoderLegacy: false, dynInvoke: false, @@ -291,6 +296,7 @@ test("semantic library cache restores and rebases IR after a comment-only edit", searchParams: false, emitter: false, zlib: false, + x509: false, copying: false, textDecoderLegacy: false, dynInvoke: false, @@ -354,6 +360,7 @@ test("semantic library cache refuses token and directive edits", async () => { searchParams: false, emitter: false, zlib: false, + x509: false, copying: false, textDecoderLegacy: false, dynInvoke: false, @@ -402,6 +409,7 @@ test("semantic LLVM cache accepts comment edits that shift source lines", async searchParams: false, emitter: false, zlib: false, + x509: false, copying: false, textDecoderLegacy: false, dynInvoke: false, @@ -440,6 +448,7 @@ test("semantic LLVM cache accepts line separator normalization", async () => { searchParams: false, emitter: false, zlib: false, + x509: false, copying: false, textDecoderLegacy: false, dynInvoke: false, @@ -501,6 +510,7 @@ test("semantic LLVM cache rebases comment edits in multi-source graphs", async ( searchParams: false, emitter: false, zlib: false, + x509: false, copying: false, textDecoderLegacy: false, dynInvoke: false, @@ -540,6 +550,7 @@ test("early library cache is separated by the host Node version", async () => { searchParams: false, emitter: false, zlib: false, + x509: false, copying: false, textDecoderLegacy: false, dynInvoke: false, @@ -571,6 +582,7 @@ test("early library cache rejects corrupted artifacts and metadata", async () => searchParams: false, emitter: false, zlib: false, + x509: false, copying: false, textDecoderLegacy: false, dynInvoke: false, @@ -605,6 +617,7 @@ test("disabled early library cache performs no reads or writes", async () => { searchParams: false, emitter: false, zlib: false, + x509: false, copying: false, textDecoderLegacy: false, dynInvoke: false, diff --git a/packages/compiler/src/library/library-cache.ts b/packages/compiler/src/library/library-cache.ts index 3e77ccced..7e61e95b8 100644 --- a/packages/compiler/src/library/library-cache.ts +++ b/packages/compiler/src/library/library-cache.ts @@ -54,6 +54,7 @@ interface EarlyLibraryCacheStamp { searchParams: boolean; emitter: boolean; zlib: boolean; + x509: boolean; copying: boolean; textDecoderLegacy: boolean; }; @@ -74,6 +75,7 @@ export interface EarlyLibraryNativeFeatures { searchParams: boolean; emitter: boolean; zlib: boolean; + x509: boolean; copying: boolean; textDecoderLegacy: boolean; /** Volatile exact-source build identity emitted from the tiny identity TU. */ @@ -132,6 +134,7 @@ const BOOLEAN_NATIVE_KEYS = [ "searchParams", "emitter", "zlib", + "x509", "copying", "textDecoderLegacy", ] as const satisfies readonly (keyof EarlyLibraryNativeFeatures)[]; diff --git a/packages/compiler/src/native/cache.ts b/packages/compiler/src/native/cache.ts index 3c9bdf411..0250e9618 100644 --- a/packages/compiler/src/native/cache.ts +++ b/packages/compiler/src/native/cache.ts @@ -1,6 +1,7 @@ import { createHash } from "node:crypto"; import { closeSync, + existsSync, mkdirSync, mkdtempSync, openSync, @@ -88,14 +89,19 @@ export class NativeCache { } } - write(family: string, key: string, bytes: string | Uint8Array): void { + write( + family: string, + key: string, + bytes: string | Uint8Array, + digest = contentDigest(bytes), + ): void { let stage: string | null = null; try { const directory = join(this.root, family); mkdirSync(directory, { recursive: true, mode: 0o700 }); stage = mkdtempSync(join(directory, ".write-")); writeFileSync(join(stage, "payload"), bytes); - writeFileSync(join(stage, "digest"), contentDigest(bytes) + "\n"); + writeFileSync(join(stage, "digest"), digest + "\n"); const path = join(directory, key); // Concurrent writers publish identical content under the same key; // a reader between the two renames simply treats it as a miss. @@ -112,6 +118,18 @@ export class NativeCache { } } + /** Whether an entry exists, without reading or checking it. */ + has(family: string, key: string): boolean { + return existsSync(join(this.root, family, key)); + } + + /** Writes `bytes` under their content digest, and returns the digest. */ + writeContent(family: string, bytes: Uint8Array): string { + const digest = contentDigest(bytes); + this.write(family, digest, bytes, digest); + return digest; + } + /** Publish text parts without joining them into one string. */ writeParts(family: string, key: string, parts: Generator): void { let stage: string | null = null; @@ -149,6 +167,9 @@ export class NativeCache { "object", "sanitizer", "executable", + "kept", + "linker-job", + "runtime-pack", "binary", "dsym", "coverage", diff --git a/packages/compiler/src/native/cli.ts b/packages/compiler/src/native/cli.ts index 21047b929..4209ecf0e 100644 --- a/packages/compiler/src/native/cli.ts +++ b/packages/compiler/src/native/cli.ts @@ -1,5 +1,5 @@ import { spawnSync } from "node:child_process"; -import { mkdtempSync, rmSync } from "node:fs"; +import { appendFileSync, mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { runCli } from "../cli/command.js"; @@ -12,11 +12,20 @@ import { mergeNativeProfiles } from "../backend/native-tools.js"; import { setDeclarationRoot } from "../frontend/dts-paths.js"; import { setFrontendDisposedHook } from "../frontend/dispose-hook.js"; import { FrontendServices } from "../frontend/services.js"; +import { runTypePatchCommand } from "../frontend/type-patches/command.js"; import { createNativeTs7Api } from "../frontend/ts7/native-api.js"; import { resolveProvenanceSourcesWithParser } from "../frontend/provenance-core.js"; import { setCompilerReleaseVersion } from "../library/sidecar.js"; import { NativeCompiler } from "./compiler.js"; -import { openNativeCache } from "./cache.js"; +import { NATIVE_STRING_BYTES, openNativeCache } from "./cache.js"; +import { + checkerExecutableIdentity, + checkerPackageVersion, + distributionPrebuiltLibDiagnostics, + recordPrebuiltLibDiagnostics, + setLibDiagnosticsStore, +} from "../frontend/ts7/lib-diagnostics.js"; +import { setCheckerFeatureStore } from "../frontend/ts7/checker-features.js"; import { loadNativeToolchain } from "./toolchain.js"; import { buildSanitizedRuntime } from "./sanitizer.js"; import { runCompilerTask } from "./task.js"; @@ -53,6 +62,38 @@ async function main(): Promise { if (toolchain.declarationsRoot !== undefined) setDeclarationRoot(toolchain.declarationsRoot); setCompilerReleaseVersion(toolchain.compilerVersion); cache = openNativeCache((path, harden) => compilerNativePrivateDirectory(path, harden)); + if (cache !== null) { + const store = cache; + setLibDiagnosticsStore( + { + read: (key) => + store.read("lib-diagnostics", key, NATIVE_STRING_BYTES)?.toString("utf8") ?? null, + write: (key, text) => store.write("lib-diagnostics", key, text), + }, + checkerExecutableIdentity(toolchain.ts7Executable), + toolchain.prebuiltLibDiagnostics === undefined + ? null + : () => + distributionPrebuiltLibDiagnostics( + toolchain.prebuiltLibDiagnostics!, + toolchain.ts7Executable, + ), + ); + // A distribution build records its prebuilt lib diagnostics (build-native-cli.mts). + const recordPath = process.env["SCRIPTC_LIB_DIAGNOSTICS_RECORD"]; + const checkerVersion = + recordPath === undefined ? null : checkerPackageVersion(toolchain.ts7Executable); + if (recordPath !== undefined && checkerVersion !== null) + recordPrebuiltLibDiagnostics({ + checker: checkerVersion, + record: (key) => appendFileSync(recordPath, `${key}\n`), + }); + setCheckerFeatureStore({ + read: (key) => + store.read("checker-features", key, NATIVE_STRING_BYTES)?.toString("utf8") ?? null, + write: (key, text) => store.write("checker-features", key, text), + }); + } compiler = new NativeCompiler(toolchain, cache); return compiler; }; @@ -122,6 +163,7 @@ async function main(): Promise { dgram: false, dnsPromises: false, dgramDyn: false, + textCodecDyn: false, watch: false, foreignFfi: false, nodeTest: false, @@ -131,6 +173,7 @@ async function main(): Promise { dh: false, pki: false, ed25519: false, + x509: false, webcrypto: false, perfLoop: false, tlsCa: false, @@ -146,6 +189,19 @@ async function main(): Promise { } return { cacheRoot: cache.root, profiles }; }, + typePatches: async (request) => { + const toolchain = getCompiler().toolchain; + const services = new FrontendServices((options) => + createNativeTs7Api({ ...options, executable: toolchain.ts7Executable }), + ); + try { + return runTypePatchCommand(request, (path, source, kind) => + services.parse(path, source, kind), + ); + } finally { + services.close(); + } + }, mergeProfiles: async (outputPath, inputPaths, sparse) => { const toolchain = getCompiler().toolchain; mergeNativeProfiles({ diff --git a/packages/compiler/src/native/compiler.test.ts b/packages/compiler/src/native/compiler.test.ts index 7a1cee62a..c9636ea76 100644 --- a/packages/compiler/src/native/compiler.test.ts +++ b/packages/compiler/src/native/compiler.test.ts @@ -1,5 +1,13 @@ import { execFileSync, spawnSync } from "node:child_process"; -import { mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs"; +import { + chmodSync, + mkdirSync, + mkdtempSync, + readFileSync, + readdirSync, + rmSync, + writeFileSync, +} from "node:fs"; import { join } from "node:path"; import { tmpdir } from "node:os"; import { afterEach, expect, test, vi } from "vitest"; @@ -7,7 +15,7 @@ import { NativeCompiler } from "./compiler.js"; import type { NativeToolchain } from "./toolchain.js"; import { runNativeFrontend } from "../frontend/pipeline-native.js"; import { emitNativeObject } from "../backend/native-tools.js"; -import { MACOS_ARM64_TARGET } from "../backend/targets.js"; +import { LINUX_X64_GNU_TARGET, MACOS_ARM64_TARGET } from "../backend/targets.js"; import { VOID, type IrModule } from "../ir/ir.js"; import { contentDigest, NativeCache } from "./cache.js"; import { NativeExecutableCache, openNativeExecutableCache } from "./executable-cache.js"; @@ -99,7 +107,7 @@ test.each([ }); vi.mocked(emitNativeObject).mockImplementation((options) => { writeFileSync(options.outputPath, "object"); - return [options.outputPath]; + return { outputs: [options.outputPath], profileMatch: null }; }); const cache = new NativeCache(join(directory, "cache")); const key = contentDigest("executable link"); @@ -242,7 +250,7 @@ test.each([false, true])( expect(emitted).toContain("define i32 @main"); if (fail) throw new Error("codegen failed"); writeFileSync(options.outputPath, "new object"); - return [options.outputPath]; + return { outputs: [options.outputPath], profileMatch: null }; }); const target = MACOS_ARM64_TARGET; const toolchain: NativeToolchain = { @@ -283,3 +291,171 @@ test.each([false, true])( expect(readdirSync(directory).sort()).toEqual(["output.o", "output.o.ll"]); }, ); + +test.skipIf(process.platform === "win32").each([false, true])( + "Linux links use lld when the driver finds it, and the default linker when lld fails (%s)", + (lldFails) => { + const directory = mkdtempSync(join("/tmp", "scriptc-native-lld-")); + directories.push(directory); + const bin = join(directory, "bin"); + mkdirSync(bin); + const clang = join(bin, "clang"); + writeFileSync(clang, "#!/bin/sh\n"); + chmodSync(clang, 0o755); + writeFileSync(join(bin, "ld.lld"), "#!/bin/sh\n"); + chmodSync(join(bin, "ld.lld"), 0o755); + const entry = join(directory, "entry.ts"); + const output = join(directory, "program"); + const module: IrModule = { + irVersion: 17, + sourceFile: entry, + entry: "%main", + functions: [ + { + name: "%main", + params: [], + locals: [], + body: [], + returnType: VOID, + loc: { file: entry, start: 0, end: 0 }, + }, + ], + }; + vi.mocked(runNativeFrontend).mockReturnValue({ + preflight: [], + entryText: () => "", + entryExports: () => new Map(), + entryContract: () => { + throw new Error("unexpected contract request"); + }, + sourceTexts: () => new Map(), + npmStatic: [], + npmImportSites: new Map(), + dispose: () => {}, + lower: () => ({ + module, + diagnostics: [], + runtimeFences: [], + stats: { statementsTotal: 0, statementsFailed: 0, functionsSkipped: 0 }, + }), + }); + vi.mocked(emitNativeObject).mockImplementation((options) => { + writeFileSync(options.outputPath, "object"); + return { outputs: [options.outputPath], profileMatch: null }; + }); + vi.mocked(openNativeExecutableCache).mockReturnValue(null); + const links: string[][] = []; + vi.mocked(execFileSync).mockImplementation((_command, args) => { + const list = args as string[]; + links.push(list); + if (lldFails && list.includes("-fuse-ld=lld")) throw new Error("lld failed"); + writeFileSync(list[list.indexOf("-o") + 1]!, "executable"); + return ""; + }); + const target = LINUX_X64_GNU_TARGET; + const compiler = new NativeCompiler({ + compilerVersion: "0.0.0", + target, + helper: target.helper, + ts7Executable: "unused", + helperExecutable: "unused", + helperPackageRoot: "unused", + runtimePackRoot: "unused", + linker: clang, + linkerArgs: [], + dsymutil: "unused", + }); + const result = compiler.compile(entry, { + outDir: directory, + outPath: output, + backend: "llvm", + optimization: "release", + strip: true, + }); + expect(result).toMatchObject({ ok: true, binaryPath: output }); + expect(links[0]).toContain("-fuse-ld=lld"); + expect(links.length).toBe(lldFails ? 2 : 1); + if (lldFails) expect(links[1]).not.toContain("-fuse-ld=lld"); + }, +); + +test.skipIf(process.platform === "win32")( + "a --profile-use executable reports the helper's profile match", + () => { + const directory = mkdtempSync(join("/tmp", "scriptc-native-profile-")); + directories.push(directory); + const clang = join(directory, "clang"); + writeFileSync(clang, "#!/bin/sh\n"); + chmodSync(clang, 0o755); + const entry = join(directory, "entry.ts"); + const output = join(directory, "program"); + const profile = join(directory, "app.profdata"); + writeFileSync(profile, "indexed profile"); + const module: IrModule = { + irVersion: 17, + sourceFile: entry, + entry: "%main", + functions: [ + { + name: "%main", + params: [], + locals: [], + body: [], + returnType: VOID, + loc: { file: entry, start: 0, end: 0 }, + }, + ], + }; + vi.mocked(runNativeFrontend).mockReturnValue({ + preflight: [], + entryText: () => "", + entryExports: () => new Map(), + entryContract: () => { + throw new Error("unexpected contract request"); + }, + sourceTexts: () => new Map(), + npmStatic: [], + npmImportSites: new Map(), + dispose: () => {}, + lower: () => ({ + module, + diagnostics: [], + runtimeFences: [], + stats: { statementsTotal: 0, statementsFailed: 0, functionsSkipped: 0 }, + }), + }); + const match = { annotated: 120, mismatched: 7, missing: 40, unknownPartitions: 0 }; + vi.mocked(emitNativeObject).mockImplementation((options) => { + expect(options.profile?.use?.path).toBe(profile); + writeFileSync(options.outputPath, "object"); + return { outputs: [options.outputPath], profileMatch: match }; + }); + vi.mocked(openNativeExecutableCache).mockReturnValue(null); + vi.mocked(execFileSync).mockImplementation((_command, args) => { + const list = args as string[]; + writeFileSync(list[list.indexOf("-o") + 1]!, "executable"); + return ""; + }); + const target = LINUX_X64_GNU_TARGET; + const compiler = new NativeCompiler({ + compilerVersion: "0.0.0", + target, + helper: target.helper, + ts7Executable: "unused", + helperExecutable: "unused", + helperPackageRoot: "unused", + runtimePackRoot: "unused", + linker: clang, + linkerArgs: [], + dsymutil: "unused", + }); + const result = compiler.compile(entry, { + outDir: directory, + outPath: output, + backend: "llvm", + optimization: "speed", + profileUse: profile, + }); + expect(result).toMatchObject({ ok: true, artifact: { kind: "exe", profileMatch: match } }); + }, +); diff --git a/packages/compiler/src/native/compiler.ts b/packages/compiler/src/native/compiler.ts index a37d76f80..ea3772db3 100644 --- a/packages/compiler/src/native/compiler.ts +++ b/packages/compiler/src/native/compiler.ts @@ -21,7 +21,7 @@ import type { CompileRequestResult, } from "../compile-types.js"; import { LlvmUnsupportedError } from "../backend/llvm/emitter.js"; -import { executableLinkInputs } from "../backend/link-plan-core.js"; +import { executableLinkInputs, systemLibraryLinkerArgs } from "../backend/link-plan-core.js"; import { formatNativeLinkInfo } from "../backend/native-link-info-core.js"; import { emitNativeObject, @@ -30,8 +30,11 @@ import { verifyNativeHelper, } from "../backend/native-tools.js"; import { + type NativeRuntimeSelection, selectNativeRuntimePack, + runtimePackManifest, stageNativeRuntimeSelection, + type RuntimePackStore, } from "../backend/runtime-pack-native.js"; import { RuntimePackError } from "../backend/runtime-pack-core.js"; import { @@ -56,16 +59,20 @@ import { clearFenceEvalCaches } from "../library/fence-eval.js"; import { archiveNativeLibrary, localizeNativeLibrary, linkNativeWasmLibrary } from "./library.js"; import type { NativeToolchain } from "./toolchain.js"; import { evaluateNativeComptime } from "./comptime.js"; -import { contentDigest, fileDigest, type NativeCache } from "./cache.js"; +import { contentDigest, fileDigest, partsDigest, type NativeCache } from "./cache.js"; import { nativeProfileKey, nativeProfileRefusal, + parseNativeProfileMatch, + type NativeProfileMatch, type NativeProfileOptions, } from "../backend/native-profile.js"; import { splitLlvmLibraryProgram } from "../backend/llvm/split.js"; import { prepareNativeExecutable, prepareNativeLibrary } from "./prepare.js"; import { buildSanitizedRuntime, sanitizerDriver, sanitizerFlags } from "./sanitizer.js"; +import { lldLinkerArgs } from "../backend/lld.js"; import { openNativeExecutableCache } from "./executable-cache.js"; +import { LinkerJobs } from "./linker-job.js"; import { nativeFileIdentity } from "./file-identity.js"; /** The first partition keeps the whole-object key used before partitioning. */ @@ -73,6 +80,18 @@ function partitionCacheKey(key: string, index: number): string { return index === 0 ? key : contentDigest(`${key}:${index}`); } +/** The object cache entry that holds a cached program's profile match. */ +function profileMatchCacheKey(key: string): string { + return contentDigest(`${key}:profile-match`); +} + +/** A program's native objects, and how a --profile-use build's profile + * matched its functions. */ +interface NativeObjects { + paths: string[]; + profileMatch?: NativeProfileMatch; +} + function failure(error: unknown, entry: string, sources: Map): CompileFailure { return { ok: false, @@ -108,7 +127,7 @@ export class NativeCompiler { private readonly cache: NativeCache | null = null, ) { const evaluator = toolchain.comptimeExecutable; - this.frontend = (entry, npmStatic, externalTypes, libraryNpmStatic) => + this.frontend = (entry, npmStatic, externalTypes, libraryNpmStatic, options) => runNativeFrontend( entry, toolchain.ts7Executable, @@ -119,6 +138,7 @@ export class NativeCompiler { : (source, timeout) => evaluateNativeComptime(source, timeout, toolchain.ts7Executable, evaluator), libraryNpmStatic, + options, ); } @@ -160,6 +180,17 @@ export class NativeCompiler { return result; } + /** Runtime pack selections and verified artifact identities, kept in the + * cache. */ + private runtimePackStore(): RuntimePackStore | undefined { + const cache = this.cache; + if (cache === null) return undefined; + return { + read: (key) => cache.read("runtime-pack", key)?.toString("utf8") ?? null, + write: (key, text) => cache.write("runtime-pack", key, text), + }; + } + private emitObject( input: string, output: string, @@ -168,7 +199,8 @@ export class NativeCompiler { outputKind: "obj" | "asm", partitions = 1, profile?: NativeProfileOptions, - ): string[] { + llvmDigest: () => string = () => fileDigest(input), + ): NativeObjects { const toolchain = this.toolchain; const outputs = nativePartitionPaths(output, partitions); const helperOptions = { @@ -186,7 +218,7 @@ export class NativeCompiler { key = contentDigest( JSON.stringify({ schema: 1, - llvm: fileDigest(input), + llvm: llvmDigest(), outputKind, optimization, source, @@ -205,7 +237,20 @@ export class NativeCompiler { verifyNativeHelper(helperOptions); if (nativeFileIdentity(toolchain.helperExecutable) === identity) { cached.forEach((bytes, index) => writeFileSync(outputs[index]!, bytes!)); - return outputs; + if (profile?.use === undefined) return { paths: outputs }; + // Without the cached match, every partition's counts are unknown. + const match = this.cache.read("object", profileMatchCacheKey(key)); + return { + paths: outputs, + profileMatch: (match === null + ? null + : parseNativeProfileMatch(match.toString("utf8"))) ?? { + annotated: 0, + mismatched: 0, + missing: 0, + unknownPartitions: outputs.length, + }, + }; } key = null; } @@ -217,7 +262,7 @@ export class NativeCompiler { this.cache === null || identity === null || partitions < 2 ? null : this.cache.partitionDirectory(identity); - emitNativeObject({ + const { profileMatch } = emitNativeObject({ executable: toolchain.helperExecutable, packageRoot: toolchain.helperPackageRoot, compilerVersion: toolchain.compilerVersion, @@ -234,15 +279,30 @@ export class NativeCompiler { }); if (this.cache !== null && key !== null) { try { - if (nativeFileIdentity(toolchain.helperExecutable) === identity) + if (nativeFileIdentity(toolchain.helperExecutable) === identity) { + // The match goes in first: a reader that finds every partition finds it too. + if (profileMatch !== null) + this.cache.write( + "object", + profileMatchCacheKey(key), + `${JSON.stringify({ + profile: { + annotated: profileMatch.annotated, + mismatched: profileMatch.mismatched, + missing: profileMatch.missing, + unknown_partitions: profileMatch.unknownPartitions, + }, + })}\n`, + ); outputs.forEach((path, index) => this.cache!.write("object", partitionCacheKey(key!, index), readFileSync(path)), ); + } } catch { /* An updated helper or unavailable cache only prevents reuse. */ } } - return outputs; + return profileMatch === null ? { paths: outputs } : { paths: outputs, profileMatch }; } private emitProgramObject( @@ -253,7 +313,8 @@ export class NativeCompiler { stage: string, library: boolean, profile?: NativeProfileOptions, - ): string[] { + llvmDigest?: () => string, + ): NativeObjects { // Library archives merge dev shards relocatably; executables link the // helper's program partitions directly. const llvm = @@ -263,19 +324,28 @@ export class NativeCompiler { const split = llvm === null ? null : splitLlvmLibraryProgram(llvm); if (split === null) { const partitions = library ? 1 : nativeProgramPartitions(statSync(input).size); - return this.emitObject(input, output, source, optimization, "obj", partitions, profile); + return this.emitObject( + input, + output, + source, + optimization, + "obj", + partitions, + profile, + llvmDigest, + ); } const objects: string[] = []; for (const [index, shard] of split.shards.entries()) { const shardInput = join(stage, `shard-${index}.ll`); const shardOutput = join(stage, `shard-${index}` + this.toolchain.target.outputSuffixes.obj); - writeFileSync(shardInput, shard.source); + writeFileSync(shardInput, shard.source()); this.emitObject(shardInput, shardOutput, source + "." + shard.name, optimization, "obj"); objects.push(shardOutput); } const merged = localizeNativeLibrary(this.toolchain, stage, objects, [], split.publicSymbols); renameSync(merged, output); - return [output]; + return { paths: [output] }; } compile(entry: string, options: CompileRequestOptions): CompileRequestResult { @@ -358,6 +428,9 @@ export class NativeCompiler { return { ok: true, artifact: { kind: "ir", path: output } }; } const { llvm, features } = prepared; + // The executable and object caches key the same LLVM text. + let llvmDigest: string | undefined; + const digestLlvm = (): string => (llvmDigest ??= partsDigest(llvm)); const inputDirectory = join(stage, "input"); mkdirSync(inputDirectory); const llvmInput = outputKind === "llvm" ? stagedOutput : join(inputDirectory, "program.ll"); @@ -372,23 +445,36 @@ export class NativeCompiler { outputKind === "exe" ? join(inputDirectory, "program" + target.outputSuffixes.obj) : stagedOutput; - const executablePack = - outputKind === "exe" - ? selectNativeRuntimePack( - toolchain.runtimePackRoot, - target, - toolchain.compilerVersion, - features, - optimization, - ) - : null; + let executablePack: NativeRuntimeSelection | null = null; + const selectPack = (): NativeRuntimeSelection => + (executablePack ??= selectNativeRuntimePack( + toolchain.runtimePackRoot, + target, + toolchain.compilerVersion, + features, + optimization, + "executable", + this.runtimePackStore(), + )); const executableCache = - executablePack === null - ? null - : openNativeExecutableCache(this.cache, toolchain, llvm, options, ffi, executablePack); - const restored = executableCache?.restore(stagedOutput) ?? false; + outputKind === "exe" + ? openNativeExecutableCache(this.cache, toolchain, digestLlvm, options, ffi, { + root: toolchain.runtimePackRoot, + features, + flavor: optimization, + select: selectPack, + }) + : null; + // An unchanged rebuild keeps the executable the last build left. + const kept = executableCache?.keep(output) ?? false; + const restored = kept || (executableCache?.restore(stagedOutput) ?? false); timing(restored ? "executable-cache-hit" : "executable-cache-miss"); + // The executable cache leaves out --profile-use builds, so this is set + // whenever the build has a profile to report. + let profileMatch: NativeProfileMatch | undefined; if (!restored) { + // A missing or invalid runtime pack fails before code generation. + if (outputKind === "exe") selectPack(); let programObjects = [object]; if (options.sanitize) { runNativeTool(sanitizerDriver(toolchain), [ @@ -399,8 +485,8 @@ export class NativeCompiler { object, ]); requireNativeArtifact(object); - } else if (outputKind === "exe") - programObjects = this.emitProgramObject( + } else if (outputKind === "exe") { + const objects = this.emitProgramObject( llvmInput, object, entry, @@ -408,8 +494,11 @@ export class NativeCompiler { stage, false, profile, + digestLlvm, ); - else + programObjects = objects.paths; + profileMatch = objects.profileMatch; + } else this.emitObject( llvmInput, object, @@ -427,16 +516,24 @@ export class NativeCompiler { toolchain.compilerVersion, features, optimization, + "executable", + this.runtimePackStore(), ) : null; - if (pack !== null) stageNativeRuntimeSelection(pack, join(stage, "runtime")); + if (pack !== null) + stageNativeRuntimeSelection(pack, join(stage, "runtime"), this.runtimePackStore()); const info = pack === null ? undefined : formatNativeLinkInfo( { programObject: output, target, ffi }, toolchain.compilerVersion, - pack, + { + root: pack.root, + manifest: runtimePackManifest(pack), + selected: pack.selected, + flavor: pack.flavor, + }, ); renameSync(stagedOutput, output); return { @@ -448,10 +545,10 @@ export class NativeCompiler { }, }; } - const pack = executablePack!; + const pack = selectPack(); const runtime = options.sanitize ? buildSanitizedRuntime(toolchain, pack, join(stage, "runtime"), "executable", this.cache) - : stageNativeRuntimeSelection(pack, join(stage, "runtime")); + : stageNativeRuntimeSelection(pack, join(stage, "runtime"), this.runtimePackStore()); timing("runtime-stage"); const plan = executableLinkInputs({ target, @@ -470,20 +567,38 @@ export class NativeCompiler { : { windowsSubsystem: options.windowsSubsystem }), ...(profile?.generate === undefined ? {} : { profileGenerate: true }), }); + // Linux links with lld when the driver finds it. + const lld = options.sanitize ? [] : lldLinkerArgs(target.platform, toolchain.linker); const linkArgs = [ ...(options.sanitize ? ["-fsanitize=address"] : toolchain.linkerArgs), + ...lld, ...plan.driverFlags, ...plan.inputs, - ...plan.systemLibraries.map((name) => `-l${name}`), + ...systemLibraryLinkerArgs(target, plan.systemLibraries), "-o", stagedOutput, ]; - const cacheable = executableCache?.trace(linkArgs, stage) ?? false; + // Host Linux links with lld run the linker job the driver would. + const jobs = LinkerJobs.open(this.cache, toolchain, lld, options.sanitize ?? false); + const job = jobs?.link([...plan.inputs, stagedOutput], stage); + const cacheable = executableCache?.trace(linkArgs, stage, job) ?? false; timing("link-inputs"); // A successful dependency trace already produced this executable. // Keep its verified output instead of asking the linker to repeat it. - if (!cacheable) - runNativeTool(options.sanitize ? sanitizerDriver(toolchain) : toolchain.linker, linkArgs); + if (!cacheable && (job?.run(linkArgs, true) ?? null) === null) { + const driver = options.sanitize ? sanitizerDriver(toolchain) : toolchain.linker; + try { + runNativeTool(driver, linkArgs); + } catch (error) { + // A failed link with lld runs again with the driver's default + // linker, whose error (if any) is the one reported. + if (lld.length === 0) throw error; + runNativeTool( + driver, + linkArgs.filter((arg) => !lld.includes(arg)), + ); + } + } timing("native-link"); requireNativeArtifact(stagedOutput); if (target.platform === "darwin" && optimization === "dev" && !options.strip) { @@ -502,12 +617,21 @@ export class NativeCompiler { irPath = join(options.outDir, `${stem}.ir.json`); writeTextPartsSync(irPath, prepared.ir!); } - renameSync(stagedOutput, output); + if (!kept) { + renameSync(stagedOutput, output); + executableCache?.recordOutput(output); + } if (target.platform === "darwin" && (optimization !== "dev" || options.strip)) rmSync(output + ".dSYM", { recursive: true, force: true }); return { ok: true, - artifact: { kind: "exe", path: output, translationUnitPath: llvmPath, backend: "llvm" }, + artifact: { + kind: "exe", + path: output, + translationUnitPath: llvmPath, + backend: "llvm", + ...(profileMatch === undefined ? {} : { profileMatch }), + }, binaryPath: output, llvmPath, backend: "llvm", @@ -595,6 +719,7 @@ export class NativeCompiler { features, profile.optimization, profile.instancePerThread ? "library-thread" : "library", + this.runtimePackStore(), ); const runtime = options.sanitize ? buildSanitizedRuntime( @@ -604,7 +729,7 @@ export class NativeCompiler { profile.instancePerThread ? "library-thread" : "library", this.cache, ) - : stageNativeRuntimeSelection(pack, join(stage, "runtime")); + : stageNativeRuntimeSelection(pack, join(stage, "runtime"), this.runtimePackStore()); timing("runtime-stage"); const stagedArchive = join(stage, wasm ? "output.wasm" : "output.a"); const localizeSymbols = libraryLocalizeSymbols(profile); diff --git a/packages/compiler/src/native/executable-cache-open.test.ts b/packages/compiler/src/native/executable-cache-open.test.ts new file mode 100644 index 000000000..0c3b2832f --- /dev/null +++ b/packages/compiler/src/native/executable-cache-open.test.ts @@ -0,0 +1,93 @@ +import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeEach, expect, test } from "vitest"; +import type { NativeLinkFeatures } from "../backend/native-link-info.js"; +import type { NativeRuntimeSelection } from "../backend/runtime-pack-native.js"; +import { NativeCache } from "./cache.js"; +import { type ExecutableRuntimePack, openNativeExecutableCache } from "./executable-cache.js"; +import type { NativeToolchain } from "./toolchain.js"; + +const directories: string[] = []; +const saved = new Map(); +beforeEach(() => { + for (const name of ["SCRIPTC_LINKER", "SCRIPTC_LLVM_HELPER", "CC", "LDFLAGS", "LIBRARY_PATH"]) { + saved.set(name, process.env[name]); + delete process.env[name]; + } +}); +afterEach(() => { + for (const [name, value] of saved) { + if (value === undefined) delete process.env[name]; + else process.env[name] = value; + } + saved.clear(); + for (const path of directories.splice(0)) rmSync(path, { recursive: true, force: true }); +}); + +/** A toolchain whose linker is a driver script named `driver`; it answers + * `-print-prog-name=clang` with its own path. */ +function fixture(platform: string, driver: string) { + const root = mkdtempSync(join(tmpdir(), "scriptc-executable-cache-open-")); + directories.push(root); + const bin = join(root, "bin"); + const helper = join(root, "helper"); + const runtime = join(root, "runtime"); + for (const directory of [bin, helper, runtime]) mkdirSync(directory); + const linker = join(bin, driver); + writeFileSync(linker, '#!/bin/sh\necho "$0"\n'); + chmodSync(linker, 0o755); + writeFileSync(join(helper, "codegen"), "helper"); + writeFileSync(join(helper, "package.json"), "{}"); + writeFileSync(join(runtime, "package.json"), "{}"); + writeFileSync(join(runtime, "runtime-pack.json"), "{}"); + const toolchain = { + compilerVersion: "test", + target: { platform }, + helperExecutable: join(helper, "codegen"), + helperPackageRoot: helper, + runtimePackRoot: runtime, + linker, + linkerArgs: [], + dsymutil: "dsymutil", + } as unknown as NativeToolchain; + const pack: ExecutableRuntimePack = { + root: runtime, + features: {} as NativeLinkFeatures, + flavor: "release", + select: () => + ({ + root: runtime, + licenses: [], + packageText: "{}", + manifestText: "{}", + selected: { runtime: [], archives: [] }, + flavor: "release", + }) as unknown as NativeRuntimeSelection, + }; + const open = () => + openNativeExecutableCache( + new NativeCache(join(root, "cache")), + toolchain, + () => "llvm digest", + { outPath: join(root, "program"), outDir: root, optimization: "release" }, + null, + pack, + ); + return { open }; +} + +test.skipIf(process.platform === "win32")( + "Linux builds that link with a clang driver use the executable cache", + () => { + expect(fixture("linux", "clang").open()).not.toBeNull(); + expect(fixture("linux", "clang-22").open()).not.toBeNull(); + expect(fixture("linux", "gcc").open()).toBeNull(); + expect(fixture("windows", "clang").open()).toBeNull(); + expect(fixture("wasi", "clang").open()).toBeNull(); + // On darwin only Apple's driver at /usr/bin/clang qualifies. + expect(fixture("darwin", "clang").open()).toBeNull(); + process.env["SCRIPTC_LLVM_HELPER"] = "/elsewhere/codegen"; + expect(fixture("linux", "clang").open()).toBeNull(); + }, +); diff --git a/packages/compiler/src/native/executable-cache.test.ts b/packages/compiler/src/native/executable-cache.test.ts index 3199ef97f..4f950e5b5 100644 --- a/packages/compiler/src/native/executable-cache.test.ts +++ b/packages/compiler/src/native/executable-cache.test.ts @@ -15,7 +15,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, expect, test, vi } from "vitest"; import { contentDigest, NativeCache } from "./cache.js"; -import { NativeExecutableCache } from "./executable-cache.js"; +import { identity, NativeExecutableCache } from "./executable-cache.js"; vi.mock("node:child_process", async (original) => ({ ...(await original()), @@ -40,6 +40,11 @@ function fixture(debug = false) { const library = join(sdk, "libSystem.tbd"); writeFileSync(runtime, "runtime"); writeFileSync(library, "system library"); + // A directory's identity is its stat. Where the file system stamps times + // at the kernel tick, a file added within the tick of the last write + // leaves the directory's times unchanged; an old time makes every later + // change to the SDK directory visible. + utimesSync(sdk, 0, 0); const cache = new NativeCache(join(root, "cache")); const key = contentDigest("llvm and options"); const open = () => new NativeExecutableCache(cache, key, "clang", debug, [runtime]); @@ -217,3 +222,152 @@ test("a failed or empty link trace cannot publish a completed executable", () => }); expect(entry.trace([], f.stage)).toBe(false); }); + +test.each(["ld", "crt", "libc"] as const)( + "a Linux key's first link records the linker, startup objects and libc; replacing the %s misses", + (input) => { + const f = fixture(); + const lib = join(f.root, "lib"); + mkdirSync(lib); + const paths = { + ld: join(f.root, "ld"), + crt: join(lib, "Scrt1.o"), + libc: join(lib, "libc.so.6"), + }; + writeFileSync(paths.ld, "linker"); + writeFileSync(paths.crt, "startup object"); + writeFileSync(paths.libc, "c library"); + const nonshared = join(lib, "libc_nonshared.a"); + writeFileSync(nonshared, "archive"); + const program = join(f.stage, "program.o"); + writeFileSync(program, "program"); + // With -v, clang prints the linker job it runs, as -### would; GNU ld's + // -t names each file it loads, and an archive member as archive(member). + const job = ` "${paths.ld}" "-pie" "-o" "${f.output}" "${paths.crt}" "-L${lib}" "${program}" "-lc"`; + const loaded = [paths.crt, program, paths.libc, `${nonshared}(atexit.oS)`].join("\n"); + vi.mocked(spawnSync).mockImplementation((_command, args) => { + const verbose = (args as string[]).includes("-v"); + return { + pid: 1, + status: 0, + signal: null, + output: [], + stdout: verbose ? loaded + "\n" : "", + stderr: verbose ? job + "\n" : "", + }; + }); + const open = () => new NativeExecutableCache(f.cache, f.key, "clang", false, [f.runtime], true); + // A key's first link traces the driver and the linker in one run, and + // records the output it leaves instead of storing the executable. + const entry = open(); + expect(entry.trace(["-o", f.output], f.stage)).toBe(true); + expect(spawnSync).toHaveBeenCalledTimes(1); + expect(vi.mocked(spawnSync).mock.calls[0]![1]).toEqual(["-o", f.output, "-v", "-Wl,-t"]); + entry.publish(f.output); + expect(f.cache.read("executable", f.key)).toBeNull(); + entry.recordOutput(f.output); + expect(open().keep(f.output)).toBe(true); + const recorded = ( + JSON.parse(f.cache.read("kept", f.key)!.toString()) as { inputs: { path: string }[] } + ).inputs.map((saved) => saved.path); + expect(recorded).toEqual(expect.arrayContaining([...Object.values(paths), nonshared, lib])); + expect(recorded).not.toContain(program); + const before = statSync(paths[input]); + writeFileSync(paths[input] + ".new", "x".repeat(before.size)); + utimesSync(paths[input] + ".new", before.atime, before.mtime); + renameSync(paths[input] + ".new", paths[input]); + expect(open().keep(f.output)).toBe(false); + }, +); + +test("a Linux build whose output changed links again, and then stores the executable", () => { + const f = fixture(); + const open = () => new NativeExecutableCache(f.cache, f.key, "clang", false, [f.runtime], true); + const first = open(); + expect(first.trace(["-o", f.output], f.stage)).toBe(true); + first.publish(f.output); + first.recordOutput(f.output); + // The output is overwritten: it is not kept, and the next link publishes. + writeFileSync(f.output, Buffer.from([1, 2, 3, 4])); + expect(open().keep(f.output)).toBe(false); + const second = open(); + expect(second.restore(f.destination)).toBe(false); + expect(second.trace(["-o", f.output], f.stage)).toBe(true); + second.publish(f.output); + second.recordOutput(f.output); + expect(open().keep(f.output)).toBe(true); + // The stored executable restores at another path. + expect(open().restore(f.destination)).toBe(true); + expect(readFileSync(f.destination)).toEqual(readFileSync(f.output)); +}); + +test("a Linux build that keeps its output does not read the deferred inputs", () => { + const f = fixture(); + const artifact = join(f.root, "inputs", "artifact.o"); + writeFileSync(artifact, "runtime pack artifact"); + const deferred = vi.fn(() => [artifact]); + const open = () => + new NativeExecutableCache(f.cache, f.key, "clang", false, [f.runtime], true, deferred); + const first = open(); + expect(first.trace(["-o", f.output], f.stage)).toBe(true); + first.recordOutput(f.output); + expect(deferred).toHaveBeenCalledTimes(1); + const recorded = ( + JSON.parse(f.cache.read("kept", f.key)!.toString()) as { inputs: { path: string }[] } + ).inputs.map((saved) => saved.path); + expect(recorded).toContain(artifact); + // The unchanged rebuild checks the recorded identities, the artifact's + // among them, without asking for the deferred inputs. + expect(open().keep(f.output)).toBe(true); + expect(deferred).toHaveBeenCalledTimes(1); + // A replaced artifact misses. + writeFileSync(artifact, "another runtime pack artifact"); + expect(open().keep(f.output)).toBe(false); + expect(deferred).toHaveBeenCalledTimes(1); + // restore() adds the deferred inputs before it checks an entry. + const second = open(); + expect(second.trace(["-o", f.output], f.stage)).toBe(true); + second.publish(f.output); + expect(open().restore(f.destination)).toBe(true); + expect(deferred).toHaveBeenCalledTimes(3); +}); + +test("a Linux link that runs as its linker job is traced from the job's run, and otherwise records it", () => { + const f = fixture(); + const lib = join(f.root, "lib"); + mkdirSync(lib); + const crt = join(lib, "Scrt1.o"); + writeFileSync(crt, "startup object"); + const program = join(f.stage, "program.o"); + writeFileSync(program, "program"); + const job = { + run: vi.fn((_args: string[], _list: boolean) => ({ + output: [crt, program].join("\n") + "\n", + driverInputs: [identity(crt), identity(lib)], + })), + record: vi.fn(), + }; + const open = () => new NativeExecutableCache(f.cache, f.key, "clang", false, [f.runtime], true); + const entry = open(); + expect(entry.trace(["-o", f.output], f.stage, job)).toBe(true); + // The job runs the traced link, and the driver does not. + expect(job.run.mock.calls[0]).toEqual([["-o", f.output, "-v", "-Wl,-t"], false]); + expect(spawnSync).not.toHaveBeenCalled(); + expect(job.record).not.toHaveBeenCalled(); + entry.recordOutput(f.output); + const recorded = ( + JSON.parse(f.cache.read("kept", f.key)!.toString()) as { inputs: { path: string }[] } + ).inputs.map((saved) => saved.path); + expect(recorded).toEqual(expect.arrayContaining([crt, lib])); + expect(recorded).not.toContain(program); + expect(open().keep(f.output)).toBe(true); + // A link without a job runs through the driver, and records the job from + // the driver's output. + const record = vi.fn(); + expect(open().trace(["-o", f.output], f.stage, { run: () => null, record })).toBe(true); + expect(spawnSync).toHaveBeenCalledTimes(1); + expect(record).toHaveBeenCalledWith(["-o", f.output, "-v", "-Wl,-t"], f.library + "\n\n", [ + identity(f.sdk), + identity(f.library), + ]); +}); diff --git a/packages/compiler/src/native/executable-cache.ts b/packages/compiler/src/native/executable-cache.ts index c0ac755b9..a26bc1d87 100644 --- a/packages/compiler/src/native/executable-cache.ts +++ b/packages/compiler/src/native/executable-cache.ts @@ -9,11 +9,14 @@ import { toolchainEnvironmentFingerprint, } from "../backend/toolchain-environment.js"; import { runNativeTool } from "../backend/native-tools.js"; +import type { NativeLinkFeatures } from "../backend/native-link-info.js"; +import type { NativeOptimization } from "../backend/optimization.js"; import type { NativeRuntimeSelection } from "../backend/runtime-pack-native.js"; import type { NativeToolchain } from "./toolchain.js"; -import { contentDigest, NativeCache, partsDigest } from "./cache.js"; +import { contentDigest, NativeCache } from "./cache.js"; +import type { LinkerJobLink } from "./linker-job.js"; -interface InputIdentity { +export interface InputIdentity { path: string; canonical: string; kind: "file" | "directory"; @@ -31,7 +34,7 @@ interface ExecutableEntry { symbols: string | null; } -function identity(path: string): InputIdentity { +export function identity(path: string): InputIdentity { path = resolve(path); const info = statSync(path); if (!info.isFile() && !info.isDirectory()) @@ -48,15 +51,28 @@ function identity(path: string): InputIdentity { }; } -function stillMatches(inputs: InputIdentity[]): boolean { +function sameIdentity(a: InputIdentity, b: InputIdentity): boolean { + return ( + a.path === b.path && + a.canonical === b.canonical && + a.kind === b.kind && + a.dev === b.dev && + a.ino === b.ino && + a.size === b.size && + a.mtime === b.mtime && + a.ctime === b.ctime + ); +} + +export function stillMatches(inputs: InputIdentity[]): boolean { try { - return inputs.every((input) => JSON.stringify(identity(input.path)) === JSON.stringify(input)); + return inputs.every((input) => sameIdentity(identity(input.path), input)); } catch { return false; } } -function commandPath(command: string): string { +export function commandPath(command: string): string { if (command.includes("/") || command.includes("\\")) return resolve(command); for (const directory of (process.env["PATH"] ?? "/usr/bin:/bin").split(delimiter)) { const candidate = join(directory || process.cwd(), command); @@ -77,7 +93,7 @@ function toolOutput(executable: string, args: string[]): string { return result.stdout + "\n" + result.stderr; } -function tracePaths(output: string, driver: boolean, roots: string[]): string[] { +export function tracePaths(output: string, driver: boolean, roots: string[]): string[] { const paths = new Set(); for (const line of output.split(/\r?\n/)) { for (const candidate of driver ? driverTraceCandidates(line) : linkTraceCandidate(line)) { @@ -111,6 +127,41 @@ function tracePaths(output: string, driver: boolean, roots: string[]): string[] return [...paths].sort(); } +/** Linux: the output a build left, which an unchanged rebuild keeps while + * the output and every input keep their identities. */ +interface KeptEntry { + schema: "scriptc.native-kept.v1"; + output: InputIdentity; + inputs: InputIdentity[]; +} + +export function validIdentity(input: InputIdentity): boolean { + return ( + input !== null && + typeof input === "object" && + typeof input.path === "string" && + isAbsolute(input.path) && + typeof input.canonical === "string" && + (input.kind === "file" || input.kind === "directory") && + [input.dev, input.ino, input.size, input.mtime, input.ctime].every( + (value) => typeof value === "number" && Number.isFinite(value), + ) + ); +} + +function validKept(value: unknown): value is KeptEntry { + if (value === null || typeof value !== "object") return false; + const entry = value as Partial; + return ( + entry.schema === "scriptc.native-kept.v1" && + entry.output !== undefined && + validIdentity(entry.output) && + Array.isArray(entry.inputs) && + entry.inputs.length > 0 && + entry.inputs.every(validIdentity) + ); +} + function validEntry(value: unknown): value is ExecutableEntry { if (value === null || typeof value !== "object") return false; const entry = value as Partial; @@ -122,18 +173,7 @@ function validEntry(value: unknown): value is ExecutableEntry { (typeof entry.symbols === "string" && /^[0-9a-f]{64}$/.test(entry.symbols))) && Array.isArray(entry.inputs) && entry.inputs.length > 0 && - entry.inputs.every( - (input) => - input !== null && - typeof input === "object" && - typeof input.path === "string" && - isAbsolute(input.path) && - typeof input.canonical === "string" && - (input.kind === "file" || input.kind === "directory") && - [input.dev, input.ino, input.size, input.mtime, input.ctime].every( - (value) => typeof value === "number" && Number.isFinite(value), - ), - ) + entry.inputs.every(validIdentity) ); } @@ -165,6 +205,9 @@ function stageSymbols(bytes: Buffer, binary: string): void { export class NativeExecutableCache { private inputs: InputIdentity[]; private traced = false; + /** The inputs of the entry restore() used. */ + private restoredInputs: InputIdentity[] | null = null; + private deferredAdded = false; constructor( private readonly cache: NativeCache, @@ -172,25 +215,62 @@ export class NativeExecutableCache { private readonly linker: string, private readonly debug: boolean, paths: string[], + /** Linux: trace with one verbose link, keep an unchanged output, and + * store the executable from a key's second link on. */ + private readonly linux = false, + /** Inputs that the key determines, such as the runtime pack's selected + * artifacts. keep() does not read them, because the entry it checks + * lists them; restore() and trace() add their identities first. */ + private readonly deferredPaths: () => string[] = () => [], ) { this.inputs = [...new Set(paths)].sort().map(identity); } + private addDeferredInputs(): void { + if (this.deferredAdded) return; + this.deferredAdded = true; + const listed = new Set(this.inputs.map((input) => input.path)); + for (const path of this.deferredPaths()) + if (!listed.has(path)) { + listed.add(path); + this.inputs.push(identity(path)); + } + } + + /** Whether `recorded`, the inputs of an entry, still match and cover this + * build's inputs. */ + private covers(recorded: InputIdentity[]): boolean { + if (!stillMatches(recorded) || !stillMatches(this.inputs)) return false; + const saved = new Map(recorded.map((input) => [input.path, input])); + return this.inputs.every((input) => { + const found = saved.get(input.path); + return found !== undefined && sameIdentity(found, input); + }); + } + + /** Linux: true when `output` is the executable the last build of this key + * left there, unchanged, and its inputs are unchanged; the caller keeps it. */ + keep(output: string): boolean { + if (!this.linux) return false; + try { + const metadata = this.cache.read("kept", this.key); + if (metadata === null) return false; + const entry: unknown = JSON.parse(metadata.toString("utf8")); + if (!validKept(entry)) return false; + const current = identity(output); + return sameIdentity(current, entry.output) && this.covers(entry.inputs); + } catch { + return false; + } + } + restore(output: string): boolean { try { const metadata = this.cache.read("executable", this.key); if (metadata === null) return false; + this.addDeferredInputs(); const entry: unknown = JSON.parse(metadata.toString("utf8")); - if (!validEntry(entry) || !stillMatches(entry.inputs) || !stillMatches(this.inputs)) - return false; - if ( - !this.inputs.every((input) => - entry.inputs.some( - (saved) => saved.path === input.path && JSON.stringify(saved) === JSON.stringify(input), - ), - ) - ) - return false; + if (!validEntry(entry) || !this.covers(entry.inputs)) return false; if (this.debug !== (entry.symbols !== null)) return false; const binary = this.cache.read("binary", entry.binary); const symbols = entry.symbols === null ? null : this.cache.read("dsym", entry.symbols); @@ -199,6 +279,7 @@ export class NativeExecutableCache { if (symbols !== null) stageSymbols(symbols, output); writeFileSync(output, binary); chmodSync(output, 0o755); + this.restoredInputs = entry.inputs; return true; } catch { return false; @@ -209,18 +290,37 @@ export class NativeExecutableCache { * Directories from the driver's search line detect a newly added candidate. * Private build inputs already belong to the LLVM/runtime cache identity. * A true result means the requested output was linked successfully and its - * inputs remained stable. The caller can publish that output directly. */ - trace(args: string[], stage: string): boolean { + * inputs remained stable. The caller can publish that output directly. On + * false, the caller links the output itself. On Linux, `job` runs the + * traced link as its recorded linker job when it can, and otherwise records + * the job the driver ran (see LinkerJobs). */ + trace(args: string[], stage: string, job?: LinkerJobLink): boolean { this.traced = false; try { + this.addDeferredInputs(); const roots = [resolve(stage), realpathSync(stage)]; - const dry = toolOutput(this.linker, [...args, "-###"]); - const driverInputs = tracePaths(dry, true, roots).map(identity); - const linked = toolOutput(this.linker, [...args, "-Wl,-t"]); + let driverInputs = this.linux + ? [] + : tracePaths(toolOutput(this.linker, [...args, "-###"]), true, roots).map(identity); + // On Linux, -v prints the jobs that -### would, while the link runs. + const traced = [...args, ...(this.linux ? ["-v"] : []), "-Wl,-t"]; + const direct = this.linux ? (job?.run(traced, false) ?? null) : null; + const linked = direct?.output ?? toolOutput(this.linker, traced); + if (this.linux) { + driverInputs = direct?.driverInputs ?? tracePaths(linked, true, roots).map(identity); + if (direct === null) job?.record(traced, linked, driverInputs); + } const paths = tracePaths(linked, false, roots); if (paths.length === 0 || !stillMatches(this.inputs) || !stillMatches(driverInputs)) return false; - this.inputs.push(...driverInputs, ...paths.map(identity)); + // The driver's job line and the linker's trace name many of the same + // files; each is checked once. + const listed = new Set(this.inputs.map((input) => input.path)); + for (const input of [...driverInputs, ...paths.map(identity)]) + if (!listed.has(input.path)) { + listed.add(input.path); + this.inputs.push(input); + } this.traced = true; return true; } catch { @@ -230,13 +330,14 @@ export class NativeExecutableCache { publish(output: string): void { try { - if (!this.traced || !stillMatches(this.inputs)) return; + // On Linux the first build of a key only records its output (see + // recordOutput); the executable is stored once a later build relinks. + if (!this.traced || (this.linux && !this.cache.has("kept", this.key))) return; + if (!stillMatches(this.inputs)) return; const binary = readFileSync(output); const symbols = this.debug ? readSymbols(output) : null; - const binaryKey = contentDigest(binary); - const symbolsKey = symbols === null ? null : contentDigest(symbols); - this.cache.write("binary", binaryKey, binary); - if (symbols !== null && symbolsKey !== null) this.cache.write("dsym", symbolsKey, symbols); + const binaryKey = this.cache.writeContent("binary", binary); + const symbolsKey = symbols === null ? null : this.cache.writeContent("dsym", symbols); if (!stillMatches(this.inputs)) return; const entry: ExecutableEntry = { schema: "scriptc.native-executable.v1", @@ -249,21 +350,52 @@ export class NativeExecutableCache { /* Build artifacts remain valid when caching is unavailable. */ } } + + /** Linux: records `output`, the executable this build left in place, for + * keep(). */ + recordOutput(output: string): void { + if (!this.linux) return; + try { + const inputs = this.traced ? this.inputs : this.restoredInputs; + if (inputs === null || !stillMatches(inputs)) return; + const entry: KeptEntry = { + schema: "scriptc.native-kept.v1", + output: identity(output), + inputs, + }; + this.cache.write("kept", this.key, JSON.stringify(entry)); + } catch { + /* Build artifacts remain valid when caching is unavailable. */ + } + } +} + +/** The runtime pack an executable links: its directory, and the link features + * and flavor that select its artifacts. `select` reads and validates the + * manifest; the cache calls it only when it needs the artifacts' identities, + * so an unchanged rebuild that keeps its output never reads the manifest. */ +export interface ExecutableRuntimePack { + root: string; + features: NativeLinkFeatures; + flavor: NativeOptimization; + select: () => NativeRuntimeSelection; } export function openNativeExecutableCache( cache: NativeCache | null, toolchain: NativeToolchain, - llvm: readonly string[], + llvmDigest: () => string, options: CompileRequestOptions, ffi: FfiProfile | null, - pack: NativeRuntimeSelection, + pack: ExecutableRuntimePack, ): NativeExecutableCache | null { - // Match the established complete-cache contract: default Apple driver, - // known runtime inputs, and no wrapper or caller-supplied library searches. + // Match the established complete-cache contract: the default Apple driver + // or a Linux clang driver, known runtime inputs, and no wrapper or + // caller-supplied library searches. + const platform = toolchain.target.platform; if ( cache === null || - toolchain.target.platform !== "darwin" || + (platform !== "darwin" && platform !== "linux") || options.sanitize || // The key names a profile by path, not by its bytes. options.profileGenerate !== undefined || @@ -277,9 +409,23 @@ export function openNativeExecutableCache( return null; try { const linker = commandPath(toolchain.linker); - if (realpathSync(linker) !== "/usr/bin/clang") return null; - const effective = commandPath(runNativeTool(linker, ["-print-prog-name=clang"]).trim()); - const debug = options.optimization === "dev" && !options.strip; + // Apple's driver at /usr/bin/clang, or a clang driver on Linux. Both + // name every input of a link in their -### and -Wl,-t traces. + if ( + platform === "darwin" + ? realpathSync(linker) !== "/usr/bin/clang" + : !/^clang(-\d+)?$/.test(basename(realpathSync(linker))) + ) + return null; + // Apple's /usr/bin/clang is a shim for the active developer directory's + // driver; a Linux clang driver is itself the driver. + const effective = + platform === "darwin" + ? commandPath(runNativeTool(linker, ["-print-prog-name=clang"]).trim()) + : linker; + // Linux keeps debug information in the executable; darwin keeps it in a + // separate dSYM, which the cache stores beside the executable. + const debug = platform === "darwin" && options.optimization === "dev" && !options.strip; const paths = [ linker, effective, @@ -287,10 +433,19 @@ export function openNativeExecutableCache( join(toolchain.helperPackageRoot, "package.json"), join(toolchain.runtimePackRoot, "package.json"), join(toolchain.runtimePackRoot, "runtime-pack.json"), - ...[...pack.selected.runtime, ...pack.selected.archives, ...pack.manifest.licenses].map( - (artifact) => join(pack.root, artifact.path), - ), ]; + const manifest = join(pack.root, "runtime-pack.json"); + if (platform === "linux") paths.push(manifest); + // The manifest's identity, the features and the flavor in the key fix + // which artifacts the selection names. + const artifacts = (): string[] => { + const selection = pack.select(); + return [ + ...selection.selected.runtime, + ...selection.selected.archives, + ...selection.licenses, + ].map((artifact) => join(selection.root, artifact.path)); + }; if (debug) { const dsymutil = commandPath(toolchain.dsymutil); if (realpathSync(dsymutil) !== "/usr/bin/dsymutil") return null; @@ -299,11 +454,16 @@ export function openNativeExecutableCache( const key = contentDigest( JSON.stringify({ schema: 1, - llvm: partsDigest(llvm), + llvm: llvmDigest(), options, toolchain, - runtimeIdentity: pack.packageText, - runtimeManifest: pack.manifestText, + runtimeIdentity: readFileSync(join(pack.root, "package.json"), "utf8"), + // On Linux the manifest's identity names it: restore checks that + // identity again, as it does every other input. + runtimeManifest: + platform === "darwin" ? readFileSync(manifest, "utf8") : identity(manifest), + runtimeFeatures: pack.features, + runtimeFlavor: pack.flavor, linker: identity(linker), effective: identity(effective), environment: toolchainEnvironmentFingerprint(), @@ -311,7 +471,15 @@ export function openNativeExecutableCache( path: process.env["PATH"] ?? null, }), ); - return new NativeExecutableCache(cache, key, linker, debug, paths); + return new NativeExecutableCache( + cache, + key, + linker, + debug, + paths, + platform === "linux", + artifacts, + ); } catch { return null; } diff --git a/packages/compiler/src/native/linker-job.test.ts b/packages/compiler/src/native/linker-job.test.ts new file mode 100644 index 000000000..b9d718c03 --- /dev/null +++ b/packages/compiler/src/native/linker-job.test.ts @@ -0,0 +1,408 @@ +import { spawnSync } from "node:child_process"; +import { + chmodSync, + mkdirSync, + mkdtempSync, + readFileSync, + renameSync, + rmSync, + statSync, + utimesSync, + writeFileSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeEach, expect, test } from "vitest"; +import { + LINUX_ARM64_GNU_TARGET, + LINUX_X64_GNU_TARGET, + MACOS_ARM64_TARGET, + type NativeTargetSpec, +} from "../backend/targets.js"; +import { NativeCache } from "./cache.js"; +import { LinkerJobs } from "./linker-job.js"; +import type { NativeToolchain } from "./toolchain.js"; + +const directories: string[] = []; +const saved = new Map(); +const variables = [ + "SCRIPTC_LINKER", + "COMPILER_PATH", + "LOG", + "LD", + "CRT", + "LIB", + "FAIL", + "BROKEN", + "DOUBLE", +]; +beforeEach(() => { + for (const name of variables) { + saved.set(name, process.env[name]); + delete process.env[name]; + } +}); +afterEach(() => { + for (const [name, value] of saved) { + if (value === undefined) delete process.env[name]; + else process.env[name] = value; + } + saved.clear(); + for (const path of directories.splice(0)) rmSync(path, { recursive: true, force: true }); +}); + +function script(path: string, text: string): string { + writeFileSync(path, text); + chmodSync(path, 0o755); + return path; +} + +/** A driver named `name` and an ld.lld that log their arguments to $LOG. The + * driver's job runs $LD with `-pie`, the startup object $CRT, `-L$LIB` and + * its own arguments (without `-fuse-ld=lld` and `-v`, `-Wl,` removed). With + * -### it prints that job as clang does, every argument quoted; with -v it + * prints the job it runs as clang does, only the program quoted. With + * $BROKEN, -### prints the job twice; with $DOUBLE, it names the path $DOUBLE + * once more. The linker writes its inputs to its output, prints them with + * `-t`, and fails with $FAIL. */ +function fixture(name = "clang") { + const root = mkdtempSync(join(tmpdir(), "scriptc-linker-job-")); + directories.push(root); + const bin = join(root, "bin"); + const lib = join(root, "lib"); + // The recorded inputs include the parents of the toolchain's paths (here + // `root`); stages are elsewhere, as a build's are. + const stages = join(root, "stages"); + for (const directory of [bin, lib, stages]) mkdirSync(directory); + const log = join(root, "log"); + writeFileSync(log, ""); + const driver = script( + join(bin, name), + `#!/bin/sh +echo "driver $*" >> "$LOG" +job=" \\"$LD\\" \\"-pie\\" \\"$CRT\\" \\"-L$LIB\\"" +raw=" \\"$LD\\" -pie $CRT -L$LIB" +listing=0; verbose=0 +for a; do + case "$a" in + -'###') listing=1 ;; + -v) verbose=1 ;; + -fuse-ld=lld) ;; + -Wl,*) job="$job \\"\${a#-Wl,}\\""; raw="$raw \${a#-Wl,}" ;; + *) job="$job \\"$a\\""; raw="$raw $a" ;; + esac +done +if [ $listing = 1 ]; then + echo "InstalledDir: $(dirname "$0")" >&2 + [ -z "$DOUBLE" ] && echo "$job" >&2 + [ -n "$BROKEN" ] && echo "$job" >&2 + [ -n "$DOUBLE" ] && echo "$job \\"$DOUBLE\\"" >&2 && exit 0 + exit 0 +fi +[ $verbose = 1 ] && echo "$raw" >&2 +eval "exec $job" +`, + ); + const ld = script( + join(bin, "ld.lld"), + `#!/bin/sh +echo "ld.lld $*" >> "$LOG" +[ -n "$FAIL" ] && exit 1 +out=""; prev="" +for a; do [ "$prev" = "-o" ] && out="$a"; prev="$a"; done +printf '%s\\n' "$@" > "$out" +for a; do [ "$a" = "-t" ] && printf '%s\\n' "$@" | grep '^/'; done +exit 0 +`, + ); + const crt = join(lib, "Scrt1.o"); + writeFileSync(crt, "startup object"); + Object.assign(process.env, { LOG: log, LD: ld, CRT: crt, LIB: lib }); + // A directory's identity is its stat: an old time makes a later change visible. + utimesSync(lib, 0, 0); + const cache = new NativeCache(join(root, "cache")); + const toolchain = (target: NativeTargetSpec = LINUX_X64_GNU_TARGET, helper = target) => + ({ + target, + helper: helper.helper, + linker: driver, + }) as unknown as NativeToolchain; + let count = 0; + /** A stage with a program object, the link's paths and its driver arguments. */ + const stage = () => { + const directory = join(stages, `stage-${++count}`); + mkdirSync(directory); + const program = join(directory, "program.o"); + const output = join(directory, "out"); + writeFileSync(program, "program"); + return { + directory, + output, + paths: [program, output], + args: ["-fuse-ld=lld", "-pthread", program, "-lm", "-o", output], + }; + }; + const calls = () => + readFileSync(log, "utf8") + .split("\n") + .filter((line) => line !== ""); + return { root, bin, lib, crt, ld, driver, cache, toolchain, stage, calls }; +} + +test.skipIf(process.platform === "win32")( + "a link records the driver's job once, and later links run ld.lld with their own paths", + () => { + const f = fixture(); + const jobs = LinkerJobs.open(f.cache, f.toolchain(), ["-fuse-ld=lld"], false)!; + expect(jobs).not.toBeNull(); + const first = f.stage(); + const run = jobs.run(first.args, first.paths, first.directory, true); + expect(run).not.toBeNull(); + // The driver only listed the job; ld.lld linked. + expect(f.calls()).toEqual([ + `driver ${first.args.join(" ")} -v -###`, + `ld.lld -pie ${f.crt} -L${f.lib} -pthread ${first.paths[0]} -lm -o ${first.output}`, + ]); + expect(readFileSync(first.output, "utf8")).toContain(first.paths[0]); + const recorded = run!.driverInputs.map((input) => input.path); + expect(recorded).toEqual(expect.arrayContaining([f.ld, f.bin, f.crt, f.lib])); + expect(recorded.some((path) => path.startsWith(first.directory))).toBe(false); + // Another build, with other paths, runs the recorded job without the driver. + const second = f.stage(); + const again = LinkerJobs.open(f.cache, f.toolchain(), ["-fuse-ld=lld"], false)!; + expect(again.run(second.args, second.paths, second.directory, true)).not.toBeNull(); + expect(f.calls().slice(2)).toEqual([ + `ld.lld -pie ${f.crt} -L${f.lib} -pthread ${second.paths[0]} -lm -o ${second.output}`, + ]); + // Other arguments are another job. + const third = f.stage(); + expect( + again.run([...third.args, "-Wl,-t"], third.paths, third.directory, true)?.output, + ).toContain(third.paths[0]); + expect(f.calls().slice(3)[0]).toBe(`driver ${third.args.join(" ")} -Wl,-t -v -###`); + }, +); + +test.skipIf(process.platform === "win32")( + "a driver link records its job from its -v output, and later links run it", + () => { + const f = fixture(); + const open = () => LinkerJobs.open(f.cache, f.toolchain(), ["-fuse-ld=lld"], false)!; + const first = f.stage(); + const traced = [...first.args, "-v", "-Wl,-t"]; + // Without an entry, a link that is not listed runs through the driver. + const link = open().link(first.paths, first.directory); + expect(link.run(traced, false)).toBeNull(); + expect(f.calls()).toEqual([]); + const linked = spawnSync(f.driver, traced, { encoding: "utf8" }); + expect(linked.status).toBe(0); + link.record(traced, linked.stdout + "\n" + linked.stderr); + const second = f.stage(); + const run = open() + .link(second.paths, second.directory) + .run([...second.args, "-v", "-Wl,-t"], false); + expect(run?.output).toContain(second.paths[0]); + expect(run!.driverInputs.map((input) => input.path)).toEqual( + expect.arrayContaining([f.ld, f.bin, f.crt, f.lib]), + ); + expect(f.calls().slice(2)).toEqual([ + `ld.lld -pie ${f.crt} -L${f.lib} -pthread ${second.paths[0]} -lm -o ${second.output} -t`, + ]); + }, +); + +test.skipIf(process.platform === "win32")( + "a changed startup object, library directory or linker sends the link to the driver", + () => { + for (const change of ["crt", "lib", "ld"] as const) { + const f = fixture(); + const open = () => LinkerJobs.open(f.cache, f.toolchain(), ["-fuse-ld=lld"], false)!; + const first = f.stage(); + expect(open().run(first.args, first.paths, first.directory, true)).not.toBeNull(); + if (change === "lib") writeFileSync(join(f.lib, "libm.so"), "new library"); + else { + const path = change === "crt" ? f.crt : f.ld; + const before = statSync(path); + writeFileSync(path + ".new", readFileSync(path)); + chmodSync(path + ".new", 0o755); + utimesSync(path + ".new", before.atime, before.mtime); + renameSync(path + ".new", path); + } + const second = f.stage(); + const count = f.calls().length; + expect(open().run(second.args, second.paths, second.directory, true)).toBeNull(); + expect(f.calls().length).toBe(count); + } + }, +); + +test.skipIf(process.platform === "win32")("a changed driver records the job again", () => { + const f = fixture(); + const first = f.stage(); + expect( + LinkerJobs.open(f.cache, f.toolchain(), ["-fuse-ld=lld"], false)!.run( + first.args, + first.paths, + first.directory, + true, + ), + ).not.toBeNull(); + writeFileSync(f.driver, readFileSync(f.driver, "utf8") + "\n"); + const second = f.stage(); + expect( + LinkerJobs.open(f.cache, f.toolchain(), ["-fuse-ld=lld"], false)!.run( + second.args, + second.paths, + second.directory, + true, + ), + ).not.toBeNull(); + expect(f.calls()[2]).toBe(`driver ${second.args.join(" ")} -v -###`); +}); + +test.skipIf(process.platform === "win32")( + "a job that fails sends this and the build's later links to the driver", + () => { + const f = fixture(); + const first = f.stage(); + const open = () => LinkerJobs.open(f.cache, f.toolchain(), ["-fuse-ld=lld"], false)!; + expect(open().run(first.args, first.paths, first.directory, true)).not.toBeNull(); + process.env["FAIL"] = "1"; + const jobs = open(); + const second = f.stage(); + expect(jobs.run(second.args, second.paths, second.directory, true)).toBeNull(); + const count = f.calls().length; + expect(jobs.run(second.args, second.paths, second.directory, true)).toBeNull(); + expect(f.calls().length).toBe(count); + }, +); + +test.skipIf(process.platform === "win32")( + "a listing without one ld.lld job naming each path once keeps the driver, and is not asked again", + () => { + for (const listing of ["two jobs", "another linker", "a path twice"] as const) { + const f = fixture(); + if (listing === "two jobs") process.env["BROKEN"] = "1"; + if (listing === "another linker") + process.env["LD"] = script(join(f.bin, "ld.bfd"), readFileSync(f.ld, "utf8")); + const open = () => LinkerJobs.open(f.cache, f.toolchain(), ["-fuse-ld=lld"], false)!; + const first = f.stage(); + if (listing === "a path twice") process.env["DOUBLE"] = first.paths[0]; + expect(open().run(first.args, first.paths, first.directory, true)).toBeNull(); + expect(f.calls()).toEqual([`driver ${first.args.join(" ")} -v -###`]); + // The answer is stored: the next build does not list the job again. + expect(open().run(first.args, first.paths, first.directory, true)).toBeNull(); + expect(f.calls().length).toBe(1); + delete process.env["BROKEN"]; + delete process.env["DOUBLE"]; + } + }, +); + +test.skipIf(process.platform === "win32")( + "arguments that name the stage besides the link's paths keep the driver", + () => { + const f = fixture(); + const first = f.stage(); + const jobs = LinkerJobs.open(f.cache, f.toolchain(), ["-fuse-ld=lld"], false)!; + const script = `-Wl,--version-script=${join(first.directory, "exports")}`; + expect(jobs.run([...first.args, script], first.paths, first.directory, true)).toBeNull(); + expect(f.calls()).toEqual([]); + }, +); + +test.skipIf(process.platform === "win32")( + "only host Linux links through a clang driver with lld run the job", + () => { + const f = fixture(); + const lld = ["-fuse-ld=lld"]; + expect(LinkerJobs.open(f.cache, f.toolchain(), lld, false)).not.toBeNull(); + // A cross-target link: the arm64 target with the x64 host's helper. + expect( + LinkerJobs.open( + f.cache, + f.toolchain(LINUX_ARM64_GNU_TARGET, LINUX_X64_GNU_TARGET), + lld, + false, + ), + ).toBeNull(); + expect( + LinkerJobs.open( + f.cache, + f.toolchain(LINUX_X64_GNU_TARGET, LINUX_ARM64_GNU_TARGET), + lld, + false, + ), + ).toBeNull(); + expect(LinkerJobs.open(f.cache, f.toolchain(MACOS_ARM64_TARGET), lld, false)).toBeNull(); + // The driver's default linker, a sanitizer link, no cache. + expect(LinkerJobs.open(f.cache, f.toolchain(), [], false)).toBeNull(); + expect(LinkerJobs.open(f.cache, f.toolchain(), lld, true)).toBeNull(); + expect(LinkerJobs.open(null, f.toolchain(), lld, false)).toBeNull(); + // A linker that is not a clang driver. + const gcc = fixture("gcc"); + expect(LinkerJobs.open(gcc.cache, gcc.toolchain(), lld, false)).toBeNull(); + // An explicit linker, or an environment that names mutable toolchain inputs. + process.env["SCRIPTC_LINKER"] = f.driver; + expect(LinkerJobs.open(f.cache, f.toolchain(), lld, false)).toBeNull(); + delete process.env["SCRIPTC_LINKER"]; + process.env["COMPILER_PATH"] = f.bin; + expect(LinkerJobs.open(f.cache, f.toolchain(), lld, false)).toBeNull(); + }, +); + +const hostTarget = + process.arch === "x64" + ? LINUX_X64_GNU_TARGET + : process.arch === "arm64" + ? LINUX_ARM64_GNU_TARGET + : null; +const hasLld = + process.platform === "linux" && + spawnSync("clang", ["-fuse-ld=lld", "-###", "-x", "c", "/dev/null"], { + encoding: "utf8", + }).stderr?.includes("ld.lld") === true; + +test.skipIf(hostTarget === null || !hasLld)("the job links the executable the driver links", () => { + const root = mkdtempSync(join(tmpdir(), "scriptc-linker-job-clang-")); + directories.push(root); + const stage = join(root, "stage"); + mkdirSync(stage); + const program = join(stage, "program.o"); + const compiled = spawnSync("clang", ["-c", "-x", "c", "-", "-o", program], { + input: '#include \nint main(void) { puts("linked"); return 0; }\n', + }); + expect(compiled.status).toBe(0); + const toolchain = { + target: hostTarget, + helper: hostTarget!.helper, + linker: "clang", + } as unknown as NativeToolchain; + const args = (output: string) => ["-fuse-ld=lld", "-pthread", program, "-lm", "-o", output]; + const viaDriver = join(stage, "driver"); + expect(spawnSync("clang", args(viaDriver)).status).toBe(0); + const cache = new NativeCache(join(root, "cache")); + // Recorded from the -v output of a traced link, and listed with -###. + const traced = (output: string) => [...args(output), "-v", "-Wl,-t"]; + const tracedOutput = join(stage, "traced"); + const linked = spawnSync("clang", traced(tracedOutput), { encoding: "utf8" }); + expect(linked.status).toBe(0); + LinkerJobs.open(cache, toolchain, ["-fuse-ld=lld"], false)! + .link([program, tracedOutput], stage) + .record(traced(tracedOutput), linked.stdout + "\n" + linked.stderr); + for (const [name, list] of [ + ["first", false], + ["second", true], + ["third", true], + ] as const) { + const output = join(stage, name); + const link = LinkerJobs.open(cache, toolchain, ["-fuse-ld=lld"], false)!.link( + [program, output], + stage, + ); + const run = list ? link.run(args(output), true) : link.run(traced(output), false); + expect(run).not.toBeNull(); + expect(readFileSync(output).equals(readFileSync(viaDriver))).toBe(true); + expect(spawnSync(output, { encoding: "utf8" }).stdout).toBe("linked\n"); + } +}); diff --git a/packages/compiler/src/native/linker-job.ts b/packages/compiler/src/native/linker-job.ts new file mode 100644 index 000000000..e1195e4c4 --- /dev/null +++ b/packages/compiler/src/native/linker-job.ts @@ -0,0 +1,286 @@ +import { spawnSync } from "node:child_process"; +import { realpathSync } from "node:fs"; +import { basename, extname, isAbsolute, resolve } from "node:path"; +import { + toolchainEnvironmentCachePolicy, + toolchainEnvironmentFingerprint, +} from "../backend/toolchain-environment.js"; +import type { NativeToolchain } from "./toolchain.js"; +import { contentDigest, type NativeCache } from "./cache.js"; +import { + commandPath, + identity, + type InputIdentity, + stillMatches, + tracePaths, + validIdentity, +} from "./executable-cache.js"; + +/** A recorded linker job. In `job`, a number is the index of one of the link's + * own paths (its inputs and output); `inputs` are the identities of the + * other files and directories the job and the driver's `-v` lines name. */ +interface LinkerJobEntry { + schema: "scriptc.linker-job.v1"; + /** null when the driver's job could not be recorded: the link runs through + * the driver. */ + job: (string | number)[] | null; + inputs: InputIdentity[]; +} + +/** A link that ran as its recorded job: the linker's output, and the + * identities the job depends on besides the link's own paths. */ +export interface LinkerJobRun { + output: string; + driverInputs: InputIdentity[]; +} + +/** One link's use of LinkerJobs, with its own paths and stage. */ +export interface LinkerJobLink { + run(args: string[], list: boolean): LinkerJobRun | null; + record(args: string[], output: string, driverInputs?: InputIdentity[]): void; +} + +const MAX_ENTRY_BYTES = 1024 * 1024; + +function validEntry(value: unknown, paths: number): value is LinkerJobEntry { + if (value === null || typeof value !== "object") return false; + const entry = value as Partial; + if (entry.schema !== "scriptc.linker-job.v1" || !Array.isArray(entry.inputs)) return false; + if (!entry.inputs.every(validIdentity)) return false; + if (entry.job === null) return true; + if (!Array.isArray(entry.job) || typeof entry.job[0] !== "string") return false; + const used = new Set(); + for (const token of entry.job) { + if (typeof token === "string") continue; + if (!Number.isInteger(token) || token < 0 || token >= paths || used.has(token)) return false; + used.add(token); + } + return used.size === paths; +} + +function underRoots(text: string, roots: string[]): boolean { + return roots.some((root) => text.includes(root)); +} + +/** The arguments of one job line as the driver prints them: with `-###` + * each argument is quoted, with `-v` only the program and the arguments that + * contain a space, `"`, `\` or `$`; a quoted argument escapes `"`, `\` and + * `$` with `\`. Null for any other line. */ +function jobArguments(line: string): string[] | null { + const pattern = /\s*(?:"((?:\\.|[^"\\])*)"|([^\s"\\]+))/y; + const args: string[] = []; + let end = 0; + let match: RegExpExecArray | null; + while ((match = pattern.exec(line)) !== null) { + args.push(match[1] === undefined ? match[2]! : match[1].replace(/\\(.)/g, "$1")); + end = pattern.lastIndex; + } + return args.length === 0 || line.slice(end).trim() !== "" ? null : args; +} + +/** + * Linux links through a clang driver with lld. The driver only starts ld.lld + * with a job it derives from its arguments, its environment, its own + * installation and the system's (startup objects, the GCC installation, + * library directories). The job is recorded once per driver identity, + * arguments and environment, with the identities of the files and + * directories it and the driver's `-v` lines name. A later link whose recorded + * paths still have those identities runs ld.lld with the job directly; every + * other link runs through the driver. + */ +export class LinkerJobs { + /** A job run that failed in this build: later links run through the + * driver, which reports the error. */ + private failed = false; + + private constructor( + private readonly cache: NativeCache, + private readonly driver: string, + private readonly driverIdentity: InputIdentity, + ) {} + + /** Null unless the link is a host Linux link through a clang driver with + * `-fuse-ld=lld` (`lld`, the link's lld arguments), outside the sanitizer, + * with a native cache and a toolchain environment that allows complete + * artifacts. */ + static open( + cache: NativeCache | null, + toolchain: NativeToolchain, + lld: readonly string[], + sanitize: boolean, + ): LinkerJobs | null { + const target = toolchain.target; + if ( + cache === null || + sanitize || + target.platform !== "linux" || + // A cross-target link keeps the driver. + target.llvmTriple !== toolchain.helper.defaultTarget || + !lld.includes("-fuse-ld=lld") || + process.env["SCRIPTC_LINKER"] !== undefined || + !toolchainEnvironmentCachePolicy().completeArtifacts + ) + return null; + try { + const driver = commandPath(toolchain.linker); + if (!/^clang(-\d+)?$/.test(basename(realpathSync(driver)))) return null; + return new LinkerJobs(cache, driver, identity(driver)); + } catch { + return null; + } + } + + /** The link whose own inputs and output are `paths`, in `stage`. */ + link(paths: readonly string[], stage: string): LinkerJobLink { + return { + run: (args, list) => this.run(args, paths, stage, list), + record: (args, output, driverInputs) => this.record(args, paths, stage, output, driverInputs), + }; + } + + /** The entry key of the link `args` with its own `paths`, or null when + * the arguments name the stage besides those paths. */ + private key(args: readonly string[], paths: readonly string[], roots: string[]): string | null { + const index = new Map(paths.map((path, i) => [path, i])); + if (index.size !== paths.length || !paths.every((path) => args.includes(path))) return null; + if (args.some((arg) => !index.has(arg) && underRoots(arg, roots))) return null; + const shape = args.map((arg) => { + const at = index.get(arg); + return at === undefined ? arg : { path: at, extension: extname(arg) }; + }); + return contentDigest( + JSON.stringify({ + schema: 1, + driver: this.driverIdentity, + args: shape, + environment: toolchainEnvironmentFingerprint(), + cwd: process.cwd(), + path: process.env["PATH"] ?? null, + }), + ); + } + + /** + * Runs the link that the driver arguments `args` describe as its recorded + * job. `paths` are the link's own inputs and output among `args`, which may + * differ from build to build; `stage` is the directory they are in. Without + * an entry, `list` records the job first with the driver's `-v -###`; + * otherwise the caller links through the driver and passes its `-v` output + * to record(). Null when the link has to run through the driver: no job is + * recorded, a recorded path changed, or the job failed. + */ + run( + args: readonly string[], + paths: readonly string[], + stage: string, + list: boolean, + ): LinkerJobRun | null { + if (this.failed) return null; + try { + const roots = [resolve(stage), realpathSync(stage)]; + const key = this.key(args, paths, roots); + if (key === null) return null; + let entry = this.read(key, paths.length); + if (entry === null && list) { + const result = spawnSync(this.driver, [...args, "-v", "-###"], { + encoding: "utf8", + stdio: "pipe", + }); + if (result.error !== undefined || result.status !== 0 || result.signal !== null) + return null; + entry = this.store(key, result.stdout + "\n" + result.stderr, paths, roots); + } + if (entry === null || entry.job === null || !stillMatches(entry.inputs)) return null; + const job = entry.job.map((token) => (typeof token === "number" ? paths[token]! : token)); + const result = spawnSync(job[0]!, job.slice(1), { + encoding: "utf8", + stdio: "pipe", + }); + if (result.error !== undefined || result.status !== 0 || result.signal !== null) { + this.failed = true; + return null; + } + return { output: result.stdout + "\n" + result.stderr, driverInputs: entry.inputs }; + } catch { + return null; + } + } + + private read(key: string, paths: number): LinkerJobEntry | null { + const bytes = this.cache.read("linker-job", key, MAX_ENTRY_BYTES); + if (bytes === null) return null; + try { + const entry: unknown = JSON.parse(bytes.toString("utf8")); + return validEntry(entry, paths) ? entry : null; + } catch { + return null; + } + } + + /** Records the job of the link `args` from `output`, the `-v` output of + * the driver's successful link, unless an entry exists. `driverInputs` + * are the identities of the paths `output` names, when the caller has + * them. */ + record( + args: readonly string[], + paths: readonly string[], + stage: string, + output: string, + driverInputs?: InputIdentity[], + ): void { + try { + const roots = [resolve(stage), realpathSync(stage)]; + const key = this.key(args, paths, roots); + if (key !== null && this.read(key, paths.length) === null) + this.store(key, output, paths, roots, driverInputs); + } catch { + /* The next link records it. */ + } + } + + /** Stores the entry for `output`, the driver's `-v` lines and job. */ + private store( + key: string, + output: string, + paths: readonly string[], + roots: string[], + driverInputs?: InputIdentity[], + ): LinkerJobEntry { + const lines = output.split(/\r?\n/).filter((line) => line.startsWith(' "')); + const job = lines.length === 1 ? this.jobOf(jobArguments(lines[0]!), paths, roots) : null; + const entry: LinkerJobEntry = { + schema: "scriptc.linker-job.v1", + job, + inputs: job === null ? [] : (driverInputs ?? tracePaths(output, true, roots).map(identity)), + }; + this.cache.write("linker-job", key, JSON.stringify(entry)); + return entry; + } + + /** `job` with each of `paths` as its index: null unless the job runs + * ld.lld and names each of the paths once, as one whole argument, and names + * nothing else in the stage. */ + private jobOf( + job: string[] | null, + paths: readonly string[], + roots: string[], + ): (string | number)[] | null { + if (job === null || job.length === 0) return null; + if (!isAbsolute(job[0]!) || basename(job[0]!) !== "ld.lld") return null; + const index = new Map(paths.map((path, i) => [path, i])); + const used = new Set(); + const tokens: (string | number)[] = []; + for (const arg of job) { + const at = index.get(arg); + if (at === undefined) { + if (underRoots(arg, roots)) return null; + tokens.push(arg); + } else { + if (used.has(at)) return null; + used.add(at); + tokens.push(at); + } + } + return used.size === paths.length ? tokens : null; + } +} diff --git a/packages/compiler/src/native/prepare.test.ts b/packages/compiler/src/native/prepare.test.ts index 112484f4a..07698820e 100644 --- a/packages/compiler/src/native/prepare.test.ts +++ b/packages/compiler/src/native/prepare.test.ts @@ -1,8 +1,12 @@ import { mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { afterEach, expect, test } from "vitest"; -import { nativeCodegenTarget } from "../backend/targets.js"; +import { afterEach, expect, test, vi } from "vitest"; +import { + LINUX_ARM64_GNU_TARGET, + nativeCodegenTarget, + WASM32_WASI_TARGET, +} from "../backend/targets.js"; import { runFrontend, type FrontendFactory } from "../frontend/pipeline.js"; import { loadProgram } from "../frontend/program-node.js"; import { ts7Executable } from "../frontend/ts7/rpc-api.js"; @@ -19,6 +23,7 @@ function text(input: ReturnType["prepare"]>) { const scratch: string[] = []; afterEach(() => { + vi.unstubAllEnvs(); for (const path of scratch.splice(0)) rmSync(path, { recursive: true, force: true }); }); @@ -66,7 +71,7 @@ function fixture() { loads++; return runFrontend(path, loadProgram, npmStatic, externalTypes); }; - const prepare = (extra: { emitIr?: boolean } = {}) => + const prepare = (extra: { emitIr?: boolean; optimization?: "release" | "dev" } = {}) => prepareNativeExecutable(entry, { ...options, ...extra }, null, toolchain, frontend, cache); return { root, entry, dependency, cache, options, prepare, loads: () => loads }; } @@ -119,6 +124,38 @@ test("comment and whitespace edits reuse the frontend output a fresh build repro expect(edited.ok && edited.llvm.join("")).not.toBe(first.llvm.join("")); }); +test("a dev build reuses its frontend output after edits that keep every token's position", () => { + const f = fixture(); + const dev = { optimization: "dev" as const }; + const first = f.prepare(dev); + expect(first.ok, JSON.stringify(first)).toBe(true); + if (!first.ok) return; + // Debug information records lines and columns: a comment after the last + // token and trailing whitespace keep them. + const appended = 'export const value = "first"; \n// The exported value.\n'; + writeFileSync(f.dependency, appended); + const reused = f.prepare(dev); + expect(f.loads()).toBe(1); + expect(reused.ok).toBe(true); + if (!reused.ok) return; + expect(reused.llvm.join("")).toBe(first.llvm.join("")); + expect(new Map(reused.sources).get(f.dependency)).toBe(appended); + const fresh = prepareNativeExecutable( + f.entry, + { ...f.options, ...dev }, + null, + fixtureToolchain(), + (path, npmStatic, externalTypes) => runFrontend(path, loadProgram, npmStatic, externalTypes), + new NativeCache(join(f.root, "fresh-cache")), + ); + expect(fresh.ok && fresh.llvm.join("")).toBe(first.llvm.join("")); + // A comment that moves the token after it is rebuilt. + writeFileSync(f.dependency, '// The exported value.\nexport const value = "first";\n'); + const moved = f.prepare(dev); + expect(f.loads()).toBe(2); + expect(moved.ok && moved.llvm.join("")).not.toBe(first.llvm.join("")); +}); + test("a comment edit inside source text the output carries is rebuilt", () => { const f = fixture(); // A failing assert(value) reports its call's source text. @@ -161,6 +198,30 @@ test("structurally invalid cached features cause a rebuild even with a valid dig expect(f.loads()).toBe(2); }); +test("timing, cache placement and emission worker variables keep the frontend output", () => { + for (const name of [ + "SCRIPTC_CACHE_DIR", + "SCRIPTC_CACHE_MAX_MB", + "SCRIPTC_EMIT_WORKERS", + "SCRIPTC_EMIT_WORKER_FUNCTIONS", + "SCRIPTC_TIMING", + "SCRIPTC_TEST_FRONTEND_KEY", + ]) + vi.stubEnv(name, undefined); + const f = fixture(); + expect(f.prepare().ok).toBe(true); + vi.stubEnv("SCRIPTC_CACHE_DIR", f.cache.root); + vi.stubEnv("SCRIPTC_CACHE_MAX_MB", "8192"); + vi.stubEnv("SCRIPTC_EMIT_WORKERS", "1"); + vi.stubEnv("SCRIPTC_EMIT_WORKER_FUNCTIONS", "100"); + vi.stubEnv("SCRIPTC_TIMING", "0"); + expect(f.prepare().ok).toBe(true); + expect(f.loads()).toBe(1); + vi.stubEnv("SCRIPTC_TEST_FRONTEND_KEY", "1"); + expect(f.prepare().ok).toBe(true); + expect(f.loads()).toBe(2); +}); + test("serialized IR travels as bounded parts through the frontend cache", () => { const f = fixture(); const first = f.prepare({ emitIr: true }); @@ -176,8 +237,31 @@ test("serialized IR travels as bounded parts through the frontend cache", () => const key = readdirSync(family).find((name) => /^[0-9a-f]{64}$/.test(name))!; const payload = JSON.parse(readFileSync(join(family, key), "utf8")); expect(payload.schema).toBe("scriptc.native-frontend.v3"); - expect(payload.input.llvm).toBe(""); + // The placeholders keep NativeExecutableInput's member types: the native + // compiler checks the parsed entry against them when it reads it back. + expect(payload.input.llvm).toEqual([]); expect(payload.input.ir).toBeNull(); expect(payload.llvm).toBe(first.llvm.join("")); expect(payload.ir).toEqual(first.ir); }); + +test("an x86-64 level is refused for targets that are not x86-64 before the frontend runs", () => { + const f = fixture(); + for (const target of [LINUX_ARM64_GNU_TARGET, WASM32_WASI_TARGET]) { + const result = prepareNativeExecutable( + f.entry, + { ...f.options, targetCpu: "x86-64-v3" }, + null, + { ...fixtureToolchain(), target }, + () => { + throw new Error("the frontend ran"); + }, + null, + ); + expect(result.ok).toBe(false); + if (result.ok) continue; + expect(result.diagnostics.map((d) => [d.code, d.message])).toEqual([ + ["SC3002", "--target-cpu=x86-64-v3 requires an x86-64 target"], + ]); + } +}); diff --git a/packages/compiler/src/native/prepare.ts b/packages/compiler/src/native/prepare.ts index f7f399f80..dc6cdafa3 100644 --- a/packages/compiler/src/native/prepare.ts +++ b/packages/compiler/src/native/prepare.ts @@ -1,6 +1,8 @@ -import type { ScrDiagnostic } from "../diagnostics/diagnostic.js"; +import { targetCpuField, targetCpuTargetRefusal } from "../backend/target-cpu.js"; +import { nativeCodegenDiag, type ScrDiagnostic } from "../diagnostics/diagnostic.js"; import type { CompilationTiming } from "../timing.js"; import { readFileSync } from "node:fs"; +import { availableParallelism } from "node:os"; import { basename, dirname, join, resolve } from "node:path"; import type { CompileFailure, @@ -30,7 +32,10 @@ import { import { compilerOwnedFileRoots } from "../frontend/dts-paths.js"; import { provenanceSources } from "../frontend/provenance-registry.js"; import { processFork } from "../frontend/process-fork.js"; -import { tokenEquivalentSource } from "../frontend/token-equivalence.js"; +import { + tokenEquivalentSource, + tokenPositionEquivalentSource, +} from "../frontend/token-equivalence.js"; import { llvmBytes, wtf8Bytes } from "../backend/literals.js"; import { prepareLibrary } from "../library/prepare.js"; import type { LibraryProfile } from "../library/library-profile.js"; @@ -59,7 +64,7 @@ export interface NativeExecutableInput { /** The two large artifacts follow the input metadata as separate members so * an entry can be written as parts instead of one JSON string; the stored - * input carries empty placeholders (`""`, `null`) for them. */ + * input carries empty placeholders (`[]`, `null`) for them. */ interface FrontendCacheEntry { schema: "scriptc.native-frontend.v3"; probes: FrontendInputSnapshot; @@ -78,7 +83,7 @@ function* frontendCacheParts( const metadata = JSON.stringify({ schema: "scriptc.native-frontend.v3", probes, - input: { ...input, llvm: "", ir: null }, + input: { ...input, llvm: [], ir: null }, }); yield metadata.slice(0, metadata.length - 1) + ',"llvm":"'; for (const part of joinedParts(input.llvm)) @@ -141,9 +146,11 @@ function readFrontendCache( /** The stale entry's input when every changed source is token-equivalent to * the text the entry was built from (edits to whitespace and to comments * that cannot affect compilation; see token-equivalence.ts). Equivalent - * sources lower to the same IR apart from source locations, which only dev - * builds' debug information and the serialized IR carry. Diagnostics carry - * locations too, so an entry that reported warnings is rebuilt. A changed + * sources lower to the same IR apart from source locations, which only the + * debug information of dev builds and the serialized IR carry. A build + * with debug information accepts only edits that keep every token's line + * and column, the positions the debug information records. Diagnostics + * carry locations too, so an entry that reported warnings is rebuilt. A changed * source whose raw text or line positions the frontend read (call-site text * in messages, comptime callbacks, test locations) is rebuilt as well: * frontendInputsSemanticallyMatch refuses its `text` probe. */ @@ -153,17 +160,17 @@ function equivalentFrontendInput( exclusions: FrontendInputExclusions, ): { input: NativeExecutableInput; probes: FrontendInputSnapshot } | null { if ( - options.optimization === "dev" || options.sanitize === true || options.emitIr === true || stale.ir !== null || (stale.input.warnings?.length ?? 0) > 0 ) return null; + const debugInfo = options.optimization === "dev" && !options.strip; const match = frontendInputsSemanticallyMatch( stale.probes, new Map(stale.input.sources), - tokenEquivalentSource, + debugInfo ? tokenPositionEquivalentSource : tokenEquivalentSource, exclusions, ); if (match === null || match.changed.length === 0) return null; @@ -204,6 +211,24 @@ function implementationIdentity(toolchain: NativeToolchain): string { ); } +/** SCRIPTC_* variables that report on a build, place its cache or set + * backend emission parallelism. The frontend's output does not depend on + * them, so they are left out of its cache key. */ +const FRONTEND_INDEPENDENT_VARIABLES = new Set([ + "SCRIPTC_CACHE_DIR", + "SCRIPTC_CACHE_MAX_MB", + "SCRIPTC_EMIT_WORKERS", + "SCRIPTC_EMIT_WORKER_FUNCTIONS", + "SCRIPTC_TIMING", +]); + +/** The SCRIPTC_* variables a frontend cache entry depends on, sorted by name. */ +function frontendEnvironment(): [string, string | undefined][] { + return Object.entries(process.env) + .filter(([name]) => name.startsWith("SCRIPTC_") && !FRONTEND_INDEPENDENT_VARIABLES.has(name)) + .sort(([a], [b]) => a.localeCompare(b)); +} + function validFeatures(value: unknown): value is NativeLinkFeatures { if (value === null || typeof value !== "object" || Array.isArray(value)) return false; const features = value as Record; @@ -239,6 +264,7 @@ function validFeatures(value: unknown): value is NativeLinkFeatures { "dgram", "dnsPromises", "dgramDyn", + "textCodecDyn", "watch", "foreignFfi", "nodeTest", @@ -249,6 +275,7 @@ function validFeatures(value: unknown): value is NativeLinkFeatures { "dh", "pki", "ed25519", + "x509", "webcrypto", "perfLoop", ].every((name) => typeof features[name] === "boolean"); @@ -315,6 +342,17 @@ export function prepareNativeExecutable( timing: CompilationTiming = () => {}, ): NativeExecutableInput | CompileFailure { const outputKind = options.outputKind ?? "exe"; + const targetCpuRefusal = targetCpuTargetRefusal( + options.targetCpu, + toolchain.target.platform, + toolchain.target.architecture, + ); + if (targetCpuRefusal !== null) + return { + ok: false, + diagnostics: [nativeCodegenDiag("SC3002", targetCpuRefusal, entry)], + sourceTexts: new Map(), + }; const stem = basename(entry).replace(/\.(ts|mts|cts|js|mjs|cjs)$/, ""); const outputPaths = [ resolve(options.outPath), @@ -343,9 +381,7 @@ export function prepareNativeExecutable( implementation: implementationIdentity(toolchain), target: toolchain.target.name, declarations: toolchain.declarationsRoot ?? null, - environment: Object.entries(process.env) - .filter(([name]) => name.startsWith("SCRIPTC_")) - .sort(([a], [b]) => a.localeCompare(b)), + environment: frontendEnvironment(), }), ); const cached = readFrontendCache(cache, key, exclusions); @@ -389,12 +425,20 @@ export function prepareNativeExecutable( pointerBits: toolchain.target.pointerBits, wasi: toolchain.target.platform === "wasi", runtimeAbiMarker: outputKind === "obj" || outputKind === "exe", + // As for the Node-hosted build: executables and their textual + // LLVM and assembly, not objects. + unwindAbi: + (outputKind === "exe" || outputKind === "llvm" || outputKind === "asm") && + toolchain.target.platform === "linux", objectAudit: options.sanitize === true, + sanitizeAttributes: options.sanitize === true, lowRegion: lowRegionTarget(toolchain.target.platform, options.sanitize === true), functionWorkers: emitWorkers(), forkPrimitives: emitForkPrimitives(), ...inlineRcOption(options.optimization, options.sanitize === true), ...(options.detachedExit === true && outputKind === "exe" ? { detachedExit: true } : {}), + ...targetCpuField(options.targetCpu), + ...(options.cpuDispatch === true && outputKind === "exe" ? { cpuDispatch: true } : {}), ...(options.optimization === "dev" && !options.strip ? { debugSources: prepared.sourceTexts } : {}), @@ -467,9 +511,7 @@ export function prepareNativeLibrary( implementation: implementationIdentity(toolchain), target: toolchain.target.name, declarations: toolchain.declarationsRoot ?? null, - environment: Object.entries(process.env) - .filter(([name]) => name.startsWith("SCRIPTC_")) - .sort(([a], [b]) => a.localeCompare(b)), + environment: frontendEnvironment(), }), ); const cached = readFrontendCache(cache, key, exclusions); @@ -505,6 +547,7 @@ export function prepareNativeLibrary( pointerBits: toolchain.target.pointerBits, wasi: toolchain.target.platform === "wasi", objectAudit: options.sanitize === true, + sanitizeAttributes: options.sanitize === true, ...inlineRcOption(profile.optimization, options.sanitize === true), }); timing("llvm-emit"); @@ -532,10 +575,12 @@ export function prepareNativeLibrary( return input; } -/** Worker processes that emit function bodies at a time - * (SCRIPTC_EMIT_WORKERS; 0 or 1 emits in this process). */ +/** Worker processes that emit function bodies at a time: up to eight, one + * per available core (SCRIPTC_EMIT_WORKERS sets the count; 0 or 1 emits in + * this process). */ function emitWorkers(): number { - const value = Number(process.env["SCRIPTC_EMIT_WORKERS"] ?? "4"); + const configured = process.env["SCRIPTC_EMIT_WORKERS"]; + const value = configured === undefined ? Math.min(8, availableParallelism()) : Number(configured); return Number.isFinite(value) && value > 1 ? Math.floor(value) : 1; } diff --git a/packages/compiler/src/native/sanitizer.test.ts b/packages/compiler/src/native/sanitizer.test.ts index f54b8cfb0..850e21c58 100644 --- a/packages/compiler/src/native/sanitizer.test.ts +++ b/packages/compiler/src/native/sanitizer.test.ts @@ -8,6 +8,79 @@ import type { RuntimePackManifest } from "../backend/runtime-pack-core.js"; import { NATIVE_TARGETS } from "../backend/targets.js"; import { buildSanitizedRuntime } from "./sanitizer.js"; +/** Every runtime feature off except regex. */ +const features = { + workers: false, + youngNursery: false, + eventTarget: false, + wasm: false, + dynTier: false, + streamDyn: false, + handleProps: false, + declAdopt: false, + typeOrigins: false, + dynForAwait: false, + eventsOnceSignal: false, + mime: false, + utilTiming: false, + dynBuffer: false, + builtinValues: false, + computedRequire: false, + warningReplace: false, + stdioExtended: false, + nodeIsland: false, + brotli: false, + moduleNamespace: false, + regex: true, + copying: false, + textDecoderLegacy: false, + fileHandle: false, + fetch: false, + zlib: false, + assert: false, + inspect: false, + dynInvoke: false, + dc: false, + dynAsync: false, + events: false, + emitter: false, + symbol: false, + bigint: false, + searchParams: false, + qs: false, + utilPromisify: false, + dynBuiltinClass: false, + dynBuiltinStream: false, + parseArgs: false, + stream: false, + net: false, + http: false, + http2: false, + dgram: false, + dnsPromises: false, + dgramDyn: false, + textCodecDyn: false, + watch: false, + foreignFfi: false, + nodeTest: false, + tls: false, + tlsCa: false, + cipher: false, + ecdh: false, + dh: false, + pki: false, + ed25519: false, + x509: false, + webcrypto: false, + perfLoop: false, + nativeFetch: false, + netEffective: false, + httpEffective: false, + tlsEffective: false, + tlsCaEffective: false, + zlibEffective: false, +}; + test.skipIf(process.platform !== "linux" && process.platform !== "darwin")( "sanitized vendor archives compile with host libc declarations", () => { @@ -64,6 +137,7 @@ test.skipIf(process.platform !== "linux" && process.platform !== "darwin")( { root: directory, manifest, + licenses: manifest.licenses, packageText: "", manifestText: "", flavor: "dev", @@ -71,69 +145,7 @@ test.skipIf(process.platform !== "linux" && process.platform !== "darwin")( runtime: [], archives: [artifact], systemLibraries: [], - features: { - workers: false, - youngNursery: false, - eventTarget: false, - wasm: false, - dynTier: false, - streamDyn: false, - handleProps: false, - declAdopt: false, - dynForAwait: false, - eventsOnceSignal: false, - dynBuffer: false, - builtinValues: false, - warningReplace: false, - stdioExtended: false, - nodeIsland: false, - regex: true, - copying: false, - textDecoderLegacy: false, - fileHandle: false, - fetch: false, - zlib: false, - assert: false, - inspect: false, - dynInvoke: false, - dc: false, - dynAsync: false, - events: false, - emitter: false, - symbol: false, - bigint: false, - searchParams: false, - qs: false, - utilPromisify: false, - dynBuiltinClass: false, - dynBuiltinStream: false, - parseArgs: false, - stream: false, - net: false, - http: false, - http2: false, - dgram: false, - dnsPromises: false, - dgramDyn: false, - watch: false, - foreignFfi: false, - nodeTest: false, - tls: false, - tlsCa: false, - cipher: false, - ecdh: false, - dh: false, - pki: false, - ed25519: false, - webcrypto: false, - perfLoop: false, - nativeFetch: false, - netEffective: false, - httpEffective: false, - tlsEffective: false, - tlsCaEffective: false, - zlibEffective: false, - }, + features, }, }, directory, @@ -152,3 +164,89 @@ test.skipIf(process.platform !== "linux" && process.platform !== "darwin")( } }, ); + +test.skipIf(process.platform !== "linux" && process.platform !== "darwin")( + "variants that share an artifact file compile once, as the selected variant", + () => { + const target = NATIVE_TARGETS.find( + (item) => + item.platform === process.platform && + item.architecture === process.arch && + !item.name.endsWith("-musl"), + ); + if (target === undefined) throw new Error("unsupported sanitizer test host"); + const runtimeSourceRoot = fileURLToPath(new URL("../../../runtime", import.meta.url)); + const { version } = JSON.parse( + readFileSync(join(runtimeSourceRoot, "package.json"), "utf8"), + ) as { version: string }; + const directory = mkdtempSync("/tmp/scriptc-sanitized-shared-"); + // The pack stores the identical objects of both variants once. + const shared = { + path: "artifacts/dev/runtime/default/scr_hash.o", + sha256: "0".repeat(64), + size: 0, + }; + const variants = [ + { ...shared, id: "default", when: { workers: false }, defines: [] }, + { ...shared, id: "default-workers", when: { workers: true }, defines: ["SCR_WORKERS"] }, + ]; + const manifest: RuntimePackManifest = { + schema: "scriptc.runtime-pack.v1", + format: 1, + package: target.runtimePackPackage, + version, + target: { + name: target.name, + llvm_triple: target.llvmTriple, + architecture: target.architecture, + object_format: target.objectFormat, + minimum_os: target.minimumOs, + }, + runtime_abi: { version: RUNTIME_ABI_VERSION, marker: RUNTIME_ABI_MARKER }, + compiler: { command: "clang", identity: "test", target: target.llvmTriple }, + macros: { executable: [], excluded: [], sanitizer: "external-toolchain-required" }, + flavors: { + dev: { + optimization: "-O0", + runtime_units: [{ source: "scr_hash.c", predicate: true, variants }], + }, + }, + archives: [], + system_libraries: [], + licenses: [], + }; + try { + const built = buildSanitizedRuntime( + { + compilerVersion: version, + target, + helper: target.helper, + runtimeSourceRoot, + ts7Executable: "unused", + helperExecutable: "unused", + helperPackageRoot: "unused", + runtimePackRoot: "unused", + linker: "unused", + linkerArgs: [], + dsymutil: "unused", + }, + { + root: directory, + manifest, + licenses: manifest.licenses, + packageText: "", + manifestText: "", + flavor: "dev", + selected: { runtime: [variants[0]!], archives: [], systemLibraries: [], features }, + }, + directory, + "executable", + null, + ); + expect(built.runtimeObjects).toEqual([join(directory, "scr_hash.o")]); + expect(statSync(join(directory, "scr_hash.o")).size).toBeGreaterThan(0); + } finally { + rmSync(directory, { recursive: true, force: true }); + } + }, +); diff --git a/packages/compiler/src/native/sanitizer.ts b/packages/compiler/src/native/sanitizer.ts index 6c1ff1e35..abc15d043 100644 --- a/packages/compiler/src/native/sanitizer.ts +++ b/packages/compiler/src/native/sanitizer.ts @@ -4,8 +4,16 @@ import { mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs"; import { basename, join } from "node:path"; import { NativeCodegenError } from "../backend/native-codegen-core.js"; import { requireNativeArtifact, runNativeTool } from "../backend/native-tools.js"; -import type { NativeRuntimePack, NativeRuntimeSelection } from "../backend/runtime-pack-native.js"; -import { runtimePackFlavorKey, type RuntimePackMode } from "../backend/runtime-pack-core.js"; +import { + runtimePackManifest, + type NativeRuntimePack, + type NativeRuntimeSelection, +} from "../backend/runtime-pack-native.js"; +import { + runtimePackFlavorKey, + selectedRuntimeUnits, + type RuntimePackMode, +} from "../backend/runtime-pack-core.js"; import { LRE_SOURCES, ZLIB_SOURCES } from "../backend/vendor-inputs.js"; import { contentDigest, type NativeCache } from "./cache.js"; import type { NativeToolchain } from "./toolchain.js"; @@ -94,6 +102,7 @@ export function buildSanitizedRuntime( const quickjs = join(vendor, "quickjs-ng"); const zlib = join(vendor, "zlib"); const mbedtls = join(vendor, "mbedtls"); + const brotli = join(vendor, "brotli", "c"); const cacheIdentity = contentDigest( JSON.stringify({ schema: 1, @@ -118,10 +127,10 @@ export function buildSanitizedRuntime( } return output; }; - const key = runtimePackFlavorKey(selection.manifest, selection.flavor, mode); - const flavor = selection.manifest.flavors[key]; + const manifest = runtimePackManifest(selection); + const key = runtimePackFlavorKey(manifest, selection.flavor, mode); + const flavor = manifest.flavors[key]; if (flavor === undefined) throw new NativeCodegenError("SC3003", `runtime pack lacks ${key}`); - const wanted = new Set(selection.selected.runtime.map((artifact) => artifact.path)); const common = [ "-std=c11", "-pthread", @@ -137,20 +146,18 @@ export function buildSanitizedRuntime( zlib, "-I", join(mbedtls, "include"), + "-I", + join(brotli, "include"), ...(toolchain.target.platform === "linux" ? ["-ffunction-sections", "-fdata-sections"] : []), ]; - const runtimeObjects: string[] = []; - for (const unit of flavor.runtime_units) { - for (const variant of unit.variants) { - if (wanted.has(variant.path)) - runtimeObjects.push( - compile(join(src, unit.source), unit.source.replace(/\.c$/, ".o"), [ - ...common, - ...variant.defines.map((define) => "-D" + define), - ]), - ); - } - } + // The units and variants the pack selection chose, compiled from source. + const runtimeObjects = selectedRuntimeUnits(flavor, selection.selected.features).map( + ({ source, variant }) => + compile(join(src, source), source.replace(/\.c$/, ".o"), [ + ...common, + ...variant.defines.map((define) => "-D" + define), + ]), + ); const archives: string[] = []; for (const archive of selection.selected.archives) { const name = basename(archive.path) @@ -173,9 +180,23 @@ export function buildSanitizedRuntime( .filter((file) => file.endsWith(".c") && !file.startsWith(".")) .sort(); args = ["-std=c11", "-I", join(mbedtls, "include"), "-I", directory]; + } else if (name === "brotli") { + directory = brotli; + sources = ["common", "dec", "enc"] + .flatMap((dir) => + readdirSync(join(brotli, dir)) + .filter((file) => file.endsWith(".c") && !file.startsWith(".")) + .map((file) => `${dir}/${file}`), + ) + .sort(); + args = ["-std=c11", "-I", join(brotli, "include")]; } else throw new NativeCodegenError("SC3003", `unsupported runtime vendor archive: ${name}`); const objects = sources.map((source) => - compile(join(directory, source), name + "-" + source.replace(/\.c$/, ".o"), args), + compile( + join(directory, source), + name + "-" + source.replaceAll("/", "-").replace(/\.c$/, ".o"), + args, + ), ); const output = join(stage, "libscriptc-" + name + ".a"); runNativeTool( diff --git a/packages/compiler/src/native/toolchain.test.ts b/packages/compiler/src/native/toolchain.test.ts index ad4570c0c..0acd4a4f5 100644 --- a/packages/compiler/src/native/toolchain.test.ts +++ b/packages/compiler/src/native/toolchain.test.ts @@ -54,6 +54,40 @@ test("native toolchain installation paths relocate with the manifest", () => { } }); +test("prebuilt lib diagnostics apply while the checker is the distribution's tsc-ts", () => { + const root = realpathSync( + mkdtempSync(join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-native-tools-")), + ); + const path = join(root, "toolchain.json"); + const manifest: NativeToolchainManifest = { + schema: "scriptc.native-toolchain.v1", + compiler_version: "1.2.3", + target: "macos-arm64", + ts7: "typescript/tsc", + tsc_ts: "tsc-ts/lib/tsc-ts", + llvm_package: "llvm", + runtime_pack: "runtime", + linker: "tools/linker", + linker_args: [], + dsymutil: "dsymutil", + lib_diagnostics: "declarations/lib-diagnostics.json", + }; + try { + writeFileSync(path, JSON.stringify(manifest)); + expect(loadNativeToolchain(path).prebuiltLibDiagnostics).toBe( + join(root, "declarations/lib-diagnostics.json"), + ); + // tsgo checks without them. + expect( + loadNativeToolchain(path, { SCRIPTC_CHECKER: "tsgo" }).prebuiltLibDiagnostics, + ).toBeUndefined(); + writeFileSync(path, JSON.stringify({ ...manifest, lib_diagnostics: "" })); + expect(() => loadNativeToolchain(path)).toThrow(); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + test("native toolchain assets resolve from the physical manifest through package links", () => { const root = realpathSync( mkdtempSync(join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-linked-tools-")), @@ -242,3 +276,66 @@ test("a manifest without tsc_ts takes the host's tsc-ts executable from the inst rmSync(root, { recursive: true, force: true }); } }); + +test.each(["nested", "hoisted", "pnpm"])( + "an npm platform package resolves its LLVM helper and runtime pack dependencies (%s)", + (layout) => { + const root = realpathSync( + mkdtempSync(join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-dependencies-")), + ); + const modules = + layout === "pnpm" + ? join(root, "node_modules/.pnpm/@scriptc+cli-linux-x64-gnu@1.2.3/node_modules") + : join(root, "node_modules"); + const platform = join(modules, "@scriptc/cli-linux-x64-gnu"); + const dependencies = layout === "nested" ? join(platform, "node_modules") : modules; + const bin = join(platform, "dist/bin"); + const path = join(bin, "scriptc.json"); + const manifest: NativeToolchainManifest = { + schema: "scriptc.native-toolchain.v1", + compiler_version: "1.2.3", + target: "linux-x64-gnu", + ts7: "../lib/typescript/tsc", + tsc_ts: "../lib/tsc-ts/lib/tsc-ts", + linker: "clang", + linker_args: [], + dsymutil: "dsymutil", + }; + const install = (name: string, version = "1.2.3") => { + mkdirSync(join(dependencies, name), { recursive: true }); + writeFileSync(join(dependencies, name, "package.json"), JSON.stringify({ name, version })); + }; + try { + mkdirSync(bin, { recursive: true }); + writeFileSync(path, JSON.stringify(manifest)); + expect(() => loadNativeToolchain(path)).toThrow( + "scriptc requires @scriptc/llvm-linux-x64-gnu@1.2.3, which is not installed beside the native compiler", + ); + install("@scriptc/llvm-linux-x64-gnu"); + install("@scriptc/runtime-linux-x64-gnu", "1.2.2"); + expect(() => loadNativeToolchain(path)).toThrow( + "scriptc requires @scriptc/runtime-linux-x64-gnu@1.2.3, found 1.2.2", + ); + install("@scriptc/runtime-linux-x64-gnu"); + const toolchain = loadNativeToolchain(path); + expect(toolchain.helperPackageRoot).toBe(join(dependencies, "@scriptc/llvm-linux-x64-gnu")); + expect(toolchain.helperExecutable).toBe( + join(dependencies, "@scriptc/llvm-linux-x64-gnu/bin/scriptc-llvm-codegen"), + ); + expect(toolchain.runtimePackRoot).toBe(join(dependencies, "@scriptc/runtime-linux-x64-gnu")); + // Other targets' packs still come from the project. + const project = join(root, "project"); + const wasi = join(project, "node_modules/@scriptc/runtime-wasm32-wasi"); + mkdirSync(wasi, { recursive: true }); + writeFileSync(join(wasi, "package.json"), "{}"); + expect( + loadNativeToolchain(path, { SCRIPTC_TARGET: "wasm32-wasi" }, project).runtimePackRoot, + ).toBe(wasi); + expect( + loadNativeToolchain(path, { SCRIPTC_RUNTIME_PACK: "../explicit" }).runtimePackRoot, + ).toBe(join(platform, "dist/explicit")); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }, +); diff --git a/packages/compiler/src/native/toolchain.ts b/packages/compiler/src/native/toolchain.ts index 5abbb329e..d93cce1af 100644 --- a/packages/compiler/src/native/toolchain.ts +++ b/packages/compiler/src/native/toolchain.ts @@ -30,6 +30,9 @@ export interface NativeToolchain { declarationsRoot?: string; comptimeExecutable?: string; wasiNodeRunner?: string; + /** The prebuilt lib diagnostics the distribution ships, present while the + * checker is the distribution's own tsc-ts (lib-diagnostics.ts). */ + prebuiltLibDiagnostics?: string; } /** Distribution paths are relative to this manifest, so moving an installed @@ -41,8 +44,12 @@ export interface NativeToolchainManifest { ts7: string; /** The tsc-ts executable, the default checker; without it, an installed `tsc-ts` package's. */ tsc_ts?: string; - llvm_package: string; - runtime_pack: string; + /** The LLVM helper package; without it, the host's helper package + * installed beside the compiler (an npm platform package's dependency). */ + llvm_package?: string; + /** The host runtime pack; without it, the host's runtime pack package + * installed beside the compiler. */ + runtime_pack?: string; runtime_sources?: string; linker: string; linker_args: string[]; @@ -54,6 +61,8 @@ export interface NativeToolchainManifest { relocatable_linker?: string; comptime?: string; wasi_node_runner?: string; + /** Answers for the supplied declaration files, computed with `tsc_ts`. */ + lib_diagnostics?: string; } function pathFrom(root: string, value: string): string { @@ -64,9 +73,31 @@ function commandFrom(root: string, value: string): string { return value.includes("/") || value.includes("\\") ? pathFrom(root, value) : value; } +/** A package the npm platform package depends on, as Node resolves it from + * the compiler's directory: nested in the platform package, beside it + * (hoisted, or pnpm's virtual store), or higher up. The version must be the + * compiler's. */ +function compilerDependency(root: string, packageName: string, version: string): string { + const directory = installedPackage(root, packageName); + if (directory === undefined) + throw new Error( + `scriptc requires ${packageName}@${version}, which is not installed beside the native compiler; reinstall scriptc`, + ); + const found = ( + JSON.parse(readFileSync(join(directory, "package.json"), "utf8")) as { + version?: unknown; + } + ).version; + if (found !== version) + throw new Error( + `scriptc requires ${packageName}@${version}, found ${String(found)}; reinstall scriptc`, + ); + return directory; +} + /** Additional targets are ordinary project dependencies. Resolve them when * requested so adding a pack never requires reinstalling the compiler. */ -function installedRuntimePack(start: string, packageName: string): string | undefined { +function installedPackage(start: string, packageName: string): string | undefined { let directory = resolve(start); for (;;) { const candidate = join(directory, "node_modules", packageName); @@ -91,15 +122,7 @@ export function loadNativeToolchain( const value = raw as Record; if (value.schema !== "scriptc.native-toolchain.v1") throw new Error("unsupported native toolchain manifest schema"); - for (const name of [ - "compiler_version", - "target", - "ts7", - "llvm_package", - "runtime_pack", - "linker", - "dsymutil", - ]) { + for (const name of ["compiler_version", "target", "ts7", "linker", "dsymutil"]) { if (typeof value[name] !== "string" || value[name] === "") throw new Error(`native toolchain requires ${name}`); } @@ -114,12 +137,15 @@ export function loadNativeToolchain( const manifest = raw as NativeToolchainManifest; for (const name of [ "tsc_ts", + "llvm_package", + "runtime_pack", "runtime_sources", "declarations", "archiver", "relocatable_linker", "comptime", "wasi_node_runner", + "lib_diagnostics", ]) { if (value[name] !== undefined && (typeof value[name] !== "string" || value[name] === "")) throw new Error(`native toolchain requires a nonempty string for ${name}`); @@ -151,21 +177,35 @@ export function loadNativeToolchain( if (target === null) throw new Error(`unsupported native compiler target: ${env["SCRIPTC_TARGET"]}`); const root = dirname(manifestPath); - const helperPackageRoot = pathFrom(root, env["SCRIPTC_LLVM_PACKAGE"] ?? manifest.llvm_package); + const llvmPackage = + env["SCRIPTC_LLVM_PACKAGE"] ?? + manifest.llvm_package ?? + compilerDependency(root, host.helper.packageName, manifest.compiler_version); + const helperPackageRoot = pathFrom(root, llvmPackage); + // The host pack of an npm platform package is its dependency; a + // distribution archive bundles it beside the compiler. + const hostRuntimePack = + manifest.runtime_pack ?? + (target.name === host.name && env["SCRIPTC_RUNTIME_PACK"] === undefined + ? compilerDependency(root, host.runtimePackPackage, manifest.compiler_version) + : undefined); let runtimePath = manifest.runtime_packs?.find((pack) => pack.target === target.name)?.path ?? (target.name === host.name - ? manifest.runtime_pack - : join(dirname(manifest.runtime_pack), target.runtimePackPackage.replace("@scriptc/", ""))); + ? hostRuntimePack + : hostRuntimePack === undefined + ? undefined + : join(dirname(hostRuntimePack), target.runtimePackPackage.replace("@scriptc/", ""))); if ( target.name !== host.name && env["SCRIPTC_RUNTIME_PACK"] === undefined && - !existsSync(pathFrom(root, runtimePath)) + (runtimePath === undefined || !existsSync(pathFrom(root, runtimePath))) ) { runtimePath = - installedRuntimePack(cwd, target.runtimePackPackage) ?? - installedRuntimePack(root, target.runtimePackPackage) ?? - runtimePath; + installedPackage(cwd, target.runtimePackPackage) ?? + installedPackage(root, target.runtimePackPackage) ?? + runtimePath ?? + join(root, "node_modules", target.runtimePackPackage); } const hostLinker = target.name === host.name && target.defaultLinker === host.defaultLinker; const linker = env["SCRIPTC_LINKER"] ?? (hostLinker ? manifest.linker : target.defaultLinker); @@ -200,7 +240,7 @@ export function loadNativeToolchain( "bin", host.platform === "win32" ? "scriptc-llvm-codegen.exe" : "scriptc-llvm-codegen", ), - runtimePackRoot: pathFrom(root, env["SCRIPTC_RUNTIME_PACK"] ?? runtimePath), + runtimePackRoot: pathFrom(root, env["SCRIPTC_RUNTIME_PACK"] ?? runtimePath!), ...(manifest.runtime_sources === undefined ? {} : { runtimeSourceRoot: pathFrom(root, manifest.runtime_sources) }), @@ -221,5 +261,11 @@ export function loadNativeToolchain( ...(manifest.wasi_node_runner === undefined ? {} : { wasiNodeRunner: pathFrom(root, manifest.wasi_node_runner) }), + // The prebuilt answers are the distribution's tsc-ts's. + ...(manifest.lib_diagnostics === undefined || + manifest.tsc_ts === undefined || + checkerBackend(env) === "tsgo" + ? {} + : { prebuiltLibDiagnostics: pathFrom(root, manifest.lib_diagnostics) }), }; } diff --git a/packages/compiler/src/node-islands/libnode.test.ts b/packages/compiler/src/node-islands/libnode.test.ts index d6ca4bf83..8f375fd08 100644 --- a/packages/compiler/src/node-islands/libnode.test.ts +++ b/packages/compiler/src/node-islands/libnode.test.ts @@ -1,4 +1,12 @@ -import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, + symlinkSync, + writeFileSync, +} from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { describe, expect, test } from "vitest"; @@ -84,6 +92,64 @@ describe("the recorded libnode pack", () => { expect(readFileSync(join(out, "app.libnode/libscr_node_bridge.so"), "utf8")).toBe("bridge"); }); + test("beside refuses to replace the pack it copies", () => { + const out = mkdtempSync(join(tmpdir(), "scriptc-libnode-out-")); + const first = pack("24.22.0"); + libnodeRecord("beside", join(out, "app"), { SCRIPTC_LIBNODE: first }); + // The placed copy as the source of the next beside build. + const placed = join(out, "app.libnode"); + expect(libnodeRecord("beside", join(out, "app"), { SCRIPTC_LIBNODE: placed })).toMatch( + /^--libnode=beside would replace the libnode pack it copies/, + ); + expect(readFileSync(join(placed, "libscr_node_bridge.so"), "utf8")).toBe("bridge"); + }); + + test("beside refuses manifest file names outside the pack", () => { + const dir = pack("24.22.0"); + writeFileSync( + join(dir, "manifest.json"), + JSON.stringify({ nodeVersion: "24.22.0", bridgeAbi: NODE_BRIDGE_ABI, files: { "../x": {} } }), + ); + const out = mkdtempSync(join(tmpdir(), "scriptc-libnode-out-")); + expect(libnodeRecord("beside", join(out, "app"), { SCRIPTC_LIBNODE: dir })).toBe( + `the libnode pack at ${dir} lists a file outside the pack ('../x')`, + ); + expect(existsSync(join(out, "app.libnode"))).toBe(false); + }); + + test("beside refuses manifest file names outside the pack", () => { + for (const name of ["..", ".", "../escaped", "/etc/passwd", "a/b", "a\\b", "x\u0000y"]) { + const dir = pack("24.22.0"); + writeFileSync( + join(dir, "manifest.json"), + JSON.stringify({ + nodeVersion: "24.22.0", + bridgeAbi: NODE_BRIDGE_ABI, + files: { [name]: {} }, + }), + ); + const out = mkdtempSync(join(tmpdir(), "scriptc-libnode-out-")); + expect(libnodeRecord("beside", join(out, "app"), { SCRIPTC_LIBNODE: dir })).toBe( + `the libnode pack at ${dir} lists a file outside the pack ('${name}')`, + ); + expect(existsSync(join(out, "app.libnode"))).toBe(false); + expect(existsSync(join(out, "escaped"))).toBe(false); + } + }); + + test("beside refuses pack files that are symbolic links", () => { + const dir = pack("24.22.0"); + const elsewhere = join(mkdtempSync(join(tmpdir(), "scriptc-libnode-elsewhere-")), "secret"); + writeFileSync(elsewhere, "secret"); + rmSync(join(dir, "libnode.so.137")); + symlinkSync(elsewhere, join(dir, "libnode.so.137")); + const out = mkdtempSync(join(tmpdir(), "scriptc-libnode-out-")); + expect(libnodeRecord("beside", join(out, "app"), { SCRIPTC_LIBNODE: dir })).toBe( + `the libnode pack at ${dir} holds libnode.so.137 as a symbolic link; pack files must be regular files`, + ); + expect(existsSync(join(out, "app.libnode"))).toBe(false); + }); + test("a pack the deployment places beside the executable needs no manifest at build time", () => { expect(libnodeRecord("$ORIGIN/node", "/out/app", {})).toEqual({ libnode: "$ORIGIN/node", diff --git a/packages/compiler/src/node-islands/libnode.ts b/packages/compiler/src/node-islands/libnode.ts index ec242bed5..7d7a38e7d 100644 --- a/packages/compiler/src/node-islands/libnode.ts +++ b/packages/compiler/src/node-islands/libnode.ts @@ -13,8 +13,17 @@ * executable and its Node travel together; a recorded `$ORIGIN/` path is * also the hook for bundling Node into the executable itself later. */ import { createHash } from "node:crypto"; -import { copyFileSync, linkSync, mkdirSync, readFileSync, rmSync, statSync } from "node:fs"; -import { basename, isAbsolute, join, resolve } from "node:path"; +import { + constants, + copyFileSync, + linkSync, + lstatSync, + mkdirSync, + readFileSync, + realpathSync, + rmSync, +} from "node:fs"; +import { basename, isAbsolute, join, resolve, sep } from "node:path"; import { findInstalledLibnodePack, hostLibnodeTarget, libnodePackPackage } from "./libnode-pack.js"; /** The oldest Node.js release Node islands support: synchronous @@ -23,7 +32,7 @@ import { findInstalledLibnodePack, hostLibnodeTarget, libnodePackPackage } from export const NODE_ISLAND_MIN_NODE = "24.13.1"; /** The bridge ABI this compiler's runtime speaks (scr_node_bridge.h). */ -export const NODE_BRIDGE_ABI = 3; +export const NODE_BRIDGE_ABI = 4; /** The prefix of a recorded path relative to the executable's directory. */ export const ORIGIN = "$ORIGIN/"; @@ -68,23 +77,59 @@ function readManifest(dir: string): PackManifest | string { }; } -/** Copies the pack next to the executable (hard links where possible). */ -function placeBeside(source: string, manifest: PackManifest, target: string): void { - rmSync(target, { recursive: true, force: true }); - mkdirSync(target, { recursive: true }); - for (const name of ["manifest.json", "LICENSE", ...Object.keys(manifest.files)]) { - const from = join(source, name); +/** Copies the pack next to the executable (hard links where possible), or + * returns why it cannot: the target would replace the pack itself, the + * manifest names a file outside the pack, or a pack file is not a regular + * file (a symbolic link could point anywhere). Every destination is checked + * to resolve inside the target directory. */ +function placeBeside(source: string, manifest: PackManifest, target: string): string | undefined { + const names = ["manifest.json", "LICENSE", ...Object.keys(manifest.files)]; + const unsafe = names.find( + (name) => + name === "" || name === "." || name === ".." || /[/\\\0]/.test(name) || isAbsolute(name), + ); + if (unsafe !== undefined) + return `the libnode pack at ${source} lists a file outside the pack ('${unsafe}')`; + const from = realpathOr(source); + const to = realpathOr(target); + if (from === to || from.startsWith(to + sep) || to.startsWith(from + sep)) + return `--libnode=beside would replace the libnode pack it copies (${source}); point SCRIPTC_LIBNODE at a pack outside ${target}`; + const present: string[] = []; + for (const name of names) { + let info; try { - statSync(from); + info = lstatSync(join(source, name)); } catch { continue; } + if (!info.isFile()) + return `the libnode pack at ${source} holds ${name} as a ${info.isSymbolicLink() ? "symbolic link" : "non-regular file"}; pack files must be regular files`; + present.push(name); + } + rmSync(target, { recursive: true, force: true }); + mkdirSync(target, { recursive: true }); + const inside = resolve(target) + sep; + for (const name of present) { + const destination = resolve(target, name); + if (!destination.startsWith(inside)) + return `the libnode pack at ${source} lists a file outside the pack ('${name}')`; + const file = join(source, name); try { - linkSync(from, join(target, name)); + linkSync(file, destination); } catch { - copyFileSync(from, join(target, name)); + copyFileSync(file, destination, constants.COPYFILE_EXCL); } } + return undefined; +} + +/** The canonical path of `path`, or `path` resolved when it does not exist. */ +function realpathOr(path: string): string { + try { + return realpathSync(path); + } catch { + return resolve(path); + } } /** What to record for a program that calls into Node, or the reason the @@ -114,17 +159,20 @@ export function libnodeRecord( if (typeof manifest === "string") return manifest; if (!beside) return { libnode: resolve(source), nodeVersion: manifest.nodeVersion }; const name = `${basename(outPath)}.libnode`; - placeBeside(source, manifest, `${outPath}.libnode`); + const refused = placeBeside(source, manifest, `${outPath}.libnode`); + if (refused !== undefined) return refused; return { libnode: `${ORIGIN}${name}`, nodeVersion: manifest.nodeVersion }; } -/** The part of an executable's cache key that names its libnode pack, or - * undefined when neither --libnode nor SCRIPTC_LIBNODE is set. It holds the - * option, the pack directory and a digest of the pack's manifest.json, so - * that a pack upgraded in place (another Node version, bridge ABI or file - * list) gives cached executables a new key instead of their old record. - * An installed @scriptc/libnode- pack needs no part: it versions - * in lockstep with the compiler, whose version every key already holds, so +/** The identity of the libnode pack a build that calls into Node.js + * records, which the program's executable cache entry keeps + * (EarlyExecutableCacheOptions.libnodeIdentity), or undefined when neither + * --libnode nor SCRIPTC_LIBNODE is set. It holds the option, the pack + * directory and a digest of the pack's manifest.json, so that a pack + * upgraded in place (another Node version, bridge ABI or file list) + * invalidates the entry instead of restoring its old record. An installed + * @scriptc/libnode- pack needs no identity: it versions in + * lockstep with the compiler, whose version every key already holds, so * the bootstrap's routed cache keeps serving builds where one is installed. */ export function libnodeCacheKey( option: string | undefined, diff --git a/packages/compiler/src/node-islands/modules.test.ts b/packages/compiler/src/node-islands/modules.test.ts new file mode 100644 index 000000000..63201e1e0 --- /dev/null +++ b/packages/compiler/src/node-islands/modules.test.ts @@ -0,0 +1,215 @@ +import { mkdirSync, mkdtempSync, realpathSync, symlinkSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { describe, expect, test } from "vitest"; +import { + NodeIslandModuleCollector, + nodeIslandManifest, + readNodeIslandModules, + scanModuleSpecifiers, +} from "./modules.js"; + +function project(files: Record): string { + const root = realpathSync(mkdtempSync(join(tmpdir(), "scriptc-island-modules-"))); + for (const [path, text] of Object.entries(files)) { + mkdirSync(dirname(join(root, path)), { recursive: true }); + writeFileSync(join(root, path), text); + } + return root; +} + +describe("module specifiers in JavaScript", () => { + test("string-literal loads in every form", () => { + const scan = scanModuleSpecifiers(` + const a = require("./a"); + const b = require.resolve('./b.json'); + import c from "c"; + import * as d from "d/sub"; + import { e } from "./e.mjs"; + import "./side"; + export * from "./f"; + export { g } from "./g"; + const h = await import("./h.js"); + import.meta.url; + obj.require("x"); + const t = \`\${require("./t")}\`; + `); + expect(scan.specifiers).toEqual([ + "./a", + "./b.json", + "c", + "d/sub", + "./e.mjs", + "./side", + "./f", + "./g", + "./h.js", + "./t", + ]); + expect(scan.computed).toEqual([]); + }); + + test("comments, strings and regular expressions hide nothing and invent nothing", () => { + const scan = scanModuleSpecifiers(` + // require("./commented") + /* import x from "./block" */ + const s = "require('./in-string')"; + const r = /require\\("\\.\\/in-regex"\\)/g; + const q = a / b / c; + const real = require("./real"); + `); + expect(scan.specifiers).toEqual(["./real"]); + expect(scan.computed).toEqual([]); + }); + + test("computed loads are reported", () => { + const forms = (text: string) => scanModuleSpecifiers(text).computed.map((c) => c.form); + expect(forms("require(name)")).toEqual(["require() with a computed specifier"]); + expect(forms("require('./a' + b)")).toEqual(["require() with a computed specifier"]); + expect(forms("import(`./x/${y}`)")).toEqual(["import() with a computed specifier"]); + expect(forms("const r = require; r('./a')")).toEqual(["require as a value"]); + expect(forms("createRequire(import.meta.url)('./a')")).toEqual([ + "a require() from createRequire()", + ]); + expect(forms("function require(x) {} typeof require")).toEqual([]); + }); +}); + +describe("the collected module tree", () => { + test("a relative module and what it loads; missing modules are not shipped", () => { + const root = project({ + "src/main.ts": "", + "src/math.cjs": + "module.exports = require('./util');\ntry { require('./optional'); } catch {}", + "src/util.js": "exports.x = require('../data.json');", + "data.json": "{}", + "src/unused.js": "", + }); + const collector = new NodeIslandModuleCollector(); + expect(collector.add(join(root, "src/main.ts"), "./math.cjs")).toBeNull(); + expect(collector.add(join(root, "src/main.ts"), "./does-not-exist.cjs")).toBeNull(); + expect(collector.add(join(root, "src/main.ts"), "node:fs")).toBeNull(); + const tree = collector.tree()!; + expect(tree.root).toBe(root); + expect(tree.files.map((f) => f.path)).toEqual(["data.json", "src/math.cjs", "src/util.js"]); + expect(NodeIslandModuleCollector.callerPath(tree, join(root, "src/main.ts"))).toBe( + "src/main.ts", + ); + }); + + test("a module of the program ships with the package.json that scopes it", () => { + const root = project({ + "package.json": JSON.stringify({ type: "module" }), + "src/main.ts": "", + "src/esm.js": "export const x = 1;", + }); + const collector = new NodeIslandModuleCollector(); + expect(collector.add(join(root, "src/main.ts"), "./esm.js")).toBeNull(); + expect(collector.tree()!.files.map((f) => f.path)).toEqual(["package.json", "src/esm.js"]); + }); + + test("a computed load in a module of the program is refused", () => { + const root = project({ "main.ts": "", "dyn.cjs": "module.exports = require(process.env.X);" }); + const problem = new NodeIslandModuleCollector().add(join(root, "main.ts"), "./dyn.cjs"); + expect(problem).toBe( + `${join(root, "dyn.cjs")}:1 loads a module through require() with a computed specifier; a module that runs in a Node island loads modules by string-literal specifiers only`, + ); + }); + + test("a package ships whole, with its dependencies, but not its nested node_modules", () => { + const root = project({ + "main.ts": "", + "node_modules/pkg/package.json": JSON.stringify({ dependencies: { dep: "1", absent: "1" } }), + "node_modules/pkg/index.js": "module.exports = require(process.env.ANYTHING_GOES);", + "node_modules/pkg/lib/extra.json": "{}", + "node_modules/pkg/node_modules/.bin/x": "", + "node_modules/dep/package.json": "{}", + "node_modules/dep/index.js": "", + "node_modules/other/index.js": "", + }); + const collector = new NodeIslandModuleCollector(); + expect(collector.add(join(root, "main.ts"), "pkg/lib/extra.json")).toBeNull(); + expect(collector.tree()!.files.map((f) => f.path)).toEqual([ + "node_modules/dep/index.js", + "node_modules/dep/package.json", + "node_modules/pkg/index.js", + "node_modules/pkg/lib/extra.json", + "node_modules/pkg/package.json", + ]); + }); + + test("pnpm's links between package directories stay links inside the tree", () => { + const root = project({ + "app/main.ts": "", + "app/node_modules/.pnpm/a@1/node_modules/a/package.json": JSON.stringify({ + dependencies: { b: "1" }, + }), + "app/node_modules/.pnpm/a@1/node_modules/a/index.js": "", + "app/node_modules/.pnpm/b@1/node_modules/b/index.js": "", + }); + symlinkSync(".pnpm/a@1/node_modules/a", join(root, "app/node_modules/a")); + symlinkSync( + "../../b@1/node_modules/b", + join(root, "app/node_modules/.pnpm/a@1/node_modules/b"), + ); + const collector = new NodeIslandModuleCollector(); + expect(collector.add(join(root, "app/main.ts"), "a")).toBeNull(); + const tree = collector.tree()!; + expect(tree.root).toBe(join(root, "app")); + expect(tree.links).toEqual([ + { path: "node_modules/.pnpm/a@1/node_modules/b", target: "../../b@1/node_modules/b" }, + { path: "node_modules/a", target: ".pnpm/a@1/node_modules/a" }, + ]); + expect(tree.files.map((f) => f.path)).toEqual([ + "node_modules/.pnpm/a@1/node_modules/a/index.js", + "node_modules/.pnpm/a@1/node_modules/a/package.json", + "node_modules/.pnpm/b@1/node_modules/b/index.js", + ]); + }); + + test("a require through a linked directory keeps the link in the tree", () => { + const root = project({ + "src/main.ts": "", + "src/entry.cjs": "module.exports = require('./lib/x.cjs');", + "shared/x.cjs": "exports.x = 1;", + }); + symlinkSync("../shared", join(root, "src/lib")); + const collector = new NodeIslandModuleCollector(); + expect(collector.add(join(root, "src/main.ts"), "./entry.cjs")).toBeNull(); + const tree = collector.tree()!; + expect(tree.links).toEqual([{ path: "src/lib", target: "../shared" }]); + expect(tree.files.map((f) => f.path)).toEqual(["shared/x.cjs", "src/entry.cjs"]); + }); + + test("an absolute specifier names the shipped file relative to its caller", () => { + const root = project({ "src/main.ts": "", "lib/m.cjs": "" }); + const collector = new NodeIslandModuleCollector(); + const target = collector.resolvedPath(join(root, "src/main.ts"), join(root, "lib/m.cjs"))!; + expect(target).toBe(join(root, "lib/m.cjs")); + expect(NodeIslandModuleCollector.relativeSpecifier(join(root, "src/main.ts"), target)).toBe( + "../lib/m.cjs", + ); + expect( + NodeIslandModuleCollector.relativeSpecifier(join(root, "main.ts"), join(root, "m.cjs")), + ).toBe("./m.cjs"); + }); + + test("built-in modules alone ship no tree", () => { + const root = project({ "main.ts": "" }); + const collector = new NodeIslandModuleCollector(); + expect(collector.add(join(root, "main.ts"), "os")).toBeNull(); + expect(collector.tree()).toBeNull(); + expect(NodeIslandModuleCollector.callerPath(null, join(root, "main.ts"))).toBe("main.ts"); + }); + + test("the manifest names relative paths only, and the data is checked against it", () => { + const root = project({ "main.ts": "", "m.cjs": "exports.f = () => 1;" }); + const collector = new NodeIslandModuleCollector(); + collector.add(join(root, "main.ts"), "./m.cjs"); + const tree = collector.tree()!; + expect(nodeIslandManifest(tree)).not.toContain(root); + expect(readNodeIslandModules(tree)).toEqual(new TextEncoder().encode("exports.f = () => 1;")); + writeFileSync(join(root, "m.cjs"), "exports.f = () => 2;"); + expect(readNodeIslandModules(tree)).toBe(`${join(root, "m.cjs")} changed during the build`); + }); +}); diff --git a/packages/compiler/src/node-islands/modules.ts b/packages/compiler/src/node-islands/modules.ts new file mode 100644 index 000000000..e1551c070 --- /dev/null +++ b/packages/compiler/src/node-islands/modules.ts @@ -0,0 +1,659 @@ +/* The island module tree: the JavaScript a program's Node island calls load, + * collected at build time and shipped with the executable, so a deployed + * binary never reads modules from the machine that built it. + * + * Each `call`/`callSync` names its module with a string literal. The build + * resolves it from the calling module as Node's require() does: + * - a relative or absolute specifier names a module of the program: the + * module and every module its string-literal `require()`, `import` and + * `export ... from` reach are collected; a computed specifier in such a + * module is refused, since the build cannot know what it loads; + * - a package specifier names a package: its whole directory is collected + * (computed requires inside packages keep working), and so are its + * `dependencies`, `optionalDependencies` and `peerDependencies`, found as + * Node finds them; + * - a Node built-in module needs nothing, and neither does a module the + * build cannot find: loading it throws MODULE_NOT_FOUND when the call + * runs, as under Node. + * Paths keep their layout relative to one root directory (the deepest + * directory that holds every collected path), with symbolic links between + * package directories (pnpm's layout) kept as links. Only these relative + * paths enter the executable. Every read goes through the frontend input + * tracker, so an edit to an island module invalidates cached builds. */ +import { createHash } from "node:crypto"; +import { mkdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; +import { builtinModules } from "node:module"; +import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path"; +import { + trackedAccessibleEntries, + trackedDirectoryExists, + trackedFileExists, + trackedReadBytes, + trackedReadFile, + trackedRealpath, +} from "../frontend/input-tracker.js"; +import type { IrNodeIslandModules } from "../ir/ir.js"; + +export type NodeIslandModuleTree = IrNodeIslandModules; +type NodeIslandModuleFile = IrNodeIslandModules["files"][number]; +type NodeIslandModuleLink = IrNodeIslandModules["links"][number]; + +const BUILTINS: ReadonlySet = new Set(builtinModules); + +/** Extensions require() tries for an extensionless file, in its order. */ +const FILE_EXTENSIONS = [".js", ".json", ".node"]; + +/** Module files whose imports are traced. */ +const TRACED = /\.(?:[cm]?js|[cm]?ts)$/; + +function isBuiltin(specifier: string): boolean { + return specifier.startsWith("node:") || BUILTINS.has(specifier); +} + +function isPathSpecifier(specifier: string): boolean { + return ( + specifier === "." || + specifier === ".." || + specifier.startsWith("./") || + specifier.startsWith("../") || + isAbsolute(specifier) + ); +} + +function packageName(specifier: string): string { + const parts = specifier.split("/"); + return specifier.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0]!; +} + +/** The file require() loads for `base` (a file, an extension away, or a + * directory with a package.json main or an index file), or null. */ +function resolveFile(base: string): string | null { + if (trackedFileExists(base)) return base; + for (const extension of FILE_EXTENSIONS) + if (trackedFileExists(base + extension)) return base + extension; + if (!trackedDirectoryExists(base)) return null; + const manifest = trackedReadFile(join(base, "package.json")); + if (manifest !== null) { + let main: unknown; + try { + main = (JSON.parse(manifest) as { main?: unknown }).main; + } catch { + main = undefined; + } + if (typeof main === "string" && main !== "") { + const target = join(base, main); + const found = resolveFile(target) ?? resolveFile(join(target, "index")); + if (found !== null) return found; + } + } + for (const extension of FILE_EXTENSIONS) { + const index = join(base, `index${extension}`); + if (trackedFileExists(index)) return index; + } + return null; +} + +/** The directory of package `name` as require() finds it from `fromDir`: + * `node_modules/` in `fromDir` and each ancestor not itself named + * node_modules. */ +function packageDirectory(fromDir: string, name: string): string | null { + for (let dir = fromDir; ;) { + if (basename(dir) !== "node_modules") { + const candidate = join(dir, "node_modules", name); + if (trackedDirectoryExists(candidate)) return candidate; + } + const parent = dirname(dir); + if (parent === dir) return null; + dir = parent; + } +} + +/** The package.json nearest to `dir`, as Node finds a module's package + * scope, or null. */ +function packageScope(dir: string): string | null { + for (let at = dir; ;) { + if (basename(at) === "node_modules") return null; + const candidate = join(at, "package.json"); + if (trackedFileExists(candidate)) return candidate; + const parent = dirname(at); + if (parent === at) return null; + at = parent; + } +} + +function sha256(bytes: Uint8Array): string { + return createHash("sha256").update(bytes).digest("hex"); +} + +/** The deepest directory that contains every path. */ +function commonDirectory(paths: readonly string[]): string { + let common = paths[0]!.split(sep); + for (const path of paths.slice(1)) { + const parts = path.split(sep); + let n = 0; + while (n < common.length && n < parts.length && common[n] === parts[n]) n++; + common = common.slice(0, n); + } + const joined = common.join(sep); + return joined === "" ? sep : joined; +} + +function portable(path: string): string { + return sep === "/" ? path : path.split(sep).join("/"); +} + +export class NodeIslandModuleCollector { + /** Real paths of collected files. */ + private readonly files = new Set(); + /** Link path (its directory real) → real target. */ + private readonly links = new Map(); + private readonly packages = new Set(); + private readonly traced = new Set(); + /** Real paths of the calling modules. */ + private readonly callers = new Set(); + + /** Collects the module `specifier` names from `caller` (a real path). + * Returns null, or the reason the build cannot ship it. */ + add(caller: string, specifier: string): string | null { + this.callers.add(caller); + return this.resolveFrom(caller, specifier); + } + + /** The collected tree, or null when no call loads a file. */ + tree(): NodeIslandModuleTree | null { + if (this.files.size === 0 && this.links.size === 0) return null; + const anchors = [ + ...this.files, + ...this.links.keys(), + ...this.links.values(), + ...[...this.callers].map((caller) => dirname(caller)), + ]; + const root = commonDirectory(anchors); + const files: NodeIslandModuleFile[] = []; + for (const file of [...this.files].sort()) { + const bytes = trackedReadBytes(file); + if (bytes === null) continue; + files.push({ + path: portable(relative(root, file)), + size: bytes.length, + sha256: sha256(bytes), + }); + } + const links: NodeIslandModuleLink[] = [...this.links] + .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)) + .map(([path, target]) => ({ + path: portable(relative(root, path)), + target: portable(relative(dirname(path), target)), + })); + return { root, files, links }; + } + + /** The tree path a caller's module has: relative to the tree root. */ + static callerPath(tree: NodeIslandModuleTree | null, caller: string): string { + return tree === null ? basename(caller) : portable(relative(tree.root, caller)); + } + + private resolveFrom(fromFile: string, specifier: string): string | null { + if (isBuiltin(specifier)) return null; + if (specifier.startsWith("#")) + return `'${specifier}' is a package import ("imports" in package.json), which Node island modules of the program cannot use`; + if (specifier.startsWith("file:") || /^[a-z][a-z0-9+.-]*:/i.test(specifier)) + return `'${specifier}' is a URL; name the module by its relative path or package name`; + // A module the build cannot find is not shipped: loading it throws + // MODULE_NOT_FOUND when the call runs, as it does under Node (an + // optional require in a try block keeps working). + if (isPathSpecifier(specifier)) { + const target = resolveFile(resolve(dirname(fromFile), specifier)); + return target === null ? null : this.addModule(this.placed(target)); + } + const dir = packageDirectory(dirname(fromFile), packageName(specifier)); + if (dir !== null) this.addPackage(dir); + return null; + } + + /** The real path of `path`, recording a link for each component of it + * that is a symbolic link (a linked directory on the way, or the file + * itself), so that the path resolves inside the tree as it does here. */ + private placed(path: string): string { + let real: string = sep; + for (const part of resolve(path).split(sep)) { + if (part === "") continue; + const lexical = join(real, part); + const target = trackedRealpath(lexical) ?? lexical; + if (target !== lexical) this.links.set(lexical, target); + real = target; + } + return real; + } + + /** The real path of the file an absolute or relative `specifier` names + * from `fromFile`, or null when there is none. */ + resolvedPath(fromFile: string, specifier: string): string | null { + if (!isPathSpecifier(specifier)) return null; + const target = resolveFile(resolve(dirname(fromFile), specifier)); + return target === null ? null : (trackedRealpath(target) ?? target); + } + + /** A specifier that names `target` from `caller` inside the tree: the + * relative path between them, with `/` separators. */ + static relativeSpecifier(caller: string, target: string): string { + const path = portable(relative(dirname(caller), target)); + return path.startsWith("../") ? path : `./${path}`; + } + + /** A module of the program: collected with the package.json that scopes + * it (Node reads its "type"), and its imports traced. */ + private addModule(file: string): string | null { + if (this.traced.has(file)) return null; + this.traced.add(file); + this.files.add(file); + const scope = packageScope(dirname(file)); + if (scope !== null) this.files.add(scope); + if (!TRACED.test(file)) return null; + const text = trackedReadFile(file); + if (text === null) return `cannot read ${file}`; + const scan = scanModuleSpecifiers(text); + if (scan.computed.length > 0) { + const first = scan.computed[0]!; + const line = text.slice(0, first.at).split("\n").length; + return `${file}:${line} loads a module through ${first.form}; a module that runs in a Node island loads modules by string-literal specifiers only`; + } + for (const specifier of scan.specifiers) { + const problem = this.resolveFrom(file, specifier); + if (problem !== null) return problem; + } + return null; + } + + /** A package: its whole directory, then its dependencies. */ + private addPackage(found: string): void { + const dir = this.placed(found); + if (this.packages.has(dir)) return; + this.packages.add(dir); + this.addDirectory(dir, new Set()); + const manifest = trackedReadFile(join(dir, "package.json")); + if (manifest === null) return; + let parsed: Record; + try { + parsed = JSON.parse(manifest) as Record; + } catch { + return; + } + const names = new Set(); + for (const field of ["dependencies", "optionalDependencies", "peerDependencies"]) { + const deps = parsed[field]; + if (deps !== null && typeof deps === "object") + for (const name of Object.keys(deps)) names.add(name); + } + for (const name of [...names].sort()) { + // A dependency that is not installed (optional, or a peer the program + // does not use) is not loaded either. + const depDir = packageDirectory(dir, name); + if (depDir !== null) this.addPackage(depDir); + } + } + + /** Every file below `dir`, except nested node_modules (dependencies come + * through package.json) and .git. */ + private addDirectory(dir: string, visited: Set): void { + const real = trackedRealpath(dir) ?? dir; + if (visited.has(real)) return; + visited.add(real); + const entries = trackedAccessibleEntries(dir); + if (entries === null) return; + for (const file of entries.files) this.files.add(join(dir, file)); + for (const sub of entries.directories) { + if (sub === "node_modules" || sub === ".git") continue; + this.addDirectory(join(dir, sub), visited); + } + } +} + +/* ── module specifiers in JavaScript source ───────────────────────────── */ + +interface Token { + kind: "name" | "string" | "punct" | "other"; + value: string; + at: number; +} + +/** Keywords after which a `/` starts a regular expression. */ +const REGEX_AFTER = new Set([ + "return", + "typeof", + "instanceof", + "in", + "of", + "new", + "delete", + "void", + "throw", + "case", + "do", + "else", + "yield", + "await", +]); + +function isNameStart(c: string): boolean { + return /[A-Za-z_$\u0080-￿]/.test(c); +} + +function isNamePart(c: string): boolean { + return /[A-Za-z0-9_$\u0080-￿]/.test(c); +} + +/** The significant tokens of a JavaScript or TypeScript source: names, + * string literals (cooked) and punctuators, without comments, template + * text or regular expressions. */ +export function moduleTokens(text: string): Token[] { + const tokens: Token[] = []; + /** Brace depths at which a template literal's `${` was opened. */ + const templates: number[] = []; + let depth = 0; + let i = 0; + const n = text.length; + const regexAllowed = (): boolean => { + const last = tokens[tokens.length - 1]; + if (last === undefined) return true; + if (last.kind === "name") return REGEX_AFTER.has(last.value); + if (last.kind === "punct") + return last.value !== ")" && last.value !== "]" && last.value !== "}"; + return false; + }; + const template = (start: number): void => { + // From just after a backtick or a closing `}` to the end of the + // template or the next `${`. + while (i < n) { + const c = text[i]!; + if (c === "\\") { + i += 2; + continue; + } + if (c === "`") { + i++; + tokens.push({ kind: "other", value: "`", at: start }); + return; + } + if (c === "$" && text[i + 1] === "{") { + i += 2; + templates.push(depth); + depth++; + tokens.push({ kind: "punct", value: "${", at: start }); + return; + } + i++; + } + }; + while (i < n) { + const c = text[i]!; + if (c === " " || c === "\t" || c === "\n" || c === "\r" || c === "\f" || c === "\v") { + i++; + continue; + } + if (c === "/" && text[i + 1] === "/") { + while (i < n && text[i] !== "\n") i++; + continue; + } + if (c === "/" && text[i + 1] === "*") { + const end = text.indexOf("*/", i + 2); + i = end < 0 ? n : end + 2; + continue; + } + if (c === "#" && i === 0 && text[1] === "!") { + while (i < n && text[i] !== "\n") i++; + continue; + } + if (c === "'" || c === '"') { + const start = i; + let value = ""; + i++; + while (i < n && text[i] !== c && text[i] !== "\n") { + if (text[i] === "\\") { + const e = text[i + 1] ?? ""; + value += e === "n" ? "\n" : e === "t" ? "\t" : e === "r" ? "\r" : e === "\n" ? "" : e; + i += 2; + continue; + } + value += text[i]; + i++; + } + i++; + tokens.push({ kind: "string", value, at: start }); + continue; + } + if (c === "`") { + const start = i; + i++; + template(start); + continue; + } + if (c === "/" && regexAllowed()) { + const start = i; + i++; + let inClass = false; + while (i < n && text[i] !== "\n") { + const r = text[i]!; + if (r === "\\") { + i += 2; + continue; + } + if (r === "[") inClass = true; + else if (r === "]") inClass = false; + else if (r === "/" && !inClass) break; + i++; + } + i++; + while (i < n && isNamePart(text[i]!)) i++; + tokens.push({ kind: "other", value: "/re/", at: start }); + continue; + } + if (isNameStart(c)) { + const start = i; + while (i < n && isNamePart(text[i]!)) i++; + tokens.push({ kind: "name", value: text.slice(start, i), at: start }); + continue; + } + if (/[0-9]/.test(c) || (c === "." && /[0-9]/.test(text[i + 1] ?? ""))) { + const start = i; + while (i < n && /[0-9A-Za-z_.]/.test(text[i]!)) i++; + tokens.push({ kind: "other", value: text.slice(start, i), at: start }); + continue; + } + if (c === "{") depth++; + if (c === "}") { + if (templates.length > 0 && templates[templates.length - 1] === depth - 1) { + templates.pop(); + depth--; + const start = i; + i++; + tokens.push({ kind: "punct", value: "}", at: start }); + template(start); + continue; + } + depth--; + } + if (c === "." && text[i + 1] === "." && text[i + 2] === ".") { + tokens.push({ kind: "punct", value: "...", at: i }); + i += 3; + continue; + } + if (c === "?" && text[i + 1] === ".") { + tokens.push({ kind: "punct", value: "?.", at: i }); + i += 2; + continue; + } + tokens.push({ kind: "punct", value: c, at: i }); + i++; + } + return tokens; +} + +export interface ModuleSpecifierScan { + /** String-literal specifiers of require(), require.resolve(), import() + * and import/export declarations, in source order. */ + specifiers: string[]; + /** Loads whose module the source does not name with a string literal. */ + computed: { at: number; form: string }[]; +} + +export function scanModuleSpecifiers(text: string): ModuleSpecifierScan { + const tokens = moduleTokens(text); + const specifiers: string[] = []; + const computed: { at: number; form: string }[] = []; + const at = (k: number): Token | undefined => tokens[k]; + const is = (k: number, kind: Token["kind"], value?: string): boolean => { + const t = tokens[k]; + return t !== undefined && t.kind === kind && (value === undefined || t.value === value); + }; + /** A call `(` at k whose single argument is a string literal. */ + const literalCall = (k: number): string | null => + is(k, "punct", "(") && + is(k + 1, "string") && + (is(k + 2, "punct", ")") || is(k + 2, "punct", ",")) + ? at(k + 1)!.value + : null; + for (let k = 0; k < tokens.length; k++) { + const t = tokens[k]!; + if (t.kind !== "name") continue; + const member = is(k - 1, "punct", ".") || is(k - 1, "punct", "?."); + if (t.value === "require" && !member && !is(k + 1, "punct", ":")) { + if (is(k - 1, "name", "function") || is(k - 1, "name", "typeof")) continue; + let call = k + 1; + let form = "require()"; + if (is(k + 1, "punct", ".") && is(k + 2, "name", "resolve")) { + call = k + 3; + form = "require.resolve()"; + } else if (is(k + 1, "punct", ".")) { + continue; + } + if (!is(call, "punct", "(")) { + computed.push({ at: t.at, form: "require as a value" }); + continue; + } + const literal = literalCall(call); + if (literal === null) computed.push({ at: t.at, form: `${form} with a computed specifier` }); + else specifiers.push(literal); + continue; + } + if (t.value === "createRequire" && !member && is(k + 1, "punct", "(")) { + computed.push({ at: t.at, form: "a require() from createRequire()" }); + continue; + } + if (t.value === "import" && !member) { + if (is(k + 1, "punct", ".")) continue; // import.meta + if (is(k + 1, "punct", "(")) { + const literal = literalCall(k + 1); + if (literal === null) + computed.push({ at: t.at, form: "import() with a computed specifier" }); + else specifiers.push(literal); + continue; + } + if (is(k + 1, "string")) { + specifiers.push(at(k + 1)!.value); + continue; + } + for (let j = k + 1; j < tokens.length && j < k + 400; j++) { + if (is(j, "punct", ";")) break; + if (is(j, "name", "from") && is(j + 1, "string")) { + specifiers.push(at(j + 1)!.value); + break; + } + } + continue; + } + if (t.value === "export" && !member) { + let j = k + 1; + if (is(j, "name", "type")) j++; + if (is(j, "punct", "*")) { + for (; j < tokens.length && j < k + 400; j++) { + if (is(j, "punct", ";")) break; + if (is(j, "name", "from") && is(j + 1, "string")) { + specifiers.push(at(j + 1)!.value); + break; + } + } + } else if (is(j, "punct", "{")) { + while (j < tokens.length && !is(j, "punct", "}")) j++; + if (is(j + 1, "name", "from") && is(j + 2, "string")) specifiers.push(at(j + 2)!.value); + } + } + } + return { specifiers, computed }; +} + +/* ── shipping the tree ────────────────────────────────────────────────── */ + +/** The manifest the executable records (scr_node_island.c reads it): + * `{"files":[[path,size,sha256],...],"links":[[path,target],...]}`. Its + * SHA-256 names the tree. */ +export function nodeIslandManifest(modules: IrNodeIslandModules): string { + return JSON.stringify({ + files: modules.files.map((file) => [file.path, file.size, file.sha256]), + links: modules.links.map((link) => [link.path, link.target]), + }); +} + +/** The tree's files concatenated in manifest order, each checked against + * the digest the build recorded, or the reason one no longer matches. */ +export function readNodeIslandModules(modules: IrNodeIslandModules): Uint8Array | string { + const parts: Uint8Array[] = []; + let total = 0; + for (const file of modules.files) { + const path = join(modules.root, ...file.path.split("/")); + let bytes: Uint8Array; + try { + bytes = new Uint8Array(readFileSync(path)); + } catch { + return `${path} was removed during the build`; + } + if (bytes.length !== file.size || sha256(bytes) !== file.sha256) + return `${path} changed during the build`; + parts.push(bytes); + total += bytes.length; + } + const data = new Uint8Array(total); + let offset = 0; + for (const part of parts) { + data.set(part, offset); + offset += part.length; + } + return data; +} + +/** Writes the tree to `target` (replacing it), for `--libnode=beside`, or + * returns the reason it cannot. */ +export function placeNodeIslandModules( + modules: IrNodeIslandModules, + target: string, +): string | null { + const data = readNodeIslandModules(modules); + if (typeof data === "string") return data; + // Every path, and every link's target, stays inside the tree. + const inside = resolve(target) + sep; + const placed = (relativePath: string): string | null => { + const path = resolve(target, ...relativePath.split("/")); + return path.startsWith(inside) ? path : null; + }; + for (const entry of [...modules.files, ...modules.links]) { + const path = placed(entry.path); + if (path === null) return `the island module tree names a path outside it ('${entry.path}')`; + if ("target" in entry && !resolve(dirname(path), entry.target).startsWith(inside)) + return `the island module tree links outside it ('${entry.path}' to '${entry.target}')`; + } + rmSync(target, { recursive: true, force: true }); + mkdirSync(target, { recursive: true }); + let offset = 0; + for (const file of modules.files) { + const path = placed(file.path)!; + mkdirSync(dirname(path), { recursive: true }); + writeFileSync(path, data.subarray(offset, offset + file.size), { flag: "wx" }); + offset += file.size; + } + for (const link of modules.links) { + const path = placed(link.path)!; + mkdirSync(dirname(path), { recursive: true }); + symlinkSync(link.target, path); + } + return null; +} diff --git a/packages/compiler/src/node-islands/report.test.ts b/packages/compiler/src/node-islands/report.test.ts index a11d9fb8b..d4221d31f 100644 --- a/packages/compiler/src/node-islands/report.test.ts +++ b/packages/compiler/src/node-islands/report.test.ts @@ -199,3 +199,81 @@ test("globMatch", () => { expect(globMatch("/a/?.js", "/a/x.js")).toBe(true); expect(globMatch("/a/x.js", "/a/x.js")).toBe(true); }); + +describe("reachability", () => { + const userEdge = (fromFile: string, to: string): PackageEdge => ({ + ...edge(null, to), + fromFile, + loc: { file: fromFile, start: 0, end: 1 }, + }); + const codeFromString = (pkg: string) => + mod(pkg, `${NM}/${pkg}/index.js`, [ + marker(`${NM}/${pkg}/index.js`, "R1.1", "code-from-string"), + ]); + const build = (scan: Omit, options: NodeIslandOptions = {}) => + buildNodeIslandReport({ + entry: "/app/main.ts", + scan: { ...scan, refusals: [] }, + npm: [], + diagnostics: [], + fences: [], + policy: nodeIslandPolicy(emptyProjectConfig(), options), + }); + + test("a marked user module owns the packages only it imports", () => { + const r = build( + { + modules: [ + mod(null, "/app/main.ts"), + mod(null, "/app/plugin.ts"), + mod("x", `${NM}/x/index.js`), + mod("y", `${NM}/y/index.js`), + ], + edges: [userEdge("/app/plugin.ts", "x"), userEdge("/app/plugin.ts", "y"), edge(null, "y")], + }, + { islands: ["/app/plugin.ts"] }, + ); + expect(r.packages.map((p) => [p.package, p.status, p.islands])).toEqual([ + ["x", "inside", ["/app/plugin.ts"]], + ["y", "shared", ["/app/plugin.ts"]], + ]); + }); + + test("an island imported by a native package lists that import site", () => { + const r = build({ + modules: [mod(null, "/app/main.ts"), mod("n", `${NM}/n/index.js`), codeFromString("i")], + edges: [edge(null, "n"), edge("n", "i")], + }); + expect(r.islands.map((i) => [i.package, i.importedFrom.map((l) => l.file)])).toEqual([ + ["i", [`${NM}/n/index.js`]], + ]); + }); + + test("the dependencies of a shared package are shared with the island", () => { + const r = build({ + modules: [ + mod(null, "/app/main.ts"), + codeFromString("a"), + mod("s", `${NM}/s/index.js`), + mod("d", `${NM}/d/index.js`), + ], + edges: [edge(null, "a"), edge(null, "s"), edge("a", "s"), edge("s", "d")], + }); + expect(r.packages.map((p) => [p.package, p.status, p.islands])).toEqual([ + ["d", "shared", ["a"]], + ["s", "shared", ["a"]], + ]); + }); + + test("with islands off, a marked user module is SC8002 and keeps the user JavaScript rules", () => { + const file = "/app/legacy.js"; + const r = build( + { modules: [mod(null, file, [marker(file, "R2.1", "constructor-function")])], edges: [] }, + { mode: "off", islands: [file], strictUserJs: true }, + ); + expect(r.userModules).toEqual([]); + expect(r.violations.map((v) => v.code)).toEqual(["SC8002"]); + expect(r.violations[0]!.message).toContain(`user module '${file}'`); + expect(r.userJs.map((d) => d.code)).toEqual(["SC8101"]); + }); +}); diff --git a/packages/compiler/src/node-islands/report.ts b/packages/compiler/src/node-islands/report.ts index a6898e855..c82d0f664 100644 --- a/packages/compiler/src/node-islands/report.ts +++ b/packages/compiler/src/node-islands/report.ts @@ -14,6 +14,7 @@ import { nativePackageDiag, nodeIslandsOffDiag, + nodeIslandsOffModuleDiag, userJsObjectModelDiag, type ScrDiagnostic, } from "../diagnostics/diagnostic.js"; @@ -67,7 +68,9 @@ interface PackageState { dir: string | null; reasons: NodeIslandReason[]; importers: Set; - nativeSites: SrcLoc[]; + /** Every import site, with its importer ("" native user code, + * "user:" a marked user module, else a package key). */ + sites: { from: string; loc: SrcLoc }[]; island: boolean; decision: IslandDecision; carriedBy: string | null; @@ -88,7 +91,7 @@ export function buildNodeIslandReport(input: NodeIslandReportInput): NodeIslandR dir, reasons: [], importers: new Set(), - nativeSites: [], + sites: [], island: false, decision: "auto", carriedBy: null, @@ -113,16 +116,22 @@ export function buildNodeIslandReport(input: NodeIslandReportInput): NodeIslandR const s = state(m.packageName, m.packageDir); for (const marker of m.markers) addReason(s, classifyMarker(marker)); } - // Import edges. + // Import edges. A marked user module is an island owner of its own + // (unless islands are off, where it is a violation and stays native). + const USER = "user:"; + const userIsland = (file: string): boolean => + policy.mode !== "off" && isMarkedUserModule(policy, file) !== null; + const imports = new Map>(); for (const e of input.scan.edges) { if (e.shim || ignored(e.to)) continue; const s = state(e.to, e.toDir); - if (e.fromDir === null) { - s.importers.add(""); - s.nativeSites.push(e.loc); - } else if (e.fromDir !== s.key) { - s.importers.add(e.fromDir); - } + const from = e.fromDir ?? (userIsland(e.fromFile) ? USER + e.fromFile : ""); + if (from === s.key) continue; + s.importers.add(from); + s.sites.push({ from, loc: e.loc }); + let out = imports.get(from); + if (out === undefined) imports.set(from, (out = new Set())); + out.add(s.key); } // Lowering refusals and fences inside npm modules. const addDiagnostics = (diags: readonly ScrDiagnostic[], source: NodeIslandReasonSource) => { @@ -161,7 +170,7 @@ export function buildNodeIslandReport(input: NodeIslandReportInput): NodeIslandR const forcedNode = policy.forcedNode.get(s.name); const forcedNative = policy.forcedNative.get(s.name); const outside = s.reasons.length > 0; - const loc = s.nativeSites[0] ?? fallbackLoc; + const loc = (s.sites.find((x) => x.from === "") ?? s.sites[0])?.loc ?? fallbackLoc; if (forcedNative !== undefined) { if (outside) { s.violation = true; @@ -197,6 +206,16 @@ export function buildNodeIslandReport(input: NodeIslandReportInput): NodeIslandR s.decision = "auto"; } + // The island that owns an importer: a marked user module, an island + // package, or the island a carried package runs inside; null for native. + const ownerOf = (from: string): string | null => { + if (from.startsWith(USER)) return from; + const importer = states.get(from); + return importer === undefined ? null : importer.island ? importer.key : importer.carriedBy; + }; + const ownerName = (owner: string): string => + owner.startsWith(USER) ? owner.slice(USER.length) : states.get(owner)!.name; + // Closure: packages imported only by islands (or by packages inside // them) run inside the island. for (let changed = true; changed;) { @@ -207,9 +226,7 @@ export function buildNodeIslandReport(input: NodeIslandReportInput): NodeIslandR let carrier: string | null = null; let all = true; for (const from of s.importers) { - const importer = states.get(from); - const owner = - importer === undefined ? null : importer.island ? importer.key : importer.carriedBy; + const owner = ownerOf(from); if (owner === null) { all = false; break; @@ -223,6 +240,31 @@ export function buildNodeIslandReport(input: NodeIslandReportInput): NodeIslandR } } + // Transitive reach: every non-island package an island loads, through + // carried and shared packages alike (an island package is a boundary). + const reach = new Map>(); + const owners = [...imports.keys()].filter( + (k) => k.startsWith(USER) || states.get(k)?.island === true, + ); + for (const owner of owners) { + const name = ownerName(owner); + const queue = [...(imports.get(owner) ?? new Set())]; + const seen = new Set(queue); + while (queue.length > 0) { + const key = queue.pop()!; + if (states.get(key)?.island === true) continue; + let names = reach.get(key); + if (names === undefined) reach.set(key, (names = new Set())); + names.add(name); + for (const next of imports.get(key) ?? new Set()) { + if (!seen.has(next)) { + seen.add(next); + queue.push(next); + } + } + } + } + // Entries. const islands: NodeIslandEntry[] = []; const packages: NativePackage[] = []; @@ -248,7 +290,7 @@ export function buildNodeIslandReport(input: NodeIslandReportInput): NodeIslandR reasons: s.reasons, modules: modules.size, carries, - importedFrom: s.nativeSites, + importedFrom: s.sites.filter((x) => ownerOf(x.from) === null).map((x) => x.loc), evaluation: "eager", }); continue; @@ -257,23 +299,13 @@ export function buildNodeIslandReport(input: NodeIslandReportInput): NodeIslandR // A package that resolves to no installed JavaScript never loads (an // optional dependency that is absent): nothing to compile or island. if (s.dir === null && s.reasons.length === 0) continue; - const reachingIslands: string[] = []; - for (const from of s.importers) { - const importer = states.get(from); - if (importer === undefined) continue; - const owner = importer.island ? importer.key : importer.carriedBy; - if (owner !== null) { - const name = states.get(owner)!.name; - if (!reachingIslands.includes(name)) reachingIslands.push(name); - } - } - reachingIslands.sort(); + const reachingIslands = [...(reach.get(s.key) ?? new Set())].sort(); packages.push({ package: s.name, version, path: s.dir, status: s.carriedBy !== null ? "inside" : reachingIslands.length > 0 ? "shared" : "native", - islands: s.carriedBy !== null ? [states.get(s.carriedBy)!.name] : reachingIslands, + islands: s.carriedBy !== null ? [ownerName(s.carriedBy)] : reachingIslands, }); } islands.sort( @@ -292,10 +324,13 @@ export function buildNodeIslandReport(input: NodeIslandReportInput): NodeIslandR for (const m of input.scan.modules) { if (m.origin !== "user") continue; const marked = isMarkedUserModule(policy, m.file); - if (marked !== null) { + if (marked !== null && policy.mode !== "off") { userModules.push({ file: m.file, source: marked }); continue; } + if (marked !== null) { + violations.push(nodeIslandsOffModuleDiag(m.file, marked, { file: m.file, start: 0, end: 0 })); + } if (!policy.strictUserJs) continue; for (const marker of m.markers) { const code = ruleInfo(marker.rule).userJsCode; diff --git a/packages/compiler/src/project-config.test.ts b/packages/compiler/src/project-config.test.ts index b9750f24e..4a479b597 100644 --- a/packages/compiler/src/project-config.test.ts +++ b/packages/compiler/src/project-config.test.ts @@ -1,5 +1,8 @@ +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; import { expect, test } from "vitest"; -import { parseProjectConfig } from "./project-config.js"; +import { parseProjectConfig, readProjectConfig } from "./project-config.js"; const manifest = "/project/package.json"; @@ -22,6 +25,34 @@ test("a package.json without a scriptc key has the default configuration", () => expect(parseProjectConfig(manifest, "{ not json").diagnostics).toEqual([]); }); +test("readProjectConfig reads the nearest package.json like parseProjectConfig", () => { + const root = mkdtempSync(join(tmpdir(), "scriptc-project-config-")); + try { + const manifests = [ + '{ "name": "app", "type": "module" }', + '{ "name": "app", "\\u0073criptc": { "packages": { "encoding": "unloaded" } } }', + '{ "name": "app", "scriptc": { "packages": { "encoding": "unloaded", "x": "bogus" } } }', + ]; + manifests.forEach((text, i) => { + const dir = join(root, `p${i}`, "src"); + mkdirSync(dir, { recursive: true }); + writeFileSync(join(root, `p${i}`, "package.json"), text); + const read = readProjectConfig(join(dir, "main.ts")); + const parsed = parseProjectConfig(join(root, `p${i}`, "package.json"), text); + expect(read.manifest).toBe(parsed.manifest); + expect(read.manifestText).toBe(text); + expect([...read.packages]).toEqual([...parsed.packages]); + expect(read.nodeIslands).toEqual(parsed.nodeIslands); + expect(read.diagnostics).toEqual(parsed.diagnostics); + }); + expect( + readProjectConfig(join(root, "p1", "src", "main.ts")).packages.get("encoding")?.unloaded, + ).toBe(true); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + test("package settings: strings and objects", () => { const c = config({ packages: { @@ -39,6 +70,7 @@ test("package settings: strings and objects", () => { expect(c.packages.get("semver")?.run).toBe("auto"); expect(c.packages.get("proper-lockfile")).toEqual({ unloaded: false, + shapes: true, run: "node", evaluate: "lazy", share: "duplicate", @@ -47,6 +79,24 @@ test("package settings: strings and objects", () => { }); }); +test("shapes: false turns off the shape overlay for one package", () => { + const c = config({ + packages: { + zod: { shapes: false }, + effect: { shapes: true, run: "native" }, + ms: { shapes: "no" }, + }, + }); + expect(c.packages.get("zod")?.shapes).toBe(false); + expect(c.packages.get("zod")?.run).toBe("auto"); + expect(c.packages.get("effect")?.shapes).toBe(true); + expect(c.packages.get("effect")?.run).toBe("native"); + expect(c.packages.has("ms")).toBe(false); + expect(c.diagnostics.map((d) => d.message)).toEqual([ + `package.json 'scriptc.packages.ms.shapes' must be true or false, not "no"`, + ]); +}); + test("nodeIslands settings", () => { const c = config({ nodeIslands: { @@ -124,3 +174,154 @@ test("a non-object packages or nodeIslands value is SC8004", () => { // Other keys of the scriptc object belong to other readers. expect(config({ somethingElse: 1 }).diagnostics).toEqual([]); }); + +test("package setting keys named like Object.prototype members are unknown settings", () => { + const c = config({ packages: { debug: { constructor: "node", toString: "node" } } }); + expect(c.diagnostics.map((d) => [d.code, d.message])).toEqual([ + ["SC8004", "package.json 'scriptc.packages.debug.constructor' is not a known package setting"], + ["SC8004", "package.json 'scriptc.packages.debug.toString' is not a known package setting"], + ]); + expect(c.packages.has("debug")).toBe(false); +}); + +test("typePatches: entries keyed by package and range", () => { + const c = config({ + typePatches: { + "commander@^15.0.0": { declarations: { "HelpContext.helpWidth": "| undefined" } }, + "@scope/lib@1.x || 2.x": { + declarations: { "Options.name": "string | null" }, + note: "https://example.invalid/issue/1", + }, + }, + }); + expect(c.diagnostics).toEqual([]); + expect( + c.typePatches.map(({ key, pkg, range, declarations }) => ({ + key, + pkg, + range, + declarations: declarations.map((d) => [d.selector, d.type]), + })), + ).toEqual([ + { + key: "commander@^15.0.0", + pkg: "commander", + range: "^15.0.0", + declarations: [["HelpContext.helpWidth", "| undefined"]], + }, + { + key: "@scope/lib@1.x || 2.x", + pkg: "@scope/lib", + range: "1.x || 2.x", + declarations: [["Options.name", "string | null"]], + }, + ]); + expect(c.typePatches[0]!.declarations[0]!.offset).toBeGreaterThan(0); +}); + +test("typePatches: malformed keys, ranges, selectors and types are SC8004", () => { + const c = config({ + typePatches: { + commander: { declarations: { "A.b": "| undefined" } }, + "ms@^banana": { declarations: {} }, + "@types/node@*": { declarations: {} }, + "typescript@5": { declarations: {} }, + "lib@^1.0.0": { + declarations: { + "1bad": "| undefined", + "A.b": 3, + "A.c": "string\nnumber", + "A.d": "| undefined", + }, + patches: {}, + }, + "other@^1.0.0": [], + }, + }); + expect(c.diagnostics.every((d) => d.code === "SC8004")).toBe(true); + expect(c.diagnostics.map((d) => d.message)).toEqual([ + "package.json 'scriptc.typePatches.commander' must name a package and a version range", + `package.json 'scriptc.typePatches.ms@^banana' has the version range "^banana", which is not a semver range`, + "package.json 'scriptc.typePatches.@types/node@*' patches @types/node, whose declarations ship with scriptc or TypeScript", + "package.json 'scriptc.typePatches.typescript@5' patches typescript, whose declarations ship with scriptc or TypeScript", + "package.json 'scriptc.typePatches.lib@^1.0.0.patches' is not a known type patch setting", + "package.json 'scriptc.typePatches.lib@^1.0.0.declarations.1bad' is not a selector: it must start with a declared name, 'export=' or 'default'", + "package.json 'scriptc.typePatches.lib@^1.0.0.declarations.A.b' must map to a type string, not 3", + "package.json 'scriptc.typePatches.lib@^1.0.0.declarations.A.c' has a type that must be one line", + "package.json 'scriptc.typePatches.other@^1.0.0' must be an object, not an array", + ]); + // The valid selector of an entry with an unknown key is dropped with it; + // entries with only bad selectors keep their valid ones. + expect(c.typePatches).toEqual([]); + const partial = config({ + typePatches: { + "lib@^1.0.0": { declarations: { "1bad": "| undefined", "A.d": "| undefined" } }, + }, + }); + expect(partial.typePatches[0]!.declarations.map((d) => d.selector)).toEqual(["A.d"]); +}); + +test("typePatches: files and types name local declaration files", () => { + const root = mkdtempSync(join(tmpdir(), "scriptc-project-config-")); + try { + mkdirSync(join(root, "patches")); + writeFileSync(join(root, "patches", "entry.d.ts"), "export {};\n"); + const text = JSON.stringify({ + scriptc: { + typePatches: { + "lib@^1.0.0": { + files: { + "dist/entry.d.ts": "./patches/entry.d.ts", + "@types/lib/index.d.ts": "patches/entry.d.ts", + "dist/index.js": "./patches/entry.d.ts", + "../outside.d.ts": "./patches/entry.d.ts", + "dist/other.d.ts": "./patches/missing.d.ts", + }, + types: "./patches/entry.d.ts", + }, + "other@^1.0.0": { types: "./patches/entry.ts" }, + }, + }, + }); + const c = parseProjectConfig(join(root, "package.json"), text); + expect(c.diagnostics.map((d) => d.message)).toEqual([ + "package.json 'scriptc.typePatches.lib@^1.0.0.files.dist/index.js' must name a declaration file inside the package", + "package.json 'scriptc.typePatches.lib@^1.0.0.files.../outside.d.ts' must name a declaration file inside the package", + `package.json 'scriptc.typePatches.lib@^1.0.0.files.dist/other.d.ts' names "./patches/missing.d.ts", which does not exist`, + `package.json 'scriptc.typePatches.other@^1.0.0.types' names "./patches/entry.ts", which is not a declaration file`, + ]); + const entry = c.typePatches.find((e) => e.pkg === "lib")!; + expect([...entry.files]).toEqual([ + ["dist/entry.d.ts", join(root, "patches/entry.d.ts")], + ["@types/lib/index.d.ts", join(root, "patches/entry.d.ts")], + ]); + expect(entry.types).toBe(join(root, "patches/entry.d.ts")); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + +test("readProjectConfig checks local type patch files again for an unchanged package.json", () => { + const root = mkdtempSync(join(tmpdir(), "scriptc-project-config-")); + try { + writeFileSync( + join(root, "package.json"), + JSON.stringify({ scriptc: { typePatches: { "lib@^1.0.0": { types: "./lib.d.ts" } } } }), + ); + const entry = join(root, "main.ts"); + const missing = readProjectConfig(entry); + expect(missing.diagnostics.map((d) => d.message)).toEqual([ + `package.json 'scriptc.typePatches.lib@^1.0.0.types' names "./lib.d.ts", which does not exist`, + ]); + expect(readProjectConfig(entry)).toBe(missing); + writeFileSync(join(root, "lib.d.ts"), "export {};\n"); + const created = readProjectConfig(entry); + expect(created.diagnostics).toEqual([]); + expect(created.typePatches[0]!.types).toBe(join(root, "lib.d.ts")); + expect(readProjectConfig(entry)).toBe(created); + rmSync(join(root, "lib.d.ts")); + expect(readProjectConfig(entry).diagnostics.map((d) => d.code)).toEqual(["SC8004"]); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); diff --git a/packages/compiler/src/project-config.ts b/packages/compiler/src/project-config.ts index cc13a868d..b4a7ac6fa 100644 --- a/packages/compiler/src/project-config.ts +++ b/packages/compiler/src/project-config.ts @@ -10,10 +10,16 @@ * "encoding": "unloaded", // never loaded (package-shims.ts) * "dd-trace": "node", // run in a Node island * "hot-shots": "native", // must compile natively (SC8003) - * "proper-lockfile": { "run": "node", "evaluate": "lazy" } + * "proper-lockfile": { "run": "node", "evaluate": "lazy" }, + * "zod": { "shapes": false } // no shape-inference overlay * }, * "nodeIslands": { "auto": true, "modules": ["src/plugins/*.ts"] }, - * "wasm": ["build/module.wasm"] + * "wasm": ["build/module.wasm"], + * "typePatches": { // frontend/type-patches/layer.ts + * "commander@^15.0.0": { + * "declarations": { "HelpContext.helpWidth": "| undefined" } + * } + * } * } * * Node islands are report-only in this version: the policy decides which @@ -22,7 +28,9 @@ * to their own readers. */ import { dirname, join, resolve } from "node:path"; import { projectConfigDiag, type ScrDiagnostic } from "./diagnostics/diagnostic.js"; -import { trackedReadFile } from "./frontend/input-tracker.js"; +import { trackedFileExists, trackedReadFile } from "./frontend/input-tracker.js"; +import { parseSelector, parseTypeText } from "./frontend/type-patches/selector.js"; +import { isValidRange } from "./frontend/type-patches/semver.js"; /** How a package runs: compiled natively, in a Node island, or decided by * the detector. */ @@ -33,9 +41,12 @@ export type PackageMarshal = "copy" | "handles"; export type PackageSchedule = "lockstep" | "free"; /** One `scriptc.packages.` entry. `unloaded` packages never load; - * the other settings choose where the package runs. */ + * `shapes: false` compiles the package without shape inference's typed + * overlay (shape-inference/overlay.ts); the other settings choose where the + * package runs. */ export interface PackageSetting { unloaded: boolean; + shapes: boolean; run: PackageRun; evaluate: PackageEvaluate; share: PackageShare; @@ -57,6 +68,32 @@ export interface NodeIslandsSetting { speculativeStart: boolean; } +/** One declaration patch: a selector and the type text it maps to. */ +export interface TypePatchDeclaration { + selector: string; + type: string; + /** Offset of the selector key in the manifest, for diagnostics. */ + offset: number; +} + +/** One `scriptc.typePatches["@"]` entry. */ +export interface TypePatchEntry { + /** The key as written. */ + key: string; + pkg: string; + range: string; + declarations: TypePatchDeclaration[]; + /** `files`: declaration files of the package (or of its `@types/` twin, + * written `@types//`) replaced by local files, by + * package-relative path. The local paths are absolute. */ + files: Map; + /** `types`: a local declaration file that becomes the package's + * declaration entry (absolute), or null. */ + types: string | null; + /** Offset of the key in the manifest. */ + offset: number; +} + export interface ProjectConfig { /** The package.json the configuration came from, or null. */ manifest: string | null; @@ -66,6 +103,12 @@ export interface ProjectConfig { nodeIslands: NodeIslandsSetting; /** `scriptc.wasm`: module paths, resolved against the manifest. */ wasm: string[]; + /** `scriptc.typePatches`, in manifest order. */ + typePatches: TypePatchEntry[]; + /** The local files the configuration checked for existence, with the + * answer. A configuration read again from an unchanged package.json is + * reused only while every answer still holds. */ + localFiles: { path: string; exists: boolean }[]; /** SC8004 for every unknown key or value. */ diagnostics: ScrDiagnostic[]; } @@ -78,6 +121,9 @@ const PACKAGE_OBJECT_KEYS: Record = { marshal: ["copy", "handles"], schedule: ["lockstep", "free"], }; +/** Every key of a package setting object: the string-valued ones above and + * the boolean `shapes`. */ +const PACKAGE_SETTING_KEYS = [...Object.keys(PACKAGE_OBJECT_KEYS), "shapes"]; const NODE_ISLANDS_KEYS = [ "auto", "modules", @@ -90,6 +136,7 @@ const NODE_ISLANDS_KEYS = [ function defaultPackageSetting(): PackageSetting { return { unloaded: false, + shapes: true, run: "auto", evaluate: "auto", share: "auto", @@ -117,6 +164,8 @@ export function emptyProjectConfig(): ProjectConfig { packages: new Map(), nodeIslands: defaultNodeIslands(), wasm: [], + typePatches: [], + localFiles: [], diagnostics: [], }; } @@ -182,7 +231,7 @@ export function parseProjectConfig(manifest: string, text: string): ProjectConfi problem( ["packages", name], `has the unknown value ${shown(value)}`, - `use one of ${quoted(PACKAGE_STRINGS)}, or an object with ${Object.keys(PACKAGE_OBJECT_KEYS).join(", ")}`, + `use one of ${quoted(PACKAGE_STRINGS)}, or an object with ${PACKAGE_SETTING_KEYS.join(", ")}`, ); continue; } @@ -195,18 +244,32 @@ export function parseProjectConfig(manifest: string, text: string): ProjectConfi problem( ["packages", name], `must be a string or an object, not ${shown(value)}`, - `use one of ${quoted(PACKAGE_STRINGS)}, or an object with ${Object.keys(PACKAGE_OBJECT_KEYS).join(", ")}`, + `use one of ${quoted(PACKAGE_STRINGS)}, or an object with ${PACKAGE_SETTING_KEYS.join(", ")}`, ); continue; } let valid = true; for (const key of Object.keys(value)) { - const allowed = PACKAGE_OBJECT_KEYS[key]; + if (key === "shapes") { + const v = value[key]; + if (typeof v !== "boolean") { + problem( + ["packages", name, key], + `must be true or false, not ${shown(v)}`, + "use a boolean", + ); + valid = false; + } else setting.shapes = v; + continue; + } + const allowed = Object.hasOwn(PACKAGE_OBJECT_KEYS, key) + ? PACKAGE_OBJECT_KEYS[key] + : undefined; if (allowed === undefined) { problem( ["packages", name, key], "is not a known package setting", - `the settings are ${Object.keys(PACKAGE_OBJECT_KEYS).join(", ")}`, + `the settings are ${PACKAGE_SETTING_KEYS.join(", ")}`, ); valid = false; continue; @@ -297,6 +360,9 @@ export function parseProjectConfig(manifest: string, text: string): ProjectConfi } } + const patches = root["typePatches"]; + if (patches !== undefined) readTypePatches(patches, config, problem, locate); + // `scriptc.wasm` keeps its lenient reading: string entries of an array. const wasm = root["wasm"]; if (Array.isArray(wasm)) { @@ -307,6 +373,195 @@ export function parseProjectConfig(manifest: string, text: string): ProjectConfi return config; } +const TYPE_PATCH_KEYS = ["declarations", "files", "types", "note"]; +const DECLARATION_FILE = /\.d\.[cm]?ts$/; +/** Packages whose declarations ship with scriptc or TypeScript: a wrong + * one is a bug to report, not a project-local patch. */ +const UNPATCHABLE = /^(?:@types\/node|typescript|@typescript\/.*)$/; +const PACKAGE_NAME = /^(?:@[a-z0-9][\w.-]*\/)?[a-z0-9][\w.-]*$/i; + +/** Reads and validates `scriptc.typePatches`. */ +function readTypePatches( + patches: unknown, + config: ProjectConfig, + problem: (path: string[], detail: string, hint: string) => void, + locate: JsonLocator, +): void { + if (!isRecord(patches)) { + problem( + ["typePatches"], + `must be an object mapping "@" to patches, not ${shown(patches)}`, + `write "typePatches": { "@^1.0.0": { "declarations": { "Type.member": "| undefined" } } }`, + ); + return; + } + for (const key of Object.keys(patches)) { + const at = key.lastIndexOf("@"); + const pkg = at > 0 ? key.slice(0, at) : key; + const range = at > 0 ? key.slice(at + 1).trim() : ""; + if (at <= 0 || range === "" || !PACKAGE_NAME.test(pkg)) { + problem( + ["typePatches", key], + "must name a package and a version range", + `write "${at > 0 ? pkg : key}@^1.0.0": the range says which installed versions the patch describes`, + ); + continue; + } + if (!isValidRange(range)) { + problem( + ["typePatches", key], + `has the version range "${range}", which is not a semver range`, + `use a range such as "^${/^\d/.test(range) ? range.replace(/[^\d.]/g, "") : "1.0.0"}" or "1.2.x"`, + ); + continue; + } + if (UNPATCHABLE.test(pkg)) { + problem( + ["typePatches", key], + `patches ${pkg}, whose declarations ship with scriptc or TypeScript`, + "report a wrong declaration there instead of patching it", + ); + continue; + } + const value = patches[key]; + if (!isRecord(value)) { + problem( + ["typePatches", key], + `must be an object, not ${shown(value)}`, + `write { "declarations": { "Type.member": "| undefined" } }`, + ); + continue; + } + const entry: TypePatchEntry = { + key, + pkg, + range, + declarations: [], + files: new Map(), + types: null, + offset: locate.offsetOf(["scriptc", "typePatches", key]), + }; + const base = dirname(config.manifest!); + const localFile = (path: string[], v: unknown): string | null => { + if (typeof v !== "string" || v === "") { + problem( + path, + `must be the path of a local declaration file, not ${shown(v)}`, + `write "./type-patches/${pkg}.d.ts"`, + ); + return null; + } + const abs = resolve(base, v); + if (!DECLARATION_FILE.test(abs)) { + problem( + path, + `names "${v}", which is not a declaration file`, + "use a .d.ts, .d.mts or .d.cts file", + ); + return null; + } + const exists = trackedFileExists(abs); + config.localFiles.push({ path: abs, exists }); + if (!exists) { + problem( + path, + `names "${v}", which does not exist`, + "give the path relative to this package.json", + ); + return null; + } + return abs; + }; + const files = value["files"]; + if (files !== undefined && !isRecord(files)) { + problem( + ["typePatches", key, "files"], + `must be an object mapping declaration files of the package to local files, not ${shown(files)}`, + `write "files": { "dist/index.d.ts": "./type-patches/${pkg}.d.ts" }`, + ); + } else if (files !== undefined) { + for (const target of Object.keys(files)) { + const path = ["typePatches", key, "files", target]; + const rel = target.replace(/^\.\//, ""); + if (!DECLARATION_FILE.test(rel) || rel.startsWith("/") || rel.split("/").includes("..")) { + problem( + path, + "must name a declaration file inside the package", + `write a package-relative .d.ts path such as "dist/index.d.ts", or "@types//index.d.ts" for its @types package`, + ); + continue; + } + const abs = localFile(path, files[target]); + if (abs !== null) entry.files.set(rel, abs); + } + } + if (value["types"] !== undefined) + entry.types = localFile(["typePatches", key, "types"], value["types"]); + let valid = true; + for (const k of Object.keys(value)) { + if (!TYPE_PATCH_KEYS.includes(k)) { + problem( + ["typePatches", key, k], + "is not a known type patch setting", + `the settings are ${TYPE_PATCH_KEYS.join(", ")}`, + ); + valid = false; + } + } + const note = value["note"]; + if (note !== undefined && typeof note !== "string") { + problem( + ["typePatches", key, "note"], + `must be a string, not ${shown(note)}`, + "write a short note, such as a link to the upstream issue", + ); + valid = false; + } + const decls = value["declarations"]; + if (decls !== undefined && !isRecord(decls)) { + problem( + ["typePatches", key, "declarations"], + `must be an object mapping selectors to types, not ${shown(decls)}`, + `write "declarations": { "Type.member": "| undefined" }`, + ); + valid = false; + } else if (decls !== undefined) { + for (const selector of Object.keys(decls)) { + const path = ["typePatches", key, "declarations", selector]; + const parsed = parseSelector(selector); + if ("error" in parsed) { + problem( + path, + `is not a selector: it ${parsed.error}`, + `name a declaration such as "Type.member", "fn()", "fn(param)", "file.js:local()" or "export=()"`, + ); + continue; + } + const type = decls[selector]; + if (typeof type !== "string") { + problem( + path, + `must map to a type string, not ${shown(type)}`, + `write "| undefined" to widen, or the whole type to replace it`, + ); + continue; + } + const op = parseTypeText(type); + if ("error" in op) { + problem( + path, + `has a type that ${op.error}`, + `write "| undefined" to widen, or the whole type on one line`, + ); + continue; + } + entry.declarations.push({ selector, type, offset: locate.offsetOf(["scriptc", ...path]) }); + } + } + if (valid) config.typePatches.push(entry); + } +} + let cachedManifest: string | null = null; let cachedText: string | null = null; let cachedConfig: ProjectConfig | null = null; @@ -318,9 +573,20 @@ export function readProjectConfig(entryPath: string): ProjectConfig { const manifest = join(dir, "package.json"); const text = trackedReadFile(manifest); if (text !== null) { - if (cachedConfig !== null && cachedManifest === manifest && cachedText === text) + if ( + cachedConfig !== null && + cachedManifest === manifest && + cachedText === text && + cachedConfig.localFiles.every((file) => trackedFileExists(file.path) === file.exists) + ) return cachedConfig; - const config = parseProjectConfig(manifest, text); + // A manifest without a `scriptc` key (none spelled out, none behind a + // \u escape) has the empty configuration; builds of such projects + // skip parsing and validating it. + const config = + text.includes("scriptc") || text.includes("\\u") + ? parseProjectConfig(manifest, text) + : { ...emptyProjectConfig(), manifest, manifestText: text }; cachedManifest = manifest; cachedText = text; cachedConfig = config; diff --git a/packages/compiler/src/startup-cache.ts b/packages/compiler/src/startup-cache.ts index 1bc74bef0..950a96d08 100644 --- a/packages/compiler/src/startup-cache.ts +++ b/packages/compiler/src/startup-cache.ts @@ -2,6 +2,7 @@ * full lowering/emitter graph; exact builds need only these cache/native * identity primitives before deciding whether that graph is necessary. */ export { readRoutedExecutableCache } from "./executable/executable-cache.js"; +export { libnodeCacheKey } from "./node-islands/libnode.js"; export type { EarlyExecutableRouteOptions } from "./executable/executable-cache.js"; export { executableNativeEnvironmentFingerprint, diff --git a/packages/compiler/surface-manifest.json b/packages/compiler/surface-manifest.json index e44f5d31e..2237619ef 100644 --- a/packages/compiler/surface-manifest.json +++ b/packages/compiler/surface-manifest.json @@ -1,6 +1,6 @@ { "schemaVersion": 2, - "compilerVersion": "0.4.2", + "compilerVersion": "0.4.5", "coverage": [ "Entries are projected mechanically from the compiler's own decision tables: the diagnostics registry, the unsupported-syntax dispatch tables, the stdlib and node-builtin lowering tables, and the supported-builtin-module list. Nothing is hand-maintained; the manifest regenerates byte-identically from the source tree at this version.", "Absence from this manifest means 'not projected', never 'unsupported'. Surfaces lowered through dedicated code paths rather than tables are not yet projected: console, JSON, Promise/async and the timer surface, the net/http/tls/https/dgram/dns/assert/test/stream/readline module member surfaces, template literals, the regex slice, global functions (parseInt, parseFloat, isNaN, isFinite), and the process surface outside its ambient slice.", @@ -568,9 +568,7 @@ "id": "node-builtin.crypto.diffieHellman", "kind": "node-builtin", "name": "crypto.diffieHellman", - "status": "unsupported", - "code": "SC2020", - "note": "crypto.diffieHellman takes DH, EC or X25519 KeyObjects and has no lowering; the objects of createDiffieHellman, getDiffieHellman and createECDH compute shared secrets" + "status": "static" }, { "id": "node-builtin.crypto.generateKey", @@ -2471,6 +2469,12 @@ "name": "url.resolve", "status": "static" }, + { + "id": "node-builtin.url.urlToHttpOptions", + "kind": "node-builtin", + "name": "url.urlToHttpOptions", + "status": "static" + }, { "id": "node-builtin.util", "kind": "node-builtin", @@ -2478,12 +2482,36 @@ "status": "static", "note": "recognized module (bare and node:-prefixed specifiers)" }, + { + "id": "node-builtin.util._extend", + "kind": "node-builtin", + "name": "util._extend", + "status": "static" + }, + { + "id": "node-builtin.util.aborted", + "kind": "node-builtin", + "name": "util.aborted", + "status": "static" + }, { "id": "node-builtin.util.callbackify", "kind": "node-builtin", "name": "util.callbackify", "status": "static" }, + { + "id": "node-builtin.util.convertProcessSignalToExitCode", + "kind": "node-builtin", + "name": "util.convertProcessSignalToExitCode", + "status": "static" + }, + { + "id": "node-builtin.util.debounce", + "kind": "node-builtin", + "name": "util.debounce", + "status": "static" + }, { "id": "node-builtin.util.debug", "kind": "node-builtin", @@ -2502,6 +2530,12 @@ "name": "util.deprecate", "status": "static" }, + { + "id": "node-builtin.util.diff", + "kind": "node-builtin", + "name": "util.diff", + "status": "static" + }, { "id": "node-builtin.util.getSystemErrorMap", "kind": "node-builtin", @@ -2526,6 +2560,12 @@ "name": "util.inherits", "status": "static" }, + { + "id": "node-builtin.util.isArray", + "kind": "node-builtin", + "name": "util.isArray", + "status": "static" + }, { "id": "node-builtin.util.isDeepStrictEqual", "kind": "node-builtin", @@ -2564,6 +2604,12 @@ "name": "util.styleText", "status": "static" }, + { + "id": "node-builtin.util.throttle", + "kind": "node-builtin", + "name": "util.throttle", + "status": "static" + }, { "id": "node-builtin.util.toUSVString", "kind": "node-builtin", @@ -2878,22 +2924,6 @@ "status": "static", "note": "recognized module (bare and node:-prefixed specifiers)" }, - { - "id": "node-builtin.zlib.BrotliCompress", - "kind": "node-builtin", - "name": "zlib.BrotliCompress", - "status": "unsupported", - "code": "SC2020", - "note": "Brotli needs a Brotli codec, which the static runtime does not vendor — the deflate/inflate, raw, gzip/gunzip, and unzip sync/callback forms with Node's options, crc32, and the Deflate/Inflate/Gzip/Gunzip/DeflateRaw/InflateRaw/Unzip streams are the lowered zlib surface" - }, - { - "id": "node-builtin.zlib.BrotliDecompress", - "kind": "node-builtin", - "name": "zlib.BrotliDecompress", - "status": "unsupported", - "code": "SC2020", - "note": "Brotli needs a Brotli codec, which the static runtime does not vendor — the deflate/inflate, raw, gzip/gunzip, and unzip sync/callback forms with Node's options, crc32, and the Deflate/Inflate/Gzip/Gunzip/DeflateRaw/InflateRaw/Unzip streams are the lowered zlib surface" - }, { "id": "node-builtin.zlib.DEFLATE", "kind": "node-builtin", @@ -3647,33 +3677,25 @@ "id": "node-builtin.zlib.brotliCompress", "kind": "node-builtin", "name": "zlib.brotliCompress", - "status": "unsupported", - "code": "SC2020", - "note": "Brotli needs a Brotli codec, which the static runtime does not vendor — the deflate/inflate, raw, gzip/gunzip, and unzip sync/callback forms with Node's options, crc32, and the Deflate/Inflate/Gzip/Gunzip/DeflateRaw/InflateRaw/Unzip streams are the lowered zlib surface" + "status": "static" }, { "id": "node-builtin.zlib.brotliCompressSync", "kind": "node-builtin", "name": "zlib.brotliCompressSync", - "status": "unsupported", - "code": "SC2020", - "note": "Brotli needs a Brotli codec, which the static runtime does not vendor — the deflate/inflate, raw, gzip/gunzip, and unzip sync/callback forms with Node's options, crc32, and the Deflate/Inflate/Gzip/Gunzip/DeflateRaw/InflateRaw/Unzip streams are the lowered zlib surface" + "status": "static" }, { "id": "node-builtin.zlib.brotliDecompress", "kind": "node-builtin", "name": "zlib.brotliDecompress", - "status": "unsupported", - "code": "SC2020", - "note": "Brotli needs a Brotli codec, which the static runtime does not vendor — the deflate/inflate, raw, gzip/gunzip, and unzip sync/callback forms with Node's options, crc32, and the Deflate/Inflate/Gzip/Gunzip/DeflateRaw/InflateRaw/Unzip streams are the lowered zlib surface" + "status": "static" }, { "id": "node-builtin.zlib.brotliDecompressSync", "kind": "node-builtin", "name": "zlib.brotliDecompressSync", - "status": "unsupported", - "code": "SC2020", - "note": "Brotli needs a Brotli codec, which the static runtime does not vendor — the deflate/inflate, raw, gzip/gunzip, and unzip sync/callback forms with Node's options, crc32, and the Deflate/Inflate/Gzip/Gunzip/DeflateRaw/InflateRaw/Unzip streams are the lowered zlib surface" + "status": "static" }, { "id": "node-builtin.zlib.crc32", @@ -3685,17 +3707,13 @@ "id": "node-builtin.zlib.createBrotliCompress", "kind": "node-builtin", "name": "zlib.createBrotliCompress", - "status": "unsupported", - "code": "SC2020", - "note": "Brotli needs a Brotli codec, which the static runtime does not vendor — the deflate/inflate, raw, gzip/gunzip, and unzip sync/callback forms with Node's options, crc32, and the Deflate/Inflate/Gzip/Gunzip/DeflateRaw/InflateRaw/Unzip streams are the lowered zlib surface" + "status": "static" }, { "id": "node-builtin.zlib.createBrotliDecompress", "kind": "node-builtin", "name": "zlib.createBrotliDecompress", - "status": "unsupported", - "code": "SC2020", - "note": "Brotli needs a Brotli codec, which the static runtime does not vendor — the deflate/inflate, raw, gzip/gunzip, and unzip sync/callback forms with Node's options, crc32, and the Deflate/Inflate/Gzip/Gunzip/DeflateRaw/InflateRaw/Unzip streams are the lowered zlib surface" + "status": "static" }, { "id": "node-builtin.zlib.createDeflate", @@ -4253,6 +4271,13 @@ "status": "static", "note": "one date string, using V8's date parser shared with new Date(dateString)" }, + { + "id": "stdlib.date.setters", + "kind": "stdlib", + "name": "Date.prototype setters", + "status": "static", + "note": "setTime and the local and UTC field setters, with their optional arguments" + }, { "id": "stdlib.date.toDateString", "kind": "stdlib", diff --git a/packages/compiler/test/array-pipeline-fusion.test.ts b/packages/compiler/test/array-pipeline-fusion.test.ts index 55cd0025e..2c0c138ce 100644 --- a/packages/compiler/test/array-pipeline-fusion.test.ts +++ b/packages/compiler/test/array-pipeline-fusion.test.ts @@ -108,5 +108,161 @@ const kept = rows.filter((r) => r.value > 3); console.log(kept.map((r) => r.id).length, kept.length); ${postlude}`); expect(init(mod).locals.some((l) => l.name === "kept")).toBe(true); - expect(helperCalls(init(mod))).toEqual(["%arr.filter", "%arr.fused"]); + expect(helperCalls(init(mod))).toEqual(["%arr.fused", "%arr.loop"]); +}); + +test("a lone call with a stable closure-literal callback runs as a loop", async () => { + const mod = await lowerSource(`${prelude} +const scale = 3; +const scaled = rows.map((r) => r.value * scale); +let total = 0; +rows.forEach((r) => { total += r.value; }); +const big = rows.filter((r) => r.value > scale); +const sum = scaled.reduce((acc, v) => acc + v, 0); +console.log(scaled.length, total, big.length, sum, rows.some((r) => r.id === scale)); +${postlude}`); + // forEach assigns \`total\`, a binding of \`run\`: it keeps its helper. + expect(helperCalls(init(mod))).toEqual([ + "%arr.forEach", + "%arr.loop", + "%arr.loop", + "%arr.loop", + "%arr.loop", + ]); + const loops = mod.functions.filter((f) => f.name.startsWith("%arr.loop.")); + expect(loops).toHaveLength(4); + // The callback bodies are copied into the loops: no closure value is + // created or called, and the captured \`scale\` is a plain parameter. + for (const fn of loops) { + expect(count(fn, "callValue")).toBe(0); + expect(count(fn, "closure")).toBe(0); + } + expect(count(init(mod), "closure")).toBe(1); + // No closure captures \`scale\` any more, so it needs no box. + expect(init(mod).locals.find((l) => l.name === "scale")?.boxed).toBeUndefined(); +}); + +test("a lone call keeps its helper when a capture can change while it runs", async () => { + const mod = await lowerSource(`${prelude} +let limit = 2; +const bump = (): void => { limit++; }; +const a = rows.filter((r) => { bump(); return r.id < limit; }); +let step = 1; +step = 2; +const b = rows.map((r) => r.id * step); +console.log(a.length, b.length, limit); +${postlude}`); + expect(helperCalls(init(mod))).toEqual(["%arr.filter", "%arr.map"]); +}); + +test("a lone call keeps its helper when a local class's method can assign a capture", async () => { + const mod = await lowerSource(`${prelude} +let k = 1; +class Bump { + go(): void { k = 10; } +} +const b = new Bump(); +const out = rows.map((r) => { if (r.id === 2) b.go(); return r.id * k; }); +console.log(out.join(",")); +${postlude}`); + expect(helperCalls(init(mod))).toEqual(["%arr.map"]); +}); + +test("a copied callback keeps its switch and for-of labels and bindings", async () => { + const mod = await lowerSource(`${prelude} +const out = rows.map((r) => { + let n = 0; + outer: switch (r.id % 3) { + case 0: + n = 10; + break outer; + case 1: + inner: for (const v of [1, 2, 3]) { + n += v; + if (v === 2) break outer; + if (v === 3) continue inner; + } + break; + default: + n = r.value; + } + return n; +}); +console.log(out.join(",")); +${postlude}`); + expect(helperCalls(init(mod))).toEqual(["%arr.loop"]); +}); + +test("a copied callback's switch-local records and known unions are rewritten in its cases", async () => { + const mod = await lowerSource(`${prelude} +type Shape = { kind: "sq"; side: number } | { kind: "rect"; w: number; h: number }; +const out = rows.map((r) => { + switch (r.id % 3) { + case 0: + const p = { a: r.id, b: 2 }; + return p.a + p.b; + case 1: + const sq = { kind: "sq" as const, side: r.value }; + const s: Shape = sq; + return s.kind === "sq" ? s.side * 10 : 0; + default: + return r.value; + } +}); +console.log(out.join(",")); +${postlude}`); + expect(helperCalls(init(mod))).toEqual(["%arr.loop"]); +}); + +test("a copied callback with early returns keeps its switch labels", async () => { + const mod = await lowerSource(`${prelude} +const out = rows.map((r) => { + let n = 0; + outer: switch (r.id % 4) { + case 0: + return -1; + case 1: + n = 10; + break outer; + default: + inner: switch (r.value % 2) { + case 0: + n = r.value; + break outer; + default: + break inner; + } + n = 1; + } + return n; +}); +const hit = rows.some((r) => { + pick: switch (r.id) { + case 3: + break pick; + default: + return false; + } + return true; +}); +console.log(out.join(","), hit); +${postlude}`); + expect(helperCalls(init(mod))).toEqual(["%arr.loop", "%arr.loop"]); +}); + +test("a lone call in a local class's method keeps its helper for the class's captures", async () => { + const mod = await lowerSource(`${prelude} +let k = 1; +class Box { + set(v: number): void { k = v; } + go(): number[] { + const b = new Box(); + return rows.map((r) => { b.set(r.id); return r.value + k; }); + } +} +console.log(new Box().go().join(",")); +${postlude}`); + const go = mod.functions.find((f) => f.name.endsWith(".Box.go")); + expect(go).toBeDefined(); + expect(helperCalls(go!)).toEqual(["%arr.map"]); }); diff --git a/packages/compiler/test/bytes-header-load-emission.test.ts b/packages/compiler/test/bytes-header-load-emission.test.ts new file mode 100644 index 000000000..bc7cb9803 --- /dev/null +++ b/packages/compiler/test/bytes-header-load-emission.test.ts @@ -0,0 +1,60 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile, deserializeModule, validateModule } from "../src/index.js"; +import { emitLlvmModule } from "../src/backend/llvm/emitter.js"; +import { type IrModule } from "../src/ir/ir.js"; + +async function lower(source: string): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-bytes-header-")); + try { + const entry = join(dir, "main.ts"), + output = join(dir, "main.ir.json"); + await writeFile(entry, source); + const result = await compile(entry, { outDir: dir, outPath: output, outputKind: "ir" }); + if (!result.ok) + throw new Error(result.diagnostics.map((item) => `${item.code}: ${item.message}`).join("\n")); + const module = deserializeModule(await readFile(output, "utf8")); + expect(validateModule(module)).toEqual([]); + return module; + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +function body(llvm: string, symbol: string): string { + const found = new RegExp(`^define internal [^\\n]*@${symbol}(?:\\.i32)?\\([^]*?^}`, "m").exec( + llvm, + ); + expect(found, symbol).not.toBeNull(); + return found![0]; +} + +const INVARIANT_HEADER = /load (?:i64|i32|ptr), ptr [^\n]*, !tbaa !\d+, !invariant\.load !\{\}$/m; + +test("a typed array's length read as a value is an ordinary load; access bounds stay invariant", async () => { + const module = await lower(` +class Table { + slots: Int32Array = new Int32Array(4); + swap(): number { + const slots = this.slots; + this.slots = new Int32Array(1); + return slots.length + slots.byteLength; + } +} +function sum(values: Float64Array, scale: number): number { + let total = 0; + for (let i = 0; i < values.length; i++) total += values[i]! * scale; + return total; +} +const table = new Table(); +console.log(table.swap(), sum(new Float64Array([1, 2]), 2)); +`); + const llvm = emitLlvmModule(module); + // The length outlives the view: the release must not be crossed. + const swap = body(llvm, "sc_bf__x25_Table_swap"); + expect(swap).toMatch(/load i64, ptr [^\n]*, !tbaa !\d+$/m); + expect(swap).not.toContain("!invariant.load"); + expect(body(llvm, "sc_bf_sum")).toMatch(INVARIANT_HEADER); +}); diff --git a/packages/compiler/test/callback-pending-emission.test.ts b/packages/compiler/test/callback-pending-emission.test.ts index 9dcbbf0a1..7e7996c83 100644 --- a/packages/compiler/test/callback-pending-emission.test.ts +++ b/packages/compiler/test/callback-pending-emission.test.ts @@ -9,7 +9,9 @@ import { emitLlvmModule } from "../src/backend/llvm/emitter.js"; // A specialized array helper calls its known callback directly after // checking the closure's entry. A callback that cannot throw skips the // pending test on that path, and the fallback through the closure's own -// entry keeps it; a callback that can throw keeps it on both paths. +// entry keeps it; a callback that can throw keeps it on both paths. The +// callbacks are passed through bindings: a closure literal at the call +// would run as a loop with its body copied in (array-pipelines.ts). test("known callbacks outside the may-throw set test the pending flag only on the fallback", async () => { const dir = mkdtempSync(join(tmpdir(), "scriptc-callback-pending-")); try { @@ -18,13 +20,16 @@ test("known callbacks outside the may-throw set test the pending flag only on th writeFileSync( entry, `const values = [1, 2, 3, 4]; -console.log(values.every((v) => v > 0)); -console.log(values.map((v) => v * 2).join(",")); +const positive = (v: number): boolean => v > 0; +const double = (v: number): number => v * 2; +const stopAtThree = (v: number): boolean => { + if (v > 2) throw new Error("stop at " + v); + return true; +}; +console.log(values.every(positive)); +console.log(values.map(double).join(",")); try { - console.log(values.every((v) => { - if (v > 2) throw new Error("stop at " + v); - return true; - })); + console.log(values.every(stopAtThree)); } catch (error) { console.log("caught", (error as Error).message); } @@ -35,9 +40,11 @@ try { const module = deserializeModule(readFileSync(output, "utf8")); expect(validateModule(module)).toEqual([]); const llvm = emitLlvmModule(module, {}); + // The fallback calls the closure's entry, after giving it an undefined + // receiver when it reads one, so its edge comes from receiver.done then. const guards = [ ...llvm.matchAll( - /(%t\d+) = phi i1 \[ false, %callback\.direct\d+ \], \[ true, %callback\.indirect\d+ \]/g, + /(%t\d+) = phi i1 \[ false, %callback\.direct\d+ \], \[ true, %(?:callback\.indirect|receiver\.done)\d+ \]/g, ), ]; // The quiet every and map callbacks; not the throwing one. diff --git a/packages/compiler/test/canon-normalize.test.ts b/packages/compiler/test/canon-normalize.test.ts new file mode 100644 index 000000000..365818a5a --- /dev/null +++ b/packages/compiler/test/canon-normalize.test.ts @@ -0,0 +1,342 @@ +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterAll, beforeAll, describe, expect, test } from "vitest"; +import { canonEqual, canonKey, Normalizer, type Canon } from "../src/frontend/canon.js"; +import { mapCanonical } from "../src/frontend/canon-map.js"; +import { + mapType, + ShapeRegistry, + UnionRegistry, + type TypeMapperCtx, +} from "../src/frontend/type-mapper.js"; +import { typeKey } from "../src/ir/ir.js"; +import { loadProgram } from "../src/frontend/program-node.js"; +import * as ts from "../src/frontend/ts7/adapter.js"; + +let directory: string; +let load: ReturnType; +let checker: ts.TypeChecker; +const aliases = new Map(); +const asTypes = new Map(); +const valueTypes = new Map(); +const typeParams = new Map(); + +beforeAll(() => { + directory = mkdtempSync(join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-canon-")); + const entry = join(directory, "input.ts"); + writeFileSync( + entry, + ` + // #2228: an intersection that restates an optional array member. + export interface BuildOptions { cwd: string; contextFiles?: Array<{ path: string; content: string }> } + export type Normalized = BuildOptions & { contextFiles: Array<{ path: string; content: string }> }; + export type Same = { x: number[] } & { x: number[] }; + // Unions flatten, drop never, read void as undefined, and dedupe. + export type Flat = string | (number | undefined) | never | void | string; + export type Flag = true | false | 0; + // Branded primitive. + export type Id = string & { readonly brand: "id" }; + // #2241/#2246: a recursive interface with a generic method. + export interface TreeNode { name: string; child?: TreeNode; send(message: T): string } + // #2227: a generic alias spelled over a generic body's own parameters. + export type Catalog = { groups: G[]; provider: P }; + export function cast(x: unknown) { + return x as Catalog; + } + // Narrowed type parameters. + export function present(x: T & {}) { + return x; + } + export const sampleNumber = 1 as number; + export const sampleText = "a" as string; + export const sampleMaybe = undefined as string | undefined; + export class Point { x = 0 } + export type Points = readonly Point[]; + export type Pair = [string, number]; + // Two type parameters named T, in different functions. + export function first(x: unknown) { + return x as T[]; + } + export function second(x: unknown) { + return x as readonly T[]; + } + // Distinct callables, and arrays of them. + export type Calls = ((a: string) => void) & ((a: number) => void); + export type CallArrays = ((a: string) => void)[] | ((a: number) => void)[]; + // A dictionary with a named property. + export type Dict = { [key: string]: number } & { size: number }; + // Recursive array and tuple aliases. + export type Nested = number | Nested[]; + export type Chain = [number, Chain] | null; + // Optional and rest tuple elements. + export type Loose = [string, number?, ...boolean[]]; + // Arrays of two instantiations of one generic class. + export type Maps = Map[] | Map[]; + // The same instantiations, each refined by an object type. + export type RefinedMaps = + | (Map & { tag: number })[] + | (Map & { tag: number })[]; + `, + ); + load = loadProgram(entry); + checker = load.program.getTypeChecker(); + ts.walkPreorder(load.entry, (node) => { + if (ts.isTypeAliasDeclaration(node) || ts.isInterfaceDeclaration(node)) + aliases.set( + node.name.text, + checker.getTypeAtLocation(ts.isTypeAliasDeclaration(node) ? node.type : node.name), + ); + if (ts.isAsExpression(node)) { + let fn: ts.Node | undefined = node.parent; + while (fn !== undefined && !ts.isFunctionDeclaration(fn)) fn = fn.parent; + const params = + fn !== undefined && ts.isFunctionDeclaration(fn) + ? (fn.typeParameters?.nodes ?? []).map((p) => checker.getSymbolAtLocation(p.name)!) + : []; + asTypes.set(ts.getTextOfNode(node.type), { + type: checker.getTypeAtLocation(node.type), + params, + }); + } + if (ts.isTypeParameterDeclaration(node) && ts.isFunctionDeclaration(node.parent)) + typeParams.set( + `${node.parent.name?.text}.${node.name.text}`, + checker.getSymbolAtLocation(node.name)!, + ); + if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name)) + valueTypes.set(node.name.text, checker.getTypeAtLocation(node.name)); + if ( + ts.isParameter(node) && + node.type !== undefined && + ts.isFunctionDeclaration(node.parent) && + node.parent.name?.text === "present" + ) + asTypes.set("present", { + type: checker.getTypeAtLocation(node.type), + params: (node.parent.typeParameters?.nodes ?? []).map((p) => + checker.getSymbolAtLocation(p.name)!, + ), + }); + }); +}); +afterAll(() => { + load?.dispose(); + if (directory) rmSync(directory, { recursive: true, force: true }); +}); + +const key = (c: Canon) => canonKey(c); + +describe("canon", () => { + test("intersections that restate members resolve to one object (#2228)", () => { + const n = new Normalizer(checker); + expect(key(n.canon(aliases.get("Same")!))).toBe("{ x: number[] }"); + expect(key(n.canon(aliases.get("Normalized")!))).toBe( + "{ cwd: string; contextFiles: { path: string; content: string }[] }", + ); + }); + + test("unions are flat, never-free and deduplicated", () => { + const n = new Normalizer(checker); + expect(key(n.canon(aliases.get("Flat")!))).toBe("string | number | undefined"); + expect(key(n.canon(aliases.get("Flag")!))).toBe("boolean | 0"); + }); + + test("a branded primitive is the primitive", () => { + expect(key(new Normalizer(checker).canon(aliases.get("Id")!))).toBe("string"); + }); + + test("recursion closes on the node under construction (#2241, #2246)", () => { + const n = new Normalizer(checker); + const tree = n.canon(aliases.get("TreeNode")!); + expect(tree.k).toBe("obj"); + if (tree.k !== "obj") return; + const child = tree + .props() + .find((p) => p.name === "child")! + .type(); + expect(child.k).toBe("union"); + if (child.k !== "union") return; + expect(child.arms).toContain(tree); + const send = tree + .props() + .find((p) => p.name === "send")! + .type(); + expect(send.k === "obj" && send.call()[0]!.generic).toBe(true); + }); + + test("a generic alias in a generic body substitutes the environment (#2227)", () => { + const n = new Normalizer(checker); + const { type, params } = asTypes.get("Catalog")!; + expect(key(n.canon(type))).toBe("{ groups: param G[]; provider: param P }"); + const bound = n.canon( + type, + new Map([ + [params[0]!, valueTypes.get("sampleNumber")!], + [params[1]!, valueTypes.get("sampleText")!], + ]), + ); + expect(key(bound)).toBe("{ groups: number[]; provider: string }"); + expect(bound.k === "obj" && bound.alias?.name).toBe("Catalog"); + expect(bound.k === "obj" && bound.alias?.tsType).toBe(type); + }); + + test("a narrowed parameter drops its unit arms", () => { + const n = new Normalizer(checker); + const { type, params } = asTypes.get("present")!; + expect(key(n.canon(type, new Map([[params[0]!, valueTypes.get("sampleMaybe")!]])))).toBe( + "string", + ); + }); + + test("type parameters with the same name are distinct in the intern key", () => { + const n = new Normalizer(checker); + const { type } = asTypes.get("T[]")!; + const firstT = typeParams.get("first.T")!; + const secondT = typeParams.get("second.T")!; + const num = valueTypes.get("sampleNumber")!; + const text = valueTypes.get("sampleText")!; + expect( + key( + n.canon( + type, + new Map([ + [firstT, num], + [secondT, text], + ]), + ), + ), + ).toBe("number[]"); + expect( + key( + n.canon( + type, + new Map([ + [firstT, text], + [secondT, num], + ]), + ), + ), + ).toBe("string[]"); + }); + + test("callables and arrays of callables with different parameters stay distinct", () => { + const n = new Normalizer(checker); + const calls = n.canon(aliases.get("Calls")!); + expect(calls.k === "obj" && calls.call().length).toBe(2); + const arms = n.canon(aliases.get("CallArrays")!); + expect(arms.k === "union" && arms.arms.length).toBe(2); + expect(key(arms)).toBe("{ (string) => void }[] | { (number) => void }[]"); + }); + + test("intersecting a dictionary with a named property keeps the index signature", () => { + const dict = new Normalizer(checker).canon(aliases.get("Dict")!); + expect(key(dict)).toBe("{ [string]: number; size: number }"); + }); + + test("recursive array and tuple aliases close on themselves", () => { + const n = new Normalizer(checker); + const nested = n.canon(aliases.get("Nested")!); + expect(nested.k).toBe("union"); + if (nested.k !== "union") return; + // The array closes on itself through the union of its element. + const array = nested.arms.find((a) => a.k === "array"); + const elem = array?.k === "array" ? array.elem : undefined; + expect(elem?.k === "union" && elem.arms.includes(array!)).toBe(true); + const chain = n.canon(aliases.get("Chain")!); + expect(chain.k).toBe("union"); + if (chain.k !== "union") return; + const tuple = chain.arms.find((a) => a.k === "tuple"); + const rest = tuple?.k === "tuple" ? tuple.elems[1]!.type : undefined; + expect(rest?.k === "union" && rest.arms.includes(tuple!)).toBe(true); + }); + + test("a failed comparison withdraws the pairs it assumed equal", () => { + const array = (p: "number" | "string"): Canon => ({ + k: "array", + elem: { k: "prim", p }, + readonly: false, + }); + // number[] fails against string[] before it matches number[]; that + // attempt must not make string[] match later. + const a: Canon = { k: "union", arms: [array("number"), array("number")] }; + const b: Canon = { k: "union", arms: [array("string"), array("number")] }; + expect(canonEqual(a, b)).toBe(false); + expect(canonEqual(b, a)).toBe(false); + }); + + test("instantiations of one generic class stay distinct", () => { + const maps = new Normalizer(checker).canon(aliases.get("Maps")!); + expect(maps.k === "union" && maps.arms.length).toBe(2); + expect(key(maps)).toBe("Map[] | Map[]"); + }); + + test("a refined instantiation keeps its type arguments", () => { + const maps = new Normalizer(checker).canon(aliases.get("RefinedMaps")!); + expect(maps.k === "union" && maps.arms.length).toBe(2); + expect(key(maps)).toBe("Map[] | Map[]"); + }); + + test("optional and rest tuple elements keep their flags", () => { + expect(key(new Normalizer(checker).canon(aliases.get("Loose")!))).toBe( + "[string, number | undefined?, ...boolean]", + ); + }); + + test("classes are nominal; arrays and tuples keep their elements", () => { + const n = new Normalizer(checker); + expect(key(n.canon(aliases.get("Points")!))).toBe("readonly Point[]"); + expect(key(n.canon(aliases.get("Pair")!))).toBe("[string, number]"); + }); +}); + +describe("mapCanonical", () => { + const ctx = (): TypeMapperCtx => ({ + checker, + shapes: new ShapeRegistry(), + unions: new UnionRegistry(), + classNamer: (decl) => decl.name?.text ?? "anonymous", + isStdlibFile: (sf) => load.program.isSourceFileDefaultLibrary(sf), + isNpmFile: () => false, + isExternalTypeFile: () => false, + isProgramFile: (sf) => load.moduleOrder.includes(sf), + }); + test.for(["Normalized", "Same", "Flat", "Id", "Pair"])("%s maps like mapType", (name) => { + const strict = mapType(aliases.get(name)!, ctx()); + const canonical = mapCanonical(new Normalizer(checker).canon(aliases.get(name)!), ctx()); + expect(strict).not.toBeNull(); + expect(canonical && typeKey(canonical)).toBe(typeKey(strict!)); + }); + test("shadow mode observes every successful top-level mapping", () => { + const seen: string[] = []; + const point = { kind: "object", className: "Point" } as const; + const observed: TypeMapperCtx = { + ...ctx(), + typeMemo: new Map(), + localClassValue: (type) => (type === aliases.get("Points") ? point : null), + canonShadow: (type, mapped) => seen.push(`${checker.typeToString(type)} ${typeKey(mapped)}`), + }; + // An intrinsic primitive, a mapping and its memo hit, and a local class + // value: one observation each; the nested mappings of `Same` are not + // observed. + const number = mapType(valueTypes.get("sampleNumber")!, observed); + const same = mapType(aliases.get("Same")!, observed); + expect(mapType(aliases.get("Same")!, observed)).toBe(same); + expect(mapType(aliases.get("Points")!, observed)).toBe(point); + expect(seen).toEqual([ + `number ${typeKey(number!)}`, + `Same ${typeKey(same!)}`, + `Same ${typeKey(same!)}`, + `Points ${typeKey(point)}`, + ]); + }); + test("a generic alias maps under bindings, where mapType needs the instance table", () => { + const { type, params } = asTypes.get("Catalog")!; + const env = new Map([ + [params[0]!, valueTypes.get("sampleNumber")!], + [params[1]!, valueTypes.get("sampleText")!], + ]); + const canonical = mapCanonical(new Normalizer(checker).canon(type, env), ctx()); + expect(canonical?.kind).toBe("record"); + }); +}); diff --git a/packages/compiler/test/chunked-emission.test.ts b/packages/compiler/test/chunked-emission.test.ts index b4912c9f6..5d54ff804 100644 --- a/packages/compiler/test/chunked-emission.test.ts +++ b/packages/compiler/test/chunked-emission.test.ts @@ -1,4 +1,4 @@ -import { mkdtemp, readFile, rm } from "node:fs/promises"; +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { expect, test } from "vitest"; @@ -9,9 +9,10 @@ import { inlineRcOption } from "../src/backend/llvm/shapes.js"; // Programs whose function bodies request module-level helpers and tables // that the chunk journal (emit-journal.ts) carries: checked-dynamic -// conversions, inspection, publication walkers, live record views, stream -// adapters, regex instances, colliding literal digests, interned C strings, -// speculative loop sites, typed message listeners and readers. +// conversions (with their discriminant tables), inspection, publication +// walkers, live record views, stream adapters, regex instances, colliding +// literal digests, interned C strings, speculative loop sites, typed message +// listeners and readers. const PROGRAMS = [ "1003-json-parse-unions.ts", "114-live-record-delete.ts", @@ -21,8 +22,10 @@ const PROGRAMS = [ "4900-console-instances.ts", "5612-readable-from-iterables.cjs", "5702-publish-candidate-buffers.ts", + "6305-json-discriminant-dispatch.ts", "7321-speculative-loop-bail-sites.ts", "7360-worker-typed-records.ts", + "7110-als-completion-callbacks.ts", "node24-text-utilities-coercion.js", ]; @@ -68,14 +71,139 @@ test("function bodies emitted in chunks merge into the text one emitter gives", } }, 600_000); -// Debug metadata is numbered per emitter (debug-info.ts) and not journaled: -// a debug build emits its function bodies in one emitter. -test("debug builds emit function bodies serially, with every metadata reference defined", async () => { +// Thunks a range creates are journaled, so the merging emitter defines +// them: here Promise.all's tuple adapter, requested by a function that is +// not async (async functions are emitted again when the merging emitter +// selects stackless bodies). +test("a Promise.all tuple adapter requested in a range is defined", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-chunked-tuple-")); + try { + const entry = join(dir, "main.ts"); + await writeFile( + entry, + [ + "function both(a: Promise, b: Promise): Promise<[number, string]> {", + " return Promise.all([a, b]);", + "}", + 'both(Promise.resolve(1), Promise.resolve("x")).then((v) => console.log(v[0], v[1]));', + "", + ].join("\n"), + ); + const outPath = join(dir, "main.ir.json"); + const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" }); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + const ir = await readFile(outPath, "utf8"); + const serial = text(emitLlvmModuleSource(deserializeModule(ir), {})); + // The adapter is named from its key (contentSymbol). + expect(serial).toMatch(/define internal void @sc_all_tuple_\d+\(/); + const chunked = text(emitLlvmModuleSource(deserializeModule(ir), { functionChunks: 2 })); + expect(chunked === serial).toBe(true); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}, 60_000); + +// Debug builds journal their metadata requests (debug-info.ts): ranges +// merged in function order number every node as one emitter does. +const DEBUG_PROGRAMS: { entry: string; sources: string[] }[] = [ + { + entry: "3066-source-debugging/main.ts", + sources: ["3066-source-debugging/main.ts", "3066-source-debugging/helper.ts"], + }, + ...[ + "1020-async-basics.ts", + "7103-fiberless-plain-calls.ts", + "7110-als-completion-callbacks.ts", + "1301-errors-subclass.ts", + "1529-class-with-statics.ts", + "1591-js-closures.js", + "2010-generators-basics.ts", + "2013-generators-sent-values.ts", + "4451-int32-field-storage.ts", + ].map((program) => ({ entry: program, sources: [program] })), +]; + +test("debug builds emitted in ranges merge into the text one emitter gives", async () => { const dir = await mkdtemp(join(tmpdir(), "scriptc-chunked-debug-")); const minFunctions = process.env["SCRIPTC_EMIT_WORKER_FUNCTIONS"]; try { - const corpus = join(import.meta.dirname, "../../../tests/corpus/3066-source-debugging"); - const entry = join(corpus, "main.ts"); + const corpus = join(import.meta.dirname, "../../../tests/corpus"); + for (const program of DEBUG_PROGRAMS) { + const outPath = join(dir, "main.ir.json"); + const result = await compile(join(corpus, program.entry), { + outDir: dir, + outPath, + outputKind: "ir", + optimization: "dev", + }); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + const ir = await readFile(outPath, "utf8"); + const debugSources = new Map(); + for (const name of program.sources) + debugSources.set(join(corpus, name), await readFile(join(corpus, name), "utf8")); + const options: LlvmTargetOptions = { debugSources }; + const serial = text(emitLlvmModuleSource(deserializeModule(ir), options)); + expect(serial, program.entry).toContain("!DILocation"); + + // A fork that fails leaves every range to the merging emitter. + process.env["SCRIPTC_EMIT_WORKER_FUNCTIONS"] = "1"; + const forkPrimitives = { + fork: (): number => -1, + exit: (): void => {}, + wait: (): number => -1, + }; + for (const variant of [ + { functionChunks: 2 }, + { functionChunks: 5 }, + { functionWorkers: 4, forkPrimitives }, + ] satisfies LlvmTargetOptions[]) { + const emitted = text( + emitLlvmModuleSource(deserializeModule(ir), { ...options, ...variant }), + ); + expect(emitted === serial, `${program.entry} ${JSON.stringify(Object.keys(variant))}`).toBe( + true, + ); + const defined = new Set([...emitted.matchAll(/^(![0-9]+) = /gm)].map((m) => m[1])); + const referenced = new Set([...emitted.matchAll(/(![0-9]+)\b/g)].map((m) => m[1])); + expect([...referenced].filter((id) => !defined.has(id))).toEqual([]); + } + if (minFunctions === undefined) delete process.env["SCRIPTC_EMIT_WORKER_FUNCTIONS"]; + else process.env["SCRIPTC_EMIT_WORKER_FUNCTIONS"] = minFunctions; + } + } finally { + if (minFunctions === undefined) delete process.env["SCRIPTC_EMIT_WORKER_FUNCTIONS"]; + else process.env["SCRIPTC_EMIT_WORKER_FUNCTIONS"] = minFunctions; + await rm(dir, { recursive: true, force: true }); + } +}, 300_000); + +// A method name holding a double quote is written into a comment after the +// function's `!dbg` attachment; the rename of the range's debug ids reads +// comments and string literals as ending at the end of their line. +test("debug ids after a comment that holds a quote are renamed", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-chunked-debug-quote-")); + try { + const entry = join(dir, "main.ts"); + const source = [ + "class Greeter {", + " ['say \"hi'](n: number): number {", + " return n + 1;", + " }", + "}", + "const greeter = new Greeter();", + "function twice(x: number): number {", + " return x * 2;", + "}", + "function report(label: string, value: number): void {", + " console.log(label, value);", + "}", + 'report("method", greeter[\'say "hi\'](1));', + 'report("twice", twice(3));', + "", + ].join("\n"); + await writeFile(entry, source); const outPath = join(dir, "main.ir.json"); const result = await compile(entry, { outDir: dir, @@ -86,37 +214,16 @@ test("debug builds emit function bodies serially, with every metadata reference if (!result.ok) throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); const ir = await readFile(outPath, "utf8"); - const debugSources = new Map(); - for (const name of ["main.ts", "helper.ts"]) - debugSources.set(join(corpus, name), await readFile(join(corpus, name), "utf8")); - const options: LlvmTargetOptions = { debugSources }; + const options: LlvmTargetOptions = { debugSources: new Map([[entry, source]]) }; const serial = text(emitLlvmModuleSource(deserializeModule(ir), options)); - expect(serial).toContain("!DILocation"); - - let forks = 0; - process.env["SCRIPTC_EMIT_WORKER_FUNCTIONS"] = "1"; - const forkPrimitives = { - fork: (): number => { - forks++; - return -1; - }, - exit: (): void => {}, - wait: (): number => -1, - }; - for (const variant of [ - { functionChunks: 3 }, - { functionWorkers: 4, forkPrimitives }, - ] satisfies LlvmTargetOptions[]) { - const emitted = text(emitLlvmModuleSource(deserializeModule(ir), { ...options, ...variant })); - expect(emitted === serial, JSON.stringify(Object.keys(variant))).toBe(true); - const defined = new Set([...emitted.matchAll(/^(![0-9]+) = /gm)].map((m) => m[1])); - const referenced = new Set([...emitted.matchAll(/(![0-9]+)\b/g)].map((m) => m[1])); - expect([...referenced].filter((id) => !defined.has(id))).toEqual([]); + expect(serial).toMatch(/!dbg ![0-9]+ \{ ;[^\n]*"/); + for (const functionChunks of [2, 3]) { + const chunked = text( + emitLlvmModuleSource(deserializeModule(ir), { ...options, functionChunks }), + ); + expect(chunked === serial, `${functionChunks} chunks`).toBe(true); } - expect(forks).toBe(0); } finally { - if (minFunctions === undefined) delete process.env["SCRIPTC_EMIT_WORKER_FUNCTIONS"]; - else process.env["SCRIPTC_EMIT_WORKER_FUNCTIONS"] = minFunctions; await rm(dir, { recursive: true, force: true }); } -}, 120_000); +}, 60_000); diff --git a/packages/compiler/test/class-attributes.test.ts b/packages/compiler/test/class-attributes.test.ts index 1a6fea654..a0e6efa62 100644 --- a/packages/compiler/test/class-attributes.test.ts +++ b/packages/compiler/test/class-attributes.test.ts @@ -82,3 +82,33 @@ b.x = 2; expect(throwing.length).toBeGreaterThan(0); expect(throwing.every((name) => name.includes("constructor"))).toBe(true); }); + +test("an untyped freeze gives every class hierarchy attribute state", async () => { + const mod = await lowerModule(` +export {}; +class Point { x = 1; } +class Labeled extends Point { label = "a"; } +function freezeUnknown(value: unknown): unknown { return Object.freeze(value); } +function move(p: Point): void { p.x = 2; } +freezeUnknown(new Labeled()); +move(new Point()); +`); + expect(mod.untypedIntegrity).toBe(true); + expect(fieldsOf(mod, "Point")).toContain("%attributes"); + expect(fieldsOf(mod, "Labeled")).toContain("%attributes"); + expect(functionsCalling(mod, "class.throwReadOnly")).toContain("move"); +}); + +test("untyped freezes of fresh literals and tagged templates leave classes unchanged", async () => { + const mod = await lowerModule( + ` +class Point { constructor() { this.x = 1; } } +const tag = (strings) => strings.length; +const frozen = Object.freeze({ a: 1 }); +console.log(tag\`a\${1}b\`, frozen.a, new Point().x); +`, + "main.js", + ); + expect(mod.untypedIntegrity).toBeUndefined(); + expect(fieldsOf(mod, "Point")).not.toContain("%attributes"); +}); diff --git a/packages/compiler/test/class-method-mutations.test.ts b/packages/compiler/test/class-method-mutations.test.ts index b508a3949..166b9d556 100644 --- a/packages/compiler/test/class-method-mutations.test.ts +++ b/packages/compiler/test/class-method-mutations.test.ts @@ -135,3 +135,79 @@ test("a reflection call with a well-known symbol key keeps checked dispatch", as const callers = await callersOf(`${range}${write}`, "%Range.sym:iterator"); expect(callers.filter((name) => !name.startsWith("%method.value:"))).toEqual([]); }); + +const dictionaries = ` +class Counter { + n = 0; + add(): number { return ++this.n; } +} +interface Settings { groups: Record> } +interface Tree { [key: string]: Tree } +function addGroup(settings: Settings, name: string): void { + settings.groups[name] = {}; +} +function grow(tree: Tree, name: string): void { + tree[name] = {}; +} +const settings: Settings = { groups: {} }; +addGroup(settings, "colours"); +grow({}, "a"); +const counter = new Counter(); +console.log(counter.add(), Object.keys(settings.groups)); +`; + +test("a store into a dictionary of dictionaries leaves method calls direct", async () => { + expect(await callersOf(dictionaries, "%dyn.class.properties")).toEqual([]); +}); + +test("a store into a dictionary whose values can be functions keeps checked dispatch", async () => { + const source = dictionaries.replace( + "Record>", + "Record | (() => number)>", + ); + expect(await callersOf(source, "%dyn.class.properties")).not.toEqual([]); +}); + +test("a store into a generic dictionary of dictionaries keeps checked dispatch", async () => { + const source = dictionaries + .replace( + "interface Settings { groups: Record> }", + "interface Settings { groups: Record> }", + ) + .replace("function addGroup(settings: Settings,", "function addGroup(settings: Settings,") + .replace("const settings: Settings =", "const settings: Settings ="); + expect(await callersOf(source, "%dyn.class.properties")).not.toEqual([]); +}); + +test("a runtime-key store into a dictionary view of a class instance keeps checked dispatch", async () => { + const source = ` +class Counter { + n = 0; + tick(): number { return ++this.n; } +} +const counter = new Counter(); +const view: {} = counter; +const slots: Record = view; +const key: string = String(process.argv.length); +slots[key] = "replaced"; +console.log(counter.tick()); +`; + // The method has a slot the write can replace, which calls consult. + expect(await callersOf(source, "%Counter.tick")).toContain("%method.value:%Counter.tick"); +}); + +test("a dot store into a dictionary view of a class instance keeps checked dispatch", async () => { + const source = ` +class Lamp { + on = false; + toggle(): boolean { this.on = !this.on; return this.on; } +} +const lamp = new Lamp(); +const view: {} = lamp; +const slots: Record = view; +slots.toggle = "replaced"; +console.log(lamp.toggle()); +`; + // The method has a slot the write can replace, which calls consult. + expect(await callersOf(source, "%Lamp.toggle")).toContain("%method.value:%Lamp.toggle"); +}); diff --git a/packages/compiler/test/collection-keys.test.ts b/packages/compiler/test/collection-keys.test.ts index d73655ef1..ec049f23c 100644 --- a/packages/compiler/test/collection-keys.test.ts +++ b/packages/compiler/test/collection-keys.test.ts @@ -90,7 +90,7 @@ test("mixed-value unions select value equality while unsupported arms remain fen expect(mapKeyKindNum(union, [reference, scalar])).toBe(7); expect(isSupportedSetElem(union, [reference, scalar])).toBe(true); } - for (const unsupported of [{ kind: "date" } as IrType, mapOf(STRING, F64), setOf(STRING)]) { + for (const unsupported of [{ kind: "regex" } as IrType, mapOf(STRING, F64), setOf(STRING)]) { expect(isSupportedMapKey(unsupported)).toBe(false); } }); @@ -171,7 +171,7 @@ test.each(["map", "set"] as const)("validator checks the arms behind a %s key un expect(validateModule(mod)).toEqual([]); mod.unions![0]!.arms = [reference, F64]; expect(validateModule(mod)).toEqual([]); - mod.unions![0]!.arms = [reference, { kind: "date" }]; + mod.unions![0]!.arms = [reference, { kind: "regex" }]; expect( validateModule(mod).some((error) => error.message.includes( diff --git a/packages/compiler/test/compat-exports.test.ts b/packages/compiler/test/compat-exports.test.ts new file mode 100644 index 000000000..57ee41339 --- /dev/null +++ b/packages/compiler/test/compat-exports.test.ts @@ -0,0 +1,12 @@ +import { expect, test } from "vitest"; +import { + LIB_FN_SIGS, + libFnSignatures, + NODE24_FETCH_COMPAT_PROFILE, + node24FetchCompatProfile, +} from "../src/index.js"; + +test("the table constants name the tables their functions build", () => { + expect(LIB_FN_SIGS).toBe(libFnSignatures()); + expect(NODE24_FETCH_COMPAT_PROFILE).toBe(node24FetchCompatProfile()); +}); diff --git a/packages/compiler/test/completion-callbacks.test.ts b/packages/compiler/test/completion-callbacks.test.ts new file mode 100644 index 000000000..817472c19 --- /dev/null +++ b/packages/compiler/test/completion-callbacks.test.ts @@ -0,0 +1,60 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, expect, test } from "vitest"; +import { compile } from "../src/index.js"; + +const directories: string[] = []; +afterEach(async () => { + await Promise.all(directories.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))); +}); + +async function emit(source: string): Promise { + const outDir = await mkdtemp(join(tmpdir(), "scriptc-completion-callbacks-")); + directories.push(outDir); + const entry = join(outDir, "main.ts"); + await writeFile(entry, source); + const outPath = join(outDir, "program.ll"); + const result = await compile(entry, { entry, outDir, outPath, outputKind: "llvm" }); + expect(result.ok, JSON.stringify(result)).toBe(true); + return readFile(outPath, "utf8"); +} + +const randomBytes = ` +import { randomBytes } from "node:crypto"; +randomBytes(4, (error, buffer) => console.log(error === null, buffer.length)); +`; + +test("programs without AsyncLocalStorage pass completion callbacks unchanged", async () => { + const llvm = await emit(randomBytes); + expect(llvm).not.toContain("@scr_als_bind_closure"); + expect(llvm).not.toContain("sc_alsbound_"); +}); + +test("programs that enter stores bind each completion callback to the active context", async () => { + const llvm = await emit(` +import { AsyncLocalStorage } from "node:async_hooks"; +${randomBytes} +const store = new AsyncLocalStorage(); +store.run("a", () => randomBytes(1, (error, buffer) => console.log(store.getStore(), buffer.length))); +`); + const binds = llvm.match( + /call ptr @scr_als_bind_closure\(ptr [^,]+, ptr @sc_alsbound_\d+, i1 false\)/g, + ); + expect(binds?.length).toBe(2); + // One thunk per callback signature. + expect(llvm.match(/^define internal void @sc_alsbound_\d+\(/gm)?.length).toBe(1); +}); + +test("AsyncLocalStorage.bind binds a typed function with an empty context too", async () => { + const llvm = await emit(` +import { AsyncLocalStorage } from "node:async_hooks"; +const store = new AsyncLocalStorage(); +const read = AsyncLocalStorage.bind((n: number) => \`\${store.getStore()} \${n}\`); +console.log(store.run("x", () => read(1))); +`); + expect(llvm).toMatch( + /call ptr @scr_als_bind_closure\(ptr [^,]+, ptr @sc_alsbound_\d+, i1 true\)/, + ); + expect(llvm).toMatch(/^define internal ptr @sc_alsbound_\d+\(ptr %self, double %a0\)/m); +}); diff --git a/packages/compiler/test/constant-choice-emission.test.ts b/packages/compiler/test/constant-choice-emission.test.ts new file mode 100644 index 000000000..bbc54001a --- /dev/null +++ b/packages/compiler/test/constant-choice-emission.test.ts @@ -0,0 +1,64 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile, deserializeModule } from "../src/index.js"; +import { emitLlvmModule } from "../src/backend/llvm/emitter.js"; + +test("a chain of literal stores is one store of a table entry, indexed as on every path", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-constant-choices-")); + try { + const entry = join(dir, "main.ts"); + await writeFile( + entry, + ` +function bases(arr: Uint8Array): void { + for (let i = 0; i < arr.length; ++i) { + const x = arr[i]; + if (x === 97) arr[i] = 0; + else if (x === 116) arr[i] = 1; + else if (x === 99) arr[i] = 2; + else arr[i] = 3; + } +} +function wide(flag: boolean): string { + const a = new Uint8Array(4); + let j = 0; + if (flag) j = 4294967297; + if (j === 0) a[j] = 11; + else if (j === 1) a[j] = 22; + else if (j === 2) a[j] = 33; + else a[j] = 44; + return a.join(","); +} +const sequence = new Uint8Array([97, 116, 99, 103]); +bases(sequence); +console.log(sequence.join(","), wide(true), wide(false)); +`, + ); + const outPath = join(dir, "main.ir.json"); + const lowered = await compile(entry, { outDir: dir, outPath, outputKind: "ir" }); + if (!lowered.ok) + throw new Error(lowered.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + const ll = emitLlvmModule(deserializeModule(await readFile(outPath, "utf8"))); + const body = (name: string): string => + ll.match( + new RegExp(`define internal [^\\n]+ @sc_[bf]+_${name}\\([^\\n]*\\) #0 \\{[\\s\\S]*?\\n\\}`), + )![0]; + + // The cases' span, 97 to 116, with the final else's 3 between the cases. + expect(ll).toMatch( + /@sc_choice_\w+ = private unnamed_addr constant \[20 x i32\] \[i32 0, i32 3, i32 2, (i32 3, ){16}i32 1\]/, + ); + expect(body("bases")).toMatch(/load i32, ptr %\S+\n/); + + // The index is the compared local. Inside the first arm its range is + // [0, 0], but the one store also runs for 4294967297, so the index + // converts as the local's whole range allows (to i64), not to i32. + const wide = body("wide"); + expect(wide).toMatch(/@sc_choice_\w+, i64 0, i64/); + expect(wide).not.toMatch(/fptosi double %\S+ to i32/); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); diff --git a/packages/compiler/test/constant-graphs.test.ts b/packages/compiler/test/constant-graphs.test.ts new file mode 100644 index 000000000..a7cbe6118 --- /dev/null +++ b/packages/compiler/test/constant-graphs.test.ts @@ -0,0 +1,164 @@ +import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeEach, expect, test, vi } from "vitest"; +import { compile } from "../src/index.js"; + +// A library in the pre-module idiom: guarded namespace declarations, a +// revealing module, an ES5 constructor with prototype methods, and the +// cases that keep an object from being constant. +const packageSource = ` +var lib = lib || {}; +lib.math = lib.math || {}; +lib.math.TAU = 6.283185307179586; +lib.math.half = function (x) { return x / 2; }; +lib.text = (function () { + function upper(s) { return s.toUpperCase(); } + return { upper: upper }; +})(); +lib.Point = function (x, y) { this.x = x; this.y = y; }; +lib.Point.prototype.norm = function () { return Math.sqrt(this.x * this.x + this.y * this.y); }; +lib.counter = { hits: 0 }; +lib.bump = function () { lib.counter.hits++; }; +lib.cache = { table: null }; +lib.lookup = function () { return lib.cache.table || (lib.cache.table = [1, 2, 3]); }; +lib.scratch = { a: 1, b: 2 }; +delete lib.scratch.b; +lib.shared = { value: 1 }; +globalThis.sharedFromLib = lib.shared; +function option(name) { return { name: name }; } +lib.options = { first: option("a"), second: option("b") }; +module.exports = { lib }; +`; + +const mainSource = ` +const { lib } = require("ns-fixture"); +let total = 0; +for (let i = 0; i < 3; i++) { + total += lib.math.half(i) + new lib.Point(i, i).norm() + lib.lookup().length; + lib.bump(); +} +console.log(total, lib.text.upper("x"), lib.counter.hits); +`; + +let dir: string; +let saved: string | undefined; +beforeEach(async () => { + dir = await mkdtemp(join(tmpdir(), "scriptc-constant-graphs-")); + const pkg = join(dir, "node_modules", "ns-fixture"); + await mkdir(pkg, { recursive: true }); + await writeFile( + join(pkg, "package.json"), + JSON.stringify({ name: "ns-fixture", version: "1.0.0", main: "index.js", types: "index.d.ts" }), + ); + await writeFile(join(pkg, "index.js"), packageSource); + await writeFile(join(pkg, "index.d.ts"), "declare const lib: any;\nexport { lib };\n"); + await writeFile( + join(dir, "package.json"), + JSON.stringify({ dependencies: { "ns-fixture": "1.0.0" } }), + ); + await writeFile(join(dir, "main.cjs"), mainSource); + saved = process.env["SCRIPTC_DEBUG_SHAPES"]; +}); +afterEach(async () => { + if (saved === undefined) delete process.env["SCRIPTC_DEBUG_SHAPES"]; + else process.env["SCRIPTC_DEBUG_SHAPES"] = saved; + vi.restoreAllMocks(); + await rm(dir, { recursive: true, force: true }); +}); + +async function report(): Promise { + process.env["SCRIPTC_DEBUG_SHAPES"] = "const"; + const lines: string[] = []; + vi.spyOn(process.stderr, "write").mockImplementation((chunk: string | Uint8Array) => { + const text = typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8"); + for (const l of text.split("\n")) if (l.startsWith("[shapes] ")) lines.push(l.slice(9)); + return true; + }); + await compile(join(dir, "main.cjs"), { + outDir: dir, + outPath: join(dir, "main.ir.json"), + outputKind: "ir", + npmStatic: ["ns-fixture"], + }); + return lines; +} + +test("constant graphs: namespaces, revealing modules and prototypes, with near misses", async () => { + const lines = await report(); + const text = lines.join("\n"); + // The namespace graph: guarded declarations, the revealing module's + // table, and the constructor function. + expect(text).toMatch( + /graph ns-fixture\/index\.js:\d+ \{lib: \{math: \{TAU: number, half: fn\}, text: \{upper: fn\}/, + ); + // The constructor's prototype object, built by member stores at init. + expect(text).toMatch(/graph ns-fixture\/index\.js:\d+ \{constructor: \{\}, norm: fn\}/); + // A field written by a function called from a loop. + expect(text).toMatch(/near miss ns-fixture\/index\.js:12 \.hits: written-after-init/); + // A lazily filled cache. + expect(text).toMatch(/near miss ns-fixture\/index\.js:14 \.table: lazy-cache/); + // A deleted key, and an object stored into the global object. + expect(text).toMatch(/near miss ns-fixture\/index\.js:16: reflective-write \(delete/); + expect(text).toMatch(/near miss ns-fixture\/index\.js:18: escapes/); + // A factory called from two init sites: one object per call site. + expect(text).toMatch(/near miss ns-fixture\/index\.js:20: init-factory \(.*2 call sites\)/); +}); + +// Functions that one call site runs repeatedly: an ES5 constructor and an +// apply call in a loop, and builtin callbacks that run once per element, +// match or parsed value. +const repeatedSource = ` +var lib = {}; +lib.newState = { v: 0 }; +function bumpNew() { lib.newState.v++; } +bumpNew(); +lib.applyState = { v: 0 }; +function bumpApply() { lib.applyState.v++; } +bumpApply(); +lib.repeat = function () { for (var i = 0; i < 3; i++) { new bumpNew(); bumpApply.apply(null, []); } }; +lib.seen = { match: null, pair: null, mapped: null, key: null }; +lib.replaced = "a-b".replace(/\\\\w/g, function (m) { lib.seen.match = { m: m }; return m; }); +lib.sorted = [3, 1, 2].toSorted(function (a, b) { lib.seen.pair = { a: a }; return a - b; }); +lib.mapped = Array.from([1, 2], function (x) { lib.seen.mapped = { x: x }; return x; }); +lib.revived = JSON.parse('{"a":{"b":1}}', function (k, v) { lib.seen.key = { k: k }; return v; }); +module.exports = { lib }; +`; + +test("constant graphs: constructors, apply calls and callbacks that one site runs repeatedly", async () => { + await writeFile(join(dir, "node_modules", "ns-fixture", "index.js"), repeatedSource); + await writeFile( + join(dir, "main.cjs"), + 'const { lib } = require("ns-fixture");\nlib.repeat();\nconsole.log(lib.newState.v, lib.applyState.v, lib.replaced, lib.sorted, lib.mapped, lib.revived);\n', + ); + const text = (await report()).join("\n"); + // `new bumpNew()` and `bumpApply.apply(...)` in a loop write after init. + expect(text).toMatch(/near miss ns-fixture\/index\.js:3 \.v: written-after-init/); + expect(text).toMatch(/near miss ns-fixture\/index\.js:6 \.v: written-after-init/); + // Each callback runs once per match, comparison, element or parsed value. + for (const field of ["match", "pair", "mapped", "key"]) + expect(text).toMatch( + new RegExp(`near miss ns-fixture/index\\.js:10 \\.${field}: written-after-init`), + ); +}); + +// A prototype method stored under a computed key and called on instances. +const computedMethodSource = ` +var lib = {}; +lib.touched = { count: 0 }; +lib.Widget = function () {}; +var methodName = ["to", "uch"].join(""); +lib.Widget.prototype[methodName] = function () { lib.touched.count++; }; +lib.useWidget = function () { new lib.Widget().touch(); }; +module.exports = { lib }; +`; + +test("constant graphs: prototype methods stored under computed keys run on their instances", async () => { + await writeFile(join(dir, "node_modules", "ns-fixture", "index.js"), computedMethodSource); + await writeFile( + join(dir, "main.cjs"), + 'const { lib } = require("ns-fixture");\nfor (let i = 0; i < 3; i++) lib.useWidget();\nconsole.log(lib.touched.count);\n', + ); + const text = (await report()).join("\n"); + expect(text).toMatch(/near miss ns-fixture\/index\.js:3 \.count: written-after-init/); +}); diff --git a/packages/compiler/test/counted-loop-emission.test.ts b/packages/compiler/test/counted-loop-emission.test.ts index 7a3fc1f44..66d31e14b 100644 --- a/packages/compiler/test/counted-loop-emission.test.ts +++ b/packages/compiler/test/counted-loop-emission.test.ts @@ -1,4 +1,4 @@ -import { mkdtemp, readFile, rm } from "node:fs/promises"; +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { expect, test } from "vitest"; @@ -45,3 +45,343 @@ test("counted loops guard exact induction and preserve wide remainders on both p await rm(dir, { recursive: true, force: true }); } }); + +test("worker programs poll counted loops over typed arrays between blocks of iterations", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-counted-poll-")); + try { + const source = (workers: boolean): string => + [ + workers ? 'import { isMainThread, Worker } from "node:worker_threads";' : "", + "export function scale(a: Float64Array, b: Float64Array, n: number): void {", + " for (let i = 0; i < n; i++) b[i] = a[i]! * 2;", + "}", + "export function codes(a: Uint8Array): void {", + " for (let i = 0; i < a.length; ++i) a[i] = a[i] === 97 ? 0 : 3;", + "}", + "export function shrink(a: Uint8Array): number {", + " let t = 0;", + " for (let i = 0; i < a.length; ++i) {", + " t += a[i]!;", + " if (i === 2) a = a.subarray(0, 3);", + " }", + " return t;", + "}", + "export function mix(n: number): number {", + " let t = 0;", + " for (let i = 0; i < n; i++) t = (t * 31 + i) % 1000003;", + " return t;", + "}", + "const a = new Float64Array(8).fill(1);", + "const b = new Float64Array(8);", + "scale(a, b, 8);", + "const c = new Uint8Array(8).fill(97);", + "codes(c);", + "console.log(b[7], mix(8), c[0], shrink(new Uint8Array(8).fill(1)));", + workers ? "if (!isMainThread) new Worker(new URL(import.meta.url));" : "", + ].join("\n"); + const emit = async (workers: boolean): Promise => { + const entry = join(dir, `${workers ? "worker" : "plain"}.ts`); + await writeFile(entry, source(workers)); + const outPath = join(dir, `${workers ? "worker" : "plain"}.ir.json`); + const result = await compile(entry, { outPath, outDir: dir, outputKind: "ir" }); + if (!result.ok) throw new Error(result.diagnostics.map((d) => d.message).join("\n")); + return emitLlvmModule(deserializeModule(await readFile(outPath, "utf8"))); + }; + // Every emitted version of a function (the number and int32 entries). + const bodies = (ll: string, name: string): string => + [ + ...ll.matchAll( + new RegExp( + `define internal [^\\n]+ @sc_b?f_${name}(?:\\.i32)?\\([^\\n]*\\n([\\s\\S]*?)\\n\\}`, + "g", + ), + ), + ] + .map((match) => match[1]!) + .join("\n"); + const workerModule = await emit(true); + const worker = bodies(workerModule, "scale"); + const blocks = worker.split(/\n(?=[\w.]+:)/); + // No budget in the body: the inner loop's latch only steps the counter, + // the poll runs after each block, and the block's bound is the nearer of + // the limit and 64 steps on. + expect(worker).not.toContain("worker cancellation budget"); + expect(worker).toContain("@llvm.sadd.sat.i64(i64 %"); + expect(worker).toMatch(/@llvm\.smin\.i64\(i64 %\w+, i64 %\w+\)/); + for (const latch of blocks.filter((block) => block.startsWith("loop.u"))) + expect(latch).not.toContain("scr_exc_flag"); + const polls = blocks.filter((block) => block.startsWith("loop.poll")); + expect(polls.length).toBeGreaterThan(0); + for (const poll of polls) expect(poll).toContain("@scr_exc_flag"); + // A counted loop without typed-array accesses keeps the budget. + // A loop to a typed array's length is strip-mined the same way, with the + // length as each block's limit. + const codes = bodies(workerModule, "codes"); + expect(codes).not.toContain("worker cancellation budget"); + expect(codes).toMatch(/@llvm\.uadd\.sat\.i64\(i64 %\w+, i64 64\)/); + expect(codes).toMatch(/@llvm\.umin\.i64\(i64 %\w+, i64 %\w+\)/); + expect(codes).toMatch(/^loop\.poll\d+:$/m); + // A loop that assigns its array reads the new length on the next + // iteration: it keeps the budget. + const shrink = bodies(workerModule, "shrink"); + expect(shrink).toContain("worker cancellation budget"); + expect(shrink).not.toMatch(/^loop\.poll\d+:$/m); + const mix = bodies(workerModule, "mix"); + expect(mix).toContain("worker cancellation budget"); + expect(mix).not.toContain("loop.poll"); + const plain = bodies(await emit(false), "scale"); + expect(plain).not.toContain("loop.poll"); + expect(plain).not.toContain("@scr_exc_flag"); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); + +test("worker programs poll no loop whose proven ranges bound its runs inside another loop", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-bounded-loop-polls-")); + try { + const entry = join(dir, "bounded.ts"); + await writeFile( + entry, + [ + 'import { isMainThread, Worker } from "node:worker_threads";', + "const LIMIT = 50;", + "export function rows(cr: Float64Array, n: number): number {", + " let t = 0;", + " for (let r = 0; r < n; r++)", + " for (let i = 0; i < 8; i += 2) {", + " const c = cr[i]!;", + " for (let j = 0; j < LIMIT; j++) {", + " t = (t * 31 + j + c) % 1000003;", + " if (t === 7) break;", + " }", + " }", + " return t;", + "}", + "export function open(cr: Float64Array, n: number, k: number): number {", + " let t = 0;", + " for (let r = 0; r < n; r++)", + " for (let i = 0; i < 8; i += 2) {", + " const c = cr[i]!;", + " for (let j = 0; j < k; j++) {", + " t = (t * 31 + j + c) % 1000003;", + " if (t === 7) break;", + " }", + " }", + " return t;", + "}", + "export function wide(n: number): number {", + " let t = 0;", + " for (let r = 0; r < n; r++) for (let j = 0; j < 5000; j++) t = (t * 31 + j) % 1000003;", + " return t;", + "}", + "export function exact(n: number): number {", + " let t = 0;", + " for (let r = 0; r < n; r++)", + " for (let j = 9007199254740900; j < 9007199254740950; j++) t = (t + (j % 7)) % 1000003;", + " return t;", + "}", + "export function stalled(n: number): number {", + " let t = 0;", + " for (let r = 0; r < n; r++)", + " for (let j = 9007199254740990; j < 9007199254741002; j++) t = (t + (j % 7)) % 1000003;", + " return t;", + "}", + "export function single(): number {", + " let t = 0;", + " for (let j = 0; j < 50; j++) t = (t * 31 + j) % 1000003;", + " return t;", + "}", + "const cr = new Float64Array(8).fill(0.5);", + 'const k = Number(process.argv[2] ?? "50");', + "console.log(rows(cr, 3), open(cr, 3, k), wide(2), exact(2), stalled(0), single());", + "if (!isMainThread) new Worker(new URL(import.meta.url));", + ].join("\n"), + ); + const outPath = join(dir, "bounded.ir.json"); + const result = await compile(entry, { outPath, outDir: dir, outputKind: "ir" }); + if (!result.ok) throw new Error(result.diagnostics.map((d) => d.message).join("\n")); + const ll = emitLlvmModule(deserializeModule(await readFile(outPath, "utf8"))); + const body = (name: string): string => + [ + ...ll.matchAll( + new RegExp( + `define internal [^\\n]+ @sc_b?f_${name}(?:\\.i32)?\\([^\\n]*\\n([\\s\\S]*?)\\n\\}`, + "g", + ), + ), + ] + .map((match) => match[1]!) + .join("\n"); + const polls = (text: string): number => text.match(/@scr_exc_flag/g)?.length ?? 0; + // The i and j loops of `rows` run at most 4 × (1 + 50) bodies: only the + // r loop polls, and the i loop over the typed array is not strip-mined. + const rows = body("rows"); + expect(rows).not.toMatch(/^loop\.poll\d+:$/m); + expect(polls(rows)).toBeGreaterThan(0); + // With a limit known only at run time the inner loops keep their polls. + // The innermost one is strip-mined, and a block that ends at the limit + // leaves it without polling: only a block that ends short of it polls. + const open = body("open"); + expect(polls(open)).toBeGreaterThan(polls(rows)); + expect(open).toMatch(/^loop\.poll\d+:$/m); + const ends = [...open.matchAll(/^loop\.end\d+:$[\s\S]*?^\S/gm)].map((match) => match[0]); + expect(ends.length).toBeGreaterThan(0); + for (const end of ends) expect(end).toMatch(/br i1 %\w+, label %loop\.poll\d+, label %/); + // A run of up to 16384 iterations is one block, polled as it leaves when + // it took more than 64. + expect(open).toMatch(/^loop\.poll\.run\d+:$/m); + // Steps of 1 below 2^53 are exact, so 50 runs near it are bounded. From + // 2^53, `j + 1 === j`: a counter ranging up to it can run without end + // and keeps its poll. + expect(polls(body("exact"))).toBe(polls(body("single"))); + expect(polls(body("stalled"))).toBeGreaterThan(polls(body("exact"))); + // A nest over the bound, or a loop no other loop encloses, polls. + expect(polls(body("wide"))).toBeGreaterThan(0); + expect(body("wide")).toMatch(/^loop\.poll\d+:$/m); + expect(polls(body("single"))).toBeGreaterThan(0); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); + +test("worker programs read and write shared elements at a strip-mined loop's counter with plain accesses", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-counted-shared-")); + try { + const entry = join(dir, "shared.ts"); + await writeFile( + entry, + [ + 'import { isMainThread, Worker } from "node:worker_threads";', + "export function scale(a: Float64Array, b: Float64Array, n: number): void {", + " for (let i = 0; i < n; i++) b[i] = a[i]! * 2;", + "}", + "export function codes(a: Uint8Array): void {", + " for (let i = 0; i < a.length; ++i) a[i] = a[i] === 97 ? 0 : 3;", + "}", + "export function shift(a: Uint8Array): void {", + " for (let i = 0; i < a.length - 1; i++) a[i] = a[i + 1]!;", + "}", + "export function rows(a: Int32Array, n: number): number {", + " let t = 0;", + " for (let i = 0; i < n; i++) for (let j = 0; j < n; j++) t += a[i]! + a[j]!;", + " return t;", + "}", + "export function waits(a: Int32Array, out: Uint8Array, n: number): number {", + " let spins = 0;", + " for (let i = 0; i < n; i++) {", + " out[i] = 1;", + " while (a[i] === 0) spins++;", + " do spins++; while (a[i] === 1);", + " for (let k = 0; a[i]! > k; k++) spins++;", + " for (let k = 0; k < 3; k += a[i]!) spins++;", + " }", + " return spins;", + "}", + "export function spin(a: Int32Array): number {", + " let n = 0;", + " while (a[0] === 0 && n < 10) n++;", + " return n;", + "}", + "const a = new Float64Array(new SharedArrayBuffer(64)).fill(1);", + "const b = new Float64Array(8);", + "scale(a, b, 8);", + "const c = new Uint8Array(new SharedArrayBuffer(8)).fill(97);", + "codes(c);", + "shift(c);", + "const d = new Int32Array(new SharedArrayBuffer(16)).fill(1);", + "console.log(b[7], c[0], rows(d, 4), spin(d), waits(new Int32Array(new SharedArrayBuffer(16)).fill(2), new Uint8Array(new SharedArrayBuffer(4)), 4));", + "if (!isMainThread) new Worker(new URL(import.meta.url));", + ].join("\n"), + ); + const outPath = join(dir, "shared.ir.json"); + const result = await compile(entry, { outPath, outDir: dir, outputKind: "ir" }); + if (!result.ok) throw new Error(result.diagnostics.map((d) => d.message).join("\n")); + const ll = emitLlvmModule(deserializeModule(await readFile(outPath, "utf8"))); + const bodies = (name: string): string => + [ + ...ll.matchAll( + new RegExp( + `define internal [^\\n]+ @sc_b?f_${name}(?:\\.i32)?\\([^\\n]*\\n([\\s\\S]*?)\\n\\}`, + "g", + ), + ), + ] + .map((match) => match[1]!) + .join("\n"); + // Element loads (the poll's flag read is atomic too, from a global). + const elementLoads = (body: string): number => + body.match(/load atomic \w+, ptr %/g)?.length ?? 0; + // Elements at the counter of a strip-mined loop: plain loads and stores. + for (const name of ["scale", "codes"]) { + const body = bodies(name); + expect(body).toMatch(/^loop\.poll\d+:$/m); + expect(elementLoads(body)).toBe(0); + expect(body).not.toContain("store atomic"); + } + // `a[i + 1]` is not at the counter: its read stays atomic, the store + // at the counter is plain. + const shift = bodies("shift"); + expect(shift).toMatch(/load atomic i8, ptr %/); + expect(shift).not.toContain("store atomic"); + // The outer counter read inside the inner loop reads one element on every + // inner iteration: it stays atomic; the inner counter's read is plain. + const rows = bodies("rows"); + expect(rows).toMatch(/load atomic i32, ptr %/); + // A loop that is not counted keeps atomic reads. + expect(bodies("spin")).toMatch(/load atomic i32, ptr %/); + // A nested loop that tests the outer counter's element in its condition + // or steps by it reads it again on every iteration: each read stays + // atomic, so another thread's write ends the wait. + const waits = bodies("waits"); + expect(waits).toMatch(/^loop\.poll\d+:$/m); + expect(waits.match(/load atomic i32, ptr %/g)?.length).toBeGreaterThanOrEqual(4); + // The outer loop's write at its counter stays plain (speculation counters + // are atomic stores to globals). + expect(waits).not.toMatch(/store atomic i8 \S+, ptr %/); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); + +test("a loop that starts from a typed-array read guards the read's integer and ordered bit", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-counted-reads-")); + try { + const entry = join(dir, "chain.ts"); + await writeFile( + entry, + [ + "export function chain(pp: Int32Array): number {", + " let p0 = pp[0]!;", + " let n = 0;", + " while (pp[p0] !== 0) {", + " n++;", + " for (let i = 1, j = p0 - 1; i < j; i++, j--) {", + " const t = pp[i]!;", + " pp[i] = pp[j]!;", + " pp[j] = t;", + " }", + " const t = pp[p0]!;", + " pp[p0] = p0;", + " p0 = t;", + " }", + " return n;", + "}", + "console.log(chain(Int32Array.from([3, 1, 2, 0])));", + ].join("\n"), + ); + const outPath = join(dir, "chain.ir.json"); + const result = await compile(entry, { outPath, outDir: dir, outputKind: "ir" }); + if (!result.ok) throw new Error(result.diagnostics.map((d) => d.message).join("\n")); + const ll = emitLlvmModule(deserializeModule(await readFile(outPath, "utf8"))); + const body = ll.match(/define internal double @sc_bf_chain\([^\n]*\n([\s\S]*?)\n\}/)![1]!; + // `p0` holds a read's integer or NaN: it keeps whether it is NaN beside + // its integer view, and `p0 - 1` starts the loop from that integer. No + // test converts the double back to check it is an exact integer. + expect(body).toContain("alloca i1 ; ordered view p0"); + expect(body).toContain("counted.fast"); + expect(body).not.toContain("bitcast double"); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); diff --git a/packages/compiler/test/cpu-dispatch-emission.test.ts b/packages/compiler/test/cpu-dispatch-emission.test.ts new file mode 100644 index 000000000..170c28ede --- /dev/null +++ b/packages/compiler/test/cpu-dispatch-emission.test.ts @@ -0,0 +1,149 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { beforeAll, expect, test } from "vitest"; +import { compile, deserializeModule, type IrModule } from "../src/index.js"; +import { emitLlvmModule, type LlvmTargetOptions } from "../src/backend/llvm/emitter.js"; + +const program = ` +function dot(a: Float64Array, b: Float64Array): number { + let s = 0; + for (let i = 0; i < a.length; i++) s += a[i]! * b[i]!; + return s; +} +function scale(a: Float64Array, k: number): void { + for (let i = 0; i < a.length; i++) a[i] = a[i]! * k; +} +function positive(x: number): boolean { + return x > 0; +} +function check(x: number): number { + if (x < 0) throw new RangeError("negative"); + return Math.sqrt(x); +} +async function later(x: number): Promise { + await null; + return dot(new Float64Array([x]), new Float64Array([x])); +} +const a = new Float64Array(64).map((_, i) => i * 0.5); +scale(a, 1.25); +console.log(dot(a, a), [1, -2, 3].filter(positive).length); +try { + console.log(check(dot(a, a)), check(-1)); +} catch (error) { + console.log((error as Error).message); +} +later(3).then((value) => console.log(value)); +`; + +let mod: IrModule; + +beforeAll(async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-cpu-dispatch-")); + try { + const entry = join(dir, "main.ts"); + const outPath = join(dir, "main.ir.json"); + await writeFile(entry, program); + const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" }); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + mod = deserializeModule(await readFile(outPath, "utf8")); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}, 120_000); + +const x86 = { targetTriple: "x86_64-unknown-linux-gnu", unwindAbi: true } as const; + +function emit(options: LlvmTargetOptions): string { + return emitLlvmModule(mod, options); +} + +/** Each definition's text by name. */ +function definitions(ll: string): Map { + const out = new Map(); + for (const m of ll.matchAll(/^define [^\n]*?@([\w.$-]+)\([^]*?^}/gm)) out.set(m[1]!, m[0]); + return out; +} + +test("a dispatching x86-64 build gives each program function a baseline and an x86-64-v3 copy", () => { + const ll = emit({ ...x86, cpuDispatch: true }); + const defs = definitions(ll); + const cloned = [...defs.keys()] + .filter((name) => name.endsWith(".v3")) + .map((name) => name.slice(0, -3)); + expect(cloned.length).toBeGreaterThan(4); + for (const name of ["sc_bf_dot", "sc_bf_scale", "sc_f_positive", "sc_f_check"]) + expect( + cloned.some((c) => c.startsWith(name)), + name, + ).toBe(true); + const dispatched = cloned.filter((name) => defs.has(name)); + for (const name of cloned) { + const v3 = defs.get(`${name}.v3`)!; + expect(defs.get(`${name}.base`), name).toBeDefined(); + expect(v3.split("\n", 1)[0]).toMatch(/ #[34]( |$)/); + // The function's own name, where the module still uses it, dispatches + // on the level. + const dispatcher = defs.get(name); + if (dispatcher === undefined) { + expect(ll, name).not.toMatch(new RegExp(`@${name.replace(/[.$]/g, "\\$&")}(?![\\w.$-])`)); + continue; + } + expect(dispatcher).toContain("load i32, ptr @sc_cpu_level"); + expect(dispatcher).toContain(`musttail call`); + expect(dispatcher).toContain(`@${name}.v3(`); + expect(dispatcher).toContain(`@${name}.base(`); + // A copy calls the other cloned functions' copies of its own tier. + for (const call of v3.matchAll(/@([\w.$-]+)\(/g)) { + const callee = call[1]!; + expect(cloned.includes(callee), `${name}.v3 calls ${callee}`).toBe(false); + if (callee.endsWith(".base")) throw new Error(`${name}.v3 calls ${callee}`); + } + for (const call of defs.get(`${name}.base`)!.matchAll(/@([\w.$-]+)\(/g)) + expect(call[1]!.endsWith(".v3"), `${name}.base calls ${call[1]}`).toBe(false); + } + // The program's entry (called from main) and dot (called from the + // coroutine body) keep their dispatcher. Functions only the copies call + // have none, so their copies have the callers the original had. + expect(dispatched).toContain("sc_f__x25_main"); + expect(dispatched).toContain("sc_bf_dot"); + for (const name of ["sc_bf_scale", "sc_f_positive", "sc_f_check"]) + expect( + dispatched.filter((d) => d.startsWith(name)), + name, + ).toEqual([]); + // Coroutine bodies keep one body. + const coroutines = [...defs].filter(([, text]) => / #1 /.test(text.split("\n", 1)[0]!)); + expect(coroutines.length).toBeGreaterThan(0); + for (const [name] of coroutines) expect(defs.has(`${name}.v3`), name).toBe(false); + // Functions used as values are referenced by their dispatching name. + expect(ll).not.toMatch(/ptr @[\w.$-]+\.(v3|base)(?!\()/); + expect(ll).toMatch(/^attributes #3 = \{ sanitize_address "target-cpu"="x86-64-v3" \}$/m); + // Unsanitized builds leave sanitize_address out of the copies' groups too. + expect(emit({ ...x86, cpuDispatch: true, sanitizeAttributes: false })).toMatch( + /^attributes #3 = \{ "target-cpu"="x86-64-v3" \}$/m, + ); + // main sets the level before anything else runs. + expect(defs.get("main")).toMatch( + /^entry:\n(?: {2}call void @scr_runtime_abi\w*\(\)\n)? call void @sc_cpu_level_init\(\)$/m, + ); + // No copy contracts floating-point operations. + expect(ll).not.toMatch( + /\b(?:fadd|fsub|fmul|fdiv|frem|call) (?:[a-z]+ )*(?:contract|fast|afn|reassoc)\b/, + ); +}); + +test("builds without dispatch, for other targets or for a target level emit no copies", () => { + const plain = emit(x86); + expect(plain).not.toContain("@sc_cpu_level"); + expect(plain).not.toMatch(/\.v3\(/); + expect(emit({ ...x86, cpuDispatch: false })).toBe(plain); + const v3 = emit({ ...x86, targetCpu: "x86-64-v3" }); + expect(emit({ ...x86, targetCpu: "x86-64-v3", cpuDispatch: true })).toBe(v3); + for (const options of [ + { targetTriple: "aarch64-unknown-linux-gnu" }, + { targetTriple: "wasm32-wasi", wasi: true, pointerBits: 32 as const }, + ]) + expect(emit({ ...options, cpuDispatch: true })).toBe(emit(options)); +}); diff --git a/packages/compiler/test/date-time-values.test.ts b/packages/compiler/test/date-time-values.test.ts new file mode 100644 index 000000000..4e90ef3e6 --- /dev/null +++ b/packages/compiler/test/date-time-values.test.ts @@ -0,0 +1,100 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile, deserializeModule } from "../src/index.js"; +import { emitLlvmModule } from "../src/backend/llvm/emitter.js"; +import type { IrModule } from "../src/ir/ir.js"; + +async function lowerSource(source: string): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-date-time-values-")); + try { + const entry = join(dir, "main.ts"); + await writeFile(entry, source); + const outPath = join(dir, "main.ir.json"); + const result = await compile(entry, { outPath, outDir: dir, outputKind: "ir" }); + if (!result.ok) throw new Error(result.diagnostics.map((d) => d.message).join("\n")); + return deserializeModule(await readFile(outPath, "utf8")); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +/** The IR type of each local named `name` in function `fn`. */ +function localKinds(mod: IrModule, fn: string, name: string): string[] { + const f = mod.functions.find((candidate) => candidate.name === fn); + if (!f) throw new Error(`no function ${fn}`); + return f.locals.filter((local) => local.name === name).map((local) => local.type.kind); +} + +test("a Date local read only for its time value stores the time value", async () => { + const mod = await lowerSource(` +function format(ms: number): string { + const d = new Date(ms); + const later = new Date(ms + 1); + return d.toISOString() + String(d) + \`\${later}\` + (d < later) + d.getUTCHours() + new Date(d).getTime(); +} +console.log(format(0)); +`); + expect(localKinds(mod, "format", "d")).toEqual(["f64"]); + expect(localKinds(mod, "format", "later")).toEqual(["f64"]); + // Reading the time value creates no Date object. + const llvm = emitLlvmModule(mod, { pointerBits: 64 }); + const body = /^define internal [^\n]*@sc_f_format\([^]*?^}/m.exec(llvm)?.[0] ?? ""; + expect(body).not.toContain("@scr_date_new("); +}); + +test("a Date local whose identity a use can observe keeps the Date object", async () => { + const mod = await lowerSource(` +const keep: Date[] = []; +function uses(ms: number): boolean { + const compared = new Date(ms); + const stored = new Date(ms); + const passed = new Date(ms); + const captured = new Date(ms); + const returned = new Date(ms); + let reassigned = new Date(ms); + reassigned = new Date(ms + 1); + keep.push(stored); + const read = (): number => captured.getTime(); + console.log(String(passed), read(), returned.getTime(), reassigned.getTime()); + return compared === compared && pass(passed) === passed && back(returned) === returned; +} +function pass(d: Date): Date { + return d; +} +function back(d: Date): Date { + return d; +} +const top = new Date(0); +console.log(uses(0), top.getTime()); +`); + for (const name of ["compared", "stored", "passed", "captured", "returned", "reassigned"]) { + expect(localKinds(mod, "uses", name), name).toEqual(["date"]); + } +}); + +test("a Date in a block or loop at module level stores the time value unless a use observes it", async () => { + const mod = await lowerSource(` +const seen: Date[] = []; +let total = 0; +for (let i = 0; i < 3; i++) { + const read = new Date(i * 1000); + const kept = new Date(i * 1000); + seen.push(kept); + total += read.getUTCSeconds() + read.toISOString().length; +} +{ + const block = new Date(5); + const captured = new Date(6); + const later = (): number => captured.getTime(); + total += block.getTime() + later(); +} +const top = new Date(7); +console.log(total, seen.length, top.getTime()); +`); + expect(localKinds(mod, "%init.0", "read")).toEqual(["f64"]); + expect(localKinds(mod, "%init.0", "block")).toEqual(["f64"]); + expect(localKinds(mod, "%init.0", "kept")).toEqual(["date"]); + expect(localKinds(mod, "%init.0", "captured")).toEqual(["date"]); +}); diff --git a/packages/compiler/test/debug-variables.test.ts b/packages/compiler/test/debug-variables.test.ts index ea92cc1d3..946f0f5d1 100644 --- a/packages/compiler/test/debug-variables.test.ts +++ b/packages/compiler/test/debug-variables.test.ts @@ -63,6 +63,8 @@ test.runIf(process.platform === "darwin")( "continue", "frame variable item->tag item->slot.arm0", "continue", + "frame variable item->tag", + "continue", "frame variable hoisted", "continue", "frame variable moduleValue", @@ -91,6 +93,7 @@ test.runIf(process.platform === "darwin")( "item->tag = f64", "item->slot.arm0 = 6", "item->slot.arm0 = 7", + "item->tag = undefined", "hoisted = 13", ]) { expect(debug.stdout).toContain(value); diff --git a/packages/compiler/test/decl-origin-quoting.test.ts b/packages/compiler/test/decl-origin-quoting.test.ts new file mode 100644 index 000000000..40b351440 --- /dev/null +++ b/packages/compiler/test/decl-origin-quoting.test.ts @@ -0,0 +1,73 @@ +/* The suggested `scriptc types patch` command in a declared-type note + * (frontend/lowering/decl-origin.ts) passes each value it carries as one + * argument. Package names, selectors and types come from package + * declarations. Run through sh with printf in place of scriptc, the POSIX + * form prints each value exactly and creates no file. The Windows form + * double-quotes values that cmd and PowerShell keep as written there, and + * leaves the arguments out when either shell would expand or escape one. */ +import { execFile } from "node:child_process"; +import { mkdtemp, readdir, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { promisify } from "node:util"; +import { expect, test } from "vitest"; +import { patchSuggestion, shellArgument } from "../src/frontend/lowering/decl-origin.js"; + +const exec = promisify(execFile); + +const hostile = [ + "Profile.a;touch semicolon", + "Profile.$(touch subst)", + "Profile.`touch backtick`", + "Profile.it's \"quoted\" \\ end'", + "Profile.a&&touch and|touch pipe>out", + "Profile.a\nb", +]; + +test("plain values stay bare and types are quoted", () => { + expect(patchSuggestion("kit", "Profile.email", "| undefined", false)).toBe( + "if that declaration is wrong, patch it: scriptc types patch kit Profile.email '| undefined'", + ); + expect(patchSuggestion("@scope/kit", "Terminal.columns()", "| undefined", true)).toBe( + 'if that declaration is wrong, patch it: scriptc types patch @scope/kit "Terminal.columns()" "| undefined"', + ); +}); + +test.skipIf(process.platform === "win32")( + "the POSIX form passes each value as one argument", + async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-decl-origin-")); + try { + for (const selector of hostile) { + const pkg = "evil;touch pkg"; + const command = patchSuggestion(pkg, selector, "| undefined", false).replace( + "if that declaration is wrong, patch it: scriptc types patch", + "printf '%s\\n'", + ); + const { stdout } = await exec("sh", ["-c", command], { cwd: dir }); + expect(stdout).toBe(`${pkg}\n${selector}\n| undefined\n`); + expect(await readdir(dir)).toEqual([]); + } + } finally { + await rm(dir, { recursive: true, force: true }); + } + }, +); + +test("the Windows form double-quotes values only where both shells keep them", () => { + expect(shellArgument("Profile.a;b&c|df", true)).toBe('"Profile.a;b&c|df"'); + for (const value of [ + "Profile.$(x)", + "Profile.`x`", + 'Profile."x"', + "Profile.%PATH%", + "Profile.!x!", + "Profile.“x”", + "Profile.‘x’", + "Profile.a\nb", + ]) + expect(shellArgument(value, true), value).toBeNull(); + expect(patchSuggestion("kit", "Profile.$(x)", "| undefined", true)).toBe( + "if that declaration is wrong, patch it with scriptc types patch", + ); +}); diff --git a/packages/compiler/test/dictionary-views.test.ts b/packages/compiler/test/dictionary-views.test.ts new file mode 100644 index 000000000..a9cfd1056 --- /dev/null +++ b/packages/compiler/test/dictionary-views.test.ts @@ -0,0 +1,89 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile, deserializeModule } from "../src/index.js"; + +/** The IR type kind of each named module-level binding. */ +async function globalKinds(source: string): Promise> { + const dir = await mkdtemp(join(tmpdir(), "scriptc-dictionary-views-")); + try { + const entry = join(dir, "main.ts"); + const outPath = join(dir, "main.ir.json"); + await writeFile(entry, source); + const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" }); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + const mod = deserializeModule(await readFile(outPath, "utf8")); + return Object.fromEntries((mod.globals ?? []).map((g) => [g.name, g.type.kind])); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +const program = (body: string): string => ` +const key = String(process.argv.length); +const counts: Record = { a: 1 }; +counts[key] = 2; +${body} +console.log(JSON.stringify(counts)); +`; + +test("a record written through a dictionary keeps that dictionary type checked", async () => { + const kinds = await globalKinds( + program(`const point: { a: string } = { a: "x" }; +const table: Record = point; +table[key] = "y"; +console.log(JSON.stringify(point));`), + ); + expect(kinds["table"]).toBe("dyn"); + // Dictionaries of other value types keep their native layout. + expect(kinds["counts"]).toBe("record"); +}); + +test("a record reaching a dictionary through a {} parameter keeps it checked", async () => { + const kinds = await globalKinds( + program(`function stamp(value: {}): void { + const inner: Record = value; + inner[key] = "y"; +} +const point: { a: string } = { a: "x" }; +stamp(point); +const other: Record = { b: "z" }; +console.log(JSON.stringify(point), other["b"]);`), + ); + expect(kinds["other"]).toBe("dyn"); +}); + +test("dictionaries keep their native layout without a record flowing in", async () => { + const kinds = await globalKinds( + program(`const table: Record = { a: "x" }; +table[key] = "y"; +const copy: Record = table; +copy[key] = "z"; +console.log(JSON.stringify(table));`), + ); + expect(kinds["table"]).toBe("record"); + expect(kinds["copy"]).toBe("record"); +}); + +test("a record converted to a dictionary that is only read keeps the native layout", async () => { + const kinds = await globalKinds( + program(`const point: { a: string } = { a: "x" }; +const table: Record = point; +console.log(table["a"]);`), + ); + expect(kinds["table"]).toBe("record"); +}); + +test("a dictionary literal whose methods read this keeps checked identity", async () => { + const kinds = await globalKinds( + program(`interface Table { [key: string]: () => number } +const methods: Table = { + base() { return 2; }, + twice() { return this.base() * 2; }, +}; +console.log(methods.twice());`), + ); + expect(kinds["methods"]).toBe("dyn"); +}); diff --git a/packages/compiler/test/fixtures/ts7-protocols.json b/packages/compiler/test/fixtures/ts7-protocols.json new file mode 100644 index 000000000..e60dd5b8d --- /dev/null +++ b/packages/compiler/test/fixtures/ts7-protocols.json @@ -0,0 +1,18 @@ +{ + "//": "getSourceFile responses for the same sources from tsc 7.0.2 (protocol 5) and tsc 7.1.0-beta (protocol 9), base64, project at /project", + "sources": { + "main.ts": "/// \nimport type { Shape } from \"./other.js\";\nimport defer * as lazy from \"./other.js\";\nimport * as other from \"./other.js\";\nexport { other, lazy };\n/** A box.\n * @template T\n */\nexport abstract class Box extends other.Base {\n #secret = \"\\ud800\ufeff\";\n abstract method(...values: U[],): U;\n get value() { return this.#secret; }\n static { console.log(\"ready\"); }\n}\nexport namespace Space.Inner { export const n = 1; }\ndeclare module \"ambient\" { export const a: number; }\nexport type Values = readonly [name: string, count?: number, ...rest: boolean[]];\nexport type Keys = { readonly [K in keyof T as `p${K & string}`]?: T[K] };\nexport enum Color { Red, Green = 3, }\nexport function call(f: (a: number, b: string,) => void, ...args: [number, string]): void {\n f(...args,);\n const tagged = String.raw`a${args[0]}b\\n`;\n label: for (const x of [1, 2, 3,]) { if (x > 1) break label; }\n void (async () => { for await (const y of [] as AsyncIterable) y satisfies number; })();\n const o = { a: 1, b, ...{ c: 2 }, [Symbol.iterator]() { return [][Symbol.iterator](); }, };\n try { throw new Error(`x${o.a}`); } catch { } finally { }\n switch (o.a) { case 1: break; default: }\n let b = 0n ** 2n; b ??= 1n; b++;\n}\n", + "other.ts": "export interface Shape { kind: \"circle\" }\nexport class Base { }\n", + "view.tsx": "export const view = (items: string[]) =>
    {items.map((item) =>
  • {item}
  • )}<>
;\n" + }, + "protocol5": { + "main.ts": "AAAABY6wdLJEh2ZzyMQ0XA6mzLQAAAAALAAAAIwDAACpCAAAuQkAANAJAAAAAAAA6QQAAOkEAAD5BAAA+QQAAAkFAAAyAAAANwAAAEAAAABKAAAAXwAAAGMAAABqAAAAdAAAAIMAAACIAAAAjwAAAJkAAAClAAAAqgAAAKwAAACwAAAA6AAAAOsAAADsAAAA7QAAAPcAAAD8AAAA/QAAAAEBAAAGAQAADQEAAAkFAAAPBQAAKAEAAC4BAAAvAQAAMAEAADUBAAA7AQAAPQEAAD4BAABEAQAARQEAAE0BAABSAQAAYwEAAGoBAAB5AQAAgAEAAIEBAACEAQAAhgEAAIsBAAAPBQAAFQUAAMMAAADLAAAAzAAAAM0AAACkAQAAqQEAAKoBAACvAQAAvwEAAMABAADDAQAAxAEAANgBAADfAQAA8AEAAPEBAAAJAgAADwIAABwCAAAgAgAAKgIAAC8CAAA9AgAAQQIAAFsCAABfAgAAYAIAAGECAABxAgAAcgIAAHwCAAB9AgAAFQUAABYFAAAWBQAAFwUAAIUCAACGAgAAkAIAAJACAACQAgAAkAIAAJUCAACWAgAAlwIAAJgCAACpAgAArgIAALECAAC0AgAAtgIAALsCAAC+AgAAvwIAANMCAADXAgAA2AIAANkCAADcAgAA3QIAAOcCAADoAgAA/wIAAAMDAAAhAwAAIgMAACYDAAAqAwAANgMAADwDAAA/AwAARQMAAEYDAABJAwAAFwUAABgFAAAYBQAAGQUAAE0DAABRAwAAUgMAAFMDAAAZBQAAGwUAAFUDAABYAwAAXQMAAGIDAABvAwAAcAMAAHUDAAB2AwAAeAMAAHkDAAB7AwAAfAMAAIYDAACHAwAAigMAAIsDAACTAwAAmAMAAMMDAADEAwAAzgMAANsDAADlAwAA5gMAAAcEAAAIBAAADQQAAA4EAAAQBAAAEQQAABMEAAAUBAAAGwQAABwEAAAeBAAAHwQAACQEAAAqBAAAKwQAADMEAABDBAAASQQAAEoEAABSBAAAbwQAAHQEAAAbBQAAHAUAABwFAAAdBQAAeQQAAHoEAAB7BAAAfAQAAH0EAAB9BAAAfQQAAH0EAACjBAAApAQAAKUEAACmBAAArwQAALAEAADKBAAAywQAAM4EAADQBAAA1AQAANYEAADYBAAA2QQAAN4EAADgBAAA4gQAAOMEAAAvLy8gPHJlZmVyZW5jZSBwYXRoPSIuL290aGVyLnRzIiAvPgppbXBvcnQgdHlwZSB7IFNoYXBlIH0gZnJvbSAiLi9vdGhlci5qcyI7CmltcG9ydCBkZWZlciAqIGFzIGxhenkgZnJvbSAiLi9vdGhlci5qcyI7CmltcG9ydCAqIGFzIG90aGVyIGZyb20gIi4vb3RoZXIuanMiOwpleHBvcnQgeyBvdGhlciwgbGF6eSB9OwovKiogQSBib3guCiAqIEB0ZW1wbGF0ZSBUCiAqLwpleHBvcnQgYWJzdHJhY3QgY2xhc3MgQm94PFQ+IGV4dGVuZHMgb3RoZXIuQmFzZSB7CiAgI3NlY3JldCA9ICJcdWQ4MDDvu78iOwogIGFic3RyYWN0IG1ldGhvZDxVPiguLi52YWx1ZXM6IFVbXSwpOiBVOwogIGdldCB2YWx1ZSgpIHsgcmV0dXJuIHRoaXMuI3NlY3JldDsgfQogIHN0YXRpYyB7IGNvbnNvbGUubG9nKCJyZWFkeSIpOyB9Cn0KZXhwb3J0IG5hbWVzcGFjZSBTcGFjZS5Jbm5lciB7IGV4cG9ydCBjb25zdCBuID0gMTsgfQpkZWNsYXJlIG1vZHVsZSAiYW1iaWVudCIgeyBleHBvcnQgY29uc3QgYTogbnVtYmVyOyB9CmV4cG9ydCB0eXBlIFZhbHVlcyA9IHJlYWRvbmx5IFtuYW1lOiBzdHJpbmcsIGNvdW50PzogbnVtYmVyLCAuLi5yZXN0OiBib29sZWFuW11dOwpleHBvcnQgdHlwZSBLZXlzPFQ+ID0geyByZWFkb25seSBbSyBpbiBrZXlvZiBUIGFzIGBwJHtLICYgc3RyaW5nfWBdPzogVFtLXSB9OwpleHBvcnQgZW51bSBDb2xvciB7IFJlZCwgR3JlZW4gPSAzLCB9CmV4cG9ydCBmdW5jdGlvbiBjYWxsKGY6IChhOiBudW1iZXIsIGI6IHN0cmluZywpID0+IHZvaWQsIC4uLmFyZ3M6IFtudW1iZXIsIHN0cmluZ10pOiB2b2lkIHsKICBmKC4uLmFyZ3MsKTsKICBjb25zdCB0YWdnZWQgPSBTdHJpbmcucmF3YGEke2FyZ3NbMF19YlxuYDsKICBsYWJlbDogZm9yIChjb25zdCB4IG9mIFsxLCAyLCAzLF0pIHsgaWYgKHggPiAxKSBicmVhayBsYWJlbDsgfQogIHZvaWQgKGFzeW5jICgpID0+IHsgZm9yIGF3YWl0IChjb25zdCB5IG9mIFtdIGFzIEFzeW5jSXRlcmFibGU8bnVtYmVyPikgeSBzYXRpc2ZpZXMgbnVtYmVyOyB9KSgpOwogIGNvbnN0IG8gPSB7IGE6IDEsIGIsIC4uLnsgYzogMiB9LCBbU3ltYm9sLml0ZXJhdG9yXSgpIHsgcmV0dXJuIFtdW1N5bWJvbC5pdGVyYXRvcl0oKTsgfSwgfTsKICB0cnkgeyB0aHJvdyBuZXcgRXJyb3IoYHgke28uYX1gKTsgfSBjYXRjaCB7IH0gZmluYWxseSB7IH0KICBzd2l0Y2ggKG8uYSkgeyBjYXNlIDE6IGJyZWFrOyBkZWZhdWx0OiB9CiAgbGV0IGIgPSAwbiAqKiAybjsgYiA/Pz0gMW47IGIrKzsKfQovcHJvamVjdC9tYWluLnRzL3Byb2plY3QvbWFpbi50c+2ggO+7v0EgYm94LnBwYWFiCnh4AAAAAAIAAAAEAAAAAAAAAAMAAAAAAAAA//////////8RAAAAFQAAAP////8DAAAACAAAAAAAAAAMAAAAAAAAABAAAAAAAAAAIAAAAAAEAAA0AAAAAAAAAEIAAAAAAAAARAAAAAAAAABYAAAAWgAAAAAAAABeAAAAYAAAAAAAAABsAAAAAAAAAIIAAACEAAAAAAAAAIgAAAAAAAAAigAAAIwAAAAAAAAAkgAAAAAAAACUAAAAAAAAAJYAAAAAAAAAmgAAAAAAAACoAAAAAAAAAK4AAAAAAAAAugAAALwAAAAAAAAAwgAAAMQAAAAAAAAAygAAAAAAAADOAAAAAAAAANAAAAAAAAAA1AAAAAAAAACRlRUfqi4vb3RoZXIudHMAwpMJDhORbwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAzAQAAAAAAAOcEAAAAAAAAAAAAAAAAAIAAAAQA/////wAAAADmBAAAbQEAAAEAAAALAAAAAAAAABEBAAAAAAAATAAAAAoAAAACAAAABgAAAAAAAAASAQAAKgAAADkAAAAJAAAAAwAAAAIAAAEAAAAAFAEAAC8AAAA5AAAAAAAAAAQAAAABAAAAAAAAAP////8xAAAANwAAAAAAAAAFAAAAAQAAAAAAAAAVAQAAMQAAADcAAAAAAAAABgAAAAIAAAAAAAAATwAAADEAAAA3AAAAAAAAAAcAAAAGAABAAAAAAAoAAAA+AAAASwAAAAAAAAADAAAAMAAAgAAAAAARAQAATAAAAHYAAAAPAAAAAgAAAAYAAAAAAAAAEgEAAFMAAABjAAAADgAAAAoAAAACAAACAAAAABMBAABZAAAAYwAAAAAAAAALAAAAAQAAAAAAAABPAAAAXgAAAGMAAAAAAAAADAAAAAoAAEAAAAAACgAAAGgAAAB1AAAAAAAAAAoAAAA4AACAAAAAABEBAAB2AAAAmwAAABQAAAACAAAABgAAAAAAAAASAQAAfQAAAIgAAAATAAAADwAAAAIAAAAAAAAAEwEAAH0AAACIAAAAAAAAABAAAAABAAAAAAAAAE8AAACCAAAAiAAAAAAAAAARAAAADgAAQAAAAAAKAAAAjQAAAJoAAAAAAAAADwAAAEAAAIAAAAAAFwEAAJsAAACzAAAAGwAAAAIAAAACAAAAAAAAABgBAACiAAAAsgAAAAAAAAAUAAAAAQAAAAAgAAD/////pAAAALAAAAAAAAAAFQAAAAIAAAAAAAAAGgEAAKQAAACqAAAAGQAAABYAAAACAAAAACAAAE8AAACkAAAAqgAAAAAAAAAXAAAAEgAAQAAgAAAaAQAAqwAAALAAAAAAAAAAFgAAAAIAAAAAIAAATwAAAKsAAACwAAAAAAAAABkAAAAUAABAACAAAAgBAACzAAAAkAEAAFoAAAACAAAAHwAAAAAAIAD/////swAAAOEAAAAfAAAAGwAAAAIAAAAAAAAAXgAAALMAAADYAAAAHgAAABwAAAAAAAAAAAAAAH8AAADYAAAA4QAAAAAAAAAcAAAAAAAAAAAAAABPAAAA5wAAAOsAAAAgAAAAGwAAABYAAEAAAAAA/////+wAAADtAAAAIwAAABsAAAABAAAAAAAAAKkAAADsAAAA7QAAAAAAAAAgAAAAAgAAAAAAAABPAAAA7AAAAO0AAAAAAAAAIQAAABgAAEAAAAAA/////+4AAAABAQAAKgAAABsAAAABAAAAAAAAACsBAADuAAAAAQEAAAAAAAAjAAAAAQAAAAAgAAD/////9gAAAAEBAAAAAAAAJAAAAAEAAAAAAAAA6gAAAPYAAAABAQAAAAAAACUAAAABAAAAACAAANQAAAD2AAAAAQEAAAAAAAAmAAAABQAAAAAgAABPAAAA9gAAAPwAAAApAAAAJwAAABoAAEAAIAAATwAAAP0AAAABAQAAAAAAACcAAAAcAABAACAAAP////8DAQAAjgEAAFEAAAAbAAAABAAAAAAAAACtAAAAAwEAABoBAAAuAAAAKgAAABIAAAAAIAAAUAAAAAMBAAANAQAALQAAACsAAAAeAABAACAAAAoAAAAPAQAAGQEAAAAAAAArAAAASAAAgAAAAACvAAAAGgEAAEQBAAA+AAAAKgAAAHUAAAAAIAAA/////xoBAAAlAQAAMQAAAC4AAAABAAAAAAAAAH8AAAAaAQAAJQEAAAAAAAAvAAAAAAAAAAAgAABPAAAAJQEAACwBAAAyAAAALgAAACIAAEAAIAAA/////y0BAAAuAQAANQAAAC4AAAABAAAAAAAAAKkAAAAtAQAALgEAAAAAAAAyAAAAAgAAAAAgAABPAAAALQEAAC4BAAAAAAAAMwAAACQAAEAAIAAA/////zABAAA/AQAAPAAAAC4AAAABAAAAAAAAAKoAAAAwAQAAPgEAAAAAAAA1AAAAFgAAAAAAAAAZAAAAMAEAADMBAAA4AAAANgAAAAAAAAAAAAAATwAAADMBAAA5AQAAOQAAADYAAAAmAABAAAAAAL0AAAA6AQAAPgEAAAAAAAA2AAAAAQAAAAAAAAC4AAAAOgEAADwBAAAAAAAAOQAAAAEAAAAAAAAATwAAADoBAAA8AQAAAAAAADoAAAAoAABAAAAAALgAAABBAQAAQwEAAAAAAAAuAAAAAQAAAAAAAABPAAAAQQEAAEMBAAAAAAAAPAAAACoAAEAAAAAAsgAAAEQBAABrAQAARwAAACoAAAAqAAAAgCAAAE8AAABKAQAAUAEAAEAAAAA+AAAALAAAQAAgAAD/////UQEAAFEBAABBAAAAPgAAAAAAAAAAAAAA8gAAAFIBAABrAQAAAAAAAD4AAAABAAAAAAAAAP////9UAQAAaQEAAAAAAABBAAAAAQAAAAAAAAD+AAAAVAEAAGkBAAAAAAAAQgAAAAEAAAAAAAAA1AAAAFsBAABoAQAAAAAAAEMAAAAFAAAAAAAAAG0AAABbAQAAYAEAAEYAAABEAAAAAAAAAAAAAABQAAAAYQEAAGgBAAAAAAAARAAAAC4AAEAAAAAAsAAAAGsBAACOAQAAAAAAACoAAAACAAAAACAAAPIAAAB0AQAAjgEAAAAAAABHAAAAAQAAAAAgAAD/////dgEAAIwBAAAAAAAASAAAAAEAAAAAAAAA9QAAAHYBAACMAQAAAAAAAEkAAAABAAAAACAAANYAAAB2AQAAiwEAAAAAAABKAAAACQAAAAAgAADUAAAAdgEAAIIBAABPAAAASwAAAAUAAAAAIAAATwAAAHYBAAB+AQAATgAAAEwAAAAwAABAACAAAE8AAAB/AQAAggEAAAAAAABMAAAAMgAAQAAgAAD/////gwEAAIoBAAAAAAAASwAAAAEAAAAAAAAACgAAAIMBAACKAQAAAAAAAE8AAABQAACAACAAADsBAACzAAAA0QAAAAAAAAAbAAAAAwAAAAAAQAD/////tAAAAMIAAABUAAAAUQAAAAEAAAAAAAAAPAEAALQAAADCAAAAAAAAAFIAAAA2AABAAABAAP/////CAAAAzwAAAAAAAABRAAAAAQAAAAAAAABRAQAAwgAAAM8AAAAAAAAAVAAAAAUAAAAAAEAATwAAAMMAAADLAAAAVwAAAFUAAAA4AABAAABAAP////8AAAAAAAAAAAAAAABVAAAAAQAAAAAAAACpAAAAzAAAAM0AAAAAAAAAVwAAAAIAAAAAAEAATwAAAMwAAADNAAAAAAAAAFgAAAA6AABAAABAAAwBAACQAQAAxQEAAGwAAAACAAAABwAAAQAAAAD/////kAEAAJcBAABdAAAAWgAAAAEAAAAAAAAAXgAAAJABAACXAQAAAAAAAFsAAAAAAAAAAAAAAE8AAAChAQAApwEAAF4AAABaAAAAPAAAQAAAAAAMAQAAqAEAAMUBAAAAAAAAWgAAAAcAAAEAAAAA/////6gBAACoAQAAYQAAAF4AAAABAAAAAAAAAF4AAACoAQAAqAEAAAAAAABfAAAAAAAAAAgAAABPAAAAqAEAAK0BAABiAAAAXgAAAD4AAEAAAAAADQEAAK0BAADFAQAAAAAAAF4AAAABAAAAAAAAAP////+vAQAAwwEAAAAAAABiAAAAAQAAAAAAAAD0AAAArwEAAMMBAAAAAAAAYwAAAAMAAAAAAAAA/////68BAAC2AQAAZwAAAGQAAAABAAAAAAAAAF4AAACvAQAAtgEAAAAAAABlAAAAAAAAAAAAAAAGAQAAtgEAAMIBAAAAAAAAZAAAAAEAAAACAAAA/////7wBAADCAQAAAAAAAGcAAAABAAAAAAAAAAUBAAC8AQAAwgEAAAAAAABoAAAACQAAAAAAAABPAAAAvAEAAL4BAABrAAAAaQAAAEAAAEAAAAAACAAAAMABAADCAQAAAAAAAGkAAABYAACAAAAAAAwBAADFAQAA+gEAAHoAAAACAAAABwAAAEAAgAD/////xQEAAM0BAABvAAAAbAAAAAEAAAAAAAAAiQAAAMUBAADNAQAAAAAAAG0AAAAAAAAAAACAAAoAAADUAQAA3gEAAHAAAABsAAAAYAAAgAAAgAANAQAA3gEAAPoBAAAAAAAAbAAAAAEAAAAAAIAA/////+ABAAD4AQAAAAAAAHAAAAABAAAAAAAAAPQAAADgAQAA+AEAAAAAAABxAAAAAwAAAAAAgAD/////4AEAAOcBAAB1AAAAcgAAAAEAAAAAAAAAXgAAAOABAADnAQAAAAAAAHMAAAAAAAAAAACAAAYBAADnAQAA9wEAAAAAAAByAAAAAQAAAAIAgAD/////7QEAAPcBAAAAAAAAdQAAAAEAAAAAAAAABQEAAO0BAAD3AQAAAAAAAHYAAAAFAAAAAACAAE8AAADtAQAA7wEAAHkAAAB3AAAARgAAQAAAgACWAAAA8AEAAPcBAAAAAAAAdwAAAAAAAAAAAIAACgEAAPoBAABMAgAAjQAAAAIAAAALAAAAAAAAAP/////6AQAAAQIAAH0AAAB6AAAAAQAAAAAAAABeAAAA+gEAAAECAAAAAAAAewAAAAAAAAAAAAAATwAAAAYCAAANAgAAfgAAAHoAAABIAABAAAAAAMcAAAAPAgAASwIAAAAAAAB6AAAAAQAAAQAAAAC+AAAAGAIAAEsCAAAAAAAAfgAAAAEAAAAAAAAA/////xoCAABKAgAAAAAAAH8AAAADAAAAAAAAAMsAAAAaAgAAJgIAAIQAAACAAAAACgAAAAAAAABPAAAAGgIAAB4CAACDAAAAgQAAAEoAAEAAAAAAmgAAAB8CAAAmAgAAAAAAAIEAAAAAAAAAAAAAAMsAAAAnAgAANgIAAIgAAACAAAAADgAAAAAAAABPAAAAJwIAAC0CAACGAAAAhAAAAEwAAEAAAAAAOQAAAC0CAAAuAgAAhwAAAIQAAAAAAAAAAAAAAJYAAAAvAgAANgIAAAAAAACEAAAAAAAAAAAAAADLAAAANwIAAEoCAAAAAAAAgAAAAAsAAAAAAAAAGQAAADcCAAA7AgAAigAAAIgAAAAAAAAAAAAAAE8AAAA7AgAAPwIAAIsAAACIAAAATgAAQAAAAAC9AAAAQAIAAEoCAAAAAAAAiAAAAAEAAAAAAAAAhwAAAEACAABIAgAAAAAAAIsAAAAAAAAAAAAAAAoBAABMAgAAmgIAAKwAAAACAAAADwAAAAAAAAD/////TAIAAFMCAACQAAAAjQAAAAEAAAAAAAAAXgAAAEwCAABTAgAAAAAAAI4AAAAAAAAAAAAAAE8AAABYAgAAXQIAAJEAAACNAAAAUAAAQAAAAAD/////XgIAAF8CAACUAAAAjQAAAAEAAAAAAAAAqQAAAF4CAABfAgAAAAAAAJEAAAACAAAAAAAAAE8AAABeAgAAXwIAAAAAAACSAAAAUgAAQAAAAADJAAAAYgIAAJkCAAAAAAAAjQAAAD8AAAAAAAAAlAAAAGQCAABtAgAAlgAAAJQAAAAAAAAAAAAAAKkAAABvAgAAewIAAJsAAACUAAAABgAAAAAAAABPAAAAbwIAAHACAACYAAAAlgAAAFQAAEAAAAAAxwAAAHMCAAB7AgAAAAAAAJYAAAABAAAAAAAAALgAAAB5AgAAewIAAAAAAACYAAAAAQAAAAAAAABPAAAAeQIAAHsCAAAAAAAAmQAAAFYAAEAAAAAAzAAAAH4CAACPAgAApQAAAJQAAAADAAAAAAAAAA8AAAB+AgAAgwIAAJ0AAACbAAAAaAAAgAAAAAD/////gwIAAI8CAAAAAAAAmwAAAAEAAAAAAAAAzQAAAIMCAACPAgAAAAAAAJ0AAAADAAAAAAAAAMIAAACDAgAAjQIAAKQAAACeAAAAAQAAAAAAAAD/////gwIAAI0CAAAAAAAAnwAAAAIAAAAAAAAAuAAAAIMCAACEAgAAowAAAKAAAAABAAAAAAAAAE8AAACDAgAAhAIAAAAAAAChAAAAXAAAQAAAAACaAAAAhgIAAI0CAAAAAAAAoAAAAAAAAAAAAAAAEQAAAI0CAACPAgAAAAAAAJ4AAAB0AACAAAAAADkAAACQAgAAkQIAAKYAAACUAAAAAAAAAAAAAADIAAAAkgIAAJcCAACrAAAAlAAAAAMAAAAAAAAAuAAAAJICAACUAgAAqQAAAKYAAAABAAAAAAAAAE8AAACSAgAAlAIAAAAAAACnAAAAYgAAQAAAAAC4AAAAlQIAAJYCAAAAAAAApgAAAAEAAAAAAAAATwAAAJUCAACWAgAAAAAAAKkAAABkAABAAAAAAP////+XAgAAlwIAAAAAAACUAAAAAAAAAAAAAAALAQAAmgIAAMACAAC2AAAAAgAAAAcAAAAAAAAA/////5oCAAChAgAArwAAAKwAAAABAAAAAAAAAF4AAACaAgAAoQIAAAAAAACtAAAAAAAAAAAAAABPAAAApgIAAKwCAACwAAAArAAAAGYAAEAAAAAA/////64CAAC+AgAAAAAAAKwAAAACAAAAAAAAADIBAACuAgAAsgIAALMAAACwAAAAAQAAAAAAAABPAAAArgIAALICAAAAAAAAsQAAAGgAAEAAAAAAMgEAALMCAAC9AgAAAAAAALAAAAADAAAAAAAAAE8AAACzAgAAuQIAALUAAACzAAAAagAAQAAAAAAIAAAAuwIAAL0CAAAAAAAAswAAAIAAAIAAAAAABwEAAMACAADmBAAAAAAAAAIAAAB1AAAAAAEAAP/////AAgAAxwIAALkAAAC2AAAAAQAAAAAAAABeAAAAwAIAAMcCAAAAAAAAtwAAAAAAAAAAAAAATwAAANACAADVAgAAugAAALYAAABuAABAAAAAAP/////WAgAAEwMAAM0AAAC2AAAAAgAAAAAAAACqAAAA1gIAAPgCAADGAAAAugAAABQAAAAAAAAATwAAANYCAADXAgAAvQAAALsAAABwAABAAAAAALkAAADYAgAA+AIAAAAAAAC7AAAABgAAAAAAAAD/////2gIAAO8CAADFAAAAvQAAAAIAAAAAAAAAqgAAANoCAADjAgAAwgAAAL4AAAAUAAAAAAAAAE8AAADaAgAA2wIAAMEAAAC/AAAAcgAAQAAAAACWAAAA3AIAAOMCAAAAAAAAvwAAAAAAAAAAAAAAqgAAAOQCAADuAgAAAAAAAL4AAAAUAAAAAAAAAE8AAADkAgAA5gIAAMQAAADCAAAAdAAAQAAAAACaAAAA5wIAAO4CAAAAAAAAwgAAAAAAAAAAAAAAcwAAAPMCAAD4AgAAAAAAAL0AAAAAAAAAAAAAAKoAAAD5AgAAEwMAAAAAAAC6AAAAFgAAAAAAAAAZAAAA+QIAAP0CAADIAAAAxgAAAAAAAAAAAAAATwAAAP0CAAABAwAAyQAAAMYAAAB2AABAAAAAAL4AAAACAwAAEwMAAAAAAADGAAAAAQAAAAAAAAD/////BAMAABIDAAAAAAAAyQAAAAIAAAAAAAAAlgAAAAQDAAAKAwAAzAAAAMoAAAAAAAAAAAAAAJoAAAALAwAAEgMAAAAAAADKAAAAAAAAAAAAAABzAAAAFQMAABoDAADOAAAAtgAAAAAAAAAAAAAA8gAAABoDAADmBAAAAAAAALYAAAABAAABAAAAAP////8cAwAA5AQAAAAAAADOAAAACgAAAAAAAAD1AAAAHAMAACsDAADWAAAAzwAAAAEAAAAAAAAA1gAAABwDAAAqAwAAAAAAANAAAAAJAAAAAAAAAE8AAAAcAwAAIAMAANMAAADRAAAAeAAAQAAAAAD/////IQMAACkDAAAAAAAA0QAAAAEAAAAAAAAA5wAAACEDAAAoAwAAAAAAANMAAAABAAAAAAAAAE8AAAAkAwAAKAMAAAAAAADUAAAAegAAQAAAAAD0AAAAKwMAAFgDAADnAAAAzwAAAAIAAAAAAAAABgEAACsDAABXAwAAAAAAANYAAAABAAAAAgAAAP////8zAwAAVwMAAAAAAADXAAAAAQAAAAAAAAAFAQAAMwMAAFcDAAAAAAAA2AAAAAkAAAAAAAAATwAAADMDAAA6AwAA2wAAANkAAAB8AABAAAAAANgAAAA8AwAAVwMAAAAAAADZAAAACQAAAAAAAADUAAAAPAMAAEcDAADfAAAA2wAAAAUAAAAAAAAATwAAADwDAABDAwAA3gAAANwAAAB+AABAAAAAAE8AAABEAwAARwMAAAAAAADcAAAAgAAAQAAAAADlAAAARwMAAFcDAAAAAAAA2wAAAAMAAAAAAAAADwAAAEcDAABLAwAA4QAAAN8AAACIAACAAAAAAP////9LAwAAVwMAAAAAAADfAAAAAQAAAAAAAADwAAAASwMAAFcDAAAAAAAA4QAAAAMAAAAAAAAA1QAAAEsDAABSAwAA5gAAAOIAAAAFAAAAAAAAAE8AAABLAwAATwMAAOUAAADjAAAAhgAAQAAAAAAIAAAAUAMAAFEDAAAAAAAA4wAAAJQAAIAAAAAAEQAAAFIDAABXAwAAAAAAAOIAAACcAACAAAAAAAEBAABYAwAAmQMAAPwAAADPAAAAAwAAAAAAAABPAAAAWAMAAGADAADpAAAA5wAAAI4AAEAAAAAA+wAAAGEDAACZAwAAAAAAAOcAAAAOAAAAAAAAAAYBAABnAwAAbgMAAO4AAADpAAAAAQAAAAIAAAD/////bAMAAG4DAAAAAAAA6gAAAAEAAAAAAAAABQEAAGwDAABuAwAAAAAAAOsAAAABAAAAAAQAAE8AAABsAwAAbgMAAAAAAADsAAAAkAAAQAAEAADSAAAAcQMAAHwDAADzAAAA6QAAAAEAAAAAAAAA/////3MDAAB7AwAAAAAAAO4AAAADAAAAAAAAAAgAAABzAwAAdAMAAPEAAADvAAAAqAAAgAAAAAAIAAAAdQMAAHcDAADyAAAA7wAAALAAAIAAAAAACAAAAHgDAAB6AwAAAAAAAO8AAAC4AACAAAAAAPIAAAB9AwAAmQMAAAAAAADpAAAAAQAAAAAAAAD/////fwMAAJcDAAAAAAAA8wAAAAEAAAAAAAAA9gAAAH8DAACXAwAAAAAAAPQAAAADAAAAAAAAAOMAAACEAwAAiQMAAPoAAAD1AAAAGgAAAAAAAABPAAAAhAMAAIUDAAD4AAAA9gAAAJgAAEAAAAAAHwAAAIUDAACHAwAA+QAAAPYAAAAAAAAAAAAAAAgAAACHAwAAiQMAAAAAAAD2AAAAwAAAgAAAAAD9AAAAigMAAJcDAAAAAAAA9QAAAAEAAAAAAAAATwAAAJADAACWAwAAAAAAAPoAAACcAABAAAAAAPUAAACZAwAA/AMAABkBAADPAAAAAQAAAAAAAADfAAAAmQMAAPsDAAAAAAAA/AAAAAEAAAAAAAAA1gAAAKADAAD7AwAAAAAAAP0AAAAJAAAAAAAAANoAAACgAwAA+QMAABgBAAD+AAAAAQAAAAAAAADcAAAAogMAAPgDAAAAAAAA/wAAADUAAAAAAQAA/////6IDAACnAwAAAwEAAAABAAABAAAAAAAAAIUAAACiAwAApwMAAAAAAAABAQAAAAAAAAAAAAD/////qQMAAKkDAAAEAQAAAAEAAAAAAAAAAAAAJgAAAKoDAACtAwAABQEAAAABAAAAAAAAAAAAAPIAAACtAwAA+AMAAAAAAAAAAQAAAQAAAAAgAAD/////rwMAAPYDAAAAAAAABQEAAAEAAAAAAAAA+wAAAK8DAAD2AwAAAAAAAAYBAAAPAAAAACAAAIYAAACzAwAAuQMAAAkBAAAHAQAAAAAAAAAgAAAGAQAAuwMAAMIDAAANAQAABwEAAAEAAAACIAAA/////8ADAADCAwAAAAAAAAkBAAABAAAAAAAAAAUBAADAAwAAwgMAAAAAAAAKAQAAAQAAAAAkAABPAAAAwAMAAMIDAAAAAAAACwEAAJ4AAEAAJAAA6wAAAMUDAADhAwAAFAEAAAcBAAADAAAAACAAANIAAADFAwAAyAMAABABAAANAQAAAQAAAAAgAAD/////xwMAAMcDAAAAAAAADgEAAAAAAAAAAAAAuAAAAMsDAADhAwAAAAAAAA0BAAADAAAAAAAAAE8AAADLAwAA2QMAABIBAAAQAQAAoAAAQAAAAAD/////2gMAAOADAAAAAAAAEAEAAAEAAAAAAAAAlgAAANoDAADgAwAAAAAAABIBAAAAAAAAAAAAAPUAAADiAwAA9gMAAAAAAAAHAQAAAQAAAAAgAADvAAAA4gMAAPUDAAAAAAAAFAEAAAMAAAAAIAAATwAAAOIDAADkAwAAFwEAABUBAACiAABAACAAAJYAAADuAwAA9QMAAAAAAAAVAQAAAAAAAAAAAAD/////+gMAAPoDAAAAAAAA/gAAAAAAAAAAAAAA9AAAAPwDAABaBAAAPAEAAM8AAAACAAAAAAAAAAYBAAD8AwAAWQQAAAAAAAAZAQAAAQAAAAIAAAD/////BAQAAFkEAAAAAAAAGgEAAAEAAAAAAAAABQEAAAQEAABZBAAAAAAAABsBAAAJAAAAAAAAAE8AAAAEBAAABgQAAB4BAAAcAQAApAAAQAAAAADTAAAACAQAAFkEAAAAAAAAHAEAAAEAAAAAAAAA/////woEAABXBAAAAAAAAB4BAAAEAAAAAAAAAC8BAAAKBAAADwQAACMBAAAfAQAAEgAAAAAAAABPAAAACgQAAAwEAAAiAQAAIAEAAKYAAEAAAAAACAAAAA0EAAAPBAAAAAAAACABAADIAACAAAAAADABAAAQBAAAEgQAACUBAAAfAQAAAgAAAAAAAABPAAAAEAQAABIEAAAAAAAAIwEAAKoAAEAAAAAAMQEAABMEAAAfBAAAKwEAAB8BAAABAAAAAAAAANMAAAAXBAAAHwQAAAAAAAAlAQAAAQAAAAAAAAD/////GAQAAB0EAAAAAAAAJgEAAAEAAAAAAAAALwEAABgEAAAdBAAAAAAAACcBAAASAAAAAAAAAE8AAAAYBAAAGgQAACoBAAAoAQAArAAAQAAAAAAIAAAAGwQAAB0EAAAAAAAAKAEAANAAAIAAAAAArwAAACAEAABWBAAAAAAAAB8BAACkAAAAAAAAAKgAAAAgBAAAMgQAADABAAArAQAAAQAAAAAAAADUAAAAIgQAADEEAAAAAAAALAEAAAUAAAAAAAAATwAAACIEAAAoBAAALwEAAC0BAACwAABAAAAAAE8AAAApBAAAMQQAAAAAAAAtAQAAsgAAQAAAAAD/////MwQAADMEAAAxAQAAKwEAAAAAAAAAAAAA8gAAADQEAABWBAAAAAAAACsBAAABAAAAAAAAAP////82BAAAVAQAAAAAAAAxAQAAAQAAAAAAAAD+AAAANgQAAFQEAAAAAAAAMgEAAAEAAAAAAAAA1gAAAD0EAABTBAAAAAAAADMBAAAJAAAAAAAAANUAAAA9BAAAUQQAADsBAAA0AQAABQAAAAAAAADSAAAAPQQAAEAEAAA4AQAANQEAAAEAAAAAAAAA/////z8EAAA/BAAAAAAAADYBAAAAAAAAAAAAANQAAABBBAAAUAQAAAAAAAA1AQAABQAAAAAAAABPAAAAQQQAAEcEAAA6AQAAOAEAALQAAEAAAAAATwAAAEgEAABQBAAAAAAAADgBAAC2AABAAAAAAP////9SBAAAUgQAAAAAAAA0AQAAAAAAAAAAAAADAQAAWgQAAJYEAABQAQAAzwAAAAcAAAAAAAAA8gAAAGAEAACABAAASwEAADwBAAABAAAAAAAAAP////9iBAAAfgQAAAAAAAA9AQAAAQAAAAAAAAACAQAAYgQAAH4EAAAAAAAAPgEAAAEAAAAAAAAA1wAAAGgEAAB9BAAAAAAAAD8BAAAFAAAAAAAAAE8AAABsBAAAcgQAAEIBAABAAQAAuAAAQAAAAAD/////cwQAAHwEAAAAAAAAQAEAAAEAAAAAAAAA5QAAAHMEAAB8BAAAAAAAAEIBAAADAAAAAAAAAA8AAABzBAAAdwQAAEUBAABDAQAA2AAAgAAAAAD/////dwQAAHwEAAAAAAAAQwEAAAEAAAAAAAAA8AAAAHcEAAB8BAAAAAAAAEUBAAADAAAAAAAAANQAAAB3BAAAegQAAEoBAABGAQAABQAAAAAAAABPAAAAdwQAAHgEAABJAQAARwEAAL4AAEAAAAAATwAAAHkEAAB6BAAAAAAAAEcBAADAAABAAAAAABEAAAB6BAAAfAQAAAAAAABGAQAA5AAAgAAAAAAsAQAAgAQAAIoEAABOAQAAPAEAAAIAAAAAAAAA8gAAAIYEAACKBAAAAAAAAEsBAAABAAAAAAAAAP////+IBAAAiAQAAAAAAABMAQAAAAAAAAAAAADyAAAAkgQAAJYEAAAAAAAAPAEAAAEAAAAAAAAA/////5QEAACUBAAAAAAAAE4BAAAAAAAAAAAAAAABAACWBAAAwQQAAFwBAADPAAAAAwAAAAAAAADUAAAAoQQAAKQEAABUAQAAUAEAAAUAAAAAAAAATwAAAKEEAACiBAAAUwEAAFEBAADGAABAAAAAAE8AAACjBAAApAQAAAAAAABRAQAAyAAAQAAAAAAOAQAApQQAAMEEAAAAAAAAUAEAAAEAAAAAAAAA/////6cEAAC/BAAAAAAAAFQBAAACAAAAAAAAACkBAACnBAAAtgQAAFoBAABVAQAAAwAAAAAAAAAIAAAArAQAAK4EAABYAQAAVgEAAPAAAIAAAAAA/////68EAAC2BAAAAAAAAFYBAAABAAAAAAAAAP0AAACvBAAAtgQAAAAAAABYAQAAAAAAAAAAAAAqAQAAtgQAAL8EAAAAAAAAVQEAAAIAAAAAAAAA/////78EAAC/BAAAAAAAAFoBAAAAAAAAAAAAAPQAAADBBAAA1QQAAGUBAADPAAAAAgAAAAAAAAAGAQAAwQQAANQEAAAAAAAAXAEAAAEAAAABAAAA/////8cEAADUBAAAAAAAAF0BAAABAAAAAAAAAAUBAADHBAAA1AQAAAAAAABeAQAACQAAAAAAAABPAAAAxwQAAMkEAABhAQAAXwEAAMwAAEAAAAAA4wAAAMsEAADUBAAAAAAAAF8BAAAaAAAAAAAAAAkAAADLBAAAzgQAAGMBAABhAQAA+AAAgAAAAAAqAAAAzgQAANEEAABkAQAAYQEAAAAAAAAAAAAACQAAANEEAADUBAAAAAAAAGEBAAAAAQCAAAAAAPUAAADVBAAA3wQAAGoBAADPAAAAAQAAAAAAAADjAAAA1QQAAN4EAAAAAAAAZQEAABoAAAAAAAAATwAAANUEAADXBAAAaAEAAGYBAADSAABAAAAAAE0AAADXBAAA2wQAAGkBAABmAQAAAAAAAAAAAAAJAAAA2wQAAN4EAAAAAAAAZgEAAAgBAIAAAAAA9QAAAN8EAADkBAAAAAAAAM8AAAABAAAAAAAAAOIAAADfBAAA4wQAAAAAAABqAQAAAQAAAAAAAABPAAAA3wQAAOEEAAAAAAAAawEAANYAAEAAAAAAAQAAAOYEAADnBAAAAAAAAAEAAAAAAAAAAAAAAA==", + "other.ts": "AAAABQnH2k8H5bbOVZ0OdlANqCcAAAAALAAAAGQAAADGAAAA/gAAAP4AAAAAAAAAQAAAAEAAAABRAAAAUQAAAGIAAAARAAAAFgAAABkAAAAdAAAAIAAAACYAAAA3AAAAOwAAAGV4cG9ydCBpbnRlcmZhY2UgU2hhcGUgeyBraW5kOiAiY2lyY2xlIiB9CmV4cG9ydCBjbGFzcyBCYXNlIHsgfQovcHJvamVjdC9vdGhlci50cy9wcm9qZWN0L290aGVyLnRzAAAAAAIAAAAEAAAAAAAAAAMAAAD///////////////////////////////8DAAAACgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMwEAAAAAAABAAAAAAAAAAAAAAAAAAACAAAAAAP////8AAAAAPwAAABEAAAABAAAAAgAAAAAAAAAJAQAAAAAAACkAAAAMAAAAAgAAABMAAAAAAAAA/////wAAAAAGAAAABgAAAAMAAAABAAAAAAAAAF4AAAAAAAAABgAAAAAAAAAEAAAAAAAAAAAAAABPAAAAEAAAABYAAAAHAAAAAwAAAAYAAEAAAAAA/////xgAAAAnAAAAAAAAAAMAAAABAAAAAAAAAKwAAAAYAAAAJwAAAAAAAAAHAAAACgAAAAAAAABPAAAAGAAAAB0AAAAKAAAACAAAAAgAAEAAAAAAygAAAB4AAAAnAAAAAAAAAAgAAAABAAAAAAAAAAoAAAAeAAAAJwAAAAAAAAAKAAAAMAAAgAAAAAAIAQAAKQAAAD8AAAAAAAAAAgAAABMAAAAAAAAA/////ykAAAAwAAAADwAAAAwAAAABAAAAAAAAAF4AAAApAAAAMAAAAAAAAAANAAAAAAAAAAAAAABPAAAANgAAADsAAAAQAAAADAAAAAwAAEAAAAAA/////z0AAAA9AAAAAAAAAAwAAAAAAAAAAAAAAAEAAAA/AAAAQAAAAAAAAAABAAAAAAAAAAAAAAA=", + "view.tsx": "AAAABRPTtHLHVfDM+81x6tFeUTsAAAAALAAAALQAAABNAQAAhQEAAIUBAAAAAAAAdwAAAHcAAACIAAAAiAAAAJkAAAANAAAAEQAAABUAAAAaAAAAKgAAACwAAAAtAAAAMgAAADQAAAA4AAAAOwAAAEAAAABBAAAARAAAAEYAAABKAAAAUAAAAFIAAABTAAAAVgAAAFgAAABcAAAAXwAAAGMAAABmAAAAaAAAAHIAAAB0AAAAZXhwb3J0IGNvbnN0IHZpZXcgPSAoaXRlbXM6IHN0cmluZ1tdKSA9PiA8dWwgY2xhc3M9Imxpc3QiPntpdGVtcy5tYXAoKGl0ZW0pID0+IDxsaSBrZXk9e2l0ZW19PntpdGVtfTwvbGk+KX08PjwvPjwvdWw+OwovcHJvamVjdC92aWV3LnRzeC9wcm9qZWN0L3ZpZXcudHN4AAAAAAIAAAAEAAAAAQAAAAQAAAD///////////////////////////////8DAAAADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMwEAAAAAAAB3AAAAAAAAAAAAAAAAAACAAAAAAP////8AAAAAdgAAADkAAAABAAAAAQAAAAAAAAD0AAAAAAAAAHYAAAAAAAAAAgAAAAMAAAAAAAAA/////wAAAAAGAAAABgAAAAMAAAABAAAAAAAAAF4AAAAAAAAABgAAAAAAAAAEAAAAAAAAAAAAAAAGAQAABgAAAHUAAAAAAAAAAwAAAAEAAAACAAAA/////wwAAAB1AAAAAAAAAAYAAAABAAAAAAAAAAUBAAAMAAAAdQAAAAAAAAAHAAAACQAAAAAAAABPAAAADAAAABEAAAAKAAAACAAAAAYAAEAAAAAA3AAAABMAAAB1AAAAAAAAAAgAAAA0AAAAAAEAAP////8VAAAAJAAAABAAAAAKAAAAAQAAAAAAAACqAAAAFQAAACQAAAAAAAAACwAAABQAAAAAAAAATwAAABUAAAAaAAAADgAAAAwAAAAIAABAAAAAAL0AAAAbAAAAJAAAAAAAAAAMAAAAAQAAAAAAAACaAAAAGwAAACIAAAAAAAAADgAAAAAAAAAAAAAAJgAAACUAAAAoAAAAEQAAAAoAAAAAAAAAAAAAAB0BAAAoAAAAdQAAAAAAAAAKAAAABwAAAAAAAAAfAQAAKAAAADoAAAAZAAAAEQAAAAUAAAAAAAAATwAAACoAAAAsAAAAFAAAABIAAAAKAABAAAAAACUBAAAsAAAAOQAAAAAAAAASAAAAAQAAAAAAAAD/////LAAAADkAAAAAAAAAFAAAAAEAAAAAAAAAJAEAACwAAAA5AAAAAAAAABUAAAADAAAAAAAAAE8AAAAsAAAAMgAAABgAAAAWAAAADAAAQAAAAAAKAAAAMwAAADkAAAAAAAAAFgAAADAAAIAAAAAA/////zoAAABwAAAANwAAABEAAAACAAAAAAAAACcBAAA6AAAAawAAADMAAAAZAAAAAgAAAAAAAADWAAAAOwAAAGoAAAAAAAAAGgAAAAkAAAAAAAAA1AAAADsAAABEAAAAHwAAABsAAAAFAAAAAAAAAE8AAAA7AAAAQAAAAB4AAAAcAAAAEAAAQAAAAABPAAAAQQAAAEQAAAAAAAAAHAAAABIAAEAAAAAA/////0UAAABpAAAAAAAAABsAAAABAAAAAAAAANwAAABFAAAAaQAAAAAAAAAfAAAANAAAAAABAAD/////RgAAAEoAAAAkAAAAIAAAAAEAAAAAAAAAqgAAAEYAAABKAAAAAAAAACEAAAAEAAAAAAAAAE8AAABGAAAASgAAAAAAAAAiAAAAFAAAQAAAAAAmAAAASwAAAE4AAAAlAAAAIAAAAAAAAAAAAAAAHQEAAE4AAABpAAAAAAAAACAAAAAHAAAAAAAAAB8BAABOAAAAXgAAAC4AAAAlAAAABQAAAAAAAABPAAAAUAAAAFIAAAAoAAAAJgAAABYAAEAAAAAAJQEAAFIAAABdAAAAAAAAACYAAAABAAAAAAAAAP////9SAAAAXQAAAAAAAAAoAAAAAQAAAAAAAAAkAQAAUgAAAF0AAAAAAAAAKQAAAAMAAAAAAAAATwAAAFIAAABWAAAALAAAACoAAAAYAABAAAAAACcBAABXAAAAXQAAAAAAAAAqAAAAAgAAAAAAAABPAAAAWAAAAFwAAAAAAAAALAAAABoAAEAAAAAA/////14AAABkAAAAMQAAACUAAAABAAAAAAAAACcBAABeAAAAZAAAAAAAAAAuAAAAAgAAAAAAAABPAAAAXwAAAGMAAAAAAAAALwAAABwAAEAAAAAAIAEAAGQAAABpAAAAAAAAACUAAAABAAAAAAAAAE8AAABmAAAAaAAAAAAAAAAxAAAAHgAAQAAAAAAhAQAAawAAAHAAAAAAAAAAGQAAAAcAAAAAAAAAIgEAAGsAAABtAAAANQAAADMAAAAAAAAAAAAAAP////9tAAAAbQAAADYAAAAzAAAAAAAAAAAAAAAjAQAAbQAAAHAAAAAAAAAAMwAAAAAAAAAAAAAAIAEAAHAAAAB1AAAAAAAAABEAAAABAAAAAAAAAE8AAAByAAAAdAAAAAAAAAA3AAAAIAAAQAAAAAABAAAAdgAAAHcAAAAAAAAAAQAAAAAAAAAAAAAA" + }, + "protocol9": { + "main.ts": "AAAACY6wdLJEh2ZzyMQ0XA6mzLQAAAAAQAAAAKADAAC9CAAA6QkAAAAKAAAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADpBAAA6QQAAPkEAAD5BAAACQUAADIAAAA3AAAAQAAAAEoAAABfAAAAYwAAAGoAAAB0AAAAgwAAAIgAAACPAAAAmQAAAKUAAACqAAAArAAAALAAAADoAAAA6wAAAOwAAADtAAAA9wAAAPwAAAD9AAAAAQEAAAYBAAANAQAACQUAAA8FAAAoAQAALgEAAC8BAAAwAQAANQEAADsBAAA9AQAAPgEAAEQBAABFAQAATQEAAFIBAABjAQAAagEAAHkBAACAAQAAgQEAAIQBAACGAQAAiwEAAA8FAAAVBQAAwwAAAMsAAADMAAAAzQAAAKQBAACpAQAAqgEAAK8BAAC/AQAAwAEAAMMBAADEAQAA2AEAAN8BAADwAQAA8QEAAAkCAAAPAgAAHAIAACACAAAqAgAALwIAAD0CAABBAgAAWwIAAF8CAABgAgAAYQIAAHECAAByAgAAfAIAAH0CAAAVBQAAFgUAABYFAAAXBQAAhQIAAIYCAACQAgAAkAIAAJACAACQAgAAlQIAAJYCAACXAgAAmAIAAKkCAACuAgAAsQIAALQCAAC2AgAAuwIAAL4CAAC/AgAA0wIAANcCAADYAgAA2QIAANwCAADdAgAA5wIAAOgCAAD/AgAAAwMAACEDAAAiAwAAJgMAACoDAAA2AwAAPAMAAD8DAABFAwAARgMAAEkDAAAXBQAAGAUAABgFAAAZBQAATQMAAFEDAABSAwAAUwMAABkFAAAbBQAAVQMAAFgDAABdAwAAYgMAAG8DAABwAwAAdQMAAHYDAAB4AwAAeQMAAHsDAAB8AwAAhgMAAIcDAACKAwAAiwMAAJMDAACYAwAAwwMAAMQDAADOAwAA2wMAAOUDAADmAwAABwQAAAgEAAANBAAADgQAABAEAAARBAAAEwQAABQEAAAbBAAAHAQAAB4EAAAfBAAAJAQAACoEAAArBAAAMwQAAEMEAABJBAAASgQAAFIEAABvBAAAdAQAABsFAAAcBQAAHAUAAB0FAAB5BAAAegQAAHsEAAB8BAAAfQQAAH0EAAB9BAAAfQQAAKMEAACkBAAApQQAAKYEAACvBAAAsAQAAMoEAADLBAAAzgQAANAEAADUBAAA1gQAANgEAADZBAAA3gQAAOAEAADiBAAA4wQAAC8vLyA8cmVmZXJlbmNlIHBhdGg9Ii4vb3RoZXIudHMiIC8+CmltcG9ydCB0eXBlIHsgU2hhcGUgfSBmcm9tICIuL290aGVyLmpzIjsKaW1wb3J0IGRlZmVyICogYXMgbGF6eSBmcm9tICIuL290aGVyLmpzIjsKaW1wb3J0ICogYXMgb3RoZXIgZnJvbSAiLi9vdGhlci5qcyI7CmV4cG9ydCB7IG90aGVyLCBsYXp5IH07Ci8qKiBBIGJveC4KICogQHRlbXBsYXRlIFQKICovCmV4cG9ydCBhYnN0cmFjdCBjbGFzcyBCb3g8VD4gZXh0ZW5kcyBvdGhlci5CYXNlIHsKICAjc2VjcmV0ID0gIlx1ZDgwMO+7vyI7CiAgYWJzdHJhY3QgbWV0aG9kPFU+KC4uLnZhbHVlczogVVtdLCk6IFU7CiAgZ2V0IHZhbHVlKCkgeyByZXR1cm4gdGhpcy4jc2VjcmV0OyB9CiAgc3RhdGljIHsgY29uc29sZS5sb2coInJlYWR5Iik7IH0KfQpleHBvcnQgbmFtZXNwYWNlIFNwYWNlLklubmVyIHsgZXhwb3J0IGNvbnN0IG4gPSAxOyB9CmRlY2xhcmUgbW9kdWxlICJhbWJpZW50IiB7IGV4cG9ydCBjb25zdCBhOiBudW1iZXI7IH0KZXhwb3J0IHR5cGUgVmFsdWVzID0gcmVhZG9ubHkgW25hbWU6IHN0cmluZywgY291bnQ/OiBudW1iZXIsIC4uLnJlc3Q6IGJvb2xlYW5bXV07CmV4cG9ydCB0eXBlIEtleXM8VD4gPSB7IHJlYWRvbmx5IFtLIGluIGtleW9mIFQgYXMgYHAke0sgJiBzdHJpbmd9YF0/OiBUW0tdIH07CmV4cG9ydCBlbnVtIENvbG9yIHsgUmVkLCBHcmVlbiA9IDMsIH0KZXhwb3J0IGZ1bmN0aW9uIGNhbGwoZjogKGE6IG51bWJlciwgYjogc3RyaW5nLCkgPT4gdm9pZCwgLi4uYXJnczogW251bWJlciwgc3RyaW5nXSk6IHZvaWQgewogIGYoLi4uYXJncywpOwogIGNvbnN0IHRhZ2dlZCA9IFN0cmluZy5yYXdgYSR7YXJnc1swXX1iXG5gOwogIGxhYmVsOiBmb3IgKGNvbnN0IHggb2YgWzEsIDIsIDMsXSkgeyBpZiAoeCA+IDEpIGJyZWFrIGxhYmVsOyB9CiAgdm9pZCAoYXN5bmMgKCkgPT4geyBmb3IgYXdhaXQgKGNvbnN0IHkgb2YgW10gYXMgQXN5bmNJdGVyYWJsZTxudW1iZXI+KSB5IHNhdGlzZmllcyBudW1iZXI7IH0pKCk7CiAgY29uc3QgbyA9IHsgYTogMSwgYiwgLi4ueyBjOiAyIH0sIFtTeW1ib2wuaXRlcmF0b3JdKCkgeyByZXR1cm4gW11bU3ltYm9sLml0ZXJhdG9yXSgpOyB9LCB9OwogIHRyeSB7IHRocm93IG5ldyBFcnJvcihgeCR7by5hfWApOyB9IGNhdGNoIHsgfSBmaW5hbGx5IHsgfQogIHN3aXRjaCAoby5hKSB7IGNhc2UgMTogYnJlYWs7IGRlZmF1bHQ6IH0KICBsZXQgYiA9IDBuICoqIDJuOyBiID8/PSAxbjsgYisrOwp9Ci9wcm9qZWN0L21haW4udHMvcHJvamVjdC9tYWluLnRz7aCA77u/QSBib3gucHBhYWIKeHgAAAAAAgAAAAQAAAAAAAAAAwAAAAAAAAD//////////xEAAAAVAAAA/////wMAAAAAAAAA////////////////////////////////CAAAAAAAAAAMAAAAAAAAABAAAAAAAAAAIAAAAAAEAAA0AAAAAAAAAEIAAAAAAAAARAAAAAAAAABYAAAAWgAAAAAAAABeAAAAYAAAAAAAAABsAAAAAAAAAIIAAACEAAAAAAAAAIgAAAAAAAAAigAAAIwAAAAAAAAAkgAAAAAAAACUAAAAAAAAAJYAAAAAAAAAmgAAAAAAAACoAAAAAAAAAK4AAAAAAAAAugAAALwAAAAAAAAAwgAAAMQAAAAAAAAAygAAAAAAAADOAAAAAAAAANAAAAAAAAAA1AAAAAAAAACRlRUfqi4vb3RoZXIudHMAwpMJDhORbwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0AQAAAAAAAOcEAAAAAAAAAAAAAAAAAIAAAAQA/////wAAAADmBAAAbQEAAAEAAAALAAAAAAAAABIBAAAAAAAATAAAAAoAAAACAAAABgAAAAAAAAATAQAAKgAAADkAAAAJAAAAAwAAAAIAAAEAAAAAFQEAAC8AAAA5AAAAAAAAAAQAAAABAAAAAAAAAP////8xAAAANwAAAAAAAAAFAAAAAQAAAAAAAAAWAQAAMQAAADcAAAAAAAAABgAAAAIAAAAAAAAATwAAADEAAAA3AAAAAAAAAAcAAAAGAABAAAAAAAoAAAA+AAAASwAAAAAAAAADAAAATAAAgAAAAAASAQAATAAAAHYAAAAPAAAAAgAAAAYAAAAAAAAAEwEAAFMAAABjAAAADgAAAAoAAAACAAACAAAAABQBAABZAAAAYwAAAAAAAAALAAAAAQAAAAAAAABPAAAAXgAAAGMAAAAAAAAADAAAAAoAAEAAAAAACgAAAGgAAAB1AAAAAAAAAAoAAABUAACAAAAAABIBAAB2AAAAmwAAABQAAAACAAAABgAAAAAAAAATAQAAfQAAAIgAAAATAAAADwAAAAIAAAAAAAAAFAEAAH0AAACIAAAAAAAAABAAAAABAAAAAAAAAE8AAACCAAAAiAAAAAAAAAARAAAADgAAQAAAAAAKAAAAjQAAAJoAAAAAAAAADwAAAFwAAIAAAAAAGAEAAJsAAACzAAAAGwAAAAIAAAACAAAAAAAAABkBAACiAAAAsgAAAAAAAAAUAAAAAQAAAAAgAAD/////pAAAALAAAAAAAAAAFQAAAAIAAAAAAAAAGwEAAKQAAACqAAAAGQAAABYAAAACAAAAACAAAE8AAACkAAAAqgAAAAAAAAAXAAAAEgAAQAAgAAAbAQAAqwAAALAAAAAAAAAAFgAAAAIAAAAAIAAATwAAAKsAAACwAAAAAAAAABkAAAAUAABAACAAAAkBAACzAAAAkAEAAFoAAAACAAAAHwAAAAAAIAD/////swAAAOEAAAAfAAAAGwAAAAIAAAAAAAAAXgAAALMAAADYAAAAHgAAABwAAAAAAAAAAAAAAH8AAADYAAAA4QAAAAAAAAAcAAAAAAAAAAAAAABPAAAA5wAAAOsAAAAgAAAAGwAAABYAAEAAAAAA/////+wAAADtAAAAIwAAABsAAAABAAAAAAAAAKoAAADsAAAA7QAAAAAAAAAgAAAAAgAAAAAAAABPAAAA7AAAAO0AAAAAAAAAIQAAABgAAEAAAAAA/////+4AAAABAQAAKgAAABsAAAABAAAAAAAAACwBAADuAAAAAQEAAAAAAAAjAAAAAQAAAAAgAAD/////9gAAAAEBAAAAAAAAJAAAAAEAAAAAAAAA6wAAAPYAAAABAQAAAAAAACUAAAABAAAAACAAANUAAAD2AAAAAQEAAAAAAAAmAAAABQAAAAAgAABPAAAA9gAAAPwAAAApAAAAJwAAABoAAEAAIAAATwAAAP0AAAABAQAAAAAAACcAAAAcAABAACAAAP////8DAQAAjgEAAFEAAAAbAAAABAAAAAAAAACuAAAAAwEAABoBAAAuAAAAKgAAABIAAAAAIAAAUAAAAAMBAAANAQAALQAAACsAAAAeAABAACAAAAoAAAAPAQAAGQEAAAAAAAArAAAAZAAAgAAAAACwAAAAGgEAAEQBAAA+AAAAKgAAAHUAAAAAIAAA/////xoBAAAlAQAAMQAAAC4AAAABAAAAAAAAAH8AAAAaAQAAJQEAAAAAAAAvAAAAAAAAAAAgAABPAAAAJQEAACwBAAAyAAAALgAAACIAAEAAIAAA/////y0BAAAuAQAANQAAAC4AAAABAAAAAAAAAKoAAAAtAQAALgEAAAAAAAAyAAAAAgAAAAAgAABPAAAALQEAAC4BAAAAAAAAMwAAACQAAEAAIAAA/////zABAAA/AQAAPAAAAC4AAAABAAAAAQAAAKsAAAAwAQAAPgEAAAAAAAA1AAAAFgAAAAAAAAAZAAAAMAEAADMBAAA4AAAANgAAAAAAAAAAAAAATwAAADMBAAA5AQAAOQAAADYAAAAmAABAAAAAAL4AAAA6AQAAPgEAAAAAAAA2AAAAAQAAAAAAAAC5AAAAOgEAADwBAAAAAAAAOQAAAAEAAAAAAAAATwAAADoBAAA8AQAAAAAAADoAAAAoAABAAAAAALkAAABBAQAAQwEAAAAAAAAuAAAAAQAAAAAAAABPAAAAQQEAAEMBAAAAAAAAPAAAACoAAEAAAAAAswAAAEQBAABrAQAARwAAACoAAAAqAAAAgCAAAE8AAABKAQAAUAEAAEAAAAA+AAAALAAAQAAgAAD/////UQEAAFEBAABBAAAAPgAAAAAAAAAAAAAA8wAAAFIBAABrAQAAAAAAAD4AAAABAAAAAAAAAP////9UAQAAaQEAAAAAAABBAAAAAQAAAAAAAAD/AAAAVAEAAGkBAAAAAAAAQgAAAAEAAAAAAAAA1QAAAFsBAABoAQAAAAAAAEMAAAAFAAAAAAAAAG0AAABbAQAAYAEAAEYAAABEAAAAAAAAAAAAAABQAAAAYQEAAGgBAAAAAAAARAAAAC4AAEAAAAAAsQAAAGsBAACOAQAAAAAAACoAAAACAAAAACAAAPMAAAB0AQAAjgEAAAAAAABHAAAAAQAAAAAgAAD/////dgEAAIwBAAAAAAAASAAAAAEAAAAAAAAA9gAAAHYBAACMAQAAAAAAAEkAAAABAAAAACAAANcAAAB2AQAAiwEAAAAAAABKAAAACQAAAAAgAADVAAAAdgEAAIIBAABPAAAASwAAAAUAAAAAIAAATwAAAHYBAAB+AQAATgAAAEwAAAAwAABAACAAAE8AAAB/AQAAggEAAAAAAABMAAAAMgAAQAAgAAD/////gwEAAIoBAAAAAAAASwAAAAEAAAAAAAAACgAAAIMBAACKAQAAAAAAAE8AAABsAACAACAAADwBAACzAAAA0QAAAAAAAAAbAAAAAwAAAAAAQAD/////tAAAAMIAAABUAAAAUQAAAAEAAAAAAAAAPQEAALQAAADCAAAAAAAAAFIAAAA2AABAAABAAP/////CAAAAzwAAAAAAAABRAAAAAQAAAAAAAABSAQAAwgAAAM8AAAAAAAAAVAAAAAUAAAAAAEAATwAAAMMAAADLAAAAVwAAAFUAAAA4AABAAABAAP////8AAAAAAAAAAAAAAABVAAAAAQAAAAAAAACqAAAAzAAAAM0AAAAAAAAAVwAAAAIAAAAAAEAATwAAAMwAAADNAAAAAAAAAFgAAAA6AABAAABAAA0BAACQAQAAxQEAAGwAAAACAAAACwAAAQAAAAD/////kAEAAJcBAABdAAAAWgAAAAEAAAAAAAAAXgAAAJABAACXAQAAAAAAAFsAAAAAAAAAAAAAAE8AAAChAQAApwEAAF4AAABaAAAAPAAAQAAAAAANAQAAqAEAAMUBAAAAAAAAWgAAAAsAAAEAAAAA/////6gBAACoAQAAYQAAAF4AAAABAAAAAAAAAF4AAACoAQAAqAEAAAAAAABfAAAAAAAAAAgAAABPAAAAqAEAAK0BAABiAAAAXgAAAD4AAEAAAAAADgEAAK0BAADFAQAAAAAAAF4AAAABAAAAAAAAAP////+vAQAAwwEAAAAAAABiAAAAAQAAAAAAAAD1AAAArwEAAMMBAAAAAAAAYwAAAAMAAAAAAAAA/////68BAAC2AQAAZwAAAGQAAAABAAAAAAAAAF4AAACvAQAAtgEAAAAAAABlAAAAAAAAAAAAAAAHAQAAtgEAAMIBAAAAAAAAZAAAAAEAAAACAAAA/////7wBAADCAQAAAAAAAGcAAAABAAAAAAAAAAYBAAC8AQAAwgEAAAAAAABoAAAACQAAAAAAAABPAAAAvAEAAL4BAABrAAAAaQAAAEAAAEAAAAAACAAAAMABAADCAQAAAAAAAGkAAAB0AACAAAAAAA0BAADFAQAA+gEAAHoAAAACAAAACwAAAEAAgAD/////xQEAAM0BAABvAAAAbAAAAAEAAAAAAAAAiQAAAMUBAADNAQAAAAAAAG0AAAAAAAAAAACAAAoAAADUAQAA3gEAAHAAAABsAAAAfAAAgAAAgAAOAQAA3gEAAPoBAAAAAAAAbAAAAAEAAAAAAIAA/////+ABAAD4AQAAAAAAAHAAAAABAAAAAAAAAPUAAADgAQAA+AEAAAAAAABxAAAAAwAAAAAAgAD/////4AEAAOcBAAB1AAAAcgAAAAEAAAAAAAAAXgAAAOABAADnAQAAAAAAAHMAAAAAAAAAAACAAAcBAADnAQAA9wEAAAAAAAByAAAAAQAAAAIAgAD/////7QEAAPcBAAAAAAAAdQAAAAEAAAAAAAAABgEAAO0BAAD3AQAAAAAAAHYAAAAFAAAAAACAAE8AAADtAQAA7wEAAHkAAAB3AAAARgAAQAAAgACWAAAA8AEAAPcBAAAAAAAAdwAAAAAAAAAAAIAACwEAAPoBAABMAgAAjQAAAAIAAAALAAAAAAAAAP/////6AQAAAQIAAH0AAAB6AAAAAQAAAAAAAABeAAAA+gEAAAECAAAAAAAAewAAAAAAAAAAAAAATwAAAAYCAAANAgAAfgAAAHoAAABIAABAAAAAAMgAAAAPAgAASwIAAAAAAAB6AAAAAQAAAQAAAAC/AAAAGAIAAEsCAAAAAAAAfgAAAAEAAAAAAAAA/////xoCAABKAgAAAAAAAH8AAAADAAAAAAAAAMwAAAAaAgAAJgIAAIQAAACAAAAACgAAAAAAAABPAAAAGgIAAB4CAACDAAAAgQAAAEoAAEAAAAAAmgAAAB8CAAAmAgAAAAAAAIEAAAAAAAAAAAAAAMwAAAAnAgAANgIAAIgAAACAAAAADgAAAAAAAABPAAAAJwIAAC0CAACGAAAAhAAAAEwAAEAAAAAAOQAAAC0CAAAuAgAAhwAAAIQAAAAAAAAAAAAAAJYAAAAvAgAANgIAAAAAAACEAAAAAAAAAAAAAADMAAAANwIAAEoCAAAAAAAAgAAAAAsAAAAAAAAAGQAAADcCAAA7AgAAigAAAIgAAAAAAAAAAAAAAE8AAAA7AgAAPwIAAIsAAACIAAAATgAAQAAAAAC+AAAAQAIAAEoCAAAAAAAAiAAAAAEAAAAAAAAAhwAAAEACAABIAgAAAAAAAIsAAAAAAAAAAAAAAAsBAABMAgAAmgIAAKwAAAACAAAADwAAAAAAAAD/////TAIAAFMCAACQAAAAjQAAAAEAAAAAAAAAXgAAAEwCAABTAgAAAAAAAI4AAAAAAAAAAAAAAE8AAABYAgAAXQIAAJEAAACNAAAAUAAAQAAAAAD/////XgIAAF8CAACUAAAAjQAAAAEAAAAAAAAAqgAAAF4CAABfAgAAAAAAAJEAAAACAAAAAAAAAE8AAABeAgAAXwIAAAAAAACSAAAAUgAAQAAAAADKAAAAYgIAAJkCAAAAAAAAjQAAAD8AAAAAAAAAlAAAAGQCAABtAgAAlgAAAJQAAAAAAAAAAAAAAKoAAABvAgAAewIAAJsAAACUAAAABgAAAAAAAABPAAAAbwIAAHACAACYAAAAlgAAAFQAAEAAAAAAyAAAAHMCAAB7AgAAAAAAAJYAAAABAAAAAAAAALkAAAB5AgAAewIAAAAAAACYAAAAAQAAAAAAAABPAAAAeQIAAHsCAAAAAAAAmQAAAFYAAEAAAAAAzQAAAH4CAACPAgAApQAAAJQAAAADAAAAAAAAAA8AAAB+AgAAgwIAAJ0AAACbAAAAhAAAgAAAAAD/////gwIAAI8CAAAAAAAAmwAAAAEAAAAAAAAAzgAAAIMCAACPAgAAAAAAAJ0AAAADAAAAAAAAAMMAAACDAgAAjQIAAKQAAACeAAAAAQAAAAAAAAD/////gwIAAI0CAAAAAAAAnwAAAAIAAAAAAAAAuQAAAIMCAACEAgAAowAAAKAAAAABAAAAAAAAAE8AAACDAgAAhAIAAAAAAAChAAAAXAAAQAAAAACaAAAAhgIAAI0CAAAAAAAAoAAAAAAAAAAAAAAAEQAAAI0CAACPAgAAAAAAAJ4AAACQAACAAAAAADkAAACQAgAAkQIAAKYAAACUAAAAAAAAAAAAAADJAAAAkgIAAJcCAACrAAAAlAAAAAMAAAAAAAAAuQAAAJICAACUAgAAqQAAAKYAAAABAAAAAAAAAE8AAACSAgAAlAIAAAAAAACnAAAAYgAAQAAAAAC5AAAAlQIAAJYCAAAAAAAApgAAAAEAAAAAAAAATwAAAJUCAACWAgAAAAAAAKkAAABkAABAAAAAAP////+XAgAAlwIAAAAAAACUAAAAAAAAAAAAAAAMAQAAmgIAAMACAAC2AAAAAgAAAAcAAAAAAAAA/////5oCAAChAgAArwAAAKwAAAABAAAAAAAAAF4AAACaAgAAoQIAAAAAAACtAAAAAAAAAAAAAABPAAAApgIAAKwCAACwAAAArAAAAGYAAEAAAAAA/////64CAAC+AgAAAAAAAKwAAAACAAAAAQAAADMBAACuAgAAsgIAALMAAACwAAAAAQAAAAAAAABPAAAArgIAALICAAAAAAAAsQAAAGgAAEAAAAAAMwEAALMCAAC9AgAAAAAAALAAAAADAAAAAAAAAE8AAACzAgAAuQIAALUAAACzAAAAagAAQAAAAAAIAAAAuwIAAL0CAAAAAAAAswAAAJwAAIAAAAAACAEAAMACAADmBAAAAAAAAAIAAAB1AAAAAAEAAP/////AAgAAxwIAALkAAAC2AAAAAQAAAAAAAABeAAAAwAIAAMcCAAAAAAAAtwAAAAAAAAAAAAAATwAAANACAADVAgAAugAAALYAAABuAABAAAAAAP/////WAgAAEwMAAM0AAAC2AAAAAgAAAAAAAACrAAAA1gIAAPgCAADGAAAAugAAABQAAAAAAAAATwAAANYCAADXAgAAvQAAALsAAABwAABAAAAAALoAAADYAgAA+AIAAAAAAAC7AAAABgAAAAAAAAD/////2gIAAO8CAADFAAAAvQAAAAIAAAABAAAAqwAAANoCAADjAgAAwgAAAL4AAAAUAAAAAAAAAE8AAADaAgAA2wIAAMEAAAC/AAAAcgAAQAAAAACWAAAA3AIAAOMCAAAAAAAAvwAAAAAAAAAAAAAAqwAAAOQCAADuAgAAAAAAAL4AAAAUAAAAAAAAAE8AAADkAgAA5gIAAMQAAADCAAAAdAAAQAAAAACaAAAA5wIAAO4CAAAAAAAAwgAAAAAAAAAAAAAAcwAAAPMCAAD4AgAAAAAAAL0AAAAAAAAAAAAAAKsAAAD5AgAAEwMAAAAAAAC6AAAAFgAAAAAAAAAZAAAA+QIAAP0CAADIAAAAxgAAAAAAAAAAAAAATwAAAP0CAAABAwAAyQAAAMYAAAB2AABAAAAAAL8AAAACAwAAEwMAAAAAAADGAAAAAQAAAAAAAAD/////BAMAABIDAAAAAAAAyQAAAAIAAAAAAAAAlgAAAAQDAAAKAwAAzAAAAMoAAAAAAAAAAAAAAJoAAAALAwAAEgMAAAAAAADKAAAAAAAAAAAAAABzAAAAFQMAABoDAADOAAAAtgAAAAAAAAAAAAAA8wAAABoDAADmBAAAAAAAALYAAAABAAABAAAAAP////8cAwAA5AQAAAAAAADOAAAACgAAAAAAAAD2AAAAHAMAACsDAADWAAAAzwAAAAEAAAAAAAAA1wAAABwDAAAqAwAAAAAAANAAAAAJAAAAAAAAAE8AAAAcAwAAIAMAANMAAADRAAAAeAAAQAAAAAD/////IQMAACkDAAAAAAAA0QAAAAEAAAABAAAA6AAAACEDAAAoAwAAAAAAANMAAAABAAAAAAAAAE8AAAAkAwAAKAMAAAAAAADUAAAAegAAQAAAAAD1AAAAKwMAAFgDAADnAAAAzwAAAAIAAAAAAAAABwEAACsDAABXAwAAAAAAANYAAAABAAAAAgAAAP////8zAwAAVwMAAAAAAADXAAAAAQAAAAAAAAAGAQAAMwMAAFcDAAAAAAAA2AAAAAkAAAAAAAAATwAAADMDAAA6AwAA2wAAANkAAAB8AABAAAAAANkAAAA8AwAAVwMAAAAAAADZAAAACQAAAAAAAADVAAAAPAMAAEcDAADfAAAA2wAAAAUAAAAAAAAATwAAADwDAABDAwAA3gAAANwAAAB+AABAAAAAAE8AAABEAwAARwMAAAAAAADcAAAAgAAAQAAAAADmAAAARwMAAFcDAAAAAAAA2wAAAAMAAAAAAAAADwAAAEcDAABLAwAA4QAAAN8AAACkAACAAAAAAP////9LAwAAVwMAAAAAAADfAAAAAQAAAAAAAADxAAAASwMAAFcDAAAAAAAA4QAAAAMAAAAAAAAA1gAAAEsDAABSAwAA5gAAAOIAAAAFAAAAAAAAAE8AAABLAwAATwMAAOUAAADjAAAAhgAAQAAAAAAIAAAAUAMAAFEDAAAAAAAA4wAAALAAAIAAAAAAEQAAAFIDAABXAwAAAAAAAOIAAAC4AACAAAAAAAIBAABYAwAAmQMAAPwAAADPAAAAAwAAAAAAAABPAAAAWAMAAGADAADpAAAA5wAAAI4AAEAAAAAA/AAAAGEDAACZAwAAAAAAAOcAAAAOAAAAAAAAAAcBAABnAwAAbgMAAO4AAADpAAAAAQAAAAIAAAD/////bAMAAG4DAAAAAAAA6gAAAAEAAAAAAAAABgEAAGwDAABuAwAAAAAAAOsAAAABAAAAAAQAAE8AAABsAwAAbgMAAAAAAADsAAAAkAAAQAAEAADTAAAAcQMAAHwDAADzAAAA6QAAAAEAAAAAAAAA/////3MDAAB7AwAAAAAAAO4AAAADAAAAAQAAAAgAAABzAwAAdAMAAPEAAADvAAAAxAAAgAAAAAAIAAAAdQMAAHcDAADyAAAA7wAAAMwAAIAAAAAACAAAAHgDAAB6AwAAAAAAAO8AAADUAACAAAAAAPMAAAB9AwAAmQMAAAAAAADpAAAAAQAAAAAAAAD/////fwMAAJcDAAAAAAAA8wAAAAEAAAAAAAAA9wAAAH8DAACXAwAAAAAAAPQAAAADAAAAAAAAAOQAAACEAwAAiQMAAPoAAAD1AAAAGgAAAAAAAABPAAAAhAMAAIUDAAD4AAAA9gAAAJgAAEAAAAAAHwAAAIUDAACHAwAA+QAAAPYAAAAAAAAAAAAAAAgAAACHAwAAiQMAAAAAAAD2AAAA3AAAgAAAAAD+AAAAigMAAJcDAAAAAAAA9QAAAAEAAAAAAAAATwAAAJADAACWAwAAAAAAAPoAAACcAABAAAAAAPYAAACZAwAA/AMAABkBAADPAAAAAQAAAAAAAADgAAAAmQMAAPsDAAAAAAAA/AAAAAEAAAAAAAAA1wAAAKADAAD7AwAAAAAAAP0AAAAJAAAAAAAAANsAAACgAwAA+QMAABgBAAD+AAAAAQAAAAAAAADdAAAAogMAAPgDAAAAAAAA/wAAADUAAAAAAQAA/////6IDAACnAwAAAwEAAAABAAABAAAAAAAAAIUAAACiAwAApwMAAAAAAAABAQAAAAAAAAAAAAD/////qQMAAKkDAAAEAQAAAAEAAAAAAAAAAAAAJgAAAKoDAACtAwAABQEAAAABAAAAAAAAAAAAAPMAAACtAwAA+AMAAAAAAAAAAQAAAQAAAAAgAAD/////rwMAAPYDAAAAAAAABQEAAAEAAAAAAAAA/AAAAK8DAAD2AwAAAAAAAAYBAAAPAAAAACAAAIYAAACzAwAAuQMAAAkBAAAHAQAAAAAAAAAgAAAHAQAAuwMAAMIDAAANAQAABwEAAAEAAAACIAAA/////8ADAADCAwAAAAAAAAkBAAABAAAAAAAAAAYBAADAAwAAwgMAAAAAAAAKAQAAAQAAAAAkAABPAAAAwAMAAMIDAAAAAAAACwEAAJ4AAEAAJAAA7AAAAMUDAADhAwAAFAEAAAcBAAADAAAAACAAANMAAADFAwAAyAMAABABAAANAQAAAQAAAAAgAAD/////xwMAAMcDAAAAAAAADgEAAAAAAAAAAAAAuQAAAMsDAADhAwAAAAAAAA0BAAADAAAAAAAAAE8AAADLAwAA2QMAABIBAAAQAQAAoAAAQAAAAAD/////2gMAAOADAAAAAAAAEAEAAAEAAAAAAAAAlgAAANoDAADgAwAAAAAAABIBAAAAAAAAAAAAAPYAAADiAwAA9gMAAAAAAAAHAQAAAQAAAAAgAADwAAAA4gMAAPUDAAAAAAAAFAEAAAMAAAAAIAAATwAAAOIDAADkAwAAFwEAABUBAACiAABAACAAAJYAAADuAwAA9QMAAAAAAAAVAQAAAAAAAAAAAAD/////+gMAAPoDAAAAAAAA/gAAAAAAAAAAAAAA9QAAAPwDAABaBAAAPAEAAM8AAAACAAAAAAAAAAcBAAD8AwAAWQQAAAAAAAAZAQAAAQAAAAIAAAD/////BAQAAFkEAAAAAAAAGgEAAAEAAAAAAAAABgEAAAQEAABZBAAAAAAAABsBAAAJAAAAAAAAAE8AAAAEBAAABgQAAB4BAAAcAQAApAAAQAAAAADUAAAACAQAAFkEAAAAAAAAHAEAAAEAAAAAAAAA/////woEAABXBAAAAAAAAB4BAAAEAAAAAQAAADABAAAKBAAADwQAACMBAAAfAQAAEgAAAAAAAABPAAAACgQAAAwEAAAiAQAAIAEAAKYAAEAAAAAACAAAAA0EAAAPBAAAAAAAACABAADkAACAAAAAADEBAAAQBAAAEgQAACUBAAAfAQAAAgAAAAAAAABPAAAAEAQAABIEAAAAAAAAIwEAAKoAAEAAAAAAMgEAABMEAAAfBAAAKwEAAB8BAAABAAAAAAAAANQAAAAXBAAAHwQAAAAAAAAlAQAAAQAAAAAAAAD/////GAQAAB0EAAAAAAAAJgEAAAEAAAAAAAAAMAEAABgEAAAdBAAAAAAAACcBAAASAAAAAAAAAE8AAAAYBAAAGgQAACoBAAAoAQAArAAAQAAAAAAIAAAAGwQAAB0EAAAAAAAAKAEAAOwAAIAAAAAAsAAAACAEAABWBAAAAAAAAB8BAACkAAAAAAAAAKkAAAAgBAAAMgQAADABAAArAQAAAQAAAAAAAADVAAAAIgQAADEEAAAAAAAALAEAAAUAAAAAAAAATwAAACIEAAAoBAAALwEAAC0BAACwAABAAAAAAE8AAAApBAAAMQQAAAAAAAAtAQAAsgAAQAAAAAD/////MwQAADMEAAAxAQAAKwEAAAAAAAAAAAAA8wAAADQEAABWBAAAAAAAACsBAAABAAAAAAAAAP////82BAAAVAQAAAAAAAAxAQAAAQAAAAAAAAD/AAAANgQAAFQEAAAAAAAAMgEAAAEAAAAAAAAA1wAAAD0EAABTBAAAAAAAADMBAAAJAAAAAAAAANYAAAA9BAAAUQQAADsBAAA0AQAABQAAAAAAAADTAAAAPQQAAEAEAAA4AQAANQEAAAEAAAAAAAAA/////z8EAAA/BAAAAAAAADYBAAAAAAAAAAAAANUAAABBBAAAUAQAAAAAAAA1AQAABQAAAAAAAABPAAAAQQQAAEcEAAA6AQAAOAEAALQAAEAAAAAATwAAAEgEAABQBAAAAAAAADgBAAC2AABAAAAAAP////9SBAAAUgQAAAAAAAA0AQAAAAAAAAAAAAAEAQAAWgQAAJYEAABQAQAAzwAAAAcAAAAAAAAA8wAAAGAEAACABAAASwEAADwBAAABAAAAAAAAAP////9iBAAAfgQAAAAAAAA9AQAAAQAAAAAAAAADAQAAYgQAAH4EAAAAAAAAPgEAAAEAAAAAAAAA2AAAAGgEAAB9BAAAAAAAAD8BAAAFAAAAAAAAAE8AAABsBAAAcgQAAEIBAABAAQAAuAAAQAAAAAD/////cwQAAHwEAAAAAAAAQAEAAAEAAAAAAAAA5gAAAHMEAAB8BAAAAAAAAEIBAAADAAAAAAAAAA8AAABzBAAAdwQAAEUBAABDAQAA9AAAgAAAAAD/////dwQAAHwEAAAAAAAAQwEAAAEAAAAAAAAA8QAAAHcEAAB8BAAAAAAAAEUBAAADAAAAAAAAANUAAAB3BAAAegQAAEoBAABGAQAABQAAAAAAAABPAAAAdwQAAHgEAABJAQAARwEAAL4AAEAAAAAATwAAAHkEAAB6BAAAAAAAAEcBAADAAABAAAAAABEAAAB6BAAAfAQAAAAAAABGAQAAAAEAgAAAAAAtAQAAgAQAAIoEAABOAQAAPAEAAAIAAAAAAAAA8wAAAIYEAACKBAAAAAAAAEsBAAABAAAAAAAAAP////+IBAAAiAQAAAAAAABMAQAAAAAAAAAAAADzAAAAkgQAAJYEAAAAAAAAPAEAAAEAAAAAAAAA/////5QEAACUBAAAAAAAAE4BAAAAAAAAAAAAAAEBAACWBAAAwQQAAFwBAADPAAAAAwAAAAAAAADVAAAAoQQAAKQEAABUAQAAUAEAAAUAAAAAAAAATwAAAKEEAACiBAAAUwEAAFEBAADGAABAAAAAAE8AAACjBAAApAQAAAAAAABRAQAAyAAAQAAAAAAPAQAApQQAAMEEAAAAAAAAUAEAAAEAAAAAAAAA/////6cEAAC/BAAAAAAAAFQBAAACAAAAAAAAACoBAACnBAAAtgQAAFoBAABVAQAAAwAAAAAAAAAIAAAArAQAAK4EAABYAQAAVgEAAAwBAIAAAAAA/////68EAAC2BAAAAAAAAFYBAAABAAAAAAAAAP4AAACvBAAAtgQAAAAAAABYAQAAAAAAAAAAAAArAQAAtgQAAL8EAAAAAAAAVQEAAAIAAAAAAAAA/////78EAAC/BAAAAAAAAFoBAAAAAAAAAAAAAPUAAADBBAAA1QQAAGUBAADPAAAAAgAAAAAAAAAHAQAAwQQAANQEAAAAAAAAXAEAAAEAAAABAAAA/////8cEAADUBAAAAAAAAF0BAAABAAAAAAAAAAYBAADHBAAA1AQAAAAAAABeAQAACQAAAAAAAABPAAAAxwQAAMkEAABhAQAAXwEAAMwAAEAAAAAA5AAAAMsEAADUBAAAAAAAAF8BAAAaAAAAAAAAAAkAAADLBAAAzgQAAGMBAABhAQAAFAEAgAAAAAAqAAAAzgQAANEEAABkAQAAYQEAAAAAAAAAAAAACQAAANEEAADUBAAAAAAAAGEBAAAcAQCAAAAAAPYAAADVBAAA3wQAAGoBAADPAAAAAQAAAAAAAADkAAAA1QQAAN4EAAAAAAAAZQEAABoAAAAAAAAATwAAANUEAADXBAAAaAEAAGYBAADSAABAAAAAAE0AAADXBAAA2wQAAGkBAABmAQAAAAAAAAAAAAAJAAAA2wQAAN4EAAAAAAAAZgEAACQBAIAAAAAA9gAAAN8EAADkBAAAAAAAAM8AAAABAAAAAAAAAOMAAADfBAAA4wQAAAAAAABqAQAAAQAAAAAAAABPAAAA3wQAAOEEAAAAAAAAawEAANYAAEAAAAAAAQAAAOYEAADnBAAAAAAAAAEAAAAAAAAAAAAAAA==", + "other.ts": "AAAACQnH2k8H5bbOVZ0OdlANqCcAAAAAQAAAAHgAAADaAAAALgEAAC4BAAAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAQAAAAFEAAABRAAAAYgAAABEAAAAWAAAAGQAAAB0AAAAgAAAAJgAAADcAAAA7AAAAZXhwb3J0IGludGVyZmFjZSBTaGFwZSB7IGtpbmQ6ICJjaXJjbGUiIH0KZXhwb3J0IGNsYXNzIEJhc2UgeyB9Ci9wcm9qZWN0L290aGVyLnRzL3Byb2plY3Qvb3RoZXIudHMAAAAAAgAAAAQAAAAAAAAAAwAAAP///////////////////////////////wMAAAAAAAAA////////////////////////////////CgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANAEAAAAAAABAAAAAAAAAAAAAAAAAAACAAAAAAP////8AAAAAPwAAABEAAAABAAAAAgAAAAAAAAAKAQAAAAAAACkAAAAMAAAAAgAAABMAAAAAAAAA/////wAAAAAGAAAABgAAAAMAAAABAAAAAAAAAF4AAAAAAAAABgAAAAAAAAAEAAAAAAAAAAAAAABPAAAAEAAAABYAAAAHAAAAAwAAAAYAAEAAAAAA/////xgAAAAnAAAAAAAAAAMAAAABAAAAAAAAAK0AAAAYAAAAJwAAAAAAAAAHAAAACgAAAAAAAABPAAAAGAAAAB0AAAAKAAAACAAAAAgAAEAAAAAAywAAAB4AAAAnAAAAAAAAAAgAAAABAAAAAAAAAAoAAAAeAAAAJwAAAAAAAAAKAAAATAAAgAAAAAAJAQAAKQAAAD8AAAAAAAAAAgAAABMAAAAAAAAA/////ykAAAAwAAAADwAAAAwAAAABAAAAAAAAAF4AAAApAAAAMAAAAAAAAAANAAAAAAAAAAAAAABPAAAANgAAADsAAAAQAAAADAAAAAwAAEAAAAAA/////z0AAAA9AAAAAAAAAAwAAAAAAAAAAAAAAAEAAAA/AAAAQAAAAAAAAAABAAAAAAAAAAAAAAA=", + "view.tsx": "AAAACRPTtHLHVfDM+81x6tFeUTsAAAAAQAAAAMgAAABhAQAAtQEAALUBAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB3AAAAdwAAAIgAAACIAAAAmQAAAA0AAAARAAAAFQAAABoAAAAqAAAALAAAAC0AAAAyAAAANAAAADgAAAA7AAAAQAAAAEEAAABEAAAARgAAAEoAAABQAAAAUgAAAFMAAABWAAAAWAAAAFwAAABfAAAAYwAAAGYAAABoAAAAcgAAAHQAAABleHBvcnQgY29uc3QgdmlldyA9IChpdGVtczogc3RyaW5nW10pID0+IDx1bCBjbGFzcz0ibGlzdCI+e2l0ZW1zLm1hcCgoaXRlbSkgPT4gPGxpIGtleT17aXRlbX0+e2l0ZW19PC9saT4pfTw+PC8+PC91bD47Ci9wcm9qZWN0L3ZpZXcudHN4L3Byb2plY3Qvdmlldy50c3gAAAAAAgAAAAQAAAABAAAABAAAAP///////////////////////////////wMAAAAAAAAA////////////////////////////////DgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANAEAAAAAAAB3AAAAAAAAAAAAAAAAAACAAAAAAP////8AAAAAdgAAADkAAAABAAAAAQAAAAAAAAD1AAAAAAAAAHYAAAAAAAAAAgAAAAMAAAAAAAAA/////wAAAAAGAAAABgAAAAMAAAABAAAAAAAAAF4AAAAAAAAABgAAAAAAAAAEAAAAAAAAAAAAAAAHAQAABgAAAHUAAAAAAAAAAwAAAAEAAAACAAAA/////wwAAAB1AAAAAAAAAAYAAAABAAAAAAAAAAYBAAAMAAAAdQAAAAAAAAAHAAAACQAAAAAAAABPAAAADAAAABEAAAAKAAAACAAAAAYAAEAAAAAA3QAAABMAAAB1AAAAAAAAAAgAAAA0AAAAAAEAAP////8VAAAAJAAAABAAAAAKAAAAAQAAAAAAAACrAAAAFQAAACQAAAAAAAAACwAAABQAAAAAAAAATwAAABUAAAAaAAAADgAAAAwAAAAIAABAAAAAAL4AAAAbAAAAJAAAAAAAAAAMAAAAAQAAAAAAAACaAAAAGwAAACIAAAAAAAAADgAAAAAAAAAAAAAAJgAAACUAAAAoAAAAEQAAAAoAAAAAAAAAAAAAAB4BAAAoAAAAdQAAAAAAAAAKAAAABwAAAAAAAAAgAQAAKAAAADoAAAAZAAAAEQAAAAUAAAAAAAAATwAAACoAAAAsAAAAFAAAABIAAAAKAABAAAAAACYBAAAsAAAAOQAAAAAAAAASAAAAAQAAAAAAAAD/////LAAAADkAAAAAAAAAFAAAAAEAAAAAAAAAJQEAACwAAAA5AAAAAAAAABUAAAADAAAAAAAAAE8AAAAsAAAAMgAAABgAAAAWAAAADAAAQAAAAAAKAAAAMwAAADkAAAAAAAAAFgAAAEwAAIAAAAAA/////zoAAABwAAAANwAAABEAAAACAAAAAAAAACgBAAA6AAAAawAAADMAAAAZAAAAAgAAAAAAAADXAAAAOwAAAGoAAAAAAAAAGgAAAAkAAAAAAAAA1QAAADsAAABEAAAAHwAAABsAAAAFAAAAAAAAAE8AAAA7AAAAQAAAAB4AAAAcAAAAEAAAQAAAAABPAAAAQQAAAEQAAAAAAAAAHAAAABIAAEAAAAAA/////0UAAABpAAAAAAAAABsAAAABAAAAAAAAAN0AAABFAAAAaQAAAAAAAAAfAAAANAAAAAABAAD/////RgAAAEoAAAAkAAAAIAAAAAEAAAAAAAAAqwAAAEYAAABKAAAAAAAAACEAAAAEAAAAAAAAAE8AAABGAAAASgAAAAAAAAAiAAAAFAAAQAAAAAAmAAAASwAAAE4AAAAlAAAAIAAAAAAAAAAAAAAAHgEAAE4AAABpAAAAAAAAACAAAAAHAAAAAAAAACABAABOAAAAXgAAAC4AAAAlAAAABQAAAAAAAABPAAAAUAAAAFIAAAAoAAAAJgAAABYAAEAAAAAAJgEAAFIAAABdAAAAAAAAACYAAAABAAAAAAAAAP////9SAAAAXQAAAAAAAAAoAAAAAQAAAAAAAAAlAQAAUgAAAF0AAAAAAAAAKQAAAAMAAAAAAAAATwAAAFIAAABWAAAALAAAACoAAAAYAABAAAAAACgBAABXAAAAXQAAAAAAAAAqAAAAAgAAAAAAAABPAAAAWAAAAFwAAAAAAAAALAAAABoAAEAAAAAA/////14AAABkAAAAMQAAACUAAAABAAAAAAAAACgBAABeAAAAZAAAAAAAAAAuAAAAAgAAAAAAAABPAAAAXwAAAGMAAAAAAAAALwAAABwAAEAAAAAAIQEAAGQAAABpAAAAAAAAACUAAAABAAAAAAAAAE8AAABmAAAAaAAAAAAAAAAxAAAAHgAAQAAAAAAiAQAAawAAAHAAAAAAAAAAGQAAAAcAAAAAAAAAIwEAAGsAAABtAAAANQAAADMAAAAAAAAAAAAAAP////9tAAAAbQAAADYAAAAzAAAAAAAAAAAAAAAkAQAAbQAAAHAAAAAAAAAAMwAAAAAAAAAAAAAAIQEAAHAAAAB1AAAAAAAAABEAAAABAAAAAAAAAE8AAAByAAAAdAAAAAAAAAA3AAAAIAAAQAAAAAABAAAAdgAAAHcAAAAAAAAAAQAAAAAAAAAAAAAA" + } +} diff --git a/packages/compiler/test/format-float-arguments.test.ts b/packages/compiler/test/format-float-arguments.test.ts index 6ce9c4bc0..795e238ef 100644 --- a/packages/compiler/test/format-float-arguments.test.ts +++ b/packages/compiler/test/format-float-arguments.test.ts @@ -3,6 +3,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { expect, test } from "vitest"; import { compile, deserializeModule, validateModule } from "../src/index.js"; +import { everyStmtList } from "../src/ir/traverse.js"; test("util.format %f lowers a boolean argument once", async () => { const dir = await mkdtemp(join(tmpdir(), "scriptc-format-float-")); @@ -22,14 +23,20 @@ test("util.format %f lowers a boolean argument once", async () => { throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); const mod = deserializeModule(await readFile(outPath, "utf8")); expect(validateModule(mod)).toEqual([]); - // One lifted helper per argument: the evaluated-for-effect statement - // reuses the lowered value instead of lowering the argument again. - const helpers = mod.functions - .map((f) => f.name) - .filter((name) => name.startsWith("%arr.")) - .map((name) => name.replace(/\.[0-9a-f]+$/, "")) - .sort(); - expect(helpers).toEqual(["%arr.every", "%arr.some"]); + // One lifted helper call per argument: the evaluated-for-effect + // statement reuses the lowered value instead of lowering the argument + // again. Each call runs its callback as a loop (array-pipelines.ts). + const helpers: string[] = []; + for (const fn of mod.functions) + everyStmtList(fn.body, { + expr: (e) => { + if (e.kind === "call" && e.callee.startsWith("%arr.")) + helpers.push(e.callee.replace(/\.[0-9a-f]+$/, "")); + return true; + }, + stmt: () => true, + }); + expect(helpers).toEqual(["%arr.loop", "%arr.loop"]); } finally { await rm(dir, { recursive: true, force: true }); } diff --git a/packages/compiler/test/function-layout.test.ts b/packages/compiler/test/function-layout.test.ts new file mode 100644 index 000000000..ecf4af7d9 --- /dev/null +++ b/packages/compiler/test/function-layout.test.ts @@ -0,0 +1,183 @@ +import { execFileSync } from "node:child_process"; +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { gzipSync } from "node:zlib"; +import { expect, test, vi } from "vitest"; +import { compile } from "../src/index.js"; +import { + functionLayoutKey, + layoutTierOf, + parseFunctionLayout, +} from "../src/backend/llvm/function-layout.js"; + +const source = ` +function first(n: number): number { return n + 1; } +function second(n: number): number { return n * 2; } +function third(n: number): number { return n - 3; } +console.log(first(Number(process.argv.length)), second(4), third(5)); +`; + +async function emitLlvm(layout?: string): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-function-layout-")); + try { + const entry = join(dir, "main.ts"); + const outPath = join(dir, "main.ll"); + await writeFile(entry, source); + let functionLayout: string | undefined; + if (layout !== undefined) { + functionLayout = join(dir, "layout.gz"); + await writeFile(functionLayout, gzipSync(layout)); + } + const result = await compile(entry, { + outDir: dir, + outPath, + outputKind: "llvm", + ...(functionLayout === undefined ? {} : { functionLayout }), + }); + expect(result.ok, JSON.stringify(result.diagnostics)).toBe(true); + return await readFile(outPath, "utf8"); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +/** Each defined function's name with the partition group line it follows. */ +function definitions(llvm: string): { name: string; group: string }[] { + const out: { name: string; group: string }[] = []; + let group = ""; + for (const line of llvm.split("\n")) { + if (line.startsWith("; scriptc-group")) group = line.slice("; scriptc-group".length).trim(); + const name = /^define [^@]*@([\w.$]+)\(/.exec(line)?.[1]; + if (name !== undefined) out.push({ name, group }); + } + return out; +} + +test("a layout writes the functions it lists first, by tier, in their tier's group", async () => { + const plain = definitions(await emitLlvm()).filter((d) => /_(first|second|third)$/.test(d.name)); + expect(plain.map((d) => d.name.replace(/^sc_b?f_/, ""))).toEqual(["first", "second", "third"]); + expect(plain.every((d) => !d.group.startsWith("layout-"))).toBe(true); + + const names = new Map(plain.map((d) => [d.name.replace(/^sc_b?f_/, ""), d.name])); + const laid = definitions(await emitLlvm(`2 ${names.get("first")}\n1 ${names.get("third")}\n`)); + const index = (fn: string) => laid.findIndex((d) => d.name === names.get(fn)); + expect(index("third")).toBeLessThan(index("first")); + expect(index("first")).toBeLessThan(index("second")); + expect(laid[index("third")]!.group).toBe("layout-1"); + expect(laid[index("first")]!.group).toBe("layout-2"); + expect(laid[index("second")]!.group).not.toMatch(/^layout-/); + // Listed functions precede every unlisted definition in the function section. + const firstUnlisted = laid.findIndex( + (d, i) => i > index("third") && !d.group.startsWith("layout-"), + ); + expect(firstUnlisted).toBeGreaterThan(index("first")); +}); + +test("layout keys leave out module numbers", () => { + expect(functionLayoutKey("sc_bf__x25_m447_lowerCall")).toBe( + functionLayoutKey("sc_bf__x25_m12_lowerCall"), + ); + expect(functionLayoutKey("sc_f__x25_init_253")).toBe(functionLayoutKey("sc_f__x25_init_7")); + expect(functionLayoutKey("sc_bf__x25_m3_Lexer_scan")).not.toBe( + functionLayoutKey("sc_bf__x25_m3_Lexer_peek"), + ); +}); + +test("a function listed twice keeps its first tier, and malformed lines are skipped", () => { + const layout = parseFunctionLayout("1 sc_f_a\n2 sc_f_a\n2 sc_f_b\nnot a line\n0 sc_f_c\n\n"); + expect([...layout]).toEqual([ + ["sc_f_a", 1], + ["sc_f_b", 2], + ]); + const text = + "define internal void @sc_f_b() {\n ret void\n}\n\ndefine void @sc_f_a() {\n ret void\n}"; + expect(layoutTierOf(layout, text)).toBe(1); + expect(layoutTierOf(layout, "define void @sc_f_z() {\n ret void\n}")).toBeUndefined(); +}); + +test("an executable built with a layout that lists helpers and functions runs unchanged", async () => { + const program = ` +class Point { constructor(public x: number, public y: number) {} } +function norm(p: Point): number { return Math.sqrt(p.x * p.x + p.y * p.y); } +const points = [new Point(3, 4), new Point(6, 8)]; +console.log(points.map(norm).join(","), points[0]!.x === 3 ? "three" : "other"); +`; + const dir = await mkdtemp(join(tmpdir(), "scriptc-function-layout-exe-")); + try { + const entry = join(dir, "main.ts"); + await writeFile(entry, program); + const llvmPath = join(dir, "main.ll"); + const emitted = await compile(entry, { outDir: dir, outPath: llvmPath, outputKind: "llvm" }); + expect(emitted.ok, JSON.stringify(emitted.diagnostics)).toBe(true); + // The generated helpers written before the functions, and one function, + // in two tiers. + const names = definitions(await readFile(llvmPath, "utf8")).map((d) => d.name); + const helpers = names.slice( + 0, + names.findIndex((name) => /^sc_b?f_/.test(name)), + ); + expect(helpers.length).toBeGreaterThan(0); + const norm = names.find((name) => /^sc_b?f_norm$/.test(name))!; + const layout = join(dir, "layout"); + await writeFile(layout, [`1 ${norm}`, ...helpers.map((name) => `2 ${name}`)].join("\n")); + const laid = join(dir, "laid.ll"); + const laidOut = await compile(entry, { + outDir: dir, + outPath: laid, + outputKind: "llvm", + functionLayout: layout, + }); + expect(laidOut.ok, JSON.stringify(laidOut.diagnostics)).toBe(true); + const order = definitions(await readFile(laid, "utf8")); + expect(order.filter((d) => d.group === "layout-2").map((d) => d.name)).toEqual(helpers); + expect(order.find((d) => d.name === norm)!.group).toBe("layout-1"); + const exe = join(dir, "main"); + const built = await compile(entry, { + outputKind: "exe", + outDir: dir, + outPath: exe, + functionLayout: layout, + }); + expect(built.ok, JSON.stringify(built.diagnostics)).toBe(true); + expect(execFileSync(exe, { encoding: "utf8" })).toBe("5,10 three\n"); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); + +test("a changed function layout misses the executable cache", async () => { + vi.stubEnv("SCRIPTC_NO_CACHE", undefined); + const dir = await mkdtemp(join(tmpdir(), "scriptc-function-layout-cache-")); + try { + vi.stubEnv("SCRIPTC_CACHE_DIR", join(dir, "cache")); + const entry = join(dir, "main.ts"); + await writeFile(entry, source); + const exe = join(dir, "main"); + const llvm = join(dir, "main.ll"); + const build = async (functionLayout?: string) => { + const built = await compile(entry, { + outputKind: "exe", + outDir: dir, + outPath: exe, + ...(functionLayout === undefined ? {} : { functionLayout }), + }); + expect(built.ok, JSON.stringify(built.diagnostics)).toBe(true); + return definitions(await readFile(llvm, "utf8")); + }; + const plain = await build(); + const third = plain.find((d) => /^sc_b?f_third$/.test(d.name))!.name; + const first = plain.find((d) => /^sc_b?f_first$/.test(d.name))!.name; + const layout = join(dir, "layout"); + await writeFile(layout, `1 ${third}\n`); + expect((await build(layout)).find((d) => d.name === third)!.group).toBe("layout-1"); + // The same path with other contents is another layout. + await writeFile(layout, `1 ${first}\n`); + const edited = await build(layout); + expect(edited.find((d) => d.name === first)!.group).toBe("layout-1"); + expect(edited.find((d) => d.name === third)!.group).not.toMatch(/^layout-/); + } finally { + vi.unstubAllEnvs(); + await rm(dir, { recursive: true, force: true }); + } +}); diff --git a/packages/compiler/test/generator-emission.test.ts b/packages/compiler/test/generator-emission.test.ts index 8c43925ea..47e5f16c5 100644 --- a/packages/compiler/test/generator-emission.test.ts +++ b/packages/compiler/test/generator-emission.test.ts @@ -50,7 +50,7 @@ console.log(sum); expect(definition(llvm, "sc_f_range")).not.toContain("@scr_coro_yield_f64("); expect(llvm).not.toContain("@scr_gen_take_out_f64("); expect(llvm).toContain("%ScrGenHead = type { i64, ptr, ptr, i32, %ScrGenSlot }"); - expect(llvm).toContain("attributes #1 = { sanitize_address presplitcoroutine }"); + expect(llvm).toContain("attributes #1 = { presplitcoroutine }"); expect(definition(llvm, "sc_gs_range")).toContain("call ptr @scr_gen_new_stackless("); expect(llvm).not.toContain("@scr_gen_yield_f64("); }); diff --git a/packages/compiler/test/int32-dynamic-exposure.test.ts b/packages/compiler/test/int32-dynamic-exposure.test.ts index 09330e057..58b0defbb 100644 --- a/packages/compiler/test/int32-dynamic-exposure.test.ts +++ b/packages/compiler/test/int32-dynamic-exposure.test.ts @@ -42,11 +42,11 @@ console.log(total, values, inspect(new Type(1, 2))); console.log(JSON.stringify(values), String(values[0]), typeof values[2]); `; -async function lowered(): Promise { +async function lowered(program = source): Promise { const dir = await mkdtemp(join(tmpdir(), "scriptc-int32-dynamic-")); try { const entry = join(dir, "main.ts"); - await writeFile(entry, source); + await writeFile(entry, program); const outPath = join(dir, "main.ir.json"); const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" }); if (!result.ok) @@ -82,3 +82,44 @@ test("classes that only reach dynamic reads keep their int32 fields", async () = // The program reaches the helper that names class prototypes for inspection. expect(mod.functions.some((fn) => fn.name.startsWith("%prototype.data."))).toBe(true); }); + +// A checked cast of a dynamic value that another holder still references +// adopts the source (dyn.adoptShared): the adoption reads the value it built +// and stores no property, so classes that reach dynamic code keep their int32 +// fields. +const adopting = ` +class Node { + kind: number; + flags = 0; + constructor(kind: number) { + this.kind = kind; + } +} +type Job = { name: string; tags: string[] }; + +const values: unknown[] = [new Node(1), new Node(2)]; +let total = 0; +for (const value of values) if (value instanceof Node) total += value.kind | value.flags; +const raw: unknown = JSON.parse(process.argv[2] ?? '{"name":"a","tags":["t"]}'); +const job = raw as Job; +const same = raw as Job; +job.tags.push("u"); +console.log(total, values, same.tags.join(","), job.name); +`; + +test("adopting checked casts keep int32 fields of dynamically reached classes", async () => { + const mod = await lowered(adopting); + const adopts = mod.functions.filter((fn) => + JSON.stringify(fn.body).includes('"fn":"dyn.adoptShared"'), + ); + expect(adopts.length).toBeGreaterThan(0); + expect(dynamicFieldStores(mod).all).toBe(false); + const slots = analyzeInt32Slots(mod); + const node = mod.classes!.find( + (c) => (c.jsName ?? c.name) === "Node" || c.name.endsWith(".Node"), + )!; + expect([slots.isField(node.name, "kind"), slots.isField(node.name, "flags")]).toEqual([ + true, + true, + ]); +}); diff --git a/packages/compiler/test/int32-element-array-emission.test.ts b/packages/compiler/test/int32-element-array-emission.test.ts index 0b68a5d3e..fccf06973 100644 --- a/packages/compiler/test/int32-element-array-emission.test.ts +++ b/packages/compiler/test/int32-element-array-emission.test.ts @@ -3,8 +3,10 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { expect, test } from "vitest"; import { compile, deserializeModule } from "../src/index.js"; -import type { IrModule } from "../src/ir/ir.js"; +import type { IrFunction, IrModule } from "../src/ir/ir.js"; import { markInt32ArrayCandidates, selectInt32Arrays } from "../src/ir/int32-arrays.js"; +import { analyzeInt32Slots, type Int32Slots } from "../src/ir/int32-slots.js"; +import { serializeModule } from "../src/ir/serialize.js"; async function emitLlvm(source: string): Promise { const dir = await mkdtemp(join(tmpdir(), "scriptc-int32-elements-")); @@ -77,6 +79,19 @@ function literalCounter(n: number): number { for (let i = 0; i < n; i++) a[0] = a[0]! + 1; return a[0]!; } +function walk(n: number, steps: number): number { + const next: number[] = []; + for (let i = 0; i < n; i++) next.push(i + 1 < n ? i + 1 : 0); + let at = next[0]!; + let total = 0; + for (let s = 0; s < steps; s++) { + const t = next[at]!; + next[at] = at; + at = t; + total = (total + at) | 0; + } + return total; +} function joined(n: number): string { const a = [n | 0, 2]; return a.join(","); @@ -91,6 +106,7 @@ console.log( shortCounter(), literalCounter(n), joined(n), + walk(n | 0, 5), ); `; @@ -119,6 +135,16 @@ test("arrays storing only int32 elements get 4-byte slots", async () => { expect(counts).not.toContain("popArray"); }); +test("an array that stores a cursor read back from its own elements gets 4-byte slots", async () => { + const llvm = await emitLlvm(source); + const walk = functions(llvm, "walk"); + expect(walk).not.toBe(""); + // `at` is assigned `next[at]!` in the loop and stored back into `next`: + // it holds an int32 whenever the array's elements are int32s. + expect(walk).toMatch(/@scr_arr_new_small\(i32 7\)/); + expect(walk).toMatch(/getelementptr inbounds i32, ptr %\w+, i64 %\w+\n\s+%\w+ = load i32/); +}); + test("arrays with a store that has no exact integer range, or that escape, keep doubles", async () => { const llvm = await emitLlvm(source); for (const name of ["fraction", "negativeZero", "joined"]) { @@ -186,13 +212,96 @@ test("only the candidates the proof keeps are rewritten", async () => { // An array that reaches `join` is never proposed. expect(proposed.some((key) => key.startsWith("joined\0"))).toBe(false); // Without a kept candidate, the module is the input itself. - expect(selectInt32Arrays(mod, () => new Set())).toBe(mod); + expect(selectInt32Arrays(mod, () => ({ kept: new Set(), functions: null }))).toBe(mod); const kept = new Set(["compose\0p.0", "compose\0q.0"]); - const selected = selectInt32Arrays( - mod, - (marked) => new Set((marked.int32ArrayCandidates ?? []).filter((key) => kept.has(key))), - ); + const selected = selectInt32Arrays(mod, (marked) => ({ + kept: new Set((marked.int32ArrayCandidates ?? []).filter((key) => kept.has(key))), + functions: null, + })); expect(selected.int32ArrayCandidates).toEqual([...kept]); for (const fn of selected.functions) if (fn.name !== "compose") expect(fn).toBe(mod.functions.find((f) => f.name === fn.name)); }); + +const sharedSource = ` +const data: number[] = []; +for (let i = 0; i < 64; i++) data.push((i * 7) & 255); +function fnv(values: number[]): number { + let h = 2166136261; + for (let i = 0; i < values.length; i++) h = Math.imul(h ^ values[i]!, 16777619) >>> 0; + return h; +} +function mean(values: number[]): number { + let s = 0; + for (let i = 0; i < values.length; i++) s += values[i]!; + return s / values.length; +} +function first(values: number[]): number { + return values[0]!; +} +const ints = [1, 2, 3]; +const halves = [0.5, 1.5]; +const pick: (values: number[]) => number = first; +console.log(fnv(data), mean(ints), mean(halves), pick([4])); +`; + +test("arrays passed to helpers and held in module globals get 4-byte slots", async () => { + const mod = await lowerIr(sharedSource); + const proposed = markInt32ArrayCandidates(mod).int32ArrayCandidates ?? []; + // A global and the parameter every call passes it to; a function used as + // a value keeps its parameter. + expect(proposed).toEqual(expect.arrayContaining(["\0%g.e.data", "fnv\0values.0"])); + expect(proposed.some((key) => key.startsWith("first\0"))).toBe(false); + const llvm = await emitLlvm(sharedSource); + // The helper reads int32 slots; `mean`, also called with fractions, + // reads doubles. + expect(functions(llvm, "fnv")).toMatch( + /getelementptr inbounds i32, ptr %\w+, i64 %\w+\n\s+%\w+ = load i32/, + ); + expect(functions(llvm, "mean")).not.toMatch(/getelementptr inbounds i32, ptr/); + expect(llvm).toMatch(/@scr_arr_new(_small)?\(i32 7/); +}); + +/** The module selectInt32Arrays selects, the analysis of its last proof, + * and how many proofs it asked for: with `revising`, each proof revises + * the functions of candidates that drop. */ +function selection( + mod: IrModule, + revising: boolean, +): { selected: IrModule; slots: Int32Slots; proofs: number } { + let slots: Int32Slots | null = null; + let proofs = 0; + const selected = selectInt32Arrays(mod, (marked, revise) => { + proofs++; + const latest = new Map(); + slots = analyzeInt32Slots( + marked, + revising && revise !== null + ? { + revise: (fnName, dropped) => { + const fn = revise(fnName, dropped); + latest.set(fnName, fn); + return fn; + }, + } + : {}, + ); + const kept = new Set([...slots.int32Arrays, ...slots.checkedInt32Arrays]); + if (latest.size === 0) return { kept, functions: null }; + return { kept, functions: marked.functions.map((fn) => latest.get(fn.name) ?? fn) }; + }); + return { selected, slots: slots!, proofs }; +} + +test("one proof that revises dropped candidates selects what rounds of proofs select", async () => { + for (const program of [source, sharedSource]) { + const mod = await lowerIr(program); + const rounds = selection(mod, false); + const revised = selection(mod, true); + // The rounds drop a candidate and prove the module again. + expect(rounds.proofs).toBeGreaterThan(1); + expect(revised.proofs).toBe(1); + expect(serializeModule(revised.selected, true)).toBe(serializeModule(rounds.selected, true)); + expect(revised.slots.summary(revised.selected)).toBe(rounds.slots.summary(rounds.selected)); + } +}); diff --git a/packages/compiler/test/int32-slots-transfer.test.ts b/packages/compiler/test/int32-slots-transfer.test.ts new file mode 100644 index 000000000..3ea4f6a6a --- /dev/null +++ b/packages/compiler/test/int32-slots-transfer.test.ts @@ -0,0 +1,88 @@ +import { mkdtemp, readFile, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile, deserializeModule } from "../src/index.js"; +import { findInitializerBindings } from "../src/ir/initializer-bindings.js"; +import { analyzeInt32Slots, dynamicFieldStores, Int32Slots } from "../src/ir/int32-slots.js"; +import type { IrExpr, IrModule } from "../src/ir/ir.js"; +import { IrNodeIndex } from "../src/ir/node-index.js"; +import { unshareNodes } from "../src/ir/unshare.js"; + +// Programs with int32 fields, parameters, results, captured boxes, contract +// pins, int32 arrays and disqualified candidates. +const PROGRAMS = [ + "4451-int32-field-storage.ts", + "4452-int32-field-dynamic-writes.ts", + "4453-int32-closed-locals.ts", + "4630-contract-int32.ts", + "4970-int32-call-abi.ts", + "5323-int32-nonnegative-slot-facts.ts", + "5332-int32-narrowed-slot-facts.ts", + "5930-contract-int32-arrays.ts", + "7326-int32-element-arrays.ts", + "7580-int32-arrays-across-calls.ts", +]; + +async function lower(program: string, dir: string): Promise { + const entry = join(import.meta.dirname, "../../../tests/corpus", program); + const outPath = join(dir, `${program}.ir.json`); + const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" }); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + return readFile(outPath, "utf8"); +} + +/** The analysis LlEmitter makes of `mod`, with its node index. */ +function analyze(mod: IrModule): { nodes: IrNodeIndex; slots: Int32Slots } { + const nodes = new IrNodeIndex(mod); + const slots = analyzeInt32Slots(mod, { + nodes, + dynamicStores: dynamicFieldStores(mod, undefined, undefined, nodes), + initializerBindings: findInitializerBindings(mod, nodes), + }); + return { nodes, slots }; +} + +// A forked process holds its own copy of the module: the encoded analysis +// names nodes by index position, which resolve in any copy of the module. +test("the int32 slot analysis decodes against another copy of its module", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-int32-transfer-")); + try { + let provenWrites = 0; + for (const program of PROGRAMS) { + const ir = await lower(program, dir); + const first = analyze(unshareNodes(deserializeModule(ir))); + const copy = unshareNodes(deserializeModule(ir)); + const second = analyze(copy); + const text = first.slots.encode(first.nodes); + expect(text, program).not.toBeNull(); + const decoded = Int32Slots.decode(text!, copy, second.nodes); + expect(decoded, program).toEqual(second.slots); + for (const node of decoded.provenWrites) + expect(second.slots.provenWrites.has(node), program).toBe(true); + provenWrites += decoded.provenWrites.size; + } + expect(provenWrites).toBeGreaterThan(0); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}, 120_000); + +test("an analysis whose proven writes the index does not record does not encode", () => { + const loc = { file: "test.ts", start: 0, end: 0 }; + const mod: IrModule = { + irVersion: 17, + sourceFile: "test.ts", + entry: "%main", + functions: [ + { loc, name: "%main", params: [], locals: [], returnType: { kind: "void" }, body: [] }, + ], + }; + const { nodes, slots } = analyze(mod); + expect(slots.encode(nodes)).not.toBeNull(); + const elsewhere: IrExpr = { loc, kind: "numLit", value: 1, type: { kind: "f64" } }; + slots.provenWrites.add(elsewhere); + expect(nodes.expressionPositions(slots.provenWrites)).toBeNull(); + expect(slots.encode(nodes)).toBeNull(); +}); diff --git a/packages/compiler/test/integer-view-compares.test.ts b/packages/compiler/test/integer-view-compares.test.ts new file mode 100644 index 000000000..4e814368d --- /dev/null +++ b/packages/compiler/test/integer-view-compares.test.ts @@ -0,0 +1,60 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile, deserializeModule } from "../src/index.js"; +import { emitLlvmModule } from "../src/backend/llvm/emitter.js"; + +const source = `function siftDown(a: Int32Array, start: number, end: number): void { + let root = start; + while (2 * root + 1 < end) { + let child = 2 * root + 1; + if (child + 1 < end && a[child]! < a[child + 1]!) child++; + if (a[root]! >= a[child]!) return; + const t = a[root]!; + a[root] = a[child]!; + a[child] = t; + root = child; + } +} +const a = new Int32Array([3, 9, 1, 7, 5]); +siftDown(a, 0, Number(process.argv[2] ?? "5")); +console.log(Array.from(a).join()); +`; + +test("integer values compare and index as integers", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-integer-views-")); + try { + const entry = join(dir, "sift.ts"); + await writeFile(entry, source); + const outPath = join(dir, "sift.ir.json"); + const result = await compile(entry, { outPath, outDir: dir, outputKind: "ir" }); + if (!result.ok) throw new Error(result.diagnostics.map((d) => d.message).join("\n")); + const ll = emitLlvmModule(deserializeModule(await readFile(outPath, "utf8"))); + const body = ll.match( + /define internal [^\n]+ @sc_bf_siftDown(?:\.i32)?\([^\n]*\) #0 \{([\s\S]*?)\n\}/, + )![1]!; + // `child + 1`: the integer the speculated copy already adds. + const sums = [...body.matchAll(/(%t\d+) = add nsw i64 %t\d+, 1\n/g)].map((m) => m[1]!); + expect(sums.length).toBeGreaterThan(0); + const uses = (op: string) => + sums.filter((sum) => new RegExp(`= ${op} i64 \\${sum}, %t\\d+\\n`).test(body)); + // `child + 1 < end` compares the integers, + expect(uses("icmp slt").length).toBeGreaterThan(0); + // and `a[child + 1]` bounds the integer index without a double round trip. + expect(uses("icmp ult").length).toBeGreaterThan(0); + // `2 * root + 1` may pass 2^53, but its double is its integer rounded + // once (doubling is exact) and `end` is a safe integer, so the integers + // compare as the doubles do. + const doubled = /(%t\d+) = mul nsw i64 2, %t\d+\n/.exec(body)![1]!; + const plusOne = new RegExp(`(%t\\d+) = add nsw i64 \\${doubled}, 1\\n`).exec(body)![1]!; + expect(body).toMatch(new RegExp(`= icmp slt i64 \\${plusOne}, %t\\d+\\n`)); + // `a[child]! < a[child + 1]!` compares the elements' int32 bits, and is + // false when either read is out of range (NaN). + const ordered = String.raw`\s+%t\d+ = fcmp ord double %t\d+, 0\.0\n\s+%t\d+ = and i1 %t\d+, %t\d+\n`; + expect(body).toMatch(new RegExp(String.raw`icmp slt i32 %t\d+, %t\d+\n(${ordered}){2}`)); + expect(body).toMatch(new RegExp(String.raw`icmp sge i32 %t\d+, %t\d+\n(${ordered}){2}`)); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); diff --git a/packages/compiler/test/ir.test.ts b/packages/compiler/test/ir.test.ts index 7f1a1ef78..5d835ef8a 100644 --- a/packages/compiler/test/ir.test.ts +++ b/packages/compiler/test/ir.test.ts @@ -8,10 +8,13 @@ test("hand-built fib module validates", () => { expect(validateModule(fibModule)).toEqual([]); }); -test("validator rejects Date union arms unsupported by the backends", () => { - const bad = structuredClone(fibModule); - bad.unions = [{ id: "u-date", arms: [{ kind: "date" }, { kind: "undefinedT" }] }]; - expect(validateModule(bad).map((e) => e.message)).toContain("union u-date: arm 0 is date"); +test("validator accepts Date union arms beside other arms", () => { + const mixed = structuredClone(fibModule); + mixed.unions = [{ id: "u-date", arms: [{ kind: "date" }, { kind: "string" }] }]; + expect(validateModule(mixed)).toEqual([]); + const nullable = structuredClone(fibModule); + nullable.unions = [{ id: "u-date", arms: [{ kind: "date" }, { kind: "undefinedT" }] }]; + expect(validateModule(nullable)).toEqual([]); }); test("fib module JSON round-trips", () => { diff --git a/packages/compiler/test/js-class-layouts.test.ts b/packages/compiler/test/js-class-layouts.test.ts index c8491f3e8..987703110 100644 --- a/packages/compiler/test/js-class-layouts.test.ts +++ b/packages/compiler/test/js-class-layouts.test.ts @@ -101,6 +101,51 @@ delete loose.label; expect(layout(deleted, "Shape").tracks).toBe(true); }); +test("freezing values other than class instances keeps JavaScript class layouts fixed", async () => { + const module = await lower(`${SHAPES} +/** @param {{ value: any }} data */ +function settle(data) { + data.value = Object.freeze(data.value); + Object.seal(data); + return Object.preventExtensions(data.value); +} +settle({ value: { sides: 1 } }); +const square = new Square(); +Object.freeze(square); +`); + expect(layout(module, "Shape").tracks).toBe(false); + expect(layout(module, "Square").tracks).toBe(false); +}); + +test("accessors that share no field's name keep JavaScript class layouts fixed", async () => { + const module = await lower(` +class Path { + constructor(key) { + this.cache = []; + this.key = key; + } + get path() { + if (!this.cache.length) this.cache.push(this.key); + return this.cache; + } +} +class Named extends Path { + constructor(key) { + super(key); + this.label = "named"; + } + /** @param {string} value */ + set label2(value) { + this.label = value; + } +} +console.log(new Named("k").path.length); +`); + expect(layout(module, "Path")).toEqual({ fields: "cache:dyn key:dyn", tracks: false }); + expect(layout(module, "Named").tracks).toBe(false); + expect(layout(module, "Named").fields).toBe("cache:dyn key:dyn label:string"); +}); + test("JavaScript subclass fields keep their types when the base constructor only assigns fields", async () => { const module = await lower(` class Base { @@ -176,3 +221,21 @@ test("a JavaScript file TypeScript checks uses its JSDoc types for class layouts expect(layout(unchecked, "Account").fields).toBe("owner:dyn balance:dyn"); expect(params(unchecked, "%Account.constructor")).toBe("(object, dyn, dyn): void"); }); + +test("instances of a class with subclasses box at their exact class", async () => { + const module = await lower(`${SHAPES} +/** @param {any} value */ +function keep(value) { + return value; +} +/** @param {Shape} shape */ +function cross(shape) { + return keep(shape); +} +console.log(cross(new Square()).size, cross(new Shape()).label); +`); + const helper = module.functions.find((fn) => fn.name === "%dyn.class.boxExact:Shape"); + expect(helper).toBeDefined(); + expect(calls(module, "cross")).toContain("%dyn.class.boxExact:Shape"); + expect(JSON.stringify(helper!.body)).toContain('"className":"Square"'); +}); diff --git a/packages/compiler/test/js-collection-types.test.ts b/packages/compiler/test/js-collection-types.test.ts new file mode 100644 index 000000000..7a5ef967a --- /dev/null +++ b/packages/compiler/test/js-collection-types.test.ts @@ -0,0 +1,91 @@ +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile, deserializeModule, validateModule } from "../src/index.js"; + +// Corpus 7985 exercises every refusal: only the collections whose uses +// prove their types lower as typed collections; the rest stay untyped. +test("untyped JavaScript collections take the types their uses prove, and only those", async () => { + const dir = mkdtempSync(join(tmpdir(), "scriptc-js-collection-types-")); + try { + const entry = join( + import.meta.dirname, + "../../../tests/corpus/7985-untyped-collection-types.mjs", + ); + const output = join(dir, "main.ir.json"); + const lowered = await compile(entry, { outputKind: "ir", outDir: dir, outPath: output }); + if (!lowered.ok) throw new Error(JSON.stringify(lowered.diagnostics)); + const module = deserializeModule(readFileSync(output, "utf8")); + expect(validateModule(module)).toEqual([]); + const types = new Map(module.globals.map((g) => [g.name, JSON.stringify(g.type)])); + const map = (key: string, value: string) => + JSON.stringify({ kind: "map", key: { kind: key }, value: { kind: value } }); + const set = (elem: string) => JSON.stringify({ kind: "set", elem: { kind: elem } }); + const array = (elem: string) => JSON.stringify({ kind: "array", elem: { kind: elem } }); + // Untyped collections hold checked-dynamic keys, values and elements. + const typed = (t: string) => !t.includes('"dyn"'); + expect(Object.fromEntries([...types].filter(([, t]) => typed(t)))).toEqual({ + counts: map("string", "f64"), + seen: set("f64"), + stack: array("f64"), + words: array("string"), + byNumber: map("f64", "string"), + nums: array("f64"), + flags: set("bool"), + cache: map("string", "f64"), + "%loaded": '{"kind":"bool"}', + }); + for (const untyped of [ + "strings", + "numbers", + "mixedKeys", + "mixedValues", + "mixedElements", + "passed", + "stored", + "iterated", + "eachSet", + "shown", + "chained", + "optional", + "indexed", + "truncated", + "spread", + "unread", + "exported", + "listed", + "renamed", + ]) + expect(typed(types.get(untyped)!), untyped).toBe(false); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +}); + +// Resolving a candidate's constructor through a destructured require reads +// types while the inference runs; those reads must not start it again. +test("the inference runs once when a candidate's values come from a destructured require", async () => { + const dir = mkdtempSync(join(tmpdir(), "scriptc-js-collection-reentry-")); + try { + const entry = join(dir, "main.cjs"); + writeFileSync( + entry, + [ + 'const util = require("node:util");', + "const { TextDecoder } = util;", + "const decoder = new TextDecoder();", + 'const decoders = new Map([["utf-8", decoder]]);', + "const seen = new Set([decoder]);", + "console.log(decoders.size, seen.size);", + "", + ].join("\n"), + ); + const output = join(dir, "main.ir.json"); + const lowered = await compile(entry, { outputKind: "ir", outDir: dir, outPath: output }); + if (!lowered.ok) throw new Error(JSON.stringify(lowered.diagnostics)); + expect(validateModule(deserializeModule(readFileSync(output, "utf8")))).toEqual([]); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +}); diff --git a/packages/compiler/test/js-ic-emission.test.ts b/packages/compiler/test/js-ic-emission.test.ts index 47d641bff..b350c743d 100644 --- a/packages/compiler/test/js-ic-emission.test.ts +++ b/packages/compiler/test/js-ic-emission.test.ts @@ -48,7 +48,7 @@ console.log(read(o), at(dyn([5, 6]), 1), call(o), o.n); expect(llvm).toMatch( /@sc_js_ic_0 = internal (thread_local )?global \[21 x i64\] zeroinitializer/, ); - expect(body(llvm, "read")).toContain("@scr_js_load_probe("); + expect(body(llvm, "read")).toContain("@scr_js_get_probe("); expect(body(llvm, "write")).toContain("@scr_js_store_probe("); expect(body(llvm, "at")).toContain("@scr_js_keyed_probe("); expect(body(llvm, "call")).toContain("@scr_js_load_probe("); @@ -64,6 +64,81 @@ console.log(read(o), at(dyn([5, 6]), 1), call(o), o.n); } }); +test("object literal members with constant keys add through store caches", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-js-ic-literal-")); + try { + const entry = join(dir, "main.js"); + const outPath = join(dir, "main.ir.json"); + await writeFile( + entry, + ` +function make(v, k) { return { a: v, b: 2, [k]: 3 }; } +var top = { t: 1 }; +console.log(JSON.stringify(make(1, "c")), top.t); +`, + ); + const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" }); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + const mod = deserializeModule(await readFile(outPath, "utf8")); + expect(moduleUsesDynTier(mod)).toBe(true); + const llvm = emitLlvmModule(mod); + const make = body(llvm, "make"); + // Each constant-key member tries its site's store cache first and + // falls back to the generic member store; the computed key keeps the + // generic store only. + expect(make.match(/@scr_js_store_probe\(/g)).toHaveLength(2); + expect(make.match(/@scr_dyn_key_set\(/g)).toHaveLength(2); + expect(make).toContain("@scr_dyn_key_set_computed("); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); + +test("property sites keep their caches in programs that declare classes and subclasses", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-js-ic-class-")); + try { + const entry = join(dir, "main.js"); + const outPath = join(dir, "main.ir.json"); + // Declared classes give every property helper class branches and + // property-bag lookups, and a subclass adds a normalization step; plain + // objects still read through the caches. + await writeFile( + entry, + ` +class Point { + constructor(x) { this.x = x; } +} +class Tagged extends Point { + constructor(x, tag) { super(x); this.tag = tag; } +} +function dyn(v) { return v; } +function read(o) { return o.x; } +function write(o, v) { o.x = v; } +function at(a, i) { return a[i]; } +var o = dyn({ x: 1 }); +var p = dyn(new Point(4)); +var t = dyn(new Tagged(6, "t")); +write(o, 3); +write(p, 5); +write(t, 7); +console.log(read(o), read(p), read(t), at(dyn([5, 6]), 1), at(p, "x"), at(t, "tag")); +`, + ); + const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" }); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + const mod = deserializeModule(await readFile(outPath, "utf8")); + expect(validateModule(mod)).toEqual([]); + const llvm = emitLlvmModule(mod); + expect(body(llvm, "read")).toContain("@scr_js_get_probe("); + expect(body(llvm, "write")).toContain("@scr_js_store_probe("); + expect(body(llvm, "at")).toContain("@scr_js_keyed_probe("); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); + test("typed programs have no cache sites and keep the ordinary runtime", async () => { const dir = await mkdtemp(join(tmpdir(), "scriptc-js-ic-typed-")); try { @@ -90,3 +165,44 @@ console.log(len2({ x: 3, y: 4 }), typeof parsed); await rm(dir, { recursive: true, force: true }); } }); + +test("keyed and length sites read native number arrays in place", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-js-ic-native-")); + try { + const entry = join(dir, "main.ts"); + const outPath = join(dir, "main.ir.json"); + await writeFile(join(dir, "package.json"), `{ "type": "module" }`); + await writeFile( + join(dir, "lib.cjs"), + `exports.total = function (xs) { + let s = 0; + for (let i = 0; i < xs.length; i++) s += xs[i]; + return s; +}; +`, + ); + await writeFile( + entry, + `import { createRequire } from "node:module"; +const require = createRequire(import.meta.url); +const lib: { total(xs: number[]): number } = require("./lib.cjs"); +console.log(lib.total([1, 2, 3])); +`, + ); + const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" }); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + const llvm = emitLlvmModule(deserializeModule(await readFile(outPath, "utf8"))); + const fn = llvm.split("\ndefine ").find((f) => f.includes("ica.f64")) ?? ""; + // The keyed read tests the capsule's element kind and loads the double + // in place; other receivers keep the probe. + expect(fn).toMatch(/icmp eq i8 %t\d+, 0 ; SCR_ELEM_F64/); + expect(fn).toMatch(/load double, ptr %t\d+/); + expect(fn).toContain("@scr_js_keyed_probe("); + // The length read answers from the native array's length. + expect(fn).toContain("icg.rest"); + expect(fn).toContain("@scr_js_get_probe_native("); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); diff --git a/packages/compiler/test/json-schema-parse-emission.test.ts b/packages/compiler/test/json-schema-parse-emission.test.ts index b96c9a032..dd6bf52ae 100644 --- a/packages/compiler/test/json-schema-parse-emission.test.ts +++ b/packages/compiler/test/json-schema-parse-emission.test.ts @@ -58,13 +58,13 @@ console.log(orders.length, typed.sku, loose.name, mixed.length, pair[0], either. // record pointer (0), the inline scalar pair (1), and a tagged box built // through the schema's box function (2), with arm tags by JSON kind. expect(llvm).toMatch( - /@sc_jsch_\d+_union = internal constant \{ i64, \[7 x i64\], \[7 x ptr\], ptr, ptr, ptr \} \{ i64 0, /, + /@sc_jsch_\d+_union = internal constant \{ i64, \[7 x i64\], \[7 x ptr\], ptr, ptr, ptr, ptr \} \{ i64 0, /, ); expect(llvm).toMatch( - /@sc_jsch_\d+_union = internal constant \{ i64, \[7 x i64\], \[7 x ptr\], ptr, ptr, ptr \} \{ i64 1, /, + /@sc_jsch_\d+_union = internal constant \{ i64, \[7 x i64\], \[7 x ptr\], ptr, ptr, ptr, ptr \} \{ i64 1, /, ); expect(llvm).toMatch( - /@sc_jsch_\d+_union = internal constant \{ i64, \[7 x i64\], \[7 x ptr\], ptr, ptr, ptr \} \{ i64 2, .*ptr @sc_jsch_\d+_box \}/, + /@sc_jsch_\d+_union = internal constant \{ i64, \[7 x i64\], \[7 x ptr\], ptr, ptr, ptr, ptr \} \{ i64 2, .*ptr @sc_jsch_\d+_box, ptr null \}/, ); expect(llvm).toMatch(/^define internal ptr @sc_jsch_\d+_box\(i32 %tag, i64 %bits\)/m); // A pure dictionary has no declared fields: its index value schema and @@ -78,9 +78,59 @@ console.log(orders.length, typed.sku, loose.name, mixed.length, pair[0], either. const wasm = emitLlvmModule(mod, { pointerBits: 32, wasi: true }); expect(wasm).toContain("= internal constant { i32, i32, ptr, ptr, ptr, ptr, ptr, ptr, i32 }"); - expect(wasm).toContain("= internal constant { i32, [7 x i32], [7 x ptr], ptr, ptr, ptr }"); + expect(wasm).toContain("= internal constant { i32, [7 x i32], [7 x ptr], ptr, ptr, ptr, ptr }"); expect(wasm).toMatch(/\{ ptr, i32, i32, ptr \} \{ ptr @sc_cs_\d+, i32 3, i32 ptrtoint/); } finally { await rm(dir, { recursive: true, force: true }); } }); + +test("JSON.parse casts to discriminated unions fuse into the schema parser", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-json-schema-union-")); + try { + const entry = join(dir, "main.ts"); + const outPath = join(dir, "main.ir.json"); + await writeFile( + entry, + ` +type Event = + | { kind: "click"; x: number; y: number } + | { kind: "focus" | "blur"; target: string } + | { kind: "load"; ms: number; next: Event | null }; +type Expr = { kind: "num"; value: number } | { kind: "add"; items: Expr[] }; +type Loose = { kind: string; a: number } | { kind: string; b: string }; +const text = process.argv[2] ?? "[]"; +const events = JSON.parse(text) as Event[]; +const tree = JSON.parse(text) as Expr; +const loose = JSON.parse(text) as Loose[]; +console.log(events.length, tree.kind, loose.length); +`, + ); + const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" }); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + const llvm = emitLlvmModule(deserializeModule(await readFile(outPath, "utf8"))); + // Event[] and the whole-document Expr take the fused path; a union + // without literal discriminants keeps the checked-dynamic route alone. + expect(llvm.match(/^define internal ptr @sc_jp_\d+(?:\.i32)?\(ptr %text\)/gm)).toHaveLength(2); + expect(llvm.match(/call ptr @scr_json_parse\(ptr /g)).toHaveLength(3); + // Each literal names its arm's tag and record schema; an arm with two + // literals is listed under both. Event, `Event | null` and Expr each + // have a table. + const cases = [...llvm.matchAll(/^@sc_jsch_\d+_case_table = internal constant \[(\d+) x /gm)]; + expect(cases.map((m) => Number(m[1])).sort()).toEqual([2, 4, 4]); + expect(llvm).toMatch( + /^@sc_jsch_\d+_cases = internal constant \{ ptr, i64, i64, ptr \} \{ ptr @sc_cs_\d+, i64 4, i64 4, ptr @sc_jsch_\d+_case_table \}/m, + ); + // The union descriptors point at their cases, and a union schema that + // can be a whole document carries its release. + expect(llvm).toMatch( + /@sc_jsch_\d+_union = internal constant \{ i64, \[7 x i64\], \[7 x ptr\], ptr, ptr, ptr, ptr \} \{ .*, ptr @sc_jsch_\d+_cases \}/, + ); + expect(llvm).toMatch( + /= internal constant \{ i64, i64, ptr, ptr, ptr, ptr, ptr, ptr, i64 \} \{ i64 5, i64 0, ptr null, ptr null, ptr null, ptr null, ptr @\S+, ptr @sc_jsch_\d+_union, i64 0 \}/, + ); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); diff --git a/packages/compiler/test/keyed-method-writes.test.ts b/packages/compiler/test/keyed-method-writes.test.ts new file mode 100644 index 000000000..407530945 --- /dev/null +++ b/packages/compiler/test/keyed-method-writes.test.ts @@ -0,0 +1,79 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile, deserializeModule } from "../src/index.js"; +import type { IrModule } from "../src/ir/ir.js"; +import { everyStmtList } from "../src/ir/traverse.js"; + +async function lowerSource(source: string): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-keyed-writes-")); + try { + const entry = join(dir, "main.ts"); + await writeFile(entry, source); + const outPath = join(dir, "main.ir.json"); + const result = await compile(entry, { outPath, outDir: dir, outputKind: "ir" }); + if (!result.ok) throw new Error(result.diagnostics.map((d) => d.message).join("\n")); + return deserializeModule(await readFile(outPath, "utf8")); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +/** Calls in `run` that go through a dynamic value. */ +function dynamicCalls(mod: IrModule): number { + const fn = mod.functions.find((f) => f.name === "run"); + if (!fn) throw new Error("no run function"); + let n = 0; + everyStmtList(fn.body, { + expr: (e) => { + if (e.kind === "dynCall" || e.kind === "dynInvoke") n++; + return true; + }, + stmt: () => true, + }); + return n; +} + +const shape = ` +type Step = "scale" | "clamp"; +class Transform { + scale(x: number): number { + return x * 2; + } + clamp(x: number): number { + return x < 0 ? 0 : x; + } +} +`; + +test("element writes of data under method names keep the methods native", async () => { + const mod = await lowerSource(`${shape} +const enabled: Record = { scale: false, clamp: false }; +function enable(name: Step): void { + enabled[name] = true; +} +const totals: Record = {}; +function run(t: Transform): number { + enable("scale"); + totals["clamp"] = 1; + return t.clamp(t.scale(3)); +} +console.log(run(new Transform())); +`); + expect(dynamicCalls(mod)).toBe(0); +}); + +test("element writes through untyped receivers can still replace a method", async () => { + const mod = await lowerSource(`${shape} +function replace(target: object, name: Step): void { + (target as any)[name] = (x: number) => x + 100; +} +function run(t: Transform): number { + replace(t, "scale"); + return t.clamp(t.scale(3)); +} +console.log(run(new Transform())); +`); + expect(dynamicCalls(mod)).toBeGreaterThan(0); +}); diff --git a/packages/compiler/test/llvm-remainder.test.ts b/packages/compiler/test/llvm-remainder.test.ts index 9e6cb02e7..e3f585144 100644 --- a/packages/compiler/test/llvm-remainder.test.ts +++ b/packages/compiler/test/llvm-remainder.test.ts @@ -90,7 +90,7 @@ console.log(remainder(5, 2.5), signed(-7.5), maybeZero(7.5, 2.5)); expect(maybeZero).toContain("rem.float"); expect(ll).not.toContain("@llvm.fptosi.sat"); expect(ll).not.toContain("@fmod"); - expect(ll).toContain("call double @pow("); + expect(ll).toContain("call double @scr_math_pow("); // Where truncation is an integer round trip, a dividend below 2^62 takes // the i64 remainder by the power of two and adds back its fraction; a diff --git a/packages/compiler/test/llvm-runtime-abi.test.ts b/packages/compiler/test/llvm-runtime-abi.test.ts index 6cfd862b7..a457feb72 100644 --- a/packages/compiler/test/llvm-runtime-abi.test.ts +++ b/packages/compiler/test/llvm-runtime-abi.test.ts @@ -355,12 +355,13 @@ describe("LLVM backend declares match scr_runtime.h prototypes", () => { #include "scr_runtime.h" /* LLVM emits: %ScrBytes = type { i64, i64, i32, ptr, ptr, i8, i8, i8, i8, ptr }. - * Keep every ABI fact used by its field-index GEPs explicit here. */ + * Keep every ABI fact used by its field-index GEPs explicit here. The C + * struct continues with fields only the runtime reads. */ _Static_assert(sizeof(size_t) == 8, "LLVM ScrBytes expects 64-bit size_t"); _Static_assert(sizeof(void *) == 8, "LLVM ScrBytes expects 64-bit pointers"); _Static_assert(sizeof(ScrBytesElem) == 4, "LLVM ScrBytes elem field is i32"); _Static_assert(_Alignof(ScrBytes) == 8, "LLVM ScrBytes alignment changed"); -_Static_assert(sizeof(ScrBytes) == 56, "LLVM ScrBytes size changed"); +_Static_assert(sizeof(ScrBytes) == 64, "ScrBytes size changed"); _Static_assert(offsetof(ScrBytes, rc) == 0, "LLVM ScrBytes.rc offset changed"); _Static_assert(offsetof(ScrBytes, len) == 8, "LLVM ScrBytes.len offset changed"); _Static_assert(offsetof(ScrBytes, elem) == 16, "LLVM ScrBytes.elem offset changed"); @@ -370,7 +371,9 @@ _Static_assert(offsetof(ScrBytes, is_buffer) == 40, "LLVM ScrBytes.is_buffer off _Static_assert(offsetof(ScrBytes, is_data_view) == 41, "LLVM ScrBytes.is_data_view offset changed"); _Static_assert(offsetof(ScrBytes, external) == 42, "LLVM ScrBytes.external offset changed"); _Static_assert(offsetof(ScrBytes, untransferable) == 43, "LLVM ScrBytes.untransferable offset changed"); +_Static_assert(offsetof(ScrBytes, properties_traced) == 44, "ScrBytes.properties_traced must stay in padding"); _Static_assert(offsetof(ScrBytes, shared) == 48, "LLVM ScrBytes.shared offset changed"); +_Static_assert(offsetof(ScrBytes, properties) == 56, "ScrBytes.properties must follow the LLVM prefix"); `, ); const driver = resolveCc(); diff --git a/packages/compiler/test/localized-failure-retries.test.ts b/packages/compiler/test/localized-failure-retries.test.ts new file mode 100644 index 000000000..6bf1b1e23 --- /dev/null +++ b/packages/compiler/test/localized-failure-retries.test.ts @@ -0,0 +1,70 @@ +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile } from "../src/index.js"; + +async function compileSource(source: string) { + const directory = mkdtempSync(join(tmpdir(), "scriptc-localized-")); + try { + const entry = join(directory, "main.ts"); + writeFileSync(entry, source); + return await compile(entry, { + outDir: directory, + outPath: join(directory, "program.ir"), + outputKind: "ir", + }); + } finally { + rmSync(directory, { recursive: true, force: true }); + } +} + +// A statement that lowers again after a later declarator degrades a type +// reuses the member bodies of a function-local class it lowered the first +// time, so the diagnostics of those bodies still report. +test("a degrading retry keeps the diagnostics of a local class lowered before it", async () => { + const result = await compileSource(`type Progress = (done: number, total: number) => void; +type Callbacks = Progress | AbortSignal | Map string> | Set; +function make(): number { + const K = class { + m(): number { + const a = [1, 2]; + (a as any).foo = 1; + return a.length; + } + }, + size = (cb: Callbacks): number => (cb instanceof Set ? cb.size : 0); + return new K().m() + size(new Set([1])); +} +console.log(make()); +`); + expect(result.ok).toBe(false); + if (result.ok) throw new Error("expected the local class's diagnostic"); + expect(result.diagnostics).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + code: "SC1090", + message: expect.stringContaining("assignment to non-variables"), + }), + ]), + ); +}); + +// Without the failing statement, the same program compiles: the arrow's +// parameter is a dynamic slot. +test("the same program without the local class's failure compiles", async () => { + const result = await compileSource(`type Progress = (done: number, total: number) => void; +type Callbacks = Progress | AbortSignal | Map string> | Set; +function make(): number { + const K = class { + m(): number { + return 2; + } + }, + size = (cb: Callbacks): number => (cb instanceof Set ? cb.size : 0); + return new K().m() + size(new Set([1])); +} +console.log(make()); +`); + expect(result.ok, JSON.stringify(result.ok ? [] : result.diagnostics)).toBe(true); +}); diff --git a/packages/compiler/test/map-get-borrow-emission.test.ts b/packages/compiler/test/map-get-borrow-emission.test.ts new file mode 100644 index 000000000..4e3c91766 --- /dev/null +++ b/packages/compiler/test/map-get-borrow-emission.test.ts @@ -0,0 +1,93 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile, deserializeModule } from "../src/index.js"; +import { emitLlvmModule } from "../src/backend/llvm/emitter.js"; + +test("a Map.get local read before any reference write holds the entry's reference", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-map-get-borrow-")); + try { + const entry = join(dir, "main.ts"); + const outPath = join(dir, "main.ir.json"); + await writeFile( + entry, + ` +class Counter { + num = 1; +} +function count(m: Map, keys: number[]): void { + for (const key of keys) { + const cur = m.get(key); + if (cur === undefined) m.set(key, new Counter()); + else cur.num++; + } +} +function named(m: Map, key: string): number { + const cur = m.get(key); + return cur === undefined ? 0 : cur.num; +} +function replaced(m: Map): number { + const old = m.get(1); + m.set(1, new Counter()); + return old === undefined ? 0 : old.num; +} +function aliased(m: Map): number { + const held = m.get(1); + const copy = held; + m.set(1, new Counter()); + return copy === undefined ? 0 : copy.num; +} +function fresh(): Map { + return new Map([[1, new Counter()]]); +} +function temporary(keys: number[]): number { + let total = 0; + for (const key of keys) { + const held = fresh().get(key); + if (held !== undefined) total += held.num; + } + return total; +} +class Holder { + m = new Map([[1, new Counter()]]); + key(): number { + this.m = new Map(); + return 1; + } + read(): number { + const held = this.m.get(this.key()); + return held === undefined ? 0 : held.num; + } +} +const numbers = new Map(); +count(numbers, [1, 2, 1]); +console.log(named(new Map([["a", new Counter()]]), "a"), replaced(numbers), aliased(numbers), temporary([1, 2]), new Holder().read()); +`, + ); + const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" }); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + const mod = deserializeModule(await readFile(outPath, "utf8")); + const ll = emitLlvmModule(mod); + const fn = (name: string): string => + new RegExp(`^define internal [^\\n]*@sc_bf_${name}(?:\\.i32)?\\([^]*?^}`, "m").exec(ll)![0]; + // The update writes the map only where the entry is absent, and a + // string key's lookup is inline: neither read retains the value. + for (const name of ["count", "named"]) { + expect(fn(name)).not.toContain("@sc_retain_Counter"); + expect(fn(name)).not.toContain("@sc_release_Counter"); + } + // A write between the read and its use keeps the read's own reference, + // also when the use goes through a local declared from the read, and so + // does a read from a map that is released after the declaration. + for (const name of ["replaced", "aliased", "temporary"]) + expect(fn(name)).toContain("@sc_retain_Counter"); + // A key whose evaluation replaces the receiver's field leaves the read + // on the old map, which the statement releases: the read keeps its own + // reference. + expect(fn("_x25_Holder_read")).toContain("@sc_retain_Counter"); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); diff --git a/packages/compiler/test/namespace-member-fences.test.ts b/packages/compiler/test/namespace-member-fences.test.ts new file mode 100644 index 000000000..c040f35c7 --- /dev/null +++ b/packages/compiler/test/namespace-member-fences.test.ts @@ -0,0 +1,67 @@ +import { spawnSync } from "node:child_process"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile } from "../src/index.js"; + +const sanitize = process.env["SCRIPTC_SAN"] === "1"; + +// Static member calls (`Math.abs(x)`) do not read the namespace objects, so +// a change to a function member's value fences (SC1090) instead of leaving +// those calls with the original. New properties and attribute-only changes +// behave as in Node (corpus 7188). +test("replacing or deleting a namespace member fences", async () => { + const dir = mkdtempSync(join(tmpdir(), "scriptc-namespace-fences-")); + try { + const entry = join(dir, "main.js"); + writeFileSync( + entry, + ` +const m = /** @type {any} */ (Math); +const j = /** @type {any} */ (JSON); +function attempt(label, run) { + try { console.log(label, run()); } catch (error) { console.log(label, error.code); } +} +attempt("alias", () => { m.abs = () => 0; return "replaced"; }); +attempt("direct", () => { Math.max = () => 0; return "replaced"; }); +attempt("define", () => Object.defineProperty(m, "min", { value: () => 0 }) === m); +attempt("same", () => Object.defineProperty(m, "min", { value: Math.min, enumerable: true }) === m); +attempt("reflect", () => Reflect.set(j, "parse", () => 0)); +attempt("assign", () => Object.assign(m, { floor: 1 }) === m); +attempt("delete", () => delete m.ceil); +attempt("added", () => { m.extra = 5; return m.extra; }); +console.log(Math.abs(-1), Math.max(1, 2), Math.min(1, 2), JSON.parse("[3]")[0], Math.floor(1.5), Math.ceil(1.5)); +console.log(Object.keys(Math).join(",")); +`, + ); + const result = await compile(entry, { + backend: "llvm", + sanitize, + outDir: dir, + outPath: join(dir, "program"), + }); + expect(result.ok, JSON.stringify(result.diagnostics)).toBe(true); + if (!result.ok) return; + const child = spawnSync(result.binaryPath, [], { encoding: "utf8" }); + expect(child.stderr).toBe(""); + expect(child.status).toBe(0); + expect(child.stdout).toBe( + [ + "alias SC1090", + "direct SC1090", + "define SC1090", + "same true", + "reflect SC1090", + "assign SC1090", + "delete SC1090", + "added 5", + "1 2 1 3 1 2", + "min,extra", + "", + ].join("\n"), + ); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +}); diff --git a/packages/compiler/test/native-codegen-integration.test.ts b/packages/compiler/test/native-codegen-integration.test.ts index cecebb49c..2ac6143b3 100644 --- a/packages/compiler/test/native-codegen-integration.test.ts +++ b/packages/compiler/test/native-codegen-integration.test.ts @@ -378,7 +378,7 @@ done: expect(await symbols(helperObject, ["-u"])).toEqual(await symbols(clangObject, ["-u"])); expect(await symbols(helperObject, ["-gU"])).toEqual(await symbols(clangObject, ["-gU"])); expect(await symbols(helperObject, ["-gU"])).toEqual(["0000000000000000 T _main"]); - expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v37"); + expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v47"); const clangExe = join(dir, "clang-program"); const linkDriver = join(dir, "clang-driver.c"); @@ -712,7 +712,7 @@ done: .split("\n"); expect(undefinedSymbols).toContain("_sf_scale"); expect(undefinedSymbols).toContain("_sf_callback_mix"); - expect(undefinedSymbols).toContain("_scr_runtime_abi_v37"); + expect(undefinedSymbols).toContain("_scr_runtime_abi_v47"); }); test("helper and clang object paths retain the same outbound FFI C ABI", async () => { diff --git a/packages/compiler/test/native-codegen-lowering.test.ts b/packages/compiler/test/native-codegen-lowering.test.ts index e9891e945..857393f21 100644 --- a/packages/compiler/test/native-codegen-lowering.test.ts +++ b/packages/compiler/test/native-codegen-lowering.test.ts @@ -51,14 +51,12 @@ test("decompression options lower for run-time validation", () => { expect(diags).toEqual([]); }); -test("Brotli keeps its SC2020 refusal", () => { +test("Brotli options lower for run-time validation", () => { const diags = diagnostics(` - import { brotliCompressSync } from "node:zlib"; - console.log(brotliCompressSync("payload").length); + import { brotliCompressSync, constants } from "node:zlib"; + console.log(brotliCompressSync("payload", { params: { [constants.BROTLI_PARAM_QUALITY]: 4 } }).length); `); - expect(diags).toHaveLength(1); - expect(diags[0]!.code).toBe("SC2020"); - expect(diags[0]!.message).toContain("brotliCompressSync"); + expect(diags).toEqual([]); }); test("union writes keep the refusal for incompatible field storage", () => { diff --git a/packages/compiler/test/native-json-value-end.test.ts b/packages/compiler/test/native-json-value-end.test.ts new file mode 100644 index 000000000..0818fbab6 --- /dev/null +++ b/packages/compiler/test/native-json-value-end.test.ts @@ -0,0 +1,151 @@ +import { execFileSync } from "node:child_process"; +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { jsonValueEnd } from "../src/frontend/ts7/semantic-json.js"; + +const sources = join(import.meta.dirname, "../native"); +const tempRoot = process.platform === "win32" ? tmpdir() : "/tmp"; + +/** jsonValueEnd's result, or -1 where it throws. */ +function expectedEnd(bytes: Uint8Array, start: number): number { + try { + return jsonValueEnd(bytes, start); + } catch { + return -1; + } +} + +/** A deterministic generator, so a failing case can be reproduced. */ +function random(seed: number): () => number { + let state = seed; + return () => { + state = (state * 1103515245 + 12345) % 2147483648; + return state / 2147483648; + }; +} + +const ALPHABET = ["{", "}", "[", "]", '"', "\\", ",", " ", "\n", "\t", "a", "1", "n", ":", "é"]; + +function cases(): { bytes: Uint8Array; start: number }[] { + const next = random(1); + const out: { bytes: Uint8Array; start: number }[] = []; + const texts = [ + "", + '""', + '"unclosed', + '"\\"', + '"\\\\"', + '"a\\"b"', + '"\\\\\\""', + "null", + "null,", + "123 ", + '{"id":1}', + '{"id":1,"name":"a}b"}', + '[{"id":1},null,{"id":2,"declarations":["1.2./a.ts","3.4./b \\"c\\".ts"]}]', + '{"id":1,"nested":{"list":[1,{"x":"]"}]}}, next', + "{", + "[[]", + "}", + "]", + ",", + " ", + ]; + for (const text of texts) { + const bytes = Buffer.from(text, "utf8"); + for (let start = 0; start <= bytes.length; start++) out.push({ bytes, start }); + } + for (let n = 0; n < 4000; n++) { + let text = ""; + const length = Math.floor(next() * 40); + for (let k = 0; k < length; k++) text += ALPHABET[Math.floor(next() * ALPHABET.length)]; + const bytes = Buffer.from(text, "utf8"); + out.push({ bytes, start: Math.floor(next() * (bytes.length + 1)) }); + } + const reply = JSON.stringify( + Array.from({ length: 200 }, (_, id) => + id % 7 === 0 + ? null + : { id, name: `name ${id} "q" \\ {[`, declarations: [`${id}.80./src/a ${id}.ts`] }, + ), + ); + const bytes = Buffer.from(reply, "utf8"); + for (let start = 0; start < bytes.length; start += 3) out.push({ bytes, start }); + return out; +} + +/** The native scanner's results for `all`, from a harness built in `dir`. */ +function nativeEnds( + dir: string, + all: { bytes: Uint8Array; start: number }[], + timeout?: number, +): number[] { + const harness = join(dir, "scan" + (process.platform === "win32" ? ".exe" : "")); + execFileSync("clang", [ + "-std=c11", + "-Wall", + "-Wextra", + "-Werror", + ...(process.env["SCRIPTC_SAN"] === "1" ? ["-fsanitize=address,undefined"] : []), + join(sources, "ts7-json.test.c"), + join(sources, "ts7-process.c"), + "-o", + harness, + ]); + const chunks: Buffer[] = []; + for (const { bytes, start } of all) { + const header = Buffer.alloc(8); + header.writeUInt32LE(bytes.length, 0); + header.writeUInt32LE(start, 4); + chunks.push(header, Buffer.from(bytes)); + } + const output = execFileSync(harness, { + input: Buffer.concat(chunks), + encoding: "utf8", + maxBuffer: 64 * 1024 * 1024, + ...(timeout === undefined ? {} : { timeout }), + }); + return output.trim().split(/\r?\n/).map(Number); +} + +test("the native scanner finds the value ends jsonValueEnd finds", () => { + const dir = mkdtempSync(join(tempRoot, "scriptc-ts7-json-")); + try { + const all = cases(); + const actual = nativeEnds(dir, all); + expect(actual.length).toBe(all.length); + for (let k = 0; k < all.length; k++) { + const { bytes, start } = all[k]!; + const expected = expectedEnd(bytes, start); + if (actual[k] !== expected) + expect({ text: Buffer.from(bytes).toString("utf8"), start, end: actual[k] }).toEqual({ + text: Buffer.from(bytes).toString("utf8"), + start, + end: expected, + }); + } + } finally { + rmSync(dir, { recursive: true, force: true }); + } +}); + +test("the native scanner reads strings with many escapes in one pass", () => { + const dir = mkdtempSync(join(tempRoot, "scriptc-ts7-json-")); + try { + // Strings of 2,000,000 escapes each: newlines, backslashes and quotes, + // alone and as a member of an object. + const strings = ["\\n", "\\\\", '\\"'].map((escape) => `"${escape.repeat(2_000_000)}"`); + const all = [ + ...strings.map((text) => ({ bytes: Buffer.from(text, "utf8"), start: 0 })), + ...strings.map((text) => ({ bytes: Buffer.from(`{"value":${text}},1`, "utf8"), start: 0 })), + ]; + // A scan that searched for the closing quote again after each escape + // would take minutes here. + const actual = nativeEnds(dir, all, 60_000); + expect(actual).toEqual(all.map(({ bytes, start }) => expectedEnd(bytes, start))); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +}); diff --git a/packages/compiler/test/native-partitions.test.ts b/packages/compiler/test/native-partitions.test.ts index ce1f14335..113cd95ad 100644 --- a/packages/compiler/test/native-partitions.test.ts +++ b/packages/compiler/test/native-partitions.test.ts @@ -29,8 +29,7 @@ afterEach(async () => { /** A program whose internal functions, constants and state must stay * reachable across partitions. `edit` changes one function's addend. */ -function program(edit = 0, extra = ""): string { - const count = 24; +function program(edit = 0, extra = "", count = 24): string { const lines = [ "; partitioned program", "%Pair = type { i64, i64 }", @@ -74,11 +73,12 @@ function program(edit = 0, extra = ""): string { return lines.join("\n") + "\n"; } -function expectedExit(edit = 0): number { - const addends = Array.from({ length: 24 }, (_, index) => (index === 5 ? index + edit : index)); +function expectedExit(edit = 0, count = 24): number { + const addends = Array.from({ length: count }, (_, index) => (index === 5 ? index + edit : index)); let total = 5; // strlen("total") - for (let start = 0; start < 24; start++) - for (let index = start; index < 24; index++) total += (start === 0 ? 4 : 1) * addends[index]!; + for (let start = 0; start < count; start++) + for (let index = start; index < count; index++) + total += (start === 0 ? 4 : 1) * addends[index]!; return total % 251; } @@ -162,6 +162,155 @@ describe.runIf(helper !== null)("LLVM program partitions", () => { }); } + test("functions under one group line share a partition", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-partitions-groups-")); + dirs.push(dir); + // Groups of four steps, each smaller than the mean partition; main ends + // the last group. Placement by group applies from 32 partitions. + const steps = 128; + const grouped = (edit: number, count = steps) => + program(edit, "", count) + .split("\n") + .flatMap((line) => { + const step = /^define internal void @step(\d+)\(/.exec(line); + if (step !== null && Number(step[1]) % 4 === 0) + return [`; scriptc-group ${Number(step[1]) / 4}`, line]; + return line.startsWith("define i32 @main(") ? ["; scriptc-group", line] : [line]; + }) + .join("\n"); + const cache = join(dir, "cache"); + const options = { outputs: 32, optLevel: "0", cache } as const; + const objects = await emit(dir, grouped(0), { ...options, name: "grouped" }); + expect(await exitCode(dir, objects, "grouped")).toBe(expectedExit(0, steps)); + const owner = new Map(); + for (const [index, object] of objects.entries()) { + const { stdout } = await execFileAsync("nm", [object]); + for (const line of stdout.split("\n")) { + const defined = /^[0-9a-fA-F]+ [A-Za-z] _?step(\d+)$/.exec(line.trim()); + if (defined !== null) owner.set(Number(defined[1]), index); + } + } + expect(owner.size).toBe(steps); + // step0's only caller is main, so it goes with main. + for (let group = 0; group < steps / 4; group++) { + const members = [0, 1, 2, 3].map((offset) => group * 4 + offset).filter((step) => step > 0); + expect(new Set(members.map((step) => owner.get(step))).size).toBe(1); + } + + // An edit to one function still changes one partition's source. + const sources = await cacheEntries(cache, ".o"); + const edited = await emit(dir, grouped(7), { ...options, name: "edited" }); + const after = await cacheEntries(cache, ".o"); + expect([...after].filter((entry) => !sources.has(entry))).toHaveLength(1); + expect(await exitCode(dir, edited, "edited")).toBe(expectedExit(7, steps)); + + // With fewer partitions, functions keep their placement by name, and a + // group line only stops a single-caller chain: step0 joins main without + // groups, and keeps its own placement in group 0. + const few = { outputs: 4, optLevel: "0" } as const; + const placed = async (objects: string[]) => { + const where = new Map(); + for (const [index, object] of objects.entries()) { + const { stdout } = await execFileAsync("nm", [object]); + for (const line of stdout.split("\n")) { + const defined = /^[0-9a-fA-F]+ [A-Za-z] _?(step\d+|main)$/.exec(line.trim()); + if (defined !== null) where.set(defined[1]!, index); + } + } + return where; + }; + const fewGrouped = await placed( + await emit(dir, grouped(0, 24), { ...few, name: "few-grouped" }), + ); + const fewPlain = await placed(await emit(dir, program(), { ...few, name: "few-plain" })); + expect(fewPlain.get("step0")).toBe(fewPlain.get("main")); + for (let step = 1; step < 24; step++) + expect(fewGrouped.get(`step${step}`)).toBe(fewPlain.get(`step${step}`)); + expect(fewGrouped.get("main")).toBe(fewPlain.get("main")); + }); + + test("internal definitions nothing reaches are left out of every partition", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-partitions-unreachable-")); + dirs.push(dir); + // An unreferenced internal function that uses live code and its own + // state, which only it references. + const unused = (name: string) => + [ + `@${name}State = internal global i64 7`, + `define internal void @${name}(i64 %value) {`, + ` %state = load i64, ptr @${name}State`, + " call void @bump(i64 %state)", + " ret void", + "}", + ].join("\n"); + const cache = join(dir, "cache"); + const options = { outputs: 4, optLevel: "0", cache } as const; + const objects = await emit(dir, program(0, unused("unusedOne")), { ...options, name: "first" }); + expect(await exitCode(dir, objects, "first")).toBe(expectedExit()); + for (const object of objects) { + const { stdout } = await execFileAsync("nm", [object]); + expect(stdout).not.toMatch(/unusedOne/); + } + // Adding another one changes no partition. + const before = await cacheEntries(cache, ".o"); + const more = await emit(dir, program(0, `${unused("unusedOne")}\n${unused("unusedTwo")}`), { + ...options, + name: "more", + }); + expect(await cacheEntries(cache, ".o")).toEqual(before); + expect(await exitCode(dir, more, "more")).toBe(expectedExit()); + }); + + test("a helper that one function uses goes to that function's partition", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-partitions-helpers-")); + dirs.push(dir); + // step7 calls @helper directly, and step9 calls @viaTable through an + // internal global only step9 reads. + const helpers = (withHelpers: boolean) => + program() + .split("\n") + .flatMap((line) => { + if (!withHelpers) return [line]; + if (line === "define internal void @step7(i64 %value) noinline {") + return [ + "define internal void @helper(i64 %value) noinline {", + " call void @bump(i64 %value)", + " ret void", + "}", + "@table = internal constant ptr @viaTable", + "define internal void @viaTable(i64 %value) noinline {", + " call void @bump(i64 %value)", + " ret void", + "}", + line, + " call void @helper(i64 0)", + ]; + if (line === "define internal void @step9(i64 %value) noinline {") + return [line, " %fn = load ptr, ptr @table", " call void %fn(i64 0)"]; + return [line]; + }) + .join("\n"); + const cache = join(dir, "cache"); + const options = { outputs: 4, optLevel: "0", cache } as const; + await emit(dir, helpers(false), { ...options, name: "before" }); + const before = await cacheEntries(cache, ".o"); + const objects = await emit(dir, helpers(true), { ...options, name: "after" }); + expect(await exitCode(dir, objects, "after")).toBe(expectedExit()); + const where = new Map(); + for (const [index, object] of objects.entries()) { + const { stdout } = await execFileAsync("nm", [object]); + for (const line of stdout.split("\n")) { + const defined = /^[0-9a-fA-F]+ [A-Za-z] _?(step7|step9|helper|viaTable)$/.exec(line.trim()); + if (defined !== null) where.set(defined[1]!, index); + } + } + expect(where.get("helper")).toBe(where.get("step7")); + expect(where.get("viaTable")).toBe(where.get("step9")); + // Only the partitions of step7 and step9 change. + const changed = [...(await cacheEntries(cache, ".o"))].filter((entry) => !before.has(entry)); + expect(changed).toHaveLength(new Set([where.get("step7"), where.get("step9")]).size); + }); + test("a table written one element per line keeps the text partitioner and its reuse", async () => { const dir = await mkdtemp(join(tmpdir(), "scriptc-partitions-table-")); dirs.push(dir); diff --git a/packages/compiler/test/native-profile.test.ts b/packages/compiler/test/native-profile.test.ts index 53d077017..9d04739d0 100644 --- a/packages/compiler/test/native-profile.test.ts +++ b/packages/compiler/test/native-profile.test.ts @@ -110,6 +110,65 @@ function expectedExit(argc: number, edit = 0): number { return Number(total % 251n); } +const INLINED = 6; + +/** A program whose helpers are too large for the inliner that runs before the + * profile annotation and each called twice, so they reach the annotation, + * and which the profile-guided inliner then inlines into their hot call + * sites in `main` and deletes. */ +function inlinedProgram(): string { + const lines = ["; profiled program, helpers inlined after annotation"]; + for (let index = 0; index < INLINED; index++) { + lines.push( + `define internal i64 @g${index}(i64 %n) {`, + "entry:", + " %x0 = add i64 %n, " + index, + ); + for (let step = 1; step <= 40; step++) + lines.push(` %x${step} = ${step % 2 === 0 ? "xor" : "mul"} i64 %x${step - 1}, ${step + 3}`); + lines.push(" ret i64 %x40", "}"); + } + lines.push( + "define i32 @main(i32 %argc, ptr %argv) {", + "entry:", + " %wide = sext i32 %argc to i64", + " %limit = mul i64 %wide, 1000", + " br label %loop", + "loop:", + " %i = phi i64 [ 0, %entry ], [ %next, %loop ]", + " %total = phi i64 [ 0, %entry ], [ %grown, %loop ]", + ...Array.from({ length: INLINED }, (_, index) => [ + ` %a${index} = call i64 @g${index}(i64 %i)`, + ` %b${index} = call i64 @g${index}(i64 %a${index})`, + ` %t${index} = add i64 ${index === 0 ? "%total" : `%t${index - 1}`}, %b${index}`, + ]).flat(), + ` %grown = add i64 %t${INLINED - 1}, 0`, + " %next = add i64 %i, 1", + " %again = icmp ult i64 %next, %limit", + " br i1 %again, label %loop, label %exit", + "exit:", + " %low = urem i64 %grown, 251", + " %code = trunc i64 %low to i32", + " ret i32 %code", + "}", + ); + return lines.join("\n") + "\n"; +} + +function inlinedExit(argc: number): number { + const mask = (1n << 64n) - 1n; + const g = (index: number, n: bigint) => { + let x = (n + BigInt(index)) & mask; + for (let step = 1; step <= 40; step++) + x = step % 2 === 0 ? x ^ BigInt(step + 3) : (x * BigInt(step + 3)) & mask; + return x; + }; + let total = 0n; + for (let i = 0n; i < BigInt(argc * 1000); i++) + for (let index = 0; index < INLINED; index++) total = (total + g(index, g(index, i))) & mask; + return Number(total % 251n); +} + type EmitOptions = { outputs: number; name: string; @@ -119,12 +178,17 @@ type EmitOptions = { }; async function emit(dir: string, source: string, options: EmitOptions): Promise { + return (await emitReporting(dir, source, options)).outputs; +} + +/** `emit`, also returning the helper's stdout (its profile match report). */ +async function emitReporting(dir: string, source: string, options: EmitOptions) { const input = join(dir, `${options.name}.ll`); await writeFile(input, source); const outputs = Array.from({ length: options.outputs }, (_, index) => join(dir, `${options.name}.${index}.o`), ); - await execFileAsync(helper!, [ + const { stdout } = await execFileAsync(helper!, [ "emit", "--input", input, @@ -145,7 +209,7 @@ async function emit(dir: string, source: string, options: EmitOptions): Promise< ...(options.use === undefined ? [] : ["--profile-use", options.use]), ...(options.cache === undefined ? [] : ["--cache-dir", options.cache]), ]); - return outputs; + return { outputs, stdout }; } async function link(dir: string, objects: string[], name: string, instrumented = false) { @@ -181,12 +245,18 @@ async function merge(output: string, inputs: string[]) { /** An instrumented build of `source` run with 1 and 3 arguments, its raw * profiles merged into one indexed profile. */ -async function collect(dir: string, source: string, outputs: number, name: string) { +async function collect( + dir: string, + source: string, + outputs: number, + name: string, + expected: (argc: number) => number = expectedExit, +) { const raw = join(dir, `${name}-raw`); const objects = await emit(dir, source, { outputs, name, generate: join(raw, "%p.profraw") }); const executable = await link(dir, objects, name, true); - expect(await run(executable, 1)).toBe(expectedExit(1)); - expect(await run(executable, 3)).toBe(expectedExit(3)); + expect(await run(executable, 1)).toBe(expected(1)); + expect(await run(executable, 3)).toBe(expected(3)); const profiles = (await readdir(raw)).map((file) => join(raw, file)); expect(profiles).toHaveLength(2); const profile = join(dir, `${name}.profdata`); @@ -286,6 +356,26 @@ describe.runIf(helper !== null)("LLVM profile-guided optimization", () => { expect(await run(await link(dir, objects, "edited"), 2)).toBe(expectedExit(2, 7)); }); + test("the match report counts annotated functions that inlining removes", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-profile-match-")); + dirs.push(dir); + const { profile } = await collect(dir, inlinedProgram(), 1, "train", inlinedExit); + const { outputs, stdout } = await emitReporting(dir, inlinedProgram(), { + outputs: 1, + name: "guided", + use: profile, + }); + expect(await run(await link(dir, outputs, "guided"), 2)).toBe(inlinedExit(2)); + // Every g and main received counts, though optimization then + // inlined the helpers into main and deleted them. + expect(JSON.parse(stdout.trim().split("\n").at(-1)!)).toEqual({ + ok: true, + profile: { annotated: INLINED + 1, mismatched: 0, missing: 0, unknown_partitions: 0 }, + }); + const symbols = await execFileAsync("nm", [outputs[0]!]); + expect(symbols.stdout).not.toMatch(/\bg\d\b/); + }); + test("the profile keys the partition cache", async () => { const dir = await mkdtemp(join(tmpdir(), "scriptc-profile-cache-")); dirs.push(dir); diff --git a/packages/compiler/test/node-fallback-modules.test.ts b/packages/compiler/test/node-fallback-modules.test.ts new file mode 100644 index 000000000..60a2e0bc9 --- /dev/null +++ b/packages/compiler/test/node-fallback-modules.test.ts @@ -0,0 +1,43 @@ +import { existsSync, readdirSync, readFileSync } from "node:fs"; +import { basename, dirname, join } from "node:path"; +import { expect, test } from "vitest"; +import { + fallbackDtsPath, + fallbackModulePath, + fallbackModulesNamedIn, +} from "../src/frontend/dts-paths.js"; + +const moduleDirectory = join(dirname(fallbackDtsPath()), "node-fallback"); +const declaredModules = (text: string): string[] => + [...text.matchAll(/^declare module "([^"]+)" \{/gm)].map((m) => m[1]!); + +test("every fallback module file declares the specifiers that map to it, and only those", () => { + const files = readdirSync(moduleDirectory).filter((name) => name.endsWith(".d.ts")); + expect(files.length).toBeGreaterThan(0); + for (const file of files) { + const text = readFileSync(join(moduleDirectory, file), "utf8"); + const declared = declaredModules(text); + expect(declared.length, file).toBeGreaterThan(0); + for (const specifier of declared) expect(basename(fallbackModulePath(specifier)!)).toBe(file); + // Every reference names another module file. + for (const [, reference] of text.matchAll(/^\/\/\/ $/gm)) + expect(existsSync(join(moduleDirectory, reference!)), `${file} -> ${reference}`).toBe(true); + } +}); + +test("the core fallback file declares no module that has a file of its own", () => { + for (const specifier of declaredModules(readFileSync(fallbackDtsPath(), "utf8"))) + expect(fallbackModulePath(specifier), specifier).toBeNull(); +}); + +test("a module named in a string literal selects its file, under either spelling", () => { + const files = fallbackModulesNamedIn( + `import { readFileSync } from "node:fs";\nconst os = require('os');\ntype T = import(\`node:stream/web\`);\nconst label = "not-a-module";`, + ).map((file) => basename(file)); + expect(files.sort()).toEqual(["fs.d.ts", "os.d.ts", "stream-web.d.ts"]); + expect(fallbackModulesNamedIn('console.log("hi")')).toEqual([]); + // Module names with digits. + expect( + fallbackModulesNamedIn(`require("http2"); import("node:v8");`).map((file) => basename(file)), + ).toEqual(["http2.d.ts", "v8.d.ts"]); +}); diff --git a/packages/compiler/test/node-pin.test.ts b/packages/compiler/test/node-pin.test.ts index 7f032c69e..f13c5558e 100644 --- a/packages/compiler/test/node-pin.test.ts +++ b/packages/compiler/test/node-pin.test.ts @@ -5,7 +5,7 @@ import { readFileSync } from "node:fs"; import { join } from "node:path"; import { expect, test } from "vitest"; -import { NODE24_FETCH_COMPAT_PROFILE } from "../src/compat/fetch-profile.js"; +import { node24FetchCompatProfile } from "../src/compat/fetch-profile.js"; import { NODE_COMPAT_VERSION } from "../src/frontend/lowering/builtins/process.js"; import { CRYPTO_CONSTANTS } from "../src/frontend/lowering/crypto-tables.js"; import { ZLIB_CONSTANTS } from "../src/frontend/lowering/zlib-constants.js"; @@ -23,7 +23,7 @@ test("the Node pins agree", () => { expect(compat.tag).toBe(`v${pinned}`); expect(read("Dockerfile.sandbox")).toMatch(new RegExp(`^ARG NODE_VERSION=${pinned}$`, "m")); expect(NODE_COMPAT_VERSION).toBe(pinned); - expect(NODE24_FETCH_COMPAT_PROFILE.target.node).toBe(pinned); + expect(node24FetchCompatProfile().target.node).toBe(pinned); expect(read("packages/runtime/src/scr_lib.c")).toContain( `#define SCR_NODE_COMPAT_VERSION "${pinned}"`, ); @@ -33,7 +33,7 @@ test("the Node pins agree", () => { }); test("the crypto.constants declaration matches the baked table", () => { - const dts = read("packages/compiler/ambient/scriptc-node-fallback.d.ts"); + const dts = read("packages/compiler/ambient/node-fallback/crypto.d.ts"); const start = dts.indexOf("export interface CryptoConstants {"); const body = dts.slice(start, dts.indexOf("\n }\n", start)); const declared = Object.fromEntries( @@ -46,7 +46,7 @@ test("the crypto.constants declaration matches the baked table", () => { }); test("the zlib.constants declaration matches the baked table", () => { - const dts = read("packages/compiler/ambient/scriptc-node-fallback.d.ts"); + const dts = read("packages/compiler/ambient/node-fallback/zlib.d.ts"); const start = dts.indexOf("export interface ZlibConstants {"); const body = dts.slice(start, dts.indexOf("\n }\n", start)); const declared = Object.fromEntries( diff --git a/packages/compiler/test/periodic-guard-emission.test.ts b/packages/compiler/test/periodic-guard-emission.test.ts new file mode 100644 index 000000000..dfb8c60ae --- /dev/null +++ b/packages/compiler/test/periodic-guard-emission.test.ts @@ -0,0 +1,53 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile } from "../src/index.js"; + +async function emitLlvm(source: string): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-periodic-guards-")); + try { + const entry = join(dir, "main.ts"); + const outPath = join(dir, "main.ll"); + await writeFile(entry, source); + const result = await compile(entry, { outDir: dir, outPath, outputKind: "llvm" }); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + return await readFile(outPath, "utf8"); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +test("a counted loop with a check every fourth iteration asks LLVM to unroll it by 4", async () => { + const llvm = await emitLlvm(` +function escape(c: number): number { + let z = 0; + let m = 0; + for (let j = 0; j < 50; j++) { + z = z * z + c; + m = z * z; + if ((j & 3) === 3 && !(m <= 4)) break; + } + return m; +} +console.log(escape(Number(process.argv[2] ?? "0.25"))); +`); + expect(llvm).toMatch(/br label %loop\.c\d+, !llvm\.loop !1000004/); + expect(llvm).toContain(`!1000004 = distinct !{!1000004, !{!"llvm.loop.unroll.count", i32 4}}`); +}); + +test("a loop without a periodic guard has no loop metadata", async () => { + const llvm = await emitLlvm(` +function sum(n: number): number { + let s = 0; + for (let j = 0; j < n; j++) { + s += j; + if (s > 1000) break; + } + return s; +} +console.log(sum(Number(process.argv[2] ?? "10"))); +`); + expect(llvm).not.toContain("!llvm.loop"); +}); diff --git a/packages/compiler/test/power-of-two-quotient-emission.test.ts b/packages/compiler/test/power-of-two-quotient-emission.test.ts new file mode 100644 index 000000000..4b748a2a1 --- /dev/null +++ b/packages/compiler/test/power-of-two-quotient-emission.test.ts @@ -0,0 +1,46 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile, deserializeModule } from "../src/index.js"; +import { emitLlvmModule } from "../src/backend/llvm/emitter.js"; + +test("a quotient by Math.pow(2, n) multiplies by the divisor's reciprocal power of two", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-pow2-quotient-")); + try { + const entry = join(dir, "main.ts"); + const outPath = join(dir, "main.ir.json"); + await writeFile( + entry, + ` +function scaled(x: number, n: number): number { return x / Math.pow(2, n); } +function plain(x: number, d: number): number { return x / d; } +function other(x: number, n: number): number { return x / Math.pow(3, n); } +console.log(scaled(3, 4), plain(3, 16), other(3, 4)); +`, + ); + const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" }); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + const mod = deserializeModule(await readFile(outPath, "utf8")); + const ll = emitLlvmModule(mod); + const fn = (name: string): string => + new RegExp(`^define internal [^\\n]*@sc_[bf]+_${name}(?:\\.i32)?\\([^]*?^}`, "m").exec( + ll, + )![0]; + // The reciprocal's bits are 0x7FE0000000000000 minus the divisor's; a + // divisor that is not a power of two from 2^-1022 to 2^1022 divides. + const quotient = fn("scaled"); + expect(quotient).toContain(`sub i64 ${0x7fe0000000000000n}, `); + expect(quotient).toMatch(/fmul double %\w+, %\w+/); + expect(quotient).toMatch(/icmp ult i64 %\w+, 2045/); + expect(quotient).toMatch(/select i1 %\w+, double %\w+, double %\w+/); + expect(quotient.match(/ = fdiv double /g)).toHaveLength(1); + for (const name of ["plain", "other"]) { + expect(fn(name)).not.toContain(`${0x7fe0000000000000n}`); + expect(fn(name)).toMatch(/fdiv double/); + } + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); diff --git a/packages/compiler/test/regex-replace-lifetimes.test.ts b/packages/compiler/test/regex-replace-lifetimes.test.ts new file mode 100644 index 000000000..0c60a9e5e --- /dev/null +++ b/packages/compiler/test/regex-replace-lifetimes.test.ts @@ -0,0 +1,57 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile } from "../src/index.js"; + +async function emitLlvm(source: string): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-regex-replace-")); + try { + const entry = join(dir, "main.ts"); + const outPath = join(dir, "main.ll"); + await writeFile(entry, source); + const result = await compile(entry, { outDir: dir, outPath, outputKind: "llvm" }); + expect(result.ok, JSON.stringify(result.diagnostics)).toBe(true); + return await readFile(outPath, "utf8"); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +function functionBody(llvm: string, name: string): string[] { + const start = llvm.split("\n").findIndex((l) => l.startsWith("define") && l.includes(name)); + expect(start).toBeGreaterThanOrEqual(0); + const lines = llvm.split("\n").slice(start); + return lines.slice(0, lines.indexOf("}")); +} + +test("a chain of substitutions releases each intermediate result when the next one returns", async () => { + const llvm = await emitLlvm(` +function chain(s: string): number { + return s + .replace(/a[bc]/g, "<1>") + .replace(/b[cd]/g, "<2>") + .replace(/c[de]/g, "<3>") + .replaceAll(/<[^>]*>/g, "|").length; +} +console.log(chain(process.argv[2] ?? "abcabc")); +`); + const body = functionBody(llvm, "chain"); + const calls = body + .map((line, index) => ({ line, index })) + .filter(({ line }) => /call ptr @scr_regex_replace(_all|_with)?\(/.test(line)); + expect(calls).toHaveLength(4); + const results = calls.map(({ line }) => /^\s*(%[\w.]+) = call/.exec(line)![1]!); + // Each intermediate result is released after the call that consumes it + // and before the next substitution starts. + for (let k = 0; k < 3; k++) { + const release = body.findIndex( + (line, index) => + index > calls[k + 1]!.index && + line.includes("release") && + line.includes(`(ptr ${results[k]})`), + ); + expect(release, `release of ${results[k]}`).toBeGreaterThan(calls[k + 1]!.index); + if (k + 2 < calls.length) expect(release).toBeLessThan(calls[k + 2]!.index); + } +}); diff --git a/packages/compiler/test/regex-replacers.test.ts b/packages/compiler/test/regex-replacers.test.ts new file mode 100644 index 000000000..746279eb4 --- /dev/null +++ b/packages/compiler/test/regex-replacers.test.ts @@ -0,0 +1,42 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile } from "../src/index.js"; + +async function emitLlvm(source: string): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-regex-replacers-")); + try { + const entry = join(dir, "main.ts"); + const outPath = join(dir, "main.ll"); + await writeFile(entry, source); + const result = await compile(entry, { outDir: dir, outPath, outputKind: "llvm" }); + expect(result.ok, JSON.stringify(result.diagnostics)).toBe(true); + return await readFile(outPath, "utf8"); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +test("global substitutions of regex literals with literal replacements run a replacer", async () => { + const llvm = await emitLlvm(` +const s = process.argv[2] ?? "ac"; +const rep = process.argv[3] ?? "-"; +console.log( + s.replace(/<[^>]*>/g, "|"), + s.replaceAll(/a[bc]/g, "<2>"), + s.replace(/<[^>]*>/g, "|"), + s.replace(/<[^>]*>/g, "$&$&"), + s.replace(/<[^>]*>/, "|"), + s.replace(/<[^>]*>/gy, "|"), + s.replace(/<[^>]*>/g, rep), +); +`); + const replacers = llvm.match(/^define internal i32 @sc_rr_\d+\(/gm) ?? []; + // One replacer per (pattern, flags, replacement): the repeated site shares it. + expect(replacers).toHaveLength(2); + expect(llvm.match(/call ptr @scr_regex_replace_with\(/g)).toHaveLength(3); + // $ patterns, non-global and sticky patterns, and computed replacements + // keep the runtime's loop. + expect(llvm.match(/call ptr @scr_regex_replace\(/g)!.length).toBeGreaterThanOrEqual(4); +}); diff --git a/packages/compiler/test/replacer-params.test.ts b/packages/compiler/test/replacer-params.test.ts new file mode 100644 index 000000000..a5d22bb82 --- /dev/null +++ b/packages/compiler/test/replacer-params.test.ts @@ -0,0 +1,89 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, describe, expect, test } from "vitest"; +import { compile } from "../src/index.js"; +import { regexLiteralCaptures } from "../src/backend/llvm/regex-matchers.js"; +import { regexCaptureParticipation } from "../src/frontend/lowering/containers/replacer-params.js"; + +// The frontend binds a replace callback parameter as a string only for a +// capture every match sets; the backend then calls the callback directly +// only when its own parse agrees. +describe("replace callback captures", () => { + test("never claims a capture the backend's parse can leave unset", () => { + const patterns = [ + "(\\w+)=(\\d)+", + "(?a)(b)?|(c)", + "x(?:(a)|(b))y(c)*(d){0,2}(e){1,3}", + "((a)(b)?)+", + "(a(b|c))d", + "(?:a|(b))(c)", + ]; + for (const pattern of patterns) { + const frontend = regexCaptureParticipation(pattern, ""); + const backend = regexLiteralCaptures(pattern, ""); + expect(frontend?.count).toBe(backend?.count); + frontend!.always.forEach((always, index) => { + if (always) expect(backend!.always[index]).toBe(true); + }); + } + }); +}); + +const directories: string[] = []; +afterEach(async () => { + await Promise.all(directories.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))); +}); + +async function emit(source: string): Promise { + const outDir = await mkdtemp(join(tmpdir(), "scriptc-replacer-")); + directories.push(outDir); + const entry = join(outDir, "main.ts"); + await writeFile(entry, source); + const outPath = join(outDir, "program.ll"); + const result = await compile(entry, { entry, outDir, outPath, outputKind: "llvm" }); + expect(result.ok, JSON.stringify(result)).toBe(true); + return readFile(outPath, "utf8"); +} + +/** The (arity, kinds) arguments of each direct replacer call, and the + * number of calls through the boxed argument array. */ +function replacerCalls(llvm: string): { direct: string[]; boxed: number } { + const lines = llvm.split("\n").filter((line) => line.includes("call ")); + return { + direct: lines + .filter((line) => line.includes("@scr_regex_replace_fn(")) + .map((line) => /i32 (-?\d+), i32 (-?\d+), i1/.exec(line)!.slice(1, 3).join(":")), + boxed: lines.filter((line) => line.includes("@scr_regex_replace_callback(")).length, + }; +} + +describe("replace callback calls", () => { + test("unannotated parameters take the kinds the replace passes", async () => { + const llvm = await emit(` +const vars: Record = { a: "1" }; +const text = String(Date.now() > 0 ? "{a} {b}" : ""); +console.log(text.replace(/\\{(\\w+)\\}/g, (_m, key) => vars[key] ?? "")); +console.log(text.replace(/\\{(\\w)/g, (_m, c) => c.toUpperCase())); +console.log(text.replace(/(a)|(b)/g, (m, a, b) => \`\${m}\${a}\${b}\`)); +console.log(text.replace(/(a)(b)?/g, (m: string, a: string, b: string | undefined, at: number) => m + a + (b ?? "") + at)); +const half = (n: number): number => n / 2; +console.log(text.replace(/b/g, (m, at) => String(half(at)))); +console.log(text.replace(/b/g, (m, at) => String(parseFloat(at) + parseInt(at)))); +console.log(text.replace(/(\\d)/g, (m, d) => String(parseInt("11", d)))); +console.log(text.replace(/(a)(b)(c)/g, (m, a, b, c, at, all, extra) => m + a + b + c + at + all + extra)); +`); + expect(replacerCalls(llvm)).toEqual({ + // Typed parameters and result (0); a checked-dynamic result (bit 31); + // two captures a match can leave unset (bits 1 and 2); + // \`string | undefined\` at capture 2 (bit 8 + 2); an offset passed to a + // number parameter keeps its checked-dynamic box (bit 1). + // parseFloat and parseInt take text, and parseInt's radix is a + // number: the offset passed as text and the capture passed as the + // radix stay checked-dynamic (bit 1), so both calls compile. + direct: ["2:0", "2:-2147483648", "3:6", "4:1024", "2:2", "2:2", "2:2"], + // Seven parameters exceed the direct calls' limit. + boxed: 1, + }); + }); +}); diff --git a/packages/compiler/test/runtime-optional-precision.test.ts b/packages/compiler/test/runtime-optional-precision.test.ts index 7840b0c62..e8b794b44 100644 --- a/packages/compiler/test/runtime-optional-precision.test.ts +++ b/packages/compiler/test/runtime-optional-precision.test.ts @@ -102,6 +102,82 @@ console.log(copy.map((item) => weigh(item)).join(",")); expect(signature(spread, "weigh")).toBe("weigh(Item|undefined): f64"); }); +test("holes stored through a dynamic view widen callbacks over the arrays it views", async () => { + const viewed = await lower(`${ITEM} +const items: Item[] = [new Item(1)]; +const view: unknown[] = items; +view[3] = new Item(2); +console.log(items.findIndex((item) => weigh(item) > 1)); +`); + expect(signature(viewed, "weigh")).toBe("weigh(Item|undefined): f64"); + // A dynamic store cannot reach an element type no dynamic value views. + const other = await lower(`${ITEM} +const items: Item[] = [new Item(1)]; +const values: unknown[] = [1]; +values[3] = 2; +console.log(items.findIndex((item) => weigh(item) > 1)); +`); + expect(signature(other, "weigh")).toBe("weigh(Item): f64"); + // A view without stores that leave holes keeps the parameters. + const read = await lower(`${ITEM} +const items: Item[] = [new Item(1)]; +const view: unknown[] = items; +console.log(view.length, items.findIndex((item) => weigh(item) > 1)); +`); + expect(signature(read, "weigh")).toBe("weigh(Item): f64"); + // Standard library functions that take arrays as `any` only read them. + const readers = await lower(`${ITEM} +const items: Item[] = [new Item(1)]; +console.log(items.length, JSON.stringify(items)); +const values: unknown[] = [1]; +values[3] = 2; +console.log(items.findIndex((item) => weigh(item) > 1)); +`); + expect(signature(readers, "weigh")).toBe("weigh(Item): f64"); + // A store through a Record type is not a store through a view. + const record = await lower(`${ITEM} +const items: Item[] = [new Item(1)]; +const view: unknown = items; +const bag: Record = {}; +bag["3"] = view; +console.log(items.findIndex((item) => weigh(item) > 1)); +`); + expect(signature(record, "weigh")).toBe("weigh(Item): f64"); + // An unknown[] that only holds the array its declaration creates is never + // a view, so stores past its end leave the viewed arrays alone. + const fresh = await lower(`${ITEM} +class Slots { + readonly states: unknown[] = []; +} +const slots = new Slots(); +const items: Item[] = [new Item(1)]; +const view: unknown = items; +const states = slots.states; +states[3] = view; +console.log(items.findIndex((item) => weigh(item) > 1)); +`); + expect(signature(fresh, "weigh")).toBe("weigh(Item): f64"); + // A mutable property every assignment of which is a fresh array is fresh + // storage too. + const reassigned = await lower(`${ITEM} +class Facts { + private facts: unknown[] = []; + begin(): void { + this.facts = []; + } + add(fact: unknown): void { + this.facts[3] = fact; + } +} +const items: Item[] = [new Item(1)]; +const facts = new Facts(); +facts.begin(); +facts.add(items); +console.log(items.findIndex((item) => weigh(item) > 1)); +`); + expect(signature(reassigned, "weigh")).toBe("weigh(Item): f64"); +}); + test("holes that only their owning slot can observe stay out of callbacks", async () => { const module = await lower(`${ITEM} class Cache { @@ -158,7 +234,9 @@ test("an unchanged captured parameter is borrowed and boxed only where a closure const module = await lower(`${ITEM} function heavier(item: Item, limits: number[]): boolean { if (limits.length === 0) return false; - return limits.some((limit) => weigh(item) > limit); + // A closure value: a closure literal at the call would run as a loop. + const over = (limit: number): boolean => weigh(item) > limit; + return limits.some(over); } function replaced(item: Item, other: Item): number { const read = () => item.weight; diff --git a/packages/compiler/test/shape-inference.test.ts b/packages/compiler/test/shape-inference.test.ts new file mode 100644 index 000000000..911cef79a --- /dev/null +++ b/packages/compiler/test/shape-inference.test.ts @@ -0,0 +1,302 @@ +import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeEach, expect, test, vi } from "vitest"; +import { compile } from "../src/index.js"; + +// A package shaped like a validation library: schema classes with a virtual +// `_parse`, per-field context and result literals, a class holding the +// context, and an output object built with computed keys. +const packageSource = ` +export class Status { + constructor() { this.value = "valid"; } + dirty() { if (this.value === "valid") this.value = "dirty"; } +} +class LazyPath { + constructor(parent, data, key) { this.parent = parent; this.data = data; this.key = key; } + get path() { return [this.key]; } +} +export class Base { + constructor(def) { this._def = def; } + ctxOf(input) { return { common: input.parent.common, data: input.data, parent: input.parent }; } + parse(data) { + const root = { common: { issues: [] }, data, parent: null }; + const result = this._parse({ data, parent: root }); + return result.value; + } +} +export class Str extends Base { + _parse(input) { + const status = new Status(); + if (typeof input.data !== "string") status.dirty(); + return { status: status.value, value: input.data }; + } +} +export class Obj extends Base { + _parse(input) { + const ctx = this.ctxOf(input); + const pairs = []; + for (const key of Object.keys(this._def.shape)) { + const field = this._def.shape[key]; + pairs.push({ key, value: field._parse(new LazyPath(ctx, ctx.data[key], key)) }); + } + const out = {}; + for (const pair of pairs) out[pair.key] = pair.value.value; + return { status: "valid", value: out }; + } +} +export const str = () => new Str({ checks: [] }); +export const obj = (shape) => new Obj({ shape }); +export function leak(value) { globalThis.leaked = value; return value; } +export function reflect() { const o = { a: 1 }; delete o.a; return o; } +class Box {} +function tag(strings, ...values) { leak(values[0]); return strings.raw.length; } +export function arrayProp() { const list = []; list.meta = { listed: 1 }; return leak(list.meta); } +export function computedField(k) { + const box = new Box(); + box[k] = { boxed: 1 }; + Box[k] = { onClass: 1 }; + const fn = function () {}; + fn[k] = { onFunction: 1 }; + return [leak(box.held), leak(Box.held), leak(fn.held)]; +} +export function revive(text) { + const seen = { revived: 1 }; + return JSON.parse(text, function (k, v) { globalThis.revived = seen; return v; }); +} +class Doc { + constructor() { this.body = { doc: 1 }; } + toJSON() { return leak(this.body); } +} +export function stringified() { + const own = { owned: 1 }; + return [ + JSON.stringify({ inner: { replaced: 1 } }, (k, x) => leak(x)), + JSON.stringify(new Doc()), + JSON.stringify({ toJSON() { return leak(own); } }), + ]; +} +export function proxied() { return new Proxy({ proxied: 1 }, {}); } +export function reflectDelete() { const o = { gone: 1 }; Reflect.deleteProperty(o, "gone"); return o; } +export function tagged() { return tag\`a\${{ part: 1 }}b\`; } +export function arrayComputed(k) { const l = []; l[k] = { arrayComputed: 1 }; return leak(l.meta); } +export function shadowed(k) { + const fn = function () {}; + fn[k] = { onPrototype: 1 }; + const box = new Box(); + box[k] = { onConstructor: 1 }; + return [leak(fn.prototype), leak(box.constructor)]; +} +export function jsonCallbacks(k) { + const inArray = { inArray: 1 }; + const list = []; + list.toJSON = function () { globalThis.inArray = inArray; return 1; }; + const inFunction = { inFunction: 1 }; + const fn = function () {}; + fn.toJSON = function () { globalThis.inFunction = inFunction; return 1; }; + const underKey = { underKey: 1 }; + const o = {}; + o[k] = function () { globalThis.underKey = underKey; return 1; }; + return JSON.stringify([list, { fn }, o]); +} +export function revivedResult(text) { const r = { fromReviver: 1 }; return leak(JSON.parse(text, function () { return r; })); } +export function constructedProxy() { return Reflect.construct(Proxy, [{ constructedProxy: 1 }, {}]); } +export function globalProxy() { return new globalThis.Proxy({ globalProxy: 1 }, {}); } +`; + +const packageTypes = ` +export declare class Base { parse(data: unknown): unknown; } +export declare class Str extends Base {} +export declare class Obj extends Base {} +export declare const str: () => Str; +export declare const obj: (shape: Record) => Obj; +export declare function leak(value: T): T; +export declare function reflect(): object; +export declare function arrayProp(): unknown; +export declare function computedField(k: string): unknown; +export declare function revive(text: string): unknown; +export declare function stringified(): unknown; +export declare function proxied(): unknown; +export declare function reflectDelete(): unknown; +export declare function tagged(): unknown; +export declare function arrayComputed(k: string): unknown; +export declare function shadowed(k: string): unknown; +export declare function jsonCallbacks(k: string): unknown; +export declare function revivedResult(text: string): unknown; +export declare function constructedProxy(): unknown; +export declare function globalProxy(): unknown; +`; + +const mainSource = ` +import { + arrayComputed, + arrayProp, + computedField, + constructedProxy, + globalProxy, + jsonCallbacks, + leak, + obj, + proxied, + reflect, + reflectDelete, + revive, + revivedResult, + shadowed, + str, + stringified, + tagged, +} from "shapes-fixture"; +const schema = obj({ a: str(), b: str() }); +const input: unknown = JSON.parse('{"a":"x","b":"y"}'); +console.log(JSON.stringify(schema.parse(input))); +console.log(JSON.stringify(leak({ kept: 1 })), JSON.stringify(reflect())); +const more = [ + arrayProp(), + computedField("held"), + revive("{}"), + stringified(), + proxied(), + reflectDelete(), + tagged(), + arrayComputed("meta"), + shadowed("prototype"), + jsonCallbacks("toJSON"), + revivedResult("{}"), + constructedProxy(), + globalProxy(), +]; +console.log(more.length); +`; + +let dir: string; +let saved: string | undefined; +beforeEach(async () => { + dir = await mkdtemp(join(tmpdir(), "scriptc-shapes-")); + const pkg = join(dir, "node_modules", "shapes-fixture"); + await mkdir(pkg, { recursive: true }); + await writeFile( + join(pkg, "package.json"), + JSON.stringify({ + name: "shapes-fixture", + version: "1.0.0", + type: "module", + main: "index.js", + types: "index.d.ts", + }), + ); + await writeFile(join(pkg, "index.js"), packageSource); + await writeFile(join(pkg, "index.d.ts"), packageTypes); + await writeFile( + join(dir, "package.json"), + JSON.stringify({ type: "module", dependencies: { "shapes-fixture": "1.0.0" } }), + ); + await writeFile(join(dir, "main.ts"), mainSource); + saved = process.env["SCRIPTC_DEBUG_SHAPES"]; +}); +afterEach(async () => { + if (saved === undefined) delete process.env["SCRIPTC_DEBUG_SHAPES"]; + else process.env["SCRIPTC_DEBUG_SHAPES"] = saved; + vi.restoreAllMocks(); + await rm(dir, { recursive: true, force: true }); +}); + +async function report(): Promise { + process.env["SCRIPTC_DEBUG_SHAPES"] = "verbose"; + const lines: string[] = []; + vi.spyOn(process.stderr, "write").mockImplementation((chunk: string | Uint8Array) => { + const text = typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8"); + for (const l of text.split("\n")) if (l.startsWith("[shapes] ")) lines.push(l.slice(9)); + return true; + }); + const entry = join(dir, "main.ts"); + await compile(entry, { + outDir: dir, + outPath: join(dir, "main.ir.json"), + outputKind: "ir", + npmStatic: ["shapes-fixture"], + }); + return lines; +} + +function site(lines: string[], label: RegExp): string { + const line = lines.find((l) => l.startsWith("site ") && label.test(l)); + if (line === undefined) throw new Error(`no site ${label}:\n${lines.join("\n")}`); + return line; +} + +test("S1 report: per-field temporaries are provable records, dynamic objects stay G", async () => { + const lines = await report(); + // result literals of both _parse implementations, the pair literal, the + // context and the root context never escape: native records would be exact + // the leaf schema's result: in a function called from a loop (hot) + expect(site(lines, /^site \{status,value\}@shapes-fixture\/index\.js:23 \[hot\]/)).toMatch( + /: P-local/, + ); + expect(site(lines, /^site \{status,value\}@shapes-fixture\/index\.js:36/)).toMatch(/: P-local/); + expect(site(lines, /^site \{key,value\}@/)).toMatch(/: P-local/); + expect(site(lines, /^site \{common,data,parent\}@shapes-fixture\/index\.js:12/)).toMatch( + /: P-local/, + ); + expect(site(lines, /^site \[\]@shapes-fixture\/index\.js:29/)).toMatch(/: P-local/); + // the output object is built with computed keys + expect(site(lines, /^site \{\}@shapes-fixture\/index\.js:34/)).toMatch(/: G — computed write/); + // a deleted key and a value stored into the global object + expect(site(lines, /^site \{a\}@/)).toMatch(/: G — delete at/); + expect(site(lines, /^site \{kept\}@main\.ts/)).toMatch(/: P-escaping — escapes: stored into ⊤/); + // receiver-typed dispatch: the field's validators are the schema classes + expect(lines.find((l) => l.startsWith("fn Str._parse@"))).toMatch(/p0=LazyPath/); + expect(lines.find((l) => l.startsWith("class LazyPath"))).toMatch( + /parent: \{common,data,parent\}@shapes-fixture\/index\.js:12;/, + ); + expect(lines.find((l) => l.startsWith("class Str"))).toMatch(/_def: \{checks\}@/); +}); + +test("S1 report follows values through array properties, unknown keys, callbacks and reflection", async () => { + const lines = await report(); + // a named property of an array, read back and leaked + expect(site(lines, /^site \{listed\}@/)).toMatch(/: P-escaping/); + // computed writes to an instance, a class and a function, read by name + expect(site(lines, /^site \{boxed\}@/)).toMatch(/: P-escaping/); + expect(site(lines, /^site \{onClass\}@/)).toMatch(/: P-escaping/); + expect(site(lines, /^site \{onFunction\}@/)).toMatch(/: P-escaping/); + // a JSON.parse reviver, a JSON.stringify replacer, and toJSON methods + expect(site(lines, /^site \{revived\}@/)).toMatch(/: P-escaping — escapes: stored into ⊤/); + expect(site(lines, /^site \{replaced\}@/)).toMatch(/: P-escaping/); + expect(site(lines, /^site \{doc\}@/)).toMatch(/: P-escaping/); + expect(site(lines, /^site \{owned\}@/)).toMatch(/: P-escaping/); + // a Proxy target and Reflect.deleteProperty + expect(site(lines, /^site \{proxied\}@/)).toMatch(/: G — proxy at/); + expect(site(lines, /^site \{gone\}@/)).toMatch(/: G — delete at/); + // a tagged template's substitution, passed to its tag + expect(site(lines, /^site \{part\}@/)).toMatch(/: P-escaping/); + // a named load of an array property written under an unknown key + expect(site(lines, /^site \{arrayComputed\}@/)).toMatch(/: P-escaping/); + // own fields written under unknown keys shadow a function's prototype and an instance's constructor + expect(site(lines, /^site \{onPrototype\}@/)).toMatch(/: P-escaping/); + expect(site(lines, /^site \{onConstructor\}@/)).toMatch(/: P-escaping/); + // toJSON on an array, on a function, and stored under an unknown key + expect(site(lines, /^site \{inArray\}@/)).toMatch(/: P-escaping/); + expect(site(lines, /^site \{inFunction\}@/)).toMatch(/: P-escaping/); + expect(site(lines, /^site \{underKey\}@/)).toMatch(/: P-escaping/); + // what a JSON.parse reviver returns is in the result + expect(site(lines, /^site \{fromReviver\}@/)).toMatch(/: P-escaping/); + // a proxy constructed through Reflect.construct and globalThis + expect(site(lines, /^site \{constructedProxy\}@/)).toMatch(/: G — proxy at/); + expect(site(lines, /^site \{globalProxy\}@/)).toMatch(/: G — proxy at/); + // the existing verdicts hold + expect(site(lines, /^site \{kept\}@main\.ts/)).toMatch(/: P-escaping — escapes: stored into ⊤/); +}); + +test("S1 report is off by default and changes nothing", async () => { + delete process.env["SCRIPTC_DEBUG_SHAPES"]; + const writes = vi.spyOn(process.stderr, "write"); + const entry = join(dir, "main.ts"); + await compile(entry, { + outDir: dir, + outPath: join(dir, "main.ir.json"), + outputKind: "ir", + npmStatic: ["shapes-fixture"], + }); + expect(writes.mock.calls.some(([c]) => String(c).includes("[shapes]"))).toBe(false); +}); diff --git a/packages/compiler/test/source-output.test.ts b/packages/compiler/test/source-output.test.ts index 8b8e3883c..ce411f102 100644 --- a/packages/compiler/test/source-output.test.ts +++ b/packages/compiler/test/source-output.test.ts @@ -3,6 +3,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, expect, test } from "vitest"; import { compile, deserializeModule, validateModule } from "../src/index.js"; +import { setLibDiagnosticsStore } from "../src/frontend/ts7/lib-diagnostics.js"; const dirs: string[] = []; afterEach(async () => { @@ -188,6 +189,38 @@ test("source outputs ignore invalid external compiler selection and create no ex } }); +test("source outputs neither read nor create declaration diagnostics a previous build stored", async () => { + const { dir, entry, outDir } = await fixture(); + const oldCache = process.env["SCRIPTC_CACHE_DIR"]; + const cache = join(dir, "must-not-exist"); + process.env["SCRIPTC_CACHE_DIR"] = cache; + // A store an earlier build of this process left configured. + const reads: string[] = []; + setLibDiagnosticsStore( + { read: (key) => (reads.push(key), null), write: () => {} }, + "earlier-checker", + ); + try { + for (const [kind, name] of [ + ["ir", "main.ir.json"], + ["llvm", "main.ll"], + ] as const) { + const result = await compile(entry, { + outDir, + outPath: join(outDir, name), + outputKind: kind, + }); + expect(result.ok).toBe(true); + } + expect(reads).toEqual([]); + await expect(readdir(cache)).rejects.toMatchObject({ code: "ENOENT" }); + } finally { + setLibDiagnosticsStore(null); + if (oldCache === undefined) delete process.env["SCRIPTC_CACHE_DIR"]; + else process.env["SCRIPTC_CACHE_DIR"] = oldCache; + } +}); + test.each(["wasm64-wasi", "totally-invalid"])( "source output rejects unsupported target %s without writing an artifact", async (target) => { diff --git a/packages/compiler/test/speculative-loop-emission.test.ts b/packages/compiler/test/speculative-loop-emission.test.ts index a81348102..dcec6d8af 100644 --- a/packages/compiler/test/speculative-loop-emission.test.ts +++ b/packages/compiler/test/speculative-loop-emission.test.ts @@ -47,9 +47,8 @@ test("a failed check finishes its iteration outside the ordinary copy", async () const llvm = await emitLlvm(source); const fn = /^define internal double @sc_bf_scaled\([^]*?^}/m.exec(llvm)?.[0] ?? ""; const order = labels(fn); - // The loop may have several speculative sites (one in each copy of a - // dense-array loop). Each site emits its ordinary copy, its third copy - // and its fast copy in that order. + // Each speculative site emits its ordinary copy, its third copy and its + // fast copy in that order. const slows = order.flatMap((l, i) => (l.startsWith("spec.slow") ? [i] : [])); expect(slows.length).toBeGreaterThan(0); let resumes = 0; @@ -91,3 +90,140 @@ test("a loop site stops entering its fast copy after repeated bails", async () = expect(fn).toMatch(new RegExp(`call i8 @llvm\\.uadd\\.sat\\.i8\\(i8 %\\S+, i8 1\\)`)); expect(fn).toContain(`store atomic i8 0, ptr ${site} unordered, align 1`); }); + +test("the slow copy of a dense-array loop has no speculative copy", async () => { + const llvm = await emitLlvm(source); + const fn = /^define internal double @sc_bf_scaled\([^]*?^}/m.exec(llvm)?.[0] ?? ""; + const order = labels(fn); + const slow = order.findIndex((l) => l.startsWith("dense.loop.slow")); + const fast = order.findIndex((l) => l.startsWith("dense.loop.fast")); + expect(slow).toBeGreaterThanOrEqual(0); + expect(fast).toBeGreaterThan(slow); + // The slow copy runs when the guard fails; only the fast copy speculates. + expect(order.slice(slow, fast).filter((l) => l.startsWith("spec."))).toEqual([]); + expect(order.slice(fast).some((l) => l.startsWith("spec.fast"))).toBe(true); + expect(llvm.match(/^@sc_spec_bails_\d+ = /gm)).toHaveLength(1); +}); + +test("the slow copy of a typed-array version keeps its proven integer loop", async () => { + // The program can start a worker, so typed-array loops are versioned on + // thread ownership. + const llvm = await emitLlvm(` +import { Worker, isMainThread } from "node:worker_threads"; +function count(bytes: Uint8Array): number { + let lines = 0; + for (let i = 0; i < bytes.length; i++) if (bytes[i] === 10) lines++; + return lines; +} +if (isMainThread) { + if (process.argv[2] === "spawn") new Worker(new URL(import.meta.url)); + console.log(count(new Uint8Array([10, 1, 10]))); +} +`); + const fn = /^define internal double @sc_bf_\w*count\([^]*?^}/m.exec(llvm)?.[0] ?? ""; + const order = labels(fn); + const slow = order.findIndex((l) => l.startsWith("bytes.version.slow")); + const fast = order.findIndex((l) => l.startsWith("bytes.version.fast")); + expect(slow).toBeGreaterThanOrEqual(0); + expect(fast).toBeGreaterThan(slow); + expect(order.slice(slow, fast).some((l) => l.startsWith("integer.leave"))).toBe(true); + expect(order.slice(fast).some((l) => l.startsWith("integer.leave"))).toBe(true); +}); + +test("the shared-storage copy of a typed-array version keeps its speculative copy", async () => { + // In a program that can start a worker, the guard requires thread-owned + // storage, so every view of a SharedArrayBuffer runs the checked copy. + const llvm = await emitLlvm(` +import { Worker, isMainThread } from "node:worker_threads"; +function trim(value: number, bits: number): number { + return value - Math.floor(value / Math.pow(2, bits)) * Math.pow(2, bits); +} +function key(bytes: Uint8Array, bits: number): number { + let k = 0; + for (let i = 0; i < bytes.length; i++) k = trim(k, bits) * 4 + bytes[i]!; + return k; +} +if (isMainThread) { + if (process.argv[2] === "spawn") new Worker(new URL(import.meta.url)); + const shared = new Uint8Array(new SharedArrayBuffer(4)); + shared.set([1, 2, 3, 0]); + console.log(key(shared, 6), key(new Uint8Array([3, 2, 1, 0]), 6)); +} +`); + const fn = /^define internal double @sc_bf_\w*key[\w.]*\([^]*?^}/m.exec(llvm)?.[0] ?? ""; + const order = labels(fn); + const slow = order.findIndex((l) => l.startsWith("bytes.version.slow")); + const fast = order.findIndex((l) => l.startsWith("bytes.version.fast")); + expect(slow).toBeGreaterThanOrEqual(0); + expect(fast).toBeGreaterThan(slow); + expect(order.slice(slow, fast).some((l) => l.startsWith("spec.fast"))).toBe(true); + expect(order.slice(fast).some((l) => l.startsWith("spec.fast"))).toBe(true); +}); + +test("a floor-mod by a power of two speculates as a mask and a product by one as a shift", async () => { + const llvm = await emitLlvm(` +function trim(value: number, bits: number): number { + return value - Math.floor(value / Math.pow(2, bits)) * Math.pow(2, bits); +} +function key(bytes: Uint8Array, bits: number): number { + let k = 0; + for (let i = 0; i < bytes.length; i++) k = trim(k, bits) * 4 + bytes[i]!; + return k; +} +console.log(key(new Uint8Array([1, 2, 3, 0]), 6)); +`); + const fn = /^define internal double @sc_bf_\w*key[\w.]*\([^]*?^}/m.exec(llvm)?.[0] ?? ""; + const fast = fn.slice(fn.search(/^spec\.fast\d+:/m)); + expect(fast.length).toBeLessThan(fn.length); + // trim(k, bits): the low `bits` bits; * 4: a shift by two. + expect(fast).toMatch(/= sub i64 %\S+, 1\n\s+%\S+ = and i64 %\S+, %\S+\n/); + expect(fast).toMatch(/= shl nsw i64 %\S+, 2\n/); + // No floor division of k (an arithmetic shift of it) and no checked + // multiply remain. + expect(fast).not.toMatch(/ashr i64 %/); + expect(fast).not.toContain("@llvm.smul.with.overflow.i64"); +}); + +test("a speculative loop reads its views' header fields once, before its copies", async () => { + // A view held by a const local or by a parameter is the same view in + // every iteration. Its data pointer is read before the branch to the fast + // copy, and no copy (ordinary, fast, or the one a failed check resumes + // in) reads it again. + const llvm = await emitLlvm(` +let last = 42; +function fill(array: ArrayBuffer, count: number): void { + const buf = new Float64Array(array); + let l = last; + for (let i = 0; i < count; i++) { + l = (l * 3877 + 29573) % 139968; + buf[i] = l; + } + last = l; +} +function fillInto(buf: Float64Array, count: number): void { + let l = last; + for (let i = 0; i < count; i++) { + l = (l * 3877 + 29573) % 139968; + buf[i] = l; + } + last = l; +} +const array = new ArrayBuffer(8 * 16); +fill(array, 16); +const view = new Float64Array(8); +fillInto(view, 8); +console.log(new Float64Array(array)[15], view[7], last); +`); + const dataReads = /getelementptr inbounds %ScrBytes, ptr %\w+, i64 0, i32 3$/gm; + for (const name of ["fill", "fillInto"]) { + const fn = + new RegExp(`^define internal void @sc_bf_${name}(?:\\.i32)?\\([^]*?^}`, "m").exec( + llvm, + )?.[0] ?? ""; + const branch = /^ {2}br i1 %\w+, label %spec\.fast\d+, label %spec\.slow\d+$/m.exec(fn); + expect(branch, name).not.toBeNull(); + const copies = fn.slice(branch!.index); + expect(copies.match(dataReads), name).toBeNull(); + expect(fn.slice(0, branch!.index).match(dataReads)?.length, name).toBeGreaterThan(0); + } +}); diff --git a/packages/compiler/test/string-char-loop-emission.test.ts b/packages/compiler/test/string-char-loop-emission.test.ts index 6f5756ce8..9f2553d85 100644 --- a/packages/compiler/test/string-char-loop-emission.test.ts +++ b/packages/compiler/test/string-char-loop-emission.test.ts @@ -43,8 +43,12 @@ console.log(spaces("a b c"), letters("abC1z"), chars("xyz").join("-"), chars("é ll.match( new RegExp(`define internal [^\\n]+ @sc_bf_${name}\\([^\\n]*\\) #0 \\{[\\s\\S]*?\\n\\}`), )![0]; - // The fast copy follows the checked one. - const fast = (fn: string): string => fn.slice(fn.lastIndexOf("bytes.version.fast")); + // The byte copy follows the checked one, and the units copy (for + // strings that are not ASCII) follows it. + const fast = (fn: string): string => { + const units = fn.search(/^bytes\.version\.fast\.units\d+:/m); + return fn.slice(fn.search(/^bytes\.version\.fast\d+:/m), units < 0 ? undefined : units); + }; // `s[i] === " "` compares the byte; the checked copy keeps the helper. const spaces = body("spaces"); diff --git a/packages/compiler/test/string-iteration-emission.test.ts b/packages/compiler/test/string-iteration-emission.test.ts new file mode 100644 index 000000000..7cda3da55 --- /dev/null +++ b/packages/compiler/test/string-iteration-emission.test.ts @@ -0,0 +1,47 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, expect, test } from "vitest"; +import { compile } from "../src/index.js"; + +const directories: string[] = []; +afterEach(async () => { + await Promise.all(directories.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))); +}); + +async function emit(source: string): Promise { + const outDir = await mkdtemp(join(tmpdir(), "scriptc-string-iteration-")); + directories.push(outDir); + const entry = join(outDir, "main.ts"); + await writeFile(entry, source); + const outPath = join(outDir, "program.ll"); + const result = await compile(entry, { entry, outDir, outPath, outputKind: "llvm" }); + expect(result.ok, JSON.stringify(result)).toBe(true); + return readFile(outPath, "utf8"); +} + +const calls = (llvm: string, symbol: string): string[] => + llvm.split("\n").filter((line) => line.includes("call ") && line.includes(`@${symbol}(`)); + +test("for-of over a string walks a byte cursor", async () => { + const llvm = await emit(` +function count(s: string): number { + let n = 0; + for (const c of s) n += c.length; + return n; +} +console.log(count(process.argv[1] ?? "")); +`); + expect(calls(llvm, "sc_str_cp_at_byte")).toHaveLength(1); + expect(calls(llvm, "scr_str_cp_at")).toHaveLength(0); + // The helper answers ASCII bytes from the one-character table. + expect(llvm).toContain("define internal ptr @sc_str_cp_at_byte(ptr %s, i64 %b)"); +}); + +test("spread and Array.from of a string build the array in one call", async () => { + const llvm = await emit(` +const s = process.argv[1] ?? ""; +console.log([...s].length, Array.from(s).length); +`); + expect(calls(llvm, "scr_str_code_points")).toHaveLength(2); +}); diff --git a/packages/compiler/test/string-scan-emission.test.ts b/packages/compiler/test/string-scan-emission.test.ts index 427e2d543..76242e3d4 100644 --- a/packages/compiler/test/string-scan-emission.test.ts +++ b/packages/compiler/test/string-scan-emission.test.ts @@ -37,7 +37,9 @@ console.log(scan("é-é", "ü-ü", "ö-ö"), at("é", 0.5)); expect(llvm).toContain("@scr_str_scan = external global"); const fallbacks = llvm .split("\n") - .filter((line) => /call double @scr_str_(utf16_len|char_code_at)\(/.test(line)); + .filter((line) => + /call (double|void) @scr_str_(utf16_len|char_code_at|scan_units)\(/.test(line), + ); expect(fallbacks.length).toBeGreaterThanOrEqual(3); for (const line of fallbacks) expect(line).not.toMatch(/\bmemory\(/); } finally { diff --git a/packages/compiler/test/string-unit-loop-emission.test.ts b/packages/compiler/test/string-unit-loop-emission.test.ts new file mode 100644 index 000000000..f522159dc --- /dev/null +++ b/packages/compiler/test/string-unit-loop-emission.test.ts @@ -0,0 +1,235 @@ +import { execFileSync } from "node:child_process"; +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile, deserializeModule } from "../src/index.js"; +import { emitLlvmModule } from "../src/backend/llvm/emitter.js"; + +test("scans of strings that are not ASCII read their UTF-16 units", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-unit-loops-")); + try { + const entry = join(dir, "main.ts"); + await writeFile( + entry, + ` +function spaces(s: string): number { + let n = 0; + for (let i = 0; i < s.length; i++) if (s.charCodeAt(i) === 32) n++; + return n; +} +function skip(s: string): number { + let pos = 0; + while (pos < s.length && s.charCodeAt(pos) !== 32) pos++; + return pos; +} +function pairs(s: string, t: string): number { + let n = 0; + for (let i = 0; i < s.length; i++) if (s.charCodeAt(i) === t.charCodeAt(i % t.length)) n++; + return n; +} +let text = ""; +for (let k = 0; k < 400; k++) text += k % 3 === 0 ? "café " : "naïve façade "; +const other = text.replace(/a/g, "ä"); +let total = 0; +for (let pass = 0; pass < 4; pass++) + total += spaces(text) + spaces(other) + pairs(text, other) + skip(text); +console.log(total, spaces("plain ascii text"), spaces("")); +`, + ); + const outPath = join(dir, "main.ir.json"); + const lowered = await compile(entry, { outDir: dir, outPath, outputKind: "ir" }); + if (!lowered.ok) + throw new Error(lowered.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + const ll = emitLlvmModule(deserializeModule(await readFile(outPath, "utf8"))); + const body = (name: string): string => + ll.match( + new RegExp( + `define internal [^\\n]+ @sc_bf_${name}(?:\\.i32)?\\([^\\n]*\\) #0 \\{[\\s\\S]*?\\n\\}`, + ), + )![0]; + + const spaces = body("spaces"); + // A string that is not ASCII is converted to units and published as the + // scan receiver once, before the loop, and its copy loads units. The + // loop runs to the length, so it gives the runtime no range (NaN). + expect(spaces).toMatch( + /call void @scr_str_scan_units_for\(ptr %\S+, double 0x7FF8000000000000, double 0x7FF8000000000000\)/, + ); + // The copies follow the checked one: bytes, then units. + const bytesAt = spaces.search(/^bytes\.version\.fast\d+:/m); + const unitsAt = spaces.search(/^bytes\.version\.fast\.units\d+:/m); + expect(bytesAt).toBeGreaterThan(0); + expect(unitsAt).toBeGreaterThan(bytesAt); + const units = spaces.slice(unitsAt); + expect(units).toMatch(/load i16, ptr %\S+\n\s+%\S+ = uitofp i16/); + expect(units).not.toContain("@sc_str_char_code_at_int"); + // An ASCII string still reads bytes. + const bytes = spaces.slice(bytesAt, unitsAt); + expect(bytes).toMatch(/load i8, ptr %\S+\n\s+%\S+ = uitofp i8/); + expect(bytes).not.toContain("@sc_str_char_code_at_int"); + // A while loop over a counter below the length reads units too. + const skip = body("skip"); + expect(skip).toMatch(/^bytes\.version\.fast\.units\d+:/m); + // Its fast copies read the length the guard took; the checked copy + // still asks for it. + const skipFastAt = skip.search(/^bytes\.version\.fast\d+:/m); + expect(skip.slice(0, skipFastAt)).toContain("@sc_str_utf16_len"); + expect(skip.slice(skipFastAt)).not.toContain("@sc_str_utf16_len"); + // Two strings cannot both be the scan receiver. + expect(body("pairs")).not.toContain("bytes.version.fast.units"); + + const exe = join(dir, "main"); + const built = await compile(entry, { outputKind: "exe", outDir: dir, outPath: exe }); + if (!built.ok) throw new Error(JSON.stringify(built.diagnostics)); + let text = ""; + for (let k = 0; k < 400; k++) text += k % 3 === 0 ? "café " : "naïve façade "; + const other = text.replace(/a/g, "ä"); + const count = (s: string): number => s.split(" ").length - 1; + let pairs = 0; + for (let i = 0; i < text.length; i++) + if (text.charCodeAt(i) === other.charCodeAt(i % other.length)) pairs++; + const total = 4 * (count(text) + count(other) + pairs + text.indexOf(" ")); + expect(execFileSync(exe, { encoding: "utf8" })).toBe(`${total} 2 0\n`); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); + +test("loops that start at each token publish the scan receiver only when it changed", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-unit-republish-")); + try { + const entry = join(dir, "main.ts"); + await writeFile( + entry, + ` +function words(s: string): number { + let n = 0, + pos = 0; + while (pos < s.length) { + while (pos < s.length && s.charCodeAt(pos) !== 32) pos++; + while (pos < s.length && s.charCodeAt(pos) === 32) pos++; + n++; + } + return n; +} +let text = ""; +for (let k = 0; k < 400; k++) text += k % 3 === 0 ? "café " : "naïve façade "; +console.log(words(text), words(text.replace(/a/g, "ä")), words("plain ascii")); +`, + ); + const outPath = join(dir, "main.ir.json"); + const lowered = await compile(entry, { outDir: dir, outPath, outputKind: "ir" }); + if (!lowered.ok) + throw new Error(lowered.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + const ll = emitLlvmModule(deserializeModule(await readFile(outPath, "utf8"))); + const words = ll.match( + /define internal [^\n]+ @sc_bf_words(?:\.i32)?\([^\n]*\) #0 \{[\s\S]*?\n\}/, + )![0]; + // Each inner loop's entry compares the scan receiver with `s` and calls + // the runtime to publish `s` only when they differ. + const guards = [ + ...words.matchAll( + /^bytes\.version\.publish\d+:[^\n]*\n\s+(%\S+) = load ptr, ptr @scr_str_scan\n\s+(%\S+) = icmp eq ptr \1, (%\S+)\n\s+br i1 \2, label %(bytes\.version\.publish\.load\d+), label %(bytes\.version\.publish\.units\d+)[^\n]*\n/gm, + ), + ]; + expect(guards.length).toBeGreaterThanOrEqual(2); + for (const [, , , s, load, convert] of guards) { + // The block reads the loop's range and makes no other call. + const block = words + .slice(words.search(new RegExp(`^${convert}:`, "m"))) + .match(new RegExp(`^${convert}:[^\\n]*\\n([\\s\\S]*?)\\n\\s+br label %${load}\\n`))!; + expect(block).not.toBeNull(); + const lines = block[1]!.split("\n").map((line) => line.trim()); + expect(lines.at(-1)).toMatch( + new RegExp(`^call void @scr_str_scan_units_for\\(ptr ${s}, double \\S+, double \\S+\\)$`), + ); + expect(lines.slice(0, -1).filter((line) => /\bcall\b(?! double @llvm\.)/.test(line))).toEqual( + [], + ); + } + const calls = words.match( + /call void @scr_str_scan_units_for\(ptr %\S+, double \S+, double \S+\)/g, + )!; + expect(calls.length).toBe(guards.length); + + const exe = join(dir, "main"); + const built = await compile(entry, { outputKind: "exe", outDir: dir, outPath: exe }); + if (!built.ok) throw new Error(JSON.stringify(built.diagnostics)); + expect(execFileSync(exe, { encoding: "utf8" })).toBe("666 666 2\n"); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); + +test("loops over part of a string give the runtime the range of indexes they read", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-unit-reach-")); + try { + const entry = join(dir, "main.ts"); + await writeFile( + entry, + ` +function trim(text: string, start: number, end: number): string { + while (start < end && text.charCodeAt(start) === 32) start++; + return text.substring(start, end); +} +function hasBreak(text: string, pos: number, end: number): boolean { + for (let i = pos; i < end; i++) if (text.charCodeAt(i) === 10) return true; + return false; +} +let text = ""; +for (let k = 0; k < 2000; k++) text += k % 7 === 0 ? " café\\n" : " naïve façade"; +let kept = 0, + breaks = 0; +for (let at = 0; at + 12 < text.length; at += 12) { + kept += trim(text, at, at + 12).length; + if (hasBreak(text, at, at + 12)) breaks++; +} +console.log(kept, breaks); +`, + ); + const outPath = join(dir, "main.ir.json"); + const lowered = await compile(entry, { outDir: dir, outPath, outputKind: "ir" }); + if (!lowered.ok) + throw new Error(lowered.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + const ll = emitLlvmModule(deserializeModule(await readFile(outPath, "utf8"))); + const body = (name: string): string => + ll.match( + new RegExp( + `define internal [^\\n]+ @sc_bf_${name}(?:\\.i32)?\\([^\\n]*\\) #0 \\{[\\s\\S]*?\\n\\}`, + ), + )![0]; + // The publish block passes the loop's range with the receiver: the + // counter and the bound (a while loop), or the start and the limit (a + // counted loop), here the function's parameters. + for (const name of ["trim", "hasBreak"]) { + const block = body(name).match( + /^bytes\.version\.publish\.units\d+:[^\n]*\n([\s\S]*?)\n\s+br label %bytes\.version\.publish\.load\d+\n/m, + )!; + expect(block, name).not.toBeNull(); + const [, lines] = block; + const call = lines!.match( + /call void @scr_str_scan_units_for\(ptr %\S+, double (%\S+), double (%\S+)\)/, + )!; + expect(call, name).not.toBeNull(); + expect(call[1]).not.toBe(call[2]); + } + + const exe = join(dir, "main"); + const built = await compile(entry, { outputKind: "exe", outDir: dir, outPath: exe }); + if (!built.ok) throw new Error(JSON.stringify(built.diagnostics)); + let text = ""; + for (let k = 0; k < 2000; k++) text += k % 7 === 0 ? " café\n" : " naïve façade"; + let kept = 0, + breaks = 0; + for (let at = 0; at + 12 < text.length; at += 12) { + let start = at; + while (start < at + 12 && text.charCodeAt(start) === 32) start++; + kept += at + 12 - start; + if (text.slice(at, at + 12).includes("\n")) breaks++; + } + expect(execFileSync(exe, { encoding: "utf8" })).toBe(`${kept} ${breaks}\n`); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); diff --git a/packages/compiler/test/target-cpu-emit.test.ts b/packages/compiler/test/target-cpu-emit.test.ts new file mode 100644 index 000000000..5dee8243c --- /dev/null +++ b/packages/compiler/test/target-cpu-emit.test.ts @@ -0,0 +1,46 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile } from "../src/index.js"; + +const program = `const a = new Int32Array(4096); +for (let i = 0; i < a.length; i++) a[i] = (i * 2654435761) | 0; +let s = 0; +for (let r = 0; r < 64; r++) for (let i = 0; i < a.length; i++) s = (s + a[i]!) | 0; +console.log(s); +`; + +test.runIf(process.platform === "linux" && process.arch === "x64")( + "assembly, object and LLVM builds honor the target CPU", + async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-target-cpu-emit-")); + const entry = join(dir, "main.ts"); + const emit = async ( + outputKind: "llvm" | "asm" | "obj", + targetCpu?: "x86-64-v3", + ): Promise => { + const outPath = join(dir, `main-${outputKind}-${targetCpu ?? "baseline"}`); + const result = await compile(entry, { + outPath, + outDir: dir, + outputKind, + ...(targetCpu === undefined ? {} : { targetCpu }), + }); + if (!result.ok) throw new Error(result.diagnostics.map((d) => d.message).join("\n")); + return readFile(outPath); + }; + try { + await writeFile(entry, program); + expect((await emit("llvm")).toString()).not.toContain('"target-cpu"'); + expect((await emit("llvm", "x86-64-v3")).toString()).toContain('"target-cpu"="x86-64-v3"'); + // The v3 build vectorizes the sums with AVX2's 256-bit registers. + expect((await emit("asm")).toString()).not.toContain("%ymm"); + expect((await emit("asm", "x86-64-v3")).toString()).toContain("%ymm"); + expect((await emit("obj", "x86-64-v3")).equals(await emit("obj"))).toBe(false); + } finally { + await rm(dir, { recursive: true, force: true }); + } + }, + 240_000, +); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5025-lifetime-region-shared-candidate.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5025-lifetime-region-shared-candidate.ts.json new file mode 100644 index 000000000..73b06cc54 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5025-lifetime-region-shared-candidate.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5025-lifetime-region-shared-candidate.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5965-date-objects.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5965-date-objects.ts.json new file mode 100644 index 000000000..c2497d502 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5965-date-objects.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5965-date-objects.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5966-date-time-values.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5966-date-time-values.ts.json new file mode 100644 index 000000000..e6155bcc4 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5966-date-time-values.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5966-date-time-values.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5967-date-alias-mutation/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5967-date-alias-mutation/main.ts.json new file mode 100644 index 000000000..bdd29bf77 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5967-date-alias-mutation/main.ts.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/5967-date-alias-mutation/clock.js", + "/tests/corpus/5967-date-alias-mutation/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6067-view-holes-in-typed-arrays.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6067-view-holes-in-typed-arrays.ts.json new file mode 100644 index 000000000..9d257fd49 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6067-view-holes-in-typed-arrays.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6067-view-holes-in-typed-arrays.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6068-foreign-elements-through-views.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6068-foreign-elements-through-views.ts.json new file mode 100644 index 000000000..7da41a80f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6068-foreign-elements-through-views.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6068-foreign-elements-through-views.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6069-foreign-element-writers.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6069-foreign-element-writers.ts.json new file mode 100644 index 000000000..973bdb93e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6069-foreign-element-writers.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6069-foreign-element-writers.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6305-json-discriminant-dispatch.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6305-json-discriminant-dispatch.ts.json new file mode 100644 index 000000000..b83ada276 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6305-json-discriminant-dispatch.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6305-json-discriminant-dispatch.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6306-discriminant-getter-dispatch.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6306-discriminant-getter-dispatch.ts.json new file mode 100644 index 000000000..ba7cba509 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6306-discriminant-getter-dispatch.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6306-discriminant-getter-dispatch.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6307-number-map-get-set-fusion.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6307-number-map-get-set-fusion.ts.json new file mode 100644 index 000000000..36c6b7a10 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6307-number-map-get-set-fusion.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6307-number-map-get-set-fusion.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6308-number-map-integer-keys.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6308-number-map-integer-keys.ts.json new file mode 100644 index 000000000..b58240a3e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6308-number-map-integer-keys.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6308-number-map-integer-keys.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6310-small-number-map-direct-index.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6310-small-number-map-direct-index.ts.json new file mode 100644 index 000000000..72472aa95 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6310-small-number-map-direct-index.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6310-small-number-map-direct-index.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6311-typed-array-integer-products.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6311-typed-array-integer-products.ts.json new file mode 100644 index 000000000..d3134396a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6311-typed-array-integer-products.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6311-typed-array-integer-products.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6312-typed-array-read-indices.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6312-typed-array-read-indices.ts.json new file mode 100644 index 000000000..af5fb12f9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6312-typed-array-read-indices.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6312-typed-array-read-indices.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6343-inspect-class-values.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6343-inspect-class-values.ts.json new file mode 100644 index 000000000..4fa8625ac --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6343-inspect-class-values.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6343-inspect-class-values.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6344-inspect-function-properties.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6344-inspect-function-properties.cjs.json new file mode 100644 index 000000000..003075140 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6344-inspect-function-properties.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6344-inspect-function-properties.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6357-format-o-hidden.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6357-format-o-hidden.ts.json new file mode 100644 index 000000000..cf0cd31d1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6357-format-o-hidden.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6357-format-o-hidden.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6358-format-o-hidden-js.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6358-format-o-hidden-js.cjs.json new file mode 100644 index 000000000..73d4eefa6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6358-format-o-hidden-js.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6358-format-o-hidden-js.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6928-class-iterator-protocol.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6928-class-iterator-protocol.ts.json new file mode 100644 index 000000000..2efd8521a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6928-class-iterator-protocol.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6928-class-iterator-protocol.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6929-class-iterator-protocol.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6929-class-iterator-protocol.mjs.json new file mode 100644 index 000000000..49e5e4304 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6929-class-iterator-protocol.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6929-class-iterator-protocol.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6935-class-async-iterator-protocol.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6935-class-async-iterator-protocol.ts.json new file mode 100644 index 000000000..93775b96b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6935-class-async-iterator-protocol.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6935-class-async-iterator-protocol.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6935-fs-url-paths-ordered.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6935-fs-url-paths-ordered.ts.json new file mode 100644 index 000000000..ec3d3f004 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6935-fs-url-paths-ordered.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6935-fs-url-paths-ordered.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6936-class-async-iterator-protocol.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6936-class-async-iterator-protocol.mjs.json new file mode 100644 index 000000000..378f7d18b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6936-class-async-iterator-protocol.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6936-class-async-iterator-protocol.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6936-x509-certificate-fields.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6936-x509-certificate-fields.ts.json new file mode 100644 index 000000000..2f7224882 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6936-x509-certificate-fields.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6936-x509-certificate-fields.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6937-class-value-arrays.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6937-class-value-arrays.mjs.json new file mode 100644 index 000000000..eefa82916 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6937-class-value-arrays.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6937-class-value-arrays.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6938-class-value-arrays.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6938-class-value-arrays.ts.json new file mode 100644 index 000000000..ff2a51546 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6938-class-value-arrays.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6938-class-value-arrays.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7012-dictionary-views-keep-objects.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7012-dictionary-views-keep-objects.ts.json new file mode 100644 index 000000000..6d7944cf7 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7012-dictionary-views-keep-objects.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7012-dictionary-views-keep-objects.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7013-dictionary-view-parameter.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7013-dictionary-view-parameter.ts.json new file mode 100644 index 000000000..41f80c41e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7013-dictionary-view-parameter.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7013-dictionary-view-parameter.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7035-promise-all-tuple-turns.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7035-promise-all-tuple-turns.ts.json new file mode 100644 index 000000000..29b935e93 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7035-promise-all-tuple-turns.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7035-promise-all-tuple-turns.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7090-x509-check-names.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7090-x509-check-names.ts.json new file mode 100644 index 000000000..e375f8d72 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7090-x509-check-names.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7090-x509-check-names.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7091-x509-certificate-keys.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7091-x509-certificate-keys.ts.json new file mode 100644 index 000000000..d4e9c4a6d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7091-x509-certificate-keys.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7091-x509-certificate-keys.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7092-x509-legacy-object.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7092-x509-legacy-object.ts.json new file mode 100644 index 000000000..fd0b42f36 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7092-x509-legacy-object.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7092-x509-legacy-object.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7093-x509-methods-js.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7093-x509-methods-js.mjs.json new file mode 100644 index 000000000..deeeb0047 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7093-x509-methods-js.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7093-x509-methods-js.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7095-tls-peer-certificates.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7095-tls-peer-certificates.ts.json new file mode 100644 index 000000000..b6ebaa3e2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7095-tls-peer-certificates.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7095-tls-peer-certificates.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7096-tls-peer-certificates.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7096-tls-peer-certificates.mjs.json new file mode 100644 index 000000000..2b85b584a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7096-tls-peer-certificates.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7096-tls-peer-certificates.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7097-tls-client-certificates.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7097-tls-client-certificates.ts.json new file mode 100644 index 000000000..ecd6a35d7 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7097-tls-client-certificates.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7097-tls-client-certificates.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7098-tls-client-certificates-paths.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7098-tls-client-certificates-paths.ts.json new file mode 100644 index 000000000..8be352a07 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7098-tls-client-certificates-paths.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7098-tls-client-certificates-paths.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7099-tls-client-certificates.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7099-tls-client-certificates.mjs.json new file mode 100644 index 000000000..9dc6fbc9f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7099-tls-client-certificates.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7099-tls-client-certificates.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7110-als-completion-callbacks.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7110-als-completion-callbacks.ts.json new file mode 100644 index 000000000..5645ac5df --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7110-als-completion-callbacks.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7110-als-completion-callbacks.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7111-als-bind-snapshot-options.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7111-als-bind-snapshot-options.ts.json new file mode 100644 index 000000000..27aa14a8d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7111-als-bind-snapshot-options.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7111-als-bind-snapshot-options.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7112-als-bind-snapshot-dynamic.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7112-als-bind-snapshot-dynamic.mjs.json new file mode 100644 index 000000000..1915cae63 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7112-als-bind-snapshot-dynamic.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7112-als-bind-snapshot-dynamic.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7119-als-net-event-context.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7119-als-net-event-context.mjs.json new file mode 100644 index 000000000..7ff81ea34 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7119-als-net-event-context.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7119-als-net-event-context.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7160-json-parse-deep-unions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7160-json-parse-deep-unions.ts.json new file mode 100644 index 000000000..fb4ef3c1f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7160-json-parse-deep-unions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7160-json-parse-deep-unions.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7161-dynamic-values-deep-unions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7161-dynamic-values-deep-unions.ts.json new file mode 100644 index 000000000..8ad1579da --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7161-dynamic-values-deep-unions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7161-dynamic-values-deep-unions.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7186-namespace-object-values.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7186-namespace-object-values.js.json new file mode 100644 index 000000000..a130b4af6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7186-namespace-object-values.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7186-namespace-object-values.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7187-namespace-object-reflection.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7187-namespace-object-reflection.js.json new file mode 100644 index 000000000..b2d9bc751 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7187-namespace-object-reflection.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7187-namespace-object-reflection.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7188-namespace-object-extensions.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7188-namespace-object-extensions.js.json new file mode 100644 index 000000000..96cbad8c2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7188-namespace-object-extensions.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7188-namespace-object-extensions.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7189-non-null-assertion-nullish-destination.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7189-non-null-assertion-nullish-destination.ts.json new file mode 100644 index 000000000..dc0d277ee --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7189-non-null-assertion-nullish-destination.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7189-non-null-assertion-nullish-destination.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7260-teardown-segments.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7260-teardown-segments.ts.json new file mode 100644 index 000000000..a7eac099f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7260-teardown-segments.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7260-teardown-segments.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7264-array-dense-paired-loops.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7264-array-dense-paired-loops.ts.json new file mode 100644 index 000000000..8396d237c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7264-array-dense-paired-loops.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7264-array-dense-paired-loops.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7304-string-optional-char-reads.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7304-string-optional-char-reads.ts.json new file mode 100644 index 000000000..36ea6f8ec --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7304-string-optional-char-reads.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7304-string-optional-char-reads.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7305-string-unit-loop-versions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7305-string-unit-loop-versions.ts.json new file mode 100644 index 000000000..21b8255e4 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7305-string-unit-loop-versions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7305-string-unit-loop-versions.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7306-dunder-exports/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7306-dunder-exports/main.ts.json new file mode 100644 index 000000000..fd14c0dfe --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7306-dunder-exports/main.ts.json @@ -0,0 +1,10 @@ +{ + "order": [ + "/tests/corpus/7306-dunder-exports/dep.ts", + "/tests/corpus/7306-dunder-exports/star.ts", + "/tests/corpus/7306-dunder-exports/facade.ts", + "/tests/corpus/7306-dunder-exports/table.cjs", + "/tests/corpus/7306-dunder-exports/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7307-string-unit-loop-evictions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7307-string-unit-loop-evictions.ts.json new file mode 100644 index 000000000..cb5864986 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7307-string-unit-loop-evictions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7307-string-unit-loop-evictions.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7309-globalthis-buffer-aliases.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7309-globalthis-buffer-aliases.ts.json new file mode 100644 index 000000000..94d458e11 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7309-globalthis-buffer-aliases.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7309-globalthis-buffer-aliases.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7310-any-destructure-string-keys.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7310-any-destructure-string-keys.ts.json new file mode 100644 index 000000000..45a9f62ff --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7310-any-destructure-string-keys.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7310-any-destructure-string-keys.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7380-als-http-event-context.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7380-als-http-event-context.mjs.json new file mode 100644 index 000000000..1087d25fe --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7380-als-http-event-context.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7380-als-http-event-context.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7380-cycle-deferred-folded-key/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7380-cycle-deferred-folded-key/main.ts.json new file mode 100644 index 000000000..18ef785c5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7380-cycle-deferred-folded-key/main.ts.json @@ -0,0 +1,8 @@ +{ + "order": [ + "/tests/corpus/7380-cycle-deferred-folded-key/util.ts", + "/tests/corpus/7380-cycle-deferred-folded-key/core.ts", + "/tests/corpus/7380-cycle-deferred-folded-key/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7381-als-async-generator-steps.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7381-als-async-generator-steps.ts.json new file mode 100644 index 000000000..47f93314d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7381-als-async-generator-steps.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7381-als-async-generator-steps.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7381-cycle-deferred-reached-tdz/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7381-cycle-deferred-reached-tdz/main.ts.json new file mode 100644 index 000000000..65d9b19e9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7381-cycle-deferred-reached-tdz/main.ts.json @@ -0,0 +1,8 @@ +{ + "order": [ + "/tests/corpus/7381-cycle-deferred-reached-tdz/beta.ts", + "/tests/corpus/7381-cycle-deferred-reached-tdz/alpha.ts", + "/tests/corpus/7381-cycle-deferred-reached-tdz/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7381-function-value-arguments-length.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7381-function-value-arguments-length.ts.json new file mode 100644 index 000000000..b9e5eacd1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7381-function-value-arguments-length.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7381-function-value-arguments-length.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7382-als-async-generator-steps.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7382-als-async-generator-steps.mjs.json new file mode 100644 index 000000000..5f55ba7bd --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7382-als-async-generator-steps.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7382-als-async-generator-steps.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7382-cycle-static-this-tdz/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7382-cycle-static-this-tdz/main.ts.json new file mode 100644 index 000000000..986683684 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7382-cycle-static-this-tdz/main.ts.json @@ -0,0 +1,8 @@ +{ + "order": [ + "/tests/corpus/7382-cycle-static-this-tdz/beta.ts", + "/tests/corpus/7382-cycle-static-this-tdz/alpha.ts", + "/tests/corpus/7382-cycle-static-this-tdz/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7383-als-stream-events.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7383-als-stream-events.ts.json new file mode 100644 index 000000000..d59808c2f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7383-als-stream-events.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7383-als-stream-events.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7383-cycle-class-expression-name-tdz/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7383-cycle-class-expression-name-tdz/main.ts.json new file mode 100644 index 000000000..69b6d63bb --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7383-cycle-class-expression-name-tdz/main.ts.json @@ -0,0 +1,8 @@ +{ + "order": [ + "/tests/corpus/7383-cycle-class-expression-name-tdz/beta.ts", + "/tests/corpus/7383-cycle-class-expression-name-tdz/alpha.ts", + "/tests/corpus/7383-cycle-class-expression-name-tdz/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7384-als-stream-events.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7384-als-stream-events.mjs.json new file mode 100644 index 000000000..51171c8d5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7384-als-stream-events.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7384-als-stream-events.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7385-als-child-process-events.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7385-als-child-process-events.ts.json new file mode 100644 index 000000000..aac67defb --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7385-als-child-process-events.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7385-als-child-process-events.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7386-als-child-fork-events/main.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7386-als-child-fork-events/main.mjs.json new file mode 100644 index 000000000..a334cf0aa --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7386-als-child-fork-events/main.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7386-als-child-fork-events/main.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7387-als-http2-events.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7387-als-http2-events.mjs.json new file mode 100644 index 000000000..272024d1b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7387-als-http2-events.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7387-als-http2-events.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7388-als-http2-compat-events.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7388-als-http2-compat-events.mjs.json new file mode 100644 index 000000000..8a64f395e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7388-als-http2-compat-events.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7388-als-http2-compat-events.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7389-als-http2-secure-events.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7389-als-http2-secure-events.mjs.json new file mode 100644 index 000000000..aec8e2d2a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7389-als-http2-secure-events.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7389-als-http2-secure-events.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7390-record-key-read-presence.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7390-record-key-read-presence.ts.json new file mode 100644 index 000000000..74bd7e30d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7390-record-key-read-presence.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7390-record-key-read-presence.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7391-als-response-end-and-finished.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7391-als-response-end-and-finished.mjs.json new file mode 100644 index 000000000..49a506703 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7391-als-response-end-and-finished.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7391-als-response-end-and-finished.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7392-non-null-operands.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7392-non-null-operands.ts.json new file mode 100644 index 000000000..be7ffaa53 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7392-non-null-operands.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7392-non-null-operands.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7394-non-null-typed-array-reads.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7394-non-null-typed-array-reads.ts.json new file mode 100644 index 000000000..5fbae0c10 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7394-non-null-typed-array-reads.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7394-non-null-typed-array-reads.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7404-frozen-records.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7404-frozen-records.ts.json new file mode 100644 index 000000000..d85f2aab5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7404-frozen-records.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7404-frozen-records.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7405-frozen-records-sloppy.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7405-frozen-records-sloppy.cjs.json new file mode 100644 index 000000000..3d87d019b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7405-frozen-records-sloppy.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7405-frozen-records-sloppy.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7406-frozen-containers.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7406-frozen-containers.ts.json new file mode 100644 index 000000000..34fb3ba0e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7406-frozen-containers.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7406-frozen-containers.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7407-frozen-class-instances.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7407-frozen-class-instances.ts.json new file mode 100644 index 000000000..da868130c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7407-frozen-class-instances.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7407-frozen-class-instances.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7408-frozen-js-class-symbol-fields.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7408-frozen-js-class-symbol-fields.js.json new file mode 100644 index 000000000..a57d1c73b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7408-frozen-js-class-symbol-fields.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7408-frozen-js-class-symbol-fields.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7440-tls-server-add-context.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7440-tls-server-add-context.ts.json new file mode 100644 index 000000000..8acc4aade --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7440-tls-server-add-context.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7440-tls-server-add-context.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7441-tls-client-error.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7441-tls-client-error.ts.json new file mode 100644 index 000000000..ee5e78748 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7441-tls-client-error.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7441-tls-client-error.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7442-tls-server-contexts.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7442-tls-server-contexts.mjs.json new file mode 100644 index 000000000..22eda3ac8 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7442-tls-server-contexts.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7442-tls-server-contexts.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7443-tls-final-flight.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7443-tls-final-flight.ts.json new file mode 100644 index 000000000..2a8d134cc --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7443-tls-final-flight.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7443-tls-final-flight.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7444-tls-final-flight.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7444-tls-final-flight.mjs.json new file mode 100644 index 000000000..98f68721a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7444-tls-final-flight.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7444-tls-final-flight.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7449-crypto-raw-keys.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7449-crypto-raw-keys.mjs.json new file mode 100644 index 000000000..48bf2dfcc --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7449-crypto-raw-keys.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7449-crypto-raw-keys.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7450-crypto-x25519.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7450-crypto-x25519.mjs.json new file mode 100644 index 000000000..f156f7530 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7450-crypto-x25519.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7450-crypto-x25519.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7451-crypto-diffie-hellman.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7451-crypto-diffie-hellman.ts.json new file mode 100644 index 000000000..593054dd5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7451-crypto-diffie-hellman.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7451-crypto-diffie-hellman.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7452-crypto-diffie-hellman-errors.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7452-crypto-diffie-hellman-errors.mjs.json new file mode 100644 index 000000000..7cb78758b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7452-crypto-diffie-hellman-errors.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7452-crypto-diffie-hellman-errors.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7453-untyped-overload-results.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7453-untyped-overload-results.cjs.json new file mode 100644 index 000000000..9ba935c1e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7453-untyped-overload-results.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7453-untyped-overload-results.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7454-tls-peer-embedded-pem.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7454-tls-peer-embedded-pem.ts.json new file mode 100644 index 000000000..e4300cd15 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7454-tls-peer-embedded-pem.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7454-tls-peer-embedded-pem.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7455-tls-als-contexts.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7455-tls-als-contexts.mjs.json new file mode 100644 index 000000000..cd99090a6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7455-tls-als-contexts.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7455-tls-als-contexts.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7503-pipeline-untyped-sources.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7503-pipeline-untyped-sources.mjs.json new file mode 100644 index 000000000..4d93af132 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7503-pipeline-untyped-sources.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7503-pipeline-untyped-sources.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7504-pipeline-untyped-source-errors.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7504-pipeline-untyped-source-errors.mjs.json new file mode 100644 index 000000000..19402586b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7504-pipeline-untyped-source-errors.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7504-pipeline-untyped-source-errors.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7505-pipeline-untyped-sources.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7505-pipeline-untyped-sources.cjs.json new file mode 100644 index 000000000..29d85351d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7505-pipeline-untyped-sources.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7505-pipeline-untyped-sources.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7506-pipeline-any-source.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7506-pipeline-any-source.ts.json new file mode 100644 index 000000000..942a2b3e8 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7506-pipeline-any-source.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7506-pipeline-any-source.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7507-stream-write-error-drain.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7507-stream-write-error-drain.mjs.json new file mode 100644 index 000000000..6fa55d09d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7507-stream-write-error-drain.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7507-stream-write-error-drain.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7508-pipeline-untyped-source-functions.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7508-pipeline-untyped-source-functions.mjs.json new file mode 100644 index 000000000..4027063d4 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7508-pipeline-untyped-source-functions.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7508-pipeline-untyped-source-functions.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7509-pipeline-untyped-function-stages.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7509-pipeline-untyped-function-stages.mjs.json new file mode 100644 index 000000000..028a5c1c6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7509-pipeline-untyped-function-stages.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7509-pipeline-untyped-function-stages.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7510-pipeline-untyped-later-iterables.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7510-pipeline-untyped-later-iterables.mjs.json new file mode 100644 index 000000000..be18c801a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7510-pipeline-untyped-later-iterables.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7510-pipeline-untyped-later-iterables.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7511-pipeline-untyped-stages.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7511-pipeline-untyped-stages.cjs.json new file mode 100644 index 000000000..2353cc44c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7511-pipeline-untyped-stages.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7511-pipeline-untyped-stages.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7512-pipeline-any-stages.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7512-pipeline-any-stages.ts.json new file mode 100644 index 000000000..acea0e98d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7512-pipeline-any-stages.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7512-pipeline-any-stages.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7513-pipeline-untyped-stage-errors.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7513-pipeline-untyped-stage-errors.mjs.json new file mode 100644 index 000000000..39a5dba57 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7513-pipeline-untyped-stage-errors.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7513-pipeline-untyped-stage-errors.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7514-stream-after-write-order.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7514-stream-after-write-order.ts.json new file mode 100644 index 000000000..a6c705cd7 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7514-stream-after-write-order.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7514-stream-after-write-order.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7515-net-socket-backpressure.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7515-net-socket-backpressure.ts.json new file mode 100644 index 000000000..16f6edb8d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7515-net-socket-backpressure.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7515-net-socket-backpressure.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7516-readable-empty-push.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7516-readable-empty-push.ts.json new file mode 100644 index 000000000..9388c4f9a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7516-readable-empty-push.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7516-readable-empty-push.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7517-stream-kept-callbacks.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7517-stream-kept-callbacks.ts.json new file mode 100644 index 000000000..b3d1f420b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7517-stream-kept-callbacks.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7517-stream-kept-callbacks.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7518-stream-dropped-callbacks.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7518-stream-dropped-callbacks.ts.json new file mode 100644 index 000000000..bf393beee --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7518-stream-dropped-callbacks.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7518-stream-dropped-callbacks.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7519-net-socket-drain-listener-close.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7519-net-socket-drain-listener-close.ts.json new file mode 100644 index 000000000..a3c10da99 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7519-net-socket-drain-listener-close.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7519-net-socket-drain-listener-close.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7520-util-format-runtime.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7520-util-format-runtime.ts.json new file mode 100644 index 000000000..825fd9882 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7520-util-format-runtime.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7520-util-format-runtime.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7521-util-format-runtime.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7521-util-format-runtime.cjs.json new file mode 100644 index 000000000..0714d5c08 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7521-util-format-runtime.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7521-util-format-runtime.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7522-promisify-untyped.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7522-promisify-untyped.cjs.json new file mode 100644 index 000000000..bcd6b54c6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7522-promisify-untyped.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7522-promisify-untyped.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7523-promisify-untyped.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7523-promisify-untyped.ts.json new file mode 100644 index 000000000..20e96495a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7523-promisify-untyped.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7523-promisify-untyped.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7524-inspect-custom.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7524-inspect-custom.ts.json new file mode 100644 index 000000000..4f5a3d007 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7524-inspect-custom.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7524-inspect-custom.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7525-inspect-custom.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7525-inspect-custom.cjs.json new file mode 100644 index 000000000..dddad2358 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7525-inspect-custom.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7525-inspect-custom.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7526-inspect-options-runtime.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7526-inspect-options-runtime.ts.json new file mode 100644 index 000000000..70b202903 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7526-inspect-options-runtime.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7526-inspect-options-runtime.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7527-buffer-from-array-buffer.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7527-buffer-from-array-buffer.ts.json new file mode 100644 index 000000000..577923b2e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7527-buffer-from-array-buffer.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7527-buffer-from-array-buffer.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7528-buffer-from-array-buffer.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7528-buffer-from-array-buffer.cjs.json new file mode 100644 index 000000000..6ef4e3917 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7528-buffer-from-array-buffer.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7528-buffer-from-array-buffer.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7529-buffer-constructor.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7529-buffer-constructor.cjs.json new file mode 100644 index 000000000..5b199cd4c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7529-buffer-constructor.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7529-buffer-constructor.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7530-buffer-constructor.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7530-buffer-constructor.ts.json new file mode 100644 index 000000000..1927980cc --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7530-buffer-constructor.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7530-buffer-constructor.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7531-buffer-constructor-dependency/main.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7531-buffer-constructor-dependency/main.cjs.json new file mode 100644 index 000000000..b760b018a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7531-buffer-constructor-dependency/main.cjs.json @@ -0,0 +1,18 @@ +{ + "order": [ + "/tests/corpus/7531-buffer-constructor-dependency/main.cjs" + ], + "diags": [ + { + "code": "SC1010", + "message": "the 'legacy-buf' module is not supported yet", + "loc": { + "file": "/tests/corpus/7531-buffer-constructor-dependency/main.cjs", + "start": 362, + "end": 392 + }, + "milestone": "M4", + "hint": "relative imports (./file, ../dir/file), package.json-mediated project imports (#alias via the imports field, self-name references via exports), statically compiled npm packages, and the built-in fs, fs/promises, path, os, url, crypto, zlib, child_process, net, http, tls, https, http2, dgram, dns, dns/promises, util, util/types, string_decoder, querystring, readline, events, stream, stream/promises, stream/consumers, stream/web, buffer, assert, assert/strict, worker_threads, cluster, tty, async_hooks, timers, timers/promises, diagnostics_channel, perf_hooks, module, v8, and console modules (bare or node:-prefixed) and node:test (node:-prefixed only, like in Node) are supported" + } + ] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7532-buffer-constructor-check-limit/main.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7532-buffer-constructor-check-limit/main.cjs.json new file mode 100644 index 000000000..e2f78ad2e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7532-buffer-constructor-check-limit/main.cjs.json @@ -0,0 +1,18 @@ +{ + "order": [ + "/tests/corpus/7532-buffer-constructor-check-limit/main.cjs" + ], + "diags": [ + { + "code": "SC1010", + "message": "the 'legacy-buf' module is not supported yet", + "loc": { + "file": "/tests/corpus/7532-buffer-constructor-check-limit/main.cjs", + "start": 279, + "end": 309 + }, + "milestone": "M4", + "hint": "relative imports (./file, ../dir/file), package.json-mediated project imports (#alias via the imports field, self-name references via exports), statically compiled npm packages, and the built-in fs, fs/promises, path, os, url, crypto, zlib, child_process, net, http, tls, https, http2, dgram, dns, dns/promises, util, util/types, string_decoder, querystring, readline, events, stream, stream/promises, stream/consumers, stream/web, buffer, assert, assert/strict, worker_threads, cluster, tty, async_hooks, timers, timers/promises, diagnostics_channel, perf_hooks, module, v8, and console modules (bare or node:-prefixed) and node:test (node:-prefixed only, like in Node) are supported" + } + ] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7533-buffer-value.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7533-buffer-value.cjs.json new file mode 100644 index 000000000..de7123599 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7533-buffer-value.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7533-buffer-value.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7534-buffer-prototype-members.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7534-buffer-prototype-members.cjs.json new file mode 100644 index 000000000..03858cfce --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7534-buffer-prototype-members.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7534-buffer-prototype-members.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7535-inspect-function-properties.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7535-inspect-function-properties.cjs.json new file mode 100644 index 000000000..9831d7def --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7535-inspect-function-properties.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7535-inspect-function-properties.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7536-inspect-custom-errors.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7536-inspect-custom-errors.ts.json new file mode 100644 index 000000000..2c5325056 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7536-inspect-custom-errors.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7536-inspect-custom-errors.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7537-inspect-custom-errors.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7537-inspect-custom-errors.cjs.json new file mode 100644 index 000000000..16cb948f6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7537-inspect-custom-errors.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7537-inspect-custom-errors.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7538-inspect-custom-prototypes.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7538-inspect-custom-prototypes.mjs.json new file mode 100644 index 000000000..279015554 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7538-inspect-custom-prototypes.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7538-inspect-custom-prototypes.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7580-int32-arrays-across-calls.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7580-int32-arrays-across-calls.ts.json new file mode 100644 index 000000000..33282756c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7580-int32-arrays-across-calls.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7580-int32-arrays-across-calls.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7621-class-methods-foreign-receivers.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7621-class-methods-foreign-receivers.mjs.json new file mode 100644 index 000000000..8f50b09a3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7621-class-methods-foreign-receivers.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7621-class-methods-foreign-receivers.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7623-key-order-javascript-built-records/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7623-key-order-javascript-built-records/main.ts.json new file mode 100644 index 000000000..c3c2aa5b0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7623-key-order-javascript-built-records/main.ts.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/7623-key-order-javascript-built-records/lib.js", + "/tests/corpus/7623-key-order-javascript-built-records/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7624-key-order-boxed-callback-records/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7624-key-order-boxed-callback-records/main.ts.json new file mode 100644 index 000000000..1a617ef56 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7624-key-order-boxed-callback-records/main.ts.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/7624-key-order-boxed-callback-records/lib.js", + "/tests/corpus/7624-key-order-boxed-callback-records/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7642-element-assignment-order.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7642-element-assignment-order.ts.json new file mode 100644 index 000000000..17369f496 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7642-element-assignment-order.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7642-element-assignment-order.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7660-pointer-union-forwarded-arguments.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7660-pointer-union-forwarded-arguments.ts.json new file mode 100644 index 000000000..8f317668f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7660-pointer-union-forwarded-arguments.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7660-pointer-union-forwarded-arguments.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7661-node-call-modules/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7661-node-call-modules/main.ts.json new file mode 100644 index 000000000..85440ddc4 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7661-node-call-modules/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7661-node-call-modules/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7662-node-call-exit-events/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7662-node-call-exit-events/main.ts.json new file mode 100644 index 000000000..455735463 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7662-node-call-exit-events/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7662-node-call-exit-events/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7663-pointer-union-stored-payload.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7663-pointer-union-stored-payload.ts.json new file mode 100644 index 000000000..51998f0eb --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7663-pointer-union-stored-payload.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7663-pointer-union-stored-payload.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7680-typed-array-search.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7680-typed-array-search.ts.json new file mode 100644 index 000000000..d93d9afdc --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7680-typed-array-search.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7680-typed-array-search.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7681-typed-array-search-dynamic.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7681-typed-array-search-dynamic.mjs.json new file mode 100644 index 000000000..ab91940c0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7681-typed-array-search-dynamic.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7681-typed-array-search-dynamic.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7682-constructor-function-globals.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7682-constructor-function-globals.js.json new file mode 100644 index 000000000..26d05338c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7682-constructor-function-globals.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7682-constructor-function-globals.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7700-util-deprecated-helpers.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7700-util-deprecated-helpers.cjs.json new file mode 100644 index 000000000..3ea85d24f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7700-util-deprecated-helpers.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7700-util-deprecated-helpers.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7701-util-deprecated-helpers.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7701-util-deprecated-helpers.ts.json new file mode 100644 index 000000000..10519ed2f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7701-util-deprecated-helpers.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7701-util-deprecated-helpers.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7702-util-signal-exit-code.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7702-util-signal-exit-code.cjs.json new file mode 100644 index 000000000..fe1de1b5f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7702-util-signal-exit-code.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7702-util-signal-exit-code.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7703-util-aborted.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7703-util-aborted.cjs.json new file mode 100644 index 000000000..81b57b493 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7703-util-aborted.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7703-util-aborted.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7704-util-aborted.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7704-util-aborted.ts.json new file mode 100644 index 000000000..d59d1194f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7704-util-aborted.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7704-util-aborted.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7705-util-diff.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7705-util-diff.cjs.json new file mode 100644 index 000000000..630cca625 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7705-util-diff.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7705-util-diff.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7706-util-diff.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7706-util-diff.ts.json new file mode 100644 index 000000000..af97c99f8 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7706-util-diff.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7706-util-diff.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7707-util-mime-type.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7707-util-mime-type.cjs.json new file mode 100644 index 000000000..4c1430134 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7707-util-mime-type.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7707-util-mime-type.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7708-util-mime-type.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7708-util-mime-type.ts.json new file mode 100644 index 000000000..2c099bc66 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7708-util-mime-type.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7708-util-mime-type.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7709-function-own-properties.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7709-function-own-properties.cjs.json new file mode 100644 index 000000000..121ca711c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7709-function-own-properties.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7709-function-own-properties.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7710-util-debounce.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7710-util-debounce.cjs.json new file mode 100644 index 000000000..ed7e96287 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7710-util-debounce.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7710-util-debounce.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7711-util-throttle.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7711-util-throttle.cjs.json new file mode 100644 index 000000000..0e5bba052 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7711-util-throttle.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7711-util-throttle.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7712-util-debounce-throttle.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7712-util-debounce-throttle.ts.json new file mode 100644 index 000000000..b9ca2c2c4 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7712-util-debounce-throttle.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7712-util-debounce-throttle.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7713-buffer-numeric-untyped-arguments.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7713-buffer-numeric-untyped-arguments.cjs.json new file mode 100644 index 000000000..d0a4957c5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7713-buffer-numeric-untyped-arguments.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7713-buffer-numeric-untyped-arguments.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7714-zlib-brotli-compress.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7714-zlib-brotli-compress.cjs.json new file mode 100644 index 000000000..fb8e2d1d9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7714-zlib-brotli-compress.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7714-zlib-brotli-compress.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7715-zlib-brotli-decompress.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7715-zlib-brotli-decompress.cjs.json new file mode 100644 index 000000000..4a40657b1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7715-zlib-brotli-decompress.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7715-zlib-brotli-decompress.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7716-zlib-brotli.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7716-zlib-brotli.ts.json new file mode 100644 index 000000000..b410a8230 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7716-zlib-brotli.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7716-zlib-brotli.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7717-math-fdlibm.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7717-math-fdlibm.ts.json new file mode 100644 index 000000000..5c790ff61 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7717-math-fdlibm.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7717-math-fdlibm.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7718-math-fdlibm.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7718-math-fdlibm.cjs.json new file mode 100644 index 000000000..201049b54 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7718-math-fdlibm.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7718-math-fdlibm.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7720-array-subclass-basics.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7720-array-subclass-basics.ts.json new file mode 100644 index 000000000..11b1d8d51 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7720-array-subclass-basics.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7720-array-subclass-basics.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7721-array-subclass-species.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7721-array-subclass-species.ts.json new file mode 100644 index 000000000..41bd59eb2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7721-array-subclass-species.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7721-array-subclass-species.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7722-array-subclass-zero-array.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7722-array-subclass-zero-array.js.json new file mode 100644 index 000000000..6cbc69c47 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7722-array-subclass-zero-array.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7722-array-subclass-zero-array.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7723-array-subclass-as-array.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7723-array-subclass-as-array.ts.json new file mode 100644 index 000000000..4671e20ea --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7723-array-subclass-as-array.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7723-array-subclass-as-array.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7724-array-subclass-cycles.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7724-array-subclass-cycles.ts.json new file mode 100644 index 000000000..7956c64c5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7724-array-subclass-cycles.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7724-array-subclass-cycles.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7725-array-subclass-operations.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7725-array-subclass-operations.ts.json new file mode 100644 index 000000000..5d9c82a12 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7725-array-subclass-operations.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7725-array-subclass-operations.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7726-array-subclass-expressions.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7726-array-subclass-expressions.js.json new file mode 100644 index 000000000..7d49ce0ac --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7726-array-subclass-expressions.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7726-array-subclass-expressions.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7728-array-subclass-species-order.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7728-array-subclass-species-order.ts.json new file mode 100644 index 000000000..33f3e83f3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7728-array-subclass-species-order.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7728-array-subclass-species-order.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7730-array-subclass-retyped-trace.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7730-array-subclass-retyped-trace.ts.json new file mode 100644 index 000000000..a02b66c5a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7730-array-subclass-retyped-trace.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7730-array-subclass-retyped-trace.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7731-array-subclass-deep-release.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7731-array-subclass-deep-release.ts.json new file mode 100644 index 000000000..76a9b3988 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7731-array-subclass-deep-release.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7731-array-subclass-deep-release.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7732-array-subclass-species-bound.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7732-array-subclass-species-bound.ts.json new file mode 100644 index 000000000..c36a8a1ba --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7732-array-subclass-species-bound.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7732-array-subclass-species-bound.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7733-array-subclass-optional-length.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7733-array-subclass-optional-length.ts.json new file mode 100644 index 000000000..96cb08414 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7733-array-subclass-optional-length.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7733-array-subclass-optional-length.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7736-key-order-replaceable-method-result/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7736-key-order-replaceable-method-result/main.ts.json new file mode 100644 index 000000000..006d4a8c7 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7736-key-order-replaceable-method-result/main.ts.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/7736-key-order-replaceable-method-result/registry.js", + "/tests/corpus/7736-key-order-replaceable-method-result/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7737-key-order-self-spread.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7737-key-order-self-spread.ts.json new file mode 100644 index 000000000..97f2ca7bf --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7737-key-order-self-spread.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7737-key-order-self-spread.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7742-response-clone-tee.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7742-response-clone-tee.ts.json new file mode 100644 index 000000000..a62bef2d0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7742-response-clone-tee.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7742-response-clone-tee.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7742-weak-collections-many-keys.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7742-weak-collections-many-keys.ts.json new file mode 100644 index 000000000..e1a75c14e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7742-weak-collections-many-keys.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7742-weak-collections-many-keys.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7743-transform-stream-identity.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7743-transform-stream-identity.ts.json new file mode 100644 index 000000000..0cfff542b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7743-transform-stream-identity.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7743-transform-stream-identity.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7745-typed-array-unions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7745-typed-array-unions.ts.json new file mode 100644 index 000000000..9c770f8dd --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7745-typed-array-unions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7745-typed-array-unions.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7746-key-order-iterable-iterator.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7746-key-order-iterable-iterator.ts.json new file mode 100644 index 000000000..821fa4f21 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7746-key-order-iterable-iterator.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7746-key-order-iterable-iterator.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7747-text-codec-untyped-slot.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7747-text-codec-untyped-slot.js.json new file mode 100644 index 000000000..23efb7db6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7747-text-codec-untyped-slot.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7747-text-codec-untyped-slot.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7747-typed-array-class-values.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7747-typed-array-class-values.ts.json new file mode 100644 index 000000000..a18e9e7df --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7747-typed-array-class-values.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7747-typed-array-class-values.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7748-npm-any-bytes-receiver/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7748-npm-any-bytes-receiver/main.ts.json new file mode 100644 index 000000000..89db88211 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7748-npm-any-bytes-receiver/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7748-npm-any-bytes-receiver/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7748-typed-array-number-array-unions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7748-typed-array-number-array-unions.ts.json new file mode 100644 index 000000000..783fb1435 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7748-typed-array-number-array-unions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7748-typed-array-number-array-unions.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7749-text-codec-unknown-roundtrip.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7749-text-codec-unknown-roundtrip.ts.json new file mode 100644 index 000000000..dd9f068f3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7749-text-codec-unknown-roundtrip.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7749-text-codec-unknown-roundtrip.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7749-typed-array-element-searches.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7749-typed-array-element-searches.ts.json new file mode 100644 index 000000000..8a1372cea --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7749-typed-array-element-searches.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7749-typed-array-element-searches.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7750-key-order-javascript-spread-sources/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7750-key-order-javascript-spread-sources/main.ts.json new file mode 100644 index 000000000..19f1ba620 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7750-key-order-javascript-spread-sources/main.ts.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/7750-key-order-javascript-spread-sources/lib.js", + "/tests/corpus/7750-key-order-javascript-spread-sources/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7750-typed-array-union-inspect.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7750-typed-array-union-inspect.ts.json new file mode 100644 index 000000000..26cc31982 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7750-typed-array-union-inspect.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7750-typed-array-union-inspect.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7751-class-well-known-symbol-fields.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7751-class-well-known-symbol-fields.ts.json new file mode 100644 index 000000000..2985bdf05 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7751-class-well-known-symbol-fields.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7751-class-well-known-symbol-fields.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7752-typed-array-dynamic-methods.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7752-typed-array-dynamic-methods.js.json new file mode 100644 index 000000000..78c9633d6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7752-typed-array-dynamic-methods.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7752-typed-array-dynamic-methods.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7753-typed-array-named-properties.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7753-typed-array-named-properties.js.json new file mode 100644 index 000000000..74a4316d9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7753-typed-array-named-properties.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7753-typed-array-named-properties.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7757-speculative-key-chains.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7757-speculative-key-chains.ts.json new file mode 100644 index 000000000..274151b18 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7757-speculative-key-chains.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7757-speculative-key-chains.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7790-js-class-integrity-layouts.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7790-js-class-integrity-layouts.js.json new file mode 100644 index 000000000..526b86044 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7790-js-class-integrity-layouts.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7790-js-class-integrity-layouts.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7791-js-class-accessor-layouts.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7791-js-class-accessor-layouts.js.json new file mode 100644 index 000000000..5350539f0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7791-js-class-accessor-layouts.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7791-js-class-accessor-layouts.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7792-untyped-property-reads-mixed-receivers.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7792-untyped-property-reads-mixed-receivers.js.json new file mode 100644 index 000000000..1b28b3fbe --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7792-untyped-property-reads-mixed-receivers.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7792-untyped-property-reads-mixed-receivers.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7793-untyped-key-writes.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7793-untyped-key-writes.js.json new file mode 100644 index 000000000..b887e0fdb --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7793-untyped-key-writes.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7793-untyped-key-writes.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7794-untyped-array-iteration.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7794-untyped-array-iteration.js.json new file mode 100644 index 000000000..5ffe60f53 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7794-untyped-array-iteration.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7794-untyped-array-iteration.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7795-untyped-global-constructors.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7795-untyped-global-constructors.js.json new file mode 100644 index 000000000..48eb29e33 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7795-untyped-global-constructors.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7795-untyped-global-constructors.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7796-boxed-subclass-identity.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7796-boxed-subclass-identity.js.json new file mode 100644 index 000000000..37f785cc2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7796-boxed-subclass-identity.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7796-boxed-subclass-identity.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7902-versioned-loop-slow-copies.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7902-versioned-loop-slow-copies.ts.json new file mode 100644 index 000000000..0147937af --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7902-versioned-loop-slow-copies.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7902-versioned-loop-slow-copies.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7940-typed-array-prototype-set.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7940-typed-array-prototype-set.mjs.json new file mode 100644 index 000000000..3c59407b3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7940-typed-array-prototype-set.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7940-typed-array-prototype-set.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7941-typed-array-prototype-set.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7941-typed-array-prototype-set.ts.json new file mode 100644 index 000000000..8fbec6a90 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7941-typed-array-prototype-set.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7941-typed-array-prototype-set.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7942-dictionary-store-method-calls.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7942-dictionary-store-method-calls.ts.json new file mode 100644 index 000000000..f04478490 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7942-dictionary-store-method-calls.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7942-dictionary-store-method-calls.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7981-typed-array-paired-loops.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7981-typed-array-paired-loops.ts.json new file mode 100644 index 000000000..71fa483a8 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7981-typed-array-paired-loops.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7981-typed-array-paired-loops.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7982-integer-view-compares.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7982-integer-view-compares.ts.json new file mode 100644 index 000000000..94919aab0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7982-integer-view-compares.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7982-integer-view-compares.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7983-integer-compares.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7983-integer-compares.ts.json new file mode 100644 index 000000000..f7c61e778 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7983-integer-compares.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7983-integer-compares.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7984-untyped-array-methods.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7984-untyped-array-methods.mjs.json new file mode 100644 index 000000000..2c823a24f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7984-untyped-array-methods.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7984-untyped-array-methods.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7985-untyped-collection-types.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7985-untyped-collection-types.mjs.json new file mode 100644 index 000000000..a807063cc --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7985-untyped-collection-types.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7985-untyped-collection-types.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7990-inferred-return-overlapping-records.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7990-inferred-return-overlapping-records.ts.json new file mode 100644 index 000000000..6a55f1f2e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7990-inferred-return-overlapping-records.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7990-inferred-return-overlapping-records.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7991-unknown-class-union-casts.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7991-unknown-class-union-casts.ts.json new file mode 100644 index 000000000..68185e837 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7991-unknown-class-union-casts.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7991-unknown-class-union-casts.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7993-json-parse-error-messages.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7993-json-parse-error-messages.ts.json new file mode 100644 index 000000000..4fec854a7 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7993-json-parse-error-messages.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7993-json-parse-error-messages.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8000-optional-element-arrays-share-storage.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8000-optional-element-arrays-share-storage.ts.json new file mode 100644 index 000000000..8cdd32baf --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8000-optional-element-arrays-share-storage.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8000-optional-element-arrays-share-storage.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8001-optional-element-writers.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8001-optional-element-writers.ts.json new file mode 100644 index 000000000..fe45249bd --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8001-optional-element-writers.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8001-optional-element-writers.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8002-while-byte-length-companions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8002-while-byte-length-companions.ts.json new file mode 100644 index 000000000..e2a36c22a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8002-while-byte-length-companions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8002-while-byte-length-companions.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8003-while-reversed-byte-reads.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8003-while-reversed-byte-reads.ts.json new file mode 100644 index 000000000..26031ac1f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8003-while-reversed-byte-reads.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8003-while-reversed-byte-reads.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8004-optional-element-conversions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8004-optional-element-conversions.ts.json new file mode 100644 index 000000000..2a1bc7c66 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8004-optional-element-conversions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8004-optional-element-conversions.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8005-buffer-encoding-paths.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8005-buffer-encoding-paths.ts.json new file mode 100644 index 000000000..7d6dac570 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8005-buffer-encoding-paths.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8005-buffer-encoding-paths.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8006-buffer-base64-decode.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8006-buffer-base64-decode.ts.json new file mode 100644 index 000000000..135fe4af6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8006-buffer-base64-decode.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8006-buffer-base64-decode.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8007-buffer-fill-swap-utf16le.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8007-buffer-fill-swap-utf16le.ts.json new file mode 100644 index 000000000..f629dac24 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8007-buffer-fill-swap-utf16le.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8007-buffer-fill-swap-utf16le.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8008-string-index-paths.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8008-string-index-paths.ts.json new file mode 100644 index 000000000..5e2082ab9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8008-string-index-paths.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8008-string-index-paths.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8009-normalize-hangul-jungseong-o-yae.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8009-normalize-hangul-jungseong-o-yae.ts.json new file mode 100644 index 000000000..872a85396 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8009-normalize-hangul-jungseong-o-yae.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8009-normalize-hangul-jungseong-o-yae.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8010-http-header-raw-bytes.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8010-http-header-raw-bytes.ts.json new file mode 100644 index 000000000..78240d310 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8010-http-header-raw-bytes.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8010-http-header-raw-bytes.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8011-foreign-element-tojson.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8011-foreign-element-tojson.ts.json new file mode 100644 index 000000000..e11c1b240 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8011-foreign-element-tojson.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8011-foreign-element-tojson.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8014-foreign-element-callbacks.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8014-foreign-element-callbacks.ts.json new file mode 100644 index 000000000..7754e8cb8 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8014-foreign-element-callbacks.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8014-foreign-element-callbacks.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8020-date-setters.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8020-date-setters.ts.json new file mode 100644 index 000000000..5b23d8ecd --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8020-date-setters.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8020-date-setters.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8021-date-coercions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8021-date-coercions.ts.json new file mode 100644 index 000000000..176788025 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8021-date-coercions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8021-date-coercions.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8022-date-unions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8022-date-unions.ts.json new file mode 100644 index 000000000..ab8c55b7b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8022-date-unions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8022-date-unions.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8023-date-clone-throw.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8023-date-clone-throw.ts.json new file mode 100644 index 000000000..934d260d8 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8023-date-clone-throw.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8023-date-clone-throw.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8024-date-setters-dynamic.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8024-date-setters-dynamic.mjs.json new file mode 100644 index 000000000..505f6305d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8024-date-setters-dynamic.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8024-date-setters-dynamic.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8025-date-subclass-setters.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8025-date-subclass-setters.ts.json new file mode 100644 index 000000000..f1c4d20e0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8025-date-subclass-setters.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8025-date-subclass-setters.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8026-date-subclass-setters-dynamic.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8026-date-subclass-setters-dynamic.mjs.json new file mode 100644 index 000000000..8a0fe9b5a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8026-date-subclass-setters-dynamic.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8026-date-subclass-setters-dynamic.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8027-date-setter-order.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8027-date-setter-order.ts.json new file mode 100644 index 000000000..cfdfddfc6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8027-date-setter-order.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8027-date-setter-order.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8028-date-setter-order-dynamic.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8028-date-setter-order-dynamic.mjs.json new file mode 100644 index 000000000..0a1b69c5f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8028-date-setter-order-dynamic.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8028-date-setter-order-dynamic.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8062-assert-message-values.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8062-assert-message-values.cjs.json new file mode 100644 index 000000000..78ba99298 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8062-assert-message-values.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8062-assert-message-values.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8065-cjs-top-level-return/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8065-cjs-top-level-return/main.js.json new file mode 100644 index 000000000..22edecf3f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8065-cjs-top-level-return/main.js.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/8065-cjs-top-level-return/early.js", + "/tests/corpus/8065-cjs-top-level-return/main.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8066-object-hasown-delete.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8066-object-hasown-delete.cjs.json new file mode 100644 index 000000000..bf862b8f2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8066-object-hasown-delete.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8066-object-hasown-delete.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8067-assert-throws-validators.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8067-assert-throws-validators.cjs.json new file mode 100644 index 000000000..89f59e97c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8067-assert-throws-validators.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8067-assert-throws-validators.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8076-assert-instance-identity.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8076-assert-instance-identity.cjs.json new file mode 100644 index 000000000..b4212e234 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8076-assert-instance-identity.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8076-assert-instance-identity.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8078-instanceof-object-function.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8078-instanceof-object-function.cjs.json new file mode 100644 index 000000000..90af753d1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8078-instanceof-object-function.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8078-instanceof-object-function.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8100-typed-array-prototype-set-data-view.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8100-typed-array-prototype-set-data-view.ts.json new file mode 100644 index 000000000..916caae33 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8100-typed-array-prototype-set-data-view.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8100-typed-array-prototype-set-data-view.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8101-url-host-and-path-parsing.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8101-url-host-and-path-parsing.js.json new file mode 100644 index 000000000..d38f437bc --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8101-url-host-and-path-parsing.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8101-url-host-and-path-parsing.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8102-url-path-to-file-url-unc-hosts.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8102-url-path-to-file-url-unc-hosts.js.json new file mode 100644 index 000000000..987c72578 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8102-url-path-to-file-url-unc-hosts.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8102-url-path-to-file-url-unc-hosts.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8103-url-inspect.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8103-url-inspect.ts.json new file mode 100644 index 000000000..06632e9e7 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8103-url-inspect.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8103-url-inspect.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8104-url-to-http-options.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8104-url-to-http-options.ts.json new file mode 100644 index 000000000..aedf0a20c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8104-url-to-http-options.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8104-url-to-http-options.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8105-url-idna-hosts.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8105-url-idna-hosts.js.json new file mode 100644 index 000000000..50ed1b401 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8105-url-idna-hosts.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8105-url-idna-hosts.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8106-shared-typed-array-set.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8106-shared-typed-array-set.ts.json new file mode 100644 index 000000000..8b7a07fac --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8106-shared-typed-array-set.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8106-shared-typed-array-set.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8107-shared-typed-array-set-workers.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8107-shared-typed-array-set-workers.ts.json new file mode 100644 index 000000000..338b4bc03 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8107-shared-typed-array-set-workers.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8107-shared-typed-array-set-workers.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8110-builtin-prototype-own-members.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8110-builtin-prototype-own-members.js.json new file mode 100644 index 000000000..033847082 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8110-builtin-prototype-own-members.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8110-builtin-prototype-own-members.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8111-plain-object-check-unknown.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8111-plain-object-check-unknown.ts.json new file mode 100644 index 000000000..2986186a2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8111-plain-object-check-unknown.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8111-plain-object-check-unknown.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8112-error-factory-own-key-order.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8112-error-factory-own-key-order.js.json new file mode 100644 index 000000000..4b64d1d75 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8112-error-factory-own-key-order.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8112-error-factory-own-key-order.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8113-error-class-field-before-name.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8113-error-class-field-before-name.js.json new file mode 100644 index 000000000..ddf5981f0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8113-error-class-field-before-name.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8113-error-class-field-before-name.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8114-record-field-slices.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8114-record-field-slices.ts.json new file mode 100644 index 000000000..58ce71d85 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8114-record-field-slices.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8114-record-field-slices.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8115-error-prototype-identity.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8115-error-prototype-identity.js.json new file mode 100644 index 000000000..f4a6f13f0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8115-error-prototype-identity.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8115-error-prototype-identity.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8116-destructured-results.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8116-destructured-results.ts.json new file mode 100644 index 000000000..f19871791 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8116-destructured-results.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8116-destructured-results.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8117-scalar-slice-bounds.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8117-scalar-slice-bounds.ts.json new file mode 100644 index 000000000..1e12a8cd2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8117-scalar-slice-bounds.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8117-scalar-slice-bounds.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8118-replacer-params.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8118-replacer-params.ts.json new file mode 100644 index 000000000..4de6ead6b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8118-replacer-params.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8118-replacer-params.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8120-inspect-custom-nested.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8120-inspect-custom-nested.ts.json new file mode 100644 index 000000000..49e0de238 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8120-inspect-custom-nested.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8120-inspect-custom-nested.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8121-inspect-custom-nested.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8121-inspect-custom-nested.cjs.json new file mode 100644 index 000000000..27372abb0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8121-inspect-custom-nested.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8121-inspect-custom-nested.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8122-inspect-custom-runtime-only.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8122-inspect-custom-runtime-only.ts.json new file mode 100644 index 000000000..0f25586d4 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8122-inspect-custom-runtime-only.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8122-inspect-custom-runtime-only.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8125-inspect-custom-throws.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8125-inspect-custom-throws.ts.json new file mode 100644 index 000000000..1dc598fd4 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8125-inspect-custom-throws.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8125-inspect-custom-throws.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8126-inspect-custom-throws.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8126-inspect-custom-throws.cjs.json new file mode 100644 index 000000000..374de8db3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8126-inspect-custom-throws.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8126-inspect-custom-throws.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8129-brotli-long-param-name.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8129-brotli-long-param-name.cjs.json new file mode 100644 index 000000000..33ef96201 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8129-brotli-long-param-name.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8129-brotli-long-param-name.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8160-assert-expected-from-fields.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8160-assert-expected-from-fields.cjs.json new file mode 100644 index 000000000..fcaacbbee --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8160-assert-expected-from-fields.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8160-assert-expected-from-fields.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8161-global-computed-reads.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8161-global-computed-reads.cjs.json new file mode 100644 index 000000000..7124f14a8 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8161-global-computed-reads.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8161-global-computed-reads.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8162-navigator-global.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8162-navigator-global.cjs.json new file mode 100644 index 000000000..2fd2d279e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8162-navigator-global.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8162-navigator-global.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8163-data-function-values.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8163-data-function-values.cjs.json new file mode 100644 index 000000000..f76f88160 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8163-data-function-values.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8163-data-function-values.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8164-atob-validation.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8164-atob-validation.cjs.json new file mode 100644 index 000000000..6081b76dd --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8164-atob-validation.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8164-atob-validation.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8165-rsa-keygen-lengths.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8165-rsa-keygen-lengths.cjs.json new file mode 100644 index 000000000..45346fc86 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8165-rsa-keygen-lengths.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8165-rsa-keygen-lengths.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8166-rsa-pss-keys.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8166-rsa-pss-keys.cjs.json new file mode 100644 index 000000000..623419ef3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8166-rsa-pss-keys.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8166-rsa-pss-keys.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8167-encrypted-private-keys.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8167-encrypted-private-keys.cjs.json new file mode 100644 index 000000000..e5e03f8e9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8167-encrypted-private-keys.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8167-encrypted-private-keys.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8168-navigator-language-values.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8168-navigator-language-values.cjs.json new file mode 100644 index 000000000..616b0a1b4 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8168-navigator-language-values.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8168-navigator-language-values.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8176-tls-empty-context-ca.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8176-tls-empty-context-ca.ts.json new file mode 100644 index 000000000..536027066 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8176-tls-empty-context-ca.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8176-tls-empty-context-ca.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8177-tls-empty-client-ca.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8177-tls-empty-client-ca.ts.json new file mode 100644 index 000000000..b28587594 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8177-tls-empty-client-ca.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8177-tls-empty-client-ca.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8178-rsa-pss-auto-min-salt.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8178-rsa-pss-auto-min-salt.cjs.json new file mode 100644 index 000000000..7b3e5eef7 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8178-rsa-pss-auto-min-salt.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8178-rsa-pss-auto-min-salt.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8179-tls-reject-unauthorized-values.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8179-tls-reject-unauthorized-values.mjs.json new file mode 100644 index 000000000..ef98570e2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8179-tls-reject-unauthorized-values.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8179-tls-reject-unauthorized-values.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8180-pow2-quotients.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8180-pow2-quotients.ts.json new file mode 100644 index 000000000..58c36cf5d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8180-pow2-quotients.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8180-pow2-quotients.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8181-map-get-borrows.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8181-map-get-borrows.ts.json new file mode 100644 index 000000000..39f43aa85 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8181-map-get-borrows.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8181-map-get-borrows.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8181-x509-raw-writes.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8181-x509-raw-writes.ts.json new file mode 100644 index 000000000..d92d8f8fd --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8181-x509-raw-writes.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8181-x509-raw-writes.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8182-typed-array-alias-scopes.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8182-typed-array-alias-scopes.ts.json new file mode 100644 index 000000000..c941e8b5d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8182-typed-array-alias-scopes.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8182-typed-array-alias-scopes.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8183-key-passphrase-encoding.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8183-key-passphrase-encoding.cjs.json new file mode 100644 index 000000000..ee89a2add --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8183-key-passphrase-encoding.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8183-key-passphrase-encoding.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8190-assert-function-values.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8190-assert-function-values.cjs.json new file mode 100644 index 000000000..730dd6360 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8190-assert-function-values.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8190-assert-function-values.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8191-assert-function-values.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8191-assert-function-values.ts.json new file mode 100644 index 000000000..0837dc2ee --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8191-assert-function-values.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8191-assert-function-values.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8193-counted-loop-products.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8193-counted-loop-products.ts.json new file mode 100644 index 000000000..7a3b97b6c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8193-counted-loop-products.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8193-counted-loop-products.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8194-row-pair-sums.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8194-row-pair-sums.ts.json new file mode 100644 index 000000000..afb53a915 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8194-row-pair-sums.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8194-row-pair-sums.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8195-wrapping-products-near-2-53.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8195-wrapping-products-near-2-53.ts.json new file mode 100644 index 000000000..234bcebd5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8195-wrapping-products-near-2-53.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8195-wrapping-products-near-2-53.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8196-counted-loops-integer-starts.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8196-counted-loops-integer-starts.ts.json new file mode 100644 index 000000000..6569a8326 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8196-counted-loops-integer-starts.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8196-counted-loops-integer-starts.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8210-require-computed-missing/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8210-require-computed-missing/main.ts.json new file mode 100644 index 000000000..659ee27aa --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8210-require-computed-missing/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8210-require-computed-missing/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8211-require-resolve-computed/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8211-require-resolve-computed/main.ts.json new file mode 100644 index 000000000..44f6cb75f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8211-require-resolve-computed/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8211-require-resolve-computed/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8212-native-helper-probe-no-display/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8212-native-helper-probe-no-display/main.ts.json new file mode 100644 index 000000000..2a83db362 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8212-native-helper-probe-no-display/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8212-native-helper-probe-no-display/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8213-cycle-star-reexport/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8213-cycle-star-reexport/main.ts.json new file mode 100644 index 000000000..c22b59997 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8213-cycle-star-reexport/main.ts.json @@ -0,0 +1,9 @@ +{ + "order": [ + "/tests/corpus/8213-cycle-star-reexport/services.ts", + "/tests/corpus/8213-cycle-star-reexport/runtime.ts", + "/tests/corpus/8213-cycle-star-reexport/sdk.ts", + "/tests/corpus/8213-cycle-star-reexport/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8214-unshimmed-package-version/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8214-unshimmed-package-version/main.ts.json new file mode 100644 index 000000000..6a6a885dc --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8214-unshimmed-package-version/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8214-unshimmed-package-version/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8215-js-any-logical-module-bindings/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8215-js-any-logical-module-bindings/main.ts.json new file mode 100644 index 000000000..0ae9f6f2b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8215-js-any-logical-module-bindings/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8215-js-any-logical-module-bindings/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8216-module-namespace-live-values/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8216-module-namespace-live-values/main.ts.json new file mode 100644 index 000000000..d856e307e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8216-module-namespace-live-values/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8216-module-namespace-live-values/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8217-intl-segmenter-word-construct/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8217-intl-segmenter-word-construct/main.ts.json new file mode 100644 index 000000000..d2ad44948 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8217-intl-segmenter-word-construct/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8217-intl-segmenter-word-construct/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8218-cjs-namespace-imports/main.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8218-cjs-namespace-imports/main.mjs.json new file mode 100644 index 000000000..d627f9153 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8218-cjs-namespace-imports/main.mjs.json @@ -0,0 +1,9 @@ +{ + "order": [ + "/tests/corpus/8218-cjs-namespace-imports/table.cjs", + "/tests/corpus/8218-cjs-namespace-imports/client/transport.js", + "/tests/corpus/8218-cjs-namespace-imports/client/index.mjs", + "/tests/corpus/8218-cjs-namespace-imports/main.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8219-cjs-stable-var-exports/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8219-cjs-stable-var-exports/main.js.json new file mode 100644 index 000000000..71e59739d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8219-cjs-stable-var-exports/main.js.json @@ -0,0 +1,9 @@ +{ + "order": [ + "/tests/corpus/8219-cjs-stable-var-exports/lib/core.js", + "/tests/corpus/8219-cjs-stable-var-exports/lib/index.js", + "/tests/corpus/8219-cjs-stable-var-exports/lib/options.js", + "/tests/corpus/8219-cjs-stable-var-exports/main.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8220-js-class-static-destructuring/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8220-js-class-static-destructuring/main.js.json new file mode 100644 index 000000000..7a2ede696 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8220-js-class-static-destructuring/main.js.json @@ -0,0 +1,9 @@ +{ + "order": [ + "/tests/corpus/8220-js-class-static-destructuring/classes/comparator.js", + "/tests/corpus/8220-js-class-static-destructuring/ranges/outside.js", + "/tests/corpus/8220-js-class-static-destructuring/formdata.js", + "/tests/corpus/8220-js-class-static-destructuring/main.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8221-js-recursive-generator-returns/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8221-js-recursive-generator-returns/main.ts.json new file mode 100644 index 000000000..003137231 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8221-js-recursive-generator-returns/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8221-js-recursive-generator-returns/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8222-js-class-static-reflection.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8222-js-class-static-reflection.js.json new file mode 100644 index 000000000..2e961ad08 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8222-js-class-static-reflection.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8222-js-class-static-reflection.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8223-argument-count-destructured-parameter.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8223-argument-count-destructured-parameter.ts.json new file mode 100644 index 000000000..6cae202d3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8223-argument-count-destructured-parameter.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8223-argument-count-destructured-parameter.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8230-function-record-dispatch.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8230-function-record-dispatch.ts.json new file mode 100644 index 000000000..1670b97b4 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8230-function-record-dispatch.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8230-function-record-dispatch.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8230-readline-interface-constructor.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8230-readline-interface-constructor.cjs.json new file mode 100644 index 000000000..05573974c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8230-readline-interface-constructor.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8230-readline-interface-constructor.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8231-builtin-member-binding-new.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8231-builtin-member-binding-new.cjs.json new file mode 100644 index 000000000..1f54f4886 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8231-builtin-member-binding-new.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8231-builtin-member-binding-new.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8231-function-table-shapes.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8231-function-table-shapes.ts.json new file mode 100644 index 000000000..8f1963ef5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8231-function-table-shapes.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8231-function-table-shapes.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8232-native-streams-untyped.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8232-native-streams-untyped.cjs.json new file mode 100644 index 000000000..2b07cde52 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8232-native-streams-untyped.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8232-native-streams-untyped.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8232-optional-function-values.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8232-optional-function-values.ts.json new file mode 100644 index 000000000..671364bc6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8232-optional-function-values.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8232-optional-function-values.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8233-string-decoder-values.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8233-string-decoder-values.cjs.json new file mode 100644 index 000000000..a734fb48c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8233-string-decoder-values.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8233-string-decoder-values.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8234-handle-typeof.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8234-handle-typeof.ts.json new file mode 100644 index 000000000..7f37dbc5d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8234-handle-typeof.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8234-handle-typeof.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8240-asserted-callee-errors.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8240-asserted-callee-errors.ts.json new file mode 100644 index 000000000..365bf7076 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8240-asserted-callee-errors.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8240-asserted-callee-errors.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8241-literal-result-slices.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8241-literal-result-slices.ts.json new file mode 100644 index 000000000..9cd832dca --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8241-literal-result-slices.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8241-literal-result-slices.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8242-string-scan-republish.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8242-string-scan-republish.ts.json new file mode 100644 index 000000000..78a4f52e6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8242-string-scan-republish.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8242-string-scan-republish.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8243-json-parse-discriminated-unions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8243-json-parse-discriminated-unions.ts.json new file mode 100644 index 000000000..96ef2084c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8243-json-parse-discriminated-unions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8243-json-parse-discriminated-unions.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8244-speculative-floor-mod-powers.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8244-speculative-floor-mod-powers.ts.json new file mode 100644 index 000000000..65e7ccb38 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8244-speculative-floor-mod-powers.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8244-speculative-floor-mod-powers.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8260-class-prototype-views.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8260-class-prototype-views.mjs.json new file mode 100644 index 000000000..6cf42e773 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8260-class-prototype-views.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8260-class-prototype-views.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8270-fs-promises-member.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8270-fs-promises-member.cjs.json new file mode 100644 index 000000000..eb3938040 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8270-fs-promises-member.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8270-fs-promises-member.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8271-cjs-top-level-return.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8271-cjs-top-level-return.cjs.json new file mode 100644 index 000000000..2fcecbe67 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8271-cjs-top-level-return.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8271-cjs-top-level-return.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8272-fs-stream-call-without-new.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8272-fs-stream-call-without-new.cjs.json new file mode 100644 index 000000000..7fdb114e4 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8272-fs-stream-call-without-new.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8272-fs-stream-call-without-new.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8273-fs-read-write-argument-checks.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8273-fs-read-write-argument-checks.cjs.json new file mode 100644 index 000000000..60e64d369 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8273-fs-read-write-argument-checks.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8273-fs-read-write-argument-checks.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8290-child-channel-members/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8290-child-channel-members/main.ts.json new file mode 100644 index 000000000..3519c4e6c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8290-child-channel-members/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8290-child-channel-members/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8291-frozen-record-union-keys.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8291-frozen-record-union-keys.ts.json new file mode 100644 index 000000000..3dcadeb0a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8291-frozen-record-union-keys.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8291-frozen-record-union-keys.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8300-native-array-capsule-reads/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8300-native-array-capsule-reads/main.ts.json new file mode 100644 index 000000000..fb6e3a0bf --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8300-native-array-capsule-reads/main.ts.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/8300-native-array-capsule-reads/lib.cjs", + "/tests/corpus/8300-native-array-capsule-reads/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8302-class-super-method-values/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8302-class-super-method-values/main.ts.json new file mode 100644 index 000000000..24fe2decc --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8302-class-super-method-values/main.ts.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/8302-class-super-method-values/lib.cjs", + "/tests/corpus/8302-class-super-method-values/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8320-assign-only-deferred-binding/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8320-assign-only-deferred-binding/main.ts.json new file mode 100644 index 000000000..399baa239 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8320-assign-only-deferred-binding/main.ts.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/8320-assign-only-deferred-binding/scanner.ts", + "/tests/corpus/8320-assign-only-deferred-binding/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8321-typed-array-constructor-kind.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8321-typed-array-constructor-kind.js.json new file mode 100644 index 000000000..6da151987 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8321-typed-array-constructor-kind.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8321-typed-array-constructor-kind.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8326-never-promise-instanceof.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8326-never-promise-instanceof.ts.json new file mode 100644 index 000000000..d10bc2c18 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8326-never-promise-instanceof.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8326-never-promise-instanceof.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8330-optional-element-undefined-receivers.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8330-optional-element-undefined-receivers.ts.json new file mode 100644 index 000000000..ef78d4391 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8330-optional-element-undefined-receivers.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8330-optional-element-undefined-receivers.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8331-map-number-hashed-integer-keys.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8331-map-number-hashed-integer-keys.ts.json new file mode 100644 index 000000000..86822b641 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8331-map-number-hashed-integer-keys.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8331-map-number-hashed-integer-keys.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8332-worker-shared-counted-loops.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8332-worker-shared-counted-loops.ts.json new file mode 100644 index 000000000..57308033d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8332-worker-shared-counted-loops.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8332-worker-shared-counted-loops.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8333-array-subclass-view-writes.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8333-array-subclass-view-writes.ts.json new file mode 100644 index 000000000..fc6fd819d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8333-array-subclass-view-writes.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8333-array-subclass-view-writes.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8334-array-subclass-inspect-depth.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8334-array-subclass-inspect-depth.ts.json new file mode 100644 index 000000000..c41447b5e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8334-array-subclass-inspect-depth.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8334-array-subclass-inspect-depth.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8344-readline-chunk-splitting.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8344-readline-chunk-splitting.ts.json new file mode 100644 index 000000000..8c01e6b47 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8344-readline-chunk-splitting.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8344-readline-chunk-splitting.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8345-replace-callback-untyped-params.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8345-replace-callback-untyped-params.ts.json new file mode 100644 index 000000000..9a4c2b510 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8345-replace-callback-untyped-params.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8345-replace-callback-untyped-params.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8346-replace-callback-untyped-params.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8346-replace-callback-untyped-params.mjs.json new file mode 100644 index 000000000..301203783 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8346-replace-callback-untyped-params.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8346-replace-callback-untyped-params.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8347-string-iteration-code-points.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8347-string-iteration-code-points.ts.json new file mode 100644 index 000000000..1e83f7b6f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8347-string-iteration-code-points.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8347-string-iteration-code-points.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8348-string-iteration-worker-characters.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8348-string-iteration-worker-characters.ts.json new file mode 100644 index 000000000..e767eaf65 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8348-string-iteration-worker-characters.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8348-string-iteration-worker-characters.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8350-worker-shared-loop-bounds.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8350-worker-shared-loop-bounds.ts.json new file mode 100644 index 000000000..db4376635 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8350-worker-shared-loop-bounds.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8350-worker-shared-loop-bounds.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8351-regex-literal-replacers.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8351-regex-literal-replacers.ts.json new file mode 100644 index 000000000..be0149287 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8351-regex-literal-replacers.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8351-regex-literal-replacers.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8355-row-pair-columns.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8355-row-pair-columns.ts.json new file mode 100644 index 000000000..900de6fb0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8355-row-pair-columns.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8355-row-pair-columns.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8356-shared-view-loop-versions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8356-shared-view-loop-versions.ts.json new file mode 100644 index 000000000..76cd99065 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8356-shared-view-loop-versions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8356-shared-view-loop-versions.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8360-record-prototype-method-calls.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8360-record-prototype-method-calls.ts.json new file mode 100644 index 000000000..2bbaaaa88 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8360-record-prototype-method-calls.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8360-record-prototype-method-calls.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8361-record-function-table-inherited.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8361-record-function-table-inherited.ts.json new file mode 100644 index 000000000..a9d8bf4b0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8361-record-function-table-inherited.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8361-record-function-table-inherited.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8362-record-inherited-order.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8362-record-inherited-order.ts.json new file mode 100644 index 000000000..e080ae4d1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8362-record-inherited-order.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8362-record-inherited-order.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8400-iterative-teardown.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8400-iterative-teardown.ts.json new file mode 100644 index 000000000..80ad38bdf --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8400-iterative-teardown.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8400-iterative-teardown.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/absent-property-reads.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/absent-property-reads.mjs.json new file mode 100644 index 000000000..3c0646fa9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/absent-property-reads.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/absent-property-reads.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/array-prototype-call-native-arrays.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/array-prototype-call-native-arrays.ts.json new file mode 100644 index 000000000..2904322c5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/array-prototype-call-native-arrays.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/array-prototype-call-native-arrays.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/boxed-record-identity.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/boxed-record-identity.ts.json new file mode 100644 index 000000000..759521f3e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/boxed-record-identity.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/boxed-record-identity.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/checked-union-record-assertions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/checked-union-record-assertions.ts.json new file mode 100644 index 000000000..ff77d1a95 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/checked-union-record-assertions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/checked-union-record-assertions.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-abort-controller-fields.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-abort-controller-fields.ts.json new file mode 100644 index 000000000..80466c474 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-abort-controller-fields.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/class-abort-controller-fields.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-brand-probe-keys.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-brand-probe-keys.ts.json new file mode 100644 index 000000000..fd60e3ff9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-brand-probe-keys.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/class-brand-probe-keys.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/constant-choice-chains.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/constant-choice-chains.ts.json new file mode 100644 index 000000000..059a21156 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/constant-choice-chains.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/constant-choice-chains.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/dyn-cast-identity-fresh.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/dyn-cast-identity-fresh.ts.json new file mode 100644 index 000000000..e1643bcab --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/dyn-cast-identity-fresh.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/dyn-cast-identity-fresh.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/dyn-cast-identity-holders.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/dyn-cast-identity-holders.ts.json new file mode 100644 index 000000000..e1a06ecc5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/dyn-cast-identity-holders.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/dyn-cast-identity-holders.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/dyn-cast-identity-workers.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/dyn-cast-identity-workers.ts.json new file mode 100644 index 000000000..e8ecb066e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/dyn-cast-identity-workers.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/dyn-cast-identity-workers.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/dyn-cast-identity.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/dyn-cast-identity.ts.json new file mode 100644 index 000000000..fdf360d21 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/dyn-cast-identity.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/dyn-cast-identity.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/dyn-literal-identity.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/dyn-literal-identity.ts.json new file mode 100644 index 000000000..41fde2c81 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/dyn-literal-identity.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/dyn-literal-identity.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/dyn-union-function-arms.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/dyn-union-function-arms.ts.json new file mode 100644 index 000000000..80675e5e6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/dyn-union-function-arms.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/dyn-union-function-arms.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/empty-mapped-alias-types.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/empty-mapped-alias-types.ts.json new file mode 100644 index 000000000..08ec4c0f3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/empty-mapped-alias-types.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/empty-mapped-alias-types.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/evolving-let-shorthand-reads.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/evolving-let-shorthand-reads.js.json new file mode 100644 index 000000000..26d07a9f9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/evolving-let-shorthand-reads.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/evolving-let-shorthand-reads.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fetch-headers-copy-cache.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fetch-headers-copy-cache.ts.json new file mode 100644 index 000000000..bd69651b1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fetch-headers-copy-cache.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/fetch-headers-copy-cache.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fs-readdir-recursive.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fs-readdir-recursive.ts.json new file mode 100644 index 000000000..c045eadf1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fs-readdir-recursive.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/fs-readdir-recursive.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/generic-alias-cast-instances.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/generic-alias-cast-instances.ts.json new file mode 100644 index 000000000..a229ee862 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/generic-alias-cast-instances.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/generic-alias-cast-instances.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/generic-interface-void-calls.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/generic-interface-void-calls.ts.json new file mode 100644 index 000000000..a5c69b714 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/generic-interface-void-calls.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/generic-interface-void-calls.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/generic-member-lambda-impls.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/generic-member-lambda-impls.ts.json new file mode 100644 index 000000000..bc87c6ea0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/generic-member-lambda-impls.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/generic-member-lambda-impls.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/instanceof-primitive-wrappers-typed.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/instanceof-primitive-wrappers-typed.ts.json new file mode 100644 index 000000000..fba491f13 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/instanceof-primitive-wrappers-typed.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/instanceof-primitive-wrappers-typed.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/instanceof-primitive-wrappers.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/instanceof-primitive-wrappers.mjs.json new file mode 100644 index 000000000..e2dddae48 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/instanceof-primitive-wrappers.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/instanceof-primitive-wrappers.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/intersection-restated-members.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/intersection-restated-members.ts.json new file mode 100644 index 000000000..4560cb4ae --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/intersection-restated-members.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/intersection-restated-members.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/localized-failure-cycles.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/localized-failure-cycles.ts.json new file mode 100644 index 000000000..7df4fd38f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/localized-failure-cycles.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/localized-failure-cycles.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/localized-failure-declarations.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/localized-failure-declarations.ts.json new file mode 100644 index 000000000..0530e8e13 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/localized-failure-declarations.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/localized-failure-declarations.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/localized-failure-forward-capture.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/localized-failure-forward-capture.ts.json new file mode 100644 index 000000000..f77570166 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/localized-failure-forward-capture.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/localized-failure-forward-capture.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/localized-failure-function-slots.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/localized-failure-function-slots.ts.json new file mode 100644 index 000000000..88890074d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/localized-failure-function-slots.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/localized-failure-function-slots.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/localized-failure-held-values.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/localized-failure-held-values.ts.json new file mode 100644 index 000000000..2327ce301 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/localized-failure-held-values.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/localized-failure-held-values.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/localized-failure-locals.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/localized-failure-locals.ts.json new file mode 100644 index 000000000..9a45d0613 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/localized-failure-locals.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/localized-failure-locals.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/localized-failure-pi-shapes.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/localized-failure-pi-shapes.ts.json new file mode 100644 index 000000000..d1603f029 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/localized-failure-pi-shapes.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/localized-failure-pi-shapes.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/localized-failure-registered-statics.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/localized-failure-registered-statics.ts.json new file mode 100644 index 000000000..b3d375ea6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/localized-failure-registered-statics.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/localized-failure-registered-statics.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/method-signature-any-params.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/method-signature-any-params.ts.json new file mode 100644 index 000000000..b3f7ed165 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/method-signature-any-params.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/method-signature-any-params.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/native-array-dynamic-reads/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/native-array-dynamic-reads/main.ts.json new file mode 100644 index 000000000..c3b3013ea --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/native-array-dynamic-reads/main.ts.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/native-array-dynamic-reads/reads.js", + "/tests/corpus/native-array-dynamic-reads/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/native-worker-length-loop-reassign.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/native-worker-length-loop-reassign.ts.json new file mode 100644 index 000000000..f5230bace --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/native-worker-length-loop-reassign.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/native-worker-length-loop-reassign.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/native-worker-terminate-bounded-loops.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/native-worker-terminate-bounded-loops.ts.json new file mode 100644 index 000000000..2e52a519c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/native-worker-terminate-bounded-loops.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/native-worker-terminate-bounded-loops.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/native-worker-terminate-counted-loops.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/native-worker-terminate-counted-loops.ts.json new file mode 100644 index 000000000..5f7c54075 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/native-worker-terminate-counted-loops.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/native-worker-terminate-counted-loops.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/net-unix-socket-connect-default-import.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/net-unix-socket-connect-default-import.mjs.json new file mode 100644 index 000000000..a1bb1df19 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/net-unix-socket-connect-default-import.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/net-unix-socket-connect-default-import.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/net-unix-socket-connect.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/net-unix-socket-connect.ts.json new file mode 100644 index 000000000..7bf0a928d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/net-unix-socket-connect.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/net-unix-socket-connect.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/never-narrowed-literal-argument.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/never-narrowed-literal-argument.ts.json new file mode 100644 index 000000000..55b7b5d8a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/never-narrowed-literal-argument.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/never-narrowed-literal-argument.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/object-literal-members.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/object-literal-members.mjs.json new file mode 100644 index 000000000..f1f9017ac --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/object-literal-members.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/object-literal-members.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/recursive-generic-method-members.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/recursive-generic-method-members.ts.json new file mode 100644 index 000000000..6c3590ab8 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/recursive-generic-method-members.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/recursive-generic-method-members.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/recursive-optional-callback-params.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/recursive-optional-callback-params.ts.json new file mode 100644 index 000000000..abf783f85 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/recursive-optional-callback-params.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/recursive-optional-callback-params.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/rest-tuple-shorthand-property.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/rest-tuple-shorthand-property.ts.json new file mode 100644 index 000000000..ca2d5289c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/rest-tuple-shorthand-property.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/rest-tuple-shorthand-property.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/speculative-loop-view-headers.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/speculative-loop-view-headers.ts.json new file mode 100644 index 000000000..9fcddc195 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/speculative-loop-view-headers.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/speculative-loop-view-headers.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/split-cjs-require-binding/main.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/split-cjs-require-binding/main.cjs.json new file mode 100644 index 000000000..60b9b0736 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/split-cjs-require-binding/main.cjs.json @@ -0,0 +1,12 @@ +{ + "order": [ + "/tests/corpus/split-cjs-require-binding/lib.cjs", + "/tests/corpus/split-cjs-require-binding/whole.cjs", + "/tests/corpus/split-cjs-require-binding/reexport.cjs", + "/tests/corpus/split-cjs-require-binding/reexport-extra.cjs", + "/tests/corpus/split-cjs-require-binding/chain.cjs", + "/tests/corpus/split-cjs-require-binding/chain-wrap.cjs", + "/tests/corpus/split-cjs-require-binding/main.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/split-cjs-require-cycle/main.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/split-cjs-require-cycle/main.cjs.json new file mode 100644 index 000000000..0ea3366d3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/split-cjs-require-cycle/main.cjs.json @@ -0,0 +1,8 @@ +{ + "order": [ + "/tests/corpus/split-cjs-require-cycle/odd.cjs", + "/tests/corpus/split-cjs-require-cycle/even.cjs", + "/tests/corpus/split-cjs-require-cycle/main.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/string-last-index-of-ascii.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/string-last-index-of-ascii.ts.json new file mode 100644 index 000000000..1d3107cd5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/string-last-index-of-ascii.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/string-last-index-of-ascii.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/string-plus-element-read-return.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/string-plus-element-read-return.ts.json new file mode 100644 index 000000000..1e78a1802 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/string-plus-element-read-return.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/string-plus-element-read-return.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/symbol-keyed-recursive-record.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/symbol-keyed-recursive-record.ts.json new file mode 100644 index 000000000..e46feaab2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/symbol-keyed-recursive-record.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/symbol-keyed-recursive-record.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/symbol-property-key-reuse.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/symbol-property-key-reuse.mjs.json new file mode 100644 index 000000000..e8fb5f353 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/symbol-property-key-reuse.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/symbol-property-key-reuse.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/text-decoder-undefined-input.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/text-decoder-undefined-input.ts.json new file mode 100644 index 000000000..a1f5f49d9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/text-decoder-undefined-input.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/text-decoder-undefined-input.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/typed-array-length-after-release.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/typed-array-length-after-release.ts.json new file mode 100644 index 000000000..ca6fcb551 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/typed-array-length-after-release.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/typed-array-length-after-release.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/typed-array-masked-index-stores.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/typed-array-masked-index-stores.ts.json new file mode 100644 index 000000000..dde5a96fa --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/typed-array-masked-index-stores.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/typed-array-masked-index-stores.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/typeof-ambient-undefined-names.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/typeof-ambient-undefined-names.ts.json new file mode 100644 index 000000000..a2bd8dc1c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/typeof-ambient-undefined-names.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/typeof-ambient-undefined-names.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/undeclared-key-optional-chain/main.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/undeclared-key-optional-chain/main.cjs.json new file mode 100644 index 000000000..252e5eb99 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/undeclared-key-optional-chain/main.cjs.json @@ -0,0 +1,18 @@ +{ + "order": [ + "/tests/corpus/undeclared-key-optional-chain/main.cjs" + ], + "diags": [ + { + "code": "SC1010", + "message": "the 'hostilekit' module is not supported yet", + "loc": { + "file": "/tests/corpus/undeclared-key-optional-chain/main.cjs", + "start": 289, + "end": 329 + }, + "milestone": "M4", + "hint": "relative imports (./file, ../dir/file), package.json-mediated project imports (#alias via the imports field, self-name references via exports), statically compiled npm packages, and the built-in fs, fs/promises, path, os, url, crypto, zlib, child_process, net, http, tls, https, http2, dgram, dns, dns/promises, util, util/types, string_decoder, querystring, readline, events, stream, stream/promises, stream/consumers, stream/web, buffer, assert, assert/strict, worker_threads, cluster, tty, async_hooks, timers, timers/promises, diagnostics_channel, perf_hooks, module, v8, and console modules (bare or node:-prefixed) and node:test (node:-prefixed only, like in Node) are supported" + } + ] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/union-clone-empty-array-overrides.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/union-clone-empty-array-overrides.ts.json new file mode 100644 index 000000000..cb11c5098 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/union-clone-empty-array-overrides.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/union-clone-empty-array-overrides.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/union-promise-function-arm-widening.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/union-promise-function-arm-widening.ts.json new file mode 100644 index 000000000..91c268992 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/union-promise-function-arm-widening.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/union-promise-function-arm-widening.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/union-record-kept-fields-arm.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/union-record-kept-fields-arm.ts.json new file mode 100644 index 000000000..a29a9d99a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/union-record-kept-fields-arm.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/union-record-kept-fields-arm.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/union-record-kept-fields-no-discriminant.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/union-record-kept-fields-no-discriminant.ts.json new file mode 100644 index 000000000..f40acc997 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/union-record-kept-fields-no-discriminant.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/union-record-kept-fields-no-discriminant.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/union-record-lossless-arm.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/union-record-lossless-arm.ts.json new file mode 100644 index 000000000..143cda7d0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/union-record-lossless-arm.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/union-record-lossless-arm.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/union-tuple-element-read.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/union-tuple-element-read.ts.json new file mode 100644 index 000000000..bf23de474 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/union-tuple-element-read.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/union-tuple-element-read.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/var-object-filled-later.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/var-object-filled-later.js.json new file mode 100644 index 000000000..26329147f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/var-object-filled-later.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/var-object-filled-later.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/weak-keys-freed-reused.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/weak-keys-freed-reused.mjs.json new file mode 100644 index 000000000..ab4ca8bda --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/weak-keys-freed-reused.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/weak-keys-freed-reused.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/any-destructure-unspelled-keys.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/any-destructure-unspelled-keys.ts.json new file mode 100644 index 000000000..f38d71f47 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/any-destructure-unspelled-keys.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/any-destructure-unspelled-keys.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/array-subclass-fences.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/array-subclass-fences.ts.json new file mode 100644 index 000000000..659372fdb --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/array-subclass-fences.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/array-subclass-fences.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/array-subclass-retyped-members.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/array-subclass-retyped-members.ts.json new file mode 100644 index 000000000..3e5dd12c9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/array-subclass-retyped-members.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/array-subclass-retyped-members.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/date-union-fences.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/date-union-fences.ts.json deleted file mode 100644 index cf6ad7398..000000000 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/date-union-fences.ts.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "order": [ - "/tests/diagnostics/date-union-fences.ts" - ], - "diags": [] -} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/function-value-arguments-length.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/function-value-arguments-length.ts.json new file mode 100644 index 000000000..6aa894918 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/function-value-arguments-length.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/function-value-arguments-length.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/globalthis-buffer-alias-values.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/globalthis-buffer-alias-values.ts.json new file mode 100644 index 000000000..4f6fe0c8c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/globalthis-buffer-alias-values.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/globalthis-buffer-alias-values.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/inspect-custom-fences.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/inspect-custom-fences.ts.json new file mode 100644 index 000000000..dff7b7d6b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/inspect-custom-fences.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/inspect-custom-fences.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/node-islands-config/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/node-islands-config/main.ts.json deleted file mode 100644 index 6b86e7f9a..000000000 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/node-islands-config/main.ts.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "order": [ - "/tests/diagnostics/node-islands-config/main.ts" - ], - "diags": [] -} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/promise-all-generic-mixed-union.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/promise-all-generic-mixed-union.ts.json new file mode 100644 index 000000000..2732b9539 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/promise-all-generic-mixed-union.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/promise-all-generic-mixed-union.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/split-cjs-require-cycle/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/split-cjs-require-cycle/main.js.json new file mode 100644 index 000000000..90d4a2353 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/split-cjs-require-cycle/main.js.json @@ -0,0 +1,19 @@ +{ + "order": [ + "/tests/diagnostics/split-cjs-require-cycle/b.js", + "/tests/diagnostics/split-cjs-require-cycle/main.js" + ], + "diags": [ + { + "code": "SC1016", + "message": "circular imports (/tests/diagnostics/split-cjs-require-cycle/main.js → /tests/diagnostics/split-cjs-require-cycle/b.js → /tests/diagnostics/split-cjs-require-cycle/main.js; the cycle closes through a require() edge: /tests/diagnostics/split-cjs-require-cycle/main.js uses its exports at /tests/diagnostics/split-cjs-require-cycle/main.js:10 outside the export statements that precede its first require, so a module of the cycle can observe them before they settle) are not supported yet", + "loc": { + "file": "/tests/diagnostics/split-cjs-require-cycle/main.js", + "start": 0, + "end": 0 + }, + "milestone": "later", + "hint": "ES-module cycles of let/const/function bindings compile as-is; rewrite the named construct (a named import instead of a namespace, a const instead of a default-export expression or var, a class declared before the code that can reach it) or break the named edge" + } + ] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/split-cjs-require-early/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/split-cjs-require-early/main.js.json new file mode 100644 index 000000000..acb338494 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/split-cjs-require-early/main.js.json @@ -0,0 +1,27 @@ +{ + "order": [ + "/tests/diagnostics/split-cjs-require-early/main.js" + ], + "diags": [ + { + "code": "SC1013", + "message": "require() binding 'lib' assigned below code that can reach it (Node reads undefined before the assignment runs, which is not modeled — move the assignment above the code that can run first) is not supported yet", + "loc": { + "file": "/tests/diagnostics/split-cjs-require-early/main.js", + "start": 396, + "end": 415 + }, + "milestone": "later" + }, + { + "code": "SC1013", + "message": "require() binding 'wrapped' assigned below code that can reach it (Node reads undefined before the assignment runs, which is not modeled — move the assignment above the code that can run first) is not supported yet", + "loc": { + "file": "/tests/diagnostics/split-cjs-require-early/main.js", + "start": 530, + "end": 549 + }, + "milestone": "later" + } + ] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/untyped-map-into-interface.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/untyped-map-into-interface.ts.json new file mode 100644 index 000000000..1d39d436f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/untyped-map-into-interface.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/untyped-map-into-interface.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/x509-fences.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/x509-fences.ts.json new file mode 100644 index 000000000..e2b83a7c2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/x509-fences.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/x509-fences.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/diffie-hellman.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/diffie-hellman.ts.json new file mode 100644 index 000000000..fb8a7a740 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/diffie-hellman.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/node-types/diffie-hellman.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/fs-url-fallible.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/fs-url-fallible.ts.json new file mode 100644 index 000000000..e4967ea93 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/fs-url-fallible.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/node-types/fs-url-fallible.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/fs-url-path-fences.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/fs-url-path-fences.ts.json deleted file mode 100644 index 7b63a5564..000000000 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/fs-url-path-fences.ts.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "order": [ - "/tests/fixtures/node-types/fs-url-path-fences.ts" - ], - "diags": [] -} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/tls-client-certificates.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/tls-client-certificates.ts.json new file mode 100644 index 000000000..449e0f3ac --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/tls-client-certificates.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/node-types/tls-client-certificates.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/tls-peer-certificates.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/tls-peer-certificates.ts.json new file mode 100644 index 000000000..c5f8ed40a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/tls-peer-certificates.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/node-types/tls-peer-certificates.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/tls-server-contexts.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/tls-server-contexts.ts.json new file mode 100644 index 000000000..73e294ea2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/tls-server-contexts.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/node-types/tls-server-contexts.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/x509-methods.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/x509-methods.ts.json new file mode 100644 index 000000000..c42f8f9cc --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/x509-methods.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/node-types/x509-methods.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hono-zod-default-hook/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hono-zod-default-hook/main.ts.json new file mode 100644 index 000000000..2310d6e0f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hono-zod-default-hook/main.ts.json @@ -0,0 +1,61 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/hono-zod-default-hook/main.ts" + ], + "diags": [ + { + "code": "SC0001", + "message": "Cannot find name 'WebSocket'.", + "loc": { + "file": "/tests/fixtures/npm/node_modules/hono/dist/types/client/types.d.ts", + "start": 1398, + "end": 1407 + } + }, + { + "code": "SC0001", + "message": "Cannot find name 'WebSocket'.", + "loc": { + "file": "/tests/fixtures/npm/node_modules/hono/dist/types/client/types.d.ts", + "start": 1413, + "end": 1422 + } + }, + { + "code": "SC0001", + "message": "Cannot find name 'WebSocket'.", + "loc": { + "file": "/tests/fixtures/npm/node_modules/hono/dist/types/client/types.d.ts", + "start": 3716, + "end": 3725 + } + }, + { + "code": "SC0001", + "message": "Cannot find name 'FormData'.", + "loc": { + "file": "/tests/fixtures/npm/node_modules/hono/dist/types/client/types.d.ts", + "start": 4798, + "end": 4806 + } + }, + { + "code": "SC0001", + "message": "Cannot find name 'FormData'.", + "loc": { + "file": "/tests/fixtures/npm/node_modules/hono/dist/types/request.d.ts", + "start": 549, + "end": 557 + } + }, + { + "code": "SC0001", + "message": "Cannot find name 'FormData'.", + "loc": { + "file": "/tests/fixtures/npm/node_modules/hono/dist/types/request.d.ts", + "start": 5572, + "end": 5580 + } + } + ] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hono-zod-validator/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hono-zod-validator/main.ts.json new file mode 100644 index 000000000..72c2578e6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hono-zod-validator/main.ts.json @@ -0,0 +1,61 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/hono-zod-validator/main.ts" + ], + "diags": [ + { + "code": "SC0001", + "message": "Cannot find name 'WebSocket'.", + "loc": { + "file": "/tests/fixtures/npm/node_modules/hono/dist/types/client/types.d.ts", + "start": 1398, + "end": 1407 + } + }, + { + "code": "SC0001", + "message": "Cannot find name 'WebSocket'.", + "loc": { + "file": "/tests/fixtures/npm/node_modules/hono/dist/types/client/types.d.ts", + "start": 1413, + "end": 1422 + } + }, + { + "code": "SC0001", + "message": "Cannot find name 'WebSocket'.", + "loc": { + "file": "/tests/fixtures/npm/node_modules/hono/dist/types/client/types.d.ts", + "start": 3716, + "end": 3725 + } + }, + { + "code": "SC0001", + "message": "Cannot find name 'FormData'.", + "loc": { + "file": "/tests/fixtures/npm/node_modules/hono/dist/types/client/types.d.ts", + "start": 4798, + "end": 4806 + } + }, + { + "code": "SC0001", + "message": "Cannot find name 'FormData'.", + "loc": { + "file": "/tests/fixtures/npm/node_modules/hono/dist/types/request.d.ts", + "start": 549, + "end": 557 + } + }, + { + "code": "SC0001", + "message": "Cannot find name 'FormData'.", + "loc": { + "file": "/tests/fixtures/npm/node_modules/hono/dist/types/request.d.ts", + "start": 5572, + "end": 5580 + } + } + ] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/legacy-namespaces/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/legacy-namespaces/main.ts.json new file mode 100644 index 000000000..7f50545d6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/legacy-namespaces/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/legacy-namespaces/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/shape-overlay-facts/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/shape-overlay-facts/main.ts.json new file mode 100644 index 000000000..f01de4ae5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/shape-overlay-facts/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/shape-overlay-facts/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/type-patch-dts/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/type-patch-dts/main.ts.json new file mode 100644 index 000000000..c9a8e15fc --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/type-patch-dts/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/type-patch-dts/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/type-patch-files/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/type-patch-files/main.ts.json new file mode 100644 index 000000000..4a15a1975 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/type-patch-files/main.ts.json @@ -0,0 +1,25 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/type-patch-files/main.ts" + ], + "diags": [ + { + "code": "SC0001", + "message": "Property 'toUpperCase' does not exist on type 'number'.", + "loc": { + "file": "/tests/fixtures/npm/cases/type-patch-files/main.ts", + "start": 346, + "end": 357 + } + }, + { + "code": "SC0001", + "message": "This comparison appears to be unintentional because the types 'number' and 'string' have no overlap.", + "loc": { + "file": "/tests/fixtures/npm/cases/type-patch-files/main.ts", + "start": 429, + "end": 447 + } + } + ] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/type-patch-types-entry/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/type-patch-types-entry/main.ts.json new file mode 100644 index 000000000..bc1622724 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/type-patch-types-entry/main.ts.json @@ -0,0 +1,16 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/type-patch-types-entry/main.ts" + ], + "diags": [ + { + "code": "SC0001", + "message": "Could not find a declaration file for module 'pointjs'. '/tests/fixtures/npm/cases/type-patch-types-entry/node_modules/pointjs/lib/point.js' implicitly has an 'any' type.\n Try `npm i --save-dev @types/pointjs` if it exists or add a new declaration (.d.ts) file containing `declare module 'pointjs';`", + "loc": { + "file": "/tests/fixtures/npm/cases/type-patch-types-entry/main.ts", + "start": 246, + "end": 255 + } + } + ] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/type-patch-types-twin/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/type-patch-types-twin/main.ts.json new file mode 100644 index 000000000..faa48f074 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/type-patch-types-twin/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/type-patch-types-twin/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/types-keyed-construction/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/types-keyed-construction/main.ts.json new file mode 100644 index 000000000..5817aa040 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/types-keyed-construction/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/types-keyed-construction/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/types-undeclared-quoted-key/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/types-undeclared-quoted-key/main.ts.json new file mode 100644 index 000000000..8c56898f2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/types-undeclared-quoted-key/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/types-undeclared-quoted-key/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/var-export-redeclared/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/var-export-redeclared/main.ts.json new file mode 100644 index 000000000..ccc1a3653 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/var-export-redeclared/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/var-export-redeclared/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/zod4-boundary/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/zod4-boundary/main.ts.json new file mode 100644 index 000000000..0d656e939 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/zod4-boundary/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/zod4-boundary/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/zod4-record/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/zod4-record/main.ts.json new file mode 100644 index 000000000..8c7700ac8 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/zod4-record/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/zod4-record/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/bench.test.ts b/packages/compiler/test/ts7/bench.test.ts index 13e34bca1..c8de220c9 100644 --- a/packages/compiler/test/ts7/bench.test.ts +++ b/packages/compiler/test/ts7/bench.test.ts @@ -28,6 +28,8 @@ import type { } from "../../src/frontend/ts7/semantic-types.js"; import { CheckerFacade } from "../../src/frontend/ts7/checker.js"; import { ambientDtsPath, fallbackDtsPath, overridesDtsPath } from "../../src/frontend/program.js"; +import { fallbackModulesNamedIn } from "../../src/frontend/dts-paths.js"; +import { readFileSync } from "node:fs"; import { ad, options7 } from "./harness.js"; const repoRoot = fileURLToPath(new URL("../../../..", import.meta.url)); @@ -230,7 +232,13 @@ for (const workload of WORKLOADS) { { retry: 2 }, () => { const program = createProgram( - [workload.entry, ambientDtsPath(), fallbackDtsPath(), overridesDtsPath()], + [ + workload.entry, + ambientDtsPath(), + fallbackDtsPath(), + ...fallbackModulesNamedIn(readFileSync(workload.entry, "utf8")), + overridesDtsPath(), + ], options7(), host, ); diff --git a/packages/compiler/test/ts7/harness.ts b/packages/compiler/test/ts7/harness.ts index 484add55d..073de1421 100644 --- a/packages/compiler/test/ts7/harness.ts +++ b/packages/compiler/test/ts7/harness.ts @@ -18,6 +18,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import ts5 from "typescript5"; import { ambientDtsPath, fallbackDtsPath } from "../../src/frontend/program.js"; +import { fallbackModulesNamedIn } from "../../src/frontend/dts-paths.js"; import * as ad from "../../src/frontend/ts7/adapter.js"; export { ad, ts5 }; @@ -78,7 +79,8 @@ export function buildTwoWorlds(sources: Record, host?: ad.Ts7Hos writeFileSync(path, text); files.push(path); } - const roots = [...files, ambientDtsPath(), fallbackDtsPath()]; + const modules = Object.values(sources).flatMap((text) => fallbackModulesNamedIn(text)); + const roots = [...files, ambientDtsPath(), fallbackDtsPath(), ...new Set(modules)]; const p5 = ts5.createProgram(roots, options5()); const p7 = createProgram(roots, options7(), host); return { diff --git a/packages/compiler/test/type-patches-command.test.ts b/packages/compiler/test/type-patches-command.test.ts new file mode 100644 index 000000000..2ac8e1c9d --- /dev/null +++ b/packages/compiler/test/type-patches-command.test.ts @@ -0,0 +1,198 @@ +/* `scriptc types patch` and `scriptc types list` + * (frontend/type-patches/command.ts) on a copy of the type patch fixture: + * matches are shown before anything is written, the patch lands in the + * nearest package.json under a range covering the installed version (or + * an existing entry that covers it), and nothing is written when the + * selector matches nothing or the build already uses the requested type. */ +import { cpSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeEach, expect, test } from "vitest"; +import { nodeFrontendServices } from "../src/frontend/services-node.js"; +import { runTypePatchCommand } from "../src/frontend/type-patches/command.js"; + +const fixture = join(import.meta.dirname, "../../../tests/fixtures/type-patches"); +const parse = (path: string, text: string, kind: "ts" | "js") => + nodeFrontendServices().parse(path, text, kind); +let root: string; + +beforeEach(() => { + root = mkdtempSync(join(tmpdir(), "scriptc-types-command-")); + cpSync(fixture, root, { recursive: true }); + writeFileSync( + join(root, "package.json"), + JSON.stringify({ name: "app", private: true, type: "module" }, null, 2) + "\n", + ); +}); +afterEach(() => rmSync(root, { recursive: true, force: true })); + +const patch = (pkg: string, selector: string, type: string, extra: object = {}) => + runTypePatchCommand( + { action: "patch", cwd: root, pkg, selector, type, range: null, dryRun: false, ...extra }, + parse, + ); + +test("patch shows the matches and writes the entry", () => { + const result = patch("termctx", "OutputContext.width", "| undefined"); + expect(result).toEqual({ + stdout: + "termctx 1.2.0 (node_modules/termctx)\n" + + " index.js:24 number -> number | undefined\n" + + `wrote package.json scriptc.typePatches["termctx@^1.2.0"].declarations["OutputContext.width"]\n`, + stderr: "", + exitCode: 0, + }); + expect(JSON.parse(readFileSync(join(root, "package.json"), "utf8"))).toEqual({ + name: "app", + private: true, + type: "module", + scriptc: { + typePatches: { + "termctx@^1.2.0": { declarations: { "OutputContext.width": "| undefined" } }, + }, + }, + }); + // A second patch joins the entry whose range covers the installed copy. + expect(patch("termctx", "Terminal.columns()", "| undefined").exitCode).toBe(0); + const text = readFileSync(join(root, "package.json"), "utf8"); + expect(text.endsWith("}\n")).toBe(true); + expect(JSON.parse(text).scriptc.typePatches).toEqual({ + "termctx@^1.2.0": { + declarations: { "OutputContext.width": "| undefined", "Terminal.columns()": "| undefined" }, + }, + }); +}); + +test("--range and --dry-run", () => { + const dry = patch("ms", "index.js:parse()", "| undefined", { dryRun: true, range: "2.1.x" }); + expect(dry.stdout).toBe( + "ms 2.1.3 (node_modules/ms)\n" + + " index.js:44 Number -> Number | undefined\n" + + `would write package.json scriptc.typePatches["ms@2.1.x"].declarations["index.js:parse()"]\n`, + ); + expect(JSON.parse(readFileSync(join(root, "package.json"), "utf8")).scriptc).toBeUndefined(); +}); + +test("nothing is written for a selector that matches nothing or is already so", () => { + const missing = patch("termctx", "OutputContext.height", "| undefined"); + expect(missing.exitCode).toBe(1); + expect(missing.stderr).toBe( + "scriptc: 'OutputContext.height' matches no declaration\nnothing was written\n", + ); + const redundant = patch("ms", "index.js:fmtShort(ms)", "| Number"); + expect(redundant).toMatchObject({ exitCode: 0 }); + expect(redundant.stdout).toContain("the declarations already say this; nothing was written\n"); + expect(patch("absent-package", "A.b", "| undefined").stderr).toBe( + "scriptc: absent-package is not installed here\n", + ); + expect(patch("termctx", "1bad", "| undefined").stderr).toContain("is not a selector"); + expect(JSON.parse(readFileSync(join(root, "package.json"), "utf8")).scriptc).toBeUndefined(); +}); + +test("a type the declarations already say replaces a covering patch that says another", () => { + const manifest = join(root, "package.json"); + writeFileSync( + manifest, + JSON.stringify( + { + name: "app", + scriptc: { + typePatches: { + "termctx@^1.2.0": { declarations: { "OutputContext.width": "| undefined" } }, + }, + }, + }, + null, + 2, + ) + "\n", + ); + const result = patch("termctx", "OutputContext.width", "number"); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain( + `wrote package.json scriptc.typePatches["termctx@^1.2.0"].declarations["OutputContext.width"]\n`, + ); + expect(JSON.parse(readFileSync(manifest, "utf8")).scriptc.typePatches).toEqual({ + "termctx@^1.2.0": { declarations: { "OutputContext.width": "number" } }, + }); + expect(patch("termctx", "OutputContext.width", "number").stdout).toContain( + "the declarations already say this; nothing was written\n", + ); +}); + +test("list shows each patch and what it matches", () => { + writeFileSync(join(root, "package.json"), readFileSync(join(fixture, "package.json"))); + const listed = runTypePatchCommand({ action: "list", cwd: root }, parse); + expect(listed.stdout).toBe( + [ + "termctx@^1.0.0", + " OutputContext.width: | undefined", + " node_modules/termctx/index.js:24 number -> number | undefined", + " Terminal.columns(): | undefined", + " node_modules/termctx/index.js:14 number -> number | undefined", + "ms@^2.1.0", + " index.js:parse(): | undefined", + " node_modules/ms/index.js:44 Number -> Number | undefined", + " export=(): | undefined", + " node_modules/ms/index.js:22 String|Number -> String|Number | undefined", + "", + ].join("\n"), + ); + writeFileSync(join(root, "stale.json"), ""); + writeFileSync( + join(root, "package.json"), + JSON.stringify({ + scriptc: { typePatches: { "termctx@^2.0.0": { declarations: { "A.b": "| undefined" } } } }, + }), + ); + expect(runTypePatchCommand({ action: "list", cwd: root }, parse).stdout).toBe( + "termctx@^2.0.0\n does not apply to the installed termctx 1.2.0\n A.b: | undefined\n", + ); +}); + +test("nothing is written when a replacement is ambiguous in one of the files it matches", () => { + const pkg = join(root, "node_modules/overloads"); + mkdirSync(pkg, { recursive: true }); + writeFileSync( + join(pkg, "package.json"), + JSON.stringify({ name: "overloads", version: "1.0.0", types: "index.d.ts" }), + ); + writeFileSync(join(pkg, "index.d.ts"), "export declare function pick(a: string): string;\n"); + writeFileSync( + join(pkg, "more.d.ts"), + "export declare function pick(a: string): string;\nexport declare function pick(a: number): string;\n", + ); + const result = patch("overloads", "pick()", "string | undefined"); + expect(result.exitCode).toBe(1); + expect(result.stderr).toBe( + "scriptc: 'pick()' matches 2 declarations in more.d.ts; a replacement must name one " + + "(widen them, or replace the declaration file) (more.d.ts)\nnothing was written\n", + ); + expect(JSON.parse(readFileSync(join(root, "package.json"), "utf8")).scriptc).toBeUndefined(); +}); + +test("the patch goes to the covering entry the compiler applies", () => { + const manifest = join(root, "package.json"); + const entries = { + "termctx@^1.0.0": { declarations: { "OutputContext.width": "| null" } }, + "termctx@1.x": { declarations: { "OutputContext.width": "| null" } }, + }; + writeFileSync(manifest, JSON.stringify({ name: "app", scriptc: { typePatches: entries } })); + // Both ranges cover termctx 1.2.0, and the later entry wins on the same + // selector: it takes the patch. + const result = patch("termctx", "OutputContext.width", "| undefined"); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain( + `wrote package.json scriptc.typePatches["termctx@1.x"].declarations["OutputContext.width"]\n`, + ); + expect(JSON.parse(readFileSync(manifest, "utf8")).scriptc.typePatches).toEqual({ + "termctx@^1.0.0": { declarations: { "OutputContext.width": "| null" } }, + "termctx@1.x": { declarations: { "OutputContext.width": "| undefined" } }, + }); + // Asked for the earlier range, the later entry would still win. + const earlier = patch("termctx", "OutputContext.width", "| undefined", { range: "^1.0.0" }); + expect(earlier.exitCode).toBe(1); + expect(earlier.stderr).toBe( + `scriptc: scriptc.typePatches["termctx@1.x"] also patches 'OutputContext.width' for termctx 1.2.0 ` + + `and takes precedence over scriptc.typePatches["termctx@^1.0.0"]\nnothing was written\n`, + ); +}); diff --git a/packages/compiler/test/type-patches.test.ts b/packages/compiler/test/type-patches.test.ts new file mode 100644 index 000000000..5b586fb01 --- /dev/null +++ b/packages/compiler/test/type-patches.test.ts @@ -0,0 +1,300 @@ +/* The type patch layer (frontend/type-patches/layer.ts) against real + * packages: commander's HelpContext.helpWidth @typedef property and ms's + * @return tags, patched from the fixtures' package.json; installed-version + * selection; and the warnings for patches that do not apply as written. */ +import { mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, expect, test } from "vitest"; +import { nodeFrontendServices } from "../src/frontend/services-node.js"; +import { + configureTypePatches, + typePatchDiagnostics, + typePatchedSpans, + typePatchedText, + typePatchFileExists, + typePatchMatches, + typePatchSuppliesTypes, +} from "../src/frontend/type-patches/layer.js"; + +const fixtures = join(import.meta.dirname, "../../../tests/fixtures"); +const parse = (path: string, text: string, kind: "ts" | "js") => + nodeFrontendServices().parse(path, text, kind); + +afterEach(() => configureTypePatches(join(tmpdir(), "none", "main.ts"), parse)); + +test("commander's HelpContext.helpWidth typedef property reads as number | undefined", () => { + const root = join(fixtures, "commander-calc"); + configureTypePatches(join(root, "calc.ts"), parse); + const command = join(root, "node_modules/commander/lib/command.js"); + const text = typePatchedText(command); + expect(text).toBeDefined(); + expect(text).toContain(" * @property {number | undefined} helpWidth\n"); + expect(text).toContain(" * @returns {HelpContext}\n"); + expect(typePatchMatches()).toMatchObject([ + { + file: command, + line: 2481, + before: "number", + after: "number | undefined", + pkg: "commander", + version: "15.0.0", + }, + ]); + const [span] = typePatchedSpans(command); + expect(text!.slice(span!.start, span!.end)).toBe(" | undefined"); + // Its declaration file has an unrelated HelpContext without helpWidth. + expect(typePatchedText(join(root, "node_modules/commander/typings/index.d.ts"))).toBeUndefined(); + // Other files of the package are served as they are. + expect(typePatchedText(join(root, "node_modules/commander/lib/help.js"))).toBeUndefined(); + expect(typePatchDiagnostics()?.diagnostics).toEqual([]); +}); + +test("ms's parse() and export return tags widen", () => { + const root = join(fixtures, "type-patches"); + configureTypePatches(join(root, "ms.ts"), parse); + const file = join(root, "node_modules/ms/index.js"); + const text = typePatchedText(file)!; + const original = readFileSync(file, "utf8"); + expect(text).toBe( + original + .replace(" * @return {String|Number}\n", " * @return {String|Number | undefined}\n") + .replace( + " * @return {Number}\n * @api private\n */\n\nfunction parse", + " * @return {Number | undefined}\n * @api private\n */\n\nfunction parse", + ), + ); + expect(text).not.toBe(original); + expect(typePatchDiagnostics()?.diagnostics.map((d) => d.code)).toEqual([]); +}); + +test("a project without type patches serves every file unchanged", () => { + configureTypePatches(join(fixtures, "npm-static", "ms-cli.ts"), parse); + expect(typePatchedText(join(fixtures, "npm-static/node_modules/ms/index.js"))).toBeUndefined(); + expect(typePatchDiagnostics()).toBeNull(); +}); + +test("patches that do not apply as written warn", () => { + const root = join(fixtures, "type-patches"); + configureTypePatches(join(root, "stale", "main.ts"), parse); + expect(typePatchedText(join(root, "node_modules/termctx/index.js"))).toBeUndefined(); + typePatchedText(join(root, "node_modules/ms/index.js")); + const diags = typePatchDiagnostics()!.diagnostics; + expect(diags.map((d) => [d.code, d.message])).toEqual([ + [ + "SC8030", + "type patch 'termctx@^2.0.0' does not apply to termctx 1.2.0 at ../node_modules/termctx; its declarations are used unpatched", + ], + [ + "SC8031", + "type patch 'ms@^2.1.0': 'index.js:format()' matches no declaration in the files of ms this program loads", + ], + ["SC8032", "type patch 'ms@^2.1.0': 'index.js:fmtShort(ms)' is already declared as Number"], + [ + "SC8033", + "type patch 'ms@^2.1.0': 'index.js:plural()' has no written type to widen; give the whole type instead (index.js)", + ], + ]); + expect( + diags.every((d) => d.loc.file === join(root, "stale", "package.json") && d.loc.start > 0), + ).toBe(true); +}); + +test("an @types twin is matched by the runtime package's installed version", () => { + const root = mkdtempSync(join(tmpdir(), "scriptc-type-patches-")); + try { + const twin = join(fixtures, "npm/cases/type-patch-types-twin"); + configureTypePatches(join(twin, "main.ts"), parse); + const text = typePatchedText(join(twin, "node_modules/@types/tallyjs/index.d.ts"))!; + expect(text).toContain(" max: number | undefined;\n mean: number | null;\n"); + // A range the installed runtime (0.4.2) does not satisfy leaves it alone, + // even though the twin's own version (0.4.0) would. + writeFileSync( + join(root, "package.json"), + JSON.stringify({ + scriptc: { + typePatches: { "tallyjs@0.4.0": { declarations: { "Totals.max": "| undefined" } } }, + }, + }), + ); + configureTypePatches(join(root, "main.ts"), parse); + expect(typePatchedText(join(twin, "node_modules/@types/tallyjs/index.d.ts"))).toBeUndefined(); + expect(typePatchDiagnostics()!.diagnostics.map((d) => d.code)).toEqual(["SC8030"]); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + +test("an @types twin in pnpm's layout is matched by the runtime package the project resolves", () => { + // pnpm keeps each package in its own directory under node_modules/.pnpm + // and links the project's dependencies from node_modules: the twin has no + // runtime package beside it. + const root = mkdtempSync(join(tmpdir(), "scriptc-type-patches-pnpm-")); + try { + const store = join(root, "node_modules/.pnpm"); + const runtime = join(store, "tallyjs@0.5.0/node_modules/tallyjs"); + const types = join(store, "@types+tallyjs@0.4.0/node_modules/@types/tallyjs"); + mkdirSync(runtime, { recursive: true }); + mkdirSync(types, { recursive: true }); + writeFileSync(join(runtime, "package.json"), '{ "name": "tallyjs", "version": "0.5.0" }'); + writeFileSync(join(runtime, "index.js"), "exports.totals = () => ({});\n"); + writeFileSync(join(types, "package.json"), '{ "name": "@types/tallyjs", "version": "0.4.0" }'); + writeFileSync( + join(types, "index.d.ts"), + "export type Totals = {\n max: number;\n};\nexport declare function totals(): Totals;\n", + ); + mkdirSync(join(root, "node_modules/@types"), { recursive: true }); + symlinkSync(runtime, join(root, "node_modules/tallyjs")); + symlinkSync(types, join(root, "node_modules/@types/tallyjs")); + const patchFor = (range: string) => + writeFileSync( + join(root, "package.json"), + JSON.stringify({ + scriptc: { + typePatches: { + [`tallyjs@${range}`]: { declarations: { "Totals.max": "| undefined" } }, + }, + }, + }), + ); + // A range the runtime (0.5.0) satisfies applies, though the twin's own + // version (0.4.0) does not satisfy it. + patchFor("0.5.0"); + configureTypePatches(join(root, "main.ts"), parse); + expect(typePatchedText(join(types, "index.d.ts"))).toContain("max: number | undefined;"); + // A range only the twin's own version satisfies does not. + patchFor("0.4.0"); + configureTypePatches(join(root, "main.ts"), parse); + expect(typePatchedText(join(types, "index.d.ts"))).toBeUndefined(); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + +test("an export before its hoisted function is patched, and string text that looks like JSDoc is not", () => { + const root = mkdtempSync(join(tmpdir(), "scriptc-type-patches-js-")); + try { + const pkg = join(root, "node_modules/hoistkit"); + mkdirSync(pkg, { recursive: true }); + writeFileSync(join(pkg, "package.json"), '{ "name": "hoistkit", "version": "1.0.0" }'); + const source = [ + '"use strict";', + "exports.measure = size;", + 'exports.doc = "/** @typedef {object} Opts\\n * @property {number} width */";', + "", + "/**", + " * @returns {number}", + " */", + "function size(s) {", + " return s.length || undefined;", + "}", + "", + ].join("\n"); + writeFileSync(join(pkg, "index.js"), source); + writeFileSync( + join(root, "package.json"), + JSON.stringify({ + scriptc: { + typePatches: { + "hoistkit@^1.0.0": { + declarations: { "measure()": "| undefined", "Opts.width": "| undefined" }, + }, + }, + }, + }), + ); + configureTypePatches(join(root, "main.ts"), parse); + // The export, under another name, comes before the function it exports: + // the function's @returns tag widens. + expect(typePatchedText(join(pkg, "index.js"))).toBe( + source.replace(" * @returns {number}\n", " * @returns {number | undefined}\n"), + ); + // The string's text is a value, not a typedef: the patch finds nothing. + expect( + typePatchDiagnostics()! + .diagnostics.map((d) => d.message) + .join("\n"), + ).toContain("Opts.width"); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + +test("files replaces a declaration file and declarations still apply on top", () => { + const root = join(fixtures, "npm/cases/type-patch-files"); + configureTypePatches(join(root, "main.ts"), parse); + const entry = typePatchedText(join(root, "node_modules/eventlog/dist/entry.d.ts")); + expect(entry).toBe(readFileSync(join(root, "type-patches/entry.d.ts"), "utf8")); + expect(typePatchedText(join(root, "node_modules/eventlog/dist/index.d.ts"))).toContain( + "{ count: number; last: string | undefined }", + ); + expect(typePatchDiagnostics()!.diagnostics).toEqual([]); +}); + +test("types gives a package without declarations an entry beside its main", () => { + const root = join(fixtures, "npm/cases/type-patch-types-entry"); + configureTypePatches(join(root, "main.ts"), parse); + const pkg = join(root, "node_modules/pointjs"); + expect(typePatchFileExists(join(pkg, "lib/point.d.ts"))).toBe(true); + expect(typePatchFileExists(join(pkg, "index.d.ts"))).toBe(false); + expect(typePatchFileExists(join(pkg, "lib/other.d.ts"))).toBe(false); + expect(typePatchSuppliesTypes(pkg)).toBe(true); + expect(typePatchedText(join(pkg, "lib/point.d.ts"))).toBe( + readFileSync(join(root, "type-patches/pointjs.d.ts"), "utf8"), + ); + // Without patches nothing exists that is not on disk. + configureTypePatches(join(tmpdir(), "none", "main.ts"), parse); + expect(typePatchFileExists(join(pkg, "lib/point.d.ts"))).toBe(false); + expect(typePatchSuppliesTypes(pkg)).toBe(false); +}); + +test("types gives a package whose main is a .jsx file an entry beside it", () => { + const root = mkdtempSync(join(tmpdir(), "scriptc-type-patches-")); + try { + const pkg = join(root, "node_modules/viewjs"); + mkdirSync(join(pkg, "lib"), { recursive: true }); + writeFileSync( + join(pkg, "package.json"), + JSON.stringify({ name: "viewjs", version: "1.0.0", main: "lib/view.jsx" }), + ); + writeFileSync(join(pkg, "lib/view.jsx"), "export const view = 1;\n"); + writeFileSync(join(root, "viewjs.d.ts"), "export declare const view: number;\n"); + writeFileSync( + join(root, "package.json"), + JSON.stringify({ scriptc: { typePatches: { "viewjs@^1.0.0": { types: "./viewjs.d.ts" } } } }), + ); + configureTypePatches(join(root, "main.ts"), parse); + expect(typePatchFileExists(join(pkg, "lib/view.d.ts"))).toBe(true); + expect(typePatchSuppliesTypes(pkg)).toBe(true); + expect(typePatchedText(join(pkg, "lib/view.d.ts"))).toBe( + "export declare const view: number;\n", + ); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + +test("a files target the installed package does not ship warns", () => { + const root = mkdtempSync(join(tmpdir(), "scriptc-type-patches-")); + try { + const twin = join(fixtures, "npm/cases/type-patch-files"); + writeFileSync(join(root, "local.d.ts"), "export type Entry = {};\n"); + writeFileSync( + join(root, "package.json"), + JSON.stringify({ + scriptc: { + typePatches: { "eventlog@^2.0.0": { files: { "dist/gone.d.ts": "./local.d.ts" } } }, + }, + }), + ); + configureTypePatches(join(root, "main.ts"), parse); + expect(typePatchedText(join(twin, "node_modules/eventlog/dist/index.d.ts"))).toBeUndefined(); + const diags = typePatchDiagnostics()!.diagnostics; + expect(diags.map((d) => d.code)).toEqual(["SC8031"]); + expect(diags[0]!.message).toMatch( + /^type patch 'eventlog@\^2\.0\.0': dist\/gone\.d\.ts is not a file of eventlog at .*type-patch-files\/node_modules\/eventlog$/, + ); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); diff --git a/packages/compiler/test/typed-array-alias-scope-emission.test.ts b/packages/compiler/test/typed-array-alias-scope-emission.test.ts new file mode 100644 index 000000000..c94b58d59 --- /dev/null +++ b/packages/compiler/test/typed-array-alias-scope-emission.test.ts @@ -0,0 +1,64 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile, deserializeModule } from "../src/index.js"; +import { emitLlvmModule } from "../src/backend/llvm/emitter.js"; + +test("a typed-array loop version proves its views disjoint and scopes their accesses", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-alias-scopes-")); + try { + const entry = join(dir, "main.ts"); + const outPath = join(dir, "main.ir.json"); + await writeFile( + entry, + ` +function copy(dst: Float64Array, src: Float64Array, n: number): void { + for (let i = 0; i < n; i++) dst[i] = src[i]! + 1; +} +function scale(xs: Float64Array, n: number): void { + for (let i = 0; i < n; i++) xs[i] = xs[i]! * 2; +} +function dot(a: Float64Array, b: Float64Array, n: number): number { + let sum = 0; + for (let i = 0; i < n; i++) sum += a[i]! * b[i]!; + return sum; +} +const a = new Float64Array(8), b = new Float64Array(8); +copy(a, b, 8); +scale(a, 8); +console.log(dot(a, b, 8)); +`, + ); + const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" }); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + const mod = deserializeModule(await readFile(outPath, "utf8")); + const ll = emitLlvmModule(mod); + const fn = (name: string): string => + new RegExp(`^define internal [^\\n]*@sc_bf_${name}(?:\\.i32)?\\([^]*?^}`, "m").exec(ll)![0]; + // Two views, one written: the guard compares their byte ranges, and + // each view's accesses in the fast copy carry its own scope. + const copy = fn("copy"); + expect(copy).toMatch(/ptrtoint ptr %\w+ to i64/); + expect(copy).toMatch(/icmp ule i64 %\w+, %\w+/); + expect(copy).toMatch( + /load double, ptr %\w+, align 1[^\n]*!alias\.scope !\{!\{!"copy\.0\.\d", !\{!"scriptc typed-array views"\}\}\}, !noalias/, + ); + expect(copy).toMatch(/store double %\w+, ptr %\w+, align 1[^\n]*!alias\.scope/); + // The fast copy declares both scopes where it starts, so they hold for + // this entry only, and declares nothing else. + for (const receiver of [0, 1]) + expect(copy).toContain( + `call void @llvm.experimental.noalias.scope.decl(metadata !{!{!"copy.0.${receiver}", !{!"scriptc typed-array views"}}})`, + ); + expect(copy.match(/noalias\.scope\.decl/g)).toHaveLength(2); + // One view, or views that are only read, need no scopes. + for (const name of ["scale", "dot"]) { + expect(fn(name)).not.toContain("!alias.scope"); + expect(fn(name)).not.toContain("ptrtoint"); + } + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); diff --git a/packages/compiler/test/typed-array-union-emission.test.ts b/packages/compiler/test/typed-array-union-emission.test.ts new file mode 100644 index 000000000..e9bb22103 --- /dev/null +++ b/packages/compiler/test/typed-array-union-emission.test.ts @@ -0,0 +1,116 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile } from "../src/index.js"; + +async function emitLlvm(source: string): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-typed-array-unions-")); + try { + const entry = join(dir, "main.ts"); + const outPath = join(dir, "main.ll"); + await writeFile(entry, source); + const result = await compile(entry, { outDir: dir, outPath, outputKind: "llvm" }); + expect(result.ok, JSON.stringify(result.diagnostics)).toBe(true); + return await readFile(outPath, "utf8"); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +function body(llvm: string, name: string): string { + return new RegExp(`^define [^@]*@sc_b?f_${name}(\\.i32)?\\([^]*?^}`, "m").exec(llvm)?.[0] ?? ""; +} + +const source = ` +type UintArray = Uint8Array | Uint16Array | Uint32Array; +const pick = (max: number, n: number): UintArray => + max <= 256 ? new Uint8Array(n) : max <= 65536 ? new Uint16Array(n) : new Uint32Array(n); +function total(xs: UintArray): number { + let s = 0; + for (let i = 0; i < xs.length; i++) s += xs[i]!; + return s; +} +function first(max: number): number { + return pick(max, 4)[0]!; +} +const xs = pick(Number(process.argv.length) * 100000, 8); +for (let i = 0; i < xs.length; i++) xs[i] = i; +console.log(total(xs), first(10)); +`; + +test("element reads of a union of typed arrays load each arm's own element kind in place", async () => { + const llvm = await emitLlvm(source); + const total = body(llvm, "total"); + // One load per arm, each of its element width, behind the union's tag. + for (const width of ["i8", "i16", "i32"]) expect(total).toMatch(new RegExp(`load ${width}, ptr`)); + expect(total).not.toContain("@sc_bf__x25_union_getNumber"); + // The loop over the union's length keeps an integer counter. + expect(total).toMatch(/alloca i64/); +}); + +test("a typed-array union read through a computed receiver goes through one helper", async () => { + const llvm = await emitLlvm(source); + // The receiver is a call: evaluated once, then dispatched by the helper. + expect(body(llvm, "first")).toContain("@sc_bf__x25_union_getNumber"); +}); + +const stores = ` +type Wide = Uint8Array | Uint16Array; +const pick = (wide: boolean): Wide => (wide ? new Uint16Array(4) : new Uint8Array(4)); +function fill(xs: Wide): void { + for (let i = 0; i < xs.length; i++) xs[i] = i + 1; +} +function rebind(): void { + let u = pick(false); + let i = 0; + const swap = (): number => { + u = pick(true); + i = 2; + return 3; + }; + u[i] = swap(); + console.log(u, i); +} +fill(pick(process.argv.length > 9)); +rebind(); +`; + +test("a typed-array union store runs in place unless its value can rebind the receiver or index", async () => { + const llvm = await emitLlvm(stores); + expect(body(llvm, "fill")).not.toContain("@sc_bf__x25_union_idxSet"); + // The call may assign the captured receiver and index, which the store + // read before it: the helper evaluates them first. + expect(body(llvm, "rebind")).toContain("@sc_bf__x25_union_idxSet"); +}); + +test("typed-array methods on a union with a Buffer arm need a narrowing first", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-typed-array-unions-")); + try { + const entry = join(dir, "main.ts"); + // The Buffer arm is found by its type, also through an alias name. + for (const [declaration, method] of [ + ["const u: Buffer | Uint16Array", "slice(1)"], + ["type Bytes = Uint16Array | Buffer;\nconst u: Bytes", "slice(1)"], + ["type Bytes = Uint16Array | Buffer;\nconst u: Bytes", "fill(1)"], + ["type Bytes = Uint16Array | Buffer;\nconst u: Bytes", "indexOf(1)"], + ]) { + await writeFile( + entry, + `import { Buffer } from "node:buffer"; +${declaration} = process.argv.length > 9 ? new Uint16Array(2) : Buffer.alloc(2); +console.log(u.${method}); +`, + ); + const result = await compile(entry, { + outDir: dir, + outPath: join(dir, "main.ll"), + outputKind: "llvm", + }); + expect(result.ok, `${declaration} ${method}`).toBe(false); + expect(JSON.stringify(result.diagnostics)).toContain(method.slice(0, method.indexOf("("))); + } + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); diff --git a/packages/compiler/test/untyped-push-emission.test.ts b/packages/compiler/test/untyped-push-emission.test.ts new file mode 100644 index 000000000..2abdf7f41 --- /dev/null +++ b/packages/compiler/test/untyped-push-emission.test.ts @@ -0,0 +1,42 @@ +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile, deserializeModule, validateModule } from "../src/index.js"; +import { emitLlvmModule } from "../src/backend/llvm/emitter.js"; + +// An untyped push in statement position drops the new length, so it calls +// the entry that does not box it; a push whose result is read keeps the +// ordinary entry, as do the other methods. +test("a statement-position untyped push skips boxing its result", async () => { + const dir = mkdtempSync(join(tmpdir(), "scriptc-untyped-push-")); + try { + const entry = join(dir, "main.mjs"); + const output = join(dir, "main.ir.json"); + writeFileSync( + entry, + `function fill(a, n) { + for (let i = 0; i < n; i++) a.push(i); + const length = a.push(n); + a.pop(); + return length; +} +console.log(fill(JSON.parse("[]"), 3)); +`, + ); + const lowered = await compile(entry, { outputKind: "ir", outDir: dir, outPath: output }); + if (!lowered.ok) throw new Error(JSON.stringify(lowered.diagnostics)); + const module = deserializeModule(readFileSync(output, "utf8")); + expect(validateModule(module)).toEqual([]); + const llvm = emitLlvmModule(module, {}); + const body = llvm.split(/\n(?=define )/).find((fn) => /@sc_[a-z]*f_fill\b/.test(fn))!; + const discarded = [...body.matchAll(/call void @scr_dyn_invoke_discard_id\(/g)]; + const kept = [...body.matchAll(/= call ptr @scr_dyn_invoke_prepared_id\(/g)]; + expect(discarded).toHaveLength(1); + // `a.push(n)` for its length, and `a.pop()`, whose result is dropped + // but which has no unboxed entry. + expect(kept).toHaveLength(2); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +}); diff --git a/packages/compiler/test/while-bytes-bounds-emission.test.ts b/packages/compiler/test/while-bytes-bounds-emission.test.ts new file mode 100644 index 000000000..a2604eed1 --- /dev/null +++ b/packages/compiler/test/while-bytes-bounds-emission.test.ts @@ -0,0 +1,150 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile } from "../src/index.js"; + +async function emitLlvm(source: string): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-while-bounds-")); + try { + const entry = join(dir, "main.ts"); + const outPath = join(dir, "main.ll"); + await writeFile(entry, source); + const result = await compile(entry, { + outDir: dir, + outPath, + outputKind: "llvm", + }); + expect(result.ok, JSON.stringify(result.diagnostics)).toBe(true); + return await readFile(outPath, "utf8"); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +/** The number of bounds-checked element reads and writes in a function's + * statement body (the borrowed-argument body when there is one). */ +function checks(ll: string, name: string): number { + const definition = (prefix: string): number => + ll.search(new RegExp(`define [^\\n]*@${prefix}_${name}\\(`)); + const start = definition("sc_bf") >= 0 ? definition("sc_bf") : definition("sc_f"); + expect(start).toBeGreaterThanOrEqual(0); + const fn = ll.slice(start, ll.indexOf("\n}\n", start)); + return fn.split("label %bytes.index.valid").length - 1; +} + +test("reads from the end of a typed array in a while loop are proven in bounds", async () => { + const ll = await emitLlvm(` +function code(c: number): number { + switch (c) { + case 65: return 84; + case 84: return 65; + } + return 0; +} +function reversed(line: Uint8Array): number { + const len = line.length; + const right = len - 1; + let i = 0; + let total = 0; + while (i < len) { + let c = line[right - i]!; + i++; + if (c === 10) { + if (i === len) break; + c = line[right - i]!; + i++; + } + total += code(c); + } + return total; +} +function pairs(values: Uint16Array): number { + let i = 0; + let total = 0; + while (values.length > i) { + i += 2; + total += values[i - 2]!; + if (i < values.length) total += values[values.length - 1 - i]!; + } + return total; +} +console.log(reversed(new Uint8Array([65, 10, 84])), pairs(new Uint16Array([1, 2, 3]))); +`); + expect(checks(ll, "reversed")).toBe(0); + expect(checks(ll, "pairs")).toBe(0); +}); + +test("reads a while loop does not keep in bounds keep their checks", async () => { + const ll = await emitLlvm(` +function inclusive(line: Uint8Array): number { + const len = line.length; + let i = 0; + let total = 0; + while (i <= len) { + total += line[len - i]!; + i++; + } + return total; +} +function unguarded(line: Uint8Array): number { + const right = line.length - 1; + let i = 0; + let total = 0; + while (i < line.length) { + total += line[right - i]!; + i++; + total += line[right - i]!; + } + return total; +} +function ahead(values: Uint16Array): number { + let i = 0; + let total = 0; + while (i < values.length) { + i += 2; + total += values[i - 1]!; + } + return total; +} +function nested(line: Uint8Array): number { + const len = line.length; + let i = 0; + let total = 0; + while (i < len) { + const c = line[len - 1 - i]!; + i++; + while (i < len && line[len - 1 - i]! === c) i++; + total += line[len - 1 - i]!; + } + return total; +} +function comma(line: Uint8Array): number { + const len = line.length; + let i = 0; + let total = 0; + while (i < len) { + if ((i++, i > 0)) total += line[len - 1 - i]!; + total += line[len - 1 - i]!; + } + return total; +} +console.log( + comma(new Uint8Array(2)), + inclusive(new Uint8Array(2)), + unguarded(new Uint8Array(2)), + ahead(new Uint16Array(3)), + nested(new Uint8Array(3)), +); +`); + // The increment in the comma expression is not a statement of the + // body, so the loop has no bound and both reads keep their checks. + expect(checks(ll, "comma")).toBe(2); + expect(checks(ll, "inclusive")).toBe(1); + // The read before the increment is in bounds; the one after it is not. + expect(checks(ll, "unguarded")).toBe(1); + expect(checks(ll, "ahead")).toBe(1); + // The read before the nested loop is in bounds. The nested loop's + // condition is not a bound, and the read after the loop can be at -1. + expect(checks(ll, "nested")).toBe(2); +}); diff --git a/packages/compiler/test/while-induction-emission.test.ts b/packages/compiler/test/while-induction-emission.test.ts new file mode 100644 index 000000000..08fbab849 --- /dev/null +++ b/packages/compiler/test/while-induction-emission.test.ts @@ -0,0 +1,94 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile } from "../src/index.js"; + +async function emitLlvm(source: string): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-while-induction-")); + try { + const entry = join(dir, "main.ts"); + const outPath = join(dir, "main.ll"); + await writeFile(entry, source); + const result = await compile(entry, { + outDir: dir, + outPath, + outputKind: "llvm", + }); + expect(result.ok, JSON.stringify(result.diagnostics)).toBe(true); + return await readFile(outPath, "utf8"); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +/** The definition holding the function's statements: the borrowed-argument + * body when there is one, otherwise the function itself. */ +function body(ll: string, name: string): string { + const definition = (prefix: string): number => + ll.search(new RegExp(`define [^\\n]*@${prefix}_${name}\\(`)); + const start = definition("sc_bf") >= 0 ? definition("sc_bf") : definition("sc_f"); + expect(start).toBeGreaterThanOrEqual(0); + return ll.slice(start, ll.indexOf("\n}\n", start)); +} + +test("a while loop bounded by a byte length keeps its counter and companions in integers", async () => { + const ll = await emitLlvm(` +function reversed(line: Uint8Array, target: Uint8Array): number { + const len = line.length; + let read = 0; + let written = 0; + let lines = 0; + while (read < len) { + const c = line[len - 1 - read]!; + read++; + if (c === 10) { + if (read === len) break; + read++; + } + target[written] = c; + written++; + if (written % 60 === 0) { + target[written] = 10; + written++; + lines++; + } + } + return written + lines; +} +console.log(reversed(new Uint8Array([65, 10, 66]), new Uint8Array(8))); +`); + const fn = body(ll, "reversed"); + expect(fn).toContain("alloca i64 ; integer read"); + expect(fn).toContain("alloca i64 ; integer written"); + expect(fn).toContain("alloca i64 ; integer lines"); +}); + +test("a companion a nested loop advances, or a counter a continue can skip, stays a double", async () => { + const ll = await emitLlvm(` +function nested(line: Uint8Array): number { + let read = 0; + let total = 0; + while (read < line.length) { + read++; + for (let j = 0; j < 2; j++) total++; + } + return total; +} +function skipped(line: Uint8Array): number { + let read = 0; + let total = 0; + while (read < line.length) { + if (line[read] === 0) continue; + read++; + total++; + } + return total; +} +console.log(nested(new Uint8Array(3)), skipped(new Uint8Array([1, 2]))); +`); + expect(body(ll, "nested")).not.toContain("; integer total"); + // The counter compared with the length directly is an integer. + expect(body(ll, "skipped")).toContain("; integer read"); + expect(body(ll, "skipped")).not.toContain("; integer total"); +}); diff --git a/packages/compiler/test/widened-returns.test.ts b/packages/compiler/test/widened-returns.test.ts new file mode 100644 index 000000000..fe78c6b75 --- /dev/null +++ b/packages/compiler/test/widened-returns.test.ts @@ -0,0 +1,108 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile, deserializeModule } from "../src/index.js"; +import type { IrModule } from "../src/ir/ir.js"; + +async function lowerModule(source: string): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-widened-returns-")); + try { + const entry = join(dir, "main.ts"); + const outPath = join(dir, "main.ir.json"); + await writeFile(entry, source); + const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" }); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + return deserializeModule(await readFile(outPath, "utf8")); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +/** Functions that each forward to the previous one along two return + * branches, above one whose inferred type the checker reduced. */ +function forwardingChain(depth: number, recursive = false): string { + const lines = [ + "export {};", + "type Short = { status: string };", + "type Long = { status: string; value: number };", + "function f0(a: Short, b: Long, n: number) {", + " if (n > 5) return b;", + " return a;", + "}", + ]; + for (let i = 1; i <= depth; i++) + lines.push( + `function f${i}(a: Short, b: Long, n: number) {`, + ...(recursive ? [` if (n > 20) return f${i}(a, b, n - 1);`] : []), + ` if (n > 1) return f${i - 1}(a, b, n - 1);`, + ` if (n > 0) return f${i - 1}(a, b, n + 1);`, + " return a;", + "}", + ); + lines.push( + `console.log(JSON.stringify(f${depth}({ status: "a" }, { status: "b", value: 1 }, 9)));`, + ); + return lines.join("\n"); +} + +test("return widening reads each forwarded callee's type once", { timeout: 60_000 }, async () => { + // Rescanning the callee along every return branch takes 2^depth scans. + const mod = await lowerModule(forwardingChain(40)); + const top = mod.functions.find((f) => f.name === "f40"); + expect(top).toBeDefined(); + expect(top!.returnType.kind).toBe("union"); +}); + +test( + "return widening reads each forwarded callee's type once when the callees call themselves", + { timeout: 60_000 }, + async () => { + const mod = await lowerModule(forwardingChain(40, true)); + const top = mod.functions.find((f) => f.name === "f40"); + expect(top).toBeDefined(); + expect(top!.returnType.kind).toBe("union"); + }, +); + +test("a nested function's widened return follows each instance's type arguments", async () => { + const mod = await lowerModule( + [ + "export {};", + "function outer(x: T, n: number) {", + " const inner = () => {", + " if (n > 0) return { status: x, value: n };", + " return { status: x };", + " };", + " return inner();", + "}", + "console.log(JSON.stringify(outer(1, 1)));", + 'console.log(JSON.stringify(outer("s", 1)));', + "console.log(JSON.stringify(outer(true, 0)));", + ].join("\n"), + ); + const shapes = new Map((mod.records ?? []).map((r) => [r.id, r])); + const unions = new Map((mod.unions ?? []).map((u) => [u.id, u])); + // Each closure returns records whose `status` has the type its captured + // `x` has. + const closures = mod.functions.filter( + (f) => f.name.startsWith("%fn.") && !f.name.includes("adapt"), + ); + expect(closures.length).toBe(3); + for (const closure of closures) { + const captured = closure.captures?.find((c) => c.type.kind !== "f64")?.type.kind ?? "f64"; + const ret = closure.returnType; + expect(ret.kind).toBe("union"); + const arms = ret.kind === "union" ? (unions.get(ret.unionId)?.arms ?? []) : []; + expect(arms.length).toBe(2); + for (const arm of arms) { + expect(arm.kind).toBe("record"); + const status = + arm.kind === "record" + ? shapes.get(arm.shapeId)?.fields.find((f) => f.name === "status") + : undefined; + expect(status?.type.kind).toBe(captured); + } + } +}); diff --git a/packages/compiler/test/worker-shared-loop-version-emission.test.ts b/packages/compiler/test/worker-shared-loop-version-emission.test.ts new file mode 100644 index 000000000..ad7ec1ae9 --- /dev/null +++ b/packages/compiler/test/worker-shared-loop-version-emission.test.ts @@ -0,0 +1,76 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile, deserializeModule } from "../src/index.js"; +import { emitLlvmModule } from "../src/backend/llvm/emitter.js"; + +async function emitted(source: string): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-worker-shared-loop-")); + try { + const entry = join(dir, "main.ts"); + const outPath = join(dir, "main.ir.json"); + await writeFile(entry, source); + const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" }); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + return emitLlvmModule(deserializeModule(await readFile(outPath, "utf8"))); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +const kernel = ` +function sumTo(view: Float64Array, n: number): number { + let total = 0; + for (let i = 0; i < n; i++) total += view[i]!; + return total; +} +`; + +function body(ll: string, name: string): string { + return new RegExp(`^define internal [^\\n]*@sc_bf_${name}(?:\\.i32)?\\([^]*?^}`, "m").exec( + ll, + )![0]; +} + +/** The instructions of the blocks whose labels start with `prefix`. */ +function blocks(fn: string, prefix: string): string { + return fn + .split(/\n(?=[\w.$-]+:)/) + .filter((block) => block.startsWith(prefix)) + .join("\n"); +} + +test("a worker program's loop over views that may be shared keeps a bounds-proven copy", async () => { + const ll = await emitted(` +import { Worker, isMainThread } from "node:worker_threads"; +${kernel} +if (isMainThread) { + const view = new Float64Array(new SharedArrayBuffer(64)); + console.log(sumTo(view, 8)); + new Worker(new URL(import.meta.url)); +} +`); + const fn = body(ll, "sumTo"); + // The guard tests the bounds first, then whether the view is shared. + expect(fn).toMatch(/^bytes\.version\.owned\d*:/m); + expect(fn).toMatch(/^bytes\.version\.fast\.shared\d*:/m); + // The shared copy reads with no bounds test. Each copy is a counted loop + // that polls between blocks, so its read at the counter is a plain load + // (plainSharedElement in expr-bytes.ts). + const shared = blocks(fn, "bytes.version.fast.shared"); + expect(shared).not.toMatch(/icmp (ult|uge|ugt) i64 %\w+, %\w+/); + expect(fn).toMatch(/load double, ptr %\w+/); + expect(fn).not.toMatch(/load atomic double/); +}); + +test("a program without workers keeps two copies of the loop", async () => { + const ll = await emitted(`${kernel} +console.log(sumTo(new Float64Array(8), 8)); +`); + const fn = body(ll, "sumTo"); + expect(fn).toMatch(/^bytes\.version\.fast\d*:/m); + expect(fn).not.toMatch(/bytes\.version\.fast\.shared/); + expect(fn).not.toMatch(/bytes\.version\.owned/); +}); diff --git a/packages/experimental/package.json b/packages/experimental/package.json index cfb4151a4..4636be674 100644 --- a/packages/experimental/package.json +++ b/packages/experimental/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/experimental", - "version": "0.4.2", + "version": "0.4.5", "description": "Experimental scriptc APIs, with Node.js implementations: lifetime regions for ProgramLifetime objects, intern tables shared between threads, and per-thread slots on shared objects", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/libnode-common/bridge/scr_node_bridge.cc b/packages/libnode-common/bridge/scr_node_bridge.cc index 7902c2d79..a457a3ede 100644 --- a/packages/libnode-common/bridge/scr_node_bridge.cc +++ b/packages/libnode-common/bridge/scr_node_bridge.cc @@ -97,11 +97,23 @@ struct StackLimit { } }; +// The start of the runtime's stack when the caller runs on it (see the +// v8::base::Stack definitions at the end of this file), else 0. +uintptr_t HostStackStart() { + return g_host != nullptr && g_host->stack_start != nullptr ? g_host->stack_start() : 0; +} + Local Utf8(Isolate *isolate, const char *text, size_t len) { return String::NewFromUtf8(isolate, text, NewStringType::kNormal, static_cast(len)) .ToLocalChecked(); } +// A string longer than V8 can hold is empty instead of a fatal error. +v8::MaybeLocal MaybeUtf8(Isolate *isolate, const char *text, size_t len) { + if (len > static_cast(String::kMaxLength)) return {}; + return String::NewFromUtf8(isolate, text, NewStringType::kNormal, static_cast(len)); +} + Local Utf8(Isolate *isolate, const char *text) { return Utf8(isolate, text, std::strlen(text)); } @@ -113,15 +125,20 @@ std::string ToStdString(Isolate *isolate, Local value) { // The outcome of an exception the dispatcher itself did not catch (a stack // overflow, for example): an Error copy with its message. +// Reading the message runs user code (a getter, a toString), which may throw +// again; the copy then has an empty message. std::string ThrownOutcome(Isolate *isolate, Local context, Local exception) { + TryCatch inner(isolate); Local message = exception; if (exception->IsObject()) { Local own; if (exception.As()->Get(context, Utf8(isolate, "message")).ToLocal(&own)) message = own; } + Local text; Local quoted; - if (!v8::JSON::Stringify(context, message->ToString(context).ToLocalChecked()).ToLocal("ed)) + if (!message->ToString(context).ToLocal(&text) || + !v8::JSON::Stringify(context, text).ToLocal("ed)) return "T{\"error\":{\"kind\":0,\"message\":\"\"}}"; return "T{\"error\":{\"kind\":0,\"message\":" + ToStdString(isolate, quoted) + "}}"; } @@ -177,12 +194,18 @@ char *Start(const ScrNodeBridgeHost *host, const char *bootstrap, const char *ar if (dladdr(reinterpret_cast(&napi_create_function), &info) && info.dli_fname) dlopen(info.dli_fname, RTLD_NOW | RTLD_NOLOAD | RTLD_GLOBAL); std::vector args = {argv0}; + // The program is not a node process: NODE_OPTIONS from its environment + // does not apply, and Node leaves the process's resource limits and code + // pages as they are. auto result = node::InitializeOncePerProcess( args, {node::ProcessInitializationFlags::kNoInitializeV8, node::ProcessInitializationFlags::kNoInitializeNodeV8Platform, node::ProcessInitializationFlags::kNoStdioInitialization, node::ProcessInitializationFlags::kNoDefaultSignalHandling, - node::ProcessInitializationFlags::kNoPrintHelpOrVersionOutput}); + node::ProcessInitializationFlags::kNoPrintHelpOrVersionOutput, + node::ProcessInitializationFlags::kDisableNodeOptionsEnv, + node::ProcessInitializationFlags::kNoAdjustResourceLimits, + node::ProcessInitializationFlags::kNoUseLargePages}); if (result->early_return() || result->exit_code() != 0) { std::string message = "Node initialization failed"; for (const std::string &error : result->errors()) message += ": " + error; @@ -192,9 +215,12 @@ char *Start(const ScrNodeBridgeHost *host, const char *bootstrap, const char *ar v8::V8::InitializePlatform(g_platform.get()); v8::V8::Initialize(); g_snapshot = node::EmbedderSnapshotData::BuiltinSnapshotData(); + // Modules resolve inside the program's own module tree, never from + // NODE_PATH or the user's global module directories. auto flags = static_cast( node::EnvironmentFlags::kDefaultFlags | node::EnvironmentFlags::kNoCreateInspector | - node::EnvironmentFlags::kNoStartDebugSignalHandler); + node::EnvironmentFlags::kNoStartDebugSignalHandler | + node::EnvironmentFlags::kNoGlobalSearchPaths); std::vector errors; g_setup = g_snapshot ? node::CommonEnvironmentSetup::CreateFromSnapshot( g_platform.get(), &errors, g_snapshot.get(), result->args(), @@ -275,12 +301,23 @@ char *Call(uint32_t id, int sync, const char *file, const char *specifier, const // No callback scope: the runtime's checkpoint drains what the call // queued, at the end of the runtime callback that made it (NI-3). TryCatch try_catch(isolate); + Local args_text; + if (!MaybeUtf8(isolate, args, args_len).ToLocal(&args_text)) { + // The argument JSON is longer than V8's string limit: the RangeError + // V8 throws for such a string. + outcome = "T{\"error\":{\"kind\":2,\"message\":\"Invalid string length\"}}"; + if (!sync) { + g_host->settle(id, outcome.data(), outcome.size()); + return nullptr; + } + return Copy(outcome, len); + } Local argv[] = {Integer::NewFromUnsigned(isolate, id), v8::Boolean::New(isolate, sync != 0), Utf8(isolate, file), Utf8(isolate, specifier), Utf8(isolate, name), - Utf8(isolate, args, args_len)}; + args_text}; Local result; if (g_call.Get(isolate) ->Call(context, v8::Undefined(isolate), sizeof argv / sizeof argv[0], argv) @@ -615,6 +652,32 @@ const ScrNodeBridge kBridge = { } // namespace +#if defined(__linux__) +// V8 asks v8::base::Stack for the start of the current thread's stack when +// it records the bound of its conservative stack scan (isolate creation, +// Locker). libnode calls these exported functions through its PLT, and this +// library precedes libnode in the lookup scope, so the definitions below +// answer for it: the runtime's stack while the caller runs on it, else +// libnode's own answer (RTLD_NEXT). Without them a garbage collection that +// scans the stack while JavaScript runs on the runtime's stack walks from +// that stack up to the main thread's and faults in the unmapped gap. +// (macOS binds libnode's calls inside libnode; its packs need another way.) +#define SCR_V8_STACK_START(fn, symbol) \ + extern "C" __attribute__((visibility("default"))) void *fn() __asm__(symbol); \ + extern "C" void *fn() { \ + static void *(*next)() = nullptr; \ + uintptr_t start = HostStackStart(); \ + if (start != 0) return reinterpret_cast(start); \ + if (next == nullptr) next = reinterpret_cast(dlsym(RTLD_NEXT, symbol)); \ + return next != nullptr ? next() : nullptr; \ + } +SCR_V8_STACK_START(ScrV8GetStackStart, "_ZN2v84base5Stack13GetStackStartEv") +SCR_V8_STACK_START(ScrV8GetStackStartUnchecked, "_ZN2v84base5Stack22GetStackStartUncheckedEv") +SCR_V8_STACK_START(ScrV8ObtainCurrentThreadStackStart, + "_ZN2v84base5Stack29ObtainCurrentThreadStackStartEv") +#undef SCR_V8_STACK_START +#endif + extern "C" __attribute__((visibility("default"))) const ScrNodeBridge *scr_node_bridge_v1() { return &kBridge; } diff --git a/packages/libnode-common/test/bridge.test.ts b/packages/libnode-common/test/bridge.test.ts index 4c37b31a3..c1dfc4589 100644 --- a/packages/libnode-common/test/bridge.test.ts +++ b/packages/libnode-common/test/bridge.test.ts @@ -45,7 +45,7 @@ exports.quit = (code) => process.exit(code); expect(result.stderr).toBe(""); expect(result.stdout).toBe( [ - "abi 3 ok 1", + "abi 4 ok 1", "sync add -> R5", 'sync fails -> T{"error":{"kind":2,"message":"out of range","code":"ERR_X"}}', `sync missing -> T{"error":{"kind":1,"message":"'missing' exported by './smoke.cjs' is not a function"}}`, diff --git a/packages/llvm-darwin-arm64/package.json b/packages/llvm-darwin-arm64/package.json index 615fe9d81..d1ac54f3e 100644 --- a/packages/llvm-darwin-arm64/package.json +++ b/packages/llvm-darwin-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/llvm-darwin-arm64", - "version": "0.4.2", + "version": "0.4.5", "description": "Pinned LLVM code-generation helper for scriptc on macOS arm64", "license": "Apache-2.0 WITH LLVM-exception", "homepage": "https://scriptc.dev", diff --git a/packages/llvm-darwin-x64/package.json b/packages/llvm-darwin-x64/package.json index fd6caf976..b96a90cae 100644 --- a/packages/llvm-darwin-x64/package.json +++ b/packages/llvm-darwin-x64/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/llvm-darwin-x64", - "version": "0.4.2", + "version": "0.4.5", "description": "Pinned LLVM code-generation helper for scriptc on macOS x64", "license": "Apache-2.0 WITH LLVM-exception", "homepage": "https://scriptc.dev", diff --git a/packages/llvm-linux-arm64-gnu/package.json b/packages/llvm-linux-arm64-gnu/package.json index 7d66257f4..49796f522 100644 --- a/packages/llvm-linux-arm64-gnu/package.json +++ b/packages/llvm-linux-arm64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/llvm-linux-arm64-gnu", - "version": "0.4.2", + "version": "0.4.5", "description": "Pinned LLVM code-generation helper for scriptc on Linux arm64", "license": "Apache-2.0 WITH LLVM-exception", "homepage": "https://scriptc.dev", diff --git a/packages/llvm-linux-arm64-gnu/scripts/build.mjs b/packages/llvm-linux-arm64-gnu/scripts/build.mjs index 603a77d39..1036d7c6e 100644 --- a/packages/llvm-linux-arm64-gnu/scripts/build.mjs +++ b/packages/llvm-linux-arm64-gnu/scripts/build.mjs @@ -1,9 +1,9 @@ #!/usr/bin/env node -import { nativeTargetCmakeArgs } from "../../../native/llvm-codegen/targets.mjs"; -import { execFileSync } from "node:child_process"; -import { chmodSync, copyFileSync, mkdirSync, readFileSync } from "node:fs"; +import { buildLinuxHelper } from "../../../native/llvm-codegen/linux-helper.mjs"; +import { readFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; + const root = dirname(dirname(fileURLToPath(import.meta.url))); const repo = fileURLToPath(new URL("../../..", import.meta.url)); if (process.platform !== "linux" || process.arch !== "arm64") { @@ -11,30 +11,10 @@ if (process.platform !== "linux" || process.arch !== "arm64") { process.exit(0); } const manifest = JSON.parse(readFileSync(join(root, "package.json"), "utf8")); -const build = join(repo, "node_modules/.cache/scriptc-llvm-linux-arm64-gnu"); -execFileSync( - "cmake", - [ - "-S", - join(repo, "native/llvm-codegen"), - "-B", - build, - "-G", - "Ninja", - `-DLLVM_DIR=${process.env.LLVM_DIR ?? "/usr/lib/llvm-22/lib/cmake/llvm"}`, - `-DSCRIPTC_PACKAGE_VERSION=${manifest.version}`, - "-DSCRIPTC_DEFAULT_TARGET=aarch64-unknown-linux-gnu", - "-DSCRIPTC_DEFAULT_DATA_LAYOUT=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", - ...nativeTargetCmakeArgs, - "-DSCRIPTC_STATIC_LINUX_BUILD=ON", - "-DCMAKE_EXE_LINKER_FLAGS=-static", - "-DCMAKE_BUILD_TYPE=Release", - ], - { stdio: "inherit" }, -); -execFileSync("cmake", ["--build", build, "--target", "scriptc-llvm-codegen"], { stdio: "inherit" }); -mkdirSync(join(root, "bin"), { recursive: true }); -const output = join(root, "bin", "scriptc-llvm-codegen"); -copyFileSync(join(build, "scriptc-llvm-codegen"), output); -execFileSync("strip", [output], { stdio: "inherit" }); -chmodSync(output, 0o755); +buildLinuxHelper({ + arch: "arm64", + libc: "gnu", + version: manifest.version, + build: join(repo, "node_modules/.cache/scriptc-llvm-linux-arm64-gnu"), + output: join(root, "bin", "scriptc-llvm-codegen"), +}); diff --git a/packages/llvm-linux-arm64-musl/package.json b/packages/llvm-linux-arm64-musl/package.json index b3a01089d..3001949c6 100644 --- a/packages/llvm-linux-arm64-musl/package.json +++ b/packages/llvm-linux-arm64-musl/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/llvm-linux-arm64-musl", - "version": "0.4.2", + "version": "0.4.5", "description": "Statically linked LLVM code-generation helper for scriptc on Linux arm64 musl", "license": "Apache-2.0 WITH LLVM-exception", "homepage": "https://scriptc.dev", diff --git a/packages/llvm-linux-arm64-musl/scripts/build.mjs b/packages/llvm-linux-arm64-musl/scripts/build.mjs index 3b9c49983..187459b2a 100644 --- a/packages/llvm-linux-arm64-musl/scripts/build.mjs +++ b/packages/llvm-linux-arm64-musl/scripts/build.mjs @@ -1,9 +1,9 @@ #!/usr/bin/env node -import { nativeTargetCmakeArgs } from "../../../native/llvm-codegen/targets.mjs"; -import { execFileSync } from "node:child_process"; -import { chmodSync, copyFileSync, mkdirSync, readFileSync } from "node:fs"; +import { buildLinuxHelper } from "../../../native/llvm-codegen/linux-helper.mjs"; +import { readFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; + const root = dirname(dirname(fileURLToPath(import.meta.url))); const repo = fileURLToPath(new URL("../../..", import.meta.url)); if (process.platform !== "linux" || process.arch !== "arm64") { @@ -11,32 +11,10 @@ if (process.platform !== "linux" || process.arch !== "arm64") { process.exit(0); } const manifest = JSON.parse(readFileSync(join(root, "package.json"), "utf8")); -const build = join(repo, "node_modules/.cache/scriptc-llvm-linux-arm64-musl"); -execFileSync( - "cmake", - [ - "-S", - join(repo, "native/llvm-codegen"), - "-B", - build, - "-G", - "Ninja", - `-DCMAKE_C_COMPILER=${process.env.CC ?? "cc"}`, - `-DCMAKE_CXX_COMPILER=${process.env.CXX ?? "c++"}`, - "-DSCRIPTC_STATIC_LINUX_BUILD=ON", - "-DCMAKE_EXE_LINKER_FLAGS=-static", - `-DLLVM_DIR=${process.env.LLVM_DIR ?? "/usr/lib/llvm-22/lib/cmake/llvm"}`, - `-DSCRIPTC_PACKAGE_VERSION=${manifest.version}`, - "-DSCRIPTC_DEFAULT_TARGET=aarch64-unknown-linux-musl", - "-DSCRIPTC_DEFAULT_DATA_LAYOUT=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", - ...nativeTargetCmakeArgs, - "-DCMAKE_BUILD_TYPE=Release", - ], - { stdio: "inherit" }, -); -execFileSync("cmake", ["--build", build, "--target", "scriptc-llvm-codegen"], { stdio: "inherit" }); -mkdirSync(join(root, "bin"), { recursive: true }); -const output = join(root, "bin", "scriptc-llvm-codegen"); -copyFileSync(join(build, "scriptc-llvm-codegen"), output); -execFileSync("strip", [output], { stdio: "inherit" }); -chmodSync(output, 0o755); +buildLinuxHelper({ + arch: "arm64", + libc: "musl", + version: manifest.version, + build: join(repo, "node_modules/.cache/scriptc-llvm-linux-arm64-musl"), + output: join(root, "bin", "scriptc-llvm-codegen"), +}); diff --git a/packages/llvm-linux-x64-gnu/package.json b/packages/llvm-linux-x64-gnu/package.json index 0b3264ec7..11b375ba4 100644 --- a/packages/llvm-linux-x64-gnu/package.json +++ b/packages/llvm-linux-x64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/llvm-linux-x64-gnu", - "version": "0.4.2", + "version": "0.4.5", "description": "Pinned LLVM code-generation helper for scriptc on Linux x64", "license": "Apache-2.0 WITH LLVM-exception", "homepage": "https://scriptc.dev", diff --git a/packages/llvm-linux-x64-gnu/scripts/build.mjs b/packages/llvm-linux-x64-gnu/scripts/build.mjs index 5f10c9652..f70fdfa43 100644 --- a/packages/llvm-linux-x64-gnu/scripts/build.mjs +++ b/packages/llvm-linux-x64-gnu/scripts/build.mjs @@ -1,7 +1,6 @@ #!/usr/bin/env node -import { nativeTargetCmakeArgs } from "../../../native/llvm-codegen/targets.mjs"; -import { execFileSync } from "node:child_process"; -import { chmodSync, copyFileSync, mkdirSync, readFileSync } from "node:fs"; +import { buildLinuxHelper } from "../../../native/llvm-codegen/linux-helper.mjs"; +import { readFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; @@ -12,30 +11,10 @@ if (process.platform !== "linux" || process.arch !== "x64") { process.exit(0); } const manifest = JSON.parse(readFileSync(join(root, "package.json"), "utf8")); -const build = join(repo, "node_modules/.cache/scriptc-llvm-linux-x64-gnu"); -execFileSync( - "cmake", - [ - "-S", - join(repo, "native/llvm-codegen"), - "-B", - build, - "-G", - "Ninja", - `-DLLVM_DIR=${process.env.LLVM_DIR ?? "/usr/lib/llvm-22/lib/cmake/llvm"}`, - `-DSCRIPTC_PACKAGE_VERSION=${manifest.version}`, - "-DSCRIPTC_DEFAULT_TARGET=x86_64-unknown-linux-gnu", - "-DSCRIPTC_DEFAULT_DATA_LAYOUT=e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128", - ...nativeTargetCmakeArgs, - "-DSCRIPTC_STATIC_LINUX_BUILD=ON", - "-DCMAKE_EXE_LINKER_FLAGS=-static", - "-DCMAKE_BUILD_TYPE=Release", - ], - { stdio: "inherit" }, -); -execFileSync("cmake", ["--build", build, "--target", "scriptc-llvm-codegen"], { stdio: "inherit" }); -mkdirSync(join(root, "bin"), { recursive: true }); -const output = join(root, "bin", "scriptc-llvm-codegen"); -copyFileSync(join(build, "scriptc-llvm-codegen"), output); -execFileSync("strip", [output], { stdio: "inherit" }); -chmodSync(output, 0o755); +buildLinuxHelper({ + arch: "x64", + libc: "gnu", + version: manifest.version, + build: join(repo, "node_modules/.cache/scriptc-llvm-linux-x64-gnu"), + output: join(root, "bin", "scriptc-llvm-codegen"), +}); diff --git a/packages/llvm-linux-x64-musl/package.json b/packages/llvm-linux-x64-musl/package.json index 6c83d1cb9..ffe1ca7ae 100644 --- a/packages/llvm-linux-x64-musl/package.json +++ b/packages/llvm-linux-x64-musl/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/llvm-linux-x64-musl", - "version": "0.4.2", + "version": "0.4.5", "description": "Statically linked LLVM code-generation helper for scriptc on Linux x64 musl", "license": "Apache-2.0 WITH LLVM-exception", "homepage": "https://scriptc.dev", diff --git a/packages/llvm-linux-x64-musl/scripts/build.mjs b/packages/llvm-linux-x64-musl/scripts/build.mjs index 20987e0e5..bb5a0ec7e 100644 --- a/packages/llvm-linux-x64-musl/scripts/build.mjs +++ b/packages/llvm-linux-x64-musl/scripts/build.mjs @@ -1,7 +1,6 @@ #!/usr/bin/env node -import { nativeTargetCmakeArgs } from "../../../native/llvm-codegen/targets.mjs"; -import { execFileSync } from "node:child_process"; -import { chmodSync, copyFileSync, mkdirSync, readFileSync } from "node:fs"; +import { buildLinuxHelper } from "../../../native/llvm-codegen/linux-helper.mjs"; +import { readFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; @@ -12,32 +11,10 @@ if (process.platform !== "linux" || process.arch !== "x64") { process.exit(0); } const manifest = JSON.parse(readFileSync(join(root, "package.json"), "utf8")); -const build = join(repo, "node_modules/.cache/scriptc-llvm-linux-x64-musl"); -execFileSync( - "cmake", - [ - "-S", - join(repo, "native/llvm-codegen"), - "-B", - build, - "-G", - "Ninja", - `-DCMAKE_C_COMPILER=${process.env.CC ?? "cc"}`, - `-DCMAKE_CXX_COMPILER=${process.env.CXX ?? "c++"}`, - "-DSCRIPTC_STATIC_LINUX_BUILD=ON", - "-DCMAKE_EXE_LINKER_FLAGS=-static", - `-DLLVM_DIR=${process.env.LLVM_DIR ?? "/usr/lib/llvm-22/lib/cmake/llvm"}`, - `-DSCRIPTC_PACKAGE_VERSION=${manifest.version}`, - "-DSCRIPTC_DEFAULT_TARGET=x86_64-unknown-linux-musl", - "-DSCRIPTC_DEFAULT_DATA_LAYOUT=e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128", - ...nativeTargetCmakeArgs, - "-DCMAKE_BUILD_TYPE=Release", - ], - { stdio: "inherit" }, -); -execFileSync("cmake", ["--build", build, "--target", "scriptc-llvm-codegen"], { stdio: "inherit" }); -mkdirSync(join(root, "bin"), { recursive: true }); -const output = join(root, "bin", "scriptc-llvm-codegen"); -copyFileSync(join(build, "scriptc-llvm-codegen"), output); -execFileSync("strip", [output], { stdio: "inherit" }); -chmodSync(output, 0o755); +buildLinuxHelper({ + arch: "x64", + libc: "musl", + version: manifest.version, + build: join(repo, "node_modules/.cache/scriptc-llvm-linux-x64-musl"), + output: join(root, "bin", "scriptc-llvm-codegen"), +}); diff --git a/packages/llvm-win32-x64-msvc/package.json b/packages/llvm-win32-x64-msvc/package.json index 9fb682f0f..77e6fe687 100644 --- a/packages/llvm-win32-x64-msvc/package.json +++ b/packages/llvm-win32-x64-msvc/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/llvm-win32-x64-msvc", - "version": "0.4.2", + "version": "0.4.5", "description": "Pinned LLVM code-generation helper for scriptc on Windows x64", "license": "Apache-2.0 WITH LLVM-exception", "homepage": "https://scriptc.dev", diff --git a/packages/node/README.md b/packages/node/README.md index 77002db49..7b1c3656d 100644 --- a/packages/node/README.md +++ b/packages/node/README.md @@ -10,6 +10,8 @@ const config = await call<{ regions: string[] }>("config-parser", "parse", sourc ``` - The module loads with `require()` from the calling module: relative specifiers resolve from its directory, bare ones from its `node_modules`. +- In scriptc builds the specifier is a string literal. The build collects the module into the executable: a module of the program with the modules its string-literal `require()` and `import` reach, or a whole package with its dependencies. The executable loads them from its own copy, never from the build machine's files. A module of the program that loads others through computed specifiers is refused (SC8006); a module the build cannot find throws `MODULE_NOT_FOUND` when the call runs. +- The executable extracts its modules into `$XDG_CACHE_HOME/scriptc/node-islands/` (or `~/.cache/scriptc/node-islands/`) and checks every file against the build's digests on each start. With `--libnode=beside` they are written next to the executable instead, in `.libnode/modules/`. - Arguments and results cross as JSON copies; scriptc checks the result against the declared type. - A thrown `Error` crosses as its nearest intrinsic error class with its `message` and string `code`. - The executable loads Node from the libnode pack recorded at build time (`SCRIPTC_LIBNODE` or `scriptc build --libnode=`; `--libnode=beside` copies the pack next to the executable). `SCRIPTC_LIBNODE` overrides it at run time. A program that never calls into Node never loads it. diff --git a/packages/node/index.d.ts b/packages/node/index.d.ts index 10642b098..87a8eb541 100644 --- a/packages/node/index.d.ts +++ b/packages/node/index.d.ts @@ -9,6 +9,8 @@ * - the module loads with require() from the calling module, so a relative * specifier resolves from the caller's directory and a bare one from its * node_modules (ES modules load through require(esm)); + * - in scriptc builds `specifier` is a string literal: the build ships the + * module, and what it loads, inside the executable; * - the arguments cross as a copy, `JSON.parse(JSON.stringify(args))`, and * the function runs with the module namespace as `this`; * - the result crosses as its JSON copy (undefined stays undefined), and diff --git a/packages/runtime-android-arm64/package.json b/packages/runtime-android-arm64/package.json index 362fbc572..7aea1da1e 100644 --- a/packages/runtime-android-arm64/package.json +++ b/packages/runtime-android-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-android-arm64", - "version": "0.4.2", + "version": "0.4.5", "description": "Precompiled scriptc library runtime pack for Android arm64", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-darwin-arm64/package.json b/packages/runtime-darwin-arm64/package.json index a4176ed60..ae068a00a 100644 --- a/packages/runtime-darwin-arm64/package.json +++ b/packages/runtime-darwin-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-darwin-arm64", - "version": "0.4.2", + "version": "0.4.5", "description": "Precompiled scriptc runtime pack for macOS arm64", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs b/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs index 7b2d3e115..197adc516 100644 --- a/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs +++ b/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs @@ -14,6 +14,7 @@ import { */ const any = (...features) => ({ any: features }); +const all = (...features) => ({ all: features }); // ld64 dead-strips Mach-O symbol subsections without an ELF-style compile // flag. Keep this checked-in metadata alongside the matrix so pack build and @@ -43,6 +44,7 @@ export const RUNTIME_PACK_MATRIX = withLibraryRuntimeFlavors( { id: "quickjs", predicate: false }, { id: "libregexp", predicate: "regex" }, { id: "zlib", predicate: "zlibEffective" }, + { id: "brotli", predicate: all("zlib", "brotli") }, { id: "mbedtls", predicate: any("tlsEffective", "cipher", "ecdh", "dh", "pki") }, ], system_libraries: [{ name: "System", predicate: true }], diff --git a/packages/runtime-darwin-arm64/scripts/build.mjs b/packages/runtime-darwin-arm64/scripts/build.mjs index e492c36b4..dc6c24a47 100644 --- a/packages/runtime-darwin-arm64/scripts/build.mjs +++ b/packages/runtime-darwin-arm64/scripts/build.mjs @@ -15,6 +15,7 @@ import { } from "../../runtime-pack-common/scripts/runtime-unit.mjs"; import { createDeterministicArchive } from "./archive.mjs"; import { installRuntimePack, withBuildLock } from "./build-state.mjs"; +import { shareIdenticalArtifacts } from "../../runtime-pack-common/scripts/share-artifacts.mjs"; const run = promisify(execFile); const packageRoot = dirname(dirname(fileURLToPath(import.meta.url))); @@ -72,6 +73,7 @@ async function build() { const quickjs = join(vendorRoot, "quickjs-ng"); const zlib = join(vendorRoot, "zlib"); const mbedtls = join(vendorRoot, "mbedtls"); + const brotli = join(vendorRoot, "brotli", "c"); const QJS_SOURCES = ["dtoa.c", "libregexp.c", "libunicode.c", "quickjs.c"]; const LRE_SOURCES = ["libregexp.c", "libunicode.c"]; const ZLIB_SOURCES = [ @@ -112,20 +114,23 @@ async function build() { return parallelMap(items, width, task); } + // Archive members are named by basename, so a source in a subdirectory + // (brotli's dec/ and enc/ share file names) is named after its path. + const objectName = (source) => source.replaceAll("/", "-").replace(/\.c$/, ".o"); async function archive(id, sources, sourceRoot, flags) { const root = join(stagedOutputRoot, "vendor", id); const objectRoot = join(root, "objects"); await parallel(sources, async (source) => { await compile( join(sourceRoot, source), - join(objectRoot, source.replace(/\.c$/, ".o")), + join(objectRoot, objectName(source)), unalignedVendorSources.has(source) ? flags.filter((flag) => !codeAlignment.includes(flag)) : flags, ); }); const output = join(root, `libscriptc-${id}.a`); - const objects = sources.map((source) => join(objectRoot, source.replace(/\.c$/, ".o"))); + const objects = sources.map((source) => join(objectRoot, objectName(source))); await createDeterministicArchive(archiver, output, objects); await rm(objectRoot, { recursive: true, force: true }); return { @@ -181,6 +186,7 @@ async function build() { ...(unit.source === "scr_zlib.c" || unit.source === "scr_fetch.c" ? ["-I", zlib] : []), + ...(unit.source === "scr_brotli.c" ? ["-I", join(brotli, "include")] : []), ]; const unitFlags = [ ...commonFlags, @@ -268,6 +274,17 @@ async function build() { const mbedtlsSources = (await readdir(join(mbedtls, "library"))) .filter((name) => !name.startsWith(".") && name.endsWith(".c")) .sort(); + const brotliSources = ( + await Promise.all( + ["common", "dec", "enc"].map(async (dir) => + (await readdir(join(brotli, dir))) + .filter((name) => !name.startsWith(".") && name.endsWith(".c")) + .map((name) => `${dir}/${name}`), + ), + ) + ) + .flat() + .sort(); const archives = [ await archive("quickjs", QJS_SOURCES, quickjs, [ "-target", @@ -301,6 +318,15 @@ async function build() { "-I", zlib, ]), + await archive("brotli", brotliSources, brotli, [ + "-target", + RUNTIME_PACK_MATRIX.target.llvm_triple, + "-std=c11", + "-Os", + ...codeAlignment, + "-I", + join(brotli, "include"), + ]), await archive("mbedtls", mbedtlsSources, join(mbedtls, "library"), [ "-target", RUNTIME_PACK_MATRIX.target.llvm_triple, @@ -323,8 +349,14 @@ async function build() { "artifacts/licenses/v8-number-radix.txt", "BSD-3-Clause", ], + [ + join(vendorRoot, "v8-ieee754", "LICENSE"), + "artifacts/licenses/v8-ieee754.txt", + "BSD-3-Clause", + ], [join(vendorRoot, "unicode", "LICENSE"), "artifacts/licenses/unicode.txt", "Unicode-3.0"], [join(zlib, "LICENSE"), "artifacts/licenses/zlib.txt", "Zlib"], + [join(vendorRoot, "brotli", "LICENSE"), "artifacts/licenses/brotli.txt", "MIT"], [join(mbedtls, "LICENSE"), "artifacts/licenses/mbedtls.txt", "Apache-2.0"], ]; await Promise.all( @@ -334,13 +366,14 @@ async function build() { await copyFile(source, output); }), ); + await shareIdenticalArtifacts(flavors, buildRoot); const manifest = { schema: "scriptc.runtime-pack.v1", format: 1, package: packageManifest.name, version: packageManifest.version, target: RUNTIME_PACK_MATRIX.target, - runtime_abi: { version: 37, marker: "scr_runtime_abi_v37" }, + runtime_abi: { version: 47, marker: "scr_runtime_abi_v47" }, compiler: { command: compiler, identity: compilerVersion, diff --git a/packages/runtime-darwin-x64/package.json b/packages/runtime-darwin-x64/package.json index 91bc4182b..7dbdaf6b9 100644 --- a/packages/runtime-darwin-x64/package.json +++ b/packages/runtime-darwin-x64/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-darwin-x64", - "version": "0.4.2", + "version": "0.4.5", "description": "Precompiled scriptc runtime pack for macOS x64", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-ios-arm64/package.json b/packages/runtime-ios-arm64/package.json index 2fdd68897..8a868a606 100644 --- a/packages/runtime-ios-arm64/package.json +++ b/packages/runtime-ios-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-ios-arm64", - "version": "0.4.2", + "version": "0.4.5", "description": "Precompiled scriptc library runtime pack for iOS arm64", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-ios-simulator-arm64/package.json b/packages/runtime-ios-simulator-arm64/package.json index cbdf96d1d..011d41b39 100644 --- a/packages/runtime-ios-simulator-arm64/package.json +++ b/packages/runtime-ios-simulator-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-ios-simulator-arm64", - "version": "0.4.2", + "version": "0.4.5", "description": "Precompiled scriptc library runtime pack for iOS simulator arm64", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-linux-arm64-gnu/package.json b/packages/runtime-linux-arm64-gnu/package.json index e0d228925..92a3192e7 100644 --- a/packages/runtime-linux-arm64-gnu/package.json +++ b/packages/runtime-linux-arm64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-linux-arm64-gnu", - "version": "0.4.2", + "version": "0.4.5", "description": "Precompiled scriptc runtime pack for Linux arm64 glibc", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-linux-arm64-gnu/scripts/build.mjs b/packages/runtime-linux-arm64-gnu/scripts/build.mjs index 729c3fbb6..d16fb1528 100644 --- a/packages/runtime-linux-arm64-gnu/scripts/build.mjs +++ b/packages/runtime-linux-arm64-gnu/scripts/build.mjs @@ -24,6 +24,10 @@ process.env.SCRIPTC_RUNTIME_PACK_CONFIG = JSON.stringify({ }, ...toolchain, compileFlags: ["-ffunction-sections", "-fdata-sections"], - systemLibraries: [{ name: "m", predicate: true }], + // scr_unwind_raise: the static libgcc_eh, which loads no shared library. + systemLibraries: [ + { name: "m", predicate: true }, + { name: "gcc_eh", predicate: true }, + ], }); await import("../../runtime-pack-common/scripts/build.mjs"); diff --git a/packages/runtime-linux-arm64-musl/package.json b/packages/runtime-linux-arm64-musl/package.json index 70eee223f..5e751d829 100644 --- a/packages/runtime-linux-arm64-musl/package.json +++ b/packages/runtime-linux-arm64-musl/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-linux-arm64-musl", - "version": "0.4.2", + "version": "0.4.5", "description": "Precompiled scriptc runtime pack for Linux arm64 musl", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-linux-arm64-musl/scripts/build.mjs b/packages/runtime-linux-arm64-musl/scripts/build.mjs index 10635d857..6c9b38244 100644 --- a/packages/runtime-linux-arm64-musl/scripts/build.mjs +++ b/packages/runtime-linux-arm64-musl/scripts/build.mjs @@ -26,6 +26,10 @@ process.env.SCRIPTC_RUNTIME_PACK_CONFIG = JSON.stringify({ }, targetArgs: ["-target", "aarch64-linux-musl"], compileFlags: ["-ffunction-sections", "-fdata-sections"], - systemLibraries: [{ name: "m", predicate: true }], + // scr_unwind_raise: Zig's libunwind. + systemLibraries: [ + { name: "m", predicate: true }, + { name: "unwind", predicate: true }, + ], }); await import("../../runtime-pack-common/scripts/build.mjs"); diff --git a/packages/runtime-linux-x64-gnu/package.json b/packages/runtime-linux-x64-gnu/package.json index 5822fd21d..63c045385 100644 --- a/packages/runtime-linux-x64-gnu/package.json +++ b/packages/runtime-linux-x64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-linux-x64-gnu", - "version": "0.4.2", + "version": "0.4.5", "description": "Precompiled scriptc runtime pack for Linux x64 glibc", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-linux-x64-gnu/scripts/build.mjs b/packages/runtime-linux-x64-gnu/scripts/build.mjs index 811bb537b..53df5f7e7 100644 --- a/packages/runtime-linux-x64-gnu/scripts/build.mjs +++ b/packages/runtime-linux-x64-gnu/scripts/build.mjs @@ -24,6 +24,10 @@ process.env.SCRIPTC_RUNTIME_PACK_CONFIG = JSON.stringify({ }, ...toolchain, compileFlags: ["-ffunction-sections", "-fdata-sections"], - systemLibraries: [{ name: "m", predicate: true }], + // scr_unwind_raise: the static libgcc_eh, which loads no shared library. + systemLibraries: [ + { name: "m", predicate: true }, + { name: "gcc_eh", predicate: true }, + ], }); await import("../../runtime-pack-common/scripts/build.mjs"); diff --git a/packages/runtime-linux-x64-musl/package.json b/packages/runtime-linux-x64-musl/package.json index 1db34d4a7..a8602b395 100644 --- a/packages/runtime-linux-x64-musl/package.json +++ b/packages/runtime-linux-x64-musl/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-linux-x64-musl", - "version": "0.4.2", + "version": "0.4.5", "description": "Precompiled scriptc runtime pack for Linux x64 musl", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-linux-x64-musl/scripts/build.mjs b/packages/runtime-linux-x64-musl/scripts/build.mjs index 33fc4c3d1..88538de76 100644 --- a/packages/runtime-linux-x64-musl/scripts/build.mjs +++ b/packages/runtime-linux-x64-musl/scripts/build.mjs @@ -26,6 +26,10 @@ process.env.SCRIPTC_RUNTIME_PACK_CONFIG = JSON.stringify({ }, targetArgs: ["-target", "x86_64-linux-musl"], compileFlags: ["-ffunction-sections", "-fdata-sections"], - systemLibraries: [{ name: "m", predicate: true }], + // scr_unwind_raise: Zig's libunwind. + systemLibraries: [ + { name: "m", predicate: true }, + { name: "unwind", predicate: true }, + ], }); await import("../../runtime-pack-common/scripts/build.mjs"); diff --git a/packages/runtime-pack-common/library-matrix.mjs b/packages/runtime-pack-common/library-matrix.mjs index d9878ec7c..1a621a375 100644 --- a/packages/runtime-pack-common/library-matrix.mjs +++ b/packages/runtime-pack-common/library-matrix.mjs @@ -13,6 +13,7 @@ export function withLibraryRuntimeFlavors(matrix) { "scr_events_emitter.c", "scr_dyn_handle.c", "scr_zlib.c", + "scr_x509.c", "scr_copying.c", "scr_dyn_invoke.c", "scr_bytes_dyn.c", diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs index a110de0bf..cba1b5deb 100644 --- a/packages/runtime-pack-common/runtime-pack-matrix.mjs +++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs @@ -30,6 +30,7 @@ export const BASE_RUNTIME_SOURCES = [ "scr_object.c", "scr_union.c", "scr_exception.c", + "scr_unwind.c", "scr_error.c", "scr_console.c", "scr_lib.c", @@ -51,6 +52,10 @@ export const BASE_RUNTIME_SOURCES = [ "scr_alloc.c", // crypto.getCipherInfo's table; it links only into programs that call it. "scr_cipher_info.c", + // Math's transcendental functions as V8 computes them, and on glibc the + // exact libm functions that let a program link without libm.so. + "scr_ieee754.c", + "scr_glibc_math.c", ]; export const OPTIONAL_RUNTIME_SOURCES = [ @@ -67,6 +72,8 @@ export const OPTIONAL_RUNTIME_SOURCES = [ ["scr_bytes_dyn.c", all("dynBuffer", "dynInvoke")], // Builtin functions read as values in JavaScript dependencies. ["scr_builtin_values.c", "builtinValues"], + // createRequire's computed require and require.resolve. + ["scr_require.c", "computedRequire"], ["scr_dc.c", "dc"], // The dynamic tier's jsval values (two-tier design I3): linked only into // programs whose IR has dynamic-tier operations. @@ -80,15 +87,20 @@ export const OPTIONAL_RUNTIME_SOURCES = [ ["scr_perf.c", "dynAsync"], // perf_hooks event-loop metrics, only for programs that read them. ["scr_perf_loop.c", "perfLoop"], + // Module namespace objects as checked-dynamic values. + ["scr_module_namespace.c", "moduleNamespace"], // Blob and File box their reads as dyn promises through scr_async_dyn.c. ["scr_blob.c", "dynAsync"], ["scr_zlib.c", "zlib"], + ["scr_brotli.c", all("zlib", "brotli")], ["scr_events.c", "events"], ["scr_readline.c", "events"], ["scr_readline_tty.c", all("events", "regex")], ["scr_events_emitter.c", "emitter"], // events.once with options registers through the emitter and event-target units. ["scr_events_once_signal.c", "eventsOnceSignal"], + ["scr_mime.c", "mime"], + ["scr_util_timing.c", "utilTiming"], ["scr_dyn_handle.c", any("emitter", "netEffective")], ["scr_symbol.c", "symbol"], ["scr_bigint_assert.c", all("assert", "bigint")], @@ -130,6 +142,7 @@ export const OPTIONAL_RUNTIME_SOURCES = [ // Own properties JavaScript adds to children. ["scr_dyn_handle_props.c", "handleProps"], ["scr_decl_adopt.c", "declAdopt"], + ["scr_type_origin.c", "typeOrigins"], // for await over checked-dynamic values. ["scr_dyn_for_await.c", "dynForAwait"], ["scr_dyn_for_await_stream.c", all("dynForAwait", "stream", "dynAsync")], @@ -148,6 +161,8 @@ export const OPTIONAL_RUNTIME_SOURCES = [ ["scr_dns_promises.c", all("dgram", "dnsPromises")], // Sockets as untyped values run over the dgram unit. ["scr_dgram_dyn.c", all("dgram", "dgramDyn")], + // TextEncoder and TextDecoder records boxed as untyped values. + ["scr_text_codec_dyn.c", "textCodecDyn"], ["scr_watch.c", "watch"], ["scr_ffi_queue.c", "foreignFfi"], ["scr_test.c", "nodeTest"], @@ -161,6 +176,9 @@ export const OPTIONAL_RUNTIME_SOURCES = [ ["scr_webcrypto.c", "webcrypto"], ["scr_webcrypto_pki.c", all("webcrypto", "pki")], ["scr_ed25519.c", all("pki", "ed25519")], + ["scr_x509.c", "x509"], + // The TLS socket certificate accessors read scr_tls.c and build with scr_x509.c. + ["scr_tls_x509.c", all("tlsEffective", "x509")], // WebCrypto's AES ciphers run over the cipher feature's mbedTLS archive. ["scr_webcrypto_aes.c", all("cipher", "webcrypto")], ["scr_fetch.c", "nativeFetch"], @@ -248,6 +266,7 @@ export function createRuntimePackMatrix({ { id: "quickjs", predicate: false }, { id: "libregexp", predicate: "regex" }, { id: "zlib", predicate: "zlibEffective" }, + { id: "brotli", predicate: all("zlib", "brotli") }, { id: "mbedtls", predicate: any("tlsEffective", "cipher", "ecdh", "dh", "pki") }, ]; const matrix = withLibraryRuntimeFlavors( diff --git a/packages/runtime-pack-common/scripts/build.mjs b/packages/runtime-pack-common/scripts/build.mjs index b31ebe819..2e4efd10e 100644 --- a/packages/runtime-pack-common/scripts/build.mjs +++ b/packages/runtime-pack-common/scripts/build.mjs @@ -15,6 +15,7 @@ import { import { createDeterministicArchive } from "./archive.mjs"; import { buildImportBitcode, buildRuntimeUnit, resolveRuntimeUnitHelper } from "./runtime-unit.mjs"; import { installRuntimePack, withBuildLock } from "./build-state.mjs"; +import { shareIdenticalArtifacts } from "./share-artifacts.mjs"; import { parallelMap } from "./parallel-map.mjs"; const run = promisify(execFile); @@ -85,6 +86,7 @@ async function build() { const quickjs = join(vendorRoot, "quickjs-ng"); const zlib = join(vendorRoot, "zlib"); const mbedtls = join(vendorRoot, "mbedtls"); + const brotli = join(vendorRoot, "brotli", "c"); const qjsSources = ["dtoa.c", "libregexp.c", "libunicode.c", "quickjs.c"]; const lreSources = ["libregexp.c", "libunicode.c"]; const zlibSources = [ @@ -131,6 +133,9 @@ async function build() { const width = Math.max(1, Math.min(8, availableParallelism())); return parallelMap(items, width, task); }; + // Archive members are named by basename, so a source in a subdirectory + // (brotli's dec/ and enc/ share file names) is named after its path. + const objectName = (source) => source.replaceAll("/", "-").replace(/\.c$/, ".o"); const archive = async (id, sources, sourceRoot, flags) => { process.stdout.write(`building ${packageManifest.name} ${id} archive\n`); const root = join(stagedOutputRoot, "vendor", id); @@ -138,7 +143,7 @@ async function build() { await parallel(sources, async (source) => compile( join(sourceRoot, source), - join(objectRoot, source.replace(/\.c$/, ".o")), + join(objectRoot, objectName(source)), unalignedVendorSources.has(source) ? flags.filter((flag) => !codeAlignment.includes(flag)) : flags, @@ -148,7 +153,7 @@ async function build() { await createDeterministicArchive( archiver, output, - sources.map((source) => join(objectRoot, source.replace(/\.c$/, ".o"))), + sources.map((source) => join(objectRoot, objectName(source))), archiverArgs, ); await rm(objectRoot, { recursive: true, force: true }); @@ -232,6 +237,7 @@ async function build() { ...(unit.source === "scr_zlib.c" || unit.source === "scr_fetch.c" ? ["-I", zlib] : []), + ...(unit.source === "scr_brotli.c" ? ["-I", join(brotli, "include")] : []), ]; // Worker variants link only into executables, whose own TLS block // holds every runtime thread-local: on ELF the initial-exec model @@ -334,6 +340,17 @@ async function build() { const mbedtlsSources = (await readdir(join(mbedtls, "library"))) .filter((name) => !name.startsWith(".") && name.endsWith(".c")) .sort(); + const brotliSources = ( + await Promise.all( + ["common", "dec", "enc"].map(async (dir) => + (await readdir(join(brotli, dir))) + .filter((name) => !name.startsWith(".") && name.endsWith(".c")) + .map((name) => `${dir}/${name}`), + ), + ) + ) + .flat() + .sort(); // Vendored QuickJS reads `clock_gettime` for its monotonic clock. WASI // exposes that declaration only when its process-clock emulation ABI is // selected, so vendor sources inherit the target's runtime defines too. @@ -370,6 +387,10 @@ async function build() { ? [ await archive("libregexp", lreSources, quickjs, [ ...vendorTarget, + // Section elimination keeps only the libunicode tables a + // program reaches: the URL host parser's NFC links the + // normalization tables without the regex property ones. + ...matrix.executable_section_elimination.compile_flags, "-std=c11", "-Os", "-I", @@ -388,6 +409,17 @@ async function build() { ]), ] : []), + ...(requestedArchives.has("brotli") + ? [ + await archive("brotli", brotliSources, brotli, [ + ...vendorTarget, + "-std=c11", + "-Os", + "-I", + join(brotli, "include"), + ]), + ] + : []), ...(requestedArchives.has("mbedtls") ? [ await archive("mbedtls", mbedtlsSources, join(mbedtls, "library"), [ @@ -416,7 +448,13 @@ async function build() { "artifacts/licenses/v8-number-radix.txt", "BSD-3-Clause", ], + [ + join(vendorRoot, "v8-ieee754", "LICENSE"), + "artifacts/licenses/v8-ieee754.txt", + "BSD-3-Clause", + ], [join(zlib, "LICENSE"), "artifacts/licenses/zlib.txt", "Zlib"], + [join(vendorRoot, "brotli", "LICENSE"), "artifacts/licenses/brotli.txt", "MIT"], [join(mbedtls, "LICENSE"), "artifacts/licenses/mbedtls.txt", "Apache-2.0"], ]; licensed.push([ @@ -431,13 +469,14 @@ async function build() { await copyFile(source, output); }), ); + await shareIdenticalArtifacts(flavors, buildRoot); const manifest = { schema: "scriptc.runtime-pack.v1", format: 1, package: packageManifest.name, version: packageManifest.version, target: matrix.target, - runtime_abi: { version: 37, marker: "scr_runtime_abi_v37" }, + runtime_abi: { version: 47, marker: "scr_runtime_abi_v47" }, compiler: { command: compiler, identity: compilerVersion, diff --git a/packages/runtime-pack-common/scripts/share-artifacts.mjs b/packages/runtime-pack-common/scripts/share-artifacts.mjs new file mode 100644 index 000000000..8eaf0707c --- /dev/null +++ b/packages/runtime-pack-common/scripts/share-artifacts.mjs @@ -0,0 +1,48 @@ +import { readdir, rm, rmdir } from "node:fs/promises"; +import { join } from "node:path"; + +/** Points the artifacts of one runtime unit that hold the same bytes at one + * file and deletes the copies. Variants whose defines a unit does not read, + * and flavors that compile a unit the same way, produce identical objects + * and bitcode. Each link selects one flavor and one variant of each unit, so + * no link names a shared file twice. `root` is the directory artifact paths + * are relative to; flavors keep the order they were built in, so the first + * flavor's path holds the shared file. */ +export async function shareIdenticalArtifacts(flavors, root) { + const firstPaths = new Map(); + for (const flavor of Object.values(flavors)) { + for (const unit of flavor.runtime_units) { + let paths = firstPaths.get(unit.source); + if (paths === undefined) { + paths = new Map(); + firstPaths.set(unit.source, paths); + } + for (const variant of unit.variants) { + for (const artifact of variant.bitcode === undefined + ? [variant] + : [variant, variant.bitcode]) { + const first = paths.get(artifact.sha256); + if (first === undefined) { + paths.set(artifact.sha256, artifact.path); + } else if (first !== artifact.path) { + await rm(join(root, artifact.path)); + artifact.path = first; + } + } + } + } + } + await removeEmptyDirectories(join(root, "artifacts")); +} + +async function removeEmptyDirectories(directory) { + const entries = await readdir(directory, { withFileTypes: true }); + let remaining = entries.length; + for (const entry of entries) { + if (entry.isDirectory() && (await removeEmptyDirectories(join(directory, entry.name)))) + remaining--; + } + if (remaining > 0) return false; + await rmdir(directory); + return true; +} diff --git a/packages/runtime-pack-common/test/share-artifacts.test.ts b/packages/runtime-pack-common/test/share-artifacts.test.ts new file mode 100644 index 000000000..a2cb68128 --- /dev/null +++ b/packages/runtime-pack-common/test/share-artifacts.test.ts @@ -0,0 +1,79 @@ +import { existsSync } from "node:fs"; +import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { expect, test } from "vitest"; +import { shareIdenticalArtifacts } from "../scripts/share-artifacts.mjs"; + +type Artifact = { path: string; sha256: string; size: number; bitcode?: Artifact }; + +async function artifact(root: string, path: string, sha256: string): Promise { + await mkdir(dirname(join(root, path)), { recursive: true }); + await writeFile(join(root, path), sha256); + return { path, sha256, size: sha256.length }; +} + +test("identical artifacts of one unit share the first flavor's file", async () => { + const root = await mkdtemp(join(tmpdir(), "scriptc-share-artifacts-")); + try { + const unit = async (flavor: string, source: string, ids: Record) => ({ + source, + variants: await Promise.all( + Object.entries(ids).map(async ([id, sha]) => ({ + id, + ...(await artifact(root, `artifacts/${flavor}/runtime/${id}/${source}.o`, sha)), + })), + ), + }); + const release = { + runtime_units: [ + await unit("release", "scr_a", { default: "a1", "default-workers": "a1" }), + await unit("release", "scr_b", { default: "b1" }), + ], + }; + release.runtime_units[1]!.variants[0]!.bitcode = await artifact( + root, + "artifacts/release/runtime/default/scr_b.bc", + "b-bitcode", + ); + const speed = { + runtime_units: [ + await unit("speed", "scr_a", { default: "a2" }), + // The same bytes as another unit's object stay a separate file. + await unit("speed", "scr_b", { default: "b1", other: "a1" }), + ], + }; + speed.runtime_units[1]!.variants[0]!.bitcode = await artifact( + root, + "artifacts/speed/runtime/default/scr_b.bc", + "b-bitcode", + ); + const flavors = { release, speed }; + await shareIdenticalArtifacts(flavors, root); + const paths = (flavor: typeof release) => + flavor.runtime_units.map((unit) => + unit.variants.map((variant: Artifact) => [variant.path, variant.bitcode?.path]), + ); + expect(paths(release)).toEqual([ + [ + ["artifacts/release/runtime/default/scr_a.o", undefined], + ["artifacts/release/runtime/default/scr_a.o", undefined], + ], + [["artifacts/release/runtime/default/scr_b.o", "artifacts/release/runtime/default/scr_b.bc"]], + ]); + expect(paths(speed)).toEqual([ + [["artifacts/speed/runtime/default/scr_a.o", undefined]], + [ + ["artifacts/release/runtime/default/scr_b.o", "artifacts/release/runtime/default/scr_b.bc"], + ["artifacts/speed/runtime/other/scr_b.o", undefined], + ], + ]); + expect(existsSync(join(root, "artifacts/release/runtime/default-workers"))).toBe(false); + expect(existsSync(join(root, "artifacts/speed/runtime/default/scr_b.o"))).toBe(false); + expect(existsSync(join(root, "artifacts/speed/runtime/default/scr_b.bc"))).toBe(false); + expect(existsSync(join(root, "artifacts/speed/runtime/default/scr_a.o"))).toBe(true); + expect(existsSync(join(root, "artifacts/speed/runtime/other/scr_b.o"))).toBe(true); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); diff --git a/packages/runtime-wasm32-wasi/package.json b/packages/runtime-wasm32-wasi/package.json index 5bbd23755..799863cf0 100644 --- a/packages/runtime-wasm32-wasi/package.json +++ b/packages/runtime-wasm32-wasi/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-wasm32-wasi", - "version": "0.4.2", + "version": "0.4.5", "description": "Precompiled scriptc runtime pack for WASI Preview 1", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-wasm32-wasi/scripts/build.mjs b/packages/runtime-wasm32-wasi/scripts/build.mjs index cbe744e4e..2a2766f4b 100644 --- a/packages/runtime-wasm32-wasi/scripts/build.mjs +++ b/packages/runtime-wasm32-wasi/scripts/build.mjs @@ -26,6 +26,7 @@ process.env.SCRIPTC_RUNTIME_PACK_CONFIG = JSON.stringify({ "scr_pki.c", "scr_webcrypto_pki.c", "scr_ed25519.c", + "scr_tls_x509.c", "scr_webcrypto_aes.c", "scr_wasm.c", "scr_fetch.c", diff --git a/packages/runtime-win32-x64-msvc/package.json b/packages/runtime-win32-x64-msvc/package.json index df4a9a109..421f1f71f 100644 --- a/packages/runtime-win32-x64-msvc/package.json +++ b/packages/runtime-win32-x64-msvc/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-win32-x64-msvc", - "version": "0.4.2", + "version": "0.4.5", "description": "Precompiled scriptc runtime pack for Windows x64 MSVC", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 9232266af..90140ab15 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime", - "version": "0.4.2", + "version": "0.4.5", "description": "scriptc native runtime sources and vendored dependencies", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime/src/scr_alloc.c b/packages/runtime/src/scr_alloc.c index 3a36f672e..087c926f7 100644 --- a/packages/runtime/src/scr_alloc.c +++ b/packages/runtime/src/scr_alloc.c @@ -567,7 +567,128 @@ static _Atomic(uintptr_t) scr_sa_heap_page; /* the page size, read with the mode * These loads run only on blocks too large for the small allocator, next to * a glibc malloc call: plain loads on x86-64, LDAR (or LDAPR) on arm64. */ +#if defined(SCR_WORKERS) && !defined(SCR_LIB) +#if defined(__aarch64__) +#include +#endif +/* glibc's per-thread heaps (worker executables; library archives run on + * their embedder's threads and leave its heaps alone). glibc serves a thread's + * blocks from a heap of its own: a 64 MiB mapping, aligned to its size, + * that it makes accessible with mprotect as it grows, a page-rounded step + * at a time, so every 2 MiB range is first touched while part of it is + * still inaccessible and gets small pages. Once a thread has allocated + * SCR_SA_THREAD_HUGE_AFTER bytes of large blocks (threads with few keep + * small pages), the heap of each large block it gets is made accessible + * whole and advised huge pages, once per heap; glibc's calloc still clears + * only what lies below its own high-water mark, so this touches nothing. + * glibc's mmap and trim thresholds are fixed at SCR_SA_THREAD_MMAP_THRESHOLD + * and SCR_SA_THREAD_TRIM_THRESHOLD: lower than the maxima glibc's adaptive + * thresholds reach, so that blocks of 4 MiB and more get mappings of their + * own and a heap's free top goes back once it passes 8 MiB, while huge + * pages keep resident memory that a heap would otherwise leave untouched. + * The brk heap policy below sets its own (higher) values when the main heap + * grows past SCR_SA_HEAP_HUGE_AFTER, and those take precedence. The + * policy is off unless THP is in madvise mode, when the glibc.malloc.hugetlb + * tunable is set (glibc then advises its heaps itself, or sizes them by + * hugetlbfs pages), when the system forbids overcommit (making a heap + * accessible would commit all of it), when malloc is not glibc's (a + * sanitizer or a preloaded allocator: blocks have no glibc chunk header), + * and when memory tagging is on (arm64 MTE, the glibc.mem.tagging tunable): + * a chunk header then carries another tag than the block's pointer, and + * reading it through that pointer faults. + * A heap is only advised when /proc/self/maps shows the anonymous mappings + * of exactly its range. */ +#define SCR_SA_GLIBC_HEAP ((uintptr_t)64 << 20) /* glibc's heap size and alignment */ +#define SCR_SA_THREAD_HUGE_AFTER ((size_t)16 << 20) +#define SCR_SA_THREAD_MMAP_THRESHOLD (4 << 20) +#define SCR_SA_THREAD_TRIM_THRESHOLD (8 << 20) +static _Thread_local size_t scr_sa_thread_large_bytes; +static _Thread_local uintptr_t scr_sa_thread_heap; /* the heap this thread advised last */ +static _Atomic(int) scr_sa_thread_heaps; /* 0 undecided, 1 advised, 2 off */ +extern void *__libc_malloc(size_t); + +/* Whether glibc tags heap memory: the glibc.mem.tagging tunable is set to a + * nonzero value, or (arm64) the kernel checks tags for this process. */ +static bool scr_sa_mem_tagging(const char *tunables) { + const char *t = tunables ? strstr(tunables, "glibc.mem.tagging=") : NULL; + if (t && t[sizeof "glibc.mem.tagging=" - 1] != '0') return true; +#if defined(__aarch64__) + /* PR_GET_TAGGED_ADDR_CTRL (56); PR_MTE_TCF_MASK is bits 1-2. A kernel + * without the call returns -1: no tag checks. */ + long ctrl = prctl(56, 0, 0, 0, 0); + if (ctrl > 0 && (ctrl & (3L << 1))) return true; +#endif + return false; +} + +/* Whether [heap, heap + SCR_SA_GLIBC_HEAP) is covered exactly by anonymous + * mappings, as a glibc heap is (its accessible part and the rest). */ +static bool scr_sa_glibc_heap_mapped(uintptr_t heap) { + FILE *f = fopen("/proc/self/maps", "r"); + if (!f) return false; + char line[512]; + uintptr_t next = heap, end = heap + SCR_SA_GLIBC_HEAP; + while (next < end && fgets(line, sizeof line, f)) { + unsigned long lo, hi, inode; + char path[2] = {0}; + if (sscanf(line, "%lx-%lx %*s %*s %*s %lu %1s", &lo, &hi, &inode, path) < 3) continue; + if (hi <= next) continue; + if (lo != next || inode != 0 || path[0]) break; + next = hi; + } + fclose(f); + return next == end; +} + +static void scr_sa_thread_heap_note(void *p, size_t n) { + if (scr_sa_thread_large_bytes < SCR_SA_THREAD_HUGE_AFTER) { + scr_sa_thread_large_bytes += n; + return; + } + int mode = atomic_load_explicit(&scr_sa_thread_heaps, memory_order_relaxed); + if (mode == 2) return; + if (mode == 0) { + int undecided = 0; + const char *tunables = getenv("GLIBC_TUNABLES"); + FILE *f = fopen("/proc/sys/vm/overcommit_memory", "r"); + int overcommit = f ? fgetc(f) : '0'; + if (f) fclose(f); + /* Loaded through volatile: the compiler takes two declared functions + * to have different addresses. */ + void *(*volatile used)(size_t) = malloc, *(*volatile own)(size_t) = __libc_malloc; + bool glibc_malloc = used == own; + mode = glibc_malloc && scr_sa_thp_mode() == 'm' && !(tunables && strstr(tunables, "glibc.malloc.hugetlb")) && + overcommit != '2' && !scr_sa_mem_tagging(tunables) + ? 1 + : 2; + if (atomic_compare_exchange_strong(&scr_sa_thread_heaps, &undecided, mode) && mode == 1 && + atomic_load_explicit(&scr_sa_heap_state, memory_order_relaxed) != 1) { + (void)mallopt(M_MMAP_THRESHOLD, SCR_SA_THREAD_MMAP_THRESHOLD); + (void)mallopt(M_TRIM_THRESHOLD, SCR_SA_THREAD_TRIM_THRESHOLD); + /* The brk heap policy marks itself on before it sets its values: if it + * did so meanwhile, its values are set again. */ + if (atomic_load(&scr_sa_heap_state) == 1) { + (void)mallopt(M_MMAP_THRESHOLD, SCR_SA_HEAP_MMAP_THRESHOLD); + (void)mallopt(M_TRIM_THRESHOLD, SCR_SA_HEAP_TRIM_THRESHOLD); + } + } + mode = atomic_load_explicit(&scr_sa_thread_heaps, memory_order_relaxed); + if (mode != 1) return; + } + /* glibc's chunk header: NON_MAIN_ARENA (4) set, IS_MMAPPED (2) clear. */ + if ((((const size_t *)p)[-1] & 6) != 4) return; + uintptr_t heap = (uintptr_t)p & ~(SCR_SA_GLIBC_HEAP - 1); + if (heap == scr_sa_thread_heap) return; + scr_sa_thread_heap = heap; + if (scr_sa_glibc_heap_mapped(heap) && mprotect((void *)heap, SCR_SA_GLIBC_HEAP, PROT_READ | PROT_WRITE) == 0) + (void)madvise((void *)heap, SCR_SA_GLIBC_HEAP, MADV_HUGEPAGE); +} +#endif + static void scr_sa_large_note(void *p, size_t n) { +#if defined(SCR_WORKERS) && !defined(SCR_LIB) + if (p) scr_sa_thread_heap_note(p, n); +#endif int state = atomic_load_explicit(&scr_sa_heap_state, memory_order_relaxed); if (state == 2 || !p) return; uintptr_t top = (uintptr_t)sbrk(0); @@ -1995,6 +2116,99 @@ void scr_heap_stats(double *total, double *used) { *used = (double)(held - (idle < held ? idle : held) + sys_used); } +/* scr_heap_trim: returns the heap pages that hold only freed memory to the + * system after a phase's data died (scr_collect_cycles_phase). The + * size-class slices keep freed blocks on their class's free list, which only + * later blocks of the same class reuse, so a program whose next phase + * allocates other sizes would otherwise grow its resident memory on top of + * the freed heap. */ +#if defined(__GLIBC__) && !defined(SCR_HEAP_SANITIZER) +#include +#endif +#if SCR_SMALL_ALLOC && !defined(SCR_WORKERS) && defined(__linux__) && defined(MADV_DONTNEED) +#include +#include + +/* Returns the pages of class c's slice below its frontier that hold only + * free blocks (MADV_DONTNEED), and relinks the class's other free blocks in + * address order. A block wholly inside a returned page leaves the free list: + * the class carves new blocks past its frontier instead, and the slice's + * reservation is far larger than what a program fills. */ +static void scr_sa_return_free_pages(unsigned c, size_t page) { + size_t sz = scr_sa_class_size(c); + char *start = (char *)scr_sa.base + ((size_t)c << scr_sa.shift); + char *top = scr_sa.bump[c]; + size_t nblocks = (size_t)(top - start) / sz; + if (!nblocks || !scr_sa.free[c]) return; + size_t words = (nblocks + 63) / 64; + uint64_t *bits = mmap(NULL, words * 8, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0); + if (bits == MAP_FAILED) return; + for (ScrSaBlock *b = scr_sa.free[c]; b; b = b->next) { + size_t k = (size_t)((char *)b - start) / sz; + bits[k / 64] |= (uint64_t)1 << (k % 64); + } +#define SCR_SA_FREE_BIT(k) ((bits[(k) / 64] >> ((k) % 64)) & 1) + /* Mark the returned pages' blocks by clearing their bits after the + * madvise; a page is returned when every block overlapping it is free. */ + uintptr_t first = ((uintptr_t)start + page - 1) & ~(uintptr_t)(page - 1); + uintptr_t last = (uintptr_t)top & ~(uintptr_t)(page - 1); + char *run = NULL; + for (uintptr_t pg = first; pg <= last; pg += page) { + bool empty = false; + if (pg < last) { + size_t k0 = (size_t)((char *)pg - start) / sz; + size_t k1 = (size_t)((char *)pg + page - 1 - start) / sz; + if (k1 >= nblocks) k1 = nblocks - 1; + empty = true; + for (size_t k = k0; k <= k1 && empty; k++) empty = SCR_SA_FREE_BIT(k); + } + if (empty && !run) run = (char *)pg; + if (!empty && run) { + (void)madvise(run, (size_t)((char *)pg - run), MADV_DONTNEED); + /* Blocks wholly inside the returned pages are dropped. */ + size_t k0 = ((size_t)(run - start) + sz - 1) / sz; + size_t k1 = (size_t)((char *)pg - start) / sz; + for (size_t k = k0; k < k1 && (char *)start + (k + 1) * sz <= (char *)pg; k++) + bits[k / 64] &= ~((uint64_t)1 << (k % 64)); + run = NULL; + } + } + /* The kept free blocks, in address order. */ + ScrSaBlock *head = NULL, **tail = &head; + for (size_t w = 0; w < words; w++) + for (uint64_t m = bits[w]; m; m &= m - 1) { + ScrSaBlock *b = (ScrSaBlock *)(start + (w * 64 + (size_t)__builtin_ctzll(m)) * sz); + *tail = b; + tail = &b->next; + } + *tail = NULL; + scr_sa.free[c] = head; +#undef SCR_SA_FREE_BIT + (void)munmap(bits, words * 8); +} +#endif + +void scr_heap_trim(void) { +#ifdef SCR_SA_TRIM + /* A worker runtime returns the calling thread's arena pages as a parking + * thread does; the arena stays the thread's own. */ + if (scr_sa_arena >= 0 && scr_sa_trim_worth()) { + scr_sa_trim(); + return; + } +#elif SCR_SMALL_ALLOC && !defined(SCR_WORKERS) && defined(__linux__) && defined(MADV_DONTNEED) + if (scr_sa.span) { + long page = sysconf(_SC_PAGESIZE); + for (unsigned c = 0; c < SCR_SA_NCLASS; c++) + if (scr_sa.lim[c]) scr_sa_return_free_pages(c, page > 0 ? (size_t)page : 4096); + } +#endif +#if defined(__GLIBC__) && !defined(SCR_HEAP_SANITIZER) + /* Large blocks: the system allocator's free pages too. */ + (void)malloc_trim(0); +#endif +} + /* ── program-lifetime region ──────────────────────────────────────────── * Instances of compact classes (llvm/classes.ts LlClassMeta.compact) in * programs built for Linux without sanitizers (runtimes with the size-class diff --git a/packages/runtime/src/scr_alloc.test.c b/packages/runtime/src/scr_alloc.test.c index e20c0733b..dfdd9bf5c 100644 --- a/packages/runtime/src/scr_alloc.test.c +++ b/packages/runtime/src/scr_alloc.test.c @@ -343,6 +343,60 @@ static void *reclaim_arena(void *arg) { return NULL; } +#if defined(__linux__) && defined(__GLIBC__) +/* A thread whose large blocks pass 16 MiB gets them from a glibc heap of + * its own, which is then advised huge pages where THP is in madvise mode. */ +enum { HEAP_BLOCKS = 320, HEAP_BLOCK = 64 << 10 }; +static void *heap_blocks[HEAP_BLOCKS]; +static char heap_flags[256]; +static void *thread_heap_allocate(void *arg) { + (void)arg; + for (unsigned i = 0; i < HEAP_BLOCKS; i++) { + heap_blocks[i] = scr_mem_alloc(HEAP_BLOCK); + assert(heap_blocks[i] && !owned(heap_blocks[i])); + ((unsigned char *)heap_blocks[i])[0] = (unsigned char)i; + } + vm_flags(heap_blocks[HEAP_BLOCKS - 1], heap_flags, sizeof heap_flags); + for (unsigned i = 0; i < HEAP_BLOCKS; i++) { + assert(((unsigned char *)heap_blocks[i])[0] == (unsigned char)i); + scr_mem_free(heap_blocks[i]); + } + scr_sa_thread_park(); + return NULL; +} + +static void check_thread_heap_huge_pages(void) { + pthread_t t; + assert(pthread_create(&t, NULL, thread_heap_allocate, NULL) == 0); + assert(pthread_join(t, NULL) == 0); + char mode[96] = {0}; + FILE *thp = fopen("/sys/kernel/mm/transparent_hugepage/enabled", "r"); + if (thp) { + size_t n = fread(mode, 1, sizeof mode - 1, thp); + mode[n] = '\0'; + fclose(thp); + } + FILE *f = fopen("/proc/sys/vm/overcommit_memory", "r"); + int overcommit = f ? fgetc(f) : '0'; + if (f) fclose(f); + /* ThreadSanitizer replaces malloc, which turns the policy off. */ + bool glibc_malloc = true; +#if defined(__SANITIZE_THREAD__) + glibc_malloc = false; +#elif defined(__has_feature) +#if __has_feature(thread_sanitizer) + glibc_malloc = false; +#endif +#endif + /* With glibc's memory tagging tunable set the heap is left alone: under + * arm64 MTE, reading its chunk headers through block pointers faults. */ + const char *tunables = getenv("GLIBC_TUNABLES"); + if (tunables && strstr(tunables, "glibc.mem.tagging=3")) assert(!strstr(heap_flags, " hg")); + else if (glibc_malloc && strstr(mode, "[madvise]") && !tunables && overcommit != '2') + assert(strstr(heap_flags, " hg")); +} +#endif + static void check_threads(void) { void *mine = scr_mem_alloc(48); assert(owned(mine)); @@ -1047,6 +1101,9 @@ int main(void) { setenv("SCR_SA_DISCARD_DELAY_MS", "100", 1); setenv("SCR_SA_DISCARD_DELAY_MAX_MS", "0", 1); check_threads(); +#if defined(__linux__) && defined(__GLIBC__) + check_thread_heap_huge_pages(); +#endif #ifdef __linux__ check_trim(); check_release_frontiers(); diff --git a/packages/runtime/src/scr_alloc.test.ts b/packages/runtime/src/scr_alloc.test.ts index 187ffce51..0ceda33cd 100644 --- a/packages/runtime/src/scr_alloc.test.ts +++ b/packages/runtime/src/scr_alloc.test.ts @@ -11,13 +11,20 @@ const exec = promisify(execFile); // UBSan-only build is the one that exercises it; the ASan build pins the // system-allocator bypass the sanitizer lane relies on. // Worker executables (-DSCR_WORKERS) give each script thread its own arena; -// the ThreadSanitizer build checks the cross-thread handoff for races. +// the ThreadSanitizer build checks the cross-thread handoff for races. The +// last build runs with glibc's memory tagging tunable set, which turns the +// thread-heap huge-page policy off. test.each([ - ["size-class", ["-fsanitize=undefined"]], - ["system", ["-fsanitize=address,undefined"]], - ["size-class workers", ["-fsanitize=undefined", "-DSCR_WORKERS", "-pthread"]], - ["size-class workers", ["-fsanitize=thread", "-DSCR_WORKERS", "-pthread"]], -])("small-object allocator contracts hold (%s, %j)", async (mode, sanitize) => { + ["size-class", ["-fsanitize=undefined"], {}], + ["system", ["-fsanitize=address,undefined"], {}], + ["size-class workers", ["-fsanitize=undefined", "-DSCR_WORKERS", "-pthread"], {}], + ["size-class workers", ["-fsanitize=thread", "-DSCR_WORKERS", "-pthread"], {}], + [ + "size-class workers", + ["-fsanitize=undefined", "-DSCR_WORKERS", "-pthread"], + { GLIBC_TUNABLES: "glibc.mem.tagging=3" }, + ], +])("small-object allocator contracts hold (%s, %j, %j)", async (mode, sanitize, extraEnv) => { const scratch = await mkdtemp(join(tmpdir(), "scriptc-alloc-")); try { const binary = join(scratch, "alloc"); @@ -41,6 +48,7 @@ test.each([ ASAN_OPTIONS: "halt_on_error=1", TSAN_OPTIONS: "halt_on_error=1", UBSAN_OPTIONS: "halt_on_error=1", + ...extraEnv, }, }); expect(result.stdout).toBe(`allocator checks passed: ${mode}\n`); diff --git a/packages/runtime/src/scr_array.c b/packages/runtime/src/scr_array.c index 91328a1e8..b9e0efa74 100644 --- a/packages/runtime/src/scr_array.c +++ b/packages/runtime/src/scr_array.c @@ -269,11 +269,57 @@ static uint64_t scr_elem_retain_slot(const ScrArr *a, uint64_t slot) { return scr_elem_retain_with(a->elem, scr_arr_ops(a), slot); } +/* ToNumber and ToString of a foreign element. Both can run user code and + * pull in the dynamic tier, so they are reached through this table, which + * scr_arr_set_foreign installs: every foreign element is stored by it, and + * a program that never stores one links neither conversion. */ +typedef struct ScrArrForeignConversions { + double (*to_number)(const ScrDyn *); + ScrStr *(*to_string)(const ScrDyn *); +} ScrArrForeignConversions; + +static const ScrArrForeignConversions scr_arr_dyn_conversions = { + scr_dyn_number_coerce, + scr_dyn_string_coerce_js, +}; +static const ScrArrForeignConversions *scr_arr_conversions; + +static const ScrArrForeignConversions *scr_arr_foreign_conversions(void) { + return __atomic_load_n(&scr_arr_conversions, __ATOMIC_RELAXED); +} + +/* The array may hold SCR_ARR_FOREIGN elements (ScrArrExt.foreign). Such + * an array always has states. */ +static inline bool scr_arr_may_hold_foreign(const ScrArr *a) { + return SCR_AX(a)->foreign; +} + +/* `dst` takes slots of `src` (moved or retained): it may hold foreign + * elements too. Marking allocates the out-of-line storage, which can move + * the dense slots: mark before taking pointers into them. */ +static inline void scr_arr_inherit_foreign(ScrArr *dst, const ScrArr *src) { + if (scr_arr_may_hold_foreign(src)) scr_arr_ext(dst)->foreign = true; +} + static uint64_t scr_arr_retain_state_slot(const ScrArr *a, uint64_t slot, uint8_t state) { + if (state == SCR_ARR_FOREIGN) { + scr_dyn_retain(scr_slot_to_ptr(slot)); + return slot; + } return state == SCR_ARR_VALUE ? scr_elem_retain_slot(a, slot) : 0; } +/* Releases what a slot in `state` owns: a reference element, or the value + * of a foreign element. */ +static void scr_arr_release_state_slot(const ScrArr *a, uint64_t slot, uint8_t state) { + if (state == SCR_ARR_VALUE) { + if (scr_elem_is_ref(a->elem)) scr_elem_release(a, slot); + } else if (state == SCR_ARR_FOREIGN) { + scr_dyn_release(scr_slot_to_ptr(slot)); + } +} + /* ── storage and lifecycle ─────────────────────────────────────────────── */ typedef struct { @@ -981,6 +1027,7 @@ static inline bool scr_arr_slot_at(const ScrArr *a, size_t index, uint64_t *out) static void scr_arr_store_state_owned(ScrArr *a, size_t index, uint64_t slot, uint8_t state) { if (state != SCR_ARR_VALUE) scr_arr_cut(a, index); + if (state == SCR_ARR_FOREIGN) scr_arr_ext(a)->foreign = true; if (!scr_arr_has_states(a) && (state != SCR_ARR_VALUE || index != a->len)) scr_arr_add_states(a); scr_arr_grow_for_index(a, index); if (index < a->cap) { @@ -1034,6 +1081,7 @@ static void scr_arr_replace_state_owned(ScrArr *a, size_t index, uint64_t slot, uint8_t old_state; uint64_t old = 0; if (state != SCR_ARR_VALUE) scr_arr_cut(a, index); + if (state == SCR_ARR_FOREIGN) scr_arr_ext(a)->foreign = true; /* Without states, a value below the length replaces a value. */ if (!scr_arr_has_states(a) && (state != SCR_ARR_VALUE || index >= a->len)) { if (state == SCR_ARR_VALUE && index == a->len) { @@ -1044,7 +1092,7 @@ static void scr_arr_replace_state_owned(ScrArr *a, size_t index, uint64_t slot, } if (index < a->cap) { old_state = scr_arr_dense_state(a, index); - if (old_state == SCR_ARR_VALUE) old = scr_arr_dense_get(a, index); + if (old_state == SCR_ARR_VALUE || old_state == SCR_ARR_FOREIGN) old = scr_arr_dense_get(a, index); scr_arr_dense_put(a, index, slot); if (scr_arr_has_states(a)) *scr_arr_state_ptr(a, index) = state; } else { @@ -1058,7 +1106,7 @@ static void scr_arr_replace_state_owned(ScrArr *a, size_t index, uint64_t slot, SCR_AX(a)->sparse[pos].slot = slot; SCR_AX(a)->sparse[pos].state = state; } - if (old_state == SCR_ARR_VALUE && scr_elem_is_ref(a->elem)) scr_elem_release(a, old); + scr_arr_release_state_slot(a, old, old_state); } static void scr_arr_replace_owned(ScrArr *a, size_t index, uint64_t slot) { @@ -1300,6 +1348,7 @@ static void scr_arr_copy_dense(ScrArr *dst, size_t to, const ScrArr *src, size_t from, size_t count, bool reverse, bool materialize_holes) { if (count == 0) return; + scr_arr_inherit_foreign(dst, src); scr_arr_cut(dst, to); /* 8-byte slots: a constant slot size, and constant-width state ends. */ size_t width = scr_arr_slot_width(dst); @@ -1350,12 +1399,15 @@ static void scr_arr_copy_dense(ScrArr *dst, size_t to, const ScrArr *src, *scr_arr_state_in(dst_states, to + i) = values ? SCR_ARR_VALUE : *scr_arr_state_in(src_states, source); } } - if (!refs && (!materialize_holes || values)) return; + bool foreign = !values && scr_arr_may_hold_foreign(src); + if (!refs && !foreign && (!materialize_holes || values)) return; uint8_t elem = src->elem; const ScrArrOps *ops = scr_arr_ops(src); for (size_t i = to; i < to + count; i++) { if (*scr_arr_state_in(dst_states, i) == SCR_ARR_VALUE) { if (refs) dst->data[i] = scr_elem_retain_with(elem, ops, dst->data[i]); + } else if (*scr_arr_state_in(dst_states, i) == SCR_ARR_FOREIGN) { + scr_dyn_retain(scr_slot_to_ptr(dst->data[i])); } else if (materialize_holes && *scr_arr_state_in(dst_states, i) == SCR_ARR_HOLE) { scr_arr_dense_put(dst, i, 0); *scr_arr_state_in(dst_states, i) = SCR_ARR_UNDEFINED; @@ -1477,11 +1529,13 @@ void scr_arr_trace_v(void *a0, ScrTraceVisit visit, void *ctx) { } else { const uint8_t *states = scr_arr_states_end(a); for (size_t i = 0; i < end; i++) { - if (*scr_arr_state_in(states, i) == SCR_ARR_VALUE) visit(scr_slot_to_ptr(a->data[i]), ctx); + uint8_t state = *scr_arr_state_in(states, i); + if (state == SCR_ARR_VALUE || state == SCR_ARR_FOREIGN) visit(scr_slot_to_ptr(a->data[i]), ctx); } } for (size_t i = 0; i < SCR_AX(a)->sparse_len; i++) { - if (SCR_AX(a)->sparse[i].state == SCR_ARR_VALUE) { + uint8_t state = SCR_AX(a)->sparse[i].state; + if (state == SCR_ARR_VALUE || state == SCR_ARR_FOREIGN) { visit(scr_slot_to_ptr(SCR_AX(a)->sparse[i].slot), ctx); } } @@ -1590,11 +1644,37 @@ static inline __attribute__((always_inline)) void scr_arr_free(ScrArr *a) { scr_mem_free(a); } -static void scr_arr_destroy(void *object) { - ScrArr *a = object; +/* Releases the values of foreign elements and leaves holes in their place, + * so the release below sees only typed elements. Arrays that hold foreign + * elements have states; a copy carries the mark of its source whatever + * range it takes, so an array with the mark and no states holds only + * values in its dense slots. */ +static void scr_arr_release_foreign(ScrArr *a) { + uint8_t *states = scr_arr_has_states(a) ? scr_arr_states_end(a) : NULL; + for (size_t i = 0, end = states ? (a->len < a->cap ? a->len : a->cap) : 0; i < end; i++) { + if (*scr_arr_state_in(states, i) != SCR_ARR_FOREIGN) continue; + uint64_t old = a->data[i]; + *scr_arr_state_in(states, i) = SCR_ARR_HOLE; + a->data[i] = 0; + scr_dyn_release(scr_slot_to_ptr(old)); + } + ScrArrExt *x = scr_arr_ext_of(a); + for (size_t i = 0; i < x->sparse_len; i++) { + if (x->sparse[i].state != SCR_ARR_FOREIGN) continue; + uint64_t old = x->sparse[i].slot; + x->sparse[i].state = SCR_ARR_HOLE; + x->sparse[i].slot = 0; + scr_dyn_release(scr_slot_to_ptr(old)); + } + x->foreign = false; +} + +/* Releases an array's elements and ext contents (destroy's first half). */ +static void scr_arr_release_contents(ScrArr *a) { if (scr_arr_ext_of(a)) { scr_dyn_release(scr_arr_ext_of(a)->metadata); scr_arr_ext_of(a)->metadata = NULL; + if (scr_arr_ext_of(a)->foreign) scr_arr_release_foreign(a); } if (scr_elem_is_ref(a->elem)) { size_t end = a->len < a->cap ? a->len : a->cap; @@ -1657,10 +1737,255 @@ static void scr_arr_destroy(void *object) { } } } +} + +static void scr_arr_destroy(void *object) { + ScrArr *a = object; + scr_arr_release_contents(a); if (a->cyc) scr_arr_gc_free(a); else scr_arr_free(a); } +void scr_arr_subclass_init(ScrArr *a, uint8_t elem, const ScrArrOps *ops, const ScrArrClass *cls, + bool cyc) { + a->len = 0; + a->cap = 0; + a->elem = elem; + a->cyc = cyc ? 1 : 0; + uint32_t id = ops ? scr_arr_ops_id(ops) : 0; + a->spare = (uint16_t)(id << SCR_ARR_OPS_SHIFT); + a->filled = 0; + a->data = NULL; + ScrArrExt *x = scr_arr_ext(a); + if (id == SCR_ARR_OPS_OVERFLOW) x->ops = ops; + x->cls = cls; +#ifdef SCR_RC_AUDIT + scr_live_arrays++; +#endif +} + +/* super(...args) in a class that extends Array, as the Array constructor: + * one number argument sets the length (a RangeError for an invalid length), + * otherwise the arguments become the elements. `args` has the instance's + * element type and is borrowed; `dyn_elems` says its elements are dyn + * values, whose number test reads the value. */ +void scr_arr_subclass_construct(ScrArr *a, ScrArr *args, bool dyn_elems) { + if (args->len == 1) { + if (args->elem == SCR_ELEM_F64 || args->elem == SCR_ELEM_I32) { + scr_arr_set_len(a, scr_arr_get_number(args, 0)); + return; + } + if (dyn_elems) { + const ScrDyn *d = scr_arr_borrow_ref(args, 0); + if (d != NULL && d->kind == SCR_DYN_NUM) { + scr_arr_set_len(a, d->v.num); + return; + } + } + } + if (args->len > 0) (void)scr_arr_push_spread(a, args); +} + +/* The implicit constructor of a class that extends Array forwards its + * arguments (a dyn array) to super: the same rule, each argument converted + * to the element type. Element types other than numbers, booleans, strings + * and dyn values take only the length form. */ +void scr_arr_subclass_construct_dyn(ScrArr *a, const ScrDyn *args, bool dyn_elems) { + if (args == NULL || args->kind != SCR_DYN_ARR) return; + size_t n = args->v.arr.len; + ScrDyn *const *items = args->v.arr.items; + if (n == 1 && items[0] != NULL && items[0]->kind == SCR_DYN_NUM) { + scr_arr_set_len(a, items[0]->v.num); + return; + } + for (size_t i = 0; i < n; i++) { + ScrDyn *d = items[i]; + if (dyn_elems) { + (void)scr_arr_push_ref(a, scr_dyn_retain(d)); + continue; + } + if ((a->elem == SCR_ELEM_F64 || a->elem == SCR_ELEM_I32) && d && d->kind == SCR_DYN_NUM) { + (void)scr_arr_push_f64(a, d->v.num); + } else if (a->elem == SCR_ELEM_BOOL && d && d->kind == SCR_DYN_BOOL) { + (void)scr_arr_push_bool(a, d->v.b); + } else if (a->elem == SCR_ELEM_STR && d && d->kind == SCR_DYN_STR) { + (void)scr_arr_push_ref(a, scr_str_retain(d->v.str)); + } else { + static const char msg[] = + "constructing this Array subclass with element arguments is not supported yet"; + scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1); + return; + } + } +} + +static void scr_arr_set_slot(ScrArr *a, double i, uint64_t slot); + +static ScrArr *scr_arr_species_fill(const ScrArrClass *cls, ScrArr *result, double ctor_length, + bool with_length, bool set_length); +static ScrArr *scr_arr_species_move(ScrArr *species, ScrArr *result, bool set_length); + +/* Gives an Array subclass instance holding no elements (only holes) another + * element type: its storage is dropped and its length kept. */ +static void scr_arr_retype_empty(ScrArr *a, uint8_t elem, const ScrArrOps *ops) { + uint32_t len = a->len; + ScrArrExt *x = scr_arr_ext_of(a); + const ScrArrClass *cls = x->cls; + scr_arr_free_parts(a); + a->data = NULL; + a->len = a->cap = a->filled = 0; + a->elem = elem; + uint32_t id = ops ? scr_arr_ops_id(ops) : 0; + a->spare = (uint16_t)(id << SCR_ARR_OPS_SHIFT); + x = scr_arr_ext(a); + if (id == SCR_ARR_OPS_OVERFLOW) x->ops = ops; + x->cls = cls; + scr_arr_set_len(a, (double)len); +} + +ScrArr *scr_arr_species_adopt(ScrArr *recv, ScrArr *result, double ctor_length, bool set_length) { + const ScrArrClass *cls = scr_arr_class_of(recv); + if (cls == NULL) return result; + if (ctor_length < 0) ctor_length = (double)result->len; + return scr_arr_species_fill(cls, result, ctor_length, true, set_length); +} + +ScrArr *scr_arr_species_create(ScrArr *recv, double ctor_length) { + const ScrArrClass *cls = scr_arr_class_of(recv); + if (cls == NULL) return scr_arr_retain(recv); + if (cls->species == NULL) { + static const char msg[] = + "this Array subclass's constructor cannot construct a species result (Symbol.species)"; + scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1); + return scr_arr_retain(recv); + } + ScrArr *species = cls->species(ctor_length, true); + if (scr_exc_pending()) { + scr_arr_release(species); + return scr_arr_retain(recv); + } + return species; +} + +ScrArr *scr_arr_species_finish(ScrArr *species, ScrArr *result, bool set_length) { + if (scr_arr_class_of(species) == NULL) return result; + return scr_arr_species_move(scr_arr_retain(species), result, set_length); +} + +ScrArr *scr_arr_class_construct_adopt(const ScrArrClass *cls, ScrArr *result, double ctor_length) { + return scr_arr_species_fill(cls, result, ctor_length < 0 ? 0 : ctor_length, ctor_length >= 0, true); +} + +static ScrArr *scr_arr_species_fill(const ScrArrClass *cls, ScrArr *result, double ctor_length, + bool with_length, bool set_length) { + if (cls->species == NULL) { + static const char msg[] = + "this Array subclass's constructor cannot construct a species result (Symbol.species)"; + scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1); + return result; + } + ScrArr *species = cls->species(ctor_length, with_length); + if (scr_exc_pending()) { + scr_arr_release(species); + return result; + } + return scr_arr_species_move(species, result, set_length); +} + +/* Defines the present elements of `result` (owned) on the species instance + * `species` (owned) and answers the instance, or the result with an error + * pending when the instance cannot take them. */ +static ScrArr *scr_arr_species_move(ScrArr *species, ScrArr *result, bool set_length) { + if (species->elem != result->elem || scr_arr_ops(species) != scr_arr_ops(result)) { + /* A result of another element type (map, flatMap): an instance the + * constructor left without elements takes the result's element type; + * its length stays. An instance whose collector header the elements + * would need, or that already holds elements, cannot. */ + const ScrArrOps *ops = scr_arr_ops(result); + bool traced = ops != NULL && ops->trace != NULL; + bool empty = true; + for (size_t i = 0; empty && i < species->len; i++) { + uint64_t slot; + empty = scr_arr_state_at(species, i, &slot) == SCR_ARR_HOLE; + } + if (!empty || (traced && !species->cyc)) { + static const char msg[] = + "an Array subclass's species result of another element type is not supported yet"; + scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1); + scr_arr_release(species); + return result; + } + scr_arr_retype_empty(species, result->elem, ops); + } + for (size_t i = 0; i < result->len; i++) { + uint64_t slot; + uint8_t state = scr_arr_state_at(result, i, &slot); + if (state == SCR_ARR_VALUE) scr_arr_set_slot(species, (double)i, scr_elem_retain_slot(result, slot)); + else if (state == SCR_ARR_UNDEFINED) scr_arr_set_undefined(species, (double)i); + /* A value written through a dynamic view (scr_arr_set_foreign retains + * it); the instance has the result's element type. */ + else if (state == SCR_ARR_FOREIGN) scr_arr_set_foreign(species, (double)i, scr_slot_to_ptr(slot)); + } + if (set_length) scr_arr_set_len(species, (double)result->len); + scr_arr_release(result); + return species; +} + +bool scr_dyn_is_array_class(const ScrDyn *d, const ScrArrClass *cls) { + if (d == NULL) return false; + /* A live view of the native instance. */ + if (d->kind == SCR_DYN_TYPED_REF && d->v.typed_ref.array != NULL && !d->v.typed_ref.record && + !d->v.typed_ref.klass && !d->v.typed_ref.bag_class) + return scr_arr_class_of(d->v.typed_ref.ptr) == cls; + /* A snapshot: scr_arr_copy_metadata names the class by its name's + * address, which each class owns. */ + if (d->kind == SCR_DYN_ARR) { + const ScrDyn *props = d->v.arr.properties; + return props != NULL && props->v.obj.class_name == cls->name; + } + return false; +} + +const ScrArrClass *scr_arr_class_of(const ScrArr *a) { + if (!(a->spare & SCR_ARR_HAS_EXT)) return NULL; + return scr_arr_ext_of((ScrArr *)a)->cls; +} + +/* Whether an Array subclass instance's elements are the collector's: its + * current element ops trace them. A species result can retype an empty + * instance's storage (scr_arr_species_move), so this follows the storage, + * not the element type the class declares. */ +static bool scr_arr_subclass_elements_traced(const ScrArr *a) { + const ScrArrOps *ops = scr_arr_ops(a); + return ops != NULL && ops->trace != NULL; +} + +void scr_arr_subclass_trace(void *a, ScrTraceVisit visit, void *ctx) { + if (scr_arr_subclass_elements_traced((ScrArr *)a)) scr_arr_trace_v(a, visit, ctx); +} + +void scr_arr_subclass_gc_drop(ScrArr *a) { + if (!scr_arr_subclass_elements_traced(a)) scr_arr_release_contents(a); + scr_arr_free_parts(a); + a->data = NULL; + a->len = a->cap = a->filled = 0; + a->spare &= (uint16_t)~SCR_ARR_HAS_EXT; +#ifdef SCR_RC_AUDIT + scr_live_arrays--; +#endif +} + +void scr_arr_subclass_drop(ScrArr *a) { + scr_arr_release_contents(a); + scr_arr_free_parts(a); + a->data = NULL; + a->len = a->cap = a->filled = 0; + a->spare &= (uint16_t)~SCR_ARR_HAS_EXT; +#ifdef SCR_RC_AUDIT + scr_live_arrays--; +#endif +} + /* Whether destroying `a`, an acyclic array, releases nothing: it has no ext * and its elements are numbers, booleans or compact references, or * references whose release does nothing (SCR_ARR_OPS_RELEASE_NOTHING). */ @@ -1674,6 +1999,12 @@ static inline bool scr_arr_releases_nothing(const ScrArr *a) { void scr_arr_release(ScrArr *a) { if (!a || a->rc == SIZE_MAX) return; /* NULL: an uninitialized `let` local */ if (--a->rc == 0) { + /* An Array subclass instance: its class tears it down. */ + if ((a->spare & SCR_ARR_HAS_EXT) && scr_arr_ext_of(a)->cls) { + if (a->cyc) scr_cyc_dead(a); + scr_rc_destroy(a, scr_arr_ext_of(a)->cls->destroy); + return; + } /* Nothing to tear down: free the storage without scr_rc_destroy's * queue (no release can recurse) or the element loop. */ if (!a->cyc && scr_arr_releases_nothing(a)) { @@ -1689,6 +2020,134 @@ void scr_arr_release(ScrArr *a) { double scr_arr_len(ScrArr *a) { return SCR_ARR_LEN_F64(a->len); } +/* ── frozen arrays ────────────────────────────────────────────────────── + * Node's TypeErrors for writes to a frozen array, shared by the emitted + * guards of published and frozen arrays and the checked-dynamic tier. */ +static void scr_frozen_throw(const char *before, double index, const char *after) { + char text[64]; + size_t n = scr_f64_to_str(index, text); + ScrJsonBuf buffer; + scr_jb_init(&buffer); + scr_jb_puts(&buffer, before); + scr_jb_write(&buffer, text, n); + scr_jb_puts(&buffer, after); + scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&buffer)); +} + +/* A relative array index argument (fill, copyWithin, splice): ToIntegerOrInfinity + * of `value`, counted from the end when negative, clamped to [0, len]. */ +static size_t scr_frozen_relative_index(double value, size_t len) { + if (value != value) return 0; + double n = trunc(value); + if (n < 0) return n + (double)len <= 0 ? 0 : (size_t)(n + (double)len); + return n >= (double)len ? len : (size_t)n; +} + +static void scr_frozen_read_only(double index) { + scr_frozen_throw("Cannot assign to read only property '", index, "' of object '[object Array]'"); +} + +static void scr_frozen_not_extensible(double index) { + scr_frozen_throw("Cannot add property ", index, ", object is not extensible"); +} + +static void scr_frozen_undeletable(double index) { + scr_frozen_throw("Cannot delete property '", index, "' of [object Array]"); +} + +void scr_array_not_extensible_error(double index) { + scr_frozen_not_extensible(index); +} + +void scr_array_undeletable_error(double index) { + scr_frozen_undeletable(index); +} + +/* Each case finds the operation's first write in the order the spec makes + * its writes. */ +bool scr_array_frozen_error(int op, size_t len, double x, double y, double z) { + switch (op) { + case 0: + if (x >= 0 && x < (double)len && x == (double)(size_t)x) scr_frozen_read_only(x); + else scr_frozen_not_extensible(x); + return true; + case 1: + if (y == 0) break; + scr_frozen_not_extensible((double)len); + return true; + case 2: + case 6: + if (len == 0) break; + if (op == 6 && len > 1) scr_frozen_read_only(0); + else scr_frozen_undeletable((double)(len - 1)); + return true; + case 3: break; + case 4: + if (y == 0) break; + scr_frozen_not_extensible(len ? (double)len + y - 1 : 0); + return true; + case 5: + if (!(x >= 0 && x < (double)len && x == (double)(size_t)x)) return false; + scr_frozen_undeletable(x); + return true; + case 7: + if (len < 2) return false; + scr_frozen_read_only(0); + return true; + case 8: { + size_t k = scr_frozen_relative_index(x, len), final = scr_frozen_relative_index(y, len); + if (k >= final) return false; + scr_frozen_read_only((double)k); + return true; + } + case 9: { + size_t to = scr_frozen_relative_index(x, len), from = scr_frozen_relative_index(y, len); + size_t final = scr_frozen_relative_index(z, len); + if (final <= from || len <= to) return false; + size_t count = final - from < len - to ? final - from : len - to; + /* An overlapping copy toward the end writes its last index first. */ + if (from < to && to < from + count) to += count - 1; + scr_frozen_read_only((double)to); + return true; + } + case 10: { + size_t start = scr_frozen_relative_index(x, len); + double count = y != y ? 0 : trunc(y); + size_t removed = count <= 0 ? 0 : count >= (double)(len - start) ? len - start : (size_t)count; + size_t items = (size_t)z; + if (items < removed) { + /* Elements after the removed ones move down, then the tail goes. */ + if (start < len - removed) scr_frozen_read_only((double)(start + items)); + else scr_frozen_undeletable((double)(len - 1)); + return true; + } + if (items > removed) { + /* Elements after the removed ones move up, past the end first. */ + if (len - removed > start) scr_frozen_not_extensible((double)(len - removed + items - 1)); + else if (start < len) scr_frozen_read_only((double)start); + else scr_frozen_not_extensible((double)start); + return true; + } + if (items > 0) { + scr_frozen_read_only((double)start); + return true; + } + break; + } + default: + return false; + } + static const char length[] = "Cannot assign to read only property 'length' of object '[object Array]'"; + scr_throw_error_msg(SCR_ERR_TYPE, length, sizeof length - 1); + return true; +} + +void scr_arr_freeze(ScrArr *a) { + if (a->rc != SIZE_MAX) a->spare |= SCR_ARR_FROZEN; +} + +bool scr_arr_is_frozen(const ScrArr *a) { return scr_arr_frozen(a); } + bool scr_arr_has(const ScrArr *a, double i) { size_t idx; if (!scr_arr_valid_index(i, &idx)) return scr_arr_prop_get(a, i, NULL); @@ -1770,14 +2229,14 @@ void scr_arr_set_len(ScrArr *a, double length) { } return; } - if (scr_elem_is_ref(a->elem)) { + if (scr_elem_is_ref(a->elem) || scr_arr_may_hold_foreign(a)) { for (size_t i = dense_stop; i < dense_end; i++) { if (*scr_arr_state_ptr(a, i) != SCR_ARR_HOLE) { uint64_t old = a->data[i]; uint8_t state = *scr_arr_state_ptr(a, i); *scr_arr_state_ptr(a, i) = 0; a->data[i] = 0; - if (state == SCR_ARR_VALUE) scr_elem_release(a, old); + scr_arr_release_state_slot(a, old, state); } } if (scr_arr_ext_of(a)) { @@ -1787,7 +2246,7 @@ void scr_arr_set_len(ScrArr *a, double length) { uint64_t old = scr_arr_ext_of(a)->sparse[i].slot; uint8_t state = scr_arr_ext_of(a)->sparse[i].state; scr_arr_ext_of(a)->sparse_len = i; - if (state == SCR_ARR_VALUE) scr_elem_release(a, old); + scr_arr_release_state_slot(a, old, state); } } } else { @@ -1821,7 +2280,7 @@ void scr_arr_copy_index(ScrArr *dst, size_t dst_index, const ScrArr *src, uint64_t slot; uint8_t state = scr_arr_state_at(src, src_index, &slot); if (state == SCR_ARR_HOLE) return; - if (state == SCR_ARR_VALUE) slot = scr_elem_retain_slot(src, slot); + slot = scr_arr_retain_state_slot(src, slot, state); scr_arr_replace_state_owned(dst, dst_index, slot, state); } @@ -1928,7 +2387,7 @@ ScrArr *scr_arr_copy_within(ScrArr *a, double target, double start, double end) if (state == SCR_ARR_HOLE) { scr_arr_delete(a, (double)(to + offset)); } else { - if (state == SCR_ARR_VALUE) slot = scr_elem_retain_slot(a, slot); + slot = scr_arr_retain_state_slot(a, slot, state); /* Each replacement publishes its edge before releasing the previous * owner; cycle collection can run between successive positions. */ scr_arr_replace_state_owned(a, to + offset, slot, state); @@ -1946,7 +2405,7 @@ static ScrArr *scr_arr_fill_slot(ScrArr *a, uint64_t slot, uint8_t state, bool refs = scr_elem_is_ref(a->elem); if (scr_arr_dense_ok(a, until, until - from)) scr_arr_grow_dense(a, until); if (state != SCR_ARR_VALUE) scr_arr_need_states(a); - if (!refs && until <= a->cap) { + if (!refs && until <= a->cap && !scr_arr_may_hold_foreign(a)) { if (state != SCR_ARR_VALUE) scr_arr_cut(a, from); if (a->elem == SCR_ELEM_BOOL) memset(scr_arr_dense_at(a, from), (int)slot, until - from); else if (a->elem == SCR_ELEM_CREF) for (size_t i = from; i < until; i++) scr_arr_dense_put(a, i, slot); @@ -1986,7 +2445,63 @@ ScrArr *scr_arr_fill_undefined(ScrArr *a, double start, double end) { * The JS fold exactly (ECMA Math.max/min applied to the elements): any * NaN poisons the result, +0 beats -0 for max (the reverse for min), and * the empty array yields the zero-argument constants. Borrows the array. */ +/* ToNumber of every element in order, as Math.max/min/hypot apply it to + * their spread arguments before comparing; for arrays that may hold + * foreign elements, whose conversion can call user code. As in the spread, + * the elements are taken first: the foreign values are retained before + * any conversion runs, so a conversion that changes the array changes + * neither the count (*count) nor the values. NULL with the exception + * pending when a conversion throws. +1 malloc'd buffer. */ +static double *scr_arr_coerced_numbers(const ScrArr *a, size_t *count) { + size_t n = a->len; + /* A sparse array's length can exceed what the buffers can count. */ + if (n > SIZE_MAX / sizeof(double)) scr_arr_oom(); + double *out = malloc((n ? n : 1) * sizeof(*out)); + ScrDyn **held = calloc(n ? n : 1, sizeof(*held)); + if (!out || !held) scr_arr_oom(); + for (size_t i = 0; i < n; i++) { + uint64_t slot; + uint8_t state = scr_arr_f64_state_at(a, i, &slot); + if (state == SCR_ARR_FOREIGN) held[i] = scr_dyn_retain(scr_slot_to_ptr(slot)); + out[i] = state == SCR_ARR_VALUE ? scr_slot_to_f64(slot) : NAN; + } + bool failed = false; + for (size_t i = 0; i < n; i++) { + if (!held[i]) continue; + if (!failed) { + out[i] = scr_arr_foreign_conversions()->to_number(held[i]); + failed = scr_exc_pending(); + } + scr_dyn_release(held[i]); + } + free(held); + if (failed) { + free(out); + return NULL; + } + *count = n; + return out; +} + +static double scr_math_extreme_foreign(ScrArr *a, bool max) { + size_t n; + double *values = scr_arr_coerced_numbers(a, &n); + if (!values) return NAN; + double best = max ? -INFINITY : INFINITY; + bool nan = false; + for (size_t i = 0; i < n; i++) { + double v = values[i]; + if (isnan(v)) nan = true; + else if (max ? (v > best || (v == 0.0 && best == 0.0 && !signbit(v))) + : (v < best || (v == 0.0 && best == 0.0 && signbit(v)))) + best = v; + } + free(values); + return nan ? NAN : best; +} + double scr_math_max_arr(ScrArr *a) { + if (scr_arr_may_hold_foreign(a)) return scr_math_extreme_foreign(a, true); double best = -INFINITY; for (size_t i = 0; i < a->len; i++) { uint64_t slot; @@ -1999,6 +2514,7 @@ double scr_math_max_arr(ScrArr *a) { } double scr_math_min_arr(ScrArr *a) { + if (scr_arr_may_hold_foreign(a)) return scr_math_extreme_foreign(a, false); double best = INFINITY; for (size_t i = 0; i < a->len; i++) { uint64_t slot; @@ -2015,30 +2531,43 @@ double scr_math_min_arr(ScrArr *a) { double scr_math_hypot_arr(ScrArr *a) { double scale = 0.0; bool has_nan = false; - for (size_t i = 0; i < a->len; i++) { + double *coerced = NULL; + size_t n = a->len; + if (scr_arr_may_hold_foreign(a) && !(coerced = scr_arr_coerced_numbers(a, &n))) return NAN; + for (size_t i = 0; i < n; i++) { uint64_t slot; - if (scr_arr_f64_state_at(a, i, &slot) != SCR_ARR_VALUE) { + double v; + if (coerced) { + v = fabs(coerced[i]); + } else if (scr_arr_f64_state_at(a, i, &slot) != SCR_ARR_VALUE) { has_nan = true; continue; + } else { + v = fabs(scr_slot_to_f64(slot)); + } + if (isinf(v)) { + free(coerced); + return INFINITY; } - double v = fabs(scr_slot_to_f64(slot)); - if (isinf(v)) return INFINITY; if (isnan(v)) has_nan = true; else if (v > scale) scale = v; } - if (has_nan) return NAN; - if (scale == 0.0) return 0.0; + if (has_nan || scale == 0.0) { + free(coerced); + return has_nan ? NAN : 0.0; + } double sum = 0.0; double correction = 0.0; - for (size_t i = 0; i < a->len; i++) { - uint64_t slot; - (void)scr_arr_f64_state_at(a, i, &slot); - double v = scr_slot_to_f64(slot) / scale; + for (size_t i = 0; i < n; i++) { + uint64_t slot = 0; + if (!coerced) (void)scr_arr_f64_state_at(a, i, &slot); + double v = (coerced ? coerced[i] : scr_slot_to_f64(slot)) / scale; double term = v * v - correction; double next = sum + term; correction = (next - sum) - term; sum = next; } + free(coerced); return scale * sqrt(sum); } @@ -2083,6 +2612,13 @@ static uint64_t scr_arr_require_index(const ScrArr *a, size_t idx, double i) { scr_trap_fmt("scriptc: RangeError: array index %s is undefined (length %zu)\n", buf, a->len); } + if (state == SCR_ARR_FOREIGN) { + char buf[32]; + scr_f64_to_str(i, buf); + scr_trap_fmt("scriptc: RangeError: array index %s holds a value outside its element type " + "(length %zu)\n", + buf, a->len); + } return slot; } @@ -2090,16 +2626,20 @@ double scr_arr_get_f64(ScrArr *a, double i) { return scr_slot_to_f64(scr_arr_require_slot(a, i)); } -static bool scr_arr_read_value(const ScrArr *a, double i, uint64_t *slot) { +/* The state of a[i] for any numeric key (a hole when missing), with its + * slot. */ +static uint8_t scr_arr_read_state(const ScrArr *a, double i, uint64_t *slot) { size_t idx; uint8_t state; if (scr_arr_valid_index(i, &idx)) { - if (idx >= a->len) return false; - state = scr_arr_state_at(a, idx, slot); - } else if (!scr_arr_prop_get_state(a, i, slot, &state)) { - return false; + if (idx >= a->len) return SCR_ARR_HOLE; + return scr_arr_state_at(a, idx, slot); } - return state == SCR_ARR_VALUE; + return scr_arr_prop_get_state(a, i, slot, &state) ? state : SCR_ARR_HOLE; +} + +static bool scr_arr_read_value(const ScrArr *a, double i, uint64_t *slot) { + return scr_arr_read_state(a, i, slot) == SCR_ARR_VALUE; } void *scr_arr_peek_ref(const ScrArr *a, double i) { @@ -2129,6 +2669,13 @@ double scr_arr_get_number(const ScrArr *a, double i) { } else if (!scr_arr_prop_get_state(a, i, &slot, &state)) { return NAN; } + if (state == SCR_ARR_FOREIGN) { + /* The conversion can call user code that removes the element. */ + ScrDyn *value = scr_dyn_retain(scr_slot_to_ptr(slot)); + double number = scr_arr_foreign_conversions()->to_number(value); + scr_dyn_release(value); + return number; + } return state == SCR_ARR_VALUE ? scr_slot_to_f64(slot) : NAN; } @@ -2139,14 +2686,26 @@ bool scr_arr_index_eq(const ScrArr *a, double i, const ScrArr *b, double j) { if (i >= 0 && i < (double)a->cap && i == trunc(i) && j >= 0 && j < (double)b->cap && j == trunc(j)) { size_t ai = (size_t)i, bi = (size_t)j; - bool ap = ai < a->len && scr_arr_dense_state(a, ai) == SCR_ARR_VALUE; - bool bp = bi < b->len && scr_arr_dense_state(b, bi) == SCR_ARR_VALUE; + uint8_t as = ai < a->len ? scr_arr_dense_state(a, ai) : SCR_ARR_HOLE; + uint8_t bs = bi < b->len ? scr_arr_dense_state(b, bi) : SCR_ARR_HOLE; + if (as == SCR_ARR_FOREIGN || bs == SCR_ARR_FOREIGN) { + /* A foreign element is never a value of the element type, undefined + * or a hole. */ + return as == bs && scr_dyn_strict_eq(scr_slot_to_ptr(scr_arr_dense_get(a, ai)), + scr_slot_to_ptr(scr_arr_dense_get(b, bi))); + } + bool ap = as == SCR_ARR_VALUE; + bool bp = bs == SCR_ARR_VALUE; if (!ap || !bp) return ap == bp; av = scr_arr_dense_get(a, ai); bv = scr_arr_dense_get(b, bi); } else { - bool ap = scr_arr_read_value(a, i, &av); - bool bp = scr_arr_read_value(b, j, &bv); + uint8_t as = scr_arr_read_state(a, i, &av); + uint8_t bs = scr_arr_read_state(b, j, &bv); + if (as == SCR_ARR_FOREIGN || bs == SCR_ARR_FOREIGN) + return as == bs && scr_dyn_strict_eq(scr_slot_to_ptr(av), scr_slot_to_ptr(bv)); + bool ap = as == SCR_ARR_VALUE; + bool bp = bs == SCR_ARR_VALUE; if (!ap || !bp) return ap == bp; } if (a->elem == SCR_ELEM_F64) return scr_slot_to_f64(av) == scr_slot_to_f64(bv); @@ -2221,6 +2780,44 @@ void scr_arr_set_undefined(ScrArr *a, double i) { scr_arr_replace_state_owned(a, idx, 0, SCR_ARR_UNDEFINED); } +bool scr_arr_set_foreign(ScrArr *a, double i, ScrDyn *v) { + size_t idx; + if (!scr_arr_valid_index(i, &idx)) return false; + if (v->kind == SCR_DYN_UNDEF) { + scr_arr_set_undefined(a, i); + return true; + } + /* A foreign element keeps a pointer in its slot: int32 slots widen to + * doubles first, and 1-byte and compact slots cannot hold one. */ + if (a->elem == SCR_ELEM_I32) scr_arr_widen_i32(a); + if (scr_arr_slot_width(a) != sizeof(uint64_t)) return false; + __atomic_store_n(&scr_arr_conversions, &scr_arr_dyn_conversions, __ATOMIC_RELAXED); + scr_dyn_retain(v); + scr_arr_need_states(a); + if (idx >= a->len) a->len = idx + 1; + scr_arr_replace_state_owned(a, idx, scr_slot_from_ptr(v), SCR_ARR_FOREIGN); + return true; +} + +ScrDyn *scr_arr_get_foreign(const ScrArr *a, double i) { + size_t idx; + uint64_t slot; + if (!scr_arr_valid_index(i, &idx) || idx >= a->len) return NULL; + if (scr_arr_state_at(a, idx, &slot) != SCR_ARR_FOREIGN) return NULL; + return scr_dyn_retain(scr_slot_to_ptr(slot)); +} + +void scr_arr_foreign_throw(const ScrArr *a, double i, const ScrStr *want) { + ScrDyn *value = scr_arr_get_foreign(a, i); + scr_dyn_check_fail(NULL, want->data, value ? value : scr_dyn_undefined()); + scr_dyn_release(value); +} + +void scr_dyn_foreign_throw(ScrDyn *value, const char *want) { + scr_dyn_check_fail(NULL, want, value); + scr_dyn_release(value); +} + bool scr_arr_delete(ScrArr *a, double i) { size_t idx; if (!scr_arr_valid_index(i, &idx)) return scr_arr_prop_delete(a, i); @@ -2228,9 +2825,7 @@ bool scr_arr_delete(ScrArr *a, double i) { uint64_t old; uint8_t state; if (!scr_arr_take_state(a, idx, &old, &state)) return false; - if (state == SCR_ARR_VALUE && scr_elem_is_ref(a->elem)) { - scr_elem_release(a, old); - } + scr_arr_release_state_slot(a, old, state); return true; } @@ -2301,8 +2896,8 @@ double scr_arr_push_spread(ScrArr *a, const ScrArr *src) { uint64_t slot; uint8_t state = scr_arr_state_at_storage(&old, i, &slot); if (state != SCR_ARR_HOLE) scr_arr_store_state_owned(a, i, slot, state); - if (state == SCR_ARR_VALUE) { - scr_arr_store_owned(a, old_len + i, scr_elem_retain_slot(a, slot)); + if (state == SCR_ARR_VALUE || state == SCR_ARR_FOREIGN) { + scr_arr_store_state_owned(a, old_len + i, scr_arr_retain_state_slot(a, slot, state), state); } else { scr_arr_store_state_owned(a, old_len + i, 0, SCR_ARR_UNDEFINED); } @@ -2317,8 +2912,8 @@ double scr_arr_push_spread(ScrArr *a, const ScrArr *src) { for (size_t i = 0; i < add; i++) { uint64_t slot; uint8_t state = scr_arr_state_at(src, i, &slot); - if (state == SCR_ARR_VALUE) { - scr_arr_store_owned(a, old_len + i, scr_elem_retain_slot(src, slot)); + if (state == SCR_ARR_VALUE || state == SCR_ARR_FOREIGN) { + scr_arr_store_state_owned(a, old_len + i, scr_arr_retain_state_slot(src, slot, state), state); } else { scr_arr_store_state_owned(a, old_len + i, 0, SCR_ARR_UNDEFINED); } @@ -2346,11 +2941,8 @@ double scr_arr_concat_copy(ScrArr *a, const ScrArr *src) { uint64_t slot; uint8_t state = scr_arr_state_at_storage(&old, i, &slot); if (state != SCR_ARR_HOLE) scr_arr_store_state_owned(a, i, slot, state); - if (state == SCR_ARR_VALUE) { - scr_arr_store_owned(a, old_len + i, scr_elem_retain_slot(a, slot)); - } else if (state == SCR_ARR_UNDEFINED) { - scr_arr_store_state_owned(a, old_len + i, 0, state); - } + if (state != SCR_ARR_HOLE) + scr_arr_store_state_owned(a, old_len + i, scr_arr_retain_state_slot(a, slot, state), state); } a->len = old_len + add; scr_arr_free_storage(&old); @@ -2531,8 +3123,9 @@ double scr_arr_unshift_spread(ScrArr *a, const ScrArr *src) { uint8_t state = src == a ? scr_arr_state_at_storage(&old, i, &slot) : scr_arr_state_at(src, i, &slot); - if (state == SCR_ARR_VALUE) { - scr_arr_store_owned(a, i, scr_arr_retain_state_slot(src == a ? a : src, slot, state)); + if (state == SCR_ARR_VALUE || state == SCR_ARR_FOREIGN) { + scr_arr_store_state_owned(a, i, scr_arr_retain_state_slot(src == a ? a : src, slot, state), + state); } else { scr_arr_store_state_owned(a, i, 0, SCR_ARR_UNDEFINED); } @@ -2641,7 +3234,8 @@ uint8_t scr_arr_shift_state(ScrArr *a, uint64_t *slot_out) { size_t old_len = a->len; if (scr_arr_is_dense(a)) { uint8_t state = scr_arr_dense_state(a, 0); - if (slot_out && state == SCR_ARR_VALUE) *slot_out = scr_arr_dense_get(a, 0); + if (slot_out && (state == SCR_ARR_VALUE || state == SCR_ARR_FOREIGN)) + *slot_out = scr_arr_dense_get(a, 0); /* The first slot becomes head room; the tail stays where it is. */ if (scr_arr_drop_front(a, 1)) return state; /* The prefix slides down with the tail. */ @@ -2714,6 +3308,7 @@ ScrArr *scr_arr_splice(ScrArr *a, double start, double deleteCount) { size_t old_len = a->len; if (scr_arr_is_dense(a)) { if (n) { + scr_arr_inherit_foreign(out, a); /* The removed elements keep their states unless all are values. */ if (!scr_arr_dense_run_values(a, from, n)) scr_arr_add_states(out); memcpy(out->data, scr_arr_dense_at(a, from), n * scr_arr_slot_width(a)); @@ -2799,6 +3394,7 @@ ScrArr *scr_arr_splice_insert(ScrArr *a, double start, double deleteCount, ? scr_arr_new_ops(scr_arr_ops(a), n) : scr_arr_new(a->elem, n); if (n) { + scr_arr_inherit_foreign(removed, a); if (!scr_arr_dense_run_values(a, from, n)) scr_arr_add_states(removed); memcpy(removed->data, scr_arr_dense_at(a, from), n * scr_arr_slot_width(a)); if (scr_arr_has_states(removed)) @@ -2853,22 +3449,30 @@ void scr_arr_splice_drop(ScrArr *a, double start, double deleteCount, size_t cou } /* Removed slots, kept until the receiver is rewritten. */ uint64_t inline_removed[8]; + uint8_t inline_states[8]; uint64_t *removed = inline_removed; + uint8_t *removed_states = inline_states; size_t kept = 0; - if (refs && n > 0) { + if ((refs || scr_arr_may_hold_foreign(a)) && n > 0) { if (n > 8) { removed = scr_mem_alloc(n * sizeof(uint64_t)); - if (!removed) scr_arr_oom(); + removed_states = scr_mem_alloc(n); + if (!removed || !removed_states) scr_arr_oom(); } for (size_t i = from; i < from + n; i++) { - if (scr_arr_dense_state(a, i) == SCR_ARR_VALUE) removed[kept++] = a->data[i]; + uint8_t state = scr_arr_dense_state(a, i); + if ((refs && state == SCR_ARR_VALUE) || state == SCR_ARR_FOREIGN) { + removed_states[kept] = state; + removed[kept++] = a->data[i]; + } } } size_t old_len = a->len; if (from == 0 && count == 0 && scr_arr_drop_front(a, n)) { /* A removed prefix becomes head room. */ - for (size_t i = 0; i < kept; i++) scr_elem_release(a, removed[i]); + for (size_t i = 0; i < kept; i++) scr_arr_release_state_slot(a, removed[i], removed_states[i]); if (removed != inline_removed) scr_mem_free(removed); + if (removed_states != inline_states) scr_mem_free(removed_states); return; } scr_arr_grow_dense(a, next_len); @@ -2883,8 +3487,9 @@ void scr_arr_splice_drop(ScrArr *a, double start, double deleteCount, size_t cou /* Values inserted into a hole-free array keep it hole-free. */ if (packed) a->filled = (uint32_t)next_len; else scr_arr_refill(a); - for (size_t i = 0; i < kept; i++) scr_elem_release(a, removed[i]); + for (size_t i = 0; i < kept; i++) scr_arr_release_state_slot(a, removed[i], removed_states[i]); if (removed != inline_removed) scr_mem_free(removed); + if (removed_states != inline_states) scr_mem_free(removed_states); } /* FlattenIntoArray for static arrays: a dense copy with depth zero, or one @@ -3108,10 +3713,15 @@ typedef struct { typedef struct { ScrElemKind kind; const ScrNumberSortKey *numbers; + /* When set, values are indices into these string keys: each element's + * String() text (reference elements such as Dates, and the elements of an + * array that may hold foreign elements). */ + ScrStr *const *texts; } ScrPrimitiveOrder; static int scr_arr_primitive_compare(uint64_t left, uint64_t right, const ScrPrimitiveOrder *order) { + if (order->texts) return scr_str_cmp_u16(order->texts[left], order->texts[right]); if (order->kind == SCR_ELEM_STR) return scr_str_cmp_u16(scr_slot_to_ptr(left), scr_slot_to_ptr(right)); if (order->kind == SCR_ELEM_BOOL) return (left > right) - (left < right); @@ -3199,10 +3809,133 @@ static void scr_arr_primitive_order(uint64_t *values, size_t count, if (runs != local_runs) free(runs); } +/* The default ordering of an array that may hold foreign elements: present + * elements sort by their string conversions, then present undefined + * values, then holes (toSorted reads them as undefined). The conversions + * may call user code, so the elements are taken first, each retained, as + * the sort's element list is; a throw leaves the array unchanged. */ +static ScrArr *scr_arr_sort_foreign(ScrArr *a, bool copy) { + size_t length = a->len; + size_t dense = a->len < a->cap ? a->len : a->cap; + size_t cap = dense + SCR_AX(a)->sparse_len; + if (cap > SIZE_MAX / sizeof(uint64_t)) scr_arr_oom(); + uint64_t *slots = cap ? malloc(cap * sizeof(*slots)) : NULL; + uint8_t *states = cap ? malloc(cap) : NULL; + ScrStr **keys = cap ? malloc(cap * sizeof(*keys)) : NULL; + uint64_t *order_of = cap ? malloc(cap * sizeof(*order_of)) : NULL; + if (cap && (!slots || !states || !keys || !order_of)) scr_arr_oom(); + size_t count = 0, undefined_count = 0; + for (size_t at = 0; at < cap; at++) { + uint64_t slot; + uint8_t state; + if (at < dense) { + state = scr_arr_dense_state(a, at); + slot = scr_arr_dense_get(a, at); + } else { + state = SCR_AX(a)->sparse[at - dense].state; + slot = SCR_AX(a)->sparse[at - dense].slot; + } + if (state == SCR_ARR_UNDEFINED) undefined_count++; + if (state != SCR_ARR_VALUE && state != SCR_ARR_FOREIGN) continue; + slots[count] = scr_arr_retain_state_slot(a, slot, state); + states[count] = state; + count++; + } + bool failed = false; + size_t keyed = 0; + for (; keyed < count; keyed++) { + uint64_t slot = slots[keyed]; + ScrStr *key; + if (states[keyed] == SCR_ARR_FOREIGN) { + key = scr_arr_foreign_conversions()->to_string(scr_slot_to_ptr(slot)); + } else if (a->elem == SCR_ELEM_STR) { + key = scr_str_retain(scr_slot_to_ptr(slot)); + } else { + char text[32]; + size_t n = scr_f64_to_str(scr_slot_to_f64(slot), text); + key = scr_str_new(text, n); + } + if (!key) { + failed = true; + break; + } + keys[keyed] = key; + order_of[keyed] = keyed; + } + ScrArr *out = a; + if (!failed) { + ScrPrimitiveOrder order = {a->elem, NULL, keys}; + scr_arr_primitive_order(order_of, count, &order); + if (copy) { + /* The values, then present undefined for every other index. */ + out = scr_arr_has_ops(a) ? scr_arr_new_ops(scr_arr_ops(a), length) + : scr_arr_new(a->elem, length); + for (size_t i = 0; i < length; i++) { + if (i < count) scr_arr_store_state_owned(out, i, slots[order_of[i]], states[order_of[i]]); + else scr_arr_store_state_owned(out, i, 0, SCR_ARR_UNDEFINED); + out->len = (uint32_t)(i + 1); + } + } else { + /* The values, then the present undefined values; holes after them + * up to the sorted length. Elements a conversion added at or past + * that length stay, and the array keeps the longer of its current + * length and the sorted count, as in Node. The taken elements own + * their references; release the ones the array holds now below the + * sorted length, which a conversion may have changed. */ + size_t now = a->len; + size_t sorted = count + undefined_count; + ScrArrStorage old = scr_arr_take_storage(a); + a->len = (uint32_t)(now > sorted ? now : sorted); + for (size_t i = 0; i < count; i++) + scr_arr_store_state_owned(a, i, slots[order_of[i]], states[order_of[i]]); + for (size_t i = count; i < sorted; i++) scr_arr_store_state_owned(a, i, 0, SCR_ARR_UNDEFINED); + size_t old_dense = now < old.cap ? now : old.cap; + for (size_t i = 0; i < old_dense + old.sparse_len; i++) { + size_t index = i < old_dense ? i : old.sparse[i - old_dense].index; + uint64_t slot = 0; + uint8_t state = scr_arr_state_at_storage(&old, index, &slot); + if (state == SCR_ARR_HOLE) continue; + if (index < length) scr_arr_release_state_slot(a, slot, state); + else scr_arr_store_state_owned(a, index, slot, state); + } + scr_arr_free_storage(&old); + } + scr_arr_refill(out); + } else { + for (size_t i = 0; i < count; i++) scr_arr_release_state_slot(a, slots[i], states[i]); + } + for (size_t i = 0; i < keyed; i++) scr_str_release(keys[i]); + free(slots); + free(states); + free(keys); + free(order_of); + if (failed) return copy ? scr_arr_new(a->elem, 0) : scr_arr_retain(a); + return copy ? out : scr_arr_retain(a); +} + +static ScrArr *scr_arr_sort_default(ScrArr *a, bool copy, ScrStr *(*text)(void *)); + +static ScrArr *scr_arr_new_like(const ScrArr *a, size_t cap) { + return a->elem == SCR_ELEM_REF ? scr_arr_new_ops(scr_arr_ops(a), cap) : scr_arr_new(a->elem, cap); +} + ScrArr *scr_arr_sort_primitive(ScrArr *a, bool copy) { if (a->elem != SCR_ELEM_STR && a->elem != SCR_ELEM_F64 && a->elem != SCR_ELEM_BOOL) scr_trap("scriptc: invalid primitive array ordering\n"); - if (a->len == 0) return copy ? scr_arr_new(a->elem, 0) : scr_arr_retain(a); + if (a->len > 0 && scr_arr_may_hold_foreign(a)) return scr_arr_sort_foreign(a, copy); + return scr_arr_sort_default(a, copy, NULL); +} + +/* The default ordering of an array of reference elements that cannot call + * user code while converting to strings (Dates): `text` answers each + * element's String() text (+1), computed once per element. */ +ScrArr *scr_arr_sort_by_text(ScrArr *a, bool copy, ScrStr *(*text)(void *)) { + if (a->elem != SCR_ELEM_REF) scr_trap("scriptc: invalid text array ordering\n"); + return scr_arr_sort_default(a, copy, text); +} + +static ScrArr *scr_arr_sort_default(ScrArr *a, bool copy, ScrStr *(*text)(void *)) { + if (a->len == 0) return copy ? scr_arr_new_like(a, 0) : scr_arr_retain(a); size_t dense = a->len < a->cap ? a->len : a->cap; size_t count = 0, undefined_count = 0; const uint8_t *states = scr_arr_scan_states(a); @@ -3234,14 +3967,35 @@ ScrArr *scr_arr_sort_primitive(ScrArr *a, bool copy) { values[i] = i; } } - ScrPrimitiveOrder order = {a->elem, numbers}; + ScrStr **texts = NULL; + uint64_t *originals = NULL; + if (text && count > 1) { + if (count > SIZE_MAX / sizeof(*texts)) scr_arr_oom(); + texts = malloc(count * sizeof(*texts)); + originals = malloc(count * sizeof(*originals)); + if (!texts || !originals) scr_arr_oom(); + for (size_t i = 0; i < count; i++) { + originals[i] = values[i]; + texts[i] = text(scr_slot_to_ptr(values[i])); + values[i] = i; + } + } + ScrPrimitiveOrder order = {a->elem, numbers, texts}; scr_arr_primitive_order(values, count, &order); if (numbers) { for (size_t i = 0; i < count; i++) values[i] = numbers[values[i]].value; free(numbers); } + if (texts) { + for (size_t i = 0; i < count; i++) { + values[i] = originals[values[i]]; + scr_str_release(texts[i]); + } + free(texts); + free(originals); + } size_t present = copy ? a->len : count + undefined_count; - ScrArr *out = copy ? scr_arr_new(a->elem, present) : a; + ScrArr *out = copy ? scr_arr_new_like(a, present) : a; if (!copy && !states) { /* Values only, in place. */ if (count) scr_arr_put_slots(a, 0, count, values); @@ -4990,6 +5744,8 @@ bool scr_arr_sort_dense(ScrArr *a, ScrClosure *f, uint32_t arity, bool owned, in if (a->rc == SIZE_MAX || elem == SCR_ELEM_CREF || !scr_arr_dense_values(a)) return false; size_t n = a->len; if (n < 2) return true; + /* A frozen array takes the snapshot path, whose write-back throws. */ + if (scr_arr_frozen(a)) return false; if (n > SIZE_MAX / 2 / sizeof(uint64_t)) scr_arr_oom(); ScrSortCompare c = scr_sort_compare_for(a, f, arity, order, key); /* Nothing else can reach a private copy: its own storage is sorted, and @@ -5012,17 +5768,15 @@ bool scr_arr_sort_dense(ScrArr *a, ScrClosure *f, uint32_t arity, bool owned, in free(src); return true; } - if (a->rc == SIZE_MAX) { - /* The comparator published the array: the first write fails as in Node - * (scr_throw_published_array's index write). */ + if (a->rc == SIZE_MAX || scr_arr_frozen(a)) { + /* The comparator published or froze the array: the first write fails + * as in Node. */ if (retained) for (size_t i = 0; i < n; i++) scr_elem_release_with(elem, ops, src[i]); - static const char assign[] = "Cannot assign to read only property '0' of object '[object Array]'"; - static const char add[] = "Cannot add property 0, object is not extensible"; - if (a->len > 0) scr_throw_error_msg(SCR_ERR_TYPE, assign, sizeof assign - 1); - else scr_throw_error_msg(SCR_ERR_TYPE, add, sizeof add - 1); + scr_array_frozen_error(0, a->len, 0, 0, 0); } else if (a->len >= n && a->cap >= n) { - if (!retained) { + /* The comparator may have stored foreign elements through a view. */ + if (!retained && !scr_arr_may_hold_foreign(a)) { memcpy(a->data, src, n * sizeof(*src)); if (scr_arr_has_states(a)) memset(scr_arr_state_run(a, 0, n), SCR_ARR_VALUE, n); } else { @@ -5030,11 +5784,11 @@ bool scr_arr_sort_dense(ScrArr *a, ScrClosure *f, uint32_t arity, bool owned, in * release can collect cycles. The buffer still holds the slots not * yet written. */ for (size_t i = 0; i < n; i++) { - bool held = scr_arr_dense_state(a, i) == SCR_ARR_VALUE; - uint64_t old = held ? a->data[i] : 0; + uint8_t old_state = scr_arr_dense_state(a, i); + uint64_t old = old_state == SCR_ARR_VALUE || old_state == SCR_ARR_FOREIGN ? a->data[i] : 0; a->data[i] = src[i]; if (scr_arr_has_states(a)) *scr_arr_state_ptr(a, i) = SCR_ARR_VALUE; - if (held) scr_elem_release(a, old); + scr_arr_release_state_slot(a, old, old_state); } } } else { @@ -5102,10 +5856,28 @@ ScrStr *scr_arr_join(ScrArr *a, ScrStr *sep) { * across its seam; the finished string then needs fusing. */ bool lone = scr_wtf8_any(); bool lead_low = lone && a->len > 1 && scr_wtf8_low_at(sep->data); - for (size_t i = 0; i < a->len; i++) { + /* The length is read once: a conversion that shortens the array leaves + * the indices past its new end empty. */ + size_t length = a->len; + for (size_t i = 0; i < length; i++) { if (i > 0) scr_join_append(&out, sep->data, sep->len); + if (i >= a->len) continue; uint64_t slot; uint8_t state = scr_arr_state_at(a, i, &slot); + if (state == SCR_ARR_FOREIGN) { + /* join converts every element but undefined and null with ToString, + * which can call user code that removes the element; a throw ends + * the join with the exception pending. */ + ScrDyn *value = scr_slot_to_ptr(slot); + if (value->kind == SCR_DYN_UNDEF || value->kind == SCR_DYN_NULL) continue; + scr_dyn_retain(value); + ScrStr *text = scr_arr_foreign_conversions()->to_string(value); + scr_dyn_release(value); + if (!text) break; + scr_join_append(&out, text->data, text->len); + scr_str_release(text); + continue; + } if (state != SCR_ARR_VALUE) continue; switch (a->elem) { case SCR_ELEM_F64: { @@ -5190,10 +5962,32 @@ bool scr_arr_numeric_key(const char *key, size_t len, double *out) { bool scr_arr_has_properties(const ScrArr *a) { const ScrArrExt *x = scr_arr_ext_of(a); - return x && (x->prop_len || x->metadata); + return x && (x->prop_len || x->metadata || x->cls); +} + +/* The own fields of an Array subclass instance (ScrArrClass.fields), a + * fresh dyn object, or NULL. */ +static ScrDyn *scr_arr_class_fields(ScrArr *a) { + const ScrArrClass *cls = scr_arr_class_of(a); + if (!cls || !cls->fields) return NULL; + return cls->fields(scr_arr_retain(a)); } void scr_arr_copy_metadata(ScrArr *a, ScrDyn *target) { + const ScrArrClass *cls = scr_arr_class_of(a); + if (cls) { + /* An Array subclass instance: the class name util.inspect prints before + * the elements, then the class's own fields as properties. */ + if (!target->v.arr.properties) target->v.arr.properties = scr_dyn_new_obj(); + target->v.arr.properties->v.obj.class_name = cls->name; + ScrDyn *fields = scr_arr_class_fields(a); + if (fields) { + for (size_t i = 0; i < fields->v.obj.len; i++) + scr_dyn_copy_entry(target, fields, &fields->v.obj.entries[i]); + scr_dyn_release(fields); + } + return; + } const ScrDyn *metadata = SCR_AX(a)->metadata; if (metadata && metadata->kind == SCR_DYN_HANDLE && metadata->v.handle.tag == SCR_DYNH_MATCH_SOURCE && scr_arr_match_copy(a, target)) @@ -5212,6 +6006,15 @@ ScrArr *scr_arr_named_keys(ScrArr *a) { ScrArr *keys = scr_arr_new(SCR_ELEM_STR, 0); for (size_t i = 0; i < x->prop_len; i++) scr_arr_push_ref(keys, scr_str_new(x->props[i].key, x->props[i].key_len)); + ScrDyn *fields = scr_arr_class_fields(a); + if (fields) { + for (size_t i = 0; i < fields->v.obj.len; i++) { + const ScrDynEntry *entry = &fields->v.obj.entries[i]; + if (entry->enumerable) scr_arr_push_ref(keys, scr_str_new(entry->key, entry->key_len)); + } + scr_dyn_release(fields); + return keys; + } const ScrDyn *metadata = x->metadata; if (metadata && metadata->kind == SCR_DYN_HANDLE && metadata->v.handle.tag == SCR_DYNH_MATCH_SOURCE) { static const char *const names[] = {"index", "input", "groups"}; @@ -5244,6 +6047,13 @@ bool scr_arr_has_named_key(ScrArr *a, ScrStr *key) { void scr_arr_adopt_view_properties(ScrArr *a, ScrDyn *view, ScrArr *target) { ScrArrExt *from = scr_arr_ext_of(target); + /* An Array subclass instance's class is in its out-of-line storage, which + * the commit gives to `a`: the class moves to `a`'s, and so stays with the + * instance, while `a` is released as a plain array. */ + if (from && from->cls) { + scr_arr_ext(a)->cls = from->cls; + from->cls = NULL; + } if (from && from->prop_len) { /* Both arrays have the same element type, so the slots keep their owner. */ ScrArrExt *x = scr_arr_ext(a); diff --git a/packages/runtime/src/scr_array.h b/packages/runtime/src/scr_array.h index 4666750ea..0b162ff5e 100644 --- a/packages/runtime/src/scr_array.h +++ b/packages/runtime/src/scr_array.h @@ -60,6 +60,10 @@ typedef enum { SCR_ARR_HOLE = 0, SCR_ARR_VALUE = 1, SCR_ARR_UNDEFINED = 2, + /* A present element outside the element type, stored through a dynamic + * view: the slot holds a retained ScrDyn * (scr_arr_set_foreign). Arrays + * that hold one mark their out-of-line storage (ScrArrExt.foreign). */ + SCR_ARR_FOREIGN = 3, } ScrArrState; /* The RC entry points of an SCR_ELEM_REF array's element type: one constant @@ -128,7 +132,8 @@ typedef struct ScrArr { * header. Kept beside `elem` so the RC fast paths test it with one load. */ uint8_t cyc; /* The low SCR_ARR_OPS_SHIFT bits: storage flags (SCR_ARR_INLINE_SLOTS, - * SCR_ARR_HEAD_ROOM, SCR_ARR_UNSHIFTED, SCR_ARR_HAS_EXT, SCR_ARR_STATES). The rest: the + * SCR_ARR_HEAD_ROOM, SCR_ARR_UNSHIFTED, SCR_ARR_HAS_EXT, SCR_ARR_STATES) + * and SCR_ARR_FROZEN. The rest: the * registry id of the element type's ops (scr_arr_ops), 0 for every kind * but SCR_ELEM_REF and SCR_ELEM_CREF. */ uint16_t spare; @@ -170,7 +175,34 @@ typedef struct ScrArrExt { /* The element ops of an array whose ops have no registry id * (SCR_ARR_OPS_OVERFLOW). */ const ScrArrOps *ops; + /* The class of an instance of a program class that extends Array + * (scr_arr_subclass_init); NULL for every other array. */ + const struct ScrArrClass *cls; + /* Some slot may hold SCR_ARR_FOREIGN; release and copies walk the states + * for their payloads only then. */ + bool foreign; } ScrArrExt; + +/* A program class that extends Array. Its instances are emitted structs + * that begin with a ScrArr (the reference count is the array's) and keep + * the class's own fields after it; they always have an ext, whose `cls` + * names the class. Every array entry point accepts them as arrays. */ +typedef struct ScrArrClass { + /* The class's name, as constructor.name reads it. */ + const char *name; + /* Destroys an instance whose count reached zero: releases the class's own + * fields, then scr_arr_subclass_drop, then frees the instance. */ + void (*destroy)(void *a); + /* The class's own fields as a dyn object in declaration order (the array + * is passed owned, +1), or NULL when the class declares none. util.inspect + * and Object.keys list them after the elements. */ + struct ScrDyn *(*fields)(ScrArr *a); + /* `new C(length)` (`new C()` without `with_length`): the instance + * ArraySpeciesCreate makes for map, filter, slice, splice, concat, flat + * and flatMap, and Array.of / Array.from called on the class. NULL when + * the class's constructor cannot take that argument. */ + ScrArr *(*species)(double length, bool with_length); +} ScrArrClass; /* The LLVM backend mirrors ScrArr as %ScrArr: its inline retain/release read * `cyc` (field 4, i8) to decide mark-live / candidate buffering exactly like * scr_arr_retain and scr_arr_release; its inline element access uses len and @@ -204,6 +236,10 @@ _Static_assert(sizeof(ScrArr) == sizeof(size_t) + 16 + sizeof(void *), #define SCR_ARR_UNSHIFTED 4u #define SCR_ARR_HAS_EXT 8u #define SCR_ARR_STATES 16u +/* Object.freeze of the array (scr_arr_freeze). Emitted guards test it + * before the writes of array types a program may freeze; the runtime's own + * writes assume it is clear. */ +#define SCR_ARR_FROZEN 32u /* An array starts without state bytes. Without them, every index below * `len` is a present value in dense storage: len <= cap, there are no @@ -214,6 +250,23 @@ _Static_assert(sizeof(ScrArr) == sizeof(size_t) + 16 + sizeof(void *), * states (scr_arr_add_states), which it keeps. */ static inline bool scr_arr_has_states(const ScrArr *a) { return (a->spare & SCR_ARR_STATES) != 0; } +static inline bool scr_arr_frozen(const ScrArr *a) { return (a->spare & SCR_ARR_FROZEN) != 0; } +/* Sets SCR_ARR_FROZEN; immortal arrays are left as they are. */ +void scr_arr_freeze(ScrArr *a); +bool scr_arr_is_frozen(const ScrArr *a); +/* Throws Node's TypeError for the first write that the array operation + * `op` makes to a frozen array of length `len`, or answers false when the + * operation writes nothing. op: 0 index write (x = index), 1 push (y = + * argument count), 2 pop, 3 length write, 4 unshift (y = argument count), + * 5 delete (x = index), 6 shift, 7 reverse or sort, 8 fill (x = start, y = + * end), 9 copyWithin (x = target, y = start, z = end), 10 splice (x = + * start, y = delete count, z = item count). */ +bool scr_array_frozen_error(int op, size_t len, double x, double y, double z); +/* Node's TypeErrors for a write that a non-extensible or sealed array + * refuses: adding the element at `index`, or deleting it. */ +void scr_array_not_extensible_error(double index); +void scr_array_undeletable_error(double index); + /* The start of the dense slots' allocation: `data` less any head room. */ static inline uint64_t *scr_arr_block(const ScrArr *a) { return (a->spare & SCR_ARR_HEAD_ROOM) ? a->data - (size_t)a->data[-1] : a->data; @@ -236,7 +289,7 @@ static inline struct ScrArrExt *scr_arr_ext_of(const ScrArr *a) { * Id 0 is no ops; ids are assigned on each descriptor's first array, in * any thread. A program with more element types than the field holds keeps * the ops of the rest in the array's ext (SCR_ARR_OPS_OVERFLOW). */ -#define SCR_ARR_OPS_SHIFT 5 +#define SCR_ARR_OPS_SHIFT 6 #define SCR_ARR_OPS_OVERFLOW (UINT16_MAX >> SCR_ARR_OPS_SHIFT) extern const ScrArrOps *scr_arr_ops_table[SCR_ARR_OPS_OVERFLOW]; /* By registry id: 1 when the descriptor has SCR_ARR_OPS_RELEASE_NOTHING. @@ -453,11 +506,59 @@ static inline ScrArr *scr_arr_retain(ScrArr *a) { * NULL-tolerant (uninitialized locals). Cycle-capable arrays (`cyc`) feed * the candidate-root buffer like every headered type. */ void scr_arr_release(ScrArr *a); +/* Initializes the ScrArr prefix of a zeroed, freshly allocated instance of + * an Array subclass (count already 1): no elements, the element kind and + * ops, and an ext naming the class. `cyc` says the instance carries the + * collector header (its class is cycle-capable). */ +void scr_arr_subclass_init(ScrArr *a, uint8_t elem, const ScrArrOps *ops, const ScrArrClass *cls, + bool cyc); +/* The collector's teardown of an Array subclass instance's storage: the + * elements are released unless they are the collector's (`traced`). */ +void scr_arr_subclass_gc_drop(ScrArr *a); +/* The collector trace of an Array subclass instance's elements: visits + * them when its current element ops trace them. */ +void scr_arr_subclass_trace(void *a, ScrTraceVisit visit, void *ctx); +/* super(...args) in a class that extends Array (the Array constructor's + * length-or-elements rule); `args` is borrowed. */ +void scr_arr_subclass_construct(ScrArr *a, ScrArr *args, bool dyn_elems); +/* The implicit constructor's super(...args) over its dyn argument array. */ +void scr_arr_subclass_construct_dyn(ScrArr *a, const struct ScrDyn *args, bool dyn_elems); +/* ArraySpeciesCreate over the result `result` (owned) of an Array method + * called on `recv` (borrowed): a plain receiver keeps the result; a + * subclass instance's result is `new C(ctor_length)`, whose indices then + * receive the result's present elements, and whose length is set to the + * result's when `set_length` (slice, splice and concat set it; map, + * filter, flat and flatMap do not). A negative `ctor_length` passes the + * result's length. Result owned. */ +ScrArr *scr_arr_species_adopt(ScrArr *recv, ScrArr *result, double ctor_length, bool set_length); +/* ArraySpeciesCreate ahead of an Array method that calls back into the + * program (map, filter, flatMap), so the species constructor runs before + * the first callback: a subclass instance `recv` (borrowed) answers + * `new C(ctor_length)`, a plain receiver itself. Result owned. */ +ScrArr *scr_arr_species_create(ScrArr *recv, double ctor_length); +/* The present elements of the method's result `result` (owned) defined on + * the instance scr_arr_species_create answered (`species`, borrowed), and + * its length set to the result's when `set_length`; a plain receiver's + * result as it is. Result owned. */ +ScrArr *scr_arr_species_finish(ScrArr *species, ScrArr *result, bool set_length); +/* Array.of / Array.from called on an Array subclass: `new C(ctor_length)` + * (`new C()` when ctor_length is negative), then the elements of `result` + * (owned) and its length, as scr_arr_species_adopt. */ +ScrArr *scr_arr_class_construct_adopt(const ScrArrClass *cls, ScrArr *result, double ctor_length); +/* `value instanceof C` for a dyn value and a class that extends Array: a + * live view of an instance of C, or a snapshot of one. */ +bool scr_dyn_is_array_class(const struct ScrDyn *d, const ScrArrClass *cls); +/* The class of an Array subclass instance, or NULL for a plain array. */ +const ScrArrClass *scr_arr_class_of(const ScrArr *a); +/* Releases the elements of an Array subclass instance and frees its + * storage and ext, leaving the instance itself to its class's destroy. */ +void scr_arr_subclass_drop(ScrArr *a); void scr_arr_copy_metadata(ScrArr *a, struct ScrDyn *target); /* The commit of a live array view calls this on `a`, the array converted * from `view`, before `target` takes its storage: `a` takes the view's named - * (non-index) properties as its metadata and the noncanonical numeric - * properties of `target`, so neither is lost when the storage is swapped. */ + * (non-index) properties as its metadata, the noncanonical numeric + * properties of `target` and the class of an Array subclass instance, so + * none is lost when the storage is swapped. */ void scr_arr_adopt_view_properties(ScrArr *a, struct ScrDyn *view, ScrArr *target); /* True when `key` is the string spelling of a number that is not an array * index ("-1", "1.5", "NaN", "Infinity"), or "undefined", with the number in @@ -573,6 +674,20 @@ void scr_arr_set_f64(ScrArr *a, double i, double v); void scr_arr_set_bool(ScrArr *a, double i, bool v); void scr_arr_set_ref(ScrArr *a, double i, void *v); void scr_arr_set_undefined(ScrArr *a, double i); +/* Stores `v` (borrowed, retained) at a canonical index as a value outside + * the element type (SCR_ARR_FOREIGN), or as a present undefined when it is + * undefined, extending the length like the typed setters. False, with + * nothing stored, for an index that is not canonical and for a foreign + * value in a boolean array, whose 1-byte dense slots cannot hold it. */ +bool scr_arr_set_foreign(ScrArr *a, double i, struct ScrDyn *v); +/* The value of a foreign element (+1), or NULL when a[i] is not one. */ +struct ScrDyn *scr_arr_get_foreign(const ScrArr *a, double i); +/* Throws the checked conversion's TypeError for the foreign element a[i] + * read where a value of type `want` is expected. */ +void scr_arr_foreign_throw(const ScrArr *a, double i, const ScrStr *want); +/* The same TypeError for a foreign element's value removed from an array + * (pop/shift into a typed result); takes and releases `value`. */ +void scr_dyn_foreign_throw(struct ScrDyn *value, const char *want); bool scr_arr_delete(ScrArr *a, double i); /* slice(start?, end?): a fresh +1 shallow copy of the index range — @@ -625,6 +740,10 @@ double scr_arr_unshift_spread(ScrArr *a, const ScrArr *src); ScrArr *scr_arr_reverse(ScrArr *a); /* Default primitive String ordering; stable, borrowed receiver, owned result. */ ScrArr *scr_arr_sort_primitive(ScrArr *a, bool copy); +/* Default ordering of reference elements by their String() text, which + * `text` answers (+1) for each element once (Date arrays). Like + * scr_arr_sort_primitive otherwise. */ +ScrArr *scr_arr_sort_by_text(ScrArr *a, bool copy, ScrStr *(*text)(void *)); /* Stable comparator sort of the first `count` slots of a private, compacted * snapshot (every slot a value; f64 or reference elements). `f` is a borrowed * closure of `arity` (0..2) element parameters returning a number. Elements diff --git a/packages/runtime/src/scr_assert.c b/packages/runtime/src/scr_assert.c index 4917b1291..bf5d1c875 100644 --- a/packages/runtime/src/scr_assert.c +++ b/packages/runtime/src/scr_assert.c @@ -212,7 +212,56 @@ bool scr_assert_skip_prototype(void) { return (g_instance_options & SCR_ASSERT_SKIP_PROTOTYPE) != 0; } +/* A message argument that is not a string (an untyped value, or an Error + * to throw): the compiled call registers it before the assertion and passes + * SCR_ASSERT_VALUE_MESSAGE as its message, which the failure paths compose + * like any custom message. A failure whose message starts with it throws + * the Error itself (innerFail's `message instanceof Error`) or puts + * String(value) in its place with generatedMessage `!value`, as Node's + * AssertionError does. Matching the sentinel keeps a registration that no + * failure consumed from reaching another assertion. */ +static SCR_TL ScrDyn *g_message_value; +static SCR_TL bool g_message_error; + +void scr_assert_message_value(ScrDyn *value, bool error) { + if (g_message_value) scr_dyn_release(g_message_value); + g_message_value = scr_dyn_retain(value); + g_message_error = error; +} + +void scr_assert_message_clear(void) { + if (g_message_value) scr_dyn_release(g_message_value); + g_message_value = NULL; +} + void scr_assert_fail(ScrStr *message, bool generated) { + static const char sentinel[] = SCR_ASSERT_VALUE_MESSAGE; + const size_t n = sizeof sentinel - 1; + if (g_message_value && message->len >= n && memcmp(message->data, sentinel, n) == 0) { + ScrDyn *value = g_message_value; + g_message_value = NULL; + if (g_message_error) { + scr_str_release(message); + scr_assert_record(NULL, NULL, NULL); + g_instance_options = 0; + scr_dyn_throw(value); + return; + } + ScrStr *text = scr_dyn_string_coerce_js(value); + generated = !scr_dyn_truthy(value); + scr_dyn_release(value); + if (!text) { + scr_str_release(message); + return; + } + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_write(&b, text->data, text->len); + scr_jb_write(&b, message->data + n, message->len - n); + scr_str_release(text); + scr_str_release(message); + message = scr_jb_finish(&b); + } ScrError *e = scr_assert_error(message, generated, scr_assert_diff_full() ? "full" : "simple"); g_instance_options = 0; scr_throw_obj(e, &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg()); @@ -2703,6 +2752,60 @@ bool scr_assert_no_error_predicate(ScrDyn *predicate, ScrDyn *actual) { return matched; } +/* assert.throws/rejects with a validation function (expectedException's + * function branch, after its instanceof check): the function runs with a + * fresh object as `this` and the caught value, and any result but true + * fails, with Node's generated message unless the call passed one. The + * caught value and the function are the AssertionError's actual and + * expected. Borrows all. */ +void scr_assert_throws_validate(ScrDyn *actual, ScrDyn *validate, ScrStr *op, ScrStr *msg, + bool has_msg) { + ScrDyn *receiver = scr_dyn_new_obj(); + scr_dyn_this_push_dyn(receiver); + ScrDyn *result = scr_dyn_call(validate, &actual, 1, "expected"); + scr_dyn_this_pop(); + scr_dyn_release(receiver); + if (!result) return; + if (result->kind == SCR_DYN_BOOL && result->v.b) { + scr_dyn_release(result); + return; + } + ScrStr *message; + bool generated = !(has_msg && msg->len > 0); + if (!generated) { + message = scr_str_retain(msg); + } else { + ScrAssertBuf b = {0}; + ab_cstr(&b, "The "); + ScrDyn *name = scr_dyn_fn_get(validate, "name", 4); + if (name && name->kind == SCR_DYN_STR && name->v.str->len > 0) { + ab_cstr(&b, "\""); + ab_bytes(&b, name->v.str->data, name->v.str->len); + ab_cstr(&b, "\" "); + } + if (name) scr_dyn_release(name); + ab_cstr(&b, "validation function is expected to return \"true\". Received "); + ScrStr *shown = scr_assert_cf_inspect(result); + ab_bytes(&b, shown->data, shown->len); + scr_str_release(shown); + if (scr_dyn_ref_is_error(actual)) { + ScrStr *text = scr_dyn_string_coerce_js(actual); + if (!text) { + scr_dyn_release(result); + scr_str_release(ab_take(&b)); + return; + } + ab_cstr(&b, "\n\nCaught error:\n\n"); + ab_bytes(&b, text->data, text->len); + scr_str_release(text); + } + message = ab_take(&b); + } + scr_dyn_release(result); + scr_assert_record_dyn(actual, validate, op); + scr_assert_fail(message, generated); +} + /* ── assert.ifError ─────────────────────────────────────────────────── * Node throws for ANY value but null/undefined (falsy included); the * frontend routes null/undefined to a no-op and everything else here. @@ -2879,3 +2982,225 @@ ScrError *scr_assert_error_new(ScrDyn *options) { scr_assert_attach_values(e, generated, actual, expected, op, diff); return e; } + +/* ── assert functions as values ────────────────────────────────────────── + * `assert.fail` stored or passed as a function (`emitter.on("x", + * assert.fail)`): one function per method, which takes its arguments as + * untyped values and asserts as the method does. */ + +/* Node's message argument: absent or null/undefined (a generated message), + * a string, an Error (thrown when the assertion fails), or another value + * (String(value)). The value is registered as scr_assert_fail reads it; the + * caller clears it after the assertion. +1 into *msg. */ +static void scr_av_message(ScrDyn *const *args, size_t argc, size_t i, ScrStr **msg, bool *has) { + const ScrDyn *m = i < argc ? args[i] : NULL; + if (!m || m->kind == SCR_DYN_UNDEF || m->kind == SCR_DYN_NULL) { + *msg = scr_str_new("", 0); + *has = false; + return; + } + *has = true; + if (m->kind == SCR_DYN_STR) { + *msg = scr_str_retain(m->v.str); + return; + } + bool error = scr_dyn_ref_is_error(m) || + (m->kind == SCR_DYN_OBJ && scr_dyn_obj_get(m, "%error", 6) != NULL); + scr_assert_message_value((ScrDyn *)m, error); + *msg = scr_str_new(SCR_ASSERT_VALUE_MESSAGE, sizeof SCR_ASSERT_VALUE_MESSAGE - 1); +} + +static const ScrDyn *scr_av_arg(ScrDyn *const *args, size_t argc, size_t i) { + return i < argc ? args[i] : scr_dyn_undefined(); +} + +static ScrDyn *scr_av_done(ScrStr *msg) { + scr_str_release(msg); + scr_assert_message_clear(); + return scr_dyn_retain(scr_dyn_undefined()); +} + +/* assert(value, message) and assert.ok: Node's generated message quotes + * the call's source, which a function value has no fixed site for; the + * message here is AssertionError's own `value == true`. */ +static ScrDyn *scr_av_ok(ScrClosure *c, ScrDyn *const *args, size_t argc) { + (void)c; + const ScrDyn *value = scr_av_arg(args, argc, 0); + if (scr_dyn_truthy(value)) return scr_dyn_retain(scr_dyn_undefined()); + scr_assert_record(scr_dyn_retain((ScrDyn *)value), scr_dyn_new_bool(true), "=="); + if (argc == 0) { + static const char text[] = "No value argument passed to `assert.ok()`"; + scr_assert_fail(scr_str_new(text, sizeof text - 1), true); + return NULL; + } + ScrStr *msg; + bool has; + scr_av_message(args, argc, 1, &msg, &has); + if (has) { + scr_assert_fail(scr_str_retain(msg), false); + return scr_av_done(msg); + } + ScrStr *shown = scr_assert_cf_inspect(value); + ScrAssertBuf b = {0}; + ab_str(&b, shown); + ab_cstr(&b, " == true"); + scr_str_release(shown); + scr_assert_fail(ab_take(&b), true); + return scr_av_done(msg); +} + +/* assert.fail(message): "Failed" without a message. */ +static ScrDyn *scr_av_fail(ScrClosure *c, ScrDyn *const *args, size_t argc) { + (void)c; + if (argc > 1) { + static const char text[] = "assert.fail with more than one argument through a function value is not supported yet"; + scr_throw_error_msg_code(SCR_ERR_ERROR, text, sizeof text - 1, "SC2020"); + return NULL; + } + ScrStr *msg; + bool has; + scr_av_message(args, argc, 0, &msg, &has); + scr_assert_record(scr_dyn_retain(scr_dyn_undefined()), scr_dyn_retain(scr_dyn_undefined()), "fail"); + if (has) scr_assert_fail(scr_str_retain(msg), false); + else scr_assert_fail(scr_str_new("Failed", 6), true); + return scr_av_done(msg); +} + +/* The comparison methods take both operands: Node's ERR_MISSING_ARGS + * TypeError otherwise. False with it pending. */ +static bool scr_av_operands(size_t argc) { + if (argc >= 2) return true; + static const char text[] = "The \"actual\" and \"expected\" arguments must be specified"; + scr_throw_error_msg_code(SCR_ERR_TYPE, text, sizeof text - 1, "ERR_MISSING_ARGS"); + return false; +} + +/* equal / notEqual: Node's loose comparison, NaN equal to NaN. */ +static ScrDyn *scr_av_loose(ScrDyn *const *args, size_t argc, bool negated) { + if (!scr_av_operands(argc)) return NULL; + const ScrDyn *a = scr_av_arg(args, argc, 0), *b = scr_av_arg(args, argc, 1); + bool nan = a->kind == SCR_DYN_NUM && b->kind == SCR_DYN_NUM && isnan(a->v.num) && isnan(b->v.num); + bool equal = nan || scr_dyn_abstract_eq(a, b); + if (scr_exc_pending()) return NULL; + if (negated ? !equal : equal) return scr_dyn_retain(scr_dyn_undefined()); + ScrStr *msg; + bool has; + scr_av_message(args, argc, 2, &msg, &has); + scr_assert_record(scr_dyn_retain((ScrDyn *)a), scr_dyn_retain((ScrDyn *)b), negated ? "!=" : "=="); + ScrStr *ia = scr_assert_cf_inspect(a), *ib = scr_assert_cf_inspect(b); + scr_assert_loose_result(equal, negated, ia, ib, msg, has); + scr_str_release(ia); + scr_str_release(ib); + return scr_av_done(msg); +} + +static ScrDyn *scr_av_equal(ScrClosure *c, ScrDyn *const *a, size_t n) { + (void)c; + return scr_av_loose(a, n, false); +} +static ScrDyn *scr_av_not_equal(ScrClosure *c, ScrDyn *const *a, size_t n) { + (void)c; + return scr_av_loose(a, n, true); +} + +/* The strict and deep comparisons, through the untyped assert paths. */ +enum { SCR_AV_STRICT, SCR_AV_DEEP_STRICT, SCR_AV_DEEP, SCR_AV_PARTIAL }; + +static ScrDyn *scr_av_compare(ScrDyn *const *args, size_t argc, int form, bool negated) { + if (!scr_av_operands(argc)) return NULL; + ScrDyn *a = (ScrDyn *)scr_av_arg(args, argc, 0), *b = (ScrDyn *)scr_av_arg(args, argc, 1); + ScrStr *msg; + bool has; + scr_av_message(args, argc, 2, &msg, &has); + if (form == SCR_AV_DEEP) scr_assert_loose_deep_dyn(a, b, negated, msg, has); + else if (form == SCR_AV_PARTIAL) scr_assert_partial_deep_dyn(a, b, msg, has); + else scr_assert_eq_dyn(a, b, negated, form == SCR_AV_DEEP_STRICT, msg, has); + return scr_av_done(msg); +} + +static ScrDyn *scr_av_strict_equal(ScrClosure *c, ScrDyn *const *a, size_t n) { + (void)c; + return scr_av_compare(a, n, SCR_AV_STRICT, false); +} +static ScrDyn *scr_av_not_strict_equal(ScrClosure *c, ScrDyn *const *a, size_t n) { + (void)c; + return scr_av_compare(a, n, SCR_AV_STRICT, true); +} +static ScrDyn *scr_av_deep_strict_equal(ScrClosure *c, ScrDyn *const *a, size_t n) { + (void)c; + return scr_av_compare(a, n, SCR_AV_DEEP_STRICT, false); +} +static ScrDyn *scr_av_not_deep_strict_equal(ScrClosure *c, ScrDyn *const *a, size_t n) { + (void)c; + return scr_av_compare(a, n, SCR_AV_DEEP_STRICT, true); +} +static ScrDyn *scr_av_deep_equal(ScrClosure *c, ScrDyn *const *a, size_t n) { + (void)c; + return scr_av_compare(a, n, SCR_AV_DEEP, false); +} +static ScrDyn *scr_av_not_deep_equal(ScrClosure *c, ScrDyn *const *a, size_t n) { + (void)c; + return scr_av_compare(a, n, SCR_AV_DEEP, true); +} +static ScrDyn *scr_av_partial_deep_strict_equal(ScrClosure *c, ScrDyn *const *a, size_t n) { + (void)c; + return scr_av_compare(a, n, SCR_AV_PARTIAL, false); +} + +static ScrDyn *scr_av_if_error(ScrClosure *c, ScrDyn *const *args, size_t argc) { + (void)c; + scr_assert_iferror_dyn(scr_av_arg(args, argc, 0)); + return scr_exc_pending() ? NULL : scr_dyn_retain(scr_dyn_undefined()); +} + +static const struct { + const char *name; + ScrDynThunk thunk; + uint32_t arity; +} scr_av_methods[] = { + {"ok", &scr_av_ok, 0}, + {"fail", &scr_av_fail, 1}, + {"equal", &scr_av_equal, 3}, + {"notEqual", &scr_av_not_equal, 3}, + {"strictEqual", &scr_av_strict_equal, 3}, + {"notStrictEqual", &scr_av_not_strict_equal, 3}, + {"deepEqual", &scr_av_deep_equal, 3}, + {"notDeepEqual", &scr_av_not_deep_equal, 3}, + {"deepStrictEqual", &scr_av_deep_strict_equal, 3}, + {"notDeepStrictEqual", &scr_av_not_deep_strict_equal, 3}, + {"partialDeepStrictEqual", &scr_av_partial_deep_strict_equal, 3}, + {"ifError", &scr_av_if_error, 1}, +}; +#define SCR_AV_COUNT (sizeof scr_av_methods / sizeof scr_av_methods[0]) +static SCR_TL ScrDyn *scr_av_values[SCR_AV_COUNT]; +static SCR_TL bool scr_av_registered; + +static void scr_av_cleanup(void) { + for (size_t i = 0; i < SCR_AV_COUNT; i++) { + scr_dyn_release(scr_av_values[i]); + scr_av_values[i] = NULL; + } + scr_av_registered = false; +} + +/* The function value of an assert method by its canonical name (the frontend + * maps assert/strict's aliases), +1; one value per method, so stored copies + * compare equal. */ +ScrDyn *scr_assert_method_value(ScrStr *name) { + for (size_t i = 0; i < SCR_AV_COUNT; i++) { + if (strlen(scr_av_methods[i].name) != name->len || + memcmp(scr_av_methods[i].name, name->data, name->len) != 0) + continue; + if (!scr_av_values[i]) { + if (!scr_av_registered) { + scr_av_registered = true; + scr_atexit(scr_av_cleanup); + } + scr_av_values[i] = scr_dyn_new_func(scr_closure_new(NULL, 0), scr_av_methods[i].thunk, + scr_av_methods[i].arity, "native:assert", + scr_av_methods[i].name); + } + return scr_dyn_retain(scr_av_values[i]); + } + return scr_dyn_retain(scr_dyn_undefined()); +} diff --git a/packages/runtime/src/scr_assert.h b/packages/runtime/src/scr_assert.h index 8037f08ec..57158a69a 100644 --- a/packages/runtime/src/scr_assert.h +++ b/packages/runtime/src/scr_assert.h @@ -24,6 +24,19 @@ void scr_assert_fail(ScrStr *message, bool generated); /* takes ownership; alway * consumes them; a passing call clears them. */ void scr_assert_set_options(int options); void scr_assert_clear_options(void); +/* A message argument that is not a string, for the next assertion that + * fails with SCR_ASSERT_VALUE_MESSAGE as its message (scr_assert.c): an + * Error (`error`) is thrown as it is, anything else reports String(value). + * Borrows value; clear drops a registration the call did not consume. */ +#define SCR_ASSERT_VALUE_MESSAGE "\x01scriptc:assert-message-value" +void scr_assert_message_value(struct ScrDyn *value, bool error); +void scr_assert_message_clear(void); +/* An assert method as a function value, by canonical name (+1). */ +struct ScrDyn *scr_assert_method_value(ScrStr *name); +/* assert.throws/rejects with a validation function: its result must be + * true (Node's expectedException), else the AssertionError. Borrows all. */ +void scr_assert_throws_validate(struct ScrDyn *actual, struct ScrDyn *validate, ScrStr *op, + ScrStr *msg, bool has_msg); bool scr_assert_diff_full(void); bool scr_assert_skip_prototype(void); void scr_assert_record(ScrDyn *actual, ScrDyn *expected, const char *op); /* values move in */ diff --git a/packages/runtime/src/scr_async.c b/packages/runtime/src/scr_async.c index 8f231b2be..0f18756e1 100644 --- a/packages/runtime/src/scr_async.c +++ b/packages/runtime/src/scr_async.c @@ -753,11 +753,13 @@ static void scr_als_prefix_end(ScrFiber *f) { } } -/* A synchronous generator has no context of its own: each step runs in its - * resumer's execution, as Node runs it. A fiber-backed one switches to its - * own slot, so its step starts from the resumer's store and a store it - * enters is the resumer's after the step (scr_als_prefix_end on the way - * back); a stackless one runs in the resumer's slot (scr_gen_coro_hop). */ +/* A generator step runs in its resumer's execution, as Node runs it. A + * fiber-backed generator switches to its own slot, so its step starts from + * the resumer's store and a store it enters is the resumer's after the + * step (scr_als_prefix_end on the way back); a stackless synchronous one + * runs in the resumer's slot (scr_gen_coro_hop). An async generator's step + * starts the same way, and after an await it continues in its own copy of + * the context, as an async function's body does after its prefix. */ static void scr_als_step_begin(ScrFiber *f) { ScrAlsCtx **slot = SCR_ALS_SLOT(); if (f->als != *slot) { @@ -812,6 +814,17 @@ void scr_als_restore(ScrAlsCtx *prev) { *SCR_ALS_SLOT() = prev; /* ownership moves back in */ } +ScrAlsCtx *scr_als_dispatch_enter(ScrAlsCtx *ctx) { + ScrAlsCtx **slot = SCR_ALS_SLOT(); + ScrAlsCtx *prev = *slot; + *slot = scr_als_ctx_retain(ctx); + return prev; +} + +void scr_als_dispatch_leave(ScrAlsCtx *prev) { scr_als_restore(prev); } + +ScrAlsCtx *scr_als_capture(void) { return scr_als_ctx_retain(*SCR_ALS_SLOT()); } + /* The value of `id` in the active snapshot (borrowed), or NULL. */ ScrDyn *scr_als_peek(double id) { const ScrAlsCtx *c = *SCR_ALS_SLOT(); @@ -1350,11 +1363,14 @@ static SCR_TL ScrNtick *scr_nt_tail = NULL; #ifdef SCR_NODE_ISLANDS static void scr_hosted_tick(ScrNtick *t); #endif +static ScrClosure *scr_als_bind(ScrClosure *cb /*moves*/); +/* A tick runs with the AsyncLocalStorage context active when it was + * queued, as Node's process.nextTick does. */ void scr_next_tick(ScrClosure *cb /*moves*/) { - ScrNtick *t = calloc(1, sizeof *t); + ScrNtick *t = scr_mem_calloc(sizeof *t); if (!t) scr_oom(); - t->cb = cb; + t->cb = scr_als_bind(cb); #ifdef SCR_NODE_ISLANDS if (scr_loop_hosted != NULL) { scr_hosted_tick(t); @@ -1371,7 +1387,7 @@ void scr_next_tick(ScrClosure *cb /*moves*/) { * adapter ABI also used by fs.rename: emitted adapters construct the * program's Error | null union, and NULL selects its null arm. */ void scr_process_write_callback(ScrClosure *cb /*moves*/, ScrFsRenameFn fn) { - ScrNtick *t = calloc(1, sizeof *t); + ScrNtick *t = scr_mem_calloc(sizeof *t); if (!t) scr_oom(); t->cb = cb; t->error_cb = fn; @@ -1392,7 +1408,7 @@ void scr_process_write_callback(ScrClosure *cb /*moves*/, ScrFsRenameFn fn) { * endReadableNT, ...). The hook dispatches exactly one stream tick; * teardown just drops markers (the stream queue owns its entries). */ void scr_next_tick_raw(void (*fn)(void)) { - ScrNtick *t = calloc(1, sizeof *t); + ScrNtick *t = scr_mem_calloc(sizeof *t); if (!t) scr_oom(); t->raw = fn; #ifdef SCR_NODE_ISLANDS @@ -1409,7 +1425,7 @@ void scr_next_tick_raw(void (*fn)(void)) { /* A raw tick at the front of the queue: it runs before the ticks already * queued. */ static void scr_next_tick_raw_first(void (*fn)(void)) { - ScrNtick *t = calloc(1, sizeof *t); + ScrNtick *t = scr_mem_calloc(sizeof *t); if (!t) scr_oom(); t->raw = fn; /* Hosted, Node's tick queue has no front: the tick queues last. */ @@ -1429,7 +1445,7 @@ void scr_nticks_teardown(void) { ScrNtick *t = scr_nt_head; scr_nt_head = t->next; if (t->cb) scr_closure_release(t->cb); - free(t); + scr_mem_free(t); } scr_nt_tail = NULL; } @@ -2026,6 +2042,12 @@ void scr_promise_all_store_ref(ScrArr *a, double i, ScrPromise *src) { scr_arr_set_ref(a, i, scr_promise_payload_ref(src)); } +/* The entry promise itself: Promise.all's tuple form reads each fulfilled + * entry at its own type once the combinator fulfills. */ +void scr_promise_all_store_promise(ScrArr *a, double i, ScrPromise *src) { + scr_arr_set_ref(a, i, scr_promise_retain(src)); +} + /* Fulfillment payload accessors for the emitted race adapters — all * retained/by-value (the source keeps its settlement). */ double scr_promise_payload_f64(ScrPromise *p) { return p->f64; } @@ -4196,9 +4218,50 @@ double scr_set_timeout_handle_als(ScrClosure *cb, double ms) { return scr_set_timeout_handle(scr_als_bind(cb), ms); } double scr_set_immediate_als(ScrClosure *cb) { return scr_set_immediate(scr_als_bind(cb)); } -void scr_next_tick_als(ScrClosure *cb) { scr_next_tick(scr_als_bind(cb)); } +void scr_next_tick_als(ScrClosure *cb) { scr_next_tick(cb); } /* scr_next_tick binds */ void scr_queue_microtask_als(ScrClosure *cb) { scr_queue_microtask(scr_als_bind(cb)); } +/* A typed callback bound to the context active when it was bound: the + * completion callbacks of callback-style APIs in programs that use + * AsyncLocalStorage, and AsyncLocalStorage.bind. The bound closure's entry + * is a thunk the compiler emits per callback signature; it reads the + * target (scr_als_bound_target), enters the captured context + * (scr_als_bound_enter), calls the target with its own arguments and + * restores the caller's context (scr_als_bound_leave). With `always` + * false an empty context leaves the callback unbound, since the loop runs + * callbacks with an empty context; AsyncLocalStorage.bind passes true, so + * a bound function called later inside run() still sees no store. */ +ScrClosure *scr_als_bind_closure(ScrClosure *cb /*moves*/, void *thunk, bool always) { + ScrAlsCtx *active = *SCR_ALS_SLOT(); + if (cb == NULL || (active == NULL && !always)) return cb; + ScrClosure *bound = scr_closure_new(thunk, 2); + bound->caps[0] = scr_box_new(SCR_BOX_FUNC); + scr_box_set_ref(bound->caps[0], cb); + bound->caps[1] = scr_box_new_obj(scr_als_ctx_retain_v, scr_als_ctx_release_v, NULL); + scr_box_set_ref(bound->caps[1], scr_als_ctx_retain(active)); + return bound; +} + +/* The bound target (+1, released by scr_als_bound_leave). */ +ScrClosure *scr_als_bound_target(ScrClosure *self) { + return scr_box_get_ref(self->caps[0]); +} + +/* Enters the captured context; returns the caller's (owned). */ +ScrAlsCtx *scr_als_bound_enter(ScrClosure *self) { + ScrAlsCtx **slot = SCR_ALS_SLOT(); + ScrAlsCtx *prev = *slot; + *slot = scr_box_get_ref(self->caps[1]); /* the slot owns this reference */ + return prev; +} + +/* Restores the caller's context, replacing whatever the target entered + * (an enterWith inside it), and releases the target. */ +void scr_als_bound_leave(ScrAlsCtx *prev, ScrClosure *target) { + scr_als_restore(prev); + scr_closure_release(target); +} + /* The checked-dynamic argument form (JS files — common.mustCall wrappers * and the suite's invalid-input probes): a non-function throws Node's * ERR_INVALID_ARG_TYPE synchronously; a function value is called with @@ -4871,7 +4934,7 @@ void scr_loop_hosted_run(uint32_t kind, uint32_t token) { ScrClosure *cb = t->cb; ScrFsRenameFn error_cb = t->error_cb; void (*raw)(void) = t->raw; - free(t); + scr_mem_free(t); if (cb) { if (error_cb) error_cb(cb, NULL); else ((void (*)(ScrClosure *))cb->fn)(cb); @@ -5180,7 +5243,7 @@ static bool scr_loop_run_ticks(void) { ScrClosure *cb = t->cb; ScrFsRenameFn error_cb = t->error_cb; void (*raw)(void) = t->raw; - free(t); + scr_mem_free(t); if (cb) { if (error_cb) error_cb(cb, NULL); else ((void (*)(ScrClosure *))cb->fn)(cb); @@ -6134,6 +6197,10 @@ struct ScrGen { * since that return takes its own turn to await x. */ bool return_queued; bool return_awaited; + /* The AsyncLocalStorage context of a return() request deferred at a + * yield (+1): the body's unwind runs in it, as Node resumes it from the + * return value's await. */ + ScrAlsCtx *return_ctx; #ifdef SCR_WORKERS bool context_abandoned; #endif @@ -6237,6 +6304,7 @@ void scr_gen_release(ScrGen *g) { scr_gen_slot_reset(&g->in); scr_gen_slot_reset(&g->ret); scr_gen_exc_reset(&g->async_error); + scr_als_ctx_release(g->return_ctx); if (g->fiber != NULL) { if (g->state == SCR_GEN_UNSTARTED) { /* Never ran: nothing on the stack owns anything — clean teardown. @@ -6591,6 +6659,12 @@ __attribute__((always_inline)) static inline void scr_gen_switched_back(ScrGen * if (release) scr_gen_release(g); } +/* Out of line, so the hop that synchronous generators share stays as it + * was: an async generator's step begins from its resumer's context and + * hands the prefix's context back at its first suspension. */ +static __attribute__((noinline)) void scr_async_gen_step_begin(ScrFiber *f) { scr_als_step_begin(f); } +static __attribute__((noinline)) void scr_async_gen_step_end(ScrFiber *f) { scr_als_prefix_end(f); } + #ifndef __wasi__ /* The stackless form of the hop: the coroutine frame runs on this stack * until its next yield or its final suspension returns here. `retain` @@ -6599,14 +6673,17 @@ __attribute__((always_inline)) static inline void scr_gen_switched_back(ScrGen * __attribute__((always_inline)) static inline void scr_gen_coro_hop(ScrGen *g, bool retain) { ScrFiber *f = g->fiber; if (retain) scr_gen_retain(g); + /* A synchronous generator has no AsyncLocalStorage context of its own: + * its step runs in the resumer's slot, as Node runs it in the execution + * that called next(). An async generator's step runs in its own slot, + * which a new step (not the continuation after an await) starts from the + * resumer's context (scr_als_step_begin). */ + if (g->is_async && g->state != SCR_GEN_AWAITING) scr_async_gen_step_begin(f); g->state = SCR_GEN_RUNNING; f->resumer = scr_current; ScrAlsCtx **resumer_slot = scr_als_active; scr_current = f; scr_exc_swap_cell(&f->exc); - /* A synchronous generator has no AsyncLocalStorage context of its own: - * its step runs in the resumer's slot, as Node runs it in the execution - * that called next(). An async generator's step runs in its own. */ if (g->is_async) scr_als_active = &f->als; if (f->coro == NULL) f->entry(f, f->argpack); else scr_coro_resume(f->coro); @@ -6617,6 +6694,7 @@ __attribute__((always_inline)) static inline void scr_gen_coro_hop(ScrGen *g, bo scr_current = me; scr_exc_swap_cell(me != NULL ? &me->exc : NULL); scr_als_active = resumer_slot; + if (g->is_async) scr_async_gen_step_end(f); scr_gen_switched_back(g, f, retain, true); } @@ -6637,8 +6715,10 @@ static void scr_gen_switch_in(ScrGen *g) { } #endif scr_gen_retain(g); + /* Each step starts from the resumer's context; an async generator's + * continuation after an await keeps its own. */ + if (!g->is_async || g->state != SCR_GEN_AWAITING) scr_als_step_begin(f); g->state = SCR_GEN_RUNNING; - if (!g->is_async) scr_als_step_begin(f); #ifdef _WIN32 ScrCtx here = scr_win_self(); #elif defined(__wasi__) @@ -6810,6 +6890,25 @@ void scr_gen_resume_throw(ScrGen *g) { static void scr_async_gen_start_queued(ScrGen *g); +/* Runs fn(g) with `ctx` (borrowed) as the active AsyncLocalStorage + * context. A settlement job runs from the loop: a request queued behind + * another starts in the context of the step before it (Node drains the + * queue in the generator's own execution), and a deferred return() in its + * call's. */ +static __attribute__((noinline)) void scr_async_gen_in_context(ScrGen *g, ScrAlsCtx *ctx, + void (*fn)(ScrGen *)) { + ScrAlsCtx **slot = SCR_ALS_SLOT(); + ScrAlsCtx *prev = *slot; + *slot = scr_als_ctx_retain(ctx); + fn(g); + scr_als_restore(prev); +} + +/* Starts the queued requests in the context of the step before them. */ +static __attribute__((noinline)) void scr_async_gen_start_queued_in_own(ScrGen *g) { + scr_async_gen_in_context(g, g->fiber != NULL ? g->fiber->als : NULL, scr_async_gen_start_queued); +} + static void scr_async_gen_schedule_settle(ScrGen *g) { if (g->settle_queued) return; ScrFiber *job = scr_mem_calloc(sizeof *job); @@ -6857,6 +6956,11 @@ static bool scr_async_gen_settles_now(ScrGen *g) { return false; } +static __attribute__((noinline)) void scr_async_gen_capture_return_ctx(ScrGen *g) { + scr_als_ctx_release(g->return_ctx); + g->return_ctx = scr_als_ctx_retain(*SCR_ALS_SLOT()); +} + static void scr_async_gen_run_active(ScrGen *g, ScrAsyncGenRequest *req) { const bool was_done = g->state == SCR_GEN_DONE; if (req->mode == SCR_AG_NEXT) { @@ -6872,6 +6976,7 @@ static void scr_async_gen_run_active(ScrGen *g, ScrAsyncGenRequest *req) { /* At a yield, AsyncGeneratorYield awaits the return value before * the body resumes, so finally blocks run one turn later. */ g->return_queued = true; + scr_async_gen_capture_return_ctx(g); scr_async_gen_schedule_settle(g); return; } @@ -6902,6 +7007,16 @@ static void scr_async_gen_run_active(ScrGen *g, ScrAsyncGenRequest *req) { } } +static void scr_async_gen_resume_returning(ScrGen *g); + +/* The deferred return() resumes in its call's context. */ +static __attribute__((noinline)) void scr_async_gen_resume_returning_in_context(ScrGen *g) { + ScrAlsCtx *ctx = g->return_ctx; + g->return_ctx = NULL; + scr_async_gen_in_context(g, ctx, scr_async_gen_resume_returning); + scr_als_ctx_release(ctx); +} + /* The return() request's await has taken its turn: resume the body with * the return completion. */ static void scr_async_gen_resume_returning(ScrGen *g) { @@ -7034,12 +7149,12 @@ static void scr_async_gen_job(ScrGen *g) { } if (g->return_queued) { scr_gen_retain(g); - scr_async_gen_resume_returning(g); + scr_async_gen_resume_returning_in_context(g); scr_gen_release(g); return; } scr_async_gen_finish_active(g, false); - scr_async_gen_start_queued(g); + scr_async_gen_start_queued_in_own(g); } static void scr_async_gen_resume_ready(ScrGen *g) { @@ -7053,11 +7168,11 @@ static void scr_async_gen_resume_ready(ScrGen *g) { scr_gen_switch_in(g); if (g->async_error.kind != SCR_EXC_NONE) { scr_async_gen_finish_active(g, true); - scr_async_gen_start_queued(g); + scr_async_gen_start_queued_in_own(g); } else if (g->state != SCR_GEN_AWAITING) { if (scr_async_gen_settles_now(g)) { scr_async_gen_finish_active(g, false); - scr_async_gen_start_queued(g); + scr_async_gen_start_queued_in_own(g); } else { scr_async_gen_schedule_settle(g); } diff --git a/packages/runtime/src/scr_async.h b/packages/runtime/src/scr_async.h index fc0128294..e31ba5b1d 100644 --- a/packages/runtime/src/scr_async.h +++ b/packages/runtime/src/scr_async.h @@ -192,12 +192,29 @@ ScrDyn *scr_als_get(double id); /* +1; the undefined value when unset */ ScrAlsCtx *scr_als_enter(double id, ScrDyn *value); /* borrows value; prev out (owned) */ ScrAlsCtx *scr_als_enter_absent(double id); /* exit()'s cleared arm */ void scr_als_restore(ScrAlsCtx *prev); /* moves prev back in */ +/* A runtime dispatch (an I/O event the loop delivers, a deferred emit) + * runs its listeners in the AsyncLocalStorage context of the object or + * request Node would run them in: enter installs `ctx` (borrowed, NULL + * for no store) and answers the previous context, which leave puts back. + * A store a listener enters (enterWith) ends with the dispatch. */ +ScrAlsCtx *scr_als_dispatch_enter(ScrAlsCtx *ctx); +void scr_als_dispatch_leave(ScrAlsCtx *prev); +/* The active context, +1 (NULL outside a store): what a handle keeps. */ +ScrAlsCtx *scr_als_capture(void); void scr_als_enter_with(double id, ScrDyn *value); /* no restore point (enterWith) */ void scr_als_disable(double id); /* run/exit: enter (or clear), call the dyn function with the forwarded * argument vector, restore (the finally). Result +1 or NULL pending. */ ScrDyn *scr_als_run(double id, ScrDyn *value, ScrDyn *fn, ScrDyn *args); ScrDyn *scr_als_exit_run(double id, ScrDyn *fn, ScrDyn *args); +/* new AsyncLocalStorage(options) (options borrowed; NaN with the exception + * pending for a non-object) and the name it carries (+1). */ +double scr_als_new_options(const ScrDyn *options); +ScrDyn *scr_als_name(double id); +/* AsyncLocalStorage.bind over a dyn function and the dyn completion + * callbacks (fn borrowed; +1), and AsyncLocalStorage.snapshot() (+1). */ +ScrDyn *scr_als_bind_dyn(ScrDyn *fn, bool always); +ScrDyn *scr_als_snapshot(void); /* process.on/once('unhandledRejection', fn): dyn listeners dispatched * per never-observed rejection at the end of its nextTick/microtask @@ -252,6 +269,10 @@ void scr_process_set_emit_warning(ScrDyn *fn); * made from node_modules) — at once for a static ES import, which Node * links before any module evaluates, otherwise on the next tick. */ void scr_punycode_warn(bool immediate, bool report); +/* The deprecated Buffer() and new Buffer() called outside node_modules + * (scr_async_dyn.c): DEP0005 on the next tick, once, within Node's + * 10000-call check (scr_buffer_deprecation_count). */ +void scr_buffer_deprecation(void); /* scr_emit_warning once a 'warning' listener exists (scr_lib.c; set by the * listener registration): units that cannot link the dyn-async unit route * their warnings through it, and print the default report otherwise. */ @@ -369,6 +390,8 @@ void scr_promise_all_store_f64(ScrArr *a, double i, ScrPromise *src); void scr_promise_all_store_bool(ScrArr *a, double i, ScrPromise *src); void scr_promise_all_store_str(ScrArr *a, double i, ScrPromise *src); void scr_promise_all_store_ref(ScrArr *a, double i, ScrPromise *src); +/* Stores the fulfilled entry promise itself (+1), for Promise.all's tuple form. */ +void scr_promise_all_store_promise(ScrArr *a, double i, ScrPromise *src); void scr_promise_adapt_copy(ScrPromise *dst, ScrPromise *src); double scr_promise_payload_f64(ScrPromise *p); bool scr_promise_payload_bool(ScrPromise *p); @@ -473,6 +496,13 @@ double scr_set_timeout_handle_als(ScrClosure *cb, double ms); double scr_set_immediate_als(ScrClosure *cb); void scr_next_tick_als(ScrClosure *cb); void scr_queue_microtask_als(ScrClosure *cb); +/* A typed callback bound to the active context (scr_async.c): `thunk` is + * the compiler's per-signature entry, which brackets the call with + * scr_als_bound_target/enter/leave. cb moves in; the result is +1. */ +ScrClosure *scr_als_bind_closure(ScrClosure *cb, void *thunk, bool always); +ScrClosure *scr_als_bound_target(ScrClosure *self); +ScrAlsCtx *scr_als_bound_enter(ScrClosure *self); +void scr_als_bound_leave(ScrAlsCtx *prev, ScrClosure *target); /* A successful process.stdout/stderr.write completion on that same queue. * The callback and its program-shaped Error | null adapter both MOVE into * the entry; the adapter is invoked with NULL (Node's success argument). */ diff --git a/packages/runtime/src/scr_async_dyn.c b/packages/runtime/src/scr_async_dyn.c index a6cd813de..046aebb38 100644 --- a/packages/runtime/src/scr_async_dyn.c +++ b/packages/runtime/src/scr_async_dyn.c @@ -11,6 +11,7 @@ * 'warning' listeners + Node's stderr report). */ #include "scr_runtime.h" +#include #include #include #include @@ -30,6 +31,21 @@ static void scr_ad_oom(void) { static SCR_TL double scr_als_counter = 0; +/* The constructor options of each store, indexed by id - 1: the default + * value getStore() answers where the store has no entry, and the name. + * NULL until a store with options exists. */ +typedef struct { + ScrDyn *default_value; /* NULL: no default */ + ScrDyn *name; /* NULL: "" */ +} ScrAlsOptions; +static SCR_TL ScrAlsOptions *scr_als_options = NULL; +static SCR_TL size_t scr_als_options_len = 0; + +static const ScrAlsOptions *scr_als_options_of(double id) { + size_t i = (size_t)id - 1; + return id >= 1 && i < scr_als_options_len ? &scr_als_options[i] : NULL; +} + /* Main-slot teardown (atexit, LIFO after scr_init's audit registration — * the dc-registry precedent): a top-level enterWith leaves a context in * the main slot at exit; release it before the RC audit counts. */ @@ -37,6 +53,13 @@ static void scr_als_teardown(void) { /* atexit runs on the main context — the active slot IS main's. */ scr_als_ctx_release(*SCR_ALS_SLOT()); *SCR_ALS_SLOT() = NULL; + for (size_t i = 0; i < scr_als_options_len; i++) { + scr_dyn_release(scr_als_options[i].default_value); + scr_dyn_release(scr_als_options[i].name); + } + free(scr_als_options); + scr_als_options = NULL; + scr_als_options_len = 0; } double scr_als_new(void) { @@ -48,6 +71,89 @@ double scr_als_new(void) { return ++scr_als_counter; } +/* options[key] as JavaScript reads it (+1, or NULL with the exception + * pending): inherited properties and getters included. A class instance + * reads its own fields through its snapshot; a member its class declares + * (a getter or a method) has no runtime view here and is refused. */ +static ScrDyn *scr_als_option(ScrDyn *options, const char *key, size_t key_len) { + ScrStr *name = scr_str_new(key, key_len); + if (options->kind == SCR_DYN_TYPED_REF && scr_dyn_typed_ref_class_has(options, name)) { + char message[160]; + snprintf(message, sizeof message, + "AsyncLocalStorage options whose class declares '%s' are not supported yet", key); + scr_str_release(name); + scr_throw_error_msg(SCR_ERR_ERROR, message, strlen(message)); + return NULL; + } + ScrDyn *key_value = scr_dyn_new_str(name); + scr_str_release(name); + ScrDyn *value = scr_dyn_reflect_get(options, key_value, options); + scr_dyn_release(key_value); + if (value == NULL && !scr_exc_pending()) value = scr_dyn_retain(scr_dyn_undefined()); + return value; +} + +/* new AsyncLocalStorage(options): Node validates that options is an + * object, then reads options.defaultValue once and options.name twice + * (whether it is defined, then its value), keeping String(name). NaN with + * the exception pending when options is not an object or a read throws. */ +double scr_als_new_options(const ScrDyn *options) { + if (options->kind == SCR_DYN_UNDEF) return scr_als_new(); + if (options->kind != SCR_DYN_OBJ && options->kind != SCR_DYN_TYPED_REF && options->kind != SCR_DYN_PROXY) { + scr_dyn_arg_type_fail("options", "of type object", options); + return NAN; + } + ScrDyn *value = scr_als_option((ScrDyn *)options, "defaultValue", 12); + if (!value) return NAN; + ScrDyn *name = scr_als_option((ScrDyn *)options, "name", 4); + if (name && name->kind != SCR_DYN_UNDEF) { + scr_dyn_release(name); + name = scr_als_option((ScrDyn *)options, "name", 4); + if (name && name->kind == SCR_DYN_SYMBOL) { + static const char message[] = "Cannot convert a Symbol value to a string"; + scr_dyn_release(name); + name = NULL; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + } else if (name) { + ScrStr *text = scr_dyn_string_coerce_js(name); + scr_dyn_release(name); + name = text ? scr_dyn_new_str(text) : NULL; + scr_str_release(text); + } + } else { + scr_dyn_release(name); + name = scr_exc_pending() ? NULL : scr_dyn_retain(scr_dyn_undefined()); + } + if (!name) { + scr_dyn_release(value); + return NAN; + } + double id = scr_als_new(); + size_t index = (size_t)id - 1; + if (index >= scr_als_options_len) { + size_t len = index + 1; + ScrAlsOptions *grown = realloc(scr_als_options, len * sizeof *grown); + if (!grown) scr_ad_oom(); + for (size_t i = scr_als_options_len; i < len; i++) grown[i] = (ScrAlsOptions){NULL, NULL}; + scr_als_options = grown; + scr_als_options_len = len; + } + scr_als_options[index].default_value = value; /* the read's +1 moves */ + scr_als_options[index].name = name->kind != SCR_DYN_UNDEF ? name : NULL; + if (name->kind == SCR_DYN_UNDEF) scr_dyn_release(name); + return id; +} + +/* asyncLocalStorage.name (+1): the constructor's name option, or "". */ +ScrDyn *scr_als_name(double id) { + const ScrAlsOptions *options = scr_als_options_of(id); + if (options && options->name) return scr_dyn_retain(options->name); + ScrStr *empty = scr_str_new("", 0); + ScrDyn *result = scr_dyn_new_str(empty); + scr_str_release(empty); + return result; +} + ScrDyn *scr_als_get(double id) { const ScrAlsCtx *c = *SCR_ALS_SLOT(); if (c) { @@ -55,6 +161,8 @@ ScrDyn *scr_als_get(double id) { if (c->entries[i].id == id) return scr_dyn_retain(c->entries[i].value); } } + const ScrAlsOptions *options = scr_als_options_of(id); + if (options && options->default_value) return scr_dyn_retain(options->default_value); return scr_dyn_retain(scr_dyn_undefined()); } @@ -104,8 +212,80 @@ ScrDyn *scr_als_run(double id, ScrDyn *value, ScrDyn *fn, ScrDyn *args) { return scr_als_call_in(scr_als_enter(id, value), fn, args); } +/* exit() runs fn with the store undefined: a store with a default value + * holds an undefined entry for the call (getStore() answers undefined, + * not the default, as in Node); other stores drop their entry. */ ScrDyn *scr_als_exit_run(double id, ScrDyn *fn, ScrDyn *args) { - return scr_als_call_in(scr_als_enter_absent(id), fn, args); + const ScrAlsOptions *options = scr_als_options_of(id); + ScrAlsCtx *prev = options && options->default_value + ? scr_als_enter(id, scr_dyn_undefined()) + : scr_als_enter_absent(id); + return scr_als_call_in(prev, fn, args); +} + +/* AsyncLocalStorage.bind(fn) and the dyn completion callbacks of + * callback-style APIs: a function that calls fn in the context active + * when it was bound, with its own arguments and receiver. caps[0] holds + * fn, caps[1] the context, as scr_als_bind_closure lays them out. */ +static void *scr_als_ctx_retain_v(void *c) { return scr_als_ctx_retain(c); } +static void scr_als_ctx_release_v(void *c) { scr_als_ctx_release(c); } + +static ScrDyn *scr_als_bound_dyn_call(ScrClosure *self, ScrDyn *const *args, size_t argc) { + ScrDyn *fn = scr_box_get_ref(self->caps[0]); + ScrAlsCtx *prev = scr_als_bound_enter(self); + ScrDyn *result = scr_dyn_call(fn, args, argc, "callback"); + scr_als_restore(prev); + scr_dyn_release(fn); + return result; +} + +static ScrClosure *scr_als_context_closure(void *fn, size_t ncaps) { + ScrClosure *closure = scr_closure_new(fn, ncaps); + closure->caps[ncaps - 1] = scr_box_new_obj(scr_als_ctx_retain_v, scr_als_ctx_release_v, NULL); + scr_box_set_ref(closure->caps[ncaps - 1], scr_als_ctx_retain(*SCR_ALS_SLOT())); + return closure; +} + +/* fn borrowed; +1. With `always` false an empty context answers fn + * itself (the completion-callback form). AsyncLocalStorage.bind throws + * Node's ERR_INVALID_ARG_TYPE for a non-function. */ +ScrDyn *scr_als_bind_dyn(ScrDyn *fn, bool always) { + if (fn->kind != SCR_DYN_FUNC) { + if (always) { + scr_dyn_arg_type_fail("fn", "of type function", fn); + return NULL; + } + return scr_dyn_retain(fn); + } + if (!always && *SCR_ALS_SLOT() == NULL) return scr_dyn_retain(fn); + ScrClosure *closure = scr_als_context_closure(NULL, 2); + closure->caps[0] = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v); + scr_box_set_ref(closure->caps[0], scr_dyn_retain(fn)); + return scr_dyn_new_func(closure, scr_als_bound_dyn_call, fn->v.fn.arity, "(...args)", "bound"); +} + +/* AsyncLocalStorage.snapshot(): a function (cb, ...args) that calls cb + * with args in the context active at the snapshot. */ +static ScrDyn *scr_als_snapshot_call(ScrClosure *self, ScrDyn *const *args, size_t argc) { + const ScrDyn *cb = argc > 0 ? args[0] : scr_dyn_undefined(); + if (cb->kind != SCR_DYN_FUNC) { + static const char message[] = "cb is not a function"; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + return NULL; + } + ScrAlsCtx **slot = SCR_ALS_SLOT(); + ScrAlsCtx *prev = *slot; + *slot = scr_box_get_ref(self->caps[0]); /* the slot owns this reference */ + /* Node applies cb to an undefined receiver, whatever the runner's own. */ + scr_dyn_this_push_dyn(scr_dyn_undefined()); + ScrDyn *result = scr_dyn_call(cb, args + 1, argc - 1, "callback"); + scr_dyn_this_pop(); + scr_als_restore(prev); + return result; +} + +ScrDyn *scr_als_snapshot(void) { + return scr_dyn_new_func(scr_als_context_closure(NULL, 1), scr_als_snapshot_call, 1, "(cb, ...args)", "bound"); } /* The two rejection-event registries share one shape: listeners with a @@ -948,6 +1128,22 @@ void scr_punycode_warn(bool immediate, bool report) { scr_str_release(message); } +void scr_buffer_deprecation(void) { + /* Node's showFlaggedDeprecation: after 10000 calls the check stops. */ + if (scr_buffer_ctor_warned || ++scr_buffer_ctor_checks > 10000) return; + static const char text[] = + "Buffer() is deprecated due to security and usability issues. Please use the " + "Buffer.alloc(), Buffer.allocUnsafe(), or Buffer.from() methods instead."; + ScrStr *message = scr_str_new(text, sizeof text - 1); + ScrStr *code = scr_str_new("DEP0005", 7); + ScrDyn *w = scr_warning_new(message, "DeprecationWarning", code); + scr_warning_emit_deferred(w); + scr_dyn_release(w); + scr_str_release(code); + scr_str_release(message); + scr_buffer_ctor_warned = true; +} + /* The console members' warnings. Node's emitWarning emits on * process.nextTick, so these queue in order and each rides one raw tick * on the shared tick queue; any left at exit are dropped with it. */ diff --git a/packages/runtime/src/scr_bigint.c b/packages/runtime/src/scr_bigint.c index dd0a70ff7..69b7009fc 100644 --- a/packages/runtime/src/scr_bigint.c +++ b/packages/runtime/src/scr_bigint.c @@ -215,30 +215,89 @@ static int bi_cmp_limbs(const bi_limb *a, size_t an, const bi_limb *b, size_t bn return 0; } -/* r = a + b over n limbs; returns the carry. r may alias a or b. */ +/* x + y + carry_in (0 or 1), with the carry out in *carry: the add-with- + * carry builtins compile to one adc chain. */ +#if defined(__has_builtin) +#if __has_builtin(__builtin_addcll) && __has_builtin(__builtin_subcll) && \ + __has_builtin(__builtin_addc) && __has_builtin(__builtin_subc) +#define BI_HAVE_ADDC 1 +#endif +#endif +static inline bi_limb bi_addc(bi_limb x, bi_limb y, bi_limb *carry) { +#if defined(BI_HAVE_ADDC) && BI_LIMB_BITS == 64 + unsigned long long c; + bi_limb s = __builtin_addcll(x, y, *carry, &c); + *carry = c; + return s; +#elif defined(BI_HAVE_ADDC) + unsigned c; + bi_limb s = __builtin_addc(x, y, *carry, &c); + *carry = c; + return s; +#else + bi_limb s = x + y; + bi_limb c1 = s < x; + bi_limb t = s + *carry; + *carry = c1 | (t < s); + return t; +#endif +} + +/* x - y - borrow_in (0 or 1), with the borrow out in *borrow. */ +static inline bi_limb bi_subc(bi_limb x, bi_limb y, bi_limb *borrow) { +#if defined(BI_HAVE_ADDC) && BI_LIMB_BITS == 64 + unsigned long long b; + bi_limb d = __builtin_subcll(x, y, *borrow, &b); + *borrow = b; + return d; +#elif defined(BI_HAVE_ADDC) + unsigned b; + bi_limb d = __builtin_subc(x, y, *borrow, &b); + *borrow = b; + return d; +#else + bi_limb d = x - y; + bi_limb b1 = x < y; + bi_limb t = d - *borrow; + *borrow = b1 | (d < *borrow); + return t; +#endif +} + +/* r = a + b over n limbs; returns the carry. r may alias a or b. Four + * limbs a pass keep the carry in one chain. */ static bi_limb bi_add_n(bi_limb *r, const bi_limb *a, const bi_limb *b, size_t n) { bi_limb carry = 0; - for (size_t i = 0; i < n; i++) { - bi_limb x = a[i], s = x + b[i]; - bi_limb c1 = s < x; - bi_limb t = s + carry; - carry = c1 | (t < s); - r[i] = t; + size_t i = 0; + for (; i + 8 <= n; i += 8) { + r[i] = bi_addc(a[i], b[i], &carry); + r[i + 1] = bi_addc(a[i + 1], b[i + 1], &carry); + r[i + 2] = bi_addc(a[i + 2], b[i + 2], &carry); + r[i + 3] = bi_addc(a[i + 3], b[i + 3], &carry); + r[i + 4] = bi_addc(a[i + 4], b[i + 4], &carry); + r[i + 5] = bi_addc(a[i + 5], b[i + 5], &carry); + r[i + 6] = bi_addc(a[i + 6], b[i + 6], &carry); + r[i + 7] = bi_addc(a[i + 7], b[i + 7], &carry); } + for (; i < n; i++) r[i] = bi_addc(a[i], b[i], &carry); return carry; } /* r = a - b over n limbs; returns the borrow. r may alias a or b. */ static bi_limb bi_sub_n(bi_limb *r, const bi_limb *a, const bi_limb *b, size_t n) { bi_limb borrow = 0; - for (size_t i = 0; i < n; i++) { - bi_limb x = a[i], y = b[i]; - bi_limb d = x - y; - bi_limb b1 = x < y; - bi_limb t = d - borrow; - borrow = b1 | (d < borrow); - r[i] = t; + size_t i = 0; + for (; i + 8 <= n; i += 8) { + r[i] = bi_subc(a[i], b[i], &borrow); + r[i + 1] = bi_subc(a[i + 1], b[i + 1], &borrow); + r[i + 2] = bi_subc(a[i + 2], b[i + 2], &borrow); + r[i + 3] = bi_subc(a[i + 3], b[i + 3], &borrow); + r[i + 4] = bi_subc(a[i + 4], b[i + 4], &borrow); + r[i + 5] = bi_subc(a[i + 5], b[i + 5], &borrow); + r[i + 6] = bi_subc(a[i + 6], b[i + 6], &borrow); + r[i + 7] = bi_subc(a[i + 7], b[i + 7], &borrow); } + for (; i < n; i++) r[i] = bi_subc(a[i], b[i], &borrow); return borrow; } @@ -277,9 +336,22 @@ static bi_limb bi_sub_into(bi_limb *r, size_t rn, const bi_limb *a, size_t an) { return borrow; } -/* r = a * m + carry over n limbs; returns the high limb. r may alias a. */ +/* r = a * m + carry over n limbs; returns the high limb. r may alias a. + * Four limbs a pass: the four products, then one carry chain adding each + * product's high limb into the next low limb. */ static bi_limb bi_mul1(bi_limb *r, const bi_limb *a, size_t n, bi_limb m, bi_limb carry) { - for (size_t i = 0; i < n; i++) { + size_t i = 0; + for (; i + 4 <= n; i += 4) { + bi_wide p0 = (bi_wide)a[i] * m, p1 = (bi_wide)a[i + 1] * m; + bi_wide p2 = (bi_wide)a[i + 2] * m, p3 = (bi_wide)a[i + 3] * m; + bi_limb c = 0; + r[i] = bi_addc((bi_limb)p0, carry, &c); + r[i + 1] = bi_addc((bi_limb)p1, (bi_limb)(p0 >> BI_LIMB_BITS), &c); + r[i + 2] = bi_addc((bi_limb)p2, (bi_limb)(p1 >> BI_LIMB_BITS), &c); + r[i + 3] = bi_addc((bi_limb)p3, (bi_limb)(p2 >> BI_LIMB_BITS), &c); + carry = (bi_limb)(p3 >> BI_LIMB_BITS) + c; + } + for (; i < n; i++) { bi_wide p = (bi_wide)a[i] * m + carry; r[i] = (bi_limb)p; carry = (bi_limb)(p >> BI_LIMB_BITS); @@ -287,10 +359,27 @@ static bi_limb bi_mul1(bi_limb *r, const bi_limb *a, size_t n, bi_limb m, bi_lim return carry; } -/* r += a * m over n limbs; returns the high limb. */ +/* r += a * m over n limbs; returns the high limb. Four limbs a pass: one + * carry chain forms the four product limbs, a second adds them to r. */ static bi_limb bi_addmul1(bi_limb *r, const bi_limb *a, size_t n, bi_limb m) { bi_limb carry = 0; - for (size_t i = 0; i < n; i++) { + size_t i = 0; + for (; i + 4 <= n; i += 4) { + bi_wide p0 = (bi_wide)a[i] * m, p1 = (bi_wide)a[i + 1] * m; + bi_wide p2 = (bi_wide)a[i + 2] * m, p3 = (bi_wide)a[i + 3] * m; + bi_limb c = 0, k = 0; + bi_limb t0 = bi_addc((bi_limb)p0, carry, &c); + bi_limb t1 = bi_addc((bi_limb)p1, (bi_limb)(p0 >> BI_LIMB_BITS), &c); + bi_limb t2 = bi_addc((bi_limb)p2, (bi_limb)(p1 >> BI_LIMB_BITS), &c); + bi_limb t3 = bi_addc((bi_limb)p3, (bi_limb)(p2 >> BI_LIMB_BITS), &c); + r[i] = bi_addc(r[i], t0, &k); + r[i + 1] = bi_addc(r[i + 1], t1, &k); + r[i + 2] = bi_addc(r[i + 2], t2, &k); + r[i + 3] = bi_addc(r[i + 3], t3, &k); + /* r + a * m over these limbs stays below B^5, so this cannot wrap */ + carry = (bi_limb)(p3 >> BI_LIMB_BITS) + c + k; + } + for (; i < n; i++) { bi_wide p = (bi_wide)a[i] * m + r[i] + carry; r[i] = (bi_limb)p; carry = (bi_limb)(p >> BI_LIMB_BITS); @@ -301,7 +390,23 @@ static bi_limb bi_addmul1(bi_limb *r, const bi_limb *a, size_t n, bi_limb m) { /* r -= a * m over n limbs; returns the borrow limb. */ static bi_limb bi_submul1(bi_limb *r, const bi_limb *a, size_t n, bi_limb m) { bi_limb carry = 0; - for (size_t i = 0; i < n; i++) { + size_t i = 0; + for (; i + 4 <= n; i += 4) { + bi_wide p0 = (bi_wide)a[i] * m, p1 = (bi_wide)a[i + 1] * m; + bi_wide p2 = (bi_wide)a[i + 2] * m, p3 = (bi_wide)a[i + 3] * m; + bi_limb c = 0, k = 0; + bi_limb t0 = bi_addc((bi_limb)p0, carry, &c); + bi_limb t1 = bi_addc((bi_limb)p1, (bi_limb)(p0 >> BI_LIMB_BITS), &c); + bi_limb t2 = bi_addc((bi_limb)p2, (bi_limb)(p1 >> BI_LIMB_BITS), &c); + bi_limb t3 = bi_addc((bi_limb)p3, (bi_limb)(p2 >> BI_LIMB_BITS), &c); + r[i] = bi_subc(r[i], t0, &k); + r[i + 1] = bi_subc(r[i + 1], t1, &k); + r[i + 2] = bi_subc(r[i + 2], t2, &k); + r[i + 3] = bi_subc(r[i + 3], t3, &k); + /* a * m + carry over these limbs stays below B^5: the borrow fits */ + carry = (bi_limb)(p3 >> BI_LIMB_BITS) + c + k; + } + for (; i < n; i++) { bi_wide p = (bi_wide)a[i] * m + carry; bi_limb lo = (bi_limb)p; carry = (bi_limb)(p >> BI_LIMB_BITS); @@ -312,6 +417,23 @@ static bi_limb bi_submul1(bi_limb *r, const bi_limb *a, size_t n, bi_limb m) { return carry; } +/* y[0..n) = the low n limbs of x << shift, for 0 < shift < BI_LIMB_BITS + * and y disjoint from x. Each limb is formed from two source limbs with no + * dependency between limbs, so the loop vectorizes. */ +static void bi_lshift(bi_limb *restrict y, const bi_limb *restrict x, size_t n, unsigned shift) { + unsigned back = BI_LIMB_BITS - shift; + y[0] = x[0] << shift; + for (size_t i = 1; i < n; i++) y[i] = (x[i] << shift) | (x[i - 1] >> back); +} + +/* y[0..n) = x[0..n) >> shift, for 0 < shift < BI_LIMB_BITS and y disjoint + * from x; like bi_lshift, each limb reads two source limbs. */ +static void bi_rshift(bi_limb *restrict y, const bi_limb *restrict x, size_t n, unsigned shift) { + unsigned back = BI_LIMB_BITS - shift; + for (size_t i = 0; i + 1 < n; i++) y[i] = (x[i] >> shift) | (x[i + 1] << back); + y[n - 1] = x[n - 1] >> shift; +} + /* Schoolbook product: r[0..an+bn) = a * b, r disjoint from a and b. */ static void bi_mul_school(bi_limb *r, const bi_limb *a, size_t an, const bi_limb *b, size_t bn) { r[an] = bi_mul1(r, a, an, b[0], 0); @@ -492,11 +614,9 @@ static void bi_divmod_knuth(bi_limb *q, bi_limb *r, const bi_limb *a, size_t an, bi_limb *un = bi_limbs(an + 1 + bn); bi_limb *vn = un + an + 1; if (s) { - for (size_t i = bn - 1; i > 0; i--) vn[i] = (b[i] << s) | (b[i - 1] >> (BI_LIMB_BITS - s)); - vn[0] = b[0] << s; + bi_lshift(vn, b, bn, s); + bi_lshift(un, a, an, s); un[an] = a[an - 1] >> (BI_LIMB_BITS - s); - for (size_t i = an - 1; i > 0; i--) un[i] = (a[i] << s) | (a[i - 1] >> (BI_LIMB_BITS - s)); - un[0] = a[0] << s; } else { memcpy(vn, b, bn * sizeof(bi_limb)); memcpy(un, a, an * sizeof(bi_limb)); @@ -534,8 +654,7 @@ static void bi_divmod_knuth(bi_limb *q, bi_limb *r, const bi_limb *a, size_t an, } if (r) { if (s) { - for (size_t i = 0; i + 1 < bn; i++) r[i] = (un[i] >> s) | (un[i + 1] << (BI_LIMB_BITS - s)); - r[bn - 1] = un[bn - 1] >> s; + bi_rshift(r, un, bn, s); } else { memcpy(r, un, bn * sizeof(bi_limb)); } @@ -543,6 +662,65 @@ static void bi_divmod_knuth(bi_limb *q, bi_limb *r, const bi_limb *a, size_t an, free(un); } +/* a / b for a and b of n >= 2 limbs each, b's top limb nonzero: the + * quotient is one limb. Knuth's estimate comes from the top limbs as + * they read normalized (three of a, two of b) and is the quotient or one + * more, which one pass of a - qhat * b over the operands as they are + * settles, with no normalized copies. r, unless NULL, receives the n + * limbs of a % b. */ +static bi_limb bi_div_qlimb(bi_limb *r, const bi_limb *a, const bi_limb *b, size_t n) { + unsigned s = bi_clz(b[n - 1]); + bi_limb d1 = b[n - 1], d0 = b[n - 2], u2 = 0, u1 = a[n - 1], u0 = a[n - 2]; + if (s) { + unsigned t = BI_LIMB_BITS - s; + bi_limb b3 = n >= 3 ? b[n - 3] : 0, a3 = n >= 3 ? a[n - 3] : 0; + d1 = (d1 << s) | (d0 >> t); + d0 = (d0 << s) | (b3 >> t); + u2 = u1 >> t; + u1 = (u1 << s) | (u0 >> t); + u0 = (u0 << s) | (a3 >> t); + } + bi_limb qhat, rhat; + bool rhat_big; /* rhat >= B: the estimate needs no further test */ + if (u2 >= d1) { + qhat = BI_LIMB_MAX; + rhat = u1 + d1; + rhat_big = rhat < d1; + } else { + bi_divisor top = bi_divisor_of(d1); + qhat = bi_div21(u2, u1, top.d, top.v, &rhat); + rhat_big = false; + } + while (!rhat_big && (bi_wide)qhat * d0 > (((bi_wide)rhat << BI_LIMB_BITS) | u0)) { + qhat--; + rhat += d1; + rhat_big = rhat < d1; + } + /* Without a remainder, the top limbs decide when they leave room: + * a - qhat * b >= (rhat * B + u0 - qhat * d0) * B^(n-2) - qhat * B^(n-2) + * as normalized, so a margin of at least qhat proves qhat. */ + if (!r && !rhat_big) { + bi_wide top = ((bi_wide)rhat << BI_LIMB_BITS) | u0, sub = (bi_wide)qhat * d0; + if (top - sub >= qhat) return qhat; + } else if (!r) { + return qhat; /* rhat >= B: the remainder exceeds B^(n-1) >= qhat * b's low part */ + } + /* a - qhat * b: negative when the product's high limb or the borrow + * reaches past a's n limbs */ + bi_limb carry = 0, borrow = 0; + for (size_t i = 0; i < n; i++) { + bi_wide p = (bi_wide)b[i] * qhat + carry; + carry = (bi_limb)(p >> BI_LIMB_BITS); + bi_limb d = bi_subc(a[i], (bi_limb)p, &borrow); + if (r) r[i] = d; + } + if (carry || borrow) { + qhat--; + if (r) bi_add_n(r, r, b, n); + } + return qhat; +} + /* ---- comparisons and hashing ----------------------------------------- */ static int bi_cmp_abs(const ScrBigInt *a, const ScrBigInt *b) { @@ -709,6 +887,13 @@ static void bi_divmod_abs(const ScrBigInt *a, const ScrBigInt *b, ScrBigInt **qo if (rout) *rout = bi_u64_signed(rem, 1); return; } + if (an == bn) { + ScrBigInt *r = rout ? bi_alloc(bn) : NULL; + bi_limb q = bi_div_qlimb(r ? r->limb : NULL, a->limb, b->limb, bn); + if (qout) *qout = bi_u64_signed(q, 1); + if (rout) *rout = bi_finish(r, bn, 1); + return; + } size_t qn = an - bn + 1; ScrBigInt *q = bi_alloc(qn); ScrBigInt *r = rout ? bi_alloc(bn) : NULL; @@ -772,13 +957,8 @@ static ScrBigInt *bi_shl_abs(const ScrBigInt *a, size_t count, int sign) { if (!shift) { memcpy(r->limb + words, a->limb, a->len * sizeof(bi_limb)); } else { - bi_limb carry = 0; - for (size_t i = 0; i < a->len; i++) { - bi_limb v = a->limb[i]; - r->limb[i + words] = (v << shift) | carry; - carry = v >> (BI_LIMB_BITS - shift); - } - if (grow) r->limb[n] = carry; + bi_lshift(r->limb + words, a->limb, a->len, shift); + if (grow) r->limb[n] = a->limb[a->len - 1] >> (BI_LIMB_BITS - shift); } r->len = n + grow; r->sign = sign; @@ -801,9 +981,7 @@ static ScrBigInt *bi_shr_abs(const ScrBigInt *a, size_t count, int sign, bool *d if (!shift) { memcpy(r->limb, a->limb + words, n * sizeof(bi_limb)); } else { - for (size_t i = 0; i + 1 < n; i++) - r->limb[i] = (a->limb[i + words] >> shift) | (a->limb[i + words + 1] << (BI_LIMB_BITS - shift)); - r->limb[n - 1] = a->limb[a->len - 1] >> shift; + bi_rshift(r->limb, a->limb + words, n, shift); } return bi_finish(r, n, sign); } diff --git a/packages/runtime/src/scr_blob.h b/packages/runtime/src/scr_blob.h index 15f4a10d4..0882e41b6 100644 --- a/packages/runtime/src/scr_blob.h +++ b/packages/runtime/src/scr_blob.h @@ -1,5 +1,6 @@ -/* scriptc runtime — Blob and File (scr_blob.c). Included by scr_runtime.h, - * which says where new declarations go. */ +/* scriptc runtime — Blob and File (scr_blob.c), and module namespace + * objects as checked-dynamic values (scr_module_namespace.c). Included by + * scr_runtime.h, which says where new declarations go. */ #ifndef SCR_BLOB_H #define SCR_BLOB_H @@ -23,9 +24,16 @@ ScrPromise *scr_blob_array_buffer(ScrDyn *self); bool scr_blob_is(const ScrDyn *value, double kind); ScrDyn *scr_intl_segmenter_new(void); +/* A word or sentence Intl.Segmenter whose segment() throws `refusal` + * (borrowed), the compiler's SC2020 message. */ +ScrDyn *scr_intl_segmenter_refusing(ScrStr *refusal); ScrStr *scr_process_builtin_id(ScrDyn *id, ScrArr *known); ScrDyn *scr_process_builtin_module(ScrStr *id, ScrDyn *getter); ScrDyn *scr_process_builtin_unsupported(ScrStr *id, ScrStr *member); +/* A module namespace object as a checked-dynamic value (scr_module_namespace.c): + * one handle per module `id`, whose reads call `getter` with the export + * name when `keys` lists it. Borrows all three; +1 handle. */ +ScrDyn *scr_module_namespace_value(ScrStr *id, ScrArr *keys, ScrDyn *getter); ScrDyn *scr_global_native(ScrArr *known); ScrDyn *scr_global_native_init(ScrArr *known, ScrDyn *(*console_get)(void)); ScrDyn *scr_console_native(void); diff --git a/packages/runtime/src/scr_brotli.c b/packages/runtime/src/scr_brotli.c new file mode 100644 index 000000000..f66ecae75 --- /dev/null +++ b/packages/runtime/src/scr_brotli.c @@ -0,0 +1,323 @@ +/* node:zlib's Brotli codec over the vendored brotli 1.2.0 (the version Node + * v24.22.0 bundles, so compressed bytes match Node's). Node's + * BrotliEncoderContext and BrotliDecoderContext (src/node_zlib.cc) plug into + * scr_zlib.c's engine as a codec, so the one-shot, callback, promise and + * stream forms run exactly as their zlib counterparts do. The option ladder + * is lib/zlib.js's Brotli() followed by ZlibBase(). Linked only when the + * program uses Brotli; scr_brotli_install, which %main calls first, + * connects it to scr_zlib.c. */ +#include "scr_runtime.h" + +#include +#include +#include +#include +#include +#include + +enum { SCR_BROTLI_DECODE = 8, SCR_BROTLI_ENCODE = 9 }; +/* kMaxBrotliParam + 1: the largest BROTLI_PARAM_* value is + * BROTLI_PARAM_NDIRECT (8). An unset slot holds 0xFFFFFFFF, as Node's + * Uint32Array filled with -1 does. */ +#define SCR_BROTLI_PARAMS 9 +#define SCR_BROTLI_UNSET 0xFFFFFFFFu + +typedef struct { + bool encode; + BrotliEncoderState *enc; + BrotliEncoderPreparedDictionary *prepared; + BrotliDecoderState *dec; + /* The dictionary stays alive while the encoder's prepared dictionary or + * the decoder holds it. */ + unsigned char *dict; + size_t dict_len; + int flush; + bool enc_ok; + BrotliDecoderResult dec_result; + BrotliDecoderErrorCode dec_error; + char dec_code[64]; +} ScrBrotli; + +static void scr_brotli_oom(void) { + scr_trap("scriptc: out of memory\n"); +} + +static bool scr_brotli_fail(ScrZlibError *out, const char *message, const char *code, int err) { + out->message = message; + out->code = code; + out->err = err; + return true; +} + +static void scr_brotli_close(ScrBrotli *b) { + if (b->enc) BrotliEncoderDestroyInstance(b->enc); + if (b->prepared) BrotliEncoderDestroyPreparedDictionary(b->prepared); + if (b->dec) BrotliDecoderDestroyInstance(b->dec); + b->enc = NULL; + b->prepared = NULL; + b->dec = NULL; +} + +/* Init(dictionary): a fresh instance with the dictionary attached. As in + * Node, the decoder's last error survives a re-initialization. True with + * *out filled on failure. */ +static bool scr_brotli_init(ScrBrotli *b, ScrZlibError *out) { + scr_brotli_close(b); + if (b->encode) { + b->enc = BrotliEncoderCreateInstance(NULL, NULL, NULL); + b->enc_ok = false; + if (!b->enc) return scr_brotli_fail(out, "Could not initialize Brotli instance", "ERR_ZLIB_INITIALIZATION_FAILED", -1); + if (b->dict_len == 0) return false; + b->prepared = BrotliEncoderPrepareDictionary(BROTLI_SHARED_DICTIONARY_RAW, b->dict_len, b->dict, BROTLI_MAX_QUALITY, + NULL, NULL, NULL); + if (!b->prepared) return scr_brotli_fail(out, "Failed to prepare brotli dictionary", "ERR_ZLIB_DICTIONARY_LOAD_FAILED", -1); + if (!BrotliEncoderAttachPreparedDictionary(b->enc, b->prepared)) + return scr_brotli_fail(out, "Failed to attach brotli dictionary", "ERR_ZLIB_DICTIONARY_LOAD_FAILED", -1); + return false; + } + b->dec = BrotliDecoderCreateInstance(NULL, NULL, NULL); + if (!b->dec) return scr_brotli_fail(out, "Could not initialize Brotli instance", "ERR_ZLIB_INITIALIZATION_FAILED", -1); + if (b->dict_len != 0 && !BrotliDecoderAttachDictionary(b->dec, BROTLI_SHARED_DICTIONARY_RAW, b->dict_len, b->dict)) + return scr_brotli_fail(out, "Failed to attach brotli dictionary", "ERR_ZLIB_DICTIONARY_LOAD_FAILED", -1); + return false; +} + +static void scr_brotli_free(void *state) { + ScrBrotli *b = state; + if (!b) return; + scr_brotli_close(b); + free(b->dict); + free(b); +} + +/* DoThreadPoolWork: runs on a worker thread and touches only the state. */ +static void scr_brotli_work(void *state, int flush, const unsigned char *in, uint32_t in_len, unsigned char *out, + uint32_t out_len, uint32_t *avail_in, uint32_t *avail_out) { + ScrBrotli *b = state; + size_t in_left = in_len, out_left = out_len; + const uint8_t *next_in = in; + uint8_t *next_out = out; + b->flush = flush; + if (b->encode) { + b->enc_ok = BrotliEncoderCompressStream(b->enc, (BrotliEncoderOperation)flush, &in_left, &next_in, &out_left, + &next_out, NULL) != BROTLI_FALSE; + } else { + b->dec_result = BrotliDecoderDecompressStream(b->dec, &in_left, &next_in, &out_left, &next_out, NULL); + if (b->dec_result == BROTLI_DECODER_RESULT_ERROR) { + b->dec_error = BrotliDecoderGetErrorCode(b->dec); + snprintf(b->dec_code, sizeof b->dec_code, "ERR_%s", BrotliDecoderErrorString(b->dec_error)); + } + } + *avail_in = (uint32_t)in_left; + *avail_out = (uint32_t)out_left; +} + +/* GetErrorInfo. */ +static bool scr_brotli_error(const void *state, ScrZlibError *out) { + const ScrBrotli *b = state; + if (b->encode) { + if (!b->enc_ok) return scr_brotli_fail(out, "Compression failed", "ERR_BROTLI_COMPRESSION_FAILED", -1); + return false; + } + if (b->dec_error != BROTLI_DECODER_NO_ERROR) + return scr_brotli_fail(out, "Decompression failed", b->dec_code, (int)b->dec_error); + /* zlib's answer for a truncated stream; Brotli has no code of its own. */ + if (b->flush == BROTLI_OPERATION_FINISH && b->dec_result == BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT) + return scr_brotli_fail(out, "unexpected end of file", "Z_BUF_ERROR", -5); + return false; +} + +/* ResetStream is Init() without a dictionary: as in Node, the reset + * instance drops the dictionary and the parameters. */ +static bool scr_brotli_reset(void *state, ScrZlibError *out) { + ScrBrotli *b = state; + free(b->dict); + b->dict = NULL; + b->dict_len = 0; + return scr_brotli_init(b, out); +} + +static const ScrZlibCodec scr_brotli_codec = { + scr_brotli_work, + scr_brotli_error, + scr_brotli_reset, + scr_brotli_free, +}; + +/* ── lib/zlib.js Brotli() ──────────────────────────────────────────── */ + +static const ScrDyn *scr_brotli_opt(const ScrDyn *opts, const char *key) { + if (opts == NULL || opts->kind != SCR_DYN_OBJ) return NULL; + return scr_dyn_obj_get((ScrDyn *)opts, key, strlen(key)); +} + +/* Uint32Array element assignment: ToUint32 of a number or boolean. */ +static uint32_t scr_brotli_uint32(const ScrDyn *v) { + if (v->kind == SCR_DYN_BOOL) return v->v.b ? 1 : 0; + double d = v->v.num; + if (!isfinite(d)) return 0; + double m = fmod(trunc(d), 4294967296.0); + if (m < 0) m += 4294967296.0; + return (uint32_t)m; +} + +/* ERR_BROTLI_INVALID_PARAM: the whole key, then Node's text. */ +static void scr_brotli_invalid_param(const ScrStr *key) { + static const char suffix[] = " is not a valid Brotli parameter"; + size_t len = key->len + sizeof suffix - 1; + char *message = malloc(len); + if (!message) scr_brotli_oom(); + memcpy(message, key->data, key->len); + memcpy(message + key->len, suffix, sizeof suffix - 1); + scr_throw_error_msg_code(SCR_ERR_RANGE, message, len, "ERR_BROTLI_INVALID_PARAM"); + free(message); +} + +/* opts.params: each own key must name a parameter (0..8) once, and each + * value must be a number or boolean. */ +static bool scr_brotli_params(const ScrDyn *params, uint32_t values[SCR_BROTLI_PARAMS]) { + for (size_t i = 0; i < SCR_BROTLI_PARAMS; i++) values[i] = SCR_BROTLI_UNSET; + if (params == NULL || !scr_dyn_truthy(params)) return true; + ScrDyn *view = params->kind == SCR_DYN_TYPED_REF ? scr_dyn_typed_ref_materialize(params) : NULL; + if (scr_exc_pending()) return false; + const ScrDyn *p = view ? view : params; + if (p->kind == SCR_DYN_STR) { + /* Object.keys of a string names its indices; the first value is a + * one-character string, which the type check rejects. */ + ScrStr *first = scr_str_new(p->v.str->data, 1); + ScrDyn *value = scr_dyn_new_str(first); + scr_str_release(first); + scr_dyn_prop_type_fail("options.params[key]", "of type number", value); + scr_dyn_release(value); + scr_dyn_release(view); + return false; + } + if (p->kind != SCR_DYN_OBJ && p->kind != SCR_DYN_ARR) { + scr_dyn_release(view); + return true; + } + ScrDyn *keys = scr_dyn_own_keys(p); + bool ok = keys != NULL; + size_t count = ok && keys->kind == SCR_DYN_ARR ? keys->v.arr.len : 0; + for (size_t i = 0; ok && i < count; i++) { + const ScrDyn *key = keys->v.arr.items[i]; + if (key->kind != SCR_DYN_STR) continue; + double index = NAN; + if (!scr_dyn_number_coerce_js(key, &index)) { + ok = false; + break; + } + if (isnan(index) || index < 0 || index > SCR_BROTLI_PARAMS - 1 || trunc(index) != index || + values[(int)index] != SCR_BROTLI_UNSET) { + scr_brotli_invalid_param(key->v.str); + ok = false; + break; + } + ScrDyn *value = scr_dyn_obj_read(p, key->v.str->data, key->v.str->len); + if (!value) { + ok = false; + break; + } + if (value->kind != SCR_DYN_NUM && value->kind != SCR_DYN_BOOL) { + scr_dyn_prop_type_fail("options.params[key]", "of type number", value); + scr_dyn_release(value); + ok = false; + break; + } + values[(int)index] = scr_brotli_uint32(value); + scr_dyn_release(value); + } + scr_dyn_release(keys); + scr_dyn_release(view); + return ok; +} + +/* opts.dictionary: an ArrayBufferView, or an ArrayBuffer read as a Buffer. + * A copy (or NULL for none) in *dict. */ +static bool scr_brotli_dictionary(const ScrDyn *d, unsigned char **dict, size_t *dict_len) { + *dict = NULL; + *dict_len = 0; + if (d == NULL || d->kind == SCR_DYN_UNDEF) return true; + ScrBytes *bytes = NULL; + if (d->kind == SCR_DYN_BYTES) { + bytes = scr_bytes_raw_view(d->v.bytes); + } else if (scr_buffer_storage_is(d)) { + bytes = scr_array_buffer_view(SCR_BYTES_U8, (ScrDyn *)d, scr_dyn_undefined(), scr_dyn_undefined()); + if (!bytes) return false; + } else { + scr_dyn_prop_type_fail("options.dictionary", "an instance of Buffer, TypedArray, DataView, or ArrayBuffer", d); + return false; + } + if (bytes->len) { + *dict = malloc(bytes->len); + if (!*dict) scr_brotli_oom(); + memcpy(*dict, bytes->data, bytes->len); + *dict_len = bytes->len; + } + scr_bytes_release(bytes); + return true; +} + +static void scr_brotli_init_failed(void) { + static const char message[] = "Initialization failed"; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "ERR_ZLIB_INITIALIZATION_FAILED"); +} + +static bool scr_brotli_options_object(int mode, const ScrDyn *opts, ScrZlibOptions *out) { + memset(out, 0, sizeof *out); + out->chunk_size = 16384; + out->flush = BROTLI_OPERATION_PROCESS; + out->finish_flush = BROTLI_OPERATION_FINISH; + out->max_output = 9007199254740991.0; + uint32_t params[SCR_BROTLI_PARAMS]; + if (!scr_brotli_params(scr_brotli_opt(opts, "params"), params)) return false; + unsigned char *dict; + size_t dict_len; + if (!scr_brotli_dictionary(scr_brotli_opt(opts, "dictionary"), &dict, &dict_len)) return false; + ScrBrotli *b = calloc(1, sizeof *b); + if (!b) scr_brotli_oom(); + b->encode = mode == SCR_BROTLI_ENCODE; + b->dict = dict; + b->dict_len = dict_len; + ScrZlibError err; + bool failed = scr_brotli_init(b, &err); + for (int i = 0; !failed && i < SCR_BROTLI_PARAMS; i++) { + if (params[i] == SCR_BROTLI_UNSET) continue; + failed = b->encode ? !BrotliEncoderSetParameter(b->enc, (BrotliEncoderParameter)i, params[i]) + : !BrotliDecoderSetParameter(b->dec, (BrotliDecoderParameter)i, params[i]); + } + if (failed) { + scr_brotli_free(b); + scr_brotli_init_failed(); + return false; + } + out->codec_state = b; + out->codec_state_free = scr_brotli_free; + /* ZlibBase's `if (opts)`: a falsy value reads no more options. */ + if (opts == NULL || !scr_dyn_truthy(opts)) return true; + return scr_zlib_base_options(opts, BROTLI_OPERATION_EMIT_METADATA, out); +} + +/* A typed options object is read through its materialized view. */ +static bool scr_brotli_options(int mode, const ScrDyn *opts, ScrZlibOptions *out) { + if (opts == NULL || opts->kind != SCR_DYN_TYPED_REF) return scr_brotli_options_object(mode, opts, out); + ScrDyn *view = scr_dyn_typed_ref_materialize(opts); + bool ok = false; + if (scr_exc_pending()) memset(out, 0, sizeof *out); + else ok = scr_brotli_options_object(mode, view, out); + scr_dyn_release(view); + return ok; +} + +/* Moves the prepared instance out of the options. */ +static ScrZlibEngine *scr_brotli_engine(int mode, ScrZlibOptions *o) { + ScrBrotli *b = o->codec_state; + o->codec_state = NULL; + return scr_zlib_engine_codec(mode, &scr_brotli_codec, b, o->reject_garbage); +} + +static const ScrZlibBrotliHooks scr_brotli_hooks = {scr_brotli_options, scr_brotli_engine}; + +void scr_brotli_install(void) { + scr_zlib_brotli_hooks = &scr_brotli_hooks; +} diff --git a/packages/runtime/src/scr_builtin_values.c b/packages/runtime/src/scr_builtin_values.c index ea240359a..f8dd2b79b 100644 --- a/packages/runtime/src/scr_builtin_values.c +++ b/packages/runtime/src/scr_builtin_values.c @@ -259,3 +259,217 @@ ScrDyn *scr_fs_realpath_value(const ScrDyn *path, const ScrDyn *options, bool na scr_str_release(resolved); return out; } + +/* ── Buffer as a value ─────────────────────────────────────────────────── + * The Buffer function read as a value in JavaScript (bn.js assigns + * `require('buffer').Buffer` to a variable and passes it to + * `allocate(ArrayType, size)`): one function per thread, named Buffer + * with length 3, whose own enumerable properties are Node's, in Node's + * order: poolSize, the statics and Symbol(kIsEncodingSymbol). The statics + * apply Node's argument handling. Calling or constructing Buffer through + * the value, reading its prototype, and the copyBytesFrom and of statics + * throw SC2020. poolSize is a data property the pool reads before each + * decision (scr_buffer_pool_refresh_hook). */ +static SCR_TL ScrDyn *scr_bv_buffer; + +#define SCR_BV_ARG(i) ((i) < argc ? args[i] : scr_dyn_undefined()) + +static ScrDyn *scr_bv_unsupported(const char *what) { + char message[160]; + int n = snprintf(message, sizeof message, "%s through a stored Buffer value is not supported yet", what); + scr_throw_error_msg_code(SCR_ERR_ERROR, message, (size_t)n, "SC2020"); + return NULL; +} + +/* A Buffer result as a value: +1, or NULL with the exception pending. */ +static ScrDyn *scr_bv_bytes(ScrBytes *bytes) { + if (!bytes) return NULL; + ScrBytes *buffer = scr_bytes_as_buffer(bytes); + scr_bytes_release(bytes); + ScrDyn *value = scr_dyn_new_bytes(buffer); + scr_bytes_release(buffer); + return value; +} + +static ScrDyn *scr_bv_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; (void)args; (void)argc; + return scr_bv_unsupported("Calling or constructing Buffer"); +} + +static ScrDyn *scr_bv_from(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; + const ScrDyn *value = SCR_BV_ARG(0), *second = SCR_BV_ARG(1), *third = SCR_BV_ARG(2); + if (value->kind != SCR_DYN_STR) return scr_bv_bytes(scr_buffer_from_dyn_range_pooled(value, second, third)); + /* fromString: an encoding that is not a non-empty string means utf8. */ + if (second->kind == SCR_DYN_STR && second->v.str->len > 0) + return scr_bv_bytes(scr_buffer_from_str_checked(value->v.str, second->v.str)); + ScrStr *utf8 = scr_str_new("utf8", 4); + ScrBytes *bytes = scr_buffer_from_str_checked(value->v.str, utf8); + scr_str_release(utf8); + return scr_bv_bytes(bytes); +} + +static ScrDyn *scr_bv_copy_bytes_from(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; (void)args; (void)argc; + return scr_bv_unsupported("Buffer.copyBytesFrom"); +} + +static ScrDyn *scr_bv_of(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; (void)args; (void)argc; + return scr_bv_unsupported("Buffer.of"); +} + +static ScrDyn *scr_bv_alloc(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; + return scr_bv_bytes(scr_buffer_alloc_value(SCR_BV_ARG(0), SCR_BV_ARG(1), SCR_BV_ARG(2))); +} + +static ScrDyn *scr_bv_alloc_unsafe(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; + const ScrDyn *size = SCR_BV_ARG(0); + if (size->kind != SCR_DYN_NUM) { + scr_dyn_arg_type_fail("size", "of type number", size); + return NULL; + } + return scr_bv_bytes(scr_buffer_alloc_unsafe(size->v.num)); +} + +static ScrDyn *scr_bv_alloc_unsafe_slow(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; + const ScrDyn *size = SCR_BV_ARG(0); + if (size->kind != SCR_DYN_NUM) { + scr_dyn_arg_type_fail("size", "of type number", size); + return NULL; + } + return scr_bv_bytes(scr_buffer_alloc_unsafe_slow(size->v.num)); +} + +static ScrDyn *scr_bv_is_buffer(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; + const ScrDyn *value = SCR_BV_ARG(0); + return scr_dyn_new_bool(value->kind == SCR_DYN_BYTES && value->buffer); +} + +static ScrDyn *scr_bv_compare(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; + double order = scr_buffer_compare_chk(SCR_BV_ARG(0), SCR_BV_ARG(1)); + return scr_exc_pending() ? NULL : scr_dyn_new_num(order); +} + +static ScrDyn *scr_bv_is_encoding(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; + const ScrDyn *encoding = SCR_BV_ARG(0); + return scr_dyn_new_bool(encoding->kind == SCR_DYN_STR && encoding->v.str->len > 0 && + scr_bytes_is_encoding(encoding->v.str)); +} + +/* Buffer.concat(list, length): Node validates the list, answers an empty + * Buffer for an empty list before it reads the length, and otherwise + * validates the length as an offset. */ +static ScrDyn *scr_bv_concat(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; + const ScrDyn *list = SCR_BV_ARG(0), *length = SCR_BV_ARG(1); + bool empty = list->kind == SCR_DYN_ARR && list->v.arr.len == 0; + if (length->kind == SCR_DYN_UNDEF || empty || list->kind != SCR_DYN_ARR) + return scr_bv_bytes(scr_buffer_concat_dyn(list)); + if (length->kind != SCR_DYN_NUM) { + scr_dyn_arg_type_fail("length", "of type number", length); + return NULL; + } + return scr_bv_bytes(scr_buffer_concat_len_dyn(list, length->v.num)); +} + +static ScrDyn *scr_bv_byte_length(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; + double length = scr_buffer_byte_length_value((ScrDyn *)SCR_BV_ARG(0), (ScrDyn *)SCR_BV_ARG(1)); + return scr_exc_pending() ? NULL : scr_dyn_new_num(length); +} + +static ScrDyn *scr_bv_prototype(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; (void)args; (void)argc; + return scr_bv_unsupported("Buffer.prototype"); +} + +/* The pool's setting is the value's poolSize: a number as it is, another + * primitive through ToNumber, anything else NaN (no pooling). */ +static void scr_bv_pool_refresh(void) { + ScrDyn *table = scr_dyn_function_properties(scr_bv_buffer); + const ScrDyn *size = scr_dyn_obj_get(table, "poolSize", 8); + double setting = NAN; + if (size && size->kind == SCR_DYN_NUM) { + setting = size->v.num; + } else if (size && (size->kind == SCR_DYN_STR || size->kind == SCR_DYN_BOOL || size->kind == SCR_DYN_NULL || + size->kind == SCR_DYN_UNDEF)) { + if (!scr_dyn_number_coerce_js(size, &setting)) setting = NAN; + } + scr_dyn_release(table); + scr_buffer_pool_apply(setting); +} + +/* A static `Buffer.poolSize = n` write updates the value's property. */ +static void scr_bv_pool_publish(double size) { + ScrDyn *table = scr_dyn_function_properties(scr_bv_buffer); + scr_dyn_obj_set(table, "poolSize", 8, scr_dyn_new_num(size)); + scr_dyn_release(table); +} + +static void scr_bv_cleanup(void) { + scr_buffer_pool_refresh_hook = NULL; + scr_buffer_pool_publish_hook = NULL; + scr_dyn_release(scr_bv_buffer); + scr_bv_buffer = NULL; +} + +ScrDyn *scr_buffer_constructor_value(void) { + if (scr_bv_buffer) return scr_dyn_retain(scr_bv_buffer); + static const struct { const char *name; uint32_t length; ScrDynThunk thunk; } statics[] = { + {"from", 3, scr_bv_from}, + {"copyBytesFrom", 3, scr_bv_copy_bytes_from}, + {"of", 0, scr_bv_of}, + {"alloc", 3, scr_bv_alloc}, + {"allocUnsafe", 1, scr_bv_alloc_unsafe}, + {"allocUnsafeSlow", 1, scr_bv_alloc_unsafe_slow}, + {"isBuffer", 1, scr_bv_is_buffer}, + {"compare", 2, scr_bv_compare}, + {"isEncoding", 1, scr_bv_is_encoding}, + {"concat", 2, scr_bv_concat}, + {"byteLength", 2, scr_bv_byte_length}, + }; + double pool_size = scr_buffer_pool_size(); + scr_bv_buffer = scr_dyn_new_func(scr_closure_new(NULL, 0), scr_bv_call, 3, "native:Buffer", "Buffer"); + scr_atexit(scr_bv_cleanup); + ScrDyn *table = scr_dyn_function_properties(scr_bv_buffer); + ScrDyn *getter = scr_dyn_new_func(scr_closure_new(NULL, 0), scr_bv_prototype, 0, "native:function", "get prototype"); + scr_dyn_obj_set_accessor(table, "prototype", 9, getter, NULL); + ScrDynEntry *entry = &table->v.obj.entries[table->v.obj.len - 1]; + entry->enumerable = entry->configurable = false; + scr_dyn_obj_set(table, "poolSize", 8, scr_dyn_new_num(pool_size)); + ScrDyn *is_encoding = NULL; + for (size_t i = 0; i < sizeof statics / sizeof statics[0]; i++) { + ScrDyn *fn = scr_dyn_new_func(scr_closure_new(NULL, 0), statics[i].thunk, statics[i].length, "native:function", + statics[i].name); + if (statics[i].thunk == scr_bv_is_encoding) is_encoding = scr_dyn_retain(fn); + scr_dyn_obj_set(table, statics[i].name, strlen(statics[i].name), fn); + } + scr_dyn_release(table); + ScrStr *description = scr_str_new("kIsEncodingSymbol", 17); + ScrSym *symbol = scr_sym_new(description); + scr_str_release(description); + ScrDyn *key = scr_dyn_new_symbol(symbol); + scr_sym_release(symbol); + scr_dyn_symbol_key_set(scr_bv_buffer, key, is_encoding); + scr_dyn_release(key); + scr_dyn_release(is_encoding); + scr_buffer_pool_refresh_hook = scr_bv_pool_refresh; + scr_buffer_pool_publish_hook = scr_bv_pool_publish; + return scr_dyn_retain(scr_bv_buffer); +} + +void scr_buffer_install_value(void) { scr_bytes_buffer_constructor_fn = scr_buffer_constructor_value; } + +ScrDyn *scr_buffer_static_value(const ScrStr *name) { + ScrDyn *buffer = scr_buffer_constructor_value(); + ScrDyn *member = scr_dyn_fn_get(buffer, name->data, name->len); + scr_dyn_release(buffer); + return member; +} diff --git a/packages/runtime/src/scr_bytes.c b/packages/runtime/src/scr_bytes.c index 7bcfd0cfb..6bb56195a 100644 --- a/packages/runtime/src/scr_bytes.c +++ b/packages/runtime/src/scr_bytes.c @@ -190,6 +190,8 @@ ScrBytes *scr_bytes_take_data(uint8_t *data, size_t len) { b->external = false; b->untransferable = false; b->shared = NULL; + b->properties = NULL; + b->properties_traced = false; #ifdef SCR_RC_AUDIT scr_live_bytes++; #endif @@ -304,6 +306,7 @@ void scr_bytes_release(ScrBytes *b) { if (!b || b->rc == SIZE_MAX) return; /* NULL: an uninitialized `let` local */ if (--b->rc == 0) { scr_weak_dispose_inline(b); + if (b->properties) scr_dyn_release(b->properties); if (b->backing) { scr_bytes_release(b->backing); /* a view: data points into the owner */ } else if (b->shared) { @@ -740,6 +743,8 @@ ScrBytes *scr_bytes_subarray(ScrBytes *b, double start, double end) { v->external = false; v->untransferable = false; v->shared = owner->shared; + v->properties = NULL; + v->properties_traced = false; #ifdef SCR_RC_AUDIT scr_live_bytes++; #endif @@ -755,12 +760,29 @@ void scr_bytes_set_from(ScrBytes *dst, const ScrBytes *src, double offset) { } size_t width = scr_bytes_elem_size(dst->elem); if (dst->shared || src->shared) { - /* Convert a private copy of the source, then publish the target range. */ SCR_BYTES_SNAPSHOT(src); size_t bytes = src->len * width; + uint8_t *target = dst->data + (size_t)t * width; + if (dst->elem == src->elem) { + /* The same element kind copies the source's bytes, NaN payloads + * included. A source range that overlaps the target range (views of + * one buffer, where no snapshot was taken) is copied out first. */ + uintptr_t to = (uintptr_t)target, from = (uintptr_t)src->data; + bool overlap = to <= from ? from - to < bytes : to - from < bytes; + if (!overlap) { + scr_bytes_raw_out(dst, target, src->data, bytes); + return; + } + uint8_t *copy = scr_bytes_scratch(bytes); + scr_bytes_raw_in(src, copy, src->data, bytes); + scr_bytes_raw_out(dst, target, copy, bytes); + free(copy); + return; + } + /* Convert a private copy of the source, then publish the target range. */ uint8_t *scratch = scr_bytes_scratch(bytes); scr_bytes_convert_span(scratch, dst->elem, src->data, src->elem, src->len); - scr_bytes_raw_out(dst, dst->data + (size_t)t * width, scratch, bytes); + scr_bytes_raw_out(dst, target, scratch, bytes); free(scratch); return; } @@ -866,7 +888,9 @@ ScrBytes *scr_bytes_from_dyn(ScrBytesElem elem, const ScrDyn *value, bool from) if (value->kind == SCR_DYN_BYTES) return scr_bytes_convert(elem, value->v.bytes); if (!from && scr_buffer_storage_is(value)) return scr_array_buffer_view(elem, value, scr_dyn_undefined(), scr_dyn_undefined()); if (!from && (value->kind == SCR_DYN_NUM || value->kind == SCR_DYN_BOOL || - value->kind == SCR_DYN_STR || value->kind == SCR_DYN_NULL || value->kind == SCR_DYN_UNDEF)) { + value->kind == SCR_DYN_STR || value->kind == SCR_DYN_NULL || value->kind == SCR_DYN_UNDEF || + value->kind == SCR_DYN_SYMBOL || value->kind == SCR_DYN_BIGINT)) { + /* ToIndex: a Symbol or BigInt length throws ToNumber's TypeError. */ double number; if (!scr_dyn_number_coerce_js(value, &number)) return NULL; return scr_bytes_new(elem, number); @@ -874,6 +898,21 @@ ScrBytes *scr_bytes_from_dyn(ScrBytesElem elem, const ScrDyn *value, bool from) // Constructors drain array iterators before element coercion. Node's .from // fast path reads ordinary array elements during conversion instead. Strings // supplied to .from iterate by code point (set reads UTF-16 code units). + // An object with a Symbol.iterator method iterates; others are array-like. + ScrDyn *method = scr_dyn_iterator_method(value); + if (method) { + ScrDyn *items = scr_dyn_iterate_with(value, method); + scr_dyn_release(method); + if (!items) return NULL; + ScrBytes *out = scr_bytes_new(elem, (double)items->v.arr.len); + if (out && !scr_bytes_copy_array_like(out, items, 0, out->len)) { + scr_bytes_release(out); + out = NULL; + } + scr_dyn_release(items); + return out; + } + if (scr_exc_pending()) return NULL; if ((!from && value->kind == SCR_DYN_ARR) || (from && value->kind == SCR_DYN_STR)) { ScrDyn *items = scr_dyn_iter_pack(value, NULL); if (!items) return NULL; @@ -977,6 +1016,8 @@ ScrBytes *scr_dataview_new(ScrBytes *src, double byte_off, bool has_len, double v->external = false; v->untransferable = false; v->shared = owner->shared; + v->properties = NULL; + v->properties_traced = false; #ifdef SCR_RC_AUDIT scr_live_bytes++; #endif @@ -1545,6 +1586,7 @@ static ScrStr *scr_bytes_encode_b64(const uint8_t *in, size_t n, bool url) { if (!url) out[o++] = '='; } out[outlen] = '\0'; + scr_str_note_ascii(s); return s; } @@ -2199,29 +2241,26 @@ ScrStr *scr_text_decode_stream(ScrDyn *state, const ScrBytes *input, return result; } -/* Decode the next code point from an ScrStr's storage — ALWAYS valid - * UTF-8 (the string runtime never stores ill-formed sequences), so no - * validation re-runs here. */ -static uint32_t scr_bytes_next_cp(const uint8_t *in, size_t *ip) { - uint8_t b = in[*ip]; - if (b < 0x80) { - *ip += 1; - return b; - } - if ((b & 0xe0) == 0xc0) { - uint32_t cp = ((uint32_t)(b & 0x1f) << 6) | (in[*ip + 1] & 0x3f); - *ip += 2; - return cp; - } - if ((b & 0xf0) == 0xe0) { - uint32_t cp = ((uint32_t)(b & 0xf) << 12) | ((uint32_t)(in[*ip + 1] & 0x3f) << 6) | - (in[*ip + 2] & 0x3f); - *ip += 3; - return cp; - } - uint32_t cp = ((uint32_t)(b & 0x7) << 18) | ((uint32_t)(in[*ip + 1] & 0x3f) << 12) | - ((uint32_t)(in[*ip + 2] & 0x3f) << 6) | (in[*ip + 3] & 0x3f); - *ip += 4; +/* Decode the next code point from the n bytes of an ScrStr's storage. + * Storage is WTF-8, except where raw outside bytes become a string + * unchecked (HTTP header values). A byte that does not start a complete, + * well-formed sequence stands for U+0000..U+00FF, as each byte of such a + * header does in Node, and advances by one; nothing is read past n. */ +static uint32_t scr_bytes_next_cp(const uint8_t *in, size_t n, size_t *ip) { + size_t i = *ip; + uint8_t b = in[i]; + *ip = i + 1; + if (b < 0x80) return b; + size_t len = b >= 0xc2 && b <= 0xdf ? 2 : b >= 0xe0 && b <= 0xef ? 3 : b >= 0xf0 && b <= 0xf4 ? 4 : 0; + if (len == 0 || n - i < len) return b; + uint8_t lo = b == 0xe0 ? 0xa0 : b == 0xf0 ? 0x90 : 0x80; + uint8_t hi = b == 0xf4 ? 0x8f : 0xbf; + if (in[i + 1] < lo || in[i + 1] > hi) return b; + for (size_t k = 2; k < len; k++) + if ((in[i + k] & 0xc0) != 0x80) return b; + uint32_t cp = b & (0x7f >> len); + for (size_t k = 1; k < len; k++) cp = (cp << 6) | (in[i + k] & 0x3f); + *ip = i + len; return cp; } @@ -2246,7 +2285,7 @@ ScrDyn *scr_text_encode_into(const ScrDyn *source, const ScrDyn *destination) { read += ascii; if (written == limit) break; size_t end = written; - uint32_t cp = scr_bytes_next_cp((const uint8_t *)text->data, &end); + uint32_t cp = scr_bytes_next_cp((const uint8_t *)text->data, text->len, &end); if (end > dest->len) break; read += cp > 0xffff ? 2 : 1; written = end; @@ -2399,7 +2438,7 @@ ScrBytes *scr_buffer_transcode(const ScrDyn *source, const ScrDyn *from, const S } ScrStr *text = scr_str_from_utf8_lossy(input->data, len); for (size_t i = 0; i < text->len;) { - uint32_t cp = scr_bytes_next_cp((const uint8_t *)text->data, &i); + uint32_t cp = scr_bytes_next_cp((const uint8_t *)text->data, text->len, &i); written = scr_transcode_put(result->data, written, cp, to_encoding); } scr_str_release(text); @@ -2454,14 +2493,157 @@ static uint32_t scr_bytes_utf16le_cp(const uint8_t *in, size_t units, size_t *u) return cu; } +/* Four units below 0x80 copy as four bytes (one word holds them), and + * other units outside the surrogate range are written inline; surrogates + * take scr_bytes_utf16le_cp. The string has one UTF-16 unit per input + * unit, which is recorded so its length needs no walk. */ static ScrStr *scr_bytes_decode_utf16le(const uint8_t *in, size_t n) { size_t units = n / 2; if (units > SIZE_MAX / 3) scr_bytes_oom(); ScrStr *s = scr_str_alloc_raw(0, units * 3); - size_t o = 0; - for (size_t u = 0; u < units;) o = scr_bytes_put_cp(s->data, o, scr_bytes_utf16le_cp(in, units, &u)); + uint8_t *out = (uint8_t *)s->data; + size_t o = 0, u = 0; + while (u < units) { +#if defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ + if (units - u >= 4) { + uint64_t w; + memcpy(&w, in + u * 2, 8); + if (!(w & UINT64_C(0xff80ff80ff80ff80))) { + out[o] = (uint8_t)w; + out[o + 1] = (uint8_t)(w >> 16); + out[o + 2] = (uint8_t)(w >> 32); + out[o + 3] = (uint8_t)(w >> 48); + o += 4; + u += 4; + continue; + } + } +#endif + uint32_t cu = (uint32_t)in[u * 2] | ((uint32_t)in[u * 2 + 1] << 8); + if (cu >= 0x800 && (cu < 0xd800 || cu > 0xdfff)) { + /* Three bytes in one four-byte store; the next write (or the + * terminator) replaces the fourth. */ + uint8_t seq[4] = {(uint8_t)(0xe0 | (cu >> 12)), (uint8_t)(0x80 | ((cu >> 6) & 0x3f)), + (uint8_t)(0x80 | (cu & 0x3f)), 0}; + memcpy(out + o, seq, 4); + o += 3; + u++; + } else if (cu < 0x80) { + out[o++] = (uint8_t)cu; + u++; + } else if (cu < 0x800) { + out[o] = (uint8_t)(0xc0 | (cu >> 6)); + out[o + 1] = (uint8_t)(0x80 | (cu & 0x3f)); + o += 2; + u++; + } else { + o = scr_bytes_put_cp(s->data, o, scr_bytes_utf16le_cp(in, units, &u)); + } + } s->len = o; s->data[o] = '\0'; + if (o == units) scr_str_note_ascii(s); + else scr_str_note_utf16_len(s, units); + return s; +} + +/* The UTF-8 bytes of U+0000..U+00FF: the byte itself below 0x80 (the + * second entry is overwritten by the next write), else a two-byte + * sequence. */ +static const uint8_t scr_latin1_utf8[256][2] = { + {0x00, 0x00}, {0x01, 0x00}, {0x02, 0x00}, {0x03, 0x00}, {0x04, 0x00}, {0x05, 0x00}, {0x06, 0x00}, {0x07, 0x00}, + {0x08, 0x00}, {0x09, 0x00}, {0x0a, 0x00}, {0x0b, 0x00}, {0x0c, 0x00}, {0x0d, 0x00}, {0x0e, 0x00}, {0x0f, 0x00}, + {0x10, 0x00}, {0x11, 0x00}, {0x12, 0x00}, {0x13, 0x00}, {0x14, 0x00}, {0x15, 0x00}, {0x16, 0x00}, {0x17, 0x00}, + {0x18, 0x00}, {0x19, 0x00}, {0x1a, 0x00}, {0x1b, 0x00}, {0x1c, 0x00}, {0x1d, 0x00}, {0x1e, 0x00}, {0x1f, 0x00}, + {0x20, 0x00}, {0x21, 0x00}, {0x22, 0x00}, {0x23, 0x00}, {0x24, 0x00}, {0x25, 0x00}, {0x26, 0x00}, {0x27, 0x00}, + {0x28, 0x00}, {0x29, 0x00}, {0x2a, 0x00}, {0x2b, 0x00}, {0x2c, 0x00}, {0x2d, 0x00}, {0x2e, 0x00}, {0x2f, 0x00}, + {0x30, 0x00}, {0x31, 0x00}, {0x32, 0x00}, {0x33, 0x00}, {0x34, 0x00}, {0x35, 0x00}, {0x36, 0x00}, {0x37, 0x00}, + {0x38, 0x00}, {0x39, 0x00}, {0x3a, 0x00}, {0x3b, 0x00}, {0x3c, 0x00}, {0x3d, 0x00}, {0x3e, 0x00}, {0x3f, 0x00}, + {0x40, 0x00}, {0x41, 0x00}, {0x42, 0x00}, {0x43, 0x00}, {0x44, 0x00}, {0x45, 0x00}, {0x46, 0x00}, {0x47, 0x00}, + {0x48, 0x00}, {0x49, 0x00}, {0x4a, 0x00}, {0x4b, 0x00}, {0x4c, 0x00}, {0x4d, 0x00}, {0x4e, 0x00}, {0x4f, 0x00}, + {0x50, 0x00}, {0x51, 0x00}, {0x52, 0x00}, {0x53, 0x00}, {0x54, 0x00}, {0x55, 0x00}, {0x56, 0x00}, {0x57, 0x00}, + {0x58, 0x00}, {0x59, 0x00}, {0x5a, 0x00}, {0x5b, 0x00}, {0x5c, 0x00}, {0x5d, 0x00}, {0x5e, 0x00}, {0x5f, 0x00}, + {0x60, 0x00}, {0x61, 0x00}, {0x62, 0x00}, {0x63, 0x00}, {0x64, 0x00}, {0x65, 0x00}, {0x66, 0x00}, {0x67, 0x00}, + {0x68, 0x00}, {0x69, 0x00}, {0x6a, 0x00}, {0x6b, 0x00}, {0x6c, 0x00}, {0x6d, 0x00}, {0x6e, 0x00}, {0x6f, 0x00}, + {0x70, 0x00}, {0x71, 0x00}, {0x72, 0x00}, {0x73, 0x00}, {0x74, 0x00}, {0x75, 0x00}, {0x76, 0x00}, {0x77, 0x00}, + {0x78, 0x00}, {0x79, 0x00}, {0x7a, 0x00}, {0x7b, 0x00}, {0x7c, 0x00}, {0x7d, 0x00}, {0x7e, 0x00}, {0x7f, 0x00}, + {0xc2, 0x80}, {0xc2, 0x81}, {0xc2, 0x82}, {0xc2, 0x83}, {0xc2, 0x84}, {0xc2, 0x85}, {0xc2, 0x86}, {0xc2, 0x87}, + {0xc2, 0x88}, {0xc2, 0x89}, {0xc2, 0x8a}, {0xc2, 0x8b}, {0xc2, 0x8c}, {0xc2, 0x8d}, {0xc2, 0x8e}, {0xc2, 0x8f}, + {0xc2, 0x90}, {0xc2, 0x91}, {0xc2, 0x92}, {0xc2, 0x93}, {0xc2, 0x94}, {0xc2, 0x95}, {0xc2, 0x96}, {0xc2, 0x97}, + {0xc2, 0x98}, {0xc2, 0x99}, {0xc2, 0x9a}, {0xc2, 0x9b}, {0xc2, 0x9c}, {0xc2, 0x9d}, {0xc2, 0x9e}, {0xc2, 0x9f}, + {0xc2, 0xa0}, {0xc2, 0xa1}, {0xc2, 0xa2}, {0xc2, 0xa3}, {0xc2, 0xa4}, {0xc2, 0xa5}, {0xc2, 0xa6}, {0xc2, 0xa7}, + {0xc2, 0xa8}, {0xc2, 0xa9}, {0xc2, 0xaa}, {0xc2, 0xab}, {0xc2, 0xac}, {0xc2, 0xad}, {0xc2, 0xae}, {0xc2, 0xaf}, + {0xc2, 0xb0}, {0xc2, 0xb1}, {0xc2, 0xb2}, {0xc2, 0xb3}, {0xc2, 0xb4}, {0xc2, 0xb5}, {0xc2, 0xb6}, {0xc2, 0xb7}, + {0xc2, 0xb8}, {0xc2, 0xb9}, {0xc2, 0xba}, {0xc2, 0xbb}, {0xc2, 0xbc}, {0xc2, 0xbd}, {0xc2, 0xbe}, {0xc2, 0xbf}, + {0xc3, 0x80}, {0xc3, 0x81}, {0xc3, 0x82}, {0xc3, 0x83}, {0xc3, 0x84}, {0xc3, 0x85}, {0xc3, 0x86}, {0xc3, 0x87}, + {0xc3, 0x88}, {0xc3, 0x89}, {0xc3, 0x8a}, {0xc3, 0x8b}, {0xc3, 0x8c}, {0xc3, 0x8d}, {0xc3, 0x8e}, {0xc3, 0x8f}, + {0xc3, 0x90}, {0xc3, 0x91}, {0xc3, 0x92}, {0xc3, 0x93}, {0xc3, 0x94}, {0xc3, 0x95}, {0xc3, 0x96}, {0xc3, 0x97}, + {0xc3, 0x98}, {0xc3, 0x99}, {0xc3, 0x9a}, {0xc3, 0x9b}, {0xc3, 0x9c}, {0xc3, 0x9d}, {0xc3, 0x9e}, {0xc3, 0x9f}, + {0xc3, 0xa0}, {0xc3, 0xa1}, {0xc3, 0xa2}, {0xc3, 0xa3}, {0xc3, 0xa4}, {0xc3, 0xa5}, {0xc3, 0xa6}, {0xc3, 0xa7}, + {0xc3, 0xa8}, {0xc3, 0xa9}, {0xc3, 0xaa}, {0xc3, 0xab}, {0xc3, 0xac}, {0xc3, 0xad}, {0xc3, 0xae}, {0xc3, 0xaf}, + {0xc3, 0xb0}, {0xc3, 0xb1}, {0xc3, 0xb2}, {0xc3, 0xb3}, {0xc3, 0xb4}, {0xc3, 0xb5}, {0xc3, 0xb6}, {0xc3, 0xb7}, + {0xc3, 0xb8}, {0xc3, 0xb9}, {0xc3, 0xba}, {0xc3, 0xbb}, {0xc3, 0xbc}, {0xc3, 0xbd}, {0xc3, 0xbe}, {0xc3, 0xbf}, +}; + +/* Each byte is U+00XX ("binary" normalizes here upstream): a byte below + * 0x80 is one UTF-8 byte and any other byte is two. Eight bytes without a + * high bit copy as one word; other bytes take their two table bytes and + * advance by their length, without branches (the terminator slot holds + * the last spare byte). */ +/* The number of bytes from 0x80: eight per step, each step adding its + * high bits into byte lanes that are summed every 255 steps. */ +static size_t scr_bytes_count_high(const uint8_t *in, size_t n) { + const uint64_t ones = UINT64_C(0x0101010101010101); + const uint64_t pairs = UINT64_C(0x00ff00ff00ff00ff); + size_t high = 0, i = 0; + while (n - i >= 8) { + size_t steps = (n - i) / 8 < 255 ? (n - i) / 8 : 255; + uint64_t lanes = 0; + for (size_t end = i + steps * 8; i < end; i += 8) { + uint64_t w; + memcpy(&w, in + i, 8); + lanes += (w >> 7) & ones; + } + lanes = (lanes & pairs) + ((lanes >> 8) & pairs); + high += (size_t)((lanes * UINT64_C(0x0001000100010001)) >> 48); + } + for (; i < n; i++) high += in[i] >> 7; + return high; +} + +static ScrStr *scr_bytes_latin1_text(const uint8_t *in, size_t n) { + size_t high = scr_bytes_count_high(in, n); + if (n > SIZE_MAX - high) scr_bytes_oom(); + ScrStr *s = scr_str_alloc_raw(n + high, n + high); + uint8_t *out = (uint8_t *)s->data; + if (high == 0) { + memcpy(out, in, n); + out[n] = '\0'; + scr_str_note_ascii(s); + return s; + } + const uint64_t top = UINT64_C(0x8080808080808080); + size_t i = 0, o = 0; + while (n - i >= 8) { + uint64_t w; + memcpy(&w, in + i, 8); + if (!(w & top)) { + memcpy(out + o, &w, 8); + i += 8; + o += 8; + continue; + } + for (size_t end = i + 8; i < end; i++) { + memcpy(out + o, scr_latin1_utf8[in[i]], 2); + o += 1 + (in[i] >> 7); + } + } + for (; i < n; i++) { + memcpy(out + o, scr_latin1_utf8[in[i]], 2); + o += 1 + (in[i] >> 7); + } + out[o] = '\0'; + scr_str_note_utf16_len(s, n); return s; } @@ -2480,21 +2662,12 @@ ScrStr *scr_bytes_to_str(const ScrBytes *b, const ScrStr *enc) { out[i * 2 + 1] = scr_hex_digits[in[i] & 0xf]; } out[n * 2] = '\0'; + scr_str_note_ascii(s); return s; } if (scr_enc_is(enc, "base64")) return scr_bytes_encode_b64(in, n, false); if (scr_enc_is(enc, "base64url")) return scr_bytes_encode_b64(in, n, true); - if (scr_enc_is(enc, "latin1")) { - /* Each byte is U+00XX ("binary" normalizes here upstream). */ - size_t high = 0; - for (size_t i = 0; i < n; i++) high += in[i] >> 7; - if (n > SIZE_MAX - high) scr_bytes_oom(); - ScrStr *s = scr_str_alloc_raw(n + high, n + high); - size_t o = 0; - for (size_t i = 0; i < n; i++) o = scr_bytes_put_cp(s->data, o, in[i]); - s->data[o] = '\0'; - return s; - } + if (scr_enc_is(enc, "latin1")) return scr_bytes_latin1_text(in, n); if (scr_enc_is(enc, "ascii")) { /* Node's ascii decode masks the high bit: byte & 0x7f. */ ScrStr *s = scr_str_alloc_raw(n, n); @@ -2589,12 +2762,25 @@ ScrStr *scr_bytes_to_str_checked_range(const ScrBytes *b, const ScrStr *enc, return out; } -static int scr_hex_val(uint8_t c) { - if (c >= '0' && c <= '9') return c - '0'; - if (c >= 'a' && c <= 'f') return c - 'a' + 10; - if (c >= 'A' && c <= 'F') return c - 'A' + 10; - return -1; -} +/* Hex digit values, -1 for any other byte. */ +static const int8_t scr_hex_values[256] = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1, + -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, +}; /* Both alphabets are accepted by both Buffer base64 encoding names. */ static const int8_t scr_b64_values[256] = { @@ -2640,20 +2826,67 @@ static ScrBytes *scr_bytes_conversion_storage(size_t capacity) { return scr_bytes_conversion_storage_in(NULL, capacity); } +/* scr_b64_values, each value shifted into its place in a four-character + * group's 24 bits; a character outside both alphabets sets bit 24. */ +#define SCR_B64_VALUE(c) \ + ((c) >= 'A' && (c) <= 'Z' ? (c) - 'A' \ + : (c) >= 'a' && (c) <= 'z' ? (c) - 'a' + 26 \ + : (c) >= '0' && (c) <= '9' ? (c) - '0' + 52 \ + : (c) == '+' || (c) == '-' ? 62 \ + : (c) == '/' || (c) == '_' ? 63 \ + : -1) +#define SCR_B64_AT(c, shift) \ + (SCR_B64_VALUE(c) < 0 ? UINT32_C(0x1000000) : (uint32_t)SCR_B64_VALUE(c) << (shift)) +#define SCR_B64_4(c, s) SCR_B64_AT(c, s), SCR_B64_AT(c + 1, s), SCR_B64_AT(c + 2, s), SCR_B64_AT(c + 3, s) +#define SCR_B64_16(c, s) SCR_B64_4(c, s), SCR_B64_4(c + 4, s), SCR_B64_4(c + 8, s), SCR_B64_4(c + 12, s) +#define SCR_B64_64(c, s) SCR_B64_16(c, s), SCR_B64_16(c + 16, s), SCR_B64_16(c + 32, s), SCR_B64_16(c + 48, s) +#define SCR_B64_256(s) SCR_B64_64(0, s), SCR_B64_64(64, s), SCR_B64_64(128, s), SCR_B64_64(192, s) +static const uint32_t scr_b64_shifted[4][256] = { + {SCR_B64_256(18)}, {SCR_B64_256(12)}, {SCR_B64_256(6)}, {SCR_B64_256(0)}}; + +/* One group's 24 bits as three bytes, in a four-byte store whose last byte + * the next store overwrites (the destination holds two spare bytes). */ +static inline void scr_b64_put_group(uint8_t *out, uint32_t bits) { +#if defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ + uint32_t word = __builtin_bswap32(bits << 8); +#else + uint32_t word = bits << 8; +#endif + memcpy(out, &word, 4); +} + static ScrBytes *scr_bytes_decode_b64(ScrBytesSink *sink, const uint8_t *in, size_t n) { ScrBytes *b = scr_bytes_conversion_storage_in(sink, n / 4 * 3 + 2); + /* A local copy: a byte store could otherwise change b->data. */ + uint8_t *out = b->data; + const uint32_t(*t)[256] = scr_b64_shifted; size_t i = 0, o = 0; unsigned acc = 0, have = 0; while (i < n) { + /* Complete groups, two per step: each character's shifted value comes + * from a table and one bit test covers all eight. The second store + * ends at o + 6, within the destination while eight characters remain. */ + if (have == 0) { + while (n - i >= 8) { + uint32_t first = t[0][in[i]] | t[1][in[i + 1]] | t[2][in[i + 2]] | t[3][in[i + 3]]; + uint32_t second = t[0][in[i + 4]] | t[1][in[i + 5]] | t[2][in[i + 6]] | t[3][in[i + 7]]; + if ((first | second) & UINT32_C(0x1000000)) break; + scr_b64_put_group(out + o, first); + scr_b64_put_group(out + o + 3, second); + o += 6; + i += 8; + } + if (i == n) break; /* the groups ended the input */ + } /* Common complete groups need neither per-character branches nor state. */ if (have == 0 && n - i >= 4) { int a = scr_b64_values[in[i]], c = scr_b64_values[in[i + 1]]; int d = scr_b64_values[in[i + 2]], e = scr_b64_values[in[i + 3]]; if ((a | c | d | e) >= 0) { unsigned v = ((unsigned)a << 18) | ((unsigned)c << 12) | ((unsigned)d << 6) | (unsigned)e; - b->data[o++] = (uint8_t)(v >> 16); - b->data[o++] = (uint8_t)(v >> 8); - b->data[o++] = (uint8_t)v; + out[o++] = (uint8_t)(v >> 16); + out[o++] = (uint8_t)(v >> 8); + out[o++] = (uint8_t)v; i += 4; continue; } @@ -2664,17 +2897,17 @@ static ScrBytes *scr_bytes_decode_b64(ScrBytesSink *sink, const uint8_t *in, siz if (v < 0) continue; acc = (acc << 6) | (unsigned)v; if (++have == 4) { - b->data[o++] = (uint8_t)(acc >> 16); - b->data[o++] = (uint8_t)(acc >> 8); - b->data[o++] = (uint8_t)acc; + out[o++] = (uint8_t)(acc >> 16); + out[o++] = (uint8_t)(acc >> 8); + out[o++] = (uint8_t)acc; acc = 0; have = 0; } } - if (have == 2) b->data[o++] = (uint8_t)(acc >> 4); + if (have == 2) out[o++] = (uint8_t)(acc >> 4); else if (have == 3) { - b->data[o++] = (uint8_t)(acc >> 10); - b->data[o++] = (uint8_t)(acc >> 2); + out[o++] = (uint8_t)(acc >> 10); + out[o++] = (uint8_t)(acc >> 2); } b->len = o; return b; @@ -2688,17 +2921,82 @@ static ScrBytes *scr_bytes_encode_str(ScrBytesSink *sink, const ScrStr *s, const size_t n = s->len; if (scr_enc_is(enc, "latin1") || scr_enc_is(enc, "ascii")) { /* Node writes charCodeAt(i) & 0xFF for BOTH spellings — an astral - * code point contributes its two surrogates' low bytes. */ + * code point contributes its two surrogates' low bytes. The ASCII + * prefix copies. Elsewhere eight ASCII bytes copy as one word. In eight bytes + * holding two or more high bytes, all of them in two-byte sequences, + * every continuation byte's output byte (the code point's low byte, + * from the continuation and its lead) is computed in one word; each + * byte is then stored without branches, a continuation replacing the + * byte its lead wrote. A lead in a word's last byte stays pending: its + * continuation, if the next byte is one, replaces it the same way. Any + * other byte decodes with scr_bytes_next_cp. */ ScrBytes *b = scr_bytes_conversion_storage_in(sink, n); - size_t o = 0, i = 0; + /* A local copy: a byte store could otherwise change b->data. */ + uint8_t *dst = b->data; + const uint64_t top = UINT64_C(0x8080808080808080); + /* An ASCII prefix (all of an ASCII string) is its own encoding. */ + size_t o = s->ascii ? n : scr_bytes_ascii_prefix(in, n), i = o; + memcpy(dst, in, o); + /* in[i - 1] is a two-byte lead (0xC2..0xDF) the word path wrote as + * dst[o - 1], and in[i] is not yet known to continue it. */ + bool pending = false; while (i < n) { - uint32_t cp = scr_bytes_next_cp(in, &i); + if (n - i >= 8) { + uint64_t w; + memcpy(&w, in + i, 8); + uint64_t high = w & top; + if (!high) { + memcpy(dst + o, &w, 8); + i += 8; + o += 8; + pending = false; + continue; + } +#if defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ + /* Byte k of the input is bits 8k..8k+7 of w; bit 8k+7 of w << j + * is bit 7 - j of byte k. */ + uint64_t cont = w & ~(w << 1) & top; /* 10xxxxxx */ + uint64_t lead = w & (w << 1) & ~(w << 2) & top; /* 110xxxxx */ + /* 0xC0 and 0xC1 (bits 4..1 clear) only begin overlong forms. */ + uint64_t overlong = lead & ~((w << 3) | (w << 4) | (w << 5) | (w << 6)); + if ((high & (high - 1)) && high == (cont | lead) && !overlong && + cont == ((lead << 8) | (pending ? 0x80u : 0u))) { + uint64_t prev = (w << 8) | (pending ? in[i - 1] : 0u); + uint64_t low = ((prev & UINT64_C(0x0303030303030303)) << 6) | + (w & UINT64_C(0x3f3f3f3f3f3f3f3f)); + uint64_t mask = (cont >> 7) * 0xff; + uint64_t out = (w & ~mask) | (low & mask); + for (unsigned k = 0; k < 8; k++) { + o -= (size_t)(cont >> (8 * k + 7)) & 1; + dst[o++] = (uint8_t)(out >> (8 * k)); + } + i += 8; + pending = (lead >> 63) != 0; + continue; + } +#endif + } + unsigned c = in[i]; + if (pending && c >> 6 == 2) { + /* The rest of the two-byte sequence whose lead ended a word. */ + dst[o - 1] = (uint8_t)(((in[i - 1] & 3u) << 6) | (c & 0x3f)); + i++; + pending = false; + continue; + } + pending = false; + if (c < 0x80) { + dst[o++] = (uint8_t)c; + i++; + continue; + } + uint32_t cp = scr_bytes_next_cp(in, n, &i); if (cp > 0xffff) { cp -= 0x10000; - b->data[o++] = (uint8_t)(0xd800 + (cp >> 10)); - b->data[o++] = (uint8_t)(0xdc00 + (cp & 0x3ff)); + dst[o++] = (uint8_t)(0xd800 + (cp >> 10)); + dst[o++] = (uint8_t)(0xdc00 + (cp & 0x3ff)); } else { - b->data[o++] = (uint8_t)cp; + dst[o++] = (uint8_t)cp; } } b->len = o; /* never grows past n: multi-byte cps shrink */ @@ -2709,7 +3007,7 @@ static ScrBytes *scr_bytes_encode_str(ScrBytesSink *sink, const ScrStr *s, const ScrBytes *b = scr_bytes_conversion_storage_in(sink, n * 2); size_t o = 0, i = 0; while (i < n) { - uint32_t cp = scr_bytes_next_cp(in, &i); + uint32_t cp = scr_bytes_next_cp(in, n, &i); if (cp > 0xffff) { cp -= 0x10000; uint32_t hi = 0xd800 + (cp >> 10), lo = 0xdc00 + (cp & 0x3ff); @@ -2730,7 +3028,7 @@ static ScrBytes *scr_bytes_encode_str(ScrBytesSink *sink, const ScrStr *s, const /* Node interprets encoded text as the low byte of each UTF-16 unit. * ASCII can borrow storage; non-ASCII must preserve that truncation. */ ScrBytes *narrowed = NULL; - if (scr_bytes_ascii_prefix(in, n) != n) { + if (!scr_str_known_ascii(s) && scr_bytes_ascii_prefix(in, n) != n) { ScrStr *latin1 = scr_str_new("latin1", 6); narrowed = scr_bytes_encode_str(NULL, s, latin1); scr_str_release(latin1); @@ -2741,11 +3039,12 @@ static ScrBytes *scr_bytes_encode_str(ScrBytesSink *sink, const ScrStr *s, const if (hex) { /* Stop at the first invalid pair, discarding an odd tail. */ b = scr_bytes_conversion_storage_in(sink, n / 2); - size_t o = 0; - for (size_t i = 0; i + 1 < n; i += 2) { - int hi = scr_hex_val(in[i]), lo = scr_hex_val(in[i + 1]); - if (hi < 0 || lo < 0) break; - b->data[o++] = (uint8_t)((hi << 4) | lo); + uint8_t *dst = b->data; + size_t pairs = n / 2, o = 0; + for (; o < pairs; o++) { + int hi = scr_hex_values[in[2 * o]], lo = scr_hex_values[in[2 * o + 1]]; + if ((hi | lo) < 0) break; + dst[o] = (uint8_t)((hi << 4) | lo); } b->len = o; } else { @@ -2787,16 +3086,30 @@ static SCR_TL bool scr_pool_registered; /* scr_buffer_pool_reset runs at context /* Buffer.poolSize >>> 1, kept with the setting. */ static SCR_TL size_t scr_pool_half = 32768; -double scr_buffer_pool_size(void) { return scr_pool_setting; } +SCR_TL void (*scr_buffer_pool_refresh_hook)(void) = NULL; +SCR_TL void (*scr_buffer_pool_publish_hook)(double size) = NULL; -void scr_buffer_set_pool_size(double size) { +double scr_buffer_pool_size(void) { + if (scr_buffer_pool_refresh_hook) scr_buffer_pool_refresh_hook(); + return scr_pool_setting; +} + +void scr_buffer_pool_apply(double size) { scr_pool_setting = size; double t = isfinite(size) ? fmod(trunc(size), 4294967296.0) : 0; if (t < 0) t += 4294967296.0; scr_pool_half = (size_t)((uint32_t)t >> 1); } -static size_t scr_pool_threshold(void) { return scr_pool_half; } +void scr_buffer_set_pool_size(double size) { + scr_buffer_pool_apply(size); + if (scr_buffer_pool_publish_hook) scr_buffer_pool_publish_hook(size); +} + +static size_t scr_pool_threshold(void) { + if (scr_buffer_pool_refresh_hook) scr_buffer_pool_refresh_hook(); + return scr_pool_half; +} /* Bytes left in the slab. Aligning can move the offset past the end of a * slab whose size is not a multiple of 8; nothing is left then. */ @@ -2854,6 +3167,8 @@ static ScrBytes *scr_pool_take(size_t len) { v->external = false; v->untransferable = false; v->shared = NULL; + v->properties = NULL; + v->properties_traced = false; #ifdef SCR_RC_AUDIT scr_live_bytes++; #endif @@ -3300,6 +3615,55 @@ double scr_bytes_index_of_num(const ScrBytes *b, double v, double off, bool fwd) return scr_bytes_index_of(b, &needle, off, 1, fwd); } +/* The first index a TypedArray search from `from` starts at + * (ToIntegerOrInfinity, then relative to the end when negative), or false + * when it starts past the last element. */ +static bool scr_bytes_search_start(size_t len, double from, size_t *start) { + double n = from != from ? 0 : trunc(from); + if (n >= (double)len) return false; + if (n < 0) n = n + (double)len < 0 ? 0 : n + (double)len; + *start = (size_t)n; + return true; +} + +double scr_bytes_index_of_elem(const ScrBytes *b, double x, double from, bool last) { + /* A Buffer viewed as a Uint8Array keeps Buffer's own search: the needle + * is taken as a byte, and a NaN byteOffset searches the whole buffer. */ + if (scr_bytes_is_buffer(b)) return scr_bytes_index_of_num(b, x, from, !last); + SCR_BYTES_SNAPSHOT(b); + size_t len = b->len; + size_t size = scr_bytes_elem_size(b->elem); + if (len == 0 || x != x) return -1; + if (!last) { + size_t k; + if (!scr_bytes_search_start(len, from, &k)) return -1; + for (; k < len; k++) + if (scr_bytes_decode(b->elem, b->data + k * size) == x) return (double)k; + return -1; + } + double n = from != from ? 0 : trunc(from); + double k = n >= 0 ? (n < (double)len - 1 ? n : (double)len - 1) : (double)len + n; + if (k < 0) return -1; + for (size_t i = (size_t)k + 1; i-- > 0;) + if (scr_bytes_decode(b->elem, b->data + i * size) == x) return (double)i; + return -1; +} + +bool scr_bytes_includes_elem(const ScrBytes *b, double x, double from) { + if (scr_bytes_is_buffer(b)) return scr_bytes_index_of_num(b, x, from, true) >= 0; + SCR_BYTES_SNAPSHOT(b); + size_t len = b->len; + size_t size = scr_bytes_elem_size(b->elem); + size_t k; + if (len == 0 || !scr_bytes_search_start(len, from, &k)) return false; + bool nan = x != x; + for (; k < len; k++) { + double v = scr_bytes_decode(b->elem, b->data + k * size); + if (v == x || (nan && v != v)) return true; + } + return false; +} + /* fill's shared core over a normalized byte pattern. nargs counts the * present offset/end args. An EMPTY pattern zero-fills when zero_ok * (Node's '' string) and is the constant TypeError otherwise (an empty @@ -3325,7 +3689,15 @@ static ScrBytes *scr_bytes_fill_core(ScrBytes *b, const uint8_t *pat, size_t pat } else if (patn == 1) { memset(dst, pat[0], e - o); } else { - for (size_t i = o; i < e; i++) dst[i - o] = pat[(i - o) % patn]; + /* Node's fill: the pattern's first bytes (memmove: the pattern may + * be a view of this buffer), then the filled prefix doubled. */ + size_t len = e - o, done = patn < len ? patn : len; + memmove(dst, pat, done); + while (done < len) { + size_t step = done < len - done ? done : len - done; + memcpy(dst + done, dst, step); + done += step; + } } if (b->shared) { scr_shared_store(b->data + o, dst, e - o); @@ -3404,11 +3776,29 @@ ScrBytes *scr_bytes_swap(ScrBytes *b, double width) { scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)mlen, "ERR_INVALID_BUFFER_SIZE"); return NULL; } - for (size_t g = 0; g < b->len; g += w) { - for (size_t i = 0; i < w / 2; i++) { - uint8_t t = b->data[g + i]; - b->data[g + i] = b->data[g + w - 1 - i]; - b->data[g + w - 1 - i] = t; + /* One load, byte swap and store per group. */ + uint8_t *p = b->data; + size_t len = b->len; + if (w == 2) { + for (size_t g = 0; g < len; g += 2) { + uint16_t v; + memcpy(&v, p + g, 2); + v = __builtin_bswap16(v); + memcpy(p + g, &v, 2); + } + } else if (w == 4) { + for (size_t g = 0; g < len; g += 4) { + uint32_t v; + memcpy(&v, p + g, 4); + v = __builtin_bswap32(v); + memcpy(p + g, &v, 4); + } + } else { + for (size_t g = 0; g < len; g += 8) { + uint64_t v; + memcpy(&v, p + g, 8); + v = __builtin_bswap64(v); + memcpy(p + g, &v, 8); } } return scr_bytes_retain(b); @@ -3522,7 +3912,7 @@ double scr_bytes_write_str(ScrBytes *b, const ScrStr *s, const ScrStr *enc, continue; } } - uint32_t cp = scr_bytes_next_cp((const uint8_t *)s->data, &i); + uint32_t cp = scr_bytes_next_cp((const uint8_t *)s->data, s->len, &i); uint32_t first = cp, second = 0; if (cp > 0xffff) { cp -= 0x10000; @@ -3751,8 +4141,8 @@ static bool scr_bytes_rw_check(const ScrBytes *b, double offset, size_t width) { * exactly Node — and writes zeros downstream. */ static bool scr_bytes_check_int(const ScrBytes *b, double value, double offset, size_t width, bool sign) { - double max = sign ? exp2((double)(8 * width - 1)) - 1 : exp2((double)(8 * width)) - 1; - double min = sign ? -exp2((double)(8 * width - 1)) : 0; + double max = sign ? ldexp(1.0, (int)(8 * width - 1)) - 1 : ldexp(1.0, (int)(8 * width)) - 1; + double min = sign ? -ldexp(1.0, (int)(8 * width - 1)) : 0; if (value > max || value < min) { char recv[48]; scr_num_received(value, recv); diff --git a/packages/runtime/src/scr_bytes.h b/packages/runtime/src/scr_bytes.h index 9b382dda5..4b9174bc0 100644 --- a/packages/runtime/src/scr_bytes.h +++ b/packages/runtime/src/scr_bytes.h @@ -65,7 +65,15 @@ typedef struct ScrBytes { bool is_data_view; /* DataView is distinct from a numeric typed array. */ bool external; /* Root aliases foreign memory; releasing it never frees data. */ bool untransferable; /* A Buffer pool slab: its ArrayBuffer cannot be transferred. */ + /* The cycle collector's last trace of `properties` from the one checked + * value that held this view (see scr_dyn_trace_v). In padding, so the + * offsets compiled code uses hold. */ + bool properties_traced; ScrSharedBytes *shared; /* Neutral backing, borrowed by views from their local owner. */ + /* Named properties a program adds to the view (`ta.foo = 1`, + * Object.defineProperty): a checked object, NULL until the first one. + * Runtime-only and last, so compiled code's field indices hold. */ + struct ScrDyn *properties; } ScrBytes; /* Copies out of and into shared storage, made of relaxed atomic accesses @@ -357,6 +365,24 @@ ScrStr *scr_text_decode(const ScrBytes *b); double scr_text_decoder_encoding(const ScrDyn *label); ScrStr *scr_text_decoder_name(double encoding); ScrDyn *scr_text_encode_into(const ScrDyn *source, const ScrDyn *destination); +/* TextEncoder and TextDecoder records as checked-dynamic handles + * (scr_text_codec_dyn.c, linked by the textCodecDyn runtime feature). The + * boxes borrow the record and retain it through `retain`; a decoder also + * names the byte offsets of its encoding (f64), fatal and ignoreBOM (i8) + * and state (dyn) fields. from_dyn answers the retained record or throws + * the path-annotated TypeError and answers NULL. */ +ScrDyn *scr_text_encoder_to_dyn(void *rec, void *(*retain)(void *), void (*release)(void *)); +ScrDyn *scr_text_decoder_to_dyn(void *rec, void *(*retain)(void *), void (*release)(void *), + size_t encoding, size_t fatal, size_t ignore_bom, size_t state); +void *scr_text_codec_from_dyn(const ScrDyn *d, bool decoder, const struct ScrDynPath *path, + const char *want); +bool scr_text_codec_is(const ScrDyn *d, bool decoder); +/* StringDecoder records as checked-dynamic handles (the same unit): the + * byte offsets of the encoding (string) and pending (f64) fields. */ +ScrDyn *scr_string_decoder_to_dyn(void *rec, void *(*retain)(void *), void (*release)(void *), + size_t encoding, size_t pending); +void *scr_string_decoder_from_dyn(const ScrDyn *d, const struct ScrDynPath *path, + const char *want); bool scr_buffer_is_ascii(const ScrDyn *input); bool scr_buffer_is_utf8(const ScrDyn *input); ScrBytes *scr_buffer_transcode(const ScrDyn *source, const ScrDyn *from, const ScrDyn *to); @@ -388,6 +414,13 @@ ScrBytes *scr_bytes_from_str(const ScrStr *s, const ScrStr *enc); * slab. A slab lives while any of its views does. All return +1 Buffers. */ double scr_buffer_pool_size(void); void scr_buffer_set_pool_size(double size); +/* The setting without publishing it (scr_buffer_pool_refresh_hook). */ +void scr_buffer_pool_apply(double size); +/* Set while a Buffer function value exists (scr_builtin_values.c): its + * poolSize property is the setting, which the refresh hook reads before + * each pool decision and the publish hook receives from static writes. */ +extern SCR_TL void (*scr_buffer_pool_refresh_hook)(void); +extern SCR_TL void (*scr_buffer_pool_publish_hook)(double size); ScrBytes *scr_buffer_alloc(double size); /* zeroed, never pooled */ ScrBytes *scr_buffer_alloc_unsafe(double size); /* pooled */ ScrBytes *scr_buffer_alloc_unsafe_slow(double size); /* never pooled */ @@ -398,6 +431,15 @@ ScrBytes *scr_buffer_from_arr(const ScrArr *arr); /* number[] */ ScrBytes *scr_buffer_from_dyn_pooled(const ScrDyn *value, const ScrStr *encoding); ScrBytes *scr_buffer_from_dyn_ref_pooled(const ScrDyn *value, const ScrStr *encoding); ScrBytes *scr_buffer_from_dyn_offset_pooled(const ScrDyn *value, const ScrDyn *second); +/* Buffer.from(value, byteOffset, length): an ArrayBuffer's view with + * Node's fromArrayBuffer checks; other values as the two-argument form. */ +ScrBytes *scr_buffer_from_dyn_range_pooled(const ScrDyn *value, const ScrDyn *second, + const ScrDyn *third); +/* The deprecated Buffer constructor's DEP0005 state (scr_bytes_io.c): a + * call from node_modules counts toward Node's 10000-call check. */ +extern SCR_TL bool scr_buffer_ctor_warned; +extern SCR_TL uint64_t scr_buffer_ctor_checks; +void scr_buffer_deprecation_count(void); ScrBytes *scr_buffer_concat(const ScrArr *list); ScrBytes *scr_buffer_concat_len(const ScrArr *list, double total); /* The untyped-list forms: Node's ERR_INVALID_ARG_TYPE checks on the list @@ -406,6 +448,17 @@ ScrBytes *scr_buffer_concat_dyn(const struct ScrDyn *list); ScrBytes *scr_buffer_alloc_value(const struct ScrDyn *size, const struct ScrDyn *fill, const struct ScrDyn *encoding); struct ScrDyn *scr_fs_realpath_value(const struct ScrDyn *path, const struct ScrDyn *options, bool native); +/* The Buffer function as a value (scr_builtin_values.c), one per thread. +1. */ +struct ScrDyn *scr_buffer_constructor_value(void); +/* Installs that function as a Buffer's `.constructor` (the program's + * %main calls it when it reads the value): scr_bytes_buffer_constructor_fn. */ +void scr_buffer_install_value(void); +extern struct ScrDyn *(*scr_bytes_buffer_constructor_fn)(void); +/* A static of that value by name (`Buffer.alloc` read as a value). +1. */ +struct ScrDyn *scr_buffer_static_value(const ScrStr *name); +/* `Buffer.prototype[key]` (scr_bytes_dyn.c). +1, or NULL with the + * exception pending. */ +struct ScrDyn *scr_buffer_prototype_member(const struct ScrDyn *key); ScrBytes *scr_buffer_concat_len_dyn(const struct ScrDyn *list, double total); /* Drops the pool's slab reference and restores Buffer.poolSize (process * exit audits and library session resets). */ @@ -485,6 +538,8 @@ ScrDyn *scr_fs_lchmod_sync_chk(const ScrDyn *path, const ScrDyn *mode); ScrPromise *scr_fsp_lchmod_chk(const ScrDyn *path, const ScrDyn *mode); void scr_fs_read_chk(const ScrDyn *fd, const ScrDyn *buffer, const ScrDyn *offset, const ScrDyn *length, const ScrDyn *position, const ScrStr *fence); +double scr_fs_read_sync_dyn(const ScrDyn *fd, const ScrDyn *buffer, const ScrDyn *offset, + const ScrDyn *length, const ScrDyn *position); void scr_fs_stream_opts_chk(const ScrDyn *path, const ScrDyn *opts, const ScrStr *fence); /* The fs streams' option pieces (scr_fs_stream.c): the getOptions, * options.fd and path checks above without the fence; the checked path @@ -499,6 +554,16 @@ ScrEmitter *scr_emitter_set_max_chk(ScrEmitter *em, const ScrDyn *n); void scr_emitter_set_default_max_chk(const ScrDyn *n, const ScrStr *name); double scr_bytes_index_of(const ScrBytes *b, const ScrBytes *needle, double off, double align, bool fwd); double scr_bytes_index_of_num(const ScrBytes *b, double v, double off, bool fwd); +/* TypedArray indexOf (last false) and lastIndexOf (last true) over any + * element kind, with strict equality: NaN is never found and -0 matches 0. + * `from` is the fromIndex argument (ToIntegerOrInfinity applies); the + * caller passes 0, or +Infinity for lastIndexOf, when it is omitted. A + * Buffer (a Uint8Array receiver can be one) searches as Buffer's + * indexOf and lastIndexOf do. */ +double scr_bytes_index_of_elem(const ScrBytes *b, double x, double from, bool last); +/* TypedArray includes: indexOf's search from `from` with SameValueZero, + * so NaN is found. A Buffer searches as Buffer's includes does. */ +bool scr_bytes_includes_elem(const ScrBytes *b, double x, double from); ScrBytes *scr_bytes_fill(ScrBytes *b, const ScrBytes *pattern, double nargs, double offset, double end); ScrBytes *scr_bytes_fill_num(ScrBytes *b, double v, double nargs, double offset, double end); ScrBytes *scr_bytes_fill_str(ScrBytes *b, const ScrStr *s, const ScrStr *enc, diff --git a/packages/runtime/src/scr_bytes.test.c b/packages/runtime/src/scr_bytes.test.c index 58f52fa63..8b2ff3cc1 100644 --- a/packages/runtime/src/scr_bytes.test.c +++ b/packages/runtime/src/scr_bytes.test.c @@ -37,6 +37,52 @@ static void external_set(void) { } } +/* Same-kind sets into and out of shared storage copy raw bytes, NaN + * payloads included, also between overlapping views of one shared buffer. */ +static void shared_set(void) { + enum { COUNT = 37 }; + for (ScrBytesElem elem = SCR_BYTES_U8; elem <= SCR_BYTES_F16; elem++) { + size_t width = scr_bytes_elem_size(elem), bytes = COUNT * width; + uint8_t pattern[COUNT * 8], expected[COUNT * 8]; + for (size_t i = 0; i < bytes; i++) pattern[i] = (uint8_t)(i * 37 + 0x7f); + /* Signaling NaNs, which a conversion through double would quiet. */ + const uint16_t nan16 = 0x7d01; + const uint32_t nan32 = 0x7fa00001; + const uint64_t nan64 = UINT64_C(0x7ff4000000000001); + for (size_t i = 1; i < COUNT; i += 6) { + if (elem == SCR_BYTES_F16) memcpy(pattern + i * width, &nan16, width); + if (elem == SCR_BYTES_F32) memcpy(pattern + i * width, &nan32, width); + if (elem == SCR_BYTES_F64) memcpy(pattern + i * width, &nan64, width); + } + for (int direction = 0; direction < 2; direction++) { + ScrBytes *shared = scr_bytes_new(elem, COUNT); + scr_bytes_make_shared(shared); + memcpy(shared->data, pattern, bytes); + ScrBytes *src = direction ? scr_bytes_subarray(shared, 3, COUNT) : scr_bytes_subarray(shared, 0, COUNT - 3); + double offset = direction ? 0 : 3; + memcpy(expected, pattern, bytes); + memmove(expected + (size_t)offset * width, pattern + (direction ? 3 : 0) * width, (COUNT - 3) * width); + scr_bytes_set_from(shared, src, offset); + assert(!scr_exc_pending()); + assert(memcmp(shared->data, expected, bytes) == 0); + scr_bytes_release(src); + + ScrBytes *local = scr_bytes_new(elem, COUNT); + memcpy(local->data, pattern, bytes); + ScrBytes *head = scr_bytes_subarray(local, 0, COUNT - 1); + scr_bytes_set_from(shared, head, 1); + memcpy(expected + width, pattern, bytes - width); + assert(memcmp(shared->data, expected, bytes) == 0); + memset(local->data, 0, bytes); + scr_bytes_set_from(local, shared, 0); + assert(memcmp(local->data, expected, bytes) == 0); + scr_bytes_release(head); + scr_bytes_release(local); + scr_bytes_release(shared); + } + } +} + static void copied_storage(void) { /* Same-kind copies preserve NaN payloads, signed zero and ownership. */ const uint64_t bits[] = {UINT64_C(0x7ff8000000000042), UINT64_C(0x8000000000000000), @@ -79,6 +125,7 @@ static void filled_storage(void) { void scr_bytes_test_bulk_storage(void) { long before = scr_bytes_live_count(); external_set(); + shared_set(); copied_storage(); filled_storage(); assert(scr_bytes_live_count() == before); diff --git a/packages/runtime/src/scr_bytes_dyn.c b/packages/runtime/src/scr_bytes_dyn.c index c492d43b1..5ce36bdac 100644 --- a/packages/runtime/src/scr_bytes_dyn.c +++ b/packages/runtime/src/scr_bytes_dyn.c @@ -44,8 +44,8 @@ static size_t scr_bytes_dyn_width(ScrBytesNumKind kind) { /* Node's checkInt range test and message, before the offset is looked at. */ static bool scr_bytes_dyn_value_in_range(double value, size_t width, bool sign) { - double max = sign ? exp2((double)(8 * width - 1)) - 1 : exp2((double)(8 * width)) - 1; - double min = sign ? -exp2((double)(8 * width - 1)) : 0; + double max = sign ? ldexp(1.0, (int)(8 * width - 1)) - 1 : ldexp(1.0, (int)(8 * width)) - 1; + double min = sign ? -ldexp(1.0, (int)(8 * width - 1)) : 0; if (!(value > max || value < min)) return true; char recv[48]; scr_num_received(value, recv); @@ -592,3 +592,215 @@ ScrDyn *scr_dyn_invoke_buffer(ScrDyn *recv, ScrDyn *callee, const char *method, } return scr_dyn_invoke_prepared_id(recv, callee, method, mid_slot, args, argc, what); } + +/* A fixed-width read or write name (readUInt8, writeDoubleLE, ...). */ +static bool scr_bytes_dyn_num_method_name(const char *method) { + bool read = !strncmp(method, "read", 4); + if (!read && strncmp(method, "write", 5)) return false; + const char *rest = method + (read ? 4 : 5); + for (size_t at = 0; at < sizeof scr_bytes_dyn_names / sizeof scr_bytes_dyn_names[0]; at++) + if (!strcmp(rest, scr_bytes_dyn_names[at].name)) return true; + return false; +} + +/* ── Buffer.prototype members read as values ──────────────────────────── + * `Buffer.prototype[name]` (smart-buffer's typeof probe for the BigInt + * methods): an own member answers one function per name, with the name and + * length Node's function has; the parent and offset getters, read on the + * prototype itself, answer undefined; constructor is the Buffer value. + * Members inherited from Uint8Array.prototype and Object.prototype, symbol + * keys and object keys throw SC2020, and other keys answer undefined. The + * functions run the method on a Buffer receiver (`.call(buf, ...)`); other + * receivers throw SC2020. */ +static const struct { const char *key; const char *name; uint32_t length; } scr_buffer_proto_own[] = { + {"readBigUInt64LE", "readBigUInt64LE", 0}, + {"readBigUInt64BE", "readBigUInt64BE", 0}, + {"readBigUint64LE", "readBigUInt64LE", 0}, + {"readBigUint64BE", "readBigUInt64BE", 0}, + {"readBigInt64LE", "readBigInt64LE", 0}, + {"readBigInt64BE", "readBigInt64BE", 0}, + {"writeBigUInt64LE", "writeBigUInt64LE", 1}, + {"writeBigUInt64BE", "writeBigUInt64BE", 1}, + {"writeBigUint64LE", "writeBigUInt64LE", 1}, + {"writeBigUint64BE", "writeBigUInt64BE", 1}, + {"writeBigInt64LE", "writeBigInt64LE", 1}, + {"writeBigInt64BE", "writeBigInt64BE", 1}, + {"readUIntLE", "readUIntLE", 2}, + {"readUInt32LE", "", 1}, + {"readUInt16LE", "", 1}, + {"readUInt8", "", 1}, + {"readUIntBE", "readUIntBE", 2}, + {"readUInt32BE", "", 1}, + {"readUInt16BE", "", 1}, + {"readUintLE", "readUIntLE", 2}, + {"readUint32LE", "", 1}, + {"readUint16LE", "", 1}, + {"readUint8", "", 1}, + {"readUintBE", "readUIntBE", 2}, + {"readUint32BE", "", 1}, + {"readUint16BE", "", 1}, + {"readIntLE", "readIntLE", 2}, + {"readInt32LE", "", 1}, + {"readInt16LE", "", 1}, + {"readInt8", "", 1}, + {"readIntBE", "readIntBE", 2}, + {"readInt32BE", "", 1}, + {"readInt16BE", "", 1}, + {"writeUIntLE", "writeUIntLE", 3}, + {"writeUInt32LE", "writeUInt32LE", 1}, + {"writeUInt16LE", "writeUInt16LE", 1}, + {"writeUInt8", "writeUInt8", 1}, + {"writeUIntBE", "writeUIntBE", 3}, + {"writeUInt32BE", "writeUInt32BE", 1}, + {"writeUInt16BE", "writeUInt16BE", 1}, + {"writeUintLE", "writeUIntLE", 3}, + {"writeUint32LE", "writeUInt32LE", 1}, + {"writeUint16LE", "writeUInt16LE", 1}, + {"writeUint8", "writeUInt8", 1}, + {"writeUintBE", "writeUIntBE", 3}, + {"writeUint32BE", "writeUInt32BE", 1}, + {"writeUint16BE", "writeUInt16BE", 1}, + {"writeIntLE", "writeIntLE", 3}, + {"writeInt32LE", "writeInt32LE", 1}, + {"writeInt16LE", "writeInt16LE", 1}, + {"writeInt8", "writeInt8", 1}, + {"writeIntBE", "writeIntBE", 3}, + {"writeInt32BE", "writeInt32BE", 1}, + {"writeInt16BE", "writeInt16BE", 1}, + {"readFloatLE", "readFloatForwards", 0}, + {"readFloatBE", "readFloatBackwards", 0}, + {"readDoubleLE", "readDoubleForwards", 0}, + {"readDoubleBE", "readDoubleBackwards", 0}, + {"writeFloatLE", "writeFloatForwards", 1}, + {"writeFloatBE", "writeFloatBackwards", 1}, + {"writeDoubleLE", "writeDoubleForwards", 1}, + {"writeDoubleBE", "writeDoubleBackwards", 1}, + {"asciiSlice", "", 2}, + {"base64Slice", "", 2}, + {"base64urlSlice", "", 2}, + {"latin1Slice", "", 2}, + {"hexSlice", "", 2}, + {"ucs2Slice", "", 2}, + {"utf8Slice", "", 2}, + {"asciiWrite", "", 3}, + {"base64Write", "", 3}, + {"base64urlWrite", "", 3}, + {"latin1Write", "", 3}, + {"hexWrite", "", 3}, + {"ucs2Write", "", 3}, + {"utf8Write", "", 3}, + {"parent", NULL, 0}, + {"offset", NULL, 0}, + {"copy", "copy", 4}, + {"toString", "toString", 3}, + {"equals", "equals", 1}, + {"inspect", "inspect", 2}, + {"compare", "compare", 5}, + {"indexOf", "indexOf", 4}, + {"lastIndexOf", "lastIndexOf", 4}, + {"includes", "includes", 4}, + {"fill", "fill", 4}, + {"write", "write", 4}, + {"toJSON", "toJSON", 0}, + {"subarray", "subarray", 2}, + {"slice", "slice", 2}, + {"swap16", "swap16", 0}, + {"swap32", "swap32", 0}, + {"swap64", "swap64", 0}, + {"toLocaleString", "toString", 3}, +}; + +static const char *const scr_buffer_proto_inherited[] = { + "BYTES_PER_ELEMENT", "buffer", "byteLength", "byteOffset", "length", "entries", "keys", "values", + "at", "copyWithin", "every", "filter", "find", "findIndex", "findLast", "findLastIndex", + "forEach", "join", "map", "reverse", "reduce", "reduceRight", "set", "some", "sort", "toReversed", + "toSorted", "with", "__defineGetter__", "__defineSetter__", "hasOwnProperty", "__lookupGetter__", + "__lookupSetter__", "isPrototypeOf", "propertyIsEnumerable", "valueOf", "__proto__", +}; + +static SCR_TL ScrDyn *scr_buffer_proto_fns; + +static void scr_buffer_proto_cleanup(void) { + scr_dyn_release(scr_buffer_proto_fns); + scr_buffer_proto_fns = NULL; +} + +static ScrDyn *scr_buffer_proto_unsupported(const char *format, const char *name) { + char message[200]; + int n = snprintf(message, sizeof message, format, name); + scr_throw_error_msg_code(SCR_ERR_ERROR, message, (size_t)n, "SC2020"); + return NULL; +} + +static ScrDyn *scr_buffer_proto_run(ScrDyn *recv, const char *method, ScrDyn *const *args, size_t argc) { + if (!scr_bytes_dyn_buffer(recv)) + return scr_buffer_proto_unsupported("Buffer.prototype.%s on a receiver that is not a Buffer is not supported yet", + method); + const char *what = "Buffer.prototype method"; + /* The method-id cache slot of a call site, here a fresh one per call. */ + int32_t slot = -1; + int32_t *mid = &slot; + if (scr_bytes_dyn_num_method_name(method)) return scr_dyn_invoke_buffer_num(recv, NULL, method, mid, args, argc, what); + if (!strcmp(method, "write")) return scr_dyn_invoke_buffer_write(recv, NULL, method, mid, args, argc, what); + if (!strcmp(method, "fill")) return scr_dyn_invoke_buffer_fill(recv, NULL, method, mid, args, argc, what); + if (!strcmp(method, "toString") || !strcmp(method, "toLocaleString")) + return scr_dyn_invoke_buffer_to_string(recv, NULL, "toString", mid, args, argc, what); + if (!strcmp(method, "indexOf") || !strcmp(method, "lastIndexOf") || !strcmp(method, "includes")) + return scr_dyn_invoke_buffer_search(recv, NULL, method, mid, args, argc, what); + if (!strcmp(method, "subarray") || !strcmp(method, "slice")) + return scr_dyn_invoke(recv, method, args, argc, what); + if (!strcmp(method, "inspect")) + return scr_buffer_proto_unsupported("Buffer.prototype.%s through a stored value is not supported yet", method); + return scr_dyn_invoke_buffer(recv, NULL, method, mid, args, argc, what); +} + +static ScrDyn *scr_buffer_proto_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + ScrDyn *recv = scr_dyn_this_get(); + ScrDyn *result = scr_buffer_proto_run(recv, closure->fn, args, argc); + scr_dyn_release(recv); + return result; +} + +ScrDyn *scr_buffer_prototype_member(const ScrDyn *key) { + if (key->kind == SCR_DYN_SYMBOL) + return scr_buffer_proto_unsupported("Buffer.prototype members keyed by %s are not supported yet", "symbols"); + if (key->kind != SCR_DYN_STR) { + if (key->kind == SCR_DYN_NUM || key->kind == SCR_DYN_BOOL || key->kind == SCR_DYN_UNDEF || + key->kind == SCR_DYN_NULL || key->kind == SCR_DYN_BIGINT) + return scr_dyn_retain(scr_dyn_undefined()); + return scr_buffer_proto_unsupported("Buffer.prototype members keyed by %s are not supported yet", "objects"); + } + const ScrStr *name = key->v.str; + if (name->len == 11 && memcmp(name->data, "constructor", 11) == 0) { + /* A program that reads the Buffer function's value answers it (the + * builtin-values unit installs the hook); otherwise Buffer keeps its + * builtin identity token, as typed array reads do (scr_json.c). */ + if (scr_bytes_buffer_constructor_fn) return scr_bytes_buffer_constructor_fn(); + ScrStr *token = scr_str_new("[builtin Buffer]", 16); + ScrDyn *result = scr_dyn_new_str(token); + scr_str_release(token); + return result; + } + for (size_t i = 0; i < sizeof scr_buffer_proto_inherited / sizeof scr_buffer_proto_inherited[0]; i++) { + const char *inherited = scr_buffer_proto_inherited[i]; + if (strlen(inherited) == name->len && memcmp(inherited, name->data, name->len) == 0) + return scr_buffer_proto_unsupported("Buffer.prototype.%s is not supported yet", inherited); + } + for (size_t i = 0; i < sizeof scr_buffer_proto_own / sizeof scr_buffer_proto_own[0]; i++) { + const char *own = scr_buffer_proto_own[i].key; + if (strlen(own) != name->len || memcmp(own, name->data, name->len) != 0) continue; + if (!scr_buffer_proto_own[i].name) return scr_dyn_retain(scr_dyn_undefined()); + if (!scr_buffer_proto_fns) { + scr_buffer_proto_fns = scr_dyn_new_obj_null_proto(); + scr_atexit(scr_buffer_proto_cleanup); + } + ScrDyn *fn = scr_dyn_obj_get(scr_buffer_proto_fns, own, name->len); + if (!fn) { + fn = scr_dyn_new_func(scr_closure_new((void *)own, 0), scr_buffer_proto_call, scr_buffer_proto_own[i].length, + "native:function", scr_buffer_proto_own[i].name); + scr_dyn_obj_set(scr_buffer_proto_fns, own, name->len, fn); + } + return scr_dyn_retain(fn); + } + return scr_dyn_retain(scr_dyn_undefined()); +} diff --git a/packages/runtime/src/scr_bytes_io.c b/packages/runtime/src/scr_bytes_io.c index 137208938..daeb47c60 100644 --- a/packages/runtime/src/scr_bytes_io.c +++ b/packages/runtime/src/scr_bytes_io.c @@ -59,6 +59,10 @@ static bool scr_fs_read_utf8(ScrStr *encoding) { return encoding->len == 0 || scr_bytes_encoding_is_utf8(encoding); } +void scr_fs_check_read_encoding(ScrStr *encoding) { + (void)scr_fs_read_encoding_valid(encoding); +} + ScrStr *scr_fs_read_file_encoded(ScrStr *path, ScrStr *encoding) { if (!scr_fs_read_encoding_valid(encoding)) return NULL; if (scr_fs_read_utf8(encoding)) return scr_fs_read_file(path); @@ -631,9 +635,12 @@ void scr_fs_cb_schedule(const ScrDyn *callback, ScrDyn *result, const ScrDyn *bu #endif } -static bool scr_fs_read_validate(const ScrDyn *, const ScrDyn *, const ScrDyn *, const ScrDyn *, const ScrDyn *); +static bool scr_fs_read_validate(const ScrDyn *, const ScrDyn *, const ScrDyn *, const ScrDyn *, const ScrDyn *, + double *); static bool scr_fs_encoding_chk(const ScrDyn *); static bool scr_fs_int_range_chk(const ScrDyn *, const char *, double, double, const char *); +static bool scr_fs_length_int32(const ScrDyn *, double *); +static bool scr_fs_write_validate(const ScrDyn *, const ScrDyn *, const ScrDyn *); static bool scr_fs_mode_chk(const ScrDyn *, const char *); /* Where Node's signature handling finds the callback: a fixed position, or @@ -852,7 +859,7 @@ static ScrDyn *scr_fs_cb_invoke(ScrStr *member, ScrDyn *const *args, size_t argc * (below); every other form checks it here, before its path. */ bool late = !strcmp(op, "access") || !strcmp(op, "copyFile") || !strcmp(op, "chmod") || !strcmp(op, "open") || !strcmp(op, "mkdir") || !strcmp(op, "fchmod") || !strcmp(op, "ftruncate") || !strcmp(op, "truncate") || - !strcmp(op, "read"); + !strcmp(op, "read") || !strcmp(op, "write"); if (!late && !scr_fs_cb_chk(callback, legacy ? "callback" : "cb")) { scr_arr_release(vectors); return NULL; } size_t count = timestamp ? argc : slot < argc ? slot : argc; #define ARG(index) scr_fs_cb_arg(args, count, index) @@ -885,7 +892,8 @@ static ScrDyn *scr_fs_cb_invoke(ScrStr *member, ScrDyn *const *args, size_t argc const ScrDyn *params = count == 3 ? ARG(2) : scr_dyn_bytes_is(ARG(1), SCR_BYTES_U8) ? NULL : ARG(1); bool object = !params || scr_fs_dyn_absent(params) || params->kind == SCR_DYN_OBJ || params->kind == SCR_DYN_BYTES || - (params->kind == SCR_DYN_TYPED_REF && !scr_dyn_ref_is_array(params)); + (params->kind == SCR_DYN_TYPED_REF && !scr_dyn_ref_is_array(params)) || + (params->kind == SCR_DYN_PROXY && scr_dyn_is_object(params)); if (!object) { scr_dyn_arg_type_fail("options", "of type object", params); return NULL; @@ -897,7 +905,28 @@ static ScrDyn *scr_fs_cb_invoke(ScrStr *member, ScrDyn *const *args, size_t argc } if (!scr_fs_cb_chk(callback, "cb")) return NULL; } - if (!strcmp(op, "read") && count >= 4 && !scr_fs_read_validate(ARG(0), ARG(1), ARG(2), ARG(3), ARG(4))) return NULL; + /* The read length after `length |= 0`, converted once (a valueOf call + * runs once, as in Node). */ + double readLength = 0; + if (!strcmp(op, "read") && count >= 4 && + !scr_fs_read_validate(ARG(0), ARG(1), ARG(2), ARG(3), ARG(4), &readLength)) + return NULL; + /* write checks its fd first, then the buffer form's callback, offset and + * length (validateOffsetLengthWrite), or the string form's data and + * callback. */ + if (!strcmp(op, "write")) { + if (!scr_fs_int_range_chk(ARG(0), "fd", 0, 2147483647.0, ">= 0 && <= 2147483647")) return NULL; + if (ARG(1)->kind == SCR_DYN_BYTES) { + if (!scr_fs_cb_chk(callback, "cb") || !scr_fs_write_validate(ARG(1), ARG(2), ARG(3))) return NULL; + } else { + if (ARG(1)->kind != SCR_DYN_STR) { + scr_dyn_arg_type_fail("buffer", "of type string or an instance of Buffer, TypedArray, or DataView", + ARG(1)); + return NULL; + } + if (!scr_fs_cb_chk(callback, "cb")) return NULL; + } + } bool descriptor = timestamp_fd || vector || newControl || !strcmp(op, "close") || !strcmp(op, "fstat") || !strcmp(op, "fsync") || !strcmp(op, "read") || !strcmp(op, "write") || !strcmp(op, "fchown"); /* truncate checks len, then the callback, before the open its request * starts with. */ @@ -1096,7 +1125,10 @@ static ScrDyn *scr_fs_cb_invoke(ScrStr *member, ScrDyn *const *args, size_t argc return NULL; } else { double offset = ownedBuffer ? scr_fs_cb_option_number(options, "offset", 0) : scr_fs_cb_number(ARG(2), "offset", 0); - double length = ownedBuffer ? scr_fs_cb_option_number(options, "length", bytes->len - offset) : scr_fs_cb_number(ARG(3), "length", bytes->len - offset); + double length = bytes->len - offset; + if (ownedBuffer) length = scr_fs_cb_option_number(options, "length", length); + else if (readOp && count >= 4) length = readLength; + else length = scr_fs_cb_number(ARG(3), "length", length); double position = ownedBuffer ? scr_fs_cb_option_number(options, "position", -1) : scr_fs_cb_number(ARG(4), "position", -1); if (!scr_exc_pending()) result = scr_dyn_new_num(readOp ? scr_fs_read_sync(fd, bytes, offset, length, position) : scr_fs_write_sync(fd, bytes, offset, length, position)); } @@ -1299,13 +1331,16 @@ ScrBytes *scr_buffer_concat_len_dyn(const ScrDyn *list, double total) { return scr_buffer_concat_dyn_impl(list, total, true); } -/* Buffer.from(arrayBuffer, byteOffset) over a checked ArrayBuffer or - * SharedArrayBuffer, as Node's fromArrayBuffer: a Buffer from the offset to - * the end of the buffer that shares its memory. The offset converts with - * ToNumber, where a missing offset (NULL) or undefined and NaN are 0; an - * offset past the end throws ERR_BUFFER_OUT_OF_BOUNDS, and one at or below - * -1 V8's start-offset RangeError. NULL with the exception pending. */ -static ScrBytes *scr_buffer_from_storage(const ScrDyn *storage, const ScrDyn *offset_value) { +/* Buffer.from(arrayBuffer, byteOffset, length) over a checked ArrayBuffer + * or SharedArrayBuffer, as Node's fromArrayBuffer: a Buffer that shares its + * memory. The offset converts with ToNumber, where a missing offset (NULL) + * or undefined and NaN are 0; an offset past the end throws + * ERR_BUFFER_OUT_OF_BOUNDS, and one at or below -1 V8's start-offset + * RangeError. A missing length runs to the end; a given one converts with + * ToNumber, past the end throws ERR_BUFFER_OUT_OF_BOUNDS for "length", and + * zero, negative and NaN lengths are 0. NULL with the exception pending. */ +static ScrBytes *scr_buffer_from_storage(const ScrDyn *storage, const ScrDyn *offset_value, + const ScrDyn *length_value) { double offset = 0; if (offset_value && offset_value->kind != SCR_DYN_UNDEF) { if (!scr_dyn_number_coerce_js(offset_value, &offset)) return NULL; @@ -1313,11 +1348,26 @@ static ScrBytes *scr_buffer_from_storage(const ScrDyn *storage, const ScrDyn *of } ScrBytes *bytes = storage->v.handle.ptr; ScrBytes *owner = bytes->backing ? bytes->backing : bytes; - if ((double)scr_bytes_byte_len(owner) - offset < 0) { + double max_length = (double)scr_bytes_byte_len(owner) - offset; + if (max_length < 0) { static const char msg[] = "\"offset\" is outside of buffer bounds"; scr_throw_error_msg_code(SCR_ERR_RANGE, msg, sizeof msg - 1, "ERR_BUFFER_OUT_OF_BOUNDS"); return NULL; } + bool has_length = length_value && length_value->kind != SCR_DYN_UNDEF; + double length = max_length; + if (has_length) { + if (!scr_dyn_number_coerce_js(length_value, &length)) return NULL; + if (length > 0) { + if (length > max_length) { + static const char msg[] = "\"length\" is outside of buffer bounds"; + scr_throw_error_msg_code(SCR_ERR_RANGE, msg, sizeof msg - 1, "ERR_BUFFER_OUT_OF_BOUNDS"); + return NULL; + } + } else { + length = 0; + } + } double start = trunc(offset); if (start < 0) { /* The offset as JavaScript prints it (-1.5, -1e+21, -Infinity). */ @@ -1331,7 +1381,7 @@ static ScrBytes *scr_buffer_from_storage(const ScrDyn *storage, const ScrDyn *of scr_throw_error(SCR_ERR_RANGE, message); return NULL; } - ScrBytes *out = scr_bytes_buffer_view(bytes, SCR_BYTES_U8, start, false, 0); + ScrBytes *out = scr_bytes_buffer_view(bytes, SCR_BYTES_U8, start, has_length, trunc(length)); if (out) out->is_buffer = true; return out; } @@ -1340,7 +1390,7 @@ ScrBytes *scr_buffer_from_dyn(const ScrDyn *value, const ScrStr *encoding) { if (value->kind == SCR_DYN_STR) return scr_bytes_from_str(value->v.str, encoding); /* The second argument here is an encoding name, which as a byte offset * converts to NaN: offset 0. */ - if (scr_buffer_storage_is(value)) return scr_buffer_from_storage(value, NULL); + if (scr_buffer_storage_is(value)) return scr_buffer_from_storage(value, NULL, NULL); if (value->kind == SCR_DYN_BYTES) return scr_bytes_convert(SCR_BYTES_U8, value->v.bytes); if (value->kind == SCR_DYN_ARR) { return scr_buffer_from_array_like(value, (double)value->v.arr.len); @@ -1386,7 +1436,7 @@ ScrBytes *scr_buffer_from_dyn_pooled(const ScrDyn *value, const ScrStr *encoding return scr_buffer_from_bytes(value->v.bytes); /* A view of an ArrayBuffer shares its memory, so it never comes from the * pool. The second argument here is an encoding name: offset 0. */ - if (scr_buffer_storage_is(value)) return scr_buffer_from_storage(value, NULL); + if (scr_buffer_storage_is(value)) return scr_buffer_from_storage(value, NULL, NULL); ScrBytes *out = scr_buffer_from_dyn(value, encoding); if (!out) return NULL; out->is_buffer = true; @@ -1413,21 +1463,51 @@ ScrBytes *scr_buffer_from_dyn_ref_pooled(const ScrDyn *value, const ScrStr *enco /* Buffer.from(value, second) over checked values whose second argument is * not a literal encoding name: an ArrayBuffer or SharedArrayBuffer takes it - * as the byte offset (scr_buffer_from_storage); other values convert as - * scr_buffer_from_dyn_ref_pooled with utf8. */ + * as the byte offset (scr_buffer_from_storage); a string takes it as the + * encoding (Node's fromString: utf8 unless it is a non-empty string, which + * must name a Buffer encoding or throw ERR_UNKNOWN_ENCODING); other values + * convert as scr_buffer_from_dyn_ref_pooled with utf8. */ ScrBytes *scr_buffer_from_dyn_offset_pooled(const ScrDyn *value, const ScrDyn *second) { + if (value->kind == SCR_DYN_STR) { + if (second->kind == SCR_DYN_STR && second->v.str->len > 0) + return scr_buffer_from_str_checked(value->v.str, second->v.str); + ScrStr *utf8 = scr_str_new("utf8", 4); + ScrBytes *out = scr_buffer_from_str(value->v.str, utf8); + scr_str_release(utf8); + return out; + } if (!scr_buffer_storage_is(value)) { ScrStr *utf8 = scr_str_new("utf8", 4); ScrBytes *out = scr_buffer_from_dyn_ref_pooled(value, utf8); scr_str_release(utf8); return out; } - return scr_buffer_from_storage(value, second); + return scr_buffer_from_storage(value, second, NULL); +} + +/* Buffer.from(value, second, third): an ArrayBuffer or SharedArrayBuffer + * takes them as its byte offset and length; other values convert as + * scr_buffer_from_dyn_offset_pooled does, without the third argument. */ +ScrBytes *scr_buffer_from_dyn_range_pooled(const ScrDyn *value, const ScrDyn *second, + const ScrDyn *third) { + if (!scr_buffer_storage_is(value)) return scr_buffer_from_dyn_offset_pooled(value, second); + return scr_buffer_from_storage(value, second, third); +} + +/* The deprecated Buffer constructor's DEP0005 state, Node's + * bufferWarningAlreadyEmitted and nodeModulesCheckCounter: a call from + * node_modules only counts toward the 10000-call check, and + * scr_buffer_deprecation (scr_async_dyn.c) reports for the others. */ +SCR_TL bool scr_buffer_ctor_warned = false; +SCR_TL uint64_t scr_buffer_ctor_checks = 0; + +void scr_buffer_deprecation_count(void) { + if (!scr_buffer_ctor_warned) scr_buffer_ctor_checks++; } /* new Buffer(number, encoding) — the deprecated ctor's string arm with a - * non-string first argument: Node's exact ERR_INVALID_ARG_TYPE (the - * throwing path never fires DEP0005, so compiled silence matches). */ + * non-string first argument: Node's exact ERR_INVALID_ARG_TYPE, thrown + * after scr_buffer_deprecation has reported DEP0005. */ ScrBytes *scr_buffer_new_string_fail(const ScrDyn *got) { scr_dyn_arg_type_fail("string", "of type string", got); return NULL; @@ -1825,68 +1905,136 @@ static bool scr_fs_int_range_chk(const ScrDyn *v, const char *name, double min, return true; } -static bool scr_fs_read_validate(const ScrDyn *fd, const ScrDyn *buffer, const ScrDyn *offset, - const ScrDyn *length, const ScrDyn *position) { - if (buffer->kind != SCR_DYN_BYTES) { - scr_dyn_arg_type_fail("buffer", "an instance of Buffer, TypedArray, or DataView", buffer); +/* `length |= 0`: ToInt32 of the length argument (a non-numeric value is + * 0). False with the exception pending when ToNumber throws. */ +static bool scr_fs_length_int32(const ScrDyn *length, double *out) { + double n; + if (length->kind == SCR_DYN_NUM) { + n = length->v.num; + } else if (!scr_dyn_number_coerce_js(length, &n)) { return false; } + if (!isfinite(n)) { + *out = 0; + return true; + } + double m = fmod(trunc(n), 4294967296.0); + if (m < 0) m += 4294967296.0; + *out = m >= 2147483648.0 ? m - 4294967296.0 : m; + return true; +} + +/* A bigint read position in [-1n, 2n ** 63n - 1n - BigInt(length)] + * (validatePosition); false with ERR_OUT_OF_RANGE pending otherwise. The + * received value groups the digits past 2^32 in threes, as Node's + * ERR_OUT_OF_RANGE does for bigints. */ +static bool scr_fs_bigint_position_chk(ScrBigInt *position, double length) { + ScrBigInt *top = scr_bigint_from_i64(INT64_MAX); + ScrBigInt *span = scr_bigint_from_f64(length); + ScrBigInt *max = scr_bigint_sub(top, span); + scr_bigint_release(top); + scr_bigint_release(span); + ScrBigInt *low = scr_bigint_from_i64(-1); + bool ok = scr_bigint_cmp_f64(position, low) >= 0 && scr_bigint_cmp_f64(position, max) <= 0; + scr_bigint_release(low); + if (ok) { + scr_bigint_release(max); + return true; + } + ScrStr *limit = scr_bigint_to_string(max, 10); + scr_bigint_release(max); + ScrStr *decimal = scr_bigint_to_string(position, 10); + if (!limit || !decimal) { + if (limit) scr_str_release(limit); + if (decimal) scr_str_release(decimal); + return false; + } + ScrBigInt *bound = scr_bigint_from_i64((int64_t)1 << 32); + ScrBigInt *negBound = scr_bigint_from_i64(-((int64_t)1 << 32)); + bool group = scr_bigint_cmp_f64(position, bound) > 0 || scr_bigint_cmp_f64(position, negBound) < 0; + scr_bigint_release(bound); + scr_bigint_release(negBound); + /* addNumericalSeparator: underscores every three digits from the right, + * after an optional sign. */ + char digits[200]; + size_t len = decimal->len < 120 ? decimal->len : 120, out = 0; + size_t start = len > 0 && decimal->data[0] == '-' ? 1 : 0; + for (size_t i = 0; i < len; i++) { + if (group && i > start && (len - i) % 3 == 0) digits[out++] = '_'; + digits[out++] = (char)decimal->data[i]; + } + digits[out] = '\0'; + char msg[384]; + int n = snprintf(msg, sizeof msg, + "The value of \"position\" is out of range. It must be >= -1 && <= %.*s. Received %sn", + (int)limit->len, (const char *)limit->data, digits); + scr_str_release(limit); + scr_str_release(decimal); + scr_throw_error_msg_code(SCR_ERR_RANGE, msg, n < 0 ? 0 : (size_t)n, "ERR_OUT_OF_RANGE"); + return false; +} + + +/* fs.read(fd, buffer, offset, length, position, cb)'s argument checks, in + * Node's order: the fd, the buffer, the offset (an integer >= 0), the + * length as `length |= 0`, the position (validatePosition, against that + * length), then for a non-empty read the empty-buffer and + * validateOffsetLengthRead checks. The converted length goes to + * *length_out when it is not NULL, so the read does not convert again. */ +static bool scr_fs_read_validate(const ScrDyn *fd, const ScrDyn *buffer, const ScrDyn *offset, + const ScrDyn *length, const ScrDyn *position, double *length_out) { if (fd->kind != SCR_DYN_NUM) { scr_dyn_arg_type_fail("fd", "of type number", fd); return false; } + if (buffer->kind != SCR_DYN_BYTES) { + scr_dyn_arg_type_fail("buffer", "an instance of Buffer, TypedArray, or DataView", buffer); + return false; + } double buflen = scr_bytes_byte_len(buffer->v.bytes); + double off = 0; if (!scr_fs_dyn_absent(offset)) { - /* validateInteger's MAX_SAFE range first, the buffer bound second — - * Node renders each with its own max. */ if (!scr_fs_int_range_chk(offset, "offset", 0, 9007199254740991.0, ">= 0 && <= 9007199254740991")) { return false; } - if (offset->v.num > buflen) { - char range[64]; - snprintf(range, sizeof range, ">= 0 && <= %.0f", buflen); - if (!scr_fs_int_range_chk(offset, "offset", 0, buflen, range)) return false; - } + off = offset->v.num; } + double count = 0; + if (!scr_fs_length_int32(length, &count)) return false; + if (length_out) *length_out = count; if (!scr_fs_dyn_absent(position)) { - if (position->kind != SCR_DYN_NUM) { + if (position->kind == SCR_DYN_BIGINT) { + if (!scr_fs_bigint_position_chk(position->v.bigint, count)) return false; + } else if (position->kind != SCR_DYN_NUM) { scr_dyn_arg_type_fail("position", "of type bigint or integer", position); return false; - } - if (!scr_fs_int_range_chk(position, "position", -1, 9007199254740991.0, - ">= -1 && <= 9007199254740991")) { + } else if (!scr_fs_int_range_chk(position, "position", -1, 9007199254740991.0, + ">= -1 && <= 9007199254740991")) { return false; } } - double off = offset->kind == SCR_DYN_NUM ? offset->v.num : 0; - if (!scr_fs_dyn_absent(length)) { - if (length->kind != SCR_DYN_NUM || !(isfinite(length->v.num) && trunc(length->v.num) == length->v.num) || - length->v.num < 0) { - /* Node renders the bare ">= 0" form here (checkPosition's cousin). */ - if (length->kind == SCR_DYN_NUM) { - char recv[48], msg[160]; - scr_num_received(length->v.num, recv); - const char *shape = (isfinite(length->v.num) && trunc(length->v.num) == length->v.num) - ? "It must be >= 0." - : "It must be an integer."; - int len = snprintf(msg, sizeof msg, - "The value of \"length\" is out of range. %s Received %s", shape, recv); - scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE"); - return false; - } - scr_dyn_arg_type_fail("length", "of type number", length); - return false; - } - if (length->v.num > buflen - off) { - char recv[48], msg[160]; - scr_num_received(length->v.num, recv); - int len = snprintf(msg, sizeof msg, - "The value of \"length\" is out of range. It must be <= %.0f. Received %s", - buflen - off, recv); - scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE"); - return false; + if (count == 0) return true; + if (buflen == 0) { + if (scr_bytes_is_buffer(buffer->v.bytes)) { + static const char empty[] = + "The argument 'buffer' is empty and cannot be written. Received "; + scr_throw_error_msg_code(SCR_ERR_TYPE, empty, sizeof empty - 1, "ERR_INVALID_ARG_VALUE"); + } else { + scr_dyn_arg_value_fail("buffer", "is empty and cannot be written", buffer); } + return false; + } + if (count < 0 || off + count > buflen) { + char recv[48], range[64], msg[192]; + scr_num_received(count, recv); + if (count < 0) snprintf(range, sizeof range, ">= 0"); + else snprintf(range, sizeof range, "<= %.0f", buflen - off); + int len = snprintf(msg, sizeof msg, + "The value of \"length\" is out of range. It must be %s. Received %s", range, + recv); + scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE"); + return false; } return true; } @@ -1894,7 +2042,7 @@ static bool scr_fs_read_validate(const ScrDyn *fd, const ScrDyn *buffer, const S void scr_fs_read_chk(const ScrDyn *fd, const ScrDyn *buffer, const ScrDyn *offset, const ScrDyn *length, const ScrDyn *position, const ScrStr *fence) { - if (scr_fs_read_validate(fd, buffer, offset, length, position)) scr_throw_lowering_fence(fence); + if (scr_fs_read_validate(fd, buffer, offset, length, position, NULL)) scr_throw_lowering_fence(fence); } /* createReadStream/createWriteStream(path, options?): getOptions' @@ -1939,3 +2087,68 @@ bool scr_fs_stream_mode(const ScrDyn *value, double *mode) { } return scr_fs_cb_file_mode(value, "mode", mode); } + +/* fs.readSync(fd, buffer, offset, length[, position]) with arguments the + * compiler could not type (JavaScript): Node's checks (validateBuffer + * first, then the fd, the offset, `length |= 0` and the position, which + * may be a bigint), then the typed read. */ +double scr_fs_read_sync_dyn(const ScrDyn *fd, const ScrDyn *buffer, const ScrDyn *offset, + const ScrDyn *length, const ScrDyn *position) { + if (buffer->kind != SCR_DYN_BYTES) { + scr_dyn_arg_type_fail("buffer", "an instance of Buffer, TypedArray, or DataView", buffer); + return 0; + } + double count = 0; + if (!scr_fs_read_validate(fd, buffer, offset, length, position, &count)) return 0; + if (count == 0) return 0; + double at = scr_fs_dyn_absent(position) ? -1 + : position->kind == SCR_DYN_BIGINT ? scr_bigint_to_f64(position->v.bigint) + : position->v.num; + ScrBytes *bytes = scr_dyn_bytes_unbox(buffer); + double read = scr_fs_read_sync(fd->v.num, bytes, scr_fs_dyn_absent(offset) ? 0 : offset->v.num, + count, at); + scr_bytes_release(bytes); + return read; +} + +/* ERR_OUT_OF_RANGE for the number `n` of argument `name`. Always false. */ +static bool scr_fs_num_range_fail(const char *name, const char *range, double n) { + char recv[48], msg[192]; + scr_num_received(n, recv); + int len = snprintf(msg, sizeof msg, "The value of \"%s\" is out of range. It must be %s. Received %s", + name, range, recv); + scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE"); + return false; +} + +/* fs.write(fd, buffer, offset, length, position, cb)'s buffer checks: the + * offset (an integer >= 0 unless omitted, null or the callback), a + * non-number length defaulting to the rest of the buffer, then + * validateOffsetLengthWrite. An options object in the offset slot is read + * by the write itself. */ +static bool scr_fs_write_validate(const ScrDyn *buffer, const ScrDyn *offset, const ScrDyn *length) { + if (offset->kind == SCR_DYN_OBJ || offset->kind == SCR_DYN_TYPED_REF || offset->kind == SCR_DYN_PROXY || + offset->kind == SCR_DYN_NULL) + return true; + double buflen = scr_bytes_byte_len(buffer->v.bytes); + double off = 0; + if (!scr_fs_dyn_absent(offset) && offset->kind != SCR_DYN_FUNC) { + if (!scr_fs_int_range_chk(offset, "offset", 0, 9007199254740991.0, ">= 0 && <= 9007199254740991")) + return false; + off = offset->v.num; + } + double len = length->kind == SCR_DYN_NUM ? length->v.num : buflen - off; + char range[64]; + if (off > buflen) { + snprintf(range, sizeof range, "<= %.0f", buflen); + return scr_fs_num_range_fail("offset", range, off); + } + if (len > buflen - off) { + snprintf(range, sizeof range, "<= %.0f", buflen - off); + return scr_fs_num_range_fail("length", range, len); + } + if (len < 0) return scr_fs_num_range_fail("length", ">= 0", len); + if (!(isfinite(len) && trunc(len) == len)) return scr_fs_num_range_fail("length", "an integer", len); + if (len > 2147483647.0) return scr_fs_num_range_fail("length", ">= 0 && <= 2147483647", len); + return true; +} diff --git a/packages/runtime/src/scr_child.c b/packages/runtime/src/scr_child.c index a70b10894..d4f9ca395 100644 --- a/packages/runtime/src/scr_child.c +++ b/packages/runtime/src/scr_child.c @@ -1411,6 +1411,10 @@ struct ScrChildStream { size_t n_data, cap_data; ScrChildStreamEndL *end_ls; size_t n_end, cap_end; + /* The AsyncLocalStorage context of the spawn() call that created the + * stream (+1, NULL outside a store): its events run in it, as Node's + * pipe handles run theirs. */ + ScrAlsCtx *als; struct ScrChildStream *next; /* the service registry (+1) */ }; @@ -1439,6 +1443,11 @@ struct ScrChildWriter { size_t n_finish, cap_finish; ScrChildErrEntry *err_ls; size_t n_write_err, cap_write_err; + /* AsyncLocalStorage contexts (+1 each, NULL outside a store): the + * latest write() call's, for 'drain' and 'error' (the spawn() call's + * until the first write), and the end() call's, for 'finish'. */ + ScrAlsCtx *als; + ScrAlsCtx *end_als; struct ScrChildWriter *next; /* the service registry (+1) */ }; @@ -1485,6 +1494,9 @@ struct ScrChild { ScrChildWriter *in_stream; ScrDyn *out_dyn, *err_dyn, *in_dyn; ScrIpc *ipc; + /* The AsyncLocalStorage context of the spawn() call (+1, NULL outside + * a store): 'spawn', 'error', 'exit' and 'close' run in it. */ + ScrAlsCtx *als; struct ScrChild *next; /* the pending registry */ }; @@ -1543,6 +1555,7 @@ void scr_child_release(ScrChild *c) { scr_child_stream_release(c->err_stream); scr_child_writer_release(c->in_stream); scr_ipc_release(c->ipc); + scr_als_ctx_release(c->als); if (c->proc != NULL) CloseHandle(c->proc); free(c); } @@ -1575,6 +1588,7 @@ void scr_child_stream_release(ScrChildStream *s) { if (s->h != NULL) CloseHandle(s->h); scr_str_release(s->encoding); free(s->capture_data); + scr_als_ctx_release(s->als); free(s); } } @@ -1594,6 +1608,7 @@ static ScrChildStream *scr_child_stream_new(HANDLE h) { ScrChildStream *s = calloc(1, sizeof *s); if (!s) scr_child_oom(); s->rc = 1; + s->als = scr_als_capture(); s->h = h; if (h == NULL) { s->eof = true; @@ -1640,8 +1655,16 @@ static ScrStr *scr_child_stream_capture_value(ScrChildStream *s) { : scr_str_new((const char *)s->capture_data, s->capture_len); } -static void scr_child_stream_emit(ScrChildStream *s, ScrBytes *bytes, - ScrStr *text) { +static void scr_child_stream_emit_in(ScrChildStream *s, ScrBytes *bytes, ScrStr *text); + +/* 'data' runs in the spawn() call's context. */ +static void scr_child_stream_emit(ScrChildStream *s, ScrBytes *bytes, ScrStr *text) { + ScrAlsCtx *prev = scr_als_dispatch_enter(s->als); + scr_child_stream_emit_in(s, bytes, text); + scr_als_dispatch_leave(prev); +} + +static void scr_child_stream_emit_in(ScrChildStream *s, ScrBytes *bytes, ScrStr *text) { size_t nd = s->n_data; if (nd == 0) return; ScrChildStreamDataL *snap = malloc(nd * sizeof *snap); @@ -1683,7 +1706,16 @@ static void scr_child_stream_emit(ScrChildStream *s, ScrBytes *bytes, free(snap); } +static void scr_child_stream_finish_in(ScrChildStream *s, bool fire_end); + +/* A stream's 'end' runs in the spawn() call's context. */ static void scr_child_stream_finish(ScrChildStream *s, bool fire_end) { + ScrAlsCtx *prev = scr_als_dispatch_enter(s->als); + scr_child_stream_finish_in(s, fire_end); + scr_als_dispatch_leave(prev); +} + +static void scr_child_stream_finish_in(ScrChildStream *s, bool fire_end) { if (fire_end && s->encoding != NULL && s->n_data > 0) { ScrStr *tail = scr_strdec_end(s->encoding, s->dec_pending); s->dec_pending = 0; @@ -1914,6 +1946,8 @@ void scr_child_writer_release(ScrChildWriter *w) { free(w->buf); scr_str_release(w->err_msg); scr_child_writer_drop_listeners(w); + scr_als_ctx_release(w->als); + scr_als_ctx_release(w->end_als); free(w); } } @@ -1929,6 +1963,7 @@ static ScrChildWriter *scr_child_writer_new(HANDLE h) { ScrChildWriter *w = calloc(1, sizeof *w); if (!w) scr_child_oom(); w->rc = 1; + w->als = scr_als_capture(); w->h = h; w->writable = h != NULL; w->destroyed = h == NULL; @@ -2075,6 +2110,8 @@ static void scr_child_writer_pump(ScrChildWriter *w) { static bool scr_child_writer_write(ScrChildWriter *w, const void *data, size_t len) { + scr_als_ctx_release(w->als); + w->als = scr_als_capture(); if (!w->writable) { scr_child_writer_fail(w, "write after end", "ERR_STREAM_WRITE_AFTER_END"); return false; @@ -2105,6 +2142,8 @@ bool scr_child_writer_write_bytes(ScrChildWriter *w, const ScrBytes *data) { void scr_child_writer_end(ScrChildWriter *w) { if (w->ending || w->finish_pending || w->finished || w->destroyed) return; + scr_als_ctx_release(w->end_als); + w->end_als = scr_als_capture(); w->writable = false; w->ending = true; scr_child_writer_pump(w); @@ -2172,7 +2211,16 @@ void scr_child_writer_on_error(ScrChildWriter *w, ScrClosure *cb /*moves*/, w->err_ls[w->n_write_err++] = (ScrChildErrEntry){cb, fn}; } +static void scr_child_writer_fire_drain_in(ScrChildWriter *w); + +/* 'drain' runs in the latest write() call's context. */ static void scr_child_writer_fire_drain(ScrChildWriter *w) { + ScrAlsCtx *prev = scr_als_dispatch_enter(w->als); + scr_child_writer_fire_drain_in(w); + scr_als_dispatch_leave(prev); +} + +static void scr_child_writer_fire_drain_in(ScrChildWriter *w) { w->drain_pending = false; size_t n = w->n_drain; if (n == 0) return; @@ -2199,7 +2247,16 @@ static void scr_child_writer_fire_drain(ScrChildWriter *w) { free(snap); } +static void scr_child_writer_fire_finish_in(ScrChildWriter *w); + +/* 'finish' runs in the end() call's context. */ static void scr_child_writer_fire_finish(ScrChildWriter *w) { + ScrAlsCtx *prev = scr_als_dispatch_enter(w->end_als); + scr_child_writer_fire_finish_in(w); + scr_als_dispatch_leave(prev); +} + +static void scr_child_writer_fire_finish_in(ScrChildWriter *w) { w->finish_pending = false; w->finished = true; w->destroyed = true; @@ -2218,7 +2275,16 @@ static void scr_child_writer_fire_finish(ScrChildWriter *w) { scr_child_writer_drop_zero_listeners(w); } +static void scr_child_writer_fire_error_in(ScrChildWriter *w); + +/* 'error' runs in the latest write() call's context. */ static void scr_child_writer_fire_error(ScrChildWriter *w) { + ScrAlsCtx *prev = scr_als_dispatch_enter(w->als); + scr_child_writer_fire_error_in(w); + scr_als_dispatch_leave(prev); +} + +static void scr_child_writer_fire_error_in(ScrChildWriter *w) { w->error_pending = false; if (w->n_write_err == 0) { fflush(stdout); @@ -2312,6 +2378,7 @@ ScrChild *scr_spawn_opts(ScrStr *cmd, ScrArr *args, double in_mode, ScrChild *c = calloc(1, sizeof(ScrChild)); if (!c) scr_child_oom(); c->rc = 1; + c->als = scr_als_capture(); /* Piped slots: the pipe NOW; a pipe failure degrades the slot to NUL * and the stream husk answers eof (fd exhaustion — nothing real). */ @@ -2710,7 +2777,16 @@ static bool scr_child_close_ready(const ScrChild *c) { scr_ipc_close_ready(c->ipc); } +static void scr_child_fire_close_in(ScrChild *c); + +/* 'close' runs in the spawn() call's context. */ static void scr_child_fire_close(ScrChild *c) { + ScrAlsCtx *prev = scr_als_dispatch_enter(c->als); + scr_child_fire_close_in(c); + scr_als_dispatch_leave(prev); +} + +static void scr_child_fire_close_in(ScrChild *c) { c->close_done = true; bool has_code = c->state == SCR_CHILD_SPAWN_FAILED ? true : c->has_code; double code = c->state == SCR_CHILD_SPAWN_FAILED @@ -2774,7 +2850,16 @@ bool scr_children_wait(double max_wait_ms) { /* Fires one settled child's terminal event — the POSIX arm's settle, * verbatim (streams drain first so 'end' precedes 'exit'; spawn * failures fire 'error' or die unhandled). */ +static void scr_child_settle_in(ScrChild *c); + +/* A child's terminal events run in its spawn() call's context. */ static void scr_child_settle(ScrChild *c) { + ScrAlsCtx *prev = scr_als_dispatch_enter(c->als); + scr_child_settle_in(c); + scr_als_dispatch_leave(prev); +} + +static void scr_child_settle_in(ScrChild *c) { if (c->reffed) { c->reffed = false; scr_children_reffed_n--; @@ -3884,6 +3969,10 @@ struct ScrChildStream { size_t n_data, cap_data; ScrChildStreamEndL *end_ls; size_t n_end, cap_end; + /* The AsyncLocalStorage context of the spawn() call that created the + * stream (+1, NULL outside a store): its events run in it, as Node's + * pipe handles run theirs. */ + ScrAlsCtx *als; struct ScrChildStream *next; /* the service registry (+1) */ }; @@ -3908,6 +3997,11 @@ struct ScrChildWriter { size_t n_finish, cap_finish; ScrChildErrEntry *err_ls; size_t n_write_err, cap_write_err; + /* AsyncLocalStorage contexts (+1 each, NULL outside a store): the + * latest write() call's, for 'drain' and 'error' (the spawn() call's + * until the first write), and the end() call's, for 'finish'. */ + ScrAlsCtx *als; + ScrAlsCtx *end_als; struct ScrChildWriter *next; /* the service registry (+1) */ }; @@ -3958,6 +4052,9 @@ struct ScrChild { ScrChildWriter *in_stream; ScrDyn *out_dyn, *err_dyn, *in_dyn; ScrIpc *ipc; + /* The AsyncLocalStorage context of the spawn() call (+1, NULL outside + * a store): 'spawn', 'error', 'exit' and 'close' run in it. */ + ScrAlsCtx *als; struct ScrChild *next; /* the pending registry */ }; @@ -4154,6 +4251,7 @@ void scr_child_release(ScrChild *c) { scr_child_stream_release(c->err_stream); scr_child_writer_release(c->in_stream); scr_ipc_release(c->ipc); + scr_als_ctx_release(c->als); free(c); } } @@ -4206,6 +4304,7 @@ void scr_child_stream_release(ScrChildStream *s) { if (s->fd >= 0) close(s->fd); scr_str_release(s->encoding); free(s->capture_data); + scr_als_ctx_release(s->als); free(s); } } @@ -4228,6 +4327,7 @@ static ScrChildStream *scr_child_stream_new(int fd) { ScrChildStream *s = calloc(1, sizeof *s); if (!s) scr_child_oom(); s->rc = 1; + s->als = scr_als_capture(); s->fd = fd; if (fd < 0) { s->eof = true; @@ -4282,8 +4382,16 @@ static ScrStr *scr_child_stream_capture_value(ScrChildStream *s) { : scr_str_new((const char *)s->capture_data, s->capture_len); } -static void scr_child_stream_emit_posix(ScrChildStream *s, ScrBytes *bytes, - ScrStr *text) { +static void scr_child_stream_emit_posix_in(ScrChildStream *s, ScrBytes *bytes, ScrStr *text); + +/* 'data' runs in the spawn() call's context. */ +static void scr_child_stream_emit_posix(ScrChildStream *s, ScrBytes *bytes, ScrStr *text) { + ScrAlsCtx *prev = scr_als_dispatch_enter(s->als); + scr_child_stream_emit_posix_in(s, bytes, text); + scr_als_dispatch_leave(prev); +} + +static void scr_child_stream_emit_posix_in(ScrChildStream *s, ScrBytes *bytes, ScrStr *text) { size_t nd = s->n_data; if (nd == 0) return; ScrChildStreamDataL *snap = malloc(nd * sizeof *snap); @@ -4333,7 +4441,16 @@ static void scr_child_stream_emit_posix(ScrChildStream *s, ScrBytes *bytes, /* EOF/error tail: 'end' fires over a snapshot, everything drops, the fd * closes (deleting its kqueue filter), and the liveness/fallback counters * settle. The caller unlinks from the registry and releases its ref. */ +static void scr_child_stream_finish_in(ScrChildStream *s, bool fire_end); + +/* A stream's 'end' runs in the spawn() call's context. */ static void scr_child_stream_finish(ScrChildStream *s, bool fire_end) { + ScrAlsCtx *prev = scr_als_dispatch_enter(s->als); + scr_child_stream_finish_in(s, fire_end); + scr_als_dispatch_leave(prev); +} + +static void scr_child_stream_finish_in(ScrChildStream *s, bool fire_end) { if (fire_end && s->encoding != NULL && s->n_data > 0) { ScrStr *tail = scr_strdec_end(s->encoding, s->dec_pending); s->dec_pending = 0; @@ -4641,6 +4758,8 @@ void scr_child_writer_release(ScrChildWriter *w) { free(w->buf); scr_str_release(w->err_msg); scr_child_writer_drop_listeners(w); + scr_als_ctx_release(w->als); + scr_als_ctx_release(w->end_als); free(w); } } @@ -4656,6 +4775,7 @@ static ScrChildWriter *scr_child_writer_new(int fd) { ScrChildWriter *w = calloc(1, sizeof *w); if (!w) scr_child_oom(); w->rc = 1; + w->als = scr_als_capture(); w->fd = fd; w->writable = fd >= 0; w->destroyed = fd < 0; @@ -4789,6 +4909,8 @@ static void scr_child_writer_pump(ScrChildWriter *w) { static bool scr_child_writer_write(ScrChildWriter *w, const void *data, size_t len) { + scr_als_ctx_release(w->als); + w->als = scr_als_capture(); if (!w->writable) { scr_child_writer_fail(w, "write after end", "ERR_STREAM_WRITE_AFTER_END"); return false; @@ -4825,6 +4947,8 @@ bool scr_child_writer_write_bytes(ScrChildWriter *w, const ScrBytes *data) { void scr_child_writer_end(ScrChildWriter *w) { if (w->ending || w->finish_pending || w->finished || w->destroyed) return; + scr_als_ctx_release(w->end_als); + w->end_als = scr_als_capture(); w->writable = false; w->ending = true; scr_child_writer_pump(w); @@ -4892,7 +5016,16 @@ void scr_child_writer_on_error(ScrChildWriter *w, ScrClosure *cb /*moves*/, w->err_ls[w->n_write_err++] = (ScrChildErrEntry){cb, fn}; } +static void scr_child_writer_fire_drain_in(ScrChildWriter *w); + +/* 'drain' runs in the latest write() call's context. */ static void scr_child_writer_fire_drain(ScrChildWriter *w) { + ScrAlsCtx *prev = scr_als_dispatch_enter(w->als); + scr_child_writer_fire_drain_in(w); + scr_als_dispatch_leave(prev); +} + +static void scr_child_writer_fire_drain_in(ScrChildWriter *w) { w->drain_pending = false; size_t n = w->n_drain; if (n == 0) return; @@ -4919,7 +5052,16 @@ static void scr_child_writer_fire_drain(ScrChildWriter *w) { free(snap); } +static void scr_child_writer_fire_finish_in(ScrChildWriter *w); + +/* 'finish' runs in the end() call's context. */ static void scr_child_writer_fire_finish(ScrChildWriter *w) { + ScrAlsCtx *prev = scr_als_dispatch_enter(w->end_als); + scr_child_writer_fire_finish_in(w); + scr_als_dispatch_leave(prev); +} + +static void scr_child_writer_fire_finish_in(ScrChildWriter *w) { w->finish_pending = false; w->finished = true; w->destroyed = true; @@ -4938,7 +5080,16 @@ static void scr_child_writer_fire_finish(ScrChildWriter *w) { scr_child_writer_drop_zero_listeners(w); } +static void scr_child_writer_fire_error_in(ScrChildWriter *w); + +/* 'error' runs in the latest write() call's context. */ static void scr_child_writer_fire_error(ScrChildWriter *w) { + ScrAlsCtx *prev = scr_als_dispatch_enter(w->als); + scr_child_writer_fire_error_in(w); + scr_als_dispatch_leave(prev); +} + +static void scr_child_writer_fire_error_in(ScrChildWriter *w) { w->error_pending = false; if (w->n_write_err == 0) { fflush(stdout); @@ -5042,6 +5193,7 @@ ScrChild *scr_spawn_opts(ScrStr *cmd, ScrArr *args, double in_mode, ScrChild *c = calloc(1, sizeof(ScrChild)); if (!c) scr_child_oom(); c->rc = 1; + c->als = scr_als_capture(); /* Piped slots: pipe(2) NOW, both ends cloexec (the file action's dup2 * clears the flag on the child's copy — every other descriptor closes @@ -5558,7 +5710,16 @@ static bool scr_child_close_ready(const ScrChild *c) { scr_ipc_close_ready(c->ipc); } +static void scr_child_fire_close_in(ScrChild *c); + +/* 'close' runs in the spawn() call's context. */ static void scr_child_fire_close(ScrChild *c) { + ScrAlsCtx *prev = scr_als_dispatch_enter(c->als); + scr_child_fire_close_in(c); + scr_als_dispatch_leave(prev); +} + +static void scr_child_fire_close_in(ScrChild *c) { c->close_done = true; bool has_code = c->state == SCR_CHILD_SPAWN_FAILED ? true : c->has_code; double code = c->state == SCR_CHILD_SPAWN_FAILED @@ -5596,7 +5757,16 @@ static void scr_children_closing_poll(void) { * ordering runs first: consumer-owning piped streams drain to EOF/EAGAIN * so their 'end' events fire BEFORE 'exit' (Node-exact — verified: data*, * end, then exit for every output shape). */ +static void scr_child_settle_in(ScrChild *c); + +/* A child's terminal events run in its spawn() call's context. */ static void scr_child_settle(ScrChild *c) { + ScrAlsCtx *prev = scr_als_dispatch_enter(c->als); + scr_child_settle_in(c); + scr_als_dispatch_leave(prev); +} + +static void scr_child_settle_in(ScrChild *c) { if (c->reffed) { c->reffed = false; scr_children_reffed_n--; @@ -5767,7 +5937,16 @@ void scr_child_on_spawn(ScrChild *c, ScrClosure *cb) { /* Success is observable on the next loop pass, before any data or exit * event. Detaching the snapshot keeps listeners registered by a callback * from participating in an event that has already started. */ +static void scr_child_fire_spawn_in(ScrChild *c); + +/* 'spawn' runs in the spawn() call's context. */ static void scr_child_fire_spawn(ScrChild *c) { + ScrAlsCtx *prev = scr_als_dispatch_enter(c->als); + scr_child_fire_spawn_in(c); + scr_als_dispatch_leave(prev); +} + +static void scr_child_fire_spawn_in(ScrChild *c) { c->spawn_done = true; ScrClosure **callbacks = c->spawn_cbs; size_t count = c->n_spawn; @@ -5973,6 +6152,7 @@ typedef struct { typedef struct { ScrClosure *cb; ScrIpcSendFn fn; + ScrAlsCtx *ctx; /* the send() call's context (+1): the callback runs in it */ } ScrIpcSendEntry; struct ScrIpc { @@ -6000,6 +6180,9 @@ struct ScrIpc { ScrIpcSendEntry *send_cbs; size_t n_send, cap_send; ScrStr *send_error; + /* The context the channel was created in (+1): 'message' and + * 'disconnect' run in it. */ + ScrAlsCtx *als; struct ScrIpc *next; }; @@ -6035,8 +6218,12 @@ static void scr_ipc_release(ScrIpc *ipc) { for (size_t i = 0; i < ipc->n_pending; i++) scr_dyn_release(ipc->pending[i]); for (size_t i = 0; i < ipc->n_message; i++) scr_closure_release(ipc->message_ls[i].cb); for (size_t i = 0; i < ipc->n_disconnect; i++) scr_closure_release(ipc->disconnect_ls[i].cb); - for (size_t i = 0; i < ipc->n_send; i++) scr_closure_release(ipc->send_cbs[i].cb); + for (size_t i = 0; i < ipc->n_send; i++) { + scr_closure_release(ipc->send_cbs[i].cb); + scr_als_ctx_release(ipc->send_cbs[i].ctx); + } scr_str_release(ipc->send_error); + scr_als_ctx_release(ipc->als); free(ipc->buffer); free(ipc->pending); free(ipc->message_ls); @@ -6156,6 +6343,7 @@ static ScrIpc *scr_ipc_new(ScrChildStream *reader, ScrChildWriter *writer) { ScrIpc *ipc = calloc(1, sizeof(*ipc)); if (!ipc) scr_trap("out of memory"); ipc->rc = 1; + ipc->als = scr_als_capture(); ipc->connected = true; ipc->reader = reader; ipc->writer = writer; @@ -6186,7 +6374,7 @@ static void scr_ipc_add_send(ScrIpc *ipc, ScrClosure *cb, ScrIpcSendFn fn) { ipc->send_cbs = realloc(ipc->send_cbs, ipc->cap_send * sizeof(*ipc->send_cbs)); if (!ipc->send_cbs) scr_trap("out of memory"); } - ipc->send_cbs[ipc->n_send++] = (ScrIpcSendEntry){cb, fn}; + ipc->send_cbs[ipc->n_send++] = (ScrIpcSendEntry){cb, fn, scr_als_capture()}; } static bool scr_ipc_send(ScrIpc *ipc, ScrStr *json, ScrClosure *cb, @@ -6270,7 +6458,16 @@ static void scr_ipc_remove_message_once(ScrIpc *ipc, ScrClosure *cb) { } } +static void scr_ipc_fire_messages_in(ScrIpc *ipc); + +/* 'message' runs in the channel's creation context. */ static void scr_ipc_fire_messages(ScrIpc *ipc) { + ScrAlsCtx *prev = scr_als_dispatch_enter(ipc->als); + scr_ipc_fire_messages_in(ipc); + scr_als_dispatch_leave(prev); +} + +static void scr_ipc_fire_messages_in(ScrIpc *ipc) { while (ipc->n_pending > 0 && ipc->n_message > 0 && !scr_exc_pending()) { ScrDyn *message = ipc->pending[0]; memmove(ipc->pending, ipc->pending + 1, @@ -6307,16 +6504,28 @@ static void scr_ipc_fire_send_callbacks(ScrIpc *ipc) { error = scr_error_new(0, ipc->send_error); scr_error_set_code(error, "ERR_IPC_CHANNEL_CLOSED"); } + ScrAlsCtx *prev = scr_als_dispatch_enter(snapshot[i].ctx); if (!scr_exc_pending()) snapshot[i].fn(snapshot[i].cb, error); else scr_error_release(error); + scr_als_dispatch_leave(prev); scr_closure_release(snapshot[i].cb); + scr_als_ctx_release(snapshot[i].ctx); } free(snapshot); scr_str_release(ipc->send_error); ipc->send_error = NULL; } +static void scr_ipc_fire_disconnect_in(ScrIpc *ipc); + +/* 'disconnect' runs in the channel's creation context. */ static void scr_ipc_fire_disconnect(ScrIpc *ipc) { + ScrAlsCtx *prev = scr_als_dispatch_enter(ipc->als); + scr_ipc_fire_disconnect_in(ipc); + scr_als_dispatch_leave(prev); +} + +static void scr_ipc_fire_disconnect_in(ScrIpc *ipc) { if (!ipc->disconnect_pending || ipc->disconnect_emitted) return; ipc->disconnect_pending = false; ipc->disconnect_emitted = true; @@ -6460,6 +6669,9 @@ double scr_process_fork_target(double target_count) { return target; } +/* The child was created with an IPC channel; it keeps it until it is freed. */ +bool scr_child_ipc_present(ScrChild *c) { return c->ipc != NULL; } + bool scr_child_ipc_connected(ScrChild *c) { return c->ipc != NULL && c->ipc->connected; } diff --git a/packages/runtime/src/scr_child.h b/packages/runtime/src/scr_child.h index a5853ad59..8b0d4d3f1 100644 --- a/packages/runtime/src/scr_child.h +++ b/packages/runtime/src/scr_child.h @@ -176,6 +176,7 @@ void scr_child_release_v(void *p); void scr_child_on_exit(ScrChild *c, ScrClosure *cb /*moves*/, ScrChildExitFn fn); void scr_child_on_close(ScrChild *c, ScrClosure *cb /*moves*/, ScrChildExitFn fn); void scr_child_on_error(ScrChild *c, ScrClosure *cb /*moves*/, ScrChildErrFn fn); +bool scr_child_ipc_present(ScrChild *c); bool scr_child_ipc_connected(ScrChild *c); bool scr_child_ipc_send(ScrChild *c, ScrStr *json); bool scr_child_ipc_send_cb(ScrChild *c, ScrStr *json, diff --git a/packages/runtime/src/scr_cipher.c b/packages/runtime/src/scr_cipher.c index a2db11434..33780fdfc 100644 --- a/packages/runtime/src/scr_cipher.c +++ b/packages/runtime/src/scr_cipher.c @@ -12,6 +12,7 @@ * compared in constant time. A cipher used as a stream reads and writes * through its own native Transform, which the stream unit builds on first * use, as Node's LazyTransform is. All arguments are borrowed. */ +#include "scr_cipher_names.h" #include "scr_runtime.h" #include @@ -114,52 +115,12 @@ static const ScCipherSpec sc_specs[] = { static const struct { const char *name; unsigned iv_len; -} sc_legacy[] = { - {"des", 8}, {"des-cbc", 8}, {"des-ecb", 0}, {"des-cfb", 8}, {"des-cfb1", 8}, {"des-cfb8", 8}, - {"des-ofb", 8}, {"desx", 8}, {"desx-cbc", 8}, {"bf", 8}, {"bf-cbc", 8}, {"bf-ecb", 0}, - {"bf-cfb", 8}, {"bf-ofb", 8}, {"blowfish", 8}, {"cast", 8}, {"cast-cbc", 8}, {"cast5-cbc", 8}, - {"cast5-ecb", 0}, {"cast5-cfb", 8}, {"cast5-ofb", 8}, {"rc2", 8}, {"rc2-cbc", 8}, {"rc2-ecb", 0}, - {"rc2-cfb", 8}, {"rc2-ofb", 8}, {"rc2-40-cbc", 8}, {"rc2-64-cbc", 8}, {"rc4", 0}, {"rc4-40", 0}, - {"rc4-hmac-md5", 0}, {"seed", 16}, {"seed-cbc", 16}, {"seed-ecb", 0}, {"seed-cfb", 16}, - {"seed-ofb", 16}, {"idea", 8}, {"idea-cbc", 8}, {"idea-ecb", 0}, {"idea-cfb", 8}, {"idea-ofb", 8}, -}; +} sc_legacy[] = {SCR_CIPHER_LEGACY_ENTRIES}; /* crypto.getCiphers() on Node 24.22 (OpenSSL 3.5). Names in this list that * have no implementation throw a coded SC2020 error instead of Node's * ERR_CRYPTO_UNKNOWN_CIPHER. */ -static const char *const sc_known[] = { - "aes-128-cbc", "aes-128-cbc-cts", "aes-128-ccm", "aes-128-cfb", "aes-128-cfb1", "aes-128-cfb8", - "aes-128-ctr", "aes-128-ecb", "aes-128-gcm", "aes-128-gcm-siv", "aes-128-ocb", "aes-128-ofb", - "aes-128-siv", "aes-128-wrap", "aes-128-wrap-inv", "aes-128-wrap-pad", "aes-128-wrap-pad-inv", - "aes-128-xts", "aes-192-cbc", "aes-192-cbc-cts", "aes-192-ccm", "aes-192-cfb", "aes-192-cfb1", - "aes-192-cfb8", "aes-192-ctr", "aes-192-ecb", "aes-192-gcm", "aes-192-gcm-siv", "aes-192-ocb", - "aes-192-ofb", "aes-192-siv", "aes-192-wrap", "aes-192-wrap-inv", "aes-192-wrap-pad", - "aes-192-wrap-pad-inv", "aes-256-cbc", "aes-256-cbc-cts", "aes-256-ccm", "aes-256-cfb", - "aes-256-cfb1", "aes-256-cfb8", "aes-256-ctr", "aes-256-ecb", "aes-256-gcm", "aes-256-gcm-siv", - "aes-256-ocb", "aes-256-ofb", "aes-256-siv", "aes-256-wrap", "aes-256-wrap-inv", - "aes-256-wrap-pad", "aes-256-wrap-pad-inv", "aes-256-xts", "aes128", "aes128-wrap", - "aes128-wrap-inv", "aes128-wrap-pad", "aes128-wrap-pad-inv", "aes192", "aes192-wrap", - "aes192-wrap-inv", "aes192-wrap-pad", "aes192-wrap-pad-inv", "aes256", "aes256-wrap", - "aes256-wrap-inv", "aes256-wrap-pad", "aes256-wrap-pad-inv", "aria-128-cbc", "aria-128-ccm", - "aria-128-cfb", "aria-128-cfb1", "aria-128-cfb8", "aria-128-ctr", "aria-128-ecb", "aria-128-gcm", - "aria-128-ofb", "aria-192-cbc", "aria-192-ccm", "aria-192-cfb", "aria-192-cfb1", "aria-192-cfb8", - "aria-192-ctr", "aria-192-ecb", "aria-192-gcm", "aria-192-ofb", "aria-256-cbc", "aria-256-ccm", - "aria-256-cfb", "aria-256-cfb1", "aria-256-cfb8", "aria-256-ctr", "aria-256-ecb", "aria-256-gcm", - "aria-256-ofb", "aria128", "aria192", "aria256", "camellia-128-cbc", "camellia-128-cbc-cts", - "camellia-128-cfb", "camellia-128-cfb1", "camellia-128-cfb8", "camellia-128-ctr", - "camellia-128-ecb", "camellia-128-ofb", "camellia-192-cbc", "camellia-192-cbc-cts", - "camellia-192-cfb", "camellia-192-cfb1", "camellia-192-cfb8", "camellia-192-ctr", - "camellia-192-ecb", "camellia-192-ofb", "camellia-256-cbc", "camellia-256-cbc-cts", - "camellia-256-cfb", "camellia-256-cfb1", "camellia-256-cfb8", "camellia-256-ctr", - "camellia-256-ecb", "camellia-256-ofb", "camellia128", "camellia192", "camellia256", "chacha20", - "chacha20-poly1305", "des-ede", "des-ede-cbc", "des-ede-cfb", "des-ede-ecb", "des-ede-ofb", - "des-ede3", "des-ede3-cbc", "des-ede3-cfb", "des-ede3-cfb1", "des-ede3-cfb8", "des-ede3-ecb", - "des-ede3-ofb", "des3", "des3-wrap", "id-aes128-ccm", "id-aes128-gcm", "id-aes128-wrap", - "id-aes128-wrap-pad", "id-aes192-ccm", "id-aes192-gcm", "id-aes192-wrap", "id-aes192-wrap-pad", - "id-aes256-ccm", "id-aes256-gcm", "id-aes256-wrap", "id-aes256-wrap-pad", - "id-smime-alg-cms3deswrap", "sm4", "sm4-cbc", "sm4-ccm", "sm4-cfb", "sm4-cfb128", "sm4-ctr", - "sm4-ecb", "sm4-gcm", "sm4-ofb", "sm4-ofb128", "sm4-xts", -}; +static const char *const sc_known[] = {SCR_CIPHER_KNOWN_NAMES}; enum { SC_NO_TAG = -1 }; enum { SC_TAG_UNKNOWN, SC_TAG_SET_BY_USER, SC_TAG_COMPUTED }; diff --git a/packages/runtime/src/scr_cipher_names.h b/packages/runtime/src/scr_cipher_names.h new file mode 100644 index 000000000..38b31e06e --- /dev/null +++ b/packages/runtime/src/scr_cipher_names.h @@ -0,0 +1,100 @@ +/* crypto.getCiphers() on Node 24.22 (OpenSSL 3.5), the names a cipher + * option can give. The cipher and pki units each include it: names + * without an implementation there throw a coded SC2020 error instead of + * Node's ERR_CRYPTO_UNKNOWN_CIPHER. */ +#ifndef SCR_CIPHER_NAMES_H +#define SCR_CIPHER_NAMES_H + +#define SCR_CIPHER_KNOWN_NAMES \ + "aes-128-cbc", "aes-128-cbc-cts", "aes-128-ccm", "aes-128-cfb", "aes-128-cfb1", "aes-128-cfb8", \ + "aes-128-ctr", "aes-128-ecb", "aes-128-gcm", "aes-128-gcm-siv", "aes-128-ocb", "aes-128-ofb", \ + "aes-128-siv", "aes-128-wrap", "aes-128-wrap-inv", "aes-128-wrap-pad", "aes-128-wrap-pad-inv", \ + "aes-128-xts", "aes-192-cbc", "aes-192-cbc-cts", "aes-192-ccm", "aes-192-cfb", "aes-192-cfb1", \ + "aes-192-cfb8", "aes-192-ctr", "aes-192-ecb", "aes-192-gcm", "aes-192-gcm-siv", "aes-192-ocb", \ + "aes-192-ofb", "aes-192-siv", "aes-192-wrap", "aes-192-wrap-inv", "aes-192-wrap-pad", \ + "aes-192-wrap-pad-inv", "aes-256-cbc", "aes-256-cbc-cts", "aes-256-ccm", "aes-256-cfb", \ + "aes-256-cfb1", "aes-256-cfb8", "aes-256-ctr", "aes-256-ecb", "aes-256-gcm", "aes-256-gcm-siv", \ + "aes-256-ocb", "aes-256-ofb", "aes-256-siv", "aes-256-wrap", "aes-256-wrap-inv", \ + "aes-256-wrap-pad", "aes-256-wrap-pad-inv", "aes-256-xts", "aes128", "aes128-wrap", \ + "aes128-wrap-inv", "aes128-wrap-pad", "aes128-wrap-pad-inv", "aes192", "aes192-wrap", \ + "aes192-wrap-inv", "aes192-wrap-pad", "aes192-wrap-pad-inv", "aes256", "aes256-wrap", \ + "aes256-wrap-inv", "aes256-wrap-pad", "aes256-wrap-pad-inv", "aria-128-cbc", "aria-128-ccm", \ + "aria-128-cfb", "aria-128-cfb1", "aria-128-cfb8", "aria-128-ctr", "aria-128-ecb", "aria-128-gcm", \ + "aria-128-ofb", "aria-192-cbc", "aria-192-ccm", "aria-192-cfb", "aria-192-cfb1", "aria-192-cfb8", \ + "aria-192-ctr", "aria-192-ecb", "aria-192-gcm", "aria-192-ofb", "aria-256-cbc", "aria-256-ccm", \ + "aria-256-cfb", "aria-256-cfb1", "aria-256-cfb8", "aria-256-ctr", "aria-256-ecb", "aria-256-gcm", \ + "aria-256-ofb", "aria128", "aria192", "aria256", "camellia-128-cbc", "camellia-128-cbc-cts", \ + "camellia-128-cfb", "camellia-128-cfb1", "camellia-128-cfb8", "camellia-128-ctr", \ + "camellia-128-ecb", "camellia-128-ofb", "camellia-192-cbc", "camellia-192-cbc-cts", \ + "camellia-192-cfb", "camellia-192-cfb1", "camellia-192-cfb8", "camellia-192-ctr", \ + "camellia-192-ecb", "camellia-192-ofb", "camellia-256-cbc", "camellia-256-cbc-cts", \ + "camellia-256-cfb", "camellia-256-cfb1", "camellia-256-cfb8", "camellia-256-ctr", \ + "camellia-256-ecb", "camellia-256-ofb", "camellia128", "camellia192", "camellia256", "chacha20", \ + "chacha20-poly1305", "des-ede", "des-ede-cbc", "des-ede-cfb", "des-ede-ecb", "des-ede-ofb", \ + "des-ede3", "des-ede3-cbc", "des-ede3-cfb", "des-ede3-cfb1", "des-ede3-cfb8", "des-ede3-ecb", \ + "des-ede3-ofb", "des3", "des3-wrap", "id-aes128-ccm", "id-aes128-gcm", "id-aes128-wrap", \ + "id-aes128-wrap-pad", "id-aes192-ccm", "id-aes192-gcm", "id-aes192-wrap", "id-aes192-wrap-pad", \ + "id-aes256-ccm", "id-aes256-gcm", "id-aes256-wrap", "id-aes256-wrap-pad", \ + "id-smime-alg-cms3deswrap", "sm4", "sm4-cbc", "sm4-ccm", "sm4-cfb", "sm4-cfb128", "sm4-ctr", \ + "sm4-ecb", "sm4-gcm", "sm4-ofb", "sm4-ofb128", "sm4-xts", + +/* The ciphers OpenSSL 3 keeps in its legacy provider, which Node 24 does + * not load, with their IV lengths: OpenSSL knows their names, and using + * them fails with its "unsupported" error. */ +#define SCR_CIPHER_LEGACY_ENTRIES \ + {"des", 8}, {"des-cbc", 8}, {"des-ecb", 0}, {"des-cfb", 8}, {"des-cfb1", 8}, {"des-cfb8", 8}, \ + {"des-ofb", 8}, {"desx", 8}, {"desx-cbc", 8}, {"bf", 8}, {"bf-cbc", 8}, {"bf-ecb", 0}, \ + {"bf-cfb", 8}, {"bf-ofb", 8}, {"blowfish", 8}, {"cast", 8}, {"cast-cbc", 8}, {"cast5-cbc", 8}, \ + {"cast5-ecb", 0}, {"cast5-cfb", 8}, {"cast5-ofb", 8}, {"rc2", 8}, {"rc2-cbc", 8}, {"rc2-ecb", 0}, \ + {"rc2-cfb", 8}, {"rc2-ofb", 8}, {"rc2-40-cbc", 8}, {"rc2-64-cbc", 8}, {"rc4", 0}, {"rc4-40", 0}, \ + {"rc4-hmac-md5", 0}, {"seed", 16}, {"seed-cbc", 16}, {"seed-ecb", 0}, {"seed-cfb", 16}, \ + {"seed-ofb", 16}, {"idea", 8}, {"idea-cbc", 8}, {"idea-ecb", 0}, {"idea-cfb", 8}, {"idea-ofb", 8}, + +/* The OID texts OpenSSL's providers register as cipher aliases, with the + * name each fetches (crypto.getCipherInfo(oid).name on Node 24.22). */ +#define SCR_CIPHER_OID_ALIASES \ + {"2.16.840.1.101.3.4.1.1", "aes-128-ecb"}, {"2.16.840.1.101.3.4.1.2", "aes-128-cbc"}, \ + {"2.16.840.1.101.3.4.1.3", "aes-128-ofb"}, {"2.16.840.1.101.3.4.1.4", "aes-128-cfb"}, \ + {"2.16.840.1.101.3.4.1.5", "id-aes128-wrap"}, {"2.16.840.1.101.3.4.1.6", "id-aes128-gcm"}, \ + {"2.16.840.1.101.3.4.1.7", "id-aes128-ccm"}, {"2.16.840.1.101.3.4.1.8", "id-aes128-wrap-pad"}, \ + {"2.16.840.1.101.3.4.1.21", "aes-192-ecb"}, {"2.16.840.1.101.3.4.1.22", "aes-192-cbc"}, \ + {"2.16.840.1.101.3.4.1.23", "aes-192-ofb"}, {"2.16.840.1.101.3.4.1.24", "aes-192-cfb"}, \ + {"2.16.840.1.101.3.4.1.25", "id-aes192-wrap"}, {"2.16.840.1.101.3.4.1.26", "id-aes192-gcm"}, \ + {"2.16.840.1.101.3.4.1.27", "id-aes192-ccm"}, {"2.16.840.1.101.3.4.1.28", "id-aes192-wrap-pad"}, \ + {"2.16.840.1.101.3.4.1.41", "aes-256-ecb"}, {"2.16.840.1.101.3.4.1.42", "aes-256-cbc"}, \ + {"2.16.840.1.101.3.4.1.43", "aes-256-ofb"}, {"2.16.840.1.101.3.4.1.44", "aes-256-cfb"}, \ + {"2.16.840.1.101.3.4.1.45", "id-aes256-wrap"}, {"2.16.840.1.101.3.4.1.46", "id-aes256-gcm"}, \ + {"2.16.840.1.101.3.4.1.47", "id-aes256-ccm"}, {"2.16.840.1.101.3.4.1.48", "id-aes256-wrap-pad"}, \ + {"1.2.410.200046.1.1.1", "aria-128-ecb"}, {"1.2.410.200046.1.1.2", "aria-128-cbc"}, \ + {"1.2.410.200046.1.1.3", "aria-128-cfb"}, {"1.2.410.200046.1.1.4", "aria-128-ofb"}, \ + {"1.2.410.200046.1.1.5", "aria-128-ctr"}, {"1.2.410.200046.1.1.6", "aria-192-ecb"}, \ + {"1.2.410.200046.1.1.7", "aria-192-cbc"}, {"1.2.410.200046.1.1.8", "aria-192-cfb"}, \ + {"1.2.410.200046.1.1.9", "aria-192-ofb"}, {"1.2.410.200046.1.1.10", "aria-192-ctr"}, \ + {"1.2.410.200046.1.1.11", "aria-256-ecb"}, {"1.2.410.200046.1.1.12", "aria-256-cbc"}, \ + {"1.2.410.200046.1.1.13", "aria-256-cfb"}, {"1.2.410.200046.1.1.14", "aria-256-ofb"}, \ + {"1.2.410.200046.1.1.15", "aria-256-ctr"}, {"1.2.410.200046.1.1.34", "aria-128-gcm"}, \ + {"1.2.410.200046.1.1.35", "aria-192-gcm"}, {"1.2.410.200046.1.1.36", "aria-256-gcm"}, \ + {"1.2.410.200046.1.1.37", "aria-128-ccm"}, {"1.2.410.200046.1.1.38", "aria-192-ccm"}, \ + {"1.2.410.200046.1.1.39", "aria-256-ccm"}, {"0.3.4401.5.3.1.9.1", "camellia-128-ecb"}, \ + {"0.3.4401.5.3.1.9.3", "camellia-128-ofb"}, {"0.3.4401.5.3.1.9.4", "camellia-128-cfb"}, \ + {"0.3.4401.5.3.1.9.9", "camellia-128-ctr"}, {"0.3.4401.5.3.1.9.21", "camellia-192-ecb"}, \ + {"0.3.4401.5.3.1.9.23", "camellia-192-ofb"}, {"0.3.4401.5.3.1.9.24", "camellia-192-cfb"}, \ + {"0.3.4401.5.3.1.9.29", "camellia-192-ctr"}, {"0.3.4401.5.3.1.9.41", "camellia-256-ecb"}, \ + {"0.3.4401.5.3.1.9.43", "camellia-256-ofb"}, {"0.3.4401.5.3.1.9.44", "camellia-256-cfb"}, \ + {"0.3.4401.5.3.1.9.49", "camellia-256-ctr"}, {"1.2.156.10197.1.104.1", "sm4-ecb"}, \ + {"1.2.156.10197.1.104.2", "sm4-cbc"}, {"1.2.156.10197.1.104.3", "sm4-ofb"}, \ + {"1.2.156.10197.1.104.4", "sm4-cfb"}, {"1.2.156.10197.1.104.7", "sm4-ctr"}, \ + {"1.2.156.10197.1.104.8", "sm4-gcm"}, {"1.2.156.10197.1.104.9", "sm4-ccm"}, \ + {"1.2.156.10197.1.104.10", "sm4-xts"}, {"1.2.392.200011.61.1.1.1.2", "camellia-128-cbc"}, \ + {"1.2.392.200011.61.1.1.1.3", "camellia-192-cbc"}, \ + {"1.2.392.200011.61.1.1.1.4", "camellia-256-cbc"}, {"1.2.840.113549.3.2", "rc2-cbc"}, \ + {"1.2.840.113549.3.4", "rc4"}, {"1.2.840.113549.3.7", "des-ede3-cbc"}, \ + {"1.2.840.113549.1.9.16.3.6", "id-smime-alg-cms3deswrap"}, {"1.3.14.3.2.6", "des-ecb"}, \ + {"1.3.14.3.2.7", "des-cbc"}, {"1.3.14.3.2.8", "des-ofb"}, {"1.3.14.3.2.9", "des-cfb"}, \ + {"1.3.14.3.2.17", "des-ede"}, {"1.3.111.2.1619.0.1.1", "aes-128-xts"}, \ + {"1.3.111.2.1619.0.1.2", "aes-256-xts"}, {"1.3.6.1.4.1.3029.1.2", "bf-cbc"}, \ + {"1.3.6.1.4.1.188.7.1.1.2", "idea-cbc"}, {"1.2.410.200004.1.3", "seed-ecb"}, \ + {"1.2.410.200004.1.4", "seed-cbc"}, {"1.2.410.200004.1.5", "seed-cfb"}, \ + {"1.2.410.200004.1.6", "seed-ofb"}, {"1.2.840.113533.7.66.10", "cast5-cbc"}, + +#endif diff --git a/packages/runtime/src/scr_closure.h b/packages/runtime/src/scr_closure.h index 9644722ce..cfca90313 100644 --- a/packages/runtime/src/scr_closure.h +++ b/packages/runtime/src/scr_closure.h @@ -98,7 +98,7 @@ typedef struct ScrClosure { * (like the dyn→closure edge): a cycle through it is merely never * collected. */ ScrBox *props; - uint32_t function_kind; /* low bits: 0 ordinary, 1 generator, 2 async, 3 async generator; bit 2: ordinary own prototype; bit 3: checked callable adapter in caps[0] */ + uint32_t function_kind; /* low bits: 0 ordinary, 1 generator, 2 async, 3 async generator; bit 2: ordinary own prototype; bit 3: checked callable adapter in caps[0]; bit 4: SCR_FN_RECEIVER_FREE */ /* The root's `length` as boxed values report it; meaningful only when * identity is set (the root's own boxes carry their arity). */ uint32_t identity_length; @@ -117,6 +117,13 @@ typedef struct ScrClosure { ScrBox *caps[]; } ScrClosure; +/* function_kind bit 4: the body never observes the ambient receiver (the + * `this` an unbound call binds), so compiled callers skip the receiver + * bracket around an indirect call. Set only by the compiler; the kind + * proper is function_kind & SCR_FN_KIND_MASK. */ +#define SCR_FN_RECEIVER_FREE 16u +#define SCR_FN_KIND_MASK 15u + ScrClosure *scr_closure_new(void *fn, size_t ncaps); /* +1; caller fills caps with +1 box refs */ static inline ScrClosure *scr_closure_retain(ScrClosure *c) { diff --git a/packages/runtime/src/scr_console.c b/packages/runtime/src/scr_console.c index 086340014..f1fefa626 100644 --- a/packages/runtime/src/scr_console.c +++ b/packages/runtime/src/scr_console.c @@ -148,7 +148,7 @@ static void scr_enable_utf8_console_output(void) { } #endif -void scr_runtime_abi_v37(void) {} +void scr_runtime_abi_v47(void) {} void scr_init(void) { #ifdef SCR_WORKERS diff --git a/packages/runtime/src/scr_console_native.c b/packages/runtime/src/scr_console_native.c index 8c7467127..492d57d82 100644 --- a/packages/runtime/src/scr_console_native.c +++ b/packages/runtime/src/scr_console_native.c @@ -523,7 +523,11 @@ ScrStr *scr_console_dir_dyn(ScrDyn *value, double depth) { scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); return scr_str_new("", 0); } - return scr_insp_dyn(value, 0, depth); + /* console.dir is inspect with customInspect: false. */ + bool custom = scr_insp_custom_inspect(false); + ScrStr *out = scr_insp_dyn(value, 0, depth); + scr_insp_custom_inspect(custom); + return out; } /* ── console.table ─────────────────────────────────────────────────── diff --git a/packages/runtime/src/scr_core.h b/packages/runtime/src/scr_core.h index 3707b8d50..f3c85e740 100644 --- a/packages/runtime/src/scr_core.h +++ b/packages/runtime/src/scr_core.h @@ -81,7 +81,7 @@ void scr_init(void); /* Program objects emitted by the bundled LLVM helper reference this symbol. * Its versioned spelling makes a mismatched manual runtime link fail before * the program can start. */ -void scr_runtime_abi_v37(void); +void scr_runtime_abi_v47(void); void scr_runtime_workers_v8(void); /* ── the trap funnel (scr_console.c; scr_library.c under -DSCR_LIB) ────── diff --git a/packages/runtime/src/scr_crypto.h b/packages/runtime/src/scr_crypto.h index f61d27406..15180e880 100644 --- a/packages/runtime/src/scr_crypto.h +++ b/packages/runtime/src/scr_crypto.h @@ -298,9 +298,43 @@ ScrDyn *scr_crypto_generate_key_pair_sync(ScrDyn *type, ScrDyn *options); void scr_crypto_generate_key_pair_async(ScrDyn *type, ScrDyn *options, ScrDyn *callback); ScrDyn *scr_crypto_sign_oneshot(ScrDyn *algorithm, ScrDyn *data, ScrDyn *key); ScrDyn *scr_crypto_verify_oneshot(ScrDyn *algorithm, ScrDyn *data, ScrDyn *key, ScrDyn *signature); +/* crypto.diffieHellman(options[, callback]): the shared secret, or undefined + * with the callback, which receives it in a later macrotask. */ +ScrDyn *scr_crypto_diffie_hellman(ScrDyn *options, ScrDyn *callback); ScrDyn *scr_crypto_create_sign(ScrDyn *algorithm); ScrDyn *scr_crypto_create_verify(ScrDyn *algorithm); bool scr_crypto_is_asymmetric_key(const ScrDyn *value); const char *scr_crypto_asymmetric_key_type(const ScrDyn *value); +/* X509Certificate fields (scr_x509.c): raw decodes the constructor's input (+1 Buffer, + * or NULL with Node's PEM error pending); the others read that DER + * (borrowed) and never throw. subjectAltName, infoAccess and + * signatureAlgorithm answer "" where Node answers undefined. */ +ScrBytes *scr_crypto_x509_raw(ScrBytes *data); +ScrBytes *scr_crypto_x509_raw_der(ScrBytes *der); +ScrBytes *scr_crypto_x509_raw_str(ScrStr *pem); +ScrStr *scr_crypto_x509_subject(ScrBytes *der); +ScrStr *scr_crypto_x509_issuer(ScrBytes *der); +ScrStr *scr_crypto_x509_subject_alt_name(ScrBytes *der); +ScrStr *scr_crypto_x509_info_access(ScrBytes *der); +ScrStr *scr_crypto_x509_serial_number(ScrBytes *der); +ScrStr *scr_crypto_x509_fingerprint256(ScrBytes *der); +ScrStr *scr_crypto_x509_fingerprint512(ScrBytes *der); +bool scr_crypto_x509_ca(ScrBytes *der); +ScrStr *scr_crypto_x509_signature_algorithm(ScrBytes *der); +ScrStr *scr_crypto_x509_signature_algorithm_oid(ScrBytes *der); +ScrStr *scr_crypto_x509_pem(ScrBytes *der); +/* X509Certificate methods over the DER (borrowed). checkHost, checkEmail + * and checkIP answer the matching name ("" for none, Node's undefined) or + * NULL with Node's argument error pending (scr_x509.c). publicKey, verify + * and checkPrivateKey read keys (scr_pki.c): publicKey is +1 or NULL, and + * the two checks answer false with the error pending when they throw. */ +ScrStr *scr_crypto_x509_check_host(ScrBytes *der, ScrDyn *name, ScrDyn *options); +ScrStr *scr_crypto_x509_check_email(ScrBytes *der, ScrDyn *email, ScrDyn *options); +ScrStr *scr_crypto_x509_check_ip(ScrBytes *der, ScrDyn *ip, ScrDyn *options); +bool scr_crypto_x509_check_issued(ScrBytes *der, ScrBytes *issuer_der); +ScrDyn *scr_crypto_x509_legacy_object(ScrBytes *der); +ScrDyn *scr_crypto_x509_public_key(ScrBytes *der); +bool scr_crypto_x509_verify(ScrBytes *der, ScrDyn *key); +bool scr_crypto_x509_check_private_key(ScrBytes *der, ScrDyn *key); #endif /* SCR_CRYPTO_H */ diff --git a/packages/runtime/src/scr_cycle.c b/packages/runtime/src/scr_cycle.c index 6dcd546b0..d421e8e45 100644 --- a/packages/runtime/src/scr_cycle.c +++ b/packages/runtime/src/scr_cycle.c @@ -170,7 +170,8 @@ void scr_weak_dispose(void *object) { #include /* Every heap object's first member is `size_t rc`; the collector works on - * its count (SCR_RC_COUNT). */ + * its count (SCR_RC_COUNT), and tests it without a frozen record's bit + * (SCR_RC_REFS). */ #define SCR_RC(obj) SCR_RC_COUNT(obj) static void scr_cyc_oom(void) { @@ -293,24 +294,22 @@ static SCR_TL ScrDestroyEntry *scr_destroy_pending; static SCR_TL size_t scr_destroy_count, scr_destroy_capacity; #endif -void scr_rc_destroy(void *obj, void (*destroy)(void *)) { - if (scr_destroy_depth == SCR_DESTROY_DEPTH) { - if (scr_destroy_count == scr_destroy_capacity) { - size_t capacity = scr_destroy_capacity ? scr_destroy_capacity * 2 : 64; - if (capacity < scr_destroy_capacity || capacity > SIZE_MAX / sizeof(ScrDestroyEntry)) - scr_cyc_oom(); - ScrDestroyEntry *entries = realloc(scr_destroy_pending, capacity * sizeof(*entries)); - if (!entries) scr_cyc_oom(); - scr_destroy_pending = entries; - scr_destroy_capacity = capacity; - } - scr_destroy_pending[scr_destroy_count++] = (ScrDestroyEntry){ obj, destroy }; - return; +/* Queues a disposal past the depth budget; the outermost disposal runs it. */ +static void scr_rc_destroy_defer(void *obj, void (*destroy)(void *)) { + if (scr_destroy_count == scr_destroy_capacity) { + size_t capacity = scr_destroy_capacity ? scr_destroy_capacity * 2 : 64; + if (capacity < scr_destroy_capacity || capacity > SIZE_MAX / sizeof(ScrDestroyEntry)) + scr_cyc_oom(); + ScrDestroyEntry *entries = realloc(scr_destroy_pending, capacity * sizeof(*entries)); + if (!entries) scr_cyc_oom(); + scr_destroy_pending = entries; + scr_destroy_capacity = capacity; } - scr_destroy_depth++; - destroy(obj); - scr_destroy_depth--; - if (scr_destroy_depth) return; + scr_destroy_pending[scr_destroy_count++] = (ScrDestroyEntry){ obj, destroy }; +} + +/* Runs the queued disposals once the outermost one has returned. */ +static void scr_rc_destroy_drain(void) { while (scr_destroy_count) { ScrDestroyEntry entry = scr_destroy_pending[--scr_destroy_count]; scr_destroy_depth = 1; @@ -326,6 +325,30 @@ void scr_rc_destroy(void *obj, void (*destroy)(void *)) { } } +void scr_rc_destroy(void *obj, void (*destroy)(void *)) { + if (scr_destroy_depth == SCR_DESTROY_DEPTH) { + scr_rc_destroy_defer(obj, destroy); + return; + } + scr_destroy_depth++; + destroy(obj); + scr_destroy_depth--; + if (scr_destroy_depth == 0) scr_rc_destroy_drain(); +} + +void *scr_rc_destroy_seg(void *obj, void *(*destroy_seg)(void *, void *), + void (*destroy)(void *), void *tail) { + if (scr_destroy_depth == SCR_DESTROY_DEPTH) { + scr_rc_destroy_defer(obj, destroy); + return tail; + } + scr_destroy_depth++; + tail = destroy_seg(obj, tail); + scr_destroy_depth--; + if (scr_destroy_depth == 0) scr_rc_destroy_drain(); + return tail; +} + /* Live cycle-headered objects and the running count of OLD objects freed. * The inline allocation/free paths (scr_runtime.h, llvm/alloc.ts) maintain * both counters so full passes can measure old-generation reclamation. */ @@ -580,7 +603,8 @@ static SCR_TL unsigned scr_cyc_backlog_shift[SCR_CYC_NGENS]; static SCR_TL bool scr_cyc_backlog_judged[SCR_CYC_NGENS]; #endif -/* Mature objects in the last full pass's dead set (scr_cyc_adapt_growth). */ +/* Mature objects in the last pass's dead set (scr_cyc_adapt_growth, + * scr_cyc_adapt_nursery). */ #ifndef SCR_WORKERS static SCR_TL size_t scr_cyc_mature_dead = 0; #endif @@ -614,14 +638,76 @@ static SCR_TL bool scr_cyc_idle_points = false; static SCR_TL size_t scr_cyc_death_due_live = 0; #endif -static size_t scr_cyc_nursery_threshold(void) { - static SCR_TL size_t cached = 0; - if (cached == 0) { +/* The nursery trigger a program sets for itself + * (scr_cyc_set_nursery_threshold), 0 for the default; SCR_CYCLE_THRESHOLD + * takes precedence over it. Either pins the trigger. */ +static long scr_cyc_nursery_threshold_set = 0; +static SCR_TL size_t scr_cyc_nursery_threshold_cached = 0; +#define SCR_CYC_NURSERY_ADAPTIVE SIZE_MAX + +/* An unpinned trigger follows where cyclic garbage dies. Garbage that + * outlives the nursery window survives its nursery pass and is promoted, + * and then only a mature or full pass finds it, after walking that level's + * live candidates. So after each scheduled mature or full pass whose dead + * set held more mature and old objects than 1/SCR_CYC_NURSERY_RATIO of what + * the nursery passes since the previous one freed, the window doubles, up + * to SCR_CYC_NURSERY_MAX_SHIFT doublings (4096 candidates); after one that + * held less than a quarter of that share, it halves, down to + * SCR_CYC_NURSERY_CANDIDATES. + * `nursery_freed`: what the nursery passes freed since the last scheduled + * mature or full pass. */ +#define SCR_CYC_NURSERY_RATIO 8 +#define SCR_CYC_NURSERY_MAX_SHIFT 4 +static SCR_TL unsigned char scr_cyc_nursery_shift = 0; +static SCR_TL size_t scr_cyc_nursery_freed = 0; + +static size_t scr_cyc_nursery_pinned(void) { + if (scr_cyc_nursery_threshold_cached == 0) { const char *env = scr_getenv("SCR_CYCLE_THRESHOLD"); long v = env ? strtol(env, NULL, 10) : 0; - cached = v > 0 ? (size_t)v : SCR_CYC_NURSERY_CANDIDATES; + if (v <= 0) v = scr_cyc_nursery_threshold_set; + scr_cyc_nursery_threshold_cached = v > 0 ? (size_t)v : SCR_CYC_NURSERY_ADAPTIVE; } - return cached; + return scr_cyc_nursery_threshold_cached; +} + +/* The pinned trigger, or the default one. The scheduled-age rule counts + * collection points in it, so the adaptation does not lengthen how long an + * older backlog waits at them. */ +static size_t scr_cyc_nursery_base(void) { + size_t pinned = scr_cyc_nursery_pinned(); + return pinned != SCR_CYC_NURSERY_ADAPTIVE ? pinned : SCR_CYC_NURSERY_CANDIDATES; +} + +static size_t scr_cyc_nursery_threshold(void) { + size_t pinned = scr_cyc_nursery_pinned(); + if (pinned != SCR_CYC_NURSERY_ADAPTIVE) return pinned; + return (size_t)SCR_CYC_NURSERY_CANDIDATES << scr_cyc_nursery_shift; +} + +/* Adapts an unpinned trigger to the mature and old objects a scheduled + * mature or full pass reclaimed; returns whether the trigger changed. */ +static SCR_CYC_SCHEDULING bool scr_cyc_adapt_nursery(size_t older_freed) { + size_t nursery_freed = scr_cyc_nursery_freed; + scr_cyc_nursery_freed = 0; + if (scr_cyc_nursery_pinned() != SCR_CYC_NURSERY_ADAPTIVE) return false; + if (older_freed * SCR_CYC_NURSERY_RATIO > nursery_freed) { + if (scr_cyc_nursery_shift == SCR_CYC_NURSERY_MAX_SHIFT) return false; + scr_cyc_nursery_shift++; + return true; + } + if (older_freed * SCR_CYC_NURSERY_RATIO * 4 < nursery_freed && scr_cyc_nursery_shift > 0) { + scr_cyc_nursery_shift--; + return true; + } + return false; +} + +void scr_cyc_set_nursery_threshold(long threshold) { + scr_cyc_nursery_threshold_set = threshold; + scr_cyc_nursery_threshold_cached = 0; + scr_cyc_nursery_shift = 0; + scr_cyc_nursery_freed = 0; } /* The number of buffered candidates (across every generation) at which the @@ -847,7 +933,14 @@ static SCR_CYC_SCHEDULING void scr_cyc_scheduled_pass(unsigned gen, bool by_grow size_t freed = scr_cyc_pass(gen); scr_cyc_keep_base = false; size_t old_reclaimed = scr_cyc_old_freed - old_freed_before + scr_cyc_mature_dead; - if (gen == SCR_CYC_NURSERY) return; + if (gen == SCR_CYC_NURSERY) { + scr_cyc_nursery_freed += freed; + return; + } + if (scr_cyc_adapt_nursery(old_reclaimed)) { + scr_cyc_trigger = scr_cyc_buffered() + scr_cyc_nursery_threshold(); + scr_cyc_window_update(); + } if (scr_cands.n != 0) { scr_cyc_backlog_judged[gen] = true; if (freed * SCR_CYC_GROWTH_DIV >= scr_cands.n) @@ -906,7 +999,7 @@ void scr_cyc_collect_scheduled(void) { if (!scr_cyc_older_backlog()) { scr_cyc_scheduled_mature_age = 0; } else if (++scr_cyc_scheduled_mature_age - >= scr_cyc_nursery_threshold()) { + >= scr_cyc_nursery_base()) { scr_cyc_scheduled_pass(SCR_CYC_OLD, false, false); return; } @@ -1308,7 +1401,8 @@ static void scr_mg_visit(void *child, void *ctx) { scr_mg_skip_edge(child); return; } - size_t rc = --SCR_RC(child); + --SCR_RC(child); + size_t rc = SCR_RC_REFS(child); if (scr_cyc_color(h) == SCR_CYC_GRAY) { if (rc == 0) { scr_gray_referenced--; @@ -1336,7 +1430,7 @@ static inline __attribute__((always_inline)) void scr_mark_gray(void *obj, uintp )) h->buf = scr_gray_comp; if (scr_white.n < SCR_CYC_GRAY_LIST_MAX) scr_cyc_push_gray(&scr_white, obj); - if (SCR_RC(obj) > 0) { + if (SCR_RC_REFS(obj) > 0) { scr_gray_referenced++; scr_comp_referenced++; } @@ -1380,7 +1474,10 @@ static void scr_scan_black(void *obj, uintptr_t depth) { const ScrCycType *type = scr_cyc_type(h); bool acyclic = type->trace == scr_cyc_no_children; if (!scr_cyc_is_buffered(h)) h->buf = acyclic ? SCR_CYC_ACYCLIC : 0; - if (scr_cyc_gen(h) < SCR_CYC_OLD) scr_cyc_push_work(&scr_promote, obj); + /* Such a candidate also keeps its generation: the generation names the + * buffer its slot is in (scr_cyc_on_dead), so it is promoted only once a + * pass takes it. */ + if (scr_cyc_gen(h) < SCR_CYC_OLD && !scr_cyc_is_buffered(h)) scr_cyc_push_work(&scr_promote, obj); if (acyclic) return; if (depth == SCR_CYC_WALK_DEPTH) { scr_cyc_push_work(&scr_restore_pending, obj); @@ -1408,7 +1505,7 @@ static void scr_scan_visit(void *child, void *ctx) { static void scr_scan(void *obj, uintptr_t depth) { ScrCycHdr *h = scr_cyc_hdr(obj); if (scr_cyc_color(h) != SCR_CYC_GRAY) return; - if (SCR_RC(obj) > 0) { + if (SCR_RC_REFS(obj) > 0) { scr_restore_from(obj); return; } @@ -1476,7 +1573,7 @@ static void scr_xg_destroy(void *obj) { } static SCR_CYC_COMPACT void scr_xg_release(void *obj) { - if (SCR_RC(obj) > 1) { + if (SCR_RC_REFS(obj) > 1) { SCR_RC(obj) -= 1; scr_cyc_on_release(obj); /* lost a reference: a possible cycle root */ return; @@ -1768,14 +1865,14 @@ static __attribute__((noinline)) size_t scr_yin_collect(void) { if (scr_cyc_gen(h) != SCR_CYC_YOUNG || !scr_cyc_is_buffered(h) || scr_cyc_buf_index(h) != k || scr_cyc_color(h) != SCR_CYC_BLACK) scr_trap("scriptc: corrupt young nursery entry\n"); - if (SCR_RC(v[k]) == 0 || SCR_RC(v[k]) < scr_yin_counts[k]) + if (SCR_RC_REFS(v[k]) == 0 || SCR_RC_REFS(v[k]) < scr_yin_counts[k]) scr_trap("scriptc: young object with uncounted references\n"); } #endif for (size_t k = 0; k < n; k++) { void *obj = v[k]; ScrCycHdr *h = scr_cyc_hdr(obj); - if (scr_cyc_color(h) == SCR_CYC_GRAY || SCR_RC(obj) <= scr_yin_counts[k]) continue; + if (scr_cyc_color(h) == SCR_CYC_GRAY || SCR_RC_REFS(obj) <= scr_yin_counts[k]) continue; scr_cyc_set_color(h, SCR_CYC_GRAY); scr_yin_push(&scr_yin_stack, obj); while (scr_yin_stack.n) { @@ -1799,7 +1896,7 @@ static __attribute__((noinline)) size_t scr_yin_collect(void) { scr_cyc_set_gen(h, SCR_CYC_YIN_PROMOTED); scr_cyc_clear_buffered(h); /* An outside root becomes a candidate (see "candidates" above). */ - if (SCR_RC(obj) > scr_yin_counts[k]) scr_cyc_on_release(obj); + if (SCR_RC_REFS(obj) > scr_yin_counts[k]) scr_cyc_on_release(obj); scr_yin_met(h->id, false); } else { scr_yin_met(h->id, true); @@ -1946,7 +2043,7 @@ static SCR_CYC_COMPACT size_t scr_cyc_settle(unsigned gen_limit, bool doomed) { ScrCycHdr *h = scr_cyc_hdr(obj); const ScrCycType *type = scr_cyc_type(h); if (gen_limit < SCR_CYC_OLD) scr_cyc_freed(type, gen_limit); - else scr_cyc_mature_dead += scr_cyc_gen(h) == SCR_CYC_MATURE; + scr_cyc_mature_dead += scr_cyc_gen(h) == SCR_CYC_MATURE; #ifdef SCR_YIN if (scr_cyc_gen(h) == SCR_CYC_YOUNG) scr_yin_leave(h, obj); #endif @@ -2147,7 +2244,7 @@ static size_t scr_cyc_pass(unsigned gen_limit) { for (size_t j = start; j < end; j++) { void *obj = scr_white.v[j]; if (scr_cyc_color(scr_cyc_hdr(obj)) != SCR_CYC_GRAY) continue; - if (SCR_RC(obj) > 0) scr_restore_from(obj); + if (SCR_RC_REFS(obj) > 0) scr_restore_from(obj); else scr_white.v[kept++] = obj; } from = end; @@ -2176,7 +2273,7 @@ static size_t scr_cyc_pass(unsigned gen_limit) { * whitened only to be restored again. */ for (size_t i = 0; i < scr_cands.n; i++) { void *obj = scr_cands.v[i]; - if (scr_cyc_color(scr_cyc_hdr(obj)) == SCR_CYC_GRAY && SCR_RC(obj) > 0) scr_restore_from(obj); + if (scr_cyc_color(scr_cyc_hdr(obj)) == SCR_CYC_GRAY && SCR_RC_REFS(obj) > 0) scr_restore_from(obj); } for (size_t i = 0; i < scr_cands.n; i++) { void *obj = scr_cands.v[i]; @@ -2256,7 +2353,9 @@ void scr_collect_cycles(void) { * finding anything, and the death rule's backoff then start over from the * heap the sweep leaves, as after a productive pass. Otherwise the next * phase's garbage would accumulate until the heap grew to that widened - * fraction. */ + * fraction. The freed memory then goes back to the system (scr_heap_trim): + * the next phase allocates other sizes than the dead data held, so it would + * reuse little of it and grow its resident memory on top. */ void scr_collect_cycles_phase(void) { scr_collect_cycles(); for (unsigned gen = SCR_CYC_MATURE; gen < SCR_CYC_NGENS; gen++) { @@ -2264,6 +2363,7 @@ void scr_collect_cycles_phase(void) { scr_cyc_growth_streak[gen] = 0; } scr_cyc_death_shift = 0; + scr_heap_trim(); } /* Worker TLS becomes unreachable when its OS thread returns. Release the diff --git a/packages/runtime/src/scr_decl_adopt.c b/packages/runtime/src/scr_decl_adopt.c index 7372826ab..fc1c4456c 100644 --- a/packages/runtime/src/scr_decl_adopt.c +++ b/packages/runtime/src/scr_decl_adopt.c @@ -15,7 +15,11 @@ * value has no prototype, accessors, symbol keys or frozen state) and * collects the shared ones; it throws the path TypeError before anything * changes. Phase two converts the shared nodes in place - * (scr_dyn_decl_become, scr_json.c). */ + * (scr_dyn_decl_become, scr_json.c). + * + * Every other checked cast of a dynamic value to a record or array runs the + * same pass leniently (scr_dyn_adopt_shared): a node that cannot become a + * capsule keeps the cast's copy. */ #include "scr_runtime.h" #include @@ -31,6 +35,9 @@ typedef struct { typedef struct { ScrDeclAdopt *items; size_t len, cap; + /* A checked cast outside the boundary (scr_dyn_adopt_shared): a node + * that cannot become a capsule keeps the copy instead of throwing. */ + bool lenient; } ScrDeclAdoptList; static void scr_decl_adopt_push(ScrDeclAdoptList *list, ScrDyn *node, ScrDyn *capsule) { @@ -44,7 +51,7 @@ static void scr_decl_adopt_push(ScrDeclAdoptList *list, ScrDyn *node, ScrDyn *ca list->len++; } -static bool scr_decl_adopt_plain(const ScrDyn *n, const ScrDynPath *path) { +static bool scr_decl_adopt_plain(const ScrDyn *n, const ScrDynPath *path, bool lenient) { const char *want = NULL; if (n->kind == SCR_DYN_OBJ) { if (n->prototype || n->null_proto || n->v.obj.class_name) @@ -72,7 +79,7 @@ static bool scr_decl_adopt_plain(const ScrDyn *n, const ScrDynPath *path) { want = "a plain array (this value is frozen, sealed or not extensible)"; } if (!want) return true; - scr_dyn_check_fail(path, want, n); + if (!lenient) scr_dyn_check_fail(path, want, n); return false; } @@ -82,6 +89,14 @@ static ScrDyn *scr_decl_capsule_ptr_holder(const ScrDeclAdoptList *list, const S return NULL; } +/* Whether the walk follows a child: plain objects and arrays. A boxed + * native record or array is already one object with its native value; a + * cast to another layout copies it (the copy keeps a cached identity per + * capsule, scr_dyn_typed_ref_cache_cast). */ +static bool scr_decl_adopt_follows(const ScrDyn *child) { + return child && (child->kind == SCR_DYN_OBJ || child->kind == SCR_DYN_ARR); +} + /* `capsule` is owned by the call: pushed or released. */ static bool scr_decl_adopt_walk(ScrDeclAdoptList *list, ScrDyn *n, ScrDyn *capsule, const ScrDynPath *path, unsigned depth) { @@ -90,15 +105,16 @@ static bool scr_decl_adopt_walk(ScrDeclAdoptList *list, ScrDyn *n, ScrDyn *capsu scr_dyn_release(capsule); return true; } - if (!scr_decl_adopt_plain(n, path)) { + if (!scr_decl_adopt_plain(n, path, list->lenient)) { scr_dyn_release(capsule); - return false; + return list->lenient; } if (n->rc > 1 && n->rc != SIZE_MAX) { ScrDyn *seen = scr_decl_capsule_ptr_holder(list, n); if (seen) { bool same = seen->v.typed_ref.ptr == capsule->v.typed_ref.ptr; scr_dyn_release(capsule); + if (!same && list->lenient) return true; if (!same) scr_dyn_check_fail(path, "a value that is not also stored elsewhere in this value (scriptc builds a separate native copy for each place)", n); return same; @@ -112,7 +128,7 @@ static bool scr_decl_adopt_walk(ScrDeclAdoptList *list, ScrDyn *n, ScrDyn *capsu if (!ops->has(ptr, i)) continue; const ScrStr *name = ops->names[i]; ScrDyn *child = scr_dyn_obj_get(n, name->data, name->len); - if (!child || (child->kind != SCR_DYN_OBJ && child->kind != SCR_DYN_ARR)) continue; + if (!scr_decl_adopt_follows(child)) continue; ScrDynPath p = { path, name->data, 0 }; if (!scr_decl_adopt_walk(list, child, ops->get(ptr, i), &p, depth + 1)) { if (!(n->rc > 1 && n->rc != SIZE_MAX)) scr_dyn_release(capsule); @@ -135,7 +151,7 @@ static bool scr_decl_adopt_walk(ScrDeclAdoptList *list, ScrDyn *n, ScrDyn *capsu size_t len = n->v.arr.len < view->v.arr.len ? n->v.arr.len : view->v.arr.len; for (size_t i = 0; i < len; i++) { ScrDyn *child = n->v.arr.items[i], *element = view->v.arr.items[i]; - if (!child || !element || (child->kind != SCR_DYN_OBJ && child->kind != SCR_DYN_ARR)) continue; + if (!element || !scr_decl_adopt_follows(child)) continue; ScrDynPath p = { path, NULL, i }; if (!scr_decl_adopt_walk(list, child, scr_dyn_retain(element), &p, depth + 1)) { scr_dyn_release(view); @@ -150,7 +166,7 @@ static bool scr_decl_adopt_walk(ScrDeclAdoptList *list, ScrDyn *n, ScrDyn *capsu const ScrDynTypedArrayOps *ops = capsule->v.typed_ref.array; for (size_t i = 0; i < n->v.arr.len; i++) { ScrDyn *child = n->v.arr.items[i]; - if (!child || (child->kind != SCR_DYN_OBJ && child->kind != SCR_DYN_ARR)) continue; + if (!scr_decl_adopt_follows(child)) continue; ScrDynPath p = { path, NULL, i }; if (!scr_decl_adopt_walk(list, child, ops->get(ptr, (double)i), &p, depth + 1)) { if (!(n->rc > 1 && n->rc != SIZE_MAX)) scr_dyn_release(capsule); @@ -162,8 +178,8 @@ static bool scr_decl_adopt_walk(ScrDeclAdoptList *list, ScrDyn *n, ScrDyn *capsu return true; } -void scr_dyn_decl_adopt(ScrDyn *source, ScrDyn *capsule) { - ScrDeclAdoptList list = { NULL, 0, 0 }; +static void scr_decl_adopt_run(ScrDyn *source, ScrDyn *capsule, bool lenient) { + ScrDeclAdoptList list = { NULL, 0, 0, lenient }; bool ok = scr_decl_adopt_walk(&list, source, scr_dyn_retain(capsule), NULL, 0); for (size_t i = 0; i < list.len; i++) { ScrDeclAdopt *item = &list.items[i]; @@ -172,3 +188,16 @@ void scr_dyn_decl_adopt(ScrDyn *source, ScrDyn *capsule) { } free(list.items); } + +void scr_dyn_decl_adopt(ScrDyn *source, ScrDyn *capsule) { + scr_decl_adopt_run(source, capsule, false); +} + +/* Any other checked cast of a dynamic value to a record or array + * (`value as T`): the same adoption, so a source that something else still + * holds keeps one identity with the built value. A node that is not plain + * data, or that the value holds in two places, keeps the separate copy the + * cast built (the conversion already succeeded; nothing throws here). */ +void scr_dyn_adopt_shared(ScrDyn *source, ScrDyn *capsule) { + scr_decl_adopt_run(source, capsule, true); +} diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 669f4aedb..5bb56f082 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -23,8 +23,8 @@ * Ownership: the checked-dynamic tree owns its children (strings, items, entry keys+values); * releasing the root releases the tree recursively. scr_json_parse borrows * the text and returns +1 — or THROWS a catchable string shaped like Node's - * V8 messages ("Unexpected end of JSON input"; approximate fidelity, see - * SEMANTICS.md) and returns NULL. scr_dyn_obj_get returns a BORROWED + * V8 messages ("Unexpected end of JSON input", "... in JSON at position + * (line column )") and returns NULL. scr_dyn_obj_get returns a BORROWED * member (the compiler's builders retain what they extract themselves). */ @@ -186,6 +186,10 @@ typedef enum { X(MATCH_SOURCE, 55) /* a match result's pending metadata (scr_regex.c) */ \ X(MESSAGE_EVENT, 50) /* ScrEvent with data — BroadcastChannel's 'message' */ \ X(MESSAGE_PORT, 36) \ + X(MIME_PARAMS, 81) /* util.MIMEParams (scr_mime.c) */ \ + X(MIME_TYPE, 82) /* util.MIMEType (scr_mime.c) */ \ + X(MODULE_NAMESPACE, 79) /* module namespace object read through its exports (scr_module_namespace.c) */ \ + X(NAVIGATOR, 83) /* Node's navigator global (scr_node_builtin.c) */ \ X(NET_BLOCK_LIST, 48) /* net.BlockList (scr_net.c) */ \ X(NET_SERVER, 3) /* ScrNetServer — net.Server (http.Server rides the same handle) */ \ X(NET_SOCKET, 2) /* ScrNetSocket — net.Socket */ \ @@ -195,11 +199,15 @@ typedef enum { X(REGEXP, 26) /* native RegExp, shared compiled pattern */ \ X(SEARCH_PARAMS, 33) /* live WHATWG URLSearchParams */ \ X(SEGMENTER, 19) /* default Unicode grapheme segmentation */ \ + X(SEGMENTER_REFUSING, 80) /* word or sentence Segmenter whose segment() throws (scr_grapheme.c) */ \ X(SEGMENTS, 20) /* iterable immutable segmentation result */ \ X(SET, 25) /* native Set, shared backing map */ \ X(SHARED_ARRAY_BUFFER, 37) \ X(SIGN, 67) /* node:crypto Sign (scr_pki.c) */ \ X(STDIO, 27) /* stable process stdin/stdout/stderr values */ \ + X(STRING_DECODER, 78) /* compiled StringDecoder record (scr_text_codec_dyn.c) */ \ + X(TEXT_DECODER, 76) /* compiled TextDecoder record (scr_text_codec_dyn.c) */ \ + X(TEXT_ENCODER, 77) /* compiled TextEncoder record (scr_text_codec_dyn.c) */ \ X(URL, 28) /* native WHATWG URL */ \ X(VERIFY, 68) /* node:crypto Verify (scr_pki.c) */ \ X(WASM_GLOBAL, 61) /* WebAssembly.Global */ \ @@ -212,6 +220,9 @@ typedef enum { X(WEB_CONTROLLER, 11) /* native ReadableStreamDefaultController */ \ X(WEB_READER, 10) /* native ReadableStreamDefaultReader */ \ X(WEB_STREAM, 9) /* native WHATWG ReadableStream */ \ + X(WEB_TRANSFORM, 73) /* native identity TransformStream (scr_fetch.c) */ \ + X(WEB_WRITABLE, 74) /* a TransformStream's WritableStream (scr_fetch.c) */ \ + X(WEB_WRITER, 75) /* WritableStreamDefaultWriter (scr_fetch.c) */ \ X(WORKER, 35) typedef enum { @@ -319,6 +330,9 @@ typedef struct { * must not inspect the value: an earlier cycle member may already be freed. * Ordinary mutations need not maintain this collector-only decision. */ bool value_traced : 1; + /* A namespace object's function member (scr_dyn_namespace_define), which + * static member calls bypass: a change to its value fences instead. */ + bool fenced : 1; /* Ordinary object insertion sets all three attributes true. */ bool writable; bool enumerable; @@ -380,6 +394,12 @@ const ScrDynTypedRecordOps *scr_dyn_typed_ref_live_record(const ScrDyn *d); * holds. Both borrowed; a node that is not plain data throws the path * TypeError before anything changes. */ void scr_dyn_decl_adopt(ScrDyn *source, ScrDyn *capsule); +void scr_dyn_adopt_shared(ScrDyn *source, ScrDyn *capsule); +/* A checked conversion into a type an npm package declares failed + * (scr_type_origin.c, linked by programs with such checks): the pending + * TypeError's message gains the declaration and the type patch that would + * admit the value. `origin` is the compiler's origin string. */ +void scr_type_origin_note(const char *origin); /* Prototype members of a compiled class seen as `unknown`, emitted per * class by the compiler: the string names of methods, getters and setters @@ -393,6 +413,10 @@ typedef struct ScrDynTypedClassOps { size_t count; ScrStr *const *names; /* immortal member-name strings */ ScrStr *class_name; /* immortal constructor name; NULL for an anonymous class */ + /* The string an inherited Symbol.toStringTag getter answers for the + * instance (+1), or NULL with a pending exception from the getter; + * NULL when the class has no such getter. */ + ScrStr *(*string_tag)(void *instance); } ScrDynTypedClassOps; /* A compiled JavaScript class whose instances record their own properties @@ -465,7 +489,12 @@ struct ScrDyn { * exits, and those clear it before release; reuse of a freed object * resets it (scr_dyn_alloc). */ const char *class_name; - struct ScrDynRegion *region; /* see ScrDynRegion */ + union { + struct ScrDynRegion *region; /* see ScrDynRegion */ + /* While the node is pooled: the key bytes it keeps for reuse, + * counted in the pool's key budget (scr_dyn_obj_drop_keys). */ + size_t cached_key_bytes; + }; /* The dynamic tier's hidden class (scr_js_object.c): the transition * tree node naming this object's key sequence, or NULL while it has * none. Only programs with dynamic-tier code read or maintain it: @@ -508,7 +537,9 @@ struct ScrDyn { * identity while the source reference remains observable. */ ScrDyn *materialized; ScrDynTypedCast *casts; - /* One word keeps this, the largest payload, at its previous size. */ + /* One word keeps this, the largest payload, at its previous size. + * Dynamic-tier inline caches read record, klass and bag_class as + * the word's top three bits (backend/llvm/js-ic.ts nativeArray). */ uint32_t type_key_len : 27; uint32_t traced : 1; /* native referent has a cycle header */ uint32_t observed : 1; /* native release reports this identity's disposal */ @@ -540,6 +571,10 @@ _Static_assert(offsetof(ScrDyn, null_proto) == sizeof(size_t) + sizeof(ScrDynKin "dyn-to-record conversions read null_proto at byte 13 (9 on wasm32)"); _Static_assert(offsetof(ScrDyn, v.typed_ref.type_key) == 16 + 3 * sizeof(void *), "typed_ref.type_key offset"); +/* Emitted brand probes read type_key_len as the low 27 bits of the 32-bit + * word after `casts` (native 88, wasm32 52). */ +_Static_assert(offsetof(ScrDyn, v.typed_ref.casts) == 16 + 8 * sizeof(void *), + "typed_ref.casts offset"); /* The region links ride in payload space the capsule variant already * reserves, so dyn nodes keep their size (and allocation size class). */ _Static_assert(sizeof(((ScrDyn *)0)->v.arr) <= sizeof(((ScrDyn *)0)->v.typed_ref) && @@ -668,6 +703,14 @@ void scr_dyn_copy_entry(ScrDyn *target, const ScrDyn *source, const ScrDynEntry bool scr_dyn_field_eq_str(const ScrDyn *d, const ScrStr *key, const ScrStr *value); bool scr_dyn_field_eq_num(const ScrDyn *d, const ScrStr *key, double value); bool scr_dyn_field_eq_bool(const ScrDyn *d, const ScrStr *key, bool value); +/* The case a union's checked match takes from its string discriminant: the + * index in `values` of the string `d`'s own data property `key` holds, or + * -1 when every literal test above fails (`d` is not an object, or its own + * data property `key` holds no string among `values`), or -2 when only the + * tests themselves can tell (a typed capsule, an accessor, or a property + * `d` does not own). Borrowed args. */ +int32_t scr_dyn_discriminant_case(const ScrDyn *d, const ScrStr *key, ScrStr *const *values, + size_t count); /* Object.keys/values/entries over the checked-dynamic tree: JS own-key order (array-index * keys ascending first), dyn-array results (+1); values/entries RETAIN @@ -806,6 +849,8 @@ ScrDyn *scr_dyn_native_date_new_fields(const ScrDyn *arguments); ScrDyn *scr_dyn_native_date_new_value(const ScrDyn *arguments); ScrStr *scr_dyn_object_tag_of(const ScrDyn *d); ScrDyn *scr_dyn_native_date_from_ms(double ms); +typedef struct ScrDate ScrDate; +ScrDyn *scr_dyn_native_date(ScrDate *date); double scr_dyn_native_date_value(const ScrDyn *value); ScrDyn *scr_dyn_native_regex(ScrRegex *value); bool scr_dyn_native_regex_is(const ScrDyn *value); @@ -847,11 +892,16 @@ bool scr_disposable_stack_use(const ScrDyn *stack, const ScrDyn *value); bool scr_disposable_stack_use_callback(const ScrDyn *stack, const ScrDyn *value, const ScrDyn *dispose); bool scr_disposable_stack_adopt(const ScrDyn *stack, const ScrDyn *value, const ScrDyn *on_dispose); void scr_dyn_symbol_key_set(ScrDyn *value, ScrDyn *key, ScrDyn *stored); +/* scr_dyn_obj_set_field for a symbol-keyed field of a class instance's + * view: replaces the value whatever the attributes copied into the view. + * Does not consume stored. */ +void scr_dyn_symbol_field_set(ScrDyn *value, ScrDyn *key, ScrDyn *stored); void scr_dyn_symbol_key_placeholder(ScrDyn *value, ScrDyn *key); bool scr_dyn_has_key_computed(const ScrDyn *value, const ScrDyn *key); bool scr_dyn_has_own_computed(const ScrDyn *value, const ScrDyn *key); bool scr_dyn_property_is_enumerable_computed(const ScrDyn *value, const ScrDyn *key); void scr_dyn_key_delete_computed(ScrDyn *value, const ScrDyn *key, bool strict); +bool scr_dyn_reflect_delete_property(ScrDyn *target, const ScrDyn *key); ScrStr *scr_dyn_string_constructor(const ScrDyn *value); ScrDyn *scr_dyn_property_key_value(const ScrDyn *value); ScrDyn *scr_dyn_to_numeric(const ScrDyn *value); @@ -861,6 +911,10 @@ ScrDyn *scr_dyn_symbol_ref(ScrSym *value, void (*release)(ScrSym *), ScrStr *(*r ScrDyn *scr_dyn_new_str(ScrStr *s); ScrDyn *scr_dyn_new_arr(void); ScrDyn *scr_dyn_array_constructor(void); +/* Gives a builtin constructor value (Object, Array, Error) its builtin + * `prototype`: non-writable, non-enumerable, non-configurable. Takes the + * prototype. */ +void scr_dyn_fn_seed_prototype(ScrDyn *function, ScrDyn *prototype); /* The interned builtin constructor `name` (Object, Date, RegExp, Map, Set, * Promise, WeakMap, WeakSet) as a dynamic function value: +1. */ ScrDyn *scr_dyn_global_constructor(const ScrStr *name); @@ -870,6 +924,19 @@ ScrDyn *scr_dyn_error_constructor(const char *name); /* A builtin global function (setTimeout, setImmediate, process.nextTick, ...) * as an interned dynamic function value: +1. */ ScrDyn *scr_dyn_builtin_function(const ScrStr *name); +/* A standard namespace object (Math, JSON, Reflect read as a JavaScript + * value): an ordinary object whose Symbol.toStringTag is `tag` (+1). + * `tag_key` is the Symbol.toStringTag key (borrowed), which the compiler + * passes so this unit needs no symbol entry points. */ +ScrDyn *scr_dyn_namespace_new(const ScrStr *tag, ScrDyn *tag_key); +/* Defines the own data property `key` of a namespace object. `attributes` + * has bits 1 writable, 2 enumerable, 4 configurable; a function value + * takes `length` as its length when it is not negative. */ +void scr_dyn_namespace_define(ScrDyn *object, const ScrStr *key, ScrDyn *value, double attributes, + double length); +/* Marks the own property `key` of `object` as a fenced namespace member + * (ScrDynEntry.fenced). */ +void scr_dyn_fence_member(ScrDyn *object, const ScrStr *key); /* A constant-key read of an Object.prototype member name: takes the * ordinary read's +1 result and answers the inherited member in place of * an undefined one (scr_dyn_invoke.c). */ @@ -903,9 +970,17 @@ ScrDyn *scr_dyn_open_iterator(const ScrDyn *value, int *kind, bool generators); * for-of protocol reads it. +1, or NULL with the error pending. */ ScrDyn *scr_dyn_iterator_result_get(const ScrDyn *result, const char *key, size_t len); void scr_dyn_install_iterator_symbol(ScrDyn *key); +/* Symbol.for('nodejs.util.inspect.custom'): the symbol unit installs it + * when the registry creates it; NULL (borrowed) until then. */ +void scr_dyn_install_inspect_custom_symbol(ScrDyn *key); +ScrDyn *scr_dyn_inspect_custom_symbol(void); void scr_dyn_install_has_instance_symbol(ScrDyn *key); void scr_dyn_install_async_iterator_symbol(ScrDyn *key); void scr_dyn_install_async_dispose_symbol(ScrDyn *key); +void scr_dyn_install_species_symbol(ScrDyn *key); +/* Records each well-known symbol the symbol unit creates. */ +void scr_dyn_install_well_known_symbol(ScrDyn *key); +void scr_dyn_install_to_string_tag_symbol(ScrDyn *key); /* True when key is Symbol.asyncDispose (registered by the symbol unit when * the program names it); handle symbol_get ops outside that unit use it. */ bool scr_dyn_is_async_dispose_symbol(const ScrDyn *key); @@ -992,6 +1067,9 @@ ScrDyn *scr_bytes_construct(const ScrDyn *callee, const ScrDyn *args, const ScrS extern SCR_TL bool (*scr_dyn_global_construct_fn)(const ScrDyn *callee, const ScrDyn *args, ScrDyn **result); ScrDyn *scr_dyn_bytes_key_get(const ScrDyn *value, const ScrStr *key); +/* A typed array's, Buffer's or DataView's own named property (+1), or NULL + * without an exception when it has none. */ +ScrDyn *scr_bytes_named_get(const ScrDyn *view, const char *key, size_t key_len); /* The Buffer-flavored twin (stream chunks): string coercion/toString * decode utf8 instead of joining elements. */ ScrDyn *scr_dyn_new_buffer(const ScrBytes *b); @@ -1046,6 +1124,19 @@ void scr_dyn_typed_ref_bind_record(ScrDyn *d, const ScrDynTypedRecordOps *ops); void scr_record_source_note(void *rec, const ScrDyn *src, const char *const *names, const size_t *lens, size_t count); bool scr_record_source_has(const void *rec); +/* A literal that starts with `...src` creates src's keys first, in src's + * order: when src kept a source, the fresh record `dst` (whose shape's + * declared names are `names`) keeps one listing the keys it declares in + * that order. Undeclared keys are not copied, as the literal copies none. */ +void scr_record_source_spread(void *dst, const void *src, const char *const *names, + const size_t *lens, size_t count); +/* Object.freeze of a record a program may freeze: sets SCR_RC_FROZEN in its + * rc word (immortal records are left as they are). */ +void scr_record_freeze(void *record); +/* Node's TypeError for a write to a frozen record: operation 0 assigns the + * existing property `key`, 1 adds it, 2 deletes it. `owner` is the object's + * text in the message ("#"). */ +void scr_record_throw_frozen(ScrStr *key, ScrStr *owner, double operation); /* The commit of a live record view converts the view into a new record * `from`, whose conversion keeps the view's undeclared properties, then * moves its fields into `to`: this moves the kept properties to `to` too, @@ -1056,6 +1147,9 @@ ScrDyn *scr_record_source_apply(ScrDyn *snapshot, const void *rec); /* Attach the prototype member names of a class instance capsule * (idempotent), so `in` sees its methods and accessors. */ void scr_dyn_typed_ref_bind_class(ScrDyn *d, const ScrDynTypedClassOps *ops); +/* A class instance capsule's inherited Symbol.toStringTag string from its + * class's getter (+1); NULL when it has none or the getter threw. */ +ScrStr *scr_dyn_typed_ref_class_tag(const ScrDyn *d); /* Attach the bag and prototype hooks of a capsule of a bag-tracked class * instance (idempotent). */ void scr_dyn_typed_ref_bind_bag_class(ScrDyn *d, const ScrDynTypedBagClassOps *ops); @@ -1153,11 +1247,21 @@ void scr_weak_dispose(void *object); double scr_dyn_arr_len(const ScrDyn *d); ScrDyn *scr_dyn_arr_at(const ScrDyn *d, double i); ScrDyn *scr_dyn_arr_named_get(const ScrDyn *d, const ScrStr *key); +/* The member `key` of an array found before Array.prototype: an own named + * property, or one on its custom prototype chain (Object.setPrototypeOf). + * NULL when the chain reaches Array.prototype without it. +1. */ +ScrDyn *scr_dyn_arr_custom_member(const ScrDyn *d, const char *key, size_t key_len); void scr_dyn_obj_set(ScrDyn *obj, const char *key, size_t key_len, ScrDyn *value); void scr_dyn_obj_set_field(ScrDyn *obj, const char *key, size_t key_len, ScrDyn *value); ScrDyn *scr_dyn_define_property(ScrDyn *target, ScrDyn *key, ScrDyn *descriptor); void scr_dyn_class_field_attributes(ScrDyn *view, ScrStr *name, double bits); void scr_dyn_class_view_attributes(ScrDyn *view, double bits); +/* The integrity state of a class instance's checked view as CLASS_* bits, + * added to `bits`. The commits of hierarchies with an integrity word store + * it in the word, in programs that freeze, seal or prevent extensions of + * untyped values. `bag`, the instance's new property bag or NULL, stops + * being extensible with the view. */ +double scr_dyn_class_view_integrity(const ScrDyn *view, double bits, ScrDyn *bag); ScrDyn *scr_class_define_field(ScrDyn *current, double field_bits, double object_bits, ScrStr *name, ScrDyn *descriptor); double scr_class_defined_bits(ScrDyn *holder, ScrStr *name); @@ -1187,10 +1291,28 @@ void scr_dyn_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value); void scr_dyn_arr_named_put(ScrDyn *arr, const char *key, size_t key_len, ScrDyn *value); void scr_dyn_key_set_computed(ScrDyn *recv, ScrDyn *key, ScrDyn *value); void scr_dyn_key_delete(ScrDyn *recv, const ScrStr *key, bool strict); +/* Removes the own property `key` of an ordinary object whatever its + * attributes, as scr_dyn_obj_set_field replaces values. */ +void scr_dyn_obj_delete_field(ScrDyn *obj, const ScrStr *key); +/* The same for the own symbol-keyed property `symbol` of an ordinary + * object. */ +void scr_dyn_obj_delete_symbol_field(ScrDyn *obj, ScrDyn *symbol); /* This thread's global object box (borrowed). Every spelling of the global * object answers it; its symbol-keyed properties are the global object's * (scr_node_builtin.c). */ ScrDyn *scr_global_object(void); +/* The program's builtin-global lookup, a dyn function (borrowed); the first + * installation stands. */ +void scr_global_values_install(ScrDyn *lookup); +/* An object whose keys are what `for (key in globalThis)` visits (+1). */ +ScrDyn *scr_global_for_in_object(void); +/* Node's navigator global (+1, one box per thread) and its members: + * navigator.language (+1), navigator.languages (+1, one frozen array per + * thread) and navigator.platform (+1). */ +ScrDyn *scr_navigator_object(void); +ScrStr *scr_navigator_language(void); +ScrDyn *scr_navigator_languages(void); +ScrStr *scr_navigator_platform(void); ScrDyn *scr_dyn_global_symbol_get(ScrSym *key); /* borrowed key; +1 value */ void scr_dyn_global_symbol_set(ScrSym *key, ScrDyn *value); /* both borrowed */ bool scr_dyn_global_symbol_has(ScrSym *key); @@ -1306,6 +1428,11 @@ ScrDyn *scr_dyn_from_error(const ScrError *e); void scr_error_register_dyn(const ScrVt *vt, ScrDyn *(*box)(void *)); ScrDyn *scr_error_dyn_fields(const ScrError *e); void scr_error_dyn_place_name(ScrDyn *view, const char *after, size_t after_len); +/* An Error subclass view of an instance whose class keeps a property bag: + * `name` and `message` take their committed positions, and data values + * from the slots (scr_json.c). `name_placed`: the class's construction sets + * `name`, and the view already has it in place. */ +void scr_error_dyn_follow_bag(ScrDyn *view, const ScrDyn *bag, const ScrError *e, bool name_placed); /* An accessor property of a fresh object: enumerable and configurable, * with the given getter and setter (either may be NULL). Moves both. */ void scr_dyn_obj_set_accessor(ScrDyn *obj, const char *key, size_t key_len, ScrDyn *getter, ScrDyn *setter); @@ -1350,6 +1477,9 @@ ScrDyn *scr_dyn_invoke_prepared(ScrDyn *recv, ScrDyn *callee, const char *method * *mid_slot, the site's -1-initialized slot, caches the name's method id * after the first call, so later calls skip the name lookup. */ ScrDyn *scr_dyn_invoke_prepared_id(ScrDyn *recv, ScrDyn *callee, const char *method, int32_t *mid_slot, ScrDyn *const *args, size_t argc, const char *what); +/* scr_dyn_invoke_prepared_id for a call whose result is discarded: a plain + * array's push appends without boxing its new length. */ +void scr_dyn_invoke_discard_id(ScrDyn *recv, ScrDyn *callee, const char *method, int32_t *mid_slot, ScrDyn *const *args, size_t argc, const char *what); /* scr_dyn_invoke_prepared_id for Buffer.prototype method names * (scr_bytes_dyn.c), one entry per method family: a Buffer receiver runs * the method, any other takes the ordinary dispatch. */ @@ -1360,11 +1490,25 @@ ScrDyn *scr_dyn_invoke_buffer_to_string(ScrDyn *recv, ScrDyn *callee, const char ScrDyn *scr_dyn_invoke_buffer_search(ScrDyn *recv, ScrDyn *callee, const char *method, int32_t *mid_slot, ScrDyn *const *args, size_t argc, const char *what); ScrDyn *scr_dyn_invoke_buffer(ScrDyn *recv, ScrDyn *callee, const char *method, int32_t *mid_slot, ScrDyn *const *args, size_t argc, const char *what); ScrDyn *scr_dyn_invoke(ScrDyn *recv, const char *method, ScrDyn *const *args, size_t argc, const char *what); +/* A %TypedArray%.prototype method called on a checked typed array (not a + * DataView): its answer (+1, or NULL with the exception pending), with + * *handled false for a name it does not implement. */ +ScrDyn *scr_dyn_bytes_method(ScrDyn *recv, const char *method, ScrDyn *const *args, size_t argc, bool *handled); +/* A plain object's Symbol.iterator method (+1), read once; NULL when it + * has none, or with the exception pending from its getter. */ +ScrDyn *scr_dyn_iterator_method(const ScrDyn *value); +/* The values the iterator `method` gives for `source` (+1), or NULL with + * the exception pending. */ +ScrDyn *scr_dyn_iterate_with(const ScrDyn *source, ScrDyn *method); /* Keyed read on a FUNC node: the own-property table first (defineProperties * writes land there), then "name" (the box's best-effort static name; "" * for anonymous) and "length" (the boxed arity). Returns +1, or NULL when * the key answers nothing (the caller's undefined). Never throws. */ ScrDyn *scr_dyn_fn_get(const ScrDyn *d, const char *key, size_t key_len); +/* A function value's own property table, created on first use. +1. */ +ScrDyn *scr_dyn_function_properties(const ScrDyn *function); +/* The table a function value already has. +1, or NULL. */ +ScrDyn *scr_dyn_function_properties_existing(const ScrDyn *function); /* Object.defineProperties over dyn values (targets: OBJ and FUNC): each * descriptor's `value` becomes an own data property. OBJ targets preserve * writable/enumerable/configurable; FUNC targets still use plain properties. @@ -1380,6 +1524,9 @@ ScrDyn *scr_dyn_new_func(ScrClosure *clo, ScrDynThunk thunk, uint32_t arity, con /* Gives a function box made with thunk its typed message entry (an * EventEmitter listener registered from it keeps the entry). */ void scr_dyn_func_set_message(ScrDyn *fn, ScrDynThunk thunk, ScrDynMessageListener listener); +/* Sets a freshly boxed function's `length` (a builtin function value whose + * length is not its adapter's parameter count). */ +void scr_dyn_function_set_length(ScrDyn *fn, uint32_t length); ScrDyn *scr_dyn_new_class(ScrClassObj *cls, const char *type_key); /* EventEmitter and the stream constructors as untyped constructor values, * and instanceof over untyped constructor functions @@ -1420,6 +1567,9 @@ ScrDyn *scr_dyn_builtin_stream_object(struct ScrStream *s); ScrDyn *scr_dyn_builtin_stream_view(struct ScrStream *s); struct ScrStream *scr_dyn_builtin_stream_check(const ScrDyn *value, double kind); struct ScrStream *scr_dyn_builtin_stream_unbox(const ScrDyn *value, const ScrStr *message); +/* stream.unboxDynStage in those programs (scr_stream_unbox_dyn_stage's + * contract, also taking constructor-function streams). */ +struct ScrStream *scr_dyn_builtin_stream_unbox_stage(const ScrDyn *value, const ScrStr *message); /* stream.dynNative in those programs: scr_stream_dyn_native's contract, * also taking constructor-function streams that keep the runtime's write * and end (+1 or NULL). */ @@ -1468,6 +1618,13 @@ ScrClassObj *scr_dyn_class_check(const ScrDyn *value, const char *type_key, cons * prints as "Uncaught TypeError: ..." and exits 1). This is * scriptc-specific behavior — JS `as` never checks (SEMANTICS.md). */ void scr_dyn_check_fail(const ScrDynPath *path, const char *want, const ScrDyn *got); +/* The Date object a checked-dynamic Date handle holds, +1, or NULL with + * the check failure pending. */ +ScrDate *scr_dyn_native_date_check(const ScrDyn *value, const ScrDynPath *path); +/* Names the analysis-proven fact the next checked conversion checks + * (shape inference's overlay), or NULL: its failure then reports a compiler + * analysis bug (" :"). */ +void scr_dyn_inferred_site(const char *site); /* Matchers and builders of recursive shapes (records and arrays whose type * reaches itself) track the dyn values they visit, so cyclic values match @@ -1504,6 +1661,19 @@ void scr_dyn_build_note(const ScrDyn *d, const void *builder, void *value); void scr_dyn_build_rekey(const ScrDyn *d, const void *builder, void *value); ScrDyn *scr_dyn_visit_view(const ScrDyn *capsule); +/* A union's builder converts the arm whose literal discriminator and + * complete structure matched; every value inside that arm then matched its + * own type too. Between scr_dyn_trust_enter and scr_dyn_trust_exit (which + * restores the value enter returned), scr_dyn_trusted_build answers true, + * and the builder of a nested union whose arms' literals are disjoint picks + * a plain object's arm by its literal alone instead of matching the arm's + * structure again (dyn.ts unionArmMatch). Code a getter or proxy trap runs + * sees false, and so does the rest of the conversion once such code has + * run: it can return a value the match never saw. */ +bool scr_dyn_trust_enter(void); +void scr_dyn_trust_exit(bool previous); +bool scr_dyn_trusted_build(void); + /* ── native handles in the checked-dynamic tree (SCR_DYN_HANDLE) ──────────────────────── * Per-tag dispatch ops, registered by the handle's owning unit * (scr_http_dyn_install / scr_net_dyn_install — emitted main() calls @@ -1569,6 +1739,12 @@ typedef struct ScrDynHandleOps { /* Past the depth limit util.inspect prints `[cls]` (the class's * inspector returns `this`) instead of the bare class name. */ bool inspect_bracketed_past_depth; + /* The class's inspector formats its view with a new util.inspect call + * (URL's does): the view renders from the top level with the caller's + * depth, and its lines take the caller's indentation. Past the depth + * limit the inspector returns the instance, which has no own + * properties, so util.inspect prints `cls {}`. */ + bool inspect_view_nested; /* The @@toStringTag when it differs from cls: String() answers * "[object ]" and util.inspect prints ` [] `, or * ` ` when the tag ends with "." and cls, as Node does. */ @@ -1797,7 +1973,8 @@ extern void scr_dyn_listener_fire_data(ScrClosure *cb, ScrBytes *chunk); extern void scr_dyn_listener_fire_err(ScrClosure *cb, ScrStr *msg); void *scr_dyn_retain_v(void *d); -/* After a compiled fast path decremented a surviving capsule or handle. */ +/* After a compiled fast path decremented a surviving value of a kind that + * can be a cycle candidate and is not one yet (scr_json.c). */ void scr_dyn_release_survivor(ScrDyn *d); void scr_dyn_release_v(void *d); void scr_dyn_trace_v(void *d, ScrTraceVisit visit, void *ctx); @@ -1811,6 +1988,9 @@ long scr_dyn_live_count(void); * scr_json.c, which programs with dynamic-tier code link. */ /* The builtin Object.prototype node, or NULL before its first use. */ const ScrDyn *scr_dyn_tier_object_prototype(void); +/* A read site's answer for a native array's length or a native iterator's + * next (scr_js_get_probe_native): +1, or NULL. */ +ScrDyn *scr_dyn_tier_unshaped_get(const ScrDyn *receiver, const ScrStr *key); /* Shared key bytes for `key` (+1, NUL-terminated), and their release. */ char *scr_dyn_tier_key_copy(const char *key, size_t key_len); void scr_dyn_tier_key_release(char *key, size_t key_len); diff --git a/packages/runtime/src/scr_dyn_builtin_stream.c b/packages/runtime/src/scr_dyn_builtin_stream.c index 2875cfa06..52c1e7269 100644 --- a/packages/runtime/src/scr_dyn_builtin_stream.c +++ b/packages/runtime/src/scr_dyn_builtin_stream.c @@ -1752,3 +1752,11 @@ ScrStream *scr_dyn_builtin_stream_unbox(const ScrDyn *value, const ScrStr *messa } return scr_stream_unbox_dyn(value, message); } + +/* stream.unboxDynStage in programs that link this unit: a + * constructor-function stream as scr_dyn_builtin_stream_unbox takes it, + * any other value as scr_stream_unbox_dyn_stage does. */ +ScrStream *scr_dyn_builtin_stream_unbox_stage(const ScrDyn *value, const ScrStr *message) { + if (bcs_of(value) && value->kind == SCR_DYN_OBJ) return scr_dyn_builtin_stream_unbox(value, message); + return scr_stream_unbox_dyn_stage(value, message); +} diff --git a/packages/runtime/src/scr_dyn_for_await.c b/packages/runtime/src/scr_dyn_for_await.c index 3a1a6458d..a313f040b 100644 --- a/packages/runtime/src/scr_dyn_for_await.c +++ b/packages/runtime/src/scr_dyn_for_await.c @@ -44,7 +44,11 @@ void scr_dyn_async_iterator_set_sync(bool sync) { } /* Whether the iterator the last scr_dyn_async_iterator_open returned is a - * synchronous one. Read immediately after the open. */ + * synchronous one. Read immediately after the open; the read clears it, so + * an iterator a class's [Symbol.asyncIterator]() returned (opened without + * the runtime) reads false. */ bool scr_dyn_async_iterator_was_sync(void) { - return scr_for_await_sync; + bool sync = scr_for_await_sync; + scr_for_await_sync = false; + return sync; } diff --git a/packages/runtime/src/scr_dyn_invoke.c b/packages/runtime/src/scr_dyn_invoke.c index 364017e05..7a9b1bc35 100644 --- a/packages/runtime/src/scr_dyn_invoke.c +++ b/packages/runtime/src/scr_dyn_invoke.c @@ -144,6 +144,10 @@ static void scr_dyn_display_buf(ScrJsonBuf *b, const ScrDyn *d, bool to_string) return; case SCR_DYN_TYPED_REF: { + /* ToString of a class instance that converts through + * Object.prototype.toString shows its Symbol.toStringTag getter's + * string. */ + if (to_string && scr_dyn_typed_ref_put_tag(b, d)) return; ScrDyn *materialized = scr_dyn_typed_ref_materialize(d); scr_dyn_display_buf(b, materialized, to_string); scr_dyn_release(materialized); @@ -155,6 +159,10 @@ static void scr_dyn_display_buf(ScrJsonBuf *b, const ScrDyn *d, bool to_string) } } +void scr_jb_join_dyn(ScrJsonBuf *b, const ScrDyn *d) { + if (d->kind != SCR_DYN_UNDEF && d->kind != SCR_DYN_NULL) scr_dyn_display_buf(b, d, true); +} + ScrStr *scr_dyn_display(const ScrDyn *d) { ScrJsonBuf b; scr_jb_init(&b); @@ -191,13 +199,6 @@ static double dyn_index_arg(ScrDyn *const *args, size_t argc, size_t i, double d return isnan(number) ? 0 : trunc(number); } -static double dyn_bytes_index_arg(ScrDyn *const *args, size_t argc, size_t index, double fallback) { - if (index >= argc || args[index]->kind == SCR_DYN_UNDEF) return fallback; - double value; - if (!scr_dyn_number_coerce_js(args[index], &value)) return 0; - return isnan(value) ? 0 : trunc(value); -} - /* JS relative-index normalization (slice's rule). */ static size_t dyn_rel_index(double rel, size_t len) { if (rel < 0) { @@ -716,7 +717,8 @@ static bool dyn_bytes_proto_real(int m) { case DYN_M_every: case DYN_M_find: case DYN_M_findIndex: case DYN_M_reverse: case DYN_M_fill: case DYN_M_set: case DYN_M_subarray: case DYN_M_sort: case DYN_M_keys: case DYN_M_values: case DYN_M_entries: case DYN_M_reduce: case DYN_M_reduceRight: case DYN_M_copyWithin: - case DYN_M_toString: case DYN_M_toLocaleString: + case DYN_M_toString: case DYN_M_toLocaleString: case DYN_M_findLast: case DYN_M_findLastIndex: + case DYN_M_toReversed: case DYN_M_toSorted: case DYN_M_with: return true; default: return false; @@ -740,6 +742,8 @@ static ScrDyn *dyn_live_array_method(ScrDyn *recv, int mid, ScrDyn *const *args, if (!ops) return NULL; ScrArr *arr = recv->v.typed_ref.ptr; size_t len = arr->len; + /* A frozen array's view refuses the write. */ + if (scr_arr_frozen(arr) && ((mid == DYN_M_push) || (mid == DYN_M_pop))) return NULL; if ((mid == DYN_M_push) && argc == 1 && ops->set) { if (!ops->set(arr, (double)len, args[0])) return NULL; *handled = true; @@ -775,6 +779,8 @@ ScrDyn *scr_dyn_prepare_method(ScrDyn *recv, const char *method) { return scr_dyn_invoke(recv, method, NULL, 0, method); if (recv->kind == SCR_DYN_OBJ) return scr_dyn_obj_read(recv, method, strlen(method)); + if (recv->kind == SCR_DYN_ARR && (recv->v.arr.properties || recv->prototype)) + return scr_dyn_arr_custom_member(recv, method, strlen(method)); if (recv->kind == SCR_DYN_PROXY) { ScrStr *key = scr_str_new(method, strlen(method)); ScrDyn *result = scr_dyn_proxy_get(recv, key); @@ -799,20 +805,228 @@ ScrDyn *scr_dyn_invoke(ScrDyn *recv, const char *method, return scr_dyn_invoke_impl(recv, method, dyn_method_of(method), args, argc, what, NULL); } -ScrDyn *scr_dyn_invoke_prepared_id(ScrDyn *recv, ScrDyn *callee, const char *method, - int32_t *mid_slot, ScrDyn *const *args, size_t argc, - const char *what) { - if (callee) return scr_dyn_invoke_prepared(recv, callee, method, args, argc, what); - /* The slot is the call site's, shared by every thread: each writes the - * same id. */ +/* A dense array without holes, named properties or a prototype of its + * own, whose length and elements are writable: its common methods below + * run without the dispatch ladder, which answers every other receiver and + * method as before. */ +static bool dyn_arr_plain(const ScrDyn *recv) { + return recv->kind == SCR_DYN_ARR && !recv->v.arr.presence && !recv->v.arr.properties && + !recv->prototype && !recv->null_proto && !recv->non_extensible && + !recv->v.arr.sealed && !recv->v.arr.frozen && !scr_dyn_is_array_prototype(recv); +} + +/* scr_dyn_strict_eq, with the number and string cases inline: an element + * search compares every element. */ +static inline bool dyn_elem_strict_eq(const ScrDyn *a, const ScrDyn *b) { + if (a->kind == SCR_DYN_STR && b->kind == SCR_DYN_STR) { + const ScrStr *x = a->v.str, *y = b->v.str; + return x == y || (x->len == y->len && memcmp(x->data, y->data, x->len) == 0); + } + if (a->kind == SCR_DYN_NUM && b->kind == SCR_DYN_NUM) return a->v.num == b->v.num; + return scr_dyn_strict_eq(a, b); +} + +/* push, pop, shift, at, includes and indexOf of a plain array, with the + * ladder's results. An argument that would run user code (a non-number + * index, a fromIndex) leaves the call to the ladder. True when answered. */ +static bool dyn_arr_fast(ScrDyn *recv, int mid, ScrDyn *const *args, size_t argc, + ScrDyn **result) { + size_t len = recv->v.arr.len; + switch (mid) { + case DYN_M_push: + for (size_t i = 0; i < argc; i++) scr_dyn_arr_push(recv, scr_dyn_retain(args[i])); + *result = scr_dyn_new_num((double)recv->v.arr.len); + return true; + case DYN_M_pop: + if (len == 0) { + *result = scr_dyn_retain(scr_dyn_undefined()); + return true; + } + recv->v.arr.len = len - 1; + *result = recv->v.arr.items[len - 1]; /* ownership moves out */ + return true; + case DYN_M_shift: + if (len == 0) { + *result = scr_dyn_retain(scr_dyn_undefined()); + return true; + } + *result = recv->v.arr.items[0]; /* ownership moves out */ + memmove(recv->v.arr.items, recv->v.arr.items + 1, (len - 1) * sizeof(ScrDyn *)); + recv->v.arr.len = len - 1; + return true; + case DYN_M_at: { + if (argc == 0 || args[0]->kind != SCR_DYN_NUM) return false; + double index = args[0]->v.num; + index = isnan(index) ? 0 : trunc(index); + if (index < 0) index += (double)len; + *result = index < 0 || index >= (double)len ? scr_dyn_retain(scr_dyn_undefined()) + : scr_dyn_retain(recv->v.arr.items[(size_t)index]); + return true; + } + case DYN_M_includes: + case DYN_M_indexOf: { + if (argc > 1) return false; + ScrDyn *needle = argc > 0 ? args[0] : scr_dyn_undefined(); + bool nan = mid == DYN_M_includes && needle->kind == SCR_DYN_NUM && needle->v.num != needle->v.num; + for (size_t i = 0; i < len; i++) { + const ScrDyn *item = recv->v.arr.items[i]; + if (nan ? item->kind == SCR_DYN_NUM && item->v.num != item->v.num + : dyn_elem_strict_eq(item, needle)) { + *result = mid == DYN_M_includes ? scr_dyn_new_bool(true) : scr_dyn_new_num((double)i); + return true; + } + } + *result = mid == DYN_M_includes ? scr_dyn_new_bool(false) : scr_dyn_new_num(-1); + return true; + } + default: + return false; + } +} + +/* The call site's method id. The slot is the site's, shared by every + * thread: each writes the same id. */ +static int32_t dyn_site_method(const char *method, int32_t *mid_slot) { int32_t mid = __atomic_load_n(mid_slot, __ATOMIC_RELAXED); if (mid < 0) { mid = dyn_method_of(method); __atomic_store_n(mid_slot, mid, __ATOMIC_RELAXED); } + return mid; +} + +ScrDyn *scr_dyn_invoke_prepared_id(ScrDyn *recv, ScrDyn *callee, const char *method, + int32_t *mid_slot, ScrDyn *const *args, size_t argc, + const char *what) { + if (callee) return scr_dyn_invoke_prepared(recv, callee, method, args, argc, what); + int32_t mid = dyn_site_method(method, mid_slot); + ScrDyn *result; + if (dyn_arr_plain(recv) && dyn_arr_fast(recv, mid, args, argc, &result)) return result; return scr_dyn_invoke_impl(recv, method, mid, args, argc, what, NULL); } +void scr_dyn_invoke_discard_id(ScrDyn *recv, ScrDyn *callee, const char *method, + int32_t *mid_slot, ScrDyn *const *args, size_t argc, + const char *what) { + /* A plain array's push whose new length nobody reads. */ + if (!callee && dyn_arr_plain(recv) && dyn_site_method(method, mid_slot) == DYN_M_push) { + for (size_t i = 0; i < argc; i++) scr_dyn_arr_push(recv, scr_dyn_retain(args[i])); + return; + } + scr_dyn_release(scr_dyn_invoke_prepared_id(recv, callee, method, mid_slot, args, argc, what)); +} + +/* The callback methods of a dense dyn array that take a thisArg: + * forEach, map, filter, some, every, find, findIndex, findLast, + * findLastIndex and flatMap. The caller binds `this` for the callbacks. + * `len` is the length before the first callback. */ +static ScrDyn *dyn_arr_callback_method(ScrDyn *recv, int mid, ScrDyn *const *args, size_t argc, + size_t len, ScrDyn *callback_recv) { + if ((mid == DYN_M_findLast) || (mid == DYN_M_findLastIndex)) { + if (!dyn_cb_check(args, argc)) return NULL; + ScrDyn *visible_recv = callback_recv ? callback_recv : recv; + for (size_t i = len; i > 0; i--) { + size_t index = i - 1; + ScrDyn *item = index < recv->v.arr.len + ? scr_dyn_retain(recv->v.arr.items[index]) + : scr_dyn_retain(scr_dyn_undefined()); + ScrDyn *result = dyn_call_cb(args[0], item, index, visible_recv); + if (!result) { scr_dyn_release(item); return NULL; } + bool matched = scr_dyn_truthy(result); + scr_dyn_release(result); + if (matched) { + if ((mid == DYN_M_findLast)) return item; + scr_dyn_release(item); + return scr_dyn_new_num((double)index); + } + scr_dyn_release(item); + } + return (mid == DYN_M_findLast) + ? scr_dyn_retain(scr_dyn_undefined()) : scr_dyn_new_num(-1); + } + if ((mid == DYN_M_forEach) || (mid == DYN_M_map) || + (mid == DYN_M_filter) || (mid == DYN_M_some) || + (mid == DYN_M_every) || (mid == DYN_M_find) || + (mid == DYN_M_findIndex)) { + if (!dyn_cb_check(args, argc)) return NULL; + ScrDyn *cb = args[0]; + ScrDyn *out = ((mid == DYN_M_map) || (mid == DYN_M_filter)) ? scr_dyn_new_arr() : NULL; + /* Array iteration methods snapshot length before the first callback. + * A shrinking callback can remove a later dense entry; an expanding + * callback must not make the new tail observable to this pass. Live + * Web-boundary capsules are the externally visible receiver, so pass + * that capsule as callback arg 3: mutations through the callback's + * array argument then commit directly to the original static array. */ + size_t n = len; + ScrDyn *visible_recv = callback_recv ? callback_recv : recv; + for (size_t step = 0; step < n; step++) { + size_t i = step; + if (i >= recv->v.arr.len) continue; + ScrDyn *item = scr_dyn_retain(recv->v.arr.items[i]); + ScrDyn *r = dyn_call_cb(cb, item, i, visible_recv); + if (!r) { scr_dyn_release(item); scr_dyn_release(out); return NULL; } + if ((mid == DYN_M_map)) { + scr_dyn_arr_push(out, r); /* ownership moves in */ + r = NULL; + } else { + bool truthy = scr_dyn_truthy(r); + scr_dyn_release(r); + if ((mid == DYN_M_filter) && truthy) scr_dyn_arr_push(out, scr_dyn_retain(item)); + if ((mid == DYN_M_some) && truthy) { scr_dyn_release(item); return scr_dyn_new_bool(true); } + if ((mid == DYN_M_every) && !truthy) { scr_dyn_release(item); return scr_dyn_new_bool(false); } + if ((mid == DYN_M_find) && truthy) return item; /* +1 moves out */ + if ((mid == DYN_M_findIndex) && truthy) { scr_dyn_release(item); return scr_dyn_new_num((double)i); } + } + scr_dyn_release(item); + } + if (out) return out; + if ((mid == DYN_M_some)) return scr_dyn_new_bool(false); + if ((mid == DYN_M_every)) return scr_dyn_new_bool(true); + if ((mid == DYN_M_find)) return scr_dyn_retain(scr_dyn_undefined()); + if ((mid == DYN_M_findIndex)) return scr_dyn_new_num(-1); + return scr_dyn_retain(scr_dyn_undefined()); /* forEach */ + } + if ((mid == DYN_M_flatMap)) { + /* JS Array.prototype.flatMap over a dyn array: map + a depth-1 + * flatten. Native dyn-array results flatten element-by-element; a + * WRAPPED engine array flattens through the routed keyed reads + * (elements wrap back scalar-normalized); everything else pushes + * as a single element (JS keeps non-array callback results whole). + * JS's spec snapshots the length up front (elements appended by + * the callback are not visited). */ + if (!dyn_cb_check(args, argc)) return NULL; + ScrDyn *cb = args[0]; + ScrDyn *out = scr_dyn_new_arr(); + size_t n = recv->v.arr.len; + for (size_t i = 0; i < n && i < recv->v.arr.len; i++) { + if (!dyn_borrow_present(recv, i)) { if (scr_exc_pending()) { scr_dyn_release(out); return NULL; } continue; } + ScrDyn *item = dyn_borrow_item(recv, i); + if (!item) { scr_dyn_release(out); return NULL; } + ScrDyn *r = dyn_call_cb( + cb, item, i, callback_recv ? callback_recv : recv); + scr_dyn_release(item); + if (!r) { scr_dyn_release(out); return NULL; } + if (r->kind == SCR_DYN_TYPED_REF && scr_dyn_ref_is_array(r)) { + ScrDyn *view = scr_dyn_typed_ref_materialize(r); + scr_dyn_release(r); + r = view; + if (!r) { scr_dyn_release(out); return NULL; } + } + if (r->kind == SCR_DYN_ARR) { + for (size_t j = 0; j < r->v.arr.len; j++) { + if (dyn_borrow_present(r, j)) scr_dyn_arr_push(out, dyn_borrow_item(r, j)); + if (scr_exc_pending()) { scr_dyn_release(r); scr_dyn_release(out); return NULL; } + } + scr_dyn_release(r); + } else { + scr_dyn_arr_push(out, r); /* ownership moves in */ + } + } + return out; + } + return NULL; +} + static ScrDyn *scr_dyn_invoke_impl( ScrDyn *recv, const char *method, int mid, ScrDyn *const *args, size_t argc, const char *what, ScrDyn *callback_recv) { @@ -1112,16 +1326,28 @@ static ScrDyn *scr_dyn_invoke_impl( return NULL; } } - if (recv->v.arr.frozen && ((mid == DYN_M_push) || - (mid == DYN_M_pop) || (mid == DYN_M_shift) || - (mid == DYN_M_unshift) || (mid == DYN_M_splice))) { - static const char message[] = "Cannot assign to read only property 'length'"; - scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + /* A frozen array throws at the operation's first write, as in Node. */ + int frozen_op = !recv->v.arr.frozen ? -1 + : (mid == DYN_M_push) ? 1 + : (mid == DYN_M_pop) ? 2 + : (mid == DYN_M_shift) ? 6 + : (mid == DYN_M_unshift) ? 4 + : (mid == DYN_M_splice) ? 10 + : ((mid == DYN_M_sort) || (mid == DYN_M_reverse)) ? 7 + : -1; + double items = (mid == DYN_M_splice) ? (argc > 2 ? (double)(argc - 2) : 0) : (double)argc; + if (frozen_op >= 0 && + scr_array_frozen_error(frozen_op, len, (double)splice_start, frozen_op == 10 ? (double)splice_removed : items, items)) + return NULL; + if ((mid == DYN_M_push) && grows && recv->non_extensible) { + scr_array_not_extensible_error((double)len); return NULL; } - if ((grows && recv->non_extensible) || (shrinks && recv->v.arr.sealed && !(mid == DYN_M_shift)) || - (recv->v.arr.frozen && (((mid == DYN_M_sort) && len > 1) || - ((mid == DYN_M_reverse) && len > 1)))) { + if ((mid == DYN_M_pop) && shrinks && recv->v.arr.sealed) { + scr_array_undeletable_error((double)(len - 1)); + return NULL; + } + if ((grows && recv->non_extensible) || (shrinks && recv->v.arr.sealed && !(mid == DYN_M_shift))) { static const char message[] = "Cannot modify a sealed or frozen array"; scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); return NULL; @@ -1150,8 +1376,7 @@ static ScrDyn *scr_dyn_invoke_impl( scr_dyn_release(recv->v.arr.items[i - 1]); recv->v.arr.items[i - 1] = scr_dyn_retain(recv->v.arr.items[i]); } - static const char message[] = "Cannot delete non-configurable property"; - scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + scr_array_undeletable_error((double)(len - 1)); return NULL; } ScrDyn *first = recv->v.arr.items[0]; @@ -1253,27 +1478,16 @@ static ScrDyn *scr_dyn_invoke_impl( } return (mid == DYN_M_includes) ? scr_dyn_new_bool(false) : scr_dyn_new_num(-1); } - if ((mid == DYN_M_findLast) || (mid == DYN_M_findLastIndex)) { - if (!dyn_cb_check(args, argc)) return NULL; - ScrDyn *visible_recv = callback_recv ? callback_recv : recv; - for (size_t i = len; i > 0; i--) { - size_t index = i - 1; - ScrDyn *item = index < recv->v.arr.len - ? scr_dyn_retain(recv->v.arr.items[index]) - : scr_dyn_retain(scr_dyn_undefined()); - ScrDyn *result = dyn_call_cb(args[0], item, index, visible_recv); - if (!result) { scr_dyn_release(item); return NULL; } - bool matched = scr_dyn_truthy(result); - scr_dyn_release(result); - if (matched) { - if ((mid == DYN_M_findLast)) return item; - scr_dyn_release(item); - return scr_dyn_new_num((double)index); - } - scr_dyn_release(item); - } - return (mid == DYN_M_findLast) - ? scr_dyn_retain(scr_dyn_undefined()) : scr_dyn_new_num(-1); + if ((mid == DYN_M_forEach) || (mid == DYN_M_map) || (mid == DYN_M_filter) || + (mid == DYN_M_some) || (mid == DYN_M_every) || (mid == DYN_M_find) || + (mid == DYN_M_findIndex) || (mid == DYN_M_findLast) || (mid == DYN_M_findLastIndex) || + (mid == DYN_M_flatMap)) { + /* Every callback runs with `this` bound to the thisArg, or undefined + * when there is none. */ + scr_dyn_this_push_dyn(argc > 1 ? args[1] : scr_dyn_undefined()); + ScrDyn *result = dyn_arr_callback_method(recv, mid, args, argc, len, callback_recv); + scr_dyn_this_pop(); + return result; } if ((mid == DYN_M_join)) { ScrJsonBuf b; @@ -1390,11 +1604,7 @@ static ScrDyn *scr_dyn_invoke_impl( if (!dyn_arr_length_unchanged(recv, len)) return NULL; size_t start = dyn_rel_index(startD, len); size_t end = dyn_rel_index(endD, len); - if (recv->v.arr.frozen && start < end) { - static const char message[] = "Cannot assign to read only array index"; - scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); - return NULL; - } + if (recv->v.arr.frozen && scr_array_frozen_error(8, len, (double)start, (double)end, 0)) return NULL; ScrDyn *fill = argc ? args[0] : scr_dyn_undefined(); if (start < end) scr_dyn_region_store(recv, fill); for (size_t i = start; i < end; i++) { @@ -1423,11 +1633,8 @@ static ScrDyn *scr_dyn_invoke_impl( size_t end = dyn_rel_index(endD, len); size_t count = end > start ? end - start : 0; if (count > len - target) count = len - target; - if (recv->v.arr.frozen && count > 0) { - static const char message[] = "Cannot assign to read only array index"; - scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + if (recv->v.arr.frozen && scr_array_frozen_error(9, len, (double)target, (double)start, (double)end)) return NULL; - } if (start < target && target < start + count) { for (size_t i = count; i > 0; i--) { ScrDyn *old = recv->v.arr.items[target + i - 1]; @@ -1443,48 +1650,6 @@ static ScrDyn *scr_dyn_invoke_impl( } return scr_dyn_retain(recv); } - if ((mid == DYN_M_forEach) || (mid == DYN_M_map) || - (mid == DYN_M_filter) || (mid == DYN_M_some) || - (mid == DYN_M_every) || (mid == DYN_M_find) || - (mid == DYN_M_findIndex)) { - if (!dyn_cb_check(args, argc)) return NULL; - ScrDyn *cb = args[0]; - ScrDyn *out = ((mid == DYN_M_map) || (mid == DYN_M_filter)) ? scr_dyn_new_arr() : NULL; - /* Array iteration methods snapshot length before the first callback. - * A shrinking callback can remove a later dense entry; an expanding - * callback must not make the new tail observable to this pass. Live - * Web-boundary capsules are the externally visible receiver, so pass - * that capsule as callback arg 3: mutations through the callback's - * array argument then commit directly to the original static array. */ - size_t n = len; - ScrDyn *visible_recv = callback_recv ? callback_recv : recv; - for (size_t step = 0; step < n; step++) { - size_t i = step; - if (i >= recv->v.arr.len) continue; - ScrDyn *item = scr_dyn_retain(recv->v.arr.items[i]); - ScrDyn *r = dyn_call_cb(cb, item, i, visible_recv); - if (!r) { scr_dyn_release(item); scr_dyn_release(out); return NULL; } - if ((mid == DYN_M_map)) { - scr_dyn_arr_push(out, r); /* ownership moves in */ - r = NULL; - } else { - bool truthy = scr_dyn_truthy(r); - scr_dyn_release(r); - if ((mid == DYN_M_filter) && truthy) scr_dyn_arr_push(out, scr_dyn_retain(item)); - if ((mid == DYN_M_some) && truthy) { scr_dyn_release(item); return scr_dyn_new_bool(true); } - if ((mid == DYN_M_every) && !truthy) { scr_dyn_release(item); return scr_dyn_new_bool(false); } - if ((mid == DYN_M_find) && truthy) return item; /* +1 moves out */ - if ((mid == DYN_M_findIndex) && truthy) { scr_dyn_release(item); return scr_dyn_new_num((double)i); } - } - scr_dyn_release(item); - } - if (out) return out; - if ((mid == DYN_M_some)) return scr_dyn_new_bool(false); - if ((mid == DYN_M_every)) return scr_dyn_new_bool(true); - if ((mid == DYN_M_find)) return scr_dyn_retain(scr_dyn_undefined()); - if ((mid == DYN_M_findIndex)) return scr_dyn_new_num(-1); - return scr_dyn_retain(scr_dyn_undefined()); /* forEach */ - } if ((mid == DYN_M_reduce) || (mid == DYN_M_reduceRight)) { if (!dyn_cb_check(args, argc)) return NULL; bool backward = (mid == DYN_M_reduceRight); @@ -1509,44 +1674,6 @@ static ScrDyn *scr_dyn_invoke_impl( scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); return NULL; } - if ((mid == DYN_M_flatMap)) { - /* JS Array.prototype.flatMap over a dyn array: map + a depth-1 - * flatten. Native dyn-array results flatten element-by-element; a - * WRAPPED engine array flattens through the routed keyed reads - * (elements wrap back scalar-normalized); everything else pushes - * as a single element (JS keeps non-array callback results whole). - * JS's spec snapshots the length up front (elements appended by - * the callback are not visited). */ - if (!dyn_cb_check(args, argc)) return NULL; - ScrDyn *cb = args[0]; - ScrDyn *out = scr_dyn_new_arr(); - size_t n = recv->v.arr.len; - for (size_t i = 0; i < n && i < recv->v.arr.len; i++) { - if (!dyn_borrow_present(recv, i)) { if (scr_exc_pending()) { scr_dyn_release(out); return NULL; } continue; } - ScrDyn *item = dyn_borrow_item(recv, i); - if (!item) { scr_dyn_release(out); return NULL; } - ScrDyn *r = dyn_call_cb( - cb, item, i, callback_recv ? callback_recv : recv); - scr_dyn_release(item); - if (!r) { scr_dyn_release(out); return NULL; } - if (r->kind == SCR_DYN_TYPED_REF && scr_dyn_ref_is_array(r)) { - ScrDyn *view = scr_dyn_typed_ref_materialize(r); - scr_dyn_release(r); - r = view; - if (!r) { scr_dyn_release(out); return NULL; } - } - if (r->kind == SCR_DYN_ARR) { - for (size_t j = 0; j < r->v.arr.len; j++) { - if (dyn_borrow_present(r, j)) scr_dyn_arr_push(out, dyn_borrow_item(r, j)); - if (scr_exc_pending()) { scr_dyn_release(r); scr_dyn_release(out); return NULL; } - } - scr_dyn_release(r); - } else { - scr_dyn_arr_push(out, r); /* ownership moves in */ - } - } - return out; - } if ((mid == DYN_M_sort)) { ScrDyn *cmp = argc > 0 ? args[0] : scr_dyn_undefined(); if (cmp->kind != SCR_DYN_UNDEF && cmp->kind != SCR_DYN_FUNC) { @@ -1609,6 +1736,20 @@ static ScrDyn *scr_dyn_invoke_impl( if (recv->kind == SCR_DYN_BYTES) { ScrBytes *bytes = recv->v.bytes; + /* An own property of the view shadows the prototype's method. */ + ScrDyn *member = bytes->properties ? scr_bytes_named_get(recv, method, strlen(method)) : NULL; + if (!member && scr_exc_pending()) return NULL; + if (member) { + ScrDyn *result = NULL; + if (!scr_dyn_is_callable(member)) dyn_throw_not_fn(what); + else { + scr_dyn_this_push_dyn(recv); + result = scr_dyn_call(member, args, argc, what); + scr_dyn_this_pop(); + } + scr_dyn_release(member); + return result; + } if (bytes->is_data_view) { static const char *const kinds[] = { "Uint8", "Int8", "Uint16", "Int16", "Uint32", "Int32", "Float32", "Float64", "BigUint64", "BigInt64", "Float16" }; const bool get = !strncmp(method, "get", 3); @@ -1661,63 +1802,9 @@ static ScrDyn *scr_dyn_invoke_impl( dyn_throw_not_fn(what); return NULL; } - size_t blen = bytes->len; - if ((mid == DYN_M_at)) { - double iD = dyn_bytes_index_arg(args, argc, 0, 0); - if (scr_exc_pending()) return NULL; - double idx = iD < 0 ? (double)blen + iD : iD; - if (idx < 0 || idx >= (double)blen) return scr_dyn_retain(scr_dyn_undefined()); - return scr_dyn_new_num(scr_bytes_get(bytes, idx)); - } - if ((mid == DYN_M_slice) || (mid == DYN_M_subarray)) { - /* subarray and Buffer.slice alias their source. TypedArray.slice - * owns an independent copy; all keep the receiver's Buffer flavor. */ - double startD = dyn_bytes_index_arg(args, argc, 0, 0); - if (scr_exc_pending()) return NULL; - double endD = dyn_bytes_index_arg(args, argc, 1, (double)blen); - if (scr_exc_pending()) return NULL; - ScrBytes *out = recv->buffer || (mid == DYN_M_subarray) - ? scr_bytes_subarray(bytes, startD, endD) : scr_bytes_slice(bytes, startD, endD); - ScrDyn *d = recv->buffer ? scr_dyn_new_buffer(out) : scr_dyn_new_bytes(out); - scr_bytes_release(out); - return d; - } - if ((mid == DYN_M_set)) { - double offset = dyn_bytes_index_arg(args, argc, 1, 0); - if (scr_exc_pending()) return NULL; - scr_bytes_set_from_dyn(bytes, argc ? args[0] : scr_dyn_undefined(), offset); - return scr_exc_pending() ? NULL : scr_dyn_retain(scr_dyn_undefined()); - } - if ((mid == DYN_M_copyWithin)) { - double target = dyn_bytes_index_arg(args, argc, 0, 0); - if (scr_exc_pending()) return NULL; - double start = dyn_bytes_index_arg(args, argc, 1, 0); - if (scr_exc_pending()) return NULL; - double end = dyn_bytes_index_arg(args, argc, 2, (double)blen); - if (scr_exc_pending()) return NULL; - scr_bytes_release(scr_bytes_copy_within(bytes, target, start, end)); - return scr_dyn_retain(recv); - } - if ((mid == DYN_M_fill) && !recv->buffer) { - double value; - if (!scr_dyn_number_coerce_js(argc ? args[0] : scr_dyn_undefined(), &value)) return NULL; - double start = dyn_bytes_index_arg(args, argc, 1, 0); - if (scr_exc_pending()) return NULL; - double end = dyn_bytes_index_arg(args, argc, 2, (double)blen); - if (scr_exc_pending()) return NULL; - scr_bytes_release(scr_bytes_fill_elem(bytes, value, start, end)); - return scr_dyn_retain(recv); - } - if ((mid == DYN_M_join) || ((mid == DYN_M_toString) && !recv->buffer)) { - ScrStr *separator = (mid == DYN_M_join) && argc && args[0]->kind != SCR_DYN_UNDEF - ? scr_dyn_string_coerce_js(args[0]) : scr_str_new(",", 1); - if (scr_exc_pending()) { scr_str_release(separator); return NULL; } - ScrStr *joined = scr_bytes_join(bytes, separator); - scr_str_release(separator); - ScrDyn *out = scr_dyn_new_str(joined); - scr_str_release(joined); - return out; - } + bool handled; + ScrDyn *answer = scr_dyn_bytes_method(recv, method, args, argc, &handled); + if (handled) return answer; if (dyn_bytes_proto_real(mid)) { dyn_throw_unsupported(scr_bytes_elem_name(bytes->elem), method); return NULL; @@ -1734,7 +1821,10 @@ static ScrDyn *dyn_array_method_value_call(ScrClosure *closure, ScrDyn *const *a ScrStr *method = scr_box_get_ref(closure->caps[0]); ScrDyn *receiver = scr_dyn_this_get(); ScrDyn *result; - if (receiver->kind == SCR_DYN_ARR) { + /* An array, or a live view of a native array, runs the method its own + * dispatch has; other receivers take the generic array-like algorithms. */ + if (receiver->kind == SCR_DYN_ARR || + (receiver->kind == SCR_DYN_TYPED_REF && scr_dyn_typed_ref_array_ops(receiver))) { result = scr_dyn_invoke(receiver, method->data, args, argc, method->data); } else { ScrDyn *pack = scr_dyn_new_arr(); @@ -1908,6 +1998,12 @@ static const char dyn_s32[] SCR_DYN_COMPAT_STR = ""; static const char dyn_s33[] SCR_DYN_COMPAT_STR = "constructing %s%s through a stored value is not supported yet"; static const char dyn_s34[] SCR_DYN_COMPAT_STR = " from a primitive"; static const char dyn_s35[] SCR_DYN_COMPAT_STR = "FinalizationRegistry"; +static const char dyn_s36[] SCR_DYN_COMPAT_STR = "atob"; +static const char dyn_s37[] SCR_DYN_COMPAT_STR = "btoa"; +static const char dyn_s38[] SCR_DYN_COMPAT_STR = "structuredClone"; +static const char dyn_s39[] SCR_DYN_COMPAT_STR = "The \"input\" argument must be specified"; +static const char dyn_s40[] SCR_DYN_COMPAT_STR = "The \"The value argument must be specified\" argument must be specified"; +static const char dyn_s41[] SCR_DYN_COMPAT_STR = "ERR_MISSING_ARGS"; static const char *const dyn_global_ctor_names[] = { dyn_s26, dyn_s29, dyn_s25, dyn_s30, dyn_s31, dyn_s21, dyn_s22, dyn_s24, dyn_s35, @@ -1972,7 +2068,7 @@ static bool dyn_global_construct(const ScrDyn *callee, const ScrDyn *args, ScrDy static ScrDyn *dyn_global_ctor(const char *name) { for (size_t i = 0; i < DYN_GLOBAL_CTORS; i++) { - if (strcmp(dyn_global_ctor_names[i], name) != 0) continue; + if (dyn_global_ctor_names[i][0] != name[0] || strcmp(dyn_global_ctor_names[i], name) != 0) continue; if (!dyn_global_ctors_registered) { dyn_global_ctors_registered = true; scr_atexit(dyn_global_ctors_cleanup); @@ -1985,6 +2081,9 @@ static ScrDyn *dyn_global_ctor(const char *name) { dyn_global_ctors[i] = scr_dyn_new_func(scr_closure_new((void *)dyn_global_ctor_names[i], 0), dyn_global_ctor_call, length, dyn_s6, dyn_global_ctor_names[i]); + /* `o.constructor.prototype` of a plain object is Object.prototype + * (zod's isPlainObject). The other prototypes are not modeled. */ + if (i == 0) scr_dyn_fn_seed_prototype(dyn_global_ctors[i], scr_dyn_object_prototype()); } return scr_dyn_retain(dyn_global_ctors[i]); } @@ -2170,7 +2269,36 @@ static ScrClosure *dyn_builtin_fn_deferred(ScrDyn *const *args, size_t argc, siz #endif +/* atob, btoa and structuredClone as values: the calls the direct + * spellings make, with Node's error for a missing argument. NULL with the + * exception pending, or the result (+1); `*handled` reports whether `name` + * is one of them. */ +static ScrDyn *dyn_builtin_data_fn_call(const ScrStr *name, ScrDyn *const *args, size_t argc, bool *handled) { + bool atob = strcmp(name->data, dyn_s36) == 0, btoa = strcmp(name->data, dyn_s37) == 0; + bool clone = strcmp(name->data, dyn_s38) == 0; + *handled = atob || btoa || clone; + if (!*handled) return NULL; + if (argc == 0) { + const char *message = clone ? dyn_s40 : dyn_s39; + scr_throw_error_msg_code(SCR_ERR_TYPE, message, strlen(message), dyn_s41); + return NULL; + } + if (clone) return scr_structured_clone(args[0], argc > 1 ? args[1] : scr_dyn_undefined()); + ScrStr *text = atob ? scr_atob(args[0]) : scr_btoa(args[0]); + if (!text) return NULL; + ScrDyn *result = scr_dyn_new_str(text); /* retains */ + scr_str_release(text); + return result; +} + static ScrDyn *dyn_builtin_fn_call(ScrClosure *clo, ScrDyn *const *args, size_t argc) { + { + ScrStr *name = scr_box_get_ref(clo->caps[0]); + bool handled; + ScrDyn *result = dyn_builtin_data_fn_call(name, args, argc, &handled); + scr_str_release(name); + if (handled) return result; + } #ifdef SCR_LIB (void)clo; (void)args; @@ -2229,13 +2357,43 @@ ScrDyn *scr_dyn_builtin_function(const ScrStr *name) { closure->caps[0] = scr_box_new(SCR_BOX_STR); scr_box_set_ref(closure->caps[0], scr_str_retain((ScrStr *)name)); const char *dot = memchr(name->data, '.', name->len); - bool two = strcmp(name->data, dyn_s16) == 0 || strcmp(name->data, dyn_s15) == 0; + bool two = strcmp(name->data, dyn_s16) == 0 || strcmp(name->data, dyn_s15) == 0 || + strcmp(name->data, dyn_s38) == 0; value = scr_dyn_new_func(closure, dyn_builtin_fn_call, two ? 2 : 1, dyn_s10, dot ? dot + 1 : name->data); scr_dyn_obj_set(dyn_builtin_fns, name->data, name->len, value); /* the table owns it */ } return scr_dyn_retain(value); } +/* An own data property of a namespace object. Attribute bits: 1 writable, + * 2 enumerable, 4 configurable. */ +static void dyn_namespace_put(ScrDyn *object, ScrDyn *key, ScrDyn *value, unsigned attributes) { + ScrDyn *descriptor = scr_dyn_new_obj(); + scr_dyn_obj_set(descriptor, "value", 5, scr_dyn_retain(value)); + scr_dyn_obj_set(descriptor, "writable", 8, scr_dyn_new_bool((attributes & 1) != 0)); + scr_dyn_obj_set(descriptor, "enumerable", 10, scr_dyn_new_bool((attributes & 2) != 0)); + scr_dyn_obj_set(descriptor, "configurable", 12, scr_dyn_new_bool((attributes & 4) != 0)); + scr_dyn_release(scr_dyn_define_property(object, key, descriptor)); + scr_dyn_release(descriptor); +} + +ScrDyn *scr_dyn_namespace_new(const ScrStr *tag, ScrDyn *tag_key) { + ScrDyn *object = scr_dyn_new_obj(); + ScrDyn *value = scr_dyn_new_str((ScrStr *)tag); + dyn_namespace_put(object, tag_key, value, 4); + scr_dyn_release(value); + return object; +} + +void scr_dyn_namespace_define(ScrDyn *object, const ScrStr *key, ScrDyn *value, double attributes, + double length) { + if (length >= 0 && value->kind == SCR_DYN_FUNC) value->v.fn.arity = (uint32_t)length; + ScrDyn *name = scr_dyn_new_str((ScrStr *)key); + dyn_namespace_put(object, name, value, (unsigned)attributes); + scr_dyn_release(name); + if (length >= 0) scr_dyn_fence_member(object, key); +} + ScrDyn *scr_dyn_array_prototype(void) { static SCR_TL bool initialized; ScrDyn *prototype = scr_dyn_array_prototype_base(); diff --git a/packages/runtime/src/scr_error.h b/packages/runtime/src/scr_error.h index fbb64aad9..4962db217 100644 --- a/packages/runtime/src/scr_error.h +++ b/packages/runtime/src/scr_error.h @@ -48,6 +48,12 @@ typedef struct ScrClassObj { size_t length; struct ScrBox *prototype_data; struct ScrBox *static_data; + /* The base class's name, which util.inspect prints as + * `[class C extends B]`; NULL without a named base. */ + const ScrStr *base_name; + /* Nonzero when the class declares public static fields, which the + * class object does not carry without static_data. */ + size_t own_static_fields; struct ScrBox *caps[]; } ScrClassObj; diff --git a/packages/runtime/src/scr_event_target.c b/packages/runtime/src/scr_event_target.c index 9ca91879a..0f59d8ca7 100644 --- a/packages/runtime/src/scr_event_target.c +++ b/packages/runtime/src/scr_event_target.c @@ -1962,3 +1962,60 @@ ScrPromise *scr_tp_scheduler_wait_signal(double ms, ScrPromise *value, bool ref, ScrPromise *scr_tp_set_immediate_signal(ScrPromise *value, bool ref, ScrDyn *signal) { return scr_tp_signal(0, value, ref, signal, true); } + +/* ── util.aborted(signal, resource) ──────────────────────────────────── + * Node's async function: the signal must be an AbortSignal and the + * resource an object (functions and arrays included), or the promise + * rejects with ERR_INVALID_ARG_TYPE. An aborted signal gives a promise of + * undefined; otherwise a once 'abort' listener that resists + * stopImmediatePropagation resolves it with the event. The answer adopts + * that promise, as an async function's result does. */ +static ScrDyn *scr_util_aborted_listener(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + ScrPromise *inner = scr_box_get_ref(closure->caps[0]); + /* scr_promise_resolve_dyn takes the value's reference. */ + scr_promise_resolve_dyn(inner, scr_dyn_retain(argc ? args[0] : scr_dyn_undefined())); + scr_promise_release(inner); + return scr_dyn_retain(scr_dyn_undefined()); +} + +ScrDyn *scr_util_aborted(ScrDyn *signal, ScrDyn *resource) { + ScrPromise *outer = scr_promise_new(); + bool aborted = false; + ScrEventTargetCore *core = scr_et_signal_core_fn ? scr_et_signal_core_fn(signal, &aborted) : NULL; + bool object = resource->kind == SCR_DYN_OBJ || resource->kind == SCR_DYN_ARR || resource->kind == SCR_DYN_FUNC || + resource->kind == SCR_DYN_TYPED_REF || resource->kind == SCR_DYN_HANDLE || + resource->kind == SCR_DYN_BYTES || resource->kind == SCR_DYN_PROMISE || + resource->kind == SCR_DYN_PROXY; + if (!core) { + static const char message[] = "signal is not of type AbortSignal."; + scr_throw_error_msg_code(SCR_ERR_TYPE, message, sizeof message - 1, "ERR_INVALID_ARG_TYPE"); + } else if (!object) { + scr_dyn_arg_type_fail("resource", "of type object", resource); + } + if (scr_exc_pending()) { + scr_promise_reject_pending(outer); + ScrDyn *rejected = scr_dyn_new_promise(outer); + scr_promise_release(outer); + return rejected; + } + ScrPromise *inner = scr_promise_new(); + if (aborted) { + scr_promise_fulfill_void(inner); + } else { + ScrClosure *closure = scr_closure_new(NULL, 1); + closure->caps[0] = scr_box_new_obj(scr_promise_retain_v, scr_promise_release_v, NULL); + scr_box_set_ref(closure->caps[0], scr_promise_retain(inner)); + ScrDyn *listener = scr_dyn_new_func(closure, scr_util_aborted_listener, 1, "native:function", ""); + ScrStr *abort = scr_str_new("abort", 5); + bool duplicate; + (void)scr_et_append(core, abort, listener, SCR_ET_ONCE | SCR_ET_RESIST, &duplicate); + scr_str_release(abort); + scr_dyn_release(listener); + } + ScrDyn *adopted = scr_dyn_new_promise(inner); + scr_promise_release(inner); + scr_promise_resolve_dyn(outer, adopted); /* moves the promise box in */ + ScrDyn *result = scr_dyn_new_promise(outer); + scr_promise_release(outer); + return result; +} diff --git a/packages/runtime/src/scr_event_target.h b/packages/runtime/src/scr_event_target.h index e1ff5d0da..67f8d1544 100644 --- a/packages/runtime/src/scr_event_target.h +++ b/packages/runtime/src/scr_event_target.h @@ -106,6 +106,8 @@ typedef struct { extern SCR_TL const ScrEtBroadcastOps *scr_et_broadcast_ops; void scr_et_broadcast_install(void); +/* util.aborted(signal, resource): +1 dyn promise; argument errors reject it. */ +ScrDyn *scr_util_aborted(ScrDyn *signal, ScrDyn *resource); /* events.addAbortListener(signal, listener): +1 disposable, or NULL. */ ScrDyn *scr_events_add_abort_listener(ScrDyn *signal, ScrDyn *listener); /* timers/promises setTimeout(delay, value, options) and setImmediate(value, diff --git a/packages/runtime/src/scr_events.c b/packages/runtime/src/scr_events.c index 066cff8c4..78b7bb0f1 100644 --- a/packages/runtime/src/scr_events.c +++ b/packages/runtime/src/scr_events.c @@ -461,8 +461,80 @@ static SCR_TL bool scr_stdin_destroyed = false; static SCR_TL bool scr_stdin_paused = false; static SCR_TL bool scr_stdin_resumed = false; static SCR_TL bool scr_stdin_read_eof = false; -static SCR_TL ScrBytes *scr_stdin_buffer; +/* Input held for read() and later consumers, as Node's BufferList: the + * chunks in arrival order and their total length, so an arrival appends + * without copying what is already held. */ +static SCR_TL ScrBytes **scr_stdin_held = NULL; +static SCR_TL size_t scr_stdin_nheld = 0, scr_stdin_held_cap = 0; +static SCR_TL size_t scr_stdin_buffered = 0; static SCR_TL size_t scr_stdin_read_demand; +/* 'readable' listeners (paused mode, as Node's once one is added): input + * stays buffered for read(), and the event fires on the next tick when + * input arrives while a read wanted more (`need`), and once at EOF. */ +static SCR_TL ScrStdinEndL *scr_stdin_readable = NULL; +static SCR_TL size_t scr_stdin_nreadable = 0, scr_stdin_readable_cap = 0; +static SCR_TL bool scr_stdin_readable_need = false; +static SCR_TL bool scr_stdin_readable_queued = false; + +/* Appends an arrived chunk (moves) to the held input. */ +static void scr_stdin_hold(ScrBytes *chunk) { + if (chunk->len == 0) { + scr_bytes_release(chunk); + return; + } + if (scr_stdin_nheld == scr_stdin_held_cap) { + scr_stdin_held_cap = scr_stdin_held_cap ? scr_stdin_held_cap * 2 : 4; + scr_stdin_held = realloc(scr_stdin_held, scr_stdin_held_cap * sizeof *scr_stdin_held); + if (!scr_stdin_held) scr_events_oom(); + } + scr_stdin_held[scr_stdin_nheld++] = chunk; + scr_stdin_buffered += chunk->len; +} + +/* The first n held bytes as one chunk (+1), 0 < n <= scr_stdin_buffered. + * A whole first chunk moves out without a copy; otherwise the bytes are + * joined once, and a partly taken chunk keeps its rest. */ +static ScrBytes *scr_stdin_take(size_t n) { + ScrBytes *first = scr_stdin_held[0]; + size_t used = 0; + ScrBytes *out; + if (n == first->len) { + out = first; + used = 1; + } else if (n < first->len) { + out = scr_bytes_slice(first, 0, (double)n); + scr_stdin_held[0] = scr_bytes_slice(first, (double)n, (double)first->len); + scr_bytes_release(first); + } else { + out = scr_bytes_new(SCR_BYTES_U8, (double)n); + size_t at = 0; + while (at < n) { + ScrBytes *chunk = scr_stdin_held[used]; + size_t part = chunk->len <= n - at ? chunk->len : n - at; + memcpy(out->data + at, chunk->data, part); + at += part; + if (part == chunk->len) { + scr_bytes_release(chunk); + used++; + } else { + scr_stdin_held[used] = scr_bytes_slice(chunk, (double)part, (double)chunk->len); + scr_bytes_release(chunk); + } + } + } + memmove(scr_stdin_held, scr_stdin_held + used, (scr_stdin_nheld - used) * sizeof *scr_stdin_held); + scr_stdin_nheld -= used; + scr_stdin_buffered -= n; + return out; +} + +static void scr_stdin_drop_held(void) { + for (size_t i = 0; i < scr_stdin_nheld; i++) scr_bytes_release(scr_stdin_held[i]); + free(scr_stdin_held); + scr_stdin_held = NULL; + scr_stdin_nheld = scr_stdin_held_cap = 0; + scr_stdin_buffered = 0; +} #ifdef SCR_STDIO_EXTENDED /* process.stdin.setEncoding(enc): the canonical encoding name (NULL until @@ -548,6 +620,78 @@ void scr_stdin_on_end(ScrClosure *cb /*moves*/, bool once) { scr_stdin_nend++; } +static void scr_stdin_finish(bool fire_end); + +static void scr_stdin_emit_readable(void) { + scr_stdin_readable_queued = false; + size_t length = scr_stdin_buffered; + if (scr_stdin_eof || scr_stdin_destroyed || (!length && !scr_stdin_read_eof)) return; + size_t n = scr_stdin_nreadable; + ScrStdinEndL *snap = malloc((n ? n : 1) * sizeof *snap); + if (!snap) scr_events_oom(); + for (size_t i = 0; i < n; i++) { + snap[i] = scr_stdin_readable[i]; + scr_closure_retain(snap[i].cb); + } + for (size_t i = 0; i < n; i++) { + if (snap[i].once) { + for (size_t j = 0; j < scr_stdin_nreadable; j++) { + if (scr_stdin_readable[j].cb == snap[i].cb) { + scr_closure_release(scr_stdin_readable[j].cb); + memmove(scr_stdin_readable + j, scr_stdin_readable + j + 1, + (scr_stdin_nreadable - j - 1) * sizeof *scr_stdin_readable); + scr_stdin_nreadable--; + break; + } + } + } + if (!scr_exc_pending() && !scr_stdin_eof && !scr_stdin_destroyed) + ((void (*)(ScrClosure *))snap[i].cb->fn)(snap[i].cb); + scr_closure_release(snap[i].cb); + } + free(snap); + length = scr_stdin_buffered; + /* Another event is due once the buffer reaches the 64 KiB mark, or the + * larger size a listener's read(size) asked for. */ + scr_stdin_readable_need = + !scr_stdin_read_eof && (length <= 65536 || length < scr_stdin_read_demand); +} + +static void scr_stdin_queue_readable(void) { + if (scr_stdin_readable_queued || !scr_stdin_nreadable) return; + scr_stdin_readable_queued = true; + scr_next_tick_raw(scr_stdin_emit_readable); +} + +/* Adding a 'readable' listener starts reading ahead to the 64 KiB mark; a + * buffer that already holds input fires the event at once (next tick). */ +static void scr_stdin_on_readable(ScrClosure *cb /*moves*/, bool once) { + if (scr_stdin_eof || scr_stdin_destroyed) { + scr_closure_release(cb); + return; + } + if (scr_stdin_nreadable == scr_stdin_readable_cap) { + scr_stdin_readable_cap = scr_stdin_readable_cap ? scr_stdin_readable_cap * 2 : 2; + scr_stdin_readable = realloc(scr_stdin_readable, scr_stdin_readable_cap * sizeof *scr_stdin_readable); + if (!scr_stdin_readable) scr_events_oom(); + } + scr_stdin_readable[scr_stdin_nreadable].cb = cb; + scr_stdin_readable[scr_stdin_nreadable].once = once; + scr_stdin_nreadable++; + scr_stdin_readable_need = true; + if (scr_stdin_read_demand < 65536) scr_stdin_read_demand = 65536; + if (scr_stdin_buffered) scr_stdin_queue_readable(); +} + +/* The input ended: with 'readable' listeners the event fires once more and + * 'end' waits for the read that finds nothing left; otherwise 'end' fires + * once the buffer is drained. */ +static void scr_stdin_reached_eof(void) { + scr_stdin_read_eof = true; + if (scr_stdin_nreadable) scr_stdin_queue_readable(); + else if (!scr_stdin_buffered) scr_stdin_finish(true); +} + void scr_stdin_on_error(ScrClosure *cb /*moves*/, ScrChildErrFn fn, bool once) { if (scr_stdin_eof || scr_stdin_destroyed) { scr_closure_release(cb); @@ -589,10 +733,15 @@ bool scr_stdin_ended(void) { return scr_stdin_eof || scr_stdin_destroyed; } * alive (a paused Node stdin with only those never flows — the process * exits with them registered). */ static bool scr_stdin_watching(void) { - return !scr_stdin_eof && !scr_stdin_destroyed && - ((!scr_stdin_paused && (scr_stdin_ndata > 0 || scr_stdin_resumed)) || - scr_stdin_waiter != NULL || - (!scr_stdin_read_eof && scr_stdin_read_demand > (scr_stdin_buffer ? scr_stdin_buffer->len : 0))); + if (scr_stdin_eof || scr_stdin_destroyed) return false; + /* 'readable' listeners keep the stream paused, as in Node, where + * resume() and 'data' listeners do not make it flow: it reads ahead only + * while a read wants more than is held (the 64 KiB mark, or a larger + * read(size)), and stops until something drains it. */ + if (scr_stdin_nreadable) return !scr_stdin_read_eof && scr_stdin_read_demand > scr_stdin_buffered; + return (!scr_stdin_paused && (scr_stdin_ndata > 0 || scr_stdin_resumed)) || + scr_stdin_waiter != NULL || + (!scr_stdin_read_eof && scr_stdin_read_demand > scr_stdin_buffered); } bool scr_stdin_pending(void) { return scr_stdin_watching(); } @@ -601,6 +750,10 @@ static void scr_stdin_drop_listeners(void) { for (size_t i = 0; i < scr_stdin_ndata; i++) scr_closure_release(scr_stdin_data[i].cb); for (size_t i = 0; i < scr_stdin_nend; i++) scr_closure_release(scr_stdin_end[i].cb); for (size_t i = 0; i < scr_stdin_nerr; i++) scr_closure_release(scr_stdin_err[i].cb); + for (size_t i = 0; i < scr_stdin_nreadable; i++) scr_closure_release(scr_stdin_readable[i].cb); + free(scr_stdin_readable); + scr_stdin_readable = NULL; + scr_stdin_nreadable = scr_stdin_readable_cap = 0; free(scr_stdin_data); free(scr_stdin_end); free(scr_stdin_err); @@ -666,8 +819,7 @@ static void scr_stdin_finish(bool fire_end) { void scr_stdin_destroy(void) { if (scr_stdin_destroyed) return; scr_stdin_destroyed = true; - scr_bytes_release(scr_stdin_buffer); - scr_stdin_buffer = NULL; + scr_stdin_drop_held(); scr_stdin_read_demand = 0; scr_stdin_settle_done(); scr_stdin_drop_listeners(); @@ -700,16 +852,9 @@ static void scr_stdin_deliver(ScrBytes *chunk); /* A chunk that arrived (moves): delivered, or buffered while paused or * without a consumer. */ static void scr_stdin_arrived(ScrBytes *chunk) { - if (scr_stdin_paused || (!scr_stdin_ndata && !scr_stdin_resumed && !scr_stdin_waiter)) { - if (scr_stdin_buffer) { - size_t before = scr_stdin_buffer->len; - ScrBytes *joined = scr_bytes_new(SCR_BYTES_U8, (double)(before + chunk->len)); - memcpy(joined->data, scr_stdin_buffer->data, before); - memcpy(joined->data + before, chunk->data, chunk->len); - scr_bytes_release(scr_stdin_buffer); - scr_bytes_release(chunk); - scr_stdin_buffer = joined; - } else scr_stdin_buffer = chunk; + if (scr_stdin_paused || scr_stdin_nreadable || (!scr_stdin_ndata && !scr_stdin_resumed && !scr_stdin_waiter)) { + if (scr_stdin_nreadable && scr_stdin_readable_need) scr_stdin_queue_readable(); + scr_stdin_hold(chunk); return; } scr_stdin_deliver(chunk); @@ -718,15 +863,18 @@ static void scr_stdin_arrived(ScrBytes *chunk) { static void scr_stdin_service(void) { if (!scr_stdin_watching()) return; - if (!scr_stdin_paused && scr_stdin_buffer) { - ScrBytes *chunk = scr_stdin_buffer; - scr_stdin_buffer = NULL; + /* Buffered input goes to a consumer: 'data' listeners, a resumed + * stream, or a parked for-await read. After the last 'readable' listener + * it stays buffered until one arrives, as in Node. */ + if (!scr_stdin_paused && !scr_stdin_nreadable && scr_stdin_buffered && + (scr_stdin_ndata > 0 || scr_stdin_resumed || scr_stdin_waiter != NULL)) { + ScrBytes *chunk = scr_stdin_take(scr_stdin_buffered); scr_stdin_deliver(chunk); scr_bytes_release(chunk); return; } if (scr_stdin_read_eof) { - if (!scr_stdin_buffer) scr_stdin_finish(true); + if (!scr_stdin_buffered && !scr_stdin_nreadable) scr_stdin_finish(true); return; } if (scr_stdin_fed) { @@ -735,10 +883,7 @@ static void scr_stdin_service(void) { bool ended = false; ScrBytes *chunk = scr_stdin_fed_take(&ended); if (chunk) scr_stdin_arrived(chunk); - else if (ended) { - scr_stdin_read_eof = true; - if (!scr_stdin_buffer) scr_stdin_finish(true); - } + else if (ended) scr_stdin_reached_eof(); return; } #ifdef _WIN32 @@ -800,8 +945,7 @@ static void scr_stdin_service(void) { return; } if (n == 0) { - scr_stdin_read_eof = true; - if (!scr_stdin_buffer) scr_stdin_finish(true); + scr_stdin_reached_eof(); return; } ScrBytes *chunk = scr_bytes_new(SCR_BYTES_U8, (double)n); @@ -875,6 +1019,8 @@ static void scr_stdin_deliver(ScrBytes *chunk) { #endif ScrPromise *p = scr_stdin_waiter; scr_stdin_waiter = NULL; + /* Node's stdin yields Buffers. */ + chunk->is_buffer = true; scr_promise_fulfill_ref(p, scr_bytes_retain(chunk), scr_bytes_retain_v, scr_bytes_release_v, NULL); scr_promise_release(p); @@ -951,6 +1097,7 @@ static void scr_stdio_data_text(ScrClosure *cb, ScrStr *text) { } #endif static void scr_stdio_end(ScrClosure *cb) { scr_stdio_fire(cb, NULL, 0); } +static void scr_stdio_readable(ScrClosure *cb) { scr_stdio_fire(cb, NULL, 0); } static void scr_stdio_error(ScrClosure *cb, ScrStr *message) { ScrError *error = scr_error_new(SCR_ERR_ERROR, message); ScrDyn *arg = scr_dyn_from_error(error); @@ -1044,7 +1191,7 @@ static void scr_stdio_queue_write_error(ScrStdio *stream, ScrDyn *callback, int } static void scr_stdio_finish_read(void) { - if (scr_stdin_read_eof && !scr_stdin_buffer && !scr_stdin_eof && !scr_stdin_destroyed) + if (scr_stdin_read_eof && !scr_stdin_buffered && !scr_stdin_eof && !scr_stdin_destroyed) scr_stdin_finish(true); } @@ -1053,21 +1200,21 @@ static ScrDyn *scr_stdio_read(ScrDyn *size) { if (!all && (size->kind != SCR_DYN_NUM || !isfinite(size->v.num) || size->v.num < 0 || size->v.num > 1073741824 || floor(size->v.num) != size->v.num)) return scr_stdio_refusal("read size"); - size_t available = scr_stdin_buffer ? scr_stdin_buffer->len : 0; + size_t available = scr_stdin_buffered; size_t wanted = all ? available : (size_t)size->v.num; if (scr_stdin_read_eof && wanted > available) wanted = available; if (wanted == 0 || wanted > available) { if (!scr_stdin_eof && !scr_stdin_destroyed) { + scr_stdin_readable_need = true; scr_stdin_read_demand = wanted > 65536 ? wanted : 65536; if (scr_stdin_read_eof) scr_next_tick_raw(scr_stdio_finish_read); } return scr_dyn_new_null(); } - ScrBytes *chunk = scr_bytes_slice(scr_stdin_buffer, 0, (double)wanted); - ScrBytes *rest = wanted < available ? scr_bytes_slice(scr_stdin_buffer, (double)wanted, (double)available) : NULL; - scr_bytes_release(scr_stdin_buffer); - scr_stdin_buffer = rest; + ScrBytes *chunk = scr_stdin_take(wanted); + bool rest = scr_stdin_buffered > 0; scr_stdin_read_demand = 65536; + if (!rest && !scr_stdin_read_eof) scr_stdin_readable_need = true; scr_stdin_deliver(chunk); ScrDyn *result = scr_dyn_new_chunk(chunk); scr_bytes_release(chunk); @@ -1223,12 +1370,14 @@ static ScrDyn *scr_stdio_method(ScrClosure *cb, ScrDyn *const *args, size_t argc bool data = arg->kind == SCR_DYN_STR && arg->v.str->len == 4 && memcmp(event, "data", 4) == 0; bool end = arg->kind == SCR_DYN_STR && arg->v.str->len == 3 && memcmp(event, "end", 3) == 0; bool error = arg->kind == SCR_DYN_STR && arg->v.str->len == 5 && memcmp(event, "error", 5) == 0; + bool readable = stream->fd == 0 && arg->kind == SCR_DYN_STR && arg->v.str->len == 8 && + memcmp(event, "readable", 8) == 0; #ifdef SCR_STDIO_EXTENDED bool resize = stream->fd != 0 && arg->kind == SCR_DYN_STR && arg->v.str->len == 6 && memcmp(event, "resize", 6) == 0; - if ((!data && !end && !error && !resize) || (stream->fd != 0 && !error && !resize)) { + if ((!data && !end && !error && !readable && !resize) || (stream->fd != 0 && !error && !resize)) { #else - if ((!data && !end && !error) || (stream->fd != 0 && !error)) { + if ((!data && !end && !error && !readable) || (stream->fd != 0 && !error)) { #endif result = scr_stdio_refusal("event"); goto done; @@ -1248,6 +1397,7 @@ static ScrDyn *scr_stdio_method(ScrClosure *cb, ScrDyn *const *args, size_t argc if (data) scr_stdin_on_data(scr_stdio_callback(listener, (void *)scr_stdio_data), scr_stdio_data, once); #endif if (end) scr_stdin_on_end(scr_stdio_callback(listener, (void *)scr_stdio_end), once); + if (readable) scr_stdin_on_readable(scr_stdio_callback(listener, (void *)scr_stdio_readable), once); if (error) scr_stdin_on_error(scr_stdio_callback(listener, (void *)scr_stdio_error), scr_stdio_error, once); } else { #define REMOVE_STDIO_LISTENER(list, count, fire) \ @@ -1260,6 +1410,7 @@ static ScrDyn *scr_stdio_method(ScrClosure *cb, ScrDyn *const *args, size_t argc } if (data) { REMOVE_STDIO_LISTENER(scr_stdin_data, scr_stdin_ndata, scr_stdio_data); } if (end) { REMOVE_STDIO_LISTENER(scr_stdin_end, scr_stdin_nend, scr_stdio_end); } + if (readable) { REMOVE_STDIO_LISTENER(scr_stdin_readable, scr_stdin_nreadable, scr_stdio_readable); } if (error) { REMOVE_STDIO_LISTENER(scr_stdin_err, scr_stdin_nerr, scr_stdio_error); } #undef REMOVE_STDIO_LISTENER } @@ -1319,7 +1470,7 @@ static ScrDyn *scr_stdio_get(void *ptr, const char *key, size_t length) { return dimension < 0 ? scr_dyn_retain(scr_dyn_undefined()) : scr_dyn_new_num(dimension); } if (stream->fd == 0 && length == 14 && memcmp(key, "readableLength", 14) == 0) - return scr_dyn_new_num(scr_stdin_buffer ? (double)scr_stdin_buffer->len : 0); + return scr_dyn_new_num((double)scr_stdin_buffered); if (stream->fd == 0 && length == 5 && memcmp(key, "isRaw", 5) == 0) return scr_process_is_tty(0) ? scr_dyn_new_bool(scr_stdio_raw) : scr_dyn_retain(scr_dyn_undefined()); if (stream->fd == 0 && length == 9 && memcmp(key, "destroyed", 9) == 0) return scr_dyn_new_bool(scr_stdin_destroyed); @@ -1651,8 +1802,7 @@ static void scr_events_cleanup_atexit(void) { if (scr_context_is_main()) scr_sig_cleanup(); scr_stdin_settle_done(); scr_stdin_drop_listeners(); - scr_bytes_release(scr_stdin_buffer); - scr_stdin_buffer = NULL; + scr_stdin_drop_held(); if (scr_stdio_initialized) scr_stdio_cleanup(); } diff --git a/packages/runtime/src/scr_exception.h b/packages/runtime/src/scr_exception.h index e99f84243..63dd4a3ca 100644 --- a/packages/runtime/src/scr_exception.h +++ b/packages/runtime/src/scr_exception.h @@ -123,6 +123,11 @@ static inline ScrExcCell *scr_exc_swap_cell_inline(ScrExcCell *cell) { _Static_assert(offsetof(ScrExcCell, kind) == 0 && sizeof(ScrExcKind) == 4 && SCR_EXC_NONE == 0, "the LLVM emitter's inline pending check loads an i32 kind at offset 0"); +/* Unwinds the native stack to the nearest landing pad of emitted code with + * the exception pending in the active cell (the unwinding ABI of emitted + * functions, unwind-abi.ts). Linux (glibc and musl) only; it never + * returns. */ +SCR_COLD _Noreturn void scr_unwind_raise(void); SCR_COLD void scr_throw_f64(double v); SCR_COLD void scr_throw_bool(bool v); SCR_COLD void scr_throw_str(ScrStr *v); /* takes ownership */ diff --git a/packages/runtime/src/scr_fetch.c b/packages/runtime/src/scr_fetch.c index aebe62659..d28707d5d 100644 --- a/packages/runtime/src/scr_fetch.c +++ b/packages/runtime/src/scr_fetch.c @@ -147,6 +147,9 @@ struct SfStream { bool pull_again; bool discarded; bool request_discarding; + /* A zero high-water mark (a TransformStream's readable side): pull only + * while a read is pending. */ + bool hwm_zero; ScrDyn *error; SfReader *reader; /* owned while locked */ SfCollector *collector; /* owned while a body reader is active */ @@ -167,12 +170,23 @@ struct SfStream { SfTransfer *response_owner; /* weak; transfer owns the stream */ bool callbacks_tracked; SfStream *callbacks_next; + /* Told when the stream errors (a tee watching its source's closed + * promise); ctx is borrowed and the watcher clears it before it goes. */ + void (*error_watch)(void *ctx, ScrDyn *reason); + void *error_watch_ctx; }; struct SfHeaders { size_t rc; ScrArr *pairs; + /* Fetch's "sort and combine" list of `pairs` as flat name/value pairs, + * built on the first iteration step and dropped by every mutation. */ + ScrArr *sorted; bool immutable; + /* `pairs` is in the form sf_add_converted_headers builds: lowercase valid + * names, trimmed valid UTF-8 values, one combined pair per name except + * set-cookie. Headers built from wire lines are not (duplicates stay). */ + bool canonical; }; struct SfResponse { @@ -898,6 +912,7 @@ static void sf_stream_release(SfStream *s) { sf_response_discard_if_unobserved(s); return; } + scr_weak_dispose_inline(s); sf_stream_drop_chunks(s); if (s->reader) { s->reader->stream = NULL; @@ -1291,6 +1306,7 @@ static void sf_stream_error(SfStream *s, ScrDyn *reason) { if (s->reader) { sf_reject_promise_reason(s->reader->closed, reason); } + if (s->error_watch) s->error_watch(s->error_watch_ctx, reason); if (s->request_owner) { sf_transfer_stream_error(s->request_owner, reason); } @@ -1618,6 +1634,14 @@ static void sf_stream_schedule_pull_complete(SfStream *s) { scr_queue_microtask(cb); } +/* A consumer waiting for the stream's next chunk: a pending read, a body + * collector, or a fetch upload (fetch keeps reading the request body, + * including after an aborted transfer stops writing it). */ +static bool sf_stream_has_consumer(const SfStream *s) { + return s->collector || (s->reader && s->reader->pending_head) || s->request_owner || + s->request_discarding; +} + static void sf_stream_pull(SfStream *s) { if (!s->started || (!s->pull_cb && !sf_stream_has_from_source(s)) || @@ -1625,6 +1649,10 @@ static void sf_stream_pull(SfStream *s) { s->error) { return; } + /* ReadableStreamDefaultControllerShouldCallPull: with no room in the + * queue, only a consumer asks for a chunk: a pending read, a body + * collector, or a fetch upload, whose reader is always waiting. */ + if (s->hwm_zero && !sf_stream_has_consumer(s)) return; if (s->pulling) { s->pull_again = true; return; @@ -1852,9 +1880,241 @@ static ScrPromise *sf_stream_collect(SfStream *s, int mode) { c->mode = mode; s->collector = c; sf_stream_drain(s); + if (s->hwm_zero && !s->head) sf_stream_pull(s); return p; } +/* ── ReadableStreamDefaultTee ────────────────────────────────────── */ + +/* + * The streams spec's ReadableStreamDefaultTee over a native stream: the + * tee locks the source with a reader and feeds two native branches whose + * pull and cancel algorithms are native functions. A pull on either branch + * reads one chunk from the source and enqueues it into every branch that + * was not canceled; canceling both branches cancels the source with both + * reasons. Response.clone and Request.clone use it (undici's cloneBody). + * + * Ownership: the tee owns the source, its reader and both branches; each + * branch's callbacks and every pending read own the tee. Closing, erroring + * or canceling a branch drops its callbacks (sf_stream_drop_source_callbacks), + * and fetch teardown drops the callbacks of branches nobody finished. + */ +typedef struct SfTee { + size_t rc; + SfStream *source; + SfReader *reader; + SfStream *branch[2]; + bool reading; + bool read_again; + bool canceled[2]; + ScrDyn *reason[2]; + ScrPromise *cancel_promise; +} SfTee; + +static SfTee *sf_tee_retain(SfTee *t) { + t->rc++; + return t; +} + +static void sf_tee_release(SfTee *t) { + if (!t || --t->rc > 0) return; + if (t->source->error_watch_ctx == t) { + t->source->error_watch = NULL; + t->source->error_watch_ctx = NULL; + } + sf_stream_release(t->branch[0]); + sf_stream_release(t->branch[1]); + sf_reader_release(t->reader); + sf_stream_release(t->source); + scr_dyn_release(t->reason[0]); + scr_dyn_release(t->reason[1]); + scr_promise_release(t->cancel_promise); + free(t); +} + +static void *sf_tee_retain_v(void *p) { return sf_tee_retain((SfTee *)p); } +static void sf_tee_release_v(void *p) { sf_tee_release((SfTee *)p); } + +static ScrBox *sf_tee_box(SfTee *t) { + ScrBox *box = scr_box_new_obj(&sf_tee_retain_v, &sf_tee_release_v, NULL); + scr_box_set_ref(box, sf_tee_retain(t)); + return box; +} + +static void sf_tee_pull(SfTee *t); + +/* The read request's chunk steps run in a microtask (spec: "queue a + * microtask"), after the read that produced the chunk settles. */ +static void sf_tee_chunk_task(ScrClosure *cb) { + SfTee *t = (SfTee *)scr_box_get_ref(cb->caps[0]); + ScrDyn *chunk = (ScrDyn *)scr_box_get_ref(cb->caps[1]); + t->read_again = false; + for (int i = 0; i < 2; i++) { + if (t->canceled[i]) continue; + SfStream *b = t->branch[i]; + if (b->close_requested || b->closed || b->error) continue; + sf_stream_enqueue_value(b, chunk); + if (scr_exc_pending()) scr_exc_clear(); + } + t->reading = false; + if (t->read_again) sf_tee_pull(t); + scr_dyn_release(chunk); + sf_tee_release(t); +} + +static void sf_tee_sink_chunk(void *ctx, ScrDyn *value) { + SfTee *t = ctx; + ScrClosure *cb = scr_closure_new((void *)&sf_tee_chunk_task, 2); + cb->caps[0] = sf_tee_box(t); + cb->caps[1] = scr_box_new_obj(&scr_dyn_retain_v, &scr_dyn_release_v, NULL); + scr_box_set_ref(cb->caps[1], scr_dyn_retain(value)); + scr_queue_microtask(cb); + sf_tee_release(t); /* the read request's reference */ +} + +static void sf_tee_sink_close(void *ctx) { + SfTee *t = ctx; + t->reading = false; + for (int i = 0; i < 2; i++) { + if (!t->canceled[i]) sf_stream_close(t->branch[i]); + } + if (!t->canceled[0] || !t->canceled[1]) { + scr_promise_fulfill_void(t->cancel_promise); + } + sf_tee_release(t); +} + +/* The source reader's closed promise rejected (spec: upon its rejection + * with r, error both branches, and resolve cancelPromise unless both + * branches were canceled), as a promise reaction: in a microtask. */ +static void sf_tee_source_errored_task(ScrClosure *cb) { + SfTee *t = (SfTee *)scr_box_get_ref(cb->caps[0]); + ScrDyn *reason = (ScrDyn *)scr_box_get_ref(cb->caps[1]); + sf_stream_error(t->branch[0], reason); + sf_stream_error(t->branch[1], reason); + if (!t->canceled[0] || !t->canceled[1]) scr_promise_fulfill_void(t->cancel_promise); + scr_dyn_release(reason); + sf_tee_release(t); +} + +/* The source errored, with or without a read of it pending. */ +static void sf_tee_source_errored(void *ctx, ScrDyn *reason) { + SfTee *t = ctx; + t->source->error_watch = NULL; + t->source->error_watch_ctx = NULL; + ScrClosure *cb = scr_closure_new((void *)&sf_tee_source_errored_task, 2); + cb->caps[0] = sf_tee_box(t); + cb->caps[1] = scr_box_new_obj(&scr_dyn_retain_v, &scr_dyn_release_v, NULL); + scr_box_set_ref(cb->caps[1], scr_dyn_retain(reason)); + scr_queue_microtask(cb); +} + +/* A read request's error steps only end the read: the source's error + * reaches the branches through sf_tee_source_errored. */ +static void sf_tee_sink_error(void *ctx, ScrDyn *reason) { + (void)reason; + SfTee *t = ctx; + t->reading = false; + sf_tee_release(t); +} + +static const ScrFetchReadSink sf_tee_sink = { + &sf_tee_sink_chunk, &sf_tee_sink_close, &sf_tee_sink_error}; + +static void sf_tee_pull(SfTee *t) { + if (t->reading) { + t->read_again = true; + return; + } + t->reading = true; + sf_tee_retain(t); /* owned by the read request */ + if (!sf_reader_read_request(t->reader, NULL, &sf_tee_sink, t)) { + scr_exc_clear(); + t->reading = false; + sf_tee_release(t); + } +} + +static ScrDyn *sf_tee_pull_thunk(ScrClosure *cb, ScrDyn *const *args, + size_t argc) { + (void)args; + (void)argc; + SfTee *t = (SfTee *)scr_box_get_ref(cb->caps[0]); + sf_tee_pull(t); + sf_tee_release(t); + return scr_dyn_retain(scr_dyn_undefined()); +} + +static ScrDyn *sf_tee_cancel(SfTee *t, int i, ScrDyn *reason) { + t->canceled[i] = true; + scr_dyn_release(t->reason[i]); + t->reason[i] = scr_dyn_retain(reason); + if (t->canceled[0] && t->canceled[1]) { + ScrDyn *composite = scr_dyn_new_arr(); + scr_dyn_arr_push(composite, scr_dyn_retain(t->reason[0])); + scr_dyn_arr_push(composite, scr_dyn_retain(t->reason[1])); + ScrPromise *result = sf_stream_cancel(t->source, composite, true); + scr_dyn_release(composite); + ScrDyn *boxed = scr_dyn_new_promise(result); + scr_promise_release(result); + sf_then_adopt(t->cancel_promise, boxed); + scr_dyn_release(boxed); + } + return scr_dyn_new_promise(t->cancel_promise); +} + +static ScrDyn *sf_tee_cancel_thunk_0(ScrClosure *cb, ScrDyn *const *args, + size_t argc) { + SfTee *t = (SfTee *)scr_box_get_ref(cb->caps[0]); + ScrDyn *out = sf_tee_cancel(t, 0, argc ? args[0] : scr_dyn_undefined()); + sf_tee_release(t); + return out; +} + +static ScrDyn *sf_tee_cancel_thunk_1(ScrClosure *cb, ScrDyn *const *args, + size_t argc) { + SfTee *t = (SfTee *)scr_box_get_ref(cb->caps[0]); + ScrDyn *out = sf_tee_cancel(t, 1, argc ? args[0] : scr_dyn_undefined()); + sf_tee_release(t); + return out; +} + +static ScrDyn *sf_tee_func(SfTee *t, ScrDynThunk thunk, const char *name) { + ScrClosure *cb = scr_closure_new((void *)thunk, 1); + cb->caps[0] = sf_tee_box(t); + return scr_dyn_new_func(cb, thunk, 1, "(controller)", name); +} + +/* Tees an unlocked, undisturbed stream: +1 branches in out[0] and out[1]. + * The source stays locked by the tee. */ +static void sf_stream_tee(SfStream *s, SfStream *out[2]) { + SfTee *t = calloc(1, sizeof *t); + if (!t) sf_oom(); + t->rc = 1; + t->source = sf_stream_retain(s); + t->reader = sf_stream_get_reader(s); + s->error_watch = &sf_tee_source_errored; + s->error_watch_ctx = t; + t->cancel_promise = scr_promise_new(); + scr_promise_mark_handled(t->cancel_promise); + for (int i = 0; i < 2; i++) { + SfStream *b = sf_stream_new_native(); + b->pull_cb = sf_tee_func(t, &sf_tee_pull_thunk, "pull"); + b->cancel_cb = sf_tee_func( + t, i == 0 ? &sf_tee_cancel_thunk_0 : &sf_tee_cancel_thunk_1, "cancel"); + b->source_this = scr_dyn_retain(scr_dyn_undefined()); + sf_stream_track_callbacks(b); + t->branch[i] = sf_stream_retain(b); + out[i] = b; + } + /* An errored source's closed promise has already rejected. */ + if (s->error) sf_tee_source_errored(t, s->error); + /* The branches start at once; a branch with room then pulls. */ + sf_stream_schedule_initial_pull(out[0]); + sf_stream_schedule_initial_pull(out[1]); + sf_tee_release(t); +} + ScrDyn *scr_fetch_stream_new(ScrDyn *source) { ScrDyn *source_view = source; ScrDyn *materialized = NULL; @@ -2164,6 +2424,24 @@ static ScrDyn *sf_controller_get(void *ptr, const char *key, size_t len) { static bool sf_header_name_ok(const char *s, size_t len); +/* A borrowed element of a runtime-built header pair array. These arrays are + * only ever appended to or rewritten whole, so they are dense. */ +static inline ScrStr *sf_pair_at(ScrArr *pairs, size_t i) { + if (i < pairs->filled) return (ScrStr *)(uintptr_t)scr_arr_dense_get(pairs, i); + return (ScrStr *)scr_arr_peek_ref(pairs, (double)i); +} + +static ScrStr *sf_header_name_lower(const char *data, size_t len); + +/* `name` in lowercase, +1: the same string when it has no uppercase byte. */ +static ScrStr *sf_header_name_lower_str(ScrStr *name) { + for (size_t i = 0; i < name->len; i++) { + char c = name->data[i]; + if (c >= 'A' && c <= 'Z') return sf_header_name_lower(name->data, name->len); + } + return scr_str_retain(name); +} + static ScrStr *sf_header_name_lower(const char *data, size_t len) { char *lower = malloc(len ? len : 1); if (!lower) sf_oom(); @@ -2232,13 +2510,8 @@ static SfHeaders *sf_headers_new_response(ScrArr *raw_pairs) { size_t n = (size_t)scr_arr_len(raw_pairs); ScrArr *pairs = scr_arr_new(SCR_ELEM_STR, n); for (size_t i = 0; i + 1 < n; i += 2) { - ScrStr *raw_name = scr_arr_get_ref(raw_pairs, (double)i); - scr_arr_push_ref( - pairs, sf_header_name_lower(raw_name->data, raw_name->len)); - scr_str_release(raw_name); - ScrStr *raw = scr_arr_get_ref(raw_pairs, (double)(i + 1)); - scr_arr_push_ref(pairs, sf_latin1_to_utf8(raw)); - scr_str_release(raw); + scr_arr_push_ref(pairs, sf_header_name_lower_str(sf_pair_at(raw_pairs, i))); + scr_arr_push_ref(pairs, sf_latin1_to_utf8(sf_pair_at(raw_pairs, i + 1))); } scr_arr_release(raw_pairs); return sf_headers_new(pairs, true); @@ -2252,9 +2525,17 @@ static SfHeaders *sf_headers_retain(SfHeaders *h) { static void sf_headers_release(SfHeaders *h) { if (!h || --h->rc > 0) return; scr_arr_release(h->pairs); + if (h->sorted) scr_arr_release(h->sorted); free(h); } +/* Every change to h->pairs goes through here first. */ +static void sf_headers_changed(SfHeaders *h) { + if (!h->sorted) return; + scr_arr_release(h->sorted); + h->sorted = NULL; +} + static void *sf_headers_retain_v(void *p) { return sf_headers_retain((SfHeaders *)p); } @@ -2294,7 +2575,7 @@ static ScrStr *sf_headers_name(const ScrDyn *value, const char *method) { scr_str_release(raw); return NULL; } - ScrStr *out = sf_header_name_lower(raw->data, raw->len); + ScrStr *out = sf_header_name_lower_str(raw); scr_str_release(raw); return out; } @@ -2305,40 +2586,36 @@ static bool sf_headers_key_eq(const ScrStr *left, const ScrStr *right) { } static ScrStr *sf_headers_get_value(SfHeaders *h, const ScrStr *name) { - size_t n = (size_t)scr_arr_len(h->pairs); + ScrArr *pairs = h->pairs; + size_t n = (size_t)scr_arr_len(pairs); size_t count = 0; size_t total = 0; + ScrStr *first = NULL; for (size_t i = 0; i + 1 < n; i += 2) { - ScrStr *key = scr_arr_get_ref(h->pairs, (double)i); - ScrStr *value = scr_arr_get_ref(h->pairs, (double)(i + 1)); - if (sf_headers_key_eq(key, name)) { - if (count > 0) total += 2; - total += value->len; - count++; - } - scr_str_release(key); - scr_str_release(value); + if (!sf_headers_key_eq(sf_pair_at(pairs, i), name)) continue; + ScrStr *value = sf_pair_at(pairs, i + 1); + if (count > 0) total += 2; + else first = value; + total += value->len; + count++; } if (count == 0) return NULL; + if (count == 1) return scr_str_retain(first); char *joined = malloc(total > 0 ? total : 1); if (!joined) sf_oom(); size_t at = 0; size_t emitted = 0; for (size_t i = 0; i + 1 < n; i += 2) { - ScrStr *key = scr_arr_get_ref(h->pairs, (double)i); - ScrStr *value = scr_arr_get_ref(h->pairs, (double)(i + 1)); - if (sf_headers_key_eq(key, name)) { - if (emitted > 0) { - joined[at++] = ','; - joined[at++] = ' '; - } - memcpy(joined + at, value->data, value->len); - at += value->len; - emitted++; + if (!sf_headers_key_eq(sf_pair_at(pairs, i), name)) continue; + ScrStr *value = sf_pair_at(pairs, i + 1); + if (emitted > 0) { + joined[at++] = ','; + joined[at++] = ' '; } - scr_str_release(key); - scr_str_release(value); + memcpy(joined + at, value->data, value->len); + at += value->len; + emitted++; } ScrStr *out = scr_str_new(joined, total); free(joined); @@ -2348,17 +2625,17 @@ static ScrStr *sf_headers_get_value(SfHeaders *h, const ScrStr *name) { static void sf_headers_delete_name(SfHeaders *h, const ScrStr *name) { ScrArr *old = h->pairs; size_t n = (size_t)scr_arr_len(old); + bool present = false; + for (size_t i = 0; i + 1 < n && !present; i += 2) + present = sf_headers_key_eq(sf_pair_at(old, i), name); + if (!present) return; + sf_headers_changed(h); ScrArr *next = scr_arr_new(SCR_ELEM_STR, n); for (size_t i = 0; i + 1 < n; i += 2) { - ScrStr *key = scr_arr_get_ref(old, (double)i); - ScrStr *value = scr_arr_get_ref(old, (double)(i + 1)); - if (sf_headers_key_eq(key, name)) { - scr_str_release(key); - scr_str_release(value); - continue; - } - scr_arr_push_ref(next, key); - scr_arr_push_ref(next, value); + ScrStr *key = sf_pair_at(old, i); + if (sf_headers_key_eq(key, name)) continue; + scr_arr_push_ref(next, scr_str_retain(key)); + scr_arr_push_ref(next, scr_str_retain(sf_pair_at(old, i + 1))); } h->pairs = next; scr_arr_release(old); @@ -2399,15 +2676,13 @@ static bool sf_headers_convert_pair( * Fetch-specific semicolon separator for Cookie. */ static void sf_headers_append_pair( SfHeaders *h, ScrStr *name, ScrStr *value) { + sf_headers_changed(h); if (!(name->len == 10 && memcmp(name->data, "set-cookie", 10) == 0)) { size_t n = (size_t)scr_arr_len(h->pairs); for (size_t i = 0; i + 1 < n; i += 2) { - ScrStr *key = scr_arr_get_ref(h->pairs, (double)i); - bool match = sf_headers_key_eq(key, name); - scr_str_release(key); - if (!match) continue; + if (!sf_headers_key_eq(sf_pair_at(h->pairs, i), name)) continue; - ScrStr *previous = scr_arr_get_ref(h->pairs, (double)(i + 1)); + ScrStr *previous = sf_pair_at(h->pairs, i + 1); const char *separator = name->len == 6 && memcmp(name->data, "cookie", 6) == 0 ? "; " @@ -2420,7 +2695,10 @@ static void sf_headers_append_pair( memcpy(joined_data + previous->len + 2, value->data, value->len); ScrStr *joined = scr_str_new(joined_data, joined_len); free(joined_data); - scr_str_release(previous); + /* Appending an empty value leaves the separator's space at the end of + * the combined value, which a copy trims again: the pairs are no + * longer in their canonical, trimmed form. */ + if (value->len == 0) h->canonical = false; scr_str_release(name); scr_str_release(value); scr_arr_set_ref(h->pairs, (double)(i + 1), joined); @@ -2447,7 +2725,7 @@ static ScrStr **sf_headers_sorted_names(SfHeaders *h, size_t *count_out) { if (capacity && !names) sf_oom(); size_t count = 0; for (size_t i = 0; i + 1 < n; i += 2) { - ScrStr *key = scr_arr_get_ref(h->pairs, (double)i); + ScrStr *key = sf_pair_at(h->pairs, i); bool seen = false; for (size_t j = 0; j < count; j++) { if (sf_headers_key_eq(names[j], key)) { @@ -2455,8 +2733,7 @@ static ScrStr **sf_headers_sorted_names(SfHeaders *h, size_t *count_out) { break; } } - if (seen) scr_str_release(key); - else names[count++] = key; + if (!seen) names[count++] = key; } if (count > 1) { qsort(names, count, sizeof *names, &sf_headers_name_cmp); @@ -2469,44 +2746,44 @@ static bool sf_headers_is_set_cookie(const ScrStr *name) { return name->len == 10 && memcmp(name->data, "set-cookie", 10) == 0; } -/* The `index`th entry of Fetch's "sort and combine" list: names in - * byte order, each with its combined value, except that every set-cookie - * value is its own entry (in insertion order), as Node's Headers - * iterator yields them. False past the end. Outputs are +1. */ +/* Fetch's "sort and combine" list as flat name/value pairs: names in byte + * order, each with its combined value, except that every set-cookie value + * is its own entry (in insertion order), as Node's Headers iterator yields + * them. Cached until the next mutation; borrowed. */ +static ScrArr *sf_headers_sorted(SfHeaders *h) { + if (h->sorted) return h->sorted; + size_t count = 0; + ScrStr **names = sf_headers_sorted_names(h, &count); /* borrowed names */ + size_t n = (size_t)scr_arr_len(h->pairs); + ScrArr *sorted = scr_arr_new(SCR_ELEM_STR, n); + for (size_t i = 0; i < count; i++) { + if (!sf_headers_is_set_cookie(names[i])) { + scr_arr_push_ref(sorted, scr_str_retain(names[i])); + scr_arr_push_ref(sorted, sf_headers_get_value(h, names[i])); + continue; + } + for (size_t j = 0; j + 1 < n; j += 2) { + if (!sf_headers_is_set_cookie(sf_pair_at(h->pairs, j))) continue; + scr_arr_push_ref(sorted, scr_str_retain(names[i])); + scr_arr_push_ref(sorted, scr_str_retain(sf_pair_at(h->pairs, j + 1))); + } + } + free(names); + h->sorted = sorted; + return sorted; +} + +/* The `index`th entry of the sort-and-combine list (sf_headers_sorted). + * False past the end. Outputs are +1. */ static bool sf_headers_sorted_entry(SfHeaders *h, size_t index, ScrStr **name_out, ScrStr **value_out) { - size_t count = 0; - ScrStr **names = sf_headers_sorted_names(h, &count); - bool found = false; - size_t at = 0; - for (size_t i = 0; i < count && !found; i++) { - if (!sf_headers_is_set_cookie(names[i])) { - if (at == index) { - *name_out = scr_str_retain(names[i]); - *value_out = sf_headers_get_value(h, names[i]); - found = true; - } - at++; - continue; - } - size_t n = (size_t)scr_arr_len(h->pairs); - for (size_t j = 0; j + 1 < n && !found; j += 2) { - ScrStr *key = scr_arr_get_ref(h->pairs, (double)j); - bool match = sf_headers_is_set_cookie(key); - scr_str_release(key); - if (!match) continue; - if (at == index) { - *name_out = scr_str_retain(names[i]); - *value_out = scr_arr_get_ref(h->pairs, (double)(j + 1)); - found = true; - } - at++; - } - } - for (size_t j = 0; j < count; j++) scr_str_release(names[j]); - free(names); - return found; + ScrArr *sorted = sf_headers_sorted(h); + size_t entries = (size_t)scr_arr_len(sorted) / 2; + if (index >= entries) return false; + *name_out = scr_str_retain(sf_pair_at(sorted, 2 * index)); + *value_out = scr_str_retain(sf_pair_at(sorted, 2 * index + 1)); + return true; } /* One step of a live Headers iterator (entries, keys or values): the @@ -2570,14 +2847,8 @@ static ScrDyn *sf_headers_invoke(void *ptr, ScrDyn *self, ScrDyn *out = scr_dyn_new_arr(); size_t n = (size_t)scr_arr_len(h->pairs); for (size_t i = 0; i + 1 < n; i += 2) { - ScrStr *key = scr_arr_get_ref(h->pairs, (double)i); - ScrStr *value = scr_arr_get_ref(h->pairs, (double)(i + 1)); - if (key->len == 10 && - memcmp(key->data, "set-cookie", 10) == 0) { - scr_dyn_arr_push(out, scr_dyn_new_str(value)); - } - scr_str_release(key); - scr_str_release(value); + if (sf_headers_is_set_cookie(sf_pair_at(h->pairs, i))) + scr_dyn_arr_push(out, scr_dyn_new_str(sf_pair_at(h->pairs, i + 1))); } return out; } @@ -2664,6 +2935,940 @@ static ScrDyn *sf_headers_get(void *ptr, const char *key, size_t len) { return NULL; } +/* ── TransformStream / WritableStream ────────────────────────────── */ + +/* + * The identity TransformStream (`new TransformStream()`): a native + * readable side (an SfStream with a zero high-water mark, fed by the + * transform) and a writable side implemented here with the streams spec's + * WritableStream state machine (default high-water mark 1, chunk size 1). + * The writable side has no other sink, so the TransformStream object, its + * WritableStream and the sink share one SfTransform; the handles differ + * only by tag. Promise reactions the spec chains on already-settled + * promises run after the same number of microtask turns as in Node's + * implementation (lib/internal/webstreams), so interleavings with user + * code match. + * + * Ownership: the transform owns its readable side and its writer; the + * readable side's pull/cancel callbacks own the transform until that side + * closes or errors (fetch teardown drops the callbacks of transforms + * nobody finished). A writer handle retains the transform while bound, + * like a reader handle retains its stream. + */ +enum { SFW_WRITABLE, SFW_ERRORING, SFW_ERRORED, SFW_CLOSED }; + +typedef struct SfTransform SfTransform; + +typedef struct SfWriteReq { + ScrDyn *chunk; + ScrPromise *promise; + struct SfWriteReq *next; +} SfWriteReq; + +typedef struct SfWriter { + size_t rc; + size_t handle_refs; + SfTransform *stream; /* weak; handle refs retain it while bound */ + ScrPromise *ready; + ScrPromise *closed; +} SfWriter; + +struct SfTransform { + size_t rc; + SfStream *readable; + /* WritableStream */ + bool started; /* the start algorithm's promise settled (one turn) */ + int state; + ScrDyn *stored_error; + SfWriter *writer; /* owned while locked */ + SfWriteReq *head; /* queued writes (chunk + request promise) */ + SfWriteReq *tail; + SfWriteReq *in_flight; + size_t queue_size; /* queueTotalSize: queued chunks + the in-flight one */ + bool close_queued; /* the close sentinel is in the queue */ + ScrPromise *close_request; + ScrPromise *in_flight_close; + bool has_pending_abort; + ScrPromise *pending_abort; + /* The sink abort found a finish promise from a cancel or close, which + * settles the abort instead (Node returns that promise). */ + bool abort_joins_finish; + /* The abort request's outcome between SFT_ABORT_DONE and + * SFT_ABORT_SETTLE: a rejection reason (+1), or NULL to fulfill. */ + ScrDyn *abort_settle_error; + ScrDyn *pending_abort_reason; + bool pending_abort_was_erroring; + bool backpressure; + /* TransformStream */ + bool t_backpressure; + ScrPromise *bp_change; + bool write_waiting; /* the in-flight write waits for bp_change */ + ScrDyn *write_error; /* the in-flight write's failure, if any */ + ScrDyn *cancel_reason; /* the readable side's cancel reason, until it reacts */ + ScrPromise *finish; /* finishPromise of close/abort/cancel */ +}; + +static SfTransform *sf_transform_retain(SfTransform *t) { + t->rc++; + return t; +} + +static void sf_write_req_free(SfWriteReq *w) { + if (!w) return; + scr_dyn_release(w->chunk); + scr_promise_release(w->promise); + free(w); +} + +static void sf_writer_release(SfWriter *w) { + if (!w || --w->rc > 0) return; + scr_promise_release(w->ready); + scr_promise_release(w->closed); + free(w); +} + +static void sf_transform_release(SfTransform *t) { + if (!t || --t->rc > 0) return; + sf_stream_release(t->readable); + scr_dyn_release(t->stored_error); + if (t->writer) { + t->writer->stream = NULL; + sf_writer_release(t->writer); + } + while (t->head) { + SfWriteReq *w = t->head; + t->head = w->next; + sf_write_req_free(w); + } + sf_write_req_free(t->in_flight); + scr_promise_release(t->close_request); + scr_promise_release(t->in_flight_close); + scr_promise_release(t->pending_abort); + scr_dyn_release(t->pending_abort_reason); + scr_dyn_release(t->abort_settle_error); + scr_promise_release(t->bp_change); + scr_promise_release(t->finish); + scr_dyn_release(t->write_error); + scr_dyn_release(t->cancel_reason); + free(t); +} + +static void *sf_transform_retain_v(void *p) { + return sf_transform_retain((SfTransform *)p); +} +static void sf_transform_release_v(void *p) { + sf_transform_release((SfTransform *)p); +} + +static void *sf_writer_handle_retain_v(void *p) { + SfWriter *w = p; + w->rc++; + w->handle_refs++; + if (w->stream) sf_transform_retain(w->stream); + return w; +} + +static void sf_writer_handle_release_v(void *p) { + SfWriter *w = p; + SfTransform *t = w->stream; + if (w->handle_refs > 0) w->handle_refs--; + sf_writer_release(w); + sf_transform_release(t); +} + +static ScrBox *sf_transform_box(SfTransform *t) { + ScrBox *box = scr_box_new_obj(&sf_transform_retain_v, + &sf_transform_release_v, NULL); + scr_box_set_ref(box, sf_transform_retain(t)); + return box; +} + +static void sf_invalid_state(const char *message) { + char buf[96]; + int n = snprintf(buf, sizeof buf, "Invalid state: %s", message); + scr_throw_error_msg_code(SCR_ERR_TYPE, buf, (size_t)n, "ERR_INVALID_STATE"); +} + +static ScrPromise *sf_promise_invalid_state(const char *message) { + ScrPromise *p = scr_promise_new(); + sf_invalid_state(message); + scr_promise_reject_pending(p); + return p; +} + +static ScrPromise *sf_promise_rejected_with(ScrDyn *reason) { + ScrPromise *p = scr_promise_new(); + sf_reject_promise_reason(p, reason); + return p; +} + +static ScrPromise *sf_promise_resolved(void) { + ScrPromise *p = scr_promise_new(); + scr_promise_fulfill_void(p); + return p; +} + +/* Deferred steps: the reaction `op` after `ticks` microtask turns. */ +enum { + SFT_WRITE_DONE, + SFT_WRITE_CONTINUE, + SFT_CLOSE_DONE, + SFT_ABORT_DONE, + SFT_START, + SFT_CANCEL_DONE, + SFT_ABORT_SETTLE, +}; + +static void sf_transform_step(SfTransform *t, int op); + +typedef struct SfTransformTask { + size_t rc; + SfTransform *t; + int op; + int ticks; +} SfTransformTask; + +static void *sf_transform_task_retain_v(void *p) { + ((SfTransformTask *)p)->rc++; + return p; +} + +static void sf_transform_task_release_v(void *p) { + SfTransformTask *task = p; + if (--task->rc > 0) return; + sf_transform_release(task->t); + free(task); +} + +static void sf_transform_tick_task(ScrClosure *cb) { + SfTransformTask *task = (SfTransformTask *)scr_box_get_ref(cb->caps[0]); + if (--task->ticks > 0) { + ScrClosure *next = scr_closure_new((void *)&sf_transform_tick_task, 1); + next->caps[0] = scr_box_new_obj(&sf_transform_task_retain_v, + &sf_transform_task_release_v, NULL); + scr_box_set_ref(next->caps[0], sf_transform_task_retain_v(task)); + scr_queue_microtask(next); + } else { + sf_transform_step(task->t, task->op); + } + sf_transform_task_release_v(task); +} + +static void sf_transform_defer(SfTransform *t, int op, int ticks) { + SfTransformTask *task = calloc(1, sizeof *task); + if (!task) sf_oom(); + task->rc = 1; + task->t = sf_transform_retain(t); + task->op = op; + task->ticks = ticks; + ScrClosure *cb = scr_closure_new((void *)&sf_transform_tick_task, 1); + cb->caps[0] = scr_box_new_obj(&sf_transform_task_retain_v, + &sf_transform_task_release_v, NULL); + scr_box_set_ref(cb->caps[0], task); /* the box takes the reference */ + scr_queue_microtask(cb); +} + +/* ── WritableStream abstract operations ── */ + +static bool sfw_close_queued_or_in_flight(const SfTransform *t) { + return t->close_request || t->in_flight_close; +} + +static bool sfw_has_operation_in_flight(const SfTransform *t) { + return t->in_flight || t->in_flight_close; +} + +static void sfw_writer_ensure_ready_rejected(SfWriter *w, ScrDyn *reason) { + if (!scr_promise_is_pending(w->ready)) { + scr_promise_release(w->ready); + w->ready = scr_promise_new(); + } + sf_reject_promise_reason(w->ready, reason); + scr_promise_mark_handled(w->ready); +} + +static void sfw_writer_ensure_closed_rejected(SfWriter *w, ScrDyn *reason) { + if (!scr_promise_is_pending(w->closed)) { + scr_promise_release(w->closed); + w->closed = scr_promise_new(); + } + sf_reject_promise_reason(w->closed, reason); + scr_promise_mark_handled(w->closed); +} + +static void sfw_update_backpressure(SfTransform *t, bool backpressure) { + SfWriter *w = t->writer; + if (w && backpressure != t->backpressure) { + if (backpressure) { + scr_promise_release(w->ready); + w->ready = scr_promise_new(); + scr_promise_mark_handled(w->ready); + } else { + scr_promise_fulfill_void(w->ready); + } + } + t->backpressure = backpressure; +} + +static void sfw_reject_close_and_closed(SfTransform *t) { + if (t->close_request) { + sf_reject_promise_reason(t->close_request, t->stored_error); + scr_promise_release(t->close_request); + t->close_request = NULL; + } + if (t->writer) { + sf_reject_promise_reason(t->writer->closed, t->stored_error); + scr_promise_mark_handled(t->writer->closed); + } +} + +static void sft_abort_algorithm(SfTransform *t, ScrDyn *reason); + +static void sfw_finish_erroring(SfTransform *t) { + t->state = SFW_ERRORED; + /* The controller's queue resets; every queued write rejects. */ + while (t->head) { + SfWriteReq *w = t->head; + t->head = w->next; + sf_reject_promise_reason(w->promise, t->stored_error); + sf_write_req_free(w); + } + t->tail = NULL; + t->queue_size = 0; + t->close_queued = false; + if (!t->has_pending_abort) { + sfw_reject_close_and_closed(t); + return; + } + if (t->pending_abort_was_erroring) { + t->has_pending_abort = false; + sf_reject_promise_reason(t->pending_abort, t->stored_error); + scr_promise_release(t->pending_abort); + t->pending_abort = NULL; + scr_dyn_release(t->pending_abort_reason); + t->pending_abort_reason = NULL; + sfw_reject_close_and_closed(t); + return; + } + /* The sink's abort runs; its promise settles the abort request. */ + sft_abort_algorithm(t, t->pending_abort_reason); +} + +static void sfw_start_erroring(SfTransform *t, ScrDyn *reason) { + t->state = SFW_ERRORING; + scr_dyn_release(t->stored_error); + t->stored_error = scr_dyn_retain(reason); + if (t->writer) sfw_writer_ensure_ready_rejected(t->writer, reason); + if (!sfw_has_operation_in_flight(t) && t->started) sfw_finish_erroring(t); +} + +static void sfw_deal_with_rejection(SfTransform *t, ScrDyn *error) { + if (t->state == SFW_WRITABLE) { + sfw_start_erroring(t, error); + return; + } + sfw_finish_erroring(t); +} + +static void sfw_error_if_needed(SfTransform *t, ScrDyn *error) { + if (t->state == SFW_WRITABLE) sfw_start_erroring(t, error); +} + +static void sft_set_backpressure(SfTransform *t, bool backpressure); +static void sft_sink_write(SfTransform *t); +static void sft_sink_close(SfTransform *t); + +static void sfw_advance_queue(SfTransform *t) { + if (!t->started || t->in_flight || t->in_flight_close) return; + if (t->state == SFW_ERRORING) { + sfw_finish_erroring(t); + return; + } + if (t->head) { + SfWriteReq *w = t->head; + t->head = w->next; + if (!t->head) t->tail = NULL; + w->next = NULL; + t->in_flight = w; + sft_sink_write(t); + return; + } + if (t->close_queued) { + t->close_queued = false; + t->in_flight_close = t->close_request; + t->close_request = NULL; + sft_sink_close(t); + } +} + +static void sfw_finish_write(SfTransform *t, ScrDyn *error) { + SfWriteReq *w = t->in_flight; + t->in_flight = NULL; + if (error) { + sf_reject_promise_reason(w->promise, error); + sf_write_req_free(w); + sfw_deal_with_rejection(t, error); + return; + } + scr_promise_fulfill_void(w->promise); + sf_write_req_free(w); + if (t->queue_size) t->queue_size--; + if (!sfw_close_queued_or_in_flight(t) && t->state == SFW_WRITABLE) { + sfw_update_backpressure(t, t->queue_size >= 1); + } + sfw_advance_queue(t); +} + +static void sfw_finish_close(SfTransform *t, ScrDyn *error) { + ScrPromise *p = t->in_flight_close; + t->in_flight_close = NULL; + if (error) { + sf_reject_promise_reason(p, error); + scr_promise_release(p); + if (t->has_pending_abort) { + t->has_pending_abort = false; + sf_reject_promise_reason(t->pending_abort, error); + scr_promise_release(t->pending_abort); + t->pending_abort = NULL; + scr_dyn_release(t->pending_abort_reason); + t->pending_abort_reason = NULL; + } + sfw_deal_with_rejection(t, error); + return; + } + scr_promise_fulfill_void(p); + scr_promise_release(p); + if (t->state == SFW_ERRORING) { + scr_dyn_release(t->stored_error); + t->stored_error = NULL; + if (t->has_pending_abort) { + t->has_pending_abort = false; + scr_promise_fulfill_void(t->pending_abort); + scr_promise_release(t->pending_abort); + t->pending_abort = NULL; + scr_dyn_release(t->pending_abort_reason); + t->pending_abort_reason = NULL; + } + } + t->state = SFW_CLOSED; + if (t->writer) scr_promise_fulfill_void(t->writer->closed); +} + +/* WritableStreamAbort: +1 promise. */ +static ScrPromise *sfw_abort(SfTransform *t, ScrDyn *reason) { + if (t->state == SFW_CLOSED || t->state == SFW_ERRORED) { + return sf_promise_resolved(); + } + if (t->has_pending_abort) return scr_promise_retain(t->pending_abort); + bool was_erroring = t->state == SFW_ERRORING; + ScrPromise *p = scr_promise_new(); + t->has_pending_abort = true; + t->pending_abort = scr_promise_retain(p); + t->pending_abort_reason = + scr_dyn_retain(was_erroring ? scr_dyn_undefined() : reason); + t->pending_abort_was_erroring = was_erroring; + if (!was_erroring) sfw_start_erroring(t, reason); + return p; +} + +/* WritableStreamClose: +1 promise. */ +static ScrPromise *sfw_close(SfTransform *t) { + if (t->state == SFW_CLOSED || t->state == SFW_ERRORED) { + char message[64]; + snprintf(message, sizeof message, "WritableStream is %s", + t->state == SFW_CLOSED ? "closed" : "errored"); + return sf_promise_invalid_state(message); + } + ScrPromise *p = scr_promise_new(); + t->close_request = scr_promise_retain(p); + if (t->writer && t->backpressure && t->state == SFW_WRITABLE) { + scr_promise_fulfill_void(t->writer->ready); + } + t->close_queued = true; + sfw_advance_queue(t); + return p; +} + +/* WritableStreamDefaultWriterWrite: +1 promise. */ +static ScrPromise *sfw_writer_write(SfWriter *w, ScrDyn *chunk) { + SfTransform *t = w->stream; + if (!t) return sf_promise_invalid_state("Writer is not bound to a WritableStream"); + if (t->state == SFW_ERRORED || t->state == SFW_ERRORING) { + return sf_promise_rejected_with(t->stored_error); + } + if (sfw_close_queued_or_in_flight(t) || t->state == SFW_CLOSED) { + return sf_promise_invalid_state("WritableStream is closed"); + } + SfWriteReq *req = calloc(1, sizeof *req); + if (!req) sf_oom(); + req->chunk = scr_dyn_retain(chunk); + req->promise = scr_promise_new(); + ScrPromise *p = scr_promise_retain(req->promise); + if (t->tail) t->tail->next = req; + else t->head = req; + t->tail = req; + t->queue_size++; + sfw_update_backpressure(t, t->queue_size >= 1); + sfw_advance_queue(t); + return p; +} + +static SfWriter *sfw_get_writer(SfTransform *t) { + if (t->writer) { + sf_invalid_state("WritableStream is locked"); + return NULL; + } + SfWriter *w = calloc(1, sizeof *w); + if (!w) sf_oom(); + w->rc = 1; + w->stream = t; + w->ready = scr_promise_new(); + w->closed = scr_promise_new(); + scr_promise_mark_handled(w->ready); + scr_promise_mark_handled(w->closed); + switch (t->state) { + case SFW_WRITABLE: + if (sfw_close_queued_or_in_flight(t) || !t->backpressure) { + scr_promise_fulfill_void(w->ready); + } + break; + case SFW_ERRORING: + sf_reject_promise_reason(w->ready, t->stored_error); + break; + case SFW_CLOSED: + scr_promise_fulfill_void(w->ready); + scr_promise_fulfill_void(w->closed); + break; + default: + sf_reject_promise_reason(w->ready, t->stored_error); + sf_reject_promise_reason(w->closed, t->stored_error); + break; + } + t->writer = w; + w->rc++; /* the stream's ownership */ + return w; +} + +static void sfw_writer_release_lock(SfWriter *w) { + SfTransform *t = w->stream; + if (!t) return; + sf_invalid_state("Writer has been released"); + ScrCaught *caught = scr_exc_take(); + ScrDyn *released = scr_caught_to_dyn(caught); + scr_caught_release(caught); + sfw_writer_ensure_ready_rejected(w, released); + sfw_writer_ensure_closed_rejected(w, released); + scr_dyn_release(released); + size_t handle_refs = w->handle_refs; + t->writer = NULL; + w->stream = NULL; + sf_writer_release(w); /* the stream's ownership */ + while (handle_refs-- > 0) sf_transform_release(t); +} + +/* ── TransformStream algorithms (identity transformer) ── */ + +static void sft_set_backpressure(SfTransform *t, bool backpressure) { + if (t->bp_change) { + scr_promise_fulfill_void(t->bp_change); + scr_promise_release(t->bp_change); + } + t->bp_change = scr_promise_new(); + t->t_backpressure = backpressure; + if (t->write_waiting) { + t->write_waiting = false; + sf_transform_defer(t, SFT_WRITE_CONTINUE, 1); + } +} + +/* After an enqueue: whether a read is still waiting. A fetch upload's read + * was just satisfied by the chunk, and it reads again through the next + * pull, as a reader's next read() does. */ +static bool sft_readable_wants_chunk(const SfStream *r) { + return r->collector || (r->reader && r->reader->pending_head); +} + +/* The readable side is closed or errored: a pull still waiting on + * bp_change can no longer receive a chunk, so complete it. */ +static void sft_finish_pull(SfTransform *t) { + if (t->bp_change && scr_promise_is_pending(t->bp_change)) scr_promise_fulfill_void(t->bp_change); +} + +static bool sft_readable_can_enqueue(const SfStream *r) { + return !r->close_requested && !r->closed && !r->error; +} + +/* TransformStreamErrorWritableAndUnblockWrite */ +static void sft_error_writable(SfTransform *t, ScrDyn *error) { + sfw_error_if_needed(t, error); + if (t->t_backpressure) sft_set_backpressure(t, false); +} + +/* The identity transform's enqueue: NULL on success, else +1 error. */ +static ScrDyn *sft_perform_transform(SfTransform *t, ScrDyn *chunk) { + SfStream *r = t->readable; + if (!sft_readable_can_enqueue(r)) { + sf_invalid_state("Unable to enqueue"); + ScrCaught *caught = scr_exc_take(); + ScrDyn *error = scr_caught_to_dyn(caught); + scr_caught_release(caught); + sft_error_writable(t, error); + return error; + } + sf_stream_enqueue_value(r, chunk); + if (scr_exc_pending()) { + ScrCaught *caught = scr_exc_take(); + ScrDyn *error = scr_caught_to_dyn(caught); + scr_caught_release(caught); + sf_stream_error(r, error); + sft_error_writable(t, error); + return error; + } + bool backpressure = !sft_readable_wants_chunk(r); + if (backpressure != t->t_backpressure) sft_set_backpressure(t, true); + return NULL; +} + +static void sft_sink_write(SfTransform *t) { + if (t->t_backpressure) { + t->write_waiting = true; + return; + } + ScrDyn *error = sft_perform_transform(t, t->in_flight->chunk); + if (error) { + scr_dyn_release(t->write_error); + t->write_error = error; + } + sf_transform_defer(t, SFT_WRITE_DONE, 2); +} + +static void sft_sink_close(SfTransform *t) { + if (!t->finish) { + t->finish = scr_promise_new(); + scr_promise_mark_handled(t->finish); + sf_transform_defer(t, SFT_CLOSE_DONE, 1); + return; + } + /* A cancel or abort already finished the transform. */ + sf_transform_defer(t, SFT_CLOSE_DONE, 1); +} + +static void sft_abort_algorithm(SfTransform *t, ScrDyn *reason) { + (void)reason; + t->abort_joins_finish = t->finish != NULL; + if (!t->finish) { + t->finish = scr_promise_new(); + scr_promise_mark_handled(t->finish); + } + sf_transform_defer(t, SFT_ABORT_DONE, 1); +} + +static void sf_transform_step(SfTransform *t, int op) { + switch (op) { + case SFT_WRITE_CONTINUE: { + if (!t->in_flight) return; + if (t->state == SFW_ERRORING) { + ScrDyn *error = scr_dyn_retain(t->stored_error); + scr_dyn_release(t->write_error); + t->write_error = error; + } else { + ScrDyn *error = sft_perform_transform(t, t->in_flight->chunk); + if (error) { + scr_dyn_release(t->write_error); + t->write_error = error; + } + } + sf_transform_defer(t, SFT_WRITE_DONE, 3); + return; + } + case SFT_WRITE_DONE: { + if (!t->in_flight) return; + ScrDyn *error = t->write_error; + t->write_error = NULL; + sfw_finish_write(t, error); + scr_dyn_release(error); + return; + } + case SFT_CLOSE_DONE: { + SfStream *r = t->readable; + ScrDyn *error = NULL; + if (r->error) { + error = scr_dyn_retain(r->error); + } else { + sf_stream_close(r); + } + sft_finish_pull(t); + if (error) sf_reject_promise_reason(t->finish, error); + else scr_promise_fulfill_void(t->finish); + if (t->in_flight_close) sfw_finish_close(t, error); + scr_dyn_release(error); + return; + } + case SFT_ABORT_DONE: { + SfStream *r = t->readable; + scr_dyn_release(t->abort_settle_error); + t->abort_settle_error = NULL; + if (t->abort_joins_finish && !scr_promise_is_pending(t->finish)) { + /* The cancel or close that created the finish promise settled + * it; the abort request takes its result. */ + if (scr_promise_is_rejected(t->finish)) + t->abort_settle_error = scr_promise_reason_dyn(t->finish); + } else { + ScrDyn *reason = t->pending_abort_reason + ? scr_dyn_retain(t->pending_abort_reason) + : scr_dyn_retain(scr_dyn_undefined()); + bool readable_errored = r->error != NULL; + if (!readable_errored) sf_stream_error(r, reason); + sft_finish_pull(t); + /* The sink abort algorithm's finish promise, which a + * readable.cancel() after the abort answers too: rejected with the + * readable side's stored error when it had already errored, else + * resolved. */ + if (t->finish && scr_promise_is_pending(t->finish)) { + if (readable_errored) sf_reject_promise_reason(t->finish, r->error); + else scr_promise_fulfill_void(t->finish); + } + if (readable_errored) t->abort_settle_error = scr_dyn_retain(r->error); + scr_dyn_release(reason); + } + /* WritableStreamFinishErroring settles the abort request in a + * reaction to the sink abort's promise, two turns after it. */ + sf_transform_defer(t, SFT_ABORT_SETTLE, 2); + return; + } + case SFT_ABORT_SETTLE: { + ScrDyn *error = t->abort_settle_error; + t->abort_settle_error = NULL; + if (t->has_pending_abort) { + t->has_pending_abort = false; + if (error) sf_reject_promise_reason(t->pending_abort, error); + else scr_promise_fulfill_void(t->pending_abort); + scr_promise_release(t->pending_abort); + t->pending_abort = NULL; + scr_dyn_release(t->pending_abort_reason); + t->pending_abort_reason = NULL; + } + sfw_reject_close_and_closed(t); + scr_dyn_release(error); + return; + } + case SFT_CANCEL_DONE: { + ScrDyn *reason = t->cancel_reason; + t->cancel_reason = NULL; + if (t->state == SFW_ERRORED) { + sf_reject_promise_reason(t->finish, t->stored_error); + } else { + sfw_error_if_needed(t, reason ? reason : scr_dyn_undefined()); + if (t->t_backpressure) sft_set_backpressure(t, false); + sft_finish_pull(t); + scr_promise_fulfill_void(t->finish); + } + scr_dyn_release(reason); + return; + } + case SFT_START: { + /* Both sides start one turn after construction, the writable side + * first (Node registers its start reaction first). */ + t->started = true; + sfw_advance_queue(t); + SfStream *r = t->readable; + r->started = true; + if (!r->close_requested && !r->closed && !r->error) { + sf_stream_drain(r); + sf_stream_pull(r); + } + return; + } + } +} + +/* The readable side's pull: release backpressure; the pull completes when + * backpressure changes again. */ +static ScrDyn *sft_pull_thunk(ScrClosure *cb, ScrDyn *const *args, + size_t argc) { + (void)args; + (void)argc; + SfTransform *t = (SfTransform *)scr_box_get_ref(cb->caps[0]); + sft_set_backpressure(t, false); + ScrDyn *out = scr_dyn_new_promise(t->bp_change); + sf_transform_release(t); + return out; +} + +/* The readable side's cancel (TransformStreamDefaultSourceCancelAlgorithm): + * errors the writable side with the reason and unblocks a waiting write. */ +static ScrDyn *sft_cancel_thunk(ScrClosure *cb, ScrDyn *const *args, + size_t argc) { + SfTransform *t = (SfTransform *)scr_box_get_ref(cb->caps[0]); + ScrDyn *reason = argc ? args[0] : scr_dyn_undefined(); + if (!t->finish) { + t->finish = scr_promise_new(); + scr_promise_mark_handled(t->finish); + scr_dyn_release(t->cancel_reason); + t->cancel_reason = scr_dyn_retain(reason); + /* The (identity) cancel algorithm's resolved promise reacts a turn + * later. */ + sf_transform_defer(t, SFT_CANCEL_DONE, 1); + } + ScrDyn *out = scr_dyn_new_promise(t->finish); + sf_transform_release(t); + return out; +} + +static ScrDyn *sft_func(SfTransform *t, ScrDynThunk thunk, const char *name) { + ScrClosure *cb = scr_closure_new((void *)thunk, 1); + cb->caps[0] = sf_transform_box(t); + return scr_dyn_new_func(cb, thunk, 1, "(controller)", name); +} + +ScrDyn *scr_fetch_transform_new(void) { + SfTransform *t = calloc(1, sizeof *t); + if (!t) sf_oom(); + t->rc = 1; + t->state = SFW_WRITABLE; + SfStream *r = sf_stream_new_native(); + r->started = false; + r->hwm_zero = true; + r->pull_cb = sft_func(t, &sft_pull_thunk, "pull"); + r->cancel_cb = sft_func(t, &sft_cancel_thunk, "cancel"); + r->source_this = scr_dyn_retain(scr_dyn_undefined()); + sf_stream_track_callbacks(r); + t->readable = r; + /* The writable side's high-water mark of 1 leaves room: no backpressure. + * The readable side's 0 does: writes wait for a read. */ + t->backpressure = false; + sft_set_backpressure(t, true); + sf_transform_defer(t, SFT_START, 1); + ScrDyn *out = scr_dyn_new_handle(t, SCR_DYNH_WEB_TRANSFORM); + sf_transform_release(t); + return out; +} + +static ScrDyn *sf_transform_get(void *ptr, const char *key, size_t len) { + SfTransform *t = ptr; + if (sf_name(key, len, "readable")) { + return scr_dyn_new_handle(t->readable, SCR_DYNH_WEB_STREAM); + } + if (sf_name(key, len, "writable")) { + return scr_dyn_new_handle(t, SCR_DYNH_WEB_WRITABLE); + } + return NULL; +} + +static ScrDyn *sf_transform_invoke(void *ptr, ScrDyn *self, + const char *method, ScrDyn *const *args, + size_t argc, const char *what) { + (void)ptr; + (void)self; + (void)args; + (void)argc; + (void)method; + sf_not_function(what); + return NULL; +} + +static ScrDyn *sf_promise_dyn(ScrPromise *p) { + ScrDyn *out = scr_dyn_new_promise(p); + scr_promise_release(p); + return out; +} + +static ScrDyn *sf_writable_invoke(void *ptr, ScrDyn *self, + const char *method, ScrDyn *const *args, + size_t argc, const char *what) { + SfTransform *t = ptr; + (void)self; + if (strcmp(method, "getWriter") == 0) { + SfWriter *w = sfw_get_writer(t); + if (!w) return NULL; + ScrDyn *out = scr_dyn_new_handle(w, SCR_DYNH_WEB_WRITER); + sf_writer_release(w); + return out; + } + if (strcmp(method, "abort") == 0) { + if (t->writer) { + return sf_promise_dyn( + sf_promise_invalid_state("WritableStream is locked")); + } + return sf_promise_dyn(sfw_abort(t, argc ? args[0] : scr_dyn_undefined())); + } + if (strcmp(method, "close") == 0) { + if (t->writer) { + return sf_promise_dyn( + sf_promise_invalid_state("WritableStream is locked")); + } + if (sfw_close_queued_or_in_flight(t)) { + return sf_promise_dyn( + sf_promise_invalid_state("WritableStream is closed")); + } + return sf_promise_dyn(sfw_close(t)); + } + sf_not_function(what); + return NULL; +} + +static ScrDyn *sf_writable_get(void *ptr, const char *key, size_t len) { + SfTransform *t = ptr; + if (sf_name(key, len, "locked")) return scr_dyn_new_bool(t->writer != NULL); + return NULL; +} + +static ScrDyn *sf_writer_invoke(void *ptr, ScrDyn *self, const char *method, + ScrDyn *const *args, size_t argc, + const char *what) { + SfWriter *w = ptr; + (void)self; + if (strcmp(method, "write") == 0) { + return sf_promise_dyn( + sfw_writer_write(w, argc ? args[0] : scr_dyn_undefined())); + } + if (strcmp(method, "close") == 0) { + SfTransform *t = w->stream; + if (!t) { + return sf_promise_dyn(sf_promise_invalid_state( + "Writer is not bound to a WritableStream")); + } + if (sfw_close_queued_or_in_flight(t)) { + return sf_promise_dyn( + sf_promise_invalid_state("WritableStream is closed")); + } + return sf_promise_dyn(sfw_close(t)); + } + if (strcmp(method, "abort") == 0) { + SfTransform *t = w->stream; + if (!t) { + return sf_promise_dyn(sf_promise_invalid_state( + "Writer is not bound to a WritableStream")); + } + return sf_promise_dyn(sfw_abort(t, argc ? args[0] : scr_dyn_undefined())); + } + if (strcmp(method, "releaseLock") == 0) { + sfw_writer_release_lock(w); + return scr_dyn_retain(scr_dyn_undefined()); + } + sf_not_function(what); + return NULL; +} + +static ScrDyn *sf_writer_get(void *ptr, const char *key, size_t len) { + SfWriter *w = ptr; + if (sf_name(key, len, "ready")) return scr_dyn_new_promise(w->ready); + if (sf_name(key, len, "closed")) return scr_dyn_new_promise(w->closed); + if (sf_name(key, len, "desiredSize")) { + SfTransform *t = w->stream; + if (!t) { + sf_invalid_state("Writer is not bound to a WritableStream"); + return NULL; + } + if (t->state == SFW_ERRORED || t->state == SFW_ERRORING) { + return scr_dyn_new_null(); + } + if (t->state == SFW_CLOSED) return scr_dyn_new_num(0); + return scr_dyn_new_num(1.0 - (double)t->queue_size); + } + return NULL; +} + /* ── Response ────────────────────────────────────────────────────── */ static SfResponse *sf_response_retain(SfResponse *r) { @@ -2729,12 +3934,54 @@ static ScrPromise *sf_response_collect(SfResponse *r, int mode) { return p; } +/* The body and metadata copy of Response.clone and Request.clone (undici + * cloneResponse/cloneBody): the body tees, so the original and the clone + * each keep one branch, and the clone's headers are a copy with the same + * guard. NULL with a TypeError pending when the body is unusable. */ +static SfResponse *sf_response_clone(SfResponse *r, const char *unusable) { + if (!r->null_body && + (r->body->disturbed || r->body->reader || r->body->internal_lock)) { + sf_type_error(unusable); + return NULL; + } + SfResponse *c = calloc(1, sizeof *c); + if (!c) sf_oom(); + c->rc = 1; + if (r->null_body) { + c->body = sf_stream_new_native(); + sf_stream_close(c->body); + } else { + SfStream *branches[2]; + sf_stream_tee(r->body, branches); + sf_stream_release(r->body); + r->body = branches[0]; + c->body = branches[1]; + } + c->headers = sf_headers_new(scr_arr_slice(r->headers->pairs, 0, INFINITY), + r->headers->immutable); + c->url = scr_str_retain(r->url); + c->status_text = scr_str_retain(r->status_text); + c->status = r->status; + c->redirected = r->redirected; + c->null_body = r->null_body; + c->light_length = r->light_length; + return c; +} + static ScrDyn *sf_response_invoke(void *ptr, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, const char *what) { SfResponse *r = ptr; sf_response_materialize(r); + if (strcmp(method, "clone") == 0) { + SfResponse *c = sf_response_clone( + r, "Response.clone: Body has already been consumed."); + if (!c) return NULL; + ScrDyn *out = scr_dyn_new_handle(c, SCR_DYNH_FETCH_RESPONSE); + sf_response_release(c); + return out; + } int mode = -1; if (strcmp(method, "json") == 0) mode = SF_COLLECT_JSON; else if (strcmp(method, "text") == 0) mode = SF_COLLECT_TEXT; @@ -3323,14 +4570,13 @@ static bool sf_pairs_have(ScrArr *pairs, const char *name) { size_t n = (size_t)scr_arr_len(pairs); size_t want = strlen(name); for (size_t i = 0; i + 1 < n; i += 2) { - ScrStr *key = (ScrStr *)scr_arr_get_ref(pairs, (double)i); + ScrStr *key = sf_pair_at(pairs, i); bool same = key->len == want; for (size_t j = 0; j < want && same; j++) { char c = key->data[j]; if (c >= 'A' && c <= 'Z') c = (char)(c - 'A' + 'a'); same = c == name[j]; } - scr_str_release(key); if (same) return true; } return false; @@ -3430,6 +4676,21 @@ static bool sf_request_header_ok( !sf_header_name_ci(name, name_len, "expect"); } +/* Whether every value in `pairs` is already trimmed (sf_trim_header_value + * would keep it): the form a copy can take unchanged (SfHeaders.canonical). + * Combining duplicate names with an empty value leaves the separator's + * space at the end of the combined value, which a copy trims again. */ +static bool sf_header_values_trimmed(ScrArr *pairs) { + size_t n = (size_t)scr_arr_len(pairs); + for (size_t i = 1; i < n; i += 2) { + const ScrStr *value = sf_pair_at(pairs, i); + if (value->len == 0) continue; + char first = value->data[0], last = value->data[value->len - 1]; + if (first == ' ' || first == '\t' || last == ' ' || last == '\t') return false; + } + return true; +} + static ScrStr *sf_trim_header_value(const ScrStr *value) { size_t start = 0; size_t end = value->len; @@ -3441,11 +4702,14 @@ static ScrStr *sf_trim_header_value(const ScrStr *value) { (value->data[end - 1] == ' ' || value->data[end - 1] == '\t')) { end--; } + if (start == 0 && end == value->len) return scr_str_retain((ScrStr *)value); return scr_str_new(value->data + start, end - start); } -static bool sf_push_header(ScrArr *pairs, const char *name, size_t name_len, +static bool sf_push_header(ScrArr *pairs, ScrStr *name_str, ScrStr *value, bool request_guard) { + const char *name = name_str->data; + size_t name_len = name_str->len; if (!sf_header_name_ok(name, name_len)) { ScrStr *display = scr_str_new(name, name_len); sf_headers_invalid("append", display, "name"); @@ -3471,7 +4735,7 @@ static bool sf_push_header(ScrArr *pairs, const char *name, size_t name_len, if (!sf_header_name_ci(name, name_len, "set-cookie")) { size_t n = (size_t)scr_arr_len(pairs); for (size_t i = 0; i + 1 < n; i += 2) { - ScrStr *key = scr_arr_get_ref(pairs, (double)i); + ScrStr *key = sf_pair_at(pairs, i); bool match = key->len == name_len; for (size_t j = 0; j < name_len && match; j++) { unsigned char a = (unsigned char)key->data[j]; @@ -3480,11 +4744,9 @@ static bool sf_push_header(ScrArr *pairs, const char *name, size_t name_len, if (b >= 'A' && b <= 'Z') b = (unsigned char)(b - 'A' + 'a'); match = a == b; } - scr_str_release(key); if (!match) continue; - ScrStr *previous = - scr_arr_get_ref(pairs, (double)(i + 1)); + ScrStr *previous = sf_pair_at(pairs, i + 1); const char *separator = sf_header_name_ci(name, name_len, "cookie") ? "; " : ", "; size_t separator_len = 2; @@ -3498,7 +4760,6 @@ static bool sf_push_header(ScrArr *pairs, const char *name, size_t name_len, normalized->data, normalized->len); ScrStr *joined = scr_str_new(joined_data, joined_len); free(joined_data); - scr_str_release(previous); scr_str_release(normalized); if (request_guard && !sf_request_header_ok(name, name_len, joined)) { scr_str_release(joined); @@ -3517,8 +4778,8 @@ static bool sf_push_header(ScrArr *pairs, const char *name, size_t name_len, scr_arr_push_ref( pairs, request_guard - ? scr_str_new(name, name_len) - : sf_header_name_lower(name, name_len)); + ? scr_str_retain(name_str) + : sf_header_name_lower_str(name_str)); scr_arr_push_ref(pairs, normalized); return true; } @@ -3545,10 +4806,13 @@ static void sf_headers_pair_length_error(size_t len) { * leaves header-name/value syntax validation for initialization. Response * construction converts this snapshot before status/statusText and only * validates it after body extraction, matching Undici's two phases. */ -static ScrArr *sf_headers_convert_init(const ScrDyn *headers) { +/* *canonical_out reports that the result copies a canonical Headers + * object's pairs (SfHeaders.canonical). */ +static ScrArr *sf_headers_convert_init(const ScrDyn *headers, bool *canonical_out) { + *canonical_out = false; if (headers && headers->kind == SCR_DYN_TYPED_REF) { ScrDyn *materialized = scr_dyn_typed_ref_materialize(headers); - ScrArr *converted = sf_headers_convert_init(materialized); + ScrArr *converted = sf_headers_convert_init(materialized, canonical_out); scr_dyn_release(materialized); return converted; } @@ -3639,13 +4903,9 @@ static ScrArr *sf_headers_convert_init(const ScrDyn *headers) { headers->v.handle.tag == SCR_DYNH_FETCH_HEADERS) { SfHeaders *source = (SfHeaders *)headers->v.handle.ptr; size_t n = (size_t)scr_arr_len(source->pairs); - for (size_t i = 0; i + 1 < n; i += 2) { - ScrStr *name = scr_arr_get_ref(source->pairs, (double)i); - ScrStr *value = - scr_arr_get_ref(source->pairs, (double)(i + 1)); - scr_arr_push_ref(converted, name); - scr_arr_push_ref(converted, value); - } + for (size_t i = 0; i < n - n % 2; i++) + scr_arr_push_ref(converted, scr_str_retain(sf_pair_at(source->pairs, i))); + *canonical_out = source->canonical; return converted; } /* Functions are WebIDL objects and an ordinary function has no @@ -3660,25 +4920,29 @@ fail: } static bool sf_add_converted_headers( - ScrArr *pairs, ScrArr *converted, bool request_guard) { + ScrArr *pairs, ScrArr *converted, bool request_guard, bool canonical) { size_t n = (size_t)scr_arr_len(converted); + /* Pushing canonical pairs into an empty list one by one through + * sf_push_header would reproduce them exactly. */ + if (canonical && !request_guard && scr_arr_len(pairs) == 0) { + for (size_t i = 0; i < n; i++) + scr_arr_push_ref(pairs, scr_str_retain(sf_pair_at(converted, i))); + return true; + } for (size_t i = 0; i + 1 < n; i += 2) { - ScrStr *name = scr_arr_get_ref(converted, (double)i); - ScrStr *value = scr_arr_get_ref(converted, (double)(i + 1)); - bool ok = sf_push_header( - pairs, name->data, name->len, value, request_guard); - scr_str_release(name); - scr_str_release(value); - if (!ok) return false; + if (!sf_push_header(pairs, sf_pair_at(converted, i), + sf_pair_at(converted, i + 1), request_guard)) + return false; } return true; } static bool sf_add_headers( ScrArr *pairs, const ScrDyn *headers, bool request_guard) { - ScrArr *converted = sf_headers_convert_init(headers); + bool canonical = false; + ScrArr *converted = sf_headers_convert_init(headers, &canonical); if (!converted) return false; - bool ok = sf_add_converted_headers(pairs, converted, request_guard); + bool ok = sf_add_converted_headers(pairs, converted, request_guard, canonical); scr_arr_release(converted); return ok; } @@ -4858,6 +6122,7 @@ static ScrDyn *sf_response_construct(ScrDyn *body, ScrDyn *init, SfJsonSerialize serialize) { ScrArr *header_pairs = NULL; ScrArr *converted_headers = NULL; + bool converted_canonical = false; ScrBytes *body_bytes = NULL; ScrStr *status_text = NULL; SfStream *stream = NULL; @@ -4922,7 +6187,7 @@ static ScrDyn *sf_response_construct(ScrDyn *body, ScrDyn *init, * later response-initialization step, after body extraction. */ ScrDyn *init_headers = sf_response_init_get(init, "headers", 7); if (light_ok) light_form = sf_light_header_form(init_headers, json_body, &light_record); - converted_headers = sf_headers_convert_init(init_headers); + converted_headers = sf_headers_convert_init(init_headers, &converted_canonical); scr_dyn_release(init_headers); if (!converted_headers) goto fail; @@ -5017,7 +6282,7 @@ static ScrDyn *sf_response_construct(ScrDyn *body, ScrDyn *init, header_pairs = scr_arr_new(SCR_ELEM_STR, 8); if (!sf_add_converted_headers( - header_pairs, converted_headers, false)) goto fail; + header_pairs, converted_headers, false, converted_canonical)) goto fail; scr_arr_release(converted_headers); converted_headers = NULL; @@ -5048,6 +6313,7 @@ static ScrDyn *sf_response_construct(ScrDyn *body, ScrDyn *init, response->rc = 1; response->body = stream; response->headers = sf_headers_new(header_pairs, false); + response->headers->canonical = sf_header_values_trimmed(header_pairs); response->url = scr_str_new("", 0); response->status_text = status_text; response->status = status; @@ -5157,19 +6423,50 @@ static ScrDyn *sf_request_get(void *ptr, const char *key, size_t len) { if (sf_name(key, len, "body") || sf_name(key, len, "bodyUsed") || sf_name(key, len, "headers")) return sf_response_get(r->payload, key, len); return NULL; } +/* Request.clone: the payload clones as a Response does, and the clone's + * signal follows the original's (undici: an AbortController aborted with + * the original signal's reason, at once when it already aborted). */ +static ScrDyn *sf_request_clone(SfRequest *r) { + SfResponse *payload = sf_response_clone(r->payload, "unusable"); + if (!payload) return NULL; + ScrDyn *sources = scr_dyn_new_arr(); + scr_dyn_arr_push(sources, + scr_dyn_new_handle(r->signal, SCR_DYNH_ABORT_SIGNAL)); + ScrDyn *follower = scr_fetch_abort_any(sources); + scr_dyn_release(sources); + if (!follower) { + sf_response_release(payload); + return NULL; + } + SfRequest *c = calloc(1, sizeof *c); + if (!c) sf_oom(); + c->rc = 1; + c->payload = payload; + c->url = scr_str_retain(r->url); + c->method = scr_str_retain(r->method); + c->redirect = scr_str_retain(r->redirect); + c->signal = sf_signal_retain((SfSignal *)follower->v.handle.ptr); + scr_dyn_release(follower); + ScrDyn *out = scr_dyn_new_handle(c, SCR_DYNH_FETCH_REQUEST); + sf_request_release_v(c); + return out; +} static ScrDyn *sf_request_invoke(void *ptr, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, const char *what) { SfRequest *r = ptr; + if (strcmp(method, "clone") == 0) return sf_request_clone(r); return sf_response_invoke(r->payload, self, method, args, argc, what); } ScrDyn *scr_fetch_headers_new(ScrDyn *init) { - ScrArr *converted = sf_headers_convert_init(init); + bool canonical = false; + ScrArr *converted = sf_headers_convert_init(init, &canonical); if (!converted) return NULL; ScrArr *pairs = scr_arr_new(SCR_ELEM_STR, 8); - bool ok = sf_add_converted_headers(pairs, converted, false); + bool ok = sf_add_converted_headers(pairs, converted, false, canonical); scr_arr_release(converted); if (!ok) { scr_arr_release(pairs); return NULL; } SfHeaders *headers = sf_headers_new(pairs, false); + headers->canonical = sf_header_values_trimmed(pairs); ScrDyn *result = scr_dyn_new_handle(headers, SCR_DYNH_FETCH_HEADERS); sf_headers_release(headers); return result; @@ -5672,13 +6969,20 @@ static const ScrDynHandleMethod sf_reader_methods[] = { {NULL, 0, false}}; static const ScrDynHandleMethod sf_body_methods[] = { {"json", 0, true}, {"text", 0, true}, {"bytes", 0, true}, - {"arrayBuffer", 0, true}, {NULL, 0, false}}; + {"arrayBuffer", 0, true}, {"clone", 0, false}, {NULL, 0, false}}; static const ScrDynHandleMethod sf_headers_methods[] = { {"append", 2, false}, {"delete", 1, false}, {"entries", 0, false}, {"forEach", 1, false}, {"get", 1, false}, {"getSetCookie", 0, false}, {"has", 1, false}, {"keys", 0, false}, {"set", 2, false}, {"values", 0, false}, {NULL, 0, false}}; +static const ScrDynHandleMethod sf_writable_methods[] = { + {"abort", 0, true}, {"close", 0, true}, {"getWriter", 0, false}, + {NULL, 0, false}}; +static const ScrDynHandleMethod sf_writer_methods[] = { + {"abort", 0, true}, {"close", 0, true}, {"releaseLock", 0, false}, + {"write", 0, true}, {NULL, 0, false}}; + static ScrDyn *sf_rejected(void) { ScrPromise *p = scr_promise_settled_ref(NULL, &scr_dyn_retain_v, &scr_dyn_release_v, NULL); @@ -5725,6 +7029,23 @@ static const ScrDynHandleOps sf_headers_ops = { &sf_headers_invoke, &sf_headers_get, &sf_no_set, NULL, NULL, &sf_headers_step, .methods = sf_headers_methods}; +static const ScrDynHandleOps sf_transform_ops = { + "TransformStream", &sf_transform_retain_v, &sf_transform_release_v, + &sf_transform_invoke, &sf_transform_get, &sf_no_set, NULL}; +static const ScrDynHandleOps sf_writable_ops = { + "WritableStream", &sf_transform_retain_v, &sf_transform_release_v, + &sf_writable_invoke, &sf_writable_get, &sf_no_set, NULL, + .methods = sf_writable_methods, + .invalid_this = "Value of \"this\" must be of type WritableStream", + .rejected = &sf_rejected}; +static const ScrDynHandleOps sf_writer_ops = { + "WritableStreamDefaultWriter", &sf_writer_handle_retain_v, + &sf_writer_handle_release_v, &sf_writer_invoke, &sf_writer_get, + &sf_no_set, NULL, .methods = sf_writer_methods, + .invalid_this = + "Value of \"this\" must be of type WritableStreamDefaultWriter", + .rejected = &sf_rejected}; + static void sf_teardown(void) { while (sf_live) { SfTransfer *t = sf_live; @@ -5764,6 +7085,9 @@ void scr_fetch_install(void) { scr_dyn_handle_install(SCR_DYNH_FETCH_REQUEST, &sf_request_ops); scr_dyn_handle_install(SCR_DYNH_FETCH_RESPONSE, &sf_response_ops); scr_dyn_handle_install(SCR_DYNH_FETCH_HEADERS, &sf_headers_ops); + scr_dyn_handle_install(SCR_DYNH_WEB_TRANSFORM, &sf_transform_ops); + scr_dyn_handle_install(SCR_DYNH_WEB_WRITABLE, &sf_writable_ops); + scr_dyn_handle_install(SCR_DYNH_WEB_WRITER, &sf_writer_ops); scr_et_signal_core_fn = sf_signal_core_of; scr_atexit(sf_teardown); } diff --git a/packages/runtime/src/scr_fs_stream.c b/packages/runtime/src/scr_fs_stream.c index 70a5ab075..940068a7b 100644 --- a/packages/runtime/src/scr_fs_stream.c +++ b/packages/runtime/src/scr_fs_stream.c @@ -79,6 +79,9 @@ typedef struct ScrFssOp { int64_t result; /* answer or -errno */ int retries; /* WRITE: zero-byte retries */ ScrError *carry; /* FSYNC/CLOSE: the error to pass on (+1) */ + /* The AsyncLocalStorage context the step was started in (+1): its + * continuation runs in it, as Node runs an fs request's callback. */ + ScrAlsCtx *ctx; } ScrFssOp; static void scr_fss_work(void *p) { @@ -101,6 +104,7 @@ static void scr_fss_op_destroy(void *p) { free(op->buf); if (op->chunk) scr_bytes_release(op->chunk); if (op->carry) scr_error_release(op->carry); + scr_als_ctx_release(op->ctx); free(op); } @@ -111,13 +115,20 @@ static ScrFssOp *scr_fss_op_new(ScrStream *s, ScrFssOpKind kind, int fd) { op->kind = kind; op->fd = fd; op->pos = -1; + op->ctx = scr_als_capture(); return op; } static void scr_fss_after(void *p); +static void scr_fss_after_in_context(void *p) { + ScrAlsCtx *prev = scr_als_dispatch_enter(((ScrFssOp *)p)->ctx); + scr_fss_after(p); + scr_als_dispatch_leave(prev); +} + static void scr_fss_submit(ScrFssOp *op) { - scr_work_submit(op, &scr_fss_work, &scr_fss_after, &scr_fss_op_destroy); + scr_work_submit(op, &scr_fss_work, &scr_fss_after_in_context, &scr_fss_op_destroy); } /* ── close (Node's close/_close) ──────────────────────────────────────── */ diff --git a/packages/runtime/src/scr_generator.h b/packages/runtime/src/scr_generator.h index 828f339d5..c73ceb900 100644 --- a/packages/runtime/src/scr_generator.h +++ b/packages/runtime/src/scr_generator.h @@ -192,6 +192,7 @@ ScrDyn *scr_fetch_abort_timeout(ScrDyn *delay); ScrDyn *scr_fetch_abort_now(ScrDyn *reason); /* borrowed reason; +1 handle */ ScrDyn *scr_fetch_abort_any(ScrDyn *signals); /* borrowed array; +1 handle or NULL pending */ ScrDyn *scr_fetch_stream_new(ScrDyn *source); /* borrowed source; +1 handle or NULL pending */ +ScrDyn *scr_fetch_transform_new(void); /* +1 identity TransformStream handle */ ScrDyn *scr_fetch_stream_from(ScrDyn *iterable); /* borrowed iterable; +1 handle or NULL pending */ ScrDyn *scr_fetch_stream_from_array( ScrArr *iterable, diff --git a/packages/runtime/src/scr_glibc_math.c b/packages/runtime/src/scr_glibc_math.c new file mode 100644 index 000000000..2ef2ed2e7 --- /dev/null +++ b/packages/runtime/src/scr_glibc_math.c @@ -0,0 +1,271 @@ +/* The libm functions that glibc executables reach without calling a Math + * transcendental: floor, ceil, trunc and round (LLVM's rounding intrinsics + * on x86-64 before SSE4.1, and the runtime's own calls), fmod (the % + * operator), sqrt (Math.sqrt's call for a negative argument), and ldexp, + * scalbn and frexp, which glibc exports from both libc and libm (the link + * would otherwise take them from libm). Each is exactly specified, so these + * definitions answer what glibc's do, the NaN payloads of NaN inputs + * included. Defining them in the executable lets a program that uses no + * other libm function link without libm.so (-lm is linked as needed), + * which saves loading it at startup. Library builds keep the embedder's + * libm. */ + +#include +#include + +#if defined(__GLIBC__) && !defined(SCR_LIB) + +/* The sanitizer runtime calls some of these functions while it initializes, + * and the dynamic loader calls the resolvers before that: none of the code + * here carries sanitizer instrumentation. no_sanitize("thread") alone keeps + * ThreadSanitizer's function entry and exit calls, which crash before its + * runtime initializes; disable_sanitizer_instrumentation removes them. */ +#pragma clang attribute push(__attribute__((no_sanitize("address", "undefined", "thread"), disable_sanitizer_instrumentation)), apply_to = function) + +static inline uint64_t sgm_bits(double d) { + uint64_t bits; + memcpy(&bits, &d, sizeof bits); + return bits; +} + +static inline double sgm_double(uint64_t bits) { + double d; + memcpy(&d, &bits, sizeof d); + return d; +} + +#define SGM_SIGN (UINT64_C(1) << 63) + +/* Hidden: the executable's own references bind to these definitions, which + * are not exported (libc.so and libm.so define the same names), so section + * GC drops the ones a program does not call. */ +#define SGM_DEFINE __attribute__((visibility("hidden"))) + +/* Values of magnitude below 2^52 round through an integer conversion: the + * truncated value, stepped by one when the rounding direction needs it, with + * the value's sign so that a zero result keeps it (ceil(-0.5) is -0). */ +static inline int sgm_small(double x) { return __builtin_fabs(x) < 0x1p52; } + +/* The unbiased exponent; 1024 for the infinities and NaN. Values with an + * exponent of 52 or more are integers already. */ +static inline int sgm_exponent(uint64_t bits) { return (int)((bits >> 52) & 0x7FF) - 1023; } + +static double sgm_floor(double x) { + if (sgm_small(x)) { + double t = (double)(int64_t)x; + return __builtin_copysign(t > x ? t - 1.0 : t, x); + } + uint64_t bits = sgm_bits(x); + int e = sgm_exponent(bits); + if (e >= 52) return e == 1024 ? x + x : x; + if (e < 0) { + if ((bits << 1) == 0) return x; + return bits & SGM_SIGN ? -1.0 : 0.0; + } + uint64_t fraction = (UINT64_C(1) << (52 - e)) - 1; + if ((bits & fraction) == 0) return x; + if (bits & SGM_SIGN) bits += fraction; + return sgm_double(bits & ~fraction); +} + +static double sgm_ceil(double x) { + if (sgm_small(x)) { + double t = (double)(int64_t)x; + return __builtin_copysign(t < x ? t + 1.0 : t, x); + } + uint64_t bits = sgm_bits(x); + int e = sgm_exponent(bits); + if (e >= 52) return e == 1024 ? x + x : x; + if (e < 0) { + if ((bits << 1) == 0) return x; + return bits & SGM_SIGN ? -0.0 : 1.0; + } + uint64_t fraction = (UINT64_C(1) << (52 - e)) - 1; + if ((bits & fraction) == 0) return x; + if (!(bits & SGM_SIGN)) bits += fraction; + return sgm_double(bits & ~fraction); +} + +static double sgm_trunc(double x) { + if (sgm_small(x)) return __builtin_copysign((double)(int64_t)x, x); + uint64_t bits = sgm_bits(x); + int e = sgm_exponent(bits); + if (e >= 52) return e == 1024 ? x + x : x; + if (e < 0) return sgm_double(bits & SGM_SIGN); + uint64_t fraction = (UINT64_C(1) << (52 - e)) - 1; + return sgm_double(bits & ~fraction); +} + +#if defined(__x86_64__) +#include +#include + +/* glibc selects SSE4.1's rounding instruction for floor, ceil and trunc when + * the CPU has it, and so do these: the same results, with no call into the + * integer path. */ +__attribute__((target("sse4.1"))) static double sgm_floor_sse41(double x) { + return _mm_cvtsd_f64(_mm_floor_sd(_mm_set_sd(x), _mm_set_sd(x))); +} +__attribute__((target("sse4.1"))) static double sgm_ceil_sse41(double x) { + return _mm_cvtsd_f64(_mm_ceil_sd(_mm_set_sd(x), _mm_set_sd(x))); +} +__attribute__((target("sse4.1"))) static double sgm_trunc_sse41(double x) { + return _mm_cvtsd_f64(_mm_round_sd(_mm_set_sd(x), _mm_set_sd(x), _MM_FROUND_TO_ZERO | _MM_FROUND_NO_EXC)); +} + +static int sgm_has_sse41(void) { + unsigned eax, ebx, ecx, edx; + return __get_cpuid(1, &eax, &ebx, &ecx, &edx) && (ecx & bit_SSE4_1) != 0; +} + +typedef double (*SgmUnary)(double); +static SgmUnary sgm_resolve_floor(void) { return sgm_has_sse41() ? sgm_floor_sse41 : sgm_floor; } +static SgmUnary sgm_resolve_ceil(void) { return sgm_has_sse41() ? sgm_ceil_sse41 : sgm_ceil; } +static SgmUnary sgm_resolve_trunc(void) { return sgm_has_sse41() ? sgm_trunc_sse41 : sgm_trunc; } +SGM_DEFINE double floor(double x) __attribute__((ifunc("sgm_resolve_floor"))); +SGM_DEFINE double ceil(double x) __attribute__((ifunc("sgm_resolve_ceil"))); +SGM_DEFINE double trunc(double x) __attribute__((ifunc("sgm_resolve_trunc"))); +#else +SGM_DEFINE double floor(double x) { return sgm_floor(x); } +SGM_DEFINE double ceil(double x) { return sgm_ceil(x); } +SGM_DEFINE double trunc(double x) { return sgm_trunc(x); } +#endif + +/* Math.sqrt is LLVM's sqrt libcall: the square root instruction inline, and + * a call to sqrt for a negative argument, where libm would set errno. This + * definition answers that call with the same instruction. */ +SGM_DEFINE double sqrt(double x) { +#if defined(__x86_64__) + return _mm_cvtsd_f64(_mm_sqrt_sd(_mm_set_sd(x), _mm_set_sd(x))); +#elif defined(__aarch64__) + double r; + __asm__("fsqrt %d0, %d1" : "=w"(r) : "w"(x)); + return r; +#else + return __builtin_sqrt(x); +#endif +} + +/* Halfway cases round away from zero. */ +SGM_DEFINE double round(double x) { + uint64_t bits = sgm_bits(x); + int e = sgm_exponent(bits); + if (e >= 52) return e == 1024 ? x + x : x; + if (e < 0) { + uint64_t sign = bits & SGM_SIGN; + return e == -1 ? sgm_double(sign | sgm_bits(1.0)) : sgm_double(sign); + } + uint64_t fraction = (UINT64_C(1) << (52 - e)) - 1; + if ((bits & fraction) == 0) return x; + bits += UINT64_C(1) << (51 - e); + return sgm_double(bits & ~fraction); +} + +/* The significand of a finite nonzero double as an integer below 2^53, and + * its exponent so that the value's magnitude is significand * 2^exponent. */ +static inline uint64_t sgm_significand(uint64_t bits, int *exponent) { + int biased = (int)((bits >> 52) & 0x7FF); + uint64_t significand = bits & ((UINT64_C(1) << 52) - 1); + if (biased == 0) { + *exponent = -1074; + return significand; + } + *exponent = biased - 1075; + return significand | (UINT64_C(1) << 52); +} + +/* x - n*y for the integer n = trunc(x/y), which is exact: the remainder + * comes from shift-and-subtract over the significands. NaN, an infinite x + * and a zero y answer (x*y)/(x*y), as glibc's fmod does. */ +SGM_DEFINE double fmod(double x, double y) { + uint64_t xb = sgm_bits(x), yb = sgm_bits(y); + uint64_t xmag = xb & ~SGM_SIGN, ymag = yb & ~SGM_SIGN; + uint64_t infinity = UINT64_C(0x7FF0000000000000); + if (ymag == 0 || xmag >= infinity || ymag > infinity) return (x * y) / (x * y); + if (xmag < ymag) return x; + if (xmag == ymag) return 0.0 * x; + int ex, ey; + uint64_t mx = sgm_significand(xb, &ex); + uint64_t my = sgm_significand(yb, &ey); + /* Normalize both to 53 significant bits, so the loop below shifts one + * exponent step at a time. */ + while (mx < (UINT64_C(1) << 52)) { + mx <<= 1; + ex--; + } + while (my < (UINT64_C(1) << 52)) { + my <<= 1; + ey--; + } + for (; ex > ey; ex--) { + if (mx >= my) mx -= my; + if (mx == 0) return 0.0 * x; + mx <<= 1; + } + if (mx >= my) mx -= my; + if (mx == 0) return 0.0 * x; + /* The remainder is mx * 2^ey, below y: rebuild it as a double. */ + while (mx < (UINT64_C(1) << 52)) { + mx <<= 1; + ey--; + } + uint64_t result; + if (ey + 1075 >= 1) { + result = ((uint64_t)(ey + 1075) << 52) | (mx & ((UINT64_C(1) << 52) - 1)); + } else { + result = mx >> (1 - (ey + 1075)); + } + return sgm_double(result | (xb & SGM_SIGN)); +} + +/* x * 2^n, rounded once. Exponents beyond one double's range scale in + * steps; the step toward the subnormals stays 53 bits above the smallest + * normal so that only the last multiplication rounds. */ +SGM_DEFINE double scalbn(double x, int n) { + if (n > 1023) { + x *= 0x1p1023; + n -= 1023; + if (n > 1023) { + x *= 0x1p1023; + n -= 1023; + if (n > 1023) n = 1023; + } + } else if (n < -1022) { + x *= 0x1p-1022 * 0x1p53; + n += 1022 - 53; + if (n < -1022) { + x *= 0x1p-1022 * 0x1p53; + n += 1022 - 53; + if (n < -1022) n = -1022; + } + } + return x * sgm_double((uint64_t)(0x3FF + n) << 52); +} + +SGM_DEFINE double ldexp(double x, int n) { return scalbn(x, n); } + +/* The significand in [0.5, 1) and the power of two: x = significand * 2^e. + * Zero, the infinities and NaN answer themselves with e = 0. */ +SGM_DEFINE double frexp(double x, int *e) { + uint64_t bits = sgm_bits(x); + int biased = (int)((bits >> 52) & 0x7FF); + if (biased == 0) { + if ((bits << 1) == 0) { + *e = 0; + return x; + } + x = frexp(x * 0x1p64, e); + *e -= 64; + return x; + } + if (biased == 0x7FF) { + *e = 0; + return x; + } + *e = biased - 1022; + return sgm_double((bits & ~(UINT64_C(0x7FF) << 52)) | (UINT64_C(0x3FE) << 52)); +} + +#pragma clang attribute pop + +#endif diff --git a/packages/runtime/src/scr_grapheme.c b/packages/runtime/src/scr_grapheme.c index 2c93bc395..f9e5ca28d 100644 --- a/packages/runtime/src/scr_grapheme.c +++ b/packages/runtime/src/scr_grapheme.c @@ -201,3 +201,58 @@ ScrDyn *scr_intl_segmenter_new(void) { scr_dyn_handle_install(SCR_DYNH_SEGMENTS, &segments); return scr_graphemes_box(NULL, SCR_DYNH_SEGMENTER); } + +/* A word or sentence Segmenter, which JavaScript dependencies may build at + * module load and use on some paths only: it constructs, and its segment() + * throws the compiler's SC2020 message. */ +typedef struct { + size_t rc; + ScrStr *refusal; /* retained */ +} ScrRefusingSegmenter; + +static void *scr_refusing_segmenter_retain(void *ptr) { + ScrRefusingSegmenter *value = ptr; + value->rc++; + return value; +} + +static void scr_refusing_segmenter_release(void *ptr) { + ScrRefusingSegmenter *value = ptr; + if (--value->rc) return; + scr_str_release(value->refusal); + free(value); +} + +static ScrDyn *scr_refusing_segmenter_invoke(void *ptr, ScrDyn *self, const char *method, + ScrDyn *const *args, size_t argc, const char *what) { + ScrRefusingSegmenter *value = ptr; + if (strcmp(method, "segment") == 0) { + ScrStr *input = scr_dyn_string_coerce_js(argc ? args[0] : scr_dyn_undefined()); + if (!input) return NULL; + scr_str_release(input); + scr_throw_error_msg_code(SCR_ERR_ERROR, value->refusal->data, value->refusal->len, "SC2020"); + return NULL; + } + return scr_graphemes_invoke(&(ScrGraphemes){ .rc = 1 }, self, method, args, argc, what); +} + +static ScrDyn *scr_refusing_segmenter_get(void *ptr, const char *key, size_t len) { + (void)ptr; + return scr_graphemes_get(&(ScrGraphemes){ .rc = 1 }, key, len); +} + +ScrDyn *scr_intl_segmenter_refusing(ScrStr *refusal) { + static const ScrDynHandleOps segmenter = { + .cls = "Segmenter", .retain = scr_refusing_segmenter_retain, + .release = scr_refusing_segmenter_release, .invoke = scr_refusing_segmenter_invoke, + .get = scr_refusing_segmenter_get, .set = scr_graphemes_set, + }; + scr_dyn_handle_install(SCR_DYNH_SEGMENTER_REFUSING, &segmenter); + ScrRefusingSegmenter *value = calloc(1, sizeof *value); + if (!value) scr_trap("scriptc: out of memory\n"); + value->rc = 1; + value->refusal = scr_str_retain(refusal); + ScrDyn *boxed = scr_dyn_new_handle(value, SCR_DYNH_SEGMENTER_REFUSING); + scr_refusing_segmenter_release(value); + return boxed; +} diff --git a/packages/runtime/src/scr_http.c b/packages/runtime/src/scr_http.c index 29eb52ef7..3fb0fdcba 100644 --- a/packages/runtime/src/scr_http.c +++ b/packages/runtime/src/scr_http.c @@ -209,6 +209,11 @@ void scr_http_set_h2_upgrade_hook(void (*hook)(void *h2ctx, ScrClosure *cb, void struct ScrHttpReq { size_t rc; + /* The AsyncLocalStorage context of its creation (+1, NULL outside a + * store): the deferred emits run in it (scr_http_proto_sweep). A server + * request is created in its connection's dispatch, which runs in the + * server's listen() context; a client response in its request's. */ + ScrAlsCtx *als; int status; /* -1 on server requests (Node's undefined statusCode) */ ScrStr *method; ScrStr *url; @@ -303,6 +308,7 @@ void scr_http_req_set_encoding(ScrHttpReq *r, ScrStr *enc /*borrowed*/) { void scr_http_req_release(ScrHttpReq *r) { if (!r || r->rc == SIZE_MAX) return; if (--r->rc == 0) { + scr_als_ctx_release(r->als); scr_str_release(r->method); scr_str_release(r->url); for (size_t i = 0; i < r->nheaders; i++) { @@ -950,6 +956,7 @@ static bool scr_http_trailers_replace(ScrHttpTrailers *t, ScrArr *pairs /*borrow struct ScrHttpRes { size_t rc; + ScrAlsCtx *als; /* +1: as ScrHttpReq's */ ScrNetSocket *sock; /* +1; writes after the connection died are no-ops */ void *h2_stream; /* +1 through scr_http_h2_ops — set on h2 compat * responses; every write path routes to frames */ @@ -990,6 +997,13 @@ struct ScrHttpRes { bool req_cleared; ScrNetLs close_ls, timeout_ls; ScrNetLs finish_ls; /* res.on('finish') — fires deferred once the body went out */ + /* The AsyncLocalStorage context of the end() call (+1, NULL outside a + * store) once `end_ctx_set`: 'finish', the end() callbacks and 'close' + * after an end run in it, as Node's do. An http2 compatibility response + * never sets it: its events follow the server stream, which has no + * store. */ + ScrAlsCtx *end_als; + bool end_ctx_set; ScrNetLs endcb_ls; /* res.end(cb) — fires just before the 'finish' listeners */ ScrNetLs wcb_ls; /* res.write(chunk, cb) — fires from the queue */ bool writable_finished; /* set where Node emits 'finish' (writableFinished) */ @@ -1007,6 +1021,8 @@ ScrHttpRes *scr_http_res_retain(ScrHttpRes *r) { void scr_http_res_release(ScrHttpRes *r) { if (!r || r->rc == SIZE_MAX) return; if (--r->rc == 0) { + scr_als_ctx_release(r->als); + scr_als_ctx_release(r->end_als); for (size_t i = 0; i < r->nheaders; i++) { scr_str_release(r->hnames[i]); scr_str_release(r->hvalues[i]); @@ -1937,6 +1953,10 @@ static void scr_http_res_cork_flush(ScrHttpRes *r) { static void scr_http_res_end_raw(ScrHttpRes *r, const char *data, size_t len) { if (r->finished) return; + if (!r->end_ctx_set && r->h2_stream == NULL) { + r->end_ctx_set = true; + r->end_als = scr_als_capture(); + } double expected; if (len > 0 && r->head_sent && scr_http_res_expected_length(r, &expected) && (double)(r->strict_bytes_written + len) != expected) { @@ -2279,9 +2299,28 @@ void scr_http_res_on_write_flush(ScrHttpRes *r, ScrClosure *cb /*moves*/) { static bool scr_http_proto_pending(void) { return scr_http_emits_head < scr_http_emits_len; } +static ScrAlsCtx *scr_http_client_ctx(struct ScrHttpClientReq *c); + +/* The context an emit's handle was created in (borrowed). */ +static ScrAlsCtx *scr_http_emit_ctx(const ScrHttpEmit *e) { + switch (e->kind) { + case SCR_HTTP_EMIT_RES_CLOSE: + case SCR_HTTP_EMIT_RES_FINISH: { + const ScrHttpRes *res = (const ScrHttpRes *)e->h; + return res->end_ctx_set ? res->end_als : res->als; + } + case SCR_HTTP_EMIT_RES_WCB: return ((ScrHttpRes *)e->h)->als; + case SCR_HTTP_EMIT_REQ_CLOSE: + case SCR_HTTP_EMIT_REQ_DRAIN: + case SCR_HTTP_EMIT_REQ_ABORTED: return ((ScrHttpReq *)e->h)->als; + default: return scr_http_client_ctx((struct ScrHttpClientReq *)e->h); + } +} + static void scr_http_proto_sweep(void) { while (scr_http_emits_head < scr_http_emits_len) { ScrHttpEmit e = scr_http_emits[scr_http_emits_head++]; + ScrAlsCtx *prev = scr_als_dispatch_enter(scr_http_emit_ctx(&e)); switch (e.kind) { case SCR_HTTP_EMIT_RES_CLOSE: { ScrHttpRes *res = (ScrHttpRes *)e.h; @@ -2372,6 +2411,7 @@ static void scr_http_proto_sweep(void) { break; } } + scr_als_dispatch_leave(prev); scr_http_emit_release(&e); if (scr_exc_pending()) return; } @@ -2894,6 +2934,7 @@ static bool scr_http_conn_parse_head(ScrHttpConn *conn, size_t head_len, bool *o ScrHttpReq *req = calloc(1, sizeof *req); if (!req) scr_http_oom(); req->rc = 1; + req->als = scr_als_capture(); req->status = -1; /* Node: statusCode is undefined on server requests */ req->gated = true; req->join_dup = conn->srv != NULL && conn->srv->join_dup; @@ -3062,6 +3103,7 @@ static bool scr_http_conn_parse_head(ScrHttpConn *conn, size_t head_len, bool *o ScrHttpRes *res = calloc(1, sizeof *res); if (!res) scr_http_oom(); res->rc = 1; + res->als = scr_als_capture(); #ifdef SCR_RC_AUDIT scr_http_live++; #endif @@ -3614,6 +3656,7 @@ ScrHttpReq *scr_http_h2_req_new(ScrNetSocket *sock /*borrowed, nullable*/, ScrHttpReq *req = calloc(1, sizeof *req); if (!req) scr_http_oom(); req->rc = 1; + req->als = scr_als_capture(); req->status = -1; /* server requests: statusCode is Node's undefined */ req->http2 = true; req->sock = sock ? scr_net_sock_retain(sock) : NULL; @@ -3685,6 +3728,7 @@ ScrHttpRes *scr_http_h2_res_new(ScrNetSocket *sock /*borrowed, nullable*/, ScrHttpRes *res = calloc(1, sizeof *res); if (!res) scr_http_oom(); res->rc = 1; + res->als = scr_als_capture(); res->sock = sock ? scr_net_sock_retain(sock) : NULL; res->h2_stream = scr_http_h2_ops->retain(stream); res->keep_alive = true; /* inert on h2 — no Connection header exists */ @@ -3794,6 +3838,9 @@ void scr_http_handler_thunk0(ScrClosure *cb, ScrHttpReq *req, ScrHttpRes *res) { struct ScrHttpClientReq { size_t rc; + /* The http.request() call's AsyncLocalStorage context (+1): the + * request's deferred emits ('socket', 'finish', 'close') run in it. */ + ScrAlsCtx *als; ScrNetSocket *sock; /* +1 */ ScrHttpConn *conn; /* borrowed backref (the socket's ctx); NULL after free */ ScrStr *host, *path, *method; @@ -3858,6 +3905,7 @@ ScrHttpClientReq *scr_http_client_retain(ScrHttpClientReq *c) { void scr_http_client_release(ScrHttpClientReq *c) { if (!c || c->rc == SIZE_MAX) return; if (--c->rc == 0) { + scr_als_ctx_release(c->als); scr_net_ls_drop(&c->resp_ls); scr_net_ls_drop(&c->err_ls); scr_net_ls_drop(&c->timeout_ls); @@ -3896,6 +3944,8 @@ static void scr_http_client_release_internal(struct ScrHttpClientReq *c) { scr_http_client_release(c); } +static ScrAlsCtx *scr_http_client_ctx(struct ScrHttpClientReq *c) { return c->als; } + void *scr_http_client_retain_v(void *p) { return scr_http_client_retain((ScrHttpClientReq *)p); } void scr_http_client_release_v(void *p) { scr_http_client_release((ScrHttpClientReq *)p); } @@ -4690,6 +4740,7 @@ static bool scr_http_client_parse_head(ScrHttpConn *conn, size_t head_len) { ScrHttpReq *res = calloc(1, sizeof *res); if (!res) scr_http_oom(); res->rc = 1; + res->als = scr_als_capture(); res->status = status; res->sock = scr_net_sock_retain(conn->sock); #ifdef SCR_RC_AUDIT @@ -5083,6 +5134,7 @@ static ScrHttpClientReq *scr_http_request_impl(ScrStr *host /*borrowed*/, double ScrHttpClientReq *c = calloc(1, sizeof *c); if (!c) scr_http_oom(); c->rc = 1; + c->als = scr_als_capture(); #ifdef SCR_RC_AUDIT scr_http_live++; #endif @@ -5128,6 +5180,7 @@ static ScrHttpClientReq *scr_http_request_impl(ScrStr *host /*borrowed*/, double if (presock != NULL) { c->sock = presock; + scr_net_sock_set_context(c->sock, c->als); } else { ScrStr *dial = scr_net_blocking_lookup(host); c->sock = scr_net_connect(port, dial, NULL); @@ -5386,6 +5439,8 @@ static void scr_http_client_rebind(ScrHttpClientReq *c, ScrHttpAgentFree *f, boo scr_net_sock_release(old); ScrHttpConn *conn = f->conn; c->sock = f->sock; /* the free entry's +1 moves */ + /* Node runs a pooled socket's events for this request in its context. */ + scr_net_sock_set_context(c->sock, c->als); c->conn = conn; conn->client = scr_http_client_retain(c); conn->pool_agent = NULL; diff --git a/packages/runtime/src/scr_http2.c b/packages/runtime/src/scr_http2.c index 4c5267e6a..489946a12 100644 --- a/packages/runtime/src/scr_http2.c +++ b/packages/runtime/src/scr_http2.c @@ -678,6 +678,10 @@ struct ScrH2Stream { int64_t send_window; ScrNetLs data_ls, end_ls, close_ls, err_ls, resp_ls, aborted_ls, ready_ls, trailers_ls, wanttrailers_ls, push_ls, frameerr_ls; + /* The AsyncLocalStorage context of the request() call that created a + * client stream (+1, NULL outside a store); NULL on server streams, + * whose own events Node runs with no store. */ + ScrAlsCtx *als; }; /* The server-side ctx (the ScrHttpSrvCtx shape; `proto` FIRST — the @@ -757,6 +761,14 @@ struct ScrH2Session { bool error_emitted; ScrNetLs stream_ls, close_ls, err_ls, connect_ls, goaway_ls, remote_settings_ls, local_settings_ls; + /* The stream the last lookup by id found (borrowed from `streams`): + * the frames of one stream arrive together, and a frame's context + * lookup and its handling ask for the same stream. */ + ScrH2Stream *last_found; + /* The AsyncLocalStorage context the session was created in (+1, NULL + * outside a store): the connect() call's for a client session, the + * server's listen() context for a server session. */ + ScrAlsCtx *als; }; /* ── refcounts + the RC audit ────────────────────────────────────────── */ @@ -838,6 +850,7 @@ static void scr_h2_stream_free(ScrH2Stream *st) { } free(st->wbuf.data); scr_http2_session_release(st->sess); + scr_als_ctx_release(st->als); free(st); #ifdef SCR_RC_AUDIT scr_h2_streams_live--; @@ -864,6 +877,7 @@ static void scr_h2_session_free(ScrH2Session *s) { scr_h2_dyntab_free(&s->dec_table); if (s->srv) scr_h2_srv_ctx_release(s->srv); if (s->sock) scr_net_sock_release(s->sock); + scr_als_ctx_release(s->als); free(s); #ifdef SCR_RC_AUDIT scr_h2_sessions_live--; @@ -1011,8 +1025,9 @@ static void scr_h2_send_headers(ScrH2Session *s, int32_t stream_id, const ScrH2B /* ── stream bookkeeping ──────────────────────────────────────────────── */ static ScrH2Stream *scr_h2_find_stream(ScrH2Session *s, int32_t id) { + if (s->last_found != NULL && s->last_found->id == id) return s->last_found; for (size_t i = 0; i < s->nstreams; i++) { - if (s->streams[i]->id == id) return s->streams[i]; + if (s->streams[i]->id == id) return s->last_found = s->streams[i]; } return NULL; } @@ -1023,6 +1038,7 @@ static ScrH2Stream *scr_h2_stream_new(ScrH2Session *sess, int32_t id) { st->rc = 1; st->id = id; st->sess = scr_http2_session_retain(sess); + if (sess->client) st->als = scr_als_capture(); st->send_window = sess->peer_init_window ? (int64_t)sess->peer_init_window : SCR_H2_DEFAULT_WINDOW; #ifdef SCR_RC_AUDIT scr_h2_streams_live++; @@ -1039,6 +1055,7 @@ static ScrH2Stream *scr_h2_stream_new(ScrH2Session *sess, int32_t id) { /* Drop the session's ref on a settled stream (its 'close' is queued or * emitted — the vector entry is what kept it discoverable by id). */ static void scr_h2_session_forget_stream(ScrH2Session *s, ScrH2Stream *st) { + if (s->last_found == st) s->last_found = NULL; for (size_t i = 0; i < s->nstreams; i++) { if (s->streams[i] == st) { memmove(s->streams + i, s->streams + i + 1, (s->nstreams - i - 1) * sizeof *s->streams); @@ -1150,6 +1167,10 @@ static bool scr_h2_proto_pending(void) { return scr_h2_emits_head < scr_h2_emits static void scr_h2_proto_sweep(void) { while (scr_h2_emits_head < scr_h2_emits_len) { ScrH2Emit e = scr_h2_emits[scr_h2_emits_head++]; + ScrAlsCtx *prev = scr_als_dispatch_enter( + e.kind == SCR_H2_EMIT_STREAM_CLOSE || e.kind == SCR_H2_EMIT_STREAM_DRAIN + ? ((ScrH2Stream *)e.h)->als + : ((ScrH2Session *)e.h)->als); switch (e.kind) { case SCR_H2_EMIT_STREAM_CLOSE: { ScrH2Stream *st = (ScrH2Stream *)e.h; @@ -1227,6 +1248,7 @@ static void scr_h2_proto_sweep(void) { break; } } + scr_als_dispatch_leave(prev); scr_h2_emit_release(&e); if (scr_exc_pending()) return; } @@ -1380,8 +1402,29 @@ static void scr_h2_fire_settings(ScrNetLs *ls, ScrH2Session *s, const ScrH2Setti } /* One complete frame. Returns false when the session tore down. */ +static bool scr_h2_on_frame_in(ScrH2Session *s, uint8_t type, uint8_t flags, int32_t stream_id, + const uint8_t *p, size_t n); + +/* A frame that drives an open stream's events (DATA, WINDOW_UPDATE, + * RST_STREAM, and a client's HEADERS, CONTINUATION and PUSH_PROMISE) runs + * in that stream's context; other frames, a server stream's opening + * HEADERS included, in the session's, which the socket callbacks below + * enter. */ static bool scr_h2_on_frame(ScrH2Session *s, uint8_t type, uint8_t flags, int32_t stream_id, const uint8_t *p, size_t n) { + bool stream_frame = type == SCR_H2_F_DATA || type == SCR_H2_F_WINDOW_UPDATE || + type == SCR_H2_F_RST_STREAM || + (s->client && (type == SCR_H2_F_HEADERS || type == SCR_H2_F_CONTINUATION || + type == SCR_H2_F_PUSH_PROMISE)); + ScrH2Stream *st = stream_frame && stream_id != 0 ? scr_h2_find_stream(s, stream_id) : NULL; + ScrAlsCtx *prev = st != NULL ? scr_als_dispatch_enter(st->als) : NULL; + bool ok = scr_h2_on_frame_in(s, type, flags, stream_id, p, n); + if (st != NULL) scr_als_dispatch_leave(prev); + return ok; +} + +static bool scr_h2_on_frame_in(ScrH2Session *s, uint8_t type, uint8_t flags, int32_t stream_id, + const uint8_t *p, size_t n) { /* An open header block admits only its CONTINUATIONs. */ if (s->hblock_active && type != SCR_H2_F_CONTINUATION) { goto proto_err; @@ -1629,7 +1672,16 @@ proto_err: return false; } +static void scr_h2_on_data_in(void *ctx, const char *buf, size_t n); + +/* The socket's callbacks run in the session's context. */ static void scr_h2_on_data(void *ctx, const char *buf, size_t n) { + ScrAlsCtx *prev = scr_als_dispatch_enter(((ScrH2Session *)ctx)->als); + scr_h2_on_data_in(ctx, buf, n); + scr_als_dispatch_leave(prev); +} + +static void scr_h2_on_data_in(void *ctx, const char *buf, size_t n) { ScrH2Session *s = ctx; if (s->destroyed) return; /* append to the reassembly buffer */ @@ -1680,7 +1732,15 @@ done: scr_http2_session_release(s); } +static void scr_h2_on_eof_in(void *ctx); + static void scr_h2_on_eof(void *ctx) { + ScrAlsCtx *prev = scr_als_dispatch_enter(((ScrH2Session *)ctx)->als); + scr_h2_on_eof_in(ctx); + scr_als_dispatch_leave(prev); +} + +static void scr_h2_on_eof_in(void *ctx) { ScrH2Session *s = ctx; if (s->destroyed) return; /* The peer is done sending. After a graceful GOAWAY exchange this is @@ -1688,7 +1748,15 @@ static void scr_h2_on_eof(void *ctx) { scr_h2_session_abort_streams(s); } +static void scr_h2_on_closed_in(void *ctx); + static void scr_h2_on_closed(void *ctx) { + ScrAlsCtx *prev = scr_als_dispatch_enter(((ScrH2Session *)ctx)->als); + scr_h2_on_closed_in(ctx); + scr_als_dispatch_leave(prev); +} + +static void scr_h2_on_closed_in(void *ctx) { ScrH2Session *s = ctx; s->destroyed = true; scr_h2_session_abort_streams(s); @@ -1700,7 +1768,16 @@ static void scr_h2_on_closed(void *ctx) { } } +static bool scr_h2_on_err_in(void *ctx, ScrStr *msg); + static bool scr_h2_on_err(void *ctx, ScrStr *msg) { + ScrAlsCtx *prev = scr_als_dispatch_enter(((ScrH2Session *)ctx)->als); + bool consumed = scr_h2_on_err_in(ctx, msg); + scr_als_dispatch_leave(prev); + return consumed; +} + +static bool scr_h2_on_err_in(void *ctx, ScrStr *msg) { ScrH2Session *s = ctx; if (s->err_ls.n == 0 && (s->srv == NULL || s->srv->session_error_ls.n == 0)) { return false; /* the socket's error story applies */ @@ -1716,6 +1793,7 @@ static ScrH2Session *scr_h2_session_new(bool client) { ScrH2Session *s = calloc(1, sizeof *s); if (!s) scr_h2_oom(); s->rc = 1; + s->als = scr_als_capture(); s->client = client; s->preface_done = client; /* clients never RECEIVE the magic */ s->next_stream_id = 1; @@ -2287,6 +2365,8 @@ static void scr_h2_upgrade_hook(void *ctx, ScrClosure *cb /*moves*/, void *fn, b * 'request' listeners (this = the net server, the http/1 shape). */ static void scr_h2_compat_dispatch(ScrH2Session *s, ScrH2Stream *st, const ScrH2Headers *h, bool end_stream, ScrNetLs *listeners) { + /* The compat pair's own events follow the stream's context. */ + ScrAlsCtx *prev = scr_als_dispatch_enter(st->als); ScrHttpReq *req = scr_http_h2_req_new(s->sock, st); for (size_t i = 0; i < h->n; i++) { ScrStr *name = h->names[i]; @@ -2301,6 +2381,7 @@ static void scr_h2_compat_dispatch(ScrH2Session *s, ScrH2Stream *st, const ScrH2 scr_http_h2_req_header(req, name, value); } ScrHttpRes *res = scr_http_h2_res_new(s->sock, st); + scr_als_dispatch_leave(prev); scr_http_res_set_req(res, req); /* res.req — the compat pair's backref */ st->compat_req = scr_http_req_retain(req); st->compat_res = scr_http_res_retain(res); @@ -2342,7 +2423,7 @@ static void scr_h2_srv_ctx_settle(void *p) { static void scr_h2_on_connection(void *ctx, ScrNetSocket *sock) { ScrH2SrvCtx *srv = (ScrH2SrvCtx *)ctx; - ScrH2Session *sess = scr_h2_session_new(false); + ScrH2Session *sess = scr_h2_session_new(false); /* in the listen() context */ sess->srv = scr_h2_srv_ctx_retain(srv); sess->sock = scr_net_sock_retain(sock); sess->next_stream_id = 2; /* server-initiated ids are even (push, unused) */ @@ -2354,6 +2435,14 @@ static void scr_h2_on_connection(void *ctx, ScrNetSocket *sock) { scr_h2_emit_push(SCR_H2_EMIT_SERVER_SESSION, scr_http2_session_retain(sess)); } +/* An accepted h2c connection: the socket's own events run with no store, + * as an accepted net socket's do (a TLS server's sockets keep the listen() + * context); the session keeps the listen() context the accept runs in. */ +static void scr_h2_on_plain_connection(void *ctx, ScrNetSocket *sock) { + scr_net_sock_set_context(sock, NULL); + scr_h2_on_connection(ctx, sock); +} + ScrNetServer *scr_http2_create_server(void) { scr_http2_install(); ScrNetServer *s = scr_net_create_server(NULL, NULL); @@ -2361,7 +2450,7 @@ ScrNetServer *scr_http2_create_server(void) { if (!ctx) scr_h2_oom(); ctx->proto = SCR_NET_PROTO_H2; ctx->rc = 1; - scr_net_server_set_native_conn(s, &scr_h2_on_connection, ctx, &scr_h2_srv_ctx_free_v); + scr_net_server_set_native_conn(s, &scr_h2_on_plain_connection, ctx, &scr_h2_srv_ctx_free_v); scr_net_server_set_http_ctx(s, ctx); scr_net_server_set_proto_settle(s, &scr_h2_srv_ctx_settle); return s; @@ -2528,11 +2617,11 @@ static void scr_h2_on_established(void *ctx) { * TLS client transport with ALPN ["h2"] (scr_tls.c — linked whenever * this unit is): the same preface/SETTINGS buffer through the handshake * and everything after establishment is protocol-identical. `ca` - * replaces the client trust anchors (len 0 = none) and + * replaces the client trust anchors (NULL = none) and * reject_unauthorized false disables the verify gate — the local-CA * self-connect shapes. Throws catchably on unparsable input. */ ScrH2Session *scr_http2_connect(ScrStr *url /*borrowed*/, bool reject_unauthorized, - const char *ca /*borrowed, len 0 = none*/, size_t ca_len, + const char *ca /*borrowed, NULL = none*/, size_t ca_len, ScrClosure *cb /*moves, nullable*/, ScrH2ConnectFn fn) { const char *p = url->data; const char *sep = strstr(p, "://"); diff --git a/packages/runtime/src/scr_http2.h b/packages/runtime/src/scr_http2.h index 25e236e6f..c4ce850d0 100644 --- a/packages/runtime/src/scr_http2.h +++ b/packages/runtime/src/scr_http2.h @@ -69,7 +69,7 @@ void scr_http2_server_on_session(ScrNetServer *s, ScrClosure *cb /*moves*/, ScrH * reject_unauthorized false disables the verify gate — the local-CA * self-connect shapes); throws catchably on bad input */ ScrH2Session *scr_http2_connect(ScrStr *url /*borrowed*/, bool reject_unauthorized, - const char *ca /*borrowed, len 0 = none*/, size_t ca_len, + const char *ca /*borrowed, NULL = none*/, size_t ca_len, ScrClosure *cb /*moves, nullable*/, ScrH2ConnectFn fn); /* +1 */ /* end_stream: -1 = the method's payload-meaningless default, 0/1 explicit */ @@ -173,7 +173,7 @@ ScrHttpClientReq *scr_https_request(ScrStr *host /*borrowed*/, double port, ScrStr *path /*borrowed*/, ScrStr *method /*borrowed*/, double timeout_ms, ScrArr *header_pairs /*borrowed*/, bool auto_end, bool reject_unauthorized, - const char *ca /*borrowed, len 0 = none*/, size_t ca_len, + const char *ca /*borrowed, NULL = none*/, size_t ca_len, ScrClosure *cb /*moves, nullable*/, ScrHttpRespFn fn); /* +1 */ /* The URL-string form (https.get('https://host/path')): scr_http.c's * shared parse, then the options form's dial with Node's no-options @@ -187,7 +187,7 @@ ScrHttpClientReq *scr_https_request_agent(ScrStr *host /*borrowed*/, double port ScrStr *path /*borrowed*/, ScrStr *method /*borrowed*/, double timeout_ms, ScrArr *header_pairs /*borrowed*/, bool auto_end, bool reject_unauthorized, - const char *ca /*borrowed, len 0 = none*/, size_t ca_len, + const char *ca /*borrowed, NULL = none*/, size_t ca_len, const struct ScrDyn *agent /*borrowed*/, ScrClosure *cb /*moves, nullable*/, ScrHttpRespFn fn); /* +1 */ /* The fetch unit's https leg (defined in scr_tls.c): a client transport diff --git a/packages/runtime/src/scr_idna.c b/packages/runtime/src/scr_idna.c index 34c9e3882..7507e5caa 100644 --- a/packages/runtime/src/scr_idna.c +++ b/packages/runtime/src/scr_idna.c @@ -1,8 +1,8 @@ /* UTS #46 domain processing for node:url — the conversions Node applies to - * url.parse() hostnames (ToASCII), url.domainToASCII()/domainToUnicode() - * and url.format()'s unicode option: nontransitional mapping, NFC, label - * validity (leading combining marks, ContextJ, and the RFC 5893 bidi rule), - * and Punycode. It matches Node 24's implementation, including its + * WHATWG URL hosts and url.parse() hostnames (ToASCII), + * url.domainToASCII()/domainToUnicode() and url.format()'s unicode option: + * nontransitional mapping, NFC, label validity (leading combining marks, + * ContextJ, and the RFC 5893 bidi rule), and Punycode. It matches Node 24's implementation, including its * validity-data snapshot and the shortcuts its label checks take; the * differential corpus and test/url-legacy-cases.txt (generated from Node) * are the oracle. @@ -511,3 +511,6 @@ ScrStr *scr_idna_to_unicode(const char *p, size_t n) { } return scr_jb_finish(&out); } + +/* The URL host parser's UTS #46 ToASCII (scr_url.c). */ +void scr_url_idna_install(void) { scr_url_host_to_ascii = &scr_idna_to_ascii; } diff --git a/packages/runtime/src/scr_ieee754.c b/packages/runtime/src/scr_ieee754.c new file mode 100644 index 000000000..798bcbcbc --- /dev/null +++ b/packages/runtime/src/scr_ieee754.c @@ -0,0 +1,2730 @@ +// The following is adapted from fdlibm (http://www.netlib.org/fdlibm). +// +// ==================================================== +// Copyright (C) 1993 by Sun Microsystems, Inc. All rights reserved. +// +// Developed at SunSoft, a Sun Microsystems, Inc. business. +// Permission to use, copy, modify, and distribute this +// software is freely granted, provided that this notice +// is preserved. +// ==================================================== +// +// The original source code covered by the above license above has been +// modified significantly by Google Inc. +// Copyright 2016 the V8 project authors. All rights reserved. + +/* Math's transcendental functions as V8 computes them: a C port of V8's + * src/base/ieee754.cc (Node v24.22.0's deps/v8), V8's adaptation of + * fdlibm. Node answers Math.sin, Math.exp, Math.cbrt and the other + * functions here through that file on every platform, so the compiled + * Math members call scr_ieee754_* and print the values Node prints. The + * functions are transcribed unchanged; the port renames the entry points, + * gives the kernels internal linkage, and replaces V8's helpers with C + * (bit casts, wraparound arithmetic, NaN and infinity constants). V8's + * BSD license is in ../vendor/v8-ieee754/LICENSE. + * + * Math.pow is not here: V8's math::pow calls the platform pow + * (scr_math_pow, scr_lib.c). */ + +#include +#include +#include + +#include "scr_ieee754.h" + +/* Floating-point contraction follows Node's build of this file. Node for + * macOS on arm64 is compiled by clang, which fuses a multiply and an add + * within one expression into a fused multiply-add; the results differ from + * the unfused ones in the last bit for some inputs. Other builds round each + * operation on its own, as Node's x86-64 builds do. */ +#if defined(__APPLE__) && defined(__aarch64__) +#pragma STDC FP_CONTRACT ON +#else +#pragma STDC FP_CONTRACT OFF +#endif + +static inline uint64_t scr_fd_bits(double d) { + uint64_t bits; + memcpy(&bits, &d, sizeof bits); + return bits; +} + +static inline double scr_fd_double(uint64_t bits) { + double d; + memcpy(&d, &bits, sizeof d); + return d; +} + +/* floor for __kernel_rem_pio2's nonnegative integer-part values, which are + * below 2^53: exact, with no libm call. */ +static inline double scr_fd_floor(double x) { + double t = (double)(int64_t)x; + return t > x ? t - 1.0 : t; +} + +/* + * The original fdlibm code used statements like: + * n0 = ((*(int*)&one)>>29)^1; * index of high word * + * ix0 = *(n0+(int*)&x); * high word of x * + * ix1 = *((1-n0)+(int*)&x); * low word of x * + * to dig two 32 bit words out of the 64 bit IEEE floating point + * value. That is non-ANSI, and, moreover, the gcc instruction + * scheduler gets it wrong. We instead use the following macros. + * Unlike the original code, we determine the endianness at compile + * time, not at run time; I don't see much benefit to selecting + * endianness at run time. + */ + +/* Get two 32 bit ints from a double. */ + +#define EXTRACT_WORDS(ix0, ix1, d) \ + do { \ + uint64_t bits = scr_fd_bits(d); \ + (ix0) = bits >> 32; \ + (ix1) = bits & 0xFFFFFFFFu; \ + } while (0) + +/* Get the more significant 32 bit int from a double. */ + +#define GET_HIGH_WORD(i, d) \ + do { \ + uint64_t bits = scr_fd_bits(d); \ + (i) = bits >> 32; \ + } while (0) + +/* Get the less significant 32 bit int from a double. */ + +#define GET_LOW_WORD(i, d) \ + do { \ + uint64_t bits = scr_fd_bits(d); \ + (i) = bits & 0xFFFFFFFFu; \ + } while (0) + +/* Set a double from two 32 bit ints. */ + +#define INSERT_WORDS(d, ix0, ix1) \ + do { \ + uint64_t bits = 0; \ + bits |= (uint64_t)(ix0) << 32; \ + bits |= (uint32_t)(ix1); \ + (d) = scr_fd_double(bits); \ + } while (0) + +/* Set the more significant 32 bits of a double from an int. */ + +#define SET_HIGH_WORD(d, v) \ + do { \ + uint64_t bits = scr_fd_bits(d); \ + bits &= 0x00000000FFFFFFFF; \ + bits |= (uint64_t)(v) << 32; \ + (d) = scr_fd_double(bits); \ + } while (0) + +/* Set the less significant 32 bits of a double from an int. */ + +#define SET_LOW_WORD(d, v) \ + do { \ + uint64_t bits = scr_fd_bits(d); \ + bits &= 0xFFFFFFFF00000000; \ + bits |= (uint32_t)(v); \ + (d) = scr_fd_double(bits); \ + } while (0) + +static int32_t __ieee754_rem_pio2(double x, double* y); +static int __kernel_rem_pio2(double* x, double* y, int e0, int nx, int prec, + const int32_t* ipio2); +static double __kernel_cos(double x, double y); +static double __kernel_sin(double x, double y, int iy); + +/* __ieee754_rem_pio2(x,y) + * + * return the remainder of x rem pi/2 in y[0]+y[1] + * use __kernel_rem_pio2() + */ +static int32_t __ieee754_rem_pio2(double x, double *y) { + /* + * Table of constants for 2/pi, 396 Hex digits (476 decimal) of 2/pi + */ + static const int32_t two_over_pi[] = { + 0xA2F983, 0x6E4E44, 0x1529FC, 0x2757D1, 0xF534DD, 0xC0DB62, 0x95993C, + 0x439041, 0xFE5163, 0xABDEBB, 0xC561B7, 0x246E3A, 0x424DD2, 0xE00649, + 0x2EEA09, 0xD1921C, 0xFE1DEB, 0x1CB129, 0xA73EE8, 0x8235F5, 0x2EBB44, + 0x84E99C, 0x7026B4, 0x5F7E41, 0x3991D6, 0x398353, 0x39F49C, 0x845F8B, + 0xBDF928, 0x3B1FF8, 0x97FFDE, 0x05980F, 0xEF2F11, 0x8B5A0A, 0x6D1F6D, + 0x367ECF, 0x27CB09, 0xB74F46, 0x3F669E, 0x5FEA2D, 0x7527BA, 0xC7EBE5, + 0xF17B3D, 0x0739F7, 0x8A5292, 0xEA6BFB, 0x5FB11F, 0x8D5D08, 0x560330, + 0x46FC7B, 0x6BABF0, 0xCFBC20, 0x9AF436, 0x1DA9E3, 0x91615E, 0xE61B08, + 0x659985, 0x5F14A0, 0x68408D, 0xFFD880, 0x4D7327, 0x310606, 0x1556CA, + 0x73A8C9, 0x60E27B, 0xC08C6B, + }; + + static const int32_t npio2_hw[] = { + 0x3FF921FB, 0x400921FB, 0x4012D97C, 0x401921FB, 0x401F6A7A, 0x4022D97C, + 0x4025FDBB, 0x402921FB, 0x402C463A, 0x402F6A7A, 0x4031475C, 0x4032D97C, + 0x40346B9C, 0x4035FDBB, 0x40378FDB, 0x403921FB, 0x403AB41B, 0x403C463A, + 0x403DD85A, 0x403F6A7A, 0x40407E4C, 0x4041475C, 0x4042106C, 0x4042D97C, + 0x4043A28C, 0x40446B9C, 0x404534AC, 0x4045FDBB, 0x4046C6CB, 0x40478FDB, + 0x404858EB, 0x404921FB, + }; + + /* + * invpio2: 53 bits of 2/pi + * pio2_1: first 33 bit of pi/2 + * pio2_1t: pi/2 - pio2_1 + * pio2_2: second 33 bit of pi/2 + * pio2_2t: pi/2 - (pio2_1+pio2_2) + * pio2_3: third 33 bit of pi/2 + * pio2_3t: pi/2 - (pio2_1+pio2_2+pio2_3) + */ + + static const double + zero = 0.00000000000000000000e+00, /* 0x00000000, 0x00000000 */ + half = 5.00000000000000000000e-01, /* 0x3FE00000, 0x00000000 */ + two24 = 1.67772160000000000000e+07, /* 0x41700000, 0x00000000 */ + invpio2 = 6.36619772367581382433e-01, /* 0x3FE45F30, 0x6DC9C883 */ + pio2_1 = 1.57079632673412561417e+00, /* 0x3FF921FB, 0x54400000 */ + pio2_1t = 6.07710050650619224932e-11, /* 0x3DD0B461, 0x1A626331 */ + pio2_2 = 6.07710050630396597660e-11, /* 0x3DD0B461, 0x1A600000 */ + pio2_2t = 2.02226624879595063154e-21, /* 0x3BA3198A, 0x2E037073 */ + pio2_3 = 2.02226624871116645580e-21, /* 0x3BA3198A, 0x2E000000 */ + pio2_3t = 8.47842766036889956997e-32; /* 0x397B839A, 0x252049C1 */ + + double z, w, t, r, fn; + double tx[3]; + int32_t e0, i, j, nx, n, ix, hx; + uint32_t low; + + z = 0; + GET_HIGH_WORD(hx, x); /* high word of x */ + ix = hx & 0x7FFFFFFF; + if (ix <= 0x3FE921FB) { /* |x| ~<= pi/4 , no need for reduction */ + y[0] = x; + y[1] = 0; + return 0; + } + if (ix < 0x4002D97C) { /* |x| < 3pi/4, special case with n=+-1 */ + if (hx > 0) { + z = x - pio2_1; + if (ix != 0x3FF921FB) { /* 33+53 bit pi is good enough */ + y[0] = z - pio2_1t; + y[1] = (z - y[0]) - pio2_1t; + } else { /* near pi/2, use 33+33+53 bit pi */ + z -= pio2_2; + y[0] = z - pio2_2t; + y[1] = (z - y[0]) - pio2_2t; + } + return 1; + } else { /* negative x */ + z = x + pio2_1; + if (ix != 0x3FF921FB) { /* 33+53 bit pi is good enough */ + y[0] = z + pio2_1t; + y[1] = (z - y[0]) + pio2_1t; + } else { /* near pi/2, use 33+33+53 bit pi */ + z += pio2_2; + y[0] = z + pio2_2t; + y[1] = (z - y[0]) + pio2_2t; + } + return -1; + } + } + if (ix <= 0x413921FB) { /* |x| ~<= 2^19*(pi/2), medium size */ + t = __builtin_fabs(x); + n = (int32_t)(t * invpio2 + half); + fn = (double)(n); + r = t - fn * pio2_1; + w = fn * pio2_1t; /* 1st round good to 85 bit */ + if (n < 32 && ix != npio2_hw[n - 1]) { + y[0] = r - w; /* quick check no cancellation */ + } else { + uint32_t high; + j = ix >> 20; + y[0] = r - w; + GET_HIGH_WORD(high, y[0]); + i = j - ((high >> 20) & 0x7FF); + if (i > 16) { /* 2nd iteration needed, good to 118 */ + t = r; + w = fn * pio2_2; + r = t - w; + w = fn * pio2_2t - ((t - r) - w); + y[0] = r - w; + GET_HIGH_WORD(high, y[0]); + i = j - ((high >> 20) & 0x7FF); + if (i > 49) { /* 3rd iteration need, 151 bits acc */ + t = r; /* will cover all possible cases */ + w = fn * pio2_3; + r = t - w; + w = fn * pio2_3t - ((t - r) - w); + y[0] = r - w; + } + } + } + y[1] = (r - y[0]) - w; + if (hx < 0) { + y[0] = -y[0]; + y[1] = -y[1]; + return -n; + } else { + return n; + } + } + /* + * all other (large) arguments + */ + if (ix >= 0x7FF00000) { /* x is inf or NaN */ + y[0] = y[1] = x - x; + return 0; + } + /* set z = scalbn(|x|,ilogb(x)-23) */ + GET_LOW_WORD(low, x); + SET_LOW_WORD(z, low); + e0 = (ix >> 20) - 1046; /* e0 = ilogb(z)-23; */ + SET_HIGH_WORD(z, ix - (int32_t)((uint32_t)(e0) << 20)); + for (i = 0; i < 2; i++) { + tx[i] = (double)((int32_t)(z)); + z = (z - tx[i]) * two24; + } + tx[2] = z; + nx = 3; + while (tx[nx - 1] == zero) nx--; /* skip zero term */ + n = __kernel_rem_pio2(tx, y, e0, nx, 2, two_over_pi); + if (hx < 0) { + y[0] = -y[0]; + y[1] = -y[1]; + return -n; + } + return n; +} + +/* __kernel_cos( x, y ) + * kernel cos function on [-pi/4, pi/4], pi/4 ~ 0.785398164 + * Input x is assumed to be bounded by ~pi/4 in magnitude. + * Input y is the tail of x. + * + * Algorithm + * 1. Since cos(-x) = cos(x), we need only to consider positive x. + * 2. if x < 2^-27 (hx<0x3E400000 0), return 1 with inexact if x!=0. + * 3. cos(x) is approximated by a polynomial of degree 14 on + * [0,pi/4] + * 4 14 + * cos(x) ~ 1 - x*x/2 + C1*x + ... + C6*x + * where the remez error is + * + * | 2 4 6 8 10 12 14 | -58 + * |cos(x)-(1-.5*x +C1*x +C2*x +C3*x +C4*x +C5*x +C6*x )| <= 2 + * | | + * + * 4 6 8 10 12 14 + * 4. let r = C1*x +C2*x +C3*x +C4*x +C5*x +C6*x , then + * cos(x) = 1 - x*x/2 + r + * since cos(x+y) ~ cos(x) - sin(x)*y + * ~ cos(x) - x*y, + * a correction term is necessary in cos(x) and hence + * cos(x+y) = 1 - (x*x/2 - (r - x*y)) + * For better accuracy when x > 0.3, let qx = |x|/4 with + * the last 32 bits mask off, and if x > 0.78125, let qx = 0.28125. + * Then + * cos(x+y) = (1-qx) - ((x*x/2-qx) - (r-x*y)). + * Note that 1-qx and (x*x/2-qx) is EXACT here, and the + * magnitude of the latter is at least a quarter of x*x/2, + * thus, reducing the rounding error in the subtraction. + */ +static inline double __kernel_cos(double x, double y) { + static const double + one = 1.00000000000000000000e+00, /* 0x3FF00000, 0x00000000 */ + C1 = 4.16666666666666019037e-02, /* 0x3FA55555, 0x5555554C */ + C2 = -1.38888888888741095749e-03, /* 0xBF56C16C, 0x16C15177 */ + C3 = 2.48015872894767294178e-05, /* 0x3EFA01A0, 0x19CB1590 */ + C4 = -2.75573143513906633035e-07, /* 0xBE927E4F, 0x809C52AD */ + C5 = 2.08757232129817482790e-09, /* 0x3E21EE9E, 0xBDB4B1C4 */ + C6 = -1.13596475577881948265e-11; /* 0xBDA8FAE9, 0xBE8838D4 */ + + double a, iz, z, r, qx; + int32_t ix; + GET_HIGH_WORD(ix, x); + ix &= 0x7FFFFFFF; /* ix = |x|'s high word*/ + if (ix < 0x3E400000) { /* if x < 2**27 */ + if ((int)(x) == 0) return one; /* generate inexact */ + } + z = x * x; + r = z * (C1 + z * (C2 + z * (C3 + z * (C4 + z * (C5 + z * C6))))); + if (ix < 0x3FD33333) { /* if |x| < 0.3 */ + return one - (0.5 * z - (z * r - x * y)); + } else { + if (ix > 0x3FE90000) { /* x > 0.78125 */ + qx = 0.28125; + } else { + INSERT_WORDS(qx, ix - 0x00200000, 0); /* x/4 */ + } + iz = 0.5 * z - qx; + a = one - qx; + return a - (iz - (z * r - x * y)); + } +} + +/* __kernel_rem_pio2(x,y,e0,nx,prec,ipio2) + * double x[],y[]; int e0,nx,prec; int ipio2[]; + * + * __kernel_rem_pio2 return the last three digits of N with + * y = x - N*pi/2 + * so that |y| < pi/2. + * + * The method is to compute the integer (mod 8) and fraction parts of + * (2/pi)*x without doing the full multiplication. In general we + * skip the part of the product that are known to be a huge integer ( + * more accurately, = 0 mod 8 ). Thus the number of operations are + * independent of the exponent of the input. + * + * (2/pi) is represented by an array of 24-bit integers in ipio2[]. + * + * Input parameters: + * x[] The input value (must be positive) is broken into nx + * pieces of 24-bit integers in double precision format. + * x[i] will be the i-th 24 bit of x. The scaled exponent + * of x[0] is given in input parameter e0 (i.e., x[0]*2^e0 + * match x's up to 24 bits. + * + * Example of breaking a double positive z into x[0]+x[1]+x[2]: + * e0 = ilogb(z)-23 + * z = scalbn(z,-e0) + * for i = 0,1,2 + * x[i] = floor(z) + * z = (z-x[i])*2**24 + * + * + * y[] output result in an array of double precision numbers. + * The dimension of y[] is: + * 24-bit precision 1 + * 53-bit precision 2 + * 64-bit precision 2 + * 113-bit precision 3 + * The actual value is the sum of them. Thus for 113-bit + * precison, one may have to do something like: + * + * long double t,w,r_head, r_tail; + * t = (long double)y[2] + (long double)y[1]; + * w = (long double)y[0]; + * r_head = t+w; + * r_tail = w - (r_head - t); + * + * e0 The exponent of x[0] + * + * nx dimension of x[] + * + * prec an integer indicating the precision: + * 0 24 bits (single) + * 1 53 bits (double) + * 2 64 bits (extended) + * 3 113 bits (quad) + * + * ipio2[] + * integer array, contains the (24*i)-th to (24*i+23)-th + * bit of 2/pi after binary point. The corresponding + * floating value is + * + * ipio2[i] * 2^(-24(i+1)). + * + * External function: + * double scalbn(), floor(); + * + * + * Here is the description of some local variables: + * + * jk jk+1 is the initial number of terms of ipio2[] needed + * in the computation. The recommended value is 2,3,4, + * 6 for single, double, extended,and quad. + * + * jz local integer variable indicating the number of + * terms of ipio2[] used. + * + * jx nx - 1 + * + * jv index for pointing to the suitable ipio2[] for the + * computation. In general, we want + * ( 2^e0*x[0] * ipio2[jv-1]*2^(-24jv) )/8 + * is an integer. Thus + * e0-3-24*jv >= 0 or (e0-3)/24 >= jv + * Hence jv = max(0,(e0-3)/24). + * + * jp jp+1 is the number of terms in PIo2[] needed, jp = jk. + * + * q[] double array with integral value, representing the + * 24-bits chunk of the product of x and 2/pi. + * + * q0 the corresponding exponent of q[0]. Note that the + * exponent for q[i] would be q0-24*i. + * + * PIo2[] double precision array, obtained by cutting pi/2 + * into 24 bits chunks. + * + * f[] ipio2[] in floating point + * + * iq[] integer array by breaking up q[] in 24-bits chunk. + * + * fq[] final product of x*(2/pi) in fq[0],..,fq[jk] + * + * ih integer. If >0 it indicates q[] is >= 0.5, hence + * it also indicates the *sign* of the result. + * + */ +static int __kernel_rem_pio2(double *x, double *y, int e0, int nx, int prec, + const int32_t *ipio2) { + /* Constants: + * The hexadecimal values are the intended ones for the following + * constants. The decimal values may be used, provided that the + * compiler will convert from decimal to binary accurately enough + * to produce the hexadecimal values shown. + */ + static const int init_jk[] = {2, 3, 4, 6}; /* initial value for jk */ + + static const double PIo2[] = { + 1.57079625129699707031e+00, /* 0x3FF921FB, 0x40000000 */ + 7.54978941586159635335e-08, /* 0x3E74442D, 0x00000000 */ + 5.39030252995776476554e-15, /* 0x3CF84698, 0x80000000 */ + 3.28200341580791294123e-22, /* 0x3B78CC51, 0x60000000 */ + 1.27065575308067607349e-29, /* 0x39F01B83, 0x80000000 */ + 1.22933308981111328932e-36, /* 0x387A2520, 0x40000000 */ + 2.73370053816464559624e-44, /* 0x36E38222, 0x80000000 */ + 2.16741683877804819444e-51, /* 0x3569F31D, 0x00000000 */ + }; + + static const double + zero = 0.0, + one = 1.0, + two24 = 1.67772160000000000000e+07, /* 0x41700000, 0x00000000 */ + twon24 = 5.96046447753906250000e-08; /* 0x3E700000, 0x00000000 */ + + int32_t jz, jx, jv, jp, jk, carry, n, iq[20], i, j, k, m, q0, ih; + double z, fw, f[20], fq[20], q[20]; + + /* initialize jk*/ + jk = init_jk[prec]; + jp = jk; + + /* determine jx,jv,q0, note that 3>q0 */ + jx = nx - 1; + jv = (e0 - 3) / 24; + if (jv < 0) jv = 0; + q0 = e0 - 24 * (jv + 1); + + /* set up f[0] to f[jx+jk] where f[jx+jk] = ipio2[jv+jk] */ + j = jv - jx; + m = jx + jk; + for (i = 0; i <= m; i++, j++) { + f[i] = (j < 0) ? zero : (double)(ipio2[j]); + } + + /* compute q[0],q[1],...q[jk] */ + for (i = 0; i <= jk; i++) { + for (j = 0, fw = 0.0; j <= jx; j++) fw += x[j] * f[jx + i - j]; + q[i] = fw; + } + + jz = jk; +recompute: + /* distill q[] into iq[] reversingly */ + for (i = 0, j = jz, z = q[jz]; j > 0; i++, j--) { + fw = (double)((int32_t)(twon24 * z)); + iq[i] = (int32_t)(z - two24 * fw); + z = q[j - 1] + fw; + } + + /* compute n */ + z = scalbn(z, q0); /* actual value of z */ + z -= 8.0 * scr_fd_floor(z * 0.125); /* trim off integer >= 8 */ + n = (int32_t)(z); + z -= (double)(n); + ih = 0; + if (q0 > 0) { /* need iq[jz-1] to determine n */ + i = (iq[jz - 1] >> (24 - q0)); + n += i; + iq[jz - 1] -= i << (24 - q0); + ih = iq[jz - 1] >> (23 - q0); + } else if (q0 == 0) { + ih = iq[jz - 1] >> 23; + } else if (z >= 0.5) { + ih = 2; + } + + if (ih > 0) { /* q > 0.5 */ + n += 1; + carry = 0; + for (i = 0; i < jz; i++) { /* compute 1-q */ + j = iq[i]; + if (carry == 0) { + if (j != 0) { + carry = 1; + iq[i] = 0x1000000 - j; + } + } else { + iq[i] = 0xFFFFFF - j; + } + } + if (q0 > 0) { /* rare case: chance is 1 in 12 */ + switch (q0) { + case 1: + iq[jz - 1] &= 0x7FFFFF; + break; + case 2: + iq[jz - 1] &= 0x3FFFFF; + break; + } + } + if (ih == 2) { + z = one - z; + if (carry != 0) z -= scalbn(one, q0); + } + } + + /* check if recomputation is needed */ + if (z == zero) { + j = 0; + for (i = jz - 1; i >= jk; i--) j |= iq[i]; + if (j == 0) { /* need recomputation */ + for (k = 1; jk >= k && iq[jk - k] == 0; k++) { + /* k = no. of terms needed */ + } + + for (i = jz + 1; i <= jz + k; i++) { /* add q[jz+1] to q[jz+k] */ + f[jx + i] = ipio2[jv + i]; + for (j = 0, fw = 0.0; j <= jx; j++) fw += x[j] * f[jx + i - j]; + q[i] = fw; + } + jz += k; + goto recompute; + } + } + + /* chop off zero terms */ + if (z == 0.0) { + jz -= 1; + q0 -= 24; + while (iq[jz] == 0) { + jz--; + q0 -= 24; + } + } else { /* break z into 24-bit if necessary */ + z = scalbn(z, -q0); + if (z >= two24) { + fw = (double)((int32_t)(twon24 * z)); + iq[jz] = z - two24 * fw; + jz += 1; + q0 += 24; + iq[jz] = fw; + } else { + iq[jz] = z; + } + } + + /* convert integer "bit" chunk to floating-point value */ + fw = scalbn(one, q0); + for (i = jz; i >= 0; i--) { + q[i] = fw * iq[i]; + fw *= twon24; + } + + /* compute PIo2[0,...,jp]*q[jz,...,0] */ + for (i = jz; i >= 0; i--) { + for (fw = 0.0, k = 0; k <= jp && k <= jz - i; k++) fw += PIo2[k] * q[i + k]; + fq[jz - i] = fw; + } + + /* compress fq[] into y[] */ + switch (prec) { + case 0: + fw = 0.0; + for (i = jz; i >= 0; i--) fw += fq[i]; + y[0] = (ih == 0) ? fw : -fw; + break; + case 1: + case 2: + fw = 0.0; + for (i = jz; i >= 0; i--) fw += fq[i]; + y[0] = (ih == 0) ? fw : -fw; + fw = fq[0] - fw; + for (i = 1; i <= jz; i++) fw += fq[i]; + y[1] = (ih == 0) ? fw : -fw; + break; + case 3: /* painful */ + for (i = jz; i > 0; i--) { + fw = fq[i - 1] + fq[i]; + fq[i] += fq[i - 1] - fw; + fq[i - 1] = fw; + } + for (i = jz; i > 1; i--) { + fw = fq[i - 1] + fq[i]; + fq[i] += fq[i - 1] - fw; + fq[i - 1] = fw; + } + for (fw = 0.0, i = jz; i >= 2; i--) fw += fq[i]; + if (ih == 0) { + y[0] = fq[0]; + y[1] = fq[1]; + y[2] = fw; + } else { + y[0] = -fq[0]; + y[1] = -fq[1]; + y[2] = -fw; + } + } + return n & 7; +} + +/* __kernel_sin( x, y, iy) + * kernel sin function on [-pi/4, pi/4], pi/4 ~ 0.7854 + * Input x is assumed to be bounded by ~pi/4 in magnitude. + * Input y is the tail of x. + * Input iy indicates whether y is 0. (if iy=0, y assume to be 0). + * + * Algorithm + * 1. Since sin(-x) = -sin(x), we need only to consider positive x. + * 2. if x < 2^-27 (hx<0x3E400000 0), return x with inexact if x!=0. + * 3. sin(x) is approximated by a polynomial of degree 13 on + * [0,pi/4] + * 3 13 + * sin(x) ~ x + S1*x + ... + S6*x + * where + * + * |sin(x) 2 4 6 8 10 12 | -58 + * |----- - (1+S1*x +S2*x +S3*x +S4*x +S5*x +S6*x )| <= 2 + * | x | + * + * 4. sin(x+y) = sin(x) + sin'(x')*y + * ~ sin(x) + (1-x*x/2)*y + * For better accuracy, let + * 3 2 2 2 2 + * r = x *(S2+x *(S3+x *(S4+x *(S5+x *S6)))) + * then 3 2 + * sin(x) = x + (S1*x + (x *(r-y/2)+y)) + */ +static inline double __kernel_sin(double x, double y, int iy) { + static const double + half = 5.00000000000000000000e-01, /* 0x3FE00000, 0x00000000 */ + S1 = -1.66666666666666324348e-01, /* 0xBFC55555, 0x55555549 */ + S2 = 8.33333333332248946124e-03, /* 0x3F811111, 0x1110F8A6 */ + S3 = -1.98412698298579493134e-04, /* 0xBF2A01A0, 0x19C161D5 */ + S4 = 2.75573137070700676789e-06, /* 0x3EC71DE3, 0x57B1FE7D */ + S5 = -2.50507602534068634195e-08, /* 0xBE5AE5E6, 0x8A2B9CEB */ + S6 = 1.58969099521155010221e-10; /* 0x3DE5D93A, 0x5ACFD57C */ + + double z, r, v; + int32_t ix; + GET_HIGH_WORD(ix, x); + ix &= 0x7FFFFFFF; /* high word of x */ + if (ix < 0x3E400000) { /* |x| < 2**-27 */ + if ((int)(x) == 0) return x; + } /* generate inexact */ + z = x * x; + v = z * x; + r = S2 + z * (S3 + z * (S4 + z * (S5 + z * S6))); + if (iy == 0) { + return x + v * (S1 + z * r); + } else { + return x - ((z * (half * y - v * r) - y) - v * S1); + } +} + +/* __kernel_tan( x, y, k ) + * kernel tan function on [-pi/4, pi/4], pi/4 ~ 0.7854 + * Input x is assumed to be bounded by ~pi/4 in magnitude. + * Input y is the tail of x. + * Input k indicates whether tan (if k=1) or + * -1/tan (if k= -1) is returned. + * + * Algorithm + * 1. Since tan(-x) = -tan(x), we need only to consider positive x. + * 2. if x < 2^-28 (hx<0x3E300000 0), return x with inexact if x!=0. + * 3. tan(x) is approximated by an odd polynomial of degree 27 on + * [0,0.67434] + * 3 27 + * tan(x) ~ x + T1*x + ... + T13*x + * where + * + * |tan(x) 2 4 26 | -59.2 + * |----- - (1+T1*x +T2*x +.... +T13*x )| <= 2 + * | x | + * + * Note: tan(x+y) = tan(x) + tan'(x)*y + * ~ tan(x) + (1+x*x)*y + * Therefore, for better accuracy in computing tan(x+y), let + * 3 2 2 2 2 + * r = x *(T2+x *(T3+x *(...+x *(T12+x *T13)))) + * then + * 3 2 + * tan(x+y) = x + (T1*x + (x *(r+y)+y)) + * + * 4. For x in [0.67434,pi/4], let y = pi/4 - x, then + * tan(x) = tan(pi/4-y) = (1-tan(y))/(1+tan(y)) + * = 1 - 2*(tan(y) - (tan(y)^2)/(1+tan(y))) + */ +static double __kernel_tan(double x, double y, int iy) { + static const double xxx[] = { + 3.33333333333334091986e-01, /* 3FD55555, 55555563 */ + 1.33333333333201242699e-01, /* 3FC11111, 1110FE7A */ + 5.39682539762260521377e-02, /* 3FABA1BA, 1BB341FE */ + 2.18694882948595424599e-02, /* 3F9664F4, 8406D637 */ + 8.86323982359930005737e-03, /* 3F8226E3, E96E8493 */ + 3.59207910759131235356e-03, /* 3F6D6D22, C9560328 */ + 1.45620945432529025516e-03, /* 3F57DBC8, FEE08315 */ + 5.88041240820264096874e-04, /* 3F4344D8, F2F26501 */ + 2.46463134818469906812e-04, /* 3F3026F7, 1A8D1068 */ + 7.81794442939557092300e-05, /* 3F147E88, A03792A6 */ + 7.14072491382608190305e-05, /* 3F12B80F, 32F0A7E9 */ + -1.85586374855275456654e-05, /* BEF375CB, DB605373 */ + 2.59073051863633712884e-05, /* 3EFB2A70, 74BF7AD4 */ + /* one */ 1.00000000000000000000e+00, /* 3FF00000, 00000000 */ + /* pio4 */ 7.85398163397448278999e-01, /* 3FE921FB, 54442D18 */ + /* pio4lo */ 3.06161699786838301793e-17 /* 3C81A626, 33145C07 */ + }; +#define one xxx[13] +#define pio4 xxx[14] +#define pio4lo xxx[15] +#define T xxx + + double z, r, v, w, s; + int32_t ix, hx; + + GET_HIGH_WORD(hx, x); /* high word of x */ + ix = hx & 0x7FFFFFFF; /* high word of |x| */ + if (ix < 0x3E300000) { /* x < 2**-28 */ + if ((int)(x) == 0) { /* generate inexact */ + uint32_t low; + GET_LOW_WORD(low, x); + if (((ix | low) | (iy + 1)) == 0) { + return one / __builtin_fabs(x); + } else { + if (iy == 1) { + return x; + } else { /* compute -1 / (x+y) carefully */ + double a, t; + + z = w = x + y; + SET_LOW_WORD(z, 0); + v = y - (z - x); + t = a = -one / w; + SET_LOW_WORD(t, 0); + s = one + t * z; + return t + a * (s + t * v); + } + } + } + } + if (ix >= 0x3FE59428) { /* |x| >= 0.6744 */ + if (hx < 0) { + x = -x; + y = -y; + } + z = pio4 - x; + w = pio4lo - y; + x = z + w; + y = 0.0; + } + z = x * x; + w = z * z; + /* + * Break x^5*(T[1]+x^2*T[2]+...) into + * x^5(T[1]+x^4*T[3]+...+x^20*T[11]) + + * x^5(x^2*(T[2]+x^4*T[4]+...+x^22*[T12])) + */ + r = T[1] + w * (T[3] + w * (T[5] + w * (T[7] + w * (T[9] + w * T[11])))); + v = z * + (T[2] + w * (T[4] + w * (T[6] + w * (T[8] + w * (T[10] + w * T[12]))))); + s = z * x; + r = y + z * (s * (r + v) + y); + r += T[0] * s; + w = x + r; + if (ix >= 0x3FE59428) { + v = iy; + return (1 - ((hx >> 30) & 2)) * (v - 2.0 * (x - (w * w / (w + v) - r))); + } + if (iy == 1) { + return w; + } else { + /* + * if allow error up to 2 ulp, simply return + * -1.0 / (x+r) here + */ + /* compute -1.0 / (x+r) accurately */ + double a, t; + z = w; + SET_LOW_WORD(z, 0); + v = r - (z - x); /* z+v = r+x */ + t = a = -1.0 / w; /* a = -1.0/w */ + SET_LOW_WORD(t, 0); + s = 1.0 + t * z; + return t + a * (s + t * v); + } + +#undef one +#undef pio4 +#undef pio4lo +#undef T +} + +/* acos(x) + * Method : + * acos(x) = pi/2 - asin(x) + * acos(-x) = pi/2 + asin(x) + * For |x|<=0.5 + * acos(x) = pi/2 - (x + x*x^2*R(x^2)) (see asin.c) + * For x>0.5 + * acos(x) = pi/2 - (pi/2 - 2asin(sqrt((1-x)/2))) + * = 2asin(sqrt((1-x)/2)) + * = 2s + 2s*z*R(z) ...z=(1-x)/2, s=sqrt(z) + * = 2f + (2c + 2s*z*R(z)) + * where f=hi part of s, and c = (z-f*f)/(s+f) is the correction term + * for f so that f+c ~ sqrt(z). + * For x<-0.5 + * acos(x) = pi - 2asin(sqrt((1-|x|)/2)) + * = pi - 0.5*(s+s*z*R(z)), where z=(1-|x|)/2,s=sqrt(z) + * + * Special cases: + * if x is NaN, return x itself; + * if |x|>1, return NaN with invalid signal. + * + * Function needed: sqrt + */ +double scr_ieee754_acos(double x) { + static const double + one = 1.00000000000000000000e+00, /* 0x3FF00000, 0x00000000 */ + pi = 3.14159265358979311600e+00, /* 0x400921FB, 0x54442D18 */ + pio2_hi = 1.57079632679489655800e+00, /* 0x3FF921FB, 0x54442D18 */ + pio2_lo = 6.12323399573676603587e-17, /* 0x3C91A626, 0x33145C07 */ + pS0 = 1.66666666666666657415e-01, /* 0x3FC55555, 0x55555555 */ + pS1 = -3.25565818622400915405e-01, /* 0xBFD4D612, 0x03EB6F7D */ + pS2 = 2.01212532134862925881e-01, /* 0x3FC9C155, 0x0E884455 */ + pS3 = -4.00555345006794114027e-02, /* 0xBFA48228, 0xB5688F3B */ + pS4 = 7.91534994289814532176e-04, /* 0x3F49EFE0, 0x7501B288 */ + pS5 = 3.47933107596021167570e-05, /* 0x3F023DE1, 0x0DFDF709 */ + qS1 = -2.40339491173441421878e+00, /* 0xC0033A27, 0x1C8A2D4B */ + qS2 = 2.02094576023350569471e+00, /* 0x40002AE5, 0x9C598AC8 */ + qS3 = -6.88283971605453293030e-01, /* 0xBFE6066C, 0x1B8D0159 */ + qS4 = 7.70381505559019352791e-02; /* 0x3FB3B8C5, 0xB12E9282 */ + + double z, p, q, r, w, s, c, df; + int32_t hx, ix; + GET_HIGH_WORD(hx, x); + ix = hx & 0x7FFFFFFF; + if (ix >= 0x3FF00000) { /* |x| >= 1 */ + uint32_t lx; + GET_LOW_WORD(lx, x); + if (((ix - 0x3FF00000) | lx) == 0) { /* |x|==1 */ + if (hx > 0) + return 0.0; /* acos(1) = 0 */ + else + return pi + 2.0 * pio2_lo; /* acos(-1)= pi */ + } + return __builtin_nans(""); // acos(|x|>1) is NaN + } + if (ix < 0x3FE00000) { /* |x| < 0.5 */ + if (ix <= 0x3C600000) return pio2_hi + pio2_lo; /*if|x|<2**-57*/ + z = x * x; + p = z * (pS0 + z * (pS1 + z * (pS2 + z * (pS3 + z * (pS4 + z * pS5))))); + q = one + z * (qS1 + z * (qS2 + z * (qS3 + z * qS4))); + r = p / q; + return pio2_hi - (x - (pio2_lo - x * r)); + } else if (hx < 0) { /* x < -0.5 */ + z = (one + x) * 0.5; + p = z * (pS0 + z * (pS1 + z * (pS2 + z * (pS3 + z * (pS4 + z * pS5))))); + q = one + z * (qS1 + z * (qS2 + z * (qS3 + z * qS4))); + s = __builtin_sqrt(z); + r = p / q; + w = r * s - pio2_lo; + return pi - 2.0 * (s + w); + } else { /* x > 0.5 */ + z = (one - x) * 0.5; + s = __builtin_sqrt(z); + df = s; + SET_LOW_WORD(df, 0); + c = (z - df * df) / (s + df); + p = z * (pS0 + z * (pS1 + z * (pS2 + z * (pS3 + z * (pS4 + z * pS5))))); + q = one + z * (qS1 + z * (qS2 + z * (qS3 + z * qS4))); + r = p / q; + w = r * s + c; + return 2.0 * (df + w); + } +} + +/* acosh(x) + * Method : + * Based on + * acosh(x) = log [ x + sqrt(x*x-1) ] + * we have + * acosh(x) := log(x)+ln2, if x is large; else + * acosh(x) := log(2x-1/(sqrt(x*x-1)+x)) if x>2; else + * acosh(x) := log1p(t+sqrt(2.0*t+t*t)); where t=x-1. + * + * Special cases: + * acosh(x) is NaN with signal if x<1. + * acosh(NaN) is NaN without signal. + */ +double scr_ieee754_acosh(double x) { + static const double + one = 1.0, + ln2 = 6.93147180559945286227e-01; /* 0x3FE62E42, 0xFEFA39EF */ + double t; + int32_t hx; + uint32_t lx; + EXTRACT_WORDS(hx, lx, x); + if (hx < 0x3FF00000) { /* x < 1 */ + return __builtin_nans(""); + } else if (hx >= 0x41B00000) { /* x > 2**28 */ + if (hx >= 0x7FF00000) { /* x is inf of NaN */ + return x + x; + } else { + return scr_ieee754_log(x) + ln2; /* acosh(huge)=log(2x) */ + } + } else if (((hx - 0x3FF00000) | lx) == 0) { + return 0.0; /* acosh(1) = 0 */ + } else if (hx > 0x40000000) { /* 2**28 > x > 2 */ + t = x * x; + return scr_ieee754_log(2.0 * x - one / (x + __builtin_sqrt(t - one))); + } else { /* 10.98 + * asin(x) = pi/2 - 2*(s+s*z*R(z)) + * = pio2_hi - (2*(s+s*z*R(z)) - pio2_lo) + * For x<=0.98, let pio4_hi = pio2_hi/2, then + * f = hi part of s; + * c = sqrt(z) - f = (z-f*f)/(s+f) ...f+c=sqrt(z) + * and + * asin(x) = pi/2 - 2*(s+s*z*R(z)) + * = pio4_hi+(pio4-2s)-(2s*z*R(z)-pio2_lo) + * = pio4_hi+(pio4-2f)-(2s*z*R(z)-(pio2_lo+2c)) + * + * Special cases: + * if x is NaN, return x itself; + * if |x|>1, return NaN with invalid signal. + */ +double scr_ieee754_asin(double x) { + static const double + one = 1.00000000000000000000e+00, /* 0x3FF00000, 0x00000000 */ + huge = 1.000e+300, + pio2_hi = 1.57079632679489655800e+00, /* 0x3FF921FB, 0x54442D18 */ + pio2_lo = 6.12323399573676603587e-17, /* 0x3C91A626, 0x33145C07 */ + pio4_hi = 7.85398163397448278999e-01, /* 0x3FE921FB, 0x54442D18 */ + /* coefficient for R(x^2) */ + pS0 = 1.66666666666666657415e-01, /* 0x3FC55555, 0x55555555 */ + pS1 = -3.25565818622400915405e-01, /* 0xBFD4D612, 0x03EB6F7D */ + pS2 = 2.01212532134862925881e-01, /* 0x3FC9C155, 0x0E884455 */ + pS3 = -4.00555345006794114027e-02, /* 0xBFA48228, 0xB5688F3B */ + pS4 = 7.91534994289814532176e-04, /* 0x3F49EFE0, 0x7501B288 */ + pS5 = 3.47933107596021167570e-05, /* 0x3F023DE1, 0x0DFDF709 */ + qS1 = -2.40339491173441421878e+00, /* 0xC0033A27, 0x1C8A2D4B */ + qS2 = 2.02094576023350569471e+00, /* 0x40002AE5, 0x9C598AC8 */ + qS3 = -6.88283971605453293030e-01, /* 0xBFE6066C, 0x1B8D0159 */ + qS4 = 7.70381505559019352791e-02; /* 0x3FB3B8C5, 0xB12E9282 */ + + double t, w, p, q, c, r, s; + int32_t hx, ix; + + t = 0; + GET_HIGH_WORD(hx, x); + ix = hx & 0x7FFFFFFF; + if (ix >= 0x3FF00000) { /* |x|>= 1 */ + uint32_t lx; + GET_LOW_WORD(lx, x); + if (((ix - 0x3FF00000) | lx) == 0) { /* asin(1)=+-pi/2 with inexact */ + return x * pio2_hi + x * pio2_lo; + } + return __builtin_nans(""); // asin(|x|>1) is NaN + } else if (ix < 0x3FE00000) { /* |x|<0.5 */ + if (ix < 0x3E400000) { /* if |x| < 2**-27 */ + if (huge + x > one) return x; /* return x with inexact if x!=0*/ + } else { + t = x * x; + } + p = t * (pS0 + t * (pS1 + t * (pS2 + t * (pS3 + t * (pS4 + t * pS5))))); + q = one + t * (qS1 + t * (qS2 + t * (qS3 + t * qS4))); + w = p / q; + return x + x * w; + } + /* 1> |x|>= 0.5 */ + w = one - __builtin_fabs(x); + t = w * 0.5; + p = t * (pS0 + t * (pS1 + t * (pS2 + t * (pS3 + t * (pS4 + t * pS5))))); + q = one + t * (qS1 + t * (qS2 + t * (qS3 + t * qS4))); + s = __builtin_sqrt(t); + if (ix >= 0x3FEF3333) { /* if |x| > 0.975 */ + w = p / q; + t = pio2_hi - (2.0 * (s + s * w) - pio2_lo); + } else { + w = s; + SET_LOW_WORD(w, 0); + c = (t - w * w) / (s + w); + r = p / q; + p = 2.0 * s * r - (pio2_lo - 2.0 * c); + q = pio4_hi - 2.0 * w; + t = pio4_hi - (p - q); + } + if (hx > 0) + return t; + else + return -t; +} +/* asinh(x) + * Method : + * Based on + * asinh(x) = sign(x) * log [ |x| + sqrt(x*x+1) ] + * we have + * asinh(x) := x if 1+x*x=1, + * := sign(x)*(log(x)+ln2)) for large |x|, else + * := sign(x)*log(2|x|+1/(|x|+sqrt(x*x+1))) if|x|>2, else + * := sign(x)*log1p(|x| + x^2/(1 + sqrt(1+x^2))) + */ +double scr_ieee754_asinh(double x) { + static const double + one = 1.00000000000000000000e+00, /* 0x3FF00000, 0x00000000 */ + ln2 = 6.93147180559945286227e-01, /* 0x3FE62E42, 0xFEFA39EF */ + huge = 1.00000000000000000000e+300; + + double t, w; + int32_t hx, ix; + GET_HIGH_WORD(hx, x); + ix = hx & 0x7FFFFFFF; + if (ix >= 0x7FF00000) return x + x; /* x is inf or NaN */ + if (ix < 0x3E300000) { /* |x|<2**-28 */ + if (huge + x > one) return x; /* return x inexact except 0 */ + } + if (ix > 0x41B00000) { /* |x| > 2**28 */ + w = scr_ieee754_log(__builtin_fabs(x)) + ln2; + } else if (ix > 0x40000000) { /* 2**28 > |x| > 2.0 */ + t = __builtin_fabs(x); + w = scr_ieee754_log(2.0 * t + one / (__builtin_sqrt(x * x + one) + t)); + } else { /* 2.0 > |x| > 2**-28 */ + t = x * x; + w = scr_ieee754_log1p(__builtin_fabs(x) + t / (one + __builtin_sqrt(one + t))); + } + if (hx > 0) { + return w; + } else { + return -w; + } +} + +/* atan(x) + * Method + * 1. Reduce x to positive by atan(x) = -atan(-x). + * 2. According to the integer k=4t+0.25 chopped, t=x, the argument + * is further reduced to one of the following intervals and the + * arctangent of t is evaluated by the corresponding formula: + * + * [0,7/16] atan(x) = t-t^3*(a1+t^2*(a2+...(a10+t^2*a11)...) + * [7/16,11/16] atan(x) = atan(1/2) + atan( (t-0.5)/(1+t/2) ) + * [11/16.19/16] atan(x) = atan( 1 ) + atan( (t-1)/(1+t) ) + * [19/16,39/16] atan(x) = atan(3/2) + atan( (t-1.5)/(1+1.5t) ) + * [39/16,INF] atan(x) = atan(INF) + atan( -1/t ) + * + * Constants: + * The hexadecimal values are the intended ones for the following + * constants. The decimal values may be used, provided that the + * compiler will convert from decimal to binary accurately enough + * to produce the hexadecimal values shown. + */ +double scr_ieee754_atan(double x) { + static const double atanhi[] = { + 4.63647609000806093515e-01, /* atan(0.5)hi 0x3FDDAC67, 0x0561BB4F */ + 7.85398163397448278999e-01, /* atan(1.0)hi 0x3FE921FB, 0x54442D18 */ + 9.82793723247329054082e-01, /* atan(1.5)hi 0x3FEF730B, 0xD281F69B */ + 1.57079632679489655800e+00, /* atan(inf)hi 0x3FF921FB, 0x54442D18 */ + }; + + static const double atanlo[] = { + 2.26987774529616870924e-17, /* atan(0.5)lo 0x3C7A2B7F, 0x222F65E2 */ + 3.06161699786838301793e-17, /* atan(1.0)lo 0x3C81A626, 0x33145C07 */ + 1.39033110312309984516e-17, /* atan(1.5)lo 0x3C700788, 0x7AF0CBBD */ + 6.12323399573676603587e-17, /* atan(inf)lo 0x3C91A626, 0x33145C07 */ + }; + + static const double aT[] = { + 3.33333333333329318027e-01, /* 0x3FD55555, 0x5555550D */ + -1.99999999998764832476e-01, /* 0xBFC99999, 0x9998EBC4 */ + 1.42857142725034663711e-01, /* 0x3FC24924, 0x920083FF */ + -1.11111104054623557880e-01, /* 0xBFBC71C6, 0xFE231671 */ + 9.09088713343650656196e-02, /* 0x3FB745CD, 0xC54C206E */ + -7.69187620504482999495e-02, /* 0xBFB3B0F2, 0xAF749A6D */ + 6.66107313738753120669e-02, /* 0x3FB10D66, 0xA0D03D51 */ + -5.83357013379057348645e-02, /* 0xBFADDE2D, 0x52DEFD9A */ + 4.97687799461593236017e-02, /* 0x3FA97B4B, 0x24760DEB */ + -3.65315727442169155270e-02, /* 0xBFA2B444, 0x2C6A6C2F */ + 1.62858201153657823623e-02, /* 0x3F90AD3A, 0xE322DA11 */ + }; + + static const double one = 1.0, huge = 1.0e300; + + double w, s1, s2, z; + int32_t ix, hx, id; + + GET_HIGH_WORD(hx, x); + ix = hx & 0x7FFFFFFF; + if (ix >= 0x44100000) { /* if |x| >= 2^66 */ + uint32_t low; + GET_LOW_WORD(low, x); + if (ix > 0x7FF00000 || (ix == 0x7FF00000 && (low != 0))) + return x + x; /* NaN */ + if (hx > 0) + return atanhi[3] + *(volatile double *)(&atanlo[3]); + else + return -atanhi[3] - *(volatile double *)(&atanlo[3]); + } + if (ix < 0x3FDC0000) { /* |x| < 0.4375 */ + if (ix < 0x3E400000) { /* |x| < 2^-27 */ + if (huge + x > one) return x; /* raise inexact */ + } + id = -1; + } else { + x = __builtin_fabs(x); + if (ix < 0x3FF30000) { /* |x| < 1.1875 */ + if (ix < 0x3FE60000) { /* 7/16 <=|x|<11/16 */ + id = 0; + x = (2.0 * x - one) / (2.0 + x); + } else { /* 11/16<=|x|< 19/16 */ + id = 1; + x = (x - one) / (x + one); + } + } else { + if (ix < 0x40038000) { /* |x| < 2.4375 */ + id = 2; + x = (x - 1.5) / (one + 1.5 * x); + } else { /* 2.4375 <= |x| < 2^66 */ + id = 3; + x = -1.0 / x; + } + } + } + /* end of argument reduction */ + z = x * x; + w = z * z; + /* break sum from i=0 to 10 aT[i]z**(i+1) into odd and even poly */ + s1 = z * (aT[0] + + w * (aT[2] + w * (aT[4] + w * (aT[6] + w * (aT[8] + w * aT[10]))))); + s2 = w * (aT[1] + w * (aT[3] + w * (aT[5] + w * (aT[7] + w * aT[9])))); + if (id < 0) { + return x - x * (s1 + s2); + } else { + z = atanhi[id] - ((x * (s1 + s2) - atanlo[id]) - x); + return (hx < 0) ? -z : z; + } +} + +/* atan2(y,x) + * Method : + * 1. Reduce y to positive by atan2(y,x)=-atan2(-y,x). + * 2. Reduce x to positive by (if x and y are unexceptional): + * ARG (x+iy) = arctan(y/x) ... if x > 0, + * ARG (x+iy) = pi - arctan[y/(-x)] ... if x < 0, + * + * Special cases: + * + * ATAN2((anything), NaN ) is NaN; + * ATAN2(NAN , (anything) ) is NaN; + * ATAN2(+-0, +(anything but NaN)) is +-0 ; + * ATAN2(+-0, -(anything but NaN)) is +-pi ; + * ATAN2(+-(anything but 0 and NaN), 0) is +-pi/2; + * ATAN2(+-(anything but INF and NaN), +INF) is +-0 ; + * ATAN2(+-(anything but INF and NaN), -INF) is +-pi; + * ATAN2(+-INF,+INF ) is +-pi/4 ; + * ATAN2(+-INF,-INF ) is +-3pi/4; + * ATAN2(+-INF, (anything but,0,NaN, and INF)) is +-pi/2; + * + * Constants: + * The hexadecimal values are the intended ones for the following + * constants. The decimal values may be used, provided that the + * compiler will convert from decimal to binary accurately enough + * to produce the hexadecimal values shown. + */ +double scr_ieee754_atan2(double y, double x) { + static volatile double tiny = 1.0e-300; + static const double + zero = 0.0, + pi_o_4 = 7.8539816339744827900E-01, /* 0x3FE921FB, 0x54442D18 */ + pi_o_2 = 1.5707963267948965580E+00, /* 0x3FF921FB, 0x54442D18 */ + pi = 3.1415926535897931160E+00; /* 0x400921FB, 0x54442D18 */ + static volatile double pi_lo = + 1.2246467991473531772E-16; /* 0x3CA1A626, 0x33145C07 */ + + double z; + int32_t k, m, hx, hy, ix, iy; + uint32_t lx, ly; + + EXTRACT_WORDS(hx, lx, x); + ix = hx & 0x7FFFFFFF; + EXTRACT_WORDS(hy, ly, y); + iy = hy & 0x7FFFFFFF; + if (((ix | ((lx | ((int32_t)(0u - (uint32_t)(lx)))) >> 31)) > 0x7FF00000) || + ((iy | ((ly | ((int32_t)(0u - (uint32_t)(ly)))) >> 31)) > 0x7FF00000)) { + return x + y; /* x or y is NaN */ + } + if ((((int32_t)((uint32_t)(hx) - (uint32_t)(0x3FF00000))) | lx) == 0) { + return scr_ieee754_atan(y); /* x=1.0 */ + } + m = ((hy >> 31) & 1) | ((hx >> 30) & 2); /* 2*sign(x)+sign(y) */ + + /* when y = 0 */ + if ((iy | ly) == 0) { + switch (m) { + case 0: + case 1: + return y; /* atan(+-0,+anything)=+-0 */ + case 2: + return pi + tiny; /* atan(+0,-anything) = pi */ + case 3: + return -pi - tiny; /* atan(-0,-anything) =-pi */ + } + } + /* when x = 0 */ + if ((ix | lx) == 0) return (hy < 0) ? -pi_o_2 - tiny : pi_o_2 + tiny; + + /* when x is INF */ + if (ix == 0x7FF00000) { + if (iy == 0x7FF00000) { + switch (m) { + case 0: + return pi_o_4 + tiny; /* atan(+INF,+INF) */ + case 1: + return -pi_o_4 - tiny; /* atan(-INF,+INF) */ + case 2: + return 3.0 * pi_o_4 + tiny; /*atan(+INF,-INF)*/ + case 3: + return -3.0 * pi_o_4 - tiny; /*atan(-INF,-INF)*/ + } + } else { + switch (m) { + case 0: + return zero; /* atan(+...,+INF) */ + case 1: + return -zero; /* atan(-...,+INF) */ + case 2: + return pi + tiny; /* atan(+...,-INF) */ + case 3: + return -pi - tiny; /* atan(-...,-INF) */ + } + } + } + /* when y is INF */ + if (iy == 0x7FF00000) return (hy < 0) ? -pi_o_2 - tiny : pi_o_2 + tiny; + + /* compute y/x */ + k = (iy - ix) >> 20; + if (k > 60) { /* |y/x| > 2**60 */ + z = pi_o_2 + 0.5 * pi_lo; + m &= 1; + } else if (hx < 0 && k < -60) { + z = 0.0; /* 0 > |y|/x > -2**-60 */ + } else { + z = scr_ieee754_atan(__builtin_fabs(y / x)); /* safe to do y/x */ + } + switch (m) { + case 0: + return z; /* atan(+,+) */ + case 1: + return -z; /* atan(-,+) */ + case 2: + return pi - (z - pi_lo); /* atan(+,-) */ + default: /* case 3 */ + return (z - pi_lo) - pi; /* atan(-,-) */ + } +} + +/* cos(x) + * Return cosine function of x. + * + * kernel function: + * __kernel_sin ... sine function on [-pi/4,pi/4] + * __kernel_cos ... cosine function on [-pi/4,pi/4] + * __ieee754_rem_pio2 ... argument reduction routine + * + * Method. + * Let S,C and T denote the sin, cos and tan respectively on + * [-PI/4, +PI/4]. Reduce the argument x to y1+y2 = x-k*pi/2 + * in [-pi/4 , +pi/4], and let n = k mod 4. + * We have + * + * n sin(x) cos(x) tan(x) + * ---------------------------------------------------------- + * 0 S C T + * 1 C -S -1/T + * 2 -S -C T + * 3 -C S -1/T + * ---------------------------------------------------------- + * + * Special cases: + * Let trig be any of sin, cos, or tan. + * trig(+-INF) is NaN, with signals; + * trig(NaN) is that NaN; + * + * Accuracy: + * TRIG(x) returns trig(x) nearly rounded + */ +double scr_ieee754_cos(double x) { + double y[2], z = 0.0; + int32_t n, ix; + + /* High word of x. */ + GET_HIGH_WORD(ix, x); + + /* |x| ~< pi/4 */ + ix &= 0x7FFFFFFF; + if (ix <= 0x3FE921FB) { + return __kernel_cos(x, z); + } else if (ix >= 0x7FF00000) { + /* cos(Inf or NaN) is NaN */ + return x - x; + } else { + /* argument reduction needed */ + n = __ieee754_rem_pio2(x, y); + switch (n & 3) { + case 0: + return __kernel_cos(y[0], y[1]); + case 1: + return -__kernel_sin(y[0], y[1], 1); + case 2: + return -__kernel_cos(y[0], y[1]); + default: + return __kernel_sin(y[0], y[1], 1); + } + } +} + +/* exp(x) + * Returns the exponential of x. + * + * Method + * 1. Argument reduction: + * Reduce x to an r so that |r| <= 0.5*ln2 ~ 0.34658. + * Given x, find r and integer k such that + * + * x = k*ln2 + r, |r| <= 0.5*ln2. + * + * Here r will be represented as r = hi-lo for better + * accuracy. + * + * 2. Approximation of exp(r) by a special rational function on + * the interval [0,0.34658]: + * Write + * R(r**2) = r*(exp(r)+1)/(exp(r)-1) = 2 + r*r/6 - r**4/360 + ... + * We use a special Remes algorithm on [0,0.34658] to generate + * a polynomial of degree 5 to approximate R. The maximum error + * of this polynomial approximation is bounded by 2**-59. In + * other words, + * R(z) ~ 2.0 + P1*z + P2*z**2 + P3*z**3 + P4*z**4 + P5*z**5 + * (where z=r*r, and the values of P1 to P5 are listed below) + * and + * | 5 | -59 + * | 2.0+P1*z+...+P5*z - R(z) | <= 2 + * | | + * The computation of exp(r) thus becomes + * 2*r + * exp(r) = 1 + ------- + * R - r + * r*R1(r) + * = 1 + r + ----------- (for better accuracy) + * 2 - R1(r) + * where + * 2 4 10 + * R1(r) = r - (P1*r + P2*r + ... + P5*r ). + * + * 3. Scale back to obtain exp(x): + * From step 1, we have + * exp(x) = 2^k * exp(r) + * + * Special cases: + * exp(INF) is INF, exp(NaN) is NaN; + * exp(-INF) is 0, and + * for finite argument, only exp(0)=1 is exact. + * + * Accuracy: + * according to an error analysis, the error is always less than + * 1 ulp (unit in the last place). + * + * Misc. info. + * For IEEE double + * if x > 7.09782712893383973096e+02 then exp(x) overflow + * if x < -7.45133219101941108420e+02 then exp(x) underflow + * + * Constants: + * The hexadecimal values are the intended ones for the following + * constants. The decimal values may be used, provided that the + * compiler will convert from decimal to binary accurately enough + * to produce the hexadecimal values shown. + */ +double scr_ieee754_exp(double x) { + static const double + one = 1.0, + halF[2] = {0.5, -0.5}, + o_threshold = 7.09782712893383973096e+02, /* 0x40862E42, 0xFEFA39EF */ + u_threshold = -7.45133219101941108420e+02, /* 0xC0874910, 0xD52D3051 */ + ln2HI[2] = {6.93147180369123816490e-01, /* 0x3FE62E42, 0xFEE00000 */ + -6.93147180369123816490e-01}, /* 0xBFE62E42, 0xFEE00000 */ + ln2LO[2] = {1.90821492927058770002e-10, /* 0x3DEA39EF, 0x35793C76 */ + -1.90821492927058770002e-10}, /* 0xBDEA39EF, 0x35793C76 */ + invln2 = 1.44269504088896338700e+00, /* 0x3FF71547, 0x652B82FE */ + P1 = 1.66666666666666019037e-01, /* 0x3FC55555, 0x5555553E */ + P2 = -2.77777777770155933842e-03, /* 0xBF66C16C, 0x16BEBD93 */ + P3 = 6.61375632143793436117e-05, /* 0x3F11566A, 0xAF25DE2C */ + P4 = -1.65339022054652515390e-06, /* 0xBEBBBD41, 0xC5D26BF1 */ + P5 = 4.13813679705723846039e-08, /* 0x3E663769, 0x72BEA4D0 */ + E = 2.718281828459045; /* 0x4005BF0A, 0x8B145769 */ + + static volatile double + huge = 1.0e+300, + twom1000 = 9.33263618503218878990e-302, /* 2**-1000=0x01700000,0*/ + two1023 = 8.988465674311579539e307; /* 0x1p1023 */ + + double y, hi = 0.0, lo = 0.0, c, t, twopk; + int32_t k = 0, xsb; + uint32_t hx; + + GET_HIGH_WORD(hx, x); + xsb = (hx >> 31) & 1; /* sign bit of x */ + hx &= 0x7FFFFFFF; /* high word of |x| */ + + /* filter out non-finite argument */ + if (hx >= 0x40862E42) { /* if |x|>=709.78... */ + if (hx >= 0x7FF00000) { + uint32_t lx; + GET_LOW_WORD(lx, x); + if (((hx & 0xFFFFF) | lx) != 0) + return x + x; /* NaN */ + else + return (xsb == 0) ? x : 0.0; /* exp(+-inf)={inf,0} */ + } + if (x > o_threshold) return huge * huge; /* overflow */ + if (x < u_threshold) return twom1000 * twom1000; /* underflow */ + } + + /* argument reduction */ + if (hx > 0x3FD62E42) { /* if |x| > 0.5 ln2 */ + if (hx < 0x3FF0A2B2) { /* and |x| < 1.5 ln2 */ + /* TODO(rtoy): We special case exp(1) here to return the correct + * value of E, as the computation below would get the last bit + * wrong. We should probably fix the algorithm instead. + */ + if (x == 1.0) return E; + hi = x - ln2HI[xsb]; + lo = ln2LO[xsb]; + k = 1 - xsb - xsb; + } else { + k = (int)(invln2 * x + halF[xsb]); + t = k; + hi = x - t * ln2HI[0]; /* t*ln2HI is exact here */ + lo = t * ln2LO[0]; + } + x = hi - lo; + } else if (hx < 0x3E300000) { /* when |x|<2**-28 */ + if (huge + x > one) return one + x; /* trigger inexact */ + } else { + k = 0; + } + + /* x is now in primary range */ + t = x * x; + if (k >= -1021) { + INSERT_WORDS( + twopk, + 0x3FF00000 + (int32_t)((uint32_t)(k) << 20), 0); + } else { + INSERT_WORDS(twopk, 0x3FF00000 + ((uint32_t)(k + 1000) << 20), + 0); + } + c = x - t * (P1 + t * (P2 + t * (P3 + t * (P4 + t * P5)))); + if (k == 0) { + return one - ((x * c) / (c - 2.0) - x); + } else { + y = one - ((lo - (x * c) / (2.0 - c)) - hi); + } + if (k >= -1021) { + if (k == 1024) return y * 2.0 * two1023; + return y * twopk; + } else { + return y * twopk * twom1000; + } +} + +/* + * Method : + * 1.Reduced x to positive by atanh(-x) = -atanh(x) + * 2.For x>=0.5 + * 1 2x x + * atanh(x) = --- * log(1 + -------) = 0.5 * log1p(2 * --------) + * 2 1 - x 1 - x + * + * For x<0.5 + * atanh(x) = 0.5*log1p(2x+2x*x/(1-x)) + * + * Special cases: + * atanh(x) is NaN if |x| > 1 with signal; + * atanh(NaN) is that NaN with no signal; + * atanh(+-1) is +-INF with signal. + * + */ +double scr_ieee754_atanh(double x) { + static const double one = 1.0, huge = 1e300; + static const double zero = 0.0; + + double t; + int32_t hx, ix; + uint32_t lx; + EXTRACT_WORDS(hx, lx, x); + ix = hx & 0x7FFFFFFF; + if ((ix | ((lx | ((int32_t)(0u - (uint32_t)(lx)))) >> 31)) > 0x3FF00000) { + /* |x|>1 */ + return __builtin_nans(""); + } + if (ix == 0x3FF00000) { + return x > 0 ? __builtin_inf() + : -__builtin_inf(); + } + if (ix < 0x3E300000 && (huge + x) > zero) return x; /* x<2**-28 */ + SET_HIGH_WORD(x, ix); + if (ix < 0x3FE00000) { /* x < 0.5 */ + t = x + x; + t = 0.5 * scr_ieee754_log1p(t + t * x / (one - x)); + } else { + t = 0.5 * scr_ieee754_log1p((x + x) / (one - x)); + } + if (hx >= 0) + return t; + else + return -t; +} + +/* log(x) + * Return the logrithm of x + * + * Method : + * 1. Argument Reduction: find k and f such that + * x = 2^k * (1+f), + * where sqrt(2)/2 < 1+f < sqrt(2) . + * + * 2. Approximation of log(1+f). + * Let s = f/(2+f) ; based on log(1+f) = log(1+s) - log(1-s) + * = 2s + 2/3 s**3 + 2/5 s**5 + ....., + * = 2s + s*R + * We use a special Reme algorithm on [0,0.1716] to generate + * a polynomial of degree 14 to approximate R The maximum error + * of this polynomial approximation is bounded by 2**-58.45. In + * other words, + * 2 4 6 8 10 12 14 + * R(z) ~ Lg1*s +Lg2*s +Lg3*s +Lg4*s +Lg5*s +Lg6*s +Lg7*s + * (the values of Lg1 to Lg7 are listed in the program) + * and + * | 2 14 | -58.45 + * | Lg1*s +...+Lg7*s - R(z) | <= 2 + * | | + * Note that 2s = f - s*f = f - hfsq + s*hfsq, where hfsq = f*f/2. + * In order to guarantee error in log below 1ulp, we compute log + * by + * log(1+f) = f - s*(f - R) (if f is not too large) + * log(1+f) = f - (hfsq - s*(hfsq+R)). (better accuracy) + * + * 3. Finally, log(x) = k*ln2 + log(1+f). + * = k*ln2_hi+(f-(hfsq-(s*(hfsq+R)+k*ln2_lo))) + * Here ln2 is split into two floating point number: + * ln2_hi + ln2_lo, + * where n*ln2_hi is always exact for |n| < 2000. + * + * Special cases: + * log(x) is NaN with signal if x < 0 (including -INF) ; + * log(+INF) is +INF; log(0) is -INF with signal; + * log(NaN) is that NaN with no signal. + * + * Accuracy: + * according to an error analysis, the error is always less than + * 1 ulp (unit in the last place). + * + * Constants: + * The hexadecimal values are the intended ones for the following + * constants. The decimal values may be used, provided that the + * compiler will convert from decimal to binary accurately enough + * to produce the hexadecimal values shown. + */ +double scr_ieee754_log(double x) { + static const double /* -- */ + ln2_hi = 6.93147180369123816490e-01, /* 3fe62e42 fee00000 */ + ln2_lo = 1.90821492927058770002e-10, /* 3dea39ef 35793c76 */ + two54 = 1.80143985094819840000e+16, /* 43500000 00000000 */ + Lg1 = 6.666666666666735130e-01, /* 3FE55555 55555593 */ + Lg2 = 3.999999999940941908e-01, /* 3FD99999 9997FA04 */ + Lg3 = 2.857142874366239149e-01, /* 3FD24924 94229359 */ + Lg4 = 2.222219843214978396e-01, /* 3FCC71C5 1D8E78AF */ + Lg5 = 1.818357216161805012e-01, /* 3FC74664 96CB03DE */ + Lg6 = 1.531383769920937332e-01, /* 3FC39A09 D078C69F */ + Lg7 = 1.479819860511658591e-01; /* 3FC2F112 DF3E5244 */ + + static const double zero = 0.0; + + double hfsq, f, s, z, R, w, t1, t2, dk; + int32_t k, hx, i, j; + uint32_t lx; + + EXTRACT_WORDS(hx, lx, x); + + k = 0; + if (hx < 0x00100000) { /* x < 2**-1022 */ + if (((hx & 0x7FFFFFFF) | lx) == 0) { + return -__builtin_inf(); /* log(+-0)=-inf */ + } + if (hx < 0) { + return __builtin_nans(""); /* log(-#) = NaN */ + } + k -= 54; + x *= two54; /* subnormal number, scale up x */ + GET_HIGH_WORD(hx, x); + } + if (hx >= 0x7FF00000) return x + x; + k += (hx >> 20) - 1023; + hx &= 0x000FFFFF; + i = (hx + 0x95F64) & 0x100000; + SET_HIGH_WORD(x, hx | (i ^ 0x3FF00000)); /* normalize x or x/2 */ + k += (i >> 20); + f = x - 1.0; + if ((0x000FFFFF & (2 + hx)) < 3) { /* -2**-20 <= f < 2**-20 */ + if (f == zero) { + if (k == 0) { + return zero; + } else { + dk = (double)(k); + return dk * ln2_hi + dk * ln2_lo; + } + } + R = f * f * (0.5 - 0.33333333333333333 * f); + if (k == 0) { + return f - R; + } else { + dk = (double)(k); + return dk * ln2_hi - ((R - dk * ln2_lo) - f); + } + } + s = f / (2.0 + f); + dk = (double)(k); + z = s * s; + i = hx - 0x6147A; + w = z * z; + j = 0x6B851 - hx; + t1 = w * (Lg2 + w * (Lg4 + w * Lg6)); + t2 = z * (Lg1 + w * (Lg3 + w * (Lg5 + w * Lg7))); + i |= j; + R = t2 + t1; + if (i > 0) { + hfsq = 0.5 * f * f; + if (k == 0) + return f - (hfsq - s * (hfsq + R)); + else + return dk * ln2_hi - ((hfsq - (s * (hfsq + R) + dk * ln2_lo)) - f); + } else { + if (k == 0) + return f - s * (f - R); + else + return dk * ln2_hi - ((s * (f - R) - dk * ln2_lo) - f); + } +} + +/* double log1p(double x) + * + * Method : + * 1. Argument Reduction: find k and f such that + * 1+x = 2^k * (1+f), + * where sqrt(2)/2 < 1+f < sqrt(2) . + * + * Note. If k=0, then f=x is exact. However, if k!=0, then f + * may not be representable exactly. In that case, a correction + * term is need. Let u=1+x rounded. Let c = (1+x)-u, then + * log(1+x) - log(u) ~ c/u. Thus, we proceed to compute log(u), + * and add back the correction term c/u. + * (Note: when x > 2**53, one can simply return log(x)) + * + * 2. Approximation of log1p(f). + * Let s = f/(2+f) ; based on log(1+f) = log(1+s) - log(1-s) + * = 2s + 2/3 s**3 + 2/5 s**5 + ....., + * = 2s + s*R + * We use a special Reme algorithm on [0,0.1716] to generate + * a polynomial of degree 14 to approximate R The maximum error + * of this polynomial approximation is bounded by 2**-58.45. In + * other words, + * 2 4 6 8 10 12 14 + * R(z) ~ Lp1*s +Lp2*s +Lp3*s +Lp4*s +Lp5*s +Lp6*s +Lp7*s + * (the values of Lp1 to Lp7 are listed in the program) + * and + * | 2 14 | -58.45 + * | Lp1*s +...+Lp7*s - R(z) | <= 2 + * | | + * Note that 2s = f - s*f = f - hfsq + s*hfsq, where hfsq = f*f/2. + * In order to guarantee error in log below 1ulp, we compute log + * by + * log1p(f) = f - (hfsq - s*(hfsq+R)). + * + * 3. Finally, log1p(x) = k*ln2 + log1p(f). + * = k*ln2_hi+(f-(hfsq-(s*(hfsq+R)+k*ln2_lo))) + * Here ln2 is split into two floating point number: + * ln2_hi + ln2_lo, + * where n*ln2_hi is always exact for |n| < 2000. + * + * Special cases: + * log1p(x) is NaN with signal if x < -1 (including -INF) ; + * log1p(+INF) is +INF; log1p(-1) is -INF with signal; + * log1p(NaN) is that NaN with no signal. + * + * Accuracy: + * according to an error analysis, the error is always less than + * 1 ulp (unit in the last place). + * + * Constants: + * The hexadecimal values are the intended ones for the following + * constants. The decimal values may be used, provided that the + * compiler will convert from decimal to binary accurately enough + * to produce the hexadecimal values shown. + * + * Note: Assuming log() return accurate answer, the following + * algorithm can be used to compute log1p(x) to within a few ULP: + * + * u = 1+x; + * if(u==1.0) return x ; else + * return log(u)*(x/(u-1.0)); + * + * See HP-15C Advanced Functions Handbook, p.193. + */ +double scr_ieee754_log1p(double x) { + static const double /* -- */ + ln2_hi = 6.93147180369123816490e-01, /* 3fe62e42 fee00000 */ + ln2_lo = 1.90821492927058770002e-10, /* 3dea39ef 35793c76 */ + two54 = 1.80143985094819840000e+16, /* 43500000 00000000 */ + Lp1 = 6.666666666666735130e-01, /* 3FE55555 55555593 */ + Lp2 = 3.999999999940941908e-01, /* 3FD99999 9997FA04 */ + Lp3 = 2.857142874366239149e-01, /* 3FD24924 94229359 */ + Lp4 = 2.222219843214978396e-01, /* 3FCC71C5 1D8E78AF */ + Lp5 = 1.818357216161805012e-01, /* 3FC74664 96CB03DE */ + Lp6 = 1.531383769920937332e-01, /* 3FC39A09 D078C69F */ + Lp7 = 1.479819860511658591e-01; /* 3FC2F112 DF3E5244 */ + + static const double zero = 0.0; + + double hfsq, f, c, s, z, R, u; + int32_t k, hx, hu, ax; + + GET_HIGH_WORD(hx, x); + ax = hx & 0x7FFFFFFF; + + k = 1; + if (hx < 0x3FDA827A) { /* 1+x < sqrt(2)+ */ + if (ax >= 0x3FF00000) { /* x <= -1.0 */ + if (x == -1.0) + return -__builtin_inf(); /* log1p(-1)=+inf */ + else + return __builtin_nans(""); // log1p(x<-1)=NaN + } + if (ax < 0x3E200000) { /* |x| < 2**-29 */ + if (two54 + x > zero /* raise inexact */ + && ax < 0x3C900000) /* |x| < 2**-54 */ + return x; + else + return x - x * x * 0.5; + } + if (hx > 0 || hx <= (int32_t)(0xBFD2BEC4)) { + k = 0; + f = x; + hu = 1; + } /* sqrt(2)/2- <= 1+x < sqrt(2)+ */ + } + if (hx >= 0x7FF00000) return x + x; + if (k != 0) { + if (hx < 0x43400000) { + u = 1.0 + x; + GET_HIGH_WORD(hu, u); + k = (hu >> 20) - 1023; + c = (k > 0) ? 1.0 - (u - x) : x - (u - 1.0); /* correction term */ + c /= u; + } else { + u = x; + GET_HIGH_WORD(hu, u); + k = (hu >> 20) - 1023; + c = 0; + } + hu &= 0x000FFFFF; + /* + * The approximation to sqrt(2) used in thresholds is not + * critical. However, the ones used above must give less + * strict bounds than the one here so that the k==0 case is + * never reached from here, since here we have committed to + * using the correction term but don't use it if k==0. + */ + if (hu < 0x6A09E) { /* u ~< sqrt(2) */ + SET_HIGH_WORD(u, hu | 0x3FF00000); /* normalize u */ + } else { + k += 1; + SET_HIGH_WORD(u, hu | 0x3FE00000); /* normalize u/2 */ + hu = (0x00100000 - hu) >> 2; + } + f = u - 1.0; + } + hfsq = 0.5 * f * f; + if (hu == 0) { /* |f| < 2**-20 */ + if (f == zero) { + if (k == 0) { + return zero; + } else { + c += k * ln2_lo; + return k * ln2_hi + c; + } + } + R = hfsq * (1.0 - 0.66666666666666666 * f); + if (k == 0) + return f - R; + else + return k * ln2_hi - ((R - (k * ln2_lo + c)) - f); + } + s = f / (2.0 + f); + z = s * s; + R = z * (Lp1 + + z * (Lp2 + z * (Lp3 + z * (Lp4 + z * (Lp5 + z * (Lp6 + z * Lp7)))))); + if (k == 0) + return f - (hfsq - s * (hfsq + R)); + else + return k * ln2_hi - ((hfsq - (s * (hfsq + R) + (k * ln2_lo + c))) - f); +} + +/* + * k_log1p(f): + * Return log(1+f) - f for 1+f in ~[sqrt(2)/2, sqrt(2)]. + * + * The following describes the overall strategy for computing + * logarithms in base e. The argument reduction and adding the final + * term of the polynomial are done by the caller for increased accuracy + * when different bases are used. + * + * Method : + * 1. Argument Reduction: find k and f such that + * x = 2^k * (1+f), + * where sqrt(2)/2 < 1+f < sqrt(2) . + * + * 2. Approximation of log(1+f). + * Let s = f/(2+f) ; based on log(1+f) = log(1+s) - log(1-s) + * = 2s + 2/3 s**3 + 2/5 s**5 + ....., + * = 2s + s*R + * We use a special Reme algorithm on [0,0.1716] to generate + * a polynomial of degree 14 to approximate R The maximum error + * of this polynomial approximation is bounded by 2**-58.45. In + * other words, + * 2 4 6 8 10 12 14 + * R(z) ~ Lg1*s +Lg2*s +Lg3*s +Lg4*s +Lg5*s +Lg6*s +Lg7*s + * (the values of Lg1 to Lg7 are listed in the program) + * and + * | 2 14 | -58.45 + * | Lg1*s +...+Lg7*s - R(z) | <= 2 + * | | + * Note that 2s = f - s*f = f - hfsq + s*hfsq, where hfsq = f*f/2. + * In order to guarantee error in log below 1ulp, we compute log + * by + * log(1+f) = f - s*(f - R) (if f is not too large) + * log(1+f) = f - (hfsq - s*(hfsq+R)). (better accuracy) + * + * 3. Finally, log(x) = k*ln2 + log(1+f). + * = k*ln2_hi+(f-(hfsq-(s*(hfsq+R)+k*ln2_lo))) + * Here ln2 is split into two floating point number: + * ln2_hi + ln2_lo, + * where n*ln2_hi is always exact for |n| < 2000. + * + * Special cases: + * log(x) is NaN with signal if x < 0 (including -INF) ; + * log(+INF) is +INF; log(0) is -INF with signal; + * log(NaN) is that NaN with no signal. + * + * Accuracy: + * according to an error analysis, the error is always less than + * 1 ulp (unit in the last place). + * + * Constants: + * The hexadecimal values are the intended ones for the following + * constants. The decimal values may be used, provided that the + * compiler will convert from decimal to binary accurately enough + * to produce the hexadecimal values shown. + */ + +static const double Lg1 = 6.666666666666735130e-01, /* 3FE55555 55555593 */ + Lg2 = 3.999999999940941908e-01, /* 3FD99999 9997FA04 */ + Lg3 = 2.857142874366239149e-01, /* 3FD24924 94229359 */ + Lg4 = 2.222219843214978396e-01, /* 3FCC71C5 1D8E78AF */ + Lg5 = 1.818357216161805012e-01, /* 3FC74664 96CB03DE */ + Lg6 = 1.531383769920937332e-01, /* 3FC39A09 D078C69F */ + Lg7 = 1.479819860511658591e-01; /* 3FC2F112 DF3E5244 */ + +/* + * We always inline k_log1p(), since doing so produces a + * substantial performance improvement (~40% on amd64). + */ +static inline double k_log1p(double f) { + double hfsq, s, z, R, w, t1, t2; + + s = f / (2.0 + f); + z = s * s; + w = z * z; + t1 = w * (Lg2 + w * (Lg4 + w * Lg6)); + t2 = z * (Lg1 + w * (Lg3 + w * (Lg5 + w * Lg7))); + R = t2 + t1; + hfsq = 0.5 * f * f; + return s * (hfsq + R); +} + +/* + * Return the base 2 logarithm of x. See e_log.c and k_log.h for most + * comments. + * + * This reduces x to {k, 1+f} exactly as in e_log.c, then calls the kernel, + * then does the combining and scaling steps + * log2(x) = (f - 0.5*f*f + k_log1p(f)) / ln2 + k + * in not-quite-routine extra precision. + */ +double scr_ieee754_log2(double x) { + static const double + two54 = 1.80143985094819840000e+16, /* 0x43500000, 0x00000000 */ + ivln2hi = 1.44269504072144627571e+00, /* 0x3FF71547, 0x65200000 */ + ivln2lo = 1.67517131648865118353e-10; /* 0x3DE705FC, 0x2EEFA200 */ + + double f, hfsq, hi, lo, r, val_hi, val_lo, w, y; + int32_t i, k, hx; + uint32_t lx; + + EXTRACT_WORDS(hx, lx, x); + + k = 0; + if (hx < 0x00100000) { /* x < 2**-1022 */ + if (((hx & 0x7FFFFFFF) | lx) == 0) { + return -__builtin_inf(); /* log(+-0)=-inf */ + } + if (hx < 0) { + return __builtin_nans(""); /* log(-#) = NaN */ + } + k -= 54; + x *= two54; /* subnormal number, scale up x */ + GET_HIGH_WORD(hx, x); + } + if (hx >= 0x7FF00000) return x + x; + if (hx == 0x3FF00000 && lx == 0) return 0.0; /* log(1) = +0 */ + k += (hx >> 20) - 1023; + hx &= 0x000FFFFF; + i = (hx + 0x95F64) & 0x100000; + SET_HIGH_WORD(x, hx | (i ^ 0x3FF00000)); /* normalize x or x/2 */ + k += (i >> 20); + y = (double)(k); + f = x - 1.0; + hfsq = 0.5 * f * f; + r = k_log1p(f); + + /* + * f-hfsq must (for args near 1) be evaluated in extra precision + * to avoid a large cancellation when x is near sqrt(2) or 1/sqrt(2). + * This is fairly efficient since f-hfsq only depends on f, so can + * be evaluated in parallel with R. Not combining hfsq with R also + * keeps R small (though not as small as a true `lo' term would be), + * so that extra precision is not needed for terms involving R. + * + * Compiler bugs involving extra precision used to break Dekker's + * theorem for spitting f-hfsq as hi+lo, unless double_t was used + * or the multi-precision calculations were avoided when double_t + * has extra precision. These problems are now automatically + * avoided as a side effect of the optimization of combining the + * Dekker splitting step with the clear-low-bits step. + * + * y must (for args near sqrt(2) and 1/sqrt(2)) be added in extra + * precision to avoid a very large cancellation when x is very near + * these values. Unlike the above cancellations, this problem is + * specific to base 2. It is strange that adding +-1 is so much + * harder than adding +-ln2 or +-log10_2. + * + * This uses Dekker's theorem to normalize y+val_hi, so the + * compiler bugs are back in some configurations, sigh. And I + * don't want to used double_t to avoid them, since that gives a + * pessimization and the support for avoiding the pessimization + * is not yet available. + * + * The multi-precision calculations for the multiplications are + * routine. + */ + hi = f - hfsq; + SET_LOW_WORD(hi, 0); + lo = (f - hi) - hfsq + r; + val_hi = hi * ivln2hi; + val_lo = (lo + hi) * ivln2lo + lo * ivln2hi; + + /* spadd(val_hi, val_lo, y), except for not using double_t: */ + w = y + val_hi; + val_lo += (y - w) + val_hi; + val_hi = w; + + return val_lo + val_hi; +} + +/* + * Return the base 10 logarithm of x + * + * Method : + * Let log10_2hi = leading 40 bits of log10(2) and + * log10_2lo = log10(2) - log10_2hi, + * ivln10 = 1/log(10) rounded. + * Then + * n = ilogb(x), + * if(n<0) n = n+1; + * x = scalbn(x,-n); + * log10(x) := n*log10_2hi + (n*log10_2lo + ivln10*log(x)) + * + * Note 1: + * To guarantee log10(10**n)=n, where 10**n is normal, the rounding + * mode must set to Round-to-Nearest. + * Note 2: + * [1/log(10)] rounded to 53 bits has error .198 ulps; + * log10 is monotonic at all binary break points. + * + * Special cases: + * log10(x) is NaN if x < 0; + * log10(+INF) is +INF; log10(0) is -INF; + * log10(NaN) is that NaN; + * log10(10**N) = N for N=0,1,...,22. + */ +double scr_ieee754_log10(double x) { + static const double + two54 = 1.80143985094819840000e+16, /* 0x43500000, 0x00000000 */ + ivln10 = 4.34294481903251816668e-01, + log10_2hi = 3.01029995663611771306e-01, /* 0x3FD34413, 0x509F6000 */ + log10_2lo = 3.69423907715893078616e-13; /* 0x3D59FEF3, 0x11F12B36 */ + + double y; + int32_t i, k, hx; + uint32_t lx; + + EXTRACT_WORDS(hx, lx, x); + + k = 0; + if (hx < 0x00100000) { /* x < 2**-1022 */ + if (((hx & 0x7FFFFFFF) | lx) == 0) { + return -__builtin_inf(); /* log(+-0)=-inf */ + } + if (hx < 0) { + return __builtin_nan(""); /* log(-#) = NaN */ + } + k -= 54; + x *= two54; /* subnormal number, scale up x */ + GET_HIGH_WORD(hx, x); + GET_LOW_WORD(lx, x); + } + if (hx >= 0x7FF00000) return x + x; + if (hx == 0x3FF00000 && lx == 0) return 0.0; /* log(1) = +0 */ + k += (hx >> 20) - 1023; + + i = (k & 0x80000000) >> 31; + hx = (hx & 0x000FFFFF) | ((0x3FF - i) << 20); + y = k + i; + SET_HIGH_WORD(x, hx); + SET_LOW_WORD(x, lx); + + double z = y * log10_2lo + ivln10 * scr_ieee754_log(x); + return z + y * log10_2hi; +} + +/* expm1(x) + * Returns exp(x)-1, the exponential of x minus 1. + * + * Method + * 1. Argument reduction: + * Given x, find r and integer k such that + * + * x = k*ln2 + r, |r| <= 0.5*ln2 ~ 0.34658 + * + * Here a correction term c will be computed to compensate + * the error in r when rounded to a floating-point number. + * + * 2. Approximating expm1(r) by a special rational function on + * the interval [0,0.34658]: + * Since + * r*(exp(r)+1)/(exp(r)-1) = 2+ r^2/6 - r^4/360 + ... + * we define R1(r*r) by + * r*(exp(r)+1)/(exp(r)-1) = 2+ r^2/6 * R1(r*r) + * That is, + * R1(r**2) = 6/r *((exp(r)+1)/(exp(r)-1) - 2/r) + * = 6/r * ( 1 + 2.0*(1/(exp(r)-1) - 1/r)) + * = 1 - r^2/60 + r^4/2520 - r^6/100800 + ... + * We use a special Reme algorithm on [0,0.347] to generate + * a polynomial of degree 5 in r*r to approximate R1. The + * maximum error of this polynomial approximation is bounded + * by 2**-61. In other words, + * R1(z) ~ 1.0 + Q1*z + Q2*z**2 + Q3*z**3 + Q4*z**4 + Q5*z**5 + * where Q1 = -1.6666666666666567384E-2, + * Q2 = 3.9682539681370365873E-4, + * Q3 = -9.9206344733435987357E-6, + * Q4 = 2.5051361420808517002E-7, + * Q5 = -6.2843505682382617102E-9; + * z = r*r, + * with error bounded by + * | 5 | -61 + * | 1.0+Q1*z+...+Q5*z - R1(z) | <= 2 + * | | + * + * expm1(r) = exp(r)-1 is then computed by the following + * specific way which minimize the accumulation rounding error: + * 2 3 + * r r [ 3 - (R1 + R1*r/2) ] + * expm1(r) = r + --- + --- * [--------------------] + * 2 2 [ 6 - r*(3 - R1*r/2) ] + * + * To compensate the error in the argument reduction, we use + * expm1(r+c) = expm1(r) + c + expm1(r)*c + * ~ expm1(r) + c + r*c + * Thus c+r*c will be added in as the correction terms for + * expm1(r+c). Now rearrange the term to avoid optimization + * screw up: + * ( 2 2 ) + * ({ ( r [ R1 - (3 - R1*r/2) ] ) } r ) + * expm1(r+c)~r - ({r*(--- * [--------------------]-c)-c} - --- ) + * ({ ( 2 [ 6 - r*(3 - R1*r/2) ] ) } 2 ) + * ( ) + * + * = r - E + * 3. Scale back to obtain expm1(x): + * From step 1, we have + * expm1(x) = either 2^k*[expm1(r)+1] - 1 + * = or 2^k*[expm1(r) + (1-2^-k)] + * 4. Implementation notes: + * (A). To save one multiplication, we scale the coefficient Qi + * to Qi*2^i, and replace z by (x^2)/2. + * (B). To achieve maximum accuracy, we compute expm1(x) by + * (i) if x < -56*ln2, return -1.0, (raise inexact if x!=inf) + * (ii) if k=0, return r-E + * (iii) if k=-1, return 0.5*(r-E)-0.5 + * (iv) if k=1 if r < -0.25, return 2*((r+0.5)- E) + * else return 1.0+2.0*(r-E); + * (v) if (k<-2||k>56) return 2^k(1-(E-r)) - 1 (or exp(x)-1) + * (vi) if k <= 20, return 2^k((1-2^-k)-(E-r)), else + * (vii) return 2^k(1-((E+2^-k)-r)) + * + * Special cases: + * expm1(INF) is INF, expm1(NaN) is NaN; + * expm1(-INF) is -1, and + * for finite argument, only expm1(0)=0 is exact. + * + * Accuracy: + * according to an error analysis, the error is always less than + * 1 ulp (unit in the last place). + * + * Misc. info. + * For IEEE double + * if x > 7.09782712893383973096e+02 then expm1(x) overflow + * + * Constants: + * The hexadecimal values are the intended ones for the following + * constants. The decimal values may be used, provided that the + * compiler will convert from decimal to binary accurately enough + * to produce the hexadecimal values shown. + */ +double scr_ieee754_expm1(double x) { + static const double + one = 1.0, + tiny = 1.0e-300, + o_threshold = 7.09782712893383973096e+02, /* 0x40862E42, 0xFEFA39EF */ + ln2_hi = 6.93147180369123816490e-01, /* 0x3FE62E42, 0xFEE00000 */ + ln2_lo = 1.90821492927058770002e-10, /* 0x3DEA39EF, 0x35793C76 */ + invln2 = 1.44269504088896338700e+00, /* 0x3FF71547, 0x652B82FE */ + /* Scaled Q's: Qn_here = 2**n * Qn_above, for R(2*z) where z = hxs = + x*x/2: */ + Q1 = -3.33333333333331316428e-02, /* BFA11111 111110F4 */ + Q2 = 1.58730158725481460165e-03, /* 3F5A01A0 19FE5585 */ + Q3 = -7.93650757867487942473e-05, /* BF14CE19 9EAADBB7 */ + Q4 = 4.00821782732936239552e-06, /* 3ED0CFCA 86E65239 */ + Q5 = -2.01099218183624371326e-07; /* BE8AFDB7 6E09C32D */ + + static volatile double huge = 1.0e+300; + + double y, hi, lo, c, t, e, hxs, hfx, r1, twopk; + int32_t k, xsb; + uint32_t hx; + + GET_HIGH_WORD(hx, x); + xsb = hx & 0x80000000; /* sign bit of x */ + hx &= 0x7FFFFFFF; /* high word of |x| */ + + /* filter out huge and non-finite argument */ + if (hx >= 0x4043687A) { /* if |x|>=56*ln2 */ + if (hx >= 0x40862E42) { /* if |x|>=709.78... */ + if (hx >= 0x7FF00000) { + uint32_t low; + GET_LOW_WORD(low, x); + if (((hx & 0xFFFFF) | low) != 0) + return x + x; /* NaN */ + else + return (xsb == 0) ? x : -1.0; /* exp(+-inf)={inf,-1} */ + } + if (x > o_threshold) return huge * huge; /* overflow */ + } + if (xsb != 0) { /* x < -56*ln2, return -1.0 with inexact */ + if (x + tiny < 0.0) /* raise inexact */ + return tiny - one; /* return -1 */ + } + } + + /* argument reduction */ + if (hx > 0x3FD62E42) { /* if |x| > 0.5 ln2 */ + if (hx < 0x3FF0A2B2) { /* and |x| < 1.5 ln2 */ + if (xsb == 0) { + hi = x - ln2_hi; + lo = ln2_lo; + k = 1; + } else { + hi = x + ln2_hi; + lo = -ln2_lo; + k = -1; + } + } else { + k = invln2 * x + ((xsb == 0) ? 0.5 : -0.5); + t = k; + hi = x - t * ln2_hi; /* t*ln2_hi is exact here */ + lo = t * ln2_lo; + } + x = hi - lo; + c = (hi - x) - lo; + } else if (hx < 0x3C900000) { /* when |x|<2**-54, return x */ + t = huge + x; /* return x with inexact flags when x!=0 */ + return x - (t - (huge + x)); + } else { + k = 0; + } + + /* x is now in primary range */ + hfx = 0.5 * x; + hxs = x * hfx; + r1 = one + hxs * (Q1 + hxs * (Q2 + hxs * (Q3 + hxs * (Q4 + hxs * Q5)))); + t = 3.0 - r1 * hfx; + e = hxs * ((r1 - t) / (6.0 - x * t)); + if (k == 0) { + return x - (x * e - hxs); /* c is 0 */ + } else { + INSERT_WORDS( + twopk, + 0x3FF00000 + (int32_t)((uint32_t)(k) << 20), + 0); /* 2^k */ + e = (x * (e - c) - c); + e -= hxs; + if (k == -1) return 0.5 * (x - e) - 0.5; + if (k == 1) { + if (x < -0.25) + return -2.0 * (e - (x + 0.5)); + else + return one + 2.0 * (x - e); + } + if (k <= -2 || k > 56) { /* suffice to return exp(x)-1 */ + y = one - (e - x); + // TODO(mvstanton): is this replacement for the hex float + // sufficient? + // if (k == 1024) y = y*2.0*0x1p1023; + if (k == 1024) + y = y * 2.0 * 8.98846567431158e+307; + else + y = y * twopk; + return y - one; + } + t = one; + if (k < 20) { + SET_HIGH_WORD(t, 0x3FF00000 - (0x200000 >> k)); /* t=1-2^-k */ + y = t - (e - x); + y = y * twopk; + } else { + SET_HIGH_WORD(t, ((0x3FF - k) << 20)); /* 2^-k */ + y = x - (e + t); + y += one; + y = y * twopk; + } + } + return y; +} + +double scr_ieee754_cbrt(double x) { + static const uint32_t + B1 = 715094163, /* B1 = (1023-1023/3-0.03306235651)*2**20 */ + B2 = 696219795; /* B2 = (1023-1023/3-54/3-0.03306235651)*2**20 */ + + /* |1/cbrt(x) - p(x)| < 2**-23.5 (~[-7.93e-8, 7.929e-8]). */ + static const double P0 = 1.87595182427177009643, /* 0x3FFE03E6, 0x0F61E692 */ + P1 = -1.88497979543377169875, /* 0xBFFE28E0, 0x92F02420 */ + P2 = 1.621429720105354466140, /* 0x3FF9F160, 0x4A49D6C2 */ + P3 = -0.758397934778766047437, /* 0xBFE844CB, 0xBEE751D9 */ + P4 = 0.145996192886612446982; /* 0x3FC2B000, 0xD4E4EDD7 */ + + int32_t hx; + double r, s, t = 0.0, w; + uint32_t sign; + uint32_t high, low; + + EXTRACT_WORDS(hx, low, x); + sign = hx & 0x80000000; /* sign= sign(x) */ + hx ^= sign; + if (hx >= 0x7FF00000) return (x + x); /* cbrt(NaN,INF) is itself */ + + /* + * Rough cbrt to 5 bits: + * cbrt(2**e*(1+m) ~= 2**(e/3)*(1+(e%3+m)/3) + * where e is integral and >= 0, m is real and in [0, 1), and "/" and + * "%" are integer division and modulus with rounding towards minus + * infinity. The RHS is always >= the LHS and has a maximum relative + * error of about 1 in 16. Adding a bias of -0.03306235651 to the + * (e%3+m)/3 term reduces the error to about 1 in 32. With the IEEE + * floating point representation, for finite positive normal values, + * ordinary integer division of the value in bits magically gives + * almost exactly the RHS of the above provided we first subtract the + * exponent bias (1023 for doubles) and later add it back. We do the + * subtraction virtually to keep e >= 0 so that ordinary integer + * division rounds towards minus infinity; this is also efficient. + */ + if (hx < 0x00100000) { /* zero or subnormal? */ + if ((hx | low) == 0) return (x); /* cbrt(0) is itself */ + SET_HIGH_WORD(t, 0x43500000); /* set t= 2**54 */ + t *= x; + GET_HIGH_WORD(high, t); + INSERT_WORDS(t, sign | ((high & 0x7FFFFFFF) / 3 + B2), 0); + } else { + INSERT_WORDS(t, sign | (hx / 3 + B1), 0); + } + + /* + * New cbrt to 23 bits: + * cbrt(x) = t*cbrt(x/t**3) ~= t*P(t**3/x) + * where P(r) is a polynomial of degree 4 that approximates 1/cbrt(r) + * to within 2**-23.5 when |r - 1| < 1/10. The rough approximation + * has produced t such than |t/cbrt(x) - 1| ~< 1/32, and cubing this + * gives us bounds for r = t**3/x. + * + * Try to optimize for parallel evaluation as in k_tanf.c. + */ + r = (t * t) * (t / x); + t = t * ((P0 + r * (P1 + r * P2)) + ((r * r) * r) * (P3 + r * P4)); + + /* + * Round t away from zero to 23 bits (sloppily except for ensuring that + * the result is larger in magnitude than cbrt(x) but not much more than + * 2 23-bit ulps larger). With rounding towards zero, the error bound + * would be ~5/6 instead of ~4/6. With a maximum error of 2 23-bit ulps + * in the rounded t, the infinite-precision error in the Newton + * approximation barely affects third digit in the final error + * 0.667; the error in the rounded t can be up to about 3 23-bit ulps + * before the final error is larger than 0.667 ulps. + */ + uint64_t bits = scr_fd_bits(t); + bits = (bits + 0x80000000) & 0xFFFFFFFFC0000000ULL; + t = scr_fd_double(bits); + + /* one step Newton iteration to 53 bits with error < 0.667 ulps */ + s = t * t; /* t*t is exact */ + r = x / s; /* error <= 0.5 ulps; |r| < |t| */ + w = t + t; /* t+t is exact */ + r = (r - t) / (w + r); /* r-t is exact; w+r ~= 3*t */ + t = t + t * r; /* error <= 0.5 + 0.5/3 + epsilon */ + + return (t); +} + +/* sin(x) + * Return sine function of x. + * + * kernel function: + * __kernel_sin ... sine function on [-pi/4,pi/4] + * __kernel_cos ... cose function on [-pi/4,pi/4] + * __ieee754_rem_pio2 ... argument reduction routine + * + * Method. + * Let S,C and T denote the sin, cos and tan respectively on + * [-PI/4, +PI/4]. Reduce the argument x to y1+y2 = x-k*pi/2 + * in [-pi/4 , +pi/4], and let n = k mod 4. + * We have + * + * n sin(x) cos(x) tan(x) + * ---------------------------------------------------------- + * 0 S C T + * 1 C -S -1/T + * 2 -S -C T + * 3 -C S -1/T + * ---------------------------------------------------------- + * + * Special cases: + * Let trig be any of sin, cos, or tan. + * trig(+-INF) is NaN, with signals; + * trig(NaN) is that NaN; + * + * Accuracy: + * TRIG(x) returns trig(x) nearly rounded + */ +double scr_ieee754_sin(double x) { + double y[2], z = 0.0; + int32_t n, ix; + + /* High word of x. */ + GET_HIGH_WORD(ix, x); + + /* |x| ~< pi/4 */ + ix &= 0x7FFFFFFF; + if (ix <= 0x3FE921FB) { + return __kernel_sin(x, z, 0); + } else if (ix >= 0x7FF00000) { + /* sin(Inf or NaN) is NaN */ + return x - x; + } else { + /* argument reduction needed */ + n = __ieee754_rem_pio2(x, y); + switch (n & 3) { + case 0: + return __kernel_sin(y[0], y[1], 1); + case 1: + return __kernel_cos(y[0], y[1]); + case 2: + return -__kernel_sin(y[0], y[1], 1); + default: + return -__kernel_cos(y[0], y[1]); + } + } +} + +/* tan(x) + * Return tangent function of x. + * + * kernel function: + * __kernel_tan ... tangent function on [-pi/4,pi/4] + * __ieee754_rem_pio2 ... argument reduction routine + * + * Method. + * Let S,C and T denote the sin, cos and tan respectively on + * [-PI/4, +PI/4]. Reduce the argument x to y1+y2 = x-k*pi/2 + * in [-pi/4 , +pi/4], and let n = k mod 4. + * We have + * + * n sin(x) cos(x) tan(x) + * ---------------------------------------------------------- + * 0 S C T + * 1 C -S -1/T + * 2 -S -C T + * 3 -C S -1/T + * ---------------------------------------------------------- + * + * Special cases: + * Let trig be any of sin, cos, or tan. + * trig(+-INF) is NaN, with signals; + * trig(NaN) is that NaN; + * + * Accuracy: + * TRIG(x) returns trig(x) nearly rounded + */ +double scr_ieee754_tan(double x) { + double y[2], z = 0.0; + int32_t n, ix; + + /* High word of x. */ + GET_HIGH_WORD(ix, x); + + /* |x| ~< pi/4 */ + ix &= 0x7FFFFFFF; + if (ix <= 0x3FE921FB) { + return __kernel_tan(x, z, 1); + } else if (ix >= 0x7FF00000) { + /* tan(Inf or NaN) is NaN */ + return x - x; /* NaN */ + } else { + /* argument reduction needed */ + n = __ieee754_rem_pio2(x, y); + /* 1 -> n even, -1 -> n odd */ + return __kernel_tan(y[0], y[1], 1 - ((n & 1) << 1)); + } +} + +/* + * ES6 draft 09-27-13, section 20.2.2.12. + * Math.cosh + * Method : + * mathematically cosh(x) if defined to be (exp(x)+exp(-x))/2 + * 1. Replace x by |x| (cosh(x) = cosh(-x)). + * 2. + * [ exp(x) - 1 ]^2 + * 0 <= x <= ln2/2 : cosh(x) := 1 + ------------------- + * 2*exp(x) + * + * exp(x) + 1/exp(x) + * ln2/2 <= x <= 22 : cosh(x) := ------------------- + * 2 + * 22 <= x <= lnovft : cosh(x) := exp(x)/2 + * lnovft <= x <= ln2ovft: cosh(x) := exp(x/2)/2 * exp(x/2) + * ln2ovft < x : cosh(x) := huge*huge (overflow) + * + * Special cases: + * cosh(x) is |x| if x is +INF, -INF, or NaN. + * only cosh(0)=1 is exact for finite x. + */ +double scr_ieee754_cosh(double x) { + static const double KCOSH_OVERFLOW = 710.4758600739439; + static const double one = 1.0, half = 0.5; + static volatile double huge = 1.0e+300; + + int32_t ix; + + /* High word of |x|. */ + GET_HIGH_WORD(ix, x); + ix &= 0x7FFFFFFF; + + // |x| in [0,0.5*log2], return 1+expm1(|x|)^2/(2*exp(|x|)) + if (ix < 0x3FD62E43) { + double t = scr_ieee754_expm1(__builtin_fabs(x)); + double w = one + t; + // For |x| < 2^-55, cosh(x) = 1 + if (ix < 0x3C800000) return w; + return one + (t * t) / (w + w); + } + + // |x| in [0.5*log2, 22], return (exp(|x|)+1/exp(|x|)/2 + if (ix < 0x40360000) { + double t = scr_ieee754_exp(__builtin_fabs(x)); + return half * t + half / t; + } + + // |x| in [22, log(maxdouble)], return half*exp(|x|) + if (ix < 0x40862E42) return half * scr_ieee754_exp(__builtin_fabs(x)); + + // |x| in [log(maxdouble), overflowthreshold] + if (__builtin_fabs(x) <= KCOSH_OVERFLOW) { + double w = scr_ieee754_exp(half * __builtin_fabs(x)); + double t = half * w; + return t * w; + } + + /* x is INF or NaN */ + if (ix >= 0x7FF00000) return x * x; + + // |x| > overflowthreshold. + return huge * huge; +} + +/* + * ES6 draft 09-27-13, section 20.2.2.30. + * Math.sinh + * Method : + * mathematically sinh(x) if defined to be (exp(x)-exp(-x))/2 + * 1. Replace x by |x| (sinh(-x) = -sinh(x)). + * 2. + * E + E/(E+1) + * 0 <= x <= 22 : sinh(x) := --------------, E=expm1(x) + * 2 + * + * 22 <= x <= lnovft : sinh(x) := exp(x)/2 + * lnovft <= x <= ln2ovft: sinh(x) := exp(x/2)/2 * exp(x/2) + * ln2ovft < x : sinh(x) := x*shuge (overflow) + * + * Special cases: + * sinh(x) is |x| if x is +Infinity, -Infinity, or NaN. + * only sinh(0)=0 is exact for finite x. + */ +double scr_ieee754_sinh(double x) { + static const double KSINH_OVERFLOW = 710.4758600739439, + TWO_M28 = + 3.725290298461914e-9, // 2^-28, empty lower half + LOG_MAXD = 709.7822265625; // 0x40862E42 00000000, empty lower half + static const double shuge = 1.0e307; + + double h = (x < 0) ? -0.5 : 0.5; + // |x| in [0, 22]. return sign(x)*0.5*(E+E/(E+1)) + double ax = __builtin_fabs(x); + if (ax < 22) { + // For |x| < 2^-28, sinh(x) = x + if (ax < TWO_M28) return x; + double t = scr_ieee754_expm1(ax); + if (ax < 1) { + return h * (2 * t - t * t / (t + 1)); + } + return h * (t + t / (t + 1)); + } + // |x| in [22, log(maxdouble)], return 0.5 * exp(|x|) + if (ax < LOG_MAXD) return h * scr_ieee754_exp(ax); + // |x| in [log(maxdouble), overflowthreshold] + // overflowthreshold = 710.4758600739426 + if (ax <= KSINH_OVERFLOW) { + double w = scr_ieee754_exp(0.5 * ax); + double t = h * w; + return t * w; + } + // |x| > overflowthreshold or is NaN. + // Return Infinity of the appropriate sign or NaN. + return x * shuge; +} + +/* Tanh(x) + * Return the Hyperbolic Tangent of x + * + * Method : + * x -x + * e - e + * 0. tanh(x) is defined to be ----------- + * x -x + * e + e + * 1. reduce x to non-negative by tanh(-x) = -tanh(x). + * 2. 0 <= x < 2**-28 : tanh(x) := x with inexact if x != 0 + * -t + * 2**-28 <= x < 1 : tanh(x) := -----; t = expm1(-2x) + * t + 2 + * 2 + * 1 <= x < 22 : tanh(x) := 1 - -----; t = expm1(2x) + * t + 2 + * 22 <= x <= INF : tanh(x) := 1. + * + * Special cases: + * tanh(NaN) is NaN; + * only tanh(0)=0 is exact for finite argument. + */ +double scr_ieee754_tanh(double x) { + static const volatile double tiny = 1.0e-300; + static const double one = 1.0, two = 2.0, huge = 1.0e300; + double t, z; + int32_t jx, ix; + + GET_HIGH_WORD(jx, x); + ix = jx & 0x7FFFFFFF; + + /* x is INF or NaN */ + if (ix >= 0x7FF00000) { + if (jx >= 0) + return one / x + one; /* tanh(+-inf)=+-1 */ + else + return one / x - one; /* tanh(NaN) = NaN */ + } + + /* |x| < 22 */ + if (ix < 0x40360000) { /* |x|<22 */ + if (ix < 0x3E300000) { /* |x|<2**-28 */ + if (huge + x > one) return x; /* tanh(tiny) = tiny with inexact */ + } + if (ix >= 0x3FF00000) { /* |x|>=1 */ + t = scr_ieee754_expm1(two * __builtin_fabs(x)); + z = one - two / (t + two); + } else { + t = scr_ieee754_expm1(-two * __builtin_fabs(x)); + z = -t / (t + two); + } + /* |x| >= 22, return +-1 */ + } else { + z = one - tiny; /* raise inexact flag */ + } + return (jx >= 0) ? z : -z; +} + +#undef EXTRACT_WORDS +#undef GET_HIGH_WORD +#undef GET_LOW_WORD +#undef INSERT_WORDS +#undef SET_HIGH_WORD +#undef SET_LOW_WORD + diff --git a/packages/runtime/src/scr_ieee754.h b/packages/runtime/src/scr_ieee754.h new file mode 100644 index 000000000..7dddf9889 --- /dev/null +++ b/packages/runtime/src/scr_ieee754.h @@ -0,0 +1,26 @@ +#ifndef SCR_IEEE754_H +#define SCR_IEEE754_H + +/* Math's transcendental functions as V8 computes them (scr_ieee754.c). */ +double scr_ieee754_acos(double x); +double scr_ieee754_acosh(double x); +double scr_ieee754_asin(double x); +double scr_ieee754_asinh(double x); +double scr_ieee754_atan(double x); +double scr_ieee754_atan2(double y, double x); +double scr_ieee754_atanh(double x); +double scr_ieee754_cbrt(double x); +double scr_ieee754_cos(double x); +double scr_ieee754_cosh(double x); +double scr_ieee754_exp(double x); +double scr_ieee754_expm1(double x); +double scr_ieee754_log(double x); +double scr_ieee754_log10(double x); +double scr_ieee754_log1p(double x); +double scr_ieee754_log2(double x); +double scr_ieee754_sin(double x); +double scr_ieee754_sinh(double x); +double scr_ieee754_tan(double x); +double scr_ieee754_tanh(double x); + +#endif diff --git a/packages/runtime/src/scr_inspect.c b/packages/runtime/src/scr_inspect.c index 1dce8d3c2..1de583af5 100644 --- a/packages/runtime/src/scr_inspect.c +++ b/packages/runtime/src/scr_inspect.c @@ -152,6 +152,154 @@ static size_t insp_width(const char *data, size_t len) { return width; } +/* ── colors (stylizeWithColor) ───────────────────────────────────────── */ + +/* ctx.colors for the render in progress: the checked-dynamic walk wraps + * each styled leaf in inspect.colors' SGR codes for its inspect.styles + * style, and the layout measures entries without the codes. Static + * renderings never run with it set. */ +static SCR_TL bool g_colors; + +typedef enum { + INSP_SPECIAL, + INSP_NUMBER, + INSP_BIGINT, + INSP_BOOLEAN, + INSP_UNDEFINED, + INSP_NULL, + INSP_STRING, + INSP_SYMBOL, + INSP_DATE, +} InspStyle; + +/* inspect.styles mapped through inspect.colors: [open, close]. */ +static const unsigned char insp_style_codes[][2] = { + {36, 39}, /* special: cyan */ + {33, 39}, /* number: yellow */ + {33, 39}, /* bigint: yellow */ + {33, 39}, /* boolean: yellow */ + {90, 39}, /* undefined: grey */ + {1, 22}, /* null: bold */ + {32, 39}, /* string: green */ + {32, 39}, /* symbol: green */ + {35, 39}, /* date: magenta */ +}; + +static void insp_style_open(InspBuf *b, InspStyle style) { + char code[8]; + int n = snprintf(code, sizeof code, "\x1b[%um", insp_style_codes[style][0]); + ib_bytes(b, code, (size_t)n); +} + +static void insp_style_close(InspBuf *b, InspStyle style) { + char code[8]; + int n = snprintf(code, sizeof code, "\x1b[%um", insp_style_codes[style][1]); + ib_bytes(b, code, (size_t)n); +} + +/* ctx.stylize(text, style): +1; moves `text` in. */ +static ScrStr *insp_styled(ScrStr *text, InspStyle style) { + if (!g_colors) return text; + InspBuf b = {0}; + insp_style_open(&b, style); + ib_str(&b, text); + insp_style_close(&b, style); + scr_str_release(text); + return ib_take(&b); +} + +static ScrStr *insp_styled_lit(const char *text, size_t len, InspStyle style) { + return insp_styled(scr_str_new(text, len), style); +} + +/* The SC2020 refusal for colored renderings of values whose styles this + * runtime does not produce; answers an empty string. */ +static ScrStr *insp_colors_fence(const char *what) { + InspBuf b = {0}; + ib_cstr(&b, "colored inspect output of "); + ib_cstr(&b, what); + ib_cstr(&b, " has no lowering"); + ScrStr *message = ib_take(&b); + scr_throw_error_msg_code(SCR_ERR_ERROR, message->data, message->len, "SC2020"); + scr_str_release(message); + return scr_str_new("", 0); +} + +static ScrStr *insp_colors_fence_values(const char *cls) { + InspBuf b = {0}; + ib_cstr(&b, cls); + ib_cstr(&b, " values"); + ScrStr *what = ib_take(&b); + ScrStr *out = insp_colors_fence(what->data); + scr_str_release(what); + return out; +} + +/* The SGR sequence Node's removeColors strips (\x1b[<1 or 2 digits>m) at + * data[i]: its length, or 0. */ +static size_t insp_sgr_at(const char *data, size_t len, size_t i) { + if (data[i] != '\x1b' || i + 3 >= len || data[i + 1] != '[') return 0; + size_t j = i + 2, digits = 0; + while (j < len && digits < 2 && data[j] >= '0' && data[j] <= '9') j++, digits++; + return digits > 0 && j < len && data[j] == 'm' ? j + 1 - i : 0; +} + +/* A control sequence stripVTControlCharacters removes at data[i] (CSI + * sequences after ESC [ or C1 CSI, and OSC sequences ended by BEL): its + * length, or 0. */ +static size_t insp_vt_at(const char *data, size_t len, size_t i) { + const unsigned char *p = (const unsigned char *)data; + size_t j; + if (p[i] == 0x1b && i + 1 < len && p[i + 1] == '[') j = i + 2; + else if (p[i] == 0xc2 && i + 1 < len && p[i + 1] == 0x9b) j = i + 2; + else if (p[i] == 0x1b && i + 1 < len && p[i + 1] == ']') { + for (j = i + 2; j < len; j++) + if (p[j] == 0x07) return j + 1 - i; + return 0; + } else return 0; + while (j < len && ((p[j] >= '0' && p[j] <= '9') || p[j] == ';' || p[j] == '?')) j++; + if (j >= len) return 0; + unsigned char c = p[j]; + bool final = (c >= '0' && c <= '9') || (c >= 'A' && c <= 'P') || (c >= 'R' && c <= 'T') || + c == 'Z' || c == 'c' || (c >= 'f' && c <= 'n') || c == 'q' || c == 'r' || + c == 't' || c == 'y' || c == '=' || c == '>' || c == '<' || c == '~'; + return final ? j + 1 - i : 0; +} + +/* An entry's display width for the grid (getStringWidth(entry, + * ctx.colors)) and its UTF-16 length for the break-length test (Node's + * removeColors under ctx.colors). */ +static size_t insp_entry_width(const ScrStr *s) { + if (!g_colors) return insp_width(s->data, s->len); + size_t width = 0; + for (size_t i = 0; i < s->len;) { + size_t skip = insp_vt_at(s->data, s->len, i); + if (skip) { + i += skip; + continue; + } + uint32_t cp = insp_cp_at(s->data, s->len, &i); + if (insp_full_width(cp)) width += 2; + else if (!insp_zero_width(cp)) width += 1; + } + return width; +} + +static size_t insp_entry_len(const ScrStr *s) { + if (!g_colors) return insp_utf16_len(s->data, s->len); + size_t units = 0; + for (size_t i = 0; i < s->len;) { + size_t skip = insp_sgr_at(s->data, s->len, i); + if (skip) { + i += skip; + continue; + } + uint32_t cp = insp_cp_at(s->data, s->len, &i); + units += cp >= 0x10000 ? 2 : 1; + } + return units; +} + /* ── number rendering ────────────────────────────────────────────────── */ /* formatNumber: JS ToString EXCEPT -0 renders as '-0'. */ @@ -245,12 +393,12 @@ static SCR_TL size_t g_frames_cap; static SCR_TL size_t g_indent; /* ctx.indentationLvl */ static SCR_TL bool g_no_break; /* breakLength: Infinity (console.table cells) */ static SCR_TL double g_break_length = 80; /* ctx.breakLength */ -static SCR_TL double g_compact = 3; /* ctx.compact: a number, 0 for false */ +static SCR_TL double g_compact = 3; /* ctx.compact: a number, -1 for false */ static SCR_TL bool g_sorted; /* ctx.sorted === true */ static SCR_TL double g_cur_depth; /* ctx.currentDepth (last-entered composite) */ /* util.inspect's breakLength, compact and sorted for the renders that - * follow (compact false is 0); the call site restores 80, 3 and false + * follow (compact false is -1); the call site restores 80, 3 and false * after its render. */ void scr_insp_layout(double break_length, double compact, bool sorted) { g_break_length = break_length; @@ -324,6 +472,9 @@ typedef struct { static SCR_TL InspSeenEnt *g_seen; static SCR_TL size_t g_nseen; +/* util.format's %o (scr_insp_dyn_hidden): this render shows hidden own + * properties, like util.inspect's showHidden. */ +static SCR_TL bool g_show_hidden; static SCR_TL size_t g_seen_cap; /* Detection-numbered circular targets (persist across the call). */ static SCR_TL const void *g_circ[64]; @@ -369,7 +520,7 @@ void scr_insp_seen_push(const void *v) { ScrStr *scr_insp_circular(double id) { char buf[32]; int n = snprintf(buf, sizeof buf, "[Circular *%.0f]", id); - return scr_str_new(buf, (size_t)n); + return insp_styled_lit(buf, (size_t)n, INSP_SPECIAL); } /* Pops the value's seen entry after its frame closed. scr_insp_end already @@ -403,7 +554,7 @@ static void insp_group(InspFrame *f, bool trailing_more) { size_t *data_len = malloc((output_len ? output_len : 1) * sizeof(size_t)); if (!data_len) insp_oom(); for (size_t i = 0; i < output_len; i++) { - size_t w = insp_width(f->items[i]->data, f->items[i]->len); + size_t w = insp_entry_width(f->items[i]); data_len[i] = w; total_length += (double)w + 2; if ((double)w > max_length) max_length = (double)w; @@ -494,7 +645,7 @@ static bool insp_below_break_length(InspFrame *f, size_t start, const ScrStr *ba size_t total = f->len + start; if ((double)(total + f->len) > g_break_length) return false; for (size_t i = 0; i < f->len; i++) { - total += insp_utf16_len(f->items[i]->data, f->items[i]->len); + total += insp_entry_len(f->items[i]); if ((double)total > g_break_length) return false; } return base->len == 0 || !insp_contains_byte(base->data, base->len, '\n'); @@ -520,12 +671,16 @@ ScrStr *scr_insp_end(ScrStr *base, ScrStr *b0, ScrStr *b1, double recurse, bool int ref_id = f->seen ? insp_circ_id(f->seen) : 0; if (ref_id) { char buf[32]; - int n = snprintf(buf, sizeof buf, base->len ? " " : "", ref_id); - size_t len = (size_t)n + base->len; - ref_base = scr_str_alloc_raw(len, len); - memcpy(ref_base->data, buf, (size_t)n); - memcpy(ref_base->data + n, base->data, base->len); - ref_base->data[len] = '\0'; + int n = snprintf(buf, sizeof buf, "", ref_id); + InspBuf rb = {0}; + if (g_colors) insp_style_open(&rb, INSP_SPECIAL); + ib_bytes(&rb, buf, (size_t)n); + if (g_colors) insp_style_close(&rb, INSP_SPECIAL); + if (base->len) { + ib_char(&rb, ' '); + ib_str(&rb, base); + } + ref_base = ib_take(&rb); base = ref_base; } size_t entries = f->len; @@ -622,6 +777,19 @@ void scr_insp_nested_begin(void) { g_ncirc = 0; } +/* nested_end's restore alone: the caller's context returns. */ +void scr_insp_nested_restore(void) { + InspSaved *sv = &g_saved[--g_nsaved]; + free(g_seen); + g_indent = sv->indent; + g_cur_depth = sv->cur_depth; + g_seen = sv->seen; + g_nseen = sv->nseen; + g_seen_cap = sv->seen_cap; + memcpy(g_circ, sv->circ, sizeof g_circ); + g_ncirc = sv->ncirc; +} + ScrStr *scr_insp_nested_end(ScrStr *prefix, ScrStr *inner) { InspSaved *sv = &g_saved[--g_nsaved]; free(g_seen); @@ -643,6 +811,59 @@ ScrStr *scr_insp_nested_end(ScrStr *prefix, ScrStr *inner) { return ib_take(&out); } +/* The inspector's state at the start of a top-level render. A render that + * an inspection refusal ends leaves its compiled frames open: the Error + * propagates out of the emitted helpers before their scr_insp_end and + * scr_insp_nested_end calls. The emitted render takes a mark first and + * unwinds to it on every exit (lower-inspect.ts), so later renders start + * from the state the refused one found. */ +typedef struct { + size_t nframes; + size_t nsaved; + size_t nseen; + size_t ncall; + size_t indent; + double cur_depth; +} InspMark; + +static size_t insp_call_depth(void); /* custom calls open, below */ +static void insp_call_unwind(size_t ncall); /* their options, below */ + +static SCR_TL InspMark *g_marks; +static SCR_TL size_t g_nmarks; +static SCR_TL size_t g_marks_cap; + +void scr_insp_mark(void) { + if (g_nmarks == g_marks_cap) { + g_marks_cap = g_marks_cap ? g_marks_cap * 2 : 4; + g_marks = realloc(g_marks, g_marks_cap * sizeof(InspMark)); + if (!g_marks) insp_oom(); + } + g_marks[g_nmarks++] = (InspMark){g_nframes, g_nsaved, g_nseen, insp_call_depth(), g_indent, g_cur_depth}; +} + +void scr_insp_unwind(void) { + InspMark m = g_marks[--g_nmarks]; + insp_call_unwind(m.ncall); + while (g_nframes > m.nframes) { + InspFrame *f = &g_frames[--g_nframes]; + for (size_t i = 0; i < f->len; i++) scr_str_release(f->items[i]); + free(f->items); + } + while (g_nsaved > m.nsaved) { + InspSaved *sv = &g_saved[--g_nsaved]; + free(g_seen); + g_seen = sv->seen; + g_seen_cap = sv->seen_cap; + memcpy(g_circ, sv->circ, sizeof g_circ); + g_ncirc = sv->ncirc; + } + if (g_nseen > m.nseen) g_nseen = m.nseen; + g_indent = m.indent; + g_cur_depth = m.cur_depth; + g_pending_seen = NULL; +} + /* ── string values (formatPrimitive's string half) ───────────────────── */ /* Byte offset of UTF-16 unit `units` — when the boundary splits an astral @@ -717,13 +938,17 @@ ScrStr *scr_insp_str(ScrStr *s) { ib_cstr(&out, " +\n"); ib_spaces(&out, g_indent + 2); } + if (g_colors) insp_style_open(&out, INSP_STRING); insp_quote_into(&out, v + chunk_start, end - chunk_start); + if (g_colors) insp_style_close(&out, INSP_STRING); chunk_start = end; first = false; } } } else { + if (g_colors) insp_style_open(&out, INSP_STRING); insp_quote_into(&out, v, vlen); + if (g_colors) insp_style_close(&out, INSP_STRING); } if (trailer[0]) ib_cstr(&out, trailer); free(val.data); @@ -743,6 +968,52 @@ ScrStr *scr_insp_regex(ScrRegex *re) { return ib_take(&out); } +/* Named properties of a typed array, Buffer or DataView (its + * ScrBytes.properties table): slot k is the k-th string-keyed entry, then + * the k-th symbol key. */ +static size_t insp_view_property_slots(const ScrDyn *props) { + if (!props) return 0; + return props->v.obj.len + (props->symbol_keys ? props->symbol_keys->v.arr.len : 0); +} + +static ScrStr *insp_accessor_tag(const ScrDynEntry *ent); +static void insp_key_into(InspBuf *b, const char *key, size_t len); + +/* Slot k's `key: value` entry when it is enumerable, else NULL. */ +static ScrStr *insp_view_property(ScrDyn *props, size_t k, double recurse, double depth) { + const ScrDynEntry *ent; + ScrStr *key = NULL; + if (k < props->v.obj.len) { + ent = &props->v.obj.entries[k]; + } else { + ScrDyn *symbol = props->symbol_keys->v.arr.items[k - props->v.obj.len]; + ent = scr_dyn_own_symbol_entry(props, symbol); + if (ent && ent->enumerable) key = scr_insp_dyn(symbol, recurse, depth); + } + if (!ent || !ent->enumerable) return NULL; + ScrStr *val = ent->accessor ? insp_accessor_tag(ent) : scr_insp_dyn(ent->value, recurse, depth); + InspBuf eb = {0}; + if (key) ib_bytes(&eb, key->data, key->len); + else insp_key_into(&eb, ent->key, ent->key_len); + ib_cstr(&eb, ": "); + ib_bytes(&eb, val->data, val->len); + scr_str_release(key); + scr_str_release(val); + return ib_take(&eb); +} + +static size_t insp_view_property_count(ScrDyn *props) { + size_t count = 0; + for (size_t k = 0; k < insp_view_property_slots(props); k++) { + if (k < props->v.obj.len) count += props->v.obj.entries[k].enumerable; + else { + const ScrDynEntry *ent = scr_dyn_own_symbol_entry(props, props->symbol_keys->v.arr.items[k - props->v.obj.len]); + count += ent && ent->enumerable; + } + } + return count; +} + /* Buffer: the custom-inspect hex form ``, capped at * INSPECT_MAX_BYTES (50); renders fully at any depth (custom inspection * runs before the depth check in Node). u8 element kind only (the @@ -765,6 +1036,17 @@ ScrStr *scr_insp_buffer(ScrBytes *b) { remaining > 1 ? "s" : ""); ib_bytes(&out, more, (size_t)n); } + /* Named properties follow as `, key: value`, rendered as the members of + * an object at depth 0 (Buffer's custom inspect). */ + bool first = true; + for (size_t k = 0; k < insp_view_property_slots(b->properties); k++) { + ScrStr *entry = insp_view_property(b->properties, k, 1, 2); + if (!entry) continue; + if (!first || b->len) ib_cstr(&out, ", "); + first = false; + ib_bytes(&out, entry->data, entry->len); + scr_str_release(entry); + } ib_char(&out, '>'); return ib_take(&out); } @@ -848,6 +1130,12 @@ static ScrStr *scr_insp_data_view(const ScrBytes *b, double recurse, double dept entry = ib_take(&eb); scr_insp_entry(entry, false); scr_str_release(entry); + for (size_t k = 0; k < insp_view_property_slots(b->properties); k++) { + entry = insp_view_property(b->properties, k, recurse + 1, depth); + if (!entry) continue; + scr_insp_entry(entry, false); + scr_str_release(entry); + } ScrStr *base = scr_str_new("", 0); ScrStr *b0 = scr_str_new("DataView {", 10); ScrStr *b1 = scr_str_new("}", 1); @@ -1016,7 +1304,9 @@ static void insp_key_into(InspBuf *b, const char *key, size_t len) { if (bare) { ib_bytes(b, key, len); } else { + if (g_colors) insp_style_open(b, INSP_STRING); insp_quote_into(b, key, len); + if (g_colors) insp_style_close(b, INSP_STRING); } } @@ -1207,6 +1497,20 @@ static bool insp_tag_names_class(const char *tag, const char *cls) { memcmp(tag + tag_len - cls_len, cls, cls_len) == 0; } +/* The Symbol.toStringTag a class instance inherits from its class's + * getter (+1), which Node brackets after the class name when it is a + * nonempty string other than the name. An own tag field is listed with + * the fields instead. A getter that throws shows no tag, as in Node. */ +static ScrStr *insp_inherited_tag(const ScrDyn *instance, const ScrStr *name) { + ScrStr *tag = scr_dyn_typed_ref_class_tag(instance); + if (!tag && scr_exc_pending()) scr_exc_clear(); + if (tag && (tag->len == 0 || (tag->len == name->len && memcmp(tag->data, name->data, name->len) == 0))) { + scr_str_release(tag); + return NULL; + } + return tag; +} + /* A native Set or Map held as a checked-dynamic value: `Set(n) { a, b }`, * `Map(n) { k => v }`, at most 100 entries like Node's maxArrayLength. */ /* Node's formatWeakCollection: the entries of a WeakMap or WeakSet are not @@ -1339,7 +1643,7 @@ static ScrStr *scr_insp_dyn_collection(ScrDyn *d, double recurse, double depth, ib_char(&out, '['); ib_cstr(&out, label); ib_char(&out, ']'); - return ib_take(&out); + return insp_styled(ib_take(&out), INSP_SPECIAL); } ScrStr *base = ib_take(&head); scr_insp_begin(recurse + 1); @@ -1384,18 +1688,25 @@ static ScrStr *scr_insp_dyn_collection(ScrDyn *d, double recurse, double depth, return out; } -/* An own enumerable symbol-keyed property of `d` (borrowed), or NULL. */ +/* An own symbol-keyed property of `d` (borrowed) this render shows, or + * NULL: enumerable ones, and under showHidden the non-enumerable ones too. */ static ScrDynEntry *scr_insp_symbol_entry(ScrDyn *d, size_t i) { ScrDynEntry *ent = scr_dyn_own_symbol_entry(d, d->symbol_keys->v.arr.items[i]); - return ent && ent->enumerable ? ent : NULL; + return ent && (ent->enumerable || g_show_hidden) ? ent : NULL; } /* An ordinary object: `Name { key: value }` with its constructor name * `cls` (NULL for a plain object). Node's default view lists the own * enumerable string keys, then the own enumerable symbol keys. */ +/* An own entry this render shows: enumerable ones, and under showHidden + * the non-enumerable ones too (never the encoding's `%` markers). */ +static bool insp_obj_shows(const ScrDynEntry *ent) { + return ent->enumerable || (g_show_hidden && !(ent->key_len && ent->key[0] == '%')); +} + static ScrStr *scr_insp_obj(ScrDyn *d, double recurse, double depth, const char *cls) { size_t shown = 0; - for (size_t i = 0; i < d->v.obj.len; i++) shown += d->v.obj.entries[i].enumerable; + for (size_t i = 0; i < d->v.obj.len; i++) shown += insp_obj_shows(&d->v.obj.entries[i]); size_t nsymbols = d->symbol_keys ? d->symbol_keys->v.arr.len : 0; for (size_t i = 0; i < nsymbols; i++) shown += scr_insp_symbol_entry(d, i) != NULL; if (shown == 0) { @@ -1404,31 +1715,42 @@ static ScrStr *scr_insp_obj(ScrDyn *d, double recurse, double depth, const char : scr_str_new("{}", 2); } if (recurse > depth) { - if (cls) return scr_insp_class_tag(cls, NULL); - return d->null_proto ? scr_str_new("[Object: null prototype]", 24) - : scr_str_new("[Object]", 8); + if (cls) return insp_styled(scr_insp_class_tag(cls, NULL), INSP_SPECIAL); + return d->null_proto ? insp_styled_lit("[Object: null prototype]", 24, INSP_SPECIAL) + : insp_styled_lit("[Object]", 8, INSP_SPECIAL); } scr_insp_begin(recurse + 1); for (size_t i = 0; i < d->v.obj.len; i++) { ScrDynEntry *ent = &d->v.obj.entries[i]; - if (!ent->enumerable) continue; - ScrStr *val = scr_insp_dyn(ent->value, recurse + 1, depth); + if (!insp_obj_shows(ent)) continue; + ScrStr *val = ent->accessor ? insp_accessor_tag(ent) : scr_insp_dyn(ent->value, recurse + 1, depth); InspBuf eb = {0}; - insp_key_into(&eb, ent->key, ent->key_len); + if (ent->enumerable) { + insp_key_into(&eb, ent->key, ent->key_len); + } else { + /* formatProperty's hidden key: the key's rendering in brackets. */ + ib_char(&eb, '['); + insp_key_into(&eb, ent->key, ent->key_len); + ib_char(&eb, ']'); + } ib_cstr(&eb, ": "); ib_bytes(&eb, val->data, val->len); scr_str_release(val); ScrStr *entry = ib_take(&eb); scr_insp_entry(entry, false); scr_str_release(entry); + if (scr_exc_pending()) break; } for (size_t i = 0; i < nsymbols; i++) { ScrDynEntry *ent = scr_insp_symbol_entry(d, i); if (!ent) continue; ScrStr *key = scr_insp_dyn(d->symbol_keys->v.arr.items[i], recurse + 1, depth); - ScrStr *val = scr_insp_dyn(ent->value, recurse + 1, depth); + ScrStr *val = ent->accessor ? insp_accessor_tag(ent) : scr_insp_dyn(ent->value, recurse + 1, depth); InspBuf eb = {0}; + /* A hidden symbol key is bracketed like a hidden string key. */ + if (!ent->enumerable) ib_char(&eb, '['); ib_bytes(&eb, key->data, key->len); + if (!ent->enumerable) ib_char(&eb, ']'); ib_cstr(&eb, ": "); ib_bytes(&eb, val->data, val->len); scr_str_release(key); @@ -1455,7 +1777,10 @@ static ScrStr *scr_insp_obj(ScrDyn *d, double recurse, double depth, const char ScrStr *scr_insp_typed_array(ScrBytes *b, double recurse, double depth) { char prefix[48]; int pn = snprintf(prefix, sizeof prefix, "%s(%zu) [", scr_bytes_elem_name(b->elem), b->len); - if (b->len == 0) { + /* Named properties follow the elements as `key: value` entries. */ + ScrDyn *props = b->properties; + size_t nprops = insp_view_property_count(props); + if (b->len == 0 && nprops == 0) { InspBuf out = {0}; ib_bytes(&out, prefix, (size_t)pn); ib_char(&out, ']'); @@ -1464,12 +1789,12 @@ ScrStr *scr_insp_typed_array(ScrBytes *b, double recurse, double depth) { if (recurse > depth) { char tag[32]; int n = snprintf(tag, sizeof tag, "[%s]", scr_bytes_elem_name(b->elem)); - return scr_str_new(tag, (size_t)n); + return insp_styled_lit(tag, (size_t)n, INSP_SPECIAL); } scr_insp_begin(recurse + 1); size_t shown = b->len < 100 ? b->len : 100; for (size_t i = 0; i < shown; i++) { - ScrStr *s = scr_insp_f64(scr_bytes_get(b, (double)i)); + ScrStr *s = insp_styled(scr_insp_f64(scr_bytes_get(b, (double)i)), INSP_NUMBER); scr_insp_entry(s, true); scr_str_release(s); } @@ -1479,42 +1804,721 @@ ScrStr *scr_insp_typed_array(ScrBytes *b, double recurse, double depth) { scr_insp_entry(m, true); scr_str_release(m); } + size_t entries = shown + (more ? 1 : 0); + for (size_t k = 0; nprops && k < insp_view_property_slots(props); k++) { + ScrStr *entry = insp_view_property(props, k, recurse + 1, depth); + if (!entry) continue; + /* Node's grouping pads by the element at the entry's output position. */ + scr_insp_entry(entry, entries < b->len); + scr_str_release(entry); + entries++; + } ScrStr *base = scr_str_new("", 0); ScrStr *b0 = scr_str_new(prefix, (size_t)pn); ScrStr *b1 = scr_str_new("]", 1); - ScrStr *out = scr_insp_end(base, b0, b1, recurse + 1, true, more); + ScrStr *out = scr_insp_end(base, b0, b1, recurse + 1, true, nprops ? entries > 100 : more); scr_str_release(base); scr_str_release(b0); scr_str_release(b1); return out; } +/* ── custom inspect functions (util.inspect.custom) ────────────────────── + * formatValue's hook: an object whose [util.inspect.custom] member is a + * function (other than util.inspect itself, on an object that is not its + * constructor's prototype) renders as that function's result. It runs + * before the circular and depth checks, with the remaining depth + * (ctx.depth - recurseTimes), the options object getUserOptions builds and + * util.inspect. A string result is re-indented at the current level; the + * object itself renders normally; any other value renders in its place. + * Class instances held as checked-dynamic values have compiled methods + * only, so one whose class defines the method throws SC2020. */ +static SCR_TL bool g_custom_inspect = true; /* ctx.customInspect */ +/* ctx.depth as the options gave it, for the custom functions' depth + * argument and options when the render's depth is unbounded: 0 the number + * g_depth_number (Infinity or NaN), 1 null, 2 undefined. */ +static SCR_TL int g_depth_kind = 1; +static SCR_TL double g_depth_number; +/* The options object that held keys inspect does not read: getUserOptions + * spreads it into the options the custom functions receive (borrowed for + * the render). */ +static SCR_TL const ScrDyn *g_user_options; + +double scr_insp_depth_kind(double kind) { + double previous = g_depth_kind; + g_depth_kind = (int)kind; + g_depth_number = INFINITY; + return previous; +} + +bool scr_insp_custom_inspect(bool enabled) { + bool previous = g_custom_inspect; + g_custom_inspect = enabled; + return previous; +} + +/* The walk's options while a custom inspect function runs. */ +typedef struct { + bool colors; + bool show_hidden; + bool custom_inspect; + bool sorted; + bool no_break; + int depth_kind; + double depth_number; + double break_length; + double compact; + const ScrDyn *user_options; + size_t nsaved; /* the nested-context stack's depth before the call */ + size_t nframes; /* the frame stack's depth before the call */ +} InspCallSaved; + +static SCR_TL InspCallSaved *g_call_saved; +static SCR_TL size_t g_ncall_saved; +static SCR_TL size_t g_call_saved_cap; + +static void insp_call_saved_cleanup(void) { + free(g_call_saved); + g_call_saved = NULL; + g_ncall_saved = g_call_saved_cap = 0; +} + +/* A custom inspect function's call: Node gives each util.inspect and + * util.format call its own ctx, so the calls the function makes start + * with no indentation, no seen objects, no colors and the default layout + * and options. begin answers a token; end(token) restores the walk's + * context as it was at that begin. A call that threw skips its end, and + * an enclosing custom function's end (or the walk's, when the function + * caught the error) discards what the throw left: the contexts of calls + * that never ended and the frames of renders that never finished. */ +double scr_insp_custom_call_begin(void) { + size_t nsaved = g_nsaved; + scr_insp_nested_begin(); + if (g_ncall_saved == g_call_saved_cap) { + if (g_call_saved_cap == 0) scr_atexit(insp_call_saved_cleanup); + g_call_saved_cap = g_call_saved_cap ? g_call_saved_cap * 2 : 4; + g_call_saved = realloc(g_call_saved, g_call_saved_cap * sizeof(InspCallSaved)); + if (!g_call_saved) insp_oom(); + } + double token = (double)g_ncall_saved; + InspCallSaved *sv = &g_call_saved[g_ncall_saved++]; + sv->nsaved = nsaved; + sv->nframes = g_nframes; + sv->colors = g_colors; + sv->show_hidden = g_show_hidden; + sv->custom_inspect = g_custom_inspect; + sv->sorted = g_sorted; + sv->no_break = g_no_break; + sv->depth_kind = g_depth_kind; + sv->depth_number = g_depth_number; + sv->break_length = g_break_length; + sv->compact = g_compact; + sv->user_options = g_user_options; + g_colors = false; + g_show_hidden = false; + g_custom_inspect = true; + g_no_break = false; + g_depth_kind = 1; + g_depth_number = 0; + g_user_options = NULL; + scr_insp_layout(80, 3, false); + return token; +} + +/* Restores the options saved by the custom call at `depth` and drops it + * and every call above it; NULL when there is none. */ +static InspCallSaved *insp_call_options_restore(size_t depth) { + if (depth >= g_ncall_saved) return NULL; + InspCallSaved *sv = &g_call_saved[depth]; + g_ncall_saved = depth; + g_colors = sv->colors; + g_show_hidden = sv->show_hidden; + g_custom_inspect = sv->custom_inspect; + g_no_break = sv->no_break; + g_depth_kind = sv->depth_kind; + g_depth_number = sv->depth_number; + g_user_options = sv->user_options; + scr_insp_layout(sv->break_length, sv->compact, sv->sorted); + return sv; +} + +void scr_insp_custom_call_end(double token) { + InspCallSaved *sv = insp_call_options_restore((size_t)token); + if (!sv) return; /* an enclosing end already restored it */ + while (g_nframes > sv->nframes) { + InspFrame *f = &g_frames[--g_nframes]; + for (size_t i = 0; i < f->len; i++) scr_str_release(f->items[i]); + free(f->items); + } + g_pending_seen = NULL; + while (g_nsaved > sv->nsaved) scr_insp_nested_restore(); +} + +/* scr_insp_unwind's share: the options of custom calls the refused render + * left open. */ +static void insp_call_unwind(size_t ncall) { insp_call_options_restore(ncall); } + +static size_t insp_call_depth(void) { return g_ncall_saved; } + +/* The style inspect.styles maps a styleType to, as [open, close] codes: + * the styles above plus regexp (red) and module (underline). false when + * the type has no style. */ +static bool insp_style_of(const ScrStr *type, unsigned *open, unsigned *close) { + static const struct { + const char *name; + unsigned open, close; + } styles[] = { + {"special", 36, 39}, {"number", 33, 39}, {"bigint", 33, 39}, {"boolean", 33, 39}, + {"undefined", 90, 39}, {"null", 1, 22}, {"string", 32, 39}, {"symbol", 32, 39}, + {"date", 35, 39}, {"regexp", 31, 39}, {"module", 4, 24}, + }; + for (size_t i = 0; i < sizeof styles / sizeof *styles; i++) { + if (type->len == strlen(styles[i].name) && memcmp(type->data, styles[i].name, type->len) == 0) { + *open = styles[i].open; + *close = styles[i].close; + return true; + } + } + return false; +} + +/* options.stylize: stylizeWithColor under colors (the text in its style's + * codes, or unchanged), stylizeNoColor otherwise (the argument itself). + * caps[0] holds whether colors apply. */ +static ScrDyn *insp_stylize_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + ScrDyn *colors = scr_box_get_ref(closure->caps[0]); + bool colored = colors && scr_dyn_truthy(colors); + scr_dyn_release(colors); + ScrDyn *text = argc > 0 ? args[0] : (ScrDyn *)scr_dyn_undefined(); + if (!colored) return scr_dyn_retain(text); + ScrStr *str = scr_dyn_string_coerce_js(text); + if (!str) return NULL; + unsigned open, close; + ScrStr *type = argc > 1 && args[1]->kind == SCR_DYN_STR ? args[1]->v.str : NULL; + if (!type || !insp_style_of(type, &open, &close)) { + ScrDyn *out = scr_dyn_new_str(str); + scr_str_release(str); + return out; + } + InspBuf b = {0}; + char code[8]; + int n = snprintf(code, sizeof code, "\x1b[%um", open); + ib_bytes(&b, code, (size_t)n); + ib_str(&b, str); + n = snprintf(code, sizeof code, "\x1b[%um", close); + ib_bytes(&b, code, (size_t)n); + scr_str_release(str); + ScrStr *styled = ib_take(&b); + ScrDyn *out = scr_dyn_new_str(styled); + scr_str_release(styled); + return out; +} + +static ScrDyn *insp_inspect_call(ScrClosure *closure, ScrDyn *const *args, size_t argc); + +/* Whether `value` is the stylize function getUserOptions built for these + * colors. */ +static bool insp_own_stylize(const ScrDyn *value, bool colors) { + if (value->kind != SCR_DYN_FUNC || value->v.fn.thunk != insp_stylize_call) return false; + ScrDyn *flag = scr_box_get_ref(value->v.fn.clo->caps[0]); + bool colored = flag && scr_dyn_truthy(flag); + scr_dyn_release(flag); + return colored == colors; +} + +/* util.inspect as a function value (the custom functions' third + * argument): a fresh inspect context over the call's arguments, the + * caller's restored afterwards. One function per thread, so the argument + * compares equal to util.inspect. +1. */ +static SCR_TL ScrDyn *g_inspect_function; + +static void insp_inspect_function_cleanup(void) { + scr_dyn_release(g_inspect_function); + g_inspect_function = NULL; +} + +/* util.inspect's own members (custom, colors, styles, defaultOptions) + * read or written through the function value: the tables are not modeled, + * so each throws SC2020 instead of answering undefined. */ +static ScrDyn *insp_inspect_member_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)args; (void)argc; + char message[128]; + int n = snprintf(message, sizeof message, "util.inspect.%s through the inspect function is not supported yet", + (const char *)closure->fn); + scr_throw_error_msg_code(SCR_ERR_ERROR, message, (size_t)n, "SC2020"); + return NULL; +} + +static ScrDyn *insp_inspect_value(void) { + if (!g_inspect_function) { + ScrClosure *closure = scr_closure_new(NULL, 0); + g_inspect_function = + scr_dyn_new_func(closure, insp_inspect_call, 2, "native:util.inspect", "inspect"); + scr_atexit(insp_inspect_function_cleanup); + static const char *const members[] = {"custom", "defaultOptions", "colors", "styles"}; + ScrDyn *table = scr_dyn_function_properties(g_inspect_function); + for (size_t i = 0; i < sizeof members / sizeof members[0]; i++) { + ScrDyn *get = scr_dyn_new_func(scr_closure_new((void *)members[i], 0), insp_inspect_member_call, 0, + "native:function", members[i]); + ScrDyn *set = scr_dyn_new_func(scr_closure_new((void *)members[i], 0), insp_inspect_member_call, 1, + "native:function", members[i]); + scr_dyn_obj_set_accessor(table, members[i], strlen(members[i]), get, set); + table->v.obj.entries[table->v.obj.len - 1].enumerable = false; + } + scr_dyn_release(table); + } + return scr_dyn_retain(g_inspect_function); +} + +static void insp_opt_set(ScrDyn *options, const char *key, ScrDyn *value /* moves in */) { + scr_dyn_obj_set(options, key, strlen(key), value); +} + +/* getUserOptions(ctx) for the render in progress at `depth`. +1. */ +static ScrDyn *insp_user_options(double depth) { + ScrDyn *options = scr_dyn_new_obj(); + ScrClosure *closure = scr_closure_new(NULL, 1); + closure->caps[0] = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v); + scr_box_set_ref(closure->caps[0], scr_dyn_new_bool(g_colors)); + insp_opt_set(options, "stylize", + scr_dyn_new_func(closure, insp_stylize_call, 2, "native:util.stylize", + g_colors ? "stylizeWithColor" : "stylizeNoColor")); + insp_opt_set(options, "showHidden", scr_dyn_new_bool(false)); + insp_opt_set(options, "depth", + isfinite(depth) ? scr_dyn_new_num(depth) + : g_depth_kind == 0 ? scr_dyn_new_num(g_depth_number) + : g_depth_kind == 1 ? scr_dyn_new_null() + : scr_dyn_retain(scr_dyn_undefined())); + insp_opt_set(options, "colors", scr_dyn_new_bool(g_colors)); + insp_opt_set(options, "customInspect", scr_dyn_new_bool(true)); + insp_opt_set(options, "showProxy", scr_dyn_new_bool(false)); + insp_opt_set(options, "maxArrayLength", scr_dyn_new_num(100)); + insp_opt_set(options, "maxStringLength", scr_dyn_new_num(10000)); + insp_opt_set(options, "breakLength", scr_dyn_new_num(g_break_length)); + insp_opt_set(options, "compact", g_compact < 0 ? scr_dyn_new_bool(false) : scr_dyn_new_num(g_compact)); + insp_opt_set(options, "sorted", scr_dyn_new_bool(g_sorted)); + insp_opt_set(options, "getters", scr_dyn_new_bool(false)); + insp_opt_set(options, "numericSeparator", scr_dyn_new_bool(false)); + if (g_user_options && g_user_options->kind == SCR_DYN_OBJ) { + for (size_t i = 0; i < g_user_options->v.obj.len; i++) { + const ScrDynEntry *ent = &g_user_options->v.obj.entries[i]; + if (!ent->enumerable) continue; + ScrDyn *value = scr_dyn_obj_read((ScrDyn *)g_user_options, ent->key, ent->key_len); + if (!value) { + scr_dyn_release(options); + return NULL; + } + scr_dyn_obj_set(options, ent->key, ent->key_len, value); + } + } + return options; +} + +/* The custom inspect functions of the program's classes, by the type key + * of their instances' typed refs: compiled functions (self, depth, + * options, inspect) that call the instance's method and answer its + * result as a checked-dynamic value. The program registers them before + * it runs. */ +typedef struct { + char *type_key; + ScrDyn *fn; +} InspCustomClass; + +static SCR_TL InspCustomClass *g_custom_classes; +static SCR_TL size_t g_ncustom_classes; + +static void insp_custom_classes_cleanup(void) { + for (size_t i = 0; i < g_ncustom_classes; i++) { + free(g_custom_classes[i].type_key); + scr_dyn_release(g_custom_classes[i].fn); + } + free(g_custom_classes); + g_custom_classes = NULL; + g_ncustom_classes = 0; +} + +void scr_insp_register_custom(ScrStr *type_key, ScrDyn *fn) { + if (g_ncustom_classes == 0) scr_atexit(insp_custom_classes_cleanup); + InspCustomClass *grown = + realloc(g_custom_classes, (g_ncustom_classes + 1) * sizeof *g_custom_classes); + if (!grown) insp_oom(); + g_custom_classes = grown; + char *key = malloc(type_key->len + 1); + if (!key) insp_oom(); + memcpy(key, type_key->data, type_key->len); + key[type_key->len] = '\0'; + g_custom_classes[g_ncustom_classes].type_key = key; + g_custom_classes[g_ncustom_classes].fn = scr_dyn_retain(fn); + g_ncustom_classes++; +} + +static ScrDyn *insp_custom_class_fn(const ScrDyn *d) { + for (size_t i = 0; i < g_ncustom_classes; i++) + if (strcmp(g_custom_classes[i].type_key, d->v.typed_ref.type_key) == 0) + return g_custom_classes[i].fn; + return NULL; +} + +/* The [util.inspect.custom] result for an object kind, or NULL when the + * value renders normally (*thrown reports a pending exception). +1. */ +static ScrStr *insp_custom(ScrDyn *d, double recurse, double depth, bool *thrown) { + *thrown = false; + if (!g_custom_inspect) return NULL; + ScrDyn *fn; + bool compiled = d->kind == SCR_DYN_TYPED_REF; + if (compiled) { + /* A class instance: its class's compiled function, when it has a + * custom inspect method. A class whose method has no compiled call + * (the compiler refuses such classes where it renders them) refuses + * here too. */ + fn = insp_custom_class_fn(d); + if (!fn) { + ScrStr *name = scr_str_new("%symbol:Symbol(nodejs.util.inspect.custom)", 42); + bool declared = scr_dyn_typed_ref_class_has(d, name); + scr_str_release(name); + if (!declared) return NULL; + static const char message[] = + "util.inspect of this class instance's [util.inspect.custom] method has no lowering"; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); + *thrown = true; + return NULL; + } + fn = scr_dyn_retain(fn); + } else { + ScrDyn *key = scr_dyn_inspect_custom_symbol(); + if (!key) return NULL; + if (d->kind != SCR_DYN_OBJ && d->kind != SCR_DYN_ARR && d->kind != SCR_DYN_FUNC) return NULL; + fn = scr_dyn_reflect_get(d, key, d); + if (!fn) { + *thrown = true; + return NULL; + } + } + if (fn->kind != SCR_DYN_FUNC || fn->v.fn.thunk == insp_inspect_call) { + scr_dyn_release(fn); + return NULL; + } + /* A constructor's prototype object renders normally. */ + ScrDyn *ctor = !compiled && d->kind == SCR_DYN_OBJ ? scr_dyn_obj_read(d, "constructor", 11) : NULL; + if (ctor && ctor->kind == SCR_DYN_FUNC) { + ScrDyn *prototype = scr_dyn_fn_get(ctor, "prototype", 9); + bool own_prototype = prototype == d; + scr_dyn_release(prototype); + if (own_prototype) { + scr_dyn_release(ctor); + scr_dyn_release(fn); + return NULL; + } + } + scr_dyn_release(ctor); + ScrDyn *args[3]; + /* ctx.depth - recurseTimes, or null for a null depth. */ + args[0] = scr_insp_custom_depth(recurse, depth); + args[1] = insp_user_options(depth); + if (!args[1]) { + scr_dyn_release(args[0]); + scr_dyn_release(fn); + *thrown = true; + return NULL; + } + args[2] = insp_inspect_value(); + ScrDyn *ret; + double token = scr_insp_custom_call_begin(); + if (compiled) { + ScrDyn *call[4] = {d, args[0], args[1], args[2]}; + ret = scr_dyn_call(fn, call, 4, "[nodejs.util.inspect.custom]"); + } else { + scr_dyn_this_push_dyn(d); + ret = scr_dyn_call(fn, args, 3, "[nodejs.util.inspect.custom]"); + scr_dyn_this_pop(); + } + scr_insp_custom_call_end(token); + for (size_t i = 0; i < 3; i++) scr_dyn_release(args[i]); + scr_dyn_release(fn); + if (!ret) { + *thrown = true; + return NULL; + } + if (ret == d || scr_insp_custom_is_self(ret, d)) { + scr_dyn_release(ret); + return NULL; + } + ScrStr *out; + if (ret->kind == SCR_DYN_STR) out = scr_insp_custom_text(ret->v.str); + else out = scr_insp_dyn(ret, recurse, depth); + scr_dyn_release(ret); + if (scr_exc_pending()) { + scr_str_release(out); + *thrown = true; + return NULL; + } + return out; +} + +bool scr_insp_custom_enabled(void) { return g_custom_inspect; } + +/* ctx.depth - recurseTimes, or null for a null depth (NaN for undefined). */ +ScrDyn *scr_insp_custom_depth(double recurse, double depth) { + if (isfinite(depth)) return scr_dyn_new_num(depth - recurse); + if (g_depth_kind == 0) return scr_dyn_new_num(g_depth_number - recurse); + return g_depth_kind == 1 ? scr_dyn_new_null() : scr_dyn_new_num(NAN); +} + +ScrDyn *scr_insp_custom_options(double depth) { return insp_user_options(depth); } + +ScrDyn *scr_insp_inspect_function(void) { return insp_inspect_value(); } + +bool scr_insp_custom_is_self(ScrDyn *result, ScrDyn *self) { + return result->kind == SCR_DYN_TYPED_REF && self->kind == SCR_DYN_TYPED_REF && + result->v.typed_ref.ptr == self->v.typed_ref.ptr; +} + +ScrStr *scr_insp_custom_result(ScrDyn *result, double recurse, double depth) { + if (result->kind == SCR_DYN_STR) return scr_insp_custom_text(result->v.str); + return scr_insp_dyn(result, recurse, depth); +} + +/* A custom function's string result: each newline followed by the + * current indentation. +1. */ +ScrStr *scr_insp_custom_text(ScrStr *text) { + if (!memchr(text->data, '\n', text->len)) return scr_str_retain(text); + InspBuf out = {0}; + for (size_t i = 0; i < text->len; i++) { + ib_char(&out, text->data[i]); + if (text->data[i] == '\n') ib_spaces(&out, g_indent); + } + return ib_take(&out); +} + +/* Node's function rendering: [Function: name] / [Function (anonymous)], + * and [class C extends B] / [class (anonymous)] for a class. The boxed + * name is the compiler's best-effort static spelling (SEMANTICS.md — + * reference-site naming). Enumerable own properties (a class's static + * fields, properties assigned to a function) follow in braces; past the + * depth limit such a function is [Function]. Borrows d; +1. */ +static ScrStr *scr_insp_dyn_func(ScrDyn *d, double recurse, double depth) { + const char *name = d->v.fn.name; + const ScrClassObj *cls = d->v.fn.class_obj; + ScrClosure *owner = scr_closure_identity(d->v.fn.clo); + ScrBox *props = owner->props ? owner->props : cls ? cls->static_data : NULL; + ScrDyn *table = props ? (ScrDyn *)scr_box_get_ref(props) : NULL; /* +1 */ + InspBuf out = {0}; + if (cls) { + ib_cstr(&out, "[class "); + ib_cstr(&out, name && name[0] ? name : "(anonymous)"); + const ScrDyn *base = table ? table->prototype : NULL; + if (cls->base_name && cls->base_name->len) { + ib_cstr(&out, " extends "); + ib_bytes(&out, cls->base_name->data, cls->base_name->len); + } else if (base && base->kind == SCR_DYN_FUNC && base->v.fn.name && base->v.fn.name[0]) { + ib_cstr(&out, " extends "); + ib_cstr(&out, base->v.fn.name); + } + ib_char(&out, ']'); + } else { + /* The base names the function's own string `name` when its table has + * one (Object.defineProperty, util.callbackify, util.debounce). */ + size_t name_len = name ? strlen(name) : 0; + const ScrDyn *own_name = table && table->kind == SCR_DYN_OBJ ? scr_dyn_obj_get(table, "name", 4) : NULL; + if (own_name && own_name->kind == SCR_DYN_STR) { + name = own_name->v.str->data; + name_len = own_name->v.str->len; + } + if (name_len) { + ib_cstr(&out, "[Function: "); + ib_bytes(&out, name, name_len); + ib_char(&out, ']'); + } else { + ib_cstr(&out, "[Function (anonymous)]"); + } + } + ScrStr *tag = ib_take(&out); + if (cls && !props && cls->own_static_fields) { + /* Node prints the static fields after the tag; the class object + * does not hold them (the reflection fence's stance). */ + scr_str_release(tag); + static const char message[] = "util.inspect of a class with static fields through unknown is not supported yet"; + scr_throw_error_msg(SCR_ERR_ERROR, message, sizeof message - 1); + return scr_str_new("", 0); + } + size_t shown = 0; + if (table && table->kind == SCR_DYN_OBJ) { + for (size_t i = 0; i < table->v.obj.len; i++) shown += table->v.obj.entries[i].enumerable; + size_t nsymbols = table->symbol_keys ? table->symbol_keys->v.arr.len : 0; + for (size_t i = 0; i < nsymbols; i++) shown += scr_insp_symbol_entry(table, i) != NULL; + } + if (shown == 0) { + scr_dyn_release(table); + return insp_styled(tag, INSP_SPECIAL); + } + /* A function already on the traversal stack renders as [Circular *N], + * even past the depth limit. Its identity is the class object for a class + * (each dynamic box of a class may carry its own closure) and the closure + * every box of a function shares otherwise. */ + const void *identity = cls ? (const void *)cls : (const void *)owner; + double circular = scr_insp_circ_check(identity); + if (circular > 0) { + scr_dyn_release(table); + scr_str_release(tag); + return scr_insp_circular(circular); + } + if (recurse > depth) { + scr_dyn_release(table); + scr_str_release(tag); + return insp_styled_lit("[Function]", 10, INSP_SPECIAL); + } + tag = insp_styled(tag, INSP_SPECIAL); + char *prefix = malloc(tag->len + 1); + if (!prefix) scr_trap("scriptc: out of memory\n"); + memcpy(prefix, tag->data, tag->len); + prefix[tag->len] = '\0'; + scr_str_release(tag); + size_t nseen = g_nseen; + g_pending_seen = identity; + ScrStr *rendered = scr_insp_obj(table, recurse, depth, prefix); + g_pending_seen = NULL; + if (g_nseen > nseen) g_nseen = nseen; + free(prefix); + scr_dyn_release(table); + return rendered; +} + +/* Under showHidden, the kinds whose hidden entries this inspector does not + * render (a function's length/name/prototype, an error's stack, a typed + * array's byte fields, a native handle's internals): the kind's name, NULL + * for the kinds it renders. */ +static const char *insp_hidden_refusal(const ScrDyn *d) { + switch (d->kind) { + case SCR_DYN_FUNC: return "function"; + case SCR_DYN_BYTES: return d->buffer ? "Buffer" : d->v.bytes->is_data_view ? "DataView" : "typed array"; + case SCR_DYN_OBJ: return scr_dyn_obj_get(d, "%error", 6) != NULL ? "Error" : NULL; + case SCR_DYN_HANDLE: + /* A MIMEParams keeps its entries private: Node prints MIMEParams {} + * with or without showHidden. */ + return d->v.handle.tag == SCR_DYNH_MAP || d->v.handle.tag == SCR_DYNH_SET || + d->v.handle.tag == SCR_DYNH_DATE || d->v.handle.tag == SCR_DYNH_MIME_PARAMS + ? NULL + : scr_dyn_handle_cls(d); + default: return NULL; + } +} + +ScrStr *scr_insp_bytes(ScrBytes *b, double recurse, double depth) { + if (b->is_data_view) return scr_insp_data_view(b, recurse, depth); + if (b->is_buffer) return scr_insp_buffer(b); + return scr_insp_typed_array(b, recurse, depth); +} + +/* Node's URLSearchParams inspector: [Object] past the depth limit, and + * otherwise each pair as `name => value` with both strings inspected, on + * one line, or one pair per line when the pairs' text is longer than + * breakLength (the closing brace stays on the last pair's line). The lines + * take the caller's indentation, as a custom inspector's text does. */ +static ScrStr *scr_insp_search_params(ScrSearchParams *sp, double recurse, double depth) { + if (recurse > depth) return scr_str_new("[Object]", 8); + size_t n = (size_t)scr_sp_size(sp); + if (n == 0) return scr_str_new("URLSearchParams {}", 18); + ScrStr **items = malloc(n * sizeof *items); + if (!items) insp_oom(); + double length = -2; + for (size_t i = 0; i < n; i++) { + ScrStr *name = scr_sp_key_at(sp, (double)i); + ScrStr *value = scr_sp_val_at(sp, (double)i); + ScrStr *name_text = scr_insp_str(name); + ScrStr *value_text = scr_insp_str(value); + InspBuf item = {0}; + ib_str(&item, name_text); + ib_cstr(&item, " => "); + ib_str(&item, value_text); + items[i] = ib_take(&item); + length += (double)insp_utf16_len(items[i]->data, items[i]->len) + 2; + scr_str_release(name); + scr_str_release(value); + scr_str_release(name_text); + scr_str_release(value_text); + } + bool multiline = length > g_break_length; + InspBuf out = {0}; + ib_cstr(&out, multiline ? "URLSearchParams {\n " : "URLSearchParams { "); + for (size_t i = 0; i < n; i++) { + if (i) ib_cstr(&out, multiline ? ",\n " : ", "); + ib_str(&out, items[i]); + scr_str_release(items[i]); + } + free(items); + ib_cstr(&out, " }"); + ScrStr *text = ib_take(&out); + InspBuf indented = {0}; + for (size_t k = 0; k < text->len; k++) { + ib_char(&indented, text->data[k]); + if (text->data[k] == '\n') ib_spaces(&indented, g_indent); + } + scr_str_release(text); + return ib_take(&indented); +} + ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { if (d->kind == SCR_DYN_PROXY) return scr_insp_dyn(d->v.proxy.target, recurse, depth); + if ((d->kind == SCR_DYN_TYPED_REF ? g_ncustom_classes > 0 : scr_dyn_inspect_custom_symbol() != NULL) && + (d->kind == SCR_DYN_OBJ || d->kind == SCR_DYN_ARR || d->kind == SCR_DYN_FUNC || + d->kind == SCR_DYN_TYPED_REF)) { + bool thrown; + ScrStr *custom = insp_custom(d, recurse, depth, &thrown); + if (custom) return custom; + if (thrown) return scr_str_new("", 0); + } + const char *refused = g_show_hidden ? insp_hidden_refusal(d) : NULL; + if (refused) { + InspBuf out = {0}; + ib_cstr(&out, "util.format %o of "); + ib_cstr(&out, refused); + ib_cstr(&out, " values is not supported yet"); + scr_throw_error(SCR_ERR_ERROR, ib_take(&out)); + return scr_str_new("", 0); + } switch (d->kind) { case SCR_DYN_NULL: - return scr_str_new("null", 4); + return insp_styled_lit("null", 4, INSP_NULL); case SCR_DYN_UNDEF: - return scr_str_new("undefined", 9); + return insp_styled_lit("undefined", 9, INSP_UNDEFINED); case SCR_DYN_BOOL: - return d->v.b ? scr_str_new("true", 4) : scr_str_new("false", 5); + return d->v.b ? insp_styled_lit("true", 4, INSP_BOOLEAN) : insp_styled_lit("false", 5, INSP_BOOLEAN); case SCR_DYN_BIGINT: - return scr_bigint_inspect(d->v.bigint); + return insp_styled(scr_bigint_inspect(d->v.bigint), INSP_BIGINT); case SCR_DYN_SYMBOL: - return d->v.symbol.render(d->v.symbol.value); + return insp_styled(d->v.symbol.render(d->v.symbol.value), INSP_SYMBOL); case SCR_DYN_NUM: - return scr_insp_f64(d->v.num); + return insp_styled(scr_insp_f64(d->v.num), INSP_NUMBER); case SCR_DYN_STR: return scr_insp_str(d->v.str); case SCR_DYN_ARR: { size_t len = d->v.arr.len; /* Named and noncanonical numeric properties follow the elements as - * `key: value` entries (Node's array extras). */ + * `key: value` entries (Node's array extras); under showHidden the + * own `length` comes first, and non-enumerable properties show too. */ const ScrDyn *props = d->v.arr.properties; size_t nprops = 0; - for (size_t i = 0; props && i < props->v.obj.len; i++) nprops += props->v.obj.entries[i].enumerable; - if (len == 0 && nprops == 0) return scr_str_new("[]", 2); - if (recurse > depth) return scr_str_new("[Array]", 7); + for (size_t i = 0; props && i < props->v.obj.len; i++) nprops += insp_obj_shows(&props->v.obj.entries[i]); + /* An instance of a class that extends Array: Node's `Name(length) ` + * prefix (scr_arr_copy_metadata names the class). */ + const char *cls = props ? props->v.obj.class_name : NULL; + char prefix[160] = ""; + if (cls) snprintf(prefix, sizeof prefix, "%.120s(%zu) ", cls, len); + if (len == 0 && nprops == 0 && !g_show_hidden) { + if (!cls) return scr_str_new("[]", 2); + InspBuf eb = {0}; + ib_cstr(&eb, prefix); + ib_cstr(&eb, "[]"); + return ib_take(&eb); + } + if (recurse > depth) { + if (!cls) return insp_styled_lit("[Array]", 7, INSP_SPECIAL); + InspBuf eb = {0}; + ib_char(&eb, '['); + ib_cstr(&eb, cls); + ib_char(&eb, ']'); + return insp_styled(ib_take(&eb), INSP_SPECIAL); + } scr_insp_begin(recurse + 1); /* Node's maxArrayLength (100) counts output entries; a run of holes * is one `` entry (formatSpecialArray). */ @@ -1527,7 +2531,7 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { while (next < len && !presence[next]) next++; char hole[48]; int hn = snprintf(hole, sizeof hole, "<%zu empty item%s>", next - i, next - i == 1 ? "" : "s"); - ScrStr *s = scr_str_new(hole, (size_t)hn); + ScrStr *s = insp_styled_lit(hole, (size_t)hn, INSP_UNDEFINED); scr_insp_entry(s, false); scr_str_release(s); i = next; @@ -1537,6 +2541,7 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { ScrStr *s = scr_insp_dyn(item, recurse + 1, depth); scr_insp_entry(s, item->kind == SCR_DYN_NUM); scr_str_release(s); + if (scr_exc_pending()) break; } bool more = i < len; size_t entries = shown + (more ? 1 : 0); @@ -1545,12 +2550,27 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { scr_insp_entry(m, d->v.arr.items[i]->kind == SCR_DYN_NUM); scr_str_release(m); } + if (g_show_hidden) { + /* The own non-enumerable `length`, after the items. Node's + * grouping reads value[k] for every output position k, this one + * included. */ + char line[48]; + int n = snprintf(line, sizeof line, "[length]: %zu", len); + ScrStr *s = scr_str_new(line, (size_t)n); + scr_insp_entry(s, entries < len && (!presence || presence[entries]) && + d->v.arr.items[entries]->kind == SCR_DYN_NUM); + scr_str_release(s); + entries++; + } for (size_t k = 0; props && k < props->v.obj.len; k++) { const ScrDynEntry *ent = &props->v.obj.entries[k]; - if (!ent->enumerable) continue; + if (!insp_obj_shows(ent)) continue; ScrStr *val = ent->accessor ? insp_accessor_tag(ent) : scr_insp_dyn(ent->value, recurse + 1, depth); InspBuf eb = {0}; + /* formatProperty's hidden key: the key's rendering in brackets. */ + if (!ent->enumerable) ib_char(&eb, '['); insp_key_into(&eb, ent->key, ent->key_len); + if (!ent->enumerable) ib_char(&eb, ']'); ib_cstr(&eb, ": "); ib_bytes(&eb, val->data, val->len); scr_str_release(val); @@ -1564,11 +2584,14 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { entries++; } /* Node's grouping leaves out the last entry when there are more - * entries than maxArrayLength (the `... more items` entry when the - * array has no properties). */ + * entries than maxArrayLength (100): the `... more items` entry, + * `[length]` under showHidden, or the last property. */ bool last_out = entries > 100; ScrStr *base = scr_str_new("", 0); - ScrStr *b0 = scr_str_new("[", 1); + InspBuf ob = {0}; + ib_cstr(&ob, prefix); + ib_char(&ob, '['); + ScrStr *b0 = ib_take(&ob); ScrStr *b1 = scr_str_new("]", 1); ScrStr *out = scr_insp_end(base, b0, b1, recurse + 1, true, last_out); scr_str_release(base); @@ -1580,29 +2603,22 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { /* An Error carried as a checked-dynamic value (a caught error, an * Error stored in an `unknown` slot): the error's own rendering, * never the encoding's internal marker. */ - if (scr_dyn_obj_get(d, "%error", 6) != NULL) return scr_insp_dyn_error(d, recurse, depth); + if (scr_dyn_obj_get(d, "%error", 6) != NULL) { + if (g_colors) return insp_colors_fence("Error values"); + return scr_insp_dyn_error(d, recurse, depth); + } /* Object.create(null)'s dictionary: Node prefixes the rendering * with "[Object: null prototype]" (formatValue's constructor-less * base), the empty form included, and the beyond-depth answer IS * the bare marker (where a plain object says [Object]). */ return scr_insp_obj(d, recurse, depth, scr_dyn_class_name(d)); } - case SCR_DYN_FUNC: { - /* Node's function rendering: [Function: name] / [Function - * (anonymous)]. The boxed name is the compiler's best-effort static - * spelling (SEMANTICS.md — reference-site naming). */ - const char *name = d->v.fn.name; - InspBuf out = {0}; - if (name && name[0]) { - ib_cstr(&out, "[Function: "); - ib_cstr(&out, name); - ib_char(&out, ']'); - } else { - ib_cstr(&out, "[Function (anonymous)]"); - } - return ib_take(&out); - } + case SCR_DYN_FUNC: + return scr_insp_dyn_func(d, recurse, depth); case SCR_DYN_HANDLE: { + if (g_colors && d->v.handle.tag != SCR_DYNH_SET && d->v.handle.tag != SCR_DYNH_MAP && + d->v.handle.tag != SCR_DYNH_DATE) + return insp_colors_fence_values(scr_dyn_handle_cls(d)); if (d->v.handle.tag == SCR_DYNH_ARRAY_BUFFER || d->v.handle.tag == SCR_DYNH_SHARED_ARRAY_BUFFER) { return scr_insp_array_buffer(d->v.handle.ptr, scr_dyn_handle_cls(d), recurse, depth); @@ -1612,7 +2628,10 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { if (d->v.handle.tag == SCR_DYNH_WEAK_MAP || d->v.handle.tag == SCR_DYNH_WEAK_SET) return scr_insp_weak_collection(d->v.handle.tag == SCR_DYNH_WEAK_MAP ? "WeakMap" : "WeakSet", recurse, depth); - if (d->v.handle.tag == SCR_DYNH_DATE) return scr_insp_date(scr_dyn_native_date_value(d)); + if (d->v.handle.tag == SCR_DYNH_DATE) + return insp_styled(scr_insp_date(scr_dyn_native_date_value(d)), INSP_DATE); + if (d->v.handle.tag == SCR_DYNH_SEARCH_PARAMS) + return scr_insp_search_params(d->v.handle.ptr, recurse, depth); if (d->v.handle.tag == SCR_DYNH_ITERATOR) { ScrStr *iterator = scr_insp_native_iterator(d, recurse, depth); if (iterator) return iterator; @@ -1623,6 +2642,14 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { * depth limit (`depth < 0`); the ones that return `this` there * print util.inspect's `[cls]`. */ if (recurse > depth && !ops->inspect_view_empty) { + /* A nested-view inspector returns the instance itself there, + * which has no own properties: `URL {}`. */ + if (ops->inspect_view_nested) { + InspBuf out = {0}; + ib_cstr(&out, ops->cls); + ib_cstr(&out, " {}"); + return ib_take(&out); + } if (!ops->inspect_bracketed_past_depth) return scr_str_new(ops->cls, strlen(ops->cls)); InspBuf out = {0}; ib_char(&out, '['); @@ -1631,6 +2658,16 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { return ib_take(&out); } ScrDyn *view = ops->inspect_view(d->v.handle.ptr); + if (ops->inspect_view_nested) { + scr_insp_nested_begin(); + ScrStr *inner = scr_insp_dyn(view, 0, depth); + scr_dyn_release(view); + ScrStr *prefix = scr_str_new(ops->cls, strlen(ops->cls)); + ScrStr *rendered = scr_insp_nested_end(prefix, inner); + scr_str_release(prefix); + scr_str_release(inner); + return rendered; + } ScrStr *body = scr_insp_dyn(view, recurse, depth); scr_dyn_release(view); InspBuf out = {0}; @@ -1661,9 +2698,14 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { } case SCR_DYN_TYPED_REF: { + if (g_colors && scr_insp_is_generator(d)) return insp_colors_fence("generator objects"); if (scr_insp_is_generator(d)) return scr_insp_generator(d); const char *cls = NULL; ScrDyn *view = scr_event_ref_hooks ? scr_event_ref_hooks->inspect_view(d, &cls) : NULL; + if (view && g_colors) { + scr_dyn_release(view); + return insp_colors_fence_values(cls); + } if (view) { /* The handle rule above: the class name past the depth limit. */ if (recurse > depth) { @@ -1693,6 +2735,11 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { double time; ScrStr *date_class = materialized->kind == SCR_DYN_OBJ && scr_dyn_date_class_time(d, &time) ? scr_dyn_date_class_name(d) : NULL; + if (date_class && g_colors) { + scr_str_release(date_class); + scr_dyn_release(materialized); + return insp_colors_fence("Date subclass instances"); + } if (date_class) { /* An instance of a class declared `extends Date`: the class name and * the time as Date.prototype.toISOString formats it, then its own @@ -1725,10 +2772,23 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { free(cls); } else if (name && materialized->kind == SCR_DYN_OBJ && scr_dyn_obj_get(materialized, "%error", 6) == NULL) { - char *cls = malloc(name->len + 1); + ScrStr *tag = insp_inherited_tag(d, name); + if (scr_exc_pending()) { + scr_dyn_release(materialized); + return scr_str_new("", 0); + } + size_t tag_len = tag ? tag->len + 3 : 0; + char *cls = malloc(name->len + tag_len + 1); if (!cls) scr_trap("scriptc: out of memory\n"); memcpy(cls, name->data, name->len); - cls[name->len] = '\0'; + if (tag) { + /* Node's `Name [tag]` prefix. */ + memcpy(cls + name->len, " [", 2); + memcpy(cls + name->len + 2, tag->data, tag->len); + cls[name->len + tag_len - 1] = ']'; + scr_str_release(tag); + } + cls[name->len + tag_len] = '\0'; size_t nseen = g_nseen; g_pending_seen = instance; out = scr_insp_obj(materialized, recurse, depth, cls); @@ -1753,6 +2813,7 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { } case SCR_DYN_BYTES: { ScrBytes *b = d->v.bytes; + if (b->is_data_view && g_colors) return insp_colors_fence("DataView values"); if (b->is_data_view) return scr_insp_data_view(b, recurse, depth); if (d->buffer) { /* Buffer-flavored (a stream chunk): Node's form. */ @@ -1772,6 +2833,579 @@ ScrStr *scr_insp_dyn_s(ScrDyn *d, double depth) { return scr_insp_dyn(d, 0, depth); } +ScrStr *scr_insp_dyn_format_s(ScrDyn *d, double depth) { + /* hasBuiltInToString is false for MIMEType and MIMEParams (their + * constructors are not globals): %s prints String(value). */ + if (d->kind == SCR_DYN_HANDLE && (d->v.handle.tag == SCR_DYNH_MIME_TYPE || d->v.handle.tag == SCR_DYNH_MIME_PARAMS)) + return scr_dyn_string_coerce_js(d); + return scr_insp_dyn_s(d, depth); +} + +/* ── util.format over checked-dynamic values ───────────────────────────── + * formatWithOptionsInternal over argument values whose types are known + * only at run time: util.format and util.formatWithOptions with a runtime + * format string or spread arguments, console output with a runtime format + * string, and debuglog's lines. The %-substitutions (%s %d %i %f %j %o %O + * %c %%) follow Node's argument cursor, then the remaining arguments join + * with spaces, strings verbatim and everything else inspected under the + * options. With colors, primitives take inspect's default styles; colored + * object renderings have no lowering. These entries call program code + * (toString, valueOf and toJSON), so unlike the rest of this file they + * can throw: NULL with the exception pending. */ +static void insp_fmt_fence(const ScrInspFormatOptions *o, const char *what) { + InspBuf b = {0}; + ib_cstr(&b, o->surface); + ib_char(&b, ' '); + ib_cstr(&b, what); + ScrStr *message = ib_take(&b); + scr_throw_error_msg_code(SCR_ERR_ERROR, message->data, message->len, "SC2020"); + scr_str_release(message); +} + +static bool insp_fmt_object(const ScrDyn *value) { + switch (value->kind) { + case SCR_DYN_OBJ: + case SCR_DYN_ARR: + case SCR_DYN_FUNC: + case SCR_DYN_BYTES: + case SCR_DYN_HANDLE: + case SCR_DYN_PROMISE: + case SCR_DYN_TYPED_REF: + case SCR_DYN_PROXY: + return true; + default: + return false; + } +} + +void scr_insp_format_defaults(ScrInspFormatOptions *o, const char *surface) { + o->surface = surface; + o->depth = 2; + o->depth_kind = 0; + o->depth_number = 2; + o->colors = false; + o->custom_inspect = true; + o->break_length = 80; + o->compact = 3; + o->sorted = false; + o->user_options = NULL; +} + +/* inspect(value, options) at the given depth, compact and colors, with + * the options' breakLength and sorted; the previous layout returns + * afterwards. +1, or NULL with the exception pending. */ +static ScrStr *insp_fmt_inspect(ScrDyn *value, const ScrInspFormatOptions *o, double depth, + double compact, bool colors) { + double break_length = g_break_length, saved_compact = g_compact; + bool sorted = g_sorted, saved_colors = g_colors, saved_custom = g_custom_inspect; + int saved_depth_kind = g_depth_kind; + double saved_depth_number = g_depth_number; + const ScrDyn *saved_user = g_user_options; + scr_insp_layout(o->break_length, compact, o->sorted); + g_colors = colors; + g_custom_inspect = o->custom_inspect; + g_depth_kind = o->depth_kind; + g_depth_number = o->depth_number; + g_user_options = o->user_options; + ScrStr *text = scr_insp_dyn(value, 0, depth); + g_colors = saved_colors; + g_custom_inspect = saved_custom; + g_depth_kind = saved_depth_kind; + g_depth_number = saved_depth_number; + g_user_options = saved_user; + scr_insp_layout(break_length, saved_compact, sorted); + if (scr_exc_pending()) { + scr_str_release(text); + return NULL; + } + return text; +} + +/* hasBuiltInToString's negative answer: a toString the program defined on + * the object or its prototype chain (1). Symbol-keyed members (where + * Symbol.toPrimitive would also count) are reported as unknown (-1), and a + * class instance whose class defines toString as -2: its methods are + * compiled for the class's own calls, which this runtime cannot make. */ +static int insp_fmt_custom_to_string(const ScrDyn *value) { + if (value->kind == SCR_DYN_TYPED_REF) { + ScrStr *to_string = scr_str_new("toString", 8); + bool has = scr_dyn_typed_ref_class_has(value, to_string); + scr_str_release(to_string); + return has ? -2 : 0; + } + for (const ScrDyn *current = value; current && current->kind == SCR_DYN_OBJ; current = current->prototype) { + if (current->symbol_properties) return -1; + const ScrDyn *member = scr_dyn_obj_get(current, "toString", 8); + if (member) return member->kind == SCR_DYN_FUNC; + } + return 0; +} + +static ScrStr *insp_fmt_s(ScrDyn *value, const ScrInspFormatOptions *o) { + if (value->kind == SCR_DYN_STR) return scr_str_retain(value->v.str); + if (value->kind == SCR_DYN_NUM) return scr_insp_f64(value->v.num); + if (value->kind == SCR_DYN_BIGINT) return scr_bigint_inspect(value->v.bigint); + if (value->kind == SCR_DYN_HANDLE && (value->v.handle.tag == SCR_DYNH_MIME_TYPE || value->v.handle.tag == SCR_DYNH_MIME_PARAMS)) + return scr_dyn_string_coerce_js(value); + if (value->kind == SCR_DYN_FUNC) { + insp_fmt_fence(o, "%s of a function prints its source text, which a compiled binary does not keep"); + return NULL; + } + if ((value->kind == SCR_DYN_OBJ && scr_dyn_obj_get(value, "%error", 6) == NULL) || + value->kind == SCR_DYN_TYPED_REF) { + int custom = insp_fmt_custom_to_string(value); + if (custom == -2) { + insp_fmt_fence(o, "%s of a class instance whose class defines toString has no lowering"); + return NULL; + } + if (custom < 0) { + insp_fmt_fence(o, "%s of objects with symbol-keyed members has no lowering"); + return NULL; + } + if (custom) return scr_dyn_string_coerce_js(value); + } + if (!insp_fmt_object(value)) return scr_insp_dyn(value, 0, 0); + /* inspect(arg, { ...inspectOptions, depth: 0, colors: false, compact: 3 }) */ + return insp_fmt_inspect(value, o, 0, 3, false); +} + +static ScrStr *insp_fmt_number(ScrDyn *value, int spec) { + if (value->kind == SCR_DYN_BIGINT && spec != 'f') return scr_bigint_inspect(value->v.bigint); + if (value->kind == SCR_DYN_SYMBOL) return scr_str_new("NaN", 3); + double number; + if (spec == 'd') { + if (!scr_dyn_number_coerce_js(value, &number)) return NULL; + } else { + ScrStr *text = scr_dyn_string_coerce_js(value); + if (!text) return NULL; + number = spec == 'i' ? scr_parse_int(text, 0) : scr_parse_float(text); + scr_str_release(text); + } + return scr_insp_f64(number); +} + +/* One formatter's conversions: %s, %j, %d/%i/%f, %O/%o and the remaining + * arguments' inspect (depth -1: the options' depth). The loop references + * only the table it runs, so a program links only the conversions it uses. */ +typedef struct { + ScrStr *(*s)(ScrDyn *value, const ScrInspFormatOptions *o); + ScrStr *(*j)(const ScrDyn *value); + ScrStr *(*number)(ScrDyn *value, int spec); + ScrStr *(*inspect)(ScrDyn *value, const ScrInspFormatOptions *o, double depth); +} InspFmtOps; + +static ScrStr *insp_format_loop(ScrDyn *const *args, size_t argc, const ScrInspFormatOptions *o, + const InspFmtOps *ops) { + if (argc == 0) return scr_str_new("", 0); + InspBuf out = {0}; + size_t a = 0; + const char *join = ""; + if (args[0]->kind == SCR_DYN_STR) { + const ScrStr *first = args[0]->v.str; + if (argc == 1) return scr_str_retain((ScrStr *)first); + const char *text = first->data; + size_t last = 0; + for (size_t i = 0; first->len > 0 && i < first->len - 1; i++) { + if (text[i] != '%') continue; + int next = (unsigned char)text[++i]; + if (a + 1 != argc) { + ScrStr *piece = NULL; + switch (next) { + case 's': piece = ops->s(args[++a], o); break; + case 'j': piece = ops->j(args[++a]); break; + case 'd': + case 'i': + case 'f': piece = ops->number(args[++a], next); break; + case 'O': piece = ops->inspect(args[++a], o, o->depth); break; + case 'o': + if (insp_fmt_object(args[a + 1])) { + insp_fmt_fence(o, "%o of objects renders hidden properties, which have no lowering; use %O"); + break; + } + piece = ops->inspect(args[++a], o, 4); + break; + case 'c': + a += 1; + piece = scr_str_new("", 0); + break; + case '%': + ib_bytes(&out, text + last, i - last); + last = i + 1; + continue; + default: + continue; + } + if (!piece) { + free(out.data); + return NULL; + } + if (last != i - 1) ib_bytes(&out, text + last, i - 1 - last); + ib_str(&out, piece); + scr_str_release(piece); + last = i + 1; + } else if (next == '%') { + ib_bytes(&out, text + last, i - last); + last = i + 1; + } + } + if (last != 0) { + a++; + join = " "; + if (last < first->len) ib_bytes(&out, text + last, first->len - last); + } + } + for (; a < argc; a++) { + ib_cstr(&out, join); + if (args[a]->kind == SCR_DYN_STR) { + ib_str(&out, args[a]->v.str); + } else { + ScrStr *piece = ops->inspect(args[a], o, o->depth); + if (!piece) { + free(out.data); + return NULL; + } + ib_str(&out, piece); + scr_str_release(piece); + } + join = " "; + } + return ib_take(&out); +} + + +static ScrStr *insp_fmt_inspect_full(ScrDyn *value, const ScrInspFormatOptions *o, double depth) { + return insp_fmt_inspect(value, o, depth, o->compact, o->colors); +} + +/* The conversion tables are filled where they run: a constant table is a + * global, and sanitized builds keep instrumented globals (and the + * conversions they point at) through --gc-sections. */ +ScrStr *scr_insp_format(ScrDyn *const *args, size_t argc, const ScrInspFormatOptions *o) { + InspFmtOps ops; + ops.s = insp_fmt_s; + ops.j = scr_util_format_j; + ops.number = insp_fmt_number; + ops.inspect = insp_fmt_inspect_full; + return insp_format_loop(args, argc, o, &ops); +} + +/* The formatter over arguments that are all strings, numbers, booleans, + * null or undefined (the compiler proves it from their types): the same + * rules for those kinds, without the checked-dynamic walk the other + * values need. */ +static ScrStr *insp_prim_to_string(const ScrDyn *v) { + switch (v->kind) { + case SCR_DYN_STR: return scr_str_retain(v->v.str); + case SCR_DYN_NUM: { + char buf[40]; + size_t n = scr_f64_to_str(v->v.num, buf); + return scr_str_new(buf, n); + } + case SCR_DYN_BOOL: return v->v.b ? scr_str_new("true", 4) : scr_str_new("false", 5); + case SCR_DYN_NULL: return scr_str_new("null", 4); + default: return scr_str_new("undefined", 9); + } +} + +static ScrStr *insp_prim_inspect(ScrDyn *v, const ScrInspFormatOptions *o, double depth) { + (void)o; + (void)depth; + if (v->kind == SCR_DYN_STR) return scr_insp_str(v->v.str); + if (v->kind == SCR_DYN_NUM) return scr_insp_f64(v->v.num); + return insp_prim_to_string(v); +} + +static ScrStr *insp_prim_s(ScrDyn *v, const ScrInspFormatOptions *o) { + (void)o; + if (v->kind == SCR_DYN_NUM) return scr_insp_f64(v->v.num); + return insp_prim_to_string(v); +} + +static ScrStr *insp_prim_j(const ScrDyn *v) { + if (v->kind == SCR_DYN_STR) { + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_put_json_str(&b, v->v.str); + return scr_jb_finish(&b); + } + if (v->kind == SCR_DYN_NUM && !isfinite(v->v.num)) return scr_str_new("null", 4); + return insp_prim_to_string(v); +} + +static ScrStr *insp_prim_number(ScrDyn *v, int spec) { + double number; + if (spec == 'd') { + number = v->kind == SCR_DYN_NUM ? v->v.num + : v->kind == SCR_DYN_STR ? scr_string_to_number(v->v.str) + : v->kind == SCR_DYN_BOOL ? (v->v.b ? 1 : 0) + : v->kind == SCR_DYN_NULL ? 0 + : NAN; + } else { + ScrStr *text = insp_prim_to_string(v); + number = spec == 'i' ? scr_parse_int(text, 0) : scr_parse_float(text); + scr_str_release(text); + } + return scr_insp_f64(number); +} + +/* A native list of checked values (the compiled format calls) as the + * runtime's argument pack. +1. */ +static ScrDyn *insp_list_pack(ScrArr *list) { + ScrDyn *pack = scr_dyn_new_arr(); + for (uint32_t i = 0; i < list->len; i++) scr_dyn_arr_push(pack, scr_arr_get_ref(list, i)); + return pack; +} + +ScrStr *scr_insp_format_primitives(ScrArr *list) { + ScrInspFormatOptions o; + scr_insp_format_defaults(&o, "util.format"); + ScrDyn *pack = insp_list_pack(list); + InspFmtOps ops; + ops.s = insp_prim_s; + ops.j = insp_prim_j; + ops.number = insp_prim_number; + ops.inspect = insp_prim_inspect; + ScrStr *out = insp_format_loop(pack->v.arr.items, pack->v.arr.len, &o, &ops); + scr_dyn_release(pack); + return out; +} + +/* The arguments of a dyn array pack (+1 text, or NULL pending). */ +static ScrStr *insp_fmt_pack(ScrDyn *pack, const ScrInspFormatOptions *o) { + if (pack->kind != SCR_DYN_ARR) return scr_insp_format(NULL, 0, o); + return scr_insp_format(pack->v.arr.items, pack->v.arr.len, o); +} + +ScrStr *scr_insp_format_pack(ScrDyn *pack) { + ScrInspFormatOptions o; + scr_insp_format_defaults(&o, "util.format"); + return insp_fmt_pack(pack, &o); +} + +ScrStr *scr_insp_format_list(ScrArr *list) { + ScrDyn *pack = insp_list_pack(list); + ScrStr *out = scr_insp_format_pack(pack); + scr_dyn_release(pack); + return out; +} + +/* Whether a format string can substitute: a '%' before its last character. + * A string without one formats as its arguments joined with spaces. */ +bool scr_insp_format_specifies(ScrStr *first) { + return first->len > 1 && memchr(first->data, '%', first->len - 1) != NULL; +} + +bool scr_insp_format_specifies_dyn(ScrDyn *first) { + return first->kind == SCR_DYN_STR && scr_insp_format_specifies(first->v.str); +} + +/* One own enumerable option of an inspect options object: +1 value, a + * borrowed undefined when absent, or NULL with the exception pending. */ +static ScrDyn *insp_opt(const ScrDyn *options, const char *key, bool *present) { + size_t len = strlen(key); + *present = options->kind == SCR_DYN_OBJ && scr_dyn_obj_get(options, key, len) != NULL && + scr_dyn_obj_enumerable(options, key, len); + if (!*present) return scr_dyn_retain(scr_dyn_undefined()); + return scr_dyn_obj_read(options, key, len); +} + +/* ctx.depth from an option or inspect's third argument: a number, null + * or undefined (no limit for null, undefined and NaN). */ +static bool insp_depth_option(const ScrDyn *value, ScrInspFormatOptions *o) { + if (value->kind == SCR_DYN_NUM) { + o->depth = isnan(value->v.num) ? INFINITY : value->v.num; + o->depth_kind = 0; + o->depth_number = value->v.num; + } else if (value->kind == SCR_DYN_NULL || value->kind == SCR_DYN_UNDEF) { + o->depth = INFINITY; + o->depth_kind = value->kind == SCR_DYN_NULL ? 1 : 2; + } else { + return false; + } + return true; +} + +/* inspect's option keys (inspectDefaultOptions and stylize) over an + * options object, as `inspect(value, options)` copies them. Options at + * their defaults, depth, colors, compact (a number or false), + * breakLength (a number) and sorted (a boolean) lower; other values of + * the known options throw the SC2020 refusal. */ +bool scr_insp_format_read_options(const ScrDyn *options, ScrInspFormatOptions *o) { + ScrDyn *view = NULL; + if (options->kind == SCR_DYN_TYPED_REF) { + view = scr_dyn_typed_ref_materialize(options); + if (scr_exc_pending()) { + scr_dyn_release(view); + return false; + } + options = view; + } + bool ok = false, present; + ScrDyn *value = NULL; +#define INSP_OPT(key) \ + do { \ + scr_dyn_release(value); \ + value = insp_opt(options, key, &present); \ + if (!value) goto done; \ + } while (0) + INSP_OPT("depth"); + if (present) { + if (!insp_depth_option(value, o)) { + insp_fmt_fence(o, "with a depth option that is not a number or null has no lowering"); + goto done; + } + } + INSP_OPT("colors"); + if (present) o->colors = scr_dyn_truthy(value); + INSP_OPT("customInspect"); + if (present) o->custom_inspect = scr_dyn_truthy(value); + INSP_OPT("compact"); + if (present) { + if (value->kind == SCR_DYN_NUM) o->compact = value->v.num < 0 ? 0 : value->v.num; + else if (value->kind == SCR_DYN_BOOL && !value->v.b) o->compact = -1; + else { + insp_fmt_fence(o, "with this compact option has no lowering (compact must be false or a number)"); + goto done; + } + } + INSP_OPT("breakLength"); + if (present) { + if (value->kind != SCR_DYN_NUM) { + insp_fmt_fence(o, "with a breakLength option that is not a number has no lowering"); + goto done; + } + o->break_length = value->v.num; + } + INSP_OPT("sorted"); + if (present) { + if (value->kind != SCR_DYN_BOOL) { + insp_fmt_fence(o, "with a sorted option that is not a boolean has no lowering"); + goto done; + } + o->sorted = value->v.b; + } + /* The options whose non-default values change renderings this runtime + * does not produce. */ + static const struct { + const char *key; + int kind; /* 0: falsy, 1: true, 2: the number 100, 3: the number 10000 */ + } fixed[] = { + {"showHidden", 0}, {"showProxy", 0}, {"maxArrayLength", 2}, {"maxStringLength", 3}, + {"getters", 0}, {"numericSeparator", 0}, {"stylize", 0}, + }; + for (size_t i = 0; i < sizeof fixed / sizeof *fixed; i++) { + INSP_OPT(fixed[i].key); + if (!present) continue; + bool at_default = fixed[i].kind == 0 ? !scr_dyn_truthy(value) + : fixed[i].kind == 1 ? value->kind == SCR_DYN_BOOL && value->v.b + : value->kind == SCR_DYN_NUM && value->v.num == (fixed[i].kind == 2 ? 100 : 10000); + /* The stylize function a custom inspect function received (options + * passed back into util.inspect) is the default for its colors. */ + if (!at_default && strcmp(fixed[i].key, "stylize") == 0 && insp_own_stylize(value, o->colors)) + at_default = true; + /* stylize is replaced by the color stylizer when colors is set. */ + if (!at_default && !(o->colors && strcmp(fixed[i].key, "stylize") == 0)) { + InspBuf b = {0}; + ib_cstr(&b, "with the "); + ib_cstr(&b, fixed[i].key); + ib_cstr(&b, " option has no lowering"); + ScrStr *what = ib_take(&b); + insp_fmt_fence(o, what->data); + scr_str_release(what); + goto done; + } + } +#undef INSP_OPT + /* Keys inspect does not read: the custom functions receive them. */ + if (!view && options->kind == SCR_DYN_OBJ) { + static const char *const known[] = { + "showHidden", "depth", "colors", "customInspect", "showProxy", + "maxArrayLength", "maxStringLength", "breakLength", "compact", "sorted", + "getters", "numericSeparator", "stylize", + }; + for (size_t i = 0; i < options->v.obj.len && !o->user_options; i++) { + const ScrDynEntry *ent = &options->v.obj.entries[i]; + if (!ent->enumerable) continue; + bool read = false; + for (size_t k = 0; k < sizeof known / sizeof *known && !read; k++) + read = ent->key_len == strlen(known[k]) && memcmp(ent->key, known[k], ent->key_len) == 0; + if (!read) o->user_options = options; + } + } + ok = true; +done: + scr_dyn_release(value); + scr_dyn_release(view); + return ok; +} + +/* util.inspect(value[, options]) and inspect(value, showHidden, depth, + * colors) over checked-dynamic arguments; `argc` is the call's argument + * count. A third argument other than undefined sets the depth and a + * fourth the colors; a boolean second argument is showHidden, and an + * object supplies the options, which apply after the legacy arguments. + * +1, or NULL with the exception pending. */ +ScrStr *scr_insp_inspect(ScrDyn *value, ScrDyn *options, ScrDyn *depth, ScrDyn *colors, + double argc) { + ScrInspFormatOptions o; + scr_insp_format_defaults(&o, "util.inspect"); + if (argc >= 3 && depth->kind != SCR_DYN_UNDEF) { + if (!insp_depth_option(depth, &o)) { + insp_fmt_fence(&o, "with a depth argument that is not a number or null has no lowering"); + return NULL; + } + } + if (argc >= 4 && colors->kind != SCR_DYN_UNDEF) o.colors = scr_dyn_truthy(colors); + if (options->kind == SCR_DYN_BOOL) { + if (options->v.b) { + insp_fmt_fence(&o, "with showHidden has no lowering"); + return NULL; + } + } else if (options->kind == SCR_DYN_OBJ || options->kind == SCR_DYN_TYPED_REF) { + if (!scr_insp_format_read_options(options, &o)) return NULL; + } + return insp_fmt_inspect(value, &o, o.depth, o.compact, o.colors); +} + +static ScrDyn *insp_inspect_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; + ScrDyn *undefined = scr_dyn_undefined(); + scr_insp_nested_begin(); + ScrStr *text = scr_insp_inspect(argc > 0 ? args[0] : undefined, argc > 1 ? args[1] : undefined, + argc > 2 ? args[2] : undefined, argc > 3 ? args[3] : undefined, + (double)argc); + scr_insp_nested_restore(); + if (!text) return NULL; + ScrDyn *out = scr_dyn_new_str(text); + scr_str_release(text); + return out; +} + +/* util.formatWithOptions(inspectOptions, ...pack): validateObject (arrays + * pass, null and functions do not), then the options as inspect reads + * them. */ +ScrStr *scr_insp_format_with_options(ScrDyn *options, ScrDyn *pack) { + if (options->kind != SCR_DYN_OBJ && options->kind != SCR_DYN_ARR && + options->kind != SCR_DYN_TYPED_REF) { + scr_dyn_arg_type_fail("inspectOptions", "of type object", options); + return NULL; + } + ScrInspFormatOptions o; + scr_insp_format_defaults(&o, "util.formatWithOptions"); + if (!scr_insp_format_read_options(options, &o)) return NULL; + return insp_fmt_pack(pack, &o); +} + +/* util.format's %o over a dyn value: util.inspect with showHidden at the + * given depth. */ +ScrStr *scr_insp_dyn_hidden(ScrDyn *d, double depth) { + bool outer = g_show_hidden; + g_show_hidden = true; + ScrStr *out = scr_insp_dyn(d, 0, depth); + g_show_hidden = outer; + return out; +} + /* ── console.table cells ─────────────────────────────────────────────── */ size_t scr_insp_display_width(const char *data, size_t len) { return insp_width(data, len); } diff --git a/packages/runtime/src/scr_inspect.h b/packages/runtime/src/scr_inspect.h index 5ac3aa9dd..aac4e6589 100644 --- a/packages/runtime/src/scr_inspect.h +++ b/packages/runtime/src/scr_inspect.h @@ -45,10 +45,64 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth); /* A numeric typed array that is not a Buffer: `Uint8Array(n) [ ... ]`, * `[Uint8Array]` past the depth limit. Borrows b; +1. */ ScrStr *scr_insp_typed_array(ScrBytes *b, double recurse, double depth); +/* A u8 byte view whose static type does not say which one it is (a union + * arm, an element or field): a DataView, a Buffer () or a + * Uint8Array, by the view's own flags. */ +ScrStr *scr_insp_bytes(ScrBytes *b, double recurse, double depth); /* format's %s / rest-args twin: dyn strings pass verbatim. */ ScrStr *scr_insp_dyn_s(ScrDyn *d, double depth); +/* util.format over checked-dynamic arguments (formatWithOptionsInternal): + * the options an inspect of each argument takes, and the API name the + * SC2020 refusals name. These entries call program code, so they answer + * NULL with the exception pending when it throws. */ +typedef struct ScrInspFormatOptions { + const char *surface; + double depth; /* Infinity for null, undefined and NaN */ + /* The depth as the options gave it, which custom inspect functions + * receive when the depth is unbounded: 0 a number (depth_number, + * Infinity or NaN), 1 null, 2 undefined. */ + int depth_kind; + double depth_number; + bool colors; + bool custom_inspect; + double break_length; + double compact; /* -1 for false */ + bool sorted; + /* The options object, when it holds keys inspect does not read: custom + * inspect functions receive them (borrowed; NULL otherwise). */ + const ScrDyn *user_options; +} ScrInspFormatOptions; +void scr_insp_format_defaults(ScrInspFormatOptions *o, const char *surface); +/* Reads inspect's options from an options object into o; false with the + * SC2020 refusal pending for values that have no lowering. */ +bool scr_insp_format_read_options(const ScrDyn *options, ScrInspFormatOptions *o); +ScrStr *scr_insp_format(ScrDyn *const *args, size_t argc, const ScrInspFormatOptions *o); +/* util.format(...pack) and util.formatWithOptions(options, ...pack) over a + * dyn array of the arguments (+1). */ +ScrStr *scr_insp_format_pack(ScrDyn *pack); +/* util.format over a native list of checked values, the compiled calls' + * held arguments (+1). */ +struct ScrArr; +ScrStr *scr_insp_format_list(struct ScrArr *list); +/* scr_insp_format_list when every argument is a string, number, boolean, + * null or undefined, without the checked-dynamic walk (+1). */ +ScrStr *scr_insp_format_primitives(struct ScrArr *list); +ScrStr *scr_insp_format_with_options(ScrDyn *options, ScrDyn *pack); +/* util.inspect(value, options, depth, colors) over checked-dynamic + * arguments with Node's legacy-argument rules; argc is the call's + * argument count (+1, or NULL with the exception pending). */ +ScrStr *scr_insp_inspect(ScrDyn *value, ScrDyn *options, ScrDyn *depth, ScrDyn *colors, + double argc); +/* Whether a format string has a '%' before its last character, the only + * strings whose substitutions can change the output. */ +bool scr_insp_format_specifies(ScrStr *first); +bool scr_insp_format_specifies_dyn(ScrDyn *first); +/* format's %s over a dyn value: scr_insp_dyn_s, except that values whose + * class defines its own toString print String(value). */ +ScrStr *scr_insp_dyn_format_s(ScrDyn *d, double depth); +ScrStr *scr_insp_dyn_hidden(ScrDyn *d, double depth); /* %o: showHidden */ void scr_insp_begin(double recurse); -/* util.inspect's breakLength, compact (0 for false) and sorted for the +/* util.inspect's breakLength, compact (-1 for false) and sorted for the * renders that follow; the call site restores the defaults (80, 3, false) * afterwards. */ void scr_insp_layout(double break_length, double compact, bool sorted); @@ -75,6 +129,42 @@ ScrStr *scr_insp_end(ScrStr *base, ScrStr *b0, ScrStr *b1, double recurse, * caller's level (+1; borrows both). */ void scr_insp_nested_begin(void); ScrStr *scr_insp_nested_end(ScrStr *prefix, ScrStr *inner); +/* nested_end's restore without its text: the caller's context returns. */ +void scr_insp_nested_restore(void); +/* ctx.customInspect for the renders that follow; answers the previous + * value for the caller to restore. */ +bool scr_insp_custom_inspect(bool enabled); +/* Around a custom inspect function's call: begin gives the calls it makes + * a fresh inspect context with the default options and answers a token; + * end(token) restores the caller's, also after calls that threw. */ +double scr_insp_custom_call_begin(void); +void scr_insp_custom_call_end(double token); +/* How the unbounded depth of the renders that follow was given, for custom + * inspect functions: 0 Infinity, 1 null, 2 undefined. Answers the + * previous kind for the caller to restore. */ +double scr_insp_depth_kind(double kind); +/* A custom inspect function's string result re-indented at the current + * level (+1; borrows text). */ +ScrStr *scr_insp_custom_text(ScrStr *text); +/* The compiled [util.inspect.custom] calls: ctx.customInspect, the depth + * argument at recursion `recurse` of `depth` and the options object (+1 + * dyn values; options NULL with an exception pending), util.inspect as a + * function value (+1), whether a dyn result is the instance itself, and a + * dyn result's rendering at the instance's level (+1). */ +bool scr_insp_custom_enabled(void); +/* Registers a class's compiled custom inspect function under the type key + * of its instances' typed refs: fn(self, depth, options, inspect) answers + * the method's result (borrows both). */ +void scr_insp_register_custom(ScrStr *type_key, ScrDyn *fn); +ScrDyn *scr_insp_custom_depth(double recurse, double depth); +ScrDyn *scr_insp_custom_options(double depth); +ScrDyn *scr_insp_inspect_function(void); +bool scr_insp_custom_is_self(ScrDyn *result, ScrDyn *self); +ScrStr *scr_insp_custom_result(ScrDyn *result, double recurse, double depth); +/* Mark the inspector's state before a top-level render, and return to the + * mark when the render ends, normally or by an inspection refusal. */ +void scr_insp_mark(void); +void scr_insp_unwind(void); /* The buf.read* / buf.write* numeric families on u8 bytes, Node-exact: * boundsError's ladder (non-integer offset → 'It must be an integer', a @@ -115,6 +205,9 @@ ScrBytes *scr_fs_read_file_bytes(ScrStr *path); /* +1 */ * with the exception pending. */ ScrDyn *scr_fs_read_file_sync_dyn(ScrStr *path, const ScrDyn *enc); ScrStr *scr_fs_read_file_encoded(ScrStr *path, ScrStr *encoding); +/* readFileSync's encoding check (ERR_INVALID_ARG_VALUE), on its own: the + * check Node runs before it converts a URL path. */ +void scr_fs_check_read_encoding(ScrStr *encoding); ScrStr *scr_fs_read_fd_encoded(double fd, ScrStr *encoding); ScrDyn *scr_fs_callback_value(ScrStr *member); ScrDyn *scr_fs_callback_call(ScrStr *member, const ScrDyn *args); diff --git a/packages/runtime/src/scr_js.h b/packages/runtime/src/scr_js.h index 6fa20fdb8..5abab2053 100644 --- a/packages/runtime/src/scr_js.h +++ b/packages/runtime/src/scr_js.h @@ -195,10 +195,13 @@ bool scr_js_truthy(ScrJsVal value); * index and which keys an object lacks. * * Each property site of compiled dynamic-tier code owns one zeroed, - * thread-local ScrJsIc. The code at the site is fixed: it tests entry 0 - * inline and calls a probe for everything else. Probes never run user - * code: they answer from plain data properties or return a miss (NULL / - * false) with nothing done, and the site then runs its generic path. + * thread-local ScrJsIc with static storage: the reset that frees a + * thread's shapes (at thread exit, and when a library session ends) clears + * every record that holds entries. The code at the site is fixed: it tests + * entry 0 inline and calls a probe for everything else. Probes never run + * user code: they answer from plain data properties or return a miss + * (NULL / false) with nothing done, and the site then runs its generic + * path. * * load entry: shape = receiver shape; target = NULL for an own property * (slot indexes the receiver's entries), else the shape of @@ -228,6 +231,14 @@ typedef struct ScrJsIc { /* `receiver.key` for a plain data property: +1 value, or NULL. */ ScrDyn *scr_js_load_probe(ScrDyn *receiver, const ScrStr *key, ScrJsIc *ic); +/* The same for a property read site, which also answers (+1) undefined for + * a key that the receiver and its plain prototype chain lack, and caches + * that answer. A method read uses scr_js_load_probe. */ +ScrDyn *scr_js_get_probe(ScrDyn *receiver, const ScrStr *key, ScrJsIc *ic); +/* scr_js_get_probe for a site whose key is `length` or `next`, which also + * answers a native array's length and a native iterator's next + * (scr_dyn_tier_unshaped_get). */ +ScrDyn *scr_js_get_probe_native(ScrDyn *receiver, const ScrStr *key, ScrJsIc *ic); /* `receiver.key = value` (value borrowed) when it replaces a writable own * data property or adds one the prototype chain lacks: true when done. */ bool scr_js_store_probe(ScrDyn *receiver, const ScrStr *key, ScrDyn *value, ScrJsIc *ic); diff --git a/packages/runtime/src/scr_js_object.c b/packages/runtime/src/scr_js_object.c index 63929ab57..4cee9e7e8 100644 --- a/packages/runtime/src/scr_js_object.c +++ b/packages/runtime/src/scr_js_object.c @@ -18,6 +18,7 @@ #include "scr_key.h" #include +#include /* The compiler's inline cache tests read these in place (64-bit targets; * backend/llvm/js-ic.ts). */ @@ -37,10 +38,10 @@ _Static_assert(sizeof(ScrJsIcEntry) == 40 && sizeof(ScrJsIc) == 168, "cache layo * take the generic path and its hash index. */ #define SCR_JS_SHAPE_MAX 64 -/* Shapes live until the thread exits, so a thread creates at most this - * many, holding at most this many key bytes. Once either budget is spent, - * objects whose key sequence has no shape yet stay unshaped; shapes that - * exist keep serving. */ +/* Shapes live until the thread exits or a library session ends, so a thread + * creates at most this many, holding at most this many key bytes. Once + * either budget is spent, objects whose key sequence has no shape yet stay + * unshaped; shapes that exist keep serving. */ #define SCR_JS_SHAPE_BUDGET ((size_t)1 << 16) #define SCR_JS_SHAPE_KEY_BYTES_BUDGET ((size_t)4 << 20) @@ -63,12 +64,37 @@ static SCR_TL size_t scr_js_transition_count; static SCR_TL size_t scr_js_shape_key_bytes; static SCR_TL bool scr_js_shapes_registered; +/* The cache records that hold entries. Entries name shapes and prototype + * objects, so the reset that frees the shapes also clears these records: a + * library's next session runs the same sites with the same records. */ +static SCR_TL ScrJsIc **scr_js_filled_ics; +static SCR_TL size_t scr_js_filled_count; +static SCR_TL size_t scr_js_filled_capacity; + /* The shape word of an object whose key sequence has no shape within the * budgets. No cache entry names it, so its accesses take the probes, * which answer with a miss until the key set changes. */ static ScrJsShape scr_js_unshaped; +/* The second-level cache of load sites whose own record is full: one entry + * per (receiver shape, key bytes) slot, filled by the probe and checked like + * a site entry. It names shapes, so the shape reset clears it. */ +#define SCR_JS_SHARED_LOADS 2048 +typedef struct ScrJsSharedLoad { + const char *key; + ScrJsIcEntry entry; +} ScrJsSharedLoad; +static SCR_TL ScrJsSharedLoad *scr_js_shared_loads; + static void scr_js_shapes_reset(void) { + for (size_t i = 0; i < scr_js_filled_count; i++) + memset(scr_js_filled_ics[i], 0, sizeof *scr_js_filled_ics[i]); + free(scr_js_shared_loads); + scr_js_shared_loads = NULL; + free(scr_js_filled_ics); + scr_js_filled_ics = NULL; + scr_js_filled_count = 0; + scr_js_filled_capacity = 0; while (scr_js_all_shapes) { ScrJsShape *shape = scr_js_all_shapes; scr_js_all_shapes = shape->all_next; @@ -192,6 +218,16 @@ static size_t scr_js_shape_find(const ScrJsShape *shape, const char *key, size_t /* Fills a cache entry: `stale` (a matching entry whose hit failed) when * there is one, else a free or replaceable one. */ static void scr_js_ic_add(ScrJsIc *ic, uint32_t stale, ScrJsIcEntry entry) { + if (ic->count == 0) { + if (scr_js_filled_count == scr_js_filled_capacity) { + size_t capacity = scr_js_filled_capacity ? scr_js_filled_capacity * 2 : 64; + ScrJsIc **grown = realloc(scr_js_filled_ics, capacity * sizeof *grown); + if (!grown) scr_trap("scriptc: out of memory\n"); + scr_js_filled_ics = grown; + scr_js_filled_capacity = capacity; + } + scr_js_filled_ics[scr_js_filled_count++] = ic; + } uint32_t index; if (stale < ic->count) { index = stale; @@ -220,38 +256,162 @@ static ScrDyn *scr_js_load_hit(ScrDyn *receiver, const ScrJsIcEntry *entry) { return found->accessor ? NULL : scr_dyn_retain(found->value); } -ScrDyn *scr_js_load_probe(ScrDyn *receiver, const ScrStr *key, ScrJsIc *ic) { +/* An absent entry: its target is this tag and its holder is a placeholder + * that is never a prototype, so the inline test of entry 0 in compiled code + * (a prototype hit needs the receiver's prototype to be the holder) always + * sends such a receiver to the probe. Its holder_shape and slot name the one + * prototype the read walked past (shape and object), or NULL and 0 when the + * receiver had no prototype. */ +static const ScrJsShape scr_js_absent; +static const ScrDyn scr_js_absent_holder; + +/* Keys whose read on a plain object is not just its [[Get]]: the members of + * Object.prototype, which reads of an absent own property answer through + * the inherited-member path, and the error constructor read. */ +static bool scr_js_absent_cacheable(const ScrStr *key) { +#define SCR_JS_INHERITED(name) {name, sizeof name - 1} + static const struct { + const char *name; + size_t len; + } inherited[] = { + SCR_JS_INHERITED("constructor"), SCR_JS_INHERITED("__proto__"), + SCR_JS_INHERITED("toString"), SCR_JS_INHERITED("toLocaleString"), + SCR_JS_INHERITED("valueOf"), SCR_JS_INHERITED("hasOwnProperty"), + SCR_JS_INHERITED("isPrototypeOf"), SCR_JS_INHERITED("propertyIsEnumerable"), + SCR_JS_INHERITED("__defineGetter__"), SCR_JS_INHERITED("__defineSetter__"), + SCR_JS_INHERITED("__lookupGetter__"), SCR_JS_INHERITED("__lookupSetter__"), + }; +#undef SCR_JS_INHERITED + for (size_t i = 0; i < sizeof inherited / sizeof inherited[0]; i++) + if (key->len == inherited[i].len && memcmp(key->data, inherited[i].name, key->len) == 0) + return false; + return true; +} + +static ScrJsSharedLoad *scr_js_shared_load(const ScrJsShape *shape, const ScrStr *key) { + if (!scr_js_shared_loads) { + scr_js_shared_loads = calloc(SCR_JS_SHARED_LOADS, sizeof *scr_js_shared_loads); + if (!scr_js_shared_loads) scr_trap("scriptc: out of memory\n"); + } + uint64_t mixed = scr_key_mix((uint64_t)(uintptr_t)shape ^ ((uint64_t)(uintptr_t)key->data << 1)); + return &scr_js_shared_loads[mixed & (SCR_JS_SHARED_LOADS - 1)]; +} + +/* Whether an absent entry still holds for `receiver`, whose shape matched: + * its own keys lack the key, so the walked prototype (if any) must be the + * same object with the same shape, and the chain must end there. */ +static bool scr_js_absent_hit(const ScrDyn *receiver, const ScrJsIcEntry *entry) { + const ScrDyn *prototype = receiver->prototype; + const ScrDyn *walked = (const ScrDyn *)entry->slot; + if (!walked) return prototype == NULL; + return prototype == walked && walked->kind == SCR_DYN_OBJ && + walked->v.obj.shape == entry->holder_shape && walked->prototype == NULL; +} + +/* The answer of a matching entry for `receiver`, or NULL when the entry no + * longer holds. */ +static inline __attribute__((always_inline)) ScrDyn *scr_js_entry_answer(ScrDyn *receiver, + const ScrJsIcEntry *entry) { + if (entry->target == &scr_js_absent) + return scr_js_absent_hit(receiver, entry) ? scr_dyn_retain(scr_dyn_undefined()) : NULL; + return scr_js_load_hit(receiver, entry); +} + +/* The object an entry's answer depends on besides the receiver: the + * prototype it read from or walked past, or NULL for an own property. */ +static const ScrDyn *scr_js_entry_prototype(const ScrJsIcEntry *entry) { + if (entry->target == &scr_js_absent) return (const ScrDyn *)entry->slot; + return entry->target ? entry->holder : NULL; +} + +/* Records `entry` in the site and, when the site's record is full, in the + * shared slot. A matching entry whose answer failed is replaced when it + * depends on the same prototype (that prototype changed); one that names + * another prototype stays, so receivers of one shape with different + * prototypes each keep an entry. */ +static void scr_js_load_fill(ScrJsIc *ic, uint32_t stale, ScrJsSharedLoad *shared, + const ScrStr *key, ScrJsIcEntry entry) { + if (stale < ic->count && + scr_js_entry_prototype(&ic->entry[stale]) != scr_js_entry_prototype(&entry)) + stale = UINT32_MAX; + if (shared) *shared = (ScrJsSharedLoad){key->data, entry}; + scr_js_ic_add(ic, stale, entry); +} + +/* A load site's probe. With `absent`, a read of a key that the receiver and + * its chain of plain shaped objects (ending in the default prototype) lack + * answers undefined and caches that answer. Method reads leave absent + * methods to the slow path, which names the missing function. */ +static __attribute__((noinline)) ScrDyn *scr_js_probe_load(ScrDyn *receiver, const ScrStr *key, + ScrJsIc *ic, bool absent) { const ScrJsShape *shape = scr_js_shape_of(receiver); if (!shape) return NULL; uint32_t stale = UINT32_MAX; for (uint32_t i = 0; i < ic->count; i++) { if (ic->entry[i].shape != shape) continue; - ScrDyn *value = scr_js_load_hit(receiver, &ic->entry[i]); + ScrDyn *value = scr_js_entry_answer(receiver, &ic->entry[i]); if (value) return value; - stale = i; - break; + if (stale == UINT32_MAX) stale = i; + } + ScrJsSharedLoad *shared = NULL; + if (ic->count == SCR_JS_IC_WAYS) { + shared = scr_js_shared_load(shape, key); + if (shared->key == key->data && shared->entry.shape == shape && + (absent || shared->entry.target != &scr_js_absent)) { + ScrDyn *value = scr_js_entry_answer(receiver, &shared->entry); + if (value) return value; + } } size_t slot = scr_js_shape_find(shape, key->data, key->len); if (slot != SIZE_MAX) { const ScrDynEntry *found = &receiver->v.obj.entries[slot]; if (found->accessor) return NULL; - scr_js_ic_add(ic, stale, (ScrJsIcEntry){shape, NULL, NULL, NULL, slot}); + scr_js_load_fill(ic, stale, shared, key, (ScrJsIcEntry){shape, NULL, NULL, NULL, slot}); return scr_dyn_retain(found->value); } + absent = absent && scr_js_absent_cacheable(key); /* One prototype link: an ordinary object other than the builtin * Object.prototype (whose reads the generic path fences). */ ScrDyn *holder = receiver->prototype; - if (!holder) return NULL; + if (!holder) { + if (!absent) return NULL; + scr_js_load_fill(ic, stale, shared, key, + (ScrJsIcEntry){shape, &scr_js_absent, &scr_js_absent_holder, NULL, 0}); + return scr_dyn_retain(scr_dyn_undefined()); + } const ScrJsShape *holder_shape = scr_js_shape_of(holder); if (!holder_shape) return NULL; slot = scr_js_shape_find(holder_shape, key->data, key->len); - if (slot == SIZE_MAX) return NULL; + if (slot == SIZE_MAX) { + if (!absent || holder->prototype != NULL) return NULL; + scr_js_load_fill(ic, stale, shared, key, + (ScrJsIcEntry){shape, &scr_js_absent, &scr_js_absent_holder, holder_shape, + (size_t)holder}); + return scr_dyn_retain(scr_dyn_undefined()); + } const ScrDynEntry *found = &holder->v.obj.entries[slot]; if (found->accessor) return NULL; - scr_js_ic_add(ic, stale, (ScrJsIcEntry){shape, holder_shape, holder, NULL, slot}); + scr_js_load_fill(ic, stale, shared, key, (ScrJsIcEntry){shape, holder_shape, holder, NULL, slot}); return scr_dyn_retain(found->value); } +/* Receivers other than ordinary objects have no shape: they miss before + * the probe sets up its frame. */ +ScrDyn *scr_js_load_probe(ScrDyn *receiver, const ScrStr *key, ScrJsIc *ic) { + if (receiver->kind != SCR_DYN_OBJ) return NULL; + return scr_js_probe_load(receiver, key, ic, false); +} + +ScrDyn *scr_js_get_probe(ScrDyn *receiver, const ScrStr *key, ScrJsIc *ic) { + if (receiver->kind != SCR_DYN_OBJ) return NULL; + return scr_js_probe_load(receiver, key, ic, true); +} + +ScrDyn *scr_js_get_probe_native(ScrDyn *receiver, const ScrStr *key, ScrJsIc *ic) { + if (receiver->kind != SCR_DYN_OBJ) return scr_dyn_tier_unshaped_get(receiver, key); + return scr_js_probe_load(receiver, key, ic, true); +} + /* ── stores ───────────────────────────────────────────────────────────── */ /* The generic store keeps a native Error's fields in sync with these keys @@ -268,7 +428,7 @@ static bool scr_js_error_synced_key(const ScrStr *key) { static bool scr_js_replace(ScrDyn *receiver, size_t slot, ScrDyn *value) { ScrDynEntry *found = &receiver->v.obj.entries[slot]; - if (found->accessor || !found->writable) return false; + if (found->accessor || !found->writable || found->fenced) return false; scr_dyn_region_store(receiver, value); ScrDyn *old = found->value; found->value = scr_dyn_retain(value); @@ -313,7 +473,7 @@ bool scr_js_store_probe(ScrDyn *receiver, const ScrStr *key, ScrDyn *value, ScrJ size_t slot = scr_js_shape_find(shape, key->data, key->len); if (slot != SIZE_MAX) { const ScrDynEntry *found = &receiver->v.obj.entries[slot]; - if (found->accessor || !found->writable) return false; + if (found->accessor || !found->writable || found->fenced) return false; scr_js_ic_add(ic, stale, (ScrJsIcEntry){shape, shape, NULL, NULL, slot}); return scr_js_replace(receiver, slot, value); } @@ -331,6 +491,9 @@ bool scr_js_store_probe(ScrDyn *receiver, const ScrStr *key, ScrDyn *value, ScrJ /* ── keyed loads ──────────────────────────────────────────────────────── */ ScrDyn *scr_js_index_probe(ScrDyn *receiver, double index) { + /* A native array passed in from typed code answers its elements in + * place, without forming the index's key string. */ + if (receiver->kind == SCR_DYN_TYPED_REF) return scr_dyn_index_get(receiver, index); if (receiver->kind != SCR_DYN_ARR || !(index >= 0) || index >= (double)receiver->v.arr.len) return NULL; size_t i = (size_t)index; diff --git a/packages/runtime/src/scr_js_object.test.c b/packages/runtime/src/scr_js_object.test.c index a14aa9ff6..06b804759 100644 --- a/packages/runtime/src/scr_js_object.test.c +++ b/packages/runtime/src/scr_js_object.test.c @@ -24,11 +24,17 @@ static ScrDyn *object_with(const ScrStr *key, double n) { return object; } +/* Cache records have static storage, as the compiled sites' do: the reset + * at exit clears every record that holds entries. Each check starts from + * an empty record. */ +static ScrJsIc load_ic; +static ScrJsIc store_ic; + /* Whether a load probe answers `object.key` and returns `n`. */ static bool probe_reads(ScrDyn *object, const ScrStr *key, double n) { - ScrJsIc ic; - memset(&ic, 0, sizeof ic); - ScrDyn *value = scr_js_load_probe(object, key, &ic); + ScrJsIc *ic = &load_ic; + memset(ic, 0, sizeof *ic); + ScrDyn *value = scr_js_load_probe(object, key, ic); if (!value) return false; assert(value->kind == SCR_DYN_NUM && value->v.num == n); scr_dyn_release(value); @@ -66,14 +72,13 @@ static void budget_bounds_shapes(size_t key_len, size_t budget) { /* A store probe that would add a new shape leaves the add to the generic * store; one that replaces through an existing shape still runs. */ - ScrJsIc ic; - memset(&ic, 0, sizeof ic); + memset(&store_ic, 0, sizeof store_ic); ScrStr *added = scr_str_new("added", 5); ScrDyn *value = scr_dyn_new_num(8); - assert(!scr_js_store_probe(object, added, value, &ic)); + assert(!scr_js_store_probe(object, added, value, &store_ic)); assert(object->v.obj.len == 1); - memset(&ic, 0, sizeof ic); - assert(scr_js_store_probe(object, first, value, &ic)); + memset(&store_ic, 0, sizeof store_ic); + assert(scr_js_store_probe(object, first, value, &store_ic)); assert(probe_reads(object, first, 8)); scr_dyn_release(value); scr_str_release(added); @@ -90,17 +95,136 @@ static void immortal_objects_stay_unshaped(void) { object->rc = SIZE_MAX; assert(!probe_reads(object, key, 3)); assert(object->v.obj.shape == NULL); - ScrJsIc ic; - memset(&ic, 0, sizeof ic); + memset(&store_ic, 0, sizeof store_ic); ScrDyn *value = scr_dyn_new_num(4); - assert(!scr_js_store_probe(object, key, value, &ic)); + assert(!scr_js_store_probe(object, key, value, &store_ic)); assert(object->v.obj.shape == NULL); scr_dyn_release(value); scr_str_release(key); } +/* A read site's probe answers a key that the receiver and its plain + * prototype chain lack with undefined, and caches that answer until the + * receiver's keys, its prototype, or the prototype's keys change. */ +static ScrJsIc get_ic; +static ScrJsIc other_ic; + +/* Each site reads one key, so each key below has its own record. */ +static bool get_reads_undefined_at(ScrJsIc *ic, ScrDyn *object, const ScrStr *key) { + ScrDyn *value = scr_js_get_probe(object, key, ic); + if (!value) return false; + assert(value->kind == SCR_DYN_UNDEF); + scr_dyn_release(value); + return true; +} + +static bool get_reads_undefined(ScrDyn *object, const ScrStr *key) { + ScrDyn *value = scr_js_get_probe(object, key, &get_ic); + if (!value) return false; + assert(value->kind == SCR_DYN_UNDEF); + scr_dyn_release(value); + return true; +} + +static bool get_reads(ScrDyn *object, const ScrStr *key, double n) { + ScrDyn *value = scr_js_get_probe(object, key, &get_ic); + if (!value) return false; + assert(value->kind == SCR_DYN_NUM && value->v.num == n); + scr_dyn_release(value); + return true; +} + +static void absent_reads(void) { + ScrStr *own = scr_str_new("own", 3); + ScrStr *missing = scr_str_new("missing", 7); + ScrStr *inherited = scr_str_new("toString", 8); + + /* No prototype: undefined, from the cache the second time. */ + ScrDyn *object = object_with(own, 1); + memset(&get_ic, 0, sizeof get_ic); + assert(get_reads_undefined(object, missing)); + assert(get_ic.count == 1); + assert(get_reads_undefined(object, missing)); + assert(get_ic.count == 1); + /* Method reads and Object.prototype member names keep the slow path. */ + memset(&load_ic, 0, sizeof load_ic); + assert(!scr_js_load_probe(object, missing, &load_ic)); + memset(&other_ic, 0, sizeof other_ic); + assert(!get_reads_undefined_at(&other_ic, object, inherited)); + /* The key added: the same record answers its value. */ + scr_dyn_obj_set(object, missing->data, missing->len, scr_dyn_new_num(2)); + assert(get_reads(object, missing, 2)); + + /* One plain prototype that lacks the key, then gains it. */ + ScrDyn *prototype = object_with(own, 5); + ScrDyn *child = object_with(own, 6); + child->prototype = scr_dyn_retain(prototype); + memset(&get_ic, 0, sizeof get_ic); + assert(get_reads_undefined(child, missing)); + assert(get_reads_undefined(child, missing)); + scr_dyn_obj_set(prototype, missing->data, missing->len, scr_dyn_new_num(3)); + assert(get_reads(child, missing, 3)); + + /* A receiver of the same shape with another prototype that lacks the + * key: both answers stay cached. */ + ScrDyn *other_prototype = object_with(own, 7); + ScrDyn *sibling = object_with(own, 8); + sibling->prototype = scr_dyn_retain(other_prototype); + assert(get_reads_undefined(sibling, missing)); + assert(get_reads(child, missing, 3)); + assert(get_reads_undefined(sibling, missing)); + + /* A longer chain is left to the generic read, even for a receiver whose + * absent answer was cached. */ + ScrDyn *grandparent = object_with(own, 9); + other_prototype->prototype = scr_dyn_retain(grandparent); + assert(!get_reads_undefined(sibling, missing)); + + /* A prototype swapped on a cached receiver: the entry no longer holds. */ + ScrDyn *plain = object_with(own, 10); + memset(&get_ic, 0, sizeof get_ic); + assert(get_reads_undefined(plain, missing)); + plain->prototype = scr_dyn_retain(prototype); + assert(get_reads(plain, missing, 3)); + + /* More shapes than the record holds: the shared cache answers the reads + * that the full record misses, absent keys included. */ + memset(&get_ic, 0, sizeof get_ic); + memset(&other_ic, 0, sizeof other_ic); + ScrDyn *shapes[SCR_JS_IC_WAYS + 3]; + char name[16]; + for (size_t i = 0; i < sizeof shapes / sizeof shapes[0]; i++) { + int n = snprintf(name, sizeof name, "k%zu", i); + shapes[i] = object_with(own, (double)i); + scr_dyn_obj_set(shapes[i], name, (size_t)n, scr_dyn_new_num((double)i)); + } + for (int round = 0; round < 3; round++) + for (size_t i = 0; i < sizeof shapes / sizeof shapes[0]; i++) { + assert(get_reads(shapes[i], own, (double)i)); + assert(get_reads_undefined_at(&other_ic, shapes[i], missing)); + } + for (size_t i = 0; i < sizeof shapes / sizeof shapes[0]; i++) scr_dyn_release(shapes[i]); + + scr_dyn_release(plain); + scr_dyn_release(sibling); + scr_dyn_release(other_prototype); + scr_dyn_release(grandparent); + scr_dyn_release(child); + scr_dyn_release(prototype); + scr_dyn_release(object); + scr_str_release(inherited); + scr_str_release(missing); + scr_str_release(own); +} + int main(int argc, char **argv) { scr_init(); + /* Shapes count against the budget, so these checks run on their own. */ + if (argc > 1 && strcmp(argv[1], "absent") == 0) { + absent_reads(); + puts("absent read checks passed"); + return 0; + } immortal_objects_stay_unshaped(); if (argc > 1 && strcmp(argv[1], "key-bytes") == 0) { /* 64 KiB keys spend the key-byte budget after 64 shapes. */ diff --git a/packages/runtime/src/scr_js_object.test.ts b/packages/runtime/src/scr_js_object.test.ts index 0a5aa1d5b..1fb90cdaa 100644 --- a/packages/runtime/src/scr_js_object.test.ts +++ b/packages/runtime/src/scr_js_object.test.ts @@ -7,7 +7,7 @@ import { expect, test } from "vitest"; const exec = promisify(execFile); -test("dynamic-tier shapes stay within the shape and key-byte budgets", async () => { +test("dynamic-tier shapes stay within the shape and key-byte budgets, and absent reads are cached", async () => { const scratch = await mkdtemp(join(tmpdir(), "scriptc-js-object-")); const src = import.meta.dirname; const binary = join(scratch, "js-object"); @@ -50,15 +50,19 @@ test("dynamic-tier shapes stay within the shape and key-byte budgets", async () ].map((file) => join(src, file)), ...(process.platform === "linux" ? ["-D_GNU_SOURCE", "-lm"] : []), ]); - for (const args of [[], ["key-bytes"]]) { - const result = await exec(binary, args, { + for (const [args, stdout] of [ + [[], "shape budget checks passed\n"], + [["key-bytes"], "shape budget checks passed\n"], + [["absent"], "absent read checks passed\n"], + ] as const) { + const result = await exec(binary, [...args], { env: { ...process.env, ASAN_OPTIONS: "detect_leaks=0:halt_on_error=1", UBSAN_OPTIONS: "halt_on_error=1", }, }); - expect(result.stdout).toBe("shape budget checks passed\n"); + expect(result.stdout).toBe(stdout); expect(result.stderr).toBe(""); } } finally { diff --git a/packages/runtime/src/scr_js_value.c b/packages/runtime/src/scr_js_value.c index 0313fb0cd..1e2021de1 100644 --- a/packages/runtime/src/scr_js_value.c +++ b/packages/runtime/src/scr_js_value.c @@ -209,7 +209,7 @@ static double scr_js_number_arithmetic(double a, double b, const ScrStr *operati case '/': return a / b; case '%': return fmod(a, b); default: - if (operation->len == 2) return (fabs(a) == 1 && !isfinite(b)) ? NAN : pow(a, b); + if (operation->len == 2) return scr_math_pow(a, b); return a * b; } } diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index da18a1c0b..1893ec193 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -9,10 +9,10 @@ * arrays, objects (later duplicate keys win, like JS), and the * four JSON whitespace characters. Syntax errors THROW catchable * SyntaxError instances (the depth cap a RangeError, like V8's) whose - * messages are shaped like Node's V8 texts ("Unexpected end of JSON - * input", "Unexpected token 'x', \"...\" is not valid JSON") — - * APPROXIMATE message fidelity by design (SEMANTICS.md; e.name is exact, - * e.message is ours), pinned by the runtime C tests. + * messages are Node's V8 texts ("Unexpected end of JSON input", + * "Unexpected token 'x', \"...\" is not valid JSON", "... in JSON at + * position (line column )"), reported where V8 + * reports them. * - scr_dyn_check_fail is the shared failure path of the compiler-emitted * dynCheck builders: a TypeError instance carrying "expected at * , got ", thrown through the exception cell. @@ -52,6 +52,14 @@ static bool scr_dyn_error_prototype_fence(const ScrDyn *value) { SCR_TL const ScrHandleStreamHooks *scr_handle_stream_hooks = NULL; +/* Static member calls (`Math.abs(x)`) do not read the namespace objects, + * so replacing or deleting one of their function members would not reach + * those calls: the change throws instead. */ +static void scr_dyn_namespace_member_fence(void) { + static const char message[] = "Replacing or deleting a Math, JSON or Reflect member is not supported"; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC1090"); +} + /* Constructor capsules expose identity and name/length, but do not model * per-class static property tables yet. Never report a fabricated empty view. */ static bool scr_dyn_class_reflection_fence(const ScrDyn *value) { @@ -88,9 +96,13 @@ static SCR_TL ScrDyn *scr_iterator_symbol; static SCR_TL ScrDyn *scr_async_iterator_symbol; static SCR_TL ScrDyn *scr_to_primitive_symbol; static bool scr_dyn_generator(const ScrDyn *value) { - return value && value->kind == SCR_DYN_TYPED_REF && - (value->v.typed_ref.type_key[0] == 'g' || value->v.typed_ref.type_key[0] == 'a') && - (!strncmp(value->v.typed_ref.type_key, "generator<", 10) || !strncmp(value->v.typed_ref.type_key, "async-generator<", 16)); + if (!value || value->kind != SCR_DYN_TYPED_REF) return false; + /* Array keys also start with 'a': the second byte sets them apart before + * any comparison. */ + const char *key = value->v.typed_ref.type_key; + size_t len = value->v.typed_ref.type_key_len; + if (key[0] == 'g') return len >= 10 && !memcmp(key, "generator<", 10); + return key[0] == 'a' && key[1] == 's' && len >= 16 && !memcmp(key, "async-generator<", 16); } static bool scr_dyn_generator_symbol(const ScrDyn *value, const ScrDyn *key) { const ScrDyn *symbol = value->v.typed_ref.type_key[0] == 'a' ? scr_async_iterator_symbol : scr_iterator_symbol; @@ -172,6 +184,24 @@ bool scr_dyn_is_async_dispose_symbol(const ScrDyn *key) { key->v.symbol.value == scr_async_dispose_symbol->v.symbol.value; } +/* Symbol.for('nodejs.util.inspect.custom'), installed by the symbol unit + * when the program creates it: the key util.inspect reads custom inspect + * functions under. */ +static SCR_TL ScrDyn *scr_inspect_custom_symbol; + +static void scr_inspect_custom_symbol_cleanup(void) { + scr_dyn_release(scr_inspect_custom_symbol); + scr_inspect_custom_symbol = NULL; +} + +void scr_dyn_install_inspect_custom_symbol(ScrDyn *key) { + if (scr_inspect_custom_symbol) return; + scr_inspect_custom_symbol = scr_dyn_retain(key); + scr_atexit(scr_inspect_custom_symbol_cleanup); +} + +ScrDyn *scr_dyn_inspect_custom_symbol(void) { return scr_inspect_custom_symbol; } + static void scr_to_primitive_symbol_cleanup(void) { scr_dyn_release(scr_to_primitive_symbol); scr_to_primitive_symbol = NULL; @@ -202,6 +232,46 @@ void scr_dyn_install_has_instance_symbol(ScrDyn *key) { scr_atexit(scr_has_instance_symbol_cleanup); } +/* Symbol.species, installed when the program first evaluates it: no + * constructor names a species before then. */ +static SCR_TL ScrDyn *scr_species_symbol; +static void scr_species_symbol_cleanup(void) { + scr_dyn_release(scr_species_symbol); + scr_species_symbol = NULL; +} +void scr_dyn_install_species_symbol(ScrDyn *key) { + if (scr_species_symbol) return; + scr_species_symbol = scr_dyn_retain(key); + scr_atexit(scr_species_symbol_cleanup); +} + +/* The well-known symbols the program has created. They live until exit, + * so their identities are compared without a reference. */ +static SCR_TL const ScrSym *scr_well_known_symbols[24]; +static SCR_TL size_t scr_well_known_count; +void scr_dyn_install_well_known_symbol(ScrDyn *key) { + if (scr_well_known_count < sizeof scr_well_known_symbols / sizeof scr_well_known_symbols[0]) + scr_well_known_symbols[scr_well_known_count++] = key->v.symbol.value; +} +static bool scr_dyn_well_known_symbol(const ScrDyn *key) { + for (size_t i = 0; i < scr_well_known_count; i++) + if (scr_well_known_symbols[i] == key->v.symbol.value) return true; + return false; +} + +/* Symbol.toStringTag, installed when the program first evaluates it: no + * object holds the key before then. */ +static SCR_TL ScrDyn *scr_to_string_tag_symbol; +static void scr_to_string_tag_symbol_cleanup(void) { + scr_dyn_release(scr_to_string_tag_symbol); + scr_to_string_tag_symbol = NULL; +} +void scr_dyn_install_to_string_tag_symbol(ScrDyn *key) { + if (scr_to_string_tag_symbol) return; + scr_to_string_tag_symbol = scr_dyn_retain(key); + scr_atexit(scr_to_string_tag_symbol_cleanup); +} + void scr_dyn_install_iterator_symbol(ScrDyn *key) { if (scr_iterator_symbol) return; scr_iterator_symbol = scr_dyn_retain(key); @@ -825,15 +895,21 @@ static SCR_TL size_t scr_dyn_cached_key_bytes; #define SCR_DYN_KEY_CACHE_LENGTH 64 #endif -static void scr_dyn_obj_drop_keys(ScrDyn *d, bool cache) { +/* Returns the key bytes it kept for reuse, which it added to the budget; + * the pooled node records them (v.obj.cached_key_bytes) so its reuse takes + * them back without walking its slots. */ +static size_t scr_dyn_obj_drop_keys(ScrDyn *d, bool cache) { + size_t kept = 0; for (size_t i = 0; i < d->v.obj.cap; i++) { ScrDynEntry *entry = &d->v.obj.entries[i]; if (!entry->key) continue; #ifndef SCR_RC_AUDIT + size_t bytes = sizeof(ScrDynKey) + entry->key_len + 1; if (cache && d->v.obj.cap <= SCR_DYN_KEY_CACHE_SLOTS && entry->key_len <= SCR_DYN_KEY_CACHE_LENGTH && - sizeof(ScrDynKey) + entry->key_len + 1 <= SCR_DYN_KEY_CACHE_MAX - scr_dyn_cached_key_bytes) { - scr_dyn_cached_key_bytes += sizeof(ScrDynKey) + entry->key_len + 1; + bytes <= SCR_DYN_KEY_CACHE_MAX - scr_dyn_cached_key_bytes) { + scr_dyn_cached_key_bytes += bytes; + kept += bytes; continue; } #else @@ -843,6 +919,7 @@ static void scr_dyn_obj_drop_keys(ScrDyn *d, bool cache) { entry->key = NULL; entry->key_len = 0; } + return kept; } /* Primitive kinds cannot acquire properties or change kind while live, so @@ -880,6 +957,17 @@ void scr_dyn_trace_v(void *ptr, ScrTraceVisit visit, void *ctx) { } break; case SCR_DYN_FUNC: visit(d->v.fn.clo, ctx); break; + case SCR_DYN_BYTES: { + /* A view's named properties belong to its ScrBytes. Only a view this + * value alone holds owns that edge; a shared view's table stays out of + * trial deletion. Teardown reads the snapshot, not the count. */ + ScrBytes *bytes = d->v.bytes; + if (bytes->properties) { + bytes->properties_traced = bytes->rc == 1; + if (bytes->properties_traced) visit(bytes->properties, ctx); + } + break; + } case SCR_DYN_PROXY: visit(d->v.proxy.target, ctx); visit(d->v.proxy.handler, ctx); @@ -1033,15 +1121,11 @@ static ScrDyn *scr_dyn_alloc(ScrDynKind kind) { d->v.arr.region = NULL; } else if (kind == SCR_DYN_OBJ) { d->v.obj.len = 0; /* cap/entries preserved */ + /* The kept keys leave the pool's budget with the node. */ + scr_dyn_cached_key_bytes -= d->v.obj.cached_key_bytes; d->v.obj.region = NULL; d->v.obj.class_name = NULL; /* a released class prototype's name */ SCR_JS_SHAPE_RESET(d); - if (d->v.obj.cap <= SCR_DYN_KEY_CACHE_SLOTS) { - for (size_t i = 0; i < d->v.obj.cap; i++) { - ScrDynEntry *entry = &d->v.obj.entries[i]; - if (entry->key) scr_dyn_cached_key_bytes -= sizeof(ScrDynKey) + entry->key_len + 1; - } - } d->v.obj.source_identity = NULL; d->v.obj.source_access = NULL; } else if (kind != SCR_DYN_NUM && kind != SCR_DYN_STR) { @@ -1103,8 +1187,26 @@ static bool scr_dyn_to_primitive_result_is_object(const ScrDyn *d); * are never the native records the kept-source table is keyed by. */ static SCR_TL bool scr_weak_observing; static void scr_weak_disposed(void *key); +/* One bit per key address, set when a key enters the weak-key table: a + * disposed object whose bit is clear was never a key, so its disposal makes + * no call. Bits are cleared when the table empties. */ +#define SCR_WEAK_KEY_FILTER_BITS ((size_t)1 << 16) +static SCR_TL uint64_t scr_weak_key_filter[SCR_WEAK_KEY_FILTER_BITS / 64]; +static inline size_t scr_weak_key_filter_bit(const void *key) { + return ((uintptr_t)key >> 4) & (SCR_WEAK_KEY_FILTER_BITS - 1); +} +static inline bool scr_weak_maybe_key(const void *key) { + size_t bit = scr_weak_key_filter_bit(key); + return scr_weak_observing && (scr_weak_key_filter[bit >> 6] & (UINT64_C(1) << (bit & 63))); +} static inline void scr_dyn_weak_dispose(ScrDyn *d) { - if (scr_weak_observing) scr_weak_disposed(d); + if (scr_weak_maybe_key(d)) scr_weak_disposed(d); +} + +/* A disposed node's child: most getters, setters, prototypes and symbol + * tables are NULL, and the test costs less than the call. */ +static inline void scr_dyn_release_child(ScrDyn *child) { + if (child) scr_dyn_release(child); } static void scr_dyn_dispose(ScrDyn *d, bool collected) { @@ -1128,9 +1230,9 @@ static void scr_dyn_dispose(ScrDyn *d, bool collected) { d->v.obj.index = NULL; } if (d->kind == SCR_DYN_ARR || d->kind == SCR_DYN_OBJ) scr_dyn_region_leave(d); - if (!collected) scr_dyn_release(d->prototype); - if (!collected) scr_dyn_release(d->symbol_properties); - if (!collected) scr_dyn_release(d->symbol_keys); + if (!collected) scr_dyn_release_child(d->prototype); + if (!collected) scr_dyn_release_child(d->symbol_properties); + if (!collected) scr_dyn_release_child(d->symbol_keys); /* Pooled nodes keep all three NULL (scr_dyn_leaf_alloc relies on it). */ d->prototype = d->symbol_properties = d->symbol_keys = NULL; switch (d->kind) { @@ -1144,23 +1246,25 @@ static void scr_dyn_dispose(ScrDyn *d, bool collected) { scr_str_release(d->v.str); break; case SCR_DYN_BYTES: + /* The collector frees a traced table itself. */ + if (collected && d->v.bytes->properties && d->v.bytes->properties_traced) d->v.bytes->properties = NULL; scr_bytes_release(d->v.bytes); break; case SCR_DYN_ARR: free(d->v.arr.presence); d->v.arr.presence = NULL; if (!collected) { - for (size_t i = 0; i < d->v.arr.len; i++) scr_dyn_release(d->v.arr.items[i]); - scr_dyn_release(d->v.arr.properties); + for (size_t i = 0; i < d->v.arr.len; i++) scr_dyn_release_child(d->v.arr.items[i]); + scr_dyn_release_child(d->v.arr.properties); } break; case SCR_DYN_OBJ: for (size_t i = 0; i < d->v.obj.len; i++) { ScrDyn *value = d->v.obj.entries[i].value; - if (!collected || !d->v.obj.entries[i].value_traced) scr_dyn_release(value); + if (!collected || !d->v.obj.entries[i].value_traced) scr_dyn_release_child(value); if (!collected) { - scr_dyn_release(d->v.obj.entries[i].getter); - scr_dyn_release(d->v.obj.entries[i].setter); + scr_dyn_release_child(d->v.obj.entries[i].getter); + scr_dyn_release_child(d->v.obj.entries[i].setter); } d->v.obj.entries[i].value = NULL; d->v.obj.entries[i].getter = NULL; @@ -1186,7 +1290,7 @@ static void scr_dyn_dispose(ScrDyn *d, bool collected) { case SCR_DYN_TYPED_REF: scr_typed_ref_forget(d); - if (!collected) scr_dyn_release(d->v.typed_ref.materialized); + if (!collected) scr_dyn_release_child(d->v.typed_ref.materialized); while (d->v.typed_ref.casts) { ScrDynTypedCast *cast = d->v.typed_ref.casts; d->v.typed_ref.casts = cast->next; @@ -1197,8 +1301,8 @@ static void scr_dyn_dispose(ScrDyn *d, bool collected) { break; case SCR_DYN_PROXY: if (!collected) { - scr_dyn_release(d->v.proxy.target); - scr_dyn_release(d->v.proxy.handler); + scr_dyn_release_child(d->v.proxy.target); + scr_dyn_release_child(d->v.proxy.handler); } break; default: @@ -1209,7 +1313,7 @@ static void scr_dyn_dispose(ScrDyn *d, bool collected) { #endif #ifndef SCR_RC_AUDIT if (!collected && scr_dyn_free_count < SCR_DYN_FREE_MAX) { - if (d->kind == SCR_DYN_OBJ) scr_dyn_obj_drop_keys(d, true); + if (d->kind == SCR_DYN_OBJ) d->v.obj.cached_key_bytes = scr_dyn_obj_drop_keys(d, true); /* Pooled blocks retain their headers and count toward the bounded * allocated heap, but never remain in a candidate buffer. */ scr_dyn_weak_dispose(d); @@ -1235,9 +1339,9 @@ static void scr_dyn_gcfree(void *ptr) { scr_dyn_dispose(ptr, true); } static void scr_dyn_destroy(void *ptr) { scr_dyn_dispose(ptr, false); } /* The cycle bookkeeping of a value that survives a release. Compiled - * release fast paths decrement first and call this for the kinds whose - * candidacy depends on per-value state, matching scr_dyn_release: arrays - * and objects not yet buffered (skipped while their document is intact, + * release fast paths decrement first and call this for every kind that can + * be a candidate and is not one yet, matching scr_dyn_release: functions + * and proxies, arrays and objects (skipped while their document is intact, * ScrDynRegion), capsules and handles. */ void scr_dyn_release_survivor(ScrDyn *d) { if (d->kind == SCR_DYN_ARR || d->kind == SCR_DYN_OBJ) { @@ -1245,6 +1349,10 @@ void scr_dyn_release_survivor(ScrDyn *d) { if (!region || !region->intact) scr_cyc_released(d); return; } + if (d->kind == SCR_DYN_FUNC || d->kind == SCR_DYN_PROXY) { + scr_cyc_released(d); + return; + } if ((d->kind == SCR_DYN_TYPED_REF && (d->v.typed_ref.materialized || d->v.typed_ref.casts || d->v.typed_ref.traced || d->prototype || d->symbol_properties || d->symbol_keys)) || @@ -1273,10 +1381,14 @@ void scr_dyn_release(ScrDyn *d) { return; } #ifndef SCR_RC_AUDIT - /* Number and string boxes own at most one string, are never cycle - * candidates and cannot be weak keys: return them to the free list. */ + /* Number and string boxes own at most one string and cannot be weak + * keys: return them to the free list. Their releases never buffer them, + * but a collector pass can (a box that traces its payload reaches them, + * and scr_xg_release buffers what it settles), so a buffered box leaves + * its candidate buffer first. */ if ((d->kind == SCR_DYN_NUM || d->kind == SCR_DYN_STR) && !d->prototype && !d->symbol_properties && !d->symbol_keys && scr_dyn_free_count < SCR_DYN_FREE_MAX) { + scr_cyc_dead(d); if (d->kind == SCR_DYN_STR) scr_str_release(d->v.str); d->v.str = (ScrStr *)scr_dyn_free_misc; scr_dyn_free_misc = d; @@ -1422,6 +1534,24 @@ bool scr_dyn_field_eq_num(const ScrDyn *d, const ScrStr *key, double value) { return matches; } +int32_t scr_dyn_discriminant_case(const ScrDyn *d, const ScrStr *key, ScrStr *const *values, + size_t count) { + if (!d) return -1; + if (d->kind == SCR_DYN_TYPED_REF) return -2; + if (d->kind != SCR_DYN_OBJ) return -1; + for (size_t i = 0; i < d->v.obj.len; i++) { + const ScrDynEntry *entry = &d->v.obj.entries[i]; + if (entry->key_len != key->len || memcmp(entry->key, key->data, key->len) != 0) continue; + if (entry->accessor) return -2; + const ScrDyn *member = entry->value; + if (!member || member->kind != SCR_DYN_STR) return -1; + for (size_t k = 0; k < count; k++) + if (scr_str_eq(member->v.str, values[k])) return (int32_t)k; + return -1; + } + return -2; +} + bool scr_dyn_field_eq_bool(const ScrDyn *d, const ScrStr *key, bool value) { ScrDyn *member = scr_dyn_discriminant(d, key); bool matches = member && member->kind == SCR_DYN_BOOL && member->v.b == value; @@ -1704,6 +1834,21 @@ ScrDyn *scr_dyn_arr_at(const ScrDyn *d, double i) { return result; } +ScrDyn *scr_dyn_arr_custom_member(const ScrDyn *d, const char *key, size_t key_len) { + if (d->v.arr.properties && scr_dyn_obj_get(d->v.arr.properties, key, key_len)) + return scr_dyn_obj_read_receiver(d->v.arr.properties, key, key_len, d); + for (const ScrDyn *link = d->prototype; link && link != scr_builtin_array_prototype; + link = link->prototype) { + const ScrDyn *table = link->kind == SCR_DYN_OBJ ? link + : link->kind == SCR_DYN_ARR ? link->v.arr.properties + : NULL; + if (table && scr_dyn_obj_get(table, key, key_len)) + return scr_dyn_obj_read_receiver(table, key, key_len, d); + if (link->kind != SCR_DYN_OBJ && link->kind != SCR_DYN_ARR) break; + } + return NULL; +} + ScrDyn *scr_dyn_arr_named_get(const ScrDyn *d, const ScrStr *key) { if (d->v.arr.properties && scr_dyn_obj_get(d->v.arr.properties, key->data, key->len)) return scr_dyn_obj_read_receiver(d->v.arr.properties, key->data, key->len, d); @@ -1728,6 +1873,11 @@ static void scr_dyn_obj_put(ScrDyn *obj, const char *key, size_t key_len, ScrDyn ScrDynEntry *existing = obj->v.obj.index ? scr_dyn_find_entry(obj, key, key_len) : scr_dyn_find_linear_entry(obj, key, key_len); if (existing) { + if (existing->fenced) { + scr_dyn_release(value); + scr_dyn_namespace_member_fence(); + return; + } if (existing->accessor || !existing->writable) { scr_dyn_release(value); static const char message[] = "Cannot assign to read only property"; @@ -1769,6 +1919,7 @@ static void scr_dyn_obj_put(ScrDyn *obj, const char *key, size_t key_len, ScrDyn e->getter = NULL; e->setter = NULL; e->accessor = false; + e->fenced = false; e->writable = true; e->enumerable = true; e->configurable = true; @@ -1911,7 +2062,10 @@ static ScrDyn *scr_error_ctor_call(ScrClosure *closure, ScrDyn *const *args, siz ScrDyn *scr_dyn_error_constructor(const char *name) { for (size_t i = 0; i < SCR_ERROR_CTORS; i++) { - if (!scr_error_ctor_names[i] || strcmp(scr_error_ctor_names[i], name) != 0) continue; + /* The first byte rules out most names without a call. */ + if (!scr_error_ctor_names[i] || scr_error_ctor_names[i][0] != name[0] || + strcmp(scr_error_ctor_names[i], name) != 0) + continue; if (!scr_error_ctors_registered) { scr_error_ctors_registered = true; scr_atexit(scr_error_ctors_cleanup); @@ -1921,6 +2075,8 @@ ScrDyn *scr_dyn_error_constructor(const char *name) { scr_error_ctor_values[i] = scr_dyn_new_func(scr_closure_new((void *)scr_error_ctor_names[i], 0), scr_error_ctor_call, length, "native:constructor", scr_error_ctor_names[i]); + /* Error.prototype is modeled; the other error prototypes are not. */ + if (i == 0) scr_dyn_fn_seed_prototype(scr_error_ctor_values[i], scr_dyn_error_prototype()); } return scr_dyn_retain(scr_error_ctor_values[i]); } @@ -2164,6 +2320,40 @@ static bool scr_dyn_prototype_valid(const ScrDyn *prototype) { } /* A valid receiver or prototype whose kind this operation does not handle. */ +/* Compiled class prototypes. Compiled code binds a class's methods, its + * base chain and its instanceof answers when it compiles, so the + * prototype's own [[Prototype]] is fixed: scr_dyn_set_prototype refuses to + * change it. Each module-level class prototype registers when it is + * created (scr_dyn_name_class_prototype) and stays alive until its thread + * exits, as the class does. */ +static SCR_TL ScrDyn **scr_dyn_fixed_prototypes; +static SCR_TL size_t scr_dyn_fixed_count, scr_dyn_fixed_cap; + +static void scr_dyn_fixed_prototypes_cleanup(void) { + for (size_t i = 0; i < scr_dyn_fixed_count; i++) scr_dyn_release(scr_dyn_fixed_prototypes[i]); + free(scr_dyn_fixed_prototypes); + scr_dyn_fixed_prototypes = NULL; + scr_dyn_fixed_count = scr_dyn_fixed_cap = 0; +} + +static void scr_dyn_fix_prototype(ScrDyn *prototype) { + if (scr_dyn_fixed_count == scr_dyn_fixed_cap) { + if (scr_dyn_fixed_cap == 0) scr_atexit(scr_dyn_fixed_prototypes_cleanup); + size_t cap = scr_dyn_fixed_cap ? scr_dyn_fixed_cap * 2 : 8; + ScrDyn **grown = realloc(scr_dyn_fixed_prototypes, cap * sizeof *grown); + if (!grown) scr_json_oom(); + scr_dyn_fixed_prototypes = grown; + scr_dyn_fixed_cap = cap; + } + scr_dyn_fixed_prototypes[scr_dyn_fixed_count++] = scr_dyn_retain(prototype); +} + +static bool scr_dyn_prototype_fixed(const ScrDyn *object) { + for (size_t i = 0; i < scr_dyn_fixed_count; i++) + if (scr_dyn_fixed_prototypes[i] == object) return true; + return false; +} + static void scr_dyn_prototype_unsupported(const char *operation) { ScrJsonBuf b; scr_jb_init(&b); @@ -2186,8 +2376,11 @@ ScrDyn *scr_dyn_obj_create(ScrDyn *prototype) { return object; } +static bool scr_dyn_error_view_on_error_prototype(const ScrDyn *object); + ScrDyn *scr_dyn_get_prototype(ScrDyn *object) { if (object == scr_builtin_object_prototype) return scr_dyn_new_null(); + if (object->kind == SCR_DYN_OBJ && scr_dyn_error_view_on_error_prototype(object)) return scr_dyn_error_prototype(); if (scr_dyn_class_reflection_fence(object)) return NULL; if (object->kind == SCR_DYN_TYPED_REF && strncmp(object->v.typed_ref.type_key, "object:", 7)) { ScrDyn *view = scr_dyn_typed_ref_materialize(object); @@ -2280,6 +2473,11 @@ ScrDyn *scr_dyn_set_prototype(ScrDyn *object, ScrDyn *prototype) { return NULL; } if (!scr_dyn_prototype_valid(prototype)) return NULL; + if (object->kind == SCR_DYN_OBJ && scr_dyn_fixed_count && scr_dyn_prototype_fixed(object) && + !(prototype->kind == SCR_DYN_NULL ? object->null_proto : prototype == object->prototype)) { + scr_dyn_prototype_unsupported("Object.setPrototypeOf"); + return NULL; + } bool typed = scr_dyn_typed_ref_prototype(prototype); if (scr_exc_pending()) return NULL; if ((object->kind != SCR_DYN_OBJ && object->kind != SCR_DYN_ARR) || @@ -2313,15 +2511,159 @@ ScrDyn *scr_dyn_set_prototype(ScrDyn *object, ScrDyn *prototype) { return scr_dyn_retain(object); } -/* A class instance's integrity state lives in its native layout, which only - * statically typed calls can reach (the compiler adds the state to the - * hierarchies they name). Through an untyped value the operation throws. */ +/* The native record a record capsule refers to, or NULL for any other + * value. */ +static void *scr_dyn_typed_record(const ScrDyn *d) { + if (d->kind != SCR_DYN_TYPED_REF || d->v.typed_ref.type_key_len < 7 || + memcmp(d->v.typed_ref.type_key, "record:", 7) != 0) + return NULL; + return d->v.typed_ref.ptr; +} + +/* The native array an array capsule refers to, or NULL for any other + * value. */ +static ScrArr *scr_dyn_typed_array(const ScrDyn *d) { + if (d->kind != SCR_DYN_TYPED_REF || d->v.typed_ref.type_key_len < 6 || + memcmp(d->v.typed_ref.type_key, "array<", 6) != 0) + return NULL; + return d->v.typed_ref.ptr; +} + +/* The native Map or Set a handle holds, or NULL for any other value. */ +static ScrMap *scr_dyn_native_collection(const ScrDyn *d) { + if (d->kind != SCR_DYN_HANDLE || (d->v.handle.tag != SCR_DYNH_MAP && d->v.handle.tag != SCR_DYNH_SET)) + return NULL; + return d->v.handle.ptr; +} + +/* Whether a capsule's native record or array is frozen. */ +static bool scr_dyn_typed_ref_frozen(const ScrDyn *d) { + void *record = scr_dyn_typed_record(d); + if (record) return scr_rc_frozen(record); + ScrArr *array = scr_dyn_typed_array(d); + return array && scr_arr_frozen(array); +} + +/* Freezes the checked view of a frozen record or array: its fields or + * elements, and every property added through the view. */ +static void scr_dyn_record_view_freeze(ScrDyn *view) { + view->non_extensible = true; + if (view->kind == SCR_DYN_OBJ) { + for (size_t i = 0; i < view->v.obj.len; i++) { + ScrDynEntry *entry = &view->v.obj.entries[i]; + entry->configurable = false; + if (!entry->accessor) entry->writable = false; + } + } else if (view->kind == SCR_DYN_ARR) { + view->v.arr.sealed = true; + view->v.arr.frozen = true; + if (view->v.arr.properties) scr_dyn_release(scr_dyn_freeze_data(view->v.arr.properties)); + } + if (view->symbol_properties) scr_dyn_release(scr_dyn_freeze_data(view->symbol_properties)); +} + +void scr_record_freeze(void *record) { + size_t *rc = record; + if (!scr_rc_immortal(record)) *rc |= SCR_RC_FROZEN; +} + +void scr_record_throw_frozen(ScrStr *key, ScrStr *owner, double operation) { + ScrJsonBuf buffer; + scr_jb_init(&buffer); + if (operation == 1) { + scr_jb_puts(&buffer, "Cannot add property "); + scr_jb_write(&buffer, key->data, key->len); + scr_jb_puts(&buffer, ", object is not extensible"); + } else { + scr_jb_puts(&buffer, operation == 2 ? "Cannot delete property '" : "Cannot assign to read only property '"); + scr_jb_write(&buffer, key->data, key->len); + scr_jb_puts(&buffer, operation == 2 ? "' of " : "' of object '"); + scr_jb_write(&buffer, owner->data, owner->len); + if (operation != 2) scr_jb_puts(&buffer, "'"); + } + scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&buffer)); +} + +/* ── class instance attributes ─────────────────────────────────────────── + * A hierarchy passed to Object.freeze, Object.seal, Object.preventExtensions + * or Object.defineProperty keeps an integrity state per instance and an + * attribute word per redefined field (CLASS_* and FIELD_* bits in the + * compiler's ir.ts). The checked view of an instance carries them, so + * reflection over the view answers as Node does. */ +enum { + SCR_CLASS_NOT_EXTENSIBLE = 1, + SCR_CLASS_SEALED = 2, + SCR_CLASS_FROZEN = 4, + SCR_FIELD_NOT_WRITABLE = 1, + SCR_FIELD_NOT_ENUMERABLE = 2, + SCR_FIELD_NOT_CONFIGURABLE = 4, +}; + +/* Set by scr_dyn_class_view_integrity, which only the commits of + * hierarchies with an integrity word call. */ +static SCR_TL bool scr_class_integrity_committed; + +double scr_dyn_class_view_integrity(const ScrDyn *view, double bits, ScrDyn *bag) { + scr_class_integrity_committed = true; + unsigned state = (unsigned)bits; + if (view->kind != SCR_DYN_OBJ || !view->non_extensible) return state; + state |= SCR_CLASS_NOT_EXTENSIBLE; + if (bag && bag->kind == SCR_DYN_OBJ) bag->non_extensible = true; + bool sealed = true, frozen = true; + /* The string-keyed entries, then the symbol-keyed ones. */ + for (const ScrDyn *table = view; table; table = table == view ? view->symbol_properties : NULL) { + if (table->kind != SCR_DYN_OBJ) continue; + for (size_t i = 0; i < table->v.obj.len; i++) { + const ScrDynEntry *entry = &table->v.obj.entries[i]; + if (entry->configurable) sealed = false; + if (!entry->accessor && entry->writable) frozen = false; + } + } + if (sealed) state |= SCR_CLASS_SEALED; + if (sealed && frozen) state |= SCR_CLASS_FROZEN; + return state; +} + +/* Object.freeze, Object.seal or Object.preventExtensions (`operation`) of + * the class instance a capsule refers to. In a program that applies them to + * untyped values, every hierarchy that can be held there keeps an + * integrity word (class-attributes.ts), and its commit stores the + * integrity state of the instance's checked view in the word + * (scr_dyn_class_view_integrity). The operation applies to the view, and + * the commit records it. A first commit of the unchanged view finds out + * whether the hierarchy has the word. Answers false for other values and + * for instances whose hierarchy has no word, whose state stays as it was, + * and true when the operation applied or threw. */ +static bool scr_dyn_class_integrity(ScrDyn *object, ScrDyn *(*operation)(ScrDyn *)) { + if (object->kind != SCR_DYN_TYPED_REF || !object->v.typed_ref.commit || + object->v.typed_ref.type_key_len < 7 || memcmp(object->v.typed_ref.type_key, "object:", 7) != 0) + return false; + ScrDyn *view = scr_dyn_typed_ref_materialize(object); + bool word = false; + if (!scr_exc_pending()) { + scr_class_integrity_committed = false; + scr_dyn_typed_ref_commit(object); + word = scr_class_integrity_committed; + } + if (word && !scr_exc_pending()) { + scr_dyn_release(operation(view)); + if (!scr_exc_pending()) scr_dyn_typed_ref_commit(object); + } + scr_dyn_release(view); + return word || scr_exc_pending(); +} + +/* A class instance's integrity state lives in its native layout. Through an + * untyped value whose hierarchy has no integrity word, the operation + * throws. */ static bool scr_dyn_class_integrity_fence(const ScrDyn *object, const char *operation) { if (object->kind != SCR_DYN_TYPED_REF) return false; ScrJsonBuf buffer; scr_jb_init(&buffer); scr_jb_puts(&buffer, operation); - scr_jb_puts(&buffer, " of a class instance held in an untyped value is not supported yet"); + scr_jb_puts(&buffer, scr_dyn_typed_record(object) ? " of an object held in an untyped value is not supported yet" + : scr_dyn_typed_array(object) ? " of an array held in an untyped value is not supported yet" + : " of a class instance held in an untyped value is not supported yet"); ScrStr *message = scr_jb_finish(&buffer); scr_throw_error_msg_code(SCR_ERR_ERROR, message->data, message->len, "SC2020"); scr_str_release(message); @@ -2339,10 +2681,27 @@ static bool scr_dyn_typed_ref_view_answer(ScrDyn *object, bool (*query)(const Sc return true; } +static ScrDyn *scr_bytes_properties(const ScrDyn *v); static bool scr_dyn_is_extensible_view(const ScrDyn *view) { + if (view->kind == SCR_DYN_BYTES) return !view->v.bytes->properties || !view->v.bytes->properties->non_extensible; return (view->kind == SCR_DYN_OBJ || view->kind == SCR_DYN_ARR) && !view->non_extensible; } +/* A view's integrity state lives on its named-property table. A typed array + * with elements cannot be frozen, and cannot be sealed while extensible: as + * in V8, the named properties are sealed or frozen and the view made + * non-extensible before the TypeError. */ +static bool scr_bytes_integrity_fence(const ScrDyn *view, const char *verb) { + if (view->v.bytes->is_data_view || view->v.bytes->len == 0) return false; + ScrJsonBuf message; + scr_jb_init(&message); + scr_jb_puts(&message, "Cannot "); + scr_jb_puts(&message, verb); + scr_jb_puts(&message, " array buffer views with elements"); + scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&message)); + return true; +} + /* A plain function's own properties (name, length, prototype, and the ones * a program adds) live in its property table, which carries the function's * integrity state too: freezing or sealing the function freezes or seals @@ -2366,9 +2725,20 @@ static ScrDyn *scr_dyn_fn_existing_table(const ScrDyn *function) { return owner->props ? (ScrDyn *)scr_box_get_ref(owner->props) : NULL; } +ScrDyn *scr_dyn_function_properties_existing(const ScrDyn *function) { + return scr_dyn_fn_existing_table(function); +} + ScrDyn *scr_dyn_prevent_extensions(ScrDyn *object) { if (scr_dyn_class_reflection_fence(object) || scr_dyn_error_prototype_fence(object)) return NULL; if (object->kind == SCR_DYN_PROXY) { scr_dyn_proxy_unsupported("Object.preventExtensions"); return NULL; } + ScrMap *collection = scr_dyn_native_collection(object); + if (collection) { + scr_map_prevent_extensions(collection); + return scr_dyn_retain(object); + } + if (scr_dyn_class_integrity(object, scr_dyn_prevent_extensions)) + return scr_exc_pending() ? NULL : scr_dyn_retain(object); if (scr_dyn_class_integrity_fence(object, "Object.preventExtensions")) return NULL; ScrDyn *table = scr_dyn_fn_integrity_table(object); if (table) { @@ -2380,6 +2750,7 @@ ScrDyn *scr_dyn_prevent_extensions(ScrDyn *object) { if (object->kind == SCR_DYN_ARR && object->v.arr.properties) object->v.arr.properties->non_extensible = true; } + if (object->kind == SCR_DYN_BYTES) scr_bytes_properties(object)->non_extensible = true; return scr_dyn_retain(object); } @@ -2388,6 +2759,8 @@ bool scr_dyn_is_extensible(ScrDyn *object) { if (object->kind == SCR_DYN_PROXY) { scr_dyn_proxy_unsupported("Object.isExtensible"); return false; } bool answer; if (scr_dyn_typed_ref_view_answer(object, scr_dyn_is_extensible_view, &answer)) return answer; + ScrMap *collection = scr_dyn_native_collection(object); + if (collection) return scr_map_is_extensible(collection); if (scr_dyn_plain_fn(object)) { ScrDyn *table = scr_dyn_fn_existing_table(object); answer = !table || !table->non_extensible; @@ -2400,7 +2773,20 @@ bool scr_dyn_is_extensible(ScrDyn *object) { ScrDyn *scr_dyn_seal(ScrDyn *object) { if (scr_dyn_class_reflection_fence(object) || scr_dyn_error_prototype_fence(object)) return NULL; if (object->kind == SCR_DYN_PROXY) { scr_dyn_proxy_unsupported("Object.seal"); return NULL; } + ScrMap *collection = scr_dyn_native_collection(object); + if (collection) { + scr_map_prevent_extensions(collection); + return scr_dyn_retain(object); + } + if (scr_dyn_class_integrity(object, scr_dyn_seal)) + return scr_exc_pending() ? NULL : scr_dyn_retain(object); if (scr_dyn_class_integrity_fence(object, "Object.seal")) return NULL; + if (object->kind == SCR_DYN_BYTES) { + bool extensible = scr_dyn_is_extensible_view(object); + scr_dyn_release(scr_dyn_seal(scr_bytes_properties(object))); + if (extensible && scr_bytes_integrity_fence(object, "seal")) return NULL; + return scr_dyn_retain(object); + } if (object->symbol_properties) scr_dyn_release(scr_dyn_seal(object->symbol_properties)); ScrDyn *table = scr_dyn_fn_integrity_table(object); if (table) { @@ -2424,6 +2810,8 @@ bool scr_dyn_is_sealed(const ScrDyn *object) { if (object->kind == SCR_DYN_PROXY) { scr_dyn_proxy_unsupported("Object.isSealed"); return false; } bool answer; if (scr_dyn_typed_ref_view_answer((ScrDyn *)object, scr_dyn_is_sealed, &answer)) return answer; + ScrMap *collection = scr_dyn_native_collection(object); + if (collection) return !scr_map_is_extensible(collection); if (object->symbol_properties && !scr_dyn_is_sealed(object->symbol_properties)) return false; if (scr_dyn_plain_fn(object)) { ScrDyn *table = scr_dyn_fn_existing_table(object); @@ -2434,6 +2822,8 @@ bool scr_dyn_is_sealed(const ScrDyn *object) { if (object->kind == SCR_DYN_ARR) return object->non_extensible && (object->v.arr.len == 0 || object->v.arr.sealed) && (!object->v.arr.properties || scr_dyn_is_sealed(object->v.arr.properties)); + /* V8 answers from a typed array's named properties alone. */ + if (object->kind == SCR_DYN_BYTES) return object->v.bytes->properties && scr_dyn_is_sealed(object->v.bytes->properties); if (object->kind != SCR_DYN_OBJ) return true; if (!object->non_extensible) return false; for (size_t i = 0; i < object->v.obj.len; i++) if (object->v.obj.entries[i].configurable) return false; @@ -2640,6 +3030,65 @@ static bool scr_dyn_data_view_proto_name(const ScrStr *key) { return scr_dyn_name_in(key, names, sizeof names / sizeof names[0]); } +/* A canonical numeric string ("1", "-0", "1.5", "Infinity"): typed arrays + * answer those keys as element indices, never as named properties. */ +static bool scr_bytes_numeric_key(const ScrStr *key, double *index) { + if (key->len == 2 && memcmp(key->data, "-0", 2) == 0) { *index = -0.0; return true; } + *index = scr_string_to_number(key); + char spelling[32]; + size_t length = scr_f64_to_str(*index, spelling); + return length == key->len && memcmp(spelling, key->data, length) == 0; +} + +/* Whether the view has an own named property `key`. */ +static bool scr_bytes_has_named(const ScrDyn *v, const ScrStr *key) { + const ScrDyn *properties = v->v.bytes->properties; + return properties && scr_dyn_obj_get(properties, key->data, key->len); +} + +/* The view's own named property `key` read with the view as a getter's + * `this` (+1), or NULL without an exception when the view has none. Only + * reads the table, so callers do not link the typed array methods. */ +ScrDyn *scr_bytes_named_get(const ScrDyn *view, const char *key, size_t key_len) { + const ScrDyn *properties = view->v.bytes->properties; + if (!properties || !scr_dyn_obj_get(properties, key, key_len)) return NULL; + return scr_dyn_obj_read_receiver(properties, key, key_len, view); +} + +/* The view's named-property table, created on first use. Borrowed. The + * table belongs to the ScrBytes, which several checked values can wrap. */ +static ScrDyn *scr_bytes_properties(const ScrDyn *v) { + ScrBytes *bytes = v->v.bytes; + if (!bytes->properties) bytes->properties = scr_dyn_new_obj_null_proto(); + return bytes->properties; +} + +/* Accessors without a setter on the view's prototype chain: writing one + * that the view does not own throws V8's strict-mode TypeError. */ +static bool scr_bytes_getter_name(const ScrDyn *v, const ScrStr *key) { + static const char *const names[] = { "byteLength", "byteOffset", "buffer", "length", "parent", "offset" }; + size_t count = v->v.bytes->is_data_view ? 3 : v->buffer ? 6 : 4; + return scr_dyn_name_in(key, names, count); +} + +/* Keys that the runtime's conversions, iteration and printing of a view + * read from its prototype without consulting its own properties. */ +static bool scr_bytes_protocol_name(const ScrStr *key) { + static const char *const names[] = { "toString", "valueOf", "toLocaleString", "toJSON", "join" }; + return scr_dyn_name_in(key, names, sizeof names / sizeof names[0]); +} + +static void scr_bytes_protocol_fence(const char *key, size_t key_len) { + ScrJsonBuf message; + scr_jb_init(&message); + scr_jb_puts(&message, "defining "); + scr_jb_write(&message, key, key_len); + scr_jb_puts(&message, " on a typed array, Buffer or DataView is not supported yet"); + ScrStr *text = scr_jb_finish(&message); + scr_throw_error_msg_code(SCR_ERR_ERROR, text->data, text->len, "SC2020"); + scr_str_release(text); +} + /* `in` on a typed array, Buffer or DataView. On a typed array, a canonical * numeric key (one that ToString(ToNumber(key)) reproduces, or "-0") * answers by bounds alone: an integral index, not -0, below the length. @@ -2647,6 +3096,7 @@ static bool scr_dyn_data_view_proto_name(const ScrStr *key) { * Buffer, or Object.prototype. A DataView has no indexed elements: its keys * name members of DataView.prototype or Object.prototype. */ static bool scr_dyn_bytes_in(const ScrDyn *v, const ScrStr *key) { + if (scr_bytes_has_named(v, key)) return true; if (v->v.bytes->is_data_view) return scr_dyn_data_view_proto_name(key) || scr_dyn_object_proto_name(key); if (key->len == 2 && memcmp(key->data, "-0", 2) == 0) return false; double index = scr_string_to_number(key); @@ -2762,6 +3212,19 @@ ScrDyn *scr_dyn_own_descriptor(const ScrDyn *value, const ScrStr *key) { scr_dyn_obj_set(out, "configurable", 12, scr_dyn_new_bool(false)); return out; } + if (value->kind == SCR_DYN_BYTES && scr_bytes_has_named(value, key)) + return scr_dyn_own_descriptor(value->v.bytes->properties, key); + if (value->kind == SCR_DYN_BYTES && !value->v.bytes->is_data_view) { + if (!scr_dyn_canonical_own_index(key, value->v.bytes->len)) return scr_dyn_retain(scr_dyn_undefined()); + size_t index = 0; + for (size_t i = 0; i < key->len; i++) index = index * 10 + (size_t)(key->data[i] - '0'); + ScrDyn *out = scr_dyn_new_obj(); + scr_dyn_obj_set(out, "value", 5, scr_dyn_new_num(scr_bytes_get(value->v.bytes, (double)index))); + scr_dyn_obj_set(out, "writable", 8, scr_dyn_new_bool(true)); + scr_dyn_obj_set(out, "enumerable", 10, scr_dyn_new_bool(true)); + scr_dyn_obj_set(out, "configurable", 12, scr_dyn_new_bool(true)); + return out; + } if (value->kind == SCR_DYN_ARR) { if (value->v.arr.properties && scr_dyn_obj_get(value->v.arr.properties, key->data, key->len)) @@ -3054,7 +3517,7 @@ static void scr_record_source_disposed(void *rec) { /* The single native dispose hook serves weak collections and the table * above; each observer runs only while it has entries to forget. */ static void scr_dispose_observers(void *object) { - if (scr_weak_observing) scr_weak_disposed(object); + if (scr_weak_maybe_key(object)) scr_weak_disposed(object); if (scr_record_source_count) scr_record_source_disposed(object); } @@ -3191,6 +3654,45 @@ void scr_record_source_note(void *rec, const ScrDyn *src, const char *const *nam scr_record_source_put(&probe); } +void scr_record_source_spread(void *dst, const void *src, const char *const *names, + const size_t *lens, size_t count) { + if (!scr_record_source_count || !dst || !src) return; + ScrRecordSource *found = scr_record_source_find(src); + if (!found) return; + ScrRecordSource probe = { dst, NULL, (const char *)names, (const char *)lens, + sizeof *names, sizeof *lens, count }; + /* Nothing to keep when the declared keys come in the table's order and + * the keys src lacks would follow them in it anyway. */ + const ScrDyn *from = found->view; + bool keep = false; + size_t next = 0; + for (size_t i = 0; i < from->v.obj.len && !keep; i++) { + const ScrDynEntry *entry = &from->v.obj.entries[i]; + size_t index = scr_record_source_index(&probe, entry->key, entry->key_len); + if (index == count) continue; + keep = index != next; + next++; + } + if (!keep) return; + ScrDyn *view = scr_dyn_new_obj(); + for (size_t i = 0; i < from->v.obj.len; i++) { + const ScrDynEntry *entry = &from->v.obj.entries[i]; + if (scr_record_source_declared(&probe, entry->key, entry->key_len)) + scr_dyn_obj_set(view, entry->key, entry->key_len, scr_dyn_retain(scr_dyn_undefined())); + } + /* The keys src lacks follow in the table's order. The view lists them + * too, so an integer key among them comes first in its own-key order, as + * the literal creates it. */ + for (size_t i = 0; i < count; i++) { + if (scr_dyn_obj_get(view, names[i], lens[i])) continue; + scr_dyn_obj_set(view, names[i], lens[i], scr_dyn_retain(scr_dyn_undefined())); + } + ScrRecordSource *old = scr_record_source_find(dst); + if (old) scr_record_source_drop(old); + probe.view = view; + scr_record_source_put(&probe); +} + void scr_record_source_move(void *from, void *to) { if (!scr_record_source_count) return; ScrRecordSource *old = scr_record_source_find(to); @@ -3649,12 +4151,14 @@ ScrDyn *scr_dyn_typed_ref_materialize(const ScrDyn *d) { fresh->rc = fresh_rc; scr_dyn_release(fresh); } + if (scr_dyn_typed_ref_frozen(capsule)) scr_dyn_record_view_freeze(capsule->v.typed_ref.materialized); return scr_dyn_retain(capsule->v.typed_ref.materialized); } void scr_dyn_typed_ref_commit(ScrDyn *d) { + /* A frozen record's or array's view cannot have changed its contents. */ if (d && d->kind == SCR_DYN_TYPED_REF && d->v.typed_ref.commit && - d->v.typed_ref.materialized) { + d->v.typed_ref.materialized && !scr_dyn_typed_ref_frozen(d)) { d->v.typed_ref.commit(d->v.typed_ref.ptr, d->v.typed_ref.materialized); } @@ -3790,6 +4294,12 @@ static bool scr_dyn_name_in_strs(const ScrStr *key, ScrStr *const *names, size_t return false; } +ScrStr *scr_dyn_typed_ref_class_tag(const ScrDyn *d) { + if (!d || d->kind != SCR_DYN_TYPED_REF || d->prototype) return NULL; + const ScrDynTypedClassOps *ops = scr_typed_ref_class_ops(d); + return ops && ops->string_tag ? ops->string_tag(d->v.typed_ref.ptr) : NULL; +} + bool scr_dyn_typed_ref_class_has(const ScrDyn *d, const ScrStr *key) { if (!d || d->kind != SCR_DYN_TYPED_REF || d->prototype) return false; const ScrDynTypedClassOps *ops = scr_typed_ref_class_ops(d); @@ -3860,6 +4370,7 @@ ScrDyn *scr_dyn_typed_ref_element(const ScrDyn *d, size_t index) { double state = scr_arr_state(arr, (double)index); if (state == (double)SCR_ARR_VALUE) return scr_dyn_typed_ref_array_ops(d)->get(arr, (double)index); if (state == (double)SCR_ARR_UNDEFINED) return scr_dyn_retain(scr_dyn_undefined()); + if (state == (double)SCR_ARR_FOREIGN) return scr_arr_get_foreign(arr, (double)index); return NULL; } @@ -3878,6 +4389,9 @@ ScrDyn *scr_dyn_index_get(const ScrDyn *d, double index) { return scr_dyn_typed_ref_element(d, i); } +static ScrStr *scr_dyn_typed_ref_conversion_tag(const ScrDyn *d); +static ScrStr *scr_dyn_tag_string(const ScrStr *tag); + ScrDyn *scr_dyn_typed_ref_string_view(const ScrDyn *d) { /* An instance of a class declared `extends Date` converts through its * toString, declared or inherited. With an exception pending the view is @@ -3891,6 +4405,16 @@ ScrDyn *scr_dyn_typed_ref_string_view(const ScrDyn *d) { scr_str_release(empty); return out; } + /* A class instance converting through Object.prototype.toString shows + * its Symbol.toStringTag getter's string. */ + ScrStr *class_tag = scr_dyn_typed_ref_conversion_tag(d); + if (class_tag || scr_exc_pending()) { + ScrStr *text = class_tag ? scr_dyn_tag_string(class_tag) : scr_str_new("", 0); + scr_str_release(class_tag); + ScrDyn *out = scr_dyn_new_str(text); + scr_str_release(text); + return out; + } ScrDyn *view = scr_dyn_typed_ref_materialize(d); const char *tag = scr_event_ref_hooks ? scr_event_ref_hooks->string_tag(d) : NULL; if (!tag || view->kind != SCR_DYN_OBJ || scr_dyn_property_owner(view, "toString", 8)) return view; @@ -3961,6 +4485,8 @@ static bool scr_typed_ref_array_has_own(const ScrDyn *d, const ScrStr *key) { static bool scr_typed_ref_array_set(ScrDyn *d, const ScrStr *key, const ScrDyn *value) { const ScrDynTypedArrayOps *ops = scr_dyn_typed_ref_array_ops(d); if (!ops || !ops->set) return false; + /* A frozen array's view refuses the write. */ + if (scr_arr_frozen(d->v.typed_ref.ptr)) return false; /* Numeric properties live in the native array's property table, which * holds values of the element type. */ double number; @@ -4114,6 +4640,212 @@ static ScrDyn *scr_bytes_ctor_call(ScrClosure *closure, ScrDyn *const *args, siz scr_trap("scriptc: invalid typed array constructor\n"); } static ScrDyn *scr_dyn_fn_properties(const ScrDyn *function); + +/* The element kind a typed array constructor value makes, or -1. */ +static int scr_bytes_ctor_elem(const ScrDyn *callee) { + for (size_t i = 0; i < SCR_BYTES_ELEM_COUNT; i++) + if (callee->kind == SCR_DYN_FUNC && scr_bytes_ctors[i] && callee->v.fn.clo == scr_bytes_ctors[i]->v.fn.clo) return (int)i; + return -1; +} + +ScrDyn *scr_dyn_iterator_method(const ScrDyn *value) { + if (!scr_iterator_symbol || value->kind != SCR_DYN_OBJ) return NULL; + ScrDyn *method = scr_dyn_symbol_key_get(value, scr_iterator_symbol, true); + if (method && (method->kind == SCR_DYN_UNDEF || method->kind == SCR_DYN_NULL)) { + scr_dyn_release(method); + return NULL; + } + return method; +} + +ScrDyn *scr_dyn_iterate_with(const ScrDyn *source, ScrDyn *method) { + if (!scr_dyn_is_callable(method)) { + static const char message[] = "Symbol.iterator is not a function"; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + return NULL; + } + scr_dyn_this_push_dyn(source); + ScrDyn *iterator = scr_dyn_call(method, NULL, 0, "[Symbol.iterator]"); + scr_dyn_this_pop(); + if (!iterator) return NULL; + if (!scr_iterator_object(iterator)) { + scr_dyn_release(iterator); + static const char message[] = "Result of the Symbol.iterator method is not an object"; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + return NULL; + } + /* A generator or native iterator drains through its own iteration. */ + if (scr_builtin_iterable(iterator)) { + ScrDyn *drained = scr_dyn_destructure_rest(iterator, NULL); + scr_dyn_release(iterator); + return drained; + } + ScrDyn *next = scr_dyn_iterator_result_get(iterator, "next", 4); + ScrDyn *values = next ? scr_dyn_new_arr() : NULL; + while (values) { + scr_dyn_this_push_dyn(iterator); + ScrDyn *result = scr_dyn_call(next, NULL, 0, "next"); + scr_dyn_this_pop(); + ScrDyn *checked = result ? scr_dyn_iterator_result(result) : NULL; + scr_dyn_release(result); + if (!checked) { scr_dyn_release(values); values = NULL; break; } + ScrDyn *done = scr_dyn_iterator_result_get(checked, "done", 4); + bool finished = done && scr_dyn_truthy(done); + scr_dyn_release(done); + ScrDyn *value = !scr_exc_pending() && !finished ? scr_dyn_iterator_result_get(checked, "value", 5) : NULL; + scr_dyn_release(checked); + if (scr_exc_pending()) { scr_dyn_release(value); scr_dyn_release(values); values = NULL; break; } + if (finished) break; + scr_dyn_arr_push(values, value); + } + scr_dyn_release(next); + scr_dyn_release(iterator); + return values; +} + +/* The values of `source` when it is iterable (+1): a builtin iterable's + * own iteration, or an object's Symbol.iterator method, read once. NULL + * for an array-like source, or with the exception pending. */ +static ScrDyn *scr_dyn_from_iterated(const ScrDyn *source, bool *iterable) { + *iterable = true; + if (scr_builtin_iterable(source)) return scr_dyn_destructure_rest(source, NULL); + ScrDyn *method = scr_dyn_iterator_method(source); + if (!method) { *iterable = false; return NULL; } + ScrDyn *values = scr_dyn_iterate_with(source, method); + scr_dyn_release(method); + return values; +} + +/* TypedArrayCreate(C, [n]) for TypedArray.from and TypedArray.of: a typed + * array constructor makes one directly; any other constructor is called + * and must answer a typed array of at least n elements. */ +static ScrDyn *scr_bytes_static_create(ScrDyn *constructor, size_t n) { + int elem = scr_bytes_ctor_elem(constructor); + if (elem >= 0) { + ScrBytes *out = scr_bytes_new((ScrBytesElem)elem, (double)n); + ScrDyn *result = scr_dyn_new_bytes(out); + scr_bytes_release(out); + return result; + } + if (!scr_dyn_is_callable(constructor)) { + static const char message[] = "this is not a constructor"; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + return NULL; + } + ScrDyn *args = scr_dyn_new_arr(); + scr_dyn_arr_push(args, scr_dyn_new_num((double)n)); + ScrStr *what = scr_str_new("TypedArray species", 18); + ScrDyn *result = scr_dyn_construct(constructor, args, what); + scr_str_release(what); + scr_dyn_release(args); + if (!result) return NULL; + const char *problem = result->kind != SCR_DYN_BYTES || result->v.bytes->is_data_view + ? "this is not a typed array." + : result->v.bytes->len < n ? "Derived TypedArray constructor created an array which was too small" : NULL; + if (problem) { + scr_dyn_release(result); + scr_throw_error_msg(SCR_ERR_TYPE, problem, strlen(problem)); + return NULL; + } + return result; +} + +/* target[k] = value, converting with ToNumber as a typed array [[Set]] does. */ +static bool scr_bytes_static_set(ScrDyn *target, size_t k, const ScrDyn *value) { + double v; + if (!scr_dyn_number_coerce_js(value, &v)) return false; + if (k < target->v.bytes->len) scr_bytes_set(target->v.bytes, (double)k, v); + return true; +} + +/* mapfn(value, k) with thisArg, or the value itself (+1). */ +static ScrDyn *scr_bytes_static_map(ScrDyn *mapfn, ScrDyn *this_arg, ScrDyn *value, size_t k) { + if (!mapfn) return scr_dyn_retain(value); + ScrDyn *index = scr_dyn_new_num((double)k); + ScrDyn *cbargs[2] = { value, index }; + scr_dyn_this_push_dyn(this_arg); + ScrDyn *mapped = scr_dyn_call(mapfn, cbargs, 2, "mapfn"); + scr_dyn_this_pop(); + scr_dyn_release(index); + return mapped; +} + +/* TypedArray.from(source, mapfn, thisArg): an iterable source is drained + * first, then the target is created; an array-like source is read element + * by element after the target exists. */ +static ScrDyn *scr_bytes_from_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; + ScrDyn *constructor = scr_dyn_this_get(); + ScrDyn *result = NULL, *values = NULL; + ScrDyn *source = argc ? args[0] : scr_dyn_undefined(); + ScrDyn *mapfn = argc > 1 && args[1]->kind != SCR_DYN_UNDEF ? args[1] : NULL; + ScrDyn *this_arg = argc > 2 ? args[2] : scr_dyn_undefined(); + if (scr_bytes_ctor_elem(constructor) < 0 && !scr_dyn_is_callable(constructor)) { + static const char message[] = "this is not a constructor"; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + goto done; + } + if (mapfn && !scr_dyn_is_callable(mapfn)) { + static const char message[] = "TypedArray.from: the mapping function is not callable"; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + goto done; + } + if (source->kind == SCR_DYN_UNDEF || source->kind == SCR_DYN_NULL) { + static const char message[] = "Cannot convert undefined or null to object"; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + goto done; + } + bool iterable; + values = scr_dyn_from_iterated(source, &iterable); + if (iterable) { + if (!values) goto done; + size_t n = values->v.arr.len; + result = scr_bytes_static_create(constructor, n); + for (size_t k = 0; result && k < n; k++) { + ScrDyn *mapped = scr_bytes_static_map(mapfn, this_arg, values->v.arr.items[k], k); + bool ok = mapped && scr_bytes_static_set(result, k, mapped); + scr_dyn_release(mapped); + if (!ok) { scr_dyn_release(result); result = NULL; } + } + goto done; + } + if (scr_exc_pending()) goto done; + double n = 0; + if (source->kind == SCR_DYN_OBJ) { + ScrDyn *length = scr_dyn_obj_read(source, "length", 6); + bool ok = !scr_exc_pending() && (!length || scr_dyn_number_coerce_js(length, &n)); + scr_dyn_release(length); + if (!ok) goto done; + n = isnan(n) || n < 0 ? 0 : n > 9007199254740991.0 ? 9007199254740991.0 : trunc(n); + } + result = scr_bytes_static_create(constructor, (size_t)n); + for (size_t k = 0; result && k < (size_t)n; k++) { + char key[32]; + int size = snprintf(key, sizeof key, "%zu", k); + ScrDyn *item = scr_dyn_obj_read(source, key, (size_t)size); + ScrDyn *mapped = scr_exc_pending() ? NULL : scr_bytes_static_map(mapfn, this_arg, item ? item : scr_dyn_undefined(), k); + bool ok = mapped && scr_bytes_static_set(result, k, mapped); + scr_dyn_release(item); + scr_dyn_release(mapped); + if (!ok) { scr_dyn_release(result); result = NULL; } + } +done: + scr_dyn_release(values); + scr_dyn_release(constructor); + return result; +} + +/* TypedArray.of(...items). */ +static ScrDyn *scr_bytes_of_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; + ScrDyn *constructor = scr_dyn_this_get(); + ScrDyn *result = scr_bytes_static_create(constructor, argc); + for (size_t k = 0; result && k < argc; k++) + if (!scr_bytes_static_set(result, k, args[k])) { scr_dyn_release(result); result = NULL; } + scr_dyn_release(constructor); + return result; +} + static ScrDyn *scr_bytes_constructor_for(ScrBytesElem elem) { if (!scr_bytes_ctors[elem]) { bool initialized = false; @@ -4125,6 +4857,14 @@ static ScrDyn *scr_bytes_constructor_for(ScrBytesElem elem) { scr_dyn_obj_set(properties, "BYTES_PER_ELEMENT", 17, scr_dyn_new_num((double)scr_bytes_elem_size(elem))); ScrDynEntry *entry = &properties->v.obj.entries[properties->v.obj.len - 1]; entry->writable = entry->enumerable = entry->configurable = false; + /* from and of live on %TypedArray%, which is not modeled: each + * constructor carries them as non-enumerable properties. */ + scr_dyn_obj_set(properties, "from", 4, + scr_dyn_new_func(scr_closure_new(NULL, 0), scr_bytes_from_call, 1, "native:TypedArray", "from")); + properties->v.obj.entries[properties->v.obj.len - 1].enumerable = false; + scr_dyn_obj_set(properties, "of", 2, + scr_dyn_new_func(scr_closure_new(NULL, 0), scr_bytes_of_call, 0, "native:TypedArray", "of")); + properties->v.obj.entries[properties->v.obj.len - 1].enumerable = false; scr_dyn_release(properties); } return scr_dyn_retain(scr_bytes_ctors[elem]); @@ -4167,6 +4907,7 @@ static ScrDyn *scr_array_ctor_call(ScrClosure *closure, ScrDyn *const *args, siz ScrDyn *scr_dyn_array_constructor(void) { if (!scr_array_ctor) { scr_array_ctor = scr_dyn_new_func(scr_closure_new(NULL, 0), scr_array_ctor_call, 1, "native:Array", "Array"); + scr_dyn_fn_seed_prototype(scr_array_ctor, scr_dyn_array_prototype_base()); scr_atexit(scr_array_ctor_cleanup); } return scr_dyn_retain(scr_array_ctor); @@ -4252,6 +4993,115 @@ int scr_dyn_has_instance_hook(const ScrDyn *value, const ScrDyn *callee) { return truthy ? 1 : 0; } +/* `value instanceof Object`: whether Object.prototype is on the value's + * prototype chain. Primitives answer false, and so does an object whose + * chain ends in null first (Object.create(null), a null-prototype record). + * Objects without a chain the runtime walks (class instances, native + * handles, errors, promises) inherit Object.prototype. False with the + * exception pending when a prototype read throws. */ +static bool scr_dyn_instanceof_object(const ScrDyn *value) { + switch (value->kind) { + case SCR_DYN_UNDEF: + case SCR_DYN_NULL: + case SCR_DYN_BOOL: + case SCR_DYN_NUM: + case SCR_DYN_STR: + case SCR_DYN_BIGINT: + case SCR_DYN_SYMBOL: + return false; + default: + break; + } + /* Object.prototype is on the chain from the value's own prototype on: + * Object.prototype itself has a null prototype, so it is not an Object. */ + ScrDyn *current = scr_dyn_retain((ScrDyn *)value); + for (bool first = true;; first = false) { + if (!first && current == scr_builtin_object_prototype) { + scr_dyn_release(current); + return true; + } + if (current == scr_builtin_object_prototype) { + scr_dyn_release(current); + return false; + } + ScrDyn *next = NULL; + if (current->kind == SCR_DYN_FUNC) { + /* A function's prototype is Function.prototype, whose own is + * Object.prototype, unless the program set it. */ + if (scr_closure_identity(current->v.fn.clo)->props) { + ScrDyn *table = scr_dyn_fn_properties(current); + next = table->prototype ? scr_dyn_retain(table->prototype) + : table->null_proto ? scr_dyn_new_null() : NULL; + scr_dyn_release(table); + } + } else if (current->kind == SCR_DYN_OBJ || current->kind == SCR_DYN_ARR || + (current->kind == SCR_DYN_TYPED_REF && + strncmp(current->v.typed_ref.type_key, "object:", 7) != 0)) { + next = scr_dyn_get_prototype(current); + if (!next) { + scr_dyn_release(current); + return false; /* the read threw: pending */ + } + } + scr_dyn_release(current); + if (!next) return true; + if (next->kind == SCR_DYN_NULL) { + scr_dyn_release(next); + return false; + } + current = next; + } +} + +/* `value instanceof Function`: whether a function is on the value's + * prototype chain. A function's own prototype is Function.prototype unless + * the program set it, so the walk ends true at the first function that + * kept its prototype; it follows set prototypes through functions and + * objects alike (`Object.create(function () {})` is a Function instance), + * and ends false at null or at a value without a chain the runtime walks. + * False with the exception pending when a prototype read throws. */ +static bool scr_dyn_instanceof_function(const ScrDyn *value) { + ScrDyn *current = NULL; + if (value->kind == SCR_DYN_FUNC) { + if (!scr_closure_identity(value->v.fn.clo)->props) return true; + ScrDyn *table = scr_dyn_fn_properties((ScrDyn *)value); + current = table->prototype ? scr_dyn_retain(table->prototype) : NULL; + bool unset = !table->prototype && !table->null_proto; + scr_dyn_release(table); + if (unset) return true; + } else if (value->kind == SCR_DYN_OBJ || value->kind == SCR_DYN_ARR) { + current = scr_dyn_get_prototype((ScrDyn *)value); + if (!current) return false; /* the read threw: pending */ + } + while (current && current->kind != SCR_DYN_NULL) { + ScrDyn *next = NULL; + if (current->kind == SCR_DYN_FUNC) { + if (!scr_closure_identity(current->v.fn.clo)->props) { + scr_dyn_release(current); + return true; + } + ScrDyn *table = scr_dyn_fn_properties(current); + bool unset = !table->prototype && !table->null_proto; + next = table->prototype ? scr_dyn_retain(table->prototype) : NULL; + scr_dyn_release(table); + if (unset) { + scr_dyn_release(current); + return true; + } + } else if (current->kind == SCR_DYN_OBJ || current->kind == SCR_DYN_ARR) { + next = scr_dyn_get_prototype(current); + if (!next) { + scr_dyn_release(current); + return false; /* the read threw: pending */ + } + } + scr_dyn_release(current); + current = next; + } + if (current) scr_dyn_release(current); + return false; +} + bool scr_bytes_instanceof_at(const ScrDyn *value, const ScrDyn *callee, const ScrStr *refusal) { if (!callee || callee->kind != SCR_DYN_FUNC) { static const char message[] = "Right-hand side of 'instanceof' is not callable"; @@ -4260,6 +5110,13 @@ bool scr_bytes_instanceof_at(const ScrDyn *value, const ScrDyn *callee, const Sc } int hooked = scr_dyn_has_instance_hook(value, callee); if (hooked >= 0) return hooked == 1; + if (callee->v.fn.sig && callee->v.fn.name) { + if (strcmp(callee->v.fn.sig, "native:constructor") == 0 && strcmp(callee->v.fn.name, "Object") == 0) + return scr_dyn_instanceof_object(value); + if (strcmp(callee->v.fn.sig, "native:function-constructor") == 0 && + strcmp(callee->v.fn.name, "Function") == 0) + return scr_dyn_instanceof_function(value); + } /* An error constructor held as a value (`inst instanceof params.Parent` * with Parent === Error): the error kind's interval, and for Error itself * objects inheriting Error.prototype. */ @@ -4269,6 +5126,9 @@ bool scr_bytes_instanceof_at(const ScrDyn *value, const ScrDyn *callee, const Sc } if (scr_array_buffer_ctor && callee->v.fn.clo == scr_array_buffer_ctor->v.fn.clo) return scr_array_buffer_is(value); if (scr_array_ctor && callee->v.fn.clo == scr_array_ctor->v.fn.clo) return value->kind == SCR_DYN_ARR || scr_dyn_ref_is_array(value); + /* The Buffer function value: Buffer instances, not other Uint8Arrays. */ + if (callee->v.fn.sig && strcmp(callee->v.fn.sig, "native:Buffer") == 0) + return value->kind == SCR_DYN_BYTES && value->buffer; for (size_t i = 0; i < SCR_BYTES_ELEM_COUNT; i++) { if (scr_bytes_ctors[i] && callee->v.fn.clo == scr_bytes_ctors[i]->v.fn.clo) return scr_dyn_typed_array_is(value, (int)i); @@ -4331,6 +5191,7 @@ bool scr_bytes_instanceof(const ScrDyn *value, const ScrDyn *callee) { return scr_bytes_instanceof_at(value, callee, NULL); } SCR_TL bool (*scr_dyn_global_construct_fn)(const ScrDyn *callee, const ScrDyn *args, ScrDyn **result) = NULL; +ScrDyn *(*scr_bytes_buffer_constructor_fn)(void) = NULL; ScrDyn *scr_bytes_construct(const ScrDyn *callee, const ScrDyn *args, const ScrStr *what) { if (callee->kind == SCR_DYN_FUNC && scr_array_ctor && callee->v.fn.clo == scr_array_ctor->v.fn.clo) @@ -4351,6 +5212,14 @@ ScrDyn *scr_bytes_construct(const ScrDyn *callee, const ScrDyn *args, const ScrS ScrDyn *constructed = NULL; if (scr_dyn_global_construct_fn && scr_dyn_global_construct_fn(callee, args, &constructed)) return constructed; + /* The Buffer function value (scr_builtin_values.c): Node's construction + * reports DEP0005 by the caller's location, which a stored value does + * not know. */ + if (callee->kind == SCR_DYN_FUNC && callee->v.fn.sig && strcmp(callee->v.fn.sig, "native:Buffer") == 0) { + static const char message[] = "Calling or constructing Buffer through a stored Buffer value is not supported yet"; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); + return NULL; + } ScrJsonBuf message; scr_jb_init(&message); scr_jb_write(&message, what->data, what->len); @@ -4359,9 +5228,540 @@ ScrDyn *scr_bytes_construct(const ScrDyn *callee, const ScrDyn *args, const ScrS return NULL; } +static bool scr_dyn_bytes_named(const char *method, const char *name) { + return strcmp(method, name) == 0; +} + +/* An optional index argument: ToIntegerOrInfinity, or `fallback` when + * absent or undefined. */ +static double scr_dyn_bytes_index_arg(ScrDyn *const *args, size_t argc, size_t index, double fallback) { + if (index >= argc || args[index]->kind == SCR_DYN_UNDEF) return fallback; + double value; + if (!scr_dyn_number_coerce_js(args[index], &value)) return 0; + return isnan(value) ? 0 : trunc(value); +} + +/* The callable-callback gate: " is not a function". */ +static bool scr_dyn_bytes_cb_check(ScrDyn *const *args, size_t argc) { + ScrDyn *cb = argc > 0 ? args[0] : scr_dyn_undefined(); + if (scr_dyn_is_callable(cb)) return true; + bool primitive = cb->kind == SCR_DYN_NUM || cb->kind == SCR_DYN_STR || cb->kind == SCR_DYN_BOOL || + cb->kind == SCR_DYN_UNDEF || cb->kind == SCR_DYN_NULL || cb->kind == SCR_DYN_BIGINT; + ScrStr *text = primitive ? scr_dyn_string_coerce_js(cb) : scr_str_new("#", 9); + if (!text) return false; + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_write(&b, text->data, text->len); + scr_jb_puts(&b, " is not a function"); + scr_str_release(text); + scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&b)); + return false; +} + +/* ── %TypedArray%.prototype methods over a checked typed array ────────── + * One dispatch for scr_dyn_invoke's typed array receivers and for the + * method values their reads give, in this always-linked unit. + * Elements are read live at each step: a callback can write the array it + * walks. Results of map, filter and the copying methods take the + * receiver's element kind (and a Buffer's flavor). */ + +static ScrDyn *dyn_bytes_wrap(const ScrDyn *recv, ScrBytes *out) { + ScrDyn *d = recv->buffer ? scr_dyn_new_buffer(out) : scr_dyn_new_bytes(out); + scr_bytes_release(out); + return d; +} + +/* The element at k, or undefined past the view's current end. */ +static ScrDyn *dyn_bytes_item(const ScrBytes *b, size_t k) { + return k < b->len ? scr_dyn_new_num(scr_bytes_get(b, (double)k)) : scr_dyn_retain(scr_dyn_undefined()); +} + +/* callback(value, index, array) with thisArg as `this`. */ +static ScrDyn *dyn_bytes_call(ScrDyn *cb, ScrDyn *this_arg, ScrDyn *const *cbargs, size_t n) { + scr_dyn_this_push_dyn(this_arg); + ScrDyn *r = scr_dyn_call(cb, (ScrDyn **)cbargs, n, "callback"); + scr_dyn_this_pop(); + return r; +} + +/* ToIntegerOrInfinity of a present argument; `fallback` when absent. */ +static bool dyn_bytes_integer_arg(ScrDyn *const *args, size_t argc, size_t i, double fallback, double *out) { + if (i >= argc) { *out = fallback; return true; } + double v; + if (!scr_dyn_number_coerce_js(args[i], &v)) return false; + *out = isnan(v) ? 0 : trunc(v); + return true; +} + +/* SortCompare: the comparator's ToNumber answer (NaN as 0), or the + * numeric order with -0 before +0 and NaN last. */ +static bool dyn_bytes_less_eq(ScrDyn *cmp, double x, double y, bool *ok) { + if (cmp->kind != SCR_DYN_UNDEF) { + ScrDyn *a = scr_dyn_new_num(x), *b = scr_dyn_new_num(y); + ScrDyn *cbargs[2] = { a, b }; + /* The comparator is called with an undefined `this`. */ + scr_dyn_this_push_dyn(scr_dyn_undefined()); + ScrDyn *r = scr_dyn_call(cmp, cbargs, 2, "comparefn"); + scr_dyn_this_pop(); + scr_dyn_release(a); + scr_dyn_release(b); + double v = 0; + if (!r || !scr_dyn_number_coerce_js(r, &v)) { scr_dyn_release(r); *ok = false; return true; } + scr_dyn_release(r); + return isnan(v) || v <= 0; + } + if (isnan(x)) return isnan(y); + if (isnan(y)) return true; + if (x < y) return true; + if (x > y) return false; + return signbit(x) || !signbit(y); +} + +/* A stable merge sort of values[0..n). False with the exception pending. */ +static bool dyn_bytes_sort_values(double *values, size_t n, ScrDyn *cmp) { + if (n < 2) return true; + double *tmp = malloc(n * sizeof *tmp); + if (!tmp) scr_trap("scriptc: out of memory\n"); + bool ok = true; + for (size_t width = 1; width < n && ok; width *= 2) { + for (size_t lo = 0; lo < n && ok; lo += 2 * width) { + size_t mid = lo + width < n ? lo + width : n, hi = lo + 2 * width < n ? lo + 2 * width : n; + size_t i = lo, j = mid, k = lo; + while (i < mid && j < hi && ok) tmp[k++] = dyn_bytes_less_eq(cmp, values[i], values[j], &ok) ? values[i++] : values[j++]; + while (i < mid) tmp[k++] = values[i++]; + while (j < hi) tmp[k++] = values[j++]; + } + if (ok) memcpy(values, tmp, n * sizeof *values); + } + free(tmp); + return ok; +} + +static bool dyn_bytes_comparefn_check(ScrDyn *const *args, size_t argc) { + if (argc == 0 || args[0]->kind == SCR_DYN_UNDEF || scr_dyn_is_callable(args[0])) return true; + static const char message[] = "The comparison function must be either a function or undefined"; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + return false; +} + +static double *dyn_bytes_values(const ScrBytes *b, size_t n) { + double *values = malloc((n ? n : 1) * sizeof *values); + if (!values) scr_trap("scriptc: out of memory\n"); + for (size_t i = 0; i < n; i++) values[i] = scr_bytes_get(b, (double)i); + return values; +} + +static ScrDyn *scr_dyn_symbol_receiver(const ScrDyn *value); +static ScrDynEntry *scr_dyn_symbol_entry(const ScrDyn *receiver, const ScrDyn *key, bool own); + +/* Whether a typed-array constructor has a Symbol.species of its own, + * shadowing the one it inherits from %TypedArray%. */ +static bool scr_bytes_ctor_own_species(const ScrDyn *constructor) { + ScrDyn *table = scr_dyn_symbol_receiver(constructor); + bool own = table && scr_dyn_symbol_entry(table, scr_species_symbol, true); + scr_dyn_release(table); + return own; +} + +/* A relative index (ToIntegerOrInfinity, negatives from the end), clamped + * to [0, len], as scr_bytes_slice computes it. */ +static size_t scr_bytes_rel_index_of(double i, size_t len) { + if (i != i) return 0; + double t = trunc(i); + if (t < 0) t += (double)len; + if (t < 0) return 0; + if (t > (double)len) return len; + return (size_t)t; +} + +/* SpeciesConstructor of a typed array: NULL (and no exception) for the + * default constructor of its kind, else the species constructor (+1). + * Only a `constructor` the program set on the view can differ. */ +static ScrDyn *scr_bytes_species(ScrDyn *recv, bool *failed) { + *failed = false; + ScrStr *name = scr_str_new("constructor", 11); + bool own = scr_bytes_has_named(recv, name); + ScrDyn *constructor = own ? scr_dyn_obj_read_receiver(recv->v.bytes->properties, name->data, name->len, recv) : NULL; + scr_str_release(name); + if (!own) return NULL; + if (!constructor) { *failed = true; return NULL; } + if (constructor->kind == SCR_DYN_UNDEF) { scr_dyn_release(constructor); return NULL; } + if (constructor->kind != SCR_DYN_OBJ && constructor->kind != SCR_DYN_FUNC) { + scr_dyn_release(constructor); + static const char message[] = "object.constructor is not an object"; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + *failed = true; + return NULL; + } + /* Before the program creates the species symbol, no constructor can have + * a Symbol.species property of its own. */ + ScrDyn *species = scr_species_symbol ? scr_dyn_symbol_key_get(constructor, scr_species_symbol, true) + : scr_dyn_retain(scr_dyn_undefined()); + if (species && species->kind == SCR_DYN_UNDEF && scr_bytes_ctor_elem(constructor) >= 0 && + (!scr_species_symbol || !scr_bytes_ctor_own_species(constructor))) { + /* A built-in typed-array constructor inherits %TypedArray%'s species + * getter, which returns the constructor itself. */ + scr_dyn_release(species); + species = scr_dyn_retain(constructor); + } + scr_dyn_release(constructor); + if (scr_exc_pending()) { scr_dyn_release(species); *failed = true; return NULL; } + if (!species || species->kind == SCR_DYN_UNDEF || species->kind == SCR_DYN_NULL || + scr_bytes_ctor_elem(species) == (int)recv->v.bytes->elem) { + scr_dyn_release(species); + return NULL; + } + if (!scr_dyn_is_callable(species)) { + scr_dyn_release(species); + static const char message[] = "object.constructor[Symbol.species] is not a constructor"; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + *failed = true; + return NULL; + } + return species; +} + +/* TypedArraySpeciesCreate(recv, [n]): a new array of the receiver's kind + * (and Buffer flavor), or the species constructor's checked result. */ +static ScrDyn *scr_bytes_species_create(ScrDyn *recv, size_t n) { + bool failed; + ScrDyn *species = scr_bytes_species(recv, &failed); + if (failed) return NULL; + if (!species) return dyn_bytes_wrap(recv, scr_bytes_new(recv->v.bytes->elem, (double)n)); + ScrDyn *result = scr_bytes_static_create(species, n); + scr_dyn_release(species); + return result; +} + +ScrDyn *scr_dyn_bytes_method(ScrDyn *recv, const char *method, ScrDyn *const *args, size_t argc, bool *handled) { + ScrBytes *bytes = recv->v.bytes; + size_t len = bytes->len; + *handled = true; + size_t blen = bytes->len; + if (scr_dyn_bytes_named(method, "at")) { + double iD = scr_dyn_bytes_index_arg(args, argc, 0, 0); + if (scr_exc_pending()) return NULL; + double idx = iD < 0 ? (double)blen + iD : iD; + if (idx < 0 || idx >= (double)blen) return scr_dyn_retain(scr_dyn_undefined()); + return scr_dyn_new_num(scr_bytes_get(bytes, idx)); + } + if (scr_dyn_bytes_named(method, "slice") || scr_dyn_bytes_named(method, "subarray")) { + /* subarray and Buffer.slice alias their source. TypedArray.slice + * owns an independent copy; all keep the receiver's Buffer flavor. */ + double startD = scr_dyn_bytes_index_arg(args, argc, 0, 0); + if (scr_exc_pending()) return NULL; + double endD = scr_dyn_bytes_index_arg(args, argc, 1, (double)blen); + if (scr_exc_pending()) return NULL; + const bool subarray = recv->buffer || scr_dyn_bytes_named(method, "subarray"); + bool failed; + ScrDyn *species = recv->buffer ? NULL : scr_bytes_species(recv, &failed); + if (!recv->buffer && failed) return NULL; + if (species && !subarray) { + /* TypedArraySpeciesCreate runs first: the constructor can change the + * source, and the copy reads its elements as they are afterwards, + * up to its current length. */ + size_t from = scr_bytes_rel_index_of(startD, blen), to = scr_bytes_rel_index_of(endD, blen); + size_t count = to > from ? to - from : 0; + ScrDyn *result = scr_bytes_static_create(species, count); + scr_dyn_release(species); + if (!result) return NULL; + size_t end = to < bytes->len ? to : bytes->len; + for (size_t k = from, n = 0; k < end; k++, n++) + scr_bytes_set(result->v.bytes, (double)n, scr_bytes_get(bytes, (double)k)); + return result; + } + ScrBytes *out = subarray ? scr_bytes_subarray(bytes, startD, endD) : scr_bytes_slice(bytes, startD, endD); + if (!species) { + ScrDyn *d = recv->buffer ? scr_dyn_new_buffer(out) : scr_dyn_new_bytes(out); + scr_bytes_release(out); + return d; + } + /* A subarray's species constructor views the same buffer. */ + ScrDyn *ctor_args = scr_dyn_new_arr(); + scr_dyn_arr_push(ctor_args, scr_array_buffer_from_bytes(bytes)); + scr_dyn_arr_push(ctor_args, scr_dyn_new_num(scr_bytes_byte_offset(out))); + scr_dyn_arr_push(ctor_args, scr_dyn_new_num((double)out->len)); + ScrStr *what = scr_str_new("TypedArray species", 18); + ScrDyn *result = scr_dyn_construct(species, ctor_args, what); + scr_str_release(what); + scr_dyn_release(ctor_args); + if (result && (result->kind != SCR_DYN_BYTES || result->v.bytes->is_data_view)) { + scr_dyn_release(result); + result = NULL; + static const char message[] = "this is not a typed array."; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + } + scr_dyn_release(species); + scr_bytes_release(out); + return result; + } + if (scr_dyn_bytes_named(method, "set")) { + double offset = scr_dyn_bytes_index_arg(args, argc, 1, 0); + if (scr_exc_pending()) return NULL; + scr_bytes_set_from_dyn(bytes, argc ? args[0] : scr_dyn_undefined(), offset); + return scr_exc_pending() ? NULL : scr_dyn_retain(scr_dyn_undefined()); + } + if (scr_dyn_bytes_named(method, "copyWithin")) { + double target = scr_dyn_bytes_index_arg(args, argc, 0, 0); + if (scr_exc_pending()) return NULL; + double start = scr_dyn_bytes_index_arg(args, argc, 1, 0); + if (scr_exc_pending()) return NULL; + double end = scr_dyn_bytes_index_arg(args, argc, 2, (double)blen); + if (scr_exc_pending()) return NULL; + scr_bytes_release(scr_bytes_copy_within(bytes, target, start, end)); + return scr_dyn_retain(recv); + } + if (scr_dyn_bytes_named(method, "fill") && !recv->buffer) { + double value; + if (!scr_dyn_number_coerce_js(argc ? args[0] : scr_dyn_undefined(), &value)) return NULL; + double start = scr_dyn_bytes_index_arg(args, argc, 1, 0); + if (scr_exc_pending()) return NULL; + double end = scr_dyn_bytes_index_arg(args, argc, 2, (double)blen); + if (scr_exc_pending()) return NULL; + scr_bytes_release(scr_bytes_fill_elem(bytes, value, start, end)); + return scr_dyn_retain(recv); + } + if (scr_dyn_bytes_named(method, "join") || (scr_dyn_bytes_named(method, "toString") && !recv->buffer)) { + ScrStr *separator = scr_dyn_bytes_named(method, "join") && argc && args[0]->kind != SCR_DYN_UNDEF + ? scr_dyn_string_coerce_js(args[0]) : scr_str_new(",", 1); + if (scr_exc_pending()) { scr_str_release(separator); return NULL; } + ScrStr *joined = scr_bytes_join(bytes, separator); + scr_str_release(separator); + ScrDyn *out = scr_dyn_new_str(joined); + scr_str_release(joined); + return out; + } + if (scr_dyn_bytes_named(method, "toLocaleString")) { + /* Each element's Number.prototype.toLocaleString, joined by ",". */ + ScrJsonBuf b; + scr_jb_init(&b); + for (size_t i = 0; i < len; i++) { + if (i > 0) scr_jb_putc(&b, ','); + if (i >= bytes->len) continue; + ScrStr *text = scr_intl_num_format_en_us(scr_bytes_get(bytes, (double)i)); + scr_jb_write(&b, text->data, text->len); + scr_str_release(text); + } + ScrStr *joined = scr_jb_finish(&b); + ScrDyn *out = scr_dyn_new_str(joined); + scr_str_release(joined); + return out; + } + if (scr_dyn_bytes_named(method, "keys") || scr_dyn_bytes_named(method, "values") || scr_dyn_bytes_named(method, "entries")) + return scr_dyn_native_handle_iterator(recv, scr_dyn_bytes_named(method, "keys") ? 1 : scr_dyn_bytes_named(method, "entries") ? 2 : 0); + if (scr_dyn_bytes_named(method, "indexOf") || scr_dyn_bytes_named(method, "lastIndexOf") || scr_dyn_bytes_named(method, "includes")) { + const bool includes = scr_dyn_bytes_named(method, "includes"), last = scr_dyn_bytes_named(method, "lastIndexOf"); + if (len == 0) return includes ? scr_dyn_new_bool(false) : scr_dyn_new_num(-1); + double n; + if (!dyn_bytes_integer_arg(args, argc, 1, last ? (double)len - 1 : 0, &n)) return NULL; + ScrDyn *needle = argc ? args[0] : scr_dyn_undefined(); + const bool number = needle->kind == SCR_DYN_NUM; + const double target = number ? needle->v.num : 0; + if (last) { + if (n == -INFINITY) return scr_dyn_new_num(-1); + double k = n >= 0 ? (n < (double)len - 1 ? n + 0.0 : (double)len - 1) : (double)len + n; + for (; number && k >= 0; k--) + if ((size_t)k < bytes->len && scr_bytes_get(bytes, k) == target) return scr_dyn_new_num(k); + return scr_dyn_new_num(-1); + } + if (n == INFINITY) return includes ? scr_dyn_new_bool(false) : scr_dyn_new_num(-1); + double k = n >= 0 ? n + 0.0 : ((double)len + n < 0 ? 0 : (double)len + n); + for (; k < (double)len; k++) { + /* An element past a shrunk view reads undefined, which includes finds. */ + if ((size_t)k >= bytes->len) { + if (includes && needle->kind == SCR_DYN_UNDEF) return scr_dyn_new_bool(true); + continue; + } + if (!number) continue; + double v = scr_bytes_get(bytes, k); + if (v == target || (includes && isnan(v) && isnan(target))) + return includes ? scr_dyn_new_bool(true) : scr_dyn_new_num(k); + } + return includes ? scr_dyn_new_bool(false) : scr_dyn_new_num(-1); + } + if (scr_dyn_bytes_named(method, "forEach") || scr_dyn_bytes_named(method, "map") || scr_dyn_bytes_named(method, "filter") || scr_dyn_bytes_named(method, "some") || + scr_dyn_bytes_named(method, "every") || scr_dyn_bytes_named(method, "find") || scr_dyn_bytes_named(method, "findIndex") || + scr_dyn_bytes_named(method, "findLast") || scr_dyn_bytes_named(method, "findLastIndex")) { + if (!scr_dyn_bytes_cb_check(args, argc)) return NULL; + ScrDyn *cb = args[0], *this_arg = argc > 1 ? args[1] : scr_dyn_undefined(); + const bool backward = scr_dyn_bytes_named(method, "findLast") || scr_dyn_bytes_named(method, "findLastIndex"); + ScrDyn *mapped = NULL; + if (scr_dyn_bytes_named(method, "map") && !(mapped = scr_bytes_species_create(recv, len))) return NULL; + double *kept = scr_dyn_bytes_named(method, "filter") ? malloc((len ? len : 1) * sizeof *kept) : NULL; + size_t nkept = 0; + for (size_t step = 0; step < len; step++) { + size_t k = backward ? len - 1 - step : step; + ScrDyn *item = dyn_bytes_item(bytes, k), *index = scr_dyn_new_num((double)k); + ScrDyn *cbargs[3] = { item, index, recv }; + ScrDyn *r = dyn_bytes_call(cb, this_arg, cbargs, 3); + scr_dyn_release(index); + if (!r) { + scr_dyn_release(item); + scr_dyn_release(mapped); + free(kept); + return NULL; + } + if (mapped) { + double v; + bool converted = scr_dyn_number_coerce_js(r, &v); + scr_dyn_release(r); + scr_dyn_release(item); + if (!converted) { scr_dyn_release(mapped); return NULL; } + if (k < mapped->v.bytes->len) scr_bytes_set(mapped->v.bytes, (double)k, v); + continue; + } + bool truthy = scr_dyn_truthy(r); + scr_dyn_release(r); + if (kept) { + if (truthy) kept[nkept++] = item->kind == SCR_DYN_NUM ? item->v.num : NAN; + } else if (truthy && (scr_dyn_bytes_named(method, "find") || scr_dyn_bytes_named(method, "findLast"))) { + return item; + } else if (truthy && (scr_dyn_bytes_named(method, "findIndex") || scr_dyn_bytes_named(method, "findLastIndex"))) { + scr_dyn_release(item); + return scr_dyn_new_num((double)k); + } else if (truthy && scr_dyn_bytes_named(method, "some")) { + scr_dyn_release(item); + return scr_dyn_new_bool(true); + } else if (!truthy && scr_dyn_bytes_named(method, "every")) { + scr_dyn_release(item); + return scr_dyn_new_bool(false); + } + scr_dyn_release(item); + } + if (mapped) return mapped; + if (kept) { + ScrDyn *out = scr_bytes_species_create(recv, nkept); + for (size_t i = 0; out && i < nkept; i++) scr_bytes_set(out->v.bytes, (double)i, kept[i]); + free(kept); + return out; + } + if (scr_dyn_bytes_named(method, "some")) return scr_dyn_new_bool(false); + if (scr_dyn_bytes_named(method, "every")) return scr_dyn_new_bool(true); + if (scr_dyn_bytes_named(method, "findIndex") || scr_dyn_bytes_named(method, "findLastIndex")) return scr_dyn_new_num(-1); + return scr_dyn_retain(scr_dyn_undefined()); /* forEach, find, findLast */ + } + if (scr_dyn_bytes_named(method, "reduce") || scr_dyn_bytes_named(method, "reduceRight")) { + if (!scr_dyn_bytes_cb_check(args, argc)) return NULL; + const bool backward = scr_dyn_bytes_named(method, "reduceRight"); + size_t step = 0; + ScrDyn *acc; + if (argc > 1) acc = scr_dyn_retain(args[1]); + else if (len == 0) { + static const char message[] = "Reduce of empty array with no initial value"; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + return NULL; + } else acc = dyn_bytes_item(bytes, backward ? len - 1 : 0), step = 1; + for (; step < len; step++) { + size_t k = backward ? len - 1 - step : step; + ScrDyn *item = dyn_bytes_item(bytes, k), *index = scr_dyn_new_num((double)k); + ScrDyn *cbargs[4] = { acc, item, index, recv }; + /* The reducer is called with an undefined `this`. */ + scr_dyn_this_push_dyn(scr_dyn_undefined()); + ScrDyn *next = scr_dyn_call(args[0], cbargs, 4, "callback"); + scr_dyn_this_pop(); + scr_dyn_release(index); + scr_dyn_release(item); + scr_dyn_release(acc); + if (!next) return NULL; + acc = next; + } + return acc; + } + if (scr_dyn_bytes_named(method, "reverse")) { + for (size_t lo = 0, hi = len; lo + 1 < hi; lo++, hi--) { + double a = scr_bytes_get(bytes, (double)lo), b = scr_bytes_get(bytes, (double)(hi - 1)); + scr_bytes_set(bytes, (double)lo, b); + scr_bytes_set(bytes, (double)(hi - 1), a); + } + return scr_dyn_retain(recv); + } + if (scr_dyn_bytes_named(method, "sort") || scr_dyn_bytes_named(method, "toSorted")) { + if (!dyn_bytes_comparefn_check(args, argc)) return NULL; + double *values = dyn_bytes_values(bytes, len); + if (!dyn_bytes_sort_values(values, len, argc ? args[0] : scr_dyn_undefined())) { free(values); return NULL; } + ScrBytes *target = scr_dyn_bytes_named(method, "sort") ? bytes : scr_bytes_new(bytes->elem, (double)len); + for (size_t i = 0; i < len && i < target->len; i++) scr_bytes_set(target, (double)i, values[i]); + free(values); + return scr_dyn_bytes_named(method, "sort") ? scr_dyn_retain(recv) : dyn_bytes_wrap(recv, target); + } + if (scr_dyn_bytes_named(method, "toReversed")) { + ScrBytes *out = scr_bytes_new(bytes->elem, (double)len); + for (size_t i = 0; i < len; i++) scr_bytes_set(out, (double)i, scr_bytes_get(bytes, (double)(len - 1 - i))); + return dyn_bytes_wrap(recv, out); + } + if (scr_dyn_bytes_named(method, "with")) { + double rel, value; + if (!dyn_bytes_integer_arg(args, argc, 0, 0, &rel)) return NULL; + double actual = rel >= 0 ? rel : (double)len + rel; + if (!scr_dyn_number_coerce_js(argc > 1 ? args[1] : scr_dyn_undefined(), &value)) return NULL; + if (!(actual >= 0 && actual < (double)bytes->len)) { + static const char message[] = "Invalid typed array index"; + scr_throw_error_msg(SCR_ERR_RANGE, message, sizeof message - 1); + return NULL; + } + ScrBytes *out = scr_bytes_new(bytes->elem, (double)len); + for (size_t i = 0; i < len; i++) scr_bytes_set(out, (double)i, scr_bytes_get(bytes, (double)i)); + scr_bytes_set(out, actual, value); + return dyn_bytes_wrap(recv, out); + } + *handled = false; + return NULL; +} + +/* A %TypedArray%.prototype method read as a value: it calls the method on + * its `this`, which must be a typed array. */ +static ScrDyn *dyn_bytes_method_value_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + ScrStr *method = scr_box_get_ref(closure->caps[0]); + ScrDyn *receiver = scr_dyn_this_get(); + ScrDyn *result = NULL; + if (receiver->kind == SCR_DYN_BYTES && !receiver->v.bytes->is_data_view) { + bool handled; + result = scr_dyn_bytes_method(receiver, method->data, args, argc, &handled); + if (!handled) { + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_puts(&b, "'TypedArray.prototype."); + scr_jb_puts(&b, method->data); + scr_jb_puts(&b, "' through a method value is not supported yet"); + scr_throw_error(SCR_ERR_ERROR, scr_jb_finish(&b)); + } + } else { + static const char message[] = "this is not a typed array."; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + } + scr_dyn_release(receiver); + scr_str_release(method); + return result; +} + +static SCR_TL ScrDyn *dyn_bytes_method_values; + +static void dyn_bytes_method_values_cleanup(void) { + scr_dyn_release(dyn_bytes_method_values); + dyn_bytes_method_values = NULL; +} + +static ScrDyn *scr_dyn_bytes_method_value(const ScrStr *name, size_t arity) { + if (!dyn_bytes_method_values) { + dyn_bytes_method_values = scr_dyn_new_obj_null_proto(); + scr_atexit(dyn_bytes_method_values_cleanup); + } + ScrDyn *value = scr_dyn_obj_get(dyn_bytes_method_values, name->data, name->len); + if (!value) { + ScrClosure *closure = scr_closure_new(NULL, 1); + closure->caps[0] = scr_box_new(SCR_BOX_STR); + scr_box_set_ref(closure->caps[0], scr_str_retain((ScrStr *)name)); + value = scr_dyn_new_func(closure, dyn_bytes_method_value_call, arity, "native:TypedArray.prototype", name->data); + scr_dyn_obj_set(dyn_bytes_method_values, name->data, name->len, value); /* the table owns it */ + } + return scr_dyn_retain(value); +} + /* Shared view metadata and indexed reads for both compiler backends. */ ScrDyn *scr_dyn_bytes_key_get(const ScrDyn *value, const ScrStr *key) { ScrBytes *bytes = value->v.bytes; + /* Own named properties first; a getter sees the view as `this`. */ + if (scr_bytes_has_named(value, key)) return scr_bytes_named_get(value, key->data, key->len); if (key->len == 10 && memcmp(key->data, "byteLength", 10) == 0) return scr_dyn_new_num(scr_bytes_byte_len(bytes)); if (key->len == 10 && memcmp(key->data, "byteOffset", 10) == 0) return scr_dyn_new_num(scr_bytes_byte_offset(bytes)); if (key->len == 6 && memcmp(key->data, "buffer", 6) == 0) return scr_array_buffer_from_bytes(bytes); @@ -4380,7 +5780,9 @@ ScrDyn *scr_dyn_bytes_key_get(const ScrDyn *value, const ScrStr *key) { return scr_dyn_new_num((double)scr_bytes_elem_size(bytes->elem)); if (key->len == 11 && memcmp(key->data, "constructor", 11) == 0) { if (!value->buffer) return scr_bytes_constructor_for(bytes->elem); - /* Buffer retains its separate builtin identity. */ + /* A program that reads the Buffer function's value answers it; + * otherwise Buffer keeps its separate builtin identity token. */ + if (scr_bytes_buffer_constructor_fn) return scr_bytes_buffer_constructor_fn(); char token[64]; int length = snprintf(token, sizeof token, "[builtin %s]", value->buffer ? "Buffer" : scr_bytes_elem_name(bytes->elem)); ScrStr *name = scr_str_new(token, (size_t)length); @@ -4397,6 +5799,18 @@ ScrDyn *scr_dyn_bytes_key_get(const ScrDyn *value, const ScrStr *key) { } if (digits && index < bytes->len) return scr_dyn_new_num(scr_bytes_get(bytes, (double)index)); } + /* %TypedArray%.prototype methods, with their lengths. */ + static const struct { const char *name; size_t arity; } methods[] = { + {"at", 1}, {"copyWithin", 2}, {"entries", 0}, {"every", 1}, {"fill", 1}, {"filter", 1}, + {"find", 1}, {"findIndex", 1}, {"findLast", 1}, {"findLastIndex", 1}, {"forEach", 1}, + {"includes", 1}, {"indexOf", 1}, {"join", 1}, {"keys", 0}, {"lastIndexOf", 1}, {"map", 1}, + {"reduce", 1}, {"reduceRight", 1}, {"reverse", 0}, {"set", 1}, {"slice", 2}, {"some", 1}, + {"sort", 1}, {"subarray", 2}, {"toLocaleString", 0}, {"toReversed", 0}, {"toSorted", 1}, + {"toString", 0}, {"values", 0}, {"with", 2}, + }; + for (size_t i = 0; i < sizeof methods / sizeof methods[0]; i++) + if (key->len == strlen(methods[i].name) && memcmp(key->data, methods[i].name, key->len) == 0) + return scr_dyn_bytes_method_value(key, methods[i].arity); return scr_dyn_retain(scr_dyn_undefined()); } @@ -4426,6 +5840,15 @@ ScrDyn *scr_dyn_new_chunk(const ScrBytes *b) { /* A boxed static function value (the compiler's static→dyn converters). * Ownership of the closure MOVES in; sig/name are static literals. */ +void scr_dyn_function_set_length(ScrDyn *fn, uint32_t length) { + if (fn->kind == SCR_DYN_FUNC) fn->v.fn.arity = length; +} + +void scr_dyn_fence_member(ScrDyn *object, const ScrStr *key) { + ScrDynEntry *entry = scr_dyn_entry(object, key); + if (entry) entry->fenced = true; +} + ScrDyn *scr_dyn_new_func(ScrClosure *clo, ScrDynThunk thunk, uint32_t arity, const char *sig, const char *name) { if (clo->function_kind & 8) { ScrDyn *original = scr_box_get_ref(clo->caps[0]); @@ -5296,6 +6719,11 @@ void scr_dyn_obj_set_accessor(ScrDyn *obj, const char *key, size_t key_len, ScrD void scr_dyn_obj_set_field(ScrDyn *obj, const char *key, size_t key_len, ScrDyn *value) { ScrDynEntry *existing = obj->v.obj.index ? scr_dyn_find_entry(obj, key, key_len) : scr_dyn_find_linear_entry(obj, key, key_len); + if (existing && existing->fenced) { + scr_dyn_release(value); + scr_dyn_namespace_member_fence(); + return; + } if (existing && !existing->accessor) { scr_dyn_region_store(obj, value); ScrDyn *old = existing->value; @@ -5335,6 +6763,45 @@ bool scr_dyn_same_value(const ScrDyn *left, const ScrDyn *right) { static void scr_dyn_descriptor_fields_drop(ScrDyn **fields) { for (size_t i = 0; i < 6; i++) scr_dyn_release(fields[i]); } + +/* ToPropertyDescriptor: the present fields of `descriptor` (enumerable, + * configurable, value, writable, get, set; +1 each, NULL when absent), + * read in that order through any getters. False with a TypeError pending + * for a primitive descriptor, a getter's exception, or an invalid + * combination; every field is then released. */ +static bool scr_dyn_descriptor_fields_read(const ScrDyn *descriptor, ScrDyn **fields) { + for (size_t i = 0; i < 6; i++) fields[i] = NULL; + if (descriptor->kind != SCR_DYN_OBJ) { + static const char msg[] = "Property description must be an object"; + scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1); + return false; + } + static const char *const names[] = {"enumerable", "configurable", "value", "writable", "get", "set"}; + static const size_t lengths[] = {10, 12, 5, 8, 3, 3}; + for (size_t i = 0; i < 6; i++) { + if (!scr_dyn_obj_get(descriptor, names[i], lengths[i])) continue; + fields[i] = scr_dyn_obj_read(descriptor, names[i], lengths[i]); + if (!fields[i]) { + scr_dyn_descriptor_fields_drop(fields); + return false; + } + } + ScrDyn *get = fields[4], *set = fields[5]; + if ((get || set) && (fields[2] || fields[3])) { + scr_dyn_descriptor_fields_drop(fields); + static const char msg[] = "Invalid property descriptor. Cannot both specify accessors and a value or writable attribute"; + scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1); + return false; + } + if ((get && get->kind != SCR_DYN_UNDEF && get->kind != SCR_DYN_FUNC) || + (set && set->kind != SCR_DYN_UNDEF && set->kind != SCR_DYN_FUNC)) { + scr_dyn_descriptor_fields_drop(fields); + static const char msg[] = "Getter and setter must be functions"; + scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1); + return false; + } + return true; +} static void scr_error_sync_cause(ScrDyn *view, const ScrStr *key); static void scr_error_sync_slots(const ScrDyn *view, ScrDyn *obj, const ScrStr *key); @@ -5348,11 +6815,12 @@ static ScrDyn *scr_dyn_fn_properties(const ScrDyn *function) { owner->props = scr_box_retain(function->v.fn.class_obj->static_data); if (!owner->props) { ScrDyn *table = scr_dyn_new_obj(); + /* V8's own-key order: length, then name. */ + scr_dyn_obj_set(table, "length", 6, scr_dyn_new_num((double)function->v.fn.arity)); const char *name = function->v.fn.name ? function->v.fn.name : ""; ScrStr *name_string = scr_str_new(name, strlen(name)); scr_dyn_obj_set(table, "name", 4, scr_dyn_new_str(name_string)); scr_str_release(name_string); - scr_dyn_obj_set(table, "length", 6, scr_dyn_new_num((double)function->v.fn.arity)); for (size_t i = 0; i < table->v.obj.len; i++) { table->v.obj.entries[i].writable = false; table->v.obj.entries[i].enumerable = false; @@ -5374,6 +6842,18 @@ static ScrDyn *scr_dyn_fn_properties(const ScrDyn *function) { return (ScrDyn *)scr_box_get_ref(owner->props); } +void scr_dyn_fn_seed_prototype(ScrDyn *function, ScrDyn *prototype) { + ScrDyn *table = scr_dyn_fn_properties(function); + scr_dyn_obj_set(table, "prototype", 9, prototype); + ScrDynEntry *entry = scr_dyn_find_entry(table, "prototype", 9); + entry->writable = false; + entry->enumerable = false; + entry->configurable = false; + scr_dyn_release(table); +} + +ScrDyn *scr_dyn_function_properties(const ScrDyn *function) { return scr_dyn_fn_properties(function); } + ScrDyn *scr_dyn_class_inherit(ScrDyn *constructor, ScrDyn *base) { if (constructor->kind != SCR_DYN_FUNC || !constructor->v.fn.class_obj || (base->kind != SCR_DYN_FUNC && base->kind != SCR_DYN_UNDEF)) { static const char message[] = "Class inheritance requires native constructors"; @@ -5428,7 +6908,7 @@ ScrDyn *scr_dyn_class_base_prototype(ScrDyn *constructor) { default: break; } if (constructor->kind != SCR_DYN_FUNC || constructor->v.fn.class_obj || - scr_closure_identity(constructor->v.fn.clo)->function_kind != 4) { + (scr_closure_identity(constructor->v.fn.clo)->function_kind & SCR_FN_KIND_MASK) != 4) { static const char message[] = "Class extends value is not a native ordinary constructor"; scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); return NULL; @@ -5567,14 +7047,17 @@ static void scr_dyn_name_prototype_chain(const ScrDyn *object) { } void scr_dyn_name_class_prototype(ScrDyn *prototype, const ScrStr *name) { - if (prototype->kind != SCR_DYN_OBJ || prototype->v.obj.class_name || name->len == 0) return; + if (prototype->kind != SCR_DYN_OBJ) return; + if (!scr_dyn_prototype_fixed(prototype)) scr_dyn_fix_prototype(prototype); + if (prototype->v.obj.class_name || name->len == 0) return; scr_dyn_stamp_prototype_name(prototype, name->data, name->len); } /* Ordinary native function constructors retain their live prototype table. * A constructor may replace the fresh receiver with another object. */ ScrDyn *scr_dyn_construct(const ScrDyn *callee, const ScrDyn *args, const ScrStr *what) { - if (callee->kind != SCR_DYN_FUNC || callee->v.fn.class_obj || scr_closure_identity(callee->v.fn.clo)->function_kind != 4) + if (callee->kind != SCR_DYN_FUNC || callee->v.fn.class_obj || + (scr_closure_identity(callee->v.fn.clo)->function_kind & SCR_FN_KIND_MASK) != 4) return scr_bytes_construct(callee, args, what); ScrDyn *prototype = scr_dyn_fn_get(callee, "prototype", 9); if (!prototype) return NULL; @@ -5599,11 +7082,44 @@ ScrDyn *scr_dyn_construct(const ScrDyn *callee, const ScrDyn *args, const ScrStr /* Symbol descriptors use the ordinary descriptor implementation in an * inaccessible table. Keep a retained key list so identities cannot be - * recycled while properties exist, and reflection returns real symbols. */ + * recycled while properties exist, and reflection returns real symbols. + * + * The storage key is the symbol's address in text, which depends on the + * address alone, so a small cache by address answers repeated defines and + * lookups without formatting a new string each time. A recycled address + * names the same key text, so entries never go stale. */ +#define SCR_SYMBOL_KEY_CACHE 256 +static SCR_TL struct { + const void *symbol; + ScrStr *key; +} scr_symbol_key_cache[SCR_SYMBOL_KEY_CACHE]; +static SCR_TL bool scr_symbol_key_cache_registered; + +static void scr_symbol_key_cache_cleanup(void) { + for (size_t i = 0; i < SCR_SYMBOL_KEY_CACHE; i++) { + scr_str_release(scr_symbol_key_cache[i].key); + scr_symbol_key_cache[i].key = NULL; + scr_symbol_key_cache[i].symbol = NULL; + } + scr_symbol_key_cache_registered = false; +} + static ScrStr *scr_dyn_symbol_storage_key(const ScrDyn *key) { + const void *symbol = key->v.symbol.value; + size_t slot = ((uintptr_t)symbol >> 4) & (SCR_SYMBOL_KEY_CACHE - 1); + if (scr_symbol_key_cache[slot].key && scr_symbol_key_cache[slot].symbol == symbol) + return scr_str_retain(scr_symbol_key_cache[slot].key); char buffer[2 * sizeof(void *) + 4]; - int length = snprintf(buffer, sizeof buffer, "%p", (void *)key->v.symbol.value); - return scr_str_new(buffer, (size_t)length); + int length = snprintf(buffer, sizeof buffer, "%p", (void *)symbol); + ScrStr *name = scr_str_new(buffer, (size_t)length); + if (!scr_symbol_key_cache_registered) { + scr_symbol_key_cache_registered = true; + scr_atexit(scr_symbol_key_cache_cleanup); + } + scr_str_release(scr_symbol_key_cache[slot].key); + scr_symbol_key_cache[slot].symbol = symbol; + scr_symbol_key_cache[slot].key = scr_str_retain(name); + return name; } static ScrDyn *scr_dyn_symbol_receiver(const ScrDyn *value) { @@ -5611,6 +7127,10 @@ static ScrDyn *scr_dyn_symbol_receiver(const ScrDyn *value) { if (value->kind == SCR_DYN_FUNC) return scr_dyn_fn_properties(value); if (value->kind == SCR_DYN_OBJ || value->kind == SCR_DYN_ARR || value->kind == SCR_DYN_HANDLE) return scr_dyn_retain((ScrDyn *)value); + /* A view keeps its symbol properties on its named-property table, which + * writes create first (scr_bytes_symbol_write). */ + if (value->kind == SCR_DYN_BYTES) + return value->v.bytes->properties ? scr_dyn_retain(value->v.bytes->properties) : NULL; if (value->kind == SCR_DYN_PROXY) { static const char message[] = "Symbol properties on this native value have no lowering"; scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); @@ -5719,6 +7239,23 @@ static void scr_native_iterator_release(void *ptr) { * next() calls still receive a fresh, independently mutable result object. */ static ScrDyn *scr_native_iterator_value(ScrNativeIterator *iterator, bool *done_out) { ScrDyn *source = iterator->source; + /* An own element of an untyped array, or its end: no getter can run, so + * the general path below is not needed. */ + if (source && source->kind == SCR_DYN_ARR && iterator->kind == 0 && !iterator->array_like && + iterator->selection == 0) { + if (iterator->index >= (double)source->v.arr.len) { + iterator->source = NULL; + scr_dyn_release(source); + *done_out = true; + return scr_dyn_retain(scr_dyn_undefined()); + } + if (scr_dyn_arr_has_index(source, (size_t)iterator->index)) { + size_t index = (size_t)iterator->index; + iterator->index++; + *done_out = false; + return scr_dyn_retain(source->v.arr.items[index]); + } + } /* An inherited indexed getter can advance this same iterator recursively. * Keep the source alive even if that nested call exhausts the iterator. */ if (source) scr_dyn_retain(source); @@ -5794,7 +7331,18 @@ static ScrDyn *scr_native_iterator_value(ScrNativeIterator *iterator, bool *done } } } else if (items->kind == SCR_DYN_BYTES) { - value = scr_dyn_new_num(scr_bytes_get(items->v.bytes, iterator->index++)); + /* keys(), values() and entries() of a typed array. */ + double index = iterator->index++; + if (iterator->selection == 1) value = scr_dyn_new_num(index); + else { + value = scr_dyn_new_num(scr_bytes_get(items->v.bytes, index)); + if (iterator->selection == 2) { + ScrDyn *entry = scr_dyn_new_arr(); + scr_dyn_arr_push(entry, scr_dyn_new_num(index)); + scr_dyn_arr_push(entry, value); + value = entry; + } + } } else { ScrStr *point = scr_str_cp_at(items->v.str, iterator->index); iterator->index += scr_str_utf16_len(point); @@ -5975,6 +7523,23 @@ static ScrDyn *scr_native_iterator_new(ScrDyn *source) { return result; } +#ifdef SCR_JS_TIER +/* A read site's answer for a receiver without a JS shape whose property is + * plain data the runtime holds directly: a native array's length (the + * generic read would build its checked view first) and a native iterator's + * `next` (the builtin the generic read returns). +1, or NULL for the slow + * path. */ +ScrDyn *scr_dyn_tier_unshaped_get(const ScrDyn *receiver, const ScrStr *key) { + if (key->len == 6 && receiver->kind == SCR_DYN_TYPED_REF && !memcmp(key->data, "length", 6) && + scr_dyn_typed_ref_array_ops(receiver) && scr_dyn_ref_is_array(receiver)) + return scr_dyn_new_num((double)((ScrArr *)receiver->v.typed_ref.ptr)->len); + if (key->len == 4 && receiver->kind == SCR_DYN_HANDLE && + receiver->v.handle.tag == SCR_DYNH_ITERATOR && !memcmp(key->data, "next", 4)) + return scr_native_iterator_get(receiver->v.handle.ptr, key->data, key->len); + return NULL; +} +#endif + static ScrDyn *scr_native_collection_iterator(ScrDyn *source, unsigned selection) { ScrDyn *result = scr_native_iterator_new(source); ((ScrNativeIterator *)result->v.handle.ptr)->selection = selection; @@ -6863,6 +8428,14 @@ ScrDyn *scr_dyn_iterator(const ScrDyn *source, const ScrStr *spell) { scr_dyn_release(method); return scr_dyn_not_iterable(source, spell); } + /* An array whose iterator method is the builtin values(): what calling + * it with the array as `this` returns, without the call. */ + if (method == scr_iterator_functions[1] && + (source->kind == SCR_DYN_ARR || + (source->kind == SCR_DYN_TYPED_REF && scr_dyn_ref_is_array(source)))) { + scr_dyn_release(method); + return scr_native_iterator_new((ScrDyn *)source); + } ScrDyn *iterator = scr_iterator_call(method, source, "value[Symbol.iterator]"); scr_dyn_release(method); if (!iterator) return NULL; @@ -6990,8 +8563,46 @@ void scr_dyn_symbol_key_placeholder(ScrDyn *value, ScrDyn *key) { entry->value = scr_dyn_retain(scr_dyn_undefined()); } +/* Symbol keys that the runtime reads from a view's prototype without + * consulting its own properties: the well-known symbols and + * util.inspect.custom. */ +static bool scr_bytes_protocol_symbol(const ScrDyn *key) { + if (scr_dyn_well_known_symbol(key)) return true; + ScrStr *description = key->v.symbol.description(key->v.symbol.value); + bool custom = description && description->len == 26 && !memcmp(description->data, "nodejs.util.inspect.custom", 26); + scr_str_release(description); + return custom; +} + +/* Before a symbol-keyed write or definition on a view: false with an + * exception pending for a key the view cannot own, else true with the + * view's table created. Symbol.toStringTag is an accessor without a setter + * on %TypedArray%.prototype, so assigning it throws V8's TypeError. */ +static bool scr_bytes_symbol_write(ScrDyn *value, const ScrDyn *key, bool assign) { + ScrDyn *receiver = scr_dyn_symbol_receiver(value); + bool own = receiver && scr_dyn_symbol_entry(receiver, key, true); + scr_dyn_release(receiver); + if (own) return true; + ScrStr *description = key->v.symbol.description(key->v.symbol.value); + bool tag = assign && !value->v.bytes->is_data_view && scr_dyn_well_known_symbol(key) && description && + description->len == 18 && !memcmp(description->data, "Symbol.toStringTag", 18); + if (tag) { + static const char message[] = "Cannot set property Symbol(Symbol.toStringTag) of [object Object] which has only a getter"; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + } else if (scr_bytes_protocol_symbol(key)) { + ScrStr *name = key->v.symbol.render(key->v.symbol.value); + scr_bytes_protocol_fence(name->data, name->len); + scr_str_release(name); + } + scr_str_release(description); + if (scr_exc_pending()) return false; + scr_bytes_properties(value); + return true; +} + void scr_dyn_symbol_key_set(ScrDyn *value, ScrDyn *key, ScrDyn *stored) { if (scr_dyn_error_prototype_fence(value)) return; + if (value->kind == SCR_DYN_BYTES && !scr_bytes_symbol_write(value, key, true)) return; ScrDyn *receiver = scr_dyn_symbol_receiver(value); if (!receiver) { if (!scr_exc_pending()) { @@ -7014,6 +8625,18 @@ void scr_dyn_symbol_key_set(ScrDyn *value, ScrDyn *key, ScrDyn *stored) { scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); } else { bool present = scr_dyn_symbol_entry(receiver, key, true) != NULL; + if (!present && receiver->non_extensible) { + ScrStr *rendered = key->v.symbol.render(key->v.symbol.value); + ScrJsonBuf message; + scr_jb_init(&message); + scr_jb_puts(&message, "Cannot add property "); + scr_jb_write(&message, rendered->data, rendered->len); + scr_jb_puts(&message, ", object is not extensible"); + scr_str_release(rendered); + scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&message)); + scr_dyn_release(receiver); + return; + } scr_dyn_symbol_table(receiver); ScrStr *name = scr_dyn_symbol_storage_key(key); scr_dyn_key_set(receiver->symbol_properties, name, stored); @@ -7023,7 +8646,39 @@ void scr_dyn_symbol_key_set(ScrDyn *value, ScrDyn *key, ScrDyn *stored) { scr_dyn_release(receiver); } +/* scr_dyn_obj_set_field for a symbol-keyed field of a class instance's + * view: the view takes the native value whatever the attributes copied into + * it from the instance's property bag (a frozen instance's view is built the + * same way as any other). An accessor entry takes the ordinary assignment. + * Does not consume value. */ +void scr_dyn_symbol_field_set(ScrDyn *obj, ScrDyn *key, ScrDyn *value) { + ScrDynEntry *entry = obj->kind == SCR_DYN_OBJ && obj->symbol_properties + ? scr_dyn_symbol_entry(obj, key, true) + : NULL; + if (entry && !entry->accessor) { + scr_dyn_region_store(obj->symbol_properties, value); + ScrDyn *old = entry->value; + entry->value = scr_dyn_retain(value); + scr_dyn_release(old); + return; + } + if (entry || obj->kind != SCR_DYN_OBJ) { + scr_dyn_symbol_key_set(obj, key, value); + return; + } + bool non_extensible = obj->non_extensible; + ScrDyn *table = obj->symbol_properties; + bool table_non_extensible = table && table->non_extensible; + obj->non_extensible = false; + if (table) table->non_extensible = false; + scr_dyn_symbol_key_set(obj, key, value); + obj->non_extensible = non_extensible; + if (obj->symbol_properties) + obj->symbol_properties->non_extensible = table ? table_non_extensible : non_extensible; +} + static ScrDyn *scr_dyn_define_symbol(ScrDyn *target, ScrDyn *key, ScrDyn *descriptor) { + if (target->kind == SCR_DYN_BYTES && !scr_bytes_symbol_write(target, key, false)) return NULL; ScrDyn *receiver = scr_dyn_symbol_receiver(target); if (!receiver) { if (!scr_exc_pending()) { @@ -7092,6 +8747,22 @@ bool scr_dyn_has_key_computed(const ScrDyn *value, const ScrDyn *key) { return s bool scr_dyn_has_own_computed(const ScrDyn *value, const ScrDyn *key) { return scr_dyn_key_probe_computed(value, key, 1); } bool scr_dyn_property_is_enumerable_computed(const ScrDyn *value, const ScrDyn *key) { return scr_dyn_key_probe_computed(value, key, 2); } +static void scr_json_delete_member(ScrDyn *object, const ScrStr *key); + +void scr_dyn_obj_delete_symbol_field(ScrDyn *obj, ScrDyn *symbol) { + if (obj->kind != SCR_DYN_OBJ || !obj->symbol_properties) return; + ScrStr *name = scr_dyn_symbol_storage_key(symbol); + scr_json_delete_member(obj->symbol_properties, name); + scr_str_release(name); + for (size_t i = 0; obj->symbol_keys && i < obj->symbol_keys->v.arr.len; i++) { + if (!scr_dyn_strict_eq(obj->symbol_keys->v.arr.items[i], symbol)) continue; + scr_dyn_release(obj->symbol_keys->v.arr.items[i]); + size_t remaining = --obj->symbol_keys->v.arr.len - i; + memmove(&obj->symbol_keys->v.arr.items[i], &obj->symbol_keys->v.arr.items[i + 1], remaining * sizeof(ScrDyn *)); + break; + } +} + void scr_dyn_key_delete_computed(ScrDyn *value, const ScrDyn *raw_key, bool strict) { ScrDyn *key = scr_dyn_property_key_value(raw_key); if (!key) return; @@ -7124,6 +8795,45 @@ void scr_dyn_key_delete_computed(ScrDyn *value, const ScrDyn *raw_key, bool stri scr_dyn_release(key); } +/* Reflect.deleteProperty(target, key): a TypeError for a primitive target, + * then the key's ToPropertyKey and a sloppy-mode delete. Answers whether + * the property is absent afterwards (false when it is non-configurable). + * A Proxy would observe the own-property check through its traps, and a + * compiled object's native fields keep their values after a delete through + * its checked-dynamic view, so both refuse. */ +bool scr_dyn_reflect_delete_property(ScrDyn *target, const ScrDyn *raw_key) { + switch (target->kind) { + case SCR_DYN_NULL: + case SCR_DYN_UNDEF: + case SCR_DYN_BOOL: + case SCR_DYN_NUM: + case SCR_DYN_STR: + case SCR_DYN_BIGINT: + case SCR_DYN_SYMBOL: { + static const char message[] = "Reflect.deleteProperty called on non-object"; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + return false; + } + default: + break; + } + if (target->kind == SCR_DYN_PROXY) { + scr_dyn_proxy_unsupported("Reflect.deleteProperty"); + return false; + } + if (target->kind == SCR_DYN_TYPED_REF) { + static const char message[] = "Reflect.deleteProperty on a compiled object is not supported yet"; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); + return false; + } + ScrDyn *key = scr_dyn_property_key_value(raw_key); + if (!key) return false; + scr_dyn_key_delete_computed(target, key, false); + bool deleted = !scr_exc_pending() && !scr_dyn_has_own_computed(target, key); + scr_dyn_release(key); + return deleted; +} + /* Native call/apply/bind lowering still owns these prototype operations. * Refuse overrides until every typed call site observes the property table. */ static bool scr_dyn_fn_property_fence(const ScrStr *key) { @@ -7218,6 +8928,63 @@ ScrDyn *scr_dyn_define_property(ScrDyn *target, ScrDyn *key, ScrDyn *descriptor) scr_dyn_release(defined); return scr_dyn_retain(target); } + if (target->kind == SCR_DYN_BYTES) { + ScrStr *name = scr_dyn_property_key(key); + if (!name) return NULL; + double index; + bool numeric = !target->v.bytes->is_data_view && scr_bytes_numeric_key(name, &index); + if (numeric) { + /* [[DefineOwnProperty]] of an element: a valid index and a data + * descriptor that keeps it writable, enumerable and configurable. */ + /* The descriptor is read first (ToPropertyDescriptor), through its + * getters, and must be an object. */ + ScrDyn *fields[6]; + if (!scr_dyn_descriptor_fields_read(descriptor, fields)) { + scr_str_release(name); + return NULL; + } + const ScrDyn *enumerable = fields[0], *configurable = fields[1], *value = fields[2]; + const ScrDyn *writable = fields[3]; + bool accessor = fields[4] || fields[5]; + bool valid = !signbit(index) && trunc(index) == index && index < (double)target->v.bytes->len; + if (!valid) { + scr_dyn_descriptor_fields_drop(fields); + scr_str_release(name); + static const char message[] = "Invalid typed array index"; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + return NULL; + } + if (accessor || (configurable && !scr_dyn_truthy(configurable)) || + (enumerable && !scr_dyn_truthy(enumerable)) || (writable && !scr_dyn_truthy(writable))) { + scr_dyn_descriptor_fields_drop(fields); + ScrJsonBuf message; + scr_jb_init(&message); + scr_jb_puts(&message, "Cannot redefine property: "); + scr_jb_write(&message, name->data, name->len); + scr_str_release(name); + scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&message)); + return NULL; + } + scr_str_release(name); + double number; + bool stored = true; + if (value) { + stored = scr_dyn_number_coerce_js(value, &number); + /* The conversion can run user code; the index is checked again. */ + if (stored && index < (double)target->v.bytes->len) scr_bytes_set(target->v.bytes, index, number); + } + scr_dyn_descriptor_fields_drop(fields); + return stored ? scr_dyn_retain(target) : NULL; + } + bool fenced = scr_bytes_protocol_name(name) && !scr_bytes_has_named(target, name); + if (fenced) scr_bytes_protocol_fence(name->data, name->len); + scr_str_release(name); + if (fenced) return NULL; + ScrDyn *defined = scr_dyn_define_property(scr_bytes_properties(target), key, descriptor); + if (!defined) return NULL; + scr_dyn_release(defined); + return scr_dyn_retain(target); + } if (target->kind != SCR_DYN_OBJ) { static const char msg[] = "Object.defineProperty called on non-object"; scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1); @@ -7225,44 +8992,16 @@ ScrDyn *scr_dyn_define_property(ScrDyn *target, ScrDyn *key, ScrDyn *descriptor) } ScrStr *name = scr_dyn_property_key(key); if (!name) return NULL; - if (descriptor->kind != SCR_DYN_OBJ) { + ScrDyn *fields[6]; + if (!scr_dyn_descriptor_fields_read(descriptor, fields)) { scr_str_release(name); - static const char msg[] = "Property description must be an object"; - scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1); return NULL; } - static const char *const names[] = {"enumerable", "configurable", "value", "writable", "get", "set"}; - static const size_t lengths[] = {10, 12, 5, 8, 3, 3}; - ScrDyn *fields[6] = {NULL}; - for (size_t i = 0; i < 6; i++) { - if (!scr_dyn_obj_get(descriptor, names[i], lengths[i])) continue; - fields[i] = scr_dyn_obj_read(descriptor, names[i], lengths[i]); - if (!fields[i]) { - scr_dyn_descriptor_fields_drop(fields); - scr_str_release(name); - return NULL; - } - } ScrDyn *e = fields[0], *c = fields[1], *value = fields[2]; ScrDyn *w = fields[3], *get = fields[4], *set = fields[5]; ScrDynEntry *prior = scr_dyn_entry(target, name); bool accessor_descriptor = get || set; bool data_descriptor = value || w; - if (accessor_descriptor && data_descriptor) { - scr_dyn_descriptor_fields_drop(fields); - scr_str_release(name); - static const char msg[] = "Invalid property descriptor. Cannot both specify accessors and a value or writable attribute"; - scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1); - return NULL; - } - if ((get && get->kind != SCR_DYN_UNDEF && get->kind != SCR_DYN_FUNC) || - (set && set->kind != SCR_DYN_UNDEF && set->kind != SCR_DYN_FUNC)) { - scr_dyn_descriptor_fields_drop(fields); - scr_str_release(name); - static const char msg[] = "Getter and setter must be functions"; - scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1); - return NULL; - } bool accessor = accessor_descriptor || (!data_descriptor && prior && prior->accessor); bool writable = w ? scr_dyn_truthy(w) : prior && !prior->accessor && !accessor ? prior->writable : false; bool enumerable = e ? scr_dyn_truthy(e) : prior ? prior->enumerable : false; @@ -7294,6 +9033,13 @@ ScrDyn *scr_dyn_define_property(ScrDyn *target, ScrDyn *key, ScrDyn *descriptor) scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&buffer)); return NULL; } + if (prior && prior->fenced && + (accessor || (value && !scr_dyn_property_same_value(prior->value, value)))) { + scr_dyn_descriptor_fields_drop(fields); + scr_str_release(name); + scr_dyn_namespace_member_fence(); + return NULL; + } if (prior) { if (accessor != prior->accessor) { ScrDyn *old_value = prior->value, *old_getter = prior->getter, *old_setter = prior->setter; @@ -7365,7 +9111,7 @@ ScrDyn *scr_dyn_get_own_property_descriptor(ScrDyn *target, ScrDyn *key) { scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1); return NULL; } - if (target->kind != SCR_DYN_OBJ && target->kind != SCR_DYN_PROXY && target->kind != SCR_DYN_FUNC && target->kind != SCR_DYN_ARR && target->kind != SCR_DYN_STR && target->kind != SCR_DYN_BOOL && target->kind != SCR_DYN_NUM && target->kind != SCR_DYN_TYPED_REF) { + if (target->kind != SCR_DYN_OBJ && target->kind != SCR_DYN_PROXY && target->kind != SCR_DYN_FUNC && target->kind != SCR_DYN_ARR && target->kind != SCR_DYN_STR && target->kind != SCR_DYN_BOOL && target->kind != SCR_DYN_NUM && target->kind != SCR_DYN_TYPED_REF && target->kind != SCR_DYN_BYTES) { static const char msg[] = "Object.getOwnPropertyDescriptor on this value is not supported yet"; scr_throw_error_msg(SCR_ERR_ERROR, msg, sizeof msg - 1); return NULL; @@ -7446,7 +9192,9 @@ bool scr_dyn_is_callable(const ScrDyn *value) { int scr_dyn_function_kind(const ScrDyn *value) { while (value->kind == SCR_DYN_PROXY) value = value->v.proxy.target; - return value->kind == SCR_DYN_FUNC ? scr_closure_identity(value->v.fn.clo)->function_kind : 0; + return value->kind == SCR_DYN_FUNC + ? (int)(scr_closure_identity(value->v.fn.clo)->function_kind & SCR_FN_KIND_MASK) + : 0; } void scr_dyn_adopt_identity(ScrClosure *adapter, const ScrDyn *value) { @@ -7497,11 +9245,66 @@ void scr_dyn_throw(ScrDyn *d) { scr_dyn_trace_v, scr_dyn_is_object(d)); } +static ScrStr *scr_dyn_tag_string(const ScrStr *tag) { + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_puts(&b, "[object "); + scr_jb_write(&b, tag->data, tag->len); + scr_jb_putc(&b, ']'); + return scr_jb_finish(&b); +} + +/* The Symbol.toStringTag getter's string of a class instance whose + * ToString is Object.prototype.toString (its class has no toString; the + * string hint never reaches valueOf, since that answers a string): +1, or + * NULL otherwise or with the getter's exception pending. */ +static ScrStr *scr_dyn_typed_ref_conversion_tag(const ScrDyn *d) { + ScrStr *to_string_key = scr_str_new("toString", 8); + bool own_conversion = scr_dyn_typed_ref_class_has(d, to_string_key); + scr_str_release(to_string_key); + return own_conversion ? NULL : scr_dyn_typed_ref_class_tag(d); +} + +bool scr_dyn_typed_ref_put_tag(ScrJsonBuf *b, const ScrDyn *d) { + ScrStr *tag = scr_dyn_typed_ref_conversion_tag(d); + if (!tag) return scr_exc_pending(); + scr_jb_puts(b, "[object "); + scr_jb_write(b, tag->data, tag->len); + scr_jb_putc(b, ']'); + scr_str_release(tag); + return true; +} + +/* Object.prototype.toString of an object whose own or inherited + * @@toStringTag is a string (+1), NULL otherwise or with a pending + * exception from its getter. */ +static ScrStr *scr_dyn_string_tag(const ScrDyn *d) { + ScrStr *getter_tag = scr_dyn_typed_ref_class_tag(d); + if (getter_tag) { + ScrStr *out = scr_dyn_tag_string(getter_tag); + scr_str_release(getter_tag); + return out; + } + if (scr_exc_pending() || !scr_to_string_tag_symbol) return NULL; + ScrDyn *tag = scr_dyn_symbol_key_get(d, scr_to_string_tag_symbol, true); + if (!tag) return NULL; + ScrStr *out = tag->kind == SCR_DYN_STR ? scr_dyn_tag_string(tag->v.str) : NULL; + scr_dyn_release(tag); + return out; +} + ScrStr *scr_dyn_object_tag(const ScrDyn *d) { if (d->kind == SCR_DYN_PROXY) { scr_dyn_proxy_unsupported("Symbol.toStringTag lookup"); return NULL; } + /* Object.prototype.toString answers a string @@toStringTag over the + * builtin tag of a plain object or a class instance. */ + if ((d->kind == SCR_DYN_OBJ || (d->kind == SCR_DYN_TYPED_REF && !scr_dyn_ref_is_error(d) && + !scr_dyn_generator(d)))) { + ScrStr *tag = scr_dyn_string_tag(d); + if (tag || scr_exc_pending()) return tag; + } if (d->kind == SCR_DYN_TYPED_REF) { if (scr_dyn_ref_is_error(d)) return scr_str_new("[object Error]", 14); if (scr_dyn_generator(d)) { @@ -7669,7 +9472,8 @@ static bool scr_dyn_json_write(ScrJsonBuf *b, const ScrDyn *d) { const ScrBytes *bytes = d->v.bytes; if (d->buffer) scr_jb_puts(b, "{\"type\":\"Buffer\",\"data\":["); else scr_jb_putc(b, '{'); - for (size_t i = 0; i < bytes->len; i++) { + size_t count = bytes->is_data_view ? 0 : bytes->len; + for (size_t i = 0; i < count; i++) { if (i > 0) scr_jb_putc(b, ','); if (!d->buffer) { char idx[32]; @@ -7678,6 +9482,32 @@ static bool scr_dyn_json_write(ScrJsonBuf *b, const ScrDyn *d) { } scr_jb_put_f64(b, scr_bytes_get(bytes, (double)i)); } + /* Named properties follow the elements; a Buffer's toJSON leaves them + * out. */ + ScrDyn *named = d->buffer || !bytes->properties ? NULL : scr_dyn_obj_keys(bytes->properties); + bool first = count == 0; + for (size_t i = 0; named && i < named->v.arr.len; i++) { + const ScrStr *key = named->v.arr.items[i]->v.str; + ScrDyn *value = scr_dyn_obj_read_receiver(bytes->properties, key->data, key->len, d); + if (!value) { scr_dyn_release(named); return false; } + ScrJsonBuf probe; + scr_jb_init(&probe); + bool present = scr_dyn_json_write(&probe, value); + scr_dyn_release(value); + if (!present) { + scr_jb_dispose(&probe); + if (scr_exc_pending()) { scr_dyn_release(named); return false; } + continue; + } + if (!first) scr_jb_putc(b, ','); + first = false; + scr_jb_put_json_str(b, key); + scr_jb_putc(b, ':'); + ScrStr *body = scr_jb_finish(&probe); + scr_jb_write(b, body->data, body->len); + scr_str_release(body); + } + scr_dyn_release(named); scr_jb_puts(b, d->buffer ? "]}" : "}"); return true; } @@ -7749,6 +9579,12 @@ typedef struct { * or NULL. The code slot holds only strings, and a subclass's typed view * rebuilds its properties from the slots on every read. */ ScrDyn *code; + /* The prototype a view built from the error's fields starts with + * (scr_error_dyn_fields: the holder of its default name and message, or + * none), retained so that no later object takes its address. Only such a + * view sets `fields_view`. */ + ScrDyn *view_prototype; + bool fields_view; } ScrErrDynEnt; static SCR_TL ScrErrDynEnt *scr_errdyn_cache = NULL; @@ -7853,6 +9689,7 @@ static void scr_errdyn_sweep(void) { scr_error_release(dead[i].err); scr_errdyn_release_node(dead[i].dyn); scr_dyn_release(dead[i].code); + scr_dyn_release(dead[i].view_prototype); } free(dead); } @@ -7874,6 +9711,8 @@ static void scr_errdyn_append(ScrError *e, ScrDyn *d) { scr_errdyn_cache[scr_errdyn_n].err = scr_error_retain(e); scr_errdyn_cache[scr_errdyn_n].dyn = scr_dyn_retain(d); scr_errdyn_cache[scr_errdyn_n].code = NULL; + scr_errdyn_cache[scr_errdyn_n].view_prototype = NULL; + scr_errdyn_cache[scr_errdyn_n].fields_view = false; scr_errdyn_n++; if (scr_errdyn_n * 2 > scr_errdyn_slots) { free(scr_errdyn_by_err); @@ -7930,6 +9769,7 @@ static void scr_errdyn_teardown(void) { scr_error_release(cache[i].err); scr_errdyn_release_node(cache[i].dyn); scr_dyn_release(cache[i].code); + scr_dyn_release(cache[i].view_prototype); } free(cache); } @@ -8033,6 +9873,19 @@ static void scr_error_dyn_add_stack(ScrDyn *d) { entry->enumerable = false; } +/* Whether `object` is the view scr_error_dyn_fields built for an error + * created as an Error (not a subclass or another error kind) and its + * prototype is still the one the view started with. Such an error's + * prototype is Error.prototype. */ +static bool scr_dyn_error_view_on_error_prototype(const ScrDyn *object) { + if (object->null_proto) return false; + size_t hit = scr_errdyn_find(object, false); + if (hit == SIZE_MAX) return false; + const ScrErrDynEnt *ent = &scr_errdyn_cache[hit]; + return ent->fields_view && object->prototype == ent->view_prototype && + ent->err->vt == &scr_error_vts[SCR_ERR_ERROR]; +} + ScrDyn *scr_error_dyn_fields(const ScrError *e) { ScrDyn *d = scr_dyn_new_obj(); scr_dyn_obj_set(d, "%error", 6, scr_dyn_new_bool(true)); /* the checked-dynamic tree's error marker */ @@ -8152,6 +10005,102 @@ void scr_error_dyn_place_name(ScrDyn *view, const char *after, size_t after_len) view->v.obj.index = NULL; } +static size_t scr_dyn_entry_index(const ScrDyn *object, const char *key, size_t key_len) { + for (size_t i = 0; i < object->v.obj.len; i++) { + const ScrDynEntry *entry = &object->v.obj.entries[i]; + if (entry->key_len == key_len && memcmp(entry->key, key, key_len) == 0) return i; + } + return SIZE_MAX; +} + +/* Whether bag entry b is one of the entries Error's constructor creates + * before the instance's class fields: `stack`, a hidden entry (a key + * starting with '%'), and a `message` that directly follows them. */ +static bool scr_error_bag_constructed(const ScrDyn *bag, size_t b) { + for (size_t i = 0; i <= b; i++) { + const ScrDynEntry *entry = &bag->v.obj.entries[i]; + bool constructed = (entry->key_len == 5 && memcmp(entry->key, "stack", 5) == 0) || + (entry->key_len > 0 && entry->key[0] == '%') || + (entry->key_len == 7 && memcmp(entry->key, "message", 7) == 0); + if (!constructed) return false; + } + return true; +} + +/* The view index at which bag entry b, now at `from`, belongs: after the + * nearest earlier bag property the view has. An entry created after + * construction (any `name` placed here, since no constructor of the class + * sets it, and a `message` that is not one of Error's constructor entries) + * also follows the class fields, which live in slots and not in the bag: it + * passes over the constructor's entries and over a `name` the construction + * set, and without another earlier property goes before the nearest later + * one the view has, or stays where the view put it. A `message` Error's + * constructor created follows the nearest earlier entry, or goes first. */ +static size_t scr_error_bag_place(const ScrDyn *view, const ScrDyn *bag, size_t b, bool name, + bool name_placed, size_t from) { + bool late = name || !scr_error_bag_constructed(bag, b); + for (size_t p = b; p-- > 0;) { + const ScrDynEntry *anchor = &bag->v.obj.entries[p]; + if (late && scr_error_bag_constructed(bag, p)) break; + if (late && name_placed && anchor->key_len == 4 && memcmp(anchor->key, "name", 4) == 0) continue; + size_t after = scr_dyn_entry_index(view, anchor->key, anchor->key_len); + if (after != SIZE_MAX) return after < from ? after + 1 : after; + } + if (!late) return 0; + for (size_t p = b + 1; p < bag->v.obj.len; p++) { + size_t before = scr_dyn_entry_index(view, bag->v.obj.entries[p].key, bag->v.obj.entries[p].key_len); + if (before != SIZE_MAX) return before > from ? before - 1 : before; + } + return from; +} + +/* The bag holds the view as it was last committed, in the order its + * properties were created: zod's error factory assigns `name` and then + * defines `message`, and Node lists them in that order. The slots rebuild + * both at fixed positions (scr_error_dyn_fields, scr_error_dyn_place_name), + * so each one present in the bag moves to follow the property it follows + * there. A typed write to `name` or `message` reaches only the slot, so a + * data property takes the slot's value. */ +void scr_error_dyn_follow_bag(ScrDyn *view, const ScrDyn *bag, const ScrError *e, bool name_placed) { + if (!bag || bag->kind != SCR_DYN_OBJ || view->kind != SCR_DYN_OBJ) return; + static const struct { const char *key; size_t len; } slots[] = {{"name", 4}, {"message", 7}}; + for (size_t s = 0; s < 2; s++) { + bool name = s == 0; + if (!(name ? e->name_present : e->message_present)) continue; + size_t at = scr_dyn_entry_index(view, slots[s].key, slots[s].len); + if (at == SIZE_MAX || view->v.obj.entries[at].accessor) continue; + ScrDyn *value = scr_dyn_new_str(name ? e->name : e->message); + scr_dyn_region_store(view, value); + ScrDyn *old = view->v.obj.entries[at].value; + view->v.obj.entries[at].value = value; + scr_dyn_release(old); + } + bool moved = false; + for (size_t b = 0; b < bag->v.obj.len; b++) { + const ScrDynEntry *bag_entry = &bag->v.obj.entries[b]; + bool slot = (bag_entry->key_len == 4 && memcmp(bag_entry->key, "name", 4) == 0) || + (bag_entry->key_len == 7 && memcmp(bag_entry->key, "message", 7) == 0); + bool name = bag_entry->key_len == 4; + /* A `name` the class's construction sets is in place already. */ + if (!slot || (name && name_placed)) continue; + size_t from = scr_dyn_entry_index(view, bag_entry->key, bag_entry->key_len); + if (from == SIZE_MAX) continue; + size_t to = scr_error_bag_place(view, bag, b, name, name_placed, from); + if (to == from) continue; + ScrDynEntry *entries = view->v.obj.entries; + ScrDynEntry entry = entries[from]; + if (to < from) memmove(&entries[to + 1], &entries[to], (from - to) * sizeof *entries); + else memmove(&entries[from], &entries[from + 1], (to - from) * sizeof *entries); + entries[to] = entry; + moved = true; + } + if (moved) { + SCR_JS_SHAPE_RESET(view); + free(view->v.obj.index); + view->v.obj.index = NULL; + } +} + ScrDyn *scr_dyn_from_error(const ScrError *e) { size_t hit = scr_errdyn_find(e, true); if (hit != SIZE_MAX) return scr_dyn_retain(scr_errdyn_cache[hit].dyn); @@ -8162,8 +10111,14 @@ ScrDyn *scr_dyn_from_error(const ScrError *e) { break; } } + bool fields_view = d == NULL; if (!d) d = scr_error_dyn_fields(e); scr_errdyn_append((ScrError *)e, d); + if (fields_view) { + ScrErrDynEnt *ent = &scr_errdyn_cache[scr_errdyn_n - 1]; + ent->fields_view = true; + ent->view_prototype = d->prototype ? scr_dyn_retain(d->prototype) : NULL; + } return d; } @@ -8455,7 +10410,8 @@ ScrStr *scr_dyn_to_string(const ScrDyn *d, const ScrStr *enc) { if (scr_dyn_error_encoding(d) && !scr_dyn_property_owner(d, "toString", 8)) return scr_dyn_error_string(d); return scr_str_new("[object Object]", 15); case SCR_DYN_HANDLE: - if (d->v.handle.tag == SCR_DYNH_REGEXP || d->v.handle.tag == SCR_DYNH_URL || d->v.handle.tag == SCR_DYNH_DATE) { + if (d->v.handle.tag == SCR_DYNH_REGEXP || d->v.handle.tag == SCR_DYNH_URL || d->v.handle.tag == SCR_DYNH_DATE || + d->v.handle.tag == SCR_DYNH_MIME_TYPE || d->v.handle.tag == SCR_DYNH_MIME_PARAMS) { ScrDyn *text = scr_dyn_handle_ops_of(d)->invoke(d->v.handle.ptr, (ScrDyn *)d, "toString", NULL, 0, "RegExp.toString"); if (!text) return NULL; ScrStr *out = scr_str_retain(text->v.str); @@ -8719,6 +10675,13 @@ ScrStr *scr_dyn_string_coerce_js(const ScrDyn *d) { return NULL; } if (d->kind == SCR_DYN_TYPED_REF) { + ScrStr *tag = scr_dyn_typed_ref_conversion_tag(d); + if (tag) { + ScrStr *out = scr_dyn_tag_string(tag); + scr_str_release(tag); + return out; + } + if (scr_exc_pending()) return NULL; ScrDyn *materialized = scr_dyn_typed_ref_string_view(d); ScrStr *out = scr_dyn_string_coerce_js(materialized); scr_dyn_release(materialized); @@ -8739,7 +10702,9 @@ ScrStr *scr_dyn_string_coerce_js(const ScrDyn *d) { scr_error_release(error); return text; } - return scr_str_new("[object Object]", 15); + // Object.prototype.toString: "[object Object]", or the object's + // Symbol.toStringTag ("[object Math]"). + return scr_dyn_object_tag(d); } if (!m || m->kind != SCR_DYN_FUNC) { scr_dyn_release(m); continue; } /* OrdinaryToPrimitive performs a method call, not a bare function @@ -9195,9 +11160,8 @@ ScrDyn *scr_dyn_arithmetic(const ScrDyn *left, const ScrDyn *right, const ScrStr if (result) { out = scr_dyn_new_bigint(result); scr_bigint_release(result); } } else { double a = l->v.num, b = r->v.num; - // ECMAScript differs from libm for ±1 ** ±Infinity and 1 ** NaN. double result = op == '-' ? a - b : op == '/' ? a / b : op == '%' ? fmod(a, b) - : operation->len == 2 ? ((fabs(a) == 1 && !isfinite(b)) ? NAN : pow(a, b)) : a * b; + : operation->len == 2 ? scr_math_pow(a, b) : a * b; out = scr_dyn_new_num(result); } scr_dyn_release(l); @@ -9317,6 +11281,7 @@ bool scr_dyn_has_key(const ScrDyn *v, const ScrStr *key) { } if (v->kind == SCR_DYN_STR) return scr_dyn_canonical_own_index(key, (size_t)scr_str_utf16_len(v->v.str)); + if (v->kind == SCR_DYN_BYTES) return scr_dyn_bytes_in(v, key); return false; } @@ -9369,10 +11334,38 @@ bool scr_dyn_in(const ScrDyn *v, const ScrStr *key) { return false; } +/* Node's TypeErrors for assigning a read-only property and deleting a + * non-configurable one of a checked object: the object reads as + * #, or [object Object] without a prototype. */ +static void scr_dyn_throw_property(const ScrDyn *object, const char *key, size_t key_len, bool deleting) { + const char *name = scr_dyn_class_name(object); + ScrJsonBuf buffer; + scr_jb_init(&buffer); + scr_jb_puts(&buffer, deleting ? "Cannot delete property '" : "Cannot assign to read only property '"); + scr_jb_write(&buffer, key, key_len); + scr_jb_puts(&buffer, deleting ? "' of " : "' of object '"); + if (object->kind == SCR_DYN_BYTES) { + /* V8 names a typed array by its tag and a DataView by its class. */ + const char *kind = object->v.bytes->is_data_view ? "DataView" : scr_bytes_elem_name(object->v.bytes->elem); + scr_jb_puts(&buffer, object->v.bytes->is_data_view ? "#<" : "[object "); + scr_jb_puts(&buffer, kind); + scr_jb_puts(&buffer, object->v.bytes->is_data_view ? ">" : "]"); + } else if (object->null_proto) scr_jb_puts(&buffer, "[object Object]"); + else { + scr_jb_puts(&buffer, "#<"); + scr_jb_puts(&buffer, name ? name : "Object"); + scr_jb_puts(&buffer, ">"); + } + if (!deleting) scr_jb_puts(&buffer, "'"); + scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&buffer)); +} + +/* One lookup of the own property decides the assignment: an own property + * is written (or runs its setter, or refuses the write); an absent one + * consults the prototype chain and is then added. */ static void scr_dyn_object_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value, const ScrDyn *receiver) { - for (size_t i = 0; i < recv->v.obj.len; i++) { - ScrDynEntry *entry = &recv->v.obj.entries[i]; - if (entry->key_len != key->len || memcmp(entry->key, key->data, key->len) != 0) continue; + ScrDynEntry *entry = scr_dyn_entry(recv, key); + if (entry) { if (entry->accessor && entry->setter) { ScrDyn *args[] = {value}; ScrDyn *setter = scr_dyn_retain(entry->setter); @@ -9384,12 +11377,21 @@ static void scr_dyn_object_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value, con return; } if (entry->accessor || !entry->writable) { - static const char msg[] = "Cannot assign to read only property"; - scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1); + scr_dyn_throw_property(receiver, key->data, key->len, false); return; } + if (entry->fenced) { + scr_dyn_namespace_member_fence(); + return; + } + scr_dyn_region_store(recv, value); + ScrDyn *old = entry->value; + entry->value = scr_dyn_retain(value); + scr_dyn_release(old); + scr_error_sync_cause(recv, key); + return; } - if (!scr_dyn_entry(recv, key)) { + { for (const ScrDyn *current = scr_dyn_proto_next(recv); current;) { ScrDyn *table = current->kind == SCR_DYN_FUNC ? scr_dyn_fn_properties(current) : NULL; const ScrDyn *object = table ? table : current; @@ -9412,8 +11414,7 @@ static void scr_dyn_object_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value, con return; } if (inherited->accessor || !inherited->writable) { - static const char message[] = "Cannot assign to read only property"; - scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + scr_dyn_throw_property(receiver, key->data, key->len, false); scr_dyn_release(table); return; } @@ -9421,7 +11422,7 @@ static void scr_dyn_object_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value, con break; } } - if (recv->non_extensible && !scr_dyn_entry(recv, key)) { + if (recv->non_extensible) { ScrJsonBuf buffer; scr_jb_init(&buffer); scr_jb_puts(&buffer, "Cannot add property "); @@ -9430,9 +11431,8 @@ static void scr_dyn_object_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value, con scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&buffer)); return; } - bool created = scr_dyn_entry(recv, key) == NULL; scr_dyn_obj_set(recv, key->data, key->len, scr_dyn_retain(value)); - if (created && key->len == 5 && memcmp(key->data, "stack", 5) == 0 && + if (key->len == 5 && memcmp(key->data, "stack", 5) == 0 && scr_errdyn_find(recv, false) != SIZE_MAX) { /* An error's own `stack` exists from construction as a non-enumerable * data property, so assigning it adds no enumerable key. */ @@ -9493,8 +11493,7 @@ void scr_dyn_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value) { if (recv->kind == SCR_DYN_ARR) { if (key->len == 6 && memcmp(key->data, "length", 6) == 0) { if (recv->v.arr.frozen) { - static const char message[] = "Cannot assign to read only property"; - scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + scr_array_frozen_error(3, recv->v.arr.len, 0, 0, 0); return; } double number; @@ -9513,8 +11512,7 @@ void scr_dyn_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value) { while (recv->v.arr.len < length) scr_dyn_arr_push_hole(recv); while (recv->v.arr.len > length) { if (recv->v.arr.sealed && scr_dyn_arr_has_index(recv, recv->v.arr.len - 1)) { - static const char message[] = "Cannot delete non-configurable property"; - scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + scr_array_frozen_error(5, recv->v.arr.len, (double)(recv->v.arr.len - 1), 0, 0); return; } scr_dyn_release(recv->v.arr.items[--recv->v.arr.len]); @@ -9532,14 +11530,13 @@ void scr_dyn_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value) { } if (is_index && idx < 4294967295ULL) { if (scr_dyn_array_prototype_element_fence(recv)) return; - if (recv->v.arr.frozen && idx < recv->v.arr.len) { - static const char message[] = "Cannot assign to read only property"; - scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + if (recv->v.arr.frozen && idx < recv->v.arr.len && scr_dyn_arr_has_index(recv, idx)) { + scr_array_frozen_error(0, recv->v.arr.len, (double)idx, 0, 0); return; } if (recv->non_extensible && !scr_dyn_arr_has_index(recv, idx)) { - static const char message[] = "Cannot add property, object is not extensible"; - scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + /* An absent index, a hole included, is a property to add. */ + scr_array_frozen_error(0, 0, (double)idx, 0, 0); return; } if (idx > recv->v.arr.len + 1000000) { @@ -9571,19 +11568,36 @@ void scr_dyn_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value) { } if (recv->kind == SCR_DYN_BYTES) { /* Integer-indexed exotic writes coerce even for an invalid canonical - * numeric index, then ignore an out-of-range index without an error. */ - double index = scr_string_to_number(key); - char spelling[32]; - size_t length = scr_f64_to_str(index, spelling); - bool negative_zero = key->len == 2 && memcmp(key->data, "-0", 2) == 0; - if (negative_zero || (length == key->len && memcmp(spelling, key->data, length) == 0)) { + * numeric index, then ignore an out-of-range index without an error. + * A DataView has no elements: its numeric keys are named properties. */ + double index; + if (!recv->v.bytes->is_data_view && scr_bytes_numeric_key(key, &index)) { double number; if (!scr_dyn_number_coerce_js(value, &number)) return; - if (!negative_zero && index >= 0 && index < (double)recv->v.bytes->len && trunc(index) == index) { + if (!signbit(index) && index < (double)recv->v.bytes->len && trunc(index) == index) { scr_bytes_set(recv->v.bytes, index, number); } return; } + /* Any other key is a named property of the view. */ + if (!scr_bytes_has_named(recv, key)) { + if (scr_bytes_getter_name(recv, key)) { + ScrJsonBuf message; + scr_jb_init(&message); + scr_jb_puts(&message, "Cannot set property "); + scr_jb_write(&message, key->data, key->len); + scr_jb_puts(&message, recv->v.bytes->is_data_view ? " of # which has only a getter" + : " of [object Object] which has only a getter"); + scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&message)); + return; + } + if (scr_bytes_protocol_name(key)) { + scr_bytes_protocol_fence(key->data, key->len); + return; + } + } + scr_dyn_object_key_set(scr_bytes_properties(recv), key, value, recv); + return; } ScrJsonBuf b; @@ -9680,7 +11694,53 @@ static const char *scr_dyn_kind_name(const ScrDyn *d) { return "unknown"; } +/* Shape inference's overlay (compiler frontend/shape-inference): a checked + * conversion of an analysis-proven fact names its site (" + * :") while it runs. A failure there is a compiler analysis + * bug, so it reports itself as one instead of as a type error. */ +static SCR_TL const char *scr_dyn_inferred_site_name; + +void scr_dyn_inferred_site(const char *site) { scr_dyn_inferred_site_name = site; } + +static void scr_dyn_inferred_fact_fail(const char *site, const ScrDynPath *path, const char *want, + const ScrDyn *got) { + const char *space = strchr(site, ' '); + size_t pkg_len = space ? (size_t)(space - site) : 0; + const char *where = space ? space + 1 : site; + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_puts(&b, "scriptc internal error: an inferred type was contradicted at "); + scr_jb_puts(&b, where); + scr_jb_puts(&b, " (expected "); + scr_jb_puts(&b, want); + scr_jb_puts(&b, " at "); + scr_dyn_path_render(&b, path); + scr_jb_puts(&b, ", got "); + scr_jb_puts(&b, scr_dyn_kind_name(got)); + scr_jb_puts(&b, "). This is a bug in scriptc's shape analysis, not in the program's data. " + "Rebuild with --no-shape-overlay, or set \"scriptc\": {\"packages\": {\""); + for (size_t i = 0; i < pkg_len; i++) scr_jb_putc(&b, site[i]); + scr_jb_puts(&b, "\": {\"shapes\": false}}} in package.json, to compile the package without it."); + /* Validation mode: every hit is logged, also when the program catches it. */ + const char *log = getenv("SCRIPTC_SHAPE_CHECK_LOG"); + if (log && *log) { + FILE *f = fopen(log, "a"); + if (f) { + fwrite(b.data, 1, b.len, f); + fputc('\n', f); + fclose(f); + } + } + scr_throw_error(SCR_ERR_ERROR, scr_jb_finish(&b)); +} + void scr_dyn_check_fail(const ScrDynPath *path, const char *want, const ScrDyn *got) { + const char *site = scr_dyn_inferred_site_name; + if (site) { + scr_dyn_inferred_site_name = NULL; + scr_dyn_inferred_fact_fail(site, path, want, got); + return; + } ScrJsonBuf b; scr_jb_init(&b); scr_jb_puts(&b, "expected "); @@ -9738,6 +11798,14 @@ static SCR_TL ScrDynVisitLog scr_dyn_viewed_log; /* The member value a matcher or builder read last and holds one * reference to until it calls scr_dyn_release_held. */ static SCR_TL const ScrDyn *scr_dyn_visit_held; +/* Set while a union's builder builds the arm it matched in full + * (scr_dyn_trust_enter). */ +static SCR_TL bool scr_dyn_trusted; +/* Set once a getter or proxy trap has run during this session, or during + * a trusted build outside any session. Their code can return a value the + * match never saw or change one it saw, so no build in the rest of the + * session, or of the outermost trusted build, trusts an earlier match. */ +static SCR_TL bool scr_dyn_session_ran_code; /* Tables up to this many slots stay allocated for the next session. */ #define SCR_DYN_VISIT_KEEP 4096 @@ -9835,27 +11903,33 @@ static void scr_dyn_visit_clear(ScrDynVisitSet *s, ScrDynVisitLog *log) { static void scr_dyn_visit_exit(void) { if (--scr_dyn_visit_depth != 0) return; + scr_dyn_session_ran_code = false; scr_dyn_visit_clear(&scr_dyn_built, &scr_dyn_built_log); scr_dyn_visit_clear(&scr_dyn_viewed, &scr_dyn_viewed_log); } /* Getters and proxy traps run user code, which may start conversions of - * its own and catch their failures. It runs with the session set aside, - * so its conversions use their own tables and leave the session's - * entries alone. */ + * its own and catch their failures. It runs with the session and any + * trusted build set aside, so its conversions use their own tables and + * leave the session's entries and the trusted build's state alone. */ typedef struct { size_t depth; ScrDynVisitSet matching, built, viewed; ScrDynVisitLog built_log, viewed_log; bool track; const ScrDyn *held; + bool trusted; + bool ran_code; } ScrDynVisitState; static void scr_dyn_visit_suspend(ScrDynVisitState *saved) { - *saved = (ScrDynVisitState){scr_dyn_visit_depth, scr_dyn_matching, scr_dyn_built, - scr_dyn_viewed, scr_dyn_built_log, scr_dyn_viewed_log, - scr_dyn_build_track, scr_dyn_visit_held}; + *saved = (ScrDynVisitState){scr_dyn_visit_depth, scr_dyn_matching, scr_dyn_built, + scr_dyn_viewed, scr_dyn_built_log, scr_dyn_viewed_log, + scr_dyn_build_track, scr_dyn_visit_held, scr_dyn_trusted, + scr_dyn_session_ran_code}; scr_dyn_visit_depth = 0; + scr_dyn_trusted = false; + scr_dyn_session_ran_code = false; scr_dyn_matching = scr_dyn_built = scr_dyn_viewed = (ScrDynVisitSet){NULL, 0, 0}; scr_dyn_built_log = scr_dyn_viewed_log = (ScrDynVisitLog){NULL, 0, 0}; scr_dyn_visit_held = NULL; @@ -9875,11 +11949,15 @@ static void scr_dyn_visit_resume(const ScrDynVisitState *saved) { scr_dyn_viewed_log = saved->viewed_log; scr_dyn_build_track = saved->track; scr_dyn_visit_held = saved->held; + scr_dyn_trusted = saved->trusted; + /* The getter or trap this session was set aside for has run. */ + scr_dyn_session_ran_code = true; } -/* Calls a getter for a member read, outside any visit session. */ +/* Calls a getter for a member read, outside any visit session or trusted + * build. */ static ScrDyn *scr_dyn_visit_call_getter(ScrDyn *getter) { - if (scr_dyn_visit_depth == 0) return scr_dyn_call(getter, NULL, 0, "getter"); + if (scr_dyn_visit_depth == 0 && !scr_dyn_trusted) return scr_dyn_call(getter, NULL, 0, "getter"); ScrDynVisitState saved; scr_dyn_visit_suspend(&saved); ScrDyn *result = scr_dyn_call(getter, NULL, 0, "getter"); @@ -9887,9 +11965,10 @@ static ScrDyn *scr_dyn_visit_call_getter(ScrDyn *getter) { return result; } -/* A proxy's get trap for a member read, outside any visit session. */ +/* A proxy's get trap for a member read, outside any visit session or + * trusted build. */ static ScrDyn *scr_dyn_visit_proxy_get(const ScrDyn *d, ScrStr *name) { - if (scr_dyn_visit_depth == 0) return scr_dyn_proxy_get(d, name); + if (scr_dyn_visit_depth == 0 && !scr_dyn_trusted) return scr_dyn_proxy_get(d, name); ScrDynVisitState saved; scr_dyn_visit_suspend(&saved); ScrDyn *result = scr_dyn_proxy_get(d, name); @@ -9897,6 +11976,21 @@ static ScrDyn *scr_dyn_visit_proxy_get(const ScrDyn *d, ScrStr *name) { return result; } +bool scr_dyn_trust_enter(void) { + bool previous = scr_dyn_trusted; + scr_dyn_trusted = true; + return previous; +} + +void scr_dyn_trust_exit(bool previous) { + scr_dyn_trusted = previous; + /* The outermost trusted build outside a session ends: later conversions + * may trust their own matches again. A session clears it on its exit. */ + if (!previous && scr_dyn_visit_depth == 0) scr_dyn_session_ran_code = false; +} + +bool scr_dyn_trusted_build(void) { return scr_dyn_trusted && !scr_dyn_session_ran_code; } + ScrDyn *scr_dyn_obj_read_held(const ScrDyn *d, const char *key, size_t key_len) { ScrDyn *value = scr_dyn_obj_read(d, key, key_len); scr_dyn_visit_held = value; @@ -10013,26 +12107,142 @@ static void scr_json_throw(const ScrJsonP *p, const char *msg) { scr_throw_error_msg(SCR_ERR_SYNTAX, msg, strlen(msg)); } +/* V8's location of a JSON error at byte `pos`: the UTF-16 offset (the + * text is WTF-8; a four-byte sequence is two units), and the 1-based line + * and column, where \r, \n and \r\n end a line. */ +static void scr_json_location(const ScrJsonP *p, size_t pos, size_t *unit, size_t *line, + size_t *column) { + const unsigned char *s = (const unsigned char *)p->s; + size_t units = 0, ln = 1, line_start = 0; + for (size_t i = 0; i < pos; i++) { + unsigned char c = s[i]; + if ((c & 0xC0) == 0x80) continue; + if (c == '\r' && i + 1 < pos && s[i + 1] == '\n') { + i++; + units++; + c = '\n'; + } + units += c >= 0xF0 ? 2 : 1; + if (c == '\r' || c == '\n') { + ln++; + line_start = units; + } + } + *unit = units; + *line = ln; + *column = 1 + units - line_start; +} + /* Errors stay out of line so the scanners that report them keep their - * registers for the common path. */ -static __attribute__((noinline, cold)) void scr_json_throw_pos(const ScrJsonP *p, const char *what, - size_t pos) { + * registers for the common path. V8's positional messages: + * " in JSON at position (line column )". */ +static __attribute__((noinline, cold)) void scr_json_throw_located(const ScrJsonP *p, + const char *what, + const char *where, size_t pos) { if (p->quiet) return; - char buf[128]; - int n = snprintf(buf, sizeof buf, "%s in JSON at position %zu", what, pos); + size_t unit, line, column; + scr_json_location(p, pos, &unit, &line, &column); + char buf[192]; + int n = snprintf(buf, sizeof buf, "%s%s at position %zu (line %zu column %zu)", what, where, unit, + line, column); scr_throw_error_msg(SCR_ERR_SYNTAX, buf, (size_t)n); } -/* V8-flavored bad-token message with a short snippet of the input around - * the offending character. Approximate fidelity (documented). */ -static void scr_json_throw_token(const ScrJsonP *p) { +static __attribute__((noinline, cold)) void scr_json_throw_pos(const ScrJsonP *p, const char *what, + size_t pos) { + scr_json_throw_located(p, what, " in JSON", pos); +} + +/* Appends the text's UTF-16 units [from, to) as WTF-8: a surrogate pair the + * range cuts contributes its half as a lone surrogate, as V8's substring. */ +static size_t scr_json_unit_slice(const ScrJsonP *p, size_t from, size_t to, char *out) { + const unsigned char *s = (const unsigned char *)p->s; + size_t n = 0, unit = 0, i = 0; + while (i < p->len && unit < to) { + unsigned char c = s[i]; + size_t width = c < 0x80 ? 1 : c < 0xE0 ? 2 : c < 0xF0 ? 3 : 4; + if (i + width > p->len) width = p->len - i; + if (width == 4) { + uint32_t cp = ((uint32_t)(c & 0x07) << 18) | ((uint32_t)(s[i + 1] & 0x3F) << 12) | + ((uint32_t)(s[i + 2] & 0x3F) << 6) | (uint32_t)(s[i + 3] & 0x3F); + bool high = unit >= from && unit < to, low = unit + 1 >= from && unit + 1 < to; + if (high && low) { + memcpy(out + n, s + i, 4); + n += 4; + } else if (high) { + n += scr_wtf8_put(out + n, 0xD800 + ((cp - 0x10000) >> 10)); + } else if (low) { + n += scr_wtf8_put(out + n, 0xDC00 + ((cp - 0x10000) & 0x3FF)); + } + unit += 2; + } else { + if (unit >= from) { + memcpy(out + n, s + i, width); + n += width; + } + unit++; + } + i += width; + } + return n; +} + +/* V8's report of an unexpected token at byte `pos`: the end of the input, + * a string or a number, or any other character, named with the source (a + * short one whole, a longer one as up to ten units on each side), except + * that the sources "NaN", "Infinity", "undefined" and "[object Object]" + * name only themselves. */ +static __attribute__((noinline, cold)) void scr_json_throw_unexpected(const ScrJsonP *p, + size_t pos) { if (p->quiet) return; - size_t start = p->pos > 8 ? p->pos - 8 : 0; - size_t take = p->len - start < 16 ? p->len - start : 16; - char buf[192]; - int n = snprintf(buf, sizeof buf, "Unexpected token '%c', %s\"%.*s\"%s is not valid JSON", - p->s[p->pos], start > 0 ? "..." : "", (int)take, p->s + start, - start + take < p->len ? "..." : ""); + if (pos >= p->len) { + scr_json_throw(p, "Unexpected end of JSON input"); + return; + } + char c = p->s[pos]; + if (c == '"') { + scr_json_throw_pos(p, "Unexpected string", pos); + return; + } + if (c == '-' || (c >= '0' && c <= '9')) { + scr_json_throw_pos(p, "Unexpected number", pos); + return; + } + static const char *const special[] = {"NaN", "Infinity", "undefined", "[object Object]"}; + char buf[256]; + int n; + for (size_t k = 0; k < sizeof special / sizeof special[0]; k++) { + if (p->len == strlen(special[k]) && memcmp(p->s, special[k], p->len) == 0) { + n = snprintf(buf, sizeof buf, "\"%s\" is not valid JSON", special[k]); + scr_throw_error_msg(SCR_ERR_SYNTAX, buf, (size_t)n); + return; + } + } + size_t unit, line, column, total; + scr_json_location(p, pos, &unit, &line, &column); + scr_json_location(p, p->len, &total, &line, &column); + char token[8]; + size_t token_len = scr_json_unit_slice(p, unit, unit + 1, token); + enum { CONTEXT = 10 }; + size_t from = 0, to = total; + const char *before = "", *after = ""; + if (total >= 2 * CONTEXT + 1) { + if (unit < CONTEXT) { + to = unit + CONTEXT; + after = "..."; + } else if (unit < total - CONTEXT) { + from = unit - CONTEXT; + to = unit + CONTEXT; + before = after = "..."; + } else { + from = unit - CONTEXT; + before = "..."; + } + } + char source[4 * 2 * CONTEXT + 8]; + size_t source_len = scr_json_unit_slice(p, from, to, source); + n = snprintf(buf, sizeof buf, "Unexpected token '%.*s', %s\"%.*s\"%s is not valid JSON", + (int)token_len, token, before, (int)source_len, source, after); scr_throw_error_msg(SCR_ERR_SYNTAX, buf, (size_t)n); } @@ -10057,12 +12267,13 @@ static void scr_json_put_cp(ScrJsonBuf *b, uint32_t cp) { /* Four hex digits at pos, or -1 (throws). */ static int32_t scr_json_hex4(ScrJsonP *p) { - if (p->len - p->pos < 4) { - scr_json_throw(p, "Unexpected end of JSON input"); - return -1; - } uint32_t v = 0; for (int i = 0; i < 4; i++) { + if (p->pos + (size_t)i >= p->len) { + /* V8 reports the escape at the end of the input. */ + scr_json_throw_pos(p, "Bad Unicode escape", p->len); + return -1; + } char c = p->s[p->pos + (size_t)i]; uint32_t digit; if (c >= '0' && c <= '9') digit = (uint32_t)(c - '0'); @@ -10103,7 +12314,7 @@ static int scr_json_string_span(ScrJsonP *p, const char **span, return -1; } } - scr_json_throw_pos(p, "Unterminated string", p->pos); + scr_json_throw_pos(p, "Unterminated string", p->len); return -1; } @@ -10119,7 +12330,7 @@ static ScrStr *scr_json_string_slow(ScrJsonP *p, size_t prefix) { for (;;) { if (p->pos >= p->len) { scr_jb_dispose(&b); - scr_json_throw_pos(p, "Unterminated string", open); + scr_json_throw_pos(p, "Unterminated string", p->len); return NULL; } unsigned char c = (unsigned char)p->s[p->pos]; @@ -10223,6 +12434,12 @@ static bool scr_json_number_value(ScrJsonP *p, double *out) { } if (s[pos] == '0') { pos++; + /* A leading zero followed by a digit: V8's unexpected number there. */ + if (pos < len && s[pos] >= '0' && s[pos] <= '9') { + p->pos = pos; + scr_json_throw_pos(p, "Unexpected number", pos); + return false; + } } else { while (pos < len && s[pos] >= '0' && s[pos] <= '9') { if (ndig < 15) { @@ -10303,7 +12520,10 @@ static bool scr_json_lit(ScrJsonP *p, const char *word, size_t n) { p->pos += n; return true; } - scr_json_throw_token(p); + /* V8 names the first character that differs, or the end of the input. */ + size_t i = 1; + while (p->pos + i < p->len && i < n && p->s[p->pos + i] == word[i]) i++; + scr_json_throw_unexpected(p, p->pos + i); return false; } @@ -10363,7 +12583,7 @@ static ScrDyn *scr_json_array(ScrJsonP *p) { scr_json_ws(p); if (p->pos >= p->len) { scr_dyn_release(arr); - scr_json_throw(p, "Unexpected end of JSON input"); + scr_json_throw_pos(p, "Expected ',' or ']' after array element", p->pos); return NULL; } if (p->s[p->pos] == ',') { @@ -10477,6 +12697,7 @@ static void scr_dyn_obj_append_shared(ScrDyn *obj, char *key, size_t key_len, Sc e->getter = NULL; e->setter = NULL; e->accessor = false; + e->fenced = false; e->writable = true; e->enumerable = true; e->configurable = true; @@ -10493,18 +12714,20 @@ static ScrDyn *scr_json_object(ScrJsonP *p) { } uint32_t shape = 0; /* the empty layout */ bool shaped = true; /* obj holds exactly the keys on shape's path */ + bool first = true; for (;;) { scr_json_ws(p); - if (p->pos >= p->len) { + if (p->pos >= p->len || p->s[p->pos] != '"') { + /* V8: the first member may also close the object; after a comma + * only a property name may follow. */ scr_dyn_release(obj); - scr_json_throw(p, "Unexpected end of JSON input"); - return NULL; - } - if (p->s[p->pos] != '"') { - scr_dyn_release(obj); - scr_json_throw_pos(p, "Expected property name or '}'", p->pos); + scr_json_throw_pos(p, + first ? "Expected property name or '}'" + : "Expected double-quoted property name", + p->pos); return NULL; } + first = false; /* Keep the key borrowed until insertion determines whether it already * exists or can reuse a recycled slot. Escaped keys own their decoded * string until the value has been parsed and inserted. */ @@ -10542,8 +12765,7 @@ static ScrDyn *scr_json_object(ScrJsonP *p) { if (p->pos >= p->len || p->s[p->pos] != ':') { scr_str_release(decoded_key); scr_dyn_release(obj); - if (p->pos >= p->len) scr_json_throw(p, "Unexpected end of JSON input"); - else scr_json_throw_pos(p, "Expected ':' after property name", p->pos); + scr_json_throw_pos(p, "Expected ':' after property name", p->pos); return NULL; } p->pos++; /* ':' */ @@ -10570,7 +12792,7 @@ static ScrDyn *scr_json_object(ScrJsonP *p) { scr_json_ws(p); if (p->pos >= p->len) { scr_dyn_release(obj); - scr_json_throw(p, "Unexpected end of JSON input"); + scr_json_throw_pos(p, "Expected ',' or '}' after property value", p->pos); return NULL; } if (p->s[p->pos] == ',') { @@ -10628,7 +12850,7 @@ static ScrDyn *scr_json_value(ScrJsonP *p) { return scr_dyn_new_null(); } if (c == '-' || (c >= '0' && c <= '9')) return scr_json_number(p); - scr_json_throw_token(p); + scr_json_throw_unexpected(p, p->pos); return NULL; } @@ -10648,10 +12870,7 @@ ScrDyn *scr_json_parse(ScrStr *text) { scr_json_ws(&p); if (p.pos < p.len) { scr_dyn_release(d); - char buf[96]; - int n = snprintf(buf, sizeof buf, - "Unexpected non-whitespace character after JSON at position %zu", p.pos); - scr_throw_error_msg(SCR_ERR_SYNTAX, buf, (size_t)n); + scr_json_throw_located(&p, "Unexpected non-whitespace character after JSON", "", p.pos); return NULL; } return d; @@ -10687,7 +12906,8 @@ static bool scr_jschema_read(ScrJsonP *p, const ScrJsonSchema *s, ScrJsonSlot *o static void scr_jschema_drop(const ScrJsonSchema *s, ScrJsonSlot *v) { if (s->kind == SCR_JSCHEMA_STR) scr_str_release(v->ptr); - else if (s->kind == SCR_JSCHEMA_REC || s->kind == SCR_JSCHEMA_ARR) s->release(v->ptr); + else if (s->kind == SCR_JSCHEMA_REC || s->kind == SCR_JSCHEMA_ARR || s->kind == SCR_JSCHEMA_UNION) + s->release(v->ptr); } static bool scr_jschema_string_skip(ScrJsonP *p) { @@ -10801,14 +13021,79 @@ static void scr_jschema_inline_store(char *slot, size_t tag, uint64_t bits) { memcpy(slot, &pair, sizeof pair); } -/* A union member value written into its field slot. Each JSON value kind - * selects at most one arm (the compiler admits no two arms one kind could - * both match), so the checked route's first full match is that arm, and - * any other value declines. */ -static __attribute__((noinline)) bool scr_jschema_union(ScrJsonP *p, const ScrJsonUnion *u, - char *slot) { - scr_json_ws(p); - if (p->pos >= p->len) return false; +/* The string at p->pos (a quote), as a span into the text or, when it + * holds escapes, decoded into *decoded (released by the caller). */ +static bool scr_jschema_string_at(ScrJsonP *p, const char **span, size_t *span_len, + ScrStr **decoded) { + *decoded = NULL; + int r = scr_json_string_span(p, span, span_len); + if (r > 0) return true; + if (r < 0) return false; + *decoded = scr_json_string_slow(p, *span_len); + if (!*decoded) return false; + *span = (*decoded)->data; + *span_len = (*decoded)->len; + return true; +} + +/* The case of discriminated union `d` that the object at p->pos (its + * opening brace) selects: the one whose literal the object's discriminant + * member holds. A shallow scan of the members up to that one finds it and + * restores p->pos. NULL when the member is missing or not a string, when + * no case has its value, or when the text before it is malformed: the + * checked route then decides. A repeated discriminant member selects by + * its first value; the arm's record then declines the repeat. */ +static __attribute__((noinline)) const ScrJsonCase *scr_jschema_case(ScrJsonP *p, + const ScrJsonDiscriminant *d) { + const size_t start = p->pos, depth = p->depth; + const ScrJsonCase *found = NULL; + if (++p->depth > SCR_JSON_MAX_DEPTH) goto done; + p->pos++; /* '{' */ + for (;;) { + scr_json_ws(p); + if (p->pos >= p->len || p->s[p->pos] != '"') goto done; + const char *key; + size_t key_len; + ScrStr *decoded; + if (!scr_jschema_string_at(p, &key, &key_len, &decoded)) goto done; + bool discriminant = key_len == d->field_len && scr_key_equal(key, d->field, key_len); + scr_str_release(decoded); + scr_json_ws(p); + if (p->pos >= p->len || p->s[p->pos] != ':') goto done; + p->pos++; + if (discriminant) { + scr_json_ws(p); + if (p->pos >= p->len || p->s[p->pos] != '"') goto done; + const char *value; + size_t value_len; + if (!scr_jschema_string_at(p, &value, &value_len, &decoded)) goto done; + for (size_t i = 0; i < d->ncases; i++) { + const ScrJsonCase *c = &d->cases[i]; + if (c->value_len == value_len && memcmp(c->value, value, value_len) == 0) { + found = c; + break; + } + } + scr_str_release(decoded); + goto done; + } + if (!scr_jschema_skip(p)) goto done; + scr_json_ws(p); + if (p->pos >= p->len || p->s[p->pos] != ',') goto done; + p->pos++; + } +done: + p->pos = start; + p->depth = depth; + return found; +} + +/* A union value's arm tag and payload at p->pos (whitespace skipped): the + * arm its JSON value kind selects or, for an object of a discriminated + * union, its case's record. A null value reads no payload (*arm_out is + * SCR_JARM_NULL). False when the value declines. */ +static bool scr_jschema_union_payload(ScrJsonP *p, const ScrJsonUnion *u, size_t *tag, + size_t *arm_out, ScrJsonSlot *v) { char c = p->s[p->pos]; size_t arm; if (c == 'n') arm = SCR_JARM_NULL; @@ -10818,16 +13103,36 @@ static __attribute__((noinline)) bool scr_jschema_union(ScrJsonP *p, const ScrJs else if (c == 't' || c == 'f') arm = SCR_JARM_BOOL; else if (c == '-' || (c >= '0' && c <= '9')) arm = SCR_JARM_NUM; else return false; - size_t tag = u->tags[arm]; - if (tag == SCR_JSCHEMA_NO_ARM) return false; + *arm_out = arm; + if (arm == SCR_JARM_OBJ && u->disc) { + const ScrJsonCase *selected = scr_jschema_case(p, u->disc); + if (!selected) return false; + *tag = selected->tag; + return scr_jschema_read(p, selected->record, v); + } + *tag = u->tags[arm]; + if (*tag == SCR_JSCHEMA_NO_ARM) return false; + if (arm == SCR_JARM_NULL) return scr_json_lit(p, "null", 4); + return scr_jschema_read(p, u->arms[arm], v); +} + +/* A union member value written into its field slot. Each JSON value kind + * selects at most one arm, and an object of a discriminated union selects + * its case's arm (the compiler admits no two arms one value could both + * match), so the checked route's first full match is that arm, and any + * other value declines. */ +static __attribute__((noinline)) bool scr_jschema_union(ScrJsonP *p, const ScrJsonUnion *u, + char *slot) { + scr_json_ws(p); + if (p->pos >= p->len) return false; + size_t tag, arm; + ScrJsonSlot v; + if (!scr_jschema_union_payload(p, u, &tag, &arm, &v)) return false; if (arm == SCR_JARM_NULL) { - if (!scr_json_lit(p, "null", 4)) return false; if (u->repr == SCR_JUNION_INLINE) scr_jschema_inline_store(slot, tag, 0); else memcpy(slot, &u->null_value, sizeof u->null_value); /* immortal */ return true; } - ScrJsonSlot v; - if (!scr_jschema_read(p, u->arms[arm], &v)) return false; uint64_t bits = 0; if (arm == SCR_JARM_NUM) memcpy(&bits, &v.f, sizeof bits); else if (arm == SCR_JARM_BOOL) bits = v.b ? 1 : 0; @@ -10843,6 +13148,19 @@ static __attribute__((noinline)) bool scr_jschema_union(ScrJsonP *p, const ScrJs return true; } +/* A discriminated union value outside a record field: an array element or + * the whole document. A pointer union's value is the case's record itself; + * any other union's is a box. */ +static bool scr_jschema_union_value(ScrJsonP *p, const ScrJsonUnion *u, ScrJsonSlot *out) { + size_t tag, arm; + ScrJsonSlot v; + if (!scr_jschema_union_payload(p, u, &tag, &arm, &v)) return false; + if (arm == SCR_JARM_NULL) out->ptr = u->null_value; /* immortal */ + else if (u->repr == SCR_JUNION_NULLABLE) out->ptr = v.ptr; + else out->ptr = u->box((uint32_t)tag, (uint64_t)(uintptr_t)v.ptr); /* the record moves in */ + return true; +} + /* A declared member value written into its (zeroed) slot of `rec`. */ static inline __attribute__((always_inline)) bool scr_jschema_field(ScrJsonP *p, const ScrJsonSchemaField *f, @@ -11188,6 +13506,8 @@ static bool scr_jschema_read(ScrJsonP *p, const ScrJsonSchema *s, ScrJsonSlot *o p->depth--; return ok; } + case SCR_JSCHEMA_UNION: + return scr_jschema_union_value(p, s->uni, out); } return false; } @@ -11255,6 +13575,11 @@ static ScrDyn *scr_json_member(const ScrDyn *holder, const ScrStr *key) { scr_dyn_release(view); return member; } + /* A view's named properties, getters included, read through the view. */ + if (holder->kind == SCR_DYN_BYTES && (holder->v.bytes->is_data_view || scr_bytes_has_named(holder, key))) { + ScrDyn *named = scr_bytes_named_get(holder, key->data, key->len); + return named || scr_exc_pending() ? named : scr_dyn_retain(scr_dyn_undefined()); + } if (holder->kind == SCR_DYN_ARR || holder->kind == SCR_DYN_BYTES) { size_t index = 0; for (size_t i = 0; i < key->len; i++) index = index * 10 + (size_t)(key->data[i] - '0'); @@ -11271,6 +13596,10 @@ static void scr_json_delete_member(ScrDyn *object, const ScrStr *key) { for (size_t i = 0; i < object->v.obj.len; i++) { ScrDynEntry *entry = &object->v.obj.entries[i]; if (entry->key_len != key->len || memcmp(entry->key, key->data, key->len) != 0) continue; + if (entry->fenced) { + scr_dyn_namespace_member_fence(); + return; + } ScrDynEntry removed = *entry; memmove(entry, entry + 1, (object->v.obj.len - i - 1) * sizeof *entry); object->v.obj.len--; @@ -11288,6 +13617,10 @@ static void scr_json_delete_member(ScrDyn *object, const ScrStr *key) { } } +void scr_dyn_obj_delete_field(ScrDyn *obj, const ScrStr *key) { + if (obj->kind == SCR_DYN_OBJ) scr_json_delete_member(obj, key); +} + /* Ordinary native objects carry removable own data properties. Dense * arrays, typed references and handles keep their explicit boundary; * deleting from a materialized snapshot would lose the mutation. */ @@ -11368,10 +13701,7 @@ void scr_dyn_key_delete(ScrDyn *recv, const ScrStr *key, bool strict) { if (recv->kind == SCR_DYN_OBJ) { ScrDynEntry *entry = scr_dyn_entry(recv, key); if (entry && !entry->configurable) { - if (strict) { - static const char message[] = "Cannot delete non-configurable property"; - scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); - } + if (strict) scr_dyn_throw_property(recv, key->data, key->len, true); return; } scr_json_delete_member(recv, key); @@ -11383,10 +13713,7 @@ void scr_dyn_key_delete(ScrDyn *recv, const ScrStr *key, bool strict) { for (size_t i = 0; i < key->len; i++) index = index * 10 + (size_t)(key->data[i] - '0'); if (!scr_dyn_arr_has_index(recv, index)) return; if (recv->v.arr.sealed) { - if (strict) { - static const char message[] = "Cannot delete non-configurable property"; - scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); - } + if (strict) scr_array_frozen_error(5, recv->v.arr.len, (double)index, 0, 0); return; } if (!recv->v.arr.presence) { @@ -11404,6 +13731,23 @@ void scr_dyn_key_delete(ScrDyn *recv, const ScrStr *key, bool strict) { scr_dyn_key_delete(recv->v.arr.properties, key, strict); return; } + if (recv->kind == SCR_DYN_BYTES) { + /* A typed array's elements cannot be deleted; other numeric keys name + * no property. Named properties follow their configurability. */ + double index; + if (!recv->v.bytes->is_data_view && scr_bytes_numeric_key(key, &index)) { + if (strict && !signbit(index) && trunc(index) == index && index < (double)recv->v.bytes->len) + scr_dyn_throw_property(recv, key->data, key->len, true); + return; + } + ScrDynEntry *entry = recv->v.bytes->properties ? scr_dyn_entry(recv->v.bytes->properties, key) : NULL; + if (entry && !entry->configurable) { + if (strict) scr_dyn_throw_property(recv, key->data, key->len, true); + return; + } + if (entry) scr_json_delete_member(recv->v.bytes->properties, key); + return; + } if (recv->kind == SCR_DYN_UNDEF || recv->kind == SCR_DYN_NULL) { const char *message = "Cannot convert undefined or null to object"; scr_throw_error_msg(SCR_ERR_TYPE, message, strlen(message)); @@ -11547,7 +13891,9 @@ static ScrDyn *scr_json_to_json_at(ScrDyn *value, const ScrStr *key, size_t inde if (scr_exc_pending()) { scr_dyn_release(view); scr_dyn_release(value); return NULL; } if (inspected->kind == SCR_DYN_HANDLE && (inspected->v.handle.tag == SCR_DYNH_DATE || inspected->v.handle.tag == SCR_DYNH_URL || inspected->v.handle.tag == SCR_DYNH_NET_SOCKET_ADDRESS || - inspected->v.handle.tag == SCR_DYNH_NET_BLOCK_LIST)) { + inspected->v.handle.tag == SCR_DYNH_NET_BLOCK_LIST || + inspected->v.handle.tag == SCR_DYNH_MIME_TYPE || + inspected->v.handle.tag == SCR_DYNH_MIME_PARAMS)) { const ScrDynHandleOps *ops = scr_dyn_handle_ops_of(inspected); ScrDyn *converted = ops->invoke(inspected->v.handle.ptr, value, "toJSON", NULL, 0, "toJSON"); scr_dyn_release(value); @@ -12150,6 +14496,13 @@ void scr_jb_put_dyn(ScrJsonBuf *b, const ScrDyn *d) { else scr_json_replaced_write(b, d, scr_dyn_undefined(), NULL, &scr_json_compact_gap, b->seen_len); } +void scr_jb_put_dyn_element(ScrJsonBuf *b, const ScrDyn *d, double index) { + ScrDyn *value = scr_json_to_json_at(scr_dyn_retain((ScrDyn *)d), NULL, (size_t)index); + if (!value) return; + scr_jb_put_dyn(b, value); + scr_dyn_release(value); +} + /* Untyped RC adapters (box/promise/exception-cell currency). */ void *scr_dyn_retain_v(void *d) { return scr_dyn_retain((ScrDyn *)d); } void scr_dyn_release_v(void *d) { scr_dyn_release((ScrDyn *)d); } @@ -12271,6 +14624,13 @@ ScrDyn *scr_dyn_fn_get(const ScrDyn *d, const char *key, size_t key_len) { ScrClosure *owner = scr_closure_identity(d->v.fn.clo); if (d->v.fn.class_obj && d->v.fn.class_obj->static_data && !owner->props) owner->props = scr_box_retain(d->v.fn.class_obj->static_data); + if (d->v.fn.thunk == scr_function_constructor_call && key_len == 9 && !memcmp(key, "prototype", 9)) { + /* A function's `constructor.prototype`: Function.prototype has no value + * here, and undefined would be a wrong answer. */ + static const char function_prototype[] = "Function.prototype through a constructor value is not supported yet"; + scr_throw_error_msg_code(SCR_ERR_ERROR, function_prototype, sizeof function_prototype - 1, "SC2020"); + return NULL; + } if (!d->v.fn.class_obj && key_len == 9 && !memcmp(key, "prototype", 9) && !owner->props) { if (owner->function_kind & 1) { static const char message[] = "Native generator function prototypes have no lowering"; @@ -12866,7 +15226,9 @@ bool scr_dyn_reflect_define(ScrDyn *receiver, ScrDyn *raw_key, ScrDyn *value) { ScrDynEntry *entry = key->kind == SCR_DYN_SYMBOL ? (symbols ? scr_dyn_symbol_entry(symbols, key, true) : NULL) : receiver->kind == SCR_DYN_OBJ ? scr_dyn_entry(receiver, key->v.str) : receiver->kind == SCR_DYN_ARR && receiver->v.arr.properties ? scr_dyn_entry(receiver->v.arr.properties, key->v.str) : NULL; - if (!(entry && (entry->accessor || !entry->writable)) && !(receiver->non_extensible && !scr_dyn_has_own_computed(receiver, key))) { + if (entry && entry->fenced) { + scr_dyn_namespace_member_fence(); + } else if (!(entry && (entry->accessor || !entry->writable)) && !(receiver->non_extensible && !scr_dyn_has_own_computed(receiver, key))) { if (entry) { ScrDyn *old = entry->value; entry->value = scr_dyn_retain(value); @@ -13047,6 +15409,20 @@ ScrDyn *scr_dyn_get_own_property_names(const ScrDyn *value) { } if (value->kind == SCR_DYN_OBJ) { ScrDyn *keys = scr_dyn_obj_own_keys(value); + if (value == scr_builtin_object_prototype) { + /* Object.prototype's builtin members, in Node's order. */ + static const char *const names[] = { + "constructor", "__defineGetter__", "__defineSetter__", "hasOwnProperty", + "__lookupGetter__", "__lookupSetter__", "isPrototypeOf", "propertyIsEnumerable", + "toString", "valueOf", "__proto__", "toLocaleString" + }; + ScrDyn *all = scr_dyn_new_arr(); + for (size_t i = 0; i < sizeof names / sizeof names[0]; i++) + scr_dyn_arr_push(all, scr_dyn_objwalk_key(names[i], strlen(names[i]))); + for (size_t i = 0; i < keys->v.arr.len; i++) scr_dyn_arr_push(all, scr_dyn_retain(keys->v.arr.items[i])); + scr_dyn_release(keys); + return all; + } if (!scr_dyn_obj_get(value, "%error", 6)) return keys; /* The checked-dynamic tree's error marker is not a property. */ ScrDyn *names = scr_dyn_new_arr(); @@ -13062,7 +15438,7 @@ ScrDyn *scr_dyn_get_own_property_names(const ScrDyn *value) { if (value->kind == SCR_DYN_ARR || value->kind == SCR_DYN_STR || value->kind == SCR_DYN_BYTES) { ScrDyn *keys = scr_dyn_new_arr(); size_t length = value->kind == SCR_DYN_ARR ? value->v.arr.len : - value->kind == SCR_DYN_BYTES ? value->v.bytes->len : + value->kind == SCR_DYN_BYTES ? (value->v.bytes->is_data_view ? 0 : value->v.bytes->len) : (size_t)scr_str_utf16_len(value->v.str); for (size_t i = 0; i < length; i++) { if (value->kind == SCR_DYN_ARR && !scr_dyn_arr_has_index(value, i)) continue; @@ -13072,10 +15448,20 @@ ScrDyn *scr_dyn_get_own_property_names(const ScrDyn *value) { } if (value->kind != SCR_DYN_BYTES) scr_dyn_arr_push(keys, scr_dyn_objwalk_key("length", 6)); - if (value->kind == SCR_DYN_ARR && value->v.arr.properties) { - ScrDyn *properties = scr_dyn_obj_own_keys(value->v.arr.properties); - for (size_t i = 0; i < properties->v.arr.len; i++) - scr_dyn_arr_push(keys, scr_dyn_retain(properties->v.arr.items[i])); + bool array_prototype = value == scr_builtin_array_prototype; + if (array_prototype) scr_dyn_arr_push(keys, scr_dyn_objwalk_key("constructor", 11)); + ScrDyn *named = value->kind == SCR_DYN_ARR ? value->v.arr.properties + : value->kind == SCR_DYN_BYTES ? value->v.bytes->properties : NULL; + if (named) { + ScrDyn *properties = scr_dyn_obj_own_keys(named); + for (size_t i = 0; i < properties->v.arr.len; i++) { + ScrDyn *name = properties->v.arr.items[i]; + /* A constructor stored on Array.prototype keeps the position above. */ + if (array_prototype && name->kind == SCR_DYN_STR && name->v.str->len == 11 && + memcmp(name->v.str->data, "constructor", 11) == 0) + continue; + scr_dyn_arr_push(keys, scr_dyn_retain(name)); + } scr_dyn_release(properties); } return keys; @@ -13338,7 +15724,7 @@ static ScrDyn *scr_dyn_objwalk(const ScrDyn *v, ScrObjWalk mode) { return out; } if (v->kind == SCR_DYN_ARR || v->kind == SCR_DYN_BYTES) { - size_t n = v->kind == SCR_DYN_ARR ? v->v.arr.len : v->v.bytes->len; + size_t n = v->kind == SCR_DYN_ARR ? v->v.arr.len : v->v.bytes->is_data_view ? 0 : v->v.bytes->len; for (size_t i = 0; i < n; i++) { if (v->kind == SCR_DYN_ARR && !scr_dyn_arr_has_index(v, i)) continue; char key[24]; @@ -13359,10 +15745,11 @@ static ScrDyn *scr_dyn_objwalk(const ScrDyn *v, ScrObjWalk mode) { scr_dyn_arr_push(out, pair); } } - if (v->kind == SCR_DYN_ARR && v->v.arr.properties) { - ScrDyn *names = scr_dyn_obj_own_keys(v->v.arr.properties); + ScrDyn *named = v->kind == SCR_DYN_ARR ? v->v.arr.properties : v->v.bytes->properties; + if (named) { + ScrDyn *names = scr_dyn_obj_own_keys(named); for (size_t i = 0; i < names->v.arr.len; i++) { - if (!scr_dyn_objwalk_entry(out, v->v.arr.properties, mode, names->v.arr.items[i]->v.str, v)) { + if (!scr_dyn_objwalk_entry(out, named, mode, names->v.arr.items[i]->v.str, v)) { scr_dyn_release(names); scr_dyn_release(out); return NULL; @@ -13466,6 +15853,7 @@ ScrDyn *scr_dyn_for_in_keys(const ScrDyn *v) { return keys; } case SCR_DYN_ARR: + case SCR_DYN_BYTES: return scr_dyn_obj_keys(v); case SCR_DYN_STR: { ScrDyn *keys = scr_dyn_new_arr(); @@ -13719,7 +16107,7 @@ static void scr_dyn_assign_strings_from(ScrDyn *target, const ScrDyn *src, bool scr_dyn_proxy_unsupported("Object.assign"); return; } - if (target->kind != SCR_DYN_OBJ && target->kind != SCR_DYN_FUNC) return; + if (target->kind != SCR_DYN_OBJ && target->kind != SCR_DYN_FUNC && target->kind != SCR_DYN_BYTES) return; if (src->kind == SCR_DYN_TYPED_REF) { ScrDyn *materialized = scr_dyn_typed_ref_materialize(src); if (materialized) scr_dyn_assign_strings_from(target, materialized, define); @@ -13782,13 +16170,15 @@ static void scr_dyn_assign_from(ScrDyn *target, const ScrDyn *src, bool define) return; } ScrDyn *view = src->kind == SCR_DYN_FUNC ? scr_dyn_fn_properties(src) : scr_dyn_retain((ScrDyn *)src); + /* A view's symbol properties live on its named-property table. */ + const ScrDyn *holder = src->kind == SCR_DYN_BYTES ? src->v.bytes->properties : view; ScrDyn *symbols = scr_dyn_new_arr(); - if (view->symbol_keys) for (size_t i = 0; i < view->symbol_keys->v.arr.len; i++) - scr_dyn_arr_push(symbols, scr_dyn_retain(view->symbol_keys->v.arr.items[i])); + if (holder && holder->symbol_keys) for (size_t i = 0; i < holder->symbol_keys->v.arr.len; i++) + scr_dyn_arr_push(symbols, scr_dyn_retain(holder->symbol_keys->v.arr.items[i])); scr_dyn_assign_strings_from(target, view, define); for (size_t i = 0; i < symbols->v.arr.len && !scr_exc_pending(); i++) { ScrDyn *key = symbols->v.arr.items[i]; - ScrDynEntry *entry = scr_dyn_symbol_entry(view, key, true); + ScrDynEntry *entry = scr_dyn_symbol_entry(holder, key, true); if (!entry || !entry->enumerable) continue; ScrDyn *value = scr_dyn_symbol_key_get(src, key, false); if (!value) break; @@ -14158,10 +16548,16 @@ bool scr_dyn_has_own(const ScrDyn *v, const ScrStr *key) { if (v->kind == SCR_DYN_OBJ) { + /* Object.prototype's builtin members are its own properties (zod's + * isPlainObject asks whether the prototype has its own isPrototypeOf). */ + if (v == scr_builtin_object_prototype && scr_dyn_object_proto_name(key)) return true; return scr_dyn_obj_get(v, key->data, key->len) != NULL; } if (v->kind == SCR_DYN_ARR) { if (key->len == 6 && memcmp(key->data, "length", 6) == 0) return true; + /* Array.prototype's own constructor (its methods are table entries). */ + if (v == scr_builtin_array_prototype && key->len == 11 && memcmp(key->data, "constructor", 11) == 0) + return true; if (v->v.arr.properties && scr_dyn_obj_get(v->v.arr.properties, key->data, key->len)) return true; if (!scr_dyn_canonical_own_index(key, v->v.arr.len)) return false; size_t index = 0; @@ -14173,7 +16569,7 @@ bool scr_dyn_has_own(const ScrDyn *v, const ScrStr *key) { return scr_dyn_canonical_own_index(key, (size_t)scr_str_utf16_len(v->v.str)); } if (v->kind == SCR_DYN_BYTES) { - return scr_dyn_canonical_own_index(key, v->v.bytes->len); + return (!v->v.bytes->is_data_view && scr_dyn_canonical_own_index(key, v->v.bytes->len)) || scr_bytes_has_named(v, key); } if (v->kind == SCR_DYN_FUNC) { ScrDyn *desc = scr_dyn_own_descriptor(v, key); @@ -14210,7 +16606,8 @@ bool scr_dyn_property_is_enumerable(const ScrDyn *value, const ScrStr *key) { return (scr_dyn_canonical_own_index(key, value->v.arr.len) && scr_dyn_has_own(value, key)) || (value->v.arr.properties && scr_dyn_obj_enumerable(value->v.arr.properties, key->data, key->len)); if (value->kind == SCR_DYN_BYTES) - return scr_dyn_canonical_own_index(key, value->v.bytes->len); + return (!value->v.bytes->is_data_view && scr_dyn_canonical_own_index(key, value->v.bytes->len)) || + (value->v.bytes->properties && scr_dyn_obj_enumerable(value->v.bytes->properties, key->data, key->len)); return false; } ScrDyn *scr_dyn_obj_values(const ScrDyn *v) { return scr_dyn_objwalk(v, SCR_OBJWALK_VALUES); } @@ -14431,9 +16828,10 @@ ScrError *scr_domex_clone(ScrError *e, const ScrDyn *options) { * They live HERE (not scr_string.c) because the argument is a dyn value: * WebIDL ToString runs over the dyn kind (Node's atob(null) decodes the * string "null"), and the string unit must stay linkable without the - * dyn. atob is forgiving-base64 exactly — ASCII whitespace stripped, a - * %4==0 input sheds up to two trailing '=', %4==1 refuses, leftover - * bits discard — decoding to the latin1 code points as a UTF-8 string; + * dyn. atob checks its input as Node's lib/buffer.js does (an invalid + * character or a misplaced '=' is "Invalid character", a length the + * forgiving-base64 rules refuse "not correctly encoded"), then decodes + * with leftover bits discarded to the latin1 code points as a UTF-8 string; * btoa refuses any code point over U+00FF. Malformed input throws the * catchable DOMException InvalidCharacterError with Node's exact * message. */ @@ -14449,61 +16847,70 @@ static int scr_b64_val(unsigned char c) { ScrStr *scr_atob(const ScrDyn *data) { ScrStr *s = scr_dyn_string_coerce(data); - /* Strip ASCII whitespace (the forgiving step). */ - char *buf = malloc(s->len ? s->len : 1); - if (!buf) scr_json_oom(); - size_t n = 0; + /* Node's checks (lib/buffer.js): only ASCII whitespace and the base64 + * alphabet with '=' are allowed, '=' only at the end and at most twice; + * the count of the other characters decides the rest. One pass checks + * the input and keeps the 6-bit value of each alphabet character. */ + unsigned char *values = malloc(s->len ? s->len : 1); + if (!values) scr_json_oom(); + size_t n = 0, equals = 0; + const char *message = NULL; for (size_t i = 0; i < s->len; i++) { unsigned char c = (unsigned char)s->data[i]; if (c == 0x09 || c == 0x0a || c == 0x0c || c == 0x0d || c == 0x20) continue; - buf[n++] = (char)c; + if (c == '=') { + if (++equals > 2) { + message = "Invalid character"; + break; + } + continue; + } + int v = scr_b64_val(c); + if (v < 0 || equals) { + message = "Invalid character"; + break; + } + values[n++] = (unsigned char)v; + } + if (!message) { + size_t remainder = (n + equals) % 4; + if (!remainder) remainder = n % 4; + else if (equals) message = "Invalid character"; + if (!message && remainder == 1) message = "The string to be decoded is not correctly encoded."; } scr_str_release(s); - /* A %4==0 input sheds one or two trailing '='. */ - if (n % 4 == 0 && n > 0) { - if (buf[n - 1] == '=') n--; - if (n > 0 && buf[n - 1] == '=') n--; + if (message) { + free(values); + scr_throw_domex("InvalidCharacterError", message); + return NULL; } - if (n % 4 == 1) goto invalid; /* Decode 6-bit groups; latin1 code points expand to UTF-8 (bytes over - * 0x7F become two-byte sequences). */ - { - size_t outBytes = (n / 4) * 3 + (n % 4 == 2 ? 1 : n % 4 == 3 ? 2 : 0); - char *out = malloc(outBytes * 2 ? outBytes * 2 : 1); - if (!out) scr_json_oom(); - size_t w = 0; - unsigned acc = 0; - int bits = 0; - for (size_t i = 0; i < n; i++) { - int v = scr_b64_val((unsigned char)buf[i]); - if (v < 0) { - free(out); - goto invalid; - } - acc = (acc << 6) | (unsigned)v; - bits += 6; - if (bits >= 8) { - bits -= 8; - unsigned char b = (unsigned char)((acc >> bits) & 0xff); - if (b < 0x80) { - out[w++] = (char)b; - } else { - out[w++] = (char)(0xc0 | (b >> 6)); - out[w++] = (char)(0x80 | (b & 0x3f)); - } + * 0x7F become two-byte sequences). Leftover bits discard + * (forgiving-base64's final step). */ + size_t outBytes = (n / 4) * 3 + (n % 4 == 2 ? 1 : n % 4 == 3 ? 2 : 0); + char *out = malloc(outBytes * 2 ? outBytes * 2 : 1); + if (!out) scr_json_oom(); + size_t w = 0; + unsigned acc = 0; + int bits = 0; + for (size_t i = 0; i < n; i++) { + acc = (acc << 6) | values[i]; + bits += 6; + if (bits >= 8) { + bits -= 8; + unsigned char b = (unsigned char)((acc >> bits) & 0xff); + if (b < 0x80) { + out[w++] = (char)b; + } else { + out[w++] = (char)(0xc0 | (b >> 6)); + out[w++] = (char)(0x80 | (b & 0x3f)); } } - /* Leftover bits discard (forgiving-base64's final step). */ - free(buf); - ScrStr *result = scr_str_new(out, w); - free(out); - return result; } -invalid: - free(buf); - scr_throw_domex("InvalidCharacterError", - "The string to be decoded is not correctly encoded."); - return NULL; + free(values); + ScrStr *result = scr_str_new(out, w); + free(out); + return result; } ScrStr *scr_btoa(const ScrDyn *data) { @@ -15019,26 +17426,179 @@ typedef struct ScrWeakEntry { unsigned kind; ScrDyn *value; ScrWeakMap *owner; - struct ScrWeakEntry *next, *all_next; + /* The owner's entries, and every collection's entries for the same key + * pointer (whatever their kind): both doubly linked, so an entry leaves + * either list in constant time. */ + struct ScrWeakEntry *next, *prev, *key_next, *key_prev; } ScrWeakEntry; struct ScrWeakMap { size_t rc; ScrWeakEntry *entries; bool is_set; }; /* Weak observers borrow key identities. Values are traced strong edges; - * value-to-key cycles require explicit deletion or container disposal. */ -#define SCR_WEAK_BUCKETS 1024 -static SCR_TL ScrWeakEntry *scr_weak_entries[SCR_WEAK_BUCKETS]; -static size_t scr_weak_bucket(void *key) { - uintptr_t bits = (uintptr_t)key; - return ((bits >> 4) ^ (bits >> 14)) & (SCR_WEAK_BUCKETS - 1); + * value-to-key cycles require explicit deletion or container disposal. + * + * Two open-addressing indexes per thread find entries in expected constant + * time: scr_weak_index by (collection, key, kind), for get/set/has/delete, + * and scr_weak_keys by key pointer, the head of that key's entries, for a + * disposal, which ends the key's entries in every collection. Both use + * linear probing with backward-shift deletion, grow at half load, and are + * freed when their last entry goes. */ +static SCR_TL ScrWeakEntry **scr_weak_index; +static SCR_TL size_t scr_weak_index_mask, scr_weak_index_used; +typedef struct { void *key; ScrWeakEntry *head; } ScrWeakKeySlot; +static SCR_TL ScrWeakKeySlot *scr_weak_keys; +static SCR_TL size_t scr_weak_keys_mask, scr_weak_keys_used; + +/* Mixed in 64 bits on every target: uintptr_t is 32 bits on wasm32, where + * these shifts would exceed its width. */ +static inline size_t scr_weak_mix(uint64_t bits) { + bits ^= bits >> 33; + bits *= UINT64_C(0xff51afd7ed558ccd); + bits ^= bits >> 29; + return (size_t)bits; +} +static inline size_t scr_weak_entry_hash(const ScrWeakMap *owner, const void *key, unsigned kind) { + return scr_weak_mix((uint64_t)(uintptr_t)key ^ ((uint64_t)(uintptr_t)owner * UINT64_C(0x9E3779B97F4A7C15)) ^ + ((uint64_t)kind << 56)); +} +static inline size_t scr_weak_key_hash(const void *key) { return scr_weak_mix((uint64_t)(uintptr_t)key); } + +static void scr_weak_index_put(ScrWeakEntry *entry) { + size_t j = scr_weak_entry_hash(entry->owner, entry->key, entry->kind) & scr_weak_index_mask; + while (scr_weak_index[j]) j = (j + 1) & scr_weak_index_mask; + scr_weak_index[j] = entry; +} +static void scr_weak_index_reserve(void) { + if (scr_weak_index && (scr_weak_index_used + 1) * 2 <= scr_weak_index_mask + 1) return; + ScrWeakEntry **old = scr_weak_index; + size_t n = old ? scr_weak_index_mask + 1 : 0, cap = n ? n * 2 : 64; + scr_weak_index = calloc(cap, sizeof *scr_weak_index); + if (!scr_weak_index) scr_json_oom(); + scr_weak_index_mask = cap - 1; + for (size_t i = 0; i < n; i++) + if (old[i]) scr_weak_index_put(old[i]); + free(old); +} +static ScrWeakEntry *scr_weak_find(ScrWeakMap *map, void *ptr, unsigned kind) { + if (!scr_weak_index) return NULL; + for (size_t j = scr_weak_entry_hash(map, ptr, kind) & scr_weak_index_mask;; j = (j + 1) & scr_weak_index_mask) { + ScrWeakEntry *entry = scr_weak_index[j]; + if (!entry) return NULL; + if (entry->owner == map && entry->key == ptr && entry->kind == kind) return entry; + } +} +static void scr_weak_index_delete(ScrWeakEntry *entry) { + size_t mask = scr_weak_index_mask; + size_t i = scr_weak_entry_hash(entry->owner, entry->key, entry->kind) & mask; + while (scr_weak_index[i] != entry) i = (i + 1) & mask; + if (--scr_weak_index_used == 0) { + /* Empty: give the table back, so a thread that exits leaves none. */ + free(scr_weak_index); + scr_weak_index = NULL; + return; + } + for (size_t k = i;;) { + scr_weak_index[i] = NULL; + for (;;) { + k = (k + 1) & mask; + ScrWeakEntry *e = scr_weak_index[k]; + if (!e) return; + size_t home = scr_weak_entry_hash(e->owner, e->key, e->kind) & mask; + /* e stays unless its home lies cyclically outside (i, k]. */ + if (i <= k ? (i < home && home <= k) : (i < home || home <= k)) continue; + scr_weak_index[i] = e; + i = k; + break; + } + } +} + +static ScrWeakKeySlot *scr_weak_key_slot(const void *key) { + if (!scr_weak_keys) return NULL; + for (size_t j = scr_weak_key_hash(key) & scr_weak_keys_mask;; j = (j + 1) & scr_weak_keys_mask) { + if (scr_weak_keys[j].key == key) return &scr_weak_keys[j]; + if (!scr_weak_keys[j].key) return NULL; + } +} +static ScrWeakKeySlot *scr_weak_key_slot_put(void *key) { + if (!scr_weak_keys || (scr_weak_keys_used + 1) * 2 > scr_weak_keys_mask + 1) { + ScrWeakKeySlot *old = scr_weak_keys; + size_t n = old ? scr_weak_keys_mask + 1 : 0, cap = n ? n * 2 : 64; + scr_weak_keys = calloc(cap, sizeof *scr_weak_keys); + if (!scr_weak_keys) scr_json_oom(); + scr_weak_keys_mask = cap - 1; + for (size_t i = 0; i < n; i++) { + if (!old[i].key) continue; + size_t j = scr_weak_key_hash(old[i].key) & scr_weak_keys_mask; + while (scr_weak_keys[j].key) j = (j + 1) & scr_weak_keys_mask; + scr_weak_keys[j] = old[i]; + } + free(old); + } + size_t j = scr_weak_key_hash(key) & scr_weak_keys_mask; + while (scr_weak_keys[j].key && scr_weak_keys[j].key != key) j = (j + 1) & scr_weak_keys_mask; + if (!scr_weak_keys[j].key) { + scr_weak_keys[j].key = key; + scr_weak_keys[j].head = NULL; + scr_weak_keys_used++; + size_t bit = scr_weak_key_filter_bit(key); + scr_weak_key_filter[bit >> 6] |= UINT64_C(1) << (bit & 63); + } + return &scr_weak_keys[j]; +} +static void scr_weak_key_slot_delete(ScrWeakKeySlot *slot) { + size_t mask = scr_weak_keys_mask; + size_t i = (size_t)(slot - scr_weak_keys); + if (--scr_weak_keys_used == 0) { + free(scr_weak_keys); + scr_weak_keys = NULL; + memset(scr_weak_key_filter, 0, sizeof scr_weak_key_filter); + return; + } + for (size_t k = i;;) { + scr_weak_keys[i].key = NULL; + scr_weak_keys[i].head = NULL; + for (;;) { + k = (k + 1) & mask; + if (!scr_weak_keys[k].key) return; + size_t home = scr_weak_key_hash(scr_weak_keys[k].key) & mask; + if (i <= k ? (i < home && home <= k) : (i < home || home <= k)) continue; + scr_weak_keys[i] = scr_weak_keys[k]; + i = k; + break; + } + } +} + +/* Indexes a new entry (its key, kind and owner set) under its key. */ +static void scr_weak_link(ScrWeakEntry *entry) { + scr_weak_index_reserve(); + scr_weak_index_put(entry); + scr_weak_index_used++; + ScrWeakKeySlot *slot = scr_weak_key_slot_put(entry->key); + entry->key_prev = NULL; + entry->key_next = slot->head; + if (slot->head) slot->head->key_prev = entry; + slot->head = entry; +} + +/* Takes an entry out of both indexes (it stays in its owner's list). */ +static void scr_weak_unlink(ScrWeakEntry *entry) { + scr_weak_index_delete(entry); + if (entry->key_next) entry->key_next->key_prev = entry->key_prev; + if (entry->key_prev) entry->key_prev->key_next = entry->key_next; + else { + ScrWeakKeySlot *slot = scr_weak_key_slot(entry->key); + if (entry->key_next) slot->head = entry->key_next; + else scr_weak_key_slot_delete(slot); + } } static void scr_weak_remove_impl(ScrWeakEntry *entry, bool collected) { - ScrWeakEntry **slot = &entry->owner->entries; - while (*slot != entry) slot = &(*slot)->next; - *slot = entry->next; - slot = &scr_weak_entries[scr_weak_bucket(entry->key)]; - while (*slot != entry) slot = &(*slot)->all_next; - *slot = entry->all_next; + ScrWeakMap *owner = entry->owner; + if (entry->next) entry->next->prev = entry->prev; + if (entry->prev) entry->prev->next = entry->next; + else owner->entries = entry->next; + scr_weak_unlink(entry); ScrDyn *value = entry->value; free(entry); /* Unlink before release: releasing a value can dispose another key. */ @@ -15062,10 +17622,9 @@ static void scr_weak_map_gcfree(void *ptr) { static void scr_weak_disposed(void *key) { for (;;) { - ScrWeakEntry *entry = scr_weak_entries[scr_weak_bucket(key)]; - while (entry && entry->key != key) entry = entry->all_next; - if (!entry) return; - scr_weak_remove(entry); + ScrWeakKeySlot *slot = scr_weak_key_slot(key); + if (!slot) return; + scr_weak_remove(slot->head); } } @@ -15109,6 +17668,7 @@ static int scr_weak_key(const ScrDyn *key, void **ptr, unsigned *kind) { key->v.handle.tag != SCR_DYNH_WEAK_SET && key->v.handle.tag != SCR_DYNH_FINALIZATION_REGISTRY && key->v.handle.tag != SCR_DYNH_STDIO && key->v.handle.tag != SCR_DYNH_FETCH_REQUEST && key->v.handle.tag != SCR_DYNH_FETCH_RESPONSE && + key->v.handle.tag != SCR_DYNH_WEB_STREAM && key->v.handle.tag != SCR_DYNH_MAP && key->v.handle.tag != SCR_DYNH_SET && key->v.handle.tag != SCR_DYNH_NET_SOCKET && key->v.handle.tag != SCR_DYNH_NET_SERVER) return -1; *ptr = key->v.handle.ptr; *kind = 4 + key->v.handle.tag; return 1; @@ -15122,12 +17682,6 @@ static int scr_weak_key(const ScrDyn *key, void **ptr, unsigned *kind) { } } -static ScrWeakEntry *scr_weak_find(ScrWeakMap *map, void *ptr, unsigned kind) { - for (ScrWeakEntry *entry = map->entries; entry; entry = entry->next) - if (entry->key == ptr && entry->kind == kind) return entry; - return NULL; -} - static bool scr_weak_set(ScrWeakMap *map, ScrDyn *key, ScrDyn *value) { void *ptr = NULL; unsigned kind = 0; @@ -15156,10 +17710,9 @@ static bool scr_weak_set(ScrWeakMap *map, ScrDyn *key, ScrDyn *value) { entry->value = scr_dyn_retain(value); entry->owner = map; entry->next = map->entries; + if (map->entries) map->entries->prev = entry; map->entries = entry; - size_t bucket = scr_weak_bucket(ptr); - entry->all_next = scr_weak_entries[bucket]; - scr_weak_entries[bucket] = entry; + scr_weak_link(entry); return true; } @@ -15876,8 +18429,31 @@ ScrStr *scr_dyn_to_string_method(const ScrDyn *d, const ScrStr *enc, const ScrSt return scr_dyn_to_string_method_args(d, enc, what, NULL, 0); } +/* Freezing a record or array capsule freezes the native value, whose + * emitted writes then refuse to change it, and its checked view. A Map or + * Set handle only stops being extensible: its entries stay writable. Answers + * NULL for every other value. */ +static ScrDyn *scr_dyn_freeze_native(ScrDyn *value) { + void *record = scr_dyn_typed_record(value); + ScrArr *array = record ? NULL : scr_dyn_typed_array(value); + ScrMap *collection = scr_dyn_native_collection(value); + if (collection) { + scr_map_prevent_extensions(collection); + return scr_dyn_retain(value); + } + if (!record && !array) return NULL; + if (record) scr_record_freeze(record); + else scr_arr_freeze(array); + if (value->v.typed_ref.materialized) scr_dyn_record_view_freeze(value->v.typed_ref.materialized); + return scr_dyn_retain(value); +} + ScrDyn *scr_dyn_freeze(ScrDyn *value) { if (scr_dyn_class_reflection_fence(value) || scr_dyn_error_prototype_fence(value)) return NULL; + ScrDyn *native = scr_dyn_freeze_native(value); + if (native) return native; + if (scr_dyn_class_integrity(value, scr_dyn_freeze_data)) + return scr_exc_pending() ? NULL : scr_dyn_retain(value); if (scr_dyn_class_integrity_fence(value, "Object.freeze")) return NULL; ScrDyn *table = scr_dyn_fn_integrity_table(value); if (!table) return scr_dyn_freeze_data(value); @@ -15889,7 +18465,14 @@ ScrDyn *scr_dyn_freeze(ScrDyn *value) { ScrDyn *scr_dyn_freeze_data(ScrDyn *value) { if (scr_dyn_class_reflection_fence(value) || scr_dyn_error_prototype_fence(value)) return NULL; + ScrDyn *native = scr_dyn_freeze_native(value); + if (native) return native; if (scr_dyn_class_integrity_fence(value, "Object.freeze")) return NULL; + if (value->kind == SCR_DYN_BYTES) { + scr_dyn_release(scr_dyn_freeze_data(scr_bytes_properties(value))); + if (scr_bytes_integrity_fence(value, "freeze")) return NULL; + return scr_dyn_retain(value); + } if (value->symbol_properties) scr_dyn_release(scr_dyn_freeze_data(value->symbol_properties)); if (value->kind == SCR_DYN_ARR) { value->non_extensible = true; @@ -15919,6 +18502,8 @@ bool scr_dyn_is_frozen(const ScrDyn *value) { if (scr_dyn_class_reflection_fence(value)) return false; bool answer; if (scr_dyn_typed_ref_view_answer((ScrDyn *)value, scr_dyn_is_frozen, &answer)) return answer; + ScrMap *collection = scr_dyn_native_collection(value); + if (collection) return !scr_map_is_extensible(collection); if (value->symbol_properties && !scr_dyn_is_frozen(value->symbol_properties)) return false; if (scr_dyn_plain_fn(value)) { ScrDyn *table = scr_dyn_fn_existing_table(value); @@ -15929,6 +18514,9 @@ bool scr_dyn_is_frozen(const ScrDyn *value) { if (value->kind == SCR_DYN_ARR) return scr_dyn_is_sealed(value) && (value->v.arr.len == 0 || value->v.arr.frozen) && (!value->v.arr.properties || scr_dyn_is_frozen(value->v.arr.properties)); + if (value->kind == SCR_DYN_BYTES) + return (value->v.bytes->is_data_view || value->v.bytes->len == 0) && value->v.bytes->properties && + scr_dyn_is_frozen(value->v.bytes->properties); if (value->kind == SCR_DYN_OBJ && !value->copied_from_native) { if (!value->non_extensible) return false; for (size_t i = 0; i < value->v.obj.len; i++) { @@ -15979,21 +18567,6 @@ ScrDyn *scr_caught_to_dyn(const ScrCaught *c) { } } -/* ── class instance attributes ─────────────────────────────────────────── - * A hierarchy passed to Object.freeze, Object.seal, Object.preventExtensions - * or Object.defineProperty keeps an integrity state per instance and an - * attribute word per redefined field (CLASS_* and FIELD_* bits in the - * compiler's ir.ts). The checked view of an instance carries them, so - * reflection over the view answers as Node does. */ -enum { - SCR_CLASS_NOT_EXTENSIBLE = 1, - SCR_CLASS_SEALED = 2, - SCR_CLASS_FROZEN = 4, - SCR_FIELD_NOT_WRITABLE = 1, - SCR_FIELD_NOT_ENUMERABLE = 2, - SCR_FIELD_NOT_CONFIGURABLE = 4, -}; - void scr_dyn_class_field_attributes(ScrDyn *view, ScrStr *name, double bits) { unsigned b = (unsigned)bits; if (b == 0 || view->kind != SCR_DYN_OBJ) return; @@ -16004,17 +18577,27 @@ void scr_dyn_class_field_attributes(ScrDyn *view, ScrStr *name, double bits) { if (b & SCR_FIELD_NOT_CONFIGURABLE) entry->configurable = false; } -void scr_dyn_class_view_attributes(ScrDyn *view, double bits) { - unsigned b = (unsigned)bits; - if (b == 0 || view->kind != SCR_DYN_OBJ) return; - view->non_extensible = true; - for (size_t i = 0; i < view->v.obj.len; i++) { - ScrDynEntry *entry = &view->v.obj.entries[i]; +/* The integrity state `b` on the entries of `table`: a view, or the table + * of its symbol-keyed properties. */ +static void scr_dyn_class_table_attributes(ScrDyn *table, unsigned b) { + table->non_extensible = true; + for (size_t i = 0; i < table->v.obj.len; i++) { + ScrDynEntry *entry = &table->v.obj.entries[i]; if (b & (SCR_CLASS_SEALED | SCR_CLASS_FROZEN)) entry->configurable = false; if ((b & SCR_CLASS_FROZEN) && !entry->accessor) entry->writable = false; } } +void scr_dyn_class_view_attributes(ScrDyn *view, double bits) { + unsigned b = (unsigned)bits; + if (b == 0 || view->kind != SCR_DYN_OBJ) return; + scr_dyn_class_table_attributes(view, b); + /* Symbol-keyed fields, and symbol-keyed properties added to the + * instance, take the same state. */ + if (view->symbol_properties && view->symbol_properties->kind == SCR_DYN_OBJ) + scr_dyn_class_table_attributes(view->symbol_properties, b); +} + /* Object.defineProperty on a declared class field. The descriptor applies to * a one-property object holding the field's current value and attributes, * so validation and errors match ordinary objects. Answers that object (+1), @@ -16371,16 +18954,14 @@ static bool scr_dyn_decl_rekey(ScrDyn *node, void *ptr) { bool moved = false; if (!scr_weak_observing) return false; for (;;) { - ScrWeakEntry **slot = &scr_weak_entries[scr_weak_bucket(node)]; - while (*slot && !((*slot)->key == (void *)node && (*slot)->kind == 0)) slot = &(*slot)->all_next; - ScrWeakEntry *entry = *slot; + ScrWeakKeySlot *slot = scr_weak_key_slot(node); + ScrWeakEntry *entry = slot ? slot->head : NULL; + while (entry && entry->kind != 0) entry = entry->key_next; if (!entry) return moved; - *slot = entry->all_next; + scr_weak_unlink(entry); entry->key = ptr; entry->kind = 32; - ScrWeakEntry **head = &scr_weak_entries[scr_weak_bucket(ptr)]; - entry->all_next = *head; - *head = entry; + scr_weak_link(entry); moved = true; } } diff --git a/packages/runtime/src/scr_json.h b/packages/runtime/src/scr_json.h index e3077703e..c1aa5dc10 100644 --- a/packages/runtime/src/scr_json.h +++ b/packages/runtime/src/scr_json.h @@ -13,11 +13,14 @@ ScrDyn *scr_json_parse(ScrStr *text); /* Compiler-emitted description of a statically known JSON.parse target * (`JSON.parse(text) as T`). Constant data in the program image; the LLVM * backend mirrors this layout ({size, size, ptr, ptr, ptr, ptr, ptr, ptr, size}, - * {ptr, size, size, ptr} and {size, [7 x size], [7 x ptr], ptr, ptr, ptr}). - * Records list their declared fields (at most 64), each with its byte - * offset in the record struct; bool fields are one byte, f64 fields - * doubles, inline union fields a {uint32 tag, uint64 bits} pair, every - * other kind a +1 pointer. Unions appear only as record field types. A + * {ptr, size, size, ptr}, {size, [7 x size], [7 x ptr], ptr, ptr, ptr, ptr}, + * and for discriminated unions {ptr, size, size, ptr} with {ptr, size, size, + * ptr} cases). Records list their declared fields (at most 64), each with + * its byte offset in the record struct; bool fields are one byte, f64 + * fields doubles, inline union fields a {uint32 tag, uint64 bits} pair, + * every other kind a +1 pointer. Unions appear as record field types; + * discriminated unions also as array elements and whole documents, whose + * values are the union's box (or its pointer, for a pointer union). A * record with an index signature names its index value type in `elem` and * its overflow map's offset in `ovf_offset`. */ enum { @@ -57,6 +60,22 @@ typedef struct { size_t offset; const ScrJsonSchema *type; } ScrJsonSchemaField; +/* One string literal of a discriminated union's discriminant member and + * the record arm (canonical tag and schema) that holds it. */ +typedef struct { + const char *value; + size_t value_len; + size_t tag; + const ScrJsonSchema *record; +} ScrJsonCase; +/* A discriminated union's object arms by discriminant literal: every + * literal names one arm. */ +typedef struct { + const char *field; + size_t field_len; + size_t ncases; + const ScrJsonCase *cases; +} ScrJsonDiscriminant; typedef struct { size_t repr; size_t tags[SCR_JARM_COUNT]; /* canonical arm tag, or SCR_JSCHEMA_NO_ARM */ @@ -66,6 +85,9 @@ typedef struct { /* boxed: a +1 box of the value arm `tag`; bits hold its double, its * bool, or its +1 payload pointer (moved in). */ void *(*box)(uint32_t tag, uint64_t bits); + /* discriminated unions: the object arms; NULL for other unions, whose + * object arm is arms[SCR_JARM_OBJ] */ + const ScrJsonDiscriminant *disc; } ScrJsonUnion; struct ScrJsonSchema { size_t kind; @@ -119,6 +141,11 @@ _Static_assert(offsetof(ScrJsonBuf, data) == 0 && offsetof(ScrJsonBuf, len) == s "LLVM backend expects ScrJsonBuf to start with { data, len, cap }"); void scr_jb_init(ScrJsonBuf *b); +/* String conversion of a class instance whose ToString is + * Object.prototype.toString and whose class has a Symbol.toStringTag + * getter: appends "[object Tag]" and answers true, also with the getter's + * exception pending; false when the ordinary conversion applies. */ +bool scr_dyn_typed_ref_put_tag(ScrJsonBuf *b, const ScrDyn *d); /* Release a buffer's text without finishing it (error paths). */ void scr_jb_abandon(ScrJsonBuf *b); /* Append a borrowed byte span verbatim, including embedded NUL bytes. */ @@ -153,6 +180,9 @@ void scr_dyn_from_enter(const void *v); void scr_dyn_from_leave(void); void scr_jb_putc(ScrJsonBuf *b, char c); void scr_jb_puts(ScrJsonBuf *b, const char *s); +/* Array.prototype.join's conversion of one element, appended to `b`: + * nothing for undefined and null, ToString of anything else. */ +void scr_jb_join_dyn(ScrJsonBuf *b, const ScrDyn *d); /* JS JSON.stringify number rules: NaN/±Infinity → null, -0 → 0, else * shortest-roundtrip via scr_f64_to_str. */ void scr_jb_put_f64(ScrJsonBuf *b, double v); @@ -162,12 +192,20 @@ void scr_jb_put_number(ScrJsonBuf *b, double v); * as \u00XX, lone surrogates as \udXXX, everything else (UTF-8 included) * verbatim — exactly the JS JSON.stringify escape set. */ void scr_jb_put_json_str(ScrJsonBuf *b, const ScrStr *s); +/* JSON.stringify of a Date object: toJSON()'s ISO text, null if invalid + * (scr_lib.c). */ +typedef struct ScrDate ScrDate; +void scr_jb_put_date(ScrJsonBuf *b, const ScrDate *date); /* JSON-serialize a dyn value into the buffer — the sc_jw_* walker for the * dyn leaves the compiler cannot type: undefined prints null (an array * slot's rule); containers follow JSON.stringify (toJSON, accessors, * circular structures). May leave an exception pending. */ void scr_jb_put_dyn(ScrJsonBuf *b, const ScrDyn *d); +/* scr_jb_put_dyn for array element `index` (a foreign element of a typed + * array): its toJSON runs first, with the index as its key. May leave an + * exception pending. */ +void scr_jb_put_dyn_element(ScrJsonBuf *b, const ScrDyn *d, double index); /* The entries of record `rec`'s `unknown` index signature (its overflow * map) after the record's earlier members: separators, keys and values in * JS own-key order with JSON.stringify's member rules (toJSON called with diff --git a/packages/runtime/src/scr_json.test.c b/packages/runtime/src/scr_json.test.c index 736eca540..29ba4bfe3 100644 --- a/packages/runtime/src/scr_json.test.c +++ b/packages/runtime/src/scr_json.test.c @@ -391,6 +391,30 @@ static bool buffered_after_read(ScrDyn *d) { return buffered(d); } +/* A collector pass can buffer a number or string box that it reached + * through an owner that traces it: scr_xg_release, settling an edge a + * restricted pass skipped, calls scr_cyc_on_release on the target. The + * box's last release must take it out of its candidate buffer before its + * block returns to the free list, which hands it to the next box. */ +static void buffered_leaf_release(void) { + for (uint16_t gen = SCR_CYC_NURSERY; gen < SCR_CYC_NGENS; gen++) { + ScrStr *text = scr_str_new("buffered", 8); + ScrDyn *leaves[2] = {scr_dyn_new_str(text), scr_dyn_new_num(0.5)}; + scr_str_release(text); + for (int i = 0; i < 2; i++) { + scr_cyc_set_gen(scr_cyc_hdr(leaves[i]), gen); + scr_cyc_on_release(leaves[i]); + scr_dyn_release(leaves[i]); + } + ScrDyn *fresh[2] = {scr_dyn_new_num(1.5), scr_dyn_new_num(2.5)}; + assert(!buffered(fresh[0]) && !buffered(fresh[1])); + scr_collect_cycles(); + assert(fresh[0]->v.num == 1.5 && fresh[1]->v.num == 2.5); + scr_dyn_release(fresh[0]); + scr_dyn_release(fresh[1]); + } +} + static ScrDyn *parse_text(const char *text) { ScrStr *input = scr_str_new(text, strlen(text)); ScrDyn *parsed = scr_json_parse(input); @@ -802,14 +826,28 @@ static void json_schema_union_members(void) { assert(test_opts_live == 0); } +/* Freezing a record sets the frozen bit beside its count; an immortal + * record keeps its word, which does not read as frozen. */ +static void record_frozen_bit(void) { + size_t record[2] = {3, 0}; + assert(!scr_rc_frozen(record)); + scr_record_freeze(record); + assert(scr_rc_frozen(record) && SCR_RC_REFS(record) == 3); + size_t immortal[2] = {SIZE_MAX, 0}; + scr_record_freeze(immortal); + assert(immortal[0] == SIZE_MAX && !scr_rc_frozen(immortal)); +} + int main(void) { json_string_boundaries(); + record_frozen_bit(); json_indent_ownership(); scr_init(); checked_storage(); shared_property_keys(); checked_leaf_cycles(); checked_number_storage(); + buffered_leaf_release(); json_intact_regions(); json_schema_parse(); json_schema_special_names(); diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index c17f44f9d..928400da3 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -1078,6 +1078,10 @@ bool scr_fs_path_ok(const ScrStr *path, const char *name) { return false; } +void scr_fs_check_path(ScrStr *path, ScrStr *name) { + (void)scr_fs_path_ok(path, name->data); +} + void scr_process_load_env_file(const ScrDyn *value) { ScrStr *path; if (value->kind == SCR_DYN_UNDEF || value->kind == SCR_DYN_NULL) path = scr_str_new("./.env", 6); @@ -4232,27 +4236,35 @@ double scr_fs_read_sync(double fd, ScrBytes *buf, double offset, double length, scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)mlen, "ERR_OUT_OF_RANGE"); return 0; } - /* Node returns after the offset and position validation when the - * requested length normalizes to zero: buffer-window bounds and fd are - * not consulted. Preserve the existing size_t coercion's [0, 1) case. */ - if (length >= 0 && length < 1) return 0; - if (offset > (double)bytelen) { - numbuf[scr_f64_to_str(offset, numbuf)] = 0; + /* `length |= 0`, then Node's order: a zero length reads nothing after + * the offset and position checks; otherwise the buffer must not be + * empty and the window must fit it (validateOffsetLengthRead). */ + if (!isfinite(length)) { + length = 0; + } else { + double wrapped = fmod(trunc(length), 4294967296.0); + if (wrapped < 0) wrapped += 4294967296.0; + length = wrapped >= 2147483648.0 ? wrapped - 4294967296.0 : wrapped; + } + if (length == 0) return 0; + if (bytelen == 0) { + static const char empty[] = + "The argument 'buffer' is empty and cannot be written. Received "; + scr_throw_error_msg_code(SCR_ERR_TYPE, empty, sizeof empty - 1, "ERR_INVALID_ARG_VALUE"); + return 0; + } + if (length < 0 || offset + length > (double)bytelen) { + char recv[48], range[48]; + scr_num_received(length, recv); + if (length < 0) snprintf(range, sizeof range, ">= 0"); + else snprintf(range, sizeof range, "<= %.0f", (double)bytelen - offset); mlen = snprintf(msg, sizeof msg, - "The value of \"offset\" is out of range. It must be >= 0 && <= 9007199254740991. Received %s", - numbuf); + "The value of \"length\" is out of range. It must be %s. Received %s", range, + recv); scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)mlen, "ERR_OUT_OF_RANGE"); return 0; } size_t off = (size_t)offset; - if (length < 0 || (double)length > (double)(bytelen - off)) { - numbuf[scr_f64_to_str(length, numbuf)] = 0; - mlen = snprintf(msg, sizeof msg, - "The value of \"length\" is out of range. It must be <= %zu. Received %s", - bytelen - off, numbuf); - scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)mlen, "ERR_OUT_OF_RANGE"); - return 0; - } size_t want = (size_t)length; uint8_t *bounce = buf->shared ? malloc(want ? want : 1) : NULL; if (buf->shared && !bounce) scr_trap("scriptc: out of memory\n"); @@ -6332,6 +6344,56 @@ ScrArr *scr_fs_readdir(ScrStr *path) { return arr; } +/* fs.readdirSync(path, { recursive: true }): Node's readdirSyncRecursive. + * Directories are listed in queue order: the base, then each directory in + * the order its entry was listed. Each entry's path is path.join of its + * directory and name; the result holds it relative to the base + * (path.relative). An entry stat(2) reports as a directory, a symlink to + * one included, joins the queue. */ +ScrArr *scr_fs_readdir_recursive(ScrStr *base) { + if (!scr_fs_path_ok(base, "path")) return NULL; + ScrArr *out = scr_arr_new(SCR_ELEM_STR, 0); + size_t len = 1, cap = 8; + ScrStr **queue = malloc(cap * sizeof *queue); + if (!queue) scr_trap("scriptc: out of memory\n"); + queue[0] = scr_str_retain(base); + bool failed = false; + for (size_t at = 0; at < len && !failed; at++) { + ScrScandir *scan = scr_fs_scandir(queue[at]); + if (!scan) { + failed = true; + break; + } + for (size_t i = 0; i < scr_fs_scandir_count(scan); i++) { + ScrArr *parts = scr_arr_new(SCR_ELEM_STR, 2); + scr_arr_push_ref(parts, scr_str_retain(queue[at])); + scr_arr_push_ref(parts, scr_fs_scandir_name(scan, i)); + ScrStr *full = scr_path_join(parts); + scr_arr_release(parts); + scr_arr_push_ref(out, scr_path_relative(base, full)); + struct stat st; + if (stat(full->data, &st) == 0 && S_ISDIR(st.st_mode)) { + if (len == cap) { + cap *= 2; + queue = realloc(queue, cap * sizeof *queue); + if (!queue) scr_trap("scriptc: out of memory\n"); + } + queue[len++] = full; + } else { + scr_str_release(full); + } + } + scr_fs_scandir_free(scan); + } + for (size_t i = 0; i < len; i++) scr_str_release(queue[i]); + free(queue); + if (failed) { + scr_arr_release(out); + return NULL; + } + return out; +} + /* ── the withFileTypes scandir snapshot ────────────────────────────── * One readdir pass capturing name + entry kind for the emitter's Dirent * assembly (scr_runtime.h has the contract). Kinds are libuv's UV_DIRENT @@ -6816,11 +6878,17 @@ double scr_math_round(double x) { return (r == 0.0 && x < 0.0) ? -0.0 : r; } -/* IEEE pow(±1, ±Infinity) and pow(1, NaN) return 1; ECMAScript requires - * NaN. The other special cases, including signed zero and odd integer - * powers, follow libm. */ +/* V8's math::pow: the platform pow, as Node's Math.pow and ** call it, + * after the cases V8 answers itself. IEEE pow(±1, ±Infinity) and + * pow(1, NaN) return 1; ECMAScript requires NaN. A NaN base becomes the + * quiet NaN, and the exponents 2 and 0.5 answer base * base and + * sqrt(base + 0) (+Infinity for an infinite base), as V8's optimizing + * compilers do; pow can differ from those in the last bit. */ double scr_math_pow(double base, double exponent) { if (isnan(exponent) || (isinf(exponent) && fabs(base) == 1.0)) return NAN; + if (isnan(base)) base = NAN; + if (exponent == 2.0) return base * base; + if (exponent == 0.5) return isinf(base) ? INFINITY : sqrt(base + 0.0); /* 2 ** n for an integer n in the double range is an exact power of two, * the value pow returns; its bits are the exponent alone. */ if (base == 2.0 && exponent >= -1074.0 && exponent <= 1023.0) { @@ -8133,6 +8201,11 @@ static bool scr_digest_alg(const char *name, size_t len, ScrDigestAlg *out) { return false; } +bool scr_digest_known(const char *name, size_t len) { + ScrDigestAlg alg; + return scr_digest_alg(name, len, &alg); +} + /* createHash() and crypto.hash() also accept "dss1" in any case. */ static bool scr_hash_alg(const char *name, size_t len, ScrDigestAlg *out) { if (scr_digest_alg(name, len, out)) return true; @@ -10522,10 +10595,29 @@ static bool scr_x509_der(const uint8_t *in, size_t n, const uint8_t **der, size_ *der = NULL; *der_len = 0; *decoded = NULL; + /* One whole DER SEQUENCE is the certificate itself: these members read + * the DER the constructor already decoded, whose extensions can carry + * PEM armor as text. */ + if (n >= 2 && in[0] == 0x30) { + size_t len = in[1], head = 2; + if (len & 0x80) { + size_t k = len & 0x7f; + len = 0; + head = 2 + k; + for (size_t i = 0; i < k && k <= 4 && head <= n; i++) len = (len << 8) | in[2 + i]; + if (k == 0 || k > 4) head = n + 1; + } + if (head <= n && len == n - head) { + *der = in; + *der_len = n; + return true; + } + } /* PEM: base64-decode the armor's body. */ const uint8_t *b = NULL; + /* OpenSSL's PEM reader looks for the armor at the start of a line. */ for (size_t i = 0; i + sizeof begin - 1 <= n; i++) { - if (memcmp(in + i, begin, sizeof begin - 1) == 0) { + if ((i == 0 || in[i - 1] == '\n') && memcmp(in + i, begin, sizeof begin - 1) == 0) { b = in + i + sizeof begin - 1; break; } @@ -11552,8 +11644,8 @@ double scr_date_get_timezone_offset(double ms) { * from local fields reads those fields back. */ /* The host zone's offset (local - UTC) in ms at UTC instant `ms` (an - * integral time value within about ±8.65e15). */ -static bool scr_date_offset_at(double ms, double *out) { + * integral time value within about ±8.65e15), from the host's localtime. */ +static bool scr_date_offset_at_uncached(double ms, double *out) { long long cms = (long long)ms; double secd = (double)(cms / 1000 - (cms % 1000 < 0)); struct tm tmv; @@ -11575,6 +11667,60 @@ static bool scr_date_offset_at(double ms, double *out) { return true; } +/* Offsets at UTC midnights, for scr_date_offset_at. A day whose two + * midnights have the same offset has that offset throughout, given at most + * one transition per day (scr_date_local_to_utc already takes transitions + * to be at least a day apart). Each local setter asks for three to five + * offsets around one time value, and consecutive ones land on the same or + * neighbouring days, so most answers come from here instead of a + * localtime(3) call: glibc takes its zone lock in each and, for dates past + * the zone file's last transition, parses the zone's rule string again. A + * change of process.env.TZ starts a new generation. */ +#define SCR_DATE_MIDNIGHTS 64 +typedef struct { + long long day; + double offset; + bool valid; +} ScrDateMidnight; +typedef struct { + bool valid; + unsigned generation; + ScrDateMidnight entries[SCR_DATE_MIDNIGHTS]; +} ScrDateMidnightCache; +static SCR_TL ScrDateMidnightCache scr_date_midnight_cache; + +static bool scr_date_midnight_offset(ScrDateMidnightCache *c, long long day, double *out) { + ScrDateMidnight *e = &c->entries[(unsigned long long)day % SCR_DATE_MIDNIGHTS]; + if (e->valid && e->day == day) { + *out = e->offset; + return true; + } + if (!scr_date_offset_at_uncached((double)day * 86400000.0, out)) return false; + e->valid = true; + e->day = day; + e->offset = *out; + return true; +} + +static bool scr_date_offset_at(double ms, double *out) { + ScrDateMidnightCache *c = &scr_date_midnight_cache; + unsigned generation = scr_date_tz_generation; + if (!c->valid || c->generation != generation) { + memset(c, 0, sizeof *c); + c->valid = true; + c->generation = generation; + } + long long cms = (long long)ms; + long long day = cms / 86400000 - (cms % 86400000 < 0); + double start, end; + if (scr_date_midnight_offset(c, day, &start) && scr_date_midnight_offset(c, day + 1, &end) && + start == end) { + *out = start; + return true; + } + return scr_date_offset_at_uncached(ms, out); +} + static volatile const double scr_date_day_ms = 86400000.0; static double scr_date_local_to_utc(double t) { @@ -11656,6 +11802,56 @@ double scr_date_new_local(double y, double mo, double d, return scr_date_from_local(scr_date_v8_make_day(y, mo, d), scr_date_v8_make_time(h, mi, s, ms)); } +/* Date.prototype's setters over a time value, as V8's builtins-date.cc + * computes them: the fields of `t` (local fields for the local setters) + * with the first `argc` of them replaced, in the order the setter names + * them, by `a`..`d`, through MakeDay/MakeTime/MakeDate, then UTC(t) for + * the local setters and TimeClip. An invalid `t` stays invalid, except + * that setFullYear and setUTCFullYear start from +0: January 1 of the + * epoch year at midnight (local midnight for setFullYear). Returns the new + * time value; the caller stores it into the Date. Never throws. */ +double scr_date_set_fields(double t, double setter_code, double argc_count, + double a, double b, double c, double d) { + int32_t setter = (int32_t)setter_code, argc = (int32_t)argc_count; + bool utc = (setter & SCR_DATE_SET_UTC) != 0; + int32_t field = setter & ~SCR_DATE_SET_UTC; + ScrDateParts p; + if (isnan(t)) { + if (field != SCR_DATE_SET_FULL_YEAR) return NAN; + p = (ScrDateParts){1970, 0, 1, 4, 0, 0, 0, 0, 0}; + } else if (utc) { + if (!scr_date_utc_parts(t, &p)) return NAN; + } else if (!scr_date_local_parts(t, &p)) { + return NAN; + } + double y = (double)p.year, mo = p.month, dt = p.date; + double h = p.hours, mi = p.minutes, s = p.seconds, ms = p.milliseconds; + double args[4] = {a, b, c, d}; + double *slots[7] = {&y, &mo, &dt, &h, &mi, &s, &ms}; + /* The first field each setter names, in slots' order. */ + int first; + switch (field) { + case SCR_DATE_SET_MILLISECONDS: first = 6; break; + case SCR_DATE_SET_SECONDS: first = 5; break; + case SCR_DATE_SET_MINUTES: first = 4; break; + case SCR_DATE_SET_HOURS: first = 3; break; + case SCR_DATE_SET_DATE: first = 2; break; + case SCR_DATE_SET_MONTH: first = 1; break; + case SCR_DATE_SET_FULL_YEAR: first = 0; break; + default: return NAN; + } + /* Each setter takes at most the fields from its own down to the next + * larger unit's end: setHours up to milliseconds, setMonth up to the + * date, setFullYear up to the date. */ + int last = first <= 2 ? 2 : 6; + for (int i = 0; i < argc && first + i <= last && i < 4; i++) *slots[first + i] = args[i]; + double day = scr_date_v8_make_day(y, mo, dt); + double time = scr_date_v8_make_time(h, mi, s, ms); + if (!utc) return scr_date_from_local(day, time); + if (!isfinite(day) || !isfinite(time)) return NAN; + return scr_date_new_ms(day * 86400000.0 + time); +} + /* ── V8's date-string parser, the fallback ───────────────────────────── * scr_date_parse answers the bounded grammars directly and asks * this parser only when they produce NaN. It follows V8's DateParser @@ -12549,6 +12745,39 @@ static ScrStr *scr_date_text(double ms, int mode) { } ScrStr *scr_date_to_string(double ms) { return scr_date_text(ms, SCR_DATE_TEXT_FULL); } + +/* JSON.stringify of a Date object: its toJSON(), the quoted + * toISOString() text, or null for an invalid date. */ +void scr_jb_put_date(ScrJsonBuf *b, const ScrDate *date) { + if (isnan(date->ms)) { + scr_jb_puts(b, "null"); + return; + } + ScrStr *iso = scr_date_to_iso(date->ms); + scr_jb_put_json_str(b, iso); + scr_str_release(iso); +} + +/* String(date) of a Date object: Date arrays' default ordering key. +1. */ +ScrStr *scr_date_object_to_string(void *date) { + return scr_date_text(((ScrDate *)date)->ms, SCR_DATE_TEXT_FULL); +} + +/* Array.prototype.join over Date objects: each element's String() text, + * holes empty. Both borrowed; +1. */ +ScrStr *scr_date_arr_join(ScrArr *dates, const ScrStr *sep) { + ScrJsonBuf b; + scr_jb_init(&b); + double len = scr_arr_len(dates); + for (double i = 0; i < len; i++) { + if (i > 0) scr_jb_put_str(&b, sep); + if (scr_arr_state(dates, i) != 1) continue; + ScrStr *text = scr_date_object_to_string(scr_arr_peek_ref(dates, i)); + scr_jb_put_str(&b, text); + scr_str_release(text); + } + return scr_jb_finish(&b); +} ScrStr *scr_date_to_date_string(double ms) { return scr_date_text(ms, SCR_DATE_TEXT_DATE); } ScrStr *scr_date_to_time_string(double ms) { return scr_date_text(ms, SCR_DATE_TEXT_TIME); } @@ -12642,8 +12871,15 @@ ScrStr *scr_num_to_string_radix_f64(double value, double requested) { bool negative = value < 0; value = fabs(value); double integer = floor(value), fraction = value - integer; - double tolerance = 0.5 * (nextafter(value, INFINITY) - value); - if (tolerance <= 0) tolerance = nextafter(0, INFINITY); + /* The next double above the positive finite value: its bits plus one + * (Infinity above the largest double). */ + uint64_t value_bits; + memcpy(&value_bits, &value, sizeof value_bits); + value_bits += 1; + double next; + memcpy(&next, &value_bits, sizeof next); + double tolerance = 0.5 * (next - value); + if (tolerance <= 0) tolerance = 0x1p-1074; if (fraction >= tolerance) { buffer[end++] = '.'; do { @@ -13009,11 +13245,29 @@ ScrArr *scr_set_to_arr_ref(const ScrMap *s, const ScrArrOps *ops) { return out; } -/* JS Date storage owns an identity; the typed read-only Date ABI continues - * to use milliseconds and explicitly extracts them for native getters. */ -typedef struct { size_t rc; double milliseconds; } ScrNativeDate; -static void *scr_native_date_retain(void *ptr) { ((ScrNativeDate *)ptr)->rc++; return ptr; } -static void scr_native_date_release(void *ptr) { if (--((ScrNativeDate *)ptr)->rc == 0) free(ptr); } +/* A Date object: one time value, and its identity. Compiled code and + * checked-dynamic Date handles share these objects, so a Date keeps its + * identity across `unknown` conversions. */ +ScrDate *scr_date_new(double ms) { + ScrDate *date = malloc(sizeof *date); + if (!date) scr_trap("scriptc: out of memory\n"); + date->rc = 1; + date->ms = ms; + return date; +} + +ScrDate *scr_date_retain(ScrDate *date) { + if (date->rc != SIZE_MAX) date->rc++; + return date; +} + +void scr_date_release(ScrDate *date) { + if (!date || date->rc == SIZE_MAX) return; + if (--date->rc == 0) free(date); +} + +void *scr_date_retain_v(void *p) { return scr_date_retain(p); } +void scr_date_release_v(void *p) { scr_date_release(p); } bool scr_dyn_native_date_is(const ScrDyn *value) { return value && value->kind == SCR_DYN_HANDLE && value->v.handle.tag == SCR_DYNH_DATE; @@ -13124,22 +13378,55 @@ static ScrStr *scr_date_class_name(const ScrDyn *value) { } double scr_dyn_native_date_value(const ScrDyn *value) { - if (scr_dyn_native_date_is(value)) return ((ScrNativeDate *)value->v.handle.ptr)->milliseconds; + if (scr_dyn_native_date_is(value)) return ((ScrDate *)value->v.handle.ptr)->ms; double time; if (scr_dyn_date_class_time(value, &time)) return time; scr_dyn_check_fail(NULL, "Date", value); return NAN; } +/* The Date object a checked-dynamic Date value holds, +1: the Date itself, + * or a new Date with the time value of an instance of a class declared + * `extends Date`. NULL with the check failure pending for any other + * value. */ +ScrDate *scr_dyn_native_date_check(const ScrDyn *value, const ScrDynPath *path) { + if (scr_dyn_native_date_is(value)) return scr_date_retain(value->v.handle.ptr); + double time; + if (scr_dyn_date_class_time(value, &time)) return scr_date_new(time); + scr_dyn_check_fail(path, "Date", value); + return NULL; +} + /* Method names without another use in this file, in named arrays (see * scr_tz_posix_ids). */ static const char scr_date_to_date_string_name[] = "toDateString"; static const char scr_date_to_time_string_name[] = "toTimeString"; +/* The field setters a checked-dynamic Date answers (scr_date_set_fields). */ +static const struct { + const char *name; + int code, max; +} scr_native_date_setters[] = { + {"setMilliseconds", SCR_DATE_SET_MILLISECONDS, 1}, + {"setSeconds", SCR_DATE_SET_SECONDS, 2}, + {"setMinutes", SCR_DATE_SET_MINUTES, 3}, + {"setHours", SCR_DATE_SET_HOURS, 4}, + {"setDate", SCR_DATE_SET_DATE, 1}, + {"setMonth", SCR_DATE_SET_MONTH, 2}, + {"setFullYear", SCR_DATE_SET_FULL_YEAR, 3}, + {"setUTCMilliseconds", SCR_DATE_SET_UTC | SCR_DATE_SET_MILLISECONDS, 1}, + {"setUTCSeconds", SCR_DATE_SET_UTC | SCR_DATE_SET_SECONDS, 2}, + {"setUTCMinutes", SCR_DATE_SET_UTC | SCR_DATE_SET_MINUTES, 3}, + {"setUTCHours", SCR_DATE_SET_UTC | SCR_DATE_SET_HOURS, 4}, + {"setUTCDate", SCR_DATE_SET_UTC | SCR_DATE_SET_DATE, 1}, + {"setUTCMonth", SCR_DATE_SET_UTC | SCR_DATE_SET_MONTH, 2}, + {"setUTCFullYear", SCR_DATE_SET_UTC | SCR_DATE_SET_FULL_YEAR, 3}, +}; + static ScrDyn *scr_native_date_invoke(void *ptr, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, const char *what) { (void)self; (void)what; - double ms = ((ScrNativeDate *)ptr)->milliseconds; + double ms = ((ScrDate *)ptr)->ms; if (!strcmp(method, "getTime") || !strcmp(method, "valueOf")) return scr_dyn_new_num(ms); if (!strcmp(method, "toString") || !strcmp(method, scr_date_to_date_string_name) || !strcmp(method, scr_date_to_time_string_name) || !strcmp(method, "toUTCString")) { @@ -13162,8 +13449,27 @@ static ScrDyn *scr_native_date_invoke(void *ptr, ScrDyn *self, const char *metho if (!strcmp(method, "setTime")) { double time = NAN; if (argc > 0 && !scr_dyn_number_coerce_js(args[0], &time)) return NULL; - ((ScrNativeDate *)ptr)->milliseconds = scr_date_new_ms(time); - return scr_dyn_new_num(((ScrNativeDate *)ptr)->milliseconds); + ((ScrDate *)ptr)->ms = scr_date_new_ms(time); + return scr_dyn_new_num(((ScrDate *)ptr)->ms); + } + for (size_t i = 0; i < sizeof scr_native_date_setters / sizeof *scr_native_date_setters; i++) { + if (strcmp(method, scr_native_date_setters[i].name)) continue; + /* V8's order: ToNumber of the first argument, then the time value, + * then (unless the date is invalid) ToNumber of the optional ones the + * setter takes. */ + int code = scr_native_date_setters[i].code, max = scr_native_date_setters[i].max; + double values[4] = {NAN, 0, 0, 0}; + if (argc > 0 && !scr_dyn_number_coerce_js(args[0], &values[0])) return NULL; + double t = ((ScrDate *)ptr)->ms; + int used = argc < (size_t)max ? (int)argc : max; + if (used < 1) used = 1; + if (!isnan(t) || (code & ~SCR_DATE_SET_UTC) == SCR_DATE_SET_FULL_YEAR) { + for (int k = 1; k < used; k++) + if (!scr_dyn_number_coerce_js(args[k], &values[k])) return NULL; + } + double next = scr_date_set_fields(t, code, used, values[0], values[1], values[2], values[3]); + ((ScrDate *)ptr)->ms = next; + return scr_dyn_new_num(next); } #define DATE_GET(name, fn) if (!strcmp(method, name)) return scr_dyn_new_num(fn(ms)) DATE_GET("getFullYear", scr_date_get_full_year_local); @@ -13209,7 +13515,7 @@ double scr_dyn_date_arguments_time(const ScrDyn *arguments) { ScrDyn *date = arguments->v.arr.len > 1 ? scr_dyn_native_date_new_fields(arguments) : scr_dyn_native_date_new_value(arguments); if (!date) return NAN; - double time = ((ScrNativeDate *)date->v.handle.ptr)->milliseconds; + double time = ((ScrDate *)date->v.handle.ptr)->ms; scr_dyn_release(date); return time; } @@ -13244,7 +13550,9 @@ static const char *const scr_native_date_methods[] = { "getFullYear", "getUTCFullYear", "getMonth", "getUTCMonth", "getDate", "getUTCDate", "getDay", "getUTCDay", "getHours", "getUTCHours", "getMinutes", "getUTCMinutes", "getSeconds", "getUTCSeconds", "getMilliseconds", - "getUTCMilliseconds", "getTimezoneOffset", "setTime", + "getUTCMilliseconds", "getTimezoneOffset", "setTime", "setMilliseconds", "setSeconds", + "setMinutes", "setHours", "setDate", "setMonth", "setFullYear", "setUTCMilliseconds", + "setUTCSeconds", "setUTCMinutes", "setUTCHours", "setUTCDate", "setUTCMonth", "setUTCFullYear", }; #define SCR_NATIVE_DATE_METHODS (sizeof scr_native_date_methods / sizeof scr_native_date_methods[0]) static SCR_TL ScrDyn *scr_native_date_method_values[SCR_NATIVE_DATE_METHODS]; @@ -13274,10 +13582,10 @@ static ScrDyn *scr_native_date_method_call(ScrClosure *closure, ScrDyn *const *a result = isfinite(time) ? scr_date_class_call(SCR_DATE_CLASS_TO_ISO_STRING, self, NULL) : scr_dyn_new_null(); } else { - ScrNativeDate date = { 1, time }; + ScrDate date = { 1, time }; result = scr_native_date_invoke(&date, self, method, args, argc, method); - if (result && date.milliseconds != time && !(isnan(date.milliseconds) && isnan(time))) { - ScrDyn *stored = scr_dyn_new_num(date.milliseconds); + if (result && date.ms != time && !(isnan(date.ms) && isnan(time))) { + ScrDyn *stored = scr_dyn_new_num(date.ms); ScrDyn *set = scr_date_class_call(SCR_DATE_CLASS_SET_TIME, self, stored); scr_dyn_release(stored); if (!set) { @@ -13330,19 +13638,50 @@ static ScrDyn *scr_native_date_get(void *ptr, const char *key, size_t key_len) { return NULL; } -/* A Date handle holding the time value `ms` (already TimeClip'd). */ -ScrDyn *scr_dyn_native_date_from_ms(double ms) { +/* structuredClone of a Date: a new Date with the same time value. */ +static ScrDyn *scr_native_date_clone(void *h) { + return scr_dyn_native_date_from_ms(((ScrDate *)h)->ms); +} + +/* A Date crossing threads (postMessage, workerData) carries its time value + * in a plain allocation any thread may read and free. */ +static void *scr_native_date_share(void *h) { + double *ms = malloc(sizeof *ms); + if (!ms) scr_trap("scriptc: out of memory\n"); + *ms = ((ScrDate *)h)->ms; + return ms; +} + +static ScrDyn *scr_native_date_adopt(void *payload) { + return scr_dyn_native_date_from_ms(*(double *)payload); +} + +static void scr_native_date_drop(void *payload) { free(payload); } + +static const ScrDynHandleTransfer scr_native_date_transfer = { + .share = scr_native_date_share, .adopt = scr_native_date_adopt, .drop = scr_native_date_drop}; + +/* A checked-dynamic handle for the Date object `date` (borrowed; the + * handle retains it), so the value stays the same object. */ +ScrDyn *scr_dyn_native_date(ScrDate *date) { static const ScrDynHandleOps ops = { - "Date", &scr_native_date_retain, &scr_native_date_release, &scr_native_date_invoke, - &scr_native_date_get, NULL, NULL, NULL, + .cls = "Date", + .retain = &scr_date_retain_v, + .release = &scr_date_release_v, + .invoke = &scr_native_date_invoke, + .get = &scr_native_date_get, + .clone = &scr_native_date_clone, + .transfer = &scr_native_date_transfer, }; scr_dyn_handle_install(SCR_DYNH_DATE, &ops); - ScrNativeDate *date = malloc(sizeof *date); - if (!date) scr_trap("scriptc: out of memory\n"); - date->rc = 1; - date->milliseconds = ms; - ScrDyn *result = scr_dyn_new_handle(date, SCR_DYNH_DATE); - scr_native_date_release(date); + return scr_dyn_new_handle(date, SCR_DYNH_DATE); +} + +/* A Date handle holding the time value `ms` (already TimeClip'd). */ +ScrDyn *scr_dyn_native_date_from_ms(double ms) { + ScrDate *date = scr_date_new(ms); + ScrDyn *result = scr_dyn_native_date(date); + scr_date_release(date); return result; } diff --git a/packages/runtime/src/scr_lib.h b/packages/runtime/src/scr_lib.h index 95f67044a..e01bc7b23 100644 --- a/packages/runtime/src/scr_lib.h +++ b/packages/runtime/src/scr_lib.h @@ -63,6 +63,9 @@ bool scr_lib_fork_info(double *target, uintptr_t *read_handle, * direct startup never filters user arguments. */ bool scr_lib_should_collapse_reexec_arg(ScrStr *cmd, ScrArr *args); ScrStr *scr_process_platform(void); /* +1 interned ("darwin", "linux", ...) */ +/* Whether OpenSSL, as Node 24 links it, names a digest `name` (its names, + * aliases and object identifiers, case-insensitively). */ +bool scr_digest_known(const char *name, size_t len); ScrStr *scr_process_cwd(void); /* +1 fresh (getcwd) */ bool scr_stdio_write(int fd, const void *data, size_t len); @@ -290,6 +293,7 @@ void scr_fs_mkdir(ScrStr *path); void scr_fs_rm(ScrStr *path); void scr_fs_rmdir(ScrStr *path); ScrArr *scr_fs_readdir(ScrStr *path); /* names (byte order on POSIX, as libuv sorts), no "."/".." */ +ScrArr *scr_fs_readdir_recursive(ScrStr *path); /* relative paths of every entry below path, Node's queue order */ /* The fs option forms (same throw discipline). mkdir_recursive is Node's * recursive algorithm (existing-dir target is a no-op; errors report @@ -409,6 +413,9 @@ void scr_fs_invalid_named_path(const ScrDyn *value, const ScrStr *path, const ch /* A string path argument: false after throwing Node's ERR_INVALID_ARG_VALUE * when it holds a null byte (`name` is the argument's name). */ bool scr_fs_path_ok(const ScrStr *path, const char *name); +/* scr_fs_path_ok as a statement: getValidatedPath's null-byte check on a + * path the call validates before converting a later URL path. */ +void scr_fs_check_path(ScrStr *path, ScrStr *name); void scr_fs_futimes(double fd, double atime, double mtime); void scr_fs_link(ScrStr *source, ScrStr *dest); void scr_fs_symlink(ScrStr *target, ScrStr *destination, int kind); diff --git a/packages/runtime/src/scr_loop_epoll.test.c b/packages/runtime/src/scr_loop_epoll.test.c index 6ab1621c5..7129a48d2 100644 --- a/packages/runtime/src/scr_loop_epoll.test.c +++ b/packages/runtime/src/scr_loop_epoll.test.c @@ -23,9 +23,21 @@ static double now_ms(void) { } static int keys[KEYS]; -static double deadline[KEYS]; +/* The poller reads the clock inside scrp_timer_arm, so a key's deadline is + * only known to lie between the delay added to the clock read before the + * call and the delay added to the read after it. A loaded machine can pause + * the test between its read and the poller's for milliseconds, so the + * checks below use these bounds instead of one guessed deadline. */ +static double earliest[KEYS], latest[KEYS]; static int state[KEYS]; /* 0 unarmed/cancelled, 1 armed, 2 fired */ +static void arm(ScrPoller *p, int i, double ms) { + double before = now_ms(); + assert(scrp_timer_arm(p, &keys[i], ms, &keys[i])); + earliest[i] = before + ms; + latest[i] = now_ms() + ms; +} + /* Wait for the poller fd, then drain; returns the events drained. */ static int pump(ScrPoller *p, ScrPollerEvent *evs, int max, int wait_ms) { struct pollfd pfd = {.fd = scrp_poller_fd(p), .events = POLLIN}; @@ -45,20 +57,14 @@ int main(void) { if (i == KEYS / 2) for (double until = now_ms() + 3; now_ms() < until;) { } - double ms = 5 + rand() % 120; - double t = now_ms(); - assert(scrp_timer_arm(p, &keys[i], ms, &keys[i])); - deadline[i] = t + ms; + arm(p, i, 5 + rand() % 120); state[i] = 1; } /* re-arm a third later and earlier, cancel a third (the heap and the key * table move entries on every one of these) */ for (int i = 0; i < KEYS; i++) { if (i % 3 == 0) { - double ms = 1 + rand() % 150; - double t = now_ms(); - assert(scrp_timer_arm(p, &keys[i], ms, &keys[i])); - deadline[i] = t + ms; + arm(p, i, 1 + rand() % 150); } else if (i % 3 == 1) { scrp_timer_cancel(p, &keys[i]); state[i] = 0; @@ -74,7 +80,7 @@ int main(void) { int armed = 0; for (int i = 0; i < KEYS; i++) armed += state[i] == 1; int fired = 0, piped = 0; - double last = 0; + double fired_earliest = 0; /* the largest earliest bound of a fired key */ ScrPollerEvent evs[16]; /* small batches leave due timers for the next drain */ while (fired < armed) { int n = pump(p, evs, 16, 1000); @@ -91,9 +97,11 @@ int main(void) { assert(i >= 0 && i < KEYS); assert(state[i] == 1); double t = now_ms(); - assert(t + 1.0 >= deadline[i]); /* never early (timerfd rounding aside) */ - assert(deadline[i] + 1.0 >= last); /* deadline order */ - if (deadline[i] > last) last = deadline[i]; + assert(t + 1.0 >= earliest[i]); /* never early (timerfd rounding aside) */ + /* deadline order: no key fired earlier has a deadline after this + * one's (equal deadlines fire in arm order, which the bounds allow) */ + assert(latest[i] >= fired_earliest); + if (earliest[i] > fired_earliest) fired_earliest = earliest[i]; state[i] = 2; fired++; } diff --git a/packages/runtime/src/scr_mailbox.c b/packages/runtime/src/scr_mailbox.c index b2af22774..6e5678378 100644 --- a/packages/runtime/src/scr_mailbox.c +++ b/packages/runtime/src/scr_mailbox.c @@ -21,6 +21,7 @@ #include #include #ifdef __linux__ +#include #include #endif #endif @@ -321,7 +322,32 @@ void scr_mailbox_wait(ScrMailbox *mailbox, double milliseconds) { if (!(milliseconds > 0)) return; if (scr_mailbox_spin(mailbox, milliseconds)) return; scr_mailbox_lock(mailbox); - if (!mailbox->head && !mailbox->closed) { + bool waiting = !mailbox->head && !mailbox->closed; +#ifdef __linux__ + /* Linux sleeps in ppoll(2) on the wake pipe rather than on the + * condition: a pipe write wakes its reader as a synchronous wakeup, + * which the scheduler can run on the writer's CPU when the writer is + * about to block. A worker that posts its result and then waits frees + * that CPU, so the thread it woke runs there at once instead of queueing + * behind another busy thread until the scheduler preempts it. Bytes left + * by signals whose events were already taken are drained first, under + * the lock: an event posted after the unlock writes a new byte. */ + if (waiting) scr_mailbox_drain(mailbox); + scr_mailbox_unlock(mailbox); + if (!waiting) return; + struct pollfd wake = {mailbox->read_fd, POLLIN, 0}; + if (isinf(milliseconds)) (void)ppoll(&wake, 1, NULL, NULL); + else { + /* The event loop rechecks its monotonic deadlines after every wake, + * including spurious ones. Bound conversion before casting time_t. */ + if (milliseconds > 86400000) milliseconds = 86400000; + struct timespec timeout = {(time_t)(milliseconds / 1000), + (long)ceil(fmod(milliseconds, 1000) * 1000000)}; + if (timeout.tv_nsec == 1000000000) { timeout.tv_sec++; timeout.tv_nsec = 0; } + (void)ppoll(&wake, 1, &timeout, NULL); + } +#else + if (waiting) { #ifdef _WIN32 DWORD timeout = milliseconds >= INFINITE ? INFINITE : (DWORD)ceil(milliseconds); (void)SleepConditionVariableSRW(&mailbox->condition, &mailbox->lock, timeout, 0); @@ -348,6 +374,7 @@ void scr_mailbox_wait(ScrMailbox *mailbox, double milliseconds) { #endif } scr_mailbox_unlock(mailbox); +#endif } int scr_mailbox_pollfd(const ScrMailbox *mailbox) { diff --git a/packages/runtime/src/scr_map.c b/packages/runtime/src/scr_map.c index 1a6a3fc00..ccfd4b907 100644 --- a/packages/runtime/src/scr_map.c +++ b/packages/runtime/src/scr_map.c @@ -988,6 +988,12 @@ double scr_map_size(const ScrMap *m) { return (double)m->nlive; } * table. Regrowth is amortized against the insertions that need it. * A map without entries has empty tables already: clearing it does no * work and keeps its storage for the next fill. */ +void scr_map_prevent_extensions(ScrMap *m) { + if (m->rc != SIZE_MAX) m->flags |= SCR_MAP_NOT_EXTENSIBLE; +} + +bool scr_map_is_extensible(const ScrMap *m) { return (m->flags & SCR_MAP_NOT_EXTENSIBLE) == 0; } + void scr_map_clear(ScrMap *m) { size_t walked = m->nentries; if (walked == 0) return; @@ -1199,9 +1205,11 @@ static __attribute__((noinline)) void scr_map_set_num(ScrMap *m, uint64_t hash, } /* A number-key map with at least this many entries moves to the direct - * index when its keys allow it, so small integer keys are indexed before - * the linear scan ends. */ -#define SCR_MAP_DENSE_AFTER 4 + * index when its keys allow it: from its second key, so a map of a few + * small integer keys (a histogram over 0..3) reads through the inline + * direct index rather than the out-of-line linear scan. A map with one + * key keeps its linear storage. */ +#define SCR_MAP_DENSE_AFTER 1 /* Whether appending one more entry would grow the lookup index rather than * reuse tombstones: scr_map_reserve_append compacts a linear map with a diff --git a/packages/runtime/src/scr_map.h b/packages/runtime/src/scr_map.h index d1ca387bf..a581cc1c7 100644 --- a/packages/runtime/src/scr_map.h +++ b/packages/runtime/src/scr_map.h @@ -116,6 +116,10 @@ typedef struct ScrMapOps { * whose object has a null prototype (Object.groupBy results). Compiled * code sets and tests the byte; the runtime never copies it. */ #define SCR_MAP_NULL_PROTO 1u +/* Object.freeze, Object.seal or Object.preventExtensions of a Map or Set: + * it has no own properties, so it is then frozen, sealed and not + * extensible at once. Its entries stay writable, as in Node. */ +#define SCR_MAP_NOT_EXTENSIBLE 2u typedef struct ScrMap { size_t rc; /* SIZE_MAX = immortal (unused for maps; kept per convention) */ @@ -272,6 +276,10 @@ void scr_map_release_v(void *m); void scr_map_trace_v(void *m, ScrTraceVisit visit, void *ctx); double scr_map_size(const ScrMap *m); /* live entries (Map.size) */ +/* Object.freeze, seal and preventExtensions of a Map or Set + * (SCR_MAP_NOT_EXTENSIBLE); immortal maps are left as they are. */ +void scr_map_prevent_extensions(ScrMap *m); +bool scr_map_is_extensible(const ScrMap *m); void scr_map_clear(ScrMap *m); bool scr_map_has_f64(const ScrMap *m, double key); diff --git a/packages/runtime/src/scr_map.test.c b/packages/runtime/src/scr_map.test.c index 9916c7406..e04e0dac9 100644 --- a/packages/runtime/src/scr_map.test.c +++ b/packages/runtime/src/scr_map.test.c @@ -184,9 +184,11 @@ static void iterate_while_mutating(void) { static void modes(void) { ScrMap *map = scr_map_new(SCR_MAP_KEY_F64, SCR_MAP_VAL_F64, NULL); - for (int i = 1; i <= 4; i++) scr_map_set_f64_f64(map, i, i); - assert(!map->ndense && !map->nbuckets); /* still a linear scan */ - scr_map_set_f64_f64(map, 5, 5); + scr_map_set_f64_f64(map, 1, 1); + assert(!map->ndense && !map->nbuckets); /* one key: still a linear scan */ + scr_map_set_f64_f64(map, 2, 2); + assert(map->ndense >= 16 && !map->nbuckets); /* from the second small integer key */ + for (int i = 3; i <= 5; i++) scr_map_set_f64_f64(map, i, i); assert(map->ndense >= 16 && !map->nbuckets); for (int i = 6; i < 1000; i++) scr_map_set_f64_f64(map, i, i); assert(map->ndense >= 1000 && map->ndense <= 1000 + 1000 / 4 + 64); /* near the largest key */ diff --git a/packages/runtime/src/scr_mime.c b/packages/runtime/src/scr_mime.c new file mode 100644 index 000000000..352a7d391 --- /dev/null +++ b/packages/runtime/src/scr_mime.c @@ -0,0 +1,603 @@ +/* node:util MIMEType and MIMEParams (Node's lib/internal/mime.js) as + * native handles of the checked-dynamic tree. The parser scans UTF-16 code + * units, as Node's string searches do, so error offsets match; every value + * that survives validation is ASCII (types, subtypes, parameter names) or + * Latin-1 (parameter values). MIMEParams keeps Map semantics: insertion + * order, a set of an existing name keeps its place, and a deleted entry + * leaves a gap that live iterators skip. A separate unit, linked only by + * programs that construct these classes. */ +#include "scr_runtime.h" + +#include +#include +#include + +typedef struct { + size_t rc; + ScrStr **keys; + ScrStr **values; /* NULL for a deleted entry */ + size_t len, cap; +} ScrMimeParams; + +typedef struct { + size_t rc; + ScrStr *type; + ScrStr *subtype; + ScrDyn *params; /* the MIMEParams handle box, owned */ +} ScrMimeType; + +static void scr_mime_oom(void) { scr_trap("scriptc: out of memory\n"); } + +/* ── code units ─────────────────────────────────────────────────────── */ + +typedef struct { + uint16_t *units; + size_t len; +} ScrMimeUnits; + +static ScrMimeUnits scr_mime_units(ScrStr *text) { + ScrMimeUnits out; + out.len = (size_t)scr_str_utf16_len(text); + out.units = malloc((out.len ? out.len : 1) * sizeof *out.units); + if (!out.units) scr_mime_oom(); + for (size_t i = 0; i < out.len; i++) out.units[i] = (uint16_t)scr_str_char_code_at(text, (double)i); + return out; +} + +static bool scr_mime_ws(uint16_t c) { return c == '\r' || c == '\n' || c == '\t' || c == ' '; } + +static bool scr_mime_token(uint16_t c) { + if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')) return true; + return c != 0 && c < 0x80 && strchr("!#$%&'*+-.^_`|~", (char)c) != NULL; +} + +static bool scr_mime_quoted(uint16_t c) { + return c == '\t' || (c >= 0x20 && c <= 0x7E) || (c >= 0x80 && c <= 0xFF); +} + +/* The index of the first unit of [from, to) outside the token set, as an + * offset from `from`, or -1. */ +static long scr_mime_invalid_token(const uint16_t *u, size_t from, size_t to) { + for (size_t i = from; i < to; i++) + if (!scr_mime_token(u[i])) return (long)(i - from); + return -1; +} + +/* A string of units that are all at most 0xFF, lowered (ASCII only) when + * `lower`. +1. */ +static ScrStr *scr_mime_latin1(const uint16_t *u, size_t from, size_t to, bool lower) { + ScrJsonBuf b; + scr_jb_init(&b); + for (size_t i = from; i < to; i++) { + uint16_t c = u[i]; + if (lower && c >= 'A' && c <= 'Z') c = (uint16_t)(c + ('a' - 'A')); + if (c < 0x80) { + scr_jb_putc(&b, (char)c); + } else { + char pair[2] = {(char)(0xC0 | (c >> 6)), (char)(0x80 | (c & 0x3F))}; + scr_jb_write(&b, pair, 2); + } + } + return scr_jb_finish(&b); +} + +/* ERR_INVALID_MIME_SYNTAX(production, str, invalidIndex). */ +static void scr_mime_syntax_fail(const char *production, const ScrStr *str, long index) { + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_puts(&b, "The MIME syntax for a "); + scr_jb_puts(&b, production); + scr_jb_puts(&b, " in \""); + scr_jb_put_str(&b, str); + scr_jb_puts(&b, "\" is invalid"); + if (index != -1) { + char at[32]; + snprintf(at, sizeof at, " at %ld", index); + scr_jb_puts(&b, at); + } + ScrStr *message = scr_jb_finish(&b); + scr_throw_error_msg_code(SCR_ERR_TYPE, message->data, message->len, "ERR_INVALID_MIME_SYNTAX"); + scr_str_release(message); +} + +/* ── MIMEParams ─────────────────────────────────────────────────────── */ + +static ScrMimeParams *scr_mime_params_alloc(void) { + ScrMimeParams *p = calloc(1, sizeof *p); + if (!p) scr_mime_oom(); + p->rc = 1; + return p; +} + +static void *scr_mime_params_retain(void *h) { + ((ScrMimeParams *)h)->rc++; + return h; +} + +static void scr_mime_params_release(void *h) { + ScrMimeParams *p = h; + if (!p || --p->rc > 0) return; + for (size_t i = 0; i < p->len; i++) { + scr_str_release(p->keys[i]); + scr_str_release(p->values[i]); + } + free(p->keys); + free(p->values); + free(p); +} + +static long scr_mime_params_find(const ScrMimeParams *p, const ScrStr *key) { + for (size_t i = 0; i < p->len; i++) + if (p->values[i] && scr_str_eq((ScrStr *)p->keys[i], (ScrStr *)key)) return (long)i; + return -1; +} + +/* Map.prototype.set: an existing name keeps its place. Both borrowed. */ +static void scr_mime_params_put(ScrMimeParams *p, ScrStr *key, ScrStr *value) { + long at = scr_mime_params_find(p, key); + if (at >= 0) { + scr_str_release(p->values[at]); + p->values[at] = scr_str_retain(value); + return; + } + if (p->len == p->cap) { + p->cap = p->cap ? p->cap * 2 : 4; + p->keys = realloc(p->keys, p->cap * sizeof *p->keys); + p->values = realloc(p->values, p->cap * sizeof *p->values); + if (!p->keys || !p->values) scr_mime_oom(); + } + p->keys[p->len] = scr_str_retain(key); + p->values[p->len] = scr_str_retain(value); + p->len++; +} + +/* Node's MIMEParams#parse over the text after the subtype's ';'. */ +static void scr_mime_params_parse(ScrMimeParams *p, const uint16_t *u, size_t n) { + size_t end = n; + while (end > 0 && scr_mime_ws(u[end - 1])) end--; + size_t pos = 0; + while (pos < end) { + while (pos < n && scr_mime_ws(u[pos])) pos++; + size_t name_end = pos; + while (name_end < n && u[name_end] != ';' && u[name_end] != '=') name_end++; + size_t name_from = pos; + pos = name_end; + if (pos < end) { + uint16_t c = u[pos]; + pos++; + if (c == ';') continue; + } + if (pos >= end) break; + uint16_t *value = NULL; + size_t value_len = 0; + if (u[pos] == '"') { + pos++; + /* QUOTED_VALUE_PATTERN: escaped units, a trailing backslash (group 1) + * and the closing quote (group 2). */ + size_t i = pos; + bool trailing_backslash = false, closed = false; + while (i < n) { + if (u[i] == '\\') { + if (i + 1 == n) { + trailing_backslash = true; + i++; + break; + } + bool pair = u[i + 1] >= 0xD800 && u[i + 1] <= 0xDBFF && i + 2 < n && u[i + 2] >= 0xDC00 && u[i + 2] <= 0xDFFF; + i += pair ? 3 : 2; + } else if (u[i] != '"') { + i++; + } else { + break; + } + } + if (i < n && u[i] == '"' && !trailing_backslash) { + closed = true; + i++; + } + size_t match = i - pos; + size_t inside = trailing_backslash || closed ? match - 1 : match; + value = malloc((inside + 2) * sizeof *value); + if (!value) scr_mime_oom(); + /* removeBackslashes, then the unmatched backslash back at the end. */ + size_t k; + for (k = 0; inside > 0 && k < inside - 1; k++) { + uint16_t c = u[pos + k]; + if (c == '\\') { + k++; + value[value_len++] = u[pos + k]; + } else { + value[value_len++] = c; + } + } + if (inside > 0 && k == inside - 1) value[value_len++] = u[pos + k]; + if (trailing_backslash) value[value_len++] = '\\'; + pos += match; + } else { + size_t value_end = pos; + while (value_end < n && u[value_end] != ';') value_end++; + size_t trimmed = value_end; + while (trimmed > pos && scr_mime_ws(u[trimmed - 1])) trimmed--; + size_t raw = value_end - pos; + if (trimmed == pos) { + pos += raw; + continue; + } + value_len = trimmed - pos; + value = malloc(value_len * sizeof *value); + if (!value) scr_mime_oom(); + memcpy(value, u + pos, value_len * sizeof *value); + pos += raw; + } + bool valid_name = name_end > name_from && scr_mime_invalid_token(u, name_from, name_end) == -1; + bool valid_value = true; + for (size_t i = 0; i < value_len && valid_value; i++) valid_value = scr_mime_quoted(value[i]); + if (valid_name && valid_value) { + ScrStr *key = scr_mime_latin1(u, name_from, name_end, true); + if (scr_mime_params_find(p, key) < 0) { + ScrStr *text = scr_mime_latin1(value, 0, value_len, false); + scr_mime_params_put(p, key, text); + scr_str_release(text); + } + scr_str_release(key); + } + free(value); + pos++; + } +} + +/* The `encode` step of MIMEParams#toString. */ +static void scr_mime_encode_value(ScrJsonBuf *b, const ScrStr *value) { + if (value->len == 0) { + scr_jb_puts(b, "\"\""); + return; + } + bool plain = true; + for (size_t i = 0; i < value->len && plain; i++) { + unsigned char c = (unsigned char)value->data[i]; + plain = c < 0x80 && scr_mime_token(c); + } + if (plain) { + scr_jb_put_str(b, value); + return; + } + scr_jb_putc(b, '"'); + for (size_t i = 0; i < value->len; i++) { + char c = value->data[i]; + if (c == '"' || c == '\\') scr_jb_putc(b, '\\'); + scr_jb_putc(b, c); + } + scr_jb_putc(b, '"'); +} + +static ScrStr *scr_mime_params_to_string(const ScrMimeParams *p) { + ScrJsonBuf b; + scr_jb_init(&b); + bool first = true; + for (size_t i = 0; i < p->len; i++) { + if (!p->values[i]) continue; + if (!first) scr_jb_putc(&b, ';'); + first = false; + scr_jb_put_str(&b, p->keys[i]); + scr_jb_putc(&b, '='); + scr_mime_encode_value(&b, p->values[i]); + } + return scr_jb_finish(&b); +} + +/* ToString(name), lowered: `toASCIILower(`${name}`)`. +1 or NULL pending. */ +static ScrStr *scr_mime_name_arg(ScrDyn *const *args, size_t argc) { + ScrStr *raw = scr_dyn_string_coerce_js(argc ? args[0] : scr_dyn_undefined()); + if (!raw) return NULL; + ScrJsonBuf b; + scr_jb_init(&b); + for (size_t i = 0; i < raw->len; i++) { + char c = raw->data[i]; + scr_jb_putc(&b, c >= 'A' && c <= 'Z' ? (char)(c + ('a' - 'A')) : c); + } + scr_str_release(raw); + return scr_jb_finish(&b); +} + +static ScrDyn *scr_mime_not_a_function(const char *what) { + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_puts(&b, what); + scr_jb_puts(&b, " is not a function"); + scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&b)); + return NULL; +} + +static ScrDyn *scr_mime_params_step(void *h, double *index, unsigned selection) { + ScrMimeParams *p = h; + size_t i = (size_t)*index; + while (i < p->len && !p->values[i]) i++; + bool done = i >= p->len; + ScrDyn *value; + if (done) { + value = scr_dyn_retain(scr_dyn_undefined()); + *index = (double)p->len; + } else { + if (selection == 2) { + value = scr_dyn_new_arr(); + scr_dyn_arr_push(value, scr_dyn_new_str(p->keys[i])); + scr_dyn_arr_push(value, scr_dyn_new_str(p->values[i])); + } else { + value = scr_dyn_new_str(selection == 1 ? p->keys[i] : p->values[i]); + } + *index = (double)(i + 1); + } + ScrDyn *result = scr_dyn_new_obj(); + scr_dyn_obj_set(result, "value", 5, value); + scr_dyn_obj_set(result, "done", 4, scr_dyn_new_bool(done)); + return result; +} + +static ScrDyn *scr_mime_params_invoke(void *h, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, + const char *what) { + ScrMimeParams *p = h; + if (!strcmp(method, "entries") || !strcmp(method, "keys") || !strcmp(method, "values")) + return scr_dyn_native_handle_iterator(self, !strcmp(method, "entries") ? 2 : !strcmp(method, "keys") ? 1 : 0); + if (!strcmp(method, "toString") || !strcmp(method, "toJSON")) { + ScrStr *text = scr_mime_params_to_string(p); + ScrDyn *result = scr_dyn_new_str(text); + scr_str_release(text); + return result; + } + bool get = !strcmp(method, "get"), has = !strcmp(method, "has"), del = !strcmp(method, "delete"); + bool set = !strcmp(method, "set"); + if (!get && !has && !del && !set) return scr_mime_not_a_function(what); + ScrStr *name = scr_mime_name_arg(args, argc); + if (!name) return NULL; + ScrDyn *result = NULL; + long at = scr_mime_params_find(p, name); + if (get) { + result = at >= 0 ? scr_dyn_new_str(p->values[at]) : scr_dyn_new_null(); + } else if (has) { + result = scr_dyn_new_bool(at >= 0); + } else if (del) { + if (at >= 0) { + scr_str_release(p->values[at]); + p->values[at] = NULL; + } + result = scr_dyn_retain(scr_dyn_undefined()); + } else { + ScrStr *value = scr_dyn_string_coerce_js(argc > 1 ? args[1] : scr_dyn_undefined()); + if (value) { + ScrMimeUnits nu = scr_mime_units(name); + long bad_name = nu.len == 0 ? -1 : scr_mime_invalid_token(nu.units, 0, nu.len); + free(nu.units); + ScrMimeUnits vu = scr_mime_units(value); + long bad_value = -1; + for (size_t i = 0; i < vu.len && bad_value == -1; i++) + if (!scr_mime_quoted(vu.units[i])) bad_value = (long)i; + free(vu.units); + if (name->len == 0 || bad_name != -1) { + scr_mime_syntax_fail("parameter name", name, bad_name); + } else if (bad_value != -1) { + scr_mime_syntax_fail("parameter value", value, bad_value); + } else { + scr_mime_params_put(p, name, value); + result = scr_dyn_retain(scr_dyn_undefined()); + } + scr_str_release(value); + } + } + scr_str_release(name); + return result; +} + +static ScrDyn *scr_mime_empty_view(void *h) { + (void)h; + return scr_dyn_new_obj(); +} + +/* A method read as a value (`params.get` without a call) has no dyn + * function value here: it throws the "not supported yet" Error; other + * names answer undefined. */ +static ScrDyn *scr_mime_member_get(const char *cls, const char *const *methods, const char *key, size_t len) { + for (size_t i = 0; methods[i]; i++) { + if (strlen(methods[i]) != len || memcmp(methods[i], key, len) != 0) continue; + char message[128]; + int n = snprintf(message, sizeof message, "'%s.prototype.%s' as a value is not supported yet", cls, methods[i]); + scr_throw_error_msg(SCR_ERR_ERROR, message, (size_t)n); + return NULL; + } + return NULL; +} + +static ScrDyn *scr_mime_params_get(void *h, const char *key, size_t len) { + (void)h; + static const char *const methods[] = {"delete", "entries", "get", "has", "keys", "set", "values", "toString", "toJSON", NULL}; + return scr_mime_member_get("MIMEParams", methods, key, len); +} + +static ScrDyn *scr_mime_params_box(ScrMimeParams *p) { + static const ScrDynHandleOps ops = { + .cls = "MIMEParams", + .retain = scr_mime_params_retain, + .release = scr_mime_params_release, + .invoke = scr_mime_params_invoke, + .get = scr_mime_params_get, + .iter_step = scr_mime_params_step, + .inspect_view = scr_mime_empty_view, + .inspect_view_empty = true, + }; + scr_dyn_handle_install(SCR_DYNH_MIME_PARAMS, &ops); + ScrDyn *box = scr_dyn_new_handle(p, SCR_DYNH_MIME_PARAMS); + scr_mime_params_release(p); + return box; +} + +/* new util.MIMEParams(): an empty parameter map. +1. */ +ScrDyn *scr_mime_params_new(void) { return scr_mime_params_box(scr_mime_params_alloc()); } + +/* ── MIMEType ───────────────────────────────────────────────────────── */ + +static void *scr_mime_type_retain(void *h) { + ((ScrMimeType *)h)->rc++; + return h; +} + +static void scr_mime_type_release(void *h) { + ScrMimeType *t = h; + if (!t || --t->rc > 0) return; + scr_str_release(t->type); + scr_str_release(t->subtype); + scr_dyn_release(t->params); + free(t); +} + +/* The setters' validation: a non-empty token, lowered. +1 or NULL pending. */ +static ScrStr *scr_mime_part(const char *production, const ScrDyn *value) { + ScrStr *text = scr_dyn_string_coerce_js(value); + if (!text) return NULL; + ScrMimeUnits u = scr_mime_units(text); + long bad = scr_mime_invalid_token(u.units, 0, u.len); + ScrStr *out = NULL; + if (u.len == 0 || bad != -1) scr_mime_syntax_fail(production, text, bad); + else out = scr_mime_latin1(u.units, 0, u.len, true); + free(u.units); + scr_str_release(text); + return out; +} + +static ScrStr *scr_mime_type_to_string(const ScrMimeType *t) { + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_put_str(&b, t->type); + scr_jb_putc(&b, '/'); + scr_jb_put_str(&b, t->subtype); + ScrStr *params = scr_mime_params_to_string(t->params->v.handle.ptr); + if (params->len) { + scr_jb_putc(&b, ';'); + scr_jb_put_str(&b, params); + } + scr_str_release(params); + return scr_jb_finish(&b); +} + +static ScrDyn *scr_mime_type_get(void *h, const char *key, size_t len) { + ScrMimeType *t = h; + if (len == 4 && !memcmp(key, "type", 4)) return scr_dyn_new_str(t->type); + if (len == 7 && !memcmp(key, "subtype", 7)) return scr_dyn_new_str(t->subtype); + if (len == 6 && !memcmp(key, "params", 6)) return scr_dyn_retain(t->params); + if (len == 7 && !memcmp(key, "essence", 7)) { + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_put_str(&b, t->type); + scr_jb_putc(&b, '/'); + scr_jb_put_str(&b, t->subtype); + ScrStr *text = scr_jb_finish(&b); + ScrDyn *out = scr_dyn_new_str(text); + scr_str_release(text); + return out; + } + static const char *const methods[] = {"toString", "toJSON", NULL}; + return scr_mime_member_get("MIMEType", methods, key, len); +} + +static bool scr_mime_type_set(void *h, const char *key, size_t len, const ScrDyn *value) { + ScrMimeType *t = h; + bool type = len == 4 && !memcmp(key, "type", 4), subtype = len == 7 && !memcmp(key, "subtype", 7); + if (!type && !subtype) return false; + ScrStr *part = scr_mime_part(type ? "type" : "subtype", value); + if (!part) return true; + ScrStr **slot = type ? &t->type : &t->subtype; + scr_str_release(*slot); + *slot = part; + return true; +} + +static ScrDyn *scr_mime_type_invoke(void *h, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, + const char *what) { + (void)self; (void)args; (void)argc; + if (strcmp(method, "toString") && strcmp(method, "toJSON")) return scr_mime_not_a_function(what); + ScrStr *text = scr_mime_type_to_string(h); + ScrDyn *result = scr_dyn_new_str(text); + scr_str_release(text); + return result; +} + +/* new MIMEType(string, noThrow) and MIMEType.parse(string): NULL with the + * error pending, or (parse) NULL with nothing pending for invalid input. */ +static ScrDyn *scr_mime_type_make(const ScrDyn *input, bool no_throw) { + ScrStr *text = scr_dyn_string_coerce_js(input); + if (!text) return NULL; + ScrMimeUnits u = scr_mime_units(text); + size_t n = u.len, pos = 0; + while (pos < n && scr_mime_ws(u.units[pos])) pos++; + size_t type_end = pos; + while (type_end < n && u.units[type_end] != '/') type_end++; + bool slash = type_end < n; + long bad_type = scr_mime_invalid_token(u.units, pos, type_end); + ScrDyn *result = NULL; + if (type_end == pos || bad_type != -1 || !slash) { + if (!no_throw) scr_mime_syntax_fail("type", text, bad_type); + goto done; + } + size_t type_from = pos; + pos = type_end + 1; + size_t sub_end = pos; + while (sub_end < n && u.units[sub_end] != ';') sub_end++; + size_t sub_from = pos; + size_t trimmed = sub_end; + while (trimmed > sub_from && scr_mime_ws(u.units[trimmed - 1])) trimmed--; + pos = sub_end < n ? sub_end + 1 : sub_end; + long bad_sub = scr_mime_invalid_token(u.units, sub_from, trimmed); + if (trimmed == sub_from || bad_sub != -1) { + if (!no_throw) scr_mime_syntax_fail("subtype", text, bad_sub); + goto done; + } + ScrMimeType *t = calloc(1, sizeof *t); + if (!t) scr_mime_oom(); + t->rc = 1; + t->type = scr_mime_latin1(u.units, type_from, type_end, true); + t->subtype = scr_mime_latin1(u.units, sub_from, trimmed, true); + ScrMimeParams *p = scr_mime_params_alloc(); + scr_mime_params_parse(p, u.units + pos, n - pos); + t->params = scr_mime_params_box(p); + static const ScrDynHandleOps ops = { + .cls = "MIMEType", + .retain = scr_mime_type_retain, + .release = scr_mime_type_release, + .invoke = scr_mime_type_invoke, + .get = scr_mime_type_get, + .set = scr_mime_type_set, + .inspect_view = scr_mime_empty_view, + .inspect_view_empty = true, + }; + scr_dyn_handle_install(SCR_DYNH_MIME_TYPE, &ops); + result = scr_dyn_new_handle(t, SCR_DYNH_MIME_TYPE); + scr_mime_type_release(t); +done: + free(u.units); + scr_str_release(text); + return result; +} + +/* new util.MIMEType(string[, noThrowSymbol]): a second argument that is not + * null or undefined throws ERR_ILLEGAL_CONSTRUCTOR (Node's internal + * kNoThrow symbol is unreachable from programs). +1 or NULL pending. */ +ScrDyn *scr_mime_type_new(ScrDyn *input, ScrDyn *no_throw) { + if (no_throw->kind != SCR_DYN_UNDEF && no_throw->kind != SCR_DYN_NULL) { + static const char message[] = "Illegal constructor"; + scr_throw_error_msg_code(SCR_ERR_TYPE, message, sizeof message - 1, "ERR_ILLEGAL_CONSTRUCTOR"); + return NULL; + } + return scr_mime_type_make(input, false); +} + +/* MIMEType.parse(string): the MIMEType, or null for invalid input. +1, or + * NULL with a conversion error pending. */ +ScrDyn *scr_mime_type_parse(ScrDyn *input) { + ScrDyn *result = scr_mime_type_make(input, true); + if (result || scr_exc_pending()) return result; + return scr_dyn_new_null(); +} + +/* `value instanceof MIMEType` (kind 0) or `MIMEParams` (1). */ +bool scr_mime_is(const ScrDyn *value, double kind) { + return value->kind == SCR_DYN_HANDLE && + value->v.handle.tag == (kind == 0 ? SCR_DYNH_MIME_TYPE : SCR_DYNH_MIME_PARAMS); +} diff --git a/packages/runtime/src/scr_module_namespace.c b/packages/runtime/src/scr_module_namespace.c new file mode 100644 index 000000000..b0d7e80c4 --- /dev/null +++ b/packages/runtime/src/scr_module_namespace.c @@ -0,0 +1,121 @@ +/* Module namespace objects as checked-dynamic values + * (module-namespace-elements.ts lowers `import * as ns` values here). */ +#include "scr_runtime.h" + +#include +#include + +/* A module namespace object taken as a checked-dynamic value + * (module-namespace-elements.ts). The compiled getter answers an export's + * current value by name, so reads see live bindings; `keys` lists the + * namespace's export names. One object per module keeps its identity. + * Writes throw Node's strict-mode TypeError for a frozen namespace. */ +typedef struct ScrModuleNamespace { + size_t rc; + ScrStr *id; + ScrDyn *getter; + ScrArr *keys; + struct ScrModuleNamespace *next; +} ScrModuleNamespace; + +static SCR_TL ScrModuleNamespace *scr_module_namespaces; +static SCR_TL bool scr_module_namespace_cleanup_registered; + +static void *scr_module_namespace_retain(void *handle) { + ScrModuleNamespace *ns = handle; + ns->rc++; + return ns; +} + +static void scr_module_namespace_release(void *handle) { + ScrModuleNamespace *ns = handle; + if (--ns->rc) return; + scr_str_release(ns->id); + scr_dyn_release(ns->getter); + scr_arr_release(ns->keys); + free(ns); +} + +static void scr_module_namespace_cleanup(void) { + while (scr_module_namespaces) { + ScrModuleNamespace *ns = scr_module_namespaces; + scr_module_namespaces = ns->next; + scr_module_namespace_release(ns); + } +} + +static bool scr_module_namespace_has(const ScrModuleNamespace *ns, const char *key, size_t length) { + for (size_t i = 0; i < ns->keys->len; i++) { + ScrStr *entry = (ScrStr *)scr_arr_get_ref(ns->keys, (double)i); + bool same = entry->len == length && memcmp(entry->data, key, length) == 0; + scr_str_release(entry); + if (same) return true; + } + return false; +} + +static ScrDyn *scr_module_namespace_get(void *handle, const char *key, size_t length) { + ScrModuleNamespace *ns = handle; + if (!scr_module_namespace_has(ns, key, length)) return NULL; + ScrStr *name = scr_str_new(key, length); + ScrDyn *arg = scr_dyn_new_str(name); + scr_str_release(name); + ScrDyn *result = scr_dyn_call(ns->getter, &arg, 1, "module namespace export"); + scr_dyn_release(arg); + return result; +} + +static bool scr_module_namespace_set(void *handle, const char *key, size_t length, const ScrDyn *value) { + (void)value; + ScrModuleNamespace *ns = handle; + ScrJsonBuf text; + scr_jb_init(&text); + if (scr_module_namespace_has(ns, key, length)) { + scr_jb_puts(&text, "Cannot assign to read only property '"); + scr_jb_write(&text, key, length); + scr_jb_puts(&text, "' of object '[object Module]'"); + } else { + scr_jb_puts(&text, "Cannot add property "); + scr_jb_write(&text, key, length); + scr_jb_puts(&text, ", object is not extensible"); + } + ScrStr *message = scr_jb_finish(&text); + scr_throw_error_msg(SCR_ERR_TYPE, message->data, message->len); + scr_str_release(message); + return true; +} + +static ScrDyn *scr_module_namespace_invoke(void *handle, ScrDyn *self, const char *key, + ScrDyn *const *args, size_t argc, const char *what) { + ScrDyn *fn = scr_module_namespace_get(handle, key, strlen(key)); + if (fn == NULL) { + if (scr_exc_pending()) return NULL; + fn = scr_dyn_retain(scr_dyn_undefined()); + } + scr_dyn_this_push_dyn(self); + ScrDyn *result = scr_dyn_call(fn, args, argc, what); + scr_dyn_this_pop(); + scr_dyn_release(fn); + return result; +} + +ScrDyn *scr_module_namespace_value(ScrStr *id, ScrArr *keys, ScrDyn *getter) { + static const ScrDynHandleOps ops = { + .cls = "Module", .retain = scr_module_namespace_retain, .release = scr_module_namespace_release, + .get = scr_module_namespace_get, .set = scr_module_namespace_set, .invoke = scr_module_namespace_invoke, + }; + scr_dyn_handle_install(SCR_DYNH_MODULE_NAMESPACE, &ops); + for (ScrModuleNamespace *ns = scr_module_namespaces; ns; ns = ns->next) { + if (ns->id->len == id->len && memcmp(ns->id->data, id->data, id->len) == 0) + return scr_dyn_new_handle(ns, SCR_DYNH_MODULE_NAMESPACE); + } + if (!scr_module_namespace_cleanup_registered) { + scr_atexit(scr_module_namespace_cleanup); + scr_module_namespace_cleanup_registered = true; + } + ScrModuleNamespace *ns = malloc(sizeof(*ns)); + if (!ns) scr_trap("scriptc: out of memory\n"); + *ns = (ScrModuleNamespace){ 1, scr_str_retain(id), scr_dyn_retain(getter), scr_arr_retain(keys), scr_module_namespaces }; + scr_module_namespaces = ns; + return scr_dyn_new_handle(ns, SCR_DYNH_MODULE_NAMESPACE); +} diff --git a/packages/runtime/src/scr_net.c b/packages/runtime/src/scr_net.c index 4d5a37f77..776829e8c 100644 --- a/packages/runtime/src/scr_net.c +++ b/packages/runtime/src/scr_net.c @@ -420,6 +420,11 @@ enum { SCR_NET_K_SERVER = 1, SCR_NET_K_SOCKET = 2 }; struct ScrNetServer { int kind; /* FIRST: poller udata routes on it */ size_t rc; + /* The AsyncLocalStorage contexts of the listen() and close() calls + * (+1, NULL outside a store): 'listening', 'connection' and 'error' run + * in the first, 'close' and the close() callbacks in the second, as + * Node runs them in its handle's and its close request's contexts. */ + ScrAlsCtx *listen_ctx, *close_ctx; int fd; /* -1 while not listening */ int port; bool listening; @@ -446,6 +451,9 @@ struct ScrNetServer { ScrStr *unix_path; /* listen(path): the bound socket file, removed at close */ size_t nconns; ScrNetLs conn_ls, err_ls, close_ls, listening_cbs, timeout_ls; + /* tls/https servers: 'tlsClientError' (err, tlsSocket), fired by the + * TLS unit for a handshake that fails */ + ScrNetLs client_error_ls; /* server.maxConnections (unset = undefined) and its 'drop' listeners */ bool max_conns_set; double max_conns; @@ -486,6 +494,14 @@ struct ScrNetServer { struct ScrNetSocket { int kind; /* FIRST: poller udata routes on it */ size_t rc; + /* The AsyncLocalStorage context of the socket's creation (+1, NULL + * outside a store): its I/O events ('connect', 'data', 'end', 'close', + * 'error', 'timeout') run in it, as Node runs them in its handle's + * context. An accepted socket has none, except under an HTTP or TLS + * server, whose sockets take the server's listen() context. end_ctx is + * the end() call's context, for 'finish' and the end() callback (Node's + * shutdown request). */ + ScrAlsCtx *als, *end_ctx; int fd; /* -1 once closed */ bool unrefd; /* socket.unref(): does not keep the loop alive */ bool connecting; @@ -500,6 +516,16 @@ struct ScrNetSocket { /* write buffer: [whead, wlen) of wbuf is unsent */ char *wbuf; size_t whead, wlen, wcap; + /* Node's writable state.length: each write with bytes in the buffer + * counts in full until its last byte is sent (a partly sent write + * included). wq[wq_head, wq_n) holds those writes in order, each with + * the offset where its bytes end, counted from wbase, the offset of + * wbuf[0] in everything buffered since the buffer last emptied. */ + struct ScrNetPendingWrite { + size_t end, len; + } *wq; + size_t wq_head, wq_n, wq_cap; + size_t wbase, wpending; /* receive buffer: [rhead, rlen) of rbuf is arrived-but-unconsumed — * fed by paused-mode reads ('readable' consumers) and unshift; drained * FIRST by every consumer (data listeners, the parser attach, the TLS @@ -549,6 +575,11 @@ struct ScrNetSocket { ScrNetLs lookup_ls; struct ScrNetLookupEv *lookup_evs; ScrNetLs data_ls, end_ls, close_ls, err_ls, conn_ls, timeout_ls, readable_ls; + /* Writable backpressure: a user write answered false (the bytes it left + * unsent reached SCR_NET_WRITE_HWM), so 'drain' fires once the write + * buffer empties */ + ScrNetLs drain_ls; + bool need_drain; /* Flow control (pause()/resume()): user_paused holds reads OFF even * with consumers (kernel/TCP backpressure is the buffer); flowing * counts as a consumer even with no 'data' listener @@ -672,6 +703,13 @@ static SCR_TL ScrPoller *scr_net_poller = NULL; static void scr_net_close_fd_raw(int fd); /* forget-then-close, defined below */ +/* The listen() call's context: the server's 'listening', 'connection' + * and 'error' events run in it. */ +static void scr_net_server_capture_listen(ScrNetServer *s) { + scr_als_ctx_release(s->listen_ctx); + s->listen_ctx = scr_als_capture(); +} + ScrNetServer *scr_net_server_retain(ScrNetServer *s) { if (s->rc != SIZE_MAX) s->rc++; return s; @@ -687,6 +725,8 @@ void scr_net_server_release(ScrNetServer *s) { scr_net_ls_drop(&s->listening_cbs); scr_net_ls_drop(&s->timeout_ls); scr_net_ls_drop(&s->drop_ls); + scr_als_ctx_release(s->listen_ctx); + scr_als_ctx_release(s->close_ctx); scr_closure_release(s->close_override); for (size_t i = 0; i < 5; i++) scr_dyn_release(s->http_timeout_dyn[i]); scr_dyn_release(s->drop_max_connection); @@ -722,9 +762,12 @@ void scr_net_sock_release(ScrNetSocket *s) { scr_net_ls_drop(&s->conn_ls); scr_net_ls_drop(&s->timeout_ls); scr_net_ls_drop(&s->readable_ls); + scr_net_ls_drop(&s->drain_ls); scr_net_ls_drop(&s->finish_ls); scr_net_ls_drop(&s->wcb_ls); scr_net_ls_drop(&s->ready_ls); + scr_als_ctx_release(s->als); + scr_als_ctx_release(s->end_ctx); scr_str_release(s->pending_err); scr_str_release(s->remote_cache); for (size_t i = 0; i < s->dial_n; i++) scr_str_release(s->dial_ips[i]); @@ -737,6 +780,7 @@ void scr_net_sock_release(ScrNetSocket *s) { if (s->lookup_evs != NULL) scr_net_lookup_ops->free(s); scr_net_tunnel_free(s->tunnel); free(s->wbuf); + free(s->wq); free(s->rbuf); if (s->pipe_dst) scr_net_sock_release(s->pipe_dst); if (s->conn_pending) scr_net_server_release(s->conn_pending); @@ -969,6 +1013,7 @@ static ScrNetSocket *scr_net_sock_new(void) { s->kind = SCR_NET_K_SOCKET; s->rc = 1; s->fd = -1; + s->als = scr_als_capture(); #ifdef SCR_RC_AUDIT scr_net_live++; #endif @@ -1054,13 +1099,34 @@ static void scr_net_sock_maybe_finish_write(ScrNetSocket *s) { else scr_net_sock_update_read(s); /* the drain-to-EOF mode arms here */ } +/* The write buffer emptied (or its bytes moved elsewhere): every pending + * write is complete. */ +static void scr_net_sock_wq_clear(ScrNetSocket *s) { + s->whead = s->wlen = 0; + s->wq_head = s->wq_n = 0; + s->wbase = 0; + s->wpending = 0; +} + +/* Completes the pending writes whose last byte the flush has sent. */ +static void scr_net_sock_wq_settle(ScrNetSocket *s) { + size_t sent_to = s->wbase + s->whead; + while (s->wq_head < s->wq_n && s->wq[s->wq_head].end <= sent_to) { + s->wpending -= s->wq[s->wq_head].len; + s->wq_head++; + } + if (s->wq_head == s->wq_n) s->wq_head = s->wq_n = 0; +} + /* Append bytes to the write buffer (post-immediate-write remainder, or - * everything while connecting/buffered). */ -static void scr_net_sock_buffer(ScrNetSocket *s, const char *data, size_t len) { + * everything while connecting/buffered). `counted` is the length of the + * write they belong to, which counts in full until they are sent. */ +static void scr_net_sock_buffer(ScrNetSocket *s, const char *data, size_t len, size_t counted) { if (len == 0) return; /* compact the consumed head first */ if (s->whead > 0) { memmove(s->wbuf, s->wbuf + s->whead, s->wlen - s->whead); + s->wbase += s->whead; s->wlen -= s->whead; s->whead = 0; } @@ -1073,6 +1139,63 @@ static void scr_net_sock_buffer(ScrNetSocket *s, const char *data, size_t len) { } memcpy(s->wbuf + s->wlen, data, len); s->wlen += len; + if (s->wq_n == s->wq_cap && s->wq_head > 0) { + /* reclaim the completed writes before growing: a buffer that never + * empties completes old writes while new ones arrive */ + memmove(s->wq, s->wq + s->wq_head, (s->wq_n - s->wq_head) * sizeof *s->wq); + s->wq_n -= s->wq_head; + s->wq_head = 0; + } + if (s->wq_n == s->wq_cap) { + s->wq_cap = s->wq_cap ? s->wq_cap * 2 : 8; + s->wq = realloc(s->wq, s->wq_cap * sizeof *s->wq); + if (!s->wq) scr_net_oom(); + } + s->wq[s->wq_n].end = s->wbase + s->wlen; + s->wq[s->wq_n].len = counted; + s->wq_n++; + s->wpending += counted; +} + +/* Node's net.Socket writableHighWaterMark (the Writable default, + * getDefaultHighWaterMark()). */ +#define SCR_NET_WRITE_HWM 65536 + +/* 'drain' once a false write's bytes are all out: Node's afterWrite emits + * it when needDrain is set and nothing is pending, unless the stream is + * ending or destroyed. */ +static void scr_net_sock_maybe_drain(ScrNetSocket *s) { + if (!s->need_drain || s->wlen > s->whead) return; + if (s->wr_ending || s->close_emitted || s->fd < 0) return; + s->need_drain = false; + scr_net_fire0_this(&s->drain_ls, s, SCR_DYNH_NET_SOCKET); +} + +static void scr_net_sock_write_raw(ScrNetSocket *s, const char *data, size_t len); + +/* The write half takes writes: not ending or closed, and connected, + * connecting, or waiting to dial (the agent's deferred dial, the caller + * lookup), which buffers them until the connection is established. */ +static bool scr_net_sock_write_open(ScrNetSocket *w) { + if (w->wr_ending || w->close_emitted) return false; + return w->fd >= 0 || w->dial_deferred || w->lookup_wait; +} + +/* A user write: Writable.write's answer, as Node 24's writeOrBuffer + * computes it after the socket writes. Each write with bytes still + * unsent counts in full (wpending, Node's state.length), so the answer is + * false when the pending writes reach the high-water mark, and needDrain + * is set. A write the kernel takes at once answers true whatever its + * size. A write to an ended or closed socket answers false, and so does + * one whose send fails and closes the socket. */ +static bool scr_net_sock_user_write(ScrNetSocket *s, const char *data, size_t len) { + ScrNetSocket *w = s->adopted_by != NULL ? s->adopted_by : s; + bool open = scr_net_sock_write_open(w); + scr_net_sock_write_raw(s, data, len); + if (!open || !scr_net_sock_write_open(w)) return false; + bool ok = w->wpending < SCR_NET_WRITE_HWM; + if (!ok) w->need_drain = true; + return ok; } /* The write path shared by write()/end(data)/pipe delivery. Ignored on a @@ -1108,7 +1231,7 @@ static void scr_net_sock_write_raw(ScrNetSocket *s, const char *data, size_t len (s->tops && !s->t_est)) { /* mid-connect and mid-handshake bytes buffer; the flush after * 'connect'/establishment sends them */ - scr_net_sock_buffer(s, data, len); + scr_net_sock_buffer(s, data, len, len); scr_net_sock_update_write(s); return; } @@ -1127,7 +1250,7 @@ static void scr_net_sock_write_raw(ScrNetSocket *s, const char *data, size_t len } } if ((size_t)sent < len) { - scr_net_sock_buffer(s, data + sent, len - (size_t)sent); + scr_net_sock_buffer(s, data + sent, len - (size_t)sent, len); scr_net_sock_update_write(s); } } @@ -1192,7 +1315,7 @@ void scr_net_sock_writev_native(ScrNetSocket *s, const ScrNetSlice *slices, size skip -= len; continue; } - scr_net_sock_buffer(s, (const char *)iov[i].iov_base + skip, len - skip); + scr_net_sock_buffer(s, (const char *)iov[i].iov_base + skip, len - skip, len); skip = 0; } scr_net_sock_update_write(s); @@ -1219,11 +1342,15 @@ static void scr_net_sock_flush(ScrNetSocket *s) { } if (sent > 0) scr_net_sock_touch(s); s->whead += (size_t)sent; + scr_net_sock_wq_settle(s); } if (s->wlen == s->whead) { - s->whead = s->wlen = 0; + scr_net_sock_wq_clear(s); scr_net_sock_update_write(s); scr_net_sock_maybe_finish_write(s); + /* Node's afterWrite for a write the socket completed later: 'drain' + * now, unless the write half is ending or the socket is gone */ + scr_net_sock_maybe_drain(s); } } @@ -1679,6 +1806,7 @@ ScrNetServer *scr_net_create_server(ScrClosure *handler /*moves, nullable*/, Scr * listen: IPv6 any with dual-stack when the kernel allows, IPv4 fallback. */ static void scr_net_listen_any(ScrNetServer *s, double port, bool reuse_port, ScrClosure *cb /*moves, nullable*/) { + scr_net_server_capture_listen(s); if (cb) scr_net_ls_add(&s->listening_cbs, cb, NULL, true); if (s->listening || s->close_emitted) return; if (!scr_net_poller_init()) { @@ -1757,6 +1885,7 @@ void scr_net_listen(ScrNetServer *s, double port, ScrClosure *cb /*moves, nullab static void scr_net_listen_opts_impl(ScrNetServer *s, double port, ScrStr *host /*borrowed*/, bool ipv6_only, bool reuse_port, ScrClosure *cb /*moves, nullable*/) { + scr_net_server_capture_listen(s); if (host == NULL || host->len == 0) { scr_net_listen_any(s, port, reuse_port, cb); return; @@ -1972,6 +2101,8 @@ void scr_net_server_close_direct(ScrNetServer *s, ScrClosure *cb /*moves, nullab scr_closure_release(cb); return; } + scr_als_ctx_release(s->close_ctx); + s->close_ctx = scr_als_capture(); if (cb) scr_net_ls_add(&s->close_ls, cb, NULL, true); if (s->listening) { scr_net_close_fd_raw(s->fd); /* stops accepting NOW (registrations dropped first) */ @@ -2105,6 +2236,53 @@ void scr_net_server_set_close_override(ScrNetServer *s, ScrClosure *ov /*moves*/ s->close_override = ov; } +void scr_net_server_on_client_error(ScrNetServer *s, ScrClosure *cb /*moves*/, void *fn, bool once) { + if (s->close_emitted) { + scr_closure_release(cb); + return; + } + scr_net_ls_add(&s->client_error_ls, cb, fn, once); +} + +typedef void (*ScrNetClientErrorFn)(ScrClosure *, ScrError *, ScrNetSocket *); + +/* Node emits 'tlsClientError' from the accepted connection's handshake, + * which has no AsyncLocalStorage store, not from the server's listen() + * context the connection's other events run in. */ +void scr_net_server_fire_client_error(ScrNetServer *s, ScrError *err /*borrowed*/, + ScrNetSocket *sock /*borrowed*/) { + if (s->client_error_ls.n == 0) return; + ScrNetL *snap; + size_t n = scr_net_ls_snapshot(&s->client_error_ls, &snap); + ScrAlsCtx *prev = scr_als_dispatch_enter(NULL); + scr_dyn_this_push(s, SCR_DYNH_NET_SERVER); + for (size_t i = 0; i < n; i++) { + if (!scr_exc_pending()) ((ScrNetClientErrorFn)snap[i].fn)(snap[i].cb, err, sock); + scr_closure_release(snap[i].cb); + } + scr_dyn_this_pop(); + scr_als_dispatch_leave(prev); + free(snap); +} + +/* The 'tlsClientError' listener adapters by arity; the arguments arrive + * borrowed and the listener takes them +1. */ +void scr_net_client_error_thunk0(ScrClosure *cb, ScrError *err, ScrNetSocket *sock) { + (void)err; + (void)sock; + ((void (*)(ScrClosure *))cb->fn)(cb); +} + +void scr_net_client_error_thunk1(ScrClosure *cb, ScrError *err, ScrNetSocket *sock) { + (void)sock; + ((void (*)(ScrClosure *, ScrError *))cb->fn)(cb, scr_error_retain(err)); +} + +void scr_net_client_error_thunk2(ScrClosure *cb, ScrError *err, ScrNetSocket *sock) { + ((void (*)(ScrClosure *, ScrError *, ScrNetSocket *))cb->fn)(cb, scr_error_retain(err), + scr_net_sock_retain(sock)); +} + void scr_net_server_on_error(ScrNetServer *s, ScrClosure *cb /*moves*/, ScrChildErrFn fn, bool once) { if (s->close_emitted) { @@ -2229,6 +2407,13 @@ static void scr_net_server_accept(ScrNetServer *srv) { continue; } ScrNetSocket *sock = scr_net_sock_new(); + /* Node runs an accepted socket's own events with no store, except + * under the protocol layer (an HTTP or TLS server), whose connections + * run in the listen() context this accept runs in. */ + if (!srv->native_conn && !srv->defer_conn) { + scr_als_ctx_release(sock->als); + sock->als = NULL; + } sock->fd = fd; sock->server = scr_net_server_retain(srv); srv->nconns++; @@ -2556,6 +2741,24 @@ ScrNetSocket *scr_net_connect_checked(double port, ScrStr *host /*borrowed, null return scr_net_connect(port, host, cb); } +/* net.connect/createConnection(path[, connectListener]) and the `path` + * option from compiled code: a client of the Unix socket at `path` + * (borrowed). For a positional string (`pipe_name_rule`), Node's isPipeName + * applies first: a string that reads as a non-negative number is a port on + * localhost. `cb` (moves, nullable) is the connect listener. */ +ScrNetSocket *scr_net_connect_path_checked(ScrStr *path, bool pipe_name_rule, ScrClosure *cb) { + double port = pipe_name_rule ? scr_string_to_number(path) : -1; + if (port >= 0) { + ScrStr *host = scr_str_new("localhost", 9); + ScrNetSocket *s = scr_net_connect_checked(port, host, cb); + scr_str_release(host); + return s; + } + ScrNetSocket *s = scr_net_connect_path(path, false); + if (cb) scr_net_ls_add(&s->conn_ls, cb, NULL, true); + return s; +} + static bool scr_net_dyn_truthy(const ScrDyn *v) { switch (v->kind) { case SCR_DYN_UNDEF: @@ -2649,7 +2852,7 @@ ScrNetSocket *scr_net_connect_deferred(double port, ScrStr *host /*borrowed, nul * for a slot takes a pooled socket instead of dialing. */ void scr_net_sock_move_writes(ScrNetSocket *from, ScrNetSocket *to) { if (from->wlen > from->whead) scr_net_sock_write_native(to, from->wbuf + from->whead, from->wlen - from->whead); - from->whead = from->wlen = 0; + scr_net_sock_wq_clear(from); } void scr_net_sock_dial_start(ScrNetSocket *s) { @@ -2780,6 +2983,7 @@ void scr_net_server_address_check(ScrNetServer *s) { * deferred 'error', success the deferred 'listening', as for a port. */ void scr_net_listen_path(ScrNetServer *s, ScrStr *path /*borrowed*/, ScrClosure *cb /*moves, nullable*/) { + scr_net_server_capture_listen(s); /* Node's isPipeName: a string that reads as a non-negative number is a port */ double port = scr_string_to_number(path); if (port >= 0) { @@ -3063,16 +3267,42 @@ void scr_net_lookup_answer(ScrClosure *self, bool has_err, ScrStr *msg /*borrowe /* ── the socket surface ──────────────────────────────────────────────── */ -void scr_net_sock_write_str(ScrNetSocket *s, ScrStr *data /*borrowed*/) { +bool scr_net_sock_write_str(ScrNetSocket *s, ScrStr *data /*borrowed*/) { char *owned; const char *bytes = scr_str_utf8(data, &owned); - scr_net_sock_write_raw(s, bytes, data->len); + bool ok = scr_net_sock_user_write(s, bytes, data->len); scr_str_utf8_end(owned); + return ok; } -void scr_net_sock_write_bytes(ScrNetSocket *s, ScrBytes *data /*borrowed*/) { +bool scr_net_sock_write_bytes(ScrNetSocket *s, ScrBytes *data /*borrowed*/) { SCR_BYTES_SNAPSHOT(data); - scr_net_sock_write_raw(s, (const char *)data->data, (data->len * scr_bytes_elem_size(data->elem))); + return scr_net_sock_user_write(s, (const char *)data->data, + (data->len * scr_bytes_elem_size(data->elem))); +} + +/* socket.writableLength: Node's state.length, the length of every write + * not yet completely sent. */ +double scr_net_sock_writable_length(ScrNetSocket *s) { + ScrNetSocket *w = s->adopted_by != NULL ? s->adopted_by : s; + return (double)w->wpending; +} + +/* socket.writableNeedDrain: a write answered false and 'drain' has not + * fired, on a socket whose write half is open (including one still + * waiting to dial). */ +bool scr_net_sock_writable_need_drain(ScrNetSocket *s) { + ScrNetSocket *w = s->adopted_by != NULL ? s->adopted_by : s; + return w->need_drain && scr_net_sock_write_open(w); +} + +/* on('drain') / once('drain'). */ +void scr_net_sock_on_drain(ScrNetSocket *s, ScrClosure *cb /*moves*/, bool once) { + if (s->close_emitted) { + scr_closure_release(cb); + return; + } + scr_net_ls_add(&s->drain_ls, cb, NULL, once); } void scr_net_sock_end(ScrNetSocket *s) { @@ -3082,6 +3312,8 @@ void scr_net_sock_end(ScrNetSocket *s) { } if (s->fd < 0 || s->wr_ending) return; s->wr_ending = true; + scr_als_ctx_release(s->end_ctx); + s->end_ctx = scr_als_capture(); scr_net_sock_maybe_finish_write(s); if (s->rd_eof && s->wr_done) scr_net_sock_close_fd(s); } @@ -3188,6 +3420,62 @@ void scr_net_sock_on_finish(ScrNetSocket *s, ScrClosure *cb /*moves*/) { if (s->wr_done) s->finish_pending = true; /* already finished: next sweep */ } +/* The context the socket's events run in from now on (borrowed): an HTTP + * agent hands a pooled socket to another request. */ +void scr_net_sock_set_context(ScrNetSocket *s, ScrAlsCtx *ctx) { + ScrAlsCtx *next = scr_als_ctx_retain(ctx); + scr_als_ctx_release(s->als); + s->als = next; +} + +/* A write callback runs in the write() call's AsyncLocalStorage context + * (Node's write request): with a store active, the list holds a closure + * that enters it around the callback. caps[0] is the callback, caps[1] + * the context. */ +static void *scr_net_ctx_retain_v(void *c) { return scr_als_ctx_retain(c); } +static void scr_net_ctx_release_v(void *c) { scr_als_ctx_release(c); } + +static void scr_net_ctx_fire0(ScrClosure *self) { + ScrClosure *cb = scr_box_get_ref(self->caps[0]); + ScrAlsCtx *ctx = scr_box_get_ref(self->caps[1]); + ScrAlsCtx *prev = scr_als_dispatch_enter(ctx); + ((void (*)(ScrClosure *))cb->fn)(cb); + scr_als_dispatch_leave(prev); + scr_als_ctx_release(ctx); + scr_closure_release(cb); +} + +static void scr_net_dyn_fire_arg(ScrClosure *cb, const ScrDyn *arg); + +static void scr_net_ctx_fire_arg(ScrClosure *self, const ScrDyn *arg) { + ScrClosure *cb = scr_box_get_ref(self->caps[0]); + ScrAlsCtx *ctx = scr_box_get_ref(self->caps[1]); + ScrAlsCtx *prev = scr_als_dispatch_enter(ctx); + scr_net_dyn_fire_arg(cb, arg); + scr_als_dispatch_leave(prev); + scr_als_ctx_release(ctx); + scr_closure_release(cb); +} + +/* Adds a write callback (moves) with its firing adapter (NULL: a + * zero-argument closure; scr_net_dyn_fire_arg: a dyn listener). */ +static __attribute__((noinline)) ScrClosure *scr_net_bind_write_cb(ScrClosure *cb /*moves*/, void **fn, + ScrAlsCtx *active) { + ScrClosure *bound = scr_closure_new(*fn == NULL ? (void *)scr_net_ctx_fire0 : NULL, 2); + bound->caps[0] = scr_box_new(SCR_BOX_FUNC); + scr_box_set_ref(bound->caps[0], cb); + bound->caps[1] = scr_box_new_obj(scr_net_ctx_retain_v, scr_net_ctx_release_v, NULL); + scr_box_set_ref(bound->caps[1], scr_als_ctx_retain(active)); + if (*fn != NULL) *fn = (void *)&scr_net_ctx_fire_arg; + return bound; +} + +static void scr_net_sock_add_write_cb(ScrNetSocket *s, ScrClosure *cb, void *fn) { + ScrAlsCtx *active = *SCR_ALS_SLOT(); + if (active != NULL) cb = scr_net_bind_write_cb(cb, &fn, active); + scr_net_ls_add(&s->wcb_ls, cb, fn, true); +} + /* write(chunk, callback): fires when the write buffer drains (this * surface's flush moment), from the sweep. */ void scr_net_sock_on_write_flush(ScrNetSocket *s, ScrClosure *cb /*moves*/) { @@ -3195,7 +3483,7 @@ void scr_net_sock_on_write_flush(ScrNetSocket *s, ScrClosure *cb /*moves*/) { scr_closure_release(cb); return; } - scr_net_ls_add(&s->wcb_ls, cb, NULL, true); + scr_net_sock_add_write_cb(s, cb, NULL); } double scr_net_sock_bytes_written(ScrNetSocket *s) { return (double)s->bytes_written; } @@ -4597,8 +4885,8 @@ void scr_net_server_close_dyn(ScrNetServer *s, const ScrDyn *listener /*borrowed void scr_net_sock_on_write_flush_dyn(ScrNetSocket *s, const ScrDyn *listener /*borrowed*/) { if (s->close_emitted) return; - scr_net_ls_add(&s->wcb_ls, scr_dyn_listener_closure_fn(listener, (void *)&scr_net_dyn_fire_arg), - (void *)&scr_net_dyn_fire_arg, true); + scr_net_sock_add_write_cb(s, scr_dyn_listener_closure_fn(listener, (void *)&scr_net_dyn_fire_arg), + (void *)&scr_net_dyn_fire_arg); } /* socket.setTimeout(ms[, callback]) as the program calls it: Node's @@ -5119,7 +5407,7 @@ ScrNetSocket *scr_net_sock_adopt(ScrNetSocket *raw) { tun->send_only = true; t->tunnel = tun; } - raw->whead = raw->wlen = 0; + scr_net_sock_wq_clear(raw); raw->adopted_by = scr_net_sock_retain(t); t->adopted_from = scr_net_sock_retain(raw); scr_net_sock_register(t); @@ -5223,6 +5511,17 @@ void scr_net_sock_transport_error_code(ScrNetSocket *s, const char *msg, const c scr_net_sock_transport_error(s, msg); } +void scr_net_sock_transport_report(ScrNetSocket *s, const char *msg, const char *code) { + if (s->pending_err) return; + s->pending_err = scr_str_new(msg, strlen(msg)); + s->pending_code = code; +} + +void scr_net_sock_transport_ended(ScrNetSocket *s) { + s->rd_eof = true; + scr_net_fire0_this(&s->end_ls, s, SCR_DYNH_NET_SOCKET); +} + void scr_net_sock_transport_error(ScrNetSocket *s, const char *msg) { if (msg != NULL) { if (!s->pending_err) s->pending_err = scr_str_new(msg, strlen(msg)); @@ -5310,6 +5609,7 @@ static void scr_net_server_settle(ScrNetServer *srv) { scr_net_ls_drop(&srv->close_ls); scr_net_ls_drop(&srv->listening_cbs); scr_net_ls_drop(&srv->timeout_ls); + scr_net_ls_drop(&srv->client_error_ls); if (srv->proto_settle && srv->http_ctx) srv->proto_settle(srv->http_ctx); scr_net_server_unregister(srv); } @@ -5355,6 +5655,9 @@ static bool scr_net_sock_settle(ScrNetSocket *sock) { scr_net_fire_arg_this(&sock->close_ls, had_error, sock, SCR_DYNH_NET_SOCKET); scr_dyn_release(had_error); } + /* the transport settles first: a listener its last events add (a TLS + * server's 'tlsClientError') drops with the rest */ + if (sock->tops && sock->tops->settle) sock->tops->settle(sock->tctx); /* settle: listeners drop (cycle story) */ scr_net_ls_drop(&sock->data_ls); scr_net_ls_drop(&sock->end_ls); @@ -5363,11 +5666,11 @@ static bool scr_net_sock_settle(ScrNetSocket *sock) { scr_net_ls_drop(&sock->conn_ls); scr_net_ls_drop(&sock->timeout_ls); scr_net_ls_drop(&sock->readable_ls); + scr_net_ls_drop(&sock->drain_ls); scr_net_ls_drop(&sock->finish_ls); scr_net_ls_drop(&sock->wcb_ls); scr_net_ls_drop(&sock->ready_ls); scr_net_ls_drop(&sock->lookup_ls); - if (sock->tops && sock->tops->settle) sock->tops->settle(sock->tctx); if (sock->pipe_dst) { scr_net_sock_release(sock->pipe_dst); sock->pipe_dst = NULL; @@ -5395,7 +5698,9 @@ static void scr_net_sweep(void) { ((sock->tops && sock->t_est && sock->tops->pending && sock->tops->pending(sock->tctx)) || (!sock->tops && sock->rlen > sock->rhead))) { /* deliver bytes the kernel can't re-signal (see flags_pending) */ + ScrAlsCtx *prev = scr_als_dispatch_enter(sock->als); scr_net_sock_read(sock); + scr_als_dispatch_leave(prev); if (scr_exc_pending()) { scr_net_sock_release(sock); return; @@ -5417,14 +5722,18 @@ static void scr_net_sweep(void) { } if (sock->finish_pending) { sock->finish_pending = false; + ScrAlsCtx *prev = scr_als_dispatch_enter(sock->end_ctx); scr_net_fire0_this(&sock->finish_ls, sock, SCR_DYNH_NET_SOCKET); + scr_als_dispatch_leave(prev); if (scr_exc_pending()) { scr_net_sock_release(sock); return; } } if (sock->lookup_evs != NULL && sock->pending_err) { + ScrAlsCtx *prev = scr_als_dispatch_enter(sock->als); scr_net_lookup_ops->flush(sock); + scr_als_dispatch_leave(prev); if (scr_exc_pending()) { scr_net_sock_release(sock); return; @@ -5438,6 +5747,7 @@ static void scr_net_sweep(void) { * teardown without an event); unconsumed errors fall through to the socket's own * listeners — and the unhandled-'error' exit */ bool consumed = false; + ScrAlsCtx *prev = scr_als_dispatch_enter(sock->als); if (sock->native_err) consumed = sock->native_err(sock->native_ctx, msg); if (!consumed && !scr_exc_pending()) { ScrError *coded = NULL; @@ -5451,6 +5761,7 @@ static void scr_net_sweep(void) { scr_event_error_payload = saved; if (coded != NULL) scr_error_release(coded); } + scr_als_dispatch_leave(prev); sock->pending_code = NULL; scr_str_release(msg); if (scr_exc_pending()) { @@ -5459,7 +5770,10 @@ static void scr_net_sweep(void) { } } if (sock->emit_close && !sock->close_emitted && sock->fd < 0) { - if (!scr_net_sock_settle(sock)) { + ScrAlsCtx *prev = scr_als_dispatch_enter(sock->als); + bool settled = scr_net_sock_settle(sock); + scr_als_dispatch_leave(prev); + if (!settled) { scr_net_sock_release(sock); return; } @@ -5473,7 +5787,9 @@ static void scr_net_sweep(void) { scr_net_server_retain(srv); if (srv->emit_listening) { srv->emit_listening = false; + ScrAlsCtx *prev = scr_als_dispatch_enter(srv->listen_ctx); scr_net_fire0_this(&srv->listening_cbs, srv, SCR_DYNH_NET_SERVER); + scr_als_dispatch_leave(prev); if (scr_exc_pending()) { scr_net_server_release(srv); return; @@ -5482,7 +5798,9 @@ static void scr_net_sweep(void) { if (srv->pending_err) { ScrStr *msg = srv->pending_err; srv->pending_err = NULL; + ScrAlsCtx *prev = scr_als_dispatch_enter(srv->listen_ctx); scr_net_fire_err_this(&srv->err_ls, msg, srv, SCR_DYNH_NET_SERVER); + scr_als_dispatch_leave(prev); scr_str_release(msg); /* A failed listen SETTLES the handle: listeners drop (an error * listener capturing its own server would otherwise cycle forever — @@ -5518,7 +5836,9 @@ static void scr_net_sweep(void) { } if (due == NULL) return; scr_net_server_retain(due); + ScrAlsCtx *prev = scr_als_dispatch_enter(due->close_ctx); scr_net_server_settle(due); + scr_als_dispatch_leave(prev); scr_net_server_release(due); if (scr_exc_pending()) return; } @@ -5552,6 +5872,70 @@ bool scr_loop_io_take(void); bool scr_loop_io_more(void); void scr_loop_io_reopen(void); +/* One poller event of a socket; false with an exception pending. */ +static bool scr_net_sock_event(ScrNetSocket *s, unsigned events) { + if (events & SCRP_TIMER) { + /* the idle period elapsed: 'timeout' fires once (one-shot — the + * next activity re-arms); the socket stays open, like Node */ + s->timeout_armed = false; + if (s->native_timeout) { + s->native_timeout(s->native_ctx); + if (scr_exc_pending()) return false; + } + if (s->server && s->server->http_timeout_surface && + s->native_http_timeout) { + ScrNetServer *srv = scr_net_server_retain(s->server); + bool handled = s->native_http_timeout(s->native_ctx); + if (scr_exc_pending()) { + scr_net_server_release(srv); + return false; + } + bool server_handled = srv->timeout_ls.n > 0; + if (server_handled) { + ScrNetL *snap; + size_t count = scr_net_ls_snapshot(&srv->timeout_ls, &snap); + scr_dyn_this_push(srv, SCR_DYNH_NET_SERVER); + for (size_t j = 0; j < count; j++) { + if (!scr_exc_pending()) ((ScrNetConnFn)snap[j].fn)(snap[j].cb, scr_net_sock_retain(s)); + scr_closure_release(snap[j].cb); + } + scr_dyn_this_pop(); + free(snap); + } + if (!handled && !server_handled && !scr_exc_pending()) + scr_net_sock_destroy(s); /* Node's unhandled server timeout */ + scr_net_server_release(srv); + if (scr_exc_pending()) return false; + } + scr_net_fire0_this(&s->timeout_ls, s, SCR_DYNH_NET_SOCKET); + if (scr_exc_pending()) return false; + if (s->fd < 0) return true; + } + /* kqueue delivers one direction per event (today's exact order); + * epoll may coalesce both into one — writes first (connect + * completion, flushes), then reads, re-checking the fd a write + * failure may have closed. */ + if (events & SCRP_WRITABLE) { + if (s->connecting) scr_net_sock_connect_done(s); + else if (s->tunnel != NULL) scr_net_sock_tunnel_pump(s); + else if (s->tops && !s->t_est) { + s->t_want_write = false; + scr_net_sock_transport_pump(s); + if (!scr_exc_pending() && s->fd >= 0) scr_net_sock_update_write(s); + } else { + s->t_want_write = false; + if (s->tops && s->tops->drain) s->tops->drain(s->tctx); + scr_net_sock_flush(s); + } + } + if ((events & SCRP_READABLE) && !scr_exc_pending() && s->fd >= 0) { + if (s->tunnel != NULL) scr_net_sock_tunnel_pump(s); + else if (s->tops && !s->t_est) scr_net_sock_transport_pump(s); + else scr_net_sock_read(s); + } + return true; +} + static void scr_net_dispatch(void) { if (!scr_net_servers && !scr_net_socks && !scr_net_proto_pending()) { @@ -5577,71 +5961,22 @@ static void scr_net_dispatch(void) { if (!udata) continue; int kind = *(int *)udata; if (kind == SCR_NET_K_SERVER) { - scr_net_server_accept((ScrNetServer *)udata); + /* 'connection' runs in the context of the listen() call. */ + ScrNetServer *srv = (ScrNetServer *)udata; + ScrAlsCtx *prev = scr_als_dispatch_enter(srv->listen_ctx); + scr_net_server_accept(srv); + scr_als_dispatch_leave(prev); } else if (kind == SCR_NET_K_SOCKET) { ScrNetSocket *s = (ScrNetSocket *)udata; /* A callback earlier in this batch may have closed this fd * (destroy inside a data listener) — its remaining events are * stale; the handle itself stays alive until the sweep. */ if (s->fd < 0) continue; - if (evs[i].events & SCRP_TIMER) { - /* the idle period elapsed: 'timeout' fires once (one-shot — the - * next activity re-arms); the socket stays open, like Node */ - s->timeout_armed = false; - if (s->native_timeout) { - s->native_timeout(s->native_ctx); - if (scr_exc_pending()) return; - } - if (s->server && s->server->http_timeout_surface && - s->native_http_timeout) { - ScrNetServer *srv = scr_net_server_retain(s->server); - bool handled = s->native_http_timeout(s->native_ctx); - if (scr_exc_pending()) { - scr_net_server_release(srv); - return; - } - bool server_handled = srv->timeout_ls.n > 0; - if (server_handled) { - ScrNetL *snap; - size_t count = scr_net_ls_snapshot(&srv->timeout_ls, &snap); - scr_dyn_this_push(srv, SCR_DYNH_NET_SERVER); - for (size_t j = 0; j < count; j++) { - if (!scr_exc_pending()) ((ScrNetConnFn)snap[j].fn)(snap[j].cb, scr_net_sock_retain(s)); - scr_closure_release(snap[j].cb); - } - scr_dyn_this_pop(); - free(snap); - } - if (!handled && !server_handled && !scr_exc_pending()) - scr_net_sock_destroy(s); /* Node's unhandled server timeout */ - scr_net_server_release(srv); - if (scr_exc_pending()) return; - } - scr_net_fire0_this(&s->timeout_ls, s, SCR_DYNH_NET_SOCKET); - if (scr_exc_pending()) return; - if (s->fd < 0) continue; - } - /* kqueue delivers one direction per event (today's exact order); - * epoll may coalesce both into one — writes first (connect - * completion, flushes), then reads, re-checking the fd a write - * failure may have closed. */ - if (evs[i].events & SCRP_WRITABLE) { - if (s->connecting) scr_net_sock_connect_done(s); - else if (s->tunnel != NULL) scr_net_sock_tunnel_pump(s); - else if (s->tops && !s->t_est) { - s->t_want_write = false; - scr_net_sock_transport_pump(s); - if (!scr_exc_pending() && s->fd >= 0) scr_net_sock_update_write(s); - } else { - s->t_want_write = false; - scr_net_sock_flush(s); - } - } - if ((evs[i].events & SCRP_READABLE) && !scr_exc_pending() && s->fd >= 0) { - if (s->tunnel != NULL) scr_net_sock_tunnel_pump(s); - else if (s->tops && !s->t_est) scr_net_sock_transport_pump(s); - else scr_net_sock_read(s); - } + /* The socket's events run in its AsyncLocalStorage context. */ + ScrAlsCtx *prev = scr_als_dispatch_enter(s->als); + bool ok = scr_net_sock_event(s, evs[i].events); + scr_als_dispatch_leave(prev); + if (!ok) return; } if (scr_exc_pending()) return; } @@ -5669,6 +6004,7 @@ static void scr_net_cleanup_atexit(void) { scr_net_ls_drop(&s->conn_ls); scr_net_ls_drop(&s->timeout_ls); scr_net_ls_drop(&s->readable_ls); + scr_net_ls_drop(&s->drain_ls); scr_net_ls_drop(&s->lookup_ls); /* Tear down the protocol layer's parser NOW: an in-flight http * exchange (a handler that threw before res.end()) holds req/res, @@ -5793,8 +6129,7 @@ static ScrDyn *scr_net_dynh_sock_invoke(void *h, ScrDyn *self, const char *metho /* fires once when the FIN goes out — once either way */ scr_net_sock_on_finish(s, scr_dyn_listener_closure0(cb)); } else if (scr_net_dynh_name_is(name, "drain")) { - /* This surface never backpressures (write answers true) — an - * accepted, never-fired registration is the consistent answer. */ + scr_net_sock_on_drain(s, scr_dyn_listener_closure0(cb), once); } else if (scr_net_dynh_tls != NULL && name->kind == SCR_DYN_STR && scr_net_dynh_tls->on(s, name->v.str->data, cb, once)) { /* a TLS event name ('secureConnect'), handled by the TLS unit */ @@ -5817,13 +6152,13 @@ static ScrDyn *scr_net_dynh_sock_invoke(void *h, ScrDyn *self, const char *metho scr_dyn_arg_type_fail("chunk", "of type string or an instance of Buffer or Uint8Array", chunk); return NULL; } - if (chunk->kind == SCR_DYN_STR) scr_net_sock_write_str(s, chunk->v.str); - else scr_net_sock_write_bytes(s, chunk->v.bytes); + bool ok = chunk->kind == SCR_DYN_STR ? scr_net_sock_write_str(s, chunk->v.str) + : scr_net_sock_write_bytes(s, chunk->v.bytes); if (argc > 1 && args[1]->kind == SCR_DYN_FUNC) { /* write(chunk, cb): fires when the buffer drains (sweep-deferred) */ scr_net_sock_on_write_flush(s, scr_dyn_listener_closure0(args[1])); } - return scr_dyn_new_bool(true); /* backpressure is not modeled (SEMANTICS.md) */ + return scr_dyn_new_bool(ok); } if (strcmp(method, "end") == 0) { const ScrDyn *chunk = argc > 0 ? args[0] : scr_dyn_undefined(); @@ -6002,10 +6337,12 @@ static ScrDyn *scr_net_dynh_sock_get(void *h, const char *key, size_t key_len) { } } if (strcmp(key, "readable") == 0) return scr_dyn_new_bool(scr_net_sock_readable(s)); + if (strcmp(key, "writableLength") == 0) return scr_dyn_new_num(scr_net_sock_writable_length(s)); + if (strcmp(key, "writableNeedDrain") == 0) + return scr_dyn_new_bool(scr_net_sock_writable_need_drain(s)); if (strcmp(key, "writableHighWaterMark") == 0 || strcmp(key, "readableHighWaterMark") == 0) { - /* Node's default stream highWaterMark — a constant here (backpressure - * is not modeled; SEMANTICS.md) */ - return scr_dyn_new_num(16384); + /* Node's default stream highWaterMark (getDefaultHighWaterMark()) */ + return scr_dyn_new_num(SCR_NET_WRITE_HWM); } if (scr_net_dynh_tls != NULL) { ScrDyn *out = NULL; @@ -6309,10 +6646,11 @@ void scr_net_dyn_install(void) { * STR/BYTES dispatch onto the typed entries; undefined/null end()s plain * (Node accepts them); anything else throws Node's ERR_INVALID_ARG_TYPE * chunk TypeError. Both borrowed. */ -void scr_net_sock_write_dynv(ScrNetSocket *s, const ScrDyn *d) { - if (d->kind == SCR_DYN_STR) scr_net_sock_write_str(s, d->v.str); - else if (d->kind == SCR_DYN_BYTES) scr_net_sock_write_bytes(s, d->v.bytes); - else scr_dyn_arg_type_fail("chunk", "of type string or an instance of Buffer or Uint8Array", d); +bool scr_net_sock_write_dynv(ScrNetSocket *s, const ScrDyn *d) { + if (d->kind == SCR_DYN_STR) return scr_net_sock_write_str(s, d->v.str); + if (d->kind == SCR_DYN_BYTES) return scr_net_sock_write_bytes(s, d->v.bytes); + scr_dyn_arg_type_fail("chunk", "of type string or an instance of Buffer or Uint8Array", d); + return false; } void scr_net_sock_end_dynv(ScrNetSocket *s, const ScrDyn *d) { diff --git a/packages/runtime/src/scr_net.h b/packages/runtime/src/scr_net.h index 6df6945ea..af4b8e1b8 100644 --- a/packages/runtime/src/scr_net.h +++ b/packages/runtime/src/scr_net.h @@ -36,6 +36,10 @@ typedef struct { } ScrNetLs; void scr_net_ls_add(ScrNetLs *l, ScrClosure *cb, void *fn, bool once); +/* The AsyncLocalStorage context a socket's events run in from now on + * (borrowed; scr_net.c keeps the creation context otherwise). */ +struct ScrAlsCtx; +void scr_net_sock_set_context(ScrNetSocket *s, struct ScrAlsCtx *ctx); ScrNetOnce *scr_net_once_new(void); /* +1 */ ScrNetOnce *scr_net_once_retain(ScrNetOnce *once); /* +1 */ void scr_net_once_release(ScrNetOnce *once); @@ -182,6 +186,14 @@ void scr_net_server_close_direct(ScrNetServer *s, ScrClosure *cb /*moves, nullab * zero-arg wrapper); reassignment releases the old one. */ void scr_net_server_set_close_override(ScrNetServer *s, ScrClosure *ov /*moves*/); void scr_net_server_on_error(ScrNetServer *s, ScrClosure *cb /*moves*/, ScrChildErrFn fn, bool once); +/* tls/https servers' 'tlsClientError': registration (fn is one of the + * thunks below, by the listener's arity) and the TLS unit's fire. */ +void scr_net_server_on_client_error(ScrNetServer *s, ScrClosure *cb /*moves*/, void *fn, bool once); +void scr_net_server_fire_client_error(ScrNetServer *s, ScrError *err /*borrowed*/, + ScrNetSocket *sock /*borrowed*/); +void scr_net_client_error_thunk0(ScrClosure *cb, ScrError *err, ScrNetSocket *sock); +void scr_net_client_error_thunk1(ScrClosure *cb, ScrError *err, ScrNetSocket *sock); +void scr_net_client_error_thunk2(ScrClosure *cb, ScrError *err, ScrNetSocket *sock); void scr_net_server_on_close(ScrNetServer *s, ScrClosure *cb /*moves*/, bool once); /* server.on('listening', cb): the deferred 'listening' list listen(port, * cb) rides — fires once per successful bind; late registrations on an @@ -215,15 +227,21 @@ ScrNetSocket *scr_net_connect_lookup(double port, ScrStr *host /*borrowed*/, ScrClosure *lookup /*moves*/, void *answer_fn); /* +1 */ void scr_net_lookup_answer(ScrClosure *self, bool has_err, ScrStr *msg /*borrowed, nullable*/, ScrArr *ips /*borrowed, nullable*/); -void scr_net_sock_write_str(ScrNetSocket *s, ScrStr *data /*borrowed*/); -void scr_net_sock_write_bytes(ScrNetSocket *s, ScrBytes *data /*borrowed*/); +/* write(): Node's Writable answer — false when the bytes the write left + * unsent reach the 65536-byte high-water mark, after which 'drain' fires + * when the write buffer empties; false on an ended or closed socket. */ +bool scr_net_sock_write_str(ScrNetSocket *s, ScrStr *data /*borrowed*/); +bool scr_net_sock_write_bytes(ScrNetSocket *s, ScrBytes *data /*borrowed*/); +double scr_net_sock_writable_length(ScrNetSocket *s); +bool scr_net_sock_writable_need_drain(ScrNetSocket *s); +void scr_net_sock_on_drain(ScrNetSocket *s, ScrClosure *cb /*moves*/, bool once); void scr_net_sock_end(ScrNetSocket *s); void scr_net_sock_end_str(ScrNetSocket *s, ScrStr *data /*borrowed*/); void scr_net_sock_end_bytes(ScrNetSocket *s, ScrBytes *data /*borrowed*/); /* Checked-dynamic chunk forms (an untyped JS payload into a typed * socket): STR/BYTES dispatch; end() takes undefined/null as no chunk; * anything else throws Node's ERR_INVALID_ARG_TYPE chunk TypeError. */ -void scr_net_sock_write_dynv(ScrNetSocket *s, const ScrDyn *d /*borrowed*/); +bool scr_net_sock_write_dynv(ScrNetSocket *s, const ScrDyn *d /*borrowed*/); void scr_net_sock_end_dynv(ScrNetSocket *s, const ScrDyn *d /*borrowed*/); void scr_net_sock_destroy(ScrNetSocket *s); /* Flow control (pause/resume — see the struct's flag comments), the @@ -247,6 +265,10 @@ ScrNetSocket *scr_net_connect_lookup_dyn(double port, ScrStr *host /*borrowed*/, /* A client socket for the Unix socket at `path`, dialed now or (deferred) * by scr_net_sock_dial_start. A failure is the deferred 'error'. */ ScrNetSocket *scr_net_connect_path(ScrStr *path /*borrowed*/, bool deferred); /* +1 */ +/* net.connect(path[, cb]) and connect({ path }): with `pipe_name_rule`, a + * numeric string is a localhost port, as in Node. */ +ScrNetSocket *scr_net_connect_path_checked(ScrStr *path /*borrowed*/, bool pipe_name_rule, + ScrClosure *cb /*moves, nullable*/); /* +1 */ /* server.listen(path[, cb]): a Unix-socket server, its file removed at close. */ void scr_net_listen_path(ScrNetServer *s, ScrStr *path /*borrowed*/, ScrClosure *cb /*moves, nullable*/); void scr_net_sock_dial_start(ScrNetSocket *s); @@ -358,6 +380,9 @@ typedef struct ScrNetTransportOps { /* nullable: runs when the closed socket settles, after its own * listeners drop; the transport drops the listeners it holds */ void (*settle)(void *tctx); + /* nullable: the established socket's fd is writable; the transport + * writes what it holds back (TLS holds its final handshake flight) */ + void (*drain)(void *tctx); } ScrNetTransportOps; void scr_net_sock_set_transport(ScrNetSocket *s, const ScrNetTransportOps *ops, void *tctx); /* new tls.TLSSocket(socket) and tls.connect({ socket }): a fresh socket @@ -402,7 +427,14 @@ void scr_net_sock_replay(ScrNetSocket *s, const char *data, size_t len); /* Transport-level failure: msg (borrowed, Node's message shape) defers * 'error' + teardown to the sweep; NULL tears down without an 'error' * event, Node's default for an unhandled server-side handshake failure. */ +/* The peer ended the stream before the transport's handshake finished: + * 'end' fires now, ahead of the failure the transport reports next. */ +void scr_net_sock_transport_ended(ScrNetSocket *s); void scr_net_sock_transport_error(ScrNetSocket *s, const char *msg); +/* An error that leaves the socket open: 'error' fires from the sweep with + * `code` (static storage), and the socket ends when the peer closes, as + * Node's TLS socket reports a fatal alert after its handshake. */ +void scr_net_sock_transport_report(ScrNetSocket *s, const char *msg, const char *code); /* The same with the error's code given (static storage), for messages * whose text does not name it. */ void scr_net_sock_transport_error_code(ScrNetSocket *s, const char *msg, const char *code); diff --git a/packages/runtime/src/scr_node_bridge.h b/packages/runtime/src/scr_node_bridge.h index d8ee5544b..73643c1bb 100644 --- a/packages/runtime/src/scr_node_bridge.h +++ b/packages/runtime/src/scr_node_bridge.h @@ -17,7 +17,7 @@ #include #include -#define SCR_NODE_BRIDGE_ABI 3u +#define SCR_NODE_BRIDGE_ABI 4u /* The symbol a bridge library exports: `const ScrNodeBridge *(void)`. */ #define SCR_NODE_BRIDGE_ENTRY "scr_node_bridge_v1" @@ -87,6 +87,12 @@ typedef struct ScrNodeBridgeHost { * is calling in from (its main thread's or an async fiber's), for V8's * stack limit; 0 leaves V8's own. */ uintptr_t (*stack_limit)(void); + /* The start (highest address) of the stack the caller runs on when the + * runtime provides it (Node's own stack), 0 for a thread's own stack. + * V8 scans the stack conservatively from the current position up to the + * start it records for the thread, so it must not record the thread's + * when JavaScript runs on the runtime's stack. */ + uintptr_t (*stack_start)(void); } ScrNodeBridgeHost; typedef struct ScrNodeBridge { diff --git a/packages/runtime/src/scr_node_builtin.c b/packages/runtime/src/scr_node_builtin.c index c0031e67b..37efa8822 100644 --- a/packages/runtime/src/scr_node_builtin.c +++ b/packages/runtime/src/scr_node_builtin.c @@ -97,6 +97,10 @@ static SCR_TL const ScrArr *scr_global_registered; static SCR_TL bool scr_global_cleanup_registered; static SCR_TL ScrDyn *(*scr_global_console_get)(void); static SCR_TL ScrDyn *(*scr_global_fetch_get)(void); +/* The program's builtin-global lookup (scr_global_values_install), or + * NULL: a dyn function from a name to the value its static spelling reads, + * undefined for the opaque identity token, or null for a name without one. */ +static SCR_TL ScrDyn *scr_global_values; void scr_global_fetch_install(ScrDyn *(*get)(void)) { scr_global_fetch_get = get; } @@ -200,6 +204,13 @@ static void scr_global_cleanup(void) { static void *scr_global_retain(void *handle) { return handle; } static void scr_global_release(void *handle) { (void)handle; } +/* A dyn string holding `s` (+1); takes over the caller's reference. */ +static ScrDyn *scr_global_take_str(ScrStr *s) { + ScrDyn *value = scr_dyn_new_str(s); + scr_str_release(s); + return value; +} + static ScrDyn *scr_global_get(void *handle, const char *key, size_t length) { (void)handle; ScrDyn *own = scr_global_own ? scr_dyn_obj_get(scr_global_own, key, length) : NULL; @@ -210,10 +221,29 @@ static ScrDyn *scr_global_get(void *handle, const char *key, size_t length) { if (length == 7 && memcmp(key, "console", length) == 0 && scr_global_console_get) return scr_global_console_get(); if (length == 5 && memcmp(key, "fetch", length) == 0 && scr_global_fetch_get) return scr_global_fetch_get(); + if (length == 9 && memcmp(key, "navigator", length) == 0) return scr_navigator_object(); if (length == 9 && memcmp(key, "undefined", length) == 0) return scr_dyn_undefined(); if (length == 3 && memcmp(key, "NaN", length) == 0) return scr_dyn_new_num(NAN); if (length == 8 && memcmp(key, "Infinity", length) == 0) return scr_dyn_new_num(INFINITY); if (!scr_dyn_obj_get(scr_global_known, key, length)) return scr_dyn_undefined(); + if (scr_global_values) { + ScrDyn *name = scr_global_take_str(scr_str_new(key, length)); + ScrDyn *value = scr_global_values->v.fn.thunk(scr_global_values->v.fn.clo, &name, 1); + scr_dyn_release(name); + if (!value) return NULL; + if (value->kind == SCR_DYN_UNDEF) { + /* The opaque identity token a JavaScript source reads for the name. */ + scr_dyn_release(value); + ScrJsonBuf token; + scr_jb_init(&token); + scr_jb_puts(&token, "[builtin "); + for (size_t i = 0; i < length; i++) scr_jb_putc(&token, key[i]); + scr_jb_putc(&token, ']'); + return scr_global_take_str(scr_jb_finish(&token)); + } + if (value->kind != SCR_DYN_NULL) return value; + scr_dyn_release(value); + } ScrJsonBuf text; scr_jb_init(&text); scr_jb_puts(&text, "reading globalThis."); @@ -250,6 +280,47 @@ ScrDyn *scr_global_object(void) { return scr_global_box; } +/* Installs the program's builtin-global lookup (borrowed): reads of + * builtin names through the global object answer what their static + * spellings read. The first installation stands; modules that read the + * global object by computed names each install the same function. */ +static void scr_global_values_cleanup(void) { + if (scr_global_values) scr_dyn_release(scr_global_values); + scr_global_values = NULL; +} + +void scr_global_values_install(ScrDyn *lookup) { + if (scr_global_values || lookup->kind != SCR_DYN_FUNC) return; + scr_global_values = scr_dyn_retain(lookup); + scr_atexit(scr_global_values_cleanup); +} + +/* What `for (key in globalThis)` visits (+1): Node 24's enumerable global + * properties in its order, then the program's own global properties in + * creation order, as keys of a fresh object. */ +ScrDyn *scr_global_for_in_object(void) { + static const char *const node_names[] = { + "global", "clearImmediate", "setImmediate", "clearInterval", "clearTimeout", + "setInterval", "setTimeout", "queueMicrotask", "structuredClone", "atob", + "btoa", "performance", "fetch", "crypto", "navigator", + }; + ScrDyn *object = scr_dyn_new_obj(); + for (size_t i = 0; i < sizeof node_names / sizeof node_names[0]; i++) + scr_dyn_obj_set(object, node_names[i], strlen(node_names[i]), scr_dyn_undefined()); + if (scr_global_own) { + ScrDyn *keys = scr_dyn_obj_keys(scr_global_own); + double count = scr_dyn_arr_len(keys); + for (double i = 0; i < count; i++) { + ScrDyn *key = scr_dyn_arr_at(keys, i); + if (key->kind == SCR_DYN_STR && !scr_dyn_obj_get(object, key->v.str->data, key->v.str->len)) + scr_dyn_obj_set(object, key->v.str->data, key->v.str->len, scr_dyn_undefined()); + scr_dyn_release(key); + } + scr_dyn_release(keys); + } + return object; +} + ScrDyn *scr_global_native_init(ScrArr *known, ScrDyn *(*console_get)(void)) { /* The compiler passes one immortal name table per program: once it is * registered, later reads of the global object only take a reference. */ @@ -388,3 +459,236 @@ ScrDyn *scr_process_builtin_unsupported(ScrStr *id, ScrStr *member) { scr_str_release(message); return NULL; } + +/* ── Node's navigator global (lib/internal/navigator.js) ───────────────── + * hardwareConcurrency is os.availableParallelism(), language the default + * locale as Node's ICU answers it from the environment, languages a frozen + * one-element array of it, platform Node's name for the process platform + * and architecture, and userAgent `Node.js/`. locks has no lowering. + * The members are getters on Navigator.prototype, so the object owns no + * properties: Object.keys answers [] and JSON.stringify `{}`. */ + +static SCR_TL ScrDyn *scr_navigator_box; +static SCR_TL ScrDyn *scr_navigator_langs; +static SCR_TL ScrStr *scr_navigator_lang; +static SCR_TL bool scr_navigator_cleanup_registered; + +static void scr_navigator_cleanup(void); + +/* Registers the release of the cached navigator values at exit, once per + * thread: a program can read navigator.language or navigator.languages + * without ever taking the navigator object. */ +static void scr_navigator_keep(void) { + if (scr_navigator_cleanup_registered) return; + scr_navigator_cleanup_registered = true; + scr_atexit(scr_navigator_cleanup); +} + +static void scr_navigator_refuse(const char *what) { + ScrJsonBuf text; + scr_jb_init(&text); + scr_jb_puts(&text, "'"); + scr_jb_puts(&text, what); + scr_jb_puts(&text, "' is part of the standard library types but has no scriptc lowering yet [SC2020]"); + ScrStr *message = scr_jb_finish(&text); + scr_throw_error_msg_code(SCR_ERR_ERROR, message->data, message->len, "SC2020"); + scr_str_release(message); +} + +static bool scr_navigator_alpha(char c) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); } +static bool scr_navigator_digit(char c) { return c >= '0' && c <= '9'; } +static char scr_navigator_lower(char c) { return c >= 'A' && c <= 'Z' ? (char)(c + 32) : c; } +static char scr_navigator_upper(char c) { return c >= 'a' && c <= 'z' ? (char)(c - 32) : c; } + +/* The BCP 47 tag of the POSIX locale ID `id`, as ICU derives the default + * locale: `language[_TERRITORY][.codeset][@modifier]`, the codeset + * dropped, the C and POSIX locales and en_US_POSIX as en-US, the ISO 639 + * codes ICU replaces, and the modifier as a variant (5 to 8 characters) or + * an lvariant private-use subtag (1 to 4). NULL for an ID outside that + * grammar, whose tag needs ICU's tables. */ +static ScrStr *scr_navigator_tag(const char *id) { + if (!id) return scr_str_new("en-US", 5); + const char *end = id + strlen(id); + const char *at = strchr(id, '@'); + const char *dot = strchr(id, '.'); + const char *base_end = dot && (!at || dot < at) ? dot : at ? at : end; + const char *modifier = at ? at + 1 : NULL; + size_t base_len = (size_t)(base_end - id); + if (base_len == 0 && !modifier) return scr_str_new("und", 3); + if ((base_len == 1 && id[0] == 'C') || (base_len == 5 && memcmp(id, "POSIX", 5) == 0)) { + if (modifier) return NULL; + return scr_str_new("en-US", 5); + } + const char *underscore = memchr(id, '_', base_len); + size_t lang_len = underscore ? (size_t)(underscore - id) : base_len; + const char *region = underscore ? underscore + 1 : NULL; + size_t region_len = region ? (size_t)(base_end - region) : 0; + if (region && memchr(region, '_', region_len)) { + /* en_US_POSIX is ICU's name for the C locale. */ + if (lang_len == 2 && scr_navigator_lower(id[0]) == 'e' && scr_navigator_lower(id[1]) == 'n' && + region_len == 8 && scr_navigator_upper(region[0]) == 'U' && scr_navigator_upper(region[1]) == 'S' && + region[2] == '_') { + bool posix = true; + for (size_t i = 0; i < 5; i++) posix = posix && scr_navigator_upper(region[3 + i]) == "POSIX"[i]; + if (posix && !modifier) return scr_str_new("en-US", 5); + } + return NULL; + } + for (size_t i = 0; i < lang_len; i++) + if (!scr_navigator_alpha(id[i])) return NULL; + if (lang_len == 1 || lang_len == 4 || lang_len > 8) return NULL; + ScrJsonBuf tag; + scr_jb_init(&tag); + char lang[9] = {0}; + for (size_t i = 0; i < lang_len; i++) lang[i] = scr_navigator_lower(id[i]); + static const char *const replaced[][2] = { + {"in", "id"}, {"iw", "he"}, {"ji", "yi"}, {"jw", "jv"}, {"mo", "ro"}, + }; + const char *language = lang_len == 0 ? "und" : lang; + for (size_t i = 0; i < sizeof replaced / sizeof replaced[0]; i++) + if (!strcmp(lang, replaced[i][0])) language = replaced[i][1]; + scr_jb_puts(&tag, language); + if (region_len > 0) { + bool alpha2 = region_len == 2 && scr_navigator_alpha(region[0]) && scr_navigator_alpha(region[1]); + bool digits3 = region_len == 3 && scr_navigator_digit(region[0]) && scr_navigator_digit(region[1]) && + scr_navigator_digit(region[2]); + if (!alpha2 && !digits3) { + scr_str_release(scr_jb_finish(&tag)); + return NULL; + } + scr_jb_putc(&tag, '-'); + for (size_t i = 0; i < region_len; i++) scr_jb_putc(&tag, scr_navigator_upper(region[i])); + } + if (modifier && *modifier) { + size_t length = (size_t)(end - modifier); + bool alnum = length <= 8; + for (size_t i = 0; i < length && alnum; i++) + alnum = scr_navigator_alpha(modifier[i]) || scr_navigator_digit(modifier[i]); + if (!alnum || (length == 7 && !memcmp(modifier, "nynorsk", 7))) { + scr_str_release(scr_jb_finish(&tag)); + return NULL; + } + scr_jb_puts(&tag, length >= 5 ? "-" : "-x-lvariant-"); + for (size_t i = 0; i < length; i++) scr_jb_putc(&tag, scr_navigator_lower(modifier[i])); + } + return scr_jb_finish(&tag); +} + +ScrStr *scr_navigator_language(void) { + if (!scr_navigator_lang) { + /* ICU reads LC_ALL, then LC_MESSAGES, then LANG; a variable that is + * set, even to the empty string, decides. */ + const char *id = getenv("LC_ALL"); + if (!id) id = getenv("LC_MESSAGES"); + if (!id) id = getenv("LANG"); + ScrStr *tag = scr_navigator_tag(id); + if (!tag) { + scr_navigator_refuse("navigator.language for this locale"); + return NULL; + } + scr_navigator_lang = tag; + scr_navigator_keep(); + } + return scr_str_retain(scr_navigator_lang); +} + +ScrDyn *scr_navigator_languages(void) { + if (!scr_navigator_langs) { + ScrStr *language = scr_navigator_language(); + if (!language) return NULL; + ScrDyn *languages = scr_dyn_new_arr(); + scr_dyn_arr_push(languages, scr_global_take_str(language)); + scr_dyn_release(scr_dyn_freeze_data(languages)); + scr_navigator_langs = languages; + scr_navigator_keep(); + } + return scr_dyn_retain(scr_navigator_langs); +} + +ScrStr *scr_navigator_platform(void) { + ScrStr *platform = scr_process_platform(); + ScrStr *arch = scr_process_arch(); + const char *p = platform->data, *a = arch->data; + ScrJsonBuf text; + scr_jb_init(&text); + bool ia32 = !strcmp(a, "ia32"), x64 = !strcmp(a, "x64"); + if (!strcmp(p, "darwin")) scr_jb_puts(&text, "MacIntel"); + else if (!strcmp(p, "win32")) scr_jb_puts(&text, "Win32"); + else if (!strcmp(p, "linux")) { + scr_jb_puts(&text, ia32 ? "Linux i686" : x64 ? "Linux x86_64" : "Linux "); + if (!ia32 && !x64) scr_jb_puts(&text, a); + } else if (!strcmp(p, "freebsd") || !strcmp(p, "openbsd")) { + scr_jb_puts(&text, p[0] == 'f' ? "FreeBSD " : "OpenBSD "); + scr_jb_puts(&text, ia32 ? "i386" : x64 ? "amd64" : a); + } else if (!strcmp(p, "sunos")) { + scr_jb_puts(&text, "SunOS "); + scr_jb_puts(&text, ia32 ? "i86pc" : a); + } else if (!strcmp(p, "aix")) scr_jb_puts(&text, "AIX"); + else { + scr_jb_putc(&text, scr_navigator_upper(p[0])); + if (p[0]) scr_jb_puts(&text, p + 1); + scr_jb_putc(&text, ' '); + scr_jb_puts(&text, a); + } + scr_str_release(platform); + scr_str_release(arch); + return scr_jb_finish(&text); +} + +static ScrDyn *scr_navigator_get(void *handle, const char *key, size_t length) { + (void)handle; + if (length == 19 && !memcmp(key, "hardwareConcurrency", length)) + return scr_dyn_new_num(scr_os_available_parallelism()); + if (length == 8 && !memcmp(key, "language", length)) { + ScrStr *language = scr_navigator_language(); + return language ? scr_global_take_str(language) : NULL; + } + if (length == 9 && !memcmp(key, "languages", length)) return scr_navigator_languages(); + if (length == 8 && !memcmp(key, "platform", length)) return scr_global_take_str(scr_navigator_platform()); + if (length == 9 && !memcmp(key, "userAgent", length)) { + ScrStr *version = scr_process_versions_node(); + const char *dot = memchr(version->data, '.', version->len); + size_t major = dot ? (size_t)(dot - version->data) : version->len; + ScrJsonBuf text; + scr_jb_init(&text); + scr_jb_puts(&text, "Node.js/"); + for (size_t i = 0; i < major; i++) scr_jb_putc(&text, version->data[i]); + scr_str_release(version); + return scr_global_take_str(scr_jb_finish(&text)); + } + if (length == 5 && !memcmp(key, "locks", length)) { + scr_navigator_refuse("navigator.locks"); + return NULL; + } + return NULL; +} + +static ScrDyn *scr_navigator_inspect_view(void *handle) { + (void)handle; + return scr_dyn_new_obj(); +} + +static void scr_navigator_cleanup(void) { + if (scr_navigator_box) scr_dyn_release(scr_navigator_box); + if (scr_navigator_langs) scr_dyn_release(scr_navigator_langs); + if (scr_navigator_lang) scr_str_release(scr_navigator_lang); + scr_navigator_box = NULL; + scr_navigator_langs = NULL; + scr_navigator_lang = NULL; + scr_navigator_cleanup_registered = false; +} + +ScrDyn *scr_navigator_object(void) { + static const ScrDynHandleOps ops = { + .cls = "Navigator", .retain = scr_global_retain, .release = scr_global_release, + .get = scr_navigator_get, .inspect_view = scr_navigator_inspect_view, + .json_empty_object = true, .inspect_view_empty = true, + }; + if (!scr_navigator_box) { + scr_dyn_handle_install(SCR_DYNH_NAVIGATOR, &ops); + static SCR_TL char token; + scr_navigator_box = scr_dyn_new_handle(&token, SCR_DYNH_NAVIGATOR); + scr_navigator_keep(); + } + return scr_dyn_retain(scr_navigator_box); +} diff --git a/packages/runtime/src/scr_node_island.c b/packages/runtime/src/scr_node_island.c index 2504e9c96..18312e819 100644 --- a/packages/runtime/src/scr_node_island.c +++ b/packages/runtime/src/scr_node_island.c @@ -6,13 +6,15 @@ * dlopens the bridge library of a libnode pack (scr_node_bridge.h), which * links libnode, so a run that never calls into Node never maps it. * - * Locating Node. The pack directory is SCRIPTC_LIBNODE when set, else the - * path recorded at build time; a recorded path that starts with `$ORIGIN/` - * resolves against the directory of the running executable (the `beside` - * packaging, and the hook for shipping Node with the program). No search - * path is used. The bridge must speak SCR_NODE_BRIDGE_ABI and link the - * Node version recorded at build time; anything else is a fatal error at - * first use, never a silent use of another Node. + * Locating Node. The pack directory is SCRIPTC_LIBNODE when set (an + * absolute path), else the path recorded at build time; a recorded path + * that starts with `$ORIGIN/` resolves against the directory of the + * running executable (the `beside` packaging, and the hook for shipping + * Node with the program). A process with elevated privileges ignores + * SCRIPTC_LIBNODE and refuses `$ORIGIN/`. No search path is used. The + * bridge must speak SCR_NODE_BRIDGE_ABI and link the Node version recorded + * at build time; anything else is a fatal error at first use, never a + * silent use of another Node. * * Calls. A call serializes its arguments as JSON, enters JavaScript through * the bridge (a callback scope, so ticks and microtasks drain when it @@ -59,10 +61,18 @@ #if !defined(_WIN32) && !defined(__wasi__) #include "scr_node_island_host.h" +#include #include +#include +#include #include +#include #include #include +#include +#if defined(__linux__) +#include +#endif #ifdef __APPLE__ #define NI_BRIDGE_FILE "libscr_node_bridge.dylib" @@ -103,6 +113,15 @@ void scr_node_island_install(const char *libnode, const char *node_version) { SCR_COLD static void ni_oom(void) { scr_trap("scriptc: out of memory\n"); } +/* `len` bytes as a NUL-terminated malloc'd copy. */ +static char *ni_cstr_of(const char *bytes, size_t len) { + char *copy = malloc(len + 1); + if (copy == NULL) ni_oom(); + memcpy(copy, bytes, len); + copy[len] = '\0'; + return copy; +} + SCR_COLD static void ni_fatal(const char *message) { fflush(stdout); fputs(message, stderr); @@ -110,27 +129,600 @@ SCR_COLD static void ni_fatal(const char *message) { _Exit(1); } +/* True when the process runs with elevated privileges (setuid, setgid or + * file capabilities). The pack directory then comes only from an absolute + * path recorded at build time, as the dynamic linker ignores + * LD_LIBRARY_PATH and $ORIGIN for such processes. */ +static bool ni_secure(void) { +#if defined(__APPLE__) + return issetugid() != 0; +#elif defined(__linux__) + return getauxval(AT_SECURE) != 0; +#else + return false; +#endif +} + /* The pack directory: SCRIPTC_LIBNODE, else the recorded path with a - * leading `$ORIGIN/` resolved against the executable's own directory. */ + * leading `$ORIGIN/` resolved against the executable's own directory. + * Both must resolve to absolute paths. */ static const char *ni_pack_dir(void) { - const char *override = getenv("SCRIPTC_LIBNODE"); + bool secure = ni_secure(); + const char *override = secure ? NULL : getenv("SCRIPTC_LIBNODE"); + if (override != NULL && override[0] != '\0' && override[0] != '/') + ni_fatal("scriptc: SCRIPTC_LIBNODE must be an absolute path to a libnode pack directory\n"); const char *dir = override != NULL && override[0] != '\0' ? override : ni_libnode; if (dir == NULL || dir[0] == '\0') ni_fatal("scriptc: this program calls into Node.js (@scriptc/node), but no libnode pack was " "recorded at build time; set SCRIPTC_LIBNODE to a pack directory\n"); static const char origin[] = "$ORIGIN/"; + int written; if (dir != override && strncmp(dir, origin, sizeof origin - 1) == 0) { + if (secure) + ni_fatal("scriptc: a program running with elevated privileges cannot load a libnode pack " + "beside its executable; build it with an absolute SCRIPTC_LIBNODE\n"); ScrStr *self = scr_process_exec_path(); size_t len = self->len; while (len > 0 && self->data[len - 1] != '/') len--; - snprintf(ni_dir, sizeof ni_dir, "%.*s%s", (int)len, self->data, dir + sizeof origin - 1); + if (len == 0 || self->data[0] != '/') { + scr_str_release(self); + ni_fatal("scriptc: cannot locate this program's executable to find its libnode pack\n"); + } + written = snprintf(ni_dir, sizeof ni_dir, "%.*s%s", (int)len, self->data, + dir + sizeof origin - 1); scr_str_release(self); } else { - snprintf(ni_dir, sizeof ni_dir, "%s", dir); + written = snprintf(ni_dir, sizeof ni_dir, "%s", dir); } + if (written < 0 || (size_t)written >= sizeof ni_dir) + ni_fatal("scriptc: the libnode pack path is too long\n"); return ni_dir; } +/* ── the island module tree ───────────────────────────────────────────── */ + +/* The JavaScript the program's calls load (node-islands/modules.ts), named + * by a manifest of relative paths with their sizes and SHA-256 digests, plus + * the links between package directories. Before Node loads anything, the + * tree is made available at one directory, and every file is checked against + * the manifest: + * - beside the executable (`--libnode=beside`), at the recorded `$ORIGIN/` + * path, checked on every start; + * - otherwise from the executable itself, extracted once into + * $XDG_CACHE_HOME/scriptc/node-islands// (or + * ~/.cache/...), checked on every start; when that directory cannot be + * used, into a private directory under $TMPDIR removed at exit. + * Directories are created 0700 and owned by the process's user; no link is + * followed below the cache directory. A process with elevated privileges + * refuses the tree, as it refuses SCRIPTC_LIBNODE. A call names its module + * by its path relative to the tree. */ +static const char *ni_manifest = NULL; +static size_t ni_manifest_len = 0; +static const unsigned char *ni_modules = NULL; +static size_t ni_modules_len = 0; +static const char *ni_modules_location = NULL; +/* The tree's directory at run time; "/" when the calls load only Node's + * built-in modules. */ +static char ni_root[4096] = "/"; + +void scr_node_island_install_modules(const char *manifest, size_t manifest_len, + const unsigned char *data, size_t data_len, + const char *location) { + ni_manifest = manifest; + ni_manifest_len = manifest_len; + ni_modules = data; + ni_modules_len = data_len; + ni_modules_location = location; +} + +/* SHA-256 (FIPS 180-4), for the manifest's digests. */ +typedef struct { + uint32_t h[8]; + uint64_t bytes; + unsigned char tail[64]; + size_t tail_len; +} NiSha256; + +static const uint32_t ni_sha256_k[64] = { + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, + 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, + 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2}; + +static uint32_t ni_rotr(uint32_t x, unsigned n) { return (x >> n) | (x << (32 - n)); } + +static void ni_sha256_block(uint32_t h[8], const unsigned char *p) { + uint32_t w[64]; + for (int i = 0; i < 16; i++) + w[i] = ((uint32_t)p[i * 4] << 24) | ((uint32_t)p[i * 4 + 1] << 16) | + ((uint32_t)p[i * 4 + 2] << 8) | (uint32_t)p[i * 4 + 3]; + for (int i = 16; i < 64; i++) { + uint32_t s0 = ni_rotr(w[i - 15], 7) ^ ni_rotr(w[i - 15], 18) ^ (w[i - 15] >> 3); + uint32_t s1 = ni_rotr(w[i - 2], 17) ^ ni_rotr(w[i - 2], 19) ^ (w[i - 2] >> 10); + w[i] = w[i - 16] + s0 + w[i - 7] + s1; + } + uint32_t a = h[0], b = h[1], c = h[2], d = h[3], e = h[4], f = h[5], g = h[6], k = h[7]; + for (int i = 0; i < 64; i++) { + uint32_t t1 = k + (ni_rotr(e, 6) ^ ni_rotr(e, 11) ^ ni_rotr(e, 25)) + ((e & f) ^ (~e & g)) + + ni_sha256_k[i] + w[i]; + uint32_t t2 = (ni_rotr(a, 2) ^ ni_rotr(a, 13) ^ ni_rotr(a, 22)) + ((a & b) ^ (a & c) ^ (b & c)); + k = g; + g = f; + f = e; + e = d + t1; + d = c; + c = b; + b = a; + a = t1 + t2; + } + h[0] += a; + h[1] += b; + h[2] += c; + h[3] += d; + h[4] += e; + h[5] += f; + h[6] += g; + h[7] += k; +} + +static void ni_sha256_init(NiSha256 *s) { + static const uint32_t initial[8] = {0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, + 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19}; + memcpy(s->h, initial, sizeof initial); + s->bytes = 0; + s->tail_len = 0; +} + +static void ni_sha256_update(NiSha256 *s, const unsigned char *data, size_t len) { + s->bytes += len; + while (len > 0) { + if (s->tail_len == 0 && len >= 64) { + ni_sha256_block(s->h, data); + data += 64; + len -= 64; + continue; + } + size_t take = 64 - s->tail_len < len ? 64 - s->tail_len : len; + memcpy(s->tail + s->tail_len, data, take); + s->tail_len += take; + data += take; + len -= take; + if (s->tail_len == 64) { + ni_sha256_block(s->h, s->tail); + s->tail_len = 0; + } + } +} + +static void ni_sha256_final(NiSha256 *s, unsigned char out[32]) { + uint64_t bits = s->bytes * 8; + unsigned char pad[72] = {0x80}; + size_t padding = (s->tail_len < 56 ? 56 : 120) - s->tail_len; + for (int i = 0; i < 8; i++) pad[padding + i] = (unsigned char)(bits >> (56 - 8 * i)); + ni_sha256_update(s, pad, padding + 8); + for (int i = 0; i < 8; i++) + for (int b = 0; b < 4; b++) out[i * 4 + b] = (unsigned char)(s->h[i] >> (24 - 8 * b)); +} + +typedef struct { + char *path; + size_t size; + unsigned char sha256[32]; + size_t offset; /* into ni_modules */ +} NiFile; + +typedef struct { + char *path, *target; +} NiLink; + +static NiFile *ni_files = NULL; +static size_t ni_nfiles = 0; +static NiLink *ni_links = NULL; +static size_t ni_nlinks = 0; + +SCR_COLD static void ni_bad_manifest(void) { + ni_fatal("scriptc: this program's Node island module manifest is malformed\n"); +} + +static char *ni_dup_str(const ScrDyn *d) { + if (d == NULL || d->kind != SCR_DYN_STR) ni_bad_manifest(); + if (memchr(d->v.str->data, '\0', d->v.str->len) != NULL) ni_bad_manifest(); + return ni_cstr_of(d->v.str->data, d->v.str->len); +} + +/* A relative path inside the tree: no leading `/`, no empty, `.` or `..` + * component. */ +static bool ni_relative_inside(const char *path) { + if (path[0] == '\0' || path[0] == '/') return false; + for (const char *p = path;;) { + const char *slash = strchr(p, '/'); + size_t n = slash != NULL ? (size_t)(slash - p) : strlen(p); + if (n == 0 || (n == 1 && p[0] == '.') || (n == 2 && p[0] == '.' && p[1] == '.')) return false; + if (slash == NULL) return true; + p = slash + 1; + } +} + +/* A link target that, read from the link's directory, stays inside the + * tree. */ +static bool ni_link_inside(const char *link, const char *target) { + if (target[0] == '\0' || target[0] == '/') return false; + long depth = 0; + for (const char *p = link; (p = strchr(p, '/')) != NULL; p++) depth++; + for (const char *p = target;;) { + const char *slash = strchr(p, '/'); + size_t n = slash != NULL ? (size_t)(slash - p) : strlen(p); + if (n == 2 && p[0] == '.' && p[1] == '.') { + if (--depth < 0) return false; + } else if (n == 0 || !(n == 1 && p[0] == '.')) { + if (n == 0) return false; + depth++; + } + if (slash == NULL) return true; + p = slash + 1; + } +} + +static int ni_hex(char c) { + return c >= '0' && c <= '9' ? c - '0' : c >= 'a' && c <= 'f' ? c - 'a' + 10 : -1; +} + +static void ni_parse_manifest(void) { + ScrStr *text = scr_str_new(ni_manifest, ni_manifest_len); + ScrDyn *body = scr_json_parse(text); + scr_str_release(text); + if (body == NULL || body->kind != SCR_DYN_OBJ) ni_bad_manifest(); + ScrDyn *files = scr_dyn_obj_get(body, "files", 5); + ScrDyn *links = scr_dyn_obj_get(body, "links", 5); + if (files == NULL || files->kind != SCR_DYN_ARR || links == NULL || links->kind != SCR_DYN_ARR) + ni_bad_manifest(); + ni_nfiles = (size_t)scr_dyn_arr_len(files); + ni_nlinks = (size_t)scr_dyn_arr_len(links); + ni_files = calloc(ni_nfiles + 1, sizeof *ni_files); + ni_links = calloc(ni_nlinks + 1, sizeof *ni_links); + if (ni_files == NULL || ni_links == NULL) ni_oom(); + size_t offset = 0; + for (size_t i = 0; i < ni_nfiles; i++) { + ScrDyn *entry = scr_dyn_arr_at(files, (double)i); + if (entry->kind != SCR_DYN_ARR || scr_dyn_arr_len(entry) != 3) ni_bad_manifest(); + ScrDyn *path = scr_dyn_arr_at(entry, 0), *size = scr_dyn_arr_at(entry, 1), + *digest = scr_dyn_arr_at(entry, 2); + NiFile *f = &ni_files[i]; + f->path = ni_dup_str(path); + if (!ni_relative_inside(f->path) || size->kind != SCR_DYN_NUM || size->v.num < 0 || + digest->kind != SCR_DYN_STR || digest->v.str->len != 64) + ni_bad_manifest(); + f->size = (size_t)size->v.num; + for (int b = 0; b < 32; b++) { + int hi = ni_hex(digest->v.str->data[b * 2]), lo = ni_hex(digest->v.str->data[b * 2 + 1]); + if (hi < 0 || lo < 0) ni_bad_manifest(); + f->sha256[b] = (unsigned char)(hi * 16 + lo); + } + f->offset = offset; + offset += f->size; + scr_dyn_release(path); + scr_dyn_release(size); + scr_dyn_release(digest); + scr_dyn_release(entry); + } + if (ni_modules != NULL && offset != ni_modules_len) ni_bad_manifest(); + for (size_t i = 0; i < ni_nlinks; i++) { + ScrDyn *entry = scr_dyn_arr_at(links, (double)i); + if (entry->kind != SCR_DYN_ARR || scr_dyn_arr_len(entry) != 2) ni_bad_manifest(); + ScrDyn *path = scr_dyn_arr_at(entry, 0), *target = scr_dyn_arr_at(entry, 1); + ni_links[i].path = ni_dup_str(path); + ni_links[i].target = ni_dup_str(target); + if (!ni_relative_inside(ni_links[i].path) || + !ni_link_inside(ni_links[i].path, ni_links[i].target)) + ni_bad_manifest(); + scr_dyn_release(path); + scr_dyn_release(target); + scr_dyn_release(entry); + } + scr_dyn_release(body); +} + +/* The directory `name` in `dirfd`, never through a link; created 0700 when + * `create` and absent. -1 when it cannot be opened. */ +static int ni_dir_at(int dirfd, const char *name, bool create) { + if (create && mkdirat(dirfd, name, 0700) != 0 && errno != EEXIST) return -1; + return openat(dirfd, name, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); +} + +/* The parent directory of the relative `path` below `rootfd` (its last + * component is left in *base), opened component by component without + * following links; created when `create`. -1 when it cannot be opened. */ +static int ni_parent_at(int rootfd, const char *path, bool create, const char **base) { + int fd = dup(rootfd); + const char *p = path; + for (const char *slash; fd >= 0 && (slash = strchr(p, '/')) != NULL; p = slash + 1) { + char component[256]; + size_t n = (size_t)(slash - p); + if (n >= sizeof component) { + close(fd); + return -1; + } + memcpy(component, p, n); + component[n] = '\0'; + int next = ni_dir_at(fd, component, create); + close(fd); + fd = next; + } + *base = p; + return fd; +} + +/* A directory this process may trust: owned by its user, writable by no + * one else. */ +static bool ni_private_dir(int fd) { + struct stat st; + return fd >= 0 && fstat(fd, &st) == 0 && S_ISDIR(st.st_mode) && st.st_uid == geteuid() && + (st.st_mode & 022) == 0; +} + +static bool ni_digest_matches(const unsigned char *data, size_t len, + const unsigned char expected[32]) { + NiSha256 s; + unsigned char got[32]; + ni_sha256_init(&s); + ni_sha256_update(&s, data, len); + ni_sha256_final(&s, got); + return memcmp(got, expected, 32) == 0; +} + +/* Whether the tree below `rootfd` holds exactly the manifest's files (by + * size and digest) and links. */ +static bool ni_tree_matches(int rootfd) { + for (size_t i = 0; i < ni_nfiles; i++) { + const char *base; + int dirfd = ni_parent_at(rootfd, ni_files[i].path, false, &base); + if (dirfd < 0) return false; + int fd = openat(dirfd, base, O_RDONLY | O_NOFOLLOW | O_CLOEXEC); + close(dirfd); + struct stat st; + if (fd < 0 || fstat(fd, &st) != 0 || !S_ISREG(st.st_mode) || + (size_t)st.st_size != ni_files[i].size) { + if (fd >= 0) close(fd); + return false; + } + NiSha256 s; + ni_sha256_init(&s); + unsigned char buffer[65536]; + size_t total = 0; + for (;;) { + ssize_t n = read(fd, buffer, sizeof buffer); + if (n < 0 && errno == EINTR) continue; + if (n <= 0) break; + ni_sha256_update(&s, buffer, (size_t)n); + total += (size_t)n; + } + close(fd); + unsigned char got[32]; + ni_sha256_final(&s, got); + if (total != ni_files[i].size || memcmp(got, ni_files[i].sha256, 32) != 0) return false; + } + for (size_t i = 0; i < ni_nlinks; i++) { + const char *base; + int dirfd = ni_parent_at(rootfd, ni_links[i].path, false, &base); + if (dirfd < 0) return false; + char target[4096]; + ssize_t n = readlinkat(dirfd, base, target, sizeof target - 1); + close(dirfd); + if (n < 0) return false; + target[n] = '\0'; + if (strcmp(target, ni_links[i].target) != 0) return false; + } + return true; +} + +/* Writes the executable's tree below `rootfd` (an empty directory). */ +static bool ni_extract(int rootfd) { + for (size_t i = 0; i < ni_nfiles; i++) { + const unsigned char *data = ni_modules + ni_files[i].offset; + if (!ni_digest_matches(data, ni_files[i].size, ni_files[i].sha256)) return false; + const char *base; + int dirfd = ni_parent_at(rootfd, ni_files[i].path, true, &base); + if (dirfd < 0) return false; + int fd = openat(dirfd, base, O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, 0600); + close(dirfd); + if (fd < 0) return false; + size_t done = 0; + while (done < ni_files[i].size) { + ssize_t n = write(fd, data + done, ni_files[i].size - done); + if (n < 0 && errno == EINTR) continue; + if (n <= 0) break; + done += (size_t)n; + } + if (close(fd) != 0 || done != ni_files[i].size) return false; + } + for (size_t i = 0; i < ni_nlinks; i++) { + const char *base; + int dirfd = ni_parent_at(rootfd, ni_links[i].path, true, &base); + if (dirfd < 0) return false; + int status = symlinkat(ni_links[i].target, dirfd, base); + close(dirfd); + if (status != 0) return false; + } + return true; +} + +/* Removes everything below `dirfd` and closes it, never following links. */ +static void ni_remove_below(int dirfd) { + DIR *dir = fdopendir(dirfd); + if (dir == NULL) { + close(dirfd); + return; + } + for (struct dirent *e; (e = readdir(dir)) != NULL;) { + if (strcmp(e->d_name, ".") == 0 || strcmp(e->d_name, "..") == 0) continue; + int sub = openat(dirfd, e->d_name, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); + if (sub >= 0) { + ni_remove_below(sub); + (void)unlinkat(dirfd, e->d_name, AT_REMOVEDIR); + } else { + (void)unlinkat(dirfd, e->d_name, 0); + } + } + closedir(dir); +} + + +static char ni_temp_root[4096] = ""; + +static void ni_remove_temp_root(void) { + int fd = open(ni_temp_root, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); + if (fd >= 0) ni_remove_below(fd); + (void)rmdir(ni_temp_root); +} + +static bool ni_set_root(const char *a, const char *b, const char *c) { + int n = snprintf(ni_root, sizeof ni_root, "%s%s%s", a, b, c); + return n > 0 && (size_t)n < sizeof ni_root; +} + +/* The tree in the cache directory, extracted there when absent. */ +static bool ni_cached_root(const char *digest) { + const char *xdg = getenv("XDG_CACHE_HOME"); + const char *home = getenv("HOME"); + char base[4096]; + int n; + if (xdg != NULL && xdg[0] == '/') + n = snprintf(base, sizeof base, "%s", xdg); + else if (home != NULL && home[0] == '/') + n = snprintf(base, sizeof base, "%s/.cache", home); + else + return false; + if (n <= 0 || (size_t)n >= sizeof base) return false; + (void)mkdir(base, 0700); + int basefd = open(base, O_RDONLY | O_DIRECTORY | O_CLOEXEC); + if (basefd < 0) return false; + int scriptc = ni_dir_at(basefd, "scriptc", true); + close(basefd); + int islands = ni_private_dir(scriptc) ? ni_dir_at(scriptc, "node-islands", true) : -1; + if (scriptc >= 0) close(scriptc); + if (!ni_private_dir(islands)) { + if (islands >= 0) close(islands); + return false; + } + bool ok = false; + int tree = ni_dir_at(islands, digest, false); + if (tree < 0 && errno == ENOENT) { + /* Extract into a fresh sibling, then move it into place. */ + char staging[128]; + snprintf(staging, sizeof staging, "%s.%ld.tmp", digest, (long)getpid()); + int fd = ni_dir_at(islands, staging, true); + if (ni_private_dir(fd) && ni_extract(fd) && ni_tree_matches(fd) && + renameat(islands, staging, islands, digest) == 0) { + close(fd); + tree = ni_dir_at(islands, digest, false); + } else { + if (fd >= 0) ni_remove_below(fd); + (void)unlinkat(islands, staging, AT_REMOVEDIR); + tree = ni_dir_at(islands, digest, false); /* another process may have placed it */ + } + } + if (ni_private_dir(tree) && ni_tree_matches(tree)) + ok = ni_set_root(base, "/scriptc/node-islands/", digest); + if (tree >= 0) close(tree); + close(islands); + return ok; +} + +/* The tree in a private directory of this run, removed at exit. */ +static bool ni_temporary_root(void) { + const char *tmp = getenv("TMPDIR"); + if (tmp == NULL || tmp[0] != '/') tmp = "/tmp"; + int n = snprintf(ni_temp_root, sizeof ni_temp_root, "%s/scriptc-node-islands-XXXXXX", tmp); + if (n <= 0 || (size_t)n >= sizeof ni_temp_root || mkdtemp(ni_temp_root) == NULL) return false; + scr_atexit(ni_remove_temp_root); + int fd = open(ni_temp_root, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); + bool ok = ni_private_dir(fd) && ni_extract(fd) && ni_tree_matches(fd); + if (fd >= 0) close(fd); + return ok && ni_set_root(ni_temp_root, "", ""); +} + +/* Makes the tree available and sets ni_root; fatal when it cannot. */ +static void ni_open_modules(void) { + if (ni_manifest == NULL) return; /* only built-in modules */ + if (ni_secure()) + ni_fatal("scriptc: a program running with elevated privileges cannot load Node island " + "modules\n"); + ni_parse_manifest(); + if (ni_modules_location != NULL && ni_modules_location[0] != '\0') { + static const char origin[] = "$ORIGIN/"; + if (strncmp(ni_modules_location, origin, sizeof origin - 1) != 0) ni_bad_manifest(); + ScrStr *self = scr_process_exec_path(); + size_t len = self->len; + while (len > 0 && self->data[len - 1] != '/') len--; + bool absolute = len > 0 && self->data[0] == '/'; + char dir[4096]; + int n = snprintf(dir, sizeof dir, "%.*s", (int)len, self->data); + scr_str_release(self); + if (!absolute || n <= 0 || (size_t)n >= sizeof dir || + !ni_set_root(dir, ni_modules_location + sizeof origin - 1, "")) + ni_fatal("scriptc: cannot locate this program's Node island modules\n"); + int fd = open(ni_root, O_RDONLY | O_DIRECTORY | O_CLOEXEC); + bool ok = fd >= 0 && ni_tree_matches(fd); + if (fd >= 0) close(fd); + if (!ok) { + char message[8192]; + snprintf(message, sizeof message, + "scriptc: the Node island modules at %s do not match this program\n", ni_root); + ni_fatal(message); + } + return; + } + NiSha256 s; + unsigned char digest[32]; + ni_sha256_init(&s); + ni_sha256_update(&s, (const unsigned char *)ni_manifest, ni_manifest_len); + ni_sha256_final(&s, digest); + char hex[65]; + for (int i = 0; i < 32; i++) snprintf(hex + i * 2, 3, "%02x", digest[i]); + if (ni_modules == NULL && ni_nfiles > 0) ni_bad_manifest(); + if (!ni_cached_root(hex) && !ni_temporary_root()) + ni_fatal("scriptc: cannot write this program's Node island modules to a private directory\n"); +} + +/* The calling module's path inside the tree: `/`. */ +static char *ni_caller_path(const ScrStr *relative) { + size_t root = strlen(ni_root); + bool slash = root > 0 && ni_root[root - 1] == '/'; + char *path = malloc(root + 1 + relative->len + 1); + if (path == NULL) ni_oom(); + memcpy(path, ni_root, root); + if (!slash) path[root++] = '/'; + memcpy(path + root, relative->data, relative->len); + path[root + relative->len] = '\0'; + return path; +} + +/* The bootstrap: the tree root for the host's resolution guard, then the + * host script. +1 malloc'd. */ +static char *ni_bootstrap(void) { + size_t host = strlen(scr_node_island_host_js); + size_t cap = 64 + strlen(ni_root) * 6 + host; + char *out = malloc(cap); + if (out == NULL) ni_oom(); + size_t n = (size_t)snprintf(out, cap, "globalThis.__scriptcNodeIslandRoot = \""); + for (const char *p = ni_root; *p != '\0'; p++) { + unsigned char c = (unsigned char)*p; + if (c == '"' || c == '\\') + n += (size_t)snprintf(out + n, cap - n, "\\%c", c); + else if (c < 0x20) + n += (size_t)snprintf(out + n, cap - n, "\\u%04x", c); + else + out[n++] = (char)c; + } + n += (size_t)snprintf(out + n, cap - n, "\";\n"); + memcpy(out + n, scr_node_island_host_js, host + 1); + return out; +} + /* ── outcomes ─────────────────────────────────────────────────────────── */ /* The result of an "R" outcome: its JSON copy, or undefined. +1, or NULL @@ -214,6 +806,9 @@ static void ni_listen(uint32_t event, const char *name, uint32_t listener, int a } } +/* The start of Node's stack for callers on it (V8's stack-scan bound). */ +static uintptr_t ni_stack_start(void); + /* The main thread's stack, for V8's stack limit (ni_stack_limit). */ static uintptr_t ni_main_lo = 0, ni_main_hi = 0; @@ -265,6 +860,13 @@ static void ni_trampoline(void) { } } +static uintptr_t ni_stack_start(void) { + uintptr_t sp = (uintptr_t)__builtin_frame_address(0); + return ni_stack != NULL && sp >= (uintptr_t)ni_stack && sp < (uintptr_t)ni_stack + NI_STACK + ? (uintptr_t)ni_stack + NI_STACK + : 0; +} + /* Runs job(arg) on Node's stack (directly when already there). */ static void ni_on_node_stack(void (*job)(void *), void *arg) { if (ni_on_stack) { @@ -358,6 +960,7 @@ static const ScrNodeBridgeHost ni_host = { &scr_loop_hosted_microtasks_done, &ni_listen, &ni_stack_limit, + &ni_stack_start, }; void scr_node_island_emit(uint32_t event, uint32_t listener, ScrStr *name, int arg) { @@ -410,8 +1013,11 @@ static void ni_start(void) { * time Node takes to load is invisible to their order. */ double since = scr_now_ms(); const char *dir = ni_pack_dir(); + ni_open_modules(); char path[4352]; - snprintf(path, sizeof path, "%s/%s", dir, NI_BRIDGE_FILE); + int written = snprintf(path, sizeof path, "%s/%s", dir, NI_BRIDGE_FILE); + if (written < 0 || (size_t)written >= sizeof path) + ni_fatal("scriptc: the libnode pack path is too long\n"); /* RTLD_GLOBAL: native addons resolve napi_* and node::* from the global * namespace. The executable exports no symbols libnode could bind to. */ void *library = dlopen(path, RTLD_NOW | RTLD_GLOBAL); @@ -449,8 +1055,10 @@ static void ni_start(void) { fflush(stdout); ni_bridge = bridge; ni_main_stack(); - NiStart start = {scr_node_island_host_js, argv0, NULL}; + char *bootstrap = ni_bootstrap(); + NiStart start = {bootstrap, argv0, NULL}; ni_on_node_stack(ni_start_job, &start); + free(bootstrap); char *error = start.error; if (error != NULL) { char message[8192]; @@ -482,17 +1090,11 @@ static ScrDyn *ni_args_json(ScrDyn *args) { } /* A borrowed ScrStr as a NUL-terminated copy for the bridge. */ -static char *ni_cstr(const ScrStr *s) { - char *copy = malloc(s->len + 1); - if (copy == NULL) ni_oom(); - memcpy(copy, s->data, s->len); - copy[s->len] = '\0'; - return copy; -} +static char *ni_cstr(const ScrStr *s) { return ni_cstr_of(s->data, s->len); } static char *ni_call(uint32_t id, bool sync, ScrStr *file, ScrStr *specifier, ScrStr *name, ScrDyn *json, size_t *len) { - char *f = ni_cstr(file), *s = ni_cstr(specifier), *n = ni_cstr(name); + char *f = ni_caller_path(file), *s = ni_cstr(specifier), *n = ni_cstr(name); NiCall call = {id, sync ? 1 : 0, f, s, n, json->v.str->data, json->v.str->len, 0, NULL}; ni_on_node_stack(ni_call_job, &call); char *outcome = call.outcome; @@ -503,7 +1105,23 @@ static char *ni_call(uint32_t id, bool sync, ScrStr *file, ScrStr *specifier, Sc return outcome; } +/* Node runs on the main thread only: its isolate is entered under the + * Locker the starting thread holds, and the loader's state and Node's + * stack are process-wide. A call from a worker thread throws an Error. */ +static bool ni_main_thread(void) { + if (scr_context_is_main()) return true; + ScrStr *code = scr_str_new("", 0); + static const char text[] = + "Node islands run on the main thread only; this call came from a worker thread"; + ScrStr *message = scr_str_new(text, sizeof text - 1); + scr_throw_node_coded(0, code, message); + scr_str_release(message); + scr_str_release(code); + return false; +} + ScrDyn *scr_node_island_call_sync(ScrStr *file, ScrStr *specifier, ScrStr *name, ScrDyn *args) { + if (!ni_main_thread()) return NULL; ScrDyn *json = ni_args_json(args); if (json == NULL) return NULL; ni_start(); @@ -518,6 +1136,10 @@ ScrDyn *scr_node_island_call_sync(ScrStr *file, ScrStr *specifier, ScrStr *name, ScrPromise *scr_node_island_call(ScrStr *file, ScrStr *specifier, ScrStr *name, ScrDyn *args) { ScrPromise *p = scr_promise_new(); + if (!ni_main_thread()) { + scr_promise_reject_pending(p); + return p; + } ScrDyn *json = ni_args_json(args); if (json == NULL) { scr_promise_reject_pending(p); @@ -559,6 +1181,16 @@ void scr_node_island_install(const char *libnode, const char *node_version) { (void)node_version; } +void scr_node_island_install_modules(const char *manifest, size_t manifest_len, + const unsigned char *data, size_t data_len, + const char *location) { + (void)manifest; + (void)manifest_len; + (void)data; + (void)data_len; + (void)location; +} + SCR_COLD static void ni_unsupported(void) { fputs("scriptc: Node islands are not supported on this target\n", stderr); fflush(stderr); diff --git a/packages/runtime/src/scr_node_island.h b/packages/runtime/src/scr_node_island.h index 961737b0e..6c6dddf22 100644 --- a/packages/runtime/src/scr_node_island.h +++ b/packages/runtime/src/scr_node_island.h @@ -14,6 +14,15 @@ * before %main. */ void scr_node_island_install(const char *libnode, const char *node_version); +/* Records the island module tree (node-islands/modules.ts): its manifest, + * and its files concatenated in manifest order when the executable carries + * them (NULL when they ship beside it at `location`, a `$ORIGIN/` path; "" + * otherwise). All static. The emitted main calls it after + * scr_node_island_install. */ +void scr_node_island_install_modules(const char *manifest, size_t manifest_len, + const unsigned char *data, size_t data_len, + const char *location); + /* @scriptc/node callSync(specifier, name, ...args): `file` is the calling * module's path (relative specifiers resolve from it), `args` the boxed * argument array, copied as JSON. Returns the JSON copy of the result, +1, diff --git a/packages/runtime/src/scr_node_island_host.cjs b/packages/runtime/src/scr_node_island_host.cjs index ad04b351d..c46bb7583 100644 --- a/packages/runtime/src/scr_node_island_host.cjs +++ b/packages/runtime/src/scr_node_island_host.cjs @@ -14,12 +14,38 @@ * them in-process under Node: both must produce the same results, errors * and messages. Outcomes are "R" (returned; "R" alone is undefined) * or "T" (threw), the encoding of scr_node_bridge.h. */ -const { createRequire } = require("node:module"); +const { createRequire, registerHooks } = require("node:module"); const { constants: osConstants } = require("node:os"); const timers = require("node:timers"); const nativeHost = globalThis.__scriptcNodeIslandHost; +// The program's island module tree (scr_node_island.c prefixes this script +// with its root, then the bridge removes the global): modules resolve only +// to files inside it, never to a file elsewhere on the machine. +const islandRoot = globalThis.__scriptcNodeIslandRoot; +delete globalThis.__scriptcNodeIslandRoot; +if (typeof islandRoot === "string" && islandRoot !== "/") { + const { realpathSync } = require("node:fs"); + const { sep } = require("node:path"); + const { fileURLToPath } = require("node:url"); + const root = realpathSync(islandRoot); + const inside = root.endsWith(sep) ? root : root + sep; + registerHooks({ + resolve(specifier, context, nextResolve) { + const result = nextResolve(specifier, context); + if (result.url.startsWith("file:") && !fileURLToPath(result.url).startsWith(inside)) { + const error = new Error( + `Cannot find module '${specifier}': it is not one of the modules this program ships`, + ); + error.code = "MODULE_NOT_FOUND"; + throw error; + } + return result; + }, + }); +} + // Error classes that cross as copies, at their scr_error.h kind index. const ERROR_KINDS = [ [TypeError, 1], @@ -77,8 +103,18 @@ function errorKind(error) { } // A thrown value crosses as a copy: an Error as its nearest intrinsic -// class with its message and string code, anything else as JSON data. +// class with its message and string code, anything else as JSON data. An +// error whose class, message or code cannot be read crosses as an Error +// with an empty message. function thrown(error) { + try { + return thrownCopy(error); + } catch { + return 'T{"error":{"kind":0,"message":""}}'; + } +} + +function thrownCopy(error) { if (error instanceof Error) { const copy = { kind: errorKind(error), message: String(error.message) }; if (typeof error.code === "string") copy.code = error.code; @@ -283,6 +319,23 @@ for (const stream of [process.stdout, process.stderr]) { stream._handle.setBlocking(true); } +// process.execPath is this program, not a node executable: a fork without +// an execPath of its own would start the program again. +const childProcess = require("node:child_process"); +const nodeFork = childProcess.fork; +childProcess.fork = function fork(modulePath, args, options) { + const forkOptions = Array.isArray(args) ? options : args; + const execPath = + forkOptions === null || typeof forkOptions !== "object" ? undefined : forkOptions.execPath; + if (execPath === undefined || execPath === process.execPath) { + throw new Error( + "child_process.fork() in a Node island needs options.execPath (a node executable): process.execPath is this program", + ); + } + return Reflect.apply(nodeFork, this, arguments); +}; +require("node:module").syncBuiltinESMExports(); + globalThis.__scriptcNodeIsland = { call, setTimer, diff --git a/packages/runtime/src/scr_node_island_host.h b/packages/runtime/src/scr_node_island_host.h index 6a8bbb4b6..73262a8ce 100644 --- a/packages/runtime/src/scr_node_island_host.h +++ b/packages/runtime/src/scr_node_island_host.h @@ -20,12 +20,38 @@ static const char scr_node_island_host_js[] = " * them in-process under Node: both must produce the same results, errors\012" " * and messages. Outcomes are \"R\" (returned; \"R\" alone is undefined)\012" " * or \"T\" (threw), the encoding of scr_node_bridge.h. */\012" - "const { createRequire } = require(\"node:module\");\012" + "const { createRequire, registerHooks } = require(\"node:module\");\012" "const { constants: osConstants } = require(\"node:os\");\012" "const timers = require(\"node:timers\");\012" "\012" "const nativeHost = globalThis.__scriptcNodeIslandHost;\012" "\012" + "// The program's island module tree (scr_node_island.c prefixes this script\012" + "// with its root, then the bridge removes the global): modules resolve only\012" + "// to files inside it, never to a file elsewhere on the machine.\012" + "const islandRoot = globalThis.__scriptcNodeIslandRoot;\012" + "delete globalThis.__scriptcNodeIslandRoot;\012" + "if (typeof islandRoot === \"string\" && islandRoot !== \"/\") {\012" + " const { realpathSync } = require(\"node:fs\");\012" + " const { sep } = require(\"node:path\");\012" + " const { fileURLToPath } = require(\"node:url\");\012" + " const root = realpathSync(islandRoot);\012" + " const inside = root.endsWith(sep) \077 root : root + sep;\012" + " registerHooks({\012" + " resolve(specifier, context, nextResolve) {\012" + " const result = nextResolve(specifier, context);\012" + " if (result.url.startsWith(\"file:\") && !fileURLToPath(result.url).startsWith(inside)) {\012" + " const error = new Error(\012" + " `Cannot find module '${specifier}': it is not one of the modules this program ships`,\012" + " );\012" + " error.code = \"MODULE_NOT_FOUND\";\012" + " throw error;\012" + " }\012" + " return result;\012" + " },\012" + " });\012" + "}\012" + "\012" "// Error classes that cross as copies, at their scr_error.h kind index.\012" "const ERROR_KINDS = [\012" " [TypeError, 1],\012" @@ -83,8 +109,18 @@ static const char scr_node_island_host_js[] = "}\012" "\012" "// A thrown value crosses as a copy: an Error as its nearest intrinsic\012" - "// class with its message and string code, anything else as JSON data.\012" + "// class with its message and string code, anything else as JSON data. An\012" + "// error whose class, message or code cannot be read crosses as an Error\012" + "// with an empty message.\012" "function thrown(error) {\012" + " try {\012" + " return thrownCopy(error);\012" + " } catch {\012" + " return 'T{\"error\":{\"kind\":0,\"message\":\"\"}}';\012" + " }\012" + "}\012" + "\012" + "function thrownCopy(error) {\012" " if (error instanceof Error) {\012" " const copy = { kind: errorKind(error), message: String(error.message) };\012" " if (typeof error.code === \"string\") copy.code = error.code;\012" @@ -289,6 +325,23 @@ static const char scr_node_island_host_js[] = " stream._handle.setBlocking(true);\012" "}\012" "\012" + "// process.execPath is this program, not a node executable: a fork without\012" + "// an execPath of its own would start the program again.\012" + "const childProcess = require(\"node:child_process\");\012" + "const nodeFork = childProcess.fork;\012" + "childProcess.fork = function fork(modulePath, args, options) {\012" + " const forkOptions = Array.isArray(args) \077 options : args;\012" + " const execPath =\012" + " forkOptions === null || typeof forkOptions !== \"object\" \077 undefined : forkOptions.execPath;\012" + " if (execPath === undefined || execPath === process.execPath) {\012" + " throw new Error(\012" + " \"child_process.fork() in a Node island needs options.execPath (a node executable): process.execPath is this program\",\012" + " );\012" + " }\012" + " return Reflect.apply(nodeFork, this, arguments);\012" + "};\012" + "require(\"node:module\").syncBuiltinESMExports();\012" + "\012" "globalThis.__scriptcNodeIsland = {\012" " call,\012" " setTimer,\012" diff --git a/packages/runtime/src/scr_number.h b/packages/runtime/src/scr_number.h index 78d19614b..39564c061 100644 --- a/packages/runtime/src/scr_number.h +++ b/packages/runtime/src/scr_number.h @@ -111,6 +111,35 @@ double scr_date_now(void); /* integer ms since epoch, like Node */ extern SCR_TL const double *scr_date_mock_now; double scr_date_new_ms(double ms); /* TimeClip (NaN when invalid) */ double scr_date_get_time(double ms); +/* Date.prototype's setters (setMilliseconds ... setFullYear and their UTC + * forms) over a time value: the new time value for `argc` arguments + * `a`..`d` (scr_lib.c). `setter` is an SCR_DATE_SET_* code; it and `argc` + * are whole numbers passed as doubles, as the compiler's lib calls pass + * numbers. */ +enum { + SCR_DATE_SET_MILLISECONDS = 0, + SCR_DATE_SET_SECONDS = 1, + SCR_DATE_SET_MINUTES = 2, + SCR_DATE_SET_HOURS = 3, + SCR_DATE_SET_DATE = 4, + SCR_DATE_SET_MONTH = 5, + SCR_DATE_SET_FULL_YEAR = 6, + SCR_DATE_SET_UTC = 8, +}; +double scr_date_set_fields(double t, double setter, double argc, + double a, double b, double c, double d); +/* A Date object: the TimeClip'd time value in milliseconds (NaN for an + * invalid date). Dates are immutable; their identity is the pointer. + * Retain/release follow the runtime's RC conventions (SIZE_MAX immortal). */ +typedef struct ScrDate { + size_t rc; + double ms; +} ScrDate; +ScrDate *scr_date_new(double ms); /* +1 */ +ScrDate *scr_date_retain(ScrDate *date); +void scr_date_release(ScrDate *date); +void *scr_date_retain_v(void *p); +void scr_date_release_v(void *p); /* Node's exact ISO 8601 UTC format (expanded ±YYYYYY years outside * 0–9999). THROWS Node's "Invalid time value" RangeError on NaN or * |ms| > 8.64e15 and returns NULL; +1 otherwise. */ @@ -150,6 +179,11 @@ double scr_date_get_timezone_offset(double ms); ScrStr *scr_date_to_string(double ms); ScrStr *scr_date_to_date_string(double ms); ScrStr *scr_date_to_time_string(double ms); +/* String() of a Date object (ScrDate *), and Array.prototype.join over an + * array of Date objects (holes empty). +1. */ +ScrStr *scr_date_object_to_string(void *date); +struct ScrArr; +ScrStr *scr_date_arr_join(struct ScrArr *dates, const ScrStr *sep); /* The long time zone name Node prints after the GMT offset for the host * zone at time value `ms`; `isdst` is tm_isdst at `ms`, or -1 when not * known. Borrowed; valid until the next call on the same thread. */ diff --git a/packages/runtime/src/scr_object.c b/packages/runtime/src/scr_object.c index 7cafbeca3..2f47a1c7b 100644 --- a/packages/runtime/src/scr_object.c +++ b/packages/runtime/src/scr_object.c @@ -33,6 +33,8 @@ ScrClassObj *scr_classobj_new(const ScrClassObj *template, size_t ncaps) { c->length = template->length; c->prototype_data = NULL; c->static_data = NULL; + c->base_name = template->base_name; + c->own_static_fields = template->own_static_fields; scr_obj_alloc_note(); return c; } diff --git a/packages/runtime/src/scr_os.c b/packages/runtime/src/scr_os.c index 8fa68738d..8bc476045 100644 --- a/packages/runtime/src/scr_os.c +++ b/packages/runtime/src/scr_os.c @@ -1701,3 +1701,33 @@ ScrDyn *scr_fs_constants_object(void) { scr_atexit(scr_fs_constants_clear); return scr_dyn_retain(o); } + +/* util.convertProcessSignalToExitCode(signalCode): 128 plus the number of + * the os.constants.signals entry, and validateOneOf's + * ERR_INVALID_ARG_VALUE, listing every name, for anything else. NaN with + * the exception pending. */ +double scr_util_signal_exit_code(const ScrDyn *code) { + ScrDyn *signals = scr_dyn_new_obj(); + scr_os_signal_constants(signals); + const ScrDyn *number = + code->kind == SCR_DYN_STR ? scr_dyn_obj_get(signals, code->v.str->data, code->v.str->len) : NULL; + double result = NAN; + if (number && number->kind == SCR_DYN_NUM) { + result = 128 + number->v.num; + } else { + ScrJsonBuf reason; + scr_jb_init(&reason); + scr_jb_puts(&reason, "must be one of: "); + for (size_t i = 0; i < signals->v.obj.len; i++) { + if (i > 0) scr_jb_puts(&reason, ", "); + scr_jb_putc(&reason, '\''); + scr_jb_write(&reason, signals->v.obj.entries[i].key, signals->v.obj.entries[i].key_len); + scr_jb_putc(&reason, '\''); + } + ScrStr *text = scr_jb_finish(&reason); + scr_dyn_arg_value_fail("signalCode", text->data, code); + scr_str_release(text); + } + scr_dyn_release(signals); + return result; +} diff --git a/packages/runtime/src/scr_os.h b/packages/runtime/src/scr_os.h index 57f219ad1..41bee5a8f 100644 --- a/packages/runtime/src/scr_os.h +++ b/packages/runtime/src/scr_os.h @@ -23,6 +23,9 @@ ScrStr *scr_os_hostname(void); ScrStr *scr_os_endianness(void); double scr_os_freemem(void); double scr_os_uptime(void); +/* util.convertProcessSignalToExitCode: 128 plus the signal number, or NaN + * with ERR_INVALID_ARG_VALUE pending (scr_os.c). */ +double scr_util_signal_exit_code(const struct ScrDyn *code); ScrArr *scr_os_loadavg(void); /* os.getPriority(pid): validateInt32, then the scheduling priority; * failures throw Node's RangeError or SystemError (ERR_SYSTEM_ERROR). */ diff --git a/packages/runtime/src/scr_path.h b/packages/runtime/src/scr_path.h index 9600b5e95..de5e85491 100644 --- a/packages/runtime/src/scr_path.h +++ b/packages/runtime/src/scr_path.h @@ -1,5 +1,6 @@ -/* scriptc runtime — node:path (scr_path.c) and path globs (scr_glob.c). - * Included by scr_runtime.h, which says where new declarations go. */ +/* scriptc runtime — node:path (scr_path.c), path globs (scr_glob.c), and + * module resolution at run time (scr_require.c). Included by scr_runtime.h, + * which says where new declarations go. */ #ifndef SCR_PATH_H #define SCR_PATH_H @@ -89,4 +90,18 @@ void scr_fs_glob_callback(ScrDyn *args); ScrArr *scr_fsp_glob_strings(const ScrDyn *pattern, const ScrDyn *options); ScrDyn *scr_fsp_glob_error(void); +/* createRequire's require(id) and require.resolve(request, options) with a + * specifier computed at run time (scr_require.c). Both resolve with Node + * 24's CommonJS algorithm from `importer`, the requiring file; `builtins` + * lists the builtin ids (process.getBuiltinModule's table), and `site` is + * the call's "file:line" for the SC2020 refusal. All arguments are + * borrowed. scr_require_computed always leaves an exception pending and + * returns NULL: Node's argument errors, or MODULE_NOT_FOUND when nothing + * resolves; a specifier that resolves ends the program with the refusal. + * scr_require_resolve answers the resolved id or real path (+1), or NULL + * with Node's error pending. */ +ScrDyn *scr_require_computed(ScrDyn *id, ScrStr *importer, ScrStr *site, ScrArr *builtins); +ScrStr *scr_require_resolve(ScrDyn *request, ScrDyn *options, ScrStr *importer, ScrStr *site, + ScrArr *builtins); + #endif /* SCR_PATH_H */ diff --git a/packages/runtime/src/scr_pki.c b/packages/runtime/src/scr_pki.c index a1a5e97c2..240595220 100644 --- a/packages/runtime/src/scr_pki.c +++ b/packages/runtime/src/scr_pki.c @@ -1,14 +1,17 @@ /* node:crypto asymmetric keys and signatures over the vendored mbedTLS * public-key layer: createPrivateKey, createPublicKey, generateKeyPairSync - * and generateKeyPair for "rsa", "ec" and "ed25519" keys, the - * PrivateKeyObject and PublicKeyObject handles (type, asymmetricKeyType, + * and generateKeyPair for "rsa", "rsa-pss", "ec", "ed25519" and "x25519" + * keys, crypto.diffieHellman (ECDH and X25519), the PrivateKeyObject and + * PublicKeyObject handles (type, asymmetricKeyType, * asymmetricKeyDetails, export(), equals()), crypto.sign and crypto.verify, * and the Sign and Verify objects of createSign and createVerify. Keys read * and write PEM and DER as PKCS#1, SEC1, PKCS#8 and SubjectPublicKeyInfo, - * and JWK; the EC curves are prime256v1, secp384r1, secp521r1 and - * secp256k1, and the Ed25519 arithmetic is scr_ed25519.c. Argument checks + * JWK, and the raw-public and raw-private encodings; the EC curves are + * prime256v1, secp384r1, secp521r1 and secp256k1, and the Ed25519 + * arithmetic is scr_ed25519.c. Argument checks * and errors follow Node 24's lib/internal/crypto/keys.js, keygen.js and - * sig.js. Encrypted keys and other key types throw a coded SC2020 error. + * sig.js. Private keys export and import encrypted as PBES2 PKCS#8 and as + * traditional PEM. Other key types throw a coded SC2020 error. * All arguments are borrowed. */ #include "scr_runtime.h" @@ -21,17 +24,21 @@ #include "mbedtls/asn1.h" #include "mbedtls/asn1write.h" #include "mbedtls/base64.h" +#include "mbedtls/cipher.h" #include "mbedtls/bignum.h" +#include "mbedtls/ecdh.h" #include "mbedtls/ecdsa.h" #include "mbedtls/ecp.h" #include "mbedtls/md.h" #include "mbedtls/oid.h" #include "mbedtls/pk.h" +#include "mbedtls/pkcs5.h" #include "mbedtls/platform_util.h" #include "mbedtls/rsa.h" #include "mbedtls/x509_crt.h" #include "scr_pki.h" +#include "scr_cipher_names.h" /* ── errors ───────────────────────────────────────────────────────────── */ @@ -39,6 +46,30 @@ static void sp_throw(int kind, const char *code, const char *message) { scr_throw_error_msg_code(kind, message, strlen(message), code); } +/* An OpenSSL error as Node's ThrowCryptoError reports it, + * "error::::", with own library, reason and code + * properties in Node's key order. A crypto job's callback gets the error + * without them (`props` false), as from Node's async jobs. */ +static void sp_throw_openssl(const char *packed, const char *library, const char *reason, const char *code, + bool props) { + char message[160]; + snprintf(message, sizeof message, "error:%s:%s::%s", packed, library, reason); + ScrStr *text = scr_str_new(message, strlen(message)); + ScrError *e = scr_error_new(SCR_ERR_ERROR, text); + scr_str_release(text); + if (props) { + const char *keys[] = {"library", "reason", "code"}; + const char *values[] = {library, reason, code}; + for (size_t i = 0; i < 3; i++) { + if (i == 2) scr_error_set_code(e, code); + ScrStr *value = scr_str_new(values[i], strlen(values[i])); + scr_error_set_prop(e, keys[i], strlen(keys[i]), scr_dyn_new_str(value)); + scr_str_release(value); + } + } + scr_throw_obj(e, &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg()); +} + void scr_pki_throw_fmt(int kind, const char *code, const char *fmt, ...) { va_list ap, sized; va_start(ap, fmt); @@ -156,8 +187,17 @@ typedef struct SpKey { mbedtls_pk_context pk; /* RSA and EC keys */ bool private_key; bool ed25519; /* an Ed25519 key: ed_seed (private keys) and ed_pub */ + bool x25519; /* an X25519 key, in the same two fields: the private key's + * bytes as given, before clamping, and the public key */ uint8_t ed_seed[32]; uint8_t ed_pub[32]; + /* An RSA-PSS key (id-RSASSA-PSS): signatures only, with PSS padding. + * With RSASSA-PSS-params it signs only with pss_md and MGF1 over + * pss_mgf1, and salts of at least pss_salt bytes. */ + bool rsa_pss; + bool pss_params; + mbedtls_md_type_t pss_md, pss_mgf1; + int pss_salt; } SpKey; static void *sp_key_retain(void *h) { @@ -214,6 +254,64 @@ static bool sp_is_ec(const SpKey *k) { return mbedtls_pk_get_type(&k->pk) == MBE static bool sp_is_ed(const SpKey *k) { return k->ed25519; } +static bool sp_is_x(const SpKey *k) { return k->x25519; } + +/* The PSS fields of `from` on `to`. */ +static void sp_pss_copy(SpKey *to, const SpKey *from) { + to->rsa_pss = from->rsa_pss; + to->pss_params = from->pss_params; + to->pss_md = from->pss_md; + to->pss_mgf1 = from->pss_mgf1; + to->pss_salt = from->pss_salt; +} + +/* Node's name for a digest RSASSA-PSS-params can name. */ +static const char *sp_pss_md_name(mbedtls_md_type_t md) { + switch (md) { + case MBEDTLS_MD_SHA1: return "sha1"; + case MBEDTLS_MD_SHA224: return "sha224"; + case MBEDTLS_MD_SHA256: return "sha256"; + case MBEDTLS_MD_SHA384: return "sha384"; + case MBEDTLS_MD_SHA512: return "sha512"; + default: return NULL; + } +} + +/* An Ed25519 or X25519 key: 32-byte private and public keys, no details. */ +static bool sp_is_okp(const SpKey *k) { return k->ed25519 || k->x25519; } + +/* X25519(scalar, point) per RFC 7748 over mbedTLS's Curve25519 group, or + * of the base point when point is NULL. False when the result is the + * all-zero value (a small-order point), which OpenSSL refuses. */ +static bool sp_x25519(const uint8_t scalar[32], const uint8_t *point, uint8_t out[32]) { + mbedtls_ecp_group grp; + mbedtls_ecp_point P, R; + mbedtls_mpi d; + mbedtls_ecp_group_init(&grp); + mbedtls_ecp_point_init(&P); + mbedtls_ecp_point_init(&R); + mbedtls_mpi_init(&d); + uint8_t k[32]; + memcpy(k, scalar, 32); + k[0] &= 248; + k[31] &= 127; + k[31] |= 64; + size_t n = 0; + bool ok = mbedtls_ecp_group_load(&grp, MBEDTLS_ECP_DP_CURVE25519) == 0 && + mbedtls_mpi_read_binary_le(&d, k, 32) == 0 && + (point ? mbedtls_ecp_point_read_binary(&grp, &P, point, 32) : mbedtls_ecp_copy(&P, &grp.G)) == 0 && + mbedtls_ecp_mul(&grp, &R, &d, &P, sp_random, NULL) == 0 && + mbedtls_ecp_point_write_binary(&grp, &R, MBEDTLS_ECP_PF_UNCOMPRESSED, &n, out, 32) == 0 && n == 32; + uint8_t any = 0; + for (size_t i = 0; ok && i < 32; i++) any |= out[i]; + mbedtls_platform_zeroize(k, sizeof k); + mbedtls_mpi_free(&d); + mbedtls_ecp_point_free(&R); + mbedtls_ecp_point_free(&P); + mbedtls_ecp_group_free(&grp); + return ok && any != 0; +} + typedef struct { const char *name; /* OpenSSL short name (namedCurve) */ const char *nist; /* EC_curve_nist2nid alias, or NULL */ @@ -240,7 +338,7 @@ static const SpCurve *sp_curve_of(const SpKey *k) { /* A parsed key is usable when it is RSA or EC on a curve above. */ static bool sp_key_supported(const SpKey *k) { - return sp_is_ed(k) || sp_is_rsa(k) || (sp_is_ec(k) && sp_curve_of(k) != NULL); + return sp_is_okp(k) || sp_is_rsa(k) || (sp_is_ec(k) && sp_curve_of(k) != NULL); } /* ── DER writing ──────────────────────────────────────────────────────── */ @@ -259,6 +357,11 @@ static const uint8_t sp_ed_pkcs8_prefix[16] = {0x30, 0x2e, 0x02, 0x01, 0x00, 0x3 0x03, 0x2b, 0x65, 0x70, 0x04, 0x22, 0x04, 0x20}; static const uint8_t sp_ed_spki_prefix[12] = {0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x03, 0x21, 0x00}; +/* X25519's, which differ in the OID's last byte (1.3.101.110). */ +static const uint8_t sp_x_pkcs8_prefix[16] = {0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, + 0x03, 0x2b, 0x65, 0x6e, 0x04, 0x22, 0x04, 0x20}; +static const uint8_t sp_x_spki_prefix[12] = {0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, + 0x2b, 0x65, 0x6e, 0x03, 0x21, 0x00}; /* Writes prefix || 32 bytes at the end of buf, as the mbedtls writers do. */ static int sp_write_ed_der(const uint8_t *prefix, size_t prefix_len, const uint8_t key[32], @@ -270,9 +373,102 @@ static int sp_write_ed_der(const uint8_t *prefix, size_t prefix_len, const uint8 return (int)n; } +/* A digest AlgorithmIdentifier with NULL parameters, written backwards + * at *c; the written length or a negative error. */ +static int sp_write_md_alg(unsigned char **c, unsigned char *start, mbedtls_md_type_t md) { + const char *oid; + size_t oid_len; + int r, len = 0; + if ((r = mbedtls_oid_get_oid_by_md(md, &oid, &oid_len)) != 0) return r; + if ((r = mbedtls_asn1_write_null(c, start)) < 0) return r; + len += r; + if ((r = mbedtls_asn1_write_oid(c, start, oid, oid_len)) < 0) return r; + len += r; + if ((r = mbedtls_asn1_write_len(c, start, (size_t)len)) < 0) return r; + len += r; + if ((r = mbedtls_asn1_write_tag(c, start, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE)) < 0) return r; + return len + r; +} + +/* An RSA-PSS key's AlgorithmIdentifier, as OpenSSL writes it: id-RSASSA-PSS + * with no parameters for a key without restrictions, or RSASSA-PSS-params + * omitting the fields at their defaults (SHA-1, MGF1 with SHA-1, salt 20). */ +static int sp_write_pss_alg(unsigned char **c, unsigned char *start, const SpKey *k) { + int r, len = 0; + if (k->pss_params) { + int params = 0; + if (k->pss_salt != 20) { + int field = 0; + if ((r = mbedtls_asn1_write_int(c, start, k->pss_salt)) < 0) return r; + field += r; + if ((r = mbedtls_asn1_write_len(c, start, (size_t)field)) < 0) return r; + field += r; + if ((r = mbedtls_asn1_write_tag(c, start, MBEDTLS_ASN1_CONTEXT_SPECIFIC | MBEDTLS_ASN1_CONSTRUCTED | 2)) < 0) + return r; + params += field + r; + } + if (k->pss_mgf1 != MBEDTLS_MD_SHA1) { + int field = 0; + if ((r = sp_write_md_alg(c, start, k->pss_mgf1)) < 0) return r; + field += r; + if ((r = mbedtls_asn1_write_oid(c, start, MBEDTLS_OID_MGF1, MBEDTLS_OID_SIZE(MBEDTLS_OID_MGF1))) < 0) + return r; + field += r; + if ((r = mbedtls_asn1_write_len(c, start, (size_t)field)) < 0) return r; + field += r; + if ((r = mbedtls_asn1_write_tag(c, start, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE)) < 0) return r; + field += r; + if ((r = mbedtls_asn1_write_len(c, start, (size_t)field)) < 0) return r; + field += r; + if ((r = mbedtls_asn1_write_tag(c, start, MBEDTLS_ASN1_CONTEXT_SPECIFIC | MBEDTLS_ASN1_CONSTRUCTED | 1)) < 0) + return r; + params += field + r; + } + if (k->pss_md != MBEDTLS_MD_SHA1) { + int field = 0; + if ((r = sp_write_md_alg(c, start, k->pss_md)) < 0) return r; + field += r; + if ((r = mbedtls_asn1_write_len(c, start, (size_t)field)) < 0) return r; + field += r; + if ((r = mbedtls_asn1_write_tag(c, start, MBEDTLS_ASN1_CONTEXT_SPECIFIC | MBEDTLS_ASN1_CONSTRUCTED | 0)) < 0) + return r; + params += field + r; + } + if ((r = mbedtls_asn1_write_len(c, start, (size_t)params)) < 0) return r; + params += r; + if ((r = mbedtls_asn1_write_tag(c, start, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE)) < 0) return r; + len += params + r; + } + if ((r = mbedtls_asn1_write_oid(c, start, MBEDTLS_OID_RSASSA_PSS, MBEDTLS_OID_SIZE(MBEDTLS_OID_RSASSA_PSS))) < 0) + return r; + len += r; + if ((r = mbedtls_asn1_write_len(c, start, (size_t)len)) < 0) return r; + len += r; + if ((r = mbedtls_asn1_write_tag(c, start, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE)) < 0) return r; + return len + r; +} + +static int sp_write_pkcs1_public_der(SpKey *k, unsigned char *buf, size_t size); + static int sp_write_spki_der(SpKey *k, unsigned char *buf, size_t size) { - if (sp_is_ed(k)) return sp_write_ed_der(sp_ed_spki_prefix, sizeof sp_ed_spki_prefix, k->ed_pub, buf, size); - return mbedtls_pk_write_pubkey_der(&k->pk, buf, size); + if (sp_is_okp(k)) + return sp_write_ed_der(sp_is_x(k) ? sp_x_spki_prefix : sp_ed_spki_prefix, 12, k->ed_pub, buf, size); + if (!k->rsa_pss) return mbedtls_pk_write_pubkey_der(&k->pk, buf, size); + /* SubjectPublicKeyInfo ::= SEQUENCE { AlgorithmIdentifier, BIT STRING + * RSAPublicKey } */ + unsigned char inner[SP_DER_MAX]; + int n = sp_write_pkcs1_public_der(k, inner, sizeof inner); + if (n < 0) return n; + unsigned char *c = buf + size; + int r, len = 0; + if ((r = mbedtls_asn1_write_bitstring(&c, buf, inner + sizeof inner - n, (size_t)n * 8)) < 0) return r; + len += r; + if ((r = sp_write_pss_alg(&c, buf, k)) < 0) return r; + len += r; + if ((r = mbedtls_asn1_write_len(&c, buf, (size_t)len)) < 0) return r; + len += r; + if ((r = mbedtls_asn1_write_tag(&c, buf, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE)) < 0) return r; + return len + r; } /* RSAPublicKey (PKCS#1) DER. */ @@ -286,8 +482,8 @@ static int sp_write_pkcs1_public_der(SpKey *k, unsigned char *buf, size_t size) * parameters, or id-ecPublicKey with the curve OID), and the inner key, * which for EC omits the parameters field. */ static int sp_write_pkcs8_der(SpKey *k, unsigned char *buf, size_t size) { - if (sp_is_ed(k)) - return sp_write_ed_der(sp_ed_pkcs8_prefix, sizeof sp_ed_pkcs8_prefix, k->ed_seed, buf, size); + if (sp_is_okp(k)) + return sp_write_ed_der(sp_is_x(k) ? sp_x_pkcs8_prefix : sp_ed_pkcs8_prefix, 16, k->ed_seed, buf, size); unsigned char inner[SP_DER_MAX]; size_t inner_len; if (sp_is_rsa(k)) { @@ -342,6 +538,11 @@ static int sp_write_pkcs8_der(SpKey *k, unsigned char *buf, size_t size) { const char *oid; size_t oid_len; size_t alg_len = 0; + if (k->rsa_pss) { + if ((r = sp_write_pss_alg(&c, buf, k)) < 0) goto done; + len += (size_t)r; + goto version; + } if (sp_is_rsa(k)) { if ((r = mbedtls_asn1_write_null(&c, buf)) < 0) goto done; alg_len += (size_t)r; @@ -367,6 +568,7 @@ static int sp_write_pkcs8_der(SpKey *k, unsigned char *buf, size_t size) { alg_len += (size_t)r; len += alg_len; } +version: if ((r = mbedtls_asn1_write_int(&c, buf, 0)) < 0) goto done; len += (size_t)r; if ((r = mbedtls_asn1_write_len(&c, buf, len)) < 0) goto done; @@ -404,6 +606,622 @@ static ScrStr *sp_pem(const char *label, const unsigned char *der, size_t len) { return s; } +/* ── encrypted private keys ─────────────────────────────────────────── + * PKCS#8 EncryptedPrivateKeyInfo as OpenSSL 3 writes it (PBES2 with + * PBKDF2-HMAC-SHA256 over a 16-byte salt and 2048 iterations), and the + * traditional PEM encryption of PKCS#1 and SEC1 keys (Proc-Type and + * DEK-Info headers, the key from EVP_BytesToKey with MD5), for the ciphers + * below. Reading takes PBES2 with any HMAC-SHA PRF and these ciphers, and + * traditional PEM with the CBC ciphers through mbedTLS. */ + +typedef struct { + const char *name; /* OpenSSL's name */ + const char *pem; /* its DEK-Info name */ + mbedtls_cipher_type_t type; + const char *oid; + size_t oid_len, key_len, iv_len; + bool ecb; + /* What OpenSSL does when PKCS#8 or traditional PEM encryption takes + * the cipher, an SpKeyCipherError. */ + unsigned char pkcs8_error, traditional_error; +} SpKeyCipher; + +typedef enum { + SP_KC_OK, /* it encrypts */ + SP_KC_NO_OID, + SP_KC_EVP_CIPHER, + SP_KC_PARAMETER, + SP_KC_INPUT_LENGTH, + SP_KC_ZERO_CONTENT, + SP_KC_OPERATION, + SP_KC_PEM_CIPHER, + SP_KC_UNSUPPORTED, +} SpKeyCipherError; + +static const struct { + const char *packed, *library, *reason, *code; +} sp_key_cipher_errors[] = { + [SP_KC_NO_OID] = {"0680006C", "asn1 encoding routines", "cipher has no object identifier", + "ERR_OSSL_ASN1_CIPHER_HAS_NO_OBJECT_IDENTIFIER"}, + [SP_KC_EVP_CIPHER] = {"0300006B", "digital envelope routines", "unsupported cipher", + "ERR_OSSL_EVP_UNSUPPORTED_CIPHER"}, + [SP_KC_PARAMETER] = {"0300007A", "digital envelope routines", "cipher parameter error", + "ERR_OSSL_EVP_CIPHER_PARAMETER_ERROR"}, + [SP_KC_INPUT_LENGTH] = {"1C8000E6", "Provider routines", "invalid input length", "ERR_OSSL_INVALID_INPUT_LENGTH"}, + [SP_KC_ZERO_CONTENT] = {"068000DE", "asn1 encoding routines", "illegal zero content", + "ERR_OSSL_ASN1_ILLEGAL_ZERO_CONTENT"}, + [SP_KC_OPERATION] = {"1C800066", "Provider routines", "cipher operation failed", + "ERR_OSSL_CIPHER_OPERATION_FAILED"}, + [SP_KC_PEM_CIPHER] = {"04800071", "PEM routines", "unsupported cipher", "ERR_OSSL_PEM_UNSUPPORTED_CIPHER"}, + [SP_KC_UNSUPPORTED] = {"0308010C", "digital envelope routines", "unsupported", "ERR_OSSL_EVP_UNSUPPORTED"}, +}; + +#define SP_AES_OID(last) "\x60\x86\x48\x01\x65\x03\x04\x01" last, 9 +static const SpKeyCipher sp_key_ciphers[] = { + {"aes-128-cbc", "AES-128-CBC", MBEDTLS_CIPHER_AES_128_CBC, SP_AES_OID("\x02"), 16, 16, false}, + {"aes-192-cbc", "AES-192-CBC", MBEDTLS_CIPHER_AES_192_CBC, SP_AES_OID("\x16"), 24, 16, false}, + {"aes-256-cbc", "AES-256-CBC", MBEDTLS_CIPHER_AES_256_CBC, SP_AES_OID("\x2a"), 32, 16, false}, + {"aes-128-ecb", "AES-128-ECB", MBEDTLS_CIPHER_AES_128_ECB, SP_AES_OID("\x01"), 16, 0, true, SP_KC_OK, SP_KC_PEM_CIPHER}, + {"aes-192-ecb", "AES-192-ECB", MBEDTLS_CIPHER_AES_192_ECB, SP_AES_OID("\x15"), 24, 0, true, SP_KC_OK, SP_KC_PEM_CIPHER}, + {"aes-256-ecb", "AES-256-ECB", MBEDTLS_CIPHER_AES_256_ECB, SP_AES_OID("\x29"), 32, 0, true, SP_KC_OK, SP_KC_PEM_CIPHER}, + {"des-ede3-cbc", "DES-EDE3-CBC", MBEDTLS_CIPHER_DES_EDE3_CBC, "\x2a\x86\x48\x86\xf7\x0d\x03\x07", 8, 24, 8, false}, +}; +static const struct { + const char *alias, *name; +} sp_key_cipher_aliases[] = { + {"aes128", "aes-128-cbc"}, {"aes192", "aes-192-cbc"}, {"aes256", "aes-256-cbc"}, {"des3", "des-ede3-cbc"}, +}; + +/* The names OpenSSL 3.5 refuses to encrypt a private key with in one form + * or both, from Node 24 (the legacy-provider names fail in both). Other + * names Node knows encrypt there and are refused here. */ +#define SP_KC_FAIL(name, pkcs8, traditional) \ + {name, NULL, MBEDTLS_CIPHER_NONE, NULL, 0, 0, 0, false, SP_KC_##pkcs8, SP_KC_##traditional} +static const SpKeyCipher sp_key_cipher_failures[] = { + SP_KC_FAIL("aes-128-cbc-cts", NO_OID, OK), SP_KC_FAIL("aes-128-ccm", EVP_CIPHER, OK), + SP_KC_FAIL("aes-128-ctr", NO_OID, OK), SP_KC_FAIL("aes-128-gcm", PARAMETER, OK), + SP_KC_FAIL("aes-128-gcm-siv", NO_OID, OK), SP_KC_FAIL("aes-128-ocb", NO_OID, OK), + SP_KC_FAIL("aes-128-siv", NO_OID, PEM_CIPHER), + SP_KC_FAIL("aes-128-wrap", INPUT_LENGTH, INPUT_LENGTH), + SP_KC_FAIL("aes-128-wrap-inv", NO_OID, INPUT_LENGTH), + SP_KC_FAIL("aes-128-wrap-pad-inv", NO_OID, OK), SP_KC_FAIL("aes-128-xts", EVP_CIPHER, OK), + SP_KC_FAIL("aes-192-cbc-cts", NO_OID, OK), SP_KC_FAIL("aes-192-ccm", EVP_CIPHER, OK), + SP_KC_FAIL("aes-192-ctr", NO_OID, OK), SP_KC_FAIL("aes-192-gcm", PARAMETER, OK), + SP_KC_FAIL("aes-192-gcm-siv", NO_OID, OK), SP_KC_FAIL("aes-192-ocb", NO_OID, OK), + SP_KC_FAIL("aes-192-siv", NO_OID, PEM_CIPHER), + SP_KC_FAIL("aes-192-wrap", INPUT_LENGTH, INPUT_LENGTH), + SP_KC_FAIL("aes-192-wrap-inv", NO_OID, INPUT_LENGTH), + SP_KC_FAIL("aes-192-wrap-pad-inv", NO_OID, OK), SP_KC_FAIL("aes-256-cbc-cts", NO_OID, OK), + SP_KC_FAIL("aes-256-ccm", EVP_CIPHER, OK), SP_KC_FAIL("aes-256-ctr", NO_OID, OK), + SP_KC_FAIL("aes-256-gcm", PARAMETER, OK), SP_KC_FAIL("aes-256-gcm-siv", NO_OID, OK), + SP_KC_FAIL("aes-256-ocb", NO_OID, OK), SP_KC_FAIL("aes-256-siv", NO_OID, PEM_CIPHER), + SP_KC_FAIL("aes-256-wrap", INPUT_LENGTH, INPUT_LENGTH), + SP_KC_FAIL("aes-256-wrap-inv", NO_OID, INPUT_LENGTH), + SP_KC_FAIL("aes-256-wrap-pad-inv", NO_OID, OK), SP_KC_FAIL("aes-256-xts", EVP_CIPHER, OK), + SP_KC_FAIL("aes128-wrap", INPUT_LENGTH, INPUT_LENGTH), + SP_KC_FAIL("aes128-wrap-inv", NO_OID, INPUT_LENGTH), + SP_KC_FAIL("aes128-wrap-pad-inv", NO_OID, OK), + SP_KC_FAIL("aes192-wrap", INPUT_LENGTH, INPUT_LENGTH), + SP_KC_FAIL("aes192-wrap-inv", NO_OID, INPUT_LENGTH), + SP_KC_FAIL("aes192-wrap-pad-inv", NO_OID, OK), + SP_KC_FAIL("aes256-wrap", INPUT_LENGTH, INPUT_LENGTH), + SP_KC_FAIL("aes256-wrap-inv", NO_OID, INPUT_LENGTH), + SP_KC_FAIL("aes256-wrap-pad-inv", NO_OID, OK), SP_KC_FAIL("aria-128-ccm", EVP_CIPHER, OK), + SP_KC_FAIL("aria-128-cfb1", NO_OID, OK), SP_KC_FAIL("aria-128-cfb8", NO_OID, OK), + SP_KC_FAIL("aria-128-ecb", OK, PEM_CIPHER), SP_KC_FAIL("aria-128-gcm", PARAMETER, OK), + SP_KC_FAIL("aria-192-ccm", EVP_CIPHER, OK), SP_KC_FAIL("aria-192-cfb1", NO_OID, OK), + SP_KC_FAIL("aria-192-cfb8", NO_OID, OK), SP_KC_FAIL("aria-192-ecb", OK, PEM_CIPHER), + SP_KC_FAIL("aria-192-gcm", PARAMETER, OK), SP_KC_FAIL("aria-256-ccm", EVP_CIPHER, OK), + SP_KC_FAIL("aria-256-cfb1", NO_OID, OK), SP_KC_FAIL("aria-256-cfb8", NO_OID, OK), + SP_KC_FAIL("aria-256-ecb", OK, PEM_CIPHER), SP_KC_FAIL("aria-256-gcm", PARAMETER, OK), + SP_KC_FAIL("camellia-128-cbc-cts", NO_OID, OK), SP_KC_FAIL("camellia-128-cfb1", NO_OID, OK), + SP_KC_FAIL("camellia-128-cfb8", NO_OID, OK), SP_KC_FAIL("camellia-128-ecb", OK, PEM_CIPHER), + SP_KC_FAIL("camellia-192-cbc-cts", NO_OID, OK), SP_KC_FAIL("camellia-192-cfb1", NO_OID, OK), + SP_KC_FAIL("camellia-192-cfb8", NO_OID, OK), SP_KC_FAIL("camellia-192-ecb", OK, PEM_CIPHER), + SP_KC_FAIL("camellia-256-cbc-cts", NO_OID, OK), SP_KC_FAIL("camellia-256-cfb1", NO_OID, OK), + SP_KC_FAIL("camellia-256-cfb8", NO_OID, OK), SP_KC_FAIL("camellia-256-ecb", OK, PEM_CIPHER), + SP_KC_FAIL("chacha20", NO_OID, OK), SP_KC_FAIL("chacha20-poly1305", NO_OID, OK), + SP_KC_FAIL("des-ede", OK, PEM_CIPHER), SP_KC_FAIL("des-ede-cbc", NO_OID, OK), + SP_KC_FAIL("des-ede-cfb", NO_OID, OK), SP_KC_FAIL("des-ede-ecb", OK, PEM_CIPHER), + SP_KC_FAIL("des-ede-ofb", NO_OID, OK), SP_KC_FAIL("des-ede3", NO_OID, PEM_CIPHER), + SP_KC_FAIL("des-ede3-cfb", ZERO_CONTENT, OK), SP_KC_FAIL("des-ede3-cfb1", ZERO_CONTENT, OK), + SP_KC_FAIL("des-ede3-cfb8", ZERO_CONTENT, OK), SP_KC_FAIL("des-ede3-ecb", NO_OID, PEM_CIPHER), + SP_KC_FAIL("des-ede3-ofb", NO_OID, OK), SP_KC_FAIL("des3-wrap", OPERATION, PEM_CIPHER), + SP_KC_FAIL("id-aes128-ccm", EVP_CIPHER, OK), SP_KC_FAIL("id-aes128-gcm", PARAMETER, OK), + SP_KC_FAIL("id-aes128-wrap", INPUT_LENGTH, INPUT_LENGTH), + SP_KC_FAIL("id-aes192-ccm", EVP_CIPHER, OK), SP_KC_FAIL("id-aes192-gcm", PARAMETER, OK), + SP_KC_FAIL("id-aes192-wrap", INPUT_LENGTH, INPUT_LENGTH), + SP_KC_FAIL("id-aes256-ccm", EVP_CIPHER, OK), SP_KC_FAIL("id-aes256-gcm", PARAMETER, OK), + SP_KC_FAIL("id-aes256-wrap", INPUT_LENGTH, INPUT_LENGTH), + SP_KC_FAIL("id-smime-alg-cms3deswrap", OPERATION, PEM_CIPHER), SP_KC_FAIL("sm4-ccm", NO_OID, OK), + SP_KC_FAIL("sm4-ecb", OK, PEM_CIPHER), SP_KC_FAIL("sm4-gcm", NO_OID, OK), + SP_KC_FAIL("sm4-xts", NO_OID, OK), SP_KC_FAIL("des", UNSUPPORTED, UNSUPPORTED), + SP_KC_FAIL("des-cbc", UNSUPPORTED, UNSUPPORTED), SP_KC_FAIL("des-ecb", UNSUPPORTED, UNSUPPORTED), + SP_KC_FAIL("des-cfb", UNSUPPORTED, UNSUPPORTED), + SP_KC_FAIL("des-cfb1", UNSUPPORTED, UNSUPPORTED), + SP_KC_FAIL("des-cfb8", UNSUPPORTED, UNSUPPORTED), + SP_KC_FAIL("des-ofb", UNSUPPORTED, UNSUPPORTED), SP_KC_FAIL("desx", UNSUPPORTED, UNSUPPORTED), + SP_KC_FAIL("desx-cbc", UNSUPPORTED, UNSUPPORTED), SP_KC_FAIL("bf", UNSUPPORTED, UNSUPPORTED), + SP_KC_FAIL("bf-cbc", UNSUPPORTED, UNSUPPORTED), SP_KC_FAIL("bf-ecb", UNSUPPORTED, UNSUPPORTED), + SP_KC_FAIL("bf-cfb", UNSUPPORTED, UNSUPPORTED), SP_KC_FAIL("bf-ofb", UNSUPPORTED, UNSUPPORTED), + SP_KC_FAIL("blowfish", UNSUPPORTED, UNSUPPORTED), SP_KC_FAIL("cast", UNSUPPORTED, UNSUPPORTED), + SP_KC_FAIL("cast-cbc", UNSUPPORTED, UNSUPPORTED), + SP_KC_FAIL("cast5-cbc", UNSUPPORTED, UNSUPPORTED), + SP_KC_FAIL("cast5-ecb", UNSUPPORTED, UNSUPPORTED), + SP_KC_FAIL("cast5-cfb", UNSUPPORTED, UNSUPPORTED), + SP_KC_FAIL("cast5-ofb", UNSUPPORTED, UNSUPPORTED), SP_KC_FAIL("rc2", UNSUPPORTED, UNSUPPORTED), + SP_KC_FAIL("rc2-cbc", UNSUPPORTED, UNSUPPORTED), SP_KC_FAIL("rc2-ecb", UNSUPPORTED, UNSUPPORTED), + SP_KC_FAIL("rc2-cfb", UNSUPPORTED, UNSUPPORTED), SP_KC_FAIL("rc2-ofb", UNSUPPORTED, UNSUPPORTED), + SP_KC_FAIL("rc2-40-cbc", UNSUPPORTED, UNSUPPORTED), + SP_KC_FAIL("rc2-64-cbc", UNSUPPORTED, UNSUPPORTED), SP_KC_FAIL("rc4", UNSUPPORTED, UNSUPPORTED), + SP_KC_FAIL("rc4-40", UNSUPPORTED, UNSUPPORTED), + SP_KC_FAIL("rc4-hmac-md5", UNSUPPORTED, UNSUPPORTED), + SP_KC_FAIL("seed", UNSUPPORTED, UNSUPPORTED), SP_KC_FAIL("seed-cbc", UNSUPPORTED, UNSUPPORTED), + SP_KC_FAIL("seed-ecb", UNSUPPORTED, UNSUPPORTED), + SP_KC_FAIL("seed-cfb", UNSUPPORTED, UNSUPPORTED), + SP_KC_FAIL("seed-ofb", UNSUPPORTED, UNSUPPORTED), SP_KC_FAIL("idea", UNSUPPORTED, UNSUPPORTED), + SP_KC_FAIL("idea-cbc", UNSUPPORTED, UNSUPPORTED), + SP_KC_FAIL("idea-ecb", UNSUPPORTED, UNSUPPORTED), + SP_KC_FAIL("idea-cfb", UNSUPPORTED, UNSUPPORTED), + SP_KC_FAIL("idea-ofb", UNSUPPORTED, UNSUPPORTED), +}; +#undef SP_KC_FAIL + +static bool sp_ascii_eq_ci(const char *a, size_t len, const char *b) { + if (strlen(b) != len) return false; + for (size_t i = 0; i < len; i++) { + char c = a[i]; + if (c >= 'A' && c <= 'Z') c = (char)(c - 'A' + 'a'); + if (c != b[i]) return false; + } + return true; +} + +/* The cipher an export names, by a name or the OID text of one: true with + * *out set (an entry of sp_key_ciphers, or of sp_key_cipher_failures); + * false with Node's + * ERR_CRYPTO_UNKNOWN_CIPHER pending, or the SC2020 refusal for a cipher + * Node encrypts with that keys here do not. */ +static bool sp_key_cipher(const ScrStr *name, const SpKeyCipher **out) { + const char *data = name->data; + size_t len = name->len; + static const struct { + const char *oid, *name; + } oids[] = {SCR_CIPHER_OID_ALIASES}; + for (size_t i = 0; i < sizeof oids / sizeof oids[0]; i++) { + if (strlen(oids[i].oid) == len && memcmp(data, oids[i].oid, len) == 0) { + data = oids[i].name; + len = strlen(data); + } + } + const char *wanted = NULL; + for (size_t i = 0; i < sizeof sp_key_cipher_aliases / sizeof sp_key_cipher_aliases[0]; i++) + if (sp_ascii_eq_ci(data, len, sp_key_cipher_aliases[i].alias)) wanted = sp_key_cipher_aliases[i].name; + for (size_t i = 0; i < sizeof sp_key_ciphers / sizeof sp_key_ciphers[0]; i++) { + if (wanted ? strcmp(wanted, sp_key_ciphers[i].name) == 0 : sp_ascii_eq_ci(data, len, sp_key_ciphers[i].name)) { + *out = &sp_key_ciphers[i]; + return true; + } + } + for (size_t i = 0; i < sizeof sp_key_cipher_failures / sizeof sp_key_cipher_failures[0]; i++) { + if (sp_ascii_eq_ci(data, len, sp_key_cipher_failures[i].name)) { + *out = &sp_key_cipher_failures[i]; + return true; + } + } + static const char *const known[] = {SCR_CIPHER_KNOWN_NAMES}; + for (size_t i = 0; i < sizeof known / sizeof known[0]; i++) { + if (sp_ascii_eq_ci(data, len, known[i])) { + sp_not_supported("private keys encrypted with this cipher"); + return false; + } + } + sp_throw(SCR_ERR_ERROR, "ERR_CRYPTO_UNKNOWN_CIPHER", "Unknown cipher"); + return false; +} + +/* Whether encryption in the form (PKCS#8, or traditional PEM) can take + * the cipher for `len` bytes of key: false with OpenSSL's error pending, + * or the SC2020 refusal for a cipher OpenSSL encrypts with that keys here + * do not. The key wrap ciphers fail only on a length that is not a + * multiple of 8; AES key wrap then encrypts, and Node's 3DES key wrap + * corrupts its heap. */ +static bool sp_key_cipher_ready(const SpKeyCipher *ci, bool pkcs8, size_t len) { + unsigned char error = pkcs8 ? ci->pkcs8_error : ci->traditional_error; + if ((error == SP_KC_INPUT_LENGTH || error == SP_KC_OPERATION) && len % 8 == 0) error = SP_KC_OK; + if (error != SP_KC_OK) { + sp_throw_openssl(sp_key_cipher_errors[error].packed, sp_key_cipher_errors[error].library, + sp_key_cipher_errors[error].reason, sp_key_cipher_errors[error].code, true); + return false; + } + if (ci->type == MBEDTLS_CIPHER_NONE) { + sp_not_supported("private keys encrypted with this cipher"); + return false; + } + return true; +} + +/* `in` through the cipher with PKCS#7 padding, as OpenSSL's EVP layer + * pads CBC and ECB: the output (+1 malloc'd, *out_len), or NULL when + * decryption finds bad padding. */ +static unsigned char *sp_key_cipher_run(const SpKeyCipher *ci, bool encrypt, const unsigned char *key, + const unsigned char *iv, const unsigned char *in, size_t in_len, + size_t *out_len) { + const mbedtls_cipher_info_t *info = mbedtls_cipher_info_from_type(ci->type); + size_t block = mbedtls_cipher_info_get_block_size(info); + size_t padded = encrypt ? (in_len / block + 1) * block : in_len; + if (!encrypt && (in_len == 0 || in_len % block != 0)) return NULL; + unsigned char *src = malloc(padded ? padded : 1), *out = malloc(padded ? padded : 1); + if (!src || !out) scr_trap("scriptc: out of memory\n"); + memcpy(src, in, in_len); + if (encrypt) memset(src + in_len, (int)(padded - in_len), padded - in_len); + mbedtls_cipher_context_t ctx; + mbedtls_cipher_init(&ctx); + int r = mbedtls_cipher_setup(&ctx, info); + if (r == 0) r = mbedtls_cipher_setkey(&ctx, key, (int)ci->key_len * 8, encrypt ? MBEDTLS_ENCRYPT : MBEDTLS_DECRYPT); + if (r == 0 && !ci->ecb) r = mbedtls_cipher_set_padding_mode(&ctx, MBEDTLS_PADDING_NONE); + if (r == 0 && !ci->ecb) r = mbedtls_cipher_set_iv(&ctx, iv, ci->iv_len); + size_t written = 0; + for (size_t i = 0; r == 0 && i < padded; i += block) { + size_t n = 0; + r = mbedtls_cipher_update(&ctx, src + i, block, out + written, &n); + written += n; + } + if (r == 0 && !ci->ecb) { + size_t n = 0; + r = mbedtls_cipher_finish(&ctx, out + written, &n); + written += n; + } + mbedtls_cipher_free(&ctx); + mbedtls_platform_zeroize(src, padded); + free(src); + if (r == 0 && !encrypt) { + unsigned char pad = written ? out[written - 1] : 0; + bool ok = pad >= 1 && pad <= block && pad <= written; + for (size_t i = 0; ok && i < pad; i++) ok = out[written - 1 - i] == pad; + if (ok) written -= pad; + else r = -1; + } + if (r != 0) { + mbedtls_platform_zeroize(out, padded); + free(out); + return NULL; + } + *out_len = written; + return out; +} + +/* EncryptedPrivateKeyInfo DER of the PKCS#8 `plain` (+1 malloc'd, + * *out_len), as OpenSSL 3 writes it; NULL on failure. */ +static unsigned char *sp_encrypt_pkcs8(const unsigned char *plain, size_t plain_len, const SpKeyCipher *ci, + const unsigned char *pass, size_t pass_len, size_t *out_len) { + unsigned char salt[16], iv[16], key[32]; + sp_random(NULL, salt, sizeof salt); + sp_random(NULL, iv, ci->iv_len); + if (mbedtls_pkcs5_pbkdf2_hmac_ext(MBEDTLS_MD_SHA256, pass, pass_len, salt, sizeof salt, 2048, + (uint32_t)ci->key_len, key) != 0) + return NULL; + size_t enc_len; + unsigned char *enc = sp_key_cipher_run(ci, true, key, iv, plain, plain_len, &enc_len); + mbedtls_platform_zeroize(key, sizeof key); + if (!enc) return NULL; + size_t size = enc_len + 256; + unsigned char *buf = malloc(size); + if (!buf) scr_trap("scriptc: out of memory\n"); + unsigned char *c = buf + size; + int r, len = 0, w; +#define SP_W(call) do { if ((w = (call)) < 0) { r = w; goto fail; } } while (0) + SP_W(mbedtls_asn1_write_octet_string(&c, buf, enc, enc_len)); + len += w; + /* encryptionScheme: the cipher with its IV (empty for ECB) */ + int scheme = 0; + SP_W(mbedtls_asn1_write_octet_string(&c, buf, iv, ci->iv_len)); + scheme += w; + SP_W(mbedtls_asn1_write_oid(&c, buf, ci->oid, ci->oid_len)); + scheme += w; + SP_W(mbedtls_asn1_write_len(&c, buf, (size_t)scheme)); + scheme += w; + SP_W(mbedtls_asn1_write_tag(&c, buf, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE)); + scheme += w; + /* keyDerivationFunc: PBKDF2 { salt, 2048, hmacWithSHA256 } */ + int kdf = 0, prf = 0; + SP_W(mbedtls_asn1_write_null(&c, buf)); + prf += w; + SP_W(mbedtls_asn1_write_oid(&c, buf, MBEDTLS_OID_HMAC_SHA256, MBEDTLS_OID_SIZE(MBEDTLS_OID_HMAC_SHA256))); + prf += w; + SP_W(mbedtls_asn1_write_len(&c, buf, (size_t)prf)); + prf += w; + SP_W(mbedtls_asn1_write_tag(&c, buf, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE)); + prf += w; + kdf += prf; + SP_W(mbedtls_asn1_write_int(&c, buf, 2048)); + kdf += w; + SP_W(mbedtls_asn1_write_octet_string(&c, buf, salt, sizeof salt)); + kdf += w; + SP_W(mbedtls_asn1_write_len(&c, buf, (size_t)kdf)); + kdf += w; + SP_W(mbedtls_asn1_write_tag(&c, buf, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE)); + kdf += w; + SP_W(mbedtls_asn1_write_oid(&c, buf, MBEDTLS_OID_PKCS5_PBKDF2, MBEDTLS_OID_SIZE(MBEDTLS_OID_PKCS5_PBKDF2))); + kdf += w; + SP_W(mbedtls_asn1_write_len(&c, buf, (size_t)kdf)); + kdf += w; + SP_W(mbedtls_asn1_write_tag(&c, buf, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE)); + kdf += w; + int params = kdf + scheme; + SP_W(mbedtls_asn1_write_len(&c, buf, (size_t)params)); + params += w; + SP_W(mbedtls_asn1_write_tag(&c, buf, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE)); + params += w; + SP_W(mbedtls_asn1_write_oid(&c, buf, MBEDTLS_OID_PKCS5_PBES2, MBEDTLS_OID_SIZE(MBEDTLS_OID_PKCS5_PBES2))); + params += w; + SP_W(mbedtls_asn1_write_len(&c, buf, (size_t)params)); + params += w; + SP_W(mbedtls_asn1_write_tag(&c, buf, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE)); + params += w; + len += params; + SP_W(mbedtls_asn1_write_len(&c, buf, (size_t)len)); + len += w; + SP_W(mbedtls_asn1_write_tag(&c, buf, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE)); + len += w; +#undef SP_W + memmove(buf, c, (size_t)len); + mbedtls_platform_zeroize(enc, enc_len); + free(enc); + *out_len = (size_t)len; + return buf; +fail: + (void)r; + mbedtls_platform_zeroize(enc, enc_len); + free(enc); + free(buf); + return NULL; +} + +/* OpenSSL's EVP_BytesToKey with MD5 and one round over the first 8 IV + * bytes, as traditional PEM encryption derives its key. */ +static void sp_bytes_to_key(const unsigned char *pass, size_t pass_len, const unsigned char *salt, unsigned char *key, + size_t key_len) { + const mbedtls_md_info_t *md5 = mbedtls_md_info_from_type(MBEDTLS_MD_MD5); + unsigned char digest[16]; + size_t done = 0; + mbedtls_md_context_t ctx; + mbedtls_md_init(&ctx); + mbedtls_md_setup(&ctx, md5, 0); + for (bool first = true; done < key_len; first = false) { + mbedtls_md_starts(&ctx); + if (!first) mbedtls_md_update(&ctx, digest, sizeof digest); + mbedtls_md_update(&ctx, pass, pass_len); + mbedtls_md_update(&ctx, salt, 8); + mbedtls_md_finish(&ctx, digest); + size_t n = key_len - done < sizeof digest ? key_len - done : sizeof digest; + memcpy(key + done, digest, n); + done += n; + } + mbedtls_md_free(&ctx); + mbedtls_platform_zeroize(digest, sizeof digest); +} + +/* The traditional encrypted PEM of a PKCS#1 or SEC1 key (+1), or NULL. */ +static ScrStr *sp_encrypted_pem(const char *label, const unsigned char *der, size_t len, const SpKeyCipher *ci, + const unsigned char *pass, size_t pass_len) { + unsigned char iv[16], key[32]; + sp_random(NULL, iv, ci->iv_len); + sp_bytes_to_key(pass, pass_len, iv, key, ci->key_len); + size_t enc_len; + unsigned char *enc = sp_key_cipher_run(ci, true, key, iv, der, len, &enc_len); + mbedtls_platform_zeroize(key, sizeof key); + if (!enc) return NULL; + ScrStr *body = sp_pem(label, enc, enc_len); + mbedtls_platform_zeroize(enc, enc_len); + free(enc); + /* The headers follow the BEGIN line, then a blank line. */ + const char *nl = memchr(body->data, '\n', body->len); + size_t head = (size_t)(nl - body->data) + 1; + ScrJsonBuf out; + scr_jb_init(&out); + for (size_t i = 0; i < head; i++) scr_jb_putc(&out, body->data[i]); + scr_jb_puts(&out, "Proc-Type: 4,ENCRYPTED\nDEK-Info: "); + scr_jb_puts(&out, ci->pem); + scr_jb_putc(&out, ','); + static const char hex[] = "0123456789ABCDEF"; + for (size_t i = 0; i < ci->iv_len; i++) { + scr_jb_putc(&out, hex[iv[i] >> 4]); + scr_jb_putc(&out, hex[iv[i] & 15]); + } + scr_jb_puts(&out, "\n\n"); + for (size_t i = head; i < body->len; i++) scr_jb_putc(&out, body->data[i]); + scr_str_release(body); + return scr_jb_finish(&out); +} + +/* OpenSSL's errors for an encrypted private key: the message, Node's + * code, and the error stack OpenSSL leaves for the input's form. */ +static void sp_throw_key_decrypt(const char *message, const char *library, const char *reason, const char *code, + const char *const *stack, size_t stack_len) { + ScrStr *text = scr_str_new(message, strlen(message)); + ScrError *e = scr_error_new(SCR_ERR_ERROR, text); + scr_str_release(text); + ScrDyn *list = scr_dyn_new_arr(); + for (size_t i = 0; i < stack_len; i++) { + ScrStr *entry = scr_str_new(stack[i], strlen(stack[i])); + scr_dyn_arr_push(list, scr_dyn_new_str(entry)); + scr_str_release(entry); + } + scr_error_set_prop(e, "opensslErrorStack", 17, list); + const char *keys[] = {"library", "reason", "code"}; + const char *values[] = {library, reason, code}; + for (size_t i = 0; i < 3; i++) { + if (i == 2) scr_error_set_code(e, code); + ScrStr *value = scr_str_new(values[i], strlen(values[i])); + scr_error_set_prop(e, keys[i], strlen(keys[i]), scr_dyn_new_str(value)); + scr_str_release(value); + } + scr_throw_obj(e, &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg()); +} + +static const char sp_err_interrupted[] = "error:07880109:common libcrypto routines::interrupted or cancelled"; +static const char sp_err_bad_decrypt[] = "error:1C800064:Provider routines::bad decrypt"; + +/* No passphrase for an encrypted PEM key (pkcs8: whether it is PKCS#8). */ +static void sp_throw_passphrase_missing(bool pkcs8) { + static const char *const pkcs8_stack[] = {"error:04800068:PEM routines::bad password read", sp_err_interrupted, + "error:1C80009F:Provider routines::unable to get passphrase", + sp_err_interrupted, sp_err_interrupted}; + static const char *const pem_stack[] = {"error:04800068:PEM routines::bad password read", sp_err_interrupted, + sp_err_interrupted}; + sp_throw_key_decrypt(sp_err_interrupted, "common libcrypto routines", "interrupted or cancelled", + "ERR_OSSL_CRYPTO_INTERRUPTED_OR_CANCELLED", pkcs8 ? pkcs8_stack : pem_stack, pkcs8 ? 5 : 3); +} + +/* A passphrase that does not decrypt the key: PKCS#8 in PEM, PKCS#8 in + * DER, or traditional PEM. */ +static void sp_throw_bad_decrypt(int form) { + static const char cipherfinal[] = "error:11800074:PKCS12 routines::pkcs12 cipherfinal error"; + static const char *const stacks[][4] = { + {cipherfinal, sp_err_bad_decrypt, cipherfinal, sp_err_bad_decrypt}, + {cipherfinal, sp_err_bad_decrypt}, + {"error:04800065:PEM routines::bad decrypt", sp_err_bad_decrypt}, + }; + sp_throw_key_decrypt(sp_err_bad_decrypt, "Provider routines", "bad decrypt", "ERR_OSSL_BAD_DECRYPT", stacks[form], + form == 0 ? 4 : 2); +} + +/* The cipher name of an encrypted traditional PEM key's DEK-Info header + * into `name` (NUL-terminated), or false for other input. */ +static bool sp_pem_dek_info(const unsigned char *data, size_t len, char *name, size_t cap) { + const char *text = (const char *)data; + const char *begin = NULL; + for (size_t i = 0; i + 11 <= len && !begin; i++) + if (memcmp(text + i, "-----BEGIN ", 11) == 0) begin = text + i; + if (!begin) return false; + const char *end = text + len, *line = memchr(begin, '\n', (size_t)(end - begin)); + bool encrypted = false; + while (line && ++line < end) { + const char *eol = memchr(line, '\n', (size_t)(end - line)); + size_t n = (size_t)((eol ? eol : end) - line); + if (n && line[n - 1] == '\r') n--; + if (n >= 10 && memcmp(line, "Proc-Type:", 10) == 0) { + for (size_t i = 10; i + 9 <= n; i++) + if (memcmp(line + i, "ENCRYPTED", 9) == 0) encrypted = true; + } else if (n >= 9 && memcmp(line, "DEK-Info:", 9) == 0) { + if (!encrypted) return false; + size_t i = 9; + while (i < n && line[i] == ' ') i++; + size_t o = 0; + while (i < n && line[i] != ',' && o + 1 < cap) name[o++] = line[i++]; + name[o] = 0; + return true; + } else if (n == 0 || memchr(line, ':', n) == NULL) { + return false; + } + line = eol; + } + return false; +} + +/* Before mbedTLS reads an encrypted traditional PEM key: false with + * OpenSSL's error pending for a DEK-Info cipher it does not know, a + * missing passphrase, or a legacy-provider cipher, or the SC2020 refusal + * for a cipher mbedTLS does not decrypt; true for other input. */ +static bool sp_pem_dek_check(const unsigned char *data, size_t len, const ScrBytes *pass) { + char name[64]; + if (!sp_pem_dek_info(data, len, name, sizeof name)) return true; + size_t n = strlen(name); + static const char *const known[] = {SCR_CIPHER_KNOWN_NAMES}; + static const struct { + const char *name; + unsigned iv_len; + } legacy[] = {SCR_CIPHER_LEGACY_ENTRIES}; + bool is_known = false, is_legacy = false; + for (size_t i = 0; i < sizeof known / sizeof known[0]; i++) + if (sp_ascii_eq_ci(name, n, known[i])) is_known = true; + for (size_t i = 0; i < sizeof legacy / sizeof legacy[0]; i++) + if (sp_ascii_eq_ci(name, n, legacy[i].name)) is_legacy = true; + if (!is_known && !is_legacy) { + static const char *const stack[] = {"error:04800072:PEM routines::unsupported encryption"}; + sp_throw_key_decrypt(stack[0], "PEM routines", "unsupported encryption", "ERR_OSSL_PEM_UNSUPPORTED_ENCRYPTION", + stack, 1); + return false; + } + if (!pass) { + sp_throw_passphrase_missing(false); + return false; + } + if (is_legacy) { + static const char unsupported[] = "error:0308010C:digital envelope routines::unsupported"; + static const char *const stack[] = {"error:04800065:PEM routines::bad decrypt", unsupported}; + sp_throw_key_decrypt(unsupported, "digital envelope routines", "unsupported", "ERR_OSSL_EVP_UNSUPPORTED", stack, + 2); + return false; + } + /* mbedTLS decrypts these names, as OpenSSL writes them. */ + static const char *const read[] = {"AES-128-CBC", "AES-192-CBC", "AES-256-CBC", "DES-EDE3-CBC"}; + for (size_t i = 0; i < sizeof read / sizeof read[0]; i++) + if (strcmp(name, read[i]) == 0) return true; + sp_not_supported("private keys encrypted with this cipher"); + return false; +} + +/* The PKCS#8 inside a PBES2 EncryptedPrivateKeyInfo `der` (+1 malloc'd, + * *out_len): 1 decrypted, 0 another scheme or cipher, -1 a passphrase + * that does not decrypt it. */ +static int sp_decrypt_pkcs8(const unsigned char *der, size_t len, const unsigned char *pass, size_t pass_len, + unsigned char **out, size_t *out_len) { + unsigned char *p = (unsigned char *)der; + const unsigned char *end = der + len; + size_t n; + mbedtls_asn1_buf alg, params; + if (mbedtls_asn1_get_tag(&p, end, &n, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE) != 0) return 0; + end = p + n; + if (mbedtls_asn1_get_alg(&p, end, &alg, ¶ms) != 0 || alg.len != MBEDTLS_OID_SIZE(MBEDTLS_OID_PKCS5_PBES2) || + memcmp(alg.p, MBEDTLS_OID_PKCS5_PBES2, alg.len) != 0 || + params.tag != (MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE)) + return 0; + unsigned char *q = params.p; + const unsigned char *params_end = params.p + params.len; + mbedtls_asn1_buf kdf, kdf_params, scheme, scheme_params; + if (mbedtls_asn1_get_alg(&q, params_end, &kdf, &kdf_params) != 0 || + kdf.len != MBEDTLS_OID_SIZE(MBEDTLS_OID_PKCS5_PBKDF2) || memcmp(kdf.p, MBEDTLS_OID_PKCS5_PBKDF2, kdf.len) != 0 || + mbedtls_asn1_get_alg(&q, params_end, &scheme, &scheme_params) != 0) + return 0; + const SpKeyCipher *ci = NULL; + for (size_t i = 0; i < sizeof sp_key_ciphers / sizeof sp_key_ciphers[0]; i++) + if (scheme.len == sp_key_ciphers[i].oid_len && memcmp(scheme.p, sp_key_ciphers[i].oid, scheme.len) == 0) + ci = &sp_key_ciphers[i]; + if (!ci || scheme_params.tag != MBEDTLS_ASN1_OCTET_STRING || scheme_params.len != ci->iv_len) return 0; + /* PBKDF2-params ::= SEQUENCE { salt OCTET STRING, iterationCount + * INTEGER, keyLength INTEGER OPTIONAL, prf AlgorithmIdentifier DEFAULT + * hmacWithSHA1 } */ + unsigned char *k = kdf_params.p; + const unsigned char *k_end = kdf_params.p + kdf_params.len; + size_t salt_len; + int iterations, key_len = (int)ci->key_len; + if (kdf_params.tag != (MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE) || + mbedtls_asn1_get_tag(&k, k_end, &salt_len, MBEDTLS_ASN1_OCTET_STRING) != 0) + return 0; + const unsigned char *salt = k; + k += salt_len; + if (mbedtls_asn1_get_int(&k, k_end, &iterations) != 0 || iterations < 1) return 0; + if (k < k_end && *k == MBEDTLS_ASN1_INTEGER && (mbedtls_asn1_get_int(&k, k_end, &key_len) != 0 || + key_len != (int)ci->key_len)) + return 0; + mbedtls_md_type_t prf = MBEDTLS_MD_SHA1; + if (k < k_end) { + mbedtls_asn1_buf prf_oid, prf_params; + if (mbedtls_asn1_get_alg(&k, k_end, &prf_oid, &prf_params) != 0 || mbedtls_oid_get_md_hmac(&prf_oid, &prf) != 0) + return 0; + } + if (mbedtls_asn1_get_tag(&p, end, &n, MBEDTLS_ASN1_OCTET_STRING) != 0) return 0; + unsigned char key[32]; + if (mbedtls_pkcs5_pbkdf2_hmac_ext(prf, pass, pass_len, salt, salt_len, (unsigned)iterations, + (uint32_t)ci->key_len, key) != 0) + return 0; + *out = sp_key_cipher_run(ci, false, key, scheme_params.p, p, n, out_len); + mbedtls_platform_zeroize(key, sizeof key); + return *out ? 1 : -1; +} + static ScrDyn *sp_dyn_buffer(const unsigned char *data, size_t len) { ScrBytes *bytes = scr_bytes_new(SCR_BYTES_U8, (double)len); bytes->is_buffer = true; @@ -472,9 +1290,9 @@ static ScrDyn *sp_b64url(const uint8_t *raw, size_t len) { static ScrDyn *sp_export_jwk(SpKey *k) { ScrDyn *jwk = scr_dyn_new_obj(); - if (sp_is_ed(k)) { + if (sp_is_okp(k)) { sp_set(jwk, "kty", sp_dyn_str(scr_str_new("OKP", 3))); - sp_set(jwk, "crv", sp_dyn_str(scr_str_new("Ed25519", 7))); + sp_set(jwk, "crv", sp_dyn_str(sp_is_x(k) ? scr_str_new("X25519", 6) : scr_str_new("Ed25519", 7))); sp_set(jwk, "x", sp_b64url(k->ed_pub, 32)); if (k->private_key) sp_set(jwk, "d", sp_b64url(k->ed_seed, 32)); return jwk; @@ -586,7 +1404,8 @@ static SpKey *sp_key_from_jwk_plain(const ScrDyn *jwk, bool want_private) { bool ok = false; if (sp_is_str(kty, "OKP")) { const ScrDyn *crv = sp_opt(jwk, "crv"); - if (!sp_is_str(crv, "Ed25519")) { + bool x = sp_is_str(crv, "X25519"); + if (!x && !sp_is_str(crv, "Ed25519")) { sp_key_release(k); if (crv->kind != SCR_DYN_STR) { scr_dyn_prop_type_fail("key.crv", "of type string", crv); @@ -595,10 +1414,13 @@ static SpKey *sp_key_from_jwk_plain(const ScrDyn *jwk, bool want_private) { sp_not_supported("JWK keys of this crv"); return NULL; } - k->ed25519 = true; + k->ed25519 = !x; + k->x25519 = x; if (private_key) { ok = sp_jwk_bytes(jwk, "d", k->ed_seed); - if (ok) { + if (ok && x) { + ok = sp_x25519(k->ed_seed, NULL, k->ed_pub); + } else if (ok) { const ScrPkiEd25519 *ed = sp_ed(); if (!ed) { sp_key_release(k); @@ -677,19 +1499,21 @@ static SpKey *sp_key_from_jwk_plain(const ScrDyn *jwk, bool want_private) { /* The private key of a public SpKey derived from `k`. */ static SpKey *sp_public_of(SpKey *k) { - if (sp_is_ed(k)) { + if (sp_is_okp(k)) { SpKey *pub = sp_key_alloc(false); - pub->ed25519 = true; + pub->ed25519 = k->ed25519; + pub->x25519 = k->x25519; memcpy(pub->ed_pub, k->ed_pub, 32); return pub; } unsigned char der[SP_DER_MAX]; - int n = sp_write_spki_der(k, der, sizeof der); + int n = k->rsa_pss ? mbedtls_pk_write_pubkey_der(&k->pk, der, sizeof der) : sp_write_spki_der(k, der, sizeof der); SpKey *pub = sp_key_alloc(false); if (n < 0 || mbedtls_pk_parse_public_key(&pub->pk, der + sizeof der - n, (size_t)n) != 0) { sp_key_release(pub); return NULL; } + sp_pss_copy(pub, k); return pub; } @@ -769,6 +1593,24 @@ static bool sp_cert_spki(const unsigned char *der, size_t len, const unsigned ch * as public answers its public key. */ static SpKey *sp_ed_from_der(const unsigned char *der, size_t len, bool private_key, bool *failed) { *failed = false; + /* X25519 keys, which need no other unit. */ + if (len == 48 && memcmp(der, sp_x_pkcs8_prefix, 16) == 0) { + SpKey *k = sp_key_alloc(private_key); + k->x25519 = true; + memcpy(k->ed_seed, der + 16, 32); + if (!sp_x25519(k->ed_seed, NULL, k->ed_pub)) { + sp_key_release(k); + return NULL; + } + if (!private_key) mbedtls_platform_zeroize(k->ed_seed, 32); + return k; + } + if (!private_key && len == 44 && memcmp(der, sp_x_spki_prefix, 12) == 0) { + SpKey *k = sp_key_alloc(false); + k->x25519 = true; + memcpy(k->ed_pub, der + 12, 32); + return k; + } if (len == 48 && memcmp(der, sp_ed_pkcs8_prefix, 16) == 0) { const ScrPkiEd25519 *ed = sp_ed(); if (!ed) { @@ -793,7 +1635,201 @@ static SpKey *sp_ed_from_der(const unsigned char *der, size_t len, bool private_ return k; } +/* The name of a digest OID that RSASSA-PSS-params can hold and this + * runtime does not sign with, for its refusal. */ +static const char *sp_pss_other_digest(const mbedtls_asn1_buf *oid) { + /* 2.16.840.1.101.3.4.2.x, the NIST hash arc */ + static const unsigned char nist[] = {0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02}; + if (oid->len == sizeof nist + 1 && memcmp(oid->p, nist, sizeof nist) == 0) { + switch (oid->p[sizeof nist]) { + case 0x05: return "sha512-224"; + case 0x06: return "sha512-256"; + case 0x07: return "sha3-224"; + case 0x08: return "sha3-256"; + case 0x09: return "sha3-384"; + case 0x0a: return "sha3-512"; + } + } + mbedtls_md_type_t md; + if (mbedtls_oid_get_md_alg(oid, &md) == 0) { + if (md == MBEDTLS_MD_MD5) return "md5"; + if (md == MBEDTLS_MD_RIPEMD160) return "ripemd160"; + } + return "this digest"; +} + +/* RSASSA-PSS-params at *p (RFC 4055): 1 with the restrictions set on `k`; + * 0 when they are malformed, which leaves the key without restrictions, as + * OpenSSL reads a negative or oversized salt; -1 with *digest named when a + * hash or MGF1 digest is one this runtime does not sign with, so that the + * key is refused rather than imported without the restrictions it carries. */ +static int sp_pss_params_read(unsigned char **p, const unsigned char *end, SpKey *k, const char **digest) { + size_t n; + if (mbedtls_asn1_get_tag(p, end, &n, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE) != 0) return 0; + const unsigned char *params_end = *p + n; + mbedtls_md_type_t md = MBEDTLS_MD_SHA1, mgf1 = MBEDTLS_MD_SHA1; + int salt = 20; + if (*p < params_end && **p == (MBEDTLS_ASN1_CONTEXT_SPECIFIC | MBEDTLS_ASN1_CONSTRUCTED | 0)) { + mbedtls_asn1_buf oid, params; + if (mbedtls_asn1_get_tag(p, params_end, &n, MBEDTLS_ASN1_CONTEXT_SPECIFIC | MBEDTLS_ASN1_CONSTRUCTED | 0) != 0 || + mbedtls_asn1_get_alg(p, *p + n, &oid, ¶ms) != 0) + return 0; + if (mbedtls_oid_get_md_alg(&oid, &md) != 0 || !sp_pss_md_name(md)) { + *digest = sp_pss_other_digest(&oid); + return -1; + } + } + if (*p < params_end && **p == (MBEDTLS_ASN1_CONTEXT_SPECIFIC | MBEDTLS_ASN1_CONSTRUCTED | 1)) { + mbedtls_asn1_buf oid, params; + if (mbedtls_asn1_get_tag(p, params_end, &n, MBEDTLS_ASN1_CONTEXT_SPECIFIC | MBEDTLS_ASN1_CONSTRUCTED | 1) != 0 || + mbedtls_asn1_get_alg(p, *p + n, &oid, ¶ms) != 0 || oid.len != MBEDTLS_OID_SIZE(MBEDTLS_OID_MGF1) || + memcmp(oid.p, MBEDTLS_OID_MGF1, oid.len) != 0) + return 0; + /* MGF1's parameters are the hash's AlgorithmIdentifier, whose + * content starts with the hash's OID. */ + if (params.tag != (MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE)) return 0; + unsigned char *alg = params.p; + mbedtls_asn1_buf hash_oid = {.tag = MBEDTLS_ASN1_OID}; + if (mbedtls_asn1_get_tag(&alg, params.p + params.len, &hash_oid.len, MBEDTLS_ASN1_OID) != 0) return 0; + hash_oid.p = alg; + if (mbedtls_oid_get_md_alg(&hash_oid, &mgf1) != 0 || !sp_pss_md_name(mgf1)) { + *digest = sp_pss_other_digest(&hash_oid); + return -1; + } + } + if (*p < params_end && **p == (MBEDTLS_ASN1_CONTEXT_SPECIFIC | MBEDTLS_ASN1_CONSTRUCTED | 2)) { + if (mbedtls_asn1_get_tag(p, params_end, &n, MBEDTLS_ASN1_CONTEXT_SPECIFIC | MBEDTLS_ASN1_CONSTRUCTED | 2) != 0 || + mbedtls_asn1_get_int(p, *p + n, &salt) != 0 || salt < 0) + return 0; + } + *p = (unsigned char *)params_end; + k->pss_params = true; + k->pss_md = md; + k->pss_mgf1 = mgf1; + k->pss_salt = salt; + return 1; +} + +/* An RSA-PSS key from PKCS#8 or SubjectPublicKeyInfo DER whose algorithm + * is id-RSASSA-PSS; NULL when the DER is neither, or with *failed set and + * Node's decoder error pending. A private key wanted as public answers its + * public key. */ +static SpKey *sp_public_of(SpKey *k); + +static SpKey *sp_pss_from_der(const unsigned char *der, size_t len, bool private_key, bool *failed) { + *failed = false; + unsigned char *p = (unsigned char *)der; + const unsigned char *end = der + len; + size_t n; + if (mbedtls_asn1_get_tag(&p, end, &n, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE) != 0) return NULL; + end = p + n; + bool pkcs8 = p < end && *p == MBEDTLS_ASN1_INTEGER; + if (pkcs8) { + int version; + if (mbedtls_asn1_get_int(&p, end, &version) != 0 || version != 0) return NULL; + } + if (mbedtls_asn1_get_tag(&p, end, &n, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE) != 0) return NULL; + const unsigned char *alg_end = p + n; + size_t oid_len; + if (mbedtls_asn1_get_tag(&p, alg_end, &oid_len, MBEDTLS_ASN1_OID) != 0 || + oid_len != MBEDTLS_OID_SIZE(MBEDTLS_OID_RSASSA_PSS) || memcmp(p, MBEDTLS_OID_RSASSA_PSS, oid_len) != 0) + return NULL; + p += oid_len; + SpKey *k = sp_key_alloc(pkcs8); + k->rsa_pss = true; + const char *digest = NULL; + if (p < alg_end) { + int read = sp_pss_params_read(&p, alg_end, k, &digest); + if (read < 0) { + sp_key_release(k); + *failed = true; + char what[96]; + snprintf(what, sizeof what, "RSA-PSS keys restricted to %s", digest); + sp_not_supported(what); + return NULL; + } + if (read == 0) k->pss_params = false; + } + p = (unsigned char *)alg_end; + int r; + if (pkcs8) { + r = mbedtls_asn1_get_tag(&p, end, &n, MBEDTLS_ASN1_OCTET_STRING); + if (r == 0) r = mbedtls_pk_parse_key(&k->pk, p, n, NULL, 0, sp_random, NULL); + } else { + r = private_key ? -1 : mbedtls_asn1_get_bitstring_null(&p, end, &n); + if (r == 0) r = mbedtls_pk_parse_public_key(&k->pk, p, n); + } + if (r != 0 || !sp_is_rsa(k)) { + sp_key_release(k); + *failed = true; + sp_unsupported_input(); + return NULL; + } + if (pkcs8 && !private_key) { + SpKey *pub = sp_public_of(k); + sp_key_release(k); + return pub; + } + return k; +} + +/* Whether `der` is a PKCS#8 EncryptedPrivateKeyInfo: a SEQUENCE of a PKCS#5 + * or PKCS#12 password-based AlgorithmIdentifier and an OCTET STRING. */ +static bool sp_is_encrypted_pkcs8(const unsigned char *der, size_t len) { + unsigned char *p = (unsigned char *)der; + const unsigned char *end = der + len; + size_t n; + mbedtls_asn1_buf alg, params; + if (mbedtls_asn1_get_tag(&p, end, &n, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE) != 0) return false; + end = p + n; + if (mbedtls_asn1_get_alg(&p, end, &alg, ¶ms) != 0) return false; + static const char pkcs5[] = MBEDTLS_OID_PKCS5, pkcs12[] = MBEDTLS_OID_PKCS12_PBE; + bool password = (alg.len > sizeof pkcs5 - 1 && memcmp(alg.p, pkcs5, sizeof pkcs5 - 1) == 0) || + (alg.len > sizeof pkcs12 - 1 && memcmp(alg.p, pkcs12, sizeof pkcs12 - 1) == 0); + return password && mbedtls_asn1_get_tag(&p, end, &n, MBEDTLS_ASN1_OCTET_STRING) == 0 && p + n == end; +} + +static SpKey *sp_parse_with(const unsigned char *data, size_t len, bool private_key, const ScrBytes *pass); + static SpKey *sp_parse(const unsigned char *data, size_t len, bool private_key) { + return sp_parse_with(data, len, private_key, NULL); +} + +/* sp_parse with a passphrase for an encrypted private key (NULL: none). */ +static SpKey *sp_parse_with(const unsigned char *data, size_t len, bool private_key, const ScrBytes *pass) { + { + unsigned char *decoded; + const unsigned char *der; + size_t der_len; + if (sp_der_of(data, len, &decoded, &der, &der_len) && sp_is_encrypted_pkcs8(der, der_len)) { + bool from_pem = decoded != NULL; + SpKey *k = NULL; + unsigned char *plain = NULL; + size_t plain_len = 0; + int r = 0; + if (!pass) { + if (from_pem) sp_throw_passphrase_missing(true); + else sp_throw(SCR_ERR_TYPE, "ERR_MISSING_PASSPHRASE", "Passphrase required for encrypted key"); + } else { + r = sp_decrypt_pkcs8(der, der_len, pass->data, (size_t)scr_bytes_byte_len(pass), &plain, &plain_len); + if (r < 0) sp_throw_bad_decrypt(from_pem ? 0 : 1); + } + if (r > 0) { + k = sp_parse_with(plain, plain_len, private_key, NULL); + mbedtls_platform_zeroize(plain, plain_len); + free(plain); + } + if (decoded) { + mbedtls_platform_zeroize(decoded, der_len); + free(decoded); + } + if (r == 0 && pass) sp_not_supported("private keys encrypted with this scheme or cipher"); + return k; + } else if (decoded) { + mbedtls_platform_zeroize(decoded, der_len); + free(decoded); + } + } { unsigned char *decoded; const unsigned char *der; @@ -801,6 +1837,7 @@ static SpKey *sp_parse(const unsigned char *data, size_t len, bool private_key) if (sp_der_of(data, len, &decoded, &der, &der_len)) { bool failed; SpKey *ed = sp_ed_from_der(der, der_len, private_key, &failed); + if (!ed && !failed) ed = sp_pss_from_der(der, der_len, private_key, &failed); if (decoded) { mbedtls_platform_zeroize(decoded, der_len); free(decoded); @@ -817,6 +1854,7 @@ static SpKey *sp_parse(const unsigned char *data, size_t len, bool private_key) break; } } + if (pem && !sp_pem_dek_check(data, len, pass)) return NULL; unsigned char *buf = malloc(len + 1); if (!buf) scr_trap("scriptc: out of memory\n"); memcpy(buf, data, len); @@ -824,15 +1862,19 @@ static SpKey *sp_parse(const unsigned char *data, size_t len, bool private_key) size_t n = pem ? len + 1 : len; SpKey *k = sp_key_alloc(private_key); bool ok; + const unsigned char *pwd = pass ? pass->data : NULL; + size_t pwd_len = pass ? (size_t)scr_bytes_byte_len(pass) : 0; + int private_rc = 0; if (private_key) { - ok = mbedtls_pk_parse_key(&k->pk, buf, n, NULL, 0, sp_random, NULL) == 0; + private_rc = mbedtls_pk_parse_key(&k->pk, buf, n, pwd, pwd_len, sp_random, NULL); + ok = private_rc == 0; } else { ok = mbedtls_pk_parse_public_key(&k->pk, buf, n) == 0; if (!ok) { SpKey *priv = sp_key_alloc(true); mbedtls_pk_free(&k->pk); mbedtls_pk_init(&k->pk); - if (mbedtls_pk_parse_key(&priv->pk, buf, n, NULL, 0, sp_random, NULL) == 0) { + if ((private_rc = mbedtls_pk_parse_key(&priv->pk, buf, n, pwd, pwd_len, sp_random, NULL)) == 0) { sp_key_release(k); k = sp_public_of(priv); ok = k != NULL; @@ -854,7 +1896,10 @@ static SpKey *sp_parse(const unsigned char *data, size_t len, bool private_key) free(buf); if (!ok) { sp_key_release(k); - sp_unsupported_input(); + /* An encrypted traditional PEM key mbedTLS read. */ + if (private_rc == MBEDTLS_ERR_PK_PASSWORD_REQUIRED) sp_throw_passphrase_missing(false); + else if (private_rc == MBEDTLS_ERR_PK_PASSWORD_MISMATCH) sp_throw_bad_decrypt(2); + else sp_unsupported_input(); return NULL; } if (!sp_key_supported(k)) { @@ -878,6 +1923,8 @@ static ScrBytes *sp_source_bytes(const ScrDyn *v, const ScrDyn *encoding) { return NULL; } +static SpKey *sp_key_from_raw(const ScrDyn *input, const ScrDyn *data, const ScrDyn *format, bool private_input); + static const char sp_key_input_types[] = "of type string or an instance of ArrayBuffer, Buffer, TypedArray, DataView, or URL"; @@ -914,13 +1961,16 @@ static ScrDyn *sp_create_key(ScrDyn *input, bool private_key) { const ScrDyn *format = scr_dyn_undefined(); const ScrDyn *type = scr_dyn_undefined(); const ScrDyn *encoding = NULL; + const ScrDyn *passphrase = NULL; if (sp_is_object(input) && !scr_buffer_storage_is(input)) { data = sp_opt(input, "key"); format = sp_opt(input, "format"); type = sp_opt(input, "type"); encoding = sp_opt(input, "encoding"); - if (!sp_absent(sp_opt(input, "passphrase"))) { - sp_not_supported("encrypted private keys"); + passphrase = sp_opt(input, "passphrase"); + if (!sp_absent(passphrase) && passphrase->kind != SCR_DYN_STR && passphrase->kind != SCR_DYN_BYTES && + !scr_buffer_storage_is(passphrase)) { + sp_property_invalid("key.passphrase", passphrase); return NULL; } SpKey *inner = sp_key_of(data); @@ -929,14 +1979,22 @@ static ScrDyn *sp_create_key(ScrDyn *input, bool private_key) { SpKey *k = sp_key_from_jwk(data, private_key); return k ? sp_key_box(k) : NULL; } + if (sp_is_str(format, "raw-public") || sp_is_str(format, "raw-private") || sp_is_str(format, "raw-seed")) { + SpKey *k = sp_key_from_raw(input, data, format, private_key); + if (!k || private_key || !k->private_key) return k ? sp_key_box(k) : NULL; + SpKey *pub = sp_public_of(k); + sp_key_release(k); + if (!pub) sp_unsupported_input(); + return pub ? sp_key_box(pub) : NULL; + } if (!sp_absent(format) && !sp_is_str(format, "pem") && !sp_is_str(format, "der")) { sp_property_invalid("key.format", format); return NULL; } if (sp_is_str(format, "der")) { - bool valid = private_key ? sp_is_str(type, "pkcs1") || sp_is_str(type, "pkcs8") || - sp_is_str(type, "sec1") - : sp_is_str(type, "pkcs1") || sp_is_str(type, "spki"); + /* createPublicKey also reads a private key's encodings. */ + bool valid = sp_is_str(type, "pkcs1") || sp_is_str(type, "pkcs8") || sp_is_str(type, "sec1") || + (!private_key && sp_is_str(type, "spki")); if (!valid) { sp_property_invalid("key.type", type); return NULL; @@ -950,7 +2008,17 @@ static ScrDyn *sp_create_key(ScrDyn *input, bool private_key) { scr_dyn_arg_type_fail(data == input ? "key" : "key.key", sp_key_input_types, data); return NULL; } - SpKey *k = sp_parse(bytes->data, (size_t)scr_bytes_byte_len(bytes), private_key); + ScrBytes *pass = NULL; + if (passphrase && !sp_absent(passphrase)) { + /* Node decodes a string passphrase with the key's encoding. */ + pass = sp_source_bytes(passphrase, encoding); + if (!pass) { + scr_bytes_release(bytes); + return NULL; + } + } + SpKey *k = sp_parse_with(bytes->data, (size_t)scr_bytes_byte_len(bytes), private_key, pass); + if (pass) scr_bytes_release(pass); scr_bytes_release(bytes); return k ? sp_key_box(k) : NULL; } @@ -959,6 +2027,191 @@ ScrDyn *scr_crypto_create_private_key(ScrDyn *key) { return sp_create_key(key, t ScrDyn *scr_crypto_create_public_key(ScrDyn *key) { return sp_create_key(key, false); } +/* ── raw keys ─────────────────────────────────────────────────────────── */ + +/* A namedCurve as Node reads it: the OpenSSL short name or the NIST name, + * matched exactly. NULL for other names. */ +static const SpCurve *sp_curve_named(const ScrDyn *name) { + for (size_t i = 0; i < sizeof sp_curves / sizeof sp_curves[0]; i++) + if (sp_is_str(name, sp_curves[i].name) || (sp_curves[i].nist && sp_is_str(name, sp_curves[i].nist))) + return &sp_curves[i]; + return NULL; +} + +static void sp_incompatible_key_type(void) { + sp_throw(SCR_ERR_ERROR, "ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS", + "The selected key encoding is incompatible with the key type"); +} + +static void sp_invalid_key_data(void) { sp_throw(SCR_ERR_TYPE, "ERR_INVALID_ARG_VALUE", "Invalid key data"); } + +/* A key's raw encoding: an EC public point, compressed when asked, or the + * private scalar at the curve's size, or an Ed25519 public key or seed. + * raw-seed, and every encoding of an RSA key, throw + * ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS. NULL with the error pending. */ +static ScrDyn *sp_export_raw(SpKey *k, bool private_part, bool seed, bool compressed) { + if (seed || sp_is_rsa(k)) { + sp_incompatible_key_type(); + return NULL; + } + if (sp_is_okp(k)) return sp_dyn_buffer(private_part ? k->ed_seed : k->ed_pub, 32); + mbedtls_ecp_keypair *ec = mbedtls_pk_ec(k->pk); + const mbedtls_ecp_group *grp = &ec->MBEDTLS_PRIVATE(grp); + unsigned char buf[1 + 2 * 66]; + size_t n = (grp->nbits + 7) / 8; + int form = compressed ? MBEDTLS_ECP_PF_COMPRESSED : MBEDTLS_ECP_PF_UNCOMPRESSED; + int rc = private_part ? mbedtls_mpi_write_binary(&ec->MBEDTLS_PRIVATE(d), buf, n) + : mbedtls_ecp_point_write_binary(grp, &ec->MBEDTLS_PRIVATE(Q), form, &n, buf, sizeof buf); + ScrDyn *out = rc == 0 ? sp_dyn_buffer(buf, n) : NULL; + mbedtls_platform_zeroize(buf, sizeof buf); + if (!out) sp_unsupported_input(); + return out; +} + +/* Node's named key types that this runtime does not generate or read: + * X448, Ed448 and the post-quantum types. */ +static const char *const sp_other_named_types[] = { + "ed448", + "x448", + "ml-dsa-44", + "ml-dsa-65", + "ml-dsa-87", + "ml-kem-512", + "ml-kem-768", + "ml-kem-1024", + "slh-dsa-sha2-128f", + "slh-dsa-sha2-128s", + "slh-dsa-sha2-192f", + "slh-dsa-sha2-192s", + "slh-dsa-sha2-256f", + "slh-dsa-sha2-256s", + "slh-dsa-shake-128f", + "slh-dsa-shake-128s", + "slh-dsa-shake-192f", + "slh-dsa-shake-192s", + "slh-dsa-shake-256f", + "slh-dsa-shake-256s", +}; + +/* createPrivateKey / createPublicKey of { key, format: "raw-public" | + * "raw-private" | "raw-seed", asymmetricKeyType, namedCurve }: an EC point + * (uncompressed or compressed) or scalar, or an Ed25519 public key or + * seed. The argument checks are Node's prepareAsymmetricKey order, then + * the key type, the seed format, the curve and the data. NULL with the + * error pending. */ +static SpKey *sp_key_from_raw(const ScrDyn *input, const ScrDyn *data, const ScrDyn *format, bool private_input) { + bool raw_public = sp_is_str(format, "raw-public"); + bool seed = sp_is_str(format, "raw-seed"); + if (private_input && raw_public) { + sp_property_invalid("key.format", format); + return NULL; + } + if (data->kind != SCR_DYN_BYTES && !scr_buffer_storage_is(data)) { + scr_dyn_arg_type_fail("key.key", "an instance of ArrayBuffer, Buffer, TypedArray, or DataView", data); + return NULL; + } + const ScrDyn *type = sp_opt(input, "asymmetricKeyType"); + if (type->kind != SCR_DYN_STR) { + scr_dyn_prop_type_fail("key.asymmetricKeyType", "of type string", type); + return NULL; + } + bool ec = sp_is_str(type, "ec"); + bool ed = sp_is_str(type, "ed25519"); + const ScrDyn *curve_name = sp_opt(input, "namedCurve"); + if (ec && curve_name->kind != SCR_DYN_STR) { + scr_dyn_prop_type_fail("key.namedCurve", "of type string", curve_name); + return NULL; + } + if (sp_is_str(type, "rsa") || sp_is_str(type, "rsa-pss") || sp_is_str(type, "dsa") || sp_is_str(type, "dh")) { + sp_incompatible_key_type(); + return NULL; + } + bool x = sp_is_str(type, "x25519"); + bool other = false; + for (size_t i = 0; i < sizeof sp_other_named_types / sizeof sp_other_named_types[0]; i++) + other = other || sp_is_str(type, sp_other_named_types[i]); + if (!ec && !ed && !x && !other) { + ScrStr *s = type->v.str; + scr_pki_throw_fmt(SCR_ERR_TYPE, "ERR_INVALID_ARG_VALUE", "Invalid asymmetricKeyType: %.*s", (int)s->len, s->data); + return NULL; + } + bool ml = other && type->v.str->len > 3 && memcmp(type->v.str->data, "ml-", 3) == 0; + if (seed && !ml) { + sp_incompatible_key_type(); + return NULL; + } + if (other) { + sp_not_supported("raw keys of this asymmetricKeyType"); + return NULL; + } + const SpCurve *curve = NULL; + if (ec && !(curve = sp_curve_named(curve_name))) { + sp_throw(SCR_ERR_TYPE, "ERR_CRYPTO_INVALID_CURVE", "Invalid EC curve name"); + return NULL; + } + ScrBytes *bytes = scr_bytes_buffer_source(data); + if (!bytes) return NULL; + const unsigned char *p = bytes->data; + size_t len = (size_t)scr_bytes_byte_len(bytes); + SpKey *k = sp_key_alloc(!raw_public); + bool ok = false; + if (x) { + k->x25519 = true; + if (len == 32 && raw_public) { + memcpy(k->ed_pub, p, 32); + ok = true; + } else if (len == 32) { + memcpy(k->ed_seed, p, 32); + ok = sp_x25519(k->ed_seed, NULL, k->ed_pub); + } + } else if (ed) { + k->ed25519 = true; + if (len == 32 && raw_public) { + memcpy(k->ed_pub, p, 32); + ok = true; + } else if (len == 32) { + const ScrPkiEd25519 *eds = sp_ed(); + if (!eds) { + sp_key_release(k); + scr_bytes_release(bytes); + return NULL; + } + memcpy(k->ed_seed, p, 32); + eds->public_key(k->ed_pub, k->ed_seed); + ok = true; + } + } else if (raw_public && len == 1 && p[0] == 0) { + sp_key_release(k); + scr_bytes_release(bytes); + sp_not_supported("EC public keys at the point at infinity"); + return NULL; + } else if (mbedtls_pk_setup(&k->pk, mbedtls_pk_info_from_type(MBEDTLS_PK_ECKEY)) == 0) { + mbedtls_ecp_keypair *kp = mbedtls_pk_ec(k->pk); + mbedtls_ecp_group *grp = &kp->MBEDTLS_PRIVATE(grp); + ok = mbedtls_ecp_group_load(grp, curve->id) == 0; + if (ok && raw_public) { + ok = mbedtls_ecp_point_read_binary(grp, &kp->MBEDTLS_PRIVATE(Q), p, len) == 0 && + mbedtls_ecp_check_pubkey(grp, &kp->MBEDTLS_PRIVATE(Q)) == 0; + } else if (ok) { + ok = len == (grp->nbits + 7) / 8 && mbedtls_mpi_read_binary(&kp->MBEDTLS_PRIVATE(d), p, len) == 0; + if (ok && mbedtls_ecp_check_privkey(grp, &kp->MBEDTLS_PRIVATE(d)) != 0) { + sp_key_release(k); + scr_bytes_release(bytes); + sp_not_supported("EC private keys outside 1 to n - 1"); + return NULL; + } + ok = ok && mbedtls_ecp_mul(grp, &kp->MBEDTLS_PRIVATE(Q), &kp->MBEDTLS_PRIVATE(d), &grp->G, sp_random, NULL) == 0; + } + } + scr_bytes_release(bytes); + if (!ok) { + sp_key_release(k); + sp_invalid_key_data(); + return NULL; + } + return k; +} + /* ── export ───────────────────────────────────────────────────────────── */ /* export({ format, type }) per Node's parseKeyEncoding: format "pem", @@ -975,12 +2228,44 @@ static ScrDyn *sp_export(SpKey *k, const ScrDyn *options) { } static ScrDyn *sp_export_plain(SpKey *k, const ScrDyn *options) { + const ScrDyn *format = sp_opt(options, "format"); + /* A private key's passphrase applies only to PEM and DER. */ + if (k->private_key && !sp_absent(sp_opt(options, "passphrase")) && !sp_is_str(format, "pem") && + !sp_is_str(format, "der")) { + ScrStr *text = scr_dyn_string_coerce_js(format); + if (!text) return NULL; + scr_pki_throw_fmt(SCR_ERR_ERROR, "ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS", + "The selected key encoding %.*s does not support encryption.", (int)text->len, text->data); + scr_str_release(text); + return NULL; + } + if (!k->private_key && sp_is_str(format, "raw-public")) { + const ScrDyn *type = sp_opt(options, "type"); + bool compressed = sp_is_str(type, "compressed"); + /* Only an EC point has a compressed form. */ + if (sp_is_ec(k) && !sp_absent(type) && !compressed && !sp_is_str(type, "uncompressed")) { + ScrStr *received = sp_received(type, false); + scr_pki_throw_fmt(SCR_ERR_TYPE, "ERR_INVALID_ARG_VALUE", + "The property 'options.type' must be one of: 'compressed', 'uncompressed'. Received %.*s", + (int)received->len, received->data); + scr_str_release(received); + return NULL; + } + return sp_export_raw(k, false, false, sp_is_ec(k) && compressed); + } + if (k->private_key && (sp_is_str(format, "raw-private") || sp_is_str(format, "raw-seed"))) + return sp_export_raw(k, true, sp_is_str(format, "raw-seed"), false); if (options == NULL || !sp_is_object(options)) { scr_dyn_arg_type_fail("options", "of type object", options ? options : scr_dyn_undefined()); return NULL; } - const ScrDyn *format = sp_opt(options, "format"); - if (sp_is_str(format, "jwk")) return sp_export_jwk(k); + if (sp_is_str(format, "jwk")) { + if (k->rsa_pss) { + sp_throw(SCR_ERR_ERROR, "ERR_CRYPTO_JWK_UNSUPPORTED_KEY_TYPE", "Unsupported JWK Key Type."); + return NULL; + } + return sp_export_jwk(k); + } bool pem = sp_is_str(format, "pem"); if (!pem && !sp_is_str(format, "der")) { sp_property_invalid("options.format", format); @@ -995,11 +2280,7 @@ static ScrDyn *sp_export_plain(SpKey *k, const ScrDyn *options) { sp_property_invalid("options.type", type); return NULL; } - if (!sp_absent(sp_opt(options, "cipher")) || !sp_absent(sp_opt(options, "passphrase"))) { - sp_not_supported("encrypted private key export"); - return NULL; - } - if (pkcs1 && !sp_is_rsa(k)) { + if (pkcs1 && (!sp_is_rsa(k) || k->rsa_pss)) { sp_incompatible("pkcs1", "can only be used for RSA keys"); return NULL; } @@ -1007,6 +2288,30 @@ static ScrDyn *sp_export_plain(SpKey *k, const ScrDyn *options) { sp_incompatible("sec1", "can only be used for EC keys"); return NULL; } + /* parseKeyEncoding's cipher and passphrase checks, then the cipher. */ + const ScrDyn *cipher = sp_opt(options, "cipher"), *passphrase = sp_opt(options, "passphrase"); + const SpKeyCipher *ci = NULL; + if (k->private_key) { + bool encrypted = !sp_absent(cipher) && cipher->kind != SCR_DYN_NULL; + if (encrypted) { + if (cipher->kind != SCR_DYN_STR) { + sp_property_invalid("options.cipher", cipher); + return NULL; + } + if (!pem && (pkcs1 || sec1)) { + sp_incompatible(pkcs1 ? "pkcs1" : "sec1", "does not support encryption"); + return NULL; + } + if (passphrase->kind != SCR_DYN_STR && passphrase->kind != SCR_DYN_BYTES && !scr_buffer_storage_is(passphrase)) { + sp_property_invalid("options.passphrase", passphrase); + return NULL; + } + if (!sp_key_cipher(cipher->v.str, &ci)) return NULL; + } else if (!sp_absent(passphrase)) { + sp_property_invalid_because("options.cipher", cipher, "is required when a passphrase is specified"); + return NULL; + } + } unsigned char *der = malloc(SP_DER_MAX); if (!der) scr_trap("scriptc: out of memory\n"); int n; @@ -1027,6 +2332,26 @@ static ScrDyn *sp_export_plain(SpKey *k, const ScrDyn *options) { ScrDyn *result = NULL; if (n < 0) { sp_unsupported_input(); + } else if (ci && !sp_key_cipher_ready(ci, pkcs8, (size_t)n)) { + /* OpenSSL's error, raised once the key is encoded. */ + } else if (ci) { + const unsigned char *start = der + SP_DER_MAX - n; + ScrBytes *pass = sp_source_bytes(passphrase, sp_opt(options, "encoding")); + const unsigned char *pass_data = pass ? pass->data : (const unsigned char *)""; + size_t pass_len = pass ? (size_t)scr_bytes_byte_len(pass) : 0; + if (pkcs8) { + size_t enc_len; + unsigned char *enc = sp_encrypt_pkcs8(start, (size_t)n, ci, pass_data, pass_len, &enc_len); + if (enc) { + result = pem ? sp_dyn_str(sp_pem("ENCRYPTED PRIVATE KEY", enc, enc_len)) : sp_dyn_buffer(enc, enc_len); + free(enc); + } + } else { + ScrStr *text = sp_encrypted_pem(label, start, (size_t)n, ci, pass_data, pass_len); + if (text) result = sp_dyn_str(text); + } + if (pass) scr_bytes_release(pass); + if (!result) sp_unsupported_input(); } else { const unsigned char *start = der + SP_DER_MAX - n; result = pem ? sp_dyn_str(sp_pem(label, start, (size_t)n)) : sp_dyn_buffer(start, (size_t)n); @@ -1040,7 +2365,7 @@ static ScrDyn *sp_export_plain(SpKey *k, const ScrDyn *options) { static ScrDyn *sp_key_details(SpKey *k) { ScrDyn *details = scr_dyn_new_obj(); - if (sp_is_ed(k)) return details; + if (sp_is_okp(k)) return details; if (sp_is_rsa(k)) { mbedtls_rsa_context *rsa = mbedtls_pk_rsa(k->pk); mbedtls_mpi e; @@ -1056,6 +2381,12 @@ static ScrDyn *sp_key_details(SpKey *k) { ScrBigInt *big = scr_bigint_from_u64(exponent); sp_set(details, "publicExponent", scr_dyn_new_bigint(big)); scr_bigint_release(big); + if (k->pss_params) { + const char *md = sp_pss_md_name(k->pss_md), *mgf1 = sp_pss_md_name(k->pss_mgf1); + sp_set(details, "hashAlgorithm", sp_dyn_str(scr_str_new(md, strlen(md)))); + sp_set(details, "mgf1HashAlgorithm", sp_dyn_str(scr_str_new(mgf1, strlen(mgf1)))); + sp_set(details, "saltLength", scr_dyn_new_num(k->pss_salt)); + } } else { const SpCurve *curve = sp_curve_of(k); sp_set(details, "namedCurve", sp_dyn_str(scr_str_new(curve->name, strlen(curve->name)))); @@ -1066,7 +2397,7 @@ static ScrDyn *sp_key_details(SpKey *k) { /* The DER that identifies a key for equals(): its private encoding for a * private key, its SubjectPublicKeyInfo otherwise. */ static int sp_identity_der(SpKey *k, unsigned char *buf, size_t size) { - if (sp_is_ed(k)) return k->private_key ? sp_write_pkcs8_der(k, buf, size) : sp_write_spki_der(k, buf, size); + if (sp_is_okp(k)) return k->private_key ? sp_write_pkcs8_der(k, buf, size) : sp_write_spki_der(k, buf, size); return k->private_key ? sp_write_private_der(k, buf, size) : sp_write_spki_der(k, buf, size); } @@ -1112,6 +2443,8 @@ static ScrDyn *sp_key_get(void *h, const char *name, size_t len) { return k->private_key ? sp_dyn_str(scr_str_new("private", 7)) : sp_dyn_str(scr_str_new("public", 6)); if (len == 17 && memcmp(name, "asymmetricKeyType", 17) == 0) return sp_is_ed(k) ? sp_dyn_str(scr_str_new("ed25519", 7)) + : sp_is_x(k) ? sp_dyn_str(scr_str_new("x25519", 6)) + : k->rsa_pss ? sp_dyn_str(scr_str_new("rsa-pss", 7)) : sp_is_rsa(k) ? sp_dyn_str(scr_str_new("rsa", 3)) : sp_dyn_str(scr_str_new("ec", 2)); if (len == 20 && memcmp(name, "asymmetricKeyDetails", 20) == 0) return sp_key_details(k); @@ -1227,7 +2560,7 @@ static bool sp_check_encoding_plain(const ScrDyn *enc, const char *name, const S /* Node's parseKeyEncoding for options.publicKeyEncoding or * options.privateKeyEncoding of a `type` key: true when the encoding is - * undefined, null or valid, false with the error pending. A valid raw or + * undefined, null or valid, false with the error pending. A valid * encrypted encoding, which this runtime does not write, sets *refusal * when it is unset. */ static bool sp_check_encoding(const ScrDyn *options, const char *key, const ScrDyn *type, bool private_key, @@ -1323,43 +2656,129 @@ static bool sp_check_encoding_plain(const ScrDyn *enc, const char *name, const S return false; } } - if (!*refusal && (raw_public || raw_private)) *refusal = "generateKeyPair with a raw key encoding"; - if (!*refusal && encrypted) *refusal = "encrypted private key export"; + (void)encrypted; return true; } /* The key generateKeyPair makes, as sp_keygen_check reads it. */ typedef struct { - enum { SP_GEN_RSA, SP_GEN_EC, SP_GEN_ED25519 } kind; + enum { SP_GEN_RSA, SP_GEN_EC, SP_GEN_ED25519, SP_GEN_X25519 } kind; double bits, exponent; /* RSA */ + bool pss; /* an RSA-PSS key, restricted when pss_params */ + bool pss_params, md_set, mgf1_set, salt_set; + mbedtls_md_type_t md, mgf1; + int salt; const SpCurve *curve; /* EC */ } SpKeyGen; -/* The key types Node generates that this runtime does not, apart from - * rsa-pss, dsa and dh: Node's named key pairs. */ -static const char *const sp_other_named_types[] = { - "ed448", - "x25519", - "x448", - "ml-dsa-44", - "ml-dsa-65", - "ml-dsa-87", - "ml-kem-512", - "ml-kem-768", - "ml-kem-1024", - "slh-dsa-sha2-128f", - "slh-dsa-sha2-128s", - "slh-dsa-sha2-192f", - "slh-dsa-sha2-192s", - "slh-dsa-sha2-256f", - "slh-dsa-sha2-256s", - "slh-dsa-shake-128f", - "slh-dsa-shake-128s", - "slh-dsa-shake-192f", - "slh-dsa-shake-192s", - "slh-dsa-shake-256f", - "slh-dsa-shake-256s", -}; + +static bool sp_digest_of(const ScrStr *name, mbedtls_md_type_t *md); + +/* A digest name of rsa-pss's options: true with *md set, or false with + * Node's ERR_CRYPTO_INVALID_DIGEST pending ("Invalid digest: x" or "Invalid + * MGF1 digest: x"), or the SC2020 refusal for a digest OpenSSL has and + * RSASSA-PSS-params here cannot name. */ +static bool sp_pss_digest(const ScrDyn *name, bool mgf1, mbedtls_md_type_t *md) { + if (sp_digest_of(name->v.str, md) && sp_pss_md_name(*md)) return true; + if (scr_digest_known(name->v.str->data, name->v.str->len)) { + sp_not_supported("generateKeyPair rsa-pss with this digest"); + return false; + } + scr_pki_throw_fmt(SCR_ERR_TYPE, "ERR_CRYPTO_INVALID_DIGEST", "Invalid %sdigest: %.*s", mgf1 ? "MGF1 " : "", + (int)name->v.str->len, name->v.str->data); + return false; +} + +/* rsa-pss's options after the modulus and exponent, in Node's order + * (lib/internal/crypto/keygen.js): saltLength, hashAlgorithm, + * mgf1HashAlgorithm, and the deprecated hash and mgf1Hash with their + * DEP0154 warnings, then the digest lookups of RsaKeyGenTraits. */ +static bool sp_pss_keygen_options(const ScrDyn *options, SpKeyGen *gen) { + const ScrDyn *salt = sp_opt(options, "saltLength"); + const ScrDyn *hash_alg = sp_opt(options, "hashAlgorithm"), *mgf1_alg = sp_opt(options, "mgf1HashAlgorithm"); + const ScrDyn *hash = sp_opt(options, "hash"), *mgf1_hash = sp_opt(options, "mgf1Hash"); + if (!sp_absent(salt)) { + if (salt->kind != SCR_DYN_NUM) { + scr_dyn_prop_type_fail("options.saltLength", "of type number", salt); + return false; + } + double x = salt->v.num; + bool integer = isfinite(x) && trunc(x) == x; + if (!integer || x < 0 || x > 2147483647.0) { + ScrStr *received = sp_received(salt, true); + scr_pki_throw_fmt(SCR_ERR_RANGE, "ERR_OUT_OF_RANGE", + "The value of \"options.saltLength\" is out of range. It must be %s. Received %.*s", + integer ? ">= 0 && <= 2147483647" : "an integer", (int)received->len, received->data); + scr_str_release(received); + return false; + } + gen->salt_set = true; + gen->salt = (int)x; + } + if (!sp_absent(hash_alg) && hash_alg->kind != SCR_DYN_STR) { + scr_dyn_prop_type_fail("options.hashAlgorithm", "of type string", hash_alg); + return false; + } + if (!sp_absent(mgf1_alg) && mgf1_alg->kind != SCR_DYN_STR) { + scr_dyn_prop_type_fail("options.mgf1HashAlgorithm", "of type string", mgf1_alg); + return false; + } + const struct { + const ScrDyn *value, *preferred; + const char *name, *preferred_name; + } deprecated[] = { + {hash, hash_alg, "options.hash", "options.hashAlgorithm"}, + {mgf1_hash, mgf1_alg, "options.mgf1Hash", "options.mgf1HashAlgorithm"}, + }; + for (size_t i = 0; i < 2; i++) { + if (sp_absent(deprecated[i].value)) continue; + ScrJsonBuf text; + scr_jb_init(&text); + scr_jb_puts(&text, "\""); + scr_jb_puts(&text, deprecated[i].name); + scr_jb_puts(&text, "\" is deprecated, use \""); + scr_jb_puts(&text, deprecated[i].preferred_name); + scr_jb_puts(&text, "\" instead."); + /* process.emitWarning: reported on the next tick, dropped under + * process.noDeprecation, and thrown from its own tick under + * process.throwDeprecation. */ + ScrStr *message = scr_jb_finish(&text); + ScrStr *code = scr_str_new("DEP0154", 7); + ScrDyn *warning = scr_warning_new(message, "DeprecationWarning", code); + scr_str_release(code); + scr_str_release(message); + scr_warning_emit_deferred(warning); + scr_dyn_release(warning); + if (deprecated[i].value->kind != SCR_DYN_STR) { + scr_dyn_prop_type_fail(deprecated[i].name, "of type string", deprecated[i].value); + return false; + } + const ScrDyn *preferred = deprecated[i].preferred; + if (preferred->kind == SCR_DYN_STR && preferred->v.str->len > 0 && + !scr_str_eq(preferred->v.str, deprecated[i].value->v.str)) { + sp_property_invalid(deprecated[i].name, deprecated[i].value); + return false; + } + } + /* `hashAlgorithm || hash`: an empty string reads as absent. */ + const ScrDyn *md = hash_alg->kind == SCR_DYN_STR && hash_alg->v.str->len > 0 ? hash_alg : hash; + const ScrDyn *mgf1 = mgf1_alg->kind == SCR_DYN_STR && mgf1_alg->v.str->len > 0 ? mgf1_alg : mgf1_hash; + if (md->kind == SCR_DYN_STR) { + if (!sp_pss_digest(md, false, &gen->md)) return false; + gen->md_set = true; + } + if (mgf1->kind == SCR_DYN_STR) { + if (!sp_pss_digest(mgf1, true, &gen->mgf1)) return false; + gen->mgf1_set = true; + } + /* OpenSSL's defaults: MGF1 over the digest and a salt of the digest's + * length once a digest is given, SHA-1 and 20 otherwise. */ + gen->pss_params = gen->md_set || gen->mgf1_set || gen->salt_set; + if (!gen->md_set) gen->md = MBEDTLS_MD_SHA1; + if (!gen->mgf1_set) gen->mgf1 = gen->md; + if (!gen->salt_set) gen->salt = gen->md_set ? (int)mbedtls_md_get_size(mbedtls_md_info_from_type(gen->md)) : 20; + return true; +} /* createJob's checks for generateKeyPair(Sync)(type, options) in Node's * order: the type, the options object, the public and private key @@ -1386,7 +2805,8 @@ static bool sp_keygen_check(const ScrDyn *type, const ScrDyn *options, SpKeyGen if (!sp_absent(exponent) && exponent->kind != SCR_DYN_NULL && !sp_check_uint32(exponent, "options.publicExponent", &gen->exponent)) return false; - if (pss && !refusal) refusal = "generateKeyPairSync for this key type"; + gen->pss = pss; + if (pss && !sp_pss_keygen_options(options, gen)) return false; } else if (sp_is_str(type, "dsa") || sp_is_str(type, "dh")) { if (!sp_check_object(options, "options")) return false; if (!refusal) refusal = "generateKeyPairSync for this key type"; @@ -1416,6 +2836,8 @@ static bool sp_keygen_check(const ScrDyn *type, const ScrDyn *options, SpKeyGen } } else if (sp_is_str(type, "ed25519")) { gen->kind = SP_GEN_ED25519; + } else if (sp_is_str(type, "x25519")) { + gen->kind = SP_GEN_X25519; } else { bool named = false; for (size_t i = 0; i < sizeof sp_other_named_types / sizeof sp_other_named_types[0]; i++) @@ -1430,6 +2852,23 @@ static bool sp_keygen_check(const ScrDyn *type, const ScrDyn *options, SpKeyGen } if (!refusal) refusal = "generateKeyPairSync for this key type"; } + /* The job's private key cipher, which Node looks up once the options + * of the type are read. */ + { + const ScrDyn *enc = sp_opt(options, "privateKeyEncoding"); + const ScrDyn *cipher = sp_is_object(enc) ? sp_opt(enc, "cipher") : scr_dyn_undefined(); + const SpKeyCipher *ci; + if (cipher->kind == SCR_DYN_STR) { + if (!sp_key_cipher(cipher->v.str, &ci)) return false; + /* OpenSSL's errors for the cipher come from the export once the + * key is generated; a cipher it encrypts with and keys here do not + * is refused first. */ + bool pkcs8 = sp_is_str(sp_opt(enc, "type"), "pkcs8"); + if (ci->type == MBEDTLS_CIPHER_NONE && (pkcs8 ? ci->pkcs8_error : ci->traditional_error) == SP_KC_OK && + !refusal) + refusal = "private keys encrypted with this cipher"; + } + } if (refusal) { sp_not_supported(refusal); return false; @@ -1437,6 +2876,54 @@ static bool sp_keygen_check(const ScrDyn *type, const ScrDyn *options, SpKeyGen return gen->kind != SP_GEN_ED25519 || sp_ed() != NULL; } +/* An RSA key of `bits` bits with public exponent `exponent`, within + * OpenSSL's limits rather than mbedtls_rsa_gen_key's (even lengths from + * 1024 bits, exponents up to INT_MAX): any length from 512 bits and odd + * exponents up to 2^32 - 1. From 2048 bits OpenSSL generates two primes + * of equal length (SP 800-56B), so an odd length yields one bit less. + * The primes have (bits + 1) / 2 and bits / 2 bits, each with its top + * bits set (mbedtls_mpi_gen_prime's FIPS 186-4 bound), so their product + * has exactly `bits` bits; each prime minus one is coprime to the + * exponent. 0 on success. */ +static int sp_rsa_generate(mbedtls_rsa_context *rsa, unsigned bits, uint64_t exponent) { + if (bits >= 2048) bits &= ~1u; + mbedtls_mpi P, Q, E, N, T, G; + mbedtls_mpi_init(&P); + mbedtls_mpi_init(&Q); + mbedtls_mpi_init(&E); + mbedtls_mpi_init(&N); + mbedtls_mpi_init(&T); + mbedtls_mpi_init(&G); + unsigned char e_bytes[8]; + for (int i = 0; i < 8; i++) e_bytes[i] = (unsigned char)(exponent >> (56 - 8 * i)); + int r = mbedtls_mpi_read_binary(&E, e_bytes, sizeof e_bytes); + const int flags = MBEDTLS_MPI_GEN_PRIME_FLAG_LOW_ERR; + while (r == 0) { + r = mbedtls_mpi_gen_prime(&P, (bits + 1) / 2, flags, sp_random, NULL); + if (r == 0) r = mbedtls_mpi_sub_int(&T, &P, 1); + if (r == 0) r = mbedtls_mpi_gcd(&G, &E, &T); + if (r != 0 || mbedtls_mpi_cmp_int(&G, 1) != 0) continue; + r = mbedtls_mpi_gen_prime(&Q, bits / 2, flags, sp_random, NULL); + if (r == 0) r = mbedtls_mpi_sub_int(&T, &Q, 1); + if (r == 0) r = mbedtls_mpi_gcd(&G, &E, &T); + if (r != 0 || mbedtls_mpi_cmp_int(&G, 1) != 0 || mbedtls_mpi_cmp_mpi(&P, &Q) == 0) continue; + r = mbedtls_mpi_mul_mpi(&N, &P, &Q); + if (r == 0 && mbedtls_mpi_bitlen(&N) == bits) break; + } + /* The larger prime is p, as OpenSSL orders them. */ + if (r == 0 && mbedtls_mpi_cmp_mpi(&P, &Q) < 0) mbedtls_mpi_swap(&P, &Q); + if (r == 0) r = mbedtls_rsa_import(rsa, &N, &P, &Q, NULL, &E); + if (r == 0) r = mbedtls_rsa_complete(rsa); + if (r == 0) r = mbedtls_rsa_check_privkey(rsa); + mbedtls_mpi_free(&P); + mbedtls_mpi_free(&Q); + mbedtls_mpi_free(&E); + mbedtls_mpi_free(&N); + mbedtls_mpi_free(&T); + mbedtls_mpi_free(&G); + return r; +} + /* OpenSSL's error for an RSA modulus length or public exponent it does not * generate, as Node reports it, or NULL. */ static const char *sp_rsa_keygen_failure(double bits, double exponent) { @@ -1458,6 +2945,20 @@ static SpKey *sp_keygen_run(const SpKeyGen *gen, bool *job_error) { scr_pki_ed25519->public_key(k->ed_pub, k->ed_seed); return k; } + if (gen->kind == SP_GEN_X25519) { + /* OpenSSL stores a generated X25519 key clamped. */ + k->x25519 = true; + sp_random(NULL, k->ed_seed, 32); + k->ed_seed[0] &= 248; + k->ed_seed[31] &= 127; + k->ed_seed[31] |= 64; + if (!sp_x25519(k->ed_seed, NULL, k->ed_pub)) { + sp_key_release(k); + sp_unsupported_input(); + return NULL; + } + return k; + } if (gen->kind == SP_GEN_RSA) { const char *failure = sp_rsa_keygen_failure(gen->bits, gen->exponent); if (failure) { @@ -1466,16 +2967,22 @@ static SpKey *sp_keygen_run(const SpKeyGen *gen, bool *job_error) { scr_pki_throw_fmt(SCR_ERR_ERROR, NULL, "%s", failure); return NULL; } - /* mbedTLS generates even modulus lengths from 1024 to 8192 bits and, - * past the checks above, odd exponents up to INT_MAX. */ + /* Moduli past mbedTLS's 8192-bit numbers stay unsupported (OpenSSL + * generates up to 16384 bits). */ double b = gen->bits, e = gen->exponent; - if (b < MBEDTLS_RSA_GEN_KEY_MIN_BITS || b > MBEDTLS_MPI_MAX_BITS || fmod(b, 2) != 0 || e > 2147483647.0 || - mbedtls_pk_setup(&k->pk, mbedtls_pk_info_from_type(MBEDTLS_PK_RSA)) != 0 || - mbedtls_rsa_gen_key(mbedtls_pk_rsa(k->pk), sp_random, NULL, (unsigned)b, (int)e) != 0) { + if (b > MBEDTLS_MPI_MAX_BITS || mbedtls_pk_setup(&k->pk, mbedtls_pk_info_from_type(MBEDTLS_PK_RSA)) != 0 || + sp_rsa_generate(mbedtls_pk_rsa(k->pk), (unsigned)b, (uint64_t)e) != 0) { sp_key_release(k); sp_not_supported("generateKeyPairSync with this RSA modulus length or exponent"); return NULL; } + if (gen->pss) { + k->rsa_pss = true; + k->pss_params = gen->pss_params; + k->pss_md = gen->md; + k->pss_mgf1 = gen->mgf1; + k->pss_salt = gen->salt; + } return k; } if (mbedtls_pk_setup(&k->pk, mbedtls_pk_info_from_type(MBEDTLS_PK_ECKEY)) != 0 || @@ -1492,7 +2999,7 @@ static SpKey *sp_keygen_run(const SpKeyGen *gen, bool *job_error) { * pending, and *job_error as sp_keygen_run sets it. */ static ScrDyn *sp_key_pair(const ScrDyn *type, const ScrDyn *options, bool *job_error) { *job_error = false; - SpKeyGen gen; + SpKeyGen gen = {0}; if (!sp_keygen_check(type, options, &gen)) return NULL; SpKey *k = sp_keygen_run(&gen, job_error); if (!k) return NULL; @@ -1502,14 +3009,18 @@ static ScrDyn *sp_key_pair(const ScrDyn *type, const ScrDyn *options, bool *job_ sp_unsupported_input(); return NULL; } + /* An encoding the key type has no form for (a raw encoding of an RSA + * key, raw-seed) fails the job, as in Node. */ ScrDyn *public_value = sp_encoded(pub, sp_opt(options, "publicKeyEncoding")); if (!public_value) { sp_key_release(k); + *job_error = true; return NULL; } ScrDyn *private_value = sp_encoded(k, sp_opt(options, "privateKeyEncoding")); if (!private_value) { scr_dyn_release(public_value); + *job_error = true; return NULL; } ScrDyn *pair = scr_dyn_new_obj(); @@ -1599,8 +3110,11 @@ static bool sp_algorithm(const ScrDyn *alg, mbedtls_md_type_t *md) { typedef struct { SpKey *key; /* +1 */ int padding; /* RSA_PKCS1_PADDING (1) or RSA_PKCS1_PSS_PADDING (6) */ - int salt; /* -1 digest length, -2 maximum (sign) or auto (verify) */ + int salt; /* -1 digest length, -2 maximum (sign) or auto (verify), + * -4 the digest length or the maximum, if smaller */ bool p1363; /* dsaEncoding: "ieee-p1363" */ + bool padding_given, salt_given; + mbedtls_md_type_t mgf1; /* PSS's MGF1 digest; NONE for the message's */ } SpSigKey; /* The key argument of a signing operation: a KeyObject, key material, or @@ -1621,6 +3135,9 @@ static bool sp_sig_key_plain(const ScrDyn *value, bool sign, SpSigKey *out) { out->padding = 1; out->salt = sign ? -2 : -2; out->p1363 = false; + out->padding_given = false; + out->salt_given = false; + out->mgf1 = MBEDTLS_MD_NONE; const ScrDyn *options = NULL; if (sp_is_object(value) && !sp_key_of(value) && !scr_buffer_storage_is(value)) { options = value; @@ -1637,6 +3154,7 @@ static bool sp_sig_key_plain(const ScrDyn *value, bool sign, SpSigKey *out) { return false; } out->padding = (int)padding->v.num; + out->padding_given = true; } const ScrDyn *salt = sp_opt(value, "saltLength"); if (!sp_absent(salt)) { @@ -1645,6 +3163,7 @@ static bool sp_sig_key_plain(const ScrDyn *value, bool sign, SpSigKey *out) { return false; } out->salt = (int)salt->v.num; + out->salt_given = true; } } SpKey *direct = sp_key_of(options ? sp_opt(options, "key") : value); @@ -1711,6 +3230,43 @@ static int sp_p1363_to_der(const unsigned char *sig, size_t len, unsigned char * static size_t sp_ec_size(SpKey *k) { return (mbedtls_pk_ec(k->pk)->MBEDTLS_PRIVATE(grp).nbits + 7) / 8; } +/* An RSA-PSS key's restrictions on a signature over a `md` digest, as + * OpenSSL's provider applies them: PSS padding only (the default), and + * for a key with RSASSA-PSS-params its digest, its MGF1 digest and salts + * of at least its own (the default). Without parameters a signature + * salts with the digest's length, or the most the key holds when that is + * less (RSA_PSS_SALTLEN_AUTO_DIGEST_MAX). False with OpenSSL's error + * pending. */ +static bool sp_pss_key_rules(SpSigKey *key, mbedtls_md_type_t md, size_t hash_len, bool sign) { + const SpKey *k = key->key; + if (!k->rsa_pss) return true; + if (key->padding_given && key->padding != 6) { + sp_throw_openssl("1C8000A5", "Provider routines", "illegal or unsupported padding mode", + "ERR_OSSL_ILLEGAL_OR_UNSUPPORTED_PADDING_MODE", true); + return false; + } + key->padding = 6; + if (!k->pss_params) { + if (!key->salt_given) key->salt = sign ? -4 : -2; + return true; + } + if (md != k->pss_md) { + sp_throw_openssl("1C8000AE", "Provider routines", "digest not allowed", "ERR_OSSL_DIGEST_NOT_ALLOWED", true); + return false; + } + key->mgf1 = k->pss_mgf1; + /* RSA_PSS_SALTLEN_AUTO (-2) is exempt from the key's minimum, as in + * OpenSSL: verification recovers the salt length from the signature and + * accepts any length (corpus 8178). */ + if (!key->salt_given) { + key->salt = k->pss_salt; + } else if (key->salt != -2 && (key->salt == -1 ? (int)hash_len : key->salt) < k->pss_salt) { + sp_throw_openssl("1C8000AC", "Provider routines", "pss saltlen too small", "ERR_OSSL_PSS_SALTLEN_TOO_SMALL", true); + return false; + } + return true; +} + /* Applies the RSA padding of a signature operation. */ static bool sp_rsa_padding(SpSigKey *key, mbedtls_md_type_t md) { if (!sp_is_rsa(key->key)) return true; @@ -1719,11 +3275,119 @@ static bool sp_rsa_padding(SpSigKey *key, mbedtls_md_type_t md) { return mbedtls_rsa_set_padding(rsa, MBEDTLS_RSA_PKCS_V15, MBEDTLS_MD_NONE) == 0; } +/* MGF1 over `mgf1` (RFC 8017 B.2.1), XORed into mask[0, len). */ +static int sp_mgf1_xor(mbedtls_md_type_t mgf1, const unsigned char *seed, size_t seed_len, unsigned char *mask, + size_t len) { + const mbedtls_md_info_t *info = mbedtls_md_info_from_type(mgf1); + size_t h = mbedtls_md_get_size(info); + unsigned char block[MBEDTLS_MD_MAX_SIZE]; + mbedtls_md_context_t ctx; + mbedtls_md_init(&ctx); + int r = mbedtls_md_setup(&ctx, info, 0); + for (uint32_t counter = 0; r == 0 && (size_t)counter * h < len; counter++) { + unsigned char c[4] = {(unsigned char)(counter >> 24), (unsigned char)(counter >> 16), + (unsigned char)(counter >> 8), (unsigned char)counter}; + if ((r = mbedtls_md_starts(&ctx)) == 0 && (r = mbedtls_md_update(&ctx, seed, seed_len)) == 0 && + (r = mbedtls_md_update(&ctx, c, 4)) == 0 && (r = mbedtls_md_finish(&ctx, block)) == 0) { + for (size_t i = 0; i < h && (size_t)counter * h + i < len; i++) mask[(size_t)counter * h + i] ^= block[i]; + } + } + mbedtls_md_free(&ctx); + mbedtls_platform_zeroize(block, sizeof block); + return r; +} + +/* An RSASSA-PSS signature whose MGF1 digest differs from the message's + * (RFC 8017 9.1.1 and 8.1.1), which mbedtls_rsa_rsassa_pss_sign_ext + * cannot make: the encoding here, then the private-key operation. */ +static int sp_pss_sign_mgf1(mbedtls_rsa_context *rsa, mbedtls_md_type_t md, mbedtls_md_type_t mgf1, + const unsigned char *hash, size_t hash_len, int salt_len, unsigned char *sig) { + size_t bytes = mbedtls_rsa_get_len(rsa); + size_t em_bits = mbedtls_rsa_get_bitlen(rsa) - 1; + size_t em_len = (em_bits + 7) / 8; + size_t salt = (size_t)salt_len; + if (em_len < hash_len + salt + 2) return MBEDTLS_ERR_RSA_BAD_INPUT_DATA; + unsigned char em[MBEDTLS_MPI_MAX_SIZE] = {0}; + unsigned char *db = em, *h = em + em_len - hash_len - 1; + size_t db_len = em_len - hash_len - 1; + unsigned char salt_bytes[MBEDTLS_MPI_MAX_SIZE]; + int r = sp_random(NULL, salt_bytes, salt); + /* H = Hash(0x00 * 8 || mHash || salt) */ + mbedtls_md_context_t ctx; + mbedtls_md_init(&ctx); + static const unsigned char zeros[8] = {0}; + if (r == 0 && (r = mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(md), 0)) == 0 && + (r = mbedtls_md_starts(&ctx)) == 0 && (r = mbedtls_md_update(&ctx, zeros, 8)) == 0 && + (r = mbedtls_md_update(&ctx, hash, hash_len)) == 0 && (r = mbedtls_md_update(&ctx, salt_bytes, salt)) == 0) + r = mbedtls_md_finish(&ctx, h); + mbedtls_md_free(&ctx); + if (r == 0) { + /* DB = PS || 0x01 || salt, masked with MGF1(H). */ + db[db_len - salt - 1] = 0x01; + memcpy(db + db_len - salt, salt_bytes, salt); + r = sp_mgf1_xor(mgf1, h, hash_len, db, db_len); + } + if (r == 0) { + db[0] &= (unsigned char)(0xff >> (8 * em_len - em_bits)); + em[em_len - 1] = 0xbc; + unsigned char block[MBEDTLS_MPI_MAX_SIZE] = {0}; + memcpy(block + bytes - em_len, em, em_len); + r = mbedtls_rsa_private(rsa, sp_random, NULL, block, sig); + mbedtls_platform_zeroize(block, sizeof block); + } + mbedtls_platform_zeroize(em, sizeof em); + mbedtls_platform_zeroize(salt_bytes, sizeof salt_bytes); + return r; +} + +/* Verifies an RSASSA-PSS signature whose MGF1 digest differs from the + * message's (RFC 8017 9.1.2), which mbedtls_rsa_rsassa_pss_verify_ext + * cannot: it hashes M' with the MGF1 digest too. `salt_len` -2 accepts any + * salt length. */ +static bool sp_pss_verify_mgf1(mbedtls_rsa_context *rsa, mbedtls_md_type_t md, mbedtls_md_type_t mgf1, + const unsigned char *hash, size_t hash_len, int salt_len, + const unsigned char *sig, size_t sig_len) { + size_t bytes = mbedtls_rsa_get_len(rsa); + if (sig_len != bytes) return false; + unsigned char decoded[MBEDTLS_MPI_MAX_SIZE]; + if (mbedtls_rsa_public(rsa, sig, decoded) != 0) return false; + size_t em_bits = mbedtls_rsa_get_bitlen(rsa) - 1; + size_t em_len = (em_bits + 7) / 8; + if (bytes > em_len && decoded[0] != 0) return false; + unsigned char *em = decoded + (bytes - em_len); + if (em_len < hash_len + 2 || em[em_len - 1] != 0xbc) return false; + size_t db_len = em_len - hash_len - 1; + unsigned char *h = em + db_len; + unsigned char top = (unsigned char)(0xff >> (8 * em_len - em_bits)); + if (em[0] & (unsigned char)~top) return false; + unsigned char db[MBEDTLS_MPI_MAX_SIZE]; + memcpy(db, em, db_len); + if (sp_mgf1_xor(mgf1, h, hash_len, db, db_len) != 0) return false; + db[0] &= top; + size_t i = 0; + while (i < db_len && db[i] == 0) i++; + if (i == db_len || db[i] != 0x01) return false; + size_t salt = db_len - i - 1; + if (salt_len >= 0 && salt != (size_t)salt_len) return false; + static const unsigned char zeros[8] = {0}; + unsigned char check[MBEDTLS_MD_MAX_SIZE]; + mbedtls_md_context_t ctx; + mbedtls_md_init(&ctx); + int r; + if ((r = mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(md), 0)) == 0 && (r = mbedtls_md_starts(&ctx)) == 0 && + (r = mbedtls_md_update(&ctx, zeros, 8)) == 0 && (r = mbedtls_md_update(&ctx, hash, hash_len)) == 0 && + (r = mbedtls_md_update(&ctx, db + i + 1, salt)) == 0) + r = mbedtls_md_finish(&ctx, check); + mbedtls_md_free(&ctx); + return r == 0 && memcmp(check, h, hash_len) == 0; +} + /* Signs a digest: +1 Buffer or NULL with the error pending. */ static ScrDyn *sp_sign_hash(SpSigKey *key, mbedtls_md_type_t md, const unsigned char *hash, size_t hash_len) { unsigned char sig[MBEDTLS_PK_SIGNATURE_MAX_SIZE]; size_t sig_len = 0; int r; + if (!sp_pss_key_rules(key, md, hash_len, true)) return NULL; if (!sp_rsa_padding(key, md)) { sp_unsupported_input(); return NULL; @@ -1736,12 +3400,15 @@ static ScrDyn *sp_sign_hash(SpSigKey *key, mbedtls_md_type_t md, const unsigned size_t max = bytes - hash_len - 2 - ((bits - 1) % 8 == 0 ? 1 : 0); if (salt == -1) salt = (int)hash_len; else if (salt == -2) salt = (int)max; + else if (salt == -4) salt = (int)(hash_len < max ? hash_len : max); if (salt < 0 || (size_t)salt > max) { sp_throw(SCR_ERR_ERROR, "ERR_OSSL_RSA_DATA_TOO_LARGE_FOR_KEY_SIZE", "error:1C8000A5:Provider routines::data too large for key size"); return NULL; } - r = mbedtls_rsa_rsassa_pss_sign_ext(rsa, sp_random, NULL, md, (unsigned)hash_len, hash, salt, sig); + r = key->mgf1 != MBEDTLS_MD_NONE && key->mgf1 != md + ? sp_pss_sign_mgf1(rsa, md, key->mgf1, hash, hash_len, salt, sig) + : mbedtls_rsa_rsassa_pss_sign_ext(rsa, sp_random, NULL, md, (unsigned)hash_len, hash, salt, sig); sig_len = bytes; } else { r = mbedtls_pk_sign(&key->key->pk, md, hash, hash_len, sig, sizeof sig, &sig_len, sp_random, NULL); @@ -1766,6 +3433,7 @@ static ScrDyn *sp_sign_hash(SpSigKey *key, mbedtls_md_type_t md, const unsigned * not verify or does not parse. */ static bool sp_verify_hash(SpSigKey *key, mbedtls_md_type_t md, const unsigned char *hash, size_t hash_len, const unsigned char *sig, size_t sig_len) { + if (!sp_pss_key_rules(key, md, hash_len, false)) return false; if (!sp_rsa_padding(key, md)) return false; if (sp_is_rsa(key->key) && key->padding == 6) { mbedtls_rsa_context *rsa = mbedtls_pk_rsa(key->key->pk); @@ -1773,6 +3441,9 @@ static bool sp_verify_hash(SpSigKey *key, mbedtls_md_type_t md, const unsigned c int salt = key->salt; if (salt == -1) salt = (int)hash_len; else if (salt == -2) salt = MBEDTLS_RSA_SALT_LEN_ANY; + if (key->mgf1 != MBEDTLS_MD_NONE && key->mgf1 != md) + return sp_pss_verify_mgf1(rsa, md, key->mgf1, hash, hash_len, salt == MBEDTLS_RSA_SALT_LEN_ANY ? -2 : salt, + sig, sig_len); return mbedtls_rsa_rsassa_pss_verify_ext(rsa, md, (unsigned)hash_len, hash, md, salt, sig) == 0; } if (sp_is_ec(key->key) && key->p1363) { @@ -1785,6 +3456,16 @@ static bool sp_verify_hash(SpSigKey *key, mbedtls_md_type_t md, const unsigned c return mbedtls_pk_verify(&key->key->pk, md, hash, hash_len, sig, sig_len) == 0; } +/* X25519 keys neither sign nor verify: OpenSSL's error, after the key + * checks. Releases the key and answers true when it rejects. */ +static bool sp_reject_x25519(SpSigKey *k) { + if (!sp_is_x(k->key)) return false; + sp_key_release(k->key); + sp_throw_openssl("03000096", "digital envelope routines", "operation not supported for this keytype", + "ERR_OSSL_EVP_OPERATION_NOT_SUPPORTED_FOR_THIS_KEYTYPE", true); + return true; +} + static const char sp_data_types[] = "an instance of Buffer, TypedArray, or DataView"; /* Ed25519 signs the message itself: an algorithm other than null or @@ -1805,6 +3486,10 @@ ScrDyn *scr_crypto_sign_oneshot(ScrDyn *algorithm, ScrDyn *data, ScrDyn *key) { } SpSigKey k; if (!sp_sig_key(key, true, &k)) return NULL; + if (sp_reject_x25519(&k)) return NULL; + /* A null algorithm signs with an RSA-PSS key's own digest. */ + if ((algorithm->kind == SCR_DYN_UNDEF || algorithm->kind == SCR_DYN_NULL) && k.key->pss_params) + md = k.key->pss_md; ScrBytes *bytes = scr_bytes_buffer_source(data); if (sp_is_ed(k.key)) { ScrDyn *result = NULL; @@ -1842,6 +3527,9 @@ ScrDyn *scr_crypto_verify_oneshot(ScrDyn *algorithm, ScrDyn *data, ScrDyn *key, scr_dyn_arg_type_fail("signature", sp_data_types, signature); return NULL; } + if (sp_reject_x25519(&k)) return NULL; + if ((algorithm->kind == SCR_DYN_UNDEF || algorithm->kind == SCR_DYN_NULL) && k.key->pss_params) + md = k.key->pss_md; ScrBytes *bytes = scr_bytes_buffer_source(data); ScrBytes *sig = scr_bytes_buffer_source(signature); if (sp_is_ed(k.key)) { @@ -1863,7 +3551,7 @@ ScrDyn *scr_crypto_verify_oneshot(ScrDyn *algorithm, ScrDyn *data, ScrDyn *key, scr_bytes_release(bytes); scr_bytes_release(sig); sp_key_release(k.key); - return scr_dyn_new_bool(ok); + return scr_exc_pending() ? NULL : scr_dyn_new_bool(ok); } /* ── Sign and Verify objects ──────────────────────────────────────────── */ @@ -1928,6 +3616,7 @@ static ScrDyn *sp_signer_invoke(void *h, ScrDyn *self, const char *method, ScrDy } SpSigKey k; if (!sp_sig_key(argc ? args[0] : scr_dyn_undefined(), true, &k)) return NULL; + if (sp_reject_x25519(&k)) return NULL; if (sp_is_ed(k.key)) { sp_key_release(k.key); sp_throw(SCR_ERR_ERROR, "ERR_CRYPTO_UNSUPPORTED_OPERATION", "Unsupported crypto operation"); @@ -1965,6 +3654,7 @@ static ScrDyn *sp_signer_invoke(void *h, ScrDyn *self, const char *method, ScrDy signature); return NULL; } + if (sp_reject_x25519(&k)) return NULL; const ScrStr *encoding = sp_encoding_arg(argc > 2 ? args[2] : NULL); ScrDyn *encoding_value = encoding ? scr_dyn_new_str((ScrStr *)encoding) : NULL; ScrBytes *sig = sp_source_bytes(signature, encoding_value); @@ -1980,7 +3670,7 @@ static ScrDyn *sp_signer_invoke(void *h, ScrDyn *self, const char *method, ScrDy sig->data, (size_t)scr_bytes_byte_len(sig)); scr_bytes_release(sig); sp_key_release(k.key); - return scr_dyn_new_bool(ok); + return scr_exc_pending() ? NULL : scr_dyn_new_bool(ok); } scr_pki_throw_fmt(SCR_ERR_TYPE, NULL, "%s is not a function", what); return NULL; @@ -2131,6 +3821,160 @@ void scr_crypto_generate_key_pair_async(ScrDyn *type, ScrDyn *options, ScrDyn *c scr_loop_defer(c); } +/* ── diffieHellman ────────────────────────────────────────────────────── */ + +/* The keys an options.privateKey or options.publicKey value can be. */ +static const char sp_dh_private_types[] = + "of type string or an instance of ArrayBuffer, Buffer, TypedArray, DataView, KeyObject, CryptoKey, or URL"; +static const char sp_dh_public_types[] = + "of type string or an instance of ArrayBuffer, Buffer, TypedArray, DataView, KeyObject, or CryptoKey"; + +/* Node's message names: a KeyObject's asymmetricKeyType. */ +static const char *sp_type_name(const SpKey *k) { + return sp_is_ed(k) ? "ed25519" + : sp_is_x(k) ? "x25519" + : k->rsa_pss ? "rsa-pss" + : sp_is_rsa(k) ? "rsa" + : "ec"; +} + +/* A key input of diffieHellman's options: a primitive other than a string + * throws Node's ERR_INVALID_ARG_TYPE for `name`; other values read as + * sign and verify read their keys. */ +static bool sp_dh_key(const ScrDyn *value, const char *name, bool private_key, SpSigKey *out) { + out->key = NULL; + /* Node's key preparation destructures the value before it checks it. */ + if (value->kind == SCR_DYN_NULL) { + scr_pki_throw_fmt(SCR_ERR_TYPE, NULL, "Cannot destructure property 'key' of 'key' as it is null."); + return false; + } + bool keyish = value->kind == SCR_DYN_STR || value->kind == SCR_DYN_BYTES || scr_buffer_storage_is(value) || + (value->kind != SCR_DYN_NULL && value->kind != SCR_DYN_FUNC && scr_dyn_is_object(value)); + if (!keyish) { + scr_dyn_prop_type_fail(name, private_key ? sp_dh_private_types : sp_dh_public_types, value); + return false; + } + return sp_sig_key(value, private_key, out); +} + +/* The shared secret of two keys of one type: X25519, or ECDH's x + * coordinate at the field's size. NULL with OpenSSL's error pending. */ +static void sp_dh_failed(bool props) { + sp_throw_openssl("1C8000A4", "Provider routines", "failed during derivation", "ERR_OSSL_FAILED_DURING_DERIVATION", + props); +} + +static ScrDyn *sp_dh_compute(SpKey *priv, SpKey *pub, bool props) { + if (sp_is_x(priv) && sp_is_x(pub)) { + uint8_t out[32]; + if (!sp_x25519(priv->ed_seed, pub->ed_pub, out)) { + sp_dh_failed(props); + return NULL; + } + ScrDyn *secret = sp_dyn_buffer(out, 32); + mbedtls_platform_zeroize(out, sizeof out); + return secret; + } + if (!sp_is_ec(priv) || !sp_is_ec(pub)) { + sp_throw_openssl("03000096", "digital envelope routines", "operation not supported for this keytype", + "ERR_OSSL_EVP_OPERATION_NOT_SUPPORTED_FOR_THIS_KEYTYPE", props); + return NULL; + } + mbedtls_ecp_keypair *a = mbedtls_pk_ec(priv->pk); + const mbedtls_ecp_keypair *b = mbedtls_pk_ec(pub->pk); + if (a->MBEDTLS_PRIVATE(grp).id != b->MBEDTLS_PRIVATE(grp).id) { + sp_throw_openssl("1C8000CB", "Provider routines", "mismatching domain parameters", + "ERR_OSSL_MISMATCHING_DOMAIN_PARAMETERS", props); + return NULL; + } + mbedtls_mpi z; + mbedtls_mpi_init(&z); + unsigned char out[66]; + size_t n = (a->MBEDTLS_PRIVATE(grp).pbits + 7) / 8; + bool ok = mbedtls_ecdh_compute_shared(&a->MBEDTLS_PRIVATE(grp), &z, &b->MBEDTLS_PRIVATE(Q), + &a->MBEDTLS_PRIVATE(d), sp_random, NULL) == 0 && + mbedtls_mpi_write_binary(&z, out, n) == 0; + mbedtls_mpi_free(&z); + ScrDyn *secret = ok ? sp_dyn_buffer(out, n) : NULL; + mbedtls_platform_zeroize(out, sizeof out); + if (!secret) sp_dh_failed(props); + return secret; +} + +static void sp_dh_deliver(ScrClosure *self) { + ScrDyn *callback = sp_cap(self, 0); + ScrDyn *secret = sp_cap(self, 1); + ScrDyn *args[2] = {scr_dyn_new_null(), secret}; + ScrDyn *result = scr_dyn_call(callback, args, 2, "callback"); + if (result) scr_dyn_release(result); + scr_dyn_release(args[0]); + scr_dyn_release(secret); + scr_dyn_release(callback); +} + +/* crypto.diffieHellman(options[, callback]) in Node's order: the options + * object, the callback, both keys present, the key types when both are + * KeyObjects, then the public and private key inputs. With a callback, + * the secret, or the error of computing it, arrives in a later macrotask. */ +ScrDyn *scr_crypto_diffie_hellman(ScrDyn *options, ScrDyn *callback) { + ScrDyn *plain = sp_plain(options); + if (!plain) return NULL; + ScrDyn *result = NULL; + SpSigKey pub = {0}, priv = {0}; + if (!sp_check_object(plain, "options")) goto done; + bool async = !sp_absent(callback); + if (async && !scr_dyn_is_callable(callback)) { + scr_dyn_arg_type_fail("callback", "of type function", callback); + goto done; + } + const ScrDyn *private_value = sp_opt(plain, "privateKey"); + const ScrDyn *public_value = sp_opt(plain, "publicKey"); + if (sp_absent(private_value)) { + sp_property_invalid("options.privateKey", private_value); + goto done; + } + if (sp_absent(public_value)) { + sp_property_invalid("options.publicKey", public_value); + goto done; + } + SpKey *a = sp_key_of(private_value), *b = sp_key_of(public_value); + if (a && b && a->private_key) { + const char *ta = sp_type_name(a), *tb = sp_type_name(b); + if (strcmp(ta, tb) != 0 || (!sp_is_ec(a) && !sp_is_x(a))) { + scr_pki_throw_fmt(SCR_ERR_ERROR, "ERR_CRYPTO_INCOMPATIBLE_KEY", + "Incompatible key types for Diffie-Hellman: %s and %s", ta, tb); + goto done; + } + } + if (!sp_dh_key(public_value, "options.publicKey", false, &pub) || + !sp_dh_key(private_value, "options.privateKey", true, &priv)) + goto done; + ScrDyn *secret = sp_dh_compute(priv.key, pub.key, !async); + if (!async) { + result = secret; + goto done; + } + void *deliver = (void *)sp_dh_deliver; + if (!secret) { + ScrCaught *caught = scr_exc_take(); + secret = scr_caught_to_dyn(caught); + scr_caught_release(caught); + deliver = (void *)sp_key_pair_fail; + } + ScrClosure *c = scr_closure_new(deliver, 2); + c->caps[0] = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, NULL); + scr_box_set_ref(c->caps[0], scr_dyn_retain(callback)); + c->caps[1] = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, NULL); + scr_box_set_ref(c->caps[1], secret); + scr_loop_defer(c); + result = scr_dyn_retain(scr_dyn_undefined()); +done: + if (pub.key) sp_key_release(pub.key); + if (priv.key) sp_key_release(priv.key); + scr_dyn_release(plain); + return result; +} + /* ── the core for the WebCrypto layer (scr_pki.h) ─────────────────────── */ ScrPkiKey *scr_pki_key_retain(ScrPkiKey *key) { return sp_key_retain(key); } @@ -2208,8 +4052,9 @@ ScrPkiKey *scr_pki_key_public_of(ScrPkiKey *key) { return sp_public_of(key); } ScrPkiKey *scr_pki_generate_rsa(unsigned bits, uint64_t exponent) { SpKey *k = sp_key_alloc(true); - if (exponent > 2147483647 || mbedtls_pk_setup(&k->pk, mbedtls_pk_info_from_type(MBEDTLS_PK_RSA)) != 0 || - mbedtls_rsa_gen_key(mbedtls_pk_rsa(k->pk), sp_random, NULL, bits, (int)exponent) != 0) { + if (bits < 512 || bits > MBEDTLS_MPI_MAX_BITS || exponent < 3 || exponent % 2 == 0 || + exponent > 4294967295u || mbedtls_pk_setup(&k->pk, mbedtls_pk_info_from_type(MBEDTLS_PK_RSA)) != 0 || + sp_rsa_generate(mbedtls_pk_rsa(k->pk), bits, exponent) != 0) { sp_key_release(k); return NULL; } @@ -2329,3 +4174,220 @@ bool scr_pki_verify(ScrPkiKey *key, const char *hash, bool pss, int salt, const SpSigKey k = {.key = key, .padding = pss ? 6 : 1, .salt = salt, .p1363 = true}; return sp_verify_hash(&k, md, digest, mbedtls_md_get_size(info), signature, signature_len); } + +/* ── X509Certificate members over keys ───────────────────────────────── */ + +/* The certificate's public key, +1; NULL with the error pending. */ +static SpKey *sp_x509_key(ScrBytes *der) { + ScrBytes *view = der; + SCR_BYTES_SNAPSHOT(view); + const unsigned char *spki; + size_t spki_len; + if (!sp_cert_spki(view->data, (size_t)scr_bytes_byte_len(view), &spki, &spki_len)) { + sp_unsupported_input(); + return NULL; + } + return sp_parse(spki, spki_len, false); +} + +/* x509.publicKey: the certificate's public key as a new KeyObject. */ +ScrDyn *scr_crypto_x509_public_key(ScrBytes *der) { + SpKey *k = sp_x509_key(der); + return k ? sp_key_box(k) : NULL; +} + +/* The pkey argument of verify (a public key) and checkPrivateKey (a + * private key), borrowed; NULL with Node's TypeError pending. */ +static SpKey *sp_x509_pkey(const ScrDyn *value, bool private_key) { + SpKey *k = sp_key_of(value); + size_t len; + if (!k && !scr_key_object_bytes(value, &len)) { + scr_dyn_arg_type_fail("pkey", "an instance of KeyObject", value); + return NULL; + } + if (!k || k->private_key != private_key) { + const char *cls = !k ? "SecretKeyObject" : k->private_key ? "PrivateKeyObject" : "PublicKeyObject"; + scr_pki_throw_fmt(SCR_ERR_TYPE, "ERR_INVALID_ARG_VALUE", "The argument 'pkey' is invalid. Received %s [KeyObject] {}", + cls); + return NULL; + } + return k; +} + +/* The digest of a signature algorithm OID's last arc (RSA and ECDSA). */ +static mbedtls_md_type_t sp_x509_rsa_md(unsigned char arc) { + switch (arc) { + case 5: return MBEDTLS_MD_SHA1; + case 11: return MBEDTLS_MD_SHA256; + case 12: return MBEDTLS_MD_SHA384; + case 13: return MBEDTLS_MD_SHA512; + case 14: return MBEDTLS_MD_SHA224; + default: return MBEDTLS_MD_NONE; + } +} + +/* A hash AlgorithmIdentifier inside RSASSA-PSS-params. */ +static mbedtls_md_type_t sp_x509_hash_alg(unsigned char *p, const unsigned char *end) { + size_t n; + if (mbedtls_asn1_get_tag(&p, end, &n, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE) != 0) + return MBEDTLS_MD_NONE; + mbedtls_asn1_buf oid = {0}; + oid.tag = MBEDTLS_ASN1_OID; + if (mbedtls_asn1_get_tag(&p, end, &oid.len, MBEDTLS_ASN1_OID) != 0) return MBEDTLS_MD_NONE; + oid.p = p; + mbedtls_md_type_t md; + return mbedtls_oid_get_md_alg(&oid, &md) == 0 ? md : MBEDTLS_MD_NONE; +} + +/* x509.verify(publicKey): whether publicKey verifies the certificate's + * signature. An algorithm the key cannot verify answers false. */ +bool scr_crypto_x509_verify(ScrBytes *der, ScrDyn *key) { + SpKey *k = sp_x509_pkey(key, false); + if (!k) return false; + ScrBytes *view = der; + SCR_BYTES_SNAPSHOT(view); + unsigned char *p = view->data; + const unsigned char *end = p + (size_t)scr_bytes_byte_len(view); + size_t n; + if (mbedtls_asn1_get_tag(&p, end, &n, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE) != 0) + return false; + end = p + n; + const unsigned char *tbs = p; + if (mbedtls_asn1_get_tag(&p, end, &n, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE) != 0) + return false; + p += n; + size_t tbs_len = (size_t)(p - tbs); + const unsigned char *outer_alg = p; + if (mbedtls_asn1_get_tag(&p, end, &n, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE) != 0) + return false; + const unsigned char *alg_end = p + n; + /* As in OpenSSL's X509_verify, the TBSCertificate's signature field must + * be the same AlgorithmIdentifier as the outer one. */ + { + unsigned char *q = (unsigned char *)tbs; + const unsigned char *tbs_end = tbs + tbs_len; + size_t len; + if (mbedtls_asn1_get_tag(&q, tbs_end, &len, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE) != 0) + return false; + if (q < tbs_end && *q == (MBEDTLS_ASN1_CONTEXT_SPECIFIC | MBEDTLS_ASN1_CONSTRUCTED | 0)) { + if (mbedtls_asn1_get_tag(&q, tbs_end, &len, MBEDTLS_ASN1_CONTEXT_SPECIFIC | MBEDTLS_ASN1_CONSTRUCTED | 0) != 0) + return false; + q += len; + } + if (mbedtls_asn1_get_tag(&q, tbs_end, &len, MBEDTLS_ASN1_INTEGER) != 0) return false; + q += len; + const unsigned char *inner_alg = q; + if (mbedtls_asn1_get_tag(&q, tbs_end, &len, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE) != 0) + return false; + size_t inner_len = (size_t)(q + len - inner_alg); + if (inner_len != (size_t)(alg_end - outer_alg) || memcmp(inner_alg, outer_alg, inner_len) != 0) return false; + } + size_t oid_len; + if (mbedtls_asn1_get_tag(&p, alg_end, &oid_len, MBEDTLS_ASN1_OID) != 0) return false; + const unsigned char *oid = p; + unsigned char *params = p + oid_len; + p = (unsigned char *)alg_end; + size_t sig_len; + if (mbedtls_asn1_get_tag(&p, end, &sig_len, MBEDTLS_ASN1_BIT_STRING) != 0 || sig_len < 1 || *p != 0) + return false; + const unsigned char *sig = p + 1; + sig_len -= 1; + static const unsigned char rsa_prefix[] = {0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01}; + static const unsigned char ecdsa_prefix[] = {0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x04}; + static const unsigned char ed25519_oid[] = {0x2B, 0x65, 0x70}; + if (oid_len == sizeof ed25519_oid && memcmp(oid, ed25519_oid, oid_len) == 0) { + if (!sp_is_ed(k)) return false; + const ScrPkiEd25519 *ed = sp_ed(); + return ed != NULL && sp_ed_verify(ed, k->ed_pub, tbs, tbs_len, sig, sig_len); + } + if (sp_is_ed(k)) return false; + unsigned char hash[MBEDTLS_MD_MAX_SIZE]; + if (oid_len == sizeof rsa_prefix + 1 && memcmp(oid, rsa_prefix, sizeof rsa_prefix) == 0) { + if (!sp_is_rsa(k)) return false; + mbedtls_rsa_context *rsa = mbedtls_pk_rsa(k->pk); + if (oid[sizeof rsa_prefix] == 10) { /* RSASSA-PSS with its parameters */ + mbedtls_md_type_t md = MBEDTLS_MD_SHA1, mgf = MBEDTLS_MD_SHA1; + int salt = 20; + unsigned char *q = params; + size_t plen; + if (mbedtls_asn1_get_tag(&q, alg_end, &plen, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE) == 0) { + const unsigned char *pend = q + plen; + while (q < pend) { + unsigned char tag = *q; + size_t len; + if (mbedtls_asn1_get_tag(&q, pend, &len, tag) != 0) return false; + unsigned char *field = q; + q += len; + if (tag == (MBEDTLS_ASN1_CONTEXT_SPECIFIC | MBEDTLS_ASN1_CONSTRUCTED | 0)) { + md = sp_x509_hash_alg(field, q); + } else if (tag == (MBEDTLS_ASN1_CONTEXT_SPECIFIC | MBEDTLS_ASN1_CONSTRUCTED | 1)) { + /* MGF1 with its hash: SEQUENCE { id-mgf1, AlgorithmIdentifier } */ + size_t mlen, ilen; + unsigned char *m = field; + if (mbedtls_asn1_get_tag(&m, q, &mlen, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE) != 0 || + mbedtls_asn1_get_tag(&m, q, &ilen, MBEDTLS_ASN1_OID) != 0) + return false; + mgf = sp_x509_hash_alg(m + ilen, q); + } else if (tag == (MBEDTLS_ASN1_CONTEXT_SPECIFIC | MBEDTLS_ASN1_CONSTRUCTED | 2)) { + if (mbedtls_asn1_get_int(&field, q, &salt) != 0) return false; + } + } + } + if (md == MBEDTLS_MD_NONE || mgf == MBEDTLS_MD_NONE || sig_len != mbedtls_rsa_get_len(rsa)) + return false; + const mbedtls_md_info_t *info = mbedtls_md_info_from_type(md); + mbedtls_md(info, tbs, tbs_len, hash); + return mbedtls_rsa_rsassa_pss_verify_ext(rsa, md, mbedtls_md_get_size(info), hash, mgf, salt, + sig) == 0; + } + mbedtls_md_type_t md = sp_x509_rsa_md(oid[sizeof rsa_prefix]); + if (md == MBEDTLS_MD_NONE) return false; + const mbedtls_md_info_t *info = mbedtls_md_info_from_type(md); + mbedtls_md(info, tbs, tbs_len, hash); + /* PKCS#1 v1.5, whatever padding the key's last signature used */ + if (mbedtls_rsa_set_padding(rsa, MBEDTLS_RSA_PKCS_V15, MBEDTLS_MD_NONE) != 0) return false; + return mbedtls_pk_verify(&k->pk, md, hash, mbedtls_md_get_size(info), sig, sig_len) == 0; + } + if (oid_len >= sizeof ecdsa_prefix + 1 && memcmp(oid, ecdsa_prefix, sizeof ecdsa_prefix) == 0) { + if (!sp_is_ec(k)) return false; + mbedtls_md_type_t md = MBEDTLS_MD_NONE; + if (oid_len == 7 && oid[6] == 0x01) md = MBEDTLS_MD_SHA1; + else if (oid_len == 8 && oid[6] == 0x03) { + static const mbedtls_md_type_t sha2[] = {MBEDTLS_MD_NONE, MBEDTLS_MD_SHA224, MBEDTLS_MD_SHA256, + MBEDTLS_MD_SHA384, MBEDTLS_MD_SHA512}; + if (oid[7] >= 1 && oid[7] <= 4) md = sha2[oid[7]]; + } + if (md == MBEDTLS_MD_NONE) return false; + const mbedtls_md_info_t *info = mbedtls_md_info_from_type(md); + mbedtls_md(info, tbs, tbs_len, hash); + return mbedtls_pk_verify(&k->pk, md, hash, mbedtls_md_get_size(info), sig, sig_len) == 0; + } + return false; +} + +/* x509.checkPrivateKey(privateKey): whether privateKey is the private half + * of the certificate's public key. */ +bool scr_crypto_x509_check_private_key(ScrBytes *der, ScrDyn *key) { + SpKey *k = sp_x509_pkey(key, true); + if (!k) return false; + SpKey *cert = sp_x509_key(der); + if (!cert) return false; + bool same = false; + if (sp_is_ed(k) || sp_is_ed(cert)) { + same = sp_is_ed(k) && sp_is_ed(cert) && memcmp(k->ed_pub, cert->ed_pub, 32) == 0; + } else if (mbedtls_pk_get_type(&k->pk) == mbedtls_pk_get_type(&cert->pk)) { + SpKey *pub = sp_public_of(k); + if (pub) { + unsigned char *a = malloc(SP_DER_MAX), *b = malloc(SP_DER_MAX); + if (!a || !b) scr_trap("scriptc: out of memory\n"); + int na = sp_write_spki_der(pub, a, SP_DER_MAX); + int nb = sp_write_spki_der(cert, b, SP_DER_MAX); + same = na > 0 && na == nb && memcmp(a + SP_DER_MAX - na, b + SP_DER_MAX - nb, (size_t)na) == 0; + free(a); + free(b); + sp_key_release(pub); + } + } + sp_key_release(cert); + return same; +} diff --git a/packages/runtime/src/scr_publish.c b/packages/runtime/src/scr_publish.c index 4767122dd..8d11d3806 100644 --- a/packages/runtime/src/scr_publish.c +++ b/packages/runtime/src/scr_publish.c @@ -128,8 +128,10 @@ bool scr_pub_mark(ScrPub *ctx, void *obj, bool cyc, int kind) { size_t *rc = obj; if (SCR_RC_PUBLISHED(obj) || ctx->refusal) return false; bool compact = (kind & SCR_PUB_COMPACT) != 0; - /* Both log the low 32 bits: a count, or a compact header word. */ - bool class_id = compact || (kind & SCR_PUB_CLASS_ID) != 0; + /* Both log the low 32 bits: a count, or a compact header word. An + * object with a layout id in its rc word (scr_rc_layout_id) keeps it, as + * a class instance keeps its class id. */ + bool class_id = compact || (kind & SCR_PUB_CLASS_ID) != 0 || scr_rc_layout_id(obj) != 0; kind &= ~(SCR_PUB_CLASS_ID | SCR_PUB_COMPACT); if (ctx->logged == ctx->log_cap) ctx->log = scr_pub_grow(ctx->log, &ctx->log_cap, sizeof *ctx->log, ctx->log_inline); @@ -527,62 +529,12 @@ void scr_throw_published_field(const char *prop, const char *owner) { scr_pub_throw("Cannot assign to read only property '%s' of object '#<%s>'", prop, owner); } -static void scr_pub_index_text(double index, char *out, size_t size) { - char buf[64]; - size_t n = scr_f64_to_str(index, buf); - if (n > size - 1) n = size - 1; - memcpy(out, buf, n); - out[n] = 0; +bool scr_throw_published_array(int op, const ScrArr *a, double index, double n) { + return scr_array_frozen_error(op, a->len, index, n, 0); } -/* op: 0 index write, 1 push (n = argument count), 2 pop, 3 length write, - * 4 unshift (n = argument count), 5 delete, 6 shift, 7 other in-place - * rewrites (splice, reverse, fill, copyWithin, sort). Returns false when - * Node's operation would not write the frozen array at all (it then is not - * an error: e.g. reversing an empty array). */ -bool scr_throw_published_array(int op, const ScrArr *a, double index, double n) { - static const char length[] = "Cannot assign to read only property 'length' of object '[object Array]'"; - char text[64]; - size_t len = a->len; - switch (op) { - case 0: - scr_pub_index_text(index, text, sizeof text); - if (index >= 0 && index < (double)len && index == (double)(size_t)index) - scr_pub_throw("Cannot assign to read only property '%s' of object '[object Array]'%s", text, ""); - else scr_pub_throw("Cannot add property %s, object is not extensible%s", text, ""); - return true; - case 1: - if (n == 0) break; - scr_pub_index_text((double)len, text, sizeof text); - scr_pub_throw("Cannot add property %s, object is not extensible%s", text, ""); - return true; - case 2: - case 6: - if (len == 0) break; - if (op == 6 && len > 1) { - scr_pub_throw("Cannot assign to read only property '0' of object '[object Array]'%s%s", "", ""); - return true; - } - scr_pub_index_text((double)(len - 1), text, sizeof text); - scr_pub_throw("Cannot delete property '%s' of [object Array]%s", text, ""); - return true; - case 3: break; - case 4: - if (n == 0) break; - scr_pub_index_text(len ? (double)len + n - 1 : 0, text, sizeof text); - scr_pub_throw("Cannot add property %s, object is not extensible%s", text, ""); - return true; - case 5: - scr_pub_index_text(index, text, sizeof text); - scr_pub_throw("Cannot delete property '%s' of [object Array]%s", text, ""); - return true; - default: - if (len == 0) return false; - scr_pub_throw("Cannot assign to read only property '0' of object '[object Array]'%s%s", "", ""); - return true; - } - scr_throw_error_msg(SCR_ERR_TYPE, length, sizeof length - 1); - return true; +bool scr_throw_published_array_range(int op, const ScrArr *a, double x, double y, double z) { + return scr_array_frozen_error(op, a->len, x, y, z); } void scr_throw_published_collection(bool set) { diff --git a/packages/runtime/src/scr_qs.h b/packages/runtime/src/scr_qs.h index fc5ca5da5..1548bfb72 100644 --- a/packages/runtime/src/scr_qs.h +++ b/packages/runtime/src/scr_qs.h @@ -62,8 +62,28 @@ bool scr_util_terminal_colors(void); * (scr_util_promisify.c). Arguments are borrowed; function results are +1 * native dyn functions, or NULL with Node's TypeError pending. */ void scr_util_inherits(struct ScrDyn *ctor, struct ScrDyn *super_ctor); +void scr_util_inherits_prototype(struct ScrDyn *ctor, struct ScrDyn *super_ctor, + struct ScrDyn *super_prototype); struct ScrDyn *scr_util_deprecate(struct ScrDyn *fn, struct ScrDyn *msg, struct ScrDyn *code, struct ScrDyn *options); +/* util.isArray and util._extend, with Node's DEP0044 and DEP0060 on the + * first call (scr_util_callable.c). _extend answers target +1. */ +bool scr_util_is_array_deprecated(const struct ScrDyn *value); +struct ScrDyn *scr_util_extend(struct ScrDyn *target, struct ScrDyn *source); +/* util.diff(actual, expected): Node's operation list (+1), or NULL with + * ERR_INVALID_ARG_TYPE pending. */ +struct ScrDyn *scr_util_diff(struct ScrDyn *actual, struct ScrDyn *expected); +/* util.MIMEType and util.MIMEParams (scr_mime.c): +1 handles, or NULL + * with the error pending; parse answers null for invalid input. */ +struct ScrDyn *scr_mime_type_new(struct ScrDyn *input, struct ScrDyn *no_throw); +struct ScrDyn *scr_mime_type_parse(struct ScrDyn *input); +struct ScrDyn *scr_mime_params_new(void); +bool scr_mime_is(const struct ScrDyn *value, double kind); +/* util.debounce(fn, wait, options) and util.throttle(fn, limit, interval, + * options) (scr_util_timing.c): the wrapping function (+1), or NULL with + * the validation error pending. */ +struct ScrDyn *scr_util_debounce(struct ScrDyn *fn, struct ScrDyn *wait, struct ScrDyn *options); +struct ScrDyn *scr_util_throttle(struct ScrDyn *fn, struct ScrDyn *limit, struct ScrDyn *interval, struct ScrDyn *options); struct ScrDyn *scr_util_callbackify(struct ScrDyn *original); struct ScrDyn *scr_util_promisify(struct ScrDyn *original); struct ScrDyn *scr_util_debuglog(struct ScrDyn *section, struct ScrDyn *callback); diff --git a/packages/runtime/src/scr_rc.h b/packages/runtime/src/scr_rc.h index 18df3a237..5a705054a 100644 --- a/packages/runtime/src/scr_rc.h +++ b/packages/runtime/src/scr_rc.h @@ -8,13 +8,27 @@ /* ── reference counts ───────────────────────────────────────────────── * Every heap object's first member is its `size_t rc` word. On 64-bit - * targets the reference count is the word's low 32 bits: an emitted - * instance of a class in a program-rooted hierarchy keeps its class id (its - * class's preorder number + 1) in the high 32 bits (llvm/classes.ts - * classIdRcWord), and no other object's count reaches 2^32. A count of SCR_RC_IMMORTAL marks - * an immortal object; for every object but those class instances the whole - * word is then SIZE_MAX. Code that reads or writes the count of an object of - * any kind goes through SCR_RC_COUNT. */ + * targets the word is + * + * bits 0-31 the reference count (SCR_RC_COUNT) + * bit 62 F: a frozen record (SCR_RC_FROZEN) + * bit 63 P: a program-lifetime record (llvm/classes.ts + * programLifetimeRc) + * bits 32-61 the layout id (scr_rc_layout_id) + * + * and no object's count reaches 2^32. The layout id names the object's + * layout in the single object model (scr_layout.h); 0 means the object + * never needed one. An emitted instance of a class in a program-rooted + * hierarchy keeps its class id (its class's preorder number + 1) there + * (llvm/classes.ts classIdRcWord): a class id is the layout id of its + * class's canonical layout. A count of SCR_RC_IMMORTAL marks an immortal + * object. Publication and the runtime's own immortal objects write the + * whole word as SIZE_MAX when the object has no layout id, and write only + * the count when it has one, so an immortal object keeps its layout id + * (scr_rc_immortal tests the count; a high half of all ones reads as no + * layout id). Code that reads or writes the count of an object of any + * kind goes through SCR_RC_COUNT; code that may see an object with a layout + * id tests immortality with scr_rc_immortal, never against SIZE_MAX. */ #if SIZE_MAX > UINT32_MAX typedef uint32_t ScrRcCount; #ifdef __BYTE_ORDER__ @@ -45,20 +59,60 @@ typedef size_t ScrRcCount; /* Whether `obj`, an object of any kind, is published (immortal). */ #define SCR_RC_PUBLISHED(obj) (SCR_RC_COUNT(obj) >= SCR_RC_PUBLISHED_MIN) -/* The class id in the high half of `obj`'s rc word, or 0 for an object - * without one: the high half of any other object's word is 0, or 0xffffffff - * (immortal) or 0x80000000 (program-lifetime), and a class id is never one of - * these. */ -static inline uint32_t scr_rc_class_id(const void *obj) { +/* Frozen records (Object.freeze). The rc word of a record a program may + * freeze (llvm/shapes.ts) carries SCR_RC_FROZEN beside its count, and that + * record's emitted release ignores the bit. On 64-bit targets the bit is in + * the high half, outside the count; on 32-bit targets it is the count's bit + * 30, which SCR_RC_REFS masks. An immortal word has the bit set too; it + * does not read as frozen. */ +#if SIZE_MAX > UINT32_MAX +#define SCR_RC_FROZEN ((size_t)1 << 62) +#define SCR_RC_REFS(obj) SCR_RC_COUNT(obj) +#else +#define SCR_RC_FROZEN ((size_t)1 << 30) +#define SCR_RC_REFS(obj) (SCR_RC_COUNT(obj) == SCR_RC_IMMORTAL ? SCR_RC_IMMORTAL : SCR_RC_COUNT(obj) & ~SCR_RC_FROZEN) +#endif +/* Whether `obj`, an object whose rc word is a count (not a compact class + * instance), is immortal: its count is SCR_RC_IMMORTAL, whatever its high + * half holds. */ +static inline bool scr_rc_immortal(const void *obj) { + return SCR_RC_COUNT(obj) == SCR_RC_IMMORTAL; +} + +static inline bool scr_rc_frozen(const void *obj) { + return !scr_rc_immortal(obj) && (*(const size_t *)obj & SCR_RC_FROZEN) != 0; +} + +/* The high half of the rc word on 64-bit targets: the program-lifetime + * bit, the frozen bit and the layout id (see the layout above). 32-bit + * targets have no high half; their objects keep a layout id elsewhere when + * they need one (scr_layout.h). */ +#define SCR_RC_HIGH_LIFETIME UINT32_C(0x80000000) +#define SCR_RC_HIGH_FROZEN UINT32_C(0x40000000) +#define SCR_RC_LAYOUT_ID_MASK UINT32_C(0x3fffffff) +/* Layout ids are below this bound: 2^30 - 1 is the id bits of an all-ones + * high half, which reads as no id. */ +#define SCR_RC_LAYOUT_ID_LIMIT SCR_RC_LAYOUT_ID_MASK + +/* The layout id in the high half of `obj`'s rc word, or 0 for an object + * without one. The whole-word immortal encoding (SIZE_MAX) has no id. */ +static inline uint32_t scr_rc_layout_id(const void *obj) { #if SIZE_MAX > UINT32_MAX uint32_t high = (uint32_t)(*(const size_t *)obj >> 32); - return high == UINT32_MAX || high == UINT32_C(0x80000000) ? 0 : high; + return high == UINT32_MAX ? 0 : high & SCR_RC_LAYOUT_ID_MASK; #else (void)obj; return 0; #endif } +/* The class id in the high half of `obj`'s rc word, or 0 for an object + * without one. A class id is the layout id of the class's canonical + * layout, and no object of any other kind has a layout id yet. */ +static inline uint32_t scr_rc_class_id(const void *obj) { + return scr_rc_layout_id(obj); +} + /* ── cycle collection (scr_cycle.c) ─────────────────────────────────── * Reference counting alone cannot free cycles, so every object that can * participate in one — capture boxes, heap closures, unions, promises, and @@ -552,6 +606,14 @@ _Static_assert(sizeof(((ScrCycHdr *)0)->gen) == 1 && SCR_CYC_OLD == 2, * depth; deeper objects wait until the outer disposal drains the worklist. * The callback owns the object and must release its children and free it. */ void scr_rc_destroy(void *obj, void (*destroy)(void *)); +/* scr_rc_destroy for an emitted teardown that keeps the blocks it frees in + * a segment, linked through their first words, instead of freeing them onto + * the free list (llvm/shapes.ts): destroy_seg tears obj down, appending its + * blocks after the segment's last link `tail`, and returns the new last + * link, which this returns. A disposal past the depth budget is queued with + * `destroy`, which frees its own blocks, and leaves `tail` as it was. */ +void *scr_rc_destroy_seg(void *obj, void *(*destroy_seg)(void *, void *), + void (*destroy)(void *), void *tail); /* RC hooks for cycle-headered types. on_release: rc was decremented and * stayed above zero — buffer the object as a possible cycle root (may run @@ -670,10 +732,21 @@ static inline void scr_cyc_dead(void *obj) { * live heap — do not put it on a per-turn path. */ void scr_collect_cycles(void); void scr_collect_cycles_phase(void); +/* Returns the heap pages that hold only freed memory to the system: the + * calling thread's size-class pages whose blocks are all free, and the + * system allocator's free pages (scr_alloc.c). */ +void scr_heap_trim(void); /* Sets the growth cap SCR_CYCLE_GROWTH_CAP sets (scr_cycle.c), for a program * that knows its own memory profile; the environment variable takes * precedence. Call it before the program allocates much. */ void scr_cyc_set_growth_cap(long cap); +/* Sets the nursery trigger SCR_CYCLE_THRESHOLD sets (scr_cycle.c): the + * number of new candidates at which the release path runs a pass, for a + * program that knows its own collection profile. The environment variable + * takes precedence; 0 restores the default, which starts at 256 and adapts + * to the program. The trigger re-arms with it at the end of the next pass. + * Call it before the program allocates much. */ +void scr_cyc_set_nursery_threshold(long threshold); void scr_cyc_context_cleanup(void); /* One pass on the normal generational schedule, for callers that reach a @@ -725,12 +798,14 @@ void scr_pub_dyn_fn(void *v, ScrPub *ctx); void scr_pub_arr(void *v, ScrPub *ctx, ScrPubFn elem); void scr_pub_map(void *v, ScrPub *ctx, ScrPubFn key, ScrPubFn val); void scr_publish_refuse(const char *message); -/* Writes into published objects (emitted guards): Node's frozen-object - * TypeErrors. scr_throw_published_array answers false (and throws nothing) - * when Node's operation would not write the array. */ +/* Writes into published or frozen objects (emitted guards): Node's + * frozen-object TypeErrors. The array guards answer false (and throw + * nothing) when Node's operation would not write the array; their ops are + * scr_array_frozen_error's (scr_array.h). */ struct ScrArr; SCR_COLD void scr_throw_published_field(const char *prop, const char *owner); bool scr_throw_published_array(int op, const struct ScrArr *a, double index, double n); +bool scr_throw_published_array_range(int op, const struct ScrArr *a, double x, double y, double z); SCR_COLD void scr_throw_published_collection(bool set); /* Young-in nursery (scr_cycle.c built with SCR_YIN, linked by programs diff --git a/packages/runtime/src/scr_rc.test.c b/packages/runtime/src/scr_rc.test.c new file mode 100644 index 000000000..159235ce9 --- /dev/null +++ b/packages/runtime/src/scr_rc.test.c @@ -0,0 +1,47 @@ +#undef NDEBUG +#include "scr_rc.h" +#include +#include + +/* The rc word's encodings (scr_rc.h): the count in the low half, and the + * program-lifetime bit, the frozen bit and the layout id in the high half. + * Immortality is a count of all ones; a layout id survives it. */ +static size_t word(uint32_t high, uint32_t count) { + return ((size_t)high << 32) | count; +} + +int main(void) { +#if SIZE_MAX > UINT32_MAX + /* An ordinary object: no layout id, not frozen, not immortal. */ + size_t plain = word(0, 3); + assert(scr_rc_layout_id(&plain) == 0 && !scr_rc_frozen(&plain) && !scr_rc_immortal(&plain)); + /* The whole-word immortal encoding has no layout id and is not frozen. */ + size_t immortal = SIZE_MAX; + assert(scr_rc_layout_id(&immortal) == 0 && scr_rc_immortal(&immortal) && !scr_rc_frozen(&immortal)); + /* A program-lifetime record (llvm/classes.ts programLifetimeRc) and a + * frozen record have no layout id. */ + size_t lifetime = word(SCR_RC_HIGH_LIFETIME, 0); + assert(scr_rc_layout_id(&lifetime) == 0 && !scr_rc_immortal(&lifetime)); + size_t frozen = word(SCR_RC_HIGH_FROZEN, 2); + assert(scr_rc_layout_id(&frozen) == 0 && scr_rc_frozen(&frozen)); + /* A class instance keeps its class id, which is its layout id, beside + * its count, also once immortal, frozen or program-lifetime. */ + size_t instance = word(7, 1); + assert(scr_rc_layout_id(&instance) == 7 && scr_rc_class_id(&instance) == 7); + SCR_RC_COUNT(&instance) = SCR_RC_IMMORTAL; + assert(scr_rc_immortal(&instance) && scr_rc_layout_id(&instance) == 7 && !scr_rc_frozen(&instance)); + size_t frozen_id = word(SCR_RC_HIGH_FROZEN | 9, 1); + assert(scr_rc_layout_id(&frozen_id) == 9 && scr_rc_frozen(&frozen_id)); + size_t lifetime_id = word(SCR_RC_HIGH_LIFETIME | 11, UINT32_C(0x80000000)); + assert(scr_rc_layout_id(&lifetime_id) == 11 && !scr_rc_immortal(&lifetime_id)); + /* The largest layout id. */ + size_t top = word(SCR_RC_LAYOUT_ID_LIMIT - 1, 1); + assert(scr_rc_layout_id(&top) == SCR_RC_LAYOUT_ID_LIMIT - 1); +#else + size_t plain = 3, immortal = SIZE_MAX; + assert(scr_rc_layout_id(&plain) == 0 && !scr_rc_immortal(&plain)); + assert(scr_rc_immortal(&immortal) && !scr_rc_frozen(&immortal)); +#endif + puts("rc word encodings"); + return 0; +} diff --git a/packages/runtime/src/scr_rc.test.ts b/packages/runtime/src/scr_rc.test.ts new file mode 100644 index 000000000..181d0beba --- /dev/null +++ b/packages/runtime/src/scr_rc.test.ts @@ -0,0 +1,36 @@ +import { execFile } from "node:child_process"; +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { promisify } from "node:util"; +import { expect, test } from "vitest"; + +const exec = promisify(execFile); + +/* The rc word helpers are inline in scr_rc.h, so the test links alone. */ +test.skipIf(process.platform === "win32")( + "the rc word keeps the count, the lifetime and frozen bits and the layout id apart", + async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-rc-")); + try { + const bin = join(dir, "rc"); + await exec("clang", [ + "-std=c11", + "-O1", + "-g", + "-fsanitize=address,undefined", + join(import.meta.dirname, "scr_rc.test.c"), + "-o", + bin, + ]); + const result = await exec(bin, [], { + timeout: 60000, + env: { ...process.env, UBSAN_OPTIONS: "halt_on_error=1" }, + }); + expect(result.stdout).toBe("rc word encodings\n"); + expect(result.stderr).toBe(""); + } finally { + await rm(dir, { recursive: true, force: true }); + } + }, +); diff --git a/packages/runtime/src/scr_readline.c b/packages/runtime/src/scr_readline.c index 1bc48bbe7..178d620c6 100644 --- a/packages/runtime/src/scr_readline.c +++ b/packages/runtime/src/scr_readline.c @@ -283,7 +283,23 @@ static void scr_rl_buf_append(ScrRl *rl, const char *p, size_t n) { rl->line_len += n; } -/* kNormalWrite over decoded UTF-8 text. */ +/* The end of the line starting at `i` in text without a CR: the next LF, + * or n when the text ends first. */ +static size_t scr_rl_next_lf(const char *p, size_t i, size_t n) { + const char *lf = memchr(p + i, '\n', n - i); + return lf ? (size_t)(lf - p) : n; +} + +/* The end of the line starting at `i`: the next LF or CR, or n. */ +static size_t scr_rl_next_break(const char *p, size_t i, size_t n) { + while (i < n && p[i] != '\n' && p[i] != '\r') i++; + return i; +} + +/* kNormalWrite over decoded UTF-8 text. The text is scanned once: LF-only + * text (no CR anywhere in it) moves from LF to LF with memchr, other text + * splits at LF, CR and CRLF. The unfinished line before the text only + * joins the first line, so the rest of the text is never copied whole. */ static void scr_rl_feed_text(ScrRl *rl, const char *p, size_t n) { if (rl->tty) { scr_rl_tty_ops->feed(rl, p, n); @@ -296,43 +312,31 @@ static void scr_rl_feed_text(ScrRl *rl, const char *p, size_t n) { } rl->saw_return_at = 0; } - bool ending = false; - for (size_t i = 0; i < n && !ending; i++) ending = p[i] == '\n' || p[i] == '\r'; - if (!ending) { + bool cr = n > 0 && memchr(p, '\r', n) != NULL; + size_t end = n == 0 ? 0 : cr ? scr_rl_next_break(p, 0, n) : scr_rl_next_lf(p, 0, n); + if (end == n) { if (n > 0) scr_rl_buf_append(rl, p, n); return; } - /* The unfinished line joins the new text; the scan runs over both. */ - char *text = p ? (char *)p : (char *)""; - size_t len = n; - char *joined = NULL; - if (rl->line_len > 0) { - joined = malloc(rl->line_len + n); - if (!joined) scr_rl_oom(); - memcpy(joined, rl->line_buf, rl->line_len); - memcpy(joined + rl->line_len, p, n); - text = joined; - len = rl->line_len + n; - } - rl->line_len = 0; - rl->saw_return_at = len > 0 && text[len - 1] == '\r' ? scr_now_ms() : 0; + rl->saw_return_at = p[n - 1] == '\r' ? scr_now_ms() : 0; /* Split first (the remainder becomes the new unfinished line before any * line is delivered, as in Node), then deliver in order. */ size_t n_lines = 0, cap_lines = 0; ScrStr **lines = NULL; size_t start = 0; - for (size_t i = 0; i < len; i++) { - size_t adv; - if (text[i] == '\n') adv = 1; - else if (text[i] == '\r') adv = i + 1 < len && text[i + 1] == '\n' ? 2 : 1; - else continue; + while (end < n) { lines = scr_rl_grow(lines, &cap_lines, n_lines + 1, sizeof *lines); - lines[n_lines++] = scr_str_new(text + start, i - start); - i += adv - 1; - start = i + 1; + if (n_lines == 0 && rl->line_len > 0) { + scr_rl_buf_append(rl, p, end); + lines[n_lines++] = scr_str_new(rl->line_buf, rl->line_len); + } else { + lines[n_lines++] = scr_str_new(p + start, end - start); + } + start = end + (p[end] == '\r' && end + 1 < n && p[end + 1] == '\n' ? 2 : 1); + end = start >= n ? n : cr ? scr_rl_next_break(p, start, n) : scr_rl_next_lf(p, start, n); } - if (start < len) scr_rl_buf_append(rl, text + start, len - start); - free(joined); + rl->line_len = 0; + if (start < n) scr_rl_buf_append(rl, p + start, n - start); for (size_t i = 0; i < n_lines; i++) { if (rl->closed || scr_exc_pending()) { scr_str_release(lines[i]); @@ -366,6 +370,11 @@ static void scr_rl_feed_bytes(ScrRl *rl, const char *p, size_t n) { rl->parked_len += n; return; } + /* ASCII with no held bytes decodes to itself: split it in place. */ + if (rl->n_held == 0 && scr_bytes_ascii_span(p, n) == n) { + scr_rl_feed_text(rl, p, n); + return; + } size_t total = rl->n_held + n; unsigned char *all = malloc(total ? total : 1); if (!all) scr_rl_oom(); @@ -401,6 +410,12 @@ static void scr_rl_feed_end(ScrRl *rl) { void scr_rl_input_bytes(double id, ScrBytes *chunk) { ScrRl *rl = scr_rl_find(id); if (!rl || rl->closed) return; + /* The splitter copies every line it keeps before it delivers one, so it + * can read a chunk's own storage; shared memory is copied out first. */ + if (!chunk->shared) { + scr_rl_feed_bytes(rl, (const char *)chunk->data, chunk->len); + return; + } char *p = malloc(chunk->len ? chunk->len : 1); if (!p) scr_rl_oom(); scr_bytes_read(chunk, 0, p, chunk->len); diff --git a/packages/runtime/src/scr_regex.c b/packages/runtime/src/scr_regex.c index 158697a14..4296ce791 100644 --- a/packages/runtime/src/scr_regex.c +++ b/packages/runtime/src/scr_regex.c @@ -1416,6 +1416,51 @@ ScrStr *scr_regex_replace(ScrStr *s, ScrRegex *re, ScrStr *rep) { return scr_replace_impl(s, re, rep); } +void scr_re_replace_out_reserve(ScrReReplaceOut *o, int64_t need) { + if (need < 0 || (uint64_t)need > SCR_STR_MAX_CAP - (uint64_t)o->len) scr_regex_oom(); + size_t cap = (size_t)o->cap * 2; + if (cap < (size_t)(o->len + need)) cap = (size_t)(o->len + need); + if (cap > SCR_STR_MAX_CAP) cap = SCR_STR_MAX_CAP; + o->str->len = (size_t)o->len; + o->str = scr_str_regrow(o->str, cap); + o->data = o->str->data; + o->cap = (int64_t)o->str->cap; +} + +/* A global substitution with a literal ASCII replacement through the + * build-time replacer of its (pattern, replacement) pair: on an ASCII + * subject the replacer writes each gap and the replacement into the output + * and continues the search after every match without returning. Every + * other subject, and a plain-text pattern (string search), takes + * scr_replace_impl. */ +ScrStr *scr_regex_replace_with(ScrStr *s, ScrRegex *re, ScrStr *rep, + ScrRegexMatcherFn replacer) { + (void)scr_regex_bc(re); + if (scr_regex_lit(re) || !scr_text_ascii(s)) return scr_replace_impl(s, re, rep); + if (s->len > (size_t)INT32_MAX - 1) scr_regex_oom(); + re->last_index = 0; + ScrReReplaceOut o; + o.str = scr_str_alloc_raw(0, s->len + 64); + o.data = o.str->data; + o.len = 0; + o.cap = (int64_t)o.str->cap; + o.next = 0; + int32_t n = replacer((const uint8_t *)s->data, (int32_t)s->len, 0, (uint8_t **)(void *)&o); + if (n == 0) { + o.str->len = 0; + scr_str_release(o.str); + return scr_str_retain(s); + } + size_t tail = s->len - (size_t)o.next; + if ((int64_t)(tail + 1) > o.cap - o.len) scr_re_replace_out_reserve(&o, (int64_t)tail + 1); + scr_copy_bytes(o.data + o.len, s->data + o.next, tail); + o.len += (int64_t)tail; + o.str->len = (size_t)o.len; + o.data[o.len] = '\0'; + scr_str_note_ascii(o.str); + return o.str; +} + typedef struct { ScrDyn *arguments; int start, end; @@ -1529,49 +1574,101 @@ ScrStr *scr_regex_replace_callback(ScrStr *s, ScrRegex *re, ScrDyn *callback, bo return scr_jb_finish(&output); } -/* One call of a typed replacer (scr_regex_replace_fn's contract): string - * arguments are passed +1 (the closure ABI's owned parameters), the offset - * as a number, and the result is the closure's +1 string. */ -static ScrStr *scr_regex_call_replacer(ScrClosure *f, uint32_t arity, int ncap, - ScrStr *const *caps, double offset, ScrStr *subject) { - void *p[4] = { NULL, NULL, NULL, NULL }; - for (uint32_t i = 0; i < arity && i < 4; i++) { - if ((int)i < ncap) { - if (!caps[i]) scr_trap("scriptc: internal error: typed replacer capture is unset\n"); - p[i] = scr_str_retain(caps[i]); - } else if ((int)i == ncap + 1) { - p[i] = scr_str_retain(subject); - } - } +/* The closure signature scr_regex_replace_fn calls: `arity` pointer + * parameters, except a number at `number_at` (-1 when there is none). A + * string and a checked-dynamic value are both pointers, and so are both + * results. */ +#define SCR_REPLACER_CALL(...) return ((void *(*)(ScrClosure *, __VA_ARGS__))fn) +static inline __attribute__((always_inline)) void *scr_regex_call_replacer( + ScrClosure *f, uint32_t arity, int number_at, void *const *p, double offset) { void *fn = f->fn; - typedef ScrStr *R; - switch (arity) { - case 0: return ((R (*)(ScrClosure *))fn)(f); - case 1: return ((R (*)(ScrClosure *, void *))fn)(f, p[0]); - case 2: - if (ncap == 1) return ((R (*)(ScrClosure *, void *, double))fn)(f, p[0], offset); - return ((R (*)(ScrClosure *, void *, void *))fn)(f, p[0], p[1]); - case 3: - if (ncap == 1) return ((R (*)(ScrClosure *, void *, double, void *))fn)(f, p[0], offset, p[2]); - if (ncap == 2) return ((R (*)(ScrClosure *, void *, void *, double))fn)(f, p[0], p[1], offset); - return ((R (*)(ScrClosure *, void *, void *, void *))fn)(f, p[0], p[1], p[2]); - case 4: - if (ncap == 2) - return ((R (*)(ScrClosure *, void *, void *, double, void *))fn)(f, p[0], p[1], offset, p[3]); - if (ncap == 3) - return ((R (*)(ScrClosure *, void *, void *, void *, double))fn)(f, p[0], p[1], p[2], offset); - if (ncap >= 4) - return ((R (*)(ScrClosure *, void *, void *, void *, void *))fn)(f, p[0], p[1], p[2], p[3]); - break; + typedef void *P; + typedef double D; + switch (arity * 8 + (uint32_t)(number_at < 0 ? 7 : number_at)) { + case 0 * 8 + 7: return ((void *(*)(ScrClosure *))fn)(f); + case 1 * 8 + 7: SCR_REPLACER_CALL(P)(f, p[0]); + case 2 * 8 + 7: SCR_REPLACER_CALL(P, P)(f, p[0], p[1]); + case 2 * 8 + 1: SCR_REPLACER_CALL(P, D)(f, p[0], offset); + case 3 * 8 + 7: SCR_REPLACER_CALL(P, P, P)(f, p[0], p[1], p[2]); + case 3 * 8 + 1: SCR_REPLACER_CALL(P, D, P)(f, p[0], offset, p[2]); + case 3 * 8 + 2: SCR_REPLACER_CALL(P, P, D)(f, p[0], p[1], offset); + case 4 * 8 + 7: SCR_REPLACER_CALL(P, P, P, P)(f, p[0], p[1], p[2], p[3]); + case 4 * 8 + 1: SCR_REPLACER_CALL(P, D, P, P)(f, p[0], offset, p[2], p[3]); + case 4 * 8 + 2: SCR_REPLACER_CALL(P, P, D, P)(f, p[0], p[1], offset, p[3]); + case 4 * 8 + 3: SCR_REPLACER_CALL(P, P, P, D)(f, p[0], p[1], p[2], offset); + case 5 * 8 + 7: SCR_REPLACER_CALL(P, P, P, P, P)(f, p[0], p[1], p[2], p[3], p[4]); + case 5 * 8 + 1: SCR_REPLACER_CALL(P, D, P, P, P)(f, p[0], offset, p[2], p[3], p[4]); + case 5 * 8 + 2: SCR_REPLACER_CALL(P, P, D, P, P)(f, p[0], p[1], offset, p[3], p[4]); + case 5 * 8 + 3: SCR_REPLACER_CALL(P, P, P, D, P)(f, p[0], p[1], p[2], offset, p[4]); + case 5 * 8 + 4: SCR_REPLACER_CALL(P, P, P, P, D)(f, p[0], p[1], p[2], p[3], offset); + case 6 * 8 + 7: SCR_REPLACER_CALL(P, P, P, P, P, P)(f, p[0], p[1], p[2], p[3], p[4], p[5]); + case 6 * 8 + 1: SCR_REPLACER_CALL(P, D, P, P, P, P)(f, p[0], offset, p[2], p[3], p[4], p[5]); + case 6 * 8 + 2: SCR_REPLACER_CALL(P, P, D, P, P, P)(f, p[0], p[1], offset, p[3], p[4], p[5]); + case 6 * 8 + 3: SCR_REPLACER_CALL(P, P, P, D, P, P)(f, p[0], p[1], p[2], offset, p[4], p[5]); + case 6 * 8 + 4: SCR_REPLACER_CALL(P, P, P, P, D, P)(f, p[0], p[1], p[2], p[3], offset, p[5]); + case 6 * 8 + 5: SCR_REPLACER_CALL(P, P, P, P, P, D)(f, p[0], p[1], p[2], p[3], p[4], offset); default: break; } scr_trap("scriptc: internal error: typed replacer signature\n"); } +#undef SCR_REPLACER_CALL + +/* Typed replacer argument k: a capture (NULL for an unset optional one) + * or the subject; the offset is passed separately. The compiler admits a + * typed parameter only at those positions. Inlined with each constant k. */ +static inline __attribute__((always_inline)) void scr_regex_typed_arg( + void **p, uint32_t k, uint32_t kinds, int capture_count, ScrStr *const *caps, ScrStr *subject) { + if ((int)k < capture_count) { + if (!caps[k] && !(kinds & (SCR_REPLACER_OPTIONAL << k))) + scr_trap("scriptc: internal error: typed replacer capture is unset\n"); + p[k] = caps[k] ? scr_str_retain(caps[k]) : NULL; + } else if ((int)k == capture_count + 1) { + p[k] = scr_str_retain(subject); + } +} + +/* One match's replacer arguments when some parameter is checked-dynamic + * (scr_regex_replace_fn's `kinds`): typed ones +1, the rest new boxes. + * `boxed_subject` is the subject's box; `groups` the match's groups + * object, or NULL when no parameter reads it. */ +__attribute__((noinline)) static void scr_regex_replacer_args( + void **p, uint32_t arity, uint32_t kinds, int capture_count, ScrStr *const *caps, double offset, + ScrStr *subject, ScrDyn *boxed_subject, ScrDyn *groups) { + for (uint32_t k = 0; k < arity; k++) { + int at = (int)k; + if (kinds & (1u << k)) { + if (at < capture_count) + p[k] = caps[k] ? scr_dyn_new_str(caps[k]) : scr_dyn_retain(scr_dyn_undefined()); + else if (at == capture_count) + p[k] = scr_dyn_new_num(offset); + else if (at == capture_count + 1) + p[k] = scr_dyn_retain(boxed_subject); + else if (at == capture_count + 2 && groups) + p[k] = scr_dyn_retain(groups); + else + p[k] = scr_dyn_retain(scr_dyn_undefined()); + } else if (at < capture_count) { + if (!caps[k] && !(kinds & (SCR_REPLACER_OPTIONAL << k))) + scr_trap("scriptc: internal error: typed replacer capture is unset\n"); + p[k] = caps[k] ? scr_str_retain(caps[k]) : NULL; + } else if (at == capture_count + 1) { + p[k] = scr_str_retain(subject); + } else if (at != capture_count) { + scr_trap("scriptc: internal error: typed replacer signature\n"); + } + } +} /* scr_regex_replace_callback's algorithm for a typed replacer: the matches - * are collected first, keeping only the captures the replacer reads, then - * the replacer runs once per match in order. */ -ScrStr *scr_regex_replace_fn(ScrStr *s, ScrRegex *re, ScrClosure *f, uint32_t arity, bool all) { + * are collected first, keeping only the captures and groups objects the + * replacer reads, then the replacer runs once per match in order. Typed + * parameters receive their values +1 (the closure ABI's owned + * parameters) and an unset optional capture NULL; checked-dynamic ones + * receive new boxes. Inlined into one copy for kinds 0, where the boxed + * handling folds away, and one for the other kinds. */ +static inline __attribute__((always_inline)) ScrStr * +scr_regex_replace_fn_impl(ScrStr *s, ScrRegex *re, ScrClosure *f, uint32_t arity, uint32_t kinds, + bool all) { uint8_t *bc = scr_regex_bc(re); int flags = lre_get_flags(bc); bool global = (flags & LRE_FLAG_GLOBAL) != 0; @@ -1580,15 +1677,28 @@ ScrStr *scr_regex_replace_fn(ScrStr *s, ScrRegex *re, ScrClosure *f, uint32_t ar scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); return NULL; } + if (arity > SCR_REPLACER_MAX_ARITY) scr_trap("scriptc: internal error: typed replacer arity\n"); bool sticky = (flags & LRE_FLAG_STICKY) != 0; bool unicode = (flags & (LRE_FLAG_UNICODE | LRE_FLAG_UNICODE_SETS)) != 0; int capture_count = lre_get_capture_count(bc); size_t keep = arity < (uint32_t)capture_count ? arity : (size_t)capture_count; + /* Node passes the groups object after the subject when the pattern has + * named groups; only a checked-dynamic parameter can read it, and no + * typed parameter follows the subject. */ + bool past_subject = (uint32_t)capture_count + 2 < arity; + if (past_subject && !(kinds & SCR_REPLACER_BOXED_PARAMS)) + scr_trap("scriptc: internal error: typed replacer signature\n"); + const char *groupnames = past_subject ? lre_get_groupnames(bc) : NULL; + bool groups_read = groupnames != NULL; + if (groups_read && !(kinds & (1u << (capture_count + 2)))) + scr_trap("scriptc: internal error: typed replacer groups parameter\n"); + int number_at = capture_count < (int)arity && !(kinds & (1u << capture_count)) ? capture_count : -1; ScrRegexInput input; scr_regex_input_init(&input, re, bc, s); int len = input.len; struct { int start, end; } *matches = NULL; ScrStr **caps = NULL; + ScrDyn **groups = NULL; size_t count = 0, capacity = 0; if (global) re->last_index = 0; int position = sticky ? scr_regex_start(re, len) : 0; @@ -1610,6 +1720,11 @@ ScrStr *scr_regex_replace_fn(ScrStr *s, ScrRegex *re, ScrClosure *f, uint32_t ar if (!grown) scr_regex_oom(); caps = grown; } + if (groups_read) { + grown = realloc(groups, next * sizeof(*groups)); + if (!grown) scr_regex_oom(); + groups = grown; + } capacity = next; } for (size_t group = 0; group < keep; group++) { @@ -1618,6 +1733,18 @@ ScrStr *scr_regex_replace_fn(ScrStr *s, ScrRegex *re, ScrClosure *f, uint32_t ar cs && ce ? scr_regex_span(&input, scr_capture_index(&input, cs), scr_capture_index(&input, ce)) : NULL; } + if (groups_read) { + ScrDyn *object = scr_dyn_new_obj_null_proto(); + const char *name = groupnames; + for (int group = 1; group < capture_count; group++) { + size_t length = strlen(name); + if (length && (capture[group * 2] || !scr_dyn_obj_get(object, name, length))) + scr_dyn_obj_set(object, name, length, + scr_regex_capture_value(&input, capture[group * 2], capture[group * 2 + 1])); + name += length + LRE_GROUP_NAME_TRAILER_LEN; + } + groups[count] = object; + } matches[count].start = start; matches[count].end = end; count++; @@ -1627,18 +1754,43 @@ ScrStr *scr_regex_replace_fn(ScrStr *s, ScrRegex *re, ScrClosure *f, uint32_t ar ScrJsonBuf output; scr_jb_init(&output); int next = 0; + /* One box of the subject serves every match. */ + ScrDyn *subject = NULL; + if (kinds & SCR_REPLACER_BOXED_PARAMS) subject = scr_dyn_new_str(s); scr_dyn_this_push_dyn(NULL); for (size_t i = 0; i < count; i++) { - ScrStr *replacement = scr_regex_call_replacer(f, arity, capture_count, caps + i * keep, - (double)matches[i].start, s); - if (scr_exc_pending()) break; + void *p[SCR_REPLACER_MAX_ARITY] = { NULL }; + double offset = (double)matches[i].start; + ScrStr *const *match_caps = caps + i * keep; + if (kinds & SCR_REPLACER_BOXED_PARAMS) { + scr_regex_replacer_args(p, arity, kinds, capture_count, match_caps, offset, s, subject, + groups_read ? groups[i] : NULL); + } else { + for (uint32_t k = 0; k < SCR_REPLACER_MAX_ARITY; k++) + if (k < arity) scr_regex_typed_arg(p, k, kinds, capture_count, match_caps, s); + } + void *result = scr_regex_call_replacer(f, arity, number_at, p, offset); + ScrStr *replacement = (ScrStr *)result; + if (kinds & SCR_REPLACER_BOXED_RESULT) { + replacement = scr_exc_pending() ? NULL : scr_dyn_string_coerce_js((ScrDyn *)result); + scr_dyn_release((ScrDyn *)result); + } + if (scr_exc_pending()) { + scr_str_release(replacement); + break; + } scr_regex_put_span(&output, &input, next, matches[i].start); scr_jb_put_str(&output, replacement); scr_str_release(replacement); next = matches[i].end; } scr_dyn_this_pop(); + if (subject) scr_dyn_release(subject); for (size_t i = 0; i < count * keep; i++) scr_str_release(caps[i]); + if (groups_read) { + for (size_t i = 0; i < count; i++) scr_dyn_release(groups[i]); + free(groups); + } free(caps); free(matches); if (scr_exc_pending()) { @@ -1651,6 +1803,24 @@ ScrStr *scr_regex_replace_fn(ScrStr *s, ScrRegex *re, ScrClosure *f, uint32_t ar return scr_jb_finish(&output); } +__attribute__((noinline)) static ScrStr *scr_regex_replace_typed(ScrStr *s, ScrRegex *re, + ScrClosure *f, uint32_t arity, + bool all) { + return scr_regex_replace_fn_impl(s, re, f, arity, 0, all); +} + +__attribute__((noinline)) static ScrStr *scr_regex_replace_kinds(ScrStr *s, ScrRegex *re, + ScrClosure *f, uint32_t arity, + uint32_t kinds, bool all) { + return scr_regex_replace_fn_impl(s, re, f, arity, kinds, all); +} + +ScrStr *scr_regex_replace_fn(ScrStr *s, ScrRegex *re, ScrClosure *f, uint32_t arity, + uint32_t kinds, bool all) { + if (kinds == 0) return scr_regex_replace_typed(s, re, f, arity, all); + return scr_regex_replace_kinds(s, re, f, arity, kinds, all); +} + /* The pinned Node ANSI matcher derives from ansi-regex, copyright (c) * Sindre Sorhus , used under the MIT license: * Permission is hereby granted, free of charge, to any person obtaining a @@ -2248,6 +2418,197 @@ static void *scr_normalize_realloc(void *opaque, void *ptr, size_t size) { return realloc(ptr, size); } +/* unicode_normalize, except that U+11A7 never composes. libunicode composes + * an LV syllable with U+11A7 as a trailing consonant (those are U+11A8 to + * U+11C2), which drops it: "\uAC00\u11A7" became "\uAC00". U+11A7 is a + * class-0 vowel that composes with nothing, so for the composing forms the + * code points between occurrences of it normalize on their own and it is + * kept as it is, as in Node. */ +static int scr_unicode_normalize(uint32_t **pdst, const uint32_t *src, int len, + UnicodeNormalizationEnum mode) { + int at = 0; + if (mode == UNICODE_NFC || mode == UNICODE_NFKC) + while (at < len && src[at] != 0x11A7) at++; + else + at = len; + if (at == len) return unicode_normalize(pdst, src, len, mode, NULL, scr_normalize_realloc); + uint32_t *out = NULL; + size_t count = 0, cap = 0; + int start = 0; + for (int i = 0; i <= len; i++) { + if (i < len && src[i] != 0x11A7) continue; + uint32_t *part = NULL; + int n = 0; + if (i > start) { + n = unicode_normalize(&part, src + start, i - start, mode, NULL, scr_normalize_realloc); + if (n < 0) { + free(out); + return -1; + } + } + size_t need = count + (size_t)n + 1; + if (need > cap) { + size_t grown = cap ? cap * 2 : 16; + if (grown < need) grown = need; + uint32_t *bigger = realloc(out, grown * sizeof *out); + if (!bigger) { + free(part); + free(out); + return -1; + } + out = bigger; + cap = grown; + } + if (n > 0) memcpy(out + count, part, (size_t)n * sizeof *out); + count += (size_t)n; + free(part); + if (i < len) out[count++] = 0x11A7; + start = i + 1; + } + *pdst = out; + return (int)count; +} + +/* Normalization quick check (UAX #15): a string whose every code point is + * inert for the form is already in that form. A code point is inert when + * normalizing it alone leaves it unchanged, its canonical combining class + * is 0, and (for the composing forms) no composition can take it as its + * second element. The last condition holds in the ranges below: none holds + * a code point with combining class 0 that composes with a preceding one + * (those are Hangul jamo vowels and trailing consonants, U+3099/U+309A, + * and vowel signs of Indic and Southeast Asian scripts above U+0900), so a + * code point outside them takes the full algorithm. Inert flags are + * computed through unicode_normalize, 256 code points at a time, when a + * block is first seen. */ +#define SCR_NQC_BLOCKS 0x200 +typedef struct { + uint8_t known[4][SCR_NQC_BLOCKS]; + uint8_t inert[4][SCR_NQC_BLOCKS][32]; + uint8_t starter[4][SCR_NQC_BLOCKS][32]; /* class 0, never a second element */ + struct { + uint32_t cp; + uint8_t len; + char bytes[15]; + } single[4][256]; /* a lone code point's normalized UTF-8 */ +} ScrNormalizeQuick; +static SCR_TL ScrNormalizeQuick *scr_nqc; + +static void scr_nqc_free(void) { + free(scr_nqc); + scr_nqc = NULL; +} + +static bool scr_nqc_in_ranges(uint32_t c) { + return c < 0x900 || (c >= 0x2000 && c < 0x3099) || (c >= 0x309B && c < 0xA000) || + (c >= 0xAC00 && c < 0xD7A4) || (c >= 0xF900 && c < 0x10000) || + (c >= 0x1F000 && c < 0x1FB00); +} + +/* Bit 0: inert. Bit 1: a starter, with class 0 and never the second element + * of a composition, so nothing before it reorders or composes with it. */ +/* Whether c has class 0 and lies in the ranges above. U+0345 is the one + * code point of class 240, which the reordering test cannot tell from + * class 0; a mark of class 1..239 after U+0345 moves before it. */ +static bool scr_nqc_class0(uint32_t c) { + if (c == 0x345 || !scr_nqc_in_ranges(c)) return false; + uint32_t pair[2] = {0x345, c}; + uint32_t *out = NULL; + int n = unicode_normalize(&out, pair, 2, UNICODE_NFD, NULL, scr_normalize_realloc); + bool starter = n >= 2 && out[0] == 0x345; + free(out); + return starter; +} + +static unsigned scr_nqc_compute(uint32_t c, UnicodeNormalizationEnum mode) { + if (!scr_nqc_class0(c)) return 0; + uint32_t *out = NULL; + int n = unicode_normalize(&out, &c, 1, mode, NULL, scr_normalize_realloc); + bool same = n == 1 && out[0] == c; + free(out); + if (same) return 3u; + /* A starter's decomposition must begin with one too (U+FF9E's + * compatibility decomposition is U+3099, which composes with the code + * point before it). */ + UnicodeNormalizationEnum decompose = + mode == UNICODE_NFKC || mode == UNICODE_NFKD ? UNICODE_NFKD : UNICODE_NFD; + out = NULL; + n = unicode_normalize(&out, &c, 1, decompose, NULL, scr_normalize_realloc); + bool leads = n >= 1 && scr_nqc_class0(out[0]); + free(out); + return leads ? 2u : 0u; +} + +/* scr_nqc_compute's bits for c, from its block's flags. */ +static unsigned scr_nqc_class(uint32_t c, UnicodeNormalizationEnum mode) { + if (c >= (uint32_t)SCR_NQC_BLOCKS << 8) return 0; + if (!scr_nqc) { + scr_nqc = calloc(1, sizeof *scr_nqc); + if (!scr_nqc) return 0; + scr_atexit(scr_nqc_free); + } + uint32_t block = c >> 8; + uint8_t *inert = scr_nqc->inert[mode][block]; + uint8_t *starter = scr_nqc->starter[mode][block]; + if (!scr_nqc->known[mode][block]) { + for (uint32_t i = 0; i < 256; i++) { + unsigned bits = scr_nqc_compute((block << 8) | i, mode); + if (bits & 1) inert[i >> 3] |= (uint8_t)(1u << (i & 7)); + if (bits & 2) starter[i >> 3] |= (uint8_t)(1u << (i & 7)); + } + scr_nqc->known[mode][block] = 1; + } + unsigned bit = c & 0xff; + return ((inert[bit >> 3] >> (bit & 7)) & 1) | (((starter[bit >> 3] >> (bit & 7)) & 1) << 1); +} + +/* Normalize the code points of data[from, to) and append them. */ +static void scr_nqc_segment(ScrJsonBuf *b, const char *data, size_t from, size_t to, + UnicodeNormalizationEnum mode) { + size_t first_step; + uint32_t first = scr_case_decode(data + from, &first_step); + if (first_step == to - from && scr_nqc) { + /* One code point (a precomposed letter between inert ones): its + * normalized bytes repeat, so they are kept per form. */ + __typeof__(scr_nqc->single[0][0]) *slot = &scr_nqc->single[mode][first & 0xff]; + if (slot->cp != first || slot->len == 0) { + uint32_t *output = NULL; + int count = unicode_normalize(&output, &first, 1, mode, NULL, scr_normalize_realloc); + if (count < 0) scr_regex_oom(); + ScrJsonBuf one; + scr_jb_init(&one); + for (int i = 0; i < count; i++) scr_case_put_cp(&one, output[i]); + free(output); + ScrStr *bytes = scr_jb_finish(&one); + if (bytes->len == 0 || bytes->len > sizeof slot->bytes) { + scr_jb_write(b, bytes->data, bytes->len); + scr_str_release(bytes); + return; + } + slot->cp = first; + slot->len = (uint8_t)bytes->len; + memcpy(slot->bytes, bytes->data, bytes->len); + scr_str_release(bytes); + } + scr_jb_write(b, slot->bytes, slot->len); + return; + } + uint32_t local[32]; + uint32_t *input = to - from <= 32 ? local : malloc((to - from) * sizeof *input); + if (!input) scr_regex_oom(); + int length = 0; + for (size_t offset = from; offset < to;) { + size_t step; + input[length++] = scr_case_decode(data + offset, &step); + offset += step; + } + uint32_t *output = NULL; + int count = scr_unicode_normalize(&output, input, length, mode); + if (input != local) free(input); + if (count < 0) scr_regex_oom(); + for (int i = 0; i < count; i++) scr_case_put_cp(b, output[i]); + free(output); +} + ScrStr *scr_str_normalize(const ScrStr *s, const ScrStr *form) { UnicodeNormalizationEnum mode; if (form->len == 3 && !memcmp(form->data, "NFC", 3)) mode = UNICODE_NFC; @@ -2262,30 +2623,65 @@ ScrStr *scr_str_normalize(const ScrStr *s, const ScrStr *form) { /* Every ASCII string is already normalized in all four forms. Validate * the form first: even an empty subject must reject an invalid form. */ if (scr_text_ascii(s)) return scr_str_retain((ScrStr *)s); - if (s->len > INT32_MAX / sizeof(uint32_t)) scr_regex_oom(); - uint32_t *input = malloc((s->len + 1) * sizeof(uint32_t)); - if (!input) scr_regex_oom(); - int length = 0; + bool inert = true; for (size_t offset = 0; offset < s->len;) { + if ((unsigned char)s->data[offset] < 0x80) { + offset++; + continue; + } size_t step; - input[length++] = scr_case_decode(s->data + offset, &step); + uint32_t c = scr_case_decode(s->data + offset, &step); offset += step; + if (!(scr_nqc_class(c, mode) & 1)) { + inert = false; + break; + } } - uint32_t *output = NULL; - int count = unicode_normalize(&output, input, length, mode, NULL, scr_normalize_realloc); - free(input); - if (count < 0) scr_regex_oom(); + if (inert) return scr_str_retain((ScrStr *)s); + if (s->len > INT32_MAX / sizeof(uint32_t)) scr_regex_oom(); + /* An inert code point bounds reordering and composition, and so does any + * other starter (class 0, never a composition's second element), so the + * text normalizes segment by segment. A segment starts at a starter: runs + * of inert code points copy as they are, and a segment with any other + * code point goes through scr_unicode_normalize. */ ScrJsonBuf buffer; scr_jb_init(&buffer); - for (int i = 0; i < count; i++) scr_case_put_cp(&buffer, output[i]); - free(output); + size_t seg = 0, last_inert = 0; + bool plain = true; + for (size_t offset = 0; offset < s->len;) { + unsigned char lead = (unsigned char)s->data[offset]; + size_t step = 1; + unsigned kind = 3; + if (lead >= 0x80) kind = scr_nqc_class(scr_case_decode(s->data + offset, &step), mode); + if (kind & 1) { + if (!plain) { + scr_nqc_segment(&buffer, s->data, seg, offset, mode); + seg = offset; + plain = true; + } + last_inert = offset; + } else if (kind & 2) { + if (plain) scr_jb_write(&buffer, s->data + seg, offset - seg); + else scr_nqc_segment(&buffer, s->data, seg, offset, mode); + seg = offset; + plain = false; + } else if (plain) { + size_t start = last_inert > seg ? last_inert : seg; + scr_jb_write(&buffer, s->data + seg, start - seg); + seg = start; + plain = false; + } + offset += step; + } + if (plain) scr_jb_write(&buffer, s->data + seg, s->len - seg); + else scr_nqc_segment(&buffer, s->data, seg, s->len, mode); return scr_jb_finish(&buffer); } uint32_t *scr_nfc_code_points(const uint32_t *input, size_t length, size_t *out_len) { if (length > INT32_MAX) scr_regex_oom(); uint32_t *output = NULL; - int count = unicode_normalize(&output, input, (int)length, UNICODE_NFC, NULL, scr_normalize_realloc); + int count = scr_unicode_normalize(&output, input, (int)length, UNICODE_NFC); if (count < 0) scr_regex_oom(); *out_len = (size_t)count; return output; diff --git a/packages/runtime/src/scr_regex.h b/packages/runtime/src/scr_regex.h index 01c545bc5..502a29266 100644 --- a/packages/runtime/src/scr_regex.h +++ b/packages/runtime/src/scr_regex.h @@ -80,16 +80,42 @@ ScrStr *scr_regex_flags(ScrRegex *re); /* +1 */ ScrStr *scr_regex_to_string(ScrRegex *re); /* +1 */ /* replace: first match without /g, every match with it. */ ScrStr *scr_regex_replace(ScrStr *s, ScrRegex *re, ScrStr *rep); -/* replace/replaceAll with a typed replacer closure of `arity` (at most 4) - * parameters returning a string. Parameter i receives what Node passes at - * position i: the match and captures (strings) below the capture count n, - * the match offset (a number) at n, the subject (a string) at n + 1. The - * compiler admits a closure only when every position it declares is one of - * those with the declared kind, and every capture it reads is set in every - * match (scr_regex_replace_callback handles the rest). Borrows all. */ +/* The output of a build-time replacer (regex-matchers.ts): the replacer + * appends each gap and the replacement at data[len] after reserving room, + * and leaves in `next` the subject index the unmatched tail starts at. */ +typedef struct { + int64_t len; + int64_t cap; + int64_t next; + char *data; /* at offset 24 on every target */ + ScrStr *str; +} ScrReReplaceOut; +/* Grows o->str so that `need` more bytes fit at o->data[o->len]. */ +void scr_re_replace_out_reserve(ScrReReplaceOut *o, int64_t need); +/* A global substitution with a literal ASCII replacement, through the + * replacer built for that pattern and replacement. Borrows all. */ +ScrStr *scr_regex_replace_with(ScrStr *s, ScrRegex *re, ScrStr *rep, + ScrRegexMatcherFn replacer); +/* replace/replaceAll with a replacer closure of `arity` (at most + * SCR_REPLACER_MAX_ARITY) parameters. Parameter i receives what Node passes + * at position i: the match and captures below the capture count n, the + * match offset at n, the subject at n + 1, the groups object at n + 2 when + * the pattern has named groups, and undefined after that. Bit i of + * `kinds` makes parameter i a checked-dynamic value, and bit + * SCR_REPLACER_OPTIONAL << i a capture held as a string or NULL for + * undefined; otherwise it is a string, or a number at n. + * SCR_REPLACER_BOXED_RESULT makes the result a checked-dynamic value, + * converted with ToString. The compiler admits a closure only when every + * typed position it declares is one of those with the declared kind, and + * every capture a string parameter reads is set in every match + * (scr_regex_replace_callback handles the rest). Borrows all. */ +#define SCR_REPLACER_MAX_ARITY 6 +#define SCR_REPLACER_BOXED_PARAMS 0xffu +#define SCR_REPLACER_OPTIONAL (1u << 8) +#define SCR_REPLACER_BOXED_RESULT (1u << 31) struct ScrClosure; ScrStr *scr_regex_replace_fn(ScrStr *s, ScrRegex *re, struct ScrClosure *f, uint32_t arity, - bool all); + uint32_t kinds, bool all); /* replaceAll: throws Node's TypeError when /g is missing (may-throw). */ ScrStr *scr_regex_replace_all(ScrStr *s, ScrRegex *re, ScrStr *rep); /* split: captured substrings are interleaved with the split pieces; diff --git a/packages/runtime/src/scr_require.c b/packages/runtime/src/scr_require.c new file mode 100644 index 000000000..d1ede2186 --- /dev/null +++ b/packages/runtime/src/scr_require.c @@ -0,0 +1,902 @@ +/* createRequire's require(id) and require.resolve(request) with a specifier + * computed at run time (module-resolution.ts lowers both calls here). + * + * The compiled module graph is fixed at build time, so neither call loads a + * module. Both resolve the specifier with Node 24's CommonJS algorithm + * (lib/internal/modules/cjs/loader.js: Module._resolveFilename and + * Module._findPath, with package.json "main", "exports" and self-reference) + * against the file system at run time: + * + * - require.resolve returns what Node returns: the builtin's id, or the real + * path of the resolved file. It throws Node's MODULE_NOT_FOUND error when + * nothing resolves. + * - require throws the same MODULE_NOT_FOUND error when nothing resolves, so + * packages that probe for optional files take their absent branch as in + * Node. When the specifier resolves (a builtin, or a file such as a native + * addon), Node would load the module, which a compiled program cannot do: + * the program ends with an SC2020 refusal naming the module. The refusal is + * not an exception, so a `catch` clause cannot take a fallback that Node + * would not take. + * + * Resolution steps whose Node result this file does not reproduce (package + * "imports", invalid package configs or export targets, unexported subpaths, + * percent-encoded export targets, an export target that names no file, a + * "main" that names no file) end the program with the same SC2020 refusal, + * naming the step. */ +#include "scr_runtime.h" + +#include +#include +#include + +typedef struct { + ScrStr *request; /* borrowed */ + ScrStr *importer; /* borrowed: the requiring file */ + ScrArr *builtins; /* borrowed: Node 24's require-able builtin ids */ + /* Set when resolution reaches a step it does not reproduce; the caller + * refuses with this reason. Static text. */ + const char *refusal; +} ScrRequire; + +static ScrStr *req_cstr(const char *s) { return scr_str_new(s, strlen(s)); } + +static bool req_has_prefix(const ScrStr *s, const char *prefix) { + size_t n = strlen(prefix); + return s->len >= n && memcmp(s->data, prefix, n) == 0; +} + +static bool req_has_suffix(const ScrStr *s, const char *suffix) { + size_t n = strlen(suffix); + return s->len >= n && memcmp(s->data + s->len - n, suffix, n) == 0; +} + +static bool req_eq(const ScrStr *s, const char *text) { + size_t n = strlen(text); + return s->len == n && memcmp(s->data, text, n) == 0; +} + +static ScrStr *req_concat(const ScrStr *a, const char *b, size_t b_len) { + ScrJsonBuf out; + scr_jb_init(&out); + scr_jb_write(&out, a->data, a->len); + scr_jb_write(&out, b, b_len); + return scr_jb_finish(&out); +} + +static ScrStr *req_resolve2(ScrStr *a, ScrStr *b) { + ScrArr *parts = scr_arr_new(SCR_ELEM_STR, 2); + scr_arr_push_ref(parts, scr_str_retain(a)); + scr_arr_push_ref(parts, scr_str_retain(b)); +#ifdef _WIN32 + ScrStr *out = scr_path_win32_resolve(parts); +#else + ScrStr *out = scr_path_resolve(parts); +#endif + scr_arr_release(parts); + return out; +} + +static ScrStr *req_dirname(ScrStr *path) { +#ifdef _WIN32 + return scr_path_win32_dirname(path); +#else + return scr_path_dirname(path); +#endif +} + +static bool req_is_absolute(ScrStr *path) { +#ifdef _WIN32 + return scr_path_win32_is_absolute(path); +#else + return scr_path_is_absolute(path); +#endif +} + +/* Node's internalModuleStat: 0 for a file, 1 for a directory, -1 when the + * path does not exist or cannot be read. Never throws. */ +static int req_stat(ScrStr *path) { + ScrStats *stats = scr_fs_stat_probe(path, false, false); + if (stats == NULL) { + if (scr_exc_pending()) scr_exc_clear(); + return -1; + } + int kind = scr_stats_is_dir(stats) ? 1 : 0; + scr_stats_release(stats); + return kind; +} + +/* tryFile: the real path of `path` when it is a file (+1), else NULL. */ +static ScrStr *req_try_file(ScrStr *path) { + if (req_stat(path) != 0) return NULL; + ScrStr *real = scr_fs_realpath(path); + if (real != NULL) return real; + if (scr_exc_pending()) scr_exc_clear(); + return scr_str_retain(path); +} + +/* tryExtensions with Module._extensions' keys, in registration order. */ +static ScrStr *req_try_extensions(ScrStr *base) { + static const char *const extensions[] = { ".js", ".json", ".node" }; + for (size_t i = 0; i < sizeof extensions / sizeof extensions[0]; i++) { + ScrStr *candidate = req_concat(base, extensions[i], strlen(extensions[i])); + ScrStr *found = req_try_file(candidate); + scr_str_release(candidate); + if (found != NULL) return found; + } + return NULL; +} + +/* ── package.json ────────────────────────────────────────────────────── */ + +/* The package.json at `pjson` parsed (+1), or NULL when there is none. + * A file Node rejects as an invalid package config (unparsable JSON, a + * non-object document, a non-string "name" or "type") sets the refusal. */ +static ScrDyn *req_read_package(ScrRequire *r, ScrStr *pjson) { + if (req_stat(pjson) != 0) return NULL; + ScrStr *text = scr_fs_read_file(pjson); + if (text == NULL) { + if (scr_exc_pending()) scr_exc_clear(); + return NULL; + } + /* Node's reader skips a leading UTF-8 byte order mark. */ + if (text->len >= 3 && (unsigned char)text->data[0] == 0xEF && + (unsigned char)text->data[1] == 0xBB && (unsigned char)text->data[2] == 0xBF) { + ScrStr *body = scr_str_new(text->data + 3, text->len - 3); + scr_str_release(text); + text = body; + } + ScrDyn *pkg = scr_json_parse(text); + scr_str_release(text); + if (pkg == NULL) { + if (scr_exc_pending()) scr_exc_clear(); + r->refusal = "a package.json that is not valid JSON"; + return NULL; + } + const ScrDyn *name = pkg->kind == SCR_DYN_OBJ ? scr_dyn_obj_get(pkg, "name", 4) : NULL; + const ScrDyn *type = pkg->kind == SCR_DYN_OBJ ? scr_dyn_obj_get(pkg, "type", 4) : NULL; + if (pkg->kind != SCR_DYN_OBJ || (name != NULL && name->kind != SCR_DYN_STR) || + (type != NULL && type->kind != SCR_DYN_STR)) { + scr_dyn_release(pkg); + r->refusal = "an invalid package.json"; + return NULL; + } + return pkg; +} + +/* A string field of a parsed package.json (borrowed), or NULL. */ +static ScrStr *req_package_string(const ScrDyn *pkg, const char *key) { + const ScrDyn *value = scr_dyn_obj_get(pkg, key, strlen(key)); + return value != NULL && value->kind == SCR_DYN_STR ? value->v.str : NULL; +} + +/* "exports" or "imports" as Node keeps them: a string, an object or an + * array (borrowed); NULL for every other value. */ +static const ScrDyn *req_package_map(const ScrDyn *pkg, const char *key) { + const ScrDyn *value = scr_dyn_obj_get(pkg, key, strlen(key)); + if (value == NULL) return NULL; + return value->kind == SCR_DYN_STR || value->kind == SCR_DYN_OBJ || value->kind == SCR_DYN_ARR ? value : NULL; +} + +/* tryPackage: a directory's "main" entry, then its index files. */ +static ScrStr *req_try_package(ScrRequire *r, ScrStr *dir) { + ScrStr *pjson_name = req_cstr("package.json"); + ScrStr *pjson = req_resolve2(dir, pjson_name); + scr_str_release(pjson_name); + ScrDyn *pkg = req_read_package(r, pjson); + scr_str_release(pjson); + if (r->refusal != NULL) return NULL; + ScrStr *main = pkg != NULL ? req_package_string(pkg, "main") : NULL; + ScrStr *index_name = req_cstr("index"); + ScrStr *index = req_resolve2(dir, index_name); + ScrStr *found = NULL; + if (main == NULL) { + found = req_try_extensions(index); + } else { + ScrStr *filename = req_resolve2(dir, main); + found = req_try_file(filename); + if (found == NULL) found = req_try_extensions(filename); + if (found == NULL) { + ScrStr *main_index = req_resolve2(filename, index_name); + found = req_try_extensions(main_index); + scr_str_release(main_index); + } + scr_str_release(filename); + if (found == NULL) { + /* Node falls back to the index with a DEP0128 warning, or throws an + * error that names the "main" entry. */ + r->refusal = "a package.json \"main\" entry that names no file"; + } + } + scr_str_release(index); + scr_str_release(index_name); + if (pkg != NULL) scr_dyn_release(pkg); + return found; +} + +/* ── package exports (lib/internal/modules/esm/resolve.js) ───────────── */ + +/* A target or subpath segment Node rejects or warns about: "", ".", "..", + * "node_modules" in any case, and their percent-encoded spellings. The + * caller refuses '%' separately, so the plain spellings suffice. */ +static bool req_invalid_segment(const char *s, size_t len) { + size_t start = 0; + for (size_t i = 0; i <= len; i++) { + if (i < len && s[i] != '/' && s[i] != '\\') continue; + size_t n = i - start; + const char *seg = s + start; + if (n == 0 && i < len) return true; + if ((n == 1 && seg[0] == '.') || (n == 2 && seg[0] == '.' && seg[1] == '.')) return true; + if (n == 12) { + static const char nm[] = "node_modules"; + bool same = true; + for (size_t k = 0; k < 12 && same; k++) { + char c = seg[k]; + if (c >= 'A' && c <= 'Z') c = (char)(c - 'A' + 'a'); + same = c == nm[k]; + } + if (same) return true; + } + start = i + 1; + } + return false; +} + +/* Characters whose URL parsing changes the path Node resolves: percent + * escapes, backslashes, query and fragment delimiters, and the control + * characters the URL parser strips. */ +static bool req_url_sensitive(const char *s, size_t len) { + for (size_t i = 0; i < len; i++) { + unsigned char c = (unsigned char)s[i]; + if (c == '%' || c == '\\' || c == '?' || c == '#' || c < 0x20 || c == 0x7f) return true; + } + return false; +} + +typedef enum { REQ_TARGET_PATH, REQ_TARGET_NULL, REQ_TARGET_UNDEFINED, REQ_TARGET_INVALID } ScrRequireTarget; + +static bool req_condition(const ScrStr *key) { + return req_eq(key, "default") || req_eq(key, "require") || req_eq(key, "node") || + req_eq(key, "node-addons") || req_eq(key, "module-sync"); +} + +static bool req_array_index(const ScrStr *key) { + if (key->len == 0 || key->len > 10) return false; + if (key->len > 1 && key->data[0] == '0') return false; + double n = 0; + for (size_t i = 0; i < key->len; i++) { + if (key->data[i] < '0' || key->data[i] > '9') return false; + n = n * 10 + (key->data[i] - '0'); + } + return n < 4294967295.0; +} + +/* resolvePackageTarget for an "exports" target. `pkg_dir` is the package + * directory, `subpath` the part the target receives. A path result is in + * `*out` (+1, not yet checked against the file system). */ +static ScrRequireTarget req_resolve_target(ScrRequire *r, ScrStr *pkg_dir, const ScrDyn *target, + const char *subpath, size_t subpath_len, bool pattern, + ScrStr **out) { + if (target->kind == SCR_DYN_STR) { + ScrStr *t = target->v.str; + if (subpath_len != 0 && !pattern && !req_has_suffix(t, "/")) return REQ_TARGET_INVALID; + if (!req_has_prefix(t, "./")) return REQ_TARGET_INVALID; + if (req_url_sensitive(t->data, t->len) || req_url_sensitive(subpath, subpath_len) || + req_invalid_segment(t->data + 2, t->len - 2) || + (subpath_len != 0 && req_invalid_segment(subpath, subpath_len))) { + r->refusal = "a package \"exports\" target that Node rejects, warns about or decodes"; + return REQ_TARGET_INVALID; + } + if (pattern && memchr(pkg_dir->data, '*', pkg_dir->len) != NULL) { + /* Node substitutes the pattern in the whole URL, directory included. */ + r->refusal = "a package \"exports\" pattern in a directory whose path contains '*'"; + return REQ_TARGET_INVALID; + } + ScrJsonBuf path; + scr_jb_init(&path); + scr_jb_write(&path, pkg_dir->data, pkg_dir->len); + const char *rest = t->data + 1; /* keeps the '/' after "." */ + size_t rest_len = t->len - 1; + if (pattern) { + for (size_t i = 0; i < rest_len; i++) { + if (rest[i] == '*') + scr_jb_write(&path, subpath, subpath_len); + else + scr_jb_putc(&path, rest[i]); + } + } else { + scr_jb_write(&path, rest, rest_len); + scr_jb_write(&path, subpath, subpath_len); + } + ScrStr *joined = scr_jb_finish(&path); +#ifdef _WIN32 + *out = scr_path_win32_normalize(joined); +#else + *out = scr_path_normalize(joined); +#endif + scr_str_release(joined); + return REQ_TARGET_PATH; + } + if (target->kind == SCR_DYN_ARR) { + double n = scr_dyn_arr_len(target); + if (n == 0) return REQ_TARGET_NULL; + ScrRequireTarget last = REQ_TARGET_UNDEFINED; + for (double i = 0; i < n; i++) { + ScrDyn *item = scr_dyn_arr_at(target, i); + ScrRequireTarget result = req_resolve_target(r, pkg_dir, item, subpath, subpath_len, pattern, out); + scr_dyn_release(item); + if (r->refusal != NULL) return REQ_TARGET_INVALID; + if (result == REQ_TARGET_PATH) return result; + if (result == REQ_TARGET_INVALID) last = REQ_TARGET_INVALID; + else if (result == REQ_TARGET_NULL) last = REQ_TARGET_NULL; + } + return last == REQ_TARGET_UNDEFINED ? REQ_TARGET_UNDEFINED : last; + } + if (target->kind == SCR_DYN_OBJ) { + ScrDyn *keys = scr_dyn_obj_own_keys(target); + double n = scr_dyn_arr_len(keys); + ScrRequireTarget result = REQ_TARGET_UNDEFINED; + for (double i = 0; i < n; i++) { + ScrDyn *key = scr_dyn_arr_at(keys, i); + bool index = req_array_index(key->v.str); + scr_dyn_release(key); + if (index) { + r->refusal = "numeric keys in a package's \"exports\""; + scr_dyn_release(keys); + return REQ_TARGET_INVALID; + } + } + for (double i = 0; i < n && result == REQ_TARGET_UNDEFINED; i++) { + ScrDyn *key = scr_dyn_arr_at(keys, i); + if (req_condition(key->v.str)) { + const ScrDyn *value = scr_dyn_obj_get(target, key->v.str->data, key->v.str->len); + result = req_resolve_target(r, pkg_dir, value, subpath, subpath_len, pattern, out); + if (r->refusal != NULL) result = REQ_TARGET_INVALID; + } + scr_dyn_release(key); + } + scr_dyn_release(keys); + return result; + } + if (target->kind == SCR_DYN_NULL) return REQ_TARGET_NULL; + return REQ_TARGET_INVALID; +} + +/* patternKeyCompare(a, b) === 1: `b` is the better pattern match. */ +static bool req_pattern_better(const ScrStr *a, const ScrStr *b) { + const char *as = a ? memchr(a->data, '*', a->len) : NULL; + const char *bs = memchr(b->data, '*', b->len); + size_t a_len = a ? a->len : 0; + size_t base_a = as ? (size_t)(as - a->data) + 1 : a_len; + size_t base_b = bs ? (size_t)(bs - b->data) + 1 : b->len; + if (base_a > base_b) return false; + if (base_b > base_a) return true; + if (as == NULL) return true; + if (bs == NULL) return false; + return b->len > a_len; +} + +/* packageExportsResolve for `subpath` ("." or "./..."), then + * finalizeEsmResolution's file check. +1 real path, or NULL with the + * refusal set. */ +static ScrStr *req_exports_resolve(ScrRequire *r, ScrStr *pkg_dir, const ScrDyn *exports, ScrStr *subpath) { + bool sugar; + if (exports->kind != SCR_DYN_OBJ) { + sugar = true; + } else { + sugar = false; + ScrDyn *keys = scr_dyn_obj_own_keys(exports); + double n = scr_dyn_arr_len(keys); + for (double i = 0; i < n; i++) { + ScrDyn *key = scr_dyn_arr_at(keys, i); + bool conditional = key->v.str->len == 0 || key->v.str->data[0] != '.'; + scr_dyn_release(key); + if (i == 0) { + sugar = conditional; + } else if (sugar != conditional) { + scr_dyn_release(keys); + r->refusal = "a package \"exports\" object that mixes subpaths and conditions"; + return NULL; + } + } + scr_dyn_release(keys); + } + if (exports->kind == SCR_DYN_STR && exports->v.str->len > 0 && + (exports->v.str->data[0] == '[' || exports->v.str->data[0] == '{')) { + r->refusal = "a package \"exports\" string that Node parses as JSON"; + return NULL; + } + ScrStr *path = NULL; + ScrRequireTarget result = REQ_TARGET_UNDEFINED; + bool has_star = memchr(subpath->data, '*', subpath->len) != NULL; + if (sugar) { + if (req_eq(subpath, ".")) + result = req_resolve_target(r, pkg_dir, exports, "", 0, false, &path); + } else { + const ScrDyn *exact = has_star || req_has_suffix(subpath, "/") + ? NULL + : scr_dyn_obj_get(exports, subpath->data, subpath->len); + if (exact != NULL) { + result = req_resolve_target(r, pkg_dir, exact, "", 0, false, &path); + } else { + ScrStr *best = NULL; + size_t best_start = 0, best_end = 0; + ScrDyn *keys = scr_dyn_obj_own_keys(exports); + double n = scr_dyn_arr_len(keys); + for (double i = 0; i < n; i++) { + ScrDyn *key_value = scr_dyn_arr_at(keys, i); + ScrStr *key = key_value->v.str; + const char *star = memchr(key->data, '*', key->len); + size_t at = star ? (size_t)(star - key->data) : 0; + if (star != NULL && subpath->len >= at && memcmp(subpath->data, key->data, at) == 0) { + size_t trailer = key->len - at - 1; + bool single = memchr(star + 1, '*', trailer) == NULL; + if (subpath->len >= key->len && + memcmp(subpath->data + subpath->len - trailer, star + 1, trailer) == 0 && + req_pattern_better(best, key) && single) { + if (best) scr_str_release(best); + best = scr_str_retain(key); + best_start = at; + best_end = subpath->len - trailer; + } + } + scr_dyn_release(key_value); + } + scr_dyn_release(keys); + if (best != NULL) { + if (req_has_suffix(subpath, "/")) { + r->refusal = "a package \"exports\" pattern matched by a subpath ending in '/'"; + } else { + const ScrDyn *target = scr_dyn_obj_get(exports, best->data, best->len); + result = req_resolve_target(r, pkg_dir, target, subpath->data + best_start, + best_end - best_start, true, &path); + } + scr_str_release(best); + } + } + } + if (r->refusal != NULL) { + if (path) scr_str_release(path); + return NULL; + } + if (result != REQ_TARGET_PATH) { + r->refusal = result == REQ_TARGET_INVALID ? "an invalid package \"exports\" target" + : "a subpath that the package's \"exports\" do not export"; + return NULL; + } + ScrStr *found = req_try_file(path); + scr_str_release(path); + if (found == NULL) r->refusal = "a package \"exports\" target that names no file"; + return found; +} + +/* ── Module._findPath ────────────────────────────────────────────────── */ + +/* EXPORTS_PATTERN: /^((?:@[^/\\%]+\/)?[^./\\%][^/\\%]*)(\/.*)?$/ — the + * package name's length in `request`, or 0 when it does not match. */ +static size_t req_package_name_len(const ScrStr *request) { + const char *s = request->data; + size_t len = request->len, i = 0; + if (len > 0 && s[0] == '@') { + i = 1; + size_t start = i; + while (i < len && s[i] != '/' && s[i] != '\\' && s[i] != '%') i++; + if (i == start || i >= len || s[i] != '/') i = 0; + else i++; + } + if (i >= len || s[i] == '.' || s[i] == '/' || s[i] == '\\' || s[i] == '%') return 0; + while (i < len && s[i] != '/' && s[i] != '\\' && s[i] != '%') i++; + if (i < len && s[i] != '/') return 0; + return i; +} + +/* resolveExports: a package under `nm_dir` whose package.json has + * "exports". +1 real path; NULL when the package has no "exports" (or the + * refusal is set). */ +static ScrStr *req_resolve_exports(ScrRequire *r, ScrStr *nm_dir) { + size_t name_len = req_package_name_len(r->request); + if (name_len == 0) return NULL; + ScrStr *name = scr_str_new(r->request->data, name_len); + ScrStr *pkg_dir = req_resolve2(nm_dir, name); + scr_str_release(name); + ScrStr *pjson = req_concat(pkg_dir, "/package.json", 13); + ScrDyn *pkg = req_read_package(r, pjson); + scr_str_release(pjson); + ScrStr *found = NULL; + const ScrDyn *exports = pkg != NULL ? req_package_map(pkg, "exports") : NULL; + if (exports != NULL) { + ScrJsonBuf sub; + scr_jb_init(&sub); + scr_jb_putc(&sub, '.'); + scr_jb_write(&sub, r->request->data + name_len, r->request->len - name_len); + ScrStr *subpath = scr_jb_finish(&sub); + found = req_exports_resolve(r, pkg_dir, exports, subpath); + scr_str_release(subpath); + } + if (pkg != NULL) scr_dyn_release(pkg); + scr_str_release(pkg_dir); + return found; +} + +/* isRelative in Module._findPath: ".", "..", and "./" or "../" prefixes. */ +static bool req_is_relative(const ScrStr *request) { + if (req_eq(request, ".") || req_eq(request, "..")) return true; + if (req_has_prefix(request, "./") || req_has_prefix(request, "../")) return true; +#ifdef _WIN32 + if (req_has_prefix(request, ".\\") || req_has_prefix(request, "..\\")) return true; +#endif + return false; +} + +static bool req_trailing_slash(const ScrStr *request) { + const char *s = request->data; + size_t n = request->len; + if (n == 0) return false; + if (s[n - 1] == '/') return true; + if (s[n - 1] != '.') return false; + if (n == 1 || s[n - 2] == '/') return true; + return s[n - 2] == '.' && (n == 2 || s[n - 3] == '/'); +} + +/* Module._findPath over `paths` (borrowed strings). +1 real path or NULL. */ +static ScrStr *req_find_path(ScrRequire *r, ScrStr *const *paths, size_t count) { + ScrStr *request = r->request; + bool absolute = req_is_absolute(request); + ScrStr *empty = NULL; + if (absolute) { + empty = scr_str_new("", 0); + paths = ∅ + count = 1; + } + bool trailing = req_trailing_slash(request); + bool inside = true; + if (req_is_relative(request)) { +#ifdef _WIN32 + ScrStr *normalized = scr_path_win32_normalize(request); +#else + ScrStr *normalized = scr_path_normalize(request); +#endif + inside = !req_has_prefix(normalized, ".."); + scr_str_release(normalized); + } + ScrStr *found = NULL; + for (size_t i = 0; i < count && found == NULL && r->refusal == NULL; i++) { + ScrStr *dir = paths[i]; + if (inside && dir->len > 0 && req_stat(dir) < 1) continue; + if (!absolute) { + found = req_resolve_exports(r, dir); + if (found != NULL || r->refusal != NULL) break; + } + ScrStr *base = req_resolve2(dir, request); + int kind = req_stat(base); + if (!trailing) { + if (kind == 0) found = req_try_file(base); + if (found == NULL) found = req_try_extensions(base); + } + if (found == NULL && kind == 1) found = req_try_package(r, base); + scr_str_release(base); + } + if (empty) scr_str_release(empty); + return found; +} + +/* Module._nodeModulePaths(from) followed by Module.globalPaths' entries + * that come from the environment (NODE_PATH, then the home directory's + * .node_modules and .node_libraries). Node also searches the lib/node + * directory of its own installation prefix, which a compiled program does + * not have. Fills `out` (+1 each) and answers the count. */ +static size_t req_lookup_paths(ScrStr *from, ScrStr ***out) { + size_t cap = 16, n = 0; + ScrStr **paths = malloc(cap * sizeof *paths); + if (!paths) scr_trap("scriptc: out of memory\n"); +#define REQ_PUSH(value) \ + do { \ + if (n == cap) { \ + cap *= 2; \ + ScrStr **grown = realloc(paths, cap * sizeof *paths); \ + if (!grown) scr_trap("scriptc: out of memory\n"); \ + paths = grown; \ + } \ + paths[n++] = (value); \ + } while (0) + ScrStr *dot = scr_str_new(".", 1); + ScrStr *abs = req_resolve2(from, dot); + scr_str_release(dot); + static const char nm[] = "node_modules"; + const char *s = abs->data; +#ifdef _WIN32 + if (abs->len >= 2 && s[abs->len - 1] == '\\' && s[abs->len - 2] == ':') { + REQ_PUSH(req_concat(abs, "node_modules", 12)); + } else { + for (long i = (long)abs->len - 1, p = 0, last = (long)abs->len; i >= 0; --i) { + char c = s[i]; + if (c == '\\' || c == '/' || c == ':') { + if (p != 12) { + ScrStr *prefix = scr_str_new(s, (size_t)last); + REQ_PUSH(req_concat(prefix, "\\node_modules", 13)); + scr_str_release(prefix); + } + last = i; + p = 0; + } else if (p != -1) { + p = (p < 12 && nm[11 - p] == c) ? p + 1 : -1; + } + } + } +#else + if (abs->len == 1 && s[0] == '/') { + REQ_PUSH(scr_str_new("/node_modules", 13)); + } else { + for (long i = (long)abs->len - 1, p = 0, last = (long)abs->len; i >= 0; --i) { + char c = s[i]; + if (c == '/') { + if (p != 12) { + ScrStr *prefix = scr_str_new(s, (size_t)last); + REQ_PUSH(req_concat(prefix, "/node_modules", 13)); + scr_str_release(prefix); + } + last = i; + p = 0; + } else if (p != -1) { + p = (p < 12 && nm[11 - p] == c) ? p + 1 : -1; + } + } + REQ_PUSH(scr_str_new("/node_modules", 13)); + } +#endif + scr_str_release(abs); +#ifdef _WIN32 + const char *node_path = scr_getenv("NODE_PATH"); + const char *home = scr_getenv("USERPROFILE"); + const char delimiter = ';'; +#else + const char *node_path = scr_getenv("NODE_PATH"); + const char *home = scr_getenv("HOME"); + const char delimiter = ':'; +#endif + if (node_path != NULL) { + for (const char *p = node_path; *p;) { + const char *end = strchr(p, delimiter); + size_t len = end ? (size_t)(end - p) : strlen(p); + if (len > 0) REQ_PUSH(scr_str_new(p, len)); + p += len; + if (*p) p++; + } + } + if (home != NULL && *home) { + ScrStr *home_dir = req_cstr(home); + static const char *const globals[] = { ".node_modules", ".node_libraries" }; + for (size_t i = 0; i < 2; i++) { + ScrStr *name = req_cstr(globals[i]); + REQ_PUSH(req_resolve2(home_dir, name)); + scr_str_release(name); + } + scr_str_release(home_dir); + } +#undef REQ_PUSH + *out = paths; + return n; +} + +/* trySelf: the nearest package.json above the importer (Node stops below + * the root and at a node_modules directory) resolves the request through + * its own "exports" when its "name" prefixes it. */ +static ScrStr *req_try_self(ScrRequire *r) { + ScrStr *dir = req_dirname(r->importer); + ScrStr *found = NULL; + for (;;) { + ScrStr *parent = req_dirname(dir); + bool root = scr_str_eq(parent, dir); + scr_str_release(parent); + if (root) break; + ScrStr *dot = scr_str_new(".", 1); + ScrStr *abs = req_resolve2(dir, dot); + scr_str_release(dot); + bool boundary = req_has_suffix(abs, "/node_modules") +#ifdef _WIN32 + || req_has_suffix(abs, "\\node_modules") +#endif + ; + scr_str_release(abs); + if (boundary) break; + ScrStr *pjson_name = req_cstr("package.json"); + ScrStr *pjson = req_resolve2(dir, pjson_name); + scr_str_release(pjson_name); + ScrDyn *pkg = req_read_package(r, pjson); + scr_str_release(pjson); + if (r->refusal != NULL) break; + if (pkg != NULL) { + ScrStr *name = req_package_string(pkg, "name"); + const ScrDyn *exports = req_package_map(pkg, "exports"); + if (req_has_prefix(r->request, "#") && req_package_map(pkg, "imports") != NULL) { + r->refusal = "a package \"imports\" specifier"; + } else if (name != NULL && exports != NULL) { + bool whole = scr_str_eq(name, r->request); + bool prefixed = r->request->len > name->len && + memcmp(r->request->data, name->data, name->len) == 0 && + r->request->data[name->len] == '/'; + if (whole || prefixed) { + ScrJsonBuf sub; + scr_jb_init(&sub); + scr_jb_putc(&sub, '.'); + scr_jb_write(&sub, r->request->data + name->len, r->request->len - name->len); + ScrStr *subpath = scr_jb_finish(&sub); + found = req_exports_resolve(r, dir, exports, subpath); + scr_str_release(subpath); + } + } + scr_dyn_release(pkg); + break; + } + ScrStr *next = req_dirname(dir); + scr_str_release(dir); + dir = next; + } + scr_str_release(dir); + return found; +} + +/* The builtin id `request` names (+1), or NULL. */ +static ScrStr *req_builtin(ScrRequire *r) { + ScrDyn *id = scr_dyn_new_str(r->request); + ScrStr *bare = scr_process_builtin_id(id, r->builtins); + scr_dyn_release(id); + if (bare == NULL || bare->len == 0) { + if (bare) scr_str_release(bare); + return NULL; + } + scr_str_release(bare); + return scr_str_retain(r->request); +} + +/* Module._resolveFilename: +1 resolved id or path; NULL when nothing + * resolves (or the refusal is set). */ +static ScrStr *req_resolve_filename(ScrRequire *r) { + ScrStr *builtin = req_builtin(r); + if (builtin != NULL) return builtin; + ScrStr *found = req_try_self(r); + if (found != NULL || r->refusal != NULL) return found; + bool lookup = r->request->len == 0 || r->request->data[0] != '.' || + (r->request->len > 1 && r->request->data[1] != '.' && r->request->data[1] != '/' +#ifdef _WIN32 + && r->request->data[1] != '\\' +#endif + ); + if (lookup) { + ScrStr *dir = req_dirname(r->importer); + ScrStr **paths = NULL; + size_t count = req_lookup_paths(dir, &paths); + scr_str_release(dir); + found = req_find_path(r, paths, count); + for (size_t i = 0; i < count; i++) scr_str_release(paths[i]); + free(paths); + } else { + ScrStr *dir = req_dirname(r->importer); + found = req_find_path(r, &dir, 1); + scr_str_release(dir); + } + return found; +} + +/* ── entry points ────────────────────────────────────────────────────── */ + +/* Ends the program with the SC2020 refusal: it names the native addon, the + * builtin or file the specifier resolves to, or the resolution step, then + * the program exits with status 1 as after an uncaught error, running the + * 'exit' listeners. */ +static void req_refuse(ScrRequire *r, const char *call, const ScrStr *found, bool builtin, ScrStr *site) { + ScrJsonBuf text; + scr_jb_init(&text); + if (r->refusal != NULL) { + scr_jb_puts(&text, call); + scr_jb_puts(&text, " of '"); + scr_jb_write(&text, r->request->data, r->request->len); + scr_jb_puts(&text, "' reaches "); + scr_jb_puts(&text, r->refusal); + scr_jb_puts(&text, ", which scriptc does not resolve at run time"); + } else if (builtin) { + scr_jb_puts(&text, call); + scr_jb_puts(&text, " of the builtin '"); + scr_jb_write(&text, found->data, found->len); + scr_jb_puts(&text, "' with a computed specifier is not supported; require it with a string literal"); + } else if (req_has_suffix(found, ".node")) { + scr_jb_puts(&text, "native addon '"); + scr_jb_write(&text, found->data, found->len); + scr_jb_puts(&text, "' requires Node's addon runtime, which is unavailable in scriptc"); + } else { + scr_jb_puts(&text, call); + scr_jb_puts(&text, " of '"); + scr_jb_write(&text, r->request->data, r->request->len); + scr_jb_puts(&text, "' resolves to '"); + scr_jb_write(&text, found->data, found->len); + scr_jb_puts(&text, "', which is not part of the compiled program: the module graph is fixed at build time"); + } + scr_jb_puts(&text, " [SC2020 at "); + scr_jb_write(&text, site->data, site->len); + scr_jb_puts(&text, "]\n"); + ScrStr *message = scr_jb_finish(&text); + fflush(stdout); + fputs("scriptc: ", stderr); + fwrite(message->data, 1, message->len, stderr); + fflush(stderr); + scr_str_release(message); + scr_process_exit(1); +} + +static void req_throw(const char *code, const char *prefix, ScrRequire *r, bool stack) { + ScrJsonBuf text; + scr_jb_init(&text); + scr_jb_puts(&text, prefix); + scr_jb_write(&text, r->request->data, r->request->len); + if (stack) { + scr_jb_puts(&text, "'\nRequire stack:\n- "); + scr_jb_write(&text, r->importer->data, r->importer->len); + } + ScrStr *message = scr_jb_finish(&text); + scr_throw_error_msg_code(SCR_ERR_ERROR, message->data, message->len, code); + scr_str_release(message); +} + +ScrDyn *scr_require_computed(ScrDyn *id, ScrStr *importer, ScrStr *site, ScrArr *builtins) { + if (id->kind != SCR_DYN_STR) { + scr_dyn_arg_type_fail("id", "of type string", id); + return NULL; + } + if (id->v.str->len == 0) { + static const char message[] = "The argument 'id' must be a non-empty string. Received ''"; + scr_throw_error_msg_code(SCR_ERR_TYPE, message, sizeof message - 1, "ERR_INVALID_ARG_VALUE"); + return NULL; + } + ScrRequire r = { id->v.str, importer, builtins, NULL }; + ScrStr *builtin = req_builtin(&r); + if (builtin == NULL && req_has_prefix(r.request, "node:")) { + req_throw("ERR_UNKNOWN_BUILTIN_MODULE", "No such built-in module: ", &r, false); + return NULL; + } + ScrStr *found = builtin != NULL ? builtin : req_resolve_filename(&r); + if (found == NULL && r.refusal == NULL) { + req_throw("MODULE_NOT_FOUND", "Cannot find module '", &r, true); + return NULL; + } + req_refuse(&r, "createRequire's require", found, builtin != NULL, site); + if (found) scr_str_release(found); + return NULL; +} + +/* require.resolve's options: Node reads `paths` and `conditions` from an + * object; other values select the default lookup. */ +static const char *req_options_refusal(const ScrDyn *options) { + switch (options->kind) { + case SCR_DYN_UNDEF: + case SCR_DYN_NULL: + case SCR_DYN_NUM: + case SCR_DYN_BOOL: + case SCR_DYN_STR: + return NULL; + case SCR_DYN_OBJ: { + const ScrDyn *paths = scr_dyn_obj_get(options, "paths", 5); + const ScrDyn *conditions = scr_dyn_obj_get(options, "conditions", 10); + if (paths != NULL && paths->kind != SCR_DYN_UNDEF) return "require.resolve's paths option"; + if (conditions != NULL && conditions->kind != SCR_DYN_UNDEF) return "require.resolve's conditions option"; + return NULL; + } + default: + return "require.resolve options that are not a plain object"; + } +} + +ScrStr *scr_require_resolve(ScrDyn *request, ScrDyn *options, ScrStr *importer, ScrStr *site, + ScrArr *builtins) { + if (request->kind != SCR_DYN_STR) { + scr_dyn_arg_type_fail("request", "of type string", request); + return NULL; + } + ScrRequire r = { request->v.str, importer, builtins, req_options_refusal(options) }; + ScrStr *found = r.refusal == NULL ? req_resolve_filename(&r) : NULL; + if (r.refusal != NULL) { + if (found) scr_str_release(found); + req_refuse(&r, "createRequire's require.resolve", NULL, false, site); + return NULL; + } + if (found == NULL) req_throw("MODULE_NOT_FOUND", "Cannot find module '", &r, true); + return found; +} diff --git a/packages/runtime/src/scr_runtime.h b/packages/runtime/src/scr_runtime.h index 79c4a105f..93ecaa468 100644 --- a/packages/runtime/src/scr_runtime.h +++ b/packages/runtime/src/scr_runtime.h @@ -31,6 +31,7 @@ #include "scr_string_methods.h" #include "scr_qs.h" #include "scr_array.h" +#include "scr_ieee754.h" #include "scr_regex.h" #include "scr_map.h" #include "scr_closure.h" diff --git a/packages/runtime/src/scr_stream.c b/packages/runtime/src/scr_stream.c index cd95e12cd..fdd7179e5 100644 --- a/packages/runtime/src/scr_stream.c +++ b/packages/runtime/src/scr_stream.c @@ -93,6 +93,10 @@ struct ScrStreamState { bool is_transform; /* the writable side routes through transform/flush */ bool allow_half_open; bool auto_destroy, emit_close; + /* Duplex.from of a pipeline() argument that is not a stream: the + * pipeline takes it as that stage's stream without wiring the stage + * (scr_stream_unbox_dyn_stage) */ + bool pipeline_body; /* shared lifecycle */ bool destroyed, destroy_calling, error_emitted, close_emitted, error_scheduled, close_scheduled; @@ -115,6 +119,10 @@ struct ScrStreamState { bool reading, in_read_sync, ended, end_emitted, end_scheduled; bool in_read_call; /* inside the user's _read (in_read_sync also starts true) */ bool need_readable, emitted_readable, readable_listening; + /* the stream has 'data' listeners: kept by scr_stream_listener_observe + * from the registry's empty/nonempty transitions, so the per-chunk + * paths read it instead of looking the name up */ + bool data_listeners; bool resume_scheduled, maybe_more_scheduled; /* The buffer's representation: encoded means every entry is a ScrStr, * otherwise every entry is a ScrBytes (scr_stream_rbuf_push checks @@ -176,6 +184,9 @@ struct ScrStreamState { size_t n, cap, head; int corked; bool writing, wsync, need_drain, after_scheduled; + /* the queued SCR_ST_AFTER_WRITE tick later synchronous writes with + * the same callback join (Node's afterWriteTickInfo), or NULL */ + struct ScrStreamTick *after_tick; /* Deferred afterWrites Node also defers (a user callback, a 'drain' * or a destroyed stream): its pendingcb, which holds the finish until * they ran — afterWrite's finishMaybe then puts 'finish' on a tick @@ -239,6 +250,16 @@ struct ScrStreamState { ScrClosure *destroy_cb; ScrStreamErrInv destroy_inv; + /* The completion closures (`callback`) the emitted option-callback + * thunks pass, owned. write_done is the one every write and writev call + * receives (scr_stream_write_closure); done_cache is the last one a + * transform, flush, final, construct or destroy call received, handed + * out again while this is its only reference (scr_stream_done_closure). + * Both hold the stream, so they are dropped at 'close' and traced for a + * stream dropped before it closes. */ + ScrClosure *write_done; + ScrClosure *done_cache; + /* finished()/pipeline() watchers: fired once at the terminal point * (the 'close' tick — or a registration-time tick when already closed) * with the stream's finish status (NULL / the error / premature close). @@ -410,6 +431,8 @@ static void scr_stream_state_drop(ScrStreamState *st, bool gc) { if (st->transform_cb) scr_closure_release(st->transform_cb); if (st->flush_cb) scr_closure_release(st->flush_cb); if (st->destroy_cb) scr_closure_release(st->destroy_cb); + if (st->write_done) scr_closure_release(st->write_done); + if (st->done_cache) scr_closure_release(st->done_cache); for (size_t i = 0; i < st->w.end_cbs_n; i++) scr_closure_release(st->w.end_cbs[i]); for (size_t i = 0; i < st->fin.n; i++) scr_closure_release(st->fin.cb[i]); for (size_t i = 0; i < st->pipes.n; i++) scr_stream_release(st->pipes.dst[i]); @@ -458,6 +481,8 @@ void scr_stream_st_trace(ScrStreamState *st, ScrTraceVisit visit, void *ctx) { if (st->transform_cb) visit(st->transform_cb, ctx); if (st->flush_cb) visit(st->flush_cb, ctx); if (st->destroy_cb) visit(st->destroy_cb, ctx); + if (st->write_done) visit(st->write_done, ctx); + if (st->done_cache) visit(st->done_cache, ctx); for (size_t i = 0; i < st->w.end_cbs_n; i++) visit(st->w.end_cbs[i], ctx); for (size_t i = 0; i < st->fin.n; i++) visit(st->fin.cb[i], ctx); for (size_t i = st->w.head; i < st->w.n; i++) { @@ -512,6 +537,56 @@ void scr_stream_release(ScrStream *s) { void *scr_stream_retain_v(void *s) { return scr_stream_retain(s); } void scr_stream_release_v(void *s) { scr_stream_release(s); } +/* A completion closure: a 1-cap closure calling fn over a box that holds + * the stream. Returns +1. */ +static ScrClosure *scr_stream_done_new(ScrStream *s, void *fn) { + ScrClosure *c = scr_closure_new(fn, 1); + ScrBox *bx = scr_box_new_obj(scr_stream_retain_v, scr_stream_release_v, scr_stream_trace); + c->caps[0] = bx; + scr_box_set_ref(bx, scr_stream_retain(s)); + return c; +} + +/* Keeps c in *slot, replacing the closure there. Nothing is kept after + * 'close'. Borrows c. */ +static void scr_stream_done_keep(ScrStreamState *st, ScrClosure **slot, ScrClosure *c) { + if (st->close_emitted) return; + ScrClosure *old = *slot; + *slot = scr_closure_retain(c); + if (old) scr_closure_release(old); +} + +/* The `callback` a write or writev option callback receives. Node passes + * the stream's one onwrite function to every _write and _writev call, so + * every call receives the same closure, kept or not. Returns +1. */ +ScrClosure *scr_stream_write_closure(ScrStream *s, void *fn) { + ScrStreamState *st = s->st; + ScrClosure *c = st->write_done; + if (c != NULL && c->fn == fn) return scr_closure_retain(c); + c = scr_stream_done_new(s, fn); + scr_stream_done_keep(st, &st->write_done, c); + return c; +} + +/* The `callback` a transform, flush, final, construct or destroy option + * callback receives. Node passes a new function to each call. The stream + * keeps the last closure and hands it out again while it holds the only + * reference and the closure has no own properties; weak collections then + * forget it first, as they would at its free, so it is a new key to them. + * Otherwise a new closure replaces it, and one the user kept keeps its own + * stream reference. Returns +1. */ +ScrClosure *scr_stream_done_closure(ScrStream *s, void *fn) { + ScrStreamState *st = s->st; + ScrClosure *c = st->done_cache; + if (c != NULL && c->rc == 1 && c->fn == fn && c->props == NULL) { + scr_weak_dispose_inline(c); + return scr_closure_retain(c); + } + c = scr_stream_done_new(s, fn); + scr_stream_done_keep(st, &st->done_cache, c); + return c; +} + /* ── the tick queue (the nextTick stand-in) ───────────────────────────── */ typedef enum { @@ -543,6 +618,13 @@ typedef struct ScrStreamTick { ScrError *err; /* owned or NULL (SCR_ST_ERROR) */ ScrClosure *cb; /* owned or NULL (SCR_ST_AFTER_WRITE / SCR_ST_WCB_ERR / SCR_ST_WRITE_ERR) */ bool hold; /* SCR_ST_AFTER_WRITE: counted in w.after_holds */ + /* SCR_ST_AFTER_WRITE: the synchronous writes it completes, which share + * cb (Node's afterWriteTickInfo.count) */ + size_t count; + /* The AsyncLocalStorage context active when the tick was scheduled (+1, + * NULL outside a store): Node schedules these steps with + * process.nextTick, so their events and callbacks run in it. */ + ScrAlsCtx *ctx; struct ScrStreamTick *next; } ScrStreamTick; @@ -553,12 +635,13 @@ static void scr_stream_dispatch_one(void); static void scr_st_tick(ScrStream *s, ScrStreamTickOp op, ScrError *err /*moves*/, ScrClosure *cb /*moves*/) { - ScrStreamTick *t = calloc(1, sizeof *t); + ScrStreamTick *t = scr_mem_calloc(sizeof *t); if (!t) scr_stream_oom(); t->s = scr_stream_retain(s); t->op = op; t->err = err; t->cb = cb; + t->ctx = scr_als_capture(); if (scr_st_tail) scr_st_tail->next = t; else scr_st_head = t; scr_st_tail = t; @@ -575,15 +658,57 @@ static bool scr_stream_ticks_pending(void) { return scr_st_head != NULL; } /* ── emit helpers ─────────────────────────────────────────────────────── */ +/* The event names the stream unit emits, as immortal strings (the layout + * of an emitted string literal), so an emit allocates no name. */ +#define SCR_STREAM_EVENT_NAME(name) \ + static const struct { \ + size_t rc; \ + size_t len; \ + size_t cap; \ + char data[sizeof #name]; \ + } scr_stream_lit_##name = {SIZE_MAX, sizeof #name - 1, \ + SCR_STR_CAP_WORD(sizeof #name - 1, 0, 1), #name} +SCR_STREAM_EVENT_NAME(data); +SCR_STREAM_EVENT_NAME(error); +SCR_STREAM_EVENT_NAME(close); +SCR_STREAM_EVENT_NAME(drain); +SCR_STREAM_EVENT_NAME(end); +SCR_STREAM_EVENT_NAME(finish); +SCR_STREAM_EVENT_NAME(pause); +SCR_STREAM_EVENT_NAME(prefinish); +SCR_STREAM_EVENT_NAME(readable); +SCR_STREAM_EVENT_NAME(resume); +SCR_STREAM_EVENT_NAME(pipe); +SCR_STREAM_EVENT_NAME(unpipe); +#undef SCR_STREAM_EVENT_NAME + +static ScrStr *const scr_stream_event_names[] = { + (ScrStr *)&scr_stream_lit_data, (ScrStr *)&scr_stream_lit_error, + (ScrStr *)&scr_stream_lit_close, (ScrStr *)&scr_stream_lit_drain, + (ScrStr *)&scr_stream_lit_end, (ScrStr *)&scr_stream_lit_finish, + (ScrStr *)&scr_stream_lit_pause, (ScrStr *)&scr_stream_lit_prefinish, + (ScrStr *)&scr_stream_lit_readable, (ScrStr *)&scr_stream_lit_resume, + (ScrStr *)&scr_stream_lit_pipe, (ScrStr *)&scr_stream_lit_unpipe, +}; + +/* The event name as a string: an immortal one for the names above, else a + * new string (+1 either way; releasing an immortal string does nothing). */ +static ScrStr *scr_stream_event_name(const char *name) { + for (size_t i = 0; i < sizeof scr_stream_event_names / sizeof *scr_stream_event_names; i++) { + if (strcmp(scr_stream_event_names[i]->data, name) == 0) return scr_stream_event_names[i]; + } + return scr_str_new(name, strlen(name)); +} + static bool scr_stream_emit0(ScrStream *s, const char *name) { - ScrStr *n = scr_str_new(name, strlen(name)); + ScrStr *n = scr_stream_event_name(name); bool had = scr_emitter_emit((ScrEmitter *)s, n); scr_str_release(n); return had; } static void scr_stream_emit_stream(ScrStream *s, const char *name, ScrStream *payload) { - ScrStr *n = scr_str_new(name, strlen(name)); + ScrStr *n = scr_stream_event_name(name); scr_emitter_emit((ScrEmitter *)s, n, payload); scr_str_release(n); } @@ -591,7 +716,7 @@ static void scr_stream_emit_stream(ScrStream *s, const char *name, ScrStream *pa /* 'error': unhandled throws the payload (Node's crash), via the emitter's * error contract. Borrows err. */ static void scr_stream_emit_error(ScrStream *s, ScrError *err) { - ScrStr *n = scr_str_new("error", 5); + ScrStr *n = (ScrStr *)&scr_stream_lit_error; scr_emitter_emit_error((ScrEmitter *)s, n, err); scr_str_release(n); } @@ -760,10 +885,14 @@ static void scr_stream_emit_data(ScrStream *s, void *chunk, ScrEntryKind kind) { } else { b = chunk; } - ScrStr *n = scr_str_new("data", 4); - scr_emitter_emit((ScrEmitter *)s, n, b, str, value); - scr_str_release(n); - if (scr_exc_pending()) return; + /* emit('data') with no listeners does nothing; a piped stream delivers + * through st->pipes below */ + if (st->r.data_listeners) { + ScrStr *n = (ScrStr *)&scr_stream_lit_data; + scr_emitter_emit((ScrEmitter *)s, n, b, str, value); + scr_str_release(n); + if (scr_exc_pending()) return; + } /* pipe delivery, with backpressure: a full destination pauses us until * its 'drain'. Encoded sources write the string (utf8 bytes at the * destination — Node writes the decoded string through). */ @@ -901,12 +1030,18 @@ static bool scr_stream_add_chunk(ScrStream *s, ScrBytes *chunk, bool front) { } entry = dec; /* owned */ } else { - entry = scr_bytes_retain(chunk); entry_len = chunk->len; + if (entry_len == 0) { + /* Node's readableAddChunk adds no empty chunk outside object mode: + * it clears reading and may read more */ + if (!front) scr_stream_maybe_read_more(s); + return !st->r.ended && (st->r.length < st->r.hwm || st->r.length == 0); + } + entry = scr_bytes_retain(chunk); } if (entry_len > 0 && st->r.flowing == 1 && st->r.length == 0 && !st->r.in_read_sync && !front && - (scr_emitter_has((ScrEmitter *)s, "data") || st->pipes.n > 0)) { + (st->r.data_listeners || st->pipes.n > 0)) { /* the direct-emit fast path: flowing, empty buffer, async push */ if (st->r.emitted_readable) st->r.emitted_readable = false; ScrEntryKind kind = scr_stream_rkind(st); @@ -1875,7 +2010,7 @@ static int scr_stream_add_entry(ScrStream *s, void *entry /*moves*/, ScrEntryKin } st->r.reading = false; if (st->r.flowing == 1 && st->r.length == 0 && !st->r.in_read_sync && - (scr_emitter_has((ScrEmitter *)s, "data") || st->pipes.n > 0)) { + (st->r.data_listeners || st->pipes.n > 0)) { if (st->r.emitted_readable) st->r.emitted_readable = false; scr_stream_emit_data(s, entry, kind); scr_stream_entry_release_kind(entry, kind); @@ -2070,7 +2205,7 @@ static void scr_stream_error_buffer(ScrStream *s) { * empty), THEN the user's write(chunk, cb) callback, then finishMaybe. * The buffered queue was already restarted by write_done (Node's onwrite * calls clearBuffer before afterWrite). cb moves. */ -static void scr_stream_after_write(ScrStream *s, ScrClosure *cb) { +static void scr_stream_after_write(ScrStream *s, ScrClosure *cb, size_t count) { ScrStreamState *st = s->st; if (st->w.length == 0 && st->w.need_drain && !st->w.ending && !st->destroyed) { st->w.need_drain = false; @@ -2090,7 +2225,11 @@ static void scr_stream_after_write(ScrStream *s, ScrClosure *cb) { } } if (cb) { - ((void (*)(ScrClosure *))cb->fn)(cb); + /* afterWrite: the callback once per write the tick completes */ + for (size_t i = 0; i < count; i++) { + ((void (*)(ScrClosure *))cb->fn)(cb); + if (scr_exc_pending()) break; + } scr_closure_release(cb); if (scr_exc_pending()) return; } @@ -2370,6 +2509,20 @@ static ScrStreamState *scr_stream_state_new(bool has_r, bool has_w, return st; } +/* The registry's empty/nonempty transitions for the names the per-chunk + * paths test (scr_emitter_observe): no script runs here. */ +static void scr_stream_listener_observe(void *context, const ScrStr *name, bool present) { + ScrStream *s = context; + if (s->st != NULL && name->len == 4 && memcmp(name->data, "data", 4) == 0) + s->st->r.data_listeners = present; +} + +/* Starts tracking the stream's 'data' listeners, once its state exists. */ +static void scr_stream_watch_listeners(ScrStream *s) { + s->st->r.data_listeners = scr_emitter_has((ScrEmitter *)s, "data"); + scr_emitter_observe((ScrEmitter *)s, &scr_stream_listener_observe, s); +} + static ScrStream *scr_stream_alloc(const ScrVt *vt, const char *cls, bool has_r, bool has_w, double rhwm, double whwm, bool auto_destroy, bool emit_close, bool allow_half_open) { @@ -2398,6 +2551,7 @@ static ScrStream *scr_stream_alloc(const ScrVt *vt, const char *cls, bool has_r, scr_emitter_reserve(em, "end"); scr_emitter_reserve(em, "readable"); } + scr_stream_watch_listeners(s); scr_obj_alloc_note(); return s; } @@ -2520,6 +2674,7 @@ void scr_stream_init_readable(ScrStream *s, double hwm, bool auto_destroy, bool ScrClosure *destroy, ScrStreamErrInv destroy_inv) { ScrStreamState *st = scr_stream_state_new(true, false, hwm, -1, auto_destroy, emit_close, true); s->st = st; + scr_stream_watch_listeners(s); st->r.read_cb = read; st->r.read_inv = read_inv; st->destroy_cb = destroy; @@ -2532,6 +2687,7 @@ void scr_stream_init_writable(ScrStream *s, double hwm, bool auto_destroy, bool ScrClosure *destroy, ScrStreamErrInv destroy_inv) { ScrStreamState *st = scr_stream_state_new(false, true, -1, hwm, auto_destroy, emit_close, true); s->st = st; + scr_stream_watch_listeners(s); st->w.write_cb = write; st->w.write_inv = write_inv; st->w.final_cb = final_cb; @@ -2549,6 +2705,7 @@ void scr_stream_init_duplex(ScrStream *s, double rhwm, double whwm, bool auto_de ScrStreamState *st = scr_stream_state_new(readable_side, writable_side, rhwm, whwm, auto_destroy, emit_close, allow_half_open); s->st = st; + scr_stream_watch_listeners(s); if (!readable_side) { st->r.ended = true; st->r.end_emitted = true; @@ -2575,6 +2732,7 @@ static void scr_stream_init_transformish(ScrStream *s, bool passthrough, ScrStreamState *st = scr_stream_state_new(true, true, rhwm, whwm, auto_destroy, emit_close, allow_half_open); s->st = st; + scr_stream_watch_listeners(s); st->is_transform = !passthrough; st->passthrough = passthrough; /* the Transform constructor's readable-half stance (see new_transformish) */ @@ -3787,7 +3945,9 @@ typedef ScrPromise *(*ScrPipeNextFn)(ScrClosure *, ScrGen *); typedef ScrDyn *(*ScrPipeReadFn)(ScrClosure *, void *); typedef ScrDyn *(*ScrPipeBoxFn)(ScrClosure *, ScrPromise *); -enum { SCR_PIPE_DEST_STREAM, SCR_PIPE_DEST_GEN, SCR_PIPE_DEST_PROMISE }; +/* SCR_PIPE_DEST_DYN: an untyped destination function's result, a + * checked-dynamic value the pipeline classifies (scr_pipe_dest_dyn_kind) */ +enum { SCR_PIPE_DEST_STREAM, SCR_PIPE_DEST_GEN, SCR_PIPE_DEST_PROMISE, SCR_PIPE_DEST_DYN }; /* flags bit 7: the callback is typed code's (stream.pipelineStagesDyn) */ enum { SCR_PIPE_TYPED_CB = 128 }; @@ -3805,7 +3965,9 @@ typedef struct { * async generator destination's PassThrough included), in pipeline * order, or NULL */ ScrClosure **helpers; - ScrClosure *box; /* SCR_PIPE_DEST_PROMISE: boxes its promise as a dyn promise */ + /* SCR_PIPE_DEST_PROMISE: boxes its promise as a dyn promise; + * SCR_PIPE_DEST_DYN: answers the async generator over a dyn iterable */ + ScrClosure *box; } ScrPipeStages; static bool scr_pipe_is_stage(const ScrPipeStages *fs, size_t j) { @@ -4171,6 +4333,71 @@ static void scr_pipe_dest_promise(ScrStreamPipeCtx *ctx, ScrPromise *p /*moves*/ scr_promise_release(scr_async_spawn_after(d->boxed->v.promise, &scr_pipe_dest_entry, d)); } +/* An untyped destination's promise: scr_pipe_dest_promise over a promise + * that already holds dyn values. */ +static void scr_pipe_dest_dyn_promise(ScrStreamPipeCtx *ctx, ScrDyn *boxed /*moves*/, size_t pi, + bool end) { + ScrPipeDest *d = malloc(sizeof *d); + if (!d) scr_stream_oom(); + d->ctx = scr_stream_pipectx_retain_v(ctx); + d->typed = scr_promise_retain(boxed->v.promise); + d->boxed = boxed; + d->pi = pi; + d->end = end; + scr_promise_release(scr_async_spawn_after(d->boxed->v.promise, &scr_pipe_dest_entry, d)); +} + +typedef ScrGen *(*ScrPipeIterFn)(ScrClosure *, ScrDyn *); +static bool scr_stream_dyn_has_method(const ScrDyn *value, const char *key); +static bool scr_stream_dyn_transform_like(const ScrDyn *value); +static void scr_stream_throw_web_stage(void); +static bool scr_stream_dyn_stage_returned_stream(const ScrDyn *value); +static void scr_stream_throw_invalid_return(const char *expected, const char *name, + const ScrDyn *got); + +/* What pipelineImpl does with an untyped destination function's result: + * a promise settles the pipeline's value (the `then` branch), and an + * async iterable, or a { readable, writable } pair's readable side, is + * pumped into the PassThrough. *out answers the promise or that + * iterable's async generator (+1). Other values throw + * ERR_INVALID_RETURN_VALUE, and thenables that are not promises and web + * streams throw the not-supported Error; -1 then. */ +enum { SCR_PIPE_DYN_PROMISE, SCR_PIPE_DYN_ITER }; +static int scr_pipe_dest_dyn_kind(const ScrPipeStages *fs, ScrDyn *r, void **out) { + if (r->kind == SCR_DYN_PROMISE) { + *out = scr_dyn_retain(r); + return SCR_PIPE_DYN_PROMISE; + } + if (r->kind == SCR_DYN_OBJ && scr_stream_dyn_has_method(r, "then")) { + static const char msg[] = + "pipeline destinations that return a thenable other than a promise are not supported yet"; + scr_throw_error_msg_code(SCR_ERR_ERROR, msg, sizeof msg - 1, "SC2020"); + return -1; + } + if (scr_exc_pending() || scr_stream_dyn_stage_returned_stream(r)) return -1; + ScrDyn *read = NULL; + int kind = scr_dyn_iterable_kind(r); + if (kind < 0) return -1; + if (r->kind == SCR_DYN_HANDLE && + (r->v.handle.tag == SCR_DYNH_WEB_STREAM || r->v.handle.tag == SCR_DYNH_WEB_TRANSFORM)) { + scr_stream_throw_web_stage(); + return -1; + } + if (kind == 2) { + read = scr_dyn_retain(r); + } else if (scr_stream_dyn_transform_like(r)) { + read = scr_dyn_obj_read(r, "readable", 8); + } + if (!read) { + if (!scr_exc_pending()) scr_stream_throw_invalid_return("AsyncIterable or Promise", "destination", r); + return -1; + } + ScrGen *g = ((ScrPipeIterFn)fs->box->fn)(fs->box, read); + if (!g) return -1; + *out = g; + return SCR_PIPE_DYN_ITER; +} + static ScrStream *scr_stream_pipeline_ex(size_t n, ScrStream **streams, ScrClosure *cb, ScrStreamErrInv inv, bool end_opt, ScrDyn *signal, ScrAbortAddFn add, const ScrPipeStages *fs, @@ -4231,8 +4458,18 @@ static ScrStream *scr_stream_pipeline_ex(size_t n, ScrStream **streams, ScrClosu : (void *)scr_stream_retain(ret); ret_gen = NULL; void *out = scr_pipe_call_stage(fs, k++, src); + int dyn_kind = -1; + if (!scr_exc_pending() && dest && fs->dest == SCR_PIPE_DEST_DYN) { + ScrDyn *r = out ? out : scr_dyn_retain(scr_dyn_undefined()); + out = NULL; + dyn_kind = scr_pipe_dest_dyn_kind(fs, r, &out); + scr_dyn_release(r); + } if (scr_exc_pending()) { - if (out && dest) scr_promise_release(out); + if (out && dest && fs->dest == SCR_PIPE_DEST_DYN) { + if (dyn_kind == SCR_PIPE_DYN_ITER) scr_gen_release(out); + else scr_dyn_release(out); + } else if (out && dest) scr_promise_release(out); else if (out) scr_gen_release(out); scr_stream_pipectx_drop(ctx); scr_stream_pipectx_release_v(ctx); @@ -4252,9 +4489,12 @@ static ScrStream *scr_stream_pipeline_ex(size_t n, ScrStream **streams, ScrClosu size_t pi = ctx->n++; ctx->streams[pi] = pt; ctx->finish_count++; - if (fs->dest == SCR_PIPE_DEST_GEN) { + if (fs->dest == SCR_PIPE_DEST_GEN || + (fs->dest == SCR_PIPE_DEST_DYN && dyn_kind == SCR_PIPE_DYN_ITER)) { /* pumpToNode(ret, pt, finish, { end }) */ scr_pump_start(ctx, out, fs->helpers + 3 * h++, pt, end); + } else if (fs->dest == SCR_PIPE_DEST_DYN) { + scr_pipe_dest_dyn_promise(ctx, out, pi, end); } else { scr_pipe_dest_promise(ctx, out, fs->box, pi, end); } @@ -4269,6 +4509,20 @@ static ScrStream *scr_stream_pipeline_ex(size_t n, ScrStream **streams, ScrClosu continue; } ScrStream *s = streams[i]; + if (s->st->pipeline_body) { + /* ret = Duplex.from(stream) for an argument that is not a stream: + * no destroyer or 'error' listener, and the previous stage is + * neither piped nor pumped into it */ + s->st->pipeline_body = false; + if (ret_gen) { + scr_gen_release(ret_gen); + ret_gen = NULL; + h++; + } + ret = s; + i++; + continue; + } bool writing = j > 0; if (reading && !scr_pipe_is_stage(fs, j + 1) && (streams[i + 1]->st->close_emitted || streams[i + 1]->st->destroyed)) { @@ -5149,13 +5403,31 @@ static void scr_sp_value_inv(ScrClosure *cb, ScrError *err, ScrDyn *value, ScrPr scr_promise_release(p); } +/* scr_sp_value_inv for an untyped destination (SCR_PIPE_DEST_DYN), whose + * result promise carries a dyn value: when the destination returned an + * async iterable (no promise), or its promise carries no payload, the + * result is undefined, as in Node. */ +static void scr_sp_dyn_value_inv(ScrClosure *cb, ScrError *err, ScrDyn *value, + ScrPromise *value_p) { + if (err != NULL || (value_p != NULL && scr_promise_payload_kind(value_p) != SCR_EXC_NONE)) { + scr_sp_value_inv(cb, err, value, value_p); + return; + } + ScrPromise *p = scr_box_get_ref(cb->caps[0]); /* +1 */ + scr_promise_fulfill_ref(p, scr_dyn_retain(scr_dyn_undefined()), scr_dyn_retain_v, + scr_dyn_release_v, NULL); + scr_promise_release(p); +} + ScrPromise *scr_sp_pipeline_stages(double n, ScrStream **streams, double count, ScrClosure **stages, double positions, double flags, double mask, ScrClosure **helpers, ScrClosure *box, ScrDyn *signal, ScrClosure *abort) { ScrPromise *p = scr_promise_new(); ScrPipeStages fs = scr_pipe_stages_of(count, stages, positions, flags, mask, helpers, box, signal); - ScrPipeValInv vi = fs.dest != SCR_PIPE_DEST_STREAM ? &scr_sp_value_inv : NULL; + ScrPipeValInv vi = fs.dest == SCR_PIPE_DEST_STREAM ? NULL + : fs.dest == SCR_PIPE_DEST_DYN ? &scr_sp_dyn_value_inv + : &scr_sp_value_inv; ScrStream *dst = scr_stream_pipeline_ex((size_t)n, streams, scr_sp_watcher(p), &scr_sp_settle_inv, true, NULL, NULL, &fs, abort, vi); if (dst) scr_stream_release(dst); @@ -5207,7 +5479,9 @@ ScrPromise *scr_sp_pipeline_stages_opts(double n, ScrStream **streams, double co ScrPromise *p = scr_promise_new(); if (!scr_sp_opts_signal(p, &outer)) return p; ScrPipeStages fs = scr_pipe_stages_of(count, stages, positions, flags, mask, helpers, box, signal); - ScrPipeValInv vi = fs.dest != SCR_PIPE_DEST_STREAM ? &scr_sp_value_inv : NULL; + ScrPipeValInv vi = fs.dest == SCR_PIPE_DEST_STREAM ? NULL + : fs.dest == SCR_PIPE_DEST_DYN ? &scr_sp_dyn_value_inv + : &scr_sp_value_inv; ScrStream *dst = scr_stream_pipeline_ex((size_t)n, streams, scr_sp_watcher(p), &scr_sp_settle_inv, end, outer, add, &fs, abort, vi); if (dst) scr_stream_release(dst); @@ -6629,6 +6903,12 @@ ScrStream *scr_stream_on_dyn(ScrStream *s, ScrStr *name, ScrDyn *cb, bool once, return (ScrStream *)scr_emitter_on_dyn((ScrEmitter *)s, name, cb, adapter, error ? scr_ee_inv_fixed1 : scr_ee_inv_fixed0, once, prepend); } + /* The stream emits 'pipe' and 'unpipe' with its source stream; other + * names reach the listener only through emit calls, with every argument + * they pass. */ + bool piping = (name->len == 4 && memcmp(name->data, "pipe", 4) == 0) || + (name->len == 6 && memcmp(name->data, "unpipe", 6) == 0); + if (!piping) return (ScrStream *)scr_emitter_on_flex((ScrEmitter *)s, name, cb, once, prepend); static const char message[] = "this stream event has no checked-dynamic listener adapter"; scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); return NULL; @@ -6861,10 +7141,12 @@ static bool scr_stream_write_chunk_enc(ScrStream *s, ScrBytes *chunk /*borrowed* scr_stream_do_write(s, scr_stream_wchunk_retain(st, chunk), cb, enc); } /* Node computes the answer AFTER the (possibly synchronous) write: - * a sync completion has already drained its bytes from length. */ - bool ret = st->w.length < st->w.hwm && !st->destroyed && !st->errored; - if (!ret) st->w.need_drain = true; - return ret; + * a sync completion has already drained its bytes from length. Only + * the length sets needDrain; an errored or destroyed stream answers + * false without waiting for 'drain'. */ + bool below = st->w.length < st->w.hwm; + if (!below) st->w.need_drain = true; + return below && !st->destroyed && !st->errored; } static bool scr_stream_write_chunk(ScrStream *s, ScrBytes *chunk /*borrowed*/, ScrClosure *cb /*moves*/) { @@ -7115,13 +7397,24 @@ void scr_stream_write_done(ScrStream *s, ScrError *err /*moves*/) { } if (was_sync) { bool hold = ucb != NULL || (st->w.need_drain && st->w.length == 0) || st->destroyed; - scr_st_tick(s, SCR_ST_AFTER_WRITE, NULL, ucb); - if (hold) { - scr_st_tail->hold = true; + ScrStreamTick *pending = st->w.after_tick; + if (pending && pending->cb == ucb) { + /* Node's onwrite: a synchronous write with the callback of the + * afterWrite tick already queued joins it (count++) */ + pending->count++; + if (ucb) scr_closure_release(ucb); + } else { + scr_st_tick(s, SCR_ST_AFTER_WRITE, NULL, ucb); + pending = scr_st_tail; + pending->count = 1; + st->w.after_tick = pending; + } + if (hold && !pending->hold) { + pending->hold = true; st->w.after_holds++; } } else { - scr_stream_after_write(s, ucb); + scr_stream_after_write(s, ucb, 1); } } @@ -7667,13 +7960,19 @@ static void scr_stream_run_tick(ScrStreamTick *t) { case SCR_ST_AFTER_WRITE: { st->w.after_scheduled = false; if (t->hold) st->w.after_holds--; + /* afterWriteTick: the batch closes when any afterWrite tick runs, so + * a write its callback makes queues a new tick */ + st->w.after_tick = NULL; ScrClosure *cb = t->cb; t->cb = NULL; if (!st->destroyed) { - scr_stream_after_write(s, cb); + scr_stream_after_write(s, cb, t->count); } else { if (cb) { - ((void (*)(ScrClosure *))cb->fn)(cb); + for (size_t i = 0; i < t->count; i++) { + ((void (*)(ScrClosure *))cb->fn)(cb); + if (scr_exc_pending()) break; + } scr_closure_release(cb); if (scr_exc_pending()) break; } @@ -7752,6 +8051,8 @@ static void scr_stream_run_tick(ScrStreamTick *t) { if (st->transform_cb) { scr_closure_release(st->transform_cb); st->transform_cb = NULL; } if (st->flush_cb) { scr_closure_release(st->flush_cb); st->flush_cb = NULL; } if (st->destroy_cb) { scr_closure_release(st->destroy_cb); st->destroy_cb = NULL; } + if (st->write_done) { scr_closure_release(st->write_done); st->write_done = NULL; } + if (st->done_cache) { scr_closure_release(st->done_cache); st->done_cache = NULL; } } break; } @@ -7835,6 +8136,22 @@ static void scr_stream_run_tick(ScrStreamTick *t) { static void scr_stream_dispatch(void); +/* A tick runs in the context it was scheduled in. */ +static void scr_stream_run_tick_in_context(ScrStreamTick *t) { + ScrAlsCtx *prev = scr_als_dispatch_enter(t->ctx); + scr_stream_run_tick(t); + scr_als_dispatch_leave(prev); +} + +static void scr_stream_tick_free(ScrStreamTick *t) { + if (t->s->st && t->s->st->w.after_tick == t) t->s->st->w.after_tick = NULL; + scr_stream_release(t->s); + if (t->err) scr_error_release(t->err); + if (t->cb) scr_closure_release(t->cb); + scr_als_ctx_release(t->ctx); + scr_mem_free(t); +} + /* One tick-marker's dispatch: the queue's FIFO head (markers and entries * are enqueued 1:1 in the same order). After an uncaught throw the * remaining entries drop through the station's exc branch below, keeping @@ -7844,11 +8161,8 @@ static void scr_stream_dispatch_one(void) { if (t == NULL || scr_exc_pending()) return; scr_st_head = t->next; if (scr_st_head == NULL) scr_st_tail = NULL; - scr_stream_run_tick(t); - scr_stream_release(t->s); - if (t->err) scr_error_release(t->err); - if (t->cb) scr_closure_release(t->cb); - free(t); + scr_stream_run_tick_in_context(t); + scr_stream_tick_free(t); if (scr_exc_pending()) scr_stream_dispatch(); /* its exc branch drops the rest */ } @@ -7857,11 +8171,8 @@ static void scr_stream_dispatch(void) { ScrStreamTick *t = scr_st_head; scr_st_head = t->next; if (scr_st_head == NULL) scr_st_tail = NULL; - scr_stream_run_tick(t); - scr_stream_release(t->s); - if (t->err) scr_error_release(t->err); - if (t->cb) scr_closure_release(t->cb); - free(t); + scr_stream_run_tick_in_context(t); + scr_stream_tick_free(t); } /* An uncaught throw ends the process (the loop returns to main): the * undelivered ticks are DROPPED — release their references here so the @@ -7870,10 +8181,7 @@ static void scr_stream_dispatch(void) { while (scr_st_head != NULL) { ScrStreamTick *t = scr_st_head; scr_st_head = t->next; - scr_stream_release(t->s); - if (t->err) scr_error_release(t->err); - if (t->cb) scr_closure_release(t->cb); - free(t); + scr_stream_tick_free(t); } scr_st_tail = NULL; } @@ -8047,10 +8355,83 @@ static ScrStream *scr_stream_view_receiver(const ScrDyn *value) { /* The members a native stream answers to checked-dynamic calls. */ static const char *const scr_stream_dyn_methods[] = { "on", "once", "addListener", "prependListener", "prependOnceListener", - "off", "removeListener", "pipe", "unpipe", "read", "pause", "resume", "isPaused", - "setEncoding", "write", "end", "destroy", "cork", "uncork", + "off", "removeListener", "removeAllListeners", "listenerCount", "eventNames", "pipe", + "unpipe", "read", "push", "unshift", "pause", "resume", "isPaused", "setEncoding", "write", + "end", "destroy", "cork", "uncork", "setDefaultEncoding", }; +/* An event name argument: a string, +1; other values throw the "not + * supported yet" Error. */ +static ScrStr *scr_stream_dyn_event_name(const ScrDyn *value) { + if (value->kind == SCR_DYN_STR) return scr_str_retain(value->v.str); + static const char message[] = "event names other than strings on a native stream are not supported yet"; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); + return NULL; +} + +/* push(chunk, encoding): a string with an encoding pushes the string's + * bytes in that encoding, undefined pushes an empty chunk outside object + * mode, and other chunks push as push(chunk) does. */ +static ScrDyn *scr_stream_dyn_push(ScrStream *stream, ScrDyn *const *args, size_t argc) { + ScrDyn *chunk = argc ? args[0] : scr_dyn_undefined(); + ScrDyn *encoding = argc > 1 ? args[1] : scr_dyn_undefined(); + if (chunk->kind == SCR_DYN_STR && encoding->kind == SCR_DYN_STR) { + ScrStr *canonical = scr_bytes_normalize_encoding(encoding->v.str); + if (!canonical) return NULL; + bool more = scr_stream_push_str_enc(stream, chunk->v.str, canonical); + scr_str_release(canonical); + return scr_dyn_new_bool(more); + } + if (chunk->kind == SCR_DYN_UNDEF && !(stream->st->has_r && stream->st->r.object_entries)) { + ScrStr *empty = scr_str_new("", 0); + bool more = scr_stream_push_str(stream, empty); + scr_str_release(empty); + return scr_dyn_new_bool(more); + } + bool more = scr_stream_push_dyn(stream, chunk); + return scr_exc_pending() ? NULL : scr_dyn_new_bool(more); +} + +/* unshift(chunk[, encoding]): a byte-mode stream takes a string chunk with + * an encoding as the bytes Buffer.from(chunk, encoding) holds, as Node's + * readableAddChunkUnshift converts it; an unknown encoding throws + * ERR_UNKNOWN_ENCODING. Answers whether the stream takes more data + * afterwards, Node's canPushMore. */ +static ScrDyn *scr_stream_dyn_unshift(ScrStream *stream, ScrDyn *const *args, size_t argc) { + ScrDyn *chunk = argc ? args[0] : scr_dyn_undefined(); + ScrDyn *encoding = argc > 1 ? args[1] : scr_dyn_undefined(); + if (chunk->kind == SCR_DYN_STR && encoding->kind == SCR_DYN_STR && + !(stream->st->has_r && stream->st->r.object_entries)) { + ScrStr *canonical = scr_bytes_normalize_encoding(encoding->v.str); + if (!canonical) return NULL; + ScrBytes *b = scr_stream_buffer(scr_bytes_from_str(chunk->v.str, canonical)); + scr_str_release(canonical); + scr_stream_unshift(stream, b); + scr_bytes_release(b); + } else { + scr_stream_unshift_dyn(stream, chunk); + } + if (scr_exc_pending()) return NULL; + ScrStreamState *st = stream->st; + bool more = chunk->kind != SCR_DYN_NULL && st->has_r && !st->destroyed && !st->errored && + !st->r.ended && (st->r.length < st->r.hwm || st->r.length == 0); + return scr_dyn_new_bool(more); +} + +/* eventNames(): the stream's event names as a string array, +1. */ +static ScrDyn *scr_stream_dyn_event_names(ScrStream *stream) { + ScrArr *names = scr_emitter_event_names((ScrEmitter *)stream); + ScrDyn *result = scr_dyn_new_arr(); + double length = scr_arr_len(names); + for (double i = 0; i < length; i++) { + ScrStr *name = scr_arr_get_ref(names, i); + scr_dyn_arr_push(result, scr_dyn_new_str(name)); + scr_str_release(name); + } + scr_arr_release(names); + return result; +} + /* A checked-dynamic call of one of a native stream's members (borrowed * stream): the result, +1, with `this` results answering `self`, or the * current `this` when self is NULL. */ @@ -8075,6 +8456,29 @@ static ScrDyn *scr_stream_dyn_call(ScrStream *stream, ScrDyn *self, const char * argc > 1 ? args[1] : scr_dyn_undefined()); scr_emitter_release(registered); } + } else if (!strcmp(method, "removeAllListeners")) { + bool all = first->kind == SCR_DYN_UNDEF; + ScrStr *name = all ? scr_str_new("", 0) : scr_stream_dyn_event_name(first); + if (name) { + scr_emitter_release(scr_emitter_remove_all((ScrEmitter *)stream, name, all)); + scr_str_release(name); + } + } else if (!strcmp(method, "listenerCount")) { + ScrStr *name = scr_stream_dyn_event_name(first); + if (name) { + ScrDyn *only = argc > 1 ? args[1] : scr_dyn_undefined(); + double count = only->kind == SCR_DYN_UNDEF || only->kind == SCR_DYN_NULL + ? scr_emitter_listener_count((ScrEmitter *)stream, name) + : only->kind == SCR_DYN_FUNC ? scr_emitter_listener_count_dyn((ScrEmitter *)stream, name, only) : 0; + scr_str_release(name); + if (!scr_exc_pending()) result = scr_dyn_new_num(count); + } + } else if (!strcmp(method, "eventNames")) result = scr_stream_dyn_event_names(stream); + else if (!strcmp(method, "push")) result = scr_stream_dyn_push(stream, args, argc); + else if (!strcmp(method, "unshift")) result = scr_stream_dyn_unshift(stream, args, argc); + else if (!strcmp(method, "setDefaultEncoding")) { + ScrStream *returned = scr_stream_set_default_encoding(stream, first); + if (returned) scr_stream_release(returned); } else if (!strcmp(method, "pipe") || !strcmp(method, "unpipe")) { ScrStream *destination = first->kind == SCR_DYN_UNDEF && !strcmp(method, "unpipe") ? NULL : scr_stream_view_receiver(first); @@ -8209,6 +8613,11 @@ ScrDyn *scr_stream_dyn_view(ScrStream *stream) { const char *name = scr_stream_dyn_properties[i]; scr_dyn_obj_set(view, name, strlen(name), scr_stream_dyn_property(stream, name)); } + if (stream->st->has_r) { + int flowing = stream->st->r.flowing; + scr_dyn_obj_set(view, "readableFlowing", 15, + flowing < 0 ? scr_dyn_new_null() : scr_dyn_new_bool(flowing == 1)); + } if (stream->st->native && stream->st->native_ops && stream->st->native_ops->view) { ScrClosure *cb = scr_closure_new((void *)scr_stream_view_call, 2); cb->caps[0] = scr_box_new_obj(scr_stream_retain_v, scr_stream_release_v, scr_stream_trace); @@ -8396,3 +8805,185 @@ ScrStream *scr_stream_unbox_dyn(const ScrDyn *value, const ScrStr *message) { scr_throw_error_msg_code(SCR_ERR_ERROR, message->data, message->len, "SC2020"); return NULL; } + +/* Whether a checked-dynamic object's member is of type "object". */ +static bool scr_stream_dyn_member_object(const ScrDyn *value, const char *key) { + ScrDyn *m = scr_dyn_obj_read(value, key, strlen(key)); + if (!m) return false; + ScrStr *t = scr_dyn_typeof(m); + bool object = t->len == 6 && memcmp(t->data, "object", 6) == 0; + scr_str_release(t); + scr_dyn_release(m); + return object; +} + +/* pipeline.js's isTransformStream for a value that is not a stream: an + * object whose readable and writable members are both of type "object". */ +static bool scr_stream_dyn_transform_like(const ScrDyn *value) { + if (value->kind == SCR_DYN_HANDLE) return value->v.handle.tag == SCR_DYNH_WEB_TRANSFORM; + return value->kind == SCR_DYN_OBJ && scr_stream_dyn_member_object(value, "readable") && + !scr_exc_pending() && scr_stream_dyn_member_object(value, "writable"); +} + +/* Whether a checked-dynamic object has a function member `key`. */ +static bool scr_stream_dyn_has_method(const ScrDyn *value, const char *key) { + ScrDyn *m = scr_dyn_obj_read(value, key, strlen(key)); + if (!m) return false; + bool fn = m->kind == SCR_DYN_FUNC; + scr_dyn_release(m); + return fn; +} + +/* pipeline.js's isNodeStream and isWebStream for a checked-dynamic value + * that is not a native stream: a stream-like object or a web stream, which + * the pipeline would pipe or pump into and has no lowering for. */ +static bool scr_stream_dyn_stream_like(const ScrDyn *value) { + if (value->kind == SCR_DYN_HANDLE) { + int tag = value->v.handle.tag; + return tag == SCR_DYNH_WEB_STREAM || tag == SCR_DYNH_WEB_TRANSFORM || tag == SCR_DYNH_WEB_WRITABLE; + } + if (value->kind != SCR_DYN_OBJ) return false; + static const char *const state_keys[] = {"_readableState", "_writableState"}; + for (size_t i = 0; i < 2; i++) { + ScrDyn *v = scr_dyn_obj_read(value, state_keys[i], strlen(state_keys[i])); + bool set = v && scr_dyn_truthy(v); + if (v) scr_dyn_release(v); + if (set) return true; + } + bool on = scr_stream_dyn_has_method(value, "on"); + if (on && (scr_stream_dyn_has_method(value, "write") || scr_stream_dyn_has_method(value, "pipe"))) + return true; + if (scr_stream_dyn_has_method(value, "pipeThrough") && + scr_stream_dyn_has_method(value, "getReader") && scr_stream_dyn_has_method(value, "cancel")) + return true; + if (scr_stream_dyn_has_method(value, "getWriter") && scr_stream_dyn_has_method(value, "abort")) + return true; + return scr_stream_dyn_transform_like(value); +} + +/* A checked-dynamic pipeline() stage that the pipeline takes as a stream + * (the stream.unboxDynStage libCall): a boxed native stream or a stream + * handle answers its stream (+1). Any other value that is not stream-like + * or a function takes pipelineImpl's last branch, Duplex.from(stream): + * an iterable answers a readable Duplex the pipeline uses without piping + * the previous stage into it, and values Duplex.from rejects throw its + * ERR_INVALID_ARG_TYPE. Stream-like objects, web streams and functions + * throw `message` as the not-supported Error. */ +ScrStream *scr_stream_unbox_dyn_stage(const ScrDyn *value, const ScrStr *message) { + ScrStream *stream = scr_stream_of_handle(value); + if (!stream) stream = scr_stream_boxed(value); + if (stream) return stream; + if (value->kind == SCR_DYN_FUNC || scr_stream_dyn_stream_like(value)) { + if (!scr_exc_pending()) + scr_throw_error_msg_code(SCR_ERR_ERROR, message->data, message->len, "SC2020"); + return NULL; + } + if (scr_exc_pending()) return NULL; + ScrStream *d = scr_stream_duplex_from_dyn((ScrDyn *)value); + if (d) d->st->pipeline_body = true; + return d; +} + +/* pipeline.js's first stage: a function is a source function, which the + * pipeline calls; a value with pipe() and on() methods is a readable node + * stream, which the stage ladder tests before isIterable; a string, a + * value with a Symbol.asyncIterator or Symbol.iterator method, or a + * { readable, writable } pair is an iterable the pipeline pumps into the + * next stage. */ +bool scr_stream_dyn_iterable_source(const ScrDyn *value) { + ScrStream *stream = scr_stream_of_handle(value); + if (!stream) stream = scr_stream_boxed(value); + if (stream) { + scr_stream_release(stream); + return false; + } + if (value->kind == SCR_DYN_STR || value->kind == SCR_DYN_FUNC) return true; + if (value->kind == SCR_DYN_HANDLE && value->v.handle.tag == SCR_DYNH_WEB_TRANSFORM) return true; + if (value->kind == SCR_DYN_OBJ) { + ScrDyn *pipe = scr_dyn_obj_read(value, "pipe", 4); + if (!pipe) return false; + bool streamish = pipe->kind == SCR_DYN_FUNC; + scr_dyn_release(pipe); + if (streamish) { + ScrDyn *on = scr_dyn_obj_read(value, "on", 2); + if (!on) return false; + streamish = on->kind == SCR_DYN_FUNC; + scr_dyn_release(on); + if (streamish) return false; + } + if (scr_stream_dyn_transform_like(value)) return true; + if (scr_exc_pending()) return false; + } + return scr_dyn_iterable_kind(value) > 0; +} + +/* Untyped web streams as pipeline stages have no lowering. */ +static void scr_stream_throw_web_stage(void) { + static const char msg[] = "pipeline stages that are untyped web streams are not supported yet"; + scr_throw_error_msg_code(SCR_ERR_ERROR, msg, sizeof msg - 1, "SC2020"); +} + +/* A native stream returned by an untyped pipeline function, which Node + * pipes or passes on as the stream: throws the not-supported Error. */ +static bool scr_stream_dyn_stage_returned_stream(const ScrDyn *value) { + ScrStream *stream = scr_stream_of_handle(value); + if (!stream) stream = scr_stream_boxed(value); + if (!stream) return false; + scr_stream_release(stream); + static const char msg[] = "pipeline function stages that return a stream are not supported yet"; + scr_throw_error_msg_code(SCR_ERR_ERROR, msg, sizeof msg - 1, "SC2020"); + return true; +} + +/* Node's ERR_INVALID_RETURN_VALUE TypeError: "Expected to be + * returned from the "" function but got ." */ +static void scr_stream_throw_invalid_return(const char *expected, const char *name, + const ScrDyn *got) { + char detail[160]; + const char *type = scr_dyn_specific_type(got, detail, sizeof detail); + char msg[384]; + int n = snprintf(msg, sizeof msg, "Expected %s to be returned from the \"%s\" function but got %s.", + expected, name, type); + if (n < 0) n = 0; + if ((size_t)n >= sizeof msg) n = (int)sizeof msg - 1; + scr_throw_error_msg_code(SCR_ERR_TYPE, msg, (size_t)n, "ERR_INVALID_RETURN_VALUE"); +} + +/* An untyped pipeline function stage's result before the last stage + * (pipelineImpl's `isIterable(ret, true)` check): an async iterable + * answers itself (+1); other values throw ERR_INVALID_RETURN_VALUE naming + * the stage, as `label` ("transform[i]") does. */ +ScrDyn *scr_stream_dyn_transform_result(const ScrDyn *value, const ScrStr *label) { + if (scr_stream_dyn_stage_returned_stream(value)) return NULL; + int kind = scr_dyn_iterable_kind(value); + if (kind < 0) return NULL; + if (kind != 2) { + scr_stream_throw_invalid_return("AsyncIterable", label->data, value); + return NULL; + } + return scr_dyn_retain((ScrDyn *)value); +} + +/* The value a checked-dynamic first stage pumps (pipelineImpl's i === 0 + * branch): for `returned`, a source function's result, which must be + * iterable; otherwise a { readable, writable } pair's readable side, or + * the value itself. +1, or NULL with an exception pending. */ +ScrDyn *scr_stream_dyn_source_value(const ScrDyn *value, bool returned) { + if (!returned) { + if (value->kind == SCR_DYN_HANDLE && value->v.handle.tag == SCR_DYNH_WEB_TRANSFORM) { + scr_stream_throw_web_stage(); + return NULL; + } + if (scr_stream_dyn_transform_like(value)) return scr_dyn_obj_read(value, "readable", 8); + if (scr_exc_pending()) return NULL; + return scr_dyn_retain((ScrDyn *)value); + } + if (scr_stream_dyn_stage_returned_stream(value)) return NULL; + int kind = scr_dyn_iterable_kind(value); + if (kind < 0) return NULL; + if (kind == 0) { + scr_stream_throw_invalid_return("Iterable, AsyncIterable or Stream", "source", value); + return NULL; + } + return scr_dyn_retain((ScrDyn *)value); +} diff --git a/packages/runtime/src/scr_stream.h b/packages/runtime/src/scr_stream.h index a4798c629..578712df2 100644 --- a/packages/runtime/src/scr_stream.h +++ b/packages/runtime/src/scr_stream.h @@ -145,6 +145,22 @@ ScrStream *scr_stream_pipe_general(ScrStream *src, ScrStream *dst, bool end); /* A boxed native stream out of a checked-dynamic pipeline stage (+1), or * NULL with `message` thrown as the not-supported Error. */ ScrStream *scr_stream_unbox_dyn(const struct ScrDyn *value, const ScrStr *message); +/* Whether pipeline() keeps a checked-dynamic first stage as an iterable + * to pump rather than as a stream (false with an exception pending when + * reading the value's members throws). */ +bool scr_stream_dyn_iterable_source(const struct ScrDyn *value); +/* A checked-dynamic pipeline() stage the pipeline takes as a stream: a + * native stream (+1), or Duplex.from of a value that is not stream-like + * or a function, which the pipeline uses without piping into it; NULL + * with an exception otherwise (`message` for the unsupported shapes). */ +ScrStream *scr_stream_unbox_dyn_stage(const struct ScrDyn *value, const ScrStr *message); +/* The iterable pipeline() pumps for a checked-dynamic first stage: a + * source function's result when `returned`, else a { readable, writable } + * pair's readable side or the value (+1, or NULL with an exception). */ +struct ScrDyn *scr_stream_dyn_source_value(const struct ScrDyn *value, bool returned); +/* An untyped function stage's result before the last pipeline stage: the + * async iterable itself (+1), or NULL with ERR_INVALID_RETURN_VALUE. */ +struct ScrDyn *scr_stream_dyn_transform_result(const struct ScrDyn *value, const ScrStr *label); /* Handles that are Transforms (ScrDynHandleOps.transform: Cipheriv and * Decipheriv). scr_stream_of_handle: the handle's Transform (+1), built on * first use, or NULL when the value is not such a handle. @@ -665,6 +681,12 @@ void scr_stream_release(ScrStream *s); void *scr_stream_retain_v(void *s); void scr_stream_release_v(void *s); void scr_stream_trace(void *obj, ScrTraceVisit visit, void *ctx); +/* The `callback` a write or writev option callback receives (+1): the + * same closure for every call, as Node passes one onwrite function. */ +ScrClosure *scr_stream_write_closure(ScrStream *s, void *fn); +/* The `callback` any other option callback receives (+1), reused while the + * stream holds its only reference. */ +ScrClosure *scr_stream_done_closure(ScrStream *s, void *fn); void scr_stream_install(void); /* The loop's stream hook (scr_async.c): `pending` keeps the loop alive diff --git a/packages/runtime/src/scr_string.c b/packages/runtime/src/scr_string.c index f94554642..71d629941 100644 --- a/packages/runtime/src/scr_string.c +++ b/packages/runtime/src/scr_string.c @@ -83,6 +83,12 @@ typedef struct { size_t cu; /* UTF-16 code-unit offset, always a code-point boundary */ size_t cb; /* matching UTF-8 byte offset, never a continuation byte */ } ScrSidxPoint; +/* One allocation: the 32-bit base of every 64th unit follows the deltas. */ +typedef struct { + size_t n; /* the UTF-16 length the offsets describe */ + uint32_t *base; /* byte offset of every 64th unit */ + uint8_t delta[]; /* each unit's byte offset past its block's base */ +} ScrSidxOffsets; typedef struct ScrSidx { const ScrStr *s; /* NULL = empty slot */ size_t u16len; /* SCR_U16_UNKNOWN until the whole current string */ @@ -94,10 +100,12 @@ typedef struct ScrSidx { size_t indexed_cb; bool no_more_points; /* metadata allocation failed/overflowed: fail open */ bool points_complete; /* points cover every stride of indexed prefix */ + uint32_t walks; /* index walks before the offsets exist */ uint16_t *units; /* the UTF-16 code units, once reads warrant them */ - size_t nunits; - size_t reads; /* indexed reads that missed the unit array */ -} ScrSidx; + uint32_t nunits; /* units and offsets need at most 1 << 24 bytes */ + uint32_t reads; /* indexed reads that missed the unit array */ + ScrSidxOffsets *offsets; /* every unit's byte offset, once walks warrant */ +} ScrSidx; /* 128 bytes on 64-bit targets: every lookup clears or copies one */ /* Keep short-string cursor traffic out of the sparse cache. A short-lived * one-byte receiver can be far more common than a large indexed one; sharing * the round-robin slots would otherwise rebuild a warm index every few calls. @@ -136,6 +144,7 @@ static void scr_sidx_clear(ScrSidx *e, unsigned *live) { scr_str_scan.n = 0; } free(e->units); + if (e->offsets) free(e->offsets); /* rare: skip the call on the hot clear */ memset(e, 0, sizeof(*e)); } @@ -337,6 +346,9 @@ static void scr_sidx_concat_append(const ScrStr *s, size_t oldlen) { e->units = NULL; e->nunits = 0; e->reads = 0; + free(e->offsets); /* and so do the offsets */ + e->offsets = NULL; + e->walks = 0; } } } @@ -1576,6 +1588,8 @@ static void scr_sidx_ascii_run(const ScrStr *s, ScrSidx *e) { /* How far past its end a forward access may grow the cached ASCII run, and * how far beyond the access the growth looks ahead. */ #define SCR_SIDX_ASCII_REACH ((size_t)4096) +/* How far from the hot cursor an index walks from it. */ +#define SCR_SIDX_NEAR_UNITS ((size_t)64) #define SCR_SIDX_ASCII_AHEAD ((size_t)256) /* Grow the cached ASCII run forward until it covers byte `want` (plus a @@ -1624,8 +1638,118 @@ static inline size_t scr_u16_to_byte_c(const ScrStr *s, ScrSidx *e, size_t u16, return scr_u16_to_byte_walk(s, e, u16, mid); } +/* Index walks on one long non-ASCII receiver (slices, substrings and + * indexed reads taken across it) each step through bytes from the nearest + * known position. Once those walks would have paid for one pass over the + * receiver (a walk per 32 bytes, at least 64 walks), a receiver from + * SCR_SIDX_MIN_BYTES to SCR_SIDX_OFFSETS_MAX bytes keeps every unit's byte + * offset: a 32-bit base per 64 units and one byte per unit past it (64 units + * span at most 192 bytes). The low half of an astral character has its high + * half's offset. The offsets are freed with the entry, and an append drops + * them (scr_sidx_concat_append). */ +#define SCR_SIDX_OFFSET_BLOCK ((size_t)64) +#define SCR_SIDX_OFFSETS_MAX ((size_t)1 << 24) +static bool scr_sidx_offsets(const ScrStr *s, ScrSidx *e) { + if (e->offsets) return true; + if (s->len < SCR_SIDX_MIN_BYTES || s->len > SCR_SIDX_OFFSETS_MAX) return false; + if (++e->walks < 64 || (size_t)e->walks * 32 < s->len) return false; + size_t n = scr_sidx_len(s, e); + if (s->ascii || e->u16len == s->len) return false; + size_t ndelta = (n + 1 + 3) & ~(size_t)3; + ScrSidxOffsets *o = + malloc(sizeof *o + ndelta + (n / SCR_SIDX_OFFSET_BLOCK + 1) * sizeof(uint32_t)); + if (!o) { + e->walks = 0; + return false; + } + o->n = n; + o->base = (uint32_t *)(o->delta + ndelta); + uint32_t *base = o->base; + uint8_t *delta = o->delta; + size_t cu = 0, cb = 0; + while (cb < s->len && cu < n) { + size_t seq = scr_utf8_seq_len((unsigned char)s->data[cb]); + for (size_t w = seq == 4 ? 2 : 1; w > 0 && cu < n; w--, cu++) { + if (cu % SCR_SIDX_OFFSET_BLOCK == 0) base[cu / SCR_SIDX_OFFSET_BLOCK] = (uint32_t)cb; + delta[cu] = (uint8_t)(cb - base[cu / SCR_SIDX_OFFSET_BLOCK]); + } + cb += seq; + } + e->offsets = o; + return true; +} + +static inline size_t scr_sidx_offset_at(const ScrSidx *e, size_t u16) { + return e->offsets->base[u16 / SCR_SIDX_OFFSET_BLOCK] + e->offsets->delta[u16]; +} + +/* Walk from a known code-point boundary (cu, cb) to UTF-16 index u16 and + * leave the hot cursor there. Same contract as scr_u16_to_byte_walk. */ +static size_t scr_sidx_walk_from(const ScrStr *s, ScrSidx *e, size_t cu, size_t cb, size_t u16, + bool *mid, bool refresh_run) { + while (cu > u16) { + /* Eight bytes starting inside a sequence can reach back at most three + * more bytes to its lead. Such a span contains at most nine UTF-16 + * units (seven ASCII bytes plus an astral pair), so it cannot overshoot + * a target at least nine units away. */ + if (cu - u16 >= 9 && cb >= 8) { + size_t start = cb - 8; + while (start > 0 && ((unsigned char)s->data[start] & 0xC0) == 0x80) + start--; + cu -= scr_utf16_units_span(s->data + start, cb - start, NULL); + cb = start; + } else { + scr_sidx_back(s, &cu, &cb); + } + } + bool m = false; + while (cu < u16 && cb < s->len) { + if (u16 - cu >= 9 && s->len - cb >= 8) { + size_t end = cb + 8; + while (end < s->len && ((unsigned char)s->data[end] & 0xC0) == 0x80) + end++; + cu += scr_utf16_units_span(s->data + cb, end - cb, NULL); + cb = end; + continue; + } + SCR_SIDX_STEP(); + size_t seq = scr_utf8_seq_len((unsigned char)s->data[cb]); + size_t w = seq == 4 ? 2 : 1; + if (cu + w > u16) { /* u16 lands between the halves of an astral char */ + m = true; + break; + } + cu += w; + cb += seq; + } + e->cu = cu; + e->cb = cb; + if (refresh_run && cb < s->len && (unsigned char)s->data[cb] < 0x80) + scr_sidx_ascii_run(s, e); + *mid = m; + return cb; +} + static __attribute__((noinline)) size_t scr_u16_to_byte_walk(const ScrStr *s, ScrSidx *e, size_t u16, bool *mid) { + if (scr_sidx_offsets(s, e)) { + *mid = false; + if (u16 >= e->offsets->n) return s->len; + size_t cb = scr_sidx_offset_at(e, u16); + size_t cu = u16; + if (u16 > 0 && scr_sidx_offset_at(e, u16 - 1) == cb) { + *mid = true; /* the low half of an astral character */ + cu = u16 - 1; + } + e->cu = cu; + e->cb = cb; + return cb; + } + /* Within a short distance of the hot cursor (a scanner + * taking slices near its position), walk from that cursor: no index + * search, extension or ASCII-run growth is cheaper. */ + if (e->cb <= s->len && scr_sidx_abs_diff(e->cu, u16) <= SCR_SIDX_NEAR_UNITS) + return scr_sidx_walk_from(s, e, e->cu, e->cb, u16, mid, false); /* A short step forward from the hot cursor (a sequential scan of * non-ASCII text) walks from the cursor itself. */ if (u16 >= e->cu && u16 - e->cu <= 8) { @@ -1665,48 +1789,7 @@ static __attribute__((noinline)) size_t scr_u16_to_byte_walk(const ScrStr *s, Sc } scr_sidx_materialize_identity_prefix(s, e); ScrSidxPoint near = scr_sidx_near_u16(s, e, u16); - size_t cu = near.cu, cb = near.cb; - while (cu > u16) { - /* Eight bytes starting inside a sequence can reach back at most three - * more bytes to its lead. Such a span contains at most nine UTF-16 - * units (seven ASCII bytes plus an astral pair), so it cannot overshoot - * a target at least nine units away. */ - if (cu - u16 >= 9 && cb >= 8) { - size_t start = cb - 8; - while (start > 0 && ((unsigned char)s->data[start] & 0xC0) == 0x80) - start--; - cu -= scr_utf16_units_span(s->data + start, cb - start, NULL); - cb = start; - } else { - scr_sidx_back(s, &cu, &cb); - } - } - bool m = false; - while (cu < u16 && cb < s->len) { - if (u16 - cu >= 9 && s->len - cb >= 8) { - size_t end = cb + 8; - while (end < s->len && ((unsigned char)s->data[end] & 0xC0) == 0x80) - end++; - cu += scr_utf16_units_span(s->data + cb, end - cb, NULL); - cb = end; - continue; - } - SCR_SIDX_STEP(); - size_t seq = scr_utf8_seq_len((unsigned char)s->data[cb]); - size_t w = seq == 4 ? 2 : 1; - if (cu + w > u16) { /* u16 lands between the halves of an astral char */ - m = true; - break; - } - cu += w; - cb += seq; - } - e->cu = cu; - e->cb = cb; - if (cb < s->len && (unsigned char)s->data[cb] < 0x80) - scr_sidx_ascii_run(s, e); - *mid = m; - return cb; + return scr_sidx_walk_from(s, e, near.cu, near.cb, u16, mid, true); } static size_t scr_byte_to_u16_walk(const ScrStr *s, ScrSidx *e, size_t byte_off); @@ -1875,10 +1958,17 @@ static uint16_t *scr_str_units(const ScrStr *s, size_t *n) { * megabytes, which scanners read in full. The array is freed with the * entry, and an append drops it (scr_sidx_concat_append). */ #define SCR_SIDX_UNITS_MAX ((size_t)1 << 24) +static void scr_sidx_take_units(const ScrStr *s, ScrSidx *e) { + size_t n = 0; + e->units = scr_str_units(s, &n); + e->nunits = (uint32_t)n; +} + static bool scr_sidx_units(const ScrStr *s, ScrSidx *e) { if (!e->units) { - if (++e->reads < 32 || e->reads * 16 < s->len || s->len > SCR_SIDX_UNITS_MAX) return false; - e->units = scr_str_units(s, &e->nunits); + if (s->len > SCR_SIDX_UNITS_MAX || ++e->reads < 32 || (size_t)e->reads * 16 < s->len) + return false; + scr_sidx_take_units(s, e); } /* Publish the scan's receiver for emitted reads (scr_runtime.h). */ if (scr_str_scan.s != s) { @@ -1889,6 +1979,31 @@ static bool scr_sidx_units(const ScrStr *s, ScrSidx *e) { return true; } +/* A loop about to read s (bytes-loop-versions.ts) between the indexes + * `from` and `to` (NaN: no bound) converts it at once instead of after the + * threshold's reads, and publishes it as the scan's receiver, when that + * range has at least one unit per 16 bytes of s (the threshold's ratio). + * A shorter loop, such as one over a line of a source text, counts as one + * read, so a short loop does not keep all of a long string's units. ASCII + * receivers and receivers past SCR_SIDX_UNITS_MAX publish nothing; the + * caller compares scr_str_scan.s with s. */ +void scr_str_scan_units_for(ScrStr *s, double from, double to) { + if (s->ascii || s->len > SCR_SIDX_UNITS_MAX) return; + ScrSidx *e = scr_sidx(s); + if (!e->units) { + if (fabs(to - from) * 16 < (double)s->len) { + (void)scr_sidx_units(s, e); + return; + } + scr_sidx_take_units(s, e); + } + if (scr_str_scan.s != s) { + scr_str_scan.s = s; + scr_str_scan.units = e->units; + scr_str_scan.n = e->nunits; + } +} + typedef enum { SCR_UNIT_FIRST, SCR_UNIT_LAST, SCR_UNIT_AT } ScrUnitSearch; /* The UTF-16 index of needle in s: the first match at or after `from`, @@ -1928,6 +2043,22 @@ static __attribute__((noinline)) double scr_str_unit_search(const ScrStr *s, con /* Search backwards from a clamped UTF-16 position. A low-surrogate position * maps to its scalar's first byte, so that scalar remains searchable. */ double scr_str_last_index_of_from(ScrStr *s, ScrStr *needle, double position) { + /* A proven-ASCII receiver: byte offsets are UTF-16 indexes, and a needle + * with other bytes (including lone surrogate halves) is never found. */ + if (s->ascii) { + size_t start = isnan(position) || position >= (double)s->len ? s->len + : position <= 0 ? 0 : (size_t)trunc(position); + if (needle->len == 0) return (double)start; + if (needle->len > s->len) return -1.0; + size_t max_start = s->len - needle->len; + if (start < max_start) max_start = start; + char first = needle->data[0]; + for (size_t i = max_start + 1; i-- > 0;) { + if (s->data[i] == first && memcmp(s->data + i, needle->data, needle->len) == 0) + return (double)i; + } + return -1.0; + } ScrSidx *e = scr_sidx_for(s); size_t len16 = scr_sidx_len(s, e); size_t start16 = isnan(position) || position >= (double)len16 ? len16 @@ -1939,8 +2070,9 @@ double scr_str_last_index_of_from(ScrStr *s, ScrStr *needle, double position) { size_t start_byte = scr_u16_to_byte_c(s, e, start16, &mid); size_t max_start = s->len - needle->len; if (start_byte < max_start) max_start = start_byte; + char first = needle->data[0]; for (size_t i = max_start + 1; i-- > 0;) { - if (memcmp(s->data + i, needle->data, needle->len) == 0) { + if (s->data[i] == first && memcmp(s->data + i, needle->data, needle->len) == 0) { return (double)scr_byte_to_u16_c(s, e, i); } } @@ -1975,6 +2107,19 @@ double scr_str_utf16_len(ScrStr *s) { return (double)(int64_t)units; } +/* Records the UTF-16 length of a string whose producer counted it (latin1 + * text has one unit per byte), so scr_str_utf16_len does not walk it. Only + * strings that scr_str_utf16_len would otherwise record are kept. */ +void scr_str_note_utf16_len(ScrStr *s, size_t units) { + if (s->ascii || s->len <= 64 || s->rc == SIZE_MAX) return; + if (!scr_u16len_tab) { + scr_u16len_tab = calloc(SCR_U16LEN_N, sizeof *scr_u16len_tab); + if (!scr_u16len_tab) return; + scr_sidx_register_cleanup(); + } + *scr_u16len_slot(s) = (ScrU16Len){s, units}; +} + double scr_str_char_code_at(ScrStr *s, double i) { /* A sequential scan of a non-ASCII receiver stays inside the bounded * ASCII span cached at its last resolved position. Its unit offset is an @@ -2194,6 +2339,17 @@ static void scr_str_slice_bounds(ScrStr *s, ScrSidx *e, double start, double end void scr_str_slice_range(ScrStr *s, double start, double end, bool substring, ScrStringSlice *out) { + if (__builtin_expect(s->ascii, 1)) { + /* Units are bytes: no index entry, and no boundary splits a pair. */ + size_t length = s->len; + size_t from = scr_slice_unit(start, length, substring); + size_t to = scr_slice_unit(end, length, substring); + if (substring && from > to) { size_t swap = from; from = to; to = swap; } + out->start = from; + out->length = to > from ? to - from : 0; + out->split = 0; + return; + } ScrSidx *e = scr_sidx_for(s); size_t from, to; scr_str_slice_bounds(s, e, start, end, substring, &from, &to); @@ -3449,6 +3605,8 @@ ScrStr *scr_bool_to_scrstr(bool b) { * decode is a whole char; an i addressing the LOW half of an astral pair * (unreachable from the iterator, which only lands on char starts) answers * the containing char. Out of range → empty string. Borrows s; +1. */ +static ScrStr *scr_str_char_of(const char *bytes, size_t adv); + ScrStr *scr_str_cp_at(ScrStr *s, double i) { double idx = scr_to_integer_or_infinity(i); if (!(idx >= 0)) return scr_str_empty(); @@ -3463,7 +3621,80 @@ ScrStr *scr_str_cp_at(ScrStr *s, double i) { if (off >= s->len) return scr_str_empty(); /* out of range */ size_t adv; (void)scr_utf8_decode(s->data + off, &adv); - return scr_str_from_span(s->data + off, adv); + return scr_str_char_of(s->data + off, adv); +} + +/* The one-character strings of U+0080..U+07FF (two-byte sequences, lead + * bytes 0xC2..0xDF), made immortal on first use and shared by every + * thread, as the ASCII ones are static: the string iterator returns them + * instead of a new string per character. A string is immortal before its + * slot publishes it; a thread that loses the race to fill a slot keeps its + * own immortal copy, so at most one extra string per slot and thread. The + * strings come from the system allocator, as other state that outlives a + * thread does: a worker's own memory goes back when it exits without + * sharing (scr_alloc.c), and the table outlives the worker that filled a + * slot. */ +static ScrStr *scr_two_byte_chars[0x800 - 0x80]; + +static ScrStr *scr_two_byte_char(const char *bytes) { + uint32_t cp = ((uint32_t)((unsigned char)bytes[0] & 0x1F) << 6) | ((unsigned char)bytes[1] & 0x3F); + ScrStr **slot = &scr_two_byte_chars[cp - 0x80]; + ScrStr *known = __atomic_load_n(slot, __ATOMIC_ACQUIRE); + if (known) return known; + ScrStr *made = calloc(1, sizeof(ScrStr) + 3); + if (!made) scr_oom(); + made->len = 2; + made->cap = 2; + memcpy(made->data, bytes, 2); + scr_str_hash_prime(made); + made->rc = SIZE_MAX; + scr_lsan_immortal(made); + ScrStr *expected = NULL; + if (__atomic_compare_exchange_n(slot, &expected, made, false, __ATOMIC_ACQ_REL, + __ATOMIC_ACQUIRE)) + return made; + return expected; +} + +/* The character whose encoding starts at bytes and is adv bytes long (+1, + * or immortal). */ +static ScrStr *scr_str_char_of(const char *bytes, size_t adv) { + unsigned char lead = (unsigned char)bytes[0]; + if (adv == 1 && lead < 0x80) return (ScrStr *)&scr_ascii1[lead]; + if (adv == 2) return scr_two_byte_char(bytes); + return scr_str_from_span(bytes, adv); +} + +ScrStr *scr_str_cp_at_byte(ScrStr *s, uint64_t b) { + if (b >= s->len) return scr_str_empty(); + size_t adv; + (void)scr_utf8_decode(s->data + b, &adv); + return scr_str_char_of(s->data + b, adv); +} + +ScrArr *scr_str_code_points(ScrStr *s) { + /* Every byte but a continuation byte starts one character; a lone + * surrogate's three bytes are one character. */ + size_t count = s->len; + if (!s->ascii) + for (size_t b = 0; b < s->len; b++) count -= ((unsigned char)s->data[b] & 0xC0) == 0x80; + ScrArr *out = scr_arr_new_inline(SCR_ELEM_STR, count, false); + size_t k = 0; + for (size_t b = 0; b < s->len;) { + unsigned char c = (unsigned char)s->data[b]; + if (c < 0x80) { + out->data[k++] = (uint64_t)(uintptr_t)&scr_ascii1[c]; + b++; + continue; + } + size_t adv; + (void)scr_utf8_decode(s->data + b, &adv); + out->data[k++] = (uint64_t)(uintptr_t)scr_str_char_of(s->data + b, adv); + b += adv; + } + out->len = (uint32_t)count; + out->filled = (uint32_t)count; + return out; } /* ── the URI component codecs (encodeURIComponent/decodeURIComponent) ── diff --git a/packages/runtime/src/scr_string.h b/packages/runtime/src/scr_string.h index fc2f15562..e269498b1 100644 --- a/packages/runtime/src/scr_string.h +++ b/packages/runtime/src/scr_string.h @@ -83,6 +83,9 @@ static inline void scr_str_hash_forget(ScrStr *s) { /* True only when every byte is proven below 0x80 (see above). */ static inline bool scr_str_known_ascii(const ScrStr *s) { return s->ascii; } +/* Record the UTF-16 length a string's producer counted (scr_string.c). */ +void scr_str_note_utf16_len(ScrStr *s, size_t units); + /* Record a proven-ASCII fact. Immortal strings may live in read-only * memory and are shared across threads, so they are never written. */ static inline void scr_str_note_ascii(ScrStr *s) { diff --git a/packages/runtime/src/scr_string_methods.h b/packages/runtime/src/scr_string_methods.h index 1f25b8766..2e69ee6ac 100644 --- a/packages/runtime/src/scr_string_methods.h +++ b/packages/runtime/src/scr_string_methods.h @@ -42,6 +42,13 @@ extern SCR_TL ScrStrScan scr_str_scan; * [0, length) — note charCodeAt(1.5) is index 1, matching Node. */ double scr_str_char_code_at(ScrStr *s, double i); +/* Converts a non-ASCII s up to the units cap to UTF-16 at once and + * publishes it in scr_str_scan, for a loop about to read it between the + * indexes `from` and `to` (NaN: no bound), when that range has at least + * one unit per 16 bytes. A shorter loop counts as one indexed read. Leaves + * scr_str_scan unchanged for any other s. */ +void scr_str_scan_units_for(ScrStr *s, double from, double to); + /* indexOf(needle, fromIndex): UTF-16 index of the first occurrence at or * after fromIndex (clamped to [0, length]), or -1. Empty needle returns the * clamped fromIndex per spec. */ @@ -143,6 +150,15 @@ ScrStr *scr_str_char_at(ScrStr *s, double i); * (two units); the consuming loop advances by the result's length. * Out of range → empty string. Returns +1. */ ScrStr *scr_str_cp_at(ScrStr *s, double i); +/* The string iterator's step over the WTF-8 bytes: the code-point + * character whose encoding starts at byte offset b, a character start + * below s->len (a lone surrogate is its own three-byte character). The + * compiler answers ASCII bytes inline and calls this for the rest. + * Returns +1. */ +ScrStr *scr_str_cp_at_byte(ScrStr *s, uint64_t b); +/* [...s] and Array.from(s): the code-point characters of s in order, as a + * fresh string array of exactly that length. Returns +1. */ +struct ScrArr *scr_str_code_points(ScrStr *s); /* trimStart()/trimEnd(): the one-sided halves of trim. Returns +1. */ ScrStr *scr_str_trim_start(ScrStr *s); diff --git a/packages/runtime/src/scr_symbol.c b/packages/runtime/src/scr_symbol.c index 647c31dc3..7e573fc28 100644 --- a/packages/runtime/src/scr_symbol.c +++ b/packages/runtime/src/scr_symbol.c @@ -22,6 +22,10 @@ struct ScrSym { ScrStr *desc; /* owned; NULL = no description (Symbol()) */ ScrStr *reg_key; /* owned; non-NULL exactly for Symbol.for symbols */ struct ScrSym *reg_next; /* the registry's intrusive chain */ + /* The symbol's checked-dynamic box while one is alive (not retained: the + * box retains the symbol and clears this when it is freed), shared by + * every conversion of the symbol to a checked value. */ + ScrDyn *box; }; ScrSym *scr_sym_retain(ScrSym *s) { @@ -50,6 +54,7 @@ ScrSym *scr_sym_new(ScrStr *desc) { s->desc = desc ? scr_str_retain(desc) : NULL; s->reg_key = NULL; s->reg_next = NULL; + s->box = NULL; return s; } @@ -80,6 +85,11 @@ ScrSym *scr_sym_well_known(ScrStr *name) { scr_str_release(description); symbol->reg_next = g_sym_well_known; g_sym_well_known = scr_sym_retain(symbol); + { + ScrDyn *key = scr_dyn_new_symbol(symbol); + scr_dyn_install_well_known_symbol(key); + scr_dyn_release(key); + } if (name->len == 8 && !memcmp(name->data, "iterator", 8)) { ScrDyn *key = scr_dyn_new_symbol(symbol); scr_dyn_install_iterator_symbol(key); @@ -90,6 +100,16 @@ ScrSym *scr_sym_well_known(ScrStr *name) { scr_dyn_install_has_instance_symbol(key); scr_dyn_release(key); } + if (name->len == 7 && !memcmp(name->data, "species", 7)) { + ScrDyn *key = scr_dyn_new_symbol(symbol); + scr_dyn_install_species_symbol(key); + scr_dyn_release(key); + } + if (name->len == 11 && !memcmp(name->data, "toStringTag", 11)) { + ScrDyn *key = scr_dyn_new_symbol(symbol); + scr_dyn_install_to_string_tag_symbol(key); + scr_dyn_release(key); + } if (name->len == 13 && !memcmp(name->data, "asyncIterator", 13)) { ScrDyn *key = scr_dyn_new_symbol(symbol); scr_dyn_install_async_iterator_symbol(key); @@ -130,6 +150,11 @@ ScrSym *scr_sym_for(ScrStr *key) { s->reg_key = scr_str_retain(key); s->reg_next = g_sym_registry; g_sym_registry = scr_sym_retain(s); + if (key->len == 26 && !memcmp(key->data, "nodejs.util.inspect.custom", 26)) { + ScrDyn *custom = scr_dyn_new_symbol(s); + scr_dyn_install_inspect_custom_symbol(custom); + scr_dyn_release(custom); + } return s; } @@ -137,8 +162,17 @@ ScrStr *scr_sym_desc(ScrSym *s) { return s->desc ? scr_str_retain(s->desc) : NUL ScrStr *scr_sym_key_for(ScrSym *s) { return s->reg_key ? scr_str_retain(s->reg_key) : NULL; } +/* The release of a symbol's shared box: the box is going away. */ +static void scr_sym_release_box(ScrSym *s) { + s->box = NULL; + scr_sym_release(s); +} + ScrDyn *scr_dyn_new_symbol(ScrSym *value) { - return scr_dyn_symbol_ref(scr_sym_retain(value), scr_sym_release, scr_sym_to_string, scr_sym_desc); + if (value->box) return scr_dyn_retain(value->box); + ScrDyn *box = scr_dyn_symbol_ref(scr_sym_retain(value), scr_sym_release_box, scr_sym_to_string, scr_sym_desc); + value->box = box; + return box; } ScrStr *scr_sym_to_string(ScrSym *s) { diff --git a/packages/runtime/src/scr_text_codec_dyn.c b/packages/runtime/src/scr_text_codec_dyn.c new file mode 100644 index 000000000..37fdfb27b --- /dev/null +++ b/packages/runtime/src/scr_text_codec_dyn.c @@ -0,0 +1,337 @@ +/* WHATWG TextEncoder and TextDecoder instances, and node:string_decoder + * StringDecoder instances, as checked-dynamic values (SCR_DYNH_TEXT_ENCODER, + * SCR_DYNH_TEXT_DECODER, SCR_DYNH_STRING_DECODER). A codec is a compiled + * record (type-mapper.ts): the handle holds that record by reference, so + * a codec stored in an untyped slot and read back keeps its identity and + * a decoder keeps its streaming state. The compiler passes the record's + * reference-count entry points and, for a TextDecoder, the byte offsets of the + * encoding, fatal, ignoreBOM and state fields when it boxes one. A unit + * of its own so programs that never box a codec link none of it + * (native-toolchain gating: the textCodecDyn runtime feature). */ +#include "scr_platform.h" +#include "scr_runtime.h" + +#include + +typedef struct { + void *(*retain)(void *); + void (*release)(void *); + size_t encoding, fatal, ignore_bom, state; +} ScrTextCodecLayout; + +/* A program has one record shape per codec class, so one layout each. */ +static SCR_TL ScrTextCodecLayout scr_tcd_encoder_layout, scr_tcd_decoder_layout; + +static void *scr_tcd_encoder_retain(void *rec) { return scr_tcd_encoder_layout.retain(rec); } +static void scr_tcd_encoder_release(void *rec) { scr_tcd_encoder_layout.release(rec); } +static void *scr_tcd_decoder_retain(void *rec) { return scr_tcd_decoder_layout.retain(rec); } +static void scr_tcd_decoder_release(void *rec) { scr_tcd_decoder_layout.release(rec); } + +static void scr_tcd_codec_not_a_function(const char *what) { + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_puts(&b, what); + scr_jb_puts(&b, " is not a function"); + scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&b)); +} + +/* Boxes an owned string (+1 box; the string's reference moves in). */ +static ScrDyn *scr_tcd_str(ScrStr *text) { + ScrDyn *boxed = scr_dyn_new_str(text); + scr_str_release(text); + return boxed; +} + +static ScrDyn *scr_tcd_codec_arg(ScrDyn *const *args, size_t argc, size_t index) { + return index < argc ? args[index] : scr_dyn_undefined(); +} + +static double scr_tcd_decoder_encoding(const void *rec) { + double encoding; + memcpy(&encoding, (const char *)rec + scr_tcd_decoder_layout.encoding, sizeof encoding); + return encoding; +} + +static bool scr_tcd_decoder_flag(const void *rec, size_t offset) { + return ((const unsigned char *)rec)[offset] != 0; +} + +static ScrDyn *scr_tcd_encoder_invoke(void *rec, ScrDyn *self, const char *method, + ScrDyn *const *args, size_t argc, const char *what) { + (void)rec; + (void)self; + if (strcmp(method, "encode") == 0) { + /* encode(input = ""): any other input converts with ToString. */ + ScrDyn *input = scr_tcd_codec_arg(args, argc, 0); + ScrStr *text = + input->kind == SCR_DYN_UNDEF ? scr_str_new("", 0) : scr_dyn_string_coerce_js(input); + if (!text) return NULL; + ScrStr *utf8 = scr_str_new("utf8", 4); + ScrBytes *bytes = scr_bytes_from_str(text, utf8); + scr_str_release(utf8); + scr_str_release(text); + /* A fresh Uint8Array, not a Buffer. */ + bytes->is_buffer = false; + ScrDyn *result = scr_dyn_new_bytes(bytes); + scr_bytes_release(bytes); + return result; + } + if (strcmp(method, "encodeInto") == 0) + return scr_text_encode_into(scr_tcd_codec_arg(args, argc, 0), + scr_tcd_codec_arg(args, argc, 1)); + scr_tcd_codec_not_a_function(what); + return NULL; +} + +static ScrDyn *scr_tcd_encoder_get(void *rec, const char *key, size_t len) { + (void)rec; + if (len == 8 && memcmp(key, "encoding", 8) == 0) return scr_tcd_str(scr_str_new("utf-8", 5)); + return NULL; +} + +/* decode(input, options): the options object's `stream` member, read + * with JS truthiness; undefined and null mean no options. */ +static bool scr_tcd_decoder_stream_option(const ScrDyn *options, bool *stream) { + *stream = false; + if (options->kind == SCR_DYN_UNDEF || options->kind == SCR_DYN_NULL) return true; + if (options->kind != SCR_DYN_OBJ && options->kind != SCR_DYN_ARR && + options->kind != SCR_DYN_FUNC && options->kind != SCR_DYN_TYPED_REF && + options->kind != SCR_DYN_HANDLE) { + scr_dyn_arg_type_fail("options", "of type object", options); + return false; + } + ScrDyn *value = scr_dyn_obj_read(options, "stream", 6); + if (!value) return false; + *stream = scr_dyn_truthy(value); + scr_dyn_release(value); + return !scr_exc_pending(); +} + +static ScrDyn *scr_tcd_decoder_invoke(void *rec, ScrDyn *self, const char *method, + ScrDyn *const *args, size_t argc, const char *what) { + (void)self; + if (strcmp(method, "decode") != 0) { + scr_tcd_codec_not_a_function(what); + return NULL; + } + ScrBytes *input = scr_bytes_buffer_source(scr_tcd_codec_arg(args, argc, 0)); + if (!input) return NULL; + bool stream; + if (!scr_tcd_decoder_stream_option(scr_tcd_codec_arg(args, argc, 1), &stream)) { + scr_bytes_release(input); + return NULL; + } + const ScrTextCodecLayout *layout = &scr_tcd_decoder_layout; + ScrDyn *state; + memcpy(&state, (const char *)rec + layout->state, sizeof state); + ScrStr *text = scr_text_decode_stream(state, input, scr_tcd_decoder_encoding(rec), + scr_tcd_decoder_flag(rec, layout->fatal), + scr_tcd_decoder_flag(rec, layout->ignore_bom), stream); + scr_bytes_release(input); + return text ? scr_tcd_str(text) : NULL; +} + +static ScrDyn *scr_tcd_decoder_get(void *rec, const char *key, size_t len) { + if (len == 8 && memcmp(key, "encoding", 8) == 0) + return scr_tcd_str(scr_text_decoder_name(scr_tcd_decoder_encoding(rec))); + if (len == 5 && memcmp(key, "fatal", 5) == 0) + return scr_dyn_new_bool(scr_tcd_decoder_flag(rec, scr_tcd_decoder_layout.fatal)); + if (len == 9 && memcmp(key, "ignoreBOM", 9) == 0) + return scr_dyn_new_bool(scr_tcd_decoder_flag(rec, scr_tcd_decoder_layout.ignore_bom)); + return NULL; +} + +static bool scr_tcd_codec_set(void *rec, const char *key, size_t len, const ScrDyn *value) { + (void)rec; + (void)key; + (void)len; + (void)value; + return false; +} + +static const ScrDynHandleMethod scr_tcd_encoder_methods[] = { + {.name = "encode", .length = 0}, + {.name = "encodeInto", .length = 2}, + {.name = NULL}, +}; + +static const ScrDynHandleMethod scr_tcd_decoder_methods[] = { + {.name = "decode", .length = 0}, + {.name = NULL}, +}; + +static const ScrDynHandleOps scr_tcd_encoder_ops = { + .cls = "TextEncoder", + .to_string_tag = "TextEncoder", + .retain = scr_tcd_encoder_retain, + .release = scr_tcd_encoder_release, + .invoke = scr_tcd_encoder_invoke, + .get = scr_tcd_encoder_get, + .set = scr_tcd_codec_set, + .json_empty_object = true, + .methods = scr_tcd_encoder_methods, + .invalid_this = "Value of \"this\" must be of type TextEncoder", +}; + +static const ScrDynHandleOps scr_tcd_decoder_ops = { + .cls = "TextDecoder", + .to_string_tag = "TextDecoder", + .retain = scr_tcd_decoder_retain, + .release = scr_tcd_decoder_release, + .invoke = scr_tcd_decoder_invoke, + .get = scr_tcd_decoder_get, + .set = scr_tcd_codec_set, + .json_empty_object = true, + .methods = scr_tcd_decoder_methods, + .invalid_this = "Value of \"this\" must be of type TextDecoder", +}; + +ScrDyn *scr_text_encoder_to_dyn(void *rec, void *(*retain)(void *), void (*release)(void *)) { + if (!scr_tcd_encoder_layout.retain) { + scr_tcd_encoder_layout.retain = retain; + scr_tcd_encoder_layout.release = release; + scr_dyn_handle_install(SCR_DYNH_TEXT_ENCODER, &scr_tcd_encoder_ops); + } + return scr_dyn_new_handle(rec, SCR_DYNH_TEXT_ENCODER); +} + +ScrDyn *scr_text_decoder_to_dyn(void *rec, void *(*retain)(void *), void (*release)(void *), + size_t encoding, size_t fatal, size_t ignore_bom, size_t state) { + if (!scr_tcd_decoder_layout.retain) { + scr_tcd_decoder_layout = (ScrTextCodecLayout){retain, release, encoding, fatal, ignore_bom, state}; + scr_dyn_handle_install(SCR_DYNH_TEXT_DECODER, &scr_tcd_decoder_ops); + } + return scr_dyn_new_handle(rec, SCR_DYNH_TEXT_DECODER); +} + +void *scr_text_codec_from_dyn(const ScrDyn *d, bool decoder, const ScrDynPath *path, + const char *want) { + return scr_dyn_handle_unbox(d, decoder ? SCR_DYNH_TEXT_DECODER : SCR_DYNH_TEXT_ENCODER, path, + want); +} + +bool scr_text_codec_is(const ScrDyn *d, bool decoder) { + return d->kind == SCR_DYN_HANDLE && + d->v.handle.tag == (decoder ? SCR_DYNH_TEXT_DECODER : SCR_DYNH_TEXT_ENCODER); +} + +/* node:string_decoder StringDecoder instances (SCR_DYNH_STRING_DECODER). + * The decoder is a compiled record of its canonical encoding (a string) + * and its pending partial sequence (an f64 packing, scr_bytes.c's + * scr_strdec_* helpers); the compiler passes both fields' byte offsets + * when it boxes one. */ +typedef struct { + void *(*retain)(void *); + void (*release)(void *); + size_t encoding, pending; +} ScrStringDecoderLayout; + +static SCR_TL ScrStringDecoderLayout scr_sdd_layout; + +static void *scr_sdd_retain(void *rec) { return scr_sdd_layout.retain(rec); } +static void scr_sdd_release(void *rec) { scr_sdd_layout.release(rec); } + +static ScrStr *scr_sdd_encoding(const void *rec) { + ScrStr *encoding; + memcpy(&encoding, (const char *)rec + scr_sdd_layout.encoding, sizeof encoding); + return encoding; +} + +static double scr_sdd_pending(const void *rec) { + double pending; + memcpy(&pending, (const char *)rec + scr_sdd_layout.pending, sizeof pending); + return pending; +} + +static void scr_sdd_set_pending(void *rec, double pending) { + memcpy((char *)rec + scr_sdd_layout.pending, &pending, sizeof pending); +} + +/* write(buf): a string answers itself; an ArrayBuffer view decodes with + * the pending bytes before it; other values throw Node's + * ERR_INVALID_ARG_TYPE. +1 or NULL. */ +static ScrStr *scr_sdd_write(void *rec, const ScrDyn *buf) { + if (buf->kind == SCR_DYN_STR) return scr_str_retain(buf->v.str); + if (buf->kind != SCR_DYN_BYTES) { + scr_dyn_arg_type_fail("buf", "an instance of Buffer, TypedArray, or DataView", buf); + return NULL; + } + ScrBytes *bytes = scr_bytes_raw_view(buf->v.bytes); + ScrStr *encoding = scr_sdd_encoding(rec); + double pending = scr_sdd_pending(rec); + ScrStr *text = scr_strdec_write(encoding, pending, bytes); + scr_sdd_set_pending(rec, scr_strdec_next(encoding, pending, bytes)); + scr_bytes_release(bytes); + return text; +} + +static ScrDyn *scr_sdd_invoke(void *rec, ScrDyn *self, const char *method, ScrDyn *const *args, + size_t argc, const char *what) { + (void)self; + ScrDyn *buf = scr_tcd_codec_arg(args, argc, 0); + if (strcmp(method, "write") == 0) { + ScrStr *text = scr_sdd_write(rec, buf); + return text ? scr_tcd_str(text) : NULL; + } + if (strcmp(method, "end") == 0) { + /* end(buf): write(buf) when buf is not undefined, then the pending + * bytes' replacement text. */ + ScrStr *head = buf->kind == SCR_DYN_UNDEF ? scr_str_new("", 0) : scr_sdd_write(rec, buf); + if (!head) return NULL; + ScrStr *tail = scr_strdec_end(scr_sdd_encoding(rec), scr_sdd_pending(rec)); + scr_sdd_set_pending(rec, 0); + ScrStr *text = scr_str_concat(head, tail); + scr_str_release(head); + scr_str_release(tail); + return scr_tcd_str(text); + } + if (strcmp(method, "text") == 0) { + static const char message[] = "StringDecoder.prototype.text is not supported yet"; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); + return NULL; + } + scr_tcd_codec_not_a_function(what); + return NULL; +} + +static ScrDyn *scr_sdd_get(void *rec, const char *key, size_t len) { + if (len == 8 && memcmp(key, "encoding", 8) == 0) + return scr_dyn_new_str(scr_sdd_encoding(rec)); + if ((len == 8 && memcmp(key, "lastChar", 8) == 0) || + (len == 8 && memcmp(key, "lastNeed", 8) == 0) || + (len == 9 && memcmp(key, "lastTotal", 9) == 0)) { + static const char message[] = "StringDecoder lastChar, lastNeed and lastTotal are not supported yet"; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); + } + return NULL; +} + +static const ScrDynHandleMethod scr_sdd_methods[] = { + {.name = "write", .length = 1}, + {.name = "end", .length = 1}, + {.name = "text", .length = 2}, + {.name = NULL}, +}; + +static const ScrDynHandleOps scr_sdd_ops = { + .cls = "StringDecoder", + .retain = scr_sdd_retain, + .release = scr_sdd_release, + .invoke = scr_sdd_invoke, + .get = scr_sdd_get, + .set = scr_tcd_codec_set, + .methods = scr_sdd_methods, +}; + +ScrDyn *scr_string_decoder_to_dyn(void *rec, void *(*retain)(void *), void (*release)(void *), + size_t encoding, size_t pending) { + if (!scr_sdd_layout.retain) { + scr_sdd_layout = (ScrStringDecoderLayout){retain, release, encoding, pending}; + scr_dyn_handle_install(SCR_DYNH_STRING_DECODER, &scr_sdd_ops); + } + return scr_dyn_new_handle(rec, SCR_DYNH_STRING_DECODER); +} + +void *scr_string_decoder_from_dyn(const ScrDyn *d, const ScrDynPath *path, const char *want) { + return scr_dyn_handle_unbox(d, SCR_DYNH_STRING_DECODER, path, want); +} diff --git a/packages/runtime/src/scr_tls.c b/packages/runtime/src/scr_tls.c index e7436ab69..547fef27a 100644 --- a/packages/runtime/src/scr_tls.c +++ b/packages/runtime/src/scr_tls.c @@ -64,10 +64,15 @@ * expired") through the socket's pending-error sweep — then 'close', * Node's order, exactly like ECONNREFUSED. * + * Client certificates: servers take requestCert, rejectUnauthorized and + * ca and report socket.authorized/authorizationError as Node does; + * clients send a cert and key when asked. A `ca` also completes the + * chain its owner sends, as OpenSSL's automatic chain does. + * * Bounds (SEMANTICS.md divergence 55): no session resumption or - * tickets, no renegotiation/post-handshake auth, no client - * certificates, and non-verify client handshake failures carry a generic - * message where Node prints an OpenSSL one. + * tickets, no renegotiation/post-handshake auth, and non-verify client + * handshake failures carry a generic message where Node prints an + * OpenSSL one. * * Certificate names: a client always checks the leaf against one name, * the servername when it is non-empty and the host otherwise. SNI carries @@ -82,6 +87,7 @@ #include #include #include +#include #include #include #ifdef _WIN32 @@ -202,6 +208,42 @@ static void scr_tls_bad_material(const char *what, int err) { _Exit(1); } +/* OpenSSL's automatic chain (ssl_add_cert_chain): a certificate option + * that holds only the leaf is sent with the issuers `store` holds, up to + * a self-issued one, as far as they are found. A chain given in the + * option is sent as it is. */ +static void scr_tls_auto_chain(mbedtls_x509_crt *own, const mbedtls_x509_crt *store) { + if (own->raw.len == 0 || own->next != NULL) return; + const mbedtls_x509_crt *at = own; + for (int depth = 0; depth < 8; depth++) { + if (at->issuer_raw.len == at->subject_raw.len && + memcmp(at->issuer_raw.p, at->subject_raw.p, at->subject_raw.len) == 0) + return; + const mbedtls_x509_crt *issuer = NULL; + for (const mbedtls_x509_crt *c = store; c != NULL && c->raw.len > 0 && issuer == NULL; c = c->next) { + if (c->subject_raw.len == at->issuer_raw.len && + memcmp(c->subject_raw.p, at->issuer_raw.p, c->subject_raw.len) == 0) + issuer = c; + } + if (issuer == NULL) return; + for (const mbedtls_x509_crt *c = own; c != NULL; c = c->next) { + if (c->raw.len == issuer->raw.len && memcmp(c->raw.p, issuer->raw.p, c->raw.len) == 0) return; + } + if (mbedtls_x509_crt_parse_der(own, issuer->raw.p, issuer->raw.len) != 0) return; + while (at->next != NULL) at = at->next; + } +} + +/* A copy of a certificate chain, completed from `store` as the automatic + * chain does, for a use that must leave the original as it is. */ +static void scr_tls_chained_copy(mbedtls_x509_crt *out, const mbedtls_x509_crt *chain, + const mbedtls_x509_crt *store) { + for (const mbedtls_x509_crt *c = chain; c != NULL && c->raw.len > 0; c = c->next) { + if (mbedtls_x509_crt_parse_der(out, c->raw.p, c->raw.len) != 0) scr_tls_bad_material("certificate", 0); + } + scr_tls_auto_chain(out, store); +} + /* ── SecureContext (tls.createSecureContext) ─────────────────────────── */ /* An opaque refcounted parsed cert/key pair — what an SNI callback @@ -210,6 +252,12 @@ struct ScrSecureCtx { size_t rc; mbedtls_x509_crt cert; mbedtls_pk_context pk; + /* the ca option: a socket created from this context (a TLSSocket's + * secureContext) sends the chain it completes, and a server that serves + * the context verifies client certificates against it. has_ca tells an + * empty ca (an empty trust store) from none (the default store). */ + bool has_ca; + mbedtls_x509_crt ca; }; #ifdef SCR_RC_AUDIT @@ -224,6 +272,7 @@ ScrSecureCtx *scr_tls_create_secure_context(const char *cert, size_t cert_len, if (!c) scr_tls_oom(); c->rc = 1; mbedtls_x509_crt_init(&c->cert); + mbedtls_x509_crt_init(&c->ca); mbedtls_pk_init(&c->pk); int r = scr_tls_parse_crt(&c->cert, cert, cert_len); if (r != 0) scr_tls_bad_material("certificate", r); @@ -243,6 +292,7 @@ ScrSecureCtx *scr_secure_ctx_retain(ScrSecureCtx *c) { void scr_secure_ctx_release(ScrSecureCtx *c) { if (!c || --c->rc != 0) return; mbedtls_x509_crt_free(&c->cert); + mbedtls_x509_crt_free(&c->ca); mbedtls_pk_free(&c->pk); free(c); #ifdef SCR_RC_AUDIT @@ -372,6 +422,29 @@ typedef struct ScrTlsSrv { * certificate and key this config serves (+1; NULL when cert/key are the * config's own) */ ScrSecureCtx *ctx; + /* client certificates: requestCert asks for one and verifies it against + * `ca` (the default store without one); with rejectUnauthorized a client + * that sends none fails the handshake and one that fails verification + * is dropped before 'secureConnection', as in Node */ + bool request_cert; + bool reject_unauthorized; + bool has_ca; + mbedtls_x509_crt ca; + /* new tls.TLSSocket(socket, { isServer }): Node verifies client + * certificates only in tls.Server, so a standalone socket reports no + * verdict (authorized false, authorizationError null) and drops no + * client for one; rejectUnauthorized there only requires a certificate */ + bool standalone; + /* server.addContext(servername, context): lowercased name patterns + * ('*' matches within one label) with their contexts (+1), newest + * last; server.setSecureContext's replacement default (+1, NULL for the + * server's own). Both serve through scr_tls_f_cert. */ + struct ScrTlsNamedCtx { + char *pattern; + ScrSecureCtx *ctx; + } *contexts; + size_t ncontexts; + ScrSecureCtx *default_ctx; } ScrTlsSrv; /* The ALPN answers (static storage — mbedTLS keeps the pointers). Plain @@ -389,6 +462,7 @@ static ScrTlsSrv *scr_tls_srv_new(const char *cert, size_t cert_len, const char if (!s) scr_tls_oom(); s->rc = 1; mbedtls_x509_crt_init(&s->cert); + mbedtls_x509_crt_init(&s->ca); mbedtls_pk_init(&s->pk); mbedtls_ssl_config_init(&s->conf); int r = scr_tls_parse_crt(&s->cert, cert, cert_len); @@ -401,7 +475,8 @@ static ScrTlsSrv *scr_tls_srv_new(const char *cert, size_t cert_len, const char scr_tls_oom(); } mbedtls_ssl_conf_rng(&s->conf, mbedtls_ctr_drbg_random, &scr_tls_drbg); - mbedtls_ssl_conf_authmode(&s->conf, MBEDTLS_SSL_VERIFY_NONE); /* no client certs, Node's default */ + /* no client certificate unless requestCert asks (scr_tls_srv_client_auth) */ + mbedtls_ssl_conf_authmode(&s->conf, MBEDTLS_SSL_VERIFY_NONE); if (mbedtls_ssl_conf_own_cert(&s->conf, &s->cert, &s->pk) != 0) { scr_tls_bad_material("certificate/key pair", 0); } @@ -416,6 +491,7 @@ static ScrTlsSrv *scr_tls_srv_from_ctx(ScrSecureCtx *ctx /*borrowed*/) { if (!s) scr_tls_oom(); s->rc = 1; mbedtls_x509_crt_init(&s->cert); + mbedtls_x509_crt_init(&s->ca); mbedtls_pk_init(&s->pk); mbedtls_ssl_config_init(&s->conf); if (mbedtls_ssl_config_defaults(&s->conf, MBEDTLS_SSL_IS_SERVER, MBEDTLS_SSL_TRANSPORT_STREAM, @@ -425,7 +501,13 @@ static ScrTlsSrv *scr_tls_srv_from_ctx(ScrSecureCtx *ctx /*borrowed*/) { mbedtls_ssl_conf_rng(&s->conf, mbedtls_ctr_drbg_random, &scr_tls_drbg); mbedtls_ssl_conf_authmode(&s->conf, MBEDTLS_SSL_VERIFY_NONE); s->ctx = scr_secure_ctx_retain(ctx); - if (mbedtls_ssl_conf_own_cert(&s->conf, &ctx->cert, &ctx->pk) != 0) { + /* the context's ca completes the chain a socket created from it sends */ + const mbedtls_x509_crt *own = &ctx->cert; + if (ctx->ca.raw.len > 0) { + scr_tls_chained_copy(&s->cert, &ctx->cert, &ctx->ca); + own = &s->cert; + } + if (mbedtls_ssl_conf_own_cert(&s->conf, (mbedtls_x509_crt *)own, &ctx->pk) != 0) { scr_tls_bad_material("certificate/key pair", 0); } return s; @@ -443,18 +525,60 @@ static void scr_tls_srv_release(ScrTlsSrv *s) { scr_closure_release(s->sni_cb); mbedtls_ssl_config_free(&s->conf); mbedtls_x509_crt_free(&s->cert); + mbedtls_x509_crt_free(&s->ca); mbedtls_pk_free(&s->pk); scr_tls_alpn_free(s->alpn); scr_secure_ctx_release(s->ctx); + for (size_t i = 0; i < s->ncontexts; i++) { + free(s->contexts[i].pattern); + scr_secure_ctx_release(s->contexts[i].ctx); + } + free(s->contexts); + scr_secure_ctx_release(s->default_ctx); free(s); } static void scr_tls_srv_release_v(void *p) { scr_tls_srv_release((ScrTlsSrv *)p); } +/* The client-certificate options of a server: requestCert, rejectUnauthorized + * (Node's true default) and ca (+1, NULL for none). */ +typedef struct { + bool request; + bool reject; + ScrBytes *ca; +} ScrTlsClientAuth; + +static mbedtls_x509_crt *scr_tls_system_ca(void); + +/* Applies a server's client-certificate options. A `ca` also completes the + * server's own chain, as OpenSSL's automatic chain sends it. */ +static void scr_tls_srv_client_auth(ScrTlsSrv *s, const ScrTlsClientAuth *auth) { + if (auth == NULL) return; + /* An empty ca Buffer is an empty trust store, not the default one. */ + if (auth->ca != NULL) { + if (auth->ca->len > 0) { + int r = scr_tls_parse_crt(&s->ca, (const char *)auth->ca->data, auth->ca->len); + if (r != 0) scr_tls_bad_material("ca certificate", r); + if (s->ctx == NULL) scr_tls_auto_chain(&s->cert, &s->ca); + } + s->has_ca = true; + } + if (!auth->request) return; + s->request_cert = true; + s->reject_unauthorized = auth->reject; + /* OPTIONAL: the handshake records the verdict and goes on, as Node's + * verify callback does; scr_tls_handshake applies rejectUnauthorized */ + mbedtls_ssl_conf_authmode(&s->conf, MBEDTLS_SSL_VERIFY_OPTIONAL); + mbedtls_ssl_conf_ca_chain(&s->conf, s->has_ca ? &s->ca : scr_tls_system_ca(), NULL); +} + /* ── the client config (one per https.request, owned by its tctx) ────── */ typedef struct ScrTlsCli { mbedtls_ssl_config conf; + /* the ca option, which replaces the default store; has_ca tells an + * empty ca (an empty trust store) from none */ + bool has_ca; mbedtls_x509_crt ca; char *hostname; /* the SNI name; NULL sends none (an IP literal, servername: "") */ char *verify_name; /* the name the leaf is checked against; NULL fails the check */ @@ -463,6 +587,7 @@ typedef struct ScrTlsCli { * VERIFY_NONE socket (the https client's rejectUnauthorized:false) * never verified, so authorized answers false like Node's. */ bool verify_recorded; + bool reject; /* rejectUnauthorized: a verification failure fails the handshake */ /* The ALPNProtocols option's names, NULL-terminated, in the caller's * order (mbedtls_ssl_conf_alpn_protocols keeps the pointer); NULL when * none are offered. */ @@ -470,6 +595,11 @@ typedef struct ScrTlsCli { /* No TLS version is in the configured range: the handshake fails as * soon as it starts (Node's ERR_SSL_NO_PROTOCOLS_AVAILABLE). */ bool no_protocols; + /* the client certificate (cert and key options), sent when the server + * asks for one */ + bool has_own; + mbedtls_x509_crt own; + mbedtls_pk_context own_pk; } ScrTlsCli; /* The default trust anchors when no `ca` option is given: the host trust @@ -562,6 +692,8 @@ static ScrTlsCli *scr_tls_cli_new2(ScrStr *host, bool reject_unauthorized, const if (!c) scr_tls_oom(); mbedtls_ssl_config_init(&c->conf); mbedtls_x509_crt_init(&c->ca); + mbedtls_x509_crt_init(&c->own); + mbedtls_pk_init(&c->own_pk); if (mbedtls_ssl_config_defaults(&c->conf, MBEDTLS_SSL_IS_CLIENT, MBEDTLS_SSL_TRANSPORT_STREAM, MBEDTLS_SSL_PRESET_DEFAULT) != 0) { @@ -569,6 +701,8 @@ static ScrTlsCli *scr_tls_cli_new2(ScrStr *host, bool reject_unauthorized, const } mbedtls_ssl_conf_rng(&c->conf, mbedtls_ctr_drbg_random, &scr_tls_drbg); c->verify_recorded = reject_unauthorized || verify_optional; + c->reject = reject_unauthorized; + c->has_ca = ca != NULL; if (ca_len > 0) { int r = scr_tls_parse_crt(&c->ca, ca, ca_len); if (r != 0) scr_tls_bad_material("ca certificate", r); @@ -578,7 +712,7 @@ static ScrTlsCli *scr_tls_cli_new2(ScrStr *host, bool reject_unauthorized, const } else { mbedtls_ssl_conf_authmode(&c->conf, reject_unauthorized ? MBEDTLS_SSL_VERIFY_REQUIRED : MBEDTLS_SSL_VERIFY_OPTIONAL); - mbedtls_ssl_conf_ca_chain(&c->conf, ca_len > 0 ? &c->ca : scr_tls_system_ca(), NULL); + mbedtls_ssl_conf_ca_chain(&c->conf, c->has_ca ? &c->ca : scr_tls_system_ca(), NULL); } /* The host is the verified name; it is also the SNI name unless it is * an IP literal. */ @@ -620,11 +754,27 @@ static void scr_tls_cli_free(ScrTlsCli *c) { mbedtls_ssl_config_free(&c->conf); scr_tls_alpn_free(c->alpn); mbedtls_x509_crt_free(&c->ca); + mbedtls_x509_crt_free(&c->own); + mbedtls_pk_free(&c->own_pk); free(c->hostname); free(c->verify_name); free(c); } +/* The cert and key options of a client: the certificate (with the issuers + * its `ca` holds, OpenSSL's automatic chain) is sent when the server asks + * for one. */ +static void scr_tls_cli_set_own(ScrTlsCli *c, const ScrBytes *cert, const ScrBytes *key) { + int r = scr_tls_parse_crt(&c->own, (const char *)cert->data, cert->len); + if (r != 0) scr_tls_bad_material("certificate", r); + r = scr_tls_parse_key(&c->own_pk, (const char *)key->data, key->len); + if (r != 0) scr_tls_bad_material("private key", r); + if (c->ca.raw.len > 0) scr_tls_auto_chain(&c->own, &c->ca); + if (mbedtls_ssl_conf_own_cert(&c->conf, &c->own, &c->own_pk) != 0) + scr_tls_bad_material("certificate/key pair", 0); + c->has_own = true; +} + /* ── the per-socket transport ────────────────────────────────────────── */ /* The SNI pre-parse states: NONE (no callback on the server — the @@ -656,6 +806,9 @@ typedef struct ScrTlsSock { unsigned char *hello; /* the raw ClientHello flight, replayed at DONE */ size_t hello_len, hello_cap; ScrSecureCtx *sni_ctx; /* owned; the answer (NULL = default cert) */ + /* scr_tls_f_cert verified client certificates against sni_ctx's ca + * (or the default store without one), not the server's */ + bool ctx_verifies; /* new tls.TLSSocket(socket, { isServer: true }): the program holds this * socket before the handshake, so a failed handshake is its 'error' * (tls.createServer's sockets fail without one) */ @@ -665,8 +818,32 @@ typedef struct ScrTlsSock { /* the 'secure' event's listeners */ ScrNetLs secure_ls; bool settled; /* the socket closed: listeners added later never fire */ + /* a TLS 1.3 client sent no certificate to a server that requires one; + * the alert goes out once the handshake is over */ + bool cert_missing; + /* a fatal alert was reported without closing: reads end the stream */ + bool alert_reported; + /* a server socket: the accepting server (+1), which 'tlsClientError' + * fires on, and a hang-up waiting for the socket to close (Node emits + * ECONNRESET 'socket hang up' from the socket's 'close') */ + ScrNetServer *server; + bool hang_up_pending; + /* a client whose verification failed: like Node's, it closes without + * sending the alert, so later writes are dropped */ + bool quiet_failure; + /* the served context's chain completed from the server's ca */ + mbedtls_x509_crt served_chain; + /* what the handshake's last step and the established callbacks write: + * Node flushes it after the nextTick queue and before the immediates + * queued in those callbacks, so a destroy() there drops it. `holding` + * collects; held[held_off..held_len) waits for the fd. */ + bool holding; + unsigned char *held; + size_t held_len, held_off, held_cap; } ScrTlsSock; +static bool scr_tls_alert_error(ScrTlsSock *t, unsigned char alert); + /* checkServerIdentity for an IP literal: the address must be one of the * certificate's IP SANs (no DNS SAN or CN applies). */ static bool scr_tls_leaf_has_ip(const mbedtls_x509_crt *crt, const char *ip) { @@ -711,6 +888,9 @@ static int scr_tls_vrfy_cb(void *ctx, mbedtls_x509_crt *crt, int depth, uint32_t *flags |= name_flags & MBEDTLS_X509_BADCERT_CN_MISMATCH; } } + /* a rejecting client fails the handshake on any flag; Node's client + * then closes without sending an alert (its Finished is discarded) */ + if (*flags != 0 && t->cli != NULL && t->cli->reject) t->quiet_failure = true; bool self = crt->issuer_raw.len == crt->subject_raw.len && memcmp(crt->issuer_raw.p, crt->subject_raw.p, crt->subject_raw.len) == 0; if (*flags & MBEDTLS_X509_BADCERT_EXPIRED) t->vrfy_expired = true; @@ -725,8 +905,52 @@ static int scr_tls_vrfy_cb(void *ctx, mbedtls_x509_crt *crt, int depth, uint32_t /* The BIOs: nonblocking fd I/O in mbedTLS's own convention. A send that * would block arms the socket's write filter so the handshake resumes on * writability. */ +static void scr_tls_hold(ScrTlsSock *t, const unsigned char *buf, size_t len) { + if (t->held_len + len > t->held_cap) { + size_t cap = t->held_cap ? t->held_cap : 4096; + while (cap < t->held_len + len) cap *= 2; + unsigned char *grown = realloc(t->held, cap); + if (grown == NULL) scr_tls_oom(); + t->held = grown; + t->held_cap = cap; + } + memcpy(t->held + t->held_len, buf, len); + t->held_len += len; +} + +/* Writes what is held: 1 once it is all out, 0 when the fd would block + * (the write filter is armed for the rest), -1 when the connection is + * gone (the bytes are dropped). */ +static int scr_tls_flush_held(ScrTlsSock *t) { + int fd = scr_net_sock_fd(t->sock); + while (t->held_off < t->held_len) { + if (fd < 0) break; + ssize_t n = write(fd, t->held + t->held_off, t->held_len - t->held_off); + if (n > 0) { + t->held_off += (size_t)n; + continue; + } + if (n < 0 && errno == EINTR) continue; + if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) { + scr_net_sock_transport_want_write(t->sock); + return 0; + } + break; + } + bool out = t->held_off == t->held_len; + t->held_off = t->held_len = 0; + return out ? 1 : -1; +} + static int scr_tls_bio_send(void *ctx, const unsigned char *buf, size_t len) { ScrTlsSock *t = (ScrTlsSock *)ctx; + if (t->quiet_failure) return (int)len; + if (t->holding) { + scr_tls_hold(t, buf, len); + return (int)len; + } + /* held bytes go first; mbedTLS retries this record once they are out */ + if (t->held_len > t->held_off && scr_tls_flush_held(t) == 0) return MBEDTLS_ERR_SSL_WANT_WRITE; int fd = scr_net_sock_fd(t->sock); if (fd < 0) return MBEDTLS_ERR_NET_CONN_RESET; ssize_t n = write(fd, buf, len); @@ -742,7 +966,7 @@ static int scr_tls_bio_send(void *ctx, const unsigned char *buf, size_t len) { /* Keeps the first bytes a wrapped socket's peer sends: a handshake that * fails on them reports what they looked like, as OpenSSL does. */ static void scr_tls_note_head(ScrTlsSock *t, const unsigned char *buf, size_t n) { - if (!t->wrapped || t->head_len == sizeof t->head) return; + if (t->srv == NULL || t->head_len == sizeof t->head) return; size_t take = sizeof t->head - t->head_len; if (take > n) take = n; memcpy(t->head + t->head_len, buf, take); @@ -770,6 +994,21 @@ static int scr_tls_bio_recv(void *ctx, unsigned char *buf, size_t len) { return MBEDTLS_ERR_NET_RECV_FAILED; } +/* Node reads what the kernel holds (one 64 KiB libuv read) before + * OpenSSL sees the records, so a failed handshake closes with nothing + * unread and the peer gets a FIN. Closing with the rest of, say, an HTTP + * request still queued would make the kernel send a reset instead. */ +static void scr_tls_drain_unread(ScrTlsSock *t) { + int fd = scr_net_sock_fd(t->sock); + if (fd < 0) return; + unsigned char sink[4096]; + for (size_t total = 0; total < 65536; ) { + ssize_t n = read(fd, sink, sizeof sink); + if (n <= 0) break; + total += (size_t)n; + } +} + /* ── the transport ops ───────────────────────────────────────────────── */ static void scr_tls_fire_session(ScrTlsSock *t); @@ -793,6 +1032,14 @@ static ssize_t scr_tls_xread(void *tctx, char *buf, size_t n) { errno = EAGAIN; return -1; } + if (t->alert_reported) return 0; /* the server closing after its alert */ + /* a TLS 1.3 server rejects a missing client certificate after the + * client finished its handshake */ + if (r == MBEDTLS_ERR_SSL_FATAL_ALERT_MESSAGE && t->cli != NULL && + t->ssl.MBEDTLS_PRIVATE(in_msg) != NULL && scr_tls_alert_error(t, t->ssl.MBEDTLS_PRIVATE(in_msg)[1])) { + errno = EAGAIN; + return -1; + } errno = ECONNRESET; /* protocol-level death reads as a reset */ return -1; } @@ -1081,6 +1328,99 @@ void scr_tls_sni_answer(ScrClosure *self, bool has_err, ScrSecureCtx *ctx /*move * ECONNRESET, and bytes that are not a TLS record name what they were * (OpenSSL's reasons without the per-process prefix and source location, * SEMANTICS.md divergence 55). */ +/* The OpenSSL reason (without its per-process prefix and source location, + * SEMANTICS.md divergence 55) and Node's code for a server handshake that + * failed with mbedTLS error `r`; false when the peer closed or reset the + * connection instead. A failure mbedTLS reports without a reason OpenSSL + * would give answers a coded SC2020 refusal. */ +static bool scr_tls_server_reason(ScrTlsSock *t, int r, const char **msg, const char **code) { + if (r == MBEDTLS_ERR_SSL_CONN_EOF || r == MBEDTLS_ERR_NET_CONN_RESET) return false; + if (r == MBEDTLS_ERR_SSL_NO_CLIENT_CERTIFICATE) { + *msg = "error:0A0000C7:SSL routines:tls_process_client_certificate:peer did not return a certificate"; + *code = "ERR_SSL_PEER_DID_NOT_RETURN_A_CERTIFICATE"; + return true; + } + const unsigned char *h = t->head; + bool record = t->head_len >= 3 && h[0] >= 20 && h[0] <= 23 && h[1] == 3; + if (t->head_len >= 5 && !record) { + static const char *const http[] = { "GET ", "POST ", "HEAD ", "PATCH", "OPTIO", + "DELET", "TRACE", "PUT ", NULL }; + *msg = "error:0A00010B:SSL routines:tls_validate_record_header:wrong version number"; + *code = "ERR_SSL_WRONG_VERSION_NUMBER"; + for (size_t i = 0; http[i] != NULL; i++) { + if (memcmp(h, http[i], strlen(http[i])) == 0) { + *msg = "error:0A00009C:SSL routines:tls_validate_record_header:http request"; + *code = "ERR_SSL_HTTP_REQUEST"; + } + } + if (memcmp(h, "CONNE", 5) == 0) { + *msg = "error:0A00009B:SSL routines:tls_validate_record_header:https proxy request"; + *code = "ERR_SSL_HTTPS_PROXY_REQUEST"; + } + return true; + } + if (r == MBEDTLS_ERR_SSL_FATAL_ALERT_MESSAGE && t->ssl.MBEDTLS_PRIVATE(in_msg) != NULL) { + /* an alert the client sent: OpenSSL's reason is 1000 + its number */ + static const struct { unsigned char alert; const char *msg, *code; } alerts[] = { + { 40, "error:0A000410:SSL routines:ssl3_read_bytes:ssl/tls alert handshake failure", + "ERR_SSL_SSL/TLS_ALERT_HANDSHAKE_FAILURE" }, + { 42, "error:0A000412:SSL routines:ssl3_read_bytes:sslv3 alert bad certificate", + "ERR_SSL_SSLV3_ALERT_BAD_CERTIFICATE" }, + { 45, "error:0A000415:SSL routines:ssl3_read_bytes:sslv3 alert certificate expired", + "ERR_SSL_SSLV3_ALERT_CERTIFICATE_EXPIRED" }, + { 46, "error:0A000416:SSL routines:ssl3_read_bytes:sslv3 alert certificate unknown", + "ERR_SSL_SSLV3_ALERT_CERTIFICATE_UNKNOWN" }, + { 48, "error:0A000418:SSL routines:ssl3_read_bytes:tlsv1 alert unknown ca", + "ERR_SSL_TLSV1_ALERT_UNKNOWN_CA" }, + { 49, "error:0A000419:SSL routines:ssl3_read_bytes:tlsv1 alert access denied", + "ERR_SSL_TLSV1_ALERT_ACCESS_DENIED" }, + { 51, "error:0A00041B:SSL routines:ssl3_read_bytes:tlsv1 alert decrypt error", + "ERR_SSL_TLSV1_ALERT_DECRYPT_ERROR" }, + { 70, "error:0A00042E:SSL routines:ssl3_read_bytes:tlsv1 alert protocol version", + "ERR_SSL_TLSV1_ALERT_PROTOCOL_VERSION" }, + { 80, "error:0A000438:SSL routines:ssl3_read_bytes:tlsv1 alert internal error", + "ERR_SSL_TLSV1_ALERT_INTERNAL_ERROR" }, + }; + unsigned char alert = t->ssl.MBEDTLS_PRIVATE(in_msg)[1]; + for (size_t i = 0; i < sizeof alerts / sizeof alerts[0]; i++) { + if (alerts[i].alert == alert) { + *msg = alerts[i].msg; + *code = alerts[i].code; + return true; + } + } + } + if (r == MBEDTLS_ERR_SSL_BAD_PROTOCOL_VERSION) { + *msg = "error:0A000102:SSL routines:tls_early_post_process_client_hello:unsupported protocol"; + *code = "ERR_SSL_UNSUPPORTED_PROTOCOL"; + return true; + } + if (r == MBEDTLS_ERR_SSL_NO_APPLICATION_PROTOCOL) { + *msg = "error:0A0000EB:SSL routines:tls_handle_alpn:no application protocol"; + *code = "ERR_SSL_NO_APPLICATION_PROTOCOL"; + return true; + } + static SCR_TL char refusal[128]; + snprintf(refusal, sizeof refusal, + "a TLS handshake failure (mbedTLS error -0x%04X) without the reason OpenSSL gives is not supported yet", + (unsigned)-r); + *msg = refusal; + *code = "SC2020"; + return true; +} + +/* 'tlsClientError' on the accepting server: Node's Error with `code`, and + * the socket. Without a listener the socket is only destroyed. */ +static void scr_tls_client_error(ScrTlsSock *t, const char *msg, const char *code) { + if (t->server == NULL) return; + ScrStr *m = scr_str_new(msg, strlen(msg)); + ScrError *e = scr_error_new(SCR_ERR_ERROR, m); + scr_str_release(m); + scr_error_set_code(e, code); + scr_net_server_fire_client_error(t->server, e, t->sock); + scr_error_release(e); +} + static void scr_tls_wrapped_failure(ScrTlsSock *t, int r) { if (r == MBEDTLS_ERR_SSL_CONN_EOF) { scr_net_sock_transport_eof(t->sock); @@ -1090,27 +1430,86 @@ static void scr_tls_wrapped_failure(ScrTlsSock *t, int r) { scr_net_sock_transport_error(t->sock, "read ECONNRESET"); return; } - const unsigned char *h = t->head; - bool record = t->head_len >= 3 && h[0] >= 20 && h[0] <= 23 && h[1] == 3; - if (t->head_len >= 5 && !record) { - static const char *const http[] = { "GET ", "POST ", "HEAD ", "PATCH", "OPTIO", - "DELET", "TRACE", "PUT ", NULL }; - const char *reason = "error:0A00010B:SSL routines:tls_validate_record_header:wrong version number"; - const char *code = "ERR_SSL_WRONG_VERSION_NUMBER"; - for (size_t i = 0; http[i] != NULL; i++) { - if (memcmp(h, http[i], strlen(http[i])) == 0) { - reason = "error:0A00009C:SSL routines:tls_validate_record_header:http request"; - code = "ERR_SSL_HTTP_REQUEST"; - } - } - if (memcmp(h, "CONNE", 5) == 0) { - reason = "error:0A00009B:SSL routines:tls_validate_record_header:https proxy request"; - code = "ERR_SSL_HTTPS_PROXY_REQUEST"; - } - scr_net_sock_transport_error_code(t->sock, reason, code); - return; + const char *msg, *code; + (void)scr_tls_server_reason(t, r, &msg, &code); + scr_net_sock_transport_error_code(t->sock, msg, code); +} + +/* A fatal alert a server sends a client without the certificate it + * requires: the client's error is OpenSSL's reason (without its + * per-process prefix and source location, SEMANTICS.md divergence 55) and + * Node's code. False for other alerts. */ +static bool scr_tls_alert_error(ScrTlsSock *t, unsigned char alert) { + if (alert == MBEDTLS_SSL_ALERT_MSG_CERT_REQUIRED) { + /* after the client's handshake: Node emits 'error' and the socket + * then ends when the server closes, with close(false) */ + scr_net_sock_transport_report(t->sock, + "error:0A00045C:SSL routines:ssl3_read_bytes:tlsv13 alert certificate required", + "ERR_SSL_TLSV13_ALERT_CERTIFICATE_REQUIRED"); + t->alert_reported = true; + return true; } - scr_net_sock_transport_error(t->sock, "server TLS handshake failed"); + if (alert == MBEDTLS_SSL_ALERT_MSG_HANDSHAKE_FAILURE) { + scr_net_sock_transport_error_code(t->sock, + "error:0A000410:SSL routines:ssl3_read_bytes:ssl/tls alert handshake failure", + "ERR_SSL_SSL/TLS_ALERT_HANDSHAKE_FAILURE"); + return true; + } + return false; +} + +/* A server that requires a client certificate (requestCert with + * rejectUnauthorized) runs its handshake a step at a time, so a client + * that sends none fails as soon as its empty Certificate is read, before + * the server's Finished in TLS 1.2, with OpenSSL's alert: certificate + * required in TLS 1.3, handshake failure in TLS 1.2. */ +static int scr_tls_handshake_requiring_cert(ScrTlsSock *t) { + while (!mbedtls_ssl_is_handshake_over(&t->ssl)) { + int r = mbedtls_ssl_handshake_step(&t->ssl); + if (r != 0) return r; + if (t->cert_missing) continue; + /* all ones until the client's Certificate is read */ + uint32_t verdict = mbedtls_ssl_get_verify_result(&t->ssl); + if (verdict == UINT32_MAX || !(verdict & MBEDTLS_X509_BADCERT_MISSING)) continue; + if (mbedtls_ssl_get_version_number(&t->ssl) != MBEDTLS_SSL_VERSION_TLS1_3) { + (void)mbedtls_ssl_send_alert_message(&t->ssl, MBEDTLS_SSL_ALERT_LEVEL_FATAL, + MBEDTLS_SSL_ALERT_MSG_HANDSHAKE_FAILURE); + return MBEDTLS_ERR_SSL_NO_CLIENT_CERTIFICATE; + } + /* TLS 1.3: the client has finished and reads under the server's + * application keys, which mbedTLS writes with only once the handshake + * is over, so the alert waits for that */ + t->cert_missing = true; + } + if (t->cert_missing) { + (void)mbedtls_ssl_send_alert_message(&t->ssl, MBEDTLS_SSL_ALERT_LEVEL_FATAL, + MBEDTLS_SSL_ALERT_MSG_CERT_REQUIRED); + return MBEDTLS_ERR_SSL_NO_CLIENT_CERTIFICATE; + } + return 0; +} + +static ScrTlsSock *scr_tls_sock_of(ScrNetSocket *s); + +/* The immediate that ends the hold: queued as the handshake finishes, + * before the established callbacks run, so it comes ahead of any + * immediate they queue. A socket destroyed by then has no fd, and what + * was held is dropped. */ +static void scr_tls_release_hold(ScrClosure *self) { + ScrNetSocket *s = scr_box_get_ref(self->caps[0]); /* +1 */ + ScrTlsSock *t = scr_tls_sock_of(s); + if (t != NULL && t->holding) { + t->holding = false; + if (t->held_len > t->held_off) (void)scr_tls_flush_held(t); + } + scr_net_sock_release(s); +} + +static void scr_tls_hold_until_immediate(ScrTlsSock *t) { + ScrClosure *cb = scr_closure_new((void *)&scr_tls_release_hold, 1); + cb->caps[0] = scr_box_new_obj(&scr_net_sock_retain_v, &scr_net_sock_release_v, NULL); + scr_box_set_ref(cb->caps[0], scr_net_sock_retain(t->sock)); + (void)scr_set_immediate(cb); } static int scr_tls_handshake(void *tctx) { @@ -1129,8 +1528,27 @@ static int scr_tls_handshake(void *tctx) { } if (t->sni_state == SCR_SNI_WAIT) return 0; /* the callback is deciding */ scr_tls_current = t; - int r = mbedtls_ssl_handshake(&t->ssl); + t->holding = true; + int r = t->srv != NULL && t->srv->request_cert && t->srv->reject_unauthorized + ? scr_tls_handshake_requiring_cert(t) + : mbedtls_ssl_handshake(&t->ssl); scr_tls_current = NULL; + if (r == 0) { + scr_tls_hold_until_immediate(t); + } else { + /* a step that did not finish the handshake sends what it wrote now */ + t->holding = false; + if (t->held_len > t->held_off) (void)scr_tls_flush_held(t); + } + if (r == 0 && t->srv != NULL && t->srv->request_cert && t->srv->reject_unauthorized && + !t->srv->standalone && mbedtls_ssl_get_verify_result(&t->ssl) != 0) { + /* a client certificate that fails verification: Node's server + * destroys the socket before 'secureConnection', and its + * 'tlsClientError' is the hang-up */ + scr_net_sock_transport_error(t->sock, NULL); + t->hang_up_pending = true; + return -1; + } if (r == 0) return 1; if (r == MBEDTLS_ERR_SSL_WANT_READ || r == MBEDTLS_ERR_SSL_WANT_WRITE) return 0; if (t->srv != NULL && t->wrapped) { @@ -1138,21 +1556,38 @@ static int scr_tls_handshake(void *tctx) { return -1; } if (t->srv != NULL) { - /* Node's server default: 'tlsClientError' with no listener destroys - * the socket without an error event — exactly what portless (no - * listener) sees. */ + /* the socket is destroyed without an error event, and the server's + * 'tlsClientError' gets the failure: now, before the socket closes, + * for an OpenSSL reason; once it closes for a hang-up, as Node + * emits ECONNRESET from the socket's 'close' */ + const char *msg, *code; + bool reason = scr_tls_server_reason(t, r, &msg, &code); + /* Node's socket still has its handle when an OpenSSL reason is + * emitted, so remoteAddress answers; the address is cached before + * the fd closes */ + if (reason) scr_str_release(scr_net_sock_remote_address(t->sock)); + scr_tls_drain_unread(t); scr_net_sock_transport_error(t->sock, NULL); + if (reason) scr_tls_client_error(t, msg, code); + else t->hang_up_pending = true; return -1; } char msg[160]; if (r == MBEDTLS_ERR_X509_CERT_VERIFY_FAILED) { scr_tls_verify_msg(t, msg, sizeof msg); - } else if (r == MBEDTLS_ERR_SSL_CONN_EOF || r == MBEDTLS_ERR_NET_CONN_RESET) { - /* The peer closed or reset the connection mid-handshake: Node's - * ECONNRESET for tls.connect and https.request alike. */ + } else if (r == MBEDTLS_ERR_SSL_CONN_EOF) { + /* The peer closed the connection mid-handshake: Node's socket ends, + * then fails with ECONNRESET, for tls.connect and https.request + * alike. */ + scr_net_sock_transport_ended(t->sock); + if (scr_exc_pending()) return -1; scr_net_sock_transport_error( t->sock, "Client network socket disconnected before secure TLS connection was established"); return -1; + } else if (r == MBEDTLS_ERR_NET_CONN_RESET) { + /* a reset is the read's own error, as in Node */ + scr_net_sock_transport_error(t->sock, "read ECONNRESET"); + return -1; } else if (r == MBEDTLS_ERR_SSL_FATAL_ALERT_MESSAGE && t->ssl.MBEDTLS_PRIVATE(in_msg) != NULL && t->ssl.MBEDTLS_PRIVATE(in_msg)[1] == MBEDTLS_SSL_ALERT_MSG_NO_APPLICATION_PROTOCOL) { @@ -1167,6 +1602,9 @@ static int scr_tls_handshake(void *tctx) { t->ssl.MBEDTLS_PRIVATE(in_msg)[1] == MBEDTLS_SSL_ALERT_MSG_PROTOCOL_VERSION) { /* The server accepts none of the offered versions. */ snprintf(msg, sizeof msg, "error:0A00042E:SSL routines:ssl3_read_bytes:tlsv1 alert protocol version"); + } else if (r == MBEDTLS_ERR_SSL_FATAL_ALERT_MESSAGE && t->ssl.MBEDTLS_PRIVATE(in_msg) != NULL && + scr_tls_alert_error(t, t->ssl.MBEDTLS_PRIVATE(in_msg)[1])) { + return -1; } else { /* non-verify handshake failures: honest but not Node's exact openssl * string (SEMANTICS.md divergence 55) */ @@ -1215,6 +1653,9 @@ static void scr_tls_shutdown_write(void *tctx) { ScrTlsSock *t = (ScrTlsSock *)tctx; if (t->notify_sent) return; t->notify_sent = true; + /* end() sends what is held, then the close_notify, ahead of the FIN */ + t->holding = false; + if (t->held_len > t->held_off) (void)scr_tls_flush_held(t); (void)mbedtls_ssl_close_notify(&t->ssl); /* best effort; EAGAIN drops it */ } @@ -1227,9 +1668,18 @@ static void scr_tls_free(void *tctx) { scr_net_ls_drop(&t->secure_ls); free(t->hello); scr_secure_ctx_release(t->sni_ctx); + mbedtls_x509_crt_free(&t->served_chain); + if (t->server) scr_net_server_release(t->server); + free(t->held); free(t); } +/* The fd is writable again: the rest of what was held goes out. */ +static void scr_tls_drain(void *tctx) { + ScrTlsSock *t = (ScrTlsSock *)tctx; + if (!t->holding && t->held_len > t->held_off) (void)scr_tls_flush_held(t); +} + /* The 'secure' event: after a client's secureConnect listeners, as Node * emits it. */ static void scr_tls_after_established(void *tctx) { @@ -1247,6 +1697,10 @@ static void scr_tls_settle(void *tctx) { t->settled = true; scr_net_ls_drop(&t->secure_ls); scr_net_ls_drop(&t->session_ls); + if (t->hang_up_pending) { + t->hang_up_pending = false; + scr_tls_client_error(t, "socket hang up", "ECONNRESET"); + } } static const ScrNetTransportOps SCR_TLS_OPS = { @@ -1259,6 +1713,7 @@ static const ScrNetTransportOps SCR_TLS_OPS = { .free = scr_tls_free, .settle = scr_tls_settle, .after_established = scr_tls_after_established, + .drain = scr_tls_drain, }; /* ── the server surface ──────────────────────────────────────────────── */ @@ -1272,8 +1727,11 @@ static void scr_tls_srv_on_conn(void *ctx, ScrNetSocket *sock) { if (!t) scr_tls_oom(); mbedtls_ssl_init(&t->ssl); if (mbedtls_ssl_setup(&t->ssl, &srv->conf) != 0) scr_tls_oom(); + if (srv->request_cert) mbedtls_ssl_set_verify(&t->ssl, &scr_tls_vrfy_cb, t); t->sock = sock; t->srv = scr_tls_srv_retain(srv); + ScrNetServer *server = scr_net_sock_server(sock); + t->server = server != NULL ? scr_net_server_retain(server) : NULL; if (srv->sni_cb != NULL || srv->max_tls12) t->sni_state = SCR_SNI_COLLECT; /* pre-parse the hello */ mbedtls_ssl_set_bio(&t->ssl, t, scr_tls_bio_send, scr_tls_bio_recv, NULL); scr_net_sock_set_transport(sock, &SCR_TLS_OPS, t); @@ -1285,8 +1743,10 @@ static ScrNetServer *scr_tls_create_server_alpn(const char *cert, size_t cert_le ScrNetConnFn fn, char **alpn /*moves, nullable*/, int min_version, int max_version, ScrClosure *sni_cb /*moves, nullable*/, - void *sni_answer_fn) { + void *sni_answer_fn, + const ScrTlsClientAuth *auth /*nullable*/) { ScrTlsSrv *srv = scr_tls_srv_new(cert, cert_len, key, key_len, false); + scr_tls_srv_client_auth(srv, auth); if (sni_cb != NULL) { srv->sni_cb = sni_cb; /* moves; released with srv */ srv->sni_answer_fn = sni_answer_fn; @@ -1319,7 +1779,7 @@ ScrNetServer *scr_tls_create_server(const char *cert, size_t cert_len, const cha size_t key_len, ScrClosure *handler /*moves, nullable*/, ScrNetConnFn fn) { return scr_tls_create_server_alpn(cert, cert_len, key, key_len, handler, fn, NULL, 12, 13, NULL, - NULL); + NULL, NULL); } /* tls.createServer({ cert, key, SNICallback }): the pre-parse machinery @@ -1332,14 +1792,15 @@ ScrNetServer *scr_tls_create_server_sni(const char *cert, size_t cert_len, const ScrNetConnFn fn, ScrClosure *sni_cb /*moves, nullable*/, void *sni_answer_fn) { return scr_tls_create_server_alpn(cert, cert_len, key, key_len, handler, fn, NULL, 12, 13, sni_cb, - sni_answer_fn); + sni_answer_fn, NULL); } -ScrNetServer *scr_https_create_server(const char *cert, size_t cert_len, const char *key, - size_t key_len, ScrClosure *handler /*moves*/, - ScrHttpReqFn fn) { +static ScrNetServer *scr_https_create_server_auth(const char *cert, size_t cert_len, const char *key, + size_t key_len, ScrClosure *handler /*moves*/, + ScrHttpReqFn fn, const ScrTlsClientAuth *auth /*nullable*/) { ScrNetServer *s = scr_http_create_server(handler, fn); ScrTlsSrv *srv = scr_tls_srv_new(cert, cert_len, key, key_len, true /* ALPN http/1.1 */); + scr_tls_srv_client_auth(srv, auth); /* wrap the parser hook: TLS first, parser at establishment */ scr_net_server_get_native_conn(s, &srv->inner_conn, &srv->inner_ctx, &srv->inner_ctx_free); scr_net_server_set_native_conn(s, &scr_tls_srv_on_conn, srv, &scr_tls_srv_release_v); @@ -1347,6 +1808,12 @@ ScrNetServer *scr_https_create_server(const char *cert, size_t cert_len, const c return s; } +ScrNetServer *scr_https_create_server(const char *cert, size_t cert_len, const char *key, + size_t key_len, ScrClosure *handler /*moves*/, + ScrHttpReqFn fn) { + return scr_https_create_server_auth(cert, cert_len, key, key_len, handler, fn, NULL); +} + /* The SNI-callback form of the allowHTTP1 compatibility server: the same * handler-less https server, plus the pre-parse machinery (design note * atop the SNI section). A NULL sni_cb — the conditional spread's @@ -1432,8 +1899,8 @@ static ScrStr *scr_tls_pool_key(ScrStr *host, bool reject_unauthorized, const ch uint64_t h = 1469598103934665603ull; for (size_t i = 0; i < ca_len; i++) h = (h ^ (unsigned char)ca[i]) * 1099511628211ull; char buf[64]; - int n = snprintf(buf, sizeof buf, "%d:%zu:%016llx:", reject_unauthorized ? 1 : 0, ca_len, - (unsigned long long)h); + int n = snprintf(buf, sizeof buf, "%d:%s%zu:%016llx:", reject_unauthorized ? 1 : 0, + ca != NULL ? "" : "-", ca_len, (unsigned long long)h); ScrJsonBuf b; scr_jb_init(&b); scr_jb_write(&b, buf, (size_t)n); @@ -1505,12 +1972,22 @@ static int scr_tls_tunnel_bio_recv(void *ctx, unsigned char *buf, size_t len) { return scr_tls_tunnel_read((ScrTlsTunnel *)ctx, buf, len); } +/* A tunnel leg's handshake has finished. The tunnel runs no established + * callbacks before it writes the CONNECT request or the request itself, so + * the last flight goes out now instead of waiting for the immediate that + * ends a socket's hold (which does not see the tunnel's legs). */ +static void scr_tls_tunnel_end_hold(ScrTlsSock *t) { + t->holding = false; + if (t->held_len > t->held_off) (void)scr_tls_flush_held(t); +} + static int scr_tls_tunnel_handshake(void *tctx) { ScrTlsTunnel *tun = (ScrTlsTunnel *)tctx; ScrNetSocket *sock = tun->outer->sock; if (tun->stage == SCR_TUN_OUTER) { int r = scr_tls_handshake(tun->outer); if (r <= 0) return r; + scr_tls_tunnel_end_hold(tun->outer); tun->outer->established = true; tun->stage = SCR_TUN_SEND; } @@ -1579,6 +2056,7 @@ static int scr_tls_tunnel_handshake(void *tctx) { if (tun->stage == SCR_TUN_INNER) { int r = scr_tls_handshake(tun->inner); if (r <= 0) return r; + scr_tls_tunnel_end_hold(tun->inner); tun->stage = SCR_TUN_UP; } return 1; @@ -1625,12 +2103,16 @@ static void scr_tls_tunnel_free(void *tctx) { free(tun); } +/* The fd is writable again: the rest of what the outer leg held. */ +static void scr_tls_tunnel_drain(void *tctx) { scr_tls_drain(((ScrTlsTunnel *)tctx)->outer); } + static const ScrNetTransportOps SCR_TLS_TUNNEL_OPS = { .xread = scr_tls_tunnel_xread, .xwrite = scr_tls_tunnel_xwrite, .handshake = scr_tls_tunnel_handshake, .on_established = scr_tls_tunnel_on_established, .pending = scr_tls_tunnel_pending, + .drain = scr_tls_tunnel_drain, .shutdown_write = scr_tls_tunnel_shutdown_write, .free = scr_tls_tunnel_free, }; @@ -1676,7 +2158,7 @@ ScrHttpClientReq *scr_https_request(ScrStr *host /*borrowed*/, double port, ScrStr *path /*borrowed*/, ScrStr *method /*borrowed*/, double timeout_ms, ScrArr *header_pairs /*borrowed*/, bool auto_end, bool reject_unauthorized, - const char *ca /*borrowed, len 0 = none*/, size_t ca_len, + const char *ca /*borrowed, NULL = none*/, size_t ca_len, ScrClosure *cb /*moves, nullable*/, ScrHttpRespFn fn) { ScrStr *servername = scr_https_take_servername(); ScrTlsCli *cli = scr_tls_cli_for(host, reject_unauthorized, ca, ca_len, servername); @@ -1717,7 +2199,7 @@ ScrHttpClientReq *scr_https_request_agent(ScrStr *host /*borrowed*/, double port ScrStr *path /*borrowed*/, ScrStr *method /*borrowed*/, double timeout_ms, ScrArr *header_pairs /*borrowed*/, bool auto_end, bool reject_unauthorized, - const char *ca /*borrowed, len 0 = none*/, size_t ca_len, + const char *ca /*borrowed, NULL = none*/, size_t ca_len, const ScrDyn *agent /*borrowed*/, ScrClosure *cb /*moves, nullable*/, ScrHttpRespFn fn) { ScrStr *servername = scr_https_take_servername(); @@ -1750,8 +2232,8 @@ static void scr_tls_opt_fence(const char *api, const char *key, const char *hint scr_throw_error(SCR_ERR_ERROR, scr_jb_finish(&b)); } -/* JS truthiness over the dyn kinds — Node reads requestCert / - * rejectUnauthorized through `if (options.x)`. */ +/* JS truthiness over the dyn kinds — Node reads requestCert (and a + * standalone TLSSocket's rejectUnauthorized) through `if (options.x)`. */ static bool scr_tls_dyn_truthy(const ScrDyn *v) { switch (v->kind) { case SCR_DYN_UNDEF: @@ -1763,6 +2245,12 @@ static bool scr_tls_dyn_truthy(const ScrDyn *v) { } } +/* tls.Server and tls.connect read rejectUnauthorized as + * `options.rejectUnauthorized !== false`: null, 0 and "" keep it on. */ +static bool scr_tls_dyn_not_false(const ScrDyn *v) { + return v->kind != SCR_DYN_BOOL || v->v.b; +} + /* PEM material from a runtime option value: a string, a Buffer/Uint8Array, * or an array of those. `concat` (the client `ca` bundle) joins array * entries; cert/key arrays accept exactly one entry (one identity per @@ -1770,6 +2258,14 @@ static bool scr_tls_dyn_truthy(const ScrDyn *v) { * the exception pending. `what` names the fence ("a tls.createServer * 'cert' option"). */ static ScrBytes *scr_tls_pem_dyn(const ScrDyn *v, const char *what, bool concat) { + if (v->kind == SCR_DYN_TYPED_REF) { + /* a typed array (an array literal in a typed record): its entries */ + ScrDyn *plain = scr_dyn_typed_ref_materialize(v); + if (plain == NULL) return NULL; + ScrBytes *b = scr_tls_pem_dyn(plain, what, concat); + scr_dyn_release(plain); + return b; + } if (v->kind == SCR_DYN_STR) { ScrBytes *b = scr_bytes_new(SCR_BYTES_U8, (double)v->v.str->len); memcpy(b->data, v->v.str->data, v->v.str->len); @@ -1820,12 +2316,40 @@ static ScrBytes *scr_tls_pem_dyn(const ScrDyn *v, const char *what, bool concat) return NULL; } +/* A `ca` option from a runtime value: NULL for none (an empty string, + * which Node reads as no ca), empty bytes for an empty Buffer or array + * (an empty trust store), else the PEM bundle (+1). False with the + * exception pending. */ +static bool scr_tls_ca_dyn(const ScrDyn *v, const char *what, ScrBytes **out) { + *out = NULL; + ScrDyn *plain = v->kind == SCR_DYN_TYPED_REF ? scr_dyn_typed_ref_materialize(v) : NULL; + if (v->kind == SCR_DYN_TYPED_REF && plain == NULL) return false; + const ScrDyn *ca = plain != NULL ? plain : v; + bool ok = true; + if (ca->kind == SCR_DYN_ARR && ca->v.arr.len == 0) { + *out = scr_bytes_new(SCR_BYTES_U8, 0); + } else if (ca->kind != SCR_DYN_STR || ca->v.str->len > 0) { + *out = scr_tls_pem_dyn(ca, what, true); + ok = *out != NULL; + } + scr_dyn_release(plain); + return ok; +} + +/* A client's ca for scr_tls_cli_new2: NULL for none, and never NULL for a + * given ca, even an empty one. */ +static const char *scr_tls_ca_data(const ScrBytes *ca) { + if (ca == NULL) return NULL; + return ca->data != NULL ? (const char *)ca->data : ""; +} + /* https.request/get with a runtime options record: the http walk, then * the TLS options the literal https path lowers (rejectUnauthorized, ca, - * servername). Other tls.connect options fence when they carry a value. */ + * servername) and a client certificate (cert and key). Other tls.connect + * options fence when they carry a value. */ static const char *const SCR_HTTPS_REQ_FENCED_OPTIONS[] = { - "ALPNProtocols", "cert", "checkServerIdentity", "ciphers", "crl", "dhparam", "ecdhCurve", - "honorCipherOrder", "key", "maxVersion", "minVersion", "passphrase", "pfx", "secureContext", + "ALPNProtocols", "checkServerIdentity", "ciphers", "crl", "dhparam", "ecdhCurve", + "honorCipherOrder", "maxVersion", "minVersion", "passphrase", "pfx", "secureContext", "secureOptions", "secureProtocol", "session", "sessionIdContext", "sigalgs", NULL, }; @@ -1840,6 +2364,7 @@ ScrHttpClientReq *scr_https_request_dyn(const ScrDyn *opts /*borrowed*/, bool au const ScrDyn *rec = o.view != NULL ? o.view : opts; bool reject = true; ScrBytes *ca = NULL; + ScrBytes *own_cert = NULL, *own_key = NULL; ScrStr *servername = NULL; bool ok = true; if (rec != NULL && rec->kind == SCR_DYN_OBJ) { @@ -1848,7 +2373,7 @@ ScrHttpClientReq *scr_https_request_dyn(const ScrDyn *opts /*borrowed*/, bool au const ScrDyn *v = scr_dyn_obj_get(rec, key, strlen(key)); if (v != NULL && scr_tls_dyn_truthy(v)) { scr_http_req_opts_fence("https.request", key, - "rejectUnauthorized, ca, and servername are the supported TLS options"); + "rejectUnauthorized, ca, servername, cert, and key are the supported TLS options"); ok = false; } } @@ -1856,8 +2381,22 @@ ScrHttpClientReq *scr_https_request_dyn(const ScrDyn *opts /*borrowed*/, bool au if (r != NULL && r->kind == SCR_DYN_BOOL && !r->v.b) reject = false; const ScrDyn *c = ok ? scr_dyn_obj_get(rec, "ca", 2) : NULL; if (c != NULL && c->kind != SCR_DYN_UNDEF && c->kind != SCR_DYN_NULL) { - ca = scr_tls_pem_dyn(c, "an https.request 'ca' option", true); - if (ca == NULL) ok = false; + ok = scr_tls_ca_dyn(c, "an https.request 'ca' option", &ca); + } + const ScrDyn *oc = ok ? scr_dyn_obj_get(rec, "cert", 4) : NULL; + if (oc != NULL && oc->kind != SCR_DYN_UNDEF && oc->kind != SCR_DYN_NULL) { + own_cert = scr_tls_pem_dyn(oc, "an https.request 'cert' option", false); + if (own_cert == NULL) ok = false; + } + const ScrDyn *ok_ = ok ? scr_dyn_obj_get(rec, "key", 3) : NULL; + if (ok_ != NULL && ok_->kind != SCR_DYN_UNDEF && ok_->kind != SCR_DYN_NULL) { + own_key = scr_tls_pem_dyn(ok_, "an https.request 'key' option", false); + if (own_key == NULL) ok = false; + } + if (ok && (own_cert == NULL) != (own_key == NULL)) { + scr_http_req_opts_fence("https.request", own_cert != NULL ? "cert" : "key", + "a client certificate lowers with both cert and key"); + ok = false; } const ScrDyn *sn = ok ? scr_dyn_obj_get(rec, "servername", 10) : NULL; if (sn != NULL && sn->kind != SCR_DYN_UNDEF && sn->kind != SCR_DYN_NULL) { @@ -1871,14 +2410,32 @@ ScrHttpClientReq *scr_https_request_dyn(const ScrDyn *opts /*borrowed*/, bool au } if (!ok) { if (ca) scr_bytes_release(ca); + scr_bytes_release(own_cert); + scr_bytes_release(own_key); scr_http_req_opts_free(&o); if (cb) scr_closure_release(cb); return NULL; } - const char *ca_data = ca ? (const char *)ca->data : NULL; + const char *ca_data = scr_tls_ca_data(ca); size_t ca_len = ca ? ca->len : 0; ScrTlsCli *cli = scr_tls_cli_for(o.host, reject, ca_data, ca_len, servername); ScrStr *key = scr_tls_pool_key(o.host, reject, ca_data, ca_len, servername); + if (own_cert != NULL) { + /* Node's agent keys pooled sockets by the client certificate too */ + scr_tls_cli_set_own(cli, own_cert, own_key); + uint64_t h = 1469598103934665603ull; + for (size_t i = 0; i < own_cert->len; i++) h = (h ^ own_cert->data[i]) * 1099511628211ull; + for (size_t i = 0; i < own_key->len; i++) h = (h ^ own_key->data[i]) * 1099511628211ull; + char suffix[24]; + int n = snprintf(suffix, sizeof suffix, ":c%016llx", (unsigned long long)h); + ScrStr *tail = scr_str_new(suffix, (size_t)n); + ScrStr *keyed = scr_str_concat(key, tail); + scr_str_release(tail); + scr_str_release(key); + key = keyed; + scr_bytes_release(own_cert); + scr_bytes_release(own_key); + } scr_str_release(servername); ScrHttpClientReq *req = scr_http_req_opts_send(&o, auto_end, cb, fn, add_abort, &scr_tls_cli_wrap, cli, key); @@ -1890,13 +2447,17 @@ ScrHttpClientReq *scr_https_request_dyn(const ScrDyn *opts /*borrowed*/, bool au /* The literal path's runtime-valued cert/key extraction (the compiler's * tls.pemDyn libCall): `what` arrives precomposed. undefined throws — the - * literal walk required the member's presence. */ + * literal walk required the member's presence — and so does an empty + * string, which Node reads as no material while empty bytes here are an + * empty ca. */ ScrBytes *scr_tls_pem_from_dyn(const ScrDyn *v, const char *what) { - if (v->kind == SCR_DYN_UNDEF || v->kind == SCR_DYN_NULL) { + bool empty = v->kind == SCR_DYN_STR && v->v.str->len == 0; + if (v->kind == SCR_DYN_UNDEF || v->kind == SCR_DYN_NULL || empty) { ScrJsonBuf b; scr_jb_init(&b); scr_jb_puts(&b, what); - scr_jb_puts(&b, " holding undefined has no static lowering — pass PEM material (a string or a Buffer)"); + scr_jb_puts(&b, empty ? " holding an empty string" : " holding undefined"); + scr_jb_puts(&b, " has no static lowering — pass PEM material (a string or a Buffer)"); scr_throw_error(SCR_ERR_ERROR, scr_jb_finish(&b)); return NULL; } @@ -2014,9 +2575,22 @@ static bool scr_tls_opts_validate(const ScrDyn *opts) { /* `server_ok`: tls.createServer reads ALPNProtocols, minVersion and * maxVersion itself; the walk's other callers keep their fences. */ static bool scr_tls_srv_opts_walk_ex(const ScrDyn *opts, const char *api, ScrBytes **cert_out, - ScrBytes **key_out, bool server_ok) { + ScrBytes **key_out, bool server_ok, ScrTlsClientAuth *auth) { + if (opts != NULL && opts->kind == SCR_DYN_TYPED_REF) { + /* a typed record (an options binding): its fields, as a plain object */ + ScrDyn *plain = scr_dyn_typed_ref_materialize(opts); + if (plain == NULL) { + *cert_out = NULL; + *key_out = NULL; + return false; + } + bool ok = scr_tls_srv_opts_walk_ex(plain, api, cert_out, key_out, server_ok, auth); + scr_dyn_release(plain); + return ok; + } *cert_out = NULL; *key_out = NULL; + if (auth != NULL) *auth = (ScrTlsClientAuth){.request = false, .reject = true, .ca = NULL}; if (opts == NULL || opts->kind != SCR_DYN_OBJ) { scr_dyn_arg_type_fail("options", "of type object", opts ? opts : scr_dyn_undefined()); return false; @@ -2038,20 +2612,32 @@ static bool scr_tls_srv_opts_walk_ex(const ScrDyn *opts, const char *api, ScrByt continue; } if (strcmp(e->key, "ca") == 0) { - /* the server's `ca` verifies CLIENT certificates — inert without - * requestCert (which fences below), so Node-observably droppable */ + /* the server's `ca` verifies client certificates and completes the + * server's own chain (scr_tls_srv_client_auth); a walk without + * client certificates drops it */ + if (auth != NULL) { + char what[96]; + snprintf(what, sizeof what, "a %s 'ca' option", api); + scr_bytes_release(auth->ca); + if (!scr_tls_ca_dyn(v, what, &auth->ca)) ok = false; + } continue; } if (strcmp(e->key, "requestCert") == 0) { + if (auth != NULL) { + auth->request = scr_tls_dyn_truthy(v); + continue; + } if (scr_tls_dyn_truthy(v)) { scr_tls_opt_fence(api, "requestCert", - "client-certificate handshakes are not modeled (SEMANTICS.md divergence 55) — requestCert stays at Node's false default"); + "requestCert lowers on tls.createServer, https.createServer and new tls.TLSSocket"); ok = false; } continue; } if (strcmp(e->key, "rejectUnauthorized") == 0) { - continue; /* meaningful only with requestCert, which fences */ + if (auth != NULL) auth->reject = scr_tls_dyn_not_false(v); + continue; /* without client certificates it has no effect */ } if (server_ok && (strcmp(e->key, "ALPNProtocols") == 0 || strcmp(e->key, "minVersion") == 0 || strcmp(e->key, "maxVersion") == 0)) @@ -2085,13 +2671,17 @@ static bool scr_tls_srv_opts_walk_ex(const ScrDyn *opts, const char *api, ScrByt scr_bytes_release(*key_out); *cert_out = NULL; *key_out = NULL; + if (auth != NULL) { + scr_bytes_release(auth->ca); + auth->ca = NULL; + } } return ok; } bool scr_tls_srv_opts_walk(const ScrDyn *opts, const char *api, ScrBytes **cert_out, ScrBytes **key_out) { - return scr_tls_srv_opts_walk_ex(opts, api, cert_out, key_out, false); + return scr_tls_srv_opts_walk_ex(opts, api, cert_out, key_out, false, NULL); } /* createSecureContext over a RUNTIME options record: Node's typed @@ -2101,9 +2691,23 @@ bool scr_tls_srv_opts_walk(const ScrDyn *opts, const char *api, ScrBytes **cert_ * +1, or NULL with the exception pending. */ ScrSecureCtx *scr_tls_create_secure_context_dyn(const ScrDyn *opts /*borrowed*/) { ScrBytes *cert, *key; - if (!scr_tls_srv_opts_walk(opts, "tls.createSecureContext", &cert, &key)) return NULL; + /* requestCert and rejectUnauthorized are no context options: Node's + * createSecureContext ignores them */ + ScrTlsClientAuth auth; + if (!scr_tls_srv_opts_walk_ex(opts, "tls.createSecureContext", &cert, &key, false, &auth)) return NULL; ScrSecureCtx *c = scr_tls_create_secure_context((const char *)cert->data, cert->len, (const char *)key->data, key->len); + if (auth.ca != NULL) { + /* kept for sockets created from the context (a TLSSocket's + * secureContext); an SNI answer's chain comes from the server's + * context in OpenSSL, so the answer's ca is not used there */ + if (auth.ca->len > 0) { + int r = scr_tls_parse_crt(&c->ca, (const char *)auth.ca->data, auth.ca->len); + if (r != 0) scr_tls_bad_material("ca certificate", r); + } + c->has_ca = true; + scr_bytes_release(auth.ca); + } scr_bytes_release(cert); scr_bytes_release(key); return c; @@ -2142,7 +2746,8 @@ ScrNetServer *scr_tls_create_server_dyn(const ScrDyn *opts /*borrowed*/, return NULL; } ScrBytes *cert, *key; - if (!scr_tls_srv_opts_walk_ex(opts, "tls.createServer", &cert, &key, true)) { + ScrTlsClientAuth auth; + if (!scr_tls_srv_opts_walk_ex(opts, "tls.createServer", &cert, &key, true, &auth)) { scr_closure_release(handler); return NULL; } @@ -2153,12 +2758,14 @@ ScrNetServer *scr_tls_create_server_dyn(const ScrDyn *opts /*borrowed*/, if (protocols != NULL && !scr_tls_alpn_dyn(protocols, &alpn)) { scr_bytes_release(cert); scr_bytes_release(key); + scr_bytes_release(auth.ca); scr_closure_release(handler); return NULL; } ScrNetServer *s = scr_tls_create_server_alpn((const char *)cert->data, cert->len, (const char *)key->data, key->len, handler, fn, - alpn, min_version, max_version, NULL, NULL); + alpn, min_version, max_version, NULL, NULL, &auth); + scr_bytes_release(auth.ca); scr_bytes_release(cert); scr_bytes_release(key); return s; @@ -2189,18 +2796,148 @@ ScrNetServer *scr_https_create_server_dyn(const ScrDyn *opts /*borrowed*/, } } ScrBytes *cert, *key; - if (!scr_tls_srv_opts_walk(opts, "https.createServer", &cert, &key)) { + ScrTlsClientAuth auth; + if (!scr_tls_srv_opts_walk_ex(opts, "https.createServer", &cert, &key, false, &auth)) { scr_closure_release(handler); return NULL; } - ScrNetServer *s = scr_https_create_server((const char *)cert->data, cert->len, - (const char *)key->data, key->len, handler, fn); + ScrNetServer *s = scr_https_create_server_auth((const char *)cert->data, cert->len, + (const char *)key->data, key->len, handler, fn, &auth); + scr_bytes_release(auth.ca); if (has_timeout_buffer) scr_net_server_timeout_set(s, 4, timeout_buffer); scr_bytes_release(cert); scr_bytes_release(key); return s; } +/* ── server.addContext and server.setSecureContext ───────────────────── */ + +/* Node's addContext pattern: the name with '*' as [^.]* (any run within a + * label, empty too), matched without regard to case. */ +static bool scr_tls_name_match(const char *pattern, const unsigned char *name, size_t len) { + if (*pattern == '\0') return len == 0; + if (*pattern == '*') { + for (size_t k = 0;; k++) { + if (scr_tls_name_match(pattern + 1, name + k, len - k)) return true; + if (k == len || name[k] == '.') return false; + } + } + return len > 0 && (unsigned char)tolower(name[0]) == (unsigned char)*pattern && + scr_tls_name_match(pattern + 1, name + 1, len - 1); +} + +/* The certificate a server handshake serves when the server has contexts + * (mbedTLS's certificate callback, after the SNI name is read): the + * newest addContext entry whose pattern matches the name, else the + * setSecureContext default, else the server's own. An SNICallback answer + * (or a server with an SNICallback, whose contexts Node does not read) + * keeps its own path. OpenSSL completes the served chain from the store + * of the connection's context, the server's current default, and verifies + * client certificates against the served context's. */ +static int scr_tls_f_cert(mbedtls_ssl_context *ssl) { + ScrTlsSock *t = scr_tls_current; + if (t == NULL || t->srv == NULL || t->sni_ctx != NULL) return 0; + ScrTlsSrv *s = t->srv; + ScrSecureCtx *pick = NULL; + if (s->sni_cb == NULL) { + size_t len = 0; + const unsigned char *name = mbedtls_ssl_get_hs_sni(ssl, &len); + for (size_t i = s->ncontexts; name != NULL && i-- > 0;) { + if (scr_tls_name_match(s->contexts[i].pattern, name, len)) { + pick = s->contexts[i].ctx; + break; + } + } + } + if (pick == NULL) pick = s->default_ctx; + if (pick == NULL) return 0; + t->sni_ctx = scr_secure_ctx_retain(pick); + const mbedtls_x509_crt *store = s->default_ctx != NULL ? &s->default_ctx->ca : &s->ca; + mbedtls_x509_crt *own = &pick->cert; + if (store->raw.len > 0 && pick->cert.next == NULL) { + scr_tls_chained_copy(&t->served_chain, &pick->cert, store); + own = &t->served_chain; + } + if (s->request_cert) { + t->ctx_verifies = true; + mbedtls_ssl_set_hs_ca_chain(ssl, pick->has_ca ? &pick->ca : scr_tls_system_ca(), NULL); + } + return mbedtls_ssl_set_hs_own_cert(ssl, own, &pick->pk); +} + +/* The TLS config of a tls or https server, or NULL with the fence thrown. */ +static ScrTlsSrv *scr_tls_srv_of_server(ScrNetServer *server, const char *what) { + ScrNetNativeConnFn fn = NULL; + void *ctx = NULL; + void (*ctx_free)(void *) = NULL; + scr_net_server_get_native_conn(server, &fn, &ctx, &ctx_free); + if (fn == &scr_tls_srv_on_conn && ctx != NULL) return (ScrTlsSrv *)ctx; + char msg[160]; + int n = snprintf(msg, sizeof msg, "%s on a server that is not a tls or https server is not supported yet", what); + scr_throw_error_msg_code(SCR_ERR_ERROR, msg, (size_t)(n < (int)sizeof msg ? n : (int)sizeof msg - 1), "SC2020"); + return NULL; +} + +/* server.addContext(servername, context): `context` is a SecureContext + * (borrowed, nullable) or the options createSecureContext reads. Node + * builds a RegExp from the name, so a name with regular-expression groups + * or alternation throws the fence. */ +void scr_tls_server_add_context(ScrNetServer *server, const ScrDyn *servername, + const ScrDyn *options /*nullable*/, ScrSecureCtx *ctx /*nullable*/) { + ScrTlsSrv *s = scr_tls_srv_of_server(server, "server.addContext"); + if (s == NULL) return; + if (!scr_tls_dyn_truthy(servername)) { + static const char msg[] = "\"servername\" is required parameter for Server.addContext"; + scr_throw_error_msg_code(SCR_ERR_ERROR, msg, sizeof msg - 1, "ERR_TLS_REQUIRED_SERVER_NAME"); + return; + } + if (servername->kind != SCR_DYN_STR) { + scr_tls_opt_fence("server.addContext", "servername", "the servername lowers as a string"); + return; + } + const ScrStr *name = servername->v.str; + for (size_t i = 0; i < name->len; i++) { + if (name->data[i] == '(' || name->data[i] == ')' || name->data[i] == '|') { + scr_tls_opt_fence("server.addContext", "servername", + "a servername with regular-expression groups or alternation has no lowering"); + return; + } + /* the RegExp's case folding beyond ASCII differs from tolower's */ + if ((unsigned char)name->data[i] >= 0x80) { + scr_tls_opt_fence("server.addContext", "servername", "a servername outside ASCII has no lowering"); + return; + } + } + ScrSecureCtx *c = ctx != NULL ? scr_secure_ctx_retain(ctx) : scr_tls_create_secure_context_dyn(options); + if (c == NULL) return; + char *pattern = malloc(name->len + 1); + struct ScrTlsNamedCtx *grown = realloc(s->contexts, (s->ncontexts + 1) * sizeof *s->contexts); + if (pattern == NULL || grown == NULL) scr_tls_oom(); + for (size_t i = 0; i < name->len; i++) pattern[i] = (char)tolower((unsigned char)name->data[i]); + pattern[name->len] = '\0'; + s->contexts = grown; + s->contexts[s->ncontexts].pattern = pattern; + s->contexts[s->ncontexts].ctx = c; + s->ncontexts++; + mbedtls_ssl_conf_cert_cb(&s->conf, &scr_tls_f_cert); +} + +/* server.setSecureContext(options): new connections serve a context made + * from `options`; open ones keep theirs. */ +void scr_tls_server_set_secure_context(ScrNetServer *server, const ScrDyn *options) { + ScrTlsSrv *s = scr_tls_srv_of_server(server, "server.setSecureContext"); + if (s == NULL) return; + if (options == NULL || (options->kind != SCR_DYN_OBJ && options->kind != SCR_DYN_TYPED_REF)) { + scr_dyn_arg_type_fail("options", "of type object", options != NULL ? options : scr_dyn_undefined()); + return; + } + ScrSecureCtx *c = scr_tls_create_secure_context_dyn(options); + if (c == NULL) return; + scr_secure_ctx_release(s->default_ctx); + s->default_ctx = c; + mbedtls_ssl_conf_cert_cb(&s->conf, &scr_tls_f_cert); +} + /* ── new tls.TLSSocket(socket, options) ───────────────────────────────── */ /* The server-mode wrap: a fresh TLS socket takes over raw's connection @@ -2226,7 +2963,7 @@ static bool scr_tls_wrap_socket_opts_check(const ScrDyn *record, const char *api if (strcmp(e->key, "requestCert") == 0) { if (!scr_tls_dyn_truthy(e->value)) continue; scr_tls_opt_fence(api, "requestCert", - "client-certificate handshakes are not modeled (SEMANTICS.md divergence 55) — requestCert stays at Node's false default"); + "requestCert lowers with cert and key options, not beside a secureContext"); return false; } if (strcmp(e->key, "SNICallback") == 0 || strcmp(e->key, "ALPNCallback") == 0) { @@ -2271,13 +3008,17 @@ ScrNetSocket *scr_tls_wrap_socket(ScrNetSocket *raw, const ScrDyn *opts /*borrow /* Node serves a secureContext as it is: the record's cert, key and * other context options are not read then. The socket's own options * still apply, so the ones without a lowering fence either way: - * requestCert asks for a client certificate this runtime cannot - * verify. */ + * requestCert lowers with cert and key, whose ca the walk reads. */ int min_version = 12, max_version = 13; ScrBytes *cert = NULL, *key = NULL; + ScrTlsClientAuth auth = {.request = false, .reject = false, .ca = NULL}; if (ctx == NULL) { if (!scr_tls_versions_dyn(record, &min_version, &max_version)) return NULL; - if (!scr_tls_srv_opts_walk_ex(record, api, &cert, &key, true)) return NULL; + if (!scr_tls_srv_opts_walk_ex(record, api, &cert, &key, true, &auth)) return NULL; + /* a standalone socket requires a certificate only for a truthy + * rejectUnauthorized; tls.Server's true default does not apply */ + const ScrDyn *reject = scr_dyn_obj_get(record, "rejectUnauthorized", 18); + auth.reject = reject != NULL && scr_tls_dyn_truthy(reject); } else if (!scr_tls_wrap_socket_opts_check(record, api)) { return NULL; } @@ -2286,6 +3027,7 @@ ScrNetSocket *scr_tls_wrap_socket(ScrNetSocket *raw, const ScrDyn *opts /*borrow if (protocols != NULL && protocols->kind != SCR_DYN_UNDEF && !scr_tls_alpn_dyn(protocols, &alpn)) { scr_bytes_release(cert); scr_bytes_release(key); + scr_bytes_release(auth.ca); return NULL; } ScrTlsSrv *srv = ctx != NULL ? scr_tls_srv_from_ctx(ctx) @@ -2293,6 +3035,9 @@ ScrNetSocket *scr_tls_wrap_socket(ScrNetSocket *raw, const ScrDyn *opts /*borrow (const char *)key->data, key->len, false); scr_bytes_release(cert); scr_bytes_release(key); + srv->standalone = true; + scr_tls_srv_client_auth(srv, ctx == NULL ? &auth : NULL); + scr_bytes_release(auth.ca); srv->alpn = alpn; /* moves */ if (alpn != NULL) mbedtls_ssl_conf_alpn_protocols(&srv->conf, (const char **)srv->alpn); if (!scr_tls_conf_versions(&srv->conf, min_version, max_version)) { @@ -2314,6 +3059,7 @@ ScrNetSocket *scr_tls_wrap_socket(ScrNetSocket *raw, const ScrDyn *opts /*borrow if (!ts) scr_tls_oom(); mbedtls_ssl_init(&ts->ssl); if (mbedtls_ssl_setup(&ts->ssl, &srv->conf) != 0) scr_tls_oom(); + if (srv->request_cert) mbedtls_ssl_set_verify(&ts->ssl, &scr_tls_vrfy_cb, ts); ts->sock = t; ts->srv = srv; /* moves */ ts->wrapped = true; @@ -2468,10 +3214,10 @@ static bool scr_tls_alpn_dyn(const ScrDyn *v, char ***out) { * throws (the server walk's stance). */ static const char *const SCR_TLS_CONNECT_FENCED_OPTIONS[] = { "allowHalfOpen", "autoSelectFamily", - "autoSelectFamilyAttemptTimeout", "cert", "certificateCompression", "checkServerIdentity", + "autoSelectFamilyAttemptTimeout", "certificateCompression", "checkServerIdentity", "ciphers", "clientCertEngine", "crl", "dhparam", "ecdhCurve", "enableTrace", "family", "fd", "hints", "highWaterMark", - "honorCipherOrder", "keepAlive", "keepAliveInitialDelay", "key", + "honorCipherOrder", "keepAlive", "keepAliveInitialDelay", "localAddress", "localPort", "lookup", "minDHSize", "noDelay", "onread", "passphrase", "path", "pfx", "privateKeyEngine", "privateKeyIdentifier", "pskCallback", "readable", "secureContext", "secureOptions", "secureProtocol", "session", @@ -2479,12 +3225,32 @@ static const char *const SCR_TLS_CONNECT_FENCED_OPTIONS[] = { "writable", NULL, }; +static ScrNetSocket *scr_tls_connect_plain(double port, ScrStr *host /*borrowed, nullable*/, + const ScrDyn *opts /*borrowed, nullable*/, + ScrClosure *cb /*moves, nullable*/); + ScrNetSocket *scr_tls_connect_dyn(double port, ScrStr *host /*borrowed, nullable*/, const ScrDyn *opts /*borrowed, nullable*/, ScrClosure *cb /*moves, nullable*/) { + if (opts == NULL || opts->kind != SCR_DYN_TYPED_REF) return scr_tls_connect_plain(port, host, opts, cb); + /* a typed record (an options binding): its fields, as a plain object */ + ScrDyn *plain = scr_dyn_typed_ref_materialize(opts); + if (plain == NULL) { + scr_closure_release(cb); + return NULL; + } + ScrNetSocket *s = scr_tls_connect_plain(port, host, plain, cb); + scr_dyn_release(plain); + return s; +} + +static ScrNetSocket *scr_tls_connect_plain(double port, ScrStr *host /*borrowed, nullable*/, + const ScrDyn *opts /*borrowed, nullable*/, + ScrClosure *cb /*moves, nullable*/) { scr_tls_rng_init(); bool reject = true; ScrBytes *ca = NULL; + ScrBytes *own_cert = NULL, *own_key = NULL; /* the client certificate */ const ScrStr *servername = NULL; const ScrStr *opt_host = NULL; char **alpn = NULL; @@ -2540,13 +3306,20 @@ ScrNetSocket *scr_tls_connect_dyn(double port, ScrStr *host /*borrowed, nullable continue; } if (strcmp(e->key, "rejectUnauthorized") == 0) { - reject = scr_tls_dyn_truthy(v); + reject = scr_tls_dyn_not_false(v); continue; } if (strcmp(e->key, "ca") == 0) { scr_bytes_release(ca); - ca = scr_tls_pem_dyn(v, "a tls.connect 'ca' option", true); - if (ca == NULL) ok = false; + if (!scr_tls_ca_dyn(v, "a tls.connect 'ca' option", &ca)) ok = false; + continue; + } + if (strcmp(e->key, "cert") == 0 || strcmp(e->key, "key") == 0) { + ScrBytes **slot = e->key[0] == 'c' ? &own_cert : &own_key; + scr_bytes_release(*slot); + *slot = scr_tls_pem_dyn(v, e->key[0] == 'c' ? "a tls.connect 'cert' option" : "a tls.connect 'key' option", + false); + if (*slot == NULL) ok = false; continue; } if (strcmp(e->key, "ALPNProtocols") == 0) { @@ -2607,6 +3380,11 @@ ScrNetSocket *scr_tls_connect_dyn(double port, ScrStr *host /*borrowed, nullable timeout_ms = timeout_value->v.num; } } + if (ok && (own_cert == NULL) != (own_key == NULL)) { + scr_tls_opt_fence("tls.connect", own_cert != NULL ? "cert" : "key", + "a client certificate lowers with both cert and key"); + ok = false; + } if (ok && port < 0 && wrap == NULL) { scr_dyn_arg_type_fail("options.port", "of type number", scr_dyn_undefined()); ok = false; @@ -2616,6 +3394,8 @@ ScrNetSocket *scr_tls_connect_dyn(double port, ScrStr *host /*borrowed, nullable if (!ok) { scr_tls_alpn_free(alpn); scr_bytes_release(ca); + scr_bytes_release(own_cert); + scr_bytes_release(own_key); scr_closure_release(cb); return NULL; } @@ -2639,6 +3419,8 @@ ScrNetSocket *scr_tls_connect_dyn(double port, ScrStr *host /*borrowed, nullable if (s == NULL) { scr_tls_alpn_free(alpn); scr_bytes_release(ca); + scr_bytes_release(own_cert); + scr_bytes_release(own_key); scr_closure_release(cb); scr_str_release(localhost_fallback); scr_str_release(socket_host); @@ -2653,9 +3435,11 @@ ScrNetSocket *scr_tls_connect_dyn(double port, ScrStr *host /*borrowed, nullable if (dial_addr) scr_str_release(dial_addr); } ScrTlsCli *cli = scr_tls_cli_new2(verify_host, reject, - ca != NULL ? (const char *)ca->data : NULL, - ca != NULL ? ca->len : 0, !reject); + scr_tls_ca_data(ca), ca != NULL ? ca->len : 0, !reject); if (servername != NULL) scr_tls_cli_set_servername(cli, servername); + if (own_cert != NULL) scr_tls_cli_set_own(cli, own_cert, own_key); + scr_bytes_release(own_cert); + scr_bytes_release(own_key); if (alpn != NULL) { cli->alpn = alpn; /* moves */ mbedtls_ssl_conf_alpn_protocols(&cli->conf, (const char **)cli->alpn); @@ -2705,7 +3489,7 @@ void scr_tls_server_wrap_h2(ScrNetServer *s, const char *cert, size_t cert_len, /* The h2-over-TLS CLIENT wrap (http2.connect with an https authority): * a client transport offering ALPN ["h2"] — SNI/verification against - * the authority host; `ca` replaces the trust anchors ("" = none) and + * the authority host; `ca` replaces the trust anchors (NULL = none) and * reject_unauthorized false records like tls.connect's. The session's * preface/SETTINGS ride the socket's write buffer until establishment. */ void scr_tls_h2_client_wrap(ScrNetSocket *sock, ScrStr *host /*borrowed*/, @@ -2731,6 +3515,20 @@ static const char *scr_tls_auth_code(ScrTlsSock *t) { return "UNABLE_TO_VERIFY_LEAF_SIGNATURE"; } +/* A server's authorizationError for the client certificate. OpenSSL + * keeps the last error its verification met: the validity period is + * checked last, the purpose (clientAuth) before it, the chain first. A + * client that sent none is UNABLE_TO_GET_ISSUER_CERT, as Node reports it. */ +static const char *scr_tls_client_cert_code(ScrTlsSock *t) { + uint32_t vr = mbedtls_ssl_get_verify_result(&t->ssl); + if (vr & MBEDTLS_X509_BADCERT_MISSING) return "UNABLE_TO_GET_ISSUER_CERT"; + if (t->vrfy_expired) return "CERT_HAS_EXPIRED"; + if (vr & (MBEDTLS_X509_BADCERT_EXT_KEY_USAGE | MBEDTLS_X509_BADCERT_KEY_USAGE)) return "INVALID_PURPOSE"; + if (t->vrfy_leaf_self) return "DEPTH_ZERO_SELF_SIGNED_CERT"; + if (t->vrfy_chain_self) return "SELF_SIGNED_CERT_IN_CHAIN"; + return "UNABLE_TO_VERIFY_LEAF_SIGNATURE"; +} + static ScrTlsSock *scr_tls_sock_of(ScrNetSocket *s) { return (ScrTlsSock *)scr_net_sock_transport_ctx_for(s, &SCR_TLS_OPS); } @@ -2775,22 +3573,107 @@ static void scr_tls_fire_session(ScrTlsSock *t) { /* The STATIC-lane TLSSocket members (the lowered socket surface). */ +/* Whether the socket verified its peer: a client against its trust + * anchors, a server that requested a client certificate against its. */ +static bool scr_tls_sock_verifies(const ScrTlsSock *t) { + return t->established && ((t->cli != NULL && t->cli->verify_recorded) || + (t->srv != NULL && t->srv->request_cert && !t->srv->standalone)); +} + bool scr_tls_sock_authorized(ScrNetSocket *s) { ScrTlsSock *t = scr_tls_sock_of(s); - return t != NULL && t->established && t->cli != NULL && t->cli->verify_recorded && - mbedtls_ssl_get_verify_result(&t->ssl) == 0; + return t != NULL && scr_tls_sock_verifies(t) && mbedtls_ssl_get_verify_result(&t->ssl) == 0; } ScrStr *scr_tls_sock_auth_error(ScrNetSocket *s) { ScrTlsSock *t = scr_tls_sock_of(s); - if (t == NULL || !t->established || t->cli == NULL || !t->cli->verify_recorded || - mbedtls_ssl_get_verify_result(&t->ssl) == 0) { + if (t == NULL || !scr_tls_sock_verifies(t) || mbedtls_ssl_get_verify_result(&t->ssl) == 0) { return NULL; /* the null arm — authorized, or never verified */ } - const char *code = scr_tls_auth_code(t); + const char *code = t->srv != NULL ? scr_tls_client_cert_code(t) : scr_tls_auth_code(t); return scr_str_new(code, strlen(code)); } +/* ── certificates for the X509Certificate accessors ────────────────── + * + * getPeerCertificate, getCertificate and their X509Certificate forms read + * the DER below (scr_tls_x509.c builds the objects, so this unit needs no + * X.509 code). Node answers from the socket's handle, which a destroyed + * socket no longer has. */ + +bool scr_tls_sock_has_handle(ScrNetSocket *s) { + return scr_tls_sock_of(s) != NULL && !scr_net_sock_destroyed(s); +} + +/* The certificates the peer sent, leaf first (borrowed DER), up to `max`; + * none before the handshake. Answers the count. */ +size_t scr_tls_sock_peer_chain(ScrNetSocket *s, const uint8_t **der, size_t *len, size_t max) { + ScrTlsSock *t = scr_tls_sock_of(s); + if (t == NULL || !t->established) return 0; + size_t n = 0; + for (const mbedtls_x509_crt *c = mbedtls_ssl_get_peer_cert(&t->ssl); c != NULL && c->raw.len > 0 && n < max; + c = c->next) { + der[n] = c->raw.p; + len[n] = c->raw.len; + n++; + } + return n; +} + +/* The certificate the socket serves (borrowed DER), or NULL: a server's + * SNI answer, secureContext or own certificate. Clients send none. */ +const uint8_t *scr_tls_sock_own_cert(ScrNetSocket *s, size_t *len) { + ScrTlsSock *t = scr_tls_sock_of(s); + if (t != NULL && t->cli != NULL && t->cli->has_own) { + *len = t->cli->own.raw.len; + return t->cli->own.raw.p; + } + if (t == NULL || t->srv == NULL) return NULL; + const mbedtls_x509_crt *c = t->sni_ctx != NULL ? &t->sni_ctx->cert + : t->srv->ctx != NULL ? &t->srv->ctx->cert + : &t->srv->cert; + if (c->raw.len == 0) return NULL; + *len = c->raw.len; + return c->raw.p; +} + +/* Visits the trust anchors the socket verified its peer against (its + * `ca`, or the default store) until `visit` answers true. */ +void scr_tls_sock_each_trusted(ScrNetSocket *s, bool (*visit)(void *ctx, const uint8_t *der, size_t len), + void *ctx) { + ScrTlsSock *t = scr_tls_sock_of(s); + if (t == NULL || (t->cli == NULL && t->srv == NULL)) return; + const mbedtls_x509_crt *c = t->cli != NULL ? (t->cli->has_ca ? &t->cli->ca : scr_tls_system_ca()) + : t->ctx_verifies ? (t->sni_ctx->has_ca ? &t->sni_ctx->ca : scr_tls_system_ca()) + : t->srv->has_ca ? &t->srv->ca + : scr_tls_system_ca(); + for (; c != NULL && c->raw.len > 0; c = c->next) + if (visit(ctx, c->raw.p, c->raw.len)) return; +} + +static ScrBytes *scr_tls_der_buffer(const uint8_t *der, size_t len) { + ScrBytes *b = scr_bytes_new(SCR_BYTES_U8, (double)len); + b->is_buffer = true; + if (len) memcpy(b->data, der, len); + return b; +} + +/* getPeerX509Certificate() and getX509Certificate(): the certificate's + * DER (+1 Buffer), empty when there is none (undefined in Node). */ +ScrBytes *scr_tls_sock_peer_cert_der(ScrNetSocket *s) { + const uint8_t *der; + size_t len; + if (!scr_tls_sock_has_handle(s) || scr_tls_sock_peer_chain(s, &der, &len, 1) == 0) + return scr_tls_der_buffer(NULL, 0); + return scr_tls_der_buffer(der, len); +} + +ScrBytes *scr_tls_sock_own_cert_der(ScrNetSocket *s) { + size_t len = 0; + const uint8_t *der = scr_tls_sock_has_handle(s) ? scr_tls_sock_own_cert(s, &len) : NULL; + return scr_tls_der_buffer(der, der ? len : 0); +} + /* tlsSocket.getProtocol(): the negotiated version ("TLSv1.2", * "TLSv1.3"); before the handshake the highest version on offer, as * OpenSSL answers, and null after close. +1. */ diff --git a/packages/runtime/src/scr_tls.h b/packages/runtime/src/scr_tls.h index cc8e3336e..8e044f12d 100644 --- a/packages/runtime/src/scr_tls.h +++ b/packages/runtime/src/scr_tls.h @@ -58,6 +58,19 @@ ScrNetSocket *scr_tls_connect_dyn(double port, ScrStr *host /*borrowed, nullable bool scr_tls_sock_authorized(ScrNetSocket *s); ScrStr *scr_tls_sock_auth_error(ScrNetSocket *s); /* +1 or NULL */ ScrDyn *scr_tls_sock_get_protocol(ScrNetSocket *s); /* +1: string or null */ +/* The certificates behind getPeerCertificate, getCertificate and their + * X509Certificate forms (scr_tls_x509.c reads them). */ +bool scr_tls_sock_has_handle(ScrNetSocket *s); +size_t scr_tls_sock_peer_chain(ScrNetSocket *s, const uint8_t **der, size_t *len, size_t max); +const uint8_t *scr_tls_sock_own_cert(ScrNetSocket *s, size_t *len); +void scr_tls_sock_each_trusted(ScrNetSocket *s, bool (*visit)(void *ctx, const uint8_t *der, size_t len), + void *ctx); +ScrBytes *scr_tls_sock_peer_cert_der(ScrNetSocket *s); /* +1 Buffer, empty for none */ +ScrBytes *scr_tls_sock_own_cert_der(ScrNetSocket *s); /* +1 Buffer, empty for none */ +/* scr_tls_x509.c: getPeerCertificate([detailed]) and getCertificate(), +1 + * (an object, {} or null). */ +ScrDyn *scr_tls_sock_get_peer_certificate(ScrNetSocket *s, const ScrDyn *detailed); +ScrDyn *scr_tls_sock_get_certificate(ScrNetSocket *s); ScrDyn *scr_tls_sock_get_cipher(ScrNetSocket *s); /* +1: object, undefined or null */ ScrDyn *scr_tls_sock_alpn_protocol(ScrNetSocket *s); /* +1: string, false or null */ void scr_tls_sock_on_secure_connect(ScrNetSocket *s, ScrClosure *cb /*moves*/, bool once); @@ -72,7 +85,7 @@ void scr_tls_sock_on_secure(ScrNetSocket *s, ScrClosure *cb /*moves*/, bool once * every http2-using binary links the TLS unit (the moduleUsesTls * switch), so no registration indirection is needed. */ void scr_tls_server_wrap_h2(ScrNetServer *s, const char *cert, size_t cert_len, const char *key, size_t key_len, ScrNetNativeConnFn h2_conn, void *h2_ctx /*moves*/, void (*h2_ctx_free)(void *), ScrClosure *sni_cb /*moves, nullable*/, void *sni_answer_fn); -void scr_tls_h2_client_wrap(ScrNetSocket *sock, ScrStr *host /*borrowed*/, bool reject_unauthorized, const char *ca /*borrowed, len 0 = none*/, size_t ca_len); +void scr_tls_h2_client_wrap(ScrNetSocket *sock, ScrStr *host /*borrowed*/, bool reject_unauthorized, const char *ca /*borrowed, NULL = none*/, size_t ca_len); /* ── tls.SecureContext + the SNI callback (scr_tls.c) ───────────────── * A SecureContext is an opaque refcounted parsed cert/key pair @@ -97,6 +110,12 @@ ScrSecureCtx *scr_tls_create_secure_context(const char *cert, size_t cert_len, c * validations first, then the pem walk (+1, or NULL with the exception * pending). Borrowed. */ ScrSecureCtx *scr_tls_create_secure_context_dyn(const ScrDyn *opts); +/* server.addContext(servername, context) (a SecureContext, or options when + * NULL) and server.setSecureContext(options) on a tls or https server; + * Node's errors pending on failure. */ +void scr_tls_server_add_context(ScrNetServer *server, const ScrDyn *servername, + const ScrDyn *options /*nullable*/, ScrSecureCtx *ctx /*nullable*/); +void scr_tls_server_set_secure_context(ScrNetServer *server, const ScrDyn *options); /* new tls.TLSSocket(socket, options) in server mode: a fresh TLS socket * takes over socket's connection and runs the server handshake, serving * ctx (options.secureContext; borrowed, nullable) or the options record's diff --git a/packages/runtime/src/scr_tls_x509.c b/packages/runtime/src/scr_tls_x509.c new file mode 100644 index 000000000..d1dfd819b --- /dev/null +++ b/packages/runtime/src/scr_tls_x509.c @@ -0,0 +1,119 @@ +/* tlsSocket.getPeerCertificate([detailed]) and getCertificate(): Node's + * legacy certificate objects, built by scr_x509.c's toLegacyObject over + * the certificates scr_tls.c keeps. Linked into programs that use both + * units, so neither unit depends on the other. */ +#include "scr_runtime.h" + +#include +#include + +/* The peer chain entries read; a longer chain is cut, as no real chain is + * this long. */ +enum { SCR_TLS_X509_MAX_CHAIN = 16 }; + +static ScrBytes *scr_tls_x509_bytes(const uint8_t *der, size_t len) { + ScrBytes *b = scr_bytes_new(SCR_BYTES_U8, (double)len); + if (len) memcpy(b->data, der, len); + return b; +} + +static bool scr_tls_x509_same(const ScrBytes *a, const ScrBytes *b) { + return a->len == b->len && memcmp(a->data, b->data, a->len) == 0; +} + +/* tlsSocket.getCertificate(): the certificate the socket serves, {} + * without one, null once the socket is destroyed. +1. */ +ScrDyn *scr_tls_sock_get_certificate(ScrNetSocket *s) { + if (!scr_tls_sock_has_handle(s)) return scr_dyn_new_null(); + size_t len = 0; + const uint8_t *der = scr_tls_sock_own_cert(s, &len); + if (der == NULL) return scr_dyn_new_obj(); + ScrBytes *cert = scr_tls_x509_bytes(der, len); + ScrDyn *result = scr_crypto_x509_legacy_object(cert); + scr_bytes_release(cert); + return result; +} + +typedef struct { + ScrBytes *subject; /* borrowed */ + ScrBytes *found; /* +1, NULL until found */ +} ScrTlsX509Lookup; + +/* SSL_CTX_get_issuer: the first trust anchor that issued the subject. */ +static bool scr_tls_x509_issuer_visit(void *ctx, const uint8_t *der, size_t len) { + ScrTlsX509Lookup *lookup = ctx; + ScrBytes *candidate = scr_tls_x509_bytes(der, len); + if (scr_crypto_x509_check_issued(lookup->subject, candidate)) { + lookup->found = candidate; + return true; + } + scr_bytes_release(candidate); + return false; +} + +/* Sets `chain`'s issuerCertificate to the legacy object of `issuer` + * (borrowed) and answers that object (borrowed; `chain` holds it). */ +static ScrDyn *scr_tls_x509_link(ScrDyn *chain, ScrBytes *issuer) { + ScrDyn *object = scr_crypto_x509_legacy_object(issuer); + scr_dyn_obj_set(chain, "issuerCertificate", 17, object); + return object; +} + +/* tlsSocket.getPeerCertificate([detailed]): the peer's certificate, {} + * before the handshake or without one, null once the socket is destroyed. + * With `detailed`, issuerCertificate links each certificate to its issuer + * as Node's GetPeerCert does: first from the certificates the peer sent, + * in their order of issue, then from the trust anchors; a self-issued + * last certificate is its own issuerCertificate. +1. */ +ScrDyn *scr_tls_sock_get_peer_certificate(ScrNetSocket *s, const ScrDyn *detailed_arg) { + /* Node reads the argument with IsTrue(): only true itself. */ + bool detailed = detailed_arg->kind == SCR_DYN_BOOL && detailed_arg->v.b; + if (!scr_tls_sock_has_handle(s)) return scr_dyn_new_null(); + const uint8_t *der[SCR_TLS_X509_MAX_CHAIN]; + size_t len[SCR_TLS_X509_MAX_CHAIN]; + size_t n = scr_tls_sock_peer_chain(s, der, len, SCR_TLS_X509_MAX_CHAIN); + if (n == 0) return scr_dyn_new_obj(); + ScrBytes *cert = scr_tls_x509_bytes(der[0], len[0]); + ScrDyn *result = scr_crypto_x509_legacy_object(cert); + if (!detailed) { + scr_bytes_release(cert); + return result; + } + ScrBytes *pool[SCR_TLS_X509_MAX_CHAIN]; + size_t pooled = 0; + for (size_t i = 1; i < n; i++) pool[pooled++] = scr_tls_x509_bytes(der[i], len[i]); + ScrDyn *chain = result; + /* AddIssuerChainToObject: take the issuer out of the peer's list, and + * stop when none issued the current certificate or the issuer taken was + * the list's last entry. */ + for (;;) { + size_t i; + for (i = 0; i < pooled; i++) { + if (!scr_crypto_x509_check_issued(cert, pool[i])) continue; + chain = scr_tls_x509_link(chain, pool[i]); + scr_bytes_release(cert); + cert = pool[i]; + memmove(pool + i, pool + i + 1, (pooled - i - 1) * sizeof pool[0]); + pooled--; + break; + } + if (i == pooled) break; + } + for (size_t i = 0; i < pooled; i++) scr_bytes_release(pool[i]); + /* GetLastIssuedCert: then the trust anchors, until a self-issued one + * (bounded, for anchors that issued each other). */ + for (size_t step = 0; step < SCR_TLS_X509_MAX_CHAIN && !scr_crypto_x509_check_issued(cert, cert); step++) { + ScrTlsX509Lookup lookup = {cert, NULL}; + scr_tls_sock_each_trusted(s, scr_tls_x509_issuer_visit, &lookup); + if (lookup.found == NULL) break; + chain = scr_tls_x509_link(chain, lookup.found); + bool same = scr_tls_x509_same(cert, lookup.found); + scr_bytes_release(cert); + cert = lookup.found; + if (same) break; + } + if (scr_crypto_x509_check_issued(cert, cert)) + scr_dyn_obj_set(chain, "issuerCertificate", 17, scr_dyn_retain(chain)); + scr_bytes_release(cert); + return result; +} diff --git a/packages/runtime/src/scr_type_origin.c b/packages/runtime/src/scr_type_origin.c new file mode 100644 index 000000000..6ea872304 --- /dev/null +++ b/packages/runtime/src/scr_type_origin.c @@ -0,0 +1,222 @@ +/* Declared-type origins on checked-conversion TypeErrors, linked only by + * programs that check values against types an npm package declares + * (runtime feature typeOrigins). + * + * A checked conversion into a type that comes from a package's + * declarations (its .d.ts, @types package or JSDoc) throws the usual + * `expected at , got ` TypeError when the value does + * not fit. The compiler passes the declaration's origin to the conversion + * site's cold exception path, which calls scr_type_origin_note: the + * pending TypeError's message gains two lines naming the declaration and + * the type patch (package.json scriptc.typePatches) that would make the + * declaration admit the value, as the `scriptc types patch` command that + * writes it. The hot path is unchanged, and so is every + * other error. + * + * The origin is one string of fields separated by \x1f: package, version, + * suggested range (which the command chooses itself), the selector of the + * declared slot, where it is declared, and the declared type. */ +#include "scr_runtime.h" + +#include +#include + +typedef struct { + char *data; + size_t len, cap; +} ScrOriginBuf; + +static void scr_origin_put(ScrOriginBuf *b, const char *s, size_t n) { + if (b->len + n + 1 > b->cap) { + size_t cap = b->cap ? b->cap : 256; + while (b->len + n + 1 > cap) cap *= 2; + char *grown = realloc(b->data, cap); + if (!grown) scr_trap("scriptc: out of memory\n"); + b->data = grown; + b->cap = cap; + } + memcpy(b->data + b->len, s, n); + b->len += n; + b->data[b->len] = 0; +} + +static void scr_origin_puts(ScrOriginBuf *b, const char *s) { scr_origin_put(b, s, strlen(s)); } + +/* One argument of the suggested `scriptc types patch` command, quoted as + * the shell passes it as written (decl-origin.ts's shellArgument quotes the + * same way): bare when it has only characters no shell treats specially; + * otherwise, for a POSIX shell, in single quotes with each ' as '\'', and + * on Windows (cmd and PowerShell), in double quotes when it has none of the + * characters either shell expands, escapes or ends a string with there. + * Package names, selectors and types come from package declarations. + * Returns false, writing nothing, when no quoting is inert. */ +static bool scr_origin_bare(const char *s, size_t n) { + if (n == 0) return false; + for (size_t i = 0; i < n; i++) { + unsigned char c = (unsigned char)s[i]; + bool plain = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || + strchr("_@+=:,./-", c) != NULL; +#ifndef _WIN32 + plain = plain || c == '%'; +#endif + if (!plain || c == 0) return false; + } + return true; +} + +static bool scr_origin_quotable(const char *s, size_t n) { +#ifdef _WIN32 + for (size_t i = 0; i < n; i++) { + unsigned char c = (unsigned char)s[i]; + if (c < 0x20 || c == 0x7f || (c != 0 && strchr("\"%!^$`", c) != NULL)) return false; + /* PowerShell also takes the typographic quotes U+2018-U+201F (UTF-8 + * E2 80 98..9F) as quotes. */ + if (c == 0xe2 && i + 2 < n && (unsigned char)s[i + 1] == 0x80 && + (unsigned char)s[i + 2] >= 0x98 && (unsigned char)s[i + 2] <= 0x9f) + return false; + } +#else + (void)s; + (void)n; +#endif + return true; +} + +static void scr_origin_put_arg(ScrOriginBuf *b, const char *s, size_t n) { + if (scr_origin_bare(s, n)) { + scr_origin_put(b, s, n); + return; + } +#ifdef _WIN32 + scr_origin_puts(b, "\""); + scr_origin_put(b, s, n); + scr_origin_puts(b, "\""); +#else + scr_origin_puts(b, "'"); + for (size_t i = 0; i < n; i++) { + if (s[i] == '\'') scr_origin_puts(b, "'\\''"); + else scr_origin_put(b, s + i, 1); + } + scr_origin_puts(b, "'"); +#endif +} + +/* Field `index` of the origin string (not terminated: [*out, *out+len)). */ +static size_t scr_origin_field(const char *origin, int index, const char **out) { + const char *p = origin; + for (int i = 0; i < index; i++) { + const char *sep = strchr(p, '\x1f'); + if (!sep) { + *out = ""; + return 0; + } + p = sep + 1; + } + const char *end = strchr(p, '\x1f'); + *out = p; + return end ? (size_t)(end - p) : strlen(p); +} + +/* The TypeScript type a `got` kind of the check message suggests adding. */ +static const char *scr_origin_arm(const char *kind, size_t len) { + if (len == 5 && memcmp(kind, "array", 5) == 0) return "unknown[]"; + if (len == 8 && memcmp(kind, "function", 8) == 0) return "Function"; + return NULL; /* the kind itself (undefined, null, string, number, ...) */ +} + +void scr_type_origin_note(const char *origin) { + ScrExcCell *cell = scr_exc_current_cell(); + if (cell->kind != SCR_EXC_OBJ || cell->payload == NULL) return; + ScrError *e = (ScrError *)cell->payload; + if (e->vt != &scr_error_vts[SCR_ERR_TYPE] || e->message == NULL) return; + const char *msg = e->message->data; + size_t mlen = e->message->len; + if (memchr(msg, '\n', mlen) != NULL) return; /* already noted */ + /* `expected at , got `: the path extends the + * declared slot's selector, the kind is what the patch admits. */ + const char *path = NULL, *got = NULL; + size_t path_len = 0, got_len = 0; + if (mlen > 9 && memcmp(msg, "expected ", 9) == 0) { + for (size_t i = mlen; i-- > 7;) { + if (memcmp(msg + i - 6, ", got ", 6) == 0) { + got = msg + i; + got_len = mlen - i; + for (size_t j = i - 6; j-- > 4;) { + if (memcmp(msg + j - 4, " at $", 5) == 0) { + path = msg + j; + path_len = i - 6 - j; + break; + } + } + break; + } + } + } + const char *pkg, *version, *selector, *where, *declared; + size_t pkg_len = scr_origin_field(origin, 0, &pkg); + size_t version_len = scr_origin_field(origin, 1, &version); + size_t selector_len = scr_origin_field(origin, 3, &selector); + size_t where_len = scr_origin_field(origin, 4, &where); + size_t declared_len = scr_origin_field(origin, 5, &declared); + + ScrOriginBuf b = {0}; + scr_origin_put(&b, msg, mlen); + scr_origin_puts(&b, "\n "); + scr_origin_put(&b, pkg, pkg_len); + if (version_len > 0) { + scr_origin_puts(&b, "@"); + scr_origin_put(&b, version, version_len); + } + scr_origin_puts(&b, " declares "); + scr_origin_put(&b, selector, selector_len); + if (declared_len > 0) { + scr_origin_puts(&b, " as "); + scr_origin_put(&b, declared, declared_len); + } + scr_origin_puts(&b, " ("); + scr_origin_put(&b, where, where_len); + scr_origin_puts(&b, ")\n "); + ScrOriginBuf sel = {0}, type = {0}; + scr_origin_put(&sel, selector, selector_len); + /* $[3].email extends the selector as [].email: elements of an array + * share one declared type. */ + for (size_t i = 1; path != NULL && i < path_len; i++) { + if (path[i] == '[') { + scr_origin_puts(&sel, "[]"); + while (i < path_len && path[i] != ']') i++; + } else { + scr_origin_put(&sel, path + i, 1); + } + } + scr_origin_puts(&type, "| "); + if (got != NULL) { + const char *arm = scr_origin_arm(got, got_len); + if (arm != NULL) scr_origin_puts(&type, arm); + else scr_origin_put(&type, got, got_len); + } else { + scr_origin_puts(&type, "undefined"); + } + if (scr_origin_quotable(pkg, pkg_len) && scr_origin_quotable(sel.data, sel.len) && + scr_origin_quotable(type.data, type.len)) { + scr_origin_puts(&b, "if that declaration is wrong, patch it: scriptc types patch "); + scr_origin_put_arg(&b, pkg, pkg_len); + scr_origin_puts(&b, " "); + scr_origin_put_arg(&b, sel.data, sel.len); + scr_origin_puts(&b, " "); + scr_origin_put_arg(&b, type.data, type.len); + } else { + scr_origin_puts(&b, "if that declaration is wrong, patch it with scriptc types patch"); + } + free(sel.data); + free(type.data); + + ScrStr *next = scr_str_new(b.data, b.len); + free(b.data); + scr_str_release(e->message); + e->message = next; + if (e->stack != NULL) { + /* The cached `stack` starts with the message. */ + scr_str_release(e->stack); + e->stack = NULL; + } +} diff --git a/packages/runtime/src/scr_type_origin.test.c b/packages/runtime/src/scr_type_origin.test.c new file mode 100644 index 000000000..bc6477d23 --- /dev/null +++ b/packages/runtime/src/scr_type_origin.test.c @@ -0,0 +1,24 @@ +/* Built by scr_type_origin.test.ts: a pending TypeError gains the + * declaration note for an origin whose package name and selector carry + * shell syntax; the test runs the suggested command through sh. */ +#include "scr_runtime.h" + +#include +#include + +int main(int argc, char **argv) { + if (argc != 3) return 2; + /* package, version, range, selector, where, declared type */ + char origin[1024]; + snprintf(origin, sizeof origin, "%s\x1f" "1.0.0\x1f^1.0.0\x1f%s\x1findex.d.ts:3\x1fstring", argv[1], + argv[2]); + const char *text = "expected string at $, got undefined"; + ScrExcCell *cell = scr_exc_current_cell(); + cell->kind = SCR_EXC_OBJ; + cell->payload = scr_error_new(SCR_ERR_TYPE, scr_str_new(text, strlen(text))); + scr_type_origin_note(origin); + ScrError *e = cell->payload; + fwrite(e->message->data, 1, e->message->len, stdout); + fputc('\n', stdout); + return 0; +} diff --git a/packages/runtime/src/scr_type_origin.test.ts b/packages/runtime/src/scr_type_origin.test.ts new file mode 100644 index 000000000..b31195deb --- /dev/null +++ b/packages/runtime/src/scr_type_origin.test.ts @@ -0,0 +1,69 @@ +/* The suggested `scriptc types patch` command on a checked-conversion + * TypeError (scr_type_origin.c) passes each value it carries as one + * argument: package names and selectors come from package declarations, + * and a command copied from the message must not run anything they spell. + * Run through sh with printf in place of scriptc, the command prints the + * package, the selector and the type exactly, and creates no file. */ +import { execFile } from "node:child_process"; +import { mkdtemp, readdir, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { promisify } from "node:util"; +import { expect, test } from "vitest"; +import { EXECUTABLE_RUNTIME_SOURCES } from "../../compiler/src/backend/native-toolchain.js"; + +const exec = promisify(execFile); + +const hostile = [ + ["kit", "Profile.email"], + ["evil;touch pkg", "Profile.a;touch semicolon"], + ["kit", "Profile.$(touch subst)"], + ["kit", "Profile.`touch backtick`"], + ["kit", "Profile.it's \"quoted\" \\ end'"], + ["kit", "Profile.a&&touch and|touch pipe>out"], +] as const; + +test.skipIf(process.platform === "win32")( + "the suggested patch command passes declaration values as single arguments", + { timeout: 300_000 }, + async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-type-origin-")); + try { + const bin = join(dir, "origin"); + await exec("clang", [ + "-std=c11", + "-O1", + "-w", + "-pthread", + ...(process.platform === "linux" ? ["-D_GNU_SOURCE"] : []), + join(import.meta.dirname, "scr_type_origin.test.c"), + join(import.meta.dirname, "scr_type_origin.c"), + ...EXECUTABLE_RUNTIME_SOURCES.map((name) => join(import.meta.dirname, name)), + ...(process.platform === "linux" ? ["-lm"] : []), + "-o", + bin, + ]); + const sandbox = join(dir, "run"); + await exec("mkdir", [sandbox]); + const commands: string[] = []; + for (const [pkg, selector] of hostile) { + const { stdout } = await exec(bin, [pkg, selector]); + const line = stdout.split("\n").find((l) => l.includes("scriptc types patch")); + expect(line, stdout).toBeDefined(); + const command = line!.slice(line!.indexOf("scriptc types patch")); + commands.push(command); + const printed = await exec( + "sh", + ["-c", command.replace("scriptc types patch", "printf '%s\\n'")], + { cwd: sandbox }, + ); + expect(printed.stdout).toBe(`${pkg}\n${selector}\n| undefined\n`); + expect(await readdir(sandbox)).toEqual([]); + } + // Values without shell syntax stay bare. + expect(commands[0]).toBe("scriptc types patch kit Profile.email '| undefined'"); + } finally { + await rm(dir, { recursive: true, force: true }); + } + }, +); diff --git a/packages/runtime/src/scr_unwind.c b/packages/runtime/src/scr_unwind.c new file mode 100644 index 000000000..50474e82e --- /dev/null +++ b/packages/runtime/src/scr_unwind.c @@ -0,0 +1,83 @@ +/* scr_unwind_raise: the native unwind of the unwinding exception ABI of + * emitted functions (the compiler's unwind-abi.ts). Linux (glibc and musl) + * only; elsewhere this unit is empty. */ +#include "scr_runtime.h" + +#if defined(__linux__) && !defined(__ANDROID__) && !defined(__wasi__) +#include +#include +#include +#include +#include + +/* Weak, so the runtime unit alone does not link the unwinder into every + * program: programs that raise reference the personality routine from + * their landing pads, and its unit references the unwinder's entry points, + * which link it in. */ +#pragma weak _Unwind_ForcedUnwind +#pragma weak _Unwind_GetCFA + +#if defined(__SANITIZE_ADDRESS__) +#define SCR_UNWIND_ASAN 1 +#elif defined(__has_feature) +#if __has_feature(address_sanitizer) +#define SCR_UNWIND_ASAN 1 +#endif +#endif +#ifdef SCR_UNWIND_ASAN +#include +#endif + +#ifdef SCR_UNWIND_ASAN +/* The lowest stack address the unwind has not cleared yet. The unwind + * outlives the frame that started it: landing pads resume it. */ +static SCR_TL uintptr_t scr_unwind_low; +#endif + +/* Emitted landing pads are cleanups: they release the frame's references + * and then either take the exception (a handler of theirs, or a frame on + * the pending-flag ABI that returns with it pending) or resume the unwind. + * So the unwind is forced, a single pass through every landing pad; the + * exception itself stays in the active cell. */ +static _Unwind_Reason_Code scr_unwind_stop(int version, _Unwind_Action actions, + _Unwind_Exception_Class cls, + struct _Unwind_Exception *exc, + struct _Unwind_Context *context, void *arg) { + (void)version; + (void)cls; + (void)exc; +#ifdef SCR_UNWIND_ASAN + /* The frames this unwind passes never return, so it clears their + * redzones' shadow itself. Emitted code calls scr_unwind_raise as an + * ordinary function: the sanitizer's hook before no-return calls needs + * stack bounds, which fiber stacks do not report. */ + uintptr_t cfa = _Unwind_GetCFA(context); + if (cfa > scr_unwind_low) { + __asan_unpoison_memory_region((void *)scr_unwind_low, cfa - scr_unwind_low); + scr_unwind_low = cfa; + } +#else + (void)context; +#endif + (void)arg; + if (actions & _UA_END_OF_STACK) { + fputs("scriptc: an exception unwound past the program's outermost frame\n", stderr); + abort(); + } + return _URC_NO_REASON; +} + +static SCR_TL struct _Unwind_Exception scr_unwind_exception; + +void scr_unwind_raise(void) { + struct _Unwind_Exception *exc = &scr_unwind_exception; + memset(exc, 0, sizeof *exc); + exc->exception_class = 0x5343524950540000ull; /* "SCRIPT\0\0" */ +#ifdef SCR_UNWIND_ASAN + scr_unwind_low = (uintptr_t)__builtin_frame_address(0); +#endif + if (_Unwind_ForcedUnwind != NULL) _Unwind_ForcedUnwind(exc, scr_unwind_stop, NULL); + fputs("scriptc: the native unwinder could not raise an exception\n", stderr); + abort(); +} +#endif diff --git a/packages/runtime/src/scr_url.c b/packages/runtime/src/scr_url.c index 67de82471..a61286c63 100644 --- a/packages/runtime/src/scr_url.c +++ b/packages/runtime/src/scr_url.c @@ -6,7 +6,7 @@ * against Node by the differential corpus: * - scheme lowercasing; "Invalid URL" TypeError on schemeless input * - special schemes (http/https/ws/wss/ftp/file): authority parsing with - * host lowercasing, default-port removal (leading zeros stripped), + * percent-decoded, lowercased hosts, default-port removal (leading zeros stripped), * canonical bracketed IPv6 literals, * backslash-as-slash, and any run of leading slashes tolerated * (http:foo.com works); file: takes an authority only after exactly @@ -17,13 +17,12 @@ * (space, quotes, angle brackets, braces, non-ASCII UTF-8 bytes, ...); * existing %-sequences pass through verbatim (no decode, no case * normalization — like Node) - * - non-special schemes: "//" parses an authority (host case preserved), - * a single "/" roots a normalized path, anything else is an OPAQUE - * path kept verbatim (data:text/plain,hi there) - * - * DOCUMENTED DIVERGENCES (SEMANTICS.md): non-ASCII and %-escaped hosts are - * rejected with "Invalid URL" (no IDNA/punycode), and opaque paths skip - * the spec's C0-encode pass (kept verbatim). + * - non-special schemes: "//" parses an authority (an opaque host, + * case preserved, C0-encoded), a single "/" roots a normalized path, + * anything else is an OPAQUE path with the C0 control set encoded + * (data:text/plain,hi there) + * - UTS #46 processing of non-ASCII hosts through the IDNA unit + * (scr_url_idna_install) * * Failures THROW catchable TypeErrors through the exception cell with * Node's messages; callers are compiler-emitted pending checks. */ @@ -176,7 +175,7 @@ static bool enc_query(unsigned char c, bool special) { } static bool enc_path(unsigned char c) { - return enc_query(c, false) || c == '?' || c == '`' || c == '{' || c == '}'; + return enc_query(c, false) || c == '?' || c == '^' || c == '`' || c == '{' || c == '}'; } static bool enc_userinfo(unsigned char c) { @@ -392,17 +391,63 @@ static bool url_ipv4_parse(const char *s, size_t n, UrlBuf *out) { return true; } -/* Shared constructor/setter host parsing keeps the existing ASCII-host - * boundary and canonical IPv6 serialization in both execution tiers. A - * special host that ends in a number is an IPv4 address: it serializes - * as dotted decimal, or the URL is invalid. */ +/* Byte classes for the host parser: URL_FORBIDDEN_HOST marks the URL + * Standard's forbidden host code points (opaque hosts), URL_FORBIDDEN_DOMAIN + * its forbidden domain code points (those plus the C0 controls, '%' and + * U+007F) and every non-ASCII byte, which a special host cannot hold + * without UTS #46 processing. */ +enum { URL_FORBIDDEN_HOST = 1, URL_FORBIDDEN_DOMAIN = 2 }; + +static const unsigned char url_host_byte_class[256] = { + 3, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 2, 2, 3, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 3, 0, 0, 3, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 3, 0, 3, 3, + 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 3, 3, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, +}; + +static bool enc_c0_control(unsigned char c) { + return c < 0x20 || c > 0x7E; +} + +static int hex_val(char c) { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return -1; +} + +/* UTS #46 ToASCII (scr_idna.c), which programs that use URLs install at + * startup (scr_url_idna_install). NULL refuses hosts that hold non-ASCII + * text. */ +ScrStr *(*scr_url_host_to_ascii)(const char *p, size_t n) = NULL; + +/* The URL Standard's host parser for the constructor and the setters, + * with canonical IPv6 serialization: + * - a non-special (opaque) host refuses forbidden host code points and + * percent-encodes the C0 control set; + * - a special host is percent-decoded and lowercased; when that leaves + * non-ASCII text it goes through UTS #46 ToASCII (as Node's host parser + * does, an ASCII host, "xn--" labels included, is only lowercased). The + * result is refused when it holds a forbidden domain code point, and + * serialized as dotted decimal when it ends in a number (or refused when + * that number is invalid). */ static bool parse_host(const char *hp, size_t host_len, bool special, bool is_file, ScrStr **host) { bool ipv6 = host_len > 0 && hp[0] == '['; size_t ipv6_end = host_len ? host_len - 1 : 0; if (ipv6 && (host_len < 3 || hp[ipv6_end] != ']')) return false; - /* Validate + lowercase (special) the host. Divergence: non-ASCII and - * %-escapes (IDNA territory) are rejected outright. */ UrlBuf hb; ub_init(&hb); if (ipv6) { @@ -410,22 +455,53 @@ static bool parse_host(const char *hp, size_t host_len, bool special, bool is_fi free(hb.data); return false; } - } else { + *host = ub_take(&hb); + return true; + } + if (!special) { for (size_t i = 0; i < host_len; i++) { unsigned char c = (unsigned char)hp[i]; - if (c >= 0x80 || c == '%' || c == ':' || c == '[' || c == ']' || - c <= 0x20 || c == '#' || c == '/' || c == '<' || c == '>' || - c == '?' || c == '@' || c == '\\' || c == '^' || c == '|') { + if (url_host_byte_class[c] & URL_FORBIDDEN_HOST) { free(hb.data); return false; } - if (special && c >= 'A' && c <= 'Z') { - c = (unsigned char)(c - 'A' + 'a'); - } - ub_push(&hb, (char)c); + ub_push_encoded(&hb, c, enc_c0_control); + } + *host = ub_take(&hb); + return true; + } + bool non_ascii = false; + for (size_t i = 0; i < host_len; i++) { + unsigned char c = (unsigned char)hp[i]; + int hi, lo; + if (c == '%' && i + 2 < host_len && (hi = hex_val(hp[i + 1])) >= 0 && + (lo = hex_val(hp[i + 2])) >= 0) { + c = (unsigned char)(hi * 16 + lo); + i += 2; + } + if (c >= 0x80) non_ascii = true; + if (c >= 'A' && c <= 'Z') c = (unsigned char)(c - 'A' + 'a'); + ub_push(&hb, (char)c); + } + if (non_ascii) { + /* UTS #46 ToASCII, or refused without the IDNA unit. */ + ScrStr *ascii = scr_url_host_to_ascii ? scr_url_host_to_ascii(hb.data, hb.len) : NULL; + free(hb.data); + if (!ascii || ascii->len == 0) { + scr_str_release(ascii); + return false; + } + ub_init(&hb); + ub_append(&hb, ascii->data, ascii->len); + scr_str_release(ascii); + } + for (size_t i = 0; i < hb.len; i++) { + if (url_host_byte_class[(unsigned char)hb.data[i]] & URL_FORBIDDEN_DOMAIN) { + free(hb.data); + return false; } } - if (!ipv6 && special && url_ends_in_number(hb.data, hb.len)) { + if (url_ends_in_number(hb.data, hb.len)) { UrlBuf ip; ub_init(&ip); bool ok = url_ipv4_parse(hb.data, hb.len, &ip); @@ -438,7 +514,7 @@ static bool parse_host(const char *hp, size_t host_len, bool special, bool is_fi } /* file: "localhost" normalizes to "" at parse time (Node). */ if (is_file && hb.len == 9 && memcmp(hb.data, "localhost", 9) == 0) hb.len = 0; - if (hb.len == 0 && special && !is_file) { + if (hb.len == 0 && !is_file) { free(hb.data); return false; /* http:// — special non-file needs a host */ } @@ -481,6 +557,13 @@ static bool parse_authority(const char *raw, size_t len, bool special, bool is_f } const char *hp = at >= 0 ? raw + at + 1 : raw; size_t hp_len = at >= 0 ? len - (size_t)at - 1 : len; + /* Credentials need a host. A file: authority is a host alone, so its + * '@' and ':' reach the host parser, which refuses them. */ + if (at >= 0 && hp_len == 0) return false; + if (is_file) { + *port = scr_str_new("", 0); + return at < 0 && parse_host(hp, hp_len, special, is_file, host); + } /* host[:port], with IPv6 literals bracketed per WHATWG. */ long colon = -1; size_t host_len = hp_len; @@ -500,6 +583,7 @@ static bool parse_authority(const char *raw, size_t len, bool special, bool is_f } host_len = colon >= 0 ? (size_t)colon : hp_len; } + if (colon >= 0 && host_len == 0) return false; if (!parse_host(hp, host_len, special, is_file, host)) return false; /* Port: digits only, leading zeros stripped, defaults dropped. */ UrlBuf pb; @@ -700,11 +784,18 @@ ScrUrl *scr_url_new(ScrStr *input) { port = scr_str_new("", 0); path = parse_rooted_path(body, body_len, false); } else { - /* Opaque path — kept verbatim (divergence: the spec C0-encodes). */ + /* Opaque path: the C0 control set is percent-encoded, and a space + * right before a query or fragment becomes %20. */ userinfo = scr_str_new("", 0); host = scr_str_new("", 0); port = scr_str_new("", 0); - path = scr_str_new(body, body_len); + UrlBuf ob; + ub_init(&ob); + bool trailing_space = qmark < rest_len && body_len > 0 && body[body_len - 1] == ' '; + size_t kept = trailing_space ? body_len - 1 : body_len; + for (size_t i = 0; i < kept; i++) ub_push_encoded(&ob, (unsigned char)body[i], enc_c0_control); + if (trailing_space) ub_append(&ob, "%20", 3); + path = ub_take(&ob); } /* Query and fragment, encoded and stored WITH their sigils. */ @@ -823,18 +914,22 @@ ScrUrl *scr_url_new_base(ScrStr *input, ScrStr *base_input) { bool drive = file && path_len >= 2 && url_drive_letter(raw, 2) && (path_len == 2 || raw[2] == '/' || raw[2] == '\\'); if (base->has_authority) { + /* A drive letter relative to a file: base keeps the base's host. */ ub_append(&absolute, "//", 2); - if (!drive) { - if (base->userinfo->len) { - ub_append(&absolute, base->userinfo->data, base->userinfo->len); - ub_push(&absolute, '@'); - } - ub_append(&absolute, base->host->data, base->host->len); - if (base->port->len) { - ub_push(&absolute, ':'); - ub_append(&absolute, base->port->data, base->port->len); - } + if (base->userinfo->len) { + ub_append(&absolute, base->userinfo->data, base->userinfo->len); + ub_push(&absolute, '@'); } + ub_append(&absolute, base->host->data, base->host->len); + if (base->port->len) { + ub_push(&absolute, ':'); + ub_append(&absolute, base->port->data, base->port->len); + } + } else if (!opaque) { + /* A host-less base path can start with "//"; the "/." prefix keeps + * the resolved path from reparsing as an authority (dot-segment + * removal drops it). */ + ub_append(&absolute, "/.", 2); } if (!path_len) { ub_append(&absolute, base->path->data, base->path->len); @@ -1279,13 +1374,6 @@ void scr_url_set_checked(ScrUrl *u, ScrStr *field, const ScrDyn *value) { /* ── the file-URL bridge ─────────────────────────────────────────────── */ -static int hex_val(char c) { - if (c >= '0' && c <= '9') return c - '0'; - if (c >= 'a' && c <= 'f') return c - 'a' + 10; - if (c >= 'A' && c <= 'F') return c - 'A' + 10; - return -1; -} - static bool url_file_scheme(ScrUrl *u) { if (u->scheme->len == 4 && memcmp(u->scheme->data, "file", 4) == 0) return true; scr_throw_error_msg_code(SCR_ERR_TYPE, "The URL must be of scheme file", 30, "ERR_INVALID_URL_SCHEME"); @@ -1394,6 +1482,22 @@ ScrStr *scr_url_to_path(ScrUrl *u) { #endif } +/* fs.existsSync and the fs.promises functions convert a URL path inside a + * try and turn its error into false or a rejection: the conversion's result + * as a value, the path string or the error. */ +ScrDyn *scr_url_to_path_value(ScrUrl *u) { + ScrStr *path = scr_url_to_path(u); + if (path) { + ScrDyn *value = scr_dyn_new_str(path); + scr_str_release(path); + return value; + } + ScrCaught *caught = scr_exc_take(); + ScrDyn *error = scr_caught_to_dyn(caught); + scr_caught_release(caught); + return error; +} + /* The string-receiver form parses first (Node accepts URL strings). */ ScrStr *scr_url_str_to_path(ScrStr *input) { ScrUrl *u = scr_url_new(input); @@ -1451,7 +1555,7 @@ static void scr_url_throw_arg_value(const char *reason, ScrStr *value) { * bindingUrl.pathToFileURL does, reusing the parser's rooted-path * machinery (WHATWG dot-segment removal with empty segments preserved, * backslash-as-slash when `backslash_sep`, the enc_path set): the bytes - * Node encodes BEYOND that set ('%', '[', ']', '^', '|' — and '\\' when + * Node encodes BEYOND that set ('%', '[', ']', '^', '|', '~' — and '\\' when * it is a path byte rather than a separator) pre-encode here so the * parser passes them through. */ static ScrStr *scr_url_encode_file_path(const char *raw, size_t len, bool backslash_sep) { @@ -1459,7 +1563,7 @@ static ScrStr *scr_url_encode_file_path(const char *raw, size_t len, bool backsl ub_init(&pre); for (size_t i = 0; i < len; i++) { unsigned char c = (unsigned char)raw[i]; - if (c == '%' || c == '[' || c == ']' || c == '^' || c == '|' || + if (c == '%' || c == '[' || c == ']' || c == '^' || c == '|' || c == '~' || (c == '\\' && !backslash_sep)) { char hex[4]; snprintf(hex, sizeof hex, "%%%02X", c); @@ -1521,24 +1625,30 @@ static ScrUrl *scr_url_from_path_impl(ScrStr *path, bool win32) { scr_str_release(resolved); return NULL; } - /* The URL host parser lowercases ASCII and TERMINATES at the URL - * delimiters '/', '?', '#' (Node feeds the servername through - * set_hostname: "\\\\?\\C:\\x" gets an EMPTY host and stays a - * drive path). No IDNA (documented divergence — Node punycodes - * non-ASCII names), and names with ':' or '%' stay verbatim where - * Node aborts on a native set_hostname assertion. */ - UrlBuf host; - ub_init(&host); + /* The servername goes through the host parser, as Node's hostname + * setter does: it drops tabs and newlines, stops at the URL + * delimiters '/', '?' and '#' + * ("\\\\?\\C:\\x" gets an empty host and stays a drive path), + * and a host it refuses throws ERR_INVALID_URL. */ + UrlBuf name; + ub_init(&name); for (size_t i = prefix; i < (size_t)host_end; i++) { char c = resolved->data[i]; if (c == '/' || c == '?' || c == '#') break; - if (c >= 'A' && c <= 'Z') c = (char)(c + 32); - ub_push(&host, c); + if (c != '\t' && c != '\n' && c != '\r') ub_push(&name, c); + } + ScrStr *host = NULL; + bool valid = parse_host(name.data, name.len, true, true, &host); + free(name.data); + if (!valid) { + scr_str_release(resolved); + scr_url_throw_invalid(); + return NULL; } ScrStr *encoded = scr_url_encode_file_path(resolved->data + host_end, resolved->len - (size_t)host_end, true); scr_str_release(resolved); - return scr_url_new_file(ub_take(&host), encoded); + return scr_url_new_file(host, encoded); } } else { ScrArr *pack = scr_arr_new(SCR_ELEM_STR, 1); @@ -1649,10 +1759,117 @@ static bool scr_native_url_set(void *ptr, const char *key, size_t len, const Scr return true; } +/* util.inspect's view of a URL: the properties Node's URL inspector lists, + * in its order, with the live searchParams object. Past the depth limit + * the inspector returns the URL itself, which util.inspect prints as + * `URL {}` (it has no own properties). */ +static ScrDyn *scr_native_url_inspect_view(void *ptr) { + static const char *const keys[] = {"href", "origin", "protocol", "username", "password", + "host", "hostname", "port", "pathname", "search", + "searchParams", "hash"}; + ScrDyn *view = scr_dyn_new_obj(); + for (size_t i = 0; i < sizeof keys / sizeof keys[0]; i++) { + size_t len = strlen(keys[i]); + scr_dyn_obj_set(view, keys[i], len, scr_native_url_get(ptr, keys[i], len)); + } + return view; +} + +/* Node's urlToHttpOptions over a URL: protocol, hostname (an IPv6 literal + * without its brackets), hash, search, pathname, path (pathname and + * search), href, then port as a number when the URL has one and auth + * (the decoded username and password) when it has credentials. */ +ScrDyn *scr_url_to_http_options(ScrUrl *u) { + ScrDyn *options = scr_dyn_new_obj_null_proto(); + ScrStr *pathname = scr_url_pathname(u); + ScrStr *search = scr_url_search(u); + ScrStr *hostname = scr_url_hostname(u); + ScrStr *port = scr_url_port(u); + ScrStr *username = scr_url_username(u); + ScrStr *password = scr_url_password(u); + ScrStr *auth = NULL; + if (username->len || password->len) { + ScrStr *user = scr_str_decode_uri_component(username); + ScrStr *pass = user ? scr_str_decode_uri_component(password) : NULL; + if (pass) { + UrlBuf b; + ub_init(&b); + ub_append(&b, user->data, user->len); + ub_push(&b, ':'); + ub_append(&b, pass->data, pass->len); + auth = ub_take(&b); + } + scr_str_release(user); + scr_str_release(pass); + if (!auth) { + scr_str_release(pathname); + scr_str_release(search); + scr_str_release(hostname); + scr_str_release(port); + scr_str_release(username); + scr_str_release(password); + scr_dyn_release(options); + return NULL; + } + } +#define PUT_STR(key, value) \ + do { \ + ScrStr *text_ = (value); \ + scr_dyn_obj_set(options, key, sizeof key - 1, scr_dyn_new_str(text_)); \ + scr_str_release(text_); \ + } while (0) + PUT_STR("protocol", scr_url_protocol(u)); + if (hostname->len >= 2 && hostname->data[0] == '[') + PUT_STR("hostname", scr_str_new(hostname->data + 1, hostname->len - 2)); + else + PUT_STR("hostname", scr_str_retain(hostname)); + PUT_STR("hash", scr_url_hash(u)); + PUT_STR("search", scr_str_retain(search)); + PUT_STR("pathname", scr_str_retain(pathname)); + UrlBuf path; + ub_init(&path); + ub_append(&path, pathname->data, pathname->len); + ub_append(&path, search->data, search->len); + PUT_STR("path", ub_take(&path)); + PUT_STR("href", scr_url_href(u)); + if (port->len) { + double number = 0; + for (size_t i = 0; i < port->len; i++) number = number * 10 + (port->data[i] - '0'); + scr_dyn_obj_set(options, "port", 4, scr_dyn_new_num(number)); + } + if (auth) PUT_STR("auth", auth); +#undef PUT_STR + scr_str_release(pathname); + scr_str_release(search); + scr_str_release(hostname); + scr_str_release(port); + scr_str_release(username); + scr_str_release(password); + return options; +} + +ScrDyn *scr_url_to_http_options_dyn(const ScrDyn *value) { + if (scr_dyn_native_url_is(value)) return scr_url_to_http_options(value->v.handle.ptr); + if (!value || value->kind <= SCR_DYN_STR || value->kind == SCR_DYN_UNDEF || + value->kind == SCR_DYN_BIGINT || value->kind == SCR_DYN_SYMBOL) { + scr_dyn_arg_type_fail("url", "of type object", value); + return NULL; + } + static const char message[] = "url.urlToHttpOptions of an object that is not a URL is not supported yet"; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); + return NULL; +} + ScrDyn *scr_dyn_native_url(ScrUrl *value) { static const ScrDynHandleOps ops = { - "URL", &scr_url_retain_v, &scr_url_release_v, &scr_native_url_invoke, - &scr_native_url_get, &scr_native_url_set, NULL, NULL, + .cls = "URL", + .retain = &scr_url_retain_v, + .release = &scr_url_release_v, + .invoke = &scr_native_url_invoke, + .get = &scr_native_url_get, + .set = &scr_native_url_set, + .inspect_view = &scr_native_url_inspect_view, + .inspect_view_nested = true, }; scr_dyn_handle_install(SCR_DYNH_URL, &ops); return scr_dyn_new_handle(value, SCR_DYNH_URL); diff --git a/packages/runtime/src/scr_url.h b/packages/runtime/src/scr_url.h index 05e5702bb..13a7d093b 100644 --- a/packages/runtime/src/scr_url.h +++ b/packages/runtime/src/scr_url.h @@ -41,6 +41,16 @@ ScrStr *scr_url_href(ScrUrl *u); /* +1; also toString() */ void scr_url_set(ScrUrl *u, ScrStr *field, ScrStr *value); /* borrowed; href may throw */ bool scr_url_set_component(ScrUrl *u, ScrStr *field, ScrStr *value); /* borrowed; false for ignored writes */ void scr_url_set_checked(ScrUrl *u, ScrStr *field, const ScrDyn *value); /* borrowed; conversion may throw */ +/* url.urlToHttpOptions: Node's null-prototype options object for a URL. + * The checked-dynamic form throws ERR_INVALID_ARG_TYPE for a primitive or + * null and has no lowering for an object that is not a URL. Borrowed; +1, + * or NULL with the error pending. */ +ScrDyn *scr_url_to_http_options(ScrUrl *u); +/* UTS #46 ToASCII for the host parser: NULL until scr_url_idna_install + * (scr_idna.c, emitted at startup by programs that use URLs) sets it. */ +extern ScrStr *(*scr_url_host_to_ascii)(const char *p, size_t n); +void scr_url_idna_install(void); +ScrDyn *scr_url_to_http_options_dyn(const ScrDyn *value); /* node:url's legacy surface (scr_url_legacy.c — linked with the regex, * querystring and dyn-async units). url.parse() answers a checked-dynamic * Url instance; format() accepts a Url-shaped object, a string, or a @@ -64,6 +74,9 @@ ScrStr *scr_punycode_to_unicode(const ScrStr *input); ScrArr *scr_punycode_ucs2_decode(const ScrStr *input); ScrStr *scr_url_domain_to_unicode(const ScrDyn *domain, bool missing); ScrStr *scr_url_to_path(ScrUrl *u); /* +1, or throws */ +/* scr_url_to_path as a value: the path string, or the error it would throw + * (+1 dyn, never throws). */ +ScrDyn *scr_url_to_path_value(ScrUrl *u); ScrStr *scr_url_str_to_path(ScrStr *s); /* +1, or throws */ ScrUrl *scr_url_from_path(ScrStr *path); /* +1; throws on win32 UNC malformations only */ ScrUrl *scr_url_from_path_platform(ScrStr *path, bool windows); /* +1; explicit options.windows */ diff --git a/packages/runtime/src/scr_util_callable.c b/packages/runtime/src/scr_util_callable.c index c905ea8ae..17076b078 100644 --- a/packages/runtime/src/scr_util_callable.c +++ b/packages/runtime/src/scr_util_callable.c @@ -9,8 +9,8 @@ * and reported on the next tick (scr_warning_emit_deferred), which also * applies process.noDeprecation and process.throwDeprecation. debuglog * matches sections against NODE_DEBUG as the process started and writes - * Node's "SECTION PID: message" lines to stderr. All arguments are - * borrowed. */ + * Node's "SECTION PID: message" lines, formatted by scr_insp_format, to + * stderr. All arguments are borrowed. */ #include "scr_runtime.h" #include @@ -149,23 +149,9 @@ static bool uc_define(ScrDyn *target, const char *key, ScrDyn *descriptor) { * Node 24's lib/util.js: nullish arguments and an undefined * superCtor.prototype throw ERR_INVALID_ARG_TYPE; then super_ becomes a * writable, configurable, non-enumerable own property and - * ctor.prototype's prototype becomes superCtor.prototype. */ -void scr_util_inherits(ScrDyn *ctor, ScrDyn *super_ctor) { - if (uc_nullish(ctor)) { - scr_dyn_arg_type_fail("ctor", "of type function", ctor); - return; - } - if (uc_nullish(super_ctor)) { - scr_dyn_arg_type_fail("superCtor", "of type function", super_ctor); - return; - } - if ((ctor->kind == SCR_DYN_FUNC && ctor->v.fn.class_obj) || - (super_ctor->kind == SCR_DYN_FUNC && super_ctor->v.fn.class_obj)) { - uc_fence("util.inherits over class constructors has no lowering; declare the subclass with extends"); - return; - } - ScrDyn *super_prototype = uc_get(super_ctor, "prototype"); - if (!super_prototype) return; + * ctor.prototype's prototype becomes superCtor.prototype. super_prototype + * is superCtor.prototype (owned), or NULL when superCtor is no object. */ +static void uc_inherits(ScrDyn *ctor, ScrDyn *super_ctor, ScrDyn *super_prototype) { if (super_prototype->kind == SCR_DYN_UNDEF) { scr_dyn_prop_type_fail("superCtor.prototype", "of type object", super_prototype); scr_dyn_release(super_prototype); @@ -198,6 +184,40 @@ void scr_util_inherits(ScrDyn *ctor, ScrDyn *super_ctor) { scr_dyn_release(super_prototype); } +void scr_util_inherits(ScrDyn *ctor, ScrDyn *super_ctor) { + if (uc_nullish(ctor)) { + scr_dyn_arg_type_fail("ctor", "of type function", ctor); + return; + } + if (uc_nullish(super_ctor)) { + scr_dyn_arg_type_fail("superCtor", "of type function", super_ctor); + return; + } + if ((ctor->kind == SCR_DYN_FUNC && ctor->v.fn.class_obj) || + (super_ctor->kind == SCR_DYN_FUNC && super_ctor->v.fn.class_obj)) { + uc_fence("util.inherits over class constructors has no lowering; declare the subclass with extends"); + return; + } + ScrDyn *super_prototype = uc_get(super_ctor, "prototype"); + if (!super_prototype) return; + uc_inherits(ctor, super_ctor, super_prototype); +} + +/* util.inherits(ctor, C) where C is a compiled class whose prototype the + * compiler exposes as super_prototype (borrowed): the class value's own + * properties do not hold it. ctor itself may not be a class. */ +void scr_util_inherits_prototype(ScrDyn *ctor, ScrDyn *super_ctor, ScrDyn *super_prototype) { + if (uc_nullish(ctor)) { + scr_dyn_arg_type_fail("ctor", "of type function", ctor); + return; + } + if (ctor->kind == SCR_DYN_FUNC && ctor->v.fn.class_obj) { + uc_fence("util.inherits over class constructors has no lowering; declare the subclass with extends"); + return; + } + uc_inherits(ctor, super_ctor, scr_dyn_retain(super_prototype)); +} + /* ── util.deprecate ──────────────────────────────────────────────────── * The wrapper warns once (and once per code across wrappers) before each * call reaches fn with the call's receiver and arguments; construction @@ -345,6 +365,66 @@ ScrDyn *scr_util_deprecate(ScrDyn *fn, ScrDyn *msg, ScrDyn *code, ScrDyn *option return deprecated; } +/* ── util.isArray and util._extend ───────────────────────────────────── + * Node wraps both with its internal deprecate(): the first call while + * deprecation warnings are on reports DEP0044 or DEP0060 on the next tick, + * unless a util.deprecate wrapper already reported that code. */ +static SCR_TL bool uc_is_array_warned = false; +static SCR_TL bool uc_extend_warned = false; + +static bool uc_internal_deprecation(bool *warned, const char *message, const char *code) { + if (scr_process_deprecation_flag_on(0) || *warned) return true; + *warned = true; + if (!uc_codes_warned) { + uc_codes_warned = scr_dyn_new_obj_null_proto(); + scr_atexit(uc_codes_warned_teardown); + } + size_t code_len = strlen(code); + if (scr_dyn_obj_get(uc_codes_warned, code, code_len)) return true; + ScrStr *text = scr_str_new(message, strlen(message)); + ScrDyn *msg = scr_dyn_new_str(text); + scr_str_release(text); + ScrStr *key = scr_str_new(code, code_len); + bool ok = uc_emit(msg, "DeprecationWarning", key); + if (ok) scr_dyn_obj_set(uc_codes_warned, code, code_len, scr_dyn_new_bool(true)); + scr_str_release(key); + scr_dyn_release(msg); + return ok; +} + +bool scr_util_is_array_deprecated(const ScrDyn *value) { + if (!uc_internal_deprecation(&uc_is_array_warned, + "The `util.isArray` API is deprecated. Please use `Array.isArray()` instead.", + "DEP0044")) + return false; + return value->kind == SCR_DYN_ARR || scr_dyn_ref_is_array(value); +} + +/* util._extend(target, source): source's own enumerable string keys, + * assigned to target from the last key to the first; a source that is + * null or not an object leaves target as it is. +1. */ +ScrDyn *scr_util_extend(ScrDyn *target, ScrDyn *source) { + if (!uc_internal_deprecation(&uc_extend_warned, + "The `util._extend` API is deprecated. Please use Object.assign() instead.", + "DEP0060")) + return NULL; + bool object = source->kind == SCR_DYN_OBJ || source->kind == SCR_DYN_ARR || source->kind == SCR_DYN_TYPED_REF || + source->kind == SCR_DYN_HANDLE || source->kind == SCR_DYN_BYTES || source->kind == SCR_DYN_PROMISE || + source->kind == SCR_DYN_PROXY; + if (!object) return scr_dyn_retain(target); + ScrDyn *keys = scr_dyn_obj_keys(source); + if (!keys) return NULL; + for (size_t i = keys->v.arr.len; i-- > 0 && !scr_exc_pending();) { + ScrDyn *key = keys->v.arr.items[i]; + ScrDyn *value = scr_dyn_reflect_get(source, key, source); + if (!value) break; + scr_dyn_key_set(target, key->v.str, value); + scr_dyn_release(value); + } + scr_dyn_release(keys); + return scr_exc_pending() ? NULL : scr_dyn_retain(target); +} + /* ── util.callbackify ────────────────────────────────────────────────── * The wrapper pops the callback, binds it to the call's receiver and * calls original; the promise's settlement schedules the callback on the @@ -513,163 +593,6 @@ ScrDyn *scr_util_callbackify(ScrDyn *original) { return callbackified; } -/* ── util.format over dyn arguments ──────────────────────────────────── - * formatWithOptionsInternal for debuglog's lines: the %-substitutions - * (%s %d %i %f %j %o %O %c %%) with Node's argument cursor, then the - * remaining arguments space-joined, strings verbatim and everything else - * inspected. With terminal colors, primitives take inspect's default - * styles; colored object renderings have no lowering. */ -static bool uc_primitive(const ScrDyn *value) { - return !uc_object(value); -} - -static ScrStr *uc_inspect(ScrDyn *value, double depth, bool colors) { - if (!colors) return scr_insp_dyn(value, 0, depth); - if (!uc_primitive(value)) { - uc_fence("util.debuglog output of objects on a color terminal has no lowering"); - return NULL; - } - const char *open = "\x1b[33m", *close = "\x1b[39m"; - if (value->kind == SCR_DYN_UNDEF) open = "\x1b[90m"; - else if (value->kind == SCR_DYN_NULL) open = "\x1b[1m", close = "\x1b[22m"; - else if (value->kind == SCR_DYN_STR || value->kind == SCR_DYN_SYMBOL) open = "\x1b[32m"; - ScrStr *text = scr_insp_dyn(value, 0, depth); - ScrJsonBuf b; - scr_jb_init(&b); - scr_jb_puts(&b, open); - scr_jb_put_str(&b, text); - scr_jb_puts(&b, close); - scr_str_release(text); - return scr_jb_finish(&b); -} - -/* hasBuiltInToString's negative answer for checked objects: an own or - * inherited toString the program defined. Symbol-keyed members (where - * Symbol.toPrimitive would also count) are reported as unknown. */ -static int uc_custom_to_string(const ScrDyn *value) { - for (const ScrDyn *current = value; current && current->kind == SCR_DYN_OBJ; current = current->prototype) { - if (current->symbol_properties) return -1; - const ScrDyn *member = scr_dyn_obj_get(current, "toString", 8); - if (member) return member->kind == SCR_DYN_FUNC; - } - return 0; -} - -static ScrStr *uc_format_s(ScrDyn *value) { - if (value->kind == SCR_DYN_STR) return scr_str_retain(value->v.str); - if (value->kind == SCR_DYN_NUM) return scr_insp_f64(value->v.num); - if (value->kind == SCR_DYN_FUNC) { - uc_fence("util.debuglog %s of a function prints its source text, which a compiled binary does not keep"); - return NULL; - } - if (value->kind == SCR_DYN_OBJ && scr_dyn_obj_get(value, "%error", 6) == NULL) { - int custom = uc_custom_to_string(value); - if (custom < 0) { - uc_fence("util.debuglog %s of objects with symbol-keyed members has no lowering"); - return NULL; - } - if (custom) return scr_dyn_string_coerce_js(value); - } - return scr_insp_dyn_s(value, 0); -} - -static ScrStr *uc_format_number(ScrDyn *value, int spec) { - if (value->kind == SCR_DYN_BIGINT && spec != 'f') return scr_bigint_inspect(value->v.bigint); - if (value->kind == SCR_DYN_SYMBOL) return scr_str_new("NaN", 3); - double number; - if (spec == 'd') { - if (value->kind == SCR_DYN_BIGINT) return scr_bigint_inspect(value->v.bigint); - if (!scr_dyn_number_coerce_js(value, &number)) return NULL; - } else { - ScrStr *text = scr_dyn_string_coerce_js(value); - if (!text) return NULL; - number = spec == 'i' ? scr_parse_int(text, 0) : scr_parse_float(text); - scr_str_release(text); - } - return scr_insp_f64(number); -} - -static ScrStr *uc_format(ScrDyn *const *args, size_t argc, bool colors) { - if (argc == 0) return scr_str_new("", 0); - ScrJsonBuf out; - scr_jb_init(&out); - size_t a = 0; - const char *join = ""; - if (args[0]->kind == SCR_DYN_STR) { - const ScrStr *first = args[0]->v.str; - if (argc == 1) { - scr_str_release(scr_jb_finish(&out)); - return scr_str_retain((ScrStr *)first); - } - const char *text = first->data; - size_t last = 0; - for (size_t i = 0; first->len > 0 && i < first->len - 1; i++) { - if (text[i] != '%') continue; - int next = (unsigned char)text[++i]; - if (a + 1 != argc) { - ScrStr *piece = NULL; - switch (next) { - case 's': piece = uc_format_s(args[++a]); break; - case 'j': piece = scr_util_format_j(args[++a]); break; - case 'd': - case 'i': - case 'f': piece = uc_format_number(args[++a], next); break; - case 'O': piece = uc_inspect(args[++a], 2, colors); break; - case 'o': - if (!uc_primitive(args[a + 1])) { - uc_fence("util.debuglog %o of objects renders hidden properties, which have no lowering; use %O"); - break; - } - piece = uc_inspect(args[++a], 4, colors); - break; - case 'c': - a += 1; - piece = scr_str_new("", 0); - break; - case '%': - scr_jb_write(&out, text + last, i - last); - last = i + 1; - continue; - default: - continue; - } - if (!piece) { - scr_str_release(scr_jb_finish(&out)); - return NULL; - } - if (last != i - 1) scr_jb_write(&out, text + last, i - 1 - last); - scr_jb_put_str(&out, piece); - scr_str_release(piece); - last = i + 1; - } else if (next == '%') { - scr_jb_write(&out, text + last, i - last); - last = i + 1; - } - } - if (last != 0) { - a++; - join = " "; - if (last < first->len) scr_jb_write(&out, text + last, first->len - last); - } - } - for (; a < argc; a++) { - scr_jb_puts(&out, join); - if (args[a]->kind == SCR_DYN_STR) { - scr_jb_put_str(&out, args[a]->v.str); - } else { - ScrStr *piece = uc_inspect(args[a], 2, colors); - if (!piece) { - scr_str_release(scr_jb_finish(&out)); - return NULL; - } - scr_jb_put_str(&out, piece); - scr_str_release(piece); - } - join = " "; - } - return scr_jb_finish(&out); -} - /* ── util.debuglog ───────────────────────────────────────────────────── * The logger defers its NODE_DEBUG test to its first call or `enabled` * read. The first call also selects the section's shared implementation @@ -735,7 +658,10 @@ static bool uc_debug_test(const ScrStr *section) { static ScrDyn *uc_debug_write(ScrClosure *closure, ScrDyn *const *args, size_t argc) { bool colors = scr_getenv("FORCE_COLOR") ? scr_util_terminal_colors() : scr_process_is_tty(2) && scr_util_terminal_colors(); - ScrStr *message = uc_format(args, argc, colors); + ScrInspFormatOptions options; + scr_insp_format_defaults(&options, "util.debuglog"); + options.colors = colors; + ScrStr *message = scr_insp_format(args, argc, &options); if (!message) return NULL; ScrDyn *set = uc_cap(closure, 0); char pid[64]; @@ -892,3 +818,158 @@ ScrDyn *scr_util_debuglog(ScrDyn *section, ScrDyn *callback) { } return logger; } + +/* ── util.diff(actual, expected) ─────────────────────────────────────── + * Node's diff over strings (one entry per UTF-16 code unit) or arrays of + * strings: identical values answer an empty array before validation; + * otherwise each value must be a string or an array of strings + * (ERR_INVALID_ARG_TYPE naming `actual`, `actual[i]`, ...), and the answer + * is myersDiff's operation list reversed: [0, item] for kept items, + * [1, item] for items of actual and [-1, item] for items of expected. */ +typedef struct { + ScrStr **items; + size_t len; +} UcDiffSeq; + +static void uc_diff_seq_free(UcDiffSeq *seq) { + for (size_t i = 0; i < seq->len; i++) scr_str_release(seq->items[i]); + free(seq->items); + seq->items = NULL; + seq->len = 0; +} + +static bool uc_diff_input(const ScrDyn *value, const char *name, UcDiffSeq *out) { + out->items = NULL; + out->len = 0; + if (value->kind == SCR_DYN_STR) { + ScrStr *text = value->v.str; + size_t len = (size_t)scr_str_utf16_len(text); + out->items = malloc((len ? len : 1) * sizeof *out->items); + if (!out->items) scr_trap("scriptc: out of memory\n"); + for (size_t i = 0; i < len; i++) out->items[out->len++] = scr_str_char_at(text, (double)i); + return true; + } + ScrDyn *array = value->kind == SCR_DYN_TYPED_REF && scr_dyn_ref_is_array(value) + ? scr_dyn_typed_ref_materialize(value) + : value->kind == SCR_DYN_ARR ? scr_dyn_retain((ScrDyn *)value) : NULL; + if (!array) { + scr_dyn_arg_type_fail(name, "of type string", value); + return false; + } + size_t len = array->v.arr.len; + out->items = malloc((len ? len : 1) * sizeof *out->items); + if (!out->items) scr_trap("scriptc: out of memory\n"); + for (size_t i = 0; i < len; i++) { + const ScrDyn *item = array->v.arr.presence && !array->v.arr.presence[i] ? scr_dyn_undefined() + : array->v.arr.items[i]; + if (item->kind != SCR_DYN_STR) { + char element[64]; + snprintf(element, sizeof element, "%s[%zu]", name, i); + scr_dyn_arg_type_fail(element, "of type string", item); + scr_dyn_release(array); + uc_diff_seq_free(out); + return false; + } + out->items[out->len++] = scr_str_retain(item->v.str); + } + scr_dyn_release(array); + return true; +} + +static void uc_diff_push(ScrDyn *result, int op, ScrStr *item) { + ScrDyn *entry = scr_dyn_new_arr(); + scr_dyn_arr_push(entry, scr_dyn_new_num((double)op)); + scr_dyn_arr_push(entry, scr_dyn_new_str(item)); + scr_dyn_arr_push(result, entry); +} + +ScrDyn *scr_util_diff(ScrDyn *actual, ScrDyn *expected) { + if (scr_dyn_strict_eq(actual, expected)) return scr_dyn_new_arr(); + UcDiffSeq a, e; + if (!uc_diff_input(actual, "actual", &a)) return NULL; + if (!uc_diff_input(expected, "expected", &e)) { + uc_diff_seq_free(&a); + return NULL; + } + size_t max = a.len + e.len; + if (max > 2147483647u) { + uc_diff_seq_free(&a); + uc_diff_seq_free(&e); + char message[160]; + int n = snprintf(message, sizeof message, + "The value of \"myersDiff input size\" is out of range. It must be < 2^31. Received %zu", max); + scr_throw_error_msg_code(SCR_ERR_RANGE, message, (size_t)n, "ERR_OUT_OF_RANGE"); + return NULL; + } + /* Two distinct empty sequences: Node's myersDiff reads past its + * one-element state, finds no end and answers undefined, which the + * ArrayPrototypeReverse of util.diff rejects. The trace below would be read + * out of bounds. */ + if (max == 0) { + uc_diff_seq_free(&a); + uc_diff_seq_free(&e); + static const char message[] = "Cannot convert undefined or null to object"; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + return NULL; + } + size_t width = 2 * max + 1; + int32_t *v = calloc(width, sizeof *v); + int32_t **trace = malloc((max + 1) * sizeof *trace); + if (!v || !trace) scr_trap("scriptc: out of memory\n"); + size_t levels = 0; + bool done = false; + for (size_t level = 0; level <= max && !done; level++) { + trace[levels] = malloc(width * sizeof **trace); + if (!trace[levels]) scr_trap("scriptc: out of memory\n"); + memcpy(trace[levels], v, width * sizeof *v); + levels++; + long d = (long)level; + for (long k = -d; k <= d; k += 2) { + size_t offset = (size_t)(k + (long)max); + int32_t previous = offset > 0 ? v[offset - 1] : 0; + int32_t next = offset + 1 < width ? v[offset + 1] : 0; + long x = k == -d || (k != d && previous < next) ? next : (long)previous + 1; + long y = x - k; + while ((size_t)x < a.len && y >= 0 && (size_t)y < e.len && scr_str_eq(a.items[x], e.items[y])) { + x++; + y++; + } + v[offset] = (int32_t)x; + if ((size_t)x >= a.len && y >= 0 && (size_t)y >= e.len) { + done = true; + break; + } + } + } + /* backtrack: the operations from the end, which the answer reverses. */ + ScrDyn *reversed = scr_dyn_new_arr(); + long x = (long)a.len, y = (long)e.len; + for (long level = (long)levels - 1; level >= 0; level--) { + const int32_t *row = trace[level]; + long k = x - y; + size_t offset = (size_t)(k + (long)max); + long prev_k; + if (k == -level || (k != level && row[offset - 1] < row[offset + 1])) prev_k = k + 1; + else prev_k = k - 1; + long prev_x = row[(size_t)(prev_k + (long)max)]; + long prev_y = prev_x - prev_k; + while (x > prev_x && y > prev_y) { + uc_diff_push(reversed, 0, a.items[x - 1]); + x--; + y--; + } + if (level > 0) { + if (x > prev_x) uc_diff_push(reversed, 1, a.items[--x]); + else uc_diff_push(reversed, -1, e.items[--y]); + } + } + for (size_t i = 0; i < levels; i++) free(trace[i]); + free(trace); + free(v); + ScrDyn *result = scr_dyn_new_arr(); + for (size_t i = reversed->v.arr.len; i-- > 0;) scr_dyn_arr_push(result, scr_dyn_retain(reversed->v.arr.items[i])); + scr_dyn_release(reversed); + uc_diff_seq_free(&a); + uc_diff_seq_free(&e); + return result; +} diff --git a/packages/runtime/src/scr_util_timing.c b/packages/runtime/src/scr_util_timing.c new file mode 100644 index 000000000..13651f5ff --- /dev/null +++ b/packages/runtime/src/scr_util_timing.c @@ -0,0 +1,1062 @@ +/* node:util's debounce and throttle over the checked-dynamic tree, step + * for step with Node's lib/internal/util/debounce.js and throttle.js. + * Each returns a native dyn function (an ordinary function with its own + * prototype, fn's name and length) whose own cancel/flush/ref/unref + * members (throttle: cancel/hasImmediateCapacity/ref/unref) and + * non-enumerable pending/pendingCount (throttle: and activeCount) getters + * share one refcounted state. Calls answer dyn promises. + * + * The state is invisible to the cycle collector, so nothing it holds + * leads back to it: fn sits in a traced box on the returned function's + * closure (fn may refer to that function), the state reaches the function through + * a weak pointer that the function's closure clears when it is freed; an + * armed timer and a throttled call's settle reaction hold the function + * and the state until they run or are cleared; and the options.signal + * 'abort' listener holds a weak slot that the state clears when it is + * freed. The listener stays registered, as in Node. The clock is the + * monotonic loop clock in whole milliseconds, like libuv's uv_now. All + * arguments are borrowed. */ +#include "scr_runtime.h" + +#include +#include +#include +#include + +/* Node's TIMEOUT_MAX (2 ** 31 - 1) and Number.MAX_SAFE_INTEGER. */ +#define UT_TIMEOUT_MAX 2147483647.0 +#define UT_SAFE_MAX 9007199254740991.0 + +typedef struct UtState UtState; + +/* The abort listener's view of a state: NULL once the state is freed. */ +typedef struct { + size_t rc; + UtState *state; +} UtWeak; + +/* The returned function's closure holds its state through this token; + * freeing the closure frees the token, which clears the state's weak + * pointer to the function. */ +typedef struct { + size_t rc; + UtState *state; +} UtOwner; + +/* One pending call: its promise and, for throttle, its arguments. */ +typedef struct { + ScrPromise *promise; + ScrDyn **args; + size_t argc; +} UtCall; + +struct UtState { + size_t rc; + bool throttle; + ScrClosure *self; /* the returned function's closure (weak), or NULL */ + ScrDynThunk thunk; + ScrDyn *signal; /* NULL without options.signal */ + UtWeak *weak; /* the abort listener's slot, or NULL */ + ScrDyn *abort_error; /* NULL until the signal aborts */ + double timeout; /* the armed timer's handle; 0 for none */ + bool refed; + /* pendingCalls: the live calls are [head, len). */ + UtCall *calls; + size_t head, len, cap; + /* debounce */ + double wait; + bool leading, reject_on_cancel; + ScrDyn **args; /* the latest call's arguments */ + size_t argc; + /* throttle */ + double limit, interval, concurrency, max_pending, active; + bool drop, strict; + double *ticks; /* strictTicks: the live ticks are [tick_head, tick_len) */ + size_t tick_head, tick_len, tick_cap; + double window_count, window_start; + bool window_open; +}; + +static const char UT_SIG[] = "native:util.debounce"; + +static void *ut_oom(void) { + scr_trap("scriptc: out of memory\n"); +} + +static ScrDyn **ut_args_copy(ScrDyn *const *args, size_t argc) { + if (argc == 0) return NULL; + ScrDyn **copy = malloc(argc * sizeof *copy); + if (!copy) return ut_oom(); + for (size_t i = 0; i < argc; i++) copy[i] = scr_dyn_retain(args[i]); + return copy; +} + +static void ut_args_free(ScrDyn **args, size_t argc) { + for (size_t i = 0; i < argc; i++) scr_dyn_release(args[i]); + free(args); +} + +static void *ut_weak_retain(void *p) { + ((UtWeak *)p)->rc++; + return p; +} + +static void ut_weak_release(void *p) { + UtWeak *weak = p; + if (weak && --weak->rc == 0) free(weak); +} + +static void *ut_state_retain(void *p) { + ((UtState *)p)->rc++; + return p; +} + +static void ut_state_release(void *p) { + UtState *s = p; + if (!s || --s->rc) return; + if (s->weak) { + s->weak->state = NULL; + ut_weak_release(s->weak); + } + scr_dyn_release(s->signal); + scr_dyn_release(s->abort_error); + ut_args_free(s->args, s->argc); + for (size_t i = s->head; i < s->len; i++) { + scr_promise_release(s->calls[i].promise); + ut_args_free(s->calls[i].args, s->calls[i].argc); + } + free(s->calls); + free(s->ticks); + free(s); +} + +static void *ut_owner_retain(void *p) { + ((UtOwner *)p)->rc++; + return p; +} + +static void ut_owner_release(void *p) { + UtOwner *owner = p; + if (--owner->rc) return; + owner->state->self = NULL; + ut_state_release(owner->state); + free(owner); +} + +static ScrBox *ut_state_box(UtState *s) { + ScrBox *box = scr_box_new_obj(ut_state_retain, ut_state_release, NULL); + scr_box_set_ref(box, ut_state_retain(s)); + return box; +} + +static ScrBox *ut_dyn_box(ScrDyn *value /* moves in */) { + ScrBox *box = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v); + scr_box_set_ref(box, value); + return box; +} + +/* The state behind a member, getter, timer or listener closure (+1). */ +static UtState *ut_cap_state(ScrClosure *closure) { + return scr_box_get_ref(closure->caps[0]); +} + +/* The state behind the returned function's own closure (+1). */ +static UtState *ut_owner_state(ScrClosure *closure) { + UtOwner *owner = scr_box_get_ref(closure->caps[0]); + UtState *s = ut_state_retain(owner->state); + ut_owner_release(owner); + return s; +} + +static ScrDyn *ut_box_fn(UtState *s, ScrClosure *closure) { + return scr_dyn_new_func(scr_closure_retain(closure), s->thunk, 0, UT_SIG, s->throttle ? "throttled" : "debounced"); +} + +/* The returned function (+1), or NULL once nothing else holds it. */ +static ScrDyn *ut_self(UtState *s) { + return s->self ? ut_box_fn(s, s->self) : NULL; +} + +static void ut_fence(const char *message) { + scr_throw_error_msg_code(SCR_ERR_ERROR, message, strlen(message), "SC2020"); +} + +static double ut_now(void) { + return floor(scr_now_ms()); +} + +/* ── promises and errors ───────────────────────────────────────────── */ + +static void ut_reject(ScrPromise *p, const ScrDyn *error) { + scr_dyn_throw(scr_dyn_retain((ScrDyn *)error)); + scr_promise_reject_pending(p); +} + +static ScrDyn *ut_promise_box(ScrPromise *p /* moves in */) { + ScrDyn *box = scr_dyn_new_promise(p); + scr_promise_release(p); + return box; +} + +/* PromiseReject(error) (+1). */ +static ScrDyn *ut_rejected(const ScrDyn *error) { + ScrPromise *p = scr_promise_new(); + ut_reject(p, error); + return ut_promise_box(p); +} + +/* new AbortError(message, { cause }) (+1); a NULL cause defines none. */ +static ScrDyn *ut_abort_error(const char *message, const ScrDyn *cause) { + ScrStr *text = scr_str_new(message, strlen(message)); + ScrError *e = scr_error_new(SCR_ERR_ERROR, text); + scr_str_release(text); + scr_str_release(e->name); + e->name = scr_str_new("AbortError", 10); + scr_error_set_code(e, "ABORT_ERR"); + e->name_present = true; + e->name_enumerable = true; + if (cause) { + /* The constructor's { cause } option: a non-enumerable own property. */ + scr_error_define_cause(e, (ScrDyn *)cause); + e->cause_enumerable = false; + } + ScrDyn *error = scr_dyn_from_error(e); + scr_error_release(e); + return error; +} + +/* cancel(reason)'s error: no cause for an undefined reason (+1). */ +static ScrDyn *ut_cancel_error(ScrDyn *const *args, size_t argc) { + const ScrDyn *reason = argc ? args[0] : scr_dyn_undefined(); + return ut_abort_error("The operation was aborted", reason->kind == SCR_DYN_UNDEF ? NULL : reason); +} + +/* fn.apply(self, args): the result (+1), or NULL with fn's throw pending. + * fn lives on the returned function's closure. */ +static ScrDyn *ut_apply(const ScrDyn *self, ScrDyn *const *args, size_t argc) { + ScrDyn *fn = scr_box_get_ref(scr_closure_identity(self->v.fn.clo)->caps[1]); + scr_dyn_this_push_dyn(self); + ScrDyn *result = scr_dyn_call(fn, args, argc, "fn"); + scr_dyn_this_pop(); + scr_dyn_release(fn); + return result; +} + +/* ── the signal ────────────────────────────────────────────────────── */ + +static bool ut_signal_aborted(const UtState *s) { + if (!s->signal || !scr_et_signal_core_fn) return false; + bool aborted = false; + (void)scr_et_signal_core_fn(s->signal, &aborted); + return aborted; +} + +static void ut_cancel_with_error(UtState *s, const ScrDyn *error); + +/* abort(): the first abort builds abortError from signal.reason. */ +static void ut_abort(UtState *s) { + if (!s->abort_error) { + ScrStr *key = scr_str_new("reason", 6); + ScrDyn *reason = scr_dyn_handle_key_get(s->signal, key); + scr_str_release(key); + if (!reason) return; + s->abort_error = ut_abort_error("The operation was aborted", reason->kind == SCR_DYN_UNDEF ? NULL : reason); + scr_dyn_release(reason); + } + ut_cancel_with_error(s, s->abort_error); +} + +static ScrDyn *ut_abort_listener(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)args; + (void)argc; + UtWeak *weak = scr_box_get_ref(closure->caps[0]); + UtState *s = weak->state ? ut_state_retain(weak->state) : NULL; + ut_weak_release(weak); + if (s) { + ut_abort(s); + ut_state_release(s); + } + return scr_exc_pending() ? NULL : scr_dyn_retain(scr_dyn_undefined()); +} + +/* addAbortListener(signal, abort). */ +static bool ut_listen(UtState *s) { + UtWeak *weak = malloc(sizeof *weak); + if (!weak) ut_oom(); + weak->rc = 1; + weak->state = s; + s->weak = weak; + ScrClosure *closure = scr_closure_new(NULL, 1); + closure->caps[0] = scr_box_new_obj(ut_weak_retain, ut_weak_release, NULL); + scr_box_set_ref(closure->caps[0], ut_weak_retain(weak)); + ScrDyn *listener = scr_dyn_new_func(closure, ut_abort_listener, 0, UT_SIG, "abort"); + ScrDyn *disposable = scr_events_add_abort_listener(s->signal, listener); + scr_dyn_release(listener); + scr_dyn_release(disposable); + return !scr_exc_pending(); +} + +/* ── pending calls ─────────────────────────────────────────────────── */ + +static size_t ut_pending_count(const UtState *s) { + return s->len - s->head; +} + +static void ut_push(UtState *s, ScrPromise *promise /* moves in */, ScrDyn **args /* moves in */, size_t argc) { + /* Calls already taken free their slots, so a queue that never empties + * keeps its storage within twice its live calls. */ + if (s->len == s->cap && s->head > 0) { + s->len -= s->head; + memmove(s->calls, s->calls + s->head, s->len * sizeof *s->calls); + s->head = 0; + } + if (s->len == s->cap) { + size_t cap = s->cap ? s->cap * 2 : 4; + UtCall *grown = realloc(s->calls, cap * sizeof *grown); + if (!grown) ut_oom(); + s->calls = grown; + s->cap = cap; + } + s->calls[s->len++] = (UtCall){promise, args, argc}; +} + +/* Takes the pending calls, leaving none: *count live calls at the result + * (free it, the promises and arguments when done). */ +static UtCall *ut_take_calls(UtState *s, size_t *count) { + UtCall *calls = s->calls; + size_t head = s->head; + *count = s->len - head; + if (head) memmove(calls, calls + head, *count * sizeof *calls); + s->calls = NULL; + s->head = s->len = s->cap = 0; + return calls; +} + +static void ut_calls_free(UtCall *calls, size_t count) { + for (size_t i = 0; i < count; i++) { + scr_promise_release(calls[i].promise); + ut_args_free(calls[i].args, calls[i].argc); + } + free(calls); +} + +/* rejectPending(error). */ +static void ut_reject_pending(UtState *s, const ScrDyn *error) { + size_t count; + UtCall *calls = ut_take_calls(s, &count); + for (size_t i = 0; i < count; i++) ut_reject(calls[i].promise, error); + ut_calls_free(calls, count); +} + +static void ut_clear_timer(UtState *s) { + double timeout = s->timeout; + s->timeout = 0; + scr_clear_interval(timeout); +} + +static void ut_arm(UtState *s, const ScrDyn *self, double delay, void (*fire)(ScrClosure *)) { + ScrClosure *timer = scr_closure_new((void *)fire, 2); + timer->caps[0] = ut_state_box(s); + timer->caps[1] = ut_dyn_box(scr_dyn_retain((ScrDyn *)self)); + s->timeout = scr_set_timeout_handle(timer, delay); + if (!s->refed) scr_timer_unref(s->timeout); +} + +/* The timer's state (+1) and function (+1). */ +static UtState *ut_timer_state(ScrClosure *timer, ScrDyn **self) { + *self = scr_box_get_ref(timer->caps[1]); + return ut_cap_state(timer); +} + +/* ── throttle's limiter ────────────────────────────────────────────── */ + +static void th_reset_limiter(UtState *s) { + s->tick_head = s->tick_len = 0; + s->window_count = 0; + s->window_open = false; +} + +static void th_prune_ticks(UtState *s, double now) { + while (s->tick_head < s->tick_len && now - s->ticks[s->tick_head] >= s->interval) s->tick_head++; + if (s->tick_head == s->tick_len) s->tick_head = s->tick_len = 0; +} + +static bool th_has_capacity(UtState *s, double now) { + if (s->strict) { + th_prune_ticks(s, now); + return (double)(s->tick_len - s->tick_head) < s->limit; + } + if (!s->window_open || now - s->window_start >= s->interval) { + s->window_count = 0; + s->window_start = now; + s->window_open = true; + } + return s->window_count < s->limit; +} + +static void th_reserve(UtState *s, double now) { + if (!s->strict) { + s->window_count++; + return; + } + /* Ticks that left the window free their slots: at most limit ticks are + * live, so the storage stays within twice that. */ + if (s->tick_len == s->tick_cap && s->tick_head > 0) { + s->tick_len -= s->tick_head; + memmove(s->ticks, s->ticks + s->tick_head, s->tick_len * sizeof *s->ticks); + s->tick_head = 0; + } + if (s->tick_len == s->tick_cap) { + size_t cap = s->tick_cap ? s->tick_cap * 2 : 8; + double *grown = realloc(s->ticks, cap * sizeof *grown); + if (!grown) ut_oom(); + s->ticks = grown; + s->tick_cap = cap; + } + s->ticks[s->tick_len++] = now; +} + +static void ut_cancel_with_error(UtState *s, const ScrDyn *error) { + if (!s->throttle) { + /* debounce's cancelWithError */ + if (s->timeout == 0) return; + ut_clear_timer(s); + ut_args_free(s->args, s->argc); + s->args = NULL; + s->argc = 0; + ut_reject_pending(s, error); + return; + } + if (s->timeout != 0) ut_clear_timer(s); + th_reset_limiter(s); + ut_reject_pending(s, error); +} + +/* ── debounce ──────────────────────────────────────────────────────── */ + +/* invoke(callArgs, calls): every call settles with fn's one result. */ +static void db_invoke(UtState *s, const ScrDyn *self, ScrDyn *const *args, size_t argc, UtCall *calls, size_t count) { + if (ut_signal_aborted(s)) { + ut_abort(s); + if (!s->abort_error) return; + for (size_t i = 0; i < count; i++) ut_reject(calls[i].promise, s->abort_error); + return; + } + ScrDyn *result = ut_apply(self, args, argc); + if (!result) { + scr_promise_reject_pending(calls[0].promise); + ScrDyn *error = scr_promise_reason_dyn(calls[0].promise); + for (size_t i = 1; i < count; i++) ut_reject(calls[i].promise, error); + scr_dyn_release(error); + return; + } + for (size_t i = 0; i < count; i++) scr_promise_resolve_dyn(calls[i].promise, scr_dyn_retain(result)); + scr_dyn_release(result); +} + +static void db_invoke_pending(UtState *s, const ScrDyn *self) { + ScrDyn **args = s->args; + size_t argc = s->argc; + s->args = NULL; + s->argc = 0; + size_t count; + UtCall *calls = ut_take_calls(s, &count); + if (count) db_invoke(s, self, args, argc, calls, count); + ut_calls_free(calls, count); + ut_args_free(args, argc); +} + +static void db_on_timeout(ScrClosure *timer) { + ScrDyn *self; + UtState *s = ut_timer_state(timer, &self); + s->timeout = 0; + db_invoke_pending(s, self); + scr_dyn_release(self); + ut_state_release(s); +} + +static ScrDyn *db_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + UtState *s = ut_owner_state(closure); + ScrDyn *self = ut_box_fn(s, closure); + ScrDyn *out = NULL; + if (ut_signal_aborted(s)) ut_abort(s); + if (scr_exc_pending()) goto done; + if (s->abort_error) { + out = ut_rejected(s->abort_error); + goto done; + } + bool invoke_now = s->leading && s->timeout == 0; + if (s->timeout != 0) { + if (ut_pending_count(s) != 0) { + scr_promise_mark_handled(s->calls[s->len - 1].promise); + if (s->reject_on_cancel) { + ScrDyn *error = ut_abort_error("The debounced call was superseded", NULL); + ut_reject_pending(s, error); + scr_dyn_release(error); + } + } + scr_timer_refresh(s->timeout); + } else { + ut_arm(s, self, s->wait, db_on_timeout); + } + if (ut_signal_aborted(s)) { + ut_abort(s); + if (s->abort_error) out = ut_rejected(s->abort_error); + goto done; + } + ScrPromise *promise = scr_promise_new(); + if (invoke_now) { + UtCall call = {promise, NULL, 0}; + db_invoke(s, self, args, argc, &call, 1); + } else { + ut_push(s, scr_promise_retain(promise), NULL, 0); + ut_args_free(s->args, s->argc); + s->args = ut_args_copy(args, argc); + s->argc = argc; + } + out = ut_promise_box(promise); +done: + scr_dyn_release(self); + ut_state_release(s); + return out; +} + +static ScrDyn *db_flush(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)args; + (void)argc; + UtState *s = ut_cap_state(closure); + if (ut_pending_count(s) != 0) { + /* The armed timer holds the function until it is cleared. */ + ScrDyn *self = ut_self(s); + ut_clear_timer(s); + db_invoke_pending(s, self); + scr_dyn_release(self); + } + ut_state_release(s); + return scr_exc_pending() ? NULL : scr_dyn_retain(scr_dyn_undefined()); +} + +/* ── throttle ──────────────────────────────────────────────────────── */ + +static void th_drain(UtState *s, const ScrDyn *self); + +static ScrDyn *th_on_settled(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)args; + (void)argc; + ScrDyn *self; + UtState *s = ut_timer_state(closure, &self); + s->active--; + th_drain(s, self); + scr_dyn_release(self); + ut_state_release(s); + return scr_exc_pending() ? NULL : scr_dyn_retain(scr_dyn_undefined()); +} + +/* invoke(call): the call adopts PromiseResolve(fn's result), and the + * active count drops when that promise settles. Moves the call in. */ +static void th_invoke(UtState *s, const ScrDyn *self, UtCall call) { + s->active++; + ScrDyn *result = ut_apply(self, call.args, call.argc); + ScrPromise *settled; + if (!result) { + settled = scr_promise_new(); + scr_promise_reject_pending(settled); + } else if (scr_dyn_promise_of(result)) { + settled = scr_promise_retain(scr_dyn_promise_of(result)); + scr_dyn_release(result); + } else { + settled = scr_promise_new(); + scr_promise_resolve_dyn(settled, result); + } + scr_promise_resolve_dyn(call.promise, scr_dyn_new_promise(settled)); + ScrClosure *reaction = scr_closure_new(NULL, 2); + reaction->caps[0] = ut_state_box(s); + reaction->caps[1] = ut_dyn_box(scr_dyn_retain((ScrDyn *)self)); + ScrDyn *on_settled = scr_dyn_new_func(reaction, th_on_settled, 0, UT_SIG, "onSettled"); + scr_dyn_release(scr_dyn_promise_then(settled, on_settled, on_settled, NULL)); + scr_dyn_release(on_settled); + scr_promise_release(settled); + scr_promise_release(call.promise); + ut_args_free(call.args, call.argc); +} + +static void th_on_timeout(ScrClosure *timer); + +static void th_schedule(UtState *s, const ScrDyn *self) { + if (s->timeout != 0 || ut_pending_count(s) == 0 || s->active >= s->concurrency) return; + double now = ut_now(); + if (th_has_capacity(s, now)) { + th_drain(s, self); + return; + } + double delay = s->strict ? s->ticks[s->tick_head] + s->interval - now : s->window_start + s->interval - now; + ut_arm(s, self, delay, th_on_timeout); +} + +static void th_drain(UtState *s, const ScrDyn *self) { + if (ut_signal_aborted(s)) { + ut_abort(s); + return; + } + while (ut_pending_count(s) != 0 && s->active < s->concurrency) { + double now = ut_now(); + if (!th_has_capacity(s, now)) break; + UtCall call = s->calls[s->head++]; + if (s->head == s->len) s->head = s->len = 0; + th_reserve(s, now); + th_invoke(s, self, call); + } + th_schedule(s, self); +} + +static void th_on_timeout(ScrClosure *timer) { + ScrDyn *self; + UtState *s = ut_timer_state(timer, &self); + s->timeout = 0; + th_drain(s, self); + scr_dyn_release(self); + ut_state_release(s); +} + +/* drop(): a handled rejection with ERR_THROTTLED (+1). */ +static ScrDyn *th_drop(void) { + static const char message[] = "The call was dropped by the throttled function"; + ScrStr *text = scr_str_new(message, sizeof message - 1); + ScrError *e = scr_error_new(SCR_ERR_ERROR, text); + scr_str_release(text); + scr_error_set_code(e, "ERR_THROTTLED"); + ScrDyn *error = scr_dyn_from_error(e); + scr_error_release(e); + ScrPromise *p = scr_promise_new(); + ut_reject(p, error); + scr_dyn_release(error); + scr_promise_mark_handled(p); + return ut_promise_box(p); +} + +static ScrDyn *th_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + UtState *s = ut_owner_state(closure); + ScrDyn *self = ut_box_fn(s, closure); + ScrDyn *out = NULL; + if (ut_signal_aborted(s)) ut_abort(s); + if (scr_exc_pending()) goto done; + if (s->abort_error) { + out = ut_rejected(s->abort_error); + goto done; + } + double now = ut_now(); + bool can_invoke = ut_pending_count(s) == 0 && s->active < s->concurrency && th_has_capacity(s, now); + if (!can_invoke && (s->drop || (double)ut_pending_count(s) >= s->max_pending)) { + out = th_drop(); + goto done; + } + ScrPromise *promise = scr_promise_new(); + out = scr_dyn_new_promise(promise); + UtCall call = {promise, ut_args_copy(args, argc), argc}; + if (can_invoke) { + th_reserve(s, now); + th_invoke(s, self, call); + } else { + ut_push(s, call.promise, call.args, call.argc); + th_schedule(s, self); + } +done: + scr_dyn_release(self); + ut_state_release(s); + return out; +} + +static ScrDyn *th_has_immediate_capacity(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)args; + (void)argc; + UtState *s = ut_cap_state(closure); + bool capacity = false; + if (!ut_signal_aborted(s) && ut_pending_count(s) == 0 && s->active < s->concurrency) { + double now = ut_now(); + if (s->strict) { + size_t index = s->tick_head; + while (index < s->tick_len && now - s->ticks[index] >= s->interval) index++; + capacity = (double)(s->tick_len - index) < s->limit; + } else { + capacity = !s->window_open || now - s->window_start >= s->interval || s->window_count < s->limit; + } + } + ut_state_release(s); + return scr_dyn_new_bool(capacity); +} + +/* ── the shared members ────────────────────────────────────────────── */ + +static ScrDyn *ut_cancel(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + UtState *s = ut_cap_state(closure); + if (s->throttle || s->timeout != 0) { + ScrDyn *error = ut_cancel_error(args, argc); + ut_cancel_with_error(s, error); + scr_dyn_release(error); + } + ut_state_release(s); + return scr_dyn_retain(scr_dyn_undefined()); +} + +static ScrDyn *ut_set_ref(ScrClosure *closure, bool refed) { + UtState *s = ut_cap_state(closure); + s->refed = refed; + if (s->timeout != 0) { + if (refed) scr_timer_ref(s->timeout); + else scr_timer_unref(s->timeout); + } + ScrDyn *self = ut_self(s); + if (!self) + ut_fence(s->throttle ? "util.throttle ref() and unref() after the throttled function is unreachable have no lowering" + : "util.debounce ref() and unref() after the debounced function is unreachable have no lowering"); + ut_state_release(s); + return self; +} + +static ScrDyn *ut_ref(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)args; + (void)argc; + return ut_set_ref(closure, true); +} + +static ScrDyn *ut_unref(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)args; + (void)argc; + return ut_set_ref(closure, false); +} + +static ScrDyn *ut_pending(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)args; + (void)argc; + UtState *s = ut_cap_state(closure); + ScrDyn *out = ut_pending_count(s) == 0 ? scr_dyn_new_null() : scr_dyn_new_promise(s->calls[s->len - 1].promise); + ut_state_release(s); + return out; +} + +static ScrDyn *ut_pending_count_get(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)args; + (void)argc; + UtState *s = ut_cap_state(closure); + ScrDyn *out = scr_dyn_new_num((double)ut_pending_count(s)); + ut_state_release(s); + return out; +} + +static ScrDyn *th_active_count(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)args; + (void)argc; + UtState *s = ut_cap_state(closure); + ScrDyn *out = scr_dyn_new_num(s->active); + ut_state_release(s); + return out; +} + +/* ── construction ──────────────────────────────────────────────────── */ + +/* Object.defineProperty(target, key, descriptor); the descriptor moves in. */ +static bool ut_define(ScrDyn *target, const char *key, ScrDyn *descriptor) { + ScrStr *name = scr_str_new(key, strlen(key)); + ScrDyn *property = scr_dyn_new_str(name); + scr_str_release(name); + ScrDyn *defined = scr_dyn_define_property(target, property, descriptor); + scr_dyn_release(property); + scr_dyn_release(descriptor); + scr_dyn_release(defined); + return defined != NULL; +} + +static ScrDyn *ut_member_fn(UtState *s, ScrDynThunk thunk, uint32_t arity, const char *name) { + ScrClosure *closure = scr_closure_new(NULL, 1); + closure->caps[0] = ut_state_box(s); + return scr_dyn_new_func(closure, thunk, arity, UT_SIG, name); +} + +/* createFn(value): a writable, enumerable, configurable method. */ +static bool ut_define_method(ScrDyn *target, UtState *s, const char *key, ScrDynThunk thunk, uint32_t arity) { + ScrDyn *descriptor = scr_dyn_new_obj(); + scr_dyn_obj_set(descriptor, "configurable", 12, scr_dyn_new_bool(true)); + scr_dyn_obj_set(descriptor, "enumerable", 10, scr_dyn_new_bool(true)); + scr_dyn_obj_set(descriptor, "writable", 8, scr_dyn_new_bool(true)); + scr_dyn_obj_set(descriptor, "value", 5, ut_member_fn(s, thunk, arity, key)); + return ut_define(target, key, descriptor); +} + +static bool ut_define_getter(ScrDyn *target, UtState *s, const char *key, ScrDynThunk thunk, const char *name) { + ScrDyn *descriptor = scr_dyn_new_obj(); + scr_dyn_obj_set(descriptor, "enumerable", 10, scr_dyn_new_bool(false)); + scr_dyn_obj_set(descriptor, "get", 3, ut_member_fn(s, thunk, 0, name)); + return ut_define(target, key, descriptor); +} + +/* `length` and `name` take fn's values. */ +static bool ut_define_value(ScrDyn *target, const char *key, ScrDyn *value /* moves in */) { + ScrDyn *descriptor = scr_dyn_new_obj(); + scr_dyn_obj_set(descriptor, "configurable", 12, scr_dyn_new_bool(true)); + scr_dyn_obj_set(descriptor, "value", 5, value); + return ut_define(target, key, descriptor); +} + +/* `value[key]` over an options object (+1, or NULL with an exception + * pending). */ +static ScrDyn *ut_get(const ScrDyn *value, const char *key) { + size_t len = strlen(key); + ScrDyn *result = NULL; + if (value->kind == SCR_DYN_TYPED_REF) { + ScrDyn *view = scr_dyn_typed_ref_materialize(value); + if (scr_exc_pending()) { + scr_dyn_release(view); + return NULL; + } + result = ut_get(view, key); + scr_dyn_release(view); + return result; + } + if (value->kind == SCR_DYN_OBJ || value->kind == SCR_DYN_ARR || value->kind == SCR_DYN_PROXY) { + result = scr_dyn_obj_read(value, key, len); + } else if (value->kind == SCR_DYN_HANDLE) { + ScrStr *name = scr_str_new(key, len); + result = scr_dyn_handle_key_get(value, name); + scr_str_release(name); + return result; + } else { + return scr_dyn_retain(scr_dyn_undefined()); + } + if (!result && !scr_exc_pending()) result = scr_dyn_retain(scr_dyn_undefined()); + return result; +} + +static void ut_type_fail(const char *name, const char *expected, const ScrDyn *got) { + if (strchr(name, '.')) scr_dyn_prop_type_fail(name, expected, got); + else scr_dyn_arg_type_fail(name, expected, got); +} + +/* validateFunction(fn, 'fn'). */ +static bool ut_function_arg(const ScrDyn *fn, const char *api) { + if (fn->kind == SCR_DYN_PROXY && scr_dyn_is_callable(fn)) { + char message[96]; + snprintf(message, sizeof message, "util.%s over Proxy functions has no lowering", api); + ut_fence(message); + return false; + } + if (fn->kind != SCR_DYN_FUNC) { + ut_type_fail("fn", "of type function", fn); + return false; + } + return true; +} + +/* validateInteger(value, name, min, max). */ +static bool ut_integer(const ScrDyn *value, const char *name, double min, double max, double *out) { + if (value->kind != SCR_DYN_NUM) { + ut_type_fail(name, "of type number", value); + return false; + } + double v = value->v.num; + bool integer = isfinite(v) && trunc(v) == v; + if (integer && v >= min && v <= max) { + *out = v; + return true; + } + char received[48]; + if (v == 0 && signbit(v)) memcpy(received, "-0", 3); + else scr_num_received(v, received); + char range[64]; + if (integer) snprintf(range, sizeof range, ">= %.0f && <= %.0f", min, max); + else memcpy(range, "an integer", 11); + char message[256]; + int n = snprintf(message, sizeof message, "The value of \"%s\" is out of range. It must be %s. Received %s", name, + range, received); + scr_throw_error_msg_code(SCR_ERR_RANGE, message, (size_t)n, "ERR_OUT_OF_RANGE"); + return false; +} + +/* validateBoolean(value, name) over a defaulted option. */ +static bool ut_boolean(const ScrDyn *value, const char *name, bool *out) { + if (value->kind == SCR_DYN_UNDEF) { + *out = false; + return true; + } + if (value->kind != SCR_DYN_BOOL) { + ut_type_fail(name, "of type boolean", value); + return false; + } + *out = value->v.b; + return true; +} + +/* validateObject(options, 'options'), with undefined as the empty object. */ +static bool ut_options_object(const ScrDyn *options) { + if (options->kind == SCR_DYN_UNDEF) return true; + bool object = options->kind == SCR_DYN_OBJ || options->kind == SCR_DYN_HANDLE || options->kind == SCR_DYN_BYTES || + options->kind == SCR_DYN_PROMISE || (options->kind == SCR_DYN_PROXY && !scr_dyn_is_callable(options)); + if (options->kind == SCR_DYN_TYPED_REF) { + ScrDyn *view = scr_dyn_typed_ref_materialize(options); + if (!view) return false; + object = view->kind == SCR_DYN_OBJ; + scr_dyn_release(view); + } + if (!object) ut_type_fail("options", "of type object", options); + return object; +} + +/* validateAbortSignal(signal, 'options.signal'), then Node's throw for an + * already-aborted signal. Objects that pass Node's duck-typed check + * without being AbortSignal instances are refused. */ +static bool ut_signal_option(const ScrDyn *signal, const char *api) { + if (signal->kind == SCR_DYN_UNDEF) return true; + bool aborted = false; + if (scr_et_signal_core_fn && scr_et_signal_core_fn(signal, &aborted)) { + if (!aborted) return true; + ScrStr *key = scr_str_new("reason", 6); + ScrDyn *reason = scr_dyn_handle_key_get(signal, key); + scr_str_release(key); + if (!reason) return false; + ScrDyn *error = ut_abort_error("The operation was aborted", reason); + scr_dyn_release(reason); + scr_dyn_throw(error); + return false; + } + bool object = signal->kind == SCR_DYN_OBJ || signal->kind == SCR_DYN_TYPED_REF || signal->kind == SCR_DYN_HANDLE || + signal->kind == SCR_DYN_PROXY; + ScrStr *key = scr_str_new("aborted", 7); + bool duck = object && signal->kind != SCR_DYN_HANDLE && scr_dyn_in(signal, key); + scr_str_release(key); + if (scr_exc_pending()) return false; + if (duck) { + char message[112]; + snprintf(message, sizeof message, "util.%s options.signal values that are not AbortSignal instances have no lowering", + api); + ut_fence(message); + return false; + } + scr_dyn_prop_type_fail("options.signal", "an instance of AbortSignal", signal); + return false; +} + +static UtState *ut_state_new(bool throttle, ScrDyn *signal) { + UtState *s = calloc(1, sizeof *s); + if (!s) ut_oom(); + s->rc = 1; + s->throttle = throttle; + s->thunk = throttle ? th_call : db_call; + s->signal = signal->kind == SCR_DYN_UNDEF ? NULL : scr_dyn_retain(signal); + s->refed = true; + return s; +} + +/* The returned function, its members and getters, fn's length and name, + * and the abort listener. Consumes the caller's state reference. fn sits + * in a traced box on the function's closure, so a cycle through it (fn + * refers to the returned function) stays collectable. */ +static ScrDyn *ut_build(UtState *s, ScrDyn *wrapped) { + ScrClosure *closure = scr_closure_new(NULL, 2); + closure->function_kind = 4; /* an ordinary function with its own prototype */ + UtOwner *owner = malloc(sizeof *owner); + if (!owner) ut_oom(); + owner->rc = 1; + owner->state = s; /* the caller's reference */ + closure->caps[0] = scr_box_new_obj(ut_owner_retain, ut_owner_release, NULL); + scr_box_set_ref(closure->caps[0], owner); + closure->caps[1] = ut_dyn_box(scr_dyn_retain(wrapped)); + s->self = closure; + ScrDyn *fn = ut_box_fn(s, closure); + scr_closure_release(closure); + bool ok = ut_define_method(fn, s, "cancel", ut_cancel, 1); + if (s->throttle) { + ok = ok && ut_define_method(fn, s, "hasImmediateCapacity", th_has_immediate_capacity, 0); + } else { + ok = ok && ut_define_method(fn, s, "flush", db_flush, 0); + } + ok = ok && ut_define_method(fn, s, "ref", ut_ref, 0) && ut_define_method(fn, s, "unref", ut_unref, 0) && + ut_define_getter(fn, s, "pending", ut_pending, "get") && + ut_define_getter(fn, s, "pendingCount", ut_pending_count_get, s->throttle ? "pendingCount" : "get"); + if (s->throttle) ok = ok && ut_define_getter(fn, s, "activeCount", th_active_count, "get"); + if (ok) { + ScrDyn *length = scr_dyn_fn_get(wrapped, "length", 6); + ok = ut_define_value(fn, "length", length ? length : scr_dyn_retain(scr_dyn_undefined())); + } + if (ok) { + ScrDyn *name = scr_dyn_fn_get(wrapped, "name", 4); + ok = ut_define_value(fn, "name", name ? name : scr_dyn_retain(scr_dyn_undefined())); + } + if (ok && s->signal) ok = ut_listen(s); + if (!ok) { + scr_dyn_release(fn); + return NULL; + } + return fn; +} + +ScrDyn *scr_util_debounce(ScrDyn *fn, ScrDyn *wait, ScrDyn *options) { + double ms; + if (!ut_function_arg(fn, "debounce") || !ut_integer(wait, "wait", 0, UT_TIMEOUT_MAX, &ms) || + !ut_options_object(options)) + return NULL; + ScrDyn *leading = ut_get(options, "leading"); + ScrDyn *reject_on_cancel = leading ? ut_get(options, "rejectOnCancel") : NULL; + ScrDyn *signal = reject_on_cancel ? ut_get(options, "signal") : NULL; + bool lead = false, reject = false; + bool ok = signal && ut_boolean(leading, "options.leading", &lead) && + ut_boolean(reject_on_cancel, "options.rejectOnCancel", &reject) && ut_signal_option(signal, "debounce"); + ScrDyn *result = NULL; + if (ok) { + UtState *s = ut_state_new(false, signal); + s->wait = ms; + s->leading = lead; + s->reject_on_cancel = reject; + result = ut_build(s, fn); + } + scr_dyn_release(signal); + scr_dyn_release(reject_on_cancel); + scr_dyn_release(leading); + return result; +} + +/* `= Infinity` options: validateInteger(value, name, min) unless Infinity. */ +static bool ut_limit_option(const ScrDyn *value, const char *name, double min, double *out) { + if (value->kind == SCR_DYN_UNDEF || (value->kind == SCR_DYN_NUM && value->v.num == INFINITY)) { + *out = INFINITY; + return true; + } + return ut_integer(value, name, min, UT_SAFE_MAX, out); +} + +ScrDyn *scr_util_throttle(ScrDyn *fn, ScrDyn *limit, ScrDyn *interval, ScrDyn *options) { + double count, ms; + if (!ut_function_arg(fn, "throttle") || !ut_integer(limit, "limit", 1, UT_SAFE_MAX, &count) || + !ut_integer(interval, "interval", 0, UT_TIMEOUT_MAX, &ms) || !ut_options_object(options)) + return NULL; + static const char *const keys[] = {"concurrency", "maxPending", "overflow", "signal", "strict"}; + ScrDyn *values[5] = {NULL}; + bool ok = true; + for (size_t i = 0; i < 5 && ok; i++) ok = (values[i] = ut_get(options, keys[i])) != NULL; + double concurrency = INFINITY, max_pending = INFINITY; + bool drop = false, strict = false; + ok = ok && ut_limit_option(values[0], "options.concurrency", 1, &concurrency) && + ut_limit_option(values[1], "options.maxPending", 0, &max_pending); + if (ok) { + const ScrDyn *overflow = values[2]; + bool queue = overflow->kind == SCR_DYN_UNDEF || + (overflow->kind == SCR_DYN_STR && overflow->v.str->len == 5 && !memcmp(overflow->v.str->data, "queue", 5)); + drop = overflow->kind == SCR_DYN_STR && overflow->v.str->len == 4 && !memcmp(overflow->v.str->data, "drop", 4); + if (!queue && !drop) { + scr_dyn_arg_value_fail("options.overflow", "must be one of: 'queue', 'drop'", overflow); + ok = false; + } + } + ok = ok && ut_signal_option(values[3], "throttle") && ut_boolean(values[4], "options.strict", &strict); + ScrDyn *result = NULL; + if (ok) { + UtState *s = ut_state_new(true, values[3]); + s->limit = count; + s->interval = ms; + s->concurrency = concurrency; + s->max_pending = max_pending; + s->drop = drop; + s->strict = strict; + result = ut_build(s, fn); + } + for (size_t i = 0; i < 5; i++) scr_dyn_release(values[i]); + return result; +} diff --git a/packages/runtime/src/scr_webcrypto_pki.c b/packages/runtime/src/scr_webcrypto_pki.c index 7439a12db..458cfeae8 100644 --- a/packages/runtime/src/scr_webcrypto_pki.c +++ b/packages/runtime/src/scr_webcrypto_pki.c @@ -380,7 +380,8 @@ static ScrPromise *sw_generate_key(const ScrDyn *args) { scr_throw_domex("OperationError", "algorithm.publicExponent must be odd"); goto done; } - if (bits > 16384 || value < 3) { + /* The pki unit's numbers hold moduli up to 8192 bits. */ + if (bits > 8192 || value < 3) { scr_wc_unsupported("generateKey", "this RSA modulus length or public exponent"); goto done; } diff --git a/packages/runtime/src/scr_x509.c b/packages/runtime/src/scr_x509.c new file mode 100644 index 000000000..7b853d68a --- /dev/null +++ b/packages/runtime/src/scr_x509.c @@ -0,0 +1,1820 @@ +/* X509Certificate fields (raw, subject, issuer, subjectAltName, + * infoAccess, serialNumber, fingerprint256, fingerprint512, ca, + * signatureAlgorithm, signatureAlgorithmOid), linked only into programs + * that read them (the x509 runtime feature). */ +#include "scr_runtime.h" + +#include +#include +#include + +/* Copies of scr_lib.c's PEM front half and DER header reader for the + * X509Certificate members it keeps, so that unit is unchanged for + * programs that do not read these fields. */ + +/* PEM armor → DER (or raw-DER passthrough), the shared front half of + * every X509Certificate member. On success `*der`/`*der_len` hold the + * certificate bytes and `*decoded` the malloc'd base64 buffer to free + * (NULL for raw-DER input); unparseable input throws Node's exact PEM + * error and answers false with the exception pending. */ +static bool scr_x509_der(const uint8_t *in, size_t n, const uint8_t **der, size_t *der_len, + uint8_t **decoded) { + static const char begin[] = "-----BEGIN CERTIFICATE-----"; + static const char end[] = "-----END CERTIFICATE-----"; + *der = NULL; + *der_len = 0; + *decoded = NULL; + /* PEM: base64-decode the armor's body. */ + const uint8_t *b = NULL; + /* OpenSSL's PEM reader looks for the armor at the start of a line. */ + for (size_t i = 0; i + sizeof begin - 1 <= n; i++) { + if ((i == 0 || in[i - 1] == '\n') && memcmp(in + i, begin, sizeof begin - 1) == 0) { + b = in + i + sizeof begin - 1; + break; + } + } + if (b != NULL) { + const uint8_t *stop = in + n; + for (const uint8_t *p = b; p + sizeof end - 1 <= in + n; p++) { + if (memcmp(p, end, sizeof end - 1) == 0) { + stop = p; + break; + } + } + uint8_t *out = malloc(((size_t)(stop - b) / 4 + 1) * 3 + 3); + if (!out) { + scr_trap("scriptc: out of memory\n"); + } + size_t o = 0; + unsigned acc = 0; + int have = 0; + for (const uint8_t *p = b; p < stop; p++) { + int v; + uint8_t c = *p; + if (c >= 'A' && c <= 'Z') v = c - 'A'; + else if (c >= 'a' && c <= 'z') v = c - 'a' + 26; + else if (c >= '0' && c <= '9') v = c - '0' + 52; + else if (c == '+') v = 62; + else if (c == '/') v = 63; + else continue; /* whitespace, '=' */ + acc = (acc << 6) | (unsigned)v; + if (++have == 4) { + out[o++] = (uint8_t)(acc >> 16); + out[o++] = (uint8_t)(acc >> 8); + out[o++] = (uint8_t)acc; + acc = 0; + have = 0; + } + } + if (have == 2) out[o++] = (uint8_t)(acc >> 4); + else if (have == 3) { + out[o++] = (uint8_t)(acc >> 10); + out[o++] = (uint8_t)(acc >> 2); + } + *decoded = out; + *der = out; + *der_len = o; + return true; + } + if (n > 0 && in[0] == 0x30) { + /* Raw DER: the certificate SEQUENCE, without bytes after it, as + * d2i_X509 reads it. */ + *der = in; + *der_len = n; + if (n >= 2) { + size_t len = in[1], head = 2; + if (len & 0x80) { + size_t k = len & 0x7f; + len = 0; + head = 2 + k; + for (size_t i = 0; i < k && k <= 4 && head <= n; i++) len = (len << 8) | in[2 + i]; + if (k == 0 || k > 4) head = n + 1; + } + if (head <= n && len <= n - head) *der_len = head + len; + } + return true; + } + scr_throw_error_msg_code(SCR_ERR_ERROR, + "error:0480006C:PEM routines::no start line", + 42, "ERR_OSSL_PEM_NO_START_LINE"); + return false; /* exception pending */ +} + +/* Reads one DER TL header at p (before end): tag to *tag, content length + * to *len, and answers the content pointer (NULL on malformed/overlong). */ +static const uint8_t *scr_der_tl(const uint8_t *p, const uint8_t *end, uint8_t *tag, size_t *len) { + if (p == NULL || end - p < 2) return NULL; + *tag = p[0]; + uint8_t l0 = p[1]; + const uint8_t *content = p + 2; + size_t l; + if (l0 < 0x80) { + l = l0; + } else { + size_t nb = l0 & 0x7f; + if (nb == 0 || nb > sizeof(size_t) || end - content < (ptrdiff_t)nb) return NULL; + l = 0; + for (size_t i = 0; i < nb; i++) l = (l << 8) | content[i]; + content += nb; + } + if ((size_t)(end - content) < l) return NULL; + *tag = p[0]; + *len = l; + return content; +} + +/* ── X509Certificate fields over the DER ───────────────────────────────── + * + * `raw` decodes the constructor's input once (scr_x509_der); every other + * field reads those DER bytes. The walks follow RFC 5280's Certificate ::= + * SEQUENCE { tbsCertificate, signatureAlgorithm, signatureValue } and + * render each value as Node does: names through X509_NAME_print_ex (one + * attribute per line, RFC 2253 escaping), alternative names and access + * descriptions through Node's PrintGeneralName, the serial number through + * BN_bn2hex. A field whose part of the certificate is missing or malformed + * answers Node's value for an absent part. */ + +typedef struct { + char *data; + size_t len, cap; +} ScrX509Buf; + +static void scr_x509_put(ScrX509Buf *b, const void *p, size_t n) { + if (b->len + n > b->cap) { + size_t cap = b->cap ? b->cap * 2 : 128; + while (cap < b->len + n) cap *= 2; + char *grown = realloc(b->data, cap); + if (!grown) scr_trap("scriptc: out of memory\n"); + b->data = grown; + b->cap = cap; + } + memcpy(b->data + b->len, p, n); + b->len += n; +} + +static void scr_x509_puts(ScrX509Buf *b, const char *s) { scr_x509_put(b, s, strlen(s)); } + +static void scr_x509_putc(ScrX509Buf *b, char c) { scr_x509_put(b, &c, 1); } + +static ScrStr *scr_x509_buf_str(ScrX509Buf *b) { + ScrStr *s = scr_str_new(b->data ? b->data : "", b->len); + free(b->data); + return s; +} + +/* The DER of a certificate field's input (raw or the bytes constructor + * input). */ +#define SCR_X509_DER(bytes) \ + ((const uint8_t *)(bytes)->data), ((bytes)->len * scr_bytes_elem_size((bytes)->elem)) + +typedef struct { + const uint8_t *tbs, *tbs_end; /* tbsCertificate contents */ + const uint8_t *sig_alg; /* outer signatureAlgorithm contents */ + size_t sig_alg_len; + const uint8_t *serial; /* serialNumber contents */ + size_t serial_len; + const uint8_t *issuer, *subject; /* Name contents */ + size_t issuer_len, subject_len; + const uint8_t *validity; /* Validity contents */ + size_t validity_len; + const uint8_t *exts; /* Extensions SEQUENCE contents, or NULL */ + size_t exts_len; + const uint8_t *spki; /* SubjectPublicKeyInfo contents, or NULL */ + size_t spki_len; + const uint8_t *spki_tlv; /* the whole SubjectPublicKeyInfo DER */ + size_t spki_tlv_len; +} ScrX509Parts; + +/* Splits the certificate into the parts the fields read; false when the + * outer structure does not parse. */ +static bool scr_x509_parts(const uint8_t *der, size_t n, ScrX509Parts *out) { + memset(out, 0, sizeof *out); + const uint8_t *end = der + n; + uint8_t tag; + size_t len; + const uint8_t *cert = scr_der_tl(der, end, &tag, &len); + if (!cert || tag != 0x30) return false; + const uint8_t *cert_end = cert + len; + const uint8_t *tbs = scr_der_tl(cert, cert_end, &tag, &len); + if (!tbs || tag != 0x30) return false; + out->tbs = tbs; + out->tbs_end = tbs + len; + const uint8_t *alg = scr_der_tl(tbs + len, cert_end, &tag, &len); + if (alg && tag == 0x30) { + out->sig_alg = alg; + out->sig_alg_len = len; + } + const uint8_t *p = tbs; + const uint8_t *c = scr_der_tl(p, out->tbs_end, &tag, &len); + if (c && tag == 0xA0) { /* [0] version */ + p = c + len; + c = scr_der_tl(p, out->tbs_end, &tag, &len); + } + if (!c || tag != 0x02) return true; + out->serial = c; + out->serial_len = len; + p = c + len; + /* signature AlgorithmIdentifier */ + c = scr_der_tl(p, out->tbs_end, &tag, &len); + if (!c) return true; + p = c + len; + c = scr_der_tl(p, out->tbs_end, &tag, &len); + if (!c || tag != 0x30) return true; + out->issuer = c; + out->issuer_len = len; + p = c + len; + c = scr_der_tl(p, out->tbs_end, &tag, &len); + if (!c || tag != 0x30) return true; + out->validity = c; + out->validity_len = len; + p = c + len; + c = scr_der_tl(p, out->tbs_end, &tag, &len); + if (!c || tag != 0x30) return true; + out->subject = c; + out->subject_len = len; + p = c + len; + c = scr_der_tl(p, out->tbs_end, &tag, &len); + if (c && tag == 0x30) { + out->spki = c; + out->spki_len = len; + out->spki_tlv = p; + out->spki_tlv_len = (size_t)(c + len - p); + } + /* subjectPublicKeyInfo, then the optional [1] [2] unique IDs and [3] + * extensions */ + while (p < out->tbs_end) { + c = scr_der_tl(p, out->tbs_end, &tag, &len); + if (!c) break; + if (tag == 0xA3) { + const uint8_t *seq = scr_der_tl(c, c + len, &tag, &len); + if (seq && tag == 0x30) { + out->exts = seq; + out->exts_len = len; + } + break; + } + p = c + len; + } + return true; +} + +/* Finds the extension with the DER-encoded OID `oid` (contents only): + * its extnValue contents, or NULL. */ +static const uint8_t *scr_x509_ext(const ScrX509Parts *parts, const uint8_t *oid, size_t oid_len, + size_t *value_len) { + if (!parts->exts) return NULL; + const uint8_t *p = parts->exts; + const uint8_t *end = parts->exts + parts->exts_len; + uint8_t tag; + size_t len; + while (p < end) { + const uint8_t *ext = scr_der_tl(p, end, &tag, &len); + if (!ext || tag != 0x30) return NULL; + const uint8_t *ext_end = ext + len; + p = ext_end; + const uint8_t *id = scr_der_tl(ext, ext_end, &tag, &len); + if (!id || tag != 0x06) continue; + bool match = len == oid_len && memcmp(id, oid, len) == 0; + const uint8_t *q = id + len; + const uint8_t *v = scr_der_tl(q, ext_end, &tag, &len); + if (v && tag == 0x01) { /* critical */ + q = v + len; + v = scr_der_tl(q, ext_end, &tag, &len); + } + if (match && v && tag == 0x04) { + *value_len = len; + return v; + } + } + return NULL; +} + +/* An OID's dotted text (OBJ_obj2txt with no_name). */ +static void scr_x509_oid_dotted(ScrX509Buf *b, const uint8_t *oid, size_t n) { + char tmp[24]; + uint64_t v = 0; + bool first = true; + for (size_t i = 0; i < n; i++) { + v = (v << 7) | (oid[i] & 0x7f); + if (oid[i] & 0x80) continue; + if (first) { + unsigned top = v < 40 ? 0 : v < 80 ? 1 : 2; + snprintf(tmp, sizeof tmp, "%u.%llu", top, (unsigned long long)(v - top * 40)); + first = false; + } else { + snprintf(tmp, sizeof tmp, ".%llu", (unsigned long long)v); + } + scr_x509_puts(b, tmp); + v = 0; + } +} + +typedef struct { + const char *oid; /* dotted */ + const char *name; +} ScrX509OidName; + +/* OpenSSL's short names for the attribute types names use (XN_FLAG_FN_SN). */ +static const ScrX509OidName scr_x509_attr_names[] = { + {"2.5.4.3", "CN"}, + {"2.5.4.4", "SN"}, + {"2.5.4.5", "serialNumber"}, + {"2.5.4.6", "C"}, + {"2.5.4.7", "L"}, + {"2.5.4.8", "ST"}, + {"2.5.4.9", "street"}, + {"2.5.4.10", "O"}, + {"2.5.4.11", "OU"}, + {"2.5.4.12", "title"}, + {"2.5.4.13", "description"}, + {"2.5.4.15", "businessCategory"}, + {"2.5.4.16", "postalAddress"}, + {"2.5.4.17", "postalCode"}, + {"2.5.4.18", "postOfficeBox"}, + {"2.5.4.20", "telephoneNumber"}, + {"2.5.4.41", "name"}, + {"2.5.4.42", "GN"}, + {"2.5.4.43", "initials"}, + {"2.5.4.44", "generationQualifier"}, + {"2.5.4.45", "x500UniqueIdentifier"}, + {"2.5.4.46", "dnQualifier"}, + {"2.5.4.65", "pseudonym"}, + {"2.5.4.72", "role"}, + {"2.5.4.97", "organizationIdentifier"}, + {"1.2.840.113549.1.9.1", "emailAddress"}, + {"1.2.840.113549.1.9.2", "unstructuredName"}, + {"0.9.2342.19200300.100.1.1", "UID"}, + {"0.9.2342.19200300.100.1.25", "DC"}, + {"1.3.6.1.4.1.311.60.2.1.1", "jurisdictionL"}, + {"1.3.6.1.4.1.311.60.2.1.2", "jurisdictionST"}, + {"1.3.6.1.4.1.311.60.2.1.3", "jurisdictionC"}, +}; + +/* OpenSSL's long names for the signature algorithms certificates use. */ +static const ScrX509OidName scr_x509_sig_names[] = { + {"1.2.840.113549.1.1.4", "md5WithRSAEncryption"}, + {"1.2.840.113549.1.1.5", "sha1WithRSAEncryption"}, + {"1.2.840.113549.1.1.10", "rsassaPss"}, + {"1.2.840.113549.1.1.11", "sha256WithRSAEncryption"}, + {"1.2.840.113549.1.1.12", "sha384WithRSAEncryption"}, + {"1.2.840.113549.1.1.13", "sha512WithRSAEncryption"}, + {"1.2.840.113549.1.1.14", "sha224WithRSAEncryption"}, + {"1.2.840.10045.4.1", "ecdsa-with-SHA1"}, + {"1.2.840.10045.4.3.1", "ecdsa-with-SHA224"}, + {"1.2.840.10045.4.3.2", "ecdsa-with-SHA256"}, + {"1.2.840.10045.4.3.3", "ecdsa-with-SHA384"}, + {"1.2.840.10045.4.3.4", "ecdsa-with-SHA512"}, + {"1.2.840.10040.4.3", "dsaWithSHA1"}, + {"2.16.840.1.101.3.4.3.1", "dsa_with_SHA224"}, + {"2.16.840.1.101.3.4.3.2", "dsa_with_SHA256"}, + {"1.3.101.112", "ED25519"}, + {"1.3.101.113", "ED448"}, +}; + +/* OpenSSL's long names for the access methods infoAccess prints. */ +static const ScrX509OidName scr_x509_access_names[] = { + {"1.3.6.1.5.5.7.48.1", "OCSP"}, + {"1.3.6.1.5.5.7.48.2", "CA Issuers"}, + {"1.3.6.1.5.5.7.48.3", "AD Time Stamping"}, + {"1.3.6.1.5.5.7.48.5", "CA Repository"}, +}; + +/* Appends the table name of an OID, or its dotted text; answers whether + * the table named it. */ +static bool scr_x509_oid_name(ScrX509Buf *b, const uint8_t *oid, size_t n, + const ScrX509OidName *table, size_t count) { + ScrX509Buf dotted = {0}; + scr_x509_oid_dotted(&dotted, oid, n); + for (size_t i = 0; i < count; i++) { + if (strlen(table[i].oid) == dotted.len && memcmp(table[i].oid, dotted.data, dotted.len) == 0) { + scr_x509_puts(b, table[i].name); + free(dotted.data); + return true; + } + } + scr_x509_put(b, dotted.data, dotted.len); + free(dotted.data); + return false; +} + +/* Appends one code point as UTF-8. */ +static void scr_x509_put_utf8(ScrX509Buf *b, uint32_t c) { + char u[4]; + size_t n; + if (c < 0x80) { + u[0] = (char)c; + n = 1; + } else if (c < 0x800) { + u[0] = (char)(0xC0 | (c >> 6)); + u[1] = (char)(0x80 | (c & 0x3F)); + n = 2; + } else if (c < 0x10000) { + u[0] = (char)(0xE0 | (c >> 12)); + u[1] = (char)(0x80 | ((c >> 6) & 0x3F)); + u[2] = (char)(0x80 | (c & 0x3F)); + n = 3; + } else { + u[0] = (char)(0xF0 | (c >> 18)); + u[1] = (char)(0x80 | ((c >> 12) & 0x3F)); + u[2] = (char)(0x80 | ((c >> 6) & 0x3F)); + u[3] = (char)(0x80 | (c & 0x3F)); + n = 4; + } + scr_x509_put(b, u, n); +} + +/* An attribute value as UTF-8 bytes (ASN1_STRFLGS_UTF8_CONVERT): a + * UTF8String as stored, BMPString and UniversalString decoded, every other + * type read as Latin-1. */ +static void scr_x509_value_utf8(ScrX509Buf *out, uint8_t tag, const uint8_t *v, size_t n) { + if (tag == 0x0C) { + scr_x509_put(out, v, n); + } else if (tag == 0x1E) { + for (size_t i = 0; i + 1 < n; i += 2) scr_x509_put_utf8(out, ((uint32_t)v[i] << 8) | v[i + 1]); + } else if (tag == 0x1C) { + for (size_t i = 0; i + 3 < n; i += 4) + scr_x509_put_utf8(out, ((uint32_t)v[i] << 24) | ((uint32_t)v[i + 1] << 16) | + ((uint32_t)v[i + 2] << 8) | v[i + 3]); + } else { + for (size_t i = 0; i < n; i++) scr_x509_put_utf8(out, v[i]); + } +} + +/* Appends a value with ASN1_STRFLGS_ESC_2253 escaping: the characters + * `"+,;<>\` get a backslash, as do a leading '#' or space and a trailing + * space; with `esc_ctrl`, control characters print as \XX. Bytes above + * 0x7f pass through. */ +static void scr_x509_put_escaped(ScrX509Buf *b, const uint8_t *s, size_t n, bool esc_ctrl) { + static const char hex[] = "0123456789ABCDEF"; + for (size_t i = 0; i < n; i++) { + uint8_t c = s[i]; + bool first = i == 0; + bool last = i + 1 == n; + if (c == '"' || c == '+' || c == ',' || c == ';' || c == '<' || c == '>' || c == '\\' || + (first && (c == '#' || c == ' ')) || (last && c == ' ')) { + scr_x509_putc(b, '\\'); + scr_x509_putc(b, (char)c); + } else if (esc_ctrl && (c < 0x20 || c == 0x7f)) { + char e[3] = {'\\', hex[c >> 4], hex[c & 0x0f]}; + scr_x509_put(b, e, 3); + } else { + scr_x509_putc(b, (char)c); + } + } +} + +/* Prints a Name. `multiline` is Node's subject/issuer form (attributes in + * order, one RDN per line, ESC_CTRL); otherwise the RFC 2253 form DirName + * alternative names use (RDNs reversed and comma-separated, unknown + * attribute types dumped as #hex of the value's DER). RDNs with several + * attributes join them with " + " or "+". */ +static void scr_x509_name_print(ScrX509Buf *b, const uint8_t *name, size_t n, bool multiline) { + const uint8_t *end = name + n; + const uint8_t *rdns[64]; + size_t rdn_lens[64]; + size_t count = 0; + uint8_t tag; + size_t len; + for (const uint8_t *p = name; p < end && count < 64;) { + const uint8_t *set = scr_der_tl(p, end, &tag, &len); + if (!set || tag != 0x31) break; + rdns[count] = set; + rdn_lens[count++] = len; + p = set + len; + } + for (size_t k = 0; k < count; k++) { + size_t r = multiline ? k : count - 1 - k; + if (k > 0) scr_x509_puts(b, multiline ? "\n" : ","); + const uint8_t *set_end = rdns[r] + rdn_lens[r]; + bool first_attr = true; + for (const uint8_t *q = rdns[r]; q < set_end;) { + const uint8_t *atv = scr_der_tl(q, set_end, &tag, &len); + if (!atv || tag != 0x30) break; + const uint8_t *atv_end = atv + len; + q = atv_end; + const uint8_t *oid = scr_der_tl(atv, atv_end, &tag, &len); + if (!oid || tag != 0x06) continue; + size_t oid_len = len; + const uint8_t *value_tl = oid + len; + uint8_t vtag; + size_t vlen; + const uint8_t *value = scr_der_tl(value_tl, atv_end, &vtag, &vlen); + if (!value) continue; + if (!first_attr) scr_x509_puts(b, multiline ? " + " : "+"); + first_attr = false; + bool known = scr_x509_oid_name(b, oid, oid_len, scr_x509_attr_names, + sizeof scr_x509_attr_names / sizeof scr_x509_attr_names[0]); + scr_x509_putc(b, '='); + if (!multiline && !known) { + static const char hex[] = "0123456789ABCDEF"; + scr_x509_putc(b, '#'); + for (const uint8_t *d = value_tl; d < value + vlen; d++) { + scr_x509_putc(b, hex[*d >> 4]); + scr_x509_putc(b, hex[*d & 0x0f]); + } + continue; + } + ScrX509Buf text = {0}; + scr_x509_value_utf8(&text, vtag, value, vlen); + scr_x509_put_escaped(b, (const uint8_t *)text.data, text.len, multiline); + free(text.data); + } + } +} + +/* Node's PrintAltName: a name with none of `"\,'` and only printable + * characters (in UTF-8 names, only non-control bytes) prints as is; + * otherwise it prints as a JSON string, `prefix:` inside the quotes, with + * `\\`, `\"` and \u00XX for commas, control characters and (outside + * UTF-8 names) bytes above 0x7e. */ +static void scr_x509_put_alt_name(ScrX509Buf *b, const uint8_t *s, size_t n, bool utf8, + const char *prefix) { + bool safe = true; + for (size_t i = 0; i < n && safe; i++) { + uint8_t c = s[i]; + if (c == '"' || c == '\\' || c == ',' || c == '\'') safe = false; + else if (utf8 ? (c < ' ' || c == 0x7f) : (c < ' ' || c > '~')) safe = false; + } + if (safe) { + if (prefix) { + scr_x509_puts(b, prefix); + scr_x509_putc(b, ':'); + } + scr_x509_put(b, s, n); + return; + } + static const char hex[] = "0123456789abcdef"; + scr_x509_putc(b, '"'); + if (prefix) { + scr_x509_puts(b, prefix); + scr_x509_putc(b, ':'); + } + for (size_t i = 0; i < n; i++) { + uint8_t c = s[i]; + if (c == '\\') { + scr_x509_puts(b, "\\\\"); + } else if (c == '"') { + scr_x509_puts(b, "\\\""); + } else if ((c >= ' ' && c != ',' && c <= '~') || (utf8 && (c & 0x80))) { + scr_x509_putc(b, (char)c); + } else { + char u[6] = {'\\', 'u', '0', '0', hex[c >> 4], hex[c & 0x0f]}; + scr_x509_put(b, u, 6); + } + } + scr_x509_putc(b, '"'); +} + +static const uint8_t SCR_X509_OID_UPN[] = {0x2B, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x14, 0x02, 0x03}; +static const uint8_t SCR_X509_OID_XMPP[] = {0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x08, 0x05}; +static const uint8_t SCR_X509_OID_SRV[] = {0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x08, 0x07}; +static const uint8_t SCR_X509_OID_NAI[] = {0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x08, 0x08}; +static const uint8_t SCR_X509_OID_SMTP_UTF8[] = {0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x08, 0x09}; + +/* Node's PrintGeneralName for one GeneralName element (tag and contents). */ +static void scr_x509_general_name(ScrX509Buf *b, uint8_t tag, const uint8_t *v, size_t n) { + switch (tag) { + case 0x81: /* rfc822Name */ + scr_x509_puts(b, "email:"); + scr_x509_put_alt_name(b, v, n, false, NULL); + return; + case 0x82: /* dNSName */ + scr_x509_puts(b, "DNS:"); + scr_x509_put_alt_name(b, v, n, false, NULL); + return; + case 0x86: /* uniformResourceIdentifier */ + scr_x509_puts(b, "URI:"); + scr_x509_put_alt_name(b, v, n, false, NULL); + return; + case 0x87: { /* iPAddress */ + char tmp[48]; + scr_x509_puts(b, "IP Address:"); + if (n == 4) { + snprintf(tmp, sizeof tmp, "%d.%d.%d.%d", v[0], v[1], v[2], v[3]); + scr_x509_puts(b, tmp); + } else if (n == 16) { + for (unsigned j = 0; j < 8; j++) { + snprintf(tmp, sizeof tmp, j == 0 ? "%X" : ":%X", (unsigned)((v[2 * j] << 8) | v[2 * j + 1])); + scr_x509_puts(b, tmp); + } + } else { + snprintf(tmp, sizeof tmp, "", n); + scr_x509_puts(b, tmp); + } + return; + } + case 0x88: /* registeredID */ + scr_x509_puts(b, "Registered ID:"); + scr_x509_oid_dotted(b, v, n); + return; + case 0xA4: { /* directoryName: [4] EXPLICIT Name */ + uint8_t ntag; + size_t nlen; + const uint8_t *name = scr_der_tl(v, v + n, &ntag, &nlen); + ScrX509Buf text = {0}; + if (name && ntag == 0x30) scr_x509_name_print(&text, name, nlen, false); + scr_x509_puts(b, "DirName:"); + scr_x509_put_alt_name(b, (const uint8_t *)(text.data ? text.data : ""), text.len, true, NULL); + free(text.data); + return; + } + case 0xA0: { /* otherName: type-id OID, [0] EXPLICIT value */ + uint8_t t; + size_t len; + const uint8_t *end = v + n; + const uint8_t *oid = scr_der_tl(v, end, &t, &len); + const char *prefix = NULL; + bool unicode = true; + if (oid && t == 0x06) { +#define SCR_X509_OID_IS(o) (len == sizeof(o) && memcmp(oid, (o), sizeof(o)) == 0) + if (SCR_X509_OID_IS(SCR_X509_OID_SMTP_UTF8)) prefix = "SmtpUTF8Mailbox"; + else if (SCR_X509_OID_IS(SCR_X509_OID_XMPP)) prefix = "XmppAddr"; + else if (SCR_X509_OID_IS(SCR_X509_OID_SRV)) { + prefix = "SRVName"; + unicode = false; + } else if (SCR_X509_OID_IS(SCR_X509_OID_UPN)) prefix = "UPN"; + else if (SCR_X509_OID_IS(SCR_X509_OID_NAI)) prefix = "NAIRealm"; +#undef SCR_X509_OID_IS + } + const uint8_t *value = NULL; + size_t value_len = 0; + uint8_t vtag = 0; + if (oid && prefix) { + const uint8_t *wrap = scr_der_tl(oid + len, end, &t, &len); + if (wrap && t == 0xA0) value = scr_der_tl(wrap, wrap + len, &vtag, &value_len); + } + if (!value || vtag != (unicode ? 0x0C : 0x16)) { + scr_x509_puts(b, "othername:"); + } else { + scr_x509_puts(b, "othername:"); + scr_x509_put_alt_name(b, value, value_len, unicode, prefix); + } + return; + } + case 0xA3: + scr_x509_puts(b, "X400Name:"); + return; + case 0xA5: + scr_x509_puts(b, "EdiPartyName:"); + return; + default: + return; + } +} + +/* The DER contents of the TBSCertificate parts, or an empty string when + * the certificate does not parse. */ +static ScrStr *scr_x509_name_field(ScrBytes *der, bool subject) { + ScrX509Parts parts; + ScrX509Buf b = {0}; + if (scr_x509_parts(SCR_X509_DER(der), &parts)) { + const uint8_t *name = subject ? parts.subject : parts.issuer; + if (name) scr_x509_name_print(&b, name, subject ? parts.subject_len : parts.issuer_len, true); + } + return scr_x509_buf_str(&b); +} + +/* new X509Certificate(data).raw: the certificate's DER as a fresh Buffer. + * Unparseable input throws Node's PEM error (exception pending, NULL). */ +static ScrBytes *scr_x509_raw(const uint8_t *in, size_t n) { + const uint8_t *der; + size_t der_len; + uint8_t *decoded; + if (!scr_x509_der(in, n, &der, &der_len, &decoded)) return NULL; + ScrBytes *out = scr_buffer_alloc_unsafe_slow((double)der_len); + memcpy(out->data, der, der_len); + free(decoded); + return out; +} + +ScrBytes *scr_crypto_x509_raw(ScrBytes *data) { + SCR_BYTES_SNAPSHOT(data); + return scr_x509_raw(SCR_X509_DER(data)); +} + +/* raw of a certificate whose DER a TLS handshake delivered: a copy, with + * no search for PEM armor that its extensions could carry. */ +ScrBytes *scr_crypto_x509_raw_der(ScrBytes *der) { + SCR_BYTES_SNAPSHOT(der); + size_t n = (size_t)scr_bytes_byte_len(der); + ScrBytes *out = scr_buffer_alloc_unsafe_slow((double)n); + memcpy(out->data, der->data, n); + return out; +} + +ScrBytes *scr_crypto_x509_raw_str(ScrStr *pem) { + return scr_x509_raw((const uint8_t *)pem->data, pem->len); +} + +ScrStr *scr_crypto_x509_subject(ScrBytes *der) { return scr_x509_name_field(der, true); } + +ScrStr *scr_crypto_x509_issuer(ScrBytes *der) { return scr_x509_name_field(der, false); } + +static const uint8_t SCR_X509_OID_SAN[] = {0x55, 0x1D, 0x11}; +static const uint8_t SCR_X509_OID_BASIC_CONSTRAINTS[] = {0x55, 0x1D, 0x13}; +static const uint8_t SCR_X509_OID_KEY_USAGE[] = {0x55, 0x1D, 0x0F}; +static const uint8_t SCR_X509_OID_AIA[] = {0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x01, 0x01}; + +/* subjectAltName: the GeneralNames joined with ", "; an empty string when + * the certificate has no subjectAltName extension (Node's undefined). */ +ScrStr *scr_crypto_x509_subject_alt_name(ScrBytes *der) { + ScrX509Parts parts; + ScrX509Buf b = {0}; + size_t n; + const uint8_t *ext = scr_x509_parts(SCR_X509_DER(der), &parts) + ? scr_x509_ext(&parts, SCR_X509_OID_SAN, sizeof SCR_X509_OID_SAN, &n) + : NULL; + uint8_t tag; + size_t len; + const uint8_t *names = ext ? scr_der_tl(ext, ext + n, &tag, &len) : NULL; + if (names && tag == 0x30) { + const uint8_t *end = names + len; + bool first = true; + for (const uint8_t *p = names; p < end;) { + const uint8_t *v = scr_der_tl(p, end, &tag, &len); + if (!v) break; + if (!first) scr_x509_puts(&b, ", "); + first = false; + scr_x509_general_name(&b, tag, v, len); + p = v + len; + } + } + return scr_x509_buf_str(&b); +} + +/* infoAccess: one " - " line per AccessDescription; + * an empty string when the extension is absent (Node's undefined). */ +ScrStr *scr_crypto_x509_info_access(ScrBytes *der) { + ScrX509Parts parts; + ScrX509Buf b = {0}; + size_t n; + const uint8_t *ext = scr_x509_parts(SCR_X509_DER(der), &parts) + ? scr_x509_ext(&parts, SCR_X509_OID_AIA, sizeof SCR_X509_OID_AIA, &n) + : NULL; + uint8_t tag; + size_t len; + const uint8_t *descs = ext ? scr_der_tl(ext, ext + n, &tag, &len) : NULL; + if (descs && tag == 0x30) { + const uint8_t *end = descs + len; + bool first = true; + for (const uint8_t *p = descs; p < end;) { + const uint8_t *desc = scr_der_tl(p, end, &tag, &len); + if (!desc || tag != 0x30) break; + const uint8_t *desc_end = desc + len; + p = desc_end; + const uint8_t *method = scr_der_tl(desc, desc_end, &tag, &len); + if (!method || tag != 0x06) continue; + if (!first) scr_x509_putc(&b, '\n'); + first = false; + scr_x509_oid_name(&b, method, len, scr_x509_access_names, + sizeof scr_x509_access_names / sizeof scr_x509_access_names[0]); + scr_x509_puts(&b, " - "); + const uint8_t *location = scr_der_tl(method + len, desc_end, &tag, &len); + if (location) scr_x509_general_name(&b, tag, location, len); + } + } + return scr_x509_buf_str(&b); +} + +/* serialNumber: BN_bn2hex of the INTEGER ("0" for zero, whole bytes in + * uppercase hex without leading zero bytes, '-' before a negative). */ +ScrStr *scr_crypto_x509_serial_number(ScrBytes *der) { + ScrX509Parts parts; + ScrX509Buf b = {0}; + if (scr_x509_parts(SCR_X509_DER(der), &parts) && parts.serial_len > 0) { + static const char hex[] = "0123456789ABCDEF"; + size_t n = parts.serial_len; + uint8_t *mag = malloc(n); + if (!mag) scr_trap("scriptc: out of memory\n"); + memcpy(mag, parts.serial, n); + bool neg = (mag[0] & 0x80) != 0; + if (neg) { /* two's complement magnitude */ + for (size_t i = 0; i < n; i++) mag[i] = (uint8_t)~mag[i]; + for (size_t i = n; i-- > 0;) { + if (++mag[i] != 0) break; + } + } + size_t i = 0; + while (i < n && mag[i] == 0) i++; + if (i == n) { + scr_x509_putc(&b, '0'); + } else { + if (neg) scr_x509_putc(&b, '-'); + for (; i < n; i++) { + scr_x509_putc(&b, hex[mag[i] >> 4]); + scr_x509_putc(&b, hex[mag[i] & 0x0f]); + } + } + free(mag); + } + return scr_x509_buf_str(&b); +} + +static ScrStr *scr_x509_fingerprint_alg(ScrBytes *der, const char *alg) { + unsigned char digest[64]; + size_t dn = scr_crypto_digest_raw(alg, SCR_X509_DER(der), digest); + static const char hex[] = "0123456789ABCDEF"; + char buf[192]; + size_t o = 0; + for (size_t i = 0; i < dn; i++) { + if (i > 0) buf[o++] = ':'; + buf[o++] = hex[digest[i] >> 4]; + buf[o++] = hex[digest[i] & 0x0f]; + } + return scr_str_new(buf, o); +} + +ScrStr *scr_crypto_x509_fingerprint256(ScrBytes *der) { + return scr_x509_fingerprint_alg(der, "sha256"); +} + +ScrStr *scr_crypto_x509_fingerprint512(ScrBytes *der) { + return scr_x509_fingerprint_alg(der, "sha512"); +} + +/* ca: X509_check_ca() == 1 — basicConstraints with cA TRUE, and a + * keyUsage extension, when present, that allows keyCertSign. */ +bool scr_crypto_x509_ca(ScrBytes *der) { + ScrX509Parts parts; + if (!scr_x509_parts(SCR_X509_DER(der), &parts)) return false; + uint8_t tag; + size_t len, n; + const uint8_t *ku = scr_x509_ext(&parts, SCR_X509_OID_KEY_USAGE, sizeof SCR_X509_OID_KEY_USAGE, &n); + if (ku) { + const uint8_t *bits = scr_der_tl(ku, ku + n, &tag, &len); + if (!bits || tag != 0x03 || len < 2 || (bits[1] & 0x04) == 0) return false; + } + const uint8_t *bc = scr_x509_ext(&parts, SCR_X509_OID_BASIC_CONSTRAINTS, + sizeof SCR_X509_OID_BASIC_CONSTRAINTS, &n); + if (!bc) return false; + const uint8_t *seq = scr_der_tl(bc, bc + n, &tag, &len); + if (!seq || tag != 0x30) return false; + const uint8_t *flag = scr_der_tl(seq, seq + len, &tag, &len); + return flag && tag == 0x01 && len == 1 && flag[0] != 0; +} + +/* The signature AlgorithmIdentifier's OID contents, or NULL. */ +static const uint8_t *scr_x509_sig_oid(ScrBytes *der, size_t *n) { + ScrX509Parts parts; + if (!scr_x509_parts(SCR_X509_DER(der), &parts) || !parts.sig_alg) return NULL; + uint8_t tag; + const uint8_t *oid = scr_der_tl(parts.sig_alg, parts.sig_alg + parts.sig_alg_len, &tag, n); + return oid && tag == 0x06 ? oid : NULL; +} + +/* signatureAlgorithm: OpenSSL's long name for the signature algorithm; an + * empty string for an algorithm OpenSSL does not name (Node's + * undefined). */ +ScrStr *scr_crypto_x509_signature_algorithm(ScrBytes *der) { + size_t n; + const uint8_t *oid = scr_x509_sig_oid(der, &n); + ScrX509Buf b = {0}; + if (oid && !scr_x509_oid_name(&b, oid, n, scr_x509_sig_names, + sizeof scr_x509_sig_names / sizeof scr_x509_sig_names[0])) + b.len = 0; + return scr_x509_buf_str(&b); +} + +/* signatureAlgorithmOid: the signature algorithm's dotted OID. */ +ScrStr *scr_crypto_x509_signature_algorithm_oid(ScrBytes *der) { + size_t n; + const uint8_t *oid = scr_x509_sig_oid(der, &n); + ScrX509Buf b = {0}; + if (oid) scr_x509_oid_dotted(&b, oid, n); + return scr_x509_buf_str(&b); +} + +/* toString() and toJSON(): PEM_write_bio_X509's armor over the DER, 64 + * base64 characters a line. */ +ScrStr *scr_crypto_x509_pem(ScrBytes *der) { + static const char digits[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + const uint8_t *d = (const uint8_t *)der->data; + size_t n = der->len * scr_bytes_elem_size(der->elem); + ScrX509Buf b = {0}; + scr_x509_puts(&b, "-----BEGIN CERTIFICATE-----\n"); + size_t col = 0; + for (size_t i = 0; i < n; i += 3) { + uint32_t v = (uint32_t)d[i] << 16; + if (i + 1 < n) v |= (uint32_t)d[i + 1] << 8; + if (i + 2 < n) v |= d[i + 2]; + char q[4] = {digits[(v >> 18) & 63], digits[(v >> 12) & 63], i + 1 < n ? digits[(v >> 6) & 63] : '=', + i + 2 < n ? digits[v & 63] : '='}; + for (int k = 0; k < 4; k++) { + scr_x509_putc(&b, q[k]); + if (++col == 64) { + scr_x509_putc(&b, '\n'); + col = 0; + } + } + } + if (col) scr_x509_putc(&b, '\n'); + scr_x509_puts(&b, "-----END CERTIFICATE-----\n"); + return scr_x509_buf_str(&b); +} + +/* ── checkHost, checkEmail and checkIP ───────────────────────────────── */ + +/* X509_CHECK_FLAG_* as Node's getFlags sets them. */ +enum { + SCR_X509_ALWAYS_CHECK_SUBJECT = 0x1, + SCR_X509_NO_WILDCARDS = 0x2, + SCR_X509_NO_PARTIAL_WILDCARDS = 0x4, + SCR_X509_MULTI_LABEL_WILDCARDS = 0x8, + SCR_X509_SINGLE_LABEL_SUBDOMAINS = 0x10, + SCR_X509_NEVER_CHECK_SUBJECT = 0x20, +}; + +/* Node's getFlags(options): validateObject, then the subject string and + * the four booleans, in Node's order. -1 with the error pending. */ +static int scr_x509_check_flags(const ScrDyn *options) { + if (options == NULL || options->kind == SCR_DYN_UNDEF) return 0; + if (options->kind == SCR_DYN_TYPED_REF) { + ScrDyn *value = scr_dyn_typed_ref_materialize((ScrDyn *)options); + if (!value) return -1; + int flags = scr_x509_check_flags(value); + scr_dyn_release(value); + return flags; + } + if (options->kind != SCR_DYN_OBJ) { + scr_dyn_arg_type_fail("options", "of type object", options); + return -1; + } + const ScrDyn *subject = scr_dyn_obj_get(options, "subject", 7); + static const char *const bool_names[] = {"wildcards", "partialWildcards", "multiLabelWildcards", + "singleLabelSubdomains"}; + static const bool bool_defaults[] = {true, true, false, false}; + bool values[4]; + if (subject != NULL && subject->kind != SCR_DYN_UNDEF && subject->kind != SCR_DYN_STR) { + scr_dyn_prop_type_fail("options.subject", "of type string", subject); + return -1; + } + for (size_t i = 0; i < 4; i++) { + const ScrDyn *v = scr_dyn_obj_get(options, bool_names[i], strlen(bool_names[i])); + if (v == NULL || v->kind == SCR_DYN_UNDEF) { + values[i] = bool_defaults[i]; + continue; + } + if (v->kind != SCR_DYN_BOOL) { + char name[48]; + snprintf(name, sizeof name, "options.%s", bool_names[i]); + scr_dyn_prop_type_fail(name, "of type boolean", v); + return -1; + } + values[i] = v->v.b; + } + int flags = 0; + if (subject != NULL && subject->kind == SCR_DYN_STR) { + const ScrStr *s = subject->v.str; + if (s->len == 6 && memcmp(s->data, "always", 6) == 0) flags |= SCR_X509_ALWAYS_CHECK_SUBJECT; + else if (s->len == 5 && memcmp(s->data, "never", 5) == 0) flags |= SCR_X509_NEVER_CHECK_SUBJECT; + else if (!(s->len == 7 && memcmp(s->data, "default", 7) == 0)) { + scr_dyn_arg_value_fail("options.subject", NULL, subject); + return -1; + } + } + if (!values[0]) flags |= SCR_X509_NO_WILDCARDS; + if (!values[1]) flags |= SCR_X509_NO_PARTIAL_WILDCARDS; + if (values[2]) flags |= SCR_X509_MULTI_LABEL_WILDCARDS; + if (values[3]) flags |= SCR_X509_SINGLE_LABEL_SUBDOMAINS; + return flags; +} + +static bool scr_x509_ieq(const uint8_t *a, const uint8_t *b, size_t n) { + for (size_t i = 0; i < n; i++) { + uint8_t x = a[i], y = b[i]; + if (x >= 'A' && x <= 'Z') x = (uint8_t)(x + 32); + if (y >= 'A' && y <= 'Z') y = (uint8_t)(y + 32); + if (x != y) return false; + } + return true; +} + +/* OpenSSL's equal_nocase after skip_prefix: a host that starts with '.' + * compares with the name's equal-length suffix when the characters + * skipped hold no NUL (and, under singleLabelSubdomains, no dot). */ +static bool scr_x509_host_equal(const uint8_t *pattern, size_t plen, const uint8_t *host, + size_t hlen, int flags) { + if (hlen > 1 && host[0] == '.') { + const uint8_t *p = pattern; + size_t n = plen; + while (n > hlen && *p) { + if ((flags & SCR_X509_SINGLE_LABEL_SUBDOMAINS) && *p == '.') break; + p++; + n--; + } + if (n == hlen) { + pattern = p; + plen = n; + } + } + return plen == hlen && memchr(pattern, 0, plen) == NULL && scr_x509_ieq(pattern, host, hlen); +} + +static bool scr_x509_label_char(uint8_t c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9'); +} + +/* OpenSSL's valid_star: the '*' of a wildcard pattern, or NULL when the + * pattern is not a valid wildcard (the star outside the first label, in + * an IDNA label, partial when partial wildcards are off, or fewer than two + * labels after it). */ +static const uint8_t *scr_x509_valid_star(const uint8_t *p, size_t len, int flags) { + const uint8_t *star = NULL; + int dots = 0; + bool atstart = true, idna = false, first = true; + for (size_t i = 0; i < len; i++) { + if (p[i] == '*') { + bool atend = i == len - 1 || p[i + 1] == '.'; + if (star != NULL || !first || idna) return NULL; + if ((flags & SCR_X509_NO_PARTIAL_WILDCARDS) && (!atstart || !atend)) return NULL; + if (!atstart && !atend) return NULL; + star = &p[i]; + atstart = false; + } else if (p[i] == '.') { + if (atstart) return NULL; + idna = false; + dots++; + atstart = true; + first = false; + } else if (p[i] == '-' || scr_x509_label_char(p[i])) { + if (atstart && len - i >= 4 && scr_x509_ieq(&p[i], (const uint8_t *)"xn--", 4)) idna = true; + atstart = false; + } else { + return NULL; + } + } + if (star == NULL || dots < 2 || atstart) return NULL; + return star; +} + +/* OpenSSL's wildcard_match: prefix and suffix around the star match the + * host case-insensitively, the star covers at least one character, never + * a dot unless multi-label wildcards are on, and never an IDNA label. */ +static bool scr_x509_wildcard_match(const uint8_t *prefix, size_t prefix_len, + const uint8_t *suffix, size_t suffix_len, + const uint8_t *host, size_t hlen, int flags) { + if (hlen < prefix_len + suffix_len) return false; + if (!scr_x509_ieq(prefix, host, prefix_len)) return false; + const uint8_t *wild_start = host + prefix_len; + const uint8_t *wild_end = host + hlen - suffix_len; + if (!scr_x509_ieq(wild_end, suffix, suffix_len)) return false; + if (prefix_len == 0 && suffix_len > 0 && *suffix == '.') { + if (wild_start == wild_end) return false; + if (wild_end - wild_start > 4 && scr_x509_ieq(wild_start, (const uint8_t *)"xn--", 4)) + return false; + } + bool allow_multi = (flags & SCR_X509_MULTI_LABEL_WILDCARDS) != 0; + for (const uint8_t *p = wild_start; p != wild_end; p++) { + if (!scr_x509_label_char(*p) && *p != '-' && !(*p == '.' && allow_multi)) return false; + } + return true; +} + +static bool scr_x509_host_match(const uint8_t *pattern, size_t plen, const uint8_t *host, + size_t hlen, int flags) { + /* a host that starts with '.' matches a wildcard only as a suffix */ + if (!(flags & SCR_X509_NO_WILDCARDS) && !(hlen > 1 && host[0] == '.')) { + const uint8_t *star = scr_x509_valid_star(pattern, plen, flags); + if (star != NULL) + return scr_x509_wildcard_match(pattern, (size_t)(star - pattern), star + 1, + plen - (size_t)(star - pattern) - 1, host, hlen, flags); + } + return scr_x509_host_equal(pattern, plen, host, hlen, flags); +} + +/* OpenSSL's equal_email: the local part as is, the domain case- + * insensitively. */ +static bool scr_x509_email_match(const uint8_t *pattern, size_t plen, const uint8_t *email, + size_t elen) { + if (plen != elen || memchr(pattern, 0, plen) != NULL) return false; + size_t at = elen; + while (at > 0 && email[at - 1] != '@') at--; + if (at == 0) return scr_x509_ieq(pattern, email, elen); + return memcmp(pattern, email, at) == 0 && scr_x509_ieq(pattern + at, email + at, elen - at); +} + +enum { SCR_X509_GEN_EMAIL = 0x81, SCR_X509_GEN_DNS = 0x82, SCR_X509_GEN_IP = 0x87 }; + +/* do_x509_check over the subjectAltName entries of one type, then the + * subject's CN (hosts) or emailAddress (emails) entries when the flags + * allow. Writes the matching certificate name into *out. */ +static bool scr_x509_check(ScrBytes *der, uint8_t gen, const uint8_t *chk, size_t chk_len, + int flags, ScrX509Buf *out) { + ScrX509Parts parts; + if (!scr_x509_parts(SCR_X509_DER(der), &parts)) return false; + bool san_present = false; + size_t n; + const uint8_t *ext = scr_x509_ext(&parts, SCR_X509_OID_SAN, sizeof SCR_X509_OID_SAN, &n); + uint8_t tag; + size_t len; + const uint8_t *names = ext ? scr_der_tl(ext, ext + n, &tag, &len) : NULL; + if (names && tag == 0x30) { + const uint8_t *end = names + len; + for (const uint8_t *p = names; p < end;) { + const uint8_t *v = scr_der_tl(p, end, &tag, &len); + if (!v) break; + p = v + len; + if (tag != gen) continue; + san_present = true; + bool hit = gen == SCR_X509_GEN_IP ? len == chk_len && memcmp(v, chk, len) == 0 + : gen == SCR_X509_GEN_DNS ? scr_x509_host_match(v, len, chk, chk_len, flags) + : scr_x509_email_match(v, len, chk, chk_len); + if (hit) { + scr_x509_put(out, v, len); + return true; + } + } + } + if (gen == SCR_X509_GEN_IP || (flags & SCR_X509_NEVER_CHECK_SUBJECT) || + (san_present && !(flags & SCR_X509_ALWAYS_CHECK_SUBJECT)) || !parts.subject) + return false; + /* the subject's CN (2.5.4.3) or emailAddress (1.2.840.113549.1.9.1) */ + static const uint8_t cn[] = {0x55, 0x04, 0x03}; + static const uint8_t email[] = {0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x09, 0x01}; + const uint8_t *want = gen == SCR_X509_GEN_DNS ? cn : email; + size_t want_len = gen == SCR_X509_GEN_DNS ? sizeof cn : sizeof email; + const uint8_t *end = parts.subject + parts.subject_len; + for (const uint8_t *p = parts.subject; p < end;) { + const uint8_t *set = scr_der_tl(p, end, &tag, &len); + if (!set || tag != 0x31) break; + const uint8_t *set_end = set + len; + p = set_end; + for (const uint8_t *q = set; q < set_end;) { + const uint8_t *atv = scr_der_tl(q, set_end, &tag, &len); + if (!atv || tag != 0x30) break; + const uint8_t *atv_end = atv + len; + q = atv_end; + const uint8_t *oid = scr_der_tl(atv, atv_end, &tag, &len); + if (!oid || tag != 0x06 || len != want_len || memcmp(oid, want, len) != 0) continue; + uint8_t vtag; + size_t vlen; + const uint8_t *value = scr_der_tl(oid + len, atv_end, &vtag, &vlen); + if (!value) continue; + ScrX509Buf text = {0}; + scr_x509_value_utf8(&text, vtag, value, vlen); + bool hit = gen == SCR_X509_GEN_DNS + ? scr_x509_host_match((const uint8_t *)text.data, text.len, chk, chk_len, flags) + : scr_x509_email_match((const uint8_t *)text.data, text.len, chk, chk_len); + if (hit) scr_x509_put(out, text.data, text.len); + free(text.data); + if (hit) return true; + } + } + return false; +} + +/* x509.checkHost(name[, options]) and x509.checkEmail(email[, options]): + * the certificate name that matched (checkHost) or the argument + * (checkEmail), "" for none (Node's undefined), or NULL with the + * validation error pending. */ +static ScrStr *scr_x509_check_name(ScrBytes *der, const ScrDyn *value, const ScrDyn *options, + const char *arg, uint8_t gen) { + if (value->kind != SCR_DYN_STR) { + scr_dyn_arg_type_fail(arg, "of type string", value); + return NULL; + } + int flags = scr_x509_check_flags(options); + if (flags < 0) return NULL; + ScrX509Buf b = {0}; + const ScrStr *s = value->v.str; + bool hit = s->len > 0 && scr_x509_check(der, gen, (const uint8_t *)s->data, s->len, flags, &b); + if (hit && gen == SCR_X509_GEN_EMAIL) { + /* checkEmail answers its argument (X509_check_email reports no name) */ + free(b.data); + return scr_str_retain((ScrStr *)s); + } + return scr_x509_buf_str(&b); +} + +ScrStr *scr_crypto_x509_check_host(ScrBytes *der, ScrDyn *name, ScrDyn *options) { + return scr_x509_check_name(der, name, options, "name", SCR_X509_GEN_DNS); +} + +ScrStr *scr_crypto_x509_check_email(ScrBytes *der, ScrDyn *email, ScrDyn *options) { + return scr_x509_check_name(der, email, options, "email", SCR_X509_GEN_EMAIL); +} + +/* inet_pton's IPv4 form, which Node's checkIP parses: four decimal parts, + * each at most 255, without leading zeros. */ +static bool scr_x509_ipv4(const char *s, size_t n, uint8_t out[4]) { + size_t part = 0, i = 0; + while (part < 4) { + if (i >= n || s[i] < '0' || s[i] > '9') return false; + unsigned v = 0; + size_t digits = 0; + if (s[i] == '0' && i + 1 < n && s[i + 1] >= '0' && s[i + 1] <= '9') return false; + while (i < n && s[i] >= '0' && s[i] <= '9') { + v = v * 10 + (unsigned)(s[i++] - '0'); + if (++digits > 3 || v > 255) return false; + } + out[part++] = (uint8_t)v; + if (part < 4) { + if (i >= n || s[i] != '.') return false; + i++; + } + } + return i == n; +} + +/* inet_pton's IPv6 form: hex groups with at most one "::", and an IPv4 + * address in place of the last two groups. */ +static bool scr_x509_ipv6(const char *s, size_t n, uint8_t out[16]) { + uint8_t head[16], tail[16]; + size_t head_len = 0, tail_len = 0; + bool compressed = false; + size_t i = 0; + if (n >= 2 && s[0] == ':' && s[1] == ':') { + compressed = true; + i = 2; + if (i == n) { + memset(out, 0, 16); + return true; + } + } else if (n > 0 && s[0] == ':') { + return false; + } + while (i < n) { + uint8_t *dst = compressed ? tail : head; + size_t *dst_len = compressed ? &tail_len : &head_len; + /* an IPv4 tail */ + const char *dot = memchr(s + i, '.', n - i); + const char *colon = memchr(s + i, ':', n - i); + if (dot != NULL && colon == NULL) { + if (*dst_len + 4 > 16 || !scr_x509_ipv4(s + i, n - i, dst + *dst_len)) return false; + *dst_len += 4; + i = n; + break; + } + unsigned v = 0; + size_t digits = 0; + while (i < n && s[i] != ':') { + char c = s[i++]; + unsigned d = c >= '0' && c <= '9' ? (unsigned)(c - '0') + : c >= 'a' && c <= 'f' ? (unsigned)(c - 'a' + 10) + : c >= 'A' && c <= 'F' ? (unsigned)(c - 'A' + 10) + : 16; + if (d == 16 || ++digits > 4) return false; + v = (v << 4) | d; + } + if (digits == 0 || *dst_len + 2 > 16) return false; + dst[(*dst_len)++] = (uint8_t)(v >> 8); + dst[(*dst_len)++] = (uint8_t)v; + if (i == n) break; + i++; /* ':' */ + if (i < n && s[i] == ':') { + if (compressed) return false; + compressed = true; + i++; + if (i == n) break; + } else if (i == n) { + return false; + } + } + if (compressed) { + if (head_len + tail_len > 14) return false; + } else if (head_len != 16) { + return false; + } + memset(out, 0, 16); + memcpy(out, head, head_len); + memcpy(out + 16 - tail_len, tail, tail_len); + return true; +} + +/* x509.checkIP(ip[, options]): the argument itself when an iPAddress + * alternative name equals it, "" for none, NULL with the error pending. + * The options are validated like checkHost's and have no effect. */ +ScrStr *scr_crypto_x509_check_ip(ScrBytes *der, ScrDyn *ip, ScrDyn *options) { + if (ip->kind != SCR_DYN_STR) { + scr_dyn_arg_type_fail("ip", "of type string", ip); + return NULL; + } + if (scr_x509_check_flags(options) < 0) return NULL; + const ScrStr *s = ip->v.str; + uint8_t addr[16]; + size_t addr_len = 0; + if (memchr(s->data, ':', s->len) != NULL) { + if (scr_x509_ipv6(s->data, s->len, addr)) addr_len = 16; + } else if (scr_x509_ipv4(s->data, s->len, addr)) { + addr_len = 4; + } + if (addr_len == 0) { + static const char msg[] = "Invalid name"; + scr_throw_error_msg_code(SCR_ERR_TYPE, msg, sizeof msg - 1, "ERR_INVALID_ARG_VALUE"); + return NULL; + } + ScrX509Buf b = {0}; + if (scr_x509_check(der, SCR_X509_GEN_IP, addr, addr_len, 0, &b)) { + free(b.data); + return scr_str_retain((ScrStr *)s); + } + return scr_x509_buf_str(&b); +} + +/* ── checkIssued ──────────────────────────────────────────────────────── */ + +/* The string types OpenSSL canonicalizes before comparing names + * (ASN1_MASK_CANON). */ +static bool scr_x509_canon_type(uint8_t tag) { + return tag == 0x0C || tag == 0x1E || tag == 0x1C || tag == 0x13 || tag == 0x14 || tag == 0x16 || + tag == 0x1A; +} + +static bool scr_x509_space(uint8_t c) { + return c == ' ' || c == '\t' || c == '\n' || c == '\v' || c == '\f' || c == '\r'; +} + +/* asn1_string_canon: UTF-8, leading and trailing whitespace dropped, + * inner runs of whitespace as one space, ASCII in lower case. */ +static void scr_x509_canon(ScrX509Buf *out, uint8_t tag, const uint8_t *v, size_t n) { + ScrX509Buf text = {0}; + scr_x509_value_utf8(&text, tag, v, n); + const uint8_t *p = (const uint8_t *)text.data; + size_t len = text.len; + while (len > 0 && scr_x509_space(*p)) p++, len--; + while (len > 0 && scr_x509_space(p[len - 1])) len--; + for (size_t i = 0; i < len; i++) { + uint8_t c = p[i]; + if (scr_x509_space(c)) { + scr_x509_putc(out, ' '); + while (i + 1 < len && scr_x509_space(p[i + 1])) i++; + } else { + scr_x509_putc(out, (char)(c >= 'A' && c <= 'Z' ? c + 32 : c)); + } + } + free(text.data); +} + +/* One Name as comparable text: per RDN, each attribute's OID and its + * canonical value (or raw DER for non-string types), with separators that + * keep the RDN grouping. */ +static void scr_x509_name_canon(ScrX509Buf *out, const uint8_t *name, size_t n) { + const uint8_t *end = name + n; + uint8_t tag; + size_t len; + for (const uint8_t *p = name; p < end;) { + const uint8_t *set = scr_der_tl(p, end, &tag, &len); + if (!set || tag != 0x31) break; + const uint8_t *set_end = set + len; + p = set_end; + scr_x509_putc(out, '\x01'); + for (const uint8_t *q = set; q < set_end;) { + const uint8_t *atv = scr_der_tl(q, set_end, &tag, &len); + if (!atv || tag != 0x30) break; + const uint8_t *atv_end = atv + len; + q = atv_end; + const uint8_t *oid = scr_der_tl(atv, atv_end, &tag, &len); + if (!oid || tag != 0x06) continue; + scr_x509_putc(out, '\x02'); + scr_x509_put(out, oid, len); + uint8_t vtag; + size_t vlen; + const uint8_t *value_tl = oid + len; + const uint8_t *value = scr_der_tl(value_tl, atv_end, &vtag, &vlen); + if (!value) continue; + scr_x509_putc(out, '\x03'); + if (scr_x509_canon_type(vtag)) scr_x509_canon(out, vtag, value, vlen); + else scr_x509_put(out, value_tl, (size_t)(value + vlen - value_tl)); + } + } +} + +static bool scr_x509_name_eq(const uint8_t *a, size_t an, const uint8_t *b, size_t bn) { + ScrX509Buf x = {0}, y = {0}; + scr_x509_name_canon(&x, a, an); + scr_x509_name_canon(&y, b, bn); + bool eq = x.len == y.len && (x.len == 0 || memcmp(x.data, y.data, x.len) == 0); + free(x.data); + free(y.data); + return eq; +} + +static const uint8_t SCR_X509_OID_SKID[] = {0x55, 0x1D, 0x0E}; +static const uint8_t SCR_X509_OID_AKID[] = {0x55, 0x1D, 0x23}; + +/* The public key algorithm a signature algorithm needs: 1 RSA, 2 EC, + * 3 Ed25519, 4 Ed448, 0 unknown. */ +static int scr_x509_sig_key_kind(const uint8_t *oid, size_t n) { + static const uint8_t rsa_prefix[] = {0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01}; + static const uint8_t ecdsa_prefix[] = {0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x04}; + if (n == sizeof rsa_prefix + 1 && memcmp(oid, rsa_prefix, sizeof rsa_prefix) == 0) { + uint8_t last = oid[sizeof rsa_prefix]; + return last == 4 || last == 5 || (last >= 10 && last <= 14) ? 1 : 0; + } + if (n >= sizeof ecdsa_prefix + 1 && memcmp(oid, ecdsa_prefix, sizeof ecdsa_prefix) == 0) + return 2; + if (n == 3 && oid[0] == 0x2B && oid[1] == 0x65 && oid[2] == 0x70) return 3; + if (n == 3 && oid[0] == 0x2B && oid[1] == 0x65 && oid[2] == 0x71) return 4; + return 0; +} + +/* The kind (as above) of a SubjectPublicKeyInfo's algorithm. */ +static int scr_x509_spki_kind(const ScrX509Parts *parts) { + if (!parts->spki) return 0; + uint8_t tag; + size_t len; + const uint8_t *alg = scr_der_tl(parts->spki, parts->spki + parts->spki_len, &tag, &len); + if (!alg || tag != 0x30) return 0; + const uint8_t *oid = scr_der_tl(alg, alg + len, &tag, &len); + if (!oid || tag != 0x06) return 0; + static const uint8_t rsa[] = {0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01}; + static const uint8_t pss[] = {0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x0A}; + static const uint8_t ec[] = {0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x02, 0x01}; + if ((len == sizeof rsa && memcmp(oid, rsa, len) == 0) || + (len == sizeof pss && memcmp(oid, pss, len) == 0)) + return 1; + if (len == sizeof ec && memcmp(oid, ec, len) == 0) return 2; + if (len == 3 && oid[0] == 0x2B && oid[1] == 0x65 && oid[2] == 0x70) return 3; + if (len == 3 && oid[0] == 0x2B && oid[1] == 0x65 && oid[2] == 0x71) return 4; + return 0; +} + +/* X509_check_akid: the subject's authorityKeyIdentifier against the + * issuer's subjectKeyIdentifier, serial number and issuer name. */ +static bool scr_x509_akid_ok(const ScrX509Parts *issuer, const ScrX509Parts *subject) { + size_t n; + const uint8_t *akid = scr_x509_ext(subject, SCR_X509_OID_AKID, sizeof SCR_X509_OID_AKID, &n); + if (!akid) return true; + uint8_t tag; + size_t len; + const uint8_t *seq = scr_der_tl(akid, akid + n, &tag, &len); + if (!seq || tag != 0x30) return true; + const uint8_t *end = seq + len; + for (const uint8_t *p = seq; p < end;) { + const uint8_t *v = scr_der_tl(p, end, &tag, &len); + if (!v) break; + p = v + len; + if (tag == 0x80) { /* keyIdentifier */ + size_t sn; + const uint8_t *skid = scr_x509_ext(issuer, SCR_X509_OID_SKID, sizeof SCR_X509_OID_SKID, &sn); + uint8_t stag; + size_t slen; + const uint8_t *id = skid ? scr_der_tl(skid, skid + sn, &stag, &slen) : NULL; + if (id && stag == 0x04 && (slen != len || memcmp(id, v, len) != 0)) return false; + } else if (tag == 0x82) { /* authorityCertSerialNumber */ + if (issuer->serial && (issuer->serial_len != len || memcmp(issuer->serial, v, len) != 0)) + return false; + } else if (tag == 0xA1 && issuer->issuer) { /* authorityCertIssuer */ + bool found = false; + const uint8_t *names_end = v + len; + for (const uint8_t *q = v; q < names_end && !found;) { + uint8_t gtag; + size_t glen; + const uint8_t *g = scr_der_tl(q, names_end, >ag, &glen); + if (!g) break; + q = g + glen; + if (gtag != 0xA4) continue; + uint8_t ntag; + size_t nlen; + const uint8_t *name = scr_der_tl(g, g + glen, &ntag, &nlen); + if (name && ntag == 0x30 && scr_x509_name_eq(name, nlen, issuer->issuer, issuer->issuer_len)) + found = true; + } + if (!found) return false; + } + } + return true; +} + +/* x509.checkIssued(otherCert): X509_check_issued(otherCert, x509) — the + * names match, the authority key identifier fits, the signature algorithm + * suits the issuer's key, and the issuer's keyUsage, when present, allows + * certificate signing. */ +bool scr_crypto_x509_check_issued(ScrBytes *der, ScrBytes *issuer_der) { + ScrX509Parts subject, issuer; + if (!scr_x509_parts(SCR_X509_DER(der), &subject) || + !scr_x509_parts(SCR_X509_DER(issuer_der), &issuer)) + return false; + if (!subject.issuer || !issuer.subject || + !scr_x509_name_eq(issuer.subject, issuer.subject_len, subject.issuer, subject.issuer_len)) + return false; + if (!scr_x509_akid_ok(&issuer, &subject)) return false; + uint8_t tag; + size_t len; + const uint8_t *sig_oid = subject.sig_alg + ? scr_der_tl(subject.sig_alg, subject.sig_alg + subject.sig_alg_len, + &tag, &len) + : NULL; + if (!sig_oid || tag != 0x06) return false; + int need = scr_x509_sig_key_kind(sig_oid, len); + if (need == 0 || need != scr_x509_spki_kind(&issuer)) return false; + size_t n; + const uint8_t *ku = scr_x509_ext(&issuer, SCR_X509_OID_KEY_USAGE, sizeof SCR_X509_OID_KEY_USAGE, &n); + if (ku) { + const uint8_t *bits = scr_der_tl(ku, ku + n, &tag, &len); + if (!bits || tag != 0x03 || len < 2 || (bits[1] & 0x04) == 0) return false; + } + return true; +} + +/* ── toLegacyObject ───────────────────────────────────────────────────── */ + +static ScrDyn *scr_x509_dyn_text(const char *s, size_t n) { + ScrStr *str = scr_str_new(s, n); + ScrDyn *d = scr_dyn_new_str(str); + scr_str_release(str); + return d; +} + +/* A fresh Buffer holding n bytes. */ +static ScrDyn *scr_x509_dyn_buffer(const uint8_t *p, size_t n) { + ScrBytes *b = scr_buffer_alloc_unsafe_slow((double)n); + if (n) memcpy(b->data, p, n); + ScrDyn *d = scr_dyn_new_bytes(b); + scr_bytes_release(b); + return d; +} + +/* A Name as Node's legacy object: one property per attribute, keyed by + * OpenSSL's short name (the dotted OID otherwise), its value the UTF-8 + * text; a repeated key collects its values in an array. */ +static ScrDyn *scr_x509_name_object(const uint8_t *name, size_t n) { + ScrDyn *obj = scr_dyn_new_obj_null_proto(); + const uint8_t *end = name + n; + uint8_t tag; + size_t len; + for (const uint8_t *p = name; p < end;) { + const uint8_t *set = scr_der_tl(p, end, &tag, &len); + if (!set || tag != 0x31) break; + const uint8_t *set_end = set + len; + p = set_end; + for (const uint8_t *q = set; q < set_end;) { + const uint8_t *atv = scr_der_tl(q, set_end, &tag, &len); + if (!atv || tag != 0x30) break; + const uint8_t *atv_end = atv + len; + q = atv_end; + const uint8_t *oid = scr_der_tl(atv, atv_end, &tag, &len); + if (!oid || tag != 0x06) continue; + size_t oid_len = len; + uint8_t vtag; + size_t vlen; + const uint8_t *value = scr_der_tl(oid + len, atv_end, &vtag, &vlen); + if (!value) continue; + ScrX509Buf key = {0}, text = {0}; + scr_x509_oid_name(&key, oid, oid_len, scr_x509_attr_names, + sizeof scr_x509_attr_names / sizeof scr_x509_attr_names[0]); + scr_x509_value_utf8(&text, vtag, value, vlen); + ScrDyn *v = scr_x509_dyn_text(text.data ? text.data : "", text.len); + const ScrDyn *prior = scr_dyn_obj_get(obj, key.data, key.len); + if (prior == NULL) { + scr_dyn_obj_set(obj, key.data, key.len, v); + } else if (prior->kind == SCR_DYN_ARR) { + scr_dyn_arr_push((ScrDyn *)prior, v); + } else { + ScrDyn *arr = scr_dyn_new_arr(); + scr_dyn_arr_push(arr, scr_dyn_retain((ScrDyn *)prior)); + scr_dyn_arr_push(arr, v); + scr_dyn_obj_set(obj, key.data, key.len, arr); + } + free(key.data); + free(text.data); + } + } + return obj; +} + +/* The JSON string literal at s (a quoted alternative name), decoded. */ +static void scr_x509_json_unquote(ScrX509Buf *out, const char *s, size_t n) { + for (size_t i = 1; i < n; i++) { + char c = s[i]; + if (c == '"') return; + if (c != '\\' || i + 1 >= n) { + scr_x509_putc(out, c); + continue; + } + c = s[++i]; + if (c == 'u' && i + 4 < n) { + unsigned v = 0; + for (int k = 1; k <= 4; k++) { + char h = s[i + k]; + v = v * 16 + (unsigned)(h <= '9' ? h - '0' : (h | 0x20) - 'a' + 10); + } + scr_x509_put_utf8(out, v); + i += 4; + } else { + scr_x509_putc(out, c == 'n' ? '\n' : c == 't' ? '\t' : c); + } + } +} + +/* Node's legacy infoAccess: the infoAccess lines split at their first + * colon into " - " keys, each holding its values in an + * array (a quoted value is JSON-decoded). */ +static ScrDyn *scr_x509_info_access_object(ScrBytes *der) { + ScrStr *text = scr_crypto_x509_info_access(der); + if (text->len == 0) { + scr_str_release(text); + return scr_dyn_undefined(); + } + ScrDyn *obj = scr_dyn_new_obj_null_proto(); + const char *s = text->data; + size_t n = text->len; + for (size_t start = 0; start < n;) { + size_t eol = start; + while (eol < n && s[eol] != '\n') eol++; + size_t colon = start; + while (colon < eol && s[colon] != ':') colon++; + if (colon < eol) { + const char *val = s + colon + 1; + size_t val_len = eol - colon - 1; + ScrX509Buf v = {0}; + if (val_len > 0 && val[0] == '"') scr_x509_json_unquote(&v, val, val_len); + else scr_x509_put(&v, val, val_len); + ScrDyn *item = scr_x509_dyn_text(v.data ? v.data : "", v.len); + free(v.data); + const ScrDyn *prior = scr_dyn_obj_get(obj, s + start, colon - start); + if (prior != NULL && prior->kind == SCR_DYN_ARR) { + scr_dyn_arr_push((ScrDyn *)prior, item); + } else { + ScrDyn *arr = scr_dyn_new_arr(); + scr_dyn_arr_push(arr, item); + scr_dyn_obj_set(obj, s + start, colon - start, arr); + } + } + start = eol + 1; + } + scr_str_release(text); + return obj; +} + +/* Hex of a big-endian magnitude: BN_bn2hex (whole bytes, upper case, no + * leading zero bytes) or, with `lower_trim`, %llx-style text. */ +static void scr_x509_put_hex(ScrX509Buf *b, const uint8_t *p, size_t n, bool lower_trim) { + const char *hex = lower_trim ? "0123456789abcdef" : "0123456789ABCDEF"; + while (n > 0 && *p == 0) p++, n--; + if (n == 0) { + scr_x509_putc(b, '0'); + return; + } + for (size_t i = 0; i < n; i++) { + if (!(lower_trim && i == 0 && (p[i] >> 4) == 0)) scr_x509_putc(b, hex[p[i] >> 4]); + scr_x509_putc(b, hex[p[i] & 0x0f]); + } +} + +typedef struct { + const char *oid; + const char *name; + const char *nist; + int bits; +} ScrX509Curve; + +static const ScrX509Curve scr_x509_curves[] = { + {"1.2.840.10045.3.1.1", "prime192v1", "P-192", 192}, + {"1.3.132.0.33", "secp224r1", "P-224", 224}, + {"1.2.840.10045.3.1.7", "prime256v1", "P-256", 256}, + {"1.3.132.0.34", "secp384r1", "P-384", 384}, + {"1.3.132.0.35", "secp521r1", "P-521", 521}, + {"1.3.132.0.31", "secp192k1", NULL, 192}, + {"1.3.132.0.32", "secp224k1", NULL, 225}, + {"1.3.132.0.10", "secp256k1", NULL, 256}, + {"1.3.36.3.3.2.8.1.1.7", "brainpoolP256r1", NULL, 256}, + {"1.3.36.3.3.2.8.1.1.11", "brainpoolP384r1", NULL, 384}, + {"1.3.36.3.3.2.8.1.1.13", "brainpoolP512r1", NULL, 512}, +}; + +/* The public key members of the legacy object: modulus, exponent, + * pubkey, bits, asn1Curve and nistCurve (each undefined where the key + * type has none). */ +static void scr_x509_key_fields(const ScrX509Parts *parts, ScrDyn *slots[6]) { + for (int i = 0; i < 6; i++) slots[i] = scr_dyn_undefined(); + if (!parts->spki) return; + int kind = scr_x509_spki_kind(parts); + uint8_t tag; + size_t len; + const uint8_t *spki_end = parts->spki + parts->spki_len; + const uint8_t *alg = scr_der_tl(parts->spki, spki_end, &tag, &len); + if (!alg || tag != 0x30) return; + const uint8_t *alg_end = alg + len; + const uint8_t *oid = scr_der_tl(alg, alg_end, &tag, &len); + if (!oid) return; + const uint8_t *params = oid + len; + uint8_t ptag; + size_t plen; + const uint8_t *param = scr_der_tl(params, alg_end, &ptag, &plen); + const uint8_t *bits = scr_der_tl(alg_end, spki_end, &tag, &len); + if (!bits || tag != 0x03 || len < 1) return; + const uint8_t *key = bits + 1; + size_t key_len = len - 1; + static const uint8_t rsa_oid[] = {0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01}; + bool rsa = kind == 1 && (size_t)(params - oid) == sizeof rsa_oid && + memcmp(oid, rsa_oid, sizeof rsa_oid) == 0; + if (rsa) { + const uint8_t *seq = scr_der_tl(key, key + key_len, &tag, &len); + if (!seq || tag != 0x30) return; + const uint8_t *seq_end = seq + len; + size_t nlen, elen; + const uint8_t *nmod = scr_der_tl(seq, seq_end, &tag, &nlen); + if (!nmod || tag != 0x02) return; + const uint8_t *nexp = scr_der_tl(nmod + nlen, seq_end, &tag, &elen); + if (!nexp || tag != 0x02) return; + ScrX509Buf m = {0}, e = {0}; + scr_x509_put_hex(&m, nmod, nlen, false); + scr_x509_puts(&e, "0x"); + scr_x509_put_hex(&e, nexp, elen, true); + slots[0] = scr_x509_dyn_text(m.data, m.len); + slots[1] = scr_x509_dyn_text(e.data, e.len); + free(m.data); + free(e.data); + slots[2] = scr_x509_dyn_buffer(parts->spki_tlv, parts->spki_tlv_len); + size_t i = 0; + while (i < nlen && nmod[i] == 0) i++; + int nbits = 0; + if (i < nlen) { + nbits = (int)((nlen - i - 1) * 8); + for (uint8_t top = nmod[i]; top; top >>= 1) nbits++; + } + slots[3] = scr_dyn_new_num(nbits); + return; + } + if (kind == 2 && param && ptag == 0x06) { + ScrX509Buf dotted = {0}; + scr_x509_oid_dotted(&dotted, param, plen); + for (size_t c = 0; c < sizeof scr_x509_curves / sizeof scr_x509_curves[0]; c++) { + const ScrX509Curve *curve = &scr_x509_curves[c]; + if (strlen(curve->oid) != dotted.len || memcmp(curve->oid, dotted.data, dotted.len) != 0) + continue; + slots[2] = scr_x509_dyn_buffer(key, key_len); + slots[3] = scr_dyn_new_num(curve->bits); + slots[4] = scr_x509_dyn_text(curve->name, strlen(curve->name)); + if (curve->nist) slots[5] = scr_x509_dyn_text(curve->nist, strlen(curve->nist)); + break; + } + free(dotted.data); + } +} + +static const uint8_t SCR_X509_OID_EKU[] = {0x55, 0x1D, 0x25}; + +/* x509.toLegacyObject(): Node's X509ToObject, member order included. */ +ScrDyn *scr_crypto_x509_legacy_object(ScrBytes *der) { + ScrX509Parts parts; + ScrDyn *obj = scr_dyn_new_obj(); + bool ok = scr_x509_parts(SCR_X509_DER(der), &parts); + if (!ok) memset(&parts, 0, sizeof parts); + scr_dyn_obj_set(obj, "subject", 7, + parts.subject ? scr_x509_name_object(parts.subject, parts.subject_len) + : scr_dyn_new_obj_null_proto()); + scr_dyn_obj_set(obj, "issuer", 6, + parts.issuer ? scr_x509_name_object(parts.issuer, parts.issuer_len) + : scr_dyn_new_obj_null_proto()); + ScrStr *san = scr_crypto_x509_subject_alt_name(der); + scr_dyn_obj_set(obj, "subjectaltname", 14, san->len ? scr_dyn_new_str(san) : scr_dyn_undefined()); + scr_str_release(san); + scr_dyn_obj_set(obj, "infoAccess", 10, scr_x509_info_access_object(der)); + scr_dyn_obj_set(obj, "ca", 2, scr_dyn_new_bool(scr_crypto_x509_ca(der))); + ScrDyn *key[6]; + scr_x509_key_fields(&parts, key); + scr_dyn_obj_set(obj, "modulus", 7, key[0]); + scr_dyn_obj_set(obj, "exponent", 8, key[1]); + scr_dyn_obj_set(obj, "pubkey", 6, key[2]); + scr_dyn_obj_set(obj, "bits", 4, key[3]); + static const struct { + const char *key; + ScrStr *(*field)(ScrBytes *); + } texts[] = { + {"valid_from", scr_crypto_x509_valid_from}, + {"valid_to", scr_crypto_x509_valid_to}, + {"fingerprint", scr_crypto_x509_fingerprint}, + {"fingerprint256", scr_crypto_x509_fingerprint256}, + {"fingerprint512", scr_crypto_x509_fingerprint512}, + }; + for (size_t i = 0; i < sizeof texts / sizeof texts[0]; i++) { + ScrStr *s = texts[i].field(der); + scr_dyn_obj_set(obj, texts[i].key, strlen(texts[i].key), scr_dyn_new_str(s)); + scr_str_release(s); + } + size_t n; + const uint8_t *eku = ok ? scr_x509_ext(&parts, SCR_X509_OID_EKU, sizeof SCR_X509_OID_EKU, &n) : NULL; + uint8_t tag; + size_t len; + const uint8_t *purposes = eku ? scr_der_tl(eku, eku + n, &tag, &len) : NULL; + if (purposes && tag == 0x30) { + ScrDyn *arr = scr_dyn_new_arr(); + const uint8_t *end = purposes + len; + for (const uint8_t *p = purposes; p < end;) { + const uint8_t *oid = scr_der_tl(p, end, &tag, &len); + if (!oid) break; + p = oid + len; + if (tag != 0x06) continue; + ScrX509Buf dotted = {0}; + scr_x509_oid_dotted(&dotted, oid, len); + scr_dyn_arr_push(arr, scr_x509_dyn_text(dotted.data, dotted.len)); + free(dotted.data); + } + scr_dyn_obj_set(obj, "ext_key_usage", 13, arr); + } else { + scr_dyn_obj_set(obj, "ext_key_usage", 13, scr_dyn_undefined()); + } + ScrStr *serial = scr_crypto_x509_serial_number(der); + scr_dyn_obj_set(obj, "serialNumber", 12, scr_dyn_new_str(serial)); + scr_str_release(serial); + { + ScrBytes *view = der; + SCR_BYTES_SNAPSHOT(view); + scr_dyn_obj_set(obj, "raw", 3, scr_x509_dyn_buffer(SCR_X509_DER(view))); + } + scr_dyn_obj_set(obj, "asn1Curve", 9, key[4]); + scr_dyn_obj_set(obj, "nistCurve", 9, key[5]); + return obj; +} diff --git a/packages/runtime/src/scr_zlib.c b/packages/runtime/src/scr_zlib.c index ff09e7ea1..8756c3d17 100644 --- a/packages/runtime/src/scr_zlib.c +++ b/packages/runtime/src/scr_zlib.c @@ -133,6 +133,11 @@ bool scr_zlib_inflate_exact(const unsigned char *src, size_t src_len, * Modes are Node's node_zlib_mode numbers (DEFLATE 1 ... UNZIP 7). */ struct ScrZlibEngine { z_stream strm; + /* Another codec's engine (scr_zlib_engine_codec): its state answers the + * entries below, which skip the z_stream. */ + const ScrZlibCodec *codec; + void *codec_state; + uint32_t codec_avail_in, codec_avail_out; int mode; int level, window_bits, mem_level, strategy; int flush; @@ -172,6 +177,17 @@ ScrZlibEngine *scr_zlib_engine_new(int mode, int level, int window_bits, int mem return e; } +ScrZlibEngine *scr_zlib_engine_codec(int mode, const ScrZlibCodec *codec, void *state, + bool reject_garbage) { + ScrZlibEngine *e = calloc(1, sizeof *e); + if (!e) scr_zlib_oom(); + e->mode = mode; + e->codec = codec; + e->codec_state = state; + e->reject_garbage = reject_garbage; + return e; +} + static bool scr_zlib_engine_deflating(const ScrZlibEngine *e) { return e->mode == SCR_Z_DEFLATE || e->mode == SCR_Z_GZIP || e->mode == SCR_Z_DEFLATERAW; } @@ -229,6 +245,11 @@ static int scr_zlib_engine_reset_stream(ScrZlibEngine *e) { void scr_zlib_engine_work(ScrZlibEngine *e, int flush, const unsigned char *in, uint32_t in_len, unsigned char *out, uint32_t out_len) { + if (e->codec) { + e->codec->work(e->codec_state, flush, in, in_len, out, out_len, &e->codec_avail_in, + &e->codec_avail_out); + return; + } e->strm.avail_in = in_len; e->strm.next_in = (Bytef *)(uintptr_t)in; e->strm.avail_out = out_len; @@ -286,6 +307,11 @@ void scr_zlib_engine_work(ScrZlibEngine *e, int flush, const unsigned char *in, } void scr_zlib_engine_offsets(const ScrZlibEngine *e, uint32_t *avail_in, uint32_t *avail_out) { + if (e->codec) { + *avail_in = e->codec_avail_in; + *avail_out = e->codec_avail_out; + return; + } *avail_in = e->strm.avail_in; *avail_out = e->strm.avail_out; } @@ -313,6 +339,7 @@ static bool scr_zlib_engine_fail(const ScrZlibEngine *e, const char *message, Sc } bool scr_zlib_engine_error(const ScrZlibEngine *e, ScrZlibError *out) { + if (e->codec) return e->codec->error(e->codec_state, out); switch (e->err) { case Z_OK: case Z_BUF_ERROR: @@ -327,6 +354,7 @@ bool scr_zlib_engine_error(const ScrZlibEngine *e, ScrZlibError *out) { } bool scr_zlib_engine_reset(ScrZlibEngine *e, ScrZlibError *out) { + if (e->codec) return e->codec->reset(e->codec_state, out); switch (scr_zlib_engine_reset_stream(e)) { case 0: return false; case 1: return scr_zlib_engine_fail(e, "Failed to init stream before reset", out); @@ -338,6 +366,7 @@ bool scr_zlib_engine_reset(ScrZlibEngine *e, ScrZlibError *out) { /* SetParams: deflateParams for DEFLATE and DEFLATERAW only (Node's * switch leaves GZIP's level and strategy unchanged). */ bool scr_zlib_engine_params(ScrZlibEngine *e, int level, int strategy, ScrZlibError *out) { + if (e->codec) return false; /* Node's Brotli streams have no params() */ if (scr_zlib_engine_init(e) && e->err != Z_OK) return scr_zlib_engine_fail(e, "Failed to init stream before set parameters", out); e->err = Z_OK; @@ -350,6 +379,11 @@ bool scr_zlib_engine_params(ScrZlibEngine *e, int level, int strategy, ScrZlibEr void scr_zlib_engine_free(ScrZlibEngine *e) { if (!e) return; + if (e->codec) { + e->codec->free(e->codec_state); + free(e); + return; + } if (e->init_done) { if (scr_zlib_engine_deflating(e)) deflateEnd(&e->strm); else inflateEnd(&e->strm); @@ -418,6 +452,15 @@ static bool scr_zlib_is_null(const ScrDyn *v) { void scr_zlib_options_drop(ScrZlibOptions *o) { if (o->dict) scr_bytes_release(o->dict); o->dict = NULL; + if (o->codec_state) o->codec_state_free(o->codec_state); + o->codec_state = NULL; +} + +const ScrZlibBrotliHooks *scr_zlib_brotli_hooks = NULL; + +/* Node's BROTLI_DECODE and BROTLI_ENCODE modes. */ +static bool scr_zlib_brotli_mode(int mode) { + return mode == 8 || mode == 9; } static bool scr_zlib_options_parse_object(int mode, const ScrDyn *opts, ScrZlibOptions *out); @@ -426,6 +469,10 @@ static bool scr_zlib_options_parse_object(int mode, const ScrDyn *opts, ScrZlibO * slot by reference) is read through its materialized view, as a plain * object is. */ bool scr_zlib_options_parse(int mode, const ScrDyn *opts, ScrZlibOptions *out) { + if (scr_zlib_brotli_mode(mode)) { + if (!scr_zlib_brotli_hooks) scr_trap("scriptc: the Brotli codec is not linked\n"); + return scr_zlib_brotli_hooks->options(mode, opts, out); + } if (opts == NULL || opts->kind != SCR_DYN_TYPED_REF) return scr_zlib_options_parse_object(mode, opts, out); ScrDyn *view = scr_dyn_typed_ref_materialize(opts); bool ok = false; @@ -475,6 +522,10 @@ static bool scr_zlib_options_parse_object(int mode, const ScrDyn *opts, ScrZlibO } out->dict = scr_bytes_raw_view(d->v.bytes); } + return scr_zlib_base_options(o, 5, out); +} + +bool scr_zlib_base_options(const ScrDyn *o, double flush_max, ScrZlibOptions *out) { const ScrDyn *rg = scr_zlib_opt(o, "rejectGarbageAfterEnd"); out->reject_garbage = rg != NULL && rg->kind == SCR_DYN_BOOL && rg->v.b; const ScrDyn *cs = scr_zlib_opt(o, "chunkSize"); @@ -488,10 +539,10 @@ static bool scr_zlib_options_parse_object(int mode, const ScrDyn *opts, ScrZlibO } } if (!ok || - !scr_zlib_check_range(scr_zlib_opt(o, "flush"), "options.flush", true, 0, 5, Z_NO_FLUSH, - &out->flush) || - !scr_zlib_check_range(scr_zlib_opt(o, "finishFlush"), "options.finishFlush", true, 0, 5, - Z_FINISH, &out->finish_flush) || + !scr_zlib_check_range(scr_zlib_opt(o, "flush"), "options.flush", true, 0, flush_max, + out->flush, &out->flush) || + !scr_zlib_check_range(scr_zlib_opt(o, "finishFlush"), "options.finishFlush", true, 0, + flush_max, out->finish_flush, &out->finish_flush) || !scr_zlib_check_range(scr_zlib_opt(o, "maxOutputLength"), "options.maxOutputLength", true, 1, 9007199254740991.0, 9007199254740991.0, &out->max_output)) { scr_zlib_options_drop(out); @@ -513,7 +564,8 @@ static bool scr_zlib_options_parse_object(int mode, const ScrDyn *opts, ScrZlibO return true; } -ScrZlibEngine *scr_zlib_engine_from_options(int mode, const ScrZlibOptions *o) { +ScrZlibEngine *scr_zlib_engine_from_options(int mode, ScrZlibOptions *o) { + if (scr_zlib_brotli_mode(mode)) return scr_zlib_brotli_hooks->engine(mode, o); return scr_zlib_engine_new(mode, (int)o->level, (int)o->window_bits, (int)o->mem_level, (int)o->strategy, o->dict ? o->dict->data : NULL, o->dict ? o->dict->len : 0, o->reject_garbage); diff --git a/packages/runtime/src/scr_zlib.h b/packages/runtime/src/scr_zlib.h index 124e157ab..d34b81424 100644 --- a/packages/runtime/src/scr_zlib.h +++ b/packages/runtime/src/scr_zlib.h @@ -42,6 +42,19 @@ bool scr_zlib_engine_error(const ScrZlibEngine *e, ScrZlibError *out); bool scr_zlib_engine_reset(ScrZlibEngine *e, ScrZlibError *out); bool scr_zlib_engine_params(ScrZlibEngine *e, int level, int strategy, ScrZlibError *out); void scr_zlib_engine_free(ScrZlibEngine *e); +/* Another codec behind the same engine (Brotli, scr_brotli.c): the engine + * entries above call these on its state. work runs on a worker thread and + * reports the unused input and output; error and reset fill *out as the + * zlib entries do. free releases the state. */ +typedef struct { + void (*work)(void *state, int flush, const unsigned char *in, uint32_t in_len, unsigned char *out, + uint32_t out_len, uint32_t *avail_in, uint32_t *avail_out); + bool (*error)(const void *state, ScrZlibError *out); + bool (*reset)(void *state, ScrZlibError *out); + void (*free)(void *state); +} ScrZlibCodec; +ScrZlibEngine *scr_zlib_engine_codec(int mode, const ScrZlibCodec *codec, void *state /* moves */, + bool reject_garbage); /* Node's Zlib() and ZlibBase() option ladder, shared by the stream classes * and the one-shot forms with options: false with Node's error pending. * `dict` is a +1 view (or NULL) that scr_zlib_options_drop releases. @@ -52,12 +65,32 @@ typedef struct { ScrBytes *dict; bool reject_garbage, info; double chunk_size, flush, finish_flush, max_output; + /* Another codec's prepared state (a Brotli instance with its parameters + * set), which scr_zlib_engine_from_options moves into the engine and + * scr_zlib_options_drop frees otherwise. */ + void *codec_state; + void (*codec_state_free)(void *state); } ScrZlibOptions; bool scr_zlib_options_parse(int mode, const ScrDyn *opts, ScrZlibOptions *out); void scr_zlib_options_drop(ScrZlibOptions *o); +/* ZlibBase's reads over a truthy options object, after the codec's own: + * chunkSize, flush and finishFlush (within 0..flush_max), maxOutputLength, + * rejectGarbageAfterEnd and info. False with Node's error pending, *out + * dropped. */ +bool scr_zlib_base_options(const ScrDyn *opts, double flush_max, ScrZlibOptions *out); +/* Brotli's option ladder and engine (scr_brotli.c), installed when the + * program uses Brotli: scr_zlib_options_parse and + * scr_zlib_engine_from_options call them for Node's BROTLI_DECODE (8) and + * BROTLI_ENCODE (9) modes. */ +typedef struct { + bool (*options)(int mode, const ScrDyn *opts, ScrZlibOptions *out); + ScrZlibEngine *(*engine)(int mode, ScrZlibOptions *o); +} ScrZlibBrotliHooks; +extern const ScrZlibBrotliHooks *scr_zlib_brotli_hooks; +void scr_brotli_install(void); bool scr_zlib_check_range(const ScrDyn *v, const char *name, bool prop, double lower, double upper, double def, double *out); -ScrZlibEngine *scr_zlib_engine_from_options(int mode, const ScrZlibOptions *o); +ScrZlibEngine *scr_zlib_engine_from_options(int mode, ScrZlibOptions *o); ScrError *scr_zlib_error_new(const ScrZlibError *ze); /* +1 */ /* deflateSync(data, options) and the other one-shot forms with an options * value (undefined for none): Node's processChunkSync over the engine, diff --git a/packages/runtime/src/scr_zlib_stream.c b/packages/runtime/src/scr_zlib_stream.c index 40daa6c76..8b932a63a 100644 --- a/packages/runtime/src/scr_zlib_stream.c +++ b/packages/runtime/src/scr_zlib_stream.c @@ -51,6 +51,9 @@ typedef struct ScrZlibStream { bool done_pending; /* Transform's kCallback: completion waits for _read */ double rlen_before; /* readableLength when the chunk's transform began */ ScrBytes *flush_bufs[6]; /* zero-length sentinels, one per flush kind */ + /* The AsyncLocalStorage context of the stream's creation (+1, NULL + * outside a store): Node runs the handle's step callbacks in it. */ + ScrAlsCtx *als; } ScrZlibStream; static void scr_zlib_stream_oom(void) { @@ -81,6 +84,7 @@ static void scr_zs_release_v(void *p) { for (size_t i = 0; i < 6; i++) { if (zs->flush_bufs[i]) scr_bytes_release(zs->flush_bufs[i]); } + scr_als_ctx_release(zs->als); free(zs); } @@ -242,6 +246,7 @@ static void scr_zs_step_after(void *payload) { ScrZlibStep *op = payload; ScrZlibStream *zs = op->zs; ScrStream *s = op->s; + ScrAlsCtx *prev = scr_als_dispatch_enter(zs->als); zs->write_in_progress = false; ScrZlibError ze; if (scr_zlib_engine_error(zs->eng, &ze)) { @@ -252,6 +257,7 @@ static void scr_zs_step_after(void *payload) { scr_zs_process(zs, s); } if (zs->pending_close) scr_zs_close_engine(zs); + scr_als_dispatch_leave(prev); } static void scr_zs_step_destroy(void *payload) { @@ -427,6 +433,8 @@ static const char *scr_zs_class_name(int mode) { case SCR_ZM_GUNZIP: return "Gunzip"; case SCR_ZM_DEFLATERAW: return "DeflateRaw"; case SCR_ZM_INFLATERAW: return "InflateRaw"; + case 8: return "BrotliDecompress"; + case 9: return "BrotliCompress"; default: return "Unzip"; } } @@ -463,6 +471,7 @@ ScrStream *scr_zlib_stream_new(double mode_d, ScrDyn *opts) { ScrZlibStream *zs = calloc(1, sizeof *zs); if (!zs) scr_zlib_stream_oom(); zs->rc = 1; + zs->als = scr_als_capture(); zs->mode = mode; zs->eng = scr_zlib_engine_from_options(mode, &zo); scr_zlib_options_drop(&zo); @@ -471,7 +480,8 @@ ScrStream *scr_zlib_stream_new(double mode_d, ScrDyn *opts) { if (!zs->out_buf) scr_zlib_stream_oom(); zs->default_flush = (int)zo.flush; zs->finish_flush = (int)zo.finish_flush; - zs->full_flush = SCR_ZF_FULL; + /* Brotli's full flush is BROTLI_OPERATION_FLUSH (brotliDefaultOpts). */ + zs->full_flush = mode == 8 || mode == 9 ? 1 : SCR_ZF_FULL; zs->level = zo.level; zs->strategy = zo.strategy; zs->reject_garbage = zo.reject_garbage; diff --git a/packages/runtime/test/common-symbols.test.ts b/packages/runtime/test/common-symbols.test.ts index 43d4461e6..344bd01d6 100644 --- a/packages/runtime/test/common-symbols.test.ts +++ b/packages/runtime/test/common-symbols.test.ts @@ -38,6 +38,7 @@ async function commonSymbols(label: string, compiler: string[]): Promise { + const buildDir = join(testDir, "build"); + await mkdir(buildDir, { recursive: true }); + for (const [name, flags] of [ + ["test_heap_trim", []], + ["test_heap_trim_workers", ["-DSCR_WORKERS", "-pthread"]], + ] as const) { + const bin = join(buildDir, name); + await execFileAsync("clang", [ + "-std=c11", + "-D_GNU_SOURCE", + "-O1", + "-Wall", + "-Wextra", + "-fsanitize=undefined", + ...flags, + "-o", + bin, + join(testDir, "test_heap_trim.c"), + join(testDir, "../src/scr_alloc.c"), + ]); + const run = await execFileAsync(bin, [], { + env: { ...process.env, UBSAN_OPTIONS: "halt_on_error=1" }, + }); + expect(run.stdout).toBe("heap trim checks passed\n"); + } + }, + 120_000, +); diff --git a/packages/runtime/test/json.test.ts b/packages/runtime/test/json.test.ts index 3315cb415..b08f2fdd3 100644 --- a/packages/runtime/test/json.test.ts +++ b/packages/runtime/test/json.test.ts @@ -1,4 +1,4 @@ -import { execFile } from "node:child_process"; +import { execFile, execFileSync } from "node:child_process"; import { mkdir } from "node:fs/promises"; import { join } from "node:path"; import { promisify } from "node:util"; @@ -53,9 +53,8 @@ beforeAll(async () => { // The C side parses/inspects dyn values as checks and routes each expected // failure through scr_exc_print_uncaught. Parse failures are SyntaxError // INSTANCES (the depth cap a RangeError, dynCheck failures TypeErrors), so -// every uncaught line renders "name: message" — e.name is Node-exact while -// the V8-flavored message wording stays approximate (SEMANTICS.md): these -// lines pin OUR rendering, not Node's. +// every uncaught line renders "name: message"; the JSON syntax error +// messages are Node's (V8's wording, positions and line/column). test("json runtime: parser, dyn, dynCheck failure path, stringify buffer", async () => { const { stdout, stderr } = await execFileAsync(bin); expect(stdout).toMatch(/^(\d+)\/\1 checks passed\n$/); @@ -63,21 +62,21 @@ test("json runtime: parser, dyn, dynCheck failure path, stringify buffer", async [ `Uncaught SyntaxError: Unexpected end of JSON input`, `Uncaught SyntaxError: Unexpected end of JSON input`, - `Uncaught SyntaxError: Expected property name or '}' in JSON at position 1`, + `Uncaught SyntaxError: Expected property name or '}' in JSON at position 1 (line 1 column 2)`, + `Uncaught SyntaxError: Unexpected end of JSON input`, + `Uncaught SyntaxError: Expected ',' or ']' after array element in JSON at position 3 (line 1 column 4)`, + `Uncaught SyntaxError: Expected ':' after property name in JSON at position 4 (line 1 column 5)`, + `Uncaught SyntaxError: Expected double-quoted property name in JSON at position 7 (line 1 column 8)`, + `Uncaught SyntaxError: Expected property name or '}' in JSON at position 1 (line 1 column 2)`, + `Uncaught SyntaxError: Unterminated string in JSON at position 13 (line 1 column 14)`, + `Uncaught SyntaxError: Bad escaped character in JSON at position 6 (line 1 column 7)`, + `Uncaught SyntaxError: Bad control character in string literal in JSON at position 6 (line 1 column 7)`, + `Uncaught SyntaxError: Bad Unicode escape in JSON at position 5 (line 1 column 6)`, + `Uncaught SyntaxError: No number after minus sign in JSON at position 1 (line 1 column 2)`, + `Uncaught SyntaxError: Unterminated fractional number in JSON at position 2 (line 1 column 3)`, + `Uncaught SyntaxError: Exponent part is missing a number in JSON at position 3 (line 1 column 4)`, + `Uncaught SyntaxError: Unexpected non-whitespace character after JSON at position 2 (line 1 column 3)`, `Uncaught SyntaxError: Unexpected end of JSON input`, - `Uncaught SyntaxError: Expected ',' or ']' after array element in JSON at position 3`, - `Uncaught SyntaxError: Expected ':' after property name in JSON at position 4`, - `Uncaught SyntaxError: Expected property name or '}' in JSON at position 7`, - `Uncaught SyntaxError: Expected property name or '}' in JSON at position 1`, - `Uncaught SyntaxError: Unterminated string in JSON at position 0`, - `Uncaught SyntaxError: Bad escaped character in JSON at position 6`, - `Uncaught SyntaxError: Bad control character in string literal in JSON at position 6`, - `Uncaught SyntaxError: Bad Unicode escape in JSON at position 5`, - `Uncaught SyntaxError: No number after minus sign in JSON at position 1`, - `Uncaught SyntaxError: Unterminated fractional number in JSON at position 2`, - `Uncaught SyntaxError: Exponent part is missing a number in JSON at position 3`, - `Uncaught SyntaxError: Unexpected non-whitespace character after JSON at position 2`, - `Uncaught SyntaxError: Unexpected token 'n', "nul" is not valid JSON`, `Uncaught RangeError: Maximum call stack size exceeded`, `Uncaught TypeError: expected number at $.items[2].price, got string`, `Uncaught TypeError: expected object at $, got undefined`, @@ -85,3 +84,32 @@ test("json runtime: parser, dyn, dynCheck failure path, stringify buffer", async ].join("\n"), ); }); + +function zigOnPath(): boolean { + try { + execFileSync("zig", ["version"], { stdio: "ignore" }); + return true; + } catch { + return false; + } +} + +// 32-bit uintptr_t: the weak-collection hashes mix in 64 bits, so no shift +// exceeds the width of its operand on wasm32. +test.skipIf(!zigOnPath())("json runtime builds for wasm32 without oversized shifts", async () => { + await execFileAsync("zig", [ + "cc", + "-target", + "wasm32-wasi", + "-std=c11", + "-O2", + "-D_GNU_SOURCE", + "-D_WASI_EMULATED_SIGNAL", + "-D_WASI_EMULATED_PROCESS_CLOCKS", + "-Werror=shift-count-overflow", + "-c", + "-o", + join(testDir, "build", "scr_json.wasm32.o"), + join(testDir, "../src/scr_json.c"), + ]); +}); diff --git a/packages/runtime/test/test_array.c b/packages/runtime/test/test_array.c index 7ca0ebd1a..9126c154f 100644 --- a/packages/runtime/test/test_array.c +++ b/packages/runtime/test/test_array.c @@ -2513,6 +2513,84 @@ static void test_states_on_demand(void) { #endif } +/* Elements outside the element type (SCR_ARR_FOREIGN): stores, reads, + * copies, sorting and removal keep the stored value's count balanced. */ +static void test_foreign_elements(void) { + ScrStr *text = scr_str_new("x", 1); + ScrDyn *x = scr_dyn_new_str(text); + scr_str_release(text); + ScrArr *a = scr_arr_new(SCR_ELEM_F64, 0); + scr_arr_push_f64(a, 1); + scr_arr_push_f64(a, 2); + check(scr_arr_set_foreign(a, 2, x), "foreign store appends"); + check_f64(scr_arr_len(a), 3, "foreign store extends the length"); + check_f64(scr_arr_state(a, 2), SCR_ARR_FOREIGN, "foreign element state"); + ScrDyn *got = scr_arr_get_foreign(a, 2); + check(got == x, "get_foreign answers the stored value"); + scr_dyn_release(got); + check(scr_arr_get_foreign(a, 0) == NULL, "a number is not a foreign element"); + check(isnan(scr_arr_get_number(a, 2)), "numeric read of a foreign 'x' is NaN"); + check(scr_arr_set_foreign(a, 3, scr_dyn_undefined()), "undefined store"); + check_f64(scr_arr_state(a, 3), SCR_ARR_UNDEFINED, "undefined is a present undefined"); + ScrStr *sep = scr_str_new(",", 1); + ScrStr *joined = scr_arr_join(a, sep); + check(joined->len == 6 && memcmp(joined->data, "1,2,x,", 6) == 0, "join converts foreign"); + scr_str_release(joined); + ScrArr *copy = scr_arr_slice(a, 0, INFINITY); + check_f64(scr_arr_state(copy, 2), SCR_ARR_FOREIGN, "slice keeps a foreign element"); + scr_arr_release(copy); + ScrArr *head = scr_arr_new(SCR_ELEM_F64, 0); + scr_arr_set_foreign(head, 0, x); + scr_arr_push_f64(head, 2); + scr_arr_push_f64(head, 3); + copy = scr_arr_slice(head, 1, INFINITY); + check_f64(scr_arr_get_f64(copy, 1), 3, "slice past a foreign element"); + scr_arr_release(copy); + scr_arr_release(head); + scr_arr_set_f64(a, 2, 5); + check_f64(scr_arr_get_f64(a, 2), 5, "a number replaces a foreign element"); + scr_arr_set_foreign(a, 2, x); + scr_arr_set_len(a, 1); + check_f64(scr_arr_len(a), 1, "truncation drops foreign elements"); + scr_arr_set_foreign(a, 1, x); + check(scr_arr_delete(a, 1), "delete removes a foreign element"); + scr_arr_set_foreign(a, 1, x); + uint64_t slot = 0; + check(scr_arr_pop_state(a, &slot) == SCR_ARR_FOREIGN, "pop answers the foreign state"); + check((ScrDyn *)(uintptr_t)slot == x, "pop moves the value out"); + scr_dyn_release((ScrDyn *)(uintptr_t)slot); + for (int i = 0; i < 12; i++) scr_arr_set_foreign(a, (double)(1 + i), x); + double seven = 7; + uint64_t inserted[1]; + memcpy(&inserted[0], &seven, sizeof seven); + scr_arr_splice_drop(a, 1, 10, 1, inserted); + check_f64(scr_arr_len(a), 4, "splice removes foreign elements"); + check_f64(scr_arr_get_f64(a, 1), 7, "splice inserts after them"); + scr_arr_release(a); + + ScrStr *letter = scr_str_new("a", 1); + ScrDyn *a_value = scr_dyn_new_str(letter); + scr_str_release(letter); + ScrArr *s = scr_arr_new(SCR_ELEM_F64, 0); + scr_arr_push_f64(s, 10); + scr_arr_push_f64(s, 9); + scr_arr_set_foreign(s, 2, a_value); + scr_arr_push_f64(s, 1); + scr_arr_release(scr_arr_sort_primitive(s, false)); + check(scr_arr_get_f64(s, 0) == 1 && scr_arr_get_f64(s, 1) == 10 && scr_arr_get_f64(s, 2) == 9, + "default sort orders numbers by their strings"); + check_f64(scr_arr_state(s, 3), SCR_ARR_FOREIGN, "default sort orders 'a' after the digits"); + scr_arr_release(s); + scr_dyn_release(a_value); + + ScrArr *flags = scr_arr_new(SCR_ELEM_BOOL, 0); + check(!scr_arr_set_foreign(flags, 0, x), "boolean arrays hold no foreign elements"); + scr_arr_release(flags); + scr_str_release(sep); + check(x->rc == 1, "every foreign store released its count"); + scr_dyn_release(x); +} + int main(int argc, char **argv) { if (argc > 1) { ScrArr *a = scr_arr_new(SCR_ELEM_F64, 0); @@ -2573,6 +2651,8 @@ int main(int argc, char **argv) { test_head_room(); test_states_on_demand(); + test_foreign_elements(); + fprintf(stderr, "%ld/%ld cases passed\n", total - failed, total); return failed == 0 ? 0 : 1; } diff --git a/packages/runtime/test/test_bytes.c b/packages/runtime/test/test_bytes.c index 8c6602948..1aea7a2d0 100644 --- a/packages/runtime/test/test_bytes.c +++ b/packages/runtime/test/test_bytes.c @@ -216,6 +216,135 @@ static void test_encodings(void) { scr_str_release(b64); } +/* The length of the well-formed WTF-8 sequence at in[i], or 0. */ +static size_t ref_seq_len(const uint8_t *in, size_t n, size_t i) { + uint8_t b = in[i]; + if (b < 0x80) return 1; + size_t len; + uint8_t lo = 0x80, hi = 0xbf; + if (b >= 0xc2 && b <= 0xdf) len = 2; + else if (b >= 0xe0 && b <= 0xef) len = 3; + else if (b >= 0xf0 && b <= 0xf4) len = 4; + else return 0; + if (b == 0xe0) lo = 0xa0; + if (b == 0xf0) lo = 0x90; + if (b == 0xf4) hi = 0x8f; + if (n - i < len || in[i + 1] < lo || in[i + 1] > hi) return 0; + for (size_t k = 2; k < len; k++) + if ((in[i + k] & 0xc0) != 0x80) return 0; + return len; +} + +/* The UTF-16 units of string storage, one at a time: a well-formed + * sequence gives its code point's units and any other byte stands for + * U+0000..U+00FF (a raw HTTP header byte). */ +static size_t ref_units(const uint8_t *in, size_t n, uint16_t *out) { + size_t k = 0; + for (size_t i = 0; i < n;) { + size_t len = ref_seq_len(in, n, i); + if (len == 0) { + out[k++] = in[i++]; + continue; + } + uint32_t cp = len == 1 ? in[i] : (uint32_t)(in[i] & (0x7f >> len)); + for (size_t j = 1; j < len; j++) cp = (cp << 6) | (in[i + j] & 0x3f); + i += len; + if (cp > 0xffff) { + out[k++] = (uint16_t)(0xd800 + ((cp - 0x10000) >> 10)); + out[k++] = (uint16_t)(0xdc00 + ((cp - 0x10000) & 0x3ff)); + } else { + out[k++] = (uint16_t)cp; + } + } + return k; +} + +/* Encodes storage that is not well-formed (raw header bytes become string + * storage unchecked) and compares latin1, ascii and utf16le with the + * reference; hex and base64 decode the latin1 bytes. ASan checks that no + * path reads or writes outside either buffer. */ +/* encs: latin1, ascii, utf16le, hex, base64. */ +static void check_raw_encodings(ScrStr *const *encs, const uint8_t *in, size_t n, const char *what) { + ScrStr *latin1 = encs[0], *ascii = encs[1], *utf16le = encs[2], *hex = encs[3], *b64 = encs[4]; + ScrStr *s = scr_str_new((const char *)in, n); + uint16_t units[64]; + size_t k = ref_units(in, n, units); + ScrStr *narrow[] = {latin1, ascii}; + for (size_t e = 0; e < 2; e++) { + ScrBytes *b = scr_bytes_from_str(s, narrow[e]); + bool ok = b->len == k; + for (size_t j = 0; ok && j < k; j++) ok = b->data[j] == (uint8_t)units[j]; + check(ok, what); + scr_bytes_release(b); + } + ScrBytes *wide = scr_bytes_from_str(s, utf16le); + bool ok = wide->len == 2 * k; + for (size_t j = 0; ok && j < k; j++) + ok = wide->data[2 * j] == (uint8_t)units[j] && wide->data[2 * j + 1] == units[j] >> 8; + check(ok, what); + scr_bytes_release(wide); + scr_bytes_release(scr_bytes_from_str(s, hex)); + scr_bytes_release(scr_bytes_from_str(s, b64)); + scr_str_release(s); +} + +static void test_raw_storage_encodings(void) { + /* Each pattern at every offset in an eight-byte word, alone and followed + * by ASCII, two-byte characters or more of the pattern. */ + static const struct { const char *bytes; size_t n; } patterns[] = { + {"\x80", 1}, {"\x80\x81\xbf\x90", 4}, {"\xc3", 1}, {"\xc3\xa9", 2}, + {"\xc3\xa9\xc3", 3}, {"\xc0\x80\xc1\xbf", 4}, {"\xe9\x41\xe9", 3}, + {"\xf0\x9f\x41", 3}, {"\xe2\x82", 2}, {"\xe2\x82\xac", 3}, + {"\xf0\x9f\x98\x80", 4}, {"\xed\xa0\x80", 3}, {"\xff\xfe\xf5", 3}, + {"\xdf\xbf\x80\x80", 4}, {"\xc2\x80\xc2", 3}, {"x\xe9y\x80\x80z\xdf", 7}, + }; + static const struct { const char *bytes; size_t n; } tails[] = { + {"", 0}, {"bcdefghij", 9}, {"\xc3\xa9\xc3\xa9\xc3\xa9\xc3\xa9", 8}, {"\x80\xc3\xa9", 3}, + }; + ScrStr *encs[] = {S("latin1"), S("ascii"), S("utf16le"), S("hex"), S("base64")}; + uint8_t buf[64]; + char what[96]; + for (size_t p = 0; p < sizeof patterns / sizeof patterns[0]; p++) { + for (size_t offset = 0; offset < 17; offset++) { + for (size_t t = 0; t < sizeof tails / sizeof tails[0]; t++) { + for (size_t lead = 0; lead < 2; lead++) { + /* lead: the offset is two-byte characters instead of ASCII. */ + size_t n = 0; + for (size_t j = 0; j < offset; j++) { + if (lead && j + 1 < offset) { + buf[n++] = 0xc3; + buf[n++] = 0xa9; + j++; + } else { + buf[n++] = 'a'; + } + } + memcpy(buf + n, patterns[p].bytes, patterns[p].n); + n += patterns[p].n; + memcpy(buf + n, tails[t].bytes, tails[t].n); + n += tails[t].n; + snprintf(what, sizeof what, "raw storage encodings: pattern %zu offset %zu tail %zu lead %zu", + p, offset, t, lead); + check_raw_encodings(encs, buf, n, what); + } + } + } + } + /* Random bytes, mostly high, of every length up to 40. */ + uint32_t seed = 12345; + for (int round = 0; round < 4000; round++) { + size_t n = (size_t)round % 41; + for (size_t j = 0; j < n; j++) { + seed = seed * 1103515245u + 12345u; + uint8_t r = (uint8_t)(seed >> 16); + buf[j] = (seed >> 27) & 1 ? (uint8_t)(0x80 | r) : (uint8_t)(r & 0x7f); + } + snprintf(what, sizeof what, "raw storage encodings: random round %d", round); + check_raw_encodings(encs, buf, n, what); + } + for (size_t e = 0; e < sizeof encs / sizeof encs[0]; e++) scr_str_release(encs[e]); +} + static void test_concat_and_u32be(void) { ScrArr *list = scr_arr_new(SCR_ELEM_BYTES, 0); ScrBytes *a = scr_bytes_new(SCR_BYTES_U8, 2); @@ -445,6 +574,7 @@ int main(int argc, char **argv) { scr_bytes_test_bulk_storage(); scr_bytes_test_buffer_pool(); test_encodings(); + test_raw_storage_encodings(); test_concat_and_u32be(); test_from_arr_and_random(); test_zlib_roundtrip(); diff --git a/packages/runtime/test/test_cycle.c b/packages/runtime/test/test_cycle.c index 160083d6b..08eaecad4 100644 --- a/packages/runtime/test/test_cycle.c +++ b/packages/runtime/test/test_cycle.c @@ -557,6 +557,78 @@ static void check_program_growth_cap(void) { scr_cyc_set_growth_cap(0); } +/* scr_cyc_set_nursery_threshold sets the trigger SCR_CYCLE_THRESHOLD sets, + * from the end of the next pass: with a trigger of 1024, dead rings released + * after a full pass wait until the 1024th, which collects them all. 0 + * restores the default. */ +static void check_program_nursery_threshold(void) { + enum { THRESHOLD = 1024 }; + if (getenv("SCR_CYCLE_THRESHOLD") != NULL) return; /* it takes precedence */ + scr_cyc_set_nursery_threshold(THRESHOLD); + scr_collect_cycles(); /* re-arm with the new trigger */ + size_t before = freed; + for (size_t i = 0; i < THRESHOLD - 1; i++) release_live(make_ring(1)); + check(freed == before, "a program's nursery trigger did not hold the pass back"); + release_live(make_ring(1)); + check(freed - before == THRESHOLD, "a program's nursery trigger did not run the pass"); + scr_cyc_set_nursery_threshold(0); + scr_collect_cycles(); +} + +/* How many dead rings the release path takes to run its next pass, from + * the end of the previous one: the nursery trigger in effect. */ +static size_t nursery_window(void) { + size_t before = freed, n = 0; + while (freed == before && n < 65536) { + release_live(make_ring(1)); + n++; + } + check(freed - before == n, "a nursery pass left dead rings behind"); + return n; +} + +/* A scheduled full pass that reclaims nothing: its only candidate is an + * older live leaf. */ +static void scheduled_pass_finding_nothing(Node *leaf) { + size_t before = freed; + read_live(leaf); + for (size_t i = 0; i < configured_nursery_threshold(); i++) + scr_cyc_collect_scheduled(); + check(freed == before, "a scheduled pass over a live leaf freed something"); +} + +/* Unpinned, the nursery trigger starts at 256 candidates. Each scheduled + * full pass that reclaims promoted rings doubles it, up to 4096; each one + * that reclaims nothing after the nursery passes freed rings halves it, + * down to 256. */ +static void check_adaptive_nursery_threshold(void) { + if (getenv("SCR_CYCLE_THRESHOLD") != NULL) return; /* it pins the trigger */ + scr_cyc_set_nursery_threshold(0); + scr_collect_cycles(); + check(nursery_window() == 256, "the unpinned nursery trigger did not start at 256"); + static const size_t widened[] = { 512, 1024, 2048, 4096, 4096 }; + for (size_t i = 0; i < sizeof(widened) / sizeof(widened[0]); i++) { + collect_dead_mature_rings(); + check(nursery_window() == widened[i], + "a full pass reclaiming promoted rings did not double the nursery trigger"); + } + + static const size_t narrowed[] = { 2048, 1024, 512, 256, 256 }; + Node *leaf = make_leaf(); + read_live(leaf); + scr_collect_cycles(); /* promote */ + for (size_t i = 0; i < sizeof(narrowed) / sizeof(narrowed[0]); i++) { + scheduled_pass_finding_nothing(leaf); + check(nursery_window() == narrowed[i], + "a full pass reclaiming nothing did not halve the nursery trigger"); + } + leaf->rc--; + scr_cyc_on_dead(leaf); + node_free(leaf); + scr_cyc_set_nursery_threshold(0); + scr_collect_cycles(); +} + /* The phase sweep (scr_collect_cycles_phase) is for a caller that knows a * phase's data has just died: it sweeps, and the growth schedule starts * over. After passes that find nothing have widened the old level's @@ -1242,6 +1314,44 @@ static void check_limited_pass(void) { check(freed - before == 4 * RING, "the live ring was not collected once dead"); } +/* A limited pass that reaches a live candidate it did not take leaves the + * candidate in its slot and in its generation, which names the slot's + * buffer: when the candidate dies afterwards, it leaves that buffer, and no + * later pass meets its freed block. */ +static void check_limited_pass_keeps_untaken_generation(void) { + enum { RING = 16 }; + if (configured_nursery_threshold() < 8) return; + scr_collect_cycles(); + size_t before = freed; + static ScrCycType cyc_type = { &node_trace, &node_free, 0 }; + Node *shared = scr_cyc_alloc(sizeof(*shared), &cyc_type); + shared->rc = 1; /* its owner */ + /* A dead ring the filter takes points at it. */ + Node *dead = make_ring(RING); + dead->other = shared; + shared->rc++; + /* It loses a reference and becomes a candidate the filter refuses. */ + shared->rc++; + release_live(shared); + release_live(dead); + ScrCycHdr *h = scr_cyc_hdr(shared); + unsigned gen = scr_cyc_gen(h); + check(scr_cyc_is_buffered(h), "the shared node is not a candidate"); + take(dead); + check(scr_cyc_collect_set(&taken, SCR_CYC_NURSERY) == RING, + "limited pass did not free the taken ring"); + check(scr_cyc_is_buffered(h) && scr_cyc_gen(h) == gen, + "limited pass moved a candidate it did not take out of its generation"); + memset(taken_slots, 0, sizeof taken_slots); + /* Its owner lets go: it dies directly, out of its buffer. */ + check(shared->rc == 1, "the shared node kept a reference from the freed ring"); + shared->rc--; + scr_cyc_on_dead(shared); + node_free(shared); + scr_collect_cycles(); + check(freed - before == RING + 1, "the passes after the limited one freed something else"); +} + /* A full pass whose fullest candidate buffer is the old generation's (more * entries than a pass keeps between passes), with nursery candidates * beside it: every dead ring in either buffer is reclaimed, the live rings @@ -1488,6 +1598,9 @@ static void check_acyclic_objects(void) { } int main(void) { + /* The scheduling checks count candidates against a fixed nursery trigger; + * check_adaptive_nursery_threshold covers the unpinned one. */ + scr_cyc_set_nursery_threshold((long)configured_nursery_threshold()); check_inline_buffering(); check_old_garbage_reclaimed_while_growing(); check_older_rings_into_old(); @@ -1518,10 +1631,13 @@ int main(void) { check_allocation_growth_runs_passes(); check_publication_takes_candidates(); check_limited_pass(); + check_limited_pass_keeps_untaken_generation(); check_large_old_buffer_with_nursery_candidates(); check_unjudged_backlog_follows_growth(); check_acyclic_objects(); check_program_growth_cap(); + check_program_nursery_threshold(); + check_adaptive_nursery_threshold(); printf("cycle collection checks passed: threshold=%zu growth_cap=%zu\n", configured_nursery_threshold(), configured_growth_cap()); return 0; diff --git a/packages/runtime/test/test_heap_trim.c b/packages/runtime/test/test_heap_trim.c new file mode 100644 index 000000000..afd140430 --- /dev/null +++ b/packages/runtime/test/test_heap_trim.c @@ -0,0 +1,145 @@ +/* scr_heap_trim: after most blocks of a class are freed, the pages holding + * only free blocks go back to the system, the blocks still allocated keep + * their contents, and the class serves new blocks that overlap none of + * them. Built with and without SCR_WORKERS; with it, another thread + * allocates and frees its own blocks throughout, and frees blocks of the + * trimming thread before the trim (remote frees). */ +#include "../src/scr_runtime.h" + +#include +#include +#include +#include +#include +#ifdef SCR_WORKERS +#include +#include +#endif + +#define COUNT 400000 +#define SIZE 48 + +_Noreturn void scr_trap(const char *msg) { + fputs(msg, stderr); + exit(EXIT_FAILURE); +} + +/* One block kept per KEEP_PAGES pages of blocks, whatever the page size: a + * kept block pins its page, so the others hold only free blocks. */ +#define KEEP_PAGES 4 +static size_t keep_every; + +static void check(bool condition, const char *message) { + if (condition) return; + fprintf(stderr, "heap trim test failed: %s\n", message); + exit(EXIT_FAILURE); +} + +/* Resident bytes of this process. */ +static size_t resident(void) { + FILE *f = fopen("/proc/self/statm", "r"); + check(f != NULL, "open /proc/self/statm"); + unsigned long size = 0, pages = 0; + check(fscanf(f, "%lu %lu", &size, &pages) == 2, "read /proc/self/statm"); + fclose(f); + return (size_t)pages * (size_t)sysconf(_SC_PAGESIZE); +} + +#ifdef SCR_WORKERS +static atomic_bool churning = true; + +/* Allocates, fills and frees blocks of several sizes until told to stop. */ +static void *churn(void *arg) { + (void)arg; + void *held[256] = {0}; + for (size_t round = 0; atomic_load(&churning); round++) { + size_t slot = round % 256, size = 16 + (round % 7) * 24; + if (held[slot]) scr_mem_free(held[slot]); + held[slot] = scr_mem_alloc(size); + check(held[slot] != NULL, "allocate on the other thread"); + memset(held[slot], 0x33, size); + } + for (size_t i = 0; i < 256; i++) + if (held[i]) scr_mem_free(held[i]); + return NULL; +} + +/* Frees `count` blocks starting at `first` from another thread. */ +typedef struct { + unsigned char **blocks; + size_t first, count; +} RemoteFree; + +static void *free_remotely(void *arg) { + RemoteFree *r = arg; + for (size_t i = r->first; i < r->first + r->count; i++) + if (i % keep_every != 0) { + scr_mem_free(r->blocks[i]); + r->blocks[i] = NULL; + } + return NULL; +} +#endif + +static int compare(const void *a, const void *b) { + uintptr_t x = *(const uintptr_t *)a, y = *(const uintptr_t *)b; + return x < y ? -1 : x > y; +} + +int main(void) { + keep_every = KEEP_PAGES * ((size_t)sysconf(_SC_PAGESIZE) / SIZE + 1); +#ifdef SCR_WORKERS + pthread_t churner; + check(pthread_create(&churner, NULL, churn, NULL) == 0, "start the other thread"); +#endif + unsigned char **blocks = malloc(COUNT * sizeof *blocks); + check(blocks != NULL, "block table"); + for (size_t i = 0; i < COUNT; i++) { + blocks[i] = scr_mem_alloc(SIZE); + check(blocks[i] != NULL, "allocate"); + memset(blocks[i], (int)(i & 0xff), SIZE); + } + size_t kept = 0; + uintptr_t *keep = malloc((COUNT / keep_every + 1) * sizeof *keep); + check(keep != NULL, "keep table"); +#ifdef SCR_WORKERS + RemoteFree remote = {blocks, 0, COUNT / 4}; + pthread_t freer; + check(pthread_create(&freer, NULL, free_remotely, &remote) == 0, "start the freeing thread"); + check(pthread_join(freer, NULL) == 0, "join the freeing thread"); +#endif + for (size_t i = 0; i < COUNT; i++) { + if (i % keep_every == 0) { + keep[kept++] = (uintptr_t)blocks[i]; + continue; + } + if (blocks[i]) scr_mem_free(blocks[i]); + } + size_t before = resident(); + scr_heap_trim(); + size_t after = resident(); + check(before > after && before - after >= (size_t)8 << 20, "the free pages go back to the system"); + for (size_t i = 0; i < COUNT; i += keep_every) + for (size_t j = 0; j < SIZE; j++) check(blocks[i][j] == (unsigned char)(i & 0xff), "kept blocks keep their contents"); + qsort(keep, kept, sizeof *keep, compare); + for (size_t i = 0; i < COUNT; i++) { + if (i % keep_every == 0) continue; + unsigned char *p = scr_mem_alloc(SIZE); + check(p != NULL, "allocate after the trim"); + uintptr_t key = (uintptr_t)p; + check(bsearch(&key, keep, kept, sizeof *keep, compare) == NULL, "a new block is not a kept one"); + memset(p, 0x5a, SIZE); + blocks[i] = p; + } + for (size_t i = 0; i < COUNT; i += keep_every) + for (size_t j = 0; j < SIZE; j++) check(blocks[i][j] == (unsigned char)(i & 0xff), "kept blocks survive new allocations"); + for (size_t i = 0; i < COUNT; i++) scr_mem_free(blocks[i]); +#ifdef SCR_WORKERS + atomic_store(&churning, false); + check(pthread_join(churner, NULL) == 0, "join the other thread"); +#endif + free(keep); + free(blocks); + printf("heap trim checks passed\n"); + return 0; +} diff --git a/packages/runtime/test/test_map.c b/packages/runtime/test/test_map.c index 46805da1e..e23618f77 100644 --- a/packages/runtime/test/test_map.c +++ b/packages/runtime/test/test_map.c @@ -422,12 +422,14 @@ static void test_live_iteration_indices(void) { /* A small (linear) map that must grow its lookup index while an iteration * is active may not compact: entry indices, tombstones and the append * position stay put. Integer keys in a compact range move to the direct - * index (dense) from the fifth entry, any other number keys to the bucket + * index (dense) from the second entry, any other number keys to the bucket * table from the ninth; both transitions are covered by running the same - * script with `offset` 0 and 0.5. */ + * script with `offset` 0 and 0.5. The walk deletes `victim`, an existing + * key, then appends the key that makes the index grow. */ static void run_small_storage_transitions(double offset) { bool dense = offset == 0; - int n = dense ? 4 : 8; /* entries a linear map holds before its index grows */ + int n = dense ? 1 : 8; /* entries a linear map holds before its index grows */ + int victim = dense ? 0 : 1; ScrMap *m = scr_map_new(SCR_MAP_KEY_F64, SCR_MAP_VAL_F64, NULL); for (int i = 0; i < n; i++) scr_map_set_f64_f64(m, i + offset, i + 10); check(m->nbuckets == 0 && m->buckets == NULL && m->ndense == 0, @@ -441,22 +443,23 @@ static void run_small_storage_transitions(double offset) { ScrMap *copy = scr_map_clone(m, false, NULL); check(copy->nbuckets == 0 && copy->ndense == 0, "small clone preserves compact storage"); scr_map_iter_enter(m); - scr_map_delete_f64(m, 1 + offset); + scr_map_delete_f64(m, victim + offset); scr_map_set_f64_f64(m, n + offset, 14); check(dense ? m->ndense > 0 && m->nbuckets == 0 : m->nbuckets > 0 && m->ndense == 0, "live iteration promotes without moving entry indices"); - check(m->nentries == (uint32_t)n + 1 && !scr_map_iter_live(m, 1) && + check(m->nentries == (uint32_t)n + 1 && !scr_map_iter_live(m, victim) && scr_map_iter_key_f64(m, n) == n + offset, "promotion preserves tombstones and append position"); for (int i = n + 1; i < 64; i++) scr_map_set_f64_f64(m, i + offset, i + 10); - check(m->nentries == 64 && !scr_map_iter_live(m, 1) && scr_map_iter_key_f64(m, 63) == 63 + offset, + check(m->nentries == 64 && !scr_map_iter_live(m, victim) && scr_map_iter_key_f64(m, 63) == 63 + offset, "growth during iteration keeps entry indices"); double out = 0; - check(scr_map_get_f64_f64(m, offset, &out) && out == 10, - "original key survives promotion and growth"); - check(!scr_map_has_f64(m, 1 + offset), "deleted small key stays absent after rehash"); + check(dense ? scr_map_get_f64_f64(m, n + offset, &out) && out == 14 + : scr_map_get_f64_f64(m, offset, &out) && out == 10, + "keys written before the promotion survive it and the growth"); + check(!scr_map_has_f64(m, victim + offset), "deleted small key stays absent after rehash"); scr_map_iter_exit(m); - check(scr_map_has_f64(copy, 1 + offset) && !scr_map_has_f64(copy, n + offset), + check(scr_map_has_f64(copy, victim + offset) && !scr_map_has_f64(copy, n + offset), "small clone remains independent"); scr_map_clear(copy); scr_map_iter_enter(copy); diff --git a/packages/runtime/test/test_string.c b/packages/runtime/test/test_string.c index e49d8eb8b..f41c2de50 100644 --- a/packages/runtime/test/test_string.c +++ b/packages/runtime/test/test_string.c @@ -522,6 +522,48 @@ static void short_string_asserts(void) { scr_str_release(source); } +/* A loop's publish converts a non-ASCII receiver before any read, while a + * lone read still walks bytes. ASCII receivers publish nothing. A loop + * that can read fewer than one unit per 16 bytes counts as one read. */ +static void scan_units_asserts(void) { + ScrStr *s = scr_str_new("a\xC3\xA9\xF0\x9F\x98\x80" "b", 8); /* a é 😀 b */ + static const double codes[] = {97, 233, 0xD83D, 0xDE00, 98}; + if (scr_str_char_code_at(s, 0) != 97 || scr_str_scan.s == s) failed++; + scr_str_scan_units_for(s, NAN, NAN); + if (scr_str_scan.s != s || scr_str_scan.n != 5) failed++; + for (size_t i = 0; i < 5; i++) + if (scr_str_scan.n == 5 && (scr_str_scan.units[i] != codes[i] || + scr_str_char_code_at(s, (double)i) != codes[i])) failed++; + if (!isnan(scr_str_char_code_at(s, 5))) failed++; + scr_str_scan_units_for(s, NAN, NAN); /* republishing keeps the same units */ + const uint16_t *units = scr_str_scan.units; + scr_str_scan_units_for(s, 0, 1); + if (scr_str_scan.s != s || scr_str_scan.units != units) failed++; + ScrStr *ascii = scr_str_new("plain", 5); + scr_str_scan_units_for(ascii, NAN, NAN); + if (scr_str_scan.s != s) failed++; + scr_str_release(ascii); + scr_str_release(s); + if (scr_str_scan.s != NULL) failed++; + /* 200 units in 400 bytes: a loop over 25 indexes converts at once. */ + char text[400]; + for (size_t i = 0; i < sizeof text; i += 2) memcpy(text + i, "\xC3\xA9", 2); + ScrStr *wide = scr_str_new(text, sizeof text); + scr_str_scan_units_for(wide, 175, 200); + if (scr_str_scan.s != wide || scr_str_scan.n != 200 || scr_str_scan.units[199] != 233) failed++; + scr_str_release(wide); + /* A loop over 24 does not; each such loop counts as one read, so the + * 32nd converts (32 reads, one per 12.5 bytes). */ + ScrStr *line = scr_str_new(text, sizeof text); + scr_str_scan_units_for(line, 24, 0); + if (scr_str_scan.s == line) failed++; + for (int i = 1; i < 31; i++) scr_str_scan_units_for(line, i, i + 1); + if (scr_str_scan.s == line) failed++; + scr_str_scan_units_for(line, 31, 32); + if (scr_str_scan.s != line || scr_str_scan.n != 200) failed++; + scr_str_release(line); + total++; +} #ifdef SCR_SIDX_TEST static void sidx_fail(const char *what) { failed++; @@ -1153,6 +1195,7 @@ int main(int argc, char **argv) { split_scratch_asserts(); accumulation_asserts(); short_string_asserts(); + scan_units_asserts(); #ifdef SCR_SIDX_TEST sparse_index_asserts(); sparse_ascii_prefix_asserts(); diff --git a/packages/runtime/vendor/README.md b/packages/runtime/vendor/README.md index 7ab8cf6b0..53238410e 100644 --- a/packages/runtime/vendor/README.md +++ b/packages/runtime/vendor/README.md @@ -16,6 +16,13 @@ The x86_64 and AArch64 register-save/restore and `makecontext` logic is adapted The double-to-shortest-decimal core of Ryū (d2s and its headers only — the float and fixed/exponent variants are not vendored). scr_number.c textually `#include`s d2s.c and calls its internal `d2d` digit generator; ECMA-262 digit *placement* stays in scr_number.c. The only modification to upstream files: `#include "ryu/x.h"` flattened to `#include "x.h"` in d2s.c and d2s_intrinsics.h so the directory is self-contained. To update, re-fetch the listed files at the new commit and re-apply that include flattening. +## v8-ieee754/ (derived source) + +- Project: https://github.com/v8/v8, `src/base/ieee754.cc` as Node v24.22.0 bundles it (`deps/v8`, V8 13.6.233.17) +- License: BSD-3-Clause (V8), with fdlibm's notice (see v8-ieee754/LICENSE) + +V8's adaptation of fdlibm, the code behind Node's Math.sin, Math.exp, Math.cbrt and the other transcendental functions on every platform. `src/scr_ieee754.c` is a C port of the file, so compiled Math members print the values Node prints. The function bodies are unchanged; the port renames the entry points (`scr_ieee754_*`), gives the kernels internal linkage, and replaces V8's C++ helpers (bit casts, wraparound arithmetic, NaN and infinity constants) with C. `legacy::pow` is not ported: Node's Math.pow calls the platform pow. To update, re-fetch `deps/v8/src/base/ieee754.cc` at the new Node version and repeat the same conversion. + ## quickjs-ng/ - Project: https://github.com/quickjs-ng/quickjs @@ -39,6 +46,16 @@ The CROSS-target arm of node:zlib (see packages/runtime/src/scr_zlib.c): host bu The objects build lazily on the first zlib-using cross compile into the per-user build cache's `vendor/zlib----/` directory — one flavor per driver/target and compiler environment/identity (the lre-objects pattern). Zlib-free binaries never compile any of this; compressed OUTPUT bytes are zlib-version-dependent, which is why the corpus only compares round-trips and fixed-blob inflation. +## brotli/ + +- Project: https://github.com/google/brotli +- Version: 1.2.0 (release tarball brotli-1.2.0.tar.gz, sha256 816c96e8e8f193b40151dad7e8ff37b1221d019dbcb9c35cd3fadbfe6477dfec), the version Node v24.22.0 bundles (`process.versions.brotli`) +- License: MIT (see brotli/LICENSE) + +The codec behind node:zlib's Brotli functions and streams (see packages/runtime/src/scr_brotli.c). Only `c/common`, `c/dec`, `c/enc` and `c/include` are vendored, in upstream's layout (LICENSE beside `c/`); the C++ lazy static initializer (`c/enc/static_init_lazy.cc`), the tools, tests, bindings and build machinery are not. No vendored file is modified, and the files match the copy Node bundles in `deps/brotli`. Because the version is Node's, compressed bytes match Node's for the same parameters, so the corpus compares compressed output as well as round trips. To update, re-fetch the release tarball, copy the same directories, and bump `BROTLI_VERSION` in vendor-inputs.ts. + +The `dec/` and `enc/` directories share file names (`static_init.c`, `state.h`, `prefix.h`), so archive members are named after their directory (`dec-static_init.o`). Brotli-free binaries compile and link none of it. + ## mbedtls/ - Project: https://github.com/Mbed-TLS/mbedtls diff --git a/packages/runtime/vendor/brotli/LICENSE b/packages/runtime/vendor/brotli/LICENSE new file mode 100644 index 000000000..33b7cdd2d --- /dev/null +++ b/packages/runtime/vendor/brotli/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2009, 2010, 2013-2016 by the Brotli Authors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/runtime/vendor/brotli/c/common/constants.c b/packages/runtime/vendor/brotli/c/common/constants.c new file mode 100644 index 000000000..ef5a2e2e0 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/common/constants.c @@ -0,0 +1,15 @@ +/* Copyright 2013 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +#include "constants.h" + +const BROTLI_MODEL("small") +BrotliPrefixCodeRange _kBrotliPrefixCodeRanges[BROTLI_NUM_BLOCK_LEN_SYMBOLS] = { + {1, 2}, {5, 2}, {9, 2}, {13, 2}, {17, 3}, {25, 3}, + {33, 3}, {41, 3}, {49, 4}, {65, 4}, {81, 4}, {97, 4}, + {113, 5}, {145, 5}, {177, 5}, {209, 5}, {241, 6}, {305, 6}, + {369, 7}, {497, 8}, {753, 9}, {1265, 10}, {2289, 11}, {4337, 12}, + {8433, 13}, {16625, 24}}; diff --git a/packages/runtime/vendor/brotli/c/common/constants.h b/packages/runtime/vendor/brotli/c/common/constants.h new file mode 100644 index 000000000..85e9802c0 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/common/constants.h @@ -0,0 +1,198 @@ +/* Copyright 2016 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/** + * @file + * Common constants used in decoder and encoder API. + */ + +#ifndef BROTLI_COMMON_CONSTANTS_H_ +#define BROTLI_COMMON_CONSTANTS_H_ + +#include "platform.h" + +/* Specification: 7.3. Encoding of the context map */ +#define BROTLI_CONTEXT_MAP_MAX_RLE 16 + +/* Specification: 2. Compressed representation overview */ +#define BROTLI_MAX_NUMBER_OF_BLOCK_TYPES 256 + +/* Specification: 3.3. Alphabet sizes: insert-and-copy length */ +#define BROTLI_NUM_LITERAL_SYMBOLS 256 +#define BROTLI_NUM_COMMAND_SYMBOLS 704 +#define BROTLI_NUM_BLOCK_LEN_SYMBOLS 26 +#define BROTLI_MAX_CONTEXT_MAP_SYMBOLS (BROTLI_MAX_NUMBER_OF_BLOCK_TYPES + \ + BROTLI_CONTEXT_MAP_MAX_RLE) +#define BROTLI_MAX_BLOCK_TYPE_SYMBOLS (BROTLI_MAX_NUMBER_OF_BLOCK_TYPES + 2) + +/* Specification: 3.5. Complex prefix codes */ +#define BROTLI_REPEAT_PREVIOUS_CODE_LENGTH 16 +#define BROTLI_REPEAT_ZERO_CODE_LENGTH 17 +#define BROTLI_CODE_LENGTH_CODES (BROTLI_REPEAT_ZERO_CODE_LENGTH + 1) +/* "code length of 8 is repeated" */ +#define BROTLI_INITIAL_REPEATED_CODE_LENGTH 8 + +/* "Large Window Brotli" */ + +/** + * The theoretical maximum number of distance bits specified for large window + * brotli, for 64-bit encoders and decoders. Even when in practice 32-bit + * encoders and decoders only support up to 30 max distance bits, the value is + * set to 62 because it affects the large window brotli file format. + * Specifically, it affects the encoding of simple huffman tree for distances, + * see Specification RFC 7932 chapter 3.4. + */ +#define BROTLI_LARGE_MAX_DISTANCE_BITS 62U +#define BROTLI_LARGE_MIN_WBITS 10 +/** + * The maximum supported large brotli window bits by the encoder and decoder. + * Large window brotli allows up to 62 bits, however the current encoder and + * decoder, designed for 32-bit integers, only support up to 30 bits maximum. + */ +#define BROTLI_LARGE_MAX_WBITS 30 + +/* Specification: 4. Encoding of distances */ +#define BROTLI_NUM_DISTANCE_SHORT_CODES 16 +/** + * Maximal number of "postfix" bits. + * + * Number of "postfix" bits is stored as 2 bits in meta-block header. + */ +#define BROTLI_MAX_NPOSTFIX 3 +#define BROTLI_MAX_NDIRECT 120 +#define BROTLI_MAX_DISTANCE_BITS 24U +#define BROTLI_DISTANCE_ALPHABET_SIZE(NPOSTFIX, NDIRECT, MAXNBITS) ( \ + BROTLI_NUM_DISTANCE_SHORT_CODES + (NDIRECT) + \ + ((MAXNBITS) << ((NPOSTFIX) + 1))) +/* BROTLI_NUM_DISTANCE_SYMBOLS == 1128 */ +#define BROTLI_NUM_DISTANCE_SYMBOLS \ + BROTLI_DISTANCE_ALPHABET_SIZE( \ + BROTLI_MAX_NDIRECT, BROTLI_MAX_NPOSTFIX, BROTLI_LARGE_MAX_DISTANCE_BITS) + +/* ((1 << 26) - 4) is the maximal distance that can be expressed in RFC 7932 + brotli stream using NPOSTFIX = 0 and NDIRECT = 0. With other NPOSTFIX and + NDIRECT values distances up to ((1 << 29) + 88) could be expressed. */ +#define BROTLI_MAX_DISTANCE 0x3FFFFFC + +/* ((1 << 31) - 4) is the safe distance limit. Using this number as a limit + allows safe distance calculation without overflows, given the distance + alphabet size is limited to corresponding size + (see kLargeWindowDistanceCodeLimits). */ +#define BROTLI_MAX_ALLOWED_DISTANCE 0x7FFFFFFC + + +/* Specification: 4. Encoding of Literal Insertion Lengths and Copy Lengths */ +#define BROTLI_NUM_INS_COPY_CODES 24 + +/* 7.1. Context modes and context ID lookup for literals */ +/* "context IDs for literals are in the range of 0..63" */ +#define BROTLI_LITERAL_CONTEXT_BITS 6 + +/* 7.2. Context ID for distances */ +#define BROTLI_DISTANCE_CONTEXT_BITS 2 + +/* 9.1. Format of the Stream Header */ +/* Number of slack bytes for window size. Don't confuse + with BROTLI_NUM_DISTANCE_SHORT_CODES. */ +#define BROTLI_WINDOW_GAP 16 +#define BROTLI_MAX_BACKWARD_LIMIT(W) (((size_t)1 << (W)) - BROTLI_WINDOW_GAP) + +typedef struct BrotliDistanceCodeLimit { + uint32_t max_alphabet_size; + uint32_t max_distance; +} BrotliDistanceCodeLimit; + +/* This function calculates maximal size of distance alphabet, such that the + distances greater than the given values can not be represented. + + This limits are designed to support fast and safe 32-bit decoders. + "32-bit" means that signed integer values up to ((1 << 31) - 1) could be + safely expressed. + + Brotli distance alphabet symbols do not represent consecutive distance + ranges. Each distance alphabet symbol (excluding direct distances and short + codes), represent interleaved (for NPOSTFIX > 0) range of distances. + A "group" of consecutive (1 << NPOSTFIX) symbols represent non-interleaved + range. Two consecutive groups require the same amount of "extra bits". + + It is important that distance alphabet represents complete "groups". + To avoid complex logic on encoder side about interleaved ranges + it was decided to restrict both sides to complete distance code "groups". + */ +BROTLI_UNUSED_FUNCTION BrotliDistanceCodeLimit BrotliCalculateDistanceCodeLimit( + uint32_t max_distance, uint32_t npostfix, uint32_t ndirect) { + BrotliDistanceCodeLimit result; + /* Marking this function as unused, because not all files + including "constants.h" use it -> compiler warns about that. */ + BROTLI_UNUSED(&BrotliCalculateDistanceCodeLimit); + if (max_distance <= ndirect) { + /* This case never happens / exists only for the sake of completeness. */ + result.max_alphabet_size = max_distance + BROTLI_NUM_DISTANCE_SHORT_CODES; + result.max_distance = max_distance; + return result; + } else { + /* The first prohibited value. */ + uint32_t forbidden_distance = max_distance + 1; + /* Subtract "directly" encoded region. */ + uint32_t offset = forbidden_distance - ndirect - 1; + uint32_t ndistbits = 0; + uint32_t tmp; + uint32_t half; + uint32_t group; + /* Postfix for the last dcode in the group. */ + uint32_t postfix = (1u << npostfix) - 1; + uint32_t extra; + uint32_t start; + /* Remove postfix and "head-start". */ + offset = (offset >> npostfix) + 4; + /* Calculate the number of distance bits. */ + tmp = offset / 2; + /* Poor-man's log2floor, to avoid extra dependencies. */ + while (tmp != 0) {ndistbits++; tmp = tmp >> 1;} + /* One bit is covered with subrange addressing ("half"). */ + ndistbits--; + /* Find subrange. */ + half = (offset >> ndistbits) & 1; + /* Calculate the "group" part of dcode. */ + group = ((ndistbits - 1) << 1) | half; + /* Calculated "group" covers the prohibited distance value. */ + if (group == 0) { + /* This case is added for correctness; does not occur for limit > 128. */ + result.max_alphabet_size = ndirect + BROTLI_NUM_DISTANCE_SHORT_CODES; + result.max_distance = ndirect; + return result; + } + /* Decrement "group", so it is the last permitted "group". */ + group--; + /* After group was decremented, ndistbits and half must be recalculated. */ + ndistbits = (group >> 1) + 1; + /* The last available distance in the subrange has all extra bits set. */ + extra = (1u << ndistbits) - 1; + /* Calculate region start. NB: ndistbits >= 1. */ + start = (1u << (ndistbits + 1)) - 4; + /* Move to subregion. */ + start += (group & 1) << ndistbits; + /* Calculate the alphabet size. */ + result.max_alphabet_size = ((group << npostfix) | postfix) + ndirect + + BROTLI_NUM_DISTANCE_SHORT_CODES + 1; + /* Calculate the maximal distance representable by alphabet. */ + result.max_distance = ((start + extra) << npostfix) + postfix + ndirect + 1; + return result; + } +} + +/* Represents the range of values belonging to a prefix code: + [offset, offset + 2^nbits) */ +typedef struct { + uint16_t offset; + uint8_t nbits; +} BrotliPrefixCodeRange; + +/* "Soft-private", it is exported, but not "advertised" as API. */ +BROTLI_COMMON_API extern const BROTLI_MODEL("small") +BrotliPrefixCodeRange _kBrotliPrefixCodeRanges[BROTLI_NUM_BLOCK_LEN_SYMBOLS]; + +#endif /* BROTLI_COMMON_CONSTANTS_H_ */ diff --git a/packages/runtime/vendor/brotli/c/common/context.c b/packages/runtime/vendor/brotli/c/common/context.c new file mode 100644 index 000000000..d3c910cb1 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/common/context.c @@ -0,0 +1,156 @@ +#include "context.h" + +#include "platform.h" + +/* Common context lookup table for all context modes. */ +const BROTLI_MODEL("small") uint8_t _kBrotliContextLookupTable[2048] = { + /* CONTEXT_LSB6, last byte. */ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, + 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, + 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, + 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, + 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, + + /* CONTEXT_LSB6, second last byte, */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + + /* CONTEXT_MSB6, last byte. */ + 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, + 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7, + 8, 8, 8, 8, 9, 9, 9, 9, 10, 10, 10, 10, 11, 11, 11, 11, + 12, 12, 12, 12, 13, 13, 13, 13, 14, 14, 14, 14, 15, 15, 15, 15, + 16, 16, 16, 16, 17, 17, 17, 17, 18, 18, 18, 18, 19, 19, 19, 19, + 20, 20, 20, 20, 21, 21, 21, 21, 22, 22, 22, 22, 23, 23, 23, 23, + 24, 24, 24, 24, 25, 25, 25, 25, 26, 26, 26, 26, 27, 27, 27, 27, + 28, 28, 28, 28, 29, 29, 29, 29, 30, 30, 30, 30, 31, 31, 31, 31, + 32, 32, 32, 32, 33, 33, 33, 33, 34, 34, 34, 34, 35, 35, 35, 35, + 36, 36, 36, 36, 37, 37, 37, 37, 38, 38, 38, 38, 39, 39, 39, 39, + 40, 40, 40, 40, 41, 41, 41, 41, 42, 42, 42, 42, 43, 43, 43, 43, + 44, 44, 44, 44, 45, 45, 45, 45, 46, 46, 46, 46, 47, 47, 47, 47, + 48, 48, 48, 48, 49, 49, 49, 49, 50, 50, 50, 50, 51, 51, 51, 51, + 52, 52, 52, 52, 53, 53, 53, 53, 54, 54, 54, 54, 55, 55, 55, 55, + 56, 56, 56, 56, 57, 57, 57, 57, 58, 58, 58, 58, 59, 59, 59, 59, + 60, 60, 60, 60, 61, 61, 61, 61, 62, 62, 62, 62, 63, 63, 63, 63, + + /* CONTEXT_MSB6, second last byte, */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + + /* CONTEXT_UTF8, last byte. */ + /* ASCII range. */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 0, 0, 4, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 8, 12, 16, 12, 12, 20, 12, 16, 24, 28, 12, 12, 32, 12, 36, 12, + 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 32, 32, 24, 40, 28, 12, + 12, 48, 52, 52, 52, 48, 52, 52, 52, 48, 52, 52, 52, 52, 52, 48, + 52, 52, 52, 52, 52, 48, 52, 52, 52, 52, 52, 24, 12, 28, 12, 12, + 12, 56, 60, 60, 60, 56, 60, 60, 60, 56, 60, 60, 60, 60, 60, 56, + 60, 60, 60, 60, 60, 56, 60, 60, 60, 60, 60, 24, 12, 28, 12, 0, + /* UTF8 continuation byte range. */ + 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, + 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, + 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, + 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, + /* UTF8 lead byte range. */ + 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, + 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, + 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, + 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, + + /* CONTEXT_UTF8 second last byte. */ + /* ASCII range. */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, + 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, + 1, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 1, 1, 1, 0, + /* UTF8 continuation byte range. */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + /* UTF8 lead byte range. */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + + /* CONTEXT_SIGNED, last byte, same as the above values shifted by 3 bits. */ + 0, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, + 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, + 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, + 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, + 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, + 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, + 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 56, + + /* CONTEXT_SIGNED, second last byte. */ + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, +}; diff --git a/packages/runtime/vendor/brotli/c/common/context.h b/packages/runtime/vendor/brotli/c/common/context.h new file mode 100644 index 000000000..45b57d8c3 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/common/context.h @@ -0,0 +1,112 @@ +/* Copyright 2013 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* Lookup table to map the previous two bytes to a context id. + + There are four different context modeling modes defined here: + CONTEXT_LSB6: context id is the least significant 6 bits of the last byte, + CONTEXT_MSB6: context id is the most significant 6 bits of the last byte, + CONTEXT_UTF8: second-order context model tuned for UTF8-encoded text, + CONTEXT_SIGNED: second-order context model tuned for signed integers. + + If |p1| and |p2| are the previous two bytes, and |mode| is current context + mode, we calculate the context as: + + context = ContextLut(mode)[p1] | ContextLut(mode)[p2 + 256]. + + For CONTEXT_UTF8 mode, if the previous two bytes are ASCII characters + (i.e. < 128), this will be equivalent to + + context = 4 * context1(p1) + context2(p2), + + where context1 is based on the previous byte in the following way: + + 0 : non-ASCII control + 1 : \t, \n, \r + 2 : space + 3 : other punctuation + 4 : " ' + 5 : % + 6 : ( < [ { + 7 : ) > ] } + 8 : , ; : + 9 : . + 10 : = + 11 : number + 12 : upper-case vowel + 13 : upper-case consonant + 14 : lower-case vowel + 15 : lower-case consonant + + and context2 is based on the second last byte: + + 0 : control, space + 1 : punctuation + 2 : upper-case letter, number + 3 : lower-case letter + + If the last byte is ASCII, and the second last byte is not (in a valid UTF8 + stream it will be a continuation byte, value between 128 and 191), the + context is the same as if the second last byte was an ASCII control or space. + + If the last byte is a UTF8 lead byte (value >= 192), then the next byte will + be a continuation byte and the context id is 2 or 3 depending on the LSB of + the last byte and to a lesser extent on the second last byte if it is ASCII. + + If the last byte is a UTF8 continuation byte, the second last byte can be: + - continuation byte: the next byte is probably ASCII or lead byte (assuming + 4-byte UTF8 characters are rare) and the context id is 0 or 1. + - lead byte (192 - 207): next byte is ASCII or lead byte, context is 0 or 1 + - lead byte (208 - 255): next byte is continuation byte, context is 2 or 3 + + The possible value combinations of the previous two bytes, the range of + context ids and the type of the next byte is summarized in the table below: + + |--------\-----------------------------------------------------------------| + | \ Last byte | + | Second \---------------------------------------------------------------| + | last byte \ ASCII | cont. byte | lead byte | + | \ (0-127) | (128-191) | (192-) | + |=============|===================|=====================|==================| + | ASCII | next: ASCII/lead | not valid | next: cont. | + | (0-127) | context: 4 - 63 | | context: 2 - 3 | + |-------------|-------------------|---------------------|------------------| + | cont. byte | next: ASCII/lead | next: ASCII/lead | next: cont. | + | (128-191) | context: 4 - 63 | context: 0 - 1 | context: 2 - 3 | + |-------------|-------------------|---------------------|------------------| + | lead byte | not valid | next: ASCII/lead | not valid | + | (192-207) | | context: 0 - 1 | | + |-------------|-------------------|---------------------|------------------| + | lead byte | not valid | next: cont. | not valid | + | (208-) | | context: 2 - 3 | | + |-------------|-------------------|---------------------|------------------| +*/ + +#ifndef BROTLI_COMMON_CONTEXT_H_ +#define BROTLI_COMMON_CONTEXT_H_ + +#include "platform.h" + +typedef enum ContextType { + CONTEXT_LSB6 = 0, + CONTEXT_MSB6 = 1, + CONTEXT_UTF8 = 2, + CONTEXT_SIGNED = 3 +} ContextType; + +/* "Soft-private", it is exported, but not "advertised" as API. */ +/* Common context lookup table for all context modes. */ +BROTLI_COMMON_API extern const uint8_t _kBrotliContextLookupTable[2048]; + +typedef const uint8_t* ContextLut; + +/* typeof(MODE) == ContextType; returns ContextLut */ +#define BROTLI_CONTEXT_LUT(MODE) (&_kBrotliContextLookupTable[(MODE) << 9]) + +/* typeof(LUT) == ContextLut */ +#define BROTLI_CONTEXT(P1, P2, LUT) ((LUT)[P1] | ((LUT) + 256)[P2]) + +#endif /* BROTLI_COMMON_CONTEXT_H_ */ diff --git a/packages/runtime/vendor/brotli/c/common/dictionary.c b/packages/runtime/vendor/brotli/c/common/dictionary.c new file mode 100644 index 000000000..ef43af9bf --- /dev/null +++ b/packages/runtime/vendor/brotli/c/common/dictionary.c @@ -0,0 +1,64 @@ +/* Copyright 2013 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +#include "dictionary.h" +#include "platform.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +#if !defined(BROTLI_EXTERNAL_DICTIONARY_DATA) +/* Embed kBrotliDictionaryData */ +#include "dictionary_inc.h" +static const BROTLI_MODEL("small") BrotliDictionary kBrotliDictionary = { +#else +static BROTLI_MODEL("small") BrotliDictionary kBrotliDictionary = { +#endif + /* size_bits_by_length */ + { + 0, 0, 0, 0, 10, 10, 11, 11, + 10, 10, 10, 10, 10, 9, 9, 8, + 7, 7, 8, 7, 7, 6, 6, 5, + 5, 0, 0, 0, 0, 0, 0, 0 + }, + + /* offsets_by_length */ + { + 0, 0, 0, 0, 0, 4096, 9216, 21504, + 35840, 44032, 53248, 63488, 74752, 87040, 93696, 100864, + 104704, 106752, 108928, 113536, 115968, 118528, 119872, 121280, + 122016, 122784, 122784, 122784, 122784, 122784, 122784, 122784 + }, + + /* data_size == sizeof(kBrotliDictionaryData) */ + 122784, + + /* data */ +#if defined(BROTLI_EXTERNAL_DICTIONARY_DATA) + NULL +#else + kBrotliDictionaryData +#endif +}; + +const BrotliDictionary* BrotliGetDictionary(void) { + return &kBrotliDictionary; +} + +void BrotliSetDictionaryData(const uint8_t* data) { +#if defined(BROTLI_EXTERNAL_DICTIONARY_DATA) + if (!!data && !kBrotliDictionary.data) { + kBrotliDictionary.data = data; + } +#else + BROTLI_UNUSED(data); // Appease -Werror=unused-parameter +#endif +} + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif diff --git a/packages/runtime/vendor/brotli/c/common/dictionary.h b/packages/runtime/vendor/brotli/c/common/dictionary.h new file mode 100644 index 000000000..ed683b0ae --- /dev/null +++ b/packages/runtime/vendor/brotli/c/common/dictionary.h @@ -0,0 +1,63 @@ +/* Copyright 2013 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* Collection of static dictionary words. */ + +#ifndef BROTLI_COMMON_DICTIONARY_H_ +#define BROTLI_COMMON_DICTIONARY_H_ + +#include "platform.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +typedef struct BrotliDictionary { + /** + * Number of bits to encode index of dictionary word in a bucket. + * + * Specification: Appendix A. Static Dictionary Data + * + * Words in a dictionary are bucketed by length. + * @c 0 means that there are no words of a given length. + * Dictionary consists of words with length of [4..24] bytes. + * Values at [0..3] and [25..31] indices should not be addressed. + */ + uint8_t size_bits_by_length[32]; + + /* assert(offset[i + 1] == offset[i] + (bits[i] ? (i << bits[i]) : 0)) */ + uint32_t offsets_by_length[32]; + + /* assert(data_size == offsets_by_length[31]) */ + size_t data_size; + + /* Data array is not bound, and should obey to size_bits_by_length values. + Specified size matches default (RFC 7932) dictionary. Its size is + defined by data_size */ + const uint8_t* data; +} BrotliDictionary; + +BROTLI_COMMON_API const BrotliDictionary* BrotliGetDictionary(void); + +/** + * Sets dictionary data. + * + * When dictionary data is already set / present, this method is no-op. + * + * Dictionary data MUST be provided before BrotliGetDictionary is invoked. + * This method is used ONLY in multi-client environment (e.g. C + Java), + * to reduce storage by sharing single dictionary between implementations. + */ +BROTLI_COMMON_API void BrotliSetDictionaryData(const uint8_t* data); + +#define BROTLI_MIN_DICTIONARY_WORD_LENGTH 4 +#define BROTLI_MAX_DICTIONARY_WORD_LENGTH 24 + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif + +#endif /* BROTLI_COMMON_DICTIONARY_H_ */ diff --git a/packages/runtime/vendor/brotli/c/common/dictionary_inc.h b/packages/runtime/vendor/brotli/c/common/dictionary_inc.h new file mode 100644 index 000000000..f40a93ecc --- /dev/null +++ b/packages/runtime/vendor/brotli/c/common/dictionary_inc.h @@ -0,0 +1,5847 @@ +static const BROTLI_MODEL("small") uint8_t kBrotliDictionaryData[] = { +116,105,109,101,100,111,119,110,108,105,102,101,108,101,102,116,98,97,99,107,99, +111,100,101,100,97,116,97,115,104,111,119,111,110,108,121,115,105,116,101,99,105 +,116,121,111,112,101,110,106,117,115,116,108,105,107,101,102,114,101,101,119,111 +,114,107,116,101,120,116,121,101,97,114,111,118,101,114,98,111,100,121,108,111, +118,101,102,111,114,109,98,111,111,107,112,108,97,121,108,105,118,101,108,105, +110,101,104,101,108,112,104,111,109,101,115,105,100,101,109,111,114,101,119,111, +114,100,108,111,110,103,116,104,101,109,118,105,101,119,102,105,110,100,112,97, +103,101,100,97,121,115,102,117,108,108,104,101,97,100,116,101,114,109,101,97,99, +104,97,114,101,97,102,114,111,109,116,114,117,101,109,97,114,107,97,98,108,101, +117,112,111,110,104,105,103,104,100,97,116,101,108,97,110,100,110,101,119,115, +101,118,101,110,110,101,120,116,99,97,115,101,98,111,116,104,112,111,115,116,117 +,115,101,100,109,97,100,101,104,97,110,100,104,101,114,101,119,104,97,116,110,97 +,109,101,76,105,110,107,98,108,111,103,115,105,122,101,98,97,115,101,104,101,108 +,100,109,97,107,101,109,97,105,110,117,115,101,114,39,41,32,43,104,111,108,100, +101,110,100,115,119,105,116,104,78,101,119,115,114,101,97,100,119,101,114,101, +115,105,103,110,116,97,107,101,104,97,118,101,103,97,109,101,115,101,101,110,99, +97,108,108,112,97,116,104,119,101,108,108,112,108,117,115,109,101,110,117,102, +105,108,109,112,97,114,116,106,111,105,110,116,104,105,115,108,105,115,116,103, +111,111,100,110,101,101,100,119,97,121,115,119,101,115,116,106,111,98,115,109, +105,110,100,97,108,115,111,108,111,103,111,114,105,99,104,117,115,101,115,108,97 +,115,116,116,101,97,109,97,114,109,121,102,111,111,100,107,105,110,103,119,105, +108,108,101,97,115,116,119,97,114,100,98,101,115,116,102,105,114,101,80,97,103, +101,107,110,111,119,97,119,97,121,46,112,110,103,109,111,118,101,116,104,97,110, +108,111,97,100,103,105,118,101,115,101,108,102,110,111,116,101,109,117,99,104, +102,101,101,100,109,97,110,121,114,111,99,107,105,99,111,110,111,110,99,101,108, +111,111,107,104,105,100,101,100,105,101,100,72,111,109,101,114,117,108,101,104, +111,115,116,97,106,97,120,105,110,102,111,99,108,117,98,108,97,119,115,108,101, +115,115,104,97,108,102,115,111,109,101,115,117,99,104,122,111,110,101,49,48,48, +37,111,110,101,115,99,97,114,101,84,105,109,101,114,97,99,101,98,108,117,101,102 +,111,117,114,119,101,101,107,102,97,99,101,104,111,112,101,103,97,118,101,104,97 +,114,100,108,111,115,116,119,104,101,110,112,97,114,107,107,101,112,116,112,97, +115,115,115,104,105,112,114,111,111,109,72,84,77,76,112,108,97,110,84,121,112, +101,100,111,110,101,115,97,118,101,107,101,101,112,102,108,97,103,108,105,110, +107,115,111,108,100,102,105,118,101,116,111,111,107,114,97,116,101,116,111,119, +110,106,117,109,112,116,104,117,115,100,97,114,107,99,97,114,100,102,105,108,101 +,102,101,97,114,115,116,97,121,107,105,108,108,116,104,97,116,102,97,108,108,97, +117,116,111,101,118,101,114,46,99,111,109,116,97,108,107,115,104,111,112,118,111 +,116,101,100,101,101,112,109,111,100,101,114,101,115,116,116,117,114,110,98,111, +114,110,98,97,110,100,102,101,108,108,114,111,115,101,117,114,108,40,115,107,105 +,110,114,111,108,101,99,111,109,101,97,99,116,115,97,103,101,115,109,101,101,116 +,103,111,108,100,46,106,112,103,105,116,101,109,118,97,114,121,102,101,108,116, +116,104,101,110,115,101,110,100,100,114,111,112,86,105,101,119,99,111,112,121,49 +,46,48,34,60,47,97,62,115,116,111,112,101,108,115,101,108,105,101,115,116,111, +117,114,112,97,99,107,46,103,105,102,112,97,115,116,99,115,115,63,103,114,97,121 +,109,101,97,110,38,103,116,59,114,105,100,101,115,104,111,116,108,97,116,101,115 +,97,105,100,114,111,97,100,118,97,114,32,102,101,101,108,106,111,104,110,114,105 +,99,107,112,111,114,116,102,97,115,116,39,85,65,45,100,101,97,100,60,47,98,62, +112,111,111,114,98,105,108,108,116,121,112,101,85,46,83,46,119,111,111,100,109, +117,115,116,50,112,120,59,73,110,102,111,114,97,110,107,119,105,100,101,119,97, +110,116,119,97,108,108,108,101,97,100,91,48,93,59,112,97,117,108,119,97,118,101, +115,117,114,101,36,40,39,35,119,97,105,116,109,97,115,115,97,114,109,115,103,111 +,101,115,103,97,105,110,108,97,110,103,112,97,105,100,33,45,45,32,108,111,99,107 +,117,110,105,116,114,111,111,116,119,97,108,107,102,105,114,109,119,105,102,101, +120,109,108,34,115,111,110,103,116,101,115,116,50,48,112,120,107,105,110,100,114 +,111,119,115,116,111,111,108,102,111,110,116,109,97,105,108,115,97,102,101,115, +116,97,114,109,97,112,115,99,111,114,101,114,97,105,110,102,108,111,119,98,97,98 +,121,115,112,97,110,115,97,121,115,52,112,120,59,54,112,120,59,97,114,116,115, +102,111,111,116,114,101,97,108,119,105,107,105,104,101,97,116,115,116,101,112, +116,114,105,112,111,114,103,47,108,97,107,101,119,101,97,107,116,111,108,100,70, +111,114,109,99,97,115,116,102,97,110,115,98,97,110,107,118,101,114,121,114,117, +110,115,106,117,108,121,116,97,115,107,49,112,120,59,103,111,97,108,103,114,101, +119,115,108,111,119,101,100,103,101,105,100,61,34,115,101,116,115,53,112,120,59, +46,106,115,63,52,48,112,120,105,102,32,40,115,111,111,110,115,101,97,116,110,111 +,110,101,116,117,98,101,122,101,114,111,115,101,110,116,114,101,101,100,102,97, +99,116,105,110,116,111,103,105,102,116,104,97,114,109,49,56,112,120,99,97,109, +101,104,105,108,108,98,111,108,100,122,111,111,109,118,111,105,100,101,97,115, +121,114,105,110,103,102,105,108,108,112,101,97,107,105,110,105,116,99,111,115, +116,51,112,120,59,106,97,99,107,116,97,103,115,98,105,116,115,114,111,108,108, +101,100,105,116,107,110,101,119,110,101,97,114,60,33,45,45,103,114,111,119,74,83 +,79,78,100,117,116,121,78,97,109,101,115,97,108,101,121,111,117,32,108,111,116, +115,112,97,105,110,106,97,122,122,99,111,108,100,101,121,101,115,102,105,115,104 +,119,119,119,46,114,105,115,107,116,97,98,115,112,114,101,118,49,48,112,120,114, +105,115,101,50,53,112,120,66,108,117,101,100,105,110,103,51,48,48,44,98,97,108, +108,102,111,114,100,101,97,114,110,119,105,108,100,98,111,120,46,102,97,105,114, +108,97,99,107,118,101,114,115,112,97,105,114,106,117,110,101,116,101,99,104,105, +102,40,33,112,105,99,107,101,118,105,108,36,40,34,35,119,97,114,109,108,111,114, +100,100,111,101,115,112,117,108,108,44,48,48,48,105,100,101,97,100,114,97,119, +104,117,103,101,115,112,111,116,102,117,110,100,98,117,114,110,104,114,101,102, +99,101,108,108,107,101,121,115,116,105,99,107,104,111,117,114,108,111,115,115, +102,117,101,108,49,50,112,120,115,117,105,116,100,101,97,108,82,83,83,34,97,103, +101,100,103,114,101,121,71,69,84,34,101,97,115,101,97,105,109,115,103,105,114, +108,97,105,100,115,56,112,120,59,110,97,118,121,103,114,105,100,116,105,112,115, +35,57,57,57,119,97,114,115,108,97,100,121,99,97,114,115,41,59,32,125,112,104,112 +,63,104,101,108,108,116,97,108,108,119,104,111,109,122,104,58,229,42,47,13,10,32 +,49,48,48,104,97,108,108,46,10,10,65,55,112,120,59,112,117,115,104,99,104,97,116 +,48,112,120,59,99,114,101,119,42,47,60,47,104,97,115,104,55,53,112,120,102,108, +97,116,114,97,114,101,32,38,38,32,116,101,108,108,99,97,109,112,111,110,116,111, +108,97,105,100,109,105,115,115,115,107,105,112,116,101,110,116,102,105,110,101, +109,97,108,101,103,101,116,115,112,108,111,116,52,48,48,44,13,10,13,10,99,111, +111,108,102,101,101,116,46,112,104,112,60,98,114,62,101,114,105,99,109,111,115, +116,103,117,105,100,98,101,108,108,100,101,115,99,104,97,105,114,109,97,116,104, +97,116,111,109,47,105,109,103,38,35,56,50,108,117,99,107,99,101,110,116,48,48,48 +,59,116,105,110,121,103,111,110,101,104,116,109,108,115,101,108,108,100,114,117, +103,70,82,69,69,110,111,100,101,110,105,99,107,63,105,100,61,108,111,115,101,110 +,117,108,108,118,97,115,116,119,105,110,100,82,83,83,32,119,101,97,114,114,101, +108,121,98,101,101,110,115,97,109,101,100,117,107,101,110,97,115,97,99,97,112, +101,119,105,115,104,103,117,108,102,84,50,51,58,104,105,116,115,115,108,111,116, +103,97,116,101,107,105,99,107,98,108,117,114,116,104,101,121,49,53,112,120,39,39 +,41,59,41,59,34,62,109,115,105,101,119,105,110,115,98,105,114,100,115,111,114, +116,98,101,116,97,115,101,101,107,84,49,56,58,111,114,100,115,116,114,101,101, +109,97,108,108,54,48,112,120,102,97,114,109,226,128,153,115,98,111,121,115,91,48 +,93,46,39,41,59,34,80,79,83,84,98,101,97,114,107,105,100,115,41,59,125,125,109, +97,114,121,116,101,110,100,40,85,75,41,113,117,97,100,122,104,58,230,45,115,105, +122,45,45,45,45,112,114,111,112,39,41,59,13,108,105,102,116,84,49,57,58,118,105, +99,101,97,110,100,121,100,101,98,116,62,82,83,83,112,111,111,108,110,101,99,107, +98,108,111,119,84,49,54,58,100,111,111,114,101,118,97,108,84,49,55,58,108,101, +116,115,102,97,105,108,111,114,97,108,112,111,108,108,110,111,118,97,99,111,108, +115,103,101,110,101,32,226,128,148,115,111,102,116,114,111,109,101,116,105,108, +108,114,111,115,115,60,104,51,62,112,111,117,114,102,97,100,101,112,105,110,107, +60,116,114,62,109,105,110,105,41,124,33,40,109,105,110,101,122,104,58,232,98,97, +114,115,104,101,97,114,48,48,41,59,109,105,108,107,32,45,45,62,105,114,111,110, +102,114,101,100,100,105,115,107,119,101,110,116,115,111,105,108,112,117,116,115, +47,106,115,47,104,111,108,121,84,50,50,58,73,83,66,78,84,50,48,58,97,100,97,109, +115,101,101,115,60,104,50,62,106,115,111,110,39,44,32,39,99,111,110,116,84,50,49 +,58,32,82,83,83,108,111,111,112,97,115,105,97,109,111,111,110,60,47,112,62,115, +111,117,108,76,73,78,69,102,111,114,116,99,97,114,116,84,49,52,58,60,104,49,62, +56,48,112,120,33,45,45,60,57,112,120,59,84,48,52,58,109,105,107,101,58,52,54,90, +110,105,99,101,105,110,99,104,89,111,114,107,114,105,99,101,122,104,58,228,39,41 +,41,59,112,117,114,101,109,97,103,101,112,97,114,97,116,111,110,101,98,111,110, +100,58,51,55,90,95,111,102,95,39,93,41,59,48,48,48,44,122,104,58,231,116,97,110, +107,121,97,114,100,98,111,119,108,98,117,115,104,58,53,54,90,74,97,118,97,51,48, +112,120,10,124,125,10,37,67,51,37,58,51,52,90,106,101,102,102,69,88,80,73,99,97, +115,104,118,105,115,97,103,111,108,102,115,110,111,119,122,104,58,233,113,117, +101,114,46,99,115,115,115,105,99,107,109,101,97,116,109,105,110,46,98,105,110, +100,100,101,108,108,104,105,114,101,112,105,99,115,114,101,110,116,58,51,54,90, +72,84,84,80,45,50,48,49,102,111,116,111,119,111,108,102,69,78,68,32,120,98,111, +120,58,53,52,90,66,79,68,89,100,105,99,107,59,10,125,10,101,120,105,116,58,51,53 +,90,118,97,114,115,98,101,97,116,39,125,41,59,100,105,101,116,57,57,57,59,97,110 +,110,101,125,125,60,47,91,105,93,46,76,97,110,103,107,109,194,178,119,105,114, +101,116,111,121,115,97,100,100,115,115,101,97,108,97,108,101,120,59,10,9,125,101 +,99,104,111,110,105,110,101,46,111,114,103,48,48,53,41,116,111,110,121,106,101, +119,115,115,97,110,100,108,101,103,115,114,111,111,102,48,48,48,41,32,50,48,48, +119,105,110,101,103,101,97,114,100,111,103,115,98,111,111,116,103,97,114,121,99, +117,116,115,116,121,108,101,116,101,109,112,116,105,111,110,46,120,109,108,99, +111,99,107,103,97,110,103,36,40,39,46,53,48,112,120,80,104,46,68,109,105,115,99, +97,108,97,110,108,111,97,110,100,101,115,107,109,105,108,101,114,121,97,110,117, +110,105,120,100,105,115,99,41,59,125,10,100,117,115,116,99,108,105,112,41,46,10, +10,55,48,112,120,45,50,48,48,68,86,68,115,55,93,62,60,116,97,112,101,100,101,109 +,111,105,43,43,41,119,97,103,101,101,117,114,111,112,104,105,108,111,112,116,115 +,104,111,108,101,70,65,81,115,97,115,105,110,45,50,54,84,108,97,98,115,112,101, +116,115,85,82,76,32,98,117,108,107,99,111,111,107,59,125,13,10,72,69,65,68,91,48 +,93,41,97,98,98,114,106,117,97,110,40,49,57,56,108,101,115,104,116,119,105,110, +60,47,105,62,115,111,110,121,103,117,121,115,102,117,99,107,112,105,112,101,124, +45,10,33,48,48,50,41,110,100,111,119,91,49,93,59,91,93,59,10,76,111,103,32,115, +97,108,116,13,10,9,9,98,97,110,103,116,114,105,109,98,97,116,104,41,123,13,10,48 +,48,112,120,10,125,41,59,107,111,58,236,102,101,101,115,97,100,62,13,115,58,47, +47,32,91,93,59,116,111,108,108,112,108,117,103,40,41,123,10,123,13,10,32,46,106, +115,39,50,48,48,112,100,117,97,108,98,111,97,116,46,74,80,71,41,59,10,125,113, +117,111,116,41,59,10,10,39,41,59,10,13,10,125,13,50,48,49,52,50,48,49,53,50,48, +49,54,50,48,49,55,50,48,49,56,50,48,49,57,50,48,50,48,50,48,50,49,50,48,50,50,50 +,48,50,51,50,48,50,52,50,48,50,53,50,48,50,54,50,48,50,55,50,48,50,56,50,48,50, +57,50,48,51,48,50,48,51,49,50,48,51,50,50,48,51,51,50,48,51,52,50,48,51,53,50,48 +,51,54,50,48,51,55,50,48,49,51,50,48,49,50,50,48,49,49,50,48,49,48,50,48,48,57, +50,48,48,56,50,48,48,55,50,48,48,54,50,48,48,53,50,48,48,52,50,48,48,51,50,48,48 +,50,50,48,48,49,50,48,48,48,49,57,57,57,49,57,57,56,49,57,57,55,49,57,57,54,49, +57,57,53,49,57,57,52,49,57,57,51,49,57,57,50,49,57,57,49,49,57,57,48,49,57,56,57 +,49,57,56,56,49,57,56,55,49,57,56,54,49,57,56,53,49,57,56,52,49,57,56,51,49,57, +56,50,49,57,56,49,49,57,56,48,49,57,55,57,49,57,55,56,49,57,55,55,49,57,55,54,49 +,57,55,53,49,57,55,52,49,57,55,51,49,57,55,50,49,57,55,49,49,57,55,48,49,57,54, +57,49,57,54,56,49,57,54,55,49,57,54,54,49,57,54,53,49,57,54,52,49,57,54,51,49,57 +,54,50,49,57,54,49,49,57,54,48,49,57,53,57,49,57,53,56,49,57,53,55,49,57,53,54, +49,57,53,53,49,57,53,52,49,57,53,51,49,57,53,50,49,57,53,49,49,57,53,48,49,48,48 +,48,49,48,50,52,49,51,57,52,48,48,48,48,57,57,57,57,99,111,109,111,109,195,161, +115,101,115,116,101,101,115,116,97,112,101,114,111,116,111,100,111,104,97,99,101 +,99,97,100,97,97,195,177,111,98,105,101,110,100,195,173,97,97,115,195,173,118, +105,100,97,99,97,115,111,111,116,114,111,102,111,114,111,115,111,108,111,111,116 +,114,97,99,117,97,108,100,105,106,111,115,105,100,111,103,114,97,110,116,105,112 +,111,116,101,109,97,100,101,98,101,97,108,103,111,113,117,195,169,101,115,116, +111,110,97,100,97,116,114,101,115,112,111,99,111,99,97,115,97,98,97,106,111,116, +111,100,97,115,105,110,111,97,103,117,97,112,117,101,115,117,110,111,115,97,110, +116,101,100,105,99,101,108,117,105,115,101,108,108,97,109,97,121,111,122,111,110 +,97,97,109,111,114,112,105,115,111,111,98,114,97,99,108,105,99,101,108,108,111, +100,105,111,115,104,111,114,97,99,97,115,105,208,183,208,176,208,189,208,176,208 +,190,208,188,209,128,208,176,209,128,209,131,209,130,208,176,208,189,208,181,208 +,191,208,190,208,190,209,130,208,184,208,183,208,189,208,190,208,180,208,190,209 +,130,208,190,208,182,208,181,208,190,208,189,208,184,209,133,208,157,208,176,208 +,181,208,181,208,177,209,139,208,188,209,139,208,146,209,139,209,129,208,190,208 +,178,209,139,208,178,208,190,208,157,208,190,208,190,208,177,208,159,208,190,208 +,187,208,184,208,189,208,184,208,160,208,164,208,157,208,181,208,156,209,139,209 +,130,209,139,208,158,208,189,208,184,208,188,208,180,208,176,208,151,208,176,208 +,148,208,176,208,157,209,131,208,158,208,177,209,130,208,181,208,152,208,183,208 +,181,208,185,208,189,209,131,208,188,208,188,208,162,209,139,209,131,208,182,217 +,129,217,138,216,163,217,134,217,133,216,167,217,133,216,185,217,131,217,132,216 +,163,217,136,216,177,216,175,217,138,216,167,217,129,217,137,217,135,217,136,217 +,132,217,133,217,132,217,131,216,167,217,136,217,132,217,135,216,168,216,179,216 +,167,217,132,216,165,217,134,217,135,217,138,216,163,217,138,217,130,216,175,217 +,135,217,132,216,171,217,133,216,168,217,135,217,132,217,136,217,132,217,138,216 +,168,217,132,216,167,217,138,216,168,217,131,216,180,217,138,216,167,217,133,216 +,163,217,133,217,134,216,170,216,168,217,138,217,132,217,134,216,173,216,168,217 +,135,217,133,217,133,216,180,217,136,216,180,102,105,114,115,116,118,105,100,101 +,111,108,105,103,104,116,119,111,114,108,100,109,101,100,105,97,119,104,105,116, +101,99,108,111,115,101,98,108,97,99,107,114,105,103,104,116,115,109,97,108,108, +98,111,111,107,115,112,108,97,99,101,109,117,115,105,99,102,105,101,108,100,111, +114,100,101,114,112,111,105,110,116,118,97,108,117,101,108,101,118,101,108,116, +97,98,108,101,98,111,97,114,100,104,111,117,115,101,103,114,111,117,112,119,111, +114,107,115,121,101,97,114,115,115,116,97,116,101,116,111,100,97,121,119,97,116, +101,114,115,116,97,114,116,115,116,121,108,101,100,101,97,116,104,112,111,119, +101,114,112,104,111,110,101,110,105,103,104,116,101,114,114,111,114,105,110,112, +117,116,97,98,111,117,116,116,101,114,109,115,116,105,116,108,101,116,111,111, +108,115,101,118,101,110,116,108,111,99,97,108,116,105,109,101,115,108,97,114,103 +,101,119,111,114,100,115,103,97,109,101,115,115,104,111,114,116,115,112,97,99, +101,102,111,99,117,115,99,108,101,97,114,109,111,100,101,108,98,108,111,99,107, +103,117,105,100,101,114,97,100,105,111,115,104,97,114,101,119,111,109,101,110,97 +,103,97,105,110,109,111,110,101,121,105,109,97,103,101,110,97,109,101,115,121, +111,117,110,103,108,105,110,101,115,108,97,116,101,114,99,111,108,111,114,103, +114,101,101,110,102,114,111,110,116,38,97,109,112,59,119,97,116,99,104,102,111, +114,99,101,112,114,105,99,101,114,117,108,101,115,98,101,103,105,110,97,102,116, +101,114,118,105,115,105,116,105,115,115,117,101,97,114,101,97,115,98,101,108,111 +,119,105,110,100,101,120,116,111,116,97,108,104,111,117,114,115,108,97,98,101, +108,112,114,105,110,116,112,114,101,115,115,98,117,105,108,116,108,105,110,107, +115,115,112,101,101,100,115,116,117,100,121,116,114,97,100,101,102,111,117,110, +100,115,101,110,115,101,117,110,100,101,114,115,104,111,119,110,102,111,114,109, +115,114,97,110,103,101,97,100,100,101,100,115,116,105,108,108,109,111,118,101, +100,116,97,107,101,110,97,98,111,118,101,102,108,97,115,104,102,105,120,101,100, +111,102,116,101,110,111,116,104,101,114,118,105,101,119,115,99,104,101,99,107, +108,101,103,97,108,114,105,118,101,114,105,116,101,109,115,113,117,105,99,107, +115,104,97,112,101,104,117,109,97,110,101,120,105,115,116,103,111,105,110,103, +109,111,118,105,101,116,104,105,114,100,98,97,115,105,99,112,101,97,99,101,115, +116,97,103,101,119,105,100,116,104,108,111,103,105,110,105,100,101,97,115,119, +114,111,116,101,112,97,103,101,115,117,115,101,114,115,100,114,105,118,101,115, +116,111,114,101,98,114,101,97,107,115,111,117,116,104,118,111,105,99,101,115,105 +,116,101,115,109,111,110,116,104,119,104,101,114,101,98,117,105,108,100,119,104, +105,99,104,101,97,114,116,104,102,111,114,117,109,116,104,114,101,101,115,112, +111,114,116,112,97,114,116,121,67,108,105,99,107,108,111,119,101,114,108,105,118 +,101,115,99,108,97,115,115,108,97,121,101,114,101,110,116,114,121,115,116,111, +114,121,117,115,97,103,101,115,111,117,110,100,99,111,117,114,116,121,111,117, +114,32,98,105,114,116,104,112,111,112,117,112,116,121,112,101,115,97,112,112,108 +,121,73,109,97,103,101,98,101,105,110,103,117,112,112,101,114,110,111,116,101, +115,101,118,101,114,121,115,104,111,119,115,109,101,97,110,115,101,120,116,114, +97,109,97,116,99,104,116,114,97,99,107,107,110,111,119,110,101,97,114,108,121,98 +,101,103,97,110,115,117,112,101,114,112,97,112,101,114,110,111,114,116,104,108, +101,97,114,110,103,105,118,101,110,110,97,109,101,100,101,110,100,101,100,84,101 +,114,109,115,112,97,114,116,115,71,114,111,117,112,98,114,97,110,100,117,115,105 +,110,103,119,111,109,97,110,102,97,108,115,101,114,101,97,100,121,97,117,100,105 +,111,116,97,107,101,115,119,104,105,108,101,46,99,111,109,47,108,105,118,101,100 +,99,97,115,101,115,100,97,105,108,121,99,104,105,108,100,103,114,101,97,116,106, +117,100,103,101,116,104,111,115,101,117,110,105,116,115,110,101,118,101,114,98, +114,111,97,100,99,111,97,115,116,99,111,118,101,114,97,112,112,108,101,102,105, +108,101,115,99,121,99,108,101,115,99,101,110,101,112,108,97,110,115,99,108,105, +99,107,119,114,105,116,101,113,117,101,101,110,112,105,101,99,101,101,109,97,105 +,108,102,114,97,109,101,111,108,100,101,114,112,104,111,116,111,108,105,109,105, +116,99,97,99,104,101,99,105,118,105,108,115,99,97,108,101,101,110,116,101,114, +116,104,101,109,101,116,104,101,114,101,116,111,117,99,104,98,111,117,110,100, +114,111,121,97,108,97,115,107,101,100,119,104,111,108,101,115,105,110,99,101,115 +,116,111,99,107,32,110,97,109,101,102,97,105,116,104,104,101,97,114,116,101,109, +112,116,121,111,102,102,101,114,115,99,111,112,101,111,119,110,101,100,109,105, +103,104,116,97,108,98,117,109,116,104,105,110,107,98,108,111,111,100,97,114,114, +97,121,109,97,106,111,114,116,114,117,115,116,99,97,110,111,110,117,110,105,111, +110,99,111,117,110,116,118,97,108,105,100,115,116,111,110,101,83,116,121,108,101 +,76,111,103,105,110,104,97,112,112,121,111,99,99,117,114,108,101,102,116,58,102, +114,101,115,104,113,117,105,116,101,102,105,108,109,115,103,114,97,100,101,110, +101,101,100,115,117,114,98,97,110,102,105,103,104,116,98,97,115,105,115,104,111, +118,101,114,97,117,116,111,59,114,111,117,116,101,46,104,116,109,108,109,105,120 +,101,100,102,105,110,97,108,89,111,117,114,32,115,108,105,100,101,116,111,112, +105,99,98,114,111,119,110,97,108,111,110,101,100,114,97,119,110,115,112,108,105, +116,114,101,97,99,104,82,105,103,104,116,100,97,116,101,115,109,97,114,99,104, +113,117,111,116,101,103,111,111,100,115,76,105,110,107,115,100,111,117,98,116,97 +,115,121,110,99,116,104,117,109,98,97,108,108,111,119,99,104,105,101,102,121,111 +,117,116,104,110,111,118,101,108,49,48,112,120,59,115,101,114,118,101,117,110, +116,105,108,104,97,110,100,115,67,104,101,99,107,83,112,97,99,101,113,117,101, +114,121,106,97,109,101,115,101,113,117,97,108,116,119,105,99,101,48,44,48,48,48, +83,116,97,114,116,112,97,110,101,108,115,111,110,103,115,114,111,117,110,100,101 +,105,103,104,116,115,104,105,102,116,119,111,114,116,104,112,111,115,116,115,108 +,101,97,100,115,119,101,101,107,115,97,118,111,105,100,116,104,101,115,101,109, +105,108,101,115,112,108,97,110,101,115,109,97,114,116,97,108,112,104,97,112,108, +97,110,116,109,97,114,107,115,114,97,116,101,115,112,108,97,121,115,99,108,97, +105,109,115,97,108,101,115,116,101,120,116,115,115,116,97,114,115,119,114,111, +110,103,60,47,104,51,62,116,104,105,110,103,46,111,114,103,47,109,117,108,116, +105,104,101,97,114,100,80,111,119,101,114,115,116,97,110,100,116,111,107,101,110 +,115,111,108,105,100,40,116,104,105,115,98,114,105,110,103,115,104,105,112,115, +115,116,97,102,102,116,114,105,101,100,99,97,108,108,115,102,117,108,108,121,102 +,97,99,116,115,97,103,101,110,116,84,104,105,115,32,47,47,45,45,62,97,100,109, +105,110,101,103,121,112,116,69,118,101,110,116,49,53,112,120,59,69,109,97,105, +108,116,114,117,101,34,99,114,111,115,115,115,112,101,110,116,98,108,111,103,115 +,98,111,120,34,62,110,111,116,101,100,108,101,97,118,101,99,104,105,110,97,115, +105,122,101,115,103,117,101,115,116,60,47,104,52,62,114,111,98,111,116,104,101, +97,118,121,116,114,117,101,44,115,101,118,101,110,103,114,97,110,100,99,114,105, +109,101,115,105,103,110,115,97,119,97,114,101,100,97,110,99,101,112,104,97,115, +101,62,60,33,45,45,101,110,95,85,83,38,35,51,57,59,50,48,48,112,120,95,110,97, +109,101,108,97,116,105,110,101,110,106,111,121,97,106,97,120,46,97,116,105,111, +110,115,109,105,116,104,85,46,83,46,32,104,111,108,100,115,112,101,116,101,114, +105,110,100,105,97,110,97,118,34,62,99,104,97,105,110,115,99,111,114,101,99,111, +109,101,115,100,111,105,110,103,112,114,105,111,114,83,104,97,114,101,49,57,57, +48,115,114,111,109,97,110,108,105,115,116,115,106,97,112,97,110,102,97,108,108, +115,116,114,105,97,108,111,119,110,101,114,97,103,114,101,101,60,47,104,50,62,97 +,98,117,115,101,97,108,101,114,116,111,112,101,114,97,34,45,47,47,87,99,97,114, +100,115,104,105,108,108,115,116,101,97,109,115,80,104,111,116,111,116,114,117, +116,104,99,108,101,97,110,46,112,104,112,63,115,97,105,110,116,109,101,116,97, +108,108,111,117,105,115,109,101,97,110,116,112,114,111,111,102,98,114,105,101, +102,114,111,119,34,62,103,101,110,114,101,116,114,117,99,107,108,111,111,107,115 +,86,97,108,117,101,70,114,97,109,101,46,110,101,116,47,45,45,62,10,60,116,114, +121,32,123,10,118,97,114,32,109,97,107,101,115,99,111,115,116,115,112,108,97,105 +,110,97,100,117,108,116,113,117,101,115,116,116,114,97,105,110,108,97,98,111,114 +,104,101,108,112,115,99,97,117,115,101,109,97,103,105,99,109,111,116,111,114,116 +,104,101,105,114,50,53,48,112,120,108,101,97,115,116,115,116,101,112,115,67,111, +117,110,116,99,111,117,108,100,103,108,97,115,115,115,105,100,101,115,102,117, +110,100,115,104,111,116,101,108,97,119,97,114,100,109,111,117,116,104,109,111, +118,101,115,112,97,114,105,115,103,105,118,101,115,100,117,116,99,104,116,101, +120,97,115,102,114,117,105,116,110,117,108,108,44,124,124,91,93,59,116,111,112, +34,62,10,60,33,45,45,80,79,83,84,34,111,99,101,97,110,60,98,114,47,62,102,108, +111,111,114,115,112,101,97,107,100,101,112,116,104,32,115,105,122,101,98,97,110, +107,115,99,97,116,99,104,99,104,97,114,116,50,48,112,120,59,97,108,105,103,110, +100,101,97,108,115,119,111,117,108,100,53,48,112,120,59,117,114,108,61,34,112,97 +,114,107,115,109,111,117,115,101,77,111,115,116,32,46,46,46,60,47,97,109,111,110 +,103,98,114,97,105,110,98,111,100,121,32,110,111,110,101,59,98,97,115,101,100,99 +,97,114,114,121,100,114,97,102,116,114,101,102,101,114,112,97,103,101,95,104,111 +,109,101,46,109,101,116,101,114,100,101,108,97,121,100,114,101,97,109,112,114, +111,118,101,106,111,105,110,116,60,47,116,114,62,100,114,117,103,115,60,33,45,45 +,32,97,112,114,105,108,105,100,101,97,108,97,108,108,101,110,101,120,97,99,116, +102,111,114,116,104,99,111,100,101,115,108,111,103,105,99,86,105,101,119,32,115, +101,101,109,115,98,108,97,110,107,112,111,114,116,115,32,40,50,48,48,115,97,118, +101,100,95,108,105,110,107,103,111,97,108,115,103,114,97,110,116,103,114,101,101 +,107,104,111,109,101,115,114,105,110,103,115,114,97,116,101,100,51,48,112,120,59 +,119,104,111,115,101,112,97,114,115,101,40,41,59,34,32,66,108,111,99,107,108,105 +,110,117,120,106,111,110,101,115,112,105,120,101,108,39,41,59,34,62,41,59,105, +102,40,45,108,101,102,116,100,97,118,105,100,104,111,114,115,101,70,111,99,117, +115,114,97,105,115,101,98,111,120,101,115,84,114,97,99,107,101,109,101,110,116, +60,47,101,109,62,98,97,114,34,62,46,115,114,99,61,116,111,119,101,114,97,108,116 +,61,34,99,97,98,108,101,104,101,110,114,121,50,52,112,120,59,115,101,116,117,112 +,105,116,97,108,121,115,104,97,114,112,109,105,110,111,114,116,97,115,116,101, +119,97,110,116,115,116,104,105,115,46,114,101,115,101,116,119,104,101,101,108, +103,105,114,108,115,47,99,115,115,47,49,48,48,37,59,99,108,117,98,115,115,116, +117,102,102,98,105,98,108,101,118,111,116,101,115,32,49,48,48,48,107,111,114,101 +,97,125,41,59,13,10,98,97,110,100,115,113,117,101,117,101,61,32,123,125,59,56,48 +,112,120,59,99,107,105,110,103,123,13,10,9,9,97,104,101,97,100,99,108,111,99,107 +,105,114,105,115,104,108,105,107,101,32,114,97,116,105,111,115,116,97,116,115,70 +,111,114,109,34,121,97,104,111,111,41,91,48,93,59,65,98,111,117,116,102,105,110, +100,115,60,47,104,49,62,100,101,98,117,103,116,97,115,107,115,85,82,76,32,61,99, +101,108,108,115,125,41,40,41,59,49,50,112,120,59,112,114,105,109,101,116,101,108 +,108,115,116,117,114,110,115,48,120,54,48,48,46,106,112,103,34,115,112,97,105, +110,98,101,97,99,104,116,97,120,101,115,109,105,99,114,111,97,110,103,101,108,45 +,45,62,60,47,103,105,102,116,115,115,116,101,118,101,45,108,105,110,107,98,111, +100,121,46,125,41,59,10,9,109,111,117,110,116,32,40,49,57,57,70,65,81,60,47,114, +111,103,101,114,102,114,97,110,107,67,108,97,115,115,50,56,112,120,59,102,101, +101,100,115,60,104,49,62,60,115,99,111,116,116,116,101,115,116,115,50,50,112,120 +,59,100,114,105,110,107,41,32,124,124,32,108,101,119,105,115,115,104,97,108,108, +35,48,51,57,59,32,102,111,114,32,108,111,118,101,100,119,97,115,116,101,48,48, +112,120,59,106,97,58,227,130,115,105,109,111,110,60,102,111,110,116,114,101,112, +108,121,109,101,101,116,115,117,110,116,101,114,99,104,101,97,112,116,105,103, +104,116,66,114,97,110,100,41,32,33,61,32,100,114,101,115,115,99,108,105,112,115, +114,111,111,109,115,111,110,107,101,121,109,111,98,105,108,109,97,105,110,46,78, +97,109,101,32,112,108,97,116,101,102,117,110,110,121,116,114,101,101,115,99,111, +109,47,34,49,46,106,112,103,119,109,111,100,101,112,97,114,97,109,83,84,65,82,84 +,108,101,102,116,32,105,100,100,101,110,44,32,50,48,49,41,59,10,125,10,102,111, +114,109,46,118,105,114,117,115,99,104,97,105,114,116,114,97,110,115,119,111,114, +115,116,80,97,103,101,115,105,116,105,111,110,112,97,116,99,104,60,33,45,45,10, +111,45,99,97,99,102,105,114,109,115,116,111,117,114,115,44,48,48,48,32,97,115, +105,97,110,105,43,43,41,123,97,100,111,98,101,39,41,91,48,93,105,100,61,49,48,98 +,111,116,104,59,109,101,110,117,32,46,50,46,109,105,46,112,110,103,34,107,101, +118,105,110,99,111,97,99,104,67,104,105,108,100,98,114,117,99,101,50,46,106,112, +103,85,82,76,41,43,46,106,112,103,124,115,117,105,116,101,115,108,105,99,101,104 +,97,114,114,121,49,50,48,34,32,115,119,101,101,116,116,114,62,13,10,110,97,109, +101,61,100,105,101,103,111,112,97,103,101,32,115,119,105,115,115,45,45,62,10,10, +35,102,102,102,59,34,62,76,111,103,46,99,111,109,34,116,114,101,97,116,115,104, +101,101,116,41,32,38,38,32,49,52,112,120,59,115,108,101,101,112,110,116,101,110, +116,102,105,108,101,100,106,97,58,227,131,105,100,61,34,99,78,97,109,101,34,119, +111,114,115,101,115,104,111,116,115,45,98,111,120,45,100,101,108,116,97,10,38, +108,116,59,98,101,97,114,115,58,52,56,90,60,100,97,116,97,45,114,117,114,97,108, +60,47,97,62,32,115,112,101,110,100,98,97,107,101,114,115,104,111,112,115,61,32, +34,34,59,112,104,112,34,62,99,116,105,111,110,49,51,112,120,59,98,114,105,97,110 +,104,101,108,108,111,115,105,122,101,61,111,61,37,50,70,32,106,111,105,110,109, +97,121,98,101,60,105,109,103,32,105,109,103,34,62,44,32,102,106,115,105,109,103, +34,32,34,41,91,48,93,77,84,111,112,66,84,121,112,101,34,110,101,119,108,121,68, +97,110,115,107,99,122,101,99,104,116,114,97,105,108,107,110,111,119,115,60,47, +104,53,62,102,97,113,34,62,122,104,45,99,110,49,48,41,59,10,45,49,34,41,59,116, +121,112,101,61,98,108,117,101,115,116,114,117,108,121,100,97,118,105,115,46,106, +115,39,59,62,13,10,60,33,115,116,101,101,108,32,121,111,117,32,104,50,62,13,10, +102,111,114,109,32,106,101,115,117,115,49,48,48,37,32,109,101,110,117,46,13,10,9 +,13,10,119,97,108,101,115,114,105,115,107,115,117,109,101,110,116,100,100,105, +110,103,98,45,108,105,107,116,101,97,99,104,103,105,102,34,32,118,101,103,97,115 +,100,97,110,115,107,101,101,115,116,105,115,104,113,105,112,115,117,111,109,105, +115,111,98,114,101,100,101,115,100,101,101,110,116,114,101,116,111,100,111,115, +112,117,101,100,101,97,195,177,111,115,101,115,116,195,161,116,105,101,110,101, +104,97,115,116,97,111,116,114,111,115,112,97,114,116,101,100,111,110,100,101,110 +,117,101,118,111,104,97,99,101,114,102,111,114,109,97,109,105,115,109,111,109, +101,106,111,114,109,117,110,100,111,97,113,117,195,173,100,195,173,97,115,115, +195,179,108,111,97,121,117,100,97,102,101,99,104,97,116,111,100,97,115,116,97, +110,116,111,109,101,110,111,115,100,97,116,111,115,111,116,114,97,115,115,105, +116,105,111,109,117,99,104,111,97,104,111,114,97,108,117,103,97,114,109,97,121, +111,114,101,115,116,111,115,104,111,114,97,115,116,101,110,101,114,97,110,116, +101,115,102,111,116,111,115,101,115,116,97,115,112,97,195,173,115,110,117,101, +118,97,115,97,108,117,100,102,111,114,111,115,109,101,100,105,111,113,117,105, +101,110,109,101,115,101,115,112,111,100,101,114,99,104,105,108,101,115,101,114, +195,161,118,101,99,101,115,100,101,99,105,114,106,111,115,195,169,101,115,116,97 +,114,118,101,110,116,97,103,114,117,112,111,104,101,99,104,111,101,108,108,111, +115,116,101,110,103,111,97,109,105,103,111,99,111,115,97,115,110,105,118,101,108 +,103,101,110,116,101,109,105,115,109,97,97,105,114,101,115,106,117,108,105,111, +116,101,109,97,115,104,97,99,105,97,102,97,118,111,114,106,117,110,105,111,108, +105,98,114,101,112,117,110,116,111,98,117,101,110,111,97,117,116,111,114,97,98, +114,105,108,98,117,101,110,97,116,101,120,116,111,109,97,114,122,111,115,97,98, +101,114,108,105,115,116,97,108,117,101,103,111,99,195,179,109,111,101,110,101, +114,111,106,117,101,103,111,112,101,114,195,186,104,97,98,101,114,101,115,116, +111,121,110,117,110,99,97,109,117,106,101,114,118,97,108,111,114,102,117,101,114 +,97,108,105,98,114,111,103,117,115,116,97,105,103,117,97,108,118,111,116,111,115 +,99,97,115,111,115,103,117,195,173,97,112,117,101,100,111,115,111,109,111,115,97 +,118,105,115,111,117,115,116,101,100,100,101,98,101,110,110,111,99,104,101,98, +117,115,99,97,102,97,108,116,97,101,117,114,111,115,115,101,114,105,101,100,105, +99,104,111,99,117,114,115,111,99,108,97,118,101,99,97,115,97,115,108,101,195,179 +,110,112,108,97,122,111,108,97,114,103,111,111,98,114,97,115,118,105,115,116,97, +97,112,111,121,111,106,117,110,116,111,116,114,97,116,97,118,105,115,116,111,99, +114,101,97,114,99,97,109,112,111,104,101,109,111,115,99,105,110,99,111,99,97,114 +,103,111,112,105,115,111,115,111,114,100,101,110,104,97,99,101,110,195,161,114, +101,97,100,105,115,99,111,112,101,100,114,111,99,101,114,99,97,112,117,101,100, +97,112,97,112,101,108,109,101,110,111,114,195,186,116,105,108,99,108,97,114,111, +106,111,114,103,101,99,97,108,108,101,112,111,110,101,114,116,97,114,100,101,110 +,97,100,105,101,109,97,114,99,97,115,105,103,117,101,101,108,108,97,115,115,105, +103,108,111,99,111,99,104,101,109,111,116,111,115,109,97,100,114,101,99,108,97, +115,101,114,101,115,116,111,110,105,195,177,111,113,117,101,100,97,112,97,115,97 +,114,98,97,110,99,111,104,105,106,111,115,118,105,97,106,101,112,97,98,108,111, +195,169,115,116,101,118,105,101,110,101,114,101,105,110,111,100,101,106,97,114, +102,111,110,100,111,99,97,110,97,108,110,111,114,116,101,108,101,116,114,97,99, +97,117,115,97,116,111,109,97,114,109,97,110,111,115,108,117,110,101,115,97,117, +116,111,115,118,105,108,108,97,118,101,110,100,111,112,101,115,97,114,116,105, +112,111,115,116,101,110,103,97,109,97,114,99,111,108,108,101,118,97,112,97,100, +114,101,117,110,105,100,111,118,97,109,111,115,122,111,110,97,115,97,109,98,111, +115,98,97,110,100,97,109,97,114,105,97,97,98,117,115,111,109,117,99,104,97,115, +117,98,105,114,114,105,111,106,97,118,105,118,105,114,103,114,97,100,111,99,104, +105,99,97,97,108,108,195,173,106,111,118,101,110,100,105,99,104,97,101,115,116, +97,110,116,97,108,101,115,115,97,108,105,114,115,117,101,108,111,112,101,115,111 +,115,102,105,110,101,115,108,108,97,109,97,98,117,115,99,111,195,169,115,116,97, +108,108,101,103,97,110,101,103,114,111,112,108,97,122,97,104,117,109,111,114,112 +,97,103,97,114,106,117,110,116,97,100,111,98,108,101,105,115,108,97,115,98,111, +108,115,97,98,97,195,177,111,104,97,98,108,97,108,117,99,104,97,195,129,114,101, +97,100,105,99,101,110,106,117,103,97,114,110,111,116,97,115,118,97,108,108,101, +97,108,108,195,161,99,97,114,103,97,100,111,108,111,114,97,98,97,106,111,101,115 +,116,195,169,103,117,115,116,111,109,101,110,116,101,109,97,114,105,111,102,105, +114,109,97,99,111,115,116,111,102,105,99,104,97,112,108,97,116,97,104,111,103,97 +,114,97,114,116,101,115,108,101,121,101,115,97,113,117,101,108,109,117,115,101, +111,98,97,115,101,115,112,111,99,111,115,109,105,116,97,100,99,105,101,108,111, +99,104,105,99,111,109,105,101,100,111,103,97,110,97,114,115,97,110,116,111,101, +116,97,112,97,100,101,98,101,115,112,108,97,121,97,114,101,100,101,115,115,105, +101,116,101,99,111,114,116,101,99,111,114,101,97,100,117,100,97,115,100,101,115, +101,111,118,105,101,106,111,100,101,115,101,97,97,103,117,97,115,38,113,117,111, +116,59,100,111,109,97,105,110,99,111,109,109,111,110,115,116,97,116,117,115,101, +118,101,110,116,115,109,97,115,116,101,114,115,121,115,116,101,109,97,99,116,105 +,111,110,98,97,110,110,101,114,114,101,109,111,118,101,115,99,114,111,108,108, +117,112,100,97,116,101,103,108,111,98,97,108,109,101,100,105,117,109,102,105,108 +,116,101,114,110,117,109,98,101,114,99,104,97,110,103,101,114,101,115,117,108, +116,112,117,98,108,105,99,115,99,114,101,101,110,99,104,111,111,115,101,110,111, +114,109,97,108,116,114,97,118,101,108,105,115,115,117,101,115,115,111,117,114,99 +,101,116,97,114,103,101,116,115,112,114,105,110,103,109,111,100,117,108,101,109, +111,98,105,108,101,115,119,105,116,99,104,112,104,111,116,111,115,98,111,114,100 +,101,114,114,101,103,105,111,110,105,116,115,101,108,102,115,111,99,105,97,108, +97,99,116,105,118,101,99,111,108,117,109,110,114,101,99,111,114,100,102,111,108, +108,111,119,116,105,116,108,101,62,101,105,116,104,101,114,108,101,110,103,116, +104,102,97,109,105,108,121,102,114,105,101,110,100,108,97,121,111,117,116,97,117 +,116,104,111,114,99,114,101,97,116,101,114,101,118,105,101,119,115,117,109,109, +101,114,115,101,114,118,101,114,112,108,97,121,101,100,112,108,97,121,101,114, +101,120,112,97,110,100,112,111,108,105,99,121,102,111,114,109,97,116,100,111,117 +,98,108,101,112,111,105,110,116,115,115,101,114,105,101,115,112,101,114,115,111, +110,108,105,118,105,110,103,100,101,115,105,103,110,109,111,110,116,104,115,102, +111,114,99,101,115,117,110,105,113,117,101,119,101,105,103,104,116,112,101,111, +112,108,101,101,110,101,114,103,121,110,97,116,117,114,101,115,101,97,114,99,104 +,102,105,103,117,114,101,104,97,118,105,110,103,99,117,115,116,111,109,111,102, +102,115,101,116,108,101,116,116,101,114,119,105,110,100,111,119,115,117,98,109, +105,116,114,101,110,100,101,114,103,114,111,117,112,115,117,112,108,111,97,100, +104,101,97,108,116,104,109,101,116,104,111,100,118,105,100,101,111,115,115,99, +104,111,111,108,102,117,116,117,114,101,115,104,97,100,111,119,100,101,98,97,116 +,101,118,97,108,117,101,115,79,98,106,101,99,116,111,116,104,101,114,115,114,105 +,103,104,116,115,108,101,97,103,117,101,99,104,114,111,109,101,115,105,109,112, +108,101,110,111,116,105,99,101,115,104,97,114,101,100,101,110,100,105,110,103, +115,101,97,115,111,110,114,101,112,111,114,116,111,110,108,105,110,101,115,113, +117,97,114,101,98,117,116,116,111,110,105,109,97,103,101,115,101,110,97,98,108, +101,109,111,118,105,110,103,108,97,116,101,115,116,119,105,110,116,101,114,70, +114,97,110,99,101,112,101,114,105,111,100,115,116,114,111,110,103,114,101,112, +101,97,116,76,111,110,100,111,110,100,101,116,97,105,108,102,111,114,109,101,100 +,100,101,109,97,110,100,115,101,99,117,114,101,112,97,115,115,101,100,116,111, +103,103,108,101,112,108,97,99,101,115,100,101,118,105,99,101,115,116,97,116,105, +99,99,105,116,105,101,115,115,116,114,101,97,109,121,101,108,108,111,119,97,116, +116,97,99,107,115,116,114,101,101,116,102,108,105,103,104,116,104,105,100,100, +101,110,105,110,102,111,34,62,111,112,101,110,101,100,117,115,101,102,117,108, +118,97,108,108,101,121,99,97,117,115,101,115,108,101,97,100,101,114,115,101,99, +114,101,116,115,101,99,111,110,100,100,97,109,97,103,101,115,112,111,114,116,115 +,101,120,99,101,112,116,114,97,116,105,110,103,115,105,103,110,101,100,116,104, +105,110,103,115,101,102,102,101,99,116,102,105,101,108,100,115,115,116,97,116, +101,115,111,102,102,105,99,101,118,105,115,117,97,108,101,100,105,116,111,114, +118,111,108,117,109,101,82,101,112,111,114,116,109,117,115,101,117,109,109,111, +118,105,101,115,112,97,114,101,110,116,97,99,99,101,115,115,109,111,115,116,108, +121,109,111,116,104,101,114,34,32,105,100,61,34,109,97,114,107,101,116,103,114, +111,117,110,100,99,104,97,110,99,101,115,117,114,118,101,121,98,101,102,111,114, +101,115,121,109,98,111,108,109,111,109,101,110,116,115,112,101,101,99,104,109, +111,116,105,111,110,105,110,115,105,100,101,109,97,116,116,101,114,67,101,110, +116,101,114,111,98,106,101,99,116,101,120,105,115,116,115,109,105,100,100,108, +101,69,117,114,111,112,101,103,114,111,119,116,104,108,101,103,97,99,121,109,97, +110,110,101,114,101,110,111,117,103,104,99,97,114,101,101,114,97,110,115,119,101 +,114,111,114,105,103,105,110,112,111,114,116,97,108,99,108,105,101,110,116,115, +101,108,101,99,116,114,97,110,100,111,109,99,108,111,115,101,100,116,111,112,105 +,99,115,99,111,109,105,110,103,102,97,116,104,101,114,111,112,116,105,111,110, +115,105,109,112,108,121,114,97,105,115,101,100,101,115,99,97,112,101,99,104,111, +115,101,110,99,104,117,114,99,104,100,101,102,105,110,101,114,101,97,115,111,110 +,99,111,114,110,101,114,111,117,116,112,117,116,109,101,109,111,114,121,105,102, +114,97,109,101,112,111,108,105,99,101,109,111,100,101,108,115,78,117,109,98,101, +114,100,117,114,105,110,103,111,102,102,101,114,115,115,116,121,108,101,115,107, +105,108,108,101,100,108,105,115,116,101,100,99,97,108,108,101,100,115,105,108, +118,101,114,109,97,114,103,105,110,100,101,108,101,116,101,98,101,116,116,101, +114,98,114,111,119,115,101,108,105,109,105,116,115,71,108,111,98,97,108,115,105, +110,103,108,101,119,105,100,103,101,116,99,101,110,116,101,114,98,117,100,103, +101,116,110,111,119,114,97,112,99,114,101,100,105,116,99,108,97,105,109,115,101, +110,103,105,110,101,115,97,102,101,116,121,99,104,111,105,99,101,115,112,105,114 +,105,116,45,115,116,121,108,101,115,112,114,101,97,100,109,97,107,105,110,103, +110,101,101,100,101,100,114,117,115,115,105,97,112,108,101,97,115,101,101,120, +116,101,110,116,83,99,114,105,112,116,98,114,111,107,101,110,97,108,108,111,119, +115,99,104,97,114,103,101,100,105,118,105,100,101,102,97,99,116,111,114,109,101, +109,98,101,114,45,98,97,115,101,100,116,104,101,111,114,121,99,111,110,102,105, +103,97,114,111,117,110,100,119,111,114,107,101,100,104,101,108,112,101,100,67, +104,117,114,99,104,105,109,112,97,99,116,115,104,111,117,108,100,97,108,119,97, +121,115,108,111,103,111,34,32,98,111,116,116,111,109,108,105,115,116,34,62,41, +123,118,97,114,32,112,114,101,102,105,120,111,114,97,110,103,101,72,101,97,100, +101,114,46,112,117,115,104,40,99,111,117,112,108,101,103,97,114,100,101,110,98, +114,105,100,103,101,108,97,117,110,99,104,82,101,118,105,101,119,116,97,107,105, +110,103,118,105,115,105,111,110,108,105,116,116,108,101,100,97,116,105,110,103, +66,117,116,116,111,110,98,101,97,117,116,121,116,104,101,109,101,115,102,111,114 +,103,111,116,83,101,97,114,99,104,97,110,99,104,111,114,97,108,109,111,115,116, +108,111,97,100,101,100,67,104,97,110,103,101,114,101,116,117,114,110,115,116,114 +,105,110,103,114,101,108,111,97,100,77,111,98,105,108,101,105,110,99,111,109,101 +,115,117,112,112,108,121,83,111,117,114,99,101,111,114,100,101,114,115,118,105, +101,119,101,100,38,110,98,115,112,59,99,111,117,114,115,101,65,98,111,117,116,32 +,105,115,108,97,110,100,60,104,116,109,108,32,99,111,111,107,105,101,110,97,109, +101,61,34,97,109,97,122,111,110,109,111,100,101,114,110,97,100,118,105,99,101, +105,110,60,47,97,62,58,32,84,104,101,32,100,105,97,108,111,103,104,111,117,115, +101,115,66,69,71,73,78,32,77,101,120,105,99,111,115,116,97,114,116,115,99,101, +110,116,114,101,104,101,105,103,104,116,97,100,100,105,110,103,73,115,108,97,110 +,100,97,115,115,101,116,115,69,109,112,105,114,101,83,99,104,111,111,108,101,102 +,102,111,114,116,100,105,114,101,99,116,110,101,97,114,108,121,109,97,110,117,97 +,108,83,101,108,101,99,116,46,10,10,79,110,101,106,111,105,110,101,100,109,101, +110,117,34,62,80,104,105,108,105,112,97,119,97,114,100,115,104,97,110,100,108, +101,105,109,112,111,114,116,79,102,102,105,99,101,114,101,103,97,114,100,115,107 +,105,108,108,115,110,97,116,105,111,110,83,112,111,114,116,115,100,101,103,114, +101,101,119,101,101,107,108,121,32,40,101,46,103,46,98,101,104,105,110,100,100, +111,99,116,111,114,108,111,103,103,101,100,117,110,105,116,101,100,60,47,98,62, +60,47,98,101,103,105,110,115,112,108,97,110,116,115,97,115,115,105,115,116,97, +114,116,105,115,116,105,115,115,117,101,100,51,48,48,112,120,124,99,97,110,97, +100,97,97,103,101,110,99,121,115,99,104,101,109,101,114,101,109,97,105,110,66, +114,97,122,105,108,115,97,109,112,108,101,108,111,103,111,34,62,98,101,121,111, +110,100,45,115,99,97,108,101,97,99,99,101,112,116,115,101,114,118,101,100,109,97 +,114,105,110,101,70,111,111,116,101,114,99,97,109,101,114,97,60,47,104,49,62,10, +95,102,111,114,109,34,108,101,97,118,101,115,115,116,114,101,115,115,34,32,47,62 +,13,10,46,103,105,102,34,32,111,110,108,111,97,100,108,111,97,100,101,114,79,120 +,102,111,114,100,115,105,115,116,101,114,115,117,114,118,105,118,108,105,115,116 +,101,110,102,101,109,97,108,101,68,101,115,105,103,110,115,105,122,101,61,34,97, +112,112,101,97,108,116,101,120,116,34,62,108,101,118,101,108,115,116,104,97,110, +107,115,104,105,103,104,101,114,102,111,114,99,101,100,97,110,105,109,97,108,97, +110,121,111,110,101,65,102,114,105,99,97,97,103,114,101,101,100,114,101,99,101, +110,116,80,101,111,112,108,101,60,98,114,32,47,62,119,111,110,100,101,114,112, +114,105,99,101,115,116,117,114,110,101,100,124,124,32,123,125,59,109,97,105,110, +34,62,105,110,108,105,110,101,115,117,110,100,97,121,119,114,97,112,34,62,102,97 +,105,108,101,100,99,101,110,115,117,115,109,105,110,117,116,101,98,101,97,99,111 +,110,113,117,111,116,101,115,49,53,48,112,120,124,101,115,116,97,116,101,114,101 +,109,111,116,101,101,109,97,105,108,34,108,105,110,107,101,100,114,105,103,104, +116,59,115,105,103,110,97,108,102,111,114,109,97,108,49,46,104,116,109,108,115, +105,103,110,117,112,112,114,105,110,99,101,102,108,111,97,116,58,46,112,110,103, +34,32,102,111,114,117,109,46,65,99,99,101,115,115,112,97,112,101,114,115,115,111 +,117,110,100,115,101,120,116,101,110,100,72,101,105,103,104,116,115,108,105,100, +101,114,85,84,70,45,56,34,38,97,109,112,59,32,66,101,102,111,114,101,46,32,87, +105,116,104,115,116,117,100,105,111,111,119,110,101,114,115,109,97,110,97,103, +101,112,114,111,102,105,116,106,81,117,101,114,121,97,110,110,117,97,108,112,97, +114,97,109,115,98,111,117,103,104,116,102,97,109,111,117,115,103,111,111,103,108 +,101,108,111,110,103,101,114,105,43,43,41,32,123,105,115,114,97,101,108,115,97, +121,105,110,103,100,101,99,105,100,101,104,111,109,101,34,62,104,101,97,100,101, +114,101,110,115,117,114,101,98,114,97,110,99,104,112,105,101,99,101,115,98,108, +111,99,107,59,115,116,97,116,101,100,116,111,112,34,62,60,114,97,99,105,110,103, +114,101,115,105,122,101,45,45,38,103,116,59,112,97,99,105,116,121,115,101,120, +117,97,108,98,117,114,101,97,117,46,106,112,103,34,32,49,48,44,48,48,48,111,98, +116,97,105,110,116,105,116,108,101,115,97,109,111,117,110,116,44,32,73,110,99,46 +,99,111,109,101,100,121,109,101,110,117,34,32,108,121,114,105,99,115,116,111,100 +,97,121,46,105,110,100,101,101,100,99,111,117,110,116,121,95,108,111,103,111,46, +70,97,109,105,108,121,108,111,111,107,101,100,77,97,114,107,101,116,108,115,101, +32,105,102,80,108,97,121,101,114,116,117,114,107,101,121,41,59,118,97,114,32,102 +,111,114,101,115,116,103,105,118,105,110,103,101,114,114,111,114,115,68,111,109, +97,105,110,125,101,108,115,101,123,105,110,115,101,114,116,66,108,111,103,60,47, +102,111,111,116,101,114,108,111,103,105,110,46,102,97,115,116,101,114,97,103,101 +,110,116,115,60,98,111,100,121,32,49,48,112,120,32,48,112,114,97,103,109,97,102, +114,105,100,97,121,106,117,110,105,111,114,100,111,108,108,97,114,112,108,97,99, +101,100,99,111,118,101,114,115,112,108,117,103,105,110,53,44,48,48,48,32,112,97, +103,101,34,62,98,111,115,116,111,110,46,116,101,115,116,40,97,118,97,116,97,114, +116,101,115,116,101,100,95,99,111,117,110,116,102,111,114,117,109,115,115,99,104 +,101,109,97,105,110,100,101,120,44,102,105,108,108,101,100,115,104,97,114,101, +115,114,101,97,100,101,114,97,108,101,114,116,40,97,112,112,101,97,114,83,117,98 +,109,105,116,108,105,110,101,34,62,98,111,100,121,34,62,10,42,32,84,104,101,84, +104,111,117,103,104,115,101,101,105,110,103,106,101,114,115,101,121,78,101,119, +115,60,47,118,101,114,105,102,121,101,120,112,101,114,116,105,110,106,117,114, +121,119,105,100,116,104,61,67,111,111,107,105,101,83,84,65,82,84,32,97,99,114, +111,115,115,95,105,109,97,103,101,116,104,114,101,97,100,110,97,116,105,118,101, +112,111,99,107,101,116,98,111,120,34,62,10,83,121,115,116,101,109,32,68,97,118, +105,100,99,97,110,99,101,114,116,97,98,108,101,115,112,114,111,118,101,100,65, +112,114,105,108,32,114,101,97,108,108,121,100,114,105,118,101,114,105,116,101, +109,34,62,109,111,114,101,34,62,98,111,97,114,100,115,99,111,108,111,114,115,99, +97,109,112,117,115,102,105,114,115,116,32,124,124,32,91,93,59,109,101,100,105,97 +,46,103,117,105,116,97,114,102,105,110,105,115,104,119,105,100,116,104,58,115, +104,111,119,101,100,79,116,104,101,114,32,46,112,104,112,34,32,97,115,115,117, +109,101,108,97,121,101,114,115,119,105,108,115,111,110,115,116,111,114,101,115, +114,101,108,105,101,102,115,119,101,100,101,110,67,117,115,116,111,109,101,97, +115,105,108,121,32,121,111,117,114,32,83,116,114,105,110,103,10,10,87,104,105, +108,116,97,121,108,111,114,99,108,101,97,114,58,114,101,115,111,114,116,102,114, +101,110,99,104,116,104,111,117,103,104,34,41,32,43,32,34,60,98,111,100,121,62,98 +,117,121,105,110,103,98,114,97,110,100,115,77,101,109,98,101,114,110,97,109,101, +34,62,111,112,112,105,110,103,115,101,99,116,111,114,53,112,120,59,34,62,118,115 +,112,97,99,101,112,111,115,116,101,114,109,97,106,111,114,32,99,111,102,102,101, +101,109,97,114,116,105,110,109,97,116,117,114,101,104,97,112,112,101,110,60,47, +110,97,118,62,107,97,110,115,97,115,108,105,110,107,34,62,73,109,97,103,101,115, +61,102,97,108,115,101,119,104,105,108,101,32,104,115,112,97,99,101,48,38,97,109, +112,59,32,10,10,73,110,32,32,112,111,119,101,114,80,111,108,115,107,105,45,99, +111,108,111,114,106,111,114,100,97,110,66,111,116,116,111,109,83,116,97,114,116, +32,45,99,111,117,110,116,50,46,104,116,109,108,110,101,119,115,34,62,48,49,46, +106,112,103,79,110,108,105,110,101,45,114,105,103,104,116,109,105,108,108,101, +114,115,101,110,105,111,114,73,83,66,78,32,48,48,44,48,48,48,32,103,117,105,100, +101,115,118,97,108,117,101,41,101,99,116,105,111,110,114,101,112,97,105,114,46, +120,109,108,34,32,32,114,105,103,104,116,115,46,104,116,109,108,45,98,108,111,99 +,107,114,101,103,69,120,112,58,104,111,118,101,114,119,105,116,104,105,110,118, +105,114,103,105,110,112,104,111,110,101,115,60,47,116,114,62,13,117,115,105,110, +103,32,10,9,118,97,114,32,62,39,41,59,10,9,60,47,116,100,62,10,60,47,116,114,62, +10,98,97,104,97,115,97,98,114,97,115,105,108,103,97,108,101,103,111,109,97,103, +121,97,114,112,111,108,115,107,105,115,114,112,115,107,105,216,177,216,175,217, +136,228,184,173,230,150,135,231,174,128,228,189,147,231,185,129,233,171,148,228, +191,161,230,129,175,228,184,173,229,155,189,230,136,145,228,187,172,228,184,128, +228,184,170,229,133,172,229,143,184,231,174,161,231,144,134,232,174,186,229,157, +155,229,143,175,228,187,165,230,156,141,229,138,161,230,151,182,233,151,180,228, +184,170,228,186,186,228,186,167,229,147,129,232,135,170,229,183,177,228,188,129, +228,184,154,230,159,165,231,156,139,229,183,165,228,189,156,232,129,148,231,179, +187,230,178,161,230,156,137,231,189,145,231,171,153,230,137,128,230,156,137,232, +175,132,232,174,186,228,184,173,229,191,131,230,150,135,231,171,160,231,148,168, +230,136,183,233,166,150,233,161,181,228,189,156,232,128,133,230,138,128,230,156, +175,233,151,174,233,162,152,231,155,184,229,133,179,228,184,139,232,189,189,230, +144,156,231,180,162,228,189,191,231,148,168,232,189,175,228,187,182,229,156,168, +231,186,191,228,184,187,233,162,152,232,181,132,230,150,153,232,167,134,233,162, +145,229,155,158,229,164,141,230,179,168,229,134,140,231,189,145,231,187,156,230, +148,182,232,151,143,229,134,133,229,174,185,230,142,168,232,141,144,229,184,130, +229,156,186,230,182,136,230,129,175,231,169,186,233,151,180,229,143,145,229,184, +131,228,187,128,228,185,136,229,165,189,229,143,139,231,148,159,230,180,187,229, +155,190,231,137,135,229,143,145,229,177,149,229,166,130,230,158,156,230,137,139, +230,156,186,230,150,176,233,151,187,230,156,128,230,150,176,230,150,185,229,188, +143,229,140,151,228,186,172,230,143,144,228,190,155,229,133,179,228,186,142,230, +155,180,229,164,154,232,191,153,228,184,170,231,179,187,231,187,159,231,159,165, +233,129,147,230,184,184,230,136,143,229,185,191,229,145,138,229,133,182,228,187, +150,229,143,145,232,161,168,229,174,137,229,133,168,231,172,172,228,184,128,228, +188,154,229,145,152,232,191,155,232,161,140,231,130,185,229,135,187,231,137,136, +230,157,131,231,148,181,229,173,144,228,184,150,231,149,140,232,174,190,232,174, +161,229,133,141,232,180,185,230,149,153,232,130,178,229,138,160,229,133,165,230, +180,187,229,138,168,228,187,150,228,187,172,229,149,134,229,147,129,229,141,154, +229,174,162,231,142,176,229,156,168,228,184,138,230,181,183,229,166,130,228,189, +149,229,183,178,231,187,143,231,149,153,232,168,128,232,175,166,231,187,134,231, +164,190,229,140,186,231,153,187,229,189,149,230,156,172,231,171,153,233,156,128, +232,166,129,228,187,183,230,160,188,230,148,175,230,140,129,229,155,189,233,153, +133,233,147,190,230,142,165,229,155,189,229,174,182,229,187,186,232,174,190,230, +156,139,229,143,139,233,152,133,232,175,187,230,179,149,229,190,139,228,189,141, +231,189,174,231,187,143,230,181,142,233,128,137,230,139,169,232,191,153,230,160, +183,229,189,147,229,137,141,229,136,134,231,177,187,230,142,146,232,161,140,229, +155,160,228,184,186,228,186,164,230,152,147,230,156,128,229,144,142,233,159,179, +228,185,144,228,184,141,232,131,189,233,128,154,232,191,135,232,161,140,228,184, +154,231,167,145,230,138,128,229,143,175,232,131,189,232,174,190,229,164,135,229, +144,136,228,189,156,229,164,167,229,174,182,231,164,190,228,188,154,231,160,148, +231,169,182,228,184,147,228,184,154,229,133,168,233,131,168,233,161,185,231,155, +174,232,191,153,233,135,140,232,191,152,230,152,175,229,188,128,229,167,139,230, +131,133,229,134,181,231,148,181,232,132,145,230,150,135,228,187,182,229,147,129, +231,137,140,229,184,174,229,138,169,230,150,135,229,140,150,232,181,132,230,186, +144,229,164,167,229,173,166,229,173,166,228,185,160,229,156,176,229,157,128,230, +181,143,232,167,136,230,138,149,232,181,132,229,183,165,231,168,139,232,166,129, +230,177,130,230,128,142,228,185,136,230,151,182,229,128,153,229,138,159,232,131, +189,228,184,187,232,166,129,231,155,174,229,137,141,232,181,132,232,174,175,229, +159,142,229,184,130,230,150,185,230,179,149,231,148,181,229,189,177,230,139,155, +232,129,152,229,163,176,230,152,142,228,187,187,228,189,149,229,129,165,229,186, +183,230,149,176,230,141,174,231,190,142,229,155,189,230,177,189,232,189,166,228, +187,139,231,187,141,228,189,134,230,152,175,228,186,164,230,181,129,231,148,159, +228,186,167,230,137,128,228,187,165,231,148,181,232,175,157,230,152,190,231,164, +186,228,184,128,228,186,155,229,141,149,228,189,141,228,186,186,229,145,152,229, +136,134,230,158,144,229,156,176,229,155,190,230,151,133,230,184,184,229,183,165, +229,133,183,229,173,166,231,148,159,231,179,187,229,136,151,231,189,145,229,143, +139,229,184,150,229,173,144,229,175,134,231,160,129,233,162,145,233,129,147,230, +142,167,229,136,182,229,156,176,229,140,186,229,159,186,230,156,172,229,133,168, +229,155,189,231,189,145,228,184,138,233,135,141,232,166,129,231,172,172,228,186, +140,229,150,156,230,172,162,232,191,155,229,133,165,229,143,139,230,131,133,232, +191,153,228,186,155,232,128,131,232,175,149,229,143,145,231,142,176,229,159,185, +232,174,173,228,187,165,228,184,138,230,148,191,229,186,156,230,136,144,228,184, +186,231,142,175,229,162,131,233,166,153,230,184,175,229,144,140,230,151,182,229, +168,177,228,185,144,229,143,145,233,128,129,228,184,128,229,174,154,229,188,128, +229,143,145,228,189,156,229,147,129,230,160,135,229,135,134,230,172,162,232,191, +142,232,167,163,229,134,179,229,156,176,230,150,185,228,184,128,228,184,139,228, +187,165,229,143,138,232,180,163,228,187,187,230,136,150,232,128,133,229,174,162, +230,136,183,228,187,163,232,161,168,231,167,175,229,136,134,229,165,179,228,186, +186,230,149,176,231,160,129,233,148,128,229,148,174,229,135,186,231,142,176,231, +166,187,231,186,191,229,186,148,231,148,168,229,136,151,232,161,168,228,184,141, +229,144,140,231,188,150,232,190,145,231,187,159,232,174,161,230,159,165,232,175, +162,228,184,141,232,166,129,230,156,137,229,133,179,230,156,186,230,158,132,229, +190,136,229,164,154,230,146,173,230,148,190,231,187,132,231,187,135,230,148,191, +231,173,150,231,155,180,230,142,165,232,131,189,229,138,155,230,157,165,230,186, +144,230,153,130,233,150,147,231,156,139,229,136,176,231,131,173,233,151,168,229, +133,179,233,148,174,228,184,147,229,140,186,233,157,158,229,184,184,232,139,177, +232,175,173,231,153,190,229,186,166,229,184,140,230,156,155,231,190,142,229,165, +179,230,175,148,232,190,131,231,159,165,232,175,134,232,167,132,229,174,154,229, +187,186,232,174,174,233,131,168,233,151,168,230,132,143,232,167,129,231,178,190, +229,189,169,230,151,165,230,156,172,230,143,144,233,171,152,229,143,145,232,168, +128,230,150,185,233,157,162,229,159,186,233,135,145,229,164,132,231,144,134,230, +157,131,233,153,144,229,189,177,231,137,135,233,147,182,232,161,140,232,191,152, +230,156,137,229,136,134,228,186,171,231,137,169,229,147,129,231,187,143,232,144, +165,230,183,187,229,138,160,228,184,147,229,174,182,232,191,153,231,167,141,232, +175,157,233,162,152,232,181,183,230,157,165,228,184,154,229,138,161,229,133,172, +229,145,138,232,174,176,229,189,149,231,174,128,228,187,139,232,180,168,233,135, +143,231,148,183,228,186,186,229,189,177,229,147,141,229,188,149,231,148,168,230, +138,165,229,145,138,233,131,168,229,136,134,229,191,171,233,128,159,229,146,168, +232,175,162,230,151,182,229,176,154,230,179,168,230,132,143,231,148,179,232,175, +183,229,173,166,230,160,161,229,186,148,232,175,165,229,142,134,229,143,178,229, +143,170,230,152,175,232,191,148,229,155,158,232,180,173,228,185,176,229,144,141, +231,167,176,228,184,186,228,186,134,230,136,144,229,138,159,232,175,180,230,152, +142,228,190,155,229,186,148,229,173,169,229,173,144,228,184,147,233,162,152,231, +168,139,229,186,143,228,184,128,232,136,172,230,156,131,229,147,161,229,143,170, +230,156,137,229,133,182,229,174,131,228,191,157,230,138,164,232,128,140,228,184, +148,228,187,138,229,164,169,231,170,151,229,143,163,229,138,168,230,128,129,231, +138,182,230,128,129,231,137,185,229,136,171,232,174,164,228,184,186,229,191,133, +233,161,187,230,155,180,230,150,176,229,176,143,232,175,180,230,136,145,229,128, +145,228,189,156,228,184,186,229,170,146,228,189,147,229,140,133,230,139,172,233, +130,163,228,185,136,228,184,128,230,160,183,229,155,189,229,134,133,230,152,175, +229,144,166,230,160,185,230,141,174,231,148,181,232,167,134,229,173,166,233,153, +162,229,133,183,230,156,137,232,191,135,231,168,139,231,148,177,228,186,142,228, +186,186,230,137,141,229,135,186,230,157,165,228,184,141,232,191,135,230,173,163, +229,156,168,230,152,142,230,152,159,230,149,133,228,186,139,229,133,179,231,179, +187,230,160,135,233,162,152,229,149,134,229,138,161,232,190,147,229,133,165,228, +184,128,231,155,180,229,159,186,231,161,128,230,149,153,229,173,166,228,186,134, +232,167,163,229,187,186,231,173,145,231,187,147,230,158,156,229,133,168,231,144, +131,233,128,154,231,159,165,232,174,161,229,136,146,229,175,185,228,186,142,232, +137,186,230,156,175,231,155,184,229,134,140,229,143,145,231,148,159,231,156,159, +231,154,132,229,187,186,231,171,139,231,173,137,231,186,167,231,177,187,229,158, +139,231,187,143,233,170,140,229,174,158,231,142,176,229,136,182,228,189,156,230, +157,165,232,135,170,230,160,135,231,173,190,228,187,165,228,184,139,229,142,159, +229,136,155,230,151,160,230,179,149,229,133,182,228,184,173,229,128,139,228,186, +186,228,184,128,229,136,135,230,140,135,229,141,151,229,133,179,233,151,173,233, +155,134,229,155,162,231,172,172,228,184,137,229,133,179,230,179,168,229,155,160, +230,173,164,231,133,167,231,137,135,230,183,177,229,156,179,229,149,134,228,184, +154,229,185,191,229,183,158,230,151,165,230,156,159,233,171,152,231,186,167,230, +156,128,232,191,145,231,187,188,229,144,136,232,161,168,231,164,186,228,184,147, +232,190,145,232,161,140,228,184,186,228,186,164,233,128,154,232,175,132,228,187, +183,232,167,137,229,190,151,231,178,190,229,141,142,229,174,182,229,186,173,229, +174,140,230,136,144,230,132,159,232,167,137,229,174,137,232,163,133,229,190,151, +229,136,176,233,130,174,228,187,182,229,136,182,229,186,166,233,163,159,229,147, +129,232,153,189,231,132,182,232,189,172,232,189,189,230,138,165,228,187,183,232, +174,176,232,128,133,230,150,185,230,161,136,232,161,140,230,148,191,228,186,186, +230,176,145,231,148,168,229,147,129,228,184,156,232,165,191,230,143,144,229,135, +186,233,133,146,229,186,151,231,132,182,229,144,142,228,187,152,230,172,190,231, +131,173,231,130,185,228,187,165,229,137,141,229,174,140,229,133,168,229,143,145, +229,184,150,232,174,190,231,189,174,233,162,134,229,175,188,229,183,165,228,184, +154,229,140,187,233,153,162,231,156,139,231,156,139,231,187,143,229,133,184,229, +142,159,229,155,160,229,185,179,229,143,176,229,144,132,231,167,141,229,162,158, +229,138,160,230,157,144,230,150,153,230,150,176,229,162,158,228,185,139,229,144, +142,232,129,140,228,184,154,230,149,136,230,158,156,228,187,138,229,185,180,232, +174,186,230,150,135,230,136,145,229,155,189,229,145,138,232,175,137,231,137,136, +228,184,187,228,191,174,230,148,185,229,143,130,228,184,142,230,137,147,229,141, +176,229,191,171,228,185,144,230,156,186,230,162,176,232,167,130,231,130,185,229, +173,152,229,156,168,231,178,190,231,165,158,232,142,183,229,190,151,229,136,169, +231,148,168,231,187,167,231,187,173,228,189,160,228,187,172,232,191,153,228,185, +136,230,168,161,229,188,143,232,175,173,232,168,128,232,131,189,229,164,159,233, +155,133,232,153,142,230,147,141,228,189,156,233,163,142,230,160,188,228,184,128, +232,181,183,231,167,145,229,173,166,228,189,147,232,130,178,231,159,173,228,191, +161,230,157,161,228,187,182,230,178,187,231,150,151,232,191,144,229,138,168,228, +186,167,228,184,154,228,188,154,232,174,174,229,175,188,232,136,170,229,133,136, +231,148,159,232,129,148,231,155,159,229,143,175,230,152,175,229,149,143,233,161, +140,231,187,147,230,158,132,228,189,156,231,148,168,232,176,131,230,159,165,232, +179,135,230,150,153,232,135,170,229,138,168,232,180,159,232,180,163,229,134,156, +228,184,154,232,174,191,233,151,174,229,174,158,230,150,189,230,142,165,229,143, +151,232,174,168,232,174,186,233,130,163,228,184,170,229,143,141,233,166,136,229, +138,160,229,188,186,229,165,179,230,128,167,232,140,131,229,155,180,230,156,141, +229,139,153,228,188,145,233,151,178,228,187,138,230,151,165,229,174,162,230,156, +141,232,167,128,231,156,139,229,143,130,229,138,160,231,154,132,232,175,157,228, +184,128,231,130,185,228,191,157,232,175,129,229,155,190,228,185,166,230,156,137, +230,149,136,230,181,139,232,175,149,231,167,187,229,138,168,230,137,141,232,131, +189,229,134,179,229,174,154,232,130,161,231,165,168,228,184,141,230,150,173,233, +156,128,230,177,130,228,184,141,229,190,151,229,138,158,230,179,149,228,185,139, +233,151,180,233,135,135,231,148,168,232,144,165,233,148,128,230,138,149,232,175, +137,231,155,174,230,160,135,231,136,177,230,131,133,230,145,132,229,189,177,230, +156,137,228,186,155,232,164,135,232,163,189,230,150,135,229,173,166,230,156,186, +228,188,154,230,149,176,229,173,151,232,163,133,228,191,174,232,180,173,231,137, +169,229,134,156,230,157,145,229,133,168,233,157,162,231,178,190,229,147,129,229, +133,182,229,174,158,228,186,139,230,131,133,230,176,180,229,185,179,230,143,144, +231,164,186,228,184,138,229,184,130,232,176,162,232,176,162,230,153,174,233,128, +154,230,149,153,229,184,136,228,184,138,228,188,160,231,177,187,229,136,171,230, +173,140,230,155,178,230,139,165,230,156,137,229,136,155,230,150,176,233,133,141, +228,187,182,229,143,170,232,166,129,230,151,182,228,187,163,232,179,135,232,168, +138,232,190,190,229,136,176,228,186,186,231,148,159,232,174,162,233,152,133,232, +128,129,229,184,136,229,177,149,231,164,186,229,191,131,231,144,134,232,180,180, +229,173,144,231,182,178,231,171,153,228,184,187,233,161,140,232,135,170,231,132, +182,231,186,167,229,136,171,231,174,128,229,141,149,230,148,185,233,157,169,233, +130,163,228,186,155,230,157,165,232,175,180,230,137,147,229,188,128,228,187,163, +231,160,129,229,136,160,233,153,164,232,175,129,229,136,184,232,138,130,231,155, +174,233,135,141,231,130,185,230,172,161,230,149,184,229,164,154,229,176,145,232, +167,132,229,136,146,232,181,132,233,135,145,230,137,190,229,136,176,228,187,165, +229,144,142,229,164,167,229,133,168,228,184,187,233,161,181,230,156,128,228,189, +179,229,155,158,231,173,148,229,164,169,228,184,139,228,191,157,233,154,156,231, +142,176,228,187,163,230,163,128,230,159,165,230,138,149,231,165,168,229,176,143, +230,151,182,230,178,146,230,156,137,230,173,163,229,184,184,231,148,154,232,135, +179,228,187,163,231,144,134,231,155,174,229,189,149,229,133,172,229,188,128,229, +164,141,229,136,182,233,135,145,232,158,141,229,185,184,231,166,143,231,137,136, +230,156,172,229,189,162,230,136,144,229,135,134,229,164,135,232,161,140,230,131, +133,229,155,158,229,136,176,230,128,157,230,131,179,230,128,142,230,160,183,229, +141,143,232,174,174,232,174,164,232,175,129,230,156,128,229,165,189,228,186,167, +231,148,159,230,140,137,231,133,167,230,156,141,232,163,133,229,185,191,228,184, +156,229,138,168,230,188,171,233,135,135,232,180,173,230,150,176,230,137,139,231, +187,132,229,155,190,233,157,162,230,157,191,229,143,130,232,128,131,230,148,191, +230,178,187,229,174,185,230,152,147,229,164,169,229,156,176,229,138,170,229,138, +155,228,186,186,228,187,172,229,141,135,231,186,167,233,128,159,229,186,166,228, +186,186,231,137,169,232,176,131,230,149,180,230,181,129,232,161,140,233,128,160, +230,136,144,230,150,135,229,173,151,233,159,169,229,155,189,232,180,184,230,152, +147,229,188,128,229,177,149,231,155,184,233,151,156,232,161,168,231,142,176,229, +189,177,232,167,134,229,166,130,230,173,164,231,190,142,229,174,185,229,164,167, +229,176,143,230,138,165,233,129,147,230,157,161,230,172,190,229,191,131,230,131, +133,232,174,184,229,164,154,230,179,149,232,167,132,229,174,182,229,177,133,228, +185,166,229,186,151,232,191,158,230,142,165,231,171,139,229,141,179,228,184,190, +230,138,165,230,138,128,229,183,167,229,165,165,232,191,144,231,153,187,229,133, +165,228,187,165,230,157,165,231,144,134,232,174,186,228,186,139,228,187,182,232, +135,170,231,148,177,228,184,173,229,141,142,229,138,158,229,133,172,229,166,136, +229,166,136,231,156,159,230,173,163,228,184,141,233,148,153,229,133,168,230,150, +135,229,144,136,229,144,140,228,187,183,229,128,188,229,136,171,228,186,186,231, +155,145,231,157,163,229,133,183,228,189,147,228,184,150,231,186,170,229,155,162, +233,152,159,229,136,155,228,184,154,230,137,191,230,139,133,229,162,158,233,149, +191,230,156,137,228,186,186,228,191,157,230,140,129,229,149,134,229,174,182,231, +187,180,228,191,174,229,143,176,230,185,190,229,183,166,229,143,179,232,130,161, +228,187,189,231,173,148,230,161,136,229,174,158,233,153,133,231,148,181,228,191, +161,231,187,143,231,144,134,231,148,159,229,145,189,229,174,163,228,188,160,228, +187,187,229,138,161,230,173,163,229,188,143,231,137,185,232,137,178,228,184,139, +230,157,165,229,141,143,228,188,154,229,143,170,232,131,189,229,189,147,231,132, +182,233,135,141,230,150,176,229,133,167,229,174,185,230,140,135,229,175,188,232, +191,144,232,161,140,230,151,165,229,191,151,232,179,163,229,174,182,232,182,133, +232,191,135,229,156,159,229,156,176,230,181,153,230,177,159,230,148,175,228,187, +152,230,142,168,229,135,186,231,171,153,233,149,191,230,157,173,229,183,158,230, +137,167,232,161,140,229,136,182,233,128,160,228,185,139,228,184,128,230,142,168, +229,185,191,231,142,176,229,156,186,230,143,143,232,191,176,229,143,152,229,140, +150,228,188,160,231,187,159,230,173,140,230,137,139,228,191,157,233,153,169,232, +175,190,231,168,139,229,140,187,231,150,151,231,187,143,232,191,135,232,191,135, +229,142,187,228,185,139,229,137,141,230,148,182,229,133,165,229,185,180,229,186, +166,230,157,130,229,191,151,231,190,142,228,184,189,230,156,128,233,171,152,231, +153,187,233,153,134,230,156,170,230,157,165,229,138,160,229,183,165,229,133,141, +232,180,163,230,149,153,231,168,139,231,137,136,229,157,151,232,186,171,228,189, +147,233,135,141,229,186,134,229,135,186,229,148,174,230,136,144,230,156,172,229, +189,162,229,188,143,229,156,159,232,177,134,229,135,186,229,131,185,228,184,156, +230,150,185,233,130,174,231,174,177,229,141,151,228,186,172,230,177,130,232,129, +140,229,143,150,229,190,151,232,129,140,228,189,141,231,155,184,228,191,161,233, +161,181,233,157,162,229,136,134,233,146,159,231,189,145,233,161,181,231,161,174, +229,174,154,229,155,190,228,190,139,231,189,145,229,157,128,231,167,175,230,158, +129,233,148,153,232,175,175,231,155,174,231,154,132,229,174,157,232,180,157,230, +156,186,229,133,179,233,163,142,233,153,169,230,142,136,230,157,131,231,151,133, +230,175,146,229,174,160,231,137,169,233,153,164,228,186,134,232,169,149,232,171, +150,231,150,190,231,151,133,229,143,138,230,151,182,230,177,130,232,180,173,231, +171,153,231,130,185,229,132,191,231,171,165,230,175,143,229,164,169,228,184,173, +229,164,174,232,174,164,232,175,134,230,175,143,228,184,170,229,164,169,230,180, +165,229,173,151,228,189,147,229,143,176,231,129,163,231,187,180,230,138,164,230, +156,172,233,161,181,228,184,170,230,128,167,229,174,152,230,150,185,229,184,184, +232,167,129,231,155,184,230,156,186,230,136,152,231,149,165,229,186,148,229,189, +147,229,190,139,229,184,136,230,150,185,228,190,191,230,160,161,229,155,173,232, +130,161,229,184,130,230,136,191,229,177,139,230,160,143,231,155,174,229,145,152, +229,183,165,229,175,188,232,135,180,231,170,129,231,132,182,233,129,147,229,133, +183,230,156,172,231,189,145,231,187,147,229,144,136,230,161,163,230,161,136,229, +138,179,229,138,168,229,143,166,229,164,150,231,190,142,229,133,131,229,188,149, +232,181,183,230,148,185,229,143,152,231,172,172,229,155,155,228,188,154,232,174, +161,232,170,170,230,152,142,233,154,144,231,167,129,229,174,157,229,174,157,232, +167,132,232,140,131,230,182,136,232,180,185,229,133,177,229,144,140,229,191,152, +232,174,176,228,189,147,231,179,187,229,184,166,230,157,165,229,144,141,229,173, +151,231,153,188,232,161,168,229,188,128,230,148,190,229,138,160,231,155,159,229, +143,151,229,136,176,228,186,140,230,137,139,229,164,167,233,135,143,230,136,144, +228,186,186,230,149,176,233,135,143,229,133,177,228,186,171,229,140,186,229,159, +159,229,165,179,229,173,169,229,142,159,229,136,153,230,137,128,229,156,168,231, +187,147,230,157,159,233,128,154,228,191,161,232,182,133,231,186,167,233,133,141, +231,189,174,229,189,147,230,151,182,228,188,152,231,167,128,230,128,167,230,132, +159,230,136,191,228,186,167,233,129,138,230,136,178,229,135,186,229,143,163,230, +143,144,228,186,164,229,176,177,228,184,154,228,191,157,229,129,165,231,168,139, +229,186,166,229,143,130,230,149,176,228,186,139,228,184,154,230,149,180,228,184, +170,229,177,177,228,184,156,230,131,133,230,132,159,231,137,185,230,174,138,229, +136,134,233,161,158,230,144,156,229,176,139,229,177,158,228,186,142,233,151,168, +230,136,183,232,180,162,229,138,161,229,163,176,233,159,179,229,143,138,229,133, +182,232,180,162,231,187,143,229,157,154,230,140,129,229,185,178,233,131,168,230, +136,144,231,171,139,229,136,169,231,155,138,232,128,131,232,153,145,230,136,144, +233,131,189,229,140,133,232,163,133,231,148,168,230,136,182,230,175,148,232,181, +155,230,150,135,230,152,142,230,139,155,229,149,134,229,174,140,230,149,180,231, +156,159,230,152,175,231,156,188,231,157,155,228,188,153,228,188,180,229,168,129, +230,156,155,233,162,134,229,159,159,229,141,171,231,148,159,228,188,152,230,131, +160,232,171,150,229,163,135,229,133,172,229,133,177,232,137,175,229,165,189,229, +133,133,229,136,134,231,172,166,229,144,136,233,153,132,228,187,182,231,137,185, +231,130,185,228,184,141,229,143,175,232,139,177,230,150,135,232,181,132,228,186, +167,230,160,185,230,156,172,230,152,142,230,152,190,229,175,134,231,162,188,229, +133,172,228,188,151,230,176,145,230,151,143,230,155,180,229,138,160,228,186,171, +229,143,151,229,144,140,229,173,166,229,144,175,229,138,168,233,128,130,229,144, +136,229,142,159,230,157,165,233,151,174,231,173,148,230,156,172,230,150,135,231, +190,142,233,163,159,231,187,191,232,137,178,231,168,179,229,174,154,231,187,136, +228,186,142,231,148,159,231,137,169,228,190,155,230,177,130,230,144,156,231,139, +144,229,138,155,233,135,143,228,184,165,233,135,141,230,176,184,232,191,156,229, +134,153,231,156,159,230,156,137,233,153,144,231,171,158,228,186,137,229,175,185, +232,177,161,232,180,185,231,148,168,228,184,141,229,165,189,231,187,157,229,175, +185,229,141,129,229,136,134,228,191,131,232,191,155,231,130,185,232,175,132,229, +189,177,233,159,179,228,188,152,229,138,191,228,184,141,229,176,145,230,172,163, +232,181,143,229,185,182,228,184,148,230,156,137,231,130,185,230,150,185,229,144, +145,229,133,168,230,150,176,228,191,161,231,148,168,232,174,190,230,150,189,229, +189,162,232,177,161,232,181,132,230,160,188,231,170,129,231,160,180,233,154,143, +231,157,128,233,135,141,229,164,167,228,186,142,230,152,175,230,175,149,228,184, +154,230,153,186,232,131,189,229,140,150,229,183,165,229,174,140,231,190,142,229, +149,134,229,159,142,231,187,159,228,184,128,229,135,186,231,137,136,230,137,147, +233,128,160,231,148,162,229,147,129,230,166,130,229,134,181,231,148,168,228,186, +142,228,191,157,231,149,153,229,155,160,231,180,160,228,184,173,229,156,139,229, +173,152,229,130,168,232,180,180,229,155,190,230,156,128,230,132,155,233,149,191, +230,156,159,229,143,163,228,187,183,231,144,134,232,180,162,229,159,186,229,156, +176,229,174,137,230,142,146,230,173,166,230,177,137,233,135,140,233,157,162,229, +136,155,229,187,186,229,164,169,231,169,186,233,166,150,229,133,136,229,174,140, +229,150,132,233,169,177,229,138,168,228,184,139,233,157,162,228,184,141,229,134, +141,232,175,154,228,191,161,230,132,143,228,185,137,233,152,179,229,133,137,232, +139,177,229,155,189,230,188,130,228,186,174,229,134,155,228,186,139,231,142,169, +229,174,182,231,190,164,228,188,151,229,134,156,230,176,145,229,141,179,229,143, +175,229,144,141,231,168,177,229,174,182,229,133,183,229,138,168,231,148,187,230, +131,179,229,136,176,230,179,168,230,152,142,229,176,143,229,173,166,230,128,167, +232,131,189,232,128,131,231,160,148,231,161,172,228,187,182,232,167,130,231,156, +139,230,184,133,230,165,154,230,144,158,231,172,145,233,166,150,233,160,129,233, +187,132,233,135,145,233,128,130,231,148,168,230,177,159,232,139,143,231,156,159, +229,174,158,228,184,187,231,174,161,233,152,182,230,174,181,232,168,187,229,134, +138,231,191,187,232,175,145,230,157,131,229,136,169,229,129,154,229,165,189,228, +188,188,228,185,142,233,128,154,232,174,175,230,150,189,229,183,165,231,139,128, +230,133,139,228,185,159,232,174,184,231,142,175,228,191,157,229,159,185,229,133, +187,230,166,130,229,191,181,229,164,167,229,158,139,230,156,186,231,165,168,231, +144,134,232,167,163,229,140,191,229,144,141,99,117,97,110,100,111,101,110,118, +105,97,114,109,97,100,114,105,100,98,117,115,99,97,114,105,110,105,99,105,111, +116,105,101,109,112,111,112,111,114,113,117,101,99,117,101,110,116,97,101,115, +116,97,100,111,112,117,101,100,101,110,106,117,101,103,111,115,99,111,110,116, +114,97,101,115,116,195,161,110,110,111,109,98,114,101,116,105,101,110,101,110, +112,101,114,102,105,108,109,97,110,101,114,97,97,109,105,103,111,115,99,105,117, +100,97,100,99,101,110,116,114,111,97,117,110,113,117,101,112,117,101,100,101,115 +,100,101,110,116,114,111,112,114,105,109,101,114,112,114,101,99,105,111,115,101, +103,195,186,110,98,117,101,110,111,115,118,111,108,118,101,114,112,117,110,116, +111,115,115,101,109,97,110,97,104,97,98,195,173,97,97,103,111,115,116,111,110, +117,101,118,111,115,117,110,105,100,111,115,99,97,114,108,111,115,101,113,117, +105,112,111,110,105,195,177,111,115,109,117,99,104,111,115,97,108,103,117,110,97 +,99,111,114,114,101,111,105,109,97,103,101,110,112,97,114,116,105,114,97,114,114 +,105,98,97,109,97,114,195,173,97,104,111,109,98,114,101,101,109,112,108,101,111, +118,101,114,100,97,100,99,97,109,98,105,111,109,117,99,104,97,115,102,117,101, +114,111,110,112,97,115,97,100,111,108,195,173,110,101,97,112,97,114,101,99,101, +110,117,101,118,97,115,99,117,114,115,111,115,101,115,116,97,98,97,113,117,105, +101,114,111,108,105,98,114,111,115,99,117,97,110,116,111,97,99,99,101,115,111, +109,105,103,117,101,108,118,97,114,105,111,115,99,117,97,116,114,111,116,105,101 +,110,101,115,103,114,117,112,111,115,115,101,114,195,161,110,101,117,114,111,112 +,97,109,101,100,105,111,115,102,114,101,110,116,101,97,99,101,114,99,97,100,101, +109,195,161,115,111,102,101,114,116,97,99,111,99,104,101,115,109,111,100,101,108 +,111,105,116,97,108,105,97,108,101,116,114,97,115,97,108,103,195,186,110,99,111, +109,112,114,97,99,117,97,108,101,115,101,120,105,115,116,101,99,117,101,114,112, +111,115,105,101,110,100,111,112,114,101,110,115,97,108,108,101,103,97,114,118, +105,97,106,101,115,100,105,110,101,114,111,109,117,114,99,105,97,112,111,100,114 +,195,161,112,117,101,115,116,111,100,105,97,114,105,111,112,117,101,98,108,111, +113,117,105,101,114,101,109,97,110,117,101,108,112,114,111,112,105,111,99,114, +105,115,105,115,99,105,101,114,116,111,115,101,103,117,114,111,109,117,101,114, +116,101,102,117,101,110,116,101,99,101,114,114,97,114,103,114,97,110,100,101,101 +,102,101,99,116,111,112,97,114,116,101,115,109,101,100,105,100,97,112,114,111, +112,105,97,111,102,114,101,99,101,116,105,101,114,114,97,101,45,109,97,105,108, +118,97,114,105,97,115,102,111,114,109,97,115,102,117,116,117,114,111,111,98,106, +101,116,111,115,101,103,117,105,114,114,105,101,115,103,111,110,111,114,109,97, +115,109,105,115,109,111,115,195,186,110,105,99,111,99,97,109,105,110,111,115,105 +,116,105,111,115,114,97,122,195,179,110,100,101,98,105,100,111,112,114,117,101, +98,97,116,111,108,101,100,111,116,101,110,195,173,97,106,101,115,195,186,115,101 +,115,112,101,114,111,99,111,99,105,110,97,111,114,105,103,101,110,116,105,101, +110,100,97,99,105,101,110,116,111,99,195,161,100,105,122,104,97,98,108,97,114, +115,101,114,195,173,97,108,97,116,105,110,97,102,117,101,114,122,97,101,115,116, +105,108,111,103,117,101,114,114,97,101,110,116,114,97,114,195,169,120,105,116, +111,108,195,179,112,101,122,97,103,101,110,100,97,118,195,173,100,101,111,101, +118,105,116,97,114,112,97,103,105,110,97,109,101,116,114,111,115,106,97,118,105, +101,114,112,97,100,114,101,115,102,195,161,99,105,108,99,97,98,101,122,97,195, +161,114,101,97,115,115,97,108,105,100,97,101,110,118,195,173,111,106,97,112,195, +179,110,97,98,117,115,111,115,98,105,101,110,101,115,116,101,120,116,111,115,108 +,108,101,118,97,114,112,117,101,100,97,110,102,117,101,114,116,101,99,111,109, +195,186,110,99,108,97,115,101,115,104,117,109,97,110,111,116,101,110,105,100,111 +,98,105,108,98,97,111,117,110,105,100,97,100,101,115,116,195,161,115,101,100,105 +,116,97,114,99,114,101,97,100,111,208,180,208,187,209,143,209,135,209,130,208, +190,208,186,208,176,208,186,208,184,208,187,208,184,209,141,209,130,208,190,208, +178,209,129,208,181,208,181,208,179,208,190,208,191,209,128,208,184,209,130,208, +176,208,186,208,181,209,137,208,181,209,131,208,182,208,181,208,154,208,176,208, +186,208,177,208,181,208,183,208,177,209,139,208,187,208,190,208,189,208,184,208, +146,209,129,208,181,208,191,208,190,208,180,208,173,209,130,208,190,209,130,208, +190,208,188,209,135,208,181,208,188,208,189,208,181,209,130,208,187,208,181,209, +130,209,128,208,176,208,183,208,190,208,189,208,176,208,179,208,180,208,181,208, +188,208,189,208,181,208,148,208,187,209,143,208,159,209,128,208,184,208,189,208, +176,209,129,208,189,208,184,209,133,209,130,208,181,208,188,208,186,209,130,208, +190,208,179,208,190,208,180,208,178,208,190,209,130,209,130,208,176,208,188,208, +161,208,168,208,144,208,188,208,176,209,143,208,167,209,130,208,190,208,178,208, +176,209,129,208,178,208,176,208,188,208,181,208,188,209,131,208,162,208,176,208, +186,208,180,208,178,208,176,208,189,208,176,208,188,209,141,209,130,208,184,209, +141,209,130,209,131,208,146,208,176,208,188,209,130,208,181,209,133,208,191,209, +128,208,190,209,130,209,131,209,130,208,189,208,176,208,180,208,180,208,189,209, +143,208,146,208,190,209,130,209,130,209,128,208,184,208,189,208,181,208,185,208, +146,208,176,209,129,208,189,208,184,208,188,209,129,208,176,208,188,209,130,208, +190,209,130,209,128,209,131,208,177,208,158,208,189,208,184,208,188,208,184,209, +128,208,189,208,181,208,181,208,158,208,158,208,158,208,187,208,184,209,134,209, +141,209,130,208,176,208,158,208,189,208,176,208,189,208,181,208,188,208,180,208, +190,208,188,208,188,208,190,208,185,208,180,208,178,208,181,208,190,208,189,208, +190,209,129,209,131,208,180,224,164,149,224,165,135,224,164,185,224,165,136,224, +164,149,224,165,128,224,164,184,224,165,135,224,164,149,224,164,190,224,164,149, +224,165,139,224,164,148,224,164,176,224,164,170,224,164,176,224,164,168,224,165, +135,224,164,143,224,164,149,224,164,149,224,164,191,224,164,173,224,165,128,224, +164,135,224,164,184,224,164,149,224,164,176,224,164,164,224,165,139,224,164,185, +224,165,139,224,164,134,224,164,170,224,164,185,224,165,128,224,164,175,224,164, +185,224,164,175,224,164,190,224,164,164,224,164,149,224,164,165,224,164,190,106, +97,103,114,97,110,224,164,134,224,164,156,224,164,156,224,165,139,224,164,133, +224,164,172,224,164,166,224,165,139,224,164,151,224,164,136,224,164,156,224,164, +190,224,164,151,224,164,143,224,164,185,224,164,174,224,164,135,224,164,168,224, +164,181,224,164,185,224,164,175,224,165,135,224,164,165,224,165,135,224,164,165, +224,165,128,224,164,152,224,164,176,224,164,156,224,164,172,224,164,166,224,165, +128,224,164,149,224,164,136,224,164,156,224,165,128,224,164,181,224,165,135,224, +164,168,224,164,136,224,164,168,224,164,143,224,164,185,224,164,176,224,164,137, +224,164,184,224,164,174,224,165,135,224,164,149,224,164,174,224,164,181,224,165, +139,224,164,178,224,165,135,224,164,184,224,164,172,224,164,174,224,164,136,224, +164,166,224,165,135,224,164,147,224,164,176,224,164,134,224,164,174,224,164,172, +224,164,184,224,164,173,224,164,176,224,164,172,224,164,168,224,164,154,224,164, +178,224,164,174,224,164,168,224,164,134,224,164,151,224,164,184,224,165,128,224, +164,178,224,165,128,216,185,217,132,217,137,216,165,217,132,217,137,217,135,216, +176,216,167,216,162,216,174,216,177,216,185,216,175,216,175,216,167,217,132,217, +137,217,135,216,176,217,135,216,181,217,136,216,177,216,186,217,138,216,177,217, +131,216,167,217,134,217,136,217,132,216,167,216,168,217,138,217,134,216,185,216, +177,216,182,216,176,217,132,217,131,217,135,217,134,216,167,217,138,217,136,217, +133,217,130,216,167,217,132,216,185,217,132,217,138,216,167,217,134,216,167,217, +132,217,131,217,134,216,173,216,170,217,137,217,130,216,168,217,132,217,136,216, +173,216,169,216,167,216,174,216,177,217,129,217,130,216,183,216,185,216,168,216, +175,216,177,217,131,217,134,216,165,216,176,216,167,217,131,217,133,216,167,216, +167,216,173,216,175,216,165,217,132,216,167,217,129,217,138,217,135,216,168,216, +185,216,182,217,131,217,138,217,129,216,168,216,173,216,171,217,136,217,133,217, +134,217,136,217,135,217,136,216,163,217,134,216,167,216,172,216,175,216,167,217, +132,217,135,216,167,216,179,217,132,217,133,216,185,217,134,216,175,217,132,217, +138,216,179,216,185,216,168,216,177,216,181,217,132,217,137,217,133,217,134,216, +176,216,168,217,135,216,167,216,163,217,134,217,135,217,133,216,171,217,132,217, +131,217,134,216,170,216,167,217,132,216,167,216,173,217,138,216,171,217,133,216, +181,216,177,216,180,216,177,216,173,216,173,217,136,217,132,217,136,217,129,217, +138,216,167,216,176,216,167,217,132,217,131,217,132,217,133,216,177,216,169,216, +167,217,134,216,170,216,167,217,132,217,129,216,163,216,168,217,136,216,174,216, +167,216,181,216,163,217,134,216,170,216,167,217,134,217,135,216,167,217,132,217, +138,216,185,216,182,217,136,217,136,217,130,216,175,216,167,216,168,217,134,216, +174,217,138,216,177,216,168,217,134,216,170,217,132,217,131,217,133,216,180,216, +167,216,161,217,136,217,135,217,138,216,167,216,168,217,136,217,130,216,181,216, +181,217,136,217,133,216,167,216,177,217,130,217,133,216,163,216,173,216,175,217, +134,216,173,217,134,216,185,216,175,217,133,216,177,216,163,217,138,216,167,216, +173,216,169,217,131,216,170,216,168,216,175,217,136,217,134,217,138,216,172,216, +168,217,133,217,134,217,135,216,170,216,173,216,170,216,172,217,135,216,169,216, +179,217,134,216,169,217,138,216,170,217,133,217,131,216,177,216,169,216,186,216, +178,216,169,217,134,217,129,216,179,216,168,217,138,216,170,217,132,217,132,217, +135,217,132,217,134,216,167,216,170,217,132,217,131,217,130,217,132,216,168,217, +132,217,133,216,167,216,185,217,134,217,135,216,163,217,136,217,132,216,180,217, +138,216,161,217,134,217,136,216,177,216,163,217,133,216,167,217,129,217,138,217, +131,216,168,217,131,217,132,216,176,216,167,216,170,216,177,216,170,216,168,216, +168,216,163,217,134,217,135,217,133,216,179,216,167,217,134,217,131,216,168,217, +138,216,185,217,129,217,130,216,175,216,173,216,179,217,134,217,132,217,135,217, +133,216,180,216,185,216,177,216,163,217,135,217,132,216,180,217,135,216,177,217, +130,216,183,216,177,216,183,217,132,216,168,112,114,111,102,105,108,101,115,101, +114,118,105,99,101,100,101,102,97,117,108,116,104,105,109,115,101,108,102,100, +101,116,97,105,108,115,99,111,110,116,101,110,116,115,117,112,112,111,114,116, +115,116,97,114,116,101,100,109,101,115,115,97,103,101,115,117,99,99,101,115,115, +102,97,115,104,105,111,110,60,116,105,116,108,101,62,99,111,117,110,116,114,121, +97,99,99,111,117,110,116,99,114,101,97,116,101,100,115,116,111,114,105,101,115, +114,101,115,117,108,116,115,114,117,110,110,105,110,103,112,114,111,99,101,115, +115,119,114,105,116,105,110,103,111,98,106,101,99,116,115,118,105,115,105,98,108 +,101,119,101,108,99,111,109,101,97,114,116,105,99,108,101,117,110,107,110,111, +119,110,110,101,116,119,111,114,107,99,111,109,112,97,110,121,100,121,110,97,109 +,105,99,98,114,111,119,115,101,114,112,114,105,118,97,99,121,112,114,111,98,108, +101,109,83,101,114,118,105,99,101,114,101,115,112,101,99,116,100,105,115,112,108 +,97,121,114,101,113,117,101,115,116,114,101,115,101,114,118,101,119,101,98,115, +105,116,101,104,105,115,116,111,114,121,102,114,105,101,110,100,115,111,112,116, +105,111,110,115,119,111,114,107,105,110,103,118,101,114,115,105,111,110,109,105, +108,108,105,111,110,99,104,97,110,110,101,108,119,105,110,100,111,119,46,97,100, +100,114,101,115,115,118,105,115,105,116,101,100,119,101,97,116,104,101,114,99, +111,114,114,101,99,116,112,114,111,100,117,99,116,101,100,105,114,101,99,116,102 +,111,114,119,97,114,100,121,111,117,32,99,97,110,114,101,109,111,118,101,100,115 +,117,98,106,101,99,116,99,111,110,116,114,111,108,97,114,99,104,105,118,101,99, +117,114,114,101,110,116,114,101,97,100,105,110,103,108,105,98,114,97,114,121,108 +,105,109,105,116,101,100,109,97,110,97,103,101,114,102,117,114,116,104,101,114, +115,117,109,109,97,114,121,109,97,99,104,105,110,101,109,105,110,117,116,101,115 +,112,114,105,118,97,116,101,99,111,110,116,101,120,116,112,114,111,103,114,97, +109,115,111,99,105,101,116,121,110,117,109,98,101,114,115,119,114,105,116,116, +101,110,101,110,97,98,108,101,100,116,114,105,103,103,101,114,115,111,117,114,99 +,101,115,108,111,97,100,105,110,103,101,108,101,109,101,110,116,112,97,114,116, +110,101,114,102,105,110,97,108,108,121,112,101,114,102,101,99,116,109,101,97,110 +,105,110,103,115,121,115,116,101,109,115,107,101,101,112,105,110,103,99,117,108, +116,117,114,101,38,113,117,111,116,59,44,106,111,117,114,110,97,108,112,114,111, +106,101,99,116,115,117,114,102,97,99,101,115,38,113,117,111,116,59,101,120,112, +105,114,101,115,114,101,118,105,101,119,115,98,97,108,97,110,99,101,69,110,103, +108,105,115,104,67,111,110,116,101,110,116,116,104,114,111,117,103,104,80,108, +101,97,115,101,32,111,112,105,110,105,111,110,99,111,110,116,97,99,116,97,118, +101,114,97,103,101,112,114,105,109,97,114,121,118,105,108,108,97,103,101,83,112, +97,110,105,115,104,103,97,108,108,101,114,121,100,101,99,108,105,110,101,109,101 +,101,116,105,110,103,109,105,115,115,105,111,110,112,111,112,117,108,97,114,113, +117,97,108,105,116,121,109,101,97,115,117,114,101,103,101,110,101,114,97,108,115 +,112,101,99,105,101,115,115,101,115,115,105,111,110,115,101,99,116,105,111,110, +119,114,105,116,101,114,115,99,111,117,110,116,101,114,105,110,105,116,105,97, +108,114,101,112,111,114,116,115,102,105,103,117,114,101,115,109,101,109,98,101, +114,115,104,111,108,100,105,110,103,100,105,115,112,117,116,101,101,97,114,108, +105,101,114,101,120,112,114,101,115,115,100,105,103,105,116,97,108,112,105,99, +116,117,114,101,65,110,111,116,104,101,114,109,97,114,114,105,101,100,116,114,97 +,102,102,105,99,108,101,97,100,105,110,103,99,104,97,110,103,101,100,99,101,110, +116,114,97,108,118,105,99,116,111,114,121,105,109,97,103,101,115,47,114,101,97, +115,111,110,115,115,116,117,100,105,101,115,102,101,97,116,117,114,101,108,105, +115,116,105,110,103,109,117,115,116,32,98,101,115,99,104,111,111,108,115,86,101, +114,115,105,111,110,117,115,117,97,108,108,121,101,112,105,115,111,100,101,112, +108,97,121,105,110,103,103,114,111,119,105,110,103,111,98,118,105,111,117,115, +111,118,101,114,108,97,121,112,114,101,115,101,110,116,97,99,116,105,111,110,115 +,60,47,117,108,62,13,10,119,114,97,112,112,101,114,97,108,114,101,97,100,121,99, +101,114,116,97,105,110,114,101,97,108,105,116,121,115,116,111,114,97,103,101,97, +110,111,116,104,101,114,100,101,115,107,116,111,112,111,102,102,101,114,101,100, +112,97,116,116,101,114,110,117,110,117,115,117,97,108,68,105,103,105,116,97,108, +99,97,112,105,116,97,108,87,101,98,115,105,116,101,102,97,105,108,117,114,101,99 +,111,110,110,101,99,116,114,101,100,117,99,101,100,65,110,100,114,111,105,100, +100,101,99,97,100,101,115,114,101,103,117,108,97,114,32,38,97,109,112,59,32,97, +110,105,109,97,108,115,114,101,108,101,97,115,101,65,117,116,111,109,97,116,103, +101,116,116,105,110,103,109,101,116,104,111,100,115,110,111,116,104,105,110,103, +80,111,112,117,108,97,114,99,97,112,116,105,111,110,108,101,116,116,101,114,115, +99,97,112,116,117,114,101,115,99,105,101,110,99,101,108,105,99,101,110,115,101, +99,104,97,110,103,101,115,69,110,103,108,97,110,100,61,49,38,97,109,112,59,72, +105,115,116,111,114,121,32,61,32,110,101,119,32,67,101,110,116,114,97,108,117, +112,100,97,116,101,100,83,112,101,99,105,97,108,78,101,116,119,111,114,107,114, +101,113,117,105,114,101,99,111,109,109,101,110,116,119,97,114,110,105,110,103,67 +,111,108,108,101,103,101,116,111,111,108,98,97,114,114,101,109,97,105,110,115,98 +,101,99,97,117,115,101,101,108,101,99,116,101,100,68,101,117,116,115,99,104,102, +105,110,97,110,99,101,119,111,114,107,101,114,115,113,117,105,99,107,108,121,98, +101,116,119,101,101,110,101,120,97,99,116,108,121,115,101,116,116,105,110,103, +100,105,115,101,97,115,101,83,111,99,105,101,116,121,119,101,97,112,111,110,115, +101,120,104,105,98,105,116,38,108,116,59,33,45,45,67,111,110,116,114,111,108,99, +108,97,115,115,101,115,99,111,118,101,114,101,100,111,117,116,108,105,110,101,97 +,116,116,97,99,107,115,100,101,118,105,99,101,115,40,119,105,110,100,111,119,112 +,117,114,112,111,115,101,116,105,116,108,101,61,34,77,111,98,105,108,101,32,107, +105,108,108,105,110,103,115,104,111,119,105,110,103,73,116,97,108,105,97,110,100 +,114,111,112,112,101,100,104,101,97,118,105,108,121,101,102,102,101,99,116,115, +45,49,39,93,41,59,10,99,111,110,102,105,114,109,67,117,114,114,101,110,116,97, +100,118,97,110,99,101,115,104,97,114,105,110,103,111,112,101,110,105,110,103,100 +,114,97,119,105,110,103,98,105,108,108,105,111,110,111,114,100,101,114,101,100, +71,101,114,109,97,110,121,114,101,108,97,116,101,100,60,47,102,111,114,109,62, +105,110,99,108,117,100,101,119,104,101,116,104,101,114,100,101,102,105,110,101, +100,83,99,105,101,110,99,101,99,97,116,97,108,111,103,65,114,116,105,99,108,101, +98,117,116,116,111,110,115,108,97,114,103,101,115,116,117,110,105,102,111,114, +109,106,111,117,114,110,101,121,115,105,100,101,98,97,114,67,104,105,99,97,103, +111,104,111,108,105,100,97,121,71,101,110,101,114,97,108,112,97,115,115,97,103, +101,44,38,113,117,111,116,59,97,110,105,109,97,116,101,102,101,101,108,105,110, +103,97,114,114,105,118,101,100,112,97,115,115,105,110,103,110,97,116,117,114,97, +108,114,111,117,103,104,108,121,46,10,10,84,104,101,32,98,117,116,32,110,111,116 +,100,101,110,115,105,116,121,66,114,105,116,97,105,110,67,104,105,110,101,115, +101,108,97,99,107,32,111,102,116,114,105,98,117,116,101,73,114,101,108,97,110, +100,34,32,100,97,116,97,45,102,97,99,116,111,114,115,114,101,99,101,105,118,101, +116,104,97,116,32,105,115,76,105,98,114,97,114,121,104,117,115,98,97,110,100,105 +,110,32,102,97,99,116,97,102,102,97,105,114,115,67,104,97,114,108,101,115,114,97 +,100,105,99,97,108,98,114,111,117,103,104,116,102,105,110,100,105,110,103,108,97 +,110,100,105,110,103,58,108,97,110,103,61,34,114,101,116,117,114,110,32,108,101, +97,100,101,114,115,112,108,97,110,110,101,100,112,114,101,109,105,117,109,112,97 +,99,107,97,103,101,65,109,101,114,105,99,97,69,100,105,116,105,111,110,93,38,113 +,117,111,116,59,77,101,115,115,97,103,101,110,101,101,100,32,116,111,118,97,108, +117,101,61,34,99,111,109,112,108,101,120,108,111,111,107,105,110,103,115,116,97, +116,105,111,110,98,101,108,105,101,118,101,115,109,97,108,108,101,114,45,109,111 +,98,105,108,101,114,101,99,111,114,100,115,119,97,110,116,32,116,111,107,105,110 +,100,32,111,102,70,105,114,101,102,111,120,121,111,117,32,97,114,101,115,105,109 +,105,108,97,114,115,116,117,100,105,101,100,109,97,120,105,109,117,109,104,101, +97,100,105,110,103,114,97,112,105,100,108,121,99,108,105,109,97,116,101,107,105, +110,103,100,111,109,101,109,101,114,103,101,100,97,109,111,117,110,116,115,102, +111,117,110,100,101,100,112,105,111,110,101,101,114,102,111,114,109,117,108,97, +100,121,110,97,115,116,121,104,111,119,32,116,111,32,83,117,112,112,111,114,116, +114,101,118,101,110,117,101,101,99,111,110,111,109,121,82,101,115,117,108,116, +115,98,114,111,116,104,101,114,115,111,108,100,105,101,114,108,97,114,103,101, +108,121,99,97,108,108,105,110,103,46,38,113,117,111,116,59,65,99,99,111,117,110, +116,69,100,119,97,114,100,32,115,101,103,109,101,110,116,82,111,98,101,114,116, +32,101,102,102,111,114,116,115,80,97,99,105,102,105,99,108,101,97,114,110,101, +100,117,112,32,119,105,116,104,104,101,105,103,104,116,58,119,101,32,104,97,118, +101,65,110,103,101,108,101,115,110,97,116,105,111,110,115,95,115,101,97,114,99, +104,97,112,112,108,105,101,100,97,99,113,117,105,114,101,109,97,115,115,105,118, +101,103,114,97,110,116,101,100,58,32,102,97,108,115,101,116,114,101,97,116,101, +100,98,105,103,103,101,115,116,98,101,110,101,102,105,116,100,114,105,118,105, +110,103,83,116,117,100,105,101,115,109,105,110,105,109,117,109,112,101,114,104, +97,112,115,109,111,114,110,105,110,103,115,101,108,108,105,110,103,105,115,32, +117,115,101,100,114,101,118,101,114,115,101,118,97,114,105,97,110,116,32,114,111 +,108,101,61,34,109,105,115,115,105,110,103,97,99,104,105,101,118,101,112,114,111 +,109,111,116,101,115,116,117,100,101,110,116,115,111,109,101,111,110,101,101,120 +,116,114,101,109,101,114,101,115,116,111,114,101,98,111,116,116,111,109,58,101, +118,111,108,118,101,100,97,108,108,32,116,104,101,115,105,116,101,109,97,112,101 +,110,103,108,105,115,104,119,97,121,32,116,111,32,32,65,117,103,117,115,116,115, +121,109,98,111,108,115,67,111,109,112,97,110,121,109,97,116,116,101,114,115,109, +117,115,105,99,97,108,97,103,97,105,110,115,116,115,101,114,118,105,110,103,125, +41,40,41,59,13,10,112,97,121,109,101,110,116,116,114,111,117,98,108,101,99,111, +110,99,101,112,116,99,111,109,112,97,114,101,112,97,114,101,110,116,115,112,108, +97,121,101,114,115,114,101,103,105,111,110,115,109,111,110,105,116,111,114,32,39 +,39,84,104,101,32,119,105,110,110,105,110,103,101,120,112,108,111,114,101,97,100 +,97,112,116,101,100,71,97,108,108,101,114,121,112,114,111,100,117,99,101,97,98, +105,108,105,116,121,101,110,104,97,110,99,101,99,97,114,101,101,114,115,41,46,32 +,84,104,101,32,99,111,108,108,101,99,116,83,101,97,114,99,104,32,97,110,99,105, +101,110,116,101,120,105,115,116,101,100,102,111,111,116,101,114,32,104,97,110, +100,108,101,114,112,114,105,110,116,101,100,99,111,110,115,111,108,101,69,97,115 +,116,101,114,110,101,120,112,111,114,116,115,119,105,110,100,111,119,115,67,104, +97,110,110,101,108,105,108,108,101,103,97,108,110,101,117,116,114,97,108,115,117 +,103,103,101,115,116,95,104,101,97,100,101,114,115,105,103,110,105,110,103,46, +104,116,109,108,34,62,115,101,116,116,108,101,100,119,101,115,116,101,114,110,99 +,97,117,115,105,110,103,45,119,101,98,107,105,116,99,108,97,105,109,101,100,74, +117,115,116,105,99,101,99,104,97,112,116,101,114,118,105,99,116,105,109,115,84, +104,111,109,97,115,32,109,111,122,105,108,108,97,112,114,111,109,105,115,101,112 +,97,114,116,105,101,115,101,100,105,116,105,111,110,111,117,116,115,105,100,101, +58,102,97,108,115,101,44,104,117,110,100,114,101,100,79,108,121,109,112,105,99, +95,98,117,116,116,111,110,97,117,116,104,111,114,115,114,101,97,99,104,101,100, +99,104,114,111,110,105,99,100,101,109,97,110,100,115,115,101,99,111,110,100,115, +112,114,111,116,101,99,116,97,100,111,112,116,101,100,112,114,101,112,97,114,101 +,110,101,105,116,104,101,114,103,114,101,97,116,108,121,103,114,101,97,116,101, +114,111,118,101,114,97,108,108,105,109,112,114,111,118,101,99,111,109,109,97,110 +,100,115,112,101,99,105,97,108,115,101,97,114,99,104,46,119,111,114,115,104,105, +112,102,117,110,100,105,110,103,116,104,111,117,103,104,116,104,105,103,104,101, +115,116,105,110,115,116,101,97,100,117,116,105,108,105,116,121,113,117,97,114, +116,101,114,67,117,108,116,117,114,101,116,101,115,116,105,110,103,99,108,101,97 +,114,108,121,101,120,112,111,115,101,100,66,114,111,119,115,101,114,108,105,98, +101,114,97,108,125,32,99,97,116,99,104,80,114,111,106,101,99,116,101,120,97,109, +112,108,101,104,105,100,101,40,41,59,70,108,111,114,105,100,97,97,110,115,119, +101,114,115,97,108,108,111,119,101,100,69,109,112,101,114,111,114,100,101,102, +101,110,115,101,115,101,114,105,111,117,115,102,114,101,101,100,111,109,83,101, +118,101,114,97,108,45,98,117,116,116,111,110,70,117,114,116,104,101,114,111,117, +116,32,111,102,32,33,61,32,110,117,108,108,116,114,97,105,110,101,100,68,101,110 +,109,97,114,107,118,111,105,100,40,48,41,47,97,108,108,46,106,115,112,114,101, +118,101,110,116,82,101,113,117,101,115,116,83,116,101,112,104,101,110,10,10,87, +104,101,110,32,111,98,115,101,114,118,101,60,47,104,50,62,13,10,77,111,100,101, +114,110,32,112,114,111,118,105,100,101,34,32,97,108,116,61,34,98,111,114,100,101 +,114,115,46,10,10,70,111,114,32,10,10,77,97,110,121,32,97,114,116,105,115,116, +115,112,111,119,101,114,101,100,112,101,114,102,111,114,109,102,105,99,116,105, +111,110,116,121,112,101,32,111,102,109,101,100,105,99,97,108,116,105,99,107,101, +116,115,111,112,112,111,115,101,100,67,111,117,110,99,105,108,119,105,116,110, +101,115,115,106,117,115,116,105,99,101,71,101,111,114,103,101,32,66,101,108,103, +105,117,109,46,46,46,60,47,97,62,116,119,105,116,116,101,114,110,111,116,97,98, +108,121,119,97,105,116,105,110,103,119,97,114,102,97,114,101,32,79,116,104,101, +114,32,114,97,110,107,105,110,103,112,104,114,97,115,101,115,109,101,110,116,105 +,111,110,115,117,114,118,105,118,101,115,99,104,111,108,97,114,60,47,112,62,13, +10,32,67,111,117,110,116,114,121,105,103,110,111,114,101,100,108,111,115,115,32, +111,102,106,117,115,116,32,97,115,71,101,111,114,103,105,97,115,116,114,97,110, +103,101,60,104,101,97,100,62,60,115,116,111,112,112,101,100,49,39,93,41,59,13,10 +,105,115,108,97,110,100,115,110,111,116,97,98,108,101,98,111,114,100,101,114,58, +108,105,115,116,32,111,102,99,97,114,114,105,101,100,49,48,48,44,48,48,48,60,47, +104,51,62,10,32,115,101,118,101,114,97,108,98,101,99,111,109,101,115,115,101,108 +,101,99,116,32,119,101,100,100,105,110,103,48,48,46,104,116,109,108,109,111,110, +97,114,99,104,111,102,102,32,116,104,101,116,101,97,99,104,101,114,104,105,103, +104,108,121,32,98,105,111,108,111,103,121,108,105,102,101,32,111,102,111,114,32, +101,118,101,110,114,105,115,101,32,111,102,38,114,97,113,117,111,59,112,108,117, +115,111,110,101,104,117,110,116,105,110,103,40,116,104,111,117,103,104,68,111, +117,103,108,97,115,106,111,105,110,105,110,103,99,105,114,99,108,101,115,70,111, +114,32,116,104,101,65,110,99,105,101,110,116,86,105,101,116,110,97,109,118,101, +104,105,99,108,101,115,117,99,104,32,97,115,99,114,121,115,116,97,108,118,97,108 +,117,101,32,61,87,105,110,100,111,119,115,101,110,106,111,121,101,100,97,32,115, +109,97,108,108,97,115,115,117,109,101,100,60,97,32,105,100,61,34,102,111,114,101 +,105,103,110,32,65,108,108,32,114,105,104,111,119,32,116,104,101,68,105,115,112, +108,97,121,114,101,116,105,114,101,100,104,111,119,101,118,101,114,104,105,100, +100,101,110,59,98,97,116,116,108,101,115,115,101,101,107,105,110,103,99,97,98, +105,110,101,116,119,97,115,32,110,111,116,108,111,111,107,32,97,116,99,111,110, +100,117,99,116,103,101,116,32,116,104,101,74,97,110,117,97,114,121,104,97,112, +112,101,110,115,116,117,114,110,105,110,103,97,58,104,111,118,101,114,79,110,108 +,105,110,101,32,70,114,101,110,99,104,32,108,97,99,107,105,110,103,116,121,112, +105,99,97,108,101,120,116,114,97,99,116,101,110,101,109,105,101,115,101,118,101, +110,32,105,102,103,101,110,101,114,97,116,100,101,99,105,100,101,100,97,114,101, +32,110,111,116,47,115,101,97,114,99,104,98,101,108,105,101,102,115,45,105,109,97 +,103,101,58,108,111,99,97,116,101,100,115,116,97,116,105,99,46,108,111,103,105, +110,34,62,99,111,110,118,101,114,116,118,105,111,108,101,110,116,101,110,116,101 +,114,101,100,102,105,114,115,116,34,62,99,105,114,99,117,105,116,70,105,110,108, +97,110,100,99,104,101,109,105,115,116,115,104,101,32,119,97,115,49,48,112,120,59 +,34,62,97,115,32,115,117,99,104,100,105,118,105,100,101,100,60,47,115,112,97,110 +,62,119,105,108,108,32,98,101,108,105,110,101,32,111,102,97,32,103,114,101,97, +116,109,121,115,116,101,114,121,47,105,110,100,101,120,46,102,97,108,108,105,110 +,103,100,117,101,32,116,111,32,114,97,105,108,119,97,121,99,111,108,108,101,103, +101,109,111,110,115,116,101,114,100,101,115,99,101,110,116,105,116,32,119,105, +116,104,110,117,99,108,101,97,114,74,101,119,105,115,104,32,112,114,111,116,101, +115,116,66,114,105,116,105,115,104,102,108,111,119,101,114,115,112,114,101,100, +105,99,116,114,101,102,111,114,109,115,98,117,116,116,111,110,32,119,104,111,32, +119,97,115,108,101,99,116,117,114,101,105,110,115,116,97,110,116,115,117,105,99, +105,100,101,103,101,110,101,114,105,99,112,101,114,105,111,100,115,109,97,114, +107,101,116,115,83,111,99,105,97,108,32,102,105,115,104,105,110,103,99,111,109, +98,105,110,101,103,114,97,112,104,105,99,119,105,110,110,101,114,115,60,98,114, +32,47,62,60,98,121,32,116,104,101,32,78,97,116,117,114,97,108,80,114,105,118,97, +99,121,99,111,111,107,105,101,115,111,117,116,99,111,109,101,114,101,115,111,108 +,118,101,83,119,101,100,105,115,104,98,114,105,101,102,108,121,80,101,114,115, +105,97,110,115,111,32,109,117,99,104,67,101,110,116,117,114,121,100,101,112,105, +99,116,115,99,111,108,117,109,110,115,104,111,117,115,105,110,103,115,99,114,105 +,112,116,115,110,101,120,116,32,116,111,98,101,97,114,105,110,103,109,97,112,112 +,105,110,103,114,101,118,105,115,101,100,106,81,117,101,114,121,40,45,119,105, +100,116,104,58,116,105,116,108,101,34,62,116,111,111,108,116,105,112,83,101,99, +116,105,111,110,100,101,115,105,103,110,115,84,117,114,107,105,115,104,121,111, +117,110,103,101,114,46,109,97,116,99,104,40,125,41,40,41,59,10,10,98,117,114,110 +,105,110,103,111,112,101,114,97,116,101,100,101,103,114,101,101,115,115,111,117, +114,99,101,61,82,105,99,104,97,114,100,99,108,111,115,101,108,121,112,108,97,115 +,116,105,99,101,110,116,114,105,101,115,60,47,116,114,62,13,10,99,111,108,111, +114,58,35,117,108,32,105,100,61,34,112,111,115,115,101,115,115,114,111,108,108, +105,110,103,112,104,121,115,105,99,115,102,97,105,108,105,110,103,101,120,101,99 +,117,116,101,99,111,110,116,101,115,116,108,105,110,107,32,116,111,68,101,102,97 +,117,108,116,60,98,114,32,47,62,10,58,32,116,114,117,101,44,99,104,97,114,116, +101,114,116,111,117,114,105,115,109,99,108,97,115,115,105,99,112,114,111,99,101, +101,100,101,120,112,108,97,105,110,60,47,104,49,62,13,10,111,110,108,105,110,101 +,46,63,120,109,108,32,118,101,104,101,108,112,105,110,103,100,105,97,109,111,110 +,100,117,115,101,32,116,104,101,97,105,114,108,105,110,101,101,110,100,32,45,45, +62,41,46,97,116,116,114,40,114,101,97,100,101,114,115,104,111,115,116,105,110, +103,35,102,102,102,102,102,102,114,101,97,108,105,122,101,86,105,110,99,101,110, +116,115,105,103,110,97,108,115,32,115,114,99,61,34,47,80,114,111,100,117,99,116, +100,101,115,112,105,116,101,100,105,118,101,114,115,101,116,101,108,108,105,110, +103,80,117,98,108,105,99,32,104,101,108,100,32,105,110,74,111,115,101,112,104,32 +,116,104,101,97,116,114,101,97,102,102,101,99,116,115,60,115,116,121,108,101,62, +97,32,108,97,114,103,101,100,111,101,115,110,39,116,108,97,116,101,114,44,32,69, +108,101,109,101,110,116,102,97,118,105,99,111,110,99,114,101,97,116,111,114,72, +117,110,103,97,114,121,65,105,114,112,111,114,116,115,101,101,32,116,104,101,115 +,111,32,116,104,97,116,77,105,99,104,97,101,108,83,121,115,116,101,109,115,80, +114,111,103,114,97,109,115,44,32,97,110,100,32,32,119,105,100,116,104,61,101,38, +113,117,111,116,59,116,114,97,100,105,110,103,108,101,102,116,34,62,10,112,101, +114,115,111,110,115,71,111,108,100,101,110,32,65,102,102,97,105,114,115,103,114, +97,109,109,97,114,102,111,114,109,105,110,103,100,101,115,116,114,111,121,105, +100,101,97,32,111,102,99,97,115,101,32,111,102,111,108,100,101,115,116,32,116, +104,105,115,32,105,115,46,115,114,99,32,61,32,99,97,114,116,111,111,110,114,101, +103,105,115,116,114,67,111,109,109,111,110,115,77,117,115,108,105,109,115,87,104 +,97,116,32,105,115,105,110,32,109,97,110,121,109,97,114,107,105,110,103,114,101, +118,101,97,108,115,73,110,100,101,101,100,44,101,113,117,97,108,108,121,47,115, +104,111,119,95,97,111,117,116,100,111,111,114,101,115,99,97,112,101,40,65,117, +115,116,114,105,97,103,101,110,101,116,105,99,115,121,115,116,101,109,44,73,110, +32,116,104,101,32,115,105,116,116,105,110,103,72,101,32,97,108,115,111,73,115, +108,97,110,100,115,65,99,97,100,101,109,121,10,9,9,60,33,45,45,68,97,110,105,101 +,108,32,98,105,110,100,105,110,103,98,108,111,99,107,34,62,105,109,112,111,115, +101,100,117,116,105,108,105,122,101,65,98,114,97,104,97,109,40,101,120,99,101, +112,116,123,119,105,100,116,104,58,112,117,116,116,105,110,103,41,46,104,116,109 +,108,40,124,124,32,91,93,59,10,68,65,84,65,91,32,42,107,105,116,99,104,101,110, +109,111,117,110,116,101,100,97,99,116,117,97,108,32,100,105,97,108,101,99,116, +109,97,105,110,108,121,32,95,98,108,97,110,107,39,105,110,115,116,97,108,108,101 +,120,112,101,114,116,115,105,102,40,116,121,112,101,73,116,32,97,108,115,111,38, +99,111,112,121,59,32,34,62,84,101,114,109,115,98,111,114,110,32,105,110,79,112, +116,105,111,110,115,101,97,115,116,101,114,110,116,97,108,107,105,110,103,99,111 +,110,99,101,114,110,103,97,105,110,101,100,32,111,110,103,111,105,110,103,106, +117,115,116,105,102,121,99,114,105,116,105,99,115,102,97,99,116,111,114,121,105, +116,115,32,111,119,110,97,115,115,97,117,108,116,105,110,118,105,116,101,100,108 +,97,115,116,105,110,103,104,105,115,32,111,119,110,104,114,101,102,61,34,47,34, +32,114,101,108,61,34,100,101,118,101,108,111,112,99,111,110,99,101,114,116,100, +105,97,103,114,97,109,100,111,108,108,97,114,115,99,108,117,115,116,101,114,112, +104,112,63,105,100,61,97,108,99,111,104,111,108,41,59,125,41,40,41,59,117,115, +105,110,103,32,97,62,60,115,112,97,110,62,118,101,115,115,101,108,115,114,101, +118,105,118,97,108,65,100,100,114,101,115,115,97,109,97,116,101,117,114,97,110, +100,114,111,105,100,97,108,108,101,103,101,100,105,108,108,110,101,115,115,119, +97,108,107,105,110,103,99,101,110,116,101,114,115,113,117,97,108,105,102,121,109 +,97,116,99,104,101,115,117,110,105,102,105,101,100,101,120,116,105,110,99,116,68 +,101,102,101,110,115,101,100,105,101,100,32,105,110,10,9,60,33,45,45,32,99,117, +115,116,111,109,115,108,105,110,107,105,110,103,76,105,116,116,108,101,32,66,111 +,111,107,32,111,102,101,118,101,110,105,110,103,109,105,110,46,106,115,63,97,114 +,101,32,116,104,101,107,111,110,116,97,107,116,116,111,100,97,121,39,115,46,104, +116,109,108,34,32,116,97,114,103,101,116,61,119,101,97,114,105,110,103,65,108, +108,32,82,105,103,59,10,125,41,40,41,59,114,97,105,115,105,110,103,32,65,108,115 +,111,44,32,99,114,117,99,105,97,108,97,98,111,117,116,34,62,100,101,99,108,97, +114,101,45,45,62,10,60,115,99,102,105,114,101,102,111,120,97,115,32,109,117,99, +104,97,112,112,108,105,101,115,105,110,100,101,120,44,32,115,44,32,98,117,116,32 +,116,121,112,101,32,61,32,10,13,10,60,33,45,45,116,111,119,97,114,100,115,82,101 +,99,111,114,100,115,80,114,105,118,97,116,101,70,111,114,101,105,103,110,80,114, +101,109,105,101,114,99,104,111,105,99,101,115,86,105,114,116,117,97,108,114,101, +116,117,114,110,115,67,111,109,109,101,110,116,80,111,119,101,114,101,100,105, +110,108,105,110,101,59,112,111,118,101,114,116,121,99,104,97,109,98,101,114,76, +105,118,105,110,103,32,118,111,108,117,109,101,115,65,110,116,104,111,110,121, +108,111,103,105,110,34,32,82,101,108,97,116,101,100,69,99,111,110,111,109,121, +114,101,97,99,104,101,115,99,117,116,116,105,110,103,103,114,97,118,105,116,121, +108,105,102,101,32,105,110,67,104,97,112,116,101,114,45,115,104,97,100,111,119, +78,111,116,97,98,108,101,60,47,116,100,62,13,10,32,114,101,116,117,114,110,115, +116,97,100,105,117,109,119,105,100,103,101,116,115,118,97,114,121,105,110,103, +116,114,97,118,101,108,115,104,101,108,100,32,98,121,119,104,111,32,97,114,101, +119,111,114,107,32,105,110,102,97,99,117,108,116,121,97,110,103,117,108,97,114, +119,104,111,32,104,97,100,97,105,114,112,111,114,116,116,111,119,110,32,111,102, +10,10,83,111,109,101,32,39,99,108,105,99,107,39,99,104,97,114,103,101,115,107, +101,121,119,111,114,100,105,116,32,119,105,108,108,99,105,116,121,32,111,102,40, +116,104,105,115,41,59,65,110,100,114,101,119,32,117,110,105,113,117,101,32,99, +104,101,99,107,101,100,111,114,32,109,111,114,101,51,48,48,112,120,59,32,114,101 +,116,117,114,110,59,114,115,105,111,110,61,34,112,108,117,103,105,110,115,119, +105,116,104,105,110,32,104,101,114,115,101,108,102,83,116,97,116,105,111,110,70, +101,100,101,114,97,108,118,101,110,116,117,114,101,112,117,98,108,105,115,104, +115,101,110,116,32,116,111,116,101,110,115,105,111,110,97,99,116,114,101,115,115 +,99,111,109,101,32,116,111,102,105,110,103,101,114,115,68,117,107,101,32,111,102 +,112,101,111,112,108,101,44,101,120,112,108,111,105,116,119,104,97,116,32,105, +115,104,97,114,109,111,110,121,97,32,109,97,106,111,114,34,58,34,104,116,116,112 +,105,110,32,104,105,115,32,109,101,110,117,34,62,10,109,111,110,116,104,108,121, +111,102,102,105,99,101,114,99,111,117,110,99,105,108,103,97,105,110,105,110,103, +101,118,101,110,32,105,110,83,117,109,109,97,114,121,100,97,116,101,32,111,102, +108,111,121,97,108,116,121,102,105,116,110,101,115,115,97,110,100,32,119,97,115, +101,109,112,101,114,111,114,115,117,112,114,101,109,101,83,101,99,111,110,100,32 +,104,101,97,114,105,110,103,82,117,115,115,105,97,110,108,111,110,103,101,115, +116,65,108,98,101,114,116,97,108,97,116,101,114,97,108,115,101,116,32,111,102,32 +,115,109,97,108,108,34,62,46,97,112,112,101,110,100,100,111,32,119,105,116,104, +102,101,100,101,114,97,108,98,97,110,107,32,111,102,98,101,110,101,97,116,104,68 +,101,115,112,105,116,101,67,97,112,105,116,97,108,103,114,111,117,110,100,115,41 +,44,32,97,110,100,32,112,101,114,99,101,110,116,105,116,32,102,114,111,109,99, +108,111,115,105,110,103,99,111,110,116,97,105,110,73,110,115,116,101,97,100,102, +105,102,116,101,101,110,97,115,32,119,101,108,108,46,121,97,104,111,111,46,114, +101,115,112,111,110,100,102,105,103,104,116,101,114,111,98,115,99,117,114,101, +114,101,102,108,101,99,116,111,114,103,97,110,105,99,61,32,77,97,116,104,46,101, +100,105,116,105,110,103,111,110,108,105,110,101,32,112,97,100,100,105,110,103,97 +,32,119,104,111,108,101,111,110,101,114,114,111,114,121,101,97,114,32,111,102, +101,110,100,32,111,102,32,98,97,114,114,105,101,114,119,104,101,110,32,105,116, +104,101,97,100,101,114,32,104,111,109,101,32,111,102,114,101,115,117,109,101,100 +,114,101,110,97,109,101,100,115,116,114,111,110,103,62,104,101,97,116,105,110, +103,114,101,116,97,105,110,115,99,108,111,117,100,102,114,119,97,121,32,111,102, +32,77,97,114,99,104,32,49,107,110,111,119,105,110,103,105,110,32,112,97,114,116, +66,101,116,119,101,101,110,108,101,115,115,111,110,115,99,108,111,115,101,115, +116,118,105,114,116,117,97,108,108,105,110,107,115,34,62,99,114,111,115,115,101, +100,69,78,68,32,45,45,62,102,97,109,111,117,115,32,97,119,97,114,100,101,100,76, +105,99,101,110,115,101,72,101,97,108,116,104,32,102,97,105,114,108,121,32,119, +101,97,108,116,104,121,109,105,110,105,109,97,108,65,102,114,105,99,97,110,99, +111,109,112,101,116,101,108,97,98,101,108,34,62,115,105,110,103,105,110,103,102, +97,114,109,101,114,115,66,114,97,115,105,108,41,100,105,115,99,117,115,115,114, +101,112,108,97,99,101,71,114,101,103,111,114,121,102,111,110,116,32,99,111,112, +117,114,115,117,101,100,97,112,112,101,97,114,115,109,97,107,101,32,117,112,114, +111,117,110,100,101,100,98,111,116,104,32,111,102,98,108,111,99,107,101,100,115, +97,119,32,116,104,101,111,102,102,105,99,101,115,99,111,108,111,117,114,115,105, +102,40,100,111,99,117,119,104,101,110,32,104,101,101,110,102,111,114,99,101,112, +117,115,104,40,102,117,65,117,103,117,115,116,32,85,84,70,45,56,34,62,70,97,110, +116,97,115,121,105,110,32,109,111,115,116,105,110,106,117,114,101,100,85,115,117 +,97,108,108,121,102,97,114,109,105,110,103,99,108,111,115,117,114,101,111,98,106 +,101,99,116,32,100,101,102,101,110,99,101,117,115,101,32,111,102,32,77,101,100, +105,99,97,108,60,98,111,100,121,62,10,101,118,105,100,101,110,116,98,101,32,117, +115,101,100,107,101,121,67,111,100,101,115,105,120,116,101,101,110,73,115,108,97 +,109,105,99,35,48,48,48,48,48,48,101,110,116,105,114,101,32,119,105,100,101,108, +121,32,97,99,116,105,118,101,32,40,116,121,112,101,111,102,111,110,101,32,99,97, +110,99,111,108,111,114,32,61,115,112,101,97,107,101,114,101,120,116,101,110,100, +115,80,104,121,115,105,99,115,116,101,114,114,97,105,110,60,116,98,111,100,121, +62,102,117,110,101,114,97,108,118,105,101,119,105,110,103,109,105,100,100,108, +101,32,99,114,105,99,107,101,116,112,114,111,112,104,101,116,115,104,105,102,116 +,101,100,100,111,99,116,111,114,115,82,117,115,115,101,108,108,32,116,97,114,103 +,101,116,99,111,109,112,97,99,116,97,108,103,101,98,114,97,115,111,99,105,97,108 +,45,98,117,108,107,32,111,102,109,97,110,32,97,110,100,60,47,116,100,62,10,32, +104,101,32,108,101,102,116,41,46,118,97,108,40,41,102,97,108,115,101,41,59,108, +111,103,105,99,97,108,98,97,110,107,105,110,103,104,111,109,101,32,116,111,110, +97,109,105,110,103,32,65,114,105,122,111,110,97,99,114,101,100,105,116,115,41,59 +,10,125,41,59,10,102,111,117,110,100,101,114,105,110,32,116,117,114,110,67,111, +108,108,105,110,115,98,101,102,111,114,101,32,66,117,116,32,116,104,101,99,104, +97,114,103,101,100,84,105,116,108,101,34,62,67,97,112,116,97,105,110,115,112,101 +,108,108,101,100,103,111,100,100,101,115,115,84,97,103,32,45,45,62,65,100,100, +105,110,103,58,98,117,116,32,119,97,115,82,101,99,101,110,116,32,112,97,116,105, +101,110,116,98,97,99,107,32,105,110,61,102,97,108,115,101,38,76,105,110,99,111, +108,110,119,101,32,107,110,111,119,67,111,117,110,116,101,114,74,117,100,97,105, +115,109,115,99,114,105,112,116,32,97,108,116,101,114,101,100,39,93,41,59,10,32, +32,104,97,115,32,116,104,101,117,110,99,108,101,97,114,69,118,101,110,116,39,44, +98,111,116,104,32,105,110,110,111,116,32,97,108,108,10,10,60,33,45,45,32,112,108 +,97,99,105,110,103,104,97,114,100,32,116,111,32,99,101,110,116,101,114,115,111, +114,116,32,111,102,99,108,105,101,110,116,115,115,116,114,101,101,116,115,66,101 +,114,110,97,114,100,97,115,115,101,114,116,115,116,101,110,100,32,116,111,102,97 +,110,116,97,115,121,100,111,119,110,32,105,110,104,97,114,98,111,117,114,70,114, +101,101,100,111,109,106,101,119,101,108,114,121,47,97,98,111,117,116,46,46,115, +101,97,114,99,104,108,101,103,101,110,100,115,105,115,32,109,97,100,101,109,111, +100,101,114,110,32,111,110,108,121,32,111,110,111,110,108,121,32,116,111,105,109 +,97,103,101,34,32,108,105,110,101,97,114,32,112,97,105,110,116,101,114,97,110, +100,32,110,111,116,114,97,114,101,108,121,32,97,99,114,111,110,121,109,100,101, +108,105,118,101,114,115,104,111,114,116,101,114,48,48,38,97,109,112,59,97,115,32 +,109,97,110,121,119,105,100,116,104,61,34,47,42,32,60,33,91,67,116,105,116,108, +101,32,61,111,102,32,116,104,101,32,108,111,119,101,115,116,32,112,105,99,107, +101,100,32,101,115,99,97,112,101,100,117,115,101,115,32,111,102,112,101,111,112, +108,101,115,32,80,117,98,108,105,99,77,97,116,116,104,101,119,116,97,99,116,105, +99,115,100,97,109,97,103,101,100,119,97,121,32,102,111,114,108,97,119,115,32,111 +,102,101,97,115,121,32,116,111,32,119,105,110,100,111,119,115,116,114,111,110, +103,32,32,115,105,109,112,108,101,125,99,97,116,99,104,40,115,101,118,101,110, +116,104,105,110,102,111,98,111,120,119,101,110,116,32,116,111,112,97,105,110,116 +,101,100,99,105,116,105,122,101,110,73,32,100,111,110,39,116,114,101,116,114,101 +,97,116,46,32,83,111,109,101,32,119,119,46,34,41,59,10,98,111,109,98,105,110,103 +,109,97,105,108,116,111,58,109,97,100,101,32,105,110,46,32,77,97,110,121,32,99, +97,114,114,105,101,115,124,124,123,125,59,119,105,119,111,114,107,32,111,102,115 +,121,110,111,110,121,109,100,101,102,101,97,116,115,102,97,118,111,114,101,100, +111,112,116,105,99,97,108,112,97,103,101,84,114,97,117,110,108,101,115,115,32, +115,101,110,100,105,110,103,108,101,102,116,34,62,60,99,111,109,83,99,111,114,65 +,108,108,32,116,104,101,106,81,117,101,114,121,46,116,111,117,114,105,115,116,67 +,108,97,115,115,105,99,102,97,108,115,101,34,32,87,105,108,104,101,108,109,115, +117,98,117,114,98,115,103,101,110,117,105,110,101,98,105,115,104,111,112,115,46, +115,112,108,105,116,40,103,108,111,98,97,108,32,102,111,108,108,111,119,115,98, +111,100,121,32,111,102,110,111,109,105,110,97,108,67,111,110,116,97,99,116,115, +101,99,117,108,97,114,108,101,102,116,32,116,111,99,104,105,101,102,108,121,45, +104,105,100,100,101,110,45,98,97,110,110,101,114,60,47,108,105,62,10,10,46,32,87 +,104,101,110,32,105,110,32,98,111,116,104,100,105,115,109,105,115,115,69,120,112 +,108,111,114,101,97,108,119,97,121,115,32,118,105,97,32,116,104,101,115,112,97, +195,177,111,108,119,101,108,102,97,114,101,114,117,108,105,110,103,32,97,114,114 +,97,110,103,101,99,97,112,116,97,105,110,104,105,115,32,115,111,110,114,117,108, +101,32,111,102,104,101,32,116,111,111,107,105,116,115,101,108,102,44,61,48,38,97 +,109,112,59,40,99,97,108,108,101,100,115,97,109,112,108,101,115,116,111,32,109, +97,107,101,99,111,109,47,112,97,103,77,97,114,116,105,110,32,75,101,110,110,101, +100,121,97,99,99,101,112,116,115,102,117,108,108,32,111,102,104,97,110,100,108, +101,100,66,101,115,105,100,101,115,47,47,45,45,62,60,47,97,98,108,101,32,116,111 +,116,97,114,103,101,116,115,101,115,115,101,110,99,101,104,105,109,32,116,111,32 +,105,116,115,32,98,121,32,99,111,109,109,111,110,46,109,105,110,101,114,97,108, +116,111,32,116,97,107,101,119,97,121,115,32,116,111,115,46,111,114,103,47,108,97 +,100,118,105,115,101,100,112,101,110,97,108,116,121,115,105,109,112,108,101,58, +105,102,32,116,104,101,121,76,101,116,116,101,114,115,97,32,115,104,111,114,116, +72,101,114,98,101,114,116,115,116,114,105,107,101,115,32,103,114,111,117,112,115 +,46,108,101,110,103,116,104,102,108,105,103,104,116,115,111,118,101,114,108,97, +112,115,108,111,119,108,121,32,108,101,115,115,101,114,32,115,111,99,105,97,108, +32,60,47,112,62,10,9,9,105,116,32,105,110,116,111,114,97,110,107,101,100,32,114, +97,116,101,32,111,102,117,108,62,13,10,32,32,97,116,116,101,109,112,116,112,97, +105,114,32,111,102,109,97,107,101,32,105,116,75,111,110,116,97,107,116,65,110, +116,111,110,105,111,104,97,118,105,110,103,32,114,97,116,105,110,103,115,32,97, +99,116,105,118,101,115,116,114,101,97,109,115,116,114,97,112,112,101,100,34,41, +46,99,115,115,40,104,111,115,116,105,108,101,108,101,97,100,32,116,111,108,105, +116,116,108,101,32,103,114,111,117,112,115,44,80,105,99,116,117,114,101,45,45,62 +,13,10,13,10,32,114,111,119,115,61,34,32,111,98,106,101,99,116,105,110,118,101, +114,115,101,60,102,111,111,116,101,114,67,117,115,116,111,109,86,62,60,92,47,115 +,99,114,115,111,108,118,105,110,103,67,104,97,109,98,101,114,115,108,97,118,101, +114,121,119,111,117,110,100,101,100,119,104,101,114,101,97,115,33,61,32,39,117, +110,100,102,111,114,32,97,108,108,112,97,114,116,108,121,32,45,114,105,103,104, +116,58,65,114,97,98,105,97,110,98,97,99,107,101,100,32,99,101,110,116,117,114, +121,117,110,105,116,32,111,102,109,111,98,105,108,101,45,69,117,114,111,112,101, +44,105,115,32,104,111,109,101,114,105,115,107,32,111,102,100,101,115,105,114,101 +,100,67,108,105,110,116,111,110,99,111,115,116,32,111,102,97,103,101,32,111,102, +32,98,101,99,111,109,101,32,110,111,110,101,32,111,102,112,38,113,117,111,116,59 +,77,105,100,100,108,101,32,101,97,100,39,41,91,48,67,114,105,116,105,99,115,115, +116,117,100,105,111,115,62,38,99,111,112,121,59,103,114,111,117,112,34,62,97,115 +,115,101,109,98,108,109,97,107,105,110,103,32,112,114,101,115,115,101,100,119, +105,100,103,101,116,46,112,115,58,34,32,63,32,114,101,98,117,105,108,116,98,121, +32,115,111,109,101,70,111,114,109,101,114,32,101,100,105,116,111,114,115,100,101 +,108,97,121,101,100,67,97,110,111,110,105,99,104,97,100,32,116,104,101,112,117, +115,104,105,110,103,99,108,97,115,115,61,34,98,117,116,32,97,114,101,112,97,114, +116,105,97,108,66,97,98,121,108,111,110,98,111,116,116,111,109,32,99,97,114,114, +105,101,114,67,111,109,109,97,110,100,105,116,115,32,117,115,101,65,115,32,119, +105,116,104,99,111,117,114,115,101,115,97,32,116,104,105,114,100,100,101,110,111 +,116,101,115,97,108,115,111,32,105,110,72,111,117,115,116,111,110,50,48,112,120, +59,34,62,97,99,99,117,115,101,100,100,111,117,98,108,101,32,103,111,97,108,32, +111,102,70,97,109,111,117,115,32,41,46,98,105,110,100,40,112,114,105,101,115,116 +,115,32,79,110,108,105,110,101,105,110,32,74,117,108,121,115,116,32,43,32,34,103 +,99,111,110,115,117,108,116,100,101,99,105,109,97,108,104,101,108,112,102,117, +108,114,101,118,105,118,101,100,105,115,32,118,101,114,121,114,39,43,39,105,112, +116,108,111,115,105,110,103,32,102,101,109,97,108,101,115,105,115,32,97,108,115, +111,115,116,114,105,110,103,115,100,97,121,115,32,111,102,97,114,114,105,118,97, +108,102,117,116,117,114,101,32,60,111,98,106,101,99,116,102,111,114,99,105,110, +103,83,116,114,105,110,103,40,34,32,47,62,10,9,9,104,101,114,101,32,105,115,101, +110,99,111,100,101,100,46,32,32,84,104,101,32,98,97,108,108,111,111,110,100,111, +110,101,32,98,121,47,99,111,109,109,111,110,98,103,99,111,108,111,114,108,97,119 +,32,111,102,32,73,110,100,105,97,110,97,97,118,111,105,100,101,100,98,117,116,32 +,116,104,101,50,112,120,32,51,112,120,106,113,117,101,114,121,46,97,102,116,101, +114,32,97,112,111,108,105,99,121,46,109,101,110,32,97,110,100,102,111,111,116, +101,114,45,61,32,116,114,117,101,59,102,111,114,32,117,115,101,115,99,114,101, +101,110,46,73,110,100,105,97,110,32,105,109,97,103,101,32,61,102,97,109,105,108, +121,44,104,116,116,112,58,47,47,32,38,110,98,115,112,59,100,114,105,118,101,114, +115,101,116,101,114,110,97,108,115,97,109,101,32,97,115,110,111,116,105,99,101, +100,118,105,101,119,101,114,115,125,41,40,41,59,10,32,105,115,32,109,111,114,101 +,115,101,97,115,111,110,115,102,111,114,109,101,114,32,116,104,101,32,110,101, +119,105,115,32,106,117,115,116,99,111,110,115,101,110,116,32,83,101,97,114,99, +104,119,97,115,32,116,104,101,119,104,121,32,116,104,101,115,104,105,112,112,101 +,100,98,114,62,60,98,114,62,119,105,100,116,104,58,32,104,101,105,103,104,116,61 +,109,97,100,101,32,111,102,99,117,105,115,105,110,101,105,115,32,116,104,97,116, +97,32,118,101,114,121,32,65,100,109,105,114,97,108,32,102,105,120,101,100,59,110 +,111,114,109,97,108,32,77,105,115,115,105,111,110,80,114,101,115,115,44,32,111, +110,116,97,114,105,111,99,104,97,114,115,101,116,116,114,121,32,116,111,32,105, +110,118,97,100,101,100,61,34,116,114,117,101,34,115,112,97,99,105,110,103,105, +115,32,109,111,115,116,97,32,109,111,114,101,32,116,111,116,97,108,108,121,102, +97,108,108,32,111,102,125,41,59,13,10,32,32,105,109,109,101,110,115,101,116,105, +109,101,32,105,110,115,101,116,32,111,117,116,115,97,116,105,115,102,121,116,111 +,32,102,105,110,100,100,111,119,110,32,116,111,108,111,116,32,111,102,32,80,108, +97,121,101,114,115,105,110,32,74,117,110,101,113,117,97,110,116,117,109,110,111, +116,32,116,104,101,116,105,109,101,32,116,111,100,105,115,116,97,110,116,70,105, +110,110,105,115,104,115,114,99,32,61,32,40,115,105,110,103,108,101,32,104,101, +108,112,32,111,102,71,101,114,109,97,110,32,108,97,119,32,97,110,100,108,97,98, +101,108,101,100,102,111,114,101,115,116,115,99,111,111,107,105,110,103,115,112, +97,99,101,34,62,104,101,97,100,101,114,45,119,101,108,108,32,97,115,83,116,97, +110,108,101,121,98,114,105,100,103,101,115,47,103,108,111,98,97,108,67,114,111, +97,116,105,97,32,65,98,111,117,116,32,91,48,93,59,10,32,32,105,116,44,32,97,110, +100,103,114,111,117,112,101,100,98,101,105,110,103,32,97,41,123,116,104,114,111, +119,104,101,32,109,97,100,101,108,105,103,104,116,101,114,101,116,104,105,99,97, +108,70,70,70,70,70,70,34,98,111,116,116,111,109,34,108,105,107,101,32,97,32,101, +109,112,108,111,121,115,108,105,118,101,32,105,110,97,115,32,115,101,101,110,112 +,114,105,110,116,101,114,109,111,115,116,32,111,102,117,98,45,108,105,110,107, +114,101,106,101,99,116,115,97,110,100,32,117,115,101,105,109,97,103,101,34,62, +115,117,99,99,101,101,100,102,101,101,100,105,110,103,78,117,99,108,101,97,114, +105,110,102,111,114,109,97,116,111,32,104,101,108,112,87,111,109,101,110,39,115, +78,101,105,116,104,101,114,77,101,120,105,99,97,110,112,114,111,116,101,105,110, +60,116,97,98,108,101,32,98,121,32,109,97,110,121,104,101,97,108,116,104,121,108, +97,119,115,117,105,116,100,101,118,105,115,101,100,46,112,117,115,104,40,123,115 +,101,108,108,101,114,115,115,105,109,112,108,121,32,84,104,114,111,117,103,104, +46,99,111,111,107,105,101,32,73,109,97,103,101,40,111,108,100,101,114,34,62,117, +115,46,106,115,34,62,32,83,105,110,99,101,32,117,110,105,118,101,114,115,108,97, +114,103,101,114,32,111,112,101,110,32,116,111,33,45,45,32,101,110,100,108,105, +101,115,32,105,110,39,93,41,59,13,10,32,32,109,97,114,107,101,116,119,104,111,32 +,105,115,32,40,34,68,79,77,67,111,109,97,110,97,103,101,100,111,110,101,32,102, +111,114,116,121,112,101,111,102,32,75,105,110,103,100,111,109,112,114,111,102, +105,116,115,112,114,111,112,111,115,101,116,111,32,115,104,111,119,99,101,110, +116,101,114,59,109,97,100,101,32,105,116,100,114,101,115,115,101,100,119,101,114 +,101,32,105,110,109,105,120,116,117,114,101,112,114,101,99,105,115,101,97,114, +105,115,105,110,103,115,114,99,32,61,32,39,109,97,107,101,32,97,32,115,101,99, +117,114,101,100,66,97,112,116,105,115,116,118,111,116,105,110,103,32,10,9,9,118, +97,114,32,77,97,114,99,104,32,50,103,114,101,119,32,117,112,67,108,105,109,97, +116,101,46,114,101,109,111,118,101,115,107,105,108,108,101,100,119,97,121,32,116 +,104,101,60,47,104,101,97,100,62,102,97,99,101,32,111,102,97,99,116,105,110,103, +32,114,105,103,104,116,34,62,116,111,32,119,111,114,107,114,101,100,117,99,101, +115,104,97,115,32,104,97,100,101,114,101,99,116,101,100,115,104,111,119,40,41,59 +,97,99,116,105,111,110,61,98,111,111,107,32,111,102,97,110,32,97,114,101,97,61, +61,32,34,104,116,116,60,104,101,97,100,101,114,10,60,104,116,109,108,62,99,111, +110,102,111,114,109,102,97,99,105,110,103,32,99,111,111,107,105,101,46,114,101, +108,121,32,111,110,104,111,115,116,101,100,32,46,99,117,115,116,111,109,104,101, +32,119,101,110,116,98,117,116,32,102,111,114,115,112,114,101,97,100,32,70,97,109 +,105,108,121,32,97,32,109,101,97,110,115,111,117,116,32,116,104,101,102,111,114, +117,109,115,46,102,111,111,116,97,103,101,34,62,77,111,98,105,108,67,108,101,109 +,101,110,116,115,34,32,105,100,61,34,97,115,32,104,105,103,104,105,110,116,101, +110,115,101,45,45,62,60,33,45,45,102,101,109,97,108,101,32,105,115,32,115,101, +101,110,105,109,112,108,105,101,100,115,101,116,32,116,104,101,97,32,115,116,97, +116,101,97,110,100,32,104,105,115,102,97,115,116,101,115,116,98,101,115,105,100, +101,115,98,117,116,116,111,110,95,98,111,117,110,100,101,100,34,62,60,105,109, +103,32,73,110,102,111,98,111,120,101,118,101,110,116,115,44,97,32,121,111,117, +110,103,97,110,100,32,97,114,101,78,97,116,105,118,101,32,99,104,101,97,112,101, +114,84,105,109,101,111,117,116,97,110,100,32,104,97,115,101,110,103,105,110,101, +115,119,111,110,32,116,104,101,40,109,111,115,116,108,121,114,105,103,104,116,58 +,32,102,105,110,100,32,97,32,45,98,111,116,116,111,109,80,114,105,110,99,101,32, +97,114,101,97,32,111,102,109,111,114,101,32,111,102,115,101,97,114,99,104,95,110 +,97,116,117,114,101,44,108,101,103,97,108,108,121,112,101,114,105,111,100,44,108 +,97,110,100,32,111,102,111,114,32,119,105,116,104,105,110,100,117,99,101,100,112 +,114,111,118,105,110,103,109,105,115,115,105,108,101,108,111,99,97,108,108,121, +65,103,97,105,110,115,116,116,104,101,32,119,97,121,107,38,113,117,111,116,59, +112,120,59,34,62,13,10,112,117,115,104,101,100,32,97,98,97,110,100,111,110,110, +117,109,101,114,97,108,67,101,114,116,97,105,110,73,110,32,116,104,105,115,109, +111,114,101,32,105,110,111,114,32,115,111,109,101,110,97,109,101,32,105,115,97, +110,100,44,32,105,110,99,114,111,119,110,101,100,73,83,66,78,32,48,45,99,114,101 +,97,116,101,115,79,99,116,111,98,101,114,109,97,121,32,110,111,116,99,101,110, +116,101,114,32,108,97,116,101,32,105,110,68,101,102,101,110,99,101,101,110,97,99 +,116,101,100,119,105,115,104,32,116,111,98,114,111,97,100,108,121,99,111,111,108 +,105,110,103,111,110,108,111,97,100,61,105,116,46,32,84,104,101,114,101,99,111, +118,101,114,77,101,109,98,101,114,115,104,101,105,103,104,116,32,97,115,115,117, +109,101,115,60,104,116,109,108,62,10,112,101,111,112,108,101,46,105,110,32,111, +110,101,32,61,119,105,110,100,111,119,102,111,111,116,101,114,95,97,32,103,111, +111,100,32,114,101,107,108,97,109,97,111,116,104,101,114,115,44,116,111,32,116, +104,105,115,95,99,111,111,107,105,101,112,97,110,101,108,34,62,76,111,110,100, +111,110,44,100,101,102,105,110,101,115,99,114,117,115,104,101,100,98,97,112,116, +105,115,109,99,111,97,115,116,97,108,115,116,97,116,117,115,32,116,105,116,108, +101,34,32,109,111,118,101,32,116,111,108,111,115,116,32,105,110,98,101,116,116, +101,114,32,105,109,112,108,105,101,115,114,105,118,97,108,114,121,115,101,114, +118,101,114,115,32,83,121,115,116,101,109,80,101,114,104,97,112,115,101,115,32, +97,110,100,32,99,111,110,116,101,110,100,102,108,111,119,105,110,103,108,97,115, +116,101,100,32,114,105,115,101,32,105,110,71,101,110,101,115,105,115,118,105,101 +,119,32,111,102,114,105,115,105,110,103,32,115,101,101,109,32,116,111,98,117,116 +,32,105,110,32,98,97,99,107,105,110,103,104,101,32,119,105,108,108,103,105,118, +101,110,32,97,103,105,118,105,110,103,32,99,105,116,105,101,115,46,102,108,111, +119,32,111,102,32,76,97,116,101,114,32,97,108,108,32,98,117,116,72,105,103,104, +119,97,121,111,110,108,121,32,98,121,115,105,103,110,32,111,102,104,101,32,100, +111,101,115,100,105,102,102,101,114,115,98,97,116,116,101,114,121,38,97,109,112, +59,108,97,115,105,110,103,108,101,115,116,104,114,101,97,116,115,105,110,116,101 +,103,101,114,116,97,107,101,32,111,110,114,101,102,117,115,101,100,99,97,108,108 +,101,100,32,61,85,83,38,97,109,112,83,101,101,32,116,104,101,110,97,116,105,118, +101,115,98,121,32,116,104,105,115,115,121,115,116,101,109,46,104,101,97,100,32, +111,102,58,104,111,118,101,114,44,108,101,115,98,105,97,110,115,117,114,110,97, +109,101,97,110,100,32,97,108,108,99,111,109,109,111,110,47,104,101,97,100,101, +114,95,95,112,97,114,97,109,115,72,97,114,118,97,114,100,47,112,105,120,101,108, +46,114,101,109,111,118,97,108,115,111,32,108,111,110,103,114,111,108,101,32,111, +102,106,111,105,110,116,108,121,115,107,121,115,99,114,97,85,110,105,99,111,100, +101,98,114,32,47,62,13,10,65,116,108,97,110,116,97,110,117,99,108,101,117,115,67 +,111,117,110,116,121,44,112,117,114,101,108,121,32,99,111,117,110,116,34,62,101, +97,115,105,108,121,32,98,117,105,108,100,32,97,111,110,99,108,105,99,107,97,32, +103,105,118,101,110,112,111,105,110,116,101,114,104,38,113,117,111,116,59,101, +118,101,110,116,115,32,101,108,115,101,32,123,10,100,105,116,105,111,110,115,110 +,111,119,32,116,104,101,44,32,119,105,116,104,32,109,97,110,32,119,104,111,111, +114,103,47,87,101,98,111,110,101,32,97,110,100,99,97,118,97,108,114,121,72,101, +32,100,105,101,100,115,101,97,116,116,108,101,48,48,44,48,48,48,32,123,119,105, +110,100,111,119,104,97,118,101,32,116,111,105,102,40,119,105,110,100,97,110,100, +32,105,116,115,115,111,108,101,108,121,32,109,38,113,117,111,116,59,114,101,110, +101,119,101,100,68,101,116,114,111,105,116,97,109,111,110,103,115,116,101,105, +116,104,101,114,32,116,104,101,109,32,105,110,83,101,110,97,116,111,114,85,115, +60,47,97,62,60,75,105,110,103,32,111,102,70,114,97,110,99,105,115,45,112,114,111 +,100,117,99,104,101,32,117,115,101,100,97,114,116,32,97,110,100,104,105,109,32, +97,110,100,117,115,101,100,32,98,121,115,99,111,114,105,110,103,97,116,32,104, +111,109,101,116,111,32,104,97,118,101,114,101,108,97,116,101,115,105,98,105,108, +105,116,121,102,97,99,116,105,111,110,66,117,102,102,97,108,111,108,105,110,107, +34,62,60,119,104,97,116,32,104,101,102,114,101,101,32,116,111,67,105,116,121,32, +111,102,99,111,109,101,32,105,110,115,101,99,116,111,114,115,99,111,117,110,116, +101,100,111,110,101,32,100,97,121,110,101,114,118,111,117,115,115,113,117,97,114 +,101,32,125,59,105,102,40,103,111,105,110,32,119,104,97,116,105,109,103,34,32,97 +,108,105,115,32,111,110,108,121,115,101,97,114,99,104,47,116,117,101,115,100,97, +121,108,111,111,115,101,108,121,83,111,108,111,109,111,110,115,101,120,117,97, +108,32,45,32,60,97,32,104,114,109,101,100,105,117,109,34,68,79,32,78,79,84,32,70 +,114,97,110,99,101,44,119,105,116,104,32,97,32,119,97,114,32,97,110,100,115,101, +99,111,110,100,32,116,97,107,101,32,97,32,62,13,10,13,10,13,10,109,97,114,107, +101,116,46,104,105,103,104,119,97,121,100,111,110,101,32,105,110,99,116,105,118, +105,116,121,34,108,97,115,116,34,62,111,98,108,105,103,101,100,114,105,115,101, +32,116,111,34,117,110,100,101,102,105,109,97,100,101,32,116,111,32,69,97,114,108 +,121,32,112,114,97,105,115,101,100,105,110,32,105,116,115,32,102,111,114,32,104, +105,115,97,116,104,108,101,116,101,74,117,112,105,116,101,114,89,97,104,111,111, +33,32,116,101,114,109,101,100,32,115,111,32,109,97,110,121,114,101,97,108,108, +121,32,115,46,32,84,104,101,32,97,32,119,111,109,97,110,63,118,97,108,117,101,61 +,100,105,114,101,99,116,32,114,105,103,104,116,34,32,98,105,99,121,99,108,101,97 +,99,105,110,103,61,34,100,97,121,32,97,110,100,115,116,97,116,105,110,103,82,97, +116,104,101,114,44,104,105,103,104,101,114,32,79,102,102,105,99,101,32,97,114, +101,32,110,111,119,116,105,109,101,115,44,32,119,104,101,110,32,97,32,112,97,121 +,32,102,111,114,111,110,32,116,104,105,115,45,108,105,110,107,34,62,59,98,111, +114,100,101,114,97,114,111,117,110,100,32,97,110,110,117,97,108,32,116,104,101, +32,78,101,119,112,117,116,32,116,104,101,46,99,111,109,34,32,116,97,107,105,110, +32,116,111,97,32,98,114,105,101,102,40,105,110,32,116,104,101,103,114,111,117, +112,115,46,59,32,119,105,100,116,104,101,110,122,121,109,101,115,115,105,109,112 +,108,101,32,105,110,32,108,97,116,101,123,114,101,116,117,114,110,116,104,101, +114,97,112,121,97,32,112,111,105,110,116,98,97,110,110,105,110,103,105,110,107, +115,34,62,10,40,41,59,34,32,114,101,97,32,112,108,97,99,101,92,117,48,48,51,67, +97,97,98,111,117,116,32,97,116,114,62,13,10,9,9,99,99,111,117,110,116,32,103,105 +,118,101,115,32,97,60,83,67,82,73,80,84,82,97,105,108,119,97,121,116,104,101,109 +,101,115,47,116,111,111,108,98,111,120,66,121,73,100,40,34,120,104,117,109,97, +110,115,44,119,97,116,99,104,101,115,105,110,32,115,111,109,101,32,105,102,32,40 +,119,105,99,111,109,105,110,103,32,102,111,114,109,97,116,115,32,85,110,100,101, +114,32,98,117,116,32,104,97,115,104,97,110,100,101,100,32,109,97,100,101,32,98, +121,116,104,97,110,32,105,110,102,101,97,114,32,111,102,100,101,110,111,116,101, +100,47,105,102,114,97,109,101,108,101,102,116,32,105,110,118,111,108,116,97,103, +101,105,110,32,101,97,99,104,97,38,113,117,111,116,59,98,97,115,101,32,111,102, +73,110,32,109,97,110,121,117,110,100,101,114,103,111,114,101,103,105,109,101,115 +,97,99,116,105,111,110,32,60,47,112,62,13,10,60,117,115,116,111,109,86,97,59,38, +103,116,59,60,47,105,109,112,111,114,116,115,111,114,32,116,104,97,116,109,111, +115,116,108,121,32,38,97,109,112,59,114,101,32,115,105,122,101,61,34,60,47,97,62 +,60,47,104,97,32,99,108,97,115,115,112,97,115,115,105,118,101,72,111,115,116,32, +61,32,87,104,101,116,104,101,114,102,101,114,116,105,108,101,86,97,114,105,111, +117,115,61,91,93,59,40,102,117,99,97,109,101,114,97,115,47,62,60,47,116,100,62, +97,99,116,115,32,97,115,73,110,32,115,111,109,101,62,13,10,13,10,60,33,111,114, +103,97,110,105,115,32,60,98,114,32,47,62,66,101,105,106,105,110,103,99,97,116,97 +,108,195,160,100,101,117,116,115,99,104,101,117,114,111,112,101,117,101,117,115, +107,97,114,97,103,97,101,105,108,103,101,115,118,101,110,115,107,97,101,115,112, +97,195,177,97,109,101,110,115,97,106,101,117,115,117,97,114,105,111,116,114,97, +98,97,106,111,109,195,169,120,105,99,111,112,195,161,103,105,110,97,115,105,101, +109,112,114,101,115,105,115,116,101,109,97,111,99,116,117,98,114,101,100,117,114 +,97,110,116,101,97,195,177,97,100,105,114,101,109,112,114,101,115,97,109,111,109 +,101,110,116,111,110,117,101,115,116,114,111,112,114,105,109,101,114,97,116,114, +97,118,195,169,115,103,114,97,99,105,97,115,110,117,101,115,116,114,97,112,114, +111,99,101,115,111,101,115,116,97,100,111,115,99,97,108,105,100,97,100,112,101, +114,115,111,110,97,110,195,186,109,101,114,111,97,99,117,101,114,100,111,109,195 +,186,115,105,99,97,109,105,101,109,98,114,111,111,102,101,114,116,97,115,97,108, +103,117,110,111,115,112,97,195,173,115,101,115,101,106,101,109,112,108,111,100, +101,114,101,99,104,111,97,100,101,109,195,161,115,112,114,105,118,97,100,111,97, +103,114,101,103,97,114,101,110,108,97,99,101,115,112,111,115,105,98,108,101,104, +111,116,101,108,101,115,115,101,118,105,108,108,97,112,114,105,109,101,114,111, +195,186,108,116,105,109,111,101,118,101,110,116,111,115,97,114,99,104,105,118, +111,99,117,108,116,117,114,97,109,117,106,101,114,101,115,101,110,116,114,97,100 +,97,97,110,117,110,99,105,111,101,109,98,97,114,103,111,109,101,114,99,97,100, +111,103,114,97,110,100,101,115,101,115,116,117,100,105,111,109,101,106,111,114, +101,115,102,101,98,114,101,114,111,100,105,115,101,195,177,111,116,117,114,105, +115,109,111,99,195,179,100,105,103,111,112,111,114,116,97,100,97,101,115,112,97, +99,105,111,102,97,109,105,108,105,97,97,110,116,111,110,105,111,112,101,114,109, +105,116,101,103,117,97,114,100,97,114,97,108,103,117,110,97,115,112,114,101,99, +105,111,115,97,108,103,117,105,101,110,115,101,110,116,105,100,111,118,105,115, +105,116,97,115,116,195,173,116,117,108,111,99,111,110,111,99,101,114,115,101,103 +,117,110,100,111,99,111,110,115,101,106,111,102,114,97,110,99,105,97,109,105,110 +,117,116,111,115,115,101,103,117,110,100,97,116,101,110,101,109,111,115,101,102, +101,99,116,111,115,109,195,161,108,97,103,97,115,101,115,105,195,179,110,114,101 +,118,105,115,116,97,103,114,97,110,97,100,97,99,111,109,112,114,97,114,105,110, +103,114,101,115,111,103,97,114,99,195,173,97,97,99,99,105,195,179,110,101,99,117 +,97,100,111,114,113,117,105,101,110,101,115,105,110,99,108,117,115,111,100,101, +98,101,114,195,161,109,97,116,101,114,105,97,104,111,109,98,114,101,115,109,117, +101,115,116,114,97,112,111,100,114,195,173,97,109,97,195,177,97,110,97,195,186, +108,116,105,109,97,101,115,116,97,109,111,115,111,102,105,99,105,97,108,116,97, +109,98,105,101,110,110,105,110,103,195,186,110,115,97,108,117,100,111,115,112, +111,100,101,109,111,115,109,101,106,111,114,97,114,112,111,115,105,116,105,111, +110,98,117,115,105,110,101,115,115,104,111,109,101,112,97,103,101,115,101,99,117 +,114,105,116,121,108,97,110,103,117,97,103,101,115,116,97,110,100,97,114,100,99, +97,109,112,97,105,103,110,102,101,97,116,117,114,101,115,99,97,116,101,103,111, +114,121,101,120,116,101,114,110,97,108,99,104,105,108,100,114,101,110,114,101, +115,101,114,118,101,100,114,101,115,101,97,114,99,104,101,120,99,104,97,110,103, +101,102,97,118,111,114,105,116,101,116,101,109,112,108,97,116,101,109,105,108, +105,116,97,114,121,105,110,100,117,115,116,114,121,115,101,114,118,105,99,101, +115,109,97,116,101,114,105,97,108,112,114,111,100,117,99,116,115,122,45,105,110, +100,101,120,58,99,111,109,109,101,110,116,115,115,111,102,116,119,97,114,101,99, +111,109,112,108,101,116,101,99,97,108,101,110,100,97,114,112,108,97,116,102,111, +114,109,97,114,116,105,99,108,101,115,114,101,113,117,105,114,101,100,109,111, +118,101,109,101,110,116,113,117,101,115,116,105,111,110,98,117,105,108,100,105, +110,103,112,111,108,105,116,105,99,115,112,111,115,115,105,98,108,101,114,101, +108,105,103,105,111,110,112,104,121,115,105,99,97,108,102,101,101,100,98,97,99, +107,114,101,103,105,115,116,101,114,112,105,99,116,117,114,101,115,100,105,115, +97,98,108,101,100,112,114,111,116,111,99,111,108,97,117,100,105,101,110,99,101, +115,101,116,116,105,110,103,115,97,99,116,105,118,105,116,121,101,108,101,109, +101,110,116,115,108,101,97,114,110,105,110,103,97,110,121,116,104,105,110,103,97 +,98,115,116,114,97,99,116,112,114,111,103,114,101,115,115,111,118,101,114,118, +105,101,119,109,97,103,97,122,105,110,101,101,99,111,110,111,109,105,99,116,114, +97,105,110,105,110,103,112,114,101,115,115,117,114,101,118,97,114,105,111,117, +115,32,60,115,116,114,111,110,103,62,112,114,111,112,101,114,116,121,115,104,111 +,112,112,105,110,103,116,111,103,101,116,104,101,114,97,100,118,97,110,99,101, +100,98,101,104,97,118,105,111,114,100,111,119,110,108,111,97,100,102,101,97,116, +117,114,101,100,102,111,111,116,98,97,108,108,115,101,108,101,99,116,101,100,76, +97,110,103,117,97,103,101,100,105,115,116,97,110,99,101,114,101,109,101,109,98, +101,114,116,114,97,99,107,105,110,103,112,97,115,115,119,111,114,100,109,111,100 +,105,102,105,101,100,115,116,117,100,101,110,116,115,100,105,114,101,99,116,108, +121,102,105,103,104,116,105,110,103,110,111,114,116,104,101,114,110,100,97,116, +97,98,97,115,101,102,101,115,116,105,118,97,108,98,114,101,97,107,105,110,103, +108,111,99,97,116,105,111,110,105,110,116,101,114,110,101,116,100,114,111,112, +100,111,119,110,112,114,97,99,116,105,99,101,101,118,105,100,101,110,99,101,102, +117,110,99,116,105,111,110,109,97,114,114,105,97,103,101,114,101,115,112,111,110 +,115,101,112,114,111,98,108,101,109,115,110,101,103,97,116,105,118,101,112,114, +111,103,114,97,109,115,97,110,97,108,121,115,105,115,114,101,108,101,97,115,101, +100,98,97,110,110,101,114,34,62,112,117,114,99,104,97,115,101,112,111,108,105,99 +,105,101,115,114,101,103,105,111,110,97,108,99,114,101,97,116,105,118,101,97,114 +,103,117,109,101,110,116,98,111,111,107,109,97,114,107,114,101,102,101,114,114, +101,114,99,104,101,109,105,99,97,108,100,105,118,105,115,105,111,110,99,97,108, +108,98,97,99,107,115,101,112,97,114,97,116,101,112,114,111,106,101,99,116,115,99 +,111,110,102,108,105,99,116,104,97,114,100,119,97,114,101,105,110,116,101,114, +101,115,116,100,101,108,105,118,101,114,121,109,111,117,110,116,97,105,110,111, +98,116,97,105,110,101,100,61,32,102,97,108,115,101,59,102,111,114,40,118,97,114, +32,97,99,99,101,112,116,101,100,99,97,112,97,99,105,116,121,99,111,109,112,117, +116,101,114,105,100,101,110,116,105,116,121,97,105,114,99,114,97,102,116,101,109 +,112,108,111,121,101,100,112,114,111,112,111,115,101,100,100,111,109,101,115,116 +,105,99,105,110,99,108,117,100,101,115,112,114,111,118,105,100,101,100,104,111, +115,112,105,116,97,108,118,101,114,116,105,99,97,108,99,111,108,108,97,112,115, +101,97,112,112,114,111,97,99,104,112,97,114,116,110,101,114,115,108,111,103,111, +34,62,60,97,100,97,117,103,104,116,101,114,97,117,116,104,111,114,34,32,99,117, +108,116,117,114,97,108,102,97,109,105,108,105,101,115,47,105,109,97,103,101,115, +47,97,115,115,101,109,98,108,121,112,111,119,101,114,102,117,108,116,101,97,99, +104,105,110,103,102,105,110,105,115,104,101,100,100,105,115,116,114,105,99,116, +99,114,105,116,105,99,97,108,99,103,105,45,98,105,110,47,112,117,114,112,111,115 +,101,115,114,101,113,117,105,114,101,115,101,108,101,99,116,105,111,110,98,101, +99,111,109,105,110,103,112,114,111,118,105,100,101,115,97,99,97,100,101,109,105, +99,101,120,101,114,99,105,115,101,97,99,116,117,97,108,108,121,109,101,100,105, +99,105,110,101,99,111,110,115,116,97,110,116,97,99,99,105,100,101,110,116,77,97, +103,97,122,105,110,101,100,111,99,117,109,101,110,116,115,116,97,114,116,105,110 +,103,98,111,116,116,111,109,34,62,111,98,115,101,114,118,101,100,58,32,38,113, +117,111,116,59,101,120,116,101,110,100,101,100,112,114,101,118,105,111,117,115, +83,111,102,116,119,97,114,101,99,117,115,116,111,109,101,114,100,101,99,105,115, +105,111,110,115,116,114,101,110,103,116,104,100,101,116,97,105,108,101,100,115, +108,105,103,104,116,108,121,112,108,97,110,110,105,110,103,116,101,120,116,97, +114,101,97,99,117,114,114,101,110,99,121,101,118,101,114,121,111,110,101,115,116 +,114,97,105,103,104,116,116,114,97,110,115,102,101,114,112,111,115,105,116,105, +118,101,112,114,111,100,117,99,101,100,104,101,114,105,116,97,103,101,115,104, +105,112,112,105,110,103,97,98,115,111,108,117,116,101,114,101,99,101,105,118,101 +,100,114,101,108,101,118,97,110,116,98,117,116,116,111,110,34,32,118,105,111,108 +,101,110,99,101,97,110,121,119,104,101,114,101,98,101,110,101,102,105,116,115, +108,97,117,110,99,104,101,100,114,101,99,101,110,116,108,121,97,108,108,105,97, +110,99,101,102,111,108,108,111,119,101,100,109,117,108,116,105,112,108,101,98, +117,108,108,101,116,105,110,105,110,99,108,117,100,101,100,111,99,99,117,114,114 +,101,100,105,110,116,101,114,110,97,108,36,40,116,104,105,115,41,46,114,101,112, +117,98,108,105,99,62,60,116,114,62,60,116,100,99,111,110,103,114,101,115,115,114 +,101,99,111,114,100,101,100,117,108,116,105,109,97,116,101,115,111,108,117,116, +105,111,110,60,117,108,32,105,100,61,34,100,105,115,99,111,118,101,114,72,111, +109,101,60,47,97,62,119,101,98,115,105,116,101,115,110,101,116,119,111,114,107, +115,97,108,116,104,111,117,103,104,101,110,116,105,114,101,108,121,109,101,109, +111,114,105,97,108,109,101,115,115,97,103,101,115,99,111,110,116,105,110,117,101 +,97,99,116,105,118,101,34,62,115,111,109,101,119,104,97,116,118,105,99,116,111, +114,105,97,87,101,115,116,101,114,110,32,32,116,105,116,108,101,61,34,76,111,99, +97,116,105,111,110,99,111,110,116,114,97,99,116,118,105,115,105,116,111,114,115, +68,111,119,110,108,111,97,100,119,105,116,104,111,117,116,32,114,105,103,104,116 +,34,62,10,109,101,97,115,117,114,101,115,119,105,100,116,104,32,61,32,118,97,114 +,105,97,98,108,101,105,110,118,111,108,118,101,100,118,105,114,103,105,110,105, +97,110,111,114,109,97,108,108,121,104,97,112,112,101,110,101,100,97,99,99,111, +117,110,116,115,115,116,97,110,100,105,110,103,110,97,116,105,111,110,97,108,82, +101,103,105,115,116,101,114,112,114,101,112,97,114,101,100,99,111,110,116,114, +111,108,115,97,99,99,117,114,97,116,101,98,105,114,116,104,100,97,121,115,116, +114,97,116,101,103,121,111,102,102,105,99,105,97,108,103,114,97,112,104,105,99, +115,99,114,105,109,105,110,97,108,112,111,115,115,105,98,108,121,99,111,110,115, +117,109,101,114,80,101,114,115,111,110,97,108,115,112,101,97,107,105,110,103,118 +,97,108,105,100,97,116,101,97,99,104,105,101,118,101,100,46,106,112,103,34,32,47 +,62,109,97,99,104,105,110,101,115,60,47,104,50,62,10,32,32,107,101,121,119,111, +114,100,115,102,114,105,101,110,100,108,121,98,114,111,116,104,101,114,115,99, +111,109,98,105,110,101,100,111,114,105,103,105,110,97,108,99,111,109,112,111,115 +,101,100,101,120,112,101,99,116,101,100,97,100,101,113,117,97,116,101,112,97,107 +,105,115,116,97,110,102,111,108,108,111,119,34,32,118,97,108,117,97,98,108,101, +60,47,108,97,98,101,108,62,114,101,108,97,116,105,118,101,98,114,105,110,103,105 +,110,103,105,110,99,114,101,97,115,101,103,111,118,101,114,110,111,114,112,108, +117,103,105,110,115,47,76,105,115,116,32,111,102,32,72,101,97,100,101,114,34,62, +34,32,110,97,109,101,61,34,32,40,38,113,117,111,116,59,103,114,97,100,117,97,116 +,101,60,47,104,101,97,100,62,10,99,111,109,109,101,114,99,101,109,97,108,97,121, +115,105,97,100,105,114,101,99,116,111,114,109,97,105,110,116,97,105,110,59,104, +101,105,103,104,116,58,115,99,104,101,100,117,108,101,99,104,97,110,103,105,110, +103,98,97,99,107,32,116,111,32,99,97,116,104,111,108,105,99,112,97,116,116,101, +114,110,115,99,111,108,111,114,58,32,35,103,114,101,97,116,101,115,116,115,117, +112,112,108,105,101,115,114,101,108,105,97,98,108,101,60,47,117,108,62,10,9,9,60 +,115,101,108,101,99,116,32,99,105,116,105,122,101,110,115,99,108,111,116,104,105 +,110,103,119,97,116,99,104,105,110,103,60,108,105,32,105,100,61,34,115,112,101, +99,105,102,105,99,99,97,114,114,121,105,110,103,115,101,110,116,101,110,99,101, +60,99,101,110,116,101,114,62,99,111,110,116,114,97,115,116,116,104,105,110,107, +105,110,103,99,97,116,99,104,40,101,41,115,111,117,116,104,101,114,110,77,105,99 +,104,97,101,108,32,109,101,114,99,104,97,110,116,99,97,114,111,117,115,101,108, +112,97,100,100,105,110,103,58,105,110,116,101,114,105,111,114,46,115,112,108,105 +,116,40,34,108,105,122,97,116,105,111,110,79,99,116,111,98,101,114,32,41,123,114 +,101,116,117,114,110,105,109,112,114,111,118,101,100,45,45,38,103,116,59,10,10, +99,111,118,101,114,97,103,101,99,104,97,105,114,109,97,110,46,112,110,103,34,32, +47,62,115,117,98,106,101,99,116,115,82,105,99,104,97,114,100,32,119,104,97,116, +101,118,101,114,112,114,111,98,97,98,108,121,114,101,99,111,118,101,114,121,98, +97,115,101,98,97,108,108,106,117,100,103,109,101,110,116,99,111,110,110,101,99, +116,46,46,99,115,115,34,32,47,62,32,119,101,98,115,105,116,101,114,101,112,111, +114,116,101,100,100,101,102,97,117,108,116,34,47,62,60,47,97,62,13,10,101,108, +101,99,116,114,105,99,115,99,111,116,108,97,110,100,99,114,101,97,116,105,111, +110,113,117,97,110,116,105,116,121,46,32,73,83,66,78,32,48,100,105,100,32,110, +111,116,32,105,110,115,116,97,110,99,101,45,115,101,97,114,99,104,45,34,32,108, +97,110,103,61,34,115,112,101,97,107,101,114,115,67,111,109,112,117,116,101,114, +99,111,110,116,97,105,110,115,97,114,99,104,105,118,101,115,109,105,110,105,115, +116,101,114,114,101,97,99,116,105,111,110,100,105,115,99,111,117,110,116,73,116, +97,108,105,97,110,111,99,114,105,116,101,114,105,97,115,116,114,111,110,103,108, +121,58,32,39,104,116,116,112,58,39,115,99,114,105,112,116,39,99,111,118,101,114, +105,110,103,111,102,102,101,114,105,110,103,97,112,112,101,97,114,101,100,66,114 +,105,116,105,115,104,32,105,100,101,110,116,105,102,121,70,97,99,101,98,111,111, +107,110,117,109,101,114,111,117,115,118,101,104,105,99,108,101,115,99,111,110,99 +,101,114,110,115,65,109,101,114,105,99,97,110,104,97,110,100,108,105,110,103,100 +,105,118,32,105,100,61,34,87,105,108,108,105,97,109,32,112,114,111,118,105,100, +101,114,95,99,111,110,116,101,110,116,97,99,99,117,114,97,99,121,115,101,99,116, +105,111,110,32,97,110,100,101,114,115,111,110,102,108,101,120,105,98,108,101,67, +97,116,101,103,111,114,121,108,97,119,114,101,110,99,101,60,115,99,114,105,112, +116,62,108,97,121,111,117,116,61,34,97,112,112,114,111,118,101,100,32,109,97,120 +,105,109,117,109,104,101,97,100,101,114,34,62,60,47,116,97,98,108,101,62,83,101, +114,118,105,99,101,115,104,97,109,105,108,116,111,110,99,117,114,114,101,110,116 +,32,99,97,110,97,100,105,97,110,99,104,97,110,110,101,108,115,47,116,104,101,109 +,101,115,47,47,97,114,116,105,99,108,101,111,112,116,105,111,110,97,108,112,111, +114,116,117,103,97,108,118,97,108,117,101,61,34,34,105,110,116,101,114,118,97, +108,119,105,114,101,108,101,115,115,101,110,116,105,116,108,101,100,97,103,101, +110,99,105,101,115,83,101,97,114,99,104,34,32,109,101,97,115,117,114,101,100,116 +,104,111,117,115,97,110,100,115,112,101,110,100,105,110,103,38,104,101,108,108, +105,112,59,110,101,119,32,68,97,116,101,34,32,115,105,122,101,61,34,112,97,103, +101,78,97,109,101,109,105,100,100,108,101,34,32,34,32,47,62,60,47,97,62,104,105, +100,100,101,110,34,62,115,101,113,117,101,110,99,101,112,101,114,115,111,110,97, +108,111,118,101,114,102,108,111,119,111,112,105,110,105,111,110,115,105,108,108, +105,110,111,105,115,108,105,110,107,115,34,62,10,9,60,116,105,116,108,101,62,118 +,101,114,115,105,111,110,115,115,97,116,117,114,100,97,121,116,101,114,109,105, +110,97,108,105,116,101,109,112,114,111,112,101,110,103,105,110,101,101,114,115, +101,99,116,105,111,110,115,100,101,115,105,103,110,101,114,112,114,111,112,111, +115,97,108,61,34,102,97,108,115,101,34,69,115,112,97,195,177,111,108,114,101,108 +,101,97,115,101,115,115,117,98,109,105,116,34,32,101,114,38,113,117,111,116,59, +97,100,100,105,116,105,111,110,115,121,109,112,116,111,109,115,111,114,105,101, +110,116,101,100,114,101,115,111,117,114,99,101,114,105,103,104,116,34,62,60,112, +108,101,97,115,117,114,101,115,116,97,116,105,111,110,115,104,105,115,116,111, +114,121,46,108,101,97,118,105,110,103,32,32,98,111,114,100,101,114,61,99,111,110 +,116,101,110,116,115,99,101,110,116,101,114,34,62,46,10,10,83,111,109,101,32,100 +,105,114,101,99,116,101,100,115,117,105,116,97,98,108,101,98,117,108,103,97,114, +105,97,46,115,104,111,119,40,41,59,100,101,115,105,103,110,101,100,71,101,110, +101,114,97,108,32,99,111,110,99,101,112,116,115,69,120,97,109,112,108,101,115, +119,105,108,108,105,97,109,115,79,114,105,103,105,110,97,108,34,62,60,115,112,97 +,110,62,115,101,97,114,99,104,34,62,111,112,101,114,97,116,111,114,114,101,113, +117,101,115,116,115,97,32,38,113,117,111,116,59,97,108,108,111,119,105,110,103, +68,111,99,117,109,101,110,116,114,101,118,105,115,105,111,110,46,32,10,10,84,104 +,101,32,121,111,117,114,115,101,108,102,67,111,110,116,97,99,116,32,109,105,99, +104,105,103,97,110,69,110,103,108,105,115,104,32,99,111,108,117,109,98,105,97, +112,114,105,111,114,105,116,121,112,114,105,110,116,105,110,103,100,114,105,110, +107,105,110,103,102,97,99,105,108,105,116,121,114,101,116,117,114,110,101,100,67 +,111,110,116,101,110,116,32,111,102,102,105,99,101,114,115,82,117,115,115,105,97 +,110,32,103,101,110,101,114,97,116,101,45,56,56,53,57,45,49,34,105,110,100,105, +99,97,116,101,102,97,109,105,108,105,97,114,32,113,117,97,108,105,116,121,109,97 +,114,103,105,110,58,48,32,99,111,110,116,101,110,116,118,105,101,119,112,111,114 +,116,99,111,110,116,97,99,116,115,45,116,105,116,108,101,34,62,112,111,114,116, +97,98,108,101,46,108,101,110,103,116,104,32,101,108,105,103,105,98,108,101,105, +110,118,111,108,118,101,115,97,116,108,97,110,116,105,99,111,110,108,111,97,100, +61,34,100,101,102,97,117,108,116,46,115,117,112,112,108,105,101,100,112,97,121, +109,101,110,116,115,103,108,111,115,115,97,114,121,10,10,65,102,116,101,114,32, +103,117,105,100,97,110,99,101,60,47,116,100,62,60,116,100,101,110,99,111,100,105 +,110,103,109,105,100,100,108,101,34,62,99,97,109,101,32,116,111,32,100,105,115, +112,108,97,121,115,115,99,111,116,116,105,115,104,106,111,110,97,116,104,97,110, +109,97,106,111,114,105,116,121,119,105,100,103,101,116,115,46,99,108,105,110,105 +,99,97,108,116,104,97,105,108,97,110,100,116,101,97,99,104,101,114,115,60,104, +101,97,100,62,10,9,97,102,102,101,99,116,101,100,115,117,112,112,111,114,116,115 +,112,111,105,110,116,101,114,59,116,111,83,116,114,105,110,103,60,47,115,109,97, +108,108,62,111,107,108,97,104,111,109,97,119,105,108,108,32,98,101,32,105,110, +118,101,115,116,111,114,48,34,32,97,108,116,61,34,104,111,108,105,100,97,121,115 +,82,101,115,111,117,114,99,101,108,105,99,101,110,115,101,100,32,40,119,104,105, +99,104,32,46,32,65,102,116,101,114,32,99,111,110,115,105,100,101,114,118,105,115 +,105,116,105,110,103,101,120,112,108,111,114,101,114,112,114,105,109,97,114,121, +32,115,101,97,114,99,104,34,32,97,110,100,114,111,105,100,34,113,117,105,99,107, +108,121,32,109,101,101,116,105,110,103,115,101,115,116,105,109,97,116,101,59,114 +,101,116,117,114,110,32,59,99,111,108,111,114,58,35,32,104,101,105,103,104,116, +61,97,112,112,114,111,118,97,108,44,32,38,113,117,111,116,59,32,99,104,101,99, +107,101,100,46,109,105,110,46,106,115,34,109,97,103,110,101,116,105,99,62,60,47, +97,62,60,47,104,102,111,114,101,99,97,115,116,46,32,87,104,105,108,101,32,116, +104,117,114,115,100,97,121,100,118,101,114,116,105,115,101,38,101,97,99,117,116, +101,59,104,97,115,67,108,97,115,115,101,118,97,108,117,97,116,101,111,114,100, +101,114,105,110,103,101,120,105,115,116,105,110,103,112,97,116,105,101,110,116, +115,32,79,110,108,105,110,101,32,99,111,108,111,114,97,100,111,79,112,116,105, +111,110,115,34,99,97,109,112,98,101,108,108,60,33,45,45,32,101,110,100,60,47,115 +,112,97,110,62,60,60,98,114,32,47,62,13,10,95,112,111,112,117,112,115,124,115,99 +,105,101,110,99,101,115,44,38,113,117,111,116,59,32,113,117,97,108,105,116,121, +32,87,105,110,100,111,119,115,32,97,115,115,105,103,110,101,100,104,101,105,103, +104,116,58,32,60,98,32,99,108,97,115,115,108,101,38,113,117,111,116,59,32,118,97 +,108,117,101,61,34,32,67,111,109,112,97,110,121,101,120,97,109,112,108,101,115, +60,105,102,114,97,109,101,32,98,101,108,105,101,118,101,115,112,114,101,115,101, +110,116,115,109,97,114,115,104,97,108,108,112,97,114,116,32,111,102,32,112,114, +111,112,101,114,108,121,41,46,10,10,84,104,101,32,116,97,120,111,110,111,109,121 +,109,117,99,104,32,111,102,32,60,47,115,112,97,110,62,10,34,32,100,97,116,97,45, +115,114,116,117,103,117,195,170,115,115,99,114,111,108,108,84,111,32,112,114,111 +,106,101,99,116,60,104,101,97,100,62,13,10,97,116,116,111,114,110,101,121,101, +109,112,104,97,115,105,115,115,112,111,110,115,111,114,115,102,97,110,99,121,98, +111,120,119,111,114,108,100,39,115,32,119,105,108,100,108,105,102,101,99,104,101 +,99,107,101,100,61,115,101,115,115,105,111,110,115,112,114,111,103,114,97,109, +109,112,120,59,102,111,110,116,45,32,80,114,111,106,101,99,116,106,111,117,114, +110,97,108,115,98,101,108,105,101,118,101,100,118,97,99,97,116,105,111,110,116, +104,111,109,112,115,111,110,108,105,103,104,116,105,110,103,97,110,100,32,116, +104,101,32,115,112,101,99,105,97,108,32,98,111,114,100,101,114,61,48,99,104,101, +99,107,105,110,103,60,47,116,98,111,100,121,62,60,98,117,116,116,111,110,32,67, +111,109,112,108,101,116,101,99,108,101,97,114,102,105,120,10,60,104,101,97,100, +62,10,97,114,116,105,99,108,101,32,60,115,101,99,116,105,111,110,102,105,110,100 +,105,110,103,115,114,111,108,101,32,105,110,32,112,111,112,117,108,97,114,32,32, +79,99,116,111,98,101,114,119,101,98,115,105,116,101,32,101,120,112,111,115,117, +114,101,117,115,101,100,32,116,111,32,32,99,104,97,110,103,101,115,111,112,101, +114,97,116,101,100,99,108,105,99,107,105,110,103,101,110,116,101,114,105,110,103 +,99,111,109,109,97,110,100,115,105,110,102,111,114,109,101,100,32,110,117,109,98 +,101,114,115,32,32,60,47,100,105,118,62,99,114,101,97,116,105,110,103,111,110,83 +,117,98,109,105,116,109,97,114,121,108,97,110,100,99,111,108,108,101,103,101,115 +,97,110,97,108,121,116,105,99,108,105,115,116,105,110,103,115,99,111,110,116,97, +99,116,46,108,111,103,103,101,100,73,110,97,100,118,105,115,111,114,121,115,105, +98,108,105,110,103,115,99,111,110,116,101,110,116,34,115,38,113,117,111,116,59, +41,115,46,32,84,104,105,115,32,112,97,99,107,97,103,101,115,99,104,101,99,107,98 +,111,120,115,117,103,103,101,115,116,115,112,114,101,103,110,97,110,116,116,111, +109,111,114,114,111,119,115,112,97,99,105,110,103,61,105,99,111,110,46,112,110, +103,106,97,112,97,110,101,115,101,99,111,100,101,98,97,115,101,98,117,116,116, +111,110,34,62,103,97,109,98,108,105,110,103,115,117,99,104,32,97,115,32,44,32, +119,104,105,108,101,32,60,47,115,112,97,110,62,32,109,105,115,115,111,117,114, +105,115,112,111,114,116,105,110,103,116,111,112,58,49,112,120,32,46,60,47,115, +112,97,110,62,116,101,110,115,105,111,110,115,119,105,100,116,104,61,34,50,108, +97,122,121,108,111,97,100,110,111,118,101,109,98,101,114,117,115,101,100,32,105, +110,32,104,101,105,103,104,116,61,34,99,114,105,112,116,34,62,10,38,110,98,115, +112,59,60,47,60,116,114,62,60,116,100,32,104,101,105,103,104,116,58,50,47,112, +114,111,100,117,99,116,99,111,117,110,116,114,121,32,105,110,99,108,117,100,101, +32,102,111,111,116,101,114,34,32,38,108,116,59,33,45,45,32,116,105,116,108,101, +34,62,60,47,106,113,117,101,114,121,46,60,47,102,111,114,109,62,10,40,231,174, +128,228,189,147,41,40,231,185,129,233,171,148,41,104,114,118,97,116,115,107,105, +105,116,97,108,105,97,110,111,114,111,109,195,162,110,196,131,116,195,188,114, +107,195,167,101,216,167,216,177,216,175,217,136,116,97,109,98,105,195,169,110, +110,111,116,105,99,105,97,115,109,101,110,115,97,106,101,115,112,101,114,115,111 +,110,97,115,100,101,114,101,99,104,111,115,110,97,99,105,111,110,97,108,115,101, +114,118,105,99,105,111,99,111,110,116,97,99,116,111,117,115,117,97,114,105,111, +115,112,114,111,103,114,97,109,97,103,111,98,105,101,114,110,111,101,109,112,114 +,101,115,97,115,97,110,117,110,99,105,111,115,118,97,108,101,110,99,105,97,99, +111,108,111,109,98,105,97,100,101,115,112,117,195,169,115,100,101,112,111,114, +116,101,115,112,114,111,121,101,99,116,111,112,114,111,100,117,99,116,111,112, +195,186,98,108,105,99,111,110,111,115,111,116,114,111,115,104,105,115,116,111, +114,105,97,112,114,101,115,101,110,116,101,109,105,108,108,111,110,101,115,109, +101,100,105,97,110,116,101,112,114,101,103,117,110,116,97,97,110,116,101,114,105 +,111,114,114,101,99,117,114,115,111,115,112,114,111,98,108,101,109,97,115,97,110 +,116,105,97,103,111,110,117,101,115,116,114,111,115,111,112,105,110,105,195,179, +110,105,109,112,114,105,109,105,114,109,105,101,110,116,114,97,115,97,109,195, +169,114,105,99,97,118,101,110,100,101,100,111,114,115,111,99,105,101,100,97,100, +114,101,115,112,101,99,116,111,114,101,97,108,105,122,97,114,114,101,103,105,115 +,116,114,111,112,97,108,97,98,114,97,115,105,110,116,101,114,195,169,115,101,110 +,116,111,110,99,101,115,101,115,112,101,99,105,97,108,109,105,101,109,98,114,111 +,115,114,101,97,108,105,100,97,100,99,195,179,114,100,111,98,97,122,97,114,97, +103,111,122,97,112,195,161,103,105,110,97,115,115,111,99,105,97,108,101,115,98, +108,111,113,117,101,97,114,103,101,115,116,105,195,179,110,97,108,113,117,105, +108,101,114,115,105,115,116,101,109,97,115,99,105,101,110,99,105,97,115,99,111, +109,112,108,101,116,111,118,101,114,115,105,195,179,110,99,111,109,112,108,101, +116,97,101,115,116,117,100,105,111,115,112,195,186,98,108,105,99,97,111,98,106, +101,116,105,118,111,97,108,105,99,97,110,116,101,98,117,115,99,97,100,111,114,99 +,97,110,116,105,100,97,100,101,110,116,114,97,100,97,115,97,99,99,105,111,110, +101,115,97,114,99,104,105,118,111,115,115,117,112,101,114,105,111,114,109,97,121 +,111,114,195,173,97,97,108,101,109,97,110,105,97,102,117,110,99,105,195,179,110, +195,186,108,116,105,109,111,115,104,97,99,105,101,110,100,111,97,113,117,101,108 +,108,111,115,101,100,105,99,105,195,179,110,102,101,114,110,97,110,100,111,97, +109,98,105,101,110,116,101,102,97,99,101,98,111,111,107,110,117,101,115,116,114, +97,115,99,108,105,101,110,116,101,115,112,114,111,99,101,115,111,115,98,97,115, +116,97,110,116,101,112,114,101,115,101,110,116,97,114,101,112,111,114,116,97,114 +,99,111,110,103,114,101,115,111,112,117,98,108,105,99,97,114,99,111,109,101,114, +99,105,111,99,111,110,116,114,97,116,111,106,195,179,118,101,110,101,115,100,105 +,115,116,114,105,116,111,116,195,169,99,110,105,99,97,99,111,110,106,117,110,116 +,111,101,110,101,114,103,195,173,97,116,114,97,98,97,106,97,114,97,115,116,117, +114,105,97,115,114,101,99,105,101,110,116,101,117,116,105,108,105,122,97,114,98, +111,108,101,116,195,173,110,115,97,108,118,97,100,111,114,99,111,114,114,101,99, +116,97,116,114,97,98,97,106,111,115,112,114,105,109,101,114,111,115,110,101,103, +111,99,105,111,115,108,105,98,101,114,116,97,100,100,101,116,97,108,108,101,115, +112,97,110,116,97,108,108,97,112,114,195,179,120,105,109,111,97,108,109,101,114, +195,173,97,97,110,105,109,97,108,101,115,113,117,105,195,169,110,101,115,99,111, +114,97,122,195,179,110,115,101,99,99,105,195,179,110,98,117,115,99,97,110,100, +111,111,112,99,105,111,110,101,115,101,120,116,101,114,105,111,114,99,111,110,99 +,101,112,116,111,116,111,100,97,118,195,173,97,103,97,108,101,114,195,173,97,101 +,115,99,114,105,98,105,114,109,101,100,105,99,105,110,97,108,105,99,101,110,99, +105,97,99,111,110,115,117,108,116,97,97,115,112,101,99,116,111,115,99,114,195, +173,116,105,99,97,100,195,179,108,97,114,101,115,106,117,115,116,105,99,105,97, +100,101,98,101,114,195,161,110,112,101,114,195,173,111,100,111,110,101,99,101, +115,105,116,97,109,97,110,116,101,110,101,114,112,101,113,117,101,195,177,111, +114,101,99,105,98,105,100,97,116,114,105,98,117,110,97,108,116,101,110,101,114, +105,102,101,99,97,110,99,105,195,179,110,99,97,110,97,114,105,97,115,100,101,115 +,99,97,114,103,97,100,105,118,101,114,115,111,115,109,97,108,108,111,114,99,97, +114,101,113,117,105,101,114,101,116,195,169,99,110,105,99,111,100,101,98,101,114 +,195,173,97,118,105,118,105,101,110,100,97,102,105,110,97,110,122,97,115,97,100, +101,108,97,110,116,101,102,117,110,99,105,111,110,97,99,111,110,115,101,106,111, +115,100,105,102,195,173,99,105,108,99,105,117,100,97,100,101,115,97,110,116,105, +103,117,97,115,97,118,97,110,122,97,100,97,116,195,169,114,109,105,110,111,117, +110,105,100,97,100,101,115,115,195,161,110,99,104,101,122,99,97,109,112,97,195, +177,97,115,111,102,116,111,110,105,99,114,101,118,105,115,116,97,115,99,111,110, +116,105,101,110,101,115,101,99,116,111,114,101,115,109,111,109,101,110,116,111, +115,102,97,99,117,108,116,97,100,99,114,195,169,100,105,116,111,100,105,118,101, +114,115,97,115,115,117,112,117,101,115,116,111,102,97,99,116,111,114,101,115,115 +,101,103,117,110,100,111,115,112,101,113,117,101,195,177,97,208,179,208,190,208, +180,208,176,208,181,209,129,208,187,208,184,208,181,209,129,209,130,209,140,208, +177,209,139,208,187,208,190,208,177,209,139,209,130,209,140,209,141,209,130,208, +190,208,188,208,149,209,129,208,187,208,184,209,130,208,190,208,179,208,190,208, +188,208,181,208,189,209,143,208,178,209,129,208,181,209,133,209,141,209,130,208, +190,208,185,208,180,208,176,208,182,208,181,208,177,209,139,208,187,208,184,208, +179,208,190,208,180,209,131,208,180,208,181,208,189,209,140,209,141,209,130,208, +190,209,130,208,177,209,139,208,187,208,176,209,129,208,181,208,177,209,143,208, +190,208,180,208,184,208,189,209,129,208,181,208,177,208,181,208,189,208,176,208, +180,208,190,209,129,208,176,208,185,209,130,209,132,208,190,209,130,208,190,208, +189,208,181,208,179,208,190,209,129,208,178,208,190,208,184,209,129,208,178,208, +190,208,185,208,184,208,179,209,128,209,139,209,130,208,190,208,182,208,181,208, +178,209,129,208,181,208,188,209,129,208,178,208,190,209,142,208,187,208,184,209, +136,209,140,209,141,209,130,208,184,209,133,208,191,208,190,208,186,208,176,208, +180,208,189,208,181,208,185,208,180,208,190,208,188,208,176,208,188,208,184,209, +128,208,176,208,187,208,184,208,177,208,190,209,130,208,181,208,188,209,131,209, +133,208,190,209,130,209,143,208,180,208,178,209,131,209,133,209,129,208,181,209, +130,208,184,208,187,209,142,208,180,208,184,208,180,208,181,208,187,208,190,208, +188,208,184,209,128,208,181,209,130,208,181,208,177,209,143,209,129,208,178,208, +190,208,181,208,178,208,184,208,180,208,181,209,135,208,181,208,179,208,190,209, +141,209,130,208,184,208,188,209,129,209,135,208,181,209,130,209,130,208,181,208, +188,209,139,209,134,208,181,208,189,209,139,209,129,209,130,208,176,208,187,208, +178,208,181,208,180,209,140,209,130,208,181,208,188,208,181,208,178,208,190,208, +180,209,139,209,130,208,181,208,177,208,181,208,178,209,139,209,136,208,181,208, +189,208,176,208,188,208,184,209,130,208,184,208,191,208,176,209,130,208,190,208, +188,209,131,208,191,209,128,208,176,208,178,208,187,208,184,209,134,208,176,208, +190,208,180,208,189,208,176,208,179,208,190,208,180,209,139,208,183,208,189,208, +176,209,142,208,188,208,190,208,179,209,131,208,180,209,128,209,131,208,179,208, +178,209,129,208,181,208,185,208,184,208,180,208,181,209,130,208,186,208,184,208, +189,208,190,208,190,208,180,208,189,208,190,208,180,208,181,208,187,208,176,208, +180,208,181,208,187,208,181,209,129,209,128,208,190,208,186,208,184,209,142,208, +189,209,143,208,178,208,181,209,129,209,140,208,149,209,129,209,130,209,140,209, +128,208,176,208,183,208,176,208,189,208,176,209,136,208,184,216,167,217,132,217, +132,217,135,216,167,217,132,216,170,217,138,216,172,217,133,217,138,216,185,216, +174,216,167,216,181,216,169,216,167,217,132,216,176,217,138,216,185,217,132,217, +138,217,135,216,172,216,175,217,138,216,175,216,167,217,132,216,162,217,134,216, +167,217,132,216,177,216,175,216,170,216,173,217,131,217,133,216,181,217,129,216, +173,216,169,217,131,216,167,217,134,216,170,216,167,217,132,217,132,217,138,217, +138,217,131,217,136,217,134,216,180,216,168,217,131,216,169,217,129,217,138,217, +135,216,167,216,168,217,134,216,167,216,170,216,173,217,136,216,167,216,161,216, +163,217,131,216,171,216,177,216,174,217,132,216,167,217,132,216,167,217,132,216, +173,216,168,216,175,217,132,217,138,217,132,216,175,216,177,217,136,216,179,216, +167,216,182,216,186,216,183,216,170,217,131,217,136,217,134,217,135,217,134,216, +167,217,131,216,179,216,167,216,173,216,169,217,134,216,167,216,175,217,138,216, +167,217,132,216,183,216,168,216,185,217,132,217,138,217,131,216,180,217,131,216, +177,216,167,217,138,217,133,217,131,217,134,217,133,217,134,217,135,216,167,216, +180,216,177,217,131,216,169,216,177,216,166,217,138,216,179,217,134,216,180,217, +138,216,183,217,133,216,167,216,176,216,167,216,167,217,132,217,129,217,134,216, +180,216,168,216,167,216,168,216,170,216,185,216,168,216,177,216,177,216,173,217, +133,216,169,217,131,216,167,217,129,216,169,217,138,217,130,217,136,217,132,217, +133,216,177,217,131,216,178,217,131,217,132,217,133,216,169,216,163,216,173,217, +133,216,175,217,130,217,132,216,168,217,138,217,138,216,185,217,134,217,138,216, +181,217,136,216,177,216,169,216,183,216,177,217,138,217,130,216,180,216,167,216, +177,217,131,216,172,217,136,216,167,217,132,216,163,216,174,216,177,217,137,217, +133,216,185,217,134,216,167,216,167,216,168,216,173,216,171,216,185,216,177,217, +136,216,182,216,168,216,180,217,131,217,132,217,133,216,179,216,172,217,132,216, +168,217,134,216,167,217,134,216,174,216,167,217,132,216,175,217,131,216,170,216, +167,216,168,217,131,217,132,217,138,216,169,216,168,216,175,217,136,217,134,216, +163,217,138,216,182,216,167,217,138,217,136,216,172,216,175,217,129,216,177,217, +138,217,130,217,131,216,170,216,168,216,170,216,163,217,129,216,182,217,132,217, +133,216,183,216,168,216,174,216,167,217,131,216,171,216,177,216,168,216,167,216, +177,217,131,216,167,217,129,216,182,217,132,216,167,216,173,217,132,217,137,217, +134,217,129,216,179,217,135,216,163,217,138,216,167,217,133,216,177,216,175,217, +136,216,175,216,163,217,134,217,135,216,167,216,175,217,138,217,134,216,167,216, +167,217,132,216,167,217,134,217,133,216,185,216,177,216,182,216,170,216,185,217, +132,217,133,216,175,216,167,216,174,217,132,217,133,217,133,217,131,217,134,0,0, +0,0,0,0,0,0,1,0,1,0,1,0,1,0,2,0,2,0,2,0,2,0,4,0,4,0,4,0,4,0,0,1,2,3,4,5,6,7,7,6, +5,4,3,2,1,0,8,9,10,11,12,13,14,15,15,14,13,12,11,10,9,8,16,17,18,19,20,21,22,23, +23,22,21,20,19,18,17,16,24,25,26,27,28,29,30,31,31,30,29,28,27,26,25,24,255,255, +255,255,0,0,0,0,0,0,0,0,255,255,255,255,1,0,0,0,2,0,0,0,2,0,0,0,1,0,0,0,1,0,0,0, +3,0,0,0,255,255,0,1,0,0,0,1,0,0,255,255,0,1,0,0,0,8,0,8,0,8,0,8,0,0,0,1,0,2,0,3, +0,4,0,5,0,6,0,7,114,101,115,111,117,114,99,101,115,99,111,117,110,116,114,105, +101,115,113,117,101,115,116,105,111,110,115,101,113,117,105,112,109,101,110,116, +99,111,109,109,117,110,105,116,121,97,118,97,105,108,97,98,108,101,104,105,103, +104,108,105,103,104,116,68,84,68,47,120,104,116,109,108,109,97,114,107,101,116, +105,110,103,107,110,111,119,108,101,100,103,101,115,111,109,101,116,104,105,110, +103,99,111,110,116,97,105,110,101,114,100,105,114,101,99,116,105,111,110,115,117 +,98,115,99,114,105,98,101,97,100,118,101,114,116,105,115,101,99,104,97,114,97,99 +,116,101,114,34,32,118,97,108,117,101,61,34,60,47,115,101,108,101,99,116,62,65, +117,115,116,114,97,108,105,97,34,32,99,108,97,115,115,61,34,115,105,116,117,97, +116,105,111,110,97,117,116,104,111,114,105,116,121,102,111,108,108,111,119,105, +110,103,112,114,105,109,97,114,105,108,121,111,112,101,114,97,116,105,111,110,99 +,104,97,108,108,101,110,103,101,100,101,118,101,108,111,112,101,100,97,110,111, +110,121,109,111,117,115,102,117,110,99,116,105,111,110,32,102,117,110,99,116,105 +,111,110,115,99,111,109,112,97,110,105,101,115,115,116,114,117,99,116,117,114, +101,97,103,114,101,101,109,101,110,116,34,32,116,105,116,108,101,61,34,112,111, +116,101,110,116,105,97,108,101,100,117,99,97,116,105,111,110,97,114,103,117,109, +101,110,116,115,115,101,99,111,110,100,97,114,121,99,111,112,121,114,105,103,104 +,116,108,97,110,103,117,97,103,101,115,101,120,99,108,117,115,105,118,101,99,111 +,110,100,105,116,105,111,110,60,47,102,111,114,109,62,13,10,115,116,97,116,101, +109,101,110,116,97,116,116,101,110,116,105,111,110,66,105,111,103,114,97,112,104 +,121,125,32,101,108,115,101,32,123,10,115,111,108,117,116,105,111,110,115,119, +104,101,110,32,116,104,101,32,65,110,97,108,121,116,105,99,115,116,101,109,112, +108,97,116,101,115,100,97,110,103,101,114,111,117,115,115,97,116,101,108,108,105 +,116,101,100,111,99,117,109,101,110,116,115,112,117,98,108,105,115,104,101,114, +105,109,112,111,114,116,97,110,116,112,114,111,116,111,116,121,112,101,105,110, +102,108,117,101,110,99,101,38,114,97,113,117,111,59,60,47,101,102,102,101,99,116 +,105,118,101,103,101,110,101,114,97,108,108,121,116,114,97,110,115,102,111,114, +109,98,101,97,117,116,105,102,117,108,116,114,97,110,115,112,111,114,116,111,114 +,103,97,110,105,122,101,100,112,117,98,108,105,115,104,101,100,112,114,111,109, +105,110,101,110,116,117,110,116,105,108,32,116,104,101,116,104,117,109,98,110,97 +,105,108,78,97,116,105,111,110,97,108,32,46,102,111,99,117,115,40,41,59,111,118, +101,114,32,116,104,101,32,109,105,103,114,97,116,105,111,110,97,110,110,111,117, +110,99,101,100,102,111,111,116,101,114,34,62,10,101,120,99,101,112,116,105,111, +110,108,101,115,115,32,116,104,97,110,101,120,112,101,110,115,105,118,101,102, +111,114,109,97,116,105,111,110,102,114,97,109,101,119,111,114,107,116,101,114, +114,105,116,111,114,121,110,100,105,99,97,116,105,111,110,99,117,114,114,101,110 +,116,108,121,99,108,97,115,115,78,97,109,101,99,114,105,116,105,99,105,115,109, +116,114,97,100,105,116,105,111,110,101,108,115,101,119,104,101,114,101,65,108, +101,120,97,110,100,101,114,97,112,112,111,105,110,116,101,100,109,97,116,101,114 +,105,97,108,115,98,114,111,97,100,99,97,115,116,109,101,110,116,105,111,110,101, +100,97,102,102,105,108,105,97,116,101,60,47,111,112,116,105,111,110,62,116,114, +101,97,116,109,101,110,116,100,105,102,102,101,114,101,110,116,47,100,101,102,97 +,117,108,116,46,80,114,101,115,105,100,101,110,116,111,110,99,108,105,99,107,61, +34,98,105,111,103,114,97,112,104,121,111,116,104,101,114,119,105,115,101,112,101 +,114,109,97,110,101,110,116,70,114,97,110,195,167,97,105,115,72,111,108,108,121, +119,111,111,100,101,120,112,97,110,115,105,111,110,115,116,97,110,100,97,114,100 +,115,60,47,115,116,121,108,101,62,10,114,101,100,117,99,116,105,111,110,68,101, +99,101,109,98,101,114,32,112,114,101,102,101,114,114,101,100,67,97,109,98,114, +105,100,103,101,111,112,112,111,110,101,110,116,115,66,117,115,105,110,101,115, +115,32,99,111,110,102,117,115,105,111,110,62,10,60,116,105,116,108,101,62,112, +114,101,115,101,110,116,101,100,101,120,112,108,97,105,110,101,100,100,111,101, +115,32,110,111,116,32,119,111,114,108,100,119,105,100,101,105,110,116,101,114, +102,97,99,101,112,111,115,105,116,105,111,110,115,110,101,119,115,112,97,112,101 +,114,60,47,116,97,98,108,101,62,10,109,111,117,110,116,97,105,110,115,108,105, +107,101,32,116,104,101,32,101,115,115,101,110,116,105,97,108,102,105,110,97,110, +99,105,97,108,115,101,108,101,99,116,105,111,110,97,99,116,105,111,110,61,34,47, +97,98,97,110,100,111,110,101,100,69,100,117,99,97,116,105,111,110,112,97,114,115 +,101,73,110,116,40,115,116,97,98,105,108,105,116,121,117,110,97,98,108,101,32, +116,111,60,47,116,105,116,108,101,62,10,114,101,108,97,116,105,111,110,115,78, +111,116,101,32,116,104,97,116,101,102,102,105,99,105,101,110,116,112,101,114,102 +,111,114,109,101,100,116,119,111,32,121,101,97,114,115,83,105,110,99,101,32,116, +104,101,116,104,101,114,101,102,111,114,101,119,114,97,112,112,101,114,34,62,97, +108,116,101,114,110,97,116,101,105,110,99,114,101,97,115,101,100,66,97,116,116, +108,101,32,111,102,112,101,114,99,101,105,118,101,100,116,114,121,105,110,103,32 +,116,111,110,101,99,101,115,115,97,114,121,112,111,114,116,114,97,121,101,100, +101,108,101,99,116,105,111,110,115,69,108,105,122,97,98,101,116,104,60,47,105, +102,114,97,109,101,62,100,105,115,99,111,118,101,114,121,105,110,115,117,114,97, +110,99,101,115,46,108,101,110,103,116,104,59,108,101,103,101,110,100,97,114,121, +71,101,111,103,114,97,112,104,121,99,97,110,100,105,100,97,116,101,99,111,114, +112,111,114,97,116,101,115,111,109,101,116,105,109,101,115,115,101,114,118,105, +99,101,115,46,105,110,104,101,114,105,116,101,100,60,47,115,116,114,111,110,103, +62,67,111,109,109,117,110,105,116,121,114,101,108,105,103,105,111,117,115,108, +111,99,97,116,105,111,110,115,67,111,109,109,105,116,116,101,101,98,117,105,108, +100,105,110,103,115,116,104,101,32,119,111,114,108,100,110,111,32,108,111,110, +103,101,114,98,101,103,105,110,110,105,110,103,114,101,102,101,114,101,110,99, +101,99,97,110,110,111,116,32,98,101,102,114,101,113,117,101,110,99,121,116,121, +112,105,99,97,108,108,121,105,110,116,111,32,116,104,101,32,114,101,108,97,116, +105,118,101,59,114,101,99,111,114,100,105,110,103,112,114,101,115,105,100,101, +110,116,105,110,105,116,105,97,108,108,121,116,101,99,104,110,105,113,117,101, +116,104,101,32,111,116,104,101,114,105,116,32,99,97,110,32,98,101,101,120,105, +115,116,101,110,99,101,117,110,100,101,114,108,105,110,101,116,104,105,115,32, +116,105,109,101,116,101,108,101,112,104,111,110,101,105,116,101,109,115,99,111, +112,101,112,114,97,99,116,105,99,101,115,97,100,118,97,110,116,97,103,101,41,59, +114,101,116,117,114,110,32,70,111,114,32,111,116,104,101,114,112,114,111,118,105 +,100,105,110,103,100,101,109,111,99,114,97,99,121,98,111,116,104,32,116,104,101, +32,101,120,116,101,110,115,105,118,101,115,117,102,102,101,114,105,110,103,115, +117,112,112,111,114,116,101,100,99,111,109,112,117,116,101,114,115,32,102,117, +110,99,116,105,111,110,112,114,97,99,116,105,99,97,108,115,97,105,100,32,116,104 +,97,116,105,116,32,109,97,121,32,98,101,69,110,103,108,105,115,104,60,47,102,114 +,111,109,32,116,104,101,32,115,99,104,101,100,117,108,101,100,100,111,119,110, +108,111,97,100,115,60,47,108,97,98,101,108,62,10,115,117,115,112,101,99,116,101, +100,109,97,114,103,105,110,58,32,48,115,112,105,114,105,116,117,97,108,60,47,104 +,101,97,100,62,10,10,109,105,99,114,111,115,111,102,116,103,114,97,100,117,97, +108,108,121,100,105,115,99,117,115,115,101,100,104,101,32,98,101,99,97,109,101, +101,120,101,99,117,116,105,118,101,106,113,117,101,114,121,46,106,115,104,111, +117,115,101,104,111,108,100,99,111,110,102,105,114,109,101,100,112,117,114,99, +104,97,115,101,100,108,105,116,101,114,97,108,108,121,100,101,115,116,114,111, +121,101,100,117,112,32,116,111,32,116,104,101,118,97,114,105,97,116,105,111,110, +114,101,109,97,105,110,105,110,103,105,116,32,105,115,32,110,111,116,99,101,110, +116,117,114,105,101,115,74,97,112,97,110,101,115,101,32,97,109,111,110,103,32, +116,104,101,99,111,109,112,108,101,116,101,100,97,108,103,111,114,105,116,104, +109,105,110,116,101,114,101,115,116,115,114,101,98,101,108,108,105,111,110,117, +110,100,101,102,105,110,101,100,101,110,99,111,117,114,97,103,101,114,101,115, +105,122,97,98,108,101,105,110,118,111,108,118,105,110,103,115,101,110,115,105, +116,105,118,101,117,110,105,118,101,114,115,97,108,112,114,111,118,105,115,105, +111,110,40,97,108,116,104,111,117,103,104,102,101,97,116,117,114,105,110,103,99, +111,110,100,117,99,116,101,100,41,44,32,119,104,105,99,104,32,99,111,110,116,105 +,110,117,101,100,45,104,101,97,100,101,114,34,62,70,101,98,114,117,97,114,121,32 +,110,117,109,101,114,111,117,115,32,111,118,101,114,102,108,111,119,58,99,111, +109,112,111,110,101,110,116,102,114,97,103,109,101,110,116,115,101,120,99,101, +108,108,101,110,116,99,111,108,115,112,97,110,61,34,116,101,99,104,110,105,99,97 +,108,110,101,97,114,32,116,104,101,32,65,100,118,97,110,99,101,100,32,115,111, +117,114,99,101,32,111,102,101,120,112,114,101,115,115,101,100,72,111,110,103,32, +75,111,110,103,32,70,97,99,101,98,111,111,107,109,117,108,116,105,112,108,101,32 +,109,101,99,104,97,110,105,115,109,101,108,101,118,97,116,105,111,110,111,102, +102,101,110,115,105,118,101,60,47,102,111,114,109,62,10,9,115,112,111,110,115, +111,114,101,100,100,111,99,117,109,101,110,116,46,111,114,32,38,113,117,111,116, +59,116,104,101,114,101,32,97,114,101,116,104,111,115,101,32,119,104,111,109,111, +118,101,109,101,110,116,115,112,114,111,99,101,115,115,101,115,100,105,102,102, +105,99,117,108,116,115,117,98,109,105,116,116,101,100,114,101,99,111,109,109,101 +,110,100,99,111,110,118,105,110,99,101,100,112,114,111,109,111,116,105,110,103, +34,32,119,105,100,116,104,61,34,46,114,101,112,108,97,99,101,40,99,108,97,115, +115,105,99,97,108,99,111,97,108,105,116,105,111,110,104,105,115,32,102,105,114, +115,116,100,101,99,105,115,105,111,110,115,97,115,115,105,115,116,97,110,116,105 +,110,100,105,99,97,116,101,100,101,118,111,108,117,116,105,111,110,45,119,114,97 +,112,112,101,114,34,101,110,111,117,103,104,32,116,111,97,108,111,110,103,32,116 +,104,101,100,101,108,105,118,101,114,101,100,45,45,62,13,10,60,33,45,45,65,109, +101,114,105,99,97,110,32,112,114,111,116,101,99,116,101,100,78,111,118,101,109, +98,101,114,32,60,47,115,116,121,108,101,62,60,102,117,114,110,105,116,117,114, +101,73,110,116,101,114,110,101,116,32,32,111,110,98,108,117,114,61,34,115,117, +115,112,101,110,100,101,100,114,101,99,105,112,105,101,110,116,98,97,115,101,100 +,32,111,110,32,77,111,114,101,111,118,101,114,44,97,98,111,108,105,115,104,101, +100,99,111,108,108,101,99,116,101,100,119,101,114,101,32,109,97,100,101,101,109, +111,116,105,111,110,97,108,101,109,101,114,103,101,110,99,121,110,97,114,114,97, +116,105,118,101,97,100,118,111,99,97,116,101,115,112,120,59,98,111,114,100,101, +114,99,111,109,109,105,116,116,101,100,100,105,114,61,34,108,116,114,34,101,109, +112,108,111,121,101,101,115,114,101,115,101,97,114,99,104,46,32,115,101,108,101, +99,116,101,100,115,117,99,99,101,115,115,111,114,99,117,115,116,111,109,101,114, +115,100,105,115,112,108,97,121,101,100,83,101,112,116,101,109,98,101,114,97,100, +100,67,108,97,115,115,40,70,97,99,101,98,111,111,107,32,115,117,103,103,101,115, +116,101,100,97,110,100,32,108,97,116,101,114,111,112,101,114,97,116,105,110,103, +101,108,97,98,111,114,97,116,101,83,111,109,101,116,105,109,101,115,73,110,115, +116,105,116,117,116,101,99,101,114,116,97,105,110,108,121,105,110,115,116,97,108 +,108,101,100,102,111,108,108,111,119,101,114,115,74,101,114,117,115,97,108,101, +109,116,104,101,121,32,104,97,118,101,99,111,109,112,117,116,105,110,103,103,101 +,110,101,114,97,116,101,100,112,114,111,118,105,110,99,101,115,103,117,97,114,97 +,110,116,101,101,97,114,98,105,116,114,97,114,121,114,101,99,111,103,110,105,122 +,101,119,97,110,116,101,100,32,116,111,112,120,59,119,105,100,116,104,58,116,104 +,101,111,114,121,32,111,102,98,101,104,97,118,105,111,117,114,87,104,105,108,101 +,32,116,104,101,101,115,116,105,109,97,116,101,100,98,101,103,97,110,32,116,111, +32,105,116,32,98,101,99,97,109,101,109,97,103,110,105,116,117,100,101,109,117, +115,116,32,104,97,118,101,109,111,114,101,32,116,104,97,110,68,105,114,101,99, +116,111,114,121,101,120,116,101,110,115,105,111,110,115,101,99,114,101,116,97, +114,121,110,97,116,117,114,97,108,108,121,111,99,99,117,114,114,105,110,103,118, +97,114,105,97,98,108,101,115,103,105,118,101,110,32,116,104,101,112,108,97,116, +102,111,114,109,46,60,47,108,97,98,101,108,62,60,102,97,105,108,101,100,32,116, +111,99,111,109,112,111,117,110,100,115,107,105,110,100,115,32,111,102,32,115,111 +,99,105,101,116,105,101,115,97,108,111,110,103,115,105,100,101,32,45,45,38,103, +116,59,10,10,115,111,117,116,104,119,101,115,116,116,104,101,32,114,105,103,104, +116,114,97,100,105,97,116,105,111,110,109,97,121,32,104,97,118,101,32,117,110, +101,115,99,97,112,101,40,115,112,111,107,101,110,32,105,110,34,32,104,114,101, +102,61,34,47,112,114,111,103,114,97,109,109,101,111,110,108,121,32,116,104,101, +32,99,111,109,101,32,102,114,111,109,100,105,114,101,99,116,111,114,121,98,117, +114,105,101,100,32,105,110,97,32,115,105,109,105,108,97,114,116,104,101,121,32, +119,101,114,101,60,47,102,111,110,116,62,60,47,78,111,114,119,101,103,105,97,110 +,115,112,101,99,105,102,105,101,100,112,114,111,100,117,99,105,110,103,112,97, +115,115,101,110,103,101,114,40,110,101,119,32,68,97,116,101,116,101,109,112,111, +114,97,114,121,102,105,99,116,105,111,110,97,108,65,102,116,101,114,32,116,104, +101,101,113,117,97,116,105,111,110,115,100,111,119,110,108,111,97,100,46,114,101 +,103,117,108,97,114,108,121,100,101,118,101,108,111,112,101,114,97,98,111,118, +101,32,116,104,101,108,105,110,107,101,100,32,116,111,112,104,101,110,111,109, +101,110,97,112,101,114,105,111,100,32,111,102,116,111,111,108,116,105,112,34,62, +115,117,98,115,116,97,110,99,101,97,117,116,111,109,97,116,105,99,97,115,112,101 +,99,116,32,111,102,65,109,111,110,103,32,116,104,101,99,111,110,110,101,99,116, +101,100,101,115,116,105,109,97,116,101,115,65,105,114,32,70,111,114,99,101,115, +121,115,116,101,109,32,111,102,111,98,106,101,99,116,105,118,101,105,109,109,101 +,100,105,97,116,101,109,97,107,105,110,103,32,105,116,112,97,105,110,116,105,110 +,103,115,99,111,110,113,117,101,114,101,100,97,114,101,32,115,116,105,108,108, +112,114,111,99,101,100,117,114,101,103,114,111,119,116,104,32,111,102,104,101,97 +,100,101,100,32,98,121,69,117,114,111,112,101,97,110,32,100,105,118,105,115,105, +111,110,115,109,111,108,101,99,117,108,101,115,102,114,97,110,99,104,105,115,101 +,105,110,116,101,110,116,105,111,110,97,116,116,114,97,99,116,101,100,99,104,105 +,108,100,104,111,111,100,97,108,115,111,32,117,115,101,100,100,101,100,105,99,97 +,116,101,100,115,105,110,103,97,112,111,114,101,100,101,103,114,101,101,32,111, +102,102,97,116,104,101,114,32,111,102,99,111,110,102,108,105,99,116,115,60,47,97 +,62,60,47,112,62,10,99,97,109,101,32,102,114,111,109,119,101,114,101,32,117,115, +101,100,110,111,116,101,32,116,104,97,116,114,101,99,101,105,118,105,110,103,69, +120,101,99,117,116,105,118,101,101,118,101,110,32,109,111,114,101,97,99,99,101, +115,115,32,116,111,99,111,109,109,97,110,100,101,114,80,111,108,105,116,105,99, +97,108,109,117,115,105,99,105,97,110,115,100,101,108,105,99,105,111,117,115,112, +114,105,115,111,110,101,114,115,97,100,118,101,110,116,32,111,102,85,84,70,45,56 +,34,32,47,62,60,33,91,67,68,65,84,65,91,34,62,67,111,110,116,97,99,116,83,111, +117,116,104,101,114,110,32,98,103,99,111,108,111,114,61,34,115,101,114,105,101, +115,32,111,102,46,32,73,116,32,119,97,115,32,105,110,32,69,117,114,111,112,101, +112,101,114,109,105,116,116,101,100,118,97,108,105,100,97,116,101,46,97,112,112, +101,97,114,105,110,103,111,102,102,105,99,105,97,108,115,115,101,114,105,111,117 +,115,108,121,45,108,97,110,103,117,97,103,101,105,110,105,116,105,97,116,101,100 +,101,120,116,101,110,100,105,110,103,108,111,110,103,45,116,101,114,109,105,110, +102,108,97,116,105,111,110,115,117,99,104,32,116,104,97,116,103,101,116,67,111, +111,107,105,101,109,97,114,107,101,100,32,98,121,60,47,98,117,116,116,111,110,62 +,105,109,112,108,101,109,101,110,116,98,117,116,32,105,116,32,105,115,105,110,99 +,114,101,97,115,101,115,100,111,119,110,32,116,104,101,32,114,101,113,117,105, +114,105,110,103,100,101,112,101,110,100,101,110,116,45,45,62,10,60,33,45,45,32, +105,110,116,101,114,118,105,101,119,87,105,116,104,32,116,104,101,32,99,111,112, +105,101,115,32,111,102,99,111,110,115,101,110,115,117,115,119,97,115,32,98,117, +105,108,116,86,101,110,101,122,117,101,108,97,40,102,111,114,109,101,114,108,121 +,116,104,101,32,115,116,97,116,101,112,101,114,115,111,110,110,101,108,115,116, +114,97,116,101,103,105,99,102,97,118,111,117,114,32,111,102,105,110,118,101,110, +116,105,111,110,87,105,107,105,112,101,100,105,97,99,111,110,116,105,110,101,110 +,116,118,105,114,116,117,97,108,108,121,119,104,105,99,104,32,119,97,115,112,114 +,105,110,99,105,112,108,101,67,111,109,112,108,101,116,101,32,105,100,101,110, +116,105,99,97,108,115,104,111,119,32,116,104,97,116,112,114,105,109,105,116,105, +118,101,97,119,97,121,32,102,114,111,109,109,111,108,101,99,117,108,97,114,112, +114,101,99,105,115,101,108,121,100,105,115,115,111,108,118,101,100,85,110,100, +101,114,32,116,104,101,118,101,114,115,105,111,110,61,34,62,38,110,98,115,112,59 +,60,47,73,116,32,105,115,32,116,104,101,32,84,104,105,115,32,105,115,32,119,105, +108,108,32,104,97,118,101,111,114,103,97,110,105,115,109,115,115,111,109,101,32, +116,105,109,101,70,114,105,101,100,114,105,99,104,119,97,115,32,102,105,114,115, +116,116,104,101,32,111,110,108,121,32,102,97,99,116,32,116,104,97,116,102,111, +114,109,32,105,100,61,34,112,114,101,99,101,100,105,110,103,84,101,99,104,110, +105,99,97,108,112,104,121,115,105,99,105,115,116,111,99,99,117,114,115,32,105, +110,110,97,118,105,103,97,116,111,114,115,101,99,116,105,111,110,34,62,115,112, +97,110,32,105,100,61,34,115,111,117,103,104,116,32,116,111,98,101,108,111,119,32 +,116,104,101,115,117,114,118,105,118,105,110,103,125,60,47,115,116,121,108,101, +62,104,105,115,32,100,101,97,116,104,97,115,32,105,110,32,116,104,101,99,97,117, +115,101,100,32,98,121,112,97,114,116,105,97,108,108,121,101,120,105,115,116,105, +110,103,32,117,115,105,110,103,32,116,104,101,119,97,115,32,103,105,118,101,110, +97,32,108,105,115,116,32,111,102,108,101,118,101,108,115,32,111,102,110,111,116, +105,111,110,32,111,102,79,102,102,105,99,105,97,108,32,100,105,115,109,105,115, +115,101,100,115,99,105,101,110,116,105,115,116,114,101,115,101,109,98,108,101, +115,100,117,112,108,105,99,97,116,101,101,120,112,108,111,115,105,118,101,114, +101,99,111,118,101,114,101,100,97,108,108,32,111,116,104,101,114,103,97,108,108, +101,114,105,101,115,123,112,97,100,100,105,110,103,58,112,101,111,112,108,101,32 +,111,102,114,101,103,105,111,110,32,111,102,97,100,100,114,101,115,115,101,115, +97,115,115,111,99,105,97,116,101,105,109,103,32,97,108,116,61,34,105,110,32,109, +111,100,101,114,110,115,104,111,117,108,100,32,98,101,109,101,116,104,111,100,32 +,111,102,114,101,112,111,114,116,105,110,103,116,105,109,101,115,116,97,109,112, +110,101,101,100,101,100,32,116,111,116,104,101,32,71,114,101,97,116,114,101,103, +97,114,100,105,110,103,115,101,101,109,101,100,32,116,111,118,105,101,119,101, +100,32,97,115,105,109,112,97,99,116,32,111,110,105,100,101,97,32,116,104,97,116, +116,104,101,32,87,111,114,108,100,104,101,105,103,104,116,32,111,102,101,120,112 +,97,110,100,105,110,103,84,104,101,115,101,32,97,114,101,99,117,114,114,101,110, +116,34,62,99,97,114,101,102,117,108,108,121,109,97,105,110,116,97,105,110,115,99 +,104,97,114,103,101,32,111,102,67,108,97,115,115,105,99,97,108,97,100,100,114, +101,115,115,101,100,112,114,101,100,105,99,116,101,100,111,119,110,101,114,115, +104,105,112,60,100,105,118,32,105,100,61,34,114,105,103,104,116,34,62,13,10,114, +101,115,105,100,101,110,99,101,108,101,97,118,101,32,116,104,101,99,111,110,116, +101,110,116,34,62,97,114,101,32,111,102,116,101,110,32,32,125,41,40,41,59,13,10, +112,114,111,98,97,98,108,121,32,80,114,111,102,101,115,115,111,114,45,98,117,116 +,116,111,110,34,32,114,101,115,112,111,110,100,101,100,115,97,121,115,32,116,104 +,97,116,104,97,100,32,116,111,32,98,101,112,108,97,99,101,100,32,105,110,72,117, +110,103,97,114,105,97,110,115,116,97,116,117,115,32,111,102,115,101,114,118,101, +115,32,97,115,85,110,105,118,101,114,115,97,108,101,120,101,99,117,116,105,111, +110,97,103,103,114,101,103,97,116,101,102,111,114,32,119,104,105,99,104,105,110, +102,101,99,116,105,111,110,97,103,114,101,101,100,32,116,111,104,111,119,101,118 +,101,114,44,32,112,111,112,117,108,97,114,34,62,112,108,97,99,101,100,32,111,110 +,99,111,110,115,116,114,117,99,116,101,108,101,99,116,111,114,97,108,115,121,109 +,98,111,108,32,111,102,105,110,99,108,117,100,105,110,103,114,101,116,117,114, +110,32,116,111,97,114,99,104,105,116,101,99,116,67,104,114,105,115,116,105,97, +110,112,114,101,118,105,111,117,115,32,108,105,118,105,110,103,32,105,110,101,97 +,115,105,101,114,32,116,111,112,114,111,102,101,115,115,111,114,10,38,108,116,59 +,33,45,45,32,101,102,102,101,99,116,32,111,102,97,110,97,108,121,116,105,99,115, +119,97,115,32,116,97,107,101,110,119,104,101,114,101,32,116,104,101,116,111,111, +107,32,111,118,101,114,98,101,108,105,101,102,32,105,110,65,102,114,105,107,97, +97,110,115,97,115,32,102,97,114,32,97,115,112,114,101,118,101,110,116,101,100, +119,111,114,107,32,119,105,116,104,97,32,115,112,101,99,105,97,108,60,102,105, +101,108,100,115,101,116,67,104,114,105,115,116,109,97,115,82,101,116,114,105,101 +,118,101,100,10,10,73,110,32,116,104,101,32,98,97,99,107,32,105,110,116,111,110, +111,114,116,104,101,97,115,116,109,97,103,97,122,105,110,101,115,62,60,115,116, +114,111,110,103,62,99,111,109,109,105,116,116,101,101,103,111,118,101,114,110, +105,110,103,103,114,111,117,112,115,32,111,102,115,116,111,114,101,100,32,105, +110,101,115,116,97,98,108,105,115,104,97,32,103,101,110,101,114,97,108,105,116, +115,32,102,105,114,115,116,116,104,101,105,114,32,111,119,110,112,111,112,117, +108,97,116,101,100,97,110,32,111,98,106,101,99,116,67,97,114,105,98,98,101,97, +110,97,108,108,111,119,32,116,104,101,100,105,115,116,114,105,99,116,115,119,105 +,115,99,111,110,115,105,110,108,111,99,97,116,105,111,110,46,59,32,119,105,100, +116,104,58,32,105,110,104,97,98,105,116,101,100,83,111,99,105,97,108,105,115,116 +,74,97,110,117,97,114,121,32,49,60,47,102,111,111,116,101,114,62,115,105,109,105 +,108,97,114,108,121,99,104,111,105,99,101,32,111,102,116,104,101,32,115,97,109, +101,32,115,112,101,99,105,102,105,99,32,98,117,115,105,110,101,115,115,32,84,104 +,101,32,102,105,114,115,116,46,108,101,110,103,116,104,59,32,100,101,115,105,114 +,101,32,116,111,100,101,97,108,32,119,105,116,104,115,105,110,99,101,32,116,104, +101,117,115,101,114,65,103,101,110,116,99,111,110,99,101,105,118,101,100,105,110 +,100,101,120,46,112,104,112,97,115,32,38,113,117,111,116,59,101,110,103,97,103, +101,32,105,110,114,101,99,101,110,116,108,121,44,102,101,119,32,121,101,97,114, +115,119,101,114,101,32,97,108,115,111,10,60,104,101,97,100,62,10,60,101,100,105, +116,101,100,32,98,121,97,114,101,32,107,110,111,119,110,99,105,116,105,101,115, +32,105,110,97,99,99,101,115,115,107,101,121,99,111,110,100,101,109,110,101,100, +97,108,115,111,32,104,97,118,101,115,101,114,118,105,99,101,115,44,102,97,109, +105,108,121,32,111,102,83,99,104,111,111,108,32,111,102,99,111,110,118,101,114, +116,101,100,110,97,116,117,114,101,32,111,102,32,108,97,110,103,117,97,103,101, +109,105,110,105,115,116,101,114,115,60,47,111,98,106,101,99,116,62,116,104,101, +114,101,32,105,115,32,97,32,112,111,112,117,108,97,114,115,101,113,117,101,110, +99,101,115,97,100,118,111,99,97,116,101,100,84,104,101,121,32,119,101,114,101,97 +,110,121,32,111,116,104,101,114,108,111,99,97,116,105,111,110,61,101,110,116,101 +,114,32,116,104,101,109,117,99,104,32,109,111,114,101,114,101,102,108,101,99,116 +,101,100,119,97,115,32,110,97,109,101,100,111,114,105,103,105,110,97,108,32,97, +32,116,121,112,105,99,97,108,119,104,101,110,32,116,104,101,121,101,110,103,105, +110,101,101,114,115,99,111,117,108,100,32,110,111,116,114,101,115,105,100,101, +110,116,115,119,101,100,110,101,115,100,97,121,116,104,101,32,116,104,105,114, +100,32,112,114,111,100,117,99,116,115,74,97,110,117,97,114,121,32,50,119,104,97, +116,32,116,104,101,121,97,32,99,101,114,116,97,105,110,114,101,97,99,116,105,111 +,110,115,112,114,111,99,101,115,115,111,114,97,102,116,101,114,32,104,105,115, +116,104,101,32,108,97,115,116,32,99,111,110,116,97,105,110,101,100,34,62,60,47, +100,105,118,62,10,60,47,97,62,60,47,116,100,62,100,101,112,101,110,100,32,111, +110,115,101,97,114,99,104,34,62,10,112,105,101,99,101,115,32,111,102,99,111,109, +112,101,116,105,110,103,82,101,102,101,114,101,110,99,101,116,101,110,110,101, +115,115,101,101,119,104,105,99,104,32,104,97,115,32,118,101,114,115,105,111,110, +61,60,47,115,112,97,110,62,32,60,60,47,104,101,97,100,101,114,62,103,105,118,101 +,115,32,116,104,101,104,105,115,116,111,114,105,97,110,118,97,108,117,101,61,34, +34,62,112,97,100,100,105,110,103,58,48,118,105,101,119,32,116,104,97,116,116,111 +,103,101,116,104,101,114,44,116,104,101,32,109,111,115,116,32,119,97,115,32,102, +111,117,110,100,115,117,98,115,101,116,32,111,102,97,116,116,97,99,107,32,111, +110,99,104,105,108,100,114,101,110,44,112,111,105,110,116,115,32,111,102,112,101 +,114,115,111,110,97,108,32,112,111,115,105,116,105,111,110,58,97,108,108,101,103 +,101,100,108,121,67,108,101,118,101,108,97,110,100,119,97,115,32,108,97,116,101, +114,97,110,100,32,97,102,116,101,114,97,114,101,32,103,105,118,101,110,119,97, +115,32,115,116,105,108,108,115,99,114,111,108,108,105,110,103,100,101,115,105, +103,110,32,111,102,109,97,107,101,115,32,116,104,101,109,117,99,104,32,108,101, +115,115,65,109,101,114,105,99,97,110,115,46,10,10,65,102,116,101,114,32,44,32,98 +,117,116,32,116,104,101,77,117,115,101,117,109,32,111,102,108,111,117,105,115, +105,97,110,97,40,102,114,111,109,32,116,104,101,109,105,110,110,101,115,111,116, +97,112,97,114,116,105,99,108,101,115,97,32,112,114,111,99,101,115,115,68,111,109 +,105,110,105,99,97,110,118,111,108,117,109,101,32,111,102,114,101,116,117,114, +110,105,110,103,100,101,102,101,110,115,105,118,101,48,48,112,120,124,114,105, +103,104,109,97,100,101,32,102,114,111,109,109,111,117,115,101,111,118,101,114,34 +,32,115,116,121,108,101,61,34,115,116,97,116,101,115,32,111,102,40,119,104,105, +99,104,32,105,115,99,111,110,116,105,110,117,101,115,70,114,97,110,99,105,115,99 +,111,98,117,105,108,100,105,110,103,32,119,105,116,104,111,117,116,32,97,119,105 +,116,104,32,115,111,109,101,119,104,111,32,119,111,117,108,100,97,32,102,111,114 +,109,32,111,102,97,32,112,97,114,116,32,111,102,98,101,102,111,114,101,32,105, +116,107,110,111,119,110,32,97,115,32,32,83,101,114,118,105,99,101,115,108,111,99 +,97,116,105,111,110,32,97,110,100,32,111,102,116,101,110,109,101,97,115,117,114, +105,110,103,97,110,100,32,105,116,32,105,115,112,97,112,101,114,98,97,99,107,118 +,97,108,117,101,115,32,111,102,13,10,60,116,105,116,108,101,62,61,32,119,105,110 +,100,111,119,46,100,101,116,101,114,109,105,110,101,101,114,38,113,117,111,116, +59,32,112,108,97,121,101,100,32,98,121,97,110,100,32,101,97,114,108,121,60,47,99 +,101,110,116,101,114,62,102,114,111,109,32,116,104,105,115,116,104,101,32,116, +104,114,101,101,112,111,119,101,114,32,97,110,100,111,102,32,38,113,117,111,116, +59,105,110,110,101,114,72,84,77,76,60,97,32,104,114,101,102,61,34,121,58,105,110 +,108,105,110,101,59,67,104,117,114,99,104,32,111,102,116,104,101,32,101,118,101, +110,116,118,101,114,121,32,104,105,103,104,111,102,102,105,99,105,97,108,32,45, +104,101,105,103,104,116,58,32,99,111,110,116,101,110,116,61,34,47,99,103,105,45, +98,105,110,47,116,111,32,99,114,101,97,116,101,97,102,114,105,107,97,97,110,115, +101,115,112,101,114,97,110,116,111,102,114,97,110,195,167,97,105,115,108,97,116, +118,105,101,197,161,117,108,105,101,116,117,118,105,197,179,196,140,101,197,161, +116,105,110,97,196,141,101,197,161,116,105,110,97,224,185,132,224,184,151,224, +184,162,230,151,165,230,156,172,232,170,158,231,174,128,228,189,147,229,173,151, +231,185,129,233,171,148,229,173,151,237,149,156,234,181,173,236,150,180,228,184, +186,228,187,128,228,185,136,232,174,161,231,174,151,230,156,186,231,172,148,232, +174,176,230,156,172,232,168,142,232,171,150,229,141,128,230,156,141,229,138,161, +229,153,168,228,186,146,232,129,148,231,189,145,230,136,191,229,156,176,228,186, +167,228,191,177,228,185,144,233,131,168,229,135,186,231,137,136,231,164,190,230, +142,146,232,161,140,230,166,156,233,131,168,232,144,189,230,160,188,232,191,155, +228,184,128,230,173,165,230,148,175,228,187,152,229,174,157,233,170,140,232,175, +129,231,160,129,229,167,148,229,145,152,228,188,154,230,149,176,230,141,174,229, +186,147,230,182,136,232,180,185,232,128,133,229,138,158,229,133,172,229,174,164, +232,174,168,232,174,186,229,140,186,230,183,177,229,156,179,229,184,130,230,146, +173,230,148,190,229,153,168,229,140,151,228,186,172,229,184,130,229,164,167,229, +173,166,231,148,159,232,182,138,230,157,165,232,182,138,231,174,161,231,144,134, +229,145,152,228,191,161,230,129,175,231,189,145,115,101,114,118,105,99,105,111, +115,97,114,116,195,173,99,117,108,111,97,114,103,101,110,116,105,110,97,98,97, +114,99,101,108,111,110,97,99,117,97,108,113,117,105,101,114,112,117,98,108,105, +99,97,100,111,112,114,111,100,117,99,116,111,115,112,111,108,195,173,116,105,99, +97,114,101,115,112,117,101,115,116,97,119,105,107,105,112,101,100,105,97,115,105 +,103,117,105,101,110,116,101,98,195,186,115,113,117,101,100,97,99,111,109,117, +110,105,100,97,100,115,101,103,117,114,105,100,97,100,112,114,105,110,99,105,112 +,97,108,112,114,101,103,117,110,116,97,115,99,111,110,116,101,110,105,100,111, +114,101,115,112,111,110,100,101,114,118,101,110,101,122,117,101,108,97,112,114, +111,98,108,101,109,97,115,100,105,99,105,101,109,98,114,101,114,101,108,97,99, +105,195,179,110,110,111,118,105,101,109,98,114,101,115,105,109,105,108,97,114, +101,115,112,114,111,121,101,99,116,111,115,112,114,111,103,114,97,109,97,115,105 +,110,115,116,105,116,117,116,111,97,99,116,105,118,105,100,97,100,101,110,99,117 +,101,110,116,114,97,101,99,111,110,111,109,195,173,97,105,109,195,161,103,101, +110,101,115,99,111,110,116,97,99,116,97,114,100,101,115,99,97,114,103,97,114,110 +,101,99,101,115,97,114,105,111,97,116,101,110,99,105,195,179,110,116,101,108,195 +,169,102,111,110,111,99,111,109,105,115,105,195,179,110,99,97,110,99,105,111,110 +,101,115,99,97,112,97,99,105,100,97,100,101,110,99,111,110,116,114,97,114,97,110 +,195,161,108,105,115,105,115,102,97,118,111,114,105,116,111,115,116,195,169,114, +109,105,110,111,115,112,114,111,118,105,110,99,105,97,101,116,105,113,117,101, +116,97,115,101,108,101,109,101,110,116,111,115,102,117,110,99,105,111,110,101, +115,114,101,115,117,108,116,97,100,111,99,97,114,195,161,99,116,101,114,112,114, +111,112,105,101,100,97,100,112,114,105,110,99,105,112,105,111,110,101,99,101,115 +,105,100,97,100,109,117,110,105,99,105,112,97,108,99,114,101,97,99,105,195,179, +110,100,101,115,99,97,114,103,97,115,112,114,101,115,101,110,99,105,97,99,111, +109,101,114,99,105,97,108,111,112,105,110,105,111,110,101,115,101,106,101,114,99 +,105,99,105,111,101,100,105,116,111,114,105,97,108,115,97,108,97,109,97,110,99, +97,103,111,110,122,195,161,108,101,122,100,111,99,117,109,101,110,116,111,112, +101,108,195,173,99,117,108,97,114,101,99,105,101,110,116,101,115,103,101,110,101 +,114,97,108,101,115,116,97,114,114,97,103,111,110,97,112,114,195,161,99,116,105, +99,97,110,111,118,101,100,97,100,101,115,112,114,111,112,117,101,115,116,97,112, +97,99,105,101,110,116,101,115,116,195,169,99,110,105,99,97,115,111,98,106,101, +116,105,118,111,115,99,111,110,116,97,99,116,111,115,224,164,174,224,165,135,224 +,164,130,224,164,178,224,164,191,224,164,143,224,164,185,224,165,136,224,164,130 +,224,164,151,224,164,175,224,164,190,224,164,184,224,164,190,224,164,165,224,164 +,143,224,164,181,224,164,130,224,164,176,224,164,185,224,165,135,224,164,149,224 +,165,139,224,164,136,224,164,149,224,165,129,224,164,155,224,164,176,224,164,185 +,224,164,190,224,164,172,224,164,190,224,164,166,224,164,149,224,164,185,224,164 +,190,224,164,184,224,164,173,224,165,128,224,164,185,224,165,129,224,164,143,224 +,164,176,224,164,185,224,165,128,224,164,174,224,165,136,224,164,130,224,164,166 +,224,164,191,224,164,168,224,164,172,224,164,190,224,164,164,100,105,112,108,111 +,100,111,99,115,224,164,184,224,164,174,224,164,175,224,164,176,224,165,130,224, +164,170,224,164,168,224,164,190,224,164,174,224,164,170,224,164,164,224,164,190, +224,164,171,224,164,191,224,164,176,224,164,148,224,164,184,224,164,164,224,164, +164,224,164,176,224,164,185,224,164,178,224,165,139,224,164,151,224,164,185,224, +165,129,224,164,134,224,164,172,224,164,190,224,164,176,224,164,166,224,165,135, +224,164,182,224,164,185,224,165,129,224,164,136,224,164,150,224,165,135,224,164, +178,224,164,175,224,164,166,224,164,191,224,164,149,224,164,190,224,164,174,224, +164,181,224,165,135,224,164,172,224,164,164,224,165,128,224,164,168,224,164,172, +224,165,128,224,164,154,224,164,174,224,165,140,224,164,164,224,164,184,224,164, +190,224,164,178,224,164,178,224,165,135,224,164,150,224,164,156,224,165,137,224, +164,172,224,164,174,224,164,166,224,164,166,224,164,164,224,164,165,224,164,190, +224,164,168,224,164,185,224,165,128,224,164,182,224,164,185,224,164,176,224,164, +133,224,164,178,224,164,151,224,164,149,224,164,173,224,165,128,224,164,168,224, +164,151,224,164,176,224,164,170,224,164,190,224,164,184,224,164,176,224,164,190, +224,164,164,224,164,149,224,164,191,224,164,143,224,164,137,224,164,184,224,165, +135,224,164,151,224,164,175,224,165,128,224,164,185,224,165,130,224,164,129,224, +164,134,224,164,151,224,165,135,224,164,159,224,165,128,224,164,174,224,164,150, +224,165,139,224,164,156,224,164,149,224,164,190,224,164,176,224,164,133,224,164, +173,224,165,128,224,164,151,224,164,175,224,165,135,224,164,164,224,165,129,224, +164,174,224,164,181,224,165,139,224,164,159,224,164,166,224,165,135,224,164,130, +224,164,133,224,164,151,224,164,176,224,164,144,224,164,184,224,165,135,224,164, +174,224,165,135,224,164,178,224,164,178,224,164,151,224,164,190,224,164,185,224, +164,190,224,164,178,224,164,138,224,164,170,224,164,176,224,164,154,224,164,190, +224,164,176,224,164,144,224,164,184,224,164,190,224,164,166,224,165,135,224,164, +176,224,164,156,224,164,191,224,164,184,224,164,166,224,164,191,224,164,178,224, +164,172,224,164,130,224,164,166,224,164,172,224,164,168,224,164,190,224,164,185, +224,165,130,224,164,130,224,164,178,224,164,190,224,164,150,224,164,156,224,165, +128,224,164,164,224,164,172,224,164,159,224,164,168,224,164,174,224,164,191,224, +164,178,224,164,135,224,164,184,224,165,135,224,164,134,224,164,168,224,165,135, +224,164,168,224,164,175,224,164,190,224,164,149,224,165,129,224,164,178,224,164, +178,224,165,137,224,164,151,224,164,173,224,164,190,224,164,151,224,164,176,224, +165,135,224,164,178,224,164,156,224,164,151,224,164,185,224,164,176,224,164,190, +224,164,174,224,164,178,224,164,151,224,165,135,224,164,170,224,165,135,224,164, +156,224,164,185,224,164,190,224,164,165,224,164,135,224,164,184,224,165,128,224, +164,184,224,164,185,224,165,128,224,164,149,224,164,178,224,164,190,224,164,160, +224,165,128,224,164,149,224,164,185,224,164,190,224,164,129,224,164,166,224,165, +130,224,164,176,224,164,164,224,164,185,224,164,164,224,164,184,224,164,190,224, +164,164,224,164,175,224,164,190,224,164,166,224,164,134,224,164,175,224,164,190, +224,164,170,224,164,190,224,164,149,224,164,149,224,165,140,224,164,168,224,164, +182,224,164,190,224,164,174,224,164,166,224,165,135,224,164,150,224,164,175,224, +164,185,224,165,128,224,164,176,224,164,190,224,164,175,224,164,150,224,165,129, +224,164,166,224,164,178,224,164,151,224,165,128,99,97,116,101,103,111,114,105, +101,115,101,120,112,101,114,105,101,110,99,101,60,47,116,105,116,108,101,62,13, +10,67,111,112,121,114,105,103,104,116,32,106,97,118,97,115,99,114,105,112,116,99 +,111,110,100,105,116,105,111,110,115,101,118,101,114,121,116,104,105,110,103,60, +112,32,99,108,97,115,115,61,34,116,101,99,104,110,111,108,111,103,121,98,97,99, +107,103,114,111,117,110,100,60,97,32,99,108,97,115,115,61,34,109,97,110,97,103, +101,109,101,110,116,38,99,111,112,121,59,32,50,48,49,106,97,118,97,83,99,114,105 +,112,116,99,104,97,114,97,99,116,101,114,115,98,114,101,97,100,99,114,117,109,98 +,116,104,101,109,115,101,108,118,101,115,104,111,114,105,122,111,110,116,97,108, +103,111,118,101,114,110,109,101,110,116,67,97,108,105,102,111,114,110,105,97,97, +99,116,105,118,105,116,105,101,115,100,105,115,99,111,118,101,114,101,100,78,97, +118,105,103,97,116,105,111,110,116,114,97,110,115,105,116,105,111,110,99,111,110 +,110,101,99,116,105,111,110,110,97,118,105,103,97,116,105,111,110,97,112,112,101 +,97,114,97,110,99,101,60,47,116,105,116,108,101,62,60,109,99,104,101,99,107,98, +111,120,34,32,116,101,99,104,110,105,113,117,101,115,112,114,111,116,101,99,116, +105,111,110,97,112,112,97,114,101,110,116,108,121,97,115,32,119,101,108,108,32, +97,115,117,110,116,39,44,32,39,85,65,45,114,101,115,111,108,117,116,105,111,110, +111,112,101,114,97,116,105,111,110,115,116,101,108,101,118,105,115,105,111,110, +116,114,97,110,115,108,97,116,101,100,87,97,115,104,105,110,103,116,111,110,110, +97,118,105,103,97,116,111,114,46,32,61,32,119,105,110,100,111,119,46,105,109,112 +,114,101,115,115,105,111,110,38,108,116,59,98,114,38,103,116,59,108,105,116,101, +114,97,116,117,114,101,112,111,112,117,108,97,116,105,111,110,98,103,99,111,108, +111,114,61,34,35,101,115,112,101,99,105,97,108,108,121,32,99,111,110,116,101,110 +,116,61,34,112,114,111,100,117,99,116,105,111,110,110,101,119,115,108,101,116, +116,101,114,112,114,111,112,101,114,116,105,101,115,100,101,102,105,110,105,116, +105,111,110,108,101,97,100,101,114,115,104,105,112,84,101,99,104,110,111,108,111 +,103,121,80,97,114,108,105,97,109,101,110,116,99,111,109,112,97,114,105,115,111, +110,117,108,32,99,108,97,115,115,61,34,46,105,110,100,101,120,79,102,40,34,99, +111,110,99,108,117,115,105,111,110,100,105,115,99,117,115,115,105,111,110,99,111 +,109,112,111,110,101,110,116,115,98,105,111,108,111,103,105,99,97,108,82,101,118 +,111,108,117,116,105,111,110,95,99,111,110,116,97,105,110,101,114,117,110,100, +101,114,115,116,111,111,100,110,111,115,99,114,105,112,116,62,60,112,101,114,109 +,105,115,115,105,111,110,101,97,99,104,32,111,116,104,101,114,97,116,109,111,115 +,112,104,101,114,101,32,111,110,102,111,99,117,115,61,34,60,102,111,114,109,32, +105,100,61,34,112,114,111,99,101,115,115,105,110,103,116,104,105,115,46,118,97, +108,117,101,103,101,110,101,114,97,116,105,111,110,67,111,110,102,101,114,101, +110,99,101,115,117,98,115,101,113,117,101,110,116,119,101,108,108,45,107,110,111 +,119,110,118,97,114,105,97,116,105,111,110,115,114,101,112,117,116,97,116,105, +111,110,112,104,101,110,111,109,101,110,111,110,100,105,115,99,105,112,108,105, +110,101,108,111,103,111,46,112,110,103,34,32,40,100,111,99,117,109,101,110,116, +44,98,111,117,110,100,97,114,105,101,115,101,120,112,114,101,115,115,105,111,110 +,115,101,116,116,108,101,109,101,110,116,66,97,99,107,103,114,111,117,110,100, +111,117,116,32,111,102,32,116,104,101,101,110,116,101,114,112,114,105,115,101,40 +,34,104,116,116,112,115,58,34,32,117,110,101,115,99,97,112,101,40,34,112,97,115, +115,119,111,114,100,34,32,100,101,109,111,99,114,97,116,105,99,60,97,32,104,114, +101,102,61,34,47,119,114,97,112,112,101,114,34,62,10,109,101,109,98,101,114,115, +104,105,112,108,105,110,103,117,105,115,116,105,99,112,120,59,112,97,100,100,105 +,110,103,112,104,105,108,111,115,111,112,104,121,97,115,115,105,115,116,97,110, +99,101,117,110,105,118,101,114,115,105,116,121,102,97,99,105,108,105,116,105,101 +,115,114,101,99,111,103,110,105,122,101,100,112,114,101,102,101,114,101,110,99, +101,105,102,32,40,116,121,112,101,111,102,109,97,105,110,116,97,105,110,101,100, +118,111,99,97,98,117,108,97,114,121,104,121,112,111,116,104,101,115,105,115,46, +115,117,98,109,105,116,40,41,59,38,97,109,112,59,110,98,115,112,59,97,110,110, +111,116,97,116,105,111,110,98,101,104,105,110,100,32,116,104,101,70,111,117,110, +100,97,116,105,111,110,112,117,98,108,105,115,104,101,114,34,97,115,115,117,109, +112,116,105,111,110,105,110,116,114,111,100,117,99,101,100,99,111,114,114,117, +112,116,105,111,110,115,99,105,101,110,116,105,115,116,115,101,120,112,108,105, +99,105,116,108,121,105,110,115,116,101,97,100,32,111,102,100,105,109,101,110,115 +,105,111,110,115,32,111,110,67,108,105,99,107,61,34,99,111,110,115,105,100,101, +114,101,100,100,101,112,97,114,116,109,101,110,116,111,99,99,117,112,97,116,105, +111,110,115,111,111,110,32,97,102,116,101,114,105,110,118,101,115,116,109,101, +110,116,112,114,111,110,111,117,110,99,101,100,105,100,101,110,116,105,102,105, +101,100,101,120,112,101,114,105,109,101,110,116,77,97,110,97,103,101,109,101,110 +,116,103,101,111,103,114,97,112,104,105,99,34,32,104,101,105,103,104,116,61,34, +108,105,110,107,32,114,101,108,61,34,46,114,101,112,108,97,99,101,40,47,100,101, +112,114,101,115,115,105,111,110,99,111,110,102,101,114,101,110,99,101,112,117, +110,105,115,104,109,101,110,116,101,108,105,109,105,110,97,116,101,100,114,101, +115,105,115,116,97,110,99,101,97,100,97,112,116,97,116,105,111,110,111,112,112, +111,115,105,116,105,111,110,119,101,108,108,32,107,110,111,119,110,115,117,112, +112,108,101,109,101,110,116,100,101,116,101,114,109,105,110,101,100,104,49,32,99 +,108,97,115,115,61,34,48,112,120,59,109,97,114,103,105,110,109,101,99,104,97,110 +,105,99,97,108,115,116,97,116,105,115,116,105,99,115,99,101,108,101,98,114,97, +116,101,100,71,111,118,101,114,110,109,101,110,116,10,10,68,117,114,105,110,103, +32,116,100,101,118,101,108,111,112,101,114,115,97,114,116,105,102,105,99,105,97, +108,101,113,117,105,118,97,108,101,110,116,111,114,105,103,105,110,97,116,101, +100,67,111,109,109,105,115,115,105,111,110,97,116,116,97,99,104,109,101,110,116, +60,115,112,97,110,32,105,100,61,34,116,104,101,114,101,32,119,101,114,101,78,101 +,100,101,114,108,97,110,100,115,98,101,121,111,110,100,32,116,104,101,114,101, +103,105,115,116,101,114,101,100,106,111,117,114,110,97,108,105,115,116,102,114, +101,113,117,101,110,116,108,121,97,108,108,32,111,102,32,116,104,101,108,97,110, +103,61,34,101,110,34,32,60,47,115,116,121,108,101,62,13,10,97,98,115,111,108,117 +,116,101,59,32,115,117,112,112,111,114,116,105,110,103,101,120,116,114,101,109, +101,108,121,32,109,97,105,110,115,116,114,101,97,109,60,47,115,116,114,111,110, +103,62,32,112,111,112,117,108,97,114,105,116,121,101,109,112,108,111,121,109,101 +,110,116,60,47,116,97,98,108,101,62,13,10,32,99,111,108,115,112,97,110,61,34,60, +47,102,111,114,109,62,10,32,32,99,111,110,118,101,114,115,105,111,110,97,98,111, +117,116,32,116,104,101,32,60,47,112,62,60,47,100,105,118,62,105,110,116,101,103, +114,97,116,101,100,34,32,108,97,110,103,61,34,101,110,80,111,114,116,117,103,117 +,101,115,101,115,117,98,115,116,105,116,117,116,101,105,110,100,105,118,105,100, +117,97,108,105,109,112,111,115,115,105,98,108,101,109,117,108,116,105,109,101, +100,105,97,97,108,109,111,115,116,32,97,108,108,112,120,32,115,111,108,105,100, +32,35,97,112,97,114,116,32,102,114,111,109,115,117,98,106,101,99,116,32,116,111, +105,110,32,69,110,103,108,105,115,104,99,114,105,116,105,99,105,122,101,100,101, +120,99,101,112,116,32,102,111,114,103,117,105,100,101,108,105,110,101,115,111, +114,105,103,105,110,97,108,108,121,114,101,109,97,114,107,97,98,108,101,116,104, +101,32,115,101,99,111,110,100,104,50,32,99,108,97,115,115,61,34,60,97,32,116,105 +,116,108,101,61,34,40,105,110,99,108,117,100,105,110,103,112,97,114,97,109,101, +116,101,114,115,112,114,111,104,105,98,105,116,101,100,61,32,34,104,116,116,112, +58,47,47,100,105,99,116,105,111,110,97,114,121,112,101,114,99,101,112,116,105, +111,110,114,101,118,111,108,117,116,105,111,110,102,111,117,110,100,97,116,105, +111,110,112,120,59,104,101,105,103,104,116,58,115,117,99,99,101,115,115,102,117, +108,115,117,112,112,111,114,116,101,114,115,109,105,108,108,101,110,110,105,117, +109,104,105,115,32,102,97,116,104,101,114,116,104,101,32,38,113,117,111,116,59, +110,111,45,114,101,112,101,97,116,59,99,111,109,109,101,114,99,105,97,108,105, +110,100,117,115,116,114,105,97,108,101,110,99,111,117,114,97,103,101,100,97,109, +111,117,110,116,32,111,102,32,117,110,111,102,102,105,99,105,97,108,101,102,102, +105,99,105,101,110,99,121,82,101,102,101,114,101,110,99,101,115,99,111,111,114, +100,105,110,97,116,101,100,105,115,99,108,97,105,109,101,114,101,120,112,101,100 +,105,116,105,111,110,100,101,118,101,108,111,112,105,110,103,99,97,108,99,117, +108,97,116,101,100,115,105,109,112,108,105,102,105,101,100,108,101,103,105,116, +105,109,97,116,101,115,117,98,115,116,114,105,110,103,40,48,34,32,99,108,97,115, +115,61,34,99,111,109,112,108,101,116,101,108,121,105,108,108,117,115,116,114,97, +116,101,102,105,118,101,32,121,101,97,114,115,105,110,115,116,114,117,109,101, +110,116,80,117,98,108,105,115,104,105,110,103,49,34,32,99,108,97,115,115,61,34, +112,115,121,99,104,111,108,111,103,121,99,111,110,102,105,100,101,110,99,101,110 +,117,109,98,101,114,32,111,102,32,97,98,115,101,110,99,101,32,111,102,102,111,99 +,117,115,101,100,32,111,110,106,111,105,110,101,100,32,116,104,101,115,116,114, +117,99,116,117,114,101,115,112,114,101,118,105,111,117,115,108,121,62,60,47,105, +102,114,97,109,101,62,111,110,99,101,32,97,103,97,105,110,98,117,116,32,114,97, +116,104,101,114,105,109,109,105,103,114,97,110,116,115,111,102,32,99,111,117,114 +,115,101,44,97,32,103,114,111,117,112,32,111,102,76,105,116,101,114,97,116,117, +114,101,85,110,108,105,107,101,32,116,104,101,60,47,97,62,38,110,98,115,112,59, +10,102,117,110,99,116,105,111,110,32,105,116,32,119,97,115,32,116,104,101,67,111 +,110,118,101,110,116,105,111,110,97,117,116,111,109,111,98,105,108,101,80,114, +111,116,101,115,116,97,110,116,97,103,103,114,101,115,115,105,118,101,97,102,116 +,101,114,32,116,104,101,32,83,105,109,105,108,97,114,108,121,44,34,32,47,62,60, +47,100,105,118,62,99,111,108,108,101,99,116,105,111,110,13,10,102,117,110,99,116 +,105,111,110,118,105,115,105,98,105,108,105,116,121,116,104,101,32,117,115,101, +32,111,102,118,111,108,117,110,116,101,101,114,115,97,116,116,114,97,99,116,105, +111,110,117,110,100,101,114,32,116,104,101,32,116,104,114,101,97,116,101,110,101 +,100,42,60,33,91,67,68,65,84,65,91,105,109,112,111,114,116,97,110,99,101,105,110 +,32,103,101,110,101,114,97,108,116,104,101,32,108,97,116,116,101,114,60,47,102, +111,114,109,62,10,60,47,46,105,110,100,101,120,79,102,40,39,105,32,61,32,48,59, +32,105,32,60,100,105,102,102,101,114,101,110,99,101,100,101,118,111,116,101,100, +32,116,111,116,114,97,100,105,116,105,111,110,115,115,101,97,114,99,104,32,102, +111,114,117,108,116,105,109,97,116,101,108,121,116,111,117,114,110,97,109,101, +110,116,97,116,116,114,105,98,117,116,101,115,115,111,45,99,97,108,108,101,100, +32,125,10,60,47,115,116,121,108,101,62,101,118,97,108,117,97,116,105,111,110,101 +,109,112,104,97,115,105,122,101,100,97,99,99,101,115,115,105,98,108,101,60,47, +115,101,99,116,105,111,110,62,115,117,99,99,101,115,115,105,111,110,97,108,111, +110,103,32,119,105,116,104,77,101,97,110,119,104,105,108,101,44,105,110,100,117, +115,116,114,105,101,115,60,47,97,62,60,98,114,32,47,62,104,97,115,32,98,101,99, +111,109,101,97,115,112,101,99,116,115,32,111,102,84,101,108,101,118,105,115,105, +111,110,115,117,102,102,105,99,105,101,110,116,98,97,115,107,101,116,98,97,108, +108,98,111,116,104,32,115,105,100,101,115,99,111,110,116,105,110,117,105,110,103 +,97,110,32,97,114,116,105,99,108,101,60,105,109,103,32,97,108,116,61,34,97,100, +118,101,110,116,117,114,101,115,104,105,115,32,109,111,116,104,101,114,109,97, +110,99,104,101,115,116,101,114,112,114,105,110,99,105,112,108,101,115,112,97,114 +,116,105,99,117,108,97,114,99,111,109,109,101,110,116,97,114,121,101,102,102,101 +,99,116,115,32,111,102,100,101,99,105,100,101,100,32,116,111,34,62,60,115,116, +114,111,110,103,62,112,117,98,108,105,115,104,101,114,115,74,111,117,114,110,97, +108,32,111,102,100,105,102,102,105,99,117,108,116,121,102,97,99,105,108,105,116, +97,116,101,97,99,99,101,112,116,97,98,108,101,115,116,121,108,101,46,99,115,115, +34,9,102,117,110,99,116,105,111,110,32,105,110,110,111,118,97,116,105,111,110,62 +,67,111,112,121,114,105,103,104,116,115,105,116,117,97,116,105,111,110,115,119, +111,117,108,100,32,104,97,118,101,98,117,115,105,110,101,115,115,101,115,68,105, +99,116,105,111,110,97,114,121,115,116,97,116,101,109,101,110,116,115,111,102,116 +,101,110,32,117,115,101,100,112,101,114,115,105,115,116,101,110,116,105,110,32, +74,97,110,117,97,114,121,99,111,109,112,114,105,115,105,110,103,60,47,116,105, +116,108,101,62,10,9,100,105,112,108,111,109,97,116,105,99,99,111,110,116,97,105, +110,105,110,103,112,101,114,102,111,114,109,105,110,103,101,120,116,101,110,115, +105,111,110,115,109,97,121,32,110,111,116,32,98,101,99,111,110,99,101,112,116,32 +,111,102,32,111,110,99,108,105,99,107,61,34,73,116,32,105,115,32,97,108,115,111, +102,105,110,97,110,99,105,97,108,32,109,97,107,105,110,103,32,116,104,101,76,117 +,120,101,109,98,111,117,114,103,97,100,100,105,116,105,111,110,97,108,97,114,101 +,32,99,97,108,108,101,100,101,110,103,97,103,101,100,32,105,110,34,115,99,114, +105,112,116,34,41,59,98,117,116,32,105,116,32,119,97,115,101,108,101,99,116,114, +111,110,105,99,111,110,115,117,98,109,105,116,61,34,10,60,33,45,45,32,69,110,100 +,32,101,108,101,99,116,114,105,99,97,108,111,102,102,105,99,105,97,108,108,121, +115,117,103,103,101,115,116,105,111,110,116,111,112,32,111,102,32,116,104,101, +117,110,108,105,107,101,32,116,104,101,65,117,115,116,114,97,108,105,97,110,79, +114,105,103,105,110,97,108,108,121,114,101,102,101,114,101,110,99,101,115,10,60, +47,104,101,97,100,62,13,10,114,101,99,111,103,110,105,115,101,100,105,110,105, +116,105,97,108,105,122,101,108,105,109,105,116,101,100,32,116,111,65,108,101,120 +,97,110,100,114,105,97,114,101,116,105,114,101,109,101,110,116,65,100,118,101, +110,116,117,114,101,115,102,111,117,114,32,121,101,97,114,115,10,10,38,108,116, +59,33,45,45,32,105,110,99,114,101,97,115,105,110,103,100,101,99,111,114,97,116, +105,111,110,104,51,32,99,108,97,115,115,61,34,111,114,105,103,105,110,115,32,111 +,102,111,98,108,105,103,97,116,105,111,110,114,101,103,117,108,97,116,105,111, +110,99,108,97,115,115,105,102,105,101,100,40,102,117,110,99,116,105,111,110,40, +97,100,118,97,110,116,97,103,101,115,98,101,105,110,103,32,116,104,101,32,104, +105,115,116,111,114,105,97,110,115,60,98,97,115,101,32,104,114,101,102,114,101, +112,101,97,116,101,100,108,121,119,105,108,108,105,110,103,32,116,111,99,111,109 +,112,97,114,97,98,108,101,100,101,115,105,103,110,97,116,101,100,110,111,109,105 +,110,97,116,105,111,110,102,117,110,99,116,105,111,110,97,108,105,110,115,105, +100,101,32,116,104,101,114,101,118,101,108,97,116,105,111,110,101,110,100,32,111 +,102,32,116,104,101,115,32,102,111,114,32,116,104,101,32,97,117,116,104,111,114, +105,122,101,100,114,101,102,117,115,101,100,32,116,111,116,97,107,101,32,112,108 +,97,99,101,97,117,116,111,110,111,109,111,117,115,99,111,109,112,114,111,109,105 +,115,101,112,111,108,105,116,105,99,97,108,32,114,101,115,116,97,117,114,97,110, +116,116,119,111,32,111,102,32,116,104,101,70,101,98,114,117,97,114,121,32,50,113 +,117,97,108,105,116,121,32,111,102,115,119,102,111,98,106,101,99,116,46,117,110, +100,101,114,115,116,97,110,100,110,101,97,114,108,121,32,97,108,108,119,114,105, +116,116,101,110,32,98,121,105,110,116,101,114,118,105,101,119,115,34,32,119,105, +100,116,104,61,34,49,119,105,116,104,100,114,97,119,97,108,102,108,111,97,116,58 +,108,101,102,116,105,115,32,117,115,117,97,108,108,121,99,97,110,100,105,100,97, +116,101,115,110,101,119,115,112,97,112,101,114,115,109,121,115,116,101,114,105, +111,117,115,68,101,112,97,114,116,109,101,110,116,98,101,115,116,32,107,110,111, +119,110,112,97,114,108,105,97,109,101,110,116,115,117,112,112,114,101,115,115, +101,100,99,111,110,118,101,110,105,101,110,116,114,101,109,101,109,98,101,114, +101,100,100,105,102,102,101,114,101,110,116,32,115,121,115,116,101,109,97,116, +105,99,104,97,115,32,108,101,100,32,116,111,112,114,111,112,97,103,97,110,100,97 +,99,111,110,116,114,111,108,108,101,100,105,110,102,108,117,101,110,99,101,115, +99,101,114,101,109,111,110,105,97,108,112,114,111,99,108,97,105,109,101,100,80, +114,111,116,101,99,116,105,111,110,108,105,32,99,108,97,115,115,61,34,83,99,105, +101,110,116,105,102,105,99,99,108,97,115,115,61,34,110,111,45,116,114,97,100,101 +,109,97,114,107,115,109,111,114,101,32,116,104,97,110,32,119,105,100,101,115,112 +,114,101,97,100,76,105,98,101,114,97,116,105,111,110,116,111,111,107,32,112,108, +97,99,101,100,97,121,32,111,102,32,116,104,101,97,115,32,108,111,110,103,32,97, +115,105,109,112,114,105,115,111,110,101,100,65,100,100,105,116,105,111,110,97, +108,10,60,104,101,97,100,62,10,60,109,76,97,98,111,114,97,116,111,114,121,78,111 +,118,101,109,98,101,114,32,50,101,120,99,101,112,116,105,111,110,115,73,110,100, +117,115,116,114,105,97,108,118,97,114,105,101,116,121,32,111,102,102,108,111,97, +116,58,32,108,101,102,68,117,114,105,110,103,32,116,104,101,97,115,115,101,115, +115,109,101,110,116,104,97,118,101,32,98,101,101,110,32,100,101,97,108,115,32, +119,105,116,104,83,116,97,116,105,115,116,105,99,115,111,99,99,117,114,114,101, +110,99,101,47,117,108,62,60,47,100,105,118,62,99,108,101,97,114,102,105,120,34, +62,116,104,101,32,112,117,98,108,105,99,109,97,110,121,32,121,101,97,114,115,119 +,104,105,99,104,32,119,101,114,101,111,118,101,114,32,116,105,109,101,44,115,121 +,110,111,110,121,109,111,117,115,99,111,110,116,101,110,116,34,62,10,112,114,101 +,115,117,109,97,98,108,121,104,105,115,32,102,97,109,105,108,121,117,115,101,114 +,65,103,101,110,116,46,117,110,101,120,112,101,99,116,101,100,105,110,99,108,117 +,100,105,110,103,32,99,104,97,108,108,101,110,103,101,100,97,32,109,105,110,111, +114,105,116,121,117,110,100,101,102,105,110,101,100,34,98,101,108,111,110,103, +115,32,116,111,116,97,107,101,110,32,102,114,111,109,105,110,32,79,99,116,111,98 +,101,114,112,111,115,105,116,105,111,110,58,32,115,97,105,100,32,116,111,32,98, +101,114,101,108,105,103,105,111,117,115,32,70,101,100,101,114,97,116,105,111,110 +,32,114,111,119,115,112,97,110,61,34,111,110,108,121,32,97,32,102,101,119,109, +101,97,110,116,32,116,104,97,116,108,101,100,32,116,111,32,116,104,101,45,45,62, +13,10,60,100,105,118,32,60,102,105,101,108,100,115,101,116,62,65,114,99,104,98, +105,115,104,111,112,32,99,108,97,115,115,61,34,110,111,98,101,105,110,103,32,117 +,115,101,100,97,112,112,114,111,97,99,104,101,115,112,114,105,118,105,108,101, +103,101,115,110,111,115,99,114,105,112,116,62,10,114,101,115,117,108,116,115,32, +105,110,109,97,121,32,98,101,32,116,104,101,69,97,115,116,101,114,32,101,103,103 +,109,101,99,104,97,110,105,115,109,115,114,101,97,115,111,110,97,98,108,101,80, +111,112,117,108,97,116,105,111,110,67,111,108,108,101,99,116,105,111,110,115,101 +,108,101,99,116,101,100,34,62,110,111,115,99,114,105,112,116,62,13,47,105,110, +100,101,120,46,112,104,112,97,114,114,105,118,97,108,32,111,102,45,106,115,115, +100,107,39,41,41,59,109,97,110,97,103,101,100,32,116,111,105,110,99,111,109,112, +108,101,116,101,99,97,115,117,97,108,116,105,101,115,99,111,109,112,108,101,116, +105,111,110,67,104,114,105,115,116,105,97,110,115,83,101,112,116,101,109,98,101, +114,32,97,114,105,116,104,109,101,116,105,99,112,114,111,99,101,100,117,114,101, +115,109,105,103,104,116,32,104,97,118,101,80,114,111,100,117,99,116,105,111,110, +105,116,32,97,112,112,101,97,114,115,80,104,105,108,111,115,111,112,104,121,102, +114,105,101,110,100,115,104,105,112,108,101,97,100,105,110,103,32,116,111,103, +105,118,105,110,103,32,116,104,101,116,111,119,97,114,100,32,116,104,101,103,117 +,97,114,97,110,116,101,101,100,100,111,99,117,109,101,110,116,101,100,99,111,108 +,111,114,58,35,48,48,48,118,105,100,101,111,32,103,97,109,101,99,111,109,109,105 +,115,115,105,111,110,114,101,102,108,101,99,116,105,110,103,99,104,97,110,103, +101,32,116,104,101,97,115,115,111,99,105,97,116,101,100,115,97,110,115,45,115, +101,114,105,102,111,110,107,101,121,112,114,101,115,115,59,32,112,97,100,100,105 +,110,103,58,72,101,32,119,97,115,32,116,104,101,117,110,100,101,114,108,121,105, +110,103,116,121,112,105,99,97,108,108,121,32,44,32,97,110,100,32,116,104,101,32, +115,114,99,69,108,101,109,101,110,116,115,117,99,99,101,115,115,105,118,101,115, +105,110,99,101,32,116,104,101,32,115,104,111,117,108,100,32,98,101,32,110,101, +116,119,111,114,107,105,110,103,97,99,99,111,117,110,116,105,110,103,117,115,101 +,32,111,102,32,116,104,101,108,111,119,101,114,32,116,104,97,110,115,104,111,119 +,115,32,116,104,97,116,60,47,115,112,97,110,62,10,9,9,99,111,109,112,108,97,105, +110,116,115,99,111,110,116,105,110,117,111,117,115,113,117,97,110,116,105,116, +105,101,115,97,115,116,114,111,110,111,109,101,114,104,101,32,100,105,100,32,110 +,111,116,100,117,101,32,116,111,32,105,116,115,97,112,112,108,105,101,100,32,116 +,111,97,110,32,97,118,101,114,97,103,101,101,102,102,111,114,116,115,32,116,111, +116,104,101,32,102,117,116,117,114,101,97,116,116,101,109,112,116,32,116,111,84, +104,101,114,101,102,111,114,101,44,99,97,112,97,98,105,108,105,116,121,82,101, +112,117,98,108,105,99,97,110,119,97,115,32,102,111,114,109,101,100,69,108,101,99 +,116,114,111,110,105,99,107,105,108,111,109,101,116,101,114,115,99,104,97,108, +108,101,110,103,101,115,112,117,98,108,105,115,104,105,110,103,116,104,101,32, +102,111,114,109,101,114,105,110,100,105,103,101,110,111,117,115,100,105,114,101, +99,116,105,111,110,115,115,117,98,115,105,100,105,97,114,121,99,111,110,115,112, +105,114,97,99,121,100,101,116,97,105,108,115,32,111,102,97,110,100,32,105,110,32 +,116,104,101,97,102,102,111,114,100,97,98,108,101,115,117,98,115,116,97,110,99, +101,115,114,101,97,115,111,110,32,102,111,114,99,111,110,118,101,110,116,105,111 +,110,105,116,101,109,116,121,112,101,61,34,97,98,115,111,108,117,116,101,108,121 +,115,117,112,112,111,115,101,100,108,121,114,101,109,97,105,110,101,100,32,97,97 +,116,116,114,97,99,116,105,118,101,116,114,97,118,101,108,108,105,110,103,115, +101,112,97,114,97,116,101,108,121,102,111,99,117,115,101,115,32,111,110,101,108, +101,109,101,110,116,97,114,121,97,112,112,108,105,99,97,98,108,101,102,111,117, +110,100,32,116,104,97,116,115,116,121,108,101,115,104,101,101,116,109,97,110,117 +,115,99,114,105,112,116,115,116,97,110,100,115,32,102,111,114,32,110,111,45,114, +101,112,101,97,116,40,115,111,109,101,116,105,109,101,115,67,111,109,109,101,114 +,99,105,97,108,105,110,32,65,109,101,114,105,99,97,117,110,100,101,114,116,97, +107,101,110,113,117,97,114,116,101,114,32,111,102,97,110,32,101,120,97,109,112, +108,101,112,101,114,115,111,110,97,108,108,121,105,110,100,101,120,46,112,104, +112,63,60,47,98,117,116,116,111,110,62,10,112,101,114,99,101,110,116,97,103,101, +98,101,115,116,45,107,110,111,119,110,99,114,101,97,116,105,110,103,32,97,34,32, +100,105,114,61,34,108,116,114,76,105,101,117,116,101,110,97,110,116,10,60,100, +105,118,32,105,100,61,34,116,104,101,121,32,119,111,117,108,100,97,98,105,108, +105,116,121,32,111,102,109,97,100,101,32,117,112,32,111,102,110,111,116,101,100, +32,116,104,97,116,99,108,101,97,114,32,116,104,97,116,97,114,103,117,101,32,116, +104,97,116,116,111,32,97,110,111,116,104,101,114,99,104,105,108,100,114,101,110, +39,115,112,117,114,112,111,115,101,32,111,102,102,111,114,109,117,108,97,116,101 +,100,98,97,115,101,100,32,117,112,111,110,116,104,101,32,114,101,103,105,111,110 +,115,117,98,106,101,99,116,32,111,102,112,97,115,115,101,110,103,101,114,115,112 +,111,115,115,101,115,115,105,111,110,46,10,10,73,110,32,116,104,101,32,66,101, +102,111,114,101,32,116,104,101,97,102,116,101,114,119,97,114,100,115,99,117,114, +114,101,110,116,108,121,32,97,99,114,111,115,115,32,116,104,101,115,99,105,101, +110,116,105,102,105,99,99,111,109,109,117,110,105,116,121,46,99,97,112,105,116, +97,108,105,115,109,105,110,32,71,101,114,109,97,110,121,114,105,103,104,116,45, +119,105,110,103,116,104,101,32,115,121,115,116,101,109,83,111,99,105,101,116,121 +,32,111,102,112,111,108,105,116,105,99,105,97,110,100,105,114,101,99,116,105,111 +,110,58,119,101,110,116,32,111,110,32,116,111,114,101,109,111,118,97,108,32,111, +102,32,78,101,119,32,89,111,114,107,32,97,112,97,114,116,109,101,110,116,115,105 +,110,100,105,99,97,116,105,111,110,100,117,114,105,110,103,32,116,104,101,117, +110,108,101,115,115,32,116,104,101,104,105,115,116,111,114,105,99,97,108,104,97, +100,32,98,101,101,110,32,97,100,101,102,105,110,105,116,105,118,101,105,110,103, +114,101,100,105,101,110,116,97,116,116,101,110,100,97,110,99,101,67,101,110,116, +101,114,32,102,111,114,112,114,111,109,105,110,101,110,99,101,114,101,97,100,121 +,83,116,97,116,101,115,116,114,97,116,101,103,105,101,115,98,117,116,32,105,110, +32,116,104,101,97,115,32,112,97,114,116,32,111,102,99,111,110,115,116,105,116, +117,116,101,99,108,97,105,109,32,116,104,97,116,108,97,98,111,114,97,116,111,114 +,121,99,111,109,112,97,116,105,98,108,101,102,97,105,108,117,114,101,32,111,102, +44,32,115,117,99,104,32,97,115,32,98,101,103,97,110,32,119,105,116,104,117,115, +105,110,103,32,116,104,101,32,116,111,32,112,114,111,118,105,100,101,102,101,97, +116,117,114,101,32,111,102,102,114,111,109,32,119,104,105,99,104,47,34,32,99,108 +,97,115,115,61,34,103,101,111,108,111,103,105,99,97,108,115,101,118,101,114,97, +108,32,111,102,100,101,108,105,98,101,114,97,116,101,105,109,112,111,114,116,97, +110,116,32,104,111,108,100,115,32,116,104,97,116,105,110,103,38,113,117,111,116, +59,32,118,97,108,105,103,110,61,116,111,112,116,104,101,32,71,101,114,109,97,110 +,111,117,116,115,105,100,101,32,111,102,110,101,103,111,116,105,97,116,101,100, +104,105,115,32,99,97,114,101,101,114,115,101,112,97,114,97,116,105,111,110,105, +100,61,34,115,101,97,114,99,104,119,97,115,32,99,97,108,108,101,100,116,104,101, +32,102,111,117,114,116,104,114,101,99,114,101,97,116,105,111,110,111,116,104,101 +,114,32,116,104,97,110,112,114,101,118,101,110,116,105,111,110,119,104,105,108, +101,32,116,104,101,32,101,100,117,99,97,116,105,111,110,44,99,111,110,110,101,99 +,116,105,110,103,97,99,99,117,114,97,116,101,108,121,119,101,114,101,32,98,117, +105,108,116,119,97,115,32,107,105,108,108,101,100,97,103,114,101,101,109,101,110 +,116,115,109,117,99,104,32,109,111,114,101,32,68,117,101,32,116,111,32,116,104, +101,119,105,100,116,104,58,32,49,48,48,115,111,109,101,32,111,116,104,101,114,75 +,105,110,103,100,111,109,32,111,102,116,104,101,32,101,110,116,105,114,101,102, +97,109,111,117,115,32,102,111,114,116,111,32,99,111,110,110,101,99,116,111,98, +106,101,99,116,105,118,101,115,116,104,101,32,70,114,101,110,99,104,112,101,111, +112,108,101,32,97,110,100,102,101,97,116,117,114,101,100,34,62,105,115,32,115,97 +,105,100,32,116,111,115,116,114,117,99,116,117,114,97,108,114,101,102,101,114, +101,110,100,117,109,109,111,115,116,32,111,102,116,101,110,97,32,115,101,112,97, +114,97,116,101,45,62,10,60,100,105,118,32,105,100,32,79,102,102,105,99,105,97, +108,32,119,111,114,108,100,119,105,100,101,46,97,114,105,97,45,108,97,98,101,108 +,116,104,101,32,112,108,97,110,101,116,97,110,100,32,105,116,32,119,97,115,100, +34,32,118,97,108,117,101,61,34,108,111,111,107,105,110,103,32,97,116,98,101,110, +101,102,105,99,105,97,108,97,114,101,32,105,110,32,116,104,101,109,111,110,105, +116,111,114,105,110,103,114,101,112,111,114,116,101,100,108,121,116,104,101,32, +109,111,100,101,114,110,119,111,114,107,105,110,103,32,111,110,97,108,108,111, +119,101,100,32,116,111,119,104,101,114,101,32,116,104,101,32,105,110,110,111,118 +,97,116,105,118,101,60,47,97,62,60,47,100,105,118,62,115,111,117,110,100,116,114 +,97,99,107,115,101,97,114,99,104,70,111,114,109,116,101,110,100,32,116,111,32,98 +,101,105,110,112,117,116,32,105,100,61,34,111,112,101,110,105,110,103,32,111,102 +,114,101,115,116,114,105,99,116,101,100,97,100,111,112,116,101,100,32,98,121,97, +100,100,114,101,115,115,105,110,103,116,104,101,111,108,111,103,105,97,110,109, +101,116,104,111,100,115,32,111,102,118,97,114,105,97,110,116,32,111,102,67,104, +114,105,115,116,105,97,110,32,118,101,114,121,32,108,97,114,103,101,97,117,116, +111,109,111,116,105,118,101,98,121,32,102,97,114,32,116,104,101,114,97,110,103, +101,32,102,114,111,109,112,117,114,115,117,105,116,32,111,102,102,111,108,108, +111,119,32,116,104,101,98,114,111,117,103,104,116,32,116,111,105,110,32,69,110, +103,108,97,110,100,97,103,114,101,101,32,116,104,97,116,97,99,99,117,115,101,100 +,32,111,102,99,111,109,101,115,32,102,114,111,109,112,114,101,118,101,110,116, +105,110,103,100,105,118,32,115,116,121,108,101,61,104,105,115,32,111,114,32,104, +101,114,116,114,101,109,101,110,100,111,117,115,102,114,101,101,100,111,109,32, +111,102,99,111,110,99,101,114,110,105,110,103,48,32,49,101,109,32,49,101,109,59, +66,97,115,107,101,116,98,97,108,108,47,115,116,121,108,101,46,99,115,115,97,110, +32,101,97,114,108,105,101,114,101,118,101,110,32,97,102,116,101,114,47,34,32,116 +,105,116,108,101,61,34,46,99,111,109,47,105,110,100,101,120,116,97,107,105,110, +103,32,116,104,101,112,105,116,116,115,98,117,114,103,104,99,111,110,116,101,110 +,116,34,62,13,60,115,99,114,105,112,116,62,40,102,116,117,114,110,101,100,32,111 +,117,116,104,97,118,105,110,103,32,116,104,101,60,47,115,112,97,110,62,13,10,32, +111,99,99,97,115,105,111,110,97,108,98,101,99,97,117,115,101,32,105,116,115,116, +97,114,116,101,100,32,116,111,112,104,121,115,105,99,97,108,108,121,62,60,47,100 +,105,118,62,10,32,32,99,114,101,97,116,101,100,32,98,121,67,117,114,114,101,110, +116,108,121,44,32,98,103,99,111,108,111,114,61,34,116,97,98,105,110,100,101,120, +61,34,100,105,115,97,115,116,114,111,117,115,65,110,97,108,121,116,105,99,115,32 +,97,108,115,111,32,104,97,115,32,97,62,60,100,105,118,32,105,100,61,34,60,47,115 +,116,121,108,101,62,10,60,99,97,108,108,101,100,32,102,111,114,115,105,110,103, +101,114,32,97,110,100,46,115,114,99,32,61,32,34,47,47,118,105,111,108,97,116,105 +,111,110,115,116,104,105,115,32,112,111,105,110,116,99,111,110,115,116,97,110, +116,108,121,105,115,32,108,111,99,97,116,101,100,114,101,99,111,114,100,105,110, +103,115,100,32,102,114,111,109,32,116,104,101,110,101,100,101,114,108,97,110,100 +,115,112,111,114,116,117,103,117,195,170,115,215,162,215,145,215,168,215,153,215 +,170,217,129,216,167,216,177,216,179,219,140,100,101,115,97,114,114,111,108,108, +111,99,111,109,101,110,116,97,114,105,111,101,100,117,99,97,99,105,195,179,110, +115,101,112,116,105,101,109,98,114,101,114,101,103,105,115,116,114,97,100,111, +100,105,114,101,99,99,105,195,179,110,117,98,105,99,97,99,105,195,179,110,112, +117,98,108,105,99,105,100,97,100,114,101,115,112,117,101,115,116,97,115,114,101, +115,117,108,116,97,100,111,115,105,109,112,111,114,116,97,110,116,101,114,101, +115,101,114,118,97,100,111,115,97,114,116,195,173,99,117,108,111,115,100,105,102 +,101,114,101,110,116,101,115,115,105,103,117,105,101,110,116,101,115,114,101,112 +,195,186,98,108,105,99,97,115,105,116,117,97,99,105,195,179,110,109,105,110,105, +115,116,101,114,105,111,112,114,105,118,97,99,105,100,97,100,100,105,114,101,99, +116,111,114,105,111,102,111,114,109,97,99,105,195,179,110,112,111,98,108,97,99, +105,195,179,110,112,114,101,115,105,100,101,110,116,101,99,111,110,116,101,110, +105,100,111,115,97,99,99,101,115,111,114,105,111,115,116,101,99,104,110,111,114, +97,116,105,112,101,114,115,111,110,97,108,101,115,99,97,116,101,103,111,114,195, +173,97,101,115,112,101,99,105,97,108,101,115,100,105,115,112,111,110,105,98,108, +101,97,99,116,117,97,108,105,100,97,100,114,101,102,101,114,101,110,99,105,97, +118,97,108,108,97,100,111,108,105,100,98,105,98,108,105,111,116,101,99,97,114, +101,108,97,99,105,111,110,101,115,99,97,108,101,110,100,97,114,105,111,112,111, +108,195,173,116,105,99,97,115,97,110,116,101,114,105,111,114,101,115,100,111,99, +117,109,101,110,116,111,115,110,97,116,117,114,97,108,101,122,97,109,97,116,101, +114,105,97,108,101,115,100,105,102,101,114,101,110,99,105,97,101,99,111,110,195, +179,109,105,99,97,116,114,97,110,115,112,111,114,116,101,114,111,100,114,195,173 +,103,117,101,122,112,97,114,116,105,99,105,112,97,114,101,110,99,117,101,110,116 +,114,97,110,100,105,115,99,117,115,105,195,179,110,101,115,116,114,117,99,116, +117,114,97,102,117,110,100,97,99,105,195,179,110,102,114,101,99,117,101,110,116, +101,115,112,101,114,109,97,110,101,110,116,101,116,111,116,97,108,109,101,110, +116,101,208,188,208,190,208,182,208,189,208,190,208,177,209,131,208,180,208,181, +209,130,208,188,208,190,208,182,208,181,209,130,208,178,209,128,208,181,208,188, +209,143,209,130,208,176,208,186,208,182,208,181,209,135,209,130,208,190,208,177, +209,139,208,177,208,190,208,187,208,181,208,181,208,190,209,135,208,181,208,189, +209,140,209,141,209,130,208,190,208,179,208,190,208,186,208,190,208,179,208,180, +208,176,208,191,208,190,209,129,208,187,208,181,208,178,209,129,208,181,208,179, +208,190,209,129,208,176,208,185,209,130,208,181,209,135,208,181,209,128,208,181, +208,183,208,188,208,190,208,179,209,131,209,130,209,129,208,176,208,185,209,130, +208,176,208,182,208,184,208,183,208,189,208,184,208,188,208,181,208,182,208,180, +209,131,208,177,209,131,208,180,209,131,209,130,208,159,208,190,208,184,209,129, +208,186,208,183,208,180,208,181,209,129,209,140,208,178,208,184,208,180,208,181, +208,190,209,129,208,178,209,143,208,183,208,184,208,189,209,131,208,182,208,189, +208,190,209,129,208,178,208,190,208,181,208,185,208,187,209,142,208,180,208,181, +208,185,208,191,208,190,209,128,208,189,208,190,208,188,208,189,208,190,208,179, +208,190,208,180,208,181,209,130,208,181,208,185,209,129,208,178,208,190,208,184, +209,133,208,191,209,128,208,176,208,178,208,176,209,130,208,176,208,186,208,190, +208,185,208,188,208,181,209,129,209,130,208,190,208,184,208,188,208,181,208,181, +209,130,208,182,208,184,208,183,208,189,209,140,208,190,208,180,208,189,208,190, +208,185,208,187,209,131,209,135,209,136,208,181,208,191,208,181,209,128,208,181, +208,180,209,135,208,176,209,129,209,130,208,184,209,135,208,176,209,129,209,130, +209,140,209,128,208,176,208,177,208,190,209,130,208,189,208,190,208,178,209,139, +209,133,208,191,209,128,208,176,208,178,208,190,209,129,208,190,208,177,208,190, +208,185,208,191,208,190,209,130,208,190,208,188,208,188,208,181,208,189,208,181, +208,181,209,135,208,184,209,129,208,187,208,181,208,189,208,190,208,178,209,139, +208,181,209,131,209,129,208,187,209,131,208,179,208,190,208,186,208,190,208,187, +208,190,208,189,208,176,208,183,208,176,208,180,209,130,208,176,208,186,208,190, +208,181,209,130,208,190,208,179,208,180,208,176,208,191,208,190,209,135,209,130, +208,184,208,159,208,190,209,129,208,187,208,181,209,130,208,176,208,186,208,184, +208,181,208,189,208,190,208,178,209,139,208,185,209,129,209,130,208,190,208,184, +209,130,209,130,208,176,208,186,208,184,209,133,209,129,209,128,208,176,208,183, +209,131,208,161,208,176,208,189,208,186,209,130,209,132,208,190,209,128,209,131, +208,188,208,154,208,190,208,179,208,180,208,176,208,186,208,189,208,184,208,179, +208,184,209,129,208,187,208,190,208,178,208,176,208,189,208,176,209,136,208,181, +208,185,208,189,208,176,208,185,209,130,208,184,209,129,208,178,208,190,208,184, +208,188,209,129,208,178,209,143,208,183,209,140,208,187,209,142,208,177,208,190, +208,185,209,135,208,176,209,129,209,130,208,190,209,129,209,128,208,181,208,180, +208,184,208,154,209,128,208,190,208,188,208,181,208,164,208,190,209,128,209,131, +208,188,209,128,209,139,208,189,208,186,208,181,209,129,209,130,208,176,208,187, +208,184,208,191,208,190,208,184,209,129,208,186,209,130,209,139,209,129,209,143, +209,135,208,188,208,181,209,129,209,143,209,134,209,134,208,181,208,189,209,130, +209,128,209,130,209,128,209,131,208,180,208,176,209,129,208,176,208,188,209,139, +209,133,209,128,209,139,208,189,208,186,208,176,208,157,208,190,208,178,209,139, +208,185,209,135,208,176,209,129,208,190,208,178,208,188,208,181,209,129,209,130, +208,176,209,132,208,184,208,187,209,140,208,188,208,188,208,176,209,128,209,130, +208,176,209,129,209,130,209,128,208,176,208,189,208,188,208,181,209,129,209,130, +208,181,209,130,208,181,208,186,209,129,209,130,208,189,208,176,209,136,208,184, +209,133,208,188,208,184,208,189,209,131,209,130,208,184,208,188,208,181,208,189, +208,184,208,184,208,188,208,181,209,142,209,130,208,189,208,190,208,188,208,181, +209,128,208,179,208,190,209,128,208,190,208,180,209,129,208,176,208,188,208,190, +208,188,209,141,209,130,208,190,208,188,209,131,208,186,208,190,208,189,209,134, +208,181,209,129,208,178,208,190,208,181,208,188,208,186,208,176,208,186,208,190, +208,185,208,144,209,128,209,133,208,184,208,178,217,133,217,134,216,170,216,175, +217,137,216,165,216,177,216,179,216,167,217,132,216,177,216,179,216,167,217,132, +216,169,216,167,217,132,216,185,216,167,217,133,217,131,216,170,216,168,217,135, +216,167,216,168,216,177,216,167,217,133,216,172,216,167,217,132,217,138,217,136, +217,133,216,167,217,132,216,181,217,136,216,177,216,172,216,175,217,138,216,175, +216,169,216,167,217,132,216,185,216,182,217,136,216,165,216,182,216,167,217,129, +216,169,216,167,217,132,217,130,216,179,217,133,216,167,217,132,216,185,216,167, +216,168,216,170,216,173,217,133,217,138,217,132,217,133,217,132,217,129,216,167, +216,170,217,133,217,132,216,170,217,130,217,137,216,170,216,185,216,175,217,138, +217,132,216,167,217,132,216,180,216,185,216,177,216,163,216,174,216,168,216,167, +216,177,216,170,216,183,217,136,217,138,216,177,216,185,217,132,217,138,217,131, +217,133,216,165,216,177,217,129,216,167,217,130,216,183,217,132,216,168,216,167, +216,170,216,167,217,132,217,132,216,186,216,169,216,170,216,177,216,170,217,138, +216,168,216,167,217,132,217,134,216,167,216,179,216,167,217,132,216,180,217,138, +216,174,217,133,217,134,216,170,216,175,217,138,216,167,217,132,216,185,216,177, +216,168,216,167,217,132,217,130,216,181,216,181,216,167,217,129,217,132,216,167, +217,133,216,185,217,132,217,138,217,135,216,167,216,170,216,173,216,175,217,138, +216,171,216,167,217,132,217,132,217,135,217,133,216,167,217,132,216,185,217,133, +217,132,217,133,217,131,216,170,216,168,216,169,217,138,217,133,217,131,217,134, +217,131,216,167,217,132,216,183,217,129,217,132,217,129,217,138,216,175,217,138, +217,136,216,165,216,175,216,167,216,177,216,169,216,170,216,167,216,177,217,138, +216,174,216,167,217,132,216,181,216,173,216,169,216,170,216,179,216,172,217,138, +217,132,216,167,217,132,217,136,217,130,216,170,216,185,217,134,216,175,217,133, +216,167,217,133,216,175,217,138,217,134,216,169,216,170,216,181,217,133,217,138, +217,133,216,163,216,177,216,180,217,138,217,129,216,167,217,132,216,176,217,138, +217,134,216,185,216,177,216,168,217,138,216,169,216,168,217,136,216,167,216,168, +216,169,216,163,217,132,216,185,216,167,216,168,216,167,217,132,216,179,217,129, +216,177,217,133,216,180,216,167,217,131,217,132,216,170,216,185,216,167,217,132, +217,137,216,167,217,132,216,163,217,136,217,132,216,167,217,132,216,179,217,134, +216,169,216,172,216,167,217,133,216,185,216,169,216,167,217,132,216,181,216,173, +217,129,216,167,217,132,216,175,217,138,217,134,217,131,217,132,217,133,216,167, +216,170,216,167,217,132,216,174,216,167,216,181,216,167,217,132,217,133,217,132, +217,129,216,163,216,185,216,182,216,167,216,161,217,131,216,170,216,167,216,168, +216,169,216,167,217,132,216,174,217,138,216,177,216,177,216,179,216,167,216,166, +217,132,216,167,217,132,217,130,217,132,216,168,216,167,217,132,216,163,216,175, +216,168,217,133,217,130,216,167,216,183,216,185,217,133,216,177,216,167,216,179, +217,132,217,133,217,134,216,183,217,130,216,169,216,167,217,132,217,131,216,170, +216,168,216,167,217,132,216,177,216,172,217,132,216,167,216,180,216,170,216,177, +217,131,216,167,217,132,217,130,216,175,217,133,217,138,216,185,216,183,217,138, +217,131,115,66,121,84,97,103,78,97,109,101,40,46,106,112,103,34,32,97,108,116,61 +,34,49,112,120,32,115,111,108,105,100,32,35,46,103,105,102,34,32,97,108,116,61, +34,116,114,97,110,115,112,97,114,101,110,116,105,110,102,111,114,109,97,116,105, +111,110,97,112,112,108,105,99,97,116,105,111,110,34,32,111,110,99,108,105,99,107 +,61,34,101,115,116,97,98,108,105,115,104,101,100,97,100,118,101,114,116,105,115, +105,110,103,46,112,110,103,34,32,97,108,116,61,34,101,110,118,105,114,111,110, +109,101,110,116,112,101,114,102,111,114,109,97,110,99,101,97,112,112,114,111,112 +,114,105,97,116,101,38,97,109,112,59,109,100,97,115,104,59,105,109,109,101,100, +105,97,116,101,108,121,60,47,115,116,114,111,110,103,62,60,47,114,97,116,104,101 +,114,32,116,104,97,110,116,101,109,112,101,114,97,116,117,114,101,100,101,118, +101,108,111,112,109,101,110,116,99,111,109,112,101,116,105,116,105,111,110,112, +108,97,99,101,104,111,108,100,101,114,118,105,115,105,98,105,108,105,116,121,58, +99,111,112,121,114,105,103,104,116,34,62,48,34,32,104,101,105,103,104,116,61,34, +101,118,101,110,32,116,104,111,117,103,104,114,101,112,108,97,99,101,109,101,110 +,116,100,101,115,116,105,110,97,116,105,111,110,67,111,114,112,111,114,97,116, +105,111,110,60,117,108,32,99,108,97,115,115,61,34,65,115,115,111,99,105,97,116, +105,111,110,105,110,100,105,118,105,100,117,97,108,115,112,101,114,115,112,101, +99,116,105,118,101,115,101,116,84,105,109,101,111,117,116,40,117,114,108,40,104, +116,116,112,58,47,47,109,97,116,104,101,109,97,116,105,99,115,109,97,114,103,105 +,110,45,116,111,112,58,101,118,101,110,116,117,97,108,108,121,32,100,101,115,99, +114,105,112,116,105,111,110,41,32,110,111,45,114,101,112,101,97,116,99,111,108, +108,101,99,116,105,111,110,115,46,74,80,71,124,116,104,117,109,98,124,112,97,114 +,116,105,99,105,112,97,116,101,47,104,101,97,100,62,60,98,111,100,121,102,108, +111,97,116,58,108,101,102,116,59,60,108,105,32,99,108,97,115,115,61,34,104,117, +110,100,114,101,100,115,32,111,102,10,10,72,111,119,101,118,101,114,44,32,99,111 +,109,112,111,115,105,116,105,111,110,99,108,101,97,114,58,98,111,116,104,59,99, +111,111,112,101,114,97,116,105,111,110,119,105,116,104,105,110,32,116,104,101,32 +,108,97,98,101,108,32,102,111,114,61,34,98,111,114,100,101,114,45,116,111,112,58 +,78,101,119,32,90,101,97,108,97,110,100,114,101,99,111,109,109,101,110,100,101, +100,112,104,111,116,111,103,114,97,112,104,121,105,110,116,101,114,101,115,116, +105,110,103,38,108,116,59,115,117,112,38,103,116,59,99,111,110,116,114,111,118, +101,114,115,121,78,101,116,104,101,114,108,97,110,100,115,97,108,116,101,114,110 +,97,116,105,118,101,109,97,120,108,101,110,103,116,104,61,34,115,119,105,116,122 +,101,114,108,97,110,100,68,101,118,101,108,111,112,109,101,110,116,101,115,115, +101,110,116,105,97,108,108,121,10,10,65,108,116,104,111,117,103,104,32,60,47,116 +,101,120,116,97,114,101,97,62,116,104,117,110,100,101,114,98,105,114,100,114,101 +,112,114,101,115,101,110,116,101,100,38,97,109,112,59,110,100,97,115,104,59,115, +112,101,99,117,108,97,116,105,111,110,99,111,109,109,117,110,105,116,105,101,115 +,108,101,103,105,115,108,97,116,105,111,110,101,108,101,99,116,114,111,110,105, +99,115,10,9,60,100,105,118,32,105,100,61,34,105,108,108,117,115,116,114,97,116, +101,100,101,110,103,105,110,101,101,114,105,110,103,116,101,114,114,105,116,111, +114,105,101,115,97,117,116,104,111,114,105,116,105,101,115,100,105,115,116,114, +105,98,117,116,101,100,54,34,32,104,101,105,103,104,116,61,34,115,97,110,115,45, +115,101,114,105,102,59,99,97,112,97,98,108,101,32,111,102,32,100,105,115,97,112, +112,101,97,114,101,100,105,110,116,101,114,97,99,116,105,118,101,108,111,111,107 +,105,110,103,32,102,111,114,105,116,32,119,111,117,108,100,32,98,101,65,102,103, +104,97,110,105,115,116,97,110,119,97,115,32,99,114,101,97,116,101,100,77,97,116, +104,46,102,108,111,111,114,40,115,117,114,114,111,117,110,100,105,110,103,99,97, +110,32,97,108,115,111,32,98,101,111,98,115,101,114,118,97,116,105,111,110,109,97 +,105,110,116,101,110,97,110,99,101,101,110,99,111,117,110,116,101,114,101,100,60 +,104,50,32,99,108,97,115,115,61,34,109,111,114,101,32,114,101,99,101,110,116,105 +,116,32,104,97,115,32,98,101,101,110,105,110,118,97,115,105,111,110,32,111,102, +41,46,103,101,116,84,105,109,101,40,41,102,117,110,100,97,109,101,110,116,97,108 +,68,101,115,112,105,116,101,32,116,104,101,34,62,60,100,105,118,32,105,100,61,34 +,105,110,115,112,105,114,97,116,105,111,110,101,120,97,109,105,110,97,116,105, +111,110,112,114,101,112,97,114,97,116,105,111,110,101,120,112,108,97,110,97,116, +105,111,110,60,105,110,112,117,116,32,105,100,61,34,60,47,97,62,60,47,115,112,97 +,110,62,118,101,114,115,105,111,110,115,32,111,102,105,110,115,116,114,117,109, +101,110,116,115,98,101,102,111,114,101,32,116,104,101,32,32,61,32,39,104,116,116 +,112,58,47,47,68,101,115,99,114,105,112,116,105,111,110,114,101,108,97,116,105, +118,101,108,121,32,46,115,117,98,115,116,114,105,110,103,40,101,97,99,104,32,111 +,102,32,116,104,101,101,120,112,101,114,105,109,101,110,116,115,105,110,102,108, +117,101,110,116,105,97,108,105,110,116,101,103,114,97,116,105,111,110,109,97,110 +,121,32,112,101,111,112,108,101,100,117,101,32,116,111,32,116,104,101,32,99,111, +109,98,105,110,97,116,105,111,110,100,111,32,110,111,116,32,104,97,118,101,77, +105,100,100,108,101,32,69,97,115,116,60,110,111,115,99,114,105,112,116,62,60,99, +111,112,121,114,105,103,104,116,34,32,112,101,114,104,97,112,115,32,116,104,101, +105,110,115,116,105,116,117,116,105,111,110,105,110,32,68,101,99,101,109,98,101, +114,97,114,114,97,110,103,101,109,101,110,116,109,111,115,116,32,102,97,109,111, +117,115,112,101,114,115,111,110,97,108,105,116,121,99,114,101,97,116,105,111,110 +,32,111,102,108,105,109,105,116,97,116,105,111,110,115,101,120,99,108,117,115, +105,118,101,108,121,115,111,118,101,114,101,105,103,110,116,121,45,99,111,110, +116,101,110,116,34,62,10,60,116,100,32,99,108,97,115,115,61,34,117,110,100,101, +114,103,114,111,117,110,100,112,97,114,97,108,108,101,108,32,116,111,100,111,99, +116,114,105,110,101,32,111,102,111,99,99,117,112,105,101,100,32,98,121,116,101, +114,109,105,110,111,108,111,103,121,82,101,110,97,105,115,115,97,110,99,101,97, +32,110,117,109,98,101,114,32,111,102,115,117,112,112,111,114,116,32,102,111,114, +101,120,112,108,111,114,97,116,105,111,110,114,101,99,111,103,110,105,116,105, +111,110,112,114,101,100,101,99,101,115,115,111,114,60,105,109,103,32,115,114,99, +61,34,47,60,104,49,32,99,108,97,115,115,61,34,112,117,98,108,105,99,97,116,105, +111,110,109,97,121,32,97,108,115,111,32,98,101,115,112,101,99,105,97,108,105,122 +,101,100,60,47,102,105,101,108,100,115,101,116,62,112,114,111,103,114,101,115, +115,105,118,101,109,105,108,108,105,111,110,115,32,111,102,115,116,97,116,101, +115,32,116,104,97,116,101,110,102,111,114,99,101,109,101,110,116,97,114,111,117, +110,100,32,116,104,101,32,111,110,101,32,97,110,111,116,104,101,114,46,112,97, +114,101,110,116,78,111,100,101,97,103,114,105,99,117,108,116,117,114,101,65,108, +116,101,114,110,97,116,105,118,101,114,101,115,101,97,114,99,104,101,114,115,116 +,111,119,97,114,100,115,32,116,104,101,77,111,115,116,32,111,102,32,116,104,101, +109,97,110,121,32,111,116,104,101,114,32,40,101,115,112,101,99,105,97,108,108, +121,60,116,100,32,119,105,100,116,104,61,34,59,119,105,100,116,104,58,49,48,48, +37,105,110,100,101,112,101,110,100,101,110,116,60,104,51,32,99,108,97,115,115,61 +,34,32,111,110,99,104,97,110,103,101,61,34,41,46,97,100,100,67,108,97,115,115,40 +,105,110,116,101,114,97,99,116,105,111,110,79,110,101,32,111,102,32,116,104,101, +32,100,97,117,103,104,116,101,114,32,111,102,97,99,99,101,115,115,111,114,105, +101,115,98,114,97,110,99,104,101,115,32,111,102,13,10,60,100,105,118,32,105,100, +61,34,116,104,101,32,108,97,114,103,101,115,116,100,101,99,108,97,114,97,116,105 +,111,110,114,101,103,117,108,97,116,105,111,110,115,73,110,102,111,114,109,97, +116,105,111,110,116,114,97,110,115,108,97,116,105,111,110,100,111,99,117,109,101 +,110,116,97,114,121,105,110,32,111,114,100,101,114,32,116,111,34,62,10,60,104, +101,97,100,62,10,60,34,32,104,101,105,103,104,116,61,34,49,97,99,114,111,115,115 +,32,116,104,101,32,111,114,105,101,110,116,97,116,105,111,110,41,59,60,47,115,99 +,114,105,112,116,62,105,109,112,108,101,109,101,110,116,101,100,99,97,110,32,98, +101,32,115,101,101,110,116,104,101,114,101,32,119,97,115,32,97,100,101,109,111, +110,115,116,114,97,116,101,99,111,110,116,97,105,110,101,114,34,62,99,111,110, +110,101,99,116,105,111,110,115,116,104,101,32,66,114,105,116,105,115,104,119,97, +115,32,119,114,105,116,116,101,110,33,105,109,112,111,114,116,97,110,116,59,112, +120,59,32,109,97,114,103,105,110,45,102,111,108,108,111,119,101,100,32,98,121,97 +,98,105,108,105,116,121,32,116,111,32,99,111,109,112,108,105,99,97,116,101,100, +100,117,114,105,110,103,32,116,104,101,32,105,109,109,105,103,114,97,116,105,111 +,110,97,108,115,111,32,99,97,108,108,101,100,60,104,52,32,99,108,97,115,115,61, +34,100,105,115,116,105,110,99,116,105,111,110,114,101,112,108,97,99,101,100,32, +98,121,103,111,118,101,114,110,109,101,110,116,115,108,111,99,97,116,105,111,110 +,32,111,102,105,110,32,78,111,118,101,109,98,101,114,119,104,101,116,104,101,114 +,32,116,104,101,60,47,112,62,10,60,47,100,105,118,62,97,99,113,117,105,115,105, +116,105,111,110,99,97,108,108,101,100,32,116,104,101,32,112,101,114,115,101,99, +117,116,105,111,110,100,101,115,105,103,110,97,116,105,111,110,123,102,111,110, +116,45,115,105,122,101,58,97,112,112,101,97,114,101,100,32,105,110,105,110,118, +101,115,116,105,103,97,116,101,101,120,112,101,114,105,101,110,99,101,100,109, +111,115,116,32,108,105,107,101,108,121,119,105,100,101,108,121,32,117,115,101, +100,100,105,115,99,117,115,115,105,111,110,115,112,114,101,115,101,110,99,101,32 +,111,102,32,40,100,111,99,117,109,101,110,116,46,101,120,116,101,110,115,105,118 +,101,108,121,73,116,32,104,97,115,32,98,101,101,110,105,116,32,100,111,101,115, +32,110,111,116,99,111,110,116,114,97,114,121,32,116,111,105,110,104,97,98,105, +116,97,110,116,115,105,109,112,114,111,118,101,109,101,110,116,115,99,104,111, +108,97,114,115,104,105,112,99,111,110,115,117,109,112,116,105,111,110,105,110, +115,116,114,117,99,116,105,111,110,102,111,114,32,101,120,97,109,112,108,101,111 +,110,101,32,111,114,32,109,111,114,101,112,120,59,32,112,97,100,100,105,110,103, +116,104,101,32,99,117,114,114,101,110,116,97,32,115,101,114,105,101,115,32,111, +102,97,114,101,32,117,115,117,97,108,108,121,114,111,108,101,32,105,110,32,116, +104,101,112,114,101,118,105,111,117,115,108,121,32,100,101,114,105,118,97,116, +105,118,101,115,101,118,105,100,101,110,99,101,32,111,102,101,120,112,101,114, +105,101,110,99,101,115,99,111,108,111,114,115,99,104,101,109,101,115,116,97,116, +101,100,32,116,104,97,116,99,101,114,116,105,102,105,99,97,116,101,60,47,97,62, +60,47,100,105,118,62,10,32,115,101,108,101,99,116,101,100,61,34,104,105,103,104, +32,115,99,104,111,111,108,114,101,115,112,111,110,115,101,32,116,111,99,111,109, +102,111,114,116,97,98,108,101,97,100,111,112,116,105,111,110,32,111,102,116,104, +114,101,101,32,121,101,97,114,115,116,104,101,32,99,111,117,110,116,114,121,105, +110,32,70,101,98,114,117,97,114,121,115,111,32,116,104,97,116,32,116,104,101,112 +,101,111,112,108,101,32,119,104,111,32,112,114,111,118,105,100,101,100,32,98,121 +,60,112,97,114,97,109,32,110,97,109,101,97,102,102,101,99,116,101,100,32,98,121, +105,110,32,116,101,114,109,115,32,111,102,97,112,112,111,105,110,116,109,101,110 +,116,73,83,79,45,56,56,53,57,45,49,34,119,97,115,32,98,111,114,110,32,105,110, +104,105,115,116,111,114,105,99,97,108,32,114,101,103,97,114,100,101,100,32,97, +115,109,101,97,115,117,114,101,109,101,110,116,105,115,32,98,97,115,101,100,32, +111,110,32,97,110,100,32,111,116,104,101,114,32,58,32,102,117,110,99,116,105,111 +,110,40,115,105,103,110,105,102,105,99,97,110,116,99,101,108,101,98,114,97,116, +105,111,110,116,114,97,110,115,109,105,116,116,101,100,47,106,115,47,106,113,117 +,101,114,121,46,105,115,32,107,110,111,119,110,32,97,115,116,104,101,111,114,101 +,116,105,99,97,108,32,116,97,98,105,110,100,101,120,61,34,105,116,32,99,111,117, +108,100,32,98,101,60,110,111,115,99,114,105,112,116,62,10,104,97,118,105,110,103 +,32,98,101,101,110,13,10,60,104,101,97,100,62,13,10,60,32,38,113,117,111,116,59, +84,104,101,32,99,111,109,112,105,108,97,116,105,111,110,104,101,32,104,97,100,32 +,98,101,101,110,112,114,111,100,117,99,101,100,32,98,121,112,104,105,108,111,115 +,111,112,104,101,114,99,111,110,115,116,114,117,99,116,101,100,105,110,116,101, +110,100,101,100,32,116,111,97,109,111,110,103,32,111,116,104,101,114,99,111,109, +112,97,114,101,100,32,116,111,116,111,32,115,97,121,32,116,104,97,116,69,110,103 +,105,110,101,101,114,105,110,103,97,32,100,105,102,102,101,114,101,110,116,114, +101,102,101,114,114,101,100,32,116,111,100,105,102,102,101,114,101,110,99,101, +115,98,101,108,105,101,102,32,116,104,97,116,112,104,111,116,111,103,114,97,112, +104,115,105,100,101,110,116,105,102,121,105,110,103,72,105,115,116,111,114,121, +32,111,102,32,82,101,112,117,98,108,105,99,32,111,102,110,101,99,101,115,115,97, +114,105,108,121,112,114,111,98,97,98,105,108,105,116,121,116,101,99,104,110,105, +99,97,108,108,121,108,101,97,118,105,110,103,32,116,104,101,115,112,101,99,116, +97,99,117,108,97,114,102,114,97,99,116,105,111,110,32,111,102,101,108,101,99,116 +,114,105,99,105,116,121,104,101,97,100,32,111,102,32,116,104,101,114,101,115,116 +,97,117,114,97,110,116,115,112,97,114,116,110,101,114,115,104,105,112,101,109, +112,104,97,115,105,115,32,111,110,109,111,115,116,32,114,101,99,101,110,116,115, +104,97,114,101,32,119,105,116,104,32,115,97,121,105,110,103,32,116,104,97,116, +102,105,108,108,101,100,32,119,105,116,104,100,101,115,105,103,110,101,100,32, +116,111,105,116,32,105,115,32,111,102,116,101,110,34,62,60,47,105,102,114,97,109 +,101,62,97,115,32,102,111,108,108,111,119,115,58,109,101,114,103,101,100,32,119, +105,116,104,116,104,114,111,117,103,104,32,116,104,101,99,111,109,109,101,114,99 +,105,97,108,32,112,111,105,110,116,101,100,32,111,117,116,111,112,112,111,114, +116,117,110,105,116,121,118,105,101,119,32,111,102,32,116,104,101,114,101,113, +117,105,114,101,109,101,110,116,100,105,118,105,115,105,111,110,32,111,102,112, +114,111,103,114,97,109,109,105,110,103,104,101,32,114,101,99,101,105,118,101,100 +,115,101,116,73,110,116,101,114,118,97,108,34,62,60,47,115,112,97,110,62,60,47, +105,110,32,78,101,119,32,89,111,114,107,97,100,100,105,116,105,111,110,97,108,32 +,99,111,109,112,114,101,115,115,105,111,110,10,10,60,100,105,118,32,105,100,61, +34,105,110,99,111,114,112,111,114,97,116,101,59,60,47,115,99,114,105,112,116,62, +60,97,116,116,97,99,104,69,118,101,110,116,98,101,99,97,109,101,32,116,104,101, +32,34,32,116,97,114,103,101,116,61,34,95,99,97,114,114,105,101,100,32,111,117, +116,83,111,109,101,32,111,102,32,116,104,101,115,99,105,101,110,99,101,32,97,110 +,100,116,104,101,32,116,105,109,101,32,111,102,67,111,110,116,97,105,110,101,114 +,34,62,109,97,105,110,116,97,105,110,105,110,103,67,104,114,105,115,116,111,112, +104,101,114,77,117,99,104,32,111,102,32,116,104,101,119,114,105,116,105,110,103, +115,32,111,102,34,32,104,101,105,103,104,116,61,34,50,115,105,122,101,32,111,102 +,32,116,104,101,118,101,114,115,105,111,110,32,111,102,32,109,105,120,116,117, +114,101,32,111,102,32,98,101,116,119,101,101,110,32,116,104,101,69,120,97,109, +112,108,101,115,32,111,102,101,100,117,99,97,116,105,111,110,97,108,99,111,109, +112,101,116,105,116,105,118,101,32,111,110,115,117,98,109,105,116,61,34,100,105, +114,101,99,116,111,114,32,111,102,100,105,115,116,105,110,99,116,105,118,101,47, +68,84,68,32,88,72,84,77,76,32,114,101,108,97,116,105,110,103,32,116,111,116,101, +110,100,101,110,99,121,32,116,111,112,114,111,118,105,110,99,101,32,111,102,119, +104,105,99,104,32,119,111,117,108,100,100,101,115,112,105,116,101,32,116,104,101 +,115,99,105,101,110,116,105,102,105,99,32,108,101,103,105,115,108,97,116,117,114 +,101,46,105,110,110,101,114,72,84,77,76,32,97,108,108,101,103,97,116,105,111,110 +,115,65,103,114,105,99,117,108,116,117,114,101,119,97,115,32,117,115,101,100,32, +105,110,97,112,112,114,111,97,99,104,32,116,111,105,110,116,101,108,108,105,103, +101,110,116,121,101,97,114,115,32,108,97,116,101,114,44,115,97,110,115,45,115, +101,114,105,102,100,101,116,101,114,109,105,110,105,110,103,80,101,114,102,111, +114,109,97,110,99,101,97,112,112,101,97,114,97,110,99,101,115,44,32,119,104,105, +99,104,32,105,115,32,102,111,117,110,100,97,116,105,111,110,115,97,98,98,114,101 +,118,105,97,116,101,100,104,105,103,104,101,114,32,116,104,97,110,115,32,102,114 +,111,109,32,116,104,101,32,105,110,100,105,118,105,100,117,97,108,32,99,111,109, +112,111,115,101,100,32,111,102,115,117,112,112,111,115,101,100,32,116,111,99,108 +,97,105,109,115,32,116,104,97,116,97,116,116,114,105,98,117,116,105,111,110,102, +111,110,116,45,115,105,122,101,58,49,101,108,101,109,101,110,116,115,32,111,102, +72,105,115,116,111,114,105,99,97,108,32,104,105,115,32,98,114,111,116,104,101, +114,97,116,32,116,104,101,32,116,105,109,101,97,110,110,105,118,101,114,115,97, +114,121,103,111,118,101,114,110,101,100,32,98,121,114,101,108,97,116,101,100,32, +116,111,32,117,108,116,105,109,97,116,101,108,121,32,105,110,110,111,118,97,116, +105,111,110,115,105,116,32,105,115,32,115,116,105,108,108,99,97,110,32,111,110, +108,121,32,98,101,100,101,102,105,110,105,116,105,111,110,115,116,111,71,77,84, +83,116,114,105,110,103,65,32,110,117,109,98,101,114,32,111,102,105,109,103,32,99 +,108,97,115,115,61,34,69,118,101,110,116,117,97,108,108,121,44,119,97,115,32,99, +104,97,110,103,101,100,111,99,99,117,114,114,101,100,32,105,110,110,101,105,103, +104,98,111,114,105,110,103,100,105,115,116,105,110,103,117,105,115,104,119,104, +101,110,32,104,101,32,119,97,115,105,110,116,114,111,100,117,99,105,110,103,116, +101,114,114,101,115,116,114,105,97,108,77,97,110,121,32,111,102,32,116,104,101, +97,114,103,117,101,115,32,116,104,97,116,97,110,32,65,109,101,114,105,99,97,110, +99,111,110,113,117,101,115,116,32,111,102,119,105,100,101,115,112,114,101,97,100 +,32,119,101,114,101,32,107,105,108,108,101,100,115,99,114,101,101,110,32,97,110, +100,32,73,110,32,111,114,100,101,114,32,116,111,101,120,112,101,99,116,101,100, +32,116,111,100,101,115,99,101,110,100,97,110,116,115,97,114,101,32,108,111,99,97 +,116,101,100,108,101,103,105,115,108,97,116,105,118,101,103,101,110,101,114,97, +116,105,111,110,115,32,98,97,99,107,103,114,111,117,110,100,109,111,115,116,32, +112,101,111,112,108,101,121,101,97,114,115,32,97,102,116,101,114,116,104,101,114 +,101,32,105,115,32,110,111,116,104,101,32,104,105,103,104,101,115,116,102,114, +101,113,117,101,110,116,108,121,32,116,104,101,121,32,100,111,32,110,111,116,97, +114,103,117,101,100,32,116,104,97,116,115,104,111,119,101,100,32,116,104,97,116, +112,114,101,100,111,109,105,110,97,110,116,116,104,101,111,108,111,103,105,99,97 +,108,98,121,32,116,104,101,32,116,105,109,101,99,111,110,115,105,100,101,114,105 +,110,103,115,104,111,114,116,45,108,105,118,101,100,60,47,115,112,97,110,62,60, +47,97,62,99,97,110,32,98,101,32,117,115,101,100,118,101,114,121,32,108,105,116, +116,108,101,111,110,101,32,111,102,32,116,104,101,32,104,97,100,32,97,108,114, +101,97,100,121,105,110,116,101,114,112,114,101,116,101,100,99,111,109,109,117, +110,105,99,97,116,101,102,101,97,116,117,114,101,115,32,111,102,103,111,118,101, +114,110,109,101,110,116,44,60,47,110,111,115,99,114,105,112,116,62,101,110,116, +101,114,101,100,32,116,104,101,34,32,104,101,105,103,104,116,61,34,51,73,110,100 +,101,112,101,110,100,101,110,116,112,111,112,117,108,97,116,105,111,110,115,108, +97,114,103,101,45,115,99,97,108,101,46,32,65,108,116,104,111,117,103,104,32,117, +115,101,100,32,105,110,32,116,104,101,100,101,115,116,114,117,99,116,105,111,110 +,112,111,115,115,105,98,105,108,105,116,121,115,116,97,114,116,105,110,103,32, +105,110,116,119,111,32,111,114,32,109,111,114,101,101,120,112,114,101,115,115, +105,111,110,115,115,117,98,111,114,100,105,110,97,116,101,108,97,114,103,101,114 +,32,116,104,97,110,104,105,115,116,111,114,121,32,97,110,100,60,47,111,112,116, +105,111,110,62,13,10,67,111,110,116,105,110,101,110,116,97,108,101,108,105,109, +105,110,97,116,105,110,103,119,105,108,108,32,110,111,116,32,98,101,112,114,97, +99,116,105,99,101,32,111,102,105,110,32,102,114,111,110,116,32,111,102,115,105, +116,101,32,111,102,32,116,104,101,101,110,115,117,114,101,32,116,104,97,116,116, +111,32,99,114,101,97,116,101,32,97,109,105,115,115,105,115,115,105,112,112,105, +112,111,116,101,110,116,105,97,108,108,121,111,117,116,115,116,97,110,100,105, +110,103,98,101,116,116,101,114,32,116,104,97,110,119,104,97,116,32,105,115,32, +110,111,119,115,105,116,117,97,116,101,100,32,105,110,109,101,116,97,32,110,97, +109,101,61,34,84,114,97,100,105,116,105,111,110,97,108,115,117,103,103,101,115, +116,105,111,110,115,84,114,97,110,115,108,97,116,105,111,110,116,104,101,32,102, +111,114,109,32,111,102,97,116,109,111,115,112,104,101,114,105,99,105,100,101,111 +,108,111,103,105,99,97,108,101,110,116,101,114,112,114,105,115,101,115,99,97,108 +,99,117,108,97,116,105,110,103,101,97,115,116,32,111,102,32,116,104,101,114,101, +109,110,97,110,116,115,32,111,102,112,108,117,103,105,110,115,112,97,103,101,47, +105,110,100,101,120,46,112,104,112,63,114,101,109,97,105,110,101,100,32,105,110, +116,114,97,110,115,102,111,114,109,101,100,72,101,32,119,97,115,32,97,108,115, +111,119,97,115,32,97,108,114,101,97,100,121,115,116,97,116,105,115,116,105,99,97 +,108,105,110,32,102,97,118,111,114,32,111,102,77,105,110,105,115,116,114,121,32, +111,102,109,111,118,101,109,101,110,116,32,111,102,102,111,114,109,117,108,97, +116,105,111,110,105,115,32,114,101,113,117,105,114,101,100,60,108,105,110,107,32 +,114,101,108,61,34,84,104,105,115,32,105,115,32,116,104,101,32,60,97,32,104,114, +101,102,61,34,47,112,111,112,117,108,97,114,105,122,101,100,105,110,118,111,108, +118,101,100,32,105,110,97,114,101,32,117,115,101,100,32,116,111,97,110,100,32, +115,101,118,101,114,97,108,109,97,100,101,32,98,121,32,116,104,101,115,101,101, +109,115,32,116,111,32,98,101,108,105,107,101,108,121,32,116,104,97,116,80,97,108 +,101,115,116,105,110,105,97,110,110,97,109,101,100,32,97,102,116,101,114,105,116 +,32,104,97,100,32,98,101,101,110,109,111,115,116,32,99,111,109,109,111,110,116, +111,32,114,101,102,101,114,32,116,111,98,117,116,32,116,104,105,115,32,105,115, +99,111,110,115,101,99,117,116,105,118,101,116,101,109,112,111,114,97,114,105,108 +,121,73,110,32,103,101,110,101,114,97,108,44,99,111,110,118,101,110,116,105,111, +110,115,116,97,107,101,115,32,112,108,97,99,101,115,117,98,100,105,118,105,115, +105,111,110,116,101,114,114,105,116,111,114,105,97,108,111,112,101,114,97,116, +105,111,110,97,108,112,101,114,109,97,110,101,110,116,108,121,119,97,115,32,108, +97,114,103,101,108,121,111,117,116,98,114,101,97,107,32,111,102,105,110,32,116, +104,101,32,112,97,115,116,102,111,108,108,111,119,105,110,103,32,97,32,120,109, +108,110,115,58,111,103,61,34,62,60,97,32,99,108,97,115,115,61,34,99,108,97,115, +115,61,34,116,101,120,116,67,111,110,118,101,114,115,105,111,110,32,109,97,121, +32,98,101,32,117,115,101,100,109,97,110,117,102,97,99,116,117,114,101,97,102,116 +,101,114,32,98,101,105,110,103,99,108,101,97,114,102,105,120,34,62,10,113,117, +101,115,116,105,111,110,32,111,102,119,97,115,32,101,108,101,99,116,101,100,116, +111,32,98,101,99,111,109,101,32,97,98,101,99,97,117,115,101,32,111,102,32,115, +111,109,101,32,112,101,111,112,108,101,105,110,115,112,105,114,101,100,32,98,121 +,115,117,99,99,101,115,115,102,117,108,32,97,32,116,105,109,101,32,119,104,101, +110,109,111,114,101,32,99,111,109,109,111,110,97,109,111,110,103,115,116,32,116, +104,101,97,110,32,111,102,102,105,99,105,97,108,119,105,100,116,104,58,49,48,48, +37,59,116,101,99,104,110,111,108,111,103,121,44,119,97,115,32,97,100,111,112,116 +,101,100,116,111,32,107,101,101,112,32,116,104,101,115,101,116,116,108,101,109, +101,110,116,115,108,105,118,101,32,98,105,114,116,104,115,105,110,100,101,120,46 +,104,116,109,108,34,67,111,110,110,101,99,116,105,99,117,116,97,115,115,105,103, +110,101,100,32,116,111,38,97,109,112,59,116,105,109,101,115,59,97,99,99,111,117, +110,116,32,102,111,114,97,108,105,103,110,61,114,105,103,104,116,116,104,101,32, +99,111,109,112,97,110,121,97,108,119,97,121,115,32,98,101,101,110,114,101,116, +117,114,110,101,100,32,116,111,105,110,118,111,108,118,101,109,101,110,116,66, +101,99,97,117,115,101,32,116,104,101,116,104,105,115,32,112,101,114,105,111,100, +34,32,110,97,109,101,61,34,113,34,32,99,111,110,102,105,110,101,100,32,116,111, +97,32,114,101,115,117,108,116,32,111,102,118,97,108,117,101,61,34,34,32,47,62, +105,115,32,97,99,116,117,97,108,108,121,69,110,118,105,114,111,110,109,101,110, +116,13,10,60,47,104,101,97,100,62,13,10,67,111,110,118,101,114,115,101,108,121, +44,62,10,60,100,105,118,32,105,100,61,34,48,34,32,119,105,100,116,104,61,34,49, +105,115,32,112,114,111,98,97,98,108,121,104,97,118,101,32,98,101,99,111,109,101, +99,111,110,116,114,111,108,108,105,110,103,116,104,101,32,112,114,111,98,108,101 +,109,99,105,116,105,122,101,110,115,32,111,102,112,111,108,105,116,105,99,105,97 +,110,115,114,101,97,99,104,101,100,32,116,104,101,97,115,32,101,97,114,108,121, +32,97,115,58,110,111,110,101,59,32,111,118,101,114,60,116,97,98,108,101,32,99, +101,108,108,118,97,108,105,100,105,116,121,32,111,102,100,105,114,101,99,116,108 +,121,32,116,111,111,110,109,111,117,115,101,100,111,119,110,119,104,101,114,101, +32,105,116,32,105,115,119,104,101,110,32,105,116,32,119,97,115,109,101,109,98, +101,114,115,32,111,102,32,114,101,108,97,116,105,111,110,32,116,111,97,99,99,111 +,109,109,111,100,97,116,101,97,108,111,110,103,32,119,105,116,104,32,73,110,32, +116,104,101,32,108,97,116,101,116,104,101,32,69,110,103,108,105,115,104,100,101, +108,105,99,105,111,117,115,34,62,116,104,105,115,32,105,115,32,110,111,116,116, +104,101,32,112,114,101,115,101,110,116,105,102,32,116,104,101,121,32,97,114,101, +97,110,100,32,102,105,110,97,108,108,121,97,32,109,97,116,116,101,114,32,111,102 +,13,10,9,60,47,100,105,118,62,13,10,13,10,60,47,115,99,114,105,112,116,62,102,97 +,115,116,101,114,32,116,104,97,110,109,97,106,111,114,105,116,121,32,111,102,97, +102,116,101,114,32,119,104,105,99,104,99,111,109,112,97,114,97,116,105,118,101, +116,111,32,109,97,105,110,116,97,105,110,105,109,112,114,111,118,101,32,116,104, +101,97,119,97,114,100,101,100,32,116,104,101,101,114,34,32,99,108,97,115,115,61, +34,102,114,97,109,101,98,111,114,100,101,114,114,101,115,116,111,114,97,116,105, +111,110,105,110,32,116,104,101,32,115,97,109,101,97,110,97,108,121,115,105,115, +32,111,102,116,104,101,105,114,32,102,105,114,115,116,68,117,114,105,110,103,32, +116,104,101,32,99,111,110,116,105,110,101,110,116,97,108,115,101,113,117,101,110 +,99,101,32,111,102,102,117,110,99,116,105,111,110,40,41,123,102,111,110,116,45, +115,105,122,101,58,32,119,111,114,107,32,111,110,32,116,104,101,60,47,115,99,114 +,105,112,116,62,10,60,98,101,103,105,110,115,32,119,105,116,104,106,97,118,97, +115,99,114,105,112,116,58,99,111,110,115,116,105,116,117,101,110,116,119,97,115, +32,102,111,117,110,100,101,100,101,113,117,105,108,105,98,114,105,117,109,97,115 +,115,117,109,101,32,116,104,97,116,105,115,32,103,105,118,101,110,32,98,121,110, +101,101,100,115,32,116,111,32,98,101,99,111,111,114,100,105,110,97,116,101,115, +116,104,101,32,118,97,114,105,111,117,115,97,114,101,32,112,97,114,116,32,111, +102,111,110,108,121,32,105,110,32,116,104,101,115,101,99,116,105,111,110,115,32, +111,102,105,115,32,97,32,99,111,109,109,111,110,116,104,101,111,114,105,101,115, +32,111,102,100,105,115,99,111,118,101,114,105,101,115,97,115,115,111,99,105,97, +116,105,111,110,101,100,103,101,32,111,102,32,116,104,101,115,116,114,101,110, +103,116,104,32,111,102,112,111,115,105,116,105,111,110,32,105,110,112,114,101, +115,101,110,116,45,100,97,121,117,110,105,118,101,114,115,97,108,108,121,116,111 +,32,102,111,114,109,32,116,104,101,98,117,116,32,105,110,115,116,101,97,100,99, +111,114,112,111,114,97,116,105,111,110,97,116,116,97,99,104,101,100,32,116,111, +105,115,32,99,111,109,109,111,110,108,121,114,101,97,115,111,110,115,32,102,111, +114,32,38,113,117,111,116,59,116,104,101,32,99,97,110,32,98,101,32,109,97,100, +101,119,97,115,32,97,98,108,101,32,116,111,119,104,105,99,104,32,109,101,97,110, +115,98,117,116,32,100,105,100,32,110,111,116,111,110,77,111,117,115,101,79,118, +101,114,97,115,32,112,111,115,115,105,98,108,101,111,112,101,114,97,116,101,100, +32,98,121,99,111,109,105,110,103,32,102,114,111,109,116,104,101,32,112,114,105, +109,97,114,121,97,100,100,105,116,105,111,110,32,111,102,102,111,114,32,115,101, +118,101,114,97,108,116,114,97,110,115,102,101,114,114,101,100,97,32,112,101,114, +105,111,100,32,111,102,97,114,101,32,97,98,108,101,32,116,111,104,111,119,101, +118,101,114,44,32,105,116,115,104,111,117,108,100,32,104,97,118,101,109,117,99, +104,32,108,97,114,103,101,114,10,9,60,47,115,99,114,105,112,116,62,97,100,111, +112,116,101,100,32,116,104,101,112,114,111,112,101,114,116,121,32,111,102,100, +105,114,101,99,116,101,100,32,98,121,101,102,102,101,99,116,105,118,101,108,121, +119,97,115,32,98,114,111,117,103,104,116,99,104,105,108,100,114,101,110,32,111, +102,80,114,111,103,114,97,109,109,105,110,103,108,111,110,103,101,114,32,116,104 +,97,110,109,97,110,117,115,99,114,105,112,116,115,119,97,114,32,97,103,97,105, +110,115,116,98,121,32,109,101,97,110,115,32,111,102,97,110,100,32,109,111,115, +116,32,111,102,115,105,109,105,108,97,114,32,116,111,32,112,114,111,112,114,105, +101,116,97,114,121,111,114,105,103,105,110,97,116,105,110,103,112,114,101,115, +116,105,103,105,111,117,115,103,114,97,109,109,97,116,105,99,97,108,101,120,112, +101,114,105,101,110,99,101,46,116,111,32,109,97,107,101,32,116,104,101,73,116,32 +,119,97,115,32,97,108,115,111,105,115,32,102,111,117,110,100,32,105,110,99,111, +109,112,101,116,105,116,111,114,115,105,110,32,116,104,101,32,85,46,83,46,114, +101,112,108,97,99,101,32,116,104,101,98,114,111,117,103,104,116,32,116,104,101, +99,97,108,99,117,108,97,116,105,111,110,102,97,108,108,32,111,102,32,116,104,101 +,116,104,101,32,103,101,110,101,114,97,108,112,114,97,99,116,105,99,97,108,108, +121,105,110,32,104,111,110,111,114,32,111,102,114,101,108,101,97,115,101,100,32, +105,110,114,101,115,105,100,101,110,116,105,97,108,97,110,100,32,115,111,109,101 +,32,111,102,107,105,110,103,32,111,102,32,116,104,101,114,101,97,99,116,105,111, +110,32,116,111,49,115,116,32,69,97,114,108,32,111,102,99,117,108,116,117,114,101 +,32,97,110,100,112,114,105,110,99,105,112,97,108,108,121,60,47,116,105,116,108, +101,62,10,32,32,116,104,101,121,32,99,97,110,32,98,101,98,97,99,107,32,116,111, +32,116,104,101,115,111,109,101,32,111,102,32,104,105,115,101,120,112,111,115,117 +,114,101,32,116,111,97,114,101,32,115,105,109,105,108,97,114,102,111,114,109,32, +111,102,32,116,104,101,97,100,100,70,97,118,111,114,105,116,101,99,105,116,105, +122,101,110,115,104,105,112,112,97,114,116,32,105,110,32,116,104,101,112,101,111 +,112,108,101,32,119,105,116,104,105,110,32,112,114,97,99,116,105,99,101,116,111, +32,99,111,110,116,105,110,117,101,38,97,109,112,59,109,105,110,117,115,59,97,112 +,112,114,111,118,101,100,32,98,121,32,116,104,101,32,102,105,114,115,116,32,97, +108,108,111,119,101,100,32,116,104,101,97,110,100,32,102,111,114,32,116,104,101, +102,117,110,99,116,105,111,110,105,110,103,112,108,97,121,105,110,103,32,116,104 +,101,115,111,108,117,116,105,111,110,32,116,111,104,101,105,103,104,116,61,34,48 +,34,32,105,110,32,104,105,115,32,98,111,111,107,109,111,114,101,32,116,104,97, +110,32,97,102,111,108,108,111,119,115,32,116,104,101,99,114,101,97,116,101,100, +32,116,104,101,112,114,101,115,101,110,99,101,32,105,110,38,110,98,115,112,59,60 +,47,116,100,62,110,97,116,105,111,110,97,108,105,115,116,116,104,101,32,105,100, +101,97,32,111,102,97,32,99,104,97,114,97,99,116,101,114,119,101,114,101,32,102, +111,114,99,101,100,32,99,108,97,115,115,61,34,98,116,110,100,97,121,115,32,111, +102,32,116,104,101,102,101,97,116,117,114,101,100,32,105,110,115,104,111,119,105 +,110,103,32,116,104,101,105,110,116,101,114,101,115,116,32,105,110,105,110,32, +112,108,97,99,101,32,111,102,116,117,114,110,32,111,102,32,116,104,101,116,104, +101,32,104,101,97,100,32,111,102,76,111,114,100,32,111,102,32,116,104,101,112, +111,108,105,116,105,99,97,108,108,121,104,97,115,32,105,116,115,32,111,119,110, +69,100,117,99,97,116,105,111,110,97,108,97,112,112,114,111,118,97,108,32,111,102 +,115,111,109,101,32,111,102,32,116,104,101,101,97,99,104,32,111,116,104,101,114, +44,98,101,104,97,118,105,111,114,32,111,102,97,110,100,32,98,101,99,97,117,115, +101,97,110,100,32,97,110,111,116,104,101,114,97,112,112,101,97,114,101,100,32, +111,110,114,101,99,111,114,100,101,100,32,105,110,98,108,97,99,107,38,113,117, +111,116,59,109,97,121,32,105,110,99,108,117,100,101,116,104,101,32,119,111,114, +108,100,39,115,99,97,110,32,108,101,97,100,32,116,111,114,101,102,101,114,115,32 +,116,111,32,97,98,111,114,100,101,114,61,34,48,34,32,103,111,118,101,114,110,109 +,101,110,116,32,119,105,110,110,105,110,103,32,116,104,101,114,101,115,117,108, +116,101,100,32,105,110,32,119,104,105,108,101,32,116,104,101,32,87,97,115,104, +105,110,103,116,111,110,44,116,104,101,32,115,117,98,106,101,99,116,99,105,116, +121,32,105,110,32,116,104,101,62,60,47,100,105,118,62,13,10,9,9,114,101,102,108, +101,99,116,32,116,104,101,116,111,32,99,111,109,112,108,101,116,101,98,101,99,97 +,109,101,32,109,111,114,101,114,97,100,105,111,97,99,116,105,118,101,114,101,106 +,101,99,116,101,100,32,98,121,119,105,116,104,111,117,116,32,97,110,121,104,105, +115,32,102,97,116,104,101,114,44,119,104,105,99,104,32,99,111,117,108,100,99,111 +,112,121,32,111,102,32,116,104,101,116,111,32,105,110,100,105,99,97,116,101,97, +32,112,111,108,105,116,105,99,97,108,97,99,99,111,117,110,116,115,32,111,102,99, +111,110,115,116,105,116,117,116,101,115,119,111,114,107,101,100,32,119,105,116, +104,101,114,60,47,97,62,60,47,108,105,62,111,102,32,104,105,115,32,108,105,102, +101,97,99,99,111,109,112,97,110,105,101,100,99,108,105,101,110,116,87,105,100, +116,104,112,114,101,118,101,110,116,32,116,104,101,76,101,103,105,115,108,97,116 +,105,118,101,100,105,102,102,101,114,101,110,116,108,121,116,111,103,101,116,104 +,101,114,32,105,110,104,97,115,32,115,101,118,101,114,97,108,102,111,114,32,97, +110,111,116,104,101,114,116,101,120,116,32,111,102,32,116,104,101,102,111,117, +110,100,101,100,32,116,104,101,101,32,119,105,116,104,32,116,104,101,32,105,115, +32,117,115,101,100,32,102,111,114,99,104,97,110,103,101,100,32,116,104,101,117, +115,117,97,108,108,121,32,116,104,101,112,108,97,99,101,32,119,104,101,114,101, +119,104,101,114,101,97,115,32,116,104,101,62,32,60,97,32,104,114,101,102,61,34, +34,62,60,97,32,104,114,101,102,61,34,116,104,101,109,115,101,108,118,101,115,44, +97,108,116,104,111,117,103,104,32,104,101,116,104,97,116,32,99,97,110,32,98,101, +116,114,97,100,105,116,105,111,110,97,108,114,111,108,101,32,111,102,32,116,104, +101,97,115,32,97,32,114,101,115,117,108,116,114,101,109,111,118,101,67,104,105, +108,100,100,101,115,105,103,110,101,100,32,98,121,119,101,115,116,32,111,102,32, +116,104,101,83,111,109,101,32,112,101,111,112,108,101,112,114,111,100,117,99,116 +,105,111,110,44,115,105,100,101,32,111,102,32,116,104,101,110,101,119,115,108, +101,116,116,101,114,115,117,115,101,100,32,98,121,32,116,104,101,100,111,119,110 +,32,116,111,32,116,104,101,97,99,99,101,112,116,101,100,32,98,121,108,105,118, +101,32,105,110,32,116,104,101,97,116,116,101,109,112,116,115,32,116,111,111,117, +116,115,105,100,101,32,116,104,101,102,114,101,113,117,101,110,99,105,101,115,72 +,111,119,101,118,101,114,44,32,105,110,112,114,111,103,114,97,109,109,101,114, +115,97,116,32,108,101,97,115,116,32,105,110,97,112,112,114,111,120,105,109,97, +116,101,97,108,116,104,111,117,103,104,32,105,116,119,97,115,32,112,97,114,116, +32,111,102,97,110,100,32,118,97,114,105,111,117,115,71,111,118,101,114,110,111, +114,32,111,102,116,104,101,32,97,114,116,105,99,108,101,116,117,114,110,101,100, +32,105,110,116,111,62,60,97,32,104,114,101,102,61,34,47,116,104,101,32,101,99, +111,110,111,109,121,105,115,32,116,104,101,32,109,111,115,116,109,111,115,116,32 +,119,105,100,101,108,121,119,111,117,108,100,32,108,97,116,101,114,97,110,100,32 +,112,101,114,104,97,112,115,114,105,115,101,32,116,111,32,116,104,101,111,99,99, +117,114,115,32,119,104,101,110,117,110,100,101,114,32,119,104,105,99,104,99,111, +110,100,105,116,105,111,110,115,46,116,104,101,32,119,101,115,116,101,114,110, +116,104,101,111,114,121,32,116,104,97,116,105,115,32,112,114,111,100,117,99,101, +100,116,104,101,32,99,105,116,121,32,111,102,105,110,32,119,104,105,99,104,32, +104,101,115,101,101,110,32,105,110,32,116,104,101,116,104,101,32,99,101,110,116, +114,97,108,98,117,105,108,100,105,110,103,32,111,102,109,97,110,121,32,111,102, +32,104,105,115,97,114,101,97,32,111,102,32,116,104,101,105,115,32,116,104,101,32 +,111,110,108,121,109,111,115,116,32,111,102,32,116,104,101,109,97,110,121,32,111 +,102,32,116,104,101,116,104,101,32,87,101,115,116,101,114,110,84,104,101,114,101 +,32,105,115,32,110,111,101,120,116,101,110,100,101,100,32,116,111,83,116,97,116, +105,115,116,105,99,97,108,99,111,108,115,112,97,110,61,50,32,124,115,104,111,114 +,116,32,115,116,111,114,121,112,111,115,115,105,98,108,101,32,116,111,116,111, +112,111,108,111,103,105,99,97,108,99,114,105,116,105,99,97,108,32,111,102,114, +101,112,111,114,116,101,100,32,116,111,97,32,67,104,114,105,115,116,105,97,110, +100,101,99,105,115,105,111,110,32,116,111,105,115,32,101,113,117,97,108,32,116, +111,112,114,111,98,108,101,109,115,32,111,102,84,104,105,115,32,99,97,110,32,98, +101,109,101,114,99,104,97,110,100,105,115,101,102,111,114,32,109,111,115,116,32, +111,102,110,111,32,101,118,105,100,101,110,99,101,101,100,105,116,105,111,110, +115,32,111,102,101,108,101,109,101,110,116,115,32,105,110,38,113,117,111,116,59, +46,32,84,104,101,99,111,109,47,105,109,97,103,101,115,47,119,104,105,99,104,32, +109,97,107,101,115,116,104,101,32,112,114,111,99,101,115,115,114,101,109,97,105, +110,115,32,116,104,101,108,105,116,101,114,97,116,117,114,101,44,105,115,32,97, +32,109,101,109,98,101,114,116,104,101,32,112,111,112,117,108,97,114,116,104,101, +32,97,110,99,105,101,110,116,112,114,111,98,108,101,109,115,32,105,110,116,105, +109,101,32,111,102,32,116,104,101,100,101,102,101,97,116,101,100,32,98,121,98, +111,100,121,32,111,102,32,116,104,101,97,32,102,101,119,32,121,101,97,114,115, +109,117,99,104,32,111,102,32,116,104,101,116,104,101,32,119,111,114,107,32,111, +102,67,97,108,105,102,111,114,110,105,97,44,115,101,114,118,101,100,32,97,115,32 +,97,103,111,118,101,114,110,109,101,110,116,46,99,111,110,99,101,112,116,115,32, +111,102,109,111,118,101,109,101,110,116,32,105,110,9,9,60,100,105,118,32,105,100 +,61,34,105,116,34,32,118,97,108,117,101,61,34,108,97,110,103,117,97,103,101,32, +111,102,97,115,32,116,104,101,121,32,97,114,101,112,114,111,100,117,99,101,100, +32,105,110,105,115,32,116,104,97,116,32,116,104,101,101,120,112,108,97,105,110, +32,116,104,101,100,105,118,62,60,47,100,105,118,62,10,72,111,119,101,118,101,114 +,32,116,104,101,108,101,97,100,32,116,111,32,116,104,101,9,60,97,32,104,114,101, +102,61,34,47,119,97,115,32,103,114,97,110,116,101,100,112,101,111,112,108,101,32 +,104,97,118,101,99,111,110,116,105,110,117,97,108,108,121,119,97,115,32,115,101, +101,110,32,97,115,97,110,100,32,114,101,108,97,116,101,100,116,104,101,32,114, +111,108,101,32,111,102,112,114,111,112,111,115,101,100,32,98,121,111,102,32,116, +104,101,32,98,101,115,116,101,97,99,104,32,111,116,104,101,114,46,67,111,110,115 +,116,97,110,116,105,110,101,112,101,111,112,108,101,32,102,114,111,109,100,105, +97,108,101,99,116,115,32,111,102,116,111,32,114,101,118,105,115,105,111,110,119, +97,115,32,114,101,110,97,109,101,100,97,32,115,111,117,114,99,101,32,111,102,116 +,104,101,32,105,110,105,116,105,97,108,108,97,117,110,99,104,101,100,32,105,110, +112,114,111,118,105,100,101,32,116,104,101,116,111,32,116,104,101,32,119,101,115 +,116,119,104,101,114,101,32,116,104,101,114,101,97,110,100,32,115,105,109,105, +108,97,114,98,101,116,119,101,101,110,32,116,119,111,105,115,32,97,108,115,111, +32,116,104,101,69,110,103,108,105,115,104,32,97,110,100,99,111,110,100,105,116, +105,111,110,115,44,116,104,97,116,32,105,116,32,119,97,115,101,110,116,105,116, +108,101,100,32,116,111,116,104,101,109,115,101,108,118,101,115,46,113,117,97,110 +,116,105,116,121,32,111,102,114,97,110,115,112,97,114,101,110,99,121,116,104,101 +,32,115,97,109,101,32,97,115,116,111,32,106,111,105,110,32,116,104,101,99,111, +117,110,116,114,121,32,97,110,100,116,104,105,115,32,105,115,32,116,104,101,84, +104,105,115,32,108,101,100,32,116,111,97,32,115,116,97,116,101,109,101,110,116, +99,111,110,116,114,97,115,116,32,116,111,108,97,115,116,73,110,100,101,120,79, +102,116,104,114,111,117,103,104,32,104,105,115,105,115,32,100,101,115,105,103, +110,101,100,116,104,101,32,116,101,114,109,32,105,115,105,115,32,112,114,111,118 +,105,100,101,100,112,114,111,116,101,99,116,32,116,104,101,110,103,60,47,97,62, +60,47,108,105,62,84,104,101,32,99,117,114,114,101,110,116,116,104,101,32,115,105 +,116,101,32,111,102,115,117,98,115,116,97,110,116,105,97,108,101,120,112,101,114 +,105,101,110,99,101,44,105,110,32,116,104,101,32,87,101,115,116,116,104,101,121, +32,115,104,111,117,108,100,115,108,111,118,101,110,196,141,105,110,97,99,111,109 +,101,110,116,97,114,105,111,115,117,110,105,118,101,114,115,105,100,97,100,99, +111,110,100,105,99,105,111,110,101,115,97,99,116,105,118,105,100,97,100,101,115, +101,120,112,101,114,105,101,110,99,105,97,116,101,99,110,111,108,111,103,195,173 +,97,112,114,111,100,117,99,99,105,195,179,110,112,117,110,116,117,97,99,105,195, +179,110,97,112,108,105,99,97,99,105,195,179,110,99,111,110,116,114,97,115,101, +195,177,97,99,97,116,101,103,111,114,195,173,97,115,114,101,103,105,115,116,114, +97,114,115,101,112,114,111,102,101,115,105,111,110,97,108,116,114,97,116,97,109, +105,101,110,116,111,114,101,103,195,173,115,116,114,97,116,101,115,101,99,114, +101,116,97,114,195,173,97,112,114,105,110,99,105,112,97,108,101,115,112,114,111, +116,101,99,99,105,195,179,110,105,109,112,111,114,116,97,110,116,101,115,105,109 +,112,111,114,116,97,110,99,105,97,112,111,115,105,98,105,108,105,100,97,100,105, +110,116,101,114,101,115,97,110,116,101,99,114,101,99,105,109,105,101,110,116,111 +,110,101,99,101,115,105,100,97,100,101,115,115,117,115,99,114,105,98,105,114,115 +,101,97,115,111,99,105,97,99,105,195,179,110,100,105,115,112,111,110,105,98,108, +101,115,101,118,97,108,117,97,99,105,195,179,110,101,115,116,117,100,105,97,110, +116,101,115,114,101,115,112,111,110,115,97,98,108,101,114,101,115,111,108,117,99 +,105,195,179,110,103,117,97,100,97,108,97,106,97,114,97,114,101,103,105,115,116, +114,97,100,111,115,111,112,111,114,116,117,110,105,100,97,100,99,111,109,101,114 +,99,105,97,108,101,115,102,111,116,111,103,114,97,102,195,173,97,97,117,116,111, +114,105,100,97,100,101,115,105,110,103,101,110,105,101,114,195,173,97,116,101, +108,101,118,105,115,105,195,179,110,99,111,109,112,101,116,101,110,99,105,97,111 +,112,101,114,97,99,105,111,110,101,115,101,115,116,97,98,108,101,99,105,100,111, +115,105,109,112,108,101,109,101,110,116,101,97,99,116,117,97,108,109,101,110,116 +,101,110,97,118,101,103,97,99,105,195,179,110,99,111,110,102,111,114,109,105,100 +,97,100,108,105,110,101,45,104,101,105,103,104,116,58,102,111,110,116,45,102,97, +109,105,108,121,58,34,32,58,32,34,104,116,116,112,58,47,47,97,112,112,108,105,99 +,97,116,105,111,110,115,108,105,110,107,34,32,104,114,101,102,61,34,115,112,101, +99,105,102,105,99,97,108,108,121,47,47,60,33,91,67,68,65,84,65,91,10,79,114,103, +97,110,105,122,97,116,105,111,110,100,105,115,116,114,105,98,117,116,105,111,110 +,48,112,120,59,32,104,101,105,103,104,116,58,114,101,108,97,116,105,111,110,115, +104,105,112,100,101,118,105,99,101,45,119,105,100,116,104,60,100,105,118,32,99, +108,97,115,115,61,34,60,108,97,98,101,108,32,102,111,114,61,34,114,101,103,105, +115,116,114,97,116,105,111,110,60,47,110,111,115,99,114,105,112,116,62,10,47,105 +,110,100,101,120,46,104,116,109,108,34,119,105,110,100,111,119,46,111,112,101, +110,40,32,33,105,109,112,111,114,116,97,110,116,59,97,112,112,108,105,99,97,116, +105,111,110,47,105,110,100,101,112,101,110,100,101,110,99,101,47,47,119,119,119, +46,103,111,111,103,108,101,111,114,103,97,110,105,122,97,116,105,111,110,97,117, +116,111,99,111,109,112,108,101,116,101,114,101,113,117,105,114,101,109,101,110, +116,115,99,111,110,115,101,114,118,97,116,105,118,101,60,102,111,114,109,32,110, +97,109,101,61,34,105,110,116,101,108,108,101,99,116,117,97,108,109,97,114,103, +105,110,45,108,101,102,116,58,49,56,116,104,32,99,101,110,116,117,114,121,97,110 +,32,105,109,112,111,114,116,97,110,116,105,110,115,116,105,116,117,116,105,111, +110,115,97,98,98,114,101,118,105,97,116,105,111,110,60,105,109,103,32,99,108,97, +115,115,61,34,111,114,103,97,110,105,115,97,116,105,111,110,99,105,118,105,108, +105,122,97,116,105,111,110,49,57,116,104,32,99,101,110,116,117,114,121,97,114,99 +,104,105,116,101,99,116,117,114,101,105,110,99,111,114,112,111,114,97,116,101, +100,50,48,116,104,32,99,101,110,116,117,114,121,45,99,111,110,116,97,105,110,101 +,114,34,62,109,111,115,116,32,110,111,116,97,98,108,121,47,62,60,47,97,62,60,47, +100,105,118,62,110,111,116,105,102,105,99,97,116,105,111,110,39,117,110,100,101, +102,105,110,101,100,39,41,70,117,114,116,104,101,114,109,111,114,101,44,98,101, +108,105,101,118,101,32,116,104,97,116,105,110,110,101,114,72,84,77,76,32,61,32, +112,114,105,111,114,32,116,111,32,116,104,101,100,114,97,109,97,116,105,99,97, +108,108,121,114,101,102,101,114,114,105,110,103,32,116,111,110,101,103,111,116, +105,97,116,105,111,110,115,104,101,97,100,113,117,97,114,116,101,114,115,83,111, +117,116,104,32,65,102,114,105,99,97,117,110,115,117,99,99,101,115,115,102,117, +108,80,101,110,110,115,121,108,118,97,110,105,97,65,115,32,97,32,114,101,115,117 +,108,116,44,60,104,116,109,108,32,108,97,110,103,61,34,38,108,116,59,47,115,117, +112,38,103,116,59,100,101,97,108,105,110,103,32,119,105,116,104,112,104,105,108, +97,100,101,108,112,104,105,97,104,105,115,116,111,114,105,99,97,108,108,121,41, +59,60,47,115,99,114,105,112,116,62,10,112,97,100,100,105,110,103,45,116,111,112, +58,101,120,112,101,114,105,109,101,110,116,97,108,103,101,116,65,116,116,114,105 +,98,117,116,101,105,110,115,116,114,117,99,116,105,111,110,115,116,101,99,104, +110,111,108,111,103,105,101,115,112,97,114,116,32,111,102,32,116,104,101,32,61, +102,117,110,99,116,105,111,110,40,41,123,115,117,98,115,99,114,105,112,116,105, +111,110,108,46,100,116,100,34,62,13,10,60,104,116,103,101,111,103,114,97,112,104 +,105,99,97,108,67,111,110,115,116,105,116,117,116,105,111,110,39,44,32,102,117, +110,99,116,105,111,110,40,115,117,112,112,111,114,116,101,100,32,98,121,97,103, +114,105,99,117,108,116,117,114,97,108,99,111,110,115,116,114,117,99,116,105,111, +110,112,117,98,108,105,99,97,116,105,111,110,115,102,111,110,116,45,115,105,122, +101,58,32,49,97,32,118,97,114,105,101,116,121,32,111,102,60,100,105,118,32,115, +116,121,108,101,61,34,69,110,99,121,99,108,111,112,101,100,105,97,105,102,114,97 +,109,101,32,115,114,99,61,34,100,101,109,111,110,115,116,114,97,116,101,100,97, +99,99,111,109,112,108,105,115,104,101,100,117,110,105,118,101,114,115,105,116, +105,101,115,68,101,109,111,103,114,97,112,104,105,99,115,41,59,60,47,115,99,114, +105,112,116,62,60,100,101,100,105,99,97,116,101,100,32,116,111,107,110,111,119, +108,101,100,103,101,32,111,102,115,97,116,105,115,102,97,99,116,105,111,110,112, +97,114,116,105,99,117,108,97,114,108,121,60,47,100,105,118,62,60,47,100,105,118, +62,69,110,103,108,105,115,104,32,40,85,83,41,97,112,112,101,110,100,67,104,105, +108,100,40,116,114,97,110,115,109,105,115,115,105,111,110,115,46,32,72,111,119, +101,118,101,114,44,32,105,110,116,101,108,108,105,103,101,110,99,101,34,32,116, +97,98,105,110,100,101,120,61,34,102,108,111,97,116,58,114,105,103,104,116,59,67, +111,109,109,111,110,119,101,97,108,116,104,114,97,110,103,105,110,103,32,102,114 +,111,109,105,110,32,119,104,105,99,104,32,116,104,101,97,116,32,108,101,97,115, +116,32,111,110,101,114,101,112,114,111,100,117,99,116,105,111,110,101,110,99,121 +,99,108,111,112,101,100,105,97,59,102,111,110,116,45,115,105,122,101,58,49,106, +117,114,105,115,100,105,99,116,105,111,110,97,116,32,116,104,97,116,32,116,105, +109,101,34,62,60,97,32,99,108,97,115,115,61,34,73,110,32,97,100,100,105,116,105, +111,110,44,100,101,115,99,114,105,112,116,105,111,110,43,99,111,110,118,101,114, +115,97,116,105,111,110,99,111,110,116,97,99,116,32,119,105,116,104,105,115,32, +103,101,110,101,114,97,108,108,121,114,34,32,99,111,110,116,101,110,116,61,34, +114,101,112,114,101,115,101,110,116,105,110,103,38,108,116,59,109,97,116,104,38, +103,116,59,112,114,101,115,101,110,116,97,116,105,111,110,111,99,99,97,115,105, +111,110,97,108,108,121,60,105,109,103,32,119,105,100,116,104,61,34,110,97,118, +105,103,97,116,105,111,110,34,62,99,111,109,112,101,110,115,97,116,105,111,110, +99,104,97,109,112,105,111,110,115,104,105,112,109,101,100,105,97,61,34,97,108, +108,34,32,118,105,111,108,97,116,105,111,110,32,111,102,114,101,102,101,114,101, +110,99,101,32,116,111,114,101,116,117,114,110,32,116,114,117,101,59,83,116,114, +105,99,116,47,47,69,78,34,32,116,114,97,110,115,97,99,116,105,111,110,115,105, +110,116,101,114,118,101,110,116,105,111,110,118,101,114,105,102,105,99,97,116, +105,111,110,73,110,102,111,114,109,97,116,105,111,110,32,100,105,102,102,105,99, +117,108,116,105,101,115,67,104,97,109,112,105,111,110,115,104,105,112,99,97,112, +97,98,105,108,105,116,105,101,115,60,33,91,101,110,100,105,102,93,45,45,62,125, +10,60,47,115,99,114,105,112,116,62,10,67,104,114,105,115,116,105,97,110,105,116, +121,102,111,114,32,101,120,97,109,112,108,101,44,80,114,111,102,101,115,115,105, +111,110,97,108,114,101,115,116,114,105,99,116,105,111,110,115,115,117,103,103, +101,115,116,32,116,104,97,116,119,97,115,32,114,101,108,101,97,115,101,100,40, +115,117,99,104,32,97,115,32,116,104,101,114,101,109,111,118,101,67,108,97,115, +115,40,117,110,101,109,112,108,111,121,109,101,110,116,116,104,101,32,65,109,101 +,114,105,99,97,110,115,116,114,117,99,116,117,114,101,32,111,102,47,105,110,100, +101,120,46,104,116,109,108,32,112,117,98,108,105,115,104,101,100,32,105,110,115, +112,97,110,32,99,108,97,115,115,61,34,34,62,60,97,32,104,114,101,102,61,34,47, +105,110,116,114,111,100,117,99,116,105,111,110,98,101,108,111,110,103,105,110, +103,32,116,111,99,108,97,105,109,101,100,32,116,104,97,116,99,111,110,115,101, +113,117,101,110,99,101,115,60,109,101,116,97,32,110,97,109,101,61,34,71,117,105, +100,101,32,116,111,32,116,104,101,111,118,101,114,119,104,101,108,109,105,110, +103,97,103,97,105,110,115,116,32,116,104,101,32,99,111,110,99,101,110,116,114,97 +,116,101,100,44,10,46,110,111,110,116,111,117,99,104,32,111,98,115,101,114,118, +97,116,105,111,110,115,60,47,97,62,10,60,47,100,105,118,62,10,102,32,40,100,111, +99,117,109,101,110,116,46,98,111,114,100,101,114,58,32,49,112,120,32,123,102,111 +,110,116,45,115,105,122,101,58,49,116,114,101,97,116,109,101,110,116,32,111,102, +48,34,32,104,101,105,103,104,116,61,34,49,109,111,100,105,102,105,99,97,116,105, +111,110,73,110,100,101,112,101,110,100,101,110,99,101,100,105,118,105,100,101, +100,32,105,110,116,111,103,114,101,97,116,101,114,32,116,104,97,110,97,99,104, +105,101,118,101,109,101,110,116,115,101,115,116,97,98,108,105,115,104,105,110, +103,74,97,118,97,83,99,114,105,112,116,34,32,110,101,118,101,114,116,104,101,108 +,101,115,115,115,105,103,110,105,102,105,99,97,110,99,101,66,114,111,97,100,99, +97,115,116,105,110,103,62,38,110,98,115,112,59,60,47,116,100,62,99,111,110,116, +97,105,110,101,114,34,62,10,115,117,99,104,32,97,115,32,116,104,101,32,105,110, +102,108,117,101,110,99,101,32,111,102,97,32,112,97,114,116,105,99,117,108,97,114 +,115,114,99,61,39,104,116,116,112,58,47,47,110,97,118,105,103,97,116,105,111,110 +,34,32,104,97,108,102,32,111,102,32,116,104,101,32,115,117,98,115,116,97,110,116 +,105,97,108,32,38,110,98,115,112,59,60,47,100,105,118,62,97,100,118,97,110,116, +97,103,101,32,111,102,100,105,115,99,111,118,101,114,121,32,111,102,102,117,110, +100,97,109,101,110,116,97,108,32,109,101,116,114,111,112,111,108,105,116,97,110, +116,104,101,32,111,112,112,111,115,105,116,101,34,32,120,109,108,58,108,97,110, +103,61,34,100,101,108,105,98,101,114,97,116,101,108,121,97,108,105,103,110,61,99 +,101,110,116,101,114,101,118,111,108,117,116,105,111,110,32,111,102,112,114,101, +115,101,114,118,97,116,105,111,110,105,109,112,114,111,118,101,109,101,110,116, +115,98,101,103,105,110,110,105,110,103,32,105,110,74,101,115,117,115,32,67,104, +114,105,115,116,80,117,98,108,105,99,97,116,105,111,110,115,100,105,115,97,103, +114,101,101,109,101,110,116,116,101,120,116,45,97,108,105,103,110,58,114,44,32, +102,117,110,99,116,105,111,110,40,41,115,105,109,105,108,97,114,105,116,105,101, +115,98,111,100,121,62,60,47,104,116,109,108,62,105,115,32,99,117,114,114,101,110 +,116,108,121,97,108,112,104,97,98,101,116,105,99,97,108,105,115,32,115,111,109, +101,116,105,109,101,115,116,121,112,101,61,34,105,109,97,103,101,47,109,97,110, +121,32,111,102,32,116,104,101,32,102,108,111,119,58,104,105,100,100,101,110,59, +97,118,97,105,108,97,98,108,101,32,105,110,100,101,115,99,114,105,98,101,32,116, +104,101,101,120,105,115,116,101,110,99,101,32,111,102,97,108,108,32,111,118,101, +114,32,116,104,101,116,104,101,32,73,110,116,101,114,110,101,116,9,60,117,108,32 +,99,108,97,115,115,61,34,105,110,115,116,97,108,108,97,116,105,111,110,110,101, +105,103,104,98,111,114,104,111,111,100,97,114,109,101,100,32,102,111,114,99,101, +115,114,101,100,117,99,105,110,103,32,116,104,101,99,111,110,116,105,110,117,101 +,115,32,116,111,78,111,110,101,116,104,101,108,101,115,115,44,116,101,109,112, +101,114,97,116,117,114,101,115,10,9,9,60,97,32,104,114,101,102,61,34,99,108,111, +115,101,32,116,111,32,116,104,101,101,120,97,109,112,108,101,115,32,111,102,32, +105,115,32,97,98,111,117,116,32,116,104,101,40,115,101,101,32,98,101,108,111,119 +,41,46,34,32,105,100,61,34,115,101,97,114,99,104,112,114,111,102,101,115,115,105 +,111,110,97,108,105,115,32,97,118,97,105,108,97,98,108,101,116,104,101,32,111, +102,102,105,99,105,97,108,9,9,60,47,115,99,114,105,112,116,62,10,10,9,9,60,100, +105,118,32,105,100,61,34,97,99,99,101,108,101,114,97,116,105,111,110,116,104,114 +,111,117,103,104,32,116,104,101,32,72,97,108,108,32,111,102,32,70,97,109,101,100 +,101,115,99,114,105,112,116,105,111,110,115,116,114,97,110,115,108,97,116,105, +111,110,115,105,110,116,101,114,102,101,114,101,110,99,101,32,116,121,112,101,61 +,39,116,101,120,116,47,114,101,99,101,110,116,32,121,101,97,114,115,105,110,32, +116,104,101,32,119,111,114,108,100,118,101,114,121,32,112,111,112,117,108,97,114 +,123,98,97,99,107,103,114,111,117,110,100,58,116,114,97,100,105,116,105,111,110, +97,108,32,115,111,109,101,32,111,102,32,116,104,101,32,99,111,110,110,101,99,116 +,101,100,32,116,111,101,120,112,108,111,105,116,97,116,105,111,110,101,109,101, +114,103,101,110,99,101,32,111,102,99,111,110,115,116,105,116,117,116,105,111,110 +,65,32,72,105,115,116,111,114,121,32,111,102,115,105,103,110,105,102,105,99,97, +110,116,32,109,97,110,117,102,97,99,116,117,114,101,100,101,120,112,101,99,116, +97,116,105,111,110,115,62,60,110,111,115,99,114,105,112,116,62,60,99,97,110,32, +98,101,32,102,111,117,110,100,98,101,99,97,117,115,101,32,116,104,101,32,104,97, +115,32,110,111,116,32,98,101,101,110,110,101,105,103,104,98,111,117,114,105,110, +103,119,105,116,104,111,117,116,32,116,104,101,32,97,100,100,101,100,32,116,111, +32,116,104,101,9,60,108,105,32,99,108,97,115,115,61,34,105,110,115,116,114,117, +109,101,110,116,97,108,83,111,118,105,101,116,32,85,110,105,111,110,97,99,107, +110,111,119,108,101,100,103,101,100,119,104,105,99,104,32,99,97,110,32,98,101, +110,97,109,101,32,102,111,114,32,116,104,101,97,116,116,101,110,116,105,111,110, +32,116,111,97,116,116,101,109,112,116,115,32,116,111,32,100,101,118,101,108,111, +112,109,101,110,116,115,73,110,32,102,97,99,116,44,32,116,104,101,60,108,105,32, +99,108,97,115,115,61,34,97,105,109,112,108,105,99,97,116,105,111,110,115,115,117 +,105,116,97,98,108,101,32,102,111,114,109,117,99,104,32,111,102,32,116,104,101, +32,99,111,108,111,110,105,122,97,116,105,111,110,112,114,101,115,105,100,101,110 +,116,105,97,108,99,97,110,99,101,108,66,117,98,98,108,101,32,73,110,102,111,114, +109,97,116,105,111,110,109,111,115,116,32,111,102,32,116,104,101,32,105,115,32, +100,101,115,99,114,105,98,101,100,114,101,115,116,32,111,102,32,116,104,101,32, +109,111,114,101,32,111,114,32,108,101,115,115,105,110,32,83,101,112,116,101,109, +98,101,114,73,110,116,101,108,108,105,103,101,110,99,101,115,114,99,61,34,104, +116,116,112,58,47,47,112,120,59,32,104,101,105,103,104,116,58,32,97,118,97,105, +108,97,98,108,101,32,116,111,109,97,110,117,102,97,99,116,117,114,101,114,104, +117,109,97,110,32,114,105,103,104,116,115,108,105,110,107,32,104,114,101,102,61, +34,47,97,118,97,105,108,97,98,105,108,105,116,121,112,114,111,112,111,114,116, +105,111,110,97,108,111,117,116,115,105,100,101,32,116,104,101,32,97,115,116,114, +111,110,111,109,105,99,97,108,104,117,109,97,110,32,98,101,105,110,103,115,110, +97,109,101,32,111,102,32,116,104,101,32,97,114,101,32,102,111,117,110,100,32,105 +,110,97,114,101,32,98,97,115,101,100,32,111,110,115,109,97,108,108,101,114,32, +116,104,97,110,97,32,112,101,114,115,111,110,32,119,104,111,101,120,112,97,110, +115,105,111,110,32,111,102,97,114,103,117,105,110,103,32,116,104,97,116,110,111, +119,32,107,110,111,119,110,32,97,115,73,110,32,116,104,101,32,101,97,114,108,121 +,105,110,116,101,114,109,101,100,105,97,116,101,100,101,114,105,118,101,100,32, +102,114,111,109,83,99,97,110,100,105,110,97,118,105,97,110,60,47,97,62,60,47,100 +,105,118,62,13,10,99,111,110,115,105,100,101,114,32,116,104,101,97,110,32,101, +115,116,105,109,97,116,101,100,116,104,101,32,78,97,116,105,111,110,97,108,60, +100,105,118,32,105,100,61,34,112,97,103,114,101,115,117,108,116,105,110,103,32, +105,110,99,111,109,109,105,115,115,105,111,110,101,100,97,110,97,108,111,103,111 +,117,115,32,116,111,97,114,101,32,114,101,113,117,105,114,101,100,47,117,108,62, +10,60,47,100,105,118,62,10,119,97,115,32,98,97,115,101,100,32,111,110,97,110,100 +,32,98,101,99,97,109,101,32,97,38,110,98,115,112,59,38,110,98,115,112,59,116,34, +32,118,97,108,117,101,61,34,34,32,119,97,115,32,99,97,112,116,117,114,101,100, +110,111,32,109,111,114,101,32,116,104,97,110,114,101,115,112,101,99,116,105,118, +101,108,121,99,111,110,116,105,110,117,101,32,116,111,32,62,13,10,60,104,101,97, +100,62,13,10,60,119,101,114,101,32,99,114,101,97,116,101,100,109,111,114,101,32, +103,101,110,101,114,97,108,105,110,102,111,114,109,97,116,105,111,110,32,117,115 +,101,100,32,102,111,114,32,116,104,101,105,110,100,101,112,101,110,100,101,110, +116,32,116,104,101,32,73,109,112,101,114,105,97,108,99,111,109,112,111,110,101, +110,116,32,111,102,116,111,32,116,104,101,32,110,111,114,116,104,105,110,99,108, +117,100,101,32,116,104,101,32,67,111,110,115,116,114,117,99,116,105,111,110,115, +105,100,101,32,111,102,32,116,104,101,32,119,111,117,108,100,32,110,111,116,32, +98,101,102,111,114,32,105,110,115,116,97,110,99,101,105,110,118,101,110,116,105, +111,110,32,111,102,109,111,114,101,32,99,111,109,112,108,101,120,99,111,108,108, +101,99,116,105,118,101,108,121,98,97,99,107,103,114,111,117,110,100,58,32,116, +101,120,116,45,97,108,105,103,110,58,32,105,116,115,32,111,114,105,103,105,110, +97,108,105,110,116,111,32,97,99,99,111,117,110,116,116,104,105,115,32,112,114, +111,99,101,115,115,97,110,32,101,120,116,101,110,115,105,118,101,104,111,119,101 +,118,101,114,44,32,116,104,101,116,104,101,121,32,97,114,101,32,110,111,116,114, +101,106,101,99,116,101,100,32,116,104,101,99,114,105,116,105,99,105,115,109,32, +111,102,100,117,114,105,110,103,32,119,104,105,99,104,112,114,111,98,97,98,108, +121,32,116,104,101,116,104,105,115,32,97,114,116,105,99,108,101,40,102,117,110, +99,116,105,111,110,40,41,123,73,116,32,115,104,111,117,108,100,32,98,101,97,110, +32,97,103,114,101,101,109,101,110,116,97,99,99,105,100,101,110,116,97,108,108, +121,100,105,102,102,101,114,115,32,102,114,111,109,65,114,99,104,105,116,101,99, +116,117,114,101,98,101,116,116,101,114,32,107,110,111,119,110,97,114,114,97,110, +103,101,109,101,110,116,115,105,110,102,108,117,101,110,99,101,32,111,110,97,116 +,116,101,110,100,101,100,32,116,104,101,105,100,101,110,116,105,99,97,108,32,116 +,111,115,111,117,116,104,32,111,102,32,116,104,101,112,97,115,115,32,116,104,114 +,111,117,103,104,120,109,108,34,32,116,105,116,108,101,61,34,119,101,105,103,104 +,116,58,98,111,108,100,59,99,114,101,97,116,105,110,103,32,116,104,101,100,105, +115,112,108,97,121,58,110,111,110,101,114,101,112,108,97,99,101,100,32,116,104, +101,60,105,109,103,32,115,114,99,61,34,47,105,104,116,116,112,115,58,47,47,119, +119,119,46,87,111,114,108,100,32,87,97,114,32,73,73,116,101,115,116,105,109,111, +110,105,97,108,115,102,111,117,110,100,32,105,110,32,116,104,101,114,101,113,117 +,105,114,101,100,32,116,111,32,97,110,100,32,116,104,97,116,32,116,104,101,98, +101,116,119,101,101,110,32,116,104,101,32,119,97,115,32,100,101,115,105,103,110, +101,100,99,111,110,115,105,115,116,115,32,111,102,32,99,111,110,115,105,100,101, +114,97,98,108,121,112,117,98,108,105,115,104,101,100,32,98,121,116,104,101,32, +108,97,110,103,117,97,103,101,67,111,110,115,101,114,118,97,116,105,111,110,99, +111,110,115,105,115,116,101,100,32,111,102,114,101,102,101,114,32,116,111,32,116 +,104,101,98,97,99,107,32,116,111,32,116,104,101,32,99,115,115,34,32,109,101,100, +105,97,61,34,80,101,111,112,108,101,32,102,114,111,109,32,97,118,97,105,108,97, +98,108,101,32,111,110,112,114,111,118,101,100,32,116,111,32,98,101,115,117,103, +103,101,115,116,105,111,110,115,34,119,97,115,32,107,110,111,119,110,32,97,115, +118,97,114,105,101,116,105,101,115,32,111,102,108,105,107,101,108,121,32,116,111 +,32,98,101,99,111,109,112,114,105,115,101,100,32,111,102,115,117,112,112,111,114 +,116,32,116,104,101,32,104,97,110,100,115,32,111,102,32,116,104,101,99,111,117, +112,108,101,100,32,119,105,116,104,99,111,110,110,101,99,116,32,97,110,100,32,98 +,111,114,100,101,114,58,110,111,110,101,59,112,101,114,102,111,114,109,97,110,99 +,101,115,98,101,102,111,114,101,32,98,101,105,110,103,108,97,116,101,114,32,98, +101,99,97,109,101,99,97,108,99,117,108,97,116,105,111,110,115,111,102,116,101, +110,32,99,97,108,108,101,100,114,101,115,105,100,101,110,116,115,32,111,102,109, +101,97,110,105,110,103,32,116,104,97,116,62,60,108,105,32,99,108,97,115,115,61, +34,101,118,105,100,101,110,99,101,32,102,111,114,101,120,112,108,97,110,97,116, +105,111,110,115,101,110,118,105,114,111,110,109,101,110,116,115,34,62,60,47,97, +62,60,47,100,105,118,62,119,104,105,99,104,32,97,108,108,111,119,115,73,110,116, +114,111,100,117,99,116,105,111,110,100,101,118,101,108,111,112,101,100,32,98,121 +,97,32,119,105,100,101,32,114,97,110,103,101,111,110,32,98,101,104,97,108,102,32 +,111,102,118,97,108,105,103,110,61,34,116,111,112,34,112,114,105,110,99,105,112, +108,101,32,111,102,97,116,32,116,104,101,32,116,105,109,101,44,60,47,110,111,115 +,99,114,105,112,116,62,13,115,97,105,100,32,116,111,32,104,97,118,101,105,110,32 +,116,104,101,32,102,105,114,115,116,119,104,105,108,101,32,111,116,104,101,114, +115,104,121,112,111,116,104,101,116,105,99,97,108,112,104,105,108,111,115,111, +112,104,101,114,115,112,111,119,101,114,32,111,102,32,116,104,101,99,111,110,116 +,97,105,110,101,100,32,105,110,112,101,114,102,111,114,109,101,100,32,98,121,105 +,110,97,98,105,108,105,116,121,32,116,111,119,101,114,101,32,119,114,105,116,116 +,101,110,115,112,97,110,32,115,116,121,108,101,61,34,105,110,112,117,116,32,110, +97,109,101,61,34,116,104,101,32,113,117,101,115,116,105,111,110,105,110,116,101, +110,100,101,100,32,102,111,114,114,101,106,101,99,116,105,111,110,32,111,102,105 +,109,112,108,105,101,115,32,116,104,97,116,105,110,118,101,110,116,101,100,32, +116,104,101,116,104,101,32,115,116,97,110,100,97,114,100,119,97,115,32,112,114, +111,98,97,98,108,121,108,105,110,107,32,98,101,116,119,101,101,110,112,114,111, +102,101,115,115,111,114,32,111,102,105,110,116,101,114,97,99,116,105,111,110,115 +,99,104,97,110,103,105,110,103,32,116,104,101,73,110,100,105,97,110,32,79,99,101 +,97,110,32,99,108,97,115,115,61,34,108,97,115,116,119,111,114,107,105,110,103,32 +,119,105,116,104,39,104,116,116,112,58,47,47,119,119,119,46,121,101,97,114,115, +32,98,101,102,111,114,101,84,104,105,115,32,119,97,115,32,116,104,101,114,101,99 +,114,101,97,116,105,111,110,97,108,101,110,116,101,114,105,110,103,32,116,104, +101,109,101,97,115,117,114,101,109,101,110,116,115,97,110,32,101,120,116,114,101 +,109,101,108,121,118,97,108,117,101,32,111,102,32,116,104,101,115,116,97,114,116 +,32,111,102,32,116,104,101,10,60,47,115,99,114,105,112,116,62,10,10,97,110,32, +101,102,102,111,114,116,32,116,111,105,110,99,114,101,97,115,101,32,116,104,101, +116,111,32,116,104,101,32,115,111,117,116,104,115,112,97,99,105,110,103,61,34,48 +,34,62,115,117,102,102,105,99,105,101,110,116,108,121,116,104,101,32,69,117,114, +111,112,101,97,110,99,111,110,118,101,114,116,101,100,32,116,111,99,108,101,97, +114,84,105,109,101,111,117,116,100,105,100,32,110,111,116,32,104,97,118,101,99, +111,110,115,101,113,117,101,110,116,108,121,102,111,114,32,116,104,101,32,110, +101,120,116,101,120,116,101,110,115,105,111,110,32,111,102,101,99,111,110,111, +109,105,99,32,97,110,100,97,108,116,104,111,117,103,104,32,116,104,101,97,114, +101,32,112,114,111,100,117,99,101,100,97,110,100,32,119,105,116,104,32,116,104, +101,105,110,115,117,102,102,105,99,105,101,110,116,103,105,118,101,110,32,98,121 +,32,116,104,101,115,116,97,116,105,110,103,32,116,104,97,116,101,120,112,101,110 +,100,105,116,117,114,101,115,60,47,115,112,97,110,62,60,47,97,62,10,116,104,111, +117,103,104,116,32,116,104,97,116,111,110,32,116,104,101,32,98,97,115,105,115,99 +,101,108,108,112,97,100,100,105,110,103,61,105,109,97,103,101,32,111,102,32,116, +104,101,114,101,116,117,114,110,105,110,103,32,116,111,105,110,102,111,114,109, +97,116,105,111,110,44,115,101,112,97,114,97,116,101,100,32,98,121,97,115,115,97, +115,115,105,110,97,116,101,100,115,34,32,99,111,110,116,101,110,116,61,34,97,117 +,116,104,111,114,105,116,121,32,111,102,110,111,114,116,104,119,101,115,116,101, +114,110,60,47,100,105,118,62,10,60,100,105,118,32,34,62,60,47,100,105,118,62,13, +10,32,32,99,111,110,115,117,108,116,97,116,105,111,110,99,111,109,109,117,110, +105,116,121,32,111,102,116,104,101,32,110,97,116,105,111,110,97,108,105,116,32, +115,104,111,117,108,100,32,98,101,112,97,114,116,105,99,105,112,97,110,116,115, +32,97,108,105,103,110,61,34,108,101,102,116,116,104,101,32,103,114,101,97,116, +101,115,116,115,101,108,101,99,116,105,111,110,32,111,102,115,117,112,101,114, +110,97,116,117,114,97,108,100,101,112,101,110,100,101,110,116,32,111,110,105,115 +,32,109,101,110,116,105,111,110,101,100,97,108,108,111,119,105,110,103,32,116, +104,101,119,97,115,32,105,110,118,101,110,116,101,100,97,99,99,111,109,112,97, +110,121,105,110,103,104,105,115,32,112,101,114,115,111,110,97,108,97,118,97,105, +108,97,98,108,101,32,97,116,115,116,117,100,121,32,111,102,32,116,104,101,111, +110,32,116,104,101,32,111,116,104,101,114,101,120,101,99,117,116,105,111,110,32, +111,102,72,117,109,97,110,32,82,105,103,104,116,115,116,101,114,109,115,32,111, +102,32,116,104,101,97,115,115,111,99,105,97,116,105,111,110,115,114,101,115,101, +97,114,99,104,32,97,110,100,115,117,99,99,101,101,100,101,100,32,98,121,100,101, +102,101,97,116,101,100,32,116,104,101,97,110,100,32,102,114,111,109,32,116,104, +101,98,117,116,32,116,104,101,121,32,97,114,101,99,111,109,109,97,110,100,101, +114,32,111,102,115,116,97,116,101,32,111,102,32,116,104,101,121,101,97,114,115, +32,111,102,32,97,103,101,116,104,101,32,115,116,117,100,121,32,111,102,60,117, +108,32,99,108,97,115,115,61,34,115,112,108,97,99,101,32,105,110,32,116,104,101, +119,104,101,114,101,32,104,101,32,119,97,115,60,108,105,32,99,108,97,115,115,61, +34,102,116,104,101,114,101,32,97,114,101,32,110,111,119,104,105,99,104,32,98,101 +,99,97,109,101,104,101,32,112,117,98,108,105,115,104,101,100,101,120,112,114,101 +,115,115,101,100,32,105,110,116,111,32,119,104,105,99,104,32,116,104,101,99,111, +109,109,105,115,115,105,111,110,101,114,102,111,110,116,45,119,101,105,103,104, +116,58,116,101,114,114,105,116,111,114,121,32,111,102,101,120,116,101,110,115, +105,111,110,115,34,62,82,111,109,97,110,32,69,109,112,105,114,101,101,113,117,97 +,108,32,116,111,32,116,104,101,73,110,32,99,111,110,116,114,97,115,116,44,104, +111,119,101,118,101,114,44,32,97,110,100,105,115,32,116,121,112,105,99,97,108, +108,121,97,110,100,32,104,105,115,32,119,105,102,101,40,97,108,115,111,32,99,97, +108,108,101,100,62,60,117,108,32,99,108,97,115,115,61,34,101,102,102,101,99,116, +105,118,101,108,121,32,101,118,111,108,118,101,100,32,105,110,116,111,115,101, +101,109,32,116,111,32,104,97,118,101,119,104,105,99,104,32,105,115,32,116,104, +101,116,104,101,114,101,32,119,97,115,32,110,111,97,110,32,101,120,99,101,108, +108,101,110,116,97,108,108,32,111,102,32,116,104,101,115,101,100,101,115,99,114, +105,98,101,100,32,98,121,73,110,32,112,114,97,99,116,105,99,101,44,98,114,111,97 +,100,99,97,115,116,105,110,103,99,104,97,114,103,101,100,32,119,105,116,104,114, +101,102,108,101,99,116,101,100,32,105,110,115,117,98,106,101,99,116,101,100,32, +116,111,109,105,108,105,116,97,114,121,32,97,110,100,116,111,32,116,104,101,32, +112,111,105,110,116,101,99,111,110,111,109,105,99,97,108,108,121,115,101,116,84, +97,114,103,101,116,105,110,103,97,114,101,32,97,99,116,117,97,108,108,121,118, +105,99,116,111,114,121,32,111,118,101,114,40,41,59,60,47,115,99,114,105,112,116, +62,99,111,110,116,105,110,117,111,117,115,108,121,114,101,113,117,105,114,101, +100,32,102,111,114,101,118,111,108,117,116,105,111,110,97,114,121,97,110,32,101, +102,102,101,99,116,105,118,101,110,111,114,116,104,32,111,102,32,116,104,101,44, +32,119,104,105,99,104,32,119,97,115,32,102,114,111,110,116,32,111,102,32,116,104 +,101,111,114,32,111,116,104,101,114,119,105,115,101,115,111,109,101,32,102,111, +114,109,32,111,102,104,97,100,32,110,111,116,32,98,101,101,110,103,101,110,101, +114,97,116,101,100,32,98,121,105,110,102,111,114,109,97,116,105,111,110,46,112, +101,114,109,105,116,116,101,100,32,116,111,105,110,99,108,117,100,101,115,32,116 +,104,101,100,101,118,101,108,111,112,109,101,110,116,44,101,110,116,101,114,101, +100,32,105,110,116,111,116,104,101,32,112,114,101,118,105,111,117,115,99,111,110 +,115,105,115,116,101,110,116,108,121,97,114,101,32,107,110,111,119,110,32,97,115 +,116,104,101,32,102,105,101,108,100,32,111,102,116,104,105,115,32,116,121,112, +101,32,111,102,103,105,118,101,110,32,116,111,32,116,104,101,116,104,101,32,116, +105,116,108,101,32,111,102,99,111,110,116,97,105,110,115,32,116,104,101,105,110, +115,116,97,110,99,101,115,32,111,102,105,110,32,116,104,101,32,110,111,114,116, +104,100,117,101,32,116,111,32,116,104,101,105,114,97,114,101,32,100,101,115,105, +103,110,101,100,99,111,114,112,111,114,97,116,105,111,110,115,119,97,115,32,116, +104,97,116,32,116,104,101,111,110,101,32,111,102,32,116,104,101,115,101,109,111, +114,101,32,112,111,112,117,108,97,114,115,117,99,99,101,101,100,101,100,32,105, +110,115,117,112,112,111,114,116,32,102,114,111,109,105,110,32,100,105,102,102, +101,114,101,110,116,100,111,109,105,110,97,116,101,100,32,98,121,100,101,115,105 +,103,110,101,100,32,102,111,114,111,119,110,101,114,115,104,105,112,32,111,102, +97,110,100,32,112,111,115,115,105,98,108,121,115,116,97,110,100,97,114,100,105, +122,101,100,114,101,115,112,111,110,115,101,84,101,120,116,119,97,115,32,105,110 +,116,101,110,100,101,100,114,101,99,101,105,118,101,100,32,116,104,101,97,115, +115,117,109,101,100,32,116,104,97,116,97,114,101,97,115,32,111,102,32,116,104, +101,112,114,105,109,97,114,105,108,121,32,105,110,116,104,101,32,98,97,115,105, +115,32,111,102,105,110,32,116,104,101,32,115,101,110,115,101,97,99,99,111,117, +110,116,115,32,102,111,114,100,101,115,116,114,111,121,101,100,32,98,121,97,116, +32,108,101,97,115,116,32,116,119,111,119,97,115,32,100,101,99,108,97,114,101,100 +,99,111,117,108,100,32,110,111,116,32,98,101,83,101,99,114,101,116,97,114,121,32 +,111,102,97,112,112,101,97,114,32,116,111,32,98,101,109,97,114,103,105,110,45, +116,111,112,58,49,47,94,92,115,43,124,92,115,43,36,47,103,101,41,123,116,104,114 +,111,119,32,101,125,59,116,104,101,32,115,116,97,114,116,32,111,102,116,119,111, +32,115,101,112,97,114,97,116,101,108,97,110,103,117,97,103,101,32,97,110,100,119 +,104,111,32,104,97,100,32,98,101,101,110,111,112,101,114,97,116,105,111,110,32, +111,102,100,101,97,116,104,32,111,102,32,116,104,101,114,101,97,108,32,110,117, +109,98,101,114,115,9,60,108,105,110,107,32,114,101,108,61,34,112,114,111,118,105 +,100,101,100,32,116,104,101,116,104,101,32,115,116,111,114,121,32,111,102,99,111 +,109,112,101,116,105,116,105,111,110,115,101,110,103,108,105,115,104,32,40,85,75 +,41,101,110,103,108,105,115,104,32,40,85,83,41,208,156,208,190,208,189,208,179, +208,190,208,187,208,161,209,128,208,191,209,129,208,186,208,184,209,129,209,128, +208,191,209,129,208,186,208,184,209,129,209,128,208,191,209,129,208,186,208,190, +217,132,216,185,216,177,216,168,217,138,216,169,230,173,163,233,171,148,228,184, +173,230,150,135,231,174,128,228,189,147,228,184,173,230,150,135,231,185,129,228, +189,147,228,184,173,230,150,135,230,156,137,233,153,144,229,133,172,229,143,184, +228,186,186,230,176,145,230,148,191,229,186,156,233,152,191,233,135,140,229,183, +180,229,183,180,231,164,190,228,188,154,228,184,187,228,185,137,230,147,141,228, +189,156,231,179,187,231,187,159,230,148,191,231,173,150,230,179,149,232,167,132, +105,110,102,111,114,109,97,99,105,195,179,110,104,101,114,114,97,109,105,101,110 +,116,97,115,101,108,101,99,116,114,195,179,110,105,99,111,100,101,115,99,114,105 +,112,99,105,195,179,110,99,108,97,115,105,102,105,99,97,100,111,115,99,111,110, +111,99,105,109,105,101,110,116,111,112,117,98,108,105,99,97,99,105,195,179,110, +114,101,108,97,99,105,111,110,97,100,97,115,105,110,102,111,114,109,195,161,116, +105,99,97,114,101,108,97,99,105,111,110,97,100,111,115,100,101,112,97,114,116,97 +,109,101,110,116,111,116,114,97,98,97,106,97,100,111,114,101,115,100,105,114,101 +,99,116,97,109,101,110,116,101,97,121,117,110,116,97,109,105,101,110,116,111,109 +,101,114,99,97,100,111,76,105,98,114,101,99,111,110,116,195,161,99,116,101,110, +111,115,104,97,98,105,116,97,99,105,111,110,101,115,99,117,109,112,108,105,109, +105,101,110,116,111,114,101,115,116,97,117,114,97,110,116,101,115,100,105,115, +112,111,115,105,99,105,195,179,110,99,111,110,115,101,99,117,101,110,99,105,97, +101,108,101,99,116,114,195,179,110,105,99,97,97,112,108,105,99,97,99,105,111,110 +,101,115,100,101,115,99,111,110,101,99,116,97,100,111,105,110,115,116,97,108,97, +99,105,195,179,110,114,101,97,108,105,122,97,99,105,195,179,110,117,116,105,108, +105,122,97,99,105,195,179,110,101,110,99,105,99,108,111,112,101,100,105,97,101, +110,102,101,114,109,101,100,97,100,101,115,105,110,115,116,114,117,109,101,110, +116,111,115,101,120,112,101,114,105,101,110,99,105,97,115,105,110,115,116,105, +116,117,99,105,195,179,110,112,97,114,116,105,99,117,108,97,114,101,115,115,117, +98,99,97,116,101,103,111,114,105,97,209,130,208,190,208,187,209,140,208,186,208, +190,208,160,208,190,209,129,209,129,208,184,208,184,209,128,208,176,208,177,208, +190,209,130,209,139,208,177,208,190,208,187,209,140,209,136,208,181,208,191,209, +128,208,190,209,129,209,130,208,190,208,188,208,190,208,182,208,181,209,130,208, +181,208,180,209,128,209,131,208,179,208,184,209,133,209,129,208,187,209,131,209, +135,208,176,208,181,209,129,208,181,208,185,209,135,208,176,209,129,208,178,209, +129,208,181,208,179,208,180,208,176,208,160,208,190,209,129,209,129,208,184,209, +143,208,156,208,190,209,129,208,186,208,178,208,181,208,180,209,128,209,131,208, +179,208,184,208,181,208,179,208,190,209,128,208,190,208,180,208,176,208,178,208, +190,208,191,209,128,208,190,209,129,208,180,208,176,208,189,208,189,209,139,209, +133,208,180,208,190,208,187,208,182,208,189,209,139,208,184,208,188,208,181,208, +189,208,189,208,190,208,156,208,190,209,129,208,186,208,178,209,139,209,128,209, +131,208,177,208,187,208,181,208,185,208,156,208,190,209,129,208,186,208,178,208, +176,209,129,209,130,209,128,208,176,208,189,209,139,208,189,208,184,209,135,208, +181,208,179,208,190,209,128,208,176,208,177,208,190,209,130,208,181,208,180,208, +190,208,187,208,182,208,181,208,189,209,131,209,129,208,187,209,131,208,179,208, +184,209,130,208,181,208,191,208,181,209,128,209,140,208,158,208,180,208,189,208, +176,208,186,208,190,208,191,208,190,209,130,208,190,208,188,209,131,209,128,208, +176,208,177,208,190,209,130,209,131,208,176,208,191,209,128,208,181,208,187,209, +143,208,178,208,190,208,190,208,177,209,137,208,181,208,190,208,180,208,189,208, +190,208,179,208,190,209,129,208,178,208,190,208,181,208,179,208,190,209,129,209, +130,208,176,209,130,209,140,208,184,208,180,209,128,209,131,208,179,208,190,208, +185,209,132,208,190,209,128,209,131,208,188,208,181,209,133,208,190,209,128,208, +190,209,136,208,190,208,191,209,128,208,190,209,130,208,184,208,178,209,129,209, +129,209,139,208,187,208,186,208,176,208,186,208,176,208,182,208,180,209,139,208, +185,208,178,208,187,208,176,209,129,209,130,208,184,208,179,209,128,209,131,208, +191,208,191,209,139,208,178,208,188,208,181,209,129,209,130,208,181,209,128,208, +176,208,177,208,190,209,130,208,176,209,129,208,186,208,176,208,183,208,176,208, +187,208,191,208,181,209,128,208,178,209,139,208,185,208,180,208,181,208,187,208, +176,209,130,209,140,208,180,208,181,208,189,209,140,208,179,208,184,208,191,208, +181,209,128,208,184,208,190,208,180,208,177,208,184,208,183,208,189,208,181,209, +129,208,190,209,129,208,189,208,190,208,178,208,181,208,188,208,190,208,188,208, +181,208,189,209,130,208,186,209,131,208,191,208,184,209,130,209,140,208,180,208, +190,208,187,208,182,208,189,208,176,209,128,208,176,208,188,208,186,208,176,209, +133,208,189,208,176,209,135,208,176,208,187,208,190,208,160,208,176,208,177,208, +190,209,130,208,176,208,162,208,190,208,187,209,140,208,186,208,190,209,129,208, +190,208,178,209,129,208,181,208,188,208,178,209,130,208,190,209,128,208,190,208, +185,208,189,208,176,209,135,208,176,208,187,208,176,209,129,208,191,208,184,209, +129,208,190,208,186,209,129,208,187,209,131,208,182,208,177,209,139,209,129,208, +184,209,129,209,130,208,181,208,188,208,191,208,181,209,135,208,176,209,130,208, +184,208,189,208,190,208,178,208,190,208,179,208,190,208,191,208,190,208,188,208, +190,209,137,208,184,209,129,208,176,208,185,209,130,208,190,208,178,208,191,208, +190,209,135,208,181,208,188,209,131,208,191,208,190,208,188,208,190,209,137,209, +140,208,180,208,190,208,187,208,182,208,189,208,190,209,129,209,129,209,139,208, +187,208,186,208,184,208,177,209,139,209,129,209,130,209,128,208,190,208,180,208, +176,208,189,208,189,209,139,208,181,208,188,208,189,208,190,208,179,208,184,208, +181,208,191,209,128,208,190,208,181,208,186,209,130,208,161,208,181,208,185,209, +135,208,176,209,129,208,188,208,190,208,180,208,181,208,187,208,184,209,130,208, +176,208,186,208,190,208,179,208,190,208,190,208,189,208,187,208,176,208,185,208, +189,208,179,208,190,209,128,208,190,208,180,208,181,208,178,208,181,209,128,209, +129,208,184,209,143,209,129,209,130,209,128,208,176,208,189,208,181,209,132,208, +184,208,187,209,140,208,188,209,139,209,131,209,128,208,190,208,178,208,189,209, +143,209,128,208,176,208,183,208,189,209,139,209,133,208,184,209,129,208,186,208, +176,209,130,209,140,208,189,208,181,208,180,208,181,208,187,209,142,209,143,208, +189,208,178,208,176,209,128,209,143,208,188,208,181,208,189,209,140,209,136,208, +181,208,188,208,189,208,190,208,179,208,184,209,133,208,180,208,176,208,189,208, +189,208,190,208,185,208,183,208,189,208,176,209,135,208,184,209,130,208,189,208, +181,208,187,209,140,208,183,209,143,209,132,208,190,209,128,209,131,208,188,208, +176,208,162,208,181,208,191,208,181,209,128,209,140,208,188,208,181,209,129,209, +143,209,134,208,176,208,183,208,176,209,137,208,184,209,130,209,139,208,155,209, +131,209,135,209,136,208,184,208,181,224,164,168,224,164,185,224,165,128,224,164, +130,224,164,149,224,164,176,224,164,168,224,165,135,224,164,133,224,164,170,224, +164,168,224,165,135,224,164,149,224,164,191,224,164,175,224,164,190,224,164,149, +224,164,176,224,165,135,224,164,130,224,164,133,224,164,168,224,165,141,224,164, +175,224,164,149,224,165,141,224,164,175,224,164,190,224,164,151,224,164,190,224, +164,135,224,164,161,224,164,172,224,164,190,224,164,176,224,165,135,224,164,149, +224,164,191,224,164,184,224,165,128,224,164,166,224,164,191,224,164,175,224,164, +190,224,164,170,224,164,185,224,164,178,224,165,135,224,164,184,224,164,191,224, +164,130,224,164,185,224,164,173,224,164,190,224,164,176,224,164,164,224,164,133, +224,164,170,224,164,168,224,165,128,224,164,181,224,164,190,224,164,178,224,165, +135,224,164,184,224,165,135,224,164,181,224,164,190,224,164,149,224,164,176,224, +164,164,224,165,135,224,164,174,224,165,135,224,164,176,224,165,135,224,164,185, +224,165,139,224,164,168,224,165,135,224,164,184,224,164,149,224,164,164,224,165, +135,224,164,172,224,164,185,224,165,129,224,164,164,224,164,184,224,164,190,224, +164,135,224,164,159,224,164,185,224,165,139,224,164,151,224,164,190,224,164,156, +224,164,190,224,164,168,224,165,135,224,164,174,224,164,191,224,164,168,224,164, +159,224,164,149,224,164,176,224,164,164,224,164,190,224,164,149,224,164,176,224, +164,168,224,164,190,224,164,137,224,164,168,224,164,149,224,165,135,224,164,175, +224,164,185,224,164,190,224,164,129,224,164,184,224,164,172,224,164,184,224,165, +135,224,164,173,224,164,190,224,164,183,224,164,190,224,164,134,224,164,170,224, +164,149,224,165,135,224,164,178,224,164,191,224,164,175,224,165,135,224,164,182, +224,165,129,224,164,176,224,165,130,224,164,135,224,164,184,224,164,149,224,165, +135,224,164,152,224,164,130,224,164,159,224,165,135,224,164,174,224,165,135,224, +164,176,224,165,128,224,164,184,224,164,149,224,164,164,224,164,190,224,164,174, +224,165,135,224,164,176,224,164,190,224,164,178,224,165,135,224,164,149,224,164, +176,224,164,133,224,164,167,224,164,191,224,164,149,224,164,133,224,164,170,224, +164,168,224,164,190,224,164,184,224,164,174,224,164,190,224,164,156,224,164,174, +224,165,129,224,164,157,224,165,135,224,164,149,224,164,190,224,164,176,224,164, +163,224,164,185,224,165,139,224,164,164,224,164,190,224,164,149,224,164,161,224, +164,188,224,165,128,224,164,175,224,164,185,224,164,190,224,164,130,224,164,185, +224,165,139,224,164,159,224,164,178,224,164,182,224,164,172,224,165,141,224,164, +166,224,164,178,224,164,191,224,164,175,224,164,190,224,164,156,224,165,128,224, +164,181,224,164,168,224,164,156,224,164,190,224,164,164,224,164,190,224,164,149, +224,165,136,224,164,184,224,165,135,224,164,134,224,164,170,224,164,149,224,164, +190,224,164,181,224,164,190,224,164,178,224,165,128,224,164,166,224,165,135,224, +164,168,224,165,135,224,164,170,224,165,130,224,164,176,224,165,128,224,164,170, +224,164,190,224,164,168,224,165,128,224,164,137,224,164,184,224,164,149,224,165, +135,224,164,185,224,165,139,224,164,151,224,165,128,224,164,172,224,165,136,224, +164,160,224,164,149,224,164,134,224,164,170,224,164,149,224,165,128,224,164,181, +224,164,176,224,165,141,224,164,183,224,164,151,224,164,190,224,164,130,224,164, +181,224,164,134,224,164,170,224,164,149,224,165,139,224,164,156,224,164,191,224, +164,178,224,164,190,224,164,156,224,164,190,224,164,168,224,164,190,224,164,184, +224,164,185,224,164,174,224,164,164,224,164,185,224,164,174,224,165,135,224,164, +130,224,164,137,224,164,168,224,164,149,224,165,128,224,164,175,224,164,190,224, +164,185,224,165,130,224,164,166,224,164,176,224,165,141,224,164,156,224,164,184, +224,165,130,224,164,154,224,165,128,224,164,170,224,164,184,224,164,130,224,164, +166,224,164,184,224,164,181,224,164,190,224,164,178,224,164,185,224,165,139,224, +164,168,224,164,190,224,164,185,224,165,139,224,164,164,224,165,128,224,164,156, +224,165,136,224,164,184,224,165,135,224,164,181,224,164,190,224,164,170,224,164, +184,224,164,156,224,164,168,224,164,164,224,164,190,224,164,168,224,165,135,224, +164,164,224,164,190,224,164,156,224,164,190,224,164,176,224,165,128,224,164,152, +224,164,190,224,164,175,224,164,178,224,164,156,224,164,191,224,164,178,224,165, +135,224,164,168,224,165,128,224,164,154,224,165,135,224,164,156,224,164,190,224, +164,130,224,164,154,224,164,170,224,164,164,224,165,141,224,164,176,224,164,151, +224,165,130,224,164,151,224,164,178,224,164,156,224,164,190,224,164,164,224,165, +135,224,164,172,224,164,190,224,164,185,224,164,176,224,164,134,224,164,170,224, +164,168,224,165,135,224,164,181,224,164,190,224,164,185,224,164,168,224,164,135, +224,164,184,224,164,149,224,164,190,224,164,184,224,165,129,224,164,172,224,164, +185,224,164,176,224,164,185,224,164,168,224,165,135,224,164,135,224,164,184,224, +164,184,224,165,135,224,164,184,224,164,185,224,164,191,224,164,164,224,164,172, +224,164,161,224,164,188,224,165,135,224,164,152,224,164,159,224,164,168,224,164, +190,224,164,164,224,164,178,224,164,190,224,164,182,224,164,170,224,164,190,224, +164,130,224,164,154,224,164,182,224,165,141,224,164,176,224,165,128,224,164,172, +224,164,161,224,164,188,224,165,128,224,164,185,224,165,139,224,164,164,224,165, +135,224,164,184,224,164,190,224,164,136,224,164,159,224,164,182,224,164,190,224, +164,175,224,164,166,224,164,184,224,164,149,224,164,164,224,165,128,224,164,156, +224,164,190,224,164,164,224,165,128,224,164,181,224,164,190,224,164,178,224,164, +190,224,164,185,224,164,156,224,164,190,224,164,176,224,164,170,224,164,159,224, +164,168,224,164,190,224,164,176,224,164,150,224,164,168,224,165,135,224,164,184, +224,164,161,224,164,188,224,164,149,224,164,174,224,164,191,224,164,178,224,164, +190,224,164,137,224,164,184,224,164,149,224,165,128,224,164,149,224,165,135,224, +164,181,224,164,178,224,164,178,224,164,151,224,164,164,224,164,190,224,164,150, +224,164,190,224,164,168,224,164,190,224,164,133,224,164,176,224,165,141,224,164, +165,224,164,156,224,164,185,224,164,190,224,164,130,224,164,166,224,165,135,224, +164,150,224,164,190,224,164,170,224,164,185,224,164,178,224,165,128,224,164,168, +224,164,191,224,164,175,224,164,174,224,164,172,224,164,191,224,164,168,224,164, +190,224,164,172,224,165,136,224,164,130,224,164,149,224,164,149,224,164,185,224, +165,128,224,164,130,224,164,149,224,164,185,224,164,168,224,164,190,224,164,166, +224,165,135,224,164,164,224,164,190,224,164,185,224,164,174,224,164,178,224,165, +135,224,164,149,224,164,190,224,164,171,224,165,128,224,164,156,224,164,172,224, +164,149,224,164,191,224,164,164,224,165,129,224,164,176,224,164,164,224,164,174, +224,164,190,224,164,130,224,164,151,224,164,181,224,164,185,224,165,128,224,164, +130,224,164,176,224,165,139,224,164,156,224,164,188,224,164,174,224,164,191,224, +164,178,224,165,128,224,164,134,224,164,176,224,165,139,224,164,170,224,164,184, +224,165,135,224,164,168,224,164,190,224,164,175,224,164,190,224,164,166,224,164, +181,224,164,178,224,165,135,224,164,168,224,165,135,224,164,150,224,164,190,224, +164,164,224,164,190,224,164,149,224,164,176,224,165,128,224,164,172,224,164,137, +224,164,168,224,164,149,224,164,190,224,164,156,224,164,181,224,164,190,224,164, +172,224,164,170,224,165,130,224,164,176,224,164,190,224,164,172,224,164,161,224, +164,188,224,164,190,224,164,184,224,165,140,224,164,166,224,164,190,224,164,182, +224,165,135,224,164,175,224,164,176,224,164,149,224,164,191,224,164,175,224,165, +135,224,164,149,224,164,185,224,164,190,224,164,130,224,164,133,224,164,149,224, +164,184,224,164,176,224,164,172,224,164,168,224,164,190,224,164,143,224,164,181, +224,164,185,224,164,190,224,164,130,224,164,184,224,165,141,224,164,165,224,164, +178,224,164,174,224,164,191,224,164,178,224,165,135,224,164,178,224,165,135,224, +164,150,224,164,149,224,164,181,224,164,191,224,164,183,224,164,175,224,164,149, +224,165,141,224,164,176,224,164,130,224,164,184,224,164,174,224,165,130,224,164, +185,224,164,165,224,164,190,224,164,168,224,164,190,216,170,216,179,216,170,216, +183,217,138,216,185,217,133,216,180,216,167,216,177,217,131,216,169,216,168,217, +136,216,167,216,179,216,183,216,169,216,167,217,132,216,181,217,129,216,173,216, +169,217,133,217,136,216,167,216,182,217,138,216,185,216,167,217,132,216,174,216, +167,216,181,216,169,216,167,217,132,217,133,216,178,217,138,216,175,216,167,217, +132,216,185,216,167,217,133,216,169,216,167,217,132,217,131,216,167,216,170,216, +168,216,167,217,132,216,177,216,175,217,136,216,175,216,168,216,177,217,134,216, +167,217,133,216,172,216,167,217,132,216,175,217,136,217,132,216,169,216,167,217, +132,216,185,216,167,217,132,217,133,216,167,217,132,217,133,217,136,217,130,216, +185,216,167,217,132,216,185,216,177,216,168,217,138,216,167,217,132,216,179,216, +177,217,138,216,185,216,167,217,132,216,172,217,136,216,167,217,132,216,167,217, +132,216,176,217,135,216,167,216,168,216,167,217,132,216,173,217,138,216,167,216, +169,216,167,217,132,216,173,217,130,217,136,217,130,216,167,217,132,217,131,216, +177,217,138,217,133,216,167,217,132,216,185,216,177,216,167,217,130,217,133,216, +173,217,129,217,136,216,184,216,169,216,167,217,132,216,171,216,167,217,134,217, +138,217,133,216,180,216,167,217,135,216,175,216,169,216,167,217,132,217,133,216, +177,216,163,216,169,216,167,217,132,217,130,216,177,216,162,217,134,216,167,217, +132,216,180,216,168,216,167,216,168,216,167,217,132,216,173,217,136,216,167,216, +177,216,167,217,132,216,172,216,175,217,138,216,175,216,167,217,132,216,163,216, +179,216,177,216,169,216,167,217,132,216,185,217,132,217,136,217,133,217,133,216, +172,217,133,217,136,216,185,216,169,216,167,217,132,216,177,216,173,217,133,217, +134,216,167,217,132,217,134,217,130,216,167,216,183,217,129,217,132,216,179,216, +183,217,138,217,134,216,167,217,132,217,131,217,136,217,138,216,170,216,167,217, +132,216,175,217,134,217,138,216,167,216,168,216,177,217,131,216,167,216,170,217, +135,216,167,217,132,216,177,217,138,216,167,216,182,216,170,216,173,217,138,216, +167,216,170,217,138,216,168,216,170,217,136,217,130,217,138,216,170,216,167,217, +132,216,163,217,136,217,132,217,137,216,167,217,132,216,168,216,177,217,138,216, +175,216,167,217,132,217,131,217,132,216,167,217,133,216,167,217,132,216,177,216, +167,216,168,216,183,216,167,217,132,216,180,216,174,216,181,217,138,216,179,217, +138,216,167,216,177,216,167,216,170,216,167,217,132,216,171,216,167,217,132,216, +171,216,167,217,132,216,181,217,132,216,167,216,169,216,167,217,132,216,173,216, +175,217,138,216,171,216,167,217,132,216,178,217,136,216,167,216,177,216,167,217, +132,216,174,217,132,217,138,216,172,216,167,217,132,216,172,217,133,217,138,216, +185,216,167,217,132,216,185,216,167,217,133,217,135,216,167,217,132,216,172,217, +133,216,167,217,132,216,167,217,132,216,179,216,167,216,185,216,169,217,133,216, +180,216,167,217,135,216,175,217,135,216,167,217,132,216,177,216,166,217,138,216, +179,216,167,217,132,216,175,216,174,217,136,217,132,216,167,217,132,217,129,217, +134,217,138,216,169,216,167,217,132,217,131,216,170,216,167,216,168,216,167,217, +132,216,175,217,136,216,177,217,138,216,167,217,132,216,175,216,177,217,136,216, +179,216,167,216,179,216,170,216,186,216,177,217,130,216,170,216,181,216,167,217, +133,217,138,217,133,216,167,217,132,216,168,217,134,216,167,216,170,216,167,217, +132,216,185,216,184,217,138,217,133,101,110,116,101,114,116,97,105,110,109,101, +110,116,117,110,100,101,114,115,116,97,110,100,105,110,103,32,61,32,102,117,110, +99,116,105,111,110,40,41,46,106,112,103,34,32,119,105,100,116,104,61,34,99,111, +110,102,105,103,117,114,97,116,105,111,110,46,112,110,103,34,32,119,105,100,116, +104,61,34,60,98,111,100,121,32,99,108,97,115,115,61,34,77,97,116,104,46,114,97, +110,100,111,109,40,41,99,111,110,116,101,109,112,111,114,97,114,121,32,85,110, +105,116,101,100,32,83,116,97,116,101,115,99,105,114,99,117,109,115,116,97,110,99 +,101,115,46,97,112,112,101,110,100,67,104,105,108,100,40,111,114,103,97,110,105, +122,97,116,105,111,110,115,60,115,112,97,110,32,99,108,97,115,115,61,34,34,62,60 +,105,109,103,32,115,114,99,61,34,47,100,105,115,116,105,110,103,117,105,115,104, +101,100,116,104,111,117,115,97,110,100,115,32,111,102,32,99,111,109,109,117,110, +105,99,97,116,105,111,110,99,108,101,97,114,34,62,60,47,100,105,118,62,105,110, +118,101,115,116,105,103,97,116,105,111,110,102,97,118,105,99,111,110,46,105,99, +111,34,32,109,97,114,103,105,110,45,114,105,103,104,116,58,98,97,115,101,100,32, +111,110,32,116,104,101,32,77,97,115,115,97,99,104,117,115,101,116,116,115,116,97 +,98,108,101,32,98,111,114,100,101,114,61,105,110,116,101,114,110,97,116,105,111, +110,97,108,97,108,115,111,32,107,110,111,119,110,32,97,115,112,114,111,110,117, +110,99,105,97,116,105,111,110,98,97,99,107,103,114,111,117,110,100,58,35,102,112 +,97,100,100,105,110,103,45,108,101,102,116,58,70,111,114,32,101,120,97,109,112, +108,101,44,32,109,105,115,99,101,108,108,97,110,101,111,117,115,38,108,116,59,47 +,109,97,116,104,38,103,116,59,112,115,121,99,104,111,108,111,103,105,99,97,108, +105,110,32,112,97,114,116,105,99,117,108,97,114,101,97,114,99,104,34,32,116,121, +112,101,61,34,102,111,114,109,32,109,101,116,104,111,100,61,34,97,115,32,111,112 +,112,111,115,101,100,32,116,111,83,117,112,114,101,109,101,32,67,111,117,114,116 +,111,99,99,97,115,105,111,110,97,108,108,121,32,65,100,100,105,116,105,111,110, +97,108,108,121,44,78,111,114,116,104,32,65,109,101,114,105,99,97,112,120,59,98, +97,99,107,103,114,111,117,110,100,111,112,112,111,114,116,117,110,105,116,105, +101,115,69,110,116,101,114,116,97,105,110,109,101,110,116,46,116,111,76,111,119, +101,114,67,97,115,101,40,109,97,110,117,102,97,99,116,117,114,105,110,103,112, +114,111,102,101,115,115,105,111,110,97,108,32,99,111,109,98,105,110,101,100,32, +119,105,116,104,70,111,114,32,105,110,115,116,97,110,99,101,44,99,111,110,115, +105,115,116,105,110,103,32,111,102,34,32,109,97,120,108,101,110,103,116,104,61, +34,114,101,116,117,114,110,32,102,97,108,115,101,59,99,111,110,115,99,105,111, +117,115,110,101,115,115,77,101,100,105,116,101,114,114,97,110,101,97,110,101,120 +,116,114,97,111,114,100,105,110,97,114,121,97,115,115,97,115,115,105,110,97,116, +105,111,110,115,117,98,115,101,113,117,101,110,116,108,121,32,98,117,116,116,111 +,110,32,116,121,112,101,61,34,116,104,101,32,110,117,109,98,101,114,32,111,102, +116,104,101,32,111,114,105,103,105,110,97,108,32,99,111,109,112,114,101,104,101, +110,115,105,118,101,114,101,102,101,114,115,32,116,111,32,116,104,101,60,47,117, +108,62,10,60,47,100,105,118,62,10,112,104,105,108,111,115,111,112,104,105,99,97, +108,108,111,99,97,116,105,111,110,46,104,114,101,102,119,97,115,32,112,117,98, +108,105,115,104,101,100,83,97,110,32,70,114,97,110,99,105,115,99,111,40,102,117, +110,99,116,105,111,110,40,41,123,10,60,100,105,118,32,105,100,61,34,109,97,105, +110,115,111,112,104,105,115,116,105,99,97,116,101,100,109,97,116,104,101,109,97, +116,105,99,97,108,32,47,104,101,97,100,62,13,10,60,98,111,100,121,115,117,103, +103,101,115,116,115,32,116,104,97,116,100,111,99,117,109,101,110,116,97,116,105, +111,110,99,111,110,99,101,110,116,114,97,116,105,111,110,114,101,108,97,116,105, +111,110,115,104,105,112,115,109,97,121,32,104,97,118,101,32,98,101,101,110,40, +102,111,114,32,101,120,97,109,112,108,101,44,84,104,105,115,32,97,114,116,105,99 +,108,101,32,105,110,32,115,111,109,101,32,99,97,115,101,115,112,97,114,116,115, +32,111,102,32,116,104,101,32,100,101,102,105,110,105,116,105,111,110,32,111,102, +71,114,101,97,116,32,66,114,105,116,97,105,110,32,99,101,108,108,112,97,100,100, +105,110,103,61,101,113,117,105,118,97,108,101,110,116,32,116,111,112,108,97,99, +101,104,111,108,100,101,114,61,34,59,32,102,111,110,116,45,115,105,122,101,58,32 +,106,117,115,116,105,102,105,99,97,116,105,111,110,98,101,108,105,101,118,101, +100,32,116,104,97,116,115,117,102,102,101,114,101,100,32,102,114,111,109,97,116, +116,101,109,112,116,101,100,32,116,111,32,108,101,97,100,101,114,32,111,102,32, +116,104,101,99,114,105,112,116,34,32,115,114,99,61,34,47,40,102,117,110,99,116, +105,111,110,40,41,32,123,97,114,101,32,97,118,97,105,108,97,98,108,101,10,9,60, +108,105,110,107,32,114,101,108,61,34,32,115,114,99,61,39,104,116,116,112,58,47, +47,105,110,116,101,114,101,115,116,101,100,32,105,110,99,111,110,118,101,110,116 +,105,111,110,97,108,32,34,32,97,108,116,61,34,34,32,47,62,60,47,97,114,101,32, +103,101,110,101,114,97,108,108,121,104,97,115,32,97,108,115,111,32,98,101,101, +110,109,111,115,116,32,112,111,112,117,108,97,114,32,99,111,114,114,101,115,112, +111,110,100,105,110,103,99,114,101,100,105,116,101,100,32,119,105,116,104,116, +121,108,101,61,34,98,111,114,100,101,114,58,60,47,97,62,60,47,115,112,97,110,62, +60,47,46,103,105,102,34,32,119,105,100,116,104,61,34,60,105,102,114,97,109,101, +32,115,114,99,61,34,116,97,98,108,101,32,99,108,97,115,115,61,34,105,110,108,105 +,110,101,45,98,108,111,99,107,59,97,99,99,111,114,100,105,110,103,32,116,111,32, +116,111,103,101,116,104,101,114,32,119,105,116,104,97,112,112,114,111,120,105, +109,97,116,101,108,121,112,97,114,108,105,97,109,101,110,116,97,114,121,109,111, +114,101,32,97,110,100,32,109,111,114,101,100,105,115,112,108,97,121,58,110,111, +110,101,59,116,114,97,100,105,116,105,111,110,97,108,108,121,112,114,101,100,111 +,109,105,110,97,110,116,108,121,38,110,98,115,112,59,124,38,110,98,115,112,59,38 +,110,98,115,112,59,60,47,115,112,97,110,62,32,99,101,108,108,115,112,97,99,105, +110,103,61,60,105,110,112,117,116,32,110,97,109,101,61,34,111,114,34,32,99,111, +110,116,101,110,116,61,34,99,111,110,116,114,111,118,101,114,115,105,97,108,112, +114,111,112,101,114,116,121,61,34,111,103,58,47,120,45,115,104,111,99,107,119,97 +,118,101,45,100,101,109,111,110,115,116,114,97,116,105,111,110,115,117,114,114, +111,117,110,100,101,100,32,98,121,78,101,118,101,114,116,104,101,108,101,115,115 +,44,119,97,115,32,116,104,101,32,102,105,114,115,116,99,111,110,115,105,100,101, +114,97,98,108,101,32,65,108,116,104,111,117,103,104,32,116,104,101,32,99,111,108 +,108,97,98,111,114,97,116,105,111,110,115,104,111,117,108,100,32,110,111,116,32, +98,101,112,114,111,112,111,114,116,105,111,110,32,111,102,60,115,112,97,110,32, +115,116,121,108,101,61,34,107,110,111,119,110,32,97,115,32,116,104,101,32,115, +104,111,114,116,108,121,32,97,102,116,101,114,102,111,114,32,105,110,115,116,97, +110,99,101,44,100,101,115,99,114,105,98,101,100,32,97,115,32,47,104,101,97,100, +62,10,60,98,111,100,121,32,115,116,97,114,116,105,110,103,32,119,105,116,104,105 +,110,99,114,101,97,115,105,110,103,108,121,32,116,104,101,32,102,97,99,116,32, +116,104,97,116,100,105,115,99,117,115,115,105,111,110,32,111,102,109,105,100,100 +,108,101,32,111,102,32,116,104,101,97,110,32,105,110,100,105,118,105,100,117,97, +108,100,105,102,102,105,99,117,108,116,32,116,111,32,112,111,105,110,116,32,111, +102,32,118,105,101,119,104,111,109,111,115,101,120,117,97,108,105,116,121,97,99, +99,101,112,116,97,110,99,101,32,111,102,60,47,115,112,97,110,62,60,47,100,105, +118,62,109,97,110,117,102,97,99,116,117,114,101,114,115,111,114,105,103,105,110, +32,111,102,32,116,104,101,99,111,109,109,111,110,108,121,32,117,115,101,100,105, +109,112,111,114,116,97,110,99,101,32,111,102,100,101,110,111,109,105,110,97,116, +105,111,110,115,98,97,99,107,103,114,111,117,110,100,58,32,35,108,101,110,103, +116,104,32,111,102,32,116,104,101,100,101,116,101,114,109,105,110,97,116,105,111 +,110,97,32,115,105,103,110,105,102,105,99,97,110,116,34,32,98,111,114,100,101, +114,61,34,48,34,62,114,101,118,111,108,117,116,105,111,110,97,114,121,112,114, +105,110,99,105,112,108,101,115,32,111,102,105,115,32,99,111,110,115,105,100,101, +114,101,100,119,97,115,32,100,101,118,101,108,111,112,101,100,73,110,100,111,45, +69,117,114,111,112,101,97,110,118,117,108,110,101,114,97,98,108,101,32,116,111, +112,114,111,112,111,110,101,110,116,115,32,111,102,97,114,101,32,115,111,109,101 +,116,105,109,101,115,99,108,111,115,101,114,32,116,111,32,116,104,101,78,101,119 +,32,89,111,114,107,32,67,105,116,121,32,110,97,109,101,61,34,115,101,97,114,99, +104,97,116,116,114,105,98,117,116,101,100,32,116,111,99,111,117,114,115,101,32, +111,102,32,116,104,101,109,97,116,104,101,109,97,116,105,99,105,97,110,98,121,32 +,116,104,101,32,101,110,100,32,111,102,97,116,32,116,104,101,32,101,110,100,32, +111,102,34,32,98,111,114,100,101,114,61,34,48,34,32,116,101,99,104,110,111,108, +111,103,105,99,97,108,46,114,101,109,111,118,101,67,108,97,115,115,40,98,114,97, +110,99,104,32,111,102,32,116,104,101,101,118,105,100,101,110,99,101,32,116,104, +97,116,33,91,101,110,100,105,102,93,45,45,62,13,10,73,110,115,116,105,116,117, +116,101,32,111,102,32,105,110,116,111,32,97,32,115,105,110,103,108,101,114,101, +115,112,101,99,116,105,118,101,108,121,46,97,110,100,32,116,104,101,114,101,102, +111,114,101,112,114,111,112,101,114,116,105,101,115,32,111,102,105,115,32,108, +111,99,97,116,101,100,32,105,110,115,111,109,101,32,111,102,32,119,104,105,99, +104,84,104,101,114,101,32,105,115,32,97,108,115,111,99,111,110,116,105,110,117, +101,100,32,116,111,32,97,112,112,101,97,114,97,110,99,101,32,111,102,32,38,97, +109,112,59,110,100,97,115,104,59,32,100,101,115,99,114,105,98,101,115,32,116,104 +,101,99,111,110,115,105,100,101,114,97,116,105,111,110,97,117,116,104,111,114,32 +,111,102,32,116,104,101,105,110,100,101,112,101,110,100,101,110,116,108,121,101, +113,117,105,112,112,101,100,32,119,105,116,104,100,111,101,115,32,110,111,116,32 +,104,97,118,101,60,47,97,62,60,97,32,104,114,101,102,61,34,99,111,110,102,117, +115,101,100,32,119,105,116,104,60,108,105,110,107,32,104,114,101,102,61,34,47,97 +,116,32,116,104,101,32,97,103,101,32,111,102,97,112,112,101,97,114,32,105,110,32 +,116,104,101,84,104,101,115,101,32,105,110,99,108,117,100,101,114,101,103,97,114 +,100,108,101,115,115,32,111,102,99,111,117,108,100,32,98,101,32,117,115,101,100, +32,115,116,121,108,101,61,38,113,117,111,116,59,115,101,118,101,114,97,108,32, +116,105,109,101,115,114,101,112,114,101,115,101,110,116,32,116,104,101,98,111, +100,121,62,10,60,47,104,116,109,108,62,116,104,111,117,103,104,116,32,116,111,32 +,98,101,112,111,112,117,108,97,116,105,111,110,32,111,102,112,111,115,115,105,98 +,105,108,105,116,105,101,115,112,101,114,99,101,110,116,97,103,101,32,111,102,97 +,99,99,101,115,115,32,116,111,32,116,104,101,97,110,32,97,116,116,101,109,112, +116,32,116,111,112,114,111,100,117,99,116,105,111,110,32,111,102,106,113,117,101 +,114,121,47,106,113,117,101,114,121,116,119,111,32,100,105,102,102,101,114,101, +110,116,98,101,108,111,110,103,32,116,111,32,116,104,101,101,115,116,97,98,108, +105,115,104,109,101,110,116,114,101,112,108,97,99,105,110,103,32,116,104,101,100 +,101,115,99,114,105,112,116,105,111,110,34,32,100,101,116,101,114,109,105,110, +101,32,116,104,101,97,118,97,105,108,97,98,108,101,32,102,111,114,65,99,99,111, +114,100,105,110,103,32,116,111,32,119,105,100,101,32,114,97,110,103,101,32,111, +102,9,60,100,105,118,32,99,108,97,115,115,61,34,109,111,114,101,32,99,111,109, +109,111,110,108,121,111,114,103,97,110,105,115,97,116,105,111,110,115,102,117, +110,99,116,105,111,110,97,108,105,116,121,119,97,115,32,99,111,109,112,108,101, +116,101,100,32,38,97,109,112,59,109,100,97,115,104,59,32,112,97,114,116,105,99, +105,112,97,116,105,111,110,116,104,101,32,99,104,97,114,97,99,116,101,114,97,110 +,32,97,100,100,105,116,105,111,110,97,108,97,112,112,101,97,114,115,32,116,111, +32,98,101,102,97,99,116,32,116,104,97,116,32,116,104,101,97,110,32,101,120,97, +109,112,108,101,32,111,102,115,105,103,110,105,102,105,99,97,110,116,108,121,111 +,110,109,111,117,115,101,111,118,101,114,61,34,98,101,99,97,117,115,101,32,116, +104,101,121,32,97,115,121,110,99,32,61,32,116,114,117,101,59,112,114,111,98,108, +101,109,115,32,119,105,116,104,115,101,101,109,115,32,116,111,32,104,97,118,101, +116,104,101,32,114,101,115,117,108,116,32,111,102,32,115,114,99,61,34,104,116, +116,112,58,47,47,102,97,109,105,108,105,97,114,32,119,105,116,104,112,111,115, +115,101,115,115,105,111,110,32,111,102,102,117,110,99,116,105,111,110,32,40,41, +32,123,116,111,111,107,32,112,108,97,99,101,32,105,110,97,110,100,32,115,111,109 +,101,116,105,109,101,115,115,117,98,115,116,97,110,116,105,97,108,108,121,60,115 +,112,97,110,62,60,47,115,112,97,110,62,105,115,32,111,102,116,101,110,32,117,115 +,101,100,105,110,32,97,110,32,97,116,116,101,109,112,116,103,114,101,97,116,32, +100,101,97,108,32,111,102,69,110,118,105,114,111,110,109,101,110,116,97,108,115, +117,99,99,101,115,115,102,117,108,108,121,32,118,105,114,116,117,97,108,108,121, +32,97,108,108,50,48,116,104,32,99,101,110,116,117,114,121,44,112,114,111,102,101 +,115,115,105,111,110,97,108,115,110,101,99,101,115,115,97,114,121,32,116,111,32, +100,101,116,101,114,109,105,110,101,100,32,98,121,99,111,109,112,97,116,105,98, +105,108,105,116,121,98,101,99,97,117,115,101,32,105,116,32,105,115,68,105,99,116 +,105,111,110,97,114,121,32,111,102,109,111,100,105,102,105,99,97,116,105,111,110 +,115,84,104,101,32,102,111,108,108,111,119,105,110,103,109,97,121,32,114,101,102 +,101,114,32,116,111,58,67,111,110,115,101,113,117,101,110,116,108,121,44,73,110, +116,101,114,110,97,116,105,111,110,97,108,97,108,116,104,111,117,103,104,32,115, +111,109,101,116,104,97,116,32,119,111,117,108,100,32,98,101,119,111,114,108,100, +39,115,32,102,105,114,115,116,99,108,97,115,115,105,102,105,101,100,32,97,115,98 +,111,116,116,111,109,32,111,102,32,116,104,101,40,112,97,114,116,105,99,117,108, +97,114,108,121,97,108,105,103,110,61,34,108,101,102,116,34,32,109,111,115,116,32 +,99,111,109,109,111,110,108,121,98,97,115,105,115,32,102,111,114,32,116,104,101, +102,111,117,110,100,97,116,105,111,110,32,111,102,99,111,110,116,114,105,98,117, +116,105,111,110,115,112,111,112,117,108,97,114,105,116,121,32,111,102,99,101,110 +,116,101,114,32,111,102,32,116,104,101,116,111,32,114,101,100,117,99,101,32,116, +104,101,106,117,114,105,115,100,105,99,116,105,111,110,115,97,112,112,114,111, +120,105,109,97,116,105,111,110,32,111,110,109,111,117,115,101,111,117,116,61,34, +78,101,119,32,84,101,115,116,97,109,101,110,116,99,111,108,108,101,99,116,105, +111,110,32,111,102,60,47,115,112,97,110,62,60,47,97,62,60,47,105,110,32,116,104, +101,32,85,110,105,116,101,100,102,105,108,109,32,100,105,114,101,99,116,111,114, +45,115,116,114,105,99,116,46,100,116,100,34,62,104,97,115,32,98,101,101,110,32, +117,115,101,100,114,101,116,117,114,110,32,116,111,32,116,104,101,97,108,116,104 +,111,117,103,104,32,116,104,105,115,99,104,97,110,103,101,32,105,110,32,116,104, +101,115,101,118,101,114,97,108,32,111,116,104,101,114,98,117,116,32,116,104,101, +114,101,32,97,114,101,117,110,112,114,101,99,101,100,101,110,116,101,100,105,115 +,32,115,105,109,105,108,97,114,32,116,111,101,115,112,101,99,105,97,108,108,121, +32,105,110,119,101,105,103,104,116,58,32,98,111,108,100,59,105,115,32,99,97,108, +108,101,100,32,116,104,101,99,111,109,112,117,116,97,116,105,111,110,97,108,105, +110,100,105,99,97,116,101,32,116,104,97,116,114,101,115,116,114,105,99,116,101, +100,32,116,111,9,60,109,101,116,97,32,110,97,109,101,61,34,97,114,101,32,116,121 +,112,105,99,97,108,108,121,99,111,110,102,108,105,99,116,32,119,105,116,104,72, +111,119,101,118,101,114,44,32,116,104,101,32,65,110,32,101,120,97,109,112,108, +101,32,111,102,99,111,109,112,97,114,101,100,32,119,105,116,104,113,117,97,110, +116,105,116,105,101,115,32,111,102,114,97,116,104,101,114,32,116,104,97,110,32, +97,99,111,110,115,116,101,108,108,97,116,105,111,110,110,101,99,101,115,115,97, +114,121,32,102,111,114,114,101,112,111,114,116,101,100,32,116,104,97,116,115,112 +,101,99,105,102,105,99,97,116,105,111,110,112,111,108,105,116,105,99,97,108,32, +97,110,100,38,110,98,115,112,59,38,110,98,115,112,59,60,114,101,102,101,114,101, +110,99,101,115,32,116,111,116,104,101,32,115,97,109,101,32,121,101,97,114,71,111 +,118,101,114,110,109,101,110,116,32,111,102,103,101,110,101,114,97,116,105,111, +110,32,111,102,104,97,118,101,32,110,111,116,32,98,101,101,110,115,101,118,101, +114,97,108,32,121,101,97,114,115,99,111,109,109,105,116,109,101,110,116,32,116, +111,9,9,60,117,108,32,99,108,97,115,115,61,34,118,105,115,117,97,108,105,122,97, +116,105,111,110,49,57,116,104,32,99,101,110,116,117,114,121,44,112,114,97,99,116 +,105,116,105,111,110,101,114,115,116,104,97,116,32,104,101,32,119,111,117,108, +100,97,110,100,32,99,111,110,116,105,110,117,101,100,111,99,99,117,112,97,116, +105,111,110,32,111,102,105,115,32,100,101,102,105,110,101,100,32,97,115,99,101, +110,116,114,101,32,111,102,32,116,104,101,116,104,101,32,97,109,111,117,110,116, +32,111,102,62,60,100,105,118,32,115,116,121,108,101,61,34,101,113,117,105,118,97 +,108,101,110,116,32,111,102,100,105,102,102,101,114,101,110,116,105,97,116,101, +98,114,111,117,103,104,116,32,97,98,111,117,116,109,97,114,103,105,110,45,108, +101,102,116,58,32,97,117,116,111,109,97,116,105,99,97,108,108,121,116,104,111, +117,103,104,116,32,111,102,32,97,115,83,111,109,101,32,111,102,32,116,104,101, +115,101,10,60,100,105,118,32,99,108,97,115,115,61,34,105,110,112,117,116,32,99, +108,97,115,115,61,34,114,101,112,108,97,99,101,100,32,119,105,116,104,105,115,32 +,111,110,101,32,111,102,32,116,104,101,101,100,117,99,97,116,105,111,110,32,97, +110,100,105,110,102,108,117,101,110,99,101,100,32,98,121,114,101,112,117,116,97, +116,105,111,110,32,97,115,10,60,109,101,116,97,32,110,97,109,101,61,34,97,99,99, +111,109,109,111,100,97,116,105,111,110,60,47,100,105,118,62,10,60,47,100,105,118 +,62,108,97,114,103,101,32,112,97,114,116,32,111,102,73,110,115,116,105,116,117, +116,101,32,102,111,114,116,104,101,32,115,111,45,99,97,108,108,101,100,32,97,103 +,97,105,110,115,116,32,116,104,101,32,73,110,32,116,104,105,115,32,99,97,115,101 +,44,119,97,115,32,97,112,112,111,105,110,116,101,100,99,108,97,105,109,101,100, +32,116,111,32,98,101,72,111,119,101,118,101,114,44,32,116,104,105,115,68,101,112 +,97,114,116,109,101,110,116,32,111,102,116,104,101,32,114,101,109,97,105,110,105 +,110,103,101,102,102,101,99,116,32,111,110,32,116,104,101,112,97,114,116,105,99, +117,108,97,114,108,121,32,100,101,97,108,32,119,105,116,104,32,116,104,101,10,60 +,100,105,118,32,115,116,121,108,101,61,34,97,108,109,111,115,116,32,97,108,119, +97,121,115,97,114,101,32,99,117,114,114,101,110,116,108,121,101,120,112,114,101, +115,115,105,111,110,32,111,102,112,104,105,108,111,115,111,112,104,121,32,111, +102,102,111,114,32,109,111,114,101,32,116,104,97,110,99,105,118,105,108,105,122, +97,116,105,111,110,115,111,110,32,116,104,101,32,105,115,108,97,110,100,115,101, +108,101,99,116,101,100,73,110,100,101,120,99,97,110,32,114,101,115,117,108,116, +32,105,110,34,32,118,97,108,117,101,61,34,34,32,47,62,116,104,101,32,115,116,114 +,117,99,116,117,114,101,32,47,62,60,47,97,62,60,47,100,105,118,62,77,97,110,121, +32,111,102,32,116,104,101,115,101,99,97,117,115,101,100,32,98,121,32,116,104,101 +,111,102,32,116,104,101,32,85,110,105,116,101,100,115,112,97,110,32,99,108,97, +115,115,61,34,109,99,97,110,32,98,101,32,116,114,97,99,101,100,105,115,32,114, +101,108,97,116,101,100,32,116,111,98,101,99,97,109,101,32,111,110,101,32,111,102 +,105,115,32,102,114,101,113,117,101,110,116,108,121,108,105,118,105,110,103,32, +105,110,32,116,104,101,116,104,101,111,114,101,116,105,99,97,108,108,121,70,111, +108,108,111,119,105,110,103,32,116,104,101,82,101,118,111,108,117,116,105,111, +110,97,114,121,103,111,118,101,114,110,109,101,110,116,32,105,110,105,115,32,100 +,101,116,101,114,109,105,110,101,100,116,104,101,32,112,111,108,105,116,105,99, +97,108,105,110,116,114,111,100,117,99,101,100,32,105,110,115,117,102,102,105,99, +105,101,110,116,32,116,111,100,101,115,99,114,105,112,116,105,111,110,34,62,115, +104,111,114,116,32,115,116,111,114,105,101,115,115,101,112,97,114,97,116,105,111 +,110,32,111,102,97,115,32,116,111,32,119,104,101,116,104,101,114,107,110,111,119 +,110,32,102,111,114,32,105,116,115,119,97,115,32,105,110,105,116,105,97,108,108, +121,100,105,115,112,108,97,121,58,98,108,111,99,107,105,115,32,97,110,32,101,120 +,97,109,112,108,101,116,104,101,32,112,114,105,110,99,105,112,97,108,99,111,110, +115,105,115,116,115,32,111,102,32,97,114,101,99,111,103,110,105,122,101,100,32, +97,115,47,98,111,100,121,62,60,47,104,116,109,108,62,97,32,115,117,98,115,116,97 +,110,116,105,97,108,114,101,99,111,110,115,116,114,117,99,116,101,100,104,101,97 +,100,32,111,102,32,115,116,97,116,101,114,101,115,105,115,116,97,110,99,101,32, +116,111,117,110,100,101,114,103,114,97,100,117,97,116,101,84,104,101,114,101,32, +97,114,101,32,116,119,111,103,114,97,118,105,116,97,116,105,111,110,97,108,97, +114,101,32,100,101,115,99,114,105,98,101,100,105,110,116,101,110,116,105,111,110 +,97,108,108,121,115,101,114,118,101,100,32,97,115,32,116,104,101,99,108,97,115, +115,61,34,104,101,97,100,101,114,111,112,112,111,115,105,116,105,111,110,32,116, +111,102,117,110,100,97,109,101,110,116,97,108,108,121,100,111,109,105,110,97,116 +,101,100,32,116,104,101,97,110,100,32,116,104,101,32,111,116,104,101,114,97,108, +108,105,97,110,99,101,32,119,105,116,104,119,97,115,32,102,111,114,99,101,100,32 +,116,111,114,101,115,112,101,99,116,105,118,101,108,121,44,97,110,100,32,112,111 +,108,105,116,105,99,97,108,105,110,32,115,117,112,112,111,114,116,32,111,102,112 +,101,111,112,108,101,32,105,110,32,116,104,101,50,48,116,104,32,99,101,110,116, +117,114,121,46,97,110,100,32,112,117,98,108,105,115,104,101,100,108,111,97,100, +67,104,97,114,116,98,101,97,116,116,111,32,117,110,100,101,114,115,116,97,110, +100,109,101,109,98,101,114,32,115,116,97,116,101,115,101,110,118,105,114,111,110 +,109,101,110,116,97,108,102,105,114,115,116,32,104,97,108,102,32,111,102,99,111, +117,110,116,114,105,101,115,32,97,110,100,97,114,99,104,105,116,101,99,116,117, +114,97,108,98,101,32,99,111,110,115,105,100,101,114,101,100,99,104,97,114,97,99, +116,101,114,105,122,101,100,99,108,101,97,114,73,110,116,101,114,118,97,108,97, +117,116,104,111,114,105,116,97,116,105,118,101,70,101,100,101,114,97,116,105,111 +,110,32,111,102,119,97,115,32,115,117,99,99,101,101,100,101,100,97,110,100,32, +116,104,101,114,101,32,97,114,101,97,32,99,111,110,115,101,113,117,101,110,99, +101,116,104,101,32,80,114,101,115,105,100,101,110,116,97,108,115,111,32,105,110, +99,108,117,100,101,100,102,114,101,101,32,115,111,102,116,119,97,114,101,115,117 +,99,99,101,115,115,105,111,110,32,111,102,100,101,118,101,108,111,112,101,100,32 +,116,104,101,119,97,115,32,100,101,115,116,114,111,121,101,100,97,119,97,121,32, +102,114,111,109,32,116,104,101,59,10,60,47,115,99,114,105,112,116,62,10,60,97, +108,116,104,111,117,103,104,32,116,104,101,121,102,111,108,108,111,119,101,100, +32,98,121,32,97,109,111,114,101,32,112,111,119,101,114,102,117,108,114,101,115, +117,108,116,101,100,32,105,110,32,97,85,110,105,118,101,114,115,105,116,121,32, +111,102,72,111,119,101,118,101,114,44,32,109,97,110,121,116,104,101,32,112,114, +101,115,105,100,101,110,116,72,111,119,101,118,101,114,44,32,115,111,109,101,105 +,115,32,116,104,111,117,103,104,116,32,116,111,117,110,116,105,108,32,116,104, +101,32,101,110,100,119,97,115,32,97,110,110,111,117,110,99,101,100,97,114,101,32 +,105,109,112,111,114,116,97,110,116,97,108,115,111,32,105,110,99,108,117,100,101 +,115,62,60,105,110,112,117,116,32,116,121,112,101,61,116,104,101,32,99,101,110, +116,101,114,32,111,102,32,68,79,32,78,79,84,32,65,76,84,69,82,117,115,101,100,32 +,116,111,32,114,101,102,101,114,116,104,101,109,101,115,47,63,115,111,114,116,61 +,116,104,97,116,32,104,97,100,32,98,101,101,110,116,104,101,32,98,97,115,105,115 +,32,102,111,114,104,97,115,32,100,101,118,101,108,111,112,101,100,105,110,32,116 +,104,101,32,115,117,109,109,101,114,99,111,109,112,97,114,97,116,105,118,101,108 +,121,100,101,115,99,114,105,98,101,100,32,116,104,101,115,117,99,104,32,97,115, +32,116,104,111,115,101,116,104,101,32,114,101,115,117,108,116,105,110,103,105, +115,32,105,109,112,111,115,115,105,98,108,101,118,97,114,105,111,117,115,32,111, +116,104,101,114,83,111,117,116,104,32,65,102,114,105,99,97,110,104,97,118,101,32 +,116,104,101,32,115,97,109,101,101,102,102,101,99,116,105,118,101,110,101,115, +115,105,110,32,119,104,105,99,104,32,99,97,115,101,59,32,116,101,120,116,45,97, +108,105,103,110,58,115,116,114,117,99,116,117,114,101,32,97,110,100,59,32,98,97, +99,107,103,114,111,117,110,100,58,114,101,103,97,114,100,105,110,103,32,116,104, +101,115,117,112,112,111,114,116,101,100,32,116,104,101,105,115,32,97,108,115,111 +,32,107,110,111,119,110,115,116,121,108,101,61,34,109,97,114,103,105,110,105,110 +,99,108,117,100,105,110,103,32,116,104,101,98,97,104,97,115,97,32,77,101,108,97, +121,117,110,111,114,115,107,32,98,111,107,109,195,165,108,110,111,114,115,107,32 +,110,121,110,111,114,115,107,115,108,111,118,101,110,197,161,196,141,105,110,97, +105,110,116,101,114,110,97,99,105,111,110,97,108,99,97,108,105,102,105,99,97,99, +105,195,179,110,99,111,109,117,110,105,99,97,99,105,195,179,110,99,111,110,115, +116,114,117,99,99,105,195,179,110,34,62,60,100,105,118,32,99,108,97,115,115,61, +34,100,105,115,97,109,98,105,103,117,97,116,105,111,110,68,111,109,97,105,110,78 +,97,109,101,39,44,32,39,97,100,109,105,110,105,115,116,114,97,116,105,111,110, +115,105,109,117,108,116,97,110,101,111,117,115,108,121,116,114,97,110,115,112, +111,114,116,97,116,105,111,110,73,110,116,101,114,110,97,116,105,111,110,97,108, +32,109,97,114,103,105,110,45,98,111,116,116,111,109,58,114,101,115,112,111,110, +115,105,98,105,108,105,116,121,60,33,91,101,110,100,105,102,93,45,45,62,10,60,47 +,62,60,109,101,116,97,32,110,97,109,101,61,34,105,109,112,108,101,109,101,110, +116,97,116,105,111,110,105,110,102,114,97,115,116,114,117,99,116,117,114,101,114 +,101,112,114,101,115,101,110,116,97,116,105,111,110,98,111,114,100,101,114,45,98 +,111,116,116,111,109,58,60,47,104,101,97,100,62,10,60,98,111,100,121,62,61,104, +116,116,112,37,51,65,37,50,70,37,50,70,60,102,111,114,109,32,109,101,116,104,111 +,100,61,34,109,101,116,104,111,100,61,34,112,111,115,116,34,32,47,102,97,118,105 +,99,111,110,46,105,99,111,34,32,125,41,59,10,60,47,115,99,114,105,112,116,62,10, +46,115,101,116,65,116,116,114,105,98,117,116,101,40,65,100,109,105,110,105,115, +116,114,97,116,105,111,110,61,32,110,101,119,32,65,114,114,97,121,40,41,59,60,33 +,91,101,110,100,105,102,93,45,45,62,13,10,100,105,115,112,108,97,121,58,98,108, +111,99,107,59,85,110,102,111,114,116,117,110,97,116,101,108,121,44,34,62,38,110, +98,115,112,59,60,47,100,105,118,62,47,102,97,118,105,99,111,110,46,105,99,111,34 +,62,61,39,115,116,121,108,101,115,104,101,101,116,39,32,105,100,101,110,116,105, +102,105,99,97,116,105,111,110,44,32,102,111,114,32,101,120,97,109,112,108,101,44 +,60,108,105,62,60,97,32,104,114,101,102,61,34,47,97,110,32,97,108,116,101,114, +110,97,116,105,118,101,97,115,32,97,32,114,101,115,117,108,116,32,111,102,112, +116,34,62,60,47,115,99,114,105,112,116,62,10,116,121,112,101,61,34,115,117,98, +109,105,116,34,32,10,40,102,117,110,99,116,105,111,110,40,41,32,123,114,101,99, +111,109,109,101,110,100,97,116,105,111,110,102,111,114,109,32,97,99,116,105,111, +110,61,34,47,116,114,97,110,115,102,111,114,109,97,116,105,111,110,114,101,99, +111,110,115,116,114,117,99,116,105,111,110,46,115,116,121,108,101,46,100,105,115 +,112,108,97,121,32,65,99,99,111,114,100,105,110,103,32,116,111,32,104,105,100, +100,101,110,34,32,110,97,109,101,61,34,97,108,111,110,103,32,119,105,116,104,32, +116,104,101,100,111,99,117,109,101,110,116,46,98,111,100,121,46,97,112,112,114, +111,120,105,109,97,116,101,108,121,32,67,111,109,109,117,110,105,99,97,116,105, +111,110,115,112,111,115,116,34,32,97,99,116,105,111,110,61,34,109,101,97,110,105 +,110,103,32,38,113,117,111,116,59,45,45,60,33,91,101,110,100,105,102,93,45,45,62 +,80,114,105,109,101,32,77,105,110,105,115,116,101,114,99,104,97,114,97,99,116, +101,114,105,115,116,105,99,60,47,97,62,32,60,97,32,99,108,97,115,115,61,116,104, +101,32,104,105,115,116,111,114,121,32,111,102,32,111,110,109,111,117,115,101,111 +,118,101,114,61,34,116,104,101,32,103,111,118,101,114,110,109,101,110,116,104, +114,101,102,61,34,104,116,116,112,115,58,47,47,119,97,115,32,111,114,105,103,105 +,110,97,108,108,121,119,97,115,32,105,110,116,114,111,100,117,99,101,100,99,108, +97,115,115,105,102,105,99,97,116,105,111,110,114,101,112,114,101,115,101,110,116 +,97,116,105,118,101,97,114,101,32,99,111,110,115,105,100,101,114,101,100,60,33, +91,101,110,100,105,102,93,45,45,62,10,10,100,101,112,101,110,100,115,32,111,110, +32,116,104,101,85,110,105,118,101,114,115,105,116,121,32,111,102,32,105,110,32, +99,111,110,116,114,97,115,116,32,116,111,32,112,108,97,99,101,104,111,108,100, +101,114,61,34,105,110,32,116,104,101,32,99,97,115,101,32,111,102,105,110,116,101 +,114,110,97,116,105,111,110,97,108,32,99,111,110,115,116,105,116,117,116,105,111 +,110,97,108,115,116,121,108,101,61,34,98,111,114,100,101,114,45,58,32,102,117, +110,99,116,105,111,110,40,41,32,123,66,101,99,97,117,115,101,32,111,102,32,116, +104,101,45,115,116,114,105,99,116,46,100,116,100,34,62,10,60,116,97,98,108,101, +32,99,108,97,115,115,61,34,97,99,99,111,109,112,97,110,105,101,100,32,98,121,97, +99,99,111,117,110,116,32,111,102,32,116,104,101,60,115,99,114,105,112,116,32,115 +,114,99,61,34,47,110,97,116,117,114,101,32,111,102,32,116,104,101,32,116,104,101 +,32,112,101,111,112,108,101,32,105,110,32,105,110,32,97,100,100,105,116,105,111, +110,32,116,111,115,41,59,32,106,115,46,105,100,32,61,32,105,100,34,32,119,105, +100,116,104,61,34,49,48,48,37,34,114,101,103,97,114,100,105,110,103,32,116,104, +101,32,82,111,109,97,110,32,67,97,116,104,111,108,105,99,97,110,32,105,110,100, +101,112,101,110,100,101,110,116,102,111,108,108,111,119,105,110,103,32,116,104, +101,32,46,103,105,102,34,32,119,105,100,116,104,61,34,49,116,104,101,32,102,111, +108,108,111,119,105,110,103,32,100,105,115,99,114,105,109,105,110,97,116,105,111 +,110,97,114,99,104,97,101,111,108,111,103,105,99,97,108,112,114,105,109,101,32, +109,105,110,105,115,116,101,114,46,106,115,34,62,60,47,115,99,114,105,112,116,62 +,99,111,109,98,105,110,97,116,105,111,110,32,111,102,32,109,97,114,103,105,110, +119,105,100,116,104,61,34,99,114,101,97,116,101,69,108,101,109,101,110,116,40, +119,46,97,116,116,97,99,104,69,118,101,110,116,40,60,47,97,62,60,47,116,100,62, +60,47,116,114,62,115,114,99,61,34,104,116,116,112,115,58,47,47,97,73,110,32,112, +97,114,116,105,99,117,108,97,114,44,32,97,108,105,103,110,61,34,108,101,102,116, +34,32,67,122,101,99,104,32,82,101,112,117,98,108,105,99,85,110,105,116,101,100, +32,75,105,110,103,100,111,109,99,111,114,114,101,115,112,111,110,100,101,110,99, +101,99,111,110,99,108,117,100,101,100,32,116,104,97,116,46,104,116,109,108,34,32 +,116,105,116,108,101,61,34,40,102,117,110,99,116,105,111,110,32,40,41,32,123,99, +111,109,101,115,32,102,114,111,109,32,116,104,101,97,112,112,108,105,99,97,116, +105,111,110,32,111,102,60,115,112,97,110,32,99,108,97,115,115,61,34,115,98,101, +108,105,101,118,101,100,32,116,111,32,98,101,101,109,101,110,116,40,39,115,99, +114,105,112,116,39,60,47,97,62,10,60,47,108,105,62,10,60,108,105,118,101,114,121 +,32,100,105,102,102,101,114,101,110,116,62,60,115,112,97,110,32,99,108,97,115, +115,61,34,111,112,116,105,111,110,32,118,97,108,117,101,61,34,40,97,108,115,111, +32,107,110,111,119,110,32,97,115,9,60,108,105,62,60,97,32,104,114,101,102,61,34, +62,60,105,110,112,117,116,32,110,97,109,101,61,34,115,101,112,97,114,97,116,101, +100,32,102,114,111,109,114,101,102,101,114,114,101,100,32,116,111,32,97,115,32, +118,97,108,105,103,110,61,34,116,111,112,34,62,102,111,117,110,100,101,114,32, +111,102,32,116,104,101,97,116,116,101,109,112,116,105,110,103,32,116,111,32,99, +97,114,98,111,110,32,100,105,111,120,105,100,101,10,10,60,100,105,118,32,99,108, +97,115,115,61,34,99,108,97,115,115,61,34,115,101,97,114,99,104,45,47,98,111,100, +121,62,10,60,47,104,116,109,108,62,111,112,112,111,114,116,117,110,105,116,121, +32,116,111,99,111,109,109,117,110,105,99,97,116,105,111,110,115,60,47,104,101,97 +,100,62,13,10,60,98,111,100,121,32,115,116,121,108,101,61,34,119,105,100,116,104 +,58,84,105,225,186,191,110,103,32,86,105,225,187,135,116,99,104,97,110,103,101, +115,32,105,110,32,116,104,101,98,111,114,100,101,114,45,99,111,108,111,114,58,35 +,48,34,32,98,111,114,100,101,114,61,34,48,34,32,60,47,115,112,97,110,62,60,47, +100,105,118,62,60,119,97,115,32,100,105,115,99,111,118,101,114,101,100,34,32,116 +,121,112,101,61,34,116,101,120,116,34,32,41,59,10,60,47,115,99,114,105,112,116, +62,10,10,68,101,112,97,114,116,109,101,110,116,32,111,102,32,101,99,99,108,101, +115,105,97,115,116,105,99,97,108,116,104,101,114,101,32,104,97,115,32,98,101,101 +,110,114,101,115,117,108,116,105,110,103,32,102,114,111,109,60,47,98,111,100,121 +,62,60,47,104,116,109,108,62,104,97,115,32,110,101,118,101,114,32,98,101,101,110 +,116,104,101,32,102,105,114,115,116,32,116,105,109,101,105,110,32,114,101,115, +112,111,110,115,101,32,116,111,97,117,116,111,109,97,116,105,99,97,108,108,121, +32,60,47,100,105,118,62,10,10,60,100,105,118,32,105,119,97,115,32,99,111,110,115 +,105,100,101,114,101,100,112,101,114,99,101,110,116,32,111,102,32,116,104,101,34 +,32,47,62,60,47,97,62,60,47,100,105,118,62,99,111,108,108,101,99,116,105,111,110 +,32,111,102,32,100,101,115,99,101,110,100,101,100,32,102,114,111,109,115,101,99, +116,105,111,110,32,111,102,32,116,104,101,97,99,99,101,112,116,45,99,104,97,114, +115,101,116,116,111,32,98,101,32,99,111,110,102,117,115,101,100,109,101,109,98, +101,114,32,111,102,32,116,104,101,32,112,97,100,100,105,110,103,45,114,105,103, +104,116,58,116,114,97,110,115,108,97,116,105,111,110,32,111,102,105,110,116,101, +114,112,114,101,116,97,116,105,111,110,32,104,114,101,102,61,39,104,116,116,112, +58,47,47,119,104,101,116,104,101,114,32,111,114,32,110,111,116,84,104,101,114, +101,32,97,114,101,32,97,108,115,111,116,104,101,114,101,32,97,114,101,32,109,97, +110,121,97,32,115,109,97,108,108,32,110,117,109,98,101,114,111,116,104,101,114, +32,112,97,114,116,115,32,111,102,105,109,112,111,115,115,105,98,108,101,32,116, +111,32,32,99,108,97,115,115,61,34,98,117,116,116,111,110,108,111,99,97,116,101, +100,32,105,110,32,116,104,101,46,32,72,111,119,101,118,101,114,44,32,116,104,101 +,97,110,100,32,101,118,101,110,116,117,97,108,108,121,65,116,32,116,104,101,32, +101,110,100,32,111,102,32,98,101,99,97,117,115,101,32,111,102,32,105,116,115,114 +,101,112,114,101,115,101,110,116,115,32,116,104,101,60,102,111,114,109,32,97,99, +116,105,111,110,61,34,32,109,101,116,104,111,100,61,34,112,111,115,116,34,105, +116,32,105,115,32,112,111,115,115,105,98,108,101,109,111,114,101,32,108,105,107, +101,108,121,32,116,111,97,110,32,105,110,99,114,101,97,115,101,32,105,110,104,97 +,118,101,32,97,108,115,111,32,98,101,101,110,99,111,114,114,101,115,112,111,110, +100,115,32,116,111,97,110,110,111,117,110,99,101,100,32,116,104,97,116,97,108, +105,103,110,61,34,114,105,103,104,116,34,62,109,97,110,121,32,99,111,117,110,116 +,114,105,101,115,102,111,114,32,109,97,110,121,32,121,101,97,114,115,101,97,114, +108,105,101,115,116,32,107,110,111,119,110,98,101,99,97,117,115,101,32,105,116, +32,119,97,115,112,116,34,62,60,47,115,99,114,105,112,116,62,13,32,118,97,108,105 +,103,110,61,34,116,111,112,34,32,105,110,104,97,98,105,116,97,110,116,115,32,111 +,102,102,111,108,108,111,119,105,110,103,32,121,101,97,114,13,10,60,100,105,118, +32,99,108,97,115,115,61,34,109,105,108,108,105,111,110,32,112,101,111,112,108, +101,99,111,110,116,114,111,118,101,114,115,105,97,108,32,99,111,110,99,101,114, +110,105,110,103,32,116,104,101,97,114,103,117,101,32,116,104,97,116,32,116,104, +101,103,111,118,101,114,110,109,101,110,116,32,97,110,100,97,32,114,101,102,101, +114,101,110,99,101,32,116,111,116,114,97,110,115,102,101,114,114,101,100,32,116, +111,100,101,115,99,114,105,98,105,110,103,32,116,104,101,32,115,116,121,108,101, +61,34,99,111,108,111,114,58,97,108,116,104,111,117,103,104,32,116,104,101,114, +101,98,101,115,116,32,107,110,111,119,110,32,102,111,114,115,117,98,109,105,116, +34,32,110,97,109,101,61,34,109,117,108,116,105,112,108,105,99,97,116,105,111,110 +,109,111,114,101,32,116,104,97,110,32,111,110,101,32,114,101,99,111,103,110,105, +116,105,111,110,32,111,102,67,111,117,110,99,105,108,32,111,102,32,116,104,101, +101,100,105,116,105,111,110,32,111,102,32,116,104,101,32,32,60,109,101,116,97,32 +,110,97,109,101,61,34,69,110,116,101,114,116,97,105,110,109,101,110,116,32,97, +119,97,121,32,102,114,111,109,32,116,104,101,32,59,109,97,114,103,105,110,45,114 +,105,103,104,116,58,97,116,32,116,104,101,32,116,105,109,101,32,111,102,105,110, +118,101,115,116,105,103,97,116,105,111,110,115,99,111,110,110,101,99,116,101,100 +,32,119,105,116,104,97,110,100,32,109,97,110,121,32,111,116,104,101,114,97,108, +116,104,111,117,103,104,32,105,116,32,105,115,98,101,103,105,110,110,105,110,103 +,32,119,105,116,104,32,60,115,112,97,110,32,99,108,97,115,115,61,34,100,101,115, +99,101,110,100,97,110,116,115,32,111,102,60,115,112,97,110,32,99,108,97,115,115, +61,34,105,32,97,108,105,103,110,61,34,114,105,103,104,116,34,60,47,104,101,97, +100,62,10,60,98,111,100,121,32,97,115,112,101,99,116,115,32,111,102,32,116,104, +101,104,97,115,32,115,105,110,99,101,32,98,101,101,110,69,117,114,111,112,101,97 +,110,32,85,110,105,111,110,114,101,109,105,110,105,115,99,101,110,116,32,111,102 +,109,111,114,101,32,100,105,102,102,105,99,117,108,116,86,105,99,101,32,80,114, +101,115,105,100,101,110,116,99,111,109,112,111,115,105,116,105,111,110,32,111, +102,112,97,115,115,101,100,32,116,104,114,111,117,103,104,109,111,114,101,32,105 +,109,112,111,114,116,97,110,116,102,111,110,116,45,115,105,122,101,58,49,49,112, +120,101,120,112,108,97,110,97,116,105,111,110,32,111,102,116,104,101,32,99,111, +110,99,101,112,116,32,111,102,119,114,105,116,116,101,110,32,105,110,32,116,104, +101,9,60,115,112,97,110,32,99,108,97,115,115,61,34,105,115,32,111,110,101,32,111 +,102,32,116,104,101,32,114,101,115,101,109,98,108,97,110,99,101,32,116,111,111, +110,32,116,104,101,32,103,114,111,117,110,100,115,119,104,105,99,104,32,99,111, +110,116,97,105,110,115,105,110,99,108,117,100,105,110,103,32,116,104,101,32,100, +101,102,105,110,101,100,32,98,121,32,116,104,101,112,117,98,108,105,99,97,116, +105,111,110,32,111,102,109,101,97,110,115,32,116,104,97,116,32,116,104,101,111, +117,116,115,105,100,101,32,111,102,32,116,104,101,115,117,112,112,111,114,116,32 +,111,102,32,116,104,101,60,105,110,112,117,116,32,99,108,97,115,115,61,34,60,115 +,112,97,110,32,99,108,97,115,115,61,34,116,40,77,97,116,104,46,114,97,110,100, +111,109,40,41,109,111,115,116,32,112,114,111,109,105,110,101,110,116,100,101,115 +,99,114,105,112,116,105,111,110,32,111,102,67,111,110,115,116,97,110,116,105,110 +,111,112,108,101,119,101,114,101,32,112,117,98,108,105,115,104,101,100,60,100, +105,118,32,99,108,97,115,115,61,34,115,101,97,112,112,101,97,114,115,32,105,110, +32,116,104,101,49,34,32,104,101,105,103,104,116,61,34,49,34,32,109,111,115,116, +32,105,109,112,111,114,116,97,110,116,119,104,105,99,104,32,105,110,99,108,117, +100,101,115,119,104,105,99,104,32,104,97,100,32,98,101,101,110,100,101,115,116, +114,117,99,116,105,111,110,32,111,102,116,104,101,32,112,111,112,117,108,97,116, +105,111,110,10,9,60,100,105,118,32,99,108,97,115,115,61,34,112,111,115,115,105, +98,105,108,105,116,121,32,111,102,115,111,109,101,116,105,109,101,115,32,117,115 +,101,100,97,112,112,101,97,114,32,116,111,32,104,97,118,101,115,117,99,99,101, +115,115,32,111,102,32,116,104,101,105,110,116,101,110,100,101,100,32,116,111,32, +98,101,112,114,101,115,101,110,116,32,105,110,32,116,104,101,115,116,121,108,101 +,61,34,99,108,101,97,114,58,98,13,10,60,47,115,99,114,105,112,116,62,13,10,60, +119,97,115,32,102,111,117,110,100,101,100,32,105,110,105,110,116,101,114,118,105 +,101,119,32,119,105,116,104,95,105,100,34,32,99,111,110,116,101,110,116,61,34,99 +,97,112,105,116,97,108,32,111,102,32,116,104,101,13,10,60,108,105,110,107,32,114 +,101,108,61,34,115,114,101,108,101,97,115,101,32,111,102,32,116,104,101,112,111, +105,110,116,32,111,117,116,32,116,104,97,116,120,77,76,72,116,116,112,82,101,113 +,117,101,115,116,97,110,100,32,115,117,98,115,101,113,117,101,110,116,115,101,99 +,111,110,100,32,108,97,114,103,101,115,116,118,101,114,121,32,105,109,112,111, +114,116,97,110,116,115,112,101,99,105,102,105,99,97,116,105,111,110,115,115,117, +114,102,97,99,101,32,111,102,32,116,104,101,97,112,112,108,105,101,100,32,116, +111,32,116,104,101,102,111,114,101,105,103,110,32,112,111,108,105,99,121,95,115, +101,116,68,111,109,97,105,110,78,97,109,101,101,115,116,97,98,108,105,115,104, +101,100,32,105,110,105,115,32,98,101,108,105,101,118,101,100,32,116,111,73,110, +32,97,100,100,105,116,105,111,110,32,116,111,109,101,97,110,105,110,103,32,111, +102,32,116,104,101,105,115,32,110,97,109,101,100,32,97,102,116,101,114,116,111, +32,112,114,111,116,101,99,116,32,116,104,101,105,115,32,114,101,112,114,101,115, +101,110,116,101,100,68,101,99,108,97,114,97,116,105,111,110,32,111,102,109,111, +114,101,32,101,102,102,105,99,105,101,110,116,67,108,97,115,115,105,102,105,99, +97,116,105,111,110,111,116,104,101,114,32,102,111,114,109,115,32,111,102,104,101 +,32,114,101,116,117,114,110,101,100,32,116,111,60,115,112,97,110,32,99,108,97, +115,115,61,34,99,112,101,114,102,111,114,109,97,110,99,101,32,111,102,40,102,117 +,110,99,116,105,111,110,40,41,32,123,13,105,102,32,97,110,100,32,111,110,108,121 +,32,105,102,114,101,103,105,111,110,115,32,111,102,32,116,104,101,108,101,97,100 +,105,110,103,32,116,111,32,116,104,101,114,101,108,97,116,105,111,110,115,32,119 +,105,116,104,85,110,105,116,101,100,32,78,97,116,105,111,110,115,115,116,121,108 +,101,61,34,104,101,105,103,104,116,58,111,116,104,101,114,32,116,104,97,110,32, +116,104,101,121,112,101,34,32,99,111,110,116,101,110,116,61,34,65,115,115,111,99 +,105,97,116,105,111,110,32,111,102,10,60,47,104,101,97,100,62,10,60,98,111,100, +121,108,111,99,97,116,101,100,32,111,110,32,116,104,101,105,115,32,114,101,102, +101,114,114,101,100,32,116,111,40,105,110,99,108,117,100,105,110,103,32,116,104, +101,99,111,110,99,101,110,116,114,97,116,105,111,110,115,116,104,101,32,105,110, +100,105,118,105,100,117,97,108,97,109,111,110,103,32,116,104,101,32,109,111,115, +116,116,104,97,110,32,97,110,121,32,111,116,104,101,114,47,62,10,60,108,105,110, +107,32,114,101,108,61,34,32,114,101,116,117,114,110,32,102,97,108,115,101,59,116 +,104,101,32,112,117,114,112,111,115,101,32,111,102,116,104,101,32,97,98,105,108, +105,116,121,32,116,111,59,99,111,108,111,114,58,35,102,102,102,125,10,46,10,60, +115,112,97,110,32,99,108,97,115,115,61,34,116,104,101,32,115,117,98,106,101,99, +116,32,111,102,100,101,102,105,110,105,116,105,111,110,115,32,111,102,62,13,10, +60,108,105,110,107,32,114,101,108,61,34,99,108,97,105,109,32,116,104,97,116,32, +116,104,101,104,97,118,101,32,100,101,118,101,108,111,112,101,100,60,116,97,98, +108,101,32,119,105,100,116,104,61,34,99,101,108,101,98,114,97,116,105,111,110,32 +,111,102,70,111,108,108,111,119,105,110,103,32,116,104,101,32,116,111,32,100,105 +,115,116,105,110,103,117,105,115,104,60,115,112,97,110,32,99,108,97,115,115,61, +34,98,116,97,107,101,115,32,112,108,97,99,101,32,105,110,117,110,100,101,114,32, +116,104,101,32,110,97,109,101,110,111,116,101,100,32,116,104,97,116,32,116,104, +101,62,60,33,91,101,110,100,105,102,93,45,45,62,10,115,116,121,108,101,61,34,109 +,97,114,103,105,110,45,105,110,115,116,101,97,100,32,111,102,32,116,104,101,105, +110,116,114,111,100,117,99,101,100,32,116,104,101,116,104,101,32,112,114,111,99, +101,115,115,32,111,102,105,110,99,114,101,97,115,105,110,103,32,116,104,101,100, +105,102,102,101,114,101,110,99,101,115,32,105,110,101,115,116,105,109,97,116,101 +,100,32,116,104,97,116,101,115,112,101,99,105,97,108,108,121,32,116,104,101,47, +100,105,118,62,60,100,105,118,32,105,100,61,34,119,97,115,32,101,118,101,110,116 +,117,97,108,108,121,116,104,114,111,117,103,104,111,117,116,32,104,105,115,116, +104,101,32,100,105,102,102,101,114,101,110,99,101,115,111,109,101,116,104,105, +110,103,32,116,104,97,116,115,112,97,110,62,60,47,115,112,97,110,62,60,47,115, +105,103,110,105,102,105,99,97,110,116,108,121,32,62,60,47,115,99,114,105,112,116 +,62,13,10,13,10,101,110,118,105,114,111,110,109,101,110,116,97,108,32,116,111,32 +,112,114,101,118,101,110,116,32,116,104,101,104,97,118,101,32,98,101,101,110,32, +117,115,101,100,101,115,112,101,99,105,97,108,108,121,32,102,111,114,117,110,100 +,101,114,115,116,97,110,100,32,116,104,101,105,115,32,101,115,115,101,110,116, +105,97,108,108,121,119,101,114,101,32,116,104,101,32,102,105,114,115,116,105,115 +,32,116,104,101,32,108,97,114,103,101,115,116,104,97,118,101,32,98,101,101,110, +32,109,97,100,101,34,32,115,114,99,61,34,104,116,116,112,58,47,47,105,110,116, +101,114,112,114,101,116,101,100,32,97,115,115,101,99,111,110,100,32,104,97,108, +102,32,111,102,99,114,111,108,108,105,110,103,61,34,110,111,34,32,105,115,32,99, +111,109,112,111,115,101,100,32,111,102,73,73,44,32,72,111,108,121,32,82,111,109, +97,110,105,115,32,101,120,112,101,99,116,101,100,32,116,111,104,97,118,101,32, +116,104,101,105,114,32,111,119,110,100,101,102,105,110,101,100,32,97,115,32,116, +104,101,116,114,97,100,105,116,105,111,110,97,108,108,121,32,104,97,118,101,32, +100,105,102,102,101,114,101,110,116,97,114,101,32,111,102,116,101,110,32,117,115 +,101,100,116,111,32,101,110,115,117,114,101,32,116,104,97,116,97,103,114,101,101 +,109,101,110,116,32,119,105,116,104,99,111,110,116,97,105,110,105,110,103,32,116 +,104,101,97,114,101,32,102,114,101,113,117,101,110,116,108,121,105,110,102,111, +114,109,97,116,105,111,110,32,111,110,101,120,97,109,112,108,101,32,105,115,32, +116,104,101,114,101,115,117,108,116,105,110,103,32,105,110,32,97,60,47,97,62,60, +47,108,105,62,60,47,117,108,62,32,99,108,97,115,115,61,34,102,111,111,116,101, +114,97,110,100,32,101,115,112,101,99,105,97,108,108,121,116,121,112,101,61,34,98 +,117,116,116,111,110,34,32,60,47,115,112,97,110,62,60,47,115,112,97,110,62,119, +104,105,99,104,32,105,110,99,108,117,100,101,100,62,10,60,109,101,116,97,32,110, +97,109,101,61,34,99,111,110,115,105,100,101,114,101,100,32,116,104,101,99,97,114 +,114,105,101,100,32,111,117,116,32,98,121,72,111,119,101,118,101,114,44,32,105, +116,32,105,115,98,101,99,97,109,101,32,112,97,114,116,32,111,102,105,110,32,114, +101,108,97,116,105,111,110,32,116,111,112,111,112,117,108,97,114,32,105,110,32, +116,104,101,116,104,101,32,99,97,112,105,116,97,108,32,111,102,119,97,115,32,111 +,102,102,105,99,105,97,108,108,121,119,104,105,99,104,32,104,97,115,32,98,101, +101,110,116,104,101,32,72,105,115,116,111,114,121,32,111,102,97,108,116,101,114, +110,97,116,105,118,101,32,116,111,100,105,102,102,101,114,101,110,116,32,102,114 +,111,109,116,111,32,115,117,112,112,111,114,116,32,116,104,101,115,117,103,103, +101,115,116,101,100,32,116,104,97,116,105,110,32,116,104,101,32,112,114,111,99, +101,115,115,32,32,60,100,105,118,32,99,108,97,115,115,61,34,116,104,101,32,102, +111,117,110,100,97,116,105,111,110,98,101,99,97,117,115,101,32,111,102,32,104, +105,115,99,111,110,99,101,114,110,101,100,32,119,105,116,104,116,104,101,32,117, +110,105,118,101,114,115,105,116,121,111,112,112,111,115,101,100,32,116,111,32, +116,104,101,116,104,101,32,99,111,110,116,101,120,116,32,111,102,60,115,112,97, +110,32,99,108,97,115,115,61,34,112,116,101,120,116,34,32,110,97,109,101,61,34, +113,34,9,9,60,100,105,118,32,99,108,97,115,115,61,34,116,104,101,32,115,99,105, +101,110,116,105,102,105,99,114,101,112,114,101,115,101,110,116,101,100,32,98,121 +,109,97,116,104,101,109,97,116,105,99,105,97,110,115,101,108,101,99,116,101,100, +32,98,121,32,116,104,101,116,104,97,116,32,104,97,118,101,32,98,101,101,110,62, +60,100,105,118,32,99,108,97,115,115,61,34,99,100,105,118,32,105,100,61,34,104, +101,97,100,101,114,105,110,32,112,97,114,116,105,99,117,108,97,114,44,99,111,110 +,118,101,114,116,101,100,32,105,110,116,111,41,59,10,60,47,115,99,114,105,112, +116,62,10,60,112,104,105,108,111,115,111,112,104,105,99,97,108,32,115,114,112, +115,107,111,104,114,118,97,116,115,107,105,116,105,225,186,191,110,103,32,86,105 +,225,187,135,116,208,160,209,131,209,129,209,129,208,186,208,184,208,185,209,128 +,209,131,209,129,209,129,208,186,208,184,208,185,105,110,118,101,115,116,105,103 +,97,99,105,195,179,110,112,97,114,116,105,99,105,112,97,99,105,195,179,110,208, +186,208,190,209,130,208,190,209,128,209,139,208,181,208,190,208,177,208,187,208, +176,209,129,209,130,208,184,208,186,208,190,209,130,208,190,209,128,209,139,208, +185,209,135,208,181,208,187,208,190,208,178,208,181,208,186,209,129,208,184,209, +129,209,130,208,181,208,188,209,139,208,157,208,190,208,178,208,190,209,129,209, +130,208,184,208,186,208,190,209,130,208,190,209,128,209,139,209,133,208,190,208, +177,208,187,208,176,209,129,209,130,209,140,208,178,209,128,208,181,208,188,208, +181,208,189,208,184,208,186,208,190,209,130,208,190,209,128,208,176,209,143,209, +129,208,181,208,179,208,190,208,180,208,189,209,143,209,129,208,186,208,176,209, +135,208,176,209,130,209,140,208,189,208,190,208,178,208,190,209,129,209,130,208, +184,208,163,208,186,209,128,208,176,208,184,208,189,209,139,208,178,208,190,208, +191,209,128,208,190,209,129,209,139,208,186,208,190,209,130,208,190,209,128,208, +190,208,185,209,129,208,180,208,181,208,187,208,176,209,130,209,140,208,191,208, +190,208,188,208,190,209,137,209,140,209,142,209,129,209,128,208,181,208,180,209, +129,209,130,208,178,208,190,208,177,209,128,208,176,208,183,208,190,208,188,209, +129,209,130,208,190,209,128,208,190,208,189,209,139,209,131,209,135,208,176,209, +129,209,130,208,184,208,181,209,130,208,181,209,135,208,181,208,189,208,184,208, +181,208,147,208,187,208,176,208,178,208,189,208,176,209,143,208,184,209,129,209, +130,208,190,209,128,208,184,208,184,209,129,208,184,209,129,209,130,208,181,208, +188,208,176,209,128,208,181,209,136,208,181,208,189,208,184,209,143,208,161,208, +186,208,176,209,135,208,176,209,130,209,140,208,191,208,190,209,141,209,130,208, +190,208,188,209,131,209,129,208,187,208,181,208,180,209,131,208,181,209,130,209, +129,208,186,208,176,208,183,208,176,209,130,209,140,209,130,208,190,208,178,208, +176,209,128,208,190,208,178,208,186,208,190,208,189,208,181,209,135,208,189,208, +190,209,128,208,181,209,136,208,181,208,189,208,184,208,181,208,186,208,190,209, +130,208,190,209,128,208,190,208,181,208,190,209,128,208,179,208,176,208,189,208, +190,208,178,208,186,208,190,209,130,208,190,209,128,208,190,208,188,208,160,208, +181,208,186,208,187,208,176,208,188,208,176,216,167,217,132,217,133,217,134,216, +170,216,175,217,137,217,133,217,134,216,170,216,175,217,138,216,167,216,170,216, +167,217,132,217,133,217,136,216,182,217,136,216,185,216,167,217,132,216,168,216, +177,216,167,217,133,216,172,216,167,217,132,217,133,217,136,216,167,217,130,216, +185,216,167,217,132,216,177,216,179,216,167,216,166,217,132,217,133,216,180,216, +167,216,177,217,131,216,167,216,170,216,167,217,132,216,163,216,185,216,182,216, +167,216,161,216,167,217,132,216,177,217,138,216,167,216,182,216,169,216,167,217, +132,216,170,216,181,217,133,217,138,217,133,216,167,217,132,216,167,216,185,216, +182,216,167,216,161,216,167,217,132,217,134,216,170,216,167,216,166,216,172,216, +167,217,132,216,163,217,132,216,185,216,167,216,168,216,167,217,132,216,170,216, +179,216,172,217,138,217,132,216,167,217,132,216,163,217,130,216,179,216,167,217, +133,216,167,217,132,216,182,216,186,216,183,216,167,216,170,216,167,217,132,217, +129,217,138,216,175,217,138,217,136,216,167,217,132,216,170,216,177,216,173,217, +138,216,168,216,167,217,132,216,172,216,175,217,138,216,175,216,169,216,167,217, +132,216,170,216,185,217,132,217,138,217,133,216,167,217,132,216,163,216,174,216, +168,216,167,216,177,216,167,217,132,216,167,217,129,217,132,216,167,217,133,216, +167,217,132,216,163,217,129,217,132,216,167,217,133,216,167,217,132,216,170,216, +167,216,177,217,138,216,174,216,167,217,132,216,170,217,130,217,134,217,138,216, +169,216,167,217,132,216,167,217,132,216,185,216,167,216,168,216,167,217,132,216, +174,217,136,216,167,216,183,216,177,216,167,217,132,217,133,216,172,216,170,217, +133,216,185,216,167,217,132,216,175,217,138,217,131,217,136,216,177,216,167,217, +132,216,179,217,138,216,167,216,173,216,169,216,185,216,168,216,175,216,167,217, +132,217,132,217,135,216,167,217,132,216,170,216,177,216,168,217,138,216,169,216, +167,217,132,216,177,217,136,216,167,216,168,216,183,216,167,217,132,216,163,216, +175,216,168,217,138,216,169,216,167,217,132,216,167,216,174,216,168,216,167,216, +177,216,167,217,132,217,133,216,170,216,173,216,175,216,169,216,167,217,132,216, +167,216,186,216,167,217,134,217,138,99,117,114,115,111,114,58,112,111,105,110, +116,101,114,59,60,47,116,105,116,108,101,62,10,60,109,101,116,97,32,34,32,104, +114,101,102,61,34,104,116,116,112,58,47,47,34,62,60,115,112,97,110,32,99,108,97, +115,115,61,34,109,101,109,98,101,114,115,32,111,102,32,116,104,101,32,119,105, +110,100,111,119,46,108,111,99,97,116,105,111,110,118,101,114,116,105,99,97,108, +45,97,108,105,103,110,58,47,97,62,32,124,32,60,97,32,104,114,101,102,61,34,60,33 +,100,111,99,116,121,112,101,32,104,116,109,108,62,109,101,100,105,97,61,34,115, +99,114,101,101,110,34,32,60,111,112,116,105,111,110,32,118,97,108,117,101,61,34, +102,97,118,105,99,111,110,46,105,99,111,34,32,47,62,10,9,9,60,100,105,118,32,99, +108,97,115,115,61,34,99,104,97,114,97,99,116,101,114,105,115,116,105,99,115,34, +32,109,101,116,104,111,100,61,34,103,101,116,34,32,47,98,111,100,121,62,10,60,47 +,104,116,109,108,62,10,115,104,111,114,116,99,117,116,32,105,99,111,110,34,32, +100,111,99,117,109,101,110,116,46,119,114,105,116,101,40,112,97,100,100,105,110, +103,45,98,111,116,116,111,109,58,114,101,112,114,101,115,101,110,116,97,116,105, +118,101,115,115,117,98,109,105,116,34,32,118,97,108,117,101,61,34,97,108,105,103 +,110,61,34,99,101,110,116,101,114,34,32,116,104,114,111,117,103,104,111,117,116, +32,116,104,101,32,115,99,105,101,110,99,101,32,102,105,99,116,105,111,110,10,32, +32,60,100,105,118,32,99,108,97,115,115,61,34,115,117,98,109,105,116,34,32,99,108 +,97,115,115,61,34,111,110,101,32,111,102,32,116,104,101,32,109,111,115,116,32, +118,97,108,105,103,110,61,34,116,111,112,34,62,60,119,97,115,32,101,115,116,97, +98,108,105,115,104,101,100,41,59,13,10,60,47,115,99,114,105,112,116,62,13,10,114 +,101,116,117,114,110,32,102,97,108,115,101,59,34,62,41,46,115,116,121,108,101,46 +,100,105,115,112,108,97,121,98,101,99,97,117,115,101,32,111,102,32,116,104,101, +32,100,111,99,117,109,101,110,116,46,99,111,111,107,105,101,60,102,111,114,109, +32,97,99,116,105,111,110,61,34,47,125,98,111,100,121,123,109,97,114,103,105,110, +58,48,59,69,110,99,121,99,108,111,112,101,100,105,97,32,111,102,118,101,114,115, +105,111,110,32,111,102,32,116,104,101,32,46,99,114,101,97,116,101,69,108,101,109 +,101,110,116,40,110,97,109,101,34,32,99,111,110,116,101,110,116,61,34,60,47,100, +105,118,62,10,60,47,100,105,118,62,10,10,97,100,109,105,110,105,115,116,114,97, +116,105,118,101,32,60,47,98,111,100,121,62,10,60,47,104,116,109,108,62,104,105, +115,116,111,114,121,32,111,102,32,116,104,101,32,34,62,60,105,110,112,117,116,32 +,116,121,112,101,61,34,112,111,114,116,105,111,110,32,111,102,32,116,104,101,32, +97,115,32,112,97,114,116,32,111,102,32,116,104,101,32,38,110,98,115,112,59,60,97 +,32,104,114,101,102,61,34,111,116,104,101,114,32,99,111,117,110,116,114,105,101, +115,34,62,10,60,100,105,118,32,99,108,97,115,115,61,34,60,47,115,112,97,110,62, +60,47,115,112,97,110,62,60,73,110,32,111,116,104,101,114,32,119,111,114,100,115, +44,100,105,115,112,108,97,121,58,32,98,108,111,99,107,59,99,111,110,116,114,111, +108,32,111,102,32,116,104,101,32,105,110,116,114,111,100,117,99,116,105,111,110, +32,111,102,47,62,10,60,109,101,116,97,32,110,97,109,101,61,34,97,115,32,119,101, +108,108,32,97,115,32,116,104,101,32,105,110,32,114,101,99,101,110,116,32,121,101 +,97,114,115,13,10,9,60,100,105,118,32,99,108,97,115,115,61,34,60,47,100,105,118, +62,10,9,60,47,100,105,118,62,10,105,110,115,112,105,114,101,100,32,98,121,32,116 +,104,101,116,104,101,32,101,110,100,32,111,102,32,116,104,101,32,99,111,109,112, +97,116,105,98,108,101,32,119,105,116,104,98,101,99,97,109,101,32,107,110,111,119 +,110,32,97,115,32,115,116,121,108,101,61,34,109,97,114,103,105,110,58,46,106,115 +,34,62,60,47,115,99,114,105,112,116,62,60,32,73,110,116,101,114,110,97,116,105, +111,110,97,108,32,116,104,101,114,101,32,104,97,118,101,32,98,101,101,110,71,101 +,114,109,97,110,32,108,97,110,103,117,97,103,101,32,115,116,121,108,101,61,34,99 +,111,108,111,114,58,35,67,111,109,109,117,110,105,115,116,32,80,97,114,116,121, +99,111,110,115,105,115,116,101,110,116,32,119,105,116,104,98,111,114,100,101,114 +,61,34,48,34,32,99,101,108,108,32,109,97,114,103,105,110,104,101,105,103,104,116 +,61,34,116,104,101,32,109,97,106,111,114,105,116,121,32,111,102,34,32,97,108,105 +,103,110,61,34,99,101,110,116,101,114,114,101,108,97,116,101,100,32,116,111,32, +116,104,101,32,109,97,110,121,32,100,105,102,102,101,114,101,110,116,32,79,114, +116,104,111,100,111,120,32,67,104,117,114,99,104,115,105,109,105,108,97,114,32, +116,111,32,116,104,101,32,47,62,10,60,108,105,110,107,32,114,101,108,61,34,115, +119,97,115,32,111,110,101,32,111,102,32,116,104,101,32,117,110,116,105,108,32, +104,105,115,32,100,101,97,116,104,125,41,40,41,59,10,60,47,115,99,114,105,112, +116,62,111,116,104,101,114,32,108,97,110,103,117,97,103,101,115,99,111,109,112, +97,114,101,100,32,116,111,32,116,104,101,112,111,114,116,105,111,110,115,32,111, +102,32,116,104,101,116,104,101,32,78,101,116,104,101,114,108,97,110,100,115,116, +104,101,32,109,111,115,116,32,99,111,109,109,111,110,98,97,99,107,103,114,111, +117,110,100,58,117,114,108,40,97,114,103,117,101,100,32,116,104,97,116,32,116, +104,101,115,99,114,111,108,108,105,110,103,61,34,110,111,34,32,105,110,99,108, +117,100,101,100,32,105,110,32,116,104,101,78,111,114,116,104,32,65,109,101,114, +105,99,97,110,32,116,104,101,32,110,97,109,101,32,111,102,32,116,104,101,105,110 +,116,101,114,112,114,101,116,97,116,105,111,110,115,116,104,101,32,116,114,97, +100,105,116,105,111,110,97,108,100,101,118,101,108,111,112,109,101,110,116,32, +111,102,32,102,114,101,113,117,101,110,116,108,121,32,117,115,101,100,97,32,99, +111,108,108,101,99,116,105,111,110,32,111,102,118,101,114,121,32,115,105,109,105 +,108,97,114,32,116,111,115,117,114,114,111,117,110,100,105,110,103,32,116,104, +101,101,120,97,109,112,108,101,32,111,102,32,116,104,105,115,97,108,105,103,110, +61,34,99,101,110,116,101,114,34,62,119,111,117,108,100,32,104,97,118,101,32,98, +101,101,110,105,109,97,103,101,95,99,97,112,116,105,111,110,32,61,97,116,116,97, +99,104,101,100,32,116,111,32,116,104,101,115,117,103,103,101,115,116,105,110,103 +,32,116,104,97,116,105,110,32,116,104,101,32,102,111,114,109,32,111,102,32,105, +110,118,111,108,118,101,100,32,105,110,32,116,104,101,105,115,32,100,101,114,105 +,118,101,100,32,102,114,111,109,110,97,109,101,100,32,97,102,116,101,114,32,116, +104,101,73,110,116,114,111,100,117,99,116,105,111,110,32,116,111,114,101,115,116 +,114,105,99,116,105,111,110,115,32,111,110,32,115,116,121,108,101,61,34,119,105, +100,116,104,58,32,99,97,110,32,98,101,32,117,115,101,100,32,116,111,32,116,104, +101,32,99,114,101,97,116,105,111,110,32,111,102,109,111,115,116,32,105,109,112, +111,114,116,97,110,116,32,105,110,102,111,114,109,97,116,105,111,110,32,97,110, +100,114,101,115,117,108,116,101,100,32,105,110,32,116,104,101,99,111,108,108,97, +112,115,101,32,111,102,32,116,104,101,84,104,105,115,32,109,101,97,110,115,32, +116,104,97,116,101,108,101,109,101,110,116,115,32,111,102,32,116,104,101,119,97, +115,32,114,101,112,108,97,99,101,100,32,98,121,97,110,97,108,121,115,105,115,32, +111,102,32,116,104,101,105,110,115,112,105,114,97,116,105,111,110,32,102,111,114 +,114,101,103,97,114,100,101,100,32,97,115,32,116,104,101,109,111,115,116,32,115, +117,99,99,101,115,115,102,117,108,107,110,111,119,110,32,97,115,32,38,113,117, +111,116,59,97,32,99,111,109,112,114,101,104,101,110,115,105,118,101,72,105,115, +116,111,114,121,32,111,102,32,116,104,101,32,119,101,114,101,32,99,111,110,115, +105,100,101,114,101,100,114,101,116,117,114,110,101,100,32,116,111,32,116,104, +101,97,114,101,32,114,101,102,101,114,114,101,100,32,116,111,85,110,115,111,117, +114,99,101,100,32,105,109,97,103,101,62,10,9,60,100,105,118,32,99,108,97,115,115 +,61,34,99,111,110,115,105,115,116,115,32,111,102,32,116,104,101,115,116,111,112, +80,114,111,112,97,103,97,116,105,111,110,105,110,116,101,114,101,115,116,32,105, +110,32,116,104,101,97,118,97,105,108,97,98,105,108,105,116,121,32,111,102,97,112 +,112,101,97,114,115,32,116,111,32,104,97,118,101,101,108,101,99,116,114,111,109, +97,103,110,101,116,105,99,101,110,97,98,108,101,83,101,114,118,105,99,101,115,40 +,102,117,110,99,116,105,111,110,32,111,102,32,116,104,101,73,116,32,105,115,32, +105,109,112,111,114,116,97,110,116,60,47,115,99,114,105,112,116,62,60,47,100,105 +,118,62,102,117,110,99,116,105,111,110,40,41,123,118,97,114,32,114,101,108,97, +116,105,118,101,32,116,111,32,116,104,101,97,115,32,97,32,114,101,115,117,108, +116,32,111,102,32,116,104,101,32,112,111,115,105,116,105,111,110,32,111,102,70, +111,114,32,101,120,97,109,112,108,101,44,32,105,110,32,109,101,116,104,111,100, +61,34,112,111,115,116,34,32,119,97,115,32,102,111,108,108,111,119,101,100,32,98, +121,38,97,109,112,59,109,100,97,115,104,59,32,116,104,101,116,104,101,32,97,112, +112,108,105,99,97,116,105,111,110,106,115,34,62,60,47,115,99,114,105,112,116,62, +13,10,117,108,62,60,47,100,105,118,62,60,47,100,105,118,62,97,102,116,101,114,32 +,116,104,101,32,100,101,97,116,104,119,105,116,104,32,114,101,115,112,101,99,116 +,32,116,111,115,116,121,108,101,61,34,112,97,100,100,105,110,103,58,105,115,32, +112,97,114,116,105,99,117,108,97,114,108,121,100,105,115,112,108,97,121,58,105, +110,108,105,110,101,59,32,116,121,112,101,61,34,115,117,98,109,105,116,34,32,105 +,115,32,100,105,118,105,100,101,100,32,105,110,116,111,228,184,173,230,150,135, +32,40,231,174,128,228,189,147,41,114,101,115,112,111,110,115,97,98,105,108,105, +100,97,100,97,100,109,105,110,105,115,116,114,97,99,105,195,179,110,105,110,116, +101,114,110,97,99,105,111,110,97,108,101,115,99,111,114,114,101,115,112,111,110, +100,105,101,110,116,101,224,164,137,224,164,170,224,164,175,224,165,139,224,164, +151,224,164,170,224,165,130,224,164,176,224,165,141,224,164,181,224,164,185,224, +164,174,224,164,190,224,164,176,224,165,135,224,164,178,224,165,139,224,164,151, +224,165,139,224,164,130,224,164,154,224,165,129,224,164,168,224,164,190,224,164, +181,224,164,178,224,165,135,224,164,149,224,164,191,224,164,168,224,164,184,224, +164,176,224,164,149,224,164,190,224,164,176,224,164,170,224,165,129,224,164,178, +224,164,191,224,164,184,224,164,150,224,165,139,224,164,156,224,165,135,224,164, +130,224,164,154,224,164,190,224,164,185,224,164,191,224,164,143,224,164,173,224, +165,135,224,164,156,224,165,135,224,164,130,224,164,182,224,164,190,224,164,174, +224,164,191,224,164,178,224,164,185,224,164,174,224,164,190,224,164,176,224,165, +128,224,164,156,224,164,190,224,164,151,224,164,176,224,164,163,224,164,172,224, +164,168,224,164,190,224,164,168,224,165,135,224,164,149,224,165,129,224,164,174, +224,164,190,224,164,176,224,164,172,224,165,141,224,164,178,224,165,137,224,164, +151,224,164,174,224,164,190,224,164,178,224,164,191,224,164,149,224,164,174,224, +164,185,224,164,191,224,164,178,224,164,190,224,164,170,224,165,131,224,164,183, +224,165,141,224,164,160,224,164,172,224,164,162,224,164,188,224,164,164,224,165, +135,224,164,173,224,164,190,224,164,156,224,164,170,224,164,190,224,164,149,224, +165,141,224,164,178,224,164,191,224,164,149,224,164,159,224,165,141,224,164,176, +224,165,135,224,164,168,224,164,150,224,164,191,224,164,178,224,164,190,224,164, +171,224,164,166,224,165,140,224,164,176,224,164,190,224,164,168,224,164,174,224, +164,190,224,164,174,224,164,178,224,165,135,224,164,174,224,164,164,224,164,166, +224,164,190,224,164,168,224,164,172,224,164,190,224,164,156,224,164,190,224,164, +176,224,164,181,224,164,191,224,164,149,224,164,190,224,164,184,224,164,149,224, +165,141,224,164,175,224,165,139,224,164,130,224,164,154,224,164,190,224,164,185, +224,164,164,224,165,135,224,164,170,224,164,185,224,165,129,224,164,129,224,164, +154,224,164,172,224,164,164,224,164,190,224,164,175,224,164,190,224,164,184,224, +164,130,224,164,181,224,164,190,224,164,166,224,164,166,224,165,135,224,164,150, +224,164,168,224,165,135,224,164,170,224,164,191,224,164,155,224,164,178,224,165, +135,224,164,181,224,164,191,224,164,182,224,165,135,224,164,183,224,164,176,224, +164,190,224,164,156,224,165,141,224,164,175,224,164,137,224,164,164,224,165,141, +224,164,164,224,164,176,224,164,174,224,165,129,224,164,130,224,164,172,224,164, +136,224,164,166,224,165,139,224,164,168,224,165,139,224,164,130,224,164,137,224, +164,170,224,164,149,224,164,176,224,164,163,224,164,170,224,164,162,224,164,188, +224,165,135,224,164,130,224,164,184,224,165,141,224,164,165,224,164,191,224,164, +164,224,164,171,224,164,191,224,164,178,224,165,141,224,164,174,224,164,174,224, +165,129,224,164,150,224,165,141,224,164,175,224,164,133,224,164,154,224,165,141, +224,164,155,224,164,190,224,164,155,224,165,130,224,164,159,224,164,164,224,165, +128,224,164,184,224,164,130,224,164,151,224,165,128,224,164,164,224,164,156,224, +164,190,224,164,143,224,164,151,224,164,190,224,164,181,224,164,191,224,164,173, +224,164,190,224,164,151,224,164,152,224,164,163,224,165,141,224,164,159,224,165, +135,224,164,166,224,165,130,224,164,184,224,164,176,224,165,135,224,164,166,224, +164,191,224,164,168,224,165,139,224,164,130,224,164,185,224,164,164,224,165,141, +224,164,175,224,164,190,224,164,184,224,165,135,224,164,149,224,165,141,224,164, +184,224,164,151,224,164,190,224,164,130,224,164,167,224,165,128,224,164,181,224, +164,191,224,164,182,224,165,141,224,164,181,224,164,176,224,164,190,224,164,164, +224,165,135,224,164,130,224,164,166,224,165,136,224,164,159,224,165,141,224,164, +184,224,164,168,224,164,149,224,165,141,224,164,182,224,164,190,224,164,184,224, +164,190,224,164,174,224,164,168,224,165,135,224,164,133,224,164,166,224,164,190, +224,164,178,224,164,164,224,164,172,224,164,191,224,164,156,224,164,178,224,165, +128,224,164,170,224,165,129,224,164,176,224,165,130,224,164,183,224,164,185,224, +164,191,224,164,130,224,164,166,224,165,128,224,164,174,224,164,191,224,164,164, +224,165,141,224,164,176,224,164,149,224,164,181,224,164,191,224,164,164,224,164, +190,224,164,176,224,165,129,224,164,170,224,164,175,224,165,135,224,164,184,224, +165,141,224,164,165,224,164,190,224,164,168,224,164,149,224,164,176,224,165,139, +224,164,161,224,164,188,224,164,174,224,165,129,224,164,149,224,165,141,224,164, +164,224,164,175,224,165,139,224,164,156,224,164,168,224,164,190,224,164,149,224, +165,131,224,164,170,224,164,175,224,164,190,224,164,170,224,165,139,224,164,184, +224,165,141,224,164,159,224,164,152,224,164,176,224,165,135,224,164,178,224,165, +130,224,164,149,224,164,190,224,164,176,224,165,141,224,164,175,224,164,181,224, +164,191,224,164,154,224,164,190,224,164,176,224,164,184,224,165,130,224,164,154, +224,164,168,224,164,190,224,164,174,224,165,130,224,164,178,224,165,141,224,164, +175,224,164,166,224,165,135,224,164,150,224,165,135,224,164,130,224,164,185,224, +164,174,224,165,135,224,164,182,224,164,190,224,164,184,224,165,141,224,164,149, +224,165,130,224,164,178,224,164,174,224,165,136,224,164,130,224,164,168,224,165, +135,224,164,164,224,165,136,224,164,175,224,164,190,224,164,176,224,164,156,224, +164,191,224,164,184,224,164,149,224,165,135,114,115,115,43,120,109,108,34,32,116 +,105,116,108,101,61,34,45,116,121,112,101,34,32,99,111,110,116,101,110,116,61,34 +,116,105,116,108,101,34,32,99,111,110,116,101,110,116,61,34,97,116,32,116,104, +101,32,115,97,109,101,32,116,105,109,101,46,106,115,34,62,60,47,115,99,114,105, +112,116,62,10,60,34,32,109,101,116,104,111,100,61,34,112,111,115,116,34,32,60,47 +,115,112,97,110,62,60,47,97,62,60,47,108,105,62,118,101,114,116,105,99,97,108,45 +,97,108,105,103,110,58,116,47,106,113,117,101,114,121,46,109,105,110,46,106,115, +34,62,46,99,108,105,99,107,40,102,117,110,99,116,105,111,110,40,32,115,116,121, +108,101,61,34,112,97,100,100,105,110,103,45,125,41,40,41,59,10,60,47,115,99,114, +105,112,116,62,10,60,47,115,112,97,110,62,60,97,32,104,114,101,102,61,34,60,97, +32,104,114,101,102,61,34,104,116,116,112,58,47,47,41,59,32,114,101,116,117,114, +110,32,102,97,108,115,101,59,116,101,120,116,45,100,101,99,111,114,97,116,105, +111,110,58,32,115,99,114,111,108,108,105,110,103,61,34,110,111,34,32,98,111,114, +100,101,114,45,99,111,108,108,97,112,115,101,58,97,115,115,111,99,105,97,116,101 +,100,32,119,105,116,104,32,66,97,104,97,115,97,32,73,110,100,111,110,101,115,105 +,97,69,110,103,108,105,115,104,32,108,97,110,103,117,97,103,101,60,116,101,120, +116,32,120,109,108,58,115,112,97,99,101,61,46,103,105,102,34,32,98,111,114,100, +101,114,61,34,48,34,60,47,98,111,100,121,62,10,60,47,104,116,109,108,62,10,111, +118,101,114,102,108,111,119,58,104,105,100,100,101,110,59,105,109,103,32,115,114 +,99,61,34,104,116,116,112,58,47,47,97,100,100,69,118,101,110,116,76,105,115,116, +101,110,101,114,114,101,115,112,111,110,115,105,98,108,101,32,102,111,114,32,115 +,46,106,115,34,62,60,47,115,99,114,105,112,116,62,10,47,102,97,118,105,99,111, +110,46,105,99,111,34,32,47,62,111,112,101,114,97,116,105,110,103,32,115,121,115, +116,101,109,34,32,115,116,121,108,101,61,34,119,105,100,116,104,58,49,116,97,114 +,103,101,116,61,34,95,98,108,97,110,107,34,62,83,116,97,116,101,32,85,110,105, +118,101,114,115,105,116,121,116,101,120,116,45,97,108,105,103,110,58,108,101,102 +,116,59,10,100,111,99,117,109,101,110,116,46,119,114,105,116,101,40,44,32,105, +110,99,108,117,100,105,110,103,32,116,104,101,32,97,114,111,117,110,100,32,116, +104,101,32,119,111,114,108,100,41,59,13,10,60,47,115,99,114,105,112,116,62,13,10 +,60,34,32,115,116,121,108,101,61,34,104,101,105,103,104,116,58,59,111,118,101, +114,102,108,111,119,58,104,105,100,100,101,110,109,111,114,101,32,105,110,102, +111,114,109,97,116,105,111,110,97,110,32,105,110,116,101,114,110,97,116,105,111, +110,97,108,97,32,109,101,109,98,101,114,32,111,102,32,116,104,101,32,111,110,101 +,32,111,102,32,116,104,101,32,102,105,114,115,116,99,97,110,32,98,101,32,102,111 +,117,110,100,32,105,110,32,60,47,100,105,118,62,10,9,9,60,47,100,105,118,62,10, +100,105,115,112,108,97,121,58,32,110,111,110,101,59,34,62,34,32,47,62,10,60,108, +105,110,107,32,114,101,108,61,34,10,32,32,40,102,117,110,99,116,105,111,110,40, +41,32,123,116,104,101,32,49,53,116,104,32,99,101,110,116,117,114,121,46,112,114, +101,118,101,110,116,68,101,102,97,117,108,116,40,108,97,114,103,101,32,110,117, +109,98,101,114,32,111,102,32,66,121,122,97,110,116,105,110,101,32,69,109,112,105 +,114,101,46,106,112,103,124,116,104,117,109,98,124,108,101,102,116,124,118,97, +115,116,32,109,97,106,111,114,105,116,121,32,111,102,109,97,106,111,114,105,116, +121,32,111,102,32,116,104,101,32,32,97,108,105,103,110,61,34,99,101,110,116,101, +114,34,62,85,110,105,118,101,114,115,105,116,121,32,80,114,101,115,115,100,111, +109,105,110,97,116,101,100,32,98,121,32,116,104,101,83,101,99,111,110,100,32,87, +111,114,108,100,32,87,97,114,100,105,115,116,114,105,98,117,116,105,111,110,32, +111,102,32,115,116,121,108,101,61,34,112,111,115,105,116,105,111,110,58,116,104, +101,32,114,101,115,116,32,111,102,32,116,104,101,32,99,104,97,114,97,99,116,101, +114,105,122,101,100,32,98,121,32,114,101,108,61,34,110,111,102,111,108,108,111, +119,34,62,100,101,114,105,118,101,115,32,102,114,111,109,32,116,104,101,114,97, +116,104,101,114,32,116,104,97,110,32,116,104,101,32,97,32,99,111,109,98,105,110, +97,116,105,111,110,32,111,102,115,116,121,108,101,61,34,119,105,100,116,104,58, +49,48,48,69,110,103,108,105,115,104,45,115,112,101,97,107,105,110,103,99,111,109 +,112,117,116,101,114,32,115,99,105,101,110,99,101,98,111,114,100,101,114,61,34, +48,34,32,97,108,116,61,34,116,104,101,32,101,120,105,115,116,101,110,99,101,32, +111,102,68,101,109,111,99,114,97,116,105,99,32,80,97,114,116,121,34,32,115,116, +121,108,101,61,34,109,97,114,103,105,110,45,70,111,114,32,116,104,105,115,32,114 +,101,97,115,111,110,44,46,106,115,34,62,60,47,115,99,114,105,112,116,62,10,9,115 +,66,121,84,97,103,78,97,109,101,40,115,41,91,48,93,106,115,34,62,60,47,115,99, +114,105,112,116,62,13,10,60,46,106,115,34,62,60,47,115,99,114,105,112,116,62,13, +10,108,105,110,107,32,114,101,108,61,34,105,99,111,110,34,32,39,32,97,108,116,61 +,39,39,32,99,108,97,115,115,61,39,102,111,114,109,97,116,105,111,110,32,111,102, +32,116,104,101,118,101,114,115,105,111,110,115,32,111,102,32,116,104,101,32,60, +47,97,62,60,47,100,105,118,62,60,47,100,105,118,62,47,112,97,103,101,62,10,32,32 +,60,112,97,103,101,62,10,60,100,105,118,32,99,108,97,115,115,61,34,99,111,110, +116,98,101,99,97,109,101,32,116,104,101,32,102,105,114,115,116,98,97,104,97,115, +97,32,73,110,100,111,110,101,115,105,97,101,110,103,108,105,115,104,32,40,115, +105,109,112,108,101,41,206,149,206,187,206,187,206,183,206,189,206,185,206,186, +206,172,209,133,209,128,208,178,208,176,209,130,209,129,208,186,208,184,208,186, +208,190,208,188,208,191,208,176,208,189,208,184,208,184,209,143,208,178,208,187, +209,143,208,181,209,130,209,129,209,143,208,148,208,190,208,177,208,176,208,178, +208,184,209,130,209,140,209,135,208,181,208,187,208,190,208,178,208,181,208,186, +208,176,209,128,208,176,208,183,208,178,208,184,209,130,208,184,209,143,208,152, +208,189,209,130,208,181,209,128,208,189,208,181,209,130,208,158,209,130,208,178, +208,181,209,130,208,184,209,130,209,140,208,189,208,176,208,191,209,128,208,184, +208,188,208,181,209,128,208,184,208,189,209,130,208,181,209,128,208,189,208,181, +209,130,208,186,208,190,209,130,208,190,209,128,208,190,208,179,208,190,209,129, +209,130,209,128,208,176,208,189,208,184,209,134,209,139,208,186,208,176,209,135, +208,181,209,129,209,130,208,178,208,181,209,131,209,129,208,187,208,190,208,178, +208,184,209,143,209,133,208,191,209,128,208,190,208,177,208,187,208,181,208,188, +209,139,208,191,208,190,208,187,209,131,209,135,208,184,209,130,209,140,209,143, +208,178,208,187,209,143,209,142,209,130,209,129,209,143,208,189,208,176,208,184, +208,177,208,190,208,187,208,181,208,181,208,186,208,190,208,188,208,191,208,176, +208,189,208,184,209,143,208,178,208,189,208,184,208,188,208,176,208,189,208,184, +208,181,209,129,209,128,208,181,208,180,209,129,209,130,208,178,208,176,216,167, +217,132,217,133,217,136,216,167,216,182,217,138,216,185,216,167,217,132,216,177, +216,166,217,138,216,179,217,138,216,169,216,167,217,132,216,167,217,134,216,170, +217,130,216,167,217,132,217,133,216,180,216,167,216,177,217,131,216,167,216,170, +217,131,216,167,217,132,216,179,217,138,216,167,216,177,216,167,216,170,216,167, +217,132,217,133,217,131,216,170,217,136,216,168,216,169,216,167,217,132,216,179, +216,185,217,136,216,175,217,138,216,169,216,167,216,173,216,181,216,167,216,166, +217,138,216,167,216,170,216,167,217,132,216,185,216,167,217,132,217,133,217,138, +216,169,216,167,217,132,216,181,217,136,216,170,217,138,216,167,216,170,216,167, +217,132,216,167,217,134,216,170,216,177,217,134,216,170,216,167,217,132,216,170, +216,181,216,167,217,133,217,138,217,133,216,167,217,132,216,165,216,179,217,132, +216,167,217,133,217,138,216,167,217,132,217,133,216,180,216,167,216,177,217,131, +216,169,216,167,217,132,217,133,216,177,216,166,217,138,216,167,216,170,114,111, +98,111,116,115,34,32,99,111,110,116,101,110,116,61,34,60,100,105,118,32,105,100, +61,34,102,111,111,116,101,114,34,62,116,104,101,32,85,110,105,116,101,100,32,83, +116,97,116,101,115,60,105,109,103,32,115,114,99,61,34,104,116,116,112,58,47,47, +46,106,112,103,124,114,105,103,104,116,124,116,104,117,109,98,124,46,106,115,34, +62,60,47,115,99,114,105,112,116,62,13,10,60,108,111,99,97,116,105,111,110,46,112 +,114,111,116,111,99,111,108,102,114,97,109,101,98,111,114,100,101,114,61,34,48, +34,32,115,34,32,47,62,10,60,109,101,116,97,32,110,97,109,101,61,34,60,47,97,62, +60,47,100,105,118,62,60,47,100,105,118,62,60,102,111,110,116,45,119,101,105,103, +104,116,58,98,111,108,100,59,38,113,117,111,116,59,32,97,110,100,32,38,113,117, +111,116,59,100,101,112,101,110,100,105,110,103,32,111,110,32,116,104,101,32,109, +97,114,103,105,110,58,48,59,112,97,100,100,105,110,103,58,34,32,114,101,108,61, +34,110,111,102,111,108,108,111,119,34,32,80,114,101,115,105,100,101,110,116,32, +111,102,32,116,104,101,32,116,119,101,110,116,105,101,116,104,32,99,101,110,116, +117,114,121,101,118,105,115,105,111,110,62,10,32,32,60,47,112,97,103,101,73,110, +116,101,114,110,101,116,32,69,120,112,108,111,114,101,114,97,46,97,115,121,110, +99,32,61,32,116,114,117,101,59,13,10,105,110,102,111,114,109,97,116,105,111,110, +32,97,98,111,117,116,60,100,105,118,32,105,100,61,34,104,101,97,100,101,114,34, +62,34,32,97,99,116,105,111,110,61,34,104,116,116,112,58,47,47,60,97,32,104,114, +101,102,61,34,104,116,116,112,115,58,47,47,60,100,105,118,32,105,100,61,34,99, +111,110,116,101,110,116,34,60,47,100,105,118,62,13,10,60,47,100,105,118,62,13,10 +,60,100,101,114,105,118,101,100,32,102,114,111,109,32,116,104,101,32,60,105,109, +103,32,115,114,99,61,39,104,116,116,112,58,47,47,97,99,99,111,114,100,105,110, +103,32,116,111,32,116,104,101,32,10,60,47,98,111,100,121,62,10,60,47,104,116,109 +,108,62,10,115,116,121,108,101,61,34,102,111,110,116,45,115,105,122,101,58,115, +99,114,105,112,116,32,108,97,110,103,117,97,103,101,61,34,65,114,105,97,108,44, +32,72,101,108,118,101,116,105,99,97,44,60,47,97,62,60,115,112,97,110,32,99,108, +97,115,115,61,34,60,47,115,99,114,105,112,116,62,60,115,99,114,105,112,116,32, +112,111,108,105,116,105,99,97,108,32,112,97,114,116,105,101,115,116,100,62,60,47 +,116,114,62,60,47,116,97,98,108,101,62,60,104,114,101,102,61,34,104,116,116,112, +58,47,47,119,119,119,46,105,110,116,101,114,112,114,101,116,97,116,105,111,110, +32,111,102,114,101,108,61,34,115,116,121,108,101,115,104,101,101,116,34,32,100, +111,99,117,109,101,110,116,46,119,114,105,116,101,40,39,60,99,104,97,114,115,101 +,116,61,34,117,116,102,45,56,34,62,10,98,101,103,105,110,110,105,110,103,32,111, +102,32,116,104,101,32,114,101,118,101,97,108,101,100,32,116,104,97,116,32,116, +104,101,116,101,108,101,118,105,115,105,111,110,32,115,101,114,105,101,115,34,32 +,114,101,108,61,34,110,111,102,111,108,108,111,119,34,62,32,116,97,114,103,101, +116,61,34,95,98,108,97,110,107,34,62,99,108,97,105,109,105,110,103,32,116,104,97 +,116,32,116,104,101,104,116,116,112,37,51,65,37,50,70,37,50,70,119,119,119,46, +109,97,110,105,102,101,115,116,97,116,105,111,110,115,32,111,102,80,114,105,109, +101,32,77,105,110,105,115,116,101,114,32,111,102,105,110,102,108,117,101,110,99, +101,100,32,98,121,32,116,104,101,99,108,97,115,115,61,34,99,108,101,97,114,102, +105,120,34,62,47,100,105,118,62,13,10,60,47,100,105,118,62,13,10,13,10,116,104, +114,101,101,45,100,105,109,101,110,115,105,111,110,97,108,67,104,117,114,99,104, +32,111,102,32,69,110,103,108,97,110,100,111,102,32,78,111,114,116,104,32,67,97, +114,111,108,105,110,97,115,113,117,97,114,101,32,107,105,108,111,109,101,116,114 +,101,115,46,97,100,100,69,118,101,110,116,76,105,115,116,101,110,101,114,100,105 +,115,116,105,110,99,116,32,102,114,111,109,32,116,104,101,99,111,109,109,111,110 +,108,121,32,107,110,111,119,110,32,97,115,80,104,111,110,101,116,105,99,32,65, +108,112,104,97,98,101,116,100,101,99,108,97,114,101,100,32,116,104,97,116,32,116 +,104,101,99,111,110,116,114,111,108,108,101,100,32,98,121,32,116,104,101,66,101, +110,106,97,109,105,110,32,70,114,97,110,107,108,105,110,114,111,108,101,45,112, +108,97,121,105,110,103,32,103,97,109,101,116,104,101,32,85,110,105,118,101,114, +115,105,116,121,32,111,102,105,110,32,87,101,115,116,101,114,110,32,69,117,114, +111,112,101,112,101,114,115,111,110,97,108,32,99,111,109,112,117,116,101,114,80, +114,111,106,101,99,116,32,71,117,116,101,110,98,101,114,103,114,101,103,97,114, +100,108,101,115,115,32,111,102,32,116,104,101,104,97,115,32,98,101,101,110,32, +112,114,111,112,111,115,101,100,116,111,103,101,116,104,101,114,32,119,105,116, +104,32,116,104,101,62,60,47,108,105,62,60,108,105,32,99,108,97,115,115,61,34,105 +,110,32,115,111,109,101,32,99,111,117,110,116,114,105,101,115,109,105,110,46,106 +,115,34,62,60,47,115,99,114,105,112,116,62,111,102,32,116,104,101,32,112,111,112 +,117,108,97,116,105,111,110,111,102,102,105,99,105,97,108,32,108,97,110,103,117, +97,103,101,60,105,109,103,32,115,114,99,61,34,105,109,97,103,101,115,47,105,100, +101,110,116,105,102,105,101,100,32,98,121,32,116,104,101,110,97,116,117,114,97, +108,32,114,101,115,111,117,114,99,101,115,99,108,97,115,115,105,102,105,99,97, +116,105,111,110,32,111,102,99,97,110,32,98,101,32,99,111,110,115,105,100,101,114 +,101,100,113,117,97,110,116,117,109,32,109,101,99,104,97,110,105,99,115,78,101, +118,101,114,116,104,101,108,101,115,115,44,32,116,104,101,109,105,108,108,105, +111,110,32,121,101,97,114,115,32,97,103,111,60,47,98,111,100,121,62,13,10,60,47, +104,116,109,108,62,13,206,149,206,187,206,187,206,183,206,189,206,185,206,186, +206,172,10,116,97,107,101,32,97,100,118,97,110,116,97,103,101,32,111,102,97,110, +100,44,32,97,99,99,111,114,100,105,110,103,32,116,111,97,116,116,114,105,98,117, +116,101,100,32,116,111,32,116,104,101,77,105,99,114,111,115,111,102,116,32,87, +105,110,100,111,119,115,116,104,101,32,102,105,114,115,116,32,99,101,110,116,117 +,114,121,117,110,100,101,114,32,116,104,101,32,99,111,110,116,114,111,108,100, +105,118,32,99,108,97,115,115,61,34,104,101,97,100,101,114,115,104,111,114,116, +108,121,32,97,102,116,101,114,32,116,104,101,110,111,116,97,98,108,101,32,101, +120,99,101,112,116,105,111,110,116,101,110,115,32,111,102,32,116,104,111,117,115 +,97,110,100,115,115,101,118,101,114,97,108,32,100,105,102,102,101,114,101,110, +116,97,114,111,117,110,100,32,116,104,101,32,119,111,114,108,100,46,114,101,97, +99,104,105,110,103,32,109,105,108,105,116,97,114,121,105,115,111,108,97,116,101, +100,32,102,114,111,109,32,116,104,101,111,112,112,111,115,105,116,105,111,110,32 +,116,111,32,116,104,101,116,104,101,32,79,108,100,32,84,101,115,116,97,109,101, +110,116,65,102,114,105,99,97,110,32,65,109,101,114,105,99,97,110,115,105,110,115 +,101,114,116,101,100,32,105,110,116,111,32,116,104,101,115,101,112,97,114,97,116 +,101,32,102,114,111,109,32,116,104,101,109,101,116,114,111,112,111,108,105,116, +97,110,32,97,114,101,97,109,97,107,101,115,32,105,116,32,112,111,115,115,105,98, +108,101,97,99,107,110,111,119,108,101,100,103,101,100,32,116,104,97,116,97,114, +103,117,97,98,108,121,32,116,104,101,32,109,111,115,116,116,121,112,101,61,34, +116,101,120,116,47,99,115,115,34,62,10,116,104,101,32,73,110,116,101,114,110,97, +116,105,111,110,97,108,65,99,99,111,114,100,105,110,103,32,116,111,32,116,104, +101,32,112,101,61,34,116,101,120,116,47,99,115,115,34,32,47,62,10,99,111,105,110 +,99,105,100,101,32,119,105,116,104,32,116,104,101,116,119,111,45,116,104,105,114 +,100,115,32,111,102,32,116,104,101,68,117,114,105,110,103,32,116,104,105,115,32, +116,105,109,101,44,100,117,114,105,110,103,32,116,104,101,32,112,101,114,105,111 +,100,97,110,110,111,117,110,99,101,100,32,116,104,97,116,32,104,101,116,104,101, +32,105,110,116,101,114,110,97,116,105,111,110,97,108,97,110,100,32,109,111,114, +101,32,114,101,99,101,110,116,108,121,98,101,108,105,101,118,101,100,32,116,104, +97,116,32,116,104,101,99,111,110,115,99,105,111,117,115,110,101,115,115,32,97, +110,100,102,111,114,109,101,114,108,121,32,107,110,111,119,110,32,97,115,115,117 +,114,114,111,117,110,100,101,100,32,98,121,32,116,104,101,102,105,114,115,116,32 +,97,112,112,101,97,114,101,100,32,105,110,111,99,99,97,115,105,111,110,97,108, +108,121,32,117,115,101,100,112,111,115,105,116,105,111,110,58,97,98,115,111,108, +117,116,101,59,34,32,116,97,114,103,101,116,61,34,95,98,108,97,110,107,34,32,112 +,111,115,105,116,105,111,110,58,114,101,108,97,116,105,118,101,59,116,101,120, +116,45,97,108,105,103,110,58,99,101,110,116,101,114,59,106,97,120,47,108,105,98, +115,47,106,113,117,101,114,121,47,49,46,98,97,99,107,103,114,111,117,110,100,45, +99,111,108,111,114,58,35,116,121,112,101,61,34,97,112,112,108,105,99,97,116,105, +111,110,47,97,110,103,117,97,103,101,34,32,99,111,110,116,101,110,116,61,34,60, +109,101,116,97,32,104,116,116,112,45,101,113,117,105,118,61,34,80,114,105,118,97 +,99,121,32,80,111,108,105,99,121,60,47,97,62,101,40,34,37,51,67,115,99,114,105, +112,116,32,115,114,99,61,39,34,32,116,97,114,103,101,116,61,34,95,98,108,97,110, +107,34,62,79,110,32,116,104,101,32,111,116,104,101,114,32,104,97,110,100,44,46, +106,112,103,124,116,104,117,109,98,124,114,105,103,104,116,124,50,60,47,100,105, +118,62,60,100,105,118,32,99,108,97,115,115,61,34,60,100,105,118,32,115,116,121, +108,101,61,34,102,108,111,97,116,58,110,105,110,101,116,101,101,110,116,104,32, +99,101,110,116,117,114,121,60,47,98,111,100,121,62,13,10,60,47,104,116,109,108, +62,13,10,60,105,109,103,32,115,114,99,61,34,104,116,116,112,58,47,47,115,59,116, +101,120,116,45,97,108,105,103,110,58,99,101,110,116,101,114,102,111,110,116,45, +119,101,105,103,104,116,58,32,98,111,108,100,59,32,65,99,99,111,114,100,105,110, +103,32,116,111,32,116,104,101,32,100,105,102,102,101,114,101,110,99,101,32,98, +101,116,119,101,101,110,34,32,102,114,97,109,101,98,111,114,100,101,114,61,34,48 +,34,32,34,32,115,116,121,108,101,61,34,112,111,115,105,116,105,111,110,58,108, +105,110,107,32,104,114,101,102,61,34,104,116,116,112,58,47,47,104,116,109,108,52 +,47,108,111,111,115,101,46,100,116,100,34,62,10,100,117,114,105,110,103,32,116, +104,105,115,32,112,101,114,105,111,100,60,47,116,100,62,60,47,116,114,62,60,47, +116,97,98,108,101,62,99,108,111,115,101,108,121,32,114,101,108,97,116,101,100,32 +,116,111,102,111,114,32,116,104,101,32,102,105,114,115,116,32,116,105,109,101,59 +,102,111,110,116,45,119,101,105,103,104,116,58,98,111,108,100,59,105,110,112,117 +,116,32,116,121,112,101,61,34,116,101,120,116,34,32,60,115,112,97,110,32,115,116 +,121,108,101,61,34,102,111,110,116,45,111,110,114,101,97,100,121,115,116,97,116, +101,99,104,97,110,103,101,9,60,100,105,118,32,99,108,97,115,115,61,34,99,108,101 +,97,114,100,111,99,117,109,101,110,116,46,108,111,99,97,116,105,111,110,46,32,70 +,111,114,32,101,120,97,109,112,108,101,44,32,116,104,101,32,97,32,119,105,100, +101,32,118,97,114,105,101,116,121,32,111,102,32,60,33,68,79,67,84,89,80,69,32, +104,116,109,108,62,13,10,60,38,110,98,115,112,59,38,110,98,115,112,59,38,110,98, +115,112,59,34,62,60,97,32,104,114,101,102,61,34,104,116,116,112,58,47,47,115,116 +,121,108,101,61,34,102,108,111,97,116,58,108,101,102,116,59,99,111,110,99,101, +114,110,101,100,32,119,105,116,104,32,116,104,101,61,104,116,116,112,37,51,65,37 +,50,70,37,50,70,119,119,119,46,105,110,32,112,111,112,117,108,97,114,32,99,117, +108,116,117,114,101,116,121,112,101,61,34,116,101,120,116,47,99,115,115,34,32,47 +,62,105,116,32,105,115,32,112,111,115,115,105,98,108,101,32,116,111,32,72,97,114 +,118,97,114,100,32,85,110,105,118,101,114,115,105,116,121,116,121,108,101,115, +104,101,101,116,34,32,104,114,101,102,61,34,47,116,104,101,32,109,97,105,110,32, +99,104,97,114,97,99,116,101,114,79,120,102,111,114,100,32,85,110,105,118,101,114 +,115,105,116,121,32,32,110,97,109,101,61,34,107,101,121,119,111,114,100,115,34, +32,99,115,116,121,108,101,61,34,116,101,120,116,45,97,108,105,103,110,58,116,104 +,101,32,85,110,105,116,101,100,32,75,105,110,103,100,111,109,102,101,100,101,114 +,97,108,32,103,111,118,101,114,110,109,101,110,116,60,100,105,118,32,115,116,121 +,108,101,61,34,109,97,114,103,105,110,32,100,101,112,101,110,100,105,110,103,32, +111,110,32,116,104,101,32,100,101,115,99,114,105,112,116,105,111,110,32,111,102, +32,116,104,101,60,100,105,118,32,99,108,97,115,115,61,34,104,101,97,100,101,114, +46,109,105,110,46,106,115,34,62,60,47,115,99,114,105,112,116,62,100,101,115,116, +114,117,99,116,105,111,110,32,111,102,32,116,104,101,115,108,105,103,104,116,108 +,121,32,100,105,102,102,101,114,101,110,116,105,110,32,97,99,99,111,114,100,97, +110,99,101,32,119,105,116,104,116,101,108,101,99,111,109,109,117,110,105,99,97, +116,105,111,110,115,105,110,100,105,99,97,116,101,115,32,116,104,97,116,32,116, +104,101,115,104,111,114,116,108,121,32,116,104,101,114,101,97,102,116,101,114, +101,115,112,101,99,105,97,108,108,121,32,105,110,32,116,104,101,32,69,117,114, +111,112,101,97,110,32,99,111,117,110,116,114,105,101,115,72,111,119,101,118,101, +114,44,32,116,104,101,114,101,32,97,114,101,115,114,99,61,34,104,116,116,112,58, +47,47,115,116,97,116,105,99,115,117,103,103,101,115,116,101,100,32,116,104,97, +116,32,116,104,101,34,32,115,114,99,61,34,104,116,116,112,58,47,47,119,119,119, +46,97,32,108,97,114,103,101,32,110,117,109,98,101,114,32,111,102,32,84,101,108, +101,99,111,109,109,117,110,105,99,97,116,105,111,110,115,34,32,114,101,108,61,34 +,110,111,102,111,108,108,111,119,34,32,116,72,111,108,121,32,82,111,109,97,110, +32,69,109,112,101,114,111,114,97,108,109,111,115,116,32,101,120,99,108,117,115, +105,118,101,108,121,34,32,98,111,114,100,101,114,61,34,48,34,32,97,108,116,61,34 +,83,101,99,114,101,116,97,114,121,32,111,102,32,83,116,97,116,101,99,117,108,109 +,105,110,97,116,105,110,103,32,105,110,32,116,104,101,67,73,65,32,87,111,114,108 +,100,32,70,97,99,116,98,111,111,107,116,104,101,32,109,111,115,116,32,105,109, +112,111,114,116,97,110,116,97,110,110,105,118,101,114,115,97,114,121,32,111,102, +32,116,104,101,115,116,121,108,101,61,34,98,97,99,107,103,114,111,117,110,100,45 +,60,108,105,62,60,101,109,62,60,97,32,104,114,101,102,61,34,47,116,104,101,32,65 +,116,108,97,110,116,105,99,32,79,99,101,97,110,115,116,114,105,99,116,108,121,32 +,115,112,101,97,107,105,110,103,44,115,104,111,114,116,108,121,32,98,101,102,111 +,114,101,32,116,104,101,100,105,102,102,101,114,101,110,116,32,116,121,112,101, +115,32,111,102,116,104,101,32,79,116,116,111,109,97,110,32,69,109,112,105,114, +101,62,60,105,109,103,32,115,114,99,61,34,104,116,116,112,58,47,47,65,110,32,73, +110,116,114,111,100,117,99,116,105,111,110,32,116,111,99,111,110,115,101,113,117 +,101,110,99,101,32,111,102,32,116,104,101,100,101,112,97,114,116,117,114,101,32, +102,114,111,109,32,116,104,101,67,111,110,102,101,100,101,114,97,116,101,32,83, +116,97,116,101,115,105,110,100,105,103,101,110,111,117,115,32,112,101,111,112, +108,101,115,80,114,111,99,101,101,100,105,110,103,115,32,111,102,32,116,104,101, +105,110,102,111,114,109,97,116,105,111,110,32,111,110,32,116,104,101,116,104,101 +,111,114,105,101,115,32,104,97,118,101,32,98,101,101,110,105,110,118,111,108,118 +,101,109,101,110,116,32,105,110,32,116,104,101,100,105,118,105,100,101,100,32, +105,110,116,111,32,116,104,114,101,101,97,100,106,97,99,101,110,116,32,99,111, +117,110,116,114,105,101,115,105,115,32,114,101,115,112,111,110,115,105,98,108, +101,32,102,111,114,100,105,115,115,111,108,117,116,105,111,110,32,111,102,32,116 +,104,101,99,111,108,108,97,98,111,114,97,116,105,111,110,32,119,105,116,104,119, +105,100,101,108,121,32,114,101,103,97,114,100,101,100,32,97,115,104,105,115,32, +99,111,110,116,101,109,112,111,114,97,114,105,101,115,102,111,117,110,100,105, +110,103,32,109,101,109,98,101,114,32,111,102,68,111,109,105,110,105,99,97,110,32 +,82,101,112,117,98,108,105,99,103,101,110,101,114,97,108,108,121,32,97,99,99,101 +,112,116,101,100,116,104,101,32,112,111,115,115,105,98,105,108,105,116,121,32, +111,102,97,114,101,32,97,108,115,111,32,97,118,97,105,108,97,98,108,101,117,110, +100,101,114,32,99,111,110,115,116,114,117,99,116,105,111,110,114,101,115,116,111 +,114,97,116,105,111,110,32,111,102,32,116,104,101,116,104,101,32,103,101,110,101 +,114,97,108,32,112,117,98,108,105,99,105,115,32,97,108,109,111,115,116,32,101, +110,116,105,114,101,108,121,112,97,115,115,101,115,32,116,104,114,111,117,103, +104,32,116,104,101,104,97,115,32,98,101,101,110,32,115,117,103,103,101,115,116, +101,100,99,111,109,112,117,116,101,114,32,97,110,100,32,118,105,100,101,111,71, +101,114,109,97,110,105,99,32,108,97,110,103,117,97,103,101,115,32,97,99,99,111, +114,100,105,110,103,32,116,111,32,116,104,101,32,100,105,102,102,101,114,101,110 +,116,32,102,114,111,109,32,116,104,101,115,104,111,114,116,108,121,32,97,102,116 +,101,114,119,97,114,100,115,104,114,101,102,61,34,104,116,116,112,115,58,47,47, +119,119,119,46,114,101,99,101,110,116,32,100,101,118,101,108,111,112,109,101,110 +,116,66,111,97,114,100,32,111,102,32,68,105,114,101,99,116,111,114,115,60,100, +105,118,32,99,108,97,115,115,61,34,115,101,97,114,99,104,124,32,60,97,32,104,114 +,101,102,61,34,104,116,116,112,58,47,47,73,110,32,112,97,114,116,105,99,117,108, +97,114,44,32,116,104,101,77,117,108,116,105,112,108,101,32,102,111,111,116,110, +111,116,101,115,111,114,32,111,116,104,101,114,32,115,117,98,115,116,97,110,99, +101,116,104,111,117,115,97,110,100,115,32,111,102,32,121,101,97,114,115,116,114, +97,110,115,108,97,116,105,111,110,32,111,102,32,116,104,101,60,47,100,105,118,62 +,13,10,60,47,100,105,118,62,13,10,13,10,60,97,32,104,114,101,102,61,34,105,110, +100,101,120,46,112,104,112,119,97,115,32,101,115,116,97,98,108,105,115,104,101, +100,32,105,110,109,105,110,46,106,115,34,62,60,47,115,99,114,105,112,116,62,10, +112,97,114,116,105,99,105,112,97,116,101,32,105,110,32,116,104,101,97,32,115,116 +,114,111,110,103,32,105,110,102,108,117,101,110,99,101,115,116,121,108,101,61,34 +,109,97,114,103,105,110,45,116,111,112,58,114,101,112,114,101,115,101,110,116, +101,100,32,98,121,32,116,104,101,103,114,97,100,117,97,116,101,100,32,102,114, +111,109,32,116,104,101,84,114,97,100,105,116,105,111,110,97,108,108,121,44,32, +116,104,101,69,108,101,109,101,110,116,40,34,115,99,114,105,112,116,34,41,59,72, +111,119,101,118,101,114,44,32,115,105,110,99,101,32,116,104,101,47,100,105,118, +62,10,60,47,100,105,118,62,10,60,100,105,118,32,108,101,102,116,59,32,109,97,114 +,103,105,110,45,108,101,102,116,58,112,114,111,116,101,99,116,105,111,110,32,97, +103,97,105,110,115,116,48,59,32,118,101,114,116,105,99,97,108,45,97,108,105,103, +110,58,85,110,102,111,114,116,117,110,97,116,101,108,121,44,32,116,104,101,116, +121,112,101,61,34,105,109,97,103,101,47,120,45,105,99,111,110,47,100,105,118,62, +10,60,100,105,118,32,99,108,97,115,115,61,34,32,99,108,97,115,115,61,34,99,108, +101,97,114,102,105,120,34,62,60,100,105,118,32,99,108,97,115,115,61,34,102,111, +111,116,101,114,9,9,60,47,100,105,118,62,10,9,9,60,47,100,105,118,62,10,116,104, +101,32,109,111,116,105,111,110,32,112,105,99,116,117,114,101,208,145,209,138,208 +,187,208,179,208,176,209,128,209,129,208,186,208,184,208,177,209,138,208,187,208 +,179,208,176,209,128,209,129,208,186,208,184,208,164,208,181,208,180,208,181,209 +,128,208,176,209,134,208,184,208,184,208,189,208,181,209,129,208,186,208,190,208 +,187,209,140,208,186,208,190,209,129,208,190,208,190,208,177,209,137,208,181,208 +,189,208,184,208,181,209,129,208,190,208,190,208,177,209,137,208,181,208,189,208 +,184,209,143,208,191,209,128,208,190,208,179,209,128,208,176,208,188,208,188,209 +,139,208,158,209,130,208,191,209,128,208,176,208,178,208,184,209,130,209,140,208 +,177,208,181,209,129,208,191,208,187,208,176,209,130,208,189,208,190,208,188,208 +,176,209,130,208,181,209,128,208,184,208,176,208,187,209,139,208,191,208,190,208 +,183,208,178,208,190,208,187,209,143,208,181,209,130,208,191,208,190,209,129,208 +,187,208,181,208,180,208,189,208,184,208,181,209,128,208,176,208,183,208,187,208 +,184,209,135,208,189,209,139,209,133,208,191,209,128,208,190,208,180,209,131,208 +,186,209,134,208,184,208,184,208,191,209,128,208,190,208,179,209,128,208,176,208 +,188,208,188,208,176,208,191,208,190,208,187,208,189,208,190,209,129,209,130,209 +,140,209,142,208,189,208,176,209,133,208,190,208,180,208,184,209,130,209,129,209 +,143,208,184,208,183,208,177,209,128,208,176,208,189,208,189,208,190,208,181,208 +,189,208,176,209,129,208,181,208,187,208,181,208,189,208,184,209,143,208,184,208 +,183,208,188,208,181,208,189,208,181,208,189,208,184,209,143,208,186,208,176,209 +,130,208,181,208,179,208,190,209,128,208,184,208,184,208,144,208,187,208,181,208 +,186,209,129,208,176,208,189,208,180,209,128,224,164,166,224,165,141,224,164,181 +,224,164,190,224,164,176,224,164,190,224,164,174,224,165,136,224,164,168,224,165 +,129,224,164,133,224,164,178,224,164,170,224,165,141,224,164,176,224,164,166,224 +,164,190,224,164,168,224,164,173,224,164,190,224,164,176,224,164,164,224,165,128 +,224,164,175,224,164,133,224,164,168,224,165,129,224,164,166,224,165,135,224,164 +,182,224,164,185,224,164,191,224,164,168,224,165,141,224,164,166,224,165,128,224 +,164,135,224,164,130,224,164,161,224,164,191,224,164,175,224,164,190,224,164,166 +,224,164,191,224,164,178,224,165,141,224,164,178,224,165,128,224,164,133,224,164 +,167,224,164,191,224,164,149,224,164,190,224,164,176,224,164,181,224,165,128,224 +,164,161,224,164,191,224,164,175,224,165,139,224,164,154,224,164,191,224,164,159 +,224,165,141,224,164,160,224,165,135,224,164,184,224,164,174,224,164,190,224,164 +,154,224,164,190,224,164,176,224,164,156,224,164,130,224,164,149,224,165,141,224 +,164,182,224,164,168,224,164,166,224,165,129,224,164,168,224,164,191,224,164,175 +,224,164,190,224,164,170,224,165,141,224,164,176,224,164,175,224,165,139,224,164 +,151,224,164,133,224,164,168,224,165,129,224,164,184,224,164,190,224,164,176,224 +,164,145,224,164,168,224,164,178,224,164,190,224,164,135,224,164,168,224,164,170 +,224,164,190,224,164,176,224,165,141,224,164,159,224,165,128,224,164,182,224,164 +,176,224,165,141,224,164,164,224,165,139,224,164,130,224,164,178,224,165,139,224 +,164,149,224,164,184,224,164,173,224,164,190,224,164,171,224,164,188,224,165,141 +,224,164,178,224,165,136,224,164,182,224,164,182,224,164,176,224,165,141,224,164 +,164,224,165,135,224,164,130,224,164,170,224,165,141,224,164,176,224,164,166,224 +,165,135,224,164,182,224,164,170,224,165,141,224,164,178,224,165,135,224,164,175 +,224,164,176,224,164,149,224,165,135,224,164,130,224,164,166,224,165,141,224,164 +,176,224,164,184,224,165,141,224,164,165,224,164,191,224,164,164,224,164,191,224 +,164,137,224,164,164,224,165,141,224,164,170,224,164,190,224,164,166,224,164,137 +,224,164,168,224,165,141,224,164,185,224,165,135,224,164,130,224,164,154,224,164 +,191,224,164,159,224,165,141,224,164,160,224,164,190,224,164,175,224,164,190,224 +,164,164,224,165,141,224,164,176,224,164,190,224,164,156,224,165,141,224,164,175 +,224,164,190,224,164,166,224,164,190,224,164,170,224,165,129,224,164,176,224,164 +,190,224,164,168,224,165,135,224,164,156,224,165,139,224,164,161,224,164,188,224 +,165,135,224,164,130,224,164,133,224,164,168,224,165,129,224,164,181,224,164,190 +,224,164,166,224,164,182,224,165,141,224,164,176,224,165,135,224,164,163,224,165 +,128,224,164,182,224,164,191,224,164,149,224,165,141,224,164,183,224,164,190,224 +,164,184,224,164,176,224,164,149,224,164,190,224,164,176,224,165,128,224,164,184 +,224,164,130,224,164,151,224,165,141,224,164,176,224,164,185,224,164,170,224,164 +,176,224,164,191,224,164,163,224,164,190,224,164,174,224,164,172,224,165,141,224 +,164,176,224,164,190,224,164,130,224,164,161,224,164,172,224,164,154,224,165,141 +,224,164,154,224,165,139,224,164,130,224,164,137,224,164,170,224,164,178,224,164 +,172,224,165,141,224,164,167,224,164,174,224,164,130,224,164,164,224,165,141,224 +,164,176,224,165,128,224,164,184,224,164,130,224,164,170,224,164,176,224,165,141 +,224,164,149,224,164,137,224,164,174,224,165,141,224,164,174,224,165,128,224,164 +,166,224,164,174,224,164,190,224,164,167,224,165,141,224,164,175,224,164,174,224 +,164,184,224,164,185,224,164,190,224,164,175,224,164,164,224,164,190,224,164,182 +,224,164,172,224,165,141,224,164,166,224,165,139,224,164,130,224,164,174,224,165 +,128,224,164,161,224,164,191,224,164,175,224,164,190,224,164,134,224,164,136,224 +,164,170,224,165,128,224,164,143,224,164,178,224,164,174,224,165,139,224,164,172 +,224,164,190,224,164,135,224,164,178,224,164,184,224,164,130,224,164,150,224,165 +,141,224,164,175,224,164,190,224,164,134,224,164,170,224,164,176,224,165,135,224 +,164,182,224,164,168,224,164,133,224,164,168,224,165,129,224,164,172,224,164,130 +,224,164,167,224,164,172,224,164,190,224,164,156,224,164,188,224,164,190,224,164 +,176,224,164,168,224,164,181,224,165,128,224,164,168,224,164,164,224,164,174,224 +,164,170,224,165,141,224,164,176,224,164,174,224,165,129,224,164,150,224,164,170 +,224,165,141,224,164,176,224,164,182,224,165,141,224,164,168,224,164,170,224,164 +,176,224,164,191,224,164,181,224,164,190,224,164,176,224,164,168,224,165,129,224 +,164,149,224,164,184,224,164,190,224,164,168,224,164,184,224,164,174,224,164,176 +,224,165,141,224,164,165,224,164,168,224,164,134,224,164,175,224,165,139,224,164 +,156,224,164,191,224,164,164,224,164,184,224,165,139,224,164,174,224,164,181,224 +,164,190,224,164,176,216,167,217,132,217,133,216,180,216,167,216,177,217,131,216 +,167,216,170,216,167,217,132,217,133,217,134,216,170,216,175,217,138,216,167,216 +,170,216,167,217,132,217,131,217,133,216,168,217,138,217,136,216,170,216,177,216 +,167,217,132,217,133,216,180,216,167,217,135,216,175,216,167,216,170,216,185,216 +,175,216,175,216,167,217,132,216,178,217,136,216,167,216,177,216,185,216,175,216 +,175,216,167,217,132,216,177,216,175,217,136,216,175,216,167,217,132,216,165,216 +,179,217,132,216,167,217,133,217,138,216,169,216,167,217,132,217,129,217,136,216 +,170,217,136,216,180,217,136,216,168,216,167,217,132,217,133,216,179,216,167,216 +,168,217,130,216,167,216,170,216,167,217,132,217,133,216,185,217,132,217,136,217 +,133,216,167,216,170,216,167,217,132,217,133,216,179,217,132,216,179,217,132,216 +,167,216,170,216,167,217,132,216,172,216,177,216,167,217,129,217,138,217,131,216 +,179,216,167,217,132,216,167,216,179,217,132,216,167,217,133,217,138,216,169,216 +,167,217,132,216,167,216,170,216,181,216,167,217,132,216,167,216,170,107,101,121 +,119,111,114,100,115,34,32,99,111,110,116,101,110,116,61,34,119,51,46,111,114, +103,47,49,57,57,57,47,120,104,116,109,108,34,62,60,97,32,116,97,114,103,101,116, +61,34,95,98,108,97,110,107,34,32,116,101,120,116,47,104,116,109,108,59,32,99,104 +,97,114,115,101,116,61,34,32,116,97,114,103,101,116,61,34,95,98,108,97,110,107, +34,62,60,116,97,98,108,101,32,99,101,108,108,112,97,100,100,105,110,103,61,34,97 +,117,116,111,99,111,109,112,108,101,116,101,61,34,111,102,102,34,32,116,101,120, +116,45,97,108,105,103,110,58,32,99,101,110,116,101,114,59,116,111,32,108,97,115, +116,32,118,101,114,115,105,111,110,32,98,121,32,98,97,99,107,103,114,111,117,110 +,100,45,99,111,108,111,114,58,32,35,34,32,104,114,101,102,61,34,104,116,116,112, +58,47,47,119,119,119,46,47,100,105,118,62,60,47,100,105,118,62,60,100,105,118,32 +,105,100,61,60,97,32,104,114,101,102,61,34,35,34,32,99,108,97,115,115,61,34,34, +62,60,105,109,103,32,115,114,99,61,34,104,116,116,112,58,47,47,99,114,105,112, +116,34,32,115,114,99,61,34,104,116,116,112,58,47,47,10,60,115,99,114,105,112,116 +,32,108,97,110,103,117,97,103,101,61,34,47,47,69,78,34,32,34,104,116,116,112,58, +47,47,119,119,119,46,119,101,110,99,111,100,101,85,82,73,67,111,109,112,111,110, +101,110,116,40,34,32,104,114,101,102,61,34,106,97,118,97,115,99,114,105,112,116, +58,60,100,105,118,32,99,108,97,115,115,61,34,99,111,110,116,101,110,116,100,111, +99,117,109,101,110,116,46,119,114,105,116,101,40,39,60,115,99,112,111,115,105, +116,105,111,110,58,32,97,98,115,111,108,117,116,101,59,115,99,114,105,112,116,32 +,115,114,99,61,34,104,116,116,112,58,47,47,32,115,116,121,108,101,61,34,109,97, +114,103,105,110,45,116,111,112,58,46,109,105,110,46,106,115,34,62,60,47,115,99, +114,105,112,116,62,10,60,47,100,105,118,62,10,60,100,105,118,32,99,108,97,115, +115,61,34,119,51,46,111,114,103,47,49,57,57,57,47,120,104,116,109,108,34,32,10, +13,10,60,47,98,111,100,121,62,13,10,60,47,104,116,109,108,62,100,105,115,116,105 +,110,99,116,105,111,110,32,98,101,116,119,101,101,110,47,34,32,116,97,114,103, +101,116,61,34,95,98,108,97,110,107,34,62,60,108,105,110,107,32,104,114,101,102, +61,34,104,116,116,112,58,47,47,101,110,99,111,100,105,110,103,61,34,117,116,102, +45,56,34,63,62,10,119,46,97,100,100,69,118,101,110,116,76,105,115,116,101,110, +101,114,63,97,99,116,105,111,110,61,34,104,116,116,112,58,47,47,119,119,119,46, +105,99,111,110,34,32,104,114,101,102,61,34,104,116,116,112,58,47,47,32,115,116, +121,108,101,61,34,98,97,99,107,103,114,111,117,110,100,58,116,121,112,101,61,34, +116,101,120,116,47,99,115,115,34,32,47,62,10,109,101,116,97,32,112,114,111,112, +101,114,116,121,61,34,111,103,58,116,60,105,110,112,117,116,32,116,121,112,101, +61,34,116,101,120,116,34,32,32,115,116,121,108,101,61,34,116,101,120,116,45,97, +108,105,103,110,58,116,104,101,32,100,101,118,101,108,111,112,109,101,110,116,32 +,111,102,32,116,121,108,101,115,104,101,101,116,34,32,116,121,112,101,61,34,116, +101,104,116,109,108,59,32,99,104,97,114,115,101,116,61,117,116,102,45,56,105,115 +,32,99,111,110,115,105,100,101,114,101,100,32,116,111,32,98,101,116,97,98,108, +101,32,119,105,100,116,104,61,34,49,48,48,37,34,32,73,110,32,97,100,100,105,116, +105,111,110,32,116,111,32,116,104,101,32,99,111,110,116,114,105,98,117,116,101, +100,32,116,111,32,116,104,101,32,100,105,102,102,101,114,101,110,99,101,115,32, +98,101,116,119,101,101,110,100,101,118,101,108,111,112,109,101,110,116,32,111, +102,32,116,104,101,32,73,116,32,105,115,32,105,109,112,111,114,116,97,110,116,32 +,116,111,32,60,47,115,99,114,105,112,116,62,10,10,60,115,99,114,105,112,116,32, +32,115,116,121,108,101,61,34,102,111,110,116,45,115,105,122,101,58,49,62,60,47, +115,112,97,110,62,60,115,112,97,110,32,105,100,61,103,98,76,105,98,114,97,114, +121,32,111,102,32,67,111,110,103,114,101,115,115,60,105,109,103,32,115,114,99,61 +,34,104,116,116,112,58,47,47,105,109,69,110,103,108,105,115,104,32,116,114,97, +110,115,108,97,116,105,111,110,65,99,97,100,101,109,121,32,111,102,32,83,99,105, +101,110,99,101,115,100,105,118,32,115,116,121,108,101,61,34,100,105,115,112,108, +97,121,58,99,111,110,115,116,114,117,99,116,105,111,110,32,111,102,32,116,104, +101,46,103,101,116,69,108,101,109,101,110,116,66,121,73,100,40,105,100,41,105, +110,32,99,111,110,106,117,110,99,116,105,111,110,32,119,105,116,104,69,108,101, +109,101,110,116,40,39,115,99,114,105,112,116,39,41,59,32,60,109,101,116,97,32, +112,114,111,112,101,114,116,121,61,34,111,103,58,208,145,209,138,208,187,208,179 +,208,176,209,128,209,129,208,186,208,184,10,32,116,121,112,101,61,34,116,101,120 +,116,34,32,110,97,109,101,61,34,62,80,114,105,118,97,99,121,32,80,111,108,105,99 +,121,60,47,97,62,97,100,109,105,110,105,115,116,101,114,101,100,32,98,121,32,116 +,104,101,101,110,97,98,108,101,83,105,110,103,108,101,82,101,113,117,101,115,116 +,115,116,121,108,101,61,38,113,117,111,116,59,109,97,114,103,105,110,58,60,47, +100,105,118,62,60,47,100,105,118,62,60,47,100,105,118,62,60,62,60,105,109,103,32 +,115,114,99,61,34,104,116,116,112,58,47,47,105,32,115,116,121,108,101,61,38,113, +117,111,116,59,102,108,111,97,116,58,114,101,102,101,114,114,101,100,32,116,111, +32,97,115,32,116,104,101,32,116,111,116,97,108,32,112,111,112,117,108,97,116,105 +,111,110,32,111,102,105,110,32,87,97,115,104,105,110,103,116,111,110,44,32,68,46 +,67,46,32,115,116,121,108,101,61,34,98,97,99,107,103,114,111,117,110,100,45,97, +109,111,110,103,32,111,116,104,101,114,32,116,104,105,110,103,115,44,111,114,103 +,97,110,105,122,97,116,105,111,110,32,111,102,32,116,104,101,112,97,114,116,105, +99,105,112,97,116,101,100,32,105,110,32,116,104,101,116,104,101,32,105,110,116, +114,111,100,117,99,116,105,111,110,32,111,102,105,100,101,110,116,105,102,105, +101,100,32,119,105,116,104,32,116,104,101,102,105,99,116,105,111,110,97,108,32, +99,104,97,114,97,99,116,101,114,32,79,120,102,111,114,100,32,85,110,105,118,101, +114,115,105,116,121,32,109,105,115,117,110,100,101,114,115,116,97,110,100,105, +110,103,32,111,102,84,104,101,114,101,32,97,114,101,44,32,104,111,119,101,118, +101,114,44,115,116,121,108,101,115,104,101,101,116,34,32,104,114,101,102,61,34, +47,67,111,108,117,109,98,105,97,32,85,110,105,118,101,114,115,105,116,121,101, +120,112,97,110,100,101,100,32,116,111,32,105,110,99,108,117,100,101,117,115,117, +97,108,108,121,32,114,101,102,101,114,114,101,100,32,116,111,105,110,100,105,99, +97,116,105,110,103,32,116,104,97,116,32,116,104,101,104,97,118,101,32,115,117, +103,103,101,115,116,101,100,32,116,104,97,116,97,102,102,105,108,105,97,116,101, +100,32,119,105,116,104,32,116,104,101,99,111,114,114,101,108,97,116,105,111,110, +32,98,101,116,119,101,101,110,110,117,109,98,101,114,32,111,102,32,100,105,102, +102,101,114,101,110,116,62,60,47,116,100,62,60,47,116,114,62,60,47,116,97,98,108 +,101,62,82,101,112,117,98,108,105,99,32,111,102,32,73,114,101,108,97,110,100,10, +60,47,115,99,114,105,112,116,62,10,60,115,99,114,105,112,116,32,117,110,100,101, +114,32,116,104,101,32,105,110,102,108,117,101,110,99,101,99,111,110,116,114,105, +98,117,116,105,111,110,32,116,111,32,116,104,101,79,102,102,105,99,105,97,108,32 +,119,101,98,115,105,116,101,32,111,102,104,101,97,100,113,117,97,114,116,101,114 +,115,32,111,102,32,116,104,101,99,101,110,116,101,114,101,100,32,97,114,111,117, +110,100,32,116,104,101,105,109,112,108,105,99,97,116,105,111,110,115,32,111,102, +32,116,104,101,104,97,118,101,32,98,101,101,110,32,100,101,118,101,108,111,112, +101,100,70,101,100,101,114,97,108,32,82,101,112,117,98,108,105,99,32,111,102,98, +101,99,97,109,101,32,105,110,99,114,101,97,115,105,110,103,108,121,99,111,110, +116,105,110,117,97,116,105,111,110,32,111,102,32,116,104,101,78,111,116,101,44, +32,104,111,119,101,118,101,114,44,32,116,104,97,116,115,105,109,105,108,97,114, +32,116,111,32,116,104,97,116,32,111,102,32,99,97,112,97,98,105,108,105,116,105, +101,115,32,111,102,32,116,104,101,97,99,99,111,114,100,97,110,99,101,32,119,105, +116,104,32,116,104,101,112,97,114,116,105,99,105,112,97,110,116,115,32,105,110, +32,116,104,101,102,117,114,116,104,101,114,32,100,101,118,101,108,111,112,109, +101,110,116,117,110,100,101,114,32,116,104,101,32,100,105,114,101,99,116,105,111 +,110,105,115,32,111,102,116,101,110,32,99,111,110,115,105,100,101,114,101,100, +104,105,115,32,121,111,117,110,103,101,114,32,98,114,111,116,104,101,114,60,47, +116,100,62,60,47,116,114,62,60,47,116,97,98,108,101,62,60,97,32,104,116,116,112, +45,101,113,117,105,118,61,34,88,45,85,65,45,112,104,121,115,105,99,97,108,32,112 +,114,111,112,101,114,116,105,101,115,111,102,32,66,114,105,116,105,115,104,32,67 +,111,108,117,109,98,105,97,104,97,115,32,98,101,101,110,32,99,114,105,116,105,99 +,105,122,101,100,40,119,105,116,104,32,116,104,101,32,101,120,99,101,112,116,105 +,111,110,113,117,101,115,116,105,111,110,115,32,97,98,111,117,116,32,116,104,101 +,112,97,115,115,105,110,103,32,116,104,114,111,117,103,104,32,116,104,101,48,34, +32,99,101,108,108,112,97,100,100,105,110,103,61,34,48,34,32,116,104,111,117,115, +97,110,100,115,32,111,102,32,112,101,111,112,108,101,114,101,100,105,114,101,99, +116,115,32,104,101,114,101,46,32,70,111,114,104,97,118,101,32,99,104,105,108,100 +,114,101,110,32,117,110,100,101,114,37,51,69,37,51,67,47,115,99,114,105,112,116, +37,51,69,34,41,41,59,60,97,32,104,114,101,102,61,34,104,116,116,112,58,47,47,119 +,119,119,46,60,108,105,62,60,97,32,104,114,101,102,61,34,104,116,116,112,58,47, +47,115,105,116,101,95,110,97,109,101,34,32,99,111,110,116,101,110,116,61,34,116, +101,120,116,45,100,101,99,111,114,97,116,105,111,110,58,110,111,110,101,115,116, +121,108,101,61,34,100,105,115,112,108,97,121,58,32,110,111,110,101,60,109,101, +116,97,32,104,116,116,112,45,101,113,117,105,118,61,34,88,45,110,101,119,32,68, +97,116,101,40,41,46,103,101,116,84,105,109,101,40,41,32,116,121,112,101,61,34, +105,109,97,103,101,47,120,45,105,99,111,110,34,60,47,115,112,97,110,62,60,115, +112,97,110,32,99,108,97,115,115,61,34,108,97,110,103,117,97,103,101,61,34,106,97 +,118,97,115,99,114,105,112,116,119,105,110,100,111,119,46,108,111,99,97,116,105, +111,110,46,104,114,101,102,60,97,32,104,114,101,102,61,34,106,97,118,97,115,99, +114,105,112,116,58,45,45,62,13,10,60,115,99,114,105,112,116,32,116,121,112,101, +61,34,116,60,97,32,104,114,101,102,61,39,104,116,116,112,58,47,47,119,119,119,46 +,104,111,114,116,99,117,116,32,105,99,111,110,34,32,104,114,101,102,61,34,60,47, +100,105,118,62,13,10,60,100,105,118,32,99,108,97,115,115,61,34,60,115,99,114,105 +,112,116,32,115,114,99,61,34,104,116,116,112,58,47,47,34,32,114,101,108,61,34, +115,116,121,108,101,115,104,101,101,116,34,32,116,60,47,100,105,118,62,10,60,115 +,99,114,105,112,116,32,116,121,112,101,61,47,97,62,32,60,97,32,104,114,101,102, +61,34,104,116,116,112,58,47,47,32,97,108,108,111,119,84,114,97,110,115,112,97, +114,101,110,99,121,61,34,88,45,85,65,45,67,111,109,112,97,116,105,98,108,101,34, +32,99,111,110,114,101,108,97,116,105,111,110,115,104,105,112,32,98,101,116,119, +101,101,110,10,60,47,115,99,114,105,112,116,62,13,10,60,115,99,114,105,112,116, +32,60,47,97,62,60,47,108,105,62,60,47,117,108,62,60,47,100,105,118,62,97,115,115 +,111,99,105,97,116,101,100,32,119,105,116,104,32,116,104,101,32,112,114,111,103, +114,97,109,109,105,110,103,32,108,97,110,103,117,97,103,101,60,47,97,62,60,97,32 +,104,114,101,102,61,34,104,116,116,112,58,47,47,60,47,97,62,60,47,108,105,62,60, +108,105,32,99,108,97,115,115,61,34,102,111,114,109,32,97,99,116,105,111,110,61, +34,104,116,116,112,58,47,47,60,100,105,118,32,115,116,121,108,101,61,34,100,105, +115,112,108,97,121,58,116,121,112,101,61,34,116,101,120,116,34,32,110,97,109,101 +,61,34,113,34,60,116,97,98,108,101,32,119,105,100,116,104,61,34,49,48,48,37,34, +32,98,97,99,107,103,114,111,117,110,100,45,112,111,115,105,116,105,111,110,58,34 +,32,98,111,114,100,101,114,61,34,48,34,32,119,105,100,116,104,61,34,114,101,108, +61,34,115,104,111,114,116,99,117,116,32,105,99,111,110,34,32,104,54,62,60,117, +108,62,60,108,105,62,60,97,32,104,114,101,102,61,34,32,32,60,109,101,116,97,32, +104,116,116,112,45,101,113,117,105,118,61,34,99,115,115,34,32,109,101,100,105,97 +,61,34,115,99,114,101,101,110,34,32,114,101,115,112,111,110,115,105,98,108,101, +32,102,111,114,32,116,104,101,32,34,32,116,121,112,101,61,34,97,112,112,108,105, +99,97,116,105,111,110,47,34,32,115,116,121,108,101,61,34,98,97,99,107,103,114, +111,117,110,100,45,104,116,109,108,59,32,99,104,97,114,115,101,116,61,117,116, +102,45,56,34,32,97,108,108,111,119,116,114,97,110,115,112,97,114,101,110,99,121, +61,34,115,116,121,108,101,115,104,101,101,116,34,32,116,121,112,101,61,34,116, +101,13,10,60,109,101,116,97,32,104,116,116,112,45,101,113,117,105,118,61,34,62, +60,47,115,112,97,110,62,60,115,112,97,110,32,99,108,97,115,115,61,34,48,34,32,99 +,101,108,108,115,112,97,99,105,110,103,61,34,48,34,62,59,10,60,47,115,99,114,105 +,112,116,62,10,60,115,99,114,105,112,116,32,115,111,109,101,116,105,109,101,115, +32,99,97,108,108,101,100,32,116,104,101,100,111,101,115,32,110,111,116,32,110, +101,99,101,115,115,97,114,105,108,121,70,111,114,32,109,111,114,101,32,105,110, +102,111,114,109,97,116,105,111,110,97,116,32,116,104,101,32,98,101,103,105,110, +110,105,110,103,32,111,102,32,60,33,68,79,67,84,89,80,69,32,104,116,109,108,62, +60,104,116,109,108,112,97,114,116,105,99,117,108,97,114,108,121,32,105,110,32, +116,104,101,32,116,121,112,101,61,34,104,105,100,100,101,110,34,32,110,97,109, +101,61,34,106,97,118,97,115,99,114,105,112,116,58,118,111,105,100,40,48,41,59,34 +,101,102,102,101,99,116,105,118,101,110,101,115,115,32,111,102,32,116,104,101,32 +,97,117,116,111,99,111,109,112,108,101,116,101,61,34,111,102,102,34,32,103,101, +110,101,114,97,108,108,121,32,99,111,110,115,105,100,101,114,101,100,62,60,105, +110,112,117,116,32,116,121,112,101,61,34,116,101,120,116,34,32,34,62,60,47,115, +99,114,105,112,116,62,13,10,60,115,99,114,105,112,116,116,104,114,111,117,103, +104,111,117,116,32,116,104,101,32,119,111,114,108,100,99,111,109,109,111,110,32, +109,105,115,99,111,110,99,101,112,116,105,111,110,97,115,115,111,99,105,97,116, +105,111,110,32,119,105,116,104,32,116,104,101,60,47,100,105,118,62,10,60,47,100, +105,118,62,10,60,100,105,118,32,99,100,117,114,105,110,103,32,104,105,115,32,108 +,105,102,101,116,105,109,101,44,99,111,114,114,101,115,112,111,110,100,105,110, +103,32,116,111,32,116,104,101,116,121,112,101,61,34,105,109,97,103,101,47,120,45 +,105,99,111,110,34,32,97,110,32,105,110,99,114,101,97,115,105,110,103,32,110,117 +,109,98,101,114,100,105,112,108,111,109,97,116,105,99,32,114,101,108,97,116,105, +111,110,115,97,114,101,32,111,102,116,101,110,32,99,111,110,115,105,100,101,114, +101,100,109,101,116,97,32,99,104,97,114,115,101,116,61,34,117,116,102,45,56,34, +32,60,105,110,112,117,116,32,116,121,112,101,61,34,116,101,120,116,34,32,101,120 +,97,109,112,108,101,115,32,105,110,99,108,117,100,101,32,116,104,101,34,62,60, +105,109,103,32,115,114,99,61,34,104,116,116,112,58,47,47,105,112,97,114,116,105, +99,105,112,97,116,105,111,110,32,105,110,32,116,104,101,116,104,101,32,101,115, +116,97,98,108,105,115,104,109,101,110,116,32,111,102,10,60,47,100,105,118,62,10, +60,100,105,118,32,99,108,97,115,115,61,34,38,97,109,112,59,110,98,115,112,59,38, +97,109,112,59,110,98,115,112,59,116,111,32,100,101,116,101,114,109,105,110,101, +32,119,104,101,116,104,101,114,113,117,105,116,101,32,100,105,102,102,101,114, +101,110,116,32,102,114,111,109,109,97,114,107,101,100,32,116,104,101,32,98,101, +103,105,110,110,105,110,103,100,105,115,116,97,110,99,101,32,98,101,116,119,101, +101,110,32,116,104,101,99,111,110,116,114,105,98,117,116,105,111,110,115,32,116, +111,32,116,104,101,99,111,110,102,108,105,99,116,32,98,101,116,119,101,101,110, +32,116,104,101,119,105,100,101,108,121,32,99,111,110,115,105,100,101,114,101,100 +,32,116,111,119,97,115,32,111,110,101,32,111,102,32,116,104,101,32,102,105,114, +115,116,119,105,116,104,32,118,97,114,121,105,110,103,32,100,101,103,114,101,101 +,115,104,97,118,101,32,115,112,101,99,117,108,97,116,101,100,32,116,104,97,116, +40,100,111,99,117,109,101,110,116,46,103,101,116,69,108,101,109,101,110,116,112, +97,114,116,105,99,105,112,97,116,105,110,103,32,105,110,32,116,104,101,111,114, +105,103,105,110,97,108,108,121,32,100,101,118,101,108,111,112,101,100,101,116,97 +,32,99,104,97,114,115,101,116,61,34,117,116,102,45,56,34,62,32,116,121,112,101, +61,34,116,101,120,116,47,99,115,115,34,32,47,62,10,105,110,116,101,114,99,104,97 +,110,103,101,97,98,108,121,32,119,105,116,104,109,111,114,101,32,99,108,111,115, +101,108,121,32,114,101,108,97,116,101,100,115,111,99,105,97,108,32,97,110,100,32 +,112,111,108,105,116,105,99,97,108,116,104,97,116,32,119,111,117,108,100,32,111, +116,104,101,114,119,105,115,101,112,101,114,112,101,110,100,105,99,117,108,97, +114,32,116,111,32,116,104,101,115,116,121,108,101,32,116,121,112,101,61,34,116, +101,120,116,47,99,115,115,116,121,112,101,61,34,115,117,98,109,105,116,34,32,110 +,97,109,101,61,34,102,97,109,105,108,105,101,115,32,114,101,115,105,100,105,110, +103,32,105,110,100,101,118,101,108,111,112,105,110,103,32,99,111,117,110,116,114 +,105,101,115,99,111,109,112,117,116,101,114,32,112,114,111,103,114,97,109,109, +105,110,103,101,99,111,110,111,109,105,99,32,100,101,118,101,108,111,112,109,101 +,110,116,100,101,116,101,114,109,105,110,97,116,105,111,110,32,111,102,32,116, +104,101,102,111,114,32,109,111,114,101,32,105,110,102,111,114,109,97,116,105,111 +,110,111,110,32,115,101,118,101,114,97,108,32,111,99,99,97,115,105,111,110,115, +112,111,114,116,117,103,117,195,170,115,32,40,69,117,114,111,112,101,117,41,208, +163,208,186,209,128,208,176,209,151,208,189,209,129,209,140,208,186,208,176,209, +131,208,186,209,128,208,176,209,151,208,189,209,129,209,140,208,186,208,176,208, +160,208,190,209,129,209,129,208,184,208,185,209,129,208,186,208,190,208,185,208, +188,208,176,209,130,208,181,209,128,208,184,208,176,208,187,208,190,208,178,208, +184,208,189,209,132,208,190,209,128,208,188,208,176,209,134,208,184,208,184,209, +131,208,191,209,128,208,176,208,178,208,187,208,181,208,189,208,184,209,143,208, +189,208,181,208,190,208,177,209,133,208,190,208,180,208,184,208,188,208,190,208, +184,208,189,209,132,208,190,209,128,208,188,208,176,209,134,208,184,209,143,208, +152,208,189,209,132,208,190,209,128,208,188,208,176,209,134,208,184,209,143,208, +160,208,181,209,129,208,191,209,131,208,177,208,187,208,184,208,186,208,184,208, +186,208,190,208,187,208,184,209,135,208,181,209,129,209,130,208,178,208,190,208, +184,208,189,209,132,208,190,209,128,208,188,208,176,209,134,208,184,209,142,209, +130,208,181,209,128,209,128,208,184,209,130,208,190,209,128,208,184,208,184,208, +180,208,190,209,129,209,130,208,176,209,130,208,190,209,135,208,189,208,190,216, +167,217,132,217,133,216,170,217,136,216,167,216,172,216,175,217,136,217,134,216, +167,217,132,216,167,216,180,216,170,216,177,216,167,217,131,216,167,216,170,216, +167,217,132,216,167,217,130,216,170,216,177,216,167,216,173,216,167,216,170,104, +116,109,108,59,32,99,104,97,114,115,101,116,61,85,84,70,45,56,34,32,115,101,116, +84,105,109,101,111,117,116,40,102,117,110,99,116,105,111,110,40,41,100,105,115, +112,108,97,121,58,105,110,108,105,110,101,45,98,108,111,99,107,59,60,105,110,112 +,117,116,32,116,121,112,101,61,34,115,117,98,109,105,116,34,32,116,121,112,101, +32,61,32,39,116,101,120,116,47,106,97,118,97,115,99,114,105,60,105,109,103,32, +115,114,99,61,34,104,116,116,112,58,47,47,119,119,119,46,34,32,34,104,116,116, +112,58,47,47,119,119,119,46,119,51,46,111,114,103,47,115,104,111,114,116,99,117, +116,32,105,99,111,110,34,32,104,114,101,102,61,34,34,32,97,117,116,111,99,111, +109,112,108,101,116,101,61,34,111,102,102,34,32,60,47,97,62,60,47,100,105,118,62 +,60,100,105,118,32,99,108,97,115,115,61,60,47,97,62,60,47,108,105,62,10,60,108, +105,32,99,108,97,115,115,61,34,99,115,115,34,32,116,121,112,101,61,34,116,101, +120,116,47,99,115,115,34,32,60,102,111,114,109,32,97,99,116,105,111,110,61,34, +104,116,116,112,58,47,47,120,116,47,99,115,115,34,32,104,114,101,102,61,34,104, +116,116,112,58,47,47,108,105,110,107,32,114,101,108,61,34,97,108,116,101,114,110 +,97,116,101,34,32,13,10,60,115,99,114,105,112,116,32,116,121,112,101,61,34,116, +101,120,116,47,32,111,110,99,108,105,99,107,61,34,106,97,118,97,115,99,114,105, +112,116,58,40,110,101,119,32,68,97,116,101,41,46,103,101,116,84,105,109,101,40, +41,125,104,101,105,103,104,116,61,34,49,34,32,119,105,100,116,104,61,34,49,34,32 +,80,101,111,112,108,101,39,115,32,82,101,112,117,98,108,105,99,32,111,102,32,32, +60,97,32,104,114,101,102,61,34,104,116,116,112,58,47,47,119,119,119,46,116,101, +120,116,45,100,101,99,111,114,97,116,105,111,110,58,117,110,100,101,114,116,104, +101,32,98,101,103,105,110,110,105,110,103,32,111,102,32,116,104,101,32,60,47,100 +,105,118,62,10,60,47,100,105,118,62,10,60,47,100,105,118,62,10,101,115,116,97,98 +,108,105,115,104,109,101,110,116,32,111,102,32,116,104,101,32,60,47,100,105,118, +62,60,47,100,105,118,62,60,47,100,105,118,62,60,47,100,35,118,105,101,119,112, +111,114,116,123,109,105,110,45,104,101,105,103,104,116,58,10,60,115,99,114,105, +112,116,32,115,114,99,61,34,104,116,116,112,58,47,47,111,112,116,105,111,110,62, +60,111,112,116,105,111,110,32,118,97,108,117,101,61,111,102,116,101,110,32,114, +101,102,101,114,114,101,100,32,116,111,32,97,115,32,47,111,112,116,105,111,110, +62,10,60,111,112,116,105,111,110,32,118,97,108,117,60,33,68,79,67,84,89,80,69,32 +,104,116,109,108,62,10,60,33,45,45,91,73,110,116,101,114,110,97,116,105,111,110, +97,108,32,65,105,114,112,111,114,116,62,10,60,97,32,104,114,101,102,61,34,104, +116,116,112,58,47,47,119,119,119,60,47,97,62,60,97,32,104,114,101,102,61,34,104, +116,116,112,58,47,47,119,224,184,160,224,184,178,224,184,169,224,184,178,224,185 +,132,224,184,151,224,184,162,225,131,165,225,131,144,225,131,160,225,131,151,225 +,131,163,225,131,154,225,131,152,230,173,163,233,171,148,228,184,173,230,150,135 +,32,40,231,185,129,233,171,148,41,224,164,168,224,164,191,224,164,176,224,165, +141,224,164,166,224,165,135,224,164,182,224,164,161,224,164,190,224,164,137,224, +164,168,224,164,178,224,165,139,224,164,161,224,164,149,224,165,141,224,164,183, +224,165,135,224,164,164,224,165,141,224,164,176,224,164,156,224,164,190,224,164, +168,224,164,149,224,164,190,224,164,176,224,165,128,224,164,184,224,164,130,224, +164,172,224,164,130,224,164,167,224,164,191,224,164,164,224,164,184,224,165,141, +224,164,165,224,164,190,224,164,170,224,164,168,224,164,190,224,164,184,224,165, +141,224,164,181,224,165,128,224,164,149,224,164,190,224,164,176,224,164,184,224, +164,130,224,164,184,224,165,141,224,164,149,224,164,176,224,164,163,224,164,184, +224,164,190,224,164,174,224,164,151,224,165,141,224,164,176,224,165,128,224,164, +154,224,164,191,224,164,159,224,165,141,224,164,160,224,165,139,224,164,130,224, +164,181,224,164,191,224,164,156,224,165,141,224,164,158,224,164,190,224,164,168, +224,164,133,224,164,174,224,165,135,224,164,176,224,164,191,224,164,149,224,164, +190,224,164,181,224,164,191,224,164,173,224,164,191,224,164,168,224,165,141,224, +164,168,224,164,151,224,164,190,224,164,161,224,164,191,224,164,175,224,164,190, +224,164,129,224,164,149,224,165,141,224,164,175,224,165,139,224,164,130,224,164, +149,224,164,191,224,164,184,224,165,129,224,164,176,224,164,149,224,165,141,224, +164,183,224,164,190,224,164,170,224,164,185,224,165,129,224,164,129,224,164,154, +224,164,164,224,165,128,224,164,170,224,165,141,224,164,176,224,164,172,224,164, +130,224,164,167,224,164,168,224,164,159,224,164,191,224,164,170,224,165,141,224, +164,170,224,164,163,224,165,128,224,164,149,224,165,141,224,164,176,224,164,191, +224,164,149,224,165,135,224,164,159,224,164,170,224,165,141,224,164,176,224,164, +190,224,164,176,224,164,130,224,164,173,224,164,170,224,165,141,224,164,176,224, +164,190,224,164,170,224,165,141,224,164,164,224,164,174,224,164,190,224,164,178, +224,164,191,224,164,149,224,165,139,224,164,130,224,164,176,224,164,171,224,164, +188,224,165,141,224,164,164,224,164,190,224,164,176,224,164,168,224,164,191,224, +164,176,224,165,141,224,164,174,224,164,190,224,164,163,224,164,178,224,164,191, +224,164,174,224,164,191,224,164,159,224,165,135,224,164,161,100,101,115,99,114, +105,112,116,105,111,110,34,32,99,111,110,116,101,110,116,61,34,100,111,99,117, +109,101,110,116,46,108,111,99,97,116,105,111,110,46,112,114,111,116,46,103,101, +116,69,108,101,109,101,110,116,115,66,121,84,97,103,78,97,109,101,40,60,33,68,79 +,67,84,89,80,69,32,104,116,109,108,62,10,60,104,116,109,108,32,60,109,101,116,97 +,32,99,104,97,114,115,101,116,61,34,117,116,102,45,56,34,62,58,117,114,108,34,32 +,99,111,110,116,101,110,116,61,34,104,116,116,112,58,47,47,46,99,115,115,34,32, +114,101,108,61,34,115,116,121,108,101,115,104,101,101,116,34,115,116,121,108,101 +,32,116,121,112,101,61,34,116,101,120,116,47,99,115,115,34,62,116,121,112,101,61 +,34,116,101,120,116,47,99,115,115,34,32,104,114,101,102,61,34,119,51,46,111,114, +103,47,49,57,57,57,47,120,104,116,109,108,34,32,120,109,108,116,121,112,101,61, +34,116,101,120,116,47,106,97,118,97,115,99,114,105,112,116,34,32,109,101,116,104 +,111,100,61,34,103,101,116,34,32,97,99,116,105,111,110,61,34,108,105,110,107,32, +114,101,108,61,34,115,116,121,108,101,115,104,101,101,116,34,32,32,61,32,100,111 +,99,117,109,101,110,116,46,103,101,116,69,108,101,109,101,110,116,116,121,112, +101,61,34,105,109,97,103,101,47,120,45,105,99,111,110,34,32,47,62,99,101,108,108 +,112,97,100,100,105,110,103,61,34,48,34,32,99,101,108,108,115,112,46,99,115,115, +34,32,116,121,112,101,61,34,116,101,120,116,47,99,115,115,34,32,60,47,97,62,60, +47,108,105,62,60,108,105,62,60,97,32,104,114,101,102,61,34,34,32,119,105,100,116 +,104,61,34,49,34,32,104,101,105,103,104,116,61,34,49,34,34,62,60,97,32,104,114, +101,102,61,34,104,116,116,112,58,47,47,119,119,119,46,115,116,121,108,101,61,34, +100,105,115,112,108,97,121,58,110,111,110,101,59,34,62,97,108,116,101,114,110,97 +,116,101,34,32,116,121,112,101,61,34,97,112,112,108,105,45,47,47,87,51,67,47,47, +68,84,68,32,88,72,84,77,76,32,49,46,48,32,101,108,108,115,112,97,99,105,110,103, +61,34,48,34,32,99,101,108,108,112,97,100,32,116,121,112,101,61,34,104,105,100, +100,101,110,34,32,118,97,108,117,101,61,34,47,97,62,38,110,98,115,112,59,60,115, +112,97,110,32,114,111,108,101,61,34,115,10,60,105,110,112,117,116,32,116,121,112 +,101,61,34,104,105,100,100,101,110,34,32,108,97,110,103,117,97,103,101,61,34,74, +97,118,97,83,99,114,105,112,116,34,32,32,100,111,99,117,109,101,110,116,46,103, +101,116,69,108,101,109,101,110,116,115,66,103,61,34,48,34,32,99,101,108,108,115, +112,97,99,105,110,103,61,34,48,34,32,121,112,101,61,34,116,101,120,116,47,99,115 +,115,34,32,109,101,100,105,97,61,34,116,121,112,101,61,39,116,101,120,116,47,106 +,97,118,97,115,99,114,105,112,116,39,119,105,116,104,32,116,104,101,32,101,120, +99,101,112,116,105,111,110,32,111,102,32,121,112,101,61,34,116,101,120,116,47,99 +,115,115,34,32,114,101,108,61,34,115,116,32,104,101,105,103,104,116,61,34,49,34, +32,119,105,100,116,104,61,34,49,34,32,61,39,43,101,110,99,111,100,101,85,82,73, +67,111,109,112,111,110,101,110,116,40,60,108,105,110,107,32,114,101,108,61,34,97 +,108,116,101,114,110,97,116,101,34,32,10,98,111,100,121,44,32,116,114,44,32,105, +110,112,117,116,44,32,116,101,120,116,109,101,116,97,32,110,97,109,101,61,34,114 +,111,98,111,116,115,34,32,99,111,110,109,101,116,104,111,100,61,34,112,111,115, +116,34,32,97,99,116,105,111,110,61,34,62,10,60,97,32,104,114,101,102,61,34,104, +116,116,112,58,47,47,119,119,119,46,99,115,115,34,32,114,101,108,61,34,115,116, +121,108,101,115,104,101,101,116,34,32,60,47,100,105,118,62,60,47,100,105,118,62, +60,100,105,118,32,99,108,97,115,115,108,97,110,103,117,97,103,101,61,34,106,97, +118,97,115,99,114,105,112,116,34,62,97,114,105,97,45,104,105,100,100,101,110,61, +34,116,114,117,101,34,62,194,183,60,114,105,112,116,34,32,116,121,112,101,61,34, +116,101,120,116,47,106,97,118,97,115,108,61,48,59,125,41,40,41,59,10,40,102,117, +110,99,116,105,111,110,40,41,123,98,97,99,107,103,114,111,117,110,100,45,105,109 +,97,103,101,58,32,117,114,108,40,47,97,62,60,47,108,105,62,60,108,105,62,60,97, +32,104,114,101,102,61,34,104,9,9,60,108,105,62,60,97,32,104,114,101,102,61,34, +104,116,116,112,58,47,47,97,116,111,114,34,32,97,114,105,97,45,104,105,100,100, +101,110,61,34,116,114,117,62,32,60,97,32,104,114,101,102,61,34,104,116,116,112, +58,47,47,119,119,119,46,108,97,110,103,117,97,103,101,61,34,106,97,118,97,115,99 +,114,105,112,116,34,32,47,111,112,116,105,111,110,62,10,60,111,112,116,105,111, +110,32,118,97,108,117,101,47,100,105,118,62,60,47,100,105,118,62,60,100,105,118, +32,99,108,97,115,115,61,114,97,116,111,114,34,32,97,114,105,97,45,104,105,100, +100,101,110,61,34,116,114,101,61,40,110,101,119,32,68,97,116,101,41,46,103,101, +116,84,105,109,101,40,41,112,111,114,116,117,103,117,195,170,115,32,40,100,111, +32,66,114,97,115,105,108,41,208,190,209,128,208,179,208,176,208,189,208,184,208, +183,208,176,209,134,208,184,208,184,208,178,208,190,208,183,208,188,208,190,208, +182,208,189,208,190,209,129,209,130,209,140,208,190,208,177,209,128,208,176,208, +183,208,190,208,178,208,176,208,189,208,184,209,143,209,128,208,181,208,179,208, +184,209,129,209,130,209,128,208,176,209,134,208,184,208,184,208,178,208,190,208, +183,208,188,208,190,208,182,208,189,208,190,209,129,209,130,208,184,208,190,208, +177,209,143,208,183,208,176,209,130,208,181,208,187,209,140,208,189,208,176,60, +33,68,79,67,84,89,80,69,32,104,116,109,108,32,80,85,66,76,73,67,32,34,110,116,45 +,84,121,112,101,34,32,99,111,110,116,101,110,116,61,34,116,101,120,116,47,60,109 +,101,116,97,32,104,116,116,112,45,101,113,117,105,118,61,34,67,111,110,116,101, +114,97,110,115,105,116,105,111,110,97,108,47,47,69,78,34,32,34,104,116,116,112, +58,60,104,116,109,108,32,120,109,108,110,115,61,34,104,116,116,112,58,47,47,119, +119,119,45,47,47,87,51,67,47,47,68,84,68,32,88,72,84,77,76,32,49,46,48,32,84,68, +84,68,47,120,104,116,109,108,49,45,116,114,97,110,115,105,116,105,111,110,97,108 +,47,47,119,119,119,46,119,51,46,111,114,103,47,84,82,47,120,104,116,109,108,49, +47,112,101,32,61,32,39,116,101,120,116,47,106,97,118,97,115,99,114,105,112,116, +39,59,60,109,101,116,97,32,110,97,109,101,61,34,100,101,115,99,114,105,112,116, +105,111,110,112,97,114,101,110,116,78,111,100,101,46,105,110,115,101,114,116,66, +101,102,111,114,101,60,105,110,112,117,116,32,116,121,112,101,61,34,104,105,100, +100,101,110,34,32,110,97,106,115,34,32,116,121,112,101,61,34,116,101,120,116,47, +106,97,118,97,115,99,114,105,40,100,111,99,117,109,101,110,116,41,46,114,101,97, +100,121,40,102,117,110,99,116,105,115,99,114,105,112,116,32,116,121,112,101,61, +34,116,101,120,116,47,106,97,118,97,115,105,109,97,103,101,34,32,99,111,110,116, +101,110,116,61,34,104,116,116,112,58,47,47,85,65,45,67,111,109,112,97,116,105,98 +,108,101,34,32,99,111,110,116,101,110,116,61,116,109,108,59,32,99,104,97,114,115 +,101,116,61,117,116,102,45,56,34,32,47,62,10,108,105,110,107,32,114,101,108,61, +34,115,104,111,114,116,99,117,116,32,105,99,111,110,60,108,105,110,107,32,114, +101,108,61,34,115,116,121,108,101,115,104,101,101,116,34,32,60,47,115,99,114,105 +,112,116,62,10,60,115,99,114,105,112,116,32,116,121,112,101,61,61,32,100,111,99, +117,109,101,110,116,46,99,114,101,97,116,101,69,108,101,109,101,110,60,97,32,116 +,97,114,103,101,116,61,34,95,98,108,97,110,107,34,32,104,114,101,102,61,32,100, +111,99,117,109,101,110,116,46,103,101,116,69,108,101,109,101,110,116,115,66,105, +110,112,117,116,32,116,121,112,101,61,34,116,101,120,116,34,32,110,97,109,101,61 +,97,46,116,121,112,101,32,61,32,39,116,101,120,116,47,106,97,118,97,115,99,114, +105,110,112,117,116,32,116,121,112,101,61,34,104,105,100,100,101,110,34,32,110, +97,109,101,104,116,109,108,59,32,99,104,97,114,115,101,116,61,117,116,102,45,56, +34,32,47,62,100,116,100,34,62,10,60,104,116,109,108,32,120,109,108,110,115,61,34 +,104,116,116,112,45,47,47,87,51,67,47,47,68,84,68,32,72,84,77,76,32,52,46,48,49, +32,84,101,110,116,115,66,121,84,97,103,78,97,109,101,40,39,115,99,114,105,112, +116,39,41,105,110,112,117,116,32,116,121,112,101,61,34,104,105,100,100,101,110, +34,32,110,97,109,60,115,99,114,105,112,116,32,116,121,112,101,61,34,116,101,120, +116,47,106,97,118,97,115,34,32,115,116,121,108,101,61,34,100,105,115,112,108,97, +121,58,110,111,110,101,59,34,62,100,111,99,117,109,101,110,116,46,103,101,116,69 +,108,101,109,101,110,116,66,121,73,100,40,61,100,111,99,117,109,101,110,116,46, +99,114,101,97,116,101,69,108,101,109,101,110,116,40,39,32,116,121,112,101,61,39, +116,101,120,116,47,106,97,118,97,115,99,114,105,112,116,39,105,110,112,117,116, +32,116,121,112,101,61,34,116,101,120,116,34,32,110,97,109,101,61,34,100,46,103, +101,116,69,108,101,109,101,110,116,115,66,121,84,97,103,78,97,109,101,40,115,110 +,105,99,97,108,34,32,104,114,101,102,61,34,104,116,116,112,58,47,47,119,119,119, +46,67,47,47,68,84,68,32,72,84,77,76,32,52,46,48,49,32,84,114,97,110,115,105,116, +60,115,116,121,108,101,32,116,121,112,101,61,34,116,101,120,116,47,99,115,115,34 +,62,10,10,60,115,116,121,108,101,32,116,121,112,101,61,34,116,101,120,116,47,99, +115,115,34,62,105,111,110,97,108,46,100,116,100,34,62,10,60,104,116,109,108,32, +120,109,108,110,115,61,104,116,116,112,45,101,113,117,105,118,61,34,67,111,110, +116,101,110,116,45,84,121,112,101,100,105,110,103,61,34,48,34,32,99,101,108,108, +115,112,97,99,105,110,103,61,34,48,34,104,116,109,108,59,32,99,104,97,114,115, +101,116,61,117,116,102,45,56,34,32,47,62,10,32,115,116,121,108,101,61,34,100,105 +,115,112,108,97,121,58,110,111,110,101,59,34,62,60,60,108,105,62,60,97,32,104, +114,101,102,61,34,104,116,116,112,58,47,47,119,119,119,46,32,116,121,112,101,61, +39,116,101,120,116,47,106,97,118,97,115,99,114,105,112,116,39,62,208,180,208,181 +,209,143,209,130,208,181,208,187,209,140,208,189,208,190,209,129,209,130,208,184 +,209,129,208,190,208,190,209,130,208,178,208,181,209,130,209,129,209,130,208,178 +,208,184,208,184,208,191,209,128,208,190,208,184,208,183,208,178,208,190,208,180 +,209,129,209,130,208,178,208,176,208,177,208,181,208,183,208,190,208,191,208,176 +,209,129,208,189,208,190,209,129,209,130,208,184,224,164,170,224,165,129,224,164 +,184,224,165,141,224,164,164,224,164,191,224,164,149,224,164,190,224,164,149,224 +,164,190,224,164,130,224,164,151,224,165,141,224,164,176,224,165,135,224,164,184 +,224,164,137,224,164,168,224,165,141,224,164,185,224,165,139,224,164,130,224,164 +,168,224,165,135,224,164,181,224,164,191,224,164,167,224,164,190,224,164,168,224 +,164,184,224,164,173,224,164,190,224,164,171,224,164,191,224,164,149,224,165,141 +,224,164,184,224,164,191,224,164,130,224,164,151,224,164,184,224,165,129,224,164 +,176,224,164,149,224,165,141,224,164,183,224,164,191,224,164,164,224,164,149,224 +,165,137,224,164,170,224,165,128,224,164,176,224,164,190,224,164,135,224,164,159 +,224,164,181,224,164,191,224,164,156,224,165,141,224,164,158,224,164,190,224,164 +,170,224,164,168,224,164,149,224,164,190,224,164,176,224,165,141,224,164,176,224 +,164,181,224,164,190,224,164,136,224,164,184,224,164,149,224,165,141,224,164,176 +,224,164,191,224,164,175,224,164,164,224,164,190 +}; diff --git a/packages/runtime/vendor/brotli/c/common/platform.c b/packages/runtime/vendor/brotli/c/common/platform.c new file mode 100644 index 000000000..1e047ced7 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/common/platform.c @@ -0,0 +1,19 @@ +/* Copyright 2016 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +#include "platform.h" + +/* Default brotli_alloc_func */ +void* BrotliDefaultAllocFunc(void* opaque, size_t size) { + BROTLI_UNUSED(opaque); + return malloc(size); +} + +/* Default brotli_free_func */ +void BrotliDefaultFreeFunc(void* opaque, void* address) { + BROTLI_UNUSED(opaque); + free(address); +} diff --git a/packages/runtime/vendor/brotli/c/common/platform.h b/packages/runtime/vendor/brotli/c/common/platform.h new file mode 100644 index 000000000..b59f9b809 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/common/platform.h @@ -0,0 +1,680 @@ +/* Copyright 2016 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* Macros for compiler / platform specific features and build options. + + Build options are: + * BROTLI_BUILD_32_BIT disables 64-bit optimizations + * BROTLI_BUILD_64_BIT forces to use 64-bit optimizations + * BROTLI_BUILD_BIG_ENDIAN forces to use big-endian optimizations + * BROTLI_BUILD_ENDIAN_NEUTRAL disables endian-aware optimizations + * BROTLI_BUILD_LITTLE_ENDIAN forces to use little-endian optimizations + * BROTLI_BUILD_NO_RBIT disables "rbit" optimization for ARM CPUs + * BROTLI_BUILD_NO_UNALIGNED_READ_FAST forces off the fast-unaligned-read + optimizations (mainly for testing purposes) + * BROTLI_DEBUG dumps file name and line number when decoder detects stream + or memory error + * BROTLI_ENABLE_LOG enables asserts and dumps various state information + * BROTLI_ENABLE_DUMP overrides default "dump" behaviour +*/ + +#ifndef BROTLI_COMMON_PLATFORM_H_ +#define BROTLI_COMMON_PLATFORM_H_ + +#include /* IWYU pragma: export memcmp, memcpy, memset */ +#include /* IWYU pragma: export exit, free, malloc */ +#include /* should include endian.h for us */ + +#include /* IWYU pragma: export */ +#include /* IWYU pragma: export */ + +#if BROTLI_MSVC_VERSION_CHECK(18, 0, 0) +#include +#endif + +#if defined(BROTLI_ENABLE_LOG) || defined(BROTLI_DEBUG) +#include +#include +#endif + +/* The following macros were borrowed from https://github.com/nemequ/hedley + * with permission of original author - Evan Nemerson */ + +/* >>> >>> >>> hedley macros */ + +/* Define "BROTLI_PREDICT_TRUE" and "BROTLI_PREDICT_FALSE" macros for capable + compilers. + +To apply compiler hint, enclose the branching condition into macros, like this: + + if (BROTLI_PREDICT_TRUE(zero == 0)) { + // main execution path + } else { + // compiler should place this code outside of main execution path + } + +OR: + + if (BROTLI_PREDICT_FALSE(something_rare_or_unexpected_happens)) { + // compiler should place this code outside of main execution path + } + +*/ +#if BROTLI_GNUC_HAS_BUILTIN(__builtin_expect, 3, 0, 0) || \ + BROTLI_INTEL_VERSION_CHECK(16, 0, 0) || \ + BROTLI_SUNPRO_VERSION_CHECK(5, 15, 0) || \ + BROTLI_ARM_VERSION_CHECK(4, 1, 0) || \ + BROTLI_IBM_VERSION_CHECK(10, 1, 0) || \ + BROTLI_TI_VERSION_CHECK(7, 3, 0) || \ + BROTLI_TINYC_VERSION_CHECK(0, 9, 27) +#define BROTLI_PREDICT_TRUE(x) (__builtin_expect(!!(x), 1)) +#define BROTLI_PREDICT_FALSE(x) (__builtin_expect(x, 0)) +#else +#define BROTLI_PREDICT_FALSE(x) (x) +#define BROTLI_PREDICT_TRUE(x) (x) +#endif + +#if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) && \ + !defined(__cplusplus) +#define BROTLI_RESTRICT restrict +#elif BROTLI_GNUC_VERSION_CHECK(3, 1, 0) || \ + BROTLI_MSVC_VERSION_CHECK(14, 0, 0) || \ + BROTLI_INTEL_VERSION_CHECK(16, 0, 0) || \ + BROTLI_ARM_VERSION_CHECK(4, 1, 0) || \ + BROTLI_IBM_VERSION_CHECK(10, 1, 0) || \ + BROTLI_PGI_VERSION_CHECK(17, 10, 0) || \ + BROTLI_TI_VERSION_CHECK(8, 0, 0) || \ + BROTLI_IAR_VERSION_CHECK(8, 0, 0) || \ + (BROTLI_SUNPRO_VERSION_CHECK(5, 14, 0) && defined(__cplusplus)) +#define BROTLI_RESTRICT __restrict +#elif BROTLI_SUNPRO_VERSION_CHECK(5, 3, 0) && !defined(__cplusplus) +#define BROTLI_RESTRICT _Restrict +#else +#define BROTLI_RESTRICT +#endif + +#if (defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L)) || \ + (defined(__cplusplus) && (__cplusplus >= 199711L)) +#define BROTLI_MAYBE_INLINE inline +#elif defined(__GNUC_STDC_INLINE__) || defined(__GNUC_GNU_INLINE__) || \ + BROTLI_ARM_VERSION_CHECK(6, 2, 0) +#define BROTLI_MAYBE_INLINE __inline__ +#elif BROTLI_MSVC_VERSION_CHECK(12, 0, 0) || \ + BROTLI_ARM_VERSION_CHECK(4, 1, 0) || BROTLI_TI_VERSION_CHECK(8, 0, 0) +#define BROTLI_MAYBE_INLINE __inline +#else +#define BROTLI_MAYBE_INLINE +#endif + +#if BROTLI_GNUC_HAS_ATTRIBUTE(always_inline, 4, 0, 0) || \ + BROTLI_INTEL_VERSION_CHECK(16, 0, 0) || \ + BROTLI_SUNPRO_VERSION_CHECK(5, 11, 0) || \ + BROTLI_ARM_VERSION_CHECK(4, 1, 0) || \ + BROTLI_IBM_VERSION_CHECK(10, 1, 0) || \ + BROTLI_TI_VERSION_CHECK(8, 0, 0) || \ + (BROTLI_TI_VERSION_CHECK(7, 3, 0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) +#define BROTLI_INLINE BROTLI_MAYBE_INLINE __attribute__((__always_inline__)) +#elif BROTLI_MSVC_VERSION_CHECK(12, 0, 0) +#define BROTLI_INLINE BROTLI_MAYBE_INLINE __forceinline +#elif BROTLI_TI_VERSION_CHECK(7, 0, 0) && defined(__cplusplus) +#define BROTLI_INLINE BROTLI_MAYBE_INLINE _Pragma("FUNC_ALWAYS_INLINE;") +#elif BROTLI_IAR_VERSION_CHECK(8, 0, 0) +#define BROTLI_INLINE BROTLI_MAYBE_INLINE _Pragma("inline=forced") +#else +#define BROTLI_INLINE BROTLI_MAYBE_INLINE +#endif + +#if BROTLI_GNUC_HAS_ATTRIBUTE(noinline, 4, 0, 0) || \ + BROTLI_INTEL_VERSION_CHECK(16, 0, 0) || \ + BROTLI_SUNPRO_VERSION_CHECK(5, 11, 0) || \ + BROTLI_ARM_VERSION_CHECK(4, 1, 0) || \ + BROTLI_IBM_VERSION_CHECK(10, 1, 0) || \ + BROTLI_TI_VERSION_CHECK(8, 0, 0) || \ + (BROTLI_TI_VERSION_CHECK(7, 3, 0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) +#define BROTLI_NOINLINE __attribute__((__noinline__)) +#elif BROTLI_MSVC_VERSION_CHECK(13, 10, 0) +#define BROTLI_NOINLINE __declspec(noinline) +#elif BROTLI_PGI_VERSION_CHECK(10, 2, 0) +#define BROTLI_NOINLINE _Pragma("noinline") +#elif BROTLI_TI_VERSION_CHECK(6, 0, 0) && defined(__cplusplus) +#define BROTLI_NOINLINE _Pragma("FUNC_CANNOT_INLINE;") +#elif BROTLI_IAR_VERSION_CHECK(8, 0, 0) +#define BROTLI_NOINLINE _Pragma("inline=never") +#else +#define BROTLI_NOINLINE +#endif + +/* <<< <<< <<< end of hedley macros. */ + +#if BROTLI_GNUC_HAS_ATTRIBUTE(unused, 2, 7, 0) || \ + BROTLI_INTEL_VERSION_CHECK(16, 0, 0) +#define BROTLI_UNUSED_FUNCTION static BROTLI_INLINE __attribute__ ((unused)) +#else +#define BROTLI_UNUSED_FUNCTION static BROTLI_INLINE +#endif + +#if BROTLI_GNUC_HAS_ATTRIBUTE(aligned, 2, 7, 0) +#define BROTLI_ALIGNED(N) __attribute__((aligned(N))) +#else +#define BROTLI_ALIGNED(N) +#endif + +#if (defined(__ARM_ARCH) && (__ARM_ARCH == 7)) || \ + (defined(M_ARM) && (M_ARM == 7)) +#define BROTLI_TARGET_ARMV7 +#endif /* ARMv7 */ + +#if (defined(__ARM_ARCH) && (__ARM_ARCH == 8)) || \ + defined(__aarch64__) || defined(__ARM64_ARCH_8__) +#define BROTLI_TARGET_ARMV8_ANY + +#if defined(__ARM_32BIT_STATE) +#define BROTLI_TARGET_ARMV8_32 +#elif defined(__ARM_64BIT_STATE) +#define BROTLI_TARGET_ARMV8_64 +#endif + +#endif /* ARMv8 */ + +#if defined(__ARM_NEON__) || defined(__ARM_NEON) +#define BROTLI_TARGET_NEON +#endif + +#if defined(__i386) || defined(_M_IX86) +#define BROTLI_TARGET_X86 +#endif + +#if defined(__x86_64__) || defined(_M_X64) +#define BROTLI_TARGET_X64 +#endif + +#if defined(__PPC64__) +#define BROTLI_TARGET_POWERPC64 +#endif + +#if defined(__riscv) && defined(__riscv_xlen) && __riscv_xlen == 64 +#define BROTLI_TARGET_RISCV64 +#endif + +#if defined(__loongarch_lp64) +#define BROTLI_TARGET_LOONGARCH64 +#endif + +/* This does not seem to be an indicator of z/Architecture (64-bit); neither + that allows to use unaligned loads. */ +#if defined(__s390x__) +#define BROTLI_TARGET_S390X +#endif + +#if defined(__mips64) +#define BROTLI_TARGET_MIPS64 +#endif + +#if defined(BROTLI_TARGET_X64) || defined(BROTLI_TARGET_ARMV8_64) || \ + defined(BROTLI_TARGET_POWERPC64) || defined(BROTLI_TARGET_RISCV64) || \ + defined(BROTLI_TARGET_LOONGARCH64) || defined(BROTLI_TARGET_MIPS64) +#define BROTLI_TARGET_64_BITS 1 +#else +#define BROTLI_TARGET_64_BITS 0 +#endif + +#if defined(BROTLI_BUILD_64_BIT) +#define BROTLI_64_BITS 1 +#elif defined(BROTLI_BUILD_32_BIT) +#define BROTLI_64_BITS 0 +#else +#define BROTLI_64_BITS BROTLI_TARGET_64_BITS +#endif + +#if (BROTLI_64_BITS) +#define brotli_reg_t uint64_t +#else +#define brotli_reg_t uint32_t +#endif + +#if defined(BROTLI_BUILD_BIG_ENDIAN) +#define BROTLI_BIG_ENDIAN 1 +#elif defined(BROTLI_BUILD_LITTLE_ENDIAN) +#define BROTLI_LITTLE_ENDIAN 1 +#elif defined(BROTLI_BUILD_ENDIAN_NEUTRAL) +/* Just break elif chain. */ +#elif defined(__BYTE_ORDER__) && (__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__) +#define BROTLI_LITTLE_ENDIAN 1 +#elif defined(_WIN32) || defined(BROTLI_TARGET_X64) +/* Win32 & x64 can currently always be assumed to be little endian */ +#define BROTLI_LITTLE_ENDIAN 1 +#elif defined(__BYTE_ORDER__) && (__BYTE_ORDER__ == __ORDER_BIG_ENDIAN__) +#define BROTLI_BIG_ENDIAN 1 +/* Likely target platform is iOS / OSX. */ +#elif defined(BYTE_ORDER) && (BYTE_ORDER == LITTLE_ENDIAN) +#define BROTLI_LITTLE_ENDIAN 1 +#elif defined(BYTE_ORDER) && (BYTE_ORDER == BIG_ENDIAN) +#define BROTLI_BIG_ENDIAN 1 +#endif + +#if !defined(BROTLI_LITTLE_ENDIAN) +#define BROTLI_LITTLE_ENDIAN 0 +#endif + +#if !defined(BROTLI_BIG_ENDIAN) +#define BROTLI_BIG_ENDIAN 0 +#endif + +#if defined(BROTLI_BUILD_NO_UNALIGNED_READ_FAST) +#define BROTLI_UNALIGNED_READ_FAST (!!0) +#elif defined(BROTLI_TARGET_X86) || defined(BROTLI_TARGET_X64) || \ + defined(BROTLI_TARGET_ARMV7) || defined(BROTLI_TARGET_ARMV8_ANY) || \ + defined(BROTLI_TARGET_RISCV64) || defined(BROTLI_TARGET_LOONGARCH64) +/* These targets are known to generate efficient code for unaligned reads + * (e.g. a single instruction, not multiple 1-byte loads, shifted and or'd + * together). */ +#define BROTLI_UNALIGNED_READ_FAST (!!1) +#else +#define BROTLI_UNALIGNED_READ_FAST (!!0) +#endif + +/* Portable unaligned memory access: read / write values via memcpy. */ +#if !defined(BROTLI_USE_PACKED_FOR_UNALIGNED) +#if defined(__mips__) && (!defined(__mips_isa_rev) || __mips_isa_rev < 6) +#define BROTLI_USE_PACKED_FOR_UNALIGNED 1 +#else +#define BROTLI_USE_PACKED_FOR_UNALIGNED 0 +#endif +#endif /* defined(BROTLI_USE_PACKED_FOR_UNALIGNED) */ + +#if BROTLI_USE_PACKED_FOR_UNALIGNED + +typedef union BrotliPackedValue { + uint16_t u16; + uint32_t u32; + uint64_t u64; + size_t szt; +} __attribute__ ((packed)) BrotliPackedValue; + +static BROTLI_INLINE uint16_t BrotliUnalignedRead16(const void* p) { + const BrotliPackedValue* address = (const BrotliPackedValue*)p; + return address->u16; +} +static BROTLI_INLINE uint32_t BrotliUnalignedRead32(const void* p) { + const BrotliPackedValue* address = (const BrotliPackedValue*)p; + return address->u32; +} +static BROTLI_INLINE uint64_t BrotliUnalignedRead64(const void* p) { + const BrotliPackedValue* address = (const BrotliPackedValue*)p; + return address->u64; +} +static BROTLI_INLINE size_t BrotliUnalignedReadSizeT(const void* p) { + const BrotliPackedValue* address = (const BrotliPackedValue*)p; + return address->szt; +} +static BROTLI_INLINE void BrotliUnalignedWrite64(void* p, uint64_t v) { + BrotliPackedValue* address = (BrotliPackedValue*)p; + address->u64 = v; +} + +#else /* not BROTLI_USE_PACKED_FOR_UNALIGNED */ + +static BROTLI_INLINE uint16_t BrotliUnalignedRead16(const void* p) { + uint16_t t; + memcpy(&t, p, sizeof t); + return t; +} +static BROTLI_INLINE uint32_t BrotliUnalignedRead32(const void* p) { + uint32_t t; + memcpy(&t, p, sizeof t); + return t; +} +static BROTLI_INLINE uint64_t BrotliUnalignedRead64(const void* p) { + uint64_t t; + memcpy(&t, p, sizeof t); + return t; +} +static BROTLI_INLINE size_t BrotliUnalignedReadSizeT(const void* p) { + size_t t; + memcpy(&t, p, sizeof t); + return t; +} +static BROTLI_INLINE void BrotliUnalignedWrite64(void* p, uint64_t v) { + memcpy(p, &v, sizeof v); +} + +#endif /* BROTLI_USE_PACKED_FOR_UNALIGNED */ + +#if BROTLI_GNUC_HAS_BUILTIN(__builtin_bswap16, 4, 3, 0) +#define BROTLI_BSWAP16(V) ((uint16_t)__builtin_bswap16(V)) +#else +#define BROTLI_BSWAP16(V) ((uint16_t)( \ + (((V) & 0xFFU) << 8) | \ + (((V) >> 8) & 0xFFU))) +#endif + +#if BROTLI_GNUC_HAS_BUILTIN(__builtin_bswap32, 4, 3, 0) +#define BROTLI_BSWAP32(V) ((uint32_t)__builtin_bswap32(V)) +#else +#define BROTLI_BSWAP32(V) ((uint32_t)( \ + (((V) & 0xFFU) << 24) | (((V) & 0xFF00U) << 8) | \ + (((V) >> 8) & 0xFF00U) | (((V) >> 24) & 0xFFU))) +#endif + +#if BROTLI_GNUC_HAS_BUILTIN(__builtin_bswap64, 4, 3, 0) +#define BROTLI_BSWAP64(V) ((uint64_t)__builtin_bswap64(V)) +#else +#define BROTLI_BSWAP64(V) ((uint64_t)( \ + (((V) & 0xFFU) << 56) | (((V) & 0xFF00U) << 40) | \ + (((V) & 0xFF0000U) << 24) | (((V) & 0xFF000000U) << 8) | \ + (((V) >> 8) & 0xFF000000U) | (((V) >> 24) & 0xFF0000U) | \ + (((V) >> 40) & 0xFF00U) | (((V) >> 56) & 0xFFU))) +#endif + +#if BROTLI_LITTLE_ENDIAN +/* Straight endianness. Just read / write values. */ +#define BROTLI_UNALIGNED_LOAD16LE BrotliUnalignedRead16 +#define BROTLI_UNALIGNED_LOAD32LE BrotliUnalignedRead32 +#define BROTLI_UNALIGNED_LOAD64LE BrotliUnalignedRead64 +#define BROTLI_UNALIGNED_STORE64LE BrotliUnalignedWrite64 +#elif BROTLI_BIG_ENDIAN /* BROTLI_LITTLE_ENDIAN */ +static BROTLI_INLINE uint16_t BROTLI_UNALIGNED_LOAD16LE(const void* p) { + uint16_t value = BrotliUnalignedRead16(p); + return BROTLI_BSWAP16(value); +} +static BROTLI_INLINE uint32_t BROTLI_UNALIGNED_LOAD32LE(const void* p) { + uint32_t value = BrotliUnalignedRead32(p); + return BROTLI_BSWAP32(value); +} +static BROTLI_INLINE uint64_t BROTLI_UNALIGNED_LOAD64LE(const void* p) { + uint64_t value = BrotliUnalignedRead64(p); + return BROTLI_BSWAP64(value); +} +static BROTLI_INLINE void BROTLI_UNALIGNED_STORE64LE(void* p, uint64_t v) { + uint64_t value = BROTLI_BSWAP64(v); + BrotliUnalignedWrite64(p, value); +} +#else /* BROTLI_LITTLE_ENDIAN */ +/* Read / store values byte-wise; hopefully compiler will understand. */ +static BROTLI_INLINE uint16_t BROTLI_UNALIGNED_LOAD16LE(const void* p) { + const uint8_t* in = (const uint8_t*)p; + return (uint16_t)(in[0] | (in[1] << 8)); +} +static BROTLI_INLINE uint32_t BROTLI_UNALIGNED_LOAD32LE(const void* p) { + const uint8_t* in = (const uint8_t*)p; + uint32_t value = (uint32_t)(in[0]); + value |= (uint32_t)(in[1]) << 8; + value |= (uint32_t)(in[2]) << 16; + value |= (uint32_t)(in[3]) << 24; + return value; +} +static BROTLI_INLINE uint64_t BROTLI_UNALIGNED_LOAD64LE(const void* p) { + const uint8_t* in = (const uint8_t*)p; + uint64_t value = (uint64_t)(in[0]); + value |= (uint64_t)(in[1]) << 8; + value |= (uint64_t)(in[2]) << 16; + value |= (uint64_t)(in[3]) << 24; + value |= (uint64_t)(in[4]) << 32; + value |= (uint64_t)(in[5]) << 40; + value |= (uint64_t)(in[6]) << 48; + value |= (uint64_t)(in[7]) << 56; + return value; +} +static BROTLI_INLINE void BROTLI_UNALIGNED_STORE64LE(void* p, uint64_t v) { + uint8_t* out = (uint8_t*)p; + out[0] = (uint8_t)v; + out[1] = (uint8_t)(v >> 8); + out[2] = (uint8_t)(v >> 16); + out[3] = (uint8_t)(v >> 24); + out[4] = (uint8_t)(v >> 32); + out[5] = (uint8_t)(v >> 40); + out[6] = (uint8_t)(v >> 48); + out[7] = (uint8_t)(v >> 56); +} +#endif /* BROTLI_LITTLE_ENDIAN */ + +static BROTLI_INLINE void* BROTLI_UNALIGNED_LOAD_PTR(const void* p) { + void* v; + memcpy(&v, p, sizeof(void*)); + return v; +} + +static BROTLI_INLINE void BROTLI_UNALIGNED_STORE_PTR(void* p, const void* v) { + memcpy(p, &v, sizeof(void*)); +} + +/* BROTLI_IS_CONSTANT macros returns true for compile-time constants. */ +#if BROTLI_GNUC_HAS_BUILTIN(__builtin_constant_p, 3, 0, 1) || \ + BROTLI_INTEL_VERSION_CHECK(16, 0, 0) +#define BROTLI_IS_CONSTANT(x) (!!__builtin_constant_p(x)) +#else +#define BROTLI_IS_CONSTANT(x) (!!0) +#endif + +#if defined(BROTLI_TARGET_ARMV7) || defined(BROTLI_TARGET_ARMV8_ANY) +#define BROTLI_HAS_UBFX (!!1) +#else +#define BROTLI_HAS_UBFX (!!0) +#endif + +#if defined(BROTLI_ENABLE_LOG) +#define BROTLI_LOG(x) printf x +#else +#define BROTLI_LOG(x) +#endif + +#if defined(BROTLI_DEBUG) || defined(BROTLI_ENABLE_LOG) +#define BROTLI_ENABLE_DUMP_DEFAULT 1 +#define BROTLI_DCHECK(x) assert(x) +#else +#define BROTLI_ENABLE_DUMP_DEFAULT 0 +#define BROTLI_DCHECK(x) +#endif + +#if !defined(BROTLI_ENABLE_DUMP) +#define BROTLI_ENABLE_DUMP BROTLI_ENABLE_DUMP_DEFAULT +#endif + +#if BROTLI_ENABLE_DUMP +static BROTLI_INLINE void BrotliDump(const char* f, int l, const char* fn) { + fprintf(stderr, "%s:%d (%s)\n", f, l, fn); + fflush(stderr); +} +#define BROTLI_DUMP() BrotliDump(__FILE__, __LINE__, __FUNCTION__) +#else +#define BROTLI_DUMP() (void)(0) +#endif + +/* BrotliRBit assumes brotli_reg_t fits native CPU register type. */ +#if (BROTLI_64_BITS == BROTLI_TARGET_64_BITS) +/* TODO(eustas): add appropriate icc/sunpro/arm/ibm/ti checks. */ +#if (BROTLI_GNUC_VERSION_CHECK(3, 0, 0) || defined(__llvm__)) && \ + !defined(BROTLI_BUILD_NO_RBIT) +#if defined(BROTLI_TARGET_ARMV7) || defined(BROTLI_TARGET_ARMV8_ANY) +/* TODO(eustas): detect ARMv6T2 and enable this code for it. */ +static BROTLI_INLINE brotli_reg_t BrotliRBit(brotli_reg_t input) { + brotli_reg_t output; + __asm__("rbit %0, %1\n" : "=r"(output) : "r"(input)); + return output; +} +#define BROTLI_RBIT(x) BrotliRBit(x) +#endif /* armv7 / armv8 */ +#endif /* gcc || clang */ +#endif /* brotli_reg_t is native */ +#if !defined(BROTLI_RBIT) +static BROTLI_INLINE void BrotliRBit(void) { /* Should break build if used. */ } +#endif /* BROTLI_RBIT */ + +#define BROTLI_REPEAT_4(X) {X; X; X; X;} +#define BROTLI_REPEAT_5(X) {X; X; X; X; X;} +#define BROTLI_REPEAT_6(X) {X; X; X; X; X; X;} + +#define BROTLI_UNUSED(X) (void)(X) + +#define BROTLI_MIN_MAX(T) \ + static BROTLI_INLINE T brotli_min_ ## T (T a, T b) { return a < b ? a : b; } \ + static BROTLI_INLINE T brotli_max_ ## T (T a, T b) { return a > b ? a : b; } +BROTLI_MIN_MAX(double) BROTLI_MIN_MAX(float) BROTLI_MIN_MAX(int) +BROTLI_MIN_MAX(size_t) BROTLI_MIN_MAX(uint32_t) BROTLI_MIN_MAX(uint8_t) +#undef BROTLI_MIN_MAX +#define BROTLI_MIN(T, A, B) (brotli_min_ ## T((A), (B))) +#define BROTLI_MAX(T, A, B) (brotli_max_ ## T((A), (B))) + +#define BROTLI_SWAP(T, A, I, J) { \ + T __brotli_swap_tmp = (A)[(I)]; \ + (A)[(I)] = (A)[(J)]; \ + (A)[(J)] = __brotli_swap_tmp; \ +} + +#if BROTLI_64_BITS +#if BROTLI_GNUC_HAS_BUILTIN(__builtin_ctzll, 3, 4, 0) || \ + BROTLI_INTEL_VERSION_CHECK(16, 0, 0) +#define BROTLI_TZCNT64 __builtin_ctzll +#elif BROTLI_MSVC_VERSION_CHECK(18, 0, 0) +#if defined(BROTLI_TARGET_X64) && !defined(_M_ARM64EC) +#define BROTLI_TZCNT64 _tzcnt_u64 +#else /* BROTLI_TARGET_X64 */ +static BROTLI_INLINE uint32_t BrotliBsf64Msvc(uint64_t x) { + uint32_t lsb; + _BitScanForward64(&lsb, x); + return lsb; +} +#define BROTLI_TZCNT64 BrotliBsf64Msvc +#endif /* BROTLI_TARGET_X64 */ +#endif /* __builtin_ctzll */ +#endif /* BROTLI_64_BITS */ + +#if BROTLI_GNUC_HAS_BUILTIN(__builtin_clz, 3, 4, 0) || \ + BROTLI_INTEL_VERSION_CHECK(16, 0, 0) +#define BROTLI_BSR32(x) (31u ^ (uint32_t)__builtin_clz(x)) +#elif BROTLI_MSVC_VERSION_CHECK(18, 0, 0) +static BROTLI_INLINE uint32_t BrotliBsr32Msvc(uint32_t x) { + unsigned long msb; + _BitScanReverse(&msb, x); + return (uint32_t)msb; +} +#define BROTLI_BSR32 BrotliBsr32Msvc +#endif /* __builtin_clz */ + +/* Default brotli_alloc_func */ +BROTLI_COMMON_API void* BrotliDefaultAllocFunc(void* opaque, size_t size); + +/* Default brotli_free_func */ +BROTLI_COMMON_API void BrotliDefaultFreeFunc(void* opaque, void* address); + +/* Circular logical rotates. */ +static BROTLI_INLINE uint16_t BrotliRotateRight16(uint16_t const value, + size_t count) { + count &= 0x0F; /* for fickle pattern recognition */ + return (value >> count) | (uint16_t)(value << ((0U - count) & 0x0F)); +} +static BROTLI_INLINE uint32_t BrotliRotateRight32(uint32_t const value, + size_t count) { + count &= 0x1F; /* for fickle pattern recognition */ + return (value >> count) | (uint32_t)(value << ((0U - count) & 0x1F)); +} +static BROTLI_INLINE uint64_t BrotliRotateRight64(uint64_t const value, + size_t count) { + count &= 0x3F; /* for fickle pattern recognition */ + return (value >> count) | (uint64_t)(value << ((0U - count) & 0x3F)); +} + +BROTLI_UNUSED_FUNCTION void BrotliSuppressUnusedFunctions(void) { + BROTLI_UNUSED(&BrotliSuppressUnusedFunctions); + BROTLI_UNUSED(&BrotliUnalignedRead16); + BROTLI_UNUSED(&BrotliUnalignedRead32); + BROTLI_UNUSED(&BrotliUnalignedRead64); + BROTLI_UNUSED(&BrotliUnalignedReadSizeT); + BROTLI_UNUSED(&BrotliUnalignedWrite64); + BROTLI_UNUSED(&BROTLI_UNALIGNED_LOAD16LE); + BROTLI_UNUSED(&BROTLI_UNALIGNED_LOAD32LE); + BROTLI_UNUSED(&BROTLI_UNALIGNED_LOAD64LE); + BROTLI_UNUSED(&BROTLI_UNALIGNED_STORE64LE); + BROTLI_UNUSED(&BROTLI_UNALIGNED_LOAD_PTR); + BROTLI_UNUSED(&BROTLI_UNALIGNED_STORE_PTR); + BROTLI_UNUSED(&BrotliRBit); + BROTLI_UNUSED(&brotli_min_double); + BROTLI_UNUSED(&brotli_max_double); + BROTLI_UNUSED(&brotli_min_float); + BROTLI_UNUSED(&brotli_max_float); + BROTLI_UNUSED(&brotli_min_int); + BROTLI_UNUSED(&brotli_max_int); + BROTLI_UNUSED(&brotli_min_size_t); + BROTLI_UNUSED(&brotli_max_size_t); + BROTLI_UNUSED(&brotli_min_uint32_t); + BROTLI_UNUSED(&brotli_max_uint32_t); + BROTLI_UNUSED(&brotli_min_uint8_t); + BROTLI_UNUSED(&brotli_max_uint8_t); + BROTLI_UNUSED(&BrotliDefaultAllocFunc); + BROTLI_UNUSED(&BrotliDefaultFreeFunc); + BROTLI_UNUSED(&BrotliRotateRight16); + BROTLI_UNUSED(&BrotliRotateRight32); + BROTLI_UNUSED(&BrotliRotateRight64); +#if BROTLI_ENABLE_DUMP + BROTLI_UNUSED(&BrotliDump); +#endif + +#if defined(_MSC_VER) && (defined(_M_X64) || defined(_M_I86)) && \ + !defined(_M_ARM64EC) +/* _mm_prefetch() is not defined outside of x86/x64 */ +/* https://msdn.microsoft.com/fr-fr/library/84szxsww(v=vs.90).aspx */ +#include +#define PREFETCH_L1(ptr) _mm_prefetch((const char*)(ptr), _MM_HINT_T0) +#define PREFETCH_L2(ptr) _mm_prefetch((const char*)(ptr), _MM_HINT_T1) +#elif BROTLI_GNUC_HAS_BUILTIN(__builtin_prefetch, 3, 1, 0) +#define PREFETCH_L1(ptr) \ + __builtin_prefetch((ptr), 0 /* rw==read */, 3 /* locality */) +#define PREFETCH_L2(ptr) \ + __builtin_prefetch((ptr), 0 /* rw==read */, 2 /* locality */) +#elif defined(__aarch64__) +#define PREFETCH_L1(ptr) \ + do { \ + __asm__ __volatile__("prfm pldl1keep, %0" ::"Q"(*(ptr))); \ + } while (0) +#define PREFETCH_L2(ptr) \ + do { \ + __asm__ __volatile__("prfm pldl2keep, %0" ::"Q"(*(ptr))); \ + } while (0) +#else +#define PREFETCH_L1(ptr) \ + do { \ + (void)(ptr); \ + } while (0) /* disabled */ +#define PREFETCH_L2(ptr) \ + do { \ + (void)(ptr); \ + } while (0) /* disabled */ +#endif + +/* The SIMD matchers are only faster at certain quality levels. */ +#if defined(_M_X64) && defined(BROTLI_TZCNT64) +#define BROTLI_MAX_SIMD_QUALITY 7 +#elif defined(BROTLI_TZCNT64) +#define BROTLI_MAX_SIMD_QUALITY 6 +#endif +} + +#if defined(_MSC_VER) +#define BROTLI_CRASH() __debugbreak(), (void)abort() +#elif BROTLI_GNUC_HAS_BUILTIN(__builtin_trap, 3, 0, 0) +#define BROTLI_CRASH() (void)__builtin_trap() +#else +#define BROTLI_CRASH() (void)abort() +#endif + +/* Make BROTLI_TEST=0 act same as undefined. */ +#if defined(BROTLI_TEST) && ((1-BROTLI_TEST-1) == 0) +#undef BROTLI_TEST +#endif + +#if BROTLI_GNUC_HAS_ATTRIBUTE(model, 3, 0, 3) +#define BROTLI_MODEL(M) __attribute__((model(M))) +#else +#define BROTLI_MODEL(M) /* M */ +#endif + +#if BROTLI_GNUC_HAS_ATTRIBUTE(cold, 4, 3, 0) +#define BROTLI_COLD __attribute__((cold)) +#else +#define BROTLI_COLD /* cold */ +#endif + +#endif /* BROTLI_COMMON_PLATFORM_H_ */ diff --git a/packages/runtime/vendor/brotli/c/common/shared_dictionary.c b/packages/runtime/vendor/brotli/c/common/shared_dictionary.c new file mode 100644 index 000000000..cef4e20fc --- /dev/null +++ b/packages/runtime/vendor/brotli/c/common/shared_dictionary.c @@ -0,0 +1,517 @@ +/* Copyright 2017 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* Shared Dictionary definition and utilities. */ + +#include + +#include "dictionary.h" +#include "platform.h" +#include "shared_dictionary_internal.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +#if defined(BROTLI_EXPERIMENTAL) + +#define BROTLI_NUM_ENCODED_LENGTHS (SHARED_BROTLI_MAX_DICTIONARY_WORD_LENGTH \ + - SHARED_BROTLI_MIN_DICTIONARY_WORD_LENGTH + 1) + +/* Max allowed by spec */ +#define BROTLI_MAX_SIZE_BITS 15u + +/* Returns BROTLI_TRUE on success, BROTLI_FALSE on failure. */ +static BROTLI_BOOL ReadBool(const uint8_t* encoded, size_t size, size_t* pos, + BROTLI_BOOL* result) { + uint8_t value; + size_t position = *pos; + if (position >= size) return BROTLI_FALSE; /* past file end */ + value = encoded[position++]; + if (value > 1) return BROTLI_FALSE; /* invalid bool */ + *result = TO_BROTLI_BOOL(value); + *pos = position; + return BROTLI_TRUE; /* success */ +} + +/* Returns BROTLI_TRUE on success, BROTLI_FALSE on failure. */ +static BROTLI_BOOL ReadUint8(const uint8_t* encoded, size_t size, size_t* pos, + uint8_t* result) { + size_t position = *pos; + if (position + sizeof(uint8_t) > size) return BROTLI_FALSE; + *result = encoded[position++]; + *pos = position; + return BROTLI_TRUE; +} + +/* Returns BROTLI_TRUE on success, BROTLI_FALSE on failure. */ +static BROTLI_BOOL ReadUint16(const uint8_t* encoded, size_t size, size_t* pos, + uint16_t* result) { + size_t position = *pos; + if (position + sizeof(uint16_t) > size) return BROTLI_FALSE; + *result = BROTLI_UNALIGNED_LOAD16LE(&encoded[position]); + position += 2; + *pos = position; + return BROTLI_TRUE; +} + +/* Reads a varint into a uint32_t, and returns error if it's too large */ +/* Returns BROTLI_TRUE on success, BROTLI_FALSE on failure. */ +static BROTLI_BOOL ReadVarint32(const uint8_t* encoded, size_t size, + size_t* pos, uint32_t* result) { + int num = 0; + uint8_t byte; + *result = 0; + for (;;) { + if (*pos >= size) return BROTLI_FALSE; + byte = encoded[(*pos)++]; + if (num == 4 && byte > 15) return BROTLI_FALSE; + *result |= (uint32_t)(byte & 127) << (num * 7); + if (byte < 128) return BROTLI_TRUE; + num++; + } +} + +/* Returns the total length of word list. */ +static size_t BrotliSizeBitsToOffsets(const uint8_t* size_bits_by_length, + uint32_t* offsets_by_length) { + uint32_t pos = 0; + uint32_t i; + for (i = 0; i <= SHARED_BROTLI_MAX_DICTIONARY_WORD_LENGTH; i++) { + offsets_by_length[i] = pos; + if (size_bits_by_length[i] != 0) { + pos += i << size_bits_by_length[i]; + } + } + return pos; +} + +static BROTLI_BOOL ParseWordList(size_t size, const uint8_t* encoded, + size_t* pos, BrotliDictionary* out) { + size_t offset; + size_t i; + size_t position = *pos; + if (position + BROTLI_NUM_ENCODED_LENGTHS > size) { + return BROTLI_FALSE; + } + + memset(out->size_bits_by_length, 0, SHARED_BROTLI_MIN_DICTIONARY_WORD_LENGTH); + memcpy(out->size_bits_by_length + SHARED_BROTLI_MIN_DICTIONARY_WORD_LENGTH, + &encoded[position], BROTLI_NUM_ENCODED_LENGTHS); + for (i = SHARED_BROTLI_MIN_DICTIONARY_WORD_LENGTH; + i <= SHARED_BROTLI_MAX_DICTIONARY_WORD_LENGTH; i++) { + if (out->size_bits_by_length[i] > BROTLI_MAX_SIZE_BITS) { + return BROTLI_FALSE; + } + } + position += BROTLI_NUM_ENCODED_LENGTHS; + offset = BrotliSizeBitsToOffsets( + out->size_bits_by_length, out->offsets_by_length); + + out->data = &encoded[position]; + out->data_size = offset; + position += offset; + if (position > size) return BROTLI_FALSE; + *pos = position; + return BROTLI_TRUE; +} + +/* Computes the cutOffTransforms of a BrotliTransforms which already has the + transforms data correctly filled in. */ +static void ComputeCutoffTransforms(BrotliTransforms* transforms) { + uint32_t i; + for (i = 0; i < BROTLI_TRANSFORMS_MAX_CUT_OFF + 1; i++) { + transforms->cutOffTransforms[i] = -1; + } + for (i = 0; i < transforms->num_transforms; i++) { + const uint8_t* prefix = BROTLI_TRANSFORM_PREFIX(transforms, i); + uint8_t type = BROTLI_TRANSFORM_TYPE(transforms, i); + const uint8_t* suffix = BROTLI_TRANSFORM_SUFFIX(transforms, i); + if (type <= BROTLI_TRANSFORM_OMIT_LAST_9 && *prefix == 0 && *suffix == 0 && + transforms->cutOffTransforms[type] == -1) { + transforms->cutOffTransforms[type] = (int16_t)i; + } + } +} + +static BROTLI_BOOL ParsePrefixSuffixTable(size_t size, const uint8_t* encoded, + size_t* pos, BrotliTransforms* out, uint16_t* out_table, + size_t* out_table_size) { + size_t position = *pos; + size_t offset = 0; + size_t stringlet_count = 0; /* NUM_PREFIX_SUFFIX */ + size_t data_length = 0; + + /* PREFIX_SUFFIX_LENGTH */ + if (!ReadUint16(encoded, size, &position, &out->prefix_suffix_size)) { + return BROTLI_FALSE; + } + data_length = out->prefix_suffix_size; + + /* Must at least have space for null terminator. */ + if (data_length < 1) return BROTLI_FALSE; + out->prefix_suffix = &encoded[position]; + if (position + data_length >= size) return BROTLI_FALSE; + while (BROTLI_TRUE) { + /* STRING_LENGTH */ + size_t stringlet_len = encoded[position + offset]; + out_table[stringlet_count] = (uint16_t)offset; + stringlet_count++; + offset++; + if (stringlet_len == 0) { + if (offset == data_length) { + break; + } else { + return BROTLI_FALSE; + } + } + if (stringlet_count > 255) return BROTLI_FALSE; + offset += stringlet_len; + if (offset >= data_length) return BROTLI_FALSE; + } + + position += data_length; + *pos = position; + *out_table_size = (uint16_t)stringlet_count; + return BROTLI_TRUE; +} + +static BROTLI_BOOL ParseTransformsList(size_t size, const uint8_t* encoded, + size_t* pos, BrotliTransforms* out, uint16_t* prefix_suffix_table, + size_t* prefix_suffix_count) { + uint32_t i; + BROTLI_BOOL has_params = BROTLI_FALSE; + BROTLI_BOOL prefix_suffix_ok = BROTLI_FALSE; + size_t position = *pos; + size_t stringlet_cnt = 0; + if (position >= size) return BROTLI_FALSE; + + prefix_suffix_ok = ParsePrefixSuffixTable( + size, encoded, &position, out, prefix_suffix_table, &stringlet_cnt); + if (!prefix_suffix_ok) return BROTLI_FALSE; + out->prefix_suffix_map = prefix_suffix_table; + *prefix_suffix_count = stringlet_cnt; + + out->num_transforms = encoded[position++]; + out->transforms = &encoded[position]; + position += (size_t)out->num_transforms * 3; + if (position > size) return BROTLI_FALSE; + /* Check for errors and read extra parameters. */ + for (i = 0; i < out->num_transforms; i++) { + uint8_t prefix_id = BROTLI_TRANSFORM_PREFIX_ID(out, i); + uint8_t type = BROTLI_TRANSFORM_TYPE(out, i); + uint8_t suffix_id = BROTLI_TRANSFORM_SUFFIX_ID(out, i); + if (prefix_id >= stringlet_cnt) return BROTLI_FALSE; + if (type >= BROTLI_NUM_TRANSFORM_TYPES) return BROTLI_FALSE; + if (suffix_id >= stringlet_cnt) return BROTLI_FALSE; + if (type == BROTLI_TRANSFORM_SHIFT_FIRST || + type == BROTLI_TRANSFORM_SHIFT_ALL) { + has_params = BROTLI_TRUE; + } + } + if (has_params) { + out->params = &encoded[position]; + position += (size_t)out->num_transforms * 2; + if (position > size) return BROTLI_FALSE; + for (i = 0; i < out->num_transforms; i++) { + uint8_t type = BROTLI_TRANSFORM_TYPE(out, i); + if (type != BROTLI_TRANSFORM_SHIFT_FIRST && + type != BROTLI_TRANSFORM_SHIFT_ALL) { + if (out->params[i * 2] != 0 || out->params[i * 2 + 1] != 0) { + return BROTLI_FALSE; + } + } + } + } else { + out->params = NULL; + } + ComputeCutoffTransforms(out); + *pos = position; + return BROTLI_TRUE; +} + +static BROTLI_BOOL DryParseDictionary(const uint8_t* encoded, + size_t size, uint32_t* num_prefix, BROTLI_BOOL* is_custom_static_dict) { + size_t pos = 0; + uint32_t chunk_size = 0; + uint8_t num_word_lists; + uint8_t num_transform_lists; + *is_custom_static_dict = BROTLI_FALSE; + *num_prefix = 0; + + /* Skip magic header bytes. */ + pos += 2; + + /* LZ77_DICTIONARY_LENGTH */ + if (!ReadVarint32(encoded, size, &pos, &chunk_size)) return BROTLI_FALSE; + if (chunk_size != 0) { + /* This limitation is not specified but the 32-bit Brotli decoder for now */ + if (chunk_size > 1073741823) return BROTLI_FALSE; + *num_prefix = 1; + if (pos + chunk_size > size) return BROTLI_FALSE; + pos += chunk_size; + } + + if (!ReadUint8(encoded, size, &pos, &num_word_lists)) { + return BROTLI_FALSE; + } + if (!ReadUint8(encoded, size, &pos, &num_transform_lists)) { + return BROTLI_FALSE; + } + + if (num_word_lists > 0 || num_transform_lists > 0) { + *is_custom_static_dict = BROTLI_TRUE; + } + + return BROTLI_TRUE; +} + +static BROTLI_BOOL ParseDictionary(const uint8_t* encoded, size_t size, + BrotliSharedDictionary* dict) { + uint32_t i; + size_t pos = 0; + uint32_t chunk_size = 0; + size_t total_prefix_suffix_count = 0; + size_t transform_list_start[SHARED_BROTLI_NUM_DICTIONARY_CONTEXTS]; + uint16_t temporary_prefix_suffix_table[256]; + + /* Skip magic header bytes. */ + pos += 2; + + /* LZ77_DICTIONARY_LENGTH */ + if (!ReadVarint32(encoded, size, &pos, &chunk_size)) return BROTLI_FALSE; + if (chunk_size != 0) { + if (pos + chunk_size > size) return BROTLI_FALSE; + dict->prefix_size[dict->num_prefix] = chunk_size; + dict->prefix[dict->num_prefix] = &encoded[pos]; + dict->num_prefix++; + /* LZ77_DICTIONARY_LENGTH bytes. */ + pos += chunk_size; + } + + /* NUM_WORD_LISTS */ + if (!ReadUint8(encoded, size, &pos, &dict->num_word_lists)) { + return BROTLI_FALSE; + } + if (dict->num_word_lists > SHARED_BROTLI_NUM_DICTIONARY_CONTEXTS) { + return BROTLI_FALSE; + } + + if (dict->num_word_lists != 0) { + dict->words_instances = (BrotliDictionary*)dict->alloc_func( + dict->memory_manager_opaque, + dict->num_word_lists * sizeof(*dict->words_instances)); + if (!dict->words_instances) return BROTLI_FALSE; /* OOM */ + } + for (i = 0; i < dict->num_word_lists; i++) { + if (!ParseWordList(size, encoded, &pos, &dict->words_instances[i])) { + return BROTLI_FALSE; + } + } + + /* NUM_TRANSFORM_LISTS */ + if (!ReadUint8(encoded, size, &pos, &dict->num_transform_lists)) { + return BROTLI_FALSE; + } + if (dict->num_transform_lists > SHARED_BROTLI_NUM_DICTIONARY_CONTEXTS) { + return BROTLI_FALSE; + } + + if (dict->num_transform_lists != 0) { + dict->transforms_instances = (BrotliTransforms*)dict->alloc_func( + dict->memory_manager_opaque, + dict->num_transform_lists * sizeof(*dict->transforms_instances)); + if (!dict->transforms_instances) return BROTLI_FALSE; /* OOM */ + } + for (i = 0; i < dict->num_transform_lists; i++) { + BROTLI_BOOL ok = BROTLI_FALSE; + size_t prefix_suffix_count = 0; + transform_list_start[i] = pos; + dict->transforms_instances[i].prefix_suffix_map = + temporary_prefix_suffix_table; + ok = ParseTransformsList( + size, encoded, &pos, &dict->transforms_instances[i], + temporary_prefix_suffix_table, &prefix_suffix_count); + if (!ok) return BROTLI_FALSE; + total_prefix_suffix_count += prefix_suffix_count; + } + if (total_prefix_suffix_count != 0) { + dict->prefix_suffix_maps = (uint16_t*)dict->alloc_func( + dict->memory_manager_opaque, + total_prefix_suffix_count * sizeof(*dict->prefix_suffix_maps)); + if (!dict->prefix_suffix_maps) return BROTLI_FALSE; /* OOM */ + } + total_prefix_suffix_count = 0; + for (i = 0; i < dict->num_transform_lists; i++) { + size_t prefix_suffix_count = 0; + size_t position = transform_list_start[i]; + uint16_t* prefix_suffix_map = + &dict->prefix_suffix_maps[total_prefix_suffix_count]; + BROTLI_BOOL ok = ParsePrefixSuffixTable( + size, encoded, &position, &dict->transforms_instances[i], + prefix_suffix_map, &prefix_suffix_count); + if (!ok) return BROTLI_FALSE; + dict->transforms_instances[i].prefix_suffix_map = prefix_suffix_map; + total_prefix_suffix_count += prefix_suffix_count; + } + + if (dict->num_word_lists != 0 || dict->num_transform_lists != 0) { + if (!ReadUint8(encoded, size, &pos, &dict->num_dictionaries)) { + return BROTLI_FALSE; + } + if (dict->num_dictionaries == 0 || + dict->num_dictionaries > SHARED_BROTLI_NUM_DICTIONARY_CONTEXTS) { + return BROTLI_FALSE; + } + for (i = 0; i < dict->num_dictionaries; i++) { + uint8_t words_index; + uint8_t transforms_index; + if (!ReadUint8(encoded, size, &pos, &words_index)) { + return BROTLI_FALSE; + } + if (words_index > dict->num_word_lists) return BROTLI_FALSE; + if (!ReadUint8(encoded, size, &pos, &transforms_index)) { + return BROTLI_FALSE; + } + if (transforms_index > dict->num_transform_lists) return BROTLI_FALSE; + dict->words[i] = words_index == dict->num_word_lists ? + BrotliGetDictionary() : &dict->words_instances[words_index]; + dict->transforms[i] = transforms_index == dict->num_transform_lists ? + BrotliGetTransforms(): &dict->transforms_instances[transforms_index]; + } + /* CONTEXT_ENABLED */ + if (!ReadBool(encoded, size, &pos, &dict->context_based)) { + return BROTLI_FALSE; + } + + /* CONTEXT_MAP */ + if (dict->context_based) { + for (i = 0; i < SHARED_BROTLI_NUM_DICTIONARY_CONTEXTS; i++) { + if (!ReadUint8(encoded, size, &pos, &dict->context_map[i])) { + return BROTLI_FALSE; + } + if (dict->context_map[i] >= dict->num_dictionaries) { + return BROTLI_FALSE; + } + } + } + } else { + dict->context_based = BROTLI_FALSE; + dict->num_dictionaries = 1; + dict->words[0] = BrotliGetDictionary(); + dict->transforms[0] = BrotliGetTransforms(); + } + + return BROTLI_TRUE; +} + +/* Decodes shared dictionary and verifies correctness. + Returns BROTLI_TRUE if dictionary is valid, BROTLI_FALSE otherwise. + The BrotliSharedDictionary must already have been initialized. If the + BrotliSharedDictionary already contains data, compound dictionaries + will be appended, but an error will be returned if it already has + custom words or transforms. + TODO(lode): link to RFC for shared brotli once published. */ +static BROTLI_BOOL DecodeSharedDictionary( + const uint8_t* encoded, size_t size, BrotliSharedDictionary* dict) { + uint32_t num_prefix = 0; + BROTLI_BOOL is_custom_static_dict = BROTLI_FALSE; + BROTLI_BOOL has_custom_static_dict = + dict->num_word_lists > 0 || dict->num_transform_lists > 0; + + /* Check magic header bytes. */ + if (size < 2) return BROTLI_FALSE; + if (encoded[0] != 0x91 || encoded[1] != 0) return BROTLI_FALSE; + + if (!DryParseDictionary(encoded, size, &num_prefix, &is_custom_static_dict)) { + return BROTLI_FALSE; + } + + if (num_prefix + dict->num_prefix > SHARED_BROTLI_MAX_COMPOUND_DICTS) { + return BROTLI_FALSE; + } + + /* Cannot combine different static dictionaries, only prefix dictionaries */ + if (has_custom_static_dict && is_custom_static_dict) return BROTLI_FALSE; + + return ParseDictionary(encoded, size, dict); +} + +#endif /* BROTLI_EXPERIMENTAL */ + +void BrotliSharedDictionaryDestroyInstance( + BrotliSharedDictionary* dict) { + if (!dict) { + return; + } else { + brotli_free_func free_func = dict->free_func; + void* opaque = dict->memory_manager_opaque; + /* Cleanup. */ + free_func(opaque, dict->words_instances); + free_func(opaque, dict->transforms_instances); + free_func(opaque, dict->prefix_suffix_maps); + /* Self-destruction. */ + free_func(opaque, dict); + } +} + +BROTLI_BOOL BrotliSharedDictionaryAttach( + BrotliSharedDictionary* dict, BrotliSharedDictionaryType type, + size_t data_size, const uint8_t data[BROTLI_ARRAY_PARAM(data_size)]) { + if (!dict) { + return BROTLI_FALSE; + } +#if defined(BROTLI_EXPERIMENTAL) + if (type == BROTLI_SHARED_DICTIONARY_SERIALIZED) { + return DecodeSharedDictionary(data, data_size, dict); + } +#endif /* BROTLI_EXPERIMENTAL */ + if (type == BROTLI_SHARED_DICTIONARY_RAW) { + if (dict->num_prefix >= SHARED_BROTLI_MAX_COMPOUND_DICTS) { + return BROTLI_FALSE; + } + dict->prefix_size[dict->num_prefix] = data_size; + dict->prefix[dict->num_prefix] = data; + dict->num_prefix++; + return BROTLI_TRUE; + } + return BROTLI_FALSE; +} + +BrotliSharedDictionary* BrotliSharedDictionaryCreateInstance( + brotli_alloc_func alloc_func, brotli_free_func free_func, void* opaque) { + BrotliSharedDictionary* dict = 0; + if (!alloc_func && !free_func) { + dict = (BrotliSharedDictionary*)malloc(sizeof(BrotliSharedDictionary)); + } else if (alloc_func && free_func) { + dict = (BrotliSharedDictionary*)alloc_func( + opaque, sizeof(BrotliSharedDictionary)); + } + if (dict == 0) { + return 0; + } + + /* TODO(eustas): explicitly initialize all the fields? */ + memset(dict, 0, sizeof(BrotliSharedDictionary)); + + dict->context_based = BROTLI_FALSE; + dict->num_dictionaries = 1; + dict->num_word_lists = 0; + dict->num_transform_lists = 0; + + dict->words[0] = BrotliGetDictionary(); + dict->transforms[0] = BrotliGetTransforms(); + + dict->alloc_func = alloc_func ? alloc_func : BrotliDefaultAllocFunc; + dict->free_func = free_func ? free_func : BrotliDefaultFreeFunc; + dict->memory_manager_opaque = opaque; + + return dict; +} + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif diff --git a/packages/runtime/vendor/brotli/c/common/shared_dictionary_internal.h b/packages/runtime/vendor/brotli/c/common/shared_dictionary_internal.h new file mode 100644 index 000000000..c6a90fa74 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/common/shared_dictionary_internal.h @@ -0,0 +1,75 @@ +/* Copyright 2017 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* (Transparent) Shared Dictionary definition. */ + +#ifndef BROTLI_COMMON_SHARED_DICTIONARY_INTERNAL_H_ +#define BROTLI_COMMON_SHARED_DICTIONARY_INTERNAL_H_ + +#include + +#include "dictionary.h" +#include "platform.h" +#include "transform.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +struct BrotliSharedDictionaryStruct { + /* LZ77 prefixes (compound dictionary). */ + uint32_t num_prefix; /* max SHARED_BROTLI_MAX_COMPOUND_DICTS */ + size_t prefix_size[SHARED_BROTLI_MAX_COMPOUND_DICTS]; + const uint8_t* prefix[SHARED_BROTLI_MAX_COMPOUND_DICTS]; + + /* If set, the context map is used to select word and transform list from 64 + contexts, if not set, the context map is not used and only words[0] and + transforms[0] are to be used. */ + BROTLI_BOOL context_based; + + uint8_t context_map[SHARED_BROTLI_NUM_DICTIONARY_CONTEXTS]; + + /* Amount of word_list+transform_list combinations. */ + uint8_t num_dictionaries; + + /* Must use num_dictionaries values. */ + const BrotliDictionary* words[SHARED_BROTLI_NUM_DICTIONARY_CONTEXTS]; + + /* Must use num_dictionaries values. */ + const BrotliTransforms* transforms[SHARED_BROTLI_NUM_DICTIONARY_CONTEXTS]; + + /* Amount of custom word lists. May be 0 if only Brotli's built-in is used */ + uint8_t num_word_lists; + + /* Contents of the custom words lists. Must be NULL if num_word_lists is 0. */ + BrotliDictionary* words_instances; + + /* Amount of custom transform lists. May be 0 if only Brotli's built-in is + used */ + uint8_t num_transform_lists; + + /* Contents of the custom transform lists. Must be NULL if num_transform_lists + is 0. */ + BrotliTransforms* transforms_instances; + + /* Concatenated prefix_suffix_maps of the custom transform lists. Must be NULL + if num_transform_lists is 0. */ + uint16_t* prefix_suffix_maps; + + /* Memory management */ + brotli_alloc_func alloc_func; + brotli_free_func free_func; + void* memory_manager_opaque; +}; + +typedef struct BrotliSharedDictionaryStruct BrotliSharedDictionaryInternal; +#define BrotliSharedDictionary BrotliSharedDictionaryInternal + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif + +#endif /* BROTLI_COMMON_SHARED_DICTIONARY_INTERNAL_H_ */ diff --git a/packages/runtime/vendor/brotli/c/common/static_init.h b/packages/runtime/vendor/brotli/c/common/static_init.h new file mode 100644 index 000000000..12744de8f --- /dev/null +++ b/packages/runtime/vendor/brotli/c/common/static_init.h @@ -0,0 +1,56 @@ +/* Copyright 2025 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* + Central point for static initialization. + + In case of "lazy" mode `BrotliXxxLazyStaticInit` is not provided by the + library. Embedder is responsible for providing it. This function should call + `BrotliXxxLazyStaticInitInner` on the first invocation. This function should + not return until execution of `BrotliXxxLazyStaticInitInner` is finished. + In C or before C++11 it is possible to call `BrotliXxxLazyStaticInitInner` + on start-up path and then `BrotliEncoderLazyStaticInit` is could be no-op; + another option is to use available thread execution controls to meet the + requirements. For possible C++11 implementation see static_init_lazy.cc. +*/ + +#ifndef THIRD_PARTY_BROTLI_COMMON_STATIC_INIT_H_ +#define THIRD_PARTY_BROTLI_COMMON_STATIC_INIT_H_ + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +/* Static data is "initialized" in compile time. */ +#define BROTLI_STATIC_INIT_NONE 0 +/* Static data is initialized before "main". */ +#define BROTLI_STATIC_INIT_EARLY 1 +/* Static data is initialized when first encoder is created. */ +#define BROTLI_STATIC_INIT_LAZY 2 + +#define BROTLI_STATIC_INIT_DEFAULT BROTLI_STATIC_INIT_NONE + +#if !defined(BROTLI_STATIC_INIT) +#define BROTLI_STATIC_INIT BROTLI_STATIC_INIT_DEFAULT +#endif + +#if (BROTLI_STATIC_INIT != BROTLI_STATIC_INIT_NONE) && \ + (BROTLI_STATIC_INIT != BROTLI_STATIC_INIT_EARLY) && \ + (BROTLI_STATIC_INIT != BROTLI_STATIC_INIT_LAZY) +#error Invalid value for BROTLI_STATIC_INIT +#endif + +#if (BROTLI_STATIC_INIT == BROTLI_STATIC_INIT_EARLY) +#if defined(BROTLI_EXTERNAL_DICTIONARY_DATA) +#error BROTLI_STATIC_INIT_EARLY will fail with BROTLI_EXTERNAL_DICTIONARY_DATA +#endif +#endif /* BROTLI_STATIC_INIT */ + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif + +#endif // THIRD_PARTY_BROTLI_COMMON_STATIC_INIT_H_ diff --git a/packages/runtime/vendor/brotli/c/common/transform.c b/packages/runtime/vendor/brotli/c/common/transform.c new file mode 100644 index 000000000..968b91238 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/common/transform.c @@ -0,0 +1,293 @@ +/* Copyright 2013 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +#include "platform.h" +#include "transform.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +/* RFC 7932 transforms string data */ +static const BROTLI_MODEL("small") char kPrefixSuffix[217] = + "\1 \2, \10 of the \4 of \2s \1.\5 and \4 " +/* 0x _0 _2 __5 _E _3 _6 _8 _E */ + "in \1\"\4 to \2\">\1\n\2. \1]\5 for \3 a \6 " +/* 2x _3_ _5 _A_ _D_ _F _2 _4 _A _E */ + "that \1\'\6 with \6 from \4 by \1(\6. T" +/* 4x _5_ _7 _E _5 _A _C */ + "he \4 on \4 as \4 is \4ing \2\n\t\1:\3ed " +/* 6x _3 _8 _D _2 _7_ _ _A _C */ + "\2=\"\4 at \3ly \1,\2=\'\5.com/\7. This \5" +/* 8x _0 _ _3 _8 _C _E _ _1 _7 _F */ + " not \3er \3al \4ful \4ive \5less \4es" +/* Ax _5 _9 _D _2 _7 _D */ + "t \4ize \2\xc2\xa0\4ous \5 the \2e "; /* \0 - implicit trailing zero. */ +/* Cx _2 _7___ ___ _A _F _5 _8 */ + +static const BROTLI_MODEL("small") uint16_t kPrefixSuffixMap[50] = { + 0x00, 0x02, 0x05, 0x0E, 0x13, 0x16, 0x18, 0x1E, 0x23, 0x25, + 0x2A, 0x2D, 0x2F, 0x32, 0x34, 0x3A, 0x3E, 0x45, 0x47, 0x4E, + 0x55, 0x5A, 0x5C, 0x63, 0x68, 0x6D, 0x72, 0x77, 0x7A, 0x7C, + 0x80, 0x83, 0x88, 0x8C, 0x8E, 0x91, 0x97, 0x9F, 0xA5, 0xA9, + 0xAD, 0xB2, 0xB7, 0xBD, 0xC2, 0xC7, 0xCA, 0xCF, 0xD5, 0xD8 +}; + +/* RFC 7932 transforms */ +static const BROTLI_MODEL("small") uint8_t kTransformsData[] = { + 49, BROTLI_TRANSFORM_IDENTITY, 49, + 49, BROTLI_TRANSFORM_IDENTITY, 0, + 0, BROTLI_TRANSFORM_IDENTITY, 0, + 49, BROTLI_TRANSFORM_OMIT_FIRST_1, 49, + 49, BROTLI_TRANSFORM_UPPERCASE_FIRST, 0, + 49, BROTLI_TRANSFORM_IDENTITY, 47, + 0, BROTLI_TRANSFORM_IDENTITY, 49, + 4, BROTLI_TRANSFORM_IDENTITY, 0, + 49, BROTLI_TRANSFORM_IDENTITY, 3, + 49, BROTLI_TRANSFORM_UPPERCASE_FIRST, 49, + 49, BROTLI_TRANSFORM_IDENTITY, 6, + 49, BROTLI_TRANSFORM_OMIT_FIRST_2, 49, + 49, BROTLI_TRANSFORM_OMIT_LAST_1, 49, + 1, BROTLI_TRANSFORM_IDENTITY, 0, + 49, BROTLI_TRANSFORM_IDENTITY, 1, + 0, BROTLI_TRANSFORM_UPPERCASE_FIRST, 0, + 49, BROTLI_TRANSFORM_IDENTITY, 7, + 49, BROTLI_TRANSFORM_IDENTITY, 9, + 48, BROTLI_TRANSFORM_IDENTITY, 0, + 49, BROTLI_TRANSFORM_IDENTITY, 8, + 49, BROTLI_TRANSFORM_IDENTITY, 5, + 49, BROTLI_TRANSFORM_IDENTITY, 10, + 49, BROTLI_TRANSFORM_IDENTITY, 11, + 49, BROTLI_TRANSFORM_OMIT_LAST_3, 49, + 49, BROTLI_TRANSFORM_IDENTITY, 13, + 49, BROTLI_TRANSFORM_IDENTITY, 14, + 49, BROTLI_TRANSFORM_OMIT_FIRST_3, 49, + 49, BROTLI_TRANSFORM_OMIT_LAST_2, 49, + 49, BROTLI_TRANSFORM_IDENTITY, 15, + 49, BROTLI_TRANSFORM_IDENTITY, 16, + 0, BROTLI_TRANSFORM_UPPERCASE_FIRST, 49, + 49, BROTLI_TRANSFORM_IDENTITY, 12, + 5, BROTLI_TRANSFORM_IDENTITY, 49, + 0, BROTLI_TRANSFORM_IDENTITY, 1, + 49, BROTLI_TRANSFORM_OMIT_FIRST_4, 49, + 49, BROTLI_TRANSFORM_IDENTITY, 18, + 49, BROTLI_TRANSFORM_IDENTITY, 17, + 49, BROTLI_TRANSFORM_IDENTITY, 19, + 49, BROTLI_TRANSFORM_IDENTITY, 20, + 49, BROTLI_TRANSFORM_OMIT_FIRST_5, 49, + 49, BROTLI_TRANSFORM_OMIT_FIRST_6, 49, + 47, BROTLI_TRANSFORM_IDENTITY, 49, + 49, BROTLI_TRANSFORM_OMIT_LAST_4, 49, + 49, BROTLI_TRANSFORM_IDENTITY, 22, + 49, BROTLI_TRANSFORM_UPPERCASE_ALL, 49, + 49, BROTLI_TRANSFORM_IDENTITY, 23, + 49, BROTLI_TRANSFORM_IDENTITY, 24, + 49, BROTLI_TRANSFORM_IDENTITY, 25, + 49, BROTLI_TRANSFORM_OMIT_LAST_7, 49, + 49, BROTLI_TRANSFORM_OMIT_LAST_1, 26, + 49, BROTLI_TRANSFORM_IDENTITY, 27, + 49, BROTLI_TRANSFORM_IDENTITY, 28, + 0, BROTLI_TRANSFORM_IDENTITY, 12, + 49, BROTLI_TRANSFORM_IDENTITY, 29, + 49, BROTLI_TRANSFORM_OMIT_FIRST_9, 49, + 49, BROTLI_TRANSFORM_OMIT_FIRST_7, 49, + 49, BROTLI_TRANSFORM_OMIT_LAST_6, 49, + 49, BROTLI_TRANSFORM_IDENTITY, 21, + 49, BROTLI_TRANSFORM_UPPERCASE_FIRST, 1, + 49, BROTLI_TRANSFORM_OMIT_LAST_8, 49, + 49, BROTLI_TRANSFORM_IDENTITY, 31, + 49, BROTLI_TRANSFORM_IDENTITY, 32, + 47, BROTLI_TRANSFORM_IDENTITY, 3, + 49, BROTLI_TRANSFORM_OMIT_LAST_5, 49, + 49, BROTLI_TRANSFORM_OMIT_LAST_9, 49, + 0, BROTLI_TRANSFORM_UPPERCASE_FIRST, 1, + 49, BROTLI_TRANSFORM_UPPERCASE_FIRST, 8, + 5, BROTLI_TRANSFORM_IDENTITY, 21, + 49, BROTLI_TRANSFORM_UPPERCASE_ALL, 0, + 49, BROTLI_TRANSFORM_UPPERCASE_FIRST, 10, + 49, BROTLI_TRANSFORM_IDENTITY, 30, + 0, BROTLI_TRANSFORM_IDENTITY, 5, + 35, BROTLI_TRANSFORM_IDENTITY, 49, + 47, BROTLI_TRANSFORM_IDENTITY, 2, + 49, BROTLI_TRANSFORM_UPPERCASE_FIRST, 17, + 49, BROTLI_TRANSFORM_IDENTITY, 36, + 49, BROTLI_TRANSFORM_IDENTITY, 33, + 5, BROTLI_TRANSFORM_IDENTITY, 0, + 49, BROTLI_TRANSFORM_UPPERCASE_FIRST, 21, + 49, BROTLI_TRANSFORM_UPPERCASE_FIRST, 5, + 49, BROTLI_TRANSFORM_IDENTITY, 37, + 0, BROTLI_TRANSFORM_IDENTITY, 30, + 49, BROTLI_TRANSFORM_IDENTITY, 38, + 0, BROTLI_TRANSFORM_UPPERCASE_ALL, 0, + 49, BROTLI_TRANSFORM_IDENTITY, 39, + 0, BROTLI_TRANSFORM_UPPERCASE_ALL, 49, + 49, BROTLI_TRANSFORM_IDENTITY, 34, + 49, BROTLI_TRANSFORM_UPPERCASE_ALL, 8, + 49, BROTLI_TRANSFORM_UPPERCASE_FIRST, 12, + 0, BROTLI_TRANSFORM_IDENTITY, 21, + 49, BROTLI_TRANSFORM_IDENTITY, 40, + 0, BROTLI_TRANSFORM_UPPERCASE_FIRST, 12, + 49, BROTLI_TRANSFORM_IDENTITY, 41, + 49, BROTLI_TRANSFORM_IDENTITY, 42, + 49, BROTLI_TRANSFORM_UPPERCASE_ALL, 17, + 49, BROTLI_TRANSFORM_IDENTITY, 43, + 0, BROTLI_TRANSFORM_UPPERCASE_FIRST, 5, + 49, BROTLI_TRANSFORM_UPPERCASE_ALL, 10, + 0, BROTLI_TRANSFORM_IDENTITY, 34, + 49, BROTLI_TRANSFORM_UPPERCASE_FIRST, 33, + 49, BROTLI_TRANSFORM_IDENTITY, 44, + 49, BROTLI_TRANSFORM_UPPERCASE_ALL, 5, + 45, BROTLI_TRANSFORM_IDENTITY, 49, + 0, BROTLI_TRANSFORM_IDENTITY, 33, + 49, BROTLI_TRANSFORM_UPPERCASE_FIRST, 30, + 49, BROTLI_TRANSFORM_UPPERCASE_ALL, 30, + 49, BROTLI_TRANSFORM_IDENTITY, 46, + 49, BROTLI_TRANSFORM_UPPERCASE_ALL, 1, + 49, BROTLI_TRANSFORM_UPPERCASE_FIRST, 34, + 0, BROTLI_TRANSFORM_UPPERCASE_FIRST, 33, + 0, BROTLI_TRANSFORM_UPPERCASE_ALL, 30, + 0, BROTLI_TRANSFORM_UPPERCASE_ALL, 1, + 49, BROTLI_TRANSFORM_UPPERCASE_ALL, 33, + 49, BROTLI_TRANSFORM_UPPERCASE_ALL, 21, + 49, BROTLI_TRANSFORM_UPPERCASE_ALL, 12, + 0, BROTLI_TRANSFORM_UPPERCASE_ALL, 5, + 49, BROTLI_TRANSFORM_UPPERCASE_ALL, 34, + 0, BROTLI_TRANSFORM_UPPERCASE_ALL, 12, + 0, BROTLI_TRANSFORM_UPPERCASE_FIRST, 30, + 0, BROTLI_TRANSFORM_UPPERCASE_ALL, 34, + 0, BROTLI_TRANSFORM_UPPERCASE_FIRST, 34, +}; + +static const BROTLI_MODEL("small") +BrotliTransforms kBrotliTransforms = { + sizeof(kPrefixSuffix), + (const uint8_t*)kPrefixSuffix, + kPrefixSuffixMap, + sizeof(kTransformsData) / (3 * sizeof(kTransformsData[0])), + kTransformsData, + NULL, /* no extra parameters */ + {0, 12, 27, 23, 42, 63, 56, 48, 59, 64} +}; + +const BrotliTransforms* BrotliGetTransforms(void) { + return &kBrotliTransforms; +} + +static int ToUpperCase(uint8_t* p) { + if (p[0] < 0xC0) { + if (p[0] >= 'a' && p[0] <= 'z') { + p[0] ^= 32; + } + return 1; + } + /* An overly simplified uppercasing model for UTF-8. */ + if (p[0] < 0xE0) { + p[1] ^= 32; + return 2; + } + /* An arbitrary transform for three byte characters. */ + p[2] ^= 5; + return 3; +} + +static int Shift(uint8_t* word, int word_len, uint16_t parameter) { + /* Limited sign extension: scalar < (1 << 24). */ + uint32_t scalar = + (parameter & 0x7FFFu) + (0x1000000u - (parameter & 0x8000u)); + if (word[0] < 0x80) { + /* 1-byte rune / 0sssssss / 7 bit scalar (ASCII). */ + scalar += (uint32_t)word[0]; + word[0] = (uint8_t)(scalar & 0x7Fu); + return 1; + } else if (word[0] < 0xC0) { + /* Continuation / 10AAAAAA. */ + return 1; + } else if (word[0] < 0xE0) { + /* 2-byte rune / 110sssss AAssssss / 11 bit scalar. */ + if (word_len < 2) return 1; + scalar += (uint32_t)((word[1] & 0x3Fu) | ((word[0] & 0x1Fu) << 6u)); + word[0] = (uint8_t)(0xC0 | ((scalar >> 6u) & 0x1F)); + word[1] = (uint8_t)((word[1] & 0xC0) | (scalar & 0x3F)); + return 2; + } else if (word[0] < 0xF0) { + /* 3-byte rune / 1110ssss AAssssss BBssssss / 16 bit scalar. */ + if (word_len < 3) return word_len; + scalar += (uint32_t)((word[2] & 0x3Fu) | ((word[1] & 0x3Fu) << 6u) | + ((word[0] & 0x0Fu) << 12u)); + word[0] = (uint8_t)(0xE0 | ((scalar >> 12u) & 0x0F)); + word[1] = (uint8_t)((word[1] & 0xC0) | ((scalar >> 6u) & 0x3F)); + word[2] = (uint8_t)((word[2] & 0xC0) | (scalar & 0x3F)); + return 3; + } else if (word[0] < 0xF8) { + /* 4-byte rune / 11110sss AAssssss BBssssss CCssssss / 21 bit scalar. */ + if (word_len < 4) return word_len; + scalar += (uint32_t)((word[3] & 0x3Fu) | ((word[2] & 0x3Fu) << 6u) | + ((word[1] & 0x3Fu) << 12u) | ((word[0] & 0x07u) << 18u)); + word[0] = (uint8_t)(0xF0 | ((scalar >> 18u) & 0x07)); + word[1] = (uint8_t)((word[1] & 0xC0) | ((scalar >> 12u) & 0x3F)); + word[2] = (uint8_t)((word[2] & 0xC0) | ((scalar >> 6u) & 0x3F)); + word[3] = (uint8_t)((word[3] & 0xC0) | (scalar & 0x3F)); + return 4; + } + return 1; +} + +int BrotliTransformDictionaryWord(uint8_t* dst, const uint8_t* word, int len, + const BrotliTransforms* transforms, int transform_idx) { + int idx = 0; + const uint8_t* prefix = BROTLI_TRANSFORM_PREFIX(transforms, transform_idx); + uint8_t type = BROTLI_TRANSFORM_TYPE(transforms, transform_idx); + const uint8_t* suffix = BROTLI_TRANSFORM_SUFFIX(transforms, transform_idx); + { + int prefix_len = *prefix++; + while (prefix_len--) { dst[idx++] = *prefix++; } + } + { + const int t = type; + int i = 0; + if (t <= BROTLI_TRANSFORM_OMIT_LAST_9) { + len -= t; + } else if (t >= BROTLI_TRANSFORM_OMIT_FIRST_1 + && t <= BROTLI_TRANSFORM_OMIT_FIRST_9) { + int skip = t - (BROTLI_TRANSFORM_OMIT_FIRST_1 - 1); + word += skip; + len -= skip; + } + while (i < len) { dst[idx++] = word[i++]; } + if (t == BROTLI_TRANSFORM_UPPERCASE_FIRST) { + ToUpperCase(&dst[idx - len]); + } else if (t == BROTLI_TRANSFORM_UPPERCASE_ALL) { + uint8_t* uppercase = &dst[idx - len]; + while (len > 0) { + int step = ToUpperCase(uppercase); + uppercase += step; + len -= step; + } + } else if (t == BROTLI_TRANSFORM_SHIFT_FIRST) { + uint16_t param = (uint16_t)(transforms->params[transform_idx * 2] + + (transforms->params[transform_idx * 2 + 1] << 8u)); + Shift(&dst[idx - len], len, param); + } else if (t == BROTLI_TRANSFORM_SHIFT_ALL) { + uint16_t param = (uint16_t)(transforms->params[transform_idx * 2] + + (transforms->params[transform_idx * 2 + 1] << 8u)); + uint8_t* shift = &dst[idx - len]; + while (len > 0) { + int step = Shift(shift, len, param); + shift += step; + len -= step; + } + } + } + { + int suffix_len = *suffix++; + while (suffix_len--) { dst[idx++] = *suffix++; } + return idx; + } +} + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif diff --git a/packages/runtime/vendor/brotli/c/common/transform.h b/packages/runtime/vendor/brotli/c/common/transform.h new file mode 100644 index 000000000..c6176d834 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/common/transform.h @@ -0,0 +1,84 @@ +/* transforms is a part of ABI, but not API. + + It means that there are some functions that are supposed to be in "common" + library, but header itself is not placed into include/brotli. This way, + aforementioned functions will be available only to brotli internals. + */ + +#ifndef BROTLI_COMMON_TRANSFORM_H_ +#define BROTLI_COMMON_TRANSFORM_H_ + +#include "platform.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +enum BrotliWordTransformType { + BROTLI_TRANSFORM_IDENTITY = 0, + BROTLI_TRANSFORM_OMIT_LAST_1 = 1, + BROTLI_TRANSFORM_OMIT_LAST_2 = 2, + BROTLI_TRANSFORM_OMIT_LAST_3 = 3, + BROTLI_TRANSFORM_OMIT_LAST_4 = 4, + BROTLI_TRANSFORM_OMIT_LAST_5 = 5, + BROTLI_TRANSFORM_OMIT_LAST_6 = 6, + BROTLI_TRANSFORM_OMIT_LAST_7 = 7, + BROTLI_TRANSFORM_OMIT_LAST_8 = 8, + BROTLI_TRANSFORM_OMIT_LAST_9 = 9, + BROTLI_TRANSFORM_UPPERCASE_FIRST = 10, + BROTLI_TRANSFORM_UPPERCASE_ALL = 11, + BROTLI_TRANSFORM_OMIT_FIRST_1 = 12, + BROTLI_TRANSFORM_OMIT_FIRST_2 = 13, + BROTLI_TRANSFORM_OMIT_FIRST_3 = 14, + BROTLI_TRANSFORM_OMIT_FIRST_4 = 15, + BROTLI_TRANSFORM_OMIT_FIRST_5 = 16, + BROTLI_TRANSFORM_OMIT_FIRST_6 = 17, + BROTLI_TRANSFORM_OMIT_FIRST_7 = 18, + BROTLI_TRANSFORM_OMIT_FIRST_8 = 19, + BROTLI_TRANSFORM_OMIT_FIRST_9 = 20, + BROTLI_TRANSFORM_SHIFT_FIRST = 21, + BROTLI_TRANSFORM_SHIFT_ALL = 22, + BROTLI_NUM_TRANSFORM_TYPES /* Counts transforms, not a transform itself. */ +}; + +#define BROTLI_TRANSFORMS_MAX_CUT_OFF BROTLI_TRANSFORM_OMIT_LAST_9 + +typedef struct BrotliTransforms { + uint16_t prefix_suffix_size; + /* Last character must be null, so prefix_suffix_size must be at least 1. */ + const uint8_t* prefix_suffix; + const uint16_t* prefix_suffix_map; + uint32_t num_transforms; + /* Each entry is a [prefix_id, transform, suffix_id] triplet. */ + const uint8_t* transforms; + /* Shift for BROTLI_TRANSFORM_SHIFT_FIRST and BROTLI_TRANSFORM_SHIFT_ALL, + must be NULL if and only if no such transforms are present. */ + const uint8_t* params; + /* Indices of transforms like ["", BROTLI_TRANSFORM_OMIT_LAST_#, ""]. + 0-th element corresponds to ["", BROTLI_TRANSFORM_IDENTITY, ""]. + -1, if cut-off transform does not exist. */ + int16_t cutOffTransforms[BROTLI_TRANSFORMS_MAX_CUT_OFF + 1]; +} BrotliTransforms; + +/* T is BrotliTransforms*; result is uint8_t. */ +#define BROTLI_TRANSFORM_PREFIX_ID(T, I) ((T)->transforms[((I) * 3) + 0]) +#define BROTLI_TRANSFORM_TYPE(T, I) ((T)->transforms[((I) * 3) + 1]) +#define BROTLI_TRANSFORM_SUFFIX_ID(T, I) ((T)->transforms[((I) * 3) + 2]) + +/* T is BrotliTransforms*; result is const uint8_t*. */ +#define BROTLI_TRANSFORM_PREFIX(T, I) (&(T)->prefix_suffix[ \ + (T)->prefix_suffix_map[BROTLI_TRANSFORM_PREFIX_ID(T, I)]]) +#define BROTLI_TRANSFORM_SUFFIX(T, I) (&(T)->prefix_suffix[ \ + (T)->prefix_suffix_map[BROTLI_TRANSFORM_SUFFIX_ID(T, I)]]) + +BROTLI_COMMON_API const BrotliTransforms* BrotliGetTransforms(void); + +BROTLI_COMMON_API int BrotliTransformDictionaryWord( + uint8_t* dst, const uint8_t* word, int len, + const BrotliTransforms* transforms, int transform_idx); + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif + +#endif /* BROTLI_COMMON_TRANSFORM_H_ */ diff --git a/packages/runtime/vendor/brotli/c/common/version.h b/packages/runtime/vendor/brotli/c/common/version.h new file mode 100644 index 000000000..0d6fcc50a --- /dev/null +++ b/packages/runtime/vendor/brotli/c/common/version.h @@ -0,0 +1,51 @@ +/* Copyright 2016 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* Version definition. */ + +#ifndef BROTLI_COMMON_VERSION_H_ +#define BROTLI_COMMON_VERSION_H_ + +/* Compose 3 components into a single number. In a hexadecimal representation + B and C components occupy exactly 3 digits. */ +#define BROTLI_MAKE_HEX_VERSION(A, B, C) ((A << 24) | (B << 12) | C) + +/* Those macros should only be used when library is compiled together with + the client. If library is dynamically linked, use BrotliDecoderVersion and + BrotliEncoderVersion methods. */ + +#define BROTLI_VERSION_MAJOR 1 +#define BROTLI_VERSION_MINOR 2 +#define BROTLI_VERSION_PATCH 0 + +#define BROTLI_VERSION BROTLI_MAKE_HEX_VERSION( \ + BROTLI_VERSION_MAJOR, BROTLI_VERSION_MINOR, BROTLI_VERSION_PATCH) + +/* This macro is used by build system to produce Libtool-friendly soname. See + https://www.gnu.org/software/libtool/manual/html_node/Libtool-versioning.html + Version evolution rules: + - interfaces added (or change is compatible) -> current+1:0:age+1 + - interfaces removed (or changed is incompatible) -> current+1:0:0 + - interfaces not changed -> current:revision+1:age + */ + +#define BROTLI_ABI_CURRENT 3 +#define BROTLI_ABI_REVISION 0 +#define BROTLI_ABI_AGE 2 + +#if BROTLI_VERSION_MAJOR != (BROTLI_ABI_CURRENT - BROTLI_ABI_AGE) +#error ABI/API version inconsistency +#endif + +#if BROTLI_VERSION_MINOR != BROTLI_ABI_AGE +#error ABI/API version inconsistency +#endif + +#if BROTLI_VERSION_PATCH != BROTLI_ABI_REVISION +#error ABI/API version inconsistency +#endif + +#endif /* BROTLI_COMMON_VERSION_H_ */ diff --git a/packages/runtime/vendor/brotli/c/dec/bit_reader.c b/packages/runtime/vendor/brotli/c/dec/bit_reader.c new file mode 100644 index 000000000..fae034fd8 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/dec/bit_reader.c @@ -0,0 +1,77 @@ +/* Copyright 2013 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* Bit reading helpers */ + +#include "bit_reader.h" + +#include "../common/platform.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +const BROTLI_MODEL("small") +brotli_reg_t kBrotliBitMask[33] = { 0x00000000, + 0x00000001, 0x00000003, 0x00000007, 0x0000000F, + 0x0000001F, 0x0000003F, 0x0000007F, 0x000000FF, + 0x000001FF, 0x000003FF, 0x000007FF, 0x00000FFF, + 0x00001FFF, 0x00003FFF, 0x00007FFF, 0x0000FFFF, + 0x0001FFFF, 0x0003FFFF, 0x0007FFFF, 0x000FFFFF, + 0x001FFFFF, 0x003FFFFF, 0x007FFFFF, 0x00FFFFFF, + 0x01FFFFFF, 0x03FFFFFF, 0x07FFFFFF, 0x0FFFFFFF, + 0x1FFFFFFF, 0x3FFFFFFF, 0x7FFFFFFF, 0xFFFFFFFF +}; + +void BrotliInitBitReader(BrotliBitReader* const br) { + br->val_ = 0; + br->bit_pos_ = 0; +} + +BROTLI_BOOL BrotliWarmupBitReader(BrotliBitReader* const br) { + size_t aligned_read_mask = (sizeof(br->val_) >> 1) - 1; + /* Fixing alignment after unaligned BrotliFillWindow would result accumulator + overflow. If unalignment is caused by BrotliSafeReadBits, then there is + enough space in accumulator to fix alignment. */ + if (BROTLI_UNALIGNED_READ_FAST) { + aligned_read_mask = 0; + } + if (BrotliGetAvailableBits(br) == 0) { + br->val_ = 0; + if (!BrotliPullByte(br)) { + return BROTLI_FALSE; + } + } + + while ((((size_t)br->next_in) & aligned_read_mask) != 0) { + if (!BrotliPullByte(br)) { + /* If we consumed all the input, we don't care about the alignment. */ + return BROTLI_TRUE; + } + } + return BROTLI_TRUE; +} + +BROTLI_BOOL BrotliSafeReadBits32Slow(BrotliBitReader* const br, + brotli_reg_t n_bits, brotli_reg_t* val) { + brotli_reg_t low_val; + brotli_reg_t high_val; + BrotliBitReaderState memento; + BROTLI_DCHECK(n_bits <= 32); + BROTLI_DCHECK(n_bits > 24); + BrotliBitReaderSaveState(br, &memento); + if (!BrotliSafeReadBits(br, 16, &low_val) || + !BrotliSafeReadBits(br, n_bits - 16, &high_val)) { + BrotliBitReaderRestoreState(br, &memento); + return BROTLI_FALSE; + } + *val = low_val | (high_val << 16); + return BROTLI_TRUE; +} + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif diff --git a/packages/runtime/vendor/brotli/c/dec/bit_reader.h b/packages/runtime/vendor/brotli/c/dec/bit_reader.h new file mode 100644 index 000000000..ef6b047ae --- /dev/null +++ b/packages/runtime/vendor/brotli/c/dec/bit_reader.h @@ -0,0 +1,419 @@ +/* Copyright 2013 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* Bit reading helpers */ + +#ifndef BROTLI_DEC_BIT_READER_H_ +#define BROTLI_DEC_BIT_READER_H_ + +#include "../common/constants.h" +#include "../common/platform.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +#define BROTLI_SHORT_FILL_BIT_WINDOW_READ (sizeof(brotli_reg_t) >> 1) + +/* 162 bits + 7 bytes */ +#define BROTLI_FAST_INPUT_SLACK 28 + +BROTLI_INTERNAL extern const brotli_reg_t kBrotliBitMask[33]; + +static BROTLI_INLINE brotli_reg_t BitMask(brotli_reg_t n) { + if (BROTLI_IS_CONSTANT(n) || BROTLI_HAS_UBFX) { + /* Masking with this expression turns to a single + "Unsigned Bit Field Extract" UBFX instruction on ARM. */ + return ~(~((brotli_reg_t)0) << n); + } else { + return kBrotliBitMask[n]; + } +} + +typedef struct { + brotli_reg_t val_; /* pre-fetched bits */ + brotli_reg_t bit_pos_; /* current bit-reading position in val_ */ + const uint8_t* next_in; /* the byte we're reading from */ + const uint8_t* guard_in; /* position from which "fast-path" is prohibited */ + const uint8_t* last_in; /* == next_in + avail_in */ +} BrotliBitReader; + +typedef struct { + brotli_reg_t val_; + brotli_reg_t bit_pos_; + const uint8_t* next_in; + size_t avail_in; +} BrotliBitReaderState; + +/* Initializes the BrotliBitReader fields. */ +BROTLI_INTERNAL void BrotliInitBitReader(BrotliBitReader* br); + +/* Ensures that accumulator is not empty. + May consume up to sizeof(brotli_reg_t) - 1 bytes of input. + Returns BROTLI_FALSE if data is required but there is no input available. + For !BROTLI_UNALIGNED_READ_FAST this function also prepares bit reader for + aligned reading. */ +BROTLI_INTERNAL BROTLI_BOOL BrotliWarmupBitReader(BrotliBitReader* br); + +/* Fallback for BrotliSafeReadBits32. Extracted as noninlined method to unburden + the main code-path. Never called for RFC brotli streams, required only for + "large-window" mode and other extensions. */ +BROTLI_INTERNAL BROTLI_NOINLINE BROTLI_BOOL BrotliSafeReadBits32Slow( + BrotliBitReader* br, brotli_reg_t n_bits, brotli_reg_t* val); + +static BROTLI_INLINE size_t +BrotliBitReaderGetAvailIn(BrotliBitReader* const br) { + return (size_t)(br->last_in - br->next_in); +} + +static BROTLI_INLINE void BrotliBitReaderSaveState( + BrotliBitReader* const from, BrotliBitReaderState* to) { + to->val_ = from->val_; + to->bit_pos_ = from->bit_pos_; + to->next_in = from->next_in; + to->avail_in = BrotliBitReaderGetAvailIn(from); +} + +static BROTLI_INLINE void BrotliBitReaderSetInput( + BrotliBitReader* const br, const uint8_t* next_in, size_t avail_in) { + br->next_in = next_in; + br->last_in = (avail_in == 0) ? next_in : (next_in + avail_in); + if (avail_in + 1 > BROTLI_FAST_INPUT_SLACK) { + br->guard_in = next_in + (avail_in + 1 - BROTLI_FAST_INPUT_SLACK); + } else { + br->guard_in = next_in; + } +} + +static BROTLI_INLINE void BrotliBitReaderRestoreState( + BrotliBitReader* const to, BrotliBitReaderState* from) { + to->val_ = from->val_; + to->bit_pos_ = from->bit_pos_; + to->next_in = from->next_in; + BrotliBitReaderSetInput(to, from->next_in, from->avail_in); +} + +static BROTLI_INLINE brotli_reg_t BrotliGetAvailableBits( + const BrotliBitReader* br) { + return br->bit_pos_; +} + +/* Returns amount of unread bytes the bit reader still has buffered from the + BrotliInput, including whole bytes in br->val_. Result is capped with + maximal ring-buffer size (larger number won't be utilized anyway). */ +static BROTLI_INLINE size_t BrotliGetRemainingBytes(BrotliBitReader* br) { + static const size_t kCap = (size_t)1 << BROTLI_LARGE_MAX_WBITS; + size_t avail_in = BrotliBitReaderGetAvailIn(br); + if (avail_in > kCap) return kCap; + return avail_in + (BrotliGetAvailableBits(br) >> 3); +} + +/* Checks if there is at least |num| bytes left in the input ring-buffer + (excluding the bits remaining in br->val_). */ +static BROTLI_INLINE BROTLI_BOOL BrotliCheckInputAmount( + BrotliBitReader* const br) { + return TO_BROTLI_BOOL(br->next_in < br->guard_in); +} + +/* Load more bits into accumulator. */ +static BROTLI_INLINE brotli_reg_t BrotliBitReaderLoadBits(brotli_reg_t val, + brotli_reg_t new_bits, + brotli_reg_t count, + brotli_reg_t offset) { + BROTLI_DCHECK( + !((val >> offset) & ~new_bits & ~(~((brotli_reg_t)0) << count))); + (void)count; + return val | (new_bits << offset); +} + +/* Guarantees that there are at least |n_bits| + 1 bits in accumulator. + Precondition: accumulator contains at least 1 bit. + |n_bits| should be in the range [1..24] for regular build. For portable + non-64-bit little-endian build only 16 bits are safe to request. */ +static BROTLI_INLINE void BrotliFillBitWindow( + BrotliBitReader* const br, brotli_reg_t n_bits) { +#if (BROTLI_64_BITS) + if (BROTLI_UNALIGNED_READ_FAST && BROTLI_IS_CONSTANT(n_bits) && + (n_bits <= 8)) { + brotli_reg_t bit_pos = br->bit_pos_; + if (bit_pos <= 8) { + br->val_ = BrotliBitReaderLoadBits(br->val_, + BROTLI_UNALIGNED_LOAD64LE(br->next_in), 56, bit_pos); + br->bit_pos_ = bit_pos + 56; + br->next_in += 7; + } + } else if (BROTLI_UNALIGNED_READ_FAST && BROTLI_IS_CONSTANT(n_bits) && + (n_bits <= 16)) { + brotli_reg_t bit_pos = br->bit_pos_; + if (bit_pos <= 16) { + br->val_ = BrotliBitReaderLoadBits(br->val_, + BROTLI_UNALIGNED_LOAD64LE(br->next_in), 48, bit_pos); + br->bit_pos_ = bit_pos + 48; + br->next_in += 6; + } + } else { + brotli_reg_t bit_pos = br->bit_pos_; + if (bit_pos <= 32) { + br->val_ = BrotliBitReaderLoadBits(br->val_, + (uint64_t)BROTLI_UNALIGNED_LOAD32LE(br->next_in), 32, bit_pos); + br->bit_pos_ = bit_pos + 32; + br->next_in += BROTLI_SHORT_FILL_BIT_WINDOW_READ; + } + } +#else + if (BROTLI_UNALIGNED_READ_FAST && BROTLI_IS_CONSTANT(n_bits) && + (n_bits <= 8)) { + brotli_reg_t bit_pos = br->bit_pos_; + if (bit_pos <= 8) { + br->val_ = BrotliBitReaderLoadBits(br->val_, + BROTLI_UNALIGNED_LOAD32LE(br->next_in), 24, bit_pos); + br->bit_pos_ = bit_pos + 24; + br->next_in += 3; + } + } else { + brotli_reg_t bit_pos = br->bit_pos_; + if (bit_pos <= 16) { + br->val_ = BrotliBitReaderLoadBits(br->val_, + (uint32_t)BROTLI_UNALIGNED_LOAD16LE(br->next_in), 16, bit_pos); + br->bit_pos_ = bit_pos + 16; + br->next_in += BROTLI_SHORT_FILL_BIT_WINDOW_READ; + } + } +#endif +} + +/* Mostly like BrotliFillBitWindow, but guarantees only 16 bits and reads no + more than BROTLI_SHORT_FILL_BIT_WINDOW_READ bytes of input. */ +static BROTLI_INLINE void BrotliFillBitWindow16(BrotliBitReader* const br) { + BrotliFillBitWindow(br, 17); +} + +/* Tries to pull one byte of input to accumulator. + Returns BROTLI_FALSE if there is no input available. */ +static BROTLI_INLINE BROTLI_BOOL BrotliPullByte(BrotliBitReader* const br) { + if (br->next_in == br->last_in) { + return BROTLI_FALSE; + } + br->val_ = BrotliBitReaderLoadBits(br->val_, + (brotli_reg_t)*br->next_in, 8, br->bit_pos_); + br->bit_pos_ += 8; + ++br->next_in; + return BROTLI_TRUE; +} + +/* Returns currently available bits. + The number of valid bits could be calculated by BrotliGetAvailableBits. */ +static BROTLI_INLINE brotli_reg_t BrotliGetBitsUnmasked( + BrotliBitReader* const br) { + return br->val_; +} + +/* Like BrotliGetBits, but does not mask the result. + The result contains at least 16 valid bits. */ +static BROTLI_INLINE brotli_reg_t BrotliGet16BitsUnmasked( + BrotliBitReader* const br) { + BrotliFillBitWindow(br, 16); + return (brotli_reg_t)BrotliGetBitsUnmasked(br); +} + +/* Returns the specified number of bits from |br| without advancing bit + position. */ +static BROTLI_INLINE brotli_reg_t BrotliGetBits( + BrotliBitReader* const br, brotli_reg_t n_bits) { + BrotliFillBitWindow(br, n_bits); + return BrotliGetBitsUnmasked(br) & BitMask(n_bits); +} + +/* Tries to peek the specified amount of bits. Returns BROTLI_FALSE, if there + is not enough input. */ +static BROTLI_INLINE BROTLI_BOOL BrotliSafeGetBits( + BrotliBitReader* const br, brotli_reg_t n_bits, brotli_reg_t* val) { + while (BrotliGetAvailableBits(br) < n_bits) { + if (!BrotliPullByte(br)) { + return BROTLI_FALSE; + } + } + *val = BrotliGetBitsUnmasked(br) & BitMask(n_bits); + return BROTLI_TRUE; +} + +/* Advances the bit pos by |n_bits|. */ +static BROTLI_INLINE void BrotliDropBits( + BrotliBitReader* const br, brotli_reg_t n_bits) { + br->bit_pos_ -= n_bits; + br->val_ >>= n_bits; +} + +/* Make sure that there are no spectre bits in accumulator. + This is important for the cases when some bytes are skipped + (i.e. never placed into accumulator). */ +static BROTLI_INLINE void BrotliBitReaderNormalize(BrotliBitReader* br) { + /* Actually, it is enough to normalize when br->bit_pos_ == 0 */ + if (br->bit_pos_ < (sizeof(brotli_reg_t) << 3u)) { + br->val_ &= (((brotli_reg_t)1) << br->bit_pos_) - 1; + } +} + +static BROTLI_INLINE void BrotliBitReaderUnload(BrotliBitReader* br) { + brotli_reg_t unused_bytes = BrotliGetAvailableBits(br) >> 3; + brotli_reg_t unused_bits = unused_bytes << 3; + br->next_in = + (unused_bytes == 0) ? br->next_in : (br->next_in - unused_bytes); + br->bit_pos_ -= unused_bits; + BrotliBitReaderNormalize(br); +} + +/* Reads the specified number of bits from |br| and advances the bit pos. + Precondition: accumulator MUST contain at least |n_bits|. */ +static BROTLI_INLINE void BrotliTakeBits(BrotliBitReader* const br, + brotli_reg_t n_bits, + brotli_reg_t* val) { + *val = BrotliGetBitsUnmasked(br) & BitMask(n_bits); + BROTLI_LOG(("[BrotliTakeBits] %d %d %d val: %6x\n", + (int)BrotliBitReaderGetAvailIn(br), (int)br->bit_pos_, + (int)n_bits, (int)*val)); + BrotliDropBits(br, n_bits); +} + +/* Reads the specified number of bits from |br| and advances the bit pos. + Assumes that there is enough input to perform BrotliFillBitWindow. + Up to 24 bits are allowed to be requested from this method. */ +static BROTLI_INLINE brotli_reg_t BrotliReadBits24( + BrotliBitReader* const br, brotli_reg_t n_bits) { + BROTLI_DCHECK(n_bits <= 24); + if (BROTLI_64_BITS || (n_bits <= 16)) { + brotli_reg_t val; + BrotliFillBitWindow(br, n_bits); + BrotliTakeBits(br, n_bits, &val); + return val; + } else { + brotli_reg_t low_val; + brotli_reg_t high_val; + BrotliFillBitWindow(br, 16); + BrotliTakeBits(br, 16, &low_val); + BrotliFillBitWindow(br, 8); + BrotliTakeBits(br, n_bits - 16, &high_val); + return low_val | (high_val << 16); + } +} + +/* Same as BrotliReadBits24, but allows reading up to 32 bits. */ +static BROTLI_INLINE brotli_reg_t BrotliReadBits32( + BrotliBitReader* const br, brotli_reg_t n_bits) { + BROTLI_DCHECK(n_bits <= 32); + if (BROTLI_64_BITS || (n_bits <= 16)) { + brotli_reg_t val; + BrotliFillBitWindow(br, n_bits); + BrotliTakeBits(br, n_bits, &val); + return val; + } else { + brotli_reg_t low_val; + brotli_reg_t high_val; + BrotliFillBitWindow(br, 16); + BrotliTakeBits(br, 16, &low_val); + BrotliFillBitWindow(br, 16); + BrotliTakeBits(br, n_bits - 16, &high_val); + return low_val | (high_val << 16); + } +} + +/* Tries to read the specified amount of bits. Returns BROTLI_FALSE, if there + is not enough input. |n_bits| MUST be positive. + Up to 24 bits are allowed to be requested from this method. */ +static BROTLI_INLINE BROTLI_BOOL BrotliSafeReadBits( + BrotliBitReader* const br, brotli_reg_t n_bits, brotli_reg_t* val) { + BROTLI_DCHECK(n_bits <= 24); + while (BrotliGetAvailableBits(br) < n_bits) { + if (!BrotliPullByte(br)) { + return BROTLI_FALSE; + } + } + BrotliTakeBits(br, n_bits, val); + return BROTLI_TRUE; +} + +/* Same as BrotliSafeReadBits, but allows reading up to 32 bits. */ +static BROTLI_INLINE BROTLI_BOOL BrotliSafeReadBits32( + BrotliBitReader* const br, brotli_reg_t n_bits, brotli_reg_t* val) { + BROTLI_DCHECK(n_bits <= 32); + if (BROTLI_64_BITS || (n_bits <= 24)) { + while (BrotliGetAvailableBits(br) < n_bits) { + if (!BrotliPullByte(br)) { + return BROTLI_FALSE; + } + } + BrotliTakeBits(br, n_bits, val); + return BROTLI_TRUE; + } else { + return BrotliSafeReadBits32Slow(br, n_bits, val); + } +} + +/* Advances the bit reader position to the next byte boundary and verifies + that any skipped bits are set to zero. */ +static BROTLI_INLINE BROTLI_BOOL BrotliJumpToByteBoundary(BrotliBitReader* br) { + brotli_reg_t pad_bits_count = BrotliGetAvailableBits(br) & 0x7; + brotli_reg_t pad_bits = 0; + if (pad_bits_count != 0) { + BrotliTakeBits(br, pad_bits_count, &pad_bits); + } + BrotliBitReaderNormalize(br); + return TO_BROTLI_BOOL(pad_bits == 0); +} + +static BROTLI_INLINE void BrotliDropBytes(BrotliBitReader* br, size_t num) { + /* Check detour is legal: accumulator must to be empty. */ + BROTLI_DCHECK(br->bit_pos_ == 0); + BROTLI_DCHECK(br->val_ == 0); + br->next_in += num; +} + +/* Copies remaining input bytes stored in the bit reader to the output. Value + |num| may not be larger than BrotliGetRemainingBytes. The bit reader must be + warmed up again after this. */ +static BROTLI_INLINE void BrotliCopyBytes(uint8_t* dest, + BrotliBitReader* br, size_t num) { + while (BrotliGetAvailableBits(br) >= 8 && num > 0) { + *dest = (uint8_t)BrotliGetBitsUnmasked(br); + BrotliDropBits(br, 8); + ++dest; + --num; + } + BrotliBitReaderNormalize(br); + if (num > 0) { + memcpy(dest, br->next_in, num); + BrotliDropBytes(br, num); + } +} + +BROTLI_UNUSED_FUNCTION void BrotliBitReaderSuppressUnusedFunctions(void) { + BROTLI_UNUSED(&BrotliBitReaderSuppressUnusedFunctions); + + BROTLI_UNUSED(&BrotliBitReaderGetAvailIn); + BROTLI_UNUSED(&BrotliBitReaderLoadBits); + BROTLI_UNUSED(&BrotliBitReaderRestoreState); + BROTLI_UNUSED(&BrotliBitReaderSaveState); + BROTLI_UNUSED(&BrotliBitReaderSetInput); + BROTLI_UNUSED(&BrotliBitReaderUnload); + BROTLI_UNUSED(&BrotliCheckInputAmount); + BROTLI_UNUSED(&BrotliCopyBytes); + BROTLI_UNUSED(&BrotliFillBitWindow16); + BROTLI_UNUSED(&BrotliGet16BitsUnmasked); + BROTLI_UNUSED(&BrotliGetBits); + BROTLI_UNUSED(&BrotliGetRemainingBytes); + BROTLI_UNUSED(&BrotliJumpToByteBoundary); + BROTLI_UNUSED(&BrotliReadBits24); + BROTLI_UNUSED(&BrotliReadBits32); + BROTLI_UNUSED(&BrotliSafeGetBits); + BROTLI_UNUSED(&BrotliSafeReadBits); + BROTLI_UNUSED(&BrotliSafeReadBits32); +} + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif + +#endif /* BROTLI_DEC_BIT_READER_H_ */ diff --git a/packages/runtime/vendor/brotli/c/dec/decode.c b/packages/runtime/vendor/brotli/c/dec/decode.c new file mode 100644 index 000000000..e87e7f5ea --- /dev/null +++ b/packages/runtime/vendor/brotli/c/dec/decode.c @@ -0,0 +1,2964 @@ +/* Copyright 2013 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +#include + +#include "../common/constants.h" +#include "../common/context.h" +#include "../common/dictionary.h" +#include "../common/platform.h" +#include "../common/shared_dictionary_internal.h" +#include "../common/transform.h" +#include "../common/version.h" +#include "bit_reader.h" +#include "huffman.h" +#include "prefix.h" +#include "state.h" +#include "static_init.h" + +#if defined(BROTLI_TARGET_NEON) +#include +#endif + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +#define BROTLI_FAILURE(CODE) (BROTLI_DUMP(), CODE) + +#define BROTLI_LOG_UINT(name) \ + BROTLI_LOG(("[%s] %s = %lu\n", __func__, #name, (unsigned long)(name))) +#define BROTLI_LOG_ARRAY_INDEX(array_name, idx) \ + BROTLI_LOG(("[%s] %s[%lu] = %lu\n", __func__, #array_name, \ + (unsigned long)(idx), (unsigned long)array_name[idx])) + +#define HUFFMAN_TABLE_BITS 8U +#define HUFFMAN_TABLE_MASK 0xFF + +/* We need the slack region for the following reasons: + - doing up to two 16-byte copies for fast backward copying + - inserting transformed dictionary word: + 255 prefix + 32 base + 255 suffix */ +static const brotli_reg_t kRingBufferWriteAheadSlack = 542; + +static const BROTLI_MODEL("small") +uint8_t kCodeLengthCodeOrder[BROTLI_CODE_LENGTH_CODES] = { + 1, 2, 3, 4, 0, 5, 17, 6, 16, 7, 8, 9, 10, 11, 12, 13, 14, 15, +}; + +/* Static prefix code for the complex code length code lengths. */ +static const BROTLI_MODEL("small") +uint8_t kCodeLengthPrefixLength[16] = { + 2, 2, 2, 3, 2, 2, 2, 4, 2, 2, 2, 3, 2, 2, 2, 4, +}; + +static const BROTLI_MODEL("small") +uint8_t kCodeLengthPrefixValue[16] = { + 0, 4, 3, 2, 0, 4, 3, 1, 0, 4, 3, 2, 0, 4, 3, 5, +}; + +BROTLI_BOOL BrotliDecoderSetParameter( + BrotliDecoderState* state, BrotliDecoderParameter p, uint32_t value) { + if (state->state != BROTLI_STATE_UNINITED) return BROTLI_FALSE; + switch (p) { + case BROTLI_DECODER_PARAM_DISABLE_RING_BUFFER_REALLOCATION: + state->canny_ringbuffer_allocation = !!value ? 0 : 1; + return BROTLI_TRUE; + + case BROTLI_DECODER_PARAM_LARGE_WINDOW: + state->large_window = TO_BROTLI_BOOL(!!value); + return BROTLI_TRUE; + + default: return BROTLI_FALSE; + } +} + +BrotliDecoderState* BrotliDecoderCreateInstance( + brotli_alloc_func alloc_func, brotli_free_func free_func, void* opaque) { + BrotliDecoderState* state = 0; + if (!BrotliDecoderEnsureStaticInit()) { + BROTLI_DUMP(); + return 0; + } + if (!alloc_func && !free_func) { + state = (BrotliDecoderState*)malloc(sizeof(BrotliDecoderState)); + } else if (alloc_func && free_func) { + state = (BrotliDecoderState*)alloc_func(opaque, sizeof(BrotliDecoderState)); + } + if (state == 0) { + BROTLI_DUMP(); + return 0; + } + if (!BrotliDecoderStateInit(state, alloc_func, free_func, opaque)) { + BROTLI_DUMP(); + if (!alloc_func && !free_func) { + free(state); + } else if (alloc_func && free_func) { + free_func(opaque, state); + } + return 0; + } + return state; +} + +/* Deinitializes and frees BrotliDecoderState instance. */ +void BrotliDecoderDestroyInstance(BrotliDecoderState* state) { + if (!state) { + return; + } else { + brotli_free_func free_func = state->free_func; + void* opaque = state->memory_manager_opaque; + BrotliDecoderStateCleanup(state); + free_func(opaque, state); + } +} + +/* Saves error code and converts it to BrotliDecoderResult. */ +static BROTLI_NOINLINE BrotliDecoderResult SaveErrorCode( + BrotliDecoderState* s, BrotliDecoderErrorCode e, size_t consumed_input) { + s->error_code = (int)e; + s->used_input += consumed_input; + if ((s->buffer_length != 0) && (s->br.next_in == s->br.last_in)) { + /* If internal buffer is depleted at last, reset it. */ + s->buffer_length = 0; + } + switch (e) { + case BROTLI_DECODER_SUCCESS: + return BROTLI_DECODER_RESULT_SUCCESS; + + case BROTLI_DECODER_NEEDS_MORE_INPUT: + return BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT; + + case BROTLI_DECODER_NEEDS_MORE_OUTPUT: + return BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT; + + default: + return BROTLI_DECODER_RESULT_ERROR; + } +} + +/* Decodes WBITS by reading 1 - 7 bits, or 0x11 for "Large Window Brotli". + Precondition: bit-reader accumulator has at least 8 bits. */ +static BrotliDecoderErrorCode DecodeWindowBits(BrotliDecoderState* s, + BrotliBitReader* br) { + brotli_reg_t n; + BROTLI_BOOL large_window = s->large_window; + s->large_window = BROTLI_FALSE; + BrotliTakeBits(br, 1, &n); + if (n == 0) { + s->window_bits = 16; + return BROTLI_DECODER_SUCCESS; + } + BrotliTakeBits(br, 3, &n); + if (n != 0) { + s->window_bits = (17u + n) & 63u; + return BROTLI_DECODER_SUCCESS; + } + BrotliTakeBits(br, 3, &n); + if (n == 1) { + if (large_window) { + BrotliTakeBits(br, 1, &n); + if (n == 1) { + return BROTLI_FAILURE(BROTLI_DECODER_ERROR_FORMAT_WINDOW_BITS); + } + s->large_window = BROTLI_TRUE; + return BROTLI_DECODER_SUCCESS; + } else { + return BROTLI_FAILURE(BROTLI_DECODER_ERROR_FORMAT_WINDOW_BITS); + } + } + if (n != 0) { + s->window_bits = (8u + n) & 63u; + return BROTLI_DECODER_SUCCESS; + } + s->window_bits = 17; + return BROTLI_DECODER_SUCCESS; +} + +static BROTLI_INLINE void memmove16(uint8_t* dst, uint8_t* src) { +#if defined(BROTLI_TARGET_NEON) + vst1q_u8(dst, vld1q_u8(src)); +#else + uint32_t buffer[4]; + memcpy(buffer, src, 16); + memcpy(dst, buffer, 16); +#endif +} + +/* Decodes a number in the range [0..255], by reading 1 - 11 bits. */ +static BROTLI_NOINLINE BrotliDecoderErrorCode DecodeVarLenUint8( + BrotliDecoderState* s, BrotliBitReader* br, brotli_reg_t* value) { + brotli_reg_t bits; + switch (s->substate_decode_uint8) { + case BROTLI_STATE_DECODE_UINT8_NONE: + if (BROTLI_PREDICT_FALSE(!BrotliSafeReadBits(br, 1, &bits))) { + return BROTLI_DECODER_NEEDS_MORE_INPUT; + } + if (bits == 0) { + *value = 0; + return BROTLI_DECODER_SUCCESS; + } + /* Fall through. */ + + case BROTLI_STATE_DECODE_UINT8_SHORT: + if (BROTLI_PREDICT_FALSE(!BrotliSafeReadBits(br, 3, &bits))) { + s->substate_decode_uint8 = BROTLI_STATE_DECODE_UINT8_SHORT; + return BROTLI_DECODER_NEEDS_MORE_INPUT; + } + if (bits == 0) { + *value = 1; + s->substate_decode_uint8 = BROTLI_STATE_DECODE_UINT8_NONE; + return BROTLI_DECODER_SUCCESS; + } + /* Use output value as a temporary storage. It MUST be persisted. */ + *value = bits; + /* Fall through. */ + + case BROTLI_STATE_DECODE_UINT8_LONG: + if (BROTLI_PREDICT_FALSE(!BrotliSafeReadBits(br, *value, &bits))) { + s->substate_decode_uint8 = BROTLI_STATE_DECODE_UINT8_LONG; + return BROTLI_DECODER_NEEDS_MORE_INPUT; + } + *value = ((brotli_reg_t)1U << *value) + bits; + s->substate_decode_uint8 = BROTLI_STATE_DECODE_UINT8_NONE; + return BROTLI_DECODER_SUCCESS; + + default: + return + BROTLI_FAILURE(BROTLI_DECODER_ERROR_UNREACHABLE); /* COV_NF_LINE */ + } +} + +/* Decodes a metablock length and flags by reading 2 - 31 bits. */ +static BrotliDecoderErrorCode BROTLI_NOINLINE DecodeMetaBlockLength( + BrotliDecoderState* s, BrotliBitReader* br) { + brotli_reg_t bits; + int i; + for (;;) { + switch (s->substate_metablock_header) { + case BROTLI_STATE_METABLOCK_HEADER_NONE: + if (!BrotliSafeReadBits(br, 1, &bits)) { + return BROTLI_DECODER_NEEDS_MORE_INPUT; + } + s->is_last_metablock = bits ? 1 : 0; + s->meta_block_remaining_len = 0; + s->is_uncompressed = 0; + s->is_metadata = 0; + if (!s->is_last_metablock) { + s->substate_metablock_header = BROTLI_STATE_METABLOCK_HEADER_NIBBLES; + break; + } + s->substate_metablock_header = BROTLI_STATE_METABLOCK_HEADER_EMPTY; + /* Fall through. */ + + case BROTLI_STATE_METABLOCK_HEADER_EMPTY: + if (!BrotliSafeReadBits(br, 1, &bits)) { + return BROTLI_DECODER_NEEDS_MORE_INPUT; + } + if (bits) { + s->substate_metablock_header = BROTLI_STATE_METABLOCK_HEADER_NONE; + return BROTLI_DECODER_SUCCESS; + } + s->substate_metablock_header = BROTLI_STATE_METABLOCK_HEADER_NIBBLES; + /* Fall through. */ + + case BROTLI_STATE_METABLOCK_HEADER_NIBBLES: + if (!BrotliSafeReadBits(br, 2, &bits)) { + return BROTLI_DECODER_NEEDS_MORE_INPUT; + } + s->size_nibbles = (uint8_t)(bits + 4); + s->loop_counter = 0; + if (bits == 3) { + s->is_metadata = 1; + s->substate_metablock_header = BROTLI_STATE_METABLOCK_HEADER_RESERVED; + break; + } + s->substate_metablock_header = BROTLI_STATE_METABLOCK_HEADER_SIZE; + /* Fall through. */ + + case BROTLI_STATE_METABLOCK_HEADER_SIZE: + i = s->loop_counter; + for (; i < (int)s->size_nibbles; ++i) { + if (!BrotliSafeReadBits(br, 4, &bits)) { + s->loop_counter = i; + return BROTLI_DECODER_NEEDS_MORE_INPUT; + } + if (i + 1 == (int)s->size_nibbles && s->size_nibbles > 4 && + bits == 0) { + return BROTLI_FAILURE(BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_NIBBLE); + } + s->meta_block_remaining_len |= (int)(bits << (i * 4)); + } + s->substate_metablock_header = + BROTLI_STATE_METABLOCK_HEADER_UNCOMPRESSED; + /* Fall through. */ + + case BROTLI_STATE_METABLOCK_HEADER_UNCOMPRESSED: + if (!s->is_last_metablock) { + if (!BrotliSafeReadBits(br, 1, &bits)) { + return BROTLI_DECODER_NEEDS_MORE_INPUT; + } + s->is_uncompressed = bits ? 1 : 0; + } + ++s->meta_block_remaining_len; + s->substate_metablock_header = BROTLI_STATE_METABLOCK_HEADER_NONE; + return BROTLI_DECODER_SUCCESS; + + case BROTLI_STATE_METABLOCK_HEADER_RESERVED: + if (!BrotliSafeReadBits(br, 1, &bits)) { + return BROTLI_DECODER_NEEDS_MORE_INPUT; + } + if (bits != 0) { + return BROTLI_FAILURE(BROTLI_DECODER_ERROR_FORMAT_RESERVED); + } + s->substate_metablock_header = BROTLI_STATE_METABLOCK_HEADER_BYTES; + /* Fall through. */ + + case BROTLI_STATE_METABLOCK_HEADER_BYTES: + if (!BrotliSafeReadBits(br, 2, &bits)) { + return BROTLI_DECODER_NEEDS_MORE_INPUT; + } + if (bits == 0) { + s->substate_metablock_header = BROTLI_STATE_METABLOCK_HEADER_NONE; + return BROTLI_DECODER_SUCCESS; + } + s->size_nibbles = (uint8_t)bits; + s->substate_metablock_header = BROTLI_STATE_METABLOCK_HEADER_METADATA; + /* Fall through. */ + + case BROTLI_STATE_METABLOCK_HEADER_METADATA: + i = s->loop_counter; + for (; i < (int)s->size_nibbles; ++i) { + if (!BrotliSafeReadBits(br, 8, &bits)) { + s->loop_counter = i; + return BROTLI_DECODER_NEEDS_MORE_INPUT; + } + if (i + 1 == (int)s->size_nibbles && s->size_nibbles > 1 && + bits == 0) { + return BROTLI_FAILURE( + BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_META_NIBBLE); + } + s->meta_block_remaining_len |= (int)(bits << (i * 8)); + } + ++s->meta_block_remaining_len; + s->substate_metablock_header = BROTLI_STATE_METABLOCK_HEADER_NONE; + return BROTLI_DECODER_SUCCESS; + + default: + return + BROTLI_FAILURE(BROTLI_DECODER_ERROR_UNREACHABLE); /* COV_NF_LINE */ + } + } +} + +/* Decodes the Huffman code. + This method doesn't read data from the bit reader, BUT drops the amount of + bits that correspond to the decoded symbol. + bits MUST contain at least 15 (BROTLI_HUFFMAN_MAX_CODE_LENGTH) valid bits. */ +static BROTLI_INLINE brotli_reg_t DecodeSymbol(brotli_reg_t bits, + const HuffmanCode* table, + BrotliBitReader* br) { + BROTLI_HC_MARK_TABLE_FOR_FAST_LOAD(table); + BROTLI_HC_ADJUST_TABLE_INDEX(table, bits & HUFFMAN_TABLE_MASK); + if (BROTLI_HC_FAST_LOAD_BITS(table) > HUFFMAN_TABLE_BITS) { + brotli_reg_t nbits = BROTLI_HC_FAST_LOAD_BITS(table) - HUFFMAN_TABLE_BITS; + BrotliDropBits(br, HUFFMAN_TABLE_BITS); + BROTLI_HC_ADJUST_TABLE_INDEX(table, + BROTLI_HC_FAST_LOAD_VALUE(table) + + ((bits >> HUFFMAN_TABLE_BITS) & BitMask(nbits))); + } + BrotliDropBits(br, BROTLI_HC_FAST_LOAD_BITS(table)); + return BROTLI_HC_FAST_LOAD_VALUE(table); +} + +/* Reads and decodes the next Huffman code from bit-stream. + This method peeks 16 bits of input and drops 0 - 15 of them. */ +static BROTLI_INLINE brotli_reg_t ReadSymbol(const HuffmanCode* table, + BrotliBitReader* br) { + return DecodeSymbol(BrotliGet16BitsUnmasked(br), table, br); +} + +/* Same as DecodeSymbol, but it is known that there is less than 15 bits of + input are currently available. */ +static BROTLI_NOINLINE BROTLI_BOOL SafeDecodeSymbol( + const HuffmanCode* table, BrotliBitReader* br, brotli_reg_t* result) { + brotli_reg_t val; + brotli_reg_t available_bits = BrotliGetAvailableBits(br); + BROTLI_HC_MARK_TABLE_FOR_FAST_LOAD(table); + if (available_bits == 0) { + if (BROTLI_HC_FAST_LOAD_BITS(table) == 0) { + *result = BROTLI_HC_FAST_LOAD_VALUE(table); + return BROTLI_TRUE; + } + return BROTLI_FALSE; /* No valid bits at all. */ + } + val = BrotliGetBitsUnmasked(br); + BROTLI_HC_ADJUST_TABLE_INDEX(table, val & HUFFMAN_TABLE_MASK); + if (BROTLI_HC_FAST_LOAD_BITS(table) <= HUFFMAN_TABLE_BITS) { + if (BROTLI_HC_FAST_LOAD_BITS(table) <= available_bits) { + BrotliDropBits(br, BROTLI_HC_FAST_LOAD_BITS(table)); + *result = BROTLI_HC_FAST_LOAD_VALUE(table); + return BROTLI_TRUE; + } else { + return BROTLI_FALSE; /* Not enough bits for the first level. */ + } + } + if (available_bits <= HUFFMAN_TABLE_BITS) { + return BROTLI_FALSE; /* Not enough bits to move to the second level. */ + } + + /* Speculatively drop HUFFMAN_TABLE_BITS. */ + val = (val & BitMask(BROTLI_HC_FAST_LOAD_BITS(table))) >> HUFFMAN_TABLE_BITS; + available_bits -= HUFFMAN_TABLE_BITS; + BROTLI_HC_ADJUST_TABLE_INDEX(table, BROTLI_HC_FAST_LOAD_VALUE(table) + val); + if (available_bits < BROTLI_HC_FAST_LOAD_BITS(table)) { + return BROTLI_FALSE; /* Not enough bits for the second level. */ + } + + BrotliDropBits(br, HUFFMAN_TABLE_BITS + BROTLI_HC_FAST_LOAD_BITS(table)); + *result = BROTLI_HC_FAST_LOAD_VALUE(table); + return BROTLI_TRUE; +} + +static BROTLI_INLINE BROTLI_BOOL SafeReadSymbol( + const HuffmanCode* table, BrotliBitReader* br, brotli_reg_t* result) { + brotli_reg_t val; + if (BROTLI_PREDICT_TRUE(BrotliSafeGetBits(br, 15, &val))) { + *result = DecodeSymbol(val, table, br); + return BROTLI_TRUE; + } + return SafeDecodeSymbol(table, br, result); +} + +/* Makes a look-up in first level Huffman table. Peeks 8 bits. */ +static BROTLI_INLINE void PreloadSymbol(int safe, + const HuffmanCode* table, + BrotliBitReader* br, + brotli_reg_t* bits, + brotli_reg_t* value) { + if (safe) { + return; + } + BROTLI_HC_MARK_TABLE_FOR_FAST_LOAD(table); + BROTLI_HC_ADJUST_TABLE_INDEX(table, BrotliGetBits(br, HUFFMAN_TABLE_BITS)); + *bits = BROTLI_HC_FAST_LOAD_BITS(table); + *value = BROTLI_HC_FAST_LOAD_VALUE(table); +} + +/* Decodes the next Huffman code using data prepared by PreloadSymbol. + Reads 0 - 15 bits. Also peeks 8 following bits. */ +static BROTLI_INLINE brotli_reg_t ReadPreloadedSymbol(const HuffmanCode* table, + BrotliBitReader* br, + brotli_reg_t* bits, + brotli_reg_t* value) { + brotli_reg_t result = *value; + if (BROTLI_PREDICT_FALSE(*bits > HUFFMAN_TABLE_BITS)) { + brotli_reg_t val = BrotliGet16BitsUnmasked(br); + const HuffmanCode* ext = table + (val & HUFFMAN_TABLE_MASK) + *value; + brotli_reg_t mask = BitMask((*bits - HUFFMAN_TABLE_BITS)); + BROTLI_HC_MARK_TABLE_FOR_FAST_LOAD(ext); + BrotliDropBits(br, HUFFMAN_TABLE_BITS); + BROTLI_HC_ADJUST_TABLE_INDEX(ext, (val >> HUFFMAN_TABLE_BITS) & mask); + BrotliDropBits(br, BROTLI_HC_FAST_LOAD_BITS(ext)); + result = BROTLI_HC_FAST_LOAD_VALUE(ext); + } else { + BrotliDropBits(br, *bits); + } + PreloadSymbol(0, table, br, bits, value); + return result; +} + +/* Reads up to limit symbols from br and copies them into ringbuffer, + starting from pos. Caller must ensure that there is enough space + for the write. Returns the amount of symbols actually copied. */ +static BROTLI_INLINE int BrotliCopyPreloadedSymbolsToU8(const HuffmanCode* table, + BrotliBitReader* br, + brotli_reg_t* bits, + brotli_reg_t* value, + uint8_t* ringbuffer, + int pos, + const int limit) { + /* Calculate range where CheckInputAmount is always true. + Start with the number of bytes we can read. */ + int64_t new_lim = br->guard_in - br->next_in; + /* Convert to bits, since symbols use variable number of bits. */ + new_lim *= 8; + /* At most 15 bits per symbol, so this is safe. */ + new_lim /= 15; + const int kMaximalOverread = 4; + int pos_limit = limit; + int copies = 0; + if ((new_lim - kMaximalOverread) <= limit) { + // Safe cast, since new_lim is already < num_steps + pos_limit = (int)(new_lim - kMaximalOverread); + } + if (pos_limit < 0) { + pos_limit = 0; + } + copies = pos_limit; + pos_limit += pos; + /* Fast path, caller made sure it is safe to write, + we verified that is is safe to read. */ + for (; pos < pos_limit; pos++) { + BROTLI_DCHECK(BrotliCheckInputAmount(br)); + ringbuffer[pos] = (uint8_t)ReadPreloadedSymbol(table, br, bits, value); + BROTLI_LOG_ARRAY_INDEX(ringbuffer, pos); + } + /* Do the remainder, caller made sure it is safe to write, + we need to bverify that it is safe to read. */ + while (BrotliCheckInputAmount(br) && copies < limit) { + ringbuffer[pos] = (uint8_t)ReadPreloadedSymbol(table, br, bits, value); + BROTLI_LOG_ARRAY_INDEX(ringbuffer, pos); + pos++; + copies++; + } + return copies; +} + +static BROTLI_INLINE brotli_reg_t Log2Floor(brotli_reg_t x) { + brotli_reg_t result = 0; + while (x) { + x >>= 1; + ++result; + } + return result; +} + +/* Reads (s->symbol + 1) symbols. + Totally 1..4 symbols are read, 1..11 bits each. + The list of symbols MUST NOT contain duplicates. */ +static BrotliDecoderErrorCode ReadSimpleHuffmanSymbols( + brotli_reg_t alphabet_size_max, brotli_reg_t alphabet_size_limit, + BrotliDecoderState* s) { + /* max_bits == 1..11; symbol == 0..3; 1..44 bits will be read. */ + BrotliBitReader* br = &s->br; + BrotliMetablockHeaderArena* h = &s->arena.header; + brotli_reg_t max_bits = Log2Floor(alphabet_size_max - 1); + brotli_reg_t i = h->sub_loop_counter; + brotli_reg_t num_symbols = h->symbol; + while (i <= num_symbols) { + brotli_reg_t v; + if (BROTLI_PREDICT_FALSE(!BrotliSafeReadBits(br, max_bits, &v))) { + h->sub_loop_counter = i; + h->substate_huffman = BROTLI_STATE_HUFFMAN_SIMPLE_READ; + return BROTLI_DECODER_NEEDS_MORE_INPUT; + } + if (v >= alphabet_size_limit) { + return + BROTLI_FAILURE(BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_ALPHABET); + } + h->symbols_lists_array[i] = (uint16_t)v; + BROTLI_LOG_UINT(h->symbols_lists_array[i]); + ++i; + } + + for (i = 0; i < num_symbols; ++i) { + brotli_reg_t k = i + 1; + for (; k <= num_symbols; ++k) { + if (h->symbols_lists_array[i] == h->symbols_lists_array[k]) { + return BROTLI_FAILURE(BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_SAME); + } + } + } + + return BROTLI_DECODER_SUCCESS; +} + +/* Process single decoded symbol code length: + A) reset the repeat variable + B) remember code length (if it is not 0) + C) extend corresponding index-chain + D) reduce the Huffman space + E) update the histogram */ +static BROTLI_INLINE void ProcessSingleCodeLength(brotli_reg_t code_len, + brotli_reg_t* symbol, brotli_reg_t* repeat, brotli_reg_t* space, + brotli_reg_t* prev_code_len, uint16_t* symbol_lists, + uint16_t* code_length_histo, int* next_symbol) { + *repeat = 0; + if (code_len != 0) { /* code_len == 1..15 */ + symbol_lists[next_symbol[code_len]] = (uint16_t)(*symbol); + next_symbol[code_len] = (int)(*symbol); + *prev_code_len = code_len; + *space -= 32768U >> code_len; + code_length_histo[code_len]++; + BROTLI_LOG(("[ReadHuffmanCode] code_length[%d] = %d\n", + (int)*symbol, (int)code_len)); + } + (*symbol)++; +} + +/* Process repeated symbol code length. + A) Check if it is the extension of previous repeat sequence; if the decoded + value is not BROTLI_REPEAT_PREVIOUS_CODE_LENGTH, then it is a new + symbol-skip + B) Update repeat variable + C) Check if operation is feasible (fits alphabet) + D) For each symbol do the same operations as in ProcessSingleCodeLength + + PRECONDITION: code_len == BROTLI_REPEAT_PREVIOUS_CODE_LENGTH or + code_len == BROTLI_REPEAT_ZERO_CODE_LENGTH */ +static BROTLI_INLINE void ProcessRepeatedCodeLength(brotli_reg_t code_len, + brotli_reg_t repeat_delta, brotli_reg_t alphabet_size, brotli_reg_t* symbol, + brotli_reg_t* repeat, brotli_reg_t* space, brotli_reg_t* prev_code_len, + brotli_reg_t* repeat_code_len, uint16_t* symbol_lists, + uint16_t* code_length_histo, int* next_symbol) { + brotli_reg_t old_repeat; + brotli_reg_t extra_bits = 3; /* for BROTLI_REPEAT_ZERO_CODE_LENGTH */ + brotli_reg_t new_len = 0; /* for BROTLI_REPEAT_ZERO_CODE_LENGTH */ + if (code_len == BROTLI_REPEAT_PREVIOUS_CODE_LENGTH) { + new_len = *prev_code_len; + extra_bits = 2; + } + if (*repeat_code_len != new_len) { + *repeat = 0; + *repeat_code_len = new_len; + } + old_repeat = *repeat; + if (*repeat > 0) { + *repeat -= 2; + *repeat <<= extra_bits; + } + *repeat += repeat_delta + 3U; + repeat_delta = *repeat - old_repeat; + if (*symbol + repeat_delta > alphabet_size) { + BROTLI_DUMP(); + *symbol = alphabet_size; + *space = 0xFFFFF; + return; + } + BROTLI_LOG(("[ReadHuffmanCode] code_length[%d..%d] = %d\n", + (int)*symbol, (int)(*symbol + repeat_delta - 1), (int)*repeat_code_len)); + if (*repeat_code_len != 0) { + brotli_reg_t last = *symbol + repeat_delta; + int next = next_symbol[*repeat_code_len]; + do { + symbol_lists[next] = (uint16_t)*symbol; + next = (int)*symbol; + } while (++(*symbol) != last); + next_symbol[*repeat_code_len] = next; + *space -= repeat_delta << (15 - *repeat_code_len); + code_length_histo[*repeat_code_len] = + (uint16_t)(code_length_histo[*repeat_code_len] + repeat_delta); + } else { + *symbol += repeat_delta; + } +} + +/* Reads and decodes symbol codelengths. */ +static BrotliDecoderErrorCode ReadSymbolCodeLengths( + brotli_reg_t alphabet_size, BrotliDecoderState* s) { + BrotliBitReader* br = &s->br; + BrotliMetablockHeaderArena* h = &s->arena.header; + brotli_reg_t symbol = h->symbol; + brotli_reg_t repeat = h->repeat; + brotli_reg_t space = h->space; + brotli_reg_t prev_code_len = h->prev_code_len; + brotli_reg_t repeat_code_len = h->repeat_code_len; + uint16_t* symbol_lists = h->symbol_lists; + uint16_t* code_length_histo = h->code_length_histo; + int* next_symbol = h->next_symbol; + if (!BrotliWarmupBitReader(br)) { + return BROTLI_DECODER_NEEDS_MORE_INPUT; + } + while (symbol < alphabet_size && space > 0) { + const HuffmanCode* p = h->table; + brotli_reg_t code_len; + BROTLI_HC_MARK_TABLE_FOR_FAST_LOAD(p); + if (!BrotliCheckInputAmount(br)) { + h->symbol = symbol; + h->repeat = repeat; + h->prev_code_len = prev_code_len; + h->repeat_code_len = repeat_code_len; + h->space = space; + return BROTLI_DECODER_NEEDS_MORE_INPUT; + } + BrotliFillBitWindow16(br); + BROTLI_HC_ADJUST_TABLE_INDEX(p, BrotliGetBitsUnmasked(br) & + BitMask(BROTLI_HUFFMAN_MAX_CODE_LENGTH_CODE_LENGTH)); + BrotliDropBits(br, BROTLI_HC_FAST_LOAD_BITS(p)); /* Use 1..5 bits. */ + code_len = BROTLI_HC_FAST_LOAD_VALUE(p); /* code_len == 0..17 */ + if (code_len < BROTLI_REPEAT_PREVIOUS_CODE_LENGTH) { + ProcessSingleCodeLength(code_len, &symbol, &repeat, &space, + &prev_code_len, symbol_lists, code_length_histo, next_symbol); + } else { /* code_len == 16..17, extra_bits == 2..3 */ + brotli_reg_t extra_bits = + (code_len == BROTLI_REPEAT_PREVIOUS_CODE_LENGTH) ? 2 : 3; + brotli_reg_t repeat_delta = + BrotliGetBitsUnmasked(br) & BitMask(extra_bits); + BrotliDropBits(br, extra_bits); + ProcessRepeatedCodeLength(code_len, repeat_delta, alphabet_size, + &symbol, &repeat, &space, &prev_code_len, &repeat_code_len, + symbol_lists, code_length_histo, next_symbol); + } + } + h->space = space; + return BROTLI_DECODER_SUCCESS; +} + +static BrotliDecoderErrorCode SafeReadSymbolCodeLengths( + brotli_reg_t alphabet_size, BrotliDecoderState* s) { + BrotliBitReader* br = &s->br; + BrotliMetablockHeaderArena* h = &s->arena.header; + BROTLI_BOOL get_byte = BROTLI_FALSE; + while (h->symbol < alphabet_size && h->space > 0) { + const HuffmanCode* p = h->table; + brotli_reg_t code_len; + brotli_reg_t available_bits; + brotli_reg_t bits = 0; + BROTLI_HC_MARK_TABLE_FOR_FAST_LOAD(p); + if (get_byte && !BrotliPullByte(br)) return BROTLI_DECODER_NEEDS_MORE_INPUT; + get_byte = BROTLI_FALSE; + available_bits = BrotliGetAvailableBits(br); + if (available_bits != 0) { + bits = (uint32_t)BrotliGetBitsUnmasked(br); + } + BROTLI_HC_ADJUST_TABLE_INDEX(p, + bits & BitMask(BROTLI_HUFFMAN_MAX_CODE_LENGTH_CODE_LENGTH)); + if (BROTLI_HC_FAST_LOAD_BITS(p) > available_bits) { + get_byte = BROTLI_TRUE; + continue; + } + code_len = BROTLI_HC_FAST_LOAD_VALUE(p); /* code_len == 0..17 */ + if (code_len < BROTLI_REPEAT_PREVIOUS_CODE_LENGTH) { + BrotliDropBits(br, BROTLI_HC_FAST_LOAD_BITS(p)); + ProcessSingleCodeLength(code_len, &h->symbol, &h->repeat, &h->space, + &h->prev_code_len, h->symbol_lists, h->code_length_histo, + h->next_symbol); + } else { /* code_len == 16..17, extra_bits == 2..3 */ + brotli_reg_t extra_bits = code_len - 14U; + brotli_reg_t repeat_delta = (bits >> BROTLI_HC_FAST_LOAD_BITS(p)) & + BitMask(extra_bits); + if (available_bits < BROTLI_HC_FAST_LOAD_BITS(p) + extra_bits) { + get_byte = BROTLI_TRUE; + continue; + } + BrotliDropBits(br, BROTLI_HC_FAST_LOAD_BITS(p) + extra_bits); + ProcessRepeatedCodeLength(code_len, repeat_delta, alphabet_size, + &h->symbol, &h->repeat, &h->space, &h->prev_code_len, + &h->repeat_code_len, h->symbol_lists, h->code_length_histo, + h->next_symbol); + } + } + return BROTLI_DECODER_SUCCESS; +} + +/* Reads and decodes 15..18 codes using static prefix code. + Each code is 2..4 bits long. In total 30..72 bits are used. */ +static BrotliDecoderErrorCode ReadCodeLengthCodeLengths(BrotliDecoderState* s) { + BrotliBitReader* br = &s->br; + BrotliMetablockHeaderArena* h = &s->arena.header; + brotli_reg_t num_codes = h->repeat; + brotli_reg_t space = h->space; + brotli_reg_t i = h->sub_loop_counter; + for (; i < BROTLI_CODE_LENGTH_CODES; ++i) { + const uint8_t code_len_idx = kCodeLengthCodeOrder[i]; + brotli_reg_t ix; + brotli_reg_t v; + if (BROTLI_PREDICT_FALSE(!BrotliSafeGetBits(br, 4, &ix))) { + brotli_reg_t available_bits = BrotliGetAvailableBits(br); + if (available_bits != 0) { + ix = BrotliGetBitsUnmasked(br) & 0xF; + } else { + ix = 0; + } + if (kCodeLengthPrefixLength[ix] > available_bits) { + h->sub_loop_counter = i; + h->repeat = num_codes; + h->space = space; + h->substate_huffman = BROTLI_STATE_HUFFMAN_COMPLEX; + return BROTLI_DECODER_NEEDS_MORE_INPUT; + } + } + v = kCodeLengthPrefixValue[ix]; + BrotliDropBits(br, kCodeLengthPrefixLength[ix]); + h->code_length_code_lengths[code_len_idx] = (uint8_t)v; + BROTLI_LOG_ARRAY_INDEX(h->code_length_code_lengths, code_len_idx); + if (v != 0) { + space = space - (32U >> v); + ++num_codes; + ++h->code_length_histo[v]; + if (space - 1U >= 32U) { + /* space is 0 or wrapped around. */ + break; + } + } + } + if (!(num_codes == 1 || space == 0)) { + return BROTLI_FAILURE(BROTLI_DECODER_ERROR_FORMAT_CL_SPACE); + } + return BROTLI_DECODER_SUCCESS; +} + +/* Decodes the Huffman tables. + There are 2 scenarios: + A) Huffman code contains only few symbols (1..4). Those symbols are read + directly; their code lengths are defined by the number of symbols. + For this scenario 4 - 49 bits will be read. + + B) 2-phase decoding: + B.1) Small Huffman table is decoded; it is specified with code lengths + encoded with predefined entropy code. 32 - 74 bits are used. + B.2) Decoded table is used to decode code lengths of symbols in resulting + Huffman table. In worst case 3520 bits are read. */ +static BrotliDecoderErrorCode ReadHuffmanCode(brotli_reg_t alphabet_size_max, + brotli_reg_t alphabet_size_limit, + HuffmanCode* table, + brotli_reg_t* opt_table_size, + BrotliDecoderState* s) { + BrotliBitReader* br = &s->br; + BrotliMetablockHeaderArena* h = &s->arena.header; + /* State machine. */ + for (;;) { + switch (h->substate_huffman) { + case BROTLI_STATE_HUFFMAN_NONE: + if (!BrotliSafeReadBits(br, 2, &h->sub_loop_counter)) { + return BROTLI_DECODER_NEEDS_MORE_INPUT; + } + BROTLI_LOG_UINT(h->sub_loop_counter); + /* The value is used as follows: + 1 for simple code; + 0 for no skipping, 2 skips 2 code lengths, 3 skips 3 code lengths */ + if (h->sub_loop_counter != 1) { + h->space = 32; + h->repeat = 0; /* num_codes */ + memset(&h->code_length_histo[0], 0, sizeof(h->code_length_histo[0]) * + (BROTLI_HUFFMAN_MAX_CODE_LENGTH_CODE_LENGTH + 1)); + memset(&h->code_length_code_lengths[0], 0, + sizeof(h->code_length_code_lengths)); + h->substate_huffman = BROTLI_STATE_HUFFMAN_COMPLEX; + continue; + } + /* Fall through. */ + + case BROTLI_STATE_HUFFMAN_SIMPLE_SIZE: + /* Read symbols, codes & code lengths directly. */ + if (!BrotliSafeReadBits(br, 2, &h->symbol)) { /* num_symbols */ + h->substate_huffman = BROTLI_STATE_HUFFMAN_SIMPLE_SIZE; + return BROTLI_DECODER_NEEDS_MORE_INPUT; + } + h->sub_loop_counter = 0; + /* Fall through. */ + + case BROTLI_STATE_HUFFMAN_SIMPLE_READ: { + BrotliDecoderErrorCode result = + ReadSimpleHuffmanSymbols(alphabet_size_max, alphabet_size_limit, s); + if (result != BROTLI_DECODER_SUCCESS) { + return result; + } + } + /* Fall through. */ + + case BROTLI_STATE_HUFFMAN_SIMPLE_BUILD: { + brotli_reg_t table_size; + if (h->symbol == 3) { + brotli_reg_t bits; + if (!BrotliSafeReadBits(br, 1, &bits)) { + h->substate_huffman = BROTLI_STATE_HUFFMAN_SIMPLE_BUILD; + return BROTLI_DECODER_NEEDS_MORE_INPUT; + } + h->symbol += bits; + } + BROTLI_LOG_UINT(h->symbol); + table_size = BrotliBuildSimpleHuffmanTable(table, HUFFMAN_TABLE_BITS, + h->symbols_lists_array, + (uint32_t)h->symbol); + if (opt_table_size) { + *opt_table_size = table_size; + } + h->substate_huffman = BROTLI_STATE_HUFFMAN_NONE; + return BROTLI_DECODER_SUCCESS; + } + + /* Decode Huffman-coded code lengths. */ + case BROTLI_STATE_HUFFMAN_COMPLEX: { + brotli_reg_t i; + BrotliDecoderErrorCode result = ReadCodeLengthCodeLengths(s); + if (result != BROTLI_DECODER_SUCCESS) { + return result; + } + BrotliBuildCodeLengthsHuffmanTable(h->table, + h->code_length_code_lengths, + h->code_length_histo); + memset(&h->code_length_histo[0], 0, sizeof(h->code_length_histo)); + for (i = 0; i <= BROTLI_HUFFMAN_MAX_CODE_LENGTH; ++i) { + h->next_symbol[i] = (int)i - (BROTLI_HUFFMAN_MAX_CODE_LENGTH + 1); + h->symbol_lists[h->next_symbol[i]] = 0xFFFF; + } + + h->symbol = 0; + h->prev_code_len = BROTLI_INITIAL_REPEATED_CODE_LENGTH; + h->repeat = 0; + h->repeat_code_len = 0; + h->space = 32768; + h->substate_huffman = BROTLI_STATE_HUFFMAN_LENGTH_SYMBOLS; + } + /* Fall through. */ + + case BROTLI_STATE_HUFFMAN_LENGTH_SYMBOLS: { + brotli_reg_t table_size; + BrotliDecoderErrorCode result = ReadSymbolCodeLengths( + alphabet_size_limit, s); + if (result == BROTLI_DECODER_NEEDS_MORE_INPUT) { + result = SafeReadSymbolCodeLengths(alphabet_size_limit, s); + } + if (result != BROTLI_DECODER_SUCCESS) { + return result; + } + + if (h->space != 0) { + BROTLI_LOG(("[ReadHuffmanCode] space = %d\n", (int)h->space)); + return BROTLI_FAILURE(BROTLI_DECODER_ERROR_FORMAT_HUFFMAN_SPACE); + } + table_size = BrotliBuildHuffmanTable( + table, HUFFMAN_TABLE_BITS, h->symbol_lists, h->code_length_histo); + if (opt_table_size) { + *opt_table_size = table_size; + } + h->substate_huffman = BROTLI_STATE_HUFFMAN_NONE; + return BROTLI_DECODER_SUCCESS; + } + + default: + return + BROTLI_FAILURE(BROTLI_DECODER_ERROR_UNREACHABLE); /* COV_NF_LINE */ + } + } +} + +/* Decodes a block length by reading 3..39 bits. */ +static BROTLI_INLINE brotli_reg_t ReadBlockLength(const HuffmanCode* table, + BrotliBitReader* br) { + brotli_reg_t code; + brotli_reg_t nbits; + code = ReadSymbol(table, br); + nbits = _kBrotliPrefixCodeRanges[code].nbits; /* nbits == 2..24 */ + return _kBrotliPrefixCodeRanges[code].offset + BrotliReadBits24(br, nbits); +} + +/* WARNING: if state is not BROTLI_STATE_READ_BLOCK_LENGTH_NONE, then + reading can't be continued with ReadBlockLength. */ +static BROTLI_INLINE BROTLI_BOOL SafeReadBlockLength( + BrotliDecoderState* s, brotli_reg_t* result, const HuffmanCode* table, + BrotliBitReader* br) { + brotli_reg_t index; + if (s->substate_read_block_length == BROTLI_STATE_READ_BLOCK_LENGTH_NONE) { + if (!SafeReadSymbol(table, br, &index)) { + return BROTLI_FALSE; + } + } else { + index = s->block_length_index; + } + { + brotli_reg_t bits; + brotli_reg_t nbits = _kBrotliPrefixCodeRanges[index].nbits; + brotli_reg_t offset = _kBrotliPrefixCodeRanges[index].offset; + if (!BrotliSafeReadBits(br, nbits, &bits)) { + s->block_length_index = index; + s->substate_read_block_length = BROTLI_STATE_READ_BLOCK_LENGTH_SUFFIX; + return BROTLI_FALSE; + } + *result = offset + bits; + s->substate_read_block_length = BROTLI_STATE_READ_BLOCK_LENGTH_NONE; + return BROTLI_TRUE; + } +} + +/* Transform: + 1) initialize list L with values 0, 1,... 255 + 2) For each input element X: + 2.1) let Y = L[X] + 2.2) remove X-th element from L + 2.3) prepend Y to L + 2.4) append Y to output + + In most cases max(Y) <= 7, so most of L remains intact. + To reduce the cost of initialization, we reuse L, remember the upper bound + of Y values, and reinitialize only first elements in L. + + Most of input values are 0 and 1. To reduce number of branches, we replace + inner for loop with do-while. */ +static BROTLI_NOINLINE void InverseMoveToFrontTransform( + uint8_t* v, brotli_reg_t v_len, BrotliDecoderState* state) { + /* Reinitialize elements that could have been changed. */ + brotli_reg_t i = 1; + brotli_reg_t upper_bound = state->mtf_upper_bound; + uint32_t* mtf = &state->mtf[1]; /* Make mtf[-1] addressable. */ + uint8_t* mtf_u8 = (uint8_t*)mtf; + /* Load endian-aware constant. */ + const uint8_t b0123[4] = {0, 1, 2, 3}; + uint32_t pattern; + memcpy(&pattern, &b0123, 4); + + /* Initialize list using 4 consequent values pattern. */ + mtf[0] = pattern; + do { + pattern += 0x04040404; /* Advance all 4 values by 4. */ + mtf[i] = pattern; + i++; + } while (i <= upper_bound); + + /* Transform the input. */ + upper_bound = 0; + for (i = 0; i < v_len; ++i) { + int index = v[i]; + uint8_t value = mtf_u8[index]; + upper_bound |= v[i]; + v[i] = value; + mtf_u8[-1] = value; + do { + index--; + mtf_u8[index + 1] = mtf_u8[index]; + } while (index >= 0); + } + /* Remember amount of elements to be reinitialized. */ + state->mtf_upper_bound = upper_bound >> 2; +} + +/* Decodes a series of Huffman table using ReadHuffmanCode function. */ +static BrotliDecoderErrorCode HuffmanTreeGroupDecode( + HuffmanTreeGroup* group, BrotliDecoderState* s) { + BrotliMetablockHeaderArena* h = &s->arena.header; + if (h->substate_tree_group != BROTLI_STATE_TREE_GROUP_LOOP) { + h->next = group->codes; + h->htree_index = 0; + h->substate_tree_group = BROTLI_STATE_TREE_GROUP_LOOP; + } + while (h->htree_index < group->num_htrees) { + brotli_reg_t table_size; + BrotliDecoderErrorCode result = ReadHuffmanCode(group->alphabet_size_max, + group->alphabet_size_limit, h->next, &table_size, s); + if (result != BROTLI_DECODER_SUCCESS) return result; + group->htrees[h->htree_index] = h->next; + h->next += table_size; + ++h->htree_index; + } + h->substate_tree_group = BROTLI_STATE_TREE_GROUP_NONE; + return BROTLI_DECODER_SUCCESS; +} + +/* Decodes a context map. + Decoding is done in 4 phases: + 1) Read auxiliary information (6..16 bits) and allocate memory. + In case of trivial context map, decoding is finished at this phase. + 2) Decode Huffman table using ReadHuffmanCode function. + This table will be used for reading context map items. + 3) Read context map items; "0" values could be run-length encoded. + 4) Optionally, apply InverseMoveToFront transform to the resulting map. */ +static BrotliDecoderErrorCode DecodeContextMap(brotli_reg_t context_map_size, + brotli_reg_t* num_htrees, + uint8_t** context_map_arg, + BrotliDecoderState* s) { + BrotliBitReader* br = &s->br; + BrotliDecoderErrorCode result = BROTLI_DECODER_SUCCESS; + BrotliMetablockHeaderArena* h = &s->arena.header; + + switch ((int)h->substate_context_map) { + case BROTLI_STATE_CONTEXT_MAP_NONE: + result = DecodeVarLenUint8(s, br, num_htrees); + if (result != BROTLI_DECODER_SUCCESS) { + return result; + } + (*num_htrees)++; + h->context_index = 0; + BROTLI_LOG_UINT(context_map_size); + BROTLI_LOG_UINT(*num_htrees); + *context_map_arg = + (uint8_t*)BROTLI_DECODER_ALLOC(s, (size_t)context_map_size); + if (*context_map_arg == 0) { + return BROTLI_FAILURE(BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MAP); + } + if (*num_htrees <= 1) { + memset(*context_map_arg, 0, (size_t)context_map_size); + return BROTLI_DECODER_SUCCESS; + } + h->substate_context_map = BROTLI_STATE_CONTEXT_MAP_READ_PREFIX; + /* Fall through. */ + + case BROTLI_STATE_CONTEXT_MAP_READ_PREFIX: { + brotli_reg_t bits; + /* In next stage ReadHuffmanCode uses at least 4 bits, so it is safe + to peek 4 bits ahead. */ + if (!BrotliSafeGetBits(br, 5, &bits)) { + return BROTLI_DECODER_NEEDS_MORE_INPUT; + } + if ((bits & 1) != 0) { /* Use RLE for zeros. */ + h->max_run_length_prefix = (bits >> 1) + 1; + BrotliDropBits(br, 5); + } else { + h->max_run_length_prefix = 0; + BrotliDropBits(br, 1); + } + BROTLI_LOG_UINT(h->max_run_length_prefix); + h->substate_context_map = BROTLI_STATE_CONTEXT_MAP_HUFFMAN; + } + /* Fall through. */ + + case BROTLI_STATE_CONTEXT_MAP_HUFFMAN: { + brotli_reg_t alphabet_size = *num_htrees + h->max_run_length_prefix; + result = ReadHuffmanCode(alphabet_size, alphabet_size, + h->context_map_table, NULL, s); + if (result != BROTLI_DECODER_SUCCESS) return result; + h->code = 0xFFFF; + h->substate_context_map = BROTLI_STATE_CONTEXT_MAP_DECODE; + } + /* Fall through. */ + + case BROTLI_STATE_CONTEXT_MAP_DECODE: { + brotli_reg_t context_index = h->context_index; + brotli_reg_t max_run_length_prefix = h->max_run_length_prefix; + uint8_t* context_map = *context_map_arg; + brotli_reg_t code = h->code; + BROTLI_BOOL skip_preamble = (code != 0xFFFF); + while (context_index < context_map_size || skip_preamble) { + if (!skip_preamble) { + if (!SafeReadSymbol(h->context_map_table, br, &code)) { + h->code = 0xFFFF; + h->context_index = context_index; + return BROTLI_DECODER_NEEDS_MORE_INPUT; + } + BROTLI_LOG_UINT(code); + + if (code == 0) { + context_map[context_index++] = 0; + continue; + } + if (code > max_run_length_prefix) { + context_map[context_index++] = + (uint8_t)(code - max_run_length_prefix); + continue; + } + } else { + skip_preamble = BROTLI_FALSE; + } + /* RLE sub-stage. */ + { + brotli_reg_t reps; + if (!BrotliSafeReadBits(br, code, &reps)) { + h->code = code; + h->context_index = context_index; + return BROTLI_DECODER_NEEDS_MORE_INPUT; + } + reps += (brotli_reg_t)1U << code; + BROTLI_LOG_UINT(reps); + if (context_index + reps > context_map_size) { + return + BROTLI_FAILURE(BROTLI_DECODER_ERROR_FORMAT_CONTEXT_MAP_REPEAT); + } + do { + context_map[context_index++] = 0; + } while (--reps); + } + } + } + /* Fall through. */ + + case BROTLI_STATE_CONTEXT_MAP_TRANSFORM: { + brotli_reg_t bits; + if (!BrotliSafeReadBits(br, 1, &bits)) { + h->substate_context_map = BROTLI_STATE_CONTEXT_MAP_TRANSFORM; + return BROTLI_DECODER_NEEDS_MORE_INPUT; + } + if (bits != 0) { + InverseMoveToFrontTransform(*context_map_arg, context_map_size, s); + } + h->substate_context_map = BROTLI_STATE_CONTEXT_MAP_NONE; + return BROTLI_DECODER_SUCCESS; + } + + default: + return + BROTLI_FAILURE(BROTLI_DECODER_ERROR_UNREACHABLE); /* COV_NF_LINE */ + } +} + +/* Decodes a command or literal and updates block type ring-buffer. + Reads 3..54 bits. */ +static BROTLI_INLINE BROTLI_BOOL DecodeBlockTypeAndLength( + int safe, BrotliDecoderState* s, int tree_type) { + brotli_reg_t max_block_type = s->num_block_types[tree_type]; + const HuffmanCode* type_tree = &s->block_type_trees[ + tree_type * BROTLI_HUFFMAN_MAX_SIZE_258]; + const HuffmanCode* len_tree = &s->block_len_trees[ + tree_type * BROTLI_HUFFMAN_MAX_SIZE_26]; + BrotliBitReader* br = &s->br; + brotli_reg_t* ringbuffer = &s->block_type_rb[tree_type * 2]; + brotli_reg_t block_type; + if (max_block_type <= 1) { + return BROTLI_FALSE; + } + + /* Read 0..15 + 3..39 bits. */ + if (!safe) { + block_type = ReadSymbol(type_tree, br); + s->block_length[tree_type] = ReadBlockLength(len_tree, br); + } else { + BrotliBitReaderState memento; + BrotliBitReaderSaveState(br, &memento); + if (!SafeReadSymbol(type_tree, br, &block_type)) return BROTLI_FALSE; + if (!SafeReadBlockLength(s, &s->block_length[tree_type], len_tree, br)) { + s->substate_read_block_length = BROTLI_STATE_READ_BLOCK_LENGTH_NONE; + BrotliBitReaderRestoreState(br, &memento); + return BROTLI_FALSE; + } + } + + if (block_type == 1) { + block_type = ringbuffer[1] + 1; + } else if (block_type == 0) { + block_type = ringbuffer[0]; + } else { + block_type -= 2; + } + if (block_type >= max_block_type) { + block_type -= max_block_type; + } + ringbuffer[0] = ringbuffer[1]; + ringbuffer[1] = block_type; + return BROTLI_TRUE; +} + +static BROTLI_INLINE void DetectTrivialLiteralBlockTypes( + BrotliDecoderState* s) { + size_t i; + for (i = 0; i < 8; ++i) s->trivial_literal_contexts[i] = 0; + for (i = 0; i < s->num_block_types[0]; i++) { + size_t offset = i << BROTLI_LITERAL_CONTEXT_BITS; + size_t error = 0; + size_t sample = s->context_map[offset]; + size_t j; + for (j = 0; j < (1u << BROTLI_LITERAL_CONTEXT_BITS);) { + /* NOLINTNEXTLINE(bugprone-macro-repeated-side-effects) */ + BROTLI_REPEAT_4({ error |= s->context_map[offset + j++] ^ sample; }) + } + if (error == 0) { + s->trivial_literal_contexts[i >> 5] |= 1u << (i & 31); + } + } +} + +static BROTLI_INLINE void PrepareLiteralDecoding(BrotliDecoderState* s) { + uint8_t context_mode; + size_t trivial; + brotli_reg_t block_type = s->block_type_rb[1]; + brotli_reg_t context_offset = block_type << BROTLI_LITERAL_CONTEXT_BITS; + s->context_map_slice = s->context_map + context_offset; + trivial = s->trivial_literal_contexts[block_type >> 5]; + s->trivial_literal_context = (trivial >> (block_type & 31)) & 1; + s->literal_htree = s->literal_hgroup.htrees[s->context_map_slice[0]]; + context_mode = s->context_modes[block_type] & 3; + s->context_lookup = BROTLI_CONTEXT_LUT(context_mode); +} + +/* Decodes the block type and updates the state for literal context. + Reads 3..54 bits. */ +static BROTLI_INLINE BROTLI_BOOL DecodeLiteralBlockSwitchInternal( + int safe, BrotliDecoderState* s) { + if (!DecodeBlockTypeAndLength(safe, s, 0)) { + return BROTLI_FALSE; + } + PrepareLiteralDecoding(s); + return BROTLI_TRUE; +} + +static void BROTLI_NOINLINE DecodeLiteralBlockSwitch(BrotliDecoderState* s) { + DecodeLiteralBlockSwitchInternal(0, s); +} + +static BROTLI_BOOL BROTLI_NOINLINE SafeDecodeLiteralBlockSwitch( + BrotliDecoderState* s) { + return DecodeLiteralBlockSwitchInternal(1, s); +} + +/* Block switch for insert/copy length. + Reads 3..54 bits. */ +static BROTLI_INLINE BROTLI_BOOL DecodeCommandBlockSwitchInternal( + int safe, BrotliDecoderState* s) { + if (!DecodeBlockTypeAndLength(safe, s, 1)) { + return BROTLI_FALSE; + } + s->htree_command = s->insert_copy_hgroup.htrees[s->block_type_rb[3]]; + return BROTLI_TRUE; +} + +static void BROTLI_NOINLINE DecodeCommandBlockSwitch(BrotliDecoderState* s) { + DecodeCommandBlockSwitchInternal(0, s); +} + +static BROTLI_BOOL BROTLI_NOINLINE SafeDecodeCommandBlockSwitch( + BrotliDecoderState* s) { + return DecodeCommandBlockSwitchInternal(1, s); +} + +/* Block switch for distance codes. + Reads 3..54 bits. */ +static BROTLI_INLINE BROTLI_BOOL DecodeDistanceBlockSwitchInternal( + int safe, BrotliDecoderState* s) { + if (!DecodeBlockTypeAndLength(safe, s, 2)) { + return BROTLI_FALSE; + } + s->dist_context_map_slice = s->dist_context_map + + (s->block_type_rb[5] << BROTLI_DISTANCE_CONTEXT_BITS); + s->dist_htree_index = s->dist_context_map_slice[s->distance_context]; + return BROTLI_TRUE; +} + +static void BROTLI_NOINLINE DecodeDistanceBlockSwitch(BrotliDecoderState* s) { + DecodeDistanceBlockSwitchInternal(0, s); +} + +static BROTLI_BOOL BROTLI_NOINLINE SafeDecodeDistanceBlockSwitch( + BrotliDecoderState* s) { + return DecodeDistanceBlockSwitchInternal(1, s); +} + +static size_t UnwrittenBytes(const BrotliDecoderState* s, BROTLI_BOOL wrap) { + size_t pos = wrap && s->pos > s->ringbuffer_size ? + (size_t)s->ringbuffer_size : (size_t)(s->pos); + size_t partial_pos_rb = (s->rb_roundtrips * (size_t)s->ringbuffer_size) + pos; + return partial_pos_rb - s->partial_pos_out; +} + +/* Dumps output. + Returns BROTLI_DECODER_NEEDS_MORE_OUTPUT only if there is more output to push + and either ring-buffer is as big as window size, or |force| is true. */ +static BrotliDecoderErrorCode BROTLI_NOINLINE WriteRingBuffer( + BrotliDecoderState* s, size_t* available_out, uint8_t** next_out, + size_t* total_out, BROTLI_BOOL force) { + uint8_t* start = + s->ringbuffer + (s->partial_pos_out & (size_t)s->ringbuffer_mask); + size_t to_write = UnwrittenBytes(s, BROTLI_TRUE); + size_t num_written = *available_out; + if (num_written > to_write) { + num_written = to_write; + } + if (s->meta_block_remaining_len < 0) { + return BROTLI_FAILURE(BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_1); + } + if (next_out && !*next_out) { + *next_out = start; + } else { + if (next_out) { + memcpy(*next_out, start, num_written); + *next_out += num_written; + } + } + *available_out -= num_written; + BROTLI_LOG_UINT(to_write); + BROTLI_LOG_UINT(num_written); + s->partial_pos_out += num_written; + if (total_out) { + *total_out = s->partial_pos_out; + } + if (num_written < to_write) { + if (s->ringbuffer_size == (1 << s->window_bits) || force) { + return BROTLI_DECODER_NEEDS_MORE_OUTPUT; + } else { + return BROTLI_DECODER_SUCCESS; + } + } + /* Wrap ring buffer only if it has reached its maximal size. */ + if (s->ringbuffer_size == (1 << s->window_bits) && + s->pos >= s->ringbuffer_size) { + s->pos -= s->ringbuffer_size; + s->rb_roundtrips++; + s->should_wrap_ringbuffer = (size_t)s->pos != 0 ? 1 : 0; + } + return BROTLI_DECODER_SUCCESS; +} + +static void BROTLI_NOINLINE WrapRingBuffer(BrotliDecoderState* s) { + if (s->should_wrap_ringbuffer) { + memcpy(s->ringbuffer, s->ringbuffer_end, (size_t)s->pos); + s->should_wrap_ringbuffer = 0; + } +} + +/* Allocates ring-buffer. + + s->ringbuffer_size MUST be updated by BrotliCalculateRingBufferSize before + this function is called. + + Last two bytes of ring-buffer are initialized to 0, so context calculation + could be done uniformly for the first two and all other positions. */ +static BROTLI_BOOL BROTLI_NOINLINE BrotliEnsureRingBuffer( + BrotliDecoderState* s) { + uint8_t* old_ringbuffer = s->ringbuffer; + if (s->ringbuffer_size == s->new_ringbuffer_size) { + return BROTLI_TRUE; + } + + s->ringbuffer = (uint8_t*)BROTLI_DECODER_ALLOC(s, + (size_t)(s->new_ringbuffer_size) + kRingBufferWriteAheadSlack); + if (s->ringbuffer == 0) { + /* Restore previous value. */ + s->ringbuffer = old_ringbuffer; + return BROTLI_FALSE; + } + s->ringbuffer[s->new_ringbuffer_size - 2] = 0; + s->ringbuffer[s->new_ringbuffer_size - 1] = 0; + + if (!!old_ringbuffer) { + memcpy(s->ringbuffer, old_ringbuffer, (size_t)s->pos); + BROTLI_DECODER_FREE(s, old_ringbuffer); + } + + s->ringbuffer_size = s->new_ringbuffer_size; + s->ringbuffer_mask = s->new_ringbuffer_size - 1; + s->ringbuffer_end = s->ringbuffer + s->ringbuffer_size; + + return BROTLI_TRUE; +} + +static BrotliDecoderErrorCode BROTLI_NOINLINE +SkipMetadataBlock(BrotliDecoderState* s) { + BrotliBitReader* br = &s->br; + int nbytes; + + if (s->meta_block_remaining_len == 0) { + return BROTLI_DECODER_SUCCESS; + } + + BROTLI_DCHECK((BrotliGetAvailableBits(br) & 7) == 0); + + /* Drain accumulator. */ + if (BrotliGetAvailableBits(br) >= 8) { + uint8_t buffer[8]; + nbytes = (int)(BrotliGetAvailableBits(br)) >> 3; + BROTLI_DCHECK(nbytes <= 8); + if (nbytes > s->meta_block_remaining_len) { + nbytes = s->meta_block_remaining_len; + } + BrotliCopyBytes(buffer, br, (size_t)nbytes); + if (s->metadata_chunk_func) { + s->metadata_chunk_func(s->metadata_callback_opaque, buffer, + (size_t)nbytes); + } + s->meta_block_remaining_len -= nbytes; + if (s->meta_block_remaining_len == 0) { + return BROTLI_DECODER_SUCCESS; + } + } + + /* Direct access to metadata is possible. */ + nbytes = (int)BrotliGetRemainingBytes(br); + if (nbytes > s->meta_block_remaining_len) { + nbytes = s->meta_block_remaining_len; + } + if (nbytes > 0) { + if (s->metadata_chunk_func) { + s->metadata_chunk_func(s->metadata_callback_opaque, br->next_in, + (size_t)nbytes); + } + BrotliDropBytes(br, (size_t)nbytes); + s->meta_block_remaining_len -= nbytes; + if (s->meta_block_remaining_len == 0) { + return BROTLI_DECODER_SUCCESS; + } + } + + BROTLI_DCHECK(BrotliGetRemainingBytes(br) == 0); + + return BROTLI_DECODER_NEEDS_MORE_INPUT; +} + +static BrotliDecoderErrorCode BROTLI_NOINLINE CopyUncompressedBlockToOutput( + size_t* available_out, uint8_t** next_out, size_t* total_out, + BrotliDecoderState* s) { + /* TODO(eustas): avoid allocation for single uncompressed block. */ + if (!BrotliEnsureRingBuffer(s)) { + return BROTLI_FAILURE(BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_1); + } + + /* State machine */ + for (;;) { + switch (s->substate_uncompressed) { + case BROTLI_STATE_UNCOMPRESSED_NONE: { + int nbytes = (int)BrotliGetRemainingBytes(&s->br); + if (nbytes > s->meta_block_remaining_len) { + nbytes = s->meta_block_remaining_len; + } + if (s->pos + nbytes > s->ringbuffer_size) { + nbytes = s->ringbuffer_size - s->pos; + } + /* Copy remaining bytes from s->br.buf_ to ring-buffer. */ + BrotliCopyBytes(&s->ringbuffer[s->pos], &s->br, (size_t)nbytes); + s->pos += nbytes; + s->meta_block_remaining_len -= nbytes; + if (s->pos < 1 << s->window_bits) { + if (s->meta_block_remaining_len == 0) { + return BROTLI_DECODER_SUCCESS; + } + return BROTLI_DECODER_NEEDS_MORE_INPUT; + } + s->substate_uncompressed = BROTLI_STATE_UNCOMPRESSED_WRITE; + } + /* Fall through. */ + + case BROTLI_STATE_UNCOMPRESSED_WRITE: { + BrotliDecoderErrorCode result; + result = WriteRingBuffer( + s, available_out, next_out, total_out, BROTLI_FALSE); + if (result != BROTLI_DECODER_SUCCESS) { + return result; + } + if (s->ringbuffer_size == 1 << s->window_bits) { + s->max_distance = s->max_backward_distance; + } + s->substate_uncompressed = BROTLI_STATE_UNCOMPRESSED_NONE; + break; + } + } + } + BROTLI_DCHECK(0); /* Unreachable */ +} + +static BROTLI_BOOL AttachCompoundDictionary( + BrotliDecoderState* state, const uint8_t* data, size_t size) { + BrotliDecoderCompoundDictionary* addon = state->compound_dictionary; + if (state->state != BROTLI_STATE_UNINITED) return BROTLI_FALSE; + if (!addon) { + addon = (BrotliDecoderCompoundDictionary*)BROTLI_DECODER_ALLOC( + state, sizeof(BrotliDecoderCompoundDictionary)); + if (!addon) return BROTLI_FALSE; + addon->num_chunks = 0; + addon->total_size = 0; + addon->br_length = 0; + addon->br_copied = 0; + addon->block_bits = -1; + addon->chunk_offsets[0] = 0; + state->compound_dictionary = addon; + } + if (addon->num_chunks == 15) return BROTLI_FALSE; + addon->chunks[addon->num_chunks] = data; + addon->num_chunks++; + addon->total_size += (int)size; + addon->chunk_offsets[addon->num_chunks] = addon->total_size; + return BROTLI_TRUE; +} + +static void EnsureCompoundDictionaryInitialized(BrotliDecoderState* state) { + BrotliDecoderCompoundDictionary* addon = state->compound_dictionary; + /* 256 = (1 << 8) slots in block map. */ + int block_bits = 8; + int cursor = 0; + int index = 0; + if (addon->block_bits != -1) return; + while (((addon->total_size - 1) >> block_bits) != 0) block_bits++; + block_bits -= 8; + addon->block_bits = block_bits; + while (cursor < addon->total_size) { + while (addon->chunk_offsets[index + 1] < cursor) index++; + addon->block_map[cursor >> block_bits] = (uint8_t)index; + cursor += 1 << block_bits; + } +} + +static BROTLI_BOOL InitializeCompoundDictionaryCopy(BrotliDecoderState* s, + int address, int length) { + BrotliDecoderCompoundDictionary* addon = s->compound_dictionary; + int index; + EnsureCompoundDictionaryInitialized(s); + index = addon->block_map[address >> addon->block_bits]; + while (address >= addon->chunk_offsets[index + 1]) index++; + if (addon->total_size < address + length) return BROTLI_FALSE; + /* Update the recent distances cache. */ + s->dist_rb[s->dist_rb_idx & 3] = s->distance_code; + ++s->dist_rb_idx; + s->meta_block_remaining_len -= length; + addon->br_index = index; + addon->br_offset = address - addon->chunk_offsets[index]; + addon->br_length = length; + addon->br_copied = 0; + return BROTLI_TRUE; +} + +static int GetCompoundDictionarySize(BrotliDecoderState* s) { + return s->compound_dictionary ? s->compound_dictionary->total_size : 0; +} + +static int CopyFromCompoundDictionary(BrotliDecoderState* s, int pos) { + BrotliDecoderCompoundDictionary* addon = s->compound_dictionary; + int orig_pos = pos; + while (addon->br_length != addon->br_copied) { + uint8_t* copy_dst = &s->ringbuffer[pos]; + const uint8_t* copy_src = + addon->chunks[addon->br_index] + addon->br_offset; + int space = s->ringbuffer_size - pos; + int rem_chunk_length = (addon->chunk_offsets[addon->br_index + 1] - + addon->chunk_offsets[addon->br_index]) - addon->br_offset; + int length = addon->br_length - addon->br_copied; + if (length > rem_chunk_length) length = rem_chunk_length; + if (length > space) length = space; + memcpy(copy_dst, copy_src, (size_t)length); + pos += length; + addon->br_offset += length; + addon->br_copied += length; + if (length == rem_chunk_length) { + addon->br_index++; + addon->br_offset = 0; + } + if (pos == s->ringbuffer_size) break; + } + return pos - orig_pos; +} + +BROTLI_BOOL BrotliDecoderAttachDictionary( + BrotliDecoderState* state, BrotliSharedDictionaryType type, + size_t data_size, const uint8_t data[BROTLI_ARRAY_PARAM(data_size)]) { + brotli_reg_t i; + brotli_reg_t num_prefix_before = state->dictionary->num_prefix; + if (state->state != BROTLI_STATE_UNINITED) return BROTLI_FALSE; + if (!BrotliSharedDictionaryAttach(state->dictionary, type, data_size, data)) { + return BROTLI_FALSE; + } + for (i = num_prefix_before; i < state->dictionary->num_prefix; i++) { + if (!AttachCompoundDictionary( + state, state->dictionary->prefix[i], + state->dictionary->prefix_size[i])) { + return BROTLI_FALSE; + } + } + return BROTLI_TRUE; +} + +/* Calculates the smallest feasible ring buffer. + + If we know the data size is small, do not allocate more ring buffer + size than needed to reduce memory usage. + + When this method is called, metablock size and flags MUST be decoded. */ +static void BROTLI_NOINLINE BrotliCalculateRingBufferSize( + BrotliDecoderState* s) { + int window_size = 1 << s->window_bits; + int new_ringbuffer_size = window_size; + /* We need at least 2 bytes of ring buffer size to get the last two + bytes for context from there */ + int min_size = s->ringbuffer_size ? s->ringbuffer_size : 1024; + int output_size; + + /* If maximum is already reached, no further extension is retired. */ + if (s->ringbuffer_size == window_size) { + return; + } + + /* Metadata blocks does not touch ring buffer. */ + if (s->is_metadata) { + return; + } + + if (!s->ringbuffer) { + output_size = 0; + } else { + output_size = s->pos; + } + output_size += s->meta_block_remaining_len; + min_size = min_size < output_size ? output_size : min_size; + + if (!!s->canny_ringbuffer_allocation) { + /* Reduce ring buffer size to save memory when server is unscrupulous. + In worst case memory usage might be 1.5x bigger for a short period of + ring buffer reallocation. */ + while ((new_ringbuffer_size >> 1) >= min_size) { + new_ringbuffer_size >>= 1; + } + } + + s->new_ringbuffer_size = new_ringbuffer_size; +} + +/* Reads 1..256 2-bit context modes. */ +static BrotliDecoderErrorCode ReadContextModes(BrotliDecoderState* s) { + BrotliBitReader* br = &s->br; + int i = s->loop_counter; + + while (i < (int)s->num_block_types[0]) { + brotli_reg_t bits; + if (!BrotliSafeReadBits(br, 2, &bits)) { + s->loop_counter = i; + return BROTLI_DECODER_NEEDS_MORE_INPUT; + } + s->context_modes[i] = (uint8_t)bits; + BROTLI_LOG_ARRAY_INDEX(s->context_modes, i); + i++; + } + return BROTLI_DECODER_SUCCESS; +} + +static BROTLI_INLINE void TakeDistanceFromRingBuffer(BrotliDecoderState* s) { + int offset = s->distance_code - 3; + if (s->distance_code <= 3) { + /* Compensate double distance-ring-buffer roll for dictionary items. */ + s->distance_context = 1 >> s->distance_code; + s->distance_code = s->dist_rb[(s->dist_rb_idx - offset) & 3]; + s->dist_rb_idx -= s->distance_context; + } else { + int index_delta = 3; + int delta; + int base = s->distance_code - 10; + if (s->distance_code < 10) { + base = s->distance_code - 4; + } else { + index_delta = 2; + } + /* Unpack one of six 4-bit values. */ + delta = ((0x605142 >> (4 * base)) & 0xF) - 3; + s->distance_code = s->dist_rb[(s->dist_rb_idx + index_delta) & 0x3] + delta; + if (s->distance_code <= 0) { + /* A huge distance will cause a BROTLI_FAILURE() soon. + This is a little faster than failing here. */ + s->distance_code = 0x7FFFFFFF; + } + } +} + +static BROTLI_INLINE BROTLI_BOOL SafeReadBits( + BrotliBitReader* const br, brotli_reg_t n_bits, brotli_reg_t* val) { + if (n_bits != 0) { + return BrotliSafeReadBits(br, n_bits, val); + } else { + *val = 0; + return BROTLI_TRUE; + } +} + +static BROTLI_INLINE BROTLI_BOOL SafeReadBits32( + BrotliBitReader* const br, brotli_reg_t n_bits, brotli_reg_t* val) { + if (n_bits != 0) { + return BrotliSafeReadBits32(br, n_bits, val); + } else { + *val = 0; + return BROTLI_TRUE; + } +} + +/* + RFC 7932 Section 4 with "..." shortenings and "[]" emendations. + + Each distance ... is represented with a pair ... + The distance code is encoded using a prefix code... The number of extra bits + can be 0..24... Two additional parameters: NPOSTFIX (0..3), and ... + NDIRECT (0..120) ... are encoded in the meta-block header... + + The first 16 distance symbols ... reference past distances... ring buffer ... + Next NDIRECT distance symbols ... represent distances from 1 to NDIRECT... + [For] distance symbols 16 + NDIRECT and greater ... the number of extra bits + ... is given by the following formula: + + [ xcode = dcode - NDIRECT - 16 ] + ndistbits = 1 + [ xcode ] >> (NPOSTFIX + 1) + + ... +*/ + +/* + RFC 7932 Section 9.2 with "..." shortenings and "[]" emendations. + + ... to get the actual value of the parameter NDIRECT, left-shift this + four-bit number by NPOSTFIX bits ... +*/ + +/* Remaining formulas from RFC 7932 Section 4 could be rewritten as following: + + alphabet_size = 16 + NDIRECT + (max_distbits << (NPOSTFIX + 1)) + + half = ((xcode >> NPOSTFIX) & 1) << ndistbits + postfix = xcode & ((1 << NPOSTFIX) - 1) + range_start = 2 * (1 << ndistbits - 1 - 1) + + distance = (range_start + half + extra) << NPOSTFIX + postfix + NDIRECT + 1 + + NB: ndistbits >= 1 -> range_start >= 0 + NB: range_start has factor 2, as the range is covered by 2 "halves" + NB: extra -1 offset in range_start formula covers the absence of + ndistbits = 0 case + NB: when NPOSTFIX = 0, NDIRECT is not greater than 15 + + In other words, xcode has the following binary structure - XXXHPPP: + - XXX represent the number of extra distance bits + - H selects upper / lower range of distances + - PPP represent "postfix" + + "Regular" distance encoding has NPOSTFIX = 0; omitting the postfix part + simplifies distance calculation. + + Using NPOSTFIX > 0 allows cheaper encoding of regular structures, e.g. where + most of distances have the same reminder of division by 2/4/8. For example, + the table of int32_t values that come from different sources; if it is likely + that 3 highest bytes of values from the same source are the same, then + copy distance often looks like 4x + y. + + Distance calculation could be rewritten to: + + ndistbits = NDISTBITS(NDIRECT, NPOSTFIX)[dcode] + distance = OFFSET(NDIRECT, NPOSTFIX)[dcode] + extra << NPOSTFIX + + NDISTBITS and OFFSET could be pre-calculated, as NDIRECT and NPOSTFIX could + change only once per meta-block. +*/ + +/* Calculates distance lookup table. + NB: it is possible to have all 64 tables precalculated. */ +static void CalculateDistanceLut(BrotliDecoderState* s) { + BrotliMetablockBodyArena* b = &s->arena.body; + brotli_reg_t npostfix = s->distance_postfix_bits; + brotli_reg_t ndirect = s->num_direct_distance_codes; + brotli_reg_t alphabet_size_limit = s->distance_hgroup.alphabet_size_limit; + brotli_reg_t postfix = (brotli_reg_t)1u << npostfix; + brotli_reg_t j; + brotli_reg_t bits = 1; + brotli_reg_t half = 0; + + /* Skip short codes. */ + brotli_reg_t i = BROTLI_NUM_DISTANCE_SHORT_CODES; + + /* Fill direct codes. */ + for (j = 0; j < ndirect; ++j) { + b->dist_extra_bits[i] = 0; + b->dist_offset[i] = j + 1; + ++i; + } + + /* Fill regular distance codes. */ + while (i < alphabet_size_limit) { + brotli_reg_t base = ndirect + ((((2 + half) << bits) - 4) << npostfix) + 1; + /* Always fill the complete group. */ + for (j = 0; j < postfix; ++j) { + b->dist_extra_bits[i] = (uint8_t)bits; + b->dist_offset[i] = base + j; + ++i; + } + bits = bits + half; + half = half ^ 1; + } +} + +/* Precondition: s->distance_code < 0. */ +static BROTLI_INLINE BROTLI_BOOL ReadDistanceInternal( + int safe, BrotliDecoderState* s, BrotliBitReader* br) { + BrotliMetablockBodyArena* b = &s->arena.body; + brotli_reg_t code; + brotli_reg_t bits; + BrotliBitReaderState memento; + HuffmanCode* distance_tree = s->distance_hgroup.htrees[s->dist_htree_index]; + if (!safe) { + code = ReadSymbol(distance_tree, br); + } else { + BrotliBitReaderSaveState(br, &memento); + if (!SafeReadSymbol(distance_tree, br, &code)) { + return BROTLI_FALSE; + } + } + --s->block_length[2]; + /* Convert the distance code to the actual distance by possibly + looking up past distances from the s->dist_rb. */ + s->distance_context = 0; + if ((code & ~0xFu) == 0) { + s->distance_code = (int)code; + TakeDistanceFromRingBuffer(s); + return BROTLI_TRUE; + } + if (!safe) { + bits = BrotliReadBits32(br, b->dist_extra_bits[code]); + } else { + if (!SafeReadBits32(br, b->dist_extra_bits[code], &bits)) { + ++s->block_length[2]; + BrotliBitReaderRestoreState(br, &memento); + return BROTLI_FALSE; + } + } + s->distance_code = + (int)(b->dist_offset[code] + (bits << s->distance_postfix_bits)); + return BROTLI_TRUE; +} + +static BROTLI_INLINE void ReadDistance( + BrotliDecoderState* s, BrotliBitReader* br) { + ReadDistanceInternal(0, s, br); +} + +static BROTLI_INLINE BROTLI_BOOL SafeReadDistance( + BrotliDecoderState* s, BrotliBitReader* br) { + return ReadDistanceInternal(1, s, br); +} + +static BROTLI_INLINE BROTLI_BOOL ReadCommandInternal( + int safe, BrotliDecoderState* s, BrotliBitReader* br, int* insert_length) { + brotli_reg_t cmd_code; + brotli_reg_t insert_len_extra = 0; + brotli_reg_t copy_length; + CmdLutElement v; + BrotliBitReaderState memento; + if (!safe) { + cmd_code = ReadSymbol(s->htree_command, br); + } else { + BrotliBitReaderSaveState(br, &memento); + if (!SafeReadSymbol(s->htree_command, br, &cmd_code)) { + return BROTLI_FALSE; + } + } + v = kCmdLut[cmd_code]; + s->distance_code = v.distance_code; + s->distance_context = v.context; + s->dist_htree_index = s->dist_context_map_slice[s->distance_context]; + *insert_length = v.insert_len_offset; + if (!safe) { + if (BROTLI_PREDICT_FALSE(v.insert_len_extra_bits != 0)) { + insert_len_extra = BrotliReadBits24(br, v.insert_len_extra_bits); + } + copy_length = BrotliReadBits24(br, v.copy_len_extra_bits); + } else { + if (!SafeReadBits(br, v.insert_len_extra_bits, &insert_len_extra) || + !SafeReadBits(br, v.copy_len_extra_bits, ©_length)) { + BrotliBitReaderRestoreState(br, &memento); + return BROTLI_FALSE; + } + } + s->copy_length = (int)copy_length + v.copy_len_offset; + --s->block_length[1]; + *insert_length += (int)insert_len_extra; + return BROTLI_TRUE; +} + +static BROTLI_INLINE void ReadCommand( + BrotliDecoderState* s, BrotliBitReader* br, int* insert_length) { + ReadCommandInternal(0, s, br, insert_length); +} + +static BROTLI_INLINE BROTLI_BOOL SafeReadCommand( + BrotliDecoderState* s, BrotliBitReader* br, int* insert_length) { + return ReadCommandInternal(1, s, br, insert_length); +} + +static BROTLI_INLINE BROTLI_BOOL CheckInputAmount( + int safe, BrotliBitReader* const br) { + if (safe) { + return BROTLI_TRUE; + } + return BrotliCheckInputAmount(br); +} + +#define BROTLI_SAFE(METHOD) \ + { \ + if (safe) { \ + if (!Safe##METHOD) { \ + result = BROTLI_DECODER_NEEDS_MORE_INPUT; \ + goto saveStateAndReturn; \ + } \ + } else { \ + METHOD; \ + } \ + } + +static BROTLI_INLINE BrotliDecoderErrorCode ProcessCommandsInternal( + int safe, BrotliDecoderState* s) { + int pos = s->pos; + int i = s->loop_counter; + BrotliDecoderErrorCode result = BROTLI_DECODER_SUCCESS; + BrotliBitReader* br = &s->br; + int compound_dictionary_size = GetCompoundDictionarySize(s); + + if (!CheckInputAmount(safe, br)) { + result = BROTLI_DECODER_NEEDS_MORE_INPUT; + goto saveStateAndReturn; + } + if (!safe) { + BROTLI_UNUSED(BrotliWarmupBitReader(br)); + } + + /* Jump into state machine. */ + if (s->state == BROTLI_STATE_COMMAND_BEGIN) { + goto CommandBegin; + } else if (s->state == BROTLI_STATE_COMMAND_INNER) { + goto CommandInner; + } else if (s->state == BROTLI_STATE_COMMAND_POST_DECODE_LITERALS) { + goto CommandPostDecodeLiterals; + } else if (s->state == BROTLI_STATE_COMMAND_POST_WRAP_COPY) { + goto CommandPostWrapCopy; + } else { + return BROTLI_FAILURE(BROTLI_DECODER_ERROR_UNREACHABLE); /* COV_NF_LINE */ + } + +CommandBegin: + if (safe) { + s->state = BROTLI_STATE_COMMAND_BEGIN; + } + if (!CheckInputAmount(safe, br)) { + s->state = BROTLI_STATE_COMMAND_BEGIN; + result = BROTLI_DECODER_NEEDS_MORE_INPUT; + goto saveStateAndReturn; + } + if (BROTLI_PREDICT_FALSE(s->block_length[1] == 0)) { + BROTLI_SAFE(DecodeCommandBlockSwitch(s)); + goto CommandBegin; + } + /* Read the insert/copy length in the command. */ + BROTLI_SAFE(ReadCommand(s, br, &i)); + BROTLI_LOG(("[ProcessCommandsInternal] pos = %d insert = %d copy = %d\n", + pos, i, s->copy_length)); + if (i == 0) { + goto CommandPostDecodeLiterals; + } + s->meta_block_remaining_len -= i; + +CommandInner: + if (safe) { + s->state = BROTLI_STATE_COMMAND_INNER; + } + /* Read the literals in the command. */ + if (s->trivial_literal_context) { + brotli_reg_t bits; + brotli_reg_t value; + PreloadSymbol(safe, s->literal_htree, br, &bits, &value); + if (!safe) { + // This is a hottest part of the decode, so we copy the loop below + // and optimize it by calculating the number of steps where all checks + // evaluate to false (ringbuffer size/block size/input size). + // Since all checks are loop invariant, we just need to find + // minimal number of iterations for a simple loop, and run + // the full version for the remainder. + int num_steps = i - 1; + if (num_steps > 0 && ((brotli_reg_t)(num_steps) > s->block_length[0])) { + // Safe cast, since block_length < steps + num_steps = (int)s->block_length[0]; + } + if (s->ringbuffer_size >= pos && + (s->ringbuffer_size - pos) <= num_steps) { + num_steps = s->ringbuffer_size - pos - 1; + } + if (num_steps < 0) { + num_steps = 0; + } + num_steps = BrotliCopyPreloadedSymbolsToU8(s->literal_htree, br, &bits, + &value, s->ringbuffer, pos, + num_steps); + pos += num_steps; + s->block_length[0] -= (brotli_reg_t)num_steps; + i -= num_steps; + do { + if (!CheckInputAmount(safe, br)) { + s->state = BROTLI_STATE_COMMAND_INNER; + result = BROTLI_DECODER_NEEDS_MORE_INPUT; + goto saveStateAndReturn; + } + if (BROTLI_PREDICT_FALSE(s->block_length[0] == 0)) { + goto NextLiteralBlock; + } + BrotliCopyPreloadedSymbolsToU8(s->literal_htree, br, &bits, &value, + s->ringbuffer, pos, 1); + --s->block_length[0]; + BROTLI_LOG_ARRAY_INDEX(s->ringbuffer, pos); + ++pos; + if (BROTLI_PREDICT_FALSE(pos == s->ringbuffer_size)) { + s->state = BROTLI_STATE_COMMAND_INNER_WRITE; + --i; + goto saveStateAndReturn; + } + } while (--i != 0); + } else { /* safe */ + do { + if (BROTLI_PREDICT_FALSE(s->block_length[0] == 0)) { + goto NextLiteralBlock; + } + brotli_reg_t literal; + if (!SafeReadSymbol(s->literal_htree, br, &literal)) { + result = BROTLI_DECODER_NEEDS_MORE_INPUT; + goto saveStateAndReturn; + } + s->ringbuffer[pos] = (uint8_t)literal; + --s->block_length[0]; + BROTLI_LOG_ARRAY_INDEX(s->ringbuffer, pos); + ++pos; + if (BROTLI_PREDICT_FALSE(pos == s->ringbuffer_size)) { + s->state = BROTLI_STATE_COMMAND_INNER_WRITE; + --i; + goto saveStateAndReturn; + } + } while (--i != 0); + } + } else { + uint8_t p1 = s->ringbuffer[(pos - 1) & s->ringbuffer_mask]; + uint8_t p2 = s->ringbuffer[(pos - 2) & s->ringbuffer_mask]; + do { + const HuffmanCode* hc; + uint8_t context; + if (!CheckInputAmount(safe, br)) { + s->state = BROTLI_STATE_COMMAND_INNER; + result = BROTLI_DECODER_NEEDS_MORE_INPUT; + goto saveStateAndReturn; + } + if (BROTLI_PREDICT_FALSE(s->block_length[0] == 0)) { + goto NextLiteralBlock; + } + context = BROTLI_CONTEXT(p1, p2, s->context_lookup); + BROTLI_LOG_UINT(context); + hc = s->literal_hgroup.htrees[s->context_map_slice[context]]; + p2 = p1; + if (!safe) { + p1 = (uint8_t)ReadSymbol(hc, br); + } else { + brotli_reg_t literal; + if (!SafeReadSymbol(hc, br, &literal)) { + result = BROTLI_DECODER_NEEDS_MORE_INPUT; + goto saveStateAndReturn; + } + p1 = (uint8_t)literal; + } + s->ringbuffer[pos] = p1; + --s->block_length[0]; + BROTLI_LOG_UINT(s->context_map_slice[context]); + BROTLI_LOG_ARRAY_INDEX(s->ringbuffer, pos & s->ringbuffer_mask); + ++pos; + if (BROTLI_PREDICT_FALSE(pos == s->ringbuffer_size)) { + s->state = BROTLI_STATE_COMMAND_INNER_WRITE; + --i; + goto saveStateAndReturn; + } + } while (--i != 0); + } + BROTLI_LOG_UINT(s->meta_block_remaining_len); + if (BROTLI_PREDICT_FALSE(s->meta_block_remaining_len <= 0)) { + s->state = BROTLI_STATE_METABLOCK_DONE; + goto saveStateAndReturn; + } + +CommandPostDecodeLiterals: + if (safe) { + s->state = BROTLI_STATE_COMMAND_POST_DECODE_LITERALS; + } + if (s->distance_code >= 0) { + /* Implicit distance case. */ + s->distance_context = s->distance_code ? 0 : 1; + --s->dist_rb_idx; + s->distance_code = s->dist_rb[s->dist_rb_idx & 3]; + } else { + /* Read distance code in the command, unless it was implicitly zero. */ + if (BROTLI_PREDICT_FALSE(s->block_length[2] == 0)) { + BROTLI_SAFE(DecodeDistanceBlockSwitch(s)); + } + BROTLI_SAFE(ReadDistance(s, br)); + } + BROTLI_LOG(("[ProcessCommandsInternal] pos = %d distance = %d\n", + pos, s->distance_code)); + if (s->max_distance != s->max_backward_distance) { + s->max_distance = + (pos < s->max_backward_distance) ? pos : s->max_backward_distance; + } + i = s->copy_length; + /* Apply copy of LZ77 back-reference, or static dictionary reference if + the distance is larger than the max LZ77 distance */ + if (s->distance_code > s->max_distance) { + /* The maximum allowed distance is BROTLI_MAX_ALLOWED_DISTANCE = 0x7FFFFFFC. + With this choice, no signed overflow can occur after decoding + a special distance code (e.g., after adding 3 to the last distance). */ + if (s->distance_code > BROTLI_MAX_ALLOWED_DISTANCE) { + BROTLI_LOG(("Invalid backward reference. pos: %d distance: %d " + "len: %d bytes left: %d\n", + pos, s->distance_code, i, s->meta_block_remaining_len)); + return BROTLI_FAILURE(BROTLI_DECODER_ERROR_FORMAT_DISTANCE); + } + if (s->distance_code - s->max_distance - 1 < compound_dictionary_size) { + int address = compound_dictionary_size - + (s->distance_code - s->max_distance); + if (!InitializeCompoundDictionaryCopy(s, address, i)) { + return BROTLI_FAILURE(BROTLI_DECODER_ERROR_COMPOUND_DICTIONARY); + } + pos += CopyFromCompoundDictionary(s, pos); + if (pos >= s->ringbuffer_size) { + s->state = BROTLI_STATE_COMMAND_POST_WRITE_1; + goto saveStateAndReturn; + } + } else if (i >= SHARED_BROTLI_MIN_DICTIONARY_WORD_LENGTH && + i <= SHARED_BROTLI_MAX_DICTIONARY_WORD_LENGTH) { + uint8_t p1 = s->ringbuffer[(pos - 1) & s->ringbuffer_mask]; + uint8_t p2 = s->ringbuffer[(pos - 2) & s->ringbuffer_mask]; + uint8_t dict_id = s->dictionary->context_based ? + s->dictionary->context_map[BROTLI_CONTEXT(p1, p2, s->context_lookup)] + : 0; + const BrotliDictionary* words = s->dictionary->words[dict_id]; + const BrotliTransforms* transforms = s->dictionary->transforms[dict_id]; + int offset = (int)words->offsets_by_length[i]; + brotli_reg_t shift = words->size_bits_by_length[i]; + int address = + s->distance_code - s->max_distance - 1 - compound_dictionary_size; + int mask = (int)BitMask(shift); + int word_idx = address & mask; + int transform_idx = address >> shift; + /* Compensate double distance-ring-buffer roll. */ + s->dist_rb_idx += s->distance_context; + offset += word_idx * i; + /* If the distance is out of bound, select a next static dictionary if + there exist multiple. */ + if ((transform_idx >= (int)transforms->num_transforms || + words->size_bits_by_length[i] == 0) && + s->dictionary->num_dictionaries > 1) { + uint8_t dict_id2; + int dist_remaining = address - + (int)(((1u << shift) & ~1u)) * (int)transforms->num_transforms; + for (dict_id2 = 0; dict_id2 < s->dictionary->num_dictionaries; + dict_id2++) { + const BrotliDictionary* words2 = s->dictionary->words[dict_id2]; + if (dict_id2 != dict_id && words2->size_bits_by_length[i] != 0) { + const BrotliTransforms* transforms2 = + s->dictionary->transforms[dict_id2]; + brotli_reg_t shift2 = words2->size_bits_by_length[i]; + int num = (int)((1u << shift2) & ~1u) * + (int)transforms2->num_transforms; + if (dist_remaining < num) { + dict_id = dict_id2; + words = words2; + transforms = transforms2; + address = dist_remaining; + shift = shift2; + mask = (int)BitMask(shift); + word_idx = address & mask; + transform_idx = address >> shift; + offset = (int)words->offsets_by_length[i] + word_idx * i; + break; + } + dist_remaining -= num; + } + } + } + if (BROTLI_PREDICT_FALSE(words->size_bits_by_length[i] == 0)) { + BROTLI_LOG(("Invalid backward reference. pos: %d distance: %d " + "len: %d bytes left: %d\n", + pos, s->distance_code, i, s->meta_block_remaining_len)); + return BROTLI_FAILURE(BROTLI_DECODER_ERROR_FORMAT_DICTIONARY); + } + if (BROTLI_PREDICT_FALSE(!words->data)) { + return BROTLI_FAILURE(BROTLI_DECODER_ERROR_DICTIONARY_NOT_SET); + } + if (transform_idx < (int)transforms->num_transforms) { + const uint8_t* word = &words->data[offset]; + int len = i; + if (transform_idx == transforms->cutOffTransforms[0]) { + memcpy(&s->ringbuffer[pos], word, (size_t)len); + BROTLI_LOG(("[ProcessCommandsInternal] dictionary word: [%.*s]\n", + len, word)); + } else { + len = BrotliTransformDictionaryWord(&s->ringbuffer[pos], word, len, + transforms, transform_idx); + BROTLI_LOG(("[ProcessCommandsInternal] dictionary word: [%.*s]," + " transform_idx = %d, transformed: [%.*s]\n", + i, word, transform_idx, len, &s->ringbuffer[pos])); + if (len == 0 && s->distance_code <= 120) { + BROTLI_LOG(("Invalid length-0 dictionary word after transform\n")); + return BROTLI_FAILURE(BROTLI_DECODER_ERROR_FORMAT_TRANSFORM); + } + } + pos += len; + s->meta_block_remaining_len -= len; + if (pos >= s->ringbuffer_size) { + s->state = BROTLI_STATE_COMMAND_POST_WRITE_1; + goto saveStateAndReturn; + } + } else { + BROTLI_LOG(("Invalid backward reference. pos: %d distance: %d " + "len: %d bytes left: %d\n", + pos, s->distance_code, i, s->meta_block_remaining_len)); + return BROTLI_FAILURE(BROTLI_DECODER_ERROR_FORMAT_TRANSFORM); + } + } else { + BROTLI_LOG(("Invalid backward reference. pos: %d distance: %d " + "len: %d bytes left: %d\n", + pos, s->distance_code, i, s->meta_block_remaining_len)); + return BROTLI_FAILURE(BROTLI_DECODER_ERROR_FORMAT_DICTIONARY); + } + } else { + int src_start = (pos - s->distance_code) & s->ringbuffer_mask; + uint8_t* copy_dst = &s->ringbuffer[pos]; + uint8_t* copy_src = &s->ringbuffer[src_start]; + int dst_end = pos + i; + int src_end = src_start + i; + /* Update the recent distances cache. */ + s->dist_rb[s->dist_rb_idx & 3] = s->distance_code; + ++s->dist_rb_idx; + s->meta_block_remaining_len -= i; + /* There are 32+ bytes of slack in the ring-buffer allocation. + Also, we have 16 short codes, that make these 16 bytes irrelevant + in the ring-buffer. Let's copy over them as a first guess. */ + memmove16(copy_dst, copy_src); + if (src_end > pos && dst_end > src_start) { + /* Regions intersect. */ + goto CommandPostWrapCopy; + } + if (dst_end >= s->ringbuffer_size || src_end >= s->ringbuffer_size) { + /* At least one region wraps. */ + goto CommandPostWrapCopy; + } + pos += i; + if (i > 16) { + if (i > 32) { + memcpy(copy_dst + 16, copy_src + 16, (size_t)(i - 16)); + } else { + /* This branch covers about 45% cases. + Fixed size short copy allows more compiler optimizations. */ + memmove16(copy_dst + 16, copy_src + 16); + } + } + } + BROTLI_LOG_UINT(s->meta_block_remaining_len); + if (s->meta_block_remaining_len <= 0) { + /* Next metablock, if any. */ + s->state = BROTLI_STATE_METABLOCK_DONE; + goto saveStateAndReturn; + } else { + goto CommandBegin; + } +CommandPostWrapCopy: + { + int wrap_guard = s->ringbuffer_size - pos; + while (--i >= 0) { + s->ringbuffer[pos] = + s->ringbuffer[(pos - s->distance_code) & s->ringbuffer_mask]; + ++pos; + if (BROTLI_PREDICT_FALSE(--wrap_guard == 0)) { + s->state = BROTLI_STATE_COMMAND_POST_WRITE_2; + goto saveStateAndReturn; + } + } + } + if (s->meta_block_remaining_len <= 0) { + /* Next metablock, if any. */ + s->state = BROTLI_STATE_METABLOCK_DONE; + goto saveStateAndReturn; + } else { + goto CommandBegin; + } + +NextLiteralBlock: + BROTLI_SAFE(DecodeLiteralBlockSwitch(s)); + goto CommandInner; + +saveStateAndReturn: + s->pos = pos; + s->loop_counter = i; + return result; +} + +#undef BROTLI_SAFE + +static BROTLI_NOINLINE BrotliDecoderErrorCode ProcessCommands( + BrotliDecoderState* s) { + return ProcessCommandsInternal(0, s); +} + +static BROTLI_NOINLINE BrotliDecoderErrorCode SafeProcessCommands( + BrotliDecoderState* s) { + return ProcessCommandsInternal(1, s); +} + +BrotliDecoderResult BrotliDecoderDecompress( + size_t encoded_size, + const uint8_t encoded_buffer[BROTLI_ARRAY_PARAM(encoded_size)], + size_t* decoded_size, + uint8_t decoded_buffer[BROTLI_ARRAY_PARAM(*decoded_size)]) { + BrotliDecoderState s; + BrotliDecoderResult result; + size_t total_out = 0; + size_t available_in = encoded_size; + const uint8_t* next_in = encoded_buffer; + size_t available_out = *decoded_size; + uint8_t* next_out = decoded_buffer; + if (!BrotliDecoderStateInit(&s, 0, 0, 0)) { + return BROTLI_DECODER_RESULT_ERROR; + } + result = BrotliDecoderDecompressStream( + &s, &available_in, &next_in, &available_out, &next_out, &total_out); + *decoded_size = total_out; + BrotliDecoderStateCleanup(&s); + if (result != BROTLI_DECODER_RESULT_SUCCESS) { + result = BROTLI_DECODER_RESULT_ERROR; + } + return result; +} + +/* Invariant: input stream is never overconsumed: + - invalid input implies that the whole stream is invalid -> any amount of + input could be read and discarded + - when result is "needs more input", then at least one more byte is REQUIRED + to complete decoding; all input data MUST be consumed by decoder, so + client could swap the input buffer + - when result is "needs more output" decoder MUST ensure that it doesn't + hold more than 7 bits in bit reader; this saves client from swapping input + buffer ahead of time + - when result is "success" decoder MUST return all unused data back to input + buffer; this is possible because the invariant is held on enter */ +BrotliDecoderResult BrotliDecoderDecompressStream( + BrotliDecoderState* s, size_t* available_in, const uint8_t** next_in, + size_t* available_out, uint8_t** next_out, size_t* total_out) { + BrotliDecoderErrorCode result = BROTLI_DECODER_SUCCESS; + BrotliBitReader* br = &s->br; + size_t input_size = *available_in; +#define BROTLI_SAVE_ERROR_CODE(code) \ + SaveErrorCode(s, (code), input_size - *available_in) + /* Ensure that |total_out| is set, even if no data will ever be pushed out. */ + if (total_out) { + *total_out = s->partial_pos_out; + } + /* Do not try to process further in a case of unrecoverable error. */ + if ((int)s->error_code < 0) { + return BROTLI_DECODER_RESULT_ERROR; + } + if (*available_out && (!next_out || !*next_out)) { + return BROTLI_SAVE_ERROR_CODE( + BROTLI_FAILURE(BROTLI_DECODER_ERROR_INVALID_ARGUMENTS)); + } + if (!*available_out) next_out = 0; + if (s->buffer_length == 0) { /* Just connect bit reader to input stream. */ + BrotliBitReaderSetInput(br, *next_in, *available_in); + } else { + /* At least one byte of input is required. More than one byte of input may + be required to complete the transaction -> reading more data must be + done in a loop -> do it in a main loop. */ + result = BROTLI_DECODER_NEEDS_MORE_INPUT; + BrotliBitReaderSetInput(br, &s->buffer.u8[0], s->buffer_length); + } + /* State machine */ + for (;;) { + if (result != BROTLI_DECODER_SUCCESS) { + /* Error, needs more input/output. */ + if (result == BROTLI_DECODER_NEEDS_MORE_INPUT) { + if (s->ringbuffer != 0) { /* Pro-actively push output. */ + BrotliDecoderErrorCode intermediate_result = WriteRingBuffer(s, + available_out, next_out, total_out, BROTLI_TRUE); + /* WriteRingBuffer checks s->meta_block_remaining_len validity. */ + if ((int)intermediate_result < 0) { + result = intermediate_result; + break; + } + } + if (s->buffer_length != 0) { /* Used with internal buffer. */ + if (br->next_in == br->last_in) { + /* Successfully finished read transaction. + Accumulator contains less than 8 bits, because internal buffer + is expanded byte-by-byte until it is enough to complete read. */ + s->buffer_length = 0; + /* Switch to input stream and restart. */ + result = BROTLI_DECODER_SUCCESS; + BrotliBitReaderSetInput(br, *next_in, *available_in); + continue; + } else if (*available_in != 0) { + /* Not enough data in buffer, but can take one more byte from + input stream. */ + result = BROTLI_DECODER_SUCCESS; + BROTLI_DCHECK(s->buffer_length < 8); + s->buffer.u8[s->buffer_length] = **next_in; + s->buffer_length++; + BrotliBitReaderSetInput(br, &s->buffer.u8[0], s->buffer_length); + (*next_in)++; + (*available_in)--; + /* Retry with more data in buffer. */ + continue; + } + /* Can't finish reading and no more input. */ + break; + } else { /* Input stream doesn't contain enough input. */ + /* Copy tail to internal buffer and return. */ + *next_in = br->next_in; + *available_in = BrotliBitReaderGetAvailIn(br); + while (*available_in) { + s->buffer.u8[s->buffer_length] = **next_in; + s->buffer_length++; + (*next_in)++; + (*available_in)--; + } + break; + } + /* Unreachable. */ + } + + /* Fail or needs more output. */ + + if (s->buffer_length != 0) { + /* Just consumed the buffered input and produced some output. Otherwise + it would result in "needs more input". Reset internal buffer. */ + s->buffer_length = 0; + } else { + /* Using input stream in last iteration. When decoder switches to input + stream it has less than 8 bits in accumulator, so it is safe to + return unused accumulator bits there. */ + BrotliBitReaderUnload(br); + *available_in = BrotliBitReaderGetAvailIn(br); + *next_in = br->next_in; + } + break; + } + switch (s->state) { + case BROTLI_STATE_UNINITED: + /* Prepare to the first read. */ + if (!BrotliWarmupBitReader(br)) { + result = BROTLI_DECODER_NEEDS_MORE_INPUT; + break; + } + /* Decode window size. */ + result = DecodeWindowBits(s, br); /* Reads 1..8 bits. */ + if (result != BROTLI_DECODER_SUCCESS) { + break; + } + if (s->large_window) { + s->state = BROTLI_STATE_LARGE_WINDOW_BITS; + break; + } + s->state = BROTLI_STATE_INITIALIZE; + break; + + case BROTLI_STATE_LARGE_WINDOW_BITS: { + brotli_reg_t bits; + if (!BrotliSafeReadBits(br, 6, &bits)) { + result = BROTLI_DECODER_NEEDS_MORE_INPUT; + break; + } + s->window_bits = bits & 63u; + if (s->window_bits < BROTLI_LARGE_MIN_WBITS || + s->window_bits > BROTLI_LARGE_MAX_WBITS) { + result = BROTLI_FAILURE(BROTLI_DECODER_ERROR_FORMAT_WINDOW_BITS); + break; + } + s->state = BROTLI_STATE_INITIALIZE; + } + /* Fall through. */ + + case BROTLI_STATE_INITIALIZE: + BROTLI_LOG_UINT(s->window_bits); + /* Maximum distance, see section 9.1. of the spec. */ + s->max_backward_distance = (1 << s->window_bits) - BROTLI_WINDOW_GAP; + + /* Allocate memory for both block_type_trees and block_len_trees. */ + s->block_type_trees = (HuffmanCode*)BROTLI_DECODER_ALLOC(s, + sizeof(HuffmanCode) * 3 * + (BROTLI_HUFFMAN_MAX_SIZE_258 + BROTLI_HUFFMAN_MAX_SIZE_26)); + if (s->block_type_trees == 0) { + result = BROTLI_FAILURE(BROTLI_DECODER_ERROR_ALLOC_BLOCK_TYPE_TREES); + break; + } + s->block_len_trees = + s->block_type_trees + 3 * BROTLI_HUFFMAN_MAX_SIZE_258; + + s->state = BROTLI_STATE_METABLOCK_BEGIN; + /* Fall through. */ + + case BROTLI_STATE_METABLOCK_BEGIN: + BrotliDecoderStateMetablockBegin(s); + BROTLI_LOG_UINT(s->pos); + s->state = BROTLI_STATE_METABLOCK_HEADER; + /* Fall through. */ + + case BROTLI_STATE_METABLOCK_HEADER: + result = DecodeMetaBlockLength(s, br); /* Reads 2 - 31 bits. */ + if (result != BROTLI_DECODER_SUCCESS) { + break; + } + BROTLI_LOG_UINT(s->is_last_metablock); + BROTLI_LOG_UINT(s->meta_block_remaining_len); + BROTLI_LOG_UINT(s->is_metadata); + BROTLI_LOG_UINT(s->is_uncompressed); + if (s->is_metadata || s->is_uncompressed) { + if (!BrotliJumpToByteBoundary(br)) { + result = BROTLI_FAILURE(BROTLI_DECODER_ERROR_FORMAT_PADDING_1); + break; + } + } + if (s->is_metadata) { + s->state = BROTLI_STATE_METADATA; + if (s->metadata_start_func) { + s->metadata_start_func(s->metadata_callback_opaque, + (size_t)s->meta_block_remaining_len); + } + break; + } + if (s->meta_block_remaining_len == 0) { + s->state = BROTLI_STATE_METABLOCK_DONE; + break; + } + BrotliCalculateRingBufferSize(s); + if (s->is_uncompressed) { + s->state = BROTLI_STATE_UNCOMPRESSED; + break; + } + s->state = BROTLI_STATE_BEFORE_COMPRESSED_METABLOCK_HEADER; + /* Fall through. */ + + case BROTLI_STATE_BEFORE_COMPRESSED_METABLOCK_HEADER: { + BrotliMetablockHeaderArena* h = &s->arena.header; + s->loop_counter = 0; + /* Initialize compressed metablock header arena. */ + h->sub_loop_counter = 0; + /* Make small negative indexes addressable. */ + h->symbol_lists = + &h->symbols_lists_array[BROTLI_HUFFMAN_MAX_CODE_LENGTH + 1]; + h->substate_huffman = BROTLI_STATE_HUFFMAN_NONE; + h->substate_tree_group = BROTLI_STATE_TREE_GROUP_NONE; + h->substate_context_map = BROTLI_STATE_CONTEXT_MAP_NONE; + s->state = BROTLI_STATE_HUFFMAN_CODE_0; + } + /* Fall through. */ + + case BROTLI_STATE_HUFFMAN_CODE_0: + if (s->loop_counter >= 3) { + s->state = BROTLI_STATE_METABLOCK_HEADER_2; + break; + } + /* Reads 1..11 bits. */ + result = DecodeVarLenUint8(s, br, &s->num_block_types[s->loop_counter]); + if (result != BROTLI_DECODER_SUCCESS) { + break; + } + s->num_block_types[s->loop_counter]++; + BROTLI_LOG_UINT(s->num_block_types[s->loop_counter]); + if (s->num_block_types[s->loop_counter] < 2) { + s->loop_counter++; + break; + } + s->state = BROTLI_STATE_HUFFMAN_CODE_1; + /* Fall through. */ + + case BROTLI_STATE_HUFFMAN_CODE_1: { + brotli_reg_t alphabet_size = s->num_block_types[s->loop_counter] + 2; + int tree_offset = s->loop_counter * BROTLI_HUFFMAN_MAX_SIZE_258; + result = ReadHuffmanCode(alphabet_size, alphabet_size, + &s->block_type_trees[tree_offset], NULL, s); + if (result != BROTLI_DECODER_SUCCESS) break; + s->state = BROTLI_STATE_HUFFMAN_CODE_2; + } + /* Fall through. */ + + case BROTLI_STATE_HUFFMAN_CODE_2: { + brotli_reg_t alphabet_size = BROTLI_NUM_BLOCK_LEN_SYMBOLS; + int tree_offset = s->loop_counter * BROTLI_HUFFMAN_MAX_SIZE_26; + result = ReadHuffmanCode(alphabet_size, alphabet_size, + &s->block_len_trees[tree_offset], NULL, s); + if (result != BROTLI_DECODER_SUCCESS) break; + s->state = BROTLI_STATE_HUFFMAN_CODE_3; + } + /* Fall through. */ + + case BROTLI_STATE_HUFFMAN_CODE_3: { + int tree_offset = s->loop_counter * BROTLI_HUFFMAN_MAX_SIZE_26; + if (!SafeReadBlockLength(s, &s->block_length[s->loop_counter], + &s->block_len_trees[tree_offset], br)) { + result = BROTLI_DECODER_NEEDS_MORE_INPUT; + break; + } + BROTLI_LOG_UINT(s->block_length[s->loop_counter]); + s->loop_counter++; + s->state = BROTLI_STATE_HUFFMAN_CODE_0; + break; + } + + case BROTLI_STATE_UNCOMPRESSED: { + result = CopyUncompressedBlockToOutput( + available_out, next_out, total_out, s); + if (result != BROTLI_DECODER_SUCCESS) { + break; + } + s->state = BROTLI_STATE_METABLOCK_DONE; + break; + } + + case BROTLI_STATE_METADATA: + result = SkipMetadataBlock(s); + if (result != BROTLI_DECODER_SUCCESS) { + break; + } + s->state = BROTLI_STATE_METABLOCK_DONE; + break; + + case BROTLI_STATE_METABLOCK_HEADER_2: { + brotli_reg_t bits; + if (!BrotliSafeReadBits(br, 6, &bits)) { + result = BROTLI_DECODER_NEEDS_MORE_INPUT; + break; + } + s->distance_postfix_bits = bits & BitMask(2); + bits >>= 2; + s->num_direct_distance_codes = bits << s->distance_postfix_bits; + BROTLI_LOG_UINT(s->num_direct_distance_codes); + BROTLI_LOG_UINT(s->distance_postfix_bits); + s->context_modes = + (uint8_t*)BROTLI_DECODER_ALLOC(s, (size_t)s->num_block_types[0]); + if (s->context_modes == 0) { + result = BROTLI_FAILURE(BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MODES); + break; + } + s->loop_counter = 0; + s->state = BROTLI_STATE_CONTEXT_MODES; + } + /* Fall through. */ + + case BROTLI_STATE_CONTEXT_MODES: + result = ReadContextModes(s); + if (result != BROTLI_DECODER_SUCCESS) { + break; + } + s->state = BROTLI_STATE_CONTEXT_MAP_1; + /* Fall through. */ + + case BROTLI_STATE_CONTEXT_MAP_1: + result = DecodeContextMap( + s->num_block_types[0] << BROTLI_LITERAL_CONTEXT_BITS, + &s->num_literal_htrees, &s->context_map, s); + if (result != BROTLI_DECODER_SUCCESS) { + break; + } + DetectTrivialLiteralBlockTypes(s); + s->state = BROTLI_STATE_CONTEXT_MAP_2; + /* Fall through. */ + + case BROTLI_STATE_CONTEXT_MAP_2: { + brotli_reg_t npostfix = s->distance_postfix_bits; + brotli_reg_t ndirect = s->num_direct_distance_codes; + brotli_reg_t distance_alphabet_size_max = BROTLI_DISTANCE_ALPHABET_SIZE( + npostfix, ndirect, BROTLI_MAX_DISTANCE_BITS); + brotli_reg_t distance_alphabet_size_limit = distance_alphabet_size_max; + BROTLI_BOOL allocation_success = BROTLI_TRUE; + if (s->large_window) { + BrotliDistanceCodeLimit limit = BrotliCalculateDistanceCodeLimit( + BROTLI_MAX_ALLOWED_DISTANCE, (uint32_t)npostfix, + (uint32_t)ndirect); + distance_alphabet_size_max = BROTLI_DISTANCE_ALPHABET_SIZE( + npostfix, ndirect, BROTLI_LARGE_MAX_DISTANCE_BITS); + distance_alphabet_size_limit = limit.max_alphabet_size; + } + result = DecodeContextMap( + s->num_block_types[2] << BROTLI_DISTANCE_CONTEXT_BITS, + &s->num_dist_htrees, &s->dist_context_map, s); + if (result != BROTLI_DECODER_SUCCESS) { + break; + } + allocation_success &= BrotliDecoderHuffmanTreeGroupInit( + s, &s->literal_hgroup, BROTLI_NUM_LITERAL_SYMBOLS, + BROTLI_NUM_LITERAL_SYMBOLS, s->num_literal_htrees); + allocation_success &= BrotliDecoderHuffmanTreeGroupInit( + s, &s->insert_copy_hgroup, BROTLI_NUM_COMMAND_SYMBOLS, + BROTLI_NUM_COMMAND_SYMBOLS, s->num_block_types[1]); + allocation_success &= BrotliDecoderHuffmanTreeGroupInit( + s, &s->distance_hgroup, distance_alphabet_size_max, + distance_alphabet_size_limit, s->num_dist_htrees); + if (!allocation_success) { + return BROTLI_SAVE_ERROR_CODE( + BROTLI_FAILURE(BROTLI_DECODER_ERROR_ALLOC_TREE_GROUPS)); + } + s->loop_counter = 0; + s->state = BROTLI_STATE_TREE_GROUP; + } + /* Fall through. */ + + case BROTLI_STATE_TREE_GROUP: { + HuffmanTreeGroup* hgroup = NULL; + switch (s->loop_counter) { + case 0: hgroup = &s->literal_hgroup; break; + case 1: hgroup = &s->insert_copy_hgroup; break; + case 2: hgroup = &s->distance_hgroup; break; + default: return BROTLI_SAVE_ERROR_CODE(BROTLI_FAILURE( + BROTLI_DECODER_ERROR_UNREACHABLE)); /* COV_NF_LINE */ + } + result = HuffmanTreeGroupDecode(hgroup, s); + if (result != BROTLI_DECODER_SUCCESS) break; + s->loop_counter++; + if (s->loop_counter < 3) { + break; + } + s->state = BROTLI_STATE_BEFORE_COMPRESSED_METABLOCK_BODY; + } + /* Fall through. */ + + case BROTLI_STATE_BEFORE_COMPRESSED_METABLOCK_BODY: + PrepareLiteralDecoding(s); + s->dist_context_map_slice = s->dist_context_map; + s->htree_command = s->insert_copy_hgroup.htrees[0]; + if (!BrotliEnsureRingBuffer(s)) { + result = BROTLI_FAILURE(BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_2); + break; + } + CalculateDistanceLut(s); + s->state = BROTLI_STATE_COMMAND_BEGIN; + /* Fall through. */ + + case BROTLI_STATE_COMMAND_BEGIN: + /* Fall through. */ + case BROTLI_STATE_COMMAND_INNER: + /* Fall through. */ + case BROTLI_STATE_COMMAND_POST_DECODE_LITERALS: + /* Fall through. */ + case BROTLI_STATE_COMMAND_POST_WRAP_COPY: + result = ProcessCommands(s); + if (result == BROTLI_DECODER_NEEDS_MORE_INPUT) { + result = SafeProcessCommands(s); + } + break; + + case BROTLI_STATE_COMMAND_INNER_WRITE: + /* Fall through. */ + case BROTLI_STATE_COMMAND_POST_WRITE_1: + /* Fall through. */ + case BROTLI_STATE_COMMAND_POST_WRITE_2: + result = WriteRingBuffer( + s, available_out, next_out, total_out, BROTLI_FALSE); + if (result != BROTLI_DECODER_SUCCESS) { + break; + } + WrapRingBuffer(s); + if (s->ringbuffer_size == 1 << s->window_bits) { + s->max_distance = s->max_backward_distance; + } + if (s->state == BROTLI_STATE_COMMAND_POST_WRITE_1) { + BrotliDecoderCompoundDictionary* addon = s->compound_dictionary; + if (addon && (addon->br_length != addon->br_copied)) { + s->pos += CopyFromCompoundDictionary(s, s->pos); + if (s->pos >= s->ringbuffer_size) continue; + } + if (s->meta_block_remaining_len == 0) { + /* Next metablock, if any. */ + s->state = BROTLI_STATE_METABLOCK_DONE; + } else { + s->state = BROTLI_STATE_COMMAND_BEGIN; + } + break; + } else if (s->state == BROTLI_STATE_COMMAND_POST_WRITE_2) { + s->state = BROTLI_STATE_COMMAND_POST_WRAP_COPY; + } else { /* BROTLI_STATE_COMMAND_INNER_WRITE */ + if (s->loop_counter == 0) { + if (s->meta_block_remaining_len == 0) { + s->state = BROTLI_STATE_METABLOCK_DONE; + } else { + s->state = BROTLI_STATE_COMMAND_POST_DECODE_LITERALS; + } + break; + } + s->state = BROTLI_STATE_COMMAND_INNER; + } + break; + + case BROTLI_STATE_METABLOCK_DONE: + if (s->meta_block_remaining_len < 0) { + result = BROTLI_FAILURE(BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_2); + break; + } + BrotliDecoderStateCleanupAfterMetablock(s); + if (!s->is_last_metablock) { + s->state = BROTLI_STATE_METABLOCK_BEGIN; + break; + } + if (!BrotliJumpToByteBoundary(br)) { + result = BROTLI_FAILURE(BROTLI_DECODER_ERROR_FORMAT_PADDING_2); + break; + } + if (s->buffer_length == 0) { + BrotliBitReaderUnload(br); + *available_in = BrotliBitReaderGetAvailIn(br); + *next_in = br->next_in; + } + s->state = BROTLI_STATE_DONE; + /* Fall through. */ + + case BROTLI_STATE_DONE: + if (s->ringbuffer != 0) { + result = WriteRingBuffer( + s, available_out, next_out, total_out, BROTLI_TRUE); + if (result != BROTLI_DECODER_SUCCESS) { + break; + } + } + return BROTLI_SAVE_ERROR_CODE(result); + } + } + return BROTLI_SAVE_ERROR_CODE(result); +#undef BROTLI_SAVE_ERROR_CODE +} + +BROTLI_BOOL BrotliDecoderHasMoreOutput(const BrotliDecoderState* s) { + /* After unrecoverable error remaining output is considered nonsensical. */ + if ((int)s->error_code < 0) { + return BROTLI_FALSE; + } + return TO_BROTLI_BOOL( + s->ringbuffer != 0 && UnwrittenBytes(s, BROTLI_FALSE) != 0); +} + +const uint8_t* BrotliDecoderTakeOutput(BrotliDecoderState* s, size_t* size) { + uint8_t* result = 0; + size_t available_out = *size ? *size : 1u << 24; + size_t requested_out = available_out; + BrotliDecoderErrorCode status; + if ((s->ringbuffer == 0) || ((int)s->error_code < 0)) { + *size = 0; + return 0; + } + WrapRingBuffer(s); + status = WriteRingBuffer(s, &available_out, &result, 0, BROTLI_TRUE); + /* Either WriteRingBuffer returns those "success" codes... */ + if (status == BROTLI_DECODER_SUCCESS || + status == BROTLI_DECODER_NEEDS_MORE_OUTPUT) { + *size = requested_out - available_out; + } else { + /* ... or stream is broken. Normally this should be caught by + BrotliDecoderDecompressStream, this is just a safeguard. */ + if ((int)status < 0) SaveErrorCode(s, status, 0); + *size = 0; + result = 0; + } + return result; +} + +BROTLI_BOOL BrotliDecoderIsUsed(const BrotliDecoderState* s) { + return TO_BROTLI_BOOL(s->state != BROTLI_STATE_UNINITED || + BrotliGetAvailableBits(&s->br) != 0); +} + +BROTLI_BOOL BrotliDecoderIsFinished(const BrotliDecoderState* s) { + return TO_BROTLI_BOOL(s->state == BROTLI_STATE_DONE) && + !BrotliDecoderHasMoreOutput(s); +} + +BrotliDecoderErrorCode BrotliDecoderGetErrorCode(const BrotliDecoderState* s) { + return (BrotliDecoderErrorCode)s->error_code; +} + +const char* BrotliDecoderErrorString(BrotliDecoderErrorCode c) { + switch (c) { +#define BROTLI_ERROR_CODE_CASE_(PREFIX, NAME, CODE) \ + case BROTLI_DECODER ## PREFIX ## NAME: return #PREFIX #NAME; +#define BROTLI_NOTHING_ + BROTLI_DECODER_ERROR_CODES_LIST(BROTLI_ERROR_CODE_CASE_, BROTLI_NOTHING_) +#undef BROTLI_ERROR_CODE_CASE_ +#undef BROTLI_NOTHING_ + default: return "INVALID"; + } +} + +uint32_t BrotliDecoderVersion(void) { + return BROTLI_VERSION; +} + +void BrotliDecoderSetMetadataCallbacks( + BrotliDecoderState* state, + brotli_decoder_metadata_start_func start_func, + brotli_decoder_metadata_chunk_func chunk_func, void* opaque) { + state->metadata_start_func = start_func; + state->metadata_chunk_func = chunk_func; + state->metadata_callback_opaque = opaque; +} + +/* Escalate internal functions visibility; for testing purposes only. */ +#if defined(BROTLI_TEST) +BROTLI_BOOL BrotliSafeReadSymbolForTest( + const HuffmanCode*, BrotliBitReader*, brotli_reg_t*); +BROTLI_BOOL BrotliSafeReadSymbolForTest( + const HuffmanCode* table, BrotliBitReader* br, brotli_reg_t* result) { + return SafeReadSymbol(table, br, result); +} +void BrotliInverseMoveToFrontTransformForTest( + uint8_t*, brotli_reg_t, BrotliDecoderState*); +void BrotliInverseMoveToFrontTransformForTest( + uint8_t* v, brotli_reg_t l, BrotliDecoderState* s) { + InverseMoveToFrontTransform(v, l, s); +} +#endif + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif diff --git a/packages/runtime/vendor/brotli/c/dec/huffman.c b/packages/runtime/vendor/brotli/c/dec/huffman.c new file mode 100644 index 000000000..06486cf81 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/dec/huffman.c @@ -0,0 +1,339 @@ +/* Copyright 2013 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* Utilities for building Huffman decoding tables. */ + +#include "huffman.h" + +#include "../common/constants.h" +#include "../common/platform.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +#define BROTLI_REVERSE_BITS_MAX 8 + +#if defined(BROTLI_RBIT) +#define BROTLI_REVERSE_BITS_BASE \ + ((sizeof(brotli_reg_t) << 3) - BROTLI_REVERSE_BITS_MAX) +#else +#define BROTLI_REVERSE_BITS_BASE 0 +static BROTLI_MODEL("small") +uint8_t kReverseBits[1 << BROTLI_REVERSE_BITS_MAX] = { + 0x00, 0x80, 0x40, 0xC0, 0x20, 0xA0, 0x60, 0xE0, + 0x10, 0x90, 0x50, 0xD0, 0x30, 0xB0, 0x70, 0xF0, + 0x08, 0x88, 0x48, 0xC8, 0x28, 0xA8, 0x68, 0xE8, + 0x18, 0x98, 0x58, 0xD8, 0x38, 0xB8, 0x78, 0xF8, + 0x04, 0x84, 0x44, 0xC4, 0x24, 0xA4, 0x64, 0xE4, + 0x14, 0x94, 0x54, 0xD4, 0x34, 0xB4, 0x74, 0xF4, + 0x0C, 0x8C, 0x4C, 0xCC, 0x2C, 0xAC, 0x6C, 0xEC, + 0x1C, 0x9C, 0x5C, 0xDC, 0x3C, 0xBC, 0x7C, 0xFC, + 0x02, 0x82, 0x42, 0xC2, 0x22, 0xA2, 0x62, 0xE2, + 0x12, 0x92, 0x52, 0xD2, 0x32, 0xB2, 0x72, 0xF2, + 0x0A, 0x8A, 0x4A, 0xCA, 0x2A, 0xAA, 0x6A, 0xEA, + 0x1A, 0x9A, 0x5A, 0xDA, 0x3A, 0xBA, 0x7A, 0xFA, + 0x06, 0x86, 0x46, 0xC6, 0x26, 0xA6, 0x66, 0xE6, + 0x16, 0x96, 0x56, 0xD6, 0x36, 0xB6, 0x76, 0xF6, + 0x0E, 0x8E, 0x4E, 0xCE, 0x2E, 0xAE, 0x6E, 0xEE, + 0x1E, 0x9E, 0x5E, 0xDE, 0x3E, 0xBE, 0x7E, 0xFE, + 0x01, 0x81, 0x41, 0xC1, 0x21, 0xA1, 0x61, 0xE1, + 0x11, 0x91, 0x51, 0xD1, 0x31, 0xB1, 0x71, 0xF1, + 0x09, 0x89, 0x49, 0xC9, 0x29, 0xA9, 0x69, 0xE9, + 0x19, 0x99, 0x59, 0xD9, 0x39, 0xB9, 0x79, 0xF9, + 0x05, 0x85, 0x45, 0xC5, 0x25, 0xA5, 0x65, 0xE5, + 0x15, 0x95, 0x55, 0xD5, 0x35, 0xB5, 0x75, 0xF5, + 0x0D, 0x8D, 0x4D, 0xCD, 0x2D, 0xAD, 0x6D, 0xED, + 0x1D, 0x9D, 0x5D, 0xDD, 0x3D, 0xBD, 0x7D, 0xFD, + 0x03, 0x83, 0x43, 0xC3, 0x23, 0xA3, 0x63, 0xE3, + 0x13, 0x93, 0x53, 0xD3, 0x33, 0xB3, 0x73, 0xF3, + 0x0B, 0x8B, 0x4B, 0xCB, 0x2B, 0xAB, 0x6B, 0xEB, + 0x1B, 0x9B, 0x5B, 0xDB, 0x3B, 0xBB, 0x7B, 0xFB, + 0x07, 0x87, 0x47, 0xC7, 0x27, 0xA7, 0x67, 0xE7, + 0x17, 0x97, 0x57, 0xD7, 0x37, 0xB7, 0x77, 0xF7, + 0x0F, 0x8F, 0x4F, 0xCF, 0x2F, 0xAF, 0x6F, 0xEF, + 0x1F, 0x9F, 0x5F, 0xDF, 0x3F, 0xBF, 0x7F, 0xFF +}; +#endif /* BROTLI_RBIT */ + +#define BROTLI_REVERSE_BITS_LOWEST \ + ((brotli_reg_t)1 << (BROTLI_REVERSE_BITS_MAX - 1 + BROTLI_REVERSE_BITS_BASE)) + +/* Returns reverse(num >> BROTLI_REVERSE_BITS_BASE, BROTLI_REVERSE_BITS_MAX), + where reverse(value, len) is the bit-wise reversal of the len least + significant bits of value. */ +static BROTLI_INLINE brotli_reg_t BrotliReverseBits(brotli_reg_t num) { +#if defined(BROTLI_RBIT) + return BROTLI_RBIT(num); +#else + return kReverseBits[num]; +#endif +} + +/* Stores code in table[0], table[step], table[2*step], ..., table[end] */ +/* Assumes that end is an integer multiple of step */ +static BROTLI_INLINE void ReplicateValue(HuffmanCode* table, + int step, int end, + HuffmanCode code) { + do { + end -= step; + table[end] = code; + } while (end > 0); +} + +/* Returns the table width of the next 2nd level table. |count| is the histogram + of bit lengths for the remaining symbols, |len| is the code length of the + next processed symbol. */ +static BROTLI_INLINE int NextTableBitSize(const uint16_t* const count, + int len, int root_bits) { + int left = 1 << (len - root_bits); + while (len < BROTLI_HUFFMAN_MAX_CODE_LENGTH) { + left -= count[len]; + if (left <= 0) break; + ++len; + left <<= 1; + } + return len - root_bits; +} + +void BrotliBuildCodeLengthsHuffmanTable(HuffmanCode* table, + const uint8_t* const code_lengths, + uint16_t* count) { + HuffmanCode code; /* current table entry */ + int symbol; /* symbol index in original or sorted table */ + brotli_reg_t key; /* prefix code */ + brotli_reg_t key_step; /* prefix code addend */ + int step; /* step size to replicate values in current table */ + int table_size; /* size of current table */ + int sorted[BROTLI_CODE_LENGTH_CODES]; /* symbols sorted by code length */ + /* offsets in sorted table for each length */ + int offset[BROTLI_HUFFMAN_MAX_CODE_LENGTH_CODE_LENGTH + 1]; + int bits; + int bits_count; + BROTLI_DCHECK(BROTLI_HUFFMAN_MAX_CODE_LENGTH_CODE_LENGTH <= + BROTLI_REVERSE_BITS_MAX); + BROTLI_DCHECK(BROTLI_HUFFMAN_MAX_CODE_LENGTH_CODE_LENGTH == 5); + + /* Generate offsets into sorted symbol table by code length. */ + symbol = -1; + bits = 1; + /* BROTLI_HUFFMAN_MAX_CODE_LENGTH_CODE_LENGTH == 5 */ + BROTLI_REPEAT_5({ + symbol += count[bits]; + offset[bits] = symbol; + bits++; + }); + /* Symbols with code length 0 are placed after all other symbols. */ + offset[0] = BROTLI_CODE_LENGTH_CODES - 1; + + /* Sort symbols by length, by symbol order within each length. */ + symbol = BROTLI_CODE_LENGTH_CODES; + do { + BROTLI_REPEAT_6({ + symbol--; + sorted[offset[code_lengths[symbol]]--] = symbol; + }); + } while (symbol != 0); + + table_size = 1 << BROTLI_HUFFMAN_MAX_CODE_LENGTH_CODE_LENGTH; + + /* Special case: all symbols but one have 0 code length. */ + if (offset[0] == 0) { + code = ConstructHuffmanCode(0, (uint16_t)sorted[0]); + for (key = 0; key < (brotli_reg_t)table_size; ++key) { + table[key] = code; + } + return; + } + + /* Fill in table. */ + key = 0; + key_step = BROTLI_REVERSE_BITS_LOWEST; + symbol = 0; + bits = 1; + step = 2; + do { + for (bits_count = count[bits]; bits_count != 0; --bits_count) { + code = ConstructHuffmanCode((uint8_t)bits, (uint16_t)sorted[symbol++]); + ReplicateValue(&table[BrotliReverseBits(key)], step, table_size, code); + key += key_step; + } + step <<= 1; + key_step >>= 1; + } while (++bits <= BROTLI_HUFFMAN_MAX_CODE_LENGTH_CODE_LENGTH); +} + +uint32_t BrotliBuildHuffmanTable(HuffmanCode* root_table, + int root_bits, + const uint16_t* const symbol_lists, + uint16_t* count) { + HuffmanCode code; /* current table entry */ + HuffmanCode* table; /* next available space in table */ + int len; /* current code length */ + int symbol; /* symbol index in original or sorted table */ + brotli_reg_t key; /* prefix code */ + brotli_reg_t key_step; /* prefix code addend */ + brotli_reg_t sub_key; /* 2nd level table prefix code */ + brotli_reg_t sub_key_step; /* 2nd level table prefix code addend */ + int step; /* step size to replicate values in current table */ + int table_bits; /* key length of current table */ + int table_size; /* size of current table */ + int total_size; /* sum of root table size and 2nd level table sizes */ + int max_length = -1; + int bits; + int bits_count; + + BROTLI_DCHECK(root_bits <= BROTLI_REVERSE_BITS_MAX); + BROTLI_DCHECK(BROTLI_HUFFMAN_MAX_CODE_LENGTH - root_bits <= + BROTLI_REVERSE_BITS_MAX); + + while (symbol_lists[max_length] == 0xFFFF) max_length--; + max_length += BROTLI_HUFFMAN_MAX_CODE_LENGTH + 1; + + table = root_table; + table_bits = root_bits; + table_size = 1 << table_bits; + total_size = table_size; + + /* Fill in the root table. Reduce the table size to if possible, + and create the repetitions by memcpy. */ + if (table_bits > max_length) { + table_bits = max_length; + table_size = 1 << table_bits; + } + key = 0; + key_step = BROTLI_REVERSE_BITS_LOWEST; + bits = 1; + step = 2; + do { + symbol = bits - (BROTLI_HUFFMAN_MAX_CODE_LENGTH + 1); + for (bits_count = count[bits]; bits_count != 0; --bits_count) { + symbol = symbol_lists[symbol]; + code = ConstructHuffmanCode((uint8_t)bits, (uint16_t)symbol); + ReplicateValue(&table[BrotliReverseBits(key)], step, table_size, code); + key += key_step; + } + step <<= 1; + key_step >>= 1; + } while (++bits <= table_bits); + + /* If root_bits != table_bits then replicate to fill the remaining slots. */ + while (total_size != table_size) { + memcpy(&table[table_size], &table[0], + (size_t)table_size * sizeof(table[0])); + table_size <<= 1; + } + + /* Fill in 2nd level tables and add pointers to root table. */ + key_step = BROTLI_REVERSE_BITS_LOWEST >> (root_bits - 1); + sub_key = (BROTLI_REVERSE_BITS_LOWEST << 1); + sub_key_step = BROTLI_REVERSE_BITS_LOWEST; + for (len = root_bits + 1, step = 2; len <= max_length; ++len) { + symbol = len - (BROTLI_HUFFMAN_MAX_CODE_LENGTH + 1); + for (; count[len] != 0; --count[len]) { + if (sub_key == (BROTLI_REVERSE_BITS_LOWEST << 1U)) { + table += table_size; + table_bits = NextTableBitSize(count, len, root_bits); + table_size = 1 << table_bits; + total_size += table_size; + sub_key = BrotliReverseBits(key); + key += key_step; + root_table[sub_key] = ConstructHuffmanCode( + (uint8_t)(table_bits + root_bits), + (uint16_t)(((size_t)(table - root_table)) - sub_key)); + sub_key = 0; + } + symbol = symbol_lists[symbol]; + code = ConstructHuffmanCode((uint8_t)(len - root_bits), (uint16_t)symbol); + ReplicateValue( + &table[BrotliReverseBits(sub_key)], step, table_size, code); + sub_key += sub_key_step; + } + step <<= 1; + sub_key_step >>= 1; + } + return (uint32_t)total_size; +} + +uint32_t BrotliBuildSimpleHuffmanTable(HuffmanCode* table, + int root_bits, + uint16_t* val, + uint32_t num_symbols) { + uint32_t table_size = 1; + const uint32_t goal_size = 1U << root_bits; + switch (num_symbols) { + case 0: + table[0] = ConstructHuffmanCode(0, val[0]); + break; + case 1: + if (val[1] > val[0]) { + table[0] = ConstructHuffmanCode(1, val[0]); + table[1] = ConstructHuffmanCode(1, val[1]); + } else { + table[0] = ConstructHuffmanCode(1, val[1]); + table[1] = ConstructHuffmanCode(1, val[0]); + } + table_size = 2; + break; + case 2: + table[0] = ConstructHuffmanCode(1, val[0]); + table[2] = ConstructHuffmanCode(1, val[0]); + if (val[2] > val[1]) { + table[1] = ConstructHuffmanCode(2, val[1]); + table[3] = ConstructHuffmanCode(2, val[2]); + } else { + table[1] = ConstructHuffmanCode(2, val[2]); + table[3] = ConstructHuffmanCode(2, val[1]); + } + table_size = 4; + break; + case 3: { + int i, k; + for (i = 0; i < 3; ++i) { + for (k = i + 1; k < 4; ++k) { + if (val[k] < val[i]) { + uint16_t t = val[k]; + val[k] = val[i]; + val[i] = t; + } + } + } + table[0] = ConstructHuffmanCode(2, val[0]); + table[2] = ConstructHuffmanCode(2, val[1]); + table[1] = ConstructHuffmanCode(2, val[2]); + table[3] = ConstructHuffmanCode(2, val[3]); + table_size = 4; + break; + } + case 4: { + if (val[3] < val[2]) { + uint16_t t = val[3]; + val[3] = val[2]; + val[2] = t; + } + table[0] = ConstructHuffmanCode(1, val[0]); + table[1] = ConstructHuffmanCode(2, val[1]); + table[2] = ConstructHuffmanCode(1, val[0]); + table[3] = ConstructHuffmanCode(3, val[2]); + table[4] = ConstructHuffmanCode(1, val[0]); + table[5] = ConstructHuffmanCode(2, val[1]); + table[6] = ConstructHuffmanCode(1, val[0]); + table[7] = ConstructHuffmanCode(3, val[3]); + table_size = 8; + break; + } + } + while (table_size != goal_size) { + memcpy(&table[table_size], &table[0], + (size_t)table_size * sizeof(table[0])); + table_size <<= 1; + } + return goal_size; +} + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif diff --git a/packages/runtime/vendor/brotli/c/dec/huffman.h b/packages/runtime/vendor/brotli/c/dec/huffman.h new file mode 100644 index 000000000..53daf600e --- /dev/null +++ b/packages/runtime/vendor/brotli/c/dec/huffman.h @@ -0,0 +1,120 @@ +/* Copyright 2013 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* Utilities for building Huffman decoding tables. */ + +#ifndef BROTLI_DEC_HUFFMAN_H_ +#define BROTLI_DEC_HUFFMAN_H_ + +#include "../common/platform.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +#define BROTLI_HUFFMAN_MAX_CODE_LENGTH 15 + +/* BROTLI_NUM_BLOCK_LEN_SYMBOLS == 26 */ +#define BROTLI_HUFFMAN_MAX_SIZE_26 396 +/* BROTLI_MAX_BLOCK_TYPE_SYMBOLS == 258 */ +#define BROTLI_HUFFMAN_MAX_SIZE_258 632 +/* BROTLI_MAX_CONTEXT_MAP_SYMBOLS == 272 */ +#define BROTLI_HUFFMAN_MAX_SIZE_272 646 + +#define BROTLI_HUFFMAN_MAX_CODE_LENGTH_CODE_LENGTH 5 + +#if ((defined(BROTLI_TARGET_ARMV7) || defined(BROTLI_TARGET_ARMV8_32)) && \ + BROTLI_GNUC_HAS_ATTRIBUTE(aligned, 2, 7, 0)) +#define BROTLI_HUFFMAN_CODE_FAST_LOAD +#endif + +#if !defined(BROTLI_HUFFMAN_CODE_FAST_LOAD) +/* Do not create this struct directly - use the ConstructHuffmanCode + * constructor below! */ +typedef struct { + uint8_t bits; /* number of bits used for this symbol */ + uint16_t value; /* symbol value or table offset */ +} HuffmanCode; + +static BROTLI_INLINE HuffmanCode ConstructHuffmanCode(const uint8_t bits, + const uint16_t value) { + HuffmanCode h; + h.bits = bits; + h.value = value; + return h; +} + +/* Please use the following macros to optimize HuffmanCode accesses in hot + * paths. + * + * For example, assuming |table| contains a HuffmanCode pointer: + * + * BROTLI_HC_MARK_TABLE_FOR_FAST_LOAD(table); + * BROTLI_HC_ADJUST_TABLE_INDEX(table, index_into_table); + * *bits = BROTLI_HC_GET_BITS(table); + * *value = BROTLI_HC_GET_VALUE(table); + * BROTLI_HC_ADJUST_TABLE_INDEX(table, offset); + * *bits2 = BROTLI_HC_GET_BITS(table); + * *value2 = BROTLI_HC_GET_VALUE(table); + * + */ + +#define BROTLI_HC_MARK_TABLE_FOR_FAST_LOAD(H) +#define BROTLI_HC_ADJUST_TABLE_INDEX(H, V) H += (V) + +/* These must be given a HuffmanCode pointer! */ +#define BROTLI_HC_FAST_LOAD_BITS(H) (H->bits) +#define BROTLI_HC_FAST_LOAD_VALUE(H) (H->value) + +#else /* BROTLI_HUFFMAN_CODE_FAST_LOAD */ + +typedef BROTLI_ALIGNED(4) uint32_t HuffmanCode; + +static BROTLI_INLINE HuffmanCode ConstructHuffmanCode(const uint8_t bits, + const uint16_t value) { + return (HuffmanCode) ((value & 0xFFFF) << 16) | (bits & 0xFF); +} + +#define BROTLI_HC_MARK_TABLE_FOR_FAST_LOAD(H) uint32_t __fastload_##H = (*H) +#define BROTLI_HC_ADJUST_TABLE_INDEX(H, V) H += (V); __fastload_##H = (*H) + +/* These must be given a HuffmanCode pointer! */ +#define BROTLI_HC_FAST_LOAD_BITS(H) ((__fastload_##H) & 0xFF) +#define BROTLI_HC_FAST_LOAD_VALUE(H) ((__fastload_##H) >> 16) +#endif /* BROTLI_HUFFMAN_CODE_FAST_LOAD */ + +/* Builds Huffman lookup table assuming code lengths are in symbol order. */ +BROTLI_INTERNAL void BrotliBuildCodeLengthsHuffmanTable(HuffmanCode* root_table, + const uint8_t* const code_lengths, uint16_t* count); + +/* Builds Huffman lookup table assuming code lengths are in symbol order. + Returns size of resulting table. */ +BROTLI_INTERNAL uint32_t BrotliBuildHuffmanTable(HuffmanCode* root_table, + int root_bits, const uint16_t* const symbol_lists, uint16_t* count); + +/* Builds a simple Huffman table. The |num_symbols| parameter is to be + interpreted as follows: 0 means 1 symbol, 1 means 2 symbols, + 2 means 3 symbols, 3 means 4 symbols with lengths [2, 2, 2, 2], + 4 means 4 symbols with lengths [1, 2, 3, 3]. */ +BROTLI_INTERNAL uint32_t BrotliBuildSimpleHuffmanTable(HuffmanCode* table, + int root_bits, uint16_t* symbols, uint32_t num_symbols); + +/* Contains a collection of Huffman trees with the same alphabet size. */ +/* alphabet_size_limit is needed due to simple codes, since + log2(alphabet_size_max) could be greater than log2(alphabet_size_limit). */ +typedef struct { + HuffmanCode** htrees; + HuffmanCode* codes; + uint16_t alphabet_size_max; + uint16_t alphabet_size_limit; + uint16_t num_htrees; +} HuffmanTreeGroup; + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif + +#endif /* BROTLI_DEC_HUFFMAN_H_ */ diff --git a/packages/runtime/vendor/brotli/c/dec/prefix.c b/packages/runtime/vendor/brotli/c/dec/prefix.c new file mode 100644 index 000000000..ce3998f24 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/dec/prefix.c @@ -0,0 +1,67 @@ +/* Copyright 2025 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +#include "prefix.h" + +#include "../common/platform.h" /* IWYU pragma: keep */ +#include "../common/static_init.h" + +#if (BROTLI_STATIC_INIT != BROTLI_STATIC_INIT_NONE) +#include "../common/constants.h" +#endif + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +#if (BROTLI_STATIC_INIT == BROTLI_STATIC_INIT_NONE) +/* Embed kCmdLut. */ +#include "prefix_inc.h" +#else +BROTLI_COLD BROTLI_BOOL BrotliDecoderInitCmdLut(CmdLutElement* items) { + static const uint8_t kInsertLengthExtraBits[24] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x02, 0x02, 0x03, 0x03, + 0x04, 0x04, 0x05, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0C, 0x0E, 0x18}; + static const uint8_t kCopyLengthExtraBits[24] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x02, 0x02, + 0x03, 0x03, 0x04, 0x04, 0x05, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x18}; + static const uint8_t kCellPos[11] = {0, 1, 0, 1, 8, 9, 2, 16, 10, 17, 18}; + + uint16_t insert_length_offsets[24]; + uint16_t copy_length_offsets[24]; + insert_length_offsets[0] = 0; + copy_length_offsets[0] = 2; + for (size_t i = 0; i < 23; ++i) { + insert_length_offsets[i + 1] = + insert_length_offsets[i] + (uint16_t)(1u << kInsertLengthExtraBits[i]); + copy_length_offsets[i + 1] = + copy_length_offsets[i] + (uint16_t)(1u << kCopyLengthExtraBits[i]); + } + + for (size_t symbol = 0; symbol < BROTLI_NUM_COMMAND_SYMBOLS; ++symbol) { + CmdLutElement* item = items + symbol; + const size_t cell_idx = symbol >> 6; + const size_t cell_pos = kCellPos[cell_idx]; + const size_t copy_code = ((cell_pos << 3) & 0x18) + (symbol & 0x7); + const uint16_t copy_len_offset = copy_length_offsets[copy_code]; + const size_t insert_code = (cell_pos & 0x18) + ((symbol >> 3) & 0x7); + item->copy_len_extra_bits = kCopyLengthExtraBits[copy_code]; + item->context = (copy_len_offset > 4) ? 3 : ((uint8_t)copy_len_offset - 2); + item->copy_len_offset = copy_len_offset; + item->distance_code = (cell_idx >= 2) ? -1 : 0; + item->insert_len_extra_bits = kInsertLengthExtraBits[insert_code]; + item->insert_len_offset = insert_length_offsets[insert_code]; + } + return BROTLI_TRUE; +} + +BROTLI_MODEL("small") +CmdLutElement kCmdLut[BROTLI_NUM_COMMAND_SYMBOLS]; +#endif /* BROTLI_STATIC_INIT */ + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif diff --git a/packages/runtime/vendor/brotli/c/dec/prefix.h b/packages/runtime/vendor/brotli/c/dec/prefix.h new file mode 100644 index 000000000..f45b8be40 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/dec/prefix.h @@ -0,0 +1,43 @@ +/* Copyright 2013 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* Lookup tables to map prefix codes to value ranges. This is used during + decoding of the block lengths, literal insertion lengths and copy lengths. */ + +#ifndef BROTLI_DEC_PREFIX_H_ +#define BROTLI_DEC_PREFIX_H_ + +#include "../common/constants.h" +#include "../common/platform.h" /* IWYU pragma: keep */ +#include "../common/static_init.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +typedef struct CmdLutElement { + uint8_t insert_len_extra_bits; + uint8_t copy_len_extra_bits; + int8_t distance_code; + uint8_t context; + uint16_t insert_len_offset; + uint16_t copy_len_offset; +} CmdLutElement; + +#if (BROTLI_STATIC_INIT == BROTLI_STATIC_INIT_NONE) +BROTLI_INTERNAL extern const BROTLI_MODEL("small") + CmdLutElement kCmdLut[BROTLI_NUM_COMMAND_SYMBOLS]; +#else +BROTLI_INTERNAL BROTLI_BOOL BrotliDecoderInitCmdLut(CmdLutElement* items); +BROTLI_INTERNAL extern BROTLI_MODEL("small") + CmdLutElement kCmdLut[BROTLI_NUM_COMMAND_SYMBOLS]; +#endif + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif + +#endif /* BROTLI_DEC_PREFIX_H_ */ diff --git a/packages/runtime/vendor/brotli/c/dec/prefix_inc.h b/packages/runtime/vendor/brotli/c/dec/prefix_inc.h new file mode 100644 index 000000000..91d81b846 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/dec/prefix_inc.h @@ -0,0 +1,707 @@ +const BROTLI_MODEL("small") +CmdLutElement kCmdLut[BROTLI_NUM_COMMAND_SYMBOLS] = { + { 0x00, 0x00, 0, 0x00, 0x0000, 0x0002 }, + { 0x00, 0x00, 0, 0x01, 0x0000, 0x0003 }, + { 0x00, 0x00, 0, 0x02, 0x0000, 0x0004 }, + { 0x00, 0x00, 0, 0x03, 0x0000, 0x0005 }, + { 0x00, 0x00, 0, 0x03, 0x0000, 0x0006 }, + { 0x00, 0x00, 0, 0x03, 0x0000, 0x0007 }, + { 0x00, 0x00, 0, 0x03, 0x0000, 0x0008 }, + { 0x00, 0x00, 0, 0x03, 0x0000, 0x0009 }, + { 0x00, 0x00, 0, 0x00, 0x0001, 0x0002 }, + { 0x00, 0x00, 0, 0x01, 0x0001, 0x0003 }, + { 0x00, 0x00, 0, 0x02, 0x0001, 0x0004 }, + { 0x00, 0x00, 0, 0x03, 0x0001, 0x0005 }, + { 0x00, 0x00, 0, 0x03, 0x0001, 0x0006 }, + { 0x00, 0x00, 0, 0x03, 0x0001, 0x0007 }, + { 0x00, 0x00, 0, 0x03, 0x0001, 0x0008 }, + { 0x00, 0x00, 0, 0x03, 0x0001, 0x0009 }, + { 0x00, 0x00, 0, 0x00, 0x0002, 0x0002 }, + { 0x00, 0x00, 0, 0x01, 0x0002, 0x0003 }, + { 0x00, 0x00, 0, 0x02, 0x0002, 0x0004 }, + { 0x00, 0x00, 0, 0x03, 0x0002, 0x0005 }, + { 0x00, 0x00, 0, 0x03, 0x0002, 0x0006 }, + { 0x00, 0x00, 0, 0x03, 0x0002, 0x0007 }, + { 0x00, 0x00, 0, 0x03, 0x0002, 0x0008 }, + { 0x00, 0x00, 0, 0x03, 0x0002, 0x0009 }, + { 0x00, 0x00, 0, 0x00, 0x0003, 0x0002 }, + { 0x00, 0x00, 0, 0x01, 0x0003, 0x0003 }, + { 0x00, 0x00, 0, 0x02, 0x0003, 0x0004 }, + { 0x00, 0x00, 0, 0x03, 0x0003, 0x0005 }, + { 0x00, 0x00, 0, 0x03, 0x0003, 0x0006 }, + { 0x00, 0x00, 0, 0x03, 0x0003, 0x0007 }, + { 0x00, 0x00, 0, 0x03, 0x0003, 0x0008 }, + { 0x00, 0x00, 0, 0x03, 0x0003, 0x0009 }, + { 0x00, 0x00, 0, 0x00, 0x0004, 0x0002 }, + { 0x00, 0x00, 0, 0x01, 0x0004, 0x0003 }, + { 0x00, 0x00, 0, 0x02, 0x0004, 0x0004 }, + { 0x00, 0x00, 0, 0x03, 0x0004, 0x0005 }, + { 0x00, 0x00, 0, 0x03, 0x0004, 0x0006 }, + { 0x00, 0x00, 0, 0x03, 0x0004, 0x0007 }, + { 0x00, 0x00, 0, 0x03, 0x0004, 0x0008 }, + { 0x00, 0x00, 0, 0x03, 0x0004, 0x0009 }, + { 0x00, 0x00, 0, 0x00, 0x0005, 0x0002 }, + { 0x00, 0x00, 0, 0x01, 0x0005, 0x0003 }, + { 0x00, 0x00, 0, 0x02, 0x0005, 0x0004 }, + { 0x00, 0x00, 0, 0x03, 0x0005, 0x0005 }, + { 0x00, 0x00, 0, 0x03, 0x0005, 0x0006 }, + { 0x00, 0x00, 0, 0x03, 0x0005, 0x0007 }, + { 0x00, 0x00, 0, 0x03, 0x0005, 0x0008 }, + { 0x00, 0x00, 0, 0x03, 0x0005, 0x0009 }, + { 0x01, 0x00, 0, 0x00, 0x0006, 0x0002 }, + { 0x01, 0x00, 0, 0x01, 0x0006, 0x0003 }, + { 0x01, 0x00, 0, 0x02, 0x0006, 0x0004 }, + { 0x01, 0x00, 0, 0x03, 0x0006, 0x0005 }, + { 0x01, 0x00, 0, 0x03, 0x0006, 0x0006 }, + { 0x01, 0x00, 0, 0x03, 0x0006, 0x0007 }, + { 0x01, 0x00, 0, 0x03, 0x0006, 0x0008 }, + { 0x01, 0x00, 0, 0x03, 0x0006, 0x0009 }, + { 0x01, 0x00, 0, 0x00, 0x0008, 0x0002 }, + { 0x01, 0x00, 0, 0x01, 0x0008, 0x0003 }, + { 0x01, 0x00, 0, 0x02, 0x0008, 0x0004 }, + { 0x01, 0x00, 0, 0x03, 0x0008, 0x0005 }, + { 0x01, 0x00, 0, 0x03, 0x0008, 0x0006 }, + { 0x01, 0x00, 0, 0x03, 0x0008, 0x0007 }, + { 0x01, 0x00, 0, 0x03, 0x0008, 0x0008 }, + { 0x01, 0x00, 0, 0x03, 0x0008, 0x0009 }, + { 0x00, 0x01, 0, 0x03, 0x0000, 0x000a }, + { 0x00, 0x01, 0, 0x03, 0x0000, 0x000c }, + { 0x00, 0x02, 0, 0x03, 0x0000, 0x000e }, + { 0x00, 0x02, 0, 0x03, 0x0000, 0x0012 }, + { 0x00, 0x03, 0, 0x03, 0x0000, 0x0016 }, + { 0x00, 0x03, 0, 0x03, 0x0000, 0x001e }, + { 0x00, 0x04, 0, 0x03, 0x0000, 0x0026 }, + { 0x00, 0x04, 0, 0x03, 0x0000, 0x0036 }, + { 0x00, 0x01, 0, 0x03, 0x0001, 0x000a }, + { 0x00, 0x01, 0, 0x03, 0x0001, 0x000c }, + { 0x00, 0x02, 0, 0x03, 0x0001, 0x000e }, + { 0x00, 0x02, 0, 0x03, 0x0001, 0x0012 }, + { 0x00, 0x03, 0, 0x03, 0x0001, 0x0016 }, + { 0x00, 0x03, 0, 0x03, 0x0001, 0x001e }, + { 0x00, 0x04, 0, 0x03, 0x0001, 0x0026 }, + { 0x00, 0x04, 0, 0x03, 0x0001, 0x0036 }, + { 0x00, 0x01, 0, 0x03, 0x0002, 0x000a }, + { 0x00, 0x01, 0, 0x03, 0x0002, 0x000c }, + { 0x00, 0x02, 0, 0x03, 0x0002, 0x000e }, + { 0x00, 0x02, 0, 0x03, 0x0002, 0x0012 }, + { 0x00, 0x03, 0, 0x03, 0x0002, 0x0016 }, + { 0x00, 0x03, 0, 0x03, 0x0002, 0x001e }, + { 0x00, 0x04, 0, 0x03, 0x0002, 0x0026 }, + { 0x00, 0x04, 0, 0x03, 0x0002, 0x0036 }, + { 0x00, 0x01, 0, 0x03, 0x0003, 0x000a }, + { 0x00, 0x01, 0, 0x03, 0x0003, 0x000c }, + { 0x00, 0x02, 0, 0x03, 0x0003, 0x000e }, + { 0x00, 0x02, 0, 0x03, 0x0003, 0x0012 }, + { 0x00, 0x03, 0, 0x03, 0x0003, 0x0016 }, + { 0x00, 0x03, 0, 0x03, 0x0003, 0x001e }, + { 0x00, 0x04, 0, 0x03, 0x0003, 0x0026 }, + { 0x00, 0x04, 0, 0x03, 0x0003, 0x0036 }, + { 0x00, 0x01, 0, 0x03, 0x0004, 0x000a }, + { 0x00, 0x01, 0, 0x03, 0x0004, 0x000c }, + { 0x00, 0x02, 0, 0x03, 0x0004, 0x000e }, + { 0x00, 0x02, 0, 0x03, 0x0004, 0x0012 }, + { 0x00, 0x03, 0, 0x03, 0x0004, 0x0016 }, + { 0x00, 0x03, 0, 0x03, 0x0004, 0x001e }, + { 0x00, 0x04, 0, 0x03, 0x0004, 0x0026 }, + { 0x00, 0x04, 0, 0x03, 0x0004, 0x0036 }, + { 0x00, 0x01, 0, 0x03, 0x0005, 0x000a }, + { 0x00, 0x01, 0, 0x03, 0x0005, 0x000c }, + { 0x00, 0x02, 0, 0x03, 0x0005, 0x000e }, + { 0x00, 0x02, 0, 0x03, 0x0005, 0x0012 }, + { 0x00, 0x03, 0, 0x03, 0x0005, 0x0016 }, + { 0x00, 0x03, 0, 0x03, 0x0005, 0x001e }, + { 0x00, 0x04, 0, 0x03, 0x0005, 0x0026 }, + { 0x00, 0x04, 0, 0x03, 0x0005, 0x0036 }, + { 0x01, 0x01, 0, 0x03, 0x0006, 0x000a }, + { 0x01, 0x01, 0, 0x03, 0x0006, 0x000c }, + { 0x01, 0x02, 0, 0x03, 0x0006, 0x000e }, + { 0x01, 0x02, 0, 0x03, 0x0006, 0x0012 }, + { 0x01, 0x03, 0, 0x03, 0x0006, 0x0016 }, + { 0x01, 0x03, 0, 0x03, 0x0006, 0x001e }, + { 0x01, 0x04, 0, 0x03, 0x0006, 0x0026 }, + { 0x01, 0x04, 0, 0x03, 0x0006, 0x0036 }, + { 0x01, 0x01, 0, 0x03, 0x0008, 0x000a }, + { 0x01, 0x01, 0, 0x03, 0x0008, 0x000c }, + { 0x01, 0x02, 0, 0x03, 0x0008, 0x000e }, + { 0x01, 0x02, 0, 0x03, 0x0008, 0x0012 }, + { 0x01, 0x03, 0, 0x03, 0x0008, 0x0016 }, + { 0x01, 0x03, 0, 0x03, 0x0008, 0x001e }, + { 0x01, 0x04, 0, 0x03, 0x0008, 0x0026 }, + { 0x01, 0x04, 0, 0x03, 0x0008, 0x0036 }, + { 0x00, 0x00, -1, 0x00, 0x0000, 0x0002 }, + { 0x00, 0x00, -1, 0x01, 0x0000, 0x0003 }, + { 0x00, 0x00, -1, 0x02, 0x0000, 0x0004 }, + { 0x00, 0x00, -1, 0x03, 0x0000, 0x0005 }, + { 0x00, 0x00, -1, 0x03, 0x0000, 0x0006 }, + { 0x00, 0x00, -1, 0x03, 0x0000, 0x0007 }, + { 0x00, 0x00, -1, 0x03, 0x0000, 0x0008 }, + { 0x00, 0x00, -1, 0x03, 0x0000, 0x0009 }, + { 0x00, 0x00, -1, 0x00, 0x0001, 0x0002 }, + { 0x00, 0x00, -1, 0x01, 0x0001, 0x0003 }, + { 0x00, 0x00, -1, 0x02, 0x0001, 0x0004 }, + { 0x00, 0x00, -1, 0x03, 0x0001, 0x0005 }, + { 0x00, 0x00, -1, 0x03, 0x0001, 0x0006 }, + { 0x00, 0x00, -1, 0x03, 0x0001, 0x0007 }, + { 0x00, 0x00, -1, 0x03, 0x0001, 0x0008 }, + { 0x00, 0x00, -1, 0x03, 0x0001, 0x0009 }, + { 0x00, 0x00, -1, 0x00, 0x0002, 0x0002 }, + { 0x00, 0x00, -1, 0x01, 0x0002, 0x0003 }, + { 0x00, 0x00, -1, 0x02, 0x0002, 0x0004 }, + { 0x00, 0x00, -1, 0x03, 0x0002, 0x0005 }, + { 0x00, 0x00, -1, 0x03, 0x0002, 0x0006 }, + { 0x00, 0x00, -1, 0x03, 0x0002, 0x0007 }, + { 0x00, 0x00, -1, 0x03, 0x0002, 0x0008 }, + { 0x00, 0x00, -1, 0x03, 0x0002, 0x0009 }, + { 0x00, 0x00, -1, 0x00, 0x0003, 0x0002 }, + { 0x00, 0x00, -1, 0x01, 0x0003, 0x0003 }, + { 0x00, 0x00, -1, 0x02, 0x0003, 0x0004 }, + { 0x00, 0x00, -1, 0x03, 0x0003, 0x0005 }, + { 0x00, 0x00, -1, 0x03, 0x0003, 0x0006 }, + { 0x00, 0x00, -1, 0x03, 0x0003, 0x0007 }, + { 0x00, 0x00, -1, 0x03, 0x0003, 0x0008 }, + { 0x00, 0x00, -1, 0x03, 0x0003, 0x0009 }, + { 0x00, 0x00, -1, 0x00, 0x0004, 0x0002 }, + { 0x00, 0x00, -1, 0x01, 0x0004, 0x0003 }, + { 0x00, 0x00, -1, 0x02, 0x0004, 0x0004 }, + { 0x00, 0x00, -1, 0x03, 0x0004, 0x0005 }, + { 0x00, 0x00, -1, 0x03, 0x0004, 0x0006 }, + { 0x00, 0x00, -1, 0x03, 0x0004, 0x0007 }, + { 0x00, 0x00, -1, 0x03, 0x0004, 0x0008 }, + { 0x00, 0x00, -1, 0x03, 0x0004, 0x0009 }, + { 0x00, 0x00, -1, 0x00, 0x0005, 0x0002 }, + { 0x00, 0x00, -1, 0x01, 0x0005, 0x0003 }, + { 0x00, 0x00, -1, 0x02, 0x0005, 0x0004 }, + { 0x00, 0x00, -1, 0x03, 0x0005, 0x0005 }, + { 0x00, 0x00, -1, 0x03, 0x0005, 0x0006 }, + { 0x00, 0x00, -1, 0x03, 0x0005, 0x0007 }, + { 0x00, 0x00, -1, 0x03, 0x0005, 0x0008 }, + { 0x00, 0x00, -1, 0x03, 0x0005, 0x0009 }, + { 0x01, 0x00, -1, 0x00, 0x0006, 0x0002 }, + { 0x01, 0x00, -1, 0x01, 0x0006, 0x0003 }, + { 0x01, 0x00, -1, 0x02, 0x0006, 0x0004 }, + { 0x01, 0x00, -1, 0x03, 0x0006, 0x0005 }, + { 0x01, 0x00, -1, 0x03, 0x0006, 0x0006 }, + { 0x01, 0x00, -1, 0x03, 0x0006, 0x0007 }, + { 0x01, 0x00, -1, 0x03, 0x0006, 0x0008 }, + { 0x01, 0x00, -1, 0x03, 0x0006, 0x0009 }, + { 0x01, 0x00, -1, 0x00, 0x0008, 0x0002 }, + { 0x01, 0x00, -1, 0x01, 0x0008, 0x0003 }, + { 0x01, 0x00, -1, 0x02, 0x0008, 0x0004 }, + { 0x01, 0x00, -1, 0x03, 0x0008, 0x0005 }, + { 0x01, 0x00, -1, 0x03, 0x0008, 0x0006 }, + { 0x01, 0x00, -1, 0x03, 0x0008, 0x0007 }, + { 0x01, 0x00, -1, 0x03, 0x0008, 0x0008 }, + { 0x01, 0x00, -1, 0x03, 0x0008, 0x0009 }, + { 0x00, 0x01, -1, 0x03, 0x0000, 0x000a }, + { 0x00, 0x01, -1, 0x03, 0x0000, 0x000c }, + { 0x00, 0x02, -1, 0x03, 0x0000, 0x000e }, + { 0x00, 0x02, -1, 0x03, 0x0000, 0x0012 }, + { 0x00, 0x03, -1, 0x03, 0x0000, 0x0016 }, + { 0x00, 0x03, -1, 0x03, 0x0000, 0x001e }, + { 0x00, 0x04, -1, 0x03, 0x0000, 0x0026 }, + { 0x00, 0x04, -1, 0x03, 0x0000, 0x0036 }, + { 0x00, 0x01, -1, 0x03, 0x0001, 0x000a }, + { 0x00, 0x01, -1, 0x03, 0x0001, 0x000c }, + { 0x00, 0x02, -1, 0x03, 0x0001, 0x000e }, + { 0x00, 0x02, -1, 0x03, 0x0001, 0x0012 }, + { 0x00, 0x03, -1, 0x03, 0x0001, 0x0016 }, + { 0x00, 0x03, -1, 0x03, 0x0001, 0x001e }, + { 0x00, 0x04, -1, 0x03, 0x0001, 0x0026 }, + { 0x00, 0x04, -1, 0x03, 0x0001, 0x0036 }, + { 0x00, 0x01, -1, 0x03, 0x0002, 0x000a }, + { 0x00, 0x01, -1, 0x03, 0x0002, 0x000c }, + { 0x00, 0x02, -1, 0x03, 0x0002, 0x000e }, + { 0x00, 0x02, -1, 0x03, 0x0002, 0x0012 }, + { 0x00, 0x03, -1, 0x03, 0x0002, 0x0016 }, + { 0x00, 0x03, -1, 0x03, 0x0002, 0x001e }, + { 0x00, 0x04, -1, 0x03, 0x0002, 0x0026 }, + { 0x00, 0x04, -1, 0x03, 0x0002, 0x0036 }, + { 0x00, 0x01, -1, 0x03, 0x0003, 0x000a }, + { 0x00, 0x01, -1, 0x03, 0x0003, 0x000c }, + { 0x00, 0x02, -1, 0x03, 0x0003, 0x000e }, + { 0x00, 0x02, -1, 0x03, 0x0003, 0x0012 }, + { 0x00, 0x03, -1, 0x03, 0x0003, 0x0016 }, + { 0x00, 0x03, -1, 0x03, 0x0003, 0x001e }, + { 0x00, 0x04, -1, 0x03, 0x0003, 0x0026 }, + { 0x00, 0x04, -1, 0x03, 0x0003, 0x0036 }, + { 0x00, 0x01, -1, 0x03, 0x0004, 0x000a }, + { 0x00, 0x01, -1, 0x03, 0x0004, 0x000c }, + { 0x00, 0x02, -1, 0x03, 0x0004, 0x000e }, + { 0x00, 0x02, -1, 0x03, 0x0004, 0x0012 }, + { 0x00, 0x03, -1, 0x03, 0x0004, 0x0016 }, + { 0x00, 0x03, -1, 0x03, 0x0004, 0x001e }, + { 0x00, 0x04, -1, 0x03, 0x0004, 0x0026 }, + { 0x00, 0x04, -1, 0x03, 0x0004, 0x0036 }, + { 0x00, 0x01, -1, 0x03, 0x0005, 0x000a }, + { 0x00, 0x01, -1, 0x03, 0x0005, 0x000c }, + { 0x00, 0x02, -1, 0x03, 0x0005, 0x000e }, + { 0x00, 0x02, -1, 0x03, 0x0005, 0x0012 }, + { 0x00, 0x03, -1, 0x03, 0x0005, 0x0016 }, + { 0x00, 0x03, -1, 0x03, 0x0005, 0x001e }, + { 0x00, 0x04, -1, 0x03, 0x0005, 0x0026 }, + { 0x00, 0x04, -1, 0x03, 0x0005, 0x0036 }, + { 0x01, 0x01, -1, 0x03, 0x0006, 0x000a }, + { 0x01, 0x01, -1, 0x03, 0x0006, 0x000c }, + { 0x01, 0x02, -1, 0x03, 0x0006, 0x000e }, + { 0x01, 0x02, -1, 0x03, 0x0006, 0x0012 }, + { 0x01, 0x03, -1, 0x03, 0x0006, 0x0016 }, + { 0x01, 0x03, -1, 0x03, 0x0006, 0x001e }, + { 0x01, 0x04, -1, 0x03, 0x0006, 0x0026 }, + { 0x01, 0x04, -1, 0x03, 0x0006, 0x0036 }, + { 0x01, 0x01, -1, 0x03, 0x0008, 0x000a }, + { 0x01, 0x01, -1, 0x03, 0x0008, 0x000c }, + { 0x01, 0x02, -1, 0x03, 0x0008, 0x000e }, + { 0x01, 0x02, -1, 0x03, 0x0008, 0x0012 }, + { 0x01, 0x03, -1, 0x03, 0x0008, 0x0016 }, + { 0x01, 0x03, -1, 0x03, 0x0008, 0x001e }, + { 0x01, 0x04, -1, 0x03, 0x0008, 0x0026 }, + { 0x01, 0x04, -1, 0x03, 0x0008, 0x0036 }, + { 0x02, 0x00, -1, 0x00, 0x000a, 0x0002 }, + { 0x02, 0x00, -1, 0x01, 0x000a, 0x0003 }, + { 0x02, 0x00, -1, 0x02, 0x000a, 0x0004 }, + { 0x02, 0x00, -1, 0x03, 0x000a, 0x0005 }, + { 0x02, 0x00, -1, 0x03, 0x000a, 0x0006 }, + { 0x02, 0x00, -1, 0x03, 0x000a, 0x0007 }, + { 0x02, 0x00, -1, 0x03, 0x000a, 0x0008 }, + { 0x02, 0x00, -1, 0x03, 0x000a, 0x0009 }, + { 0x02, 0x00, -1, 0x00, 0x000e, 0x0002 }, + { 0x02, 0x00, -1, 0x01, 0x000e, 0x0003 }, + { 0x02, 0x00, -1, 0x02, 0x000e, 0x0004 }, + { 0x02, 0x00, -1, 0x03, 0x000e, 0x0005 }, + { 0x02, 0x00, -1, 0x03, 0x000e, 0x0006 }, + { 0x02, 0x00, -1, 0x03, 0x000e, 0x0007 }, + { 0x02, 0x00, -1, 0x03, 0x000e, 0x0008 }, + { 0x02, 0x00, -1, 0x03, 0x000e, 0x0009 }, + { 0x03, 0x00, -1, 0x00, 0x0012, 0x0002 }, + { 0x03, 0x00, -1, 0x01, 0x0012, 0x0003 }, + { 0x03, 0x00, -1, 0x02, 0x0012, 0x0004 }, + { 0x03, 0x00, -1, 0x03, 0x0012, 0x0005 }, + { 0x03, 0x00, -1, 0x03, 0x0012, 0x0006 }, + { 0x03, 0x00, -1, 0x03, 0x0012, 0x0007 }, + { 0x03, 0x00, -1, 0x03, 0x0012, 0x0008 }, + { 0x03, 0x00, -1, 0x03, 0x0012, 0x0009 }, + { 0x03, 0x00, -1, 0x00, 0x001a, 0x0002 }, + { 0x03, 0x00, -1, 0x01, 0x001a, 0x0003 }, + { 0x03, 0x00, -1, 0x02, 0x001a, 0x0004 }, + { 0x03, 0x00, -1, 0x03, 0x001a, 0x0005 }, + { 0x03, 0x00, -1, 0x03, 0x001a, 0x0006 }, + { 0x03, 0x00, -1, 0x03, 0x001a, 0x0007 }, + { 0x03, 0x00, -1, 0x03, 0x001a, 0x0008 }, + { 0x03, 0x00, -1, 0x03, 0x001a, 0x0009 }, + { 0x04, 0x00, -1, 0x00, 0x0022, 0x0002 }, + { 0x04, 0x00, -1, 0x01, 0x0022, 0x0003 }, + { 0x04, 0x00, -1, 0x02, 0x0022, 0x0004 }, + { 0x04, 0x00, -1, 0x03, 0x0022, 0x0005 }, + { 0x04, 0x00, -1, 0x03, 0x0022, 0x0006 }, + { 0x04, 0x00, -1, 0x03, 0x0022, 0x0007 }, + { 0x04, 0x00, -1, 0x03, 0x0022, 0x0008 }, + { 0x04, 0x00, -1, 0x03, 0x0022, 0x0009 }, + { 0x04, 0x00, -1, 0x00, 0x0032, 0x0002 }, + { 0x04, 0x00, -1, 0x01, 0x0032, 0x0003 }, + { 0x04, 0x00, -1, 0x02, 0x0032, 0x0004 }, + { 0x04, 0x00, -1, 0x03, 0x0032, 0x0005 }, + { 0x04, 0x00, -1, 0x03, 0x0032, 0x0006 }, + { 0x04, 0x00, -1, 0x03, 0x0032, 0x0007 }, + { 0x04, 0x00, -1, 0x03, 0x0032, 0x0008 }, + { 0x04, 0x00, -1, 0x03, 0x0032, 0x0009 }, + { 0x05, 0x00, -1, 0x00, 0x0042, 0x0002 }, + { 0x05, 0x00, -1, 0x01, 0x0042, 0x0003 }, + { 0x05, 0x00, -1, 0x02, 0x0042, 0x0004 }, + { 0x05, 0x00, -1, 0x03, 0x0042, 0x0005 }, + { 0x05, 0x00, -1, 0x03, 0x0042, 0x0006 }, + { 0x05, 0x00, -1, 0x03, 0x0042, 0x0007 }, + { 0x05, 0x00, -1, 0x03, 0x0042, 0x0008 }, + { 0x05, 0x00, -1, 0x03, 0x0042, 0x0009 }, + { 0x05, 0x00, -1, 0x00, 0x0062, 0x0002 }, + { 0x05, 0x00, -1, 0x01, 0x0062, 0x0003 }, + { 0x05, 0x00, -1, 0x02, 0x0062, 0x0004 }, + { 0x05, 0x00, -1, 0x03, 0x0062, 0x0005 }, + { 0x05, 0x00, -1, 0x03, 0x0062, 0x0006 }, + { 0x05, 0x00, -1, 0x03, 0x0062, 0x0007 }, + { 0x05, 0x00, -1, 0x03, 0x0062, 0x0008 }, + { 0x05, 0x00, -1, 0x03, 0x0062, 0x0009 }, + { 0x02, 0x01, -1, 0x03, 0x000a, 0x000a }, + { 0x02, 0x01, -1, 0x03, 0x000a, 0x000c }, + { 0x02, 0x02, -1, 0x03, 0x000a, 0x000e }, + { 0x02, 0x02, -1, 0x03, 0x000a, 0x0012 }, + { 0x02, 0x03, -1, 0x03, 0x000a, 0x0016 }, + { 0x02, 0x03, -1, 0x03, 0x000a, 0x001e }, + { 0x02, 0x04, -1, 0x03, 0x000a, 0x0026 }, + { 0x02, 0x04, -1, 0x03, 0x000a, 0x0036 }, + { 0x02, 0x01, -1, 0x03, 0x000e, 0x000a }, + { 0x02, 0x01, -1, 0x03, 0x000e, 0x000c }, + { 0x02, 0x02, -1, 0x03, 0x000e, 0x000e }, + { 0x02, 0x02, -1, 0x03, 0x000e, 0x0012 }, + { 0x02, 0x03, -1, 0x03, 0x000e, 0x0016 }, + { 0x02, 0x03, -1, 0x03, 0x000e, 0x001e }, + { 0x02, 0x04, -1, 0x03, 0x000e, 0x0026 }, + { 0x02, 0x04, -1, 0x03, 0x000e, 0x0036 }, + { 0x03, 0x01, -1, 0x03, 0x0012, 0x000a }, + { 0x03, 0x01, -1, 0x03, 0x0012, 0x000c }, + { 0x03, 0x02, -1, 0x03, 0x0012, 0x000e }, + { 0x03, 0x02, -1, 0x03, 0x0012, 0x0012 }, + { 0x03, 0x03, -1, 0x03, 0x0012, 0x0016 }, + { 0x03, 0x03, -1, 0x03, 0x0012, 0x001e }, + { 0x03, 0x04, -1, 0x03, 0x0012, 0x0026 }, + { 0x03, 0x04, -1, 0x03, 0x0012, 0x0036 }, + { 0x03, 0x01, -1, 0x03, 0x001a, 0x000a }, + { 0x03, 0x01, -1, 0x03, 0x001a, 0x000c }, + { 0x03, 0x02, -1, 0x03, 0x001a, 0x000e }, + { 0x03, 0x02, -1, 0x03, 0x001a, 0x0012 }, + { 0x03, 0x03, -1, 0x03, 0x001a, 0x0016 }, + { 0x03, 0x03, -1, 0x03, 0x001a, 0x001e }, + { 0x03, 0x04, -1, 0x03, 0x001a, 0x0026 }, + { 0x03, 0x04, -1, 0x03, 0x001a, 0x0036 }, + { 0x04, 0x01, -1, 0x03, 0x0022, 0x000a }, + { 0x04, 0x01, -1, 0x03, 0x0022, 0x000c }, + { 0x04, 0x02, -1, 0x03, 0x0022, 0x000e }, + { 0x04, 0x02, -1, 0x03, 0x0022, 0x0012 }, + { 0x04, 0x03, -1, 0x03, 0x0022, 0x0016 }, + { 0x04, 0x03, -1, 0x03, 0x0022, 0x001e }, + { 0x04, 0x04, -1, 0x03, 0x0022, 0x0026 }, + { 0x04, 0x04, -1, 0x03, 0x0022, 0x0036 }, + { 0x04, 0x01, -1, 0x03, 0x0032, 0x000a }, + { 0x04, 0x01, -1, 0x03, 0x0032, 0x000c }, + { 0x04, 0x02, -1, 0x03, 0x0032, 0x000e }, + { 0x04, 0x02, -1, 0x03, 0x0032, 0x0012 }, + { 0x04, 0x03, -1, 0x03, 0x0032, 0x0016 }, + { 0x04, 0x03, -1, 0x03, 0x0032, 0x001e }, + { 0x04, 0x04, -1, 0x03, 0x0032, 0x0026 }, + { 0x04, 0x04, -1, 0x03, 0x0032, 0x0036 }, + { 0x05, 0x01, -1, 0x03, 0x0042, 0x000a }, + { 0x05, 0x01, -1, 0x03, 0x0042, 0x000c }, + { 0x05, 0x02, -1, 0x03, 0x0042, 0x000e }, + { 0x05, 0x02, -1, 0x03, 0x0042, 0x0012 }, + { 0x05, 0x03, -1, 0x03, 0x0042, 0x0016 }, + { 0x05, 0x03, -1, 0x03, 0x0042, 0x001e }, + { 0x05, 0x04, -1, 0x03, 0x0042, 0x0026 }, + { 0x05, 0x04, -1, 0x03, 0x0042, 0x0036 }, + { 0x05, 0x01, -1, 0x03, 0x0062, 0x000a }, + { 0x05, 0x01, -1, 0x03, 0x0062, 0x000c }, + { 0x05, 0x02, -1, 0x03, 0x0062, 0x000e }, + { 0x05, 0x02, -1, 0x03, 0x0062, 0x0012 }, + { 0x05, 0x03, -1, 0x03, 0x0062, 0x0016 }, + { 0x05, 0x03, -1, 0x03, 0x0062, 0x001e }, + { 0x05, 0x04, -1, 0x03, 0x0062, 0x0026 }, + { 0x05, 0x04, -1, 0x03, 0x0062, 0x0036 }, + { 0x00, 0x05, -1, 0x03, 0x0000, 0x0046 }, + { 0x00, 0x05, -1, 0x03, 0x0000, 0x0066 }, + { 0x00, 0x06, -1, 0x03, 0x0000, 0x0086 }, + { 0x00, 0x07, -1, 0x03, 0x0000, 0x00c6 }, + { 0x00, 0x08, -1, 0x03, 0x0000, 0x0146 }, + { 0x00, 0x09, -1, 0x03, 0x0000, 0x0246 }, + { 0x00, 0x0a, -1, 0x03, 0x0000, 0x0446 }, + { 0x00, 0x18, -1, 0x03, 0x0000, 0x0846 }, + { 0x00, 0x05, -1, 0x03, 0x0001, 0x0046 }, + { 0x00, 0x05, -1, 0x03, 0x0001, 0x0066 }, + { 0x00, 0x06, -1, 0x03, 0x0001, 0x0086 }, + { 0x00, 0x07, -1, 0x03, 0x0001, 0x00c6 }, + { 0x00, 0x08, -1, 0x03, 0x0001, 0x0146 }, + { 0x00, 0x09, -1, 0x03, 0x0001, 0x0246 }, + { 0x00, 0x0a, -1, 0x03, 0x0001, 0x0446 }, + { 0x00, 0x18, -1, 0x03, 0x0001, 0x0846 }, + { 0x00, 0x05, -1, 0x03, 0x0002, 0x0046 }, + { 0x00, 0x05, -1, 0x03, 0x0002, 0x0066 }, + { 0x00, 0x06, -1, 0x03, 0x0002, 0x0086 }, + { 0x00, 0x07, -1, 0x03, 0x0002, 0x00c6 }, + { 0x00, 0x08, -1, 0x03, 0x0002, 0x0146 }, + { 0x00, 0x09, -1, 0x03, 0x0002, 0x0246 }, + { 0x00, 0x0a, -1, 0x03, 0x0002, 0x0446 }, + { 0x00, 0x18, -1, 0x03, 0x0002, 0x0846 }, + { 0x00, 0x05, -1, 0x03, 0x0003, 0x0046 }, + { 0x00, 0x05, -1, 0x03, 0x0003, 0x0066 }, + { 0x00, 0x06, -1, 0x03, 0x0003, 0x0086 }, + { 0x00, 0x07, -1, 0x03, 0x0003, 0x00c6 }, + { 0x00, 0x08, -1, 0x03, 0x0003, 0x0146 }, + { 0x00, 0x09, -1, 0x03, 0x0003, 0x0246 }, + { 0x00, 0x0a, -1, 0x03, 0x0003, 0x0446 }, + { 0x00, 0x18, -1, 0x03, 0x0003, 0x0846 }, + { 0x00, 0x05, -1, 0x03, 0x0004, 0x0046 }, + { 0x00, 0x05, -1, 0x03, 0x0004, 0x0066 }, + { 0x00, 0x06, -1, 0x03, 0x0004, 0x0086 }, + { 0x00, 0x07, -1, 0x03, 0x0004, 0x00c6 }, + { 0x00, 0x08, -1, 0x03, 0x0004, 0x0146 }, + { 0x00, 0x09, -1, 0x03, 0x0004, 0x0246 }, + { 0x00, 0x0a, -1, 0x03, 0x0004, 0x0446 }, + { 0x00, 0x18, -1, 0x03, 0x0004, 0x0846 }, + { 0x00, 0x05, -1, 0x03, 0x0005, 0x0046 }, + { 0x00, 0x05, -1, 0x03, 0x0005, 0x0066 }, + { 0x00, 0x06, -1, 0x03, 0x0005, 0x0086 }, + { 0x00, 0x07, -1, 0x03, 0x0005, 0x00c6 }, + { 0x00, 0x08, -1, 0x03, 0x0005, 0x0146 }, + { 0x00, 0x09, -1, 0x03, 0x0005, 0x0246 }, + { 0x00, 0x0a, -1, 0x03, 0x0005, 0x0446 }, + { 0x00, 0x18, -1, 0x03, 0x0005, 0x0846 }, + { 0x01, 0x05, -1, 0x03, 0x0006, 0x0046 }, + { 0x01, 0x05, -1, 0x03, 0x0006, 0x0066 }, + { 0x01, 0x06, -1, 0x03, 0x0006, 0x0086 }, + { 0x01, 0x07, -1, 0x03, 0x0006, 0x00c6 }, + { 0x01, 0x08, -1, 0x03, 0x0006, 0x0146 }, + { 0x01, 0x09, -1, 0x03, 0x0006, 0x0246 }, + { 0x01, 0x0a, -1, 0x03, 0x0006, 0x0446 }, + { 0x01, 0x18, -1, 0x03, 0x0006, 0x0846 }, + { 0x01, 0x05, -1, 0x03, 0x0008, 0x0046 }, + { 0x01, 0x05, -1, 0x03, 0x0008, 0x0066 }, + { 0x01, 0x06, -1, 0x03, 0x0008, 0x0086 }, + { 0x01, 0x07, -1, 0x03, 0x0008, 0x00c6 }, + { 0x01, 0x08, -1, 0x03, 0x0008, 0x0146 }, + { 0x01, 0x09, -1, 0x03, 0x0008, 0x0246 }, + { 0x01, 0x0a, -1, 0x03, 0x0008, 0x0446 }, + { 0x01, 0x18, -1, 0x03, 0x0008, 0x0846 }, + { 0x06, 0x00, -1, 0x00, 0x0082, 0x0002 }, + { 0x06, 0x00, -1, 0x01, 0x0082, 0x0003 }, + { 0x06, 0x00, -1, 0x02, 0x0082, 0x0004 }, + { 0x06, 0x00, -1, 0x03, 0x0082, 0x0005 }, + { 0x06, 0x00, -1, 0x03, 0x0082, 0x0006 }, + { 0x06, 0x00, -1, 0x03, 0x0082, 0x0007 }, + { 0x06, 0x00, -1, 0x03, 0x0082, 0x0008 }, + { 0x06, 0x00, -1, 0x03, 0x0082, 0x0009 }, + { 0x07, 0x00, -1, 0x00, 0x00c2, 0x0002 }, + { 0x07, 0x00, -1, 0x01, 0x00c2, 0x0003 }, + { 0x07, 0x00, -1, 0x02, 0x00c2, 0x0004 }, + { 0x07, 0x00, -1, 0x03, 0x00c2, 0x0005 }, + { 0x07, 0x00, -1, 0x03, 0x00c2, 0x0006 }, + { 0x07, 0x00, -1, 0x03, 0x00c2, 0x0007 }, + { 0x07, 0x00, -1, 0x03, 0x00c2, 0x0008 }, + { 0x07, 0x00, -1, 0x03, 0x00c2, 0x0009 }, + { 0x08, 0x00, -1, 0x00, 0x0142, 0x0002 }, + { 0x08, 0x00, -1, 0x01, 0x0142, 0x0003 }, + { 0x08, 0x00, -1, 0x02, 0x0142, 0x0004 }, + { 0x08, 0x00, -1, 0x03, 0x0142, 0x0005 }, + { 0x08, 0x00, -1, 0x03, 0x0142, 0x0006 }, + { 0x08, 0x00, -1, 0x03, 0x0142, 0x0007 }, + { 0x08, 0x00, -1, 0x03, 0x0142, 0x0008 }, + { 0x08, 0x00, -1, 0x03, 0x0142, 0x0009 }, + { 0x09, 0x00, -1, 0x00, 0x0242, 0x0002 }, + { 0x09, 0x00, -1, 0x01, 0x0242, 0x0003 }, + { 0x09, 0x00, -1, 0x02, 0x0242, 0x0004 }, + { 0x09, 0x00, -1, 0x03, 0x0242, 0x0005 }, + { 0x09, 0x00, -1, 0x03, 0x0242, 0x0006 }, + { 0x09, 0x00, -1, 0x03, 0x0242, 0x0007 }, + { 0x09, 0x00, -1, 0x03, 0x0242, 0x0008 }, + { 0x09, 0x00, -1, 0x03, 0x0242, 0x0009 }, + { 0x0a, 0x00, -1, 0x00, 0x0442, 0x0002 }, + { 0x0a, 0x00, -1, 0x01, 0x0442, 0x0003 }, + { 0x0a, 0x00, -1, 0x02, 0x0442, 0x0004 }, + { 0x0a, 0x00, -1, 0x03, 0x0442, 0x0005 }, + { 0x0a, 0x00, -1, 0x03, 0x0442, 0x0006 }, + { 0x0a, 0x00, -1, 0x03, 0x0442, 0x0007 }, + { 0x0a, 0x00, -1, 0x03, 0x0442, 0x0008 }, + { 0x0a, 0x00, -1, 0x03, 0x0442, 0x0009 }, + { 0x0c, 0x00, -1, 0x00, 0x0842, 0x0002 }, + { 0x0c, 0x00, -1, 0x01, 0x0842, 0x0003 }, + { 0x0c, 0x00, -1, 0x02, 0x0842, 0x0004 }, + { 0x0c, 0x00, -1, 0x03, 0x0842, 0x0005 }, + { 0x0c, 0x00, -1, 0x03, 0x0842, 0x0006 }, + { 0x0c, 0x00, -1, 0x03, 0x0842, 0x0007 }, + { 0x0c, 0x00, -1, 0x03, 0x0842, 0x0008 }, + { 0x0c, 0x00, -1, 0x03, 0x0842, 0x0009 }, + { 0x0e, 0x00, -1, 0x00, 0x1842, 0x0002 }, + { 0x0e, 0x00, -1, 0x01, 0x1842, 0x0003 }, + { 0x0e, 0x00, -1, 0x02, 0x1842, 0x0004 }, + { 0x0e, 0x00, -1, 0x03, 0x1842, 0x0005 }, + { 0x0e, 0x00, -1, 0x03, 0x1842, 0x0006 }, + { 0x0e, 0x00, -1, 0x03, 0x1842, 0x0007 }, + { 0x0e, 0x00, -1, 0x03, 0x1842, 0x0008 }, + { 0x0e, 0x00, -1, 0x03, 0x1842, 0x0009 }, + { 0x18, 0x00, -1, 0x00, 0x5842, 0x0002 }, + { 0x18, 0x00, -1, 0x01, 0x5842, 0x0003 }, + { 0x18, 0x00, -1, 0x02, 0x5842, 0x0004 }, + { 0x18, 0x00, -1, 0x03, 0x5842, 0x0005 }, + { 0x18, 0x00, -1, 0x03, 0x5842, 0x0006 }, + { 0x18, 0x00, -1, 0x03, 0x5842, 0x0007 }, + { 0x18, 0x00, -1, 0x03, 0x5842, 0x0008 }, + { 0x18, 0x00, -1, 0x03, 0x5842, 0x0009 }, + { 0x02, 0x05, -1, 0x03, 0x000a, 0x0046 }, + { 0x02, 0x05, -1, 0x03, 0x000a, 0x0066 }, + { 0x02, 0x06, -1, 0x03, 0x000a, 0x0086 }, + { 0x02, 0x07, -1, 0x03, 0x000a, 0x00c6 }, + { 0x02, 0x08, -1, 0x03, 0x000a, 0x0146 }, + { 0x02, 0x09, -1, 0x03, 0x000a, 0x0246 }, + { 0x02, 0x0a, -1, 0x03, 0x000a, 0x0446 }, + { 0x02, 0x18, -1, 0x03, 0x000a, 0x0846 }, + { 0x02, 0x05, -1, 0x03, 0x000e, 0x0046 }, + { 0x02, 0x05, -1, 0x03, 0x000e, 0x0066 }, + { 0x02, 0x06, -1, 0x03, 0x000e, 0x0086 }, + { 0x02, 0x07, -1, 0x03, 0x000e, 0x00c6 }, + { 0x02, 0x08, -1, 0x03, 0x000e, 0x0146 }, + { 0x02, 0x09, -1, 0x03, 0x000e, 0x0246 }, + { 0x02, 0x0a, -1, 0x03, 0x000e, 0x0446 }, + { 0x02, 0x18, -1, 0x03, 0x000e, 0x0846 }, + { 0x03, 0x05, -1, 0x03, 0x0012, 0x0046 }, + { 0x03, 0x05, -1, 0x03, 0x0012, 0x0066 }, + { 0x03, 0x06, -1, 0x03, 0x0012, 0x0086 }, + { 0x03, 0x07, -1, 0x03, 0x0012, 0x00c6 }, + { 0x03, 0x08, -1, 0x03, 0x0012, 0x0146 }, + { 0x03, 0x09, -1, 0x03, 0x0012, 0x0246 }, + { 0x03, 0x0a, -1, 0x03, 0x0012, 0x0446 }, + { 0x03, 0x18, -1, 0x03, 0x0012, 0x0846 }, + { 0x03, 0x05, -1, 0x03, 0x001a, 0x0046 }, + { 0x03, 0x05, -1, 0x03, 0x001a, 0x0066 }, + { 0x03, 0x06, -1, 0x03, 0x001a, 0x0086 }, + { 0x03, 0x07, -1, 0x03, 0x001a, 0x00c6 }, + { 0x03, 0x08, -1, 0x03, 0x001a, 0x0146 }, + { 0x03, 0x09, -1, 0x03, 0x001a, 0x0246 }, + { 0x03, 0x0a, -1, 0x03, 0x001a, 0x0446 }, + { 0x03, 0x18, -1, 0x03, 0x001a, 0x0846 }, + { 0x04, 0x05, -1, 0x03, 0x0022, 0x0046 }, + { 0x04, 0x05, -1, 0x03, 0x0022, 0x0066 }, + { 0x04, 0x06, -1, 0x03, 0x0022, 0x0086 }, + { 0x04, 0x07, -1, 0x03, 0x0022, 0x00c6 }, + { 0x04, 0x08, -1, 0x03, 0x0022, 0x0146 }, + { 0x04, 0x09, -1, 0x03, 0x0022, 0x0246 }, + { 0x04, 0x0a, -1, 0x03, 0x0022, 0x0446 }, + { 0x04, 0x18, -1, 0x03, 0x0022, 0x0846 }, + { 0x04, 0x05, -1, 0x03, 0x0032, 0x0046 }, + { 0x04, 0x05, -1, 0x03, 0x0032, 0x0066 }, + { 0x04, 0x06, -1, 0x03, 0x0032, 0x0086 }, + { 0x04, 0x07, -1, 0x03, 0x0032, 0x00c6 }, + { 0x04, 0x08, -1, 0x03, 0x0032, 0x0146 }, + { 0x04, 0x09, -1, 0x03, 0x0032, 0x0246 }, + { 0x04, 0x0a, -1, 0x03, 0x0032, 0x0446 }, + { 0x04, 0x18, -1, 0x03, 0x0032, 0x0846 }, + { 0x05, 0x05, -1, 0x03, 0x0042, 0x0046 }, + { 0x05, 0x05, -1, 0x03, 0x0042, 0x0066 }, + { 0x05, 0x06, -1, 0x03, 0x0042, 0x0086 }, + { 0x05, 0x07, -1, 0x03, 0x0042, 0x00c6 }, + { 0x05, 0x08, -1, 0x03, 0x0042, 0x0146 }, + { 0x05, 0x09, -1, 0x03, 0x0042, 0x0246 }, + { 0x05, 0x0a, -1, 0x03, 0x0042, 0x0446 }, + { 0x05, 0x18, -1, 0x03, 0x0042, 0x0846 }, + { 0x05, 0x05, -1, 0x03, 0x0062, 0x0046 }, + { 0x05, 0x05, -1, 0x03, 0x0062, 0x0066 }, + { 0x05, 0x06, -1, 0x03, 0x0062, 0x0086 }, + { 0x05, 0x07, -1, 0x03, 0x0062, 0x00c6 }, + { 0x05, 0x08, -1, 0x03, 0x0062, 0x0146 }, + { 0x05, 0x09, -1, 0x03, 0x0062, 0x0246 }, + { 0x05, 0x0a, -1, 0x03, 0x0062, 0x0446 }, + { 0x05, 0x18, -1, 0x03, 0x0062, 0x0846 }, + { 0x06, 0x01, -1, 0x03, 0x0082, 0x000a }, + { 0x06, 0x01, -1, 0x03, 0x0082, 0x000c }, + { 0x06, 0x02, -1, 0x03, 0x0082, 0x000e }, + { 0x06, 0x02, -1, 0x03, 0x0082, 0x0012 }, + { 0x06, 0x03, -1, 0x03, 0x0082, 0x0016 }, + { 0x06, 0x03, -1, 0x03, 0x0082, 0x001e }, + { 0x06, 0x04, -1, 0x03, 0x0082, 0x0026 }, + { 0x06, 0x04, -1, 0x03, 0x0082, 0x0036 }, + { 0x07, 0x01, -1, 0x03, 0x00c2, 0x000a }, + { 0x07, 0x01, -1, 0x03, 0x00c2, 0x000c }, + { 0x07, 0x02, -1, 0x03, 0x00c2, 0x000e }, + { 0x07, 0x02, -1, 0x03, 0x00c2, 0x0012 }, + { 0x07, 0x03, -1, 0x03, 0x00c2, 0x0016 }, + { 0x07, 0x03, -1, 0x03, 0x00c2, 0x001e }, + { 0x07, 0x04, -1, 0x03, 0x00c2, 0x0026 }, + { 0x07, 0x04, -1, 0x03, 0x00c2, 0x0036 }, + { 0x08, 0x01, -1, 0x03, 0x0142, 0x000a }, + { 0x08, 0x01, -1, 0x03, 0x0142, 0x000c }, + { 0x08, 0x02, -1, 0x03, 0x0142, 0x000e }, + { 0x08, 0x02, -1, 0x03, 0x0142, 0x0012 }, + { 0x08, 0x03, -1, 0x03, 0x0142, 0x0016 }, + { 0x08, 0x03, -1, 0x03, 0x0142, 0x001e }, + { 0x08, 0x04, -1, 0x03, 0x0142, 0x0026 }, + { 0x08, 0x04, -1, 0x03, 0x0142, 0x0036 }, + { 0x09, 0x01, -1, 0x03, 0x0242, 0x000a }, + { 0x09, 0x01, -1, 0x03, 0x0242, 0x000c }, + { 0x09, 0x02, -1, 0x03, 0x0242, 0x000e }, + { 0x09, 0x02, -1, 0x03, 0x0242, 0x0012 }, + { 0x09, 0x03, -1, 0x03, 0x0242, 0x0016 }, + { 0x09, 0x03, -1, 0x03, 0x0242, 0x001e }, + { 0x09, 0x04, -1, 0x03, 0x0242, 0x0026 }, + { 0x09, 0x04, -1, 0x03, 0x0242, 0x0036 }, + { 0x0a, 0x01, -1, 0x03, 0x0442, 0x000a }, + { 0x0a, 0x01, -1, 0x03, 0x0442, 0x000c }, + { 0x0a, 0x02, -1, 0x03, 0x0442, 0x000e }, + { 0x0a, 0x02, -1, 0x03, 0x0442, 0x0012 }, + { 0x0a, 0x03, -1, 0x03, 0x0442, 0x0016 }, + { 0x0a, 0x03, -1, 0x03, 0x0442, 0x001e }, + { 0x0a, 0x04, -1, 0x03, 0x0442, 0x0026 }, + { 0x0a, 0x04, -1, 0x03, 0x0442, 0x0036 }, + { 0x0c, 0x01, -1, 0x03, 0x0842, 0x000a }, + { 0x0c, 0x01, -1, 0x03, 0x0842, 0x000c }, + { 0x0c, 0x02, -1, 0x03, 0x0842, 0x000e }, + { 0x0c, 0x02, -1, 0x03, 0x0842, 0x0012 }, + { 0x0c, 0x03, -1, 0x03, 0x0842, 0x0016 }, + { 0x0c, 0x03, -1, 0x03, 0x0842, 0x001e }, + { 0x0c, 0x04, -1, 0x03, 0x0842, 0x0026 }, + { 0x0c, 0x04, -1, 0x03, 0x0842, 0x0036 }, + { 0x0e, 0x01, -1, 0x03, 0x1842, 0x000a }, + { 0x0e, 0x01, -1, 0x03, 0x1842, 0x000c }, + { 0x0e, 0x02, -1, 0x03, 0x1842, 0x000e }, + { 0x0e, 0x02, -1, 0x03, 0x1842, 0x0012 }, + { 0x0e, 0x03, -1, 0x03, 0x1842, 0x0016 }, + { 0x0e, 0x03, -1, 0x03, 0x1842, 0x001e }, + { 0x0e, 0x04, -1, 0x03, 0x1842, 0x0026 }, + { 0x0e, 0x04, -1, 0x03, 0x1842, 0x0036 }, + { 0x18, 0x01, -1, 0x03, 0x5842, 0x000a }, + { 0x18, 0x01, -1, 0x03, 0x5842, 0x000c }, + { 0x18, 0x02, -1, 0x03, 0x5842, 0x000e }, + { 0x18, 0x02, -1, 0x03, 0x5842, 0x0012 }, + { 0x18, 0x03, -1, 0x03, 0x5842, 0x0016 }, + { 0x18, 0x03, -1, 0x03, 0x5842, 0x001e }, + { 0x18, 0x04, -1, 0x03, 0x5842, 0x0026 }, + { 0x18, 0x04, -1, 0x03, 0x5842, 0x0036 }, + { 0x06, 0x05, -1, 0x03, 0x0082, 0x0046 }, + { 0x06, 0x05, -1, 0x03, 0x0082, 0x0066 }, + { 0x06, 0x06, -1, 0x03, 0x0082, 0x0086 }, + { 0x06, 0x07, -1, 0x03, 0x0082, 0x00c6 }, + { 0x06, 0x08, -1, 0x03, 0x0082, 0x0146 }, + { 0x06, 0x09, -1, 0x03, 0x0082, 0x0246 }, + { 0x06, 0x0a, -1, 0x03, 0x0082, 0x0446 }, + { 0x06, 0x18, -1, 0x03, 0x0082, 0x0846 }, + { 0x07, 0x05, -1, 0x03, 0x00c2, 0x0046 }, + { 0x07, 0x05, -1, 0x03, 0x00c2, 0x0066 }, + { 0x07, 0x06, -1, 0x03, 0x00c2, 0x0086 }, + { 0x07, 0x07, -1, 0x03, 0x00c2, 0x00c6 }, + { 0x07, 0x08, -1, 0x03, 0x00c2, 0x0146 }, + { 0x07, 0x09, -1, 0x03, 0x00c2, 0x0246 }, + { 0x07, 0x0a, -1, 0x03, 0x00c2, 0x0446 }, + { 0x07, 0x18, -1, 0x03, 0x00c2, 0x0846 }, + { 0x08, 0x05, -1, 0x03, 0x0142, 0x0046 }, + { 0x08, 0x05, -1, 0x03, 0x0142, 0x0066 }, + { 0x08, 0x06, -1, 0x03, 0x0142, 0x0086 }, + { 0x08, 0x07, -1, 0x03, 0x0142, 0x00c6 }, + { 0x08, 0x08, -1, 0x03, 0x0142, 0x0146 }, + { 0x08, 0x09, -1, 0x03, 0x0142, 0x0246 }, + { 0x08, 0x0a, -1, 0x03, 0x0142, 0x0446 }, + { 0x08, 0x18, -1, 0x03, 0x0142, 0x0846 }, + { 0x09, 0x05, -1, 0x03, 0x0242, 0x0046 }, + { 0x09, 0x05, -1, 0x03, 0x0242, 0x0066 }, + { 0x09, 0x06, -1, 0x03, 0x0242, 0x0086 }, + { 0x09, 0x07, -1, 0x03, 0x0242, 0x00c6 }, + { 0x09, 0x08, -1, 0x03, 0x0242, 0x0146 }, + { 0x09, 0x09, -1, 0x03, 0x0242, 0x0246 }, + { 0x09, 0x0a, -1, 0x03, 0x0242, 0x0446 }, + { 0x09, 0x18, -1, 0x03, 0x0242, 0x0846 }, + { 0x0a, 0x05, -1, 0x03, 0x0442, 0x0046 }, + { 0x0a, 0x05, -1, 0x03, 0x0442, 0x0066 }, + { 0x0a, 0x06, -1, 0x03, 0x0442, 0x0086 }, + { 0x0a, 0x07, -1, 0x03, 0x0442, 0x00c6 }, + { 0x0a, 0x08, -1, 0x03, 0x0442, 0x0146 }, + { 0x0a, 0x09, -1, 0x03, 0x0442, 0x0246 }, + { 0x0a, 0x0a, -1, 0x03, 0x0442, 0x0446 }, + { 0x0a, 0x18, -1, 0x03, 0x0442, 0x0846 }, + { 0x0c, 0x05, -1, 0x03, 0x0842, 0x0046 }, + { 0x0c, 0x05, -1, 0x03, 0x0842, 0x0066 }, + { 0x0c, 0x06, -1, 0x03, 0x0842, 0x0086 }, + { 0x0c, 0x07, -1, 0x03, 0x0842, 0x00c6 }, + { 0x0c, 0x08, -1, 0x03, 0x0842, 0x0146 }, + { 0x0c, 0x09, -1, 0x03, 0x0842, 0x0246 }, + { 0x0c, 0x0a, -1, 0x03, 0x0842, 0x0446 }, + { 0x0c, 0x18, -1, 0x03, 0x0842, 0x0846 }, + { 0x0e, 0x05, -1, 0x03, 0x1842, 0x0046 }, + { 0x0e, 0x05, -1, 0x03, 0x1842, 0x0066 }, + { 0x0e, 0x06, -1, 0x03, 0x1842, 0x0086 }, + { 0x0e, 0x07, -1, 0x03, 0x1842, 0x00c6 }, + { 0x0e, 0x08, -1, 0x03, 0x1842, 0x0146 }, + { 0x0e, 0x09, -1, 0x03, 0x1842, 0x0246 }, + { 0x0e, 0x0a, -1, 0x03, 0x1842, 0x0446 }, + { 0x0e, 0x18, -1, 0x03, 0x1842, 0x0846 }, + { 0x18, 0x05, -1, 0x03, 0x5842, 0x0046 }, + { 0x18, 0x05, -1, 0x03, 0x5842, 0x0066 }, + { 0x18, 0x06, -1, 0x03, 0x5842, 0x0086 }, + { 0x18, 0x07, -1, 0x03, 0x5842, 0x00c6 }, + { 0x18, 0x08, -1, 0x03, 0x5842, 0x0146 }, + { 0x18, 0x09, -1, 0x03, 0x5842, 0x0246 }, + { 0x18, 0x0a, -1, 0x03, 0x5842, 0x0446 }, + { 0x18, 0x18, -1, 0x03, 0x5842, 0x0846 }, +}; diff --git a/packages/runtime/vendor/brotli/c/dec/state.c b/packages/runtime/vendor/brotli/c/dec/state.c new file mode 100644 index 000000000..dcf61b9eb --- /dev/null +++ b/packages/runtime/vendor/brotli/c/dec/state.c @@ -0,0 +1,186 @@ +/* Copyright 2015 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +#include "state.h" + +#include "../common/dictionary.h" +#include "../common/platform.h" +#include "huffman.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +#ifdef BROTLI_REPORTING +/* When BROTLI_REPORTING is defined extra reporting module have to be linked. */ +void BrotliDecoderOnStart(const BrotliDecoderState* s); +void BrotliDecoderOnFinish(const BrotliDecoderState* s); +#define BROTLI_DECODER_ON_START(s) BrotliDecoderOnStart(s); +#define BROTLI_DECODER_ON_FINISH(s) BrotliDecoderOnFinish(s); +#else +#if !defined(BROTLI_DECODER_ON_START) +#define BROTLI_DECODER_ON_START(s) (void)(s); +#endif +#if !defined(BROTLI_DECODER_ON_FINISH) +#define BROTLI_DECODER_ON_FINISH(s) (void)(s); +#endif +#endif + +BROTLI_BOOL BrotliDecoderStateInit(BrotliDecoderState* s, + brotli_alloc_func alloc_func, brotli_free_func free_func, void* opaque) { + BROTLI_DECODER_ON_START(s); + if (!alloc_func) { + s->alloc_func = BrotliDefaultAllocFunc; + s->free_func = BrotliDefaultFreeFunc; + s->memory_manager_opaque = 0; + } else { + s->alloc_func = alloc_func; + s->free_func = free_func; + s->memory_manager_opaque = opaque; + } + + s->error_code = 0; /* BROTLI_DECODER_NO_ERROR */ + + BrotliInitBitReader(&s->br); + s->state = BROTLI_STATE_UNINITED; + s->large_window = 0; + s->substate_metablock_header = BROTLI_STATE_METABLOCK_HEADER_NONE; + s->substate_uncompressed = BROTLI_STATE_UNCOMPRESSED_NONE; + s->substate_decode_uint8 = BROTLI_STATE_DECODE_UINT8_NONE; + s->substate_read_block_length = BROTLI_STATE_READ_BLOCK_LENGTH_NONE; + + s->buffer_length = 0; + s->loop_counter = 0; + s->pos = 0; + s->rb_roundtrips = 0; + s->partial_pos_out = 0; + s->used_input = 0; + + s->block_type_trees = NULL; + s->block_len_trees = NULL; + s->ringbuffer = NULL; + s->ringbuffer_size = 0; + s->new_ringbuffer_size = 0; + s->ringbuffer_mask = 0; + + s->context_map = NULL; + s->context_modes = NULL; + s->dist_context_map = NULL; + s->context_map_slice = NULL; + s->dist_context_map_slice = NULL; + + s->literal_hgroup.codes = NULL; + s->literal_hgroup.htrees = NULL; + s->insert_copy_hgroup.codes = NULL; + s->insert_copy_hgroup.htrees = NULL; + s->distance_hgroup.codes = NULL; + s->distance_hgroup.htrees = NULL; + + s->is_last_metablock = 0; + s->is_uncompressed = 0; + s->is_metadata = 0; + s->should_wrap_ringbuffer = 0; + s->canny_ringbuffer_allocation = 1; + + s->window_bits = 0; + s->max_distance = 0; + s->dist_rb[0] = 16; + s->dist_rb[1] = 15; + s->dist_rb[2] = 11; + s->dist_rb[3] = 4; + s->dist_rb_idx = 0; + s->block_type_trees = NULL; + s->block_len_trees = NULL; + + s->mtf_upper_bound = 63; + + s->compound_dictionary = NULL; + s->dictionary = + BrotliSharedDictionaryCreateInstance(alloc_func, free_func, opaque); + if (!s->dictionary) return BROTLI_FALSE; + + s->metadata_start_func = NULL; + s->metadata_chunk_func = NULL; + s->metadata_callback_opaque = 0; + + return BROTLI_TRUE; +} + +void BrotliDecoderStateMetablockBegin(BrotliDecoderState* s) { + s->meta_block_remaining_len = 0; + s->block_length[0] = BROTLI_BLOCK_SIZE_CAP; + s->block_length[1] = BROTLI_BLOCK_SIZE_CAP; + s->block_length[2] = BROTLI_BLOCK_SIZE_CAP; + s->num_block_types[0] = 1; + s->num_block_types[1] = 1; + s->num_block_types[2] = 1; + s->block_type_rb[0] = 1; + s->block_type_rb[1] = 0; + s->block_type_rb[2] = 1; + s->block_type_rb[3] = 0; + s->block_type_rb[4] = 1; + s->block_type_rb[5] = 0; + s->context_map = NULL; + s->context_modes = NULL; + s->dist_context_map = NULL; + s->context_map_slice = NULL; + s->literal_htree = NULL; + s->dist_context_map_slice = NULL; + s->dist_htree_index = 0; + s->context_lookup = NULL; + s->literal_hgroup.codes = NULL; + s->literal_hgroup.htrees = NULL; + s->insert_copy_hgroup.codes = NULL; + s->insert_copy_hgroup.htrees = NULL; + s->distance_hgroup.codes = NULL; + s->distance_hgroup.htrees = NULL; +} + +void BrotliDecoderStateCleanupAfterMetablock(BrotliDecoderState* s) { + BROTLI_DECODER_FREE(s, s->context_modes); + BROTLI_DECODER_FREE(s, s->context_map); + BROTLI_DECODER_FREE(s, s->dist_context_map); + BROTLI_DECODER_FREE(s, s->literal_hgroup.htrees); + BROTLI_DECODER_FREE(s, s->insert_copy_hgroup.htrees); + BROTLI_DECODER_FREE(s, s->distance_hgroup.htrees); +} + +void BrotliDecoderStateCleanup(BrotliDecoderState* s) { + BrotliDecoderStateCleanupAfterMetablock(s); + + BROTLI_DECODER_ON_FINISH(s); + + BROTLI_DECODER_FREE(s, s->compound_dictionary); + BrotliSharedDictionaryDestroyInstance(s->dictionary); + s->dictionary = NULL; + BROTLI_DECODER_FREE(s, s->ringbuffer); + BROTLI_DECODER_FREE(s, s->block_type_trees); +} + +BROTLI_BOOL BrotliDecoderHuffmanTreeGroupInit(BrotliDecoderState* s, + HuffmanTreeGroup* group, brotli_reg_t alphabet_size_max, + brotli_reg_t alphabet_size_limit, brotli_reg_t ntrees) { + /* 376 = 256 (1-st level table) + 4 + 7 + 15 + 31 + 63 (2-nd level mix-tables) + This number is discovered "unlimited" "enough" calculator; it is actually + a wee bigger than required in several cases (especially for alphabets with + less than 16 symbols). */ + const size_t max_table_size = alphabet_size_limit + 376; + const size_t code_size = sizeof(HuffmanCode) * ntrees * max_table_size; + const size_t htree_size = sizeof(HuffmanCode*) * ntrees; + /* Pointer alignment is, hopefully, wider than sizeof(HuffmanCode). */ + HuffmanCode** p = (HuffmanCode**)BROTLI_DECODER_ALLOC(s, + code_size + htree_size); + group->alphabet_size_max = (uint16_t)alphabet_size_max; + group->alphabet_size_limit = (uint16_t)alphabet_size_limit; + group->num_htrees = (uint16_t)ntrees; + group->htrees = p; + group->codes = p ? (HuffmanCode*)(&p[ntrees]) : NULL; + return !!p; +} + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif diff --git a/packages/runtime/vendor/brotli/c/dec/state.h b/packages/runtime/vendor/brotli/c/dec/state.h new file mode 100644 index 000000000..aa371fba2 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/dec/state.h @@ -0,0 +1,396 @@ +/* Copyright 2015 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* Brotli state for partial streaming decoding. */ + +#ifndef BROTLI_DEC_STATE_H_ +#define BROTLI_DEC_STATE_H_ + +#include "../common/constants.h" +#include "../common/platform.h" +#include +#include +#include "bit_reader.h" +#include "huffman.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +/* Graphviz diagram that describes state transitions: + +digraph States { + graph [compound=true] + concentrate=true + node [shape="box"] + + UNINITED -> {LARGE_WINDOW_BITS -> INITIALIZE} + subgraph cluster_metablock_workflow { + style="rounded" + label=< METABLOCK CYCLE > + METABLOCK_BEGIN -> METABLOCK_HEADER + METABLOCK_HEADER:sw -> METADATA + METABLOCK_HEADER:s -> UNCOMPRESSED + METABLOCK_HEADER:se -> METABLOCK_DONE:ne + METADATA:s -> METABLOCK_DONE:w + UNCOMPRESSED:s -> METABLOCK_DONE:n + METABLOCK_DONE:e -> METABLOCK_BEGIN:e [constraint="false"] + } + INITIALIZE -> METABLOCK_BEGIN + METABLOCK_DONE -> DONE + + subgraph cluster_compressed_metablock { + style="rounded" + label=< COMPRESSED METABLOCK > + + subgraph cluster_command { + style="rounded" + label=< HOT LOOP > + + _METABLOCK_DONE_PORT_ [shape=point style=invis] + + { + // Set different shape for nodes returning from "compressed metablock". + node [shape=invhouse]; CMD_INNER CMD_POST_DECODE_LITERALS; + CMD_POST_WRAP_COPY; CMD_INNER_WRITE; CMD_POST_WRITE_1; + } + + CMD_BEGIN -> CMD_INNER -> CMD_POST_DECODE_LITERALS -> CMD_POST_WRAP_COPY + + // IO ("write") nodes are not in the hot loop! + CMD_INNER_WRITE [style=dashed] + CMD_INNER -> CMD_INNER_WRITE + CMD_POST_WRITE_1 [style=dashed] + CMD_POST_DECODE_LITERALS -> CMD_POST_WRITE_1 + CMD_POST_WRITE_2 [style=dashed] + CMD_POST_WRAP_COPY -> CMD_POST_WRITE_2 + + CMD_POST_WRITE_1 -> CMD_BEGIN:s [constraint="false"] + CMD_INNER_WRITE -> {CMD_INNER CMD_POST_DECODE_LITERALS} + [constraint="false"] + CMD_BEGIN:ne -> CMD_POST_DECODE_LITERALS [constraint="false"] + CMD_POST_WRAP_COPY -> CMD_BEGIN [constraint="false"] + CMD_POST_DECODE_LITERALS -> CMD_BEGIN:ne [constraint="false"] + CMD_POST_WRITE_2 -> CMD_POST_WRAP_COPY [constraint="false"] + {rank=same; CMD_BEGIN; CMD_INNER; CMD_POST_DECODE_LITERALS; + CMD_POST_WRAP_COPY} + {rank=same; CMD_INNER_WRITE; CMD_POST_WRITE_1; CMD_POST_WRITE_2} + + {CMD_INNER CMD_POST_DECODE_LITERALS CMD_POST_WRAP_COPY} -> + _METABLOCK_DONE_PORT_ [style=invis] + {CMD_INNER_WRITE CMD_POST_WRITE_1} -> _METABLOCK_DONE_PORT_ + [constraint="false" style=invis] + } + + BEFORE_COMPRESSED_METABLOCK_HEADER:s -> HUFFMAN_CODE_0:n + HUFFMAN_CODE_0 -> HUFFMAN_CODE_1 -> HUFFMAN_CODE_2 -> HUFFMAN_CODE_3 + HUFFMAN_CODE_0 -> METABLOCK_HEADER_2 -> CONTEXT_MODES -> CONTEXT_MAP_1 + CONTEXT_MAP_1 -> CONTEXT_MAP_2 -> TREE_GROUP + TREE_GROUP -> BEFORE_COMPRESSED_METABLOCK_BODY:e + BEFORE_COMPRESSED_METABLOCK_BODY:s -> CMD_BEGIN:n + + HUFFMAN_CODE_3:e -> HUFFMAN_CODE_0:ne [constraint="false"] + {rank=same; HUFFMAN_CODE_0; HUFFMAN_CODE_1; HUFFMAN_CODE_2; HUFFMAN_CODE_3} + {rank=same; METABLOCK_HEADER_2; CONTEXT_MODES; CONTEXT_MAP_1; CONTEXT_MAP_2; + TREE_GROUP} + } + METABLOCK_HEADER:e -> BEFORE_COMPRESSED_METABLOCK_HEADER:n + + _METABLOCK_DONE_PORT_ -> METABLOCK_DONE:se + [constraint="false" ltail=cluster_command] + + UNINITED [shape=Mdiamond]; + DONE [shape=Msquare]; +} + + + */ + +typedef enum { + BROTLI_STATE_UNINITED, + BROTLI_STATE_LARGE_WINDOW_BITS, + BROTLI_STATE_INITIALIZE, + BROTLI_STATE_METABLOCK_BEGIN, + BROTLI_STATE_METABLOCK_HEADER, + BROTLI_STATE_METABLOCK_HEADER_2, + BROTLI_STATE_CONTEXT_MODES, + BROTLI_STATE_COMMAND_BEGIN, + BROTLI_STATE_COMMAND_INNER, + BROTLI_STATE_COMMAND_POST_DECODE_LITERALS, + BROTLI_STATE_COMMAND_POST_WRAP_COPY, + BROTLI_STATE_UNCOMPRESSED, + BROTLI_STATE_METADATA, + BROTLI_STATE_COMMAND_INNER_WRITE, + BROTLI_STATE_METABLOCK_DONE, + BROTLI_STATE_COMMAND_POST_WRITE_1, + BROTLI_STATE_COMMAND_POST_WRITE_2, + BROTLI_STATE_BEFORE_COMPRESSED_METABLOCK_HEADER, + BROTLI_STATE_HUFFMAN_CODE_0, + BROTLI_STATE_HUFFMAN_CODE_1, + BROTLI_STATE_HUFFMAN_CODE_2, + BROTLI_STATE_HUFFMAN_CODE_3, + BROTLI_STATE_CONTEXT_MAP_1, + BROTLI_STATE_CONTEXT_MAP_2, + BROTLI_STATE_TREE_GROUP, + BROTLI_STATE_BEFORE_COMPRESSED_METABLOCK_BODY, + BROTLI_STATE_DONE +} BrotliRunningState; + +typedef enum { + BROTLI_STATE_METABLOCK_HEADER_NONE, + BROTLI_STATE_METABLOCK_HEADER_EMPTY, + BROTLI_STATE_METABLOCK_HEADER_NIBBLES, + BROTLI_STATE_METABLOCK_HEADER_SIZE, + BROTLI_STATE_METABLOCK_HEADER_UNCOMPRESSED, + BROTLI_STATE_METABLOCK_HEADER_RESERVED, + BROTLI_STATE_METABLOCK_HEADER_BYTES, + BROTLI_STATE_METABLOCK_HEADER_METADATA +} BrotliRunningMetablockHeaderState; + +typedef enum { + BROTLI_STATE_UNCOMPRESSED_NONE, + BROTLI_STATE_UNCOMPRESSED_WRITE +} BrotliRunningUncompressedState; + +typedef enum { + BROTLI_STATE_TREE_GROUP_NONE, + BROTLI_STATE_TREE_GROUP_LOOP +} BrotliRunningTreeGroupState; + +typedef enum { + BROTLI_STATE_CONTEXT_MAP_NONE, + BROTLI_STATE_CONTEXT_MAP_READ_PREFIX, + BROTLI_STATE_CONTEXT_MAP_HUFFMAN, + BROTLI_STATE_CONTEXT_MAP_DECODE, + BROTLI_STATE_CONTEXT_MAP_TRANSFORM +} BrotliRunningContextMapState; + +typedef enum { + BROTLI_STATE_HUFFMAN_NONE, + BROTLI_STATE_HUFFMAN_SIMPLE_SIZE, + BROTLI_STATE_HUFFMAN_SIMPLE_READ, + BROTLI_STATE_HUFFMAN_SIMPLE_BUILD, + BROTLI_STATE_HUFFMAN_COMPLEX, + BROTLI_STATE_HUFFMAN_LENGTH_SYMBOLS +} BrotliRunningHuffmanState; + +typedef enum { + BROTLI_STATE_DECODE_UINT8_NONE, + BROTLI_STATE_DECODE_UINT8_SHORT, + BROTLI_STATE_DECODE_UINT8_LONG +} BrotliRunningDecodeUint8State; + +typedef enum { + BROTLI_STATE_READ_BLOCK_LENGTH_NONE, + BROTLI_STATE_READ_BLOCK_LENGTH_SUFFIX +} BrotliRunningReadBlockLengthState; + +/* BrotliDecoderState addon, used for Compound Dictionary functionality. */ +typedef struct BrotliDecoderCompoundDictionary { + int num_chunks; + int total_size; + int br_index; + int br_offset; + int br_length; + int br_copied; + const uint8_t* chunks[16]; + int chunk_offsets[16]; + int block_bits; + uint8_t block_map[256]; +} BrotliDecoderCompoundDictionary; + +typedef struct BrotliMetablockHeaderArena { + BrotliRunningTreeGroupState substate_tree_group; + BrotliRunningContextMapState substate_context_map; + BrotliRunningHuffmanState substate_huffman; + + brotli_reg_t sub_loop_counter; + + brotli_reg_t repeat_code_len; + brotli_reg_t prev_code_len; + + /* For ReadHuffmanCode. */ + brotli_reg_t symbol; + brotli_reg_t repeat; + brotli_reg_t space; + + /* Huffman table for "histograms". */ + HuffmanCode table[32]; + /* List of heads of symbol chains. */ + uint16_t* symbol_lists; + /* Storage from symbol_lists. */ + uint16_t symbols_lists_array[BROTLI_HUFFMAN_MAX_CODE_LENGTH + 1 + + BROTLI_NUM_COMMAND_SYMBOLS]; + /* Tails of symbol chains. */ + int next_symbol[32]; + uint8_t code_length_code_lengths[BROTLI_CODE_LENGTH_CODES]; + /* Population counts for the code lengths. */ + uint16_t code_length_histo[16]; + /* TODO(eustas): +2 bytes padding */ + + /* For HuffmanTreeGroupDecode. */ + int htree_index; + HuffmanCode* next; + + /* For DecodeContextMap. */ + brotli_reg_t context_index; + brotli_reg_t max_run_length_prefix; + brotli_reg_t code; + HuffmanCode context_map_table[BROTLI_HUFFMAN_MAX_SIZE_272]; +} BrotliMetablockHeaderArena; + +typedef struct BrotliMetablockBodyArena { + uint8_t dist_extra_bits[544]; + brotli_reg_t dist_offset[544]; +} BrotliMetablockBodyArena; + +struct BrotliDecoderStateStruct { + BrotliRunningState state; + + /* This counter is reused for several disjoint loops. */ + int loop_counter; + + BrotliBitReader br; + + brotli_alloc_func alloc_func; + brotli_free_func free_func; + void* memory_manager_opaque; + + /* Temporary storage for remaining input. Brotli stream format is designed in + a way, that 64 bits are enough to make progress in decoding. */ + union { + uint64_t u64; + uint8_t u8[8]; + } buffer; + brotli_reg_t buffer_length; + + int pos; + int max_backward_distance; + int max_distance; + int ringbuffer_size; + int ringbuffer_mask; + int dist_rb_idx; + int dist_rb[4]; + int error_code; + int meta_block_remaining_len; + + uint8_t* ringbuffer; + uint8_t* ringbuffer_end; + HuffmanCode* htree_command; + const uint8_t* context_lookup; + uint8_t* context_map_slice; + uint8_t* dist_context_map_slice; + + /* This ring buffer holds a few past copy distances that will be used by + some special distance codes. */ + HuffmanTreeGroup literal_hgroup; + HuffmanTreeGroup insert_copy_hgroup; + HuffmanTreeGroup distance_hgroup; + HuffmanCode* block_type_trees; + HuffmanCode* block_len_trees; + /* This is true if the literal context map histogram type always matches the + block type. It is then not needed to keep the context (faster decoding). */ + int trivial_literal_context; + /* Distance context is actual after command is decoded and before distance is + computed. After distance computation it is used as a temporary variable. */ + int distance_context; + brotli_reg_t block_length[3]; + brotli_reg_t block_length_index; + brotli_reg_t num_block_types[3]; + brotli_reg_t block_type_rb[6]; + brotli_reg_t distance_postfix_bits; + brotli_reg_t num_direct_distance_codes; + brotli_reg_t num_dist_htrees; + uint8_t* dist_context_map; + HuffmanCode* literal_htree; + + /* For partial write operations. */ + size_t rb_roundtrips; /* how many times we went around the ring-buffer */ + size_t partial_pos_out; /* how much output to the user in total */ + + /* For InverseMoveToFrontTransform. */ + brotli_reg_t mtf_upper_bound; + uint32_t mtf[64 + 1]; + + int copy_length; + int distance_code; + + uint8_t dist_htree_index; + /* TODO(eustas): +3 bytes padding */ + + /* Less used attributes are at the end of this struct. */ + + brotli_decoder_metadata_start_func metadata_start_func; + brotli_decoder_metadata_chunk_func metadata_chunk_func; + void* metadata_callback_opaque; + + /* For reporting. */ + uint64_t used_input; /* how many bytes of input are consumed */ + + /* States inside function calls. */ + BrotliRunningMetablockHeaderState substate_metablock_header; + BrotliRunningUncompressedState substate_uncompressed; + BrotliRunningDecodeUint8State substate_decode_uint8; + BrotliRunningReadBlockLengthState substate_read_block_length; + + int new_ringbuffer_size; + /* TODO(eustas): +4 bytes padding */ + + unsigned int is_last_metablock : 1; + unsigned int is_uncompressed : 1; + unsigned int is_metadata : 1; + unsigned int should_wrap_ringbuffer : 1; + unsigned int canny_ringbuffer_allocation : 1; + unsigned int large_window : 1; + unsigned int window_bits : 6; + unsigned int size_nibbles : 8; + /* TODO(eustas): +12 bits padding */ + + brotli_reg_t num_literal_htrees; + uint8_t* context_map; + uint8_t* context_modes; + + BrotliSharedDictionary* dictionary; + BrotliDecoderCompoundDictionary* compound_dictionary; + + uint32_t trivial_literal_contexts[8]; /* 256 bits */ + + union { + BrotliMetablockHeaderArena header; + BrotliMetablockBodyArena body; + } arena; +}; + +typedef struct BrotliDecoderStateStruct BrotliDecoderStateInternal; +#define BrotliDecoderState BrotliDecoderStateInternal + +BROTLI_INTERNAL BROTLI_BOOL BrotliDecoderStateInit(BrotliDecoderState* s, + brotli_alloc_func alloc_func, brotli_free_func free_func, void* opaque); +BROTLI_INTERNAL void BrotliDecoderStateCleanup(BrotliDecoderState* s); +BROTLI_INTERNAL void BrotliDecoderStateMetablockBegin(BrotliDecoderState* s); +BROTLI_INTERNAL void BrotliDecoderStateCleanupAfterMetablock( + BrotliDecoderState* s); +BROTLI_INTERNAL BROTLI_BOOL BrotliDecoderHuffmanTreeGroupInit( + BrotliDecoderState* s, HuffmanTreeGroup* group, + brotli_reg_t alphabet_size_max, brotli_reg_t alphabet_size_limit, + brotli_reg_t ntrees); + +#define BROTLI_DECODER_ALLOC(S, L) S->alloc_func(S->memory_manager_opaque, L) + +#define BROTLI_DECODER_FREE(S, X) { \ + S->free_func(S->memory_manager_opaque, X); \ + X = NULL; \ +} + +/* Literal/Command/Distance block size maximum; same as maximum metablock size; + used as block size when there is no block switching. */ +#define BROTLI_BLOCK_SIZE_CAP (1U << 24) + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif + +#endif /* BROTLI_DEC_STATE_H_ */ diff --git a/packages/runtime/vendor/brotli/c/dec/static_init.c b/packages/runtime/vendor/brotli/c/dec/static_init.c new file mode 100644 index 000000000..1345052c5 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/dec/static_init.c @@ -0,0 +1,53 @@ +/* Copyright 2025 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +#include "static_init.h" + +#include "../common/platform.h" +#include "../common/static_init.h" + +#if (BROTLI_STATIC_INIT != BROTLI_STATIC_INIT_NONE) +#include "../common/dictionary.h" +#include "prefix.h" +#endif + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +#if (BROTLI_STATIC_INIT != BROTLI_STATIC_INIT_NONE) +static BROTLI_BOOL DoBrotliDecoderStaticInit(void) { + BROTLI_BOOL ok = BrotliDecoderInitCmdLut(kCmdLut); + if (!ok) return BROTLI_FALSE; + return BROTLI_TRUE; +} +#endif /* BROTLI_STATIC_INIT_NONE */ + +#if (BROTLI_STATIC_INIT == BROTLI_STATIC_INIT_EARLY) +static BROTLI_BOOL kEarlyInitOk; +static __attribute__((constructor)) void BrotliDecoderStaticInitEarly(void) { + kEarlyInitOk = DoBrotliDecoderStaticInit(); +} +#elif (BROTLI_STATIC_INIT == BROTLI_STATIC_INIT_LAZY) +static BROTLI_BOOL kLazyInitOk; +void BrotliDecoderLazyStaticInitInner(void) { + kLazyInitOk = DoBrotliDecoderStaticInit(); +} +#endif /* BROTLI_STATIC_INIT_EARLY */ + +BROTLI_BOOL BrotliDecoderEnsureStaticInit(void) { +#if (BROTLI_STATIC_INIT == BROTLI_STATIC_INIT_NONE) + return BROTLI_TRUE; +#elif (BROTLI_STATIC_INIT == BROTLI_STATIC_INIT_EARLY) + return kEarlyInitOk; +#else + return kLazyInitOk; +#endif +} + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif diff --git a/packages/runtime/vendor/brotli/c/dec/static_init.h b/packages/runtime/vendor/brotli/c/dec/static_init.h new file mode 100644 index 000000000..4ae063ef5 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/dec/static_init.h @@ -0,0 +1,30 @@ +/* Copyright 2025 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* Central point for static initialization. */ + +#ifndef THIRD_PARTY_BROTLI_DEC_STATIC_INIT_H_ +#define THIRD_PARTY_BROTLI_DEC_STATIC_INIT_H_ + +#include "../common/platform.h" +#include "../common/static_init.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +#if (BROTLI_STATIC_INIT == BROTLI_STATIC_INIT_LAZY) +BROTLI_INTERNAL void BrotliDecoderLazyStaticInitInner(void); +BROTLI_INTERNAL void BrotliDecoderLazyStaticInit(void); +#endif /* BROTLI_STATIC_INIT */ + +BROTLI_INTERNAL BROTLI_BOOL BrotliDecoderEnsureStaticInit(void); + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif + +#endif // THIRD_PARTY_BROTLI_DEC_STATIC_INIT_H_ diff --git a/packages/runtime/vendor/brotli/c/enc/backward_references.c b/packages/runtime/vendor/brotli/c/enc/backward_references.c new file mode 100644 index 000000000..5d1c0ed63 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/backward_references.c @@ -0,0 +1,231 @@ +/* Copyright 2013 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* Function to find backward reference copies. */ + +#include "backward_references.h" + +#include "../common/constants.h" +#include "../common/context.h" +#include "../common/platform.h" +#include "command.h" +#include "compound_dictionary.h" +#include "encoder_dict.h" +#include "hash.h" +#include "params.h" +#include "quality.h" /* IWYU pragma: keep for inc */ + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +static BROTLI_INLINE size_t ComputeDistanceCode(size_t distance, + size_t max_distance, + const int* dist_cache) { + if (distance <= max_distance) { + size_t distance_plus_3 = distance + 3; + size_t offset0 = distance_plus_3 - (size_t)dist_cache[0]; + size_t offset1 = distance_plus_3 - (size_t)dist_cache[1]; + if (distance == (size_t)dist_cache[0]) { + return 0; + } else if (distance == (size_t)dist_cache[1]) { + return 1; + } else if (offset0 < 7) { + return (0x9750468 >> (4 * offset0)) & 0xF; + } else if (offset1 < 7) { + return (0xFDB1ACE >> (4 * offset1)) & 0xF; + } else if (distance == (size_t)dist_cache[2]) { + return 2; + } else if (distance == (size_t)dist_cache[3]) { + return 3; + } + } + return distance + BROTLI_NUM_DISTANCE_SHORT_CODES - 1; +} + +#define EXPAND_CAT(a, b) CAT(a, b) +#define CAT(a, b) a ## b +#define FN(X) EXPAND_CAT(X, HASHER()) +#define EXPORT_FN(X) EXPAND_CAT(X, EXPAND_CAT(PREFIX(), HASHER())) + +#define PREFIX() N +#define ENABLE_COMPOUND_DICTIONARY 0 + +#define HASHER() H2 +/* NOLINTNEXTLINE(build/include) */ +#include "backward_references_inc.h" +#undef HASHER + +#define HASHER() H3 +/* NOLINTNEXTLINE(build/include) */ +#include "backward_references_inc.h" +#undef HASHER + +#define HASHER() H4 +/* NOLINTNEXTLINE(build/include) */ +#include "backward_references_inc.h" +#undef HASHER + +#define HASHER() H5 +/* NOLINTNEXTLINE(build/include) */ +#include "backward_references_inc.h" +#undef HASHER + +#define HASHER() H6 +/* NOLINTNEXTLINE(build/include) */ +#include "backward_references_inc.h" +#undef HASHER + +#define HASHER() H40 +/* NOLINTNEXTLINE(build/include) */ +#include "backward_references_inc.h" +#undef HASHER + +#define HASHER() H41 +/* NOLINTNEXTLINE(build/include) */ +#include "backward_references_inc.h" +#undef HASHER + +#define HASHER() H42 +/* NOLINTNEXTLINE(build/include) */ +#include "backward_references_inc.h" +#undef HASHER + +#define HASHER() H54 +/* NOLINTNEXTLINE(build/include) */ +#include "backward_references_inc.h" +#undef HASHER + +#define HASHER() H35 +/* NOLINTNEXTLINE(build/include) */ +#include "backward_references_inc.h" +#undef HASHER + +#define HASHER() H55 +/* NOLINTNEXTLINE(build/include) */ +#include "backward_references_inc.h" +#undef HASHER + +#define HASHER() H65 +/* NOLINTNEXTLINE(build/include) */ +#include "backward_references_inc.h" +#undef HASHER + +#if defined(BROTLI_MAX_SIMD_QUALITY) +#define HASHER() H58 +/* NOLINTNEXTLINE(build/include) */ +#include "backward_references_inc.h" +#undef HASHER + +#define HASHER() H68 +/* NOLINTNEXTLINE(build/include) */ +#include "backward_references_inc.h" +#undef HASHER +#endif + +#undef ENABLE_COMPOUND_DICTIONARY +#undef PREFIX +#define PREFIX() D +#define ENABLE_COMPOUND_DICTIONARY 1 + +#define HASHER() H5 +/* NOLINTNEXTLINE(build/include) */ +#include "backward_references_inc.h" +#undef HASHER +#define HASHER() H6 +/* NOLINTNEXTLINE(build/include) */ +#include "backward_references_inc.h" +#undef HASHER +#define HASHER() H40 +/* NOLINTNEXTLINE(build/include) */ +#include "backward_references_inc.h" +#undef HASHER +#define HASHER() H41 +/* NOLINTNEXTLINE(build/include) */ +#include "backward_references_inc.h" +#undef HASHER +#define HASHER() H42 +/* NOLINTNEXTLINE(build/include) */ +#include "backward_references_inc.h" +#undef HASHER +#define HASHER() H55 +/* NOLINTNEXTLINE(build/include) */ +#include "backward_references_inc.h" +#undef HASHER +#define HASHER() H65 +/* NOLINTNEXTLINE(build/include) */ +#include "backward_references_inc.h" +#undef HASHER +#if defined(BROTLI_MAX_SIMD_QUALITY) +#define HASHER() H58 +/* NOLINTNEXTLINE(build/include) */ +#include "backward_references_inc.h" +#undef HASHER +#define HASHER() H68 +/* NOLINTNEXTLINE(build/include) */ +#include "backward_references_inc.h" +#undef HASHER +#endif + +#undef ENABLE_COMPOUND_DICTIONARY +#undef PREFIX + +#undef EXPORT_FN +#undef FN +#undef CAT +#undef EXPAND_CAT + +void BrotliCreateBackwardReferences(size_t num_bytes, + size_t position, const uint8_t* ringbuffer, size_t ringbuffer_mask, + ContextLut literal_context_lut, const BrotliEncoderParams* params, + Hasher* hasher, int* dist_cache, size_t* last_insert_len, + Command* commands, size_t* num_commands, size_t* num_literals) { + if (params->dictionary.compound.num_chunks != 0) { + switch (params->hasher.type) { +#define CASE_(N) \ + case N: \ + CreateBackwardReferencesDH ## N(num_bytes, \ + position, ringbuffer, ringbuffer_mask, \ + literal_context_lut, params, hasher, dist_cache, \ + last_insert_len, commands, num_commands, num_literals); \ + return; + CASE_(5) + CASE_(6) +#if defined(BROTLI_MAX_SIMD_QUALITY) + CASE_(58) + CASE_(68) +#endif + CASE_(40) + CASE_(41) + CASE_(42) + CASE_(55) + CASE_(65) +#undef CASE_ + default: + BROTLI_DCHECK(BROTLI_FALSE); + break; + } + } + + switch (params->hasher.type) { +#define CASE_(N) \ + case N: \ + CreateBackwardReferencesNH ## N(num_bytes, \ + position, ringbuffer, ringbuffer_mask, \ + literal_context_lut, params, hasher, dist_cache, \ + last_insert_len, commands, num_commands, num_literals); \ + return; + FOR_GENERIC_HASHERS(CASE_) +#undef CASE_ + default: + BROTLI_DCHECK(BROTLI_FALSE); + break; + } +} + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif diff --git a/packages/runtime/vendor/brotli/c/enc/backward_references.h b/packages/runtime/vendor/brotli/c/enc/backward_references.h new file mode 100644 index 000000000..17f0c8495 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/backward_references.h @@ -0,0 +1,36 @@ +/* Copyright 2013 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* Function to find backward reference copies. */ + +#ifndef BROTLI_ENC_BACKWARD_REFERENCES_H_ +#define BROTLI_ENC_BACKWARD_REFERENCES_H_ + +#include "../common/context.h" +#include "../common/platform.h" +#include "command.h" +#include "hash.h" +#include "params.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +/* "commands" points to the next output command to write to, "*num_commands" is + initially the total amount of commands output by previous + CreateBackwardReferences calls, and must be incremented by the amount written + by this call. */ +BROTLI_INTERNAL void BrotliCreateBackwardReferences(size_t num_bytes, + size_t position, const uint8_t* ringbuffer, size_t ringbuffer_mask, + ContextLut literal_context_lut, const BrotliEncoderParams* params, + Hasher* hasher, int* dist_cache, size_t* last_insert_len, + Command* commands, size_t* num_commands, size_t* num_literals); + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif + +#endif /* BROTLI_ENC_BACKWARD_REFERENCES_H_ */ diff --git a/packages/runtime/vendor/brotli/c/enc/backward_references_hq.c b/packages/runtime/vendor/brotli/c/enc/backward_references_hq.c new file mode 100644 index 000000000..ab8b12187 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/backward_references_hq.c @@ -0,0 +1,939 @@ +/* Copyright 2013 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* Function to find backward reference copies. */ + +#include "backward_references_hq.h" + +#include "../common/constants.h" +#include "../common/context.h" +#include "../common/platform.h" +#include "command.h" +#include "compound_dictionary.h" +#include "encoder_dict.h" +#include "fast_log.h" +#include "find_match_length.h" +#include "hash.h" +#include "literal_cost.h" +#include "memory.h" +#include "params.h" +#include "prefix.h" +#include "quality.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +/* BrotliCalculateDistanceCodeLimit(BROTLI_MAX_ALLOWED_DISTANCE, 3, 120). */ +#define BROTLI_MAX_EFFECTIVE_DISTANCE_ALPHABET_SIZE 544 + +static const float kInfinity = 1.7e38f; /* ~= 2 ^ 127 */ + +static const BROTLI_MODEL("small") uint32_t kDistanceCacheIndex[] = { + 0, 1, 2, 3, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, +}; +static const BROTLI_MODEL("small") int kDistanceCacheOffset[] = { + 0, 0, 0, 0, -1, 1, -2, 2, -3, 3, -1, 1, -2, 2, -3, 3 +}; + +void BrotliInitZopfliNodes(ZopfliNode* array, size_t length) { + ZopfliNode stub; + size_t i; + stub.length = 1; + stub.distance = 0; + stub.dcode_insert_length = 0; + stub.u.cost = kInfinity; + for (i = 0; i < length; ++i) array[i] = stub; +} + +static BROTLI_INLINE uint32_t ZopfliNodeCopyLength(const ZopfliNode* self) { + return self->length & 0x1FFFFFF; +} + +static BROTLI_INLINE uint32_t ZopfliNodeLengthCode(const ZopfliNode* self) { + const uint32_t modifier = self->length >> 25; + return ZopfliNodeCopyLength(self) + 9u - modifier; +} + +static BROTLI_INLINE uint32_t ZopfliNodeCopyDistance(const ZopfliNode* self) { + return self->distance; +} + +static BROTLI_INLINE uint32_t ZopfliNodeDistanceCode(const ZopfliNode* self) { + const uint32_t short_code = self->dcode_insert_length >> 27; + return short_code == 0 ? + ZopfliNodeCopyDistance(self) + BROTLI_NUM_DISTANCE_SHORT_CODES - 1 : + short_code - 1; +} + +static BROTLI_INLINE uint32_t ZopfliNodeCommandLength(const ZopfliNode* self) { + return ZopfliNodeCopyLength(self) + (self->dcode_insert_length & 0x7FFFFFF); +} + +/* Temporary data for ZopfliCostModelSetFromCommands. */ +typedef struct ZopfliCostModelArena { + uint32_t histogram_literal[BROTLI_NUM_LITERAL_SYMBOLS]; + uint32_t histogram_cmd[BROTLI_NUM_COMMAND_SYMBOLS]; + uint32_t histogram_dist[BROTLI_MAX_EFFECTIVE_DISTANCE_ALPHABET_SIZE]; + float cost_literal[BROTLI_NUM_LITERAL_SYMBOLS]; +} ZopfliCostModelArena; + +/* Histogram based cost model for zopflification. */ +typedef struct ZopfliCostModel { + /* The insert and copy length symbols. */ + float cost_cmd_[BROTLI_NUM_COMMAND_SYMBOLS]; + float* cost_dist_; + uint32_t distance_histogram_size; + /* Cumulative costs of literals per position in the stream. */ + float* literal_costs_; + float min_cost_cmd_; + size_t num_bytes_; + + /* Temporary data. */ + union { + size_t literal_histograms[3 * 256]; + ZopfliCostModelArena arena; + }; +} ZopfliCostModel; + +static void InitZopfliCostModel( + MemoryManager* m, ZopfliCostModel* self, const BrotliDistanceParams* dist, + size_t num_bytes) { + self->num_bytes_ = num_bytes; + self->literal_costs_ = BROTLI_ALLOC(m, float, num_bytes + 2); + self->cost_dist_ = BROTLI_ALLOC(m, float, dist->alphabet_size_limit); + self->distance_histogram_size = dist->alphabet_size_limit; + if (BROTLI_IS_OOM(m)) return; +} + +static void CleanupZopfliCostModel(MemoryManager* m, ZopfliCostModel* self) { + BROTLI_FREE(m, self->literal_costs_); + BROTLI_FREE(m, self->cost_dist_); +} + +static void SetCost(const uint32_t* histogram, size_t histogram_size, + BROTLI_BOOL literal_histogram, float* cost) { + size_t sum = 0; + size_t missing_symbol_sum; + float log2sum; + float missing_symbol_cost; + size_t i; + for (i = 0; i < histogram_size; i++) { + sum += histogram[i]; + } + log2sum = (float)FastLog2(sum); + missing_symbol_sum = sum; + if (!literal_histogram) { + for (i = 0; i < histogram_size; i++) { + if (histogram[i] == 0) missing_symbol_sum++; + } + } + missing_symbol_cost = (float)FastLog2(missing_symbol_sum) + 2; + for (i = 0; i < histogram_size; i++) { + if (histogram[i] == 0) { + cost[i] = missing_symbol_cost; + continue; + } + + /* Shannon bits for this symbol. */ + cost[i] = log2sum - (float)FastLog2(histogram[i]); + + /* Cannot be coded with less than 1 bit */ + if (cost[i] < 1) cost[i] = 1; + } +} + +static void ZopfliCostModelSetFromCommands(ZopfliCostModel* self, + size_t position, + const uint8_t* ringbuffer, + size_t ringbuffer_mask, + const Command* commands, + size_t num_commands, + size_t last_insert_len) { + ZopfliCostModelArena* arena = &self->arena; + size_t pos = position - last_insert_len; + float min_cost_cmd = kInfinity; + size_t i; + float* cost_cmd = self->cost_cmd_; + + memset(arena->histogram_literal, 0, sizeof(arena->histogram_literal)); + memset(arena->histogram_cmd, 0, sizeof(arena->histogram_cmd)); + memset(arena->histogram_dist, 0, sizeof(arena->histogram_dist)); + + for (i = 0; i < num_commands; i++) { + size_t inslength = commands[i].insert_len_; + size_t copylength = CommandCopyLen(&commands[i]); + size_t distcode = commands[i].dist_prefix_ & 0x3FF; + size_t cmdcode = commands[i].cmd_prefix_; + size_t j; + + arena->histogram_cmd[cmdcode]++; + if (cmdcode >= 128) arena->histogram_dist[distcode]++; + + for (j = 0; j < inslength; j++) { + arena->histogram_literal[ringbuffer[(pos + j) & ringbuffer_mask]]++; + } + + pos += inslength + copylength; + } + + SetCost(arena->histogram_literal, BROTLI_NUM_LITERAL_SYMBOLS, BROTLI_TRUE, + arena->cost_literal); + SetCost(arena->histogram_cmd, BROTLI_NUM_COMMAND_SYMBOLS, BROTLI_FALSE, + cost_cmd); + SetCost(arena->histogram_dist, self->distance_histogram_size, BROTLI_FALSE, + self->cost_dist_); + + for (i = 0; i < BROTLI_NUM_COMMAND_SYMBOLS; ++i) { + min_cost_cmd = BROTLI_MIN(float, min_cost_cmd, cost_cmd[i]); + } + self->min_cost_cmd_ = min_cost_cmd; + + { + float* literal_costs = self->literal_costs_; + float literal_carry = 0.0; + size_t num_bytes = self->num_bytes_; + literal_costs[0] = 0.0; + for (i = 0; i < num_bytes; ++i) { + literal_carry += + arena->cost_literal[ringbuffer[(position + i) & ringbuffer_mask]]; + literal_costs[i + 1] = literal_costs[i] + literal_carry; + literal_carry -= literal_costs[i + 1] - literal_costs[i]; + } + } +} + +static void ZopfliCostModelSetFromLiteralCosts(ZopfliCostModel* self, + size_t position, + const uint8_t* ringbuffer, + size_t ringbuffer_mask) { + float* literal_costs = self->literal_costs_; + float literal_carry = 0.0; + float* cost_dist = self->cost_dist_; + float* cost_cmd = self->cost_cmd_; + size_t num_bytes = self->num_bytes_; + size_t i; + BrotliEstimateBitCostsForLiterals(position, num_bytes, ringbuffer_mask, + ringbuffer, self->literal_histograms, + &literal_costs[1]); + literal_costs[0] = 0.0; + for (i = 0; i < num_bytes; ++i) { + literal_carry += literal_costs[i + 1]; + literal_costs[i + 1] = literal_costs[i] + literal_carry; + literal_carry -= literal_costs[i + 1] - literal_costs[i]; + } + for (i = 0; i < BROTLI_NUM_COMMAND_SYMBOLS; ++i) { + cost_cmd[i] = (float)FastLog2(11 + (uint32_t)i); + } + for (i = 0; i < self->distance_histogram_size; ++i) { + cost_dist[i] = (float)FastLog2(20 + (uint32_t)i); + } + self->min_cost_cmd_ = (float)FastLog2(11); +} + +static BROTLI_INLINE float ZopfliCostModelGetCommandCost( + const ZopfliCostModel* self, uint16_t cmdcode) { + return self->cost_cmd_[cmdcode]; +} + +static BROTLI_INLINE float ZopfliCostModelGetDistanceCost( + const ZopfliCostModel* self, size_t distcode) { + return self->cost_dist_[distcode]; +} + +static BROTLI_INLINE float ZopfliCostModelGetLiteralCosts( + const ZopfliCostModel* self, size_t from, size_t to) { + return self->literal_costs_[to] - self->literal_costs_[from]; +} + +static BROTLI_INLINE float ZopfliCostModelGetMinCostCmd( + const ZopfliCostModel* self) { + return self->min_cost_cmd_; +} + +/* REQUIRES: len >= 2, start_pos <= pos */ +/* REQUIRES: cost < kInfinity, nodes[start_pos].cost < kInfinity */ +/* Maintains the "ZopfliNode array invariant". */ +static BROTLI_INLINE void UpdateZopfliNode(ZopfliNode* nodes, size_t pos, + size_t start_pos, size_t len, size_t len_code, size_t dist, + size_t short_code, float cost) { + ZopfliNode* next = &nodes[pos + len]; + next->length = (uint32_t)(len | ((len + 9u - len_code) << 25)); + next->distance = (uint32_t)dist; + next->dcode_insert_length = (uint32_t)( + (short_code << 27) | (pos - start_pos)); + next->u.cost = cost; +} + +typedef struct PosData { + size_t pos; + int distance_cache[4]; + float costdiff; + float cost; +} PosData; + +/* Maintains the smallest 8 cost difference together with their positions */ +typedef struct StartPosQueue { + PosData q_[8]; + size_t idx_; +} StartPosQueue; + +static BROTLI_INLINE void InitStartPosQueue(StartPosQueue* self) { + self->idx_ = 0; +} + +static size_t StartPosQueueSize(const StartPosQueue* self) { + return BROTLI_MIN(size_t, self->idx_, 8); +} + +static void StartPosQueuePush(StartPosQueue* self, const PosData* posdata) { + size_t offset = ~(self->idx_++) & 7; + size_t len = StartPosQueueSize(self); + size_t i; + PosData* q = self->q_; + q[offset] = *posdata; + /* Restore the sorted order. In the list of |len| items at most |len - 1| + adjacent element comparisons / swaps are required. */ + for (i = 1; i < len; ++i) { + if (q[offset & 7].costdiff > q[(offset + 1) & 7].costdiff) { + BROTLI_SWAP(PosData, q, offset & 7, (offset + 1) & 7); + } + ++offset; + } +} + +static const PosData* StartPosQueueAt(const StartPosQueue* self, size_t k) { + return &self->q_[(k - self->idx_) & 7]; +} + +/* Returns the minimum possible copy length that can improve the cost of any */ +/* future position. */ +static size_t ComputeMinimumCopyLength(const float start_cost, + const ZopfliNode* nodes, + const size_t num_bytes, + const size_t pos) { + /* Compute the minimum possible cost of reaching any future position. */ + float min_cost = start_cost; + size_t len = 2; + size_t next_len_bucket = 4; + size_t next_len_offset = 10; + while (pos + len <= num_bytes && nodes[pos + len].u.cost <= min_cost) { + /* We already reached (pos + len) with no more cost than the minimum + possible cost of reaching anything from this pos, so there is no point in + looking for lengths <= len. */ + ++len; + if (len == next_len_offset) { + /* We reached the next copy length code bucket, so we add one more + extra bit to the minimum cost. */ + min_cost += 1.0f; + next_len_offset += next_len_bucket; + next_len_bucket *= 2; + } + } + return len; +} + +/* REQUIRES: nodes[pos].cost < kInfinity + REQUIRES: nodes[0..pos] satisfies that "ZopfliNode array invariant". */ +static uint32_t ComputeDistanceShortcut(const size_t block_start, + const size_t pos, + const size_t max_backward_limit, + const size_t gap, + const ZopfliNode* nodes) { + const size_t c_len = ZopfliNodeCopyLength(&nodes[pos]); + const size_t i_len = nodes[pos].dcode_insert_length & 0x7FFFFFF; + const size_t dist = ZopfliNodeCopyDistance(&nodes[pos]); + /* Since |block_start + pos| is the end position of the command, the copy part + starts from |block_start + pos - c_len|. Distances that are greater than + this or greater than |max_backward_limit| + |gap| are static dictionary + references, and do not update the last distances. + Also distance code 0 (last distance) does not update the last distances. */ + if (pos == 0) { + return 0; + } else if (dist + c_len <= block_start + pos + gap && + dist <= max_backward_limit + gap && + ZopfliNodeDistanceCode(&nodes[pos]) > 0) { + return (uint32_t)pos; + } else { + return nodes[pos - c_len - i_len].u.shortcut; + } +} + +/* Fills in dist_cache[0..3] with the last four distances (as defined by + Section 4. of the Spec) that would be used at (block_start + pos) if we + used the shortest path of commands from block_start, computed from + nodes[0..pos]. The last four distances at block_start are in + starting_dist_cache[0..3]. + REQUIRES: nodes[pos].cost < kInfinity + REQUIRES: nodes[0..pos] satisfies that "ZopfliNode array invariant". */ +static void ComputeDistanceCache(const size_t pos, + const int* starting_dist_cache, + const ZopfliNode* nodes, + int* dist_cache) { + int idx = 0; + size_t p = nodes[pos].u.shortcut; + while (idx < 4 && p > 0) { + const size_t i_len = nodes[p].dcode_insert_length & 0x7FFFFFF; + const size_t c_len = ZopfliNodeCopyLength(&nodes[p]); + const size_t dist = ZopfliNodeCopyDistance(&nodes[p]); + dist_cache[idx++] = (int)dist; + /* Because of prerequisite, p >= c_len + i_len >= 2. */ + p = nodes[p - c_len - i_len].u.shortcut; + } + for (; idx < 4; ++idx) { + dist_cache[idx] = *starting_dist_cache++; + } +} + +/* Maintains "ZopfliNode array invariant" and pushes node to the queue, if it + is eligible. */ +static void EvaluateNode( + const size_t block_start, const size_t pos, const size_t max_backward_limit, + const size_t gap, const int* starting_dist_cache, + const ZopfliCostModel* model, StartPosQueue* queue, ZopfliNode* nodes) { + /* Save cost, because ComputeDistanceCache invalidates it. */ + float node_cost = nodes[pos].u.cost; + nodes[pos].u.shortcut = ComputeDistanceShortcut( + block_start, pos, max_backward_limit, gap, nodes); + if (node_cost <= ZopfliCostModelGetLiteralCosts(model, 0, pos)) { + PosData posdata; + posdata.pos = pos; + posdata.cost = node_cost; + posdata.costdiff = node_cost - + ZopfliCostModelGetLiteralCosts(model, 0, pos); + ComputeDistanceCache( + pos, starting_dist_cache, nodes, posdata.distance_cache); + StartPosQueuePush(queue, &posdata); + } +} + +/* Returns longest copy length. */ +static size_t UpdateNodes( + const size_t num_bytes, const size_t block_start, const size_t pos, + const uint8_t* ringbuffer, const size_t ringbuffer_mask, + const BrotliEncoderParams* params, const size_t max_backward_limit, + const int* starting_dist_cache, const size_t num_matches, + const BackwardMatch* matches, const ZopfliCostModel* model, + StartPosQueue* queue, ZopfliNode* nodes) { + const size_t stream_offset = params->stream_offset; + const size_t cur_ix = block_start + pos; + const size_t cur_ix_masked = cur_ix & ringbuffer_mask; + const size_t max_distance = BROTLI_MIN(size_t, cur_ix, max_backward_limit); + const size_t dictionary_start = BROTLI_MIN(size_t, + cur_ix + stream_offset, max_backward_limit); + const size_t max_len = num_bytes - pos; + const size_t max_zopfli_len = MaxZopfliLen(params); + const size_t max_iters = MaxZopfliCandidates(params); + size_t min_len; + size_t result = 0; + size_t k; + const CompoundDictionary* addon = ¶ms->dictionary.compound; + size_t gap = addon->total_size; + + BROTLI_DCHECK(cur_ix_masked + max_len <= ringbuffer_mask); + + EvaluateNode(block_start + stream_offset, pos, max_backward_limit, gap, + starting_dist_cache, model, queue, nodes); + + { + const PosData* posdata = StartPosQueueAt(queue, 0); + float min_cost = (posdata->cost + ZopfliCostModelGetMinCostCmd(model) + + ZopfliCostModelGetLiteralCosts(model, posdata->pos, pos)); + min_len = ComputeMinimumCopyLength(min_cost, nodes, num_bytes, pos); + } + + /* Go over the command starting positions in order of increasing cost + difference. */ + for (k = 0; k < max_iters && k < StartPosQueueSize(queue); ++k) { + const PosData* posdata = StartPosQueueAt(queue, k); + const size_t start = posdata->pos; + const uint16_t inscode = GetInsertLengthCode(pos - start); + const float start_costdiff = posdata->costdiff; + const float base_cost = start_costdiff + (float)GetInsertExtra(inscode) + + ZopfliCostModelGetLiteralCosts(model, 0, pos); + + /* Look for last distance matches using the distance cache from this + starting position. */ + size_t best_len = min_len - 1; + size_t j = 0; + for (; j < BROTLI_NUM_DISTANCE_SHORT_CODES && best_len < max_len; ++j) { + const size_t idx = kDistanceCacheIndex[j]; + const size_t backward = + (size_t)(posdata->distance_cache[idx] + kDistanceCacheOffset[j]); + size_t prev_ix = cur_ix - backward; + size_t len = 0; + uint8_t continuation = ringbuffer[cur_ix_masked + best_len]; + if (cur_ix_masked + best_len > ringbuffer_mask) { + break; + } + if (BROTLI_PREDICT_FALSE(backward > dictionary_start + gap)) { + /* Word dictionary -> ignore. */ + continue; + } + if (backward <= max_distance) { + /* Regular backward reference. */ + if (prev_ix >= cur_ix) { + continue; + } + + prev_ix &= ringbuffer_mask; + if (prev_ix + best_len > ringbuffer_mask || + continuation != ringbuffer[prev_ix + best_len]) { + continue; + } + len = FindMatchLengthWithLimit(&ringbuffer[prev_ix], + &ringbuffer[cur_ix_masked], + max_len); + } else if (backward > dictionary_start) { + size_t d = 0; + size_t offset; + size_t limit; + const uint8_t* source; + offset = dictionary_start + 1 + addon->total_size - 1; + while (offset >= backward + addon->chunk_offsets[d + 1]) d++; + source = addon->chunk_source[d]; + offset = offset - addon->chunk_offsets[d] - backward; + limit = addon->chunk_offsets[d + 1] - addon->chunk_offsets[d] - offset; + limit = limit > max_len ? max_len : limit; + if (best_len >= limit || + continuation != source[offset + best_len]) { + continue; + } + len = FindMatchLengthWithLimit(&source[offset], + &ringbuffer[cur_ix_masked], + limit); + } else { + /* "Gray" area. It is addressable by decoder, but this encoder + instance does not have that data -> should not touch it. */ + continue; + } + { + const float dist_cost = base_cost + + ZopfliCostModelGetDistanceCost(model, j); + size_t l; + for (l = best_len + 1; l <= len; ++l) { + const uint16_t copycode = GetCopyLengthCode(l); + const uint16_t cmdcode = + CombineLengthCodes(inscode, copycode, j == 0); + const float cost = (cmdcode < 128 ? base_cost : dist_cost) + + (float)GetCopyExtra(copycode) + + ZopfliCostModelGetCommandCost(model, cmdcode); + if (cost < nodes[pos + l].u.cost) { + UpdateZopfliNode(nodes, pos, start, l, l, backward, j + 1, cost); + result = BROTLI_MAX(size_t, result, l); + } + best_len = l; + } + } + } + + /* At higher iterations look only for new last distance matches, since + looking only for new command start positions with the same distances + does not help much. */ + if (k >= 2) continue; + + { + /* Loop through all possible copy lengths at this position. */ + size_t len = min_len; + for (j = 0; j < num_matches; ++j) { + BackwardMatch match = matches[j]; + size_t dist = match.distance; + BROTLI_BOOL is_dictionary_match = + TO_BROTLI_BOOL(dist > dictionary_start + gap); + /* We already tried all possible last distance matches, so we can use + normal distance code here. */ + size_t dist_code = dist + BROTLI_NUM_DISTANCE_SHORT_CODES - 1; + uint16_t dist_symbol; + uint32_t distextra; + uint32_t distnumextra; + float dist_cost; + size_t max_match_len; + PrefixEncodeCopyDistance( + dist_code, params->dist.num_direct_distance_codes, + params->dist.distance_postfix_bits, &dist_symbol, &distextra); + distnumextra = dist_symbol >> 10; + dist_cost = base_cost + (float)distnumextra + + ZopfliCostModelGetDistanceCost(model, dist_symbol & 0x3FF); + + /* Try all copy lengths up until the maximum copy length corresponding + to this distance. If the distance refers to the static dictionary, or + the maximum length is long enough, try only one maximum length. */ + max_match_len = BackwardMatchLength(&match); + if (len < max_match_len && + (is_dictionary_match || max_match_len > max_zopfli_len)) { + len = max_match_len; + } + for (; len <= max_match_len; ++len) { + const size_t len_code = + is_dictionary_match ? BackwardMatchLengthCode(&match) : len; + const uint16_t copycode = GetCopyLengthCode(len_code); + const uint16_t cmdcode = CombineLengthCodes(inscode, copycode, 0); + const float cost = dist_cost + (float)GetCopyExtra(copycode) + + ZopfliCostModelGetCommandCost(model, cmdcode); + if (cost < nodes[pos + len].u.cost) { + UpdateZopfliNode(nodes, pos, start, len, len_code, dist, 0, cost); + result = BROTLI_MAX(size_t, result, len); + } + } + } + } + } + return result; +} + +static size_t ComputeShortestPathFromNodes(size_t num_bytes, + ZopfliNode* nodes) { + size_t index = num_bytes; + size_t num_commands = 0; + while ((nodes[index].dcode_insert_length & 0x7FFFFFF) == 0 && + nodes[index].length == 1) --index; + nodes[index].u.next = BROTLI_UINT32_MAX; + while (index != 0) { + size_t len = ZopfliNodeCommandLength(&nodes[index]); + index -= len; + nodes[index].u.next = (uint32_t)len; + num_commands++; + } + return num_commands; +} + +/* REQUIRES: nodes != NULL and len(nodes) >= num_bytes + 1 */ +void BrotliZopfliCreateCommands(const size_t num_bytes, + const size_t block_start, const ZopfliNode* nodes, int* dist_cache, + size_t* last_insert_len, const BrotliEncoderParams* params, + Command* commands, size_t* num_literals) { + const size_t stream_offset = params->stream_offset; + const size_t max_backward_limit = BROTLI_MAX_BACKWARD_LIMIT(params->lgwin); + size_t pos = 0; + uint32_t offset = nodes[0].u.next; + size_t i; + size_t gap = params->dictionary.compound.total_size; + for (i = 0; offset != BROTLI_UINT32_MAX; i++) { + const ZopfliNode* next = &nodes[pos + offset]; + size_t copy_length = ZopfliNodeCopyLength(next); + size_t insert_length = next->dcode_insert_length & 0x7FFFFFF; + pos += insert_length; + offset = next->u.next; + if (i == 0) { + insert_length += *last_insert_len; + *last_insert_len = 0; + } + { + size_t distance = ZopfliNodeCopyDistance(next); + size_t len_code = ZopfliNodeLengthCode(next); + size_t dictionary_start = BROTLI_MIN(size_t, + block_start + pos + stream_offset, max_backward_limit); + BROTLI_BOOL is_dictionary = + TO_BROTLI_BOOL(distance > dictionary_start + gap); + size_t dist_code = ZopfliNodeDistanceCode(next); + InitCommand(&commands[i], ¶ms->dist, insert_length, + copy_length, (int)len_code - (int)copy_length, dist_code); + + if (!is_dictionary && dist_code > 0) { + dist_cache[3] = dist_cache[2]; + dist_cache[2] = dist_cache[1]; + dist_cache[1] = dist_cache[0]; + dist_cache[0] = (int)distance; + } + } + + *num_literals += insert_length; + pos += copy_length; + } + *last_insert_len += num_bytes - pos; +} + +static size_t ZopfliIterate(size_t num_bytes, size_t position, + const uint8_t* ringbuffer, size_t ringbuffer_mask, + const BrotliEncoderParams* params, const size_t gap, const int* dist_cache, + const ZopfliCostModel* model, const uint32_t* num_matches, + const BackwardMatch* matches, ZopfliNode* nodes) { + const size_t stream_offset = params->stream_offset; + const size_t max_backward_limit = BROTLI_MAX_BACKWARD_LIMIT(params->lgwin); + const size_t max_zopfli_len = MaxZopfliLen(params); + StartPosQueue queue; + size_t cur_match_pos = 0; + size_t i; + nodes[0].length = 0; + nodes[0].u.cost = 0; + InitStartPosQueue(&queue); + for (i = 0; i + 3 < num_bytes; i++) { + size_t skip = UpdateNodes(num_bytes, position, i, ringbuffer, + ringbuffer_mask, params, max_backward_limit, dist_cache, + num_matches[i], &matches[cur_match_pos], model, &queue, nodes); + if (skip < BROTLI_LONG_COPY_QUICK_STEP) skip = 0; + cur_match_pos += num_matches[i]; + if (num_matches[i] == 1 && + BackwardMatchLength(&matches[cur_match_pos - 1]) > max_zopfli_len) { + skip = BROTLI_MAX(size_t, + BackwardMatchLength(&matches[cur_match_pos - 1]), skip); + } + if (skip > 1) { + skip--; + while (skip) { + i++; + if (i + 3 >= num_bytes) break; + EvaluateNode(position + stream_offset, i, max_backward_limit, gap, + dist_cache, model, &queue, nodes); + cur_match_pos += num_matches[i]; + skip--; + } + } + } + return ComputeShortestPathFromNodes(num_bytes, nodes); +} + +static void MergeMatches(BackwardMatch* dst, + BackwardMatch* src1, size_t len1, BackwardMatch* src2, size_t len2) { + while (len1 > 0 && len2 > 0) { + size_t l1 = BackwardMatchLength(src1); + size_t l2 = BackwardMatchLength(src2); + if (l1 < l2 || ((l1 == l2) && (src1->distance < src2->distance))) { + *dst++ = *src1++; + len1--; + } else { + *dst++ = *src2++; + len2--; + } + } + while (len1-- > 0) *dst++ = *src1++; + while (len2-- > 0) *dst++ = *src2++; +} + +/* REQUIRES: nodes != NULL and len(nodes) >= num_bytes + 1 */ +size_t BrotliZopfliComputeShortestPath(MemoryManager* m, size_t num_bytes, + size_t position, const uint8_t* ringbuffer, size_t ringbuffer_mask, + ContextLut literal_context_lut, const BrotliEncoderParams* params, + const int* dist_cache, Hasher* hasher, ZopfliNode* nodes) { + const size_t stream_offset = params->stream_offset; + const size_t max_backward_limit = BROTLI_MAX_BACKWARD_LIMIT(params->lgwin); + const size_t max_zopfli_len = MaxZopfliLen(params); + StartPosQueue queue; + BackwardMatch* BROTLI_RESTRICT matches = + BROTLI_ALLOC(m, BackwardMatch, 2 * (MAX_NUM_MATCHES_H10 + 64)); + const size_t store_end = num_bytes >= StoreLookaheadH10() ? + position + num_bytes - StoreLookaheadH10() + 1 : position; + size_t i; + const CompoundDictionary* addon = ¶ms->dictionary.compound; + size_t gap = addon->total_size; + size_t lz_matches_offset = + (addon->num_chunks != 0) ? (MAX_NUM_MATCHES_H10 + 128) : 0; + ZopfliCostModel* model = BROTLI_ALLOC(m, ZopfliCostModel, 1); + if (BROTLI_IS_OOM(m) || BROTLI_IS_NULL(model) || BROTLI_IS_NULL(matches)) { + return 0; + } + nodes[0].length = 0; + nodes[0].u.cost = 0; + InitZopfliCostModel(m, model, ¶ms->dist, num_bytes); + if (BROTLI_IS_OOM(m)) return 0; + ZopfliCostModelSetFromLiteralCosts( + model, position, ringbuffer, ringbuffer_mask); + InitStartPosQueue(&queue); + for (i = 0; i + HashTypeLengthH10() - 1 < num_bytes; i++) { + const size_t pos = position + i; + const size_t max_distance = BROTLI_MIN(size_t, pos, max_backward_limit); + const size_t dictionary_start = BROTLI_MIN(size_t, + pos + stream_offset, max_backward_limit); + size_t skip; + size_t num_matches; + int dict_id = 0; + if (params->dictionary.contextual.context_based) { + uint8_t p1 = pos >= 1 ? + ringbuffer[(size_t)(pos - 1) & ringbuffer_mask] : 0; + uint8_t p2 = pos >= 2 ? + ringbuffer[(size_t)(pos - 2) & ringbuffer_mask] : 0; + dict_id = params->dictionary.contextual.context_map[ + BROTLI_CONTEXT(p1, p2, literal_context_lut)]; + } + num_matches = FindAllMatchesH10(&hasher->privat._H10, + params->dictionary.contextual.dict[dict_id], + ringbuffer, ringbuffer_mask, pos, num_bytes - i, max_distance, + dictionary_start + gap, params, &matches[lz_matches_offset]); + if (addon->num_chunks != 0) { + size_t cd_matches = LookupAllCompoundDictionaryMatches(addon, + ringbuffer, ringbuffer_mask, pos, 3, num_bytes - i, + dictionary_start, params->dist.max_distance, + &matches[lz_matches_offset - 64], 64); + MergeMatches(matches, &matches[lz_matches_offset - 64], cd_matches, + &matches[lz_matches_offset], num_matches); + num_matches += cd_matches; + } + if (num_matches > 0 && + BackwardMatchLength(&matches[num_matches - 1]) > max_zopfli_len) { + matches[0] = matches[num_matches - 1]; + num_matches = 1; + } + skip = UpdateNodes(num_bytes, position, i, ringbuffer, ringbuffer_mask, + params, max_backward_limit, dist_cache, num_matches, matches, model, + &queue, nodes); + if (skip < BROTLI_LONG_COPY_QUICK_STEP) skip = 0; + if (num_matches == 1 && BackwardMatchLength(&matches[0]) > max_zopfli_len) { + skip = BROTLI_MAX(size_t, BackwardMatchLength(&matches[0]), skip); + } + if (skip > 1) { + /* Add the tail of the copy to the hasher. */ + StoreRangeH10(&hasher->privat._H10, + ringbuffer, ringbuffer_mask, pos + 1, BROTLI_MIN( + size_t, pos + skip, store_end)); + skip--; + while (skip) { + i++; + if (i + HashTypeLengthH10() - 1 >= num_bytes) break; + EvaluateNode(position + stream_offset, i, max_backward_limit, gap, + dist_cache, model, &queue, nodes); + skip--; + } + } + } + CleanupZopfliCostModel(m, model); + BROTLI_FREE(m, model); + BROTLI_FREE(m, matches); + return ComputeShortestPathFromNodes(num_bytes, nodes); +} + +void BrotliCreateZopfliBackwardReferences(MemoryManager* m, size_t num_bytes, + size_t position, const uint8_t* ringbuffer, size_t ringbuffer_mask, + ContextLut literal_context_lut, const BrotliEncoderParams* params, + Hasher* hasher, int* dist_cache, size_t* last_insert_len, + Command* commands, size_t* num_commands, size_t* num_literals) { + ZopfliNode* nodes = BROTLI_ALLOC(m, ZopfliNode, num_bytes + 1); + if (BROTLI_IS_OOM(m) || BROTLI_IS_NULL(nodes)) return; + BrotliInitZopfliNodes(nodes, num_bytes + 1); + *num_commands += BrotliZopfliComputeShortestPath(m, num_bytes, + position, ringbuffer, ringbuffer_mask, literal_context_lut, params, + dist_cache, hasher, nodes); + if (BROTLI_IS_OOM(m)) return; + BrotliZopfliCreateCommands(num_bytes, position, nodes, dist_cache, + last_insert_len, params, commands, num_literals); + BROTLI_FREE(m, nodes); +} + +void BrotliCreateHqZopfliBackwardReferences(MemoryManager* m, size_t num_bytes, + size_t position, const uint8_t* ringbuffer, size_t ringbuffer_mask, + ContextLut literal_context_lut, const BrotliEncoderParams* params, + Hasher* hasher, int* dist_cache, size_t* last_insert_len, + Command* commands, size_t* num_commands, size_t* num_literals) { + const size_t stream_offset = params->stream_offset; + const size_t max_backward_limit = BROTLI_MAX_BACKWARD_LIMIT(params->lgwin); + uint32_t* num_matches = BROTLI_ALLOC(m, uint32_t, num_bytes); + size_t matches_size = 4 * num_bytes; + const size_t store_end = num_bytes >= StoreLookaheadH10() ? + position + num_bytes - StoreLookaheadH10() + 1 : position; + size_t cur_match_pos = 0; + size_t i; + size_t orig_num_literals; + size_t orig_last_insert_len; + int orig_dist_cache[4]; + size_t orig_num_commands; + ZopfliCostModel* model = BROTLI_ALLOC(m, ZopfliCostModel, 1); + ZopfliNode* nodes; + BackwardMatch* matches = BROTLI_ALLOC(m, BackwardMatch, matches_size); + const CompoundDictionary* addon = ¶ms->dictionary.compound; + size_t gap = addon->total_size; + size_t shadow_matches = + (addon->num_chunks != 0) ? (MAX_NUM_MATCHES_H10 + 128) : 0; + if (BROTLI_IS_OOM(m) || BROTLI_IS_NULL(model) || + BROTLI_IS_NULL(num_matches) || BROTLI_IS_NULL(matches)) { + return; + } + for (i = 0; i + HashTypeLengthH10() - 1 < num_bytes; ++i) { + const size_t pos = position + i; + size_t max_distance = BROTLI_MIN(size_t, pos, max_backward_limit); + size_t dictionary_start = BROTLI_MIN(size_t, + pos + stream_offset, max_backward_limit); + size_t max_length = num_bytes - i; + size_t num_found_matches; + size_t cur_match_end; + size_t j; + int dict_id = 0; + if (params->dictionary.contextual.context_based) { + uint8_t p1 = pos >= 1 ? + ringbuffer[(size_t)(pos - 1) & ringbuffer_mask] : 0; + uint8_t p2 = pos >= 2 ? + ringbuffer[(size_t)(pos - 2) & ringbuffer_mask] : 0; + dict_id = params->dictionary.contextual.context_map[ + BROTLI_CONTEXT(p1, p2, literal_context_lut)]; + } + /* Ensure that we have enough free slots. */ + BROTLI_ENSURE_CAPACITY(m, BackwardMatch, matches, matches_size, + cur_match_pos + MAX_NUM_MATCHES_H10 + shadow_matches); + if (BROTLI_IS_OOM(m)) return; + num_found_matches = FindAllMatchesH10(&hasher->privat._H10, + params->dictionary.contextual.dict[dict_id], + ringbuffer, ringbuffer_mask, pos, max_length, + max_distance, dictionary_start + gap, params, + &matches[cur_match_pos + shadow_matches]); + if (addon->num_chunks != 0) { + size_t cd_matches = LookupAllCompoundDictionaryMatches(addon, + ringbuffer, ringbuffer_mask, pos, 3, max_length, + dictionary_start, params->dist.max_distance, + &matches[cur_match_pos + shadow_matches - 64], 64); + MergeMatches(&matches[cur_match_pos], + &matches[cur_match_pos + shadow_matches - 64], cd_matches, + &matches[cur_match_pos + shadow_matches], num_found_matches); + num_found_matches += cd_matches; + } + cur_match_end = cur_match_pos + num_found_matches; + for (j = cur_match_pos; j + 1 < cur_match_end; ++j) { + BROTLI_DCHECK(BackwardMatchLength(&matches[j]) <= + BackwardMatchLength(&matches[j + 1])); + } + num_matches[i] = (uint32_t)num_found_matches; + if (num_found_matches > 0) { + const size_t match_len = BackwardMatchLength(&matches[cur_match_end - 1]); + if (match_len > MAX_ZOPFLI_LEN_QUALITY_11) { + const size_t skip = match_len - 1; + matches[cur_match_pos++] = matches[cur_match_end - 1]; + num_matches[i] = 1; + /* Add the tail of the copy to the hasher. */ + StoreRangeH10(&hasher->privat._H10, + ringbuffer, ringbuffer_mask, pos + 1, + BROTLI_MIN(size_t, pos + match_len, store_end)); + memset(&num_matches[i + 1], 0, skip * sizeof(num_matches[0])); + i += skip; + } else { + cur_match_pos = cur_match_end; + } + } + } + orig_num_literals = *num_literals; + orig_last_insert_len = *last_insert_len; + memcpy(orig_dist_cache, dist_cache, 4 * sizeof(dist_cache[0])); + orig_num_commands = *num_commands; + nodes = BROTLI_ALLOC(m, ZopfliNode, num_bytes + 1); + if (BROTLI_IS_OOM(m) || BROTLI_IS_NULL(nodes)) return; + InitZopfliCostModel(m, model, ¶ms->dist, num_bytes); + if (BROTLI_IS_OOM(m)) return; + for (i = 0; i < 2; i++) { + BrotliInitZopfliNodes(nodes, num_bytes + 1); + if (i == 0) { + ZopfliCostModelSetFromLiteralCosts( + model, position, ringbuffer, ringbuffer_mask); + } else { + ZopfliCostModelSetFromCommands(model, position, ringbuffer, + ringbuffer_mask, commands, *num_commands - orig_num_commands, + orig_last_insert_len); + } + *num_commands = orig_num_commands; + *num_literals = orig_num_literals; + *last_insert_len = orig_last_insert_len; + memcpy(dist_cache, orig_dist_cache, 4 * sizeof(dist_cache[0])); + *num_commands += ZopfliIterate(num_bytes, position, ringbuffer, + ringbuffer_mask, params, gap, dist_cache, model, num_matches, matches, + nodes); + BrotliZopfliCreateCommands(num_bytes, position, nodes, dist_cache, + last_insert_len, params, commands, num_literals); + } + CleanupZopfliCostModel(m, model); + BROTLI_FREE(m, model); + BROTLI_FREE(m, nodes); + BROTLI_FREE(m, matches); + BROTLI_FREE(m, num_matches); +} + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif diff --git a/packages/runtime/vendor/brotli/c/enc/backward_references_hq.h b/packages/runtime/vendor/brotli/c/enc/backward_references_hq.h new file mode 100644 index 000000000..4aa270674 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/backward_references_hq.h @@ -0,0 +1,92 @@ +/* Copyright 2013 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* Function to find backward reference copies. */ + +#ifndef BROTLI_ENC_BACKWARD_REFERENCES_HQ_H_ +#define BROTLI_ENC_BACKWARD_REFERENCES_HQ_H_ + +#include "../common/context.h" +#include "../common/platform.h" +#include "command.h" +#include "hash.h" +#include "memory.h" +#include "params.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +BROTLI_INTERNAL void BrotliCreateZopfliBackwardReferences(MemoryManager* m, + size_t num_bytes, + size_t position, const uint8_t* ringbuffer, size_t ringbuffer_mask, + ContextLut literal_context_lut, const BrotliEncoderParams* params, + Hasher* hasher, int* dist_cache, size_t* last_insert_len, + Command* commands, size_t* num_commands, size_t* num_literals); + +BROTLI_INTERNAL void BrotliCreateHqZopfliBackwardReferences(MemoryManager* m, + size_t num_bytes, + size_t position, const uint8_t* ringbuffer, size_t ringbuffer_mask, + ContextLut literal_context_lut, const BrotliEncoderParams* params, + Hasher* hasher, int* dist_cache, size_t* last_insert_len, + Command* commands, size_t* num_commands, size_t* num_literals); + +typedef struct ZopfliNode { + /* Best length to get up to this byte (not including this byte itself) + highest 7 bit is used to reconstruct the length code. */ + uint32_t length; + /* Distance associated with the length. */ + uint32_t distance; + /* Number of literal inserts before this copy; highest 5 bits contain + distance short code + 1 (or zero if no short code). */ + uint32_t dcode_insert_length; + + /* This union holds information used by dynamic-programming. During forward + pass |cost| it used to store the goal function. When node is processed its + |cost| is invalidated in favor of |shortcut|. On path back-tracing pass + |next| is assigned the offset to next node on the path. */ + union { + /* Smallest cost to get to this byte from the beginning, as found so far. */ + float cost; + /* Offset to the next node on the path. Equals to command_length() of the + next node on the path. For last node equals to BROTLI_UINT32_MAX */ + uint32_t next; + /* Node position that provides next distance for distance cache. */ + uint32_t shortcut; + } u; +} ZopfliNode; + +BROTLI_INTERNAL void BrotliInitZopfliNodes(ZopfliNode* array, size_t length); + +/* Computes the shortest path of commands from position to at most + position + num_bytes. + + On return, path->size() is the number of commands found and path[i] is the + length of the i-th command (copy length plus insert length). + Note that the sum of the lengths of all commands can be less than num_bytes. + + On return, the nodes[0..num_bytes] array will have the following + "ZopfliNode array invariant": + For each i in [1..num_bytes], if nodes[i].cost < kInfinity, then + (1) nodes[i].copy_length() >= 2 + (2) nodes[i].command_length() <= i and + (3) nodes[i - nodes[i].command_length()].cost < kInfinity */ +BROTLI_INTERNAL size_t BrotliZopfliComputeShortestPath( + MemoryManager* m, size_t num_bytes, + size_t position, const uint8_t* ringbuffer, size_t ringbuffer_mask, + ContextLut literal_context_lut, const BrotliEncoderParams* params, + const int* dist_cache, Hasher* hasher, ZopfliNode* nodes); + +BROTLI_INTERNAL void BrotliZopfliCreateCommands( + const size_t num_bytes, const size_t block_start, const ZopfliNode* nodes, + int* dist_cache, size_t* last_insert_len, const BrotliEncoderParams* params, + Command* commands, size_t* num_literals); + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif + +#endif /* BROTLI_ENC_BACKWARD_REFERENCES_HQ_H_ */ diff --git a/packages/runtime/vendor/brotli/c/enc/backward_references_inc.h b/packages/runtime/vendor/brotli/c/enc/backward_references_inc.h new file mode 100644 index 000000000..752c12e9f --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/backward_references_inc.h @@ -0,0 +1,189 @@ +/* NOLINT(build/header_guard) */ +/* Copyright 2013 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* template parameters: EXPORT_FN, FN */ + +static BROTLI_NOINLINE void EXPORT_FN(CreateBackwardReferences)( + size_t num_bytes, size_t position, + const uint8_t* ringbuffer, size_t ringbuffer_mask, + ContextLut literal_context_lut, const BrotliEncoderParams* params, + Hasher* hasher, int* dist_cache, size_t* last_insert_len, + Command* commands, size_t* num_commands, size_t* num_literals) { + HASHER()* privat = &hasher->privat.FN(_); + /* Set maximum distance, see section 9.1. of the spec. */ + const size_t max_backward_limit = BROTLI_MAX_BACKWARD_LIMIT(params->lgwin); + const size_t position_offset = params->stream_offset; + + const Command* const orig_commands = commands; + size_t insert_length = *last_insert_len; + const size_t pos_end = position + num_bytes; + const size_t store_end = num_bytes >= FN(StoreLookahead)() ? + position + num_bytes - FN(StoreLookahead)() + 1 : position; + + /* For speed up heuristics for random data. */ + const size_t random_heuristics_window_size = + LiteralSpreeLengthForSparseSearch(params); + size_t apply_random_heuristics = position + random_heuristics_window_size; + const size_t gap = params->dictionary.compound.total_size; + + /* Minimum score to accept a backward reference. */ + const score_t kMinScore = BROTLI_SCORE_BASE + 100; + + FN(PrepareDistanceCache)(privat, dist_cache); + + while (position + FN(HashTypeLength)() < pos_end) { + size_t max_length = pos_end - position; + size_t max_distance = BROTLI_MIN(size_t, position, max_backward_limit); + size_t dictionary_start = BROTLI_MIN(size_t, + position + position_offset, max_backward_limit); + HasherSearchResult sr; + int dict_id = 0; + uint8_t p1 = 0; + uint8_t p2 = 0; + if (params->dictionary.contextual.context_based) { + p1 = position >= 1 ? + ringbuffer[(size_t)(position - 1) & ringbuffer_mask] : 0; + p2 = position >= 2 ? + ringbuffer[(size_t)(position - 2) & ringbuffer_mask] : 0; + dict_id = params->dictionary.contextual.context_map[ + BROTLI_CONTEXT(p1, p2, literal_context_lut)]; + } + sr.len = 0; + sr.len_code_delta = 0; + sr.distance = 0; + sr.score = kMinScore; + FN(FindLongestMatch)(privat, params->dictionary.contextual.dict[dict_id], + ringbuffer, ringbuffer_mask, dist_cache, position, max_length, + max_distance, dictionary_start + gap, params->dist.max_distance, &sr); + if (ENABLE_COMPOUND_DICTIONARY) { + LookupCompoundDictionaryMatch(¶ms->dictionary.compound, ringbuffer, + ringbuffer_mask, dist_cache, position, max_length, + dictionary_start, params->dist.max_distance, &sr); + } + if (sr.score > kMinScore) { + /* Found a match. Let's look for something even better ahead. */ + int delayed_backward_references_in_row = 0; + --max_length; + for (;; --max_length) { + const score_t cost_diff_lazy = 175; + HasherSearchResult sr2; + sr2.len = params->quality < MIN_QUALITY_FOR_EXTENSIVE_REFERENCE_SEARCH ? + BROTLI_MIN(size_t, sr.len - 1, max_length) : 0; + sr2.len_code_delta = 0; + sr2.distance = 0; + sr2.score = kMinScore; + max_distance = BROTLI_MIN(size_t, position + 1, max_backward_limit); + dictionary_start = BROTLI_MIN(size_t, + position + 1 + position_offset, max_backward_limit); + if (params->dictionary.contextual.context_based) { + p2 = p1; + p1 = ringbuffer[position & ringbuffer_mask]; + dict_id = params->dictionary.contextual.context_map[ + BROTLI_CONTEXT(p1, p2, literal_context_lut)]; + } + FN(FindLongestMatch)(privat, + params->dictionary.contextual.dict[dict_id], + ringbuffer, ringbuffer_mask, dist_cache, position + 1, max_length, + max_distance, dictionary_start + gap, params->dist.max_distance, + &sr2); + if (ENABLE_COMPOUND_DICTIONARY) { + LookupCompoundDictionaryMatch( + ¶ms->dictionary.compound, ringbuffer, + ringbuffer_mask, dist_cache, position + 1, max_length, + dictionary_start, params->dist.max_distance, &sr2); + } + if (sr2.score >= sr.score + cost_diff_lazy) { + /* Ok, let's just write one byte for now and start a match from the + next byte. */ + ++position; + ++insert_length; + sr = sr2; + if (++delayed_backward_references_in_row < 4 && + position + FN(HashTypeLength)() < pos_end) { + continue; + } + } + break; + } + apply_random_heuristics = + position + 2 * sr.len + random_heuristics_window_size; + dictionary_start = BROTLI_MIN(size_t, + position + position_offset, max_backward_limit); + { + /* The first 16 codes are special short-codes, + and the minimum offset is 1. */ + size_t distance_code = ComputeDistanceCode( + sr.distance, dictionary_start + gap, dist_cache); + if ((sr.distance <= (dictionary_start + gap)) && distance_code > 0) { + dist_cache[3] = dist_cache[2]; + dist_cache[2] = dist_cache[1]; + dist_cache[1] = dist_cache[0]; + dist_cache[0] = (int)sr.distance; + FN(PrepareDistanceCache)(privat, dist_cache); + } + InitCommand(commands++, ¶ms->dist, insert_length, + sr.len, sr.len_code_delta, distance_code); + } + *num_literals += insert_length; + insert_length = 0; + /* Put the hash keys into the table, if there are enough bytes left. + Depending on the hasher implementation, it can push all positions + in the given range or only a subset of them. + Avoid hash poisoning with RLE data. */ + { + size_t range_start = position + 2; + size_t range_end = BROTLI_MIN(size_t, position + sr.len, store_end); + if (sr.distance < (sr.len >> 2)) { + range_start = BROTLI_MIN(size_t, range_end, BROTLI_MAX(size_t, + range_start, position + sr.len - (sr.distance << 2))); + } + FN(StoreRange)(privat, ringbuffer, ringbuffer_mask, range_start, + range_end); + } + position += sr.len; + } else { + ++insert_length; + ++position; + /* If we have not seen matches for a long time, we can skip some + match lookups. Unsuccessful match lookups are very very expensive + and this kind of a heuristic speeds up compression quite + a lot. */ + if (position > apply_random_heuristics) { + /* Going through uncompressible data, jump. */ + if (position > + apply_random_heuristics + 4 * random_heuristics_window_size) { + /* It is quite a long time since we saw a copy, so we assume + that this data is not compressible, and store hashes less + often. Hashes of non compressible data are less likely to + turn out to be useful in the future, too, so we store less of + them to not to flood out the hash table of good compressible + data. */ + const size_t kMargin = + BROTLI_MAX(size_t, FN(StoreLookahead)() - 1, 4); + size_t pos_jump = + BROTLI_MIN(size_t, position + 16, pos_end - kMargin); + for (; position < pos_jump; position += 4) { + FN(Store)(privat, ringbuffer, ringbuffer_mask, position); + insert_length += 4; + } + } else { + const size_t kMargin = + BROTLI_MAX(size_t, FN(StoreLookahead)() - 1, 2); + size_t pos_jump = + BROTLI_MIN(size_t, position + 8, pos_end - kMargin); + for (; position < pos_jump; position += 2) { + FN(Store)(privat, ringbuffer, ringbuffer_mask, position); + insert_length += 2; + } + } + } + } + } + insert_length += pos_end - position; + *last_insert_len = insert_length; + *num_commands += (size_t)(commands - orig_commands); +} diff --git a/packages/runtime/vendor/brotli/c/enc/bit_cost.c b/packages/runtime/vendor/brotli/c/enc/bit_cost.c new file mode 100644 index 000000000..059f8d996 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/bit_cost.c @@ -0,0 +1,60 @@ +/* Copyright 2013 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* Functions to estimate the bit cost of Huffman trees. */ + +#include "bit_cost.h" + +#include "../common/platform.h" +#include "fast_log.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +double BrotliBitsEntropy(const uint32_t* population, size_t size) { + size_t sum = 0; + double retval = 0; + const uint32_t* population_end = population + size; + size_t p; + if (size & 1) { + goto odd_number_of_elements_left; + } + while (population < population_end) { + p = *population++; + sum += p; + retval -= (double)p * FastLog2(p); + odd_number_of_elements_left: + p = *population++; + sum += p; + retval -= (double)p * FastLog2(p); + } + if (sum) retval += (double)sum * FastLog2(sum); + + if (retval < (double)sum) { + /* TODO(eustas): consider doing that per-symbol? */ + /* At least one bit per literal is needed. */ + retval = (double)sum; + } + + return retval; +} + +#define FN(X) X ## Literal +#include "bit_cost_inc.h" /* NOLINT(build/include) */ +#undef FN + +#define FN(X) X ## Command +#include "bit_cost_inc.h" /* NOLINT(build/include) */ +#undef FN + +#define FN(X) X ## Distance +#include "bit_cost_inc.h" /* NOLINT(build/include) */ +#undef FN + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif diff --git a/packages/runtime/vendor/brotli/c/enc/bit_cost.h b/packages/runtime/vendor/brotli/c/enc/bit_cost.h new file mode 100644 index 000000000..90313c4c3 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/bit_cost.h @@ -0,0 +1,32 @@ +/* Copyright 2013 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* Functions to estimate the bit cost of Huffman trees. */ + +#ifndef BROTLI_ENC_BIT_COST_H_ +#define BROTLI_ENC_BIT_COST_H_ + +#include "../common/platform.h" +#include "histogram.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +BROTLI_INTERNAL double BrotliBitsEntropy( + const uint32_t* population, size_t size); +BROTLI_INTERNAL double BrotliPopulationCostLiteral( + const HistogramLiteral* histogram); +BROTLI_INTERNAL double BrotliPopulationCostCommand( + const HistogramCommand* histogram); +BROTLI_INTERNAL double BrotliPopulationCostDistance( + const HistogramDistance* histogram); + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif + +#endif /* BROTLI_ENC_BIT_COST_H_ */ diff --git a/packages/runtime/vendor/brotli/c/enc/bit_cost_inc.h b/packages/runtime/vendor/brotli/c/enc/bit_cost_inc.h new file mode 100644 index 000000000..8e67f2503 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/bit_cost_inc.h @@ -0,0 +1,127 @@ +/* NOLINT(build/header_guard) */ +/* Copyright 2013 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* template parameters: FN */ + +#define HistogramType FN(Histogram) + +double FN(BrotliPopulationCost)(const HistogramType* histogram) { + static const double kOneSymbolHistogramCost = 12; + static const double kTwoSymbolHistogramCost = 20; + static const double kThreeSymbolHistogramCost = 28; + static const double kFourSymbolHistogramCost = 37; + const size_t data_size = FN(HistogramDataSize)(); + int count = 0; + size_t s[5]; + double bits = 0.0; + size_t i; + if (histogram->total_count_ == 0) { + return kOneSymbolHistogramCost; + } + for (i = 0; i < data_size; ++i) { + if (histogram->data_[i] > 0) { + s[count] = i; + ++count; + if (count > 4) break; + } + } + if (count == 1) { + return kOneSymbolHistogramCost; + } + if (count == 2) { + return (kTwoSymbolHistogramCost + (double)histogram->total_count_); + } + if (count == 3) { + const uint32_t histo0 = histogram->data_[s[0]]; + const uint32_t histo1 = histogram->data_[s[1]]; + const uint32_t histo2 = histogram->data_[s[2]]; + const uint32_t histomax = + BROTLI_MAX(uint32_t, histo0, BROTLI_MAX(uint32_t, histo1, histo2)); + return (kThreeSymbolHistogramCost + + 2 * (histo0 + histo1 + histo2) - histomax); + } + if (count == 4) { + uint32_t histo[4]; + uint32_t h23; + uint32_t histomax; + for (i = 0; i < 4; ++i) { + histo[i] = histogram->data_[s[i]]; + } + /* Sort */ + for (i = 0; i < 4; ++i) { + size_t j; + for (j = i + 1; j < 4; ++j) { + if (histo[j] > histo[i]) { + BROTLI_SWAP(uint32_t, histo, j, i); + } + } + } + h23 = histo[2] + histo[3]; + histomax = BROTLI_MAX(uint32_t, h23, histo[0]); + return (kFourSymbolHistogramCost + + 3 * h23 + 2 * (histo[0] + histo[1]) - histomax); + } + + { + /* In this loop we compute the entropy of the histogram and simultaneously + build a simplified histogram of the code length codes where we use the + zero repeat code 17, but we don't use the non-zero repeat code 16. */ + size_t max_depth = 1; + uint32_t depth_histo[BROTLI_CODE_LENGTH_CODES] = { 0 }; + const double log2total = FastLog2(histogram->total_count_); + for (i = 0; i < data_size;) { + if (histogram->data_[i] > 0) { + /* Compute -log2(P(symbol)) = -log2(count(symbol)/total_count) = + = log2(total_count) - log2(count(symbol)) */ + double log2p = log2total - FastLog2(histogram->data_[i]); + /* Approximate the bit depth by round(-log2(P(symbol))) */ + size_t depth = (size_t)(log2p + 0.5); + bits += histogram->data_[i] * log2p; + if (depth > 15) { + depth = 15; + } + if (depth > max_depth) { + max_depth = depth; + } + ++depth_histo[depth]; + ++i; + } else { + /* Compute the run length of zeros and add the appropriate number of 0 + and 17 code length codes to the code length code histogram. */ + uint32_t reps = 1; + size_t k; + for (k = i + 1; k < data_size && histogram->data_[k] == 0; ++k) { + ++reps; + } + i += reps; + if (i == data_size) { + /* Don't add any cost for the last zero run, since these are encoded + only implicitly. */ + break; + } + if (reps < 3) { + depth_histo[0] += reps; + } else { + reps -= 2; + while (reps > 0) { + ++depth_histo[BROTLI_REPEAT_ZERO_CODE_LENGTH]; + /* Add the 3 extra bits for the 17 code length code. */ + bits += 3; + reps >>= 3; + } + } + } + } + /* Add the estimated encoding cost of the code length code histogram. */ + bits += (double)(18 + 2 * max_depth); + /* Add the entropy of the code length code histogram. */ + bits += BrotliBitsEntropy(depth_histo, BROTLI_CODE_LENGTH_CODES); + } + return bits; +} + +#undef HistogramType diff --git a/packages/runtime/vendor/brotli/c/enc/block_encoder_inc.h b/packages/runtime/vendor/brotli/c/enc/block_encoder_inc.h new file mode 100644 index 000000000..8cbd5eac6 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/block_encoder_inc.h @@ -0,0 +1,34 @@ +/* NOLINT(build/header_guard) */ +/* Copyright 2014 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* template parameters: FN */ + +#define HistogramType FN(Histogram) + +/* Creates entropy codes for all block types and stores them to the bit + stream. */ +static void FN(BuildAndStoreEntropyCodes)(MemoryManager* m, BlockEncoder* self, + const HistogramType* histograms, const size_t histograms_size, + const size_t alphabet_size, HuffmanTree* tree, + size_t* storage_ix, uint8_t* storage) { + const size_t table_size = histograms_size * self->histogram_length_; + self->depths_ = BROTLI_ALLOC(m, uint8_t, table_size); + self->bits_ = BROTLI_ALLOC(m, uint16_t, table_size); + if (BROTLI_IS_OOM(m)) return; + + { + size_t i; + for (i = 0; i < histograms_size; ++i) { + size_t ix = i * self->histogram_length_; + BuildAndStoreHuffmanTree(&histograms[i].data_[0], self->histogram_length_, + alphabet_size, tree, &self->depths_[ix], &self->bits_[ix], + storage_ix, storage); + } + } +} + +#undef HistogramType diff --git a/packages/runtime/vendor/brotli/c/enc/block_splitter.c b/packages/runtime/vendor/brotli/c/enc/block_splitter.c new file mode 100644 index 000000000..13e924f09 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/block_splitter.c @@ -0,0 +1,214 @@ +/* Copyright 2013 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* Block split point selection utilities. */ + +#include "block_splitter.h" + +#include "../common/platform.h" +#include "bit_cost.h" +#include "cluster.h" +#include "command.h" +#include "fast_log.h" +#include "histogram.h" +#include "memory.h" +#include "quality.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +static const size_t kMaxLiteralHistograms = 100; +static const size_t kMaxCommandHistograms = 50; +static const double kLiteralBlockSwitchCost = 28.1; +static const double kCommandBlockSwitchCost = 13.5; +static const double kDistanceBlockSwitchCost = 14.6; +static const size_t kLiteralStrideLength = 70; +static const size_t kCommandStrideLength = 40; +static const size_t kDistanceStrideLength = 40; +static const size_t kSymbolsPerLiteralHistogram = 544; +static const size_t kSymbolsPerCommandHistogram = 530; +static const size_t kSymbolsPerDistanceHistogram = 544; +static const size_t kMinLengthForBlockSplitting = 128; +static const size_t kIterMulForRefining = 2; +static const size_t kMinItersForRefining = 100; + +static size_t CountLiterals(const Command* cmds, const size_t num_commands) { + /* Count how many we have. */ + size_t total_length = 0; + size_t i; + for (i = 0; i < num_commands; ++i) { + total_length += cmds[i].insert_len_; + } + return total_length; +} + +static void CopyLiteralsToByteArray(const Command* cmds, + const size_t num_commands, + const uint8_t* data, + const size_t offset, + const size_t mask, + uint8_t* literals) { + size_t pos = 0; + size_t from_pos = offset & mask; + size_t i; + for (i = 0; i < num_commands; ++i) { + size_t insert_len = cmds[i].insert_len_; + if (from_pos + insert_len > mask) { + size_t head_size = mask + 1 - from_pos; + memcpy(literals + pos, data + from_pos, head_size); + from_pos = 0; + pos += head_size; + insert_len -= head_size; + } + if (insert_len > 0) { + memcpy(literals + pos, data + from_pos, insert_len); + pos += insert_len; + } + from_pos = (from_pos + insert_len + CommandCopyLen(&cmds[i])) & mask; + } +} + +static BROTLI_INLINE uint32_t MyRand(uint32_t* seed) { + /* Initial seed should be 7. In this case, loop length is (1 << 29). */ + *seed *= 16807U; + return *seed; +} + +static BROTLI_INLINE double BitCost(size_t count) { + return count == 0 ? -2.0 : FastLog2(count); +} + +#define HISTOGRAMS_PER_BATCH 64 +#define CLUSTERS_PER_BATCH 16 + +#define FN(X) X ## Literal +#define DataType uint8_t +/* NOLINTNEXTLINE(build/include) */ +#include "block_splitter_inc.h" +#undef DataType +#undef FN + +#define FN(X) X ## Command +#define DataType uint16_t +/* NOLINTNEXTLINE(build/include) */ +#include "block_splitter_inc.h" +#undef FN + +#define FN(X) X ## Distance +/* NOLINTNEXTLINE(build/include) */ +#include "block_splitter_inc.h" +#undef DataType +#undef FN + +void BrotliInitBlockSplit(BlockSplit* self) { + self->num_types = 0; + self->num_blocks = 0; + self->types = 0; + self->lengths = 0; + self->types_alloc_size = 0; + self->lengths_alloc_size = 0; +} + +void BrotliDestroyBlockSplit(MemoryManager* m, BlockSplit* self) { + BROTLI_FREE(m, self->types); + BROTLI_FREE(m, self->lengths); +} + +/* Extracts literals, command distance and prefix codes, then applies + * SplitByteVector to create partitioning. */ +void BrotliSplitBlock(MemoryManager* m, + const Command* cmds, + const size_t num_commands, + const uint8_t* data, + const size_t pos, + const size_t mask, + const BrotliEncoderParams* params, + BlockSplit* literal_split, + BlockSplit* insert_and_copy_split, + BlockSplit* dist_split) { + { + size_t literals_count = CountLiterals(cmds, num_commands); + uint8_t* literals = BROTLI_ALLOC(m, uint8_t, literals_count); + if (BROTLI_IS_OOM(m) || BROTLI_IS_NULL(literals)) return; + /* Create a continuous array of literals. */ + CopyLiteralsToByteArray(cmds, num_commands, data, pos, mask, literals); + /* Create the block split on the array of literals. + * Literal histograms can have alphabet size up to 256. + * Though, to accommodate context modeling, less than half of maximum size + * is allowed. */ + SplitByteVectorLiteral( + m, literals, literals_count, + kSymbolsPerLiteralHistogram, kMaxLiteralHistograms, + kLiteralStrideLength, kLiteralBlockSwitchCost, params, + literal_split); + if (BROTLI_IS_OOM(m)) return; + BROTLI_FREE(m, literals); + /* NB: this might be a good place for injecting extra splitting without + * increasing encoder complexity; however, output partition would be less + * optimal than one produced with forced splitting inside + * SplitByteVector (FindBlocks / ClusterBlocks). */ + } + + { + /* Compute prefix codes for commands. */ + uint16_t* insert_and_copy_codes = BROTLI_ALLOC(m, uint16_t, num_commands); + size_t i; + if (BROTLI_IS_OOM(m) || BROTLI_IS_NULL(insert_and_copy_codes)) return; + for (i = 0; i < num_commands; ++i) { + insert_and_copy_codes[i] = cmds[i].cmd_prefix_; + } + /* Create the block split on the array of command prefixes. */ + SplitByteVectorCommand( + m, insert_and_copy_codes, num_commands, + kSymbolsPerCommandHistogram, kMaxCommandHistograms, + kCommandStrideLength, kCommandBlockSwitchCost, params, + insert_and_copy_split); + if (BROTLI_IS_OOM(m)) return; + /* TODO(eustas): reuse for distances? */ + BROTLI_FREE(m, insert_and_copy_codes); + } + + { + /* Create a continuous array of distance prefixes. */ + uint16_t* distance_prefixes = BROTLI_ALLOC(m, uint16_t, num_commands); + size_t j = 0; + size_t i; + if (BROTLI_IS_OOM(m) || BROTLI_IS_NULL(distance_prefixes)) return; + for (i = 0; i < num_commands; ++i) { + const Command* cmd = &cmds[i]; + if (CommandCopyLen(cmd) && cmd->cmd_prefix_ >= 128) { + distance_prefixes[j++] = cmd->dist_prefix_ & 0x3FF; + } + } + /* Create the block split on the array of distance prefixes. */ + SplitByteVectorDistance( + m, distance_prefixes, j, + kSymbolsPerDistanceHistogram, kMaxCommandHistograms, + kDistanceStrideLength, kDistanceBlockSwitchCost, params, + dist_split); + if (BROTLI_IS_OOM(m)) return; + BROTLI_FREE(m, distance_prefixes); + } +} + +#if defined(BROTLI_TEST) +size_t BrotliCountLiteralsForTest(const Command*, size_t); +size_t BrotliCountLiteralsForTest(const Command* cmds, size_t num_commands) { + return CountLiterals(cmds, num_commands); +} +void BrotliCopyLiteralsToByteArrayForTest( + const Command*, size_t, const uint8_t*, size_t, size_t, uint8_t*); +void BrotliCopyLiteralsToByteArrayForTest(const Command* cmds, + size_t num_commands, const uint8_t* data, size_t offset, size_t mask, + uint8_t* literals) { + CopyLiteralsToByteArray(cmds, num_commands, data, offset, mask, literals); +} +#endif + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif diff --git a/packages/runtime/vendor/brotli/c/enc/block_splitter.h b/packages/runtime/vendor/brotli/c/enc/block_splitter.h new file mode 100644 index 000000000..c927794eb --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/block_splitter.h @@ -0,0 +1,49 @@ +/* Copyright 2013 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* Block split point selection utilities. */ + +#ifndef BROTLI_ENC_BLOCK_SPLITTER_H_ +#define BROTLI_ENC_BLOCK_SPLITTER_H_ + +#include "../common/platform.h" +#include "command.h" +#include "memory.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +typedef struct BlockSplit { + size_t num_types; /* Amount of distinct types */ + size_t num_blocks; /* Amount of values in types and length */ + uint8_t* types; + uint32_t* lengths; + + size_t types_alloc_size; + size_t lengths_alloc_size; +} BlockSplit; + +BROTLI_INTERNAL void BrotliInitBlockSplit(BlockSplit* self); +BROTLI_INTERNAL void BrotliDestroyBlockSplit(MemoryManager* m, + BlockSplit* self); + +BROTLI_INTERNAL void BrotliSplitBlock(MemoryManager* m, + const Command* cmds, + const size_t num_commands, + const uint8_t* data, + const size_t offset, + const size_t mask, + const BrotliEncoderParams* params, + BlockSplit* literal_split, + BlockSplit* insert_and_copy_split, + BlockSplit* dist_split); + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif + +#endif /* BROTLI_ENC_BLOCK_SPLITTER_H_ */ diff --git a/packages/runtime/vendor/brotli/c/enc/block_splitter_inc.h b/packages/runtime/vendor/brotli/c/enc/block_splitter_inc.h new file mode 100644 index 000000000..5e34cb9c9 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/block_splitter_inc.h @@ -0,0 +1,487 @@ +/* NOLINT(build/header_guard) */ +/* Copyright 2013 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* template parameters: FN, DataType */ + +#define HistogramType FN(Histogram) + +static void FN(InitialEntropyCodes)(const DataType* data, size_t length, + size_t stride, + size_t num_histograms, + HistogramType* histograms) { + uint32_t seed = 7; + size_t block_length = length / num_histograms; + size_t i; + FN(ClearHistograms)(histograms, num_histograms); + for (i = 0; i < num_histograms; ++i) { + size_t pos = length * i / num_histograms; + if (i != 0) { + pos += MyRand(&seed) % block_length; + } + if (pos + stride >= length) { + pos = length - stride - 1; + } + FN(HistogramAddVector)(&histograms[i], data + pos, stride); + } +} + +static void FN(RandomSample)(uint32_t* seed, + const DataType* data, + size_t length, + size_t stride, + HistogramType* sample) { + size_t pos = 0; + if (stride >= length) { + stride = length; + } else { + pos = MyRand(seed) % (length - stride + 1); + } + FN(HistogramAddVector)(sample, data + pos, stride); +} + +static void FN(RefineEntropyCodes)(const DataType* data, size_t length, + size_t stride, + size_t num_histograms, + HistogramType* histograms, + HistogramType* tmp) { + size_t iters = + kIterMulForRefining * length / stride + kMinItersForRefining; + uint32_t seed = 7; + size_t iter; + iters = ((iters + num_histograms - 1) / num_histograms) * num_histograms; + for (iter = 0; iter < iters; ++iter) { + FN(HistogramClear)(tmp); + FN(RandomSample)(&seed, data, length, stride, tmp); + FN(HistogramAddHistogram)(&histograms[iter % num_histograms], tmp); + } +} + +/* Assigns a block id from the range [0, num_histograms) to each data element + in data[0..length) and fills in block_id[0..length) with the assigned values. + Returns the number of blocks, i.e. one plus the number of block switches. */ +static size_t FN(FindBlocks)(const DataType* data, const size_t length, + const double block_switch_bitcost, + const size_t num_histograms, + const HistogramType* histograms, + double* insert_cost, + double* cost, + uint8_t* switch_signal, + uint8_t* block_id) { + const size_t alphabet_size = FN(HistogramDataSize)(); + const size_t bitmap_len = (num_histograms + 7) >> 3; + size_t num_blocks = 1; + size_t byte_ix; + size_t i; + size_t j; + BROTLI_DCHECK(num_histograms <= 256); + + /* Trivial case: single histogram -> single block type. */ + if (num_histograms <= 1) { + for (i = 0; i < length; ++i) { + block_id[i] = 0; + } + return 1; + } + + /* Fill bitcost for each symbol of all histograms. + * Non-existing symbol cost: 2 + log2(total_count). + * Regular symbol cost: -log2(symbol_count / total_count). */ + memset(insert_cost, 0, + sizeof(insert_cost[0]) * alphabet_size * num_histograms); + for (i = 0; i < num_histograms; ++i) { + insert_cost[i] = FastLog2((uint32_t)histograms[i].total_count_); + } + for (i = alphabet_size; i != 0;) { + /* Reverse order to use the 0-th row as a temporary storage. */ + --i; + for (j = 0; j < num_histograms; ++j) { + insert_cost[i * num_histograms + j] = + insert_cost[j] - BitCost(histograms[j].data_[i]); + } + } + + /* After each iteration of this loop, cost[k] will contain the difference + between the minimum cost of arriving at the current byte position using + entropy code k, and the minimum cost of arriving at the current byte + position. This difference is capped at the block switch cost, and if it + reaches block switch cost, it means that when we trace back from the last + position, we need to switch here. */ + memset(cost, 0, sizeof(cost[0]) * num_histograms); + memset(switch_signal, 0, sizeof(switch_signal[0]) * length * bitmap_len); + for (byte_ix = 0; byte_ix < length; ++byte_ix) { + size_t ix = byte_ix * bitmap_len; + size_t symbol = data[byte_ix]; + size_t insert_cost_ix = symbol * num_histograms; + double min_cost = 1e99; + double block_switch_cost = block_switch_bitcost; + static const size_t prologue_length = 2000; + static const double multiplier = 0.07 / 2000; + size_t k; + for (k = 0; k < num_histograms; ++k) { + /* We are coding the symbol with entropy code k. */ + cost[k] += insert_cost[insert_cost_ix + k]; + if (cost[k] < min_cost) { + min_cost = cost[k]; + block_id[byte_ix] = (uint8_t)k; + } + } + /* More blocks for the beginning. */ + if (byte_ix < prologue_length) { + block_switch_cost *= 0.77 + multiplier * (double)byte_ix; + } + for (k = 0; k < num_histograms; ++k) { + cost[k] -= min_cost; + if (cost[k] >= block_switch_cost) { + const uint8_t mask = (uint8_t)(1u << (k & 7)); + cost[k] = block_switch_cost; + BROTLI_DCHECK((k >> 3) < bitmap_len); + switch_signal[ix + (k >> 3)] |= mask; + } + } + } + + byte_ix = length - 1; + { /* Trace back from the last position and switch at the marked places. */ + size_t ix = byte_ix * bitmap_len; + uint8_t cur_id = block_id[byte_ix]; + while (byte_ix > 0) { + const uint8_t mask = (uint8_t)(1u << (cur_id & 7)); + BROTLI_DCHECK(((size_t)cur_id >> 3) < bitmap_len); + --byte_ix; + ix -= bitmap_len; + if (switch_signal[ix + (cur_id >> 3)] & mask) { + if (cur_id != block_id[byte_ix]) { + cur_id = block_id[byte_ix]; + ++num_blocks; + } + } + block_id[byte_ix] = cur_id; + } + } + return num_blocks; +} + +static size_t FN(RemapBlockIds)(uint8_t* block_ids, const size_t length, + uint16_t* new_id, const size_t num_histograms) { + static const uint16_t kInvalidId = 256; + uint16_t next_id = 0; + size_t i; + for (i = 0; i < num_histograms; ++i) { + new_id[i] = kInvalidId; + } + for (i = 0; i < length; ++i) { + BROTLI_DCHECK(block_ids[i] < num_histograms); + if (new_id[block_ids[i]] == kInvalidId) { + new_id[block_ids[i]] = next_id++; + } + } + for (i = 0; i < length; ++i) { + block_ids[i] = (uint8_t)new_id[block_ids[i]]; + BROTLI_DCHECK(block_ids[i] < num_histograms); + } + BROTLI_DCHECK(next_id <= num_histograms); + return next_id; +} + +static void FN(BuildBlockHistograms)(const DataType* data, const size_t length, + const uint8_t* block_ids, + const size_t num_histograms, + HistogramType* histograms) { + size_t i; + FN(ClearHistograms)(histograms, num_histograms); + for (i = 0; i < length; ++i) { + FN(HistogramAdd)(&histograms[block_ids[i]], data[i]); + } +} + +/* Given the initial partitioning build partitioning with limited number + * of histograms (and block types). */ +static void FN(ClusterBlocks)(MemoryManager* m, + const DataType* data, const size_t length, + const size_t num_blocks, + uint8_t* block_ids, + BlockSplit* split) { + uint32_t* histogram_symbols = BROTLI_ALLOC(m, uint32_t, num_blocks); + uint32_t* u32 = + BROTLI_ALLOC(m, uint32_t, num_blocks + 4 * HISTOGRAMS_PER_BATCH); + const size_t expected_num_clusters = CLUSTERS_PER_BATCH * + (num_blocks + HISTOGRAMS_PER_BATCH - 1) / HISTOGRAMS_PER_BATCH; + size_t all_histograms_size = 0; + size_t all_histograms_capacity = expected_num_clusters; + HistogramType* all_histograms = + BROTLI_ALLOC(m, HistogramType, all_histograms_capacity); + size_t cluster_size_size = 0; + size_t cluster_size_capacity = expected_num_clusters; + uint32_t* cluster_size = BROTLI_ALLOC(m, uint32_t, cluster_size_capacity); + size_t num_clusters = 0; + HistogramType* histograms = BROTLI_ALLOC(m, HistogramType, + BROTLI_MIN(size_t, num_blocks, HISTOGRAMS_PER_BATCH)); + size_t max_num_pairs = + HISTOGRAMS_PER_BATCH * HISTOGRAMS_PER_BATCH / 2; + size_t pairs_capacity = max_num_pairs + 1; + HistogramPair* pairs = BROTLI_ALLOC(m, HistogramPair, pairs_capacity); + size_t pos = 0; + uint32_t* clusters; + size_t num_final_clusters; + static const uint32_t kInvalidIndex = BROTLI_UINT32_MAX; + uint32_t* new_index; + size_t i; + uint32_t* BROTLI_RESTRICT const sizes = + u32 ? (u32 + 0 * HISTOGRAMS_PER_BATCH) : NULL; + uint32_t* BROTLI_RESTRICT const new_clusters = + u32 ? (u32 + 1 * HISTOGRAMS_PER_BATCH) : NULL; + uint32_t* BROTLI_RESTRICT const symbols = + u32 ? (u32 + 2 * HISTOGRAMS_PER_BATCH) : NULL; + uint32_t* BROTLI_RESTRICT const remap = + u32 ? (u32 + 3 * HISTOGRAMS_PER_BATCH) : NULL; + uint32_t* BROTLI_RESTRICT const block_lengths = + u32 ? (u32 + 4 * HISTOGRAMS_PER_BATCH) : NULL; + /* TODO(eustas): move to arena? */ + HistogramType* tmp = BROTLI_ALLOC(m, HistogramType, 2); + + if (BROTLI_IS_OOM(m) || BROTLI_IS_NULL(histogram_symbols) || + BROTLI_IS_NULL(u32) || BROTLI_IS_NULL(all_histograms) || + BROTLI_IS_NULL(cluster_size) || BROTLI_IS_NULL(histograms) || + BROTLI_IS_NULL(pairs) || BROTLI_IS_NULL(tmp)) { + return; + } + + memset(u32, 0, (num_blocks + 4 * HISTOGRAMS_PER_BATCH) * sizeof(uint32_t)); + + /* Calculate block lengths (convert repeating values -> series length). */ + { + size_t block_idx = 0; + for (i = 0; i < length; ++i) { + BROTLI_DCHECK(block_idx < num_blocks); + ++block_lengths[block_idx]; + if (i + 1 == length || block_ids[i] != block_ids[i + 1]) { + ++block_idx; + } + } + BROTLI_DCHECK(block_idx == num_blocks); + } + + /* Pre-cluster blocks (cluster batches). */ + for (i = 0; i < num_blocks; i += HISTOGRAMS_PER_BATCH) { + const size_t num_to_combine = + BROTLI_MIN(size_t, num_blocks - i, HISTOGRAMS_PER_BATCH); + size_t num_new_clusters; + size_t j; + for (j = 0; j < num_to_combine; ++j) { + size_t k; + size_t block_length = block_lengths[i + j]; + FN(HistogramClear)(&histograms[j]); + for (k = 0; k < block_length; ++k) { + FN(HistogramAdd)(&histograms[j], data[pos++]); + } + histograms[j].bit_cost_ = FN(BrotliPopulationCost)(&histograms[j]); + new_clusters[j] = (uint32_t)j; + symbols[j] = (uint32_t)j; + sizes[j] = 1; + } + num_new_clusters = FN(BrotliHistogramCombine)( + histograms, tmp, sizes, symbols, new_clusters, pairs, num_to_combine, + num_to_combine, HISTOGRAMS_PER_BATCH, max_num_pairs); + BROTLI_ENSURE_CAPACITY(m, HistogramType, all_histograms, + all_histograms_capacity, all_histograms_size + num_new_clusters); + BROTLI_ENSURE_CAPACITY(m, uint32_t, cluster_size, + cluster_size_capacity, cluster_size_size + num_new_clusters); + if (BROTLI_IS_OOM(m)) return; + for (j = 0; j < num_new_clusters; ++j) { + all_histograms[all_histograms_size++] = histograms[new_clusters[j]]; + cluster_size[cluster_size_size++] = sizes[new_clusters[j]]; + remap[new_clusters[j]] = (uint32_t)j; + } + for (j = 0; j < num_to_combine; ++j) { + histogram_symbols[i + j] = (uint32_t)num_clusters + remap[symbols[j]]; + } + num_clusters += num_new_clusters; + BROTLI_DCHECK(num_clusters == cluster_size_size); + BROTLI_DCHECK(num_clusters == all_histograms_size); + } + BROTLI_FREE(m, histograms); + + /* Final clustering. */ + max_num_pairs = + BROTLI_MIN(size_t, 64 * num_clusters, (num_clusters / 2) * num_clusters); + if (pairs_capacity < max_num_pairs + 1) { + BROTLI_FREE(m, pairs); + pairs = BROTLI_ALLOC(m, HistogramPair, max_num_pairs + 1); + if (BROTLI_IS_OOM(m) || BROTLI_IS_NULL(pairs)) return; + } + clusters = BROTLI_ALLOC(m, uint32_t, num_clusters); + if (BROTLI_IS_OOM(m) || BROTLI_IS_NULL(clusters)) return; + for (i = 0; i < num_clusters; ++i) { + clusters[i] = (uint32_t)i; + } + num_final_clusters = FN(BrotliHistogramCombine)( + all_histograms, tmp, cluster_size, histogram_symbols, clusters, pairs, + num_clusters, num_blocks, BROTLI_MAX_NUMBER_OF_BLOCK_TYPES, + max_num_pairs); + BROTLI_FREE(m, pairs); + BROTLI_FREE(m, cluster_size); + + /* Assign blocks to final histograms. */ + new_index = BROTLI_ALLOC(m, uint32_t, num_clusters); + if (BROTLI_IS_OOM(m) || BROTLI_IS_NULL(new_index)) return; + for (i = 0; i < num_clusters; ++i) new_index[i] = kInvalidIndex; + pos = 0; + { + uint32_t next_index = 0; + for (i = 0; i < num_blocks; ++i) { + size_t j; + uint32_t best_out; + double best_bits; + FN(HistogramClear)(tmp); + for (j = 0; j < block_lengths[i]; ++j) { + FN(HistogramAdd)(tmp, data[pos++]); + } + /* Among equally good histograms prefer last used. */ + /* TODO(eustas): should we give a block-switch discount here? */ + best_out = (i == 0) ? histogram_symbols[0] : histogram_symbols[i - 1]; + best_bits = FN(BrotliHistogramBitCostDistance)( + tmp, &all_histograms[best_out], tmp + 1); + for (j = 0; j < num_final_clusters; ++j) { + const double cur_bits = FN(BrotliHistogramBitCostDistance)( + tmp, &all_histograms[clusters[j]], tmp + 1); + if (cur_bits < best_bits) { + best_bits = cur_bits; + best_out = clusters[j]; + } + } + histogram_symbols[i] = best_out; + if (new_index[best_out] == kInvalidIndex) { + new_index[best_out] = next_index++; + } + } + } + BROTLI_FREE(m, tmp); + BROTLI_FREE(m, clusters); + BROTLI_FREE(m, all_histograms); + BROTLI_ENSURE_CAPACITY( + m, uint8_t, split->types, split->types_alloc_size, num_blocks); + BROTLI_ENSURE_CAPACITY( + m, uint32_t, split->lengths, split->lengths_alloc_size, num_blocks); + if (BROTLI_IS_OOM(m)) return; + + /* Rewrite final assignment to block-split. There might be less blocks + * than |num_blocks| due to clustering. */ + { + uint32_t cur_length = 0; + size_t block_idx = 0; + uint8_t max_type = 0; + for (i = 0; i < num_blocks; ++i) { + cur_length += block_lengths[i]; + if (i + 1 == num_blocks || + histogram_symbols[i] != histogram_symbols[i + 1]) { + const uint8_t id = (uint8_t)new_index[histogram_symbols[i]]; + split->types[block_idx] = id; + split->lengths[block_idx] = cur_length; + max_type = BROTLI_MAX(uint8_t, max_type, id); + cur_length = 0; + ++block_idx; + } + } + split->num_blocks = block_idx; + split->num_types = (size_t)max_type + 1; + } + BROTLI_FREE(m, new_index); + BROTLI_FREE(m, u32); + BROTLI_FREE(m, histogram_symbols); +} + +/* Create BlockSplit (partitioning) given the limits, estimates and "effort" + * parameters. + * + * NB: max_histograms is often less than number of histograms allowed by format; + * this is done intentionally, to save some "space" for context-aware + * clustering (here entropy is estimated for context-free symbols). */ +static void FN(SplitByteVector)(MemoryManager* m, + const DataType* data, const size_t length, + const size_t symbols_per_histogram, + const size_t max_histograms, + const size_t sampling_stride_length, + const double block_switch_cost, + const BrotliEncoderParams* params, + BlockSplit* split) { + const size_t data_size = FN(HistogramDataSize)(); + HistogramType* histograms; + HistogramType* tmp; + /* Calculate number of histograms; initial estimate is one histogram per + * specified amount of symbols; however, this value is capped. */ + size_t num_histograms = length / symbols_per_histogram + 1; + if (num_histograms > max_histograms) { + num_histograms = max_histograms; + } + + /* Corner case: no input. */ + if (length == 0) { + split->num_types = 1; + return; + } + + if (length < kMinLengthForBlockSplitting) { + BROTLI_ENSURE_CAPACITY(m, uint8_t, + split->types, split->types_alloc_size, split->num_blocks + 1); + BROTLI_ENSURE_CAPACITY(m, uint32_t, + split->lengths, split->lengths_alloc_size, split->num_blocks + 1); + if (BROTLI_IS_OOM(m)) return; + split->num_types = 1; + split->types[split->num_blocks] = 0; + split->lengths[split->num_blocks] = (uint32_t)length; + split->num_blocks++; + return; + } + histograms = BROTLI_ALLOC(m, HistogramType, num_histograms + 1); + tmp = histograms + num_histograms; + if (BROTLI_IS_OOM(m) || BROTLI_IS_NULL(histograms)) return; + /* Find good entropy codes. */ + FN(InitialEntropyCodes)(data, length, + sampling_stride_length, + num_histograms, histograms); + FN(RefineEntropyCodes)(data, length, + sampling_stride_length, + num_histograms, histograms, tmp); + { + /* Find a good path through literals with the good entropy codes. */ + uint8_t* block_ids = BROTLI_ALLOC(m, uint8_t, length); + size_t num_blocks = 0; + const size_t bitmaplen = (num_histograms + 7) >> 3; + double* insert_cost = BROTLI_ALLOC(m, double, data_size * num_histograms); + double* cost = BROTLI_ALLOC(m, double, num_histograms); + uint8_t* switch_signal = BROTLI_ALLOC(m, uint8_t, length * bitmaplen); + uint16_t* new_id = BROTLI_ALLOC(m, uint16_t, num_histograms); + const size_t iters = params->quality < HQ_ZOPFLIFICATION_QUALITY ? 3 : 10; + size_t i; + if (BROTLI_IS_OOM(m) || BROTLI_IS_NULL(block_ids) || + BROTLI_IS_NULL(insert_cost) || BROTLI_IS_NULL(cost) || + BROTLI_IS_NULL(switch_signal) || BROTLI_IS_NULL(new_id)) { + return; + } + for (i = 0; i < iters; ++i) { + num_blocks = FN(FindBlocks)(data, length, + block_switch_cost, + num_histograms, histograms, + insert_cost, cost, switch_signal, + block_ids); + num_histograms = FN(RemapBlockIds)(block_ids, length, + new_id, num_histograms); + FN(BuildBlockHistograms)(data, length, block_ids, + num_histograms, histograms); + } + BROTLI_FREE(m, insert_cost); + BROTLI_FREE(m, cost); + BROTLI_FREE(m, switch_signal); + BROTLI_FREE(m, new_id); + BROTLI_FREE(m, histograms); + FN(ClusterBlocks)(m, data, length, num_blocks, block_ids, split); + if (BROTLI_IS_OOM(m)) return; + BROTLI_FREE(m, block_ids); + } +} + +#undef HistogramType diff --git a/packages/runtime/vendor/brotli/c/enc/brotli_bit_stream.c b/packages/runtime/vendor/brotli/c/enc/brotli_bit_stream.c new file mode 100644 index 000000000..1e9e946fe --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/brotli_bit_stream.c @@ -0,0 +1,1337 @@ +/* Copyright 2014 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* Brotli bit stream functions to support the low level format. There are no + compression algorithms here, just the right ordering of bits to match the + specs. */ + +#include "brotli_bit_stream.h" + +#include "../common/constants.h" +#include "../common/context.h" +#include "../common/platform.h" +#include "entropy_encode.h" +#include "entropy_encode_static.h" +#include "fast_log.h" +#include "histogram.h" +#include "memory.h" +#include "write_bits.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +#define MAX_HUFFMAN_TREE_SIZE (2 * BROTLI_NUM_COMMAND_SYMBOLS + 1) +/* The maximum size of Huffman dictionary for distances assuming that + NPOSTFIX = 0 and NDIRECT = 0. */ +#define MAX_SIMPLE_DISTANCE_ALPHABET_SIZE \ + BROTLI_DISTANCE_ALPHABET_SIZE(0, 0, BROTLI_LARGE_MAX_DISTANCE_BITS) +/* MAX_SIMPLE_DISTANCE_ALPHABET_SIZE == 140 */ + +static BROTLI_INLINE uint32_t BlockLengthPrefixCode(uint32_t len) { + uint32_t code = (len >= 177) ? (len >= 753 ? 20 : 14) : (len >= 41 ? 7 : 0); + while (code < (BROTLI_NUM_BLOCK_LEN_SYMBOLS - 1) && + len >= _kBrotliPrefixCodeRanges[code + 1].offset) ++code; + return code; +} + +static BROTLI_INLINE void GetBlockLengthPrefixCode(uint32_t len, size_t* code, + uint32_t* n_extra, uint32_t* extra) { + *code = BlockLengthPrefixCode(len); + *n_extra = _kBrotliPrefixCodeRanges[*code].nbits; + *extra = len - _kBrotliPrefixCodeRanges[*code].offset; +} + +typedef struct BlockTypeCodeCalculator { + size_t last_type; + size_t second_last_type; +} BlockTypeCodeCalculator; + +static void InitBlockTypeCodeCalculator(BlockTypeCodeCalculator* self) { + self->last_type = 1; + self->second_last_type = 0; +} + +static BROTLI_INLINE size_t NextBlockTypeCode( + BlockTypeCodeCalculator* calculator, uint8_t type) { + size_t type_code = (type == calculator->last_type + 1) ? 1u : + (type == calculator->second_last_type) ? 0u : type + 2u; + calculator->second_last_type = calculator->last_type; + calculator->last_type = type; + return type_code; +} + +/* |nibblesbits| represents the 2 bits to encode MNIBBLES (0-3) + REQUIRES: length > 0 + REQUIRES: length <= (1 << 24) */ +static void BrotliEncodeMlen(size_t length, uint64_t* bits, + size_t* numbits, uint64_t* nibblesbits) { + size_t lg = (length == 1) ? 1 : Log2FloorNonZero((uint32_t)(length - 1)) + 1; + size_t mnibbles = (lg < 16 ? 16 : (lg + 3)) / 4; + BROTLI_DCHECK(length > 0); + BROTLI_DCHECK(length <= (1 << 24)); + BROTLI_DCHECK(lg <= 24); + *nibblesbits = mnibbles - 4; + *numbits = mnibbles * 4; + *bits = length - 1; +} + +static BROTLI_INLINE void StoreCommandExtra( + const Command* cmd, size_t* storage_ix, uint8_t* storage) { + uint32_t copylen_code = CommandCopyLenCode(cmd); + uint16_t inscode = GetInsertLengthCode(cmd->insert_len_); + uint16_t copycode = GetCopyLengthCode(copylen_code); + uint32_t insnumextra = GetInsertExtra(inscode); + uint64_t insextraval = cmd->insert_len_ - GetInsertBase(inscode); + uint64_t copyextraval = copylen_code - GetCopyBase(copycode); + uint64_t bits = (copyextraval << insnumextra) | insextraval; + BrotliWriteBits( + insnumextra + GetCopyExtra(copycode), bits, storage_ix, storage); +} + +/* Data structure that stores almost everything that is needed to encode each + block switch command. */ +typedef struct BlockSplitCode { + BlockTypeCodeCalculator type_code_calculator; + uint8_t type_depths[BROTLI_MAX_BLOCK_TYPE_SYMBOLS]; + uint16_t type_bits[BROTLI_MAX_BLOCK_TYPE_SYMBOLS]; + uint8_t length_depths[BROTLI_NUM_BLOCK_LEN_SYMBOLS]; + uint16_t length_bits[BROTLI_NUM_BLOCK_LEN_SYMBOLS]; +} BlockSplitCode; + +/* Stores a number between 0 and 255. */ +static void StoreVarLenUint8(size_t n, size_t* storage_ix, uint8_t* storage) { + if (n == 0) { + BrotliWriteBits(1, 0, storage_ix, storage); + } else { + size_t nbits = Log2FloorNonZero(n); + BrotliWriteBits(1, 1, storage_ix, storage); + BrotliWriteBits(3, nbits, storage_ix, storage); + BrotliWriteBits(nbits, n - ((size_t)1 << nbits), storage_ix, storage); + } +} + +/* Stores the compressed meta-block header. + REQUIRES: length > 0 + REQUIRES: length <= (1 << 24) */ +static void StoreCompressedMetaBlockHeader(BROTLI_BOOL is_final_block, + size_t length, + size_t* storage_ix, + uint8_t* storage) { + uint64_t lenbits; + size_t nlenbits; + uint64_t nibblesbits; + + /* Write ISLAST bit. */ + BrotliWriteBits(1, (uint64_t)is_final_block, storage_ix, storage); + /* Write ISEMPTY bit. */ + if (is_final_block) { + BrotliWriteBits(1, 0, storage_ix, storage); + } + + BrotliEncodeMlen(length, &lenbits, &nlenbits, &nibblesbits); + BrotliWriteBits(2, nibblesbits, storage_ix, storage); + BrotliWriteBits(nlenbits, lenbits, storage_ix, storage); + + if (!is_final_block) { + /* Write ISUNCOMPRESSED bit. */ + BrotliWriteBits(1, 0, storage_ix, storage); + } +} + +/* Stores the uncompressed meta-block header. + REQUIRES: length > 0 + REQUIRES: length <= (1 << 24) */ +static void BrotliStoreUncompressedMetaBlockHeader(size_t length, + size_t* storage_ix, + uint8_t* storage) { + uint64_t lenbits; + size_t nlenbits; + uint64_t nibblesbits; + + /* Write ISLAST bit. + Uncompressed block cannot be the last one, so set to 0. */ + BrotliWriteBits(1, 0, storage_ix, storage); + BrotliEncodeMlen(length, &lenbits, &nlenbits, &nibblesbits); + BrotliWriteBits(2, nibblesbits, storage_ix, storage); + BrotliWriteBits(nlenbits, lenbits, storage_ix, storage); + /* Write ISUNCOMPRESSED bit. */ + BrotliWriteBits(1, 1, storage_ix, storage); +} + +static void BrotliStoreHuffmanTreeOfHuffmanTreeToBitMask( + const int num_codes, const uint8_t* code_length_bitdepth, + size_t* storage_ix, uint8_t* storage) { + static const BROTLI_MODEL("small") + uint8_t kStorageOrder[BROTLI_CODE_LENGTH_CODES] = { + 1, 2, 3, 4, 0, 5, 17, 6, 16, 7, 8, 9, 10, 11, 12, 13, 14, 15 + }; + /* The bit lengths of the Huffman code over the code length alphabet + are compressed with the following static Huffman code: + Symbol Code + ------ ---- + 0 00 + 1 1110 + 2 110 + 3 01 + 4 10 + 5 1111 */ + static const BROTLI_MODEL("small") + uint8_t kHuffmanBitLengthHuffmanCodeSymbols[6] = { + 0, 7, 3, 2, 1, 15 + }; + static const BROTLI_MODEL("small") + uint8_t kHuffmanBitLengthHuffmanCodeBitLengths[6] = { + 2, 4, 3, 2, 2, 4 + }; + + size_t skip_some = 0; /* skips none. */ + + /* Throw away trailing zeros: */ + size_t codes_to_store = BROTLI_CODE_LENGTH_CODES; + if (num_codes > 1) { + for (; codes_to_store > 0; --codes_to_store) { + if (code_length_bitdepth[kStorageOrder[codes_to_store - 1]] != 0) { + break; + } + } + } + if (code_length_bitdepth[kStorageOrder[0]] == 0 && + code_length_bitdepth[kStorageOrder[1]] == 0) { + skip_some = 2; /* skips two. */ + if (code_length_bitdepth[kStorageOrder[2]] == 0) { + skip_some = 3; /* skips three. */ + } + } + BrotliWriteBits(2, skip_some, storage_ix, storage); + { + size_t i; + for (i = skip_some; i < codes_to_store; ++i) { + size_t l = code_length_bitdepth[kStorageOrder[i]]; + BrotliWriteBits(kHuffmanBitLengthHuffmanCodeBitLengths[l], + kHuffmanBitLengthHuffmanCodeSymbols[l], storage_ix, storage); + } + } +} + +static void BrotliStoreHuffmanTreeToBitMask( + const size_t huffman_tree_size, const uint8_t* huffman_tree, + const uint8_t* huffman_tree_extra_bits, const uint8_t* code_length_bitdepth, + const uint16_t* code_length_bitdepth_symbols, + size_t* BROTLI_RESTRICT storage_ix, uint8_t* BROTLI_RESTRICT storage) { + size_t i; + for (i = 0; i < huffman_tree_size; ++i) { + size_t ix = huffman_tree[i]; + BrotliWriteBits(code_length_bitdepth[ix], code_length_bitdepth_symbols[ix], + storage_ix, storage); + /* Extra bits */ + switch (ix) { + case BROTLI_REPEAT_PREVIOUS_CODE_LENGTH: + BrotliWriteBits(2, huffman_tree_extra_bits[i], storage_ix, storage); + break; + case BROTLI_REPEAT_ZERO_CODE_LENGTH: + BrotliWriteBits(3, huffman_tree_extra_bits[i], storage_ix, storage); + break; + } + } +} + +static void StoreSimpleHuffmanTree(const uint8_t* depths, + size_t symbols[4], + size_t num_symbols, + size_t max_bits, + size_t* storage_ix, uint8_t* storage) { + /* value of 1 indicates a simple Huffman code */ + BrotliWriteBits(2, 1, storage_ix, storage); + BrotliWriteBits(2, num_symbols - 1, storage_ix, storage); /* NSYM - 1 */ + + { + /* Sort */ + size_t i; + for (i = 0; i < num_symbols; i++) { + size_t j; + for (j = i + 1; j < num_symbols; j++) { + if (depths[symbols[j]] < depths[symbols[i]]) { + BROTLI_SWAP(size_t, symbols, j, i); + } + } + } + } + + if (num_symbols == 2) { + BrotliWriteBits(max_bits, symbols[0], storage_ix, storage); + BrotliWriteBits(max_bits, symbols[1], storage_ix, storage); + } else if (num_symbols == 3) { + BrotliWriteBits(max_bits, symbols[0], storage_ix, storage); + BrotliWriteBits(max_bits, symbols[1], storage_ix, storage); + BrotliWriteBits(max_bits, symbols[2], storage_ix, storage); + } else { + BrotliWriteBits(max_bits, symbols[0], storage_ix, storage); + BrotliWriteBits(max_bits, symbols[1], storage_ix, storage); + BrotliWriteBits(max_bits, symbols[2], storage_ix, storage); + BrotliWriteBits(max_bits, symbols[3], storage_ix, storage); + /* tree-select */ + BrotliWriteBits(1, depths[symbols[0]] == 1 ? 1 : 0, storage_ix, storage); + } +} + +/* num = alphabet size + depths = symbol depths */ +void BrotliStoreHuffmanTree(const uint8_t* depths, size_t num, + HuffmanTree* tree, + size_t* storage_ix, uint8_t* storage) { + /* Write the Huffman tree into the brotli-representation. + The command alphabet is the largest, so this allocation will fit all + alphabets. */ + /* TODO(eustas): fix me */ + uint8_t huffman_tree[BROTLI_NUM_COMMAND_SYMBOLS]; + uint8_t huffman_tree_extra_bits[BROTLI_NUM_COMMAND_SYMBOLS]; + size_t huffman_tree_size = 0; + uint8_t code_length_bitdepth[BROTLI_CODE_LENGTH_CODES] = { 0 }; + uint16_t code_length_bitdepth_symbols[BROTLI_CODE_LENGTH_CODES]; + uint32_t huffman_tree_histogram[BROTLI_CODE_LENGTH_CODES] = { 0 }; + size_t i; + int num_codes = 0; + size_t code = 0; + + BROTLI_DCHECK(num <= BROTLI_NUM_COMMAND_SYMBOLS); + + BrotliWriteHuffmanTree(depths, num, &huffman_tree_size, huffman_tree, + huffman_tree_extra_bits); + + /* Calculate the statistics of the Huffman tree in brotli-representation. */ + for (i = 0; i < huffman_tree_size; ++i) { + ++huffman_tree_histogram[huffman_tree[i]]; + } + + for (i = 0; i < BROTLI_CODE_LENGTH_CODES; ++i) { + if (huffman_tree_histogram[i]) { + if (num_codes == 0) { + code = i; + num_codes = 1; + } else if (num_codes == 1) { + num_codes = 2; + break; + } + } + } + + /* Calculate another Huffman tree to use for compressing both the + earlier Huffman tree with. */ + BrotliCreateHuffmanTree(huffman_tree_histogram, BROTLI_CODE_LENGTH_CODES, + 5, tree, code_length_bitdepth); + BrotliConvertBitDepthsToSymbols(code_length_bitdepth, + BROTLI_CODE_LENGTH_CODES, + code_length_bitdepth_symbols); + + /* Now, we have all the data, let's start storing it */ + BrotliStoreHuffmanTreeOfHuffmanTreeToBitMask(num_codes, code_length_bitdepth, + storage_ix, storage); + + if (num_codes == 1) { + code_length_bitdepth[code] = 0; + } + + /* Store the real Huffman tree now. */ + BrotliStoreHuffmanTreeToBitMask(huffman_tree_size, + huffman_tree, + huffman_tree_extra_bits, + code_length_bitdepth, + code_length_bitdepth_symbols, + storage_ix, storage); +} + +/* Builds a Huffman tree from histogram[0:length] into depth[0:length] and + bits[0:length] and stores the encoded tree to the bit stream. */ +static void BuildAndStoreHuffmanTree(const uint32_t* histogram, + const size_t histogram_length, + const size_t alphabet_size, + HuffmanTree* tree, + uint8_t* depth, + uint16_t* bits, + size_t* storage_ix, + uint8_t* storage) { + size_t count = 0; + size_t s4[4] = { 0 }; + size_t i; + size_t max_bits = 0; + for (i = 0; i < histogram_length; i++) { + if (histogram[i]) { + if (count < 4) { + s4[count] = i; + } else if (count > 4) { + break; + } + count++; + } + } + + { + size_t max_bits_counter = alphabet_size - 1; + while (max_bits_counter) { + max_bits_counter >>= 1; + ++max_bits; + } + } + + if (count <= 1) { + BrotliWriteBits(4, 1, storage_ix, storage); + BrotliWriteBits(max_bits, s4[0], storage_ix, storage); + depth[s4[0]] = 0; + bits[s4[0]] = 0; + return; + } + + memset(depth, 0, histogram_length * sizeof(depth[0])); + BrotliCreateHuffmanTree(histogram, histogram_length, 15, tree, depth); + BrotliConvertBitDepthsToSymbols(depth, histogram_length, bits); + + if (count <= 4) { + StoreSimpleHuffmanTree(depth, s4, count, max_bits, storage_ix, storage); + } else { + BrotliStoreHuffmanTree(depth, histogram_length, tree, storage_ix, storage); + } +} + +static BROTLI_INLINE BROTLI_BOOL SortHuffmanTree( + const HuffmanTree* v0, const HuffmanTree* v1) { + return TO_BROTLI_BOOL(v0->total_count_ < v1->total_count_); +} + +void BrotliBuildAndStoreHuffmanTreeFast(HuffmanTree* tree, + const uint32_t* histogram, + const size_t histogram_total, + const size_t max_bits, + uint8_t* depth, uint16_t* bits, + size_t* storage_ix, + uint8_t* storage) { + size_t count = 0; + size_t symbols[4] = { 0 }; + size_t length = 0; + size_t total = histogram_total; + while (total != 0) { + if (histogram[length]) { + if (count < 4) { + symbols[count] = length; + } + ++count; + total -= histogram[length]; + } + ++length; + } + + if (count <= 1) { + BrotliWriteBits(4, 1, storage_ix, storage); + BrotliWriteBits(max_bits, symbols[0], storage_ix, storage); + depth[symbols[0]] = 0; + bits[symbols[0]] = 0; + return; + } + + memset(depth, 0, length * sizeof(depth[0])); + { + uint32_t count_limit; + for (count_limit = 1; ; count_limit *= 2) { + HuffmanTree* node = tree; + size_t l; + for (l = length; l != 0;) { + --l; + if (histogram[l]) { + if (BROTLI_PREDICT_TRUE(histogram[l] >= count_limit)) { + InitHuffmanTree(node, histogram[l], -1, (int16_t)l); + } else { + InitHuffmanTree(node, count_limit, -1, (int16_t)l); + } + ++node; + } + } + { + const int n = (int)(node - tree); + HuffmanTree sentinel; + int i = 0; /* Points to the next leaf node. */ + int j = n + 1; /* Points to the next non-leaf node. */ + int k; + + SortHuffmanTreeItems(tree, (size_t)n, SortHuffmanTree); + /* The nodes are: + [0, n): the sorted leaf nodes that we start with. + [n]: we add a sentinel here. + [n + 1, 2n): new parent nodes are added here, starting from + (n+1). These are naturally in ascending order. + [2n]: we add a sentinel at the end as well. + There will be (2n+1) elements at the end. */ + InitHuffmanTree(&sentinel, BROTLI_UINT32_MAX, -1, -1); + *node++ = sentinel; + *node++ = sentinel; + + for (k = n - 1; k > 0; --k) { + int left, right; + if (tree[i].total_count_ <= tree[j].total_count_) { + left = i; + ++i; + } else { + left = j; + ++j; + } + if (tree[i].total_count_ <= tree[j].total_count_) { + right = i; + ++i; + } else { + right = j; + ++j; + } + /* The sentinel node becomes the parent node. */ + node[-1].total_count_ = + tree[left].total_count_ + tree[right].total_count_; + node[-1].index_left_ = (int16_t)left; + node[-1].index_right_or_value_ = (int16_t)right; + /* Add back the last sentinel node. */ + *node++ = sentinel; + } + if (BrotliSetDepth(2 * n - 1, tree, depth, 14)) { + /* We need to pack the Huffman tree in 14 bits. If this was not + successful, add fake entities to the lowest values and retry. */ + break; + } + } + } + } + BrotliConvertBitDepthsToSymbols(depth, length, bits); + if (count <= 4) { + size_t i; + /* value of 1 indicates a simple Huffman code */ + BrotliWriteBits(2, 1, storage_ix, storage); + BrotliWriteBits(2, count - 1, storage_ix, storage); /* NSYM - 1 */ + + /* Sort */ + for (i = 0; i < count; i++) { + size_t j; + for (j = i + 1; j < count; j++) { + if (depth[symbols[j]] < depth[symbols[i]]) { + BROTLI_SWAP(size_t, symbols, j, i); + } + } + } + + if (count == 2) { + BrotliWriteBits(max_bits, symbols[0], storage_ix, storage); + BrotliWriteBits(max_bits, symbols[1], storage_ix, storage); + } else if (count == 3) { + BrotliWriteBits(max_bits, symbols[0], storage_ix, storage); + BrotliWriteBits(max_bits, symbols[1], storage_ix, storage); + BrotliWriteBits(max_bits, symbols[2], storage_ix, storage); + } else { + BrotliWriteBits(max_bits, symbols[0], storage_ix, storage); + BrotliWriteBits(max_bits, symbols[1], storage_ix, storage); + BrotliWriteBits(max_bits, symbols[2], storage_ix, storage); + BrotliWriteBits(max_bits, symbols[3], storage_ix, storage); + /* tree-select */ + BrotliWriteBits(1, depth[symbols[0]] == 1 ? 1 : 0, storage_ix, storage); + } + } else { + uint8_t previous_value = 8; + size_t i; + /* Complex Huffman Tree */ + StoreStaticCodeLengthCode(storage_ix, storage); + + /* Actual RLE coding. */ + for (i = 0; i < length;) { + const uint8_t value = depth[i]; + size_t reps = 1; + size_t k; + for (k = i + 1; k < length && depth[k] == value; ++k) { + ++reps; + } + i += reps; + if (value == 0) { + BrotliWriteBits(kZeroRepsDepth[reps], kZeroRepsBits[reps], + storage_ix, storage); + } else { + if (previous_value != value) { + BrotliWriteBits(kCodeLengthDepth[value], kCodeLengthBits[value], + storage_ix, storage); + --reps; + } + if (reps < 3) { + while (reps != 0) { + reps--; + BrotliWriteBits(kCodeLengthDepth[value], kCodeLengthBits[value], + storage_ix, storage); + } + } else { + reps -= 3; + BrotliWriteBits(kNonZeroRepsDepth[reps], kNonZeroRepsBits[reps], + storage_ix, storage); + } + previous_value = value; + } + } + } +} + +static size_t IndexOf(const uint8_t* v, size_t v_size, uint8_t value) { + size_t i = 0; + for (; i < v_size; ++i) { + if (v[i] == value) return i; + } + return i; +} + +static void MoveToFront(uint8_t* v, size_t index) { + uint8_t value = v[index]; + size_t i; + for (i = index; i != 0; --i) { + v[i] = v[i - 1]; + } + v[0] = value; +} + +static void MoveToFrontTransform(const uint32_t* BROTLI_RESTRICT v_in, + const size_t v_size, + uint32_t* v_out) { + size_t i; + uint8_t mtf[256]; + uint32_t max_value; + if (v_size == 0) { + return; + } + max_value = v_in[0]; + for (i = 1; i < v_size; ++i) { + if (v_in[i] > max_value) max_value = v_in[i]; + } + BROTLI_DCHECK(max_value < 256u); + for (i = 0; i <= max_value; ++i) { + mtf[i] = (uint8_t)i; + } + { + size_t mtf_size = max_value + 1; + for (i = 0; i < v_size; ++i) { + size_t index = IndexOf(mtf, mtf_size, (uint8_t)v_in[i]); + BROTLI_DCHECK(index < mtf_size); + v_out[i] = (uint32_t)index; + MoveToFront(mtf, index); + } + } +} + +/* Finds runs of zeros in v[0..in_size) and replaces them with a prefix code of + the run length plus extra bits (lower 9 bits is the prefix code and the rest + are the extra bits). Non-zero values in v[] are shifted by + *max_length_prefix. Will not create prefix codes bigger than the initial + value of *max_run_length_prefix. The prefix code of run length L is simply + Log2Floor(L) and the number of extra bits is the same as the prefix code. */ +static void RunLengthCodeZeros(const size_t in_size, + uint32_t* BROTLI_RESTRICT v, size_t* BROTLI_RESTRICT out_size, + uint32_t* BROTLI_RESTRICT max_run_length_prefix) { + uint32_t max_reps = 0; + size_t i; + uint32_t max_prefix; + for (i = 0; i < in_size;) { + uint32_t reps = 0; + for (; i < in_size && v[i] != 0; ++i) ; + for (; i < in_size && v[i] == 0; ++i) { + ++reps; + } + max_reps = BROTLI_MAX(uint32_t, reps, max_reps); + } + max_prefix = max_reps > 0 ? Log2FloorNonZero(max_reps) : 0; + max_prefix = BROTLI_MIN(uint32_t, max_prefix, *max_run_length_prefix); + *max_run_length_prefix = max_prefix; + *out_size = 0; + for (i = 0; i < in_size;) { + BROTLI_DCHECK(*out_size <= i); + if (v[i] != 0) { + v[*out_size] = v[i] + *max_run_length_prefix; + ++i; + ++(*out_size); + } else { + uint32_t reps = 1; + size_t k; + for (k = i + 1; k < in_size && v[k] == 0; ++k) { + ++reps; + } + i += reps; + while (reps != 0) { + if (reps < (2u << max_prefix)) { + uint32_t run_length_prefix = Log2FloorNonZero(reps); + const uint32_t extra_bits = reps - (1u << run_length_prefix); + v[*out_size] = run_length_prefix + (extra_bits << 9); + ++(*out_size); + break; + } else { + const uint32_t extra_bits = (1u << max_prefix) - 1u; + v[*out_size] = max_prefix + (extra_bits << 9); + reps -= (2u << max_prefix) - 1u; + ++(*out_size); + } + } + } + } +} + +#define SYMBOL_BITS 9 + +typedef struct EncodeContextMapArena { + uint32_t histogram[BROTLI_MAX_CONTEXT_MAP_SYMBOLS]; + uint8_t depths[BROTLI_MAX_CONTEXT_MAP_SYMBOLS]; + uint16_t bits[BROTLI_MAX_CONTEXT_MAP_SYMBOLS]; +} EncodeContextMapArena; + +static void EncodeContextMap(MemoryManager* m, + EncodeContextMapArena* arena, + const uint32_t* context_map, + size_t context_map_size, + size_t num_clusters, + HuffmanTree* tree, + size_t* storage_ix, uint8_t* storage) { + size_t i; + uint32_t* rle_symbols; + uint32_t max_run_length_prefix = 6; + size_t num_rle_symbols = 0; + uint32_t* BROTLI_RESTRICT const histogram = arena->histogram; + static const uint32_t kSymbolMask = (1u << SYMBOL_BITS) - 1u; + uint8_t* BROTLI_RESTRICT const depths = arena->depths; + uint16_t* BROTLI_RESTRICT const bits = arena->bits; + + StoreVarLenUint8(num_clusters - 1, storage_ix, storage); + + if (num_clusters == 1) { + return; + } + + rle_symbols = BROTLI_ALLOC(m, uint32_t, context_map_size); + if (BROTLI_IS_OOM(m) || BROTLI_IS_NULL(rle_symbols)) return; + MoveToFrontTransform(context_map, context_map_size, rle_symbols); + RunLengthCodeZeros(context_map_size, rle_symbols, + &num_rle_symbols, &max_run_length_prefix); + memset(histogram, 0, sizeof(arena->histogram)); + for (i = 0; i < num_rle_symbols; ++i) { + ++histogram[rle_symbols[i] & kSymbolMask]; + } + { + BROTLI_BOOL use_rle = TO_BROTLI_BOOL(max_run_length_prefix > 0); + BrotliWriteBits(1, (uint64_t)use_rle, storage_ix, storage); + if (use_rle) { + BrotliWriteBits(4, max_run_length_prefix - 1, storage_ix, storage); + } + } + BuildAndStoreHuffmanTree(histogram, num_clusters + max_run_length_prefix, + num_clusters + max_run_length_prefix, + tree, depths, bits, storage_ix, storage); + for (i = 0; i < num_rle_symbols; ++i) { + const uint32_t rle_symbol = rle_symbols[i] & kSymbolMask; + const uint32_t extra_bits_val = rle_symbols[i] >> SYMBOL_BITS; + BrotliWriteBits(depths[rle_symbol], bits[rle_symbol], storage_ix, storage); + if (rle_symbol > 0 && rle_symbol <= max_run_length_prefix) { + BrotliWriteBits(rle_symbol, extra_bits_val, storage_ix, storage); + } + } + BrotliWriteBits(1, 1, storage_ix, storage); /* use move-to-front */ + BROTLI_FREE(m, rle_symbols); +} + +/* Stores the block switch command with index block_ix to the bit stream. */ +static BROTLI_INLINE void StoreBlockSwitch(BlockSplitCode* code, + const uint32_t block_len, + const uint8_t block_type, + BROTLI_BOOL is_first_block, + size_t* storage_ix, + uint8_t* storage) { + size_t typecode = NextBlockTypeCode(&code->type_code_calculator, block_type); + size_t lencode; + uint32_t len_nextra; + uint32_t len_extra; + if (!is_first_block) { + BrotliWriteBits(code->type_depths[typecode], code->type_bits[typecode], + storage_ix, storage); + } + GetBlockLengthPrefixCode(block_len, &lencode, &len_nextra, &len_extra); + + BrotliWriteBits(code->length_depths[lencode], code->length_bits[lencode], + storage_ix, storage); + BrotliWriteBits(len_nextra, len_extra, storage_ix, storage); +} + +/* Builds a BlockSplitCode data structure from the block split given by the + vector of block types and block lengths and stores it to the bit stream. */ +static void BuildAndStoreBlockSplitCode(const uint8_t* types, + const uint32_t* lengths, + const size_t num_blocks, + const size_t num_types, + HuffmanTree* tree, + BlockSplitCode* code, + size_t* storage_ix, + uint8_t* storage) { + uint32_t type_histo[BROTLI_MAX_BLOCK_TYPE_SYMBOLS]; + uint32_t length_histo[BROTLI_NUM_BLOCK_LEN_SYMBOLS]; + size_t i; + BlockTypeCodeCalculator type_code_calculator; + memset(type_histo, 0, (num_types + 2) * sizeof(type_histo[0])); + memset(length_histo, 0, sizeof(length_histo)); + InitBlockTypeCodeCalculator(&type_code_calculator); + for (i = 0; i < num_blocks; ++i) { + size_t type_code = NextBlockTypeCode(&type_code_calculator, types[i]); + if (i != 0) ++type_histo[type_code]; + ++length_histo[BlockLengthPrefixCode(lengths[i])]; + } + StoreVarLenUint8(num_types - 1, storage_ix, storage); + if (num_types > 1) { /* TODO(eustas): else? could StoreBlockSwitch occur? */ + BuildAndStoreHuffmanTree(&type_histo[0], num_types + 2, num_types + 2, tree, + &code->type_depths[0], &code->type_bits[0], + storage_ix, storage); + BuildAndStoreHuffmanTree(&length_histo[0], BROTLI_NUM_BLOCK_LEN_SYMBOLS, + BROTLI_NUM_BLOCK_LEN_SYMBOLS, + tree, &code->length_depths[0], + &code->length_bits[0], storage_ix, storage); + StoreBlockSwitch(code, lengths[0], types[0], 1, storage_ix, storage); + } +} + +/* Stores a context map where the histogram type is always the block type. */ +static void StoreTrivialContextMap(EncodeContextMapArena* arena, + size_t num_types, + size_t context_bits, + HuffmanTree* tree, + size_t* storage_ix, + uint8_t* storage) { + StoreVarLenUint8(num_types - 1, storage_ix, storage); + if (num_types > 1) { + size_t repeat_code = context_bits - 1u; + size_t repeat_bits = (1u << repeat_code) - 1u; + size_t alphabet_size = num_types + repeat_code; + uint32_t* BROTLI_RESTRICT const histogram = arena->histogram; + uint8_t* BROTLI_RESTRICT const depths = arena->depths; + uint16_t* BROTLI_RESTRICT const bits = arena->bits; + size_t i; + memset(histogram, 0, alphabet_size * sizeof(histogram[0])); + /* Write RLEMAX. */ + BrotliWriteBits(1, 1, storage_ix, storage); + BrotliWriteBits(4, repeat_code - 1, storage_ix, storage); + histogram[repeat_code] = (uint32_t)num_types; + histogram[0] = 1; + for (i = context_bits; i < alphabet_size; ++i) { + histogram[i] = 1; + } + BuildAndStoreHuffmanTree(histogram, alphabet_size, alphabet_size, + tree, depths, bits, storage_ix, storage); + for (i = 0; i < num_types; ++i) { + size_t code = (i == 0 ? 0 : i + context_bits - 1); + BrotliWriteBits(depths[code], bits[code], storage_ix, storage); + BrotliWriteBits( + depths[repeat_code], bits[repeat_code], storage_ix, storage); + BrotliWriteBits(repeat_code, repeat_bits, storage_ix, storage); + } + /* Write IMTF (inverse-move-to-front) bit. */ + BrotliWriteBits(1, 1, storage_ix, storage); + } +} + +/* Manages the encoding of one block category (literal, command or distance). */ +typedef struct BlockEncoder { + size_t histogram_length_; + size_t num_block_types_; + const uint8_t* block_types_; /* Not owned. */ + const uint32_t* block_lengths_; /* Not owned. */ + size_t num_blocks_; + BlockSplitCode block_split_code_; + size_t block_ix_; + size_t block_len_; + size_t entropy_ix_; + uint8_t* depths_; + uint16_t* bits_; +} BlockEncoder; + +static void InitBlockEncoder(BlockEncoder* self, size_t histogram_length, + size_t num_block_types, const uint8_t* block_types, + const uint32_t* block_lengths, const size_t num_blocks) { + self->histogram_length_ = histogram_length; + self->num_block_types_ = num_block_types; + self->block_types_ = block_types; + self->block_lengths_ = block_lengths; + self->num_blocks_ = num_blocks; + InitBlockTypeCodeCalculator(&self->block_split_code_.type_code_calculator); + self->block_ix_ = 0; + self->block_len_ = num_blocks == 0 ? 0 : block_lengths[0]; + self->entropy_ix_ = 0; + self->depths_ = 0; + self->bits_ = 0; +} + +static void CleanupBlockEncoder(MemoryManager* m, BlockEncoder* self) { + BROTLI_FREE(m, self->depths_); + BROTLI_FREE(m, self->bits_); +} + +/* Creates entropy codes of block lengths and block types and stores them + to the bit stream. */ +static void BuildAndStoreBlockSwitchEntropyCodes(BlockEncoder* self, + HuffmanTree* tree, size_t* storage_ix, uint8_t* storage) { + BuildAndStoreBlockSplitCode(self->block_types_, self->block_lengths_, + self->num_blocks_, self->num_block_types_, tree, &self->block_split_code_, + storage_ix, storage); +} + +/* Stores the next symbol with the entropy code of the current block type. + Updates the block type and block length at block boundaries. */ +static void StoreSymbol(BlockEncoder* self, size_t symbol, size_t* storage_ix, + uint8_t* storage) { + if (self->block_len_ == 0) { + size_t block_ix = ++self->block_ix_; + uint32_t block_len = self->block_lengths_[block_ix]; + uint8_t block_type = self->block_types_[block_ix]; + self->block_len_ = block_len; + self->entropy_ix_ = block_type * self->histogram_length_; + StoreBlockSwitch(&self->block_split_code_, block_len, block_type, 0, + storage_ix, storage); + } + --self->block_len_; + { + size_t ix = self->entropy_ix_ + symbol; + BrotliWriteBits(self->depths_[ix], self->bits_[ix], storage_ix, storage); + } +} + +/* Stores the next symbol with the entropy code of the current block type and + context value. + Updates the block type and block length at block boundaries. */ +static void StoreSymbolWithContext(BlockEncoder* self, size_t symbol, + size_t context, const uint32_t* context_map, size_t* storage_ix, + uint8_t* storage, const size_t context_bits) { + if (self->block_len_ == 0) { + size_t block_ix = ++self->block_ix_; + uint32_t block_len = self->block_lengths_[block_ix]; + uint8_t block_type = self->block_types_[block_ix]; + self->block_len_ = block_len; + self->entropy_ix_ = (size_t)block_type << context_bits; + StoreBlockSwitch(&self->block_split_code_, block_len, block_type, 0, + storage_ix, storage); + } + --self->block_len_; + { + size_t histo_ix = context_map[self->entropy_ix_ + context]; + size_t ix = histo_ix * self->histogram_length_ + symbol; + BrotliWriteBits(self->depths_[ix], self->bits_[ix], storage_ix, storage); + } +} + +#define FN(X) X ## Literal +/* NOLINTNEXTLINE(build/include) */ +#include "block_encoder_inc.h" +#undef FN + +#define FN(X) X ## Command +/* NOLINTNEXTLINE(build/include) */ +#include "block_encoder_inc.h" +#undef FN + +#define FN(X) X ## Distance +/* NOLINTNEXTLINE(build/include) */ +#include "block_encoder_inc.h" +#undef FN + +static void JumpToByteBoundary(size_t* storage_ix, uint8_t* storage) { + *storage_ix = (*storage_ix + 7u) & ~7u; + storage[*storage_ix >> 3] = 0; +} + +typedef struct StoreMetablockArena { + BlockEncoder literal_enc; + BlockEncoder command_enc; + BlockEncoder distance_enc; + EncodeContextMapArena context_map_arena; +} StoreMetablockArena; + +void BrotliStoreMetaBlock(MemoryManager* m, + const uint8_t* input, size_t start_pos, size_t length, size_t mask, + uint8_t prev_byte, uint8_t prev_byte2, BROTLI_BOOL is_last, + const BrotliEncoderParams* params, ContextType literal_context_mode, + const Command* commands, size_t n_commands, const MetaBlockSplit* mb, + size_t* storage_ix, uint8_t* storage) { + + size_t pos = start_pos; + size_t i; + uint32_t num_distance_symbols = params->dist.alphabet_size_max; + uint32_t num_effective_distance_symbols = params->dist.alphabet_size_limit; + HuffmanTree* tree; + ContextLut literal_context_lut = BROTLI_CONTEXT_LUT(literal_context_mode); + StoreMetablockArena* arena = NULL; + BlockEncoder* literal_enc = NULL; + BlockEncoder* command_enc = NULL; + BlockEncoder* distance_enc = NULL; + const BrotliDistanceParams* dist = ¶ms->dist; + BROTLI_DCHECK( + num_effective_distance_symbols <= BROTLI_NUM_HISTOGRAM_DISTANCE_SYMBOLS); + + StoreCompressedMetaBlockHeader(is_last, length, storage_ix, storage); + + tree = BROTLI_ALLOC(m, HuffmanTree, MAX_HUFFMAN_TREE_SIZE); + arena = BROTLI_ALLOC(m, StoreMetablockArena, 1); + if (BROTLI_IS_OOM(m) || BROTLI_IS_NULL(tree) || BROTLI_IS_NULL(arena)) return; + literal_enc = &arena->literal_enc; + command_enc = &arena->command_enc; + distance_enc = &arena->distance_enc; + InitBlockEncoder(literal_enc, BROTLI_NUM_LITERAL_SYMBOLS, + mb->literal_split.num_types, mb->literal_split.types, + mb->literal_split.lengths, mb->literal_split.num_blocks); + InitBlockEncoder(command_enc, BROTLI_NUM_COMMAND_SYMBOLS, + mb->command_split.num_types, mb->command_split.types, + mb->command_split.lengths, mb->command_split.num_blocks); + InitBlockEncoder(distance_enc, num_effective_distance_symbols, + mb->distance_split.num_types, mb->distance_split.types, + mb->distance_split.lengths, mb->distance_split.num_blocks); + + BuildAndStoreBlockSwitchEntropyCodes(literal_enc, tree, storage_ix, storage); + BuildAndStoreBlockSwitchEntropyCodes(command_enc, tree, storage_ix, storage); + BuildAndStoreBlockSwitchEntropyCodes(distance_enc, tree, storage_ix, storage); + + BrotliWriteBits(2, dist->distance_postfix_bits, storage_ix, storage); + BrotliWriteBits( + 4, dist->num_direct_distance_codes >> dist->distance_postfix_bits, + storage_ix, storage); + for (i = 0; i < mb->literal_split.num_types; ++i) { + BrotliWriteBits(2, literal_context_mode, storage_ix, storage); + } + + if (mb->literal_context_map_size == 0) { + StoreTrivialContextMap( + &arena->context_map_arena, mb->literal_histograms_size, + BROTLI_LITERAL_CONTEXT_BITS, tree, storage_ix, storage); + } else { + EncodeContextMap(m, &arena->context_map_arena, + mb->literal_context_map, mb->literal_context_map_size, + mb->literal_histograms_size, tree, storage_ix, storage); + if (BROTLI_IS_OOM(m)) return; + } + + if (mb->distance_context_map_size == 0) { + StoreTrivialContextMap( + &arena->context_map_arena, mb->distance_histograms_size, + BROTLI_DISTANCE_CONTEXT_BITS, tree, storage_ix, storage); + } else { + EncodeContextMap(m, &arena->context_map_arena, + mb->distance_context_map, mb->distance_context_map_size, + mb->distance_histograms_size, tree, storage_ix, storage); + if (BROTLI_IS_OOM(m)) return; + } + + BuildAndStoreEntropyCodesLiteral(m, literal_enc, mb->literal_histograms, + mb->literal_histograms_size, BROTLI_NUM_LITERAL_SYMBOLS, tree, + storage_ix, storage); + if (BROTLI_IS_OOM(m)) return; + BuildAndStoreEntropyCodesCommand(m, command_enc, mb->command_histograms, + mb->command_histograms_size, BROTLI_NUM_COMMAND_SYMBOLS, tree, + storage_ix, storage); + if (BROTLI_IS_OOM(m)) return; + BuildAndStoreEntropyCodesDistance(m, distance_enc, mb->distance_histograms, + mb->distance_histograms_size, num_distance_symbols, tree, + storage_ix, storage); + if (BROTLI_IS_OOM(m)) return; + BROTLI_FREE(m, tree); + + for (i = 0; i < n_commands; ++i) { + const Command cmd = commands[i]; + size_t cmd_code = cmd.cmd_prefix_; + StoreSymbol(command_enc, cmd_code, storage_ix, storage); + StoreCommandExtra(&cmd, storage_ix, storage); + if (mb->literal_context_map_size == 0) { + size_t j; + for (j = cmd.insert_len_; j != 0; --j) { + StoreSymbol(literal_enc, input[pos & mask], storage_ix, storage); + ++pos; + } + } else { + size_t j; + for (j = cmd.insert_len_; j != 0; --j) { + size_t context = + BROTLI_CONTEXT(prev_byte, prev_byte2, literal_context_lut); + uint8_t literal = input[pos & mask]; + StoreSymbolWithContext(literal_enc, literal, context, + mb->literal_context_map, storage_ix, storage, + BROTLI_LITERAL_CONTEXT_BITS); + prev_byte2 = prev_byte; + prev_byte = literal; + ++pos; + } + } + pos += CommandCopyLen(&cmd); + if (CommandCopyLen(&cmd)) { + prev_byte2 = input[(pos - 2) & mask]; + prev_byte = input[(pos - 1) & mask]; + if (cmd.cmd_prefix_ >= 128) { + size_t dist_code = cmd.dist_prefix_ & 0x3FF; + uint32_t distnumextra = cmd.dist_prefix_ >> 10; + uint64_t distextra = cmd.dist_extra_; + if (mb->distance_context_map_size == 0) { + StoreSymbol(distance_enc, dist_code, storage_ix, storage); + } else { + size_t context = CommandDistanceContext(&cmd); + StoreSymbolWithContext(distance_enc, dist_code, context, + mb->distance_context_map, storage_ix, storage, + BROTLI_DISTANCE_CONTEXT_BITS); + } + BrotliWriteBits(distnumextra, distextra, storage_ix, storage); + } + } + } + CleanupBlockEncoder(m, distance_enc); + CleanupBlockEncoder(m, command_enc); + CleanupBlockEncoder(m, literal_enc); + BROTLI_FREE(m, arena); + if (is_last) { + JumpToByteBoundary(storage_ix, storage); + } +} + +static void BuildHistograms(const uint8_t* input, + size_t start_pos, + size_t mask, + const Command* commands, + size_t n_commands, + HistogramLiteral* lit_histo, + HistogramCommand* cmd_histo, + HistogramDistance* dist_histo) { + size_t pos = start_pos; + size_t i; + for (i = 0; i < n_commands; ++i) { + const Command cmd = commands[i]; + size_t j; + HistogramAddCommand(cmd_histo, cmd.cmd_prefix_); + for (j = cmd.insert_len_; j != 0; --j) { + HistogramAddLiteral(lit_histo, input[pos & mask]); + ++pos; + } + pos += CommandCopyLen(&cmd); + if (CommandCopyLen(&cmd) && cmd.cmd_prefix_ >= 128) { + HistogramAddDistance(dist_histo, cmd.dist_prefix_ & 0x3FF); + } + } +} + +static void StoreDataWithHuffmanCodes(const uint8_t* input, + size_t start_pos, + size_t mask, + const Command* commands, + size_t n_commands, + const uint8_t* lit_depth, + const uint16_t* lit_bits, + const uint8_t* cmd_depth, + const uint16_t* cmd_bits, + const uint8_t* dist_depth, + const uint16_t* dist_bits, + size_t* storage_ix, + uint8_t* storage) { + size_t pos = start_pos; + size_t i; + for (i = 0; i < n_commands; ++i) { + const Command cmd = commands[i]; + const size_t cmd_code = cmd.cmd_prefix_; + size_t j; + BrotliWriteBits( + cmd_depth[cmd_code], cmd_bits[cmd_code], storage_ix, storage); + StoreCommandExtra(&cmd, storage_ix, storage); + for (j = cmd.insert_len_; j != 0; --j) { + const uint8_t literal = input[pos & mask]; + BrotliWriteBits( + lit_depth[literal], lit_bits[literal], storage_ix, storage); + ++pos; + } + pos += CommandCopyLen(&cmd); + if (CommandCopyLen(&cmd) && cmd.cmd_prefix_ >= 128) { + const size_t dist_code = cmd.dist_prefix_ & 0x3FF; + const uint32_t distnumextra = cmd.dist_prefix_ >> 10; + const uint32_t distextra = cmd.dist_extra_; + BrotliWriteBits(dist_depth[dist_code], dist_bits[dist_code], + storage_ix, storage); + BrotliWriteBits(distnumextra, distextra, storage_ix, storage); + } + } +} + +/* TODO(eustas): pull alloc/dealloc to caller? */ +typedef struct MetablockArena { + HistogramLiteral lit_histo; + HistogramCommand cmd_histo; + HistogramDistance dist_histo; + /* TODO(eustas): merge bits and depth? */ + uint8_t lit_depth[BROTLI_NUM_LITERAL_SYMBOLS]; + uint16_t lit_bits[BROTLI_NUM_LITERAL_SYMBOLS]; + uint8_t cmd_depth[BROTLI_NUM_COMMAND_SYMBOLS]; + uint16_t cmd_bits[BROTLI_NUM_COMMAND_SYMBOLS]; + uint8_t dist_depth[MAX_SIMPLE_DISTANCE_ALPHABET_SIZE]; + uint16_t dist_bits[MAX_SIMPLE_DISTANCE_ALPHABET_SIZE]; + HuffmanTree tree[MAX_HUFFMAN_TREE_SIZE]; +} MetablockArena; + +void BrotliStoreMetaBlockTrivial(MemoryManager* m, + const uint8_t* input, size_t start_pos, size_t length, size_t mask, + BROTLI_BOOL is_last, const BrotliEncoderParams* params, + const Command* commands, size_t n_commands, + size_t* storage_ix, uint8_t* storage) { + MetablockArena* arena = BROTLI_ALLOC(m, MetablockArena, 1); + uint32_t num_distance_symbols = params->dist.alphabet_size_max; + if (BROTLI_IS_OOM(m) || BROTLI_IS_NULL(arena)) return; + + StoreCompressedMetaBlockHeader(is_last, length, storage_ix, storage); + + HistogramClearLiteral(&arena->lit_histo); + HistogramClearCommand(&arena->cmd_histo); + HistogramClearDistance(&arena->dist_histo); + + BuildHistograms(input, start_pos, mask, commands, n_commands, + &arena->lit_histo, &arena->cmd_histo, &arena->dist_histo); + + BrotliWriteBits(13, 0, storage_ix, storage); + + BuildAndStoreHuffmanTree(arena->lit_histo.data_, BROTLI_NUM_LITERAL_SYMBOLS, + BROTLI_NUM_LITERAL_SYMBOLS, arena->tree, + arena->lit_depth, arena->lit_bits, + storage_ix, storage); + BuildAndStoreHuffmanTree(arena->cmd_histo.data_, BROTLI_NUM_COMMAND_SYMBOLS, + BROTLI_NUM_COMMAND_SYMBOLS, arena->tree, + arena->cmd_depth, arena->cmd_bits, + storage_ix, storage); + BuildAndStoreHuffmanTree(arena->dist_histo.data_, + MAX_SIMPLE_DISTANCE_ALPHABET_SIZE, + num_distance_symbols, arena->tree, + arena->dist_depth, arena->dist_bits, + storage_ix, storage); + StoreDataWithHuffmanCodes(input, start_pos, mask, commands, + n_commands, arena->lit_depth, arena->lit_bits, + arena->cmd_depth, arena->cmd_bits, + arena->dist_depth, arena->dist_bits, + storage_ix, storage); + BROTLI_FREE(m, arena); + if (is_last) { + JumpToByteBoundary(storage_ix, storage); + } +} + +void BrotliStoreMetaBlockFast(MemoryManager* m, + const uint8_t* input, size_t start_pos, size_t length, size_t mask, + BROTLI_BOOL is_last, const BrotliEncoderParams* params, + const Command* commands, size_t n_commands, + size_t* storage_ix, uint8_t* storage) { + MetablockArena* arena = BROTLI_ALLOC(m, MetablockArena, 1); + uint32_t num_distance_symbols = params->dist.alphabet_size_max; + uint32_t distance_alphabet_bits = + Log2FloorNonZero(num_distance_symbols - 1) + 1; + if (BROTLI_IS_OOM(m) || BROTLI_IS_NULL(arena)) return; + + StoreCompressedMetaBlockHeader(is_last, length, storage_ix, storage); + + BrotliWriteBits(13, 0, storage_ix, storage); + + if (n_commands <= 128) { + uint32_t histogram[BROTLI_NUM_LITERAL_SYMBOLS] = { 0 }; + size_t pos = start_pos; + size_t num_literals = 0; + size_t i; + for (i = 0; i < n_commands; ++i) { + const Command cmd = commands[i]; + size_t j; + for (j = cmd.insert_len_; j != 0; --j) { + ++histogram[input[pos & mask]]; + ++pos; + } + num_literals += cmd.insert_len_; + pos += CommandCopyLen(&cmd); + } + BrotliBuildAndStoreHuffmanTreeFast(arena->tree, histogram, num_literals, + /* max_bits = */ 8, + arena->lit_depth, arena->lit_bits, + storage_ix, storage); + StoreStaticCommandHuffmanTree(storage_ix, storage); + StoreStaticDistanceHuffmanTree(storage_ix, storage); + StoreDataWithHuffmanCodes(input, start_pos, mask, commands, + n_commands, arena->lit_depth, arena->lit_bits, + kStaticCommandCodeDepth, + kStaticCommandCodeBits, + kStaticDistanceCodeDepth, + kStaticDistanceCodeBits, + storage_ix, storage); + } else { + HistogramClearLiteral(&arena->lit_histo); + HistogramClearCommand(&arena->cmd_histo); + HistogramClearDistance(&arena->dist_histo); + BuildHistograms(input, start_pos, mask, commands, n_commands, + &arena->lit_histo, &arena->cmd_histo, &arena->dist_histo); + BrotliBuildAndStoreHuffmanTreeFast(arena->tree, arena->lit_histo.data_, + arena->lit_histo.total_count_, + /* max_bits = */ 8, + arena->lit_depth, arena->lit_bits, + storage_ix, storage); + BrotliBuildAndStoreHuffmanTreeFast(arena->tree, arena->cmd_histo.data_, + arena->cmd_histo.total_count_, + /* max_bits = */ 10, + arena->cmd_depth, arena->cmd_bits, + storage_ix, storage); + BrotliBuildAndStoreHuffmanTreeFast(arena->tree, arena->dist_histo.data_, + arena->dist_histo.total_count_, + /* max_bits = */ + distance_alphabet_bits, + arena->dist_depth, arena->dist_bits, + storage_ix, storage); + StoreDataWithHuffmanCodes(input, start_pos, mask, commands, + n_commands, arena->lit_depth, arena->lit_bits, + arena->cmd_depth, arena->cmd_bits, + arena->dist_depth, arena->dist_bits, + storage_ix, storage); + } + + BROTLI_FREE(m, arena); + + if (is_last) { + JumpToByteBoundary(storage_ix, storage); + } +} + +/* This is for storing uncompressed blocks (simple raw storage of + bytes-as-bytes). */ +void BrotliStoreUncompressedMetaBlock(BROTLI_BOOL is_final_block, + const uint8_t* BROTLI_RESTRICT input, + size_t position, size_t mask, + size_t len, + size_t* BROTLI_RESTRICT storage_ix, + uint8_t* BROTLI_RESTRICT storage) { + size_t masked_pos = position & mask; + BrotliStoreUncompressedMetaBlockHeader(len, storage_ix, storage); + JumpToByteBoundary(storage_ix, storage); + + if (masked_pos + len > mask + 1) { + size_t len1 = mask + 1 - masked_pos; + memcpy(&storage[*storage_ix >> 3], &input[masked_pos], len1); + *storage_ix += len1 << 3; + len -= len1; + masked_pos = 0; + } + memcpy(&storage[*storage_ix >> 3], &input[masked_pos], len); + *storage_ix += len << 3; + + /* We need to clear the next 4 bytes to continue to be + compatible with BrotliWriteBits. */ + BrotliWriteBitsPrepareStorage(*storage_ix, storage); + + /* Since the uncompressed block itself may not be the final block, add an + empty one after this. */ + if (is_final_block) { + BrotliWriteBits(1, 1, storage_ix, storage); /* islast */ + BrotliWriteBits(1, 1, storage_ix, storage); /* isempty */ + JumpToByteBoundary(storage_ix, storage); + } +} + +#if defined(BROTLI_TEST) +void BrotliGetBlockLengthPrefixCodeForTest(uint32_t len, size_t* code, + uint32_t* n_extra, uint32_t* extra); +void BrotliGetBlockLengthPrefixCodeForTest(uint32_t len, size_t* code, + uint32_t* n_extra, uint32_t* extra) { + GetBlockLengthPrefixCode(len, code, n_extra, extra); +} +#endif + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif diff --git a/packages/runtime/vendor/brotli/c/enc/brotli_bit_stream.h b/packages/runtime/vendor/brotli/c/enc/brotli_bit_stream.h new file mode 100644 index 000000000..7b72bdcee --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/brotli_bit_stream.h @@ -0,0 +1,83 @@ +/* Copyright 2014 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* Functions to convert brotli-related data structures into the + brotli bit stream. The functions here operate under + assumption that there is enough space in the storage, i.e., there are + no out-of-range checks anywhere. + + These functions do bit addressing into a byte array. The byte array + is called "storage" and the index to the bit is called storage_ix + in function arguments. */ + +#ifndef BROTLI_ENC_BROTLI_BIT_STREAM_H_ +#define BROTLI_ENC_BROTLI_BIT_STREAM_H_ + +#include "../common/context.h" +#include "../common/platform.h" +#include "command.h" +#include "entropy_encode.h" +#include "memory.h" +#include "metablock.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +/* All Store functions here will use a storage_ix, which is always the bit + position for the current storage. */ + +BROTLI_INTERNAL void BrotliStoreHuffmanTree(const uint8_t* depths, size_t num, + HuffmanTree* tree, size_t* storage_ix, uint8_t* storage); + +BROTLI_INTERNAL void BrotliBuildAndStoreHuffmanTreeFast( + HuffmanTree* tree, const uint32_t* histogram, const size_t histogram_total, + const size_t max_bits, uint8_t* depth, uint16_t* bits, size_t* storage_ix, + uint8_t* storage); + +/* REQUIRES: length > 0 */ +/* REQUIRES: length <= (1 << 24) */ +BROTLI_INTERNAL void BrotliStoreMetaBlock(MemoryManager* m, + const uint8_t* input, size_t start_pos, size_t length, size_t mask, + uint8_t prev_byte, uint8_t prev_byte2, BROTLI_BOOL is_last, + const BrotliEncoderParams* params, ContextType literal_context_mode, + const Command* commands, size_t n_commands, const MetaBlockSplit* mb, + size_t* storage_ix, uint8_t* storage); + +/* Stores the meta-block without doing any block splitting, just collects + one histogram per block category and uses that for entropy coding. + REQUIRES: length > 0 + REQUIRES: length <= (1 << 24) */ +BROTLI_INTERNAL void BrotliStoreMetaBlockTrivial(MemoryManager* m, + const uint8_t* input, size_t start_pos, size_t length, size_t mask, + BROTLI_BOOL is_last, const BrotliEncoderParams* params, + const Command* commands, size_t n_commands, + size_t* storage_ix, uint8_t* storage); + +/* Same as above, but uses static prefix codes for histograms with a only a few + symbols, and uses static code length prefix codes for all other histograms. + REQUIRES: length > 0 + REQUIRES: length <= (1 << 24) */ +BROTLI_INTERNAL void BrotliStoreMetaBlockFast(MemoryManager* m, + const uint8_t* input, size_t start_pos, size_t length, size_t mask, + BROTLI_BOOL is_last, const BrotliEncoderParams* params, + const Command* commands, size_t n_commands, + size_t* storage_ix, uint8_t* storage); + +/* This is for storing uncompressed blocks (simple raw storage of + bytes-as-bytes). + REQUIRES: length > 0 + REQUIRES: length <= (1 << 24) */ +BROTLI_INTERNAL void BrotliStoreUncompressedMetaBlock( + BROTLI_BOOL is_final_block, const uint8_t* BROTLI_RESTRICT input, + size_t position, size_t mask, size_t len, + size_t* BROTLI_RESTRICT storage_ix, uint8_t* BROTLI_RESTRICT storage); + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif + +#endif /* BROTLI_ENC_BROTLI_BIT_STREAM_H_ */ diff --git a/packages/runtime/vendor/brotli/c/enc/cluster.c b/packages/runtime/vendor/brotli/c/enc/cluster.c new file mode 100644 index 000000000..4bf94ab76 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/cluster.c @@ -0,0 +1,55 @@ +/* Copyright 2013 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* Functions for clustering similar histograms together. */ + +#include "cluster.h" + +#include "../common/platform.h" +#include "bit_cost.h" /* BrotliPopulationCost */ +#include "fast_log.h" +#include "histogram.h" +#include "memory.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +static BROTLI_INLINE BROTLI_BOOL HistogramPairIsLess( + const HistogramPair* p1, const HistogramPair* p2) { + if (p1->cost_diff != p2->cost_diff) { + return TO_BROTLI_BOOL(p1->cost_diff > p2->cost_diff); + } + return TO_BROTLI_BOOL((p1->idx2 - p1->idx1) > (p2->idx2 - p2->idx1)); +} + +/* Returns entropy reduction of the context map when we combine two clusters. */ +static BROTLI_INLINE double ClusterCostDiff(size_t size_a, size_t size_b) { + size_t size_c = size_a + size_b; + return (double)size_a * FastLog2(size_a) + + (double)size_b * FastLog2(size_b) - + (double)size_c * FastLog2(size_c); +} + +#define CODE(X) X + +#define FN(X) X ## Literal +#include "cluster_inc.h" /* NOLINT(build/include) */ +#undef FN + +#define FN(X) X ## Command +#include "cluster_inc.h" /* NOLINT(build/include) */ +#undef FN + +#define FN(X) X ## Distance +#include "cluster_inc.h" /* NOLINT(build/include) */ +#undef FN + +#undef CODE + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif diff --git a/packages/runtime/vendor/brotli/c/enc/cluster.h b/packages/runtime/vendor/brotli/c/enc/cluster.h new file mode 100644 index 000000000..6519116c4 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/cluster.h @@ -0,0 +1,47 @@ +/* Copyright 2013 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* Functions for clustering similar histograms together. */ + +#ifndef BROTLI_ENC_CLUSTER_H_ +#define BROTLI_ENC_CLUSTER_H_ + +#include "../common/platform.h" +#include "histogram.h" +#include "memory.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +typedef struct HistogramPair { + uint32_t idx1; + uint32_t idx2; + double cost_combo; + double cost_diff; +} HistogramPair; + +#define CODE(X) /* Declaration */; + +#define FN(X) X ## Literal +#include "cluster_inc.h" /* NOLINT(build/include) */ +#undef FN + +#define FN(X) X ## Command +#include "cluster_inc.h" /* NOLINT(build/include) */ +#undef FN + +#define FN(X) X ## Distance +#include "cluster_inc.h" /* NOLINT(build/include) */ +#undef FN + +#undef CODE + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif + +#endif /* BROTLI_ENC_CLUSTER_H_ */ diff --git a/packages/runtime/vendor/brotli/c/enc/cluster_inc.h b/packages/runtime/vendor/brotli/c/enc/cluster_inc.h new file mode 100644 index 000000000..d6215ef06 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/cluster_inc.h @@ -0,0 +1,325 @@ +/* NOLINT(build/header_guard) */ +/* Copyright 2013 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* template parameters: FN, CODE */ + +#define HistogramType FN(Histogram) + +/* Computes the bit cost reduction by combining out[idx1] and out[idx2] and if + it is below a threshold, stores the pair (idx1, idx2) in the *pairs queue. */ +BROTLI_INTERNAL void FN(BrotliCompareAndPushToQueue)( + const HistogramType* out, HistogramType* tmp, const uint32_t* cluster_size, + uint32_t idx1, uint32_t idx2, size_t max_num_pairs, HistogramPair* pairs, + size_t* num_pairs) CODE({ + BROTLI_BOOL is_good_pair = BROTLI_FALSE; + HistogramPair p; + p.idx1 = p.idx2 = 0; + p.cost_diff = p.cost_combo = 0; + if (idx1 == idx2) { + return; + } + if (idx2 < idx1) { + uint32_t t = idx2; + idx2 = idx1; + idx1 = t; + } + p.idx1 = idx1; + p.idx2 = idx2; + p.cost_diff = 0.5 * ClusterCostDiff(cluster_size[idx1], cluster_size[idx2]); + p.cost_diff -= out[idx1].bit_cost_; + p.cost_diff -= out[idx2].bit_cost_; + + if (out[idx1].total_count_ == 0) { + p.cost_combo = out[idx2].bit_cost_; + is_good_pair = BROTLI_TRUE; + } else if (out[idx2].total_count_ == 0) { + p.cost_combo = out[idx1].bit_cost_; + is_good_pair = BROTLI_TRUE; + } else { + double threshold = *num_pairs == 0 ? 1e99 : + BROTLI_MAX(double, 0.0, pairs[0].cost_diff); + double cost_combo; + *tmp = out[idx1]; + FN(HistogramAddHistogram)(tmp, &out[idx2]); + cost_combo = FN(BrotliPopulationCost)(tmp); + if (cost_combo < threshold - p.cost_diff) { + p.cost_combo = cost_combo; + is_good_pair = BROTLI_TRUE; + } + } + if (is_good_pair) { + p.cost_diff += p.cost_combo; + if (*num_pairs > 0 && HistogramPairIsLess(&pairs[0], &p)) { + /* Replace the top of the queue if needed. */ + if (*num_pairs < max_num_pairs) { + pairs[*num_pairs] = pairs[0]; + ++(*num_pairs); + } + pairs[0] = p; + } else if (*num_pairs < max_num_pairs) { + pairs[*num_pairs] = p; + ++(*num_pairs); + } + } +}) + +BROTLI_INTERNAL size_t FN(BrotliHistogramCombine)(HistogramType* out, + HistogramType* tmp, + uint32_t* cluster_size, + uint32_t* symbols, + uint32_t* clusters, + HistogramPair* pairs, + size_t num_clusters, + size_t symbols_size, + size_t max_clusters, + size_t max_num_pairs) CODE({ + double cost_diff_threshold = 0.0; + size_t min_cluster_size = 1; + size_t num_pairs = 0; + + { + /* We maintain a vector of histogram pairs, with the property that the pair + with the maximum bit cost reduction is the first. */ + size_t idx1; + for (idx1 = 0; idx1 < num_clusters; ++idx1) { + size_t idx2; + for (idx2 = idx1 + 1; idx2 < num_clusters; ++idx2) { + FN(BrotliCompareAndPushToQueue)(out, tmp, cluster_size, clusters[idx1], + clusters[idx2], max_num_pairs, &pairs[0], &num_pairs); + } + } + } + + while (num_clusters > min_cluster_size) { + uint32_t best_idx1; + uint32_t best_idx2; + size_t i; + if (pairs[0].cost_diff >= cost_diff_threshold) { + cost_diff_threshold = 1e99; + min_cluster_size = max_clusters; + continue; + } + /* Take the best pair from the top of heap. */ + best_idx1 = pairs[0].idx1; + best_idx2 = pairs[0].idx2; + FN(HistogramAddHistogram)(&out[best_idx1], &out[best_idx2]); + out[best_idx1].bit_cost_ = pairs[0].cost_combo; + cluster_size[best_idx1] += cluster_size[best_idx2]; + for (i = 0; i < symbols_size; ++i) { + if (symbols[i] == best_idx2) { + symbols[i] = best_idx1; + } + } + for (i = 0; i < num_clusters; ++i) { + if (clusters[i] == best_idx2) { + memmove(&clusters[i], &clusters[i + 1], + (num_clusters - i - 1) * sizeof(clusters[0])); + break; + } + } + --num_clusters; + { + /* Remove pairs intersecting the just combined best pair. */ + size_t copy_to_idx = 0; + for (i = 0; i < num_pairs; ++i) { + HistogramPair* p = &pairs[i]; + if (p->idx1 == best_idx1 || p->idx2 == best_idx1 || + p->idx1 == best_idx2 || p->idx2 == best_idx2) { + /* Remove invalid pair from the queue. */ + continue; + } + if (HistogramPairIsLess(&pairs[0], p)) { + /* Replace the top of the queue if needed. */ + HistogramPair front = pairs[0]; + pairs[0] = *p; + pairs[copy_to_idx] = front; + } else { + pairs[copy_to_idx] = *p; + } + ++copy_to_idx; + } + num_pairs = copy_to_idx; + } + + /* Push new pairs formed with the combined histogram to the heap. */ + for (i = 0; i < num_clusters; ++i) { + FN(BrotliCompareAndPushToQueue)(out, tmp, cluster_size, best_idx1, + clusters[i], max_num_pairs, &pairs[0], &num_pairs); + } + } + return num_clusters; +}) + +/* What is the bit cost of moving histogram from cur_symbol to candidate. */ +BROTLI_INTERNAL double FN(BrotliHistogramBitCostDistance)( + const HistogramType* histogram, const HistogramType* candidate, + HistogramType* tmp) CODE({ + if (histogram->total_count_ == 0) { + return 0.0; + } else { + *tmp = *histogram; + FN(HistogramAddHistogram)(tmp, candidate); + return FN(BrotliPopulationCost)(tmp) - candidate->bit_cost_; + } +}) + +/* Find the best 'out' histogram for each of the 'in' histograms. + When called, clusters[0..num_clusters) contains the unique values from + symbols[0..in_size), but this property is not preserved in this function. + Note: we assume that out[]->bit_cost_ is already up-to-date. */ +BROTLI_INTERNAL void FN(BrotliHistogramRemap)(const HistogramType* in, + size_t in_size, const uint32_t* clusters, size_t num_clusters, + HistogramType* out, HistogramType* tmp, uint32_t* symbols) CODE({ + size_t i; + for (i = 0; i < in_size; ++i) { + uint32_t best_out = i == 0 ? symbols[0] : symbols[i - 1]; + double best_bits = + FN(BrotliHistogramBitCostDistance)(&in[i], &out[best_out], tmp); + size_t j; + for (j = 0; j < num_clusters; ++j) { + const double cur_bits = + FN(BrotliHistogramBitCostDistance)(&in[i], &out[clusters[j]], tmp); + if (cur_bits < best_bits) { + best_bits = cur_bits; + best_out = clusters[j]; + } + } + symbols[i] = best_out; + } + + /* Recompute each out based on raw and symbols. */ + for (i = 0; i < num_clusters; ++i) { + FN(HistogramClear)(&out[clusters[i]]); + } + for (i = 0; i < in_size; ++i) { + FN(HistogramAddHistogram)(&out[symbols[i]], &in[i]); + } +}) + +/* Reorders elements of the out[0..length) array and changes values in + symbols[0..length) array in the following way: + * when called, symbols[] contains indexes into out[], and has N unique + values (possibly N < length) + * on return, symbols'[i] = f(symbols[i]) and + out'[symbols'[i]] = out[symbols[i]], for each 0 <= i < length, + where f is a bijection between the range of symbols[] and [0..N), and + the first occurrences of values in symbols'[i] come in consecutive + increasing order. + Returns N, the number of unique values in symbols[]. */ +BROTLI_INTERNAL size_t FN(BrotliHistogramReindex)(MemoryManager* m, + HistogramType* out, uint32_t* symbols, size_t length) CODE({ + static const uint32_t kInvalidIndex = BROTLI_UINT32_MAX; + uint32_t* new_index = BROTLI_ALLOC(m, uint32_t, length); + uint32_t next_index; + HistogramType* tmp; + size_t i; + if (BROTLI_IS_OOM(m) || BROTLI_IS_NULL(new_index)) return 0; + for (i = 0; i < length; ++i) { + new_index[i] = kInvalidIndex; + } + next_index = 0; + for (i = 0; i < length; ++i) { + if (new_index[symbols[i]] == kInvalidIndex) { + new_index[symbols[i]] = next_index; + ++next_index; + } + } + /* TODO(eustas): by using idea of "cycle-sort" we can avoid allocation of + tmp and reduce the number of copying by the factor of 2. */ + tmp = BROTLI_ALLOC(m, HistogramType, next_index); + if (BROTLI_IS_OOM(m) || BROTLI_IS_NULL(tmp)) return 0; + next_index = 0; + for (i = 0; i < length; ++i) { + if (new_index[symbols[i]] == next_index) { + tmp[next_index] = out[symbols[i]]; + ++next_index; + } + symbols[i] = new_index[symbols[i]]; + } + BROTLI_FREE(m, new_index); + for (i = 0; i < next_index; ++i) { + out[i] = tmp[i]; + } + BROTLI_FREE(m, tmp); + return next_index; +}) + +BROTLI_INTERNAL void FN(BrotliClusterHistograms)( + MemoryManager* m, const HistogramType* in, const size_t in_size, + size_t max_histograms, HistogramType* out, size_t* out_size, + uint32_t* histogram_symbols) CODE({ + uint32_t* cluster_size = BROTLI_ALLOC(m, uint32_t, in_size); + uint32_t* clusters = BROTLI_ALLOC(m, uint32_t, in_size); + size_t num_clusters = 0; + const size_t max_input_histograms = 64; + size_t pairs_capacity = max_input_histograms * max_input_histograms / 2; + /* For the first pass of clustering, we allow all pairs. */ + HistogramPair* pairs = BROTLI_ALLOC(m, HistogramPair, pairs_capacity + 1); + /* TODO(eustas): move to "persistent" arena? */ + HistogramType* tmp = BROTLI_ALLOC(m, HistogramType, 1); + size_t i; + + if (BROTLI_IS_OOM(m) || BROTLI_IS_NULL(cluster_size) || + BROTLI_IS_NULL(clusters) || BROTLI_IS_NULL(pairs)|| BROTLI_IS_NULL(tmp)) { + return; + } + + for (i = 0; i < in_size; ++i) { + cluster_size[i] = 1; + } + + for (i = 0; i < in_size; ++i) { + out[i] = in[i]; + out[i].bit_cost_ = FN(BrotliPopulationCost)(&in[i]); + histogram_symbols[i] = (uint32_t)i; + } + + for (i = 0; i < in_size; i += max_input_histograms) { + size_t num_to_combine = + BROTLI_MIN(size_t, in_size - i, max_input_histograms); + size_t num_new_clusters; + size_t j; + for (j = 0; j < num_to_combine; ++j) { + clusters[num_clusters + j] = (uint32_t)(i + j); + } + num_new_clusters = + FN(BrotliHistogramCombine)(out, tmp, cluster_size, + &histogram_symbols[i], + &clusters[num_clusters], pairs, + num_to_combine, num_to_combine, + max_histograms, pairs_capacity); + num_clusters += num_new_clusters; + } + + { + /* For the second pass, we limit the total number of histogram pairs. + After this limit is reached, we only keep searching for the best pair. */ + size_t max_num_pairs = BROTLI_MIN(size_t, + 64 * num_clusters, (num_clusters / 2) * num_clusters); + BROTLI_ENSURE_CAPACITY( + m, HistogramPair, pairs, pairs_capacity, max_num_pairs + 1); + if (BROTLI_IS_OOM(m)) return; + + /* Collapse similar histograms. */ + num_clusters = FN(BrotliHistogramCombine)(out, tmp, cluster_size, + histogram_symbols, clusters, + pairs, num_clusters, in_size, + max_histograms, max_num_pairs); + } + BROTLI_FREE(m, pairs); + BROTLI_FREE(m, cluster_size); + /* Find the optimal map from original histograms to the final ones. */ + FN(BrotliHistogramRemap)(in, in_size, clusters, num_clusters, + out, tmp, histogram_symbols); + BROTLI_FREE(m, tmp); + BROTLI_FREE(m, clusters); + /* Convert the context map to a canonical form. */ + *out_size = FN(BrotliHistogramReindex)(m, out, histogram_symbols, in_size); + if (BROTLI_IS_OOM(m)) return; +}) + +#undef HistogramType diff --git a/packages/runtime/vendor/brotli/c/enc/command.c b/packages/runtime/vendor/brotli/c/enc/command.c new file mode 100644 index 000000000..699fa8098 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/command.c @@ -0,0 +1,28 @@ +/* Copyright 2013 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +#include "command.h" + +#include "../common/platform.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +const uint32_t kBrotliInsBase[BROTLI_NUM_INS_COPY_CODES] = { + 0, 1, 2, 3, 4, 5, 6, 8, 10, 14, 18, 26, + 34, 50, 66, 98, 130, 194, 322, 578, 1090, 2114, 6210, 22594}; +const uint32_t kBrotliInsExtra[BROTLI_NUM_INS_COPY_CODES] = { + 0, 0, 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 7, 8, 9, 10, 12, 14, 24}; +const uint32_t kBrotliCopyBase[BROTLI_NUM_INS_COPY_CODES] = { + 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 14, 18, + 22, 30, 38, 54, 70, 102, 134, 198, 326, 582, 1094, 2118}; +const uint32_t kBrotliCopyExtra[BROTLI_NUM_INS_COPY_CODES] = { + 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 7, 8, 9, 10, 24}; + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif diff --git a/packages/runtime/vendor/brotli/c/enc/command.h b/packages/runtime/vendor/brotli/c/enc/command.h new file mode 100644 index 000000000..c1cb208e6 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/command.h @@ -0,0 +1,189 @@ +/* Copyright 2013 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* This class models a sequence of literals and a backward reference copy. */ + +#ifndef BROTLI_ENC_COMMAND_H_ +#define BROTLI_ENC_COMMAND_H_ + +#include "../common/constants.h" +#include "../common/platform.h" +#include "fast_log.h" +#include "params.h" +#include "prefix.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +BROTLI_INTERNAL extern const BROTLI_MODEL("small") +uint32_t kBrotliInsBase[BROTLI_NUM_INS_COPY_CODES]; +BROTLI_INTERNAL extern const BROTLI_MODEL("small") +uint32_t kBrotliInsExtra[BROTLI_NUM_INS_COPY_CODES]; +BROTLI_INTERNAL extern const BROTLI_MODEL("small") +uint32_t kBrotliCopyBase[BROTLI_NUM_INS_COPY_CODES]; +BROTLI_INTERNAL extern const BROTLI_MODEL("small") +uint32_t kBrotliCopyExtra[BROTLI_NUM_INS_COPY_CODES]; + +static BROTLI_INLINE uint16_t GetInsertLengthCode(size_t insertlen) { + if (insertlen < 6) { + return (uint16_t)insertlen; + } else if (insertlen < 130) { + uint32_t nbits = Log2FloorNonZero(insertlen - 2) - 1u; + return (uint16_t)((nbits << 1) + ((insertlen - 2) >> nbits) + 2); + } else if (insertlen < 2114) { + return (uint16_t)(Log2FloorNonZero(insertlen - 66) + 10); + } else if (insertlen < 6210) { + return 21u; + } else if (insertlen < 22594) { + return 22u; + } else { + return 23u; + } +} + +static BROTLI_INLINE uint16_t GetCopyLengthCode(size_t copylen) { + if (copylen < 10) { + return (uint16_t)(copylen - 2); + } else if (copylen < 134) { + uint32_t nbits = Log2FloorNonZero(copylen - 6) - 1u; + return (uint16_t)((nbits << 1) + ((copylen - 6) >> nbits) + 4); + } else if (copylen < 2118) { + return (uint16_t)(Log2FloorNonZero(copylen - 70) + 12); + } else { + return 23u; + } +} + +static BROTLI_INLINE uint16_t CombineLengthCodes( + uint16_t inscode, uint16_t copycode, BROTLI_BOOL use_last_distance) { + uint16_t bits64 = + (uint16_t)((copycode & 0x7u) | ((inscode & 0x7u) << 3u)); + if (use_last_distance && inscode < 8u && copycode < 16u) { + return (copycode < 8u) ? bits64 : (bits64 | 64u); + } else { + /* Specification: 5 Encoding of ... (last table) */ + /* offset = 2 * index, where index is in range [0..8] */ + uint32_t offset = 2u * ((copycode >> 3u) + 3u * (inscode >> 3u)); + /* All values in specification are K * 64, + where K = [2, 3, 6, 4, 5, 8, 7, 9, 10], + i + 1 = [1, 2, 3, 4, 5, 6, 7, 8, 9], + K - i - 1 = [1, 1, 3, 0, 0, 2, 0, 1, 2] = D. + All values in D require only 2 bits to encode. + Magic constant is shifted 6 bits left, to avoid final multiplication. */ + offset = (offset << 5u) + 0x40u + ((0x520D40u >> offset) & 0xC0u); + return (uint16_t)(offset | bits64); + } +} + +static BROTLI_INLINE void GetLengthCode(size_t insertlen, size_t copylen, + BROTLI_BOOL use_last_distance, + uint16_t* code) { + uint16_t inscode = GetInsertLengthCode(insertlen); + uint16_t copycode = GetCopyLengthCode(copylen); + *code = CombineLengthCodes(inscode, copycode, use_last_distance); +} + +static BROTLI_INLINE uint32_t GetInsertBase(uint16_t inscode) { + return kBrotliInsBase[inscode]; +} + +static BROTLI_INLINE uint32_t GetInsertExtra(uint16_t inscode) { + return kBrotliInsExtra[inscode]; +} + +static BROTLI_INLINE uint32_t GetCopyBase(uint16_t copycode) { + return kBrotliCopyBase[copycode]; +} + +static BROTLI_INLINE uint32_t GetCopyExtra(uint16_t copycode) { + return kBrotliCopyExtra[copycode]; +} + +typedef struct Command { + uint32_t insert_len_; + /* Stores copy_len in low 25 bits and copy_code - copy_len in high 7 bit. */ + uint32_t copy_len_; + /* Stores distance extra bits. */ + uint32_t dist_extra_; + uint16_t cmd_prefix_; + /* Stores distance code in low 10 bits + and number of extra bits in high 6 bits. */ + uint16_t dist_prefix_; +} Command; + +/* distance_code is e.g. 0 for same-as-last short code, or 16 for offset 1. */ +static BROTLI_INLINE void InitCommand(Command* self, + const BrotliDistanceParams* dist, size_t insertlen, + size_t copylen, int copylen_code_delta, size_t distance_code) { + /* Don't rely on signed int representation, use honest casts. */ + uint32_t delta = (uint8_t)((int8_t)copylen_code_delta); + self->insert_len_ = (uint32_t)insertlen; + self->copy_len_ = (uint32_t)(copylen | (delta << 25)); + /* The distance prefix and extra bits are stored in this Command as if + npostfix and ndirect were 0, they are only recomputed later after the + clustering if needed. */ + PrefixEncodeCopyDistance( + distance_code, dist->num_direct_distance_codes, + dist->distance_postfix_bits, &self->dist_prefix_, &self->dist_extra_); + GetLengthCode( + insertlen, (size_t)((int)copylen + copylen_code_delta), + TO_BROTLI_BOOL((self->dist_prefix_ & 0x3FF) == 0), &self->cmd_prefix_); +} + +static BROTLI_INLINE void InitInsertCommand(Command* self, size_t insertlen) { + self->insert_len_ = (uint32_t)insertlen; + self->copy_len_ = 4 << 25; + self->dist_extra_ = 0; + self->dist_prefix_ = BROTLI_NUM_DISTANCE_SHORT_CODES; + GetLengthCode(insertlen, 4, BROTLI_FALSE, &self->cmd_prefix_); +} + +static BROTLI_INLINE uint32_t CommandRestoreDistanceCode( + const Command* self, const BrotliDistanceParams* dist) { + if ((self->dist_prefix_ & 0x3FFu) < + BROTLI_NUM_DISTANCE_SHORT_CODES + dist->num_direct_distance_codes) { + return self->dist_prefix_ & 0x3FFu; + } else { + uint32_t dcode = self->dist_prefix_ & 0x3FFu; + uint32_t nbits = self->dist_prefix_ >> 10; + uint32_t extra = self->dist_extra_; + uint32_t postfix_mask = (1U << dist->distance_postfix_bits) - 1U; + uint32_t hcode = (dcode - dist->num_direct_distance_codes - + BROTLI_NUM_DISTANCE_SHORT_CODES) >> + dist->distance_postfix_bits; + uint32_t lcode = (dcode - dist->num_direct_distance_codes - + BROTLI_NUM_DISTANCE_SHORT_CODES) & postfix_mask; + uint32_t offset = ((2U + (hcode & 1U)) << nbits) - 4U; + return ((offset + extra) << dist->distance_postfix_bits) + lcode + + dist->num_direct_distance_codes + BROTLI_NUM_DISTANCE_SHORT_CODES; + } +} + +static BROTLI_INLINE uint32_t CommandDistanceContext(const Command* self) { + uint32_t r = self->cmd_prefix_ >> 6; + uint32_t c = self->cmd_prefix_ & 7; + if ((r == 0 || r == 2 || r == 4 || r == 7) && (c <= 2)) { + return c; + } + return 3; +} + +static BROTLI_INLINE uint32_t CommandCopyLen(const Command* self) { + return self->copy_len_ & 0x1FFFFFF; +} + +static BROTLI_INLINE uint32_t CommandCopyLenCode(const Command* self) { + uint32_t modifier = self->copy_len_ >> 25; + int32_t delta = (int8_t)((uint8_t)(modifier | ((modifier & 0x40) << 1))); + return (uint32_t)((int32_t)(self->copy_len_ & 0x1FFFFFF) + delta); +} + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif + +#endif /* BROTLI_ENC_COMMAND_H_ */ diff --git a/packages/runtime/vendor/brotli/c/enc/compound_dictionary.c b/packages/runtime/vendor/brotli/c/enc/compound_dictionary.c new file mode 100644 index 000000000..bf323a4b3 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/compound_dictionary.c @@ -0,0 +1,205 @@ +/* Copyright 2017 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +#include "compound_dictionary.h" + +#include "../common/platform.h" +#include +#include "memory.h" + +static PreparedDictionary* CreatePreparedDictionaryWithParams(MemoryManager* m, + const uint8_t* source, size_t source_size, uint32_t bucket_bits, + uint32_t slot_bits, uint32_t hash_bits, uint16_t bucket_limit) { + /* Step 1: create "bloated" hasher. */ + uint32_t num_slots = 1u << slot_bits; + uint32_t num_buckets = 1u << bucket_bits; + uint32_t hash_shift = 64u - bucket_bits; + uint64_t hash_mask = (~((uint64_t)0U)) >> (64 - hash_bits); + uint32_t slot_mask = num_slots - 1; + size_t alloc_size = (sizeof(uint32_t) << slot_bits) + + (sizeof(uint32_t) << slot_bits) + + (sizeof(uint16_t) << bucket_bits) + + (sizeof(uint32_t) << bucket_bits) + + (sizeof(uint32_t) * source_size); + uint8_t* flat = NULL; + PreparedDictionary* result = NULL; + uint16_t* num = NULL; + uint32_t* bucket_heads = NULL; + uint32_t* next_bucket = NULL; + uint32_t* slot_offsets = NULL; + uint16_t* heads = NULL; + uint32_t* items = NULL; + uint8_t** source_ref = NULL; + uint32_t i; + uint32_t* slot_size = NULL; + uint32_t* slot_limit = NULL; + uint32_t total_items = 0; + if (slot_bits > 16) return NULL; + if (slot_bits > bucket_bits) return NULL; + if (bucket_bits - slot_bits >= 16) return NULL; + + flat = BROTLI_ALLOC(m, uint8_t, alloc_size); + if (BROTLI_IS_OOM(m) || BROTLI_IS_NULL(flat)) return NULL; + + slot_size = (uint32_t*)flat; + slot_limit = (uint32_t*)(&slot_size[num_slots]); + num = (uint16_t*)(&slot_limit[num_slots]); + bucket_heads = (uint32_t*)(&num[num_buckets]); + next_bucket = (uint32_t*)(&bucket_heads[num_buckets]); + memset(num, 0, num_buckets * sizeof(num[0])); + + /* TODO(eustas): apply custom "store" order. */ + for (i = 0; i + 7 < source_size; ++i) { + const uint64_t h = (BROTLI_UNALIGNED_LOAD64LE(&source[i]) & hash_mask) * + kPreparedDictionaryHashMul64Long; + const uint32_t key = (uint32_t)(h >> hash_shift); + uint16_t count = num[key]; + next_bucket[i] = (count == 0) ? ((uint32_t)(-1)) : bucket_heads[key]; + bucket_heads[key] = i; + count++; + if (count > bucket_limit) count = bucket_limit; + num[key] = count; + } + + /* Step 2: find slot limits. */ + for (i = 0; i < num_slots; ++i) { + BROTLI_BOOL overflow = BROTLI_FALSE; + slot_limit[i] = bucket_limit; + while (BROTLI_TRUE) { + uint32_t limit = slot_limit[i]; + size_t j; + uint32_t count = 0; + overflow = BROTLI_FALSE; + for (j = i; j < num_buckets; j += num_slots) { + uint32_t size = num[j]; + /* Last chain may span behind 64K limit; overflow happens only if + we are about to use 0xFFFF+ as item offset. */ + if (count >= 0xFFFF) { + overflow = BROTLI_TRUE; + break; + } + if (size > limit) size = limit; + count += size; + } + if (!overflow) { + slot_size[i] = count; + total_items += count; + break; + } + slot_limit[i]--; + } + } + + /* Step 3: transfer data to "slim" hasher. */ + alloc_size = sizeof(PreparedDictionary) + (sizeof(uint32_t) << slot_bits) + + (sizeof(uint16_t) << bucket_bits) + (sizeof(uint32_t) * total_items) + + sizeof(uint8_t*); + + result = (PreparedDictionary*)BROTLI_ALLOC(m, uint8_t, alloc_size); + if (BROTLI_IS_OOM(m) || BROTLI_IS_NULL(result)) { + BROTLI_FREE(m, flat); + return NULL; + } + slot_offsets = (uint32_t*)(&result[1]); + heads = (uint16_t*)(&slot_offsets[num_slots]); + items = (uint32_t*)(&heads[num_buckets]); + source_ref = (uint8_t**)(&items[total_items]); + + result->magic = kLeanPreparedDictionaryMagic; + result->num_items = total_items; + result->source_size = (uint32_t)source_size; + result->hash_bits = hash_bits; + result->bucket_bits = bucket_bits; + result->slot_bits = slot_bits; + BROTLI_UNALIGNED_STORE_PTR(source_ref, source); + + total_items = 0; + for (i = 0; i < num_slots; ++i) { + slot_offsets[i] = total_items; + total_items += slot_size[i]; + slot_size[i] = 0; + } + for (i = 0; i < num_buckets; ++i) { + uint32_t slot = i & slot_mask; + uint32_t count = num[i]; + uint32_t pos; + size_t j; + size_t cursor = slot_size[slot]; + if (count > slot_limit[slot]) count = slot_limit[slot]; + if (count == 0) { + heads[i] = 0xFFFF; + continue; + } + heads[i] = (uint16_t)cursor; + cursor += slot_offsets[slot]; + slot_size[slot] += count; + pos = bucket_heads[i]; + for (j = 0; j < count; j++) { + items[cursor++] = pos; + pos = next_bucket[pos]; + } + items[cursor - 1] |= 0x80000000; + } + + BROTLI_FREE(m, flat); + return result; +} + +PreparedDictionary* CreatePreparedDictionary(MemoryManager* m, + const uint8_t* source, size_t source_size) { + uint32_t bucket_bits = 17; + uint32_t slot_bits = 7; + uint32_t hash_bits = 40; + uint16_t bucket_limit = 32; + size_t volume = 16u << bucket_bits; + /* Tune parameters to fit dictionary size. */ + while (volume < source_size && bucket_bits < 22) { + bucket_bits++; + slot_bits++; + volume <<= 1; + } + return CreatePreparedDictionaryWithParams(m, + source, source_size, bucket_bits, slot_bits, hash_bits, bucket_limit); +} + +void DestroyPreparedDictionary(MemoryManager* m, + PreparedDictionary* dictionary) { + if (!dictionary) return; + BROTLI_FREE(m, dictionary); +} + +BROTLI_BOOL AttachPreparedDictionary( + CompoundDictionary* compound, const PreparedDictionary* dictionary) { + size_t length = 0; + size_t index = 0; + + if (compound->num_chunks == SHARED_BROTLI_MAX_COMPOUND_DICTS) { + return BROTLI_FALSE; + } + + if (!dictionary) return BROTLI_FALSE; + + length = dictionary->source_size; + index = compound->num_chunks; + compound->total_size += length; + compound->chunks[index] = dictionary; + compound->chunk_offsets[index + 1] = compound->total_size; + { + uint32_t* slot_offsets = (uint32_t*)(&dictionary[1]); + uint16_t* heads = (uint16_t*)(&slot_offsets[(size_t)1u << dictionary->slot_bits]); + uint32_t* items = (uint32_t*)(&heads[(size_t)1u << dictionary->bucket_bits]); + const void* tail = (void*)&items[dictionary->num_items]; + if (dictionary->magic == kPreparedDictionaryMagic) { + compound->chunk_source[index] = (const uint8_t*)tail; + } else { + /* dictionary->magic == kLeanPreparedDictionaryMagic */ + compound->chunk_source[index] = + (const uint8_t*)BROTLI_UNALIGNED_LOAD_PTR((const uint8_t**)tail); + } + } + compound->num_chunks++; + return BROTLI_TRUE; +} diff --git a/packages/runtime/vendor/brotli/c/enc/compound_dictionary.h b/packages/runtime/vendor/brotli/c/enc/compound_dictionary.h new file mode 100644 index 000000000..686f92fda --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/compound_dictionary.h @@ -0,0 +1,71 @@ +/* Copyright 2017 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +#ifndef BROTLI_ENC_PREPARED_DICTIONARY_H_ +#define BROTLI_ENC_PREPARED_DICTIONARY_H_ + +#include "../common/platform.h" +#include +#include "memory.h" + +/* "Fat" prepared dictionary, could be cooked outside of C implementation, + * e.g. on Java side. LZ77 data is copied inside PreparedDictionary struct. */ +static const uint32_t kPreparedDictionaryMagic = 0xDEBCEDE0; + +static const uint32_t kSharedDictionaryMagic = 0xDEBCEDE1; + +static const uint32_t kManagedDictionaryMagic = 0xDEBCEDE2; + +/* "Lean" prepared dictionary. LZ77 data is referenced. It is the responsibility + * of caller of "prepare dictionary" to keep the LZ77 data while prepared + * dictionary is in use. */ +static const uint32_t kLeanPreparedDictionaryMagic = 0xDEBCEDE3; + +static const uint64_t kPreparedDictionaryHashMul64Long = + BROTLI_MAKE_UINT64_T(0x1FE35A7Bu, 0xD3579BD3u); + +typedef struct PreparedDictionary { + uint32_t magic; + uint32_t num_items; + uint32_t source_size; + uint32_t hash_bits; + uint32_t bucket_bits; + uint32_t slot_bits; + + /* --- Dynamic size members --- */ + + /* uint32_t slot_offsets[1 << slot_bits]; */ + /* uint16_t heads[1 << bucket_bits]; */ + /* uint32_t items[variable]; */ + + /* [maybe] uint8_t* source_ref, depending on magic. */ + /* [maybe] uint8_t source[source_size], depending on magic. */ +} PreparedDictionary; + +BROTLI_INTERNAL PreparedDictionary* CreatePreparedDictionary(MemoryManager* m, + const uint8_t* source, size_t source_size); + +BROTLI_INTERNAL void DestroyPreparedDictionary(MemoryManager* m, + PreparedDictionary* dictionary); + +typedef struct CompoundDictionary { + /* LZ77 prefix, compound dictionary */ + size_t num_chunks; + size_t total_size; + /* Client instances. */ + const PreparedDictionary* chunks[SHARED_BROTLI_MAX_COMPOUND_DICTS + 1]; + const uint8_t* chunk_source[SHARED_BROTLI_MAX_COMPOUND_DICTS + 1]; + size_t chunk_offsets[SHARED_BROTLI_MAX_COMPOUND_DICTS + 1]; + + size_t num_prepared_instances_; + /* Owned instances. */ + PreparedDictionary* prepared_instances_[SHARED_BROTLI_MAX_COMPOUND_DICTS + 1]; +} CompoundDictionary; + +BROTLI_INTERNAL BROTLI_BOOL AttachPreparedDictionary( + CompoundDictionary* compound, const PreparedDictionary* dictionary); + +#endif /* BROTLI_ENC_PREPARED_DICTIONARY */ diff --git a/packages/runtime/vendor/brotli/c/enc/compress_fragment.c b/packages/runtime/vendor/brotli/c/enc/compress_fragment.c new file mode 100644 index 000000000..14fec4fbf --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/compress_fragment.c @@ -0,0 +1,790 @@ +/* Copyright 2015 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* Function for fast encoding of an input fragment, independently from the input + history. This function uses one-pass processing: when we find a backward + match, we immediately emit the corresponding command and literal codes to + the bit stream. + + Adapted from the CompressFragment() function in + https://github.com/google/snappy/blob/master/snappy.cc */ + +#include "compress_fragment.h" + +#include "../common/constants.h" +#include "../common/platform.h" +#include "brotli_bit_stream.h" +#include "entropy_encode.h" +#include "fast_log.h" +#include "find_match_length.h" +#include "hash_base.h" +#include "write_bits.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +#define MAX_DISTANCE (long)BROTLI_MAX_BACKWARD_LIMIT(18) + +static BROTLI_INLINE uint32_t Hash(const uint8_t* p, size_t shift) { + const uint64_t h = (BROTLI_UNALIGNED_LOAD64LE(p) << 24) * kHashMul32; + return (uint32_t)(h >> shift); +} + +static BROTLI_INLINE uint32_t HashBytesAtOffset( + uint64_t v, int offset, size_t shift) { + BROTLI_DCHECK(offset >= 0); + BROTLI_DCHECK(offset <= 3); + { + const uint64_t h = ((v >> (8 * offset)) << 24) * kHashMul32; + return (uint32_t)(h >> shift); + } +} + +static BROTLI_INLINE BROTLI_BOOL IsMatch(const uint8_t* p1, const uint8_t* p2) { + return TO_BROTLI_BOOL( + BrotliUnalignedRead32(p1) == BrotliUnalignedRead32(p2) && + p1[4] == p2[4]); +} + +/* Builds a literal prefix code into "depths" and "bits" based on the statistics + of the "input" string and stores it into the bit stream. + Note that the prefix code here is built from the pre-LZ77 input, therefore + we can only approximate the statistics of the actual literal stream. + Moreover, for long inputs we build a histogram from a sample of the input + and thus have to assign a non-zero depth for each literal. + Returns estimated compression ratio millibytes/char for encoding given input + with generated code. */ +static size_t BuildAndStoreLiteralPrefixCode(BrotliOnePassArena* s, + const uint8_t* input, + const size_t input_size, + uint8_t depths[256], + uint16_t bits[256], + size_t* storage_ix, + uint8_t* storage) { + uint32_t* BROTLI_RESTRICT const histogram = s->histogram; + size_t histogram_total; + size_t i; + memset(histogram, 0, sizeof(s->histogram)); + + if (input_size < (1 << 15)) { + for (i = 0; i < input_size; ++i) { + ++histogram[input[i]]; + } + histogram_total = input_size; + for (i = 0; i < 256; ++i) { + /* We weigh the first 11 samples with weight 3 to account for the + balancing effect of the LZ77 phase on the histogram. */ + const uint32_t adjust = 2 * BROTLI_MIN(uint32_t, histogram[i], 11u); + histogram[i] += adjust; + histogram_total += adjust; + } + } else { + static const size_t kSampleRate = 29; + for (i = 0; i < input_size; i += kSampleRate) { + ++histogram[input[i]]; + } + histogram_total = (input_size + kSampleRate - 1) / kSampleRate; + for (i = 0; i < 256; ++i) { + /* We add 1 to each population count to avoid 0 bit depths (since this is + only a sample and we don't know if the symbol appears or not), and we + weigh the first 11 samples with weight 3 to account for the balancing + effect of the LZ77 phase on the histogram (more frequent symbols are + more likely to be in backward references instead as literals). */ + const uint32_t adjust = 1 + 2 * BROTLI_MIN(uint32_t, histogram[i], 11u); + histogram[i] += adjust; + histogram_total += adjust; + } + } + BrotliBuildAndStoreHuffmanTreeFast(s->tree, histogram, histogram_total, + /* max_bits = */ 8, + depths, bits, storage_ix, storage); + { + size_t literal_ratio = 0; + for (i = 0; i < 256; ++i) { + if (histogram[i]) literal_ratio += histogram[i] * depths[i]; + } + /* Estimated encoding ratio, millibytes per symbol. */ + return (literal_ratio * 125) / histogram_total; + } +} + +/* Builds a command and distance prefix code (each 64 symbols) into "depth" and + "bits" based on "histogram" and stores it into the bit stream. */ +static void BuildAndStoreCommandPrefixCode(BrotliOnePassArena* s, + size_t* storage_ix, uint8_t* storage) { + const uint32_t* const histogram = s->cmd_histo; + uint8_t* const depth = s->cmd_depth; + uint16_t* const bits = s->cmd_bits; + uint8_t* BROTLI_RESTRICT const tmp_depth = s->tmp_depth; + uint16_t* BROTLI_RESTRICT const tmp_bits = s->tmp_bits; + /* TODO(eustas): do only once on initialization. */ + memset(tmp_depth, 0, BROTLI_NUM_COMMAND_SYMBOLS); + + BrotliCreateHuffmanTree(histogram, 64, 15, s->tree, depth); + BrotliCreateHuffmanTree(&histogram[64], 64, 14, s->tree, &depth[64]); + /* We have to jump through a few hoops here in order to compute + the command bits because the symbols are in a different order than in + the full alphabet. This looks complicated, but having the symbols + in this order in the command bits saves a few branches in the Emit* + functions. */ + memcpy(tmp_depth, depth, 24); + memcpy(tmp_depth + 24, depth + 40, 8); + memcpy(tmp_depth + 32, depth + 24, 8); + memcpy(tmp_depth + 40, depth + 48, 8); + memcpy(tmp_depth + 48, depth + 32, 8); + memcpy(tmp_depth + 56, depth + 56, 8); + BrotliConvertBitDepthsToSymbols(tmp_depth, 64, tmp_bits); + memcpy(bits, tmp_bits, 48); + memcpy(bits + 24, tmp_bits + 32, 16); + memcpy(bits + 32, tmp_bits + 48, 16); + memcpy(bits + 40, tmp_bits + 24, 16); + memcpy(bits + 48, tmp_bits + 40, 16); + memcpy(bits + 56, tmp_bits + 56, 16); + BrotliConvertBitDepthsToSymbols(&depth[64], 64, &bits[64]); + { + /* Create the bit length array for the full command alphabet. */ + size_t i; + memset(tmp_depth, 0, 64); /* only 64 first values were used */ + memcpy(tmp_depth, depth, 8); + memcpy(tmp_depth + 64, depth + 8, 8); + memcpy(tmp_depth + 128, depth + 16, 8); + memcpy(tmp_depth + 192, depth + 24, 8); + memcpy(tmp_depth + 384, depth + 32, 8); + for (i = 0; i < 8; ++i) { + tmp_depth[128 + 8 * i] = depth[40 + i]; + tmp_depth[256 + 8 * i] = depth[48 + i]; + tmp_depth[448 + 8 * i] = depth[56 + i]; + } + /* TODO(eustas): could/should full-length machinery be avoided? */ + BrotliStoreHuffmanTree( + tmp_depth, BROTLI_NUM_COMMAND_SYMBOLS, s->tree, storage_ix, storage); + } + BrotliStoreHuffmanTree(&depth[64], 64, s->tree, storage_ix, storage); +} + +/* REQUIRES: insertlen < 6210 */ +static BROTLI_INLINE void EmitInsertLen(size_t insertlen, + const uint8_t depth[128], + const uint16_t bits[128], + uint32_t histo[128], + size_t* storage_ix, + uint8_t* storage) { + if (insertlen < 6) { + const size_t code = insertlen + 40; + BrotliWriteBits(depth[code], bits[code], storage_ix, storage); + ++histo[code]; + } else if (insertlen < 130) { + const size_t tail = insertlen - 2; + const uint32_t nbits = Log2FloorNonZero(tail) - 1u; + const size_t prefix = tail >> nbits; + const size_t inscode = (nbits << 1) + prefix + 42; + BrotliWriteBits(depth[inscode], bits[inscode], storage_ix, storage); + BrotliWriteBits(nbits, tail - (prefix << nbits), storage_ix, storage); + ++histo[inscode]; + } else if (insertlen < 2114) { + const size_t tail = insertlen - 66; + const uint32_t nbits = Log2FloorNonZero(tail); + const size_t code = nbits + 50; + BrotliWriteBits(depth[code], bits[code], storage_ix, storage); + BrotliWriteBits(nbits, tail - ((size_t)1 << nbits), storage_ix, storage); + ++histo[code]; + } else { + BrotliWriteBits(depth[61], bits[61], storage_ix, storage); + BrotliWriteBits(12, insertlen - 2114, storage_ix, storage); + ++histo[61]; + } +} + +static BROTLI_INLINE void EmitLongInsertLen(size_t insertlen, + const uint8_t depth[128], + const uint16_t bits[128], + uint32_t histo[128], + size_t* storage_ix, + uint8_t* storage) { + if (insertlen < 22594) { + BrotliWriteBits(depth[62], bits[62], storage_ix, storage); + BrotliWriteBits(14, insertlen - 6210, storage_ix, storage); + ++histo[62]; + } else { + BrotliWriteBits(depth[63], bits[63], storage_ix, storage); + BrotliWriteBits(24, insertlen - 22594, storage_ix, storage); + ++histo[63]; + } +} + +static BROTLI_INLINE void EmitCopyLen(size_t copylen, + const uint8_t depth[128], + const uint16_t bits[128], + uint32_t histo[128], + size_t* storage_ix, + uint8_t* storage) { + if (copylen < 10) { + BrotliWriteBits( + depth[copylen + 14], bits[copylen + 14], storage_ix, storage); + ++histo[copylen + 14]; + } else if (copylen < 134) { + const size_t tail = copylen - 6; + const uint32_t nbits = Log2FloorNonZero(tail) - 1u; + const size_t prefix = tail >> nbits; + const size_t code = (nbits << 1) + prefix + 20; + BrotliWriteBits(depth[code], bits[code], storage_ix, storage); + BrotliWriteBits(nbits, tail - (prefix << nbits), storage_ix, storage); + ++histo[code]; + } else if (copylen < 2118) { + const size_t tail = copylen - 70; + const uint32_t nbits = Log2FloorNonZero(tail); + const size_t code = nbits + 28; + BrotliWriteBits(depth[code], bits[code], storage_ix, storage); + BrotliWriteBits(nbits, tail - ((size_t)1 << nbits), storage_ix, storage); + ++histo[code]; + } else { + BrotliWriteBits(depth[39], bits[39], storage_ix, storage); + BrotliWriteBits(24, copylen - 2118, storage_ix, storage); + ++histo[39]; + } +} + +static BROTLI_INLINE void EmitCopyLenLastDistance(size_t copylen, + const uint8_t depth[128], + const uint16_t bits[128], + uint32_t histo[128], + size_t* storage_ix, + uint8_t* storage) { + if (copylen < 12) { + BrotliWriteBits(depth[copylen - 4], bits[copylen - 4], storage_ix, storage); + ++histo[copylen - 4]; + } else if (copylen < 72) { + const size_t tail = copylen - 8; + const uint32_t nbits = Log2FloorNonZero(tail) - 1; + const size_t prefix = tail >> nbits; + const size_t code = (nbits << 1) + prefix + 4; + BrotliWriteBits(depth[code], bits[code], storage_ix, storage); + BrotliWriteBits(nbits, tail - (prefix << nbits), storage_ix, storage); + ++histo[code]; + } else if (copylen < 136) { + const size_t tail = copylen - 8; + const size_t code = (tail >> 5) + 30; + BrotliWriteBits(depth[code], bits[code], storage_ix, storage); + BrotliWriteBits(5, tail & 31, storage_ix, storage); + BrotliWriteBits(depth[64], bits[64], storage_ix, storage); + ++histo[code]; + ++histo[64]; + } else if (copylen < 2120) { + const size_t tail = copylen - 72; + const uint32_t nbits = Log2FloorNonZero(tail); + const size_t code = nbits + 28; + BrotliWriteBits(depth[code], bits[code], storage_ix, storage); + BrotliWriteBits(nbits, tail - ((size_t)1 << nbits), storage_ix, storage); + BrotliWriteBits(depth[64], bits[64], storage_ix, storage); + ++histo[code]; + ++histo[64]; + } else { + BrotliWriteBits(depth[39], bits[39], storage_ix, storage); + BrotliWriteBits(24, copylen - 2120, storage_ix, storage); + BrotliWriteBits(depth[64], bits[64], storage_ix, storage); + ++histo[39]; + ++histo[64]; + } +} + +static BROTLI_INLINE void EmitDistance(size_t distance, + const uint8_t depth[128], + const uint16_t bits[128], + uint32_t histo[128], + size_t* storage_ix, uint8_t* storage) { + const size_t d = distance + 3; + const uint32_t nbits = Log2FloorNonZero(d) - 1u; + const size_t prefix = (d >> nbits) & 1; + const size_t offset = (2 + prefix) << nbits; + const size_t distcode = 2 * (nbits - 1) + prefix + 80; + BrotliWriteBits(depth[distcode], bits[distcode], storage_ix, storage); + BrotliWriteBits(nbits, d - offset, storage_ix, storage); + ++histo[distcode]; +} + +static BROTLI_INLINE void EmitLiterals(const uint8_t* input, const size_t len, + const uint8_t depth[256], + const uint16_t bits[256], + size_t* storage_ix, uint8_t* storage) { + size_t j; + for (j = 0; j < len; j++) { + const uint8_t lit = input[j]; + BrotliWriteBits(depth[lit], bits[lit], storage_ix, storage); + } +} + +/* REQUIRES: len <= 1 << 24. */ +static void BrotliStoreMetaBlockHeader( + size_t len, BROTLI_BOOL is_uncompressed, size_t* storage_ix, + uint8_t* storage) { + size_t nibbles = 6; + /* ISLAST */ + BrotliWriteBits(1, 0, storage_ix, storage); + if (len <= (1U << 16)) { + nibbles = 4; + } else if (len <= (1U << 20)) { + nibbles = 5; + } + BrotliWriteBits(2, nibbles - 4, storage_ix, storage); + BrotliWriteBits(nibbles * 4, len - 1, storage_ix, storage); + /* ISUNCOMPRESSED */ + BrotliWriteBits(1, (uint64_t)is_uncompressed, storage_ix, storage); +} + +static void UpdateBits(size_t n_bits, uint32_t bits, size_t pos, + uint8_t* array) { + while (n_bits > 0) { + size_t byte_pos = pos >> 3; + size_t n_unchanged_bits = pos & 7; + size_t n_changed_bits = BROTLI_MIN(size_t, n_bits, 8 - n_unchanged_bits); + size_t total_bits = n_unchanged_bits + n_changed_bits; + uint32_t mask = + (~((1u << total_bits) - 1u)) | ((1u << n_unchanged_bits) - 1u); + uint32_t unchanged_bits = array[byte_pos] & mask; + uint32_t changed_bits = bits & ((1u << n_changed_bits) - 1u); + array[byte_pos] = + (uint8_t)((changed_bits << n_unchanged_bits) | unchanged_bits); + n_bits -= n_changed_bits; + bits >>= n_changed_bits; + pos += n_changed_bits; + } +} + +static void RewindBitPosition(const size_t new_storage_ix, + size_t* storage_ix, uint8_t* storage) { + const size_t bitpos = new_storage_ix & 7; + const size_t mask = (1u << bitpos) - 1; + storage[new_storage_ix >> 3] &= (uint8_t)mask; + *storage_ix = new_storage_ix; +} + +static BROTLI_BOOL ShouldMergeBlock(BrotliOnePassArena* s, + const uint8_t* data, size_t len, const uint8_t* depths) { + uint32_t* BROTLI_RESTRICT const histo = s->histogram; + static const size_t kSampleRate = 43; + size_t i; + memset(histo, 0, sizeof(s->histogram)); + for (i = 0; i < len; i += kSampleRate) { + ++histo[data[i]]; + } + { + const size_t total = (len + kSampleRate - 1) / kSampleRate; + double r = (FastLog2(total) + 0.5) * (double)total + 200; + for (i = 0; i < 256; ++i) { + r -= (double)histo[i] * (depths[i] + FastLog2(histo[i])); + } + return TO_BROTLI_BOOL(r >= 0.0); + } +} + +/* Acceptable loss for uncompressible speedup is 2% */ +#define MIN_RATIO 980 + +static BROTLI_INLINE BROTLI_BOOL ShouldUseUncompressedMode( + const uint8_t* metablock_start, const uint8_t* next_emit, + const size_t insertlen, const size_t literal_ratio) { + const size_t compressed = (size_t)(next_emit - metablock_start); + if (compressed * 50 > insertlen) { + return BROTLI_FALSE; + } else { + return TO_BROTLI_BOOL(literal_ratio > MIN_RATIO); + } +} + +static void EmitUncompressedMetaBlock(const uint8_t* begin, const uint8_t* end, + const size_t storage_ix_start, + size_t* storage_ix, uint8_t* storage) { + const size_t len = (size_t)(end - begin); + RewindBitPosition(storage_ix_start, storage_ix, storage); + BrotliStoreMetaBlockHeader(len, 1, storage_ix, storage); + *storage_ix = (*storage_ix + 7u) & ~7u; + memcpy(&storage[*storage_ix >> 3], begin, len); + *storage_ix += len << 3; + storage[*storage_ix >> 3] = 0; +} + +static BROTLI_MODEL("small") uint32_t kCmdHistoSeed[128] = { + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 0, 0, 0, 0, +}; + +static BROTLI_INLINE void BrotliCompressFragmentFastImpl( + BrotliOnePassArena* s, const uint8_t* input, size_t input_size, + BROTLI_BOOL is_last, int* table, size_t table_bits, + size_t* storage_ix, uint8_t* storage) { + uint8_t* BROTLI_RESTRICT const cmd_depth = s->cmd_depth; + uint16_t* BROTLI_RESTRICT const cmd_bits = s->cmd_bits; + uint32_t* BROTLI_RESTRICT const cmd_histo = s->cmd_histo; + uint8_t* BROTLI_RESTRICT const lit_depth = s->lit_depth; + uint16_t* BROTLI_RESTRICT const lit_bits = s->lit_bits; + const uint8_t* ip_end; + + /* "next_emit" is a pointer to the first byte that is not covered by a + previous copy. Bytes between "next_emit" and the start of the next copy or + the end of the input will be emitted as literal bytes. */ + const uint8_t* next_emit = input; + /* Save the start of the first block for position and distance computations. + */ + const uint8_t* base_ip = input; + + static const size_t kFirstBlockSize = 3 << 15; + static const size_t kMergeBlockSize = 1 << 16; + + const size_t kInputMarginBytes = BROTLI_WINDOW_GAP; + const size_t kMinMatchLen = 5; + + const uint8_t* metablock_start = input; + size_t block_size = BROTLI_MIN(size_t, input_size, kFirstBlockSize); + size_t total_block_size = block_size; + /* Save the bit position of the MLEN field of the meta-block header, so that + we can update it later if we decide to extend this meta-block. */ + size_t mlen_storage_ix = *storage_ix + 3; + + size_t literal_ratio; + + const uint8_t* ip; + int last_distance; + + const size_t shift = 64u - table_bits; + + BrotliStoreMetaBlockHeader(block_size, 0, storage_ix, storage); + /* No block splits, no contexts. */ + BrotliWriteBits(13, 0, storage_ix, storage); + + literal_ratio = BuildAndStoreLiteralPrefixCode( + s, input, block_size, s->lit_depth, s->lit_bits, storage_ix, storage); + + { + /* Store the pre-compressed command and distance prefix codes. */ + size_t i; + for (i = 0; i + 7 < s->cmd_code_numbits; i += 8) { + BrotliWriteBits(8, s->cmd_code[i >> 3], storage_ix, storage); + } + } + BrotliWriteBits(s->cmd_code_numbits & 7, + s->cmd_code[s->cmd_code_numbits >> 3], storage_ix, storage); + + emit_commands: + /* Initialize the command and distance histograms. We will gather + statistics of command and distance codes during the processing + of this block and use it to update the command and distance + prefix codes for the next block. */ + memcpy(s->cmd_histo, kCmdHistoSeed, sizeof(kCmdHistoSeed)); + + /* "ip" is the input pointer. */ + ip = input; + last_distance = -1; + ip_end = input + block_size; + + if (BROTLI_PREDICT_TRUE(block_size >= kInputMarginBytes)) { + /* For the last block, we need to keep a 16 bytes margin so that we can be + sure that all distances are at most window size - 16. + For all other blocks, we only need to keep a margin of 5 bytes so that + we don't go over the block size with a copy. */ + const size_t len_limit = BROTLI_MIN(size_t, block_size - kMinMatchLen, + input_size - kInputMarginBytes); + const uint8_t* ip_limit = input + len_limit; + + uint32_t next_hash; + for (next_hash = Hash(++ip, shift); ; ) { + /* Step 1: Scan forward in the input looking for a 5-byte-long match. + If we get close to exhausting the input then goto emit_remainder. + + Heuristic match skipping: If 32 bytes are scanned with no matches + found, start looking only at every other byte. If 32 more bytes are + scanned, look at every third byte, etc.. When a match is found, + immediately go back to looking at every byte. This is a small loss + (~5% performance, ~0.1% density) for compressible data due to more + bookkeeping, but for non-compressible data (such as JPEG) it's a huge + win since the compressor quickly "realizes" the data is incompressible + and doesn't bother looking for matches everywhere. + + The "skip" variable keeps track of how many bytes there are since the + last match; dividing it by 32 (i.e. right-shifting by five) gives the + number of bytes to move ahead for each iteration. */ + uint32_t skip = 32; + + const uint8_t* next_ip = ip; + const uint8_t* candidate; + BROTLI_DCHECK(next_emit < ip); +trawl: + do { + uint32_t hash = next_hash; + uint32_t bytes_between_hash_lookups = skip++ >> 5; + BROTLI_DCHECK(hash == Hash(next_ip, shift)); + ip = next_ip; + next_ip = ip + bytes_between_hash_lookups; + if (BROTLI_PREDICT_FALSE(next_ip > ip_limit)) { + goto emit_remainder; + } + next_hash = Hash(next_ip, shift); + candidate = ip - last_distance; + if (IsMatch(ip, candidate)) { + if (BROTLI_PREDICT_TRUE(candidate < ip)) { + table[hash] = (int)(ip - base_ip); + break; + } + } + candidate = base_ip + table[hash]; + BROTLI_DCHECK(candidate >= base_ip); + BROTLI_DCHECK(candidate < ip); + + table[hash] = (int)(ip - base_ip); + } while (BROTLI_PREDICT_TRUE(!IsMatch(ip, candidate))); + + /* Check copy distance. If candidate is not feasible, continue search. + Checking is done outside of hot loop to reduce overhead. */ + if (ip - candidate > MAX_DISTANCE) goto trawl; + + /* Step 2: Emit the found match together with the literal bytes from + "next_emit" to the bit stream, and then see if we can find a next match + immediately afterwards. Repeat until we find no match for the input + without emitting some literal bytes. */ + + { + /* We have a 5-byte match at ip, and we need to emit bytes in + [next_emit, ip). */ + const uint8_t* base = ip; + size_t matched = 5 + FindMatchLengthWithLimit( + candidate + 5, ip + 5, (size_t)(ip_end - ip) - 5); + int distance = (int)(base - candidate); /* > 0 */ + size_t insert = (size_t)(base - next_emit); + ip += matched; + BROTLI_LOG(("[CompressFragment] pos = %d insert = %lu copy = %d\n", + (int)(next_emit - base_ip), (unsigned long)insert, 2)); + BROTLI_DCHECK(0 == memcmp(base, candidate, matched)); + if (BROTLI_PREDICT_TRUE(insert < 6210)) { + EmitInsertLen(insert, cmd_depth, cmd_bits, cmd_histo, + storage_ix, storage); + } else if (ShouldUseUncompressedMode(metablock_start, next_emit, insert, + literal_ratio)) { + EmitUncompressedMetaBlock(metablock_start, base, mlen_storage_ix - 3, + storage_ix, storage); + input_size -= (size_t)(base - input); + input = base; + next_emit = input; + goto next_block; + } else { + EmitLongInsertLen(insert, cmd_depth, cmd_bits, cmd_histo, + storage_ix, storage); + } + EmitLiterals(next_emit, insert, lit_depth, lit_bits, + storage_ix, storage); + if (distance == last_distance) { + BrotliWriteBits(cmd_depth[64], cmd_bits[64], storage_ix, storage); + ++cmd_histo[64]; + } else { + EmitDistance((size_t)distance, cmd_depth, cmd_bits, + cmd_histo, storage_ix, storage); + last_distance = distance; + } + EmitCopyLenLastDistance(matched, cmd_depth, cmd_bits, cmd_histo, + storage_ix, storage); + BROTLI_LOG(("[CompressFragment] pos = %d distance = %d\n" + "[CompressFragment] pos = %d insert = %d copy = %d\n" + "[CompressFragment] pos = %d distance = %d\n", + (int)(base - base_ip), (int)distance, + (int)(base - base_ip) + 2, 0, (int)matched - 2, + (int)(base - base_ip) + 2, (int)distance)); + + next_emit = ip; + if (BROTLI_PREDICT_FALSE(ip >= ip_limit)) { + goto emit_remainder; + } + /* We could immediately start working at ip now, but to improve + compression we first update "table" with the hashes of some positions + within the last copy. */ + { + uint64_t input_bytes = BROTLI_UNALIGNED_LOAD64LE(ip - 3); + uint32_t prev_hash = HashBytesAtOffset(input_bytes, 0, shift); + uint32_t cur_hash = HashBytesAtOffset(input_bytes, 3, shift); + table[prev_hash] = (int)(ip - base_ip - 3); + prev_hash = HashBytesAtOffset(input_bytes, 1, shift); + table[prev_hash] = (int)(ip - base_ip - 2); + prev_hash = HashBytesAtOffset(input_bytes, 2, shift); + table[prev_hash] = (int)(ip - base_ip - 1); + + candidate = base_ip + table[cur_hash]; + table[cur_hash] = (int)(ip - base_ip); + } + } + + while (IsMatch(ip, candidate)) { + /* We have a 5-byte match at ip, and no need to emit any literal bytes + prior to ip. */ + const uint8_t* base = ip; + size_t matched = 5 + FindMatchLengthWithLimit( + candidate + 5, ip + 5, (size_t)(ip_end - ip) - 5); + if (ip - candidate > MAX_DISTANCE) break; + ip += matched; + last_distance = (int)(base - candidate); /* > 0 */ + BROTLI_DCHECK(0 == memcmp(base, candidate, matched)); + EmitCopyLen(matched, cmd_depth, cmd_bits, cmd_histo, + storage_ix, storage); + EmitDistance((size_t)last_distance, cmd_depth, cmd_bits, + cmd_histo, storage_ix, storage); + BROTLI_LOG(("[CompressFragment] pos = %d insert = %d copy = %d\n" + "[CompressFragment] pos = %d distance = %d\n", + (int)(base - base_ip), 0, (int)matched, + (int)(base - base_ip), (int)last_distance)); + + next_emit = ip; + if (BROTLI_PREDICT_FALSE(ip >= ip_limit)) { + goto emit_remainder; + } + /* We could immediately start working at ip now, but to improve + compression we first update "table" with the hashes of some positions + within the last copy. */ + { + uint64_t input_bytes = BROTLI_UNALIGNED_LOAD64LE(ip - 3); + uint32_t prev_hash = HashBytesAtOffset(input_bytes, 0, shift); + uint32_t cur_hash = HashBytesAtOffset(input_bytes, 3, shift); + table[prev_hash] = (int)(ip - base_ip - 3); + prev_hash = HashBytesAtOffset(input_bytes, 1, shift); + table[prev_hash] = (int)(ip - base_ip - 2); + prev_hash = HashBytesAtOffset(input_bytes, 2, shift); + table[prev_hash] = (int)(ip - base_ip - 1); + + candidate = base_ip + table[cur_hash]; + table[cur_hash] = (int)(ip - base_ip); + } + } + + next_hash = Hash(++ip, shift); + } + } + + emit_remainder: + BROTLI_DCHECK(next_emit <= ip_end); + input += block_size; + input_size -= block_size; + block_size = BROTLI_MIN(size_t, input_size, kMergeBlockSize); + + /* Decide if we want to continue this meta-block instead of emitting the + last insert-only command. */ + if (input_size > 0 && + total_block_size + block_size <= (1 << 20) && + ShouldMergeBlock(s, input, block_size, lit_depth)) { + BROTLI_DCHECK(total_block_size > (1 << 16)); + /* Update the size of the current meta-block and continue emitting commands. + We can do this because the current size and the new size both have 5 + nibbles. */ + total_block_size += block_size; + UpdateBits(20, (uint32_t)(total_block_size - 1), mlen_storage_ix, storage); + goto emit_commands; + } + + /* Emit the remaining bytes as literals. */ + if (next_emit < ip_end) { + const size_t insert = (size_t)(ip_end - next_emit); + BROTLI_LOG(("[CompressFragment] pos = %d insert = %lu copy = %d\n", + (int)(next_emit - base_ip), (unsigned long)insert, 2)); + if (BROTLI_PREDICT_TRUE(insert < 6210)) { + EmitInsertLen(insert, cmd_depth, cmd_bits, cmd_histo, + storage_ix, storage); + EmitLiterals(next_emit, insert, lit_depth, lit_bits, storage_ix, storage); + } else if (ShouldUseUncompressedMode(metablock_start, next_emit, insert, + literal_ratio)) { + EmitUncompressedMetaBlock(metablock_start, ip_end, mlen_storage_ix - 3, + storage_ix, storage); + } else { + EmitLongInsertLen(insert, cmd_depth, cmd_bits, cmd_histo, + storage_ix, storage); + EmitLiterals(next_emit, insert, lit_depth, lit_bits, + storage_ix, storage); + } + } + next_emit = ip_end; + +next_block: + /* If we have more data, write a new meta-block header and prefix codes and + then continue emitting commands. */ + if (input_size > 0) { + metablock_start = input; + block_size = BROTLI_MIN(size_t, input_size, kFirstBlockSize); + total_block_size = block_size; + /* Save the bit position of the MLEN field of the meta-block header, so that + we can update it later if we decide to extend this meta-block. */ + mlen_storage_ix = *storage_ix + 3; + BrotliStoreMetaBlockHeader(block_size, 0, storage_ix, storage); + /* No block splits, no contexts. */ + BrotliWriteBits(13, 0, storage_ix, storage); + literal_ratio = BuildAndStoreLiteralPrefixCode( + s, input, block_size, lit_depth, lit_bits, storage_ix, storage); + BuildAndStoreCommandPrefixCode(s, storage_ix, storage); + goto emit_commands; + } + + if (!is_last) { + /* If this is not the last block, update the command and distance prefix + codes for the next block and store the compressed forms. */ + s->cmd_code[0] = 0; + s->cmd_code_numbits = 0; + BuildAndStoreCommandPrefixCode(s, &s->cmd_code_numbits, s->cmd_code); + } +} + +#define FOR_TABLE_BITS_(X) X(9) X(11) X(13) X(15) + +#define BAKE_METHOD_PARAM_(B) \ +static BROTLI_NOINLINE void BrotliCompressFragmentFastImpl ## B( \ + BrotliOnePassArena* s, const uint8_t* input, size_t input_size, \ + BROTLI_BOOL is_last, int* table, size_t* storage_ix, uint8_t* storage) { \ + BrotliCompressFragmentFastImpl(s, input, input_size, is_last, table, B, \ + storage_ix, storage); \ +} +FOR_TABLE_BITS_(BAKE_METHOD_PARAM_) +#undef BAKE_METHOD_PARAM_ + +void BrotliCompressFragmentFast( + BrotliOnePassArena* s, const uint8_t* input, size_t input_size, + BROTLI_BOOL is_last, int* table, size_t table_size, + size_t* storage_ix, uint8_t* storage) { + const size_t initial_storage_ix = *storage_ix; + const size_t table_bits = Log2FloorNonZero(table_size); + + if (input_size == 0) { + BROTLI_DCHECK(is_last); + BrotliWriteBits(1, 1, storage_ix, storage); /* islast */ + BrotliWriteBits(1, 1, storage_ix, storage); /* isempty */ + *storage_ix = (*storage_ix + 7u) & ~7u; + return; + } + + switch (table_bits) { +#define CASE_(B) \ + case B: \ + BrotliCompressFragmentFastImpl ## B( \ + s, input, input_size, is_last, table, storage_ix, storage);\ + break; + FOR_TABLE_BITS_(CASE_) +#undef CASE_ + default: BROTLI_DCHECK(0); break; + } + + /* If output is larger than single uncompressed block, rewrite it. */ + if (*storage_ix - initial_storage_ix > 31 + (input_size << 3)) { + EmitUncompressedMetaBlock(input, input + input_size, initial_storage_ix, + storage_ix, storage); + } + + if (is_last) { + BrotliWriteBits(1, 1, storage_ix, storage); /* islast */ + BrotliWriteBits(1, 1, storage_ix, storage); /* isempty */ + *storage_ix = (*storage_ix + 7u) & ~7u; + } +} + +#undef FOR_TABLE_BITS_ + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif diff --git a/packages/runtime/vendor/brotli/c/enc/compress_fragment.h b/packages/runtime/vendor/brotli/c/enc/compress_fragment.h new file mode 100644 index 000000000..462f366b3 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/compress_fragment.h @@ -0,0 +1,84 @@ +/* Copyright 2015 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* Function for fast encoding of an input fragment, independently from the input + history. This function uses one-pass processing: when we find a backward + match, we immediately emit the corresponding command and literal codes to + the bit stream. */ + +#ifndef BROTLI_ENC_COMPRESS_FRAGMENT_H_ +#define BROTLI_ENC_COMPRESS_FRAGMENT_H_ + +#include "../common/constants.h" +#include "../common/platform.h" +#include "entropy_encode.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +typedef struct BrotliOnePassArena { + uint8_t lit_depth[256]; + uint16_t lit_bits[256]; + + /* Command and distance prefix codes (each 64 symbols, stored back-to-back) + used for the next block. The command prefix code is over a smaller alphabet + with the following 64 symbols: + 0 - 15: insert length code 0, copy length code 0 - 15, same distance + 16 - 39: insert length code 0, copy length code 0 - 23 + 40 - 63: insert length code 0 - 23, copy length code 0 + Note that symbols 16 and 40 represent the same code in the full alphabet, + but we do not use either of them. */ + uint8_t cmd_depth[128]; + uint16_t cmd_bits[128]; + uint32_t cmd_histo[128]; + + /* The compressed form of the command and distance prefix codes for the next + block. */ + uint8_t cmd_code[512]; + size_t cmd_code_numbits; + + HuffmanTree tree[2 * BROTLI_NUM_LITERAL_SYMBOLS + 1]; + uint32_t histogram[256]; + uint8_t tmp_depth[BROTLI_NUM_COMMAND_SYMBOLS]; + uint16_t tmp_bits[64]; +} BrotliOnePassArena; + +/* Compresses "input" string to the "*storage" buffer as one or more complete + meta-blocks, and updates the "*storage_ix" bit position. + + If "is_last" is 1, emits an additional empty last meta-block. + + "cmd_depth" and "cmd_bits" contain the command and distance prefix codes + (see comment in encode.h) used for the encoding of this input fragment. + If "is_last" is 0, they are updated to reflect the statistics + of this input fragment, to be used for the encoding of the next fragment. + + "*cmd_code_numbits" is the number of bits of the compressed representation + of the command and distance prefix codes, and "cmd_code" is an array of + at least "(*cmd_code_numbits + 7) >> 3" size that contains the compressed + command and distance prefix codes. If "is_last" is 0, these are also + updated to represent the updated "cmd_depth" and "cmd_bits". + + REQUIRES: "input_size" is greater than zero, or "is_last" is 1. + REQUIRES: "input_size" is less or equal to maximal metablock size (1 << 24). + REQUIRES: All elements in "table[0..table_size-1]" are initialized to zero. + REQUIRES: "table_size" is an odd (9, 11, 13, 15) power of two + OUTPUT: maximal copy distance <= |input_size| + OUTPUT: maximal copy distance <= BROTLI_MAX_BACKWARD_LIMIT(18) */ +BROTLI_INTERNAL void BrotliCompressFragmentFast(BrotliOnePassArena* s, + const uint8_t* input, + size_t input_size, + BROTLI_BOOL is_last, + int* table, size_t table_size, + size_t* storage_ix, + uint8_t* storage); + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif + +#endif /* BROTLI_ENC_COMPRESS_FRAGMENT_H_ */ diff --git a/packages/runtime/vendor/brotli/c/enc/compress_fragment_two_pass.c b/packages/runtime/vendor/brotli/c/enc/compress_fragment_two_pass.c new file mode 100644 index 000000000..01e4794d8 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/compress_fragment_two_pass.c @@ -0,0 +1,647 @@ +/* Copyright 2015 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* Function for fast encoding of an input fragment, independently from the input + history. This function uses two-pass processing: in the first pass we save + the found backward matches and literal bytes into a buffer, and in the + second pass we emit them into the bit stream using prefix codes built based + on the actual command and literal byte histograms. */ + +#include "compress_fragment_two_pass.h" + +#include "../common/constants.h" +#include "../common/platform.h" +#include "bit_cost.h" +#include "brotli_bit_stream.h" +#include "entropy_encode.h" +#include "fast_log.h" +#include "find_match_length.h" +#include "hash_base.h" +#include "write_bits.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +#define MAX_DISTANCE (long)BROTLI_MAX_BACKWARD_LIMIT(18) + +static BROTLI_INLINE uint32_t Hash(const uint8_t* p, + size_t shift, size_t length) { + const uint64_t h = + (BROTLI_UNALIGNED_LOAD64LE(p) << ((8 - length) * 8)) * kHashMul32; + return (uint32_t)(h >> shift); +} + +static BROTLI_INLINE uint32_t HashBytesAtOffset(uint64_t v, size_t offset, + size_t shift, size_t length) { + BROTLI_DCHECK(offset <= 8 - length); + { + const uint64_t h = ((v >> (8 * offset)) << ((8 - length) * 8)) * kHashMul32; + return (uint32_t)(h >> shift); + } +} + +static BROTLI_INLINE BROTLI_BOOL IsMatch(const uint8_t* p1, const uint8_t* p2, + size_t length) { + if (BrotliUnalignedRead32(p1) == BrotliUnalignedRead32(p2)) { + if (length == 4) return BROTLI_TRUE; + return TO_BROTLI_BOOL(p1[4] == p2[4] && p1[5] == p2[5]); + } + return BROTLI_FALSE; +} + +/* Builds a command and distance prefix code (each 64 symbols) into "depth" and + "bits" based on "histogram" and stores it into the bit stream. */ +static void BuildAndStoreCommandPrefixCode(BrotliTwoPassArena* s, + size_t* storage_ix, + uint8_t* storage) { + /* Tree size for building a tree over 64 symbols is 2 * 64 + 1. */ + /* TODO(eustas): initialize once. */ + memset(s->tmp_depth, 0, sizeof(s->tmp_depth)); + BrotliCreateHuffmanTree(s->cmd_histo, 64, 15, s->tmp_tree, s->cmd_depth); + BrotliCreateHuffmanTree(&s->cmd_histo[64], 64, 14, s->tmp_tree, + &s->cmd_depth[64]); + /* We have to jump through a few hoops here in order to compute + the command bits because the symbols are in a different order than in + the full alphabet. This looks complicated, but having the symbols + in this order in the command bits saves a few branches in the Emit* + functions. */ + memcpy(s->tmp_depth, s->cmd_depth + 24, 24); + memcpy(s->tmp_depth + 24, s->cmd_depth, 8); + memcpy(s->tmp_depth + 32, s->cmd_depth + 48, 8); + memcpy(s->tmp_depth + 40, s->cmd_depth + 8, 8); + memcpy(s->tmp_depth + 48, s->cmd_depth + 56, 8); + memcpy(s->tmp_depth + 56, s->cmd_depth + 16, 8); + BrotliConvertBitDepthsToSymbols(s->tmp_depth, 64, s->tmp_bits); + memcpy(s->cmd_bits, s->tmp_bits + 24, 16); + memcpy(s->cmd_bits + 8, s->tmp_bits + 40, 16); + memcpy(s->cmd_bits + 16, s->tmp_bits + 56, 16); + memcpy(s->cmd_bits + 24, s->tmp_bits, 48); + memcpy(s->cmd_bits + 48, s->tmp_bits + 32, 16); + memcpy(s->cmd_bits + 56, s->tmp_bits + 48, 16); + BrotliConvertBitDepthsToSymbols(&s->cmd_depth[64], 64, &s->cmd_bits[64]); + { + /* Create the bit length array for the full command alphabet. */ + size_t i; + memset(s->tmp_depth, 0, 64); /* only 64 first values were used */ + memcpy(s->tmp_depth, s->cmd_depth + 24, 8); + memcpy(s->tmp_depth + 64, s->cmd_depth + 32, 8); + memcpy(s->tmp_depth + 128, s->cmd_depth + 40, 8); + memcpy(s->tmp_depth + 192, s->cmd_depth + 48, 8); + memcpy(s->tmp_depth + 384, s->cmd_depth + 56, 8); + for (i = 0; i < 8; ++i) { + s->tmp_depth[128 + 8 * i] = s->cmd_depth[i]; + s->tmp_depth[256 + 8 * i] = s->cmd_depth[8 + i]; + s->tmp_depth[448 + 8 * i] = s->cmd_depth[16 + i]; + } + BrotliStoreHuffmanTree(s->tmp_depth, BROTLI_NUM_COMMAND_SYMBOLS, + s->tmp_tree, storage_ix, storage); + } + BrotliStoreHuffmanTree(&s->cmd_depth[64], 64, s->tmp_tree, storage_ix, + storage); +} + +static BROTLI_INLINE void EmitInsertLen( + uint32_t insertlen, uint32_t** commands) { + if (insertlen < 6) { + **commands = insertlen; + } else if (insertlen < 130) { + const uint32_t tail = insertlen - 2; + const uint32_t nbits = Log2FloorNonZero(tail) - 1u; + const uint32_t prefix = tail >> nbits; + const uint32_t inscode = (nbits << 1) + prefix + 2; + const uint32_t extra = tail - (prefix << nbits); + **commands = inscode | (extra << 8); + } else if (insertlen < 2114) { + const uint32_t tail = insertlen - 66; + const uint32_t nbits = Log2FloorNonZero(tail); + const uint32_t code = nbits + 10; + const uint32_t extra = tail - (1u << nbits); + **commands = code | (extra << 8); + } else if (insertlen < 6210) { + const uint32_t extra = insertlen - 2114; + **commands = 21 | (extra << 8); + } else if (insertlen < 22594) { + const uint32_t extra = insertlen - 6210; + **commands = 22 | (extra << 8); + } else { + const uint32_t extra = insertlen - 22594; + **commands = 23 | (extra << 8); + } + ++(*commands); +} + +static BROTLI_INLINE void EmitCopyLen(size_t copylen, uint32_t** commands) { + if (copylen < 10) { + **commands = (uint32_t)(copylen + 38); + } else if (copylen < 134) { + const size_t tail = copylen - 6; + const size_t nbits = Log2FloorNonZero(tail) - 1; + const size_t prefix = tail >> nbits; + const size_t code = (nbits << 1) + prefix + 44; + const size_t extra = tail - (prefix << nbits); + **commands = (uint32_t)(code | (extra << 8)); + } else if (copylen < 2118) { + const size_t tail = copylen - 70; + const size_t nbits = Log2FloorNonZero(tail); + const size_t code = nbits + 52; + const size_t extra = tail - ((size_t)1 << nbits); + **commands = (uint32_t)(code | (extra << 8)); + } else { + const size_t extra = copylen - 2118; + **commands = (uint32_t)(63 | (extra << 8)); + } + ++(*commands); +} + +static BROTLI_INLINE void EmitCopyLenLastDistance( + size_t copylen, uint32_t** commands) { + if (copylen < 12) { + **commands = (uint32_t)(copylen + 20); + ++(*commands); + } else if (copylen < 72) { + const size_t tail = copylen - 8; + const size_t nbits = Log2FloorNonZero(tail) - 1; + const size_t prefix = tail >> nbits; + const size_t code = (nbits << 1) + prefix + 28; + const size_t extra = tail - (prefix << nbits); + **commands = (uint32_t)(code | (extra << 8)); + ++(*commands); + } else if (copylen < 136) { + const size_t tail = copylen - 8; + const size_t code = (tail >> 5) + 54; + const size_t extra = tail & 31; + **commands = (uint32_t)(code | (extra << 8)); + ++(*commands); + **commands = 64; + ++(*commands); + } else if (copylen < 2120) { + const size_t tail = copylen - 72; + const size_t nbits = Log2FloorNonZero(tail); + const size_t code = nbits + 52; + const size_t extra = tail - ((size_t)1 << nbits); + **commands = (uint32_t)(code | (extra << 8)); + ++(*commands); + **commands = 64; + ++(*commands); + } else { + const size_t extra = copylen - 2120; + **commands = (uint32_t)(63 | (extra << 8)); + ++(*commands); + **commands = 64; + ++(*commands); + } +} + +static BROTLI_INLINE void EmitDistance(uint32_t distance, uint32_t** commands) { + uint32_t d = distance + 3; + uint32_t nbits = Log2FloorNonZero(d) - 1; + const uint32_t prefix = (d >> nbits) & 1; + const uint32_t offset = (2 + prefix) << nbits; + const uint32_t distcode = 2 * (nbits - 1) + prefix + 80; + uint32_t extra = d - offset; + **commands = distcode | (extra << 8); + ++(*commands); +} + +/* REQUIRES: len <= 1 << 24. */ +static void BrotliStoreMetaBlockHeader( + size_t len, BROTLI_BOOL is_uncompressed, size_t* storage_ix, + uint8_t* storage) { + size_t nibbles = 6; + /* ISLAST */ + BrotliWriteBits(1, 0, storage_ix, storage); + if (len <= (1U << 16)) { + nibbles = 4; + } else if (len <= (1U << 20)) { + nibbles = 5; + } + BrotliWriteBits(2, nibbles - 4, storage_ix, storage); + BrotliWriteBits(nibbles * 4, len - 1, storage_ix, storage); + /* ISUNCOMPRESSED */ + BrotliWriteBits(1, (uint64_t)is_uncompressed, storage_ix, storage); +} + +static BROTLI_INLINE void CreateCommands(const uint8_t* input, + size_t block_size, size_t input_size, const uint8_t* base_ip, int* table, + size_t table_bits, size_t min_match, + uint8_t** literals, uint32_t** commands) { + /* "ip" is the input pointer. */ + const uint8_t* ip = input; + const size_t shift = 64u - table_bits; + const uint8_t* ip_end = input + block_size; + /* "next_emit" is a pointer to the first byte that is not covered by a + previous copy. Bytes between "next_emit" and the start of the next copy or + the end of the input will be emitted as literal bytes. */ + const uint8_t* next_emit = input; + + int last_distance = -1; + const size_t kInputMarginBytes = BROTLI_WINDOW_GAP; + + if (BROTLI_PREDICT_TRUE(block_size >= kInputMarginBytes)) { + /* For the last block, we need to keep a 16 bytes margin so that we can be + sure that all distances are at most window size - 16. + For all other blocks, we only need to keep a margin of 5 bytes so that + we don't go over the block size with a copy. */ + const size_t len_limit = BROTLI_MIN(size_t, block_size - min_match, + input_size - kInputMarginBytes); + const uint8_t* ip_limit = input + len_limit; + + uint32_t next_hash; + for (next_hash = Hash(++ip, shift, min_match); ; ) { + /* Step 1: Scan forward in the input looking for a 6-byte-long match. + If we get close to exhausting the input then goto emit_remainder. + + Heuristic match skipping: If 32 bytes are scanned with no matches + found, start looking only at every other byte. If 32 more bytes are + scanned, look at every third byte, etc.. When a match is found, + immediately go back to looking at every byte. This is a small loss + (~5% performance, ~0.1% density) for compressible data due to more + bookkeeping, but for non-compressible data (such as JPEG) it's a huge + win since the compressor quickly "realizes" the data is incompressible + and doesn't bother looking for matches everywhere. + + The "skip" variable keeps track of how many bytes there are since the + last match; dividing it by 32 (ie. right-shifting by five) gives the + number of bytes to move ahead for each iteration. */ + uint32_t skip = 32; + + const uint8_t* next_ip = ip; + const uint8_t* candidate; + + BROTLI_DCHECK(next_emit < ip); +trawl: + do { + uint32_t hash = next_hash; + uint32_t bytes_between_hash_lookups = skip++ >> 5; + ip = next_ip; + BROTLI_DCHECK(hash == Hash(ip, shift, min_match)); + next_ip = ip + bytes_between_hash_lookups; + if (BROTLI_PREDICT_FALSE(next_ip > ip_limit)) { + goto emit_remainder; + } + next_hash = Hash(next_ip, shift, min_match); + candidate = ip - last_distance; + if (IsMatch(ip, candidate, min_match)) { + if (BROTLI_PREDICT_TRUE(candidate < ip)) { + table[hash] = (int)(ip - base_ip); + break; + } + } + candidate = base_ip + table[hash]; + BROTLI_DCHECK(candidate >= base_ip); + BROTLI_DCHECK(candidate < ip); + + table[hash] = (int)(ip - base_ip); + } while (BROTLI_PREDICT_TRUE(!IsMatch(ip, candidate, min_match))); + + /* Check copy distance. If candidate is not feasible, continue search. + Checking is done outside of hot loop to reduce overhead. */ + if (ip - candidate > MAX_DISTANCE) goto trawl; + + /* Step 2: Emit the found match together with the literal bytes from + "next_emit", and then see if we can find a next match immediately + afterwards. Repeat until we find no match for the input + without emitting some literal bytes. */ + + { + /* We have a 6-byte match at ip, and we need to emit bytes in + [next_emit, ip). */ + const uint8_t* base = ip; + size_t matched = min_match + FindMatchLengthWithLimit( + candidate + min_match, ip + min_match, + (size_t)(ip_end - ip) - min_match); + int distance = (int)(base - candidate); /* > 0 */ + int insert = (int)(base - next_emit); + ip += matched; + BROTLI_DCHECK(0 == memcmp(base, candidate, matched)); + EmitInsertLen((uint32_t)insert, commands); + BROTLI_LOG(("[CompressFragment] pos = %d insert = %d copy = %d\n", + (int)(next_emit - base_ip), insert, 2)); + memcpy(*literals, next_emit, (size_t)insert); + *literals += insert; + if (distance == last_distance) { + **commands = 64; + ++(*commands); + } else { + EmitDistance((uint32_t)distance, commands); + last_distance = distance; + } + EmitCopyLenLastDistance(matched, commands); + BROTLI_LOG(("[CompressFragment] pos = %d distance = %d\n" + "[CompressFragment] pos = %d insert = %d copy = %d\n" + "[CompressFragment] pos = %d distance = %d\n", + (int)(base - base_ip), (int)distance, + (int)(base - base_ip) + 2, 0, (int)matched - 2, + (int)(base - base_ip) + 2, (int)distance)); + + next_emit = ip; + if (BROTLI_PREDICT_FALSE(ip >= ip_limit)) { + goto emit_remainder; + } + { + /* We could immediately start working at ip now, but to improve + compression we first update "table" with the hashes of some + positions within the last copy. */ + uint64_t input_bytes; + uint32_t cur_hash; + uint32_t prev_hash; + if (min_match == 4) { + input_bytes = BROTLI_UNALIGNED_LOAD64LE(ip - 3); + cur_hash = HashBytesAtOffset(input_bytes, 3, shift, min_match); + prev_hash = HashBytesAtOffset(input_bytes, 0, shift, min_match); + table[prev_hash] = (int)(ip - base_ip - 3); + prev_hash = HashBytesAtOffset(input_bytes, 1, shift, min_match); + table[prev_hash] = (int)(ip - base_ip - 2); + prev_hash = HashBytesAtOffset(input_bytes, 0, shift, min_match); + table[prev_hash] = (int)(ip - base_ip - 1); + } else { + input_bytes = BROTLI_UNALIGNED_LOAD64LE(ip - 5); + prev_hash = HashBytesAtOffset(input_bytes, 0, shift, min_match); + table[prev_hash] = (int)(ip - base_ip - 5); + prev_hash = HashBytesAtOffset(input_bytes, 1, shift, min_match); + table[prev_hash] = (int)(ip - base_ip - 4); + prev_hash = HashBytesAtOffset(input_bytes, 2, shift, min_match); + table[prev_hash] = (int)(ip - base_ip - 3); + input_bytes = BROTLI_UNALIGNED_LOAD64LE(ip - 2); + cur_hash = HashBytesAtOffset(input_bytes, 2, shift, min_match); + prev_hash = HashBytesAtOffset(input_bytes, 0, shift, min_match); + table[prev_hash] = (int)(ip - base_ip - 2); + prev_hash = HashBytesAtOffset(input_bytes, 1, shift, min_match); + table[prev_hash] = (int)(ip - base_ip - 1); + } + + candidate = base_ip + table[cur_hash]; + table[cur_hash] = (int)(ip - base_ip); + } + } + + while (ip - candidate <= MAX_DISTANCE && + IsMatch(ip, candidate, min_match)) { + /* We have a 6-byte match at ip, and no need to emit any + literal bytes prior to ip. */ + const uint8_t* base = ip; + size_t matched = min_match + FindMatchLengthWithLimit( + candidate + min_match, ip + min_match, + (size_t)(ip_end - ip) - min_match); + ip += matched; + last_distance = (int)(base - candidate); /* > 0 */ + BROTLI_DCHECK(0 == memcmp(base, candidate, matched)); + EmitCopyLen(matched, commands); + EmitDistance((uint32_t)last_distance, commands); + BROTLI_LOG(("[CompressFragment] pos = %d insert = %d copy = %d\n" + "[CompressFragment] pos = %d distance = %d\n", + (int)(base - base_ip), 0, (int)matched, + (int)(base - base_ip), (int)last_distance)); + + next_emit = ip; + if (BROTLI_PREDICT_FALSE(ip >= ip_limit)) { + goto emit_remainder; + } + { + /* We could immediately start working at ip now, but to improve + compression we first update "table" with the hashes of some + positions within the last copy. */ + uint64_t input_bytes; + uint32_t cur_hash; + uint32_t prev_hash; + if (min_match == 4) { + input_bytes = BROTLI_UNALIGNED_LOAD64LE(ip - 3); + cur_hash = HashBytesAtOffset(input_bytes, 3, shift, min_match); + prev_hash = HashBytesAtOffset(input_bytes, 0, shift, min_match); + table[prev_hash] = (int)(ip - base_ip - 3); + prev_hash = HashBytesAtOffset(input_bytes, 1, shift, min_match); + table[prev_hash] = (int)(ip - base_ip - 2); + prev_hash = HashBytesAtOffset(input_bytes, 2, shift, min_match); + table[prev_hash] = (int)(ip - base_ip - 1); + } else { + input_bytes = BROTLI_UNALIGNED_LOAD64LE(ip - 5); + prev_hash = HashBytesAtOffset(input_bytes, 0, shift, min_match); + table[prev_hash] = (int)(ip - base_ip - 5); + prev_hash = HashBytesAtOffset(input_bytes, 1, shift, min_match); + table[prev_hash] = (int)(ip - base_ip - 4); + prev_hash = HashBytesAtOffset(input_bytes, 2, shift, min_match); + table[prev_hash] = (int)(ip - base_ip - 3); + input_bytes = BROTLI_UNALIGNED_LOAD64LE(ip - 2); + cur_hash = HashBytesAtOffset(input_bytes, 2, shift, min_match); + prev_hash = HashBytesAtOffset(input_bytes, 0, shift, min_match); + table[prev_hash] = (int)(ip - base_ip - 2); + prev_hash = HashBytesAtOffset(input_bytes, 1, shift, min_match); + table[prev_hash] = (int)(ip - base_ip - 1); + } + + candidate = base_ip + table[cur_hash]; + table[cur_hash] = (int)(ip - base_ip); + } + } + + next_hash = Hash(++ip, shift, min_match); + } + } + +emit_remainder: + BROTLI_DCHECK(next_emit <= ip_end); + /* Emit the remaining bytes as literals. */ + if (next_emit < ip_end) { + const uint32_t insert = (uint32_t)(ip_end - next_emit); + EmitInsertLen(insert, commands); + BROTLI_LOG(("[CompressFragment] pos = %d insert = %d copy = %d\n", + (int)(next_emit - base_ip), insert, 2)); + memcpy(*literals, next_emit, insert); + *literals += insert; + } +} + +static void StoreCommands(BrotliTwoPassArena* s, + const uint8_t* literals, const size_t num_literals, + const uint32_t* commands, const size_t num_commands, + size_t* storage_ix, uint8_t* storage) { + static const BROTLI_MODEL("small") uint32_t kNumExtraBits[128] = { + 0, 0, 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, + 6, 7, 8, 9, 10, 12, 14, 24, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 2, 2, 3, 3, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 7, 8, 9, 10, 24, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, + 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, 14, 14, 15, 15, 16, 16, + 17, 17, 18, 18, 19, 19, 20, 20, 21, 21, 22, 22, 23, 23, 24, 24, + }; + static const BROTLI_MODEL("small") uint32_t kInsertOffset[24] = { + 0, 1, 2, 3, 4, 5, 6, 8, 10, 14, 18, 26, + 34, 50, 66, 98, 130, 194, 322, 578, 1090, 2114, 6210, 22594, + }; + + size_t i; + memset(s->lit_histo, 0, sizeof(s->lit_histo)); + /* TODO(eustas): is that necessary? */ + memset(s->cmd_depth, 0, sizeof(s->cmd_depth)); + /* TODO(eustas): is that necessary? */ + memset(s->cmd_bits, 0, sizeof(s->cmd_bits)); + memset(s->cmd_histo, 0, sizeof(s->cmd_histo)); + for (i = 0; i < num_literals; ++i) { + ++s->lit_histo[literals[i]]; + } + BrotliBuildAndStoreHuffmanTreeFast(s->tmp_tree, s->lit_histo, num_literals, + /* max_bits = */ 8, s->lit_depth, + s->lit_bits, storage_ix, storage); + + for (i = 0; i < num_commands; ++i) { + const uint32_t code = commands[i] & 0xFF; + BROTLI_DCHECK(code < 128); + ++s->cmd_histo[code]; + } + s->cmd_histo[1] += 1; + s->cmd_histo[2] += 1; + s->cmd_histo[64] += 1; + s->cmd_histo[84] += 1; + BuildAndStoreCommandPrefixCode(s, storage_ix, storage); + + for (i = 0; i < num_commands; ++i) { + const uint32_t cmd = commands[i]; + const uint32_t code = cmd & 0xFF; + const uint32_t extra = cmd >> 8; + BROTLI_DCHECK(code < 128); + BrotliWriteBits(s->cmd_depth[code], s->cmd_bits[code], storage_ix, storage); + BrotliWriteBits(kNumExtraBits[code], extra, storage_ix, storage); + if (code < 24) { + const uint32_t insert = kInsertOffset[code] + extra; + uint32_t j; + for (j = 0; j < insert; ++j) { + const uint8_t lit = *literals; + BrotliWriteBits(s->lit_depth[lit], s->lit_bits[lit], storage_ix, + storage); + ++literals; + } + } + } +} + +/* Acceptable loss for uncompressible speedup is 2% */ +#define MIN_RATIO 0.98 +#define SAMPLE_RATE 43 + +static BROTLI_BOOL ShouldCompress(BrotliTwoPassArena* s, + const uint8_t* input, size_t input_size, size_t num_literals) { + double corpus_size = (double)input_size; + if ((double)num_literals < MIN_RATIO * corpus_size) { + return BROTLI_TRUE; + } else { + const double max_total_bit_cost = corpus_size * 8 * MIN_RATIO / SAMPLE_RATE; + size_t i; + memset(s->lit_histo, 0, sizeof(s->lit_histo)); + for (i = 0; i < input_size; i += SAMPLE_RATE) { + ++s->lit_histo[input[i]]; + } + return TO_BROTLI_BOOL( + BrotliBitsEntropy(s->lit_histo, 256) < max_total_bit_cost); + } +} + +static void RewindBitPosition(const size_t new_storage_ix, + size_t* storage_ix, uint8_t* storage) { + const size_t bitpos = new_storage_ix & 7; + const size_t mask = (1u << bitpos) - 1; + storage[new_storage_ix >> 3] &= (uint8_t)mask; + *storage_ix = new_storage_ix; +} + +static void EmitUncompressedMetaBlock(const uint8_t* input, size_t input_size, + size_t* storage_ix, uint8_t* storage) { + BrotliStoreMetaBlockHeader(input_size, 1, storage_ix, storage); + *storage_ix = (*storage_ix + 7u) & ~7u; + memcpy(&storage[*storage_ix >> 3], input, input_size); + *storage_ix += input_size << 3; + storage[*storage_ix >> 3] = 0; +} + +static BROTLI_INLINE void BrotliCompressFragmentTwoPassImpl( + BrotliTwoPassArena* s, const uint8_t* input, size_t input_size, + BROTLI_BOOL is_last, uint32_t* command_buf, uint8_t* literal_buf, + int* table, size_t table_bits, size_t min_match, + size_t* storage_ix, uint8_t* storage) { + /* Save the start of the first block for position and distance computations. + */ + const uint8_t* base_ip = input; + BROTLI_UNUSED(is_last); + + while (input_size > 0) { + size_t block_size = + BROTLI_MIN(size_t, input_size, kCompressFragmentTwoPassBlockSize); + uint32_t* commands = command_buf; + uint8_t* literals = literal_buf; + size_t num_literals; + CreateCommands(input, block_size, input_size, base_ip, table, + table_bits, min_match, &literals, &commands); + num_literals = (size_t)(literals - literal_buf); + if (ShouldCompress(s, input, block_size, num_literals)) { + const size_t num_commands = (size_t)(commands - command_buf); + BrotliStoreMetaBlockHeader(block_size, 0, storage_ix, storage); + /* No block splits, no contexts. */ + BrotliWriteBits(13, 0, storage_ix, storage); + StoreCommands(s, literal_buf, num_literals, command_buf, num_commands, + storage_ix, storage); + } else { + /* Since we did not find many backward references and the entropy of + the data is close to 8 bits, we can simply emit an uncompressed block. + This makes compression speed of uncompressible data about 3x faster. */ + EmitUncompressedMetaBlock(input, block_size, storage_ix, storage); + } + input += block_size; + input_size -= block_size; + } +} + +#define FOR_TABLE_BITS_(X) \ + X(8) X(9) X(10) X(11) X(12) X(13) X(14) X(15) X(16) X(17) + +#define BAKE_METHOD_PARAM_(B) \ +static BROTLI_NOINLINE void BrotliCompressFragmentTwoPassImpl ## B( \ + BrotliTwoPassArena* s, const uint8_t* input, size_t input_size, \ + BROTLI_BOOL is_last, uint32_t* command_buf, uint8_t* literal_buf, \ + int* table, size_t* storage_ix, uint8_t* storage) { \ + size_t min_match = (B <= 15) ? 4 : 6; \ + BrotliCompressFragmentTwoPassImpl(s, input, input_size, is_last, command_buf,\ + literal_buf, table, B, min_match, storage_ix, storage); \ +} +FOR_TABLE_BITS_(BAKE_METHOD_PARAM_) +#undef BAKE_METHOD_PARAM_ + +void BrotliCompressFragmentTwoPass( + BrotliTwoPassArena* s, const uint8_t* input, size_t input_size, + BROTLI_BOOL is_last, uint32_t* command_buf, uint8_t* literal_buf, + int* table, size_t table_size, size_t* storage_ix, uint8_t* storage) { + const size_t initial_storage_ix = *storage_ix; + const size_t table_bits = Log2FloorNonZero(table_size); + switch (table_bits) { +#define CASE_(B) \ + case B: \ + BrotliCompressFragmentTwoPassImpl ## B( \ + s, input, input_size, is_last, command_buf, \ + literal_buf, table, storage_ix, storage); \ + break; + FOR_TABLE_BITS_(CASE_) +#undef CASE_ + default: BROTLI_DCHECK(0); break; + } + + /* If output is larger than single uncompressed block, rewrite it. */ + if (*storage_ix - initial_storage_ix > 31 + (input_size << 3)) { + RewindBitPosition(initial_storage_ix, storage_ix, storage); + EmitUncompressedMetaBlock(input, input_size, storage_ix, storage); + } + + if (is_last) { + BrotliWriteBits(1, 1, storage_ix, storage); /* islast */ + BrotliWriteBits(1, 1, storage_ix, storage); /* isempty */ + *storage_ix = (*storage_ix + 7u) & ~7u; + } +} + +#undef FOR_TABLE_BITS_ + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif diff --git a/packages/runtime/vendor/brotli/c/enc/compress_fragment_two_pass.h b/packages/runtime/vendor/brotli/c/enc/compress_fragment_two_pass.h new file mode 100644 index 000000000..e98b5e0e2 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/compress_fragment_two_pass.h @@ -0,0 +1,70 @@ +/* Copyright 2015 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* Function for fast encoding of an input fragment, independently from the input + history. This function uses two-pass processing: in the first pass we save + the found backward matches and literal bytes into a buffer, and in the + second pass we emit them into the bit stream using prefix codes built based + on the actual command and literal byte histograms. */ + +#ifndef BROTLI_ENC_COMPRESS_FRAGMENT_TWO_PASS_H_ +#define BROTLI_ENC_COMPRESS_FRAGMENT_TWO_PASS_H_ + +#include "../common/constants.h" +#include "../common/platform.h" +#include "entropy_encode.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +/* TODO(eustas): turn to macro. */ +static const size_t kCompressFragmentTwoPassBlockSize = 1 << 17; + +typedef struct BrotliTwoPassArena { + uint32_t lit_histo[256]; + uint8_t lit_depth[256]; + uint16_t lit_bits[256]; + + uint32_t cmd_histo[128]; + uint8_t cmd_depth[128]; + uint16_t cmd_bits[128]; + + /* BuildAndStoreCommandPrefixCode */ + HuffmanTree tmp_tree[2 * BROTLI_NUM_LITERAL_SYMBOLS + 1]; + uint8_t tmp_depth[BROTLI_NUM_COMMAND_SYMBOLS]; + uint16_t tmp_bits[64]; +} BrotliTwoPassArena; + +/* Compresses "input" string to the "*storage" buffer as one or more complete + meta-blocks, and updates the "*storage_ix" bit position. + + If "is_last" is 1, emits an additional empty last meta-block. + + REQUIRES: "input_size" is greater than zero, or "is_last" is 1. + REQUIRES: "input_size" is less or equal to maximal metablock size (1 << 24). + REQUIRES: "command_buf" and "literal_buf" point to at least + kCompressFragmentTwoPassBlockSize long arrays. + REQUIRES: All elements in "table[0..table_size-1]" are initialized to zero. + REQUIRES: "table_size" is a power of two + OUTPUT: maximal copy distance <= |input_size| + OUTPUT: maximal copy distance <= BROTLI_MAX_BACKWARD_LIMIT(18) */ +BROTLI_INTERNAL void BrotliCompressFragmentTwoPass(BrotliTwoPassArena* s, + const uint8_t* input, + size_t input_size, + BROTLI_BOOL is_last, + uint32_t* command_buf, + uint8_t* literal_buf, + int* table, + size_t table_size, + size_t* storage_ix, + uint8_t* storage); + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif + +#endif /* BROTLI_ENC_COMPRESS_FRAGMENT_TWO_PASS_H_ */ diff --git a/packages/runtime/vendor/brotli/c/enc/dictionary_hash.c b/packages/runtime/vendor/brotli/c/enc/dictionary_hash.c new file mode 100644 index 000000000..193f7a41c --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/dictionary_hash.c @@ -0,0 +1,145 @@ +/* Copyright 2015 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* Hash table on the 4-byte prefixes of static dictionary words. */ + +#include "dictionary_hash.h" + +#include "../common/platform.h" /* IWYU pragma: keep */ +#include "../common/static_init.h" + +#if (BROTLI_STATIC_INIT != BROTLI_STATIC_INIT_NONE) +#include "../common/dictionary.h" +#include "hash_base.h" +#endif + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +#if (BROTLI_STATIC_INIT != BROTLI_STATIC_INIT_NONE) +BROTLI_BOOL BROTLI_COLD BrotliEncoderInitDictionaryHash( + const BrotliDictionary* dict, uint16_t* words, uint8_t* lengths) { + size_t global_idx = 0; + size_t len; + size_t i; + static const uint8_t frozen_idx[1688] = {0, 0, 8, 164, 32, 56, 31, 191, 36, 4, +128, 81, 68, 132, 145, 129, 0, 0, 0, 28, 0, 8, 1, 1, 64, 3, 1, 0, 0, 0, 0, 0, 4, +64, 1, 2, 128, 0, 132, 49, 0, 0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 0, 1, 0, 36, 152, +0, 0, 0, 0, 128, 8, 0, 0, 128, 0, 0, 8, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, +0, 0, 0, 1, 0, 64, 133, 0, 32, 0, 0, 128, 1, 0, 0, 0, 0, 4, 4, 4, 32, 16, 130, +0, 128, 8, 0, 0, 0, 0, 0, 64, 0, 64, 0, 160, 0, 148, 53, 0, 0, 0, 0, 0, 128, 0, +130, 0, 0, 0, 8, 0, 0, 0, 0, 0, 48, 0, 0, 0, 0, 0, 0, 32, 1, 32, 129, 0, 12, 0, +1, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 16, 32, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 8, +0, 0, 2, 0, 0, 0, 0, 0, 32, 0, 0, 0, 2, 66, 128, 0, 0, 16, 0, 0, 0, 0, 64, 1, 6, +128, 8, 0, 192, 24, 32, 0, 0, 8, 4, 128, 128, 2, 160, 0, 160, 0, 64, 0, 0, 2, 0, +0, 0, 0, 0, 0, 0, 0, 0, 32, 1, 0, 0, 64, 0, 0, 0, 0, 0, 0, 32, 0, 66, 0, 2, 0, +4, 0, 8, 0, 2, 0, 0, 33, 8, 0, 0, 0, 8, 0, 128, 162, 4, 128, 0, 2, 33, 0, 160, +0, 8, 0, 64, 0, 160, 0, 129, 4, 0, 0, 32, 0, 0, 32, 0, 2, 0, 0, 0, 0, 0, 0, 128, +0, 0, 0, 0, 0, 64, 10, 0, 0, 0, 0, 32, 64, 0, 0, 0, 0, 0, 16, 0, 16, 16, 0, 0, +80, 2, 0, 0, 0, 0, 8, 0, 0, 16, 0, 8, 0, 0, 0, 8, 64, 128, 0, 0, 0, 8, 208, 0, +0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 32, 0, 8, 0, 128, 0, 0, 0, 1, 0, 0, 0, +16, 8, 1, 136, 0, 0, 36, 0, 64, 9, 0, 1, 32, 8, 0, 64, 64, 131, 16, 224, 32, 4, +0, 4, 5, 160, 0, 131, 0, 4, 96, 0, 0, 184, 192, 0, 177, 205, 96, 0, 0, 0, 0, 2, +0, 32, 0, 0, 0, 0, 0, 0, 0, 0, 64, 0, 0, 128, 0, 0, 8, 0, 0, 0, 0, 1, 4, 0, 1, +0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 4, 0, 0, 64, 69, 0, 0, 8, 2, 66, 32, 64, 0, 0, 0, +0, 0, 1, 0, 128, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 16, 0, 0, 4, 128, 64, +0, 0, 0, 0, 0, 0, 0, 0, 224, 0, 8, 0, 0, 130, 16, 64, 128, 2, 64, 0, 0, 0, 128, +2, 192, 64, 0, 65, 0, 0, 0, 16, 0, 0, 0, 32, 4, 2, 2, 76, 0, 0, 0, 4, 72, 52, +131, 44, 76, 0, 0, 0, 0, 64, 1, 16, 148, 4, 0, 16, 10, 64, 0, 2, 0, 1, 0, 128, +64, 68, 0, 0, 0, 0, 0, 64, 144, 0, 8, 0, 2, 0, 0, 0, 0, 0, 0, 3, 64, 0, 0, 0, 0, +1, 128, 0, 0, 32, 66, 0, 0, 0, 40, 0, 18, 0, 0, 0, 0, 0, 33, 0, 0, 32, 0, 0, 32, +0, 128, 4, 64, 145, 140, 0, 0, 0, 128, 0, 2, 0, 0, 20, 0, 80, 38, 0, 0, 32, 0, +32, 64, 4, 4, 0, 4, 0, 0, 0, 129, 4, 0, 0, 144, 17, 32, 130, 16, 132, 24, 134, +0, 0, 64, 2, 5, 50, 8, 194, 33, 1, 68, 117, 1, 8, 32, 161, 54, 0, 130, 34, 0, 0, +0, 64, 128, 0, 0, 2, 0, 0, 0, 0, 32, 1, 0, 0, 0, 3, 14, 0, 0, 0, 0, 0, 16, 4, 0, +0, 0, 0, 0, 0, 0, 0, 96, 1, 24, 18, 0, 1, 128, 24, 0, 64, 0, 4, 0, 16, 128, 0, +64, 0, 0, 0, 64, 0, 8, 0, 0, 0, 0, 0, 66, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +0, 0, 0, 0, 16, 0, 64, 2, 0, 0, 0, 0, 6, 0, 8, 8, 2, 0, 64, 0, 0, 0, 0, 128, 2, +2, 12, 64, 0, 64, 0, 8, 0, 128, 32, 0, 0, 10, 0, 0, 32, 0, 128, 32, 33, 8, 136, +0, 96, 64, 0, 0, 0, 0, 0, 64, 4, 16, 4, 8, 0, 0, 0, 16, 0, 2, 0, 0, 1, 128, 0, +64, 16, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 2, 0, 16, 0, 4, 0, 8, 0, 0, 0, 0, 0, +20, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 136, 0, 0, 0, 0, 0, 8, 0, +0, 0, 0, 0, 2, 0, 0, 0, 64, 0, 0, 1, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, +0, 0, 4, 0, 0, 0, 0, 65, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, +0, 0, 0, 0, 2, 128, 0, 0, 0, 8, 2, 0, 0, 128, 0, 16, 2, 0, 0, 4, 0, 32, 0, 0, 1, +4, 64, 64, 0, 4, 0, 1, 0, 16, 0, 32, 68, 4, 4, 65, 10, 0, 20, 37, 18, 1, 148, 0, +32, 128, 3, 8, 0, 64, 0, 0, 0, 0, 0, 0, 4, 0, 16, 1, 128, 0, 0, 0, 128, 16, 0, +0, 0, 0, 1, 128, 0, 0, 128, 64, 128, 64, 0, 130, 0, 164, 8, 0, 0, 1, 64, 128, 0, +18, 0, 2, 150, 0, 8, 0, 0, 64, 0, 81, 0, 0, 16, 128, 2, 8, 36, 32, 129, 4, 144, +13, 0, 0, 3, 8, 1, 0, 2, 0, 0, 64, 0, 5, 0, 1, 34, 1, 32, 2, 16, 128, 128, 128, +0, 0, 0, 2, 0, 4, 18, 8, 12, 34, 32, 192, 6, 64, 224, 33, 0, 0, 137, 72, 64, 0, +24, 8, 128, 128, 0, 16, 0, 32, 128, 128, 132, 8, 0, 0, 16, 0, 64, 0, 0, 4, 0, 0, +16, 0, 4, 128, 64, 0, 0, 1, 0, 4, 64, 32, 144, 130, 2, 128, 0, 192, 0, 64, 82, +64, 1, 32, 128, 128, 2, 0, 84, 0, 32, 0, 44, 24, 72, 80, 32, 16, 0, 0, 44, 16, +96, 64, 1, 72, 131, 0, 0, 0, 16, 0, 0, 165, 0, 129, 2, 49, 48, 64, 64, 12, 64, +176, 64, 84, 8, 128, 20, 64, 213, 136, 104, 1, 41, 15, 83, 170, 0, 0, 41, 1, 64, +64, 0, 193, 64, 64, 8, 0, 128, 0, 0, 64, 8, 64, 8, 1, 16, 0, 8, 0, 0, 2, 1, 128, +28, 84, 141, 97, 0, 0, 68, 0, 0, 129, 8, 0, 16, 8, 32, 0, 64, 0, 0, 0, 24, 0, 0, +0, 192, 0, 8, 128, 0, 0, 0, 0, 0, 64, 0, 1, 0, 0, 0, 0, 40, 1, 128, 64, 0, 4, 2, +32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 32, 8, 0, 32, 0, 0, 0, 16, 17, 0, +2, 4, 0, 0, 33, 128, 2, 0, 0, 0, 0, 129, 0, 2, 0, 0, 0, 36, 0, 32, 2, 0, 0, 0, +0, 0, 0, 32, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 4, 32, 64, 0, 0, 0, 0, 0, 0, +32, 0, 0, 32, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 16, 0, 0, 0, 0, 0, 0, 0, +1, 0, 136, 0, 0, 24, 192, 128, 3, 0, 17, 18, 2, 0, 66, 0, 4, 24, 0, 9, 208, 167, +0, 144, 20, 64, 0, 130, 64, 0, 2, 16, 136, 8, 74, 32, 0, 168, 0, 65, 32, 8, 12, +1, 3, 1, 64, 180, 3, 0, 64, 0, 8, 0, 0, 32, 65, 0, 4, 16, 4, 16, 68, 32, 64, 36, +32, 24, 33, 1, 128, 0, 0, 8, 0, 32, 64, 81, 0, 1, 10, 19, 8, 0, 0, 4, 5, 144, 0, +0, 8, 128, 0, 0, 4, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 8, 0, 0, 0, 0, 0, 80, 1, 0, 0, +33, 0, 32, 66, 4, 2, 0, 1, 43, 2, 0, 0, 4, 32, 16, 0, 64, 0, 3, 32, 0, 2, 64, +64, 116, 0, 65, 52, 64, 0, 17, 64, 192, 96, 8, 10, 8, 2, 4, 0, 17, 64, 0, 4, 0, +0, 4, 128, 0, 0, 9, 0, 0, 130, 2, 0, 192, 0, 48, 128, 64, 0, 96, 0, 64, 0, 1, +16, 32, 0, 1, 32, 6, 128, 2, 32, 0, 12, 0, 0, 48, 32, 8, 0, 0, 128, 0, 18, 0, +0, 28, 24, 41, 16, 5, 32, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +0, 0, 0, 0, 1, 0, 0, 0, 16, 0, 0, 0, 0, 64, 0, 0, 0, 0, 8, 0, 0, 0, 0, 16, 128, +0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 33, 0, +0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +0, 0, 0, 0, 0, 0, 0}; + + memset(lengths, 0, BROTLI_ENC_NUM_HASH_BUCKETS); + + for (len = BROTLI_MAX_DICTIONARY_WORD_LENGTH; + len >= BROTLI_MIN_DICTIONARY_WORD_LENGTH; --len) { + size_t length_lt_8 = len < 8 ? 1 : 0; + size_t n = 1u << dict->size_bits_by_length[len]; + const uint8_t* dict_words = dict->data + dict->offsets_by_length[len]; + for (i = 0; i < n; ++i) { + size_t j = n - 1 - i; + const uint8_t* word = dict_words + len * j; + const uint32_t key = Hash14(word); + size_t idx = (key << 1) + length_lt_8; + if ((lengths[idx] & 0x80) == 0) { + BROTLI_BOOL is_final = TO_BROTLI_BOOL(frozen_idx[global_idx / 8] & + (1u << (global_idx % 8))); + words[idx] = (uint16_t)j; + lengths[idx] = (uint8_t)(len + (is_final ? 0x80 : 0)); + } + global_idx++; + } + } + for (i = 0; i < BROTLI_ENC_NUM_HASH_BUCKETS; ++i) { + lengths[i] &= 0x7F; + } + + return BROTLI_TRUE; +} + +BROTLI_MODEL("small") +uint16_t kStaticDictionaryHashWords[BROTLI_ENC_NUM_HASH_BUCKETS]; +BROTLI_MODEL("small") +uint8_t kStaticDictionaryHashLengths[BROTLI_ENC_NUM_HASH_BUCKETS]; + +#else /* BROTLI_STATIC_INIT */ + +/* Embed kStaticDictionaryHashWords and kStaticDictionaryHashLengths. */ +#include "dictionary_hash_inc.h" + +#endif /* BROTLI_STATIC_INIT */ + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif diff --git a/packages/runtime/vendor/brotli/c/enc/dictionary_hash.h b/packages/runtime/vendor/brotli/c/enc/dictionary_hash.h new file mode 100644 index 000000000..dfddc017d --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/dictionary_hash.h @@ -0,0 +1,45 @@ +/* Copyright 2015 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* Hash table on the 4-byte prefixes of static dictionary words. */ + +#ifndef BROTLI_ENC_DICTIONARY_HASH_H_ +#define BROTLI_ENC_DICTIONARY_HASH_H_ + +#include "../common/platform.h" +#include "../common/static_init.h" + +#if (BROTLI_STATIC_INIT != BROTLI_STATIC_INIT_NONE) +#include "../common/dictionary.h" +#endif + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +/* Bucket is (Hash14 * 2 + length_lt_8); in other words we reserve 2 buckets + for each hash - one for shorter words and one for longer words. */ +#define BROTLI_ENC_NUM_HASH_BUCKETS 32768 + +#if (BROTLI_STATIC_INIT != BROTLI_STATIC_INIT_NONE) +BROTLI_BOOL BROTLI_INTERNAL BrotliEncoderInitDictionaryHash( + const BrotliDictionary* dictionary, uint16_t* words, uint8_t* lengths); +BROTLI_INTERNAL extern BROTLI_MODEL("small") uint16_t + kStaticDictionaryHashWords[BROTLI_ENC_NUM_HASH_BUCKETS]; +BROTLI_INTERNAL extern BROTLI_MODEL("small") uint8_t + kStaticDictionaryHashLengths[BROTLI_ENC_NUM_HASH_BUCKETS]; +#else +BROTLI_INTERNAL extern const BROTLI_MODEL("small") uint16_t + kStaticDictionaryHashWords[BROTLI_ENC_NUM_HASH_BUCKETS]; +BROTLI_INTERNAL extern const BROTLI_MODEL("small") uint8_t + kStaticDictionaryHashLengths[BROTLI_ENC_NUM_HASH_BUCKETS]; +#endif + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif + +#endif /* BROTLI_ENC_DICTIONARY_HASH_H_ */ diff --git a/packages/runtime/vendor/brotli/c/enc/dictionary_hash_inc.h b/packages/runtime/vendor/brotli/c/enc/dictionary_hash_inc.h new file mode 100644 index 000000000..89e04f0f4 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/dictionary_hash_inc.h @@ -0,0 +1,1829 @@ +const BROTLI_MODEL("small") +uint16_t kStaticDictionaryHashWords[BROTLI_ENC_NUM_HASH_BUCKETS] = { +1002,0,0,0,0,0,0,0,0,683,0,0,0,0,0,0,0,1265,0,0,0,0,0,1431,0,0,0,0,0,0,40,0,0,0, +0,155,8,741,0,624,0,0,0,0,0,0,0,0,0,0,0,0,66,503,0,0,0,451,0,0,0,0,0,0,0,835,70, +0,0,539,0,0,0,0,0,0,0,0,0,113,0,0,0,0,718,0,0,0,0,0,0,520,0,1070,0,0,0,0,0,1515, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,78,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,610,0,0,750,0,0,0,307,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,964,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,999,0,0,0,0,0,0,0,0, +645,75,0,649,52,282,0,200,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1621,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,211,225,0,0,687,718,0,0,110,0,58,0,0,0,0,0,0,345,0,0,301,0,0, +0,203,0,0,1154,674,1949,0,0,0,0,0,0,0,0,0,259,0,0,0,0,0,0,0,1275,0,0,0,1231,254, +0,0,0,0,0,0,0,277,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,248,0,0,800,0,0,0,29, +116,100,490,0,0,0,0,0,1641,0,543,0,0,0,0,41,181,0,657,0,0,202,25,0,0,0,0,0,0,0, +0,0,0,423,0,0,0,113,0,0,0,927,963,0,976,0,206,0,0,0,0,0,0,0,0,0,2002,0,0,0,0,0, +0,0,0,0,0,0,696,0,1170,0,0,0,0,226,13,0,769,678,551,0,0,0,0,0,0,57,0,0,0,10,188, +0,0,0,624,0,0,0,0,0,0,0,0,0,1941,130,0,0,0,0,378,269,0,0,528,0,1146,0,0,0,1105, +0,1616,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,656,0,1940,0,0,0,0,0,173,0,0,0,0,0,0,0,0,0, +0,0,457,342,810,0,0,0,0,620,0,0,0,0,0,0,0,967,95,447,406,0,0,0,477,0,1268,944, +1941,0,0,0,629,0,0,0,0,0,375,0,0,0,1636,0,0,0,0,774,0,1,1034,0,0,0,0,0,824,0,0, +0,0,0,118,0,0,560,296,0,0,0,0,0,0,0,0,1009,894,0,0,0,0,0,0,0,0,0,0,0,0,0,1474, +366,0,0,0,0,0,0,0,0,0,79,1723,0,0,200,0,0,0,0,0,0,0,0,1759,372,0,16,0,943,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,258,0,0,900,1839,707,30,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,2004,0,0,10,115,0,50,0,0,0,0,0,0,0,0,0,0,520,1,0,738,98,482,0,0,0,0, +0,0,0,0,0,0,701,2,0,0,0,0,0,0,0,0,557,0,0,0,0,0,0,0,0,0,347,0,0,0,0,572,0,0,0,0, +0,0,0,0,0,832,0,0,797,809,0,0,0,0,0,0,0,0,0,0,0,528,0,0,0,861,0,0,294,0,0,0,109, +0,0,0,0,0,0,0,0,1187,290,266,0,0,0,0,49,50,748,0,0,466,399,0,0,0,0,0,0,0,378,0, +519,0,0,0,0,0,0,0,0,0,0,0,0,667,351,902,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,180, +0,0,869,0,0,0,0,0,0,0,260,0,0,0,0,0,0,0,0,0,0,523,36,0,0,587,510,809,29,260,0,0, +0,0,0,0,0,0,570,0,565,0,1464,0,0,0,0,0,0,10,0,0,787,399,380,200,0,0,0,0,516,0, +844,887,0,0,0,0,0,0,0,44,0,0,0,305,1655,0,0,0,0,0,0,0,0,0,0,0,0,0,0,786,10,0,0, +0,0,0,0,0,0,0,2031,0,0,0,0,0,684,0,0,0,0,0,1480,0,0,0,27,0,0,0,395,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,813,511,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,56,0,0,0,206, +496,0,0,0,0,0,909,0,891,0,0,0,0,0,0,0,0,0,687,0,0,0,1342,0,0,0,0,0,0,0,0,0,0, +160,41,0,0,0,0,0,0,0,0,0,0,0,1718,778,0,0,0,0,0,0,0,0,0,0,1610,0,0,0,0,0,115,0, +0,0,0,314,294,0,0,0,983,178,193,0,0,0,0,0,0,0,0,0,174,0,0,0,0,0,0,0,0,0,0,848, +1796,0,0,0,0,0,0,221,0,687,1660,0,0,0,0,262,0,0,179,0,0,0,0,0,66,0,773,0,352,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,35,0,152,0,0,1197,0,0,0,0,0,0,0,0,0,0,0,0,560,0,0, +564,0,0,0,797,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,556,0,819,0,0,0,0,0,0,0,0,719,544, +637,5,0,0,0,0,0,0,0,0,0,0,0,101,0,1441,0,0,0,893,0,0,0,0,0,0,0,0,0,238,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,1296,0,0,969,1729,314,60,0,0,0,0,0,1144,0,1147,0,0,0,0,0, +0,0,0,0,0,437,1853,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,828,0,176,0,0,0,0,0,0,434,39,0, +0,0,0,0,159,0,0,0,902,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,270,0,0,0,0,801,556,0,0, +0,0,0,0,0,416,19,197,369,0,0,0,0,0,0,0,0,0,28,34,0,757,0,0,898,1553,0,721,0,0,0, +0,1012,0,0,0,0,1102,0,898,183,0,0,0,0,0,0,0,136,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,247,277,0,0,0,435,0,0,0,0,0,1311,0,0,0,0, +0,0,211,437,0,0,0,28,0,0,750,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2012,0,702, +0,808,0,0,0,0,739,166,0,0,0,0,0,0,719,170,500,0,0,0,0,0,0,0,0,1500,327,0,0,450, +0,0,0,1318,0,0,0,1602,0,0,331,754,0,0,0,0,0,1368,0,0,557,0,0,0,799,850,0,0,0,0, +0,0,0,0,908,0,0,0,0,0,19,62,459,0,0,0,0,0,0,0,0,0,0,0,0,1802,0,0,0,0,0,0,0,0,0, +1397,0,0,0,0,120,238,0,0,0,0,0,0,0,0,0,0,0,1324,0,0,0,0,0,0,0,0,602,201,0,0,164, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,615,0,0,0,0,0,0,0,0,0,0,0,0,0,1243,0,0,0,0,968,0,0, +0,0,0,0,882,0,0,0,907,329,100,0,0,0,0,0,0,0,0,0,0,0,176,26,9,0,0,265,256,0,0,0, +0,0,0,0,0,0,643,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,127,610,0,0,0,0,973,2001,0, +0,0,0,0,0,522,0,0,0,0,0,0,0,0,0,0,0,553,0,0,0,0,0,0,1582,0,1578,0,0,0,0,0,0,0,0, +0,0,0,795,0,0,0,432,0,0,0,0,0,0,84,126,0,0,0,0,790,0,377,64,0,1529,0,0,0,0,530, +1857,539,1104,0,0,0,0,0,0,0,0,0,0,0,0,977,0,0,0,34,0,0,0,0,0,0,0,0,0,0,0,24,26, +0,0,918,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,183,379,0,0,0,0,0,0,0,792, +0,0,0,0,0,0,0,0,0,1920,0,0,0,0,0,0,0,0,0,771,0,0,0,1979,0,901,254,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,140,0,0,0,0,0,440,37,0, +508,0,0,0,513,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,533,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,752,920,0,1048,0,153,0, +0,391,0,0,1952,0,0,0,0,0,0,0,0,0,0,126,0,0,0,0,640,0,483,69,1616,0,0,0,0,0,734, +0,0,0,0,0,0,480,0,495,0,472,0,0,0,0,0,0,0,0,874,229,0,0,0,0,948,0,0,0,0,0,0,0,0, +1009,748,0,555,0,0,0,0,0,0,193,0,653,0,0,0,0,0,0,0,0,0,0,984,0,0,0,172,0,0,0,0, +0,0,0,0,83,1568,0,0,384,0,0,0,0,0,0,0,164,880,0,0,0,0,0,0,0,0,0,0,0,367,121,0,0, +828,0,0,0,0,0,0,0,1541,0,0,0,0,0,0,0,343,0,0,0,0,0,0,0,0,561,57,0,0,0,0,0,0,0, +926,0,0,0,0,827,0,194,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,22,0,0,0,0,0,0,0, +0,0,0,896,1249,0,0,0,0,0,1614,0,0,0,860,0,0,0,0,0,0,0,0,964,102,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,899,0,569,0,0,0,0,795,2045,0,0,0, +0,0,0,104,52,0,0,0,0,0,604,0,0,0,0,779,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,32,0,0,0,0, +494,0,677,0,0,0,0,0,0,0,508,0,0,0,0,0,0,0,0,0,1014,0,957,0,0,630,310,0,0,0,570, +0,0,449,0,64,537,0,0,0,0,0,0,0,244,0,0,0,0,0,0,0,0,0,0,0,0,0,0,702,1650,49,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,338,0,0,0,0,1279,0,0,0,0,0,0,0,896,0,0, +178,0,0,0,0,0,0,0,0,0,0,0,0,0,808,695,0,0,0,0,539,1117,0,0,0,0,0,0,0,0,257,0, +1003,0,0,0,1,448,0,516,0,0,960,0,125,4,0,1268,30,748,0,0,852,0,0,0,6,0,0,848, +236,1385,862,1811,0,0,0,0,698,803,0,0,0,0,0,0,0,610,992,0,0,878,0,1847,0,0,0,0, +0,0,0,383,0,1404,0,0,0,0,986,0,347,0,0,0,0,0,0,0,0,0,0,0,592,572,0,1411,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,64,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,606,0,0,0,0,0,0, +0,0,0,0,0,0,0,1829,0,0,0,0,0,0,0,0,0,0,0,0,700,748,0,0,0,0,0,0,365,0,0,127,0,0, +83,198,0,0,0,0,0,0,864,55,0,0,0,0,726,1752,0,0,0,0,0,0,0,0,0,0,0,0,0,1066,0,764, +0,0,0,0,683,0,550,309,0,0,874,1212,0,0,0,1364,0,986,381,723,0,0,0,1573,0,0,0,0, +0,1025,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1559,0,0,0,0,493,133,0,0,0,0,148, +119,0,0,0,0,0,0,537,14,541,0,635,126,0,0,0,495,0,0,0,0,861,998,1009,0,0,0,0,0,0, +0,359,368,0,0,0,0,304,1577,0,0,0,0,0,1107,0,0,0,0,0,929,0,0,0,1142,0,0,0,0,289, +175,0,432,0,219,0,0,0,0,0,785,0,0,595,0,0,0,0,0,0,0,0,0,0,0,0,0,80,0,0,0,0,0,0, +931,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1323,0,0,0,0,290,0,559,1751,127,0,0,0, +934,1167,0,963,0,260,0,0,0,573,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +580,1689,0,0,0,0,0,0,0,0,0,1164,0,0,982,1922,0,63,0,0,0,0,0,793,0,0,0,0,0,0,0,0, +0,0,0,0,0,67,790,0,0,0,0,0,0,0,0,0,0,391,443,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,271,0,0,0,0,0,0,0,0,0,0,0,1140,0,0,0,0,340,300,0,897,0,0,0,0,0,0, +0,0,0,0,890,0,0,0,0,818,321,53,0,0,0,0,0,0,0,0,0,468,0,243,0,870,0,0,0,1765,121, +0,0,0,180,518,0,822,419,634,0,0,0,0,0,0,0,0,0,898,0,0,0,0,454,36,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,806,0,0,0,0,0,0,0,0,0,0,0,0,1326,0,104,0,0,0,0,0,0,0, +0,0,260,0,0,0,0,0,0,0,0,0,0,0,0,542,45,0,0,263,1516,42,0,0,0,0,0,468,0,1005,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,288,87,0,0,0,0,0,0,0,0,502,988,133,0,0,0,0,0,0, +141,0,0,872,1842,0,0,0,0,0,0,0,0,261,619,0,0,0,0,189,246,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,678,0,0,0,0,0,0,0,0,0,0,0,0,285,35,0,517,0,0,0,0,0,0,0,0,0,0, +540,214,667,0,74,0,0,125,0,0,0,0,0,761,131,0,0,0,0,0,0,0,0,0,0,0,0,0,333,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1338,94,0,0,0,0,0,0,0,0,0,0,0,0,449,0,646,103, +86,641,2028,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,869,87,277,117,39,0,0,0,0,0,0,0,0,938, +297,0,0,0,0,558,464,0,0,0,0,0,0,0,0,0,0,731,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1608,0, +0,0,0,0,0,0,1429,0,0,733,1010,0,0,338,1656,0,0,0,1038,979,2010,0,0,0,0,0,0,0, +1005,0,0,121,0,0,0,219,20,0,0,0,0,0,0,872,1440,0,0,0,683,0,1070,0,0,522,0,0,0,0, +439,669,0,0,0,0,0,0,0,0,1245,0,0,0,0,0,1218,0,0,547,233,0,0,0,0,0,0,0,0,0,482,0, +0,0,0,0,0,0,886,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,795,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,371,0,0,0,0,0,0,0,0,0,0,0,0,0,622,0,625,0,0,0,339,29,0,0,338,0,0,0, +0,130,0,0,0,0,0,0,0,0,0,307,0,0,0,0,0,0,0,0,0,0,2044,0,0,0,0,0,0,0,0,308,770,0, +0,0,0,0,1266,0,0,0,0,0,0,0,0,0,400,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,690,739,0,0, +0,0,0,0,0,990,0,0,0,1831,0,0,0,0,0,0,0,0,0,0,0,0,0,613,0,0,0,0,0,0,0,0,0,0,0,0, +0,763,0,878,0,0,0,977,0,100,0,0,0,0,0,0,0,0,0,463,0,0,0,0,623,318,0,0,296,463, +137,0,0,454,0,0,0,1527,58,0,0,0,0,0,0,0,18,48,0,0,0,0,0,729,0,0,0,442,0,0,0,0, +40,449,0,853,0,0,0,0,0,0,227,0,0,0,0,0,0,1491,0,0,0,0,0,0,0,0,0,0,161,55,0,450, +0,1174,62,0,207,0,0,0,0,0,0,0,0,869,0,0,0,0,80,213,0,0,0,0,0,0,0,0,0,0,354,820, +0,0,747,0,0,0,954,0,0,1073,0,556,0,0,0,692,0,191,0,804,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,831,162,0,0,35,0,0,0,0,0,0,0,0,1235,0,0,0,0,0,1234,0,0, +0,0,0,0,0,0,0,0,96,0,0,0,0,0,0,0,149,0,0,0,902,204,0,0,833,0,287,366,0,0,0,0,0, +0,992,2020,0,0,0,0,0,0,0,0,0,0,0,356,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,784,0,0,567, +630,0,0,0,539,0,0,27,0,0,0,0,0,0,0,0,0,0,755,0,0,0,0,0,0,0,0,0,0,0,0,814,0,0,0, +0,0,0,0,0,0,0,0,0,0,987,0,0,255,761,194,0,1086,0,0,0,0,0,0,1016,0,0,1396,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,562,271,913,0,0,0,0,0,0,0,0,320,153,45,475,0,0, +0,0,0,0,0,713,0,327,0,0,0,0,0,0,604,552,3,359,0,0,0,0,853,80,0,0,0,0,0,0,0,2016, +6,887,0,0,0,0,975,0,961,0,0,0,0,0,916,1891,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,100,101,390,708,0,0,0,587,983,512,0,0,0,0,0,0,0,0,0,0,0,645,0,0,0,851,0,0,0, +0,0,498,140,217,0,0,0,1448,0,0,0,0,0,0,0,0,0,905,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +643,105,0,792,0,0,0,0,0,0,0,0,0,0,0,0,56,0,0,0,0,0,0,0,0,0,0,535,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,1748,0,0,0,0,0,754,0,0,0,0,0,0,0,0,0,0,0,0,91,0,0,1565,0,91,792, +939,3,370,0,0,0,0,95,0,0,0,0,551,7,619,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1150,0, +0,0,0,0,0,0,0,0,0,0,0,0,671,0,0,0,0,0,888,368,149,0,0,105,1134,0,983,0,0,458,31, +0,643,0,0,0,312,0,740,0,0,0,1642,0,0,0,0,0,0,0,236,0,0,0,0,0,0,0,59,68,0,0,0,0, +0,867,795,0,0,0,0,970,1977,0,0,0,0,0,0,0,1148,0,775,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,970,0,0,0,0,0,0,0,0,0,665,71,0,0,0,0,827,0,0,0,0,0,0,0,0,0, +0,479,0,0,0,0,0,0,0,0,99,607,0,0,0,0,0,0,0,1960,0,0,0,793,0,0,871,41,0,0,241,94, +0,0,0,0,209,0,0,1497,0,0,0,0,0,0,0,0,0,98,0,0,0,463,0,0,0,0,291,0,0,0,0,0,0,0,0, +0,0,984,0,0,0,0,0,205,0,0,0,0,0,0,205,42,0,801,0,0,0,0,0,635,0,0,533,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,371,0,1282,0,0,0,825,0,0,0,0,0,0,0,0,0,357,879,467,0,317,0,0, +0,0,0,0,0,924,0,0,0,0,849,1795,0,0,0,0,895,1799,43,0,0,0,0,0,0,0,0,0,0,1820,0,0, +0,0,0,0,0,525,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,110,0,493,0,174,417,0,0, +0,0,0,583,733,0,0,0,0,0,0,481,215,0,0,0,0,477,0,0,0,0,0,0,0,0,308,0,0,0,0,0,0,0, +0,297,126,0,0,361,1551,0,0,0,0,0,0,871,1807,0,0,0,0,0,1307,0,685,0,0,0,0,0,0,0, +797,0,858,0,565,0,0,0,0,0,0,0,0,0,0,0,0,434,252,826,0,0,0,0,0,0,791,0,0,0,0,509, +231,178,601,0,0,0,0,0,0,0,0,43,1591,0,0,0,0,0,1683,0,0,0,0,45,0,0,0,0,0,0,0,0,0, +0,1120,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,556,494,0,398,0,0,0,1030,0,0,0,0,0,0, +168,0,0,0,0,0,0,0,0,0,0,973,0,642,0,0,0,0,0,0,0,0,0,1615,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,378,594,0,1093,0,679,112,0,0,0,0,1492,540,1374,714, +1486,0,0,0,0,825,1511,0,0,0,0,0,0,0,0,0,0,0,0,0,952,0,0,736,143,0,700,0,1540,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1557,0,0,0,860,990,0,0,0,807,0,0,0,0,0,131, +515,0,646,0,0,0,0,117,728,508,121,0,0,0,0,0,0,357,0,0,0,0,0,0,237,0,0,0,0,0,0,0, +0,0,1784,0,0,0,0,0,0,0,0,0,0,0,713,348,1536,0,738,0,0,0,0,0,0,0,434,0,0,0,0,0,0, +366,1877,39,0,0,0,0,0,0,580,0,0,0,0,0,0,0,0,0,0,0,0,0,0,873,0,0,0,0,171,0,625, +550,107,343,943,0,0,0,0,0,0,0,768,0,0,0,0,0,0,0,799,0,0,0,894,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,1673,0,0,0,0,0,0,0,0,0,0,0,1052,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +272,0,441,0,0,3,9,0,0,0,1182,0,1346,0,0,0,0,0,0,0,0,682,0,0,1004,24,0,0,968,0,0, +0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,185,0,0,0,578, +474,0,0,0,0,0,0,0,0,0,0,0,0,0,0,113,530,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,556,0,0,0,0,0,0,16,1317,0,0,97,0,0,0,703,0,0,0,0,0,0,0,0,892,0,0,0,1571,0,0, +426,186,0,1101,0,0,0,0,0,0,0,0,937,585,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,644,291, +0,0,0,0,749,0,162,0,0,381,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,762,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,628,21,0,0,0,0,0,0,0,0,919,0,0,0,0,0,0,0,0,0, +633,0,0,0,0,332,0,0,0,0,0,0,0,0,0,1489,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,832,398,0,645,0,0,0,13,0,0,0,0,0,0,0,0,0,0,20,0,800,0,0,0,0,0,0,0,0,0, +0,0,0,0,1993,0,0,0,0,769,0,0,0,665,0,0,0,0,0,0,0,0,0,0,1426,0,0,0,0,60,0,0,0, +641,1874,0,644,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1757,0,0,0,0,0,937,0,1652,0,654,0, +0,0,0,0,0,0,527,0,0,0,0,0,0,0,0,0,0,0,0,0,226,0,0,0,0,0,1486,0,0,0,0,0,0,0,0,0, +0,0,325,0,0,0,0,0,0,0,1345,0,0,91,0,404,0,0,0,0,0,0,0,0,0,0,0,0,973,0,0,0,0,0,0, +0,1176,0,549,0,0,0,0,0,0,0,0,0,0,976,0,0,0,0,0,21,0,0,0,0,0,51,0,0,0,0,314,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,198,6,0,1093,0,0,0,0,0,0,0,0,0, +0,0,0,0,1776,0,0,0,0,0,1528,0,419,0,0,0,0,0,0,0,0,76,138,0,0,0,0,638,29,0,0,0,0, +0,0,0,1418,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1710,0,0,0,0,0, +0,0,0,0,0,0,0,532,23,0,0,0,0,0,0,0,862,0,0,946,592,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,70,0,0,0,0,0,0,0,0,0,812,0,0,0,76,0,0,988,0,442,0,0,0,896,0,0,0,0,0,0, +483,0,0,0,0,1709,0,0,0,0,0,0,119,0,0,0,117,0,309,0,0,0,0,0,596,976,0,0,0,0,0,0, +0,0,0,0,0,768,0,0,0,0,0,0,0,0,0,518,0,0,0,0,0,0,0,0,0,0,0,0,0,0,863,0,0,0,24, +145,1020,0,0,1984,0,0,0,0,0,0,0,658,0,0,0,0,0,0,0,0,0,0,106,1827,0,1010,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,582,87,0,0,0,0,0,0,0,267,0,0,0,703,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,496,0,0,0,0,1121,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,249,561,0,0,0,0,0, +0,0,760,0,0,154,0,0,0,255,0,419,323,0,0,0,0,0,368,0,0,0,0,0,0,0,0,0,0,522,0,0,0, +0,0,0,0,551,562,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,92,0,0,0,0, +0,0,0,284,525,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,958,0,0,594,0,0,0,0,0,0,6,479,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,61,0,0,0,0,0,0,0,820,1641,0,1556,0,0,0,0,0,0,0,302,0,0, +0,0,0,148,0,0,676,0,0,0,0,0,0,1674,0,0,0,0,0,0,178,0,0,0,0,0,0,0,94,389,0,0,0,0, +91,8,0,0,0,0,0,0,0,0,0,0,112,0,0,0,0,0,0,0,0,0,0,747,0,0,0,0,0,0,0,1746,0,0,0,0, +0,24,0,1352,158,1530,0,0,718,130,280,1401,0,0,0,0,0,1946,8,0,0,0,0,1607,0,0,0,0, +0,0,882,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,417,0,0,0,1597,633,433,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,18,234,0,0,0,0,0,0,0,0,680,1950,0,0,0,0,249,5,0,0,0, +0,0,0,0,0,0,1216,0,1773,0,0,0,0,0,0,0,0,0,0,0,0,0,0,509,180,0,0,0,0,0,0,0,1002, +0,0,0,0,0,0,0,0,0,0,0,0,0,931,0,0,0,0,0,0,0,0,747,943,0,1837,0,0,0,0,0,0,0,641, +0,0,0,0,280,0,0,0,5,0,0,0,0,0,72,545,0,0,0,0,0,0,0,0,0,742,0,0,254,151,872,0,0, +0,0,0,0,0,0,0,0,0,0,921,0,0,517,833,0,1680,0,0,436,251,584,0,0,0,0,0,0,0,0,0,0, +0,24,500,0,0,0,0,0,0,0,0,195,1775,514,389,0,0,0,0,0,0,0,743,0,0,0,0,0,0,292,0,0, +0,227,1283,774,1805,0,0,0,0,0,0,0,0,0,0,119,81,0,0,0,0,0,0,0,0,0,0,0,0,0,0,913, +1910,0,0,0,1826,490,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1162,700,30, +0,0,0,721,839,0,0,0,617,0,0,0,0,0,0,0,0,0,169,428,0,0,0,0,0,1648,637,1205,0,0,0, +1596,0,0,4,266,0,0,0,0,0,0,0,0,0,0,0,862,0,0,0,0,0,0,0,0,0,0,16,0,0,0,0,0,0,0,0, +0,279,157,391,604,0,0,713,945,877,973,0,0,0,0,0,0,0,0,0,0,0,0,0,0,859,567,628, +1846,0,0,0,0,0,0,0,0,0,762,0,0,191,0,0,0,0,298,0,0,767,909,0,0,0,0,0,0,0,795,0, +0,301,0,0,1970,0,0,0,0,0,0,0,0,0,1236,0,0,0,0,0,0,644,369,15,0,160,71,0,0,0,0,0, +1447,0,0,0,0,0,0,0,0,735,1255,76,0,0,0,0,0,0,0,0,0,0,474,0,0,0,0,0,0,0,0,0,0, +841,0,0,0,0,0,0,0,0,0,0,836,0,0,0,0,0,1622,0,0,735,0,0,0,0,1601,804,1390,394,0, +0,0,0,0,0,96,0,289,0,0,35,688,0,0,0,667,0,513,0,0,0,0,0,0,0,2034,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,704,0,1524,0,1078,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,306, +0,0,0,0,0,0,0,431,0,1196,0,0,54,0,15,1448,0,1418,0,0,0,0,0,0,0,0,0,907,0,0,0,0, +0,0,194,1767,0,0,0,0,0,840,0,900,0,0,0,0,0,0,0,0,0,0,0,1436,0,0,0,0,642,1560,0, +0,0,0,0,0,94,386,0,0,0,0,0,0,0,0,0,0,830,416,0,0,20,731,0,0,0,0,0,0,0,0,697,0,0, +662,0,0,0,0,0,0,0,0,0,861,0,0,0,0,0,0,0,871,671,864,0,928,7,0,332,0,0,0,0,1055, +0,0,0,0,0,0,986,0,0,0,0,0,44,76,0,0,0,0,0,0,0,0,0,0,300,0,0,0,0,0,0,0,175,518, +831,1108,0,0,0,836,0,1852,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,843,1804,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,246,0,0,0,610,202,0,0,36,0,0,0,240,654,13,0,0,0,0,0,0,0, +0,391,0,403,0,0,0,0,0,0,0,0,0,0,75,0,366,815,0,0,631,0,0,0,0,0,0,0,0,345,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,952,0,0,0,0,0,0,0,0,0,0,0,673,35,662,0,287,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,5,34,0,0,0,0,0,0,0,0,151,0,427,0,0,382,0,0,0,329,0,0,279,0,0,0, +0,0,0,0,0,0,0,906,0,0,366,843,0,1443,0,1372,992,0,36,123,0,649,0,0,0,0,0,767,0, +1018,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,995,0,0,0,0,0,0,0,72,368,0,0,1345,0,0,0, +589,0,0,0,0,0,0,0,0,0,1988,0,0,220,541,0,0,0,686,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,32,196,0,0,0,0,0,0,0,0,0,0,0,0,0,381,0,0,0,0,0,0,0,0,0,1452,0, +0,0,616,0,0,0,0,0,0,0,0,0,1229,0,0,0,0,0,0,0,0,0,0,667,120,0,0,0,0,0,0,0,1146,0, +0,0,0,0,0,0,0,0,0,0,352,0,0,0,0,0,293,0,0,0,0,0,0,0,0,0,0,0,0,0,935,0,1050,0, +147,88,0,0,923,0,0,0,0,0,934,0,0,0,0,0,0,0,0,114,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,341,222,0,0,0,0,0,0,0,0,0,0,293,0,0,0,0,0,0,0,0,0,0,0,0, +637,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1159,0,0,0,847,0,0,0,0,0,0,683,0,867,944,0,0, +0,0,0,1809,0,0,0,0,0,0,0,0,0,0,395,170,0,0,0,0,0,0,0,0,0,0,618,535,0,1625,0,0,0, +0,0,0,0,0,23,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,778,0,0,0,0,0,46,0,2032,0,0,37, +1458,0,938,363,34,0,0,0,0,0,0,0,0,0,0,0,0,0,0,314,0,0,0,0,0,0,889,0,0,0,0,0,0,0, +0,0,0,0,462,0,0,0,0,525,0,0,23,0,0,0,0,0,0,0,0,0,0,0,676,0,0,0,0,0,0,0,0,0,0,0, +0,498,725,0,0,0,0,7,0,0,0,0,773,0,0,0,164,0,0,0,0,0,0,0,0,936,583,659,1462,0, +220,0,0,0,0,803,0,0,544,119,0,0,0,0,0,0,0,0,0,0,0,181,176,0,1192,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,1878,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,26,0,0,0,0,0,0, +944,0,0,0,0,0,0,0,273,0,0,0,0,0,855,0,0,0,0,5,127,0,0,0,0,0,0,0,0,752,230,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,68,162,0,654,48,156,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,240,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,197, +0,0,0,0,0,0,0,963,0,0,0,0,0,0,0,0,0,0,858,0,0,0,0,0,0,0,0,0,0,676,1978,0,0,102, +972,0,0,0,0,0,0,0,361,0,461,0,0,0,472,0,0,0,0,0,0,0,0,0,0,0,0,0,0,747,905,0,0,0, +155,0,0,0,0,0,0,0,0,0,0,319,163,0,0,0,0,0,0,0,0,0,848,0,0,36,631,0,0,0,0,0,1769, +0,0,0,0,0,144,0,0,0,0,0,0,0,0,0,0,369,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,555,247,0,0, +996,0,0,189,0,0,0,0,0,0,0,0,0,0,280,0,0,0,0,0,0,0,0,0,0,0,526,746,0,0,345,0,0,0, +1017,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,651,428,0,0,0,1162,230,327,546,792,0,0,0, +1203,0,0,0,0,0,0,0,0,0,672,189,0,0,0,0,0,0,99,0,0,0,298,0,0,0,0,0,0,555,397,0,0, +0,0,0,1157,0,0,0,0,0,0,0,0,0,0,398,1523,0,366,0,0,787,0,0,0,282,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,157,0,941,0,0,0,0,0,1336,0,0,116,0,0,0,0,0,0,787,0,0,0,0,0,0,0,0,0, +0,170,160,0,1815,0,0,0,0,0,866,0,0,0,0,0,0,0,0,0,689,0,0,0,0,820,0,498,108,0,0, +0,1119,0,0,0,244,609,1005,0,581,0,0,0,0,0,895,0,0,0,1898,0,0,0,0,0,926,0,0,0,0, +0,0,0,0,0,0,0,0,0,538,496,294,301,0,0,0,18,0,0,757,0,0,0,0,0,1263,0,820,0,722,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2028,0,0,0,0,124,1875,0,0,0,881,0,0,0,1348, +0,0,0,0,0,0,0,911,0,954,0,0,0,0,414,0,0,0,0,517,0,0,0,0,0,816,0,0,0,0,0,0,0,0, +713,0,0,0,0,0,0,0,33,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,593,150,0,0,0,0, +0,553,0,0,0,0,0,0,0,0,0,0,108,0,0,0,0,420,0,0,0,0,0,0,0,0,0,0,0,1777,0,0,55,493, +0,0,81,0,321,980,0,0,0,0,0,0,0,0,0,0,0,0,0,0,362,112,0,74,0,0,0,0,0,0,0,625,0,0, +0,0,0,0,377,16,0,0,61,281,0,0,0,0,0,0,0,0,0,0,0,0,0,0,224,1031,0,0,0,0,0,0,51,0, +0,0,0,0,0,0,211,309,15,125,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,789,173,0,439,9,648, +0,0,294,0,0,0,0,0,0,0,374,8,0,1099,0,0,0,0,0,0,0,575,0,0,0,518,0,0,0,702,0,0,0, +0,0,0,87,0,0,0,438,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,464,122,0,0,0,1802,0,0,0,0, +0,0,499,0,0,0,87,476,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,840,283,0,0,0,0,1620,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,609,1160,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,600, +323,372,0,0,0,0,471,722,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,0,0,0,0,0,0,0,0,0,0, +477,1304,0,1774,0,0,88,0,438,12,0,0,0,0,0,0,0,0,671,997,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,639,22,0,0,782,681,0,0,0,0,0,0,0,0,0,0,1013,664,0,942,0,1349,0,0,0,0,0,0,0, +0,0,0,0,0,356,0,0,32,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,215,289,0,1975, +109,450,0,0,0,0,0,0,0,0,0,0,705,0,0,664,0,0,0,0,0,0,0,1238,0,0,318,0,0,0,0,0,0, +0,0,0,0,0,0,0,960,1872,0,0,0,0,0,0,0,0,0,0,0,0,0,0,103,0,0,0,0,0,0,0,0,0,239, +777,0,26,0,0,0,0,0,0,0,0,0,0,0,0,375,414,0,17,0,0,0,1350,0,955,0,0,0,0,0,0,0,0, +887,960,0,0,0,0,0,0,0,0,0,0,708,710,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,919,0,0,0, +0,502,280,7,45,0,0,0,0,777,0,0,0,0,410,0,1110,0,0,0,0,0,0,414,341,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,787,0,0,0,436,0,0,0,0,0,0,0,1707,613,377,96,0,0,0,0,451, +0,0,0,0,0,0,0,0,0,0,0,0,0,680,0,483,916,0,0,0,0,0,0,937,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,739,0,0,0,0,0,0,0,0,82,0,0,663,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,128,0,0,0,0,0,0,0,0,1087,0,0,0,0,0,0,0,503,0,0,0,0,0,0,9,113,104,324,0,460,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,935,702,434,485,1014,949,423,0,900, +0,0,0,0,0,0,0,2018,574,0,0,0,0,0,0,0,0,0,0,0,0,1206,0,0,0,0,0,0,0,0,38,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,1022,0,0,0,0,143,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,2029,0,0,0,0,0,0,0,0,0,0,0,0,523,0,0,0,0,0,0,625,0,0,425,37,0,0,0,1943,0,0,0, +0,0,765,0,0,0,0,0,0,0,0,0,0,551,0,0,0,0,0,0,0,0,0,0,0,0,168,0,0,1010,0,0,1994,0, +0,0,91,0,0,0,0,532,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1884,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,240,15,0,0,0,1227,0,1534,0,0,0,0,0,0,0,0,0,0,0,0,0,0,392,0, +0,0,0,0,0,0,0,0,0,0,0,655,562,395,0,0,0,501,1019,0,0,0,0,509,267,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1099,0,0,0,0,0,0,948,0,0,0,0,0,0,0, +462,114,0,0,258,404,0,1717,0,0,0,0,82,1061,0,724,0,0,0,0,0,1133,0,0,0,0,0,0, +1021,841,0,1021,0,0,0,0,0,0,0,0,0,0,488,373,37,0,0,0,0,564,0,0,0,0,0,513,0,0,0, +825,0,0,899,0,0,778,0,0,12,1417,0,1116,0,0,0,0,0,0,0,0,0,0,0,0,0,0,114,545,0,5, +0,0,0,0,0,0,0,192,0,0,763,0,0,0,0,0,0,0,755,759,0,0,0,0,0,0,0,0,0,370,0,1237,0, +0,0,0,0,0,298,87,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,31,0,0, +0,0,0,0,814,991,0,757,57,0,0,0,0,0,0,0,0,0,540,0,0,0,0,608,0,0,0,0,0,0,0,0,1014, +0,0,0,902,0,0,0,0,553,1668,0,0,0,0,0,0,0,0,0,559,60,0,0,0,0,0,511,0,0,675,0,0, +156,0,0,0,0,0,0,709,0,698,0,0,0,1745,0,0,0,0,0,0,0,0,0,714,0,0,0,0,0,0,0,0,206, +8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,776,0,0,0,0,0,0,0,0,0,1272,0,0, +0,0,0,1059,0,0,0,0,0,0,406,0,0,0,0,0,0,0,0,0,0,947,0,0,0,0,0,0,168,0,0,0,0,0,0, +870,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,554,0,0,0,0,784,908,0,0,0,0,0,0, +0,396,358,0,0,0,0,0,0,0,0,2,228,0,0,0,0,0,0,0,0,0,0,0,845,14,0,716,1820,594,0, +81,1428,0,161,0,782,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,64,0,0,0,0,0,998,0, +0,0,0,0,0,0,0,0,0,0,0,1043,0,1496,0,0,0,0,0,0,0,0,781,0,0,0,0,0,0,0,817,1114,0, +1814,958,0,0,0,0,812,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,139,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,236,643,0,0,0,0,0,0,0,0,0,1172,0,0,0,0,0,0,0,0,0,1338,0,0,0, +0,0,0,0,0,0,0,0,54,0,0,0,256,0,0,351,0,955,1885,0,469,0,0,0,1270,0,744,0,313,0, +0,0,0,0,0,0,0,402,969,0,0,0,0,0,0,50,0,0,0,0,572,0,0,0,0,847,0,0,0,0,0,0,0,248, +43,0,369,0,0,0,0,0,0,0,0,0,0,0,0,0,766,0,363,0,0,0,0,0,0,0,0,0,0,0,678,0,0,409, +258,82,249,0,0,0,0,0,0,0,0,0,0,0,0,32,393,0,788,0,0,0,1281,509,1968,0,0,0,0,39, +291,0,0,0,589,0,0,54,1059,0,0,0,0,0,0,824,0,0,0,0,0,0,0,0,0,0,1005,0,1598,0,0,0, +0,0,919,0,0,0,0,0,0,0,0,52,132,0,0,0,0,0,328,0,0,0,0,173,0,0,0,0,0,65,1411,0,0, +0,0,0,0,0,0,0,0,442,0,842,0,0,0,0,0,0,0,0,0,534,0,0,0,0,0,0,0,0,0,0,0,0,0,845, +210,0,0,0,0,0,0,0,0,892,0,0,223,0,0,0,0,529,0,0,0,807,0,137,218,0,1444,0,0,0,0, +0,332,661,0,0,0,0,0,0,0,76,1517,0,0,0,0,0,0,0,0,0,0,0,418,0,0,0,0,0,0,0,0,481, +379,0,0,0,0,0,149,18,0,0,0,0,0,0,0,0,742,304,142,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,799,925,195,51,0,0,0,0,688,0,0,0,0,697,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,1169,751,0,0,0,452,929,0,221,0,1437,0,0,0,0,955,1251,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,192,0,132,0,0,0,0,0,865,0,0,0,0,0,0,0,767, +672,42,0,0,0,1050,0,0,0,0,0,0,0,0,368,44,0,0,0,0,0,0,0,570,29,0,0,0,0,0,0,227,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,522,0,0,0,0,0,0,0,1529,0,0,0,0,0,0,739,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,1667,0,0,0,0,0,0,132,511,0,138,208,1020,0,0,23,565,0,344,0,0,0, +0,0,922,0,0,0,0,0,0,0,240,0,0,415,171,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,402,0,0,754,31,716,0,982,731,0,0,0,0,0,0,0,888,0,0,0,803,847,0,0,823, +0,0,0,0,0,0,785,0,0,2,0,0,0,0,0,0,0,532,0,0,681,0,0,314,0,384,684,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,649,447,0,1818,1007,0,321,0,66,360,0,0,0,385,0,0,0,0,0,0, +0,900,73,254,0,0,0,0,683,1959,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,86,0,0,725,0,0,0,0,0,196,0,0,0,0,0,831,0,0,0,0,723,0,0,0,0,0,994,627,0,0, +0,0,0,0,0,0,0,0,764,66,0,0,0,0,205,36,0,0,0,0,0,0,0,950,0,0,0,887,111,0,0,831, +388,165,0,0,0,0,0,155,0,0,0,0,0,0,0,0,0,0,0,0,0,0,780,755,0,0,0,0,898,146,0,0,0, +0,0,0,0,45,7,0,0,0,0,0,0,0,0,607,0,0,0,0,0,0,65,0,0,0,0,0,0,0,0,0,88,0,0,0,0,0, +621,600,0,367,0,0,0,0,0,0,0,561,0,559,0,585,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +287,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,672,157,0,0,0,0,714,0,0,0, +0,0,456,0,925,0,0,0,0,0,0,0,0,19,0,0,0,0,1473,0,0,0,0,0,0,0,0,0,0,113,0,0,0,0,0, +0,0,0,0,0,0,0,0,69,463,0,0,82,193,2,471,0,0,0,0,633,0,0,0,0,0,0,1148,129,1392, +542,803,0,0,0,0,0,0,0,0,0,0,0,0,438,0,0,0,0,0,0,875,0,0,0,0,0,237,0,0,0,0,0,0,0, +65,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,563,0,0,0,9,444,0,0,43,1260,0,0,0,0,0,0, +971,0,0,699,0,0,0,0,0,1116,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,829,242,0, +0,593,0,0,0,0,0,0,0,0,201,36,224,0,0,0,0,0,0,1430,0,1806,0,523,0,0,212,1889,0,0, +0,827,0,0,0,0,0,2043,136,242,0,0,0,0,0,0,284,148,10,0,0,0,0,0,0,1249,0,0,0,807, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,94,0,0,0,494,0,0,0,0,0,0,0,0,1510,0,0,0,0,0, +0,0,0,0,0,505,1306,0,0,764,268,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,384,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1703,0,0,0,0,159,964,583,0,0,0, +0,0,0,515,0,0,854,0,0,0,0,0,0,0,0,0,0,0,0,1123,0,0,0,0,0,0,0,136,0,0,0,0,0,1782, +0,0,44,1287,0,0,0,0,0,732,0,0,0,0,313,679,0,0,316,0,0,0,0,595,0,0,0,0,0,0,753, +147,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,137,0,0,0,0,414,0,1762,0,0,0,0,0,0,0,0, +0,0,0,599,0,0,0,0,0,0,0,0,0,1749,0,0,0,1627,0,488,0,0,0,0,0,83,0,0,0,0,676,0,0, +1639,0,0,0,0,0,0,0,0,0,278,0,0,0,0,0,0,97,0,14,1085,0,0,0,0,0,0,781,388,0,849, +59,229,0,0,0,0,0,1115,0,0,0,0,108,0,0,0,0,700,0,0,0,0,0,0,0,0,0,1414,0,0,0,0,0, +0,0,0,0,0,0,0,0,660,737,1035,0,0,0,0,0,0,521,690,0,0,0,0,0,0,0,0,0,0,0,0,272,0, +0,0,0,0,0,0,0,0,0,1744,0,0,0,0,0,0,128,733,0,0,277,0,0,0,0,0,0,0,0,0,4,0,0,0,0, +0,0,0,0,0,0,0,0,0,936,1981,40,0,0,0,0,0,0,0,0,775,0,0,0,0,0,0,0,0,0,306,0,0,0,0, +0,0,0,979,0,0,0,0,0,611,0,0,0,0,0,178,0,0,0,1969,0,0,0,0,0,0,0,664,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,390,0,0,0,1510,0,0,0,0,0,0,0,0,0,0,0,493,0,0,37,0,0,0,0,724,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,96,1537,0,0,168,473,0,0,0,105,0,0,0,0, +627,438,0,0,0,0,0,0,0,0,0,0,11,1256,0,0,0,1626,0,779,0,0,0,0,25,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,308,0,0,0,0,0,741,0,671,0,0,0,0,649,150,0,0,99,521,0,0,3,339,0,0,0, +543,0,0,0,0,0,0,0,0,0,1358,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,234,155, +0,0,0,0,0,0,0,1628,0,766,0,0,0,0,0,0,0,0,0,0,0,0,0,829,0,0,0,1445,0,0,0,486,0,0, +0,0,2,1635,0,0,0,0,558,0,0,0,0,0,0,0,0,0,0,1461,0,0,0,0,0,599,0,0,0,0,0,0,0,0,0, +1376,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,93,0,0,0,0,0,0,447,0,0,66,1432,0,0,0,0, +0,0,307,0,413,609,0,0,0,930,0,0,0,0,21,939,0,0,0,0,0,962,4,651,0,0,0,0,15,579,0, +0,0,0,0,597,0,0,0,0,0,981,0,0,0,545,0,0,0,0,0,0,0,1558,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,800,17,0,0,17,0,907,0,0,0,110,0,0,0,53,458,0,1983,0,0,0,0,0,0,0,0,0,0,443,0, +0,0,0,0,0,0,0,0,0,0,924,1844,0,1232,0,0,0,0,70,519,0,993,0,0,0,0,0,0,14,530,0, +907,0,0,0,0,0,733,0,0,0,0,0,0,0,0,55,0,188,531,56,0,0,1693,0,0,0,0,0,0,0,0,441, +0,192,928,0,0,0,0,0,241,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1525,0,259,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,512,185,0,464,1603,0,0,0,0,0,0,0,0,0,0,0,1113, +284,720,0,0,722,0,0,0,0,0,13,0,0,0,0,0,0,0,4,289,43,0,0,0,0,0,0,1694,0,0,0,0, +193,0,0,0,0,409,0,0,0,0,0,0,0,0,0,0,0,0,308,0,0,1863,0,0,0,0,0,0,0,0,0,790,0,0, +745,1002,0,0,0,0,0,0,0,0,0,289,68,477,13,0,0,0,0,0,0,0,0,0,0,609,0,0,0,0,0,0,0, +0,0,0,0,367,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,528,0,0,0,0,0,0,0,0,0,694,58, +548,0,0,0,0,0,0,687,0,0,0,0,1749,0,0,0,0,0,0,0,0,1004,661,0,0,0,0,0,0,445,0,0,0, +74,0,0,0,0,213,0,0,0,0,0,0,0,0,0,0,0,0,0,834,0,0,189,1672,0,0,0,0,0,0,0,1548, +192,0,0,0,0,0,0,0,0,0,0,0,0,0,32,751,0,78,0,0,0,0,0,0,544,1602,105,473,0,0,0,0, +0,0,156,1949,0,1779,0,0,0,0,0,0,0,0,0,0,0,763,0,0,0,0,0,0,0,0,29,0,0,0,0,0,0,0, +0,0,0,883,0,0,0,0,0,0,0,488,0,617,0,0,50,0,694,1518,785,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,546,0,0,0,0,0,0,0,0,0,0,22,0,0,0,0,1016,0,0,0,577,0,0,0,0,0,0, +184,935,114,720,0,0,100,0,0,0,0,0,0,0,0,0,3,0,0,0,0,0,95,14,0,969,0,0,0,0,0,0,0, +727,0,1021,0,0,0,0,0,1190,0,0,0,0,0,0,0,0,0,0,0,0,0,153,0,0,0,0,0,0,0,0,0,798,0, +587,0,0,695,42,0,1929,141,957,0,465,7,908,0,0,450,148,0,0,0,1166,0,0,0,0,0,0,0, +0,0,0,0,0,253,0,1003,0,0,0,0,0,0,0,0,0,0,0,46,0,0,879,0,806,0,1868,0,0,0,0,0, +1846,0,0,0,730,0,0,0,0,0,0,0,965,0,0,0,0,506,0,0,0,10,0,0,0,22,0,0,0,0,0,0,0,0, +0,0,0,0,0,960,296,0,0,0,0,0,0,0,0,0,0,0,587,0,0,0,0,20,0,0,0,32,982,0,0,0,0,0,0, +0,0,0,0,941,0,0,0,0,435,0,0,0,0,0,0,71,419,0,0,0,0,0,0,688,740,94,345,0,0,679, +582,0,0,0,0,0,0,0,945,0,0,0,0,0,0,0,0,0,0,0,0,539,0,684,1993,0,0,0,659,0,583,0, +803,0,704,0,0,0,0,0,198,181,347,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,481,405,203,0,0,99,826,0,0,0,0,0,0,0,492,0,408,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,665,349,137,0,0,0,0,612,1270,0,0,0,0,0,371,0,0,0,826,0,0,0,0,21,1535,858, +374,0,0,0,0,0,0,311,0,0,0,991,1968,0,0,0,0,494,1647,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,769,0,0,0,0,0,642,0,0,157,123,0,0,0,1435,0,0,0,0,0,0,0,0,0,0,79,0,0,0, +0,0,0,1425,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,106,393,486,1690,0,0,0,0, +0,0,0,0,0,0,0,0,756,184,0,0,0,1382,0,0,0,175,0,1493,0,1007,0,0,0,0,0,0,0,0,0,0, +0,219,0,0,0,0,515,99,0,851,0,0,0,0,0,1278,0,0,0,0,0,0,0,1000,982,0,762,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,910,1819,0,0,0,0,0,0,906,0,0,0,0,0,0,0,0,0,0,1730,0,0, +0,0,0,0,0,0,0,0,0,1185,0,0,0,0,0,0,0,0,40,0,0,0,147,0,0,0,0,0,0,0,0,0,0,0,0,0, +650,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,56,30,0,553,0,0,20,597,0,1614,0,0,0,0,0,327, +49,0,0,0,0,0,0,0,78,0,0,786,134,0,0,0,12,496,0,0,0,0,0,0,0,0,0,0,42,204,0,614,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,147,247,0,0,0,0,942,0,0,2023,0,0,0,0, +0,0,67,285,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1309,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,41,532,0,0,0,0,0,0,0, +1692,0,0,0,0,55,1704,0,0,0,0,988,0,0,0,223,0,0,0,0,0,0,0,57,1123,0,0,0,0,0,1764, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2015,0,0,0,1599,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,129,0,0,0,0,0,0,0,0,0,0,0,534,0,0,0,0,0,0,0,0,0,0,0, +0,0,504,621,1248,321,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1397,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,441,75,0,0,0,0,0,0,0,0,0,0,841,0,0,0,0,0,693,0,650,314,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,913,0,0,0,0,0,0,0,0,0,0,0,0,0,0,880,0,475,0, +0,1016,179,602,111,329,0,0,0,1864,0,0,0,0,846,1888,0,0,780,0,0,0,82,0,0,0,0,821, +0,0,0,0,0,0,0,0,0,0,0,956,112,0,0,0,261,455,0,0,0,0,0,0,337,385,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,184,1865,0,0,721,16,0,486,0,0,0,265,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,621,0,0,0,0,0,0,0,0,234,0,0,815,0,0,743, +1987,205,197,0,0,0,0,0,0,0,0,0,314,0,0,0,0,0,0,0,0,0,0,0,0,0,0,219,452,589,0, +176,333,0,0,0,0,0,0,0,1110,47,0,0,0,0,0,0,0,0,0,0,0,864,0,0,300,0,1237,0,0,0,0, +0,0,0,0,0,0,0,1685,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,135,395,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,631,0,0,0,0,0,0,835,0,0,0,606,459,0,979,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,612,0,0,0,0,0,0,0,0,158,372,0,854,0,0,0,0,0, +0,0,1492,0,0,0,833,0,0,0,0,0,0,0,1739,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +195,0,0,0,0,0,0,0,0,730,1997,0,0,0,0,0,0,0,0,61,0,0,0,0,0,0,0,266,751,0,0,0,0,0, +0,0,821,0,0,0,715,0,0,0,868,0,959,0,0,0,0,0,0,0,0,0,0,0,1053,0,0,0,950,0,1081,0, +1595,0,0,0,0,59,0,0,0,0,0,0,0,0,0,0,47,684,0,0,0,0,0,0,1606,0,777,0,1020,0,0,0, +1094,0,0,0,0,0,0,0,350,0,0,0,0,0,0,242,1812,0,0,0,967,0,0,0,473,286,0,0,0,0,0,0, +798,629,222,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,513,337,306,0,0,0,0,0,0,0,0,0, +146,0,0,1646,0,0,0,0,0,465,0,0,0,525,0,0,0,0,0,0,299,165,0,0,0,0,0,0,0,1064,0,0, +0,0,0,596,0,0,0,0,0,0,0,0,0,0,0,0,0,0,238,1741,0,1233,451,1824,0,0,0,0,733,495, +0,0,0,0,0,1204,0,0,0,559,341,0,224,21,0,0,0,0,0,0,0,0,97,1446,0,0,0,0,0,0,0,729, +0,0,565,727,0,1948,0,0,0,519,0,0,0,0,0,0,0,0,0,1193,0,0,0,0,0,0,790,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,323,2,201,0,0,59,0,0,34,0,896,961,0,1285,0,0,46,0,479,0,0, +0,0,549,0,663,0,0,0,0,0,783,65,682,0,0,0,0,0,11,0,0,0,0,0,522,0,0,0,52,0,0,0,0, +0,383,0,0,0,0,0,0,0,0,127,0,0,0,0,0,397,194,0,0,635,0,0,0,0,0,0,0,0,0,0,975,0,0, +0,0,0,0,0,0,0,0,116,0,51,0,0,858,0,1075,535,448,0,0,0,0,0,610,0,0,0,0,0,0,0,0,0, +0,191,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,267,673,319,94,92,0,551,0,0,218, +1406,69,256,0,0,952,1980,0,833,0,0,0,0,0,0,0,0,0,0,0,0,39,0,0,0,0,0,0,0,81,0,0, +0,352,634,0,0,0,0,0,618,0,0,0,0,0,0,73,339,0,0,0,0,0,0,0,0,0,0,0,0,0,0,169,759, +0,0,0,0,0,0,0,0,0,0,0,0,0,1075,0,0,0,0,0,0,482,649,0,0,0,0,0,0,0,0,386,336,0,0, +0,1035,0,0,0,0,0,0,0,0,0,0,0,924,0,73,0,0,0,0,0,1971,0,0,0,0,0,0,0,0,0,1344,0, +501,0,0,0,0,0,0,0,0,46,799,0,0,0,0,0,0,0,276,0,0,0,0,0,0,0,770,0,0,0,0,0,0,0,0, +0,0,0,0,0,158,0,0,0,0,0,1432,0,0,0,0,0,0,0,0,0,0,25,0,0,2001,0,0,0,0,0,0,0,0,0, +0,0,0,0,478,0,0,0,0,0,0,91,1461,211,602,0,0,0,0,0,0,0,0,0,1068,0,0,124,567,0,0, +0,1006,0,0,0,0,0,0,0,0,0,735,812,0,0,323,0,0,0,304,0,0,0,0,0,0,0,0,0,148,0,0,0, +0,0,0,0,0,0,523,0,0,144,730,0,0,981,0,0,111,0,0,132,0,0,0,0,0,0,890,0,0,0,0,0, +444,0,1787,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,2041,932,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,937,0,995,0,0,255,0,0,138,863,965,0,0,631,0,0,0,0,1394,16,652,0,0,0,0,0,0, +0,0,0,0,0,0,0,897,0,321,0,0,0,0,0,922,0,619,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,844,0,0,0,0,0,0,1659,0,1100,0,0,0,1173,0,1930,268,251,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,390,711,0,0,0,0,0,0,0,0,0,0,0,0,0,744,0,0,0,0,0,0,0,0,0,624,0,0,0, +1998,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1125,0,0,0,594,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,268,0,0,0,0,0,0,0,563,0,0,0,0,0,0,0,0,2,39,0,0,0,1332,0,0,0,0,0, +0,0,508,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,66,796,0,0,0,0,527,0,0,0,0,98,0,0,576,0, +0,0,0,0,122,0,276,37,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,645,0,0,0,0, +0,0,0,0,0,0,0,290,0,0,762,1292,0,0,0,1315,0,1955,0,0,0,0,0,0,0,0,0,0,210,131,0, +0,0,0,797,0,38,0,11,488,0,936,0,441,0,0,0,0,0,595,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +991,0,0,0,0,0,0,0,0,0,0,0,653,0,523,0,0,0,903,0,0,0,0,0,0,0,0,0,0,0,0,80,0,0,0, +0,0,0,0,0,0,432,0,0,314,0,0,0,0,232,1368,534,0,0,0,0,0,27,0,0,0,12,0,0,0,0,0,0, +0,0,0,264,736,0,1657,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1117,0,127,0,0,0,1208,0,1294, +0,0,0,0,364,0,0,0,0,0,125,1334,0,0,0,0,0,0,0,0,0,0,0,0,0,0,792,0,0,0,0,0,0,0, +849,699,0,0,0,0,0,968,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1446, +124,397,0,0,0,0,0,0,0,0,0,0,0,641,0,0,0,0,0,0,0,0,0,0,0,0,127,346,0,0,517,75,0, +0,0,0,0,0,0,0,83,0,0,0,0,0,0,1031,0,0,0,0,0,0,0,1470,0,954,0,0,345,304,410,0,0, +0,0,734,0,0,0,0,0,1822,0,0,0,1798,0,0,0,0,0,0,0,0,0,0,0,0,0,14,0,0,0,0,0,0,161, +1865,69,0,0,0,0,0,0,922,0,0,0,0,0,0,0,0,0,0,0,541,0,627,0,0,0,0,0,0,0,0,0,166,0, +0,0,0,0,0,0,0,0,849,0,0,0,0,0,0,0,717,0,0,0,0,0,0,0,0,0,0,0,0,0,0,600,0,0,0,0,0, +0,654,0,0,188,273,0,0,0,543,0,410,87,0,0,941,0,0,186,250,0,1785,0,0,0,0,0,1339, +462,961,0,780,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,529,0,0,0,0,0,0,474,1276,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,24,948,0,0,0,0,657,753,0,0,0,0,941,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,706,985,837,0,1861,0,0,0,0,0,0,0,0,0,0,0,0,0,0,292,933,0,0,0,0,0, +0,0,0,0,767,0,0,0,0,0,0,0,641,0,0,0,1233,114,0,883,0,274,2008,0,1794,285,0,0, +571,0,0,0,0,0,0,0,0,0,0,823,960,16,617,0,431,0,0,0,0,0,0,0,0,0,0,567,0,401,0,2, +781,424,33,0,2006,0,0,274,0,0,1882,0,794,0,0,0,1848,0,0,0,0,0,0,448,47,0,0,0, +1199,0,0,0,0,0,0,0,0,417,0,0,0,0,0,0,0,0,0,0,295,0,0,0,0,0,0,0,1019,0,0,0,0,0,0, +0,0,0,0,0,0,0,620,0,0,0,0,464,0,0,0,0,208,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,442,0,930,0,0,0,0,0,516,68,0,0,0,0,0,1128,104,0,0,0,0,0,0,0,0,787,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,491,0,0,0,0,0,0,711,0,0,9,0,101,441,0,0,0,0,0,0,0,0, +0,0,160,396,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,679,326,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,1128,0,0,0,0,0,737,0,1796,0,0,0,0,0,0,0,0,0,0,0,0,338,574,0,0, +0,0,0,1096,491,405,0,0,0,0,0,1081,0,0,0,0,0,0,0,0,0,0,0,0,0,1676,0,1207,0,0,0,0, +0,0,969,354,0,0,0,0,598,0,297,0,0,0,0,0,0,0,0,1772,751,0,37,0,0,1828,0,0,0,0,0, +0,0,0,0,257,191,582,0,0,0,0,0,0,790,0,0,0,0,0,47,0,0,0,0,0,0,0,449,306,1011,0,0, +0,0,0,299,0,0,0,0,0,0,837,0,0,0,0,0,0,10,329,0,0,0,0,0,1320,0,0,0,0,0,0,158,657, +0,1191,0,0,0,0,0,0,7,0,974,1939,0,1665,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,288, +66,0,0,0,0,494,175,0,1643,0,0,0,0,0,0,0,0,570,750,719,0,0,0,0,0,0,0,0,0,0,0,0,0, +13,0,0,1247,0,0,221,356,0,0,0,0,0,0,0,0,0,0,694,1809,0,0,0,0,0,0,0,411,0,44,31, +0,0,0,0,669,0,673,0,0,0,0,0,0,0,0,0,1303,704,299,0,0,0,275,0,0,216,1761,0,0,0,0, +0,0,0,0,0,0,0,1319,0,0,428,0,0,0,0,0,0,0,0,0,0,514,0,0,0,0,0,0,49,55,102,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,364,0,0,0,0,379,0,921,971,52,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,1258,0,0,0,1058,0,0,0,0,0,656,0,0,0,0,0,144,0,0,0,0,0,0,0,0,0,0, +0,1373,10,605,0,0,0,0,0,0,0,838,0,1012,0,0,0,0,0,0,0,0,0,0,0,0,0,0,154,365,0,0, +0,0,0,0,0,0,0,340,0,0,0,0,0,810,0,0,0,0,0,0,495,0,0,0,0,0,0,0,0,0,261,0,535,248, +0,358,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,567,445,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,697,0,0,0,1336,0,0,0,0,0,0,0,0,917,174,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,972,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,351,0,0,0,0,0,0,0,0,0,0, +0,0,0,286,0,0,56,438,0,0,0,0,0,1950,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,738,0,0,0,0,0, +0,0,0,0,0,969,2047,0,0,0,0,0,0,0,818,0,0,0,0,0,0,0,866,0,0,0,0,0,0,0,1467,0,0,0, +0,0,0,0,0,0,0,0,0,0,972,0,355,0,0,0,116,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,267,189,104,0,0,0,0,1613,0,0,0,0,0,0,0,116,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,886,0,86,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,45,0,0,863,0,0,0,0,0, +0,0,1953,450,1773,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,381,0,0,0,0,0,0,0, +0,0,0,0,0,1142,0,1189,0,0,0,663,0,0,0,0,0,0,0,846,0,0,528,0,393,378,0,0,0,0,0,0, +325,899,680,1880,0,1770,0,0,0,0,0,648,0,0,0,0,0,0,185,167,0,2046,0,0,0,0,0,0, +249,1645,0,152,0,0,0,1733,0,0,0,0,0,1006,0,0,0,0,0,420,0,0,0,832,0,0,0,0,0,351, +0,0,0,0,6,40,0,0,60,0,0,0,0,1354,745,724,0,0,0,0,0,0,0,0,772,1951,275,108,639,0, +0,0,0,0,0,0,0,0,500,1758,0,0,0,0,0,0,0,0,0,0,0,1886,711,205,0,0,965,865,0,0,0, +534,0,0,0,0,691,0,0,0,237,443,0,878,0,0,0,0,0,1410,0,0,0,0,0,0,0,0,0,0,0,0,0, +995,0,0,0,0,0,0,0,0,0,0,0,0,0,578,0,0,0,0,881,0,0,0,0,0,0,0,0,822,0,923,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,924,0,0,0,665,0,0,0,0,0,1901,0,0,0,0,0,950,498,93, +0,0,0,1451,0,0,0,0,0,747,828,788,400,184,0,198,0,0,0,0,0,0,0,0,0,0,0,994,0,0,0, +0,0,0,0,0,615,320,0,0,0,978,843,905,0,0,0,0,0,0,0,0,850,974,0,0,0,0,6,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,509,0,0,0,0,0,273,0,0,0,0,0,0,0,0,0,0,0,0,0, +201,0,0,0,1041,0,0,0,1040,0,0,0,0,0,0,0,0,0,693,234,774,0,336,0,1399,22,0,805, +802,777,167,789,0,0,1705,0,0,0,0,0,0,0,0,0,0,0,10,13,11,0,0,204,264,0,0,56,0,0, +1917,0,470,0,0,0,0,0,0,0,0,0,0,0,1198,0,0,0,0,0,0,0,0,0,0,1015,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,715,0,0,1002,0,0,0,298,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,867,0,0,724,0,0,0,0,0,0,0,0,0,0,0,0,768,0,0,0,0,0,1066,0,0,0,0,67,0,174,948, +0,0,0,0,0,0,0,0,0,0,0,0,0,764,0,0,0,0,75,137,0,756,0,0,0,0,0,0,1008,842,643,0,0, +0,67,0,0,0,0,0,0,0,0,0,0,0,135,821,0,0,0,0,0,0,0,0,736,0,389,355,0,0,786,0,0,0, +0,0,0,2044,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1030,0,0,0,1083,0,0,0,0,0, +1226,0,0,0,0,356,319,8,389,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,474,0,0,0,427, +0,413,0,730,0,0,0,0,0,373,0,0,0,0,0,0,0,0,0,799,0,0,0,1793,0,0,0,322,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,89,290,2,0,0,0,0,0,0,0,0,0,0,672, +699,1860,0,0,0,737,0,0,0,1612,0,0,0,0,0,0,0,0,0,0,0,145,124,884,0,0,0,0,0,387,0, +0,0,0,0,0,0,0,0,0,0,679,0,550,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1305,0,0,0,0,0,0,0, +576,0,0,0,0,0,0,0,686,0,607,0,0,37,0,0,0,0,0,0,0,0,0,101,1726,0,0,0,0,0,958,0,0, +0,903,0,0,0,0,147,0,0,0,0,0,0,0,0,0,0,0,367,0,0,0,0,690,0,705,273,0,0,887,0,0,0, +0,0,0,0,0,0,0,0,90,0,0,0,0,0,0,0,908,0,0,0,0,0,0,0,1261,0,0,497,1235,0,429,0,0, +0,0,904,0,12,125,0,0,0,841,0,0,0,0,0,860,946,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,768,0,770,160,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,271,0,0,0,0,0,0,0,719,0,699,581,0,0,0,0,0,0,0,0,0,0,862,304,0,631,0,0,0,0,880, +1513,0,0,0,0,0,981,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,434,0,0,0,0,0,550,0,0,476,930, +824,553,0,0,452,0,151,0,0,0,0,0,0,772,0,292,135,0,0,0,0,0,0,0,504,0,0,1089,0,0, +0,0,0,0,0,0,0,0,0,783,0,0,0,0,0,0,206,393,0,0,0,0,0,0,0,0,232,912,0,0,0,0,0,977, +0,0,716,98,0,0,0,0,0,733,0,0,0,0,0,0,0,0,19,0,0,0,0,668,0,360,0,0,0,0,0,0,656,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,9,726,0,0,0,0,0,0,0,0,0,0,0,0,72,0,0,1269,0,0,463,0, +0,0,0,0,0,1454,0,1287,245,0,989,0,0,0,0,0,0,0,0,0,107,164,0,0,0,0,0,0,0,1061,0, +0,0,0,2,484,0,0,0,0,0,0,0,1127,0,0,0,0,0,0,0,460,0,0,0,0,0,932,0,0,0,0,0,0,0, +588,625,0,0,0,0,76,92,0,0,0,0,0,0,0,0,0,0,0,0,0,104,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +763,0,622,0,0,0,253,0,546,0,0,110,0,256,916,0,0,35,212,0,0,746,0,0,0,150,0,0, +1466,0,0,0,1299,0,0,0,0,0,0,0,0,0,1518,0,0,0,0,0,0,0,0,0,0,0,0,0,1229,0,0,0,816, +0,0,0,0,0,0,159,0,0,0,0,0,734,869,126,1716,0,0,0,0,0,0,202,232,0,0,0,0,212,0,0, +0,0,0,111,1003,0,0,0,0,0,0,0,0,0,0,0,1712,0,0,216,0,0,0,0,516,0,0,0,0,0,650,0,0, +0,0,57,99,0,0,0,0,300,574,0,0,0,0,1023,0,0,302,0,1871,0,728,252,0,0,461,0,0,0, +323,0,0,0,0,0,0,775,461,0,0,0,0,0,0,172,0,0,464,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,73,727,0,1023,0,0,0,0,0,0,0,0,0,0,577,0,0,0,0,0,0,0,0,1037,0,0,0,0,0,0, +0,0,280,677,0,0,0,0,0,0,0,0,0,0,0,799,0,0,0,0,159,0,446,1730,0,0,0,0,0,0,0,0,0, +395,0,0,0,0,145,0,0,0,0,0,0,0,20,0,0,426,608,0,0,0,0,0,977,0,250,0,0,0,0,0,100, +0,0,0,0,1982,0,0,0,0,0,476,0,0,0,0,0,0,594,76,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,447,0,0,0,0,526,0,0,14,1124,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,188,0,0,0,0,0,0,0,0,362,301,0,0,0,1743,0,178,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,872,0,831,0,0,208,202,0,0,0,0,0,0,0,1954,0, +0,0,0,516,872,0,0,313,224,0,0,24,0,11,546,0,0,0,1937,242,241,46,0,0,0,830,1273, +0,0,0,0,0,0,0,825,327,1006,0,0,0,0,0,1580,516,366,0,0,0,0,0,1736,0,0,0,0,0,0,0, +0,0,0,0,1935,0,826,0,0,0,0,139,331,0,0,0,0,0,0,0,0,0,0,0,288,0,916,0,0,0,0,0, +1888,0,0,0,0,0,0,0,1471,0,1570,0,394,0,0,0,0,0,0,0,1931,0,1719,0,658,228,0,0,0, +0,0,374,0,0,0,0,735,0,0,0,0,0,0,323,498,0,1063,0,0,0,0,155,0,0,0,0,0,0,0,0,906, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1139,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,108,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,616, +902,0,0,0,0,0,692,0,0,0,0,0,0,823,0,0,0,305,0,0,0,0,0,0,0,681,0,0,0,0,0,214, +1004,0,0,0,0,0,0,0,23,0,0,1703,0,0,0,0,0,0,0,0,0,1443,0,0,19,714,0,0,0,0,64,737, +0,0,345,1758,0,0,579,47,0,0,539,139,0,0,0,0,388,0,0,0,0,253,0,0,0,0,0,0,252,0, +745,0,0,0,0,0,0,0,0,0,0,0,504,107,0,871,0,0,0,229,0,0,0,0,0,903,0,0,71,0,0,549, +6,47,0,0,0,0,0,0,0,0,0,980,865,705,0,0,0,161,0,0,0,0,143,1331,0,0,0,1388,33,724, +0,0,0,19,0,0,0,395,0,0,0,0,0,846,210,0,0,0,122,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,695,937,497,0,0,0,0,0,718,0,0,0,0,0,0,0,1581,0, +0,0,0,0,0,161,49,0,0,0,0,0,0,0,0,0,597,0,0,0,1094,0,0,0,811,908,0,0,0,0,0,0,0,0, +0,0,1471,0,0,0,0,0,0,0,0,0,0,42,1935,0,0,0,2014,66,2007,0,0,586,0,0,0,0,0,0,0,0, +0,28,1077,0,0,0,1221,0,0,62,0,0,0,0,0,0,0,0,0,0,1766,0,0,0,0,0,0,0,0,0,0,0,0,25, +0,499,1388,0,0,97,10,0,0,0,0,0,481,0,0,0,0,0,0,0,0,0,0,37,134,155,486,0,1442,0, +0,0,0,0,591,0,0,0,0,0,0,310,1173,0,0,0,0,409,1156,0,0,0,482,0,0,263,926,0,0,0,0, +0,0,0,0,0,0,0,0,0,804,0,0,0,0,0,0,0,0,0,0,0,0,0,1265,0,415,0,348,0,0,0,1012,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,165,1803,0,0,0,0,0,0,0,408, +0,0,0,0,0,0,257,1321,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1138,0,0,0,249,0, +0,0,576,0,0,0,0,231,0,0,0,288,0,0,0,0,0,0,0,0,0,433,1487,569,1678,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,87,0,0,0,0,0,779,538,0,0,0,413,0,0,0, +0,0,0,0,0,0,0,495,0,0,0,0,0,191,54,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,530,567, +0,0,0,0,0,1484,0,0,0,0,0,0,815,609,0,0,0,0,0,484,0,0,0,0,0,0,0,0,0,0,900,0,0,0, +0,1335,0,1724,0,0,0,0,0,0,0,0,0,0,0,640,0,0,0,0,0,0,0,0,0,0,0,1831,0,0,0,0,0,0, +0,0,0,0,0,0,0,474,0,0,0,0,0,0,0,0,0,1103,0,1504,655,1034,0,0,0,0,0,305,0,0,0,0, +0,0,0,0,0,1236,0,0,429,217,0,0,0,0,739,278,0,0,0,0,0,0,0,708,0,0,0,0,0,1840,233, +0,0,0,0,0,0,0,0,2017,0,0,0,0,0,1488,0,0,0,1590,0,0,0,0,0,1800,28,0,0,0,0,0,0,0, +0,0,45,0,36,0,22,1442,378,0,0,0,0,0,0,1507,0,0,0,0,0,0,0,0,0,0,39,0,0,1054,725, +1955,0,2036,0,0,0,0,0,0,0,0,0,0,896,1871,0,0,0,0,0,0,0,0,0,0,805,0,0,0,0,2046,0, +0,0,0,17,712,0,617,55,320,271,0,0,0,0,0,0,0,0,0,445,0,184,103,0,0,0,0,0,0,0,0, +659,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,676,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +337,0,0,0,506,0,0,0,0,0,843,77,0,458,0,0,0,0,0,1420,382,109,142,330,0,0,0,0,0,0, +0,0,0,0,0,0,87,0,0,0,492,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1239,0,0,0,0,0,0, +211,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1049,0,321,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,1985,0,0,122,0,0,234,0,0,0,1098,0,0,0,0,0,0,549,253,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,522,131,0,0,149,0,0,0,0,0,0,0,0,0,0,0,0,0,0,507,0,0,0,0,811,630,0,0,0,343, +0,0,0,0,0,448,591,455,0,1381,0,0,0,0,0,0,0,575,0,0,0,0,0,1175,0,0,0,0,0,0,0,0,0, +653,0,0,0,1761,0,1198,0,0,0,0,297,1127,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,678,0,0, +164,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,35,0,45,0,0,0,0,0,121,0,0,0,0,0,0, +0,0,125,0,0,0,1622,0,0,0,0,0,721,145,0,0,0,970,792,0,0,0,715,0,0,0,0,0,1999,0,0, +74,531,0,0,65,0,0,0,105,220,0,0,0,0,0,0,0,960,0,0,0,0,0,0,428,19,0,0,401,96,0,0, +0,0,0,1595,116,0,1021,0,0,0,0,0,750,1961,0,0,148,0,0,0,0,0,0,0,0,0,0,0,0,0,75,0, +0,1383,0,0,0,0,0,0,0,0,0,0,0,0,0,0,779,0,0,0,0,0,0,0,0,598,0,424,0,0,0,0,0,0,0, +1222,0,0,0,876,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,133,0,0,0,0,187,0,8,0,0,0,0,0, +0,0,429,0,685,0,0,0,0,0,0,0,0,0,0,0,132,472,0,0,0,0,0,0,0,0,0,938,0,0,874,0,0,0, +0,0,774,0,0,0,0,0,92,0,0,0,0,0,0,830,701,0,0,0,0,0,426,350,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,603,59,0,0,0,0,0,0,0,0,0,0,293,0,0,0,0,0,0,0,0,0,0,0,0,0,0,441,163,4,0, +0,0,0,0,0,0,0,0,806,0,0,0,0,0,0,233,0,0,0,0,1994,0,1739,0,0,393,0,47,1038,0,0,0, +309,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,363,0,0,0,175,0,0,0,0,0,0,0,666, +0,0,1675,0,1600,0,0,0,808,0,0,0,0,0,0,0,0,0,0,0,280,54,0,0,0,0,0,0,0,0,421,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,249,0,0,103,254,0,262,1,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,805,0,0,0,0,0,0,0,0,0,1630,0,0,0,0,0,0,0,0,0,0,0,0,0,671,972,989,0,0, +0,0,0,0,0,889,0,0,0,1382,0,0,0,0,0,0,0,775,0,0,0,0,0,0,0,0,0,0,388,202,0,0,0,0, +16,560,0,0,0,841,0,0,566,0,0,0,938,0,0,0,0,0,0,0,0,0,0,912,0,0,0,1361,0,0,0,0,0, +0,618,236,0,1854,0,0,318,190,0,1376,0,0,0,0,0,0,0,349,0,0,0,0,951,1972,0,0,0,0, +0,0,344,0,0,0,0,0,0,0,0,850,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,910,0,0,0,0,0,0,0,0,0,0,0,0,0,0,19,0,163,85,0,487,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,145,0,83,0,0,1013,0,0,0,1922,0,0,169,557,66,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,1193,82,0,352,454,57,0,0,1333,396,107,0,370,0,0,0,0,0,0,0,0,0,204,0,0,0, +0,0,1706,0,0,0,0,0,0,0,0,0,0,0,0,394,1204,0,0,0,0,0,1007,0,0,0,1696,0,1519,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,981,0,0,0,0,1072,0,0,0,712,0,1629,0,0,0,0,0,0,0,728,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1271,0,0,0,1608,16,0,0,0,0,485,0,0,0,0,0,0, +153,27,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1991,0,0,0,0,0,0,0,0,52,0,21,0, +0,0,0,0,0,0,0,0,819,0,0,0,0,0,917,0,0,0,0,784,0,0,0,0,135,0,0,0,0,0,454,0,0,0,0, +0,0,0,0,0,852,1719,0,0,0,0,0,852,0,0,0,0,0,952,0,0,0,0,568,0,0,0,0,0,448,0,0,0, +67,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1826,657,0,729,666,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +669,0,0,0,0,0,0,0,402,0,0,152,0,0,0,0,912,0,0,0,0,0,0,51,320,0,445,0,0,0,0,308, +0,0,0,0,0,386,0,0,239,0,0,130,83,0,143,0,348,0,0,0,0,0,0,0,958,0,0,0,0,0,210,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,430,0,0,0,0,0,0,0,0,0,0,0,0,7,213,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,801,0,0,0,0,0,0,0,0,0,936,0,108,0,0, +0,0,0,0,0,0,0,885,587,219,398,364,0,1165,0,0,342,241,303,0,0,0,0,0,0,0,0,0,0, +1454,0,0,0,0,0,0,0,0,0,0,254,562,0,786,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1294,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,493,216,0,0,0,0,219,341,0,0,0,0,0, +0,0,0,0,0,130,1734,154,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,701,604,0,0,879,0,195, +666,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1669,0,0,0,1791,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,1228,0,0,0,0,0,623,0,0,0,0,0,0,0,798,0,0,0,0,0,0,0,0,0,0,0,0,84, +122,0,0,0,837,0,0,0,0,0,0,1013,0,0,577,0,0,0,460,932,0,0,0,0,0,0,0,0,0,0,0,31, +131,0,0,0,605,0,0,0,1246,0,0,0,0,68,278,165,307,781,0,0,0,0,0,0,33,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,1113,0,0,720,1953,203,0,0,0,0,0,0,0,425,326,0,0,0,0,0, +0,0,0,0,0,241,1316,0,0,0,0,0,416,0,0,0,1300,0,847,0,0,662,358,0,0,0,0,839,1823, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,654,1522,0,0,0,0,0,0,163,0,0,0,0,0,314,978,0,0,0, +601,0,0,0,0,0,946,434,0,0,0,402,411,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,13,1467, +410,0,0,0,0,0,0,0,0,0,0,0,0,0,0,483,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,677,0,0,0,0,0,0,0,0,0,0,0,0,70,0,0,0,0,1405,0,0,0,0,0,0,108,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,777,0,0,0,0,0,747,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,68,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,505,0,326,0,0,164,628,654,0,0,0, +37,32,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,668,152,0,0,0,0,0,0,0,0,0,0,0,581, +0,0,0,0,44,126,89,0,0,0,0,0,0,0,0,1531,0,0,0,0,0,0,0,0,203,1167,0,0,0,0,0,0,0,0, +531,1232,0,0,0,0,0,943,0,670,231,880,0,1617,0,0,0,1957,0,0,0,0,0,0,0,975,0,0,0, +0,0,0,0,0,0,0,0,242,0,0,0,0,0,0,0,0,0,421,0,0,14,834,0,0,0,0,0,0,0,0,0,0,0,0, +465,0,0,0,0,0,834,688,413,855,0,0,0,590,0,0,0,0,0,0,0,0,114,0,0,0,0,0,0,0,0,0,0, +0,45,169,0,0,0,0,0,0,0,0,0,0,0,198,0,0,565,585,0,0,0,0,0,0,0,0,0,0,0,0,0,691,0, +0,0,593,0,0,0,0,0,0,0,0,0,913,116,0,0,0,0,1360,0,0,0,802,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,673,308,0,709,1006,1895,0,228,0,0,0,1840,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,608,0,0,0,0,0,0,0,0,0,1573,0,2039,136,540,0,0,0,0,0,0,0, +897,0,0,938,1878,0,0,0,0,0,0,0,0,0,1469,0,999,0,299,0,0,0,0,0,0,0,578,0,0,0,0,0, +456,0,0,0,1679,163,693,0,0,0,0,0,0,48,755,0,0,0,0,0,0,0,0,0,0,0,0,338,0,0,0,0, +1091,0,0,0,0,695,0,0,1464,0,0,0,0,0,975,0,0,335,0,0,1979,0,0,0,0,269,1566,630, +396,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1815,634,0,0,0,966,0,0,0,0,0,0,0,9, +412,0,958,0,0,579,382,0,212,0,0,0,0,965,681,0,0,16,0,0,0,0,0,0,0,0,0,0,0,0,655, +0,0,0,0,67,0,0,0,0,0,0,751,0,0,0,0,423,231,0,0,1016,300,0,0,0,0,100,237,0,0,0, +1370,0,0,0,1208,0,0,0,0,0,1219,129,0,0,0,0,0,0,0,0,0,0,0,0,0,0,199,0,0,427,0,0, +0,0,949,665,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,712,0,0,0,0,0,1186,0,0,0,0,0,0,0,0,0,0,295,312,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +151,0,0,0,0,588,4,0,0,0,0,0,414,104,0,0,757,263,0,561,0,0,0,320,0,0,0,0,0,0,0,0, +0,0,0,225,0,0,0,0,37,817,0,974,0,0,0,0,0,0,0,0,0,0,0,0,0,2026,131,235,16,0,590, +1157,0,0,0,0,0,0,0,0,221,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,140,390,0,0,0,0, +0,0,0,1144,0,0,0,464,0,0,0,0,0,0,0,0,0,0,0,0,204,407,303,1218,0,0,0,0,5,325,0,0, +0,0,12,800,0,1783,0,0,0,0,0,0,0,0,0,0,504,621,0,0,0,0,0,0,0,0,0,920,0,376,0,0,0, +0,0,218,580,0,768,454,0,0,0,0,0,0,0,0,0,0,0,0,676,0,0,0,0,0,0,164,0,0,0,0,0,0,0, +0,50,0,0,0,0,0,0,0,0,0,0,0,0,0,120,285,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,226,343, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,29,0,0,1812,0,0,8,0,0,0,21,1125,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,1327,0,0,0,0,575,1598,0,0,0,0,0,0,0,0,0,895,0,0,0,959,0,0, +0,0,0,1759,173,0,0,0,0,266,261,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,1427,0,0,300,1033,0,0,0,0,0,0,0,0,0,0,0,584,0,0,0,0,52,734, +0,0,217,239,0,1129,0,0,0,0,0,0,0,0,732,20,0,0,0,0,0,0,0,0,0,0,0,418,0,0,0,613,0, +0,0,0,0,0,0,0,0,632,0,0,85,984,0,0,0,0,909,694,7,1109,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,167,0,0,0,0,280,62,0,0,33,0,0,359,186,980,0,0,0,0,0,0,0,0,0,0,0,585,0,0,0, +211,0,0,336,145,0,1130,0,873,0,0,840,263,0,0,0,0,0,0,0,0,0,916,0,0,0,0,0,0,0,0, +0,0,155,0,0,0,461,97,0,0,0,0,0,1356,0,0,0,0,0,0,0,593,0,0,0,0,0,1392,0,0,0,0, +126,0,0,0,0,1179,0,0,0,0,0,162,0,0,0,0,0,765,0,187,0,1286,0,0,0,0,0,0,0,0,0,635, +0,0,23,215,0,0,0,1306,0,0,97,716,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,657,0, +0,0,0,0,0,0,0,299,0,0,0,0,0,0,134,0,0,0,0,0,0,0,0,0,0,0,658,1082,0,0,0,0,0,2002, +0,0,0,0,0,0,833,248,0,0,0,0,0,1654,0,0,531,0,0,0,0,0,0,634,0,0,0,0,0,0,0,0,0, +853,573,249,0,0,0,0,0,0,0,0,527,0,0,0,0,1419,0,0,0,0,0,0,20,49,0,0,0,992,0,0,0, +728,0,0,0,0,0,0,0,0,0,0,0,0,497,1579,0,0,0,0,62,268,0,0,0,0,0,0,0,1201,0,0,0,0, +0,0,0,0,0,0,0,0,495,193,0,0,0,0,106,0,0,859,0,0,23,0,0,0,0,0,0,0,813,925,0,0, +223,613,953,0,0,0,0,0,0,0,0,666,0,0,0,0,0,0,0,0,0,670,0,0,40,216,0,0,0,0,0,0, +259,0,0,0,440,1114,0,0,0,0,0,0,0,0,74,475,0,0,188,139,0,797,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,1572,0,0,0,0,39,0,0,0,0,0,0,0,0,0,0,0,0,1594,0,0,0,0,0,0,0,290,0,232, +0,0,887,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,521,14,0,0,0,0,0,741,0,0,0,992,0, +0,0,0,0,0,0,0,111,0,0,425,0,0,0,0,0,789,0,0,0,1593,0,1768,0,0,233,0,0,0,0,943,0, +0,0,0,0,0,0,955,225,245,0,0,0,0,0,0,241,0,0,0,0,1943,0,0,0,1284,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,709,0,0,0,0,0,0,554,0,0,0,0,0,0,0,0,1564,0,0,0, +443,0,0,0,0,0,0,280,0,0,0,0,0,0,0,0,729,0,0,0,348,0,0,0,0,0,0,0,758,848,298,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,829,1422,189,121,0,0,632,812,0,0,556,0,0,0,0,0,436,172, +530,844,232,984,0,0,0,0,0,0,0,0,0,0,147,0,0,0,0,0,0,0,0,537,0,0,0,0,0,859,0,0, +842,0,0,0,0,0,0,0,0,0,0,1291,0,0,0,0,0,0,0,0,0,0,0,1482,612,392,0,0,0,262,31,0, +0,0,0,0,0,0,0,0,0,753,549,0,0,0,0,0,0,696,0,0,0,0,0,0,0,834,0,0,0,0,0,771,0,0,0, +0,0,0,0,0,0,0,0,0,0,921,0,0,0,674,0,0,0,0,0,0,0,0,0,0,308,444,0,0,0,0,0,0,805, +180,0,0,278,271,0,0,214,505,0,1215,0,0,0,0,0,0,387,271,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,1645,42,92,0,459,0,0,330,1557,0,0,0,0,0,0,0,0,113,18,0,0,0, +1742,0,0,0,965,0,0,0,0,0,0,0,0,0,0,0,0,0,182,0,0,65,0,0,0,0,0,0,0,0,0,0,0,0,973, +0,0,0,0,0,328,0,0,588,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1786, +0,0,962,1985,0,0,0,308,508,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,588,0,0,0,0,0,0,614,793,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,290,0,0,0,0,0,0,0,0,0,0,1136,0,0,0,0,0,0,0,0,0,0,796,719,0,0, +326,210,0,0,0,701,758,472,0,0,0,1947,278,1079,0,0,0,0,0,0,497,41,0,0,634,46,961, +0,810,524,0,0,33,0,0,0,0,0,0,0,0,0,0,0,0,532,0,997,0,0,0,0,0,0,0,0,0,0,0,1301,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1298,0,671,0,0,0,306,0,0,0,0,0,0,0,0,0,0, +693,1823,0,0,0,759,0,0,0,0,0,1932,0,0,0,0,0,0,0,0,0,0,0,0,0,0,88,182,0,0,0,1964, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,521,0,0,0,0,0,0,424,857,0,0,0,0,671,328,0, +529,0,0,0,0,0,716,0,1509,80,67,0,0,0,0,59,141,0,0,0,0,0,0,783,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1498,0,0,0,0,343,430,803,1183,677, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1357,53,0,0,0,0,590,0,0,0,0,0,0,0,0,0,0, +0,0,0,329,0,0,0,0,0,0,0,469,0,0,0,0,0,0,0,0,0,0,460,0,0,1743,0,0,963,340,0,0,0, +0,0,1603,0,0,250,0,0,0,0,0,646,218,0,1794,0,0,0,571,0,455,0,0,0,1012,0,0,0,0,0, +0,0,0,0,0,0,0,597,161,0,349,0,524,0,0,0,0,0,0,0,0,0,0,0,0,322,432,0,0,0,0,0,0, +325,223,0,0,0,0,0,566,0,0,0,1394,481,436,0,48,457,610,756,618,0,0,0,755,0,1217, +0,0,0,0,0,197,0,0,0,0,0,0,0,0,0,0,0,0,0,0,544,492,107,414,0,0,0,0,0,0,0,0,0,0,0, +1007,0,0,0,0,5,0,0,1580,0,0,0,0,0,0,0,0,0,0,0,0,0,673,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,1843,0,0,0,0,0,0,0,0,0,165,0,0,0,0,0,0,809,885,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,498,0,0,0,306,9,0,0,0,0,0,0,0,437,721,146,0,0,0,0,0,0,0,0,0,0,0,177,0,0,0,0, +0,0,0,1377,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,200,0,959,0,0,0,1928,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1435,0,481,0,0,0,0,0,0,142,84,0,0,0,0,0, +1015,0,0,0,315,0,0,0,0,0,0,759,0,0,0,0,0,0,0,0,712,0,0,0,1722,0,0,0,0,0,0,0,0,0, +0,0,0,222,0,985,1414,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1273, +538,706,0,0,0,0,0,0,0,0,115,0,0,0,0,0,0,0,0,0,0,1781,0,0,0,0,0,431,97,665,42, +237,0,0,0,264,0,0,213,0,0,0,0,0,0,0,455,0,0,0,906,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +624,0,574,0,0,0,0,0,0,0,0,0,0,0,0,354,0,0,0,1558,0,0,0,0,0,0,0,0,0,0,0,0,0,0,68, +235,723,1813,0,0,0,957,0,830,0,0,0,0,0,0,0,0,0,0,0,0,23,0,0,496,0,0,0,0,0,0,0, +547,239,88,0,0,0,0,0,0,0,0,0,1310,0,0,0,0,0,0,0,0,80,1076,0,0,118,0,0,0,479,274, +0,0,0,0,0,0,0,0,0,0,0,497,0,0,669,261,0,0,0,0,13,0,0,0,0,0,0,791,250,642,0,0,0, +1429,939,949,0,0,0,0,0,0,0,0,0,0,0,0,0,818,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,982,330,0,0,0,0,545,0,0,0,0,0,0,947,0,1188,0,0,0,0,0,904,0,0,0,0,0,1372,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,693,377,0,0,0,0,0,0,0,0,0,0,0,0,0,0,695,0,0, +713,386,0,0,0,0,128,1575,0,0,0,0,0,0,424,893,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,904,0,0,0,0,0,552,322,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,28,1808,49,0,0,0,0, +1832,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,421,0,0,442,415,0,0,289, +0,0,0,0,0,206,110,0,0,0,0,0,205,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +19,1539,0,0,0,0,0,1340,0,1194,0,0,0,0,0,0,0,0,549,0,0,0,0,0,0,0,0,1720,0,0,0,0, +0,0,0,0,0,319,0,0,0,0,112,1180,0,0,0,0,0,0,0,0,0,0,0,967,0,0,0,0,0,0,0,0,0,1940, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,735,0,0,0,0,0,0,0,0,0,897,132,0,0,0,0,0,0,0, +0,0,0,38,838,0,0,0,379,218,8,660,1017,0,0,0,0,0,0,111,387,647,877,0,0,53,790,0, +0,0,0,0,0,0,0,458,0,0,0,0,0,0,954,0,0,0,394,0,1367,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,882,0,0,0,0,0,0,0,1409,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,38,124,342,199,0,0,0,0, +0,0,0,0,0,0,724,628,0,0,0,0,804,266,0,0,0,0,0,208,0,79,0,0,0,0,0,0,0,0,741,0,0, +0,0,0,0,0,0,0,0,606,0,1494,821,1553,0,0,135,405,0,0,178,100,0,0,0,0,0,0,0,0,0,0, +0,0,0,481,0,0,0,1378,0,0,0,0,0,0,0,0,0,0,0,0,0,791,33,1227,857,0,467,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,447,0,0,0,0,0,0,86,128,0,0,0,0,0,0,587,0,0,0,692,1018,0, +195,0,0,0,0,0,0,0,1546,0,0,0,0,0,0,0,0,0,0,0,684,0,0,345,0,0,0,0,0,0,365,0,1683, +0,0,472,0,433,0,0,0,0,0,0,0,28,0,0,0,997,0,705,3,0,0,0,0,0,0,0,0,0,229,0,0,0,0, +102,0,0,0,0,866,1022,0,0,0,0,0,0,0,0,0,55,0,115,0,0,0,0,933,0,0,0,0,0,0,0,702,0, +0,0,0,0,0,0,1728,26,484,0,0,0,185,618,417,0,803,0,0,0,0,0,0,0,0,0,0,0,1262,0,0, +0,0,0,0,0,0,0,0,0,0,0,633,0,0,0,0,0,0,0,0,0,0,0,0,0,479,262,0,0,0,0,0,0,830,0,0, +0,0,26,70,0,0,0,0,0,0,0,0,217,0,640,51,0,0,360,1586,0,0,0,0,0,652,0,0,0,0,0,766, +0,0,0,0,298,737,0,0,0,0,0,0,0,0,0,0,655,222,906,0,0,1013,991,2009,0,0,0,0,503,0, +0,0,216,154,0,0,0,716,0,844,0,0,0,0,621,252,0,0,0,0,748,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,103,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,576,0,0,0,648,0,0,0,331,0,0,0, +0,0,0,0,0,0,0,0,0,632,0,0,0,518,107,0,0,0,0,0,0,0,0,851,0,0,0,0,504,0,0,0,0,0,0, +0,0,0,0,0,0,7,883,0,0,0,0,0,0,0,922,0,0,0,0,0,0,0,0,91,993,0,0,0,0,0,0,200,131, +10,0,0,0,0,0,0,0,0,0,0,0,0,0,365,1433,0,0,0,0,28,103,0,0,798,1013,0,0,0,0,0,0,0, +0,39,1925,0,853,0,0,271,519,0,0,0,0,338,0,0,300,470,419,0,0,0,0,0,0,836,0,0,0,0, +0,0,1937,0,0,0,0,0,393,0,0,357,0,0,0,0,0,703,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,387,0,0,0,0,0,0,75,708,453,1351,0,303,0,0,772,0,0,0,0,0,0,0,0,749,0,0, +0,0,0,0,0,0,0,0,0,0,0,1065,0,0,717,226,0,0,0,0,0,890,431,626,0,0,0,0,706,0,0,0, +51,698,0,0,0,0,0,0,0,0,0,0,0,828,0,0,17,0,0,0,0,1929,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,84,0,0,0,0,0,0,0,0,0,0,0,0,0,0,27,871,498,0,101,1793,0,0,0,0,0,0,435,0, +0,0,0,0,966,0,129,1644,0,0,0,0,0,0,0,0,0,0,0,0,0,997,502,0,0,0,0,0,0,0,0,0,0,0, +0,823,0,1927,0,0,0,0,98,1756,0,0,0,0,0,0,0,0,0,0,0,0,8,0,160,1046,0,492,0,0,0,0, +0,0,129,45,0,0,0,0,0,0,353,558,0,0,0,0,0,785,0,0,0,1145,189,0,0,0,26,353,0,0,0, +0,0,2024,0,0,0,606,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,32,855,0,0,0,0,0,0,0,0,0,0,0, +0,0,2011,0,0,5,4,0,0,461,764,0,0,0,1449,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1445,0,0, +0,1168,0,0,0,233,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,0,0,216,0,0,0,286,0,0,0, +3,0,0,0,723,536,0,0,0,0,0,285,0,0,0,560,0,0,0,0,0,690,0,0,0,0,0,1246,0,0,63,0, +33,0,0,0,0,0,520,1862,0,0,0,0,0,0,0,0,0,0,0,0,630,0,0,0,0,554,0,0,0,0,0,1001,0, +0,0,0,0,446,0,0,0,0,0,0,0,1313,0,0,837,636,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,278, +0,0,0,0,0,0,0,0,868,0,0,0,0,1010,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +1231,0,304,0,506,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,26,0,93,1408,794, +843,704,0,285,114,485,898,145,0,19,2035,0,0,0,1933,0,0,0,0,0,0,0,1728,0,0,0,0,0, +0,0,0,746,0,0,0,0,0,0,0,995,1964,0,0,0,0,0,0,0,0,0,0,0,1550,0,874,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,1018,0,0,0,814,126,0,0,1264,0,0,814,955,0,0,0,0,0,0, +0,981,0,0,0,0,0,0,0,0,915,56,0,0,100,0,0,0,0,0,0,0,0,0,638,0,0,0,0,738,0,0,0,0, +0,0,0,0,0,758,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1112,0,0,214,0,0,0,133,0,196, +168,0,0,0,0,0,1152,0,1245,0,0,538,169,871,1816,0,0,413,133,0,0,0,978,0,0,43,93, +371,0,0,0,0,0,0,526,25,0,754,335,0,0,0,0,182,0,0,0,0,0,0,0,0,0,0,0,39,601,0,0,0, +0,0,0,0,181,370,0,0,1652,358,0,0,0,0,0,0,0,0,0,176,286,0,788,0,0,0,0,0,1223,780, +254,1003,896,0,0,0,1447,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,744,0,0,0,0,0,126,0, +41,788,0,0,0,629,0,0,0,0,0,0,0,0,0,0,0,293,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,420,37,1900,0,0,0,0,542,1570,957,0,0,0,0,0,0, +0,373,31,0,0,0,0,125,325,0,0,0,0,0,0,323,0,0,1547,0,0,0,0,0,0,0,0,0,0,0,0,0, +1216,0,0,0,0,0,0,198,1905,629,15,0,0,0,0,0,0,20,75,543,1353,0,0,0,533,0,0,6,0,0, +0,0,0,0,538,0,0,0,0,0,0,0,0,0,0,0,338,0,0,0,0,11,0,0,0,284,659,0,989,0,0,0,0,0, +0,0,0,0,848,0,0,507,0,0,0,0,0,0,0,0,188,991,884,0,0,0,0,60,959,0,0,0,0,0,1653,0, +0,922,337,0,638,0,0,500,0,0,0,0,0,0,0,0,0,0,0,166,0,0,0,0,0,0,0,0,0,0,0,0,418,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,760,0,0,0,0,0,0,1277,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,770,0,0,0,0,0,0,0,243,89,0,0,0,0,0,0,0,0,0,1396,0, +560,0,0,3,1658,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,586,0,0,1271,0,0,0,505,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,637,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1947, +41,445,0,0,0,0,0,0,0,0,57,189,0,0,371,0,0,0,0,552,0,883,0,923,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,875,0,0,0,1788,49,0,0,0,0,0, +0,0,0,0,0,0,661,0,0,1945,0,0,0,0,0,794,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,1135,0,0,0,745,0,0,0,0,0,0,0,84,0,0,0,0,0,0,0,410,0,976,0,0,0,0,0,703,0,0, +0,0,0,0,187,322,0,0,0,227,0,0,0,0,560,0,31,1395,0,0,0,0,0,466,0,0,0,0,643,167,0, +0,0,1428,0,412,0,0,0,0,0,0,0,0,0,1118,562,0,0,0,0,0,256,0,0,0,0,0,0,1771,0,0,0, +0,0,1190,132,0,66,0,0,0,0,0,0,0,0,0,0,317,0,0,0,63,0,0,0,0,0,0,0,1475,0,0,0,0,0, +0,0,288,0,0,0,0,608,0,0,0,0,0,0,0,0,1225,0,1189,0,0,0,0,0,0,0,1468,0,0,0,0,0, +689,120,0,0,0,0,0,0,0,1,0,329,0,0,0,0,226,0,0,0,0,0,1855,0,0,461,0,0,0,0,1346,0, +0,0,0,0,85,0,0,299,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1171,0,0, +0,980,0,0,0,0,0,0,0,0,637,279,0,0,0,0,0,293,0,0,0,0,528,17,0,0,0,0,5,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,48,0,0,0,0,0,0,0,601,0,0,0,0,0,0,779,0, +196,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1322,737,752,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,412,192,80,0,0,8,1470,0,0,0,0,0,0,0,0,0,873,0,0,0,0,0,835,0,0,0,0,256, +38,986,0,0,0,0,0,0,0,0,0,91,257,278,911,0,0,0,0,0,0,0,0,749,151,0,0,0,0,0,0,0,0, +0,0,0,0,989,0,0,990,0,0,90,194,0,0,0,0,0,425,0,0,0,0,0,774,0,0,0,0,0,0,0,0,0,0, +646,827,752,0,0,0,662,0,22,21,0,0,0,0,0,0,95,239,0,0,0,431,0,0,0,0,0,874,0,0, +265,65,0,0,0,1350,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1887,0,0,0,0,0,0,0,809, +0,696,0,1074,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,630,0,0,802,0,0,0,56,776,0, +970,0,0,797,0,0,0,0,0,400,0,0,1951,0,0,41,0,11,118,0,0,0,0,0,0,0,0,251,615,0,0, +0,1044,0,0,0,0,0,0,0,0,0,0,0,225,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,370,0,0,0,0, +104,48,209,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,930,0,0,0,0, +0,0,0,0,0,0,0,1286,0,759,0,120,385,0,0,0,429,0,0,0,0,0,0,0,0,820,0,0,0,0,0,0, +199,0,10,151,0,0,0,761,365,0,0,0,0,0,0,0,0,0,46,1086,0,0,0,0,11,1624,58,344,0,0, +1008,1868,0,0,0,888,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,711,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,440,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,914,1913,0,958,0,885,0,0,0,0,0,0,0,0,0,0,0, +0,0,847,276,0,302,65,0,0,0,510,0,1514,0,0,0,0,0,0,152,291,0,0,0,0,0,0,0,0,0,0,0, +0,282,589,0,0,0,0,0,0,0,0,0,0,0,0,0,130,0,0,463,42,0,0,0,0,0,372,0,0,0,0,0,0,0, +0,0,680,0,0,0,0,0,0,0,0,977,1997,0,0,0,810,0,0,0,0,0,0,0,0,0,1390,0,0,0,644,0,0, +867,982,0,0,0,0,0,0,0,540,0,123,0,0,0,1978,0,0,0,0,789,623,0,1723,0,1220,0,0,0, +0,0,0,0,480,0,0,0,0,0,0,0,0,0,0,0,888,0,0,0,0,0,0,0,0,0,0,0,0,299,1995,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,788,179,0,0,0,0,0,0,431,156,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1373,39,80,196,0,0,507,0,0,0,646,0,0,0,0, +0,1214,0,0,0,0,926,0,0,0,1,114,0,0,0,0,0,446,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,490,0,0,0,491,0,1584,0,0,507,250,0,0,0,158, +10,362,1,0,0,0,0,0,0,0,0,0,408,228,860,480,0,779,0,0,0,557,0,0,142,197,0,0,0,0, +0,0,0,0,0,0,0,1490,11,378,316,1057,0,0,18,579,299,1546,0,177,0,0,0,0,0,0,0,0,0, +411,0,0,0,0,727,439,0,0,0,0,0,1528,0,0,0,0,0,0,58,0,482,0,0,0,505,1952,0,0,0,0, +0,0,0,0,0,0,0,242,0,0,0,0,0,0,0,953,0,0,0,0,802,0,0,0,0,0,0,0,0,0,0,290,0,0,791, +52,0,0,0,0,0,0,0,0,0,0,0,112,0,0,0,0,0,1028,0,0,138,0,0,0,0,1811,0,0,0,0,0,0, +934,1821,0,0,0,0,371,38,0,0,0,1296,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,723,0,0,0,0,0, +0,0,0,0,0,0,0,0,1330,0,0,0,0,0,0,0,1255,296,109,0,0,0,0,0,660,0,0,0,0,270,591,0, +0,0,0,0,0,0,1090,81,0,0,0,0,391,0,0,0,0,249,322,0,0,0,0,0,0,0,1412,0,0,0,0,0,0, +0,0,0,0,526,632,0,0,0,0,0,0,235,144,0,0,0,0,0,940,0,0,0,52,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,309,196,0,0,0,0,0,1912,0,1290,0,686,0,0,625,0,0,0,0,0,0,0,0,0,0,0,412,0, +0,0,0,43,0,0,0,0,11,967,758,0,0,0,0,0,0,0,0,0,0,0,0,0,0,220,0,0,0,0,0,0,0,0,0,0, +873,11,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,890,0,0,2,0,0,0,0,0,0,0,0,1774, +393,263,0,0,0,0,0,0,818,456,0,0,251,178,393,97,0,0,0,0,0,674,168,0,0,0,0,0,0,0, +159,1639,0,0,0,0,0,0,0,0,59,934,0,191,0,0,0,0,346,165,0,877,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,128,0,0,0,0,0,0,1297,0,0,0,0,0,0,164,0,0,0,15,132,241,1073,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,228,324,53,0,0,910,0,0,0,0,0,0,0,0,734,705, +217,73,0,0,0,0,0,0,0,0,636,389,0,1409,0,0,0,0,0,893,0,0,0,0,21,0,0,0,0,0,0,0,0, +0,0,0,0,0,721,0,0,0,959,0,0,0,0,1433,0,0,0,0,0,0,0,0,0,0,0,0,174,189,0,0,0,0,0, +0,0,0,0,0,22,2,0,0,815,354,0,0,0,0,425,0,411,60,13,1611,0,0,0,0,0,0,0,0,0,0,0,0, +0,1478,596,0,0,398,0,50,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,192,1159,0,0,0,0,0, +592,223,0,0,0,0,0,0,0,245,64,0,0,0,0,278,0,604,0,0,1502,265,0,0,0,0,0,0,0,310, +1763,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,129,0,0,0,0,0,0,0,0,0,1356,0,0,0,0,0,0,0, +0,505,0,0,0,0,0,0,0,1000,0,0,966,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,839,0,0,0,0,0,0, +0,0,0,0,0,0,0,637,0,0,0,0,0,0,0,0,0,0,0,0,0,0,590,0,0,0,0,280,0,0,0,1386,0,0,0, +281,0,1064,0,0,0,0,0,917,0,0,15,555,0,0,1014,1883,0,0,0,965,0,0,117,33,0,0,0, +801,0,0,0,0,0,877,0,824,0,0,0,0,0,0,0,0,0,0,0,365,0,0,0,0,0,0,774,7,0,430,0,0, +231,360,0,0,0,0,0,0,0,0,822,740,0,0,929,1485,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,852,0,0,0,0,17,0,0,0,0,0,0,1001,0,0,0,0,35,831,0,0,384,457,0,0,0,1351,0,27, +0,0,984,0,264,552,0,401,0,0,0,710,0,1211,0,0,11,205,0,0,0,0,0,0,0,0,0,0,0,0,5, +579,0,717,0,0,1011,0,0,0,0,0,0,0,0,0,0,0,0,0,0,805,0,0,0,0,0,0,0,0,0,0,0,489,0, +0,0,1024,0,0,0,0,0,0,0,0,0,892,0,0,0,0,0,0,0,0,0,0,0,0,473,0,0,0,659,864,0,0,0, +0,0,0,152,819,0,51,0,0,0,0,0,0,0,0,0,0,130,0,0,0,0,0,229,0,0,0,0,674,0,0,0,0,0, +0,0,0,0,770,52,79,0,0,0,1666,0,409,0,0,0,0,0,0,0,195,0,688,0,0,0,0,0,0,0,0,0,0, +0,889,174,160,0,0,0,0,0,0,0,0,0,0,0,0,0,872,0,918,569,268,0,0,0,1224,0,1361,0,0, +0,0,0,0,0,0,0,374,0,0,0,0,0,731,0,0,0,0,190,0,0,0,0,0,0,0,202,506,444,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,835,0,17,1526,0,0,0,0,0,477,0,0, +994,1374,76,0,0,0,0,0,0,0,355,287,0,1389,0,0,0,0,0,0,455,384,0,0,0,264,0,0,0,0, +0,0,0,0,0,0,0,0,1001,0,0,0,0,0,0,0,0,0,0,0,0,28,0,0,0,851,175,359,0,0,0,0,0,0,0, +0,287,740,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,857,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +819,1402,0,0,0,0,0,0,174,224,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1649, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,655,573,0,0,0,0,0,0,0,0,128,351,0,0,0,0,0,0, +0,0,0,0,0,918,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,687,0,0,0,0,0,0,0,0,0,1525, +0,0,0,1009,0,0,0,0,0,0,0,340,0,0,0,0,0,0,0,0,0,0,861,0,176,0,0,0,0,0,0,0,0,0,96, +985,0,615,0,0,0,0,0,0,0,1919,0,0,0,0,0,1131,0,0,0,0,0,0,0,247,0,0,0,0,27,23,0,0, +0,0,0,0,0,0,38,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1015,0,0,0,0,0,1088,0,0, +0,0,0,1585,0,0,0,0,227,0,0,0,478,360,0,0,0,95,0,0,0,0,0,0,699,0,0,0,26,0,0,0,0, +1119,0,0,0,739,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,741,67,0,0,0,0,0,0,464,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,42,0,96,0,0,0,26,342,0,0,0,0,0,0,203,0,0,449,0, +0,0,0,0,0,0,0,0,0,256,311,0,0,0,0,0,0,758,0,0,0,0,0,0,0,0,827,0,0,0,0,581,64,0, +1047,0,0,0,0,0,288,0,0,0,0,0,1375,0,0,0,0,0,0,0,0,0,0,0,1309,0,0,0,0,0,0,0,0, +376,12,0,0,0,0,0,154,0,1520,0,1753,95,502,0,0,0,0,0,0,0,269,291,1197,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,15,0,0,0,0,1341,0,1017,0,0,0,0,0,0,0, +0,857,1810,533,0,0,1453,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,836,211,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,19,0,156,0,0,0,0,1009,0,0,0,0,0,0,0,0,0,0,0,0,0,820,0,0, +0,0,0,0,0,0,0,228,0,0,0,1131,0,1276,0,0,0,0,0,0,0,0,0,0,0,0,849,1792,0,0,389, +291,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,525,0,0, +0,453,0,0,0,0,666,0,0,0,422,0,355,0,0,0,0,165,0,260,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,865,0,0,0,0,0,0,0,1625,0,0,0,234,0,1383,0,0,0,0,0,0,0,0,306,0,0,0,802,1921, +0,0,0,0,0,0,180,0,0,0,0,1312,814,0,0,0,0,0,0,0,0,0,0,707,0,0,0,1493,11,61,733,0, +0,0,341,0,0,0,98,0,0,0,0,0,0,0,0,0,0,0,1014,0,0,0,0,0,0,0,142,102,0,0,30,0,0, +823,0,1045,0,0,0,1930,0,1512,0,0,0,0,0,0,0,87,0,1243,245,0,0,0,0,0,0,0,48,68,0, +0,0,0,0,0,0,0,126,77,625,938,0,0,351,0,0,0,174,1668,0,707,0,0,0,0,0,0,0,0,0,0,0, +403,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,282,0,0,0,0,0,0,8,44,0,0,363,115,0,0,0,0,0,0, +0,0,0,0,0,0,545,761,0,0,835,1254,0,0,0,0,930,1936,0,0,0,0,0,0,0,0,653,0,0,0,0,0, +344,0,0,1483,673,185,0,0,460,93,753,478,0,0,0,0,0,1020,0,0,0,0,0,0,0,103,0,0,0, +499,0,0,0,0,0,0,207,0,0,0,0,0,0,0,0,0,0,0,0,0,0,96,0,968,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,3,0,0,0,0,399,0,0,0,0,224,563,0,0,0,0,0,704,0,0,0,0,0,0,0,0,0,0,0, +1559,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,861,0,0,0,0,946,333,746,0,0,0,0,0, +0,0,910,0,0,0,0,0,0,0,0,0,0,0,0,0,652,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +1393,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1514,0,0,0,0,201,0,510,717,0,0,528,0,0,0,0, +20,0,0,0,1251,0,0,0,1163,0,0,0,307,0,0,0,0,0,1091,0,0,0,0,0,0,0,0,0,0,0,429,0,0, +0,881,0,0,0,0,0,621,0,0,0,0,0,0,0,736,0,348,0,868,0,0,0,0,433,0,0,0,771,1495,0, +0,0,0,215,0,0,0,0,0,124,0,0,0,0,0,0,0,0,0,0,0,55,0,0,0,0,0,0,0,112,62,0,856,270, +0,572,0,0,0,0,939,0,0,0,0,0,0,0,352,0,0,0,0,0,0,0,0,0,647,0,0,0,0,10,0,0,0,0,0, +0,0,220,0,0,0,0,0,0,0,0,0,0,0,0,0,464,0,0,109,0,0,0,1746,0,0,0,515,0,0,0,566,0, +0,0,0,0,0,67,40,0,0,722,992,0,0,923,0,0,0,0,0,0,1145,0,0,0,0,0,0,0,0,0,0,0,568, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,247,0,0,0,0,645,0,0,328,0,0,0,0,0,0,0,0,0,0,0,0, +1363,0,0,0,0,0,1280,0,0,0,0,0,0,0,0,0,0,7,28,360,162,0,0,0,0,0,0,0,0,0,0,0,764, +0,0,833,862,0,856,0,0,0,0,0,0,736,92,0,0,948,1944,0,1479,63,590,0,0,0,1521,0,0, +0,709,0,0,61,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,483,0,0,0,0,1213, +0,0,0,0,29,1022,0,1712,0,466,0,0,0,0,0,0,0,0,0,0,0,0,0,731,0,0,0,0,0,0,171,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,241,0,0,0,0,0,0,0,0,0,0,0,964,2005,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,1100,0,0,0,954,0,0,0,0,0,0,0,0,0,1958,0,0,34,549,994,0,0,449, +137,850,0,0,670,146,0,0,0,0,518,159,0,0,0,0,0,0,0,0,151,0,0,1027,0,0,0,0,0,0,0, +0,0,0,983,0,0,0,0,993,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,141,501,0,0,0, +0,0,0,0,0,0,452,0,0,0,0,0,0,0,0,0,0,233,149,0,0,0,0,0,0,0,0,582,0,0,0,801,0,0,0, +0,0,0,70,0,0,369,0,36,0,0,0,0,0,0,0,204,721,430,241,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1817,16,1078,1021,0,0, +406,0,0,0,0,0,69,0,0,0,0,0,1830,0,0,0,824,0,0,0,0,0,0,0,0,0,826,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,816,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1000,717,1845,0,423,0,0, +0,0,0,0,0,0,510,0,0,1048,0,0,0,618,0,0,0,520,0,0,0,0,990,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,321,0,0,0,0,0,0,0,1135,0,0,921,0,0,0,24,397,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,856,0,0,0,139,282,981,0,288,0,0,0,1890,651,56,0,0,0,0,0,0,0, +0,261,0,0,0,0,0,0,0,0,0,0,0,617,1403,0,1205,0,0,563,0,0,0,0,0,0,0,0,333,0,0,0,0, +0,369,0,0,0,0,0,0,0,0,0,622,0,0,0,1407,0,0,0,0,0,0,0,0,0,0,0,0,624,160,0,363,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,619,0,174,292,0,0,656,616,0,0,0,685,0,0,0,0,0,0,0,0,0,0,0,0,0,647,0,0,0,631,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1267,0,0,0,1797,0,0,0,1684,0,0,469,0,531, +1230,73,0,0,0,0,0,0,0,0,0,268,0,0,0,0,0,102,558,109,65,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,595,0,0,0,0,0,374,1832,0,0,0,0,0,0,16,0,405,6,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,881,0,1495,0,0,0,0,0,0,0,0,0,142,0,0,0,0,0,0,0,0,0,0,21,466,23, +257,0,0,0,0,0,0,77,404,0,0,0,0,0,0,712,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,860, +1848,0,0,652,629,0,0,0,0,13,377,0,1842,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1501,0, +0,0,1906,0,0,0,0,0,0,0,0,0,0,0,0,0,491,234,171,0,0,0,0,631,1186,0,0,0,0,0,0,0,0, +0,0,0,0,931,0,170,0,0,0,0,0,0,0,0,0,0,1587,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +765,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,424,0,0,714,0,0,0,0,685,0,0,0,0,0, +0,285,0,0,0,0,0,0,429,0,0,0,0,0,0,0,0,0,0,71,18,0,0,0,0,0,0,0,0,0,0,116,828,0,0, +0,0,0,0,289,0,0,0,0,0,0,0,0,675,0,0,0,1424,0,0,0,0,0,647,0,0,0,1334,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,36,209,0,0,0,0,0,0,0,342,0,0,0,928,0,0,0,0,0,1838,118,856,654, +318,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,915,895,454,0,0,513,1425,0,0, +0,0,0,0,791,0,153,0,0,0,0,0,0,796,909,445,345,0,0,0,0,0,0,0,0,578,0,0,0,1387,0, +0,0,555,0,0,0,0,0,0,766,0,0,0,0,0,0,0,0,0,0,541,0,0,0,0,0,0,0,0,0,0,0,0,0,880,0, +0,0,0,0,1506,0,0,983,0,768,0,0,0,0,584,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,737, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,226,30,426,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +117,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,462,0,0,0,385,0,398,0,0,0,0,0,0, +0,0,0,347,0,0,0,0,125,1259,644,136,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,469,0,0,0,0,0, +1367,0,0,0,0,0,0,0,0,0,0,0,719,0,0,0,0,0,0,0,0,0,0,0,0,0,1423,0,0,0,0,0,0,0,0,0, +749,0,0,0,0,546,645,0,0,0,0,0,0,277,0,0,1275,0,0,0,0,0,0,0,453,536,555,0,0,987, +1107,0,0,90,0,0,0,0,0,0,0,0,860,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +257,0,1768,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1071,0,0,0,0,0,0,0,0,0,0,0,0,0,83, +0,835,0,0,0,0,0,0,0,2006,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,696,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,95,1718,0,0,0,0,0,0,0,26,0,550,0,0,0,0,0,901,0,0,0,0,0, +0,822,0,0,122,0,0,0,807,0,0,0,0,0,262,0,620,601,34,0,0,170,0,0,0,0,537,0,0,0,0, +0,0,0,0,0,332,0,0,208,1909,182,261,0,0,0,1721,0,0,0,0,0,933,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,1609,0,895,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,812,0,0,942,1916,0,0,0,0, +0,0,0,778,0,0,0,137,0,1314,0,0,0,0,0,0,0,1661,0,0,0,0,0,0,0,1591,0,0,0,0,0,0, +820,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,185,89,0,1160,230,6,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,63,29,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1740,0,0,177, +170,0,1961,0,0,0,0,0,0,0,0,0,0,0,0,91,0,17,44,0,0,0,0,0,0,0,0,0,270,0,296,0,0,0, +0,0,0,0,1523,0,0,0,0,0,0,0,0,0,0,757,7,0,0,0,0,0,0,0,0,0,0,530,588,0,0,0,0,0,0, +0,0,0,786,0,0,0,0,0,580,627,88,447,57,0,0,0,0,0,0,0,0,845,735,0,0,0,0,0,31,15,0, +460,521,12,424,0,0,0,1302,0,0,0,0,0,0,0,595,0,0,0,13,548,97,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,1472,452,1767,0,0,0,0,0,0,0,0,0,0,115,0,0,0,0,0,0,1543,0,1111,0,0,0,0, +1,0,359,488,0,267,0,0,0,1983,0,0,0,0,0,0,0,1155,0,1575,0,1438,31,0,0,377,101,0, +0,0,0,0,0,0,0,0,0,0,0,0,476,0,0,0,0,0,0,0,0,2023,0,0,0,0,0,1836,0,0,0,0,35,843, +0,0,0,0,0,0,0,554,0,0,0,536,625,207,0,1371,0,0,0,424,785,336,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,896,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,27,750,0,0,0,0,238,0,0, +0,0,0,383,0,0,0,0,0,0,0,0,603,725,11,0,0,0,0,0,0,0,0,0,476,0,0,0,0,0,1552,0,0,0, +0,0,0,0,680,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,435,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +1431,0,0,13,112,0,0,356,0,0,0,0,0,0,0,0,0,0,1963,0,0,0,1244,18,0,0,0,0,0,0,867, +0,0,0,0,0,0,50,708,73,592,0,502,0,0,0,0,0,0,161,347,0,0,0,0,470,33,0,246,571,10, +0,465,614,0,237,0,0,0,0,0,24,18,0,506,0,0,0,0,0,0,33,309,0,0,0,0,0,0,0,0,0,0, +140,0,0,0,0,1056,0,0,0,1704,0,0,0,0,0,0,0,1036,0,0,0,0,0,0,0,0,0,1315,432,86, +264,524,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,107,0,0,0,0,0,123,927,0,0,957,1149,0,0, +0,0,0,778,0,502,196,0,0,0,0,1312,0,0,0,0,0,0,0,855,0,0,0,0,0,0,0,0,0,0,45,1400, +0,0,0,1003,0,0,0,0,0,1097,0,0,0,0,0,0,0,0,545,612,0,0,0,0,0,0,0,0,0,0,0,0,54,0, +0,0,0,172,0,0,0,1029,0,0,0,0,0,0,0,0,0,568,0,0,0,732,617,0,0,974,94,989,733,0,0, +0,0,0,0,1789,0,0,665,2015,0,0,0,0,0,0,806,287,0,0,0,0,0,1539,0,0,0,0,0,0,0,0,0, +0,182,1563,0,0,0,0,0,0,0,0,0,484,0,0,0,0,0,1623,0,0,0,0,0,0,0,0,878,1833,0,1569, +0,0,0,0,0,0,0,0,93,0,715,994,0,0,0,0,0,63,0,591,0,0,0,0,0,0,0,749,0,0,0,0,547, +366,0,0,0,1747,0,0,0,0,0,0,0,89,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1463,0,772, +893,0,0,0,48,0,0,941,0,0,690,1785,106,440,0,0,0,0,0,0,0,0,0,0,32,0,332,216,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,852,0, +0,416,564,0,918,0,1764,0,0,3,0,0,274,0,0,0,0,501,0,0,0,0,0,0,0,851,743,0,49,0, +879,0,0,47,0,0,0,0,0,0,865,0,1202,0,0,0,0,0,0,47,272,0,0,0,0,0,0,0,0,0,0,0,1455, +0,0,0,0,891,1911,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,761,0,0,0,0,0,0,0,0,0,407,0, +183,0,0,490,0,0,0,0,0,0,0,35,731,0,0,0,0,0,0,0,819,0,0,0,0,0,0,0,0,0,0,0,0,0, +575,0,0,0,0,45,818,0,0,77,222,0,0,0,0,849,1880,0,0,0,633,0,1308,0,0,0,0,0,0,0,0, +0,0,86,0,0,0,0,0,0,0,0,0,0,0,0,0,0,817,0,0,0,0,0,0,0,0,0,882,0,0,0,914,0,0,0,0, +0,0,0,0,0,0,865,0,0,426,399,58,0,0,0,0,0,0,538,102,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,876,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,139,566,0,63,12,0,0,0, +0,0,0,0,0,0,0,0,0,0,3,114,0,0,0,0,0,0,0,0,576,0,0,0,0,0,0,0,0,933,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,692,0,0,0,0,0,0,0,0,0,0,0,0,752,0,0,0,0, +0,0,0,0,375,0,1011,0,0,96,0,0,0,0,0,0,0,0,0,148,0,0,0,0,0,0,0,0,0,0,0,337,56, +666,0,246,394,0,0,0,0,0,0,0,0,437,0,0,0,506,0,0,0,0,1003,0,1163,0,328,0,0,0,0,0, +0,0,0,1000,0,0,0,0,0,744,101,0,0,0,0,0,726,0,0,176,0,146,9,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,839,0,0,0,0,0,0,223,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,246,1931,29,0,0,1771,0,0,0,0,0,846,6,157,0,0,0,0,0,0,0,0,0,875,0,0,477, +773,177,639,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1747,0,0,0,0,158,873,0,659,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,391,0,0,0,0,0,0,0,0,0,0,0,0,668,883,0,78,628,0,0,0, +0,0,0,0,0,0,0,0,0,1460,0,962,0,0,0,0,0,460,0,0,0,0,0,0,0,0,0,0,0,0,0,0,34,199,0, +0,0,388,474,0,271,0,333,608,0,0,0,0,0,0,49,0,988,0,707,617,0,0,0,0,0,0,0,756,0, +0,0,0,0,1583,0,0,0,0,0,0,0,0,0,0,285,0,0,0,0,0,0,0,0,0,0,0,0,0,0,344,0,0,0,0,0, +0,0,0,515,1709,0,0,0,0,0,0,0,0,404,0,0,0,0,500,0,0,0,0,0,0,0,0,0,68,216,0,0,0,0, +0,0,0,488,353,0,0,177,236,0,0,458,490,0,0,0,0,0,0,756,1504,0,757,0,1735,0,0,108, +598,0,0,0,0}; +const BROTLI_MODEL("small") +uint8_t kStaticDictionaryHashLengths[BROTLI_ENC_NUM_HASH_BUCKETS] = { +8,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,6,0,0,0,0,0,6,0,0,0,0,0,0,12,0,0,0,0,4,22,5,0, +4,0,0,0,0,0,0,0,0,0,0,0,0,14,6,0,0,0,5,0,0,0,0,0,0,0,7,13,0,0,4,0,0,0,0,0,0,0,0, +0,6,0,0,0,0,8,0,0,0,0,0,0,7,0,7,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,15,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,12,0,0,4,0,0,0,4, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,10,4,0,5,13,7,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9,6,0,0,8,7,0,0,9,0,8,0,0,0,0,0,0,6,0, +0,9,0,0,0,11,0,0,6,8,7,0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,7,0,0,0,6,8,0,0,0,0,0, +0,0,11,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,0,9,0,0,0,8,4,13,7,0,0,0,0,0, +7,0,5,0,0,0,0,8,5,0,5,0,0,8,7,0,0,0,0,0,0,0,0,0,0,9,0,0,0,8,0,0,0,10,4,0,5,0,4, +0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,6,0,6,0,0,0,0,8,7,0,4,9,4,0,0,0,0,0,0, +9,0,0,0,8,5,0,0,0,6,0,0,0,0,0,0,0,0,0,7,18,0,0,0,0,4,9,0,0,4,0,6,0,0,0,6,0,6,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,6,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,5,8,7,0,0,0, +0,9,0,0,0,0,0,0,0,8,6,10,6,0,0,0,4,0,6,8,6,0,0,0,4,0,0,0,0,0,5,0,0,0,6,0,0,0,0, +10,0,12,7,0,0,0,0,0,4,0,0,0,0,0,5,0,0,8,7,0,0,0,0,0,0,0,0,9,5,0,0,0,0,0,0,0,0,0, +0,0,0,0,6,11,0,0,0,0,0,0,0,0,0,8,7,0,0,10,0,0,0,0,0,0,0,0,6,10,0,17,0,8,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,0,8,6,9,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +7,0,0,11,4,0,5,0,0,0,0,0,0,0,0,0,0,10,5,0,6,8,5,0,0,0,0,0,0,0,0,0,0,11,5,0,0,0, +0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,9,0,0,0,0,5,0,0,0,0,0,0,0,0,0,6,0,0,8,7,0,0,0,0,0, +0,0,0,0,0,0,5,0,0,0,6,0,0,10,0,0,0,20,0,0,0,0,0,0,0,0,6,9,5,0,0,0,0,10,4,8,0,0, +4,13,0,0,0,0,0,0,0,9,0,9,0,0,0,0,0,0,0,0,0,0,0,0,4,8,6,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,12,0,0,4,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,12,5,0,0,10,4,10,7,13, +0,0,0,0,0,0,0,0,6,0,6,0,6,0,0,0,0,0,0,19,0,0,4,12,6,9,0,0,0,0,4,0,4,11,0,0,0,0, +0,0,0,12,0,0,0,8,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,11,4,0,0,0,0,0,0,0,0,0,6,0,0,0,0, +0,5,0,0,0,0,0,6,0,0,0,6,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,8,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,17,0,0,0,9,6,0,0,0,0,0,4,0,4,0,0,0,0,0,0,0,0,0,4,0,0,0, +6,0,0,0,0,0,0,0,0,0,0,13,6,0,0,0,0,0,0,0,0,0,0,0,6,8,0,0,0,0,0,0,0,0,0,0,6,0,0, +0,0,0,5,0,0,0,0,14,4,0,0,0,4,12,5,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,8,6,0, +0,0,0,0,0,12,0,9,6,0,0,0,0,13,0,0,5,0,0,0,0,0,4,0,6,0,7,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,13,0,9,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,12,0,0,5,0,0,0,6,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,6,0,6,0,0,0,0,0,0,0,0,8,7,8,4,0,0,0,0,0,0,0,0,0,0,0,7,0,7,0,0,0,4,0, +0,0,0,0,0,0,0,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,8,6,8,4,0,0,0,0,0,6,0,7,0, +0,0,0,0,0,0,0,0,0,10,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,7,0,0,0,0,0,0,9,5,0,0, +0,0,0,7,0,0,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,0,9,4,0,0,0,0,0,0,0,4, +12,5,11,0,0,0,0,0,0,0,0,0,8,7,0,5,0,0,8,7,0,5,0,0,0,0,8,0,0,0,0,7,0,4,10,0,0,0, +0,0,0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +13,5,0,0,0,4,0,0,0,0,0,6,0,0,0,0,0,0,14,5,0,0,0,7,0,0,10,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,6,0,4,0,5,0,0,0,0,8,5,0,0,0,0,0,0,9,5,9,0,0,0,0,0,0,0,0,6,9,0, +0,4,0,0,0,7,0,0,0,6,0,0,10,4,0,0,0,0,0,6,0,0,10,0,0,0,8,5,0,0,0,0,0,0,0,0,10,0, +0,0,0,0,18,4,12,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,6,0,0,0,0,8,7,0,0,0, +0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,8,4,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,0,0, +0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,8,0,0,0,0,0,0,6,0,0,0,4,10,5,0,0,0,0,0,0,0,0,0,0, +0,4,8,7,0,0,8,6,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10,6,0, +0,0,0,8,6,0,0,0,0,0,0,12,0,0,0,0,0,0,0,0,0,0,0,10,0,0,0,0,0,0,6,0,7,0,0,0,0,0,0, +0,0,0,0,0,6,0,0,0,7,0,0,0,0,0,0,8,7,0,0,0,0,8,0,12,6,0,6,0,0,0,0,9,7,11,7,0,0,0, +0,0,0,0,0,0,0,0,0,11,0,0,0,9,0,0,0,0,0,0,0,0,0,0,0,8,7,0,0,10,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,6,0,0,0,0,0,0,0,5,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0, +0,0,0,6,0,0,0,7,0,4,14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,14,0,0,0,0,0,8,4,0,4,0,0,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,20,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,12,5,0,7,0,5,0,0,10,0,0,7,0,0,0,0,0,0,0,0,0,0,14,0,0,0,0,6,0,4,9,7,0,0,0, +0,0,7,0,0,0,0,0,0,10,0,9,0,9,0,0,0,0,0,0,0,0,4,9,0,0,0,0,6,0,0,0,0,0,0,0,0,11,4, +0,6,0,0,0,0,0,0,8,0,8,0,0,0,0,0,0,0,0,0,0,4,0,0,0,5,0,0,0,0,0,0,0,0,13,6,0,0,11, +0,0,0,0,0,0,0,9,7,0,0,0,0,0,0,0,0,0,0,0,6,18,0,0,4,0,0,0,0,0,0,0,6,0,0,0,0,0,0, +0,5,0,0,0,0,0,0,0,0,9,7,0,0,0,0,0,0,0,6,0,0,0,0,9,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,11,7,0,0,0,0,0,6,0,0,0,7,0,0,0,0,0,0,0,0,11, +4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,4,0,0,0,0,8, +6,0,0,0,0,0,0,9,6,0,0,0,0,0,4,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,19,0,0,0, +0,6,0,6,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,6,0,6,0,0,10,6,0,0,0,7,0,0,8,0,8,7,0, +0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10,6,20,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,9,0,0,0,0,6,0,0,0,0,0,0,0,5,0,0,18,0,0,0,0,0,0,0,0,0,0,0,0,0,9,6,0, +0,0,0,8,7,0,0,0,0,0,0,0,0,12,0,12,0,0,0,11,6,0,5,0,0,12,0,12,5,0,7,11,6,0,0,11, +0,0,0,12,0,0,4,12,7,8,6,0,0,0,0,8,5,0,0,0,0,0,0,0,4,11,0,0,6,0,7,0,0,0,0,0,0,0, +5,0,6,0,0,0,0,8,0,10,0,0,0,0,0,0,0,0,0,0,0,9,7,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,11,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0, +0,0,0,0,0,0,0,0,0,11,7,0,0,0,0,0,0,10,0,0,5,0,0,12,6,0,0,0,0,0,0,10,6,0,0,0,0,8, +6,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,5,0,0,0,0,11,0,10,6,0,0,8,6,0,0,0,6,0,7,10,6,0, +0,0,7,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,10,7,0,0,0,0, +10,6,0,0,0,0,0,0,8,5,11,0,8,4,0,0,0,4,0,0,0,0,9,4,8,0,0,0,0,0,0,0,11,6,0,0,0,0, +10,7,0,0,0,0,0,6,0,0,0,0,0,6,0,0,0,7,0,0,0,0,9,6,0,5,0,7,0,0,0,0,0,7,0,0,11,0,0, +0,0,0,0,0,0,0,0,0,0,0,12,0,0,0,0,0,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6, +0,0,0,0,13,0,8,6,13,0,0,0,11,7,0,7,0,6,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,9,6,0,0,0,0,0,0,0,0,0,6,0,0,9,6,0,6,0,0,0,0,0,5,0,0,0,0,0,0,0,0, +0,0,0,0,0,5,9,0,0,0,0,0,0,0,0,0,0,4,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,0,9,7,0,7,0,0,0,0,0,0,0,0,0,0,10,0,0,0,0, +5,11,5,0,0,0,0,0,0,0,0,0,4,0,7,0,6,0,0,0,6,20,0,0,0,10,7,0,5,14,4,0,0,0,0,0,0,0, +0,0,6,0,0,0,0,8,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,0,0,0,0,0,0, +0,0,6,0,4,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,9,7,0,0,11,6,15,0,0,0,0,0, +10,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14,7,0,0,0,0,0,0,0,0,9,7,13,0,0,0,0,0, +0,7,0,0,8,6,0,0,0,0,0,0,0,0,9,4,0,0,0,0,8,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,5,0,0,0,0,0,0,0,0,0,0,0,0,8,5,0,4,0,0,0,0,0,0,0,0,0,0,12,6,8,0,12,0,0,7,0,0,0, +0,0,5,10,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7, +14,0,0,0,0,0,0,0,0,0,0,0,0,5,0,5,8,7,10,7,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,6,18,6, +14,7,0,0,0,0,0,0,0,0,11,6,0,0,0,0,8,7,0,0,0,0,0,0,0,0,0,0,12,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,6,0,0,0,0,0,0,0,6,0,0,11,7,0,0,10,7,0,0,0,6,8,6,0,0,0,0,0,0,0,6,0,0, +19,0,0,0,9,5,0,0,0,0,0,0,11,7,0,0,0,7,0,6,0,0,11,0,0,0,0,4,8,0,0,0,0,0,0,0,0,6, +0,0,0,0,0,6,0,0,8,4,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,7, +0,7,0,0,0,7,15,0,0,5,0,0,0,0,10,0,0,0,0,0,0,0,0,0,12,0,0,0,0,0,0,0,0,0,0,7,0,0, +0,0,0,0,0,0,9,6,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +11,7,0,0,0,0,0,0,0,6,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0, +0,0,5,0,4,0,0,0,4,0,4,0,0,0,0,0,0,0,0,0,6,0,0,0,0,11,6,0,0,8,5,14,0,0,4,0,0,0,7, +17,0,0,0,0,0,0,0,13,5,0,0,0,0,0,5,0,0,0,5,0,0,0,0,16,6,0,4,0,0,0,0,0,0,12,0,0,0, +0,0,0,6,0,0,0,0,0,0,0,0,0,0,12,5,0,5,0,6,10,0,12,0,0,0,0,0,0,0,0,7,0,0,0,0,8,4, +0,0,0,0,0,0,0,0,0,0,8,7,0,0,8,0,0,0,8,0,0,6,0,7,0,0,0,5,0,6,0,4,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,6,0,0,22,0,0,0,0,0,0,0,0,7,0,0,0,0,0,6,0,0,0, +0,0,0,0,0,0,0,13,0,0,0,0,0,0,0,18,0,0,0,9,4,0,0,8,0,9,7,0,0,0,0,0,0,8,6,0,0,0,0, +0,0,0,0,0,0,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,9,7,0,0,0,6,0,0,14,0,0,0,0, +0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,11,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,7,10,4, +0,6,0,0,0,0,0,0,8,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,9,6,0,0,0,0,0,0, +0,0,11,6,12,7,0,0,0,0,0,0,0,6,0,5,0,0,0,0,0,0,9,6,11,6,0,0,0,0,9,5,0,0,0,0,0,0, +0,6,8,5,0,0,0,0,8,0,10,0,0,0,0,0,9,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9, +5,10,7,0,0,0,5,8,7,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,6,0,0,0,0,0,4,8,7,0,0,0,6,0,0, +0,0,0,0,0,0,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,7,0,6,0,0,0,0,0,0,0,0,0,0,0,0,22, +0,0,0,0,0,0,0,0,0,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,5,0,0,0,0,0,0,0, +0,0,0,0,0,17,0,0,6,0,6,12,4,19,6,0,0,0,0,16,0,0,0,0,7,15,7,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,4,10,4,0,0,8,7,0,7,0,0,9, +4,0,6,0,0,0,4,0,5,0,0,0,7,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,7,10,0,0,0,0,0,11,7,0,0, +0,0,12,6,0,0,0,0,0,0,0,6,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8, +0,0,0,0,0,0,0,0,0,10,4,0,0,0,0,11,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,8,7,0,0, +0,0,0,0,0,6,0,0,0,4,0,0,11,4,0,0,12,7,0,0,0,0,9,0,0,6,0,0,0,0,0,0,0,0,0,5,0,0,0, +4,0,0,0,0,14,0,0,0,0,0,0,0,0,0,0,5,0,0,0,0,0,6,0,0,0,0,0,0,9,4,0,6,0,0,0,0,0,4, +0,0,11,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,6,0,0,0,5,0,0,0,0,0,0,0,0,0,7,9,6,0,7,0, +0,0,0,0,0,0,6,0,0,0,0,8,6,0,0,0,0,10,6,11,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,5, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,5,0,4,8,0,0,0,0,0,9,7,0,0,0,0,0,0, +13,5,0,0,0,0,8,0,0,0,0,0,0,0,0,5,0,0,0,0,0,0,0,0,8,5,0,0,11,7,0,0,0,0,0,0,8,6,0, +0,0,0,0,7,0,4,0,0,0,0,0,0,0,5,0,6,0,5,0,0,0,0,0,0,0,0,0,0,0,0,10,4,9,0,0,0,0,0, +0,4,0,0,0,0,10,5,10,7,0,0,0,0,0,0,0,0,16,7,0,0,0,0,0,7,0,0,0,0,11,0,0,0,0,0,0,0, +0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9,5,0,4,0,0,0,7,0,0,0,0,0,0,13,0,0, +0,0,0,0,0,0,0,0,7,0,4,0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,13,7,0,7,0,4,16,0,0,0,0,6,8,7,9,7,0,0,0,0,8,6,0,0,0,0,0,0,0,0,0,0,0,0, +0,6,0,0,8,5,0,4,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,5,11,7,0,0,11, +0,0,0,0,0,9,5,0,4,0,0,0,0,9,7,8,6,0,0,0,0,0,0,10,0,0,0,0,0,0,5,0,0,0,0,0,0,0,0, +0,7,0,0,0,0,0,0,0,0,0,0,0,4,10,6,0,7,0,0,0,0,0,0,0,5,0,0,0,0,0,0,10,7,10,0,0,0, +0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,7,0,6,8,7,12,4,0,0,0,0,0,0,0,5,14, +0,0,0,0,0,0,4,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0, +6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,4,0,0,20,4,0,0,0,7,0,6,0,0,0,0,0,0,0,0,8,0, +0,6,15,0,0,6,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,12,0,0,0,9,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,5,0,0,0,0,0,0,8,6,0,0,18,0,0,0,10,0,0,0,0,0,0,0,0,6,0,0,0,6,0,0,9,6,0, +6,0,0,0,0,0,0,0,0,9,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,11,7,0,0,0,0,9,0,9,0,0,4, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,9,5,0,0,0,0,0,0,0,0,11,0,0,0,0,0,0,0,0,0,10,0,0,0,0,7,0,0,0,0,0,0,0,0,0,7,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,8,0,8,0,0,0,16,0,0,0,0,0,0,0, +0,0,0,6,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,0,8,0,0,0,11,0,0,0,0,0,0,0,0,0,0, +6,0,0,0,0,11,0,0,0,9,7,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,7,0,7,0,6, +0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,6, +0,0,0,0,0,0,0,6,0,0,18,0,8,0,0,0,0,0,0,0,0,0,0,0,0,5,0,0,0,0,0,0,0,7,0,4,0,0,0, +0,0,0,0,0,0,0,8,0,0,0,0,0,16,0,0,0,0,0,16,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,18,0,0,0,0,0,0,0,0,0,9,7,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,6,0,4,0, +0,0,0,0,0,0,0,9,4,0,0,0,0,12,5,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,12,5,0,0,0,0,0,0,0,5,0,0,10,6,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,9,0,0,0,11,0,0,6,0,6,0,0, +0,7,0,0,0,0,0,0,8,0,0,0,0,6,0,0,0,0,0,0,19,0,0,0,12,0,9,0,0,0,0,0,10,7,0,0,0,0, +0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9,0,0,0,16,7,12, +0,0,6,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,12,6,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,10,5,0,0,0,0,0,0,0,4,0,0,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,7,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10,4,0,0,0,0,0,0,0,4,0,0,9,0,0,0,8,0,12,4,0,0,0,0, +0,4,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,8,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,9,0,0,0,0,0,0,0,12,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,11,0,0,5,0, +0,0,0,0,0,13,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,17,0,0,0,0,0,0,0,8,6,0,6,0,0,0,0,0,0, +0,4,0,0,0,0,0,6,0,0,9,0,0,0,0,0,0,6,0,0,0,0,0,0,11,0,0,0,0,0,0,0,10,6,0,0,0,0,8, +6,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,7,0,0,0,0,0,7,0,6, +10,7,0,0,10,5,11,6,0,0,0,0,0,7,16,0,0,0,0,6,0,0,0,0,0,0,10,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,5,0,0,0,7,9,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9,6,0,0,0, +0,0,0,0,0,8,7,0,0,0,0,11,6,0,0,0,0,0,0,0,0,0,6,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +8,7,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,12,7,0,7,0,0,0, +0,0,0,0,6,0,0,0,0,9,0,0,0,23,0,0,0,0,0,10,5,0,0,0,0,0,0,0,0,0,4,0,0,11,7,10,0,0, +0,0,0,0,0,0,0,0,0,0,6,0,0,8,7,0,7,0,0,8,7,8,0,0,0,0,0,0,0,0,0,0,0,14,5,0,0,0,0, +0,0,0,0,18,6,8,7,0,0,0,0,0,0,0,4,0,0,0,0,0,0,11,0,0,0,9,7,12,6,0,0,0,0,0,0,0,0, +0,0,12,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9,6,0,0,0,7,11,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,6,8,7,0,0,0,6,10,0,0,0,9,0,0,0,0,0,0,0,0,0,8,6,0,0,0,0,0,6, +10,7,0,0,0,7,0,0,8,4,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,19,0,0,0,0,0,0, +0,0,0,8,7,8,6,0,0,11,7,10,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9,4,8,7,0,0,0,0,0,0,0,0, +0,5,0,0,13,0,0,0,0,5,0,0,9,7,0,0,0,0,0,0,0,4,0,0,11,0,0,7,0,0,0,0,0,0,0,0,0,6,0, +0,0,0,0,0,12,7,19,0,8,6,0,0,0,0,0,6,0,0,0,0,0,0,0,0,10,6,8,0,0,0,0,0,0,0,0,0,0, +6,0,0,0,0,0,0,0,0,0,0,10,0,0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,7,0,0,12,0,0,0,0,6,9,6, +14,0,0,0,0,0,0,6,0,5,0,0,8,7,0,0,0,6,0,4,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,4,0,6,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,0,0,0,0,0,0,5,0, +7,0,0,10,0,9,7,0,6,0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,12,6,0,0,0,0,0,5,0,6,0,0,0,0, +0,0,0,0,0,0,0,6,0,0,0,0,9,7,0,0,0,0,0,0,11,6,0,0,0,0,0,0,0,0,0,0,11,7,0,0,13,7, +0,0,0,0,0,0,0,0,12,0,0,4,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,6,11,5,0,5,13,0,8,0, +0,0,0,6,0,0,0,0,0,0,11,0,0,0,0,0,8,7,0,0,0,0,0,0,0,0,0,0,11,0,0,0,0,0,0,0,11,5, +9,6,0,0,0,4,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,6,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,10,0,0,0,8,5,0,0,9,0,0,0,8,7,9,0,0,0,0,0,0,0,0,7,0,6,0,0,0,0,0,0,0,0,0, +0,11,0,13,6,0,0,9,0,0,0,0,0,0,0,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0, +0,0,0,0,0,5,21,6,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,24,5,0,0,0,0,0,0,0,0,10,0,8,0, +0,6,0,0,0,4,0,0,9,0,0,0,0,0,0,0,0,0,0,4,0,0,8,6,0,6,0,7,10,0,8,4,0,4,0,0,0,0,0, +5,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,0,0,0,0,0,0,6,12,0,0,7,0,0,0,5,0,0, +0,0,0,0,0,0,0,6,0,0,8,6,0,0,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +15,7,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,0,0,0,0,0,0,0,0,6,0,0,24,7,0,0,0,0,0,0,0,0,0, +7,0,0,0,0,0,0,0,0,0,0,9,6,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,6,0, +0,0,0,0,0,0,0,0,0,0,0,0,6,0,6,0,4,12,0,0,7,0,0,0,0,0,5,0,0,0,0,0,0,0,0,15,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,7,0,0,0,0,0,0,0,0,0,0,8,0,0,0, +0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,6,0,0,0,0,0,0,9,0,9,6, +0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,8,6,0,0,0,0,0,0,0,0,0,0,8,4,0,7,0,0,0,0,0,0,0,0, +22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,4,0,7,0,0,21,7,0,7,9,6,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,10,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,8,0,0,6, +0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,8,7,0,0,0,0,23,0,0,0,0,7,0,0,0, +4,0,0,0,0,0,0,0,0,9,4,11,7,0,5,0,0,0,0,11,0,0,4,20,0,0,0,0,0,0,0,0,0,0,0,11,5,0, +7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +21,0,0,0,0,0,0,7,0,0,0,0,0,0,0,5,0,0,0,0,0,6,0,0,0,0,11,6,0,0,0,0,0,0,0,0,9,6,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,5,0,4,9,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0, +0,0,0,10,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,8,7,0,0,11,7,0,0,0,0,0,0,0,4, +0,4,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10,6,0,0,0,5,0,0,0,0,0,0,0,0,0,0,8,7,0, +0,0,0,0,0,0,0,0,6,0,0,21,6,0,0,0,0,0,6,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,14,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,9,6,0,0,8,0,0,7,0,0,0,0,0,0,0,0,0,0,10,0,0,0,0,0,0,0,0, +0,0,0,8,7,0,0,11,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10,5,0,0,0,7,13,7,10,4,0, +0,0,6,0,0,0,0,0,0,0,0,0,5,10,0,0,0,0,0,0,5,0,0,0,7,0,0,0,0,0,0,8,4,0,0,0,0,0,6, +0,0,0,0,0,0,0,0,0,0,12,7,0,6,0,0,10,0,0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,6,0, +0,0,0,0,7,0,0,8,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,10,5,0,6,0,0,0,0,0,4,0,0,0,0, +0,0,0,0,0,4,0,0,0,0,9,0,11,4,0,0,0,6,0,0,0,5,12,7,0,5,0,0,0,0,0,4,0,0,0,7,0,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,4,13,6,10,0,0,0,17,0,0,4,0,0,0,0,0,6,0,4,0,5,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,0,11,7,0,0,0,7,0,0,0,6,0,0,0,0,0,0, +0,6,0,4,0,0,0,0,8,0,0,0,0,5,0,0,0,0,0,4,0,0,0,0,0,0,0,0,9,0,0,0,0,0,0,0,9,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9,5,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,12,0,0, +0,0,7,0,0,0,0,0,0,0,0,0,0,0,7,0,0,16,4,0,0,11,0,8,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +8,7,0,4,0,0,0,0,0,0,0,4,0,0,0,0,0,0,8,6,0,0,8,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10, +7,0,0,0,0,0,0,9,0,0,0,0,0,0,0,12,5,10,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10,5,0, +5,18,7,0,0,14,0,0,0,0,0,0,0,9,4,0,7,0,0,0,0,0,0,0,5,0,0,0,6,0,0,0,6,0,0,0,0,0,0, +8,0,0,0,10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10,5,0,0,0,7,0,0,0,0,0,0,11,0,0,0, +10,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,14,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +11,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,14,6,0,0,0,0,11,4,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,0,0,0,0,0,0,0,10,7,0,6,0,0,9,0,9,5,0,0,0,0,0, +0,0,0,10,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,6,0,0,8,5,0,0,0,0,0,0,0,0,0,0,11,4,0,6, +0,6,0,0,0,0,0,0,0,0,0,0,0,0,11,0,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,12,4,0,6,8,6,0,0,0,0,0,0,0,0,0,0,8,0,0,5,0,0,0,0,0,0,0,7,0,0,13,0,0,0,0,0,0,0, +0,0,0,0,0,0,9,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,0,0,0,0,0,12,7,0,6,0,0,0, +0,0,0,0,0,0,0,0,0,13,4,0,7,0,0,0,7,0,7,0,0,0,0,0,0,0,0,10,4,0,0,0,0,0,0,0,0,0,0, +9,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,0,10,6,21,5,0,0,0,0,8,0,0,0,0,4,0, +7,0,0,0,0,0,0,11,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,4,0,0,0,0,0,0, +0,7,9,6,11,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,7,10,0,0,0,0,0,0,6,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,5,0,0,0,0,0,0,0,0,19,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,18,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,7,0,0,0,0,0,0,9,4,10,4,0,7,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,9,7,9,7,10,4,0,7,0,0,0,0,0,0,0,6,12,0, +0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,20,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0, +0,14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,8,0, +0,0,0,0,0,5,0,0,8,7,0,0,0,7,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,11,0,0,0,0,0,0,0,0, +0,0,0,0,4,0,0,8,0,0,6,0,0,0,7,0,0,0,0,10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10,6,0,0,0,6,0,6,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,11,0,0,0,0,0,0,0,0,0,0,0,0,7,9,7,0,0,0,4,8,0,0,0,0,6,11,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,10,0,0,0,0,0,0,0,13,4,0,0, +12,6,0,6,0,0,0,0,8,7,0,7,0,0,0,0,0,6,0,0,0,0,0,0,12,6,0,4,0,0,0,0,0,0,0,0,0,0,9, +7,22,0,0,0,0,4,0,0,0,0,0,6,0,0,0,4,0,0,9,0,0,6,0,0,24,7,0,7,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,10,6,0,5,0,0,0,0,0,0,0,7,0,0,8,0,0,0,0,0,0,0,10,5,0,0,0,0,0,0,0,0,0,7,0, +7,0,0,0,0,0,0,13,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,18,0, +0,0,0,0,0,7,12,0,9,4,0,0,0,0,0,0,0,0,0,4,0,0,0,0,8,0,0,0,0,0,0,0,0,4,0,0,0,7,0, +0,0,0,8,7,0,0,0,0,0,0,0,0,0,4,18,0,0,0,0,0,10,0,0,5,0,0,11,0,0,0,0,0,0,5,0,6,0, +0,0,6,0,0,0,0,0,0,0,0,0,5,0,0,0,0,0,0,0,0,8,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,6,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0, +4,0,0,0,0,0,0,10,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,12,0, +0,0,0,5,8,0,0,0,0,0,0,0,8,6,0,0,0,0,0,0,0,0,20,7,0,0,0,0,0,0,0,0,0,0,0,4,9,0,12, +6,8,0,14,7,0,5,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,17,0,0,0,0,0,10,0,0, +0,0,0,0,0,0,0,0,0,0,6,0,6,0,0,0,0,0,0,0,0,12,0,0,0,0,0,0,0,9,6,0,7,12,0,0,0,0,4, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,7, +0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,5,0,0,0,4,0,0,9,0, +12,6,0,5,0,0,0,6,0,4,0,6,0,0,0,0,0,0,0,0,10,7,0,0,0,0,0,0,8,0,0,0,0,4,0,0,0,0, +10,0,0,0,0,0,0,0,8,6,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,5,0,0,0,0,0,0,0,0,0,0,0, +6,0,0,12,6,20,5,0,0,0,0,0,0,0,0,0,0,0,0,9,5,0,5,0,0,0,6,13,7,0,0,0,0,15,6,0,0,0, +6,0,0,13,7,0,0,0,0,0,0,9,0,0,0,0,0,0,0,0,0,0,5,0,7,0,0,0,0,0,4,0,0,0,0,0,0,0,0, +10,6,0,0,0,0,0,6,0,0,0,0,9,0,0,0,0,0,19,6,0,0,0,0,0,0,0,0,0,0,13,0,11,0,0,0,0,0, +0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,10,6,0,0,0,0,0,0,0,0,10,0,0,6,0,0,0,0,8,0,0, +0,9,0,15,4,0,6,0,0,0,0,0,6,12,0,0,0,0,0,0,0,14,7,0,0,0,0,0,0,0,0,0,0,0,5,0,0,0, +0,0,0,0,0,8,7,0,0,0,0,0,6,10,0,0,0,0,0,0,0,0,7,8,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9,7,10,5,0,0,0,0,8,0,0,0,0,4,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,6,12,0,0,0,10,7,0,5,0,6,0,0,0,0,8,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,6,0,4,0,0,0,0,0,7,0,0,0,0,0,0,0,4,9,6,0,0,0,7,0,0,0,0,0,0,0,0,8,6,0,0, +0,0,0,0,0,4,12,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,0,0,0,0,0,0,7,0, +0,0,0,0,0,12,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,12,6,0,6,9,4,0,0,8,4,0,6, +0,0,0,0,0,4,0,0,0,0,0,0,0,6,0,0,9,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,8,0,0,6,13,4,0,5,8,0,0,0,0,0,0,0,8,0,0,0,10,5,0,0,9,0,0,0,0,0,0,6,0,0, +24,0,0,0,0,0,0,0,8,0,0,7,0,0,12,0,8,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9,7,0, +6,8,0,10,0,9,7,0,0,0,5,0,0,0,0,0,0,0,4,8,5,0,0,0,0,8,7,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,19,0,0,4,0,0,0,0,0,6,0,0,0,0,0,5,0,0,0,0,8,0,0, +0,0,0,8,6,0,0,0,0,0,0,0,0,0,0,8,6,0,0,0,0,10,4,0,0,0,0,0,0,0,6,0,0,0,4,20,0,0,7, +10,6,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10,7,0,0,0,0,9,6,0,0,0,0,0,0,0,4, +12,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,12,0,0,0,0,0,0,0,0,0,11,0,0,0,0,0,9,4,0,5,0,0, +0,0,0,0,0,6,0,6,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,6,9,0,0,0,0,7,0,0,0,0,0,6,0,5,0,0,0,0,0,0,0,0,9,0,0,0, +0,6,0,0,0,0,0,0,0,0,0,0,11,0,0,0,0,0,0,0,0,0,0,0,0,0,17,7,0,0,13,6,14,6,0,0,0,0, +8,0,0,0,0,0,0,7,12,7,8,7,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,4,0,0,0,0,0,4,0, +0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,12,0,0,0,12,4,0,0,10,7,0,0,0, +0,0,0,10,0,0,6,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,12,0,0,6, +0,0,0,0,0,0,0,0,8,7,12,0,0,0,0,0,0,6,0,6,0,4,0,0,18,6,0,0,0,6,0,0,0,0,0,6,10,6, +0,0,0,0,0,0,8,7,14,0,0,0,0,0,0,6,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,19, +0,0,0,8,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,8,7,0,0,10,5,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0, +0,0,9,4,8,0,0,0,0,0,0,4,0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,4,0,0,0,0, +0,6,0,0,9,7,0,0,0,0,0,5,0,0,0,0,8,7,0,0,14,0,0,0,0,6,0,0,0,0,0,0,9,6,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,10,0,0,0,0,5,0,7,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0, +0,0,0,6,0,0,0,6,0,4,0,0,0,0,0,4,0,0,0,0,12,0,0,7,0,0,0,0,0,0,0,0,0,6,0,0,0,0,0, +0,12,0,16,6,0,0,0,0,0,0,11,7,0,4,8,7,0,0,0,0,0,6,0,0,0,0,16,0,0,0,0,6,0,0,0,0,0, +0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,4,10,7,0,0,0,0,0,0,12,7,0,0,0,0,0,0,0,0,0,0, +0,0,10,0,0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,13,4,0,0,10,0,0,0,0,0,0,0,0,0,19,0,0,0, +0,0,0,0,0,0,0,0,0,0,8,6,22,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0, +5,0,0,0,0,0,5,0,0,0,0,0,5,0,0,0,6,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +4,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,7,0,0,18,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,14,7,0,0,11,5,0,0,0,5,0,0,0,0,12,5,0,0,0,0,0,0,0,0,0,0,24,6,0,0, +0,7,0,4,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,7,0,4,0,0,0,0,8,7,0,0, +9,6,0,0,14,5,0,0,0,6,0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,12,6,0,0,0,0,0,0,0,6,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,7,0,0,0,5,0,0, +0,0,12,7,0,0,0,0,10,0,0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,5,0,0,0,0,0,0,0,0,0,6,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,0,0,6,0,0,13,7,0,0,0,0,0,0,14,0,11,4,0, +0,0,4,0,0,0,0,14,5,0,0,0,0,0,5,11,5,0,0,0,0,22,5,0,0,0,0,0,7,0,0,0,0,0,4,0,0,0, +4,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10,6,0,0,17,0,10,0,0,0,8,0,0,0,19, +5,18,7,0,0,0,0,0,0,0,0,0,0,10,0,0,0,0,0,0,0,0,0,0,0,10,6,0,6,0,0,0,0,10,4,0,4,0, +0,0,0,0,0,14,7,0,5,0,0,0,0,0,6,0,0,0,0,0,0,0,0,8,0,9,6,12,0,0,6,0,0,0,0,0,0,0,0, +12,0,10,6,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,4,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,5,13,0,9,7,0,0,0,0,0,0,0,0,0,0,0,7,9,7,0,0,8,0,0,0,0,0, +22,0,0,0,0,0,0,0,23,6,14,0,0,0,0,0,0,7,0,0,0,0,11,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0, +0,0,10,0,0,6,0,0,0,0,0,0,0,0,0,6,0,0,8,5,0,0,0,0,0,0,0,0,0,7,11,6,21,0,0,0,0,0, +0,0,0,0,0,5,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0, +0,0,0,0,0,0,0,4,9,7,0,0,0,0,0,0,12,0,0,0,0,7,0,0,0,0,0,0,0,0,10,4,0,0,0,0,0,0,9, +0,0,0,20,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,11,7,0,0,0,0,0,0,0,6,15,0,0, +0,0,0,0,0,0,0,0,0,0,0,12,4,0,5,0,0,0,0,0,0,11,7,17,6,0,0,0,0,0,0,15,6,0,7,0,0,0, +0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,14,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,6,0,5, +0,0,11,0,11,7,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0, +17,0,0,0,0,6,0,0,0,5,0,0,0,0,0,0,8,7,9,6,0,0,14,0,0,0,0,0,0,0,0,0,16,0,0,0,0,0, +8,7,0,4,0,0,0,0,0,0,0,6,0,5,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0, +0,0,0,5,0,4,0,0,8,7,0,6,12,5,0,7,18,7,0,0,8,5,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0, +10,0,11,0,0,0,0,0,0,0,0,0,0,0,9,0,0,4,0,6,0,7,0,0,0,0,0,6,0,0,0,6,0,0,0,0,0,0,0, +7,0,0,0,0,8,0,0,0,15,0,0,0,10,0,0,0,0,0,0,0,0,0,0,0,0,0,10,6,0,0,0,0,0,0,0,0,0, +0,0,6,0,0,0,0,23,0,0,0,10,7,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,5,0,0,0,0,0,0,8,6,0,0, +0,0,0,0,12,7,9,7,0,0,10,6,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,9,0,8,7,0,0,0, +6,0,6,0,4,0,5,0,0,0,0,0,5,8,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,7,10,5,0,0,11,6,0,0,0,0,0,0,0,6,0,6,0,0,0,0,0,0,0,0,0,0,14,0,0,0,0,4,9,7,0, +0,0,0,11,7,0,0,0,0,0,5,0,0,0,7,0,0,0,0,23,6,11,4,0,0,0,0,0,0,9,0,0,0,10,6,0,0,0, +0,9,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,6,0,0,10,6,0,0,0,7,0,0, +0,0,0,0,0,0,0,0,20,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,13, +6,11,7,0,0,0,0,0,0,0,0,0,0,0,0,10,5,0,0,0,6,0,0,0,5,0,6,0,6,0,0,0,0,0,0,0,0,0,0, +0,6,0,0,0,0,8,7,0,5,0,0,0,0,0,6,0,0,0,0,0,0,0,4,10,0,8,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,10,6,0,0,0,0,0,0,10,0,0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,6,0,0, +0,0,0,0,0,0,10,0,0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,12,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,11,6,0,4,0,0,14,5,0,7,0,0,0,0,0,6,16,0,0,0,0,0,0,0,10,0,0,7,15,0,0,0,11,7,0,0, +0,0,0,0,0,0,0,0,8,7,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,18,5,0,0,0, +0,8,0,0,6,0,0,0,0,0,0,9,5,0,0,23,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,16,6,0, +0,0,0,0,0,0,7,0,0,0,0,15,7,0,0,0,0,8,0,0,0,14,0,0,0,0,0,0,0,16,7,0,0,0,0,0,7,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,12,6,11,7, +9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,13, +7,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,0,12,0,10,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10,0,8,0,0,5,8,7,10,6,0,0,0,7,0,0,0,0,12,6, +0,0,9,0,0,0,12,0,0,0,0,5,0,0,0,0,0,0,0,0,0,0,0,4,10,0,0,0,10,5,0,0,0,0,0,0,9,6, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,18,6,0,0,9,5,0,4,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,0,0,0,0,0,0,0,9,0,0,5,0,0,8,7,8, +6,0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10,6,10,0,9,4,0,0,0,0,0,0,0,6, +11,0,0,0,0,0,0,0,0,0,0,0,8,0,0,6,0,6,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,8,7,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0, +0,0,0,10,0,0,0,8,7,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,0,0,0,0,0, +0,0,8,4,0,5,0,0,0,0,0,0,0,7,0,0,0,6,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,5,0,0,0,0,0,0,0,0,8,7,0,0,0,0,0,0,0,0,22,0,0,0,0,0,0,0,8,5,0,0,0, +0,0,0,0,7,0,0,0,6,0,0,0,6,0,6,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,5,0,6,0,7,0,0,0,0, +20,0,0,0,0,0,0,0,0,0,0,7,9,0,0,0,0,0,0,6,0,6,0,7,0,0,0,7,0,0,0,0,0,0,0,4,0,0,0, +0,0,0,14,7,0,0,0,5,0,0,22,4,10,0,0,0,0,0,0,4,8,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,11,5,13,0,0,0,0,0,0,0,0,0,8,0,0,7,0,0,0,0,0,4,0,0,0,4,0,0,0,0,0,0,10,7,0, +0,0,0,0,0,0,6,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,11,7,0,7,14,6,0,0,0,0,9,5, +0,0,0,0,0,6,0,0,0,5,10,0,8,6,0,0,0,0,0,0,0,0,9,7,0,0,0,0,0,0,0,6,0,0,8,4,0,6,0, +0,0,5,0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,12,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,7, +14,0,0,5,0,0,18,0,8,4,0,6,0,0,20,0,13,0,0,0,0,7,0,4,0,0,0,0,0,4,8,4,0,0,0,0,0,6, +0,0,0,0,0,4,0,0,0,4,0,0,0,0,0,4,0,0,0,0,0,0,0,0,14,0,0,0,0,0,9,7,0,0,9,0,0,0,0, +0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,20,0,14,0,0,4,0,6,8,5,0,0,0,0,0,7,0,0,0,0,0,0, +0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,10,4,12,7,0,6,0,0,9,7,10,5, +0,0,8,6,0,4,0,0,0,0,0,0,0,0,0,0,0,0,17,0,0,0,0,0,0,0,18,0,0,0,14,7,0,0,0,0,0,4, +0,0,0,0,0,0,17,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,13,4,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0, +0,0,0,0,0,8,6,0,0,0,0,0,0,0,0,8,5,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,5,0,7,0,0,0,0,0, +7,0,0,0,0,0,0,0,0,0,7,0,6,0,0,0,0,0,0,0,0,8,5,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,5,0, +0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,23,0,0,7,0,0,0,0,0,0, +0,0,0,0,0,0,0,4,0,0,0,0,0,0,12,7,8,4,0,0,0,0,0,0,0,0,0,6,0,0,9,5,0,0,0,7,0,0,0, +0,0,0,0,0,0,4,10,0,0,7,0,0,0,5,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,5,0,0,18,7, +0,0,8,0,0,5,0,0,10,0,0,0,0,0,0,6,0,0,0,0,0,5,0,7,0,0,0,0,0,0,0,0,0,0,16,0,0,0,0, +6,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,6,0,0,10,0,0,5,10,4,0,0,12,0,0,0,0, +6,22,4,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,5,0,0,0,0,0,7,0,5,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,12,0,0,0,0,0,0,6,0,7,0,0,0,6,0,6,8,5,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,8,5,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,5,0,0,0,7,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0, +0,0,0,0,0,0,5,0,0,0,0,0,0,0,0,16,6,0,0,0,6,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,12,7,0,0,0,0,9,0,0,0,0,6,0,0,11,0,0,0,0,0,13,0,9,6,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,0,0,0,0,0,0,0,0,0,0,4,0,0,10,7,0,0,0,7,0,6,0, +0,0,0,0,0,0,0,0,0,8,7,0,0,0,0,11,0,15,0,22,7,0,4,0,6,0,0,0,0,0,7,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,4,0,7,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0, +18,0,0,0,0,0,0,0,0,0,14,0,0,4,0,0,0,0,8,7,9,0,0,0,0,0,9,0,0,0,14,0,0,0,0,0,0,0, +0,0,11,7,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,7,0,0,0,6,0,6,0,0,0,0,8,0,0,0,0, +0,11,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,0,0,0,9,4,0,0,0,0,0,4,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,8,7,0,0,0,0,0,0,0,0,0,0,0,5,0,0,0,0,0,0, +0,0,0,0,0,0,8,6,0,0,9,5,0,0,0,0,0,0,0,0,14,0,0,0,0,0,0,6,0,0,0,0,0,0,0,6,0,5,0, +0,10,6,9,0,0,0,0,6,0,0,0,0,0,6,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0, +11,7,12,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,4,0,5,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0, +0,0,0,0,6,0,0,0,0,0,0,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,5,0,0,10,6, +0,0,0,4,0,7,13,0,0,4,0,0,11,4,0,6,0,0,0,0,0,6,8,7,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,5,0,0,0,0,0,0,8,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,5,0,0,0,0,12,6,0,0,0,0, +11,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,11,5,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8, +7,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,4,0,0,0,6,17,0,9,0,10,6,0,6,12,0,0,4,0,0,0, +0,0,0,0,0,0,0,8,5,12,7,0,4,0,0,0,0,0,0,0,0,0,0,11,0,9,0,10,6,11,5,0,7,0,0,8,0,0, +7,0,4,0,0,0,7,0,0,0,0,0,0,8,6,0,0,0,6,0,0,0,0,0,0,0,0,9,0,0,0,0,0,0,0,0,0,0,6,0, +0,0,0,0,0,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,11,0,0,0,0,6,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10,0,10,0,0,0,0,0,8,6,0,0,0,0,0,6,12,0,0,0,0,0, +0,0,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,6,0,0,16,0,11,5,0,0,0,0,0, +0,0,0,0,0,10,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,9,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,6,0,7,0,0,0,0,0,0,0,0,0,0,0,0,8,4,0,0,0,0,0,6,10, +7,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,6,0,0,0,0,0,0,9,5,0,0,0,0,8,0,9,0,0, +0,0,0,0,0,0,7,10,0,13,0,0,6,0,0,0,0,0,0,0,0,0,6,9,4,0,0,0,0,0,0,10,0,0,0,0,0,10, +0,0,0,0,0,0,0,10,6,11,0,0,0,0,0,9,0,0,0,0,0,0,4,0,0,0,0,0,0,10,5,0,0,0,0,0,6,0, +0,0,0,0,0,18,4,0,7,0,0,0,0,0,0,24,0,8,6,0,7,0,0,0,0,15,0,0,0,0,0,0,0,0,0,0,0,0, +0,8,5,0,0,0,0,10,7,0,6,0,0,0,0,0,0,0,0,8,5,10,0,0,0,0,0,0,0,0,0,0,0,0,0,11,0,0, +6,0,0,8,7,0,0,0,0,0,0,0,0,0,0,12,6,0,0,0,0,0,0,0,4,0,5,15,0,0,0,0,7,0,7,0,0,0,0, +0,0,0,0,0,6,10,5,0,0,0,6,0,0,8,7,0,0,0,0,0,0,0,0,0,0,0,7,0,0,12,0,0,0,0,0,0,0,0, +0,0,5,0,0,0,0,0,0,14,4,18,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,0,0,0,11,0,10,4,9,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,6,0,0,0,0,0,4,0,0,0,0,0,7,0,0,0, +0,0,0,0,0,0,0,0,7,13,7,0,0,0,0,0,0,0,5,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,7,0,0, +0,0,0,0,0,0,0,5,0,0,0,0,0,6,0,0,0,0,0,0,11,0,0,0,0,0,0,0,0,0,8,0,10,6,0,4,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,6,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,6,0, +0,0,0,0,0,0,0,10,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,9,7,0,0,0,0,0,6, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,10,6,0,0,0,0,0,0,0,6,0,0,0, +0,0,0,0,5,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,6,0,0,0,5,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,6,11,0,0,0,0,6,0,0,0,0,0,0,0,6, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,22,0,0, +6,0,0,0,0,0,0,0,6,10,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14,0,0,0,0,0, +0,0,0,0,0,0,0,6,0,6,0,0,0,5,0,0,0,0,0,0,0,5,0,0,10,0,11,5,0,0,0,0,0,0,14,7,9,7, +0,6,0,0,0,0,0,4,0,0,0,0,0,0,11,7,0,6,0,0,0,0,0,0,9,7,0,4,0,0,0,7,0,0,0,0,0,5,0, +0,0,0,0,5,0,0,0,7,0,0,0,0,0,5,0,0,0,0,17,5,0,0,8,0,0,0,0,6,9,4,0,0,0,0,0,0,0,0, +8,7,11,7,9,0,0,0,0,0,0,0,0,0,8,7,0,0,0,0,0,0,0,0,0,0,0,6,9,5,0,0,8,6,0,0,0,5,0, +0,0,0,9,0,0,0,9,6,0,7,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0, +0,0,0,0,4,0,0,0,0,10,0,0,0,0,0,0,0,0,4,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,4,0,0,0,5,0,0,0,0,0,7,0,0,0,0,0,7,13,5,0,0,0,7,0,0,0,0,0,7,9,6,11,7,0,7,0,0,0, +0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,8,5,0,0,0,5,9,4,0,0,0,0,0,0,0,0,8,4,0,0,0,0, +24,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0, +0,0,0,0,6,0,0,0,7,0,0,0,6,0,0,0,0,0,0,0,0,0,5,11,6,0,4,0,7,20,0,8,5,9,5,9,0,0,6, +0,0,0,0,0,0,0,0,0,0,0,7,23,5,0,0,8,4,0,0,10,0,0,6,0,5,0,0,0,0,0,0,0,0,0,0,0,7,0, +0,0,0,0,0,0,0,0,0,11,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,0,9,0,0,0, +10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0, +6,0,0,0,0,14,0,18,4,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,9,6,0,4,0,0,0,0,0,0,8,4, +11,0,0,0,11,0,0,0,0,0,0,0,0,0,0,0,8,4,0,0,0,0,0,0,0,0,12,0,10,7,0,0,10,0,0,0,0, +0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,6,0,0,0,0,0,6,0,0,0,0,8, +6,10,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,0,0,4,0,6,0,4,0,0,0,0,0,5,0,0, +0,0,0,0,0,0,0,7,0,0,0,7,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,17,7,11,0,0,0,0,0,0,0,0,0,0,4,12,6,0,0,0,5,0,0,0,6,0,0,0,0,0,0,0,0,0,0, +0,5,12,7,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,6,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +7,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,6,0,6,0,0,20,0,0,0,0,0,0,0,0,0,8,7,0,0,0,0,0,4, +0,0,0,5,0,0,0,0,12,0,0,0,0,0,0,0,0,0,0,0,10,0,0,0,0,6,0,4,13,0,0,7,0,0,0,0,0,0, +0,0,0,0,0,6,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,6,0,0,12,6,0,7,0,0,0,0,10,0,23,6,0,0, +0,4,0,0,0,0,0,6,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +10,0,9,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,12,0,0,0,0,0,0,0,11,0,9,7,0,0, +0,0,0,0,0,0,0,0,9,7,0,4,0,0,0,0,8,7,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4, +0,0,0,0,0,6,0,0,10,7,10,5,0,0,8,0,8,0,0,0,0,0,0,4,0,5,10,0,0,0,0,0,0,0,9,0,0,6, +0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,11,7,0,0,0,0,0,0,0,0,9,4,0,0,0,0,0,6,0,0,8, +7,0,0,0,0,0,5,0,0,0,0,0,0,0,0,10,0,0,0,0,5,0,4,0,0,0,0,0,0,12,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,24,7,0,0,0,0,0,0,0,0,0,0,0,0,9,0,0,6,0,0,9,0,0,0,0,0,0,7,0,6,13,0,8, +0,0,0,0,0,0,0,0,0,9,7,0,0,0,0,0,0,0,6,0,0,0,0,8,5,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0, +4,0,0,0,0,0,4,0,0,0,0,0,0,0,6,8,0,0,0,0,6,8,0,0,0,0,0,0,0,0,0,0,0,0,0,14,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,4,0,4,0,0,0,5,0,7,0,0,10,0,10,7,0,0,12,5,0,0,9,0,0,0,10,0, +0,6,0,0,0,6,0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,5,0,0,0,0,0,0, +12,0,0,0,0,0,8,5,13,6,0,0,0,0,0,0,9,4,0,0,0,0,8,0,0,0,0,0,8,7,0,0,0,0,0,0,0,0,0, +0,0,6,0,0,14,0,0,0,0,6,0,0,0,0,0,6,0,0,0,0,17,6,0,0,0,0,12,6,0,0,0,0,8,0,0,7,0, +7,0,4,9,0,0,6,0,0,0,6,0,0,0,0,0,0,8,7,0,0,0,0,0,0,11,0,0,4,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,18,7,0,4,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,6,0,0,0,0,0, +0,0,0,12,5,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,0,8,0,11,7,0,0,0,0,0,0,0,0,0,4,0,0,0,0, +11,0,0,0,0,0,0,0,21,0,0,6,10,0,0,0,0,0,9,0,10,0,0,0,0,0,11,0,0,0,0,6,0,0,0,0,0, +5,0,0,0,0,0,0,10,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,4,0,0,23,7,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,9,7,0,0,0,7, +0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,6,0,0, +11,6,0,0,0,0,0,0,0,6,0,0,0,0,10,7,0,0,9,4,0,0,11,0,8,5,0,0,0,7,8,5,22,0,0,0,9,6, +0,0,0,0,0,0,0,6,10,4,0,0,0,0,0,7,9,4,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,6,0,4,0, +0,0,0,11,6,0,0,0,0,0,0,0,0,0,0,0,7,0,6,0,0,0,0,0,7,0,0,0,0,0,0,0,6,0,6,0,4,0,0, +0,0,0,0,0,7,0,7,0,4,13,0,0,0,0,0,8,0,0,0,0,7,0,0,0,0,0,0,11,6,0,7,0,0,0,0,9,0,0, +0,0,0,0,0,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,4,8,0,0,0,0,0,8,0,0,0,0,0,0,6,0,0,0,4,0,0,0,0,0,0,0,6,0,0,0,0,13,5,8,0,0, +0,0,0,0,0,14,0,0,6,0,0,0,0,0,0,0,0,0,7,0,0,17,6,0,0,0,0,13,4,0,0,9,6,0,0,10,5,0, +0,10,5,0,0,0,0,13,0,0,0,0,6,0,0,0,0,0,0,10,0,12,0,0,0,0,0,0,0,0,0,0,0,8,4,0,4,0, +0,0,4,0,0,0,0,0,4,0,0,12,0,0,5,9,4,0,0,0,0,0,0,0,0,0,5,8,5,0,0,0,7,0,0,0,0,8,7, +0,0,0,6,12,5,0,0,0,5,0,0,0,5,0,0,0,0,0,4,12,0,0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,8,7,0,0,0,0,0,6,0,0,0,0,0,0,0,6,0,0,0,0,0, +0,9,6,0,0,0,0,0,0,0,0,0,4,0,0,0,6,0,0,0,4,11,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0, +0,0,0,0,12,7,0,0,0,7,10,7,0,0,11,0,0,0,0,0,0,0,0,0,11,7,0,0,0,6,0,0,11,0,0,0,0, +0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,22,0,10,7,0,0,8,5,0,0,0,0,0,5,0,0,0,0,0,0, +0,0,0,0,9,6,8,7,0,6,0,0,0,0,0,5,0,0,0,0,0,0,8,7,0,0,0,0,9,7,0,0,0,6,0,0,8,7,0,0, +0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,4,0,5,0,0,0,4,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,18,6,0,0,0,0,0,0,0,4,0,0,0,0,0,0,9, +6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,4,0,0,0,5,0,0,0,0,14,0,0,0, +9,0,0,0,0,0,0,0,0,0,9,7,12,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,12,0,0,0,0,0,12,7,0,0,0,5,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,10,7,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,8,7,0,0,0,0,0,6,0,0,0,0,0,0,9,6,0,0,0,0,0,6,0,0,0,0,0, +0,0,0,0,0,9,0,0,0,0,7,0,6,0,0,0,0,0,0,0,0,0,0,0,5,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0, +0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,6,0,7,12,6,0,0,0,0,0,5,0,0,0,0,0,0,0,0, +0,7,0,0,8,6,0,0,0,0,10,7,0,0,0,0,0,0,0,6,0,0,0,0,0,6,12,0,0,0,0,0,0,0,0,6,0,0,0, +0,0,6,0,0,0,6,0,0,0,0,0,6,16,0,0,0,0,0,0,0,0,0,9,0,17,0,14,7,8,0,0,0,0,0,0,6,0, +0,0,0,0,0,0,0,0,0,11,0,0,6,8,7,0,6,0,0,0,0,0,0,0,0,0,0,12,6,0,0,0,0,0,0,0,0,0,0, +9,0,0,0,0,7,0,0,0,0,11,5,0,4,9,6,8,0,0,0,0,0,0,0,0,0,10,0,11,7,0,0,0,0,0,0,0,0, +9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,11, +0,0,0,12,0,0,0,0,0,10,5,0,4,0,0,0,0,0,7,10,6,11,6,0,0,0,0,0,0,0,0,0,0,0,0,17,0, +0,0,11,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,11,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,6,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,8,0,0,4,0,0,0,6,0,0,0, +0,0,0,8,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9,6,0,0,14,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,4,0,0,0,0,9,6,0,0,0,4,0,0,0,0,0,4,10,7,0,7,0,0,0,0,0,0,0,6,0,0,0,0,0,6,0,0,0, +0,0,0,0,0,0,6,0,0,0,6,0,6,0,0,0,0,10,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,18,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14,0,13,0,0,0,0,0,10,0,0,0,0,0,0,0,0,4, +0,0,0,6,0,0,0,0,0,4,8,0,0,0,11,7,0,0,0,4,0,0,0,0,0,7,0,0,8,5,0,0,16,0,0,0,13,6, +0,0,0,0,0,0,0,6,0,0,0,0,20,0,11,6,0,0,8,7,0,0,0,0,0,6,17,0,8,0,0,0,0,0,8,7,0,0, +9,0,0,0,0,0,0,0,0,0,0,0,0,0,14,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,0,0, +0,0,4,0,7,0,0,0,0,0,0,0,6,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,0,0,0,8, +0,8,0,0,0,0,0,0,0,11,0,8,0,0,0,0,0,0,0,0,0,0,0,8,6,0,0,0,0,0,0,0,0,0,6,0,0,9,0, +0,0,0,0,8,0,0,0,0,0,18,0,0,0,0,0,0,4,9,0,0,0,0,0,8,5,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,9,6,0,0,0,0,0,0,0,0,0,0,13,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,8,7,0,0,0,0,0,0,0,0, +0,4,0,0,0,0,0,0,14,0,0,0,0,7,0,6,0,0,8,0,20,7,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,8,0,0,0,14,0,0,0,0,0,0,0,8,0,0,7,0,6,0,0,0,7,0,0,0,0,0,0,0,0, +0,0,0,4,12,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,10,6,0, +5,16,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0, +0,0,0,5,8,4,0,0,0,0,0,0,0,4,0,0,0,7,0,0,0,0,0,0,0,5,0,0,0,0,0,0,0,0,0,0,12,7,0, +0,0,0,13,6,0,0,0,7,0,0,8,0,0,0,8,0,0,0,0,0,0,0,0,0,0,5,0,0,0,7,0,0,0,0,0,0,11,5, +0,6,0,0,8,5,0,7,0,0,0,0,0,0,0,7,0,0,0,0,8,6,0,0,0,0,0,0,9,0,0,0,0,0,0,0,0,4,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +14,0,10,7,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10,0,19,0,0,4,0,0,0,7, +0,0,11,5,11,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,16,0,10,5,18,0,0,7,9,6,0,5,0,0,0,0,0, +0,0,0,0,5,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,8,7,0,0,0,0,0,5,0,0,0,7,0,6,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,6,0,0,0,4,0,6,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,6,0,0,0,7,23,0,0,0,0,5,0,0,0,0,0,0,8,5,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,14,0,20,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0, +11,0,0,0,0,7,0,0,0,0,15,0,0,0,0,0,10,0,0,0,0,0,0,0,0,0,9,6,0,0,0,0,0,7,0,0,0,0, +0,4,0,0,0,0,10,0,0,0,0,0,9,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,10,0,11,6,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,5,0,0,11,0,0,0,0,7,0,0,0,0,0,0,8,7,0, +4,0,0,0,0,11,0,0,0,0,0,11,0,0,5,0,0,8,7,0,4,0,7,0,0,0,0,0,0,0,6,0,0,0,0,0,4,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,10,5,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,6,0,5,0,0,0,0,0,0,0, +0,0,4,11,5,10,7,0,7,0,0,9,6,9,0,0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,9,4,0,4, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,8,6,0,0,0,0,11,7,0,0,0,0,0,0,0,0,0,0,11,7,12,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,8,5,0,0,8,0,9,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,6,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,4,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0, +10,7,0,0,0,6,0,0,0,0,0,0,8,0,0,6,0,0,0,6,10,0,0,0,0,0,0,0,0,0,0,0,8,5,0,0,0,6,0, +0,0,6,0,0,0,0,9,5,8,5,8,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0, +0,8,7,10,0,0,0,0,0,0,0,9,6,0,0,0,0,0,0,0,0,0,0,11,7,0,0,0,0,0,5,0,0,0,6,0,7,0,0, +10,5,0,0,0,0,8,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,12,6,0,0,0,0,0,0,11,0,0,0,0,0,13,4, +0,0,0,4,0,0,0,0,0,5,8,0,0,0,12,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,12,7,14,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,7,0,0,0,0,0,0,9,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,4,0,0,0,0,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,5,0,0,15,6,10,0,0,0,8,6,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,9,6,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,14,6,10,0,0,0,0,0,0,0,0,6,0, +0,0,0,0,0,0,0,12,6,0,0,0,0,0,0,0,0,9,7,0,0,0,0,0,6,0,5,11,4,0,6,0,0,0,7,0,0,0,0, +0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,5,0,0,8,5,0,0,0,0,0,0,0,0,0,0, +0,0,10,0,0,0,0,0,9,6,9,4,0,0,0,4,0,0,0,0,0,0,0,0,11,0,0,0,0,0,0,0,0,0,0,0,8,5,0, +0,0,0,0,0,0,0,0,0,0,4,0,0,11,5,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,5,0,0,0,0,0,0, +0,0,0,7,12,0,0,0,0,6,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9,6,0, +4,9,6,0,4,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,6,0, +7,8,6,0,0,0,0,0,0,0,4,0,0,9,6,0,0,0,0,0,0,0,0,0,6,0,5,0,4,0,0,0,0,0,0,0,5,0,0,0, +0,0,5,0,0,0,7,12,7,0,0,0,0,0,0,18,4,0,0,0,0,0,0,0,0,0,0,0,0,11,0,0,0,0,6,0,0,0, +0,12,0,0,7,0,0,0,0,0,7,0,0,13,0,0,6,0,0,0,0,8,7,9,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,7,10,5,0,0,8,0,0,0,0,0,0,0,8,6,0,7,0,0,8,4,0,4,0,0,0,0,10,4,0,0,14,0, +0,0,0,0,0,0,0,0,0,0,0,5,0,0,0,0,17,0,0,0,0,0,0,6,0,0,0,0,8,6,0,0,10,5,0,0,0,0,8, +6,0,0,0,6,0,0,0,7,0,0,0,0,0,6,14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,0,12,0,0,0,0,6, +8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0, +0,0,0,6,0,0,0,0,0,0,0,0,0,0,12,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,4,24,0,0, +0,0,0,12,6,0,0,10,6,0,5,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,17,7,0,5,0,0,0, +0,0,0,0,0,0,0,0,0,0,6,11,5,9,0,8,7,0,0,0,0,0,0,0,0,10,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,10,7,0,0,0,0,0,0,0,7,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,11,5,8,7,0,0,0, +0,8,5,0,0,0,0,10,7,0,7,0,0,0,0,0,0,0,0,0,0,13,6,0,0,0,0,0,0,0,0,0,6,0,4,0,0,0,0, +0,6,12,0,8,7,0,0,0,0,0,0,0,0,0,0,16,0,10,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,22,0,0,0, +0,0,0,0,0,0,0,0,0,0,13,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9,6,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,22,0,0,6,0,0,21,0,0,0,22,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +6,0,0,0,0,9,6,0,0,0,0,0,0,0,0,0,6,0,0,0,5,0,0,0,0,0,7,8,0,0,0,0,6,14,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,8,6,0,0,0,0,0,0, +0,0,0,0,0,6,0,0,0,0,8,5,0,0,11,7,0,6,0,0,0,0,0,0,0,0,8,7,0,0,0,0,0,0,0,0,0,0,0, +6,0,0,0,5,0,0,0,0,0,0,0,0,0,4,0,0,8,7,0,0,0,0,8,5,11,7,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,4,0,0,0,0,8,5,0,0,10,0,0,4,13,7,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,5,0,0,13,6, +0,6,0,7,0,0,8,4,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,15,0,0,0,10,7,0,0,0,0,0, +7,0,0,0,0,0,0,0,4,0,0,0,0,0,6,0,0,0,0,19,0,0,0,0,6,0,0,0,0,0,4,0,0,0,0,0,6,0,5, +0,7,0,0,0,0,0,0,0,0,0,6,0,0,11,4,0,0,0,6,0,0,13,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,8,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,14,0,0,0,0,0,0,0,0,0,0,0,12,6,0,0,0,0, +0,7,0,0,0,0,0,0,11,7,0,0,0,0,0,6,0,0,10,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,5,11,6, +0,0,0,0,0,0,0,0,10,0,0,0,0,6,0,0,0,0,0,0,8,7,0,0,0,5,0,0,0,5,0,0,0,0,0,0,0,0,0, +0,0,0,8,7,0,0,0,0,9,6,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,10,7,0,0,0,0,10,0, +0,6,0,0,13,0,0,0,0,0,0,0,9,6,0,0,8,6,8,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,4,0, +0,9,7,0,0,0,0,0,0,11,0,0,0,10,7,0,0,0,0,0,0,0,0,9,6,0,0,12,4,0,4,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,6,0,0,0,0,21,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,6,0,5,0,0, +9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,4,0,0,0,0,0,6,0,0,0,6,0,0,0,0,0,0,0,0, +16,0,0,4,0,0,0,0,0,7,0,0,0,6,0,6,0,0,11,0,0,0,0,5,0,0,0,0,0,0,0,4,8,5,0,0,0,0,0, +0,14,0,0,0,0,6,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0, +0,0,8,0,0,0,0,0,0,0,0,6,0,0,0,4,0,0,0,0,0,0,14,0,0,0,0,0,0,0,0,4,0,0,0,4,0,0,0, +0,0,0,0,6,9,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9,6,14,7,0,0,9,7,0,0,11,0,0,0,0,0,10, +4,11,5,13,6,0,0,0,0,0,0,0,0,0,0,10,0,0,0,0,0,0,0,0,5,0,0,0,0,0,4,0,0,9,0,0,0,0, +0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,6,12,5,0,0,0,6,14,0,0,0,0,0,0,0,0,0,0,4,9,4, +0,0,0,0,0,5,0,0,0,0,0,0,0,4,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,0,0,5,0,0, +0,0,0,0,0,0,0,0,8,6,0,0,0,0,0,0,11,6,0,0,13,7,0,0,13,6,0,7,0,0,0,0,0,0,8,6,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,10,6,0,4,0,0,12,6,0,0,0,0,0,0,0,0,10,6, +0,0,0,6,0,0,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,10,0,0,0,0,0,0,0,0,0,0,0,0,6,0, +0,0,0,0,7,0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,8,6,0, +0,0,7,11,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0, +0,0,0,5,12,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,12,0,0, +0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,8,7,0,0,8,5,0,0,0,4,9,5,0,0,0,7,10,6,0,0, +0,0,0,0,9,7,0,0,8,5,8,0,8,4,0,0,13,0,0,0,0,0,0,0,0,0,0,0,0,5,0,5,0,0,0,0,0,0,0, +0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,6,0,0,0,4,0,0,0,0,0,0,0,0,0, +0,11,7,0,0,0,7,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,17,5,0,0,0,7,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,9,7,0,0,0,0,8,5,0,4,0,0,0,0,0,6,0,6,14, +6,0,0,0,0,9,6,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,6,0,0,0,0,14,7,9,7,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,16, +0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,14,0, +0,6,0,0,8,6,0,0,0,0,0,6,0,0,12,0,0,0,0,0,8,5,0,7,11,0,0,5,0,4,0,0,0,6,0,0,0,0,0, +0,0,0,0,0,0,0,9,6,0,4,0,6,0,0,0,0,0,0,0,0,0,0,0,0,11,6,0,0,0,0,0,0,10,5,0,0,0,0, +0,4,0,0,0,7,11,6,0,4,8,5,9,5,0,0,0,5,0,7,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,8,5,14,7,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,16,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,4, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,9,6,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,9,0,0,0,12,5,0,0,0,0,0,0,0,4,10,5,0,0,0,0,0,0,0,0,0,0,0,6,0, +0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,4,0,0,0,6,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,6,0,0,0,0,0,0,10,4,0,0,0,0,0,5,0,0,0,4, +0,0,0,0,0,0,9,0,0,0,0,0,0,0,0,6,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,8,0,10,7,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,10,7,0,0,0,0,0,0,0,0,15,0,0,0, +0,0,0,0,0,0,0,7,0,0,0,0,0,7,10,7,9,7,0,0,0,7,0,0,8,0,0,0,0,0,0,0,9,0,0,0,8,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,11,0,8,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,7,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,15,7,12,6,0,0,0,7,0,5,0,0,0,0,0,0,0,0,0,0,0,0,18,0,0,5,0,0,0,0, +0,0,0,6,9,5,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,9,7,0,0,14,0,0,0,11,7,0,0,0,0,0, +0,0,0,0,0,0,4,0,0,11,7,0,0,0,0,8,0,0,0,0,0,0,6,8,7,0,0,0,7,10,4,0,0,0,0,0,0,0,0, +0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,7,0,0,0,0,10,0,0,0,0,0,0, +6,0,6,0,0,0,0,0,4,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,11,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,7,0,0,10,7,0,0,0,0,9,7,0,0,0,0,0,0,13,7,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,9,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9,6,12,0, +0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,9,6,0,0,11,0,0, +0,0,0,14,4,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,23,7,0,0, +0,0,0,6,0,7,0,0,0,0,0,0,0,0,12,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,5,0,0,0,0,20, +7,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,8,0,0,0,0,0,0,0,0,0,11,5,0,0,0,0,0,0,0,0,0,0,10,4,0,0,0,5,8,5,10,4,0,0,0,0,0, +0,13,6,9,7,0,0,10,7,0,0,0,0,0,0,0,0,14,0,0,0,0,0,0,6,0,0,0,7,0,6,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,7,10,7,0,0, +0,0,0,0,0,0,0,0,12,4,0,0,0,0,8,7,0,0,0,0,0,7,0,6,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0, +0,0,0,0,6,0,6,9,6,0,0,12,5,0,0,8,6,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,6,0,0,0,0, +0,0,0,0,0,0,0,0,0,5,8,7,9,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,11, +4,0,0,0,0,0,0,8,0,0,0,10,7,0,4,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,6,0,0,8,0, +0,0,0,0,0,5,0,6,0,0,10,0,14,0,0,0,0,0,0,0,23,0,0,0,12,0,10,5,0,0,0,0,0,0,0,0,0, +5,0,0,0,0,8,0,0,0,0,6,8,0,0,0,0,0,0,0,0,0,22,0,8,0,0,0,0,6,0,0,0,0,0,0,0,5,0,0, +0,0,0,0,0,6,18,4,0,0,0,7,10,6,0,6,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,7,10,0,0,0,0,0,0,6,0,0,0,0,11,5,0,0,0,0,0,0,0,0, +15,0,8,6,0,0,13,7,0,0,0,0,0,7,0,0,0,0,0,7,0,0,0,0,8,7,0,0,0,0,0,0,0,0,0,0,9,5,9, +0,0,6,8,6,0,0,0,0,10,0,0,0,18,5,0,0,0,5,0,7,0,0,0,0,8,6,0,0,0,0,9,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,14,0,23,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,6,0,0,0,5,0, +0,0,0,0,0,0,0,0,0,0,0,12,0,0,0,8,5,0,0,0,0,0,0,0,0,9,0,0,0,0,4,0,0,0,0,0,0,0,0, +0,0,0,0,20,5,0,0,0,0,0,0,0,5,0,0,0,0,0,0,0,0,9,5,0,0,0,0,0,0,8,4,24,0,0,0,0,0,0, +0,0,0,0,0,0,0,9,7,0,0,0,0,10,5,0,0,8,5,0,0,0,0,0,0,0,0,12,7,0,6,0,0,10,6,0,0,0, +0,14,0,0,4,9,5,0,0,0,0,0,0,9,0,0,0,0,0,0,6,0,0,0,0,0,4,0,0,8,0,0,0,0,0,11,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,0,0,0,0,0,8,5,11,7,0,4,0,0,10,0,0,0,0, +0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,11,6,0,0,0,0,0,5,14,6,0,0,0,0,10,0,0, +0,13,4,0,0,0,0,0,0,0,0,0,0,0,6,0,0,10,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,19,7,12,0,10,6,0,0,0,0,0,0,10,0,0,0,0,0,10,0,9, +7,0,0,0,0,0,0,0,0,0,0,0,0,0,7,8,0,0,0,0,0,0,0,0,0,0,0,0,4,0,7,0,0,0,0,9,7,0,0,0, +0,0,0,0,0,0,0,0,0,24,0,11,7,0,7,0,0,0,0,0,0,8,6,0,0,0,0,0,0,8,7,0,0,0,0,0,5,0,0, +0,6,9,0,0,0,23,5,0,0,0,0,0,6,0,0,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,16,7,0,0,0, +0,0,0,0,0,0,0,0,0,0,6,0,0,18,4,0,0,11,7,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6, +0,0,0,6,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,9,0,0,0,11,0,0,0,23,0,0, +0,10,4,0,0,0,0,0,7,0,0,0,7,0,0,0,0,0,4,0,0,0,0,0,7,0,0,19,0,11,0,0,0,0,0,12,7,0, +0,0,0,0,0,0,0,0,0,0,0,11,0,0,0,0,5,0,0,0,0,0,5,0,0,0,0,0,5,0,0,0,0,0,0,0,6,0,0, +9,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,11,0,0,0,0,0,0,0,0,4,0,0,0,0,10,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,4,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,22,0,8,7,10,4,11,0,13,5,8,7,9,0,8,7,0,0,0,7,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0, +0,8,0,0,0,0,0,0,0,8,6,0,0,0,0,0,0,0,0,0,0,0,6,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,4,0,0,0,4,11,0,0,6,0,0,8,5,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,8,5,0,0, +20,0,0,0,0,0,0,0,0,0,11,0,0,0,0,5,0,0,0,0,0,0,0,0,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,7,0,0,14,0,0,0,9,0,13,7,0,0,0,0,0,6,0,7,0,0,8,6,10,6,0,0,8,6,0,0,0,6,0, +0,12,6,9,0,0,0,0,0,0,5,9,0,12,4,0,0,0,0,11,0,0,0,0,0,0,0,0,0,0,0,8,5,0,0,0,0,0, +0,0,4,8,0,0,6,8,0,0,0,0,0,0,0,0,0,13,6,0,7,0,0,0,0,0,6,8,7,8,6,0,0,0,7,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,12,0,0,0,0,0,18,0,11,4,0,0,0,5,0,0,0,0,0,0,0,0,0,0, +0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,14, +6,0,0,0,0,12,7,8,0,0,0,0,0,0,0,8,7,0,0,0,0,10,4,0,0,0,0,0,0,10,0,0,6,0,0,0,0,0, +0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,15,6,9,7,0,0,0,0,0,0,15,6,11,7,0,0,0,7,0,0,21,0,0, +0,0,0,0,5,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,17,6,0,0,10,5,0,5,0,0,0,0,0,0,0,0,0,7, +0,0,10,0,0,0,0,0,0,0,0,4,11,5,0,0,0,0,16,7,0,0,0,0,0,6,0,0,8,7,0,4,0,0,10,0,0,0, +0,0,0,0,0,0,0,0,12,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,8,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0, +0,0,0,10,4,0,0,0,0,0,0,0,0,0,6,0,5,0,0,9,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10,0, +0,7,0,0,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,6,10,7,0,0,0,0,0,0,0,0,8,4,0,0,10,0,0,0,0,4,0,6,0,6,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,7,17,0,0,0,0,0, +0,0,0,0,0,0,10,0,0,7,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +6,0,0,0,5,0,0,0,0,0,0,0,5,0,0,0,0,0,0,0,5,0,4,0,0,0,0,0,6,0,0,0,0,0,0,10,5,0,0, +0,5,0,0,0,0,9,0,19,7,0,0,0,0,0,7,0,0,0,0,10,6,0,0,0,6,0,5,0,0,0,0,0,0,0,0,0,6,8, +0,0,0,0,0,11,0,0,0,0,0,0,6,0,0,0,0,0,7,9,0,15,0,0,0,0,0,0,0,0,0,0,4,0,0,0,5,0,0, +0,0,0,0,0,6,0,0,0,0,0,0,0,4,0,0,0,0,9,0,0,0,0,0,0,0,0,6,0,7,0,0,0,0,0,0,0,6,0,0, +0,0,0,6,10,0,0,0,0,0,0,0,23,0,14,0,0,0,0,7,0,0,0,0,0,7,0,0,9,0,0,0,0,7,0,0,0,0, +0,6,0,0,14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,4,0,0,0, +0,0,0,0,0,9,5,0,0,0,0,0,4,0,0,0,0,9,5,0,0,0,0,22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,22,0,0,0,0,0,0,0,10,0,0,0,0,0,0,5,0,4,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,6,11,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,14,7,0,0,12,7,0,0,0, +0,0,0,0,0,0,4,0,0,0,0,0,6,0,0,0,0,8,6,10,0,0,0,0,0,0,0,0,0,10,7,8,5,0,0,0,0,0,0, +0,0,8,4,0,0,0,0,0,0,0,0,0,0,0,0,10,0,0,5,0,0,9,5,0,0,0,0,0,5,0,0,0,0,0,4,0,0,0, +0,0,0,0,0,0,0,12,4,11,0,0,0,9,0,11,7,0,0,0,0,0,0,10,6,0,0,0,6,0,0,0,0,15,5,0,0, +11,6,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,4,0,4,0,6,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,0,8,0,0,0,19,7,0,4,0,0,9,0,0,0,0,0,10,0, +0,6,0,0,13,0,12,6,0,0,0,0,0,0,0,0,10,7,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,5,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,13,7,10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,6,0,6,0,4,9,0,0,0,10,0,0,0,0,0,0,0, +0,5,0,0,0,0,0,0,10,0,23,6,0,0,0,6,8,0,0,0,0,0,0,0,0,0,17,7,0,0,0,0,11,6,22,5,0, +0,9,6,0,0,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,9,6,0,5,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,4,11,0,9,4,0,0, +0,7,0,7,0,0,0,0,0,0,12,4,0,0,0,0,0,0,0,0,0,0,0,0,11,4,0,0,0,0,0,0,0,0,0,0,0,0,0, +4,0,0,11,5,0,0,0,0,0,5,0,0,0,0,0,0,0,0,0,5,0,0,0,0,0,0,0,0,8,6,0,0,0,4,0,0,0,0, +0,0,0,0,0,7,0,0,0,4,0,0,10,4,0,0,0,0,0,0,0,7,0,7,0,0,0,6,0,0,0,0,8,6,0,6,0,6,0, +0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,8,7,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10,6,0,0,0,0,0,0,9,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,6,22,6,12,0,0,6,0,0,0,6,0,0,0,0,0,7,0,0,0,0,11,0,0,0, +9,7,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,6,0,0,0,6,0,6,0,0,8,7,0,0,0,4,9,7,19,0,0,0,0,0,0,0,0,0,9,6,10,6,0,6,0,0,0, +4,0,0,8,7,0,0,0,0,0,0,0,0,0,0,0,6,16,7,10,6,0,0,23,6,11,7,0,4,0,0,0,0,0,0,0,0,0, +5,0,0,0,0,10,7,0,0,0,0,0,7,0,0,0,0,0,0,15,0,10,0,0,0,14,6,0,0,0,0,0,0,0,0,0,0,0, +5,0,0,0,0,0,0,0,5,0,0,0,0,11,0,0,0,0,0,0,0,0,0,0,5,0,0,11,5,0,0,0,0,0,0,0,0,0,0, +0,4,0,0,0,0,0,6,0,0,10,0,0,0,0,7,0,0,0,0,0,0,10,6,0,0,0,0,8,4,0,0,0,7,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,7,12,5,0,0,0,0, +0,6,0,0,0,0,9,6,0,0,0,0,0,0,0,6,9,0,0,0,0,6,0,0,0,0,8,7,0,0,0,0,0,0,0,6,0,0,0,0, +0,0,0,0,0,0,10,5,0,0,0,0,0,0,8,6,0,0,0,0,0,6,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,8,5,0,0,0,0,0,7,0,7,0,4,0,0,10,0,0,0,0,0,0,0,0,0,0,0,11,0,0,0,0,5,0,0,0,0,13, +7,12,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,8,7,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,7,0,0,13,0,0,0,0,0,0,0,0,7,10,5,0,0,0,0,0,0,9,7,0,0,8,6,9, +5,0,0,0,0,0,6,12,0,0,0,0,0,0,0,18,6,0,0,0,0,0,0,0,0,19,7,0,4,0,0,0,0,9,5,0,5,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,7,0,0,0,0,0,0,14,0,0,0,23,7,8,7,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,6,22,0,0,7,0,0,0,0,0,0,0,0,9,7,8,4,0, +0,0,0,0,0,0,0,8,5,0,6,0,0,0,0,0,6,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,12,0,0,0, +8,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,12,5,0,0,0,0,0,0,0,0,0,0,8,6,0,0,11,7,0,0,0, +0,12,0,8,6,19,7,0,0,0,0,0,0,0,0,0,0,0,0,0,6,11,0,0,6,0,7,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,11,7,0,0,0,0,0,4,10,0,0,0,0,0,0,0,8,7,0,0,0,0,14,0,8,0,0,6,10,0,0, +0,0,0,0,0,12,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,0,0,0,0,0,0,0,0,6,0,0,0,0, +0,0,0,0,13,0,0,0,0,0,0,0,11,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0, +0,0,0,0,0,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,5,0,0,0,6,0,0,0,5,0,7,0,0,0, +0,0,6,0,0,21,7,0,0,9,6,0,0,0,6,0,0,13,7,0,0,0,5,0,0,0,0,0,4,0,6,0,0,0,0,0,0,0,0, +0,0,0,4,0,0,0,0,0,0,11,5,0,6,0,0,10,5,0,0,0,0,0,0,0,0,9,6,0,0,8,7,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,9,0,0,0,0,0,0,6,0,0,0,0,15,4,0,0,12,7,0,0,0,6, +0,7,0,0,8,0,9,5,0,4,0,0,0,6,0,6,0,0,23,4,0,0,0,0,0,0,0,0,0,0,0,0,10,7,0,4,0,0,8, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,6,0,0,0,0,0,0,0,0,0, +7,0,0,0,0,0,0,0,0,0,0,0,0,9,0,0,0,12,6,0,0,0,0,0,0,10,7,0,7,0,0,0,0,0,0,0,0,0,0, +9,0,0,0,0,0,8,0,0,0,0,4,0,0,0,0,0,0,0,0,0,4,11,5,0,0,0,6,0,6,0,0,0,0,0,0,0,6,0, +4,0,0,0,0,0,0,0,0,0,0,0,5,8,4,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,6,8,7,0,0,0,6,0,6,0, +0,0,0,0,0,0,0,0,5,0,0,0,0,0,5,0,0,0,0,11,0,0,0,0,0,0,0,10,5,9,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,23,7,0,0,0,0,0,7,0,0,10,6,18,0,0,0, +0,0,0,0,8,7,0,6,0,0,0,0,0,0,8,5,0,0,0,5,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0, +0,0,0,0,0,6,0,0,0,4,12,7,0,0,0,0,0,0,0,0,10,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9,6,0,0,0,0,0,0,13,5,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,4,0,0,0,0,0,0,0,0, +11,7,0,0,0,0,0,0,0,0,0,0,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,0,0, +0,0,0,0,6,0,0,0,4,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,10,0,11,0,0,0,0,0,0,0,0,0, +17,5,0,4,0,0,0,0,0,0,0,7,0,0,0,0,0,6,0,0,0,0,0,0,0,4,0,0,0,0,8,7,0,0,0,0,0,0,0, +0,13,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,6,0,0,0,0,0,6,0,0,0,0, +10,0,0,0,8,6,0,0,0,7,0,0,0,0,0,0,8,0,0,0,14,0,0,0,0,7,0,0,0,4,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,9,4,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14,0, +10,0,0,0,16,5,0,0,0,0,0,0,8,0,0,4,0,0,0,0,0,0,0,0,0,0,9,6,0,0,0,0,0,0,10,0,0,0, +0,0,0,0,0,5,0,0,0,0,12,5,0,7,0,0,0,0,0,6,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,7,0, +0,0,0,0,0,0,0,12,6,0,0,0,0,0,7,0,6,0,6,12,6,0,0,0,0,0,0,0,4,8,7,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0,6,0,6,0,0,0,0,0,0,0,0,10,6,8,0,0, +6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +16,0,8,0,0,0,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,23,5,0,0,0,7,0,6,0, +0,0,0,0,0,0,0,0,0,0,0,10,6,0,0,9,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,10,0,0,0,14,0,0,0,0,7,0,0,0,4,17,5,0,0,0,0,11,0,9,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,0,0,0,0,0,0,6,0,0,0,5,0,7,0,0,0,0,0,0,0,0,8,0,0,0, +12,6,0,0,0,0,0,0,13,0,0,0,0,7,9,0,0,0,0,0,0,0,0,0,0,5,0,0,0,7,10,7,12,0,0,0,9,0, +0,0,14,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,15,6,0,0,23,0,0,7,0,6,0,0,0,7,0,6, +0,0,0,0,0,0,0,6,0,6,9,0,0,0,0,0,0,0,8,7,0,0,0,0,0,0,0,0,8,7,9,4,0,0,10,0,0,0,10, +6,0,7,0,0,0,0,0,0,0,0,0,0,0,5,0,0,0,0,0,0,0,0,0,0,0,0,23,0,0,6,0,0,0,0,0,0,9,4, +0,0,10,7,0,0,0,0,0,0,0,0,0,0,0,0,9,7,0,0,9,6,0,0,0,0,8,6,0,0,0,0,0,0,0,0,12,0,0, +0,0,0,8,0,0,6,11,6,0,0,8,7,8,5,0,0,0,0,0,5,0,0,0,0,0,0,0,4,0,0,0,4,0,0,0,0,0,0, +10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10,0,0,0,0, +7,0,0,0,0,9,6,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,8,0,0,0,0,6,12,5,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,0,10,0,10, +7,0,0,8,0,0,0,0,4,0,0,0,6,0,0,0,6,0,0,0,6,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,5,0, +0,0,4,0,0,0,0,0,4,0,0,0,0,0,0,0,6,0,6,0,5,0,0,0,0,8,0,0,0,10,7,0,0,0,0,10,0,0,0, +0,0,13,0,0,0,0,0,0,0,0,0,0,0,12,0,0,0,0,0,0,0,19,7,0,4,12,0,8,0,0,0,0,6,0,0,0,0, +0,0,0,6,0,0,0,0,0,0,0,0,0,4,0,0,0,0,18,0,0,0,0,0,0,0,10,0,0,0,0,0,0,0,0,0,0,0,0, +0,14,0,0,4,0,0,0,6,0,0,0,6,0,0,0,7,0,0,0,0,0,0,10,4,0,0,9,7,0,0,11,0,0,0,0,0,0, +7,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,4,0,0,12,0,0,0, +0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,22,5,9,7,0,0,0,0,0,0,0,0,0, +0,0,6,0,0,9,6,0,5,0,0,0,0,0,0,10,5,0,0,8,6,0,6,10,5,0,0,0,6,0,0,0,6,0,0,20,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,6,0,0,0,0,17,4,0,7,0,6, +0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10, +0,0,0,0,0,0,0,0,0,0,0,8,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,7,0,0,0,0,0,0,0, +0,0,7,0,0,8,6,12,0,0,7,18,7,0,0,8,4,0,0,0,0,9,6,0,0,0,0,0,0,0,0,13,0,0,6,0,0,0, +0,0,0,0,0,0,0,10,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10,7,0,0, +0,0,0,0,0,0,0,5,0,0,0,0,0,0,0,0,0,0,8,5,0,0,0,0,0,0,0,0,12,0,0,0,8,0,0,0,0,0,0, +4,0,0,10,0,16,0,0,0,0,0,0,0,12,7,10,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,16,6,10,0,0,5,0,0,0,0,0,6,0,0,0,0, +0,7,0,0,0,7,0,0,0,0,0,0,0,0,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,5,8,7,0,7,0,0,0,0,0,0,0,0,8,0,0,6,0,0,0,6,0,0,0,4,0,0,0,0, +8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,7,0,0,8,0,0,0, +9,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,7,13,5,0,5,0,0,0,7,8,4,0,0,0,0,0,0,0, +0,12,0,0,0,0,0,0,0,0,0,0,0,8,6,0,6,0,0,11,0,0,0,0,0,0,0,0,6,0,0,0,0,0,4,0,0,0,0, +0,0,0,0,0,6,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,10,7,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9,0,11,6,0,0,10,6,0,0, +0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6, +0,0,0,6,0,0,0,7,0,0,9,0,8,7,11,0,0,0,0,0,0,0,0,0,12,0,0,0,0,0,9,6,10,5,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,10,7,0,0,0,0,0,0,11,0,9,6,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,5,0,6,0,0,0,0,0,0,0,0,0,5,0,0,0,0,0,0,0,0,0,0,15,5,12,5, +0,0,0,0,0,0,12,7,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,6,0,0,12,6,0, +0,0,0,24,4,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,7,0,0,0,0,0,0,0,0,0,0, +0,0,0,4,10,4,0,0,0,0,10,7,0,0,0,0,0,0,0,0,0,0,0,0,9,0,11,0,0,0,0,0,0,0,0,0,0,6, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5, +0,0,8,0,0,0,0,7,0,0,0,0,0,0,10,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,10,7,0,0,0,0,0, +0,0,0,0,0,14,7,0,0,0,0,0,0,10,0,0,0,0,0,0,0,0,4,0,0,0,6,0,0,0,0,0,6,0,0,0,6,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,11,6,0,0,0,0,0,0,0,4,0,0,0,4,0,0,0,0,0,7,20,7,11,4,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,19,7,9,6,0,0,12,7,0,0,0,0,0,0,10,0,12,0, +0,0,0,0,0,4,9,6,13,0,0,0,0,0,0,0,0,6,0,0,0,6,0,0,0,5,0,0,0,0,0,0,8,0,0,0,0,0,0, +0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,6,0,0,11,0,9,0,0,0,0,4,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,12,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,8,5,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,19,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0, +0,4,0,5,0,0,0,0,0,0,0,0,0,4,0,0,0,0,9,7,8,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0, +0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,6, +0,0,0,0,8,7,0,0,0,0,0,0,12,0,0,6,0,0,0,0,0,0,0,6,8,4,0,0,10,7,0,0,10,0,0,0,0,0, +0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,7,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,4,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9,7,0,0,0,0,0,0,0,5, +0,4,0,0,0,0,0,6,0,0,0,0,0,0,8,0,0,6,0,0,0,6,0,0,0,0,0,7,0,5,8,4,0,0,9,0,0,0,0,4, +0,0,0,0,0,0,0,0,0,5,0,0,15,6,8,6,0,0,0,6,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,6,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,0,9,6,0,0,0,0,0,0,0,7,0,0,0,4,0, +6,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,6,0,0,0,0,0,0,11,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,9,5,0,6,12,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,11,6,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,8,7,0,6,0,0,0,0,0,0,0,0,0,0,0,0,11,0,12,7,0,0,0,0, +0,0,0,0,0,5,0,5,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,11,4,0,0,0,0,0,0,0,0,0,0,10, +7,0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,7,8,7,9,6,0,0,0,0,0,0,0,0,8,6,0,0,0,0,0,5,12,0, +10,5,12,6,0,0,0,7,0,0,0,0,0,0,0,5,0,0,0,5,9,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7, +11,7,0,0,0,0,0,0,0,0,0,0,17,0,0,0,0,0,0,6,0,7,0,0,0,0,8,0,8,5,0,6,0,0,0,6,0,0,0, +0,0,0,0,6,0,6,0,6,9,0,0,5,17,0,0,0,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,0,0,0,0,7,0,0, +0,0,0,7,0,0,0,0,16,5,0,0,0,0,0,0,0,4,0,0,0,5,11,5,0,7,0,0,0,4,8,7,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,11,7,0,0,0,0,12,0,0,0, +0,0,12,0,0,0,0,0,0,0,0,4,10,4,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,6,0,0,0,0,0,0,0,4,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,11,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,20,5,0,0, +10,0,0,0,0,0,0,0,0,0,0,6,0,0,0,6,12,0,0,0,0,0,0,6,0,0,0,0,0,0,9,4,10,7,0,4,0,0, +0,0,0,0,10,6,0,0,0,0,8,4,0,7,8,6,0,6,8,0,10,0,0,0,0,0,13,5,0,6,0,0,0,0,0,0,22,4, +0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,6,0,0,0,6,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,6,10, +5,8,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10,0,0,0,0,0,10,4,0,0,10,7,0,0,0,0,0,5,0, +5,8,0,0,0,0,6,0,0,0,0,0,0,0,5,0,0,0,0,0,0,0,0,0,0,10,7,0,0,0,4,0,0,0,0,0,6,0,0, +0,0,0,0,0,0,8,7,0,0,0,0,0,0,0,0,0,0,0,0,11,0,0,0,0,7,0,0,0,6,0,0,0,0,0,0,0,0,0, +4,0,0,0,4,10,0,0,6,13,7,8,0,0,0,0,0,0,7,0,0,12,7,0,0,0,0,0,0,10,5,0,0,0,0,0,6,0, +0,0,0,0,0,0,0,0,0,13,7,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,6,0,0,0,0,0,0,0,0,8,6,0,6, +0,0,0,0,0,0,0,0,12,0,8,4,0,0,0,0,0,4,0,4,0,0,0,0,0,0,0,5,0,0,0,0,12,5,0,0,0,7,0, +0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,6,10,0,0,0,20,0,0,5,0,0,10, +7,11,7,0,0,0,0,0,0,0,0,0,0,17,0,9,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,0,10,7,0,4,0,6,0,0,24,0,0,5,0,0,0,0,8,0,0, +0,0,0,0,0,10,5,0,4,0,6,0,0,8,0,0,0,0,0,0,4,0,6,0,0,0,0,0,0,9,5,0,0,0,0,0,0,0,0, +0,0,0,6,0,0,0,0,9,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,4,0,7, +0,0,13,0,0,0,0,0,0,0,11,6,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0, +17,7,0,0,11,6,0,0,0,0,12,6,0,0,0,6,0,6,0,0,0,0,0,0,0,0,0,0,10,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,5,0,0,0,6,0,0,0,0,0,0,0,0,0,0,10,0,0,4,8,6,0,0,0, +0,0,0,9,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,9,5,0,7,18,0,0,0,0,0,0,0,0,0,0,0,0,0,8,6,0,0,0,0,0,0,0,0,8,0,0,0, +0,0,0,0,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0, +0,0,0,0,0,0,10,0,0,0,0,0,0,0,0,4,0,6,0,0,9,0,0,0,0,0,0,0,0,0,13,0,0,0,0,0,0,0,0, +0,0,0,8,7,10,0,8,5,0,0,0,0,0,0,0,0,9,0,0,0,10,0,0,0,0,6,0,7,0,4,0,0,0,0,0,0,0,0, +8,0,0,0,0,0,8,4,0,0,0,0,0,5,0,0,10,0,12,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +4,0,0,0,0,0,0,12,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,18,6,11,0,0, +7,0,0,0,0,0,6,10,5,0,0,0,0,0,0,0,0,0,5,0,0,9,5,12,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,6,0,0,0,0,13,6,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,0,0,0,0,0,0,0,0, +0,0,0,8,4,0,6,12,0,0,0,0,0,0,0,0,0,0,0,0,6,0,6,0,0,0,0,0,5,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,11,4,0,0,0,6,14,0,11,0,9,6,0,0,0,0,0,0,22,0,12,0,8,6,0,0,0,0,0,0,0,6,0, +0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,11,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0, +10,7,0,0,0,0,0,0,0,0,9,0,0,0,0,4,0,0,0,0,0,0,0,0,0,5,11,0,0,0,0,0,0,0,8,6,0,0,9, +7,0,0,12,4,0,0,0,0,0,0,12,6,0,6,0,7,0,0,8,5,0,0,0,0}; diff --git a/packages/runtime/vendor/brotli/c/enc/encode.c b/packages/runtime/vendor/brotli/c/enc/encode.c new file mode 100644 index 000000000..ba21aa2ec --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/encode.c @@ -0,0 +1,2021 @@ +/* Copyright 2013 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* Implementation of Brotli compressor. */ + +#include + +#include "../common/constants.h" +#include "../common/context.h" +#include "../common/platform.h" +#include +#include "../common/version.h" +#include "backward_references_hq.h" +#include "backward_references.h" +#include "bit_cost.h" +#include "brotli_bit_stream.h" +#include "command.h" +#include "compound_dictionary.h" +#include "compress_fragment_two_pass.h" +#include "compress_fragment.h" +#include "dictionary_hash.h" +#include "encoder_dict.h" +#include "fast_log.h" +#include "hash.h" +#include "histogram.h" +#include "memory.h" +#include "metablock.h" +#include "params.h" +#include "quality.h" +#include "ringbuffer.h" +#include "state.h" +#include "static_init.h" +#include "utf8_util.h" +#include "write_bits.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +#define COPY_ARRAY(dst, src) memcpy(dst, src, sizeof(src)); + +static size_t InputBlockSize(BrotliEncoderState* s) { + return (size_t)1 << s->params.lgblock; +} + +static uint64_t UnprocessedInputSize(BrotliEncoderState* s) { + return s->input_pos_ - s->last_processed_pos_; +} + +static size_t RemainingInputBlockSize(BrotliEncoderState* s) { + const uint64_t delta = UnprocessedInputSize(s); + size_t block_size = InputBlockSize(s); + if (delta >= block_size) return 0; + return block_size - (size_t)delta; +} + +BROTLI_BOOL BrotliEncoderSetParameter( + BrotliEncoderState* state, BrotliEncoderParameter p, uint32_t value) { + /* Changing parameters on the fly is not implemented yet. */ + if (state->is_initialized_) return BROTLI_FALSE; + /* TODO(eustas): Validate/clamp parameters here. */ + switch (p) { + case BROTLI_PARAM_MODE: + state->params.mode = (BrotliEncoderMode)value; + return BROTLI_TRUE; + + case BROTLI_PARAM_QUALITY: + state->params.quality = (int)value; + return BROTLI_TRUE; + + case BROTLI_PARAM_LGWIN: + state->params.lgwin = (int)value; + return BROTLI_TRUE; + + case BROTLI_PARAM_LGBLOCK: + state->params.lgblock = (int)value; + return BROTLI_TRUE; + + case BROTLI_PARAM_DISABLE_LITERAL_CONTEXT_MODELING: + if ((value != 0) && (value != 1)) return BROTLI_FALSE; + state->params.disable_literal_context_modeling = TO_BROTLI_BOOL(!!value); + return BROTLI_TRUE; + + case BROTLI_PARAM_SIZE_HINT: + state->params.size_hint = value; + return BROTLI_TRUE; + + case BROTLI_PARAM_LARGE_WINDOW: + state->params.large_window = TO_BROTLI_BOOL(!!value); + return BROTLI_TRUE; + + case BROTLI_PARAM_NPOSTFIX: + state->params.dist.distance_postfix_bits = value; + return BROTLI_TRUE; + + case BROTLI_PARAM_NDIRECT: + state->params.dist.num_direct_distance_codes = value; + return BROTLI_TRUE; + + case BROTLI_PARAM_STREAM_OFFSET: + if (value > (1u << 30)) return BROTLI_FALSE; + state->params.stream_offset = value; + return BROTLI_TRUE; + + default: return BROTLI_FALSE; + } +} + +/* Wraps 64-bit input position to 32-bit ring-buffer position preserving + "not-a-first-lap" feature. */ +static uint32_t WrapPosition(uint64_t position) { + uint32_t result = (uint32_t)position; + uint64_t gb = position >> 30; + if (gb > 2) { + /* Wrap every 2GiB; The first 3GB are continuous. */ + result = (result & ((1u << 30) - 1)) | ((uint32_t)((gb - 1) & 1) + 1) << 30; + } + return result; +} + +static uint8_t* GetBrotliStorage(BrotliEncoderState* s, size_t size) { + MemoryManager* m = &s->memory_manager_; + if (s->storage_size_ < size) { + BROTLI_FREE(m, s->storage_); + s->storage_ = BROTLI_ALLOC(m, uint8_t, size); + if (BROTLI_IS_OOM(m) || BROTLI_IS_NULL(s->storage_)) return NULL; + s->storage_size_ = size; + } + return s->storage_; +} + +static size_t HashTableSize(size_t max_table_size, size_t input_size) { + size_t htsize = 256; + while (htsize < max_table_size && htsize < input_size) { + htsize <<= 1; + } + return htsize; +} + +static int* GetHashTable(BrotliEncoderState* s, int quality, + size_t input_size, size_t* table_size) { + /* Use smaller hash table when input.size() is smaller, since we + fill the table, incurring O(hash table size) overhead for + compression, and if the input is short, we won't need that + many hash table entries anyway. */ + MemoryManager* m = &s->memory_manager_; + const size_t max_table_size = MaxHashTableSize(quality); + size_t htsize = HashTableSize(max_table_size, input_size); + int* table; + BROTLI_DCHECK(max_table_size >= 256); + if (quality == FAST_ONE_PASS_COMPRESSION_QUALITY) { + /* Only odd shifts are supported by fast-one-pass. */ + if ((htsize & 0xAAAAA) == 0) { + htsize <<= 1; + } + } + + if (htsize <= sizeof(s->small_table_) / sizeof(s->small_table_[0])) { + table = s->small_table_; + } else { + if (htsize > s->large_table_size_) { + s->large_table_size_ = htsize; + BROTLI_FREE(m, s->large_table_); + s->large_table_ = BROTLI_ALLOC(m, int, htsize); + if (BROTLI_IS_OOM(m) || BROTLI_IS_NULL(s->large_table_)) return 0; + } + table = s->large_table_; + } + + *table_size = htsize; + memset(table, 0, htsize * sizeof(*table)); + return table; +} + +static void EncodeWindowBits(int lgwin, BROTLI_BOOL large_window, + uint16_t* last_bytes, uint8_t* last_bytes_bits) { + if (large_window) { + *last_bytes = (uint16_t)(((lgwin & 0x3F) << 8) | 0x11); + *last_bytes_bits = 14; + } else { + if (lgwin == 16) { + *last_bytes = 0; + *last_bytes_bits = 1; + } else if (lgwin == 17) { + *last_bytes = 1; + *last_bytes_bits = 7; + } else if (lgwin > 17) { + *last_bytes = (uint16_t)(((lgwin - 17) << 1) | 0x01); + *last_bytes_bits = 4; + } else { + *last_bytes = (uint16_t)(((lgwin - 8) << 4) | 0x01); + *last_bytes_bits = 7; + } + } +} + +/* TODO(eustas): move to compress_fragment.c? */ +/* Initializes the command and distance prefix codes for the first block. */ +static void InitCommandPrefixCodes(BrotliOnePassArena* s) { + static const BROTLI_MODEL("small") uint8_t kDefaultCommandDepths[128] = { + 0, 4, 4, 5, 6, 6, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, + 0, 0, 0, 4, 4, 4, 4, 4, 5, 5, 6, 6, 6, 6, 7, 7, + 7, 7, 10, 10, 10, 10, 10, 10, 0, 4, 4, 5, 5, 5, 6, 6, + 7, 8, 8, 9, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, + 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 6, 6, 6, 6, 6, 6, 5, 5, 5, 5, 5, 5, 4, 4, 4, 4, + 4, 4, 4, 5, 5, 5, 5, 5, 5, 6, 6, 7, 7, 7, 8, 10, + 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, + }; + static const BROTLI_MODEL("small") uint16_t kDefaultCommandBits[128] = { + 0, 0, 8, 9, 3, 35, 7, 71, + 39, 103, 23, 47, 175, 111, 239, 31, + 0, 0, 0, 4, 12, 2, 10, 6, + 13, 29, 11, 43, 27, 59, 87, 55, + 15, 79, 319, 831, 191, 703, 447, 959, + 0, 14, 1, 25, 5, 21, 19, 51, + 119, 159, 95, 223, 479, 991, 63, 575, + 127, 639, 383, 895, 255, 767, 511, 1023, + 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 27, 59, 7, 39, 23, 55, 30, 1, 17, 9, 25, 5, 0, 8, 4, 12, + 2, 10, 6, 21, 13, 29, 3, 19, 11, 15, 47, 31, 95, 63, 127, 255, + 767, 2815, 1791, 3839, 511, 2559, 1535, 3583, 1023, 3071, 2047, 4095, + }; + static const BROTLI_MODEL("small") uint8_t kDefaultCommandCode[] = { + 0xff, 0x77, 0xd5, 0xbf, 0xe7, 0xde, 0xea, 0x9e, 0x51, 0x5d, 0xde, 0xc6, + 0x70, 0x57, 0xbc, 0x58, 0x58, 0x58, 0xd8, 0xd8, 0x58, 0xd5, 0xcb, 0x8c, + 0xea, 0xe0, 0xc3, 0x87, 0x1f, 0x83, 0xc1, 0x60, 0x1c, 0x67, 0xb2, 0xaa, + 0x06, 0x83, 0xc1, 0x60, 0x30, 0x18, 0xcc, 0xa1, 0xce, 0x88, 0x54, 0x94, + 0x46, 0xe1, 0xb0, 0xd0, 0x4e, 0xb2, 0xf7, 0x04, 0x00, + }; + static const size_t kDefaultCommandCodeNumBits = 448; + COPY_ARRAY(s->cmd_depth, kDefaultCommandDepths); + COPY_ARRAY(s->cmd_bits, kDefaultCommandBits); + + /* Initialize the pre-compressed form of the command and distance prefix + codes. */ + COPY_ARRAY(s->cmd_code, kDefaultCommandCode); + s->cmd_code_numbits = kDefaultCommandCodeNumBits; +} + +/* TODO(eustas): avoid FP calculations. */ +static double EstimateEntropy(const uint32_t* population, size_t size) { + size_t total = 0; + double result = 0; + for (size_t i = 0; i < size; ++i) { + uint32_t p = population[i]; + total += p; + result += (double)p * FastLog2(p); + } + result = (double)total * FastLog2(total) - result; + return result; +} + +/* Decide about the context map based on the ability of the prediction + ability of the previous byte UTF8-prefix on the next byte. The + prediction ability is calculated as Shannon entropy. Here we need + Shannon entropy instead of 'BrotliBitsEntropy' since the prefix will be + encoded with the remaining 6 bits of the following byte, and + BrotliBitsEntropy will assume that symbol to be stored alone using Huffman + coding. */ +static void ChooseContextMap(int quality, + uint32_t* bigram_histo, + size_t* num_literal_contexts, + const uint32_t** literal_context_map) { + static const BROTLI_MODEL("small") + uint32_t kStaticContextMapContinuation[64] = { + 1, 1, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + }; + static const BROTLI_MODEL("small") + uint32_t kStaticContextMapSimpleUTF8[64] = { + 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + }; + + uint32_t monogram_histo[3] = { 0 }; + uint32_t two_prefix_histo[6] = { 0 }; + size_t total; + size_t i; + double entropy[4]; + for (i = 0; i < 9; ++i) { + monogram_histo[i % 3] += bigram_histo[i]; + two_prefix_histo[i % 6] += bigram_histo[i]; + } + entropy[1] = EstimateEntropy(monogram_histo, 3); + entropy[2] = (EstimateEntropy(two_prefix_histo, 3) + + EstimateEntropy(two_prefix_histo + 3, 3)); + entropy[3] = 0; + for (i = 0; i < 3; ++i) { + entropy[3] += EstimateEntropy(bigram_histo + 3 * i, 3); + } + + total = monogram_histo[0] + monogram_histo[1] + monogram_histo[2]; + BROTLI_DCHECK(total != 0); + entropy[0] = 1.0 / (double)total; + entropy[1] *= entropy[0]; + entropy[2] *= entropy[0]; + entropy[3] *= entropy[0]; + + if (quality < MIN_QUALITY_FOR_HQ_CONTEXT_MODELING) { + /* 3 context models is a bit slower, don't use it at lower qualities. */ + entropy[3] = entropy[1] * 10; + } + /* If expected savings by symbol are less than 0.2 bits, skip the + context modeling -- in exchange for faster decoding speed. */ + if (entropy[1] - entropy[2] < 0.2 && + entropy[1] - entropy[3] < 0.2) { + *num_literal_contexts = 1; + } else if (entropy[2] - entropy[3] < 0.02) { + *num_literal_contexts = 2; + *literal_context_map = kStaticContextMapSimpleUTF8; + } else { + *num_literal_contexts = 3; + *literal_context_map = kStaticContextMapContinuation; + } +} + +/* Decide if we want to use a more complex static context map containing 13 + context values, based on the entropy reduction of histograms over the + first 5 bits of literals. */ +static BROTLI_BOOL ShouldUseComplexStaticContextMap(const uint8_t* input, + size_t start_pos, size_t length, size_t mask, int quality, size_t size_hint, + size_t* num_literal_contexts, const uint32_t** literal_context_map, + uint32_t* arena) { + static const BROTLI_MODEL("small") + uint32_t kStaticContextMapComplexUTF8[64] = { + 11, 11, 12, 12, /* 0 special */ + 0, 0, 0, 0, /* 4 lf */ + 1, 1, 9, 9, /* 8 space */ + 2, 2, 2, 2, /* !, first after space/lf and after something else. */ + 1, 1, 1, 1, /* " */ + 8, 3, 3, 3, /* % */ + 1, 1, 1, 1, /* ({[ */ + 2, 2, 2, 2, /* }]) */ + 8, 4, 4, 4, /* :; */ + 8, 7, 4, 4, /* . */ + 8, 0, 0, 0, /* > */ + 3, 3, 3, 3, /* [0..9] */ + 5, 5, 10, 5, /* [A-Z] */ + 5, 5, 10, 5, + 6, 6, 6, 6, /* [a-z] */ + 6, 6, 6, 6, + }; + BROTLI_UNUSED(quality); + /* Try the more complex static context map only for long data. */ + if (size_hint < (1 << 20)) { + return BROTLI_FALSE; + } else { + const size_t end_pos = start_pos + length; + /* To make entropy calculations faster, we collect histograms + over the 5 most significant bits of literals. One histogram + without context and 13 additional histograms for each context value. */ + uint32_t* BROTLI_RESTRICT const combined_histo = arena; + uint32_t* BROTLI_RESTRICT const context_histo = arena + 32; + uint32_t total = 0; + double entropy[3]; + size_t i; + ContextLut utf8_lut = BROTLI_CONTEXT_LUT(CONTEXT_UTF8); + memset(arena, 0, sizeof(arena[0]) * 32 * (BROTLI_MAX_STATIC_CONTEXTS + 1)); + for (; start_pos + 64 <= end_pos; start_pos += 4096) { + const size_t stride_end_pos = start_pos + 64; + uint8_t prev2 = input[start_pos & mask]; + uint8_t prev1 = input[(start_pos + 1) & mask]; + size_t pos; + /* To make the analysis of the data faster we only examine 64 byte long + strides at every 4kB intervals. */ + for (pos = start_pos + 2; pos < stride_end_pos; ++pos) { + const uint8_t literal = input[pos & mask]; + const uint8_t context = (uint8_t)kStaticContextMapComplexUTF8[ + BROTLI_CONTEXT(prev1, prev2, utf8_lut)]; + ++total; + ++combined_histo[literal >> 3]; + ++context_histo[(context << 5) + (literal >> 3)]; + prev2 = prev1; + prev1 = literal; + } + } + entropy[1] = EstimateEntropy(combined_histo, 32); + entropy[2] = 0; + for (i = 0; i < BROTLI_MAX_STATIC_CONTEXTS; ++i) { + entropy[2] += EstimateEntropy(context_histo + (i << 5), 32); + } + entropy[0] = 1.0 / (double)total; + entropy[1] *= entropy[0]; + entropy[2] *= entropy[0]; + /* The triggering heuristics below were tuned by compressing the individual + files of the silesia corpus. If we skip this kind of context modeling + for not very well compressible input (i.e. entropy using context modeling + is 60% of maximal entropy) or if expected savings by symbol are less + than 0.2 bits, then in every case when it triggers, the final compression + ratio is improved. Note however that this heuristics might be too strict + for some cases and could be tuned further. */ + if (entropy[2] > 3.0 || entropy[1] - entropy[2] < 0.2) { + return BROTLI_FALSE; + } else { + *num_literal_contexts = BROTLI_MAX_STATIC_CONTEXTS; + *literal_context_map = kStaticContextMapComplexUTF8; + return BROTLI_TRUE; + } + } +} + +static void DecideOverLiteralContextModeling(const uint8_t* input, + size_t start_pos, size_t length, size_t mask, int quality, size_t size_hint, + size_t* num_literal_contexts, const uint32_t** literal_context_map, + uint32_t* arena) { + if (quality < MIN_QUALITY_FOR_CONTEXT_MODELING || length < 64) { + return; + } else if (ShouldUseComplexStaticContextMap( + input, start_pos, length, mask, quality, size_hint, + num_literal_contexts, literal_context_map, arena)) { + /* Context map was already set, nothing else to do. */ + } else { + /* Gather bi-gram data of the UTF8 byte prefixes. To make the analysis of + UTF8 data faster we only examine 64 byte long strides at every 4kB + intervals. */ + const size_t end_pos = start_pos + length; + uint32_t* BROTLI_RESTRICT const bigram_prefix_histo = arena; + memset(bigram_prefix_histo, 0, sizeof(arena[0]) * 9); + for (; start_pos + 64 <= end_pos; start_pos += 4096) { + static const int lut[4] = { 0, 0, 1, 2 }; + const size_t stride_end_pos = start_pos + 64; + int prev = lut[input[start_pos & mask] >> 6] * 3; + size_t pos; + for (pos = start_pos + 1; pos < stride_end_pos; ++pos) { + const uint8_t literal = input[pos & mask]; + ++bigram_prefix_histo[prev + lut[literal >> 6]]; + prev = lut[literal >> 6] * 3; + } + } + ChooseContextMap(quality, &bigram_prefix_histo[0], num_literal_contexts, + literal_context_map); + } +} + +static BROTLI_BOOL ShouldCompress( + const uint8_t* data, const size_t mask, const uint64_t last_flush_pos, + const size_t bytes, const size_t num_literals, const size_t num_commands) { + /* TODO(eustas): find more precise minimal block overhead. */ + if (bytes <= 2) return BROTLI_FALSE; + if (num_commands < (bytes >> 8) + 2) { + if ((double)num_literals > 0.99 * (double)bytes) { + uint32_t literal_histo[256] = { 0 }; + static const uint32_t kSampleRate = 13; + static const double kInvSampleRate = 1.0 / 13.0; + static const double kMinEntropy = 7.92; + const double bit_cost_threshold = + (double)bytes * kMinEntropy * kInvSampleRate; + size_t t = (bytes + kSampleRate - 1) / kSampleRate; + uint32_t pos = (uint32_t)last_flush_pos; + size_t i; + for (i = 0; i < t; i++) { + ++literal_histo[data[pos & mask]]; + pos += kSampleRate; + } + if (BrotliBitsEntropy(literal_histo, 256) > bit_cost_threshold) { + return BROTLI_FALSE; + } + } + } + return BROTLI_TRUE; +} + +/* Chooses the literal context mode for a metablock */ +static ContextType ChooseContextMode(const BrotliEncoderParams* params, + const uint8_t* data, const size_t pos, const size_t mask, + const size_t length) { + /* We only do the computation for the option of something else than + CONTEXT_UTF8 for the highest qualities */ + if (params->quality >= MIN_QUALITY_FOR_HQ_BLOCK_SPLITTING && + !BrotliIsMostlyUTF8(data, pos, mask, length, kMinUTF8Ratio)) { + return CONTEXT_SIGNED; + } + return CONTEXT_UTF8; +} + +static void WriteMetaBlockInternal(MemoryManager* m, + const uint8_t* data, + const size_t mask, + const uint64_t last_flush_pos, + const size_t bytes, + const BROTLI_BOOL is_last, + ContextType literal_context_mode, + const BrotliEncoderParams* params, + const uint8_t prev_byte, + const uint8_t prev_byte2, + const size_t num_literals, + const size_t num_commands, + Command* commands, + const int* saved_dist_cache, + int* dist_cache, + size_t* storage_ix, + uint8_t* storage) { + const uint32_t wrapped_last_flush_pos = WrapPosition(last_flush_pos); + uint16_t last_bytes; + uint8_t last_bytes_bits; + ContextLut literal_context_lut = BROTLI_CONTEXT_LUT(literal_context_mode); + BrotliEncoderParams block_params = *params; + + if (bytes == 0) { + /* Write the ISLAST and ISEMPTY bits. */ + BrotliWriteBits(2, 3, storage_ix, storage); + *storage_ix = (*storage_ix + 7u) & ~7u; + return; + } + + if (!ShouldCompress(data, mask, last_flush_pos, bytes, + num_literals, num_commands)) { + /* Restore the distance cache, as its last update by + CreateBackwardReferences is now unused. */ + memcpy(dist_cache, saved_dist_cache, 4 * sizeof(dist_cache[0])); + BrotliStoreUncompressedMetaBlock(is_last, data, + wrapped_last_flush_pos, mask, bytes, + storage_ix, storage); + return; + } + + BROTLI_DCHECK(*storage_ix <= 14); + last_bytes = (uint16_t)((storage[1] << 8) | storage[0]); + last_bytes_bits = (uint8_t)(*storage_ix); + if (params->quality <= MAX_QUALITY_FOR_STATIC_ENTROPY_CODES) { + BrotliStoreMetaBlockFast(m, data, wrapped_last_flush_pos, + bytes, mask, is_last, params, + commands, num_commands, + storage_ix, storage); + if (BROTLI_IS_OOM(m)) return; + } else if (params->quality < MIN_QUALITY_FOR_BLOCK_SPLIT) { + BrotliStoreMetaBlockTrivial(m, data, wrapped_last_flush_pos, + bytes, mask, is_last, params, + commands, num_commands, + storage_ix, storage); + if (BROTLI_IS_OOM(m)) return; + } else { + MetaBlockSplit mb; + InitMetaBlockSplit(&mb); + if (params->quality < MIN_QUALITY_FOR_HQ_BLOCK_SPLITTING) { + size_t num_literal_contexts = 1; + const uint32_t* literal_context_map = NULL; + if (!params->disable_literal_context_modeling) { + /* TODO(eustas): pull to higher level and reuse. */ + uint32_t* arena = + BROTLI_ALLOC(m, uint32_t, 32 * (BROTLI_MAX_STATIC_CONTEXTS + 1)); + if (BROTLI_IS_OOM(m) || BROTLI_IS_NULL(arena)) return; + DecideOverLiteralContextModeling( + data, wrapped_last_flush_pos, bytes, mask, params->quality, + params->size_hint, &num_literal_contexts, + &literal_context_map, arena); + BROTLI_FREE(m, arena); + } + BrotliBuildMetaBlockGreedy(m, data, wrapped_last_flush_pos, mask, + prev_byte, prev_byte2, literal_context_lut, num_literal_contexts, + literal_context_map, commands, num_commands, &mb); + if (BROTLI_IS_OOM(m)) return; + } else { + BrotliBuildMetaBlock(m, data, wrapped_last_flush_pos, mask, &block_params, + prev_byte, prev_byte2, + commands, num_commands, + literal_context_mode, + &mb); + if (BROTLI_IS_OOM(m)) return; + } + if (params->quality >= MIN_QUALITY_FOR_OPTIMIZE_HISTOGRAMS) { + /* The number of distance symbols effectively used for distance + histograms. It might be less than distance alphabet size + for "Large Window Brotli" (32-bit). */ + BrotliOptimizeHistograms(block_params.dist.alphabet_size_limit, &mb); + } + BrotliStoreMetaBlock(m, data, wrapped_last_flush_pos, bytes, mask, + prev_byte, prev_byte2, + is_last, + &block_params, + literal_context_mode, + commands, num_commands, + &mb, + storage_ix, storage); + if (BROTLI_IS_OOM(m)) return; + DestroyMetaBlockSplit(m, &mb); + } + if (bytes + 4 < (*storage_ix >> 3)) { + /* Restore the distance cache and last byte. */ + memcpy(dist_cache, saved_dist_cache, 4 * sizeof(dist_cache[0])); + storage[0] = (uint8_t)last_bytes; + storage[1] = (uint8_t)(last_bytes >> 8); + *storage_ix = last_bytes_bits; + BrotliStoreUncompressedMetaBlock(is_last, data, + wrapped_last_flush_pos, mask, + bytes, storage_ix, storage); + } +} + +static void ChooseDistanceParams(BrotliEncoderParams* params) { + uint32_t distance_postfix_bits = 0; + uint32_t num_direct_distance_codes = 0; + + if (params->quality >= MIN_QUALITY_FOR_NONZERO_DISTANCE_PARAMS) { + uint32_t ndirect_msb; + if (params->mode == BROTLI_MODE_FONT) { + distance_postfix_bits = 1; + num_direct_distance_codes = 12; + } else { + distance_postfix_bits = params->dist.distance_postfix_bits; + num_direct_distance_codes = params->dist.num_direct_distance_codes; + } + ndirect_msb = (num_direct_distance_codes >> distance_postfix_bits) & 0x0F; + if (distance_postfix_bits > BROTLI_MAX_NPOSTFIX || + num_direct_distance_codes > BROTLI_MAX_NDIRECT || + (ndirect_msb << distance_postfix_bits) != num_direct_distance_codes) { + distance_postfix_bits = 0; + num_direct_distance_codes = 0; + } + } + + BrotliInitDistanceParams(¶ms->dist, distance_postfix_bits, + num_direct_distance_codes, params->large_window); +} + +static BROTLI_BOOL EnsureInitialized(BrotliEncoderState* s) { + MemoryManager* m = &s->memory_manager_; + if (BROTLI_IS_OOM(m)) return BROTLI_FALSE; + if (s->is_initialized_) return BROTLI_TRUE; + + s->last_bytes_bits_ = 0; + s->last_bytes_ = 0; + s->flint_ = BROTLI_FLINT_DONE; + s->remaining_metadata_bytes_ = BROTLI_UINT32_MAX; + + SanitizeParams(&s->params); + s->params.lgblock = ComputeLgBlock(&s->params); + ChooseDistanceParams(&s->params); + + if (s->params.stream_offset != 0) { + s->flint_ = BROTLI_FLINT_NEEDS_2_BYTES; + /* Poison the distance cache. -16 +- 3 is still less than zero (invalid). */ + s->dist_cache_[0] = -16; + s->dist_cache_[1] = -16; + s->dist_cache_[2] = -16; + s->dist_cache_[3] = -16; + memcpy(s->saved_dist_cache_, s->dist_cache_, sizeof(s->saved_dist_cache_)); + } + + RingBufferSetup(&s->params, &s->ringbuffer_); + + /* Initialize last byte with stream header. */ + { + int lgwin = s->params.lgwin; + if (s->params.quality == FAST_ONE_PASS_COMPRESSION_QUALITY || + s->params.quality == FAST_TWO_PASS_COMPRESSION_QUALITY) { + lgwin = BROTLI_MAX(int, lgwin, 18); + } + if (s->params.stream_offset == 0) { + EncodeWindowBits(lgwin, s->params.large_window, + &s->last_bytes_, &s->last_bytes_bits_); + } else { + /* Bigger values have the same effect, but could cause overflows. */ + s->params.stream_offset = BROTLI_MIN(size_t, + s->params.stream_offset, BROTLI_MAX_BACKWARD_LIMIT(lgwin)); + } + } + + if (s->params.quality == FAST_ONE_PASS_COMPRESSION_QUALITY) { + s->one_pass_arena_ = BROTLI_ALLOC(m, BrotliOnePassArena, 1); + if (BROTLI_IS_OOM(m)) return BROTLI_FALSE; + InitCommandPrefixCodes(s->one_pass_arena_); + } else if (s->params.quality == FAST_TWO_PASS_COMPRESSION_QUALITY) { + s->two_pass_arena_ = BROTLI_ALLOC(m, BrotliTwoPassArena, 1); + if (BROTLI_IS_OOM(m)) return BROTLI_FALSE; + } + + s->is_initialized_ = BROTLI_TRUE; + return BROTLI_TRUE; +} + +static void BrotliEncoderInitParams(BrotliEncoderParams* params) { + params->mode = BROTLI_DEFAULT_MODE; + params->large_window = BROTLI_FALSE; + params->quality = BROTLI_DEFAULT_QUALITY; + params->lgwin = BROTLI_DEFAULT_WINDOW; + params->lgblock = 0; + params->stream_offset = 0; + params->size_hint = 0; + params->disable_literal_context_modeling = BROTLI_FALSE; + BrotliInitSharedEncoderDictionary(¶ms->dictionary); + params->dist.distance_postfix_bits = 0; + params->dist.num_direct_distance_codes = 0; + params->dist.alphabet_size_max = + BROTLI_DISTANCE_ALPHABET_SIZE(0, 0, BROTLI_MAX_DISTANCE_BITS); + params->dist.alphabet_size_limit = params->dist.alphabet_size_max; + params->dist.max_distance = BROTLI_MAX_DISTANCE; +} + +static void BrotliEncoderCleanupParams(MemoryManager* m, + BrotliEncoderParams* params) { + BrotliCleanupSharedEncoderDictionary(m, ¶ms->dictionary); +} + +#ifdef BROTLI_REPORTING +/* When BROTLI_REPORTING is defined extra reporting module have to be linked. */ +void BrotliEncoderOnStart(const BrotliEncoderState* s); +void BrotliEncoderOnFinish(const BrotliEncoderState* s); +#define BROTLI_ENCODER_ON_START(s) BrotliEncoderOnStart(s); +#define BROTLI_ENCODER_ON_FINISH(s) BrotliEncoderOnFinish(s); +#else +#if !defined(BROTLI_ENCODER_ON_START) +#define BROTLI_ENCODER_ON_START(s) (void)(s); +#endif +#if !defined(BROTLI_ENCODER_ON_FINISH) +#define BROTLI_ENCODER_ON_FINISH(s) (void)(s); +#endif +#endif + +static void BrotliEncoderInitState(BrotliEncoderState* s) { + BROTLI_ENCODER_ON_START(s); + BrotliEncoderInitParams(&s->params); + s->input_pos_ = 0; + s->num_commands_ = 0; + s->num_literals_ = 0; + s->last_insert_len_ = 0; + s->last_flush_pos_ = 0; + s->last_processed_pos_ = 0; + s->prev_byte_ = 0; + s->prev_byte2_ = 0; + s->storage_size_ = 0; + s->storage_ = 0; + HasherInit(&s->hasher_); + s->large_table_ = NULL; + s->large_table_size_ = 0; + s->one_pass_arena_ = NULL; + s->two_pass_arena_ = NULL; + s->command_buf_ = NULL; + s->literal_buf_ = NULL; + s->total_in_ = 0; + s->next_out_ = NULL; + s->available_out_ = 0; + s->total_out_ = 0; + s->stream_state_ = BROTLI_STREAM_PROCESSING; + s->is_last_block_emitted_ = BROTLI_FALSE; + s->is_initialized_ = BROTLI_FALSE; + + RingBufferInit(&s->ringbuffer_); + + s->commands_ = 0; + s->cmd_alloc_size_ = 0; + + /* Initialize distance cache. */ + s->dist_cache_[0] = 4; + s->dist_cache_[1] = 11; + s->dist_cache_[2] = 15; + s->dist_cache_[3] = 16; + /* Save the state of the distance cache in case we need to restore it for + emitting an uncompressed block. */ + memcpy(s->saved_dist_cache_, s->dist_cache_, sizeof(s->saved_dist_cache_)); +} + +BrotliEncoderState* BrotliEncoderCreateInstance( + brotli_alloc_func alloc_func, brotli_free_func free_func, void* opaque) { + BROTLI_BOOL healthy = BrotliEncoderEnsureStaticInit(); + if (!healthy) { + return 0; + } + BrotliEncoderState* state = (BrotliEncoderState*)BrotliBootstrapAlloc( + sizeof(BrotliEncoderState), alloc_func, free_func, opaque); + if (state == NULL) { + /* BROTLI_DUMP(); */ + return 0; + } + BrotliInitMemoryManager( + &state->memory_manager_, alloc_func, free_func, opaque); + BrotliEncoderInitState(state); + return state; +} + +static void BrotliEncoderCleanupState(BrotliEncoderState* s) { + MemoryManager* m = &s->memory_manager_; + + BROTLI_ENCODER_ON_FINISH(s); + + if (BROTLI_IS_OOM(m)) { + BrotliWipeOutMemoryManager(m); + return; + } + + BROTLI_FREE(m, s->storage_); + BROTLI_FREE(m, s->commands_); + RingBufferFree(m, &s->ringbuffer_); + DestroyHasher(m, &s->hasher_); + BROTLI_FREE(m, s->large_table_); + BROTLI_FREE(m, s->one_pass_arena_); + BROTLI_FREE(m, s->two_pass_arena_); + BROTLI_FREE(m, s->command_buf_); + BROTLI_FREE(m, s->literal_buf_); + BrotliEncoderCleanupParams(m, &s->params); +} + +/* Deinitializes and frees BrotliEncoderState instance. */ +void BrotliEncoderDestroyInstance(BrotliEncoderState* state) { + if (!state) { + return; + } else { + BrotliEncoderCleanupState(state); + BrotliBootstrapFree(state, &state->memory_manager_); + } +} + +/* + Copies the given input data to the internal ring buffer of the compressor. + No processing of the data occurs at this time and this function can be + called multiple times before calling WriteBrotliData() to process the + accumulated input. At most input_block_size() bytes of input data can be + copied to the ring buffer, otherwise the next WriteBrotliData() will fail. + */ +static void CopyInputToRingBuffer(BrotliEncoderState* s, + const size_t input_size, + const uint8_t* input_buffer) { + RingBuffer* ringbuffer_ = &s->ringbuffer_; + MemoryManager* m = &s->memory_manager_; + RingBufferWrite(m, input_buffer, input_size, ringbuffer_); + if (BROTLI_IS_OOM(m)) return; + s->input_pos_ += input_size; + + /* TL;DR: If needed, initialize 7 more bytes in the ring buffer to make the + hashing not depend on uninitialized data. This makes compression + deterministic and it prevents uninitialized memory warnings in Valgrind. + Even without erasing, the output would be valid (but nondeterministic). + + Background information: The compressor stores short (at most 8 bytes) + substrings of the input already read in a hash table, and detects + repetitions by looking up such substrings in the hash table. If it + can find a substring, it checks whether the substring is really there + in the ring buffer (or it's just a hash collision). Should the hash + table become corrupt, this check makes sure that the output is + still valid, albeit the compression ratio would be bad. + + The compressor populates the hash table from the ring buffer as it's + reading new bytes from the input. However, at the last few indexes of + the ring buffer, there are not enough bytes to build full-length + substrings from. Since the hash table always contains full-length + substrings, we overwrite with zeros here to make sure that those + substrings will contain zeros at the end instead of uninitialized + data. + + Please note that erasing is not necessary (because the + memory region is already initialized since he ring buffer + has a `tail' that holds a copy of the beginning,) so we + skip erasing if we have already gone around at least once in + the ring buffer. + + Only clear during the first round of ring-buffer writes. On + subsequent rounds data in the ring-buffer would be affected. */ + if (ringbuffer_->pos_ <= ringbuffer_->mask_) { + /* This is the first time when the ring buffer is being written. + We clear 7 bytes just after the bytes that have been copied from + the input buffer. + + The ring-buffer has a "tail" that holds a copy of the beginning, + but only once the ring buffer has been fully written once, i.e., + pos <= mask. For the first time, we need to write values + in this tail (where index may be larger than mask), so that + we have exactly defined behavior and don't read uninitialized + memory. Due to performance reasons, hashing reads data using a + LOAD64, which can go 7 bytes beyond the bytes written in the + ring-buffer. */ + memset(ringbuffer_->buffer_ + ringbuffer_->pos_, 0, 7); + } +} + +/* Marks all input as processed. + Returns true if position wrapping occurs. */ +static BROTLI_BOOL UpdateLastProcessedPos(BrotliEncoderState* s) { + uint32_t wrapped_last_processed_pos = WrapPosition(s->last_processed_pos_); + uint32_t wrapped_input_pos = WrapPosition(s->input_pos_); + s->last_processed_pos_ = s->input_pos_; + return TO_BROTLI_BOOL(wrapped_input_pos < wrapped_last_processed_pos); +} + +static void ExtendLastCommand(BrotliEncoderState* s, uint32_t* bytes, + uint32_t* wrapped_last_processed_pos) { + Command* last_command = &s->commands_[s->num_commands_ - 1]; + const uint8_t* data = s->ringbuffer_.buffer_; + const uint32_t mask = s->ringbuffer_.mask_; + uint64_t max_backward_distance = + (((uint64_t)1) << s->params.lgwin) - BROTLI_WINDOW_GAP; + uint64_t last_copy_len = last_command->copy_len_ & 0x1FFFFFF; + uint64_t last_processed_pos = s->last_processed_pos_ - last_copy_len; + uint64_t max_distance = last_processed_pos < max_backward_distance ? + last_processed_pos : max_backward_distance; + uint64_t cmd_dist = (uint64_t)s->dist_cache_[0]; + uint32_t distance_code = CommandRestoreDistanceCode(last_command, + &s->params.dist); + const CompoundDictionary* dict = &s->params.dictionary.compound; + size_t compound_dictionary_size = dict->total_size; + if (distance_code < BROTLI_NUM_DISTANCE_SHORT_CODES || + distance_code - (BROTLI_NUM_DISTANCE_SHORT_CODES - 1) == cmd_dist) { + if (cmd_dist <= max_distance) { + while (*bytes != 0 && data[*wrapped_last_processed_pos & mask] == + data[(*wrapped_last_processed_pos - cmd_dist) & mask]) { + last_command->copy_len_++; + (*bytes)--; + (*wrapped_last_processed_pos)++; + } + } else { + if ((cmd_dist - max_distance - 1) < compound_dictionary_size && + last_copy_len < cmd_dist - max_distance) { + size_t address = + compound_dictionary_size - (size_t)(cmd_dist - max_distance) + + (size_t)last_copy_len; + size_t br_index = 0; + size_t br_offset; + const uint8_t* chunk; + size_t chunk_length; + while (address >= dict->chunk_offsets[br_index + 1]) br_index++; + br_offset = address - dict->chunk_offsets[br_index]; + chunk = dict->chunk_source[br_index]; + chunk_length = + dict->chunk_offsets[br_index + 1] - dict->chunk_offsets[br_index]; + while (*bytes != 0 && data[*wrapped_last_processed_pos & mask] == + chunk[br_offset]) { + last_command->copy_len_++; + (*bytes)--; + (*wrapped_last_processed_pos)++; + if (++br_offset == chunk_length) { + br_index++; + br_offset = 0; + if (br_index != dict->num_chunks) { + chunk = dict->chunk_source[br_index]; + chunk_length = dict->chunk_offsets[br_index + 1] - + dict->chunk_offsets[br_index]; + } else { + break; + } + } + } + } + } + /* The copy length is at most the metablock size, and thus expressible. */ + GetLengthCode(last_command->insert_len_, + (size_t)((int)(last_command->copy_len_ & 0x1FFFFFF) + + (int)(last_command->copy_len_ >> 25)), + TO_BROTLI_BOOL((last_command->dist_prefix_ & 0x3FF) == 0), + &last_command->cmd_prefix_); + } +} + +/* + Processes the accumulated input data and sets |*out_size| to the length of + the new output meta-block, or to zero if no new output meta-block has been + created (in this case the processed input data is buffered internally). + If |*out_size| is positive, |*output| points to the start of the output + data. If |is_last| or |force_flush| is BROTLI_TRUE, an output meta-block is + always created. However, until |is_last| is BROTLI_TRUE encoder may retain up + to 7 bits of the last byte of output. Byte-alignment could be enforced by + emitting an empty meta-data block. + Returns BROTLI_FALSE if the size of the input data is larger than + input_block_size(). + */ +static BROTLI_BOOL EncodeData( + BrotliEncoderState* s, const BROTLI_BOOL is_last, + const BROTLI_BOOL force_flush, size_t* out_size, uint8_t** output) { + const uint64_t delta = UnprocessedInputSize(s); + uint32_t bytes = (uint32_t)delta; + uint32_t wrapped_last_processed_pos = WrapPosition(s->last_processed_pos_); + uint8_t* data; + uint32_t mask; + MemoryManager* m = &s->memory_manager_; + ContextType literal_context_mode; + ContextLut literal_context_lut; + BROTLI_BOOL fast_compress = + s->params.quality == FAST_ONE_PASS_COMPRESSION_QUALITY || + s->params.quality == FAST_TWO_PASS_COMPRESSION_QUALITY; + + data = s->ringbuffer_.buffer_; + mask = s->ringbuffer_.mask_; + + if (delta == 0) { /* No new input; still might want to flush or finish. */ + if (!data) { /* No input has been processed so far. */ + if (is_last) { /* Emit complete finalized stream. */ + BROTLI_DCHECK(s->last_bytes_bits_ <= 14); + s->last_bytes_ |= (uint16_t)(3u << s->last_bytes_bits_); + s->last_bytes_bits_ = (uint8_t)(s->last_bytes_bits_ + 2u); + s->tiny_buf_.u8[0] = (uint8_t)s->last_bytes_; + s->tiny_buf_.u8[1] = (uint8_t)(s->last_bytes_ >> 8); + *output = s->tiny_buf_.u8; + *out_size = (s->last_bytes_bits_ + 7u) >> 3u; + return BROTLI_TRUE; + } else { /* No data, not last -> no-op. */ + *out_size = 0; + return BROTLI_TRUE; + } + } else { + /* Fast compress performs flush every block -> flush is no-op. */ + if (!is_last && (!force_flush || fast_compress)) { /* Another no-op. */ + *out_size = 0; + return BROTLI_TRUE; + } + } + } + BROTLI_DCHECK(data); + + if (s->params.quality > s->params.dictionary.max_quality) return BROTLI_FALSE; + /* Adding more blocks after "last" block is forbidden. */ + if (s->is_last_block_emitted_) return BROTLI_FALSE; + if (is_last) s->is_last_block_emitted_ = BROTLI_TRUE; + + if (delta > InputBlockSize(s)) { + return BROTLI_FALSE; + } + if (s->params.quality == FAST_TWO_PASS_COMPRESSION_QUALITY && + !s->command_buf_) { + s->command_buf_ = + BROTLI_ALLOC(m, uint32_t, kCompressFragmentTwoPassBlockSize); + s->literal_buf_ = + BROTLI_ALLOC(m, uint8_t, kCompressFragmentTwoPassBlockSize); + if (BROTLI_IS_OOM(m) || BROTLI_IS_NULL(s->command_buf_) || + BROTLI_IS_NULL(s->literal_buf_)) { + return BROTLI_FALSE; + } + } + + if (fast_compress) { + uint8_t* storage; + size_t storage_ix = s->last_bytes_bits_; + size_t table_size; + int* table; + + storage = GetBrotliStorage(s, 2 * bytes + 503); + if (BROTLI_IS_OOM(m)) return BROTLI_FALSE; + storage[0] = (uint8_t)s->last_bytes_; + storage[1] = (uint8_t)(s->last_bytes_ >> 8); + table = GetHashTable(s, s->params.quality, bytes, &table_size); + if (BROTLI_IS_OOM(m)) return BROTLI_FALSE; + if (s->params.quality == FAST_ONE_PASS_COMPRESSION_QUALITY) { + BrotliCompressFragmentFast( + s->one_pass_arena_, &data[wrapped_last_processed_pos & mask], + bytes, is_last, + table, table_size, + &storage_ix, storage); + if (BROTLI_IS_OOM(m)) return BROTLI_FALSE; + } else { + BrotliCompressFragmentTwoPass( + s->two_pass_arena_, &data[wrapped_last_processed_pos & mask], + bytes, is_last, + s->command_buf_, s->literal_buf_, + table, table_size, + &storage_ix, storage); + if (BROTLI_IS_OOM(m)) return BROTLI_FALSE; + } + s->last_bytes_ = (uint16_t)(storage[storage_ix >> 3]); + s->last_bytes_bits_ = storage_ix & 7u; + UpdateLastProcessedPos(s); + *output = &storage[0]; + *out_size = storage_ix >> 3; + return BROTLI_TRUE; + } + + { + /* Theoretical max number of commands is 1 per 2 bytes. */ + size_t newsize = s->num_commands_ + bytes / 2 + 1; + if (newsize > s->cmd_alloc_size_) { + Command* new_commands; + /* Reserve a bit more memory to allow merging with a next block + without reallocation: that would impact speed. */ + newsize += (bytes / 4) + 16; + s->cmd_alloc_size_ = newsize; + new_commands = BROTLI_ALLOC(m, Command, newsize); + if (BROTLI_IS_OOM(m) || BROTLI_IS_NULL(new_commands)) return BROTLI_FALSE; + if (s->commands_) { + memcpy(new_commands, s->commands_, sizeof(Command) * s->num_commands_); + BROTLI_FREE(m, s->commands_); + } + s->commands_ = new_commands; + } + } + + InitOrStitchToPreviousBlock(m, &s->hasher_, data, mask, &s->params, + wrapped_last_processed_pos, bytes, is_last); + + literal_context_mode = ChooseContextMode( + &s->params, data, WrapPosition(s->last_flush_pos_), + mask, (size_t)(s->input_pos_ - s->last_flush_pos_)); + literal_context_lut = BROTLI_CONTEXT_LUT(literal_context_mode); + + if (BROTLI_IS_OOM(m)) return BROTLI_FALSE; + + if (s->num_commands_ && s->last_insert_len_ == 0) { + ExtendLastCommand(s, &bytes, &wrapped_last_processed_pos); + } + + if (s->params.quality == ZOPFLIFICATION_QUALITY) { + BROTLI_DCHECK(s->params.hasher.type == 10); + BrotliCreateZopfliBackwardReferences(m, bytes, wrapped_last_processed_pos, + data, mask, literal_context_lut, &s->params, + &s->hasher_, s->dist_cache_, + &s->last_insert_len_, &s->commands_[s->num_commands_], + &s->num_commands_, &s->num_literals_); + if (BROTLI_IS_OOM(m)) return BROTLI_FALSE; + } else if (s->params.quality == HQ_ZOPFLIFICATION_QUALITY) { + BROTLI_DCHECK(s->params.hasher.type == 10); + BrotliCreateHqZopfliBackwardReferences(m, bytes, wrapped_last_processed_pos, + data, mask, literal_context_lut, &s->params, + &s->hasher_, s->dist_cache_, + &s->last_insert_len_, &s->commands_[s->num_commands_], + &s->num_commands_, &s->num_literals_); + if (BROTLI_IS_OOM(m)) return BROTLI_FALSE; + } else { + BrotliCreateBackwardReferences(bytes, wrapped_last_processed_pos, + data, mask, literal_context_lut, &s->params, + &s->hasher_, s->dist_cache_, + &s->last_insert_len_, &s->commands_[s->num_commands_], + &s->num_commands_, &s->num_literals_); + } + + { + const size_t max_length = MaxMetablockSize(&s->params); + const size_t max_literals = max_length / 8; + const size_t max_commands = max_length / 8; + const size_t processed_bytes = (size_t)(s->input_pos_ - s->last_flush_pos_); + /* If maximal possible additional block doesn't fit metablock, flush now. */ + /* TODO(eustas): Postpone decision until next block arrives? */ + const BROTLI_BOOL next_input_fits_metablock = TO_BROTLI_BOOL( + processed_bytes + InputBlockSize(s) <= max_length); + /* If block splitting is not used, then flush as soon as there is some + amount of commands / literals produced. */ + const BROTLI_BOOL should_flush = TO_BROTLI_BOOL( + s->params.quality < MIN_QUALITY_FOR_BLOCK_SPLIT && + s->num_literals_ + s->num_commands_ >= MAX_NUM_DELAYED_SYMBOLS); + if (!is_last && !force_flush && !should_flush && + next_input_fits_metablock && + s->num_literals_ < max_literals && + s->num_commands_ < max_commands) { + /* Merge with next input block. Everything will happen later. */ + if (UpdateLastProcessedPos(s)) { + HasherReset(&s->hasher_); + } + *out_size = 0; + return BROTLI_TRUE; + } + } + + /* Create the last insert-only command. */ + if (s->last_insert_len_ > 0) { + InitInsertCommand(&s->commands_[s->num_commands_++], s->last_insert_len_); + s->num_literals_ += s->last_insert_len_; + s->last_insert_len_ = 0; + } + + if (!is_last && s->input_pos_ == s->last_flush_pos_) { + /* We have no new input data and we don't have to finish the stream, so + nothing to do. */ + *out_size = 0; + return BROTLI_TRUE; + } + BROTLI_DCHECK(s->input_pos_ >= s->last_flush_pos_); + BROTLI_DCHECK(s->input_pos_ > s->last_flush_pos_ || is_last); + BROTLI_DCHECK(s->input_pos_ - s->last_flush_pos_ <= 1u << 24); + { + const uint32_t metablock_size = + (uint32_t)(s->input_pos_ - s->last_flush_pos_); + uint8_t* storage = GetBrotliStorage(s, 2 * metablock_size + 503); + size_t storage_ix = s->last_bytes_bits_; + if (BROTLI_IS_OOM(m)) return BROTLI_FALSE; + storage[0] = (uint8_t)s->last_bytes_; + storage[1] = (uint8_t)(s->last_bytes_ >> 8); + WriteMetaBlockInternal( + m, data, mask, s->last_flush_pos_, metablock_size, is_last, + literal_context_mode, &s->params, s->prev_byte_, s->prev_byte2_, + s->num_literals_, s->num_commands_, s->commands_, s->saved_dist_cache_, + s->dist_cache_, &storage_ix, storage); + if (BROTLI_IS_OOM(m)) return BROTLI_FALSE; + s->last_bytes_ = (uint16_t)(storage[storage_ix >> 3]); + s->last_bytes_bits_ = storage_ix & 7u; + s->last_flush_pos_ = s->input_pos_; + if (UpdateLastProcessedPos(s)) { + HasherReset(&s->hasher_); + } + if (s->last_flush_pos_ > 0) { + s->prev_byte_ = data[((uint32_t)s->last_flush_pos_ - 1) & mask]; + } + if (s->last_flush_pos_ > 1) { + s->prev_byte2_ = data[(uint32_t)(s->last_flush_pos_ - 2) & mask]; + } + s->num_commands_ = 0; + s->num_literals_ = 0; + /* Save the state of the distance cache in case we need to restore it for + emitting an uncompressed block. */ + memcpy(s->saved_dist_cache_, s->dist_cache_, sizeof(s->saved_dist_cache_)); + *output = &storage[0]; + *out_size = storage_ix >> 3; + return BROTLI_TRUE; + } +} + +/* Dumps remaining output bits and metadata header to |header|. + Returns number of produced bytes. + REQUIRED: |header| should be 8-byte aligned and at least 16 bytes long. + REQUIRED: |block_size| <= (1 << 24). */ +static size_t WriteMetadataHeader( + BrotliEncoderState* s, const size_t block_size, uint8_t* header) { + size_t storage_ix; + storage_ix = s->last_bytes_bits_; + header[0] = (uint8_t)s->last_bytes_; + header[1] = (uint8_t)(s->last_bytes_ >> 8); + s->last_bytes_ = 0; + s->last_bytes_bits_ = 0; + + BrotliWriteBits(1, 0, &storage_ix, header); + BrotliWriteBits(2, 3, &storage_ix, header); + BrotliWriteBits(1, 0, &storage_ix, header); + if (block_size == 0) { + BrotliWriteBits(2, 0, &storage_ix, header); + } else { + uint32_t nbits = (block_size == 1) ? 1 : + (Log2FloorNonZero((uint32_t)block_size - 1) + 1); + uint32_t nbytes = (nbits + 7) / 8; + BrotliWriteBits(2, nbytes, &storage_ix, header); + BrotliWriteBits(8 * nbytes, block_size - 1, &storage_ix, header); + } + return (storage_ix + 7u) >> 3; +} + +size_t BrotliEncoderMaxCompressedSize(size_t input_size) { + /* [window bits / empty metadata] + N * [uncompressed] + [last empty] */ + size_t num_large_blocks = input_size >> 14; + size_t overhead = 2 + (4 * num_large_blocks) + 3 + 1; + size_t result = input_size + overhead; + if (input_size == 0) return 2; + return (result < input_size) ? 0 : result; +} + +/* Wraps data to uncompressed brotli stream with minimal window size. + |output| should point at region with at least BrotliEncoderMaxCompressedSize + addressable bytes. + Returns the length of stream. */ +static size_t MakeUncompressedStream( + const uint8_t* input, size_t input_size, uint8_t* output) { + size_t size = input_size; + size_t result = 0; + size_t offset = 0; + if (input_size == 0) { + output[0] = 6; + return 1; + } + output[result++] = 0x21; /* window bits = 10, is_last = false */ + output[result++] = 0x03; /* empty metadata, padding */ + while (size > 0) { + uint32_t nibbles = 0; + uint32_t chunk_size; + uint32_t bits; + chunk_size = (size > (1u << 24)) ? (1u << 24) : (uint32_t)size; + if (chunk_size > (1u << 16)) nibbles = (chunk_size > (1u << 20)) ? 2 : 1; + bits = + (nibbles << 1) | ((chunk_size - 1) << 3) | (1u << (19 + 4 * nibbles)); + output[result++] = (uint8_t)bits; + output[result++] = (uint8_t)(bits >> 8); + output[result++] = (uint8_t)(bits >> 16); + if (nibbles == 2) output[result++] = (uint8_t)(bits >> 24); + memcpy(&output[result], &input[offset], chunk_size); + result += chunk_size; + offset += chunk_size; + size -= chunk_size; + } + output[result++] = 3; + return result; +} + +BROTLI_BOOL BrotliEncoderCompress( + int quality, int lgwin, BrotliEncoderMode mode, size_t input_size, + const uint8_t input_buffer[BROTLI_ARRAY_PARAM(input_size)], + size_t* encoded_size, + uint8_t encoded_buffer[BROTLI_ARRAY_PARAM(*encoded_size)]) { + BrotliEncoderState* s; + size_t out_size = *encoded_size; + const uint8_t* input_start = input_buffer; + uint8_t* output_start = encoded_buffer; + size_t max_out_size = BrotliEncoderMaxCompressedSize(input_size); + if (out_size == 0) { + /* Output buffer needs at least one byte. */ + return BROTLI_FALSE; + } + if (input_size == 0) { + /* Handle the special case of empty input. */ + *encoded_size = 1; + *encoded_buffer = 6; + return BROTLI_TRUE; + } + + s = BrotliEncoderCreateInstance(0, 0, 0); + if (!s) { + return BROTLI_FALSE; + } else { + size_t available_in = input_size; + const uint8_t* next_in = input_buffer; + size_t available_out = *encoded_size; + uint8_t* next_out = encoded_buffer; + size_t total_out = 0; + BROTLI_BOOL result = BROTLI_FALSE; + /* TODO(eustas): check that parameters are sane. */ + BrotliEncoderSetParameter(s, BROTLI_PARAM_QUALITY, (uint32_t)quality); + BrotliEncoderSetParameter(s, BROTLI_PARAM_LGWIN, (uint32_t)lgwin); + BrotliEncoderSetParameter(s, BROTLI_PARAM_MODE, (uint32_t)mode); + BrotliEncoderSetParameter(s, BROTLI_PARAM_SIZE_HINT, (uint32_t)input_size); + if (lgwin > BROTLI_MAX_WINDOW_BITS) { + BrotliEncoderSetParameter(s, BROTLI_PARAM_LARGE_WINDOW, BROTLI_TRUE); + } + result = BrotliEncoderCompressStream(s, BROTLI_OPERATION_FINISH, + &available_in, &next_in, &available_out, &next_out, &total_out); + if (!BrotliEncoderIsFinished(s)) result = 0; + *encoded_size = total_out; + BrotliEncoderDestroyInstance(s); + if (!result || (max_out_size && *encoded_size > max_out_size)) { + goto fallback; + } + return BROTLI_TRUE; + } +fallback: + *encoded_size = 0; + if (!max_out_size) return BROTLI_FALSE; + if (out_size >= max_out_size) { + *encoded_size = + MakeUncompressedStream(input_start, input_size, output_start); + return BROTLI_TRUE; + } + return BROTLI_FALSE; +} + +static void InjectBytePaddingBlock(BrotliEncoderState* s) { + uint32_t seal = s->last_bytes_; + size_t seal_bits = s->last_bytes_bits_; + uint8_t* destination; + s->last_bytes_ = 0; + s->last_bytes_bits_ = 0; + /* is_last = 0, data_nibbles = 11, reserved = 0, meta_nibbles = 00 */ + seal |= 0x6u << seal_bits; + seal_bits += 6; + /* If we have already created storage, then append to it. + Storage is valid until next block is being compressed. */ + if (s->next_out_) { + destination = s->next_out_ + s->available_out_; + } else { + destination = s->tiny_buf_.u8; + s->next_out_ = destination; + } + destination[0] = (uint8_t)seal; + if (seal_bits > 8) destination[1] = (uint8_t)(seal >> 8); + if (seal_bits > 16) destination[2] = (uint8_t)(seal >> 16); + s->available_out_ += (seal_bits + 7) >> 3; +} + +/* Fills the |total_out|, if it is not NULL. */ +static void SetTotalOut(BrotliEncoderState* s, size_t* total_out) { + if (total_out) { + /* Saturating conversion uint64_t -> size_t */ + size_t result = (size_t)-1; + if (s->total_out_ < result) { + result = (size_t)s->total_out_; + } + *total_out = result; + } +} + +/* Injects padding bits or pushes compressed data to output. + Returns false if nothing is done. */ +static BROTLI_BOOL InjectFlushOrPushOutput(BrotliEncoderState* s, + size_t* available_out, uint8_t** next_out, size_t* total_out) { + if (s->stream_state_ == BROTLI_STREAM_FLUSH_REQUESTED && + s->last_bytes_bits_ != 0) { + InjectBytePaddingBlock(s); + return BROTLI_TRUE; + } + + if (s->available_out_ != 0 && *available_out != 0) { + size_t copy_output_size = + BROTLI_MIN(size_t, s->available_out_, *available_out); + memcpy(*next_out, s->next_out_, copy_output_size); + *next_out += copy_output_size; + *available_out -= copy_output_size; + s->next_out_ += copy_output_size; + s->available_out_ -= copy_output_size; + s->total_out_ += copy_output_size; + SetTotalOut(s, total_out); + return BROTLI_TRUE; + } + + return BROTLI_FALSE; +} + +static void CheckFlushComplete(BrotliEncoderState* s) { + if (s->stream_state_ == BROTLI_STREAM_FLUSH_REQUESTED && + s->available_out_ == 0) { + s->stream_state_ = BROTLI_STREAM_PROCESSING; + s->next_out_ = 0; + } +} + +static BROTLI_BOOL BrotliEncoderCompressStreamFast( + BrotliEncoderState* s, BrotliEncoderOperation op, size_t* available_in, + const uint8_t** next_in, size_t* available_out, uint8_t** next_out, + size_t* total_out) { + const size_t block_size_limit = (size_t)1 << s->params.lgwin; + const size_t buf_size = BROTLI_MIN(size_t, kCompressFragmentTwoPassBlockSize, + BROTLI_MIN(size_t, *available_in, block_size_limit)); + uint32_t* tmp_command_buf = NULL; + uint32_t* command_buf = NULL; + uint8_t* tmp_literal_buf = NULL; + uint8_t* literal_buf = NULL; + MemoryManager* m = &s->memory_manager_; + if (s->params.quality != FAST_ONE_PASS_COMPRESSION_QUALITY && + s->params.quality != FAST_TWO_PASS_COMPRESSION_QUALITY) { + return BROTLI_FALSE; + } + if (s->params.quality == FAST_TWO_PASS_COMPRESSION_QUALITY) { + if (!s->command_buf_ && buf_size == kCompressFragmentTwoPassBlockSize) { + s->command_buf_ = + BROTLI_ALLOC(m, uint32_t, kCompressFragmentTwoPassBlockSize); + s->literal_buf_ = + BROTLI_ALLOC(m, uint8_t, kCompressFragmentTwoPassBlockSize); + if (BROTLI_IS_OOM(m) || BROTLI_IS_NULL(s->command_buf_) || + BROTLI_IS_NULL(s->literal_buf_)) { + return BROTLI_FALSE; + } + } + if (s->command_buf_) { + command_buf = s->command_buf_; + literal_buf = s->literal_buf_; + } else { + tmp_command_buf = BROTLI_ALLOC(m, uint32_t, buf_size); + tmp_literal_buf = BROTLI_ALLOC(m, uint8_t, buf_size); + if (BROTLI_IS_OOM(m) || BROTLI_IS_NULL(tmp_command_buf) || + BROTLI_IS_NULL(tmp_literal_buf)) { + return BROTLI_FALSE; + } + command_buf = tmp_command_buf; + literal_buf = tmp_literal_buf; + } + } + + while (BROTLI_TRUE) { + if (InjectFlushOrPushOutput(s, available_out, next_out, total_out)) { + continue; + } + + /* Compress block only when internal output buffer is empty, stream is not + finished, there is no pending flush request, and there is either + additional input or pending operation. */ + if (s->available_out_ == 0 && + s->stream_state_ == BROTLI_STREAM_PROCESSING && + (*available_in != 0 || op != BROTLI_OPERATION_PROCESS)) { + size_t block_size = BROTLI_MIN(size_t, block_size_limit, *available_in); + BROTLI_BOOL is_last = + (*available_in == block_size) && (op == BROTLI_OPERATION_FINISH); + BROTLI_BOOL force_flush = + (*available_in == block_size) && (op == BROTLI_OPERATION_FLUSH); + size_t max_out_size = 2 * block_size + 503; + BROTLI_BOOL inplace = BROTLI_TRUE; + uint8_t* storage = NULL; + size_t storage_ix = s->last_bytes_bits_; + size_t table_size; + int* table; + + if (force_flush && block_size == 0) { + s->stream_state_ = BROTLI_STREAM_FLUSH_REQUESTED; + continue; + } + if (max_out_size <= *available_out) { + storage = *next_out; + } else { + inplace = BROTLI_FALSE; + storage = GetBrotliStorage(s, max_out_size); + if (BROTLI_IS_OOM(m)) return BROTLI_FALSE; + } + storage[0] = (uint8_t)s->last_bytes_; + storage[1] = (uint8_t)(s->last_bytes_ >> 8); + table = GetHashTable(s, s->params.quality, block_size, &table_size); + if (BROTLI_IS_OOM(m)) return BROTLI_FALSE; + + if (s->params.quality == FAST_ONE_PASS_COMPRESSION_QUALITY) { + BrotliCompressFragmentFast(s->one_pass_arena_, *next_in, block_size, + is_last, table, table_size, &storage_ix, storage); + if (BROTLI_IS_OOM(m)) return BROTLI_FALSE; + } else { + BrotliCompressFragmentTwoPass(s->two_pass_arena_, *next_in, block_size, + is_last, command_buf, literal_buf, table, table_size, + &storage_ix, storage); + if (BROTLI_IS_OOM(m)) return BROTLI_FALSE; + } + if (block_size != 0) { + *next_in += block_size; + *available_in -= block_size; + s->total_in_ += block_size; + } + if (inplace) { + size_t out_bytes = storage_ix >> 3; + BROTLI_DCHECK(out_bytes <= *available_out); + BROTLI_DCHECK((storage_ix & 7) == 0 || out_bytes < *available_out); + *next_out += out_bytes; + *available_out -= out_bytes; + s->total_out_ += out_bytes; + SetTotalOut(s, total_out); + } else { + size_t out_bytes = storage_ix >> 3; + s->next_out_ = storage; + s->available_out_ = out_bytes; + } + s->last_bytes_ = (uint16_t)(storage[storage_ix >> 3]); + s->last_bytes_bits_ = storage_ix & 7u; + + if (force_flush) s->stream_state_ = BROTLI_STREAM_FLUSH_REQUESTED; + if (is_last) s->stream_state_ = BROTLI_STREAM_FINISHED; + continue; + } + break; + } + BROTLI_FREE(m, tmp_command_buf); + BROTLI_FREE(m, tmp_literal_buf); + CheckFlushComplete(s); + return BROTLI_TRUE; +} + +static BROTLI_BOOL ProcessMetadata( + BrotliEncoderState* s, size_t* available_in, const uint8_t** next_in, + size_t* available_out, uint8_t** next_out, size_t* total_out) { + if (*available_in > (1u << 24)) return BROTLI_FALSE; + /* Switch to metadata block workflow, if required. */ + if (s->stream_state_ == BROTLI_STREAM_PROCESSING) { + s->remaining_metadata_bytes_ = (uint32_t)*available_in; + s->stream_state_ = BROTLI_STREAM_METADATA_HEAD; + } + if (s->stream_state_ != BROTLI_STREAM_METADATA_HEAD && + s->stream_state_ != BROTLI_STREAM_METADATA_BODY) { + return BROTLI_FALSE; + } + + while (BROTLI_TRUE) { + if (InjectFlushOrPushOutput(s, available_out, next_out, total_out)) { + continue; + } + if (s->available_out_ != 0) break; + + if (s->input_pos_ != s->last_flush_pos_) { + BROTLI_BOOL result = EncodeData(s, BROTLI_FALSE, BROTLI_TRUE, + &s->available_out_, &s->next_out_); + if (!result) return BROTLI_FALSE; + continue; + } + + if (s->stream_state_ == BROTLI_STREAM_METADATA_HEAD) { + s->next_out_ = s->tiny_buf_.u8; + s->available_out_ = + WriteMetadataHeader(s, s->remaining_metadata_bytes_, s->next_out_); + s->stream_state_ = BROTLI_STREAM_METADATA_BODY; + continue; + } else { + /* Exit workflow only when there is no more input and no more output. + Otherwise client may continue producing empty metadata blocks. */ + if (s->remaining_metadata_bytes_ == 0) { + s->remaining_metadata_bytes_ = BROTLI_UINT32_MAX; + s->stream_state_ = BROTLI_STREAM_PROCESSING; + break; + } + if (*available_out) { + /* Directly copy input to output. */ + uint32_t copy = (uint32_t)BROTLI_MIN( + size_t, s->remaining_metadata_bytes_, *available_out); + memcpy(*next_out, *next_in, copy); + *next_in += copy; + *available_in -= copy; + s->total_in_ += copy; /* not actually data input, though */ + s->remaining_metadata_bytes_ -= copy; + *next_out += copy; + *available_out -= copy; + } else { + /* This guarantees progress in "TakeOutput" workflow. */ + uint32_t copy = BROTLI_MIN(uint32_t, s->remaining_metadata_bytes_, 16); + s->next_out_ = s->tiny_buf_.u8; + memcpy(s->next_out_, *next_in, copy); + *next_in += copy; + *available_in -= copy; + s->total_in_ += copy; /* not actually data input, though */ + s->remaining_metadata_bytes_ -= copy; + s->available_out_ = copy; + } + continue; + } + } + + return BROTLI_TRUE; +} + +static void UpdateSizeHint(BrotliEncoderState* s, size_t available_in) { + if (s->params.size_hint == 0) { + uint64_t delta = UnprocessedInputSize(s); + uint64_t tail = available_in; + uint32_t limit = 1u << 30; + uint32_t total; + if ((delta >= limit) || (tail >= limit) || ((delta + tail) >= limit)) { + total = limit; + } else { + total = (uint32_t)(delta + tail); + } + s->params.size_hint = total; + } +} + +BROTLI_BOOL BrotliEncoderCompressStream( + BrotliEncoderState* s, BrotliEncoderOperation op, size_t* available_in, + const uint8_t** next_in, size_t* available_out, uint8_t** next_out, + size_t* total_out) { + if (!EnsureInitialized(s)) return BROTLI_FALSE; + + /* Unfinished metadata block; check requirements. */ + if (s->remaining_metadata_bytes_ != BROTLI_UINT32_MAX) { + if (*available_in != s->remaining_metadata_bytes_) return BROTLI_FALSE; + if (op != BROTLI_OPERATION_EMIT_METADATA) return BROTLI_FALSE; + } + + if (op == BROTLI_OPERATION_EMIT_METADATA) { + UpdateSizeHint(s, 0); /* First data metablock might be emitted here. */ + return ProcessMetadata( + s, available_in, next_in, available_out, next_out, total_out); + } + + if (s->stream_state_ == BROTLI_STREAM_METADATA_HEAD || + s->stream_state_ == BROTLI_STREAM_METADATA_BODY) { + return BROTLI_FALSE; + } + + if (s->stream_state_ != BROTLI_STREAM_PROCESSING && *available_in != 0) { + return BROTLI_FALSE; + } + if (s->params.quality == FAST_ONE_PASS_COMPRESSION_QUALITY || + s->params.quality == FAST_TWO_PASS_COMPRESSION_QUALITY) { + return BrotliEncoderCompressStreamFast(s, op, available_in, next_in, + available_out, next_out, total_out); + } + while (BROTLI_TRUE) { + size_t remaining_block_size = RemainingInputBlockSize(s); + /* Shorten input to flint size. */ + if (s->flint_ >= 0 && remaining_block_size > (size_t)s->flint_) { + remaining_block_size = (size_t)s->flint_; + } + + if (remaining_block_size != 0 && *available_in != 0) { + size_t copy_input_size = + BROTLI_MIN(size_t, remaining_block_size, *available_in); + CopyInputToRingBuffer(s, copy_input_size, *next_in); + *next_in += copy_input_size; + *available_in -= copy_input_size; + s->total_in_ += copy_input_size; + if (s->flint_ > 0) s->flint_ = (int8_t)(s->flint_ - (int)copy_input_size); + continue; + } + + if (InjectFlushOrPushOutput(s, available_out, next_out, total_out)) { + /* Exit the "emit flint" workflow. */ + if (s->flint_ == BROTLI_FLINT_WAITING_FOR_FLUSHING) { + CheckFlushComplete(s); + if (s->stream_state_ == BROTLI_STREAM_PROCESSING) { + s->flint_ = BROTLI_FLINT_DONE; + } + } + continue; + } + + /* Compress data only when internal output buffer is empty, stream is not + finished and there is no pending flush request. */ + if (s->available_out_ == 0 && + s->stream_state_ == BROTLI_STREAM_PROCESSING) { + if (remaining_block_size == 0 || op != BROTLI_OPERATION_PROCESS) { + BROTLI_BOOL is_last = TO_BROTLI_BOOL( + (*available_in == 0) && op == BROTLI_OPERATION_FINISH); + BROTLI_BOOL force_flush = TO_BROTLI_BOOL( + (*available_in == 0) && op == BROTLI_OPERATION_FLUSH); + BROTLI_BOOL result; + /* Force emitting (uncompressed) piece containing flint. */ + if (!is_last && s->flint_ == 0) { + s->flint_ = BROTLI_FLINT_WAITING_FOR_FLUSHING; + force_flush = BROTLI_TRUE; + } + UpdateSizeHint(s, *available_in); + result = EncodeData(s, is_last, force_flush, + &s->available_out_, &s->next_out_); + if (!result) return BROTLI_FALSE; + if (force_flush) s->stream_state_ = BROTLI_STREAM_FLUSH_REQUESTED; + if (is_last) s->stream_state_ = BROTLI_STREAM_FINISHED; + continue; + } + } + break; + } + CheckFlushComplete(s); + return BROTLI_TRUE; +} + +BROTLI_BOOL BrotliEncoderIsFinished(BrotliEncoderState* s) { + return TO_BROTLI_BOOL(s->stream_state_ == BROTLI_STREAM_FINISHED && + !BrotliEncoderHasMoreOutput(s)); +} + +BROTLI_BOOL BrotliEncoderHasMoreOutput(BrotliEncoderState* s) { + return TO_BROTLI_BOOL(s->available_out_ != 0); +} + +const uint8_t* BrotliEncoderTakeOutput(BrotliEncoderState* s, size_t* size) { + size_t consumed_size = s->available_out_; + uint8_t* result = s->next_out_; + if (*size) { + consumed_size = BROTLI_MIN(size_t, *size, s->available_out_); + } + if (consumed_size) { + s->next_out_ += consumed_size; + s->available_out_ -= consumed_size; + s->total_out_ += consumed_size; + CheckFlushComplete(s); + *size = consumed_size; + } else { + *size = 0; + result = 0; + } + return result; +} + +uint32_t BrotliEncoderVersion(void) { + return BROTLI_VERSION; +} + +BrotliEncoderPreparedDictionary* BrotliEncoderPrepareDictionary( + BrotliSharedDictionaryType type, size_t size, + const uint8_t data[BROTLI_ARRAY_PARAM(size)], int quality, + brotli_alloc_func alloc_func, brotli_free_func free_func, void* opaque) { + ManagedDictionary* managed_dictionary = NULL; + BROTLI_BOOL type_is_known = BROTLI_FALSE; + type_is_known |= (type == BROTLI_SHARED_DICTIONARY_RAW); +#if defined(BROTLI_EXPERIMENTAL) + type_is_known |= (type == BROTLI_SHARED_DICTIONARY_SERIALIZED); +#endif /* BROTLI_EXPERIMENTAL */ + if (!type_is_known) { + return NULL; + } + managed_dictionary = + BrotliCreateManagedDictionary(alloc_func, free_func, opaque); + if (managed_dictionary == NULL) { + return NULL; + } + if (type == BROTLI_SHARED_DICTIONARY_RAW) { + managed_dictionary->dictionary = (uint32_t*)CreatePreparedDictionary( + &managed_dictionary->memory_manager_, data, size); + } +#if defined(BROTLI_EXPERIMENTAL) + if (type == BROTLI_SHARED_DICTIONARY_SERIALIZED) { + SharedEncoderDictionary* dict = (SharedEncoderDictionary*)BrotliAllocate( + &managed_dictionary->memory_manager_, sizeof(SharedEncoderDictionary)); + managed_dictionary->dictionary = (uint32_t*)dict; + if (dict != NULL) { + BROTLI_BOOL ok = BrotliInitCustomSharedEncoderDictionary( + &managed_dictionary->memory_manager_, data, size, quality, dict); + if (!ok) { + BrotliFree(&managed_dictionary->memory_manager_, dict); + managed_dictionary->dictionary = NULL; + } + } + } +#else /* BROTLI_EXPERIMENTAL */ + (void)quality; +#endif /* BROTLI_EXPERIMENTAL */ + if (managed_dictionary->dictionary == NULL) { + BrotliDestroyManagedDictionary(managed_dictionary); + return NULL; + } + return (BrotliEncoderPreparedDictionary*)managed_dictionary; +} + +void BROTLI_COLD BrotliEncoderDestroyPreparedDictionary( + BrotliEncoderPreparedDictionary* dictionary) { + ManagedDictionary* dict = (ManagedDictionary*)dictionary; + if (!dictionary) return; + /* First field of dictionary structs. */ + /* Only managed dictionaries are eligible for destruction by this method. */ + if (dict->magic != kManagedDictionaryMagic) { + return; + } + if (dict->dictionary == NULL) { + /* This should never ever happen. */ + } else if (*dict->dictionary == kLeanPreparedDictionaryMagic) { + DestroyPreparedDictionary( + &dict->memory_manager_, (PreparedDictionary*)dict->dictionary); + } else if (*dict->dictionary == kSharedDictionaryMagic) { + BrotliCleanupSharedEncoderDictionary(&dict->memory_manager_, + (SharedEncoderDictionary*)dict->dictionary); + BrotliFree(&dict->memory_manager_, dict->dictionary); + } else { + /* There is also kPreparedDictionaryMagic, but such instances should be + * constructed and destroyed by different means. */ + } + dict->dictionary = NULL; + BrotliDestroyManagedDictionary(dict); +} + +BROTLI_BOOL BROTLI_COLD BrotliEncoderAttachPreparedDictionary( + BrotliEncoderState* state, + const BrotliEncoderPreparedDictionary* dictionary) { + /* First field of dictionary structs */ + const BrotliEncoderPreparedDictionary* dict = dictionary; + uint32_t magic = *((const uint32_t*)dict); + SharedEncoderDictionary* current = NULL; + if (magic == kManagedDictionaryMagic) { + /* Unwrap managed dictionary. */ + ManagedDictionary* managed_dictionary = (ManagedDictionary*)dict; + magic = *managed_dictionary->dictionary; + dict = (BrotliEncoderPreparedDictionary*)managed_dictionary->dictionary; + } + current = &state->params.dictionary; + if (magic == kPreparedDictionaryMagic || + magic == kLeanPreparedDictionaryMagic) { + const PreparedDictionary* prepared = (const PreparedDictionary*)dict; + if (!AttachPreparedDictionary(¤t->compound, prepared)) { + return BROTLI_FALSE; + } + } else if (magic == kSharedDictionaryMagic) { + const SharedEncoderDictionary* attached = + (const SharedEncoderDictionary*)dict; + BROTLI_BOOL was_default = !current->contextual.context_based && + current->contextual.num_dictionaries == 1 && + current->contextual.dict[0]->hash_table_words == + kStaticDictionaryHashWords && + current->contextual.dict[0]->hash_table_lengths == + kStaticDictionaryHashLengths; + BROTLI_BOOL new_default = !attached->contextual.context_based && + attached->contextual.num_dictionaries == 1 && + attached->contextual.dict[0]->hash_table_words == + kStaticDictionaryHashWords && + attached->contextual.dict[0]->hash_table_lengths == + kStaticDictionaryHashLengths; + size_t i; + if (state->is_initialized_) return BROTLI_FALSE; + current->max_quality = + BROTLI_MIN(int, current->max_quality, attached->max_quality); + for (i = 0; i < attached->compound.num_chunks; i++) { + if (!AttachPreparedDictionary(¤t->compound, + attached->compound.chunks[i])) { + return BROTLI_FALSE; + } + } + if (!new_default) { + if (!was_default) return BROTLI_FALSE; + /* Copy by value, but then set num_instances_ to 0 because their memory + is managed by attached, not by current */ + current->contextual = attached->contextual; + current->contextual.num_instances_ = 0; + } + } else { + return BROTLI_FALSE; + } + return BROTLI_TRUE; +} + +size_t BROTLI_COLD BrotliEncoderEstimatePeakMemoryUsage(int quality, int lgwin, + size_t input_size) { + BrotliEncoderParams params; + size_t memory_manager_slots = BROTLI_ENCODER_MEMORY_MANAGER_SLOTS; + size_t memory_manager_size = memory_manager_slots * sizeof(void*); + BrotliEncoderInitParams(¶ms); + params.quality = quality; + params.lgwin = lgwin; + params.size_hint = input_size; + params.large_window = lgwin > BROTLI_MAX_WINDOW_BITS; + SanitizeParams(¶ms); + params.lgblock = ComputeLgBlock(¶ms); + ChooseHasher(¶ms, ¶ms.hasher); + if (params.quality == FAST_ONE_PASS_COMPRESSION_QUALITY || + params.quality == FAST_TWO_PASS_COMPRESSION_QUALITY) { + size_t state_size = sizeof(BrotliEncoderState); + size_t block_size = BROTLI_MIN(size_t, input_size, ((size_t)1ul << params.lgwin)); + size_t hash_table_size = + HashTableSize(MaxHashTableSize(params.quality), block_size); + size_t hash_size = + (hash_table_size < (1u << 10)) ? 0 : sizeof(int) * hash_table_size; + size_t cmdbuf_size = params.quality == FAST_TWO_PASS_COMPRESSION_QUALITY ? + 5 * BROTLI_MIN(size_t, block_size, 1ul << 17) : 0; + if (params.quality == FAST_ONE_PASS_COMPRESSION_QUALITY) { + state_size += sizeof(BrotliOnePassArena); + } else { + state_size += sizeof(BrotliTwoPassArena); + } + return hash_size + cmdbuf_size + state_size; + } else { + size_t short_ringbuffer_size = (size_t)1 << params.lgblock; + int ringbuffer_bits = ComputeRbBits(¶ms); + size_t ringbuffer_size = input_size < short_ringbuffer_size ? + input_size : ((size_t)1u << ringbuffer_bits) + short_ringbuffer_size; + size_t hash_size[4] = {0}; + size_t metablock_size = + BROTLI_MIN(size_t, input_size, MaxMetablockSize(¶ms)); + size_t inputblock_size = + BROTLI_MIN(size_t, input_size, (size_t)1 << params.lgblock); + size_t cmdbuf_size = metablock_size * 2 + inputblock_size * 6; + size_t outbuf_size = metablock_size * 2 + 503; + size_t histogram_size = 0; + HasherSize(¶ms, BROTLI_TRUE, input_size, hash_size); + if (params.quality < MIN_QUALITY_FOR_BLOCK_SPLIT) { + cmdbuf_size = BROTLI_MIN(size_t, cmdbuf_size, + MAX_NUM_DELAYED_SYMBOLS * sizeof(Command) + inputblock_size * 12); + } + if (params.quality >= MIN_QUALITY_FOR_HQ_BLOCK_SPLITTING) { + /* Only a very rough estimation, based on enwik8. */ + histogram_size = 200 << 20; + } else if (params.quality >= MIN_QUALITY_FOR_BLOCK_SPLIT) { + size_t literal_histograms = + BROTLI_MIN(size_t, metablock_size / 6144, 256); + size_t command_histograms = + BROTLI_MIN(size_t, metablock_size / 6144, 256); + size_t distance_histograms = + BROTLI_MIN(size_t, metablock_size / 6144, 256); + histogram_size = literal_histograms * sizeof(HistogramLiteral) + + command_histograms * sizeof(HistogramCommand) + + distance_histograms * sizeof(HistogramDistance); + } + return (memory_manager_size + ringbuffer_size + + hash_size[0] + hash_size[1] + hash_size[2] + hash_size[3] + + cmdbuf_size + + outbuf_size + + histogram_size); + } +} +size_t BROTLI_COLD BrotliEncoderGetPreparedDictionarySize( + const BrotliEncoderPreparedDictionary* prepared_dictionary) { + /* First field of dictionary structs */ + const BrotliEncoderPreparedDictionary* prepared = prepared_dictionary; + uint32_t magic = *((const uint32_t*)prepared); + size_t overhead = 0; + if (magic == kManagedDictionaryMagic) { + const ManagedDictionary* managed = (const ManagedDictionary*)prepared; + overhead = sizeof(ManagedDictionary); + magic = *managed->dictionary; + prepared = (const BrotliEncoderPreparedDictionary*)managed->dictionary; + } + + if (magic == kPreparedDictionaryMagic) { + const PreparedDictionary* dictionary = + (const PreparedDictionary*)prepared; + /* Keep in sync with step 3 of CreatePreparedDictionary */ + return sizeof(PreparedDictionary) + dictionary->source_size + + (sizeof(uint32_t) << dictionary->slot_bits) + + (sizeof(uint16_t) << dictionary->bucket_bits) + + (sizeof(uint32_t) * dictionary->num_items) + overhead; + } else if (magic == kLeanPreparedDictionaryMagic) { + const PreparedDictionary* dictionary = + (const PreparedDictionary*)prepared; + /* Keep in sync with step 3 of CreatePreparedDictionary */ + return sizeof(PreparedDictionary) + sizeof(uint8_t*) + + (sizeof(uint32_t) << dictionary->slot_bits) + + (sizeof(uint16_t) << dictionary->bucket_bits) + + (sizeof(uint32_t) * dictionary->num_items) + overhead; + } else if (magic == kSharedDictionaryMagic) { + const SharedEncoderDictionary* dictionary = + (const SharedEncoderDictionary*)prepared; + const CompoundDictionary* compound = &dictionary->compound; + const ContextualEncoderDictionary* contextual = &dictionary->contextual; + size_t result = sizeof(*dictionary); + size_t i; + size_t num_instances; + const BrotliEncoderDictionary* instances; + for (i = 0; i < compound->num_prepared_instances_; i++) { + size_t size = BrotliEncoderGetPreparedDictionarySize( + (const BrotliEncoderPreparedDictionary*) + compound->prepared_instances_[i]); + if (!size) return 0; /* error */ + result += size; + } + if (contextual->context_based) { + num_instances = contextual->num_instances_; + instances = contextual->instances_; + result += sizeof(*instances) * num_instances; + } else { + num_instances = 1; + instances = &contextual->instance_; + } + for (i = 0; i < num_instances; i++) { + const BrotliEncoderDictionary* dict = &instances[i]; + result += dict->trie.pool_capacity * sizeof(BrotliTrieNode); + if (dict->hash_table_data_words_) { + result += sizeof(kStaticDictionaryHashWords); + } + if (dict->hash_table_data_lengths_) { + result += sizeof(kStaticDictionaryHashLengths); + } + if (dict->buckets_data_) { + result += sizeof(*dict->buckets_data_) * dict->buckets_alloc_size_; + } + if (dict->dict_words_data_) { + result += sizeof(*dict->dict_words) * dict->dict_words_alloc_size_; + } + if (dict->words_instance_) { + result += sizeof(*dict->words_instance_); + /* data_size not added here: it is never allocated by the + SharedEncoderDictionary, instead it always points to the file + already loaded in memory. So if the caller wants to include + this memory as well, add the size of the loaded dictionary + file to this. */ + } + } + return result + overhead; + } + return 0; /* error */ +} + +#if defined(BROTLI_TEST) +size_t BrotliMakeUncompressedStreamForTest(const uint8_t*, size_t, uint8_t*); +size_t BrotliMakeUncompressedStreamForTest( + const uint8_t* input, size_t input_size, uint8_t* output) { + return MakeUncompressedStream(input, input_size, output); +} +#endif + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif diff --git a/packages/runtime/vendor/brotli/c/enc/encoder_dict.c b/packages/runtime/vendor/brotli/c/enc/encoder_dict.c new file mode 100644 index 000000000..f75ee5984 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/encoder_dict.c @@ -0,0 +1,642 @@ +/* Copyright 2017 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +#include "encoder_dict.h" + +#include "../common/dictionary.h" +#include "../common/platform.h" +#include +#include "../common/shared_dictionary_internal.h" +#include "../common/transform.h" +#include +#include "compound_dictionary.h" +#include "dictionary_hash.h" +#include "hash_base.h" +#include "hash.h" +#include "memory.h" +#include "quality.h" +#include "static_dict_lut.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +#define NUM_HASH_BITS 15u +#define NUM_HASH_BUCKETS (1u << NUM_HASH_BITS) + +static void BrotliTrieInit(BrotliTrie* trie) { + trie->pool_capacity = 0; + trie->pool_size = 0; + trie->pool = 0; + + /* Set up the root node */ + trie->root.single = 0; + trie->root.len_ = 0; + trie->root.idx_ = 0; + trie->root.sub = 0; +} + +static void BrotliTrieFree(MemoryManager* m, BrotliTrie* trie) { + BrotliFree(m, trie->pool); +} + +/* Initializes to RFC 7932 static dictionary / transforms. */ +static void InitEncoderDictionary(BrotliEncoderDictionary* dict) { + dict->words = BrotliGetDictionary(); + dict->num_transforms = (uint32_t)BrotliGetTransforms()->num_transforms; + + dict->hash_table_words = kStaticDictionaryHashWords; + dict->hash_table_lengths = kStaticDictionaryHashLengths; + dict->buckets = kStaticDictionaryBuckets; + dict->dict_words = kStaticDictionaryWords; + + dict->cutoffTransformsCount = kCutoffTransformsCount; + dict->cutoffTransforms = kCutoffTransforms; + + dict->parent = 0; + + dict->hash_table_data_words_ = 0; + dict->hash_table_data_lengths_ = 0; + dict->buckets_alloc_size_ = 0; + dict->buckets_data_ = 0; + dict->dict_words_alloc_size_ = 0; + dict->dict_words_data_ = 0; + dict->words_instance_ = 0; + dict->has_words_heavy = BROTLI_FALSE; + BrotliTrieInit(&dict->trie); +} + +static void BrotliDestroyEncoderDictionary(MemoryManager* m, + BrotliEncoderDictionary* dict) { + BrotliFree(m, dict->hash_table_data_words_); + BrotliFree(m, dict->hash_table_data_lengths_); + BrotliFree(m, dict->buckets_data_); + BrotliFree(m, dict->dict_words_data_); + BrotliFree(m, dict->words_instance_); + BrotliTrieFree(m, &dict->trie); +} + +#if defined(BROTLI_EXPERIMENTAL) +/* Word length must be at least 4 bytes */ +static uint32_t Hash(const uint8_t* data, int bits) { + uint32_t h = BROTLI_UNALIGNED_LOAD32LE(data) * kHashMul32; + /* The higher bits contain more mixture from the multiplication, + so we take our results from there. */ + return h >> (32 - bits); +} + +/* Theoretical max possible word size after transform */ +#define kTransformedBufferSize \ + (256 + 256 + SHARED_BROTLI_MAX_DICTIONARY_WORD_LENGTH) + +/* To be safe buffer must have at least kTransformedBufferSize */ +static void TransformedDictionaryWord(uint32_t word_idx, int len, int transform, + const BrotliTransforms* transforms, + const BrotliEncoderDictionary* dict, + uint8_t* buffer, size_t* size) { + const uint8_t* dict_word = &dict->words->data[ + dict->words->offsets_by_length[len] + (uint32_t)len * word_idx]; + *size = (size_t)BrotliTransformDictionaryWord(buffer, dict_word, len, + transforms, transform); +} + +static DictWord MakeDictWord(uint8_t len, uint8_t transform, uint16_t idx) { + DictWord result; + result.len = len; + result.transform = transform; + result.idx = idx; + return result; +} + +static uint32_t BrotliTrieAlloc(MemoryManager* m, size_t num, BrotliTrie* trie, + BrotliTrieNode** keep) { + uint32_t result; + uint32_t keep_index = 0; + if (keep && *keep != &trie->root) { + /* Optional node to keep, since address may change after re-allocating */ + keep_index = (uint32_t)(*keep - trie->pool); + } + if (trie->pool_size == 0) { + /* Have a placeholder node in the front. We do not want the result to be 0, + it must be at least 1, 0 represents "null pointer" */ + trie->pool_size = 1; + } + BROTLI_ENSURE_CAPACITY(m, BrotliTrieNode, trie->pool, trie->pool_capacity, + trie->pool_size + num); + if (BROTLI_IS_OOM(m)) return 0; + /* Init the new nodes to empty */ + memset(trie->pool + trie->pool_size, 0, sizeof(*trie->pool) * num); + result = (uint32_t)trie->pool_size; + trie->pool_size += num; + if (keep && *keep != &trie->root) { + *keep = trie->pool + keep_index; + } + return result; +} + +/** + * len and idx: payload for last node + * word, size: the string + * index: position in the string + */ +static BROTLI_BOOL BrotliTrieNodeAdd(MemoryManager* m, uint8_t len, + uint32_t idx, const uint8_t* word, size_t size, int index, + BrotliTrieNode* node, BrotliTrie* trie) { + BrotliTrieNode* child = 0; + uint8_t c; + if ((size_t)index == size) { + if (!node->len_ || idx < node->idx_) { + node->len_ = len; + node->idx_ = idx; + } + return BROTLI_TRUE; + } + c = word[index]; + if (node->single && c != node->c) { + BrotliTrieNode old = trie->pool[node->sub]; + uint32_t new_nodes = BrotliTrieAlloc(m, 32, trie, &node); + if (BROTLI_IS_OOM(m)) return BROTLI_FALSE; + node->single = 0; + node->sub = new_nodes; + trie->pool[node->sub + (node->c >> 4)].sub = new_nodes + 16; + trie->pool[trie->pool[node->sub + (node->c >> 4)].sub + (node->c & 15)] = + old; + } + if (!node->sub) { + uint32_t new_node = BrotliTrieAlloc(m, 1, trie, &node); + if (BROTLI_IS_OOM(m)) return BROTLI_FALSE; + node->single = 1; + node->c = c; + node->sub = new_node; + } + if (node->single) { + child = &trie->pool[node->sub]; + } else { + if (!trie->pool[node->sub + (c >> 4)].sub) { + uint32_t new_nodes = BrotliTrieAlloc(m, 16, trie, &node); + if (BROTLI_IS_OOM(m)) return BROTLI_FALSE; + trie->pool[node->sub + (c >> 4)].sub = new_nodes; + } + child = &trie->pool[trie->pool[node->sub + (c >> 4)].sub + (c & 15)]; + } + return BrotliTrieNodeAdd(m, len, idx, word, size, index + 1, child, trie); +} + +static BROTLI_BOOL BrotliTrieAdd(MemoryManager* m, uint8_t len, uint32_t idx, + const uint8_t* word, size_t size, BrotliTrie* trie) { + return BrotliTrieNodeAdd(m, len, idx, word, size, 0, &trie->root, trie); +} + +const BrotliTrieNode* BrotliTrieSub(const BrotliTrie* trie, + const BrotliTrieNode* node, uint8_t c) { + BrotliTrieNode* temp_node; + if (node->single) { + if (node->c == c) return &trie->pool[node->sub]; + return 0; + } + if (!node->sub) return 0; + temp_node = &trie->pool[node->sub + (c >> 4)]; + if (!temp_node->sub) return 0; + return &trie->pool[temp_node->sub + (c & 15)]; +} + +static const BrotliTrieNode* BrotliTrieFind(const BrotliTrie* trie, + const uint8_t* word, size_t size) { + const BrotliTrieNode* node = &trie->root; + size_t i; + for (i = 0; i < size; i++) { + node = BrotliTrieSub(trie, node, word[i]); + if (!node) return 0; + } + return node; +} + +static BROTLI_BOOL BuildDictionaryLut(MemoryManager* m, + const BrotliTransforms* transforms, + BrotliEncoderDictionary* dict) { + uint32_t i; + DictWord* dict_words; + uint16_t* buckets; + DictWord** words_by_hash; + size_t* words_by_hash_size; + size_t* words_by_hash_capacity; + BrotliTrie dedup; + uint8_t word[kTransformedBufferSize]; + size_t word_size; + size_t total = 0; + uint8_t l; + uint16_t idx; + + BrotliTrieInit(&dedup); + + words_by_hash = (DictWord**)BrotliAllocate(m, + sizeof(*words_by_hash) * NUM_HASH_BUCKETS); + words_by_hash_size = (size_t*)BrotliAllocate(m, + sizeof(*words_by_hash_size) * NUM_HASH_BUCKETS); + words_by_hash_capacity = (size_t*)BrotliAllocate(m, + sizeof(*words_by_hash_capacity) * NUM_HASH_BUCKETS); + if (BROTLI_IS_OOM(m)) return BROTLI_FALSE; + memset(words_by_hash, 0, sizeof(*words_by_hash) * NUM_HASH_BUCKETS); + memset(words_by_hash_size, 0, sizeof(*words_by_hash_size) * NUM_HASH_BUCKETS); + memset(words_by_hash_capacity, 0, + sizeof(*words_by_hash_capacity) * NUM_HASH_BUCKETS); + + if (transforms->num_transforms > 0) { + for (l = SHARED_BROTLI_MIN_DICTIONARY_WORD_LENGTH; + l <= SHARED_BROTLI_MAX_DICTIONARY_WORD_LENGTH; ++l) { + uint16_t n = dict->words->size_bits_by_length[l] ? + (uint16_t)(1 << dict->words->size_bits_by_length[l]) : 0u; + for (idx = 0; idx < n; ++idx) { + uint32_t key; + /* First transform (usually identity) */ + TransformedDictionaryWord(idx, l, 0, transforms, dict, word, + &word_size); + /* Cannot hash words smaller than 4 bytes */ + if (word_size < 4) { + /* Break instead of continue, all next words of this length will have + same length after transform */ + break; + } + if (!BrotliTrieAdd(m, 0, idx, word, word_size, &dedup)) { + return BROTLI_FALSE; + } + key = Hash(word, NUM_HASH_BITS); + BROTLI_ENSURE_CAPACITY_APPEND(m, DictWord, words_by_hash[key], + words_by_hash_capacity[key], words_by_hash_size[key], + MakeDictWord(l, 0, idx)); + ++total; + } + } + } + + /* These LUT transforms only supported if no custom transforms. This is + ok, we will use the heavy trie instead. */ + if (transforms == BrotliGetTransforms()) { + for (l = SHARED_BROTLI_MIN_DICTIONARY_WORD_LENGTH; + l <= SHARED_BROTLI_MAX_DICTIONARY_WORD_LENGTH; ++l) { + uint16_t n = dict->words->size_bits_by_length[l] ? + (uint16_t)(1 << dict->words->size_bits_by_length[l]) : 0u; + for (idx = 0; idx < n; ++idx) { + int k; + BROTLI_BOOL is_ascii = BROTLI_TRUE; + size_t offset = dict->words->offsets_by_length[l] + (size_t)l * idx; + const uint8_t* data = &dict->words->data[offset]; + for (k = 0; k < l; ++k) { + if (data[k] >= 128) is_ascii = BROTLI_FALSE; + } + if (data[0] < 128) { + int transform = 9; /* {empty, uppercase first, empty} */ + uint32_t ix = idx + (uint32_t)transform * n; + const BrotliTrieNode* it; + TransformedDictionaryWord(idx, l, transform, transforms, + dict, word, &word_size); + it = BrotliTrieFind(&dedup, word, word_size); + if (!it || it->idx_ > ix) { + uint32_t key = Hash(word, NUM_HASH_BITS); + if (!BrotliTrieAdd(m, 0, ix, word, word_size, &dedup)) { + return BROTLI_FALSE; + } + BROTLI_ENSURE_CAPACITY_APPEND(m, DictWord, words_by_hash[key], + words_by_hash_capacity[key], words_by_hash_size[key], + MakeDictWord(l, BROTLI_TRANSFORM_UPPERCASE_FIRST, idx)); + ++total; + } + } + if (is_ascii) { + int transform = 44; /* {empty, uppercase all, empty} */ + uint32_t ix = idx + (uint32_t)transform * n; + const BrotliTrieNode* it; + TransformedDictionaryWord(idx, l, transform, transforms, + dict, word, &word_size); + it = BrotliTrieFind(&dedup, word, word_size); + if (!it || it->idx_ > ix) { + uint32_t key = Hash(word, NUM_HASH_BITS); + if (!BrotliTrieAdd(m, 0, ix, word, word_size, &dedup)) { + return BROTLI_FALSE; + } + BROTLI_ENSURE_CAPACITY_APPEND(m, DictWord, words_by_hash[key], + words_by_hash_capacity[key], words_by_hash_size[key], + MakeDictWord(l, BROTLI_TRANSFORM_UPPERCASE_ALL, idx)); + ++total; + } + } + } + } + } + + dict_words = (DictWord*)BrotliAllocate(m, + sizeof(*dict->dict_words) * (total + 1)); + buckets = (uint16_t*)BrotliAllocate(m, + sizeof(*dict->buckets) * NUM_HASH_BUCKETS); + if (BROTLI_IS_OOM(m)) return BROTLI_FALSE; + dict->dict_words_alloc_size_ = total + 1; + dict->dict_words = dict->dict_words_data_ = dict_words; + dict->buckets_alloc_size_ = NUM_HASH_BUCKETS; + dict->buckets = dict->buckets_data_ = buckets; + + /* Unused; makes offsets start from 1. */ + dict_words[0] = MakeDictWord(0, 0, 0); + total = 1; + for (i = 0; i < NUM_HASH_BUCKETS; ++i) { + size_t num_words = words_by_hash_size[i]; + if (num_words > 0) { + buckets[i] = (uint16_t)(total); + memcpy(&dict_words[total], &words_by_hash[i][0], + sizeof(dict_words[0]) * num_words); + total += num_words; + dict_words[total - 1].len |= 0x80; + } else { + buckets[i] = 0; + } + } + + for (i = 0; i < NUM_HASH_BUCKETS; ++i) { + BrotliFree(m, words_by_hash[i]); + } + BrotliFree(m, words_by_hash); + BrotliFree(m, words_by_hash_size); + BrotliFree(m, words_by_hash_capacity); + BrotliTrieFree(m, &dedup); + + return BROTLI_TRUE; +} + +static void BuildDictionaryHashTable(uint16_t* hash_table_words, + uint8_t* hash_table_lengths, const BrotliDictionary* dict) { + int j, len; + /* The order of the loops is such that in case of collision, words with + shorter length are preferred, and in case of same length, words with + smaller index. There is only a single word per bucket. */ + /* TODO(lode): consider adding optional user-supplied frequency_map to use + for preferred words instead, this can make the encoder better for + quality 9 and below without affecting the decoder */ + memset(hash_table_words, 0, sizeof(kStaticDictionaryHashWords)); + memset(hash_table_lengths, 0, sizeof(kStaticDictionaryHashLengths)); + for (len = SHARED_BROTLI_MAX_DICTIONARY_WORD_LENGTH; + len >= SHARED_BROTLI_MIN_DICTIONARY_WORD_LENGTH; --len) { + const size_t num_words = dict->size_bits_by_length[len] ? + (1u << dict->size_bits_by_length[len]) : 0; + for (j = (int)num_words - 1; j >= 0; --j) { + size_t offset = dict->offsets_by_length[len] + + (size_t)len * (size_t)j; + const uint8_t* word = &dict->data[offset]; + const uint32_t key = Hash(word, 14); + int idx = (int)(key << 1) + (len < 8 ? 1 : 0); + BROTLI_DCHECK(idx < (int)NUM_HASH_BUCKETS); + hash_table_words[idx] = (uint16_t)j; + hash_table_lengths[idx] = (uint8_t)len; + } + } +} + +static BROTLI_BOOL GenerateWordsHeavy(MemoryManager* m, + const BrotliTransforms* transforms, + BrotliEncoderDictionary* dict) { + int i, j, l; + for (j = (int)transforms->num_transforms - 1; j >= 0 ; --j) { + for (l = 0; l < 32; l++) { + int num = (int)((1u << dict->words->size_bits_by_length[l]) & ~1u); + for (i = 0; i < num; i++) { + uint8_t transformed[kTransformedBufferSize]; + size_t size; + TransformedDictionaryWord( + (uint32_t)i, l, j, transforms, dict, transformed, &size); + if (size < 4) continue; + if (!BrotliTrieAdd(m, (uint8_t)l, (uint32_t)(i + num * j), + transformed, size, &dict->trie)) { + return BROTLI_FALSE; + } + } + } + } + return BROTLI_TRUE; +} + +/* Computes cutoffTransformsCount (in count) and cutoffTransforms (in data) for + the custom transforms, where possible within the limits of the + cutoffTransforms encoding. The fast encoder uses this to do fast lookup for + transforms that remove the N last characters (OmitLast). */ +static void ComputeCutoffTransforms( + const BrotliTransforms* transforms, + uint32_t* count, uint64_t* data) { + int i; + /* The encoding in a 64-bit integer of transform N in the data is: (N << 2) + + ((cutoffTransforms >> (N * 6)) & 0x3F), so for example the identity + transform code must be 0-63, for N=1 the transform code must be 4-67, ..., + for N=9 it must be 36-99. + TODO(lode): consider a simple flexible uint8_t[10] instead of the uint64_t + for the cutoff transforms, so that shared dictionaries can have the + OmitLast transforms anywhere without loss. */ + *count = 0; + *data = 0; + for (i = 0; i < BROTLI_TRANSFORMS_MAX_CUT_OFF + 1; i++) { + int idx = transforms->cutOffTransforms[i]; + if (idx == -1) break; /* Not found */ + if (idx < (i << 2)) break; /* Too small for the encoding */ + if (idx >= (i << 2) + 64) break; /* Too large for the encoding */ + (*count)++; + *data |= (uint64_t)(((uint64_t)idx - + ((uint64_t)i << 2u)) << ((uint64_t)i * 6u)); + } +} + +static BROTLI_BOOL ComputeDictionary(MemoryManager* m, int quality, + const BrotliTransforms* transforms, + BrotliEncoderDictionary* current) { + int default_words = current->words == BrotliGetDictionary(); + int default_transforms = transforms == BrotliGetTransforms(); + + if (default_words && default_transforms) { + /* hashes are already set to Brotli defaults */ + return BROTLI_TRUE; + } + + current->hash_table_data_words_ = (uint16_t*)BrotliAllocate( + m, sizeof(kStaticDictionaryHashWords)); + current->hash_table_data_lengths_ = (uint8_t*)BrotliAllocate( + m, sizeof(kStaticDictionaryHashLengths)); + if (BROTLI_IS_OOM(m)) return BROTLI_FALSE; + current->hash_table_words = current->hash_table_data_words_; + current->hash_table_lengths = current->hash_table_data_lengths_; + + BuildDictionaryHashTable(current->hash_table_data_words_, + current->hash_table_data_lengths_, current->words); + + ComputeCutoffTransforms(transforms, + ¤t->cutoffTransformsCount, ¤t->cutoffTransforms); + + /* Only compute the data for slow encoder if the requested quality is high + enough to need it */ + if (quality >= ZOPFLIFICATION_QUALITY) { + if (!BuildDictionaryLut(m, transforms, current)) return BROTLI_FALSE; + + /* For the built-in Brotli transforms, there is a hard-coded function to + handle all transforms, but for custom transforms, we use the following + large hammer instead */ + current->has_words_heavy = !default_transforms; + if (current->has_words_heavy) { + if (!GenerateWordsHeavy(m, transforms, current)) return BROTLI_FALSE; + } + } + + return BROTLI_TRUE; +} +#endif /* BROTLI_EXPERIMENTAL */ + +void BrotliInitSharedEncoderDictionary(SharedEncoderDictionary* dict) { + dict->magic = kSharedDictionaryMagic; + + dict->compound.num_chunks = 0; + dict->compound.total_size = 0; + dict->compound.chunk_offsets[0] = 0; + dict->compound.num_prepared_instances_ = 0; + + dict->contextual.context_based = 0; + dict->contextual.num_dictionaries = 1; + dict->contextual.instances_ = 0; + dict->contextual.num_instances_ = 1; /* The instance_ field */ + dict->contextual.dict[0] = &dict->contextual.instance_; + InitEncoderDictionary(&dict->contextual.instance_); + dict->contextual.instance_.parent = &dict->contextual; + + dict->max_quality = BROTLI_MAX_QUALITY; +} + +#if defined(BROTLI_EXPERIMENTAL) +/* TODO(eustas): make sure that tooling will warn user if not all the cutoff + transforms are available (for low-quality encoder). */ +static BROTLI_BOOL InitCustomSharedEncoderDictionary( + MemoryManager* m, const BrotliSharedDictionary* decoded_dict, + int quality, SharedEncoderDictionary* dict) { + ContextualEncoderDictionary* contextual; + CompoundDictionary* compound; + BrotliEncoderDictionary* instances; + int i; + BrotliInitSharedEncoderDictionary(dict); + + contextual = &dict->contextual; + compound = &dict->compound; + + for (i = 0; i < (int)decoded_dict->num_prefix; i++) { + PreparedDictionary* prepared = CreatePreparedDictionary(m, + decoded_dict->prefix[i], decoded_dict->prefix_size[i]); + AttachPreparedDictionary(compound, prepared); + /* remember for cleanup */ + compound->prepared_instances_[ + compound->num_prepared_instances_++] = prepared; + } + + dict->max_quality = quality; + contextual->context_based = decoded_dict->context_based; + if (decoded_dict->context_based) { + memcpy(contextual->context_map, decoded_dict->context_map, + SHARED_BROTLI_NUM_DICTIONARY_CONTEXTS); + } + + contextual->num_dictionaries = decoded_dict->num_dictionaries; + contextual->num_instances_ = decoded_dict->num_dictionaries; + if (contextual->num_instances_ == 1) { + instances = &contextual->instance_; + } else { + contextual->instances_ = (BrotliEncoderDictionary*) + BrotliAllocate(m, sizeof(*contextual->instances_) * + contextual->num_instances_); + if (BROTLI_IS_OOM(m)) return BROTLI_FALSE; + instances = contextual->instances_; + } + for (i = 0; i < (int)contextual->num_instances_; i++) { + BrotliEncoderDictionary* current = &instances[i]; + InitEncoderDictionary(current); + current->parent = &dict->contextual; + if (decoded_dict->words[i] == BrotliGetDictionary()) { + current->words = BrotliGetDictionary(); + } else { + current->words_instance_ = (BrotliDictionary*)BrotliAllocate( + m, sizeof(BrotliDictionary)); + if (BROTLI_IS_OOM(m)) return BROTLI_FALSE; + *current->words_instance_ = *decoded_dict->words[i]; + current->words = current->words_instance_; + } + current->num_transforms = + (uint32_t)decoded_dict->transforms[i]->num_transforms; + if (!ComputeDictionary( + m, quality, decoded_dict->transforms[i], current)) { + return BROTLI_FALSE; + } + + contextual->dict[i] = current; + } + + return BROTLI_TRUE; /* success */ +} + +BROTLI_BOOL BrotliInitCustomSharedEncoderDictionary( + MemoryManager* m, const uint8_t* encoded_dict, size_t size, + int quality, SharedEncoderDictionary* dict) { + BROTLI_BOOL success = BROTLI_FALSE; + BrotliSharedDictionary* decoded_dict = BrotliSharedDictionaryCreateInstance( + m->alloc_func, m->free_func, m->opaque); + if (!decoded_dict) { /* OOM */ + return BROTLI_FALSE; + } + success = BrotliSharedDictionaryAttach( + decoded_dict, BROTLI_SHARED_DICTIONARY_SERIALIZED, size, encoded_dict); + if (success) { + success = InitCustomSharedEncoderDictionary(m, + decoded_dict, quality, dict); + } + BrotliSharedDictionaryDestroyInstance(decoded_dict); + return success; +} +#endif /* BROTLI_EXPERIMENTAL */ + +void BrotliCleanupSharedEncoderDictionary(MemoryManager* m, + SharedEncoderDictionary* dict) { + size_t i; + for (i = 0; i < dict->compound.num_prepared_instances_; i++) { + DestroyPreparedDictionary(m, + (PreparedDictionary*)dict->compound.prepared_instances_[i]); + } + if (dict->contextual.num_instances_ == 1) { + BrotliDestroyEncoderDictionary(m, &dict->contextual.instance_); + } else if (dict->contextual.num_instances_ > 1) { + for (i = 0; i < dict->contextual.num_instances_; i++) { + BrotliDestroyEncoderDictionary(m, &dict->contextual.instances_[i]); + } + BrotliFree(m, dict->contextual.instances_); + } +} + +ManagedDictionary* BrotliCreateManagedDictionary( + brotli_alloc_func alloc_func, brotli_free_func free_func, void* opaque) { + ManagedDictionary* result = (ManagedDictionary*)BrotliBootstrapAlloc( + sizeof(ManagedDictionary), alloc_func, free_func, opaque); + if (result == NULL) return NULL; + + result->magic = kManagedDictionaryMagic; + BrotliInitMemoryManager( + &result->memory_manager_, alloc_func, free_func, opaque); + result->dictionary = NULL; + + return result; +} + +void BrotliDestroyManagedDictionary(ManagedDictionary* dictionary) { + if (!dictionary) return; + BrotliBootstrapFree(dictionary, &dictionary->memory_manager_); +} + +/* Escalate internal functions visibility; for testing purposes only. */ +#if defined(BROTLI_TEST) +void BrotliInitEncoderDictionaryForTest(BrotliEncoderDictionary*); +void BrotliInitEncoderDictionaryForTest(BrotliEncoderDictionary* d) { + InitEncoderDictionary(d); +} +#endif + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif diff --git a/packages/runtime/vendor/brotli/c/enc/encoder_dict.h b/packages/runtime/vendor/brotli/c/enc/encoder_dict.h new file mode 100644 index 000000000..76f037977 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/encoder_dict.h @@ -0,0 +1,155 @@ +/* Copyright 2017 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +#ifndef BROTLI_ENC_ENCODER_DICT_H_ +#define BROTLI_ENC_ENCODER_DICT_H_ + +#include "../common/dictionary.h" +#include +#include "../common/platform.h" +#include "compound_dictionary.h" +#include "memory.h" +#include "static_dict_lut.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +/* +Dictionary hierarchy for Encoder: +-SharedEncoderDictionary +--CompoundDictionary +---PreparedDictionary [up to 15x] + = prefix dictionary with precomputed hashes +--ContextualEncoderDictionary +---BrotliEncoderDictionary [up to 64x] + = for each context, precomputed static dictionary with words + transforms + +Dictionary hierarchy from common: similar, but without precomputed hashes +-BrotliSharedDictionary +--BrotliDictionary [up to 64x] +--BrotliTransforms [up to 64x] +--const uint8_t* prefix [up to 15x]: compound dictionaries +*/ + +typedef struct BrotliTrieNode { + uint8_t single; /* if 1, sub is a single node for c instead of 256 */ + uint8_t c; + uint8_t len_; /* untransformed length */ + uint32_t idx_; /* word index + num words * transform index */ + uint32_t sub; /* index of sub node(s) in the pool */ +} BrotliTrieNode; + +typedef struct BrotliTrie { + BrotliTrieNode* pool; + size_t pool_capacity; + size_t pool_size; + BrotliTrieNode root; +} BrotliTrie; + +#if defined(BROTLI_EXPERIMENTAL) +BROTLI_INTERNAL const BrotliTrieNode* BrotliTrieSub(const BrotliTrie* trie, + const BrotliTrieNode* node, uint8_t c); +#endif /* BROTLI_EXPERIMENTAL */ + +/* Dictionary data (words and transforms) for 1 possible context */ +typedef struct BrotliEncoderDictionary { + const BrotliDictionary* words; + uint32_t num_transforms; + + /* cut off for fast encoder */ + uint32_t cutoffTransformsCount; + uint64_t cutoffTransforms; + + /* from dictionary_hash.h, for fast encoder */ + const uint16_t* hash_table_words; + const uint8_t* hash_table_lengths; + + /* from static_dict_lut.h, for slow encoder */ + const uint16_t* buckets; + const DictWord* dict_words; + /* Heavy version, for use by slow encoder when there are custom transforms. + Contains every possible transformed dictionary word in a trie. It encodes + about as fast as the non-heavy encoder but consumes a lot of memory and + takes time to build. */ + BrotliTrie trie; + BROTLI_BOOL has_words_heavy; + + /* Reference to other dictionaries. */ + const struct ContextualEncoderDictionary* parent; + + /* Allocated memory, used only when not using the Brotli defaults */ + uint16_t* hash_table_data_words_; + uint8_t* hash_table_data_lengths_; + size_t buckets_alloc_size_; + uint16_t* buckets_data_; + size_t dict_words_alloc_size_; + DictWord* dict_words_data_; + BrotliDictionary* words_instance_; +} BrotliEncoderDictionary; + +/* Dictionary data for all 64 contexts */ +typedef struct ContextualEncoderDictionary { + BROTLI_BOOL context_based; + uint8_t num_dictionaries; + uint8_t context_map[SHARED_BROTLI_NUM_DICTIONARY_CONTEXTS]; + const BrotliEncoderDictionary* dict[SHARED_BROTLI_NUM_DICTIONARY_CONTEXTS]; + + /* If num_instances_ is 1, instance_ is used, else dynamic allocation with + instances_ is used. */ + size_t num_instances_; + BrotliEncoderDictionary instance_; + BrotliEncoderDictionary* instances_; +} ContextualEncoderDictionary; + +typedef struct SharedEncoderDictionary { + /* Magic value to distinguish this struct from PreparedDictionary for + certain external usages. */ + uint32_t magic; + + /* LZ77 prefix, compound dictionary */ + CompoundDictionary compound; + + /* Custom static dictionary (optionally context-based) */ + ContextualEncoderDictionary contextual; + + /* The maximum quality the dictionary was computed for */ + int max_quality; +} SharedEncoderDictionary; + +typedef struct ManagedDictionary { + uint32_t magic; + MemoryManager memory_manager_; + uint32_t* dictionary; +} ManagedDictionary; + +/* Initializes to the brotli built-in dictionary */ +BROTLI_INTERNAL void BrotliInitSharedEncoderDictionary( + SharedEncoderDictionary* dict); + +#if defined(BROTLI_EXPERIMENTAL) +/* Initializes to shared dictionary that will be parsed from + encoded_dict. Requires that you keep the encoded_dict buffer + around, parts of data will point to it. */ +BROTLI_INTERNAL BROTLI_BOOL BrotliInitCustomSharedEncoderDictionary( + MemoryManager* m, const uint8_t* encoded_dict, size_t size, + int quality, SharedEncoderDictionary* dict); +#endif /* BROTLI_EXPERIMENTAL */ + +BROTLI_INTERNAL void BrotliCleanupSharedEncoderDictionary( + MemoryManager* m, SharedEncoderDictionary* dict); + +BROTLI_INTERNAL ManagedDictionary* BrotliCreateManagedDictionary( + brotli_alloc_func alloc_func, brotli_free_func free_func, void* opaque); + +BROTLI_INTERNAL void BrotliDestroyManagedDictionary( + ManagedDictionary* dictionary); + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif + +#endif /* BROTLI_ENC_ENCODER_DICT_H_ */ diff --git a/packages/runtime/vendor/brotli/c/enc/entropy_encode.c b/packages/runtime/vendor/brotli/c/enc/entropy_encode.c new file mode 100644 index 000000000..1b4eab97f --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/entropy_encode.c @@ -0,0 +1,501 @@ +/* Copyright 2010 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* Entropy encoding (Huffman) utilities. */ + +#include "entropy_encode.h" + +#include "../common/constants.h" +#include "../common/platform.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +const BROTLI_MODEL("small") size_t kBrotliShellGaps[] = {132, 57, 23, 10, 4, 1}; + +BROTLI_BOOL BrotliSetDepth( + int p0, HuffmanTree* pool, uint8_t* depth, int max_depth) { + int stack[16]; + int level = 0; + int p = p0; + BROTLI_DCHECK(max_depth <= 15); + stack[0] = -1; + while (BROTLI_TRUE) { + if (pool[p].index_left_ >= 0) { + level++; + if (level > max_depth) return BROTLI_FALSE; + stack[level] = pool[p].index_right_or_value_; + p = pool[p].index_left_; + continue; + } else { + depth[pool[p].index_right_or_value_] = (uint8_t)level; + } + while (level >= 0 && stack[level] == -1) level--; + if (level < 0) return BROTLI_TRUE; + p = stack[level]; + stack[level] = -1; + } +} + +/* Sort the root nodes, least popular first. */ +static BROTLI_INLINE BROTLI_BOOL SortHuffmanTree( + const HuffmanTree* v0, const HuffmanTree* v1) { + if (v0->total_count_ != v1->total_count_) { + return TO_BROTLI_BOOL(v0->total_count_ < v1->total_count_); + } + return TO_BROTLI_BOOL(v0->index_right_or_value_ > v1->index_right_or_value_); +} + +/* This function will create a Huffman tree. + + The catch here is that the tree cannot be arbitrarily deep. + Brotli specifies a maximum depth of 15 bits for "code trees" + and 7 bits for "code length code trees." + + count_limit is the value that is to be faked as the minimum value + and this minimum value is raised until the tree matches the + maximum length requirement. + + This algorithm is not of excellent performance for very long data blocks, + especially when population counts are longer than 2**tree_limit, but + we are not planning to use this with extremely long blocks. + + See http://en.wikipedia.org/wiki/Huffman_coding */ +void BrotliCreateHuffmanTree(const uint32_t* data, + const size_t length, + const int tree_limit, + HuffmanTree* tree, + uint8_t* depth) { + uint32_t count_limit; + HuffmanTree sentinel; + InitHuffmanTree(&sentinel, BROTLI_UINT32_MAX, -1, -1); + /* For block sizes below 64 kB, we never need to do a second iteration + of this loop. Probably all of our block sizes will be smaller than + that, so this loop is mostly of academic interest. If we actually + would need this, we would be better off with the Katajainen algorithm. */ + for (count_limit = 1; ; count_limit *= 2) { + size_t n = 0; + size_t i; + size_t j; + size_t k; + for (i = length; i != 0;) { + --i; + if (data[i]) { + const uint32_t count = BROTLI_MAX(uint32_t, data[i], count_limit); + InitHuffmanTree(&tree[n++], count, -1, (int16_t)i); + } + } + + if (n == 1) { + depth[tree[0].index_right_or_value_] = 1; /* Only one element. */ + break; + } + + SortHuffmanTreeItems(tree, n, SortHuffmanTree); + + /* The nodes are: + [0, n): the sorted leaf nodes that we start with. + [n]: we add a sentinel here. + [n + 1, 2n): new parent nodes are added here, starting from + (n+1). These are naturally in ascending order. + [2n]: we add a sentinel at the end as well. + There will be (2n+1) elements at the end. */ + tree[n] = sentinel; + tree[n + 1] = sentinel; + + i = 0; /* Points to the next leaf node. */ + j = n + 1; /* Points to the next non-leaf node. */ + for (k = n - 1; k != 0; --k) { + size_t left, right; + if (tree[i].total_count_ <= tree[j].total_count_) { + left = i; + ++i; + } else { + left = j; + ++j; + } + if (tree[i].total_count_ <= tree[j].total_count_) { + right = i; + ++i; + } else { + right = j; + ++j; + } + + { + /* The sentinel node becomes the parent node. */ + size_t j_end = 2 * n - k; + tree[j_end].total_count_ = + tree[left].total_count_ + tree[right].total_count_; + tree[j_end].index_left_ = (int16_t)left; + tree[j_end].index_right_or_value_ = (int16_t)right; + + /* Add back the last sentinel node. */ + tree[j_end + 1] = sentinel; + } + } + if (BrotliSetDepth((int)(2 * n - 1), &tree[0], depth, tree_limit)) { + /* We need to pack the Huffman tree in tree_limit bits. If this was not + successful, add fake entities to the lowest values and retry. */ + break; + } + } +} + +static void Reverse(uint8_t* v, size_t start, size_t end) { + --end; + while (start < end) { + uint8_t tmp = v[start]; + v[start] = v[end]; + v[end] = tmp; + ++start; + --end; + } +} + +static void BrotliWriteHuffmanTreeRepetitions( + const uint8_t previous_value, + const uint8_t value, + size_t repetitions, + size_t* tree_size, + uint8_t* tree, + uint8_t* extra_bits_data) { + BROTLI_DCHECK(repetitions > 0); + if (previous_value != value) { + tree[*tree_size] = value; + extra_bits_data[*tree_size] = 0; + ++(*tree_size); + --repetitions; + } + if (repetitions == 7) { + tree[*tree_size] = value; + extra_bits_data[*tree_size] = 0; + ++(*tree_size); + --repetitions; + } + if (repetitions < 3) { + size_t i; + for (i = 0; i < repetitions; ++i) { + tree[*tree_size] = value; + extra_bits_data[*tree_size] = 0; + ++(*tree_size); + } + } else { + size_t start = *tree_size; + repetitions -= 3; + while (BROTLI_TRUE) { + tree[*tree_size] = BROTLI_REPEAT_PREVIOUS_CODE_LENGTH; + extra_bits_data[*tree_size] = repetitions & 0x3; + ++(*tree_size); + repetitions >>= 2; + if (repetitions == 0) { + break; + } + --repetitions; + } + Reverse(tree, start, *tree_size); + Reverse(extra_bits_data, start, *tree_size); + } +} + +static void BrotliWriteHuffmanTreeRepetitionsZeros( + size_t repetitions, + size_t* tree_size, + uint8_t* tree, + uint8_t* extra_bits_data) { + if (repetitions == 11) { + tree[*tree_size] = 0; + extra_bits_data[*tree_size] = 0; + ++(*tree_size); + --repetitions; + } + if (repetitions < 3) { + size_t i; + for (i = 0; i < repetitions; ++i) { + tree[*tree_size] = 0; + extra_bits_data[*tree_size] = 0; + ++(*tree_size); + } + } else { + size_t start = *tree_size; + repetitions -= 3; + while (BROTLI_TRUE) { + tree[*tree_size] = BROTLI_REPEAT_ZERO_CODE_LENGTH; + extra_bits_data[*tree_size] = repetitions & 0x7; + ++(*tree_size); + repetitions >>= 3; + if (repetitions == 0) { + break; + } + --repetitions; + } + Reverse(tree, start, *tree_size); + Reverse(extra_bits_data, start, *tree_size); + } +} + +void BrotliOptimizeHuffmanCountsForRle(size_t length, uint32_t* counts, + uint8_t* good_for_rle) { + size_t nonzero_count = 0; + size_t stride; + size_t limit; + size_t sum; + const size_t streak_limit = 1240; + /* Let's make the Huffman code more compatible with RLE encoding. */ + size_t i; + for (i = 0; i < length; i++) { + if (counts[i]) { + ++nonzero_count; + } + } + if (nonzero_count < 16) { + return; + } + while (length != 0 && counts[length - 1] == 0) { + --length; + } + if (length == 0) { + return; /* All zeros. */ + } + /* Now counts[0..length - 1] does not have trailing zeros. */ + { + size_t nonzeros = 0; + uint32_t smallest_nonzero = 1 << 30; + for (i = 0; i < length; ++i) { + if (counts[i] != 0) { + ++nonzeros; + if (smallest_nonzero > counts[i]) { + smallest_nonzero = counts[i]; + } + } + } + if (nonzeros < 5) { + /* Small histogram will model it well. */ + return; + } + if (smallest_nonzero < 4) { + size_t zeros = length - nonzeros; + if (zeros < 6) { + for (i = 1; i < length - 1; ++i) { + if (counts[i - 1] != 0 && counts[i] == 0 && counts[i + 1] != 0) { + counts[i] = 1; + } + } + } + } + if (nonzeros < 28) { + return; + } + } + /* 2) Let's mark all population counts that already can be encoded + with an RLE code. */ + memset(good_for_rle, 0, length); + { + /* Let's not spoil any of the existing good RLE codes. + Mark any seq of 0's that is longer as 5 as a good_for_rle. + Mark any seq of non-0's that is longer as 7 as a good_for_rle. */ + uint32_t symbol = counts[0]; + size_t step = 0; + for (i = 0; i <= length; ++i) { + if (i == length || counts[i] != symbol) { + if ((symbol == 0 && step >= 5) || + (symbol != 0 && step >= 7)) { + size_t k; + for (k = 0; k < step; ++k) { + good_for_rle[i - k - 1] = 1; + } + } + step = 1; + if (i != length) { + symbol = counts[i]; + } + } else { + ++step; + } + } + } + /* 3) Let's replace those population counts that lead to more RLE codes. + Math here is in 24.8 fixed point representation. */ + stride = 0; + limit = 256 * (counts[0] + counts[1] + counts[2]) / 3 + 420; + sum = 0; + for (i = 0; i <= length; ++i) { + if (i == length || good_for_rle[i] || + (i != 0 && good_for_rle[i - 1]) || + (256 * counts[i] - limit + streak_limit) >= 2 * streak_limit) { + if (stride >= 4 || (stride >= 3 && sum == 0)) { + size_t k; + /* The stride must end, collapse what we have, if we have enough (4). */ + size_t count = (sum + stride / 2) / stride; + if (count == 0) { + count = 1; + } + if (sum == 0) { + /* Don't make an all zeros stride to be upgraded to ones. */ + count = 0; + } + for (k = 0; k < stride; ++k) { + /* We don't want to change value at counts[i], + that is already belonging to the next stride. Thus - 1. */ + counts[i - k - 1] = (uint32_t)count; + } + } + stride = 0; + sum = 0; + if (i < length - 2) { + /* All interesting strides have a count of at least 4, */ + /* at least when non-zeros. */ + limit = 256 * (counts[i] + counts[i + 1] + counts[i + 2]) / 3 + 420; + } else if (i < length) { + limit = 256 * counts[i]; + } else { + limit = 0; + } + } + ++stride; + if (i != length) { + sum += counts[i]; + if (stride >= 4) { + limit = (256 * sum + stride / 2) / stride; + } + if (stride == 4) { + limit += 120; + } + } + } +} + +static void DecideOverRleUse(const uint8_t* depth, const size_t length, + BROTLI_BOOL* use_rle_for_non_zero, + BROTLI_BOOL* use_rle_for_zero) { + size_t total_reps_zero = 0; + size_t total_reps_non_zero = 0; + size_t count_reps_zero = 1; + size_t count_reps_non_zero = 1; + size_t i; + for (i = 0; i < length;) { + const uint8_t value = depth[i]; + size_t reps = 1; + size_t k; + for (k = i + 1; k < length && depth[k] == value; ++k) { + ++reps; + } + if (reps >= 3 && value == 0) { + total_reps_zero += reps; + ++count_reps_zero; + } + if (reps >= 4 && value != 0) { + total_reps_non_zero += reps; + ++count_reps_non_zero; + } + i += reps; + } + *use_rle_for_non_zero = + TO_BROTLI_BOOL(total_reps_non_zero > count_reps_non_zero * 2); + *use_rle_for_zero = TO_BROTLI_BOOL(total_reps_zero > count_reps_zero * 2); +} + +void BrotliWriteHuffmanTree(const uint8_t* depth, + size_t length, + size_t* tree_size, + uint8_t* tree, + uint8_t* extra_bits_data) { + uint8_t previous_value = BROTLI_INITIAL_REPEATED_CODE_LENGTH; + size_t i; + BROTLI_BOOL use_rle_for_non_zero = BROTLI_FALSE; + BROTLI_BOOL use_rle_for_zero = BROTLI_FALSE; + + /* Throw away trailing zeros. */ + size_t new_length = length; + for (i = 0; i < length; ++i) { + if (depth[length - i - 1] == 0) { + --new_length; + } else { + break; + } + } + + /* First gather statistics on if it is a good idea to do RLE. */ + if (length > 50) { + /* Find RLE coding for longer codes. + Shorter codes seem not to benefit from RLE. */ + DecideOverRleUse(depth, new_length, + &use_rle_for_non_zero, &use_rle_for_zero); + } + + /* Actual RLE coding. */ + for (i = 0; i < new_length;) { + const uint8_t value = depth[i]; + size_t reps = 1; + if ((value != 0 && use_rle_for_non_zero) || + (value == 0 && use_rle_for_zero)) { + size_t k; + for (k = i + 1; k < new_length && depth[k] == value; ++k) { + ++reps; + } + } + if (value == 0) { + BrotliWriteHuffmanTreeRepetitionsZeros( + reps, tree_size, tree, extra_bits_data); + } else { + BrotliWriteHuffmanTreeRepetitions(previous_value, + value, reps, tree_size, + tree, extra_bits_data); + previous_value = value; + } + i += reps; + } +} + +static uint16_t BrotliReverseBits(size_t num_bits, uint16_t bits) { + static const size_t BROTLI_MODEL("small") kLut[16] = + { /* Pre-reversed 4-bit values. */ + 0x00, 0x08, 0x04, 0x0C, 0x02, 0x0A, 0x06, 0x0E, + 0x01, 0x09, 0x05, 0x0D, 0x03, 0x0B, 0x07, 0x0F + }; + size_t retval = kLut[bits & 0x0F]; + size_t i; + for (i = 4; i < num_bits; i += 4) { + retval <<= 4; + bits = (uint16_t)(bits >> 4); + retval |= kLut[bits & 0x0F]; + } + retval >>= ((0 - num_bits) & 0x03); + return (uint16_t)retval; +} + +/* 0..15 are values for bits */ +#define MAX_HUFFMAN_BITS 16 + +void BrotliConvertBitDepthsToSymbols(const uint8_t* depth, + size_t len, + uint16_t* bits) { + /* In Brotli, all bit depths are [1..15] + 0 bit depth means that the symbol does not exist. */ + uint16_t bl_count[MAX_HUFFMAN_BITS] = { 0 }; + uint16_t next_code[MAX_HUFFMAN_BITS]; + size_t i; + int code = 0; + for (i = 0; i < len; ++i) { + ++bl_count[depth[i]]; + } + bl_count[0] = 0; + next_code[0] = 0; + for (i = 1; i < MAX_HUFFMAN_BITS; ++i) { + code = (code + bl_count[i - 1]) << 1; + next_code[i] = (uint16_t)code; + } + for (i = 0; i < len; ++i) { + if (depth[i]) { + bits[i] = BrotliReverseBits(depth[i], next_code[depth[i]]++); + } + } +} + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif diff --git a/packages/runtime/vendor/brotli/c/enc/entropy_encode.h b/packages/runtime/vendor/brotli/c/enc/entropy_encode.h new file mode 100644 index 000000000..0470c16e0 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/entropy_encode.h @@ -0,0 +1,121 @@ +/* Copyright 2010 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* Entropy encoding (Huffman) utilities. */ + +#ifndef BROTLI_ENC_ENTROPY_ENCODE_H_ +#define BROTLI_ENC_ENTROPY_ENCODE_H_ + +#include "../common/platform.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +/* A node of a Huffman tree. */ +typedef struct HuffmanTree { + uint32_t total_count_; + int16_t index_left_; + int16_t index_right_or_value_; +} HuffmanTree; + +static BROTLI_INLINE void InitHuffmanTree(HuffmanTree* self, uint32_t count, + int16_t left, int16_t right) { + self->total_count_ = count; + self->index_left_ = left; + self->index_right_or_value_ = right; +} + +/* Returns 1 is assignment of depths succeeded, otherwise 0. */ +BROTLI_INTERNAL BROTLI_BOOL BrotliSetDepth( + int p, HuffmanTree* pool, uint8_t* depth, int max_depth); + +/* This function will create a Huffman tree. + + The (data,length) contains the population counts. + The tree_limit is the maximum bit depth of the Huffman codes. + + The depth contains the tree, i.e., how many bits are used for + the symbol. + + The actual Huffman tree is constructed in the tree[] array, which has to + be at least 2 * length + 1 long. + + See http://en.wikipedia.org/wiki/Huffman_coding */ +BROTLI_INTERNAL void BrotliCreateHuffmanTree(const uint32_t* data, + const size_t length, + const int tree_limit, + HuffmanTree* tree, + uint8_t* depth); + +/* Change the population counts in a way that the consequent + Huffman tree compression, especially its RLE-part will be more + likely to compress this data more efficiently. + + length contains the size of the histogram. + counts contains the population counts. + good_for_rle is a buffer of at least length size */ +BROTLI_INTERNAL void BrotliOptimizeHuffmanCountsForRle( + size_t length, uint32_t* counts, uint8_t* good_for_rle); + +/* Write a Huffman tree from bit depths into the bit-stream representation + of a Huffman tree. The generated Huffman tree is to be compressed once + more using a Huffman tree */ +BROTLI_INTERNAL void BrotliWriteHuffmanTree(const uint8_t* depth, + size_t length, + size_t* tree_size, + uint8_t* tree, + uint8_t* extra_bits_data); + +/* Get the actual bit values for a tree of bit depths. */ +BROTLI_INTERNAL void BrotliConvertBitDepthsToSymbols(const uint8_t* depth, + size_t len, + uint16_t* bits); + +BROTLI_INTERNAL extern BROTLI_MODEL("small") const size_t kBrotliShellGaps[6]; +/* Input size optimized Shell sort. */ +typedef BROTLI_BOOL (*HuffmanTreeComparator)( + const HuffmanTree*, const HuffmanTree*); +static BROTLI_INLINE void SortHuffmanTreeItems(HuffmanTree* items, + const size_t n, HuffmanTreeComparator comparator) { + if (n < 13) { + /* Insertion sort. */ + size_t i; + for (i = 1; i < n; ++i) { + HuffmanTree tmp = items[i]; + size_t k = i; + size_t j = i - 1; + while (comparator(&tmp, &items[j])) { + items[k] = items[j]; + k = j; + if (!j--) break; + } + items[k] = tmp; + } + return; + } else { + /* Shell sort. */ + int g = n < 57 ? 2 : 0; + for (; g < 6; ++g) { + size_t gap = kBrotliShellGaps[g]; + size_t i; + for (i = gap; i < n; ++i) { + size_t j = i; + HuffmanTree tmp = items[i]; + for (; j >= gap && comparator(&tmp, &items[j - gap]); j -= gap) { + items[j] = items[j - gap]; + } + items[j] = tmp; + } + } + } +} + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif + +#endif /* BROTLI_ENC_ENTROPY_ENCODE_H_ */ diff --git a/packages/runtime/vendor/brotli/c/enc/entropy_encode_static.h b/packages/runtime/vendor/brotli/c/enc/entropy_encode_static.h new file mode 100644 index 000000000..5056eaa13 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/entropy_encode_static.h @@ -0,0 +1,548 @@ +/* Copyright 2015 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* Static entropy codes used for faster meta-block encoding. */ + +#ifndef BROTLI_ENC_ENTROPY_ENCODE_STATIC_H_ +#define BROTLI_ENC_ENTROPY_ENCODE_STATIC_H_ + +#include "../common/constants.h" +#include "../common/platform.h" +#include "write_bits.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +static const BROTLI_MODEL("small") uint8_t kCodeLengthDepth[18] = { + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 0, 4, 4, +}; + +static const BROTLI_MODEL("small") +uint8_t kStaticCommandCodeDepth[BROTLI_NUM_COMMAND_SYMBOLS] = { + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, + 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, + 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, + 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, + 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, + 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, + 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, + 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, + 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, + 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, + 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, + 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, + 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, + 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, + 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, + 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, +}; + +static const BROTLI_MODEL("small") +uint8_t kStaticDistanceCodeDepth[64] = { + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, +}; + +/* GENERATED CODE START */ +static const BROTLI_MODEL("small") +uint32_t kCodeLengthBits[18] = { + 0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 15, 31, 0, 11, 7, +}; + +static BROTLI_INLINE void StoreStaticCodeLengthCode( + size_t* storage_ix, uint8_t* storage) { + BrotliWriteBits( + 40, BROTLI_MAKE_UINT64_T(0x0000FFu, 0x55555554u), storage_ix, storage); +} + +static const BROTLI_MODEL("small") +uint64_t kZeroRepsBits[BROTLI_NUM_COMMAND_SYMBOLS] = { + 0x00000000, 0x00000000, 0x00000000, 0x00000007, 0x00000017, 0x00000027, + 0x00000037, 0x00000047, 0x00000057, 0x00000067, 0x00000077, 0x00000770, + 0x00000b87, 0x00001387, 0x00001b87, 0x00002387, 0x00002b87, 0x00003387, + 0x00003b87, 0x00000397, 0x00000b97, 0x00001397, 0x00001b97, 0x00002397, + 0x00002b97, 0x00003397, 0x00003b97, 0x000003a7, 0x00000ba7, 0x000013a7, + 0x00001ba7, 0x000023a7, 0x00002ba7, 0x000033a7, 0x00003ba7, 0x000003b7, + 0x00000bb7, 0x000013b7, 0x00001bb7, 0x000023b7, 0x00002bb7, 0x000033b7, + 0x00003bb7, 0x000003c7, 0x00000bc7, 0x000013c7, 0x00001bc7, 0x000023c7, + 0x00002bc7, 0x000033c7, 0x00003bc7, 0x000003d7, 0x00000bd7, 0x000013d7, + 0x00001bd7, 0x000023d7, 0x00002bd7, 0x000033d7, 0x00003bd7, 0x000003e7, + 0x00000be7, 0x000013e7, 0x00001be7, 0x000023e7, 0x00002be7, 0x000033e7, + 0x00003be7, 0x000003f7, 0x00000bf7, 0x000013f7, 0x00001bf7, 0x000023f7, + 0x00002bf7, 0x000033f7, 0x00003bf7, 0x0001c387, 0x0005c387, 0x0009c387, + 0x000dc387, 0x0011c387, 0x0015c387, 0x0019c387, 0x001dc387, 0x0001cb87, + 0x0005cb87, 0x0009cb87, 0x000dcb87, 0x0011cb87, 0x0015cb87, 0x0019cb87, + 0x001dcb87, 0x0001d387, 0x0005d387, 0x0009d387, 0x000dd387, 0x0011d387, + 0x0015d387, 0x0019d387, 0x001dd387, 0x0001db87, 0x0005db87, 0x0009db87, + 0x000ddb87, 0x0011db87, 0x0015db87, 0x0019db87, 0x001ddb87, 0x0001e387, + 0x0005e387, 0x0009e387, 0x000de387, 0x0011e387, 0x0015e387, 0x0019e387, + 0x001de387, 0x0001eb87, 0x0005eb87, 0x0009eb87, 0x000deb87, 0x0011eb87, + 0x0015eb87, 0x0019eb87, 0x001deb87, 0x0001f387, 0x0005f387, 0x0009f387, + 0x000df387, 0x0011f387, 0x0015f387, 0x0019f387, 0x001df387, 0x0001fb87, + 0x0005fb87, 0x0009fb87, 0x000dfb87, 0x0011fb87, 0x0015fb87, 0x0019fb87, + 0x001dfb87, 0x0001c397, 0x0005c397, 0x0009c397, 0x000dc397, 0x0011c397, + 0x0015c397, 0x0019c397, 0x001dc397, 0x0001cb97, 0x0005cb97, 0x0009cb97, + 0x000dcb97, 0x0011cb97, 0x0015cb97, 0x0019cb97, 0x001dcb97, 0x0001d397, + 0x0005d397, 0x0009d397, 0x000dd397, 0x0011d397, 0x0015d397, 0x0019d397, + 0x001dd397, 0x0001db97, 0x0005db97, 0x0009db97, 0x000ddb97, 0x0011db97, + 0x0015db97, 0x0019db97, 0x001ddb97, 0x0001e397, 0x0005e397, 0x0009e397, + 0x000de397, 0x0011e397, 0x0015e397, 0x0019e397, 0x001de397, 0x0001eb97, + 0x0005eb97, 0x0009eb97, 0x000deb97, 0x0011eb97, 0x0015eb97, 0x0019eb97, + 0x001deb97, 0x0001f397, 0x0005f397, 0x0009f397, 0x000df397, 0x0011f397, + 0x0015f397, 0x0019f397, 0x001df397, 0x0001fb97, 0x0005fb97, 0x0009fb97, + 0x000dfb97, 0x0011fb97, 0x0015fb97, 0x0019fb97, 0x001dfb97, 0x0001c3a7, + 0x0005c3a7, 0x0009c3a7, 0x000dc3a7, 0x0011c3a7, 0x0015c3a7, 0x0019c3a7, + 0x001dc3a7, 0x0001cba7, 0x0005cba7, 0x0009cba7, 0x000dcba7, 0x0011cba7, + 0x0015cba7, 0x0019cba7, 0x001dcba7, 0x0001d3a7, 0x0005d3a7, 0x0009d3a7, + 0x000dd3a7, 0x0011d3a7, 0x0015d3a7, 0x0019d3a7, 0x001dd3a7, 0x0001dba7, + 0x0005dba7, 0x0009dba7, 0x000ddba7, 0x0011dba7, 0x0015dba7, 0x0019dba7, + 0x001ddba7, 0x0001e3a7, 0x0005e3a7, 0x0009e3a7, 0x000de3a7, 0x0011e3a7, + 0x0015e3a7, 0x0019e3a7, 0x001de3a7, 0x0001eba7, 0x0005eba7, 0x0009eba7, + 0x000deba7, 0x0011eba7, 0x0015eba7, 0x0019eba7, 0x001deba7, 0x0001f3a7, + 0x0005f3a7, 0x0009f3a7, 0x000df3a7, 0x0011f3a7, 0x0015f3a7, 0x0019f3a7, + 0x001df3a7, 0x0001fba7, 0x0005fba7, 0x0009fba7, 0x000dfba7, 0x0011fba7, + 0x0015fba7, 0x0019fba7, 0x001dfba7, 0x0001c3b7, 0x0005c3b7, 0x0009c3b7, + 0x000dc3b7, 0x0011c3b7, 0x0015c3b7, 0x0019c3b7, 0x001dc3b7, 0x0001cbb7, + 0x0005cbb7, 0x0009cbb7, 0x000dcbb7, 0x0011cbb7, 0x0015cbb7, 0x0019cbb7, + 0x001dcbb7, 0x0001d3b7, 0x0005d3b7, 0x0009d3b7, 0x000dd3b7, 0x0011d3b7, + 0x0015d3b7, 0x0019d3b7, 0x001dd3b7, 0x0001dbb7, 0x0005dbb7, 0x0009dbb7, + 0x000ddbb7, 0x0011dbb7, 0x0015dbb7, 0x0019dbb7, 0x001ddbb7, 0x0001e3b7, + 0x0005e3b7, 0x0009e3b7, 0x000de3b7, 0x0011e3b7, 0x0015e3b7, 0x0019e3b7, + 0x001de3b7, 0x0001ebb7, 0x0005ebb7, 0x0009ebb7, 0x000debb7, 0x0011ebb7, + 0x0015ebb7, 0x0019ebb7, 0x001debb7, 0x0001f3b7, 0x0005f3b7, 0x0009f3b7, + 0x000df3b7, 0x0011f3b7, 0x0015f3b7, 0x0019f3b7, 0x001df3b7, 0x0001fbb7, + 0x0005fbb7, 0x0009fbb7, 0x000dfbb7, 0x0011fbb7, 0x0015fbb7, 0x0019fbb7, + 0x001dfbb7, 0x0001c3c7, 0x0005c3c7, 0x0009c3c7, 0x000dc3c7, 0x0011c3c7, + 0x0015c3c7, 0x0019c3c7, 0x001dc3c7, 0x0001cbc7, 0x0005cbc7, 0x0009cbc7, + 0x000dcbc7, 0x0011cbc7, 0x0015cbc7, 0x0019cbc7, 0x001dcbc7, 0x0001d3c7, + 0x0005d3c7, 0x0009d3c7, 0x000dd3c7, 0x0011d3c7, 0x0015d3c7, 0x0019d3c7, + 0x001dd3c7, 0x0001dbc7, 0x0005dbc7, 0x0009dbc7, 0x000ddbc7, 0x0011dbc7, + 0x0015dbc7, 0x0019dbc7, 0x001ddbc7, 0x0001e3c7, 0x0005e3c7, 0x0009e3c7, + 0x000de3c7, 0x0011e3c7, 0x0015e3c7, 0x0019e3c7, 0x001de3c7, 0x0001ebc7, + 0x0005ebc7, 0x0009ebc7, 0x000debc7, 0x0011ebc7, 0x0015ebc7, 0x0019ebc7, + 0x001debc7, 0x0001f3c7, 0x0005f3c7, 0x0009f3c7, 0x000df3c7, 0x0011f3c7, + 0x0015f3c7, 0x0019f3c7, 0x001df3c7, 0x0001fbc7, 0x0005fbc7, 0x0009fbc7, + 0x000dfbc7, 0x0011fbc7, 0x0015fbc7, 0x0019fbc7, 0x001dfbc7, 0x0001c3d7, + 0x0005c3d7, 0x0009c3d7, 0x000dc3d7, 0x0011c3d7, 0x0015c3d7, 0x0019c3d7, + 0x001dc3d7, 0x0001cbd7, 0x0005cbd7, 0x0009cbd7, 0x000dcbd7, 0x0011cbd7, + 0x0015cbd7, 0x0019cbd7, 0x001dcbd7, 0x0001d3d7, 0x0005d3d7, 0x0009d3d7, + 0x000dd3d7, 0x0011d3d7, 0x0015d3d7, 0x0019d3d7, 0x001dd3d7, 0x0001dbd7, + 0x0005dbd7, 0x0009dbd7, 0x000ddbd7, 0x0011dbd7, 0x0015dbd7, 0x0019dbd7, + 0x001ddbd7, 0x0001e3d7, 0x0005e3d7, 0x0009e3d7, 0x000de3d7, 0x0011e3d7, + 0x0015e3d7, 0x0019e3d7, 0x001de3d7, 0x0001ebd7, 0x0005ebd7, 0x0009ebd7, + 0x000debd7, 0x0011ebd7, 0x0015ebd7, 0x0019ebd7, 0x001debd7, 0x0001f3d7, + 0x0005f3d7, 0x0009f3d7, 0x000df3d7, 0x0011f3d7, 0x0015f3d7, 0x0019f3d7, + 0x001df3d7, 0x0001fbd7, 0x0005fbd7, 0x0009fbd7, 0x000dfbd7, 0x0011fbd7, + 0x0015fbd7, 0x0019fbd7, 0x001dfbd7, 0x0001c3e7, 0x0005c3e7, 0x0009c3e7, + 0x000dc3e7, 0x0011c3e7, 0x0015c3e7, 0x0019c3e7, 0x001dc3e7, 0x0001cbe7, + 0x0005cbe7, 0x0009cbe7, 0x000dcbe7, 0x0011cbe7, 0x0015cbe7, 0x0019cbe7, + 0x001dcbe7, 0x0001d3e7, 0x0005d3e7, 0x0009d3e7, 0x000dd3e7, 0x0011d3e7, + 0x0015d3e7, 0x0019d3e7, 0x001dd3e7, 0x0001dbe7, 0x0005dbe7, 0x0009dbe7, + 0x000ddbe7, 0x0011dbe7, 0x0015dbe7, 0x0019dbe7, 0x001ddbe7, 0x0001e3e7, + 0x0005e3e7, 0x0009e3e7, 0x000de3e7, 0x0011e3e7, 0x0015e3e7, 0x0019e3e7, + 0x001de3e7, 0x0001ebe7, 0x0005ebe7, 0x0009ebe7, 0x000debe7, 0x0011ebe7, + 0x0015ebe7, 0x0019ebe7, 0x001debe7, 0x0001f3e7, 0x0005f3e7, 0x0009f3e7, + 0x000df3e7, 0x0011f3e7, 0x0015f3e7, 0x0019f3e7, 0x001df3e7, 0x0001fbe7, + 0x0005fbe7, 0x0009fbe7, 0x000dfbe7, 0x0011fbe7, 0x0015fbe7, 0x0019fbe7, + 0x001dfbe7, 0x0001c3f7, 0x0005c3f7, 0x0009c3f7, 0x000dc3f7, 0x0011c3f7, + 0x0015c3f7, 0x0019c3f7, 0x001dc3f7, 0x0001cbf7, 0x0005cbf7, 0x0009cbf7, + 0x000dcbf7, 0x0011cbf7, 0x0015cbf7, 0x0019cbf7, 0x001dcbf7, 0x0001d3f7, + 0x0005d3f7, 0x0009d3f7, 0x000dd3f7, 0x0011d3f7, 0x0015d3f7, 0x0019d3f7, + 0x001dd3f7, 0x0001dbf7, 0x0005dbf7, 0x0009dbf7, 0x000ddbf7, 0x0011dbf7, + 0x0015dbf7, 0x0019dbf7, 0x001ddbf7, 0x0001e3f7, 0x0005e3f7, 0x0009e3f7, + 0x000de3f7, 0x0011e3f7, 0x0015e3f7, 0x0019e3f7, 0x001de3f7, 0x0001ebf7, + 0x0005ebf7, 0x0009ebf7, 0x000debf7, 0x0011ebf7, 0x0015ebf7, 0x0019ebf7, + 0x001debf7, 0x0001f3f7, 0x0005f3f7, 0x0009f3f7, 0x000df3f7, 0x0011f3f7, + 0x0015f3f7, 0x0019f3f7, 0x001df3f7, 0x0001fbf7, 0x0005fbf7, 0x0009fbf7, + 0x000dfbf7, 0x0011fbf7, 0x0015fbf7, 0x0019fbf7, 0x001dfbf7, 0x00e1c387, + 0x02e1c387, 0x04e1c387, 0x06e1c387, 0x08e1c387, 0x0ae1c387, 0x0ce1c387, + 0x0ee1c387, 0x00e5c387, 0x02e5c387, 0x04e5c387, 0x06e5c387, 0x08e5c387, + 0x0ae5c387, 0x0ce5c387, 0x0ee5c387, 0x00e9c387, 0x02e9c387, 0x04e9c387, + 0x06e9c387, 0x08e9c387, 0x0ae9c387, 0x0ce9c387, 0x0ee9c387, 0x00edc387, + 0x02edc387, 0x04edc387, 0x06edc387, 0x08edc387, 0x0aedc387, 0x0cedc387, + 0x0eedc387, 0x00f1c387, 0x02f1c387, 0x04f1c387, 0x06f1c387, 0x08f1c387, + 0x0af1c387, 0x0cf1c387, 0x0ef1c387, 0x00f5c387, 0x02f5c387, 0x04f5c387, + 0x06f5c387, 0x08f5c387, 0x0af5c387, 0x0cf5c387, 0x0ef5c387, 0x00f9c387, + 0x02f9c387, 0x04f9c387, 0x06f9c387, 0x08f9c387, 0x0af9c387, 0x0cf9c387, + 0x0ef9c387, 0x00fdc387, 0x02fdc387, 0x04fdc387, 0x06fdc387, 0x08fdc387, + 0x0afdc387, 0x0cfdc387, 0x0efdc387, 0x00e1cb87, 0x02e1cb87, 0x04e1cb87, + 0x06e1cb87, 0x08e1cb87, 0x0ae1cb87, 0x0ce1cb87, 0x0ee1cb87, 0x00e5cb87, + 0x02e5cb87, 0x04e5cb87, 0x06e5cb87, 0x08e5cb87, 0x0ae5cb87, 0x0ce5cb87, + 0x0ee5cb87, 0x00e9cb87, 0x02e9cb87, 0x04e9cb87, 0x06e9cb87, 0x08e9cb87, + 0x0ae9cb87, 0x0ce9cb87, 0x0ee9cb87, 0x00edcb87, 0x02edcb87, 0x04edcb87, + 0x06edcb87, 0x08edcb87, 0x0aedcb87, 0x0cedcb87, 0x0eedcb87, 0x00f1cb87, + 0x02f1cb87, 0x04f1cb87, 0x06f1cb87, 0x08f1cb87, 0x0af1cb87, 0x0cf1cb87, + 0x0ef1cb87, 0x00f5cb87, 0x02f5cb87, 0x04f5cb87, 0x06f5cb87, 0x08f5cb87, + 0x0af5cb87, 0x0cf5cb87, 0x0ef5cb87, 0x00f9cb87, 0x02f9cb87, 0x04f9cb87, + 0x06f9cb87, 0x08f9cb87, +}; + +static const BROTLI_MODEL("small") +uint32_t kZeroRepsDepth[BROTLI_NUM_COMMAND_SYMBOLS] = { + 0, 4, 8, 7, 7, 7, 7, 7, 7, 7, 7, 11, 14, 14, 14, 14, + 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, + 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, + 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, + 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 28, 28, 28, 28, 28, + 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, + 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, + 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, + 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, + 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, + 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, + 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, +}; + +static const BROTLI_MODEL("small") +uint64_t kNonZeroRepsBits[BROTLI_NUM_COMMAND_SYMBOLS] = { + 0x0000000b, 0x0000001b, 0x0000002b, 0x0000003b, 0x000002cb, 0x000006cb, + 0x00000acb, 0x00000ecb, 0x000002db, 0x000006db, 0x00000adb, 0x00000edb, + 0x000002eb, 0x000006eb, 0x00000aeb, 0x00000eeb, 0x000002fb, 0x000006fb, + 0x00000afb, 0x00000efb, 0x0000b2cb, 0x0001b2cb, 0x0002b2cb, 0x0003b2cb, + 0x0000b6cb, 0x0001b6cb, 0x0002b6cb, 0x0003b6cb, 0x0000bacb, 0x0001bacb, + 0x0002bacb, 0x0003bacb, 0x0000becb, 0x0001becb, 0x0002becb, 0x0003becb, + 0x0000b2db, 0x0001b2db, 0x0002b2db, 0x0003b2db, 0x0000b6db, 0x0001b6db, + 0x0002b6db, 0x0003b6db, 0x0000badb, 0x0001badb, 0x0002badb, 0x0003badb, + 0x0000bedb, 0x0001bedb, 0x0002bedb, 0x0003bedb, 0x0000b2eb, 0x0001b2eb, + 0x0002b2eb, 0x0003b2eb, 0x0000b6eb, 0x0001b6eb, 0x0002b6eb, 0x0003b6eb, + 0x0000baeb, 0x0001baeb, 0x0002baeb, 0x0003baeb, 0x0000beeb, 0x0001beeb, + 0x0002beeb, 0x0003beeb, 0x0000b2fb, 0x0001b2fb, 0x0002b2fb, 0x0003b2fb, + 0x0000b6fb, 0x0001b6fb, 0x0002b6fb, 0x0003b6fb, 0x0000bafb, 0x0001bafb, + 0x0002bafb, 0x0003bafb, 0x0000befb, 0x0001befb, 0x0002befb, 0x0003befb, + 0x002cb2cb, 0x006cb2cb, 0x00acb2cb, 0x00ecb2cb, 0x002db2cb, 0x006db2cb, + 0x00adb2cb, 0x00edb2cb, 0x002eb2cb, 0x006eb2cb, 0x00aeb2cb, 0x00eeb2cb, + 0x002fb2cb, 0x006fb2cb, 0x00afb2cb, 0x00efb2cb, 0x002cb6cb, 0x006cb6cb, + 0x00acb6cb, 0x00ecb6cb, 0x002db6cb, 0x006db6cb, 0x00adb6cb, 0x00edb6cb, + 0x002eb6cb, 0x006eb6cb, 0x00aeb6cb, 0x00eeb6cb, 0x002fb6cb, 0x006fb6cb, + 0x00afb6cb, 0x00efb6cb, 0x002cbacb, 0x006cbacb, 0x00acbacb, 0x00ecbacb, + 0x002dbacb, 0x006dbacb, 0x00adbacb, 0x00edbacb, 0x002ebacb, 0x006ebacb, + 0x00aebacb, 0x00eebacb, 0x002fbacb, 0x006fbacb, 0x00afbacb, 0x00efbacb, + 0x002cbecb, 0x006cbecb, 0x00acbecb, 0x00ecbecb, 0x002dbecb, 0x006dbecb, + 0x00adbecb, 0x00edbecb, 0x002ebecb, 0x006ebecb, 0x00aebecb, 0x00eebecb, + 0x002fbecb, 0x006fbecb, 0x00afbecb, 0x00efbecb, 0x002cb2db, 0x006cb2db, + 0x00acb2db, 0x00ecb2db, 0x002db2db, 0x006db2db, 0x00adb2db, 0x00edb2db, + 0x002eb2db, 0x006eb2db, 0x00aeb2db, 0x00eeb2db, 0x002fb2db, 0x006fb2db, + 0x00afb2db, 0x00efb2db, 0x002cb6db, 0x006cb6db, 0x00acb6db, 0x00ecb6db, + 0x002db6db, 0x006db6db, 0x00adb6db, 0x00edb6db, 0x002eb6db, 0x006eb6db, + 0x00aeb6db, 0x00eeb6db, 0x002fb6db, 0x006fb6db, 0x00afb6db, 0x00efb6db, + 0x002cbadb, 0x006cbadb, 0x00acbadb, 0x00ecbadb, 0x002dbadb, 0x006dbadb, + 0x00adbadb, 0x00edbadb, 0x002ebadb, 0x006ebadb, 0x00aebadb, 0x00eebadb, + 0x002fbadb, 0x006fbadb, 0x00afbadb, 0x00efbadb, 0x002cbedb, 0x006cbedb, + 0x00acbedb, 0x00ecbedb, 0x002dbedb, 0x006dbedb, 0x00adbedb, 0x00edbedb, + 0x002ebedb, 0x006ebedb, 0x00aebedb, 0x00eebedb, 0x002fbedb, 0x006fbedb, + 0x00afbedb, 0x00efbedb, 0x002cb2eb, 0x006cb2eb, 0x00acb2eb, 0x00ecb2eb, + 0x002db2eb, 0x006db2eb, 0x00adb2eb, 0x00edb2eb, 0x002eb2eb, 0x006eb2eb, + 0x00aeb2eb, 0x00eeb2eb, 0x002fb2eb, 0x006fb2eb, 0x00afb2eb, 0x00efb2eb, + 0x002cb6eb, 0x006cb6eb, 0x00acb6eb, 0x00ecb6eb, 0x002db6eb, 0x006db6eb, + 0x00adb6eb, 0x00edb6eb, 0x002eb6eb, 0x006eb6eb, 0x00aeb6eb, 0x00eeb6eb, + 0x002fb6eb, 0x006fb6eb, 0x00afb6eb, 0x00efb6eb, 0x002cbaeb, 0x006cbaeb, + 0x00acbaeb, 0x00ecbaeb, 0x002dbaeb, 0x006dbaeb, 0x00adbaeb, 0x00edbaeb, + 0x002ebaeb, 0x006ebaeb, 0x00aebaeb, 0x00eebaeb, 0x002fbaeb, 0x006fbaeb, + 0x00afbaeb, 0x00efbaeb, 0x002cbeeb, 0x006cbeeb, 0x00acbeeb, 0x00ecbeeb, + 0x002dbeeb, 0x006dbeeb, 0x00adbeeb, 0x00edbeeb, 0x002ebeeb, 0x006ebeeb, + 0x00aebeeb, 0x00eebeeb, 0x002fbeeb, 0x006fbeeb, 0x00afbeeb, 0x00efbeeb, + 0x002cb2fb, 0x006cb2fb, 0x00acb2fb, 0x00ecb2fb, 0x002db2fb, 0x006db2fb, + 0x00adb2fb, 0x00edb2fb, 0x002eb2fb, 0x006eb2fb, 0x00aeb2fb, 0x00eeb2fb, + 0x002fb2fb, 0x006fb2fb, 0x00afb2fb, 0x00efb2fb, 0x002cb6fb, 0x006cb6fb, + 0x00acb6fb, 0x00ecb6fb, 0x002db6fb, 0x006db6fb, 0x00adb6fb, 0x00edb6fb, + 0x002eb6fb, 0x006eb6fb, 0x00aeb6fb, 0x00eeb6fb, 0x002fb6fb, 0x006fb6fb, + 0x00afb6fb, 0x00efb6fb, 0x002cbafb, 0x006cbafb, 0x00acbafb, 0x00ecbafb, + 0x002dbafb, 0x006dbafb, 0x00adbafb, 0x00edbafb, 0x002ebafb, 0x006ebafb, + 0x00aebafb, 0x00eebafb, 0x002fbafb, 0x006fbafb, 0x00afbafb, 0x00efbafb, + 0x002cbefb, 0x006cbefb, 0x00acbefb, 0x00ecbefb, 0x002dbefb, 0x006dbefb, + 0x00adbefb, 0x00edbefb, 0x002ebefb, 0x006ebefb, 0x00aebefb, 0x00eebefb, + 0x002fbefb, 0x006fbefb, 0x00afbefb, 0x00efbefb, 0x0b2cb2cb, 0x1b2cb2cb, + 0x2b2cb2cb, 0x3b2cb2cb, 0x0b6cb2cb, 0x1b6cb2cb, 0x2b6cb2cb, 0x3b6cb2cb, + 0x0bacb2cb, 0x1bacb2cb, 0x2bacb2cb, 0x3bacb2cb, 0x0becb2cb, 0x1becb2cb, + 0x2becb2cb, 0x3becb2cb, 0x0b2db2cb, 0x1b2db2cb, 0x2b2db2cb, 0x3b2db2cb, + 0x0b6db2cb, 0x1b6db2cb, 0x2b6db2cb, 0x3b6db2cb, 0x0badb2cb, 0x1badb2cb, + 0x2badb2cb, 0x3badb2cb, 0x0bedb2cb, 0x1bedb2cb, 0x2bedb2cb, 0x3bedb2cb, + 0x0b2eb2cb, 0x1b2eb2cb, 0x2b2eb2cb, 0x3b2eb2cb, 0x0b6eb2cb, 0x1b6eb2cb, + 0x2b6eb2cb, 0x3b6eb2cb, 0x0baeb2cb, 0x1baeb2cb, 0x2baeb2cb, 0x3baeb2cb, + 0x0beeb2cb, 0x1beeb2cb, 0x2beeb2cb, 0x3beeb2cb, 0x0b2fb2cb, 0x1b2fb2cb, + 0x2b2fb2cb, 0x3b2fb2cb, 0x0b6fb2cb, 0x1b6fb2cb, 0x2b6fb2cb, 0x3b6fb2cb, + 0x0bafb2cb, 0x1bafb2cb, 0x2bafb2cb, 0x3bafb2cb, 0x0befb2cb, 0x1befb2cb, + 0x2befb2cb, 0x3befb2cb, 0x0b2cb6cb, 0x1b2cb6cb, 0x2b2cb6cb, 0x3b2cb6cb, + 0x0b6cb6cb, 0x1b6cb6cb, 0x2b6cb6cb, 0x3b6cb6cb, 0x0bacb6cb, 0x1bacb6cb, + 0x2bacb6cb, 0x3bacb6cb, 0x0becb6cb, 0x1becb6cb, 0x2becb6cb, 0x3becb6cb, + 0x0b2db6cb, 0x1b2db6cb, 0x2b2db6cb, 0x3b2db6cb, 0x0b6db6cb, 0x1b6db6cb, + 0x2b6db6cb, 0x3b6db6cb, 0x0badb6cb, 0x1badb6cb, 0x2badb6cb, 0x3badb6cb, + 0x0bedb6cb, 0x1bedb6cb, 0x2bedb6cb, 0x3bedb6cb, 0x0b2eb6cb, 0x1b2eb6cb, + 0x2b2eb6cb, 0x3b2eb6cb, 0x0b6eb6cb, 0x1b6eb6cb, 0x2b6eb6cb, 0x3b6eb6cb, + 0x0baeb6cb, 0x1baeb6cb, 0x2baeb6cb, 0x3baeb6cb, 0x0beeb6cb, 0x1beeb6cb, + 0x2beeb6cb, 0x3beeb6cb, 0x0b2fb6cb, 0x1b2fb6cb, 0x2b2fb6cb, 0x3b2fb6cb, + 0x0b6fb6cb, 0x1b6fb6cb, 0x2b6fb6cb, 0x3b6fb6cb, 0x0bafb6cb, 0x1bafb6cb, + 0x2bafb6cb, 0x3bafb6cb, 0x0befb6cb, 0x1befb6cb, 0x2befb6cb, 0x3befb6cb, + 0x0b2cbacb, 0x1b2cbacb, 0x2b2cbacb, 0x3b2cbacb, 0x0b6cbacb, 0x1b6cbacb, + 0x2b6cbacb, 0x3b6cbacb, 0x0bacbacb, 0x1bacbacb, 0x2bacbacb, 0x3bacbacb, + 0x0becbacb, 0x1becbacb, 0x2becbacb, 0x3becbacb, 0x0b2dbacb, 0x1b2dbacb, + 0x2b2dbacb, 0x3b2dbacb, 0x0b6dbacb, 0x1b6dbacb, 0x2b6dbacb, 0x3b6dbacb, + 0x0badbacb, 0x1badbacb, 0x2badbacb, 0x3badbacb, 0x0bedbacb, 0x1bedbacb, + 0x2bedbacb, 0x3bedbacb, 0x0b2ebacb, 0x1b2ebacb, 0x2b2ebacb, 0x3b2ebacb, + 0x0b6ebacb, 0x1b6ebacb, 0x2b6ebacb, 0x3b6ebacb, 0x0baebacb, 0x1baebacb, + 0x2baebacb, 0x3baebacb, 0x0beebacb, 0x1beebacb, 0x2beebacb, 0x3beebacb, + 0x0b2fbacb, 0x1b2fbacb, 0x2b2fbacb, 0x3b2fbacb, 0x0b6fbacb, 0x1b6fbacb, + 0x2b6fbacb, 0x3b6fbacb, 0x0bafbacb, 0x1bafbacb, 0x2bafbacb, 0x3bafbacb, + 0x0befbacb, 0x1befbacb, 0x2befbacb, 0x3befbacb, 0x0b2cbecb, 0x1b2cbecb, + 0x2b2cbecb, 0x3b2cbecb, 0x0b6cbecb, 0x1b6cbecb, 0x2b6cbecb, 0x3b6cbecb, + 0x0bacbecb, 0x1bacbecb, 0x2bacbecb, 0x3bacbecb, 0x0becbecb, 0x1becbecb, + 0x2becbecb, 0x3becbecb, 0x0b2dbecb, 0x1b2dbecb, 0x2b2dbecb, 0x3b2dbecb, + 0x0b6dbecb, 0x1b6dbecb, 0x2b6dbecb, 0x3b6dbecb, 0x0badbecb, 0x1badbecb, + 0x2badbecb, 0x3badbecb, 0x0bedbecb, 0x1bedbecb, 0x2bedbecb, 0x3bedbecb, + 0x0b2ebecb, 0x1b2ebecb, 0x2b2ebecb, 0x3b2ebecb, 0x0b6ebecb, 0x1b6ebecb, + 0x2b6ebecb, 0x3b6ebecb, 0x0baebecb, 0x1baebecb, 0x2baebecb, 0x3baebecb, + 0x0beebecb, 0x1beebecb, 0x2beebecb, 0x3beebecb, 0x0b2fbecb, 0x1b2fbecb, + 0x2b2fbecb, 0x3b2fbecb, 0x0b6fbecb, 0x1b6fbecb, 0x2b6fbecb, 0x3b6fbecb, + 0x0bafbecb, 0x1bafbecb, 0x2bafbecb, 0x3bafbecb, 0x0befbecb, 0x1befbecb, + 0x2befbecb, 0x3befbecb, 0x0b2cb2db, 0x1b2cb2db, 0x2b2cb2db, 0x3b2cb2db, + 0x0b6cb2db, 0x1b6cb2db, 0x2b6cb2db, 0x3b6cb2db, 0x0bacb2db, 0x1bacb2db, + 0x2bacb2db, 0x3bacb2db, 0x0becb2db, 0x1becb2db, 0x2becb2db, 0x3becb2db, + 0x0b2db2db, 0x1b2db2db, 0x2b2db2db, 0x3b2db2db, 0x0b6db2db, 0x1b6db2db, + 0x2b6db2db, 0x3b6db2db, 0x0badb2db, 0x1badb2db, 0x2badb2db, 0x3badb2db, + 0x0bedb2db, 0x1bedb2db, 0x2bedb2db, 0x3bedb2db, 0x0b2eb2db, 0x1b2eb2db, + 0x2b2eb2db, 0x3b2eb2db, 0x0b6eb2db, 0x1b6eb2db, 0x2b6eb2db, 0x3b6eb2db, + 0x0baeb2db, 0x1baeb2db, 0x2baeb2db, 0x3baeb2db, 0x0beeb2db, 0x1beeb2db, + 0x2beeb2db, 0x3beeb2db, 0x0b2fb2db, 0x1b2fb2db, 0x2b2fb2db, 0x3b2fb2db, + 0x0b6fb2db, 0x1b6fb2db, 0x2b6fb2db, 0x3b6fb2db, 0x0bafb2db, 0x1bafb2db, + 0x2bafb2db, 0x3bafb2db, 0x0befb2db, 0x1befb2db, 0x2befb2db, 0x3befb2db, + 0x0b2cb6db, 0x1b2cb6db, 0x2b2cb6db, 0x3b2cb6db, 0x0b6cb6db, 0x1b6cb6db, + 0x2b6cb6db, 0x3b6cb6db, 0x0bacb6db, 0x1bacb6db, 0x2bacb6db, 0x3bacb6db, + 0x0becb6db, 0x1becb6db, 0x2becb6db, 0x3becb6db, 0x0b2db6db, 0x1b2db6db, + 0x2b2db6db, 0x3b2db6db, 0x0b6db6db, 0x1b6db6db, 0x2b6db6db, 0x3b6db6db, + 0x0badb6db, 0x1badb6db, 0x2badb6db, 0x3badb6db, 0x0bedb6db, 0x1bedb6db, + 0x2bedb6db, 0x3bedb6db, 0x0b2eb6db, 0x1b2eb6db, 0x2b2eb6db, 0x3b2eb6db, + 0x0b6eb6db, 0x1b6eb6db, 0x2b6eb6db, 0x3b6eb6db, 0x0baeb6db, 0x1baeb6db, + 0x2baeb6db, 0x3baeb6db, +}; + +static const BROTLI_MODEL("small") +uint32_t kNonZeroRepsDepth[BROTLI_NUM_COMMAND_SYMBOLS] = { + 6, 6, 6, 6, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, + 12, 12, 12, 12, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, + 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, + 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, + 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, + 18, 18, 18, 18, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 24, 24, 24, 24, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, + 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, + 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, + 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, + 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, + 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, + 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, + 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, + 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, + 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, + 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, + 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, + 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, + 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, + 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, + 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, + 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, + 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, + 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, + 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, + 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, + 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, + 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, +}; + +static const BROTLI_MODEL("small") +uint16_t kStaticCommandCodeBits[BROTLI_NUM_COMMAND_SYMBOLS] = { + 0, 256, 128, 384, 64, 320, 192, 448, + 32, 288, 160, 416, 96, 352, 224, 480, + 16, 272, 144, 400, 80, 336, 208, 464, + 48, 304, 176, 432, 112, 368, 240, 496, + 8, 264, 136, 392, 72, 328, 200, 456, + 40, 296, 168, 424, 104, 360, 232, 488, + 24, 280, 152, 408, 88, 344, 216, 472, + 56, 312, 184, 440, 120, 376, 248, 504, + 4, 260, 132, 388, 68, 324, 196, 452, + 36, 292, 164, 420, 100, 356, 228, 484, + 20, 276, 148, 404, 84, 340, 212, 468, + 52, 308, 180, 436, 116, 372, 244, 500, + 12, 268, 140, 396, 76, 332, 204, 460, + 44, 300, 172, 428, 108, 364, 236, 492, + 28, 284, 156, 412, 92, 348, 220, 476, + 60, 316, 188, 444, 124, 380, 252, 508, + 2, 258, 130, 386, 66, 322, 194, 450, + 34, 290, 162, 418, 98, 354, 226, 482, + 18, 274, 146, 402, 82, 338, 210, 466, + 50, 306, 178, 434, 114, 370, 242, 498, + 10, 266, 138, 394, 74, 330, 202, 458, + 42, 298, 170, 426, 106, 362, 234, 490, + 26, 282, 154, 410, 90, 346, 218, 474, + 58, 314, 186, 442, 122, 378, 250, 506, + 6, 262, 134, 390, 70, 326, 198, 454, + 38, 294, 166, 422, 102, 358, 230, 486, + 22, 278, 150, 406, 86, 342, 214, 470, + 54, 310, 182, 438, 118, 374, 246, 502, + 14, 270, 142, 398, 78, 334, 206, 462, + 46, 302, 174, 430, 110, 366, 238, 494, + 30, 286, 158, 414, 94, 350, 222, 478, + 62, 318, 190, 446, 126, 382, 254, 510, + 1, 257, 129, 385, 65, 321, 193, 449, + 33, 289, 161, 417, 97, 353, 225, 481, + 17, 273, 145, 401, 81, 337, 209, 465, + 49, 305, 177, 433, 113, 369, 241, 497, + 9, 265, 137, 393, 73, 329, 201, 457, + 41, 297, 169, 425, 105, 361, 233, 489, + 25, 281, 153, 409, 89, 345, 217, 473, + 57, 313, 185, 441, 121, 377, 249, 505, + 5, 261, 133, 389, 69, 325, 197, 453, + 37, 293, 165, 421, 101, 357, 229, 485, + 21, 277, 149, 405, 85, 341, 213, 469, + 53, 309, 181, 437, 117, 373, 245, 501, + 13, 269, 141, 397, 77, 333, 205, 461, + 45, 301, 173, 429, 109, 365, 237, 493, + 29, 285, 157, 413, 93, 349, 221, 477, + 61, 317, 189, 445, 125, 381, 253, 509, + 3, 259, 131, 387, 67, 323, 195, 451, + 35, 291, 163, 419, 99, 355, 227, 483, + 19, 275, 147, 403, 83, 339, 211, 467, + 51, 307, 179, 435, 115, 371, 243, 499, + 11, 267, 139, 395, 75, 331, 203, 459, + 43, 299, 171, 427, 107, 363, 235, 491, + 27, 283, 155, 411, 91, 347, 219, 475, + 59, 315, 187, 443, 123, 379, 251, 507, + 7, 1031, 519, 1543, 263, 1287, 775, 1799, + 135, 1159, 647, 1671, 391, 1415, 903, 1927, + 71, 1095, 583, 1607, 327, 1351, 839, 1863, + 199, 1223, 711, 1735, 455, 1479, 967, 1991, + 39, 1063, 551, 1575, 295, 1319, 807, 1831, + 167, 1191, 679, 1703, 423, 1447, 935, 1959, + 103, 1127, 615, 1639, 359, 1383, 871, 1895, + 231, 1255, 743, 1767, 487, 1511, 999, 2023, + 23, 1047, 535, 1559, 279, 1303, 791, 1815, + 151, 1175, 663, 1687, 407, 1431, 919, 1943, + 87, 1111, 599, 1623, 343, 1367, 855, 1879, + 215, 1239, 727, 1751, 471, 1495, 983, 2007, + 55, 1079, 567, 1591, 311, 1335, 823, 1847, + 183, 1207, 695, 1719, 439, 1463, 951, 1975, + 119, 1143, 631, 1655, 375, 1399, 887, 1911, + 247, 1271, 759, 1783, 503, 1527, 1015, 2039, + 15, 1039, 527, 1551, 271, 1295, 783, 1807, + 143, 1167, 655, 1679, 399, 1423, 911, 1935, + 79, 1103, 591, 1615, 335, 1359, 847, 1871, + 207, 1231, 719, 1743, 463, 1487, 975, 1999, + 47, 1071, 559, 1583, 303, 1327, 815, 1839, + 175, 1199, 687, 1711, 431, 1455, 943, 1967, + 111, 1135, 623, 1647, 367, 1391, 879, 1903, + 239, 1263, 751, 1775, 495, 1519, 1007, 2031, + 31, 1055, 543, 1567, 287, 1311, 799, 1823, + 159, 1183, 671, 1695, 415, 1439, 927, 1951, + 95, 1119, 607, 1631, 351, 1375, 863, 1887, + 223, 1247, 735, 1759, 479, 1503, 991, 2015, + 63, 1087, 575, 1599, 319, 1343, 831, 1855, + 191, 1215, 703, 1727, 447, 1471, 959, 1983, + 127, 1151, 639, 1663, 383, 1407, 895, 1919, + 255, 1279, 767, 1791, 511, 1535, 1023, 2047, +}; + +static BROTLI_INLINE void StoreStaticCommandHuffmanTree( + size_t* storage_ix, uint8_t* storage) { + BrotliWriteBits( + 56, BROTLI_MAKE_UINT64_T(0x926244U, 0x16307003U), storage_ix, storage); + BrotliWriteBits(3, 0x00000000U, storage_ix, storage); +} + +static const BROTLI_MODEL("small") uint16_t kStaticDistanceCodeBits[64] = { + 0, 32, 16, 48, 8, 40, 24, 56, 4, 36, 20, 52, 12, 44, 28, 60, + 2, 34, 18, 50, 10, 42, 26, 58, 6, 38, 22, 54, 14, 46, 30, 62, + 1, 33, 17, 49, 9, 41, 25, 57, 5, 37, 21, 53, 13, 45, 29, 61, + 3, 35, 19, 51, 11, 43, 27, 59, 7, 39, 23, 55, 15, 47, 31, 63, +}; + +static BROTLI_INLINE void StoreStaticDistanceHuffmanTree( + size_t* storage_ix, uint8_t* storage) { + BrotliWriteBits(28, 0x0369DC03u, storage_ix, storage); +} +/* GENERATED CODE END */ + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif + +#endif /* BROTLI_ENC_ENTROPY_ENCODE_STATIC_H_ */ diff --git a/packages/runtime/vendor/brotli/c/enc/fast_log.c b/packages/runtime/vendor/brotli/c/enc/fast_log.c new file mode 100644 index 000000000..7b7ac0295 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/fast_log.c @@ -0,0 +1,105 @@ +/* Copyright 2013 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +#include "fast_log.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +/* ", ".join(["%.16ff" % x for x in [0.0]+[log2(x) for x in range(1, 256)]]) */ +const BROTLI_MODEL("small") double kBrotliLog2Table[BROTLI_LOG2_TABLE_SIZE] = { + 0.0000000000000000f, 0.0000000000000000f, 1.0000000000000000f, + 1.5849625007211563f, 2.0000000000000000f, 2.3219280948873622f, + 2.5849625007211561f, 2.8073549220576042f, 3.0000000000000000f, + 3.1699250014423126f, 3.3219280948873626f, 3.4594316186372978f, + 3.5849625007211565f, 3.7004397181410922f, 3.8073549220576037f, + 3.9068905956085187f, 4.0000000000000000f, 4.0874628412503400f, + 4.1699250014423122f, 4.2479275134435852f, 4.3219280948873626f, + 4.3923174227787607f, 4.4594316186372973f, 4.5235619560570131f, + 4.5849625007211570f, 4.6438561897747244f, 4.7004397181410926f, + 4.7548875021634691f, 4.8073549220576037f, 4.8579809951275728f, + 4.9068905956085187f, 4.9541963103868758f, 5.0000000000000000f, + 5.0443941193584534f, 5.0874628412503400f, 5.1292830169449664f, + 5.1699250014423122f, 5.2094533656289501f, 5.2479275134435852f, + 5.2854022188622487f, 5.3219280948873626f, 5.3575520046180838f, + 5.3923174227787607f, 5.4262647547020979f, 5.4594316186372973f, + 5.4918530963296748f, 5.5235619560570131f, 5.5545888516776376f, + 5.5849625007211570f, 5.6147098441152083f, 5.6438561897747244f, + 5.6724253419714961f, 5.7004397181410926f, 5.7279204545631996f, + 5.7548875021634691f, 5.7813597135246599f, 5.8073549220576046f, + 5.8328900141647422f, 5.8579809951275719f, 5.8826430493618416f, + 5.9068905956085187f, 5.9307373375628867f, 5.9541963103868758f, + 5.9772799234999168f, 6.0000000000000000f, 6.0223678130284544f, + 6.0443941193584534f, 6.0660891904577721f, 6.0874628412503400f, + 6.1085244567781700f, 6.1292830169449672f, 6.1497471195046822f, + 6.1699250014423122f, 6.1898245588800176f, 6.2094533656289510f, + 6.2288186904958804f, 6.2479275134435861f, 6.2667865406949019f, + 6.2854022188622487f, 6.3037807481771031f, 6.3219280948873617f, + 6.3398500028846252f, 6.3575520046180847f, 6.3750394313469254f, + 6.3923174227787598f, 6.4093909361377026f, 6.4262647547020979f, + 6.4429434958487288f, 6.4594316186372982f, 6.4757334309663976f, + 6.4918530963296748f, 6.5077946401986964f, 6.5235619560570131f, + 6.5391588111080319f, 6.5545888516776376f, 6.5698556083309478f, + 6.5849625007211561f, 6.5999128421871278f, 6.6147098441152092f, + 6.6293566200796095f, 6.6438561897747253f, 6.6582114827517955f, + 6.6724253419714952f, 6.6865005271832185f, 6.7004397181410917f, + 6.7142455176661224f, 6.7279204545631988f, 6.7414669864011465f, + 6.7548875021634691f, 6.7681843247769260f, 6.7813597135246599f, + 6.7944158663501062f, 6.8073549220576037f, 6.8201789624151887f, + 6.8328900141647422f, 6.8454900509443757f, 6.8579809951275719f, + 6.8703647195834048f, 6.8826430493618416f, 6.8948177633079437f, + 6.9068905956085187f, 6.9188632372745955f, 6.9307373375628867f, + 6.9425145053392399f, 6.9541963103868758f, 6.9657842846620879f, + 6.9772799234999168f, 6.9886846867721664f, 7.0000000000000000f, + 7.0112272554232540f, 7.0223678130284544f, 7.0334230015374501f, + 7.0443941193584534f, 7.0552824355011898f, 7.0660891904577721f, + 7.0768155970508317f, 7.0874628412503400f, 7.0980320829605272f, + 7.1085244567781700f, 7.1189410727235076f, 7.1292830169449664f, + 7.1395513523987937f, 7.1497471195046822f, 7.1598713367783891f, + 7.1699250014423130f, 7.1799090900149345f, 7.1898245588800176f, + 7.1996723448363644f, 7.2094533656289492f, 7.2191685204621621f, + 7.2288186904958804f, 7.2384047393250794f, 7.2479275134435861f, + 7.2573878426926521f, 7.2667865406949019f, 7.2761244052742384f, + 7.2854022188622487f, 7.2946207488916270f, 7.3037807481771031f, + 7.3128829552843557f, 7.3219280948873617f, 7.3309168781146177f, + 7.3398500028846243f, 7.3487281542310781f, 7.3575520046180847f, + 7.3663222142458151f, 7.3750394313469254f, 7.3837042924740528f, + 7.3923174227787607f, 7.4008794362821844f, 7.4093909361377026f, + 7.4178525148858991f, 7.4262647547020979f, 7.4346282276367255f, + 7.4429434958487288f, 7.4512111118323299f, 7.4594316186372973f, + 7.4676055500829976f, 7.4757334309663976f, 7.4838157772642564f, + 7.4918530963296748f, 7.4998458870832057f, 7.5077946401986964f, + 7.5156998382840436f, 7.5235619560570131f, 7.5313814605163119f, + 7.5391588111080319f, 7.5468944598876373f, 7.5545888516776376f, + 7.5622424242210728f, 7.5698556083309478f, 7.5774288280357487f, + 7.5849625007211561f, 7.5924570372680806f, 7.5999128421871278f, + 7.6073303137496113f, 7.6147098441152075f, 7.6220518194563764f, + 7.6293566200796095f, 7.6366246205436488f, 7.6438561897747244f, + 7.6510516911789290f, 7.6582114827517955f, 7.6653359171851765f, + 7.6724253419714952f, 7.6794800995054464f, 7.6865005271832185f, + 7.6934869574993252f, 7.7004397181410926f, 7.7073591320808825f, + 7.7142455176661224f, 7.7210991887071856f, 7.7279204545631996f, + 7.7347096202258392f, 7.7414669864011465f, 7.7481928495894596f, + 7.7548875021634691f, 7.7615512324444795f, 7.7681843247769260f, + 7.7747870596011737f, 7.7813597135246608f, 7.7879025593914317f, + 7.7944158663501062f, 7.8008998999203047f, 7.8073549220576037f, + 7.8137811912170374f, 7.8201789624151887f, 7.8265484872909159f, + 7.8328900141647422f, 7.8392037880969445f, 7.8454900509443757f, + 7.8517490414160571f, 7.8579809951275719f, 7.8641861446542798f, + 7.8703647195834048f, 7.8765169465650002f, 7.8826430493618425f, + 7.8887432488982601f, 7.8948177633079446f, 7.9008668079807496f, + 7.9068905956085187f, 7.9128893362299619f, 7.9188632372745955f, + 7.9248125036057813f, 7.9307373375628867f, 7.9366379390025719f, + 7.9425145053392399f, 7.9483672315846778f, 7.9541963103868758f, + 7.9600019320680806f, 7.9657842846620870f, 7.9715435539507720f, + 7.9772799234999168f, 7.9829935746943104f, 7.9886846867721664f, + 7.9943534368588578f +}; + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif diff --git a/packages/runtime/vendor/brotli/c/enc/fast_log.h b/packages/runtime/vendor/brotli/c/enc/fast_log.h new file mode 100644 index 000000000..a245443ff --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/fast_log.h @@ -0,0 +1,66 @@ +/* Copyright 2013 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* Utilities for fast computation of logarithms. */ + +#ifndef BROTLI_ENC_FAST_LOG_H_ +#define BROTLI_ENC_FAST_LOG_H_ + +#include + +#include "../common/platform.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +static BROTLI_INLINE uint32_t Log2FloorNonZero(size_t n) { +#if defined(BROTLI_BSR32) + return BROTLI_BSR32((uint32_t)n); +#else + uint32_t result = 0; + while (n >>= 1) result++; + return result; +#endif +} + +#define BROTLI_LOG2_TABLE_SIZE 256 + +/* A lookup table for small values of log2(int) to be used in entropy + computation. */ +BROTLI_INTERNAL extern const BROTLI_MODEL("small") +double kBrotliLog2Table[BROTLI_LOG2_TABLE_SIZE]; + +/* Visual Studio 2012 and Android API levels < 18 do not have the log2() + * function defined, so we use log() and a multiplication instead. */ +#if !defined(BROTLI_HAVE_LOG2) +#if ((defined(_MSC_VER) && _MSC_VER <= 1700) || \ + (defined(__ANDROID_API__) && __ANDROID_API__ < 18)) +#define BROTLI_HAVE_LOG2 0 +#else +#define BROTLI_HAVE_LOG2 1 +#endif +#endif + +#define LOG_2_INV 1.4426950408889634 + +/* Faster logarithm for small integers, with the property of log2(0) == 0. */ +static BROTLI_INLINE double FastLog2(size_t v) { + if (v < BROTLI_LOG2_TABLE_SIZE) { + return kBrotliLog2Table[v]; + } +#if !(BROTLI_HAVE_LOG2) + return log((double)v) * LOG_2_INV; +#else + return log2((double)v); +#endif +} + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif + +#endif /* BROTLI_ENC_FAST_LOG_H_ */ diff --git a/packages/runtime/vendor/brotli/c/enc/find_match_length.h b/packages/runtime/vendor/brotli/c/enc/find_match_length.h new file mode 100644 index 000000000..096be9331 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/find_match_length.h @@ -0,0 +1,70 @@ +/* Copyright 2010 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* Function to find maximal matching prefixes of strings. */ + +#ifndef BROTLI_ENC_FIND_MATCH_LENGTH_H_ +#define BROTLI_ENC_FIND_MATCH_LENGTH_H_ + +#include "../common/platform.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +/* Separate implementation for little-endian 64-bit targets, for speed. */ +#if defined(BROTLI_TZCNT64) && BROTLI_64_BITS && BROTLI_LITTLE_ENDIAN +static BROTLI_INLINE size_t FindMatchLengthWithLimit(const uint8_t* s1, + const uint8_t* s2, + size_t limit) { + const uint8_t *s1_orig = s1; + for (; limit >= 8; limit -= 8) { + uint64_t x = BROTLI_UNALIGNED_LOAD64LE(s2) ^ + BROTLI_UNALIGNED_LOAD64LE(s1); + s2 += 8; + if (x != 0) { + size_t matching_bits = (size_t)BROTLI_TZCNT64(x); + return (size_t)(s1 - s1_orig) + (matching_bits >> 3); + } + s1 += 8; + } + while (limit && *s1 == *s2) { + limit--; + ++s2; + ++s1; + } + return (size_t)(s1 - s1_orig); +} +#else +static BROTLI_INLINE size_t FindMatchLengthWithLimit(const uint8_t* s1, + const uint8_t* s2, + size_t limit) { + size_t matched = 0; + const uint8_t* s2_limit = s2 + limit; + const uint8_t* s2_ptr = s2; + /* Find out how long the match is. We loop over the data 32 bits at a + time until we find a 32-bit block that doesn't match; then we find + the first non-matching bit and use that to calculate the total + length of the match. */ + while (s2_ptr <= s2_limit - 4 && + BrotliUnalignedRead32(s2_ptr) == + BrotliUnalignedRead32(s1 + matched)) { + s2_ptr += 4; + matched += 4; + } + while ((s2_ptr < s2_limit) && (s1[matched] == *s2_ptr)) { + ++s2_ptr; + ++matched; + } + return matched; +} +#endif + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif + +#endif /* BROTLI_ENC_FIND_MATCH_LENGTH_H_ */ diff --git a/packages/runtime/vendor/brotli/c/enc/hash.h b/packages/runtime/vendor/brotli/c/enc/hash.h new file mode 100644 index 000000000..d86fa1895 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/hash.h @@ -0,0 +1,735 @@ +/* Copyright 2010 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* A (forgetful) hash table to the data seen by the compressor, to + help create backward references to previous data. */ + +#ifndef BROTLI_ENC_HASH_H_ +#define BROTLI_ENC_HASH_H_ + +#include "../common/constants.h" +#include "../common/dictionary.h" +#include "../common/platform.h" +#include "compound_dictionary.h" +#include "encoder_dict.h" +#include "fast_log.h" +#include "find_match_length.h" +#include "hash_base.h" +#include "matching_tag_mask.h" +#include "memory.h" +#include "params.h" +#include "quality.h" +#include "static_dict.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +typedef struct { + /** + * Dynamically allocated areas; regular hasher uses one or two allocations; + * "composite" hasher uses up to 4 allocations. + */ + void* extra[4]; + + /** + * False before the first invocation of HasherSetup (where "extra" memory) + * is allocated. + */ + BROTLI_BOOL is_setup_; + + size_t dict_num_lookups; + size_t dict_num_matches; + + BrotliHasherParams params; + + /** + * False if hasher needs to be "prepared" before use (before the first + * invocation of HasherSetup or after HasherReset). "preparation" is hasher + * data initialization (using input ringbuffer). + */ + BROTLI_BOOL is_prepared_; +} HasherCommon; + +#define score_t size_t + +static const uint32_t kCutoffTransformsCount = 10; +/* 0, 12, 27, 23, 42, 63, 56, 48, 59, 64 */ +/* 0+0, 4+8, 8+19, 12+11, 16+26, 20+43, 24+32, 28+20, 32+27, 36+28 */ +static const uint64_t kCutoffTransforms = + BROTLI_MAKE_UINT64_T(0x071B520A, 0xDA2D3200); + +typedef struct HasherSearchResult { + size_t len; + size_t distance; + score_t score; + int len_code_delta; /* == len_code - len */ +} HasherSearchResult; + +static BROTLI_INLINE void PrepareDistanceCache( + int* BROTLI_RESTRICT distance_cache, const int num_distances) { + if (num_distances > 4) { + int last_distance = distance_cache[0]; + distance_cache[4] = last_distance - 1; + distance_cache[5] = last_distance + 1; + distance_cache[6] = last_distance - 2; + distance_cache[7] = last_distance + 2; + distance_cache[8] = last_distance - 3; + distance_cache[9] = last_distance + 3; + if (num_distances > 10) { + int next_last_distance = distance_cache[1]; + distance_cache[10] = next_last_distance - 1; + distance_cache[11] = next_last_distance + 1; + distance_cache[12] = next_last_distance - 2; + distance_cache[13] = next_last_distance + 2; + distance_cache[14] = next_last_distance - 3; + distance_cache[15] = next_last_distance + 3; + } + } +} + +#define BROTLI_LITERAL_BYTE_SCORE 135 +#define BROTLI_DISTANCE_BIT_PENALTY 30 +/* Score must be positive after applying maximal penalty. */ +#define BROTLI_SCORE_BASE (BROTLI_DISTANCE_BIT_PENALTY * 8 * sizeof(size_t)) + +/* Usually, we always choose the longest backward reference. This function + allows for the exception of that rule. + + If we choose a backward reference that is further away, it will + usually be coded with more bits. We approximate this by assuming + log2(distance). If the distance can be expressed in terms of the + last four distances, we use some heuristic constants to estimate + the bits cost. For the first up to four literals we use the bit + cost of the literals from the literal cost model, after that we + use the average bit cost of the cost model. + + This function is used to sometimes discard a longer backward reference + when it is not much longer and the bit cost for encoding it is more + than the saved literals. + + backward_reference_offset MUST be positive. */ +static BROTLI_INLINE score_t BackwardReferenceScore( + size_t copy_length, size_t backward_reference_offset) { + return BROTLI_SCORE_BASE + BROTLI_LITERAL_BYTE_SCORE * (score_t)copy_length - + BROTLI_DISTANCE_BIT_PENALTY * Log2FloorNonZero(backward_reference_offset); +} + +static BROTLI_INLINE score_t BackwardReferenceScoreUsingLastDistance( + size_t copy_length) { + return BROTLI_LITERAL_BYTE_SCORE * (score_t)copy_length + + BROTLI_SCORE_BASE + 15; +} + +static BROTLI_INLINE score_t BackwardReferencePenaltyUsingLastDistance( + size_t distance_short_code) { + return (score_t)39 + ((0x1CA10 >> (distance_short_code & 0xE)) & 0xE); +} + +static BROTLI_INLINE BROTLI_BOOL TestStaticDictionaryItem( + const BrotliEncoderDictionary* dictionary, size_t len, size_t word_idx, + const uint8_t* data, size_t max_length, size_t max_backward, + size_t max_distance, HasherSearchResult* out) { + size_t offset; + size_t matchlen; + size_t backward; + score_t score; + offset = dictionary->words->offsets_by_length[len] + len * word_idx; + if (len > max_length) { + return BROTLI_FALSE; + } + + matchlen = + FindMatchLengthWithLimit(data, &dictionary->words->data[offset], len); + if (matchlen + dictionary->cutoffTransformsCount <= len || matchlen == 0) { + return BROTLI_FALSE; + } + { + size_t cut = len - matchlen; + size_t transform_id = (cut << 2) + + (size_t)((dictionary->cutoffTransforms >> (cut * 6)) & 0x3F); + backward = max_backward + 1 + word_idx + + (transform_id << dictionary->words->size_bits_by_length[len]); + } + if (backward > max_distance) { + return BROTLI_FALSE; + } + score = BackwardReferenceScore(matchlen, backward); + if (score < out->score) { + return BROTLI_FALSE; + } + out->len = matchlen; + out->len_code_delta = (int)len - (int)matchlen; + out->distance = backward; + out->score = score; + return BROTLI_TRUE; +} + +static BROTLI_INLINE void SearchInStaticDictionary( + const BrotliEncoderDictionary* dictionary, + HasherCommon* common, const uint8_t* data, size_t max_length, + size_t max_backward, size_t max_distance, + HasherSearchResult* out, BROTLI_BOOL shallow) { + size_t key; + size_t i; + if (common->dict_num_matches < (common->dict_num_lookups >> 7)) { + return; + } + key = Hash14(data) << 1; + for (i = 0; i < (shallow ? 1u : 2u); ++i, ++key) { + common->dict_num_lookups++; + if (dictionary->hash_table_lengths[key] != 0) { + BROTLI_BOOL item_matches = TestStaticDictionaryItem( + dictionary, dictionary->hash_table_lengths[key], + dictionary->hash_table_words[key], data, + max_length, max_backward, max_distance, out); + if (item_matches) { + common->dict_num_matches++; + } + } + } +} + +typedef struct BackwardMatch { + uint32_t distance; + uint32_t length_and_code; +} BackwardMatch; + +static BROTLI_INLINE void InitBackwardMatch(BackwardMatch* self, + size_t dist, size_t len) { + self->distance = (uint32_t)dist; + self->length_and_code = (uint32_t)(len << 5); +} + +static BROTLI_INLINE void InitDictionaryBackwardMatch(BackwardMatch* self, + size_t dist, size_t len, size_t len_code) { + self->distance = (uint32_t)dist; + self->length_and_code = + (uint32_t)((len << 5) | (len == len_code ? 0 : len_code)); +} + +static BROTLI_INLINE size_t BackwardMatchLength(const BackwardMatch* self) { + return self->length_and_code >> 5; +} + +static BROTLI_INLINE size_t BackwardMatchLengthCode(const BackwardMatch* self) { + size_t code = self->length_and_code & 31; + return code ? code : BackwardMatchLength(self); +} + +#define EXPAND_CAT(a, b) CAT(a, b) +#define CAT(a, b) a ## b +#define FN(X) EXPAND_CAT(X, HASHER()) + +#define HASHER() H10 +#define BUCKET_BITS 17 +#define MAX_TREE_SEARCH_DEPTH 64 +#define MAX_TREE_COMP_LENGTH 128 +#include "hash_to_binary_tree_inc.h" /* NOLINT(build/include) */ +#undef MAX_TREE_SEARCH_DEPTH +#undef MAX_TREE_COMP_LENGTH +#undef BUCKET_BITS +#undef HASHER +/* MAX_NUM_MATCHES == 64 + MAX_TREE_SEARCH_DEPTH */ +#define MAX_NUM_MATCHES_H10 128 + +/* For BUCKET_SWEEP_BITS == 0, enabling the dictionary lookup makes compression + a little faster (0.5% - 1%) and it compresses 0.15% better on small text + and HTML inputs. */ + +#define HASHER() H2 +#define BUCKET_BITS 16 +#define BUCKET_SWEEP_BITS 0 +#define HASH_LEN 5 +#define USE_DICTIONARY 1 +#include "hash_longest_match_quickly_inc.h" /* NOLINT(build/include) */ +#undef BUCKET_SWEEP_BITS +#undef USE_DICTIONARY +#undef HASHER + +#define HASHER() H3 +#define BUCKET_SWEEP_BITS 1 +#define USE_DICTIONARY 0 +#include "hash_longest_match_quickly_inc.h" /* NOLINT(build/include) */ +#undef USE_DICTIONARY +#undef BUCKET_SWEEP_BITS +#undef BUCKET_BITS +#undef HASHER + +#define HASHER() H4 +#define BUCKET_BITS 17 +#define BUCKET_SWEEP_BITS 2 +#define USE_DICTIONARY 1 +#include "hash_longest_match_quickly_inc.h" /* NOLINT(build/include) */ +#undef USE_DICTIONARY +#undef HASH_LEN +#undef BUCKET_SWEEP_BITS +#undef BUCKET_BITS +#undef HASHER + +#define HASHER() H5 +#include "hash_longest_match_inc.h" /* NOLINT(build/include) */ +#undef HASHER + +#define HASHER() H6 +#include "hash_longest_match64_inc.h" /* NOLINT(build/include) */ +#undef HASHER + +#if defined(BROTLI_MAX_SIMD_QUALITY) +#define HASHER() H58 +#include "hash_longest_match_simd_inc.h" /* NOLINT(build/include) */ +#undef HASHER + +#define HASHER() H68 +#include "hash_longest_match64_simd_inc.h" /* NOLINT(build/include) */ +#undef HASHER +#endif + +#define BUCKET_BITS 15 + +#define NUM_LAST_DISTANCES_TO_CHECK 4 +#define NUM_BANKS 1 +#define BANK_BITS 16 +#define HASHER() H40 +#include "hash_forgetful_chain_inc.h" /* NOLINT(build/include) */ +#undef HASHER +#undef NUM_LAST_DISTANCES_TO_CHECK + +#define NUM_LAST_DISTANCES_TO_CHECK 10 +#define HASHER() H41 +#include "hash_forgetful_chain_inc.h" /* NOLINT(build/include) */ +#undef HASHER +#undef NUM_LAST_DISTANCES_TO_CHECK +#undef NUM_BANKS +#undef BANK_BITS + +#define NUM_LAST_DISTANCES_TO_CHECK 16 +#define NUM_BANKS 512 +#define BANK_BITS 9 +#define HASHER() H42 +#include "hash_forgetful_chain_inc.h" /* NOLINT(build/include) */ +#undef HASHER +#undef NUM_LAST_DISTANCES_TO_CHECK +#undef NUM_BANKS +#undef BANK_BITS + +#undef BUCKET_BITS + +#define HASHER() H54 +#define BUCKET_BITS 20 +#define BUCKET_SWEEP_BITS 2 +#define HASH_LEN 7 +#define USE_DICTIONARY 0 +#include "hash_longest_match_quickly_inc.h" /* NOLINT(build/include) */ +#undef USE_DICTIONARY +#undef HASH_LEN +#undef BUCKET_SWEEP_BITS +#undef BUCKET_BITS +#undef HASHER + +/* fast large window hashers */ + +#define HASHER() HROLLING_FAST +#define CHUNKLEN 32 +#define JUMP 4 +#define NUMBUCKETS 16777216 +#define MASK ((NUMBUCKETS * 64) - 1) +#include "hash_rolling_inc.h" /* NOLINT(build/include) */ +#undef JUMP +#undef HASHER + + +#define HASHER() HROLLING +#define JUMP 1 +#include "hash_rolling_inc.h" /* NOLINT(build/include) */ +#undef MASK +#undef NUMBUCKETS +#undef JUMP +#undef CHUNKLEN +#undef HASHER + +#define HASHER() H35 +#define HASHER_A H3 +#define HASHER_B HROLLING_FAST +#include "hash_composite_inc.h" /* NOLINT(build/include) */ +#undef HASHER_A +#undef HASHER_B +#undef HASHER + +#define HASHER() H55 +#define HASHER_A H54 +#define HASHER_B HROLLING_FAST +#include "hash_composite_inc.h" /* NOLINT(build/include) */ +#undef HASHER_A +#undef HASHER_B +#undef HASHER + +#define HASHER() H65 +#define HASHER_A H6 +#define HASHER_B HROLLING +#include "hash_composite_inc.h" /* NOLINT(build/include) */ +#undef HASHER_A +#undef HASHER_B +#undef HASHER + +#undef FN +#undef CAT +#undef EXPAND_CAT + +#if defined(BROTLI_MAX_SIMD_QUALITY) +#define FOR_SIMPLE_HASHERS(H) \ + H(2) H(3) H(4) H(5) H(6) H(40) H(41) H(42) H(54) H(58) H(68) +#else +#define FOR_SIMPLE_HASHERS(H) \ + H(2) H(3) H(4) H(5) H(6) H(40) H(41) H(42) H(54) +#endif +#define FOR_COMPOSITE_HASHERS(H) H(35) H(55) H(65) +#define FOR_GENERIC_HASHERS(H) FOR_SIMPLE_HASHERS(H) FOR_COMPOSITE_HASHERS(H) +#define FOR_ALL_HASHERS(H) FOR_GENERIC_HASHERS(H) H(10) + +typedef struct { + HasherCommon common; + + union { +#define MEMBER_(N) \ + H ## N _H ## N; + FOR_ALL_HASHERS(MEMBER_) +#undef MEMBER_ + } privat; +} Hasher; + +/* MUST be invoked before any other method. */ +static BROTLI_INLINE void HasherInit(Hasher* hasher) { + hasher->common.is_setup_ = BROTLI_FALSE; + hasher->common.extra[0] = NULL; + hasher->common.extra[1] = NULL; + hasher->common.extra[2] = NULL; + hasher->common.extra[3] = NULL; +} + +static BROTLI_INLINE void DestroyHasher(MemoryManager* m, Hasher* hasher) { + if (hasher->common.extra[0] != NULL) BROTLI_FREE(m, hasher->common.extra[0]); + if (hasher->common.extra[1] != NULL) BROTLI_FREE(m, hasher->common.extra[1]); + if (hasher->common.extra[2] != NULL) BROTLI_FREE(m, hasher->common.extra[2]); + if (hasher->common.extra[3] != NULL) BROTLI_FREE(m, hasher->common.extra[3]); +} + +static BROTLI_INLINE void HasherReset(Hasher* hasher) { + hasher->common.is_prepared_ = BROTLI_FALSE; +} + +static BROTLI_INLINE void HasherSize(const BrotliEncoderParams* params, + BROTLI_BOOL one_shot, const size_t input_size, size_t* alloc_size) { + switch (params->hasher.type) { +#define SIZE_(N) \ + case N: \ + HashMemAllocInBytesH ## N(params, one_shot, input_size, alloc_size); \ + break; + FOR_ALL_HASHERS(SIZE_) +#undef SIZE_ + default: + break; + } +} + +static BROTLI_INLINE void HasherSetup(MemoryManager* m, Hasher* hasher, + BrotliEncoderParams* params, const uint8_t* data, size_t position, + size_t input_size, BROTLI_BOOL is_last) { + BROTLI_BOOL one_shot = (position == 0 && is_last); + if (!hasher->common.is_setup_) { + size_t alloc_size[4] = {0}; + size_t i; + ChooseHasher(params, ¶ms->hasher); + hasher->common.params = params->hasher; + hasher->common.dict_num_lookups = 0; + hasher->common.dict_num_matches = 0; + HasherSize(params, one_shot, input_size, alloc_size); + for (i = 0; i < 4; ++i) { + if (alloc_size[i] == 0) continue; + hasher->common.extra[i] = BROTLI_ALLOC(m, uint8_t, alloc_size[i]); + if (BROTLI_IS_OOM(m) || BROTLI_IS_NULL(hasher->common.extra[i])) return; + } + switch (hasher->common.params.type) { +#define INITIALIZE_(N) \ + case N: \ + InitializeH ## N(&hasher->common, \ + &hasher->privat._H ## N, params); \ + break; + FOR_ALL_HASHERS(INITIALIZE_); +#undef INITIALIZE_ + default: + break; + } + HasherReset(hasher); + hasher->common.is_setup_ = BROTLI_TRUE; + } + + if (!hasher->common.is_prepared_) { + switch (hasher->common.params.type) { +#define PREPARE_(N) \ + case N: \ + PrepareH ## N( \ + &hasher->privat._H ## N, \ + one_shot, input_size, data); \ + break; + FOR_ALL_HASHERS(PREPARE_) +#undef PREPARE_ + default: break; + } + hasher->common.is_prepared_ = BROTLI_TRUE; + } +} + +static BROTLI_INLINE void InitOrStitchToPreviousBlock( + MemoryManager* m, Hasher* hasher, const uint8_t* data, size_t mask, + BrotliEncoderParams* params, size_t position, size_t input_size, + BROTLI_BOOL is_last) { + HasherSetup(m, hasher, params, data, position, input_size, is_last); + if (BROTLI_IS_OOM(m)) return; + switch (hasher->common.params.type) { +#define INIT_(N) \ + case N: \ + StitchToPreviousBlockH ## N( \ + &hasher->privat._H ## N, \ + input_size, position, data, mask); \ + break; + FOR_ALL_HASHERS(INIT_) +#undef INIT_ + default: break; + } +} + +/* NB: when seamless dictionary-ring-buffer copies are implemented, don't forget + to add proper guards for non-zero-BROTLI_PARAM_STREAM_OFFSET. */ +static BROTLI_INLINE void FindCompoundDictionaryMatch( + const PreparedDictionary* self, const uint8_t* BROTLI_RESTRICT data, + const size_t ring_buffer_mask, const int* BROTLI_RESTRICT distance_cache, + const size_t cur_ix, const size_t max_length, const size_t distance_offset, + const size_t max_distance, HasherSearchResult* BROTLI_RESTRICT out) { + const uint32_t source_size = self->source_size; + const size_t boundary = distance_offset - source_size; + const uint32_t hash_bits = self->hash_bits; + const uint32_t bucket_bits = self->bucket_bits; + const uint32_t slot_bits = self->slot_bits; + + const uint32_t hash_shift = 64u - bucket_bits; + const uint32_t slot_mask = (~((uint32_t)0U)) >> (32 - slot_bits); + const uint64_t hash_mask = (~((uint64_t)0U)) >> (64 - hash_bits); + + const uint32_t* slot_offsets = (uint32_t*)(&self[1]); + const uint16_t* heads = (uint16_t*)(&slot_offsets[(size_t)1u << slot_bits]); + const uint32_t* items = (uint32_t*)(&heads[(size_t)1u << bucket_bits]); + const uint8_t* source = NULL; + + const size_t cur_ix_masked = cur_ix & ring_buffer_mask; + score_t best_score = out->score; + size_t best_len = out->len; + size_t i; + const uint64_t h = + (BROTLI_UNALIGNED_LOAD64LE(&data[cur_ix_masked]) & hash_mask) * + kPreparedDictionaryHashMul64Long; + const uint32_t key = (uint32_t)(h >> hash_shift); + const uint32_t slot = key & slot_mask; + const uint32_t head = heads[key]; + const uint32_t* BROTLI_RESTRICT chain = &items[slot_offsets[slot] + head]; + uint32_t item = (head == 0xFFFF) ? 1 : 0; + + const void* tail = (void*)&items[self->num_items]; + if (self->magic == kPreparedDictionaryMagic) { + source = (const uint8_t*)tail; + } else { + /* kLeanPreparedDictionaryMagic */ + source = (const uint8_t*)BROTLI_UNALIGNED_LOAD_PTR((const uint8_t**)tail); + } + + BROTLI_DCHECK(cur_ix_masked + max_length <= ring_buffer_mask); + + for (i = 0; i < 4; ++i) { + const size_t distance = (size_t)distance_cache[i]; + size_t offset; + size_t limit; + size_t len; + if (distance <= boundary || distance > distance_offset) continue; + offset = distance_offset - distance; + limit = source_size - offset; + limit = limit > max_length ? max_length : limit; + len = FindMatchLengthWithLimit(&source[offset], &data[cur_ix_masked], + limit); + if (len >= 2) { + score_t score = BackwardReferenceScoreUsingLastDistance(len); + if (best_score < score) { + if (i != 0) score -= BackwardReferencePenaltyUsingLastDistance(i); + if (best_score < score) { + best_score = score; + if (len > best_len) best_len = len; + out->len = len; + out->len_code_delta = 0; + out->distance = distance; + out->score = best_score; + } + } + } + } + /* we require matches of len >4, so increase best_len to 3, so we can compare + * 4 bytes all the time. */ + if (best_len < 3) { + best_len = 3; + } + while (item == 0) { + size_t offset; + size_t distance; + size_t limit; + item = *chain; + chain++; + offset = item & 0x7FFFFFFF; + item &= 0x80000000; + distance = distance_offset - offset; + limit = source_size - offset; + limit = (limit > max_length) ? max_length : limit; + if (distance > max_distance) continue; + if (cur_ix_masked + best_len > ring_buffer_mask || best_len >= limit || + /* compare 4 bytes ending at best_len + 1 */ + BrotliUnalignedRead32(&data[cur_ix_masked + best_len - 3]) != + BrotliUnalignedRead32(&source[offset + best_len - 3])) { + continue; + } + { + const size_t len = FindMatchLengthWithLimit(&source[offset], + &data[cur_ix_masked], + limit); + if (len >= 4) { + score_t score = BackwardReferenceScore(len, distance); + if (best_score < score) { + best_score = score; + best_len = len; + out->len = best_len; + out->len_code_delta = 0; + out->distance = distance; + out->score = best_score; + } + } + } + } +} + +/* NB: when seamless dictionary-ring-buffer copies are implemented, don't forget + to add proper guards for non-zero-BROTLI_PARAM_STREAM_OFFSET. */ +static BROTLI_INLINE size_t FindAllCompoundDictionaryMatches( + const PreparedDictionary* self, const uint8_t* BROTLI_RESTRICT data, + const size_t ring_buffer_mask, const size_t cur_ix, const size_t min_length, + const size_t max_length, const size_t distance_offset, + const size_t max_distance, BackwardMatch* matches, size_t match_limit) { + const uint32_t source_size = self->source_size; + const uint32_t hash_bits = self->hash_bits; + const uint32_t bucket_bits = self->bucket_bits; + const uint32_t slot_bits = self->slot_bits; + + const uint32_t hash_shift = 64u - bucket_bits; + const uint32_t slot_mask = (~((uint32_t)0U)) >> (32 - slot_bits); + const uint64_t hash_mask = (~((uint64_t)0U)) >> (64 - hash_bits); + + const uint32_t* slot_offsets = (uint32_t*)(&self[1]); + const uint16_t* heads = (uint16_t*)(&slot_offsets[(size_t)1u << slot_bits]); + const uint32_t* items = (uint32_t*)(&heads[(size_t)1u << bucket_bits]); + const uint8_t* source = NULL; + + const size_t cur_ix_masked = cur_ix & ring_buffer_mask; + size_t best_len = min_length; + const uint64_t h = + (BROTLI_UNALIGNED_LOAD64LE(&data[cur_ix_masked]) & hash_mask) * + kPreparedDictionaryHashMul64Long; + const uint32_t key = (uint32_t)(h >> hash_shift); + const uint32_t slot = key & slot_mask; + const uint32_t head = heads[key]; + const uint32_t* BROTLI_RESTRICT chain = &items[slot_offsets[slot] + head]; + uint32_t item = (head == 0xFFFF) ? 1 : 0; + size_t found = 0; + + const void* tail = (void*)&items[self->num_items]; + if (self->magic == kPreparedDictionaryMagic) { + source = (const uint8_t*)tail; + } else { + /* kLeanPreparedDictionaryMagic */ + source = (const uint8_t*)BROTLI_UNALIGNED_LOAD_PTR((const uint8_t**)tail); + } + + BROTLI_DCHECK(cur_ix_masked + max_length <= ring_buffer_mask); + + while (item == 0) { + size_t offset; + size_t distance; + size_t limit; + size_t len; + item = *chain; + chain++; + offset = item & 0x7FFFFFFF; + item &= 0x80000000; + distance = distance_offset - offset; + limit = source_size - offset; + limit = (limit > max_length) ? max_length : limit; + if (distance > max_distance) continue; + if (cur_ix_masked + best_len > ring_buffer_mask || + best_len >= limit || + data[cur_ix_masked + best_len] != source[offset + best_len]) { + continue; + } + len = FindMatchLengthWithLimit( + &source[offset], &data[cur_ix_masked], limit); + if (len > best_len) { + best_len = len; + InitBackwardMatch(matches++, distance, len); + found++; + if (found == match_limit) break; + } + } + return found; +} + +static BROTLI_INLINE void LookupCompoundDictionaryMatch( + const CompoundDictionary* addon, const uint8_t* BROTLI_RESTRICT data, + const size_t ring_buffer_mask, const int* BROTLI_RESTRICT distance_cache, + const size_t cur_ix, const size_t max_length, + const size_t max_ring_buffer_distance, const size_t max_distance, + HasherSearchResult* sr) { + size_t base_offset = max_ring_buffer_distance + 1 + addon->total_size - 1; + size_t d; + for (d = 0; d < addon->num_chunks; ++d) { + /* Only one prepared dictionary type is currently supported. */ + FindCompoundDictionaryMatch( + (const PreparedDictionary*)addon->chunks[d], data, ring_buffer_mask, + distance_cache, cur_ix, max_length, + base_offset - addon->chunk_offsets[d], max_distance, sr); + } +} + +static BROTLI_INLINE size_t LookupAllCompoundDictionaryMatches( + const CompoundDictionary* addon, const uint8_t* BROTLI_RESTRICT data, + const size_t ring_buffer_mask, const size_t cur_ix, size_t min_length, + const size_t max_length, const size_t max_ring_buffer_distance, + const size_t max_distance, BackwardMatch* matches, + size_t match_limit) { + size_t base_offset = max_ring_buffer_distance + 1 + addon->total_size - 1; + size_t d; + size_t total_found = 0; + for (d = 0; d < addon->num_chunks; ++d) { + /* Only one prepared dictionary type is currently supported. */ + total_found += FindAllCompoundDictionaryMatches( + (const PreparedDictionary*)addon->chunks[d], data, ring_buffer_mask, + cur_ix, min_length, max_length, base_offset - addon->chunk_offsets[d], + max_distance, matches + total_found, match_limit - total_found); + if (total_found == match_limit) break; + if (total_found > 0) { + min_length = BackwardMatchLength(&matches[total_found - 1]); + } + } + return total_found; +} + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif + +#endif /* BROTLI_ENC_HASH_H_ */ diff --git a/packages/runtime/vendor/brotli/c/enc/hash_base.h b/packages/runtime/vendor/brotli/c/enc/hash_base.h new file mode 100644 index 000000000..edf1ef020 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/hash_base.h @@ -0,0 +1,38 @@ +/* Copyright 2025 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* Basic common hash functions / constants. */ + +#ifndef THIRD_PARTY_BROTLI_ENC_HASH_BASE_H_ +#define THIRD_PARTY_BROTLI_ENC_HASH_BASE_H_ + +#include "../common/platform.h" + +/* kHashMul32 multiplier has these properties: + * The multiplier must be odd. Otherwise we may lose the highest bit. + * No long streaks of ones or zeros. + * There is no effort to ensure that it is a prime, the oddity is enough + for this use. + * The number has been tuned heuristically against compression benchmarks. */ +static const uint32_t kHashMul32 = 0x1E35A7BD; +static const uint64_t kHashMul64 = + BROTLI_MAKE_UINT64_T(0x1FE35A7Bu, 0xD3579BD3u); + +static BROTLI_INLINE uint32_t Hash14(const uint8_t* data) { + uint32_t h = BROTLI_UNALIGNED_LOAD32LE(data) * kHashMul32; + /* The higher bits contain more mixture from the multiplication, + so we take our results from there. */ + return h >> (32 - 14); +} + +static BROTLI_INLINE uint32_t Hash15(const uint8_t* data) { + uint32_t h = BROTLI_UNALIGNED_LOAD32LE(data) * kHashMul32; + /* The higher bits contain more mixture from the multiplication, + so we take our results from there. */ + return h >> (32 - 15); +} + +#endif // THIRD_PARTY_BROTLI_ENC_HASH_BASE_H_ diff --git a/packages/runtime/vendor/brotli/c/enc/hash_composite_inc.h b/packages/runtime/vendor/brotli/c/enc/hash_composite_inc.h new file mode 100644 index 000000000..3923bc72c --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/hash_composite_inc.h @@ -0,0 +1,140 @@ +/* NOLINT(build/header_guard) */ +/* Copyright 2018 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* template parameters: FN, HASHER_A, HASHER_B */ + +/* Composite hasher: This hasher allows to combine two other hashers, HASHER_A + and HASHER_B. */ + +#define HashComposite HASHER() + +#define FN_A(X) EXPAND_CAT(X, HASHER_A) +#define FN_B(X) EXPAND_CAT(X, HASHER_B) + +static BROTLI_INLINE size_t FN(HashTypeLength)(void) { + size_t a = FN_A(HashTypeLength)(); + size_t b = FN_B(HashTypeLength)(); + return a > b ? a : b; +} + +static BROTLI_INLINE size_t FN(StoreLookahead)(void) { + size_t a = FN_A(StoreLookahead)(); + size_t b = FN_B(StoreLookahead)(); + return a > b ? a : b; +} + +typedef struct HashComposite { + HASHER_A ha; + HASHER_B hb; + HasherCommon ha_common; + HasherCommon hb_common; + + /* Shortcuts. */ + HasherCommon* common; + + BROTLI_BOOL fresh; + const BrotliEncoderParams* params; +} HashComposite; + +static void FN(Initialize)(HasherCommon* common, + HashComposite* BROTLI_RESTRICT self, const BrotliEncoderParams* params) { + self->common = common; + + self->ha_common = *self->common; + self->hb_common = *self->common; + self->fresh = BROTLI_TRUE; + self->params = params; + /* TODO(lode): Initialize of the hashers is deferred to Prepare (and params + remembered here) because we don't get the one_shot and input_size params + here that are needed to know the memory size of them. Instead provide + those params to all hashers FN(Initialize) */ +} + +static void FN(Prepare)( + HashComposite* BROTLI_RESTRICT self, BROTLI_BOOL one_shot, + size_t input_size, const uint8_t* BROTLI_RESTRICT data) { + if (self->fresh) { + self->fresh = BROTLI_FALSE; + self->ha_common.extra[0] = self->common->extra[0]; + self->ha_common.extra[1] = self->common->extra[1]; + self->ha_common.extra[2] = NULL; + self->ha_common.extra[3] = NULL; + self->hb_common.extra[0] = self->common->extra[2]; + self->hb_common.extra[1] = self->common->extra[3]; + self->hb_common.extra[2] = NULL; + self->hb_common.extra[3] = NULL; + + FN_A(Initialize)(&self->ha_common, &self->ha, self->params); + FN_B(Initialize)(&self->hb_common, &self->hb, self->params); + } + FN_A(Prepare)(&self->ha, one_shot, input_size, data); + FN_B(Prepare)(&self->hb, one_shot, input_size, data); +} + +static BROTLI_INLINE void FN(HashMemAllocInBytes)( + const BrotliEncoderParams* params, BROTLI_BOOL one_shot, + size_t input_size, size_t* alloc_size) { + size_t alloc_size_a[4] = {0}; + size_t alloc_size_b[4] = {0}; + FN_A(HashMemAllocInBytes)(params, one_shot, input_size, alloc_size_a); + FN_B(HashMemAllocInBytes)(params, one_shot, input_size, alloc_size_b); + /* Should never happen. */ + if (alloc_size_a[2] != 0 || alloc_size_a[3] != 0) exit(EXIT_FAILURE); + if (alloc_size_b[2] != 0 || alloc_size_b[3] != 0) exit(EXIT_FAILURE); + alloc_size[0] = alloc_size_a[0]; + alloc_size[1] = alloc_size_a[1]; + alloc_size[2] = alloc_size_b[0]; + alloc_size[3] = alloc_size_b[1]; +} + +static BROTLI_INLINE void FN(Store)(HashComposite* BROTLI_RESTRICT self, + const uint8_t* BROTLI_RESTRICT data, const size_t mask, const size_t ix) { + FN_A(Store)(&self->ha, data, mask, ix); + FN_B(Store)(&self->hb, data, mask, ix); +} + +static BROTLI_INLINE void FN(StoreRange)( + HashComposite* BROTLI_RESTRICT self, const uint8_t* BROTLI_RESTRICT data, + const size_t mask, const size_t ix_start, + const size_t ix_end) { + FN_A(StoreRange)(&self->ha, data, mask, ix_start, ix_end); + FN_B(StoreRange)(&self->hb, data, mask, ix_start, ix_end); +} + +static BROTLI_INLINE void FN(StitchToPreviousBlock)( + HashComposite* BROTLI_RESTRICT self, + size_t num_bytes, size_t position, const uint8_t* ringbuffer, + size_t ring_buffer_mask) { + FN_A(StitchToPreviousBlock)(&self->ha, num_bytes, position, + ringbuffer, ring_buffer_mask); + FN_B(StitchToPreviousBlock)(&self->hb, num_bytes, position, + ringbuffer, ring_buffer_mask); +} + +static BROTLI_INLINE void FN(PrepareDistanceCache)( + HashComposite* BROTLI_RESTRICT self, int* BROTLI_RESTRICT distance_cache) { + FN_A(PrepareDistanceCache)(&self->ha, distance_cache); + FN_B(PrepareDistanceCache)(&self->hb, distance_cache); +} + +static BROTLI_INLINE void FN(FindLongestMatch)( + HashComposite* BROTLI_RESTRICT self, + const BrotliEncoderDictionary* dictionary, + const uint8_t* BROTLI_RESTRICT data, const size_t ring_buffer_mask, + const int* BROTLI_RESTRICT distance_cache, const size_t cur_ix, + const size_t max_length, const size_t max_backward, + const size_t dictionary_distance, const size_t max_distance, + HasherSearchResult* BROTLI_RESTRICT out) { + FN_A(FindLongestMatch)(&self->ha, dictionary, data, ring_buffer_mask, + distance_cache, cur_ix, max_length, max_backward, dictionary_distance, + max_distance, out); + FN_B(FindLongestMatch)(&self->hb, dictionary, data, ring_buffer_mask, + distance_cache, cur_ix, max_length, max_backward, dictionary_distance, + max_distance, out); +} + +#undef HashComposite diff --git a/packages/runtime/vendor/brotli/c/enc/hash_forgetful_chain_inc.h b/packages/runtime/vendor/brotli/c/enc/hash_forgetful_chain_inc.h new file mode 100644 index 000000000..c44276fa4 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/hash_forgetful_chain_inc.h @@ -0,0 +1,305 @@ +/* NOLINT(build/header_guard) */ +/* Copyright 2016 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* template parameters: FN, BUCKET_BITS, NUM_BANKS, BANK_BITS, + NUM_LAST_DISTANCES_TO_CHECK */ + +/* A (forgetful) hash table to the data seen by the compressor, to + help create backward references to previous data. + + Hashes are stored in chains which are bucketed to groups. Group of chains + share a storage "bank". When more than "bank size" chain nodes are added, + oldest nodes are replaced; this way several chains may share a tail. */ + +#define HashForgetfulChain HASHER() + +#define BANK_SIZE (1 << BANK_BITS) + +/* Number of hash buckets. */ +#define BUCKET_SIZE (1 << BUCKET_BITS) + +#define CAPPED_CHAINS 0 + +static BROTLI_INLINE size_t FN(HashTypeLength)(void) { return 4; } +static BROTLI_INLINE size_t FN(StoreLookahead)(void) { return 4; } + +/* HashBytes is the function that chooses the bucket to place the address in.*/ +static BROTLI_INLINE size_t FN(HashBytes)(const uint8_t* BROTLI_RESTRICT data) { + const uint32_t h = BROTLI_UNALIGNED_LOAD32LE(data) * kHashMul32; + /* The higher bits contain more mixture from the multiplication, + so we take our results from there. */ + return h >> (32 - BUCKET_BITS); +} + +typedef struct FN(Slot) { + uint16_t delta; + uint16_t next; +} FN(Slot); + +typedef struct FN(Bank) { + FN(Slot) slots[BANK_SIZE]; +} FN(Bank); + +typedef struct HashForgetfulChain { + uint16_t free_slot_idx[NUM_BANKS]; /* Up to 1KiB. Move to dynamic? */ + size_t max_hops; + + /* Shortcuts. */ + void* extra[2]; + HasherCommon* common; + + /* --- Dynamic size members --- */ + + /* uint32_t addr[BUCKET_SIZE]; */ + + /* uint16_t head[BUCKET_SIZE]; */ + + /* Truncated hash used for quick rejection of "distance cache" candidates. */ + /* uint8_t tiny_hash[65536];*/ + + /* FN(Bank) banks[NUM_BANKS]; */ +} HashForgetfulChain; + +static uint32_t* FN(Addr)(void* extra) { + return (uint32_t*)extra; +} + +static uint16_t* FN(Head)(void* extra) { + return (uint16_t*)(&FN(Addr)(extra)[BUCKET_SIZE]); +} + +static uint8_t* FN(TinyHash)(void* extra) { + return (uint8_t*)(&FN(Head)(extra)[BUCKET_SIZE]); +} + +static FN(Bank)* FN(Banks)(void* extra) { + return (FN(Bank)*)(extra); +} + +static void FN(Initialize)( + HasherCommon* common, HashForgetfulChain* BROTLI_RESTRICT self, + const BrotliEncoderParams* params) { + self->common = common; + self->extra[0] = common->extra[0]; + self->extra[1] = common->extra[1]; + + self->max_hops = (params->quality > 6 ? 7u : 8u) << (params->quality - 4); +} + +static void FN(Prepare)( + HashForgetfulChain* BROTLI_RESTRICT self, BROTLI_BOOL one_shot, + size_t input_size, const uint8_t* BROTLI_RESTRICT data) { + uint32_t* BROTLI_RESTRICT addr = FN(Addr)(self->extra[0]); + uint16_t* BROTLI_RESTRICT head = FN(Head)(self->extra[0]); + uint8_t* BROTLI_RESTRICT tiny_hash = FN(TinyHash)(self->extra[0]); + /* Partial preparation is 100 times slower (per socket). */ + size_t partial_prepare_threshold = BUCKET_SIZE >> 6; + if (one_shot && input_size <= partial_prepare_threshold) { + size_t i; + for (i = 0; i < input_size; ++i) { + size_t bucket = FN(HashBytes)(&data[i]); + /* See InitEmpty comment. */ + addr[bucket] = 0xCCCCCCCC; + head[bucket] = 0xCCCC; + } + } else { + /* Fill |addr| array with 0xCCCCCCCC value. Because of wrapping, position + processed by hasher never reaches 3GB + 64M; this makes all new chains + to be terminated after the first node. */ + memset(addr, 0xCC, sizeof(uint32_t) * BUCKET_SIZE); + memset(head, 0, sizeof(uint16_t) * BUCKET_SIZE); + } + memset(tiny_hash, 0, sizeof(uint8_t) * 65536); + memset(self->free_slot_idx, 0, sizeof(self->free_slot_idx)); +} + +static BROTLI_INLINE void FN(HashMemAllocInBytes)( + const BrotliEncoderParams* params, BROTLI_BOOL one_shot, + size_t input_size, size_t* alloc_size) { + BROTLI_UNUSED(params); + BROTLI_UNUSED(one_shot); + BROTLI_UNUSED(input_size); + alloc_size[0] = sizeof(uint32_t) * BUCKET_SIZE + + sizeof(uint16_t) * BUCKET_SIZE + sizeof(uint8_t) * 65536; + alloc_size[1] = sizeof(FN(Bank)) * NUM_BANKS; +} + +/* Look at 4 bytes at &data[ix & mask]. Compute a hash from these, and prepend + node to corresponding chain; also update tiny_hash for current position. */ +static BROTLI_INLINE void FN(Store)(HashForgetfulChain* BROTLI_RESTRICT self, + const uint8_t* BROTLI_RESTRICT data, const size_t mask, const size_t ix) { + uint32_t* BROTLI_RESTRICT addr = FN(Addr)(self->extra[0]); + uint16_t* BROTLI_RESTRICT head = FN(Head)(self->extra[0]); + uint8_t* BROTLI_RESTRICT tiny_hash = FN(TinyHash)(self->extra[0]); + FN(Bank)* BROTLI_RESTRICT banks = FN(Banks)(self->extra[1]); + const size_t key = FN(HashBytes)(&data[ix & mask]); + const size_t bank = key & (NUM_BANKS - 1); + const size_t idx = self->free_slot_idx[bank]++ & (BANK_SIZE - 1); + size_t delta = ix - addr[key]; + tiny_hash[(uint16_t)ix] = (uint8_t)key; + if (delta > 0xFFFF) delta = CAPPED_CHAINS ? 0 : 0xFFFF; + banks[bank].slots[idx].delta = (uint16_t)delta; + banks[bank].slots[idx].next = head[key]; + addr[key] = (uint32_t)ix; + head[key] = (uint16_t)idx; +} + +static BROTLI_INLINE void FN(StoreRange)( + HashForgetfulChain* BROTLI_RESTRICT self, + const uint8_t* BROTLI_RESTRICT data, const size_t mask, + const size_t ix_start, const size_t ix_end) { + size_t i; + for (i = ix_start; i < ix_end; ++i) { + FN(Store)(self, data, mask, i); + } +} + +static BROTLI_INLINE void FN(StitchToPreviousBlock)( + HashForgetfulChain* BROTLI_RESTRICT self, + size_t num_bytes, size_t position, const uint8_t* ringbuffer, + size_t ring_buffer_mask) { + if (num_bytes >= FN(HashTypeLength)() - 1 && position >= 3) { + /* Prepare the hashes for three last bytes of the last write. + These could not be calculated before, since they require knowledge + of both the previous and the current block. */ + FN(Store)(self, ringbuffer, ring_buffer_mask, position - 3); + FN(Store)(self, ringbuffer, ring_buffer_mask, position - 2); + FN(Store)(self, ringbuffer, ring_buffer_mask, position - 1); + } +} + +static BROTLI_INLINE void FN(PrepareDistanceCache)( + HashForgetfulChain* BROTLI_RESTRICT self, + int* BROTLI_RESTRICT distance_cache) { + BROTLI_UNUSED(self); + PrepareDistanceCache(distance_cache, NUM_LAST_DISTANCES_TO_CHECK); +} + +/* Find a longest backward match of &data[cur_ix] up to the length of + max_length and stores the position cur_ix in the hash table. + + REQUIRES: FN(PrepareDistanceCache) must be invoked for current distance cache + values; if this method is invoked repeatedly with the same distance + cache values, it is enough to invoke FN(PrepareDistanceCache) once. + + Does not look for matches longer than max_length. + Does not look for matches further away than max_backward. + Writes the best match into |out|. + |out|->score is updated only if a better match is found. */ +static BROTLI_INLINE void FN(FindLongestMatch)( + HashForgetfulChain* BROTLI_RESTRICT self, + const BrotliEncoderDictionary* dictionary, + const uint8_t* BROTLI_RESTRICT data, const size_t ring_buffer_mask, + const int* BROTLI_RESTRICT distance_cache, + const size_t cur_ix, const size_t max_length, const size_t max_backward, + const size_t dictionary_distance, const size_t max_distance, + HasherSearchResult* BROTLI_RESTRICT out) { + uint32_t* BROTLI_RESTRICT addr = FN(Addr)(self->extra[0]); + uint16_t* BROTLI_RESTRICT head = FN(Head)(self->extra[0]); + uint8_t* BROTLI_RESTRICT tiny_hashes = FN(TinyHash)(self->extra[0]); + FN(Bank)* BROTLI_RESTRICT banks = FN(Banks)(self->extra[1]); + const size_t cur_ix_masked = cur_ix & ring_buffer_mask; + /* Don't accept a short copy from far away. */ + score_t min_score = out->score; + score_t best_score = out->score; + size_t best_len = out->len; + size_t i; + const size_t key = FN(HashBytes)(&data[cur_ix_masked]); + const uint8_t tiny_hash = (uint8_t)(key); + out->len = 0; + out->len_code_delta = 0; + + BROTLI_DCHECK(cur_ix_masked + max_length <= ring_buffer_mask); + + /* Try last distance first. */ + for (i = 0; i < NUM_LAST_DISTANCES_TO_CHECK; ++i) { + const size_t backward = (size_t)distance_cache[i]; + size_t prev_ix = (cur_ix - backward); + /* For distance code 0 we want to consider 2-byte matches. */ + if (i > 0 && tiny_hashes[(uint16_t)prev_ix] != tiny_hash) continue; + if (prev_ix >= cur_ix || backward > max_backward) { + continue; + } + prev_ix &= ring_buffer_mask; + { + const size_t len = FindMatchLengthWithLimit(&data[prev_ix], + &data[cur_ix_masked], + max_length); + if (len >= 2) { + score_t score = BackwardReferenceScoreUsingLastDistance(len); + if (best_score < score) { + if (i != 0) score -= BackwardReferencePenaltyUsingLastDistance(i); + if (best_score < score) { + best_score = score; + best_len = len; + out->len = best_len; + out->distance = backward; + out->score = best_score; + } + } + } + } + } + /* we require matches of len >4, so increase best_len to 3, so we can compare + * 4 bytes all the time. */ + if (best_len < 3) { + best_len = 3; + } + { + const size_t bank = key & (NUM_BANKS - 1); + size_t backward = 0; + size_t hops = self->max_hops; + size_t delta = cur_ix - addr[key]; + size_t slot = head[key]; + while (hops--) { + size_t prev_ix; + size_t last = slot; + backward += delta; + if (backward > max_backward || (CAPPED_CHAINS && !delta)) break; + prev_ix = (cur_ix - backward) & ring_buffer_mask; + slot = banks[bank].slots[last].next; + delta = banks[bank].slots[last].delta; + if (cur_ix_masked + best_len > ring_buffer_mask || + prev_ix + best_len > ring_buffer_mask || + /* compare 4 bytes ending at best_len + 1 */ + BrotliUnalignedRead32(&data[cur_ix_masked + best_len - 3]) != + BrotliUnalignedRead32(&data[prev_ix + best_len - 3])) { + continue; + } + { + const size_t len = FindMatchLengthWithLimit(&data[prev_ix], + &data[cur_ix_masked], + max_length); + if (len >= 4) { + /* Comparing for >= 3 does not change the semantics, but just saves + for a few unnecessary binary logarithms in backward reference + score, since we are not interested in such short matches. */ + score_t score = BackwardReferenceScore(len, backward); + if (best_score < score) { + best_score = score; + best_len = len; + out->len = best_len; + out->distance = backward; + out->score = best_score; + } + } + } + } + FN(Store)(self, data, ring_buffer_mask, cur_ix); + } + if (out->score == min_score) { + SearchInStaticDictionary(dictionary, + self->common, &data[cur_ix_masked], max_length, dictionary_distance, + max_distance, out, BROTLI_FALSE); + } +} + +#undef BANK_SIZE +#undef BUCKET_SIZE +#undef CAPPED_CHAINS + +#undef HashForgetfulChain diff --git a/packages/runtime/vendor/brotli/c/enc/hash_longest_match64_inc.h b/packages/runtime/vendor/brotli/c/enc/hash_longest_match64_inc.h new file mode 100644 index 000000000..bdbb30aa5 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/hash_longest_match64_inc.h @@ -0,0 +1,279 @@ +/* NOLINT(build/header_guard) */ +/* Copyright 2010 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* template parameters: FN */ + +/* A (forgetful) hash table to the data seen by the compressor, to + help create backward references to previous data. + + This is a hash map of fixed size (bucket_size_) to a ring buffer of + fixed size (block_size_). The ring buffer contains the last block_size_ + index positions of the given hash key in the compressed data. */ + +#define HashLongestMatch HASHER() + +static BROTLI_INLINE size_t FN(HashTypeLength)(void) { return 8; } +static BROTLI_INLINE size_t FN(StoreLookahead)(void) { return 8; } + +/* HashBytes is the function that chooses the bucket to place the address in. */ +static BROTLI_INLINE size_t FN(HashBytes)(const uint8_t* BROTLI_RESTRICT data, + uint64_t hash_mul) { + const uint64_t h = BROTLI_UNALIGNED_LOAD64LE(data) * hash_mul; + /* The higher bits contain more mixture from the multiplication, + so we take our results from there. */ + return (size_t)(h >> (64 - 15)); +} + +typedef struct HashLongestMatch { + /* Number of hash buckets. */ + size_t bucket_size_; + /* Only block_size_ newest backward references are kept, + and the older are forgotten. */ + size_t block_size_; + /* Hash multiplier tuned to match length. */ + uint64_t hash_mul_; + /* Mask for accessing entries in a block (in a ring-buffer manner). */ + uint32_t block_mask_; + + int block_bits_; + int num_last_distances_to_check_; + + /* Shortcuts. */ + HasherCommon* common_; + + /* --- Dynamic size members --- */ + + /* Number of entries in a particular bucket. */ + uint16_t* num_; /* uint16_t[bucket_size]; */ + + /* Buckets containing block_size_ of backward references. */ + uint32_t* buckets_; /* uint32_t[bucket_size * block_size]; */ +} HashLongestMatch; + +static void FN(Initialize)( + HasherCommon* common, HashLongestMatch* BROTLI_RESTRICT self, + const BrotliEncoderParams* params) { + self->common_ = common; + + BROTLI_UNUSED(params); + self->hash_mul_ = kHashMul64 << (64 - 5 * 8); + BROTLI_DCHECK(common->params.bucket_bits == 15); + self->bucket_size_ = (size_t)1 << common->params.bucket_bits; + self->block_bits_ = common->params.block_bits; + self->block_size_ = (size_t)1 << common->params.block_bits; + self->block_mask_ = (uint32_t)(self->block_size_ - 1); + self->num_last_distances_to_check_ = + common->params.num_last_distances_to_check; + self->num_ = (uint16_t*)common->extra[0]; + self->buckets_ = (uint32_t*)common->extra[1]; +} + +static void FN(Prepare)( + HashLongestMatch* BROTLI_RESTRICT self, BROTLI_BOOL one_shot, + size_t input_size, const uint8_t* BROTLI_RESTRICT data) { + uint16_t* BROTLI_RESTRICT num = self->num_; + /* Partial preparation is 100 times slower (per socket). */ + size_t partial_prepare_threshold = self->bucket_size_ >> 6; + if (one_shot && input_size <= partial_prepare_threshold) { + size_t i; + for (i = 0; i < input_size; ++i) { + const size_t key = FN(HashBytes)(&data[i], self->hash_mul_); + num[key] = 0; + } + } else { + memset(num, 0, self->bucket_size_ * sizeof(num[0])); + } +} + +static BROTLI_INLINE void FN(HashMemAllocInBytes)( + const BrotliEncoderParams* params, BROTLI_BOOL one_shot, + size_t input_size, size_t* alloc_size) { + size_t bucket_size = (size_t)1 << params->hasher.bucket_bits; + size_t block_size = (size_t)1 << params->hasher.block_bits; + BROTLI_UNUSED(one_shot); + BROTLI_UNUSED(input_size); + alloc_size[0] = sizeof(uint16_t) * bucket_size; + alloc_size[1] = sizeof(uint32_t) * bucket_size * block_size; +} + +/* Look at 4 bytes at &data[ix & mask]. + Compute a hash from these, and store the value of ix at that position. */ +static BROTLI_INLINE void FN(Store)( + HashLongestMatch* BROTLI_RESTRICT self, const uint8_t* BROTLI_RESTRICT data, + const size_t mask, const size_t ix) { + uint16_t* BROTLI_RESTRICT num = self->num_; + uint32_t* BROTLI_RESTRICT buckets = self->buckets_; + const size_t key = FN(HashBytes)(&data[ix & mask], self->hash_mul_); + const size_t minor_ix = num[key] & self->block_mask_; + const size_t offset = minor_ix + (key << self->block_bits_); + ++num[key]; + buckets[offset] = (uint32_t)ix; +} + +static BROTLI_INLINE void FN(StoreRange)(HashLongestMatch* BROTLI_RESTRICT self, + const uint8_t* BROTLI_RESTRICT data, const size_t mask, + const size_t ix_start, const size_t ix_end) { + size_t i; + for (i = ix_start; i < ix_end; ++i) { + FN(Store)(self, data, mask, i); + } +} + +static BROTLI_INLINE void FN(StitchToPreviousBlock)( + HashLongestMatch* BROTLI_RESTRICT self, + size_t num_bytes, size_t position, const uint8_t* ringbuffer, + size_t ringbuffer_mask) { + if (num_bytes >= FN(HashTypeLength)() - 1 && position >= 3) { + /* Prepare the hashes for three last bytes of the last write. + These could not be calculated before, since they require knowledge + of both the previous and the current block. */ + FN(Store)(self, ringbuffer, ringbuffer_mask, position - 3); + FN(Store)(self, ringbuffer, ringbuffer_mask, position - 2); + FN(Store)(self, ringbuffer, ringbuffer_mask, position - 1); + } +} + +static BROTLI_INLINE void FN(PrepareDistanceCache)( + HashLongestMatch* BROTLI_RESTRICT self, + int* BROTLI_RESTRICT distance_cache) { + PrepareDistanceCache(distance_cache, self->num_last_distances_to_check_); +} + +/* Find a longest backward match of &data[cur_ix] up to the length of + max_length and stores the position cur_ix in the hash table. + + REQUIRES: FN(PrepareDistanceCache) must be invoked for current distance cache + values; if this method is invoked repeatedly with the same distance + cache values, it is enough to invoke FN(PrepareDistanceCache) once. + + Does not look for matches longer than max_length. + Does not look for matches further away than max_backward. + Writes the best match into |out|. + |out|->score is updated only if a better match is found. */ +static BROTLI_INLINE void FN(FindLongestMatch)( + HashLongestMatch* BROTLI_RESTRICT self, + const BrotliEncoderDictionary* dictionary, + const uint8_t* BROTLI_RESTRICT data, const size_t ring_buffer_mask, + const int* BROTLI_RESTRICT distance_cache, const size_t cur_ix, + const size_t max_length, const size_t max_backward, + const size_t dictionary_distance, const size_t max_distance, + HasherSearchResult* BROTLI_RESTRICT out) { + uint16_t* BROTLI_RESTRICT num = self->num_; + uint32_t* BROTLI_RESTRICT buckets = self->buckets_; + const size_t cur_ix_masked = cur_ix & ring_buffer_mask; + /* Don't accept a short copy from far away. */ + score_t min_score = out->score; + score_t best_score = out->score; + size_t best_len = out->len; + size_t i; + /* Precalculate the hash key and prefetch the bucket. */ + const size_t key = FN(HashBytes)(&data[cur_ix_masked], self->hash_mul_); + uint32_t* BROTLI_RESTRICT bucket = &buckets[key << self->block_bits_]; + PREFETCH_L1(bucket); + if (self->block_bits_ > 4) PREFETCH_L1(bucket + 16); + out->len = 0; + out->len_code_delta = 0; + + BROTLI_DCHECK(cur_ix_masked + max_length <= ring_buffer_mask); + + /* Try last distance first. */ + for (i = 0; i < (size_t)self->num_last_distances_to_check_; ++i) { + const size_t backward = (size_t)distance_cache[i]; + size_t prev_ix = (size_t)(cur_ix - backward); + if (prev_ix >= cur_ix) { + continue; + } + if (BROTLI_PREDICT_FALSE(backward > max_backward)) { + continue; + } + prev_ix &= ring_buffer_mask; + + if (cur_ix_masked + best_len > ring_buffer_mask) { + break; + } + if (prev_ix + best_len > ring_buffer_mask || + data[cur_ix_masked + best_len] != data[prev_ix + best_len]) { + continue; + } + { + const size_t len = FindMatchLengthWithLimit(&data[prev_ix], + &data[cur_ix_masked], + max_length); + if (len >= 3 || (len == 2 && i < 2)) { + /* Comparing for >= 2 does not change the semantics, but just saves for + a few unnecessary binary logarithms in backward reference score, + since we are not interested in such short matches. */ + score_t score = BackwardReferenceScoreUsingLastDistance(len); + if (best_score < score) { + if (i != 0) score -= BackwardReferencePenaltyUsingLastDistance(i); + if (best_score < score) { + best_score = score; + best_len = len; + out->len = best_len; + out->distance = backward; + out->score = best_score; + } + } + } + } + } + /* we require matches of len >4, so increase best_len to 3, so we can compare + * 4 bytes all the time. */ + if (best_len < 3) { + best_len = 3; + } + { + const size_t down = + (num[key] > self->block_size_) ? + (num[key] - self->block_size_) : 0u; + const uint32_t first4 = BrotliUnalignedRead32(data + cur_ix_masked); + const size_t max_length_m4 = max_length - 4; + i = num[key]; + for (; i > down;) { + size_t prev_ix = bucket[--i & self->block_mask_]; + uint32_t current4; + const size_t backward = cur_ix - prev_ix; + if (BROTLI_PREDICT_FALSE(backward > max_backward)) { + break; + } + prev_ix &= ring_buffer_mask; + if (cur_ix_masked + best_len > ring_buffer_mask) { + break; + } + if (prev_ix + best_len > ring_buffer_mask || + /* compare 4 bytes ending at best_len + 1 */ + BrotliUnalignedRead32(&data[cur_ix_masked + best_len - 3]) != + BrotliUnalignedRead32(&data[prev_ix + best_len - 3])) { + continue; + } + current4 = BrotliUnalignedRead32(data + prev_ix); + if (first4 != current4) continue; + { + const size_t len = FindMatchLengthWithLimit(&data[prev_ix + 4], + &data[cur_ix_masked + 4], + max_length_m4) + 4; + const score_t score = BackwardReferenceScore(len, backward); + if (best_score < score) { + best_score = score; + best_len = len; + out->len = best_len; + out->distance = backward; + out->score = best_score; + } + } + } + bucket[num[key] & self->block_mask_] = (uint32_t)cur_ix; + ++num[key]; + } + if (min_score == out->score) { + SearchInStaticDictionary(dictionary, + self->common_, &data[cur_ix_masked], max_length, dictionary_distance, + max_distance, out, BROTLI_FALSE); + } +} + +#undef HashLongestMatch diff --git a/packages/runtime/vendor/brotli/c/enc/hash_longest_match64_simd_inc.h b/packages/runtime/vendor/brotli/c/enc/hash_longest_match64_simd_inc.h new file mode 100644 index 000000000..12bd8cffd --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/hash_longest_match64_simd_inc.h @@ -0,0 +1,304 @@ +/* NOLINT(build/header_guard) */ +/* Copyright 2010 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* template parameters: FN */ + +/* A (forgetful) hash table to the data seen by the compressor, to + help create backward references to previous data. + + This is a hash map of fixed size (bucket_size_) to a ring buffer of + fixed size (block_size_). The ring buffer contains the last block_size_ + index positions of the given hash key in the compressed data. */ + +#define HashLongestMatch HASHER() + +#define TAG_HASH_BITS 8 +#define TAG_HASH_MASK ((1 << TAG_HASH_BITS) - 1) + +static BROTLI_INLINE size_t FN(HashTypeLength)(void) { return 8; } +static BROTLI_INLINE size_t FN(StoreLookahead)(void) { return 8; } + +/* HashBytes is the function that chooses the bucket to place the address in. */ +static BROTLI_INLINE size_t FN(HashBytes)(const uint8_t* BROTLI_RESTRICT data, + uint64_t hash_mul) { + const uint64_t h = BROTLI_UNALIGNED_LOAD64LE(data) * hash_mul; + /* The higher bits contain more mixture from the multiplication, + so we take our results from there. */ + return (size_t)(h >> (64 - 15 - TAG_HASH_BITS)); +} + +typedef struct HashLongestMatch { + /* Number of hash buckets. */ + size_t bucket_size_; + /* Only block_size_ newest backward references are kept, + and the older are forgotten. */ + size_t block_size_; + /* Hash multiplier tuned to match length. */ + uint64_t hash_mul_; + /* Mask for accessing entries in a block (in a ring-buffer manner). */ + uint32_t block_mask_; + + int block_bits_; + int num_last_distances_to_check_; + + /* Shortcuts. */ + HasherCommon* common_; + + /* --- Dynamic size members --- */ + + /* Number of entries in a particular bucket. */ + uint16_t* num_; /* uint16_t[bucket_size]; */ + + uint8_t* tags_; + + /* Buckets containing block_size_ of backward references. */ + uint32_t* buckets_; /* uint32_t[bucket_size * block_size]; */ +} HashLongestMatch; + +static void FN(Initialize)( + HasherCommon* common, HashLongestMatch* BROTLI_RESTRICT self, + const BrotliEncoderParams* params) { + self->common_ = common; + + BROTLI_UNUSED(params); + self->hash_mul_ = kHashMul64 << (64 - 5 * 8); + BROTLI_DCHECK(common->params.bucket_bits == 15); + self->bucket_size_ = (size_t)1 << common->params.bucket_bits; + self->block_bits_ = common->params.block_bits; + self->block_size_ = (size_t)1 << common->params.block_bits; + self->block_mask_ = (uint32_t)(self->block_size_ - 1); + self->num_last_distances_to_check_ = + common->params.num_last_distances_to_check; + self->num_ = (uint16_t*)common->extra[0]; + self->tags_ = (uint8_t*)common->extra[1]; + self->buckets_ = (uint32_t*)common->extra[2]; +} + +static void FN(Prepare)( + HashLongestMatch* BROTLI_RESTRICT self, BROTLI_BOOL one_shot, + size_t input_size, const uint8_t* BROTLI_RESTRICT data) { + uint16_t* BROTLI_RESTRICT num = self->num_; + /* Partial preparation is 100 times slower (per socket). */ + size_t partial_prepare_threshold = self->bucket_size_ >> 6; + if (one_shot && input_size <= partial_prepare_threshold) { + size_t i; + for (i = 0; i < input_size; ++i) { + const size_t hash = FN(HashBytes)(&data[i], self->hash_mul_); + const size_t key = hash >> TAG_HASH_BITS; + num[key] = 65535; + } + } else { + /* Set all the bytes of num to 255, which makes each uint16_t 65535. */ + memset(num, 255, self->bucket_size_ * sizeof(num[0])); + } +} + +static BROTLI_INLINE void FN(HashMemAllocInBytes)( + const BrotliEncoderParams* params, BROTLI_BOOL one_shot, + size_t input_size, size_t* alloc_size) { + size_t bucket_size = (size_t)1 << params->hasher.bucket_bits; + size_t block_size = (size_t)1 << params->hasher.block_bits; + BROTLI_UNUSED(one_shot); + BROTLI_UNUSED(input_size); + alloc_size[0] = sizeof(uint16_t) * bucket_size; + alloc_size[1] = sizeof(uint8_t) * bucket_size * block_size; + alloc_size[2] = sizeof(uint32_t) * bucket_size * block_size; +} + +/* Look at 4 bytes at &data[ix & mask]. + Compute a hash from these, and store the value of ix at that position. */ +static BROTLI_INLINE void FN(Store)( + HashLongestMatch* BROTLI_RESTRICT self, const uint8_t* BROTLI_RESTRICT data, + const size_t mask, const size_t ix) { + uint16_t* BROTLI_RESTRICT num = self->num_; + uint8_t* BROTLI_RESTRICT tags = self->tags_; + uint32_t* BROTLI_RESTRICT buckets = self->buckets_; + const size_t hash = FN(HashBytes)(&data[ix & mask], self->hash_mul_); + const size_t key = hash >> TAG_HASH_BITS; + const uint8_t tag = hash & TAG_HASH_MASK; + const size_t minor_ix = num[key] & self->block_mask_; + const size_t offset = minor_ix + (key << self->block_bits_); + --num[key]; + buckets[offset] = (uint32_t)ix; + tags[offset] = tag; +} + +static BROTLI_INLINE void FN(StoreRange)(HashLongestMatch* BROTLI_RESTRICT self, + const uint8_t* BROTLI_RESTRICT data, const size_t mask, + const size_t ix_start, const size_t ix_end) { + size_t i; + for (i = ix_start; i < ix_end; ++i) { + FN(Store)(self, data, mask, i); + } +} + +static BROTLI_INLINE void FN(StitchToPreviousBlock)( + HashLongestMatch* BROTLI_RESTRICT self, + size_t num_bytes, size_t position, const uint8_t* ringbuffer, + size_t ringbuffer_mask) { + if (num_bytes >= FN(HashTypeLength)() - 1 && position >= 3) { + /* Prepare the hashes for three last bytes of the last write. + These could not be calculated before, since they require knowledge + of both the previous and the current block. */ + FN(Store)(self, ringbuffer, ringbuffer_mask, position - 3); + FN(Store)(self, ringbuffer, ringbuffer_mask, position - 2); + FN(Store)(self, ringbuffer, ringbuffer_mask, position - 1); + } +} + +static BROTLI_INLINE void FN(PrepareDistanceCache)( + HashLongestMatch* BROTLI_RESTRICT self, + int* BROTLI_RESTRICT distance_cache) { + PrepareDistanceCache(distance_cache, self->num_last_distances_to_check_); +} + +/* Find a longest backward match of &data[cur_ix] up to the length of + max_length and stores the position cur_ix in the hash table. + + REQUIRES: FN(PrepareDistanceCache) must be invoked for current distance cache + values; if this method is invoked repeatedly with the same distance + cache values, it is enough to invoke FN(PrepareDistanceCache) once. + + Does not look for matches longer than max_length. + Does not look for matches further away than max_backward. + Writes the best match into |out|. + |out|->score is updated only if a better match is found. */ +static BROTLI_INLINE void FN(FindLongestMatch)( + HashLongestMatch* BROTLI_RESTRICT self, + const BrotliEncoderDictionary* dictionary, + const uint8_t* BROTLI_RESTRICT data, const size_t ring_buffer_mask, + const int* BROTLI_RESTRICT distance_cache, const size_t cur_ix, + const size_t max_length, const size_t max_backward, + const size_t dictionary_distance, const size_t max_distance, + HasherSearchResult* BROTLI_RESTRICT out) { + uint16_t* BROTLI_RESTRICT num = self->num_; + uint32_t* BROTLI_RESTRICT buckets = self->buckets_; + uint8_t* BROTLI_RESTRICT tags = self->tags_; + const size_t cur_ix_masked = cur_ix & ring_buffer_mask; + /* Don't accept a short copy from far away. */ + score_t min_score = out->score; + score_t best_score = out->score; + size_t best_len = out->len; + size_t i; + /* Precalculate the hash key and prefetch the bucket. */ + const size_t hash = FN(HashBytes)(&data[cur_ix_masked], self->hash_mul_); + const size_t key = hash >> TAG_HASH_BITS; + uint32_t* BROTLI_RESTRICT bucket = &buckets[key << self->block_bits_]; + uint8_t* BROTLI_RESTRICT tag_bucket = &tags[key << self->block_bits_]; + PREFETCH_L1(bucket); + PREFETCH_L1(tag_bucket); + if (self->block_bits_ > 4) PREFETCH_L1(bucket + 16); + out->len = 0; + out->len_code_delta = 0; + + BROTLI_DCHECK(cur_ix_masked + max_length <= ring_buffer_mask); + + /* Try last distance first. */ + for (i = 0; i < (size_t)self->num_last_distances_to_check_; ++i) { + const size_t backward = (size_t)distance_cache[i]; + size_t prev_ix = (size_t)(cur_ix - backward); + if (prev_ix >= cur_ix) { + continue; + } + if (BROTLI_PREDICT_FALSE(backward > max_backward)) { + continue; + } + prev_ix &= ring_buffer_mask; + + if (cur_ix_masked + best_len > ring_buffer_mask) { + break; + } + if (prev_ix + best_len > ring_buffer_mask || + data[cur_ix_masked + best_len] != data[prev_ix + best_len]) { + continue; + } + { + const size_t len = FindMatchLengthWithLimit(&data[prev_ix], + &data[cur_ix_masked], + max_length); + if (len >= 3 || (len == 2 && i < 2)) { + /* Comparing for >= 2 does not change the semantics, but just saves for + a few unnecessary binary logarithms in backward reference score, + since we are not interested in such short matches. */ + score_t score = BackwardReferenceScoreUsingLastDistance(len); + if (best_score < score) { + if (i != 0) score -= BackwardReferencePenaltyUsingLastDistance(i); + if (best_score < score) { + best_score = score; + best_len = len; + out->len = best_len; + out->distance = backward; + out->score = best_score; + } + } + } + } + } + /* we require matches of len >4, so increase best_len to 3, so we can compare + * 4 bytes all the time. */ + if (best_len < 3) { + best_len = 3; + } + { + const uint8_t tag = hash & TAG_HASH_MASK; + const uint32_t first4 = BrotliUnalignedRead32(data + cur_ix_masked); + const size_t max_length_m4 = max_length - 4; + const size_t head = (num[key] + 1) & self->block_mask_; + uint64_t matches = + GetMatchingTagMask(self->block_size_ / 16, tag, tag_bucket, head); + /* Mask off any matches from uninitialized tags. */ + uint16_t n = 65535 - num[key]; + uint64_t block_has_unused_slots = self->block_size_ > n; + uint64_t mask = (block_has_unused_slots << (n & (64 - 1))) - 1; + matches &= mask; + for (; matches > 0; matches &= (matches - 1)) { + const size_t rb_index = + (head + (size_t)BROTLI_TZCNT64(matches)) & self->block_mask_; + size_t prev_ix = bucket[rb_index]; + uint32_t current4; + const size_t backward = cur_ix - prev_ix; + if (BROTLI_PREDICT_FALSE(backward > max_backward)) { + break; + } + prev_ix &= ring_buffer_mask; + if (cur_ix_masked + best_len > ring_buffer_mask) { + break; + } + if (prev_ix + best_len > ring_buffer_mask || + /* compare 4 bytes ending at best_len + 1 */ + BrotliUnalignedRead32(&data[cur_ix_masked + best_len - 3]) != + BrotliUnalignedRead32(&data[prev_ix + best_len - 3])) { + continue; + } + current4 = BrotliUnalignedRead32(data + prev_ix); + if (first4 != current4) continue; + { + const size_t len = FindMatchLengthWithLimit(&data[prev_ix + 4], + &data[cur_ix_masked + 4], + max_length_m4) + 4; + const score_t score = BackwardReferenceScore(len, backward); + if (best_score < score) { + best_score = score; + best_len = len; + out->len = best_len; + out->distance = backward; + out->score = best_score; + } + } + } + bucket[num[key] & self->block_mask_] = (uint32_t)cur_ix; + tag_bucket[num[key] & self->block_mask_] = tag; + --num[key]; + } + if (min_score == out->score) { + SearchInStaticDictionary(dictionary, + self->common_, &data[cur_ix_masked], max_length, dictionary_distance, + max_distance, out, BROTLI_FALSE); + } +} + +#undef HashLongestMatch diff --git a/packages/runtime/vendor/brotli/c/enc/hash_longest_match_inc.h b/packages/runtime/vendor/brotli/c/enc/hash_longest_match_inc.h new file mode 100644 index 000000000..58e88b3a6 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/hash_longest_match_inc.h @@ -0,0 +1,277 @@ +/* NOLINT(build/header_guard) */ +/* Copyright 2010 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* template parameters: FN */ + +/* A (forgetful) hash table to the data seen by the compressor, to + help create backward references to previous data. + + This is a hash map of fixed size (bucket_size_) to a ring buffer of + fixed size (block_size_). The ring buffer contains the last block_size_ + index positions of the given hash key in the compressed data. */ + +#define HashLongestMatch HASHER() + +static BROTLI_INLINE size_t FN(HashTypeLength)(void) { return 4; } +static BROTLI_INLINE size_t FN(StoreLookahead)(void) { return 4; } + +/* HashBytes is the function that chooses the bucket to place the address in. */ +static uint32_t FN(HashBytes)( + const uint8_t* BROTLI_RESTRICT data, const int shift) { + uint32_t h = BROTLI_UNALIGNED_LOAD32LE(data) * kHashMul32; + /* The higher bits contain more mixture from the multiplication, + so we take our results from there. */ + return (uint32_t)(h >> shift); +} + +typedef struct HashLongestMatch { + /* Number of hash buckets. */ + size_t bucket_size_; + /* Only block_size_ newest backward references are kept, + and the older are forgotten. */ + size_t block_size_; + /* Left-shift for computing hash bucket index from hash value. */ + int hash_shift_; + /* Mask for accessing entries in a block (in a ring-buffer manner). */ + uint32_t block_mask_; + + int block_bits_; + int num_last_distances_to_check_; + + /* Shortcuts. */ + HasherCommon* common_; + + /* --- Dynamic size members --- */ + + /* Number of entries in a particular bucket. */ + uint16_t* num_; /* uint16_t[bucket_size]; */ + + /* Buckets containing block_size_ of backward references. */ + uint32_t* buckets_; /* uint32_t[bucket_size * block_size]; */ +} HashLongestMatch; + +static void FN(Initialize)( + HasherCommon* common, HashLongestMatch* BROTLI_RESTRICT self, + const BrotliEncoderParams* params) { + self->common_ = common; + + BROTLI_UNUSED(params); + self->hash_shift_ = 32 - common->params.bucket_bits; + self->bucket_size_ = (size_t)1 << common->params.bucket_bits; + self->block_size_ = (size_t)1 << common->params.block_bits; + self->block_mask_ = (uint32_t)(self->block_size_ - 1); + self->num_ = (uint16_t*)common->extra[0]; + self->buckets_ = (uint32_t*)common->extra[1]; + self->block_bits_ = common->params.block_bits; + self->num_last_distances_to_check_ = + common->params.num_last_distances_to_check; +} + +static void FN(Prepare)( + HashLongestMatch* BROTLI_RESTRICT self, BROTLI_BOOL one_shot, + size_t input_size, const uint8_t* BROTLI_RESTRICT data) { + uint16_t* BROTLI_RESTRICT num = self->num_; + /* Partial preparation is 100 times slower (per socket). */ + size_t partial_prepare_threshold = self->bucket_size_ >> 6; + if (one_shot && input_size <= partial_prepare_threshold) { + size_t i; + for (i = 0; i < input_size; ++i) { + const uint32_t key = FN(HashBytes)(&data[i], self->hash_shift_); + num[key] = 0; + } + } else { + memset(num, 0, self->bucket_size_ * sizeof(num[0])); + } +} + +static BROTLI_INLINE void FN(HashMemAllocInBytes)( + const BrotliEncoderParams* params, BROTLI_BOOL one_shot, + size_t input_size, size_t* alloc_size) { + size_t bucket_size = (size_t)1 << params->hasher.bucket_bits; + size_t block_size = (size_t)1 << params->hasher.block_bits; + BROTLI_UNUSED(one_shot); + BROTLI_UNUSED(input_size); + alloc_size[0] = sizeof(uint16_t) * bucket_size; + alloc_size[1] = sizeof(uint32_t) * bucket_size * block_size; +} + +/* Look at 4 bytes at &data[ix & mask]. + Compute a hash from these, and store the value of ix at that position. */ +static BROTLI_INLINE void FN(Store)( + HashLongestMatch* BROTLI_RESTRICT self, const uint8_t* BROTLI_RESTRICT data, + const size_t mask, const size_t ix) { + uint16_t* BROTLI_RESTRICT num = self->num_; + uint32_t* BROTLI_RESTRICT buckets = self->buckets_; + const uint32_t key = FN(HashBytes)(&data[ix & mask], self->hash_shift_); + const size_t minor_ix = num[key] & self->block_mask_; + const size_t offset = minor_ix + (key << self->block_bits_); + ++num[key]; + buckets[offset] = (uint32_t)ix; +} + +static BROTLI_INLINE void FN(StoreRange)(HashLongestMatch* BROTLI_RESTRICT self, + const uint8_t* BROTLI_RESTRICT data, const size_t mask, + const size_t ix_start, const size_t ix_end) { + size_t i; + for (i = ix_start; i < ix_end; ++i) { + FN(Store)(self, data, mask, i); + } +} + +static BROTLI_INLINE void FN(StitchToPreviousBlock)( + HashLongestMatch* BROTLI_RESTRICT self, + size_t num_bytes, size_t position, const uint8_t* ringbuffer, + size_t ringbuffer_mask) { + if (num_bytes >= FN(HashTypeLength)() - 1 && position >= 3) { + /* Prepare the hashes for three last bytes of the last write. + These could not be calculated before, since they require knowledge + of both the previous and the current block. */ + FN(Store)(self, ringbuffer, ringbuffer_mask, position - 3); + FN(Store)(self, ringbuffer, ringbuffer_mask, position - 2); + FN(Store)(self, ringbuffer, ringbuffer_mask, position - 1); + } +} + +static BROTLI_INLINE void FN(PrepareDistanceCache)( + HashLongestMatch* BROTLI_RESTRICT self, + int* BROTLI_RESTRICT distance_cache) { + PrepareDistanceCache(distance_cache, self->num_last_distances_to_check_); +} + +/* Find a longest backward match of &data[cur_ix] up to the length of + max_length and stores the position cur_ix in the hash table. + + REQUIRES: FN(PrepareDistanceCache) must be invoked for current distance cache + values; if this method is invoked repeatedly with the same distance + cache values, it is enough to invoke FN(PrepareDistanceCache) once. + + Does not look for matches longer than max_length. + Does not look for matches further away than max_backward. + Writes the best match into |out|. + |out|->score is updated only if a better match is found. */ +static BROTLI_INLINE void FN(FindLongestMatch)( + HashLongestMatch* BROTLI_RESTRICT self, + const BrotliEncoderDictionary* dictionary, + const uint8_t* BROTLI_RESTRICT data, const size_t ring_buffer_mask, + const int* BROTLI_RESTRICT distance_cache, const size_t cur_ix, + const size_t max_length, const size_t max_backward, + const size_t dictionary_distance, const size_t max_distance, + HasherSearchResult* BROTLI_RESTRICT out) { + uint16_t* BROTLI_RESTRICT num = self->num_; + uint32_t* BROTLI_RESTRICT buckets = self->buckets_; + const size_t cur_ix_masked = cur_ix & ring_buffer_mask; + /* Don't accept a short copy from far away. */ + score_t min_score = out->score; + score_t best_score = out->score; + size_t best_len = out->len; + size_t i; + /* Precalculate the hash key and prefetch the bucket. */ + const uint32_t key = + FN(HashBytes)(&data[cur_ix_masked], self->hash_shift_); + uint32_t* BROTLI_RESTRICT bucket = &buckets[key << self->block_bits_]; + PREFETCH_L1(bucket); + if (self->block_bits_ > 4) PREFETCH_L1(bucket + 16); + out->len = 0; + out->len_code_delta = 0; + + BROTLI_DCHECK(cur_ix_masked + max_length <= ring_buffer_mask); + + /* Try last distance first. */ + for (i = 0; i < (size_t)self->num_last_distances_to_check_; ++i) { + const size_t backward = (size_t)distance_cache[i]; + size_t prev_ix = (size_t)(cur_ix - backward); + if (prev_ix >= cur_ix) { + continue; + } + if (BROTLI_PREDICT_FALSE(backward > max_backward)) { + continue; + } + prev_ix &= ring_buffer_mask; + + if (cur_ix_masked + best_len > ring_buffer_mask) { + break; + } + if (prev_ix + best_len > ring_buffer_mask || + data[cur_ix_masked + best_len] != data[prev_ix + best_len]) { + continue; + } + { + const size_t len = FindMatchLengthWithLimit(&data[prev_ix], + &data[cur_ix_masked], + max_length); + if (len >= 3 || (len == 2 && i < 2)) { + /* Comparing for >= 2 does not change the semantics, but just saves for + a few unnecessary binary logarithms in backward reference score, + since we are not interested in such short matches. */ + score_t score = BackwardReferenceScoreUsingLastDistance(len); + if (best_score < score) { + if (i != 0) score -= BackwardReferencePenaltyUsingLastDistance(i); + if (best_score < score) { + best_score = score; + best_len = len; + out->len = best_len; + out->distance = backward; + out->score = best_score; + } + } + } + } + } + /* we require matches of len >4, so increase best_len to 3, so we can compare + * 4 bytes all the time. */ + if (best_len < 3) { + best_len = 3; + } + { + const size_t down = + (num[key] > self->block_size_) ? (num[key] - self->block_size_) : 0u; + for (i = num[key]; i > down;) { + size_t prev_ix = bucket[--i & self->block_mask_]; + const size_t backward = cur_ix - prev_ix; + if (BROTLI_PREDICT_FALSE(backward > max_backward)) { + break; + } + prev_ix &= ring_buffer_mask; + if (cur_ix_masked + best_len > ring_buffer_mask) { + break; + } + if (prev_ix + best_len > ring_buffer_mask || + /* compare 4 bytes ending at best_len + 1 */ + BrotliUnalignedRead32(&data[cur_ix_masked + best_len - 3]) != + BrotliUnalignedRead32(&data[prev_ix + best_len - 3])) { + continue; + } + { + const size_t len = FindMatchLengthWithLimit(&data[prev_ix], + &data[cur_ix_masked], + max_length); + if (len >= 4) { + /* Comparing for >= 3 does not change the semantics, but just saves + for a few unnecessary binary logarithms in backward reference + score, since we are not interested in such short matches. */ + score_t score = BackwardReferenceScore(len, backward); + if (best_score < score) { + best_score = score; + best_len = len; + out->len = best_len; + out->distance = backward; + out->score = best_score; + } + } + } + } + bucket[num[key] & self->block_mask_] = (uint32_t)cur_ix; + ++num[key]; + } + if (min_score == out->score) { + SearchInStaticDictionary(dictionary, + self->common_, &data[cur_ix_masked], max_length, dictionary_distance, + max_distance, out, BROTLI_FALSE); + } +} + +#undef HashLongestMatch diff --git a/packages/runtime/vendor/brotli/c/enc/hash_longest_match_quickly_inc.h b/packages/runtime/vendor/brotli/c/enc/hash_longest_match_quickly_inc.h new file mode 100644 index 000000000..57ed586fe --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/hash_longest_match_quickly_inc.h @@ -0,0 +1,270 @@ +/* NOLINT(build/header_guard) */ +/* Copyright 2010 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* template parameters: FN, BUCKET_BITS, BUCKET_SWEEP_BITS, HASH_LEN, + USE_DICTIONARY + */ + +#define HashLongestMatchQuickly HASHER() + +#define BUCKET_SIZE (1 << BUCKET_BITS) +#define BUCKET_MASK (BUCKET_SIZE - 1) +#define BUCKET_SWEEP (1 << BUCKET_SWEEP_BITS) +#define BUCKET_SWEEP_MASK ((BUCKET_SWEEP - 1) << 3) + +static BROTLI_INLINE size_t FN(HashTypeLength)(void) { return 8; } +static BROTLI_INLINE size_t FN(StoreLookahead)(void) { return 8; } + +/* HashBytes is the function that chooses the bucket to place + the address in. The HashLongestMatch and HashLongestMatchQuickly + classes have separate, different implementations of hashing. */ +static uint32_t FN(HashBytes)(const uint8_t* data) { + const uint64_t h = ((BROTLI_UNALIGNED_LOAD64LE(data) << (64 - 8 * HASH_LEN)) * + kHashMul64); + /* The higher bits contain more mixture from the multiplication, + so we take our results from there. */ + return (uint32_t)(h >> (64 - BUCKET_BITS)); +} + +/* A (forgetful) hash table to the data seen by the compressor, to + help create backward references to previous data. + + This is a hash map of fixed size (BUCKET_SIZE). */ +typedef struct HashLongestMatchQuickly { + /* Shortcuts. */ + HasherCommon* common; + + /* --- Dynamic size members --- */ + + uint32_t* buckets_; /* uint32_t[BUCKET_SIZE]; */ +} HashLongestMatchQuickly; + +static void FN(Initialize)( + HasherCommon* common, HashLongestMatchQuickly* BROTLI_RESTRICT self, + const BrotliEncoderParams* params) { + self->common = common; + + BROTLI_UNUSED(params); + self->buckets_ = (uint32_t*)common->extra[0]; +} + +static void FN(Prepare)( + HashLongestMatchQuickly* BROTLI_RESTRICT self, BROTLI_BOOL one_shot, + size_t input_size, const uint8_t* BROTLI_RESTRICT data) { + uint32_t* BROTLI_RESTRICT buckets = self->buckets_; + /* Partial preparation is 100 times slower (per socket). */ + size_t partial_prepare_threshold = BUCKET_SIZE >> 5; + if (one_shot && input_size <= partial_prepare_threshold) { + size_t i; + for (i = 0; i < input_size; ++i) { + const uint32_t key = FN(HashBytes)(&data[i]); + if (BUCKET_SWEEP == 1) { + buckets[key] = 0; + } else { + uint32_t j; + for (j = 0; j < BUCKET_SWEEP; ++j) { + buckets[(key + (j << 3)) & BUCKET_MASK] = 0; + } + } + } + } else { + /* It is not strictly necessary to fill this buffer here, but + not filling will make the results of the compression stochastic + (but correct). This is because random data would cause the + system to find accidentally good backward references here and there. */ + memset(buckets, 0, sizeof(uint32_t) * BUCKET_SIZE); + } +} + +static BROTLI_INLINE void FN(HashMemAllocInBytes)( + const BrotliEncoderParams* params, BROTLI_BOOL one_shot, + size_t input_size, size_t* alloc_size) { + BROTLI_UNUSED(params); + BROTLI_UNUSED(one_shot); + BROTLI_UNUSED(input_size); + alloc_size[0] = sizeof(uint32_t) * BUCKET_SIZE; +} + +/* Look at 5 bytes at &data[ix & mask]. + Compute a hash from these, and store the value somewhere within + [ix .. ix+3]. */ +static BROTLI_INLINE void FN(Store)( + HashLongestMatchQuickly* BROTLI_RESTRICT self, + const uint8_t* BROTLI_RESTRICT data, const size_t mask, const size_t ix) { + const uint32_t key = FN(HashBytes)(&data[ix & mask]); + if (BUCKET_SWEEP == 1) { + self->buckets_[key] = (uint32_t)ix; + } else { + /* Wiggle the value with the bucket sweep range. */ + const uint32_t off = ix & BUCKET_SWEEP_MASK; + self->buckets_[(key + off) & BUCKET_MASK] = (uint32_t)ix; + } +} + +static BROTLI_INLINE void FN(StoreRange)( + HashLongestMatchQuickly* BROTLI_RESTRICT self, + const uint8_t* BROTLI_RESTRICT data, const size_t mask, + const size_t ix_start, const size_t ix_end) { + size_t i; + for (i = ix_start; i < ix_end; ++i) { + FN(Store)(self, data, mask, i); + } +} + +static BROTLI_INLINE void FN(StitchToPreviousBlock)( + HashLongestMatchQuickly* BROTLI_RESTRICT self, + size_t num_bytes, size_t position, + const uint8_t* ringbuffer, size_t ringbuffer_mask) { + if (num_bytes >= FN(HashTypeLength)() - 1 && position >= 3) { + /* Prepare the hashes for three last bytes of the last write. + These could not be calculated before, since they require knowledge + of both the previous and the current block. */ + FN(Store)(self, ringbuffer, ringbuffer_mask, position - 3); + FN(Store)(self, ringbuffer, ringbuffer_mask, position - 2); + FN(Store)(self, ringbuffer, ringbuffer_mask, position - 1); + } +} + +static BROTLI_INLINE void FN(PrepareDistanceCache)( + HashLongestMatchQuickly* BROTLI_RESTRICT self, + int* BROTLI_RESTRICT distance_cache) { + BROTLI_UNUSED(self); + BROTLI_UNUSED(distance_cache); +} + +/* Find a longest backward match of &data[cur_ix & ring_buffer_mask] + up to the length of max_length and stores the position cur_ix in the + hash table. + + Does not look for matches longer than max_length. + Does not look for matches further away than max_backward. + Writes the best match into |out|. + |out|->score is updated only if a better match is found. */ +static BROTLI_INLINE void FN(FindLongestMatch)( + HashLongestMatchQuickly* BROTLI_RESTRICT self, + const BrotliEncoderDictionary* dictionary, + const uint8_t* BROTLI_RESTRICT data, + const size_t ring_buffer_mask, const int* BROTLI_RESTRICT distance_cache, + const size_t cur_ix, const size_t max_length, const size_t max_backward, + const size_t dictionary_distance, const size_t max_distance, + HasherSearchResult* BROTLI_RESTRICT out) { + uint32_t* BROTLI_RESTRICT buckets = self->buckets_; + const size_t best_len_in = out->len; + const size_t cur_ix_masked = cur_ix & ring_buffer_mask; + /* TODO: compare 4 bytes at once (and set the minimum best len to 4) */ + int compare_char = data[cur_ix_masked + best_len_in]; + size_t key = FN(HashBytes)(&data[cur_ix_masked]); + size_t key_out; + score_t min_score = out->score; + score_t best_score = out->score; + size_t best_len = best_len_in; + size_t cached_backward = (size_t)distance_cache[0]; + size_t prev_ix = cur_ix - cached_backward; + + BROTLI_DCHECK(cur_ix_masked + max_length <= ring_buffer_mask); + + out->len_code_delta = 0; + if (prev_ix < cur_ix) { + prev_ix &= (uint32_t)ring_buffer_mask; + if (compare_char == data[prev_ix + best_len]) { + const size_t len = FindMatchLengthWithLimit( + &data[prev_ix], &data[cur_ix_masked], max_length); + if (len >= 4) { + const score_t score = BackwardReferenceScoreUsingLastDistance(len); + if (best_score < score) { + out->len = len; + out->distance = cached_backward; + out->score = score; + if (BUCKET_SWEEP == 1) { + buckets[key] = (uint32_t)cur_ix; + return; + } else { + best_len = len; + best_score = score; + compare_char = data[cur_ix_masked + len]; + } + } + } + } + } + if (BUCKET_SWEEP == 1) { + size_t backward; + size_t len; + /* Only one to look for, don't bother to prepare for a loop. */ + prev_ix = buckets[key]; + buckets[key] = (uint32_t)cur_ix; + backward = cur_ix - prev_ix; + prev_ix &= (uint32_t)ring_buffer_mask; + if (compare_char != data[prev_ix + best_len_in]) { + return; + } + if (BROTLI_PREDICT_FALSE(backward == 0 || backward > max_backward)) { + return; + } + len = FindMatchLengthWithLimit(&data[prev_ix], + &data[cur_ix_masked], + max_length); + if (len >= 4) { + const score_t score = BackwardReferenceScore(len, backward); + if (best_score < score) { + out->len = len; + out->distance = backward; + out->score = score; + return; + } + } + } else { + size_t keys[BUCKET_SWEEP]; + size_t i; + for (i = 0; i < BUCKET_SWEEP; ++i) { + keys[i] = (key + (i << 3)) & BUCKET_MASK; + } + key_out = keys[(cur_ix & BUCKET_SWEEP_MASK) >> 3]; + for (i = 0; i < BUCKET_SWEEP; ++i) { + size_t len; + size_t backward; + prev_ix = buckets[keys[i]]; + backward = cur_ix - prev_ix; + prev_ix &= (uint32_t)ring_buffer_mask; + if (compare_char != data[prev_ix + best_len]) { + continue; + } + if (BROTLI_PREDICT_FALSE(backward == 0 || backward > max_backward)) { + continue; + } + len = FindMatchLengthWithLimit(&data[prev_ix], + &data[cur_ix_masked], + max_length); + if (len >= 4) { + const score_t score = BackwardReferenceScore(len, backward); + if (best_score < score) { + best_len = len; + out->len = len; + compare_char = data[cur_ix_masked + len]; + best_score = score; + out->score = score; + out->distance = backward; + } + } + } + } + if (USE_DICTIONARY && min_score == out->score) { + SearchInStaticDictionary(dictionary, + self->common, &data[cur_ix_masked], max_length, dictionary_distance, + max_distance, out, BROTLI_TRUE); + } + if (BUCKET_SWEEP != 1) { + buckets[key_out] = (uint32_t)cur_ix; + } +} + +#undef BUCKET_SWEEP_MASK +#undef BUCKET_SWEEP +#undef BUCKET_MASK +#undef BUCKET_SIZE + +#undef HashLongestMatchQuickly diff --git a/packages/runtime/vendor/brotli/c/enc/hash_longest_match_simd_inc.h b/packages/runtime/vendor/brotli/c/enc/hash_longest_match_simd_inc.h new file mode 100644 index 000000000..79b341ae5 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/hash_longest_match_simd_inc.h @@ -0,0 +1,278 @@ +/* NOLINT(build/header_guard) */ +/* Copyright 2010 Google Inc. All Rights Reserved. + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ +/* template parameters: FN */ +/* A (forgetful) hash table to the data seen by the compressor, to + help create backward references to previous data. + This is a hash map of fixed size (bucket_size_) to a ring buffer of + fixed size (block_size_). The ring buffer contains the last block_size_ + index positions of the given hash key in the compressed data. */ +#define HashLongestMatch HASHER() +#define TAG_HASH_BITS 8 +#define TAG_HASH_MASK ((1 << TAG_HASH_BITS) - 1) +static BROTLI_INLINE size_t FN(HashTypeLength)(void) { return 4; } +static BROTLI_INLINE size_t FN(StoreLookahead)(void) { return 4; } +/* HashBytes is the function that chooses the bucket to place the address in. */ +static uint32_t FN(HashBytes)( + const uint8_t* BROTLI_RESTRICT data, const int shift) { + uint32_t h = BROTLI_UNALIGNED_LOAD32LE(data) * kHashMul32; + /* The higher bits contain more mixture from the multiplication, + so we take our results from there. */ + return (uint32_t)(h >> shift); +} +typedef struct HashLongestMatch { + /* Number of hash buckets. */ + size_t bucket_size_; + /* Only block_size_ newest backward references are kept, + and the older are forgotten. */ + size_t block_size_; + /* Left-shift for computing hash bucket index from hash value. */ + int hash_shift_; + /* Mask for accessing entries in a block (in a ring-buffer manner). */ + uint32_t block_mask_; + int block_bits_; + int num_last_distances_to_check_; + /* Shortcuts. */ + HasherCommon* common_; + /* --- Dynamic size members --- */ + /* Number of entries in a particular bucket. */ + uint16_t* num_; /* uint16_t[bucket_size]; */ + uint8_t* tags_; + /* Buckets containing block_size_ of backward references. */ + uint32_t* buckets_; /* uint32_t[bucket_size * block_size]; */ +} HashLongestMatch; +static void FN(Initialize)( + HasherCommon* common, HashLongestMatch* BROTLI_RESTRICT self, + const BrotliEncoderParams* params) { + self->common_ = common; + BROTLI_UNUSED(params); + self->hash_shift_ = 32 - common->params.bucket_bits - TAG_HASH_BITS; + self->bucket_size_ = (size_t)1 << common->params.bucket_bits; + self->block_size_ = (size_t)1 << common->params.block_bits; + self->block_mask_ = (uint32_t)(self->block_size_ - 1); + self->num_ = (uint16_t*)common->extra[0]; + self->tags_ = (uint8_t*)common->extra[1]; + self->buckets_ = (uint32_t*)common->extra[2]; + self->block_bits_ = common->params.block_bits; + self->num_last_distances_to_check_ = + common->params.num_last_distances_to_check; +} +static void FN(Prepare)( + HashLongestMatch* BROTLI_RESTRICT self, BROTLI_BOOL one_shot, + size_t input_size, const uint8_t* BROTLI_RESTRICT data) { + uint16_t* BROTLI_RESTRICT num = self->num_; + /* Partial preparation is 100 times slower (per socket). */ + size_t partial_prepare_threshold = self->bucket_size_ >> 6; + if (one_shot && input_size <= partial_prepare_threshold) { + size_t i; + for (i = 0; i < input_size; ++i) { + const uint32_t hash = FN(HashBytes)(&data[i], self->hash_shift_); + const uint32_t key = hash >> TAG_HASH_BITS; + num[key] = 65535; + } + } else { + /* Set all the bytes of num to 255, which makes each uint16_t 65535. */ + memset(num, 255, self->bucket_size_ * sizeof(num[0])); + } +} +static BROTLI_INLINE void FN(HashMemAllocInBytes)( + const BrotliEncoderParams* params, BROTLI_BOOL one_shot, + size_t input_size, size_t* alloc_size) { + size_t bucket_size = (size_t)1 << params->hasher.bucket_bits; + size_t block_size = (size_t)1 << params->hasher.block_bits; + BROTLI_UNUSED(one_shot); + BROTLI_UNUSED(input_size); + alloc_size[0] = sizeof(uint16_t) * bucket_size; + alloc_size[1] = sizeof(uint8_t) * bucket_size * block_size; + alloc_size[2] = sizeof(uint32_t) * bucket_size * block_size; +} +/* Look at 4 bytes at &data[ix & mask]. + Compute a hash from these, and store the value of ix at that position. */ +static BROTLI_INLINE void FN(Store)( + HashLongestMatch* BROTLI_RESTRICT self, const uint8_t* BROTLI_RESTRICT data, + const size_t mask, const size_t ix) { + uint16_t* BROTLI_RESTRICT num = self->num_; + uint8_t* BROTLI_RESTRICT tags = self->tags_; + uint32_t* BROTLI_RESTRICT buckets = self->buckets_; + const size_t hash = FN(HashBytes)(&data[ix & mask], self->hash_shift_); + const size_t key = hash >> TAG_HASH_BITS; + const uint8_t tag = hash & TAG_HASH_MASK; + const size_t minor_ix = num[key] & self->block_mask_; + const size_t offset = minor_ix + (key << self->block_bits_); + --num[key]; + buckets[offset] = (uint32_t)ix; + tags[offset] = tag; +} +static BROTLI_INLINE void FN(StoreRange)(HashLongestMatch* BROTLI_RESTRICT self, + const uint8_t* BROTLI_RESTRICT data, const size_t mask, + const size_t ix_start, const size_t ix_end) { + size_t i; + for (i = ix_start; i < ix_end; ++i) { + FN(Store)(self, data, mask, i); + } +} +static BROTLI_INLINE void FN(StitchToPreviousBlock)( + HashLongestMatch* BROTLI_RESTRICT self, + size_t num_bytes, size_t position, const uint8_t* ringbuffer, + size_t ringbuffer_mask) { + if (num_bytes >= FN(HashTypeLength)() - 1 && position >= 3) { + /* Prepare the hashes for three last bytes of the last write. + These could not be calculated before, since they require knowledge + of both the previous and the current block. */ + FN(Store)(self, ringbuffer, ringbuffer_mask, position - 3); + FN(Store)(self, ringbuffer, ringbuffer_mask, position - 2); + FN(Store)(self, ringbuffer, ringbuffer_mask, position - 1); + } +} +static BROTLI_INLINE void FN(PrepareDistanceCache)( + HashLongestMatch* BROTLI_RESTRICT self, + int* BROTLI_RESTRICT distance_cache) { + PrepareDistanceCache(distance_cache, self->num_last_distances_to_check_); +} + +/* Find a longest backward match of &data[cur_ix] up to the length of + max_length and stores the position cur_ix in the hash table. + REQUIRES: FN(PrepareDistanceCache) must be invoked for current distance cache + values; if this method is invoked repeatedly with the same distance + cache values, it is enough to invoke FN(PrepareDistanceCache) once. + Does not look for matches longer than max_length. + Does not look for matches further away than max_backward. + Writes the best match into |out|. + |out|->score is updated only if a better match is found. */ +static BROTLI_INLINE void FN(FindLongestMatch)( + HashLongestMatch* BROTLI_RESTRICT self, + const BrotliEncoderDictionary* dictionary, + const uint8_t* BROTLI_RESTRICT data, const size_t ring_buffer_mask, + const int* BROTLI_RESTRICT distance_cache, const size_t cur_ix, + const size_t max_length, const size_t max_backward, + const size_t dictionary_distance, const size_t max_distance, + HasherSearchResult* BROTLI_RESTRICT out) { + uint16_t* BROTLI_RESTRICT num = self->num_; + uint32_t* BROTLI_RESTRICT buckets = self->buckets_; + uint8_t* BROTLI_RESTRICT tags = self->tags_; + const size_t cur_ix_masked = cur_ix & ring_buffer_mask; + /* Don't accept a short copy from far away. */ + score_t min_score = out->score; + score_t best_score = out->score; + size_t best_len = out->len; + size_t i; + /* Precalculate the hash key and prefetch the bucket. */ + const uint32_t hash = + FN(HashBytes)(&data[cur_ix_masked], self->hash_shift_); + const uint32_t key = hash >> TAG_HASH_BITS; + uint32_t* BROTLI_RESTRICT bucket = &buckets[key << self->block_bits_]; + uint8_t* BROTLI_RESTRICT tag_bucket = &tags[key << self->block_bits_]; + PREFETCH_L1(bucket); + PREFETCH_L1(tag_bucket); + if (self->block_bits_ > 4) PREFETCH_L1(bucket + 16); + out->len = 0; + out->len_code_delta = 0; + + BROTLI_DCHECK(cur_ix_masked + max_length <= ring_buffer_mask); + + /* Try last distance first. */ + for (i = 0; i < (size_t)self->num_last_distances_to_check_; ++i) { + const size_t backward = (size_t)distance_cache[i]; + size_t prev_ix = (size_t)(cur_ix - backward); + if (prev_ix >= cur_ix) { + continue; + } + if (BROTLI_PREDICT_FALSE(backward > max_backward)) { + continue; + } + prev_ix &= ring_buffer_mask; + + if (cur_ix_masked + best_len > ring_buffer_mask) { + break; + } + if (prev_ix + best_len > ring_buffer_mask || + data[cur_ix_masked + best_len] != data[prev_ix + best_len]) { + continue; + } + { + const size_t len = FindMatchLengthWithLimit(&data[prev_ix], + &data[cur_ix_masked], + max_length); + if (len >= 3 || (len == 2 && i < 2)) { + /* Comparing for >= 2 does not change the semantics, but just saves for + a few unnecessary binary logarithms in backward reference score, + since we are not interested in such short matches. */ + score_t score = BackwardReferenceScoreUsingLastDistance(len); + if (best_score < score) { + if (i != 0) score -= BackwardReferencePenaltyUsingLastDistance(i); + if (best_score < score) { + best_score = score; + best_len = len; + out->len = best_len; + out->distance = backward; + out->score = best_score; + } + } + } + } + } + /* we require matches of len >4, so increase best_len to 3, so we can compare + * 4 bytes all the time. */ + if (best_len < 3) { + best_len = 3; + } + { + const uint8_t tag = hash & TAG_HASH_MASK; + const size_t head = (num[key] + 1) & self->block_mask_; + uint64_t matches = + GetMatchingTagMask(self->block_size_ / 16, tag, tag_bucket, head); + /* Mask off any matches from uninitialized tags. */ + uint16_t n = 65535 - num[key]; + uint64_t block_has_unused_slots = self->block_size_ > n; + uint64_t mask = (block_has_unused_slots << (n & (64 - 1))) - 1; + matches &= mask; + for (; matches > 0; matches &= (matches - 1)) { + const size_t rb_index = + (head + (size_t)BROTLI_TZCNT64(matches)) & self->block_mask_; + size_t prev_ix = bucket[rb_index]; + const size_t backward = cur_ix - prev_ix; + if (BROTLI_PREDICT_FALSE(backward > max_backward)) { + break; + } + prev_ix &= ring_buffer_mask; + if (cur_ix_masked + best_len > ring_buffer_mask) { + break; + } + if (prev_ix + best_len > ring_buffer_mask || + /* compare 4 bytes ending at best_len + 1 */ + BrotliUnalignedRead32(&data[cur_ix_masked + best_len - 3]) != + BrotliUnalignedRead32(&data[prev_ix + best_len - 3])) { + continue; + } + { + const size_t len = FindMatchLengthWithLimit(&data[prev_ix], + &data[cur_ix_masked], + max_length); + if (len >= 4) { + /* Comparing for >= 3 does not change the semantics, but just saves + for a few unnecessary binary logarithms in backward reference + score, since we are not interested in such short matches. */ + score_t score = BackwardReferenceScore(len, backward); + if (best_score < score) { + best_score = score; + best_len = len; + out->len = best_len; + out->distance = backward; + out->score = best_score; + } + } + } + } + bucket[num[key] & self->block_mask_] = (uint32_t)cur_ix; + tag_bucket[num[key] & self->block_mask_] = tag; + --num[key]; + } + if (min_score == out->score) { + SearchInStaticDictionary(dictionary, + self->common_, &data[cur_ix_masked], max_length, dictionary_distance, + max_distance, out, BROTLI_FALSE); + } +} +#undef HashLongestMatch diff --git a/packages/runtime/vendor/brotli/c/enc/hash_rolling_inc.h b/packages/runtime/vendor/brotli/c/enc/hash_rolling_inc.h new file mode 100644 index 000000000..4c7a6b199 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/hash_rolling_inc.h @@ -0,0 +1,212 @@ +/* NOLINT(build/header_guard) */ +/* Copyright 2018 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* template parameters: FN, JUMP, NUMBUCKETS, MASK, CHUNKLEN */ +/* NUMBUCKETS / (MASK + 1) = probability of storing and using hash code. */ +/* JUMP = skip bytes for speedup */ + +/* Rolling hash for long distance long string matches. Stores one position + per bucket, bucket key is computed over a long region. */ + +#define HashRolling HASHER() + +static const uint32_t FN(kRollingHashMul32) = 69069; +static const uint32_t FN(kInvalidPos) = 0xffffffff; + +/* This hasher uses a longer forward length, but returning a higher value here + will hurt compression by the main hasher when combined with a composite + hasher. The hasher tests for forward itself instead. */ +static BROTLI_INLINE size_t FN(HashTypeLength)(void) { return 4; } +static BROTLI_INLINE size_t FN(StoreLookahead)(void) { return 4; } + +/* Computes a code from a single byte. A lookup table of 256 values could be + used, but simply adding 1 works about as good. */ +static uint32_t FN(HashByte)(uint8_t byte) { + return (uint32_t)byte + 1u; +} + +static uint32_t FN(HashRollingFunctionInitial)(uint32_t state, uint8_t add, + uint32_t factor) { + return (uint32_t)(factor * state + FN(HashByte)(add)); +} + +static uint32_t FN(HashRollingFunction)(uint32_t state, uint8_t add, + uint8_t rem, uint32_t factor, + uint32_t factor_remove) { + return (uint32_t)(factor * state + + FN(HashByte)(add) - factor_remove * FN(HashByte)(rem)); +} + +typedef struct HashRolling { + uint32_t state; + uint32_t* table; + size_t next_ix; + + uint32_t chunk_len; + uint32_t factor; + uint32_t factor_remove; +} HashRolling; + +static void FN(Initialize)( + HasherCommon* common, HashRolling* BROTLI_RESTRICT self, + const BrotliEncoderParams* params) { + size_t i; + self->state = 0; + self->next_ix = 0; + + self->factor = FN(kRollingHashMul32); + + /* Compute the factor of the oldest byte to remove: factor**steps modulo + 0xffffffff (the multiplications rely on 32-bit overflow) */ + self->factor_remove = 1; + for (i = 0; i < CHUNKLEN; i += JUMP) { + self->factor_remove *= self->factor; + } + + self->table = (uint32_t*)common->extra[0]; + for (i = 0; i < NUMBUCKETS; i++) { + self->table[i] = FN(kInvalidPos); + } + + BROTLI_UNUSED(params); +} + +static void FN(Prepare)(HashRolling* BROTLI_RESTRICT self, BROTLI_BOOL one_shot, + size_t input_size, const uint8_t* BROTLI_RESTRICT data) { + size_t i; + /* Too small size, cannot use this hasher. */ + if (input_size < CHUNKLEN) return; + self->state = 0; + for (i = 0; i < CHUNKLEN; i += JUMP) { + self->state = FN(HashRollingFunctionInitial)( + self->state, data[i], self->factor); + } + BROTLI_UNUSED(one_shot); +} + +static BROTLI_INLINE void FN(HashMemAllocInBytes)( + const BrotliEncoderParams* params, BROTLI_BOOL one_shot, + size_t input_size, size_t* alloc_size) { + BROTLI_UNUSED(params); + BROTLI_UNUSED(one_shot); + BROTLI_UNUSED(input_size); + alloc_size[0] = NUMBUCKETS * sizeof(uint32_t); +} + +static BROTLI_INLINE void FN(Store)(HashRolling* BROTLI_RESTRICT self, + const uint8_t* BROTLI_RESTRICT data, const size_t mask, const size_t ix) { + BROTLI_UNUSED(self); + BROTLI_UNUSED(data); + BROTLI_UNUSED(mask); + BROTLI_UNUSED(ix); +} + +static BROTLI_INLINE void FN(StoreRange)(HashRolling* BROTLI_RESTRICT self, + const uint8_t* BROTLI_RESTRICT data, const size_t mask, + const size_t ix_start, const size_t ix_end) { + BROTLI_UNUSED(self); + BROTLI_UNUSED(data); + BROTLI_UNUSED(mask); + BROTLI_UNUSED(ix_start); + BROTLI_UNUSED(ix_end); +} + +static BROTLI_INLINE void FN(StitchToPreviousBlock)( + HashRolling* BROTLI_RESTRICT self, + size_t num_bytes, size_t position, const uint8_t* ringbuffer, + size_t ring_buffer_mask) { + /* In this case we must re-initialize the hasher from scratch from the + current position. */ + size_t position_masked; + size_t available = num_bytes; + if ((position & (JUMP - 1)) != 0) { + size_t diff = JUMP - (position & (JUMP - 1)); + available = (diff > available) ? 0 : (available - diff); + position += diff; + } + position_masked = position & ring_buffer_mask; + /* wrapping around ringbuffer not handled. */ + if (available > ring_buffer_mask - position_masked) { + available = ring_buffer_mask - position_masked; + } + + FN(Prepare)(self, BROTLI_FALSE, available, + ringbuffer + (position & ring_buffer_mask)); + self->next_ix = position; + BROTLI_UNUSED(num_bytes); +} + +static BROTLI_INLINE void FN(PrepareDistanceCache)( + HashRolling* BROTLI_RESTRICT self, + int* BROTLI_RESTRICT distance_cache) { + BROTLI_UNUSED(self); + BROTLI_UNUSED(distance_cache); +} + +static BROTLI_INLINE void FN(FindLongestMatch)( + HashRolling* BROTLI_RESTRICT self, + const BrotliEncoderDictionary* dictionary, + const uint8_t* BROTLI_RESTRICT data, const size_t ring_buffer_mask, + const int* BROTLI_RESTRICT distance_cache, const size_t cur_ix, + const size_t max_length, const size_t max_backward, + const size_t dictionary_distance, const size_t max_distance, + HasherSearchResult* BROTLI_RESTRICT out) { + const size_t cur_ix_masked = cur_ix & ring_buffer_mask; + size_t pos; + + if ((cur_ix & (JUMP - 1)) != 0) return; + + /* Not enough lookahead */ + if (max_length < CHUNKLEN) return; + + for (pos = self->next_ix; pos <= cur_ix; pos += JUMP) { + uint32_t code = self->state & MASK; + + uint8_t rem = data[pos & ring_buffer_mask]; + uint8_t add = data[(pos + CHUNKLEN) & ring_buffer_mask]; + size_t found_ix = FN(kInvalidPos); + + self->state = FN(HashRollingFunction)( + self->state, add, rem, self->factor, self->factor_remove); + + if (code < NUMBUCKETS) { + found_ix = self->table[code]; + self->table[code] = (uint32_t)pos; + if (pos == cur_ix && found_ix != FN(kInvalidPos)) { + /* The cast to 32-bit makes backward distances up to 4GB work even + if cur_ix is above 4GB, despite using 32-bit values in the table. */ + size_t backward = (uint32_t)(cur_ix - found_ix); + if (backward <= max_backward) { + const size_t found_ix_masked = found_ix & ring_buffer_mask; + const size_t len = FindMatchLengthWithLimit(&data[found_ix_masked], + &data[cur_ix_masked], + max_length); + if (len >= 4 && len > out->len) { + score_t score = BackwardReferenceScore(len, backward); + if (score > out->score) { + out->len = len; + out->distance = backward; + out->score = score; + out->len_code_delta = 0; + } + } + } + } + } + } + + self->next_ix = cur_ix + JUMP; + + /* NOTE: this hasher does not search in the dictionary. It is used as + backup-hasher, the main hasher already searches in it. */ + BROTLI_UNUSED(dictionary); + BROTLI_UNUSED(distance_cache); + BROTLI_UNUSED(dictionary_distance); + BROTLI_UNUSED(max_distance); +} + +#undef HashRolling diff --git a/packages/runtime/vendor/brotli/c/enc/hash_to_binary_tree_inc.h b/packages/runtime/vendor/brotli/c/enc/hash_to_binary_tree_inc.h new file mode 100644 index 000000000..a639d2d4b --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/hash_to_binary_tree_inc.h @@ -0,0 +1,330 @@ +/* NOLINT(build/header_guard) */ +/* Copyright 2016 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* template parameters: FN, BUCKET_BITS, MAX_TREE_COMP_LENGTH, + MAX_TREE_SEARCH_DEPTH */ + +/* A (forgetful) hash table where each hash bucket contains a binary tree of + sequences whose first 4 bytes share the same hash code. + Each sequence is MAX_TREE_COMP_LENGTH long and is identified by its starting + position in the input data. The binary tree is sorted by the lexicographic + order of the sequences, and it is also a max-heap with respect to the + starting positions. */ + +#define HashToBinaryTree HASHER() + +#define BUCKET_SIZE (1 << BUCKET_BITS) + +static BROTLI_INLINE size_t FN(HashTypeLength)(void) { return 4; } +static BROTLI_INLINE size_t FN(StoreLookahead)(void) { + return MAX_TREE_COMP_LENGTH; +} + +static uint32_t FN(HashBytes)(const uint8_t* BROTLI_RESTRICT data) { + uint32_t h = BROTLI_UNALIGNED_LOAD32LE(data) * kHashMul32; + /* The higher bits contain more mixture from the multiplication, + so we take our results from there. */ + return h >> (32 - BUCKET_BITS); +} + +typedef struct HashToBinaryTree { + /* The window size minus 1 */ + size_t window_mask_; + + /* Hash table that maps the 4-byte hashes of the sequence to the last + position where this hash was found, which is the root of the binary + tree of sequences that share this hash bucket. */ + uint32_t* buckets_; /* uint32_t[BUCKET_SIZE]; */ + + /* A position used to mark a non-existent sequence, i.e. a tree is empty if + its root is at invalid_pos_ and a node is a leaf if both its children + are at invalid_pos_. */ + uint32_t invalid_pos_; + + /* --- Dynamic size members --- */ + + /* The union of the binary trees of each hash bucket. The root of the tree + corresponding to a hash is a sequence starting at buckets_[hash] and + the left and right children of a sequence starting at pos are + forest_[2 * pos] and forest_[2 * pos + 1]. */ + uint32_t* forest_; /* uint32_t[2 * num_nodes] */ +} HashToBinaryTree; + +static void FN(Initialize)( + HasherCommon* common, HashToBinaryTree* BROTLI_RESTRICT self, + const BrotliEncoderParams* params) { + self->buckets_ = (uint32_t*)common->extra[0]; + self->forest_ = (uint32_t*)common->extra[1]; + + self->window_mask_ = (1u << params->lgwin) - 1u; + self->invalid_pos_ = (uint32_t)(0 - self->window_mask_); +} + +static void FN(Prepare) + (HashToBinaryTree* BROTLI_RESTRICT self, BROTLI_BOOL one_shot, + size_t input_size, const uint8_t* BROTLI_RESTRICT data) { + uint32_t invalid_pos = self->invalid_pos_; + uint32_t i; + uint32_t* BROTLI_RESTRICT buckets = self->buckets_; + BROTLI_UNUSED(data); + BROTLI_UNUSED(one_shot); + BROTLI_UNUSED(input_size); + for (i = 0; i < BUCKET_SIZE; i++) { + buckets[i] = invalid_pos; + } +} + +static BROTLI_INLINE void FN(HashMemAllocInBytes)( + const BrotliEncoderParams* params, BROTLI_BOOL one_shot, + size_t input_size, size_t* alloc_size) { + size_t num_nodes = (size_t)1 << params->lgwin; + if (one_shot && input_size < num_nodes) { + num_nodes = input_size; + } + alloc_size[0] = sizeof(uint32_t) * BUCKET_SIZE; + alloc_size[1] = 2 * sizeof(uint32_t) * num_nodes; +} + +static BROTLI_INLINE size_t FN(LeftChildIndex)( + HashToBinaryTree* BROTLI_RESTRICT self, + const size_t pos) { + return 2 * (pos & self->window_mask_); +} + +static BROTLI_INLINE size_t FN(RightChildIndex)( + HashToBinaryTree* BROTLI_RESTRICT self, + const size_t pos) { + return 2 * (pos & self->window_mask_) + 1; +} + +/* Stores the hash of the next 4 bytes and in a single tree-traversal, the + hash bucket's binary tree is searched for matches and is re-rooted at the + current position. + + If less than MAX_TREE_COMP_LENGTH data is available, the hash bucket of the + current position is searched for matches, but the state of the hash table + is not changed, since we can not know the final sorting order of the + current (incomplete) sequence. + + This function must be called with increasing cur_ix positions. */ +static BROTLI_INLINE BackwardMatch* FN(StoreAndFindMatches)( + HashToBinaryTree* BROTLI_RESTRICT self, const uint8_t* BROTLI_RESTRICT data, + const size_t cur_ix, const size_t ring_buffer_mask, const size_t max_length, + const size_t max_backward, size_t* const BROTLI_RESTRICT best_len, + BackwardMatch* BROTLI_RESTRICT matches) { + const size_t cur_ix_masked = cur_ix & ring_buffer_mask; + const size_t max_comp_len = + BROTLI_MIN(size_t, max_length, MAX_TREE_COMP_LENGTH); + const BROTLI_BOOL should_reroot_tree = + TO_BROTLI_BOOL(max_length >= MAX_TREE_COMP_LENGTH); + const uint32_t key = FN(HashBytes)(&data[cur_ix_masked]); + uint32_t* BROTLI_RESTRICT buckets = self->buckets_; + uint32_t* BROTLI_RESTRICT forest = self->forest_; + size_t prev_ix = buckets[key]; + /* The forest index of the rightmost node of the left subtree of the new + root, updated as we traverse and re-root the tree of the hash bucket. */ + size_t node_left = FN(LeftChildIndex)(self, cur_ix); + /* The forest index of the leftmost node of the right subtree of the new + root, updated as we traverse and re-root the tree of the hash bucket. */ + size_t node_right = FN(RightChildIndex)(self, cur_ix); + /* The match length of the rightmost node of the left subtree of the new + root, updated as we traverse and re-root the tree of the hash bucket. */ + size_t best_len_left = 0; + /* The match length of the leftmost node of the right subtree of the new + root, updated as we traverse and re-root the tree of the hash bucket. */ + size_t best_len_right = 0; + size_t depth_remaining; + if (should_reroot_tree) { + buckets[key] = (uint32_t)cur_ix; + } + for (depth_remaining = MAX_TREE_SEARCH_DEPTH; ; --depth_remaining) { + const size_t backward = cur_ix - prev_ix; + const size_t prev_ix_masked = prev_ix & ring_buffer_mask; + if (backward == 0 || backward > max_backward || depth_remaining == 0) { + if (should_reroot_tree) { + forest[node_left] = self->invalid_pos_; + forest[node_right] = self->invalid_pos_; + } + break; + } + { + const size_t cur_len = BROTLI_MIN(size_t, best_len_left, best_len_right); + size_t len; + BROTLI_DCHECK(cur_len <= MAX_TREE_COMP_LENGTH); + len = cur_len + + FindMatchLengthWithLimit(&data[cur_ix_masked + cur_len], + &data[prev_ix_masked + cur_len], + max_length - cur_len); + BROTLI_DCHECK( + 0 == memcmp(&data[cur_ix_masked], &data[prev_ix_masked], len)); + if (matches && len > *best_len) { + *best_len = len; + InitBackwardMatch(matches++, backward, len); + } + if (len >= max_comp_len) { + if (should_reroot_tree) { + forest[node_left] = forest[FN(LeftChildIndex)(self, prev_ix)]; + forest[node_right] = forest[FN(RightChildIndex)(self, prev_ix)]; + } + break; + } + if (data[cur_ix_masked + len] > data[prev_ix_masked + len]) { + best_len_left = len; + if (should_reroot_tree) { + forest[node_left] = (uint32_t)prev_ix; + } + node_left = FN(RightChildIndex)(self, prev_ix); + prev_ix = forest[node_left]; + } else { + best_len_right = len; + if (should_reroot_tree) { + forest[node_right] = (uint32_t)prev_ix; + } + node_right = FN(LeftChildIndex)(self, prev_ix); + prev_ix = forest[node_right]; + } + } + } + return matches; +} + +/* Finds all backward matches of &data[cur_ix & ring_buffer_mask] up to the + length of max_length and stores the position cur_ix in the hash table. + + Sets *num_matches to the number of matches found, and stores the found + matches in matches[0] to matches[*num_matches - 1]. The matches will be + sorted by strictly increasing length and (non-strictly) increasing + distance. */ +static BROTLI_INLINE size_t FN(FindAllMatches)( + HashToBinaryTree* BROTLI_RESTRICT self, + const BrotliEncoderDictionary* dictionary, + const uint8_t* BROTLI_RESTRICT data, + const size_t ring_buffer_mask, const size_t cur_ix, + const size_t max_length, const size_t max_backward, + const size_t dictionary_distance, const BrotliEncoderParams* params, + BackwardMatch* matches) { + BackwardMatch* const orig_matches = matches; + const size_t cur_ix_masked = cur_ix & ring_buffer_mask; + size_t best_len = 1; + const size_t short_match_max_backward = + params->quality != HQ_ZOPFLIFICATION_QUALITY ? 16 : 64; + size_t stop = cur_ix - short_match_max_backward; + uint32_t dict_matches[BROTLI_MAX_STATIC_DICTIONARY_MATCH_LEN + 1]; + size_t i; + if (cur_ix < short_match_max_backward) { stop = 0; } + for (i = cur_ix - 1; i > stop && best_len <= 2; --i) { + size_t prev_ix = i; + const size_t backward = cur_ix - prev_ix; + if (BROTLI_PREDICT_FALSE(backward > max_backward)) { + break; + } + prev_ix &= ring_buffer_mask; + if (data[cur_ix_masked] != data[prev_ix] || + data[cur_ix_masked + 1] != data[prev_ix + 1]) { + continue; + } + { + const size_t len = + FindMatchLengthWithLimit(&data[prev_ix], &data[cur_ix_masked], + max_length); + if (len > best_len) { + best_len = len; + InitBackwardMatch(matches++, backward, len); + } + } + } + if (best_len < max_length) { + matches = FN(StoreAndFindMatches)(self, data, cur_ix, + ring_buffer_mask, max_length, max_backward, &best_len, matches); + } + for (i = 0; i <= BROTLI_MAX_STATIC_DICTIONARY_MATCH_LEN; ++i) { + dict_matches[i] = kInvalidMatch; + } + { + size_t minlen = BROTLI_MAX(size_t, 4, best_len + 1); + if (BrotliFindAllStaticDictionaryMatches(dictionary, + &data[cur_ix_masked], minlen, max_length, &dict_matches[0])) { + size_t maxlen = BROTLI_MIN( + size_t, BROTLI_MAX_STATIC_DICTIONARY_MATCH_LEN, max_length); + size_t l; + for (l = minlen; l <= maxlen; ++l) { + uint32_t dict_id = dict_matches[l]; + if (dict_id < kInvalidMatch) { + size_t distance = dictionary_distance + (dict_id >> 5) + 1; + if (distance <= params->dist.max_distance) { + InitDictionaryBackwardMatch(matches++, distance, l, dict_id & 31); + } + } + } + } + } + return (size_t)(matches - orig_matches); +} + +/* Stores the hash of the next 4 bytes and re-roots the binary tree at the + current sequence, without returning any matches. + REQUIRES: ix + MAX_TREE_COMP_LENGTH <= end-of-current-block */ +static BROTLI_INLINE void FN(Store)(HashToBinaryTree* BROTLI_RESTRICT self, + const uint8_t* BROTLI_RESTRICT data, + const size_t mask, const size_t ix) { + /* Maximum distance is window size - 16, see section 9.1. of the spec. */ + const size_t max_backward = self->window_mask_ - BROTLI_WINDOW_GAP + 1; + FN(StoreAndFindMatches)(self, data, ix, mask, MAX_TREE_COMP_LENGTH, + max_backward, NULL, NULL); +} + +static BROTLI_INLINE void FN(StoreRange)(HashToBinaryTree* BROTLI_RESTRICT self, + const uint8_t* BROTLI_RESTRICT data, const size_t mask, + const size_t ix_start, const size_t ix_end) { + size_t i = ix_start; + size_t j = ix_start; + if (ix_start + 63 <= ix_end) { + i = ix_end - 63; + } + if (ix_start + 512 <= i) { + for (; j < i; j += 8) { + FN(Store)(self, data, mask, j); + } + } + for (; i < ix_end; ++i) { + FN(Store)(self, data, mask, i); + } +} + +static BROTLI_INLINE void FN(StitchToPreviousBlock)( + HashToBinaryTree* BROTLI_RESTRICT self, + size_t num_bytes, size_t position, const uint8_t* ringbuffer, + size_t ringbuffer_mask) { + if (num_bytes >= FN(HashTypeLength)() - 1 && + position >= MAX_TREE_COMP_LENGTH) { + /* Store the last `MAX_TREE_COMP_LENGTH - 1` positions in the hasher. + These could not be calculated before, since they require knowledge + of both the previous and the current block. */ + const size_t i_start = position - MAX_TREE_COMP_LENGTH + 1; + const size_t i_end = BROTLI_MIN(size_t, position, i_start + num_bytes); + size_t i; + for (i = i_start; i < i_end; ++i) { + /* Maximum distance is window size - 16, see section 9.1. of the spec. + Furthermore, we have to make sure that we don't look further back + from the start of the next block than the window size, otherwise we + could access already overwritten areas of the ring-buffer. */ + const size_t max_backward = + self->window_mask_ - BROTLI_MAX(size_t, + BROTLI_WINDOW_GAP - 1, + position - i); + /* We know that i + MAX_TREE_COMP_LENGTH <= position + num_bytes, i.e. the + end of the current block and that we have at least + MAX_TREE_COMP_LENGTH tail in the ring-buffer. */ + FN(StoreAndFindMatches)(self, ringbuffer, i, ringbuffer_mask, + MAX_TREE_COMP_LENGTH, max_backward, NULL, NULL); + } + } +} + +#undef BUCKET_SIZE + +#undef HashToBinaryTree diff --git a/packages/runtime/vendor/brotli/c/enc/histogram.c b/packages/runtime/vendor/brotli/c/enc/histogram.c new file mode 100644 index 000000000..43e4b38e3 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/histogram.c @@ -0,0 +1,101 @@ +/* Copyright 2013 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* Build per-context histograms of literals, commands and distance codes. */ + +#include "histogram.h" + +#include "../common/context.h" +#include "../common/platform.h" +#include "block_splitter.h" +#include "command.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +typedef struct BlockSplitIterator { + const BlockSplit* split_; /* Not owned. */ + size_t idx_; + size_t type_; + size_t length_; +} BlockSplitIterator; + +static void InitBlockSplitIterator(BlockSplitIterator* self, + const BlockSplit* split) { + self->split_ = split; + self->idx_ = 0; + self->type_ = 0; + self->length_ = split->lengths ? split->lengths[0] : 0; +} + +static void BlockSplitIteratorNext(BlockSplitIterator* self) { + if (self->length_ == 0) { + ++self->idx_; + self->type_ = self->split_->types[self->idx_]; + self->length_ = self->split_->lengths[self->idx_]; + } + --self->length_; +} + +void BrotliBuildHistogramsWithContext( + const Command* cmds, const size_t num_commands, + const BlockSplit* literal_split, const BlockSplit* insert_and_copy_split, + const BlockSplit* dist_split, const uint8_t* ringbuffer, size_t start_pos, + size_t mask, uint8_t prev_byte, uint8_t prev_byte2, + const ContextType* context_modes, HistogramLiteral* literal_histograms, + HistogramCommand* insert_and_copy_histograms, + HistogramDistance* copy_dist_histograms) { + size_t pos = start_pos; + BlockSplitIterator literal_it; + BlockSplitIterator insert_and_copy_it; + BlockSplitIterator dist_it; + size_t i; + + InitBlockSplitIterator(&literal_it, literal_split); + InitBlockSplitIterator(&insert_and_copy_it, insert_and_copy_split); + InitBlockSplitIterator(&dist_it, dist_split); + for (i = 0; i < num_commands; ++i) { + const Command* cmd = &cmds[i]; + size_t j; + BlockSplitIteratorNext(&insert_and_copy_it); + HistogramAddCommand(&insert_and_copy_histograms[insert_and_copy_it.type_], + cmd->cmd_prefix_); + /* TODO(eustas): unwrap iterator blocks. */ + for (j = cmd->insert_len_; j != 0; --j) { + size_t context; + BlockSplitIteratorNext(&literal_it); + context = literal_it.type_; + if (context_modes) { + ContextLut lut = BROTLI_CONTEXT_LUT(context_modes[context]); + context = (context << BROTLI_LITERAL_CONTEXT_BITS) + + BROTLI_CONTEXT(prev_byte, prev_byte2, lut); + } + HistogramAddLiteral(&literal_histograms[context], + ringbuffer[pos & mask]); + prev_byte2 = prev_byte; + prev_byte = ringbuffer[pos & mask]; + ++pos; + } + pos += CommandCopyLen(cmd); + if (CommandCopyLen(cmd)) { + prev_byte2 = ringbuffer[(pos - 2) & mask]; + prev_byte = ringbuffer[(pos - 1) & mask]; + if (cmd->cmd_prefix_ >= 128) { + size_t context; + BlockSplitIteratorNext(&dist_it); + context = (dist_it.type_ << BROTLI_DISTANCE_CONTEXT_BITS) + + CommandDistanceContext(cmd); + HistogramAddDistance(©_dist_histograms[context], + cmd->dist_prefix_ & 0x3FF); + } + } + } +} + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif diff --git a/packages/runtime/vendor/brotli/c/enc/histogram.h b/packages/runtime/vendor/brotli/c/enc/histogram.h new file mode 100644 index 000000000..a2accc331 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/histogram.h @@ -0,0 +1,60 @@ +/* Copyright 2013 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* Models the histograms of literals, commands and distance codes. */ + +#ifndef BROTLI_ENC_HISTOGRAM_H_ +#define BROTLI_ENC_HISTOGRAM_H_ + +#include "../common/constants.h" +#include "../common/context.h" +#include "../common/platform.h" +#include "block_splitter.h" +#include "command.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +/* The distance symbols effectively used by "Large Window Brotli" (32-bit). */ +#define BROTLI_NUM_HISTOGRAM_DISTANCE_SYMBOLS 544 + +#define FN(X) X ## Literal +#define DATA_SIZE BROTLI_NUM_LITERAL_SYMBOLS +#define DataType uint8_t +#include "histogram_inc.h" /* NOLINT(build/include) */ +#undef DataType +#undef DATA_SIZE +#undef FN + +#define FN(X) X ## Command +#define DataType uint16_t +#define DATA_SIZE BROTLI_NUM_COMMAND_SYMBOLS +#include "histogram_inc.h" /* NOLINT(build/include) */ +#undef DATA_SIZE +#undef FN + +#define FN(X) X ## Distance +#define DATA_SIZE BROTLI_NUM_HISTOGRAM_DISTANCE_SYMBOLS +#include "histogram_inc.h" /* NOLINT(build/include) */ +#undef DataType +#undef DATA_SIZE +#undef FN + +BROTLI_INTERNAL void BrotliBuildHistogramsWithContext( + const Command* cmds, const size_t num_commands, + const BlockSplit* literal_split, const BlockSplit* insert_and_copy_split, + const BlockSplit* dist_split, const uint8_t* ringbuffer, size_t pos, + size_t mask, uint8_t prev_byte, uint8_t prev_byte2, + const ContextType* context_modes, HistogramLiteral* literal_histograms, + HistogramCommand* insert_and_copy_histograms, + HistogramDistance* copy_dist_histograms); + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif + +#endif /* BROTLI_ENC_HISTOGRAM_H_ */ diff --git a/packages/runtime/vendor/brotli/c/enc/histogram_inc.h b/packages/runtime/vendor/brotli/c/enc/histogram_inc.h new file mode 100644 index 000000000..50eaf7468 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/histogram_inc.h @@ -0,0 +1,51 @@ +/* NOLINT(build/header_guard) */ +/* Copyright 2013 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* template parameters: Histogram, DATA_SIZE, DataType */ + +/* A simple container for histograms of data in blocks. */ + +typedef struct FN(Histogram) { + uint32_t data_[DATA_SIZE]; + size_t total_count_; + double bit_cost_; +} FN(Histogram); + +static BROTLI_INLINE void FN(HistogramClear)(FN(Histogram)* self) { + memset(self->data_, 0, sizeof(self->data_)); + self->total_count_ = 0; + self->bit_cost_ = HUGE_VAL; +} + +static BROTLI_INLINE void FN(ClearHistograms)( + FN(Histogram)* array, size_t length) { + size_t i; + for (i = 0; i < length; ++i) FN(HistogramClear)(array + i); +} + +static BROTLI_INLINE void FN(HistogramAdd)(FN(Histogram)* self, size_t val) { + ++self->data_[val]; + ++self->total_count_; +} + +static BROTLI_INLINE void FN(HistogramAddVector)(FN(Histogram)* self, + const DataType* p, size_t n) { + self->total_count_ += n; + n += 1; + while (--n) ++self->data_[*p++]; +} + +static BROTLI_INLINE void FN(HistogramAddHistogram)(FN(Histogram)* self, + const FN(Histogram)* v) { + size_t i; + self->total_count_ += v->total_count_; + for (i = 0; i < DATA_SIZE; ++i) { + self->data_[i] += v->data_[i]; + } +} + +static BROTLI_INLINE size_t FN(HistogramDataSize)(void) { return DATA_SIZE; } diff --git a/packages/runtime/vendor/brotli/c/enc/literal_cost.c b/packages/runtime/vendor/brotli/c/enc/literal_cost.c new file mode 100644 index 000000000..c4e207d09 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/literal_cost.c @@ -0,0 +1,178 @@ +/* Copyright 2013 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* Literal cost model to allow backward reference replacement to be efficient. +*/ + +#include "literal_cost.h" + +#include "../common/platform.h" +#include "fast_log.h" +#include "utf8_util.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +static size_t UTF8Position(size_t last, size_t c, size_t clamp) { + if (c < 128) { + return 0; /* Next one is the 'Byte 1' again. */ + } else if (c >= 192) { /* Next one is the 'Byte 2' of utf-8 encoding. */ + return BROTLI_MIN(size_t, 1, clamp); + } else { + /* Let's decide over the last byte if this ends the sequence. */ + if (last < 0xE0) { + return 0; /* Completed two or three byte coding. */ + } else { /* Next one is the 'Byte 3' of utf-8 encoding. */ + return BROTLI_MIN(size_t, 2, clamp); + } + } +} + +static size_t DecideMultiByteStatsLevel(size_t pos, size_t len, size_t mask, + const uint8_t* data) { + size_t counts[3] = { 0 }; + size_t max_utf8 = 1; /* should be 2, but 1 compresses better. */ + size_t last_c = 0; + size_t i; + for (i = 0; i < len; ++i) { + size_t c = data[(pos + i) & mask]; + ++counts[UTF8Position(last_c, c, 2)]; + last_c = c; + } + if (counts[2] < 500) { + max_utf8 = 1; + } + if (counts[1] + counts[2] < 25) { + max_utf8 = 0; + } + return max_utf8; +} + +static void EstimateBitCostsForLiteralsUTF8(size_t pos, size_t len, size_t mask, + const uint8_t* data, + size_t* histogram, float* cost) { + /* max_utf8 is 0 (normal ASCII single byte modeling), + 1 (for 2-byte UTF-8 modeling), or 2 (for 3-byte UTF-8 modeling). */ + const size_t max_utf8 = DecideMultiByteStatsLevel(pos, len, mask, data); + size_t window_half = 495; + size_t in_window = BROTLI_MIN(size_t, window_half, len); + size_t in_window_utf8[3] = { 0 }; + size_t i; + memset(histogram, 0, 3 * 256 * sizeof(histogram[0])); + + { /* Bootstrap histograms. */ + size_t last_c = 0; + size_t utf8_pos = 0; + for (i = 0; i < in_window; ++i) { + size_t c = data[(pos + i) & mask]; + ++histogram[256 * utf8_pos + c]; + ++in_window_utf8[utf8_pos]; + utf8_pos = UTF8Position(last_c, c, max_utf8); + last_c = c; + } + } + + /* Compute bit costs with sliding window. */ + for (i = 0; i < len; ++i) { + if (i >= window_half) { + /* Remove a byte in the past. */ + size_t c = + i < window_half + 1 ? 0 : data[(pos + i - window_half - 1) & mask]; + size_t last_c = + i < window_half + 2 ? 0 : data[(pos + i - window_half - 2) & mask]; + size_t utf8_pos2 = UTF8Position(last_c, c, max_utf8); + --histogram[256 * utf8_pos2 + data[(pos + i - window_half) & mask]]; + --in_window_utf8[utf8_pos2]; + } + if (i + window_half < len) { + /* Add a byte in the future. */ + size_t c = data[(pos + i + window_half - 1) & mask]; + size_t last_c = data[(pos + i + window_half - 2) & mask]; + size_t utf8_pos2 = UTF8Position(last_c, c, max_utf8); + ++histogram[256 * utf8_pos2 + data[(pos + i + window_half) & mask]]; + ++in_window_utf8[utf8_pos2]; + } + { + size_t c = i < 1 ? 0 : data[(pos + i - 1) & mask]; + size_t last_c = i < 2 ? 0 : data[(pos + i - 2) & mask]; + size_t utf8_pos = UTF8Position(last_c, c, max_utf8); + size_t masked_pos = (pos + i) & mask; + size_t histo = histogram[256 * utf8_pos + data[masked_pos]]; + static const size_t prologue_length = 2000; + static const double multiplier = 0.35 / 2000; + double lit_cost; + if (histo == 0) { + histo = 1; + } + lit_cost = FastLog2(in_window_utf8[utf8_pos]) - FastLog2(histo); + lit_cost += 0.02905; + if (lit_cost < 1.0) { + lit_cost *= 0.5; + lit_cost += 0.5; + } + /* Make the first bytes more expensive -- seems to help, not sure why. + Perhaps because the entropy source is changing its properties + rapidly in the beginning of the file, perhaps because the beginning + of the data is a statistical "anomaly". */ + if (i < prologue_length) { + lit_cost += 0.35 + multiplier * (double)i; + } + cost[i] = (float)lit_cost; + } + } +} + +void BrotliEstimateBitCostsForLiterals(size_t pos, size_t len, size_t mask, + const uint8_t* data, + size_t* histogram, float* cost) { + if (BrotliIsMostlyUTF8(data, pos, mask, len, kMinUTF8Ratio)) { + EstimateBitCostsForLiteralsUTF8(pos, len, mask, data, histogram, cost); + return; + } else { + size_t window_half = 2000; + size_t in_window = BROTLI_MIN(size_t, window_half, len); + size_t i; + memset(histogram, 0, 256 * sizeof(histogram[0])); + + /* Bootstrap histogram. */ + for (i = 0; i < in_window; ++i) { + ++histogram[data[(pos + i) & mask]]; + } + + /* Compute bit costs with sliding window. */ + for (i = 0; i < len; ++i) { + size_t histo; + if (i >= window_half) { + /* Remove a byte in the past. */ + --histogram[data[(pos + i - window_half) & mask]]; + --in_window; + } + if (i + window_half < len) { + /* Add a byte in the future. */ + ++histogram[data[(pos + i + window_half) & mask]]; + ++in_window; + } + histo = histogram[data[(pos + i) & mask]]; + if (histo == 0) { + histo = 1; + } + { + double lit_cost = FastLog2(in_window) - FastLog2(histo); + lit_cost += 0.029; + if (lit_cost < 1.0) { + lit_cost *= 0.5; + lit_cost += 0.5; + } + cost[i] = (float)lit_cost; + } + } + } +} + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif diff --git a/packages/runtime/vendor/brotli/c/enc/literal_cost.h b/packages/runtime/vendor/brotli/c/enc/literal_cost.h new file mode 100644 index 000000000..eb3f33836 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/literal_cost.h @@ -0,0 +1,30 @@ +/* Copyright 2013 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* Literal cost model to allow backward reference replacement to be efficient. +*/ + +#ifndef BROTLI_ENC_LITERAL_COST_H_ +#define BROTLI_ENC_LITERAL_COST_H_ + +#include "../common/platform.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +/* Estimates how many bits the literals in the interval [pos, pos + len) in the + ring-buffer (data, mask) will take entropy coded and writes these estimates + to the cost[0..len) array. */ +BROTLI_INTERNAL void BrotliEstimateBitCostsForLiterals( + size_t pos, size_t len, size_t mask, const uint8_t* data, size_t* histogram, + float* cost); + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif + +#endif /* BROTLI_ENC_LITERAL_COST_H_ */ diff --git a/packages/runtime/vendor/brotli/c/enc/matching_tag_mask.h b/packages/runtime/vendor/brotli/c/enc/matching_tag_mask.h new file mode 100644 index 000000000..8a1be1d71 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/matching_tag_mask.h @@ -0,0 +1,69 @@ +#ifndef THIRD_PARTY_BROTLI_ENC_MATCHING_TAG_MASK_H_ +#define THIRD_PARTY_BROTLI_ENC_MATCHING_TAG_MASK_H_ + +#include "../common/platform.h" + +#if defined(__SSE2__) || defined(_M_AMD64) || \ + (defined(_M_IX86) && defined(_M_IX86_FP) && (_M_IX86_FP >= 2)) +#define SUPPORTS_SSE_2 +#endif + +#if defined(SUPPORTS_SSE_2) +#include +#endif + + +static BROTLI_INLINE uint64_t GetMatchingTagMask( + size_t chunk_count, const uint8_t tag, + const uint8_t* BROTLI_RESTRICT tag_bucket, const size_t head) { + uint64_t matches = 0; +#if defined(SUPPORTS_SSE_2) + const __m128i comparison_mask = _mm_set1_epi8((char)tag); + size_t i; + for (i = 0; i < chunk_count && i < 4; i++) { + const __m128i chunk = + _mm_loadu_si128((const __m128i*)(const void*)(tag_bucket + 16 * i)); + const __m128i equal_mask = _mm_cmpeq_epi8(chunk, comparison_mask); + matches |= (uint64_t)_mm_movemask_epi8(equal_mask) << 16 * i; + } +#else + const int chunk_size = sizeof(size_t); + const size_t shift_amount = ((chunk_size * 8) - chunk_size); + const size_t xFF = ~((size_t)0); + const size_t x01 = xFF / 0xFF; + const size_t x80 = x01 << 7; + const size_t splat_char = tag * x01; + int i = ((int)chunk_count * 16) - chunk_size; + BROTLI_DCHECK((sizeof(size_t) == 4) || (sizeof(size_t) == 8)); +#if BROTLI_LITTLE_ENDIAN + const size_t extractMagic = (xFF / 0x7F) >> chunk_size; + do { + size_t chunk = BrotliUnalignedReadSizeT(&tag_bucket[i]); + chunk ^= splat_char; + chunk = (((chunk | x80) - x01) | chunk) & x80; + matches <<= chunk_size; + matches |= (chunk * extractMagic) >> shift_amount; + i -= chunk_size; + } while (i >= 0); +#else + const size_t msb = xFF ^ (xFF >> 1); + const size_t extractMagic = (msb / 0x1FF) | msb; + do { + size_t chunk = BrotliUnalignedReadSizeT(&tag_bucket[i]); + chunk ^= splat_char; + chunk = (((chunk | x80) - x01) | chunk) & x80; + matches <<= chunk_size; + matches |= ((chunk >> 7) * extractMagic) >> shift_amount; + i -= chunk_size; + } while (i >= 0); +#endif + matches = ~matches; +#endif + if (chunk_count == 1) return BrotliRotateRight16((uint16_t)matches, head); + if (chunk_count == 2) return BrotliRotateRight32((uint32_t)matches, head); + return BrotliRotateRight64(matches, head); +} + +#undef SUPPORTS_SSE_2 + +#endif // THIRD_PARTY_BROTLI_ENC_MATCHING_TAG_MASK_H_ diff --git a/packages/runtime/vendor/brotli/c/enc/memory.c b/packages/runtime/vendor/brotli/c/enc/memory.c new file mode 100644 index 000000000..67f81ffde --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/memory.c @@ -0,0 +1,189 @@ +/* Copyright 2015 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* Algorithms for distributing the literals and commands of a metablock between + block types and contexts. */ + +#include "memory.h" + +#include "../common/platform.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +#define MAX_NEW_ALLOCATED (BROTLI_ENCODER_MEMORY_MANAGER_SLOTS >> 2) +#define MAX_NEW_FREED (BROTLI_ENCODER_MEMORY_MANAGER_SLOTS >> 2) +#define MAX_PERM_ALLOCATED (BROTLI_ENCODER_MEMORY_MANAGER_SLOTS >> 1) + +#define PERM_ALLOCATED_OFFSET 0 +#define NEW_ALLOCATED_OFFSET MAX_PERM_ALLOCATED +#define NEW_FREED_OFFSET (MAX_PERM_ALLOCATED + MAX_NEW_ALLOCATED) + +void BrotliInitMemoryManager( + MemoryManager* m, brotli_alloc_func alloc_func, brotli_free_func free_func, + void* opaque) { + if (!alloc_func) { + m->alloc_func = BrotliDefaultAllocFunc; + m->free_func = BrotliDefaultFreeFunc; + m->opaque = 0; + } else { + m->alloc_func = alloc_func; + m->free_func = free_func; + m->opaque = opaque; + } +#if !defined(BROTLI_ENCODER_EXIT_ON_OOM) + m->is_oom = BROTLI_FALSE; + m->perm_allocated = 0; + m->new_allocated = 0; + m->new_freed = 0; +#endif /* BROTLI_ENCODER_EXIT_ON_OOM */ +} + +#if defined(BROTLI_ENCODER_EXIT_ON_OOM) + +void* BrotliAllocate(MemoryManager* m, size_t n) { + void* result = m->alloc_func(m->opaque, n); + if (!result) exit(EXIT_FAILURE); + return result; +} + +void BrotliFree(MemoryManager* m, void* p) { + m->free_func(m->opaque, p); +} + +void BrotliWipeOutMemoryManager(MemoryManager* m) { + BROTLI_UNUSED(m); +} + +#else /* BROTLI_ENCODER_EXIT_ON_OOM */ + +static void SortPointers(void** items, const size_t n) { + /* Shell sort. */ + /* TODO(eustas): fine-tune for "many slots" case */ + static const size_t gaps[] = {23, 10, 4, 1}; + int g = 0; + for (; g < 4; ++g) { + size_t gap = gaps[g]; + size_t i; + for (i = gap; i < n; ++i) { + size_t j = i; + void* tmp = items[i]; + for (; j >= gap && tmp < items[j - gap]; j -= gap) { + items[j] = items[j - gap]; + } + items[j] = tmp; + } + } +} + +static size_t Annihilate(void** a, size_t a_len, void** b, size_t b_len) { + size_t a_read_index = 0; + size_t b_read_index = 0; + size_t a_write_index = 0; + size_t b_write_index = 0; + size_t annihilated = 0; + while (a_read_index < a_len && b_read_index < b_len) { + if (a[a_read_index] == b[b_read_index]) { + a_read_index++; + b_read_index++; + annihilated++; + } else if (a[a_read_index] < b[b_read_index]) { + a[a_write_index++] = a[a_read_index++]; + } else { + b[b_write_index++] = b[b_read_index++]; + } + } + while (a_read_index < a_len) a[a_write_index++] = a[a_read_index++]; + while (b_read_index < b_len) b[b_write_index++] = b[b_read_index++]; + return annihilated; +} + +static void CollectGarbagePointers(MemoryManager* m) { + size_t annihilated; + SortPointers(m->pointers + NEW_ALLOCATED_OFFSET, m->new_allocated); + SortPointers(m->pointers + NEW_FREED_OFFSET, m->new_freed); + annihilated = Annihilate( + m->pointers + NEW_ALLOCATED_OFFSET, m->new_allocated, + m->pointers + NEW_FREED_OFFSET, m->new_freed); + m->new_allocated -= annihilated; + m->new_freed -= annihilated; + + if (m->new_freed != 0) { + annihilated = Annihilate( + m->pointers + PERM_ALLOCATED_OFFSET, m->perm_allocated, + m->pointers + NEW_FREED_OFFSET, m->new_freed); + m->perm_allocated -= annihilated; + m->new_freed -= annihilated; + BROTLI_DCHECK(m->new_freed == 0); + } + + if (m->new_allocated != 0) { + BROTLI_DCHECK(m->perm_allocated + m->new_allocated <= MAX_PERM_ALLOCATED); + memcpy(m->pointers + PERM_ALLOCATED_OFFSET + m->perm_allocated, + m->pointers + NEW_ALLOCATED_OFFSET, + sizeof(void*) * m->new_allocated); + m->perm_allocated += m->new_allocated; + m->new_allocated = 0; + SortPointers(m->pointers + PERM_ALLOCATED_OFFSET, m->perm_allocated); + } +} + +void* BrotliAllocate(MemoryManager* m, size_t n) { + void* result = m->alloc_func(m->opaque, n); + if (!result) { + m->is_oom = BROTLI_TRUE; + return NULL; + } + if (m->new_allocated == MAX_NEW_ALLOCATED) CollectGarbagePointers(m); + m->pointers[NEW_ALLOCATED_OFFSET + (m->new_allocated++)] = result; + return result; +} + +void BrotliFree(MemoryManager* m, void* p) { + if (!p) return; + m->free_func(m->opaque, p); + if (m->new_freed == MAX_NEW_FREED) CollectGarbagePointers(m); + m->pointers[NEW_FREED_OFFSET + (m->new_freed++)] = p; +} + +void BrotliWipeOutMemoryManager(MemoryManager* m) { + size_t i; + CollectGarbagePointers(m); + /* Now all unfreed pointers are in perm-allocated list. */ + for (i = 0; i < m->perm_allocated; ++i) { + m->free_func(m->opaque, m->pointers[PERM_ALLOCATED_OFFSET + i]); + } + m->perm_allocated = 0; +} + +#endif /* BROTLI_ENCODER_EXIT_ON_OOM */ + +void* BrotliBootstrapAlloc(size_t size, + brotli_alloc_func alloc_func, brotli_free_func free_func, void* opaque) { + if (!alloc_func && !free_func) { + return malloc(size); + } else if (alloc_func && free_func) { + return alloc_func(opaque, size); + } + return NULL; +} + +void BrotliBootstrapFree(void* address, MemoryManager* m) { + if (!address) { + /* Should not happen! */ + return; + } else { + /* Copy values, as those would be freed. */ + brotli_free_func free_func = m->free_func; + void* opaque = m->opaque; + free_func(opaque, address); + } +} + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif diff --git a/packages/runtime/vendor/brotli/c/enc/memory.h b/packages/runtime/vendor/brotli/c/enc/memory.h new file mode 100644 index 000000000..d8374815b --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/memory.h @@ -0,0 +1,127 @@ +/* Copyright 2016 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* Macros for memory management. */ + +#ifndef BROTLI_ENC_MEMORY_H_ +#define BROTLI_ENC_MEMORY_H_ + +#include "../common/platform.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +#if !defined(BROTLI_ENCODER_CLEANUP_ON_OOM) && \ + !defined(BROTLI_ENCODER_EXIT_ON_OOM) +#define BROTLI_ENCODER_EXIT_ON_OOM +#endif + +#if !defined(BROTLI_ENCODER_EXIT_ON_OOM) +#if defined(BROTLI_EXPERIMENTAL) +#define BROTLI_ENCODER_MEMORY_MANAGER_SLOTS (48*1024) +#else /* BROTLI_EXPERIMENTAL */ +#define BROTLI_ENCODER_MEMORY_MANAGER_SLOTS 256 +#endif /* BROTLI_EXPERIMENTAL */ +#else /* BROTLI_ENCODER_EXIT_ON_OOM */ +#define BROTLI_ENCODER_MEMORY_MANAGER_SLOTS 0 +#endif /* BROTLI_ENCODER_EXIT_ON_OOM */ + +typedef struct MemoryManager { + brotli_alloc_func alloc_func; + brotli_free_func free_func; + void* opaque; +#if !defined(BROTLI_ENCODER_EXIT_ON_OOM) + BROTLI_BOOL is_oom; + size_t perm_allocated; + size_t new_allocated; + size_t new_freed; + void* pointers[BROTLI_ENCODER_MEMORY_MANAGER_SLOTS]; +#endif /* BROTLI_ENCODER_EXIT_ON_OOM */ +} MemoryManager; + +BROTLI_INTERNAL void BrotliInitMemoryManager( + MemoryManager* m, brotli_alloc_func alloc_func, brotli_free_func free_func, + void* opaque); + +BROTLI_INTERNAL void* BrotliAllocate(MemoryManager* m, size_t n); +#define BROTLI_ALLOC(M, T, N) \ + ((N) > 0 ? ((T*)BrotliAllocate((M), (N) * sizeof(T))) : NULL) + +BROTLI_INTERNAL void BrotliFree(MemoryManager* m, void* p); +#define BROTLI_FREE(M, P) { \ + BrotliFree((M), (P)); \ + P = NULL; \ +} + +#if defined(BROTLI_ENCODER_EXIT_ON_OOM) +#define BROTLI_IS_OOM(M) (!!0) +#else /* BROTLI_ENCODER_EXIT_ON_OOM */ +#define BROTLI_IS_OOM(M) (!!(M)->is_oom) +#endif /* BROTLI_ENCODER_EXIT_ON_OOM */ + +/* +BROTLI_IS_NULL is a fake check, BROTLI_IS_OOM does the heavy lifting. +The only purpose of it is to explain static analyzers the state of things. +NB: use ONLY together with BROTLI_IS_OOM + AND ONLY for allocations in the current scope. + */ +#if defined(__clang_analyzer__) && !defined(BROTLI_ENCODER_EXIT_ON_OOM) +#define BROTLI_IS_NULL(A) ((A) == nullptr) +#else /* defined(__clang_analyzer__) */ +#define BROTLI_IS_NULL(A) (!!0) +#endif /* defined(__clang_analyzer__) */ + +BROTLI_INTERNAL void BrotliWipeOutMemoryManager(MemoryManager* m); + +/* +Dynamically grows array capacity to at least the requested size +M: MemoryManager +T: data type +A: array +C: capacity +R: requested size +*/ +#define BROTLI_ENSURE_CAPACITY(M, T, A, C, R) { \ + if (C < (R)) { \ + size_t _new_size = (C == 0) ? (R) : C; \ + T* new_array; \ + while (_new_size < (R)) _new_size *= 2; \ + new_array = BROTLI_ALLOC((M), T, _new_size); \ + if (!BROTLI_IS_OOM(M) && !BROTLI_IS_NULL(new_array) && C != 0) \ + memcpy(new_array, A, C * sizeof(T)); \ + BROTLI_FREE((M), A); \ + A = new_array; \ + C = _new_size; \ + } \ +} + +/* +Appends value and dynamically grows array capacity when needed +M: MemoryManager +T: data type +A: array +C: array capacity +S: array size +V: value to append +*/ +#define BROTLI_ENSURE_CAPACITY_APPEND(M, T, A, C, S, V) { \ + (S)++; \ + BROTLI_ENSURE_CAPACITY(M, T, A, C, S); \ + A[(S) - 1] = (V); \ +} + +/* "Bootstrap" allocations are not tracked by memory manager; should be used + only to allocate MemoryManager itself (or structure containing it). */ +BROTLI_INTERNAL void* BrotliBootstrapAlloc(size_t size, + brotli_alloc_func alloc_func, brotli_free_func free_func, void* opaque); +BROTLI_INTERNAL void BrotliBootstrapFree(void* address, MemoryManager* m); + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif + +#endif /* BROTLI_ENC_MEMORY_H_ */ diff --git a/packages/runtime/vendor/brotli/c/enc/metablock.c b/packages/runtime/vendor/brotli/c/enc/metablock.c new file mode 100644 index 000000000..dfe8120c8 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/metablock.c @@ -0,0 +1,675 @@ +/* Copyright 2015 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* Algorithms for distributing the literals and commands of a metablock between + block types and contexts. */ + +#include "metablock.h" + +#include "../common/constants.h" +#include "../common/context.h" +#include "../common/platform.h" +#include "bit_cost.h" +#include "block_splitter.h" +#include "cluster.h" +#include "command.h" +#include "entropy_encode.h" +#include "histogram.h" +#include "memory.h" +#include "params.h" +#include "prefix.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +void BrotliInitDistanceParams(BrotliDistanceParams* dist_params, + uint32_t npostfix, uint32_t ndirect, BROTLI_BOOL large_window) { + uint32_t alphabet_size_max; + uint32_t alphabet_size_limit; + uint32_t max_distance; + + dist_params->distance_postfix_bits = npostfix; + dist_params->num_direct_distance_codes = ndirect; + + alphabet_size_max = BROTLI_DISTANCE_ALPHABET_SIZE( + npostfix, ndirect, BROTLI_MAX_DISTANCE_BITS); + alphabet_size_limit = alphabet_size_max; + max_distance = ndirect + (1U << (BROTLI_MAX_DISTANCE_BITS + npostfix + 2)) - + (1U << (npostfix + 2)); + + if (large_window) { + BrotliDistanceCodeLimit limit = BrotliCalculateDistanceCodeLimit( + BROTLI_MAX_ALLOWED_DISTANCE, npostfix, ndirect); + alphabet_size_max = BROTLI_DISTANCE_ALPHABET_SIZE( + npostfix, ndirect, BROTLI_LARGE_MAX_DISTANCE_BITS); + alphabet_size_limit = limit.max_alphabet_size; + max_distance = limit.max_distance; + } + + dist_params->alphabet_size_max = alphabet_size_max; + dist_params->alphabet_size_limit = alphabet_size_limit; + dist_params->max_distance = max_distance; +} + +static void RecomputeDistancePrefixes(Command* cmds, + size_t num_commands, + const BrotliDistanceParams* orig_params, + const BrotliDistanceParams* new_params) { + size_t i; + + if (orig_params->distance_postfix_bits == new_params->distance_postfix_bits && + orig_params->num_direct_distance_codes == + new_params->num_direct_distance_codes) { + return; + } + + for (i = 0; i < num_commands; ++i) { + Command* cmd = &cmds[i]; + if (CommandCopyLen(cmd) && cmd->cmd_prefix_ >= 128) { + PrefixEncodeCopyDistance(CommandRestoreDistanceCode(cmd, orig_params), + new_params->num_direct_distance_codes, + new_params->distance_postfix_bits, + &cmd->dist_prefix_, + &cmd->dist_extra_); + } + } +} + +static BROTLI_BOOL ComputeDistanceCost(const Command* cmds, + size_t num_commands, + const BrotliDistanceParams* orig_params, + const BrotliDistanceParams* new_params, + double* cost, + HistogramDistance* tmp) { + size_t i; + BROTLI_BOOL equal_params = BROTLI_FALSE; + uint16_t dist_prefix; + uint32_t dist_extra; + double extra_bits = 0.0; + HistogramClearDistance(tmp); + + if (orig_params->distance_postfix_bits == new_params->distance_postfix_bits && + orig_params->num_direct_distance_codes == + new_params->num_direct_distance_codes) { + equal_params = BROTLI_TRUE; + } + + for (i = 0; i < num_commands; i++) { + const Command* cmd = &cmds[i]; + if (CommandCopyLen(cmd) && cmd->cmd_prefix_ >= 128) { + if (equal_params) { + dist_prefix = cmd->dist_prefix_; + } else { + uint32_t distance = CommandRestoreDistanceCode(cmd, orig_params); + if (distance > new_params->max_distance) { + return BROTLI_FALSE; + } + PrefixEncodeCopyDistance(distance, + new_params->num_direct_distance_codes, + new_params->distance_postfix_bits, + &dist_prefix, + &dist_extra); + } + HistogramAddDistance(tmp, dist_prefix & 0x3FF); + extra_bits += dist_prefix >> 10; + } + } + + *cost = BrotliPopulationCostDistance(tmp) + extra_bits; + return BROTLI_TRUE; +} + +void BrotliBuildMetaBlock(MemoryManager* m, + const uint8_t* ringbuffer, + const size_t pos, + const size_t mask, + BrotliEncoderParams* params, + uint8_t prev_byte, + uint8_t prev_byte2, + Command* cmds, + size_t num_commands, + ContextType literal_context_mode, + MetaBlockSplit* mb) { + /* Histogram ids need to fit in one byte. */ + static const size_t kMaxNumberOfHistograms = 256; + HistogramDistance* distance_histograms; + HistogramLiteral* literal_histograms; + ContextType* literal_context_modes = NULL; + size_t literal_histograms_size; + size_t distance_histograms_size; + size_t i; + size_t literal_context_multiplier = 1; + uint32_t npostfix; + uint32_t ndirect_msb = 0; + BROTLI_BOOL check_orig = BROTLI_TRUE; + double best_dist_cost = 1e99; + BrotliDistanceParams orig_params = params->dist; + BrotliDistanceParams new_params = params->dist; + HistogramDistance* tmp = BROTLI_ALLOC(m, HistogramDistance, 1); + + if (BROTLI_IS_OOM(m) || BROTLI_IS_NULL(tmp)) return; + + for (npostfix = 0; npostfix <= BROTLI_MAX_NPOSTFIX; npostfix++) { + for (; ndirect_msb < 16; ndirect_msb++) { + uint32_t ndirect = ndirect_msb << npostfix; + BROTLI_BOOL skip; + double dist_cost; + BrotliInitDistanceParams(&new_params, npostfix, ndirect, + params->large_window); + if (npostfix == orig_params.distance_postfix_bits && + ndirect == orig_params.num_direct_distance_codes) { + check_orig = BROTLI_FALSE; + } + skip = !ComputeDistanceCost( + cmds, num_commands, &orig_params, &new_params, &dist_cost, tmp); + if (skip || (dist_cost > best_dist_cost)) { + break; + } + best_dist_cost = dist_cost; + params->dist = new_params; + } + if (ndirect_msb > 0) ndirect_msb--; + ndirect_msb /= 2; + } + if (check_orig) { + double dist_cost; + ComputeDistanceCost(cmds, num_commands, &orig_params, &orig_params, + &dist_cost, tmp); + if (dist_cost < best_dist_cost) { + /* NB: currently unused; uncomment when more param tuning is added. */ + /* best_dist_cost = dist_cost; */ + params->dist = orig_params; + } + } + BROTLI_FREE(m, tmp); + RecomputeDistancePrefixes(cmds, num_commands, &orig_params, ¶ms->dist); + + BrotliSplitBlock(m, cmds, num_commands, + ringbuffer, pos, mask, params, + &mb->literal_split, + &mb->command_split, + &mb->distance_split); + if (BROTLI_IS_OOM(m)) return; + + if (!params->disable_literal_context_modeling) { + literal_context_multiplier = 1 << BROTLI_LITERAL_CONTEXT_BITS; + literal_context_modes = + BROTLI_ALLOC(m, ContextType, mb->literal_split.num_types); + if (BROTLI_IS_OOM(m) || BROTLI_IS_NULL(literal_context_modes)) return; + for (i = 0; i < mb->literal_split.num_types; ++i) { + literal_context_modes[i] = literal_context_mode; + } + } + + literal_histograms_size = + mb->literal_split.num_types * literal_context_multiplier; + literal_histograms = + BROTLI_ALLOC(m, HistogramLiteral, literal_histograms_size); + if (BROTLI_IS_OOM(m) || BROTLI_IS_NULL(literal_histograms)) return; + ClearHistogramsLiteral(literal_histograms, literal_histograms_size); + + distance_histograms_size = + mb->distance_split.num_types << BROTLI_DISTANCE_CONTEXT_BITS; + distance_histograms = + BROTLI_ALLOC(m, HistogramDistance, distance_histograms_size); + if (BROTLI_IS_OOM(m) || BROTLI_IS_NULL(distance_histograms)) return; + ClearHistogramsDistance(distance_histograms, distance_histograms_size); + + BROTLI_DCHECK(mb->command_histograms == 0); + mb->command_histograms_size = mb->command_split.num_types; + mb->command_histograms = + BROTLI_ALLOC(m, HistogramCommand, mb->command_histograms_size); + if (BROTLI_IS_OOM(m) || BROTLI_IS_NULL(mb->command_histograms)) return; + ClearHistogramsCommand(mb->command_histograms, mb->command_histograms_size); + + BrotliBuildHistogramsWithContext(cmds, num_commands, + &mb->literal_split, &mb->command_split, &mb->distance_split, + ringbuffer, pos, mask, prev_byte, prev_byte2, literal_context_modes, + literal_histograms, mb->command_histograms, distance_histograms); + BROTLI_FREE(m, literal_context_modes); + + BROTLI_DCHECK(mb->literal_context_map == 0); + mb->literal_context_map_size = + mb->literal_split.num_types << BROTLI_LITERAL_CONTEXT_BITS; + mb->literal_context_map = + BROTLI_ALLOC(m, uint32_t, mb->literal_context_map_size); + if (BROTLI_IS_OOM(m) || BROTLI_IS_NULL(mb->literal_context_map)) return; + + BROTLI_DCHECK(mb->literal_histograms == 0); + mb->literal_histograms_size = mb->literal_context_map_size; + mb->literal_histograms = + BROTLI_ALLOC(m, HistogramLiteral, mb->literal_histograms_size); + if (BROTLI_IS_OOM(m) || BROTLI_IS_NULL(mb->literal_histograms)) return; + + BrotliClusterHistogramsLiteral(m, literal_histograms, literal_histograms_size, + kMaxNumberOfHistograms, mb->literal_histograms, + &mb->literal_histograms_size, mb->literal_context_map); + if (BROTLI_IS_OOM(m)) return; + BROTLI_FREE(m, literal_histograms); + + if (params->disable_literal_context_modeling) { + /* Distribute assignment to all contexts. */ + for (i = mb->literal_split.num_types; i != 0;) { + size_t j = 0; + i--; + for (; j < (1 << BROTLI_LITERAL_CONTEXT_BITS); j++) { + mb->literal_context_map[(i << BROTLI_LITERAL_CONTEXT_BITS) + j] = + mb->literal_context_map[i]; + } + } + } + + BROTLI_DCHECK(mb->distance_context_map == 0); + mb->distance_context_map_size = + mb->distance_split.num_types << BROTLI_DISTANCE_CONTEXT_BITS; + mb->distance_context_map = + BROTLI_ALLOC(m, uint32_t, mb->distance_context_map_size); + if (BROTLI_IS_OOM(m) || BROTLI_IS_NULL(mb->distance_context_map)) return; + + BROTLI_DCHECK(mb->distance_histograms == 0); + mb->distance_histograms_size = mb->distance_context_map_size; + mb->distance_histograms = + BROTLI_ALLOC(m, HistogramDistance, mb->distance_histograms_size); + if (BROTLI_IS_OOM(m) || BROTLI_IS_NULL(mb->distance_histograms)) return; + + BrotliClusterHistogramsDistance(m, distance_histograms, + mb->distance_context_map_size, + kMaxNumberOfHistograms, + mb->distance_histograms, + &mb->distance_histograms_size, + mb->distance_context_map); + if (BROTLI_IS_OOM(m)) return; + BROTLI_FREE(m, distance_histograms); +} + +#define FN(X) X ## Literal +#include "metablock_inc.h" /* NOLINT(build/include) */ +#undef FN + +#define FN(X) X ## Command +#include "metablock_inc.h" /* NOLINT(build/include) */ +#undef FN + +#define FN(X) X ## Distance +#include "metablock_inc.h" /* NOLINT(build/include) */ +#undef FN + +/* Greedy block splitter for one block category (literal, command or distance). + Gathers histograms for all context buckets. */ +typedef struct ContextBlockSplitter { + /* Alphabet size of particular block category. */ + size_t alphabet_size_; + size_t num_contexts_; + size_t max_block_types_; + /* We collect at least this many symbols for each block. */ + size_t min_block_size_; + /* We merge histograms A and B if + entropy(A+B) < entropy(A) + entropy(B) + split_threshold_, + where A is the current histogram and B is the histogram of the last or the + second last block type. */ + double split_threshold_; + + size_t num_blocks_; + BlockSplit* split_; /* not owned */ + HistogramLiteral* histograms_; /* not owned */ + size_t* histograms_size_; /* not owned */ + + /* The number of symbols that we want to collect before deciding on whether + or not to merge the block with a previous one or emit a new block. */ + size_t target_block_size_; + /* The number of symbols in the current histogram. */ + size_t block_size_; + /* Offset of the current histogram. */ + size_t curr_histogram_ix_; + /* Offset of the histograms of the previous two block types. */ + size_t last_histogram_ix_[2]; + /* Entropy of the previous two block types. */ + double last_entropy_[2 * BROTLI_MAX_STATIC_CONTEXTS]; + /* The number of times we merged the current block with the last one. */ + size_t merge_last_count_; +} ContextBlockSplitter; + +static void InitContextBlockSplitter( + MemoryManager* m, ContextBlockSplitter* self, size_t alphabet_size, + size_t num_contexts, size_t min_block_size, double split_threshold, + size_t num_symbols, BlockSplit* split, HistogramLiteral** histograms, + size_t* histograms_size) { + size_t max_num_blocks = num_symbols / min_block_size + 1; + size_t max_num_types; + BROTLI_DCHECK(num_contexts <= BROTLI_MAX_STATIC_CONTEXTS); + + self->alphabet_size_ = alphabet_size; + self->num_contexts_ = num_contexts; + self->max_block_types_ = BROTLI_MAX_NUMBER_OF_BLOCK_TYPES / num_contexts; + self->min_block_size_ = min_block_size; + self->split_threshold_ = split_threshold; + self->num_blocks_ = 0; + self->split_ = split; + self->histograms_size_ = histograms_size; + self->target_block_size_ = min_block_size; + self->block_size_ = 0; + self->curr_histogram_ix_ = 0; + self->merge_last_count_ = 0; + + /* We have to allocate one more histogram than the maximum number of block + types for the current histogram when the meta-block is too big. */ + max_num_types = + BROTLI_MIN(size_t, max_num_blocks, self->max_block_types_ + 1); + BROTLI_ENSURE_CAPACITY(m, uint8_t, + split->types, split->types_alloc_size, max_num_blocks); + BROTLI_ENSURE_CAPACITY(m, uint32_t, + split->lengths, split->lengths_alloc_size, max_num_blocks); + if (BROTLI_IS_OOM(m)) return; + split->num_blocks = max_num_blocks; + if (BROTLI_IS_OOM(m)) return; + BROTLI_DCHECK(*histograms == 0); + *histograms_size = max_num_types * num_contexts; + *histograms = BROTLI_ALLOC(m, HistogramLiteral, *histograms_size); + self->histograms_ = *histograms; + if (BROTLI_IS_OOM(m) || BROTLI_IS_NULL(*histograms)) return; + /* Clear only current histogram. */ + ClearHistogramsLiteral(&self->histograms_[0], num_contexts); + self->last_histogram_ix_[0] = self->last_histogram_ix_[1] = 0; +} + +/* Does either of three things: + (1) emits the current block with a new block type; + (2) emits the current block with the type of the second last block; + (3) merges the current block with the last block. */ +static void ContextBlockSplitterFinishBlock( + ContextBlockSplitter* self, MemoryManager* m, BROTLI_BOOL is_final) { + BlockSplit* split = self->split_; + const size_t num_contexts = self->num_contexts_; + double* last_entropy = self->last_entropy_; + HistogramLiteral* histograms = self->histograms_; + + if (self->block_size_ < self->min_block_size_) { + self->block_size_ = self->min_block_size_; + } + if (self->num_blocks_ == 0) { + size_t i; + /* Create first block. */ + split->lengths[0] = (uint32_t)self->block_size_; + split->types[0] = 0; + + for (i = 0; i < num_contexts; ++i) { + last_entropy[i] = + BrotliBitsEntropy(histograms[i].data_, self->alphabet_size_); + last_entropy[num_contexts + i] = last_entropy[i]; + } + ++self->num_blocks_; + ++split->num_types; + self->curr_histogram_ix_ += num_contexts; + if (self->curr_histogram_ix_ < *self->histograms_size_) { + ClearHistogramsLiteral( + &self->histograms_[self->curr_histogram_ix_], self->num_contexts_); + } + self->block_size_ = 0; + } else if (self->block_size_ > 0) { + /* Try merging the set of histograms for the current block type with the + respective set of histograms for the last and second last block types. + Decide over the split based on the total reduction of entropy across + all contexts. */ + double entropy[BROTLI_MAX_STATIC_CONTEXTS]; + HistogramLiteral* combined_histo = + BROTLI_ALLOC(m, HistogramLiteral, 2 * num_contexts); + double combined_entropy[2 * BROTLI_MAX_STATIC_CONTEXTS]; + double diff[2] = { 0.0 }; + size_t i; + if (BROTLI_IS_OOM(m) || BROTLI_IS_NULL(combined_histo)) return; + for (i = 0; i < num_contexts; ++i) { + size_t curr_histo_ix = self->curr_histogram_ix_ + i; + size_t j; + entropy[i] = BrotliBitsEntropy(histograms[curr_histo_ix].data_, + self->alphabet_size_); + for (j = 0; j < 2; ++j) { + size_t jx = j * num_contexts + i; + size_t last_histogram_ix = self->last_histogram_ix_[j] + i; + combined_histo[jx] = histograms[curr_histo_ix]; + HistogramAddHistogramLiteral(&combined_histo[jx], + &histograms[last_histogram_ix]); + combined_entropy[jx] = BrotliBitsEntropy( + &combined_histo[jx].data_[0], self->alphabet_size_); + diff[j] += combined_entropy[jx] - entropy[i] - last_entropy[jx]; + } + } + + if (split->num_types < self->max_block_types_ && + diff[0] > self->split_threshold_ && + diff[1] > self->split_threshold_) { + /* Create new block. */ + split->lengths[self->num_blocks_] = (uint32_t)self->block_size_; + split->types[self->num_blocks_] = (uint8_t)split->num_types; + self->last_histogram_ix_[1] = self->last_histogram_ix_[0]; + self->last_histogram_ix_[0] = split->num_types * num_contexts; + for (i = 0; i < num_contexts; ++i) { + last_entropy[num_contexts + i] = last_entropy[i]; + last_entropy[i] = entropy[i]; + } + ++self->num_blocks_; + ++split->num_types; + self->curr_histogram_ix_ += num_contexts; + if (self->curr_histogram_ix_ < *self->histograms_size_) { + ClearHistogramsLiteral( + &self->histograms_[self->curr_histogram_ix_], self->num_contexts_); + } + self->block_size_ = 0; + self->merge_last_count_ = 0; + self->target_block_size_ = self->min_block_size_; + } else if (diff[1] < diff[0] - 20.0) { + /* Combine this block with second last block. */ + split->lengths[self->num_blocks_] = (uint32_t)self->block_size_; + split->types[self->num_blocks_] = split->types[self->num_blocks_ - 2]; + BROTLI_SWAP(size_t, self->last_histogram_ix_, 0, 1); + for (i = 0; i < num_contexts; ++i) { + histograms[self->last_histogram_ix_[0] + i] = + combined_histo[num_contexts + i]; + last_entropy[num_contexts + i] = last_entropy[i]; + last_entropy[i] = combined_entropy[num_contexts + i]; + HistogramClearLiteral(&histograms[self->curr_histogram_ix_ + i]); + } + ++self->num_blocks_; + self->block_size_ = 0; + self->merge_last_count_ = 0; + self->target_block_size_ = self->min_block_size_; + } else { + /* Combine this block with last block. */ + split->lengths[self->num_blocks_ - 1] += (uint32_t)self->block_size_; + for (i = 0; i < num_contexts; ++i) { + histograms[self->last_histogram_ix_[0] + i] = combined_histo[i]; + last_entropy[i] = combined_entropy[i]; + if (split->num_types == 1) { + last_entropy[num_contexts + i] = last_entropy[i]; + } + HistogramClearLiteral(&histograms[self->curr_histogram_ix_ + i]); + } + self->block_size_ = 0; + if (++self->merge_last_count_ > 1) { + self->target_block_size_ += self->min_block_size_; + } + } + BROTLI_FREE(m, combined_histo); + } + if (is_final) { + *self->histograms_size_ = split->num_types * num_contexts; + split->num_blocks = self->num_blocks_; + } +} + +/* Adds the next symbol to the current block type and context. When the + current block reaches the target size, decides on merging the block. */ +static void ContextBlockSplitterAddSymbol( + ContextBlockSplitter* self, MemoryManager* m, + size_t symbol, size_t context) { + HistogramAddLiteral(&self->histograms_[self->curr_histogram_ix_ + context], + symbol); + ++self->block_size_; + if (self->block_size_ == self->target_block_size_) { + ContextBlockSplitterFinishBlock(self, m, /* is_final = */ BROTLI_FALSE); + if (BROTLI_IS_OOM(m)) return; + } +} + +static void MapStaticContexts(MemoryManager* m, + size_t num_contexts, + const uint32_t* static_context_map, + MetaBlockSplit* mb) { + size_t i; + BROTLI_DCHECK(mb->literal_context_map == 0); + mb->literal_context_map_size = + mb->literal_split.num_types << BROTLI_LITERAL_CONTEXT_BITS; + mb->literal_context_map = + BROTLI_ALLOC(m, uint32_t, mb->literal_context_map_size); + if (BROTLI_IS_OOM(m) || BROTLI_IS_NULL(mb->literal_context_map)) return; + + for (i = 0; i < mb->literal_split.num_types; ++i) { + uint32_t offset = (uint32_t)(i * num_contexts); + size_t j; + for (j = 0; j < (1u << BROTLI_LITERAL_CONTEXT_BITS); ++j) { + mb->literal_context_map[(i << BROTLI_LITERAL_CONTEXT_BITS) + j] = + offset + static_context_map[j]; + } + } +} + +typedef struct GreedyMetablockArena { + union { + BlockSplitterLiteral plain; + ContextBlockSplitter ctx; + } lit_blocks; + BlockSplitterCommand cmd_blocks; + BlockSplitterDistance dist_blocks; +} GreedyMetablockArena; + +static BROTLI_INLINE void BrotliBuildMetaBlockGreedyInternal( + MemoryManager* m, GreedyMetablockArena* arena, const uint8_t* ringbuffer, + size_t pos, size_t mask, uint8_t prev_byte, uint8_t prev_byte2, + ContextLut literal_context_lut, const size_t num_contexts, + const uint32_t* static_context_map, const Command* commands, + size_t n_commands, MetaBlockSplit* mb) { + size_t num_literals = 0; + size_t i; + for (i = 0; i < n_commands; ++i) { + num_literals += commands[i].insert_len_; + } + + if (num_contexts == 1) { + InitBlockSplitterLiteral(m, &arena->lit_blocks.plain, 256, 512, 400.0, + num_literals, &mb->literal_split, &mb->literal_histograms, + &mb->literal_histograms_size); + } else { + InitContextBlockSplitter(m, &arena->lit_blocks.ctx, 256, num_contexts, 512, + 400.0, num_literals, &mb->literal_split, &mb->literal_histograms, + &mb->literal_histograms_size); + } + if (BROTLI_IS_OOM(m)) return; + InitBlockSplitterCommand(m, &arena->cmd_blocks, BROTLI_NUM_COMMAND_SYMBOLS, + 1024, 500.0, n_commands, &mb->command_split, &mb->command_histograms, + &mb->command_histograms_size); + if (BROTLI_IS_OOM(m)) return; + InitBlockSplitterDistance(m, &arena->dist_blocks, 64, 512, 100.0, n_commands, + &mb->distance_split, &mb->distance_histograms, + &mb->distance_histograms_size); + if (BROTLI_IS_OOM(m)) return; + + for (i = 0; i < n_commands; ++i) { + const Command cmd = commands[i]; + size_t j; + BlockSplitterAddSymbolCommand(&arena->cmd_blocks, cmd.cmd_prefix_); + for (j = cmd.insert_len_; j != 0; --j) { + uint8_t literal = ringbuffer[pos & mask]; + if (num_contexts == 1) { + BlockSplitterAddSymbolLiteral(&arena->lit_blocks.plain, literal); + } else { + size_t context = + BROTLI_CONTEXT(prev_byte, prev_byte2, literal_context_lut); + ContextBlockSplitterAddSymbol(&arena->lit_blocks.ctx, m, literal, + static_context_map[context]); + if (BROTLI_IS_OOM(m)) return; + } + prev_byte2 = prev_byte; + prev_byte = literal; + ++pos; + } + pos += CommandCopyLen(&cmd); + if (CommandCopyLen(&cmd)) { + prev_byte2 = ringbuffer[(pos - 2) & mask]; + prev_byte = ringbuffer[(pos - 1) & mask]; + if (cmd.cmd_prefix_ >= 128) { + BlockSplitterAddSymbolDistance( + &arena->dist_blocks, cmd.dist_prefix_ & 0x3FF); + } + } + } + + if (num_contexts == 1) { + BlockSplitterFinishBlockLiteral( + &arena->lit_blocks.plain, /* is_final = */ BROTLI_TRUE); + } else { + ContextBlockSplitterFinishBlock( + &arena->lit_blocks.ctx, m, /* is_final = */ BROTLI_TRUE); + if (BROTLI_IS_OOM(m)) return; + } + BlockSplitterFinishBlockCommand( + &arena->cmd_blocks, /* is_final = */ BROTLI_TRUE); + BlockSplitterFinishBlockDistance( + &arena->dist_blocks, /* is_final = */ BROTLI_TRUE); + + if (num_contexts > 1) { + MapStaticContexts(m, num_contexts, static_context_map, mb); + } +} + +void BrotliBuildMetaBlockGreedy(MemoryManager* m, + const uint8_t* ringbuffer, + size_t pos, + size_t mask, + uint8_t prev_byte, + uint8_t prev_byte2, + ContextLut literal_context_lut, + size_t num_contexts, + const uint32_t* static_context_map, + const Command* commands, + size_t n_commands, + MetaBlockSplit* mb) { + GreedyMetablockArena* arena = BROTLI_ALLOC(m, GreedyMetablockArena, 1); + if (BROTLI_IS_OOM(m) || BROTLI_IS_NULL(arena)) return; + if (num_contexts == 1) { + BrotliBuildMetaBlockGreedyInternal(m, arena, ringbuffer, pos, mask, + prev_byte, prev_byte2, literal_context_lut, 1, NULL, commands, + n_commands, mb); + } else { + BrotliBuildMetaBlockGreedyInternal(m, arena, ringbuffer, pos, mask, + prev_byte, prev_byte2, literal_context_lut, num_contexts, + static_context_map, commands, n_commands, mb); + } + BROTLI_FREE(m, arena); +} + +void BrotliOptimizeHistograms(uint32_t num_distance_codes, + MetaBlockSplit* mb) { + uint8_t good_for_rle[BROTLI_NUM_COMMAND_SYMBOLS]; + size_t i; + for (i = 0; i < mb->literal_histograms_size; ++i) { + BrotliOptimizeHuffmanCountsForRle(256, mb->literal_histograms[i].data_, + good_for_rle); + } + for (i = 0; i < mb->command_histograms_size; ++i) { + BrotliOptimizeHuffmanCountsForRle(BROTLI_NUM_COMMAND_SYMBOLS, + mb->command_histograms[i].data_, + good_for_rle); + } + for (i = 0; i < mb->distance_histograms_size; ++i) { + BrotliOptimizeHuffmanCountsForRle(num_distance_codes, + mb->distance_histograms[i].data_, + good_for_rle); + } +} + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif diff --git a/packages/runtime/vendor/brotli/c/enc/metablock.h b/packages/runtime/vendor/brotli/c/enc/metablock.h new file mode 100644 index 000000000..e7d10526d --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/metablock.h @@ -0,0 +1,106 @@ +/* Copyright 2015 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* Algorithms for distributing the literals and commands of a metablock between + block types and contexts. */ + +#ifndef BROTLI_ENC_METABLOCK_H_ +#define BROTLI_ENC_METABLOCK_H_ + +#include "../common/context.h" +#include "../common/platform.h" +#include "block_splitter.h" +#include "command.h" +#include "histogram.h" +#include "memory.h" +#include "params.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +#define BROTLI_MAX_STATIC_CONTEXTS 13 + +typedef struct MetaBlockSplit { + BlockSplit literal_split; + BlockSplit command_split; + BlockSplit distance_split; + uint32_t* literal_context_map; + size_t literal_context_map_size; + uint32_t* distance_context_map; + size_t distance_context_map_size; + HistogramLiteral* literal_histograms; + size_t literal_histograms_size; + HistogramCommand* command_histograms; + size_t command_histograms_size; + HistogramDistance* distance_histograms; + size_t distance_histograms_size; +} MetaBlockSplit; + +static BROTLI_INLINE void InitMetaBlockSplit(MetaBlockSplit* mb) { + BrotliInitBlockSplit(&mb->literal_split); + BrotliInitBlockSplit(&mb->command_split); + BrotliInitBlockSplit(&mb->distance_split); + mb->literal_context_map = 0; + mb->literal_context_map_size = 0; + mb->distance_context_map = 0; + mb->distance_context_map_size = 0; + mb->literal_histograms = 0; + mb->literal_histograms_size = 0; + mb->command_histograms = 0; + mb->command_histograms_size = 0; + mb->distance_histograms = 0; + mb->distance_histograms_size = 0; +} + +static BROTLI_INLINE void DestroyMetaBlockSplit( + MemoryManager* m, MetaBlockSplit* mb) { + BrotliDestroyBlockSplit(m, &mb->literal_split); + BrotliDestroyBlockSplit(m, &mb->command_split); + BrotliDestroyBlockSplit(m, &mb->distance_split); + BROTLI_FREE(m, mb->literal_context_map); + BROTLI_FREE(m, mb->distance_context_map); + BROTLI_FREE(m, mb->literal_histograms); + BROTLI_FREE(m, mb->command_histograms); + BROTLI_FREE(m, mb->distance_histograms); +} + +/* Uses the slow shortest-path block splitter and does context clustering. + The distance parameters are dynamically selected based on the commands + which get recomputed under the new distance parameters. The new distance + parameters are stored into *params. */ +BROTLI_INTERNAL void BrotliBuildMetaBlock(MemoryManager* m, + const uint8_t* ringbuffer, + const size_t pos, + const size_t mask, + BrotliEncoderParams* params, + uint8_t prev_byte, + uint8_t prev_byte2, + Command* cmds, + size_t num_commands, + ContextType literal_context_mode, + MetaBlockSplit* mb); + +/* Uses a fast greedy block splitter that tries to merge current block with the + last or the second last block and uses a static context clustering which + is the same for all block types. */ +BROTLI_INTERNAL void BrotliBuildMetaBlockGreedy( + MemoryManager* m, const uint8_t* ringbuffer, size_t pos, size_t mask, + uint8_t prev_byte, uint8_t prev_byte2, ContextLut literal_context_lut, + size_t num_contexts, const uint32_t* static_context_map, + const Command* commands, size_t n_commands, MetaBlockSplit* mb); + +BROTLI_INTERNAL void BrotliOptimizeHistograms(uint32_t num_distance_codes, + MetaBlockSplit* mb); + +BROTLI_INTERNAL void BrotliInitDistanceParams(BrotliDistanceParams* params, + uint32_t npostfix, uint32_t ndirect, BROTLI_BOOL large_window); + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif + +#endif /* BROTLI_ENC_METABLOCK_H_ */ diff --git a/packages/runtime/vendor/brotli/c/enc/metablock_inc.h b/packages/runtime/vendor/brotli/c/enc/metablock_inc.h new file mode 100644 index 000000000..e7a7bc821 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/metablock_inc.h @@ -0,0 +1,185 @@ +/* NOLINT(build/header_guard) */ +/* Copyright 2015 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* template parameters: FN */ + +#define HistogramType FN(Histogram) + +/* Greedy block splitter for one block category (literal, command or distance). +*/ +typedef struct FN(BlockSplitter) { + /* Alphabet size of particular block category. */ + size_t alphabet_size_; + /* We collect at least this many symbols for each block. */ + size_t min_block_size_; + /* We merge histograms A and B if + entropy(A+B) < entropy(A) + entropy(B) + split_threshold_, + where A is the current histogram and B is the histogram of the last or the + second last block type. */ + double split_threshold_; + + size_t num_blocks_; + BlockSplit* split_; /* not owned */ + HistogramType* histograms_; /* not owned */ + size_t* histograms_size_; /* not owned */ + + /* Temporary storage for BlockSplitterFinishBlock. */ + HistogramType combined_histo[2]; + + /* The number of symbols that we want to collect before deciding on whether + or not to merge the block with a previous one or emit a new block. */ + size_t target_block_size_; + /* The number of symbols in the current histogram. */ + size_t block_size_; + /* Offset of the current histogram. */ + size_t curr_histogram_ix_; + /* Offset of the histograms of the previous two block types. */ + size_t last_histogram_ix_[2]; + /* Entropy of the previous two block types. */ + double last_entropy_[2]; + /* The number of times we merged the current block with the last one. */ + size_t merge_last_count_; +} FN(BlockSplitter); + +static void FN(InitBlockSplitter)( + MemoryManager* m, FN(BlockSplitter)* self, size_t alphabet_size, + size_t min_block_size, double split_threshold, size_t num_symbols, + BlockSplit* split, HistogramType** histograms, size_t* histograms_size) { + size_t max_num_blocks = num_symbols / min_block_size + 1; + /* We have to allocate one more histogram than the maximum number of block + types for the current histogram when the meta-block is too big. */ + size_t max_num_types = + BROTLI_MIN(size_t, max_num_blocks, BROTLI_MAX_NUMBER_OF_BLOCK_TYPES + 1); + self->alphabet_size_ = alphabet_size; + self->min_block_size_ = min_block_size; + self->split_threshold_ = split_threshold; + self->num_blocks_ = 0; + self->split_ = split; + self->histograms_size_ = histograms_size; + self->target_block_size_ = min_block_size; + self->block_size_ = 0; + self->curr_histogram_ix_ = 0; + self->merge_last_count_ = 0; + BROTLI_ENSURE_CAPACITY(m, uint8_t, + split->types, split->types_alloc_size, max_num_blocks); + BROTLI_ENSURE_CAPACITY(m, uint32_t, + split->lengths, split->lengths_alloc_size, max_num_blocks); + if (BROTLI_IS_OOM(m)) return; + self->split_->num_blocks = max_num_blocks; + BROTLI_DCHECK(*histograms == 0); + *histograms_size = max_num_types; + *histograms = BROTLI_ALLOC(m, HistogramType, *histograms_size); + self->histograms_ = *histograms; + if (BROTLI_IS_OOM(m) || BROTLI_IS_NULL(*histograms)) return; + /* Clear only current histogram. */ + FN(HistogramClear)(&self->histograms_[0]); + self->last_histogram_ix_[0] = self->last_histogram_ix_[1] = 0; +} + +/* Does either of three things: + (1) emits the current block with a new block type; + (2) emits the current block with the type of the second last block; + (3) merges the current block with the last block. */ +static void FN(BlockSplitterFinishBlock)( + FN(BlockSplitter)* self, BROTLI_BOOL is_final) { + BlockSplit* split = self->split_; + double* last_entropy = self->last_entropy_; + HistogramType* histograms = self->histograms_; + self->block_size_ = + BROTLI_MAX(size_t, self->block_size_, self->min_block_size_); + if (self->num_blocks_ == 0) { + /* Create first block. */ + split->lengths[0] = (uint32_t)self->block_size_; + split->types[0] = 0; + last_entropy[0] = + BrotliBitsEntropy(histograms[0].data_, self->alphabet_size_); + last_entropy[1] = last_entropy[0]; + ++self->num_blocks_; + ++split->num_types; + ++self->curr_histogram_ix_; + if (self->curr_histogram_ix_ < *self->histograms_size_) + FN(HistogramClear)(&histograms[self->curr_histogram_ix_]); + self->block_size_ = 0; + } else if (self->block_size_ > 0) { + double entropy = BrotliBitsEntropy( + histograms[self->curr_histogram_ix_].data_, self->alphabet_size_); + double combined_entropy[2]; + double diff[2]; + size_t j; + for (j = 0; j < 2; ++j) { + size_t last_histogram_ix = self->last_histogram_ix_[j]; + self->combined_histo[j] = histograms[self->curr_histogram_ix_]; + FN(HistogramAddHistogram)(&self->combined_histo[j], + &histograms[last_histogram_ix]); + combined_entropy[j] = BrotliBitsEntropy( + &self->combined_histo[j].data_[0], self->alphabet_size_); + diff[j] = combined_entropy[j] - entropy - last_entropy[j]; + } + + if (split->num_types < BROTLI_MAX_NUMBER_OF_BLOCK_TYPES && + diff[0] > self->split_threshold_ && + diff[1] > self->split_threshold_) { + /* Create new block. */ + split->lengths[self->num_blocks_] = (uint32_t)self->block_size_; + split->types[self->num_blocks_] = (uint8_t)split->num_types; + self->last_histogram_ix_[1] = self->last_histogram_ix_[0]; + self->last_histogram_ix_[0] = (uint8_t)split->num_types; + last_entropy[1] = last_entropy[0]; + last_entropy[0] = entropy; + ++self->num_blocks_; + ++split->num_types; + ++self->curr_histogram_ix_; + if (self->curr_histogram_ix_ < *self->histograms_size_) + FN(HistogramClear)(&histograms[self->curr_histogram_ix_]); + self->block_size_ = 0; + self->merge_last_count_ = 0; + self->target_block_size_ = self->min_block_size_; + } else if (diff[1] < diff[0] - 20.0) { + /* Combine this block with second last block. */ + split->lengths[self->num_blocks_] = (uint32_t)self->block_size_; + split->types[self->num_blocks_] = split->types[self->num_blocks_ - 2]; + BROTLI_SWAP(size_t, self->last_histogram_ix_, 0, 1); + histograms[self->last_histogram_ix_[0]] = self->combined_histo[1]; + last_entropy[1] = last_entropy[0]; + last_entropy[0] = combined_entropy[1]; + ++self->num_blocks_; + self->block_size_ = 0; + FN(HistogramClear)(&histograms[self->curr_histogram_ix_]); + self->merge_last_count_ = 0; + self->target_block_size_ = self->min_block_size_; + } else { + /* Combine this block with last block. */ + split->lengths[self->num_blocks_ - 1] += (uint32_t)self->block_size_; + histograms[self->last_histogram_ix_[0]] = self->combined_histo[0]; + last_entropy[0] = combined_entropy[0]; + if (split->num_types == 1) { + last_entropy[1] = last_entropy[0]; + } + self->block_size_ = 0; + FN(HistogramClear)(&histograms[self->curr_histogram_ix_]); + if (++self->merge_last_count_ > 1) { + self->target_block_size_ += self->min_block_size_; + } + } + } + if (is_final) { + *self->histograms_size_ = split->num_types; + split->num_blocks = self->num_blocks_; + } +} + +/* Adds the next symbol to the current histogram. When the current histogram + reaches the target size, decides on merging the block. */ +static void FN(BlockSplitterAddSymbol)(FN(BlockSplitter)* self, size_t symbol) { + FN(HistogramAdd)(&self->histograms_[self->curr_histogram_ix_], symbol); + ++self->block_size_; + if (self->block_size_ == self->target_block_size_) { + FN(BlockSplitterFinishBlock)(self, /* is_final = */ BROTLI_FALSE); + } +} + +#undef HistogramType diff --git a/packages/runtime/vendor/brotli/c/enc/params.h b/packages/runtime/vendor/brotli/c/enc/params.h new file mode 100644 index 000000000..fd1de0096 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/params.h @@ -0,0 +1,46 @@ +/* Copyright 2017 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* Parameters for the Brotli encoder with chosen quality levels. */ + +#ifndef BROTLI_ENC_PARAMS_H_ +#define BROTLI_ENC_PARAMS_H_ + +#include +#include "encoder_dict.h" + +typedef struct BrotliHasherParams { + int type; + int bucket_bits; + int block_bits; + int num_last_distances_to_check; +} BrotliHasherParams; + +typedef struct BrotliDistanceParams { + uint32_t distance_postfix_bits; + uint32_t num_direct_distance_codes; + uint32_t alphabet_size_max; + uint32_t alphabet_size_limit; + size_t max_distance; +} BrotliDistanceParams; + +/* Encoding parameters */ +typedef struct BrotliEncoderParams { + BrotliEncoderMode mode; + int quality; + int lgwin; + int lgblock; + size_t stream_offset; + size_t size_hint; + BROTLI_BOOL disable_literal_context_modeling; + BROTLI_BOOL large_window; + BrotliHasherParams hasher; + BrotliDistanceParams dist; + /* TODO(eustas): rename to BrotliShared... */ + SharedEncoderDictionary dictionary; +} BrotliEncoderParams; + +#endif /* BROTLI_ENC_PARAMS_H_ */ diff --git a/packages/runtime/vendor/brotli/c/enc/prefix.h b/packages/runtime/vendor/brotli/c/enc/prefix.h new file mode 100644 index 000000000..24c0afc17 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/prefix.h @@ -0,0 +1,52 @@ +/* Copyright 2013 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* Functions for encoding of integers into prefix codes the amount of extra + bits, and the actual values of the extra bits. */ + +#ifndef BROTLI_ENC_PREFIX_H_ +#define BROTLI_ENC_PREFIX_H_ + +#include "../common/constants.h" +#include "../common/platform.h" +#include "fast_log.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +/* Here distance_code is an intermediate code, i.e. one of the special codes or + the actual distance increased by BROTLI_NUM_DISTANCE_SHORT_CODES - 1. */ +static BROTLI_INLINE void PrefixEncodeCopyDistance(size_t distance_code, + size_t num_direct_codes, + size_t postfix_bits, + uint16_t* code, + uint32_t* extra_bits) { + if (distance_code < BROTLI_NUM_DISTANCE_SHORT_CODES + num_direct_codes) { + *code = (uint16_t)distance_code; + *extra_bits = 0; + return; + } else { + size_t dist = ((size_t)1 << (postfix_bits + 2u)) + + (distance_code - BROTLI_NUM_DISTANCE_SHORT_CODES - num_direct_codes); + size_t bucket = Log2FloorNonZero(dist) - 1; + size_t postfix_mask = (1u << postfix_bits) - 1; + size_t postfix = dist & postfix_mask; + size_t prefix = (dist >> bucket) & 1; + size_t offset = (2 + prefix) << bucket; + size_t nbits = bucket - postfix_bits; + *code = (uint16_t)((nbits << 10) | + (BROTLI_NUM_DISTANCE_SHORT_CODES + num_direct_codes + + ((2 * (nbits - 1) + prefix) << postfix_bits) + postfix)); + *extra_bits = (uint32_t)((dist - offset) >> postfix_bits); + } +} + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif + +#endif /* BROTLI_ENC_PREFIX_H_ */ diff --git a/packages/runtime/vendor/brotli/c/enc/quality.h b/packages/runtime/vendor/brotli/c/enc/quality.h new file mode 100644 index 000000000..45d67bf40 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/quality.h @@ -0,0 +1,209 @@ +/* Copyright 2016 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* Constants and formulas that affect speed-ratio trade-offs and thus define + quality levels. */ + +#ifndef BROTLI_ENC_QUALITY_H_ +#define BROTLI_ENC_QUALITY_H_ + +#include "../common/platform.h" +#include +#include "params.h" + +#define FAST_ONE_PASS_COMPRESSION_QUALITY 0 +#define FAST_TWO_PASS_COMPRESSION_QUALITY 1 +#define ZOPFLIFICATION_QUALITY 10 +#define HQ_ZOPFLIFICATION_QUALITY 11 + +#define MAX_QUALITY_FOR_STATIC_ENTROPY_CODES 2 +#define MIN_QUALITY_FOR_BLOCK_SPLIT 4 +#define MIN_QUALITY_FOR_NONZERO_DISTANCE_PARAMS 4 +#define MIN_QUALITY_FOR_OPTIMIZE_HISTOGRAMS 4 +#define MIN_QUALITY_FOR_EXTENSIVE_REFERENCE_SEARCH 5 +#define MIN_QUALITY_FOR_CONTEXT_MODELING 5 +#define MIN_QUALITY_FOR_HQ_CONTEXT_MODELING 7 +#define MIN_QUALITY_FOR_HQ_BLOCK_SPLITTING 10 + +/* For quality below MIN_QUALITY_FOR_BLOCK_SPLIT there is no block splitting, + so we buffer at most this much literals and commands. */ +#define MAX_NUM_DELAYED_SYMBOLS 0x2FFF + +/* Returns hash-table size for quality levels 0 and 1. */ +static BROTLI_INLINE size_t MaxHashTableSize(int quality) { + return quality == FAST_ONE_PASS_COMPRESSION_QUALITY ? 1 << 15 : 1 << 17; +} + +/* The maximum length for which the zopflification uses distinct distances. */ +#define MAX_ZOPFLI_LEN_QUALITY_10 150 +#define MAX_ZOPFLI_LEN_QUALITY_11 325 + +/* Do not thoroughly search when a long copy is found. */ +#define BROTLI_LONG_COPY_QUICK_STEP 16384 + +static BROTLI_INLINE size_t MaxZopfliLen(const BrotliEncoderParams* params) { + return params->quality <= 10 ? + MAX_ZOPFLI_LEN_QUALITY_10 : + MAX_ZOPFLI_LEN_QUALITY_11; +} + +/* Number of best candidates to evaluate to expand Zopfli chain. */ +static BROTLI_INLINE size_t MaxZopfliCandidates( + const BrotliEncoderParams* params) { + return params->quality <= 10 ? 1 : 5; +} + +static BROTLI_INLINE void SanitizeParams(BrotliEncoderParams* params) { + params->quality = BROTLI_MIN(int, BROTLI_MAX_QUALITY, + BROTLI_MAX(int, BROTLI_MIN_QUALITY, params->quality)); + if (params->quality <= MAX_QUALITY_FOR_STATIC_ENTROPY_CODES) { + params->large_window = BROTLI_FALSE; + } + if (params->lgwin < BROTLI_MIN_WINDOW_BITS) { + params->lgwin = BROTLI_MIN_WINDOW_BITS; + } else { + int max_lgwin = params->large_window ? BROTLI_LARGE_MAX_WINDOW_BITS : + BROTLI_MAX_WINDOW_BITS; + if (params->lgwin > max_lgwin) params->lgwin = max_lgwin; + } +} + +/* Returns optimized lg_block value. */ +static BROTLI_INLINE int ComputeLgBlock(const BrotliEncoderParams* params) { + int lgblock = params->lgblock; + if (params->quality == FAST_ONE_PASS_COMPRESSION_QUALITY || + params->quality == FAST_TWO_PASS_COMPRESSION_QUALITY) { + lgblock = params->lgwin; + } else if (params->quality < MIN_QUALITY_FOR_BLOCK_SPLIT) { + lgblock = 14; + } else if (lgblock == 0) { + lgblock = 16; + if (params->quality >= 9 && params->lgwin > lgblock) { + lgblock = BROTLI_MIN(int, 18, params->lgwin); + } + } else { + lgblock = BROTLI_MIN(int, BROTLI_MAX_INPUT_BLOCK_BITS, + BROTLI_MAX(int, BROTLI_MIN_INPUT_BLOCK_BITS, lgblock)); + } + return lgblock; +} + +/* Returns log2 of the size of main ring buffer area. + Allocate at least lgwin + 1 bits for the ring buffer so that the newly + added block fits there completely and we still get lgwin bits and at least + read_block_size_bits + 1 bits because the copy tail length needs to be + smaller than ring-buffer size. */ +static BROTLI_INLINE int ComputeRbBits(const BrotliEncoderParams* params) { + return 1 + BROTLI_MAX(int, params->lgwin, params->lgblock); +} + +static BROTLI_INLINE size_t MaxMetablockSize( + const BrotliEncoderParams* params) { + int bits = + BROTLI_MIN(int, ComputeRbBits(params), BROTLI_MAX_INPUT_BLOCK_BITS); + return (size_t)1 << bits; +} + +/* When searching for backward references and have not seen matches for a long + time, we can skip some match lookups. Unsuccessful match lookups are very + expensive and this kind of a heuristic speeds up compression quite a lot. + At first 8 byte strides are taken and every second byte is put to hasher. + After 4x more literals stride by 16 bytes, every put 4-th byte to hasher. + Applied only to qualities 2 to 9. */ +static BROTLI_INLINE size_t LiteralSpreeLengthForSparseSearch( + const BrotliEncoderParams* params) { + return params->quality < 9 ? 64 : 512; +} + +/* Quality to hasher mapping: + + - q02: h02 (longest_match_quickly), b16, l5 + + - q03: h03 (longest_match_quickly), b17, l5 + + - q04: h04 (longest_match_quickly), b17, l5 + - q04: h54 (longest_match_quickly), b20, l7 | for large files + + - q05: h58 (longest_match_simd ), b14, l4 + - q05: h68 (longest_match64_simd ), b15, l5 | for large files + - q05: h40 (forgetful_chain ), b15, l4 | for small window + + - q06: h58 (longest_match_simd ), b14, l4 + - q06: h68 (longest_match64_simd ), b15, l5 | for large files + - q06: h40 (forgetful_chain ), b15, l4 | for small window + + - q07: h58 (longest_match_simd ), b15, l4 + - q07: h68 (longest_match64_simd ), b15, l5 | for large files + - q07: h41 (forgetful_chain ), b15, l4 | for small window + + - q08: h05 (longest_match ), b15, l4 + - q08: h06 (longest_match64 ), b15, l5 | for large files + - q08: h41 (forgetful_chain ), b15, l4 | for small window + + - q09: h05 (longest_match ), b15, l4 + - q09: h06 (longest_match64 ), b15, l5 | for large files + - q09: h42 (forgetful_chain ), b15, l4 | for small window + + - q10: t10 (to_binary_tree ), b17, l128 + + - q11: t10 (to_binary_tree ), b17, l128 + + Where "q" is quality, "h" is hasher type, "b" is bucket bits, + "l" is source len. */ +static BROTLI_INLINE void ChooseHasher(const BrotliEncoderParams* params, + BrotliHasherParams* hparams) { + if (params->quality > 9) { + hparams->type = 10; + } else if (params->quality == 4 && params->size_hint >= (1 << 20)) { + hparams->type = 54; + } else if (params->quality < 5) { + hparams->type = params->quality; + } else if (params->lgwin <= 16) { + hparams->type = params->quality < 7 ? 40 : params->quality < 9 ? 41 : 42; + } else if (params->size_hint >= (1 << 20) && params->lgwin >= 19) { +#if defined(BROTLI_MAX_SIMD_QUALITY) + hparams->type = params->quality <= BROTLI_MAX_SIMD_QUALITY ? 68 : 6; +#else + hparams->type = 6; +#endif + hparams->block_bits = params->quality - 1; + hparams->bucket_bits = 15; + hparams->num_last_distances_to_check = + params->quality < 7 ? 4 : params->quality < 9 ? 10 : 16; + } else { + /* TODO(eustas): often previous setting (H6) is faster and denser; consider + adding an option to use it. */ +#if defined(BROTLI_MAX_SIMD_QUALITY) + hparams->type = params->quality <= BROTLI_MAX_SIMD_QUALITY ? 58 : 5; +#else + hparams->type = 5; +#endif + hparams->block_bits = params->quality - 1; + hparams->bucket_bits = params->quality < 7 ? 14 : 15; + hparams->num_last_distances_to_check = + params->quality < 7 ? 4 : params->quality < 9 ? 10 : 16; + } + + if (params->lgwin > 24) { + /* Different hashers for large window brotli: not for qualities <= 2, + these are too fast for large window. Not for qualities >= 10: their + hasher already works well with large window. So the changes are: + H3 --> H35: for quality 3. + H54 --> H55: for quality 4 with size hint > 1MB + H6/H68 --> H65: for qualities 5, 6, 7, 8, 9. */ + if (hparams->type == 3) { + hparams->type = 35; + } + if (hparams->type == 54) { + hparams->type = 55; + } + if (hparams->type == 6 || hparams->type == 68) { + hparams->type = 65; + } + } +} + +#endif /* BROTLI_ENC_QUALITY_H_ */ diff --git a/packages/runtime/vendor/brotli/c/enc/ringbuffer.h b/packages/runtime/vendor/brotli/c/enc/ringbuffer.h new file mode 100644 index 000000000..5bb75ed7c --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/ringbuffer.h @@ -0,0 +1,165 @@ +/* Copyright 2013 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* Sliding window over the input data. */ + +#ifndef BROTLI_ENC_RINGBUFFER_H_ +#define BROTLI_ENC_RINGBUFFER_H_ + +#include "../common/platform.h" +#include "memory.h" +#include "params.h" +#include "quality.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +/* A RingBuffer(window_bits, tail_bits) contains `1 << window_bits' bytes of + data in a circular manner: writing a byte writes it to: + `position() % (1 << window_bits)'. + For convenience, the RingBuffer array contains another copy of the + first `1 << tail_bits' bytes: + buffer_[i] == buffer_[i + (1 << window_bits)], if i < (1 << tail_bits), + and another copy of the last two bytes: + buffer_[-1] == buffer_[(1 << window_bits) - 1] and + buffer_[-2] == buffer_[(1 << window_bits) - 2]. */ +typedef struct RingBuffer { + /* Size of the ring-buffer is (1 << window_bits) + tail_size_. */ + const uint32_t size_; + const uint32_t mask_; + const uint32_t tail_size_; + const uint32_t total_size_; + + uint32_t cur_size_; + /* Position to write in the ring buffer. */ + uint32_t pos_; + /* The actual ring buffer containing the copy of the last two bytes, the data, + and the copy of the beginning as a tail. */ + uint8_t* data_; + /* The start of the ring-buffer. */ + uint8_t* buffer_; +} RingBuffer; + +static BROTLI_INLINE void RingBufferInit(RingBuffer* rb) { + rb->cur_size_ = 0; + rb->pos_ = 0; + rb->data_ = 0; + rb->buffer_ = 0; +} + +static BROTLI_INLINE void RingBufferSetup( + const BrotliEncoderParams* params, RingBuffer* rb) { + int window_bits = ComputeRbBits(params); + int tail_bits = params->lgblock; + *(uint32_t*)&rb->size_ = 1u << window_bits; + *(uint32_t*)&rb->mask_ = (1u << window_bits) - 1; + *(uint32_t*)&rb->tail_size_ = 1u << tail_bits; + *(uint32_t*)&rb->total_size_ = rb->size_ + rb->tail_size_; +} + +static BROTLI_INLINE void RingBufferFree(MemoryManager* m, RingBuffer* rb) { + BROTLI_FREE(m, rb->data_); +} + +/* Allocates or re-allocates data_ to the given length + plus some slack + region before and after. Fills the slack regions with zeros. */ +static BROTLI_INLINE void RingBufferInitBuffer( + MemoryManager* m, const uint32_t buflen, RingBuffer* rb) { + static const size_t kSlackForEightByteHashingEverywhere = 7; + uint8_t* new_data = BROTLI_ALLOC( + m, uint8_t, 2 + buflen + kSlackForEightByteHashingEverywhere); + size_t i; + if (BROTLI_IS_OOM(m) || BROTLI_IS_NULL(new_data)) return; + if (rb->data_) { + memcpy(new_data, rb->data_, + 2 + rb->cur_size_ + kSlackForEightByteHashingEverywhere); + BROTLI_FREE(m, rb->data_); + } + rb->data_ = new_data; + rb->cur_size_ = buflen; + rb->buffer_ = rb->data_ + 2; + rb->buffer_[-2] = rb->buffer_[-1] = 0; + for (i = 0; i < kSlackForEightByteHashingEverywhere; ++i) { + rb->buffer_[rb->cur_size_ + i] = 0; + } +} + +static BROTLI_INLINE void RingBufferWriteTail( + const uint8_t* bytes, size_t n, RingBuffer* rb) { + const size_t masked_pos = rb->pos_ & rb->mask_; + if (BROTLI_PREDICT_FALSE(masked_pos < rb->tail_size_)) { + /* Just fill the tail buffer with the beginning data. */ + const size_t p = rb->size_ + masked_pos; + memcpy(&rb->buffer_[p], bytes, + BROTLI_MIN(size_t, n, rb->tail_size_ - masked_pos)); + } +} + +/* Push bytes into the ring buffer. */ +static BROTLI_INLINE void RingBufferWrite( + MemoryManager* m, const uint8_t* bytes, size_t n, RingBuffer* rb) { + if (rb->pos_ == 0 && n < rb->tail_size_) { + /* Special case for the first write: to process the first block, we don't + need to allocate the whole ring-buffer and we don't need the tail + either. However, we do this memory usage optimization only if the + first write is less than the tail size, which is also the input block + size, otherwise it is likely that other blocks will follow and we + will need to reallocate to the full size anyway. */ + rb->pos_ = (uint32_t)n; + RingBufferInitBuffer(m, rb->pos_, rb); + if (BROTLI_IS_OOM(m)) return; + memcpy(rb->buffer_, bytes, n); + return; + } + if (rb->cur_size_ < rb->total_size_) { + /* Lazily allocate the full buffer. */ + RingBufferInitBuffer(m, rb->total_size_, rb); + if (BROTLI_IS_OOM(m)) return; + /* Initialize the last two bytes to zero, so that we don't have to worry + later when we copy the last two bytes to the first two positions. */ + rb->buffer_[rb->size_ - 2] = 0; + rb->buffer_[rb->size_ - 1] = 0; + /* Initialize tail; might be touched by "best_len++" optimization when + ring buffer is "full". */ + rb->buffer_[rb->size_] = 241; + } + { + const size_t masked_pos = rb->pos_ & rb->mask_; + /* The length of the writes is limited so that we do not need to worry + about a write */ + RingBufferWriteTail(bytes, n, rb); + if (BROTLI_PREDICT_TRUE(masked_pos + n <= rb->size_)) { + /* A single write fits. */ + memcpy(&rb->buffer_[masked_pos], bytes, n); + } else { + /* Split into two writes. + Copy into the end of the buffer, including the tail buffer. */ + memcpy(&rb->buffer_[masked_pos], bytes, + BROTLI_MIN(size_t, n, rb->total_size_ - masked_pos)); + /* Copy into the beginning of the buffer */ + memcpy(&rb->buffer_[0], bytes + (rb->size_ - masked_pos), + n - (rb->size_ - masked_pos)); + } + } + { + BROTLI_BOOL not_first_lap = (rb->pos_ & (1u << 31)) != 0; + uint32_t rb_pos_mask = (1u << 31) - 1; + rb->buffer_[-2] = rb->buffer_[rb->size_ - 2]; + rb->buffer_[-1] = rb->buffer_[rb->size_ - 1]; + rb->pos_ = (rb->pos_ & rb_pos_mask) + (uint32_t)(n & rb_pos_mask); + if (not_first_lap) { + /* Wrap, but preserve not-a-first-lap feature. */ + rb->pos_ |= 1u << 31; + } + } +} + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif + +#endif /* BROTLI_ENC_RINGBUFFER_H_ */ diff --git a/packages/runtime/vendor/brotli/c/enc/state.h b/packages/runtime/vendor/brotli/c/enc/state.h new file mode 100644 index 000000000..325f9f3dd --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/state.h @@ -0,0 +1,104 @@ +/* Copyright 2022 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* Encoder state. */ + +#ifndef BROTLI_ENC_STATE_H_ +#define BROTLI_ENC_STATE_H_ + +#include "../common/constants.h" +#include "../common/platform.h" +#include "command.h" +#include "compress_fragment.h" +#include "compress_fragment_two_pass.h" +#include "hash.h" +#include "memory.h" +#include "params.h" +#include "ringbuffer.h" + +typedef enum BrotliEncoderStreamState { + /* Default state. */ + BROTLI_STREAM_PROCESSING = 0, + /* Intermediate state; after next block is emitted, byte-padding should be + performed before getting back to default state. */ + BROTLI_STREAM_FLUSH_REQUESTED = 1, + /* Last metablock was produced; no more input is acceptable. */ + BROTLI_STREAM_FINISHED = 2, + /* Flushing compressed block and writing meta-data block header. */ + BROTLI_STREAM_METADATA_HEAD = 3, + /* Writing metadata block body. */ + BROTLI_STREAM_METADATA_BODY = 4 +} BrotliEncoderStreamState; + +typedef enum BrotliEncoderFlintState { + BROTLI_FLINT_NEEDS_2_BYTES = 2, + BROTLI_FLINT_NEEDS_1_BYTE = 1, + BROTLI_FLINT_WAITING_FOR_PROCESSING = 0, + BROTLI_FLINT_WAITING_FOR_FLUSHING = -1, + BROTLI_FLINT_DONE = -2 +} BrotliEncoderFlintState; + +typedef struct BrotliEncoderStateStruct { + BrotliEncoderParams params; + + MemoryManager memory_manager_; + + uint64_t input_pos_; + RingBuffer ringbuffer_; + size_t cmd_alloc_size_; + Command* commands_; + size_t num_commands_; + size_t num_literals_; + size_t last_insert_len_; + uint64_t last_flush_pos_; + uint64_t last_processed_pos_; + int dist_cache_[BROTLI_NUM_DISTANCE_SHORT_CODES]; + int saved_dist_cache_[4]; + uint16_t last_bytes_; + uint8_t last_bytes_bits_; + /* "Flint" is a tiny uncompressed block emitted before the continuation + block to unwire literal context from previous data. Despite being int8_t, + field is actually BrotliEncoderFlintState enum. */ + int8_t flint_; + uint8_t prev_byte_; + uint8_t prev_byte2_; + size_t storage_size_; + uint8_t* storage_; + + Hasher hasher_; + + /* Hash table for FAST_ONE_PASS_COMPRESSION_QUALITY mode. */ + int small_table_[1 << 10]; /* 4KiB */ + int* large_table_; /* Allocated only when needed */ + size_t large_table_size_; + + BrotliOnePassArena* one_pass_arena_; + BrotliTwoPassArena* two_pass_arena_; + + /* Command and literal buffers for FAST_TWO_PASS_COMPRESSION_QUALITY. */ + uint32_t* command_buf_; + uint8_t* literal_buf_; + + uint64_t total_in_; + uint8_t* next_out_; + size_t available_out_; + uint64_t total_out_; + /* Temporary buffer for padding flush bits or metadata block header / body. */ + union { + uint64_t u64[2]; + uint8_t u8[16]; + } tiny_buf_; + uint32_t remaining_metadata_bytes_; + BrotliEncoderStreamState stream_state_; + + BROTLI_BOOL is_last_block_emitted_; + BROTLI_BOOL is_initialized_; +} BrotliEncoderStateStruct; + +typedef struct BrotliEncoderStateStruct BrotliEncoderStateInternal; +#define BrotliEncoderState BrotliEncoderStateInternal + +#endif // BROTLI_ENC_STATE_H_ diff --git a/packages/runtime/vendor/brotli/c/enc/static_dict.c b/packages/runtime/vendor/brotli/c/enc/static_dict.c new file mode 100644 index 000000000..854674600 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/static_dict.c @@ -0,0 +1,536 @@ +/* Copyright 2013 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +#include "static_dict.h" + +#include "../common/dictionary.h" +#include "../common/platform.h" +#include "../common/transform.h" +#include "encoder_dict.h" +#include "find_match_length.h" +#include "hash_base.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +static BROTLI_INLINE void AddMatch(size_t distance, size_t len, size_t len_code, + uint32_t* matches) { + uint32_t match = (uint32_t)((distance << 5) + len_code); + matches[len] = BROTLI_MIN(uint32_t, matches[len], match); +} + +static BROTLI_INLINE size_t DictMatchLength(const BrotliDictionary* dictionary, + const uint8_t* data, + size_t id, + size_t len, + size_t maxlen) { + const size_t offset = dictionary->offsets_by_length[len] + len * id; + return FindMatchLengthWithLimit(&dictionary->data[offset], data, + BROTLI_MIN(size_t, len, maxlen)); +} + +static BROTLI_INLINE BROTLI_BOOL IsMatch(const BrotliDictionary* dictionary, + DictWord w, const uint8_t* data, size_t max_length) { + if (w.len > max_length) { + return BROTLI_FALSE; + } else { + const size_t offset = dictionary->offsets_by_length[w.len] + + (size_t)w.len * (size_t)w.idx; + const uint8_t* dict = &dictionary->data[offset]; + if (w.transform == 0) { + /* Match against base dictionary word. */ + return + TO_BROTLI_BOOL(FindMatchLengthWithLimit(dict, data, w.len) == w.len); + } else if (w.transform == 10) { + /* Match against uppercase first transform. + Note that there are only ASCII uppercase words in the lookup table. */ + return TO_BROTLI_BOOL(dict[0] >= 'a' && dict[0] <= 'z' && + (dict[0] ^ 32) == data[0] && + FindMatchLengthWithLimit(&dict[1], &data[1], w.len - 1u) == + w.len - 1u); + } else { + /* Match against uppercase all transform. + Note that there are only ASCII uppercase words in the lookup table. */ + size_t i; + for (i = 0; i < w.len; ++i) { + if (dict[i] >= 'a' && dict[i] <= 'z') { + if ((dict[i] ^ 32) != data[i]) return BROTLI_FALSE; + } else { + if (dict[i] != data[i]) return BROTLI_FALSE; + } + } + return BROTLI_TRUE; + } + } +} + +/* Finds matches for a single static dictionary */ +static BROTLI_BOOL BrotliFindAllStaticDictionaryMatchesFor( + const BrotliEncoderDictionary* dictionary, const uint8_t* data, + size_t min_length, size_t max_length, uint32_t* matches) { + BROTLI_BOOL has_found_match = BROTLI_FALSE; +#if defined(BROTLI_EXPERIMENTAL) + if (dictionary->has_words_heavy) { + const BrotliTrieNode* node = &dictionary->trie.root; + size_t l = 0; + while (node && l < max_length) { + uint8_t c; + if (l >= min_length && node->len_) { + AddMatch(node->idx_, l, node->len_, matches); + has_found_match = BROTLI_TRUE; + } + c = data[l++]; + node = BrotliTrieSub(&dictionary->trie, node, c); + } + return has_found_match; + } +#endif /* BROTLI_EXPERIMENTAL */ + { + size_t offset = dictionary->buckets[Hash15(data)]; + BROTLI_BOOL end = !offset; + while (!end) { + DictWord w = dictionary->dict_words[offset++]; + const size_t l = w.len & 0x1F; + const size_t n = (size_t)1 << dictionary->words->size_bits_by_length[l]; + const size_t id = w.idx; + end = !!(w.len & 0x80); + w.len = (uint8_t)l; + if (w.transform == 0) { + const size_t matchlen = + DictMatchLength(dictionary->words, data, id, l, max_length); + const uint8_t* s; + size_t minlen; + size_t maxlen; + size_t len; + /* Transform "" + BROTLI_TRANSFORM_IDENTITY + "" */ + if (matchlen == l) { + AddMatch(id, l, l, matches); + has_found_match = BROTLI_TRUE; + } + /* Transforms "" + BROTLI_TRANSFORM_OMIT_LAST_1 + "" and + "" + BROTLI_TRANSFORM_OMIT_LAST_1 + "ing " */ + if (matchlen >= l - 1) { + AddMatch(id + 12 * n, l - 1, l, matches); + if (l + 2 < max_length && + data[l - 1] == 'i' && data[l] == 'n' && data[l + 1] == 'g' && + data[l + 2] == ' ') { + AddMatch(id + 49 * n, l + 3, l, matches); + } + has_found_match = BROTLI_TRUE; + } + /* Transform "" + BROTLI_TRANSFORM_OMIT_LAST_# + "" (# = 2 .. 9) */ + minlen = min_length; + if (l > 9) minlen = BROTLI_MAX(size_t, minlen, l - 9); + maxlen = BROTLI_MIN(size_t, matchlen, l - 2); + for (len = minlen; len <= maxlen; ++len) { + size_t cut = l - len; + size_t transform_id = (cut << 2) + + (size_t)((dictionary->cutoffTransforms >> (cut * 6)) & 0x3F); + AddMatch(id + transform_id * n, len, l, matches); + has_found_match = BROTLI_TRUE; + } + if (matchlen < l || l + 6 >= max_length) { + continue; + } + s = &data[l]; + /* Transforms "" + BROTLI_TRANSFORM_IDENTITY + */ + if (s[0] == ' ') { + AddMatch(id + n, l + 1, l, matches); + if (s[1] == 'a') { + if (s[2] == ' ') { + AddMatch(id + 28 * n, l + 3, l, matches); + } else if (s[2] == 's') { + if (s[3] == ' ') AddMatch(id + 46 * n, l + 4, l, matches); + } else if (s[2] == 't') { + if (s[3] == ' ') AddMatch(id + 60 * n, l + 4, l, matches); + } else if (s[2] == 'n') { + if (s[3] == 'd' && s[4] == ' ') { + AddMatch(id + 10 * n, l + 5, l, matches); + } + } + } else if (s[1] == 'b') { + if (s[2] == 'y' && s[3] == ' ') { + AddMatch(id + 38 * n, l + 4, l, matches); + } + } else if (s[1] == 'i') { + if (s[2] == 'n') { + if (s[3] == ' ') AddMatch(id + 16 * n, l + 4, l, matches); + } else if (s[2] == 's') { + if (s[3] == ' ') AddMatch(id + 47 * n, l + 4, l, matches); + } + } else if (s[1] == 'f') { + if (s[2] == 'o') { + if (s[3] == 'r' && s[4] == ' ') { + AddMatch(id + 25 * n, l + 5, l, matches); + } + } else if (s[2] == 'r') { + if (s[3] == 'o' && s[4] == 'm' && s[5] == ' ') { + AddMatch(id + 37 * n, l + 6, l, matches); + } + } + } else if (s[1] == 'o') { + if (s[2] == 'f') { + if (s[3] == ' ') AddMatch(id + 8 * n, l + 4, l, matches); + } else if (s[2] == 'n') { + if (s[3] == ' ') AddMatch(id + 45 * n, l + 4, l, matches); + } + } else if (s[1] == 'n') { + if (s[2] == 'o' && s[3] == 't' && s[4] == ' ') { + AddMatch(id + 80 * n, l + 5, l, matches); + } + } else if (s[1] == 't') { + if (s[2] == 'h') { + if (s[3] == 'e') { + if (s[4] == ' ') AddMatch(id + 5 * n, l + 5, l, matches); + } else if (s[3] == 'a') { + if (s[4] == 't' && s[5] == ' ') { + AddMatch(id + 29 * n, l + 6, l, matches); + } + } + } else if (s[2] == 'o') { + if (s[3] == ' ') AddMatch(id + 17 * n, l + 4, l, matches); + } + } else if (s[1] == 'w') { + if (s[2] == 'i' && s[3] == 't' && s[4] == 'h' && s[5] == ' ') { + AddMatch(id + 35 * n, l + 6, l, matches); + } + } + } else if (s[0] == '"') { + AddMatch(id + 19 * n, l + 1, l, matches); + if (s[1] == '>') { + AddMatch(id + 21 * n, l + 2, l, matches); + } + } else if (s[0] == '.') { + AddMatch(id + 20 * n, l + 1, l, matches); + if (s[1] == ' ') { + AddMatch(id + 31 * n, l + 2, l, matches); + if (s[2] == 'T' && s[3] == 'h') { + if (s[4] == 'e') { + if (s[5] == ' ') AddMatch(id + 43 * n, l + 6, l, matches); + } else if (s[4] == 'i') { + if (s[5] == 's' && s[6] == ' ') { + AddMatch(id + 75 * n, l + 7, l, matches); + } + } + } + } + } else if (s[0] == ',') { + AddMatch(id + 76 * n, l + 1, l, matches); + if (s[1] == ' ') { + AddMatch(id + 14 * n, l + 2, l, matches); + } + } else if (s[0] == '\n') { + AddMatch(id + 22 * n, l + 1, l, matches); + if (s[1] == '\t') { + AddMatch(id + 50 * n, l + 2, l, matches); + } + } else if (s[0] == ']') { + AddMatch(id + 24 * n, l + 1, l, matches); + } else if (s[0] == '\'') { + AddMatch(id + 36 * n, l + 1, l, matches); + } else if (s[0] == ':') { + AddMatch(id + 51 * n, l + 1, l, matches); + } else if (s[0] == '(') { + AddMatch(id + 57 * n, l + 1, l, matches); + } else if (s[0] == '=') { + if (s[1] == '"') { + AddMatch(id + 70 * n, l + 2, l, matches); + } else if (s[1] == '\'') { + AddMatch(id + 86 * n, l + 2, l, matches); + } + } else if (s[0] == 'a') { + if (s[1] == 'l' && s[2] == ' ') { + AddMatch(id + 84 * n, l + 3, l, matches); + } + } else if (s[0] == 'e') { + if (s[1] == 'd') { + if (s[2] == ' ') AddMatch(id + 53 * n, l + 3, l, matches); + } else if (s[1] == 'r') { + if (s[2] == ' ') AddMatch(id + 82 * n, l + 3, l, matches); + } else if (s[1] == 's') { + if (s[2] == 't' && s[3] == ' ') { + AddMatch(id + 95 * n, l + 4, l, matches); + } + } + } else if (s[0] == 'f') { + if (s[1] == 'u' && s[2] == 'l' && s[3] == ' ') { + AddMatch(id + 90 * n, l + 4, l, matches); + } + } else if (s[0] == 'i') { + if (s[1] == 'v') { + if (s[2] == 'e' && s[3] == ' ') { + AddMatch(id + 92 * n, l + 4, l, matches); + } + } else if (s[1] == 'z') { + if (s[2] == 'e' && s[3] == ' ') { + AddMatch(id + 100 * n, l + 4, l, matches); + } + } + } else if (s[0] == 'l') { + if (s[1] == 'e') { + if (s[2] == 's' && s[3] == 's' && s[4] == ' ') { + AddMatch(id + 93 * n, l + 5, l, matches); + } + } else if (s[1] == 'y') { + if (s[2] == ' ') AddMatch(id + 61 * n, l + 3, l, matches); + } + } else if (s[0] == 'o') { + if (s[1] == 'u' && s[2] == 's' && s[3] == ' ') { + AddMatch(id + 106 * n, l + 4, l, matches); + } + } + } else { + /* Set is_all_caps=0 for BROTLI_TRANSFORM_UPPERCASE_FIRST and + is_all_caps=1 otherwise (BROTLI_TRANSFORM_UPPERCASE_ALL) + transform. */ + const BROTLI_BOOL is_all_caps = + TO_BROTLI_BOOL(w.transform != BROTLI_TRANSFORM_UPPERCASE_FIRST); + const uint8_t* s; + if (!IsMatch(dictionary->words, w, data, max_length)) { + continue; + } + /* Transform "" + kUppercase{First,All} + "" */ + AddMatch(id + (is_all_caps ? 44 : 9) * n, l, l, matches); + has_found_match = BROTLI_TRUE; + if (l + 1 >= max_length) { + continue; + } + /* Transforms "" + kUppercase{First,All} + */ + s = &data[l]; + if (s[0] == ' ') { + AddMatch(id + (is_all_caps ? 68 : 4) * n, l + 1, l, matches); + } else if (s[0] == '"') { + AddMatch(id + (is_all_caps ? 87 : 66) * n, l + 1, l, matches); + if (s[1] == '>') { + AddMatch(id + (is_all_caps ? 97 : 69) * n, l + 2, l, matches); + } + } else if (s[0] == '.') { + AddMatch(id + (is_all_caps ? 101 : 79) * n, l + 1, l, matches); + if (s[1] == ' ') { + AddMatch(id + (is_all_caps ? 114 : 88) * n, l + 2, l, matches); + } + } else if (s[0] == ',') { + AddMatch(id + (is_all_caps ? 112 : 99) * n, l + 1, l, matches); + if (s[1] == ' ') { + AddMatch(id + (is_all_caps ? 107 : 58) * n, l + 2, l, matches); + } + } else if (s[0] == '\'') { + AddMatch(id + (is_all_caps ? 94 : 74) * n, l + 1, l, matches); + } else if (s[0] == '(') { + AddMatch(id + (is_all_caps ? 113 : 78) * n, l + 1, l, matches); + } else if (s[0] == '=') { + if (s[1] == '"') { + AddMatch(id + (is_all_caps ? 105 : 104) * n, l + 2, l, matches); + } else if (s[1] == '\'') { + AddMatch(id + (is_all_caps ? 116 : 108) * n, l + 2, l, matches); + } + } + } + } + } + /* Transforms with prefixes " " and "." */ + if (max_length >= 5 && (data[0] == ' ' || data[0] == '.')) { + BROTLI_BOOL is_space = TO_BROTLI_BOOL(data[0] == ' '); + size_t offset = dictionary->buckets[Hash15(&data[1])]; + BROTLI_BOOL end = !offset; + while (!end) { + DictWord w = dictionary->dict_words[offset++]; + const size_t l = w.len & 0x1F; + const size_t n = (size_t)1 << dictionary->words->size_bits_by_length[l]; + const size_t id = w.idx; + end = !!(w.len & 0x80); + w.len = (uint8_t)l; + if (w.transform == 0) { + const uint8_t* s; + if (!IsMatch(dictionary->words, w, &data[1], max_length - 1)) { + continue; + } + /* Transforms " " + BROTLI_TRANSFORM_IDENTITY + "" and + "." + BROTLI_TRANSFORM_IDENTITY + "" */ + AddMatch(id + (is_space ? 6 : 32) * n, l + 1, l, matches); + has_found_match = BROTLI_TRUE; + if (l + 2 >= max_length) { + continue; + } + /* Transforms " " + BROTLI_TRANSFORM_IDENTITY + and + "." + BROTLI_TRANSFORM_IDENTITY + + */ + s = &data[l + 1]; + if (s[0] == ' ') { + AddMatch(id + (is_space ? 2 : 77) * n, l + 2, l, matches); + } else if (s[0] == '(') { + AddMatch(id + (is_space ? 89 : 67) * n, l + 2, l, matches); + } else if (is_space) { + if (s[0] == ',') { + AddMatch(id + 103 * n, l + 2, l, matches); + if (s[1] == ' ') { + AddMatch(id + 33 * n, l + 3, l, matches); + } + } else if (s[0] == '.') { + AddMatch(id + 71 * n, l + 2, l, matches); + if (s[1] == ' ') { + AddMatch(id + 52 * n, l + 3, l, matches); + } + } else if (s[0] == '=') { + if (s[1] == '"') { + AddMatch(id + 81 * n, l + 3, l, matches); + } else if (s[1] == '\'') { + AddMatch(id + 98 * n, l + 3, l, matches); + } + } + } + } else if (is_space) { + /* Set is_all_caps=0 for BROTLI_TRANSFORM_UPPERCASE_FIRST and + is_all_caps=1 otherwise (BROTLI_TRANSFORM_UPPERCASE_ALL) + transform. */ + const BROTLI_BOOL is_all_caps = + TO_BROTLI_BOOL(w.transform != BROTLI_TRANSFORM_UPPERCASE_FIRST); + const uint8_t* s; + if (!IsMatch(dictionary->words, w, &data[1], max_length - 1)) { + continue; + } + /* Transforms " " + kUppercase{First,All} + "" */ + AddMatch(id + (is_all_caps ? 85 : 30) * n, l + 1, l, matches); + has_found_match = BROTLI_TRUE; + if (l + 2 >= max_length) { + continue; + } + /* Transforms " " + kUppercase{First,All} + */ + s = &data[l + 1]; + if (s[0] == ' ') { + AddMatch(id + (is_all_caps ? 83 : 15) * n, l + 2, l, matches); + } else if (s[0] == ',') { + if (!is_all_caps) { + AddMatch(id + 109 * n, l + 2, l, matches); + } + if (s[1] == ' ') { + AddMatch(id + (is_all_caps ? 111 : 65) * n, l + 3, l, matches); + } + } else if (s[0] == '.') { + AddMatch(id + (is_all_caps ? 115 : 96) * n, l + 2, l, matches); + if (s[1] == ' ') { + AddMatch(id + (is_all_caps ? 117 : 91) * n, l + 3, l, matches); + } + } else if (s[0] == '=') { + if (s[1] == '"') { + AddMatch(id + (is_all_caps ? 110 : 118) * n, l + 3, l, matches); + } else if (s[1] == '\'') { + AddMatch(id + (is_all_caps ? 119 : 120) * n, l + 3, l, matches); + } + } + } + } + } + if (max_length >= 6) { + /* Transforms with prefixes "e ", "s ", ", " and "\xC2\xA0" */ + if ((data[1] == ' ' && + (data[0] == 'e' || data[0] == 's' || data[0] == ',')) || + (data[0] == 0xC2 && data[1] == 0xA0)) { + size_t offset = dictionary->buckets[Hash15(&data[2])]; + BROTLI_BOOL end = !offset; + while (!end) { + DictWord w = dictionary->dict_words[offset++]; + const size_t l = w.len & 0x1F; + const size_t n = (size_t)1 << dictionary->words->size_bits_by_length[l]; + const size_t id = w.idx; + end = !!(w.len & 0x80); + w.len = (uint8_t)l; + if (w.transform == 0 && + IsMatch(dictionary->words, w, &data[2], max_length - 2)) { + if (data[0] == 0xC2) { + AddMatch(id + 102 * n, l + 2, l, matches); + has_found_match = BROTLI_TRUE; + } else if (l + 2 < max_length && data[l + 2] == ' ') { + size_t t = data[0] == 'e' ? 18 : (data[0] == 's' ? 7 : 13); + AddMatch(id + t * n, l + 3, l, matches); + has_found_match = BROTLI_TRUE; + } + } + } + } + } + if (max_length >= 9) { + /* Transforms with prefixes " the " and ".com/" */ + if ((data[0] == ' ' && data[1] == 't' && data[2] == 'h' && + data[3] == 'e' && data[4] == ' ') || + (data[0] == '.' && data[1] == 'c' && data[2] == 'o' && + data[3] == 'm' && data[4] == '/')) { + size_t offset = dictionary->buckets[Hash15(&data[5])]; + BROTLI_BOOL end = !offset; + while (!end) { + DictWord w = dictionary->dict_words[offset++]; + const size_t l = w.len & 0x1F; + const size_t n = (size_t)1 << dictionary->words->size_bits_by_length[l]; + const size_t id = w.idx; + end = !!(w.len & 0x80); + w.len = (uint8_t)l; + if (w.transform == 0 && + IsMatch(dictionary->words, w, &data[5], max_length - 5)) { + AddMatch(id + (data[0] == ' ' ? 41 : 72) * n, l + 5, l, matches); + has_found_match = BROTLI_TRUE; + if (l + 5 < max_length) { + const uint8_t* s = &data[l + 5]; + if (data[0] == ' ') { + if (l + 8 < max_length && + s[0] == ' ' && s[1] == 'o' && s[2] == 'f' && s[3] == ' ') { + AddMatch(id + 62 * n, l + 9, l, matches); + if (l + 12 < max_length && + s[4] == 't' && s[5] == 'h' && s[6] == 'e' && s[7] == ' ') { + AddMatch(id + 73 * n, l + 13, l, matches); + } + } + } + } + } + } + } + } + return has_found_match; +} + +/* Finds matches for one or more dictionaries, if multiple are present + in the contextual dictionary */ +BROTLI_BOOL BrotliFindAllStaticDictionaryMatches( + const BrotliEncoderDictionary* dictionary, const uint8_t* data, + size_t min_length, size_t max_length, uint32_t* matches) { + BROTLI_BOOL has_found_match = + BrotliFindAllStaticDictionaryMatchesFor( + dictionary, data, min_length, max_length, matches); + + if (!!dictionary->parent && dictionary->parent->num_dictionaries > 1) { + uint32_t matches2[BROTLI_MAX_STATIC_DICTIONARY_MATCH_LEN + 1]; + int l; + const BrotliEncoderDictionary* dictionary2 = dictionary->parent->dict[0]; + if (dictionary2 == dictionary) { + dictionary2 = dictionary->parent->dict[1]; + } + + for (l = 0; l < BROTLI_MAX_STATIC_DICTIONARY_MATCH_LEN + 1; l++) { + matches2[l] = kInvalidMatch; + } + + has_found_match |= BrotliFindAllStaticDictionaryMatchesFor( + dictionary2, data, min_length, max_length, matches2); + + for (l = 0; l < BROTLI_MAX_STATIC_DICTIONARY_MATCH_LEN + 1; l++) { + if (matches2[l] != kInvalidMatch) { + uint32_t dist = (uint32_t)(matches2[l] >> 5); + uint32_t len_code = matches2[l] & 31; + uint32_t skipdist = (uint32_t)((uint32_t)(1 << dictionary->words-> + size_bits_by_length[len_code]) & ~1u) * + (uint32_t)dictionary->num_transforms; + /* TODO(lode): check for dist overflow */ + dist += skipdist; + AddMatch(dist, (size_t)l, len_code, matches); + } + } + } + return has_found_match; +} +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif diff --git a/packages/runtime/vendor/brotli/c/enc/static_dict.h b/packages/runtime/vendor/brotli/c/enc/static_dict.h new file mode 100644 index 000000000..bc69b1591 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/static_dict.h @@ -0,0 +1,39 @@ +/* Copyright 2013 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* Class to model the static dictionary. */ + +#ifndef BROTLI_ENC_STATIC_DICT_H_ +#define BROTLI_ENC_STATIC_DICT_H_ + +#include "../common/platform.h" + +#include "encoder_dict.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +#define BROTLI_MAX_STATIC_DICTIONARY_MATCH_LEN 37 +static const uint32_t kInvalidMatch = 0xFFFFFFF; + +/* Matches data against static dictionary words, and for each length l, + for which a match is found, updates matches[l] to be the minimum possible + (distance << 5) + len_code. + Returns 1 if matches have been found, otherwise 0. + Prerequisites: + matches array is at least BROTLI_MAX_STATIC_DICTIONARY_MATCH_LEN + 1 long + all elements are initialized to kInvalidMatch */ +BROTLI_INTERNAL BROTLI_BOOL BrotliFindAllStaticDictionaryMatches( + const BrotliEncoderDictionary* dictionary, + const uint8_t* data, size_t min_length, size_t max_length, + uint32_t* matches); + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif + +#endif /* BROTLI_ENC_STATIC_DICT_H_ */ diff --git a/packages/runtime/vendor/brotli/c/enc/static_dict_lut.c b/packages/runtime/vendor/brotli/c/enc/static_dict_lut.c new file mode 100644 index 000000000..109366f60 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/static_dict_lut.c @@ -0,0 +1,224 @@ +/* Copyright 2025 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* Lookup table for static dictionary and transforms. */ + +#include "static_dict_lut.h" + +#include "../common/platform.h" /* IWYU pragma: keep */ +#include "../common/static_init.h" + +#if (BROTLI_STATIC_INIT != BROTLI_STATIC_INIT_NONE) +#include "../common/dictionary.h" +#include "../common/transform.h" +#include "hash_base.h" +#endif + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +#if (BROTLI_STATIC_INIT != BROTLI_STATIC_INIT_NONE) + +/* TODO(eustas): deal with largest bucket(s). Not it contains 163 items. */ +static BROTLI_BOOL BROTLI_COLD DoBrotliEncoderInitStaticDictionaryLut( + const BrotliDictionary* dict, uint16_t* buckets, DictWord* words, + void* arena) { + DictWord* slots = (DictWord*)arena; + uint16_t* heads = (uint16_t*)(slots + BROTLI_ENC_STATIC_DICT_LUT_NUM_ITEMS); + uint16_t* counts = heads + BROTLI_ENC_STATIC_DICT_LUT_NUM_BUCKETS; + uint16_t* prev = counts + BROTLI_ENC_STATIC_DICT_LUT_NUM_BUCKETS; + size_t next_slot = 0; + uint8_t transformed_word[24]; + uint8_t transformed_other_word[24]; + size_t l; + size_t pos; + size_t i; + + memset(counts, 0, BROTLI_ENC_STATIC_DICT_LUT_NUM_BUCKETS * sizeof(uint16_t)); + memset(heads, 0, BROTLI_ENC_STATIC_DICT_LUT_NUM_BUCKETS * sizeof(uint16_t)); + memset(prev, 0, BROTLI_ENC_STATIC_DICT_LUT_NUM_ITEMS * sizeof(uint16_t)); + + for (l = 4; l <= 24; ++l) { + size_t n = 1u << dict->size_bits_by_length[l]; + const uint8_t* dict_words = dict->data + dict->offsets_by_length[l]; + for (i = 0; i < n; ++i) { + const uint8_t* dict_word = dict_words + l * i; + uint32_t key = Hash15(dict_word); + slots[next_slot].len = (uint8_t)l; + slots[next_slot].transform = BROTLI_TRANSFORM_IDENTITY; + slots[next_slot].idx = (uint16_t)i; + prev[next_slot] = heads[key]; + heads[key] = (uint16_t)next_slot; + counts[key]++; + ++next_slot; + } + for (i = 0; i < n; ++i) { + uint32_t key; + uint32_t prefix; + BROTLI_BOOL found; + size_t curr; + const uint8_t* dict_word = dict_words + l * i; + if (dict_word[0] < 'a' || dict_word[0] > 'z') continue; + memcpy(transformed_word, dict_word, l); + transformed_word[0] = transformed_word[0] - 32; + key = Hash15(transformed_word); + prefix = BROTLI_UNALIGNED_LOAD32LE(transformed_word) & ~0x20202020u; + found = BROTLI_FALSE; + curr = heads[key]; + while (curr != 0) { + const uint8_t* other_word; + uint32_t other_prefix; + if (slots[curr].len != l) break; + other_word = dict_words + l * slots[curr].idx; + other_prefix = BROTLI_UNALIGNED_LOAD32LE(other_word) & ~0x20202020u; + if (prefix == other_prefix) { + if (memcmp(transformed_word, other_word, l) == 0) { + found = BROTLI_TRUE; + break; + } + } + curr = prev[curr]; + } + if (found) continue; + slots[next_slot].len = (uint8_t)l; + slots[next_slot].transform = BROTLI_TRANSFORM_UPPERCASE_FIRST; + slots[next_slot].idx = (uint16_t)i; + prev[next_slot] = heads[key]; + heads[key] = (uint16_t)next_slot; + counts[key]++; + ++next_slot; + } + for (i = 0; i < n; ++i) { + const uint8_t* dict_word = dict_words + l * i; + BROTLI_BOOL is_ascii = BROTLI_TRUE; + BROTLI_BOOL has_lower = BROTLI_FALSE; + size_t k; + uint32_t prefix; + uint32_t key; + size_t curr; + BROTLI_BOOL found; + for (k = 0; k < l; ++k) { + if (dict_word[k] >= 128) is_ascii = BROTLI_FALSE; + if (k > 0 && dict_word[k] >= 'a' && dict_word[k] <= 'z') + has_lower = BROTLI_TRUE; + } + if (!is_ascii || !has_lower) continue; + memcpy(transformed_word, dict_word, l); + prefix = BROTLI_UNALIGNED_LOAD32LE(transformed_word) & ~0x20202020u; + for (k = 0; k < l; ++k) { + if (transformed_word[k] >= 'a' && transformed_word[k] <= 'z') { + transformed_word[k] = transformed_word[k] - 32; + } + } + key = Hash15(transformed_word); + found = BROTLI_FALSE; + curr = heads[key]; + while (curr != 0) { + const uint8_t* other_word; + uint32_t other_prefix; + if (slots[curr].len != l) break; + other_word = dict_words + l * slots[curr].idx; + other_prefix = BROTLI_UNALIGNED_LOAD32LE(other_word) & ~0x20202020u; + if (prefix == other_prefix) { + if (slots[curr].transform == BROTLI_TRANSFORM_IDENTITY) { + if (memcmp(transformed_word, other_word, l) == 0) { + found = BROTLI_TRUE; + break; + } + } else if (slots[curr].transform == + BROTLI_TRANSFORM_UPPERCASE_FIRST) { + if ((transformed_word[0] == (other_word[0] - 32)) && + memcmp(transformed_word + 1, other_word + 1, l - 1) == 0) { + found = BROTLI_TRUE; + break; + } + } else { + for (k = 0; k < l; ++k) { + if (other_word[k] >= 'a' && other_word[k] <= 'z') { + transformed_other_word[k] = other_word[k] - 32; + } else { + transformed_other_word[k] = other_word[k]; + } + } + if (memcmp(transformed_word, transformed_other_word, l) == 0) { + found = BROTLI_TRUE; + break; + } + } + } + curr = prev[curr]; + } + if (found) { + continue; + } + slots[next_slot].len = (uint8_t)l; + slots[next_slot].transform = BROTLI_TRANSFORM_UPPERCASE_ALL; + slots[next_slot].idx = (uint16_t)i; + prev[next_slot] = heads[key]; + heads[key] = (uint16_t)next_slot; + counts[key]++; + ++next_slot; + } + } + + if (next_slot != 31704) return BROTLI_FALSE; + pos = 0; + /* Unused; makes offsets start from 1. */ + words[pos].len = 0; + words[pos].transform = 0; + words[pos].idx = 0; + pos++; + for (i = 0; i < BROTLI_ENC_STATIC_DICT_LUT_NUM_BUCKETS; ++i) { + size_t num_words = counts[i]; + size_t curr; + if (num_words == 0) { + buckets[i] = 0; + continue; + } + buckets[i] = (uint16_t)pos; + curr = heads[i]; + pos += num_words; + for (size_t k = 0; k < num_words; ++k) { + words[pos - 1 - k] = slots[curr]; + curr = prev[curr]; + } + words[pos - 1].len |= 0x80; + } + return BROTLI_TRUE; +} + +BROTLI_BOOL BrotliEncoderInitStaticDictionaryLut( + const BrotliDictionary* dict, uint16_t* buckets, DictWord* words) { + size_t arena_size = + BROTLI_ENC_STATIC_DICT_LUT_NUM_ITEMS * + (sizeof(uint16_t) + sizeof(DictWord)) + + BROTLI_ENC_STATIC_DICT_LUT_NUM_BUCKETS * 2 * sizeof(uint16_t); + void* arena = malloc(arena_size); + BROTLI_BOOL ok; + if (arena == NULL) { + return BROTLI_FALSE; + } + ok = DoBrotliEncoderInitStaticDictionaryLut(dict, buckets, words, arena); + free(arena); + return ok; +} + +BROTLI_MODEL("small") +uint16_t kStaticDictionaryBuckets[BROTLI_ENC_STATIC_DICT_LUT_NUM_BUCKETS]; +BROTLI_MODEL("small") +DictWord kStaticDictionaryWords[BROTLI_ENC_STATIC_DICT_LUT_NUM_ITEMS]; + +#else /* BROTLI_STATIC_INIT */ + +/* Embed kStaticDictionaryBuckets and kStaticDictionaryWords. */ +#include "static_dict_lut_inc.h" + +#endif /* BROTLI_STATIC_INIT */ + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif diff --git a/packages/runtime/vendor/brotli/c/enc/static_dict_lut.h b/packages/runtime/vendor/brotli/c/enc/static_dict_lut.h new file mode 100644 index 000000000..078f55424 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/static_dict_lut.h @@ -0,0 +1,49 @@ +/* Copyright 2015 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* Lookup table for static dictionary and transforms. */ + +#ifndef BROTLI_ENC_STATIC_DICT_LUT_H_ +#define BROTLI_ENC_STATIC_DICT_LUT_H_ + +#include "../common/dictionary.h" +#include "../common/platform.h" +#include "../common/static_init.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +typedef struct DictWord { + /* Highest bit is used to indicate end of bucket. */ + uint8_t len; + uint8_t transform; + uint16_t idx; +} DictWord; + +/* Buckets are Hash15 results. */ +#define BROTLI_ENC_STATIC_DICT_LUT_NUM_BUCKETS 32768 +#define BROTLI_ENC_STATIC_DICT_LUT_NUM_ITEMS 31705 + +#if (BROTLI_STATIC_INIT != BROTLI_STATIC_INIT_NONE) +BROTLI_INTERNAL BROTLI_BOOL BrotliEncoderInitStaticDictionaryLut( + const BrotliDictionary* dictionary, uint16_t* buckets, DictWord* words); +BROTLI_INTERNAL extern BROTLI_MODEL("small") uint16_t + kStaticDictionaryBuckets[BROTLI_ENC_STATIC_DICT_LUT_NUM_BUCKETS]; +BROTLI_INTERNAL extern BROTLI_MODEL("small") DictWord + kStaticDictionaryWords[BROTLI_ENC_STATIC_DICT_LUT_NUM_ITEMS]; +#else +BROTLI_INTERNAL extern const BROTLI_MODEL("small") uint16_t + kStaticDictionaryBuckets[BROTLI_ENC_STATIC_DICT_LUT_NUM_BUCKETS]; +BROTLI_INTERNAL extern const BROTLI_MODEL("small") DictWord + kStaticDictionaryWords[BROTLI_ENC_STATIC_DICT_LUT_NUM_ITEMS]; +#endif + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif + +#endif /* BROTLI_ENC_STATIC_DICT_LUT_H_ */ diff --git a/packages/runtime/vendor/brotli/c/enc/static_dict_lut_inc.h b/packages/runtime/vendor/brotli/c/enc/static_dict_lut_inc.h new file mode 100644 index 000000000..fea97f8d7 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/static_dict_lut_inc.h @@ -0,0 +1,5830 @@ +const BROTLI_MODEL("small") +uint16_t kStaticDictionaryBuckets[BROTLI_ENC_STATIC_DICT_LUT_NUM_BUCKETS] = { +1,0,0,0,0,0,0,0,0,3,6,0,0,0,0,0,20,0,0,0,21,0,22,0,0,0,0,0,0,0,0,23,0,0,25,0,29, +0,53,0,0,0,0,0,0,55,0,0,0,0,0,0,61,76,0,0,0,94,0,0,0,0,0,0,96,0,97,0,98,0,0,0,0, +0,0,0,99,101,106,108,0,0,0,0,0,110,0,111,112,0,113,118,124,0,0,0,0,0,125,128,0,0 +,0,0,129,0,0,131,0,0,0,0,0,0,132,0,0,135,0,0,0,137,0,0,0,0,0,138,139,0,0,0,0,0,0 +,0,142,143,144,0,0,0,0,0,145,0,0,0,146,149,151,152,0,0,153,0,0,0,0,0,0,0,0,0,0,0 +,0,0,0,0,154,0,0,0,0,0,0,155,0,0,0,0,160,182,0,0,0,0,0,0,183,0,0,0,188,189,0,0, +192,0,0,0,0,0,0,194,0,0,0,0,0,0,0,0,197,202,209,0,0,210,0,224,0,0,0,225,0,0,0,0, +0,0,0,0,0,0,231,0,0,0,232,0,240,0,0,242,0,0,0,0,0,0,0,0,0,0,0,244,0,0,0,246,0,0, +249,251,253,0,0,0,0,0,258,0,0,261,263,0,0,0,267,0,0,268,0,269,0,0,0,0,0,0,0,0,0, +271,0,0,0,0,0,0,272,0,273,0,277,0,278,286,0,0,0,0,287,0,289,290,291,0,0,0,295,0, +0,296,297,0,0,0,0,0,0,0,0,0,0,298,0,0,0,299,0,0,305,0,324,0,0,0,0,0,327,0,328, +329,0,0,0,0,336,0,0,340,0,341,342,343,0,0,346,0,348,0,0,0,0,0,0,349,351,0,0,355, +0,363,0,364,0,368,369,0,370,0,0,0,0,0,0,0,372,0,0,0,0,0,0,0,0,0,0,0,373,0,375,0, +0,0,0,376,377,0,0,394,395,396,0,0,398,0,0,0,0,400,0,0,408,0,0,0,0,420,0,0,0,0,0, +0,421,0,0,422,423,0,0,429,435,436,442,0,0,443,0,444,445,453,456,0,457,0,0,0,0,0, +458,0,0,0,459,0,0,0,460,0,462,463,465,0,0,0,0,0,0,466,469,0,0,0,0,0,0,470,0,0,0, +474,0,476,0,0,0,0,483,0,485,0,0,0,486,0,0,488,491,492,0,0,497,499,500,0,501,0,0, +0,505,0,0,506,0,0,0,507,0,0,0,509,0,0,0,0,511,512,519,0,0,0,0,0,0,529,530,0,0,0, +534,0,0,0,0,543,0,0,0,0,0,0,0,0,0,553,0,0,0,0,557,560,0,0,0,0,0,0,561,0,564,0,0, +0,0,0,0,565,566,0,575,0,619,0,620,0,0,623,624,0,0,0,625,0,0,626,627,0,0,628,0,0, +0,0,630,0,631,0,0,0,0,0,0,0,0,0,641,0,0,0,0,643,656,668,0,0,0,673,0,0,0,674,0,0, +0,0,0,0,0,0,682,0,687,0,690,0,693,699,700,0,0,0,0,0,0,704,705,0,0,0,0,707,710,0, +711,0,0,0,0,726,0,0,729,0,0,0,730,731,0,0,0,0,0,752,0,0,0,762,0,763,0,0,767,0,0, +0,770,774,0,0,775,0,0,0,0,0,0,0,0,0,0,776,0,0,0,777,783,0,0,0,785,788,0,0,0,0, +790,0,0,0,793,0,0,0,0,794,0,0,804,819,821,0,827,0,0,0,834,0,0,835,0,0,0,841,0, +844,0,850,851,859,0,860,0,0,0,0,0,0,0,874,0,876,0,877,890,0,0,0,0,0,0,0,0,893, +894,898,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,899,0,0,0,900,904,906,0,0,0,907,0,908,909, +0,910,0,0,0,0,911,0,0,0,0,0,916,0,0,0,922,925,0,930,0,934,0,0,0,0,0,943,0,0,944, +0,953,954,0,0,0,0,0,0,955,0,962,963,0,0,976,0,0,977,978,979,980,0,981,0,0,0,0, +984,0,0,985,0,0,987,989,991,0,0,0,0,0,0,0,0,0,992,0,0,0,993,0,0,0,0,0,0,996,0,0, +0,1000,0,0,0,0,0,1002,0,0,0,0,1005,1007,0,0,0,1009,0,0,0,1010,0,0,0,0,0,0,1011,0 +,1012,0,0,0,0,1014,1016,0,0,0,1020,0,1021,0,0,0,0,1022,0,0,0,1024,0,0,0,0,0,0, +1025,0,0,1026,1027,0,0,0,0,0,1031,0,1033,0,0,0,0,1034,0,0,0,1037,1040,0,0,0,1042 +,1043,0,0,1053,0,1054,0,0,1057,0,0,0,1058,0,0,1060,0,0,0,0,0,0,0,1061,0,0,1062,0 +,0,0,0,1063,0,0,0,0,1064,0,0,0,0,0,1065,0,0,0,0,1066,1067,0,0,0,1069,1070,1072,0 +,0,0,0,0,0,1073,0,1075,0,0,0,0,0,0,1080,1084,0,0,0,0,1088,0,0,0,0,0,0,1094,0, +1095,0,1107,0,0,0,1112,1114,0,1119,0,1122,0,0,1126,0,1129,0,1130,0,0,0,0,0,1132, +0,0,0,0,0,0,1144,0,0,1145,1146,0,1148,1149,0,0,1150,1151,0,0,0,0,1152,0,1153,0,0 +,0,0,0,1154,0,1163,0,0,0,1164,0,0,0,0,0,1165,0,1167,0,1170,0,0,0,0,0,1171,1172,0 +,0,0,0,0,0,0,0,1173,1175,1177,0,1186,0,0,0,0,0,0,0,0,0,0,1195,0,0,1221,0,0,1224, +0,0,1227,0,0,0,0,0,1228,1229,0,0,1230,0,0,0,0,0,0,0,0,0,1231,0,0,0,1233,0,0,1243 +,1244,1246,1248,0,0,0,0,1254,1255,1258,1259,0,0,0,1260,0,0,1261,0,0,0,1262,1264, +0,0,1265,0,0,0,0,0,0,0,0,0,0,0,0,1266,0,1267,0,0,0,0,1273,1274,1276,1289,0,0, +1291,1292,1293,0,0,1294,1295,1296,0,0,0,0,1302,0,1304,0,0,0,0,0,0,0,0,0,1311, +1312,0,1314,0,1316,1320,1321,0,0,0,0,0,0,0,1322,1323,1324,0,1335,0,1336,0,0,0,0, +1341,1342,0,1346,0,1357,0,0,0,1358,1360,0,0,0,0,0,0,1361,0,0,0,1362,1365,0,1366, +0,0,0,0,0,0,0,1379,0,0,0,0,0,0,0,0,0,0,0,0,1386,0,1388,0,0,0,0,0,0,0,0,0,0,0,0,0 +,0,1395,0,0,0,0,1403,0,1405,0,0,1407,0,0,0,0,0,1408,1409,0,1410,0,0,0,1412,1413, +1416,0,0,1429,1451,0,0,1454,0,0,0,0,0,0,0,1455,0,0,0,0,0,0,0,1456,0,0,0,0,1459, +1460,1461,1475,0,0,0,0,0,0,1477,0,1480,0,1481,0,0,1486,0,0,1495,0,0,0,1496,0,0, +1498,1499,1501,1520,1521,0,0,0,1526,0,0,0,0,1528,1529,0,1533,1536,0,0,0,1537, +1538,1549,0,1550,1558,1559,1572,0,1573,0,0,0,0,0,0,0,0,0,1575,0,0,0,0,0,1579,0, +1599,0,1603,0,1604,0,1605,0,0,0,0,0,1608,1610,0,0,0,0,1611,0,1615,0,1616,1618,0, +1619,0,0,1622,0,0,0,0,1634,0,0,0,1635,0,0,0,1641,0,0,0,0,0,0,0,0,0,1643,0,0,0, +1650,0,0,1652,0,0,0,0,0,1653,0,0,0,1654,0,0,0,0,1655,0,1662,0,0,1663,1664,0,0, +1668,0,0,1669,1670,0,1672,1673,0,0,0,0,0,1674,0,0,0,1675,1676,1680,0,1682,0,0, +1687,0,0,0,0,0,1704,0,0,1705,0,0,1721,0,0,0,0,1734,1735,0,0,0,0,1737,0,0,0,0, +1739,0,0,1740,0,0,0,0,0,0,0,0,0,0,1741,1743,0,0,0,0,1745,0,0,0,1749,0,0,0,1751,0 +,0,0,0,0,0,1760,0,0,0,0,1765,0,0,0,0,0,1784,0,1785,1787,0,0,0,0,1788,1789,0,0,0, +0,1790,1791,1793,0,1798,1799,0,0,0,0,1801,0,1803,1805,0,0,0,1806,1811,0,1812, +1814,0,1821,0,0,0,0,0,1822,1833,0,0,0,0,0,0,1848,0,0,0,0,0,0,1857,0,0,0,1859,0,0 +,0,0,1861,0,0,0,0,0,0,0,1866,0,1921,1925,0,0,0,1929,1930,0,0,0,0,0,0,0,0,0,1931, +0,0,0,0,1932,0,0,0,1934,0,0,0,0,0,0,0,0,1946,0,0,1948,0,0,0,0,1950,0,1957,0,1958 +,0,0,0,0,0,1965,1967,0,0,0,0,1968,0,1969,0,1971,1972,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +,0,1973,0,0,0,0,1975,0,0,0,0,1976,1979,0,1982,0,0,0,0,1984,1988,0,0,0,0,1990, +2004,2008,0,0,0,2012,2013,0,0,0,0,0,0,0,0,0,0,2015,0,2016,2017,0,0,0,0,2021,0,0, +2025,0,0,0,0,0,2029,2036,2040,0,2042,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2043,0,0,0,0,0, +2045,0,0,0,0,0,0,0,2046,2047,0,2048,2049,0,2059,0,0,2063,0,2064,2065,0,0,2066,0, +0,0,0,0,0,2069,0,0,0,0,2070,0,2071,0,2072,0,0,0,0,2080,2082,2083,0,0,0,0,0,2085, +0,2086,2088,2089,2105,0,0,0,0,2107,0,0,2116,2117,0,2120,0,0,2122,0,0,0,0,0,2123, +0,0,2125,2127,2128,0,0,0,2130,0,0,0,2137,2139,2140,2141,0,0,0,0,0,0,0,0,0,2144, +2145,0,0,2146,2149,0,0,0,0,2150,0,0,2151,2158,0,2159,0,2160,0,0,0,0,0,0,2161, +2162,0,0,2194,2202,0,0,0,0,0,0,2205,2217,0,2220,0,2221,0,2222,2224,0,0,0,0,2237, +0,0,0,0,0,2238,0,2239,2241,0,0,2242,0,0,0,0,0,2243,0,0,0,0,0,0,2252,0,0,2253,0,0 +,0,2257,2258,0,0,0,2260,0,0,0,0,0,0,0,2262,0,2264,0,0,0,0,0,2269,2270,0,0,0,0,0, +0,0,0,0,2271,0,2273,0,0,0,0,2277,0,0,0,0,2278,0,0,0,0,2279,0,2280,0,2283,0,0,0,0 +,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2287,0,0,0,0,0,0,0,2289,2290,0,0,0,0,2291,0,2292,0, +0,0,2293,2295,2296,0,0,0,0,0,0,0,2298,0,0,0,0,0,2303,0,2305,0,0,2306,0,2307,0,0, +0,0,0,0,0,0,0,0,0,0,2313,2314,2315,2316,0,0,2318,0,2319,0,2322,0,0,2323,0,2324,0 +,2326,0,0,0,0,0,0,0,2335,0,2336,2338,2339,0,2340,0,0,0,2355,0,2375,0,2382,2386,0 +,2387,0,0,2394,0,0,0,0,2395,0,2397,0,0,0,0,0,2398,0,0,0,0,0,0,0,2399,2402,2404, +2408,2411,0,0,0,2413,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2415,0,0,2416,2417,2419,0,2420, +0,0,0,0,0,2425,0,0,0,2426,0,0,0,0,0,0,0,0,0,0,0,0,2427,2428,0,2429,0,0,2430,2434 +,0,2436,0,0,0,0,0,0,2441,2442,0,2445,0,0,2446,2457,0,2459,0,0,2462,0,2464,0,2477 +,0,2478,2486,0,0,0,2491,0,0,2493,0,0,2494,0,2495,0,2513,2523,0,0,0,0,2524,0,0,0, +0,0,0,2528,2529,2530,0,0,2531,0,2533,0,0,2534,2535,0,2536,2537,0,2538,0,2539, +2540,0,0,0,2545,2546,0,0,0,0,0,0,0,2548,0,0,2549,0,2550,2555,0,0,0,0,0,2557,0, +2560,0,0,0,0,0,0,0,0,0,0,0,2561,0,2576,0,0,0,0,0,0,0,0,0,2577,2578,0,0,0,2579,0, +0,0,0,0,0,0,2580,0,0,0,0,2581,0,0,0,0,2583,0,2584,0,2588,2590,0,0,0,2591,0,0,0,0 +,2593,2594,0,2595,0,2601,2602,0,0,2603,0,2605,0,0,0,2606,2607,2611,0,2615,0,0,0, +2617,0,0,0,0,0,0,0,0,0,0,0,0,0,2619,0,0,2620,0,0,0,2621,0,2623,0,2625,0,0,2628, +2629,0,0,2635,2636,2637,0,0,2639,0,0,0,2642,0,0,0,0,2643,0,2644,0,2649,0,0,0,0,0 +,0,2655,2656,0,0,2657,0,0,0,0,0,2658,0,0,0,0,0,2659,0,0,0,0,2664,2685,0,2687,0, +2688,0,0,2689,0,0,2694,0,2695,0,0,2698,0,2701,2706,0,0,0,2707,0,2709,2710,2711,0 +,0,0,2720,2730,2735,0,0,0,0,2738,2740,0,0,0,0,2747,0,0,0,0,0,0,2748,0,0,2749,0,0 +,0,0,0,2750,0,0,2752,2754,0,0,0,0,0,2758,0,0,0,0,2762,0,0,0,0,2763,0,0,0,0,0,0,0 +,2764,2767,0,0,0,0,2768,0,0,2770,0,0,0,0,0,0,0,2771,0,0,0,0,0,0,0,0,0,2772,0,0,0 +,0,0,2773,2776,0,0,2783,0,0,2784,0,2789,0,2790,0,0,0,2792,0,0,0,0,0,0,0,0,0,0, +2793,2795,0,0,0,0,0,0,2796,0,0,0,0,0,0,2797,2799,0,0,0,0,2803,0,0,0,0,2806,0, +2807,2808,2817,2819,0,0,0,0,0,2821,0,0,0,0,2822,2823,0,0,0,0,0,0,0,2824,0,0,2828 +,0,2834,0,0,0,0,0,0,2836,0,2838,0,0,2839,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2841, +0,0,0,2842,0,0,0,0,0,2843,2844,0,0,0,0,2846,0,0,2847,0,2849,0,2853,0,0,0,0,0, +2857,0,0,0,0,2858,0,2859,0,0,2860,0,2862,2868,0,0,0,0,2875,0,2876,0,0,2877,2878, +2884,2889,2890,0,0,2891,0,0,2892,0,0,0,2906,2912,0,2913,0,0,0,0,0,0,0,0,2916,0, +2934,0,0,0,0,0,2935,0,0,0,0,2939,0,2940,0,0,0,0,0,0,0,2941,0,0,0,2946,0,2949,0,0 +,2950,2954,2955,0,0,0,2959,2961,0,0,2962,0,2963,0,0,0,0,0,0,2964,2965,2966,2967, +0,0,0,0,0,0,0,2969,0,0,0,0,0,2970,2975,0,2982,2983,2984,0,0,0,0,0,2989,0,0,2990, +0,0,0,0,0,0,0,2991,0,0,0,0,0,0,0,0,2998,0,3000,3001,0,0,3002,0,0,0,3003,0,0,3012 +,0,0,3022,0,0,3024,0,0,3025,3027,0,0,0,3030,0,0,0,0,3034,3035,0,0,3036,0,3039,0, +3049,0,0,3050,0,0,0,0,0,0,3051,0,3053,0,0,0,0,3057,0,3058,0,0,0,0,0,0,0,0,3063,0 +,0,3073,3074,3078,3079,0,3080,3086,0,0,0,0,0,0,0,0,3087,0,3092,0,3095,0,3099,0,0 +,0,3100,0,3101,3102,0,3122,0,0,0,3124,0,3125,0,0,0,0,0,0,3132,3134,0,0,3136,0,0, +0,0,0,0,0,3147,0,0,3149,0,0,0,0,0,3150,3151,3152,0,0,0,0,3158,0,0,3160,0,0,3161, +0,0,3162,0,3163,3166,3168,0,0,3169,3170,0,0,3171,0,0,0,0,0,0,0,3182,0,3184,0,0, +3188,0,0,3194,0,0,0,0,0,0,3204,0,0,0,0,3209,0,0,0,0,0,0,0,0,0,0,0,3216,3217,0,0, +0,0,0,0,0,3219,0,0,3220,3222,0,3223,0,0,0,0,3224,0,3225,3226,0,3228,3233,0,3239, +3241,3242,0,0,3251,3252,3253,3255,0,0,0,0,0,0,0,0,3260,0,0,3261,0,0,0,3267,0,0,0 +,0,0,0,0,0,3271,0,0,0,3278,0,3282,0,0,0,3284,0,0,0,3285,3286,0,0,0,0,0,0,0,3287, +3292,0,0,0,0,3294,3296,0,0,3299,3300,3301,0,3302,0,0,0,0,0,3304,3306,0,0,0,0,0,0 +,3308,0,0,0,0,0,0,0,0,0,3311,0,0,0,0,0,0,0,0,3312,3314,3315,0,3318,0,0,0,0,0,0,0 +,0,3319,0,0,0,0,0,3321,0,0,0,0,0,0,0,0,0,3322,0,0,3324,3325,0,0,3326,0,0,3328, +3329,3331,0,0,3335,0,0,3337,0,3338,0,0,0,0,3343,3347,0,0,0,3348,0,0,3351,0,0,0,0 +,0,0,3354,0,0,0,0,0,0,0,0,0,0,3355,0,0,3365,3366,3367,0,0,0,0,0,0,3368,3369,0, +3370,0,0,3373,0,0,3376,0,0,3377,0,3379,3387,0,0,0,0,0,3390,0,0,0,0,0,0,0,3402,0, +3403,3436,3437,3439,0,0,3441,0,0,0,3442,0,0,3449,0,0,0,3450,0,0,0,0,0,0,0,3451,0 +,0,3452,0,3453,3456,0,3457,0,0,3458,0,3459,0,0,0,0,0,0,0,0,0,3460,0,0,3469,3470, +0,0,3475,0,0,0,3480,3487,3489,0,3490,0,0,3491,3499,0,3500,0,0,3501,0,0,0,3502,0, +3514,0,0,0,3516,3517,0,0,0,3518,0,0,0,0,3520,3521,3522,0,0,3526,3530,0,0,0,0, +3531,0,0,0,0,3536,0,0,0,0,0,0,0,3539,3541,0,0,3542,3544,0,3547,3548,0,0,3550,0, +3553,0,0,0,0,0,0,0,3554,0,3555,0,3558,0,3559,0,0,0,0,0,0,0,0,3563,0,3581,0,0,0, +3599,0,0,0,3600,0,3601,0,3602,3603,0,0,3606,3608,0,3610,3611,0,0,0,0,0,0,0,0,0, +3612,3616,3619,0,0,0,0,0,0,0,0,0,0,0,0,0,3624,3628,0,3629,3634,3635,0,0,0,0,0,0, +3636,0,3637,0,0,3638,3651,0,0,0,0,0,0,3652,3653,0,0,0,0,3656,3657,0,0,0,0,0,3658 +,0,0,0,0,3659,0,3661,3663,3664,0,3665,0,3692,0,0,0,3694,3696,0,0,0,0,0,0,0,0,0,0 +,0,0,3698,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3700,0,0,3701,0,0,0,3708,3709,0,0,0,3711 +,3712,0,0,0,0,0,3723,0,3724,3725,0,0,3726,0,0,0,0,0,0,3728,3729,0,3734,3735,3737 +,0,0,0,3743,0,3745,0,0,3746,0,0,3747,3748,0,3757,0,3759,3766,3767,0,3768,0,0,0,0 +,3769,0,0,3771,0,3774,0,0,0,0,0,0,3775,0,0,0,0,0,0,3776,0,3777,3786,0,3788,3789, +0,0,0,0,0,0,0,0,0,3791,0,3811,0,0,0,0,0,3814,3815,3816,3820,0,0,0,0,0,0,0,3821,0 +,0,3825,0,0,0,0,3835,0,0,3848,3849,0,0,0,0,3850,3851,3853,0,0,0,0,3859,0,3860, +3862,0,0,0,0,0,3863,0,0,0,0,0,0,0,0,3873,0,3874,0,3875,3886,0,3887,0,0,0,0,3892, +3913,0,3914,0,0,0,3925,3931,0,0,0,0,3934,3941,3942,0,0,0,0,3943,0,0,0,3944,0,0,0 +,0,0,3945,0,3947,0,0,0,3956,3957,0,0,0,0,0,0,0,0,0,3958,0,3959,3965,0,0,0,0,3966 +,0,0,0,3967,0,0,0,3968,3974,0,0,0,0,0,3975,3977,3978,0,0,0,0,3980,0,3985,0,0,0,0 +,0,0,0,0,3986,4011,0,0,4017,0,0,0,0,0,0,0,0,0,0,0,4018,0,0,0,0,4019,0,4023,0,0,0 +,4027,4028,0,0,0,0,0,0,0,0,4031,4034,0,0,4035,4037,4039,4040,0,0,0,0,0,4059,0, +4060,4061,0,4062,4063,4066,0,0,4072,0,0,0,0,0,0,0,0,0,0,0,0,0,4088,0,0,0,0,0, +4091,0,0,0,0,4094,4095,0,0,4096,0,0,0,0,0,4098,4099,0,0,0,4101,0,4104,0,0,0,4105 +,4108,0,4113,0,0,4115,4116,0,4126,0,0,4127,0,0,0,0,0,0,0,4128,4132,4133,0,4134,0 +,0,0,4137,0,0,4141,0,0,0,0,4144,4146,4147,0,0,0,0,4148,0,0,4311,0,0,0,4314,4329, +0,4331,4332,0,4333,0,4334,0,0,0,4335,0,4336,0,0,0,4337,0,0,0,4342,4345,4346,4350 +,0,4351,4352,0,4354,4355,0,0,4364,0,0,0,0,4369,0,0,0,4373,0,4374,0,0,0,0,4377,0, +0,0,0,4378,0,0,0,4380,0,0,0,4381,4382,0,0,0,0,0,0,0,4384,0,0,0,0,4385,0,0,0,4386 +,0,0,0,4391,4398,0,0,0,0,4407,4409,0,0,0,0,4410,0,0,4411,0,4414,4415,4418,0,4427 +,4428,4430,0,4431,0,4448,0,0,0,0,0,4449,0,0,0,4451,4452,0,4453,4454,0,4456,0,0,0 +,0,0,0,0,4459,0,4463,0,0,0,0,0,4466,0,4467,0,4469,0,0,0,0,0,0,0,0,0,0,0,0,0,4470 +,4471,0,4473,0,0,4475,0,0,0,0,4477,4478,0,0,0,4479,4481,0,4482,0,4484,0,0,0,0,0, +0,0,4486,0,0,4488,0,0,4497,0,4508,0,0,4510,4511,0,4520,4523,0,4524,0,4525,0,4527 +,0,0,4528,0,0,0,0,4530,0,4531,0,0,4532,0,0,0,4533,0,0,0,0,0,4535,0,0,0,4536,0,0, +0,0,0,4541,4543,4544,4545,4547,0,4548,0,0,0,0,4550,4551,0,4553,0,0,0,0,4562,0,0, +4571,0,0,0,4574,0,0,0,4575,0,4576,0,4577,0,0,0,4581,0,0,0,0,0,4582,0,0,4586,0,0, +0,4588,0,0,4597,0,4598,0,0,0,0,4616,4617,0,4618,0,0,0,0,4619,0,4620,0,0,4621,0, +4624,0,0,0,0,0,4625,0,0,0,0,4657,0,4659,0,4667,0,0,0,4668,4670,0,4672,0,0,0,0,0, +4673,4676,0,0,0,0,4687,0,0,0,0,4697,0,0,0,0,4699,0,4701,0,0,0,0,4702,0,0,4706,0, +0,4713,0,0,0,4714,4715,4716,0,0,0,0,0,0,0,0,0,0,0,0,4717,0,0,4720,0,4721,4729, +4735,0,0,0,4737,0,0,0,4739,0,0,0,4740,0,0,0,4741,0,0,0,0,0,4742,0,4745,4746,4747 +,0,0,0,0,0,0,0,0,4748,0,0,0,4749,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4751, +4786,0,4787,0,4788,4796,0,0,4797,4798,0,4799,4806,4807,0,0,0,0,4809,4810,0,0,0,0 +,0,0,4811,0,0,0,0,0,4812,0,4813,0,0,4815,0,4821,4822,0,0,0,0,4823,0,0,0,0,0,0,0, +0,0,0,4824,0,0,0,0,4826,0,0,0,4828,0,4829,0,0,0,4843,0,0,4847,0,4853,4855,4858,0 +,0,0,0,0,4859,0,4864,0,0,4879,0,0,0,0,4880,0,0,0,0,4881,0,4882,0,0,0,0,0,0,0,0,0 +,4883,0,0,0,0,4884,0,0,0,0,0,4886,4887,4888,4894,4896,0,4902,0,0,4905,0,0,4915,0 +,0,0,0,0,0,0,4916,4917,4919,4921,0,0,0,0,0,4926,0,0,0,0,4927,0,0,0,0,0,0,0,0, +4929,0,4930,4931,0,4938,0,4952,0,4953,4957,4960,4964,0,0,0,0,0,0,0,5019,5020, +5022,0,0,0,0,0,5023,0,0,0,5024,0,0,0,5025,0,0,0,0,5028,0,0,0,0,5029,5030,5031,0, +5033,0,0,0,0,0,0,0,0,0,5034,5035,0,5036,0,0,5037,0,0,0,0,5038,0,0,5039,0,0,0, +5041,5042,0,0,0,0,5044,5049,5054,0,5055,0,5057,0,0,0,5060,0,0,0,0,0,5063,0,5064, +5065,0,5067,0,0,0,5068,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5076,0,0,0,0,0,0, +0,5077,0,0,5078,5080,0,0,5083,0,0,0,0,0,0,0,0,5085,0,0,0,0,0,0,5098,5099,5101, +5105,5107,0,5108,0,5109,0,0,0,0,0,0,0,5110,0,0,0,0,0,5117,5118,0,5121,0,5122,0,0 +,5130,0,0,0,5137,0,0,0,5148,0,0,0,0,0,0,0,5151,5154,0,0,0,5155,0,0,5156,5159, +5161,0,0,0,0,5162,0,0,0,0,5163,5164,0,5166,0,0,0,0,0,0,0,0,0,0,5167,0,0,0,5172,0 +,0,0,0,0,0,5178,5179,0,0,5190,0,0,5191,5192,5194,0,0,5198,5201,0,0,0,0,0,5203,0, +5206,5209,0,0,0,0,0,0,5213,0,5214,5216,0,0,0,0,0,5217,0,0,0,0,0,0,0,0,5218,5219, +0,5231,0,0,5244,5249,0,5254,0,5255,0,0,5257,0,0,0,0,0,5258,0,5260,5270,0,5277,0, +0,0,0,0,0,5280,5281,5282,5283,0,0,0,0,0,5284,0,5285,0,0,0,0,0,5287,5288,0,0,0,0, +0,0,0,0,0,0,5289,5291,0,0,5294,0,0,5295,0,0,0,0,0,0,0,5304,0,0,5306,5307,5308,0, +5309,0,0,5310,0,0,0,0,5311,5312,0,5313,0,0,0,0,0,5316,0,0,0,5317,0,0,0,0,0,0,0,0 +,0,5325,0,0,0,0,0,0,5326,0,5327,5329,0,5332,0,0,0,0,5338,0,0,0,0,0,0,0,0,5340,0, +0,5341,0,0,0,5342,0,5343,5344,0,0,5345,0,0,0,0,0,0,5347,5348,0,0,0,0,0,0,0,0,0, +5349,0,5350,0,5354,0,0,0,0,5358,0,0,5359,0,0,5361,0,0,5365,0,5367,0,5373,0,0,0, +5379,0,0,0,5380,0,0,0,5382,0,5384,0,0,0,0,0,0,5385,0,0,0,0,5387,0,0,0,0,0,0,5388 +,5390,5393,0,0,0,0,0,0,0,0,0,0,0,5396,0,0,0,0,5397,5402,0,0,0,0,0,5403,0,0,0, +5404,5405,0,0,0,0,0,0,0,0,0,0,0,0,5406,0,0,0,0,5410,0,0,5411,0,5415,0,0,0,0,5416 +,5434,0,0,0,0,0,0,0,0,0,0,0,5438,0,5440,0,0,0,0,0,0,5441,5442,0,0,0,5443,5444, +5447,0,0,5448,5449,5451,0,0,0,5456,5457,0,0,0,5459,0,0,0,5461,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,5464,0,5466,0,0,5467,0,5470,0,0,5473,0,0,5474,0,0,5476,0,0,0,0,0,0,0,0 +,0,0,0,5477,0,0,0,0,0,0,0,5484,0,0,5485,5486,0,0,0,0,0,5488,0,0,0,0,0,0,0,5489,0 +,0,0,0,0,5507,0,0,0,5510,0,5511,0,0,5512,0,0,0,5513,0,5515,0,0,5516,5517,0,5518, +0,0,5522,0,0,0,0,0,5534,5535,0,0,5536,0,5538,0,0,5543,0,5544,0,0,5545,0,5547,0, +5557,0,0,5558,0,5560,5567,0,0,0,0,5568,0,0,0,5571,5573,0,5574,0,5575,0,0,0,0, +5577,0,0,5598,0,0,0,0,0,0,0,0,0,5600,5609,0,0,0,0,5610,0,0,5612,0,5624,0,5625,0, +0,0,5629,0,5641,0,5642,5643,0,0,0,0,0,0,5651,0,0,0,5652,5653,0,5661,5662,5678,0, +5679,0,0,0,0,5685,5686,0,0,0,0,0,5690,5692,0,5703,0,0,0,0,0,5706,0,0,0,0,5707,0, +0,0,0,0,0,5708,0,0,5709,0,5710,0,0,0,5712,0,5733,0,5734,5735,0,0,5744,5751,0,0,0 +,0,0,0,0,0,0,0,0,0,5752,0,5754,0,0,0,0,0,0,5757,5758,0,5760,5761,0,0,0,0,5763, +5764,5765,0,5766,0,5767,5768,0,5770,0,0,0,0,5776,5780,0,0,0,0,5782,0,0,0,0,5784, +0,0,5788,0,0,0,0,0,0,0,0,0,0,0,5797,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5799,0,0,5801, +0,0,0,5811,0,0,0,0,0,0,5816,0,0,5827,0,0,0,0,0,0,0,0,5830,5831,0,0,5832,0,0,5833 +,0,5835,5844,5845,0,5846,0,0,0,0,0,5850,0,0,0,0,0,5852,0,5855,5857,0,0,5859,0, +5861,0,0,5863,0,5865,0,0,0,5873,5875,0,0,0,5877,0,5879,0,0,0,5888,0,0,5889,5891, +0,5894,0,0,0,0,0,0,5895,0,5897,0,0,0,0,0,0,5907,0,5911,0,0,5912,0,5913,5922,5924 +,0,5927,5928,0,0,0,0,5929,5930,0,5933,0,0,0,0,5949,0,0,5951,0,0,0,0,0,0,0,0,5953 +,0,0,5954,0,5959,5960,5961,0,5964,0,0,0,5976,5978,5987,5990,0,0,0,0,0,5991,0, +5992,0,0,0,5994,5995,0,0,5996,0,0,6001,6003,0,0,0,0,6007,0,0,0,0,0,6008,0,0,6009 +,0,6010,0,0,0,6011,6015,0,6017,0,6019,0,6023,0,0,0,0,0,0,0,6025,0,0,0,0,0,0,0,0, +0,0,6026,0,6030,0,0,6032,0,0,0,6033,6038,6040,0,0,0,6041,6045,0,0,6046,0,0,6053, +0,0,6054,0,6055,0,0,0,0,0,0,6057,0,6063,0,0,0,6064,0,6066,6071,6072,0,0,0,0,0,0, +6075,6076,0,0,6077,0,0,0,0,0,0,0,0,0,6078,6079,0,0,0,0,0,0,0,0,6080,0,6083,0,0,0 +,0,0,6084,0,0,6088,0,6089,0,0,6093,6105,0,0,6107,0,6110,0,0,0,6111,6125,6126,0,0 +,0,6129,0,0,0,0,6130,0,0,0,6131,6134,0,0,0,0,0,0,6142,0,0,0,0,0,6144,0,0,6146, +6151,6153,0,6156,0,6163,0,6180,6181,0,0,0,0,0,6182,0,0,0,0,6184,6195,0,0,6206,0, +6208,0,0,6212,6213,6214,0,6215,0,0,0,6228,0,0,0,6234,0,0,0,0,0,0,6235,6240,0, +6242,6243,6244,0,6250,6255,0,0,0,0,0,6257,0,0,0,6258,6278,0,6284,0,0,0,6285,0,0, +0,0,0,0,0,0,6286,0,0,0,6320,0,0,6322,6332,0,0,0,0,0,0,0,0,6334,0,0,0,0,0,0,0, +6335,0,0,6337,0,6338,0,6339,6340,0,0,6356,6357,6369,0,0,0,6370,6371,6372,0,6373, +0,0,0,0,0,6376,0,0,0,0,0,6382,6383,6384,0,0,0,0,6386,0,6389,6397,6400,6411,0, +6414,0,0,0,0,0,0,0,6415,6416,0,0,0,0,0,0,6417,0,0,0,0,6418,0,0,0,0,0,0,0,6420,0, +6421,6423,6425,0,6429,6430,0,6433,6438,0,0,0,0,0,0,0,0,0,0,6439,6440,0,0,6441,0, +0,6444,0,0,0,0,6446,0,0,0,0,6447,6448,0,0,6450,0,0,0,6454,0,0,6455,0,6461,0,0,0, +0,0,0,6462,0,0,6463,0,6464,0,6465,6467,0,0,0,6468,0,6479,6480,0,0,0,0,0,0,0,6481 +,0,0,6485,6487,0,0,0,0,0,0,6493,0,0,0,0,0,0,0,0,6494,6495,6496,0,0,0,0,0,6498,0, +0,0,6507,6508,0,0,0,0,0,0,0,0,0,0,6511,6512,0,0,0,0,6513,0,0,0,6514,0,0,0,0,0, +6516,0,0,6517,6518,0,0,0,6519,6520,6521,0,6523,0,0,0,0,6524,6528,0,6530,0,0,6532 +,0,6578,0,0,0,6583,0,6584,0,0,0,6587,0,0,0,6590,0,6591,0,0,0,0,0,6592,0,0,0,0, +6593,6594,0,0,0,0,0,6599,6600,0,0,6601,6602,6604,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +6608,0,0,0,0,0,0,0,0,6610,6611,0,6615,0,6616,6618,6620,0,6637,0,0,0,0,6639,0,0,0 +,0,6641,0,6642,0,0,0,6647,0,6660,6663,0,6664,0,6666,6669,0,6675,6676,6677,0,0,0, +0,0,0,0,0,0,6678,0,0,0,6679,0,6680,0,0,0,0,0,0,0,6693,0,0,0,0,0,0,0,0,0,6704, +6705,6706,0,0,6711,6713,0,0,0,0,0,6716,0,0,0,6717,0,6719,6724,0,0,0,0,0,0,0,0, +6725,6726,0,0,0,0,0,6728,6729,6735,0,6737,6742,0,0,6743,6750,0,6751,0,0,6752, +6753,0,0,0,0,0,0,6754,0,0,0,0,0,6756,0,0,0,0,0,0,6763,0,0,6764,6765,0,0,0,6770,0 +,0,0,6776,6780,0,6781,0,0,0,6783,0,6784,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +6785,0,0,0,6792,0,0,0,6793,0,0,6802,0,0,0,0,0,6803,0,0,0,6804,0,0,0,6812,0,0, +6823,0,6824,6839,0,0,0,0,6852,0,0,6854,0,6856,6857,0,0,0,0,0,0,0,0,0,6867,0,6868 +,6870,6872,0,0,0,6873,6874,0,0,0,0,0,6875,0,0,6877,0,0,0,0,0,0,0,6878,0,0,0,6879 +,0,6880,0,0,0,0,0,0,0,0,0,0,6887,0,6888,6891,6893,0,6895,0,0,0,0,0,0,0,0,6899,0, +0,0,0,6901,0,0,0,0,6910,0,6911,0,0,6912,0,0,6913,6914,0,0,0,6915,0,0,0,6916,6919 +,0,0,0,0,0,0,6924,0,6925,0,0,0,6926,6927,6928,0,6929,0,6930,0,0,6931,6935,0,6936 +,0,0,0,0,6939,6940,6941,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6942,6948,6949,0,0,0,0,0,0 +,0,6952,6954,6963,6965,6966,0,0,6967,6968,0,0,0,0,0,0,0,0,0,6969,0,0,6970,6979,0 +,0,6980,0,0,6983,0,0,0,0,0,6984,0,0,0,0,0,0,0,6988,6990,6992,0,0,0,0,0,0,0,6995, +0,0,0,7012,0,0,0,0,0,0,0,0,0,7019,0,0,0,0,0,0,0,0,7021,0,0,7022,7023,7028,0,7030 +,7033,0,0,0,0,0,0,7038,0,0,0,0,0,0,0,0,0,0,7039,0,0,0,0,0,7046,0,7047,0,0,0,0,0, +0,0,0,0,0,0,7048,7052,0,0,0,0,0,7054,0,7060,0,0,0,0,7061,0,7065,0,0,0,0,7067, +7069,0,7070,7071,7072,0,0,7078,0,7080,7081,0,7083,0,0,0,7084,7087,7088,0,0,7090, +0,7093,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7107,0,0,7108,0,0,0,0,0,0,0,0,7110,0,7114,0 +,0,0,0,0,0,0,7115,0,7116,0,0,0,0,0,7117,0,0,7118,0,0,7124,0,7125,0,0,7126,0,0,0, +0,7128,0,0,0,0,0,7129,0,7130,0,7132,7133,0,0,7134,0,0,7139,0,7148,7150,0,0,0,0, +7152,0,0,0,7153,7156,7157,0,0,0,0,0,7158,0,0,0,0,0,0,0,0,0,0,7163,7165,7169,0, +7171,0,0,0,0,0,0,0,0,0,7172,0,7173,7181,0,0,0,0,0,7182,7185,0,0,0,0,7187,0,7201, +7204,0,0,0,0,0,7206,7207,0,0,0,0,7211,7216,0,7218,0,0,0,0,7226,7228,7230,7232, +7233,7235,7237,0,0,0,0,7238,7241,0,7242,0,0,7247,0,0,0,7266,0,0,0,0,0,0,0,7289,0 +,0,7290,7291,0,0,7292,0,7297,0,0,0,0,0,0,0,0,0,0,7300,0,7301,0,0,0,0,0,0,0,0,0,0 +,0,0,7302,0,0,0,0,7305,0,0,0,0,7307,0,7308,0,7310,0,7335,0,0,0,0,0,0,0,7337,0, +7343,7347,0,0,0,0,0,7348,0,7349,7350,7352,7354,0,0,0,0,7357,0,7358,7366,0,7367, +7368,0,0,7373,0,0,0,7374,0,0,0,0,0,0,0,7376,0,0,0,7377,0,0,0,0,0,7378,0,7379, +7380,0,0,0,0,0,7383,0,0,7386,0,0,0,0,7398,0,0,0,7399,7400,0,7401,0,0,0,0,0,0,0, +7402,0,0,0,0,0,7405,0,0,0,0,0,7406,0,0,0,0,0,0,0,0,7421,7427,7429,0,0,0,7435,0,0 +,7436,0,0,0,7437,0,0,0,0,0,0,7438,7443,0,7446,0,7448,0,0,0,0,0,0,0,0,0,0,7456,0, +0,0,0,0,7457,0,0,7461,0,0,0,0,0,7462,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7463,7466,7472, +0,7476,0,0,7490,0,7491,0,0,7493,0,0,0,7498,7499,0,0,7508,0,0,0,0,0,7512,0,0,0, +7513,7514,7516,0,0,0,0,7518,0,0,7519,7521,7522,0,0,0,7526,0,0,7529,0,0,7531,0, +7536,0,7538,0,7539,0,0,7541,7542,7546,0,0,0,0,0,7547,0,7548,0,0,0,0,0,7550,0,0, +7552,7553,0,0,0,0,0,0,0,0,0,0,7554,7563,0,7573,0,0,0,0,0,0,7574,7576,0,7578,7581 +,7583,0,0,0,7584,0,7587,0,0,0,0,0,7589,0,0,0,7594,0,0,7595,0,0,7600,7602,7610,0, +0,0,0,0,7612,0,7613,7614,0,0,7615,0,0,7616,0,7620,0,7621,7622,0,7623,0,0,0,0, +7626,0,0,0,0,7627,7629,7631,0,0,7633,0,0,0,0,0,7639,0,7640,7642,0,0,7643,0,0,0,0 +,7644,0,0,0,0,0,0,0,7645,0,0,0,0,0,7661,7662,7663,7665,0,7666,0,7667,0,7684,7688 +,7690,0,7691,0,0,0,0,0,0,7692,0,0,7700,0,7707,0,7708,0,7709,0,7721,0,0,0,7722,0, +7724,0,0,0,0,0,0,7729,7731,0,7732,0,7733,7735,0,0,0,0,0,0,0,7739,0,0,7741,7745,0 +,7748,0,0,0,7751,0,0,0,7752,0,0,0,0,0,0,0,7753,0,0,7756,0,7757,0,7759,0,7760,0,0 +,0,0,7761,7768,0,0,7769,0,0,7770,0,0,7771,0,0,7772,0,0,7773,0,0,0,0,0,7778,7783, +0,0,0,0,0,7784,7785,0,7790,0,0,0,0,7792,0,7798,0,0,0,0,0,7799,0,7810,0,0,7813,0, +7814,0,7816,0,7818,7824,7825,7826,0,7828,7830,0,0,0,7840,0,7842,0,7843,0,0,0,0, +7844,0,0,0,0,0,0,0,7846,0,0,0,0,0,7856,7857,7858,7862,0,7865,0,0,7866,0,0,7913,0 +,0,0,0,7914,0,0,7915,7917,7918,7919,0,7920,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7921, +7922,0,7924,0,0,7925,0,0,7927,0,7930,7935,0,0,7937,0,0,0,0,0,0,7939,0,7940,0,0,0 +,0,0,7941,0,0,0,0,7945,0,0,0,0,7949,0,0,0,0,0,0,0,0,7950,0,7953,0,0,0,0,0,0,0, +7968,0,0,0,0,7969,7972,7992,0,7993,0,0,0,0,0,0,0,0,0,0,0,7994,0,0,0,0,8007,8008, +0,0,0,0,0,0,0,0,0,0,0,0,8010,0,0,0,8012,0,0,0,0,0,0,0,0,8018,0,8028,8029,0,0, +8030,0,0,8032,8033,0,0,8034,8036,0,0,0,0,0,0,0,0,0,0,8037,0,0,0,8043,8052,8059, +8060,0,0,8061,0,0,0,8062,0,8063,0,8064,0,8066,8068,0,0,0,8080,8081,0,8089,0,0,0, +0,0,8092,0,0,0,0,0,0,8093,8110,0,0,0,0,0,0,0,8111,0,0,0,0,0,8112,8115,0,8117,0,0 +,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8120,8121,8122,8128,8129,8130,8131,0,0,8139,0,0, +8144,0,0,0,0,8145,8146,8153,0,0,0,0,0,0,0,0,8154,0,8157,8160,8162,0,8164,8165,0, +0,0,0,8166,8167,0,0,8179,0,0,0,8185,0,0,0,8186,0,0,8187,0,0,0,8188,0,0,0,0,0, +8204,0,0,0,0,8210,0,0,0,0,0,8213,0,8214,0,0,8215,0,0,0,0,0,0,8218,0,0,0,0,0,0,0, +0,0,8219,0,8221,0,0,8222,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8225,0,0,0,8233,0,0, +8242,0,0,0,0,0,0,0,0,0,0,0,8247,0,8248,8252,0,8256,8257,0,0,8261,0,8264,8265,0,0 +,0,0,8267,0,0,0,8269,0,0,0,0,0,0,0,0,0,8270,0,0,0,8278,0,8279,8283,0,0,8285,8286 +,8289,8292,0,0,0,0,8293,8295,8299,8300,8301,0,0,0,0,0,0,8304,8307,0,0,0,0,0,0,0, +8321,0,0,0,8322,8323,8325,8326,8327,0,0,8332,8338,0,0,8340,0,0,0,0,0,8350,0,0, +8351,0,8354,8355,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8360,8372,0,0,0,0,0,0,0,0,8377,0,0, +0,0,8380,0,0,0,8383,0,8384,0,0,0,0,8386,8392,0,0,8394,0,0,0,0,0,0,0,8396,8397,0, +8398,0,8399,0,0,0,0,0,8400,0,8401,8410,8411,0,8412,8413,8422,0,0,0,0,8423,0,0,0, +0,8424,0,0,8425,0,0,0,0,0,0,0,8441,8442,0,0,0,0,0,0,8443,0,0,8444,0,8447,0,0,0,0 +,8451,0,8458,0,8462,0,0,8468,0,8469,0,0,0,8470,0,8473,8479,8480,0,0,0,0,8481, +8483,0,0,0,0,0,0,0,0,0,8484,0,0,8490,0,0,0,0,0,0,8491,8493,8494,0,8528,0,0,0,0,0 +,0,0,8530,0,0,0,0,0,0,0,0,8534,8538,8540,0,0,8541,0,0,8545,0,8557,0,0,8569,8570, +0,0,8571,8574,8575,8579,0,8583,0,0,0,0,8591,0,0,0,0,0,0,0,0,8606,0,8607,0,0,0,0, +0,0,0,0,0,8608,0,0,8609,0,0,0,8610,0,0,0,8611,0,0,8613,8617,8621,0,0,8622,0,8623 +,0,8624,8625,0,0,0,0,0,0,0,0,0,8637,8638,8639,8650,0,0,0,0,8652,8654,8655,0,0,0, +0,0,0,0,0,0,0,8656,0,0,0,0,0,8657,0,0,0,0,0,0,0,0,0,8658,0,0,8659,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,8660,0,0,0,0,0,0,8661,8663,8664,0,0,0,0,8665,0,8669,0, +0,0,0,0,0,0,8671,8674,0,8684,0,8686,0,0,0,8689,0,0,0,8690,0,8706,0,0,0,0,0,0,0,0 +,0,0,0,8710,0,8711,8713,8714,8724,8727,8728,8733,8736,0,8737,8739,0,0,0,0,8742, +8743,8745,8754,0,0,0,0,8756,0,0,0,0,0,0,8757,8760,0,0,0,0,0,8762,8763,8764,0, +8766,8769,8770,8773,0,8774,0,8779,0,0,0,0,8780,0,0,8781,0,0,8783,0,0,0,0,0,0,0,0 +,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8784,0,0,0,0,0,0,0,0,8785,0,0,0,0,8786,0,0,0,0,8788 +,8790,0,0,0,8803,0,8813,8814,0,0,0,0,0,8815,8816,0,0,0,0,8818,0,0,0,0,8822,8828, +8829,0,8831,0,0,0,0,8833,0,0,0,8834,0,0,0,8835,0,8836,0,0,0,8837,0,0,0,0,0,0, +8838,8839,0,0,0,0,0,0,0,0,0,0,0,8840,0,0,0,8841,0,8842,0,0,0,8846,0,0,0,0,0,0,0, +8847,0,8848,0,0,8864,0,0,8866,0,0,8870,8872,0,0,8873,8874,0,0,0,0,0,0,8875,0, +8876,0,0,0,0,8896,8900,0,0,0,0,8901,0,0,0,0,0,8904,0,8907,0,0,0,0,8911,8912,8913 +,0,0,0,8914,0,8915,0,0,0,0,0,0,0,0,0,0,0,0,8916,0,0,0,8929,0,0,0,0,0,0,0,0,0,0, +8930,0,8932,0,8943,0,0,0,8945,8947,0,0,0,0,8949,0,8950,0,8954,8957,0,0,8970,0,0, +0,0,8971,0,8996,0,0,0,0,8997,9000,0,0,0,0,9001,9002,0,9004,9009,9024,0,0,0,0,0,0 +,0,0,0,0,0,0,9027,9082,0,0,9083,9089,0,0,0,0,0,0,9090,0,0,0,9092,0,0,9093,0,9095 +,0,0,9096,9097,9101,9102,0,0,0,0,0,0,0,0,9112,0,0,0,0,0,0,9114,0,0,9120,0,9121, +9122,0,0,0,9123,9124,0,0,9125,0,0,9126,0,9127,0,0,9129,9131,0,0,0,9132,0,0,9136, +0,9144,0,0,9148,0,0,0,0,0,0,9149,0,9152,9163,0,0,9165,0,0,0,0,0,0,0,0,0,0,0,0,0, +9166,0,9169,0,0,0,0,0,0,0,9170,0,0,0,0,9172,0,9174,9175,9176,0,9177,0,0,0,0,0,0, +0,0,9186,0,9187,0,0,0,9188,9189,0,0,9190,0,0,0,0,9191,0,0,0,9193,0,0,0,0,9197, +9198,0,0,0,9208,9211,0,0,0,0,9216,9217,0,9220,0,0,0,0,9221,9222,9223,0,9224,9225 +,0,0,9227,0,9228,9229,0,0,9230,0,9232,0,9233,0,0,0,0,0,9234,9235,0,0,9237,0,0,0, +0,0,0,0,0,9238,9240,0,0,9241,0,0,0,0,9244,0,0,0,0,9247,0,0,0,0,0,0,0,0,0,0,9248, +0,0,0,9249,0,0,0,0,0,9250,0,0,0,0,9251,0,0,9252,9255,0,0,0,9256,0,0,0,0,0,0,0, +9257,0,0,9258,0,0,0,0,0,0,9259,0,0,0,0,0,9262,9263,0,0,9265,9266,0,0,0,0,0,0,0,0 +,9268,9271,0,0,0,0,0,0,0,0,0,9273,0,0,0,9276,9277,9279,0,0,0,0,0,0,0,9280,0,0, +9293,0,0,0,0,0,9297,9301,0,0,0,0,0,0,0,0,0,0,0,9308,9309,9313,9321,9322,0,9326, +9327,0,0,9477,0,9479,0,0,0,0,9482,0,0,0,9483,0,9484,0,0,0,0,0,0,0,0,0,9485,0,0, +9486,0,0,0,9489,0,0,0,0,9490,9491,0,0,0,0,9493,0,9495,9496,0,0,0,0,0,0,0,0,9500, +0,9502,0,0,0,0,0,9504,9507,0,9509,0,9511,0,0,9513,0,0,0,0,0,0,0,0,9515,0,0,0,0,0 +,0,9516,9517,0,0,0,0,9532,0,0,9533,0,0,9538,0,9539,9540,0,0,0,0,9541,0,0,0,9542, +0,0,0,0,0,0,0,0,9544,9545,0,9546,0,0,0,0,0,0,9547,9548,0,0,0,9550,0,9557,0,9558, +0,9561,0,9563,9570,0,9572,9574,9575,0,0,0,9577,9592,0,0,9596,0,0,0,9598,0,9600,0 +,9601,0,0,0,0,0,0,9608,0,9638,9639,0,0,0,0,0,0,0,9641,0,0,9643,9644,9645,9646,0, +0,0,9648,0,0,0,0,0,0,0,9650,9654,0,0,0,0,0,0,0,0,9655,0,0,0,0,0,9656,0,9657,0,0, +0,0,9658,0,0,9659,0,0,9664,0,0,9665,0,9667,9669,0,0,0,0,0,0,0,0,0,0,0,0,9671,0, +9673,9681,0,0,0,0,9682,9683,9684,0,0,0,0,9686,9698,0,0,9700,9701,9702,0,9703, +9717,0,0,0,0,9718,0,9726,0,0,0,0,9727,0,0,0,9728,0,9742,0,9744,0,0,0,9750,0,9754 +,9755,0,0,0,0,0,9756,0,9757,9768,0,9769,0,0,0,9770,9771,0,9773,0,9774,0,9775,0,0 +,0,9776,9777,9784,0,0,0,9786,0,9789,0,0,0,0,9793,9794,0,0,0,9808,0,0,0,0,0,9811, +0,0,0,0,0,0,0,0,0,0,0,0,9812,0,9820,0,9823,0,9828,0,0,0,0,9830,0,0,9833,9836,0,0 +,0,9840,0,0,0,9841,0,0,9842,0,9845,0,0,0,9847,9848,0,0,9855,0,0,0,0,0,0,9856, +9863,9865,0,0,0,0,0,0,0,0,9866,9867,9868,9873,9875,0,0,0,0,0,0,9880,0,9886,0,0,0 +,9887,0,0,9891,0,0,0,0,0,0,0,9906,9907,9908,0,0,0,9909,0,0,0,0,0,0,9910,0,0,0,0, +9913,0,0,0,0,9914,0,0,0,0,0,9922,0,0,0,0,9923,9925,0,0,0,0,0,0,9930,0,0,0,9931,0 +,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9932,0,9939,0,0,9940,9962,9966,0,9969,9970,0,0,9974 +,0,9979,9981,9982,0,0,0,9985,0,0,0,0,0,0,9987,0,0,0,0,0,0,0,9988,9993,0,0,9994,0 +,0,0,9997,0,10004,0,0,0,0,0,10007,10019,10020,10022,0,0,0,10031,0,0,0,0,0,10032, +0,0,10034,0,10036,0,0,0,0,10038,0,10039,10040,10041,10042,0,0,0,0,0,10043,0,0,0, +0,0,10045,10054,0,0,0,0,10055,0,0,10057,10058,0,0,0,0,0,0,10059,0,0,0,0,0,0,0, +10060,0,0,0,0,0,0,0,10063,0,10066,0,0,0,10070,0,10072,0,0,10076,10077,0,0,10084, +0,10087,10090,10091,0,0,0,10094,10097,0,0,0,0,0,0,10098,0,0,0,0,0,0,10103,0, +10104,0,10108,0,0,0,0,0,0,0,0,10120,0,0,0,10122,0,0,10125,0,0,0,0,10127,10128,0, +0,10134,0,10135,10136,0,10137,0,0,10147,0,10149,10150,0,0,10156,0,10158,10159, +10160,10168,0,0,10171,0,10173,0,0,0,10176,0,0,0,0,10177,0,0,0,0,10178,0,0,0,0, +10194,0,10202,0,0,10203,10204,0,10205,10206,0,10207,0,0,0,0,10209,0,0,0,0,0,0,0, +10213,0,0,0,0,0,0,10217,0,10229,0,10230,10231,0,0,10232,0,0,10237,10238,10244,0, +0,0,0,0,10250,0,10252,0,0,0,0,0,0,10255,0,0,10257,0,0,0,0,0,0,10258,0,10259,0,0, +0,0,0,0,0,0,10260,0,0,0,0,0,0,0,10284,10288,10289,0,0,0,10290,0,10296,0,0,0,0,0, +10297,0,0,0,0,0,0,10298,0,0,0,0,10299,10303,0,0,0,0,0,10306,0,0,0,10307,0,10308, +0,0,0,0,10311,0,0,0,0,0,0,0,10315,10317,0,0,0,10318,10319,0,10321,0,10326,0, +10328,0,0,0,0,10329,0,0,10331,0,10332,0,0,0,0,0,0,10334,0,0,10335,10338,0,0,0,0, +0,10339,10349,0,0,0,0,0,0,10351,0,10353,0,0,0,0,0,0,10362,0,10368,0,10369,0,0,0, +10372,10373,0,0,0,0,0,10374,0,0,0,10375,0,10376,0,0,10386,10388,10390,0,0,0,0,0, +0,0,10391,0,0,10392,10394,0,0,10396,0,10397,0,10403,0,0,0,0,0,0,0,0,10404,0, +10405,10410,0,0,10411,0,10412,0,0,0,0,0,0,0,10421,10422,10423,0,0,0,0,0,0,0,0,0, +10425,0,0,10427,0,0,10430,0,0,0,0,0,10432,0,10433,10434,0,0,0,0,10436,10437,0, +10438,0,10439,0,10444,10446,0,0,0,0,0,10448,0,0,0,0,0,10449,0,0,0,0,0,0,0,10451, +0,10453,0,0,0,10454,10457,0,0,10459,0,10469,0,0,0,0,0,10472,10481,0,0,0,0,0, +10482,10483,0,10492,0,0,0,0,0,0,0,0,0,0,10499,0,0,0,10502,0,0,10510,0,10521, +10524,0,0,10525,10526,10528,0,0,0,0,0,0,0,0,10530,0,0,0,0,10533,0,10534,0,0,0,0, +0,0,0,0,0,0,10535,10536,0,0,10544,0,10553,10556,0,10557,10559,0,0,0,0,0,10562, +10563,10564,0,10565,0,0,0,10566,0,10567,0,0,0,0,10575,0,0,10576,0,10578,0,0,0,0, +0,0,0,0,0,0,10585,10586,10587,10589,0,10590,0,0,10594,0,0,0,0,0,10598,0,0,10601, +0,0,0,10602,0,10603,0,10604,0,10605,0,0,10607,0,10626,0,10627,0,0,0,0,0,10629, +10630,10631,0,0,0,10646,0,0,0,10647,0,10650,0,10651,0,0,0,10652,10653,10655,0, +10658,0,0,10659,0,10667,0,0,0,0,10669,0,0,0,0,0,0,0,0,0,10670,0,0,0,10671,0,0,0, +0,10672,10673,0,10674,0,0,0,10676,0,0,0,0,0,0,10678,0,10682,0,0,10692,0,10697,0, +0,0,0,10698,0,0,0,10700,0,0,0,0,0,10703,0,10704,0,0,0,0,0,0,0,10705,0,10715, +10718,10720,0,0,10722,0,0,0,0,0,0,0,0,10723,0,0,0,0,10726,0,0,0,0,0,10727,10730, +10743,0,0,0,0,0,0,10744,0,0,10745,0,0,0,0,0,0,10748,0,0,0,0,10750,0,0,10752, +10753,0,0,0,10756,0,0,0,0,0,0,10758,0,0,0,10759,0,10769,0,0,10772,0,0,0,0,0,0, +10773,0,0,0,10777,0,0,10779,0,0,0,0,0,0,0,0,10780,10784,0,0,0,10789,0,0,0,10791, +0,0,0,0,0,0,0,0,0,10795,0,0,10796,0,10808,0,10809,0,0,0,10810,0,0,0,10812,0,0, +10814,0,0,0,0,0,0,0,0,0,10815,0,0,0,0,10816,10817,0,0,0,0,10819,0,10820,0,0,0,0, +10821,10822,10823,0,10826,10849,0,0,0,0,10850,0,0,10852,0,10853,0,0,10856,0,0, +10857,10858,10859,10860,0,0,0,0,0,0,10863,0,10866,10867,10872,10890,0,0,10891, +10892,0,0,0,0,0,10893,0,0,0,10896,10899,0,0,10900,10902,0,0,0,0,0,10903,0,0,0,0, +0,0,0,0,0,0,0,0,10905,0,10906,0,0,0,0,10908,10911,0,10912,0,0,10916,0,0,0,0,0, +10917,0,10918,0,0,0,10923,0,0,0,0,0,10924,0,0,10928,10929,0,0,10930,0,0,0,10932, +0,0,0,0,10939,0,0,10945,0,0,0,10947,0,0,10948,0,0,0,0,0,0,0,0,0,0,0,0,10958,0, +10960,10962,0,0,10964,0,0,0,10966,0,0,0,0,0,0,0,0,0,0,10967,0,0,0,10968,0,0,0, +10973,0,0,0,0,0,10975,0,0,0,10976,10978,0,0,10982,10984,10987,0,0,10988,0,10989, +0,0,10991,0,0,0,0,10992,0,0,0,10993,0,10995,0,0,0,10996,10997,0,0,0,10998,0, +10999,0,11001,0,0,0,0,0,0,11010,11012,0,11013,11016,11017,0,0,11019,11020,11021, +0,0,0,0,0,0,0,0,0,0,0,0,11022,0,0,11023,11029,0,0,0,0,11031,0,0,0,11034,0,0,0,0, +11055,0,0,0,0,0,11056,11060,0,0,0,0,0,0,11061,0,0,11064,11065,0,11066,0,11069,0, +11085,0,0,0,0,0,11086,0,0,0,11088,0,0,0,11094,0,0,0,11095,11096,0,0,0,0,0,0, +11097,11098,0,0,0,0,0,0,11099,0,0,11102,11108,0,0,0,11109,0,11114,11119,0,11131, +0,0,0,11142,0,0,11143,0,11146,0,11147,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,11148,0, +11149,11152,11153,11154,0,11156,0,11157,0,0,0,11158,0,0,11159,11160,0,0,0,0,0,0, +0,0,0,0,0,0,11163,0,0,11164,11166,0,0,0,11172,11174,0,0,0,11176,0,0,0,0,0,11182, +11183,0,0,0,11184,11187,0,0,11188,11189,0,0,0,0,0,0,11194,0,0,0,0,0,0,0,11200, +11202,0,0,0,0,0,0,11203,0,11204,0,0,0,0,0,11205,0,0,0,11206,0,11207,0,0,11209,0, +11211,0,11214,0,0,11231,0,0,0,11293,11295,0,0,11296,11297,11302,0,0,0,11307,0,0, +0,0,11309,11310,0,11311,0,0,0,11313,0,11314,0,0,0,0,11334,0,11338,0,0,0,11339,0, +0,0,0,0,11340,0,11341,11342,0,11344,0,11345,0,0,0,11348,11349,0,0,11350,0,0,0, +11355,0,0,0,0,0,0,11356,0,11357,11370,0,0,11371,0,11374,11376,0,0,0,11377,0,0, +11378,11383,0,11386,11399,0,11400,11406,0,0,0,11408,0,0,11409,11412,0,0,0,0, +11417,0,0,0,11418,0,11421,0,11426,11429,0,0,0,0,0,11430,0,11437,0,11438,0,0,0,0, +0,11440,11453,0,0,0,0,0,0,11454,0,0,0,0,11455,0,0,11456,11460,11461,11463,0, +11469,0,11473,0,0,0,0,11474,0,0,0,11475,0,11476,11477,11480,0,0,0,0,11481,0,0, +11484,0,0,11487,0,0,0,0,0,0,0,0,0,0,11497,0,0,11502,0,11509,0,0,11510,11511, +11513,0,0,0,0,0,0,0,0,0,0,11515,0,0,0,0,11516,0,11520,11521,0,0,0,0,0,0,0,0,0,0, +0,11529,11530,11531,11534,0,0,11543,0,0,0,0,0,11547,0,11548,0,0,0,0,0,11552, +11556,0,11557,0,0,11559,0,11560,0,0,0,0,0,0,11561,0,0,11563,11564,0,11565,0,0,0, +0,11567,0,0,0,11569,0,11574,0,11575,0,0,0,11577,0,11578,0,0,0,11580,11581,0,0,0, +11582,11584,0,0,0,0,0,0,0,11587,0,11588,11591,0,11595,0,0,0,0,0,0,0,0,11596,0, +11597,0,0,0,0,11598,11601,0,0,0,11602,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,11603, +11604,0,11606,0,0,11608,0,0,0,0,11610,0,0,11611,0,0,0,0,11613,0,11622,0,0,0, +11623,0,0,0,0,11625,0,0,11626,11627,11628,11630,0,0,0,0,0,0,11639,0,0,11646,0, +11648,11649,0,11650,0,0,0,0,0,0,0,0,0,11651,0,0,11652,11653,11656,0,0,11677, +11679,0,0,0,0,11680,0,0,11681,0,11685,0,0,0,0,0,0,0,0,11688,0,0,0,11716,0,11719, +0,0,0,0,0,11721,0,0,11724,11743,0,0,0,0,0,0,0,0,11745,11748,11750,0,0,0,0,0, +11751,0,0,0,11752,11754,0,11755,0,0,0,0,0,0,0,11759,0,0,0,0,0,0,11760,0,0,0, +11761,0,0,0,0,0,0,11766,11767,0,11772,11773,0,11774,0,0,11775,0,11777,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,11778,11780,0,0,0,0,0,0,0,11783,0,11784,0,0,0,11785, +0,0,0,11786,0,0,0,0,11788,0,0,11789,11791,11792,0,0,0,0,11795,11834,11835,11836, +0,0,11837,0,0,0,11838,0,0,11846,11851,0,11852,0,11869,0,0,0,11871,0,0,0,11872, +11874,0,0,0,0,0,0,11875,0,11876,11877,0,0,0,0,0,0,0,0,0,0,11883,0,0,0,0,0,0,0, +11884,0,11885,0,11886,0,0,11887,0,11894,11895,11897,11909,11910,0,11912,11918,0, +0,11920,0,11922,11924,11927,11928,0,0,0,0,11929,0,11934,0,0,0,0,0,11941,11943, +11944,0,11945,0,0,0,0,11948,11949,0,0,0,0,11953,0,11954,0,11955,0,11956,0,0,0,0, +0,11957,0,0,11959,0,0,0,0,0,0,0,0,11961,0,0,0,0,0,11978,0,0,0,11979,11980,11986, +11987,0,11992,0,0,0,0,0,11993,0,0,0,11994,0,11999,12004,12005,12006,0,0,0,0,0, +12011,0,0,12012,12014,0,0,12015,0,0,12019,12028,0,0,12029,0,0,12032,12033,0,0,0, +0,12034,0,12041,12043,0,0,12044,0,0,0,0,0,0,0,12046,0,0,0,0,0,0,0,12054,12055,0, +12056,0,0,0,12060,12064,0,0,0,0,0,12065,12067,12068,0,0,0,0,0,0,0,0,12074,0,0,0, +12075,12076,0,0,0,12079,0,12081,12086,12087,0,0,12088,0,0,0,0,12089,0,12092,0,0, +0,0,12097,0,0,0,0,0,0,0,0,12098,0,0,0,0,0,0,0,0,0,0,0,0,0,12102,12103,12104, +12111,0,0,12114,12116,0,0,0,12118,0,0,0,12119,12120,12128,0,0,0,0,12130,0,0,0,0, +0,0,12131,0,0,0,12132,12134,0,0,0,0,12137,0,12139,0,12141,0,0,12142,0,0,0,12144, +0,0,0,0,0,12145,0,12148,0,12153,0,0,0,0,12154,12171,12173,0,0,0,12175,0,0,0,0, +12178,0,0,0,0,0,0,0,12183,0,0,0,0,0,0,0,0,12184,0,0,0,12186,0,0,0,0,0,12187, +12188,0,0,12189,0,12196,0,12197,0,0,12198,0,12201,0,0,0,0,12203,0,12209,0,0,0,0, +12210,12211,12212,12213,0,12217,12218,0,0,0,0,0,0,0,0,0,12222,0,0,0,0,0,0,0, +12223,0,0,12229,0,0,0,0,12233,0,0,0,0,12234,0,0,12236,12242,0,0,0,12243,0,0,0, +12244,12253,0,12254,12256,0,12257,0,0,12275,0,0,0,0,0,12277,0,0,0,0,0,12278,0, +12289,0,0,12290,0,12292,12293,0,0,12294,0,12295,0,0,12296,0,12297,0,12298,0,0,0, +0,12301,0,0,0,0,0,0,0,0,0,0,0,0,0,12309,0,12338,12340,0,0,0,0,12341,0,0,0,0,0,0, +0,0,12342,12343,0,12344,0,0,0,0,0,0,0,0,0,12345,0,0,0,0,0,0,0,0,12346,0,0,0,0, +12348,0,0,0,0,0,0,0,0,0,0,0,0,12350,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,12351,0,12355, +12356,12357,0,0,12367,12370,12371,0,0,0,0,0,12372,12376,0,0,0,0,0,0,0,0,12379,0, +12382,0,12383,0,0,12384,0,0,0,0,12393,0,0,12394,0,0,0,0,12398,12403,0,0,12404,0, +0,0,0,0,0,0,0,0,0,0,0,0,12410,0,0,0,12411,0,0,0,12412,0,0,0,0,12420,0,12421,0,0, +0,0,0,12423,0,12425,12429,0,0,0,12431,12432,0,0,0,0,0,0,0,0,0,0,0,0,12434,0,0,0, +0,0,12435,12436,0,0,0,0,0,0,0,0,12437,0,0,0,0,0,12438,0,0,0,0,0,0,0,0,12445,0,0, +0,12450,12451,0,0,0,0,0,0,0,0,12452,12475,0,0,12493,12494,0,0,0,12495,0,0,0,0, +12496,12502,12509,0,0,0,0,12510,0,12512,12513,0,0,0,0,12514,0,0,0,12515,0,12520, +0,0,0,12524,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,12527,0,0,0,12528,0,0,0,12529,0,0,0, +0,0,12530,0,12535,0,0,12536,0,12538,0,0,0,0,0,0,0,0,0,0,0,0,12540,0,12548,0,0,0, +0,0,12550,0,0,0,12551,12552,0,0,0,12554,0,0,0,0,0,0,0,0,12555,0,0,12562,0,12565, +0,12566,0,0,0,0,0,0,0,0,0,0,0,0,12569,0,0,0,12571,12574,0,0,0,0,0,0,0,12577,0,0, +0,0,0,0,0,12578,12579,12603,0,12608,0,0,12611,0,12612,0,12615,0,12625,0,0,0,0, +12627,12646,0,12648,0,0,12657,0,0,0,0,0,0,0,0,0,0,0,0,0,0,12670,0,0,12671,0, +12673,12677,0,0,0,0,0,0,0,0,0,0,0,12679,0,12681,0,12682,12693,0,12694,0,12697,0, +12701,0,0,0,12703,12704,0,0,0,0,12707,12737,0,0,12739,0,0,12740,0,0,12742,12743, +0,0,0,0,0,0,0,0,0,12745,0,12746,12747,0,12748,0,0,12759,12767,0,0,0,0,12773,0, +12774,12778,0,0,0,0,0,0,0,12779,0,0,0,0,0,12780,12793,0,12824,0,12825,0,12836,0, +0,0,0,12839,0,12842,0,0,0,0,0,0,0,0,0,0,0,0,12843,12845,0,12846,0,0,0,0,12847,0, +0,12850,12852,12853,0,0,0,12854,0,0,0,12855,0,12856,0,12858,0,0,12859,0,12862,0, +12863,0,0,12866,0,12869,12872,12873,0,0,0,0,0,0,0,0,0,12875,0,12877,0,0,12878,0, +0,0,0,0,0,0,0,0,12884,12885,12888,0,12889,0,0,0,0,12893,0,0,0,12895,12896,12898, +0,0,0,0,0,0,0,12902,0,12909,12910,0,12926,0,12928,0,0,0,12929,0,12930,0,0,0,0, +12931,0,12932,12933,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,12934,0,12942,0,0,0,0,12944, +0,0,0,0,0,0,0,0,12946,0,0,12948,0,0,12949,0,0,0,0,12950,0,0,0,0,12951,0,12952,0, +12953,0,0,0,12954,12958,12959,0,0,0,0,0,12960,12964,0,0,0,0,0,12966,0,0,0,0,0,0, +0,0,12970,0,12971,0,0,0,0,0,0,12972,0,0,12982,0,0,0,12984,12985,0,12986,12996, +12997,13001,13002,0,0,0,0,13004,0,0,13005,0,0,13007,13009,0,13017,0,0,0,13020,0, +13021,0,0,0,0,0,0,0,0,0,0,13022,0,0,0,0,0,0,0,0,13024,13027,0,0,0,0,0,13028,0,0, +13029,0,0,0,0,0,0,0,13032,0,13037,0,0,0,0,0,0,13040,0,0,13041,0,0,0,13043,13044, +13046,0,0,0,0,13047,0,0,0,0,0,0,0,13049,13054,0,13056,0,0,13060,13061,0,0,0,0,0, +13067,0,0,13068,0,13071,0,0,0,0,0,13077,13078,0,0,0,0,0,13079,13080,13081,0, +13082,0,0,0,13085,0,0,0,0,0,0,0,13086,0,13087,13088,0,0,0,0,0,13094,0,13099,0, +13100,0,0,0,13101,0,13125,13126,13128,13129,0,0,13130,0,13131,0,0,0,0,0,0,13134, +0,0,0,0,0,0,0,0,0,0,0,13150,0,13168,0,0,0,0,0,0,0,0,0,13169,0,0,13170,0,0,0,0, +13174,0,0,0,13176,0,0,0,0,0,13177,0,13178,13183,13187,0,0,0,13189,0,0,13190,0,0, +13191,0,0,13206,0,0,0,13207,0,0,0,0,0,0,0,0,0,0,13212,0,0,13219,13232,0,0,0, +13241,0,13249,13253,0,0,0,0,0,13255,13259,0,13260,13261,0,13262,0,13272,0,0,0,0, +13276,0,0,0,0,13277,13299,0,0,13301,13302,0,0,13303,0,0,13305,0,13310,0,0,0, +13311,0,0,0,0,13325,0,13328,0,0,0,13329,0,0,0,0,0,0,13330,0,0,13331,0,13335,0,0, +13342,0,0,0,0,0,13343,0,13354,0,13362,0,13366,13367,13369,0,0,13371,13372,0, +13373,13374,0,13376,0,13380,13381,13386,0,13387,13388,0,13389,13391,13395,0,0,0, +0,0,13401,13409,0,13410,0,0,0,0,13420,0,0,0,0,0,13422,0,0,0,0,13423,0,0,0,0, +13425,0,0,0,0,0,13427,0,0,0,13428,0,0,13430,13438,0,13439,0,13445,0,13448,13449, +0,0,0,0,0,0,13451,0,13457,0,0,0,0,13458,13459,0,13460,0,0,0,0,13464,13465,13466, +13470,0,13471,13472,13474,13475,0,13476,0,0,13478,13479,0,13481,0,0,0,0,13487,0, +13490,0,13493,0,0,13494,0,0,13495,0,0,0,0,0,0,0,0,0,0,0,0,0,0,13496,13497,0, +13500,0,0,13516,13522,0,0,13525,13528,0,0,0,13530,13535,0,13537,13539,0,13540,0, +13543,0,13544,0,0,0,0,0,0,13545,0,0,0,0,0,0,13547,0,0,0,13549,13555,0,0,0,13556, +13557,0,0,0,0,0,0,0,13558,0,13563,0,0,0,0,13564,0,0,0,0,0,0,0,0,13566,0,0,0,0,0, +0,13569,0,0,13571,0,0,0,0,13573,0,0,0,0,0,0,13578,0,0,0,0,0,0,0,0,0,0,13581,0, +13586,0,13595,0,13600,0,0,0,0,0,0,0,0,13601,13603,0,13604,13605,13606,13607,0,0, +13617,13618,0,0,0,0,0,0,0,13623,0,13625,13627,0,0,0,0,0,0,0,0,13629,0,0,0,13634, +0,0,0,13638,0,0,0,0,0,0,0,0,13654,0,0,0,0,0,0,0,0,0,0,13656,0,13659,0,0,13660,0, +0,13662,0,0,0,13663,0,13664,0,0,0,0,0,13668,0,13669,13671,0,0,13672,0,0,0,0,0,0, +13675,13685,0,13686,0,0,0,13687,0,0,0,13692,13694,13697,0,0,0,13702,0,0,0,0,0, +13705,0,0,0,0,13707,0,0,0,13714,0,0,0,0,0,0,0,0,0,13715,0,13716,13717,0,0,13719, +13724,13730,13731,0,0,0,0,0,0,0,0,13732,0,0,0,0,0,0,0,13734,0,13736,0,0,13737, +13738,13747,0,13751,0,0,13752,0,0,0,13753,0,13757,0,0,13762,13763,0,13764,13765, +0,13766,0,0,13767,0,0,0,13768,0,0,0,0,0,0,0,13769,0,0,13772,0,13775,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,13776,13778,13787,0,0,0,13797,0,13798,0,13801,0,13804, +13806,0,0,0,0,13816,13817,0,0,0,0,0,0,0,0,0,0,0,0,0,13834,0,13836,0,0,13838,0,0, +13839,0,13840,0,0,0,0,13842,0,0,0,0,0,0,13843,0,0,0,0,0,0,0,0,0,13845,0,0,0,0,0, +13858,0,0,13860,0,0,13861,0,0,13862,13863,0,13868,0,13869,13870,0,0,0,0,0,0,0,0, +0,0,13872,0,0,0,0,13873,13878,0,0,0,0,0,0,0,0,0,0,13886,0,13888,13889,13890,0,0, +13891,13894,0,13897,13899,13900,13904,0,0,13906,0,0,0,13909,0,0,0,13910,0,0,0, +13911,0,0,0,0,0,13912,13917,0,0,0,0,13918,0,13919,0,0,13920,0,0,0,13921,0,0, +13922,0,0,0,0,0,0,0,13924,0,13927,0,0,0,0,0,13932,0,13933,0,13934,0,0,13935,0, +13944,0,0,0,13954,0,0,13955,0,0,0,0,13956,0,13957,0,13967,13969,0,0,0,0,0,0,0,0, +0,0,0,0,13970,13990,0,13991,13994,0,13995,0,0,0,0,13996,0,0,13999,0,0,0,14018,0, +14019,0,14021,0,0,0,0,0,0,14041,0,0,0,0,0,0,0,0,14043,0,0,0,0,14046,0,0,0,14048, +14049,0,0,0,0,0,0,0,0,0,0,14051,0,0,14052,14056,0,14063,0,14064,14066,0,0,14067, +0,0,0,0,0,0,0,0,0,14068,0,0,0,14072,0,14074,14075,0,14076,14079,14085,14086, +14087,14093,0,0,0,0,14095,0,0,0,0,0,0,14096,14097,0,0,0,0,0,0,0,14098,0,14102,0, +0,0,0,0,14103,0,0,0,14104,0,0,14105,0,0,0,14107,14108,0,0,14109,0,0,0,0,0,0,0,0, +14117,0,0,0,0,14118,0,0,0,0,14119,0,0,14120,0,0,14121,0,14122,14127,0,14128, +14136,0,0,14138,0,14140,0,0,0,14141,14142,0,0,0,0,14146,0,0,14149,0,14151,0,0,0, +14152,0,0,14153,0,0,0,0,0,0,0,0,0,14154,0,14156,14157,0,0,14159,0,14161,0,0,0,0, +14162,0,0,0,0,0,0,14163,0,0,14173,0,0,0,0,0,0,14174,0,0,14176,0,0,14178,0,0, +14179,14181,0,0,14182,14185,14187,0,14190,0,0,14197,0,0,0,0,0,0,0,0,0,0,0,0, +14198,0,0,0,0,0,0,14199,14200,0,0,0,14204,0,0,14208,0,0,0,0,0,0,0,0,0,0,0,14231, +0,0,0,0,0,0,0,0,0,14234,0,0,14235,0,0,0,14240,14241,0,0,0,14246,0,0,0,14247,0, +14250,0,0,14251,0,0,14254,0,0,14256,0,0,0,14260,0,14261,0,0,0,0,14262,14267, +14269,0,0,14277,0,0,14278,0,14279,14282,0,0,0,14283,0,0,0,14284,14285,0,0,0,0, +14286,0,0,0,14288,0,0,0,14289,0,14290,0,14293,14301,14302,14304,14305,0,14307,0, +14308,14309,0,0,0,0,0,0,0,0,0,0,0,14311,14312,0,0,14317,0,0,0,0,0,0,0,14318,0,0, +0,0,14320,0,0,0,0,14321,14322,0,0,0,0,0,14326,14329,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +14330,14331,0,0,0,0,14332,0,0,0,14333,0,0,14337,14340,0,14341,0,0,14342,0,14345, +14346,0,0,14347,0,14362,0,0,0,0,0,14364,14365,14371,0,14373,0,0,14374,0,14379,0, +14400,0,0,0,0,0,14401,0,0,14405,0,14406,0,14408,14409,0,0,0,14417,0,0,14424,0,0, +0,0,0,0,0,0,0,14430,0,0,0,14431,0,0,14435,0,14440,0,0,0,0,0,0,14442,0,0,14443,0, +0,0,0,0,14446,0,0,0,0,0,0,0,14454,0,14457,0,14460,0,0,14466,0,0,0,0,0,14467,0,0, +0,0,0,0,14469,0,14477,0,0,0,0,0,0,14478,14482,0,0,0,14483,0,0,0,14485,14486,0,0, +0,14487,14488,14489,14492,14493,14494,14495,14496,14497,0,14499,0,14501,0,0,0,0, +0,0,0,0,0,0,14502,0,14507,14512,14513,14514,0,0,0,0,0,0,0,0,0,0,0,14515,14526, +14530,0,14537,0,14544,0,14547,0,0,14548,14550,14551,0,0,14552,0,0,0,14553,0, +14554,0,0,0,0,14556,14564,0,0,14565,14566,0,0,0,0,0,0,14568,0,0,14569,0,0,0, +14571,14576,0,0,14577,14578,14579,0,0,14580,0,0,0,0,14582,0,0,0,0,0,0,0,0,0,0,0, +0,14583,0,0,0,0,0,14587,0,14588,0,0,14600,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,14601,0,0,14604,14605,14611,0,14613,0,0,0,0,14615,0,0,0,0,0,0,14627,0,14628,0, +0,0,0,14631,0,14633,14634,0,0,0,0,14635,0,0,0,0,0,0,0,0,14636,0,0,14639,14642,0, +0,0,0,14644,0,0,0,0,14645,14646,0,14653,0,0,14654,0,14658,0,14661,0,0,0,14665,0, +0,0,14668,0,0,0,0,0,0,0,0,0,14669,0,0,14670,0,0,0,14680,0,0,14681,0,0,0,0,0, +14682,14683,0,0,0,0,14686,0,0,0,0,14687,14697,0,0,0,0,14699,14705,14711,0,0,0,0, +0,0,0,0,0,0,14712,0,0,0,14713,0,0,0,0,14719,0,14720,14721,14726,0,0,0,14728, +14729,0,0,0,0,14731,0,0,0,0,0,0,0,14733,14736,14737,0,0,14740,14742,0,0,0,14744, +14753,0,0,0,0,14755,14758,14760,0,0,0,0,0,14761,14762,14765,14771,0,14772,0, +14773,14774,0,0,14775,0,0,14776,0,0,0,0,14777,0,14779,0,0,14782,0,0,14785,14786, +14788,0,0,0,0,0,14795,0,0,0,0,0,0,14798,0,14803,14804,14806,0,0,0,14809,0,0,0,0, +0,0,14810,0,0,0,0,14811,0,14812,0,0,0,0,0,14815,0,0,0,0,0,0,0,0,14816,0,14818,0, +0,0,0,0,0,14819,0,14820,0,14823,0,0,0,14824,0,0,14826,14827,0,0,0,0,0,0,0,0,0,0, +0,0,14830,0,0,0,0,0,14833,0,14845,0,0,0,0,0,14846,0,0,14847,14871,0,14873,0, +14876,0,14877,14878,14880,0,0,0,0,0,14881,0,14882,14894,0,0,0,0,14895,0,14907,0, +14908,0,0,0,0,0,0,0,14911,0,0,0,0,14920,0,0,14931,0,14932,14934,14935,0,0,14936, +0,14945,0,0,0,0,0,0,0,14947,0,0,14948,14949,14951,0,0,14952,0,0,0,14964,14973,0, +0,14990,0,0,0,0,14995,0,0,14998,15001,0,0,15002,15020,0,0,0,0,0,0,15021,0,15022, +0,0,0,0,15023,0,0,15025,15029,15033,0,0,0,15034,0,0,0,15035,0,0,0,0,0,15043, +15044,0,0,0,15045,15046,15048,15050,0,15065,0,0,0,0,15066,0,0,15075,15082,15084, +0,0,15085,15086,0,0,0,0,0,0,0,0,15088,0,0,0,15089,0,0,0,0,15094,0,15096,0,15097, +0,15100,0,0,15102,0,0,0,0,0,0,0,0,15105,0,0,15106,0,15109,15113,0,0,0,15115,0, +15118,0,0,0,0,0,0,15119,0,0,15120,0,0,0,0,0,15123,15129,0,0,0,15130,0,15131,0,0, +15134,0,15135,0,0,0,15137,15138,0,0,0,0,0,0,15139,0,0,0,0,0,15140,0,0,15154, +15162,0,15169,15170,0,15175,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,15177,0,15178,15179,0, +0,0,0,0,15183,0,0,0,0,0,0,0,0,0,0,0,0,15185,15187,0,15194,15195,15196,0,0,0,0,0, +0,0,15204,0,0,0,0,15206,0,0,0,0,0,15207,0,0,0,0,0,0,0,0,0,15213,0,15214,0,0,0,0, +0,0,0,15232,0,0,0,0,15234,0,15238,15240,0,15248,0,0,0,0,15250,15251,0,0,0,0,0,0, +0,15252,0,0,0,15255,15262,15266,0,0,0,15267,0,0,0,15277,15279,0,0,0,15280,15281, +15282,0,0,0,0,0,15285,0,0,0,0,15289,0,0,15291,0,0,0,0,0,0,0,15296,15297,0,0, +15304,0,0,0,0,15306,0,0,0,0,0,0,15307,15308,0,15309,0,0,15311,0,0,15312,15313,0, +0,0,0,0,0,0,0,0,0,0,0,15314,15317,0,0,0,15318,15319,0,0,0,0,15320,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,15321,0,0,0,0,0,15324,0,15325,15326,0,15330,0,0,0,0,15334,0, +15335,0,15341,0,0,15342,0,0,15343,15344,0,0,0,0,15345,0,0,0,0,15347,0,0,15348, +15349,15350,0,15356,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,15357,0,15358,0,0,0,0,0,0,0, +15359,15360,15364,0,15380,0,0,0,0,0,15392,0,0,15393,0,15395,0,0,0,0,0,0,0,0, +15396,0,0,15397,15398,0,0,0,0,0,0,0,0,0,15399,0,15400,0,0,0,15402,0,15405,15410, +0,0,0,0,15411,0,0,0,15412,0,15416,0,0,0,0,0,0,0,15428,0,15435,0,0,15438,0,0,0,0, +15439,0,0,0,15440,0,0,0,15441,15449,15451,0,0,0,0,0,0,0,15452,0,0,15455,0,0,0, +15456,0,0,15458,0,15460,15461,0,0,0,0,0,15462,15464,0,15465,0,0,15466,0,0,15467, +0,0,0,0,0,15468,0,0,0,0,15481,0,0,15484,0,15485,15486,0,0,0,15487,0,0,0,0,0, +15488,0,15492,15498,0,0,0,15499,0,0,0,15500,0,15501,0,0,15512,0,15522,0,0,0, +15524,0,15525,15526,0,0,15527,0,0,15545,15546,0,15548,15552,0,15553,0,0,0,15554, +0,15555,0,15557,15565,15573,15577,15578,0,15582,0,15583,0,0,0,0,0,0,0,0,0,0,0,0, +0,15586,0,0,0,0,0,0,0,0,0,0,0,0,0,0,15588,0,0,0,0,0,15589,0,0,0,0,0,0,0,15593, +15594,0,0,0,0,15595,0,0,0,0,0,0,15596,0,0,0,15597,0,0,0,0,15600,0,0,15601,0,0,0, +0,15602,15603,0,0,0,0,0,0,15604,0,15609,0,0,15612,0,0,15613,0,0,15615,15617, +15618,0,0,15620,0,15636,15637,0,0,15649,0,0,0,0,0,0,0,15650,0,0,15651,0,0,0, +15656,0,15658,0,0,0,15664,0,0,15665,0,0,15668,0,0,0,0,0,15669,0,0,15674,0,0, +15675,0,0,0,0,15676,0,0,0,0,0,0,0,0,0,0,0,15677,0,0,0,0,15678,0,0,0,0,0,15679,0, +0,15681,0,15686,0,0,0,0,15687,0,15688,0,0,15690,0,0,0,15697,0,15699,15700,0,0,0, +0,0,0,0,0,0,15701,0,15702,15703,0,15704,0,15705,0,15707,0,15709,0,15712,15716,0, +15717,0,15718,15720,0,0,0,0,0,15724,0,0,0,15725,0,15726,0,0,0,15740,0,15745, +15746,0,0,15747,0,15748,0,0,0,0,0,15749,0,0,0,15752,0,15753,0,0,0,0,0,0,15759,0, +0,0,15765,0,0,0,0,0,0,0,0,0,15767,0,0,0,15771,0,0,15784,0,0,0,0,15785,15790, +15791,0,0,15792,0,0,0,15807,0,15811,0,0,0,0,0,0,0,0,0,0,0,0,15818,0,0,0,15819,0, +0,0,0,15821,0,0,0,0,0,15822,15824,0,0,15827,0,0,15829,15831,0,15832,0,0,15833,0, +15835,15838,15839,15843,0,0,0,0,0,0,0,0,0,0,0,15844,0,0,0,0,15845,15851,15856,0, +0,0,0,0,0,0,15858,15860,0,15861,0,0,0,15864,0,0,0,0,15865,0,0,0,0,0,0,15866,0, +15872,0,0,15876,0,0,0,0,15877,15878,15883,15885,0,0,15888,0,0,0,0,0,15889,15890, +0,0,0,0,0,0,0,0,15892,0,0,0,0,0,0,0,15893,0,0,15894,0,0,0,15895,0,15896,15897,0, +15898,15901,15902,0,15911,15915,0,15916,0,15924,15935,0,15937,0,0,0,0,0,15950,0, +0,0,0,0,0,0,15958,0,0,0,15961,0,0,15966,0,15967,0,0,15977,0,0,15978,0,0,15981, +15982,15983,0,0,0,0,0,0,0,15986,0,0,0,15990,0,15991,15995,15998,0,15999,0,16000, +0,0,0,0,16008,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,16009,16011,0,16013,0,0,0,0, +0,0,0,0,16014,0,0,16015,16023,16024,16025,0,0,16026,0,16030,0,16032,0,16033,0,0, +0,0,0,0,16035,16036,16037,0,0,0,0,0,16039,0,0,0,0,16041,0,0,0,0,0,16043,16044,0, +0,16047,0,0,0,16048,0,0,16049,16050,16052,0,0,0,0,0,16055,0,0,0,0,0,0,0,0,16056, +0,0,0,0,0,0,0,16058,16060,16061,0,0,16063,0,0,16064,0,0,0,16067,16068,0,0,16069, +16078,0,0,0,16079,0,0,0,16080,0,16081,0,0,0,16088,0,0,0,0,0,0,0,0,0,0,0,16089, +16093,0,16097,0,16103,0,16104,16105,0,0,16256,0,0,16259,0,0,0,0,0,0,0,16260, +16261,0,0,16262,0,0,16263,0,16268,0,0,0,0,0,0,0,16269,0,0,16270,16273,0,16274,0, +0,0,0,16275,16276,16277,16280,0,0,0,16281,16284,0,0,0,16286,0,16289,0,0,0,0,0,0, +0,0,0,16290,0,0,0,0,16291,0,0,0,0,0,0,0,16292,0,0,0,0,0,0,0,0,16293,16295,16297, +0,16302,0,16304,0,16305,0,16306,0,0,0,0,0,0,0,0,0,0,0,0,16307,16308,16312,0,0,0, +0,0,0,16313,16315,0,16318,0,0,0,16321,0,0,0,0,0,0,0,16326,16333,16336,0,0,0,0, +16337,16340,0,0,0,0,0,16345,0,0,16346,0,0,0,0,0,0,0,0,0,16347,0,0,16348,0,0,0,0, +16349,0,0,0,16350,0,16357,0,0,0,0,16359,16360,0,0,0,0,16362,16363,16364,16365,0, +0,16366,0,0,0,0,16367,16368,0,16369,16374,0,0,0,0,0,0,0,16376,0,0,0,0,16378, +16379,0,16380,0,0,0,16381,16383,0,0,0,0,0,16390,0,0,0,16399,0,16402,16404,16406, +16407,0,0,0,16409,16411,0,0,0,0,16412,0,16413,16415,16423,0,0,0,0,0,16424,0,0,0, +16428,16434,16435,16449,0,16450,16451,0,0,0,16453,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +16454,0,0,16456,16458,0,0,16459,0,0,16460,0,0,0,0,16462,0,16463,0,0,16466,0,0,0, +0,0,16479,0,0,16480,0,16481,16484,0,0,0,0,0,0,0,0,0,0,16485,0,0,0,0,0,0,16489,0, +0,0,0,0,16491,0,0,16498,0,0,16503,0,16505,0,0,0,0,0,0,0,0,16506,0,0,0,16508, +16509,0,0,0,0,0,0,0,0,16511,16513,0,0,0,16516,0,16517,0,16519,0,16529,0,0,16531, +0,0,0,0,0,0,16534,0,0,16541,16542,0,0,0,0,0,0,0,0,0,16543,16547,16548,0,0,0, +16551,0,16552,0,0,0,16553,0,0,16558,0,0,16562,16565,0,0,0,16570,0,0,0,16573, +16585,0,0,0,16586,16587,16595,0,16596,0,16598,0,0,0,16600,0,0,0,0,0,0,0,0,0,0,0, +0,0,16601,0,0,0,0,16603,0,0,0,0,0,0,0,16604,16612,0,0,0,0,16613,0,16618,0,0,0, +16640,0,0,16641,0,0,0,0,0,0,16645,0,0,0,0,16646,0,0,0,0,0,0,16651,0,0,0,0,16653, +16654,0,0,0,16655,0,0,16656,16667,0,0,0,0,16671,0,16672,0,0,0,16673,0,0,0,0,0, +16676,0,16686,0,0,0,0,16689,0,16690,0,16692,0,16693,0,16694,0,16696,0,0,0,16705, +0,0,0,0,0,0,16707,0,0,0,16709,0,0,0,0,16711,0,16712,16713,0,0,0,16715,0,0,0,0, +16716,0,0,0,0,0,0,0,0,0,16718,16724,0,0,16726,16727,0,0,0,0,0,0,0,16728,0,16729, +0,0,16730,0,0,0,0,0,16731,0,0,0,16732,0,0,0,0,16734,16738,0,0,0,0,0,0,0,0,16743, +0,0,16745,0,0,0,0,0,16749,0,16752,0,0,0,0,16756,0,0,16758,0,16759,0,0,0,0,0, +16760,0,0,0,0,0,0,0,16762,0,16769,0,16770,0,16772,0,0,0,16777,16780,0,0,0,0,0,0, +16781,0,0,16782,0,16784,0,0,16785,16787,16792,0,0,16794,0,0,0,16798,0,0,16809,0, +0,16814,16816,16817,0,16819,0,0,0,0,0,0,0,0,0,0,16820,0,0,16836,16839,0,0,16841, +16851,16857,0,0,16858,16859,0,0,16860,0,0,0,0,0,0,0,0,16862,0,16863,0,0,0,0,0,0, +0,16864,0,0,0,0,0,0,0,16876,0,16881,16882,0,16885,16886,0,16887,0,0,0,16889, +16891,0,0,0,0,0,16894,16895,0,0,0,0,0,0,0,0,0,0,0,16897,0,16898,0,0,0,0,0,16913, +0,0,16924,16925,16926,0,0,16927,0,0,0,16937,16938,0,0,0,16940,16941,0,0,0,16942, +16945,0,16946,16949,16950,0,0,0,16952,16955,0,0,0,16965,0,16969,0,0,16975,0,0, +16976,0,0,0,0,16978,0,0,16981,0,16983,16989,0,0,0,0,16990,0,0,16991,0,0,0,16993, +0,16994,16996,17000,0,0,0,0,0,17002,17004,0,17006,0,0,17007,0,0,0,0,17008,17013, +17014,0,0,0,0,0,0,0,0,0,17021,0,17031,0,0,0,0,0,17033,17036,0,17038,0,0,17039,0, +17045,0,0,17046,17047,0,0,0,0,17048,0,17049,17050,0,17051,17053,0,17054,0,17055, +0,0,0,0,0,17063,0,0,17064,0,0,0,0,0,0,0,17065,0,0,17068,0,0,0,0,0,17072,0,0,0,0, +0,0,17073,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,17074,0,17080,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,17081,17083,17084,0,0,0,17085,0,0,0,0,17092,0,0,0,0,0,0,0, +0,0,17093,0,17095,17102,0,0,0,0,0,0,17103,0,0,17105,0,17107,0,0,0,0,17114,0,0,0, +0,0,17115,17125,17127,0,0,17128,0,0,0,17129,17130,0,17131,0,0,0,0,0,17132,17135, +17145,0,0,0,0,0,0,0,0,17146,0,17147,0,17148,0,0,0,0,0,0,17149,17150,0,17151, +17153,0,17155,0,0,0,0,17163,17171,0,17174,0,0,0,0,17179,0,0,17182,17185,0,0,0,0, +0,17186,0,0,17188,0,0,0,0,0,0,0,17189,17191,0,17194,0,0,0,0,0,0,0,0,0,17195, +17196,17203,17204,0,0,17205,17217,0,0,0,0,0,17218,0,0,0,0,17219,0,17220,0,17221, +0,0,17230,0,0,0,0,0,17236,0,17238,17239,0,0,0,17241,17244,0,0,17245,0,17248,0,0, +17251,0,17252,0,0,17264,0,17266,0,0,0,17268,0,0,0,0,17271,17272,0,17273,0,17295, +0,17302,0,17305,0,0,0,17306,0,0,0,0,0,0,0,17308,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +17309,0,17310,17313,0,0,0,0,17314,17315,0,17317,0,0,0,0,17318,0,0,0,0,0,0,0, +17320,0,0,0,0,0,0,17334,0,17344,17348,0,0,0,17350,17351,0,0,17353,0,0,17354,0,0, +0,0,0,0,0,0,0,17355,0,0,0,0,0,0,17356,17357,0,0,17359,0,0,0,17371,0,17372,0,0,0, +17393,0,0,0,0,17394,0,0,0,0,0,17395,0,0,17399,0,0,0,17401,17417,0,17418,0,17419, +0,0,0,0,0,17422,17423,0,0,0,0,0,17424,0,0,0,0,0,17428,17429,17433,0,0,0,17437,0, +0,17441,0,0,17442,0,0,17453,0,0,0,0,0,0,0,0,17454,17456,17462,0,0,17466,0,0, +17468,0,0,17469,0,0,0,0,17470,0,17475,0,0,0,0,0,17479,0,0,0,17483,17484,0,17485, +0,17486,0,17491,17492,0,0,17493,0,17494,17495,0,0,0,17496,0,0,0,17497,0,0,0, +17502,0,0,0,0,0,17503,0,17505,0,17507,0,0,0,17512,17513,17514,0,0,17515,0,0,0, +17519,0,0,0,17522,0,0,17523,0,0,0,0,0,0,0,0,0,17527,0,0,0,17528,0,0,0,17534,0,0, +0,0,17536,0,0,0,17539,0,17540,17543,17549,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,17556, +0,0,17558,0,17559,0,0,17560,0,0,0,17563,0,0,0,0,0,0,17564,0,0,17565,17566,0, +17567,0,0,0,0,0,0,17569,17570,0,17575,0,0,0,0,0,0,0,0,0,0,0,17581,0,0,0,17582, +17583,0,17586,0,0,17587,0,0,0,0,0,0,0,17588,0,0,0,0,17596,17597,0,0,17598,17600, +0,0,0,0,0,0,17601,0,0,0,17604,0,0,17605,0,0,17607,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,17612,0,0,17618,0,17621,17622,0,0,0,0,17623,0,0,17624,0,0,17630,0,0, +17631,17633,17634,0,0,0,0,0,0,0,17635,0,0,17636,0,0,17637,0,17638,0,17640,0,0,0, +0,0,0,0,0,0,0,17641,0,0,0,0,0,0,0,0,0,0,17643,0,0,0,0,17645,0,0,0,0,0,0,0,0, +17646,17662,0,0,0,0,0,0,0,0,0,17663,17664,0,17665,17666,0,0,0,17669,17671,17673, +0,17679,0,0,0,0,0,0,0,17684,0,0,0,17686,0,17714,0,0,17720,17722,17726,0,0,17728, +0,0,17729,0,0,0,17732,0,17733,0,17734,0,0,0,17735,0,0,0,0,17737,0,0,0,0,17739,0, +0,0,17741,17742,0,0,0,0,17743,17744,17745,0,0,0,17749,0,17750,17751,17752,17754, +17761,17762,0,17763,0,17766,0,17772,0,0,0,0,0,17775,0,0,0,0,0,0,0,17776,0,0, +17777,0,0,17778,17779,0,17782,17783,0,0,0,0,0,0,0,0,0,0,17784,0,0,0,0,0,0,0, +17821,0,0,0,17822,0,0,0,17823,17825,0,0,0,0,0,17826,17831,17832,17833,0,0,17845, +0,0,0,17846,0,0,0,17848,17850,17854,0,17855,0,0,17859,0,0,0,0,0,0,17860,17861,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,17870,17871,0,0,0,0,0,0,17872,0,0,0,17879,0, +0,0,17881,17883,0,17884,0,17885,0,0,17886,0,0,17887,17891,17953,0,0,0,0,17954,0, +0,17955,0,17968,0,0,17972,0,0,0,0,0,17974,0,0,0,0,17976,17978,0,0,17983,0,0,0,0, +18003,0,0,0,0,0,18007,0,0,0,0,0,18009,0,0,0,0,0,0,0,18010,0,0,0,0,0,0,18012,0,0, +18014,0,0,0,18015,0,0,0,18016,0,18017,0,0,0,18030,0,0,0,0,0,0,0,18031,0,0,18036, +18037,18038,0,0,18049,18056,0,18057,18058,0,18059,0,0,0,0,0,0,0,0,18062,0,0,0,0, +18064,0,0,0,0,0,0,0,0,18067,0,0,0,18068,0,0,18075,0,0,18078,18093,18094,0,0,0,0, +0,0,0,0,18097,0,0,0,0,0,18098,18100,0,0,0,18108,0,18111,0,0,18112,0,18113,0,0, +18115,18116,0,18118,0,0,0,0,18121,0,0,0,0,18123,0,0,0,0,0,0,0,0,0,18124,0,0,0,0, +18125,18126,0,18127,0,0,18128,18135,0,0,0,0,0,0,0,0,0,18150,0,0,0,0,0,18151, +18152,0,0,18156,18164,0,18166,18171,0,0,0,0,0,0,0,0,0,18172,18183,0,18184,0,0,0, +0,18185,0,18187,0,0,0,0,0,18188,0,0,0,0,0,0,0,0,18189,0,0,18190,0,0,18191,18192, +0,0,18194,18195,18196,0,0,0,18197,0,18203,0,18204,0,0,0,0,18205,0,0,0,18207, +18208,0,0,18214,0,0,0,18215,18216,0,0,0,18220,0,0,18222,0,0,0,0,0,18223,0,18225, +18231,0,18234,0,18235,0,0,0,0,18240,0,0,18241,18242,0,0,0,0,0,18243,18251,0, +18253,0,18254,0,0,0,18266,0,0,0,0,0,0,18269,18270,18271,18273,18281,0,0,0,0,0,0, +0,0,0,0,0,0,18282,0,18283,0,18284,0,0,0,0,0,0,18285,0,18287,18289,0,0,18290,0,0, +0,0,18308,0,0,0,18310,0,0,0,0,0,0,0,0,0,0,0,0,18311,0,18312,18313,0,18315,0,0, +18316,18320,0,18331,0,18332,0,18336,0,0,0,0,18337,0,18340,0,0,0,0,0,0,0,0,0, +18341,0,18344,18345,0,18346,0,0,0,0,0,18348,0,18351,0,0,18356,0,0,0,0,0,0,18357, +0,0,0,0,0,18367,0,0,0,18368,0,18369,0,18370,18371,0,0,0,18437,18444,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,18445,18450,0,0,0,0,18451,0,18452,0,0,0,18453,0,0,0,0,0,18455,0, +0,0,18456,0,18457,0,18460,0,0,18461,0,0,0,0,0,0,0,0,18466,0,0,18467,0,0,0,0, +18473,0,0,0,18476,0,18477,0,0,0,18478,18479,18480,0,0,0,18485,0,0,0,18486,0,0,0, +0,0,0,18488,18490,0,0,0,0,0,0,18491,0,0,0,0,0,18495,0,0,18496,0,0,0,0,0,0,18505, +0,18521,0,18522,18523,0,0,0,18525,18526,0,0,0,0,0,18527,0,0,0,0,18532,18533,0, +18534,0,0,0,0,0,0,18535,18537,0,18538,0,0,0,0,0,0,18540,18541,18542,18543,0, +18546,0,0,0,0,18553,18556,0,0,18558,0,0,18569,18571,0,0,0,18572,0,18574,0,0,0,0, +18586,0,0,0,0,0,18588,0,0,18589,0,0,0,0,0,0,18590,0,18592,0,0,0,0,18594,0,0,0, +18596,0,0,18597,18598,0,0,18601,0,0,0,0,18602,0,0,0,18603,18604,0,18605,0,0,0,0, +18608,0,0,18611,0,0,0,0,0,0,0,0,0,18612,0,18616,0,0,18617,18619,0,0,0,18628,0,0, +0,18629,0,0,18630,0,0,0,0,0,0,0,18631,0,18632,0,0,18635,18637,0,0,0,0,0,0,18641, +18643,18648,0,18652,0,0,18653,0,18655,18656,0,0,0,18657,0,0,18666,18674,0,0,0,0, +18677,18684,18685,0,0,18686,0,0,18690,0,0,0,0,0,0,0,18695,18696,0,0,0,0,0,0,0,0, +0,0,18697,0,0,18700,0,0,0,0,0,0,18702,0,18708,0,0,18709,0,18710,0,0,18711,0, +18714,0,0,18718,0,0,0,0,0,0,18719,0,0,18722,0,18726,0,0,0,0,0,0,0,0,0,0,0,0,0, +18731,0,0,0,0,0,18739,18741,0,0,18742,0,18743,18744,18746,18748,0,18752,18753,0, +0,18754,18763,0,18765,0,0,0,18766,0,0,0,18769,0,0,0,0,0,18773,18778,18779,18781, +0,0,18784,18787,0,18788,0,18793,0,0,0,0,0,0,18795,0,0,18800,0,0,0,0,0,18801, +18804,0,0,0,0,0,0,0,18806,0,0,0,18811,18815,18816,0,0,0,0,18825,0,0,18827,18829, +0,0,18830,0,0,0,0,18831,0,0,18832,0,0,0,0,18833,0,18840,0,18841,0,18842,0,0,0,0, +18843,0,18844,0,0,0,0,0,0,18845,18846,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +18848,0,0,0,18853,18860,0,0,18862,18866,0,0,18867,18869,0,0,18874,18881,18891,0, +0,0,0,0,0,0,0,0,0,18892,0,0,0,0,0,0,0,0,18895,0,18896,0,0,0,18900,0,0,0,18901,0, +18902,18915,18916,0,0,0,0,0,0,0,0,18919,0,0,0,0,0,18920,0,0,0,18921,18929,0,0,0, +0,18930,0,0,0,0,0,0,18932,0,0,0,0,18934,18942,0,0,0,18951,18957,0,0,0,0,18958,0, +0,0,0,18959,18960,0,0,18961,0,0,18962,0,0,0,0,18963,18964,0,0,0,18965,0,18967,0, +0,0,0,0,0,0,0,0,18968,0,18969,0,18970,18973,18976,0,0,0,0,0,0,18977,0,0,0,18981, +0,0,0,18990,0,18998,0,0,0,0,0,18999,19003,0,0,19005,0,0,0,19006,0,0,0,0,0,0, +19008,19011,0,0,19018,0,0,19019,0,19024,0,19031,19032,0,19039,0,19041,19050,0,0, +0,19051,19055,19056,0,19059,19063,19064,0,0,19088,0,0,0,19093,19094,0,0,0,0, +19095,0,19096,0,0,0,19097,0,0,19098,0,19099,19100,0,0,19103,0,0,0,0,0,0,0,19111, +0,0,0,0,0,0,19112,0,0,0,19116,19117,0,19121,19122,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,19123,19124,0,0,0,0,0,0,0,19125,19126,0,19128,0,0,0,0,0,0,0,0,0,0, +19129,19130,19131,19132,0,0,19146,0,0,19147,19156,19158,0,0,0,0,0,0,0,0,19182, +19185,0,0,19187,0,0,0,19193,0,0,0,0,0,19194,0,19197,0,0,0,0,19198,0,0,0,0,0,0,0, +0,0,0,19202,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,19203,0,19205,19210, +0,0,0,19213,0,19218,0,0,0,19223,19229,0,0,19230,0,0,19231,19232,19233,19239,0,0, +0,0,0,19240,0,19248,19249,0,0,0,0,19254,0,19256,19258,19259,0,0,19261,0,19266,0, +0,0,19272,0,19278,19281,19282,0,0,0,0,0,0,0,0,0,0,0,0,19283,0,0,19284,0,0,19285, +19287,0,0,0,0,0,0,0,0,0,0,0,0,0,0,19288,19291,0,19292,0,0,0,0,19297,0,19298,0,0, +0,0,19302,19303,0,0,0,0,19304,19305,0,0,0,0,19314,0,0,19315,0,0,19321,0,0,0,0,0, +0,0,19322,0,19333,0,19334,19335,0,19336,19337,0,0,0,0,0,0,0,0,0,0,0,19346,0,0, +19353,0,19354,19362,0,19366,19367,0,0,19369,0,19375,0,19377,19380,19388,0,0,0,0, +0,19389,19390,0,0,0,0,19392,0,0,0,0,0,19402,0,0,0,0,0,0,0,0,19412,0,0,19413, +19422,0,19424,0,0,0,19425,0,0,0,19428,0,0,0,0,19431,0,0,0,0,0,19432,0,0,0,0,0, +19448,19459,0,0,19461,0,19462,19463,0,19467,19474,19482,0,0,0,0,19494,0,0,0,0, +19501,0,0,0,0,0,0,0,0,0,0,19502,19504,0,0,0,0,0,0,0,19505,0,0,0,0,19506,19507,0, +0,0,19508,0,0,19511,0,0,19514,0,19515,0,19516,0,19518,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,19530,0,19537,19538,0,19543,19546,0,19547,19551,0,0,0,0,0,0,19552, +19553,0,0,0,0,0,0,0,0,0,0,0,0,19555,0,0,19556,0,0,0,0,0,0,0,0,0,0,0,0,19560, +19561,0,0,19562,0,0,0,0,0,0,19565,19567,0,19568,0,0,0,19569,19570,0,19578,0,0,0, +0,19580,0,0,0,0,19581,19584,0,0,0,0,0,0,0,19585,19586,0,0,0,19587,19588,0,19589, +0,0,0,0,0,0,19592,19593,19599,0,19600,0,0,19604,0,0,19605,0,19606,19608,19610,0, +19613,19614,0,0,0,0,0,0,19616,19617,0,0,19618,0,0,19619,0,0,0,19620,19621,19631, +0,0,19632,19634,19636,0,19643,0,0,19644,19658,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,19659,0,0,0,0,0,0,0,0,0,0,0,19675,19677,0,0,0,0,19679,0,19683,0,19684,0,0, +0,0,0,0,19687,0,0,0,0,0,0,0,0,19688,19689,19692,0,0,0,0,0,0,0,19695,19697,0,0,0, +0,0,19698,19699,0,0,19700,0,19702,0,0,19703,0,0,0,0,0,0,19704,19708,0,19710,0, +19713,0,0,0,19715,0,0,0,0,19718,0,0,0,0,0,0,0,19720,0,19722,0,0,19725,0,0,0,0,0, +0,0,0,0,0,0,0,0,19730,0,0,0,0,0,19731,0,19734,19735,19739,0,0,19740,0,19741,0,0, +0,19746,0,0,19747,0,19771,0,0,0,0,0,0,0,0,19772,19775,0,0,0,0,0,0,19778,0,0,0,0, +0,19779,0,0,19780,19790,0,19791,0,0,19792,0,0,0,19793,0,0,19796,19797,0,0,0, +19799,0,0,0,19801,0,0,0,0,19803,0,19804,0,19805,0,0,19807,0,0,0,19808,0,0,0,0,0, +0,19809,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,19816,0,19821,0,19822,19830,19831,0,0, +0,19833,0,0,0,0,0,0,0,0,0,0,19838,0,0,0,0,19839,0,0,19843,0,0,0,0,19845,0,0,0,0, +19847,0,0,19848,0,19849,0,0,0,0,0,0,0,19851,0,0,0,19854,0,0,0,0,0,0,0,0,0,19864, +0,19865,0,19866,0,0,0,0,0,0,0,19868,0,0,19870,0,0,19871,0,0,19872,19873,19875,0, +19880,19882,19884,0,0,19885,19886,19888,0,0,0,0,0,0,0,0,0,0,0,0,19890,19892, +19893,0,0,19894,0,0,0,19895,0,19896,19902,0,0,19903,0,0,19905,0,0,0,19906,0, +19908,0,19909,19911,0,0,0,19913,19920,0,19938,19939,19940,0,0,0,0,0,0,0,19942,0, +19943,0,19945,0,0,0,19951,19952,19954,19960,0,19965,0,19971,0,0,0,0,0,19975,0, +19976,0,19990,0,0,19991,0,19993,0,19995,0,0,0,19998,19999,20001,0,20003,20005,0, +20011,20012,0,0,0,0,0,0,20014,0,20020,0,0,0,0,20021,0,0,0,0,0,20023,20024,0,0,0, +0,0,20025,0,0,20027,0,0,20029,0,0,20032,0,0,0,0,20044,20045,0,20048,20049,0,0, +20050,0,20052,0,0,20054,20057,0,0,0,0,0,0,0,0,0,20059,0,0,20061,0,20062,0,20064, +0,0,20066,0,0,20067,0,0,0,0,20069,0,0,0,0,0,0,20070,20071,0,0,0,0,0,0,0,0,0,0,0, +20072,0,0,20073,20074,0,0,0,0,0,20075,0,20078,0,0,0,0,20080,0,20081,0,0,0,0,0,0, +20095,0,20098,0,0,0,0,0,0,0,20107,0,0,0,0,0,0,0,0,20112,0,0,0,20113,20114,0,0,0, +20115,20123,20124,0,0,0,20131,20133,20134,0,0,0,0,20136,0,0,20137,20138,20150,0, +20152,0,0,0,20153,0,0,20154,0,0,0,20158,0,20163,0,0,20164,0,0,0,0,0,0,0,20166,0, +20168,0,20170,0,20175,0,0,20178,0,0,0,0,20223,0,0,0,0,20224,0,20226,0,0,20230,0, +20231,0,0,0,0,20232,0,0,20233,20234,0,20244,0,20247,0,0,0,0,0,0,20249,0,0,0, +20250,0,0,0,0,20251,0,20253,0,20254,0,0,0,0,20256,0,0,20264,0,0,0,0,20266,0,0,0, +20278,0,0,20279,20282,0,0,0,0,0,20283,0,20284,0,20285,0,20287,20290,0,0,0,0, +20292,0,0,0,0,20293,20297,0,0,0,0,0,0,20299,0,20300,20303,0,0,0,0,0,0,20307,0,0, +20308,0,20309,0,20310,0,0,0,0,0,0,20312,0,0,0,20314,0,0,0,0,20315,20316,0,20322, +0,0,0,0,0,0,20339,0,0,0,20342,0,0,0,0,20352,0,0,0,0,0,0,0,0,0,0,20362,0,0,20365, +0,20375,20377,0,0,0,0,0,0,0,0,0,0,0,20378,20379,0,20380,0,0,20381,0,20382,0, +20383,0,20388,0,0,0,0,0,0,0,0,0,0,0,0,0,0,20390,20392,20393,0,0,20395,0,0,0,0,0, +20396,0,0,0,0,0,0,0,0,20398,20415,0,0,0,20417,0,0,20420,0,0,20426,20428,0,20431, +0,0,20432,0,20433,20434,20435,0,0,0,0,20440,0,0,0,0,0,20442,0,20443,0,20446,0,0, +0,0,20448,0,20451,0,0,0,0,0,0,0,0,0,20452,20453,0,0,20454,0,0,0,0,0,0,20457,0, +20458,0,0,0,20465,0,0,0,0,0,20469,0,0,0,20473,0,20476,0,0,0,0,0,0,0,0,20477,0,0, +20485,0,0,20486,0,0,20487,0,20496,0,20497,0,0,20498,0,0,0,0,0,0,0,0,0,0,20499, +20500,0,20501,0,0,0,0,0,20520,20527,0,20529,0,0,0,0,20539,0,0,20540,0,0,0,20543, +0,0,0,20546,0,0,0,0,0,20548,0,0,20563,0,0,20564,0,20566,0,0,0,0,0,20589,0,0,0,0, +20590,0,0,20593,20594,0,0,0,0,20595,0,20597,20598,0,0,0,20618,20620,0,0,0,0, +20621,0,0,0,0,20627,0,0,0,0,0,20628,0,0,0,20629,0,20630,0,0,20639,0,0,0,0,0, +20707,0,0,20709,0,0,0,20713,20714,0,0,0,0,0,20724,20725,0,0,0,0,20726,20728, +20729,0,20733,0,20734,0,20735,20736,0,20737,0,0,20744,0,20745,0,20748,0,0,20749, +0,0,0,0,0,0,0,0,20750,0,0,0,0,20754,0,0,0,20761,0,0,20763,0,0,0,0,0,0,0,20766,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,20767,0,0,0,0,20768,0,20769,20777,0,0,0,0,0,0,20785,0, +0,0,20786,20795,20801,0,20802,0,20807,0,0,20808,0,0,20810,0,0,20811,0,20812,0,0, +0,0,0,20813,0,0,20818,20820,20821,0,0,0,20822,0,20823,0,0,0,20826,0,0,0,0,0,0,0, +20829,20830,20831,0,20832,20836,0,0,20839,0,0,20840,20842,0,20843,0,20844,0, +20854,0,0,0,20855,0,0,0,0,20856,0,0,0,20869,0,0,20871,0,0,0,0,0,0,0,20873,0,0,0, +0,0,20876,0,0,0,0,0,20880,0,0,20882,0,0,0,0,20883,20884,0,0,20890,0,0,0,0,0,0,0, +0,0,20891,0,0,0,0,0,20905,0,20906,20910,0,0,20912,20915,0,0,0,0,0,20916,0,20917, +0,20919,20920,20922,0,20927,0,20928,20929,20930,0,0,20935,0,0,20939,0,0,20941,0, +0,0,20943,0,0,0,20946,20947,0,0,0,0,0,20950,0,20954,0,0,20955,20964,0,0,20967,0, +0,0,0,0,20973,20975,0,0,0,20984,0,20987,20988,0,0,0,0,0,20989,0,0,0,20995,0, +20998,0,20999,0,0,0,0,21000,21001,0,0,0,0,21008,0,21010,0,21016,0,0,0,21017, +21018,0,0,0,0,0,21021,21026,21027,21028,0,0,21029,0,0,0,0,0,21030,0,0,0,0,0,0,0, +0,0,0,0,0,0,21031,21032,0,0,0,0,0,21037,0,0,21038,0,0,0,0,0,0,0,0,0,21039,0, +21041,0,21046,21047,0,0,0,21049,21053,0,0,21057,21064,21065,0,0,21066,21067,0,0, +0,21069,0,0,0,21071,21072,0,0,21073,0,21074,0,0,21078,0,0,0,0,21079,0,0,21080, +21081,0,0,21086,21087,0,21089,0,0,0,0,0,0,0,21091,0,21093,0,21094,0,0,0,0,0,0,0, +0,21095,0,0,0,0,0,21096,0,21098,0,0,0,0,0,0,0,21099,0,0,21100,21101,21102,0,0,0, +0,0,21103,0,21104,0,0,0,0,0,21105,21108,21109,0,0,21112,21113,0,0,0,0,0,0,21115, +21122,21123,0,0,0,0,0,21125,0,0,0,0,0,0,0,0,21129,21131,0,0,21134,0,0,0,21137, +21142,0,21143,0,0,21144,0,21145,21146,0,21152,21154,21155,21156,0,0,0,21160,0,0, +0,0,0,0,21161,0,21164,0,21166,0,0,0,0,21170,0,0,0,0,21171,0,0,21172,0,21174,0, +21175,0,0,0,0,0,21176,21179,21188,0,0,0,21189,0,0,21190,0,0,0,21192,0,0,21193,0, +0,0,21198,0,21212,0,0,21213,0,0,0,0,0,0,21215,21216,0,0,21223,21225,0,21226,0,0, +0,0,21227,21228,0,0,21229,0,0,0,0,21230,21236,0,0,0,0,0,0,0,0,0,0,0,0,0,21237,0, +0,21238,21239,0,0,0,0,21256,0,0,0,0,0,21257,0,0,0,0,0,0,0,21259,0,0,0,21263,0, +21272,0,21274,0,21282,0,0,0,0,0,0,0,0,21283,0,0,0,0,0,0,0,0,21294,0,0,21297,0,0, +0,0,21298,0,0,0,21299,0,21300,21302,0,21316,0,21318,21322,21323,0,21324,0,21326, +0,0,0,21327,21328,0,0,0,21352,0,0,21354,21361,0,0,0,0,0,0,0,0,0,0,0,0,0,21362,0, +0,0,21363,0,0,0,0,0,0,0,0,0,21366,0,0,21367,21372,21374,0,0,0,21375,21377,0, +21378,0,0,0,21380,0,0,0,0,0,0,0,0,0,0,21381,0,0,0,0,0,0,21382,0,21383,0,0,21384, +0,0,21385,0,0,0,0,21389,21390,0,0,0,0,0,0,0,0,0,0,0,0,0,21397,21398,0,0,0,0,0,0, +0,0,0,0,21399,0,21400,0,0,0,0,21402,0,0,0,21403,21404,0,21405,21406,0,0,0,21407, +0,0,0,0,0,0,0,0,0,0,0,0,21408,0,0,0,0,21409,0,21421,0,21422,0,0,0,21425,21428,0, +0,0,0,21429,0,0,0,0,0,21433,0,0,0,0,0,0,0,0,0,0,21434,0,21443,0,21444,21449,0, +21452,0,21453,21454,0,0,0,21457,0,0,21458,0,0,0,21460,21461,0,0,21464,0,0,0, +21473,21478,0,0,21479,0,0,21481,21483,0,0,0,0,0,0,0,0,21484,0,0,21485,21486,0,0, +21488,0,0,0,0,0,0,21523,0,0,21525,0,0,0,0,0,0,0,21526,0,0,0,0,0,0,21529,21530,0, +0,21531,0,0,21533,0,0,21539,21564,0,21567,0,0,0,0,0,0,0,0,21575,0,0,0,0,21577,0, +0,0,0,0,21591,0,0,21604,0,0,0,0,0,0,0,0,0,21605,0,21606,0,0,21617,21618,21619, +21620,0,0,0,0,0,0,0,0,0,0,0,0,0,21623,0,0,0,0,21631,0,21635,0,0,0,0,21639,21646, +21653,21662,0,0,21663,21664,0,21666,0,0,21667,0,21670,21672,21673,0,21674,21683, +0,0,0,0,0,21684,0,21694,0,0,0,0,21695,21700,0,21703,0,21704,0,0,21709,0,0,0, +21710,0,0,0,0,0,0,0,0,21711,0,0,0,21712,0,21717,0,21730,0,0,0,21731,21733,0,0,0, +0,21737,21741,21742,0,21747,0,0,0,21749,0,0,0,0,0,0,0,0,0,0,0,0,0,21750,0,0,0,0, +0,21752,0,0,0,0,21753,0,0,0,0,0,0,21755,21756,0,21757,0,0,0,0,0,0,21760,0,0, +21763,0,0,0,0,0,0,0,0,0,21764,0,0,21766,0,0,21767,0,0,0,0,0,0,0,0,0,21773,0, +21774,0,0,21775,0,0,0,0,21776,0,0,21777,0,0,0,0,0,0,0,0,0,21780,21787,21788, +21791,0,0,0,21797,0,0,0,0,0,21805,0,0,0,0,21806,0,21807,21809,0,21810,21811,0, +21817,21819,21820,0,21823,0,21824,0,0,21825,0,0,21826,21832,0,0,0,0,0,21833, +21848,21849,0,0,21867,21870,21871,21873,0,0,0,21874,0,0,0,0,0,0,0,0,0,21875,0, +21878,0,0,0,21879,0,21881,21886,0,0,0,0,21887,0,0,21888,21894,21895,21897,0, +21901,0,21904,0,0,21906,0,0,0,21909,21910,21911,0,0,21912,0,0,21913,21914,21915, +0,21919,0,0,0,0,0,0,0,21921,0,0,21922,21933,21939,0,0,0,0,0,0,0,0,0,0,0,21944,0, +0,0,0,0,21945,0,21947,0,0,0,0,0,0,0,0,0,0,21949,0,0,0,21950,0,0,0,0,0,0,0,0,0,0, +0,0,0,21951,0,21952,0,0,0,0,0,0,0,0,0,21954,21957,0,0,0,0,21958,0,21959,0,0,0,0, +0,0,21962,21963,0,0,0,0,0,0,0,0,21964,21965,0,0,21969,21970,0,0,0,21974,0,0, +21980,21981,0,21982,0,0,0,0,0,21985,0,21988,0,21992,0,21999,0,0,0,0,0,0,22001,0, +22002,0,0,0,0,0,0,22003,0,0,0,0,0,22004,0,0,0,22008,0,22009,22015,0,0,22016,0,0, +0,22017,22019,0,0,0,0,0,0,0,0,0,22020,0,0,0,0,0,0,0,0,0,0,22021,22037,0,22039,0, +0,0,22040,0,0,0,22048,22049,0,0,22053,22055,22056,22059,0,0,22060,22061,0,0, +22064,0,0,0,0,22066,0,0,0,0,0,0,0,22073,0,0,0,22074,22075,0,0,0,0,0,0,0,22076,0, +0,0,0,22077,22084,22099,0,0,0,0,0,0,0,22104,0,0,22107,0,22108,0,22109,0,22110,0, +0,0,0,0,0,0,22111,22119,0,22120,22122,0,0,0,0,22125,0,0,0,22128,22129,0,0,0,0,0, +0,22141,0,0,0,22142,0,0,22144,22146,0,22148,22149,22151,22154,0,0,0,22162,0,0,0, +0,22164,22177,0,0,0,0,22179,0,22182,22183,0,0,22184,22188,0,0,0,0,0,0,0,0,22190, +0,22194,22201,0,0,22208,0,22209,0,22212,0,0,22215,0,22223,22231,0,0,22232,0, +22234,0,0,22235,22236,0,22237,0,22240,0,0,0,0,0,22241,0,0,0,22242,22246,22247,0, +0,0,22259,22268,0,22269,0,0,0,0,0,0,0,22270,0,0,0,0,22271,0,22272,0,22277,0,0,0, +0,0,22278,22280,22283,22286,0,0,22287,22289,0,0,22290,0,22293,0,0,0,0,0,0,0,0,0, +0,22295,0,22301,22302,0,0,0,22305,0,22308,0,0,0,0,0,0,0,0,0,0,22315,0,0,0,22317, +0,22334,0,0,0,22335,0,0,0,0,0,22336,0,22338,22344,0,22347,22349,0,22350,0,0,0,0, +0,0,0,22357,0,0,0,0,0,22358,0,0,0,0,0,0,0,0,0,0,22359,22360,0,0,0,0,0,0,0,0, +22361,22366,0,0,22369,0,22370,22373,0,0,0,0,0,22375,0,22377,0,0,0,0,0,22378,0,0, +0,0,22381,0,0,0,0,22382,0,22383,0,0,0,0,0,0,0,0,0,22391,0,0,22392,22395,22396, +22402,0,0,0,0,0,0,0,0,0,0,0,0,0,22405,0,0,22406,0,0,22408,0,0,22409,22410,0,0,0, +0,0,0,22424,0,0,0,0,22426,0,0,0,22427,0,22428,0,22432,0,22435,22442,22443,0,0,0, +0,22444,0,0,0,0,0,22446,0,22454,0,22455,0,0,0,22465,0,22470,0,22471,0,0,0,0, +22472,22473,0,22487,0,0,0,22488,0,0,0,0,22489,0,0,22499,0,0,0,0,0,0,22514,0,0, +22515,0,0,0,0,0,0,0,22516,0,0,0,22517,22520,0,0,0,22534,0,0,22535,0,0,22536,0, +22540,22553,0,22555,0,0,0,0,22561,0,0,22562,0,0,0,0,0,0,0,0,0,0,0,22566,0,0,0,0, +22567,22568,0,0,22575,0,22579,0,22582,22583,22585,0,0,0,0,0,22586,0,0,22587,0,0, +22590,0,0,0,0,0,22591,0,22592,0,0,0,0,0,22593,0,22602,0,0,22604,0,0,22609,0,0, +22618,0,0,0,0,0,0,22619,0,22624,22625,0,0,22638,0,0,0,0,0,22639,0,0,22640,0,0,0, +0,0,0,0,22644,0,22645,22647,0,0,0,0,22652,22653,0,0,0,22654,0,22655,0,0,0,22656, +0,0,0,0,0,0,0,0,0,0,22673,22675,22676,0,0,22678,22679,0,22691,0,0,0,0,0,0,0, +22693,0,0,22696,0,22699,22707,22708,0,0,0,0,0,0,0,0,22718,0,22719,0,0,0,0,22723, +0,0,0,22724,22725,0,0,0,0,0,0,0,0,0,0,0,0,0,0,22726,22728,0,0,0,0,0,0,0,0,22729, +0,0,22731,0,0,0,0,22732,22735,22736,0,0,0,0,22739,0,22749,0,0,22751,0,0,0,0,0,0, +0,0,0,0,0,22758,0,0,0,0,0,22760,0,0,0,0,0,22764,22765,22766,0,22768,0,0,0,0,0, +22769,22770,0,0,0,0,0,0,22771,0,0,22772,22775,0,22776,22777,22780,0,0,22782, +22784,0,22787,0,22789,22796,0,0,0,0,0,22798,0,0,0,0,0,0,22802,0,22803,22804,0,0, +0,0,0,0,0,0,0,0,22805,0,0,22810,22811,22814,22816,0,22825,22826,0,22831,22833,0, +0,0,0,0,0,0,0,0,22834,0,22836,22838,0,22839,0,0,0,0,0,22840,0,22847,0,0,0,0,0, +22856,22857,0,22858,22859,0,0,22862,0,0,22864,0,0,0,0,22865,0,0,0,0,0,0,0,0,0,0, +0,22866,0,22867,22868,0,0,0,0,22869,0,22871,0,22872,0,22873,22881,22882,22884, +22885,0,0,0,0,0,0,0,22886,22887,0,22894,0,22895,0,0,0,22900,0,22901,0,0,0,0, +22904,0,0,0,0,22905,22907,0,0,0,22915,22917,0,0,22918,0,0,0,22920,0,0,0,22929, +22930,0,0,0,22941,22942,0,0,0,22943,0,0,0,22944,0,0,0,0,0,0,0,22946,0,22947,0,0, +22954,0,22956,0,0,22962,0,0,0,0,0,0,0,22963,0,0,22964,0,0,0,0,0,0,0,22965,0, +22968,0,0,0,22969,0,0,0,0,0,22970,0,22971,0,0,0,0,0,22978,0,0,22979,0,22987,0,0, +22989,0,0,0,0,0,0,22990,0,23005,0,0,0,0,0,0,0,23006,23007,23008,0,0,23023,23024, +23029,0,0,0,0,23030,0,0,0,0,0,23032,0,0,0,0,0,23035,0,0,0,0,23038,0,0,0,23048,0, +23049,23052,23053,23060,23061,0,23063,0,0,0,0,23067,23068,0,0,0,23069,23073,0,0, +0,23127,0,23128,0,0,0,0,0,23129,0,23138,23141,0,23149,0,0,23150,0,0,0,23152,0,0, +0,0,0,0,0,0,23154,0,0,0,0,23157,23159,23160,0,0,0,0,0,0,0,0,0,0,0,0,23180,0,0,0, +0,23181,0,0,23188,0,23189,0,0,0,0,0,0,0,0,0,0,0,0,23195,0,0,23196,23199,0,0,0,0, +0,0,0,0,0,23202,0,23204,0,23207,0,23209,23210,0,0,0,0,0,0,23227,23229,0,0,23230, +23234,23238,0,0,0,23245,23246,23248,0,0,0,0,23249,23254,0,0,0,23265,0,0,0,0,0,0, +0,23268,0,23276,0,0,0,0,23277,0,23297,0,23298,0,0,0,0,23299,0,23302,0,0,23303, +23312,0,0,23314,0,23320,0,0,0,0,23324,0,23325,0,23328,0,23334,0,0,0,23337,0,0,0, +0,23343,23344,23346,0,23348,0,0,0,0,0,0,0,0,23353,0,0,0,0,23355,0,23356,23358,0, +0,0,23359,23360,0,23361,0,23367,0,23369,0,0,23373,0,23378,23379,0,23382,23383,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,23387,0,0,0,0,0,0,23388,23390,0,0,23393,23398,0,0,0, +23399,0,0,0,23400,0,0,0,0,23401,0,0,0,23415,0,0,0,0,0,0,0,0,23416,0,23422,0, +23443,23444,0,0,0,0,23448,0,23454,0,0,0,0,0,0,23456,0,0,23458,23464,0,0,0,0,0,0, +23465,0,0,0,23470,23471,0,0,23472,0,0,0,23473,23496,0,0,0,0,0,0,0,0,23497,0, +23499,0,0,23502,0,0,23503,0,0,23513,0,0,23515,0,0,0,23517,0,0,0,0,23518,23519, +23521,23524,0,23525,23528,23539,0,0,0,0,0,23541,0,0,23544,0,0,23556,0,0,23557,0, +0,0,0,0,0,0,0,0,0,0,0,0,23559,0,23560,0,0,23561,0,0,23566,0,0,0,0,0,23568,23569, +23570,0,0,0,0,23571,0,23574,0,0,0,0,0,0,0,0,0,0,0,23575,0,23579,0,0,23581,0,0,0, +0,0,0,23587,0,0,0,0,0,0,0,23596,23598,0,0,0,0,23602,23606,0,0,23607,0,23608,0,0, +0,23614,23616,0,0,0,0,0,23618,0,0,23619,0,0,0,0,23621,23626,0,23627,0,0,0,0,0,0, +0,23629,0,23630,0,0,0,0,23634,0,23636,0,0,0,0,0,0,23638,0,0,0,0,23640,23667,0, +23669,0,0,0,23681,0,0,0,0,0,0,0,23682,0,23683,0,0,0,0,0,23684,0,0,0,23685,23689, +0,23693,23694,23700,0,23702,0,23709,0,0,0,0,0,0,0,23712,0,0,0,0,0,23714,0,0, +23715,0,0,0,0,23718,0,0,23720,0,0,0,0,23722,0,0,0,23726,23729,0,23741,23746,0, +23748,0,0,0,0,23749,0,0,0,0,0,23750,0,0,0,0,23751,0,23753,0,0,0,0,23757,23765,0, +0,0,23770,0,0,0,0,0,0,0,23771,0,23772,23781,0,0,23796,0,0,0,0,23798,0,23799,0,0, +0,23802,0,0,23806,0,23807,0,0,23808,0,23809,0,23819,0,0,0,23821,0,23827,0,0,0, +23829,0,0,0,0,0,0,0,23830,0,0,0,0,0,0,23832,23833,23834,23835,0,0,0,0,23837, +23838,0,0,0,0,0,23846,0,0,0,0,0,0,23847,0,0,0,0,0,23879,23881,0,0,23882,23883, +23895,0,23899,0,0,0,0,23901,0,0,0,0,0,0,23902,0,0,0,0,0,23903,23905,0,23906,0, +23907,23918,23919,23920,0,23922,0,23924,0,23927,0,23934,0,23937,23941,0,23942, +23946,0,0,0,0,0,23955,23956,23958,0,0,0,0,0,0,23959,0,23962,23965,0,23966,0,0,0, +0,23967,23968,0,0,23973,0,0,23974,0,0,0,0,23975,0,23976,0,0,0,0,0,0,0,0,0,0,0,0, +0,23977,0,0,0,0,0,0,0,0,23980,0,0,23984,0,23985,0,0,23987,0,0,23988,23990,23991, +0,0,0,0,0,0,23992,0,0,0,0,0,0,0,0,23994,0,0,0,23998,0,0,0,0,0,0,0,0,0,23999,0,0, +24003,0,24004,0,24006,0,0,0,24007,0,0,24008,0,0,0,0,0,0,0,24009,0,0,24010,0,0, +24011,0,0,24013,24014,0,0,24015,24016,24027,0,24028,24029,0,24030,0,0,0,0,0, +24033,24034,0,24035,0,0,24036,0,0,24044,0,24048,24049,24063,24067,0,24068,24070, +0,0,24071,24078,24087,0,24090,0,0,0,24095,0,24098,24101,24104,24106,0,24107,0,0, +0,24108,0,0,0,0,24110,24111,0,24113,0,0,24115,24120,0,0,0,0,0,0,24124,0,24125,0, +24126,0,24127,0,0,0,0,0,24135,0,0,24136,0,24137,24142,0,0,0,24146,0,0,24147, +24149,24154,0,24163,0,0,0,24165,24166,24167,0,0,0,0,0,0,0,0,0,0,24169,24170, +24175,0,0,0,24178,0,0,24179,0,0,24181,0,24184,24197,0,24201,24204,0,0,0,0,0,0, +24206,24212,24220,0,0,0,24224,0,0,0,0,0,0,0,0,24226,0,24234,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,24235,0,24236,0,0,0,0,0,24239,24240,24241,0,0,24248,0,0,24249,0, +24251,0,0,0,0,0,0,24253,0,24268,0,0,0,24269,0,24271,24272,0,0,0,0,24273,0,0, +24274,0,0,24279,0,0,0,0,0,0,0,24280,0,24293,24294,0,0,0,0,0,0,24296,0,0,24323,0, +0,0,24329,24330,24331,24339,0,24351,0,0,24369,24370,0,0,0,24371,0,0,0,0,24372, +24373,24374,0,0,0,0,0,24378,0,0,0,0,24379,0,24381,0,24383,24389,0,24390,0,0, +24394,24395,24400,0,0,0,24401,24402,0,24406,0,0,0,24411,0,0,0,24415,0,24416,0,0, +0,0,0,24417,0,24419,0,24422,0,24423,24428,0,24435,0,0,0,24439,0,0,0,24440,24442, +24446,0,0,0,24447,24448,24449,24452,0,0,0,0,24453,24457,0,0,24458,24459,24460,0, +24465,0,0,0,0,0,0,0,24470,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,24471,0,24473, +24474,24475,24476,0,24478,0,0,0,0,24480,0,0,0,0,0,0,0,0,0,0,24481,0,0,0,0,0,0,0, +0,0,0,24482,24485,0,0,0,0,24486,0,0,0,24488,0,0,0,24494,0,0,0,0,24497,0,0,24498, +0,0,0,24499,24506,0,0,0,24507,0,0,24511,0,0,24513,24514,0,0,0,0,0,24517,0,24518, +0,24520,0,24521,24524,24525,0,0,0,0,0,24527,0,0,0,0,0,0,0,0,0,0,0,0,0,0,24528,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,24537,24539,0,24540,0,0,0,24548,0,0,0,0,0,24549, +24550,0,0,0,24553,24554,0,24555,0,24556,0,24558,0,0,0,0,0,24560,0,0,0,24561,0,0, +0,0,0,24562,0,0,0,0,0,0,0,0,0,0,0,0,0,24567,0,0,0,0,0,24569,0,0,0,24574,0,24575, +0,0,0,0,0,0,0,0,0,0,0,24577,24581,0,24584,0,0,0,0,0,24585,0,0,0,0,0,24586,0,0, +24587,0,24588,0,0,0,0,0,0,0,0,0,0,24590,24591,0,0,0,0,24592,0,0,0,0,0,0,0,24594, +0,0,0,0,0,0,0,24596,24597,0,0,0,0,24602,24603,0,0,0,0,24604,0,0,24605,0,24610,0, +0,24611,0,0,0,0,24612,24615,24616,24624,0,0,0,24627,0,24638,24639,0,0,0,0,24640, +0,0,0,24655,24656,24657,0,0,0,0,0,0,0,0,24662,0,24663,24664,0,0,0,0,0,24665,0,0, +0,0,24667,0,0,0,0,0,0,24668,24669,0,24670,24674,0,0,0,24675,0,24678,0,0,24679,0, +0,0,24681,0,24683,0,0,0,0,24684,0,24685,0,0,24686,0,0,24688,24689,0,0,0,0,24690, +24691,0,0,0,0,0,0,0,24697,0,24698,0,0,0,0,0,0,0,0,24709,0,0,0,0,0,24710,0,24712, +0,0,0,0,0,0,24713,24714,0,24715,0,24716,24718,0,24719,0,0,0,0,24720,0,0,24725,0, +0,24738,0,24749,24750,0,0,0,24752,0,0,0,24753,0,0,0,24758,0,0,0,0,0,24762,0, +24763,0,0,0,0,0,0,0,24764,0,0,0,0,0,24765,24767,24768,0,24772,0,0,0,0,24773,0,0, +0,0,24777,0,0,0,0,0,24785,0,24786,24788,0,0,0,24789,0,0,0,0,24794,24798,0,24799, +24800,0,0,0,24803,0,24804,24806,0,24807,0,0,0,24810,0,0,0,0,0,0,24827,24828,0, +24835,0,0,0,0,0,0,24836,0,0,0,0,0,24839,0,24843,24844,0,0,0,0,0,0,0,0,0,0,24847, +0,0,24848,0,0,0,0,0,0,24849,0,24850,24851,0,0,0,24852,0,24853,0,0,0,0,0,0,0,0,0, +24854,0,24855,0,0,24868,0,0,0,24883,0,0,0,24884,0,24895,24897,0,0,0,0,0,24899,0, +0,0,0,0,24900,0,24913,0,0,0,0,0,0,24914,0,0,24917,24930,24931,0,0,0,24932,0,0, +24939,0,0,24942,0,0,0,0,0,0,0,0,0,24945,24950,0,24951,0,0,24953,0,0,0,24954,0, +24959,0,0,0,24961,0,0,24962,0,24964,24968,24970,24972,0,0,0,0,0,24976,0,0,0, +24977,0,24982,0,0,24983,0,0,24984,0,0,0,24993,0,0,0,24994,0,0,25001,0,0,0,25003, +0,0,25018,0,0,25023,0,0,0,25034,0,0,25035,25036,0,25037,0,0,0,0,0,0,0,25039,0,0, +0,0,0,25040,0,0,0,0,0,0,0,25042,0,0,25043,25045,0,0,0,0,0,0,25049,0,0,25051,0, +25052,25053,0,0,25054,0,0,0,25055,0,0,0,0,25057,25059,0,0,25060,25064,0,25065, +25069,25070,0,0,0,0,25072,0,25073,0,25090,0,0,25092,25093,25101,0,0,0,0,0,0, +25105,25108,0,0,25113,0,0,25115,25116,0,0,0,0,0,0,25117,0,0,0,25120,25121,0,0,0, +0,0,0,0,25125,0,0,0,25126,0,25130,25134,0,25139,0,25143,0,0,0,25151,0,25161,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,25163,0,0,0,0,0,0,0,25174,0,25175,0,25207,0,0, +0,25209,0,0,0,0,25213,0,25219,0,25223,0,25225,0,0,0,25227,0,0,0,25228,0,0,0, +25229,0,0,0,0,0,0,0,25231,25233,0,0,0,0,25237,25239,0,0,0,25243,0,0,0,25252,0, +25257,25258,0,0,0,0,25260,25265,0,25268,0,0,25273,25324,0,25325,0,25326,0,0,0,0, +0,0,0,0,25327,0,0,0,0,0,25328,0,0,0,0,0,0,25332,0,0,0,25333,0,0,0,25336,25337, +25338,0,0,25343,0,25350,0,0,0,0,0,0,0,25352,0,25354,0,25375,0,25379,0,0,0,0, +25384,0,0,0,0,0,0,0,0,0,25386,0,25388,0,25390,0,0,25399,0,0,25401,0,0,0,25402,0, +0,0,25407,0,0,0,0,0,0,0,0,0,0,0,25413,25415,0,0,25417,0,0,0,0,0,0,0,25419,0,0,0, +25421,0,0,0,25424,0,0,0,0,25433,0,0,0,0,0,0,0,0,0,25435,0,0,0,0,0,0,25436,0,0,0, +25437,0,0,25440,0,0,0,0,0,0,25442,0,0,25443,0,25446,0,0,25449,0,0,0,25450,0,0,0, +0,25452,0,25453,25454,25455,0,0,0,25456,0,25457,0,0,0,25459,0,25461,0,25468,0,0, +0,0,0,0,0,0,25469,0,0,0,0,0,25471,0,0,0,0,0,25474,0,0,0,0,0,0,0,0,25475,0,0,0,0, +25477,0,0,0,0,25483,0,0,0,0,0,25484,0,0,0,0,0,0,0,0,0,0,0,0,25485,0,25497,0,0, +25498,0,25504,0,25510,0,25512,0,0,25513,25514,0,0,0,0,0,0,25517,25518,25519,0, +25520,0,0,0,0,0,0,0,25521,0,25522,25527,25534,0,25536,0,25537,0,0,25548,25550,0, +0,25551,0,25552,0,0,0,0,0,25554,0,25555,0,25556,25557,25568,0,0,0,25570,25571,0, +0,0,0,0,0,25574,0,0,0,0,25579,0,0,0,25581,0,0,0,25582,0,0,0,0,0,0,0,0,0,25588,0, +0,0,0,25589,0,0,0,0,25590,0,25591,25592,25593,0,25594,0,0,0,25596,0,25597,25615, +0,0,0,0,0,25618,0,0,0,0,25619,25623,0,0,25629,0,0,25631,0,0,0,25635,25636,0,0, +25649,0,0,0,0,25654,0,0,0,25661,25663,0,0,25671,0,0,25678,25698,0,25699,25702, +25703,0,0,0,0,0,0,0,0,25704,0,0,0,0,0,25706,0,0,25710,0,25711,0,25712,0,25715, +25716,25717,0,0,25718,25728,25732,0,0,0,25734,0,0,0,0,0,0,0,0,0,25737,0,0,25739, +0,0,0,25740,0,25741,25745,0,25746,0,25748,25772,25778,0,0,0,0,0,25780,0,0,0,0, +25781,0,25782,25784,25785,0,0,0,25789,0,0,0,0,0,0,25797,25801,0,0,0,25808,25809, +0,0,25811,25814,25815,0,0,25817,0,0,0,0,0,0,0,0,25820,0,0,0,0,25832,25833,0,0,0, +25846,0,0,0,25847,25848,0,0,0,0,0,0,0,0,0,25849,25850,0,0,25851,0,0,25852,0, +25862,0,0,0,25863,25865,0,0,0,0,0,0,0,25867,25868,0,25869,25874,0,25875,0,25876, +25877,0,0,0,0,25878,25902,0,0,0,0,0,0,0,25903,25904,25905,0,0,0,25908,25909,0,0, +0,0,25910,0,0,0,0,0,0,0,25912,0,25913,0,0,0,0,0,0,0,0,25914,0,0,25916,0,0,0,0,0, +25917,25927,0,0,0,0,25928,0,0,25930,0,0,0,25933,0,0,25938,25942,0,0,0,0,0,0,0, +25945,0,25950,0,25956,0,0,25961,25962,0,0,25963,0,25964,25965,25966,0,0,0,0,0, +25967,0,0,0,0,25968,0,0,0,25969,25971,0,0,0,0,0,25973,25975,0,0,0,0,0,0,0,25978, +0,25981,0,0,0,25982,0,0,0,25984,0,0,0,0,0,0,0,25993,0,0,0,0,0,0,0,0,0,0,0,0,0, +26002,0,0,0,26005,0,0,0,26006,26007,0,0,26014,26015,26016,0,0,0,0,0,0,26017, +26018,26020,0,26022,26023,0,0,0,26024,26028,0,26029,26033,26034,26044,0,0,0,0,0, +26046,0,0,26047,0,0,26049,0,26050,0,26051,0,0,0,0,0,26053,0,0,0,0,26054,26059,0, +0,0,0,0,0,26060,0,26066,0,0,0,0,0,0,0,0,0,0,0,0,26067,0,26069,0,0,26071,0,0,0, +26073,0,26074,26077,0,0,0,0,26078,0,0,0,26079,0,26090,0,0,26094,0,0,0,0,0,0,0,0, +26095,0,0,0,0,0,0,0,0,0,0,0,26096,26101,0,26107,26122,0,26124,0,0,26125,0,0,0,0, +0,0,26136,26141,26155,0,0,0,0,0,0,0,0,0,26164,26166,0,0,0,26167,0,26170,26171,0, +0,26172,0,0,26174,0,0,0,0,0,0,0,0,0,0,0,0,0,26175,0,0,0,26176,26177,0,26321, +26322,0,26323,0,0,26324,0,0,0,0,0,0,0,26325,0,26331,0,0,0,0,0,0,26335,0,0,0, +26350,0,0,0,26379,0,0,26382,26383,26385,0,0,26392,26406,0,0,0,0,26411,0,0,0,0,0, +26412,0,0,26420,0,0,26423,0,26424,26426,26432,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +26435,0,26436,0,0,0,0,0,26441,0,26444,0,0,0,26446,0,0,0,0,26447,0,0,0,0,26449,0, +26450,26452,0,26453,26454,0,0,0,26455,0,0,0,26456,0,0,26458,0,0,26460,0,26463,0, +0,0,0,0,0,0,0,26464,26470,0,0,0,0,0,0,0,0,0,26473,0,0,26474,0,0,0,0,0,0,0,26475, +0,0,0,0,0,0,0,26477,0,26485,0,0,26486,0,26487,0,0,26488,26493,26494,0,0,26495,0, +26497,26504,26506,0,0,0,0,0,26507,0,0,0,0,0,26509,0,0,26510,0,0,0,0,0,0,0,0,0,0, +0,0,0,26512,0,26513,26515,0,0,0,26518,0,0,0,26519,0,26524,26526,0,0,0,26527,0, +26532,0,26533,26537,26558,0,0,0,26559,0,0,0,26571,0,0,26573,0,26588,0,26593,0,0, +0,0,0,0,26603,0,26604,0,0,0,0,0,0,0,0,0,0,26606,0,0,0,0,0,0,0,26607,26609,26611, +26614,0,0,0,26616,26620,0,26621,0,0,0,0,0,26627,0,26629,0,0,26630,0,0,26632, +26643,0,0,0,26644,0,0,0,0,0,0,0,0,0,26646,26647,0,0,0,26650,0,0,26656,0,0,0,0, +26663,26670,26671,0,0,0,26685,26686,26687,0,26689,0,0,0,0,26744,0,26745,0,26747, +26748,0,26749,26750,26751,0,0,0,0,26752,26755,0,0,0,26756,26769,0,0,0,26774,0,0, +0,0,0,26775,0,26777,26778,0,26786,0,0,0,26787,0,0,0,0,0,0,0,0,0,0,0,0,0,26788,0, +0,26789,0,0,0,0,0,26791,0,26792,26793,0,0,0,26794,0,26797,26798,0,0,0,26800,0,0, +26803,0,26804,0,0,0,0,0,0,0,0,0,26805,0,0,26808,0,0,26809,0,0,0,0,0,0,0,26812,0, +26825,0,0,0,0,0,0,0,26826,0,0,26827,26829,26834,0,0,0,0,26835,0,0,26849,0,26851, +0,0,0,0,0,0,0,0,0,26852,0,26853,26857,0,26858,0,26859,0,0,0,0,0,0,0,26876,0, +26878,26882,26883,0,0,0,0,26890,26894,0,0,0,0,26895,26896,0,0,0,0,0,26900,0,0,0, +0,0,0,0,26911,26913,26914,26915,26916,26919,0,0,0,26921,26922,0,0,26925,0,0,0, +26928,0,0,26929,26930,0,0,0,26931,0,26932,0,0,0,0,0,26933,0,0,0,0,0,0,26937,0,0, +26943,0,0,26944,0,0,0,26946,0,0,0,0,0,0,0,26956,0,26958,0,0,26963,0,0,0,0,0,0,0, +26965,0,26969,26970,26972,0,0,0,0,0,26973,0,26974,0,26978,0,26980,0,0,0,0,0,0, +26982,0,26986,26987,0,26990,0,0,0,0,27003,27006,0,0,27007,27010,27012,27013,0,0, +0,0,0,0,0,0,27014,27015,27018,0,27019,0,0,0,0,0,27025,0,0,0,27026,0,0,0,0,27029, +27030,27031,27034,0,0,27036,27037,0,0,0,27038,27042,0,0,0,27044,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,27045,0,0,0,0,0,0,0,27046,0,0,0,0,0,0,0,27047,27049,0,27050,0,0,0, +27051,27052,0,27055,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,27056,27058,27059,0, +27061,0,27064,0,0,0,0,0,27069,0,0,27070,0,0,0,0,0,0,0,27072,0,0,0,0,0,0,0,0, +27076,0,0,0,0,0,27078,0,27079,0,0,0,27081,0,0,0,0,0,0,27082,0,27083,27086,0,0,0, +0,27087,0,0,0,0,0,27088,27090,0,27094,0,0,27095,0,27099,27102,0,0,0,27103,0,0,0, +0,27105,0,0,0,27106,0,0,0,0,0,0,27107,0,0,0,0,27108,27117,0,0,0,0,27118,0,0, +27124,0,27126,0,0,27130,27131,0,0,0,0,0,0,27147,0,0,0,0,27148,27149,0,0,0,0, +27150,27151,0,27152,0,27159,0,0,0,27164,0,0,0,0,0,0,0,27175,0,27189,0,0,27191,0, +27193,0,27195,0,27198,0,0,0,0,0,27200,0,0,0,0,27202,0,0,0,0,27203,0,0,27204,0,0, +27206,0,27207,0,0,0,0,27209,0,0,0,27213,0,0,27216,27219,27220,27222,27223,0, +27224,0,27225,27226,0,0,27233,0,0,0,0,27235,0,27237,0,27238,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,27239,0,27242,27243,0,27250,0,0,0,27251,0,27253,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,27254,27255,27258,0,0,0,27259,0,0,0,0,0,0,27267,0,27276,27278, +0,0,0,0,0,0,0,0,0,27296,27297,27301,0,0,0,0,0,0,27302,0,0,0,0,0,0,27312,27313,0, +0,0,0,0,27318,0,27320,0,27329,0,27330,27331,0,27332,0,0,0,0,27340,0,0,0,27348,0, +0,0,0,0,0,27350,0,27351,0,0,0,0,27355,0,0,27358,27359,27361,0,0,0,27365,0,27367, +0,27376,27378,0,0,27379,0,0,0,0,0,0,27396,0,27397,27404,0,0,0,0,0,27408,0,0,0,0, +27453,0,0,0,27456,0,0,0,27458,0,0,0,0,0,0,0,27459,0,0,0,27460,0,0,27461,0,27465, +27467,0,0,27469,0,27470,0,27471,0,27477,27482,0,0,0,0,0,0,27484,0,0,0,0,0,0, +27485,0,0,0,0,0,27493,0,27494,27502,0,0,0,0,0,0,0,0,0,0,0,0,27511,27532,0,0,0, +27533,27545,0,0,0,27546,0,0,0,0,0,0,0,0,0,0,27547,0,0,27549,27550,0,27551,0,0,0, +0,0,0,0,27555,0,0,27571,0,27573,27574,27575,27577,0,27578,0,0,27579,27585,0,0,0, +0,0,27586,0,0,27588,27589,0,0,0,0,27596,0,0,27600,0,0,0,0,0,0,0,0,0,0,0,27608,0, +0,0,0,0,0,0,0,0,0,0,27610,0,0,0,27618,0,0,27620,0,0,0,27631,0,0,27632,27634,0, +27636,27638,0,0,0,27643,0,27644,27649,0,0,0,0,0,0,0,0,0,0,0,0,0,27651,27660,0, +27661,0,0,0,0,0,0,0,27662,0,0,27664,0,27665,0,0,0,27669,0,27671,0,0,0,27673, +27674,0,0,0,27682,0,0,0,27711,0,27712,27713,27719,27720,0,0,27728,0,27729,0,0,0, +0,0,0,0,0,0,27731,0,0,27732,0,27733,0,27738,0,0,0,27742,0,0,0,27743,27744,0,0,0, +0,0,0,27745,27746,0,0,0,27747,27748,27751,27752,0,0,0,27768,27770,0,0,0,27774, +27775,0,27776,27777,0,0,27781,0,27784,0,27786,0,0,27791,0,27792,27793,27804,0, +27812,27813,0,0,0,0,0,0,0,0,27814,0,27825,0,27827,0,0,0,0,27828,27861,27862,0,0, +0,27864,0,0,0,27865,27884,0,27889,0,0,0,0,0,27890,0,27891,0,0,0,27892,0,0,0,0,0, +27897,27898,0,0,27899,0,0,0,27901,27905,0,0,27920,0,0,27921,0,27922,0,0,0,27931, +27934,0,0,0,0,0,0,0,0,0,0,27941,0,27942,0,27945,0,27947,27954,0,0,0,0,27960, +27963,0,0,0,0,0,0,0,0,27964,27965,0,0,0,27967,0,27969,27975,0,27976,27977,0, +27981,0,27983,28051,28052,0,0,0,0,0,28056,0,0,0,0,0,0,28058,28059,0,0,28061,0,0, +0,0,0,0,0,28063,0,0,0,0,0,0,28066,0,0,0,0,0,0,28069,28070,28072,0,28073,0,0, +28074,0,0,0,0,28075,0,0,0,0,0,0,0,28078,0,0,0,0,28085,0,0,0,0,28086,0,0,0,0,0,0, +28088,0,0,0,0,0,0,0,0,28090,0,28097,28114,28115,0,0,0,0,0,0,0,28116,0,0,0,0,0, +28118,0,28129,0,28131,0,0,28135,0,0,0,28140,28141,0,0,0,28146,0,0,0,0,28152,0,0, +0,0,28155,28157,28161,0,0,0,0,28166,0,28167,0,0,0,0,0,0,0,0,0,0,0,28172,0,0,0,0, +0,0,28173,0,0,28175,0,0,0,0,0,0,0,0,0,28178,28188,0,28190,0,0,0,0,0,28191,0, +28193,28206,0,0,28207,28209,0,28211,0,28213,0,0,0,28215,28216,28217,0,28222,0, +28223,28225,0,0,0,28226,0,28227,28229,28232,0,0,0,0,0,0,0,0,0,28235,0,28241,0,0, +28242,0,0,0,0,28243,0,0,0,28245,0,0,0,28248,28250,0,28251,28252,0,0,0,0,0,0, +28253,0,0,28254,28255,0,0,28256,0,0,28258,0,0,0,0,0,28259,0,0,28260,0,0,28261,0, +0,0,0,28262,28263,0,0,28264,0,0,0,28266,0,28268,28269,0,28270,28272,28274,0, +28277,28278,0,0,0,28279,0,28280,28281,28283,0,28292,0,28294,0,28297,0,0,0,0, +28299,0,0,0,0,0,28300,0,0,0,0,0,0,0,28301,0,0,0,0,0,0,0,0,0,0,0,0,0,28302,28303, +0,0,0,0,28304,0,0,28305,0,28312,0,28313,28314,0,0,0,0,0,0,28315,0,0,0,28320, +28321,0,0,28328,0,0,0,28329,28338,0,28339,0,0,28344,0,0,0,0,0,0,0,0,28347,0,0,0, +0,0,0,0,0,28348,0,0,0,0,0,28411,0,28412,28413,0,28416,0,0,0,28420,0,0,0,0,0, +28421,0,0,0,0,28423,0,0,0,28424,0,0,28428,0,0,0,0,0,28429,0,0,0,28431,28434,0, +28458,0,0,0,0,0,0,0,0,0,0,0,28464,0,0,0,0,28465,0,28467,0,0,0,0,0,0,28471,0,0,0, +0,28474,0,28480,0,28481,0,0,28485,0,0,0,0,28486,28488,0,0,28489,0,0,0,0,28492,0, +0,0,28495,0,28497,0,28499,0,0,0,0,28500,0,0,28502,28503,0,0,0,28508,0,0,0,28510, +0,0,28512,28513,28514,28521,0,28526,0,28527,28528,0,0,0,0,28529,0,0,28532,0,0, +28537,28538,0,0,0,28539,0,28548,0,28553,28554,0,0,0,0,0,0,0,0,0,0,0,0,28560, +28563,0,0,28564,0,0,0,0,28565,0,0,0,0,0,0,0,28566,28568,0,0,0,0,0,0,28569,0,0,0, +28570,0,28572,28573,0,0,0,0,28575,0,0,0,0,28576,28581,28588,0,0,28589,0,0,0, +28590,28595,0,28598,0,0,28601,0,0,28605,0,0,0,0,28614,28615,28619,0,0,0,0,0,0, +28620,0,28626,0,0,28628,0,28631,0,28632,0,0,0,0,0,0,28635,0,0,0,28637,28638,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,28639,0,28643,0,0,28652,0,0,0,28662,0, +28670,28671,0,0,0,0,0,0,0,0,0,28672,28673,28675,28676,0,0,0,0,0,0,0,28691,0,0,0, +28695,0,0,0,28696,0,28697,28698,0,28705,0,28707,28708,28710,0,0,0,0,0,0,0,28711, +28728,0,0,0,28736,0,0,0,28737,0,0,0,0,0,0,0,0,0,28738,0,28739,0,28741,0,0,28742, +0,0,0,0,0,0,0,0,0,0,0,28745,0,0,0,0,0,0,28749,28750,28752,28754,28756,0,28757,0, +0,0,0,28759,28760,0,0,0,0,0,0,28762,0,0,0,28764,0,0,0,0,0,0,28766,0,28767,28768, +0,0,0,0,28769,28770,0,0,0,0,0,0,0,0,0,0,0,0,0,28771,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,28772,0,28773,0,28782,0,0,0,0,0,0,28784,0,28785,0,28786,0,0,0,28787,0,0,0, +28797,0,0,0,0,0,0,28799,0,0,28801,0,0,0,0,28802,0,28805,0,0,28806,0,0,28807,0,0, +0,0,0,0,0,28808,0,0,0,0,0,28810,28812,0,0,28816,28819,0,0,28821,0,28826,0,0,0, +28842,28852,0,0,28853,0,28854,28855,0,0,0,28857,0,0,0,28858,0,28867,28868,28869, +0,0,0,28874,28880,28882,28890,28892,0,0,0,0,0,0,0,28895,0,0,0,28898,28899,0,0,0, +28900,0,0,28904,0,28906,0,0,0,0,28907,0,0,0,0,0,0,28908,0,0,0,28910,0,28914,0,0, +0,0,0,0,0,28915,28916,28919,0,0,28920,0,28921,0,0,0,0,0,0,0,0,28924,0,0,0,0, +28926,28929,0,0,0,28930,0,28936,0,28939,0,0,0,0,28942,0,0,0,0,0,0,28956,0,0,0, +28966,0,0,0,0,28967,0,0,0,0,0,0,0,0,0,28968,0,28971,0,28975,28976,0,28982,28983, +0,0,28984,28989,28996,28997,28998,0,0,0,0,0,0,28999,0,0,0,0,0,29000,0,29001,0,0, +0,29009,0,0,29011,0,0,29021,0,0,0,0,29024,0,29025,0,0,0,0,0,29026,0,0,0,29036,0, +0,0,29037,0,0,0,0,29038,0,29045,0,29047,0,0,0,0,0,0,0,0,0,29051,0,0,0,29054, +29056,29062,0,29070,29082,0,0,0,29083,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,29084,0,0, +0,0,29085,29088,0,0,0,0,0,0,0,29090,29097,0,0,0,29103,0,0,0,0,0,0,0,0,29105,0,0, +0,0,0,29107,0,29109,0,0,0,29115,0,0,29120,0,0,29138,29140,0,0,0,0,0,0,0,0,0, +29152,0,29160,29174,0,29176,0,0,29180,0,29181,0,0,0,0,0,0,0,0,29228,0,0,29229,0, +0,29230,0,0,0,0,0,0,0,0,0,0,29234,0,0,0,29241,0,29245,0,29248,0,29250,29256, +29280,0,29282,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,29285,0,0,29286,29291,29292,0,0,0,0, +29294,0,29295,0,0,0,0,0,29296,29297,29298,29300,0,29302,0,0,29304,29307,0,29312, +0,0,0,29322,0,0,29323,0,0,29324,29326,29328,0,29335,0,0,0,0,0,0,0,29338,29339,0, +0,0,0,0,29341,29343,0,0,0,0,29344,0,0,0,0,0,29345,0,0,0,0,29346,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,29347,29348,29349,0,0,29354,0,0,29355,0,0,0,0,0,0,0,0,29357,0,0, +0,0,29364,0,29365,0,0,0,0,0,0,0,29366,0,0,29368,0,0,0,0,0,0,0,0,29378,0,29381,0, +0,0,0,0,0,0,0,29386,0,0,0,0,0,0,29389,0,0,0,29390,0,0,29391,29397,0,29398,29412, +29414,29418,29419,0,0,0,0,0,0,0,29420,0,0,0,0,0,0,0,29423,0,0,0,29435,0,0,0, +29437,0,0,29439,0,29441,0,0,0,0,29443,0,29446,29450,29452,0,0,0,0,0,29456,0,0,0, +0,0,29461,0,0,0,29464,0,0,0,0,0,0,0,0,29468,0,29473,0,0,0,29486,0,0,0,29490,0,0, +0,29491,29492,0,0,29497,0,0,0,29498,0,29499,0,29502,29505,0,29509,0,0,0,29510,0, +0,0,29512,0,0,0,29516,0,0,0,0,0,0,0,0,29518,0,29519,0,0,0,0,0,29520,29521,29529, +0,0,0,0,0,0,0,0,29530,0,0,29531,29538,0,29540,0,0,0,29542,0,29543,29544,29547,0, +0,29548,0,0,0,29549,0,0,0,29550,0,0,29552,0,0,0,0,29558,29561,0,29562,29564,0,0, +29565,0,0,29566,0,0,0,0,0,0,0,0,0,0,29578,29584,29586,29591,0,0,0,0,29593,29594, +0,0,29597,0,0,29613,0,29614,0,29615,0,0,0,0,29616,29617,0,0,29625,0,0,0,29632,0, +0,0,0,0,0,0,29633,0,0,0,0,0,29634,29635,29637,0,29638,0,29641,29643,0,0,0,0,0,0, +29644,0,29645,0,29649,0,0,0,29650,0,29653,0,0,0,0,0,0,0,0,0,0,0,0,0,0,29656, +29659,0,0,29660,0,0,0,29661,0,0,0,0,0,29664,0,0,0,29671,29673,0,0,0,0,0,0,0, +29675,0,29677,29679,0,0,29684,0,0,0,0,0,29685,0,0,0,29687,0,0,0,29688,0,29689, +29690,29700,0,29701,0,0,0,29702,0,29706,0,0,0,0,0,0,0,29720,0,29721,0,29727,0, +29733,29734,0,29750,29761,0,29763,0,0,0,0,0,29764,0,0,29765,0,0,0,29771,0,0,0,0, +0,0,0,0,0,0,0,0,29772,0,0,0,29773,29774,29775,0,0,0,0,0,0,0,0,0,0,0,29822,0,0,0, +29824,0,29825,0,0,0,0,0,29827,0,0,0,0,0,0,0,0,29829,0,29832,29834,0,0,29835,0,0, +29837,29838,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,29843,0,0,0,0,29844,29845,0,0,0, +0,0,0,0,0,0,29849,0,0,29869,29872,29890,29905,0,0,0,0,0,29907,29921,0,29922,0,0, +29923,29926,29944,29946,0,0,0,0,0,0,0,29947,29948,0,0,0,29951,0,0,0,0,0,29953,0, +0,29956,0,29957,0,0,29962,0,0,0,0,29971,0,0,0,29972,0,0,0,0,0,29978,0,29979, +29992,30007,30008,30010,0,0,0,30013,0,0,0,0,30014,30016,0,0,0,0,0,0,0,0,0,0,0, +30017,0,0,0,0,0,30023,30031,0,0,30033,0,0,0,0,0,0,0,0,0,0,30034,0,30038,0,30039, +0,30040,0,0,0,0,0,0,30067,30068,0,0,0,30069,0,30072,0,0,0,30073,0,0,0,0,30075,0, +0,0,0,0,0,30079,0,0,30080,0,0,0,0,0,30082,0,0,0,0,0,0,0,0,0,0,0,30084,30090,0,0, +30091,0,0,0,0,30098,30118,0,30119,0,30121,30130,0,0,0,0,0,0,0,0,0,0,0,0,0,30131, +30132,30133,0,0,0,0,0,0,30135,0,0,0,0,0,0,0,0,0,0,0,30136,0,0,30137,30138,0,0,0, +30139,30146,0,0,0,0,0,30147,0,0,30148,30151,0,0,0,30168,0,30172,30173,0,0,0,0,0, +0,0,0,30180,30181,0,30192,0,0,0,0,0,0,0,30194,30196,0,0,30199,0,0,30202,0,0,0,0, +30203,0,0,0,0,0,0,0,0,0,0,30213,0,0,0,30216,0,0,30217,0,0,0,30218,0,0,0,0,30219, +0,30220,0,30222,30227,0,0,0,0,0,30231,0,0,30233,30235,0,0,0,0,30238,0,30240, +30243,30245,0,30250,30252,0,0,0,30269,0,0,30271,30272,0,0,0,30278,30280,0,0, +30282,0,30284,0,30294,0,0,0,0,30295,30296,0,0,0,0,0,30298,30299,30302,30304, +30306,0,0,0,0,0,0,30316,30317,0,0,0,30318,0,0,0,30319,0,30320,30322,30326,0,0,0, +0,0,30327,0,30332,30348,30349,0,0,30356,0,0,0,0,0,0,0,0,30357,0,30358,0,30359, +30360,0,0,30365,30366,30378,0,0,0,0,30379,0,0,30381,0,30385,0,30388,30397,0,0,0, +30401,0,0,0,0,30403,0,0,0,0,0,30404,0,0,30405,0,30406,30408,0,30409,0,30410,0,0, +0,30417,0,0,30418,30419,0,30420,0,30424,0,0,0,30427,30430,30432,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,30433,0,0,0,0,0,0,0,30436,0,30437,30438,0,30441,30442,0,0, +0,30445,0,0,0,0,30452,30456,30457,0,0,0,30458,0,30464,0,0,0,0,0,0,30467,0,30469, +0,0,0,0,0,30477,0,0,30484,0,0,0,0,0,30485,0,0,0,0,0,30486,30487,30497,30498,0,0, +0,0,0,0,0,0,0,0,30505,0,30508,0,0,0,30509,30510,0,30514,30516,0,0,0,0,0,0,0,0,0, +0,0,30523,0,30524,0,30525,0,0,0,0,30537,0,0,30538,0,0,0,0,0,30553,0,0,30555, +30556,30558,30559,30560,0,0,30561,0,30562,0,0,0,0,0,0,0,0,30563,30570,30571,0, +30586,30587,0,0,30590,0,0,30594,0,0,0,0,30611,30612,30623,30634,0,0,30636,30640, +30655,30656,0,30657,0,0,30658,30669,0,30670,0,30676,30678,0,0,0,0,0,0,0,30679,0, +0,0,0,0,0,0,0,0,0,0,30695,0,0,30698,0,0,0,0,30700,0,0,0,0,30701,0,30702,30703,0, +0,0,0,30707,0,0,0,30709,0,0,30710,30719,30729,0,0,0,0,0,0,0,0,0,30731,0,0,30733, +0,0,0,30734,0,0,0,0,0,30736,30737,0,0,0,30740,0,0,0,30743,0,30746,0,30747,30748, +0,0,30751,30752,30753,0,0,0,30754,0,0,30760,0,0,0,0,0,0,0,30763,0,30764,0,0, +30766,0,30769,30770,30771,30774,30777,0,0,30779,30780,30781,0,0,0,0,30790,0,0,0, +30792,0,0,0,0,30810,0,0,0,0,0,0,0,30812,30819,0,0,30823,30824,0,30825,0,30827,0, +0,0,0,0,0,30828,0,0,30830,0,0,0,30834,0,30835,0,30837,30838,0,30845,0,0,0,0,0, +30846,30847,0,0,30849,0,30851,0,0,0,0,0,30852,30858,0,0,30859,0,30865,0,0,30866, +0,0,30868,0,0,30869,0,0,0,30881,30883,0,0,0,0,0,30889,0,30891,0,0,0,0,30894,0, +30895,0,30897,0,30898,0,0,0,30904,30906,0,30909,0,0,0,0,0,0,30910,0,0,0,30915, +30933,30942,0,0,0,0,30943,0,0,30945,0,0,0,0,0,0,30946,0,0,30947,0,0,30955,30956, +0,0,30960,0,0,30961,30962,30966,0,0,30969,30974,0,0,0,30976,0,0,30977,0,30978, +30982,0,0,0,0,0,0,0,30994,30995,30998,0,31000,0,0,31001,0,0,31003,31005,0,0, +31006,31011,0,0,31014,0,31016,0,0,0,0,31018,0,0,31020,31023,31024,31025,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,31027,31028,31029,0,0,0,0,0,0,31032,0,0,0,0,0,0,0,0,0,0,0, +31036,31037,31038,0,0,0,31041,31043,31045,0,31047,0,0,0,31048,0,31049,0,0,0, +31053,31054,31055,0,0,31063,0,0,0,0,0,31066,0,31068,31071,0,0,0,31072,31073,0,0, +0,0,31075,0,0,31076,0,0,0,31077,31079,0,31080,0,0,0,0,0,0,0,0,0,0,31087,0,31142, +0,31144,0,0,31145,31146,31147,0,0,0,0,0,0,0,0,0,0,0,0,0,0,31149,0,31151,31152,0, +0,0,0,0,0,0,31162,31171,31174,31175,0,0,0,31176,0,0,0,0,0,0,0,31179,0,0,0,31186, +0,0,0,31192,31195,0,0,31196,0,0,0,0,0,0,0,0,31198,0,0,0,0,0,31199,0,0,0,31205,0, +0,0,0,31211,31215,0,0,0,0,31231,0,31232,0,0,0,0,0,0,0,0,0,0,31233,31236,31253,0, +31254,0,0,0,0,0,0,31255,0,0,31257,0,0,0,0,0,0,0,0,0,31258,31259,0,0,31260,0, +31261,0,0,0,0,0,31262,31263,0,0,31264,0,31266,0,31267,0,0,0,0,0,31281,0,31282,0, +31284,0,0,31285,31287,31288,0,0,31290,0,0,0,31292,31295,0,31299,0,31300,0,0,0,0, +0,31302,0,0,0,0,31303,0,0,0,0,0,0,31304,0,0,0,0,0,31305,31308,31309,31315,0, +31317,0,0,0,0,0,31323,0,31324,0,0,0,0,0,31325,31327,0,0,31331,0,0,0,0,0,31333,0, +0,0,0,0,31336,0,0,31337,0,0,0,0,0,0,31338,0,0,0,0,0,0,0,0,0,0,0,0,31339,0,0,0,0, +0,0,0,31342,0,0,0,0,31345,0,0,0,0,0,0,0,0,31347,0,0,0,0,0,0,31348,0,0,31350, +31351,0,31352,0,0,31354,0,0,0,0,31355,0,0,31356,0,0,0,0,0,0,0,0,0,0,31363,0, +31372,0,0,31373,0,0,0,0,0,0,0,0,0,31376,0,31388,0,31389,0,31392,0,31401,0,31405, +31407,31408,0,31409,0,0,0,0,0,0,31413,31415,0,0,0,31416,31418,0,0,0,0,0,0,31422, +31423,0,0,31424,0,31425,31432,0,0,0,0,0,0,0,0,0,31433,0,0,0,0,0,0,0,0,31434,0,0, +0,0,0,0,31435,0,0,0,0,31438,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,31442,0,31444,0, +31448,0,0,31451,0,0,0,0,31452,0,31461,31465,0,0,31466,0,0,31467,0,0,31468,0,0,0, +31469,31473,0,31476,0,0,0,0,31489,31490,0,0,0,0,0,0,0,31492,31493,31494,0,0,0,0, +31501,31504,31505,0,0,0,0,0,0,0,0,0,31509,0,0,0,0,31510,0,0,31511,0,0,31513,0,0, +0,0,0,0,0,0,0,31514,0,31522,31536,31539,31540,0,31541,0,0,0,0,0,0,31546,31553, +31559,0,0,0,31560,31561,31562,0,0,31564,31567,0,31569,0,0,0,31570,0,0,0,0,31571, +0,0,0,0,0,0,31572,31574,31580,31581,0,0,31582,31584,31585,31586,31595,0,31596,0, +0,0,0,31597,0,31599,0,31600,31601,0,0,31603,31604,0,0,31608,31610,0,0,0,31611,0, +31615,0,0,0,0,31616,0,0,0,0,0,0,31617,0,0,0,0,0,31618,0,0,0,0,0,0,31621,0,0,0,0, +0,0,0,0,0,31622,31625,0,0,0,0,31627,0,31641,0,0,31642,0,0,31643,0,0,0,0,0,0,0,0, +0,31644,0,31646,0,0,0,0,31648,0,0,0,31652,0,0,0,31657,0,0,31676,0,0,0,0,0,0,0, +31689,31691,31692,0,31694,0,0,0,31696,0,31702,0,31703,0}; + +const BROTLI_MODEL("small") +DictWord kStaticDictionaryWords[BROTLI_ENC_STATIC_DICT_LUT_NUM_ITEMS] = { +{0,0,0},{8,0,1002},{136,0,1015},{4,0,683},{4,10,325},{138,10,125},{7,11,572},{9, +11,592},{11,11,680},{11,11,842},{11,11,924},{12,11,356},{12,11,550},{13,11,317}, +{13,11,370},{13,11,469},{13,11,471},{14,11,397},{18,11,69},{146,11,145},{134,0, +1265},{136,11,534},{134,0,1431},{11,0,138},{140,0,40},{4,0,155},{4,10,718},{7,0, +1689},{135,10,1216},{4,0,245},{5,0,151},{5,0,741},{6,0,1147},{7,0,498},{7,0,870} +,{7,0,1542},{12,0,213},{14,0,36},{14,0,391},{17,0,111},{18,0,6},{18,0,46},{18,0, +151},{19,0,36},{20,0,32},{20,0,56},{20,0,69},{20,0,102},{21,0,4},{22,0,8},{22,0, +10},{22,0,14},{150,0,31},{4,0,624},{135,0,1752},{5,10,124},{5,10,144},{6,10,548} +,{7,10,15},{7,10,153},{137,10,629},{6,0,503},{7,10,1531},{8,10,416},{9,0,586},{9 +,10,275},{10,10,100},{11,10,658},{11,10,979},{12,10,86},{13,0,468},{14,0,66},{14 +,10,207},{15,10,20},{15,10,25},{144,0,58},{5,0,603},{5,10,915},{6,10,1783},{7,0, +1212},{7,10,211},{7,10,1353},{9,0,565},{9,10,83},{10,10,376},{10,10,431},{11,10, +543},{12,10,664},{13,10,280},{13,10,428},{14,0,301},{14,10,128},{17,10,52},{145, +10,81},{4,0,492},{133,0,451},{135,0,835},{141,0,70},{132,0,539},{7,11,748},{139, +11,700},{7,11,1517},{11,11,597},{14,11,76},{14,11,335},{148,11,33},{6,0,113},{ +135,0,436},{4,10,338},{133,10,400},{136,0,718},{133,11,127},{133,11,418},{6,0, +1505},{6,11,198},{7,0,520},{11,10,892},{140,11,83},{4,10,221},{5,10,659},{5,10, +989},{7,10,697},{7,10,1211},{138,10,284},{135,0,1070},{5,11,276},{6,11,55},{135, +11,1369},{134,0,1515},{6,11,1752},{136,11,726},{138,10,507},{4,10,188},{7,10,805 +},{143,0,78},{5,10,884},{139,10,991},{133,11,764},{134,10,1653},{6,11,309},{7,11 +,331},{138,11,550},{135,11,1861},{132,11,348},{135,11,986},{135,11,1573},{12,0, +610},{13,0,431},{144,0,59},{9,11,799},{140,10,166},{134,0,1530},{132,0,750},{132 +,0,307},{133,0,964},{6,11,194},{7,11,133},{10,11,493},{10,11,570},{139,11,664},{ +5,11,24},{5,11,569},{6,11,3},{6,11,119},{6,11,143},{6,11,440},{7,11,295},{7,11, +599},{7,11,1686},{7,11,1854},{8,11,424},{9,11,43},{9,11,584},{9,11,760},{10,11, +148},{10,11,328},{11,11,159},{11,11,253},{11,11,506},{12,11,487},{12,11,531},{ +144,11,33},{136,10,760},{5,11,14},{5,11,892},{6,11,283},{7,11,234},{136,11,537}, +{135,11,1251},{4,11,126},{8,11,635},{147,11,34},{4,11,316},{135,11,1561},{6,0, +999},{6,0,1310},{137,11,861},{4,11,64},{5,11,352},{5,11,720},{6,11,368},{139,11, +359},{4,0,75},{5,0,180},{6,0,500},{7,0,58},{7,0,710},{8,10,770},{138,0,645},{133 +,0,649},{6,0,276},{7,0,282},{7,0,879},{7,0,924},{8,0,459},{9,0,599},{9,0,754},{ +11,0,574},{12,0,128},{12,0,494},{13,0,52},{13,0,301},{15,0,30},{143,0,132},{132, +0,200},{4,10,89},{5,10,489},{6,10,315},{7,10,553},{7,10,1745},{138,10,243},{135, +11,1050},{6,10,1658},{7,0,1621},{9,10,3},{10,10,154},{11,10,641},{13,10,85},{13, +10,201},{141,10,346},{6,11,175},{137,11,289},{5,11,432},{133,11,913},{6,0,225},{ +137,0,211},{7,0,718},{8,0,687},{139,0,374},{4,10,166},{133,10,505},{6,10,1670},{ +137,0,110},{8,0,58},{9,0,724},{11,0,809},{13,0,113},{145,0,72},{6,0,345},{7,0, +1247},{144,11,82},{5,11,931},{134,11,1698},{8,0,767},{8,0,803},{9,0,301},{137,0, +903},{139,0,203},{134,0,1154},{7,0,1949},{136,0,674},{134,0,259},{135,0,1275},{5 +,11,774},{6,11,1637},{6,11,1686},{134,11,1751},{134,0,1231},{7,10,445},{8,10,307 +},{8,10,704},{10,10,41},{10,10,439},{11,10,237},{11,10,622},{140,10,201},{136,0, +254},{6,11,260},{135,11,1484},{139,0,277},{135,10,1977},{4,10,189},{5,10,713},{6 +,11,573},{136,10,57},{138,10,371},{132,10,552},{134,11,344},{133,0,248},{6,11, +240},{9,0,800},{10,0,693},{11,0,482},{11,0,734},{139,0,789},{4,0,116},{4,11,292} +,{4,11,736},{5,0,95},{5,0,445},{5,11,871},{6,11,171},{6,11,1689},{7,0,1688},{7, +11,1324},{7,11,1944},{8,0,29},{9,0,272},{9,11,415},{9,11,580},{11,0,509},{11,0, +915},{14,11,230},{146,11,68},{7,0,490},{13,0,100},{143,0,75},{135,0,1641},{133,0 +,543},{7,11,209},{8,11,661},{10,11,42},{11,11,58},{12,11,58},{12,11,118},{141,11 +,32},{5,0,181},{6,11,63},{7,11,920},{136,0,41},{133,0,657},{133,11,793},{138,0, +709},{7,0,25},{8,0,202},{138,0,536},{5,11,665},{135,10,1788},{145,10,49},{9,0, +423},{140,0,89},{5,11,67},{6,11,62},{6,11,374},{135,11,1391},{8,0,113},{9,0,877} +,{9,11,790},{10,0,554},{11,0,83},{12,0,136},{12,11,47},{147,0,109},{138,10,661}, +{4,0,963},{7,10,1945},{10,0,927},{142,0,442},{133,0,976},{132,0,206},{4,11,391}, +{135,11,1169},{134,0,2002},{6,0,696},{134,0,1008},{134,0,1170},{132,11,271},{7,0 +,13},{8,0,226},{10,0,537},{11,0,570},{11,0,605},{11,0,799},{11,0,804},{12,0,85}, +{12,0,516},{12,0,623},{12,11,314},{13,0,112},{13,0,361},{14,0,77},{14,0,78},{17, +0,28},{147,0,110},{132,0,769},{134,0,1544},{4,0,551},{137,0,678},{5,10,84},{134, +10,163},{9,0,57},{9,0,459},{10,0,425},{11,0,119},{12,0,184},{12,0,371},{13,0,358 +},{145,0,51},{4,11,253},{5,0,188},{5,0,814},{5,10,410},{5,11,544},{7,11,300},{8, +0,10},{9,0,421},{9,0,729},{9,11,340},{10,0,609},{139,0,689},{134,0,624},{138,11, +321},{135,0,1941},{5,10,322},{8,10,186},{9,10,262},{10,10,187},{14,10,208},{146, +0,130},{5,11,53},{5,11,541},{6,11,94},{6,11,499},{7,11,230},{139,11,321},{133,10 +,227},{4,0,378},{4,11,920},{5,11,25},{5,11,790},{6,11,457},{135,11,853},{137,0, +269},{132,0,528},{134,0,1146},{7,10,1395},{8,10,486},{9,10,236},{9,10,878},{10, +10,218},{11,10,95},{19,10,17},{147,10,31},{7,10,2043},{8,10,672},{141,10,448},{ +134,0,1105},{134,0,1616},{134,11,1765},{140,11,163},{5,10,412},{133,11,822},{132 +,11,634},{6,0,656},{134,11,1730},{134,0,1940},{5,0,104},{6,0,173},{135,0,1631},{ +136,10,562},{6,11,36},{7,11,658},{8,11,454},{147,11,86},{5,0,457},{134,10,1771}, +{7,0,810},{8,0,138},{8,0,342},{9,0,84},{10,0,193},{11,0,883},{140,0,359},{7,10, +1190},{137,0,620},{137,10,132},{7,11,975},{137,11,789},{6,0,95},{6,0,1934},{136, +0,967},{141,11,335},{6,0,406},{10,0,409},{10,0,447},{11,0,44},{140,0,100},{4,10, +317},{135,10,1279},{132,0,477},{134,0,1268},{5,10,63},{5,10,509},{6,0,1941},{136 +,0,944},{132,0,629},{132,11,104},{4,0,246},{133,0,375},{4,10,288},{134,0,1636},{ +135,11,1614},{8,10,89},{8,10,620},{9,0,49},{10,0,774},{11,10,628},{12,10,322},{ +143,10,124},{4,0,282},{7,0,1034},{11,0,398},{11,0,634},{12,0,1},{12,0,79},{12,0, +544},{14,0,237},{17,0,10},{146,0,20},{132,0,824},{7,11,45},{9,11,542},{9,11,566} +,{138,11,728},{5,0,118},{5,0,499},{6,0,476},{6,0,665},{6,0,1176},{6,0,1196},{7,0 +,600},{7,0,888},{135,0,1096},{7,0,296},{7,0,596},{8,0,560},{8,0,586},{9,0,612},{ +11,0,304},{12,0,46},{13,0,89},{14,0,112},{145,0,122},{5,0,894},{6,0,1772},{9,0, +1009},{138,10,120},{5,11,533},{7,11,755},{138,11,780},{151,10,1},{6,0,1474},{7, +11,87},{142,11,288},{139,0,366},{137,10,461},{7,11,988},{7,11,1939},{9,11,64},{9 +,11,502},{12,11,7},{12,11,34},{13,11,12},{13,11,234},{147,11,77},{7,0,1599},{7,0 +,1723},{8,0,79},{8,0,106},{8,0,190},{8,0,302},{8,0,383},{8,0,713},{9,0,119},{9,0 +,233},{9,0,419},{9,0,471},{10,0,181},{10,0,406},{11,0,57},{11,0,85},{11,0,120},{ +11,0,177},{11,0,296},{11,0,382},{11,0,454},{11,0,758},{11,0,999},{12,0,27},{12,0 +,98},{12,0,131},{12,0,245},{12,0,312},{12,0,446},{12,0,454},{13,0,25},{13,0,98}, +{13,0,426},{13,0,508},{14,0,70},{14,0,163},{14,0,272},{14,0,277},{14,0,370},{15, +0,95},{15,0,138},{15,0,167},{17,0,38},{148,0,96},{135,10,1346},{10,0,200},{19,0, +2},{151,0,22},{135,11,141},{134,10,85},{134,0,1759},{138,0,372},{145,0,16},{4,11 +,619},{136,0,943},{139,11,88},{5,11,246},{8,11,189},{9,11,355},{9,11,512},{10,11 +,124},{10,11,453},{11,11,143},{11,11,416},{11,11,859},{141,11,341},{5,0,258},{ +134,0,719},{6,0,1798},{6,0,1839},{8,0,900},{10,0,874},{10,0,886},{12,0,698},{12, +0,732},{12,0,770},{16,0,106},{18,0,163},{18,0,170},{18,0,171},{152,0,20},{9,0, +707},{9,11,707},{11,0,326},{11,0,339},{11,11,326},{11,11,339},{12,0,423},{12,0, +502},{12,11,423},{12,11,502},{20,0,62},{148,11,62},{5,0,30},{7,0,495},{8,0,134}, +{9,0,788},{140,0,438},{133,11,678},{5,10,279},{6,10,235},{7,10,468},{8,10,446},{ +9,10,637},{10,10,717},{11,10,738},{140,10,514},{5,11,35},{6,11,287},{7,11,862},{ +7,11,1886},{138,11,179},{4,11,517},{7,0,1948},{135,0,2004},{5,10,17},{6,10,371}, +{137,10,528},{4,0,115},{5,0,669},{6,0,407},{8,0,311},{11,0,10},{141,0,5},{137,0, +381},{5,0,50},{6,0,439},{7,0,780},{135,0,1040},{136,11,667},{11,11,403},{146,11, +83},{5,0,1},{6,0,81},{138,0,520},{134,0,738},{5,0,482},{8,0,98},{9,0,172},{10,0, +360},{10,0,700},{10,0,822},{11,0,302},{11,0,778},{12,0,50},{12,0,127},{12,0,396} +,{13,0,62},{13,0,328},{14,0,122},{147,0,72},{9,11,157},{10,11,131},{140,11,72},{ +135,11,714},{135,11,539},{5,0,2},{6,0,512},{7,0,797},{7,0,1494},{8,0,253},{8,0, +589},{9,0,77},{10,0,1},{10,0,129},{10,0,225},{11,0,118},{11,0,226},{11,0,251},{ +11,0,430},{11,0,701},{11,0,974},{11,0,982},{12,0,64},{12,0,260},{12,0,488},{140, +0,690},{5,11,394},{7,11,367},{7,11,487},{7,11,857},{7,11,1713},{8,11,246},{9,11, +537},{10,11,165},{12,11,219},{140,11,561},{136,0,557},{5,10,779},{5,10,807},{6, +10,1655},{134,10,1676},{4,10,196},{5,10,558},{133,10,949},{11,11,827},{12,11,56} +,{14,11,34},{143,11,148},{137,0,347},{133,0,572},{134,0,832},{4,0,12},{7,0,504}, +{7,0,522},{7,0,809},{8,0,797},{141,0,88},{4,10,752},{133,11,449},{7,11,86},{8,11 +,103},{145,11,69},{7,11,2028},{138,11,641},{5,0,528},{6,11,1},{142,11,2},{134,0, +861},{4,10,227},{5,10,159},{5,10,409},{7,10,80},{10,0,294},{10,10,479},{12,10, +418},{14,10,50},{14,10,249},{142,10,295},{7,10,1470},{8,10,66},{8,10,137},{8,10, +761},{9,10,638},{11,10,80},{11,10,212},{11,10,368},{11,10,418},{12,10,8},{13,10, +15},{16,10,61},{17,10,59},{19,10,28},{148,10,84},{7,11,1148},{148,0,109},{6,11, +277},{7,11,1274},{7,11,1386},{7,11,1392},{12,11,129},{146,11,87},{6,11,187},{7, +11,39},{7,11,1203},{8,11,380},{8,11,542},{14,11,117},{149,11,28},{134,0,1187},{5 +,0,266},{9,0,290},{9,0,364},{10,0,293},{11,0,606},{142,0,45},{6,11,297},{7,11, +793},{139,11,938},{4,0,50},{6,0,594},{9,0,121},{10,0,49},{10,0,412},{139,0,834}, +{136,0,748},{7,11,464},{8,11,438},{11,11,105},{11,11,363},{12,11,231},{14,11,386 +},{15,11,102},{148,11,75},{132,0,466},{6,10,38},{7,10,1220},{8,10,185},{8,10,256 +},{9,10,22},{9,10,331},{10,10,738},{11,10,205},{11,10,540},{11,10,746},{13,0,399 +},{13,10,465},{14,0,337},{142,10,194},{9,0,378},{141,0,162},{137,0,519},{4,10, +159},{6,10,115},{7,10,252},{7,10,257},{7,10,1928},{8,10,69},{9,10,384},{10,10,91 +},{10,10,615},{12,10,375},{14,10,235},{18,10,117},{147,10,123},{5,10,911},{5,11, +604},{136,10,278},{132,0,667},{4,10,151},{7,10,1567},{8,0,351},{137,0,322},{134, +0,902},{133,10,990},{5,10,194},{7,10,1662},{9,10,90},{140,0,180},{4,0,869},{134, +0,1996},{134,0,813},{133,10,425},{137,11,761},{132,0,260},{133,10,971},{5,11,20} +,{6,11,298},{7,11,659},{7,11,1366},{137,11,219},{4,0,39},{5,0,36},{7,0,1843},{8, +0,407},{11,0,144},{140,0,523},{4,0,510},{10,0,587},{139,10,752},{7,0,29},{7,0,66 +},{7,0,1980},{10,0,487},{138,0,809},{9,10,662},{13,0,260},{14,0,82},{146,0,63},{ +5,10,72},{6,10,264},{7,10,21},{7,10,46},{7,10,2013},{8,10,215},{8,10,513},{10,10 +,266},{139,10,22},{134,0,570},{4,11,439},{6,0,565},{7,0,1667},{10,10,95},{11,10, +603},{12,11,242},{13,10,443},{14,10,160},{143,10,4},{134,0,1464},{134,10,431},{5 +,10,874},{6,10,1677},{9,0,372},{15,0,2},{15,10,0},{19,0,10},{147,0,18},{132,0, +787},{6,0,380},{7,10,939},{7,10,1172},{7,10,1671},{9,10,540},{10,10,696},{11,10, +265},{11,10,732},{11,10,928},{11,10,937},{12,0,399},{13,10,438},{149,0,19},{137, +0,200},{132,11,233},{132,0,516},{134,11,577},{132,0,844},{11,0,887},{14,0,365},{ +142,0,375},{132,11,482},{8,0,821},{140,0,44},{7,0,1655},{136,0,305},{5,10,682},{ +135,10,1887},{135,11,346},{132,10,696},{4,0,10},{7,0,917},{139,0,786},{5,11,795} +,{6,11,1741},{8,11,417},{137,11,782},{4,0,1016},{134,0,2031},{4,10,726},{5,0,684 +},{133,10,630},{6,0,1021},{134,0,1480},{8,10,802},{136,10,838},{134,0,27},{134,0 +,395},{135,11,622},{7,11,625},{135,11,1750},{4,11,203},{135,11,1936},{6,10,118}, +{7,10,215},{7,10,1521},{140,10,11},{132,0,813},{136,0,511},{7,10,615},{138,10, +251},{135,10,1044},{145,0,56},{133,10,225},{6,0,342},{6,0,496},{8,0,275},{137,0, +206},{4,0,909},{133,0,940},{132,0,891},{7,11,311},{9,11,308},{140,11,255},{4,10, +370},{5,10,756},{135,10,1326},{4,0,687},{134,0,1596},{134,0,1342},{6,10,1662},{7 +,10,48},{8,10,771},{10,10,116},{13,10,104},{14,10,105},{14,10,184},{15,10,168},{ +19,10,92},{148,10,68},{138,10,209},{4,11,400},{5,11,267},{135,11,232},{151,11,12 +},{6,0,41},{141,0,160},{141,11,314},{134,0,1718},{136,0,778},{142,11,261},{134,0 +,1610},{133,0,115},{132,0,294},{4,10,120},{142,0,314},{132,0,983},{5,0,193},{140 +,0,178},{138,10,429},{5,10,820},{135,10,931},{5,11,732},{6,0,994},{6,0,1051},{6, +0,1439},{135,0,174},{4,11,100},{7,11,679},{8,11,313},{138,10,199},{6,10,151},{6, +10,1675},{7,10,383},{151,10,10},{6,0,1796},{8,0,848},{8,0,867},{8,0,907},{10,0, +855},{140,0,703},{140,0,221},{4,0,122},{5,0,796},{5,0,952},{6,0,1660},{6,0,1671} +,{8,0,567},{9,0,687},{9,0,742},{10,0,686},{11,0,682},{11,0,909},{140,0,281},{5, +11,362},{5,11,443},{6,11,318},{7,11,1019},{139,11,623},{5,11,463},{136,11,296},{ +6,10,1624},{11,0,583},{12,10,422},{13,0,262},{142,10,360},{5,0,179},{7,0,1095},{ +135,0,1213},{4,10,43},{4,11,454},{5,10,344},{133,10,357},{4,0,66},{7,0,722},{135 +,0,904},{134,0,773},{5,10,888},{135,0,352},{5,11,48},{5,11,404},{6,11,557},{7,11 +,458},{8,11,597},{10,11,455},{10,11,606},{11,11,49},{11,11,548},{12,11,476},{13, +11,18},{141,11,450},{134,11,418},{132,10,711},{5,11,442},{135,11,1984},{141,0,35 +},{137,0,152},{134,0,1197},{135,11,1093},{137,11,203},{137,10,440},{10,0,592},{ +10,0,753},{12,0,317},{12,0,355},{12,0,465},{12,0,469},{12,0,560},{12,0,578},{141 +,0,243},{133,0,564},{134,0,797},{5,10,958},{133,10,987},{5,11,55},{7,11,376},{ +140,11,161},{133,11,450},{134,0,556},{134,0,819},{11,10,276},{142,10,293},{7,0, +544},{138,0,61},{4,10,65},{5,10,479},{5,10,1004},{7,10,1913},{8,0,719},{8,10,317 +},{9,10,302},{10,10,612},{141,10,22},{4,0,5},{4,10,261},{4,11,213},{5,0,498},{7, +10,510},{7,11,223},{8,0,637},{8,11,80},{137,0,521},{4,10,291},{5,0,927},{7,0,101 +},{7,11,381},{7,11,806},{7,11,820},{8,11,354},{8,11,437},{8,11,787},{9,10,515},{ +9,11,657},{10,11,58},{10,11,339},{10,11,749},{11,11,914},{12,10,152},{12,10,443} +,{12,11,162},{13,10,392},{13,11,75},{14,10,357},{14,11,106},{14,11,198},{14,11, +320},{14,11,413},{146,11,43},{6,0,1153},{7,0,1441},{136,11,747},{4,0,893},{5,0, +780},{133,0,893},{138,11,654},{133,11,692},{133,0,238},{134,11,191},{4,10,130},{ +135,10,843},{5,10,42},{5,10,879},{6,0,1296},{7,10,245},{7,10,324},{7,10,1532},{ +11,10,463},{11,10,472},{13,10,363},{144,10,52},{134,0,1729},{6,0,1999},{136,0, +969},{4,10,134},{133,10,372},{4,0,60},{7,0,941},{7,0,1800},{8,0,314},{9,0,700},{ +139,0,487},{134,0,1144},{6,11,162},{7,11,1960},{136,11,831},{132,11,706},{135,0, +1147},{138,11,426},{138,11,89},{7,0,1853},{138,0,437},{136,0,419},{135,10,1634}, +{133,0,828},{4,10,644},{5,0,806},{7,0,176},{7,0,178},{7,0,1240},{135,0,1976},{ +135,11,1877},{5,11,420},{135,11,1449},{4,0,51},{5,0,39},{6,0,4},{7,0,591},{7,0, +849},{7,0,951},{7,0,1613},{7,0,1760},{7,0,1988},{9,0,434},{10,0,754},{11,0,25},{ +139,0,37},{10,11,57},{138,11,277},{135,10,540},{132,11,204},{135,0,159},{139,11, +231},{133,0,902},{7,0,928},{7,11,366},{9,11,287},{12,11,199},{12,11,556},{140,11 +,577},{6,10,623},{136,10,789},{4,10,908},{5,10,359},{5,10,508},{6,10,1723},{7,10 +,343},{7,10,1996},{135,10,2026},{134,0,270},{4,10,341},{135,10,480},{5,11,356},{ +135,11,224},{11,11,588},{11,11,864},{11,11,968},{143,11,160},{132,0,556},{137,0, +801},{132,0,416},{142,0,372},{5,0,152},{5,0,197},{7,0,340},{7,0,867},{10,0,548}, +{10,0,581},{11,0,6},{12,0,3},{12,0,19},{14,0,110},{142,0,289},{139,0,369},{7,11, +630},{9,11,567},{11,11,150},{11,11,444},{141,11,119},{134,11,539},{7,10,1995},{8 +,10,299},{11,10,890},{140,10,674},{7,0,34},{7,0,190},{8,0,28},{8,0,141},{8,0,444 +},{8,0,811},{9,0,468},{11,0,334},{12,0,24},{12,0,386},{140,0,576},{133,0,757},{7 +,0,1553},{136,0,898},{133,0,721},{136,0,1012},{4,0,789},{5,0,647},{135,0,1102},{ +132,0,898},{4,10,238},{5,10,503},{6,10,179},{7,10,2003},{8,10,381},{8,10,473},{9 +,10,149},{10,0,183},{10,10,788},{15,10,45},{15,10,86},{20,10,110},{150,10,57},{4 +,10,121},{5,10,156},{5,10,349},{9,0,136},{10,10,605},{14,10,342},{147,0,107},{4, +11,235},{135,11,255},{4,11,194},{5,11,584},{6,11,384},{7,11,583},{10,11,761},{11 +,11,760},{139,11,851},{6,10,80},{6,10,1694},{7,10,173},{7,10,1974},{9,10,547},{ +10,10,730},{14,10,18},{150,10,39},{4,10,923},{134,10,1711},{5,0,277},{141,0,247} +,{132,0,435},{133,11,562},{134,0,1311},{5,11,191},{137,11,271},{132,10,595},{7, +11,1537},{14,11,96},{143,11,73},{5,0,437},{6,10,459},{7,0,502},{7,0,519},{7,0, +1122},{7,0,1751},{7,10,1753},{7,10,1805},{8,10,658},{9,10,1},{11,10,959},{13,10, +446},{142,0,211},{4,11,470},{5,11,473},{6,0,814},{6,11,153},{7,11,1503},{7,11, +1923},{10,11,701},{11,11,132},{11,11,168},{11,11,227},{11,11,320},{11,11,436},{ +11,11,525},{11,11,855},{12,11,41},{12,11,286},{13,11,103},{13,11,284},{14,11,255 +},{14,11,262},{15,11,117},{143,11,127},{5,0,265},{6,0,212},{135,0,28},{138,0,750 +},{133,11,327},{6,11,552},{7,11,1754},{137,11,604},{134,0,2012},{132,0,702},{5, +11,80},{6,11,405},{7,11,403},{7,11,1502},{7,11,1626},{8,11,456},{9,11,487},{9,11 +,853},{9,11,889},{10,11,309},{11,11,721},{11,11,994},{12,11,430},{141,11,165},{5 +,0,808},{135,0,2045},{5,0,166},{8,0,739},{140,0,511},{134,10,490},{4,11,453},{5, +11,887},{6,11,535},{8,11,6},{136,11,543},{4,0,119},{5,0,170},{5,0,447},{7,0,1708 +},{7,0,1889},{9,0,357},{9,0,719},{12,0,486},{140,0,596},{137,0,500},{7,10,250},{ +136,10,507},{132,10,158},{6,0,809},{134,0,1500},{4,10,140},{7,10,362},{8,10,209} +,{9,0,327},{9,10,10},{9,10,503},{9,10,614},{10,10,689},{11,0,350},{11,0,831},{11 +,10,327},{11,10,725},{12,10,252},{12,10,583},{13,0,352},{13,10,192},{14,10,269}, +{14,10,356},{148,10,50},{135,11,741},{4,0,450},{7,0,1158},{19,10,1},{19,10,26},{ +150,10,9},{6,0,597},{135,0,1318},{134,0,1602},{6,10,228},{7,10,1341},{9,10,408}, +{138,10,343},{7,0,1375},{7,0,1466},{138,0,331},{132,0,754},{132,10,557},{5,11, +101},{6,11,88},{6,11,543},{7,11,1677},{9,11,100},{10,11,677},{14,11,169},{14,11, +302},{14,11,313},{15,11,48},{143,11,84},{134,0,1368},{4,11,310},{9,11,795},{10, +11,733},{11,11,451},{12,11,249},{14,11,115},{14,11,286},{143,11,100},{132,10,548 +},{7,10,197},{8,10,142},{8,10,325},{9,10,150},{9,10,596},{10,0,557},{10,10,353}, +{11,10,74},{11,10,315},{12,10,662},{12,10,681},{14,10,423},{143,10,141},{133,11, +587},{5,0,850},{136,0,799},{10,0,908},{12,0,701},{12,0,757},{142,0,466},{4,0,62} +,{5,0,275},{6,10,399},{6,10,579},{7,10,692},{7,10,846},{7,10,1015},{7,10,1799},{ +8,10,403},{9,10,394},{10,10,133},{12,10,4},{12,10,297},{12,10,452},{16,10,81},{ +18,0,19},{18,10,25},{21,10,14},{22,10,12},{151,10,18},{7,10,1546},{11,10,299},{ +12,0,459},{142,10,407},{132,10,177},{132,11,498},{7,11,217},{8,11,140},{138,11, +610},{5,10,411},{135,10,653},{134,0,1802},{7,10,439},{10,10,727},{11,10,260},{ +139,10,684},{133,11,905},{11,11,580},{142,11,201},{134,0,1397},{5,10,208},{7,10, +753},{135,10,1528},{7,0,238},{7,0,2033},{8,0,120},{8,0,188},{8,0,659},{9,0,598}, +{10,0,466},{12,0,342},{12,0,588},{13,0,503},{14,0,246},{143,0,92},{135,11,1041}, +{4,11,456},{7,11,105},{7,11,358},{7,11,1637},{8,11,643},{139,11,483},{6,0,1318}, +{134,0,1324},{4,0,201},{5,10,242},{7,0,1744},{8,0,602},{11,0,247},{11,0,826},{ +145,0,65},{8,0,164},{146,0,62},{133,10,953},{139,10,802},{133,0,615},{7,11,1566} +,{8,11,269},{9,11,212},{9,11,718},{14,11,15},{14,11,132},{142,11,227},{133,10, +290},{132,10,380},{5,10,52},{7,10,277},{9,10,368},{139,10,791},{135,0,1243},{133 +,11,539},{11,11,919},{141,11,409},{136,0,968},{133,11,470},{134,0,882},{132,0, +907},{5,0,100},{10,0,329},{12,0,416},{149,0,29},{10,10,138},{139,10,476},{5,10, +725},{5,10,727},{6,11,91},{7,10,1811},{135,11,435},{4,11,16},{5,11,316},{5,11, +842},{6,11,370},{6,11,1778},{8,11,166},{11,11,812},{12,11,206},{12,11,351},{14, +11,418},{16,11,15},{16,11,34},{18,11,3},{19,11,3},{19,11,7},{20,11,4},{149,11,21 +},{132,0,176},{5,0,636},{5,0,998},{7,0,9},{7,0,1508},{8,0,26},{9,0,317},{9,0,358 +},{10,0,210},{10,0,292},{10,0,533},{11,0,555},{12,0,526},{12,0,607},{13,0,263},{ +13,0,459},{142,0,271},{4,10,38},{6,0,256},{7,10,307},{7,10,999},{7,10,1481},{7, +10,1732},{7,10,1738},{8,0,265},{9,10,414},{11,10,316},{12,10,52},{13,10,420},{ +147,10,100},{135,10,1296},{4,11,611},{133,11,606},{4,0,643},{142,11,21},{133,11, +715},{133,10,723},{6,0,610},{135,11,597},{10,0,127},{141,0,27},{6,0,1995},{6,0, +2001},{8,0,119},{136,0,973},{4,11,149},{138,11,368},{4,11,154},{5,10,109},{6,10, +1784},{7,10,1895},{7,11,1134},{8,11,105},{12,0,522},{12,10,296},{140,10,302},{4, +11,31},{6,11,429},{7,11,962},{9,11,458},{139,11,691},{4,11,312},{5,10,216},{7,10 +,1879},{9,10,141},{9,10,270},{9,10,679},{10,0,553},{10,10,159},{11,0,876},{11,10 +,197},{12,10,538},{12,10,559},{13,0,193},{13,0,423},{14,0,166},{14,10,144},{14, +10,167},{15,10,67},{147,0,84},{134,0,1582},{7,0,1578},{135,11,1578},{137,10,81}, +{132,11,236},{134,10,391},{134,0,795},{7,10,322},{136,10,249},{5,11,836},{5,11, +857},{6,11,1680},{7,11,59},{147,11,53},{135,0,432},{10,11,68},{139,11,494},{4,11 +,81},{139,11,867},{7,0,126},{136,0,84},{142,11,280},{5,11,282},{8,11,650},{9,11, +295},{9,11,907},{138,11,443},{136,0,790},{5,10,632},{138,10,526},{6,0,64},{12,0, +377},{13,0,309},{14,0,141},{14,0,429},{14,11,141},{142,11,429},{134,0,1529},{6,0 +,321},{7,0,1857},{7,10,948},{7,10,1042},{8,10,235},{8,10,461},{9,0,530},{9,10, +453},{10,10,354},{17,10,77},{147,0,99},{7,0,1104},{11,0,269},{11,0,539},{11,0, +627},{11,0,706},{11,0,975},{12,0,248},{12,0,434},{12,0,600},{12,0,622},{13,0,297 +},{13,0,485},{14,0,69},{14,0,409},{143,0,108},{4,10,362},{7,10,52},{7,10,303},{ +10,11,70},{12,11,26},{14,11,17},{14,11,178},{15,11,34},{149,11,12},{11,0,977},{ +141,0,507},{9,0,34},{139,0,484},{5,10,196},{6,10,486},{7,10,212},{8,10,309},{136 +,10,346},{6,0,1700},{7,0,26},{7,0,293},{7,0,382},{7,0,1026},{7,0,1087},{7,0,2027 +},{7,10,1912},{8,0,24},{8,0,114},{8,0,252},{8,0,727},{8,0,729},{9,0,30},{9,0,199 +},{9,0,231},{9,0,251},{9,0,334},{9,0,361},{9,0,712},{10,0,55},{10,0,60},{10,0, +232},{10,0,332},{10,0,384},{10,0,396},{10,0,504},{10,0,542},{10,0,652},{11,0,20} +,{11,0,48},{11,0,207},{11,0,291},{11,0,298},{11,0,342},{11,0,365},{11,0,394},{11 +,0,620},{11,0,705},{11,0,1017},{12,0,123},{12,0,340},{12,0,406},{12,0,643},{13,0 +,61},{13,0,269},{13,0,311},{13,0,319},{13,0,486},{14,0,234},{15,0,62},{15,0,85}, +{16,0,71},{18,0,119},{148,0,105},{4,11,71},{5,11,376},{7,11,119},{138,11,665},{4 +,10,686},{8,11,55},{10,0,918},{138,0,926},{138,10,625},{136,10,706},{132,11,479} +,{4,10,30},{133,10,43},{6,0,379},{7,0,270},{7,11,607},{8,0,176},{8,0,183},{8,11, +99},{9,0,432},{9,0,661},{12,0,247},{12,0,617},{18,0,125},{152,11,4},{5,0,792},{ +133,0,900},{4,11,612},{133,11,561},{4,10,220},{4,11,41},{5,11,74},{7,10,1535},{7 +,11,1627},{11,11,871},{140,11,619},{135,0,1920},{7,11,94},{11,11,329},{11,11,965 +},{12,11,241},{14,11,354},{15,11,22},{148,11,63},{9,11,209},{137,11,300},{134,0, +771},{135,0,1979},{4,0,901},{133,0,776},{142,0,254},{133,11,98},{9,11,16},{141, +11,386},{133,11,984},{4,11,182},{6,11,205},{135,11,220},{7,10,1725},{7,10,1774}, +{138,10,393},{5,10,263},{134,10,414},{4,11,42},{9,11,205},{9,11,786},{138,11,659 +},{14,0,140},{148,0,41},{6,10,178},{6,10,1750},{6,11,289},{7,11,1670},{8,0,440}, +{9,10,690},{10,0,359},{10,10,155},{10,10,373},{11,10,698},{12,11,57},{13,10,155} +,{20,10,93},{151,11,4},{4,0,37},{5,0,334},{7,0,1253},{151,11,25},{4,0,508},{4,11 +,635},{5,10,97},{137,10,393},{139,11,533},{4,0,640},{133,0,513},{134,10,1639},{ +132,11,371},{4,11,272},{7,11,836},{7,11,1651},{145,11,89},{5,11,825},{6,11,444}, +{6,11,1640},{136,11,308},{4,10,191},{7,10,934},{8,10,647},{145,10,97},{12,0,246} +,{15,0,162},{19,0,64},{20,0,8},{20,0,95},{22,0,24},{152,0,17},{4,0,533},{5,10, +165},{9,10,346},{138,10,655},{5,11,737},{139,10,885},{133,10,877},{8,10,128},{ +139,10,179},{137,11,307},{140,0,752},{133,0,920},{135,0,1048},{5,0,153},{6,0,580 +},{6,10,1663},{7,10,132},{7,10,1154},{7,10,1415},{7,10,1507},{12,10,493},{15,10, +105},{151,10,15},{5,10,459},{7,10,1073},{8,10,241},{136,10,334},{138,0,391},{135 +,0,1952},{133,11,525},{8,11,641},{11,11,388},{140,11,580},{142,0,126},{134,0,640 +},{132,0,483},{6,10,324},{6,10,520},{7,0,1616},{7,10,338},{7,10,1729},{8,10,228} +,{9,0,69},{139,10,750},{5,11,493},{134,11,528},{135,0,734},{4,11,174},{135,11, +911},{138,0,480},{9,0,495},{146,0,104},{135,10,705},{4,10,73},{6,10,612},{7,10, +927},{7,10,1330},{7,10,1822},{8,10,217},{9,0,472},{9,10,765},{9,10,766},{10,10, +408},{11,10,51},{11,10,793},{12,10,266},{15,10,158},{20,10,89},{150,10,32},{7,11 +,548},{137,11,58},{4,11,32},{5,11,215},{6,11,269},{7,11,1782},{7,11,1892},{10,11 +,16},{11,11,822},{11,11,954},{141,11,481},{132,0,874},{5,10,389},{8,10,636},{137 +,0,229},{7,11,1749},{136,11,477},{134,0,948},{5,11,308},{135,11,1088},{4,0,748}, +{139,0,1009},{136,10,21},{6,0,555},{135,0,485},{5,11,126},{8,11,297},{9,11,366}, +{9,11,445},{12,11,53},{12,11,374},{141,11,492},{7,11,1551},{139,11,361},{136,0, +193},{136,0,472},{8,0,653},{13,0,93},{147,0,14},{132,0,984},{132,11,175},{4,11, +685},{5,0,172},{134,0,1971},{149,11,8},{133,11,797},{5,10,189},{7,10,442},{7,10, +443},{8,10,281},{12,10,174},{13,0,83},{141,10,261},{134,0,1568},{133,11,565},{ +139,0,384},{133,0,260},{6,10,2},{7,0,758},{7,0,880},{7,0,1359},{7,10,1262},{7,10 +,1737},{8,10,22},{8,10,270},{8,10,612},{9,0,164},{9,0,167},{9,10,312},{9,10,436} +,{10,0,156},{10,0,588},{10,10,311},{10,10,623},{11,10,72},{11,10,330},{11,10,455 +},{12,0,101},{12,10,321},{12,10,504},{12,10,530},{12,10,543},{13,10,17},{13,10, +156},{13,10,334},{14,0,48},{15,0,70},{17,10,60},{148,10,64},{4,11,252},{7,11, +1068},{10,11,434},{11,11,228},{11,11,426},{13,11,231},{18,11,106},{148,11,87},{7 +,10,354},{10,10,410},{139,10,815},{6,0,367},{7,10,670},{7,10,1327},{8,10,411},{8 +,10,435},{9,10,653},{9,10,740},{10,10,385},{11,10,222},{11,10,324},{11,10,829},{ +140,10,611},{6,10,166},{7,0,1174},{135,10,374},{146,0,121},{132,0,828},{5,11,231 +},{138,11,509},{7,11,601},{9,11,277},{9,11,674},{10,11,178},{10,11,257},{10,11, +418},{11,11,531},{11,11,544},{11,11,585},{12,11,113},{12,11,475},{13,11,99},{142 +,11,428},{134,0,1541},{135,11,1779},{5,0,343},{134,10,398},{135,10,50},{135,11, +1683},{4,0,440},{7,0,57},{8,0,167},{8,0,375},{9,0,82},{9,0,561},{9,0,744},{9,11, +744},{138,0,620},{134,0,926},{6,10,517},{7,10,1159},{10,10,621},{139,10,192},{ +137,0,827},{8,0,194},{136,0,756},{10,10,223},{139,10,645},{7,10,64},{136,10,245} +,{4,11,399},{5,11,119},{5,11,494},{7,11,751},{137,11,556},{132,0,808},{135,0,22} +,{7,10,1763},{140,10,310},{5,0,639},{6,11,584},{7,0,1249},{139,0,896},{134,0, +1614},{135,0,860},{135,11,1121},{5,10,129},{6,10,61},{135,10,947},{4,0,102},{7,0 +,815},{7,0,1699},{139,0,964},{13,10,505},{141,10,506},{139,10,1000},{132,11,679} +,{132,0,899},{132,0,569},{5,11,694},{137,11,714},{136,0,795},{6,0,2045},{139,11, +7},{6,0,52},{9,0,104},{9,0,559},{12,0,308},{147,0,87},{4,0,301},{132,0,604},{133 +,10,637},{136,0,779},{5,11,143},{5,11,769},{6,11,1760},{7,11,682},{7,11,1992},{ +136,11,736},{137,10,590},{147,0,32},{137,11,527},{5,10,280},{135,10,1226},{134,0 +,494},{6,0,677},{6,0,682},{134,0,1044},{133,10,281},{135,10,1064},{5,11,860},{ +135,0,508},{6,11,422},{7,11,0},{7,11,1544},{9,11,577},{11,11,990},{12,11,141},{ +12,11,453},{13,11,47},{141,11,266},{134,0,1014},{5,11,515},{137,11,131},{134,0, +957},{132,11,646},{6,0,310},{7,0,1849},{8,0,72},{8,0,272},{8,0,431},{9,0,12},{9, +0,376},{10,0,563},{10,0,630},{10,0,796},{10,0,810},{11,0,367},{11,0,599},{11,0, +686},{140,0,672},{4,11,396},{7,0,570},{7,10,120},{7,11,728},{8,10,489},{9,10,319 +},{9,11,117},{10,10,820},{11,10,1004},{12,10,379},{12,10,679},{13,10,117},{13,10 +,412},{13,11,202},{14,10,25},{15,10,52},{15,10,161},{16,10,47},{20,11,51},{149, +10,2},{6,11,121},{6,11,124},{6,11,357},{7,11,1138},{7,11,1295},{8,11,162},{139, +11,655},{4,10,937},{5,10,801},{8,0,449},{136,11,449},{139,11,958},{6,0,181},{7,0 +,537},{8,0,64},{9,0,127},{10,0,496},{12,0,510},{141,0,384},{138,11,253},{4,0,244 +},{135,0,233},{133,11,237},{132,10,365},{6,0,1650},{10,0,702},{139,0,245},{5,10, +7},{139,10,774},{13,0,463},{13,11,463},{20,0,49},{148,11,49},{4,10,734},{5,10, +662},{134,10,430},{4,10,746},{135,10,1090},{5,10,360},{136,10,237},{137,0,338},{ +143,11,10},{7,11,571},{138,11,366},{134,0,1279},{9,11,513},{10,11,22},{10,11,39} +,{12,11,122},{140,11,187},{133,0,896},{146,0,178},{134,0,695},{137,0,808},{134, +11,587},{7,11,107},{7,11,838},{8,11,550},{138,11,401},{7,0,1117},{136,0,539},{4, +10,277},{5,10,608},{6,10,493},{7,10,457},{140,10,384},{133,11,768},{7,10,27},{7, +10,316},{140,0,257},{140,0,1003},{4,0,207},{5,0,586},{5,0,676},{5,10,552},{6,0, +448},{8,0,244},{11,0,1},{13,0,3},{16,0,54},{17,0,4},{146,0,13},{4,10,401},{137, +10,264},{5,0,516},{7,0,1883},{135,11,1883},{4,11,894},{140,0,960},{5,0,4},{5,0, +810},{6,0,13},{6,0,538},{6,0,1690},{6,0,1726},{7,0,499},{7,0,1819},{8,0,148},{8, +0,696},{8,0,791},{12,0,125},{143,0,9},{135,0,1268},{9,10,543},{10,10,524},{11,0, +30},{12,10,524},{14,0,315},{16,10,18},{20,10,26},{148,10,65},{4,10,205},{5,10, +623},{6,0,748},{7,10,104},{136,10,519},{11,0,542},{139,0,852},{140,0,6},{132,0, +848},{7,0,1385},{7,10,579},{9,10,41},{9,10,244},{9,10,669},{10,10,5},{11,0,582}, +{11,0,650},{11,0,901},{11,0,949},{11,10,861},{11,10,951},{11,10,980},{12,0,232}, +{12,0,236},{13,0,413},{13,0,501},{146,0,116},{4,0,945},{6,0,1811},{6,0,1845},{6, +0,1853},{6,0,1858},{8,0,862},{12,0,782},{12,0,788},{18,0,160},{148,0,117},{132, +10,717},{4,0,925},{5,0,803},{8,0,698},{138,0,828},{134,0,1416},{132,0,610},{139, +0,992},{6,0,878},{134,0,1477},{135,0,1847},{138,11,531},{137,11,539},{134,11,272 +},{133,0,383},{134,0,1404},{132,10,489},{4,11,9},{5,11,128},{7,11,368},{11,11, +480},{148,11,3},{136,0,986},{9,0,660},{138,0,347},{135,10,892},{136,11,682},{7,0 +,572},{9,0,592},{11,0,680},{12,0,356},{140,0,550},{7,0,1411},{138,11,527},{4,11, +2},{7,11,545},{135,11,894},{137,10,473},{7,10,819},{7,11,481},{9,10,26},{9,10, +392},{9,11,792},{10,10,152},{10,10,226},{11,0,64},{12,10,276},{12,10,426},{12,10 +,589},{13,10,460},{15,10,97},{19,10,48},{148,10,104},{135,10,51},{136,11,445},{ +136,11,646},{135,0,606},{132,10,674},{6,0,1829},{134,0,1830},{132,10,770},{5,10, +79},{7,10,1027},{7,10,1477},{139,10,52},{5,11,530},{142,11,113},{134,10,1666},{7 +,0,748},{139,0,700},{134,10,195},{133,10,789},{4,10,251},{4,10,688},{7,10,513},{ +7,10,1284},{9,0,87},{138,0,365},{136,11,111},{133,0,127},{6,0,198},{140,0,83},{ +133,11,556},{133,10,889},{4,10,160},{5,10,330},{7,10,1434},{136,10,174},{5,0,276 +},{6,0,55},{7,0,1369},{138,0,864},{8,11,16},{140,11,568},{6,0,1752},{136,0,726}, +{135,0,1066},{133,0,764},{6,11,186},{137,11,426},{11,0,683},{139,11,683},{6,0, +309},{7,0,331},{138,0,550},{133,10,374},{6,0,1212},{6,0,1852},{7,0,1062},{8,0, +874},{8,0,882},{138,0,936},{132,11,585},{134,0,1364},{5,10,731},{135,0,986},{6,0 +,723},{6,0,1408},{138,0,381},{135,0,1573},{134,0,1025},{4,10,626},{5,10,642},{6, +10,425},{10,10,202},{139,10,141},{4,11,93},{5,11,252},{6,11,229},{7,11,291},{9, +11,550},{139,11,644},{137,11,749},{137,11,162},{132,11,381},{135,0,1559},{6,0, +194},{7,0,133},{10,0,493},{10,0,570},{139,0,664},{5,0,24},{5,0,569},{6,0,3},{6,0 +,119},{6,0,143},{6,0,440},{7,0,295},{7,0,599},{7,0,1686},{7,0,1854},{8,0,424},{9 +,0,43},{9,0,584},{9,0,760},{10,0,148},{10,0,328},{11,0,159},{11,0,253},{11,0,506 +},{12,0,487},{140,0,531},{6,0,661},{134,0,1517},{136,10,835},{151,10,17},{5,0,14 +},{5,0,892},{6,0,283},{7,0,234},{136,0,537},{139,0,541},{4,0,126},{8,0,635},{147 +,0,34},{4,0,316},{4,0,495},{135,0,1561},{4,11,187},{5,11,184},{5,11,690},{7,11, +1869},{138,11,756},{139,11,783},{4,0,998},{137,0,861},{136,0,1009},{139,11,292}, +{5,11,21},{6,11,77},{6,11,157},{7,11,974},{7,11,1301},{7,11,1339},{7,11,1490},{7 +,11,1873},{137,11,628},{7,11,1283},{9,11,227},{9,11,499},{10,11,341},{11,11,325} +,{11,11,408},{14,11,180},{15,11,144},{18,11,47},{147,11,49},{4,0,64},{5,0,352},{ +5,0,720},{6,0,368},{139,0,359},{5,10,384},{8,10,455},{140,10,48},{5,10,264},{134 +,10,184},{7,0,1577},{10,0,304},{10,0,549},{12,0,365},{13,0,220},{13,0,240},{142, +0,33},{134,0,1107},{134,0,929},{135,0,1142},{6,0,175},{137,0,289},{5,0,432},{133 +,0,913},{5,10,633},{6,0,279},{7,0,219},{135,10,1323},{7,0,785},{7,10,359},{8,10, +243},{140,10,175},{139,0,595},{132,10,105},{8,11,398},{9,11,681},{139,11,632},{ +140,0,80},{5,0,931},{134,0,1698},{142,11,241},{134,11,20},{134,0,1323},{11,0,526 +},{11,0,939},{141,0,290},{5,0,774},{6,0,780},{6,0,1637},{6,0,1686},{6,0,1751},{8 +,0,559},{141,0,109},{141,0,127},{7,0,1167},{7,11,709},{11,0,934},{13,0,391},{145 +,0,76},{135,0,963},{6,0,260},{135,0,1484},{134,0,573},{4,10,758},{139,11,941},{ +135,10,1649},{145,11,36},{4,0,292},{137,0,580},{4,0,736},{5,0,871},{6,0,1689},{ +135,0,1944},{7,11,945},{11,11,713},{139,11,744},{134,0,1164},{135,11,937},{6,0, +1922},{7,11,1652},{9,0,982},{15,0,173},{15,0,178},{15,0,200},{18,0,189},{18,0, +207},{149,0,47},{7,0,1695},{139,10,128},{6,0,63},{135,0,920},{133,0,793},{143,11 +,134},{133,10,918},{5,0,67},{6,0,62},{6,0,374},{135,0,1391},{4,11,579},{5,11,226 +},{5,11,323},{7,11,960},{9,0,790},{140,0,47},{10,11,784},{141,11,191},{4,0,391}, +{135,0,1169},{137,0,443},{13,11,232},{146,11,35},{132,10,340},{132,0,271},{137, +11,313},{5,11,973},{137,11,659},{134,0,1140},{6,11,135},{135,11,1176},{4,0,253}, +{5,0,544},{7,0,300},{137,0,340},{5,10,985},{7,0,897},{7,10,509},{145,10,96},{138 +,11,735},{135,10,1919},{138,0,890},{5,0,818},{134,0,1122},{5,0,53},{5,0,541},{6, +0,94},{6,0,499},{7,0,230},{139,0,321},{4,0,920},{5,0,25},{5,0,790},{6,0,457},{7, +0,853},{8,0,788},{142,11,31},{132,10,247},{135,11,314},{132,0,468},{6,10,337},{7 +,0,243},{7,10,494},{8,10,27},{8,10,599},{138,10,153},{4,10,184},{5,10,390},{7,10 +,618},{7,10,1456},{139,10,710},{134,0,870},{134,0,1238},{134,0,1765},{10,0,853}, +{10,0,943},{14,0,437},{14,0,439},{14,0,443},{14,0,446},{14,0,452},{14,0,469},{14 +,0,471},{14,0,473},{16,0,93},{16,0,102},{16,0,110},{148,0,121},{4,0,605},{7,0, +518},{7,0,1282},{7,0,1918},{10,0,180},{139,0,218},{133,0,822},{4,0,634},{11,0, +916},{142,0,419},{6,11,281},{7,11,6},{8,11,282},{8,11,480},{8,11,499},{9,11,198} +,{10,11,143},{10,11,169},{10,11,211},{10,11,417},{10,11,574},{11,11,147},{11,11, +395},{12,11,75},{12,11,407},{12,11,608},{13,11,500},{142,11,251},{134,0,898},{6, +0,36},{7,0,658},{8,0,454},{150,11,48},{133,11,674},{135,11,1776},{4,11,419},{10, +10,227},{11,10,497},{11,10,709},{140,10,415},{6,10,360},{7,10,1664},{136,10,478} +,{137,0,806},{12,11,508},{14,11,102},{14,11,226},{144,11,57},{135,11,1123},{4,11 +,138},{7,11,1012},{7,11,1280},{137,11,76},{5,11,29},{140,11,638},{136,10,699},{ +134,0,1326},{132,0,104},{135,11,735},{132,10,739},{134,0,1331},{7,0,260},{135,11 +,260},{135,11,1063},{7,0,45},{9,0,542},{9,0,566},{9,10,869},{138,0,728},{4,10,67 +},{5,10,422},{7,10,1037},{7,10,1289},{7,10,1555},{9,10,741},{145,10,108},{139,0, +263},{134,0,1516},{14,0,146},{15,0,42},{16,0,23},{17,0,86},{146,0,17},{138,0,468 +},{136,0,1005},{4,11,17},{5,11,23},{7,11,995},{11,11,383},{11,11,437},{12,11,460 +},{140,11,532},{7,0,87},{142,0,288},{138,10,96},{135,11,626},{144,10,26},{7,0, +988},{7,0,1939},{9,0,64},{9,0,502},{12,0,22},{12,0,34},{13,0,12},{13,0,234},{147 +,0,77},{8,10,203},{11,10,823},{11,10,846},{12,10,482},{13,0,133},{13,10,277},{13 +,10,302},{13,10,464},{14,10,205},{142,10,221},{4,10,449},{133,10,718},{135,0,141 +},{6,0,1842},{136,0,872},{8,11,70},{12,11,171},{141,11,272},{4,10,355},{6,10,311 +},{9,10,256},{138,10,404},{132,0,619},{137,0,261},{10,10,758},{10,11,233},{139, +11,76},{5,0,246},{8,0,189},{9,0,355},{9,0,512},{10,0,124},{10,0,453},{11,0,143}, +{11,0,416},{11,0,859},{141,0,341},{134,11,442},{133,10,827},{5,10,64},{140,10, +581},{4,10,442},{7,10,1047},{7,10,1352},{135,10,1643},{134,11,1709},{5,0,678},{5 +,10,977},{6,0,305},{7,0,775},{135,0,1065},{11,11,69},{12,11,105},{12,11,117},{13 +,11,213},{14,11,13},{14,11,62},{14,11,177},{14,11,421},{15,11,19},{146,11,141},{ +137,11,309},{5,0,35},{7,0,862},{7,0,1886},{138,0,179},{136,0,285},{132,0,517},{7 +,11,976},{9,11,146},{10,11,206},{10,11,596},{13,11,218},{142,11,153},{132,10,254 +},{4,10,275},{6,0,214},{7,10,1219},{12,0,540},{140,10,376},{8,0,667},{11,0,403}, +{146,0,83},{10,11,648},{11,11,671},{12,0,74},{143,11,46},{135,0,125},{134,10, +1753},{133,0,761},{4,11,518},{6,0,912},{6,10,369},{6,10,502},{7,10,1036},{7,11, +1136},{8,10,348},{9,10,452},{10,10,26},{11,10,224},{11,10,387},{11,10,772},{12, +10,95},{12,10,629},{13,10,195},{13,10,207},{13,10,241},{14,10,260},{14,10,270},{ +143,10,140},{10,0,131},{140,0,72},{132,10,269},{5,10,480},{7,10,532},{7,10,1197} +,{7,10,1358},{8,10,291},{11,10,349},{142,10,396},{8,11,689},{137,11,863},{8,0, +333},{138,0,182},{4,11,18},{7,11,145},{7,11,444},{7,11,1278},{8,11,49},{8,11,400 +},{9,11,71},{9,11,250},{10,11,459},{12,11,160},{144,11,24},{14,11,35},{142,11, +191},{135,11,1864},{135,0,1338},{148,10,15},{14,0,94},{15,0,65},{16,0,4},{16,0, +77},{16,0,80},{145,0,5},{12,11,82},{143,11,36},{133,11,1010},{133,0,449},{133,0, +646},{7,0,86},{7,10,657},{136,0,103},{7,0,2028},{138,0,641},{136,10,533},{134,0, +1},{139,11,970},{5,11,87},{7,11,313},{7,11,1103},{10,11,112},{10,11,582},{11,11, +389},{11,11,813},{12,11,385},{13,11,286},{14,11,124},{146,11,108},{4,11,267},{ +134,0,869},{6,0,277},{7,0,1274},{7,0,1386},{146,0,87},{6,0,187},{7,0,39},{7,0, +1203},{8,0,380},{14,0,117},{149,0,28},{4,10,211},{4,10,332},{5,10,335},{6,10,238 +},{7,10,269},{7,10,811},{7,10,1797},{8,10,836},{9,10,507},{141,10,242},{4,0,785} +,{5,0,368},{6,0,297},{7,0,793},{139,0,938},{7,0,464},{8,0,558},{11,0,105},{12,0, +231},{14,0,386},{15,0,102},{148,0,75},{133,10,1009},{8,0,877},{140,0,731},{139, +11,289},{10,11,249},{139,11,209},{132,11,561},{134,0,1608},{132,11,760},{134,0, +1429},{9,11,154},{140,11,485},{5,10,228},{6,10,203},{7,10,156},{8,10,347},{137, +10,265},{7,0,1010},{11,0,733},{11,0,759},{13,0,34},{14,0,427},{146,0,45},{7,10, +1131},{135,10,1468},{136,11,255},{7,0,1656},{9,0,369},{10,0,338},{10,0,490},{11, +0,154},{11,0,545},{11,0,775},{13,0,77},{141,0,274},{133,11,621},{134,0,1038},{4, +11,368},{135,11,641},{6,0,2010},{8,0,979},{8,0,985},{10,0,951},{138,0,1011},{134 +,0,1005},{5,10,291},{5,10,318},{7,10,765},{9,10,389},{12,10,548},{147,0,121},{5, +0,20},{6,0,298},{7,0,659},{137,0,219},{7,0,1440},{11,0,854},{11,0,872},{11,0,921 +},{12,0,551},{13,0,472},{142,0,367},{5,0,490},{6,0,615},{6,0,620},{135,0,683},{6 +,0,1070},{134,0,1597},{139,0,522},{132,0,439},{136,0,669},{6,0,766},{6,0,1143},{ +6,0,1245},{10,10,525},{139,10,82},{9,11,92},{147,11,91},{6,0,668},{134,0,1218},{ +6,11,525},{9,11,876},{140,11,284},{132,0,233},{136,0,547},{132,10,422},{5,10,355 +},{145,10,0},{6,11,300},{135,11,1515},{4,0,482},{137,10,905},{4,0,886},{5,11,594 +},{135,0,346},{133,10,865},{5,10,914},{134,10,1625},{135,0,334},{5,0,795},{5,10, +234},{134,0,1741},{135,10,1383},{6,11,1641},{136,11,820},{135,0,371},{7,11,1313} +,{138,11,660},{135,10,1312},{135,0,622},{7,0,625},{135,0,1750},{135,0,339},{4,0, +203},{135,0,1936},{15,0,29},{15,11,29},{16,0,38},{144,11,38},{5,0,338},{135,0, +1256},{135,10,1493},{6,10,421},{7,10,61},{7,10,1540},{10,0,130},{138,10,501},{6, +11,389},{7,11,149},{9,11,142},{138,11,94},{137,10,341},{11,0,678},{12,0,307},{ +142,10,98},{6,11,8},{7,11,1881},{136,11,91},{135,0,2044},{6,0,770},{6,0,802},{6, +0,812},{6,10,102},{7,0,311},{7,10,72},{9,0,308},{12,0,255},{15,10,142},{147,10, +67},{151,10,30},{135,10,823},{135,0,1266},{135,11,1746},{135,10,1870},{4,0,400}, +{5,0,267},{135,0,232},{7,11,24},{11,11,542},{139,11,852},{135,11,1739},{4,11,503 +},{135,11,1661},{5,11,130},{7,11,1314},{9,11,610},{10,11,718},{11,11,601},{11,11 +,819},{11,11,946},{140,11,536},{10,11,149},{11,11,280},{142,11,336},{7,0,739},{7 +,11,1946},{8,10,48},{8,10,88},{8,10,582},{8,10,681},{9,10,373},{9,10,864},{11,0, +690},{11,10,157},{11,10,843},{148,10,27},{134,0,990},{4,10,88},{5,10,137},{5,10, +174},{5,10,777},{6,10,1664},{6,10,1725},{7,10,77},{7,10,426},{7,10,1317},{7,10, +1355},{8,10,126},{8,10,563},{9,10,523},{9,10,750},{10,10,310},{10,10,836},{11,10 +,42},{11,10,318},{11,10,731},{12,10,68},{12,10,92},{12,10,507},{12,10,692},{13, +10,81},{13,10,238},{13,10,374},{14,10,436},{18,10,138},{19,10,78},{19,10,111},{ +20,10,55},{20,10,77},{148,10,92},{141,10,418},{4,10,938},{135,0,1831},{6,0,776}, +{134,0,915},{138,10,351},{5,11,348},{6,10,1668},{6,11,522},{7,10,1499},{8,10,117 +},{9,10,314},{138,10,174},{135,10,707},{132,0,613},{133,10,403},{132,11,392},{5, +11,433},{9,11,633},{139,11,629},{133,0,763},{132,0,878},{132,0,977},{132,0,100}, +{4,10,44},{5,10,311},{6,0,463},{7,10,639},{7,10,762},{7,10,1827},{9,10,8},{9,10, +462},{148,10,83},{134,11,234},{4,10,346},{7,10,115},{9,10,180},{9,10,456},{138, +10,363},{5,0,362},{5,0,443},{6,0,318},{7,0,1019},{139,0,623},{5,0,463},{7,11,140 +},{7,11,1950},{8,0,296},{8,11,680},{11,11,817},{147,11,88},{7,11,1222},{138,11, +386},{142,0,137},{132,0,454},{6,11,5},{7,0,1914},{7,10,1051},{9,10,545},{11,11, +249},{12,11,313},{16,11,66},{145,11,26},{135,0,1527},{145,0,58},{148,11,59},{5,0 +,48},{5,0,404},{6,0,557},{7,0,458},{8,0,597},{10,0,455},{10,0,606},{11,0,49},{11 +,0,548},{12,0,476},{13,0,18},{141,0,450},{5,11,963},{134,11,1773},{133,0,729},{ +138,11,586},{5,0,442},{135,0,1984},{134,0,449},{144,0,40},{4,0,853},{7,11,180},{ +8,11,509},{136,11,792},{6,10,185},{7,10,1899},{9,10,875},{139,10,673},{134,11, +524},{4,10,327},{5,10,478},{7,10,1332},{8,10,753},{140,0,227},{5,10,1020},{5,10, +1022},{134,0,1491},{4,10,103},{133,10,401},{132,11,931},{4,10,499},{135,10,1421} +,{5,0,55},{7,0,376},{140,0,161},{133,0,450},{6,0,1174},{134,0,1562},{7,11,1837}, +{10,0,62},{141,0,400},{140,0,207},{135,0,869},{4,11,773},{5,11,618},{137,11,756} +,{132,10,96},{4,0,213},{7,0,223},{7,10,968},{136,0,80},{4,11,90},{5,11,337},{5, +11,545},{7,11,754},{9,11,186},{10,11,72},{10,11,782},{11,11,513},{11,11,577},{11 +,11,610},{11,11,889},{11,11,961},{12,11,354},{12,11,362},{12,11,461},{12,11,595} +,{13,11,79},{143,11,121},{7,0,381},{7,0,806},{7,0,820},{8,0,354},{8,0,437},{8,0, +787},{9,0,657},{10,0,58},{10,0,339},{10,0,749},{11,0,914},{12,0,162},{13,0,75},{ +14,0,106},{14,0,198},{14,0,320},{14,0,413},{146,0,43},{136,0,747},{136,0,954},{ +134,0,1073},{135,0,556},{7,11,151},{9,11,329},{139,11,254},{5,0,692},{134,0,1395 +},{6,10,563},{137,10,224},{134,0,191},{132,0,804},{9,11,187},{10,11,36},{17,11, +44},{146,11,64},{7,11,165},{7,11,919},{136,11,517},{4,11,506},{5,11,295},{7,11, +1680},{15,11,14},{144,11,5},{4,0,706},{6,0,162},{7,0,1960},{136,0,831},{135,11, +1376},{7,11,987},{9,11,688},{10,11,522},{11,11,788},{140,11,566},{150,0,35},{138 +,0,426},{135,0,1235},{135,11,1741},{7,11,389},{7,11,700},{7,11,940},{8,11,514},{ +9,11,116},{9,11,535},{10,11,118},{11,11,107},{11,11,148},{11,11,922},{12,11,254} +,{12,11,421},{142,11,238},{134,0,1234},{132,11,743},{4,10,910},{5,10,832},{135, +11,1335},{141,0,96},{135,11,185},{146,0,149},{4,0,204},{137,0,902},{4,11,784},{ +133,11,745},{136,0,833},{136,0,949},{5,11,81},{7,0,366},{7,11,146},{7,11,1342},{ +7,11,1446},{8,11,53},{8,11,561},{8,11,694},{8,11,754},{9,0,287},{9,11,97},{9,11, +115},{9,11,894},{10,11,462},{10,11,813},{11,11,230},{11,11,657},{11,11,699},{11, +11,748},{12,0,199},{12,0,556},{12,0,577},{12,11,119},{12,11,200},{12,11,283},{14 +,11,273},{145,11,15},{5,11,408},{137,11,747},{9,11,498},{140,11,181},{6,0,2020}, +{136,0,992},{5,0,356},{135,0,224},{134,0,784},{7,0,630},{8,10,528},{9,0,567},{9, +10,348},{11,0,150},{11,0,444},{141,0,119},{134,0,539},{4,10,20},{133,10,616},{ +142,0,27},{7,11,30},{8,11,86},{8,11,315},{8,11,700},{9,11,576},{9,11,858},{11,11 +,310},{11,11,888},{11,11,904},{12,11,361},{141,11,248},{138,11,839},{134,0,755}, +{134,0,1063},{7,10,1091},{135,10,1765},{134,11,428},{7,11,524},{8,11,169},{8,11, +234},{9,11,480},{138,11,646},{139,0,814},{7,11,1462},{139,11,659},{4,10,26},{5, +10,429},{6,10,245},{7,10,704},{7,10,1379},{135,10,1474},{7,11,1205},{138,11,637} +,{139,11,803},{132,10,621},{136,0,987},{4,11,266},{8,11,4},{9,11,39},{10,11,166} +,{11,11,918},{12,11,635},{20,11,10},{22,11,27},{150,11,43},{4,0,235},{135,0,255} +,{4,0,194},{5,0,584},{6,0,384},{7,0,583},{10,0,761},{11,0,760},{139,0,851},{133, +10,542},{134,0,1086},{133,10,868},{8,0,1016},{136,0,1018},{7,0,1396},{7,11,1396} +,{136,10,433},{135,10,1495},{138,10,215},{141,10,124},{7,11,157},{8,11,279},{9, +11,759},{16,11,31},{16,11,39},{16,11,75},{18,11,24},{20,11,42},{152,11,1},{5,0, +562},{134,11,604},{134,0,913},{5,0,191},{137,0,271},{4,0,470},{6,0,153},{7,0, +1503},{7,0,1923},{10,0,701},{11,0,132},{11,0,227},{11,0,320},{11,0,436},{11,0, +525},{11,0,855},{11,0,873},{12,0,41},{12,0,286},{13,0,103},{13,0,284},{14,0,255} +,{14,0,262},{15,0,117},{143,0,127},{7,0,475},{12,0,45},{147,10,112},{132,11,567} +,{137,11,859},{6,0,713},{6,0,969},{6,0,1290},{134,0,1551},{133,0,327},{6,0,552}, +{6,0,1292},{7,0,1754},{137,0,604},{4,0,223},{5,11,762},{6,0,359},{7,11,1880},{9, +11,680},{11,0,3},{11,11,798},{13,0,108},{14,0,89},{144,0,22},{5,0,80},{6,0,405}, +{7,0,403},{7,0,1502},{8,0,456},{9,0,487},{9,0,853},{9,0,889},{10,0,309},{11,0, +721},{11,0,994},{12,0,430},{141,0,165},{133,11,298},{132,10,647},{134,0,2016},{ +18,10,10},{146,11,10},{4,0,453},{5,0,887},{6,0,535},{8,0,6},{8,0,543},{136,0,826 +},{136,0,975},{10,0,961},{138,0,962},{138,10,220},{6,0,1891},{6,0,1893},{9,0,916 +},{9,0,965},{9,0,972},{12,0,801},{12,0,859},{12,0,883},{15,0,226},{149,0,51},{ +132,10,109},{135,11,267},{7,11,92},{7,11,182},{8,11,453},{9,11,204},{11,11,950}, +{12,11,94},{12,11,644},{16,11,20},{16,11,70},{16,11,90},{147,11,55},{134,10,1746 +},{6,11,71},{7,11,845},{7,11,1308},{8,11,160},{137,11,318},{5,0,101},{6,0,88},{7 +,0,263},{7,0,628},{7,0,1677},{7,11,237},{8,0,349},{8,11,664},{9,0,100},{9,11,42} +,{9,11,266},{9,11,380},{9,11,645},{10,0,677},{10,11,177},{10,11,276},{14,0,169}, +{14,0,302},{14,0,313},{15,0,48},{143,0,84},{138,11,69},{4,0,310},{7,0,708},{7,0, +996},{9,0,795},{10,0,390},{10,0,733},{11,0,451},{12,0,249},{14,0,115},{14,0,286} +,{143,0,100},{4,10,40},{5,0,587},{10,10,67},{11,10,117},{11,10,768},{139,10,935} +,{6,0,1942},{7,0,512},{136,0,983},{7,10,992},{8,10,301},{9,10,722},{12,10,63},{ +13,10,29},{14,10,161},{143,10,18},{136,11,76},{139,10,923},{134,0,645},{134,0, +851},{4,0,498},{132,11,293},{7,0,217},{8,0,140},{10,0,610},{14,11,352},{17,11,53 +},{18,11,146},{18,11,152},{19,11,11},{150,11,54},{134,0,1448},{138,11,841},{133, +0,905},{4,11,605},{7,11,518},{7,11,1282},{7,11,1918},{10,11,180},{139,11,218},{ +139,11,917},{135,10,825},{140,10,328},{4,0,456},{7,0,105},{7,0,358},{7,0,1637},{ +8,0,643},{139,0,483},{134,0,792},{6,11,96},{135,11,1426},{137,11,691},{4,11,651} +,{133,11,289},{7,11,688},{8,11,35},{9,11,511},{10,11,767},{147,11,118},{150,0,56 +},{5,0,243},{5,0,535},{6,10,204},{10,10,320},{10,10,583},{13,10,502},{14,10,72}, +{14,10,274},{14,10,312},{14,10,344},{15,10,159},{16,10,62},{16,10,69},{17,10,30} +,{18,10,42},{18,10,53},{18,10,84},{18,10,140},{19,10,68},{19,10,85},{20,10,5},{ +20,10,45},{20,10,101},{22,10,7},{150,10,20},{4,10,558},{6,10,390},{7,10,162},{7, +10,689},{9,10,360},{138,10,653},{146,11,23},{135,0,1748},{5,10,856},{6,10,1672}, +{6,10,1757},{134,10,1781},{4,11,704},{5,0,539},{5,0,754},{134,0,876},{135,11, +1078},{5,10,92},{10,10,736},{140,10,102},{5,10,590},{9,10,213},{145,0,91},{134,0 +,1565},{6,0,91},{135,0,435},{4,0,939},{140,0,792},{134,0,1399},{4,0,16},{4,11, +720},{5,0,316},{5,0,842},{5,11,306},{6,0,370},{6,0,1778},{8,0,166},{11,0,812},{ +12,0,206},{12,0,351},{14,0,418},{16,0,15},{16,0,34},{18,0,3},{19,0,3},{19,0,7},{ +20,0,4},{149,0,21},{144,0,95},{133,11,431},{132,11,234},{135,0,551},{4,0,999},{6 +,0,1966},{134,0,2042},{7,0,619},{10,0,547},{11,0,122},{12,0,601},{15,0,7},{148,0 +,20},{5,11,464},{6,11,236},{7,11,276},{7,11,696},{7,11,914},{7,11,1108},{7,11, +1448},{9,11,15},{9,11,564},{10,11,14},{12,11,565},{13,11,449},{14,11,53},{15,11, +13},{16,11,64},{145,11,41},{6,0,884},{6,0,1019},{134,0,1150},{6,11,1767},{12,11, +194},{145,11,107},{136,10,503},{133,11,840},{6,10,466},{135,0,671},{132,0,888},{ +4,0,149},{138,0,368},{4,0,154},{7,0,1134},{136,0,105},{135,0,983},{9,11,642},{11 +,11,236},{142,11,193},{4,0,31},{6,0,429},{7,0,962},{9,0,458},{139,0,691},{6,0, +643},{134,0,1102},{132,0,312},{4,11,68},{5,11,634},{6,11,386},{7,11,794},{8,11, +273},{9,11,563},{10,11,105},{10,11,171},{11,11,94},{139,11,354},{133,0,740},{135 +,0,1642},{4,11,95},{7,11,416},{8,11,211},{139,11,830},{132,0,236},{138,10,241},{ +7,11,731},{13,11,20},{143,11,11},{5,0,836},{5,0,857},{6,0,1680},{135,0,59},{10,0 +,68},{11,0,494},{152,11,6},{4,0,81},{139,0,867},{135,0,795},{133,11,689},{4,0, +1001},{5,0,282},{6,0,1932},{6,0,1977},{6,0,1987},{6,0,1992},{8,0,650},{8,0,919}, +{8,0,920},{8,0,923},{8,0,926},{8,0,927},{8,0,931},{8,0,939},{8,0,947},{8,0,956}, +{8,0,997},{9,0,907},{10,0,950},{10,0,953},{10,0,954},{10,0,956},{10,0,958},{10,0 +,959},{10,0,964},{10,0,970},{10,0,972},{10,0,973},{10,0,975},{10,0,976},{10,0, +980},{10,0,981},{10,0,984},{10,0,988},{10,0,990},{10,0,995},{10,0,999},{10,0, +1002},{10,0,1003},{10,0,1005},{10,0,1006},{10,0,1008},{10,0,1009},{10,0,1012},{ +10,0,1014},{10,0,1015},{10,0,1019},{10,0,1020},{10,0,1022},{12,0,959},{12,0,961} +,{12,0,962},{12,0,963},{12,0,964},{12,0,965},{12,0,967},{12,0,968},{12,0,969},{ +12,0,970},{12,0,971},{12,0,972},{12,0,973},{12,0,974},{12,0,975},{12,0,976},{12, +0,977},{12,0,979},{12,0,981},{12,0,982},{12,0,983},{12,0,984},{12,0,985},{12,0, +986},{12,0,987},{12,0,989},{12,0,990},{12,0,992},{12,0,993},{12,0,995},{12,0,998 +},{12,0,999},{12,0,1000},{12,0,1001},{12,0,1002},{12,0,1004},{12,0,1005},{12,0, +1006},{12,0,1007},{12,0,1008},{12,0,1009},{12,0,1010},{12,0,1011},{12,0,1012},{ +12,0,1014},{12,0,1015},{12,0,1016},{12,0,1017},{12,0,1018},{12,0,1019},{12,0, +1022},{12,0,1023},{14,0,475},{14,0,477},{14,0,478},{14,0,479},{14,0,480},{14,0, +482},{14,0,483},{14,0,484},{14,0,485},{14,0,486},{14,0,487},{14,0,488},{14,0,489 +},{14,0,490},{14,0,491},{14,0,492},{14,0,493},{14,0,494},{14,0,495},{14,0,496},{ +14,0,497},{14,0,498},{14,0,499},{14,0,500},{14,0,501},{14,0,502},{14,0,503},{14, +0,504},{14,0,506},{14,0,507},{14,0,508},{14,0,509},{14,0,510},{14,0,511},{16,0, +113},{16,0,114},{16,0,115},{16,0,117},{16,0,118},{16,0,119},{16,0,121},{16,0,122 +},{16,0,123},{16,0,124},{16,0,125},{16,0,126},{16,0,127},{18,0,242},{18,0,243},{ +18,0,244},{18,0,245},{18,0,248},{18,0,249},{18,0,250},{18,0,251},{18,0,252},{18, +0,253},{18,0,254},{18,0,255},{20,0,125},{20,0,126},{148,0,127},{7,11,1717},{7,11 +,1769},{138,11,546},{7,11,1127},{7,11,1572},{10,11,297},{10,11,422},{11,11,764}, +{11,11,810},{12,11,264},{13,11,102},{13,11,300},{13,11,484},{14,11,147},{14,11, +229},{17,11,71},{18,11,118},{147,11,120},{6,0,1148},{134,0,1586},{132,0,775},{ +135,10,954},{133,11,864},{133,11,928},{138,11,189},{135,10,1958},{6,10,549},{8, +10,34},{8,10,283},{9,10,165},{138,10,475},{5,10,652},{5,10,701},{135,10,449},{ +135,11,695},{4,10,655},{7,10,850},{17,10,75},{146,10,137},{140,11,682},{133,11, +523},{8,0,970},{136,10,670},{136,11,555},{7,11,76},{8,11,44},{9,11,884},{10,11, +580},{11,11,399},{11,11,894},{15,11,122},{18,11,144},{147,11,61},{6,10,159},{6, +10,364},{7,10,516},{7,10,1439},{137,10,518},{4,0,71},{5,0,376},{7,0,119},{138,0, +665},{141,10,151},{11,0,827},{14,0,34},{143,0,148},{133,11,518},{4,0,479},{135, +11,1787},{135,11,1852},{135,10,993},{7,0,607},{136,0,99},{134,0,1960},{132,0,793 +},{4,0,41},{5,0,74},{7,0,1627},{11,0,871},{140,0,619},{7,0,94},{11,0,329},{11,0, +965},{12,0,241},{14,0,354},{15,0,22},{148,0,63},{7,10,501},{9,10,111},{10,10,141 +},{11,10,332},{13,10,43},{13,10,429},{14,10,130},{14,10,415},{145,10,102},{9,0, +209},{137,0,300},{134,0,1497},{138,11,255},{4,11,934},{5,11,138},{136,11,610},{ +133,0,98},{6,0,1316},{10,11,804},{138,11,832},{8,11,96},{9,11,36},{10,11,607},{ +11,11,423},{11,11,442},{12,11,309},{14,11,199},{15,11,90},{145,11,110},{132,0, +463},{5,10,149},{136,10,233},{133,10,935},{4,11,652},{8,11,320},{9,11,13},{9,11, +398},{9,11,727},{10,11,75},{10,11,184},{10,11,230},{10,11,564},{10,11,569},{11, +11,973},{12,11,70},{12,11,189},{13,11,57},{13,11,257},{22,11,6},{150,11,16},{142 +,0,291},{12,10,582},{146,10,131},{136,10,801},{133,0,984},{145,11,116},{4,11,692 +},{133,11,321},{4,0,182},{6,0,205},{135,0,220},{4,0,42},{9,0,205},{9,0,786},{138 +,0,659},{6,0,801},{11,11,130},{140,11,609},{132,0,635},{5,11,345},{135,11,1016}, +{139,0,533},{132,0,371},{4,0,272},{135,0,836},{6,0,1282},{135,11,1100},{5,0,825} +,{134,0,1640},{135,11,1325},{133,11,673},{4,11,287},{133,11,1018},{135,0,357},{6 +,0,467},{137,0,879},{7,0,317},{135,0,569},{6,0,924},{134,0,1588},{5,10,406},{5, +11,34},{10,11,724},{12,11,444},{13,11,354},{18,11,32},{23,11,24},{23,11,31},{152 +,11,5},{6,0,1795},{6,0,1835},{6,0,1836},{6,0,1856},{8,0,844},{8,0,849},{8,0,854} +,{8,0,870},{8,0,887},{10,0,852},{138,0,942},{6,10,69},{135,10,117},{137,0,307},{ +4,0,944},{6,0,1799},{6,0,1825},{10,0,848},{10,0,875},{10,0,895},{10,0,899},{10,0 +,902},{140,0,773},{11,0,43},{13,0,72},{141,0,142},{135,10,1830},{134,11,382},{4, +10,432},{135,10,824},{132,11,329},{7,0,1820},{139,11,124},{133,10,826},{133,0, +525},{132,11,906},{7,11,1940},{136,11,366},{138,11,10},{4,11,123},{4,11,649},{5, +11,605},{7,11,1509},{136,11,36},{6,0,110},{135,0,1681},{133,0,493},{133,11,767}, +{4,0,174},{135,0,911},{138,11,786},{8,0,417},{137,0,782},{133,10,1000},{7,0,733} +,{137,0,583},{4,10,297},{6,10,529},{7,10,152},{7,10,713},{7,10,1845},{8,10,710}, +{8,10,717},{12,10,639},{140,10,685},{4,0,32},{5,0,215},{6,0,269},{7,0,1782},{7,0 +,1892},{10,0,16},{11,0,822},{11,0,954},{141,0,481},{4,11,273},{5,11,658},{133,11 +,995},{136,0,477},{134,11,72},{135,11,1345},{4,10,520},{5,0,308},{7,0,1088},{135 +,10,575},{133,11,589},{5,0,126},{8,0,297},{9,0,366},{140,0,374},{7,0,1551},{139, +0,361},{5,11,117},{6,11,514},{6,11,541},{7,11,1164},{7,11,1436},{8,11,220},{8,11 +,648},{10,11,688},{139,11,560},{133,11,686},{4,0,946},{6,0,1807},{8,0,871},{10,0 +,854},{10,0,870},{10,0,888},{10,0,897},{10,0,920},{12,0,722},{12,0,761},{12,0, +763},{12,0,764},{14,0,454},{14,0,465},{16,0,107},{18,0,167},{18,0,168},{146,0, +172},{132,0,175},{135,0,1307},{132,0,685},{135,11,1834},{133,0,797},{6,0,745},{6 +,0,858},{134,0,963},{133,0,565},{5,10,397},{6,10,154},{7,10,676},{7,11,196},{8, +10,443},{8,10,609},{9,10,24},{9,10,325},{10,10,35},{10,11,765},{11,10,535},{11, +10,672},{11,10,1018},{11,11,347},{11,11,552},{11,11,576},{11,11,790},{12,10,637} +,{12,11,263},{13,11,246},{13,11,270},{13,11,395},{14,11,74},{14,11,176},{14,11, +190},{14,11,398},{14,11,412},{15,11,32},{15,11,63},{16,10,30},{16,11,88},{147,11 +,105},{13,11,84},{141,11,122},{4,0,252},{7,0,1068},{10,0,434},{11,0,228},{11,0, +426},{13,0,231},{18,0,106},{148,0,87},{137,0,826},{4,11,589},{139,11,282},{5,11, +381},{135,11,1792},{132,0,791},{5,0,231},{5,10,981},{138,0,509},{7,0,601},{9,0, +277},{9,0,674},{10,0,178},{10,0,418},{10,0,571},{11,0,531},{12,0,113},{12,0,475} +,{13,0,99},{142,0,428},{4,10,56},{7,10,1791},{7,11,616},{8,10,607},{8,10,651},{ +10,11,413},{11,10,465},{11,10,835},{12,10,337},{141,10,480},{7,0,1591},{144,0,43 +},{9,10,158},{138,10,411},{135,0,1683},{8,0,289},{11,0,45},{12,0,278},{140,0,537 +},{6,11,120},{7,11,1188},{7,11,1710},{8,11,286},{9,11,667},{11,11,592},{139,11, +730},{136,10,617},{135,0,1120},{135,11,1146},{139,10,563},{4,10,369},{4,11,352}, +{135,11,687},{143,11,38},{4,0,399},{5,0,119},{5,0,494},{7,0,751},{9,0,556},{14, +11,179},{15,11,151},{150,11,11},{4,11,192},{5,11,49},{6,11,200},{6,11,293},{6,11 +,1696},{135,11,488},{4,0,398},{133,0,660},{6,10,622},{135,0,1030},{135,11,595},{ +141,0,168},{132,11,147},{7,0,973},{10,10,624},{142,10,279},{132,10,363},{132,0, +642},{133,11,934},{134,0,1615},{7,11,505},{135,11,523},{7,0,594},{7,0,851},{7,0, +1858},{9,0,411},{9,0,574},{9,0,666},{9,0,737},{10,0,346},{10,0,712},{11,0,246},{ +11,0,432},{11,0,517},{11,0,647},{11,0,679},{11,0,727},{12,0,304},{12,0,305},{12, +0,323},{12,0,483},{12,0,572},{12,0,593},{12,0,602},{13,0,95},{13,0,101},{13,0, +171},{13,0,315},{13,0,378},{13,0,425},{13,0,475},{14,0,63},{14,0,380},{14,0,384} +,{15,0,133},{18,0,112},{148,0,72},{135,0,1093},{132,0,679},{8,0,913},{10,0,903}, +{10,0,915},{10,11,438},{12,0,648},{12,0,649},{14,0,455},{144,0,112},{137,0,203}, +{134,10,292},{134,0,1492},{5,10,177},{6,10,616},{7,0,1374},{7,10,827},{8,0,540}, +{9,10,525},{138,10,656},{135,0,1486},{9,0,714},{138,10,31},{136,0,825},{134,0, +1511},{132,11,637},{134,0,952},{4,10,161},{133,10,631},{5,0,143},{5,0,769},{6,0, +1760},{7,0,682},{7,0,1992},{136,0,736},{132,0,700},{134,0,1540},{132,11,777},{9, +11,867},{138,11,837},{7,0,1557},{135,10,1684},{133,0,860},{6,0,422},{7,0,0},{7,0 +,1544},{9,0,605},{9,10,469},{9,10,709},{11,0,990},{12,0,235},{12,0,453},{12,10, +512},{13,0,47},{13,0,266},{14,10,65},{145,10,12},{10,10,229},{11,0,807},{11,10, +73},{139,10,376},{6,11,170},{7,11,1080},{8,11,395},{8,11,487},{11,11,125},{141, +11,147},{5,0,515},{137,0,131},{7,0,1605},{11,0,962},{146,0,139},{132,0,646},{4,0 +,396},{7,0,728},{9,0,117},{13,0,202},{148,0,51},{4,11,535},{6,0,121},{6,0,124},{ +6,0,357},{6,10,558},{7,0,1138},{7,0,1295},{7,10,651},{8,0,162},{8,0,508},{8,11, +618},{9,10,0},{10,10,34},{11,0,655},{139,10,1008},{135,11,1245},{138,0,357},{150 +,11,23},{133,0,237},{135,0,1784},{7,10,1832},{138,10,374},{132,0,713},{132,11,46 +},{5,11,811},{6,0,1536},{6,11,1679},{6,11,1714},{7,11,2032},{138,0,348},{11,11, +182},{142,11,195},{6,0,523},{7,0,738},{7,10,771},{7,10,1731},{9,10,405},{138,10, +421},{7,11,1458},{9,11,407},{139,11,15},{6,11,34},{7,11,69},{7,11,640},{7,11, +1089},{8,11,708},{8,11,721},{9,11,363},{9,11,643},{10,11,628},{148,11,98},{133,0 +,434},{135,0,1877},{7,0,571},{138,0,366},{5,10,881},{133,10,885},{9,0,513},{10,0 +,25},{10,0,39},{12,0,122},{140,0,187},{132,0,580},{5,10,142},{134,10,546},{132, +11,462},{137,0,873},{5,10,466},{11,10,571},{12,10,198},{13,10,283},{14,10,186},{ +15,10,21},{143,10,103},{4,10,185},{5,10,257},{5,10,839},{5,10,936},{7,0,171},{9, +10,399},{10,10,258},{10,10,395},{10,10,734},{11,10,1014},{12,10,23},{13,10,350}, +{14,10,150},{147,10,6},{134,0,625},{7,0,107},{7,0,838},{8,0,550},{138,0,401},{5, +11,73},{6,11,23},{134,11,338},{4,0,943},{6,0,1850},{12,0,713},{142,0,434},{7,10, +404},{7,10,1377},{7,10,1430},{7,10,2017},{8,10,149},{8,10,239},{8,10,512},{8,10, +793},{8,10,818},{9,10,474},{9,10,595},{10,10,122},{10,10,565},{10,10,649},{10,10 +,783},{11,0,588},{11,0,864},{11,0,936},{11,0,968},{11,10,239},{11,10,295},{11,10 +,447},{11,10,528},{11,10,639},{11,10,800},{12,0,73},{12,0,343},{12,0,394},{12,10 +,25},{12,10,157},{12,10,316},{12,10,390},{12,10,391},{12,10,395},{12,10,478},{12 +,10,503},{12,10,592},{12,10,680},{13,0,275},{13,10,50},{13,10,53},{13,10,132},{ +13,10,198},{13,10,322},{13,10,415},{13,10,511},{14,0,257},{14,10,71},{14,10,395} +,{15,0,160},{15,10,71},{15,10,136},{17,10,123},{18,10,93},{147,10,58},{133,0,768 +},{11,0,103},{142,0,0},{136,10,712},{132,0,799},{132,0,894},{7,11,725},{8,11,498 +},{139,11,268},{135,11,1798},{135,11,773},{141,11,360},{4,10,377},{152,10,13},{ +135,0,1673},{132,11,583},{134,0,1052},{133,11,220},{140,11,69},{132,11,544},{4, +10,180},{135,10,1906},{134,0,272},{4,0,441},{134,0,1421},{4,0,9},{5,0,128},{7,0, +368},{11,0,480},{148,0,3},{5,11,176},{6,11,437},{6,11,564},{11,11,181},{141,11, +183},{132,10,491},{7,0,1182},{141,11,67},{4,10,171},{6,0,1346},{138,10,234},{4, +10,586},{7,10,1186},{138,10,631},{136,0,682},{134,0,1004},{15,0,24},{143,11,24}, +{134,0,968},{4,0,2},{6,0,742},{6,0,793},{7,0,545},{7,0,894},{9,10,931},{10,10, +334},{148,10,71},{136,11,600},{133,10,765},{9,0,769},{140,0,185},{4,11,790},{5, +11,273},{134,11,394},{7,0,474},{137,0,578},{4,11,135},{6,11,127},{7,11,1185},{7, +11,1511},{8,11,613},{11,11,5},{12,11,133},{12,11,495},{12,11,586},{14,11,385},{ +15,11,118},{17,11,20},{146,11,98},{133,10,424},{5,0,530},{142,0,113},{6,11,230}, +{7,11,961},{7,11,1085},{136,11,462},{7,11,1954},{137,11,636},{136,10,714},{149, +11,6},{135,10,685},{9,10,420},{10,10,269},{10,10,285},{10,10,576},{11,10,397},{ +13,10,175},{145,10,90},{132,10,429},{5,0,556},{5,11,162},{136,11,68},{132,11,654 +},{4,11,156},{7,11,998},{7,11,1045},{7,11,1860},{9,11,48},{9,11,692},{11,11,419} +,{139,11,602},{6,0,1317},{7,11,1276},{8,0,16},{8,11,474},{9,0,825},{9,11,652},{ +140,0,568},{7,10,18},{7,10,699},{7,10,1966},{8,10,752},{9,10,273},{9,10,412},{9, +10,703},{10,10,71},{10,10,427},{10,10,508},{146,0,97},{7,11,1454},{10,0,703},{ +138,11,703},{4,10,53},{5,10,186},{135,10,752},{134,0,892},{134,0,1571},{8,10,575 +},{10,10,289},{139,10,319},{6,0,186},{137,0,426},{134,0,1101},{132,10,675},{132, +0,585},{6,0,1870},{137,0,937},{152,11,10},{9,11,197},{10,11,300},{12,11,473},{13 +,11,90},{141,11,405},{4,0,93},{5,0,252},{6,0,229},{7,0,291},{9,0,550},{139,0,644 +},{137,0,749},{6,10,209},{8,10,468},{9,0,162},{9,10,210},{11,10,36},{12,10,28},{ +12,10,630},{13,10,21},{13,10,349},{14,10,7},{145,10,13},{132,0,381},{132,11,606} +,{4,10,342},{135,10,1179},{7,11,1587},{7,11,1707},{10,11,528},{139,11,504},{12, +11,39},{13,11,265},{141,11,439},{4,10,928},{133,10,910},{7,10,1838},{7,11,1978}, +{136,11,676},{6,0,762},{6,0,796},{134,0,956},{4,10,318},{4,10,496},{7,10,856},{ +139,10,654},{137,11,242},{4,11,361},{133,11,315},{132,11,461},{132,11,472},{132, +0,857},{5,0,21},{6,0,77},{6,0,157},{7,0,974},{7,0,1301},{7,0,1339},{7,0,1490},{7 +,0,1873},{7,10,915},{8,10,247},{9,0,628},{147,10,0},{4,10,202},{5,10,382},{6,10, +454},{7,10,936},{7,10,1803},{8,10,758},{9,10,375},{9,10,895},{10,10,743},{10,10, +792},{11,10,978},{11,10,1012},{142,10,109},{7,11,617},{10,11,498},{11,11,501},{ +12,11,16},{140,11,150},{7,10,1150},{7,10,1425},{7,10,1453},{10,11,747},{140,10, +513},{133,11,155},{11,0,919},{141,0,409},{138,10,791},{10,0,633},{139,11,729},{7 +,11,163},{8,11,319},{9,11,402},{10,11,24},{10,11,681},{11,11,200},{11,11,567},{ +12,11,253},{12,11,410},{142,11,219},{5,11,475},{7,11,1780},{9,11,230},{11,11,297 +},{11,11,558},{14,11,322},{147,11,76},{6,10,445},{7,0,332},{137,10,909},{135,11, +1956},{136,11,274},{134,10,578},{135,0,1489},{135,11,1848},{5,11,944},{134,11, +1769},{132,11,144},{136,10,766},{4,0,832},{135,10,541},{8,0,398},{9,0,681},{139, +0,632},{136,0,645},{9,0,791},{10,0,93},{16,0,13},{17,0,23},{18,0,135},{19,0,12}, +{20,0,1},{20,0,12},{148,0,14},{6,11,247},{137,11,555},{134,0,20},{132,0,800},{ +135,0,1841},{139,10,983},{137,10,768},{132,10,584},{141,11,51},{4,11,620},{6,0, +1993},{138,11,280},{136,0,769},{7,11,1810},{11,0,290},{11,0,665},{11,11,866},{12 +,11,103},{13,11,495},{17,11,67},{147,11,74},{134,0,1426},{139,0,60},{4,10,326},{ +135,10,1770},{4,10,226},{7,0,1874},{137,0,641},{5,10,426},{6,0,644},{8,10,30},{9 +,10,2},{11,10,549},{147,10,122},{5,11,428},{138,11,442},{135,11,1871},{135,0, +1757},{147,10,117},{135,0,937},{135,0,1652},{6,0,654},{134,0,1476},{133,11,99},{ +135,0,527},{132,10,345},{4,10,385},{4,11,397},{7,10,265},{135,10,587},{4,0,579}, +{5,0,226},{5,0,323},{135,0,960},{134,0,1486},{8,11,502},{144,11,9},{4,10,347},{5 +,10,423},{5,10,996},{135,10,1329},{7,11,727},{146,11,73},{4,11,485},{7,10,1259}, +{7,11,353},{7,11,1523},{9,10,125},{139,10,65},{5,10,136},{6,0,325},{6,11,366},{7 +,11,1384},{7,11,1601},{136,10,644},{138,11,160},{6,0,1345},{137,11,282},{18,0,91 +},{147,0,70},{136,0,404},{4,11,157},{133,11,471},{133,0,973},{6,0,135},{135,0, +1176},{8,11,116},{11,11,551},{142,11,159},{4,0,549},{4,10,433},{133,10,719},{136 +,0,976},{5,11,160},{7,11,363},{7,11,589},{10,11,170},{141,11,55},{144,0,21},{144 +,0,51},{135,0,314},{135,10,1363},{4,11,108},{7,11,405},{10,11,491},{139,11,498}, +{146,0,4},{4,10,555},{8,10,536},{10,10,288},{139,10,1005},{135,11,1005},{6,0,281 +},{7,0,6},{8,0,282},{8,0,480},{8,0,499},{9,0,198},{10,0,143},{10,0,169},{10,0, +211},{10,0,417},{10,0,574},{11,0,147},{11,0,395},{12,0,75},{12,0,407},{12,0,608} +,{13,0,500},{142,0,251},{6,0,1093},{6,0,1405},{9,10,370},{138,10,90},{4,11,926}, +{133,11,983},{135,0,1776},{134,0,1528},{132,0,419},{132,11,538},{6,11,294},{7,11 +,1267},{136,11,624},{135,11,1772},{138,11,301},{4,10,257},{135,10,2031},{4,0,138 +},{7,0,1012},{7,0,1280},{7,10,1768},{137,0,76},{132,11,757},{5,0,29},{140,0,638} +,{7,11,655},{135,11,1844},{6,11,257},{7,0,1418},{135,11,1522},{8,11,469},{138,11 +,47},{142,11,278},{6,10,83},{6,10,1733},{135,10,1389},{11,11,204},{11,11,243},{ +140,11,293},{135,11,1875},{6,0,1710},{135,0,2038},{137,11,299},{4,0,17},{5,0,23} +,{7,0,995},{11,0,383},{11,0,437},{12,0,460},{140,0,532},{133,0,862},{137,10,696} +,{6,0,592},{138,0,946},{138,11,599},{7,10,1718},{9,10,95},{9,10,274},{10,10,279} +,{10,10,317},{10,10,420},{11,10,303},{11,10,808},{12,10,134},{12,10,367},{13,10, +149},{13,10,347},{14,10,349},{14,10,406},{18,10,22},{18,10,89},{18,10,122},{147, +10,47},{8,0,70},{12,0,171},{141,0,272},{133,10,26},{132,10,550},{137,0,812},{10, +0,233},{139,0,76},{134,0,988},{134,0,442},{136,10,822},{4,10,902},{5,10,809},{6, +10,122},{135,0,896},{5,11,150},{7,11,106},{8,11,603},{9,11,593},{9,11,634},{10, +11,44},{10,11,173},{11,11,462},{11,11,515},{13,11,216},{13,11,288},{142,11,400}, +{136,0,483},{135,10,262},{5,10,620},{134,0,1709},{4,10,34},{5,10,574},{7,10,279} +,{7,10,1624},{136,10,601},{137,10,170},{147,0,119},{12,11,108},{141,11,291},{11, +0,69},{12,0,105},{12,0,117},{13,0,213},{14,0,13},{14,0,62},{14,0,177},{14,0,421} +,{15,0,19},{146,0,141},{137,0,309},{11,11,278},{142,11,73},{7,0,608},{7,0,976},{ +9,0,146},{10,0,206},{10,0,596},{13,0,218},{142,0,153},{133,10,332},{6,10,261},{8 +,10,182},{139,10,943},{4,11,493},{144,11,55},{134,10,1721},{132,0,768},{4,10,933 +},{133,10,880},{7,11,555},{7,11,1316},{7,11,1412},{7,11,1839},{9,11,192},{9,11, +589},{11,11,241},{11,11,676},{11,11,811},{11,11,891},{12,11,140},{12,11,346},{12 +,11,479},{13,11,30},{13,11,49},{13,11,381},{14,11,188},{15,11,150},{16,11,76},{ +18,11,30},{148,11,52},{4,0,518},{135,0,1136},{6,11,568},{7,11,112},{7,11,1804},{ +8,11,362},{8,11,410},{8,11,830},{9,11,514},{11,11,649},{142,11,157},{135,11,673} +,{8,0,689},{137,0,863},{4,0,18},{4,11,625},{7,0,145},{7,0,444},{7,0,1278},{8,0, +49},{8,0,400},{9,0,71},{9,0,250},{10,0,459},{12,0,160},{144,0,24},{140,0,1020},{ +4,0,997},{6,0,1946},{6,0,1984},{134,0,1998},{6,11,16},{6,11,158},{7,11,43},{7,11 +,129},{7,11,181},{8,11,276},{8,11,377},{10,11,523},{11,11,816},{12,11,455},{13, +11,303},{142,11,135},{133,10,812},{134,0,658},{4,11,1},{7,11,1143},{7,11,1463},{ +8,11,61},{9,11,207},{9,11,390},{9,11,467},{139,11,836},{150,11,26},{140,0,106},{ +4,10,137},{6,0,1827},{7,10,1178},{7,10,1520},{7,11,1319},{10,0,931},{18,0,166},{ +148,0,114},{133,0,1010},{4,11,723},{5,11,895},{7,11,1031},{8,11,199},{8,11,340}, +{9,11,153},{9,11,215},{10,11,21},{10,11,59},{10,11,80},{10,11,224},{11,11,229},{ +11,11,652},{12,11,192},{13,11,146},{142,11,91},{132,11,295},{6,11,619},{7,11,898 +},{7,11,1092},{8,11,485},{18,11,28},{147,11,116},{137,11,51},{6,10,1661},{7,10, +1975},{7,10,2009},{135,10,2011},{5,11,309},{140,11,211},{5,0,87},{7,0,313},{7,0, +1103},{10,0,208},{10,0,582},{11,0,389},{11,0,813},{12,0,385},{13,0,286},{14,0, +124},{146,0,108},{5,11,125},{8,11,77},{138,11,15},{132,0,267},{133,0,703},{137, +11,155},{133,11,439},{11,11,164},{140,11,76},{5,10,89},{7,10,1915},{9,0,496},{9, +10,185},{9,10,235},{10,10,64},{10,10,270},{10,10,403},{10,10,469},{10,10,529},{ +10,10,590},{11,10,140},{11,10,860},{13,10,1},{13,10,422},{14,10,341},{14,10,364} +,{17,10,93},{18,10,113},{19,10,97},{147,10,113},{133,10,695},{135,0,1121},{5,10, +6},{6,10,183},{7,10,680},{7,10,978},{7,10,1013},{7,10,1055},{12,10,230},{13,10, +172},{146,10,29},{4,11,8},{7,11,1152},{7,11,1153},{7,11,1715},{9,11,374},{10,11, +478},{139,11,648},{135,11,1099},{6,10,29},{139,10,63},{4,0,561},{10,0,249},{139, +0,209},{132,0,760},{7,11,799},{138,11,511},{136,11,87},{9,0,154},{140,0,485},{ +136,0,255},{132,0,323},{140,0,419},{132,10,311},{134,10,1740},{4,0,368},{135,0, +641},{7,10,170},{8,10,90},{8,10,177},{8,10,415},{11,10,714},{142,10,281},{4,11, +69},{5,11,122},{9,11,656},{138,11,464},{5,11,849},{134,11,1633},{8,0,522},{142,0 +,328},{11,10,91},{13,10,129},{15,10,101},{145,10,125},{4,10,494},{6,10,74},{7,0, +562},{7,10,44},{8,0,551},{11,11,499},{12,10,17},{15,10,5},{148,10,11},{4,10,276} +,{133,10,296},{9,0,92},{147,0,91},{4,10,7},{5,10,90},{5,10,158},{6,10,542},{7,10 +,221},{7,10,1574},{9,10,490},{10,10,540},{11,10,443},{139,10,757},{6,0,525},{6,0 +,1976},{8,0,806},{9,0,876},{140,0,284},{5,11,859},{7,10,588},{7,11,1160},{8,11, +107},{9,10,175},{9,11,291},{9,11,439},{10,10,530},{10,11,663},{11,11,609},{140, +11,197},{7,11,168},{13,11,196},{141,11,237},{139,0,958},{133,0,594},{135,10,580} +,{7,10,88},{136,10,627},{5,10,872},{6,0,479},{6,0,562},{6,10,57},{7,0,1060},{7, +10,471},{9,10,447},{9,10,454},{141,0,6},{136,11,413},{145,11,19},{4,11,117},{6, +11,372},{7,11,1905},{142,11,323},{4,11,722},{139,11,471},{5,10,31},{6,10,614},{ +145,0,61},{8,10,330},{140,10,477},{7,10,1200},{138,10,460},{6,10,424},{135,10, +1866},{6,0,1641},{136,0,820},{6,0,1556},{134,0,1618},{9,11,5},{12,11,216},{12,11 +,294},{12,11,298},{12,11,400},{12,11,518},{13,11,229},{143,11,139},{15,11,155},{ +144,11,79},{4,0,302},{135,0,1766},{5,10,13},{134,10,142},{6,0,148},{7,0,1313},{7 +,10,116},{8,10,322},{8,10,755},{9,10,548},{10,10,714},{11,10,884},{141,10,324},{ +137,0,676},{9,11,88},{139,11,270},{5,11,12},{7,11,375},{137,11,438},{134,0,1674} +,{7,10,1472},{135,10,1554},{7,10,1071},{7,10,1541},{7,10,1767},{7,10,1806},{11,0 +,178},{11,10,162},{11,10,242},{12,10,605},{15,10,26},{144,10,44},{6,0,389},{7,0, +149},{9,0,142},{138,0,94},{140,11,71},{145,10,115},{6,0,8},{7,0,1881},{8,0,91},{ +11,11,966},{12,11,287},{13,11,342},{13,11,402},{15,11,110},{143,11,163},{4,11, +258},{136,11,639},{6,11,22},{7,11,903},{138,11,577},{133,11,681},{135,10,1111},{ +135,11,1286},{8,10,1},{9,0,112},{138,10,326},{5,10,488},{6,10,527},{7,10,489},{7 +,10,1636},{8,10,121},{8,10,144},{8,10,359},{9,10,193},{9,10,241},{9,10,336},{9, +10,882},{11,10,266},{11,10,372},{11,10,944},{12,10,401},{140,10,641},{4,11,664}, +{133,11,804},{6,0,747},{134,0,1015},{135,0,1746},{9,10,31},{10,10,244},{10,10, +699},{12,10,149},{141,10,497},{133,10,377},{135,0,24},{5,11,32},{6,0,1352},{145, +10,101},{7,0,1530},{10,0,158},{13,0,13},{13,0,137},{13,0,258},{14,0,111},{14,0, +225},{14,0,253},{14,0,304},{14,0,339},{14,0,417},{146,0,33},{4,0,503},{135,0, +1661},{5,0,130},{6,0,845},{7,0,1314},{9,0,610},{10,0,718},{11,0,601},{11,0,819}, +{11,0,946},{140,0,536},{10,0,149},{11,0,280},{142,0,336},{134,0,1401},{135,0, +1946},{8,0,663},{144,0,8},{134,0,1607},{135,10,2023},{4,11,289},{7,11,629},{7,11 +,1698},{7,11,1711},{140,11,215},{6,11,450},{136,11,109},{10,0,882},{10,0,883},{ +10,0,914},{138,0,928},{133,10,843},{136,11,705},{132,10,554},{133,10,536},{5,0, +417},{9,10,79},{11,10,625},{145,10,7},{7,11,1238},{142,11,37},{4,0,392},{135,0, +1597},{4,10,424},{5,0,433},{9,0,633},{139,0,629},{7,10,336},{136,10,785},{134,11 +,355},{6,0,234},{7,0,769},{9,0,18},{138,0,358},{4,10,896},{134,10,1777},{138,11, +323},{7,0,140},{7,0,1950},{8,0,680},{11,0,817},{147,0,88},{7,0,1222},{138,0,386} +,{139,11,908},{11,0,249},{12,0,313},{16,0,66},{145,0,26},{134,0,5},{7,10,750},{9 +,10,223},{11,10,27},{11,10,466},{12,10,624},{14,10,265},{146,10,61},{134,11,26}, +{134,0,1216},{5,0,963},{134,0,1773},{4,11,414},{5,11,467},{9,11,654},{10,11,451} +,{12,11,59},{141,11,375},{135,11,17},{4,10,603},{133,10,661},{4,10,11},{6,10,128 +},{7,10,231},{7,10,1533},{138,10,725},{135,11,955},{7,0,180},{8,0,509},{136,0, +792},{132,10,476},{132,0,1002},{133,11,538},{135,10,1807},{132,0,931},{7,0,943}, +{11,0,614},{140,0,747},{135,0,1837},{9,10,20},{10,10,324},{10,10,807},{139,10, +488},{134,0,641},{6,11,280},{10,11,502},{11,11,344},{140,11,38},{5,11,45},{7,11, +1161},{11,11,448},{11,11,880},{13,11,139},{13,11,407},{15,11,16},{17,11,95},{18, +11,66},{18,11,88},{18,11,123},{149,11,7},{9,0,280},{138,0,134},{22,0,22},{23,0,5 +},{151,0,29},{136,11,777},{4,0,90},{4,11,410},{5,0,545},{7,0,754},{7,11,521},{9, +0,186},{10,0,72},{10,0,782},{11,0,577},{11,0,610},{11,0,960},{12,0,354},{12,0, +362},{140,0,595},{135,11,1778},{5,10,112},{6,10,103},{134,10,150},{138,10,356},{ +132,0,742},{7,0,151},{9,0,329},{139,0,254},{8,0,853},{8,0,881},{8,0,911},{8,0, +912},{10,0,872},{12,0,741},{12,0,742},{152,0,18},{4,11,573},{136,11,655},{6,0, +921},{134,0,934},{9,0,187},{10,0,36},{11,0,1016},{17,0,44},{146,0,64},{7,0,833}, +{136,0,517},{4,0,506},{5,0,295},{135,0,1680},{4,10,708},{8,10,15},{9,10,50},{9, +10,386},{11,10,18},{11,10,529},{140,10,228},{4,10,563},{7,0,251},{7,0,1701},{7, +10,592},{7,10,637},{7,10,770},{8,0,436},{8,10,463},{9,10,60},{9,10,335},{9,10, +904},{10,10,73},{11,10,434},{12,10,585},{13,10,331},{18,10,110},{148,10,60},{132 +,10,502},{136,0,584},{6,10,347},{138,10,161},{7,0,987},{7,11,899},{9,0,688},{10, +0,522},{11,0,788},{12,0,137},{12,0,566},{14,0,9},{14,0,24},{14,0,64},{142,11,325 +},{4,0,214},{5,0,500},{5,10,102},{6,10,284},{7,10,1079},{7,10,1423},{7,10,1702}, +{8,10,470},{9,10,554},{9,10,723},{139,10,333},{7,10,246},{135,10,840},{6,10,10}, +{8,10,571},{9,10,739},{143,10,91},{133,10,626},{146,0,195},{134,0,1775},{7,0,389 +},{7,0,700},{7,0,940},{8,0,514},{9,0,116},{9,0,535},{10,0,118},{11,0,107},{11,0, +148},{11,0,922},{12,0,254},{12,0,421},{142,0,238},{5,10,18},{6,10,526},{13,10,24 +},{13,10,110},{19,10,5},{147,10,44},{132,0,743},{4,10,309},{5,10,462},{7,10,970} +,{7,10,1097},{139,0,292},{22,10,30},{150,10,33},{139,11,338},{135,11,1598},{7,0, +1283},{9,0,227},{11,0,325},{11,0,408},{14,0,180},{146,0,47},{4,0,953},{6,0,1805} +,{6,0,1814},{6,0,1862},{140,0,774},{6,11,611},{135,11,1733},{135,11,1464},{5,0, +81},{7,0,146},{7,0,1342},{8,0,53},{8,0,561},{8,0,694},{8,0,754},{9,0,115},{9,0, +179},{9,0,894},{10,0,462},{10,0,813},{11,0,230},{11,0,657},{11,0,699},{11,0,748} +,{12,0,119},{12,0,200},{12,0,283},{142,0,273},{5,0,408},{6,0,789},{6,0,877},{6,0 +,1253},{6,0,1413},{137,0,747},{134,10,1704},{135,11,663},{6,0,1910},{6,0,1915},{ +6,0,1923},{9,0,913},{9,0,928},{9,0,950},{9,0,954},{9,0,978},{9,0,993},{12,0,812} +,{12,0,819},{12,0,831},{12,0,833},{12,0,838},{12,0,909},{12,0,928},{12,0,931},{ +12,0,950},{15,0,186},{15,0,187},{15,0,195},{15,0,196},{15,0,209},{15,0,215},{15, +0,236},{15,0,241},{15,0,249},{15,0,253},{18,0,180},{18,0,221},{18,0,224},{18,0, +227},{18,0,229},{149,0,60},{7,0,1826},{135,0,1938},{5,10,86},{7,10,743},{9,10,85 +},{10,10,281},{10,10,432},{11,0,490},{12,10,251},{13,10,118},{14,10,378},{146,0, +143},{5,10,524},{133,10,744},{141,11,442},{10,10,107},{140,10,436},{135,11,503}, +{134,0,1162},{132,10,927},{7,0,30},{8,0,86},{8,0,315},{8,0,700},{9,0,576},{9,0, +858},{10,0,414},{11,0,310},{11,0,888},{11,0,904},{12,0,361},{12,10,670},{13,0, +248},{13,0,371},{14,0,142},{146,10,94},{134,0,721},{4,11,113},{5,11,163},{5,11, +735},{7,10,1149},{7,11,1009},{9,10,156},{9,11,9},{9,11,771},{12,11,90},{13,11, +138},{13,11,410},{143,11,128},{138,0,839},{133,10,778},{137,0,617},{133,10,502}, +{8,10,196},{10,10,283},{139,10,406},{6,0,428},{7,0,524},{8,0,169},{8,0,234},{9,0 +,480},{138,0,646},{133,10,855},{134,0,1648},{7,0,1205},{138,0,637},{4,11,935},{5 +,11,823},{135,0,1596},{5,11,269},{7,11,434},{7,11,891},{8,11,339},{9,11,702},{11 +,11,594},{11,11,718},{145,11,100},{7,11,878},{9,11,485},{141,11,264},{4,0,266},{ +6,11,1713},{8,0,4},{9,0,39},{10,0,166},{11,0,918},{12,0,635},{20,0,10},{22,0,27} +,{22,0,43},{150,0,52},{7,10,1400},{9,10,446},{138,10,45},{135,11,900},{132,0,862 +},{134,0,1554},{135,11,1033},{19,0,16},{147,11,16},{135,11,1208},{7,0,157},{136, +0,279},{6,0,604},{136,0,391},{13,10,455},{15,10,99},{15,10,129},{144,10,68},{135 +,10,172},{7,0,945},{11,0,713},{139,0,744},{4,0,973},{10,0,877},{10,0,937},{10,0, +938},{140,0,711},{139,0,1022},{132,10,568},{142,11,143},{4,0,567},{4,10,732},{ +137,0,859},{7,0,1846},{136,0,628},{136,10,733},{133,0,762},{4,10,428},{135,10, +1789},{7,10,2015},{10,0,784},{12,10,665},{141,0,191},{133,0,298},{7,0,633},{7,0, +905},{7,0,909},{7,0,1538},{9,0,767},{140,0,636},{138,10,806},{132,0,795},{139,0, +301},{135,0,1970},{5,11,625},{135,11,1617},{135,11,275},{7,11,37},{8,11,425},{8, +11,693},{9,11,720},{10,11,380},{10,11,638},{11,11,273},{11,11,307},{11,11,473},{ +12,11,61},{143,11,43},{135,11,198},{134,0,1236},{7,0,369},{12,0,644},{12,0,645}, +{144,0,90},{19,0,15},{149,0,27},{6,0,71},{6,10,1623},{6,10,1681},{7,0,845},{8,0, +160},{137,0,318},{134,0,1447},{134,0,1255},{138,0,735},{4,11,168},{136,0,76},{6, +10,1748},{8,10,715},{9,10,802},{10,10,46},{10,10,819},{13,10,308},{14,10,351},{ +14,10,363},{146,10,67},{135,11,91},{4,10,63},{5,10,347},{134,0,474},{133,10,749} +,{138,0,841},{133,10,366},{4,11,225},{134,0,836},{135,0,1622},{135,10,89},{140,0 +,735},{134,0,1601},{138,11,145},{6,0,1390},{137,0,804},{142,0,394},{6,11,15},{7, +11,70},{10,11,240},{147,11,93},{6,0,96},{135,0,1426},{4,0,651},{133,0,289},{7,10 +,977},{7,11,956},{7,11,1157},{7,11,1506},{7,11,1606},{7,11,1615},{7,11,1619},{7, +11,1736},{7,11,1775},{8,11,590},{9,11,324},{9,11,736},{9,11,774},{9,11,776},{9, +11,784},{10,11,567},{10,11,708},{11,11,518},{11,11,613},{11,11,695},{11,11,716}, +{11,11,739},{11,11,770},{11,11,771},{11,11,848},{11,11,857},{11,11,931},{11,11, +947},{12,11,326},{12,11,387},{12,11,484},{12,11,528},{12,11,552},{12,11,613},{13 +,11,189},{13,11,256},{13,11,340},{13,11,432},{13,11,436},{13,11,440},{13,11,454} +,{14,11,174},{14,11,220},{14,11,284},{14,11,390},{145,11,121},{7,0,688},{8,0,35} +,{9,0,511},{10,0,767},{147,0,118},{134,0,667},{4,0,513},{5,10,824},{133,10,941}, +{7,10,440},{8,10,230},{139,10,106},{134,0,2034},{135,11,1399},{143,11,66},{135, +11,1529},{4,11,145},{6,11,176},{7,11,395},{9,11,562},{144,11,28},{132,11,501},{ +132,0,704},{134,0,1524},{6,11,464},{135,0,1078},{6,11,509},{10,11,82},{20,11,91} +,{151,11,13},{4,0,720},{133,0,306},{133,0,431},{4,10,914},{5,10,800},{5,10,852}, +{135,0,1196},{135,11,1189},{10,0,54},{141,10,115},{7,10,564},{142,10,168},{4,10, +918},{5,0,464},{5,10,876},{6,0,236},{7,0,696},{7,0,914},{7,0,1108},{7,0,1448},{9 +,0,15},{9,0,564},{10,0,14},{12,0,565},{13,0,449},{14,0,53},{15,0,13},{16,0,64},{ +145,0,41},{6,0,1418},{134,10,1764},{4,10,92},{133,10,274},{134,0,907},{4,11,114} +,{8,10,501},{9,11,492},{13,11,462},{142,11,215},{4,11,77},{5,11,361},{6,11,139}, +{6,11,401},{6,11,404},{7,11,413},{7,11,715},{7,11,1716},{11,11,279},{12,11,179}, +{12,11,258},{13,11,244},{142,11,358},{6,0,1767},{12,0,194},{145,0,107},{134,11, +1717},{5,10,743},{142,11,329},{4,10,49},{7,10,280},{135,10,1633},{5,0,840},{7,11 +,1061},{8,11,82},{11,11,250},{12,11,420},{141,11,184},{135,11,724},{134,0,900},{ +136,10,47},{134,0,1436},{144,11,0},{5,10,272},{5,10,908},{5,10,942},{6,0,675},{7 +,0,1008},{7,0,1560},{8,10,197},{9,0,642},{9,10,47},{11,0,236},{11,10,538},{11,10 +,742},{142,0,193},{4,0,68},{5,0,628},{5,0,634},{6,0,386},{7,0,794},{8,0,273},{9, +0,563},{10,0,105},{10,0,171},{11,0,94},{139,0,354},{135,10,1911},{137,10,891},{4 +,0,95},{6,0,1297},{6,0,1604},{7,0,416},{139,0,830},{6,11,513},{135,11,1052},{7,0 +,731},{13,0,20},{143,0,11},{137,11,899},{10,0,850},{140,0,697},{4,0,662},{7,11, +1417},{12,11,382},{17,11,48},{152,11,12},{133,0,736},{132,0,861},{4,10,407},{132 +,10,560},{141,10,490},{6,11,545},{7,11,565},{7,11,1669},{10,11,114},{11,11,642}, +{140,11,618},{6,0,871},{134,0,1000},{5,0,864},{5,11,5},{10,0,648},{11,0,671},{ +143,0,46},{133,0,928},{4,10,475},{11,0,90},{11,10,35},{13,0,7},{13,10,71},{13,10 +,177},{142,10,422},{136,0,332},{135,11,192},{134,0,1055},{136,11,763},{11,0,986} +,{140,0,682},{7,0,76},{8,0,44},{9,0,884},{10,0,580},{11,0,399},{11,0,894},{143,0 +,122},{135,11,1237},{135,10,636},{6,10,222},{7,10,1620},{8,10,409},{9,10,693},{ +139,0,300},{4,11,87},{5,11,250},{10,11,601},{13,11,298},{13,11,353},{141,11,376} +,{5,0,518},{10,0,340},{11,0,175},{149,0,16},{140,0,771},{6,0,1108},{137,0,831},{ +132,0,836},{135,0,1852},{4,0,957},{6,0,1804},{8,0,842},{8,0,843},{8,0,851},{8,0, +855},{140,0,767},{135,11,814},{4,11,57},{7,11,1195},{7,11,1438},{7,11,1548},{7, +11,1835},{7,11,1904},{9,11,757},{10,11,604},{139,11,519},{133,10,882},{138,0,246 +},{4,0,934},{5,0,202},{7,11,1897},{8,0,610},{12,11,290},{13,11,80},{13,11,437},{ +145,11,74},{8,0,96},{9,0,36},{10,0,607},{10,0,804},{10,0,832},{11,0,423},{11,0, +442},{12,0,309},{14,0,199},{15,0,90},{145,0,110},{132,10,426},{6,10,58},{7,0,654 +},{7,10,745},{7,10,1969},{8,0,240},{8,10,675},{9,10,479},{9,10,731},{10,10,330}, +{10,10,593},{10,10,817},{11,10,32},{11,10,133},{11,10,221},{145,10,68},{9,0,13}, +{9,0,398},{9,0,727},{10,0,75},{10,0,184},{10,0,230},{10,0,564},{10,0,569},{11,0, +973},{12,0,70},{12,0,189},{13,0,57},{141,0,257},{4,11,209},{135,11,902},{7,0,391 +},{137,10,538},{134,0,403},{6,11,303},{7,11,335},{7,11,1437},{7,11,1668},{8,11, +553},{8,11,652},{8,11,656},{9,11,558},{11,11,743},{149,11,18},{132,11,559},{11,0 +,75},{142,0,267},{6,0,815},{141,11,2},{141,0,366},{137,0,631},{133,11,1017},{5,0 +,345},{135,0,1016},{133,11,709},{134,11,1745},{133,10,566},{6,10,48},{7,0,952},{ +9,10,139},{10,10,399},{11,10,469},{12,10,634},{141,10,223},{133,0,673},{7,11,8}, +{8,11,206},{137,0,850},{6,0,662},{149,0,35},{4,0,287},{133,0,1018},{6,10,114},{7 +,10,1224},{7,10,1556},{136,10,3},{8,10,576},{137,10,267},{4,0,884},{5,0,34},{10, +0,724},{12,0,444},{13,0,354},{18,0,32},{23,0,24},{23,0,31},{152,0,5},{133,10,933 +},{132,11,776},{138,0,151},{136,0,427},{134,0,382},{132,0,329},{9,0,846},{10,0, +827},{138,11,33},{7,11,1297},{9,0,279},{10,0,407},{14,0,84},{150,0,18},{136,11, +406},{132,0,906},{136,0,366},{134,0,843},{134,0,1443},{135,0,1372},{138,0,992},{ +4,0,123},{5,0,605},{7,0,1509},{136,0,36},{132,0,649},{8,11,175},{10,11,168},{138 +,11,573},{133,0,767},{134,0,1018},{135,11,1305},{12,10,30},{13,10,148},{14,10,87 +},{14,10,182},{16,10,42},{148,10,70},{134,11,607},{4,0,273},{5,0,658},{133,0,995 +},{6,0,72},{139,11,174},{7,10,56},{7,10,1989},{8,10,337},{8,10,738},{9,10,600},{ +10,0,483},{12,0,368},{13,10,447},{142,10,92},{5,11,784},{138,10,666},{135,0,1345 +},{139,11,882},{134,0,1293},{133,0,589},{134,0,1988},{5,0,117},{6,0,514},{6,0, +541},{7,0,1164},{7,0,1436},{8,0,220},{8,0,648},{10,0,688},{139,0,560},{136,0,379 +},{5,0,686},{7,10,866},{135,10,1163},{132,10,328},{9,11,14},{9,11,441},{10,11, +306},{139,11,9},{4,10,101},{135,10,1171},{5,10,833},{136,10,744},{5,11,161},{7, +11,839},{135,11,887},{7,0,196},{10,0,765},{11,0,347},{11,0,552},{11,0,790},{12,0 +,263},{13,0,246},{13,0,270},{13,0,395},{14,0,176},{14,0,190},{14,0,398},{14,0, +412},{15,0,32},{15,0,63},{16,0,88},{147,0,105},{6,10,9},{6,10,397},{7,10,53},{7, +10,1742},{10,10,632},{11,10,828},{140,10,146},{5,0,381},{135,0,1792},{134,0,1452 +},{135,11,429},{8,0,367},{10,0,760},{14,0,79},{20,0,17},{152,0,0},{7,0,616},{138 +,0,413},{11,10,417},{12,10,223},{140,10,265},{7,11,1611},{13,11,14},{15,11,44},{ +19,11,13},{148,11,76},{135,0,1229},{6,0,120},{7,0,1188},{7,0,1710},{8,0,286},{9, +0,667},{11,0,592},{139,0,730},{135,11,1814},{135,0,1146},{4,10,186},{5,10,157},{ +8,10,168},{138,10,6},{4,0,352},{135,0,687},{4,0,192},{5,0,49},{6,0,200},{6,0,293 +},{6,0,1696},{135,0,1151},{133,10,875},{5,10,773},{5,10,991},{6,10,1635},{134,10 +,1788},{7,10,111},{136,10,581},{6,0,935},{134,0,1151},{134,0,1050},{132,0,650},{ +132,0,147},{11,0,194},{11,11,194},{12,0,62},{12,0,88},{12,11,62},{140,11,88},{6, +0,339},{135,0,923},{134,10,1747},{7,11,643},{136,11,236},{133,0,934},{7,10,1364} +,{7,10,1907},{141,10,158},{132,10,659},{4,10,404},{135,10,675},{7,11,581},{9,11, +644},{137,11,699},{13,0,211},{14,0,133},{14,0,204},{15,0,64},{15,0,69},{15,0,114 +},{16,0,10},{19,0,23},{19,0,35},{19,0,39},{19,0,51},{19,0,71},{19,0,75},{152,0, +15},{133,10,391},{5,11,54},{135,11,1513},{5,10,540},{6,10,1697},{7,0,222},{136,0 +,341},{134,10,78},{132,11,744},{136,0,293},{137,11,701},{7,11,930},{10,11,402},{ +10,11,476},{13,11,452},{18,11,55},{147,11,104},{132,0,637},{133,10,460},{8,11,50 +},{137,11,624},{132,11,572},{134,0,1159},{4,10,199},{139,10,34},{134,0,847},{134 +,10,388},{6,11,43},{7,11,38},{8,11,248},{9,11,504},{138,11,513},{4,10,511},{6,10 +,608},{9,0,683},{9,10,333},{10,10,602},{11,10,441},{11,10,723},{11,10,976},{140, +10,357},{9,0,867},{138,0,837},{6,0,944},{135,11,326},{135,0,1809},{5,10,938},{7, +11,783},{136,10,707},{133,11,766},{133,11,363},{6,0,170},{7,0,1080},{8,0,395},{8 +,0,487},{141,0,147},{6,11,258},{140,11,409},{4,0,535},{5,11,249},{8,0,618},{148, +11,82},{6,0,1379},{149,11,15},{135,0,1625},{150,0,23},{5,11,393},{6,11,378},{7, +11,1981},{9,11,32},{9,11,591},{10,11,685},{10,11,741},{142,11,382},{133,11,788}, +{7,11,1968},{10,11,19},{139,11,911},{7,11,1401},{135,11,1476},{4,11,61},{5,11,58 +},{5,11,171},{5,11,635},{5,11,683},{5,11,700},{6,11,291},{6,11,566},{7,11,1650}, +{11,11,523},{12,11,273},{12,11,303},{15,11,39},{143,11,111},{6,10,469},{7,10, +1709},{138,10,515},{4,0,778},{134,11,589},{132,0,46},{5,0,811},{6,0,1679},{6,0, +1714},{135,0,2032},{7,0,1458},{9,0,407},{11,0,15},{12,0,651},{149,0,37},{4,10, +500},{135,0,938},{6,0,34},{7,0,69},{7,0,1089},{7,0,1281},{8,0,708},{8,0,721},{9, +0,363},{148,0,98},{10,11,231},{147,11,124},{7,11,726},{152,11,9},{5,10,68},{134, +10,383},{136,11,583},{4,11,917},{133,11,1005},{11,10,216},{139,10,340},{135,11, +1675},{8,0,441},{10,0,314},{143,0,3},{132,11,919},{4,10,337},{6,10,353},{7,10, +1934},{8,10,488},{137,10,429},{7,0,889},{7,10,1795},{8,10,259},{9,10,135},{9,10, +177},{9,10,860},{10,10,825},{11,10,115},{11,10,370},{11,10,405},{11,10,604},{12, +10,10},{12,10,667},{12,10,669},{13,10,76},{14,10,310},{15,10,76},{15,10,147},{ +148,10,23},{4,10,15},{4,11,255},{5,10,22},{5,11,302},{6,10,244},{6,11,132},{7,10 +,40},{7,10,200},{7,10,906},{7,10,1199},{7,11,128},{7,11,283},{7,11,1299},{9,10, +616},{10,10,716},{10,11,52},{10,11,514},{11,10,635},{11,10,801},{11,11,925},{12, +10,458},{13,11,92},{142,11,309},{132,0,462},{137,11,173},{135,10,1735},{5,10,598 +},{7,10,791},{8,0,525},{8,10,108},{137,10,123},{5,0,73},{6,0,23},{134,0,338},{ +132,0,676},{132,10,683},{7,0,725},{8,0,498},{139,0,268},{12,0,21},{151,0,7},{135 +,0,773},{4,10,155},{135,10,1689},{4,0,164},{5,0,730},{5,10,151},{5,10,741},{6,11 +,210},{7,10,498},{7,10,870},{7,10,1542},{12,10,213},{14,10,36},{14,10,391},{17, +10,111},{18,10,6},{18,10,46},{18,10,151},{19,10,36},{20,10,32},{20,10,56},{20,10 +,69},{20,10,102},{21,10,4},{22,10,8},{22,10,10},{22,10,14},{150,10,31},{4,10,624 +},{135,10,1752},{4,0,583},{6,11,588},{9,0,936},{15,0,214},{18,0,199},{152,0,26}, +{4,11,284},{6,11,223},{7,0,1462},{139,0,659},{133,0,220},{139,0,803},{132,0,544} +,{4,10,492},{133,10,451},{16,0,98},{148,0,119},{4,11,218},{7,11,526},{143,11,137 +},{135,10,835},{4,11,270},{5,11,192},{6,11,332},{7,11,1322},{13,10,70},{13,11,9} +,{14,11,104},{142,11,311},{132,10,539},{140,11,661},{5,0,176},{6,0,437},{6,0,564 +},{11,0,181},{141,0,183},{135,0,1192},{6,10,113},{135,10,436},{136,10,718},{135, +10,520},{135,0,1878},{140,11,196},{7,11,379},{8,11,481},{137,11,377},{5,11,1003} +,{6,11,149},{137,11,746},{8,11,262},{9,11,627},{10,11,18},{11,11,214},{11,11,404 +},{11,11,457},{11,11,780},{11,11,849},{11,11,913},{13,11,330},{13,11,401},{142, +11,200},{149,0,26},{136,11,304},{132,11,142},{135,0,944},{4,0,790},{5,0,273},{ +134,0,394},{134,0,855},{4,0,135},{6,0,127},{7,0,1185},{7,0,1511},{8,0,613},{11,0 +,5},{12,0,336},{12,0,495},{12,0,586},{12,0,660},{12,0,668},{14,0,385},{15,0,118} +,{17,0,20},{146,0,98},{6,0,230},{9,0,752},{12,10,610},{13,10,431},{16,10,59},{ +146,0,109},{7,0,1954},{135,11,925},{4,11,471},{5,11,51},{6,11,602},{8,11,484},{ +10,11,195},{140,11,159},{132,10,307},{136,11,688},{132,11,697},{7,11,812},{7,11, +1261},{7,11,1360},{9,11,632},{140,11,352},{5,0,162},{5,10,964},{136,0,68},{4,0, +654},{136,11,212},{4,0,156},{7,0,998},{7,0,1045},{7,0,1860},{9,0,48},{9,0,692},{ +11,0,419},{139,0,602},{133,11,221},{4,11,373},{5,11,283},{6,11,480},{135,11,609} +,{142,11,216},{132,0,240},{6,11,192},{9,11,793},{145,11,55},{4,10,75},{5,10,180} +,{6,10,500},{7,10,58},{7,10,710},{138,10,645},{4,11,132},{5,10,649},{5,11,69},{ +135,11,1242},{6,10,276},{7,10,282},{7,10,879},{7,10,924},{8,10,459},{9,10,599},{ +9,10,754},{11,10,574},{12,10,128},{12,10,494},{13,10,52},{13,10,301},{15,10,30}, +{143,10,132},{132,10,200},{4,11,111},{135,11,302},{9,0,197},{10,0,300},{12,0,473 +},{13,0,90},{141,0,405},{132,11,767},{6,11,42},{7,11,1416},{7,11,1590},{7,11, +2005},{8,11,131},{8,11,466},{9,11,672},{13,11,252},{148,11,103},{8,0,958},{8,0, +999},{10,0,963},{138,0,1001},{135,10,1621},{135,0,858},{4,0,606},{137,11,444},{6 +,11,44},{136,11,368},{139,11,172},{4,11,570},{133,11,120},{139,11,624},{6,10,225 +},{7,0,1978},{8,0,676},{137,10,211},{7,0,972},{8,10,687},{139,0,102},{6,11,227}, +{135,11,1589},{8,10,58},{9,10,724},{11,10,809},{13,10,113},{145,10,72},{4,0,361} +,{133,0,315},{132,0,461},{6,10,345},{135,10,1247},{132,0,472},{8,10,767},{8,10, +803},{9,10,301},{137,10,903},{135,11,1333},{135,11,477},{7,10,1949},{136,10,674} +,{6,0,905},{138,0,747},{133,0,155},{134,10,259},{7,0,163},{8,0,319},{9,0,402},{ +10,0,24},{10,0,681},{11,0,200},{12,0,253},{12,0,410},{142,0,219},{5,0,475},{6,11 +,1667},{7,0,1780},{7,11,2036},{9,0,230},{10,11,600},{11,0,297},{11,0,558},{14,0, +322},{147,0,76},{136,10,254},{6,0,848},{135,0,1956},{6,11,511},{140,11,132},{5, +11,568},{6,11,138},{135,11,1293},{6,0,631},{137,0,838},{149,0,36},{4,11,565},{8, +11,23},{136,11,827},{5,0,944},{134,0,1769},{4,0,144},{4,11,922},{5,11,1023},{6,0 +,842},{134,0,1400},{133,10,248},{9,10,800},{10,10,693},{11,10,482},{11,10,734},{ +139,10,789},{7,11,1002},{139,11,145},{4,10,116},{5,10,95},{5,10,445},{7,10,1688} +,{8,10,29},{9,10,272},{11,10,509},{139,10,915},{14,0,369},{146,0,72},{135,10, +1641},{132,11,740},{133,10,543},{140,11,116},{5,10,181},{6,0,247},{8,10,41},{137 +,0,555},{133,10,657},{136,0,996},{138,10,709},{7,0,189},{8,10,202},{138,10,536}, +{136,11,402},{4,11,716},{141,11,31},{10,0,280},{138,0,797},{9,10,423},{140,10,89 +},{8,10,113},{9,10,877},{10,10,554},{11,10,83},{12,10,136},{147,10,109},{133,10, +976},{4,10,206},{135,0,746},{136,0,526},{139,0,345},{136,0,1017},{8,11,152},{9, +11,53},{9,11,268},{9,11,901},{10,11,518},{10,11,829},{11,11,188},{13,11,74},{14, +11,46},{15,11,17},{15,11,33},{17,11,40},{18,11,36},{19,11,20},{22,11,1},{152,11, +2},{133,11,736},{136,11,532},{5,0,428},{138,0,651},{135,11,681},{135,0,1162},{7, +0,327},{8,10,226},{10,10,537},{11,10,570},{11,10,605},{11,10,799},{11,10,804},{ +12,10,85},{12,10,516},{12,10,623},{12,11,677},{13,0,230},{13,10,361},{14,10,77}, +{14,10,78},{17,0,113},{147,10,110},{4,0,792},{4,10,769},{7,0,1717},{138,0,546},{ +4,11,684},{136,11,384},{132,10,551},{134,0,1203},{9,10,57},{9,10,459},{10,10,425 +},{11,10,119},{12,10,184},{12,10,371},{13,10,358},{145,10,51},{5,0,672},{5,10, +814},{8,10,10},{9,10,421},{9,10,729},{10,10,609},{139,10,689},{138,0,189},{134, +10,624},{7,11,110},{7,11,188},{8,11,290},{8,11,591},{9,11,382},{9,11,649},{11,11 +,71},{11,11,155},{11,11,313},{12,11,5},{13,11,325},{142,11,287},{133,0,99},{6,0, +1053},{135,0,298},{7,11,360},{7,11,425},{9,11,66},{9,11,278},{138,11,644},{4,0, +397},{136,0,555},{137,10,269},{132,10,528},{4,11,900},{133,11,861},{5,11,254},{6 +,0,1157},{7,11,985},{136,11,73},{7,11,1959},{136,11,683},{12,0,398},{20,0,39},{ +21,0,11},{150,0,41},{4,0,485},{7,0,353},{135,0,1523},{6,0,366},{7,0,1384},{135,0 +,1601},{138,0,787},{137,0,282},{5,10,104},{6,10,173},{135,10,1631},{139,11,146}, +{4,0,157},{133,0,471},{134,0,941},{132,11,725},{7,0,1336},{8,10,138},{8,10,342}, +{9,10,84},{10,10,193},{11,10,883},{140,10,359},{134,11,196},{136,0,116},{133,11, +831},{134,0,787},{134,10,95},{6,10,406},{10,10,409},{10,10,447},{11,10,44},{140, +10,100},{5,0,160},{7,0,363},{7,0,589},{10,0,170},{141,0,55},{134,0,1815},{132,0, +866},{4,11,321},{6,0,889},{6,0,1067},{6,0,1183},{134,11,569},{5,11,848},{134,11, +66},{4,11,36},{6,10,1636},{7,11,1387},{10,11,205},{11,11,755},{141,11,271},{132, +0,689},{4,10,282},{7,10,1034},{9,0,820},{11,10,398},{11,10,634},{12,10,1},{12,10 +,79},{12,10,544},{14,10,237},{17,10,10},{146,10,20},{4,0,108},{7,0,804},{139,0, +498},{132,11,887},{6,0,1119},{135,11,620},{6,11,165},{138,11,388},{5,0,244},{5, +10,499},{6,10,476},{7,10,600},{7,10,888},{135,10,1096},{140,0,609},{135,0,1005}, +{4,0,412},{133,0,581},{4,11,719},{135,11,155},{7,10,296},{7,10,596},{8,10,560},{ +8,10,586},{9,10,612},{11,10,304},{12,10,46},{13,10,89},{14,10,112},{145,10,122}, +{4,0,895},{133,0,772},{142,11,307},{135,0,1898},{4,0,926},{133,0,983},{4,11,353} +,{6,11,146},{6,11,1789},{7,11,288},{7,11,990},{7,11,1348},{9,11,665},{9,11,898}, +{11,11,893},{142,11,212},{132,0,538},{133,11,532},{6,0,294},{7,0,1267},{8,0,624} +,{141,0,496},{4,11,45},{7,0,1325},{135,11,1257},{138,0,301},{7,10,1599},{7,10, +1723},{8,10,79},{8,10,106},{8,10,190},{8,10,302},{8,10,383},{8,10,713},{9,0,298} +,{9,10,119},{9,10,233},{9,10,419},{9,10,471},{10,10,181},{10,10,406},{11,10,57}, +{11,10,85},{11,10,120},{11,10,177},{11,10,296},{11,10,382},{11,10,454},{11,10, +758},{11,10,999},{12,0,291},{12,10,27},{12,10,131},{12,10,245},{12,10,312},{12, +10,446},{12,10,454},{13,0,276},{13,10,98},{13,10,426},{13,10,508},{14,0,6},{14, +10,163},{14,10,272},{14,10,277},{14,10,370},{15,10,95},{15,10,138},{15,10,167},{ +17,0,18},{17,10,38},{20,10,96},{149,0,32},{132,0,757},{134,0,1263},{4,0,820},{ +134,10,1759},{133,0,722},{136,11,816},{138,10,372},{145,10,16},{134,0,1039},{4,0 +,991},{134,0,2028},{133,10,258},{7,0,1875},{139,0,124},{6,11,559},{6,11,1691},{ +135,11,586},{5,0,324},{7,0,881},{8,10,134},{9,10,788},{140,10,438},{7,11,1823},{ +139,11,693},{6,0,1348},{134,0,1545},{134,0,911},{132,0,954},{7,10,1948},{7,10, +2004},{8,0,329},{136,0,414},{5,0,517},{6,10,439},{7,10,780},{135,10,1040},{132,0 +,816},{5,10,1},{6,10,81},{138,10,520},{5,10,482},{8,10,98},{9,0,713},{10,0,222}, +{10,10,700},{10,10,822},{11,10,302},{11,10,778},{12,10,50},{12,10,127},{12,10, +396},{13,10,62},{13,10,328},{14,10,122},{147,10,72},{137,0,33},{5,10,2},{7,10, +1494},{136,10,589},{6,10,512},{7,10,797},{8,10,253},{9,10,77},{10,10,1},{10,10, +129},{10,10,225},{10,11,108},{11,10,118},{11,10,226},{11,10,251},{11,10,430},{11 +,10,701},{11,10,974},{11,10,982},{11,11,116},{12,10,64},{12,10,260},{12,10,488}, +{140,10,690},{134,11,456},{133,11,925},{5,0,150},{7,0,106},{7,0,774},{8,0,603},{ +9,0,593},{9,0,634},{10,0,44},{10,0,173},{11,0,462},{11,0,515},{13,0,216},{13,0, +288},{142,0,400},{137,10,347},{5,0,748},{134,0,553},{12,0,108},{141,0,291},{4,10 +,12},{7,0,420},{7,10,522},{7,10,809},{8,10,797},{141,10,88},{6,11,193},{7,11,240 +},{7,11,1682},{10,11,51},{10,11,640},{11,11,410},{13,11,82},{14,11,247},{14,11, +331},{142,11,377},{133,10,528},{135,0,1777},{4,0,493},{144,0,55},{136,11,633},{ +139,0,81},{6,0,980},{136,0,321},{148,10,109},{5,10,266},{9,10,290},{9,10,364},{ +10,10,293},{11,10,606},{142,10,45},{6,0,568},{7,0,112},{7,0,1804},{8,0,362},{8,0 +,410},{8,0,830},{9,0,514},{11,0,649},{142,0,157},{4,0,74},{6,0,510},{6,10,594},{ +9,10,121},{10,10,49},{10,10,412},{139,10,834},{134,0,838},{136,10,748},{132,10, +466},{132,0,625},{135,11,1443},{4,11,237},{135,11,514},{9,10,378},{141,10,162},{ +6,0,16},{6,0,158},{7,0,43},{7,0,129},{7,0,181},{8,0,276},{8,0,377},{10,0,523},{ +11,0,816},{12,0,455},{13,0,303},{142,0,135},{135,0,281},{4,0,1},{7,0,1143},{7,0, +1463},{8,0,61},{9,0,207},{9,0,390},{9,0,467},{139,0,836},{6,11,392},{7,11,65},{ +135,11,2019},{132,10,667},{4,0,723},{5,0,895},{7,0,1031},{8,0,199},{8,0,340},{9, +0,153},{9,0,215},{10,0,21},{10,0,59},{10,0,80},{10,0,224},{10,0,838},{11,0,229}, +{11,0,652},{12,0,192},{13,0,146},{142,0,91},{132,0,295},{137,0,51},{9,11,222},{ +10,11,43},{139,11,900},{5,0,309},{140,0,211},{5,0,125},{8,0,77},{138,0,15},{136, +11,604},{138,0,789},{4,10,39},{5,0,173},{7,10,1843},{8,10,407},{11,10,144},{140, +10,523},{138,11,265},{133,0,439},{132,10,510},{7,0,648},{7,0,874},{7,10,1980},{ +10,10,487},{10,10,809},{11,0,164},{12,0,76},{146,0,9},{12,0,111},{13,10,260},{14 +,0,294},{18,10,63},{147,0,45},{133,11,549},{134,10,570},{4,0,8},{7,0,1152},{7,0, +1153},{7,0,1715},{9,0,374},{10,0,478},{139,0,648},{135,0,1099},{5,0,575},{6,0, +354},{135,0,701},{7,11,36},{8,11,201},{136,11,605},{4,10,787},{136,11,156},{6,0, +518},{149,11,13},{140,11,224},{134,0,702},{132,10,516},{5,11,724},{10,11,305},{ +11,11,151},{12,11,33},{12,11,121},{12,11,381},{17,11,3},{17,11,27},{17,11,78},{ +18,11,18},{19,11,54},{149,11,5},{4,11,523},{5,11,638},{8,0,87},{11,10,887},{14, +10,365},{142,10,375},{138,0,438},{136,10,821},{135,11,1908},{6,11,242},{7,11,227 +},{7,11,1581},{8,11,104},{9,11,113},{9,11,220},{9,11,427},{10,11,74},{10,11,239} +,{11,11,579},{11,11,1023},{13,11,4},{13,11,204},{13,11,316},{18,11,95},{148,11, +86},{4,0,69},{5,0,122},{5,0,849},{6,0,1633},{9,0,656},{138,0,464},{4,10,10},{7,0 +,1802},{139,10,786},{135,11,861},{139,0,499},{7,0,476},{7,0,1592},{138,0,87},{ +133,10,684},{4,0,840},{134,10,27},{142,0,283},{6,0,1620},{7,11,1328},{136,11,494 +},{5,0,859},{7,0,1160},{8,0,107},{9,0,291},{9,0,439},{10,0,663},{11,0,609},{140, +0,197},{7,11,1306},{8,11,505},{9,11,482},{10,11,126},{11,11,225},{12,11,347},{12 +,11,449},{13,11,19},{142,11,218},{5,11,268},{10,11,764},{12,11,120},{13,11,39},{ +145,11,127},{145,10,56},{7,11,1672},{10,11,472},{11,11,189},{143,11,51},{6,10, +342},{6,10,496},{8,10,275},{137,10,206},{133,0,600},{4,0,117},{6,0,372},{7,0, +1905},{142,0,323},{4,10,909},{5,10,940},{135,11,1471},{132,10,891},{4,0,722},{ +139,0,471},{4,11,384},{135,11,1022},{132,10,687},{9,0,5},{12,0,216},{12,0,294},{ +12,0,298},{12,0,400},{12,0,518},{13,0,229},{143,0,139},{135,11,1703},{7,11,1602} +,{10,11,698},{12,11,212},{141,11,307},{6,10,41},{141,10,160},{135,11,1077},{9,11 +,159},{11,11,28},{140,11,603},{4,0,514},{7,0,1304},{138,0,477},{134,0,1774},{9,0 +,88},{139,0,270},{5,0,12},{6,10,1718},{7,0,375},{137,0,438},{132,11,515},{136,10 +,778},{8,11,632},{8,11,697},{137,11,854},{6,0,362},{6,0,997},{146,0,51},{7,0,816 +},{7,0,1241},{9,0,283},{9,0,520},{10,0,213},{10,0,307},{10,0,463},{10,0,671},{10 +,0,746},{11,0,401},{11,0,794},{12,0,517},{18,0,107},{147,0,115},{133,10,115},{ +150,11,28},{4,11,136},{133,11,551},{142,10,314},{132,0,258},{6,0,22},{7,0,903},{ +7,0,1963},{8,0,639},{138,0,577},{5,0,681},{5,10,193},{8,0,782},{12,10,178},{13,0 +,130},{145,0,84},{9,11,17},{138,11,291},{7,11,1287},{9,11,44},{10,11,552},{10,11 +,642},{11,11,839},{12,11,274},{12,11,275},{12,11,372},{13,11,91},{142,11,125},{ +135,10,174},{4,0,664},{5,0,804},{139,0,1013},{134,0,942},{6,0,1349},{6,0,1353},{ +6,0,1450},{7,11,1518},{139,11,694},{4,10,122},{5,10,796},{5,10,952},{6,10,1660}, +{6,10,1671},{8,10,567},{9,10,687},{9,10,742},{10,10,686},{11,0,356},{11,10,682}, +{140,10,281},{5,0,32},{6,11,147},{7,11,886},{9,11,753},{138,11,268},{5,10,179},{ +7,10,1095},{135,10,1213},{4,10,66},{7,10,722},{135,10,904},{135,10,352},{9,11, +245},{138,11,137},{4,0,289},{5,11,414},{7,0,629},{7,0,1698},{7,0,1711},{140,0, +215},{6,0,1975},{135,11,1762},{6,0,450},{136,0,109},{141,10,35},{134,11,599},{ +136,0,705},{133,0,664},{134,11,1749},{11,11,402},{12,11,109},{12,11,431},{13,11, +179},{13,11,206},{14,11,175},{14,11,217},{16,11,3},{148,11,53},{135,0,1238},{134 +,11,1627},{132,11,488},{10,10,592},{10,10,753},{12,10,317},{12,10,355},{12,10, +465},{12,10,469},{12,10,560},{12,10,578},{141,0,318},{133,10,564},{132,11,83},{ +140,11,676},{6,0,1872},{6,0,1906},{6,0,1907},{9,0,934},{9,0,956},{9,0,960},{9,0, +996},{12,0,794},{12,0,876},{12,0,880},{12,0,918},{15,0,230},{18,0,234},{18,0,238 +},{21,0,38},{149,0,62},{134,10,556},{134,11,278},{137,0,103},{7,10,544},{8,10, +719},{138,10,61},{4,10,5},{5,10,498},{8,10,637},{137,10,521},{7,0,777},{12,0,229 +},{12,0,239},{12,11,229},{12,11,239},{15,0,12},{143,11,12},{6,0,26},{7,11,388},{ +7,11,644},{139,11,781},{7,11,229},{8,11,59},{9,11,190},{9,11,257},{10,11,378},{ +140,11,191},{133,10,927},{135,10,1441},{4,10,893},{5,10,780},{133,10,893},{4,0, +414},{5,0,467},{9,0,654},{10,0,451},{12,0,59},{141,0,375},{142,0,173},{135,0,17} +,{5,10,238},{135,0,1350},{135,0,955},{4,0,960},{10,0,887},{12,0,753},{18,0,161}, +{18,0,162},{152,0,19},{136,11,344},{6,10,1729},{137,11,288},{132,11,660},{4,0, +217},{4,10,60},{5,0,710},{7,0,760},{7,0,1926},{7,10,1800},{8,10,314},{9,0,428},{ +9,0,708},{9,10,700},{10,0,254},{10,0,296},{10,0,720},{11,0,109},{11,0,255},{11, +10,487},{12,0,165},{12,0,315},{13,0,107},{13,0,203},{14,0,54},{14,0,99},{14,0, +114},{14,0,388},{16,0,85},{17,0,9},{17,0,33},{20,0,25},{20,0,28},{20,0,29},{21,0 +,9},{21,0,10},{21,0,34},{150,0,17},{7,11,1035},{138,11,737},{7,11,690},{9,11,217 +},{9,11,587},{140,11,521},{6,0,919},{7,11,706},{7,11,1058},{138,11,538},{7,10, +1853},{138,10,437},{136,10,419},{6,0,280},{10,0,502},{11,0,344},{140,0,38},{5,0, +45},{7,0,1161},{11,0,448},{11,0,880},{13,0,139},{13,0,407},{15,0,16},{17,0,95},{ +18,0,66},{18,0,88},{18,0,123},{149,0,7},{11,11,92},{11,11,196},{11,11,409},{11, +11,450},{11,11,666},{11,11,777},{12,11,262},{13,11,385},{13,11,393},{15,11,115}, +{16,11,45},{145,11,82},{136,0,777},{134,11,1744},{4,0,410},{5,10,828},{135,0,521 +},{134,0,673},{7,0,1110},{7,0,1778},{7,10,176},{135,10,178},{5,10,806},{7,10, +1976},{7,11,268},{136,11,569},{4,11,733},{9,11,194},{10,11,92},{11,11,198},{12, +11,84},{12,11,87},{13,11,128},{144,11,74},{4,10,51},{5,0,341},{6,10,4},{7,0,1129 +},{7,10,591},{7,10,849},{7,10,951},{7,10,1613},{7,10,1760},{7,10,1988},{9,10,434 +},{10,10,754},{11,0,414},{11,10,25},{139,10,37},{133,10,902},{135,10,928},{135,0 +,787},{132,0,436},{134,10,270},{7,0,1587},{135,0,1707},{6,0,377},{7,0,1025},{9,0 +,613},{145,0,104},{7,11,982},{7,11,1361},{10,11,32},{143,11,56},{139,0,96},{132, +0,451},{132,10,416},{142,10,372},{5,10,152},{5,10,197},{7,10,340},{7,10,867},{7, +11,306},{10,10,548},{10,10,581},{11,10,6},{12,10,3},{12,10,19},{14,10,110},{142, +10,289},{134,0,680},{134,11,609},{7,0,483},{7,10,190},{8,10,28},{8,10,141},{8,10 +,444},{8,10,811},{9,10,468},{11,10,334},{12,10,24},{12,10,386},{140,10,576},{5, +10,757},{138,0,916},{5,10,721},{135,10,1553},{133,11,178},{134,0,937},{132,10, +898},{133,0,739},{147,0,82},{135,0,663},{146,0,128},{5,10,277},{141,10,247},{134 +,0,1087},{132,10,435},{6,11,381},{7,11,645},{7,11,694},{136,11,546},{7,0,503},{ +135,0,1885},{6,0,1965},{8,0,925},{138,0,955},{4,0,113},{5,0,163},{5,0,735},{7,0, +1009},{9,0,9},{9,0,771},{12,0,90},{13,0,138},{13,0,410},{143,0,128},{4,0,324},{ +138,0,104},{5,10,265},{6,10,212},{135,0,460},{133,11,105},{7,11,261},{7,11,1107} +,{7,11,1115},{7,11,1354},{7,11,1588},{7,11,1705},{7,11,1902},{9,11,465},{10,11, +248},{10,11,349},{10,11,647},{11,11,527},{11,11,660},{11,11,669},{12,11,529},{ +141,11,305},{5,11,438},{9,11,694},{12,11,627},{141,11,210},{152,11,11},{4,0,935} +,{133,0,823},{132,10,702},{5,0,269},{5,10,808},{7,0,434},{7,0,891},{7,10,2045},{ +8,0,339},{9,0,702},{11,0,594},{11,0,718},{145,0,100},{7,0,1014},{9,0,485},{141,0 +,264},{134,0,1713},{7,0,1810},{11,0,866},{12,0,103},{12,11,233},{141,0,495},{4,0 +,423},{10,0,949},{138,0,1013},{135,0,900},{8,11,25},{138,11,826},{5,10,166},{8, +10,739},{140,10,511},{134,0,2018},{7,11,1270},{139,11,612},{4,10,119},{5,10,170} +,{5,10,447},{7,10,1708},{7,10,1889},{9,10,357},{9,10,719},{12,10,486},{140,10, +596},{12,0,574},{140,11,574},{132,11,308},{6,0,964},{6,0,1206},{134,0,1302},{4, +10,450},{135,10,1158},{135,11,150},{136,11,649},{14,0,213},{148,0,38},{9,11,45}, +{9,11,311},{141,11,42},{134,11,521},{7,10,1375},{7,10,1466},{138,10,331},{132,10 +,754},{5,11,339},{7,11,1442},{14,11,3},{15,11,41},{147,11,66},{136,11,378},{134, +0,1022},{5,10,850},{136,10,799},{142,0,143},{135,0,2029},{134,11,1628},{8,0,523} +,{150,0,34},{5,0,625},{135,0,1617},{7,0,275},{7,10,238},{7,10,2033},{8,10,120},{ +8,10,188},{8,10,659},{9,10,598},{10,10,466},{12,10,342},{12,10,588},{13,10,503}, +{14,10,246},{143,10,92},{7,0,37},{8,0,425},{8,0,693},{9,0,720},{10,0,380},{10,0, +638},{11,0,273},{11,0,473},{12,0,61},{143,0,43},{135,11,829},{135,0,1943},{132,0 +,765},{5,11,486},{135,11,1349},{7,11,1635},{8,11,17},{10,11,217},{138,11,295},{4 +,10,201},{7,10,1744},{8,10,602},{11,10,247},{11,10,826},{145,10,65},{138,11,558} +,{11,0,551},{142,0,159},{8,10,164},{146,10,62},{139,11,176},{132,0,168},{136,0, +1010},{134,0,1994},{135,0,91},{138,0,532},{135,10,1243},{135,0,1884},{132,10,907 +},{5,10,100},{10,10,329},{12,10,416},{149,10,29},{134,11,447},{132,10,176},{5,10 +,636},{5,10,998},{7,10,9},{7,10,1508},{8,10,26},{9,10,317},{9,10,358},{10,10,210 +},{10,10,292},{10,10,533},{11,10,555},{12,10,526},{12,10,607},{13,10,263},{13,10 +,459},{142,10,271},{4,11,609},{135,11,756},{6,0,15},{7,0,70},{10,0,240},{147,0, +93},{4,11,930},{133,11,947},{134,0,1227},{134,0,1534},{133,11,939},{133,11,962}, +{5,11,651},{8,11,170},{9,11,61},{9,11,63},{10,11,23},{10,11,37},{10,11,834},{11, +11,4},{11,11,187},{11,11,281},{11,11,503},{11,11,677},{12,11,96},{12,11,130},{12 +,11,244},{14,11,5},{14,11,40},{14,11,162},{14,11,202},{146,11,133},{4,11,406},{5 +,11,579},{12,11,492},{150,11,15},{139,0,392},{6,10,610},{10,10,127},{141,10,27}, +{7,0,655},{7,0,1844},{136,10,119},{4,0,145},{6,0,176},{7,0,395},{137,0,562},{132 +,0,501},{140,11,145},{136,0,1019},{134,0,509},{139,0,267},{6,11,17},{7,11,16},{7 +,11,1001},{7,11,1982},{9,11,886},{10,11,489},{10,11,800},{11,11,782},{12,11,320} +,{13,11,467},{14,11,145},{14,11,387},{143,11,119},{145,11,17},{5,11,458},{134,0, +1099},{7,11,1983},{8,11,0},{8,11,171},{9,11,120},{9,11,732},{10,11,473},{11,11, +656},{11,11,998},{18,11,0},{18,11,2},{147,11,21},{12,11,427},{146,11,38},{10,0, +948},{138,0,968},{7,10,126},{136,10,84},{136,10,790},{4,0,114},{9,0,492},{13,0, +462},{142,0,215},{6,10,64},{12,10,377},{141,10,309},{4,0,77},{5,0,361},{6,0,139} +,{6,0,401},{6,0,404},{7,0,413},{7,0,715},{7,0,1716},{11,0,279},{12,0,179},{12,0, +258},{13,0,244},{142,0,358},{134,0,1717},{7,0,772},{7,0,1061},{7,0,1647},{7,10, +1104},{8,0,82},{11,0,250},{11,0,607},{11,10,269},{11,10,539},{11,10,627},{11,10, +706},{11,10,975},{12,0,311},{12,0,420},{12,10,248},{12,10,434},{12,10,600},{12, +10,622},{13,0,184},{13,0,367},{13,10,297},{13,10,485},{14,10,69},{14,10,409},{ +143,10,108},{135,0,724},{4,11,512},{4,11,519},{133,11,342},{134,0,1133},{145,11, +29},{11,10,977},{141,10,507},{6,0,841},{6,0,1042},{6,0,1194},{10,0,993},{140,0, +1021},{6,11,31},{7,11,491},{7,11,530},{8,11,592},{9,10,34},{11,10,484},{11,11,53 +},{11,11,779},{12,11,167},{12,11,411},{14,11,14},{14,11,136},{15,11,72},{16,11, +17},{144,11,72},{4,0,1021},{5,11,907},{134,0,2037},{6,10,1700},{7,0,373},{7,10, +293},{7,10,382},{7,10,1026},{7,10,1087},{7,10,2027},{8,0,335},{8,0,596},{8,10, +252},{8,10,727},{8,10,729},{9,0,488},{9,10,30},{9,10,199},{9,10,231},{9,10,251}, +{9,10,334},{9,10,361},{9,10,712},{10,10,55},{10,10,60},{10,10,232},{10,10,332},{ +10,10,384},{10,10,396},{10,10,504},{10,10,542},{10,10,652},{11,10,20},{11,10,48} +,{11,10,207},{11,10,291},{11,10,298},{11,10,342},{11,10,365},{11,10,394},{11,10, +620},{11,10,705},{11,10,1017},{12,10,123},{12,10,340},{12,10,406},{12,10,643},{ +13,10,61},{13,10,269},{13,10,311},{13,10,319},{13,10,486},{14,10,234},{15,10,62} +,{15,10,85},{16,10,71},{18,10,119},{148,10,105},{150,0,37},{4,11,208},{5,11,106} +,{6,11,531},{8,11,408},{9,11,188},{138,11,572},{132,0,564},{6,0,513},{135,0,1052 +},{132,0,825},{9,0,899},{140,11,441},{134,0,778},{133,11,379},{7,0,1417},{12,0, +382},{17,0,48},{152,0,12},{132,11,241},{6,10,379},{7,0,1116},{7,10,270},{8,10, +176},{8,10,183},{9,10,432},{9,10,661},{12,10,247},{12,10,617},{146,10,125},{5,10 +,792},{133,10,900},{6,0,545},{7,0,565},{7,0,1669},{10,0,114},{11,0,642},{140,0, +618},{133,0,5},{138,11,7},{132,11,259},{135,0,192},{134,0,701},{136,0,763},{135, +10,1979},{4,10,901},{133,10,776},{10,0,755},{147,0,29},{133,0,759},{4,11,173},{5 +,11,312},{5,11,512},{135,11,1285},{7,11,1603},{7,11,1691},{9,11,464},{11,11,195} +,{12,11,279},{12,11,448},{14,11,11},{147,11,102},{7,0,370},{7,0,1007},{7,0,1177} +,{135,0,1565},{135,0,1237},{4,0,87},{5,0,250},{141,0,298},{4,11,452},{5,11,583}, +{5,11,817},{6,11,433},{7,11,593},{7,11,720},{7,11,1378},{8,11,161},{9,11,284},{ +10,11,313},{139,11,886},{4,11,547},{135,11,1409},{136,11,722},{4,10,37},{5,10, +334},{135,10,1253},{132,10,508},{12,0,107},{146,0,31},{8,11,420},{139,11,193},{ +135,0,814},{135,11,409},{140,0,991},{4,0,57},{7,0,1195},{7,0,1438},{7,0,1548},{7 +,0,1835},{7,0,1904},{9,0,757},{10,0,604},{139,0,519},{132,0,540},{138,11,308},{ +132,10,533},{136,0,608},{144,11,65},{4,0,1014},{134,0,2029},{4,0,209},{5,11,1002 +},{7,0,902},{136,11,745},{134,0,2030},{6,0,303},{7,0,335},{7,0,1437},{7,0,1668}, +{8,0,553},{8,0,652},{8,0,656},{9,0,558},{11,0,743},{149,0,18},{5,11,575},{6,11, +354},{135,11,701},{4,11,239},{6,11,477},{7,11,1607},{11,11,68},{139,11,617},{132 +,0,559},{8,0,527},{18,0,60},{147,0,24},{133,10,920},{138,0,511},{133,0,1017},{ +133,0,675},{138,10,391},{7,10,1952},{139,0,156},{138,11,369},{132,11,367},{133,0 +,709},{6,0,698},{134,0,887},{142,10,126},{134,0,1745},{132,10,483},{13,11,299},{ +142,11,75},{133,0,714},{7,0,8},{136,0,206},{138,10,480},{4,11,694},{9,10,495},{ +146,10,104},{7,11,1248},{11,11,621},{139,11,702},{140,11,687},{132,0,776},{139, +10,1009},{135,0,1272},{134,0,1059},{8,10,653},{13,10,93},{147,10,14},{135,11,213 +},{136,0,406},{133,10,172},{132,0,947},{8,0,175},{10,0,168},{138,0,573},{132,0, +870},{6,0,1567},{151,11,28},{134,11,472},{5,10,260},{136,11,132},{4,11,751},{11, +11,390},{140,11,32},{4,11,409},{133,11,78},{6,11,473},{12,0,554},{145,11,105},{ +133,0,784},{8,0,908},{136,11,306},{139,0,882},{6,0,358},{7,0,1393},{7,11,1759},{ +8,0,396},{8,11,396},{10,0,263},{10,11,263},{14,0,154},{14,11,154},{16,0,48},{16, +11,48},{17,0,8},{145,11,8},{13,11,163},{13,11,180},{18,11,78},{148,11,35},{14,0, +32},{18,0,85},{20,0,2},{152,0,16},{7,0,228},{8,10,167},{8,10,375},{9,10,82},{9, +10,561},{10,0,770},{138,10,620},{132,0,845},{9,0,14},{9,0,441},{10,0,306},{139,0 +,9},{8,10,194},{8,10,756},{11,0,966},{12,0,287},{13,0,342},{13,0,402},{15,0,110} +,{143,0,163},{134,0,1578},{4,0,967},{6,0,1820},{6,0,1847},{140,0,716},{136,0,594 +},{7,0,1428},{7,0,1640},{7,0,1867},{7,11,883},{9,0,169},{9,0,182},{9,0,367},{9,0 +,478},{9,0,506},{9,0,551},{9,0,557},{9,0,648},{9,0,697},{9,0,705},{9,0,725},{9,0 +,787},{9,0,794},{10,0,198},{10,0,214},{10,0,267},{10,0,275},{10,0,456},{10,0,551 +},{10,0,561},{10,0,613},{10,0,627},{10,0,668},{10,0,675},{10,0,691},{10,0,695},{ +10,0,707},{10,0,715},{11,0,183},{11,0,201},{11,0,244},{11,0,262},{11,0,352},{11, +0,439},{11,0,493},{11,0,572},{11,0,591},{11,0,608},{11,0,611},{11,0,646},{11,0, +674},{11,0,711},{11,0,751},{11,0,761},{11,0,776},{11,0,785},{11,0,850},{11,0,853 +},{11,0,862},{11,0,865},{11,0,868},{11,0,875},{11,0,898},{11,0,902},{11,0,903},{ +11,0,910},{11,0,932},{11,0,942},{11,0,957},{11,0,967},{11,0,972},{12,0,148},{12, +0,195},{12,0,220},{12,0,237},{12,0,318},{12,0,339},{12,0,393},{12,0,445},{12,0, +450},{12,0,474},{12,0,505},{12,0,509},{12,0,533},{12,0,591},{12,0,594},{12,0,597 +},{12,0,621},{12,0,633},{12,0,642},{13,0,59},{13,0,60},{13,0,145},{13,0,239},{13 +,0,250},{13,0,329},{13,0,344},{13,0,365},{13,0,372},{13,0,387},{13,0,403},{13,0, +414},{13,0,456},{13,0,470},{13,0,478},{13,0,483},{13,0,489},{14,0,55},{14,0,57}, +{14,0,81},{14,0,90},{14,0,148},{14,0,239},{14,0,266},{14,0,321},{14,0,326},{14,0 +,327},{14,0,330},{14,0,347},{14,0,355},{14,0,401},{14,0,404},{14,0,411},{14,0, +414},{14,0,416},{14,0,420},{15,0,61},{15,0,74},{15,0,87},{15,0,88},{15,0,94},{15 +,0,96},{15,0,116},{15,0,149},{15,0,154},{16,0,50},{16,0,63},{16,0,73},{17,0,2},{ +17,0,66},{17,0,92},{17,0,103},{17,0,112},{17,0,120},{18,0,50},{18,0,54},{18,0,82 +},{18,0,86},{18,0,90},{18,0,111},{18,0,115},{18,0,156},{19,0,40},{19,0,79},{20,0 +,78},{149,0,22},{5,0,161},{135,0,839},{4,0,782},{13,11,293},{142,11,56},{133,11, +617},{139,11,50},{135,10,22},{145,0,64},{5,10,639},{7,10,1249},{139,10,896},{138 +,0,998},{135,11,2042},{4,11,546},{142,11,233},{6,0,1043},{134,0,1574},{134,0, +1496},{4,10,102},{7,10,815},{7,10,1699},{139,10,964},{12,0,781},{142,0,461},{4, +11,313},{133,11,577},{6,0,639},{6,0,1114},{137,0,817},{8,11,184},{141,11,433},{7 +,0,1814},{135,11,935},{10,0,997},{140,0,958},{4,0,812},{137,11,625},{132,10,899} +,{136,10,795},{5,11,886},{6,11,46},{6,11,1790},{7,11,14},{7,11,732},{7,11,1654}, +{8,11,95},{8,11,327},{8,11,616},{10,11,598},{10,11,769},{11,11,134},{11,11,747}, +{12,11,378},{142,11,97},{136,0,139},{6,10,52},{9,10,104},{9,10,559},{12,10,308}, +{147,10,87},{133,11,1021},{132,10,604},{132,10,301},{136,10,779},{7,0,643},{136, +0,236},{132,11,153},{134,0,1172},{147,10,32},{133,11,798},{4,11,587},{134,0,1338 +},{6,11,598},{7,11,42},{8,11,695},{10,11,212},{11,11,158},{14,11,196},{145,11,85 +},{135,10,508},{5,11,957},{5,11,1008},{135,11,249},{4,11,129},{135,11,465},{5,0, +54},{7,11,470},{7,11,1057},{7,11,1201},{9,11,755},{11,11,906},{140,11,527},{7,11 +,908},{146,11,7},{5,11,148},{136,11,450},{144,11,1},{4,0,256},{135,0,1488},{6,10 +,310},{7,10,1849},{8,10,72},{8,10,272},{8,10,431},{9,0,351},{9,10,12},{10,10,563 +},{10,10,630},{10,10,796},{10,10,810},{11,10,367},{11,10,599},{11,10,686},{140, +10,672},{6,0,1885},{6,0,1898},{6,0,1899},{140,0,955},{4,0,714},{133,0,469},{6,0, +1270},{134,0,1456},{132,0,744},{6,0,313},{7,10,537},{8,10,64},{9,10,127},{10,10, +496},{12,10,510},{141,10,384},{4,10,244},{4,11,217},{5,11,710},{7,10,233},{7,11, +1926},{9,11,428},{9,11,708},{10,11,254},{10,11,296},{10,11,720},{11,11,109},{11, +11,255},{12,11,165},{12,11,315},{13,11,107},{13,11,203},{14,11,54},{14,11,99},{ +14,11,114},{14,11,388},{16,11,85},{17,11,9},{17,11,33},{20,11,25},{20,11,28},{20 +,11,29},{21,11,9},{21,11,10},{21,11,34},{150,11,17},{138,0,402},{7,0,969},{146,0 +,55},{8,0,50},{137,0,624},{134,0,1355},{132,0,572},{134,10,1650},{10,10,702},{ +139,10,245},{10,0,847},{142,0,445},{6,0,43},{7,0,38},{8,0,248},{138,0,513},{133, +0,369},{137,10,338},{133,0,766},{133,0,363},{133,10,896},{8,11,392},{11,11,54},{ +13,11,173},{13,11,294},{148,11,7},{134,0,678},{7,11,1230},{136,11,531},{6,0,258} +,{140,0,409},{5,0,249},{148,0,82},{7,10,1117},{136,10,539},{5,0,393},{6,0,378},{ +7,0,1981},{9,0,32},{9,0,591},{10,0,685},{10,0,741},{142,0,382},{133,0,788},{134, +0,1281},{134,0,1295},{7,0,1968},{141,0,509},{4,0,61},{5,0,58},{5,0,171},{5,0,683 +},{6,0,291},{6,0,566},{7,0,1650},{11,0,523},{12,0,273},{12,0,303},{15,0,39},{143 +,0,111},{6,0,706},{134,0,1283},{134,0,589},{135,11,1433},{133,11,435},{5,10,4},{ +5,10,810},{6,10,13},{6,10,538},{6,10,1690},{6,10,1726},{7,0,1059},{7,10,1819},{8 +,10,148},{8,10,696},{8,10,791},{12,10,125},{13,0,54},{143,10,9},{135,10,1268},{5 +,11,85},{6,11,419},{7,11,134},{7,11,305},{7,11,361},{7,11,1337},{8,11,71},{140, +11,519},{137,0,824},{140,11,688},{5,11,691},{7,10,1385},{7,11,345},{9,11,94},{11 +,10,582},{11,10,650},{11,10,901},{11,10,949},{12,10,232},{12,10,236},{12,11,169} +,{13,10,413},{13,10,501},{146,10,116},{4,0,917},{133,0,1005},{5,11,183},{6,11, +582},{7,0,1598},{9,11,344},{10,11,679},{140,11,435},{4,10,925},{5,10,803},{8,10, +698},{138,10,828},{132,0,919},{135,11,511},{139,10,992},{4,0,255},{5,0,302},{6,0 +,132},{7,0,128},{7,0,283},{7,0,1299},{10,0,52},{10,0,514},{11,0,925},{13,0,92},{ +142,0,309},{134,0,1369},{135,10,1847},{134,0,328},{7,11,1993},{136,11,684},{133, +10,383},{137,0,173},{134,11,583},{134,0,1411},{5,11,704},{8,11,357},{10,11,745}, +{14,11,426},{17,11,94},{19,0,65},{147,11,57},{9,10,660},{138,10,347},{4,11,179}, +{5,11,198},{133,11,697},{7,11,347},{7,11,971},{8,11,181},{138,11,711},{141,0,442 +},{7,10,572},{9,10,592},{11,0,842},{11,0,924},{11,10,680},{12,10,356},{12,10,550 +},{13,0,317},{13,0,370},{13,0,469},{13,0,471},{14,0,397},{18,0,69},{146,0,145},{ +14,11,19},{14,11,28},{144,11,29},{136,0,534},{4,11,243},{5,11,203},{7,11,19},{7, +11,71},{7,11,113},{10,11,405},{11,11,357},{142,11,240},{6,0,210},{10,0,845},{138 +,0,862},{7,11,1351},{9,11,581},{10,11,639},{11,11,453},{140,11,584},{7,11,1450}, +{139,11,99},{10,0,892},{12,0,719},{144,0,105},{4,0,284},{6,0,223},{134,11,492},{ +5,11,134},{6,11,408},{6,11,495},{135,11,1593},{136,0,529},{137,0,807},{4,0,218}, +{7,0,526},{143,0,137},{6,0,1444},{142,11,4},{132,11,665},{4,0,270},{4,11,248},{5 +,0,192},{6,0,332},{7,0,1322},{7,11,137},{137,11,349},{140,0,661},{7,0,1517},{7, +10,748},{11,0,597},{11,10,700},{14,0,76},{14,0,335},{148,0,33},{5,11,371},{135, +11,563},{146,11,57},{133,10,127},{133,0,418},{4,11,374},{7,11,547},{7,11,1700},{ +7,11,1833},{139,11,858},{6,10,198},{140,10,83},{7,11,1812},{13,11,259},{13,11, +356},{14,11,242},{147,11,114},{5,10,276},{6,10,55},{7,0,379},{7,10,1369},{8,0, +481},{137,0,377},{138,11,286},{5,0,1003},{6,0,149},{6,10,1752},{136,10,726},{6, +11,1647},{7,11,1552},{7,11,2010},{8,0,262},{9,0,627},{9,11,494},{9,11,509},{10,0 +,18},{11,0,214},{11,0,404},{11,0,457},{11,0,780},{11,0,913},{13,0,401},{142,0, +200},{135,0,742},{136,0,304},{132,0,142},{133,10,764},{6,10,309},{7,10,331},{138 +,10,550},{135,10,1062},{6,11,123},{7,10,986},{7,11,214},{9,11,728},{10,11,157},{ +11,11,346},{11,11,662},{143,11,106},{135,10,1573},{7,0,925},{137,0,799},{4,0,471 +},{5,0,51},{6,0,602},{8,0,484},{138,0,195},{136,0,688},{132,0,697},{6,0,1169},{6 +,0,1241},{6,10,194},{7,10,133},{10,10,493},{10,10,570},{139,10,664},{140,0,751}, +{5,10,24},{5,10,569},{6,10,3},{6,10,119},{6,10,143},{6,10,440},{7,0,929},{7,10, +599},{7,10,1686},{7,10,1854},{8,10,424},{9,10,43},{9,10,584},{9,10,760},{10,0, +452},{10,10,328},{11,0,878},{11,10,159},{11,10,253},{12,10,487},{12,10,531},{144 +,0,33},{4,11,707},{13,11,106},{18,11,49},{147,11,41},{5,0,221},{5,11,588},{134, +11,393},{134,0,1437},{6,11,211},{7,11,1690},{11,11,486},{140,11,369},{5,10,14},{ +5,10,892},{6,10,283},{7,10,234},{136,10,537},{4,0,988},{136,0,955},{135,0,1251}, +{4,10,126},{8,10,635},{147,10,34},{4,10,316},{135,10,1561},{137,10,861},{4,10,64 +},{5,10,352},{5,10,720},{6,10,368},{139,10,359},{134,0,192},{4,0,132},{5,0,69},{ +135,0,1242},{7,10,1577},{10,10,304},{10,10,549},{12,10,365},{13,10,220},{13,10, +240},{142,10,33},{4,0,111},{6,11,219},{135,0,865},{5,11,582},{6,11,1646},{7,11, +99},{7,11,1962},{7,11,1986},{8,11,515},{8,11,773},{9,11,23},{9,11,491},{12,11, +620},{14,11,52},{145,11,50},{132,0,767},{7,11,568},{148,11,21},{5,11,851},{6,0, +42},{7,0,1416},{7,0,2005},{8,0,131},{8,0,466},{9,0,672},{13,0,252},{148,0,103},{ +135,0,1050},{6,10,175},{137,10,289},{5,10,432},{133,10,913},{6,0,44},{136,0,368} +,{135,11,784},{132,0,570},{133,0,120},{139,10,595},{140,0,29},{6,0,227},{135,0, +1589},{4,11,98},{7,11,1365},{9,11,422},{9,11,670},{10,11,775},{11,11,210},{13,11 +,26},{13,11,457},{141,11,476},{140,10,80},{5,10,931},{134,10,1698},{133,0,522},{ +134,0,1120},{135,0,1529},{12,0,739},{14,0,448},{142,0,467},{11,10,526},{11,10, +939},{141,10,290},{5,10,774},{6,10,1637},{6,10,1686},{134,10,1751},{6,0,1667},{ +135,0,2036},{7,10,1167},{11,10,934},{13,10,391},{145,10,76},{137,11,147},{6,10, +260},{7,10,1484},{11,11,821},{12,11,110},{12,11,153},{18,11,41},{150,11,19},{6,0 +,511},{6,10,573},{140,0,132},{5,0,568},{6,0,138},{135,0,1293},{132,0,1020},{8,0, +258},{9,0,208},{137,0,359},{4,0,565},{8,0,23},{136,0,827},{134,0,344},{4,0,922}, +{5,0,1023},{13,11,477},{14,11,120},{148,11,61},{134,0,240},{5,11,209},{6,11,30}, +{11,11,56},{139,11,305},{4,10,292},{4,10,736},{5,10,871},{6,0,171},{6,10,1689},{ +7,0,1002},{7,0,1324},{7,10,1944},{9,0,415},{9,10,580},{14,0,230},{146,0,68},{9, +11,635},{139,11,559},{4,11,150},{5,11,303},{134,11,327},{6,10,63},{135,10,920},{ +133,10,793},{8,11,192},{10,11,78},{10,11,555},{11,11,308},{13,11,359},{147,11,95 +},{135,11,786},{135,11,1712},{136,0,402},{6,0,754},{6,11,1638},{7,11,79},{7,11, +496},{9,11,138},{10,11,336},{11,11,12},{12,11,412},{12,11,440},{142,11,305},{4,0 +,716},{141,0,31},{133,0,982},{5,10,67},{6,10,62},{6,10,374},{7,10,1391},{8,0,691 +},{136,0,731},{9,10,790},{140,10,47},{139,11,556},{151,11,1},{7,11,204},{7,11, +415},{8,11,42},{10,11,85},{11,11,33},{11,11,564},{12,11,571},{149,11,1},{7,11, +610},{7,11,1501},{136,0,888},{4,10,391},{135,10,1169},{5,0,847},{9,0,840},{138,0 +,803},{137,0,823},{134,0,785},{8,0,152},{9,0,53},{9,0,268},{9,0,901},{10,0,518}, +{10,0,829},{11,0,188},{13,0,74},{14,0,46},{15,0,17},{15,0,33},{17,0,40},{18,0,36 +},{19,0,20},{22,0,1},{152,0,2},{4,11,3},{5,11,247},{5,11,644},{7,11,744},{7,11, +1207},{7,11,1225},{7,11,1909},{146,11,147},{136,0,532},{135,0,681},{132,10,271}, +{140,0,314},{140,0,677},{4,0,684},{136,0,384},{5,11,285},{9,11,67},{13,11,473},{ +143,11,82},{4,10,253},{5,10,544},{7,10,300},{137,10,340},{7,0,110},{7,0,447},{8, +0,290},{8,0,591},{9,0,382},{9,0,649},{11,0,71},{11,0,155},{11,0,313},{12,0,5},{ +13,0,325},{142,0,287},{134,0,1818},{136,0,1007},{138,0,321},{7,0,360},{7,0,425}, +{9,0,66},{9,0,278},{138,0,644},{133,10,818},{5,0,385},{5,10,541},{6,10,94},{6,10 +,499},{7,10,230},{139,10,321},{4,10,920},{5,10,25},{5,10,790},{6,10,457},{7,10, +853},{136,10,788},{4,0,900},{133,0,861},{5,0,254},{7,0,985},{136,0,73},{7,0,1959 +},{136,0,683},{134,10,1765},{133,10,822},{132,10,634},{4,11,29},{6,11,532},{7,11 +,1628},{7,11,1648},{9,11,303},{9,11,350},{10,11,433},{11,11,97},{11,11,557},{11, +11,745},{12,11,289},{12,11,335},{12,11,348},{12,11,606},{13,11,116},{13,11,233}, +{13,11,466},{14,11,181},{14,11,209},{14,11,232},{14,11,236},{14,11,300},{16,11, +41},{148,11,97},{6,10,36},{7,10,658},{8,10,454},{147,0,86},{135,11,1692},{132,0, +725},{5,11,501},{7,11,1704},{9,11,553},{11,11,520},{12,11,557},{141,11,249},{134 +,0,196},{133,0,831},{136,0,723},{7,0,1897},{13,0,80},{13,0,437},{145,0,74},{4,0, +992},{6,0,627},{136,0,994},{135,11,1294},{132,10,104},{5,0,848},{6,0,66},{136,0, +764},{4,0,36},{7,0,1387},{10,0,205},{139,0,755},{6,0,1046},{134,0,1485},{134,0, +950},{132,0,887},{14,0,450},{148,0,111},{7,0,620},{7,0,831},{9,10,542},{9,10,566 +},{138,10,728},{6,0,165},{138,0,388},{139,10,263},{4,0,719},{135,0,155},{138,10, +468},{6,11,453},{144,11,36},{134,11,129},{5,0,533},{7,0,755},{138,0,780},{134,0, +1465},{4,0,353},{6,0,146},{6,0,1789},{7,0,427},{7,0,990},{7,0,1348},{9,0,665},{9 +,0,898},{11,0,893},{142,0,212},{7,10,87},{142,10,288},{4,0,45},{135,0,1257},{7, +10,988},{7,10,1939},{9,10,64},{9,10,502},{12,0,7},{12,10,34},{13,10,12},{13,10, +234},{147,10,77},{4,0,607},{5,11,60},{6,11,504},{7,11,614},{7,11,1155},{140,11,0 +},{135,10,141},{8,11,198},{11,11,29},{140,11,534},{140,0,65},{136,0,816},{132,10 +,619},{139,0,88},{5,10,246},{8,10,189},{9,10,355},{9,10,512},{10,10,124},{10,10, +453},{11,10,143},{11,10,416},{11,10,859},{141,10,341},{4,11,379},{135,11,1397},{ +4,0,600},{137,0,621},{133,0,367},{134,0,561},{6,0,559},{134,0,1691},{6,0,585},{ +134,11,585},{135,11,1228},{4,11,118},{5,10,678},{6,11,274},{6,11,361},{7,11,75}, +{141,11,441},{135,11,1818},{137,11,841},{5,0,573},{6,0,287},{7,10,862},{7,10, +1886},{138,10,179},{132,10,517},{140,11,693},{5,11,314},{6,11,221},{7,11,419},{ +10,11,650},{11,11,396},{12,11,156},{13,11,369},{14,11,333},{145,11,47},{140,10, +540},{136,10,667},{11,10,403},{146,10,83},{5,10,761},{134,0,672},{9,0,157},{10, +10,131},{140,10,72},{6,11,460},{135,0,714},{134,0,456},{133,0,925},{5,11,682},{ +135,11,1887},{136,11,510},{136,11,475},{133,11,1016},{7,11,602},{8,11,179},{9,0, +19},{10,11,781},{140,11,126},{6,11,329},{138,11,111},{6,0,822},{134,0,1473},{144 +,11,86},{11,0,113},{139,11,113},{5,11,821},{134,11,1687},{133,10,449},{7,0,463}, +{8,10,103},{145,0,69},{7,10,2028},{138,10,641},{6,0,193},{7,0,240},{7,0,1682},{ +10,0,51},{10,0,640},{11,0,410},{13,0,82},{14,0,247},{14,0,331},{142,0,377},{6,0, +471},{11,0,411},{142,0,2},{5,11,71},{7,11,1407},{9,11,388},{9,11,704},{10,11,261 +},{10,11,619},{11,11,547},{11,11,619},{143,11,157},{136,0,633},{135,0,1148},{6,0 +,554},{7,0,1392},{7,10,1274},{7,10,1386},{7,11,2008},{9,11,337},{10,11,517},{12, +0,129},{146,10,87},{6,10,187},{7,0,803},{7,10,1203},{8,0,542},{8,10,380},{14,10, +117},{149,10,28},{6,10,297},{7,10,793},{139,10,938},{7,10,464},{8,0,438},{11,0, +363},{11,10,105},{12,10,231},{14,10,386},{15,10,102},{148,10,75},{5,11,16},{6,11 +,86},{6,11,603},{7,11,292},{7,11,561},{8,11,257},{8,11,382},{9,11,721},{9,11,778 +},{11,11,581},{140,11,466},{4,11,486},{5,11,491},{134,0,717},{132,0,875},{132,11 +,72},{6,11,265},{135,11,847},{4,0,237},{135,0,514},{6,0,392},{7,0,65},{135,0, +2019},{140,11,261},{135,11,922},{137,11,404},{7,10,1010},{11,10,733},{11,10,759} +,{12,0,563},{13,10,34},{14,0,101},{18,0,129},{146,10,45},{7,10,1656},{9,10,369}, +{10,10,338},{10,10,490},{11,10,154},{11,10,545},{11,10,775},{13,10,77},{141,10, +274},{4,0,444},{10,0,146},{140,0,9},{139,11,163},{7,0,1260},{135,0,1790},{9,0, +222},{10,0,43},{139,0,900},{137,11,234},{138,0,971},{137,0,761},{134,0,699},{136 +,11,434},{5,10,20},{6,0,1116},{6,10,298},{6,11,197},{7,0,1366},{7,10,659},{8,11, +205},{137,10,219},{132,11,490},{11,11,820},{150,11,51},{7,10,1440},{11,10,854},{ +11,10,872},{11,10,921},{12,10,551},{13,10,472},{142,10,367},{140,11,13},{132,0, +829},{4,10,439},{140,0,242},{136,10,669},{6,0,593},{6,11,452},{7,11,312},{138,11 +,219},{4,11,333},{9,11,176},{12,11,353},{141,11,187},{7,0,36},{8,0,201},{136,0, +605},{140,0,224},{132,10,233},{134,0,1430},{134,0,1806},{4,0,523},{133,0,638},{6 +,0,1889},{9,0,958},{9,0,971},{9,0,976},{12,0,796},{12,0,799},{12,0,808},{12,0, +835},{12,0,836},{12,0,914},{12,0,946},{15,0,216},{15,0,232},{18,0,183},{18,0,187 +},{18,0,194},{18,0,212},{18,0,232},{149,0,49},{132,10,482},{6,0,827},{134,0,1434 +},{135,10,346},{134,0,2043},{6,0,242},{7,0,227},{7,0,1581},{8,0,104},{9,0,113},{ +9,0,220},{9,0,427},{10,0,136},{10,0,239},{11,0,579},{11,0,1023},{13,0,4},{13,0, +204},{13,0,316},{148,0,86},{134,11,1685},{7,0,148},{8,0,284},{141,0,63},{142,0, +10},{135,11,584},{134,0,1249},{7,0,861},{135,10,334},{5,10,795},{6,10,1741},{137 +,11,70},{132,0,807},{7,11,135},{8,11,7},{8,11,62},{9,11,243},{10,11,658},{10,11, +697},{11,11,456},{139,11,756},{9,11,395},{138,11,79},{137,11,108},{147,0,94},{ +136,0,494},{135,11,631},{135,10,622},{7,0,1510},{135,10,1750},{4,10,203},{135,10 +,1936},{7,11,406},{7,11,459},{8,11,606},{139,11,726},{7,0,1306},{8,0,505},{9,0, +482},{10,0,126},{11,0,225},{12,0,347},{12,0,449},{13,0,19},{14,0,218},{142,0,435 +},{5,0,268},{10,0,764},{12,0,120},{13,0,39},{145,0,127},{142,11,68},{11,10,678}, +{140,10,307},{12,11,268},{12,11,640},{142,11,119},{135,10,2044},{133,11,612},{4, +11,372},{7,11,482},{8,11,158},{9,11,602},{9,11,615},{10,11,245},{10,11,678},{10, +11,744},{11,11,248},{139,11,806},{7,10,311},{9,10,308},{140,10,255},{4,0,384},{ +135,0,1022},{5,11,854},{135,11,1991},{135,10,1266},{4,10,400},{5,10,267},{135,10 +,232},{135,0,1703},{9,0,159},{11,0,661},{140,0,603},{4,0,964},{9,11,106},{9,11, +163},{9,11,296},{10,11,167},{10,11,172},{10,11,777},{11,11,16},{14,0,438},{14,0, +444},{14,0,456},{22,0,60},{150,0,63},{136,0,583},{132,0,515},{8,0,632},{8,0,697} +,{137,0,854},{5,11,195},{135,11,1685},{6,0,1123},{134,0,1365},{134,11,328},{7,11 +,1997},{8,11,730},{139,11,1006},{4,0,136},{133,0,551},{134,0,1782},{7,0,1287},{9 +,0,44},{10,0,552},{10,0,642},{11,0,839},{12,0,274},{12,0,275},{12,0,372},{13,0, +91},{142,0,125},{5,11,751},{11,11,797},{140,11,203},{133,0,732},{4,10,100},{7,0, +679},{7,11,821},{136,0,313},{10,0,361},{142,0,316},{134,0,595},{6,0,147},{7,0, +886},{9,0,753},{138,0,268},{5,10,362},{5,10,443},{6,10,318},{7,10,1019},{139,10, +623},{5,10,463},{136,10,296},{4,10,454},{5,11,950},{5,11,994},{134,11,351},{138, +0,137},{5,10,48},{5,10,404},{6,10,557},{7,10,458},{8,10,597},{10,10,455},{10,10, +606},{11,10,49},{11,10,548},{12,10,476},{13,10,18},{141,10,450},{133,0,414},{135 +,0,1762},{5,11,421},{135,11,47},{5,10,442},{135,10,1984},{134,0,599},{134,0,1749 +},{134,0,1627},{4,0,488},{132,11,350},{137,11,751},{132,0,83},{140,0,676},{133, +11,967},{5,10,55},{7,0,1639},{140,10,161},{4,11,473},{7,11,623},{8,11,808},{9,11 +,871},{9,11,893},{11,11,38},{11,11,431},{12,11,112},{12,11,217},{12,11,243},{12, +11,562},{12,11,683},{13,11,141},{13,11,197},{13,11,227},{13,11,406},{13,11,487}, +{14,11,156},{14,11,203},{14,11,224},{14,11,256},{18,11,58},{150,11,0},{133,10, +450},{7,11,736},{139,11,264},{134,0,278},{4,11,222},{7,11,286},{136,11,629},{135 +,10,869},{140,0,97},{144,0,14},{134,0,1085},{4,10,213},{7,10,223},{136,10,80},{7 +,0,388},{7,0,644},{139,0,781},{132,0,849},{7,0,229},{8,0,59},{9,0,190},{10,0,378 +},{140,0,191},{7,10,381},{7,10,806},{7,10,820},{8,10,354},{8,10,437},{8,10,787}, +{9,10,657},{10,10,58},{10,10,339},{10,10,749},{11,10,914},{12,10,162},{13,10,75} +,{14,10,106},{14,10,198},{14,10,320},{14,10,413},{146,10,43},{141,11,306},{136, +10,747},{134,0,1115},{8,11,146},{16,0,94},{144,0,108},{6,0,700},{6,0,817},{134,0 +,1002},{133,10,692},{4,11,465},{135,11,1663},{134,10,191},{6,0,1414},{135,11,913 +},{132,0,660},{7,0,1035},{138,0,737},{6,10,162},{7,10,1960},{136,10,831},{132,10 +,706},{7,0,690},{9,0,217},{9,0,587},{140,0,521},{138,10,426},{135,10,1235},{6,11 +,82},{7,11,138},{7,11,517},{9,11,673},{139,11,238},{138,0,272},{5,11,495},{7,11, +834},{9,11,733},{139,11,378},{134,0,1744},{132,0,1011},{7,11,828},{142,11,116},{ +4,0,733},{5,11,559},{9,0,194},{10,0,92},{11,0,198},{12,0,84},{141,0,128},{6,11, +21},{6,11,1737},{7,11,1444},{8,11,224},{10,0,57},{138,0,277},{4,10,204},{137,10, +902},{136,10,833},{7,10,366},{9,10,287},{11,0,348},{12,0,99},{12,10,199},{12,10, +556},{12,10,577},{18,0,1},{18,0,11},{147,0,4},{6,0,1981},{136,0,936},{21,0,33},{ +150,0,40},{5,11,519},{138,11,204},{5,10,356},{135,10,224},{134,0,775},{135,0,306 +},{7,10,630},{9,10,567},{11,10,150},{11,10,444},{141,10,119},{5,0,979},{134,10, +539},{133,0,611},{4,11,402},{135,11,1679},{5,0,178},{7,11,2},{8,11,323},{136,11, +479},{5,11,59},{135,11,672},{4,0,1010},{6,0,1969},{138,11,237},{133,11,412},{146 +,11,34},{7,11,1740},{146,11,48},{134,0,664},{139,10,814},{4,11,85},{135,11,549}, +{133,11,94},{133,11,457},{132,0,390},{134,0,1510},{4,10,235},{135,10,255},{4,10, +194},{5,10,584},{6,10,384},{6,11,11},{7,10,583},{7,11,187},{10,10,761},{11,10, +760},{139,10,851},{4,11,522},{139,11,802},{135,0,493},{10,11,776},{13,11,345},{ +142,11,425},{146,0,37},{4,11,52},{135,11,661},{134,0,724},{134,0,829},{133,11, +520},{133,10,562},{4,11,281},{5,11,38},{7,11,194},{7,11,668},{7,11,1893},{137,11 +,397},{5,10,191},{137,10,271},{7,0,1537},{14,0,96},{143,0,73},{4,10,470},{5,0, +473},{6,10,153},{7,10,1503},{7,10,1923},{10,10,701},{11,0,168},{11,10,132},{11, +10,227},{11,10,320},{11,10,436},{11,10,525},{11,10,855},{12,10,41},{12,10,286},{ +13,10,103},{13,10,284},{14,10,255},{14,10,262},{15,10,117},{143,10,127},{133,0, +105},{5,0,438},{9,0,694},{12,0,627},{141,0,210},{133,10,327},{6,10,552},{7,10, +1754},{137,10,604},{134,0,1256},{152,0,11},{5,11,448},{11,11,98},{139,11,524},{5 +,10,80},{6,10,405},{7,0,1626},{7,10,403},{7,10,1502},{8,10,456},{9,10,487},{9,10 +,853},{9,10,889},{10,10,309},{11,10,721},{11,10,994},{12,10,430},{13,10,165},{14 +,11,16},{146,11,44},{132,0,779},{8,0,25},{138,0,826},{4,10,453},{5,10,887},{6,10 +,535},{8,10,6},{8,10,543},{136,10,826},{137,11,461},{140,11,632},{132,0,308},{ +135,0,741},{132,0,671},{7,0,150},{8,0,649},{136,0,1020},{6,11,336},{8,11,552},{9 +,0,99},{9,11,285},{10,11,99},{139,11,568},{134,0,521},{5,0,339},{14,0,3},{15,0, +41},{15,0,166},{147,0,66},{6,11,423},{7,11,665},{7,11,1210},{9,11,218},{141,11, +222},{5,10,101},{5,11,256},{6,0,543},{6,10,88},{7,10,1677},{9,10,100},{10,10,677 +},{14,10,169},{14,10,302},{14,10,313},{15,10,48},{143,10,84},{4,10,310},{7,10, +708},{7,10,996},{9,10,795},{10,10,390},{10,10,733},{11,10,451},{12,10,249},{14, +10,115},{14,10,286},{143,10,100},{133,10,587},{13,11,417},{14,11,129},{143,11,15 +},{134,0,1358},{136,11,554},{132,10,498},{7,10,217},{8,10,140},{138,10,610},{135 +,11,989},{135,11,634},{6,0,155},{140,0,234},{135,11,462},{132,11,618},{134,0, +1628},{132,0,766},{4,11,339},{5,10,905},{135,11,259},{135,0,829},{4,11,759},{141 +,11,169},{4,10,456},{7,0,1445},{7,10,358},{7,10,1637},{8,10,643},{139,10,483},{5 +,0,486},{135,0,1349},{5,11,688},{135,11,712},{7,0,1635},{8,0,17},{10,0,217},{10, +0,295},{12,0,2},{140,11,2},{138,0,558},{150,10,56},{4,11,278},{5,11,465},{135,11 +,1367},{136,11,482},{133,10,535},{6,0,1362},{6,0,1461},{10,11,274},{10,11,625},{ +139,11,530},{5,0,599},{5,11,336},{6,11,341},{6,11,478},{6,11,1763},{136,11,386}, +{7,10,1748},{137,11,151},{134,0,1376},{133,10,539},{135,11,73},{135,11,1971},{ +139,11,283},{9,0,93},{139,0,474},{6,10,91},{135,10,435},{5,11,396},{6,0,447},{ +134,11,501},{4,10,16},{5,10,316},{5,10,842},{6,10,370},{6,10,1778},{8,10,166},{ +11,10,812},{12,10,206},{12,10,351},{14,10,418},{16,10,15},{16,10,34},{18,10,3},{ +19,10,3},{19,10,7},{20,10,4},{149,10,21},{7,0,577},{7,0,1432},{9,0,475},{9,0,505 +},{9,0,526},{9,0,609},{9,0,689},{9,0,726},{9,0,735},{9,0,738},{10,0,556},{10,0, +674},{10,0,684},{11,0,89},{11,0,202},{11,0,272},{11,0,380},{11,0,415},{11,0,505} +,{11,0,537},{11,0,550},{11,0,562},{11,0,640},{11,0,667},{11,0,688},{11,0,847},{ +11,0,927},{11,0,930},{11,0,940},{12,0,144},{12,0,325},{12,0,329},{12,0,389},{12, +0,403},{12,0,451},{12,0,515},{12,0,604},{12,0,616},{12,0,626},{13,0,66},{13,0, +131},{13,0,167},{13,0,236},{13,0,368},{13,0,411},{13,0,434},{13,0,453},{13,0,461 +},{13,0,474},{14,0,59},{14,0,60},{14,0,139},{14,0,152},{14,0,276},{14,0,353},{14 +,0,402},{15,0,28},{15,0,81},{15,0,123},{15,0,152},{18,0,136},{148,0,88},{4,11, +929},{133,11,799},{136,11,46},{142,0,307},{4,0,609},{7,0,756},{9,0,544},{11,0, +413},{144,0,25},{7,10,619},{10,0,687},{10,10,547},{11,10,122},{140,10,601},{4,0, +930},{133,0,947},{133,0,939},{142,0,21},{4,11,892},{133,11,770},{133,0,962},{5,0 +,651},{8,0,170},{9,0,61},{9,0,63},{10,0,23},{10,0,37},{10,0,834},{11,0,4},{11,0, +187},{11,0,281},{11,0,503},{11,0,677},{12,0,96},{12,0,130},{12,0,244},{14,0,5},{ +14,0,40},{14,0,162},{14,0,202},{146,0,133},{4,0,406},{5,0,579},{12,0,492},{150,0 +,15},{135,11,158},{135,0,597},{132,0,981},{132,10,888},{4,10,149},{138,10,368},{ +132,0,545},{4,10,154},{7,10,1134},{136,10,105},{135,11,2001},{134,0,1558},{4,10, +31},{6,10,429},{7,10,962},{9,10,458},{139,10,691},{132,10,312},{135,10,1642},{6, +0,17},{6,0,1304},{7,0,16},{7,0,1001},{9,0,886},{10,0,489},{10,0,800},{11,0,782}, +{12,0,320},{13,0,467},{14,0,145},{14,0,387},{143,0,119},{135,0,1982},{7,11,1461} +,{12,11,91},{145,0,17},{4,10,236},{132,11,602},{138,0,907},{136,0,110},{5,10,836 +},{5,10,857},{6,10,1680},{7,0,272},{147,0,53},{5,0,458},{7,11,1218},{136,11,303} +,{7,0,1983},{8,0,0},{8,0,171},{9,0,120},{9,0,732},{10,0,473},{10,10,68},{11,0, +656},{11,0,998},{11,10,494},{18,0,0},{18,0,2},{147,0,21},{137,11,662},{4,11,13}, +{5,11,567},{7,11,1498},{9,11,124},{11,11,521},{140,11,405},{4,10,81},{139,10,867 +},{135,11,1006},{7,11,800},{7,11,1783},{138,11,12},{5,10,282},{8,10,650},{9,0, +295},{9,10,907},{138,0,443},{132,11,735},{4,10,775},{4,11,170},{135,11,323},{6,0 +,1844},{10,0,924},{11,11,844},{12,11,104},{140,11,625},{5,11,304},{7,11,1403},{ +140,11,498},{134,0,1232},{4,0,519},{10,0,70},{12,0,26},{14,0,17},{14,0,178},{15, +0,34},{149,0,12},{132,0,993},{4,11,148},{133,11,742},{6,0,31},{7,0,491},{7,0,530 +},{8,0,592},{11,0,53},{11,0,779},{12,0,167},{12,0,411},{14,0,14},{14,0,136},{15, +0,72},{16,0,17},{144,0,72},{133,0,907},{134,0,733},{133,11,111},{4,10,71},{5,10, +376},{7,10,119},{138,10,665},{136,0,55},{8,0,430},{136,11,430},{4,0,208},{5,0, +106},{6,0,531},{8,0,408},{9,0,188},{138,0,572},{11,10,827},{12,0,56},{14,10,34}, +{143,10,148},{134,0,1693},{133,11,444},{132,10,479},{140,0,441},{9,0,449},{10,0, +192},{138,0,740},{134,0,928},{4,0,241},{7,10,607},{136,10,99},{8,11,123},{15,11, +6},{144,11,7},{6,11,285},{8,11,654},{11,11,749},{12,11,190},{12,11,327},{13,11, +120},{13,11,121},{13,11,327},{15,11,47},{146,11,40},{4,10,41},{5,10,74},{7,10, +1627},{11,10,871},{140,10,619},{7,0,1525},{11,10,329},{11,10,965},{12,10,241},{ +14,10,354},{15,10,22},{148,10,63},{132,0,259},{135,11,183},{9,10,209},{137,10, +300},{5,11,937},{135,11,100},{133,10,98},{4,0,173},{5,0,312},{5,0,512},{135,0, +1285},{141,0,185},{7,0,1603},{7,0,1691},{9,0,464},{11,0,195},{12,0,279},{12,0, +448},{14,0,11},{147,0,102},{135,0,1113},{133,10,984},{4,0,452},{5,0,583},{135,0, +720},{4,0,547},{5,0,817},{6,0,433},{7,0,593},{7,0,1378},{8,0,161},{9,0,284},{10, +0,313},{139,0,886},{4,10,182},{6,10,205},{7,10,220},{136,0,722},{150,0,13},{4,10 +,42},{9,10,205},{9,10,786},{138,10,659},{6,0,289},{7,0,1670},{12,0,57},{151,0,4} +,{132,10,635},{14,0,43},{146,0,21},{139,10,533},{135,0,1694},{8,0,420},{139,0, +193},{135,0,409},{132,10,371},{4,10,272},{135,10,836},{5,10,825},{134,10,1640},{ +5,11,251},{5,11,956},{8,11,268},{9,11,214},{146,11,142},{138,0,308},{6,0,1863},{ +141,11,37},{137,10,879},{7,10,317},{135,10,569},{132,11,294},{134,0,790},{5,0, +1002},{136,0,745},{5,11,346},{5,11,711},{136,11,390},{135,0,289},{5,0,504},{9,10 +,307},{139,0,68},{4,0,239},{6,0,477},{7,0,1607},{139,0,617},{149,0,13},{133,0, +609},{133,11,624},{5,11,783},{7,11,1998},{135,11,2047},{133,10,525},{132,0,367}, +{132,11,594},{5,10,493},{134,0,528},{4,10,174},{135,10,911},{8,10,417},{137,10, +782},{132,0,694},{7,0,548},{137,0,58},{4,10,32},{5,10,215},{6,10,269},{7,10,1782 +},{7,10,1892},{10,10,16},{11,10,822},{11,10,954},{141,10,481},{140,0,687},{7,0, +1749},{136,10,477},{132,11,569},{133,10,308},{135,10,1088},{4,0,661},{138,0,1004 +},{5,11,37},{6,11,39},{6,11,451},{7,11,218},{7,11,667},{7,11,1166},{7,11,1687},{ +8,11,662},{144,11,2},{5,10,126},{8,10,297},{9,0,445},{9,10,366},{12,0,53},{12,10 +,374},{141,0,492},{7,10,1551},{139,10,361},{148,0,74},{134,11,508},{135,0,213},{ +132,10,175},{132,10,685},{6,0,760},{6,0,834},{134,0,1248},{7,11,453},{7,11,635}, +{7,11,796},{8,11,331},{9,11,328},{9,11,330},{9,11,865},{10,11,119},{10,11,235},{ +11,11,111},{11,11,129},{11,11,240},{12,11,31},{12,11,66},{12,11,222},{12,11,269} +,{12,11,599},{12,11,689},{13,11,186},{13,11,364},{142,11,345},{7,0,1672},{139,0, +189},{133,10,797},{133,10,565},{6,0,1548},{6,11,98},{7,11,585},{135,11,702},{9,0 +,968},{15,0,192},{149,0,56},{4,10,252},{6,11,37},{7,10,1068},{7,11,299},{7,11, +1666},{8,11,195},{8,11,316},{9,11,178},{9,11,276},{9,11,339},{9,11,536},{10,10, +434},{10,11,102},{10,11,362},{10,11,785},{11,10,228},{11,10,426},{11,11,55},{11, +11,149},{11,11,773},{13,10,231},{13,11,416},{13,11,419},{14,11,38},{14,11,41},{ +14,11,210},{18,10,106},{148,10,87},{4,0,751},{11,0,390},{140,0,32},{4,0,409},{ +133,0,78},{11,11,458},{12,11,15},{140,11,432},{5,10,231},{7,0,1602},{7,10,601},{ +9,10,277},{9,10,674},{10,0,257},{10,0,698},{10,10,178},{10,10,418},{10,10,509},{ +11,0,544},{11,0,585},{11,10,531},{12,0,212},{12,10,113},{12,10,475},{13,0,307},{ +13,10,99},{142,10,428},{6,0,473},{145,0,105},{5,11,645},{6,0,1949},{143,0,156},{ +7,10,1591},{144,10,43},{135,0,1779},{135,10,1683},{4,11,290},{135,11,1356},{134, +0,763},{6,11,70},{7,11,1292},{10,11,762},{139,11,288},{142,0,29},{140,11,428},{7 +,0,883},{7,11,131},{7,11,422},{8,11,210},{140,11,573},{134,0,488},{4,10,399},{5, +10,119},{5,10,494},{7,10,751},{137,10,556},{133,0,617},{132,11,936},{139,0,50},{ +7,0,1518},{139,0,694},{137,0,785},{4,0,546},{135,0,2042},{7,11,716},{13,11,97},{ +141,11,251},{132,11,653},{145,0,22},{134,0,1016},{4,0,313},{133,0,577},{136,11, +657},{8,0,184},{141,0,433},{135,0,935},{6,0,720},{9,0,114},{146,11,80},{7,10,594 +},{7,10,851},{7,10,1858},{9,10,411},{9,10,574},{9,10,666},{9,10,737},{10,10,346} +,{10,10,712},{11,10,246},{11,10,432},{11,10,517},{11,10,647},{11,10,679},{11,10, +727},{12,0,186},{12,0,292},{12,10,304},{12,10,305},{12,10,323},{12,10,483},{12, +10,572},{12,10,593},{12,10,602},{13,10,95},{13,10,101},{13,10,171},{13,10,315},{ +13,10,378},{13,10,425},{13,10,475},{14,0,100},{14,10,63},{14,10,380},{14,10,384} +,{15,10,133},{18,0,70},{18,10,112},{148,10,72},{135,10,1093},{135,11,1836},{132, +10,679},{137,10,203},{11,0,402},{12,0,109},{12,0,431},{13,0,179},{13,0,206},{14, +0,217},{16,0,3},{148,0,53},{7,11,1368},{8,11,232},{8,11,361},{10,11,682},{138,11 +,742},{137,10,714},{5,0,886},{6,0,46},{6,0,1790},{7,0,14},{7,0,732},{7,0,1654},{ +8,0,95},{8,0,327},{8,0,616},{9,0,892},{9,11,534},{10,0,598},{10,0,769},{11,0,134 +},{11,0,747},{12,0,378},{142,0,97},{4,0,969},{136,10,825},{137,11,27},{6,0,727}, +{142,11,12},{133,0,1021},{134,0,1190},{134,11,1657},{5,10,143},{5,10,769},{6,10, +1760},{7,10,682},{7,10,1992},{136,10,736},{132,0,153},{135,11,127},{133,0,798},{ +132,0,587},{6,0,598},{7,0,42},{8,0,695},{10,0,212},{11,0,158},{14,0,196},{145,0, +85},{133,10,860},{6,0,1929},{134,0,1933},{5,0,957},{5,0,1008},{6,10,422},{7,10,0 +},{7,10,1544},{8,11,364},{9,0,577},{11,10,990},{12,0,141},{12,10,453},{13,10,47} +,{141,10,266},{134,0,1319},{4,0,129},{135,0,465},{7,0,470},{7,0,1057},{7,0,1201} +,{9,0,755},{11,0,906},{140,0,527},{7,0,908},{146,0,7},{5,0,148},{136,0,450},{5, +10,515},{137,10,131},{7,10,1605},{11,10,962},{146,10,139},{132,10,646},{134,0, +1166},{4,10,396},{7,10,728},{9,10,117},{13,10,202},{148,10,51},{6,10,121},{6,10, +124},{6,10,357},{7,10,1138},{7,10,1295},{8,10,162},{139,10,655},{14,0,374},{142, +11,374},{138,0,253},{139,0,1003},{5,11,909},{9,11,849},{138,11,805},{133,10,237} +,{7,11,525},{7,11,1579},{8,11,497},{136,11,573},{137,0,46},{132,0,879},{134,0, +806},{135,0,1868},{6,0,1837},{134,0,1846},{6,0,730},{134,0,881},{7,0,965},{7,0, +1460},{7,0,1604},{7,11,193},{7,11,397},{7,11,1105},{8,11,124},{8,11,619},{9,11, +305},{10,11,264},{11,11,40},{12,11,349},{13,11,134},{13,11,295},{14,11,155},{15, +11,120},{146,11,105},{136,0,506},{143,0,10},{4,11,262},{7,10,571},{7,10,1877},{7 +,11,342},{10,10,366},{141,11,23},{133,11,641},{9,10,513},{10,0,22},{10,10,39},{ +12,10,122},{140,10,187},{135,11,1431},{150,11,49},{4,11,99},{6,11,250},{6,11,346 +},{8,11,127},{138,11,81},{6,0,2014},{8,0,928},{10,0,960},{10,0,979},{140,0,996}, +{134,0,296},{132,11,915},{5,11,75},{9,11,517},{10,11,470},{12,11,155},{141,11, +224},{137,10,873},{4,0,854},{140,11,18},{134,0,587},{7,10,107},{7,10,838},{8,10, +550},{138,10,401},{11,0,636},{11,10,588},{11,10,864},{11,10,968},{15,0,145},{15, +10,160},{17,0,34},{19,0,50},{151,0,20},{135,11,216},{7,0,982},{10,0,32},{143,0, +56},{133,10,768},{133,11,954},{6,11,304},{7,11,1114},{8,11,418},{10,11,345},{11, +11,341},{11,11,675},{141,11,40},{9,11,410},{139,11,425},{136,0,941},{4,10,894},{ +133,0,435},{5,0,85},{6,0,419},{7,0,134},{7,0,305},{7,0,361},{7,0,1337},{8,0,71}, +{140,0,519},{140,0,688},{135,0,740},{5,0,691},{7,0,345},{9,0,94},{140,0,169},{5, +0,183},{6,0,582},{10,0,679},{140,0,435},{134,11,14},{6,0,945},{135,0,511},{134, +11,1708},{5,11,113},{6,11,243},{7,11,1865},{11,11,161},{16,11,37},{145,11,99},{ +132,11,274},{137,0,539},{6,10,272},{7,0,1993},{136,0,684},{6,0,659},{134,0,982}, +{4,10,9},{5,10,128},{7,10,368},{11,10,480},{148,10,3},{134,0,583},{132,0,803},{ +133,0,704},{4,0,179},{5,0,198},{133,0,697},{7,0,347},{7,0,971},{7,11,166},{8,0, +181},{138,0,711},{136,10,682},{4,10,2},{7,10,545},{7,10,894},{136,11,521},{135,0 +,481},{132,0,243},{5,0,203},{7,0,19},{7,0,71},{7,0,113},{10,0,405},{11,0,357},{ +142,0,240},{5,11,725},{5,11,727},{135,11,1811},{6,0,826},{137,11,304},{7,0,1450} +,{139,0,99},{133,11,654},{134,0,492},{5,0,134},{6,0,408},{6,0,495},{6,11,273},{7 +,0,1593},{10,11,188},{13,11,377},{146,11,77},{9,10,769},{140,10,185},{135,11,410 +},{142,0,4},{4,0,665},{134,11,1785},{4,0,248},{7,0,137},{137,0,349},{5,10,530},{ +142,10,113},{7,0,1270},{139,0,612},{132,11,780},{5,0,371},{135,0,563},{135,0,826 +},{6,0,1535},{23,0,21},{151,0,23},{4,0,374},{7,0,547},{7,0,1700},{7,0,1833},{139 +,0,858},{133,10,556},{7,11,612},{8,11,545},{8,11,568},{8,11,642},{9,11,717},{10, +11,541},{10,11,763},{11,11,449},{12,11,489},{13,11,153},{13,11,296},{14,11,138}, +{14,11,392},{15,11,50},{16,11,6},{16,11,12},{148,11,9},{9,0,311},{141,0,42},{8, +10,16},{140,10,568},{6,0,1968},{6,0,2027},{138,0,991},{6,0,1647},{7,0,1552},{7,0 +,2010},{9,0,494},{137,0,509},{133,11,948},{6,10,186},{137,10,426},{134,0,769},{ +134,0,642},{132,10,585},{6,0,123},{7,0,214},{9,0,728},{10,0,157},{11,0,346},{11, +0,662},{143,0,106},{142,11,381},{135,0,1435},{4,11,532},{5,11,706},{135,11,662}, +{5,11,837},{134,11,1651},{4,10,93},{5,10,252},{6,10,229},{7,10,291},{9,10,550},{ +139,10,644},{148,0,79},{137,10,749},{134,0,1425},{137,10,162},{4,11,362},{7,11, +52},{7,11,303},{140,11,166},{132,10,381},{4,11,330},{7,11,933},{7,11,2012},{136, +11,292},{135,11,767},{4,0,707},{5,0,588},{6,0,393},{13,0,106},{18,0,49},{147,0, +41},{6,0,211},{7,0,1690},{11,0,486},{140,0,369},{137,11,883},{4,11,703},{135,11, +207},{4,0,187},{5,0,184},{5,0,690},{7,0,1869},{10,0,756},{139,0,783},{132,11,571 +},{134,0,1382},{5,0,175},{6,10,77},{6,10,157},{7,10,974},{7,10,1301},{7,10,1339} +,{7,10,1490},{7,10,1873},{137,10,628},{134,0,1493},{5,11,873},{133,11,960},{134, +0,1007},{12,11,93},{12,11,501},{13,11,362},{14,11,151},{15,11,40},{15,11,59},{16 +,11,46},{17,11,25},{18,11,14},{18,11,134},{19,11,25},{19,11,69},{20,11,16},{20, +11,19},{20,11,66},{21,11,23},{21,11,25},{150,11,42},{11,10,919},{141,10,409},{ +134,0,219},{5,0,582},{6,0,1646},{7,0,99},{7,0,1962},{7,0,1986},{8,0,515},{8,0, +773},{9,0,23},{9,0,491},{12,0,620},{142,0,93},{133,0,851},{5,11,33},{134,11,470} +,{135,11,1291},{134,0,1278},{135,11,1882},{135,10,1489},{132,0,1000},{138,0,982} +,{8,0,762},{8,0,812},{137,0,910},{6,11,47},{7,11,90},{7,11,664},{7,11,830},{7,11 +,1380},{7,11,2025},{8,11,448},{136,11,828},{4,0,98},{4,0,940},{6,0,1819},{6,0, +1834},{6,0,1841},{7,0,1365},{8,0,859},{8,0,897},{8,0,918},{8,10,398},{9,0,422},{ +9,0,670},{9,10,681},{10,0,775},{10,0,894},{10,0,909},{10,0,910},{10,0,935},{11,0 +,210},{11,10,632},{12,0,750},{12,0,755},{13,0,26},{13,0,457},{13,0,476},{16,0, +100},{16,0,109},{18,0,173},{146,0,175},{9,11,417},{137,11,493},{136,10,645},{138 +,0,906},{134,0,1730},{134,10,20},{133,11,1019},{134,0,1185},{8,10,769},{138,0,40 +},{6,11,208},{137,0,147},{140,0,650},{5,0,209},{6,0,30},{11,0,56},{139,0,305},{ +132,0,553},{138,11,344},{6,11,68},{7,11,398},{7,11,448},{7,11,1629},{7,11,1813}, +{8,11,387},{8,11,442},{9,11,710},{10,11,282},{138,11,722},{5,0,597},{14,0,20},{ +142,11,20},{135,0,1614},{135,10,1757},{4,0,150},{5,0,303},{6,0,327},{135,10,937} +,{7,10,1652},{16,0,49},{144,11,49},{8,0,192},{10,0,78},{141,0,359},{135,0,786},{ +143,0,134},{6,0,1638},{7,0,79},{7,0,496},{9,0,138},{10,0,336},{11,0,12},{12,0, +412},{12,0,440},{142,0,305},{136,11,491},{4,10,579},{5,10,226},{5,10,323},{135, +10,960},{7,0,204},{7,0,415},{8,0,42},{10,0,85},{139,0,564},{132,0,614},{4,11,403 +},{5,11,441},{7,11,450},{11,11,101},{12,11,193},{141,11,430},{135,11,1927},{135, +11,1330},{4,0,3},{5,0,247},{5,0,644},{7,0,744},{7,0,1207},{7,0,1225},{7,0,1909}, +{146,0,147},{136,0,942},{4,0,1019},{134,0,2023},{5,10,973},{133,11,679},{5,0,285 +},{9,0,67},{13,0,473},{143,0,82},{7,11,328},{137,11,326},{151,0,8},{6,10,135},{ +135,10,1176},{135,11,1128},{134,0,1309},{135,11,1796},{135,10,314},{4,11,574},{7 +,11,350},{7,11,1024},{8,11,338},{9,11,677},{10,11,808},{139,11,508},{7,11,818},{ +17,11,14},{17,11,45},{18,11,75},{148,11,18},{146,10,4},{135,11,1081},{4,0,29},{6 +,0,532},{7,0,1628},{7,0,1648},{9,0,350},{10,0,433},{11,0,97},{11,0,557},{11,0, +745},{12,0,289},{12,0,335},{12,0,348},{12,0,606},{13,0,116},{13,0,233},{13,0,466 +},{14,0,181},{14,0,209},{14,0,232},{14,0,236},{14,0,300},{16,0,41},{148,0,97},{6 +,10,281},{7,0,318},{8,10,282},{8,10,480},{8,10,499},{9,10,198},{10,10,143},{10, +10,169},{10,10,211},{10,10,417},{10,10,574},{11,10,147},{11,10,395},{12,10,75},{ +12,10,407},{12,10,608},{13,10,500},{142,10,251},{135,11,1676},{135,11,2037},{135 +,0,1692},{5,0,501},{7,0,1704},{9,0,553},{11,0,520},{12,0,557},{141,0,249},{6,0, +1527},{14,0,324},{14,11,324},{15,0,55},{15,0,80},{15,11,55},{143,11,80},{135,10, +1776},{8,0,988},{137,11,297},{132,10,419},{142,0,223},{139,11,234},{7,0,1123},{ +12,0,508},{14,0,102},{14,0,226},{144,0,57},{4,10,138},{7,10,1012},{7,10,1280},{ +137,10,76},{5,10,29},{7,0,1764},{140,10,638},{134,0,2015},{134,0,1599},{138,11, +56},{6,11,306},{7,11,1140},{7,11,1340},{8,11,133},{138,11,449},{139,11,1011},{6, +10,1710},{135,10,2038},{7,11,1763},{140,11,310},{4,10,17},{5,10,23},{6,0,129},{7 +,10,995},{11,10,383},{11,10,437},{12,10,460},{140,10,532},{5,11,329},{136,11,260 +},{133,10,862},{132,0,534},{6,0,811},{135,0,626},{132,11,657},{4,0,25},{5,0,60}, +{6,0,504},{7,0,614},{7,0,1155},{12,0,0},{152,11,7},{7,0,1248},{11,0,621},{139,0, +702},{137,0,321},{8,10,70},{12,10,171},{141,10,272},{10,10,233},{139,10,76},{4,0 +,379},{6,10,442},{135,0,1397},{5,11,66},{7,11,1896},{136,11,288},{134,11,1643},{ +134,10,1709},{4,11,21},{5,11,91},{5,11,570},{5,11,648},{5,11,750},{5,11,781},{6, +11,54},{6,11,112},{6,11,402},{6,11,1732},{7,11,315},{7,11,749},{7,11,1347},{7,11 +,1900},{9,11,78},{9,11,508},{10,11,611},{11,11,510},{11,11,728},{13,11,36},{14, +11,39},{16,11,83},{17,11,124},{148,11,30},{4,0,118},{6,0,274},{6,0,361},{7,0,75} +,{141,0,441},{10,11,322},{10,11,719},{139,11,407},{147,10,119},{12,11,549},{14, +11,67},{147,11,60},{11,10,69},{12,10,105},{12,10,117},{13,10,213},{14,10,13},{14 +,10,62},{14,10,177},{14,10,421},{15,10,19},{146,10,141},{9,0,841},{137,10,309},{ +7,10,608},{7,10,976},{8,11,125},{8,11,369},{8,11,524},{9,10,146},{10,10,206},{10 +,10,596},{10,11,486},{11,11,13},{11,11,381},{11,11,736},{11,11,766},{11,11,845}, +{13,10,218},{13,11,114},{13,11,292},{14,10,153},{142,11,47},{7,11,759},{140,0, +693},{5,0,314},{6,0,221},{7,0,419},{10,0,650},{11,0,396},{12,0,156},{13,0,369},{ +14,0,333},{145,0,47},{6,11,1684},{6,11,1731},{7,11,356},{7,11,1932},{8,11,54},{8 +,11,221},{9,11,225},{9,11,356},{10,11,77},{10,11,446},{10,11,731},{12,11,404},{ +141,11,491},{132,11,375},{4,10,518},{135,10,1136},{4,0,913},{4,11,411},{11,11, +643},{140,11,115},{4,11,80},{133,11,44},{8,10,689},{137,10,863},{138,0,880},{4, +10,18},{7,10,145},{7,10,444},{7,10,1278},{8,10,49},{8,10,400},{9,10,71},{9,10, +250},{10,10,459},{12,10,160},{144,10,24},{136,0,475},{5,0,1016},{5,11,299},{135, +11,1083},{7,0,602},{8,0,179},{10,0,781},{140,0,126},{6,0,329},{138,0,111},{135,0 +,1864},{4,11,219},{7,11,1761},{137,11,86},{6,0,1888},{6,0,1892},{6,0,1901},{6,0, +1904},{9,0,953},{9,0,985},{9,0,991},{9,0,1001},{12,0,818},{12,0,846},{12,0,847}, +{12,0,861},{12,0,862},{12,0,873},{12,0,875},{12,0,877},{12,0,879},{12,0,881},{12 +,0,884},{12,0,903},{12,0,915},{12,0,926},{12,0,939},{15,0,182},{15,0,219},{15,0, +255},{18,0,191},{18,0,209},{18,0,211},{149,0,41},{5,11,328},{135,11,918},{137,0, +780},{12,0,82},{143,0,36},{133,10,1010},{5,0,821},{134,0,1687},{133,11,514},{132 +,0,956},{134,0,1180},{5,10,87},{7,10,313},{7,10,1103},{10,0,112},{10,10,582},{11 +,10,389},{11,10,813},{12,10,385},{13,10,286},{14,10,124},{146,10,108},{5,0,71},{ +7,0,1407},{9,0,704},{10,0,261},{10,0,619},{11,0,547},{11,0,619},{143,0,157},{4,0 +,531},{5,0,455},{5,11,301},{6,11,571},{14,11,49},{146,11,102},{132,10,267},{6,0, +385},{7,0,2008},{9,0,337},{138,0,517},{133,11,726},{133,11,364},{4,11,76},{7,11, +1550},{9,11,306},{9,11,430},{9,11,663},{10,11,683},{11,11,427},{11,11,753},{12, +11,334},{12,11,442},{14,11,258},{14,11,366},{143,11,131},{6,0,1865},{6,0,1879},{ +6,0,1881},{6,0,1894},{6,0,1908},{9,0,915},{9,0,926},{9,0,940},{9,0,943},{9,0,966 +},{9,0,980},{9,0,989},{9,0,1005},{9,0,1010},{12,0,813},{12,0,817},{12,0,840},{12 +,0,843},{12,0,855},{12,0,864},{12,0,871},{12,0,872},{12,0,899},{12,0,905},{12,0, +924},{15,0,171},{15,0,181},{15,0,224},{15,0,235},{15,0,251},{146,0,184},{137,11, +52},{5,0,16},{6,0,86},{6,0,603},{7,0,292},{7,0,561},{8,0,257},{8,0,382},{9,0,721 +},{9,0,778},{11,0,581},{140,0,466},{4,0,486},{5,0,491},{135,10,1121},{4,0,72},{6 +,0,265},{135,0,1300},{135,11,1183},{10,10,249},{139,10,209},{132,10,561},{137,11 +,519},{4,10,760},{4,11,656},{135,11,779},{9,10,154},{140,10,485},{135,11,1793},{ +135,11,144},{136,10,255},{133,0,621},{4,10,368},{135,10,641},{135,11,1373},{7,11 +,554},{7,11,605},{141,11,10},{137,0,234},{5,0,815},{6,0,1688},{134,0,1755},{5,11 +,838},{5,11,841},{134,11,1649},{7,0,1987},{7,0,2040},{136,0,743},{133,11,1012},{ +6,0,197},{136,0,205},{6,0,314},{134,11,314},{144,11,53},{6,11,251},{7,11,365},{7 +,11,1357},{7,11,1497},{8,11,154},{141,11,281},{133,11,340},{6,0,452},{7,0,312},{ +138,0,219},{138,0,589},{4,0,333},{9,0,176},{12,0,353},{141,0,187},{9,10,92},{147 +,10,91},{134,0,1110},{11,0,47},{139,11,495},{6,10,525},{8,10,806},{9,10,876},{ +140,10,284},{8,11,261},{9,11,144},{9,11,466},{10,11,370},{12,11,470},{13,11,144} +,{142,11,348},{137,11,897},{6,11,248},{8,0,863},{8,0,864},{8,0,868},{8,0,884},{9 +,11,546},{10,0,866},{10,0,868},{10,0,873},{10,0,911},{10,0,912},{10,0,944},{10, +11,535},{11,11,681},{12,0,727},{141,11,135},{6,0,300},{135,0,1515},{134,0,1237}, +{139,10,958},{133,10,594},{140,11,250},{134,0,1685},{134,11,567},{7,0,135},{8,0, +7},{8,0,62},{9,0,243},{10,0,658},{10,0,697},{11,0,456},{139,0,756},{9,0,395},{ +138,0,79},{6,10,1641},{136,10,820},{4,10,302},{135,10,1766},{134,11,174},{135,10 +,1313},{135,0,631},{134,10,1674},{134,11,395},{138,0,835},{7,0,406},{7,0,459},{8 +,0,606},{139,0,726},{134,11,617},{134,0,979},{6,10,389},{7,10,149},{9,10,142},{ +138,10,94},{5,11,878},{133,11,972},{6,10,8},{7,10,1881},{8,10,91},{136,11,511},{ +133,0,612},{132,11,351},{4,0,372},{7,0,482},{8,0,158},{9,0,602},{9,0,615},{10,0, +245},{10,0,678},{10,0,744},{11,0,248},{139,0,806},{5,0,854},{135,0,1991},{132,11 +,286},{135,11,344},{7,11,438},{7,11,627},{7,11,1516},{8,11,40},{9,11,56},{9,11, +294},{10,11,30},{10,11,259},{11,11,969},{146,11,148},{135,0,1492},{5,11,259},{7, +11,414},{7,11,854},{142,11,107},{135,10,1746},{6,0,833},{134,0,998},{135,10,24}, +{6,0,750},{135,0,1739},{4,10,503},{135,10,1661},{5,10,130},{7,10,1314},{9,10,610 +},{10,10,718},{11,10,601},{11,10,819},{11,10,946},{140,10,536},{10,10,149},{11, +10,280},{142,10,336},{132,11,738},{135,10,1946},{5,0,195},{135,0,1685},{7,0,1997 +},{8,0,730},{139,0,1006},{151,11,17},{133,11,866},{14,0,463},{14,0,470},{150,0, +61},{4,10,392},{5,0,751},{7,10,1597},{8,0,266},{139,0,578},{5,10,433},{9,10,633} +,{139,10,629},{135,0,821},{6,0,715},{134,0,1325},{133,11,116},{4,11,457},{134,0, +868},{134,0,959},{6,10,234},{138,11,199},{7,0,1053},{7,10,1950},{8,10,680},{11, +10,817},{147,10,88},{7,10,1222},{138,10,386},{5,0,950},{5,0,994},{6,0,351},{134, +0,1124},{134,0,1081},{6,10,5},{7,0,1595},{11,10,249},{12,10,313},{16,10,66},{145 +,10,26},{148,0,59},{5,11,527},{6,11,189},{135,11,859},{5,10,963},{6,10,1773},{11 +,11,104},{11,11,554},{15,11,60},{143,11,125},{135,0,47},{137,0,684},{134,11,116} +,{134,0,1606},{134,0,777},{7,0,1020},{8,10,509},{136,10,792},{135,0,1094},{132,0 +,350},{133,11,487},{4,11,86},{5,11,667},{5,11,753},{6,11,316},{6,11,455},{135,11 +,946},{7,0,1812},{13,0,259},{13,0,356},{14,0,242},{147,0,114},{132,10,931},{133, +0,967},{4,0,473},{7,0,623},{8,0,808},{9,0,871},{9,0,893},{11,0,38},{11,0,431},{ +12,0,112},{12,0,217},{12,0,243},{12,0,562},{12,0,663},{12,0,683},{13,0,141},{13, +0,197},{13,0,227},{13,0,406},{13,0,487},{14,0,156},{14,0,203},{14,0,224},{14,0, +256},{18,0,58},{150,0,0},{138,0,286},{7,10,943},{139,10,614},{135,10,1837},{150, +11,45},{132,0,798},{4,0,222},{7,0,286},{136,0,629},{4,11,79},{7,11,1773},{10,11, +450},{11,11,589},{13,11,332},{13,11,493},{14,11,183},{14,11,334},{14,11,362},{14 +,11,368},{14,11,376},{14,11,379},{19,11,90},{19,11,103},{19,11,127},{148,11,90}, +{4,10,90},{5,0,337},{5,10,545},{7,10,754},{9,10,186},{10,10,72},{10,10,782},{11, +0,513},{11,0,889},{11,0,961},{11,10,577},{11,10,610},{12,0,461},{12,10,354},{12, +10,362},{12,10,595},{13,0,79},{143,0,121},{141,0,306},{136,0,146},{7,0,1646},{9, +10,329},{11,10,254},{141,11,124},{4,0,465},{135,0,1663},{132,0,525},{133,11,663} +,{9,10,187},{10,0,299},{11,10,1016},{17,10,44},{146,0,74},{4,10,506},{7,0,165},{ +7,0,919},{136,10,517},{5,10,295},{135,10,1680},{133,11,846},{134,0,1064},{5,11, +378},{7,11,1402},{7,11,1414},{8,11,465},{9,11,286},{10,11,185},{10,11,562},{10, +11,635},{11,11,31},{11,11,393},{12,11,456},{13,11,312},{18,11,65},{18,11,96},{ +147,11,89},{132,0,596},{7,10,987},{9,10,688},{10,10,522},{11,10,788},{140,10,566 +},{4,11,648},{6,0,82},{6,10,1775},{7,0,138},{7,0,517},{7,0,1741},{139,0,238},{7, +0,1233},{7,10,700},{7,10,940},{8,10,514},{9,10,116},{9,10,535},{10,10,118},{11, +10,107},{11,10,148},{11,10,922},{12,10,254},{12,10,421},{142,10,238},{4,0,962},{ +6,0,1824},{8,0,894},{12,0,708},{12,0,725},{14,0,451},{20,0,94},{22,0,59},{150,0, +62},{5,11,945},{6,11,1656},{6,11,1787},{7,11,167},{8,11,824},{9,11,391},{10,11, +375},{139,11,185},{5,0,495},{7,0,834},{9,0,733},{139,0,378},{4,10,743},{135,11, +1273},{6,0,1204},{7,11,1645},{8,11,352},{137,11,249},{139,10,292},{133,0,559},{ +132,11,152},{7,10,1283},{9,0,499},{9,10,227},{10,0,341},{11,10,325},{11,10,408}, +{14,10,180},{15,0,144},{18,10,47},{147,0,49},{6,0,21},{6,0,1737},{7,0,1444},{136 +,0,224},{133,11,1006},{5,10,81},{7,0,1446},{7,10,146},{7,10,1342},{8,10,53},{8, +10,561},{8,10,694},{8,10,754},{9,0,97},{9,10,115},{9,10,894},{10,10,462},{10,10, +813},{11,10,230},{11,10,657},{11,10,699},{11,10,748},{12,10,119},{12,10,200},{12 +,10,283},{14,10,273},{145,0,15},{5,10,408},{137,10,747},{135,11,431},{135,11,832 +},{6,0,729},{134,0,953},{4,0,727},{5,11,351},{7,11,264},{8,0,565},{136,11,565},{ +134,0,1948},{5,0,519},{5,11,40},{7,11,598},{7,11,1638},{8,11,78},{9,11,166},{9, +11,640},{9,11,685},{9,11,773},{11,11,215},{13,11,65},{14,11,172},{14,11,317},{ +145,11,6},{8,11,60},{9,11,343},{139,11,769},{137,11,455},{134,0,1193},{140,0,790 +},{7,11,1951},{8,11,765},{8,11,772},{140,11,671},{7,11,108},{8,11,219},{8,11,388 +},{9,11,639},{9,11,775},{11,11,275},{140,11,464},{132,11,468},{7,10,30},{8,10,86 +},{8,10,315},{8,10,700},{9,10,576},{9,10,858},{11,10,310},{11,10,888},{11,10,904 +},{12,10,361},{141,10,248},{5,11,15},{6,11,56},{7,11,1758},{8,11,500},{9,11,730} +,{11,11,331},{13,11,150},{142,11,282},{4,0,402},{7,0,2},{8,0,323},{136,0,479},{ +138,10,839},{11,0,580},{142,0,201},{5,0,59},{135,0,672},{137,10,617},{146,0,34}, +{134,11,1886},{4,0,961},{136,0,896},{5,11,205},{6,0,1285},{6,11,438},{137,11,711 +},{134,10,428},{7,10,524},{8,10,169},{8,10,234},{9,10,480},{138,10,646},{148,0, +46},{141,0,479},{133,11,534},{6,0,2019},{134,10,1648},{4,0,85},{7,0,549},{7,10, +1205},{138,10,637},{4,0,663},{5,0,94},{7,11,235},{7,11,1475},{15,11,68},{146,11, +120},{6,11,443},{9,11,237},{9,11,571},{9,11,695},{10,11,139},{11,11,715},{12,11, +417},{141,11,421},{132,0,783},{4,0,682},{8,0,65},{9,10,39},{10,10,166},{11,10, +918},{12,10,635},{20,10,10},{22,10,27},{22,10,43},{150,10,52},{6,0,11},{135,0, +187},{132,0,522},{4,0,52},{135,0,661},{4,0,383},{133,0,520},{135,11,546},{11,0, +343},{142,0,127},{4,11,578},{7,10,157},{7,11,624},{7,11,916},{8,10,279},{10,11, +256},{11,11,87},{139,11,703},{134,10,604},{4,0,281},{5,0,38},{7,0,194},{7,0,668} +,{7,0,1893},{137,0,397},{7,10,945},{11,10,713},{139,10,744},{139,10,1022},{9,0, +635},{139,0,559},{5,11,923},{7,11,490},{12,11,553},{13,11,100},{14,11,118},{143, +11,75},{132,0,975},{132,10,567},{137,10,859},{7,10,1846},{7,11,1846},{8,10,628}, +{136,11,628},{148,0,116},{138,11,750},{14,0,51},{14,11,51},{15,11,7},{148,11,20} +,{132,0,858},{134,0,1075},{4,11,924},{133,10,762},{136,0,535},{133,0,448},{10,10 +,784},{141,10,191},{133,10,298},{7,0,610},{135,0,1501},{7,10,633},{7,10,905},{7, +10,909},{7,10,1538},{9,10,767},{140,10,636},{4,11,265},{7,11,807},{135,11,950},{ +5,11,93},{12,11,267},{144,11,26},{136,0,191},{139,10,301},{135,10,1970},{135,0, +267},{4,0,319},{5,0,699},{138,0,673},{6,0,336},{7,0,92},{7,0,182},{8,0,453},{8,0 +,552},{9,0,204},{9,0,285},{10,0,99},{11,0,568},{11,0,950},{12,0,94},{12,10,644}, +{16,0,20},{16,0,70},{16,10,90},{147,0,55},{6,0,551},{7,0,1308},{7,10,845},{7,11, +994},{8,10,160},{137,10,318},{19,11,1},{19,11,26},{150,11,9},{7,0,1406},{9,0,218 +},{141,0,222},{5,0,256},{138,0,69},{5,11,233},{5,11,320},{6,11,140},{7,11,330},{ +136,11,295},{6,0,1980},{136,0,952},{4,0,833},{137,11,678},{133,11,978},{4,11,905 +},{6,11,1701},{137,11,843},{138,10,735},{136,10,76},{17,0,39},{148,0,36},{18,0, +81},{146,11,81},{14,0,352},{17,0,53},{18,0,146},{18,0,152},{19,0,11},{150,0,54}, +{135,0,634},{138,10,841},{132,0,618},{4,0,339},{4,11,275},{7,0,259},{12,11,376}, +{145,0,73},{132,11,509},{7,11,273},{139,11,377},{4,0,759},{9,10,804},{141,0,169} +,{6,10,96},{135,10,1426},{4,10,651},{133,10,289},{7,0,1075},{8,10,35},{9,10,511} +,{10,10,767},{147,10,118},{6,0,649},{6,0,670},{136,0,482},{5,0,336},{6,0,341},{6 +,0,478},{6,0,1763},{136,0,386},{5,11,802},{7,11,2021},{8,11,805},{14,11,94},{15, +11,65},{16,11,4},{16,11,77},{16,11,80},{145,11,5},{5,11,167},{5,11,899},{6,0, +1035},{6,11,410},{137,11,777},{134,11,1705},{5,0,924},{133,0,969},{132,10,704},{ +135,0,73},{135,11,10},{135,10,1078},{5,11,11},{6,11,117},{6,11,485},{7,11,1133}, +{9,11,582},{9,11,594},{11,11,21},{11,11,818},{12,11,535},{141,11,86},{135,0,1971 +},{4,11,264},{7,11,1067},{8,11,204},{8,11,385},{139,11,953},{6,0,1458},{135,0, +1344},{5,0,396},{134,0,501},{4,10,720},{133,10,306},{4,0,929},{5,0,799},{5,10, +431},{8,0,46},{136,0,740},{7,11,646},{7,11,1730},{11,11,446},{141,11,178},{5,10, +464},{6,10,236},{7,0,276},{7,10,696},{7,10,914},{7,10,1108},{7,10,1448},{9,10,15 +},{9,10,564},{10,10,14},{12,10,565},{13,10,449},{14,10,53},{15,10,13},{16,10,64} +,{145,10,41},{4,0,892},{133,0,770},{6,10,1767},{12,10,194},{145,10,107},{135,0, +158},{5,10,840},{138,11,608},{134,0,1432},{138,11,250},{8,11,794},{9,11,400},{10 +,11,298},{142,11,228},{151,0,25},{7,11,1131},{135,11,1468},{135,0,2001},{9,10, +642},{11,10,236},{142,10,193},{4,10,68},{5,10,634},{6,10,386},{7,10,794},{8,10, +273},{9,10,563},{10,10,105},{10,10,171},{11,10,94},{139,10,354},{136,11,724},{ +132,0,478},{11,11,512},{13,11,205},{19,11,30},{22,11,36},{151,11,19},{7,0,1461}, +{140,0,91},{6,11,190},{7,11,768},{135,11,1170},{4,0,602},{4,10,95},{7,10,416},{8 +,0,211},{139,10,830},{7,10,731},{13,10,20},{143,10,11},{6,0,1068},{135,0,1872},{ +4,0,13},{5,0,567},{7,0,1498},{7,11,1023},{9,0,124},{11,0,521},{140,0,405},{135,0 +,1006},{132,0,735},{138,0,812},{4,0,170},{135,0,323},{6,11,137},{9,11,75},{9,11, +253},{10,11,194},{138,11,444},{5,0,304},{5,10,864},{7,0,1403},{10,10,648},{11,10 +,671},{143,10,46},{135,11,1180},{133,10,928},{4,0,148},{133,0,742},{11,10,986},{ +140,10,682},{133,0,523},{135,11,1743},{7,0,730},{8,10,44},{9,10,884},{10,10,580} +,{11,10,399},{11,10,894},{15,10,122},{18,0,144},{147,0,61},{5,11,760},{7,11,542} +,{8,11,135},{136,11,496},{136,0,981},{133,0,111},{10,0,132},{11,0,191},{11,0,358 +},{139,0,460},{7,11,319},{7,11,355},{7,11,763},{10,11,389},{145,11,43},{134,0, +890},{134,0,1420},{136,11,557},{133,10,518},{133,0,444},{135,0,1787},{135,10, +1852},{8,0,123},{15,0,6},{144,0,7},{6,0,2041},{10,11,38},{139,11,784},{136,0,932 +},{5,0,937},{135,0,100},{4,11,58},{5,11,286},{6,0,995},{6,11,319},{7,11,402},{7, +11,1254},{7,11,1903},{8,11,356},{140,11,408},{4,11,389},{9,11,181},{9,11,255},{ +10,11,8},{10,11,29},{10,11,816},{11,11,311},{11,11,561},{12,11,67},{141,11,181}, +{138,0,255},{4,10,934},{5,0,138},{136,10,610},{4,0,965},{10,0,863},{138,0,898},{ +10,10,804},{138,10,832},{8,10,96},{9,10,36},{10,10,607},{11,10,423},{11,10,442}, +{12,0,631},{12,10,309},{14,10,199},{15,10,90},{145,10,110},{134,0,1394},{4,0,652 +},{8,0,320},{9,10,13},{9,10,398},{9,10,727},{10,10,75},{10,10,184},{10,10,230},{ +10,10,564},{10,10,569},{11,10,973},{12,10,70},{12,10,189},{13,10,57},{13,10,257} +,{22,0,6},{150,0,16},{6,0,897},{134,0,1333},{4,0,692},{133,0,321},{133,11,373},{ +135,0,922},{5,0,619},{133,0,698},{137,10,631},{5,10,345},{135,10,1016},{9,0,957} +,{9,0,1018},{12,0,828},{12,0,844},{12,0,897},{12,0,901},{12,0,943},{15,0,180},{ +18,0,197},{18,0,200},{18,0,213},{18,0,214},{146,0,226},{5,0,917},{134,0,1659},{ +135,0,1100},{134,0,1173},{134,0,1930},{5,0,251},{5,0,956},{8,0,268},{9,0,214},{ +146,0,142},{133,10,673},{137,10,850},{4,10,287},{133,10,1018},{132,11,672},{5,0, +346},{5,0,711},{8,0,390},{11,11,752},{139,11,885},{5,10,34},{10,10,724},{12,10, +444},{13,10,354},{18,10,32},{23,10,24},{23,10,31},{152,10,5},{4,11,710},{134,11, +606},{134,0,744},{134,10,382},{133,11,145},{4,10,329},{7,11,884},{140,11,124},{4 +,11,467},{5,11,405},{134,11,544},{9,10,846},{138,10,827},{133,0,624},{9,11,372}, +{15,11,2},{19,11,10},{147,11,18},{4,11,387},{135,11,1288},{5,0,783},{7,0,1998},{ +135,0,2047},{132,10,906},{136,10,366},{135,11,550},{4,10,123},{4,10,649},{5,10, +605},{7,10,1509},{136,10,36},{134,0,1125},{132,0,594},{133,10,767},{135,11,1227} +,{136,11,467},{4,11,576},{135,11,1263},{4,0,268},{7,0,1534},{135,11,1534},{4,10, +273},{5,10,658},{5,10,995},{5,11,919},{134,11,1673},{133,0,563},{134,10,72},{135 +,10,1345},{4,11,82},{5,11,333},{5,11,904},{6,11,207},{7,11,325},{7,11,1726},{8, +11,101},{10,11,778},{139,11,220},{5,0,37},{5,10,589},{6,0,39},{6,0,451},{7,0,218 +},{7,0,667},{7,0,1166},{7,0,1687},{8,0,662},{144,0,2},{134,0,1332},{133,11,903}, +{134,0,508},{5,10,117},{6,10,514},{6,10,541},{7,10,1164},{7,10,1436},{8,10,220}, +{8,10,648},{10,10,688},{11,10,560},{140,11,147},{6,11,555},{135,11,485},{133,10, +686},{7,0,453},{7,0,635},{7,0,796},{8,0,331},{9,0,330},{9,0,865},{10,0,119},{10, +0,235},{11,0,111},{11,0,129},{11,0,240},{12,0,31},{12,0,66},{12,0,222},{12,0,269 +},{12,0,599},{12,0,684},{12,0,689},{12,0,691},{142,0,345},{135,0,1834},{4,11,705 +},{7,11,615},{138,11,251},{136,11,345},{137,0,527},{6,0,98},{7,0,702},{135,0,991 +},{7,10,196},{10,10,765},{11,0,576},{11,10,347},{11,10,552},{11,10,790},{12,10, +263},{13,10,246},{13,10,270},{13,10,395},{14,0,74},{14,10,176},{14,10,190},{14, +10,398},{14,10,412},{15,10,32},{15,10,63},{16,10,88},{147,10,105},{134,11,90},{ +13,0,84},{141,0,122},{6,0,37},{7,0,299},{7,0,1666},{8,0,195},{8,0,316},{9,0,178} +,{9,0,276},{9,0,339},{9,0,536},{10,0,102},{10,0,362},{10,0,785},{11,0,55},{11,0, +149},{11,0,773},{13,0,416},{13,0,419},{14,0,38},{14,0,41},{142,0,210},{5,10,381} +,{135,10,1792},{7,11,813},{12,11,497},{141,11,56},{7,10,616},{138,10,413},{133,0 +,645},{6,11,125},{135,11,1277},{132,0,290},{6,0,70},{7,0,1292},{10,0,762},{139,0 +,288},{6,10,120},{7,10,1188},{7,10,1710},{8,10,286},{9,10,667},{11,10,592},{139, +10,730},{135,11,1784},{7,0,1315},{135,11,1315},{134,0,1955},{135,10,1146},{7,0, +131},{7,0,422},{8,0,210},{140,0,573},{4,10,352},{135,10,687},{139,0,797},{143,0, +38},{14,0,179},{15,0,151},{150,0,11},{4,10,192},{5,10,49},{6,10,200},{6,10,293}, +{6,10,1696},{135,0,488},{132,0,936},{135,11,703},{6,11,160},{7,11,1106},{9,11, +770},{10,11,618},{11,11,112},{140,11,413},{5,0,453},{134,0,441},{135,0,595},{132 +,10,650},{132,10,147},{6,0,991},{6,0,1182},{12,11,271},{145,11,109},{133,10,934} +,{140,11,221},{132,0,653},{7,0,505},{135,0,523},{134,0,903},{135,11,479},{7,11, +304},{9,11,646},{9,11,862},{10,11,262},{11,11,696},{12,11,208},{15,11,79},{147, +11,108},{146,0,80},{135,11,981},{142,0,432},{132,0,314},{137,11,152},{7,0,1368}, +{8,0,232},{8,0,361},{10,0,682},{138,0,742},{135,11,1586},{4,11,434},{9,0,534},{ +11,11,663},{12,11,210},{13,11,166},{13,11,310},{14,11,373},{147,11,43},{7,11, +1091},{135,11,1765},{6,11,550},{135,11,652},{137,0,27},{142,0,12},{4,10,637},{5, +11,553},{7,11,766},{138,11,824},{7,11,737},{8,11,298},{136,11,452},{7,0,736},{ +139,0,264},{134,0,1657},{133,11,292},{138,11,135},{6,0,844},{134,0,1117},{135,0, +127},{9,10,867},{138,10,837},{6,0,1184},{134,0,1208},{134,0,1294},{136,0,364},{6 +,0,1415},{6,10,170},{7,0,1334},{7,11,393},{8,10,395},{8,10,487},{10,11,603},{11, +0,125},{11,11,206},{141,10,147},{137,11,748},{4,11,912},{137,11,232},{4,10,535}, +{136,10,618},{137,0,792},{7,11,1973},{136,11,716},{135,11,98},{5,0,909},{9,0,849 +},{138,0,805},{4,0,630},{132,0,699},{5,11,733},{14,11,103},{150,10,23},{12,11, +158},{18,11,8},{19,11,62},{20,11,6},{22,11,4},{23,11,2},{151,11,9},{132,0,968},{ +132,10,778},{132,10,46},{5,10,811},{6,10,1679},{6,10,1714},{135,10,2032},{6,0, +1446},{7,10,1458},{9,10,407},{139,10,15},{6,10,34},{7,0,206},{7,0,397},{7,0,621} +,{7,0,640},{7,10,1089},{8,0,124},{8,0,619},{8,10,708},{8,10,721},{9,0,305},{9,0, +643},{9,10,363},{10,0,264},{10,0,628},{11,0,40},{12,0,349},{13,0,134},{13,0,295} +,{14,0,155},{15,0,120},{18,0,105},{148,10,98},{4,0,262},{5,0,641},{135,0,342},{ +137,11,72},{4,0,99},{6,0,250},{6,0,346},{8,0,127},{138,0,81},{132,0,915},{5,0,75 +},{9,0,517},{10,0,470},{12,0,155},{141,0,224},{132,10,462},{11,11,600},{11,11, +670},{141,11,245},{142,0,83},{5,10,73},{6,10,23},{134,10,338},{6,0,1031},{139,11 +,923},{7,11,164},{7,11,1571},{9,11,107},{140,11,225},{134,0,1470},{133,0,954},{6 +,0,304},{8,0,418},{10,0,345},{11,0,341},{139,0,675},{9,0,410},{139,0,425},{4,11, +27},{5,11,484},{5,11,510},{6,11,434},{7,11,1000},{7,11,1098},{8,11,2},{136,11, +200},{134,0,734},{140,11,257},{7,10,725},{8,10,498},{139,10,268},{134,0,1822},{ +135,0,1798},{135,10,773},{132,11,460},{4,11,932},{133,11,891},{134,0,14},{132,10 +,583},{7,10,1462},{8,11,625},{139,10,659},{5,0,113},{5,10,220},{6,0,243},{6,0, +1708},{7,0,1865},{11,0,161},{16,0,37},{145,0,99},{134,11,76},{5,11,461},{135,11, +1925},{140,0,69},{8,11,92},{137,11,221},{139,10,803},{132,10,544},{4,0,274},{134 +,0,922},{132,0,541},{5,0,627},{6,10,437},{6,10,564},{11,10,181},{141,10,183},{ +135,10,1192},{4,11,763},{135,0,166},{133,11,253},{134,0,849},{9,11,73},{10,11, +110},{14,11,185},{145,11,119},{5,11,212},{12,11,35},{141,11,382},{133,0,717},{ +137,0,304},{136,0,600},{133,0,654},{6,0,273},{10,0,188},{13,0,377},{146,0,77},{4 +,10,790},{5,10,273},{134,10,394},{132,0,543},{135,0,410},{11,0,98},{11,0,524},{ +141,0,87},{132,0,941},{135,11,1175},{4,0,250},{6,10,127},{7,0,1612},{7,10,1511}, +{8,10,613},{11,0,186},{12,0,133},{12,10,495},{12,10,586},{12,10,660},{12,10,668} +,{14,10,385},{15,10,118},{17,10,20},{146,10,98},{5,11,816},{134,0,1785},{134,0, +1339},{6,10,230},{7,0,961},{7,0,1085},{7,0,1727},{7,11,1727},{136,0,462},{7,10, +1954},{137,0,636},{132,0,780},{5,11,869},{5,11,968},{6,11,1626},{8,11,734},{136, +11,784},{4,11,542},{6,11,1716},{6,11,1727},{7,11,1082},{7,11,1545},{8,11,56},{8, +11,118},{8,11,412},{8,11,564},{9,11,888},{9,11,908},{10,11,50},{10,11,423},{11, +11,685},{11,11,697},{11,11,933},{12,11,299},{13,11,126},{13,11,136},{13,11,170}, +{141,11,190},{134,11,226},{4,11,232},{9,11,202},{10,11,474},{140,11,433},{137,11 +,500},{5,0,529},{136,10,68},{132,10,654},{4,10,156},{7,10,998},{7,10,1045},{7,10 +,1860},{9,10,48},{9,10,692},{11,10,419},{139,10,602},{6,11,108},{7,0,1276},{7,11 +,1003},{7,11,1181},{8,0,474},{8,11,343},{137,0,652},{7,11,1264},{7,11,1678},{11, +11,945},{12,11,341},{12,11,471},{140,11,569},{134,11,1712},{5,0,948},{12,0,468}, +{19,0,96},{148,0,24},{4,11,133},{7,11,711},{7,11,1298},{7,11,1585},{135,11,1929} +,{6,0,753},{140,0,657},{139,0,941},{6,11,99},{7,11,1808},{145,11,57},{6,11,574}, +{7,11,428},{7,11,1250},{10,11,669},{11,11,485},{11,11,840},{12,11,300},{142,11, +250},{4,0,532},{5,0,706},{135,0,662},{5,0,837},{6,0,1651},{139,0,985},{7,0,1861} +,{9,10,197},{10,10,300},{12,10,473},{13,10,90},{141,10,405},{137,11,252},{6,11, +323},{135,11,1564},{4,0,330},{4,0,863},{7,0,933},{7,0,2012},{7,11,461},{8,0,292} +,{8,11,775},{138,11,435},{132,10,606},{4,11,655},{7,11,850},{17,11,75},{146,11, +137},{135,0,767},{7,10,1978},{136,10,676},{132,0,641},{135,11,1559},{134,0,1233} +,{137,0,242},{4,10,361},{5,10,315},{145,0,114},{137,0,883},{132,10,461},{138,0, +274},{134,0,2008},{134,0,1794},{4,0,703},{135,0,207},{4,10,472},{140,0,285},{132 +,0,571},{5,0,873},{5,0,960},{8,0,823},{8,11,577},{137,0,881},{7,0,617},{10,0,498 +},{11,0,501},{12,0,16},{140,0,150},{138,10,747},{132,0,431},{133,10,155},{7,10, +163},{8,10,319},{9,10,402},{10,10,24},{10,10,681},{11,0,283},{11,0,567},{11,10, +200},{12,10,253},{12,10,410},{142,10,219},{4,11,413},{5,11,677},{8,11,432},{140, +11,280},{5,10,475},{7,10,1780},{9,0,401},{11,10,297},{11,10,558},{14,10,322},{ +147,10,76},{6,0,781},{9,0,134},{10,0,2},{10,0,27},{10,0,333},{11,0,722},{143,0,1 +},{5,0,33},{6,0,470},{139,0,424},{135,0,2006},{7,10,1956},{140,0,783},{136,0,274 +},{135,0,1882},{132,0,794},{135,0,1848},{5,10,944},{134,10,1769},{6,0,47},{7,0, +90},{7,0,664},{7,0,830},{7,0,1380},{7,0,2025},{8,0,448},{136,0,828},{132,10,144} +,{134,0,1199},{4,11,395},{139,11,762},{135,11,1504},{9,0,417},{137,0,493},{9,11, +174},{10,11,164},{11,11,440},{11,11,841},{143,11,98},{134,11,426},{139,11,1002}, +{134,0,295},{134,0,816},{6,10,247},{137,10,555},{133,0,1019},{4,0,620},{5,11,476 +},{10,10,280},{138,10,797},{139,0,464},{5,11,76},{6,11,458},{6,11,497},{7,11,764 +},{7,11,868},{9,11,658},{10,11,594},{11,11,173},{11,11,566},{12,11,20},{12,11, +338},{141,11,200},{134,0,208},{4,11,526},{7,11,1029},{135,11,1054},{132,11,636}, +{6,11,233},{7,11,660},{7,11,1124},{17,11,31},{19,11,22},{151,11,14},{5,10,428},{ +138,0,442},{10,0,930},{140,0,778},{6,0,68},{7,0,448},{7,0,1629},{7,0,1769},{7,0, +1813},{7,10,1717},{8,0,442},{8,0,516},{9,0,710},{10,0,282},{10,0,722},{138,10, +546},{134,0,1128},{11,0,844},{12,0,104},{140,0,625},{4,11,432},{135,11,824},{138 +,10,189},{133,0,787},{133,10,99},{4,11,279},{7,11,301},{137,11,362},{4,10,397},{ +8,0,491},{136,10,555},{4,11,178},{133,11,399},{134,0,711},{144,0,9},{4,0,403},{5 +,0,441},{7,0,450},{10,0,840},{11,0,101},{12,0,193},{141,0,430},{135,11,1246},{12 +,10,398},{20,10,39},{21,10,11},{150,10,41},{4,10,485},{7,10,353},{135,10,1523},{ +6,10,366},{7,10,1384},{7,10,1601},{135,11,1912},{7,0,396},{7,11,396},{138,0,160} +,{137,10,282},{134,11,1692},{4,10,157},{5,10,471},{6,11,202},{10,11,448},{11,11, +208},{12,11,360},{17,11,117},{17,11,118},{18,11,27},{148,11,67},{133,0,679},{137 +,0,326},{136,10,116},{7,11,872},{10,11,516},{139,11,167},{132,11,224},{5,11,546} +,{7,11,35},{8,11,11},{8,11,12},{9,11,315},{9,11,533},{10,11,802},{11,11,166},{12 +,11,525},{142,11,243},{7,0,1128},{135,11,1920},{5,11,241},{8,11,242},{9,11,451}, +{10,11,667},{11,11,598},{140,11,429},{5,10,160},{6,0,737},{7,10,363},{7,10,589}, +{10,10,170},{141,10,55},{135,0,1796},{142,11,254},{4,0,574},{7,0,350},{7,0,1024} +,{8,0,338},{9,0,677},{138,0,808},{134,0,1096},{137,11,516},{4,10,108},{4,11,366} +,{7,0,405},{10,0,491},{139,10,498},{11,11,337},{142,11,303},{134,11,1736},{7,0, +1081},{140,11,364},{7,10,1005},{140,10,609},{4,10,895},{5,10,772},{135,0,1676},{ +135,0,2037},{6,0,1207},{11,11,916},{142,11,419},{14,11,140},{148,11,41},{6,11, +331},{136,11,623},{4,10,926},{5,10,983},{9,0,944},{9,0,969},{9,0,1022},{12,0,913 +},{12,0,936},{15,0,177},{143,0,193},{5,0,354},{135,11,506},{8,0,598},{9,0,664},{ +138,0,441},{4,11,640},{133,11,513},{137,0,297},{132,10,538},{6,10,294},{7,10, +1267},{136,10,624},{7,0,1772},{7,11,1888},{8,11,289},{11,11,45},{12,11,278},{140 +,11,537},{135,10,1325},{138,0,751},{141,0,37},{134,0,1828},{132,10,757},{132,11, +394},{6,0,257},{135,0,1522},{4,0,582},{7,11,1931},{137,0,191},{7,11,574},{7,11, +1719},{137,11,145},{132,11,658},{4,11,369},{138,0,790},{9,11,781},{10,11,144},{ +11,11,385},{13,11,161},{13,11,228},{13,11,268},{148,11,107},{8,0,469},{8,11,374} +,{138,0,47},{6,0,306},{7,0,1140},{7,0,1340},{8,0,133},{138,0,449},{139,0,1011},{ +7,10,1875},{139,10,124},{4,11,344},{6,11,498},{139,11,323},{137,0,299},{132,0, +837},{133,11,906},{5,0,329},{8,0,260},{138,0,10},{134,0,1320},{4,0,657},{146,0, +158},{135,0,1191},{152,0,7},{6,0,1939},{8,0,974},{138,0,996},{135,0,1665},{11,11 +,126},{139,11,287},{143,0,8},{14,11,149},{14,11,399},{143,11,57},{5,0,66},{7,0, +1896},{136,0,288},{5,10,150},{7,0,175},{8,10,603},{9,10,593},{9,10,634},{10,0, +494},{10,10,173},{11,10,462},{11,10,515},{13,10,216},{13,10,288},{142,10,400},{ +134,0,1643},{136,11,21},{4,0,21},{5,0,91},{5,0,648},{5,0,750},{5,0,781},{6,0,54} +,{6,0,112},{6,0,402},{6,0,1732},{7,0,315},{7,0,749},{7,0,1427},{7,0,1900},{9,0, +78},{9,0,508},{10,0,611},{10,0,811},{11,0,510},{11,0,728},{13,0,36},{14,0,39},{ +16,0,83},{17,0,124},{148,0,30},{4,0,668},{136,0,570},{10,0,322},{10,0,719},{139, +0,407},{135,11,1381},{136,11,193},{12,10,108},{141,10,291},{132,11,616},{136,11, +692},{8,0,125},{8,0,369},{8,0,524},{10,0,486},{11,0,13},{11,0,381},{11,0,736},{ +11,0,766},{11,0,845},{13,0,114},{13,0,292},{142,0,47},{134,0,1247},{6,0,1684},{6 +,0,1731},{7,0,356},{8,0,54},{8,0,221},{9,0,225},{9,0,356},{10,0,77},{10,0,446},{ +10,0,731},{12,0,404},{141,0,491},{135,10,1777},{4,10,493},{4,11,305},{144,10,55} +,{4,0,951},{6,0,1809},{6,0,1849},{8,0,846},{8,0,866},{8,0,899},{10,0,896},{12,0, +694},{142,0,468},{5,11,214},{7,11,603},{8,11,611},{9,11,686},{10,11,88},{11,11, +459},{11,11,496},{12,11,463},{12,11,590},{13,11,0},{142,11,214},{132,0,411},{4,0 +,80},{133,0,44},{140,11,74},{143,0,31},{6,10,568},{7,0,669},{7,10,1804},{8,10, +362},{8,10,410},{8,10,830},{9,10,514},{11,10,649},{142,10,157},{6,11,1703},{135, +0,673},{132,10,625},{134,0,1303},{5,0,299},{135,0,1083},{138,0,704},{6,0,275},{6 +,10,158},{7,0,408},{7,10,129},{7,10,181},{8,10,276},{8,10,377},{10,10,523},{11, +10,816},{12,10,455},{13,10,303},{142,10,135},{4,0,219},{7,0,367},{7,0,1713},{7,0 +,1761},{9,0,86},{9,0,537},{10,0,165},{12,0,219},{140,0,561},{4,10,1},{4,11,737}, +{6,11,317},{7,10,1143},{7,10,1463},{8,0,216},{9,10,207},{9,10,390},{9,10,467},{ +10,11,98},{11,10,836},{11,11,294},{12,11,60},{12,11,437},{13,11,64},{13,11,380}, +{142,11,430},{6,11,1758},{8,11,520},{9,11,345},{9,11,403},{142,11,350},{5,11,47} +,{10,11,242},{138,11,579},{5,11,139},{7,11,1168},{138,11,539},{135,0,1319},{4,10 +,295},{4,10,723},{5,10,895},{7,10,1031},{8,10,199},{8,10,340},{9,10,153},{9,10, +215},{10,10,21},{10,10,59},{10,10,80},{10,10,224},{10,10,838},{11,10,229},{11,10 +,652},{12,10,192},{13,10,146},{142,10,91},{140,0,428},{137,10,51},{133,0,514},{5 +,10,309},{140,10,211},{5,10,125},{6,0,1010},{8,10,77},{138,10,15},{4,0,55},{5,0, +301},{6,0,571},{142,0,49},{146,0,102},{136,11,370},{4,11,107},{7,11,613},{8,11, +358},{8,11,439},{8,11,504},{9,11,501},{10,11,383},{139,11,477},{132,11,229},{133 +,0,364},{133,10,439},{4,11,903},{135,11,1816},{11,0,379},{140,10,76},{4,0,76},{4 +,0,971},{7,0,1550},{9,0,306},{9,0,430},{9,0,663},{10,0,683},{10,0,921},{11,0,427 +},{11,0,753},{12,0,334},{12,0,442},{14,0,258},{14,0,366},{143,0,131},{137,0,52}, +{4,11,47},{6,11,373},{7,11,452},{7,11,543},{7,11,1714},{7,11,1856},{9,11,6},{11, +11,257},{139,11,391},{4,10,8},{7,10,1152},{7,10,1153},{7,10,1715},{9,10,374},{10 +,10,478},{139,10,648},{4,11,785},{133,11,368},{135,10,1099},{135,11,860},{5,11, +980},{134,11,1754},{134,0,1258},{6,0,1058},{6,0,1359},{7,11,536},{7,11,1331},{ +136,11,143},{4,0,656},{135,0,779},{136,10,87},{5,11,19},{6,11,533},{146,11,126}, +{7,0,144},{138,10,438},{5,11,395},{5,11,951},{134,11,1776},{135,0,1373},{7,0,554 +},{7,0,605},{141,0,10},{4,10,69},{5,10,122},{9,10,656},{138,10,464},{5,10,849},{ +134,10,1633},{5,0,838},{5,0,841},{134,0,1649},{133,0,1012},{139,10,499},{7,10, +476},{7,10,1592},{138,10,87},{6,0,251},{7,0,365},{7,0,1357},{7,0,1497},{8,0,154} +,{141,0,281},{132,11,441},{132,11,695},{7,11,497},{9,11,387},{147,11,81},{133,0, +340},{14,10,283},{142,11,283},{134,0,810},{135,11,1894},{139,0,495},{5,11,284},{ +6,11,49},{6,11,350},{7,11,1},{7,11,377},{7,11,1693},{8,11,18},{8,11,678},{9,11, +161},{9,11,585},{9,11,671},{9,11,839},{11,11,912},{141,11,427},{5,10,859},{7,10, +1160},{8,10,107},{9,10,291},{9,10,439},{10,10,663},{11,10,609},{140,10,197},{8,0 +,261},{9,0,144},{9,0,466},{10,0,370},{12,0,470},{13,0,144},{142,0,348},{137,0, +897},{6,0,248},{9,0,546},{10,0,535},{11,0,681},{141,0,135},{4,0,358},{135,0,1496 +},{134,0,567},{136,0,445},{4,10,117},{6,10,372},{7,10,1905},{142,10,323},{4,10, +722},{139,10,471},{6,0,697},{134,0,996},{7,11,2007},{9,11,101},{9,11,450},{10,11 +,66},{10,11,842},{11,11,536},{140,11,587},{132,0,577},{134,0,1336},{9,10,5},{12, +10,216},{12,10,294},{12,10,298},{12,10,400},{12,10,518},{13,10,229},{143,10,139} +,{6,0,174},{138,0,917},{134,10,1774},{5,10,12},{7,10,375},{9,10,88},{9,10,438},{ +11,10,270},{139,11,62},{134,11,1766},{6,11,0},{7,10,816},{7,10,1241},{7,11,84},{ +9,10,283},{9,10,520},{10,10,213},{10,10,307},{10,10,463},{10,10,671},{10,10,746} +,{11,10,401},{11,10,794},{11,11,895},{12,10,517},{17,11,11},{18,10,107},{147,10, +115},{5,0,878},{133,0,972},{6,11,1665},{7,11,256},{7,11,1388},{138,11,499},{4,10 +,258},{136,10,639},{4,11,22},{5,11,10},{6,10,22},{7,10,903},{7,10,1963},{7,11, +848},{8,11,97},{138,10,577},{5,10,681},{136,10,782},{133,11,481},{132,0,351},{4, +10,664},{5,10,804},{139,10,1013},{6,11,134},{7,11,437},{7,11,959},{9,11,37},{14, +11,285},{14,11,371},{144,11,60},{7,11,486},{8,11,155},{11,11,93},{140,11,164},{ +132,0,286},{7,0,438},{7,0,627},{7,0,1516},{8,0,40},{9,0,56},{9,0,294},{10,0,30}, +{11,0,969},{11,0,995},{146,0,148},{5,11,591},{135,11,337},{134,0,1950},{133,10, +32},{138,11,500},{5,11,380},{5,11,650},{136,11,310},{4,11,364},{7,11,1156},{7,11 +,1187},{137,11,409},{4,0,738},{134,11,482},{4,11,781},{6,11,487},{7,11,926},{8, +11,263},{139,11,500},{135,11,418},{4,10,289},{6,0,2047},{7,10,629},{7,10,1698},{ +7,10,1711},{10,0,969},{140,10,215},{6,10,450},{136,10,109},{134,0,818},{136,10, +705},{133,0,866},{4,11,94},{135,11,1265},{132,11,417},{134,0,1467},{135,10,1238} +,{4,0,972},{6,0,1851},{134,0,1857},{134,0,355},{133,0,116},{132,0,457},{135,11, +1411},{4,11,408},{4,11,741},{135,11,500},{134,10,26},{142,11,137},{5,0,527},{6,0 +,189},{7,0,859},{136,0,267},{11,0,104},{11,0,554},{15,0,60},{143,0,125},{134,0, +1613},{4,10,414},{5,10,467},{9,10,654},{10,10,451},{12,10,59},{141,10,375},{135, +10,17},{134,0,116},{135,11,541},{135,10,955},{6,11,73},{135,11,177},{133,11,576} +,{134,0,886},{133,0,487},{4,0,86},{5,0,667},{5,0,753},{6,0,316},{6,0,455},{135,0 +,946},{142,11,231},{150,0,45},{134,0,863},{134,0,1953},{6,10,280},{10,10,502},{ +11,10,344},{140,10,38},{4,0,79},{7,0,1773},{10,0,450},{11,0,589},{13,0,332},{13, +0,493},{14,0,183},{14,0,334},{14,0,362},{14,0,368},{14,0,376},{14,0,379},{19,0, +90},{19,0,103},{19,0,127},{148,0,90},{5,10,45},{7,10,1161},{11,10,448},{11,10, +880},{13,10,139},{13,10,407},{15,10,16},{17,10,95},{18,10,66},{18,10,88},{18,10, +123},{149,10,7},{136,10,777},{4,10,410},{135,10,521},{135,10,1778},{135,11,538}, +{142,0,381},{133,11,413},{134,0,1142},{6,0,1189},{136,11,495},{5,0,663},{6,0, +1962},{134,0,2003},{7,11,54},{8,11,312},{10,11,191},{10,11,614},{140,11,567},{ +132,10,436},{133,0,846},{7,10,1587},{7,10,1707},{10,0,528},{139,0,504},{5,0,378} +,{8,0,465},{9,0,286},{10,0,185},{10,0,562},{10,0,635},{11,0,31},{11,0,393},{13,0 +,312},{18,0,65},{18,0,96},{147,0,89},{6,11,468},{7,0,899},{7,11,567},{7,11,1478} +,{8,11,530},{14,0,325},{142,11,290},{7,0,1880},{9,0,680},{139,0,798},{134,0,1770 +},{132,0,648},{150,11,35},{5,0,945},{6,0,1656},{6,0,1787},{7,0,167},{8,0,824},{9 +,0,391},{10,0,375},{139,0,185},{6,11,484},{135,11,822},{134,0,2046},{7,0,1645},{ +8,0,352},{137,0,249},{132,0,152},{6,0,611},{135,0,1733},{6,11,1724},{135,11,2022 +},{133,0,1006},{141,11,96},{5,0,420},{135,0,1449},{146,11,149},{135,0,832},{135, +10,663},{133,0,351},{5,0,40},{7,0,598},{7,0,1638},{8,0,78},{9,0,166},{9,0,640},{ +9,0,685},{9,0,773},{11,0,215},{13,0,65},{14,0,172},{14,0,317},{145,0,6},{8,0,60} +,{9,0,343},{139,0,769},{134,0,1354},{132,0,724},{137,0,745},{132,11,474},{7,0, +1951},{8,0,765},{8,0,772},{140,0,671},{7,0,108},{8,0,219},{8,0,388},{9,0,775},{ +11,0,275},{140,0,464},{137,0,639},{135,10,503},{133,11,366},{5,0,15},{5,11,305}, +{6,0,56},{7,0,1758},{8,0,500},{9,0,730},{9,11,560},{11,0,331},{13,0,150},{13,11, +208},{142,0,282},{4,10,113},{5,10,163},{5,10,735},{7,10,1009},{9,10,9},{9,10,771 +},{12,10,90},{13,10,138},{13,10,410},{143,10,128},{4,10,324},{138,10,104},{135, +11,466},{142,11,27},{134,0,1886},{4,11,480},{5,0,205},{6,0,438},{6,11,167},{6,11 +,302},{6,11,1642},{7,11,130},{7,11,656},{7,11,837},{7,11,1547},{7,11,1657},{8,11 +,429},{9,0,711},{9,11,228},{10,11,643},{13,11,289},{13,11,343},{147,11,101},{134 +,0,865},{6,0,2025},{136,0,965},{7,11,278},{10,11,739},{11,11,708},{141,11,348},{ +133,0,534},{135,11,1922},{137,0,691},{4,10,935},{133,10,823},{6,0,443},{9,0,237} +,{9,0,571},{9,0,695},{10,0,139},{11,0,715},{12,0,417},{141,0,421},{5,10,269},{7, +10,434},{7,10,891},{8,10,339},{9,10,702},{11,10,594},{11,10,718},{145,10,100},{6 +,0,1555},{7,0,878},{9,10,485},{141,10,264},{134,10,1713},{7,10,1810},{11,10,866} +,{12,10,103},{141,10,495},{135,10,900},{6,0,1410},{9,11,316},{139,11,256},{4,0, +995},{135,0,1033},{132,0,578},{10,0,881},{12,0,740},{12,0,743},{140,0,759},{132, +0,822},{133,0,923},{142,10,143},{135,11,1696},{6,11,363},{7,11,1955},{136,11,725 +},{132,0,924},{133,0,665},{135,10,2029},{135,0,1901},{4,0,265},{6,0,1092},{6,0, +1417},{7,0,807},{135,0,950},{5,0,93},{12,0,267},{141,0,498},{135,0,1451},{5,11, +813},{135,11,2046},{5,10,625},{135,10,1617},{135,0,747},{6,0,788},{137,0,828},{5 +,11,712},{7,0,184},{7,11,1855},{8,10,425},{8,10,693},{9,10,720},{10,10,380},{10, +10,638},{11,0,307},{11,0,400},{11,10,473},{11,11,17},{12,10,61},{13,11,321},{15, +0,130},{144,11,67},{135,0,198},{6,11,320},{7,11,781},{7,11,1921},{9,11,55},{10, +11,186},{10,11,273},{10,11,664},{10,11,801},{11,11,996},{11,11,997},{13,11,157}, +{142,11,170},{136,11,271},{135,0,994},{7,11,103},{7,11,863},{11,11,184},{14,11, +299},{145,11,62},{11,10,551},{142,10,159},{5,0,233},{5,0,320},{6,0,140},{8,0,295 +},{8,0,615},{136,11,615},{133,0,978},{4,0,905},{6,0,1701},{137,0,843},{132,10, +168},{4,0,974},{8,0,850},{12,0,709},{12,0,768},{140,0,786},{135,10,91},{152,0,6} +,{138,10,532},{135,10,1884},{132,0,509},{6,0,1307},{135,0,273},{5,11,77},{7,11, +1455},{10,11,843},{19,11,73},{150,11,5},{132,11,458},{135,11,1420},{6,11,109},{ +138,11,382},{6,0,201},{6,11,330},{7,10,70},{7,11,1084},{10,10,240},{11,11,142},{ +147,10,93},{7,0,1041},{140,11,328},{133,11,354},{134,0,1040},{133,0,693},{134,0, +774},{139,0,234},{132,0,336},{7,0,1399},{139,10,392},{20,0,22},{148,11,22},{5,0, +802},{7,0,2021},{136,0,805},{5,0,167},{5,0,899},{6,0,410},{137,0,777},{137,0,789 +},{134,0,1705},{7,10,655},{135,10,1844},{4,10,145},{6,10,176},{7,10,395},{137,10 +,562},{132,10,501},{135,0,10},{5,0,11},{6,0,117},{6,0,485},{6,10,509},{7,0,1133} +,{9,0,582},{9,0,594},{10,0,82},{11,0,21},{11,0,818},{12,0,535},{13,0,86},{20,0, +91},{151,0,13},{4,0,264},{7,0,1067},{8,0,204},{8,0,385},{139,0,953},{139,11,737} +,{138,0,56},{134,0,1917},{133,0,470},{10,11,657},{14,11,297},{142,11,361},{135, +11,412},{7,0,1198},{7,11,1198},{8,11,556},{14,11,123},{14,11,192},{143,11,27},{7 +,11,1985},{14,11,146},{15,11,42},{16,11,23},{17,11,86},{146,11,17},{8,11,122},{ +139,0,1015},{4,10,114},{9,10,492},{13,10,462},{142,10,215},{4,10,77},{5,10,361}, +{6,10,139},{6,10,401},{6,10,404},{7,10,413},{7,10,715},{7,10,1716},{11,10,279},{ +12,10,179},{12,10,258},{13,10,244},{142,10,358},{134,10,1717},{7,10,1061},{8,10, +82},{11,10,250},{12,10,420},{141,10,184},{133,0,715},{135,10,724},{9,0,919},{9,0 +,922},{9,0,927},{9,0,933},{9,0,962},{9,0,1000},{9,0,1002},{9,0,1021},{12,0,890}, +{12,0,907},{12,0,930},{15,0,207},{15,0,228},{15,0,238},{149,0,61},{8,0,794},{9,0 +,400},{10,0,298},{142,0,228},{5,11,430},{5,11,932},{6,11,131},{7,11,417},{9,11, +522},{11,11,314},{141,11,390},{132,0,867},{4,11,507},{136,0,724},{137,11,261},{4 +,11,343},{133,11,511},{6,0,190},{7,0,768},{135,0,1170},{6,10,513},{135,10,1052}, +{7,11,455},{138,11,591},{134,0,1066},{137,10,899},{14,0,67},{147,0,60},{4,0,948} +,{18,0,174},{146,0,176},{135,0,1023},{7,10,1417},{12,10,382},{17,10,48},{152,10, +12},{134,11,575},{132,0,764},{6,10,545},{7,10,565},{7,10,1669},{10,10,114},{11, +10,642},{140,10,618},{6,0,137},{9,0,75},{9,0,253},{10,0,194},{138,0,444},{4,0, +756},{133,10,5},{7,10,192},{136,0,1008},{132,0,842},{8,10,763},{11,0,643},{140,0 +,115},{139,0,67},{133,10,759},{4,0,821},{5,0,760},{7,0,542},{7,11,580},{8,0,135} +,{136,0,496},{7,10,370},{7,10,1007},{7,10,1177},{135,10,1565},{135,10,1237},{140 +,0,736},{7,0,319},{7,0,355},{7,0,763},{10,0,389},{145,0,43},{8,11,333},{138,11, +182},{4,10,87},{5,10,250},{141,10,298},{138,0,786},{134,0,2044},{8,11,330},{140, +11,477},{135,11,1338},{132,11,125},{134,0,1030},{134,0,1083},{132,11,721},{135, +10,814},{7,11,776},{8,11,145},{147,11,56},{134,0,1226},{4,10,57},{7,10,1195},{7, +10,1438},{7,10,1548},{7,10,1835},{7,10,1904},{9,10,757},{10,10,604},{139,10,519} +,{7,11,792},{8,11,147},{10,11,821},{139,11,1021},{137,11,797},{4,0,58},{5,0,286} +,{6,0,319},{7,0,402},{7,0,1254},{7,0,1903},{8,0,356},{140,0,408},{4,0,389},{4,0, +815},{9,0,181},{9,0,255},{10,0,8},{10,0,29},{10,0,816},{11,0,311},{11,0,561},{12 +,0,67},{141,0,181},{7,11,1472},{135,11,1554},{7,11,1071},{7,11,1541},{7,11,1767} +,{7,11,1806},{7,11,1999},{9,11,248},{10,11,400},{11,11,162},{11,11,178},{11,11, +242},{12,11,605},{15,11,26},{144,11,44},{5,11,168},{5,11,930},{8,11,74},{9,11, +623},{12,11,500},{12,11,579},{13,11,41},{143,11,93},{6,11,220},{7,11,1101},{141, +11,105},{4,10,209},{5,0,474},{7,0,507},{7,10,902},{135,11,507},{132,0,427},{6,0, +413},{7,10,335},{7,10,1437},{7,10,1668},{8,10,553},{8,10,652},{8,10,656},{9,10, +558},{11,10,743},{149,10,18},{132,0,730},{6,11,19},{7,11,1413},{139,11,428},{133 +,0,373},{132,10,559},{7,11,96},{8,11,401},{137,11,896},{5,10,1017},{7,0,799},{7, +0,1972},{138,10,511},{135,0,1793},{7,11,1961},{7,11,1965},{8,11,702},{136,11,750 +},{8,11,150},{8,11,737},{140,11,366},{132,0,322},{133,10,709},{8,11,800},{9,11, +148},{9,11,872},{9,11,890},{11,11,309},{11,11,1001},{13,11,267},{141,11,323},{ +134,10,1745},{7,0,290},{136,10,206},{7,0,1651},{145,0,89},{139,0,2},{132,0,672}, +{6,0,1860},{8,0,905},{10,0,844},{10,0,846},{10,0,858},{12,0,699},{12,0,746},{140 +,0,772},{135,11,424},{133,11,547},{133,0,737},{5,11,490},{6,11,615},{6,11,620},{ +135,11,683},{6,0,746},{134,0,1612},{132,10,776},{9,11,385},{149,11,17},{133,0, +145},{135,10,1272},{7,0,884},{140,0,124},{4,0,387},{135,0,1288},{5,11,133},{136, +10,406},{136,11,187},{6,0,679},{8,11,8},{138,11,0},{135,0,550},{135,11,798},{136 +,11,685},{7,11,1086},{145,11,46},{8,10,175},{10,10,168},{138,10,573},{135,0,1305 +},{4,0,576},{135,0,1263},{6,0,686},{134,0,1563},{134,0,607},{5,0,919},{134,0, +1673},{148,0,37},{8,11,774},{10,11,670},{140,11,51},{133,10,784},{139,10,882},{4 +,0,82},{5,0,333},{5,0,904},{6,0,207},{7,0,325},{7,0,1726},{8,0,101},{10,0,778},{ +139,0,220},{135,11,371},{132,0,958},{133,0,903},{4,11,127},{5,11,350},{6,11,356} +,{8,11,426},{9,11,572},{10,11,247},{139,11,312},{140,0,147},{6,11,59},{7,11,885} +,{9,11,603},{141,11,397},{9,10,14},{9,10,441},{10,0,367},{139,10,9},{11,10,966}, +{12,10,287},{13,10,342},{13,10,402},{15,10,110},{143,10,163},{134,0,690},{132,0, +705},{7,10,1428},{7,10,1640},{7,10,1867},{9,0,651},{9,10,169},{9,10,182},{9,10, +367},{9,10,478},{9,10,506},{9,10,551},{9,10,557},{9,10,648},{9,10,697},{9,10,705 +},{9,10,725},{9,10,787},{9,10,794},{10,10,198},{10,10,214},{10,10,267},{10,10, +275},{10,10,456},{10,10,551},{10,10,561},{10,10,613},{10,10,627},{10,10,668},{10 +,10,675},{10,10,691},{10,10,695},{10,10,707},{10,10,715},{11,0,971},{11,10,183}, +{11,10,201},{11,10,262},{11,10,352},{11,10,439},{11,10,493},{11,10,572},{11,10, +591},{11,10,608},{11,10,611},{11,10,646},{11,10,674},{11,10,711},{11,10,751},{11 +,10,761},{11,10,776},{11,10,785},{11,10,850},{11,10,853},{11,10,862},{11,10,865} +,{11,10,868},{11,10,875},{11,10,898},{11,10,902},{11,10,903},{11,10,910},{11,10, +932},{11,10,942},{11,10,957},{11,10,967},{11,10,972},{12,10,148},{12,10,195},{12 +,10,220},{12,10,237},{12,10,318},{12,10,339},{12,10,393},{12,10,445},{12,10,450} +,{12,10,474},{12,10,505},{12,10,509},{12,10,533},{12,10,591},{12,10,594},{12,10, +597},{12,10,621},{12,10,633},{12,10,642},{13,0,273},{13,10,59},{13,10,60},{13,10 +,145},{13,10,239},{13,10,250},{13,10,329},{13,10,344},{13,10,365},{13,10,372},{ +13,10,387},{13,10,403},{13,10,414},{13,10,456},{13,10,470},{13,10,478},{13,10, +483},{13,10,489},{14,10,55},{14,10,57},{14,10,81},{14,10,90},{14,10,148},{14,10, +239},{14,10,266},{14,10,321},{14,10,326},{14,10,327},{14,10,330},{14,10,347},{14 +,10,355},{14,10,401},{14,10,404},{14,10,411},{14,10,414},{14,10,416},{14,10,420} +,{15,10,61},{15,10,74},{15,10,87},{15,10,88},{15,10,94},{15,10,96},{15,10,116},{ +15,10,149},{15,10,154},{16,10,50},{16,10,63},{16,10,73},{17,10,2},{17,10,66},{17 +,10,92},{17,10,103},{17,10,112},{17,10,120},{18,10,50},{18,10,54},{18,10,82},{18 +,10,86},{18,10,90},{18,10,111},{18,10,115},{18,10,156},{19,10,40},{19,10,79},{20 +,10,78},{149,10,22},{5,10,161},{7,0,887},{135,10,839},{142,11,98},{134,0,90},{ +138,11,356},{135,11,441},{6,11,111},{7,11,4},{8,11,163},{8,11,776},{138,11,566}, +{134,0,908},{134,0,1261},{7,0,813},{12,0,497},{141,0,56},{134,0,1235},{135,0,429 +},{135,11,1994},{138,0,904},{6,0,125},{7,0,1277},{137,0,772},{151,0,12},{4,0,841 +},{5,0,386},{133,11,386},{5,11,297},{135,11,1038},{6,0,860},{6,0,1069},{135,11, +309},{136,0,946},{135,10,1814},{141,11,418},{136,11,363},{10,0,768},{139,0,787}, +{22,11,30},{150,11,33},{6,0,160},{7,0,1106},{9,0,770},{11,0,112},{140,0,413},{11 +,11,216},{139,11,340},{136,10,139},{135,11,1390},{135,11,808},{132,11,280},{7,10 +,643},{8,10,236},{12,0,271},{145,0,109},{140,11,54},{4,11,421},{133,11,548},{11, +0,719},{12,0,36},{141,0,337},{7,0,581},{9,0,644},{137,0,699},{11,11,511},{13,11, +394},{14,11,298},{14,11,318},{146,11,103},{7,0,304},{9,0,646},{9,0,862},{11,0, +696},{12,0,208},{15,0,79},{147,0,108},{4,0,631},{7,0,1126},{135,0,1536},{135,11, +1527},{8,0,880},{10,0,869},{138,0,913},{5,10,54},{6,11,254},{7,0,1513},{9,11,109 +},{138,11,103},{135,0,981},{133,11,729},{132,10,744},{132,0,434},{134,0,550},{6, +11,1630},{7,0,930},{10,0,476},{10,10,402},{13,0,452},{18,10,55},{147,0,104},{5,0 +,553},{138,0,824},{136,0,452},{8,0,151},{137,10,624},{132,10,572},{132,0,772},{ +133,11,671},{133,0,292},{138,0,135},{132,11,889},{140,11,207},{6,10,43},{7,10,38 +},{8,10,248},{9,0,504},{138,10,513},{6,0,1089},{135,11,1910},{4,11,627},{133,11, +775},{135,0,783},{133,10,766},{133,10,363},{7,0,387},{135,11,387},{7,0,393},{7, +11,202},{10,0,603},{11,0,206},{11,11,362},{11,11,948},{140,11,388},{6,11,507},{7 +,11,451},{8,11,389},{12,11,490},{13,11,16},{13,11,215},{13,11,351},{18,11,132},{ +147,11,125},{4,0,912},{7,11,841},{137,0,232},{6,10,258},{140,10,409},{5,10,249}, +{148,10,82},{136,11,566},{6,0,977},{135,11,1214},{7,0,1973},{136,0,716},{135,0, +98},{133,0,733},{5,11,912},{134,11,1695},{5,10,393},{6,10,378},{7,10,1981},{9,10 +,32},{9,10,591},{10,10,685},{10,10,741},{142,10,382},{133,10,788},{7,10,1968},{ +10,0,19},{11,0,911},{141,10,509},{5,0,668},{5,11,236},{6,11,572},{8,11,492},{11, +11,618},{144,11,56},{135,11,1789},{4,0,360},{5,0,635},{5,0,700},{5,10,58},{5,10, +171},{5,10,683},{6,10,291},{6,10,566},{7,10,1650},{11,10,523},{12,10,273},{12,10 +,303},{15,10,39},{143,10,111},{133,0,901},{134,10,589},{5,11,190},{136,11,318},{ +140,0,656},{7,0,726},{152,0,9},{4,10,917},{133,10,1005},{135,10,1598},{134,11, +491},{4,10,919},{133,11,434},{137,0,72},{6,0,1269},{6,0,1566},{134,0,1621},{4,10 +,255},{5,10,302},{6,10,132},{7,10,128},{7,10,283},{7,10,1299},{9,0,463},{10,0, +595},{10,10,52},{10,10,514},{11,10,925},{13,10,92},{142,10,309},{135,0,1454},{ +134,0,1287},{9,10,173},{11,0,600},{141,0,245},{136,0,989},{7,0,164},{7,0,1571},{ +9,0,107},{140,0,225},{6,0,1061},{141,10,442},{4,0,27},{5,0,484},{5,0,510},{6,0, +434},{7,0,1000},{7,0,1098},{136,0,2},{7,11,85},{7,11,247},{8,11,585},{10,11,163} +,{138,11,316},{11,11,103},{142,11,0},{134,0,1127},{4,0,460},{134,0,852},{134,10, +210},{4,0,932},{133,0,891},{6,0,588},{147,11,83},{4,10,284},{6,10,223},{136,0, +625},{134,0,76},{8,0,92},{137,0,221},{4,11,124},{10,11,457},{11,11,121},{11,11, +169},{11,11,422},{11,11,870},{12,11,214},{13,11,389},{14,11,187},{143,11,77},{9, +11,618},{138,11,482},{4,10,218},{7,10,526},{143,10,137},{4,10,270},{5,10,192},{6 +,10,332},{7,10,1322},{13,0,9},{14,0,104},{142,0,311},{140,10,661},{135,11,1193}, +{6,11,107},{7,11,638},{7,11,1632},{137,11,396},{132,0,763},{4,0,622},{5,11,370}, +{134,11,1756},{133,0,253},{135,0,546},{5,11,204},{9,0,73},{10,0,110},{14,0,185}, +{145,0,119},{7,0,624},{7,0,916},{10,0,256},{139,0,87},{7,10,379},{8,10,481},{137 +,10,377},{5,0,212},{5,11,970},{6,11,1706},{12,0,35},{141,0,382},{5,10,1003},{6, +10,149},{137,0,746},{8,10,262},{9,10,627},{10,0,150},{11,0,849},{11,10,214},{11, +10,404},{11,10,457},{11,10,780},{11,10,913},{13,0,330},{13,10,401},{142,10,200}, +{134,0,1466},{135,11,3},{4,11,35},{5,11,121},{5,11,483},{5,11,685},{6,0,1299},{6 +,11,489},{7,11,1204},{136,11,394},{135,10,742},{4,10,142},{136,10,304},{4,11,921 +},{133,11,1007},{134,0,1518},{6,0,1229},{135,0,1175},{133,0,816},{4,10,471},{4, +11,712},{5,10,51},{6,10,602},{7,10,925},{8,10,484},{10,10,195},{140,0,159},{134, +11,1629},{5,0,869},{5,0,968},{6,0,1626},{8,0,734},{136,0,784},{4,0,542},{6,0, +1716},{6,0,1727},{7,0,1082},{7,0,1545},{8,0,56},{8,0,118},{8,0,412},{8,0,564},{8 +,10,688},{9,0,888},{9,0,908},{10,0,50},{10,0,423},{11,0,685},{11,0,697},{11,0, +933},{12,0,299},{13,0,126},{13,0,136},{13,0,170},{141,0,190},{132,10,697},{4,0, +232},{9,0,202},{10,0,474},{140,0,433},{136,0,212},{6,0,108},{7,0,1003},{7,0,1181 +},{8,0,111},{136,0,343},{5,10,221},{135,11,1255},{133,11,485},{134,0,1712},{142, +0,216},{4,11,285},{5,0,643},{5,11,317},{6,0,516},{6,11,301},{7,11,7},{8,11,153}, +{10,11,766},{11,11,468},{12,11,467},{141,11,143},{4,0,133},{7,0,711},{7,0,1298}, +{135,0,1585},{134,0,650},{135,11,512},{6,0,99},{7,0,1808},{145,0,57},{6,0,246},{ +6,0,574},{7,0,428},{9,0,793},{10,0,669},{11,0,485},{11,0,840},{12,0,300},{14,0, +250},{145,0,55},{4,10,132},{5,10,69},{135,10,1242},{136,0,1023},{4,10,111},{135, +0,302},{135,0,1871},{132,0,728},{4,10,767},{137,0,252},{6,0,461},{7,0,1590},{7, +10,1416},{7,10,2005},{8,10,131},{8,10,466},{9,10,672},{13,10,252},{148,10,103},{ +6,0,323},{135,0,1564},{7,0,461},{136,0,775},{6,10,44},{136,10,368},{139,0,172},{ +132,0,464},{4,10,570},{133,10,120},{137,11,269},{6,10,227},{135,10,1589},{6,11, +1719},{6,11,1735},{7,11,2016},{7,11,2020},{8,11,837},{137,11,852},{7,0,727},{146 +,0,73},{132,0,1023},{135,11,852},{135,10,1529},{136,0,577},{138,11,568},{134,0, +1037},{8,11,67},{138,11,419},{4,0,413},{5,0,677},{8,0,432},{140,0,280},{6,10, +1667},{7,10,2036},{7,11,967},{10,0,600},{141,11,11},{6,10,511},{140,10,132},{5, +10,568},{6,0,799},{6,10,138},{135,10,1293},{4,10,565},{8,0,159},{136,10,827},{7, +0,646},{7,0,1730},{11,0,446},{141,0,178},{4,10,922},{133,10,1023},{135,11,11},{ +132,0,395},{7,10,1002},{139,0,145},{9,0,174},{10,0,164},{11,0,440},{11,0,514},{ +11,0,841},{15,0,98},{149,0,20},{134,0,426},{10,0,608},{139,0,1002},{7,11,320},{8 +,11,51},{12,11,481},{12,11,570},{148,11,106},{4,11,445},{9,0,977},{137,0,983},{ +138,0,250},{139,0,100},{6,0,1982},{136,10,402},{133,11,239},{4,10,716},{141,10, +31},{5,0,476},{7,11,83},{7,11,1990},{8,11,130},{139,11,720},{8,10,691},{136,10, +731},{5,11,123},{6,11,530},{7,11,348},{135,11,1419},{5,0,76},{6,0,458},{6,0,497} +,{7,0,868},{9,0,658},{10,0,594},{11,0,173},{11,0,566},{12,0,20},{12,0,338},{141, +0,200},{9,11,139},{10,11,399},{11,11,469},{12,11,634},{141,11,223},{9,10,840},{ +138,10,803},{133,10,847},{11,11,223},{140,11,168},{132,11,210},{8,0,447},{9,10, +53},{9,10,268},{9,10,901},{10,10,518},{10,10,829},{11,10,188},{13,10,74},{14,10, +46},{15,10,17},{15,10,33},{17,10,40},{18,10,36},{19,10,20},{22,10,1},{152,10,2}, +{4,0,526},{7,0,1029},{135,0,1054},{19,11,59},{150,11,2},{4,0,636},{6,0,1875},{6, +0,1920},{8,10,532},{9,0,999},{12,0,807},{12,0,825},{15,0,179},{15,0,190},{146,0, +182},{6,0,1699},{7,0,660},{7,0,1124},{17,0,31},{19,0,22},{151,0,14},{135,10,681} +,{132,11,430},{140,10,677},{4,10,684},{136,10,384},{132,11,756},{133,11,213},{7, +0,188},{7,10,110},{8,10,290},{8,10,591},{9,10,382},{9,10,649},{11,10,71},{11,10, +155},{11,10,313},{12,10,5},{13,10,325},{142,10,287},{7,10,360},{7,10,425},{9,10, +66},{9,10,278},{138,10,644},{142,11,164},{4,0,279},{7,0,301},{137,0,362},{134,11 +,586},{135,0,1743},{4,0,178},{133,0,399},{4,10,900},{133,10,861},{5,10,254},{7, +10,985},{136,10,73},{133,11,108},{7,10,1959},{136,10,683},{133,11,219},{4,11,193 +},{5,11,916},{7,11,364},{10,11,398},{10,11,726},{11,11,317},{11,11,626},{12,11, +142},{12,11,288},{12,11,678},{13,11,313},{15,11,113},{18,11,114},{21,11,30},{150 +,11,53},{6,11,241},{7,11,907},{8,11,832},{9,11,342},{10,11,729},{11,11,284},{11, +11,445},{11,11,651},{11,11,863},{13,11,398},{146,11,99},{132,0,872},{134,0,831}, +{134,0,1692},{6,0,202},{6,0,1006},{9,0,832},{9,11,734},{10,0,636},{11,0,208},{12 +,0,360},{17,0,118},{18,0,27},{148,0,67},{132,10,725},{7,11,993},{138,11,666},{ +134,0,1954},{134,10,196},{7,0,872},{10,0,516},{139,0,167},{133,10,831},{4,11,562 +},{9,11,254},{139,11,879},{137,0,313},{4,0,224},{132,11,786},{8,10,723},{11,0,24 +},{140,0,170},{5,0,546},{7,0,35},{8,0,11},{8,0,12},{9,0,315},{9,0,533},{10,0,802 +},{11,0,166},{12,0,525},{142,0,243},{7,0,1937},{13,10,80},{13,10,437},{145,10,74 +},{5,0,241},{8,0,242},{9,0,451},{10,0,667},{11,0,598},{140,0,429},{150,0,46},{6, +0,1273},{137,0,830},{5,10,848},{6,10,66},{136,10,764},{6,0,825},{134,0,993},{4,0 +,1006},{4,10,36},{7,10,1387},{10,0,327},{11,10,755},{141,0,271},{134,0,1023},{ +135,0,1580},{4,0,366},{137,0,516},{132,10,887},{6,0,1736},{135,0,1891},{6,11,216 +},{7,11,901},{7,11,1343},{136,11,493},{6,10,165},{138,10,388},{7,11,341},{139,11 +,219},{4,10,719},{135,10,155},{134,0,1935},{132,0,826},{6,0,331},{6,0,1605},{8,0 +,623},{11,0,139},{139,0,171},{135,11,1734},{10,11,115},{11,11,420},{12,11,154},{ +13,11,404},{14,11,346},{15,11,54},{143,11,112},{4,10,353},{6,10,146},{6,10,1789} +,{7,0,288},{7,10,990},{7,10,1348},{9,10,665},{9,10,898},{11,10,893},{142,10,212} +,{6,0,916},{134,0,1592},{4,10,45},{7,0,1888},{135,10,1257},{5,11,1011},{136,11, +701},{139,11,596},{4,11,54},{5,11,666},{7,11,1039},{7,11,1130},{9,11,195},{138, +11,302},{134,0,1471},{134,0,1570},{132,0,394},{140,10,65},{136,10,816},{135,0, +1931},{7,0,574},{135,0,1719},{134,11,467},{132,0,658},{6,11,1669},{9,0,781},{10, +0,144},{11,0,385},{13,0,161},{13,0,228},{13,0,268},{148,0,107},{136,0,374},{135, +0,735},{4,0,344},{6,0,498},{139,0,323},{6,10,559},{6,10,1691},{7,0,586},{135,0, +1063},{137,0,155},{133,0,906},{7,11,122},{9,11,259},{10,11,84},{11,11,470},{12, +11,541},{141,11,379},{134,0,1139},{10,0,108},{139,0,116},{134,10,456},{133,10, +925},{5,11,82},{5,11,131},{7,11,1755},{8,11,31},{9,11,168},{9,11,764},{139,11, +869},{134,11,605},{5,11,278},{137,11,68},{4,11,163},{5,11,201},{5,11,307},{5,11, +310},{6,11,335},{7,11,284},{136,11,165},{135,11,1660},{6,11,33},{135,11,1244},{4 +,0,616},{136,11,483},{4,11,199},{8,0,857},{8,0,902},{8,0,910},{10,0,879},{11,11, +34},{140,0,726},{136,0,692},{6,10,193},{7,10,240},{7,10,1682},{10,10,51},{10,10, +640},{11,10,410},{13,10,82},{14,10,247},{14,10,331},{142,10,377},{6,0,823},{134, +0,983},{139,10,411},{132,0,305},{136,10,633},{138,11,203},{134,0,681},{6,11,326} +,{7,11,677},{137,11,425},{5,0,214},{7,0,603},{8,0,611},{9,0,686},{10,0,88},{11,0 +,459},{11,0,496},{12,0,463},{12,0,590},{141,0,0},{136,0,1004},{142,0,23},{134,0, +1703},{147,11,8},{145,11,56},{135,0,1443},{4,10,237},{135,10,514},{6,0,714},{145 +,0,19},{5,11,358},{7,11,473},{7,11,1184},{10,11,662},{13,11,212},{13,11,304},{13 +,11,333},{145,11,98},{4,0,737},{10,0,98},{11,0,294},{12,0,60},{12,0,437},{13,0, +64},{13,0,380},{142,0,430},{6,10,392},{7,10,65},{135,10,2019},{6,0,1758},{8,0, +520},{9,0,345},{9,0,403},{142,0,350},{5,0,47},{10,0,242},{138,0,579},{5,0,139},{ +7,0,1168},{138,0,539},{134,0,1459},{13,0,388},{141,11,388},{134,0,253},{7,10, +1260},{135,10,1790},{9,10,222},{10,0,252},{139,10,900},{140,0,745},{133,11,946}, +{4,0,107},{7,0,613},{8,0,439},{8,0,504},{9,0,501},{10,0,383},{139,0,477},{135,11 +,1485},{132,0,871},{7,11,411},{7,11,590},{8,11,631},{9,11,323},{10,11,355},{11, +11,491},{12,11,143},{12,11,402},{13,11,73},{14,11,408},{15,11,107},{146,11,71},{ +132,0,229},{132,0,903},{140,0,71},{133,0,549},{4,0,47},{6,0,373},{7,0,452},{7,0, +543},{7,0,1828},{7,0,1856},{9,0,6},{11,0,257},{139,0,391},{7,11,1467},{8,11,328} +,{10,11,544},{11,11,955},{13,11,320},{145,11,83},{5,0,980},{134,0,1754},{136,0, +865},{5,0,705},{137,0,606},{7,0,161},{8,10,201},{136,10,605},{143,11,35},{5,11, +835},{6,11,483},{140,10,224},{7,0,536},{7,0,1331},{136,0,143},{134,0,1388},{5,0, +724},{10,0,305},{11,0,151},{12,0,33},{12,0,121},{12,0,381},{17,0,3},{17,0,27},{ +17,0,78},{18,0,18},{19,0,54},{149,0,5},{4,10,523},{133,10,638},{5,0,19},{134,0, +533},{5,0,395},{5,0,951},{134,0,1776},{135,0,1908},{132,0,846},{6,10,242},{7,10, +227},{7,10,1581},{8,10,104},{9,10,113},{9,10,220},{9,10,427},{10,0,74},{10,10, +239},{11,0,663},{11,10,579},{11,10,1023},{12,0,210},{13,0,166},{13,0,310},{13,10 +,4},{13,10,204},{13,10,316},{14,0,373},{18,0,95},{19,0,43},{148,10,86},{9,11,716 +},{11,11,108},{13,11,123},{14,11,252},{19,11,38},{21,11,3},{151,11,11},{8,0,372} +,{9,0,122},{138,0,175},{132,11,677},{7,11,1374},{136,11,540},{135,10,861},{132,0 +,695},{7,0,497},{9,0,387},{147,0,81},{136,0,937},{134,0,718},{7,0,1328},{136,10, +494},{132,11,331},{5,11,747},{134,0,1581},{5,0,284},{6,0,49},{6,0,350},{7,0,1},{ +7,0,377},{7,0,1693},{8,0,18},{8,0,678},{9,0,161},{9,0,585},{9,0,671},{9,0,839},{ +11,0,912},{141,0,427},{7,10,1306},{8,10,505},{9,10,482},{10,10,126},{11,10,225}, +{12,10,347},{12,10,449},{13,10,19},{14,10,218},{142,10,435},{10,10,764},{12,10, +120},{13,10,39},{145,10,127},{4,0,597},{133,10,268},{134,0,1094},{4,0,1008},{134 +,0,1973},{132,0,811},{139,0,908},{135,0,1471},{133,11,326},{4,10,384},{135,10, +1022},{4,11,691},{7,0,1935},{7,11,1935},{8,0,324},{8,11,324},{9,11,35},{10,11, +680},{11,11,364},{12,0,42},{12,11,42},{13,11,357},{146,11,16},{135,0,2014},{6,11 +,32},{7,0,2007},{7,11,385},{7,11,757},{7,11,1916},{8,11,37},{8,11,94},{8,11,711} +,{9,0,101},{9,0,450},{9,11,541},{10,0,66},{10,0,842},{10,11,162},{10,11,795},{11 +,0,536},{11,11,989},{11,11,1010},{12,0,587},{12,11,14},{142,11,308},{139,0,586}, +{135,10,1703},{7,0,1077},{9,10,159},{11,0,28},{140,10,603},{6,0,1221},{136,10, +583},{6,11,152},{6,11,349},{6,11,1682},{7,11,1252},{8,11,112},{9,11,435},{9,11, +668},{10,11,290},{10,11,319},{10,11,815},{11,11,180},{11,11,837},{12,11,240},{13 +,11,152},{13,11,219},{142,11,158},{139,0,62},{132,10,515},{8,10,632},{8,10,697}, +{137,10,854},{134,0,1766},{132,11,581},{6,11,126},{7,11,573},{8,11,397},{142,11, +44},{150,0,28},{4,10,136},{5,10,551},{11,0,670},{150,0,25},{6,0,1665},{7,0,256}, +{7,0,1388},{138,0,499},{4,0,22},{5,0,10},{7,0,1576},{136,0,97},{134,10,1782},{5, +0,481},{7,10,1287},{9,10,44},{10,10,552},{10,10,642},{11,10,839},{12,10,274},{12 +,10,275},{12,10,372},{13,10,91},{142,10,125},{133,11,926},{7,11,1232},{137,11, +531},{6,0,134},{7,0,437},{7,0,1824},{9,0,37},{14,0,285},{142,0,371},{7,0,486},{8 +,0,155},{11,0,93},{140,0,164},{6,0,1391},{134,0,1442},{133,11,670},{133,0,591},{ +6,10,147},{7,10,886},{7,11,1957},{9,10,753},{138,10,268},{5,0,380},{5,0,650},{7, +0,1173},{136,0,310},{4,0,364},{7,0,1156},{7,0,1187},{137,0,409},{135,11,1621},{ +134,0,482},{133,11,506},{4,0,781},{6,0,487},{7,0,926},{8,0,263},{139,0,500},{138 +,10,137},{135,11,242},{139,11,96},{133,10,414},{135,10,1762},{134,0,804},{5,11, +834},{7,11,1202},{8,11,14},{9,11,481},{137,11,880},{134,10,599},{4,0,94},{135,0, +1265},{4,0,415},{132,0,417},{5,0,348},{6,0,522},{6,10,1749},{7,11,1526},{138,11, +465},{134,10,1627},{132,0,1012},{132,10,488},{4,11,357},{6,11,172},{7,11,143},{ +137,11,413},{4,10,83},{4,11,590},{146,11,76},{140,10,676},{7,11,287},{8,11,355}, +{9,11,293},{137,11,743},{134,10,278},{5,11,169},{6,0,1803},{7,11,333},{8,11,45}, +{18,0,165},{152,0,21},{12,10,97},{140,11,97},{4,0,408},{4,0,741},{135,0,500},{ +132,11,198},{7,10,388},{7,10,644},{139,10,781},{4,11,24},{5,11,140},{5,11,185},{ +7,11,1500},{11,11,565},{139,11,838},{6,0,1321},{7,10,229},{8,10,59},{9,0,257},{9 +,10,190},{10,10,378},{140,10,191},{4,11,334},{133,11,593},{135,11,1885},{134,0, +1138},{4,0,249},{6,0,73},{135,0,177},{133,0,576},{142,0,231},{137,0,288},{132,10 +,660},{7,10,1035},{138,10,737},{135,0,1487},{6,0,989},{7,10,690},{9,0,433},{9,10 +,587},{140,10,521},{7,0,1264},{7,0,1678},{11,0,945},{12,0,341},{12,0,471},{140,0 +,569},{132,11,709},{133,11,897},{5,11,224},{13,11,174},{146,11,52},{135,11,1840} +,{134,10,1744},{4,10,733},{9,10,194},{10,10,92},{11,10,198},{12,0,87},{12,10,84} +,{13,10,128},{144,0,74},{140,0,779},{135,0,538},{4,11,608},{133,11,497},{133,0, +413},{7,11,1375},{7,11,1466},{138,11,331},{136,0,495},{6,11,540},{136,11,136},{7 +,0,54},{8,0,312},{10,0,191},{10,0,614},{140,0,567},{5,11,999},{6,0,468},{7,0,567 +},{7,0,1478},{8,0,530},{142,0,290},{4,11,299},{7,10,306},{135,11,1004},{142,11, +296},{134,0,1484},{133,10,979},{6,0,609},{9,0,815},{12,11,137},{14,11,9},{14,11, +24},{142,11,64},{133,11,456},{6,0,484},{135,0,822},{133,10,178},{136,11,180},{ +132,11,755},{137,0,900},{135,0,1335},{6,0,1724},{135,0,2022},{135,11,1139},{4,10 +,390},{133,0,640},{6,0,1831},{138,11,633},{135,11,566},{4,11,890},{5,11,805},{5, +11,819},{5,11,961},{6,11,396},{6,11,1631},{6,11,1678},{7,11,1967},{7,11,2041},{9 +,11,630},{11,11,8},{11,11,1019},{12,11,176},{13,11,225},{14,11,292},{149,11,24}, +{132,0,474},{134,0,1103},{135,0,1504},{134,0,1576},{6,0,961},{6,0,1034},{140,0, +655},{11,11,514},{149,11,20},{5,0,305},{135,11,1815},{7,11,1505},{10,11,190},{10 +,11,634},{11,11,792},{12,11,358},{140,11,447},{5,11,0},{6,11,536},{7,11,604},{13 +,11,445},{145,11,126},{5,10,105},{135,0,1236},{4,0,480},{6,0,217},{6,0,302},{6,0 +,1642},{6,11,232},{6,11,412},{7,0,130},{7,0,837},{7,0,1321},{7,0,1547},{7,0,1657 +},{7,11,1074},{8,0,429},{8,11,9},{8,11,157},{8,11,786},{9,0,228},{9,11,196},{9, +11,352},{9,11,457},{10,11,337},{11,11,232},{11,11,877},{12,11,480},{12,11,546},{ +13,0,289},{13,0,343},{147,0,101},{5,10,438},{7,11,958},{9,10,694},{12,10,627},{ +13,10,210},{141,11,38},{4,11,382},{136,11,579},{7,0,278},{10,0,739},{11,0,708},{ +141,0,348},{4,11,212},{135,11,1206},{135,11,1898},{6,0,708},{6,0,1344},{152,10, +11},{137,11,768},{134,0,1840},{140,0,233},{8,10,25},{138,10,826},{5,11,655},{134 +,0,2017},{6,0,1488},{139,11,290},{132,10,308},{134,0,1590},{134,0,1800},{134,0, +1259},{6,11,231},{7,11,95},{8,11,423},{144,0,28},{133,11,300},{135,10,150},{136, +10,649},{7,11,1874},{137,11,641},{6,11,237},{7,11,611},{8,11,100},{9,11,416},{11 +,11,335},{12,11,173},{146,11,101},{137,0,45},{134,10,521},{14,11,26},{17,0,36},{ +146,11,150},{5,10,339},{7,0,1442},{14,0,22},{15,10,41},{15,10,166},{147,10,66},{ +6,11,581},{7,11,1119},{136,0,378},{134,0,1507},{147,11,117},{139,0,39},{134,0, +1054},{6,0,363},{7,0,1955},{136,0,725},{134,0,2036},{133,11,199},{6,0,1871},{9,0 +,935},{9,0,961},{9,0,1004},{9,0,1016},{12,0,805},{12,0,852},{12,0,853},{12,0,869 +},{12,0,882},{12,0,896},{12,0,906},{12,0,917},{12,0,940},{15,0,170},{15,0,176},{ +15,0,188},{15,0,201},{15,0,205},{15,0,212},{15,0,234},{15,0,244},{18,0,181},{18, +0,193},{18,0,196},{18,0,201},{18,0,202},{18,0,210},{18,0,217},{18,0,235},{18,0, +236},{18,0,237},{21,0,54},{21,0,55},{21,0,58},{21,0,59},{152,0,22},{134,10,1628} +,{137,0,805},{5,0,813},{135,0,2046},{142,11,42},{5,0,712},{6,0,1240},{11,0,17},{ +13,0,321},{144,0,67},{132,0,617},{135,10,829},{6,0,320},{7,0,781},{7,0,1921},{9, +0,55},{10,0,186},{10,0,273},{10,0,664},{10,0,801},{11,0,996},{11,0,997},{13,0, +157},{142,0,170},{136,0,271},{5,10,486},{135,10,1349},{18,11,91},{147,11,70},{7, +10,1635},{8,10,17},{10,0,445},{138,10,295},{136,11,404},{7,0,103},{7,0,863},{11, +0,184},{145,0,62},{138,10,558},{137,0,659},{6,11,312},{6,11,1715},{10,11,584},{ +11,11,546},{11,11,692},{12,11,259},{12,11,295},{13,11,46},{141,11,154},{134,0, +676},{132,11,588},{4,11,231},{5,11,61},{6,11,104},{7,11,729},{7,11,964},{7,11, +1658},{140,11,414},{6,11,263},{138,11,757},{11,0,337},{142,0,303},{135,11,1363}, +{132,11,320},{140,0,506},{134,10,447},{5,0,77},{7,0,1455},{10,0,843},{147,0,73}, +{7,10,577},{7,10,1432},{9,10,475},{9,10,505},{9,10,526},{9,10,609},{9,10,689},{9 +,10,726},{9,10,735},{9,10,738},{10,10,556},{10,10,674},{10,10,684},{11,10,89},{ +11,10,202},{11,10,272},{11,10,380},{11,10,415},{11,10,505},{11,10,537},{11,10, +550},{11,10,562},{11,10,640},{11,10,667},{11,10,688},{11,10,847},{11,10,927},{11 +,10,930},{11,10,940},{12,10,144},{12,10,325},{12,10,329},{12,10,389},{12,10,403} +,{12,10,451},{12,10,515},{12,10,604},{12,10,616},{12,10,626},{13,10,66},{13,10, +131},{13,10,167},{13,10,236},{13,10,368},{13,10,411},{13,10,434},{13,10,453},{13 +,10,461},{13,10,474},{14,10,59},{14,10,60},{14,10,139},{14,10,152},{14,10,276},{ +14,10,353},{14,10,402},{15,10,28},{15,10,81},{15,10,123},{15,10,152},{18,10,136} +,{148,10,88},{132,0,458},{135,0,1420},{4,10,609},{4,11,405},{6,0,109},{7,10,756} +,{7,11,817},{9,10,544},{10,0,382},{11,10,413},{14,10,307},{14,11,58},{16,10,25}, +{17,11,37},{146,11,124},{5,11,974},{6,0,330},{7,0,1084},{139,0,142},{4,10,930},{ +133,10,947},{5,10,939},{142,11,394},{16,0,91},{145,0,87},{5,10,962},{5,11,235},{ +7,11,1239},{11,11,131},{140,11,370},{5,10,651},{8,10,170},{9,10,61},{9,10,63},{ +10,10,23},{10,10,37},{10,10,834},{11,0,492},{11,10,4},{11,10,281},{11,10,503},{ +11,10,677},{12,10,96},{12,10,130},{12,10,244},{14,10,5},{14,10,40},{14,10,162},{ +14,10,202},{146,10,133},{4,10,406},{5,10,579},{12,10,492},{150,10,15},{9,11,137} +,{138,11,221},{134,0,1239},{11,0,211},{140,0,145},{7,11,390},{138,11,140},{135, +11,1418},{135,11,1144},{134,0,1049},{6,10,17},{7,0,321},{7,10,1001},{7,10,1982}, +{9,10,886},{10,10,489},{10,10,800},{11,10,782},{12,10,320},{13,10,467},{14,10, +145},{14,10,387},{143,10,119},{145,10,17},{5,11,407},{11,11,489},{19,11,37},{20, +11,73},{150,11,38},{133,10,458},{135,0,1985},{7,10,1983},{8,10,0},{8,10,171},{9, +10,120},{9,10,732},{10,10,473},{11,10,656},{11,10,998},{18,10,0},{18,10,2},{147, +10,21},{5,11,325},{7,11,1483},{8,11,5},{8,11,227},{9,11,105},{10,11,585},{140,11 +,614},{136,0,122},{132,0,234},{135,11,1196},{6,0,976},{6,0,1098},{134,0,1441},{7 +,0,253},{136,0,549},{6,11,621},{13,11,504},{144,11,19},{132,10,519},{5,0,430},{5 +,0,932},{6,0,131},{7,0,417},{9,0,522},{11,0,314},{141,0,390},{14,0,149},{14,0, +399},{143,0,57},{5,10,907},{6,10,31},{6,11,218},{7,10,491},{7,10,530},{8,10,592} +,{11,10,53},{11,10,779},{12,10,167},{12,10,411},{14,10,14},{14,10,136},{15,10,72 +},{16,10,17},{144,10,72},{140,11,330},{7,11,454},{7,11,782},{136,11,768},{132,0, +507},{10,11,676},{140,11,462},{4,10,208},{5,10,106},{6,0,630},{6,10,531},{8,10, +408},{9,0,811},{9,10,188},{138,10,572},{4,0,343},{5,0,511},{134,10,1693},{134,11 +,164},{132,0,448},{7,0,455},{138,0,591},{135,0,1381},{12,10,441},{150,11,50},{9, +10,449},{10,10,192},{138,10,740},{4,10,241},{134,0,575},{134,0,1175},{134,0,653} +,{134,0,1761},{134,0,1198},{132,10,259},{6,11,343},{7,11,195},{9,11,226},{10,11, +197},{10,11,575},{11,11,502},{139,11,899},{7,0,1127},{7,0,1572},{10,0,297},{10,0 +,422},{11,0,764},{11,0,810},{12,0,264},{13,0,102},{13,0,300},{13,0,484},{14,0, +147},{14,0,229},{17,0,71},{18,0,118},{147,0,120},{135,11,666},{132,0,678},{4,10, +173},{5,10,312},{5,10,512},{135,10,1285},{7,10,1603},{7,10,1691},{9,10,464},{11, +10,195},{12,10,279},{12,10,448},{14,10,11},{147,10,102},{16,0,99},{146,0,164},{7 +,11,1125},{9,11,143},{11,11,61},{14,11,405},{150,11,21},{137,11,260},{4,10,452}, +{5,10,583},{5,10,817},{6,10,433},{7,10,593},{7,10,720},{7,10,1378},{8,10,161},{9 +,10,284},{10,10,313},{139,10,886},{132,10,547},{136,10,722},{14,0,35},{142,0,191 +},{141,0,45},{138,0,121},{132,0,125},{134,0,1622},{133,11,959},{8,10,420},{139, +10,193},{132,0,721},{135,10,409},{136,0,145},{7,0,792},{8,0,147},{8,11,173},{10, +0,821},{11,0,970},{139,0,1021},{134,11,266},{132,0,715},{7,0,1999},{138,10,308}, +{133,0,531},{5,0,168},{5,0,930},{8,0,74},{9,0,623},{12,0,500},{140,0,579},{144,0 +,65},{138,11,246},{6,0,220},{7,0,1101},{13,0,105},{142,11,314},{5,10,1002},{136, +10,745},{134,0,960},{20,0,0},{148,11,0},{4,0,1005},{4,10,239},{6,10,477},{7,10, +1607},{11,10,68},{139,10,617},{6,0,19},{7,0,1413},{139,0,428},{149,10,13},{7,0, +96},{8,0,401},{8,0,703},{8,11,300},{137,0,896},{134,0,1595},{145,0,116},{136,0, +1021},{7,0,1961},{7,0,1965},{7,0,2030},{8,0,150},{8,0,702},{8,0,737},{8,0,750},{ +140,0,366},{11,11,75},{142,11,267},{132,10,367},{5,11,427},{5,11,734},{7,11,478} +,{8,0,800},{8,11,52},{9,0,148},{9,0,872},{9,0,890},{11,0,309},{11,0,1001},{13,0, +267},{141,0,323},{7,11,239},{11,11,217},{142,11,165},{132,11,323},{140,11,419},{ +13,0,299},{142,0,75},{6,11,87},{6,11,1734},{7,11,20},{7,11,1056},{8,11,732},{9, +11,406},{9,11,911},{138,11,694},{134,0,1383},{132,10,694},{133,11,613},{137,0, +779},{4,0,598},{140,10,687},{6,0,970},{135,0,424},{133,0,547},{7,11,32},{7,11, +984},{8,11,85},{8,11,709},{9,11,579},{9,11,847},{9,11,856},{10,11,799},{11,11, +258},{11,11,1007},{12,11,331},{12,11,615},{13,11,188},{13,11,435},{14,11,8},{15, +11,165},{16,11,27},{148,11,40},{6,0,1222},{134,0,1385},{132,0,876},{138,11,151}, +{135,10,213},{4,11,167},{135,11,82},{133,0,133},{6,11,24},{7,11,74},{7,11,678},{ +137,11,258},{5,11,62},{6,11,534},{7,11,684},{7,11,1043},{7,11,1072},{8,11,280},{ +8,11,541},{8,11,686},{10,11,519},{11,11,252},{140,11,282},{136,0,187},{8,0,8},{ +10,0,0},{10,0,818},{139,0,988},{132,11,359},{7,10,1672},{11,0,429},{143,0,51},{ +136,0,685},{5,11,211},{7,11,88},{136,11,627},{134,0,472},{136,0,132},{6,11,145}, +{141,11,336},{4,10,751},{11,10,390},{140,10,32},{4,10,409},{4,11,263},{5,10,78}, +{6,0,938},{134,0,1060},{137,0,874},{4,11,916},{6,10,473},{7,10,1602},{8,0,774},{ +10,0,670},{10,10,698},{12,0,51},{12,10,212},{13,10,307},{145,10,105},{146,0,92}, +{143,10,156},{132,0,830},{137,0,701},{4,11,599},{6,11,1634},{7,11,5},{7,11,55},{ +7,11,67},{7,11,97},{7,11,691},{7,11,979},{7,11,1697},{8,11,207},{8,11,214},{8,11 +,231},{8,11,294},{8,11,336},{8,11,428},{8,11,451},{8,11,460},{8,11,471},{8,11, +622},{8,11,626},{8,11,679},{8,11,759},{8,11,829},{9,11,11},{9,11,246},{9,11,484} +,{9,11,573},{9,11,706},{9,11,762},{9,11,798},{9,11,855},{9,11,870},{9,11,912},{ +10,11,303},{10,11,335},{10,11,424},{10,11,461},{10,11,543},{10,11,759},{10,11, +814},{11,11,59},{11,11,199},{11,11,235},{11,11,475},{11,11,590},{11,11,929},{11, +11,963},{12,11,114},{12,11,182},{12,11,226},{12,11,332},{12,11,439},{12,11,575}, +{12,11,598},{13,11,8},{13,11,125},{13,11,194},{13,11,287},{14,11,197},{14,11,383 +},{15,11,53},{17,11,63},{19,11,46},{19,11,98},{19,11,106},{148,11,85},{4,0,127}, +{5,0,350},{6,0,356},{8,0,426},{9,0,572},{10,0,247},{139,0,312},{134,0,1215},{6,0 +,59},{7,11,1853},{9,0,603},{10,11,437},{141,0,397},{134,0,1762},{147,11,126},{ +135,10,883},{13,0,293},{142,0,56},{133,10,617},{139,10,50},{5,11,187},{7,10,1518 +},{139,10,694},{135,0,441},{6,0,111},{7,0,4},{8,0,163},{8,0,776},{138,0,566},{ +132,0,806},{4,11,215},{9,11,38},{10,11,3},{11,11,23},{11,11,127},{139,11,796},{4 +,10,546},{7,10,2042},{142,0,233},{135,0,1994},{134,0,1739},{135,11,1530},{136,0, +393},{5,0,297},{7,0,1038},{14,0,359},{19,0,52},{148,0,47},{135,0,309},{4,10,313} +,{133,10,577},{8,10,184},{141,10,433},{135,10,935},{12,10,186},{12,10,292},{14, +10,100},{146,10,70},{136,0,363},{11,10,402},{12,10,109},{12,10,431},{13,10,179}, +{13,10,206},{14,0,175},{14,10,217},{16,10,3},{148,10,53},{5,10,886},{6,10,46},{6 +,10,1790},{7,10,14},{7,10,732},{7,10,1654},{8,10,95},{8,10,327},{8,10,616},{9,10 +,892},{10,10,598},{10,10,769},{11,10,134},{11,10,747},{12,10,378},{142,10,97},{ +136,0,666},{135,0,1675},{6,0,655},{134,0,1600},{135,0,808},{133,10,1021},{4,11, +28},{5,11,440},{7,11,248},{11,11,833},{140,11,344},{134,11,1654},{132,0,280},{ +140,0,54},{4,0,421},{133,0,548},{132,10,153},{6,11,339},{135,11,923},{133,11,853 +},{133,10,798},{132,10,587},{6,11,249},{7,11,1234},{139,11,573},{6,10,598},{7,10 +,42},{8,10,695},{10,10,212},{11,10,158},{14,10,196},{145,10,85},{5,10,957},{5,10 +,1008},{135,0,249},{4,10,129},{135,10,465},{6,0,254},{7,0,842},{7,0,1659},{7,10, +908},{7,10,1201},{9,0,109},{9,10,755},{10,0,103},{11,10,906},{12,10,527},{146,10 +,7},{5,0,262},{136,10,450},{144,0,1},{10,11,201},{142,11,319},{7,11,49},{7,11, +392},{8,11,20},{8,11,172},{8,11,690},{9,11,383},{9,11,845},{10,11,48},{11,11,293 +},{11,11,832},{11,11,920},{141,11,221},{5,11,858},{133,11,992},{134,0,805},{139, +10,1003},{6,0,1630},{134,11,307},{7,11,1512},{135,11,1794},{6,11,268},{137,11,62 +},{135,10,1868},{133,0,671},{4,0,989},{8,0,972},{136,0,998},{132,11,423},{132,0, +889},{135,0,1382},{135,0,1910},{7,10,965},{7,10,1460},{135,10,1604},{4,0,627},{5 +,0,775},{138,11,106},{134,11,348},{7,0,202},{11,0,362},{11,0,948},{140,0,388},{ +138,11,771},{6,11,613},{136,11,223},{6,0,560},{7,0,451},{8,0,389},{12,0,490},{13 +,0,16},{13,0,215},{13,0,351},{18,0,132},{147,0,125},{135,0,841},{136,0,566},{136 +,0,938},{132,11,670},{5,0,912},{6,0,1695},{140,11,55},{9,11,40},{139,11,136},{7, +0,1361},{7,10,982},{10,10,32},{143,10,56},{11,11,259},{140,11,270},{5,0,236},{6, +0,572},{8,0,492},{11,0,618},{144,0,56},{8,11,572},{9,11,310},{9,11,682},{137,11, +698},{134,0,1854},{5,0,190},{136,0,318},{133,10,435},{135,0,1376},{4,11,296},{6, +11,352},{7,11,401},{7,11,1410},{7,11,1594},{7,11,1674},{8,11,63},{8,11,660},{137 +,11,74},{5,10,85},{6,10,419},{7,0,349},{7,10,305},{7,10,361},{7,10,1337},{8,10, +71},{140,10,519},{4,11,139},{4,11,388},{140,11,188},{6,0,1972},{6,0,2013},{8,0, +951},{10,0,947},{10,0,974},{10,0,1018},{142,0,476},{140,10,688},{135,10,740},{5, +10,691},{7,10,345},{9,10,94},{140,10,169},{5,10,183},{6,10,582},{9,0,344},{10,10 +,679},{140,10,435},{135,10,511},{132,0,850},{8,11,441},{10,11,314},{143,11,3},{7 +,10,1993},{136,10,684},{4,11,747},{6,10,583},{6,11,290},{7,11,649},{7,11,1479},{ +135,11,1583},{133,11,232},{133,10,704},{134,0,910},{4,10,179},{5,10,198},{133,10 +,697},{7,10,347},{7,10,971},{8,10,181},{138,10,711},{136,11,525},{14,0,19},{14,0 +,28},{144,0,29},{7,0,85},{7,0,247},{8,0,585},{138,0,163},{4,0,487},{7,11,472},{7 +,11,1801},{10,11,748},{141,11,458},{4,10,243},{5,10,203},{7,10,19},{7,10,71},{7, +10,113},{10,10,405},{11,10,357},{142,10,240},{7,10,1450},{139,10,99},{132,11,425 +},{138,0,145},{147,0,83},{6,10,492},{137,11,247},{4,0,1013},{134,0,2033},{5,10, +134},{6,10,408},{6,10,495},{135,10,1593},{135,0,1922},{134,11,1768},{4,0,124},{ +10,0,457},{11,0,121},{11,0,169},{11,0,870},{11,0,874},{12,0,214},{14,0,187},{143 +,0,77},{5,0,557},{135,0,1457},{139,0,66},{5,11,943},{6,11,1779},{142,10,4},{4,10 +,248},{4,10,665},{7,10,137},{137,10,349},{5,11,245},{6,11,576},{7,0,1193},{7,11, +582},{136,11,225},{144,0,82},{7,10,1270},{139,10,612},{5,0,454},{10,0,352},{138, +11,352},{5,10,371},{7,10,563},{146,0,57},{135,0,1333},{6,0,107},{6,11,610},{7,0, +638},{7,0,1632},{137,0,396},{5,0,370},{134,0,1756},{4,10,374},{7,10,547},{7,10, +1700},{7,10,1833},{139,10,858},{133,0,204},{6,0,1305},{9,10,311},{141,10,42},{5, +0,970},{134,0,1706},{6,10,1647},{7,10,1552},{7,10,2010},{9,10,494},{137,10,509}, +{13,11,455},{15,11,99},{15,11,129},{144,11,68},{135,0,3},{4,0,35},{5,0,121},{5,0 +,483},{5,0,685},{6,0,489},{6,0,782},{6,0,1032},{7,0,1204},{136,0,394},{4,0,921}, +{133,0,1007},{8,11,360},{138,11,63},{135,0,1696},{134,0,1519},{132,11,443},{135, +11,944},{6,10,123},{7,10,214},{9,10,728},{10,10,157},{11,10,346},{11,10,662},{ +143,10,106},{137,0,981},{135,10,1435},{134,0,1072},{132,0,712},{134,0,1629},{134 +,0,728},{4,11,298},{137,11,483},{5,11,164},{6,0,1177},{6,0,1271},{7,11,121},{142 +,11,189},{4,10,707},{5,10,588},{6,10,393},{7,0,1608},{13,10,106},{18,10,49},{147 +,10,41},{23,0,16},{151,11,16},{6,10,211},{7,10,1690},{11,10,486},{140,10,369},{ +133,0,485},{19,11,15},{149,11,27},{4,11,172},{9,11,611},{10,11,436},{12,11,673}, +{141,11,255},{5,11,844},{10,11,484},{11,11,754},{12,11,457},{14,11,171},{14,11, +389},{146,11,153},{4,0,285},{5,0,27},{5,0,317},{6,0,301},{7,0,7},{8,0,153},{10,0 +,766},{11,0,468},{12,0,467},{141,0,143},{134,0,1462},{9,11,263},{10,11,147},{138 +,11,492},{133,11,537},{6,0,1945},{6,0,1986},{6,0,1991},{134,0,2038},{134,10,219} +,{137,11,842},{5,10,582},{6,10,1646},{7,10,99},{7,10,1962},{7,10,1986},{8,10,515 +},{8,10,773},{9,10,23},{9,10,491},{12,10,620},{14,0,52},{14,10,93},{145,0,50},{ +138,11,97},{5,10,851},{20,0,21},{148,0,44},{136,0,819},{139,0,917},{5,11,230},{5 +,11,392},{6,11,420},{8,10,762},{8,10,812},{9,10,910},{9,11,568},{140,11,612},{ +135,0,784},{15,0,135},{143,11,135},{10,0,454},{140,0,324},{4,11,0},{5,11,41},{7, +11,1459},{7,11,1469},{7,11,1618},{7,11,1859},{9,11,549},{139,11,905},{4,10,98},{ +7,10,1365},{9,10,422},{9,10,670},{10,10,775},{11,10,210},{13,10,26},{13,10,457}, +{141,10,476},{6,0,1719},{6,0,1735},{7,0,2016},{7,0,2020},{8,0,837},{137,0,852},{ +133,11,696},{135,0,852},{132,0,952},{134,10,1730},{132,11,771},{138,0,568},{137, +0,448},{139,0,146},{8,0,67},{138,0,419},{133,11,921},{137,10,147},{134,0,1826},{ +10,0,657},{14,0,297},{142,0,361},{6,0,666},{6,0,767},{134,0,1542},{139,0,729},{6 +,11,180},{7,11,1137},{8,11,751},{139,11,805},{4,11,183},{7,11,271},{11,11,824},{ +11,11,952},{13,11,278},{13,11,339},{13,11,482},{14,11,424},{148,11,99},{4,0,669} +,{5,11,477},{5,11,596},{6,11,505},{7,11,1221},{11,11,907},{12,11,209},{141,11, +214},{135,11,1215},{5,0,402},{6,10,30},{11,10,56},{139,10,305},{7,11,564},{142, +11,168},{139,0,152},{7,0,912},{135,10,1614},{4,10,150},{5,10,303},{134,10,327},{ +7,0,320},{8,0,51},{9,0,868},{10,0,833},{12,0,481},{12,0,570},{148,0,106},{132,0, +445},{7,11,274},{11,11,263},{11,11,479},{11,11,507},{140,11,277},{6,11,1645},{8, +10,192},{10,0,555},{10,10,78},{11,0,308},{13,10,359},{147,0,95},{135,10,786},{6, +11,92},{6,11,188},{7,11,1269},{7,11,1524},{7,11,1876},{10,11,228},{139,11,1020}, +{4,11,459},{133,11,966},{6,10,1638},{7,10,79},{7,10,496},{9,10,138},{10,10,336}, +{11,0,386},{12,10,412},{12,10,440},{142,10,305},{133,0,239},{7,0,83},{7,0,1990}, +{8,0,130},{139,0,720},{138,11,709},{4,0,143},{5,0,550},{133,0,752},{5,0,123},{6, +0,530},{7,0,348},{135,0,1419},{135,0,2024},{6,11,18},{7,11,179},{7,11,721},{7,11 +,932},{8,11,548},{8,11,757},{9,11,54},{9,11,65},{9,11,532},{9,11,844},{10,11,113 +},{10,11,117},{10,11,236},{10,11,315},{10,11,430},{10,11,798},{11,11,153},{11,11 +,351},{11,11,375},{12,11,78},{12,11,151},{12,11,392},{14,11,248},{143,11,23},{7, +10,204},{7,10,415},{8,10,42},{10,10,85},{139,10,564},{134,0,958},{133,11,965},{ +132,0,210},{135,11,1429},{138,11,480},{134,11,182},{139,11,345},{10,11,65},{10, +11,488},{138,11,497},{4,10,3},{5,10,247},{5,10,644},{7,10,744},{7,10,1207},{7,10 +,1225},{7,10,1909},{146,10,147},{132,0,430},{5,10,285},{9,10,67},{13,10,473},{ +143,10,82},{144,11,16},{7,11,1162},{9,11,588},{10,11,260},{151,10,8},{133,0,213} +,{138,0,7},{135,0,801},{134,11,1786},{135,11,308},{6,0,936},{134,0,1289},{133,0, +108},{132,0,885},{133,0,219},{139,0,587},{4,0,193},{5,0,916},{6,0,1041},{7,0,364 +},{10,0,398},{10,0,726},{11,0,317},{11,0,626},{12,0,142},{12,0,288},{12,0,678},{ +13,0,313},{15,0,113},{146,0,114},{135,0,1165},{6,0,241},{9,0,342},{10,0,729},{11 +,0,284},{11,0,445},{11,0,651},{11,0,863},{13,0,398},{146,0,99},{7,0,907},{136,0, +832},{4,10,29},{6,10,532},{7,10,1628},{7,10,1648},{9,0,303},{9,10,350},{10,10, +433},{11,10,97},{11,10,557},{11,10,745},{12,10,289},{12,10,335},{12,10,348},{12, +10,606},{13,10,116},{13,10,233},{13,10,466},{14,10,181},{14,10,209},{14,10,232}, +{14,10,236},{14,10,300},{16,10,41},{148,10,97},{7,10,1692},{7,11,423},{136,11, +588},{6,0,931},{134,0,1454},{5,10,501},{7,10,1704},{9,10,553},{11,10,520},{12,10 +,557},{141,10,249},{136,11,287},{4,0,562},{9,0,254},{139,0,879},{132,0,786},{14, +11,32},{18,11,85},{20,11,2},{152,11,16},{135,0,1294},{7,11,723},{135,11,1135},{6 +,0,216},{6,11,403},{7,0,901},{7,0,1343},{136,0,493},{7,11,719},{8,11,809},{136, +11,834},{5,11,210},{6,11,213},{7,11,60},{10,11,364},{139,11,135},{5,11,607},{7,0 +,341},{8,11,326},{8,11,490},{139,0,219},{4,11,701},{5,11,472},{5,11,639},{7,11, +1249},{9,11,758},{139,11,896},{135,11,380},{135,11,1947},{139,0,130},{135,0,1734 +},{10,0,115},{11,0,420},{12,0,154},{13,0,404},{14,0,346},{143,0,54},{134,10,129} +,{4,11,386},{7,11,41},{8,11,405},{9,11,497},{11,11,110},{11,11,360},{15,11,37},{ +144,11,84},{141,11,282},{5,11,46},{7,11,1452},{7,11,1480},{8,11,634},{140,11,472 +},{4,11,524},{136,11,810},{10,11,238},{141,11,33},{133,0,604},{5,0,1011},{136,0, +701},{8,0,856},{8,0,858},{8,0,879},{12,0,702},{142,0,447},{4,0,54},{5,0,666},{7, +0,1039},{7,0,1130},{9,0,195},{138,0,302},{4,10,25},{5,10,60},{6,10,504},{7,10, +614},{7,10,1155},{140,10,0},{7,10,1248},{11,10,621},{139,10,702},{133,11,997},{ +137,10,321},{134,0,1669},{134,0,1791},{4,10,379},{135,10,1397},{138,11,372},{5, +11,782},{5,11,829},{134,11,1738},{135,0,1228},{4,10,118},{6,10,274},{6,10,361},{ +7,10,75},{141,10,441},{132,0,623},{9,11,279},{10,11,407},{14,11,84},{150,11,18}, +{137,10,841},{135,0,798},{140,10,693},{5,10,314},{6,10,221},{7,10,419},{10,10, +650},{11,10,396},{12,10,156},{13,10,369},{14,10,333},{145,10,47},{135,11,1372},{ +7,0,122},{9,0,259},{10,0,84},{11,0,470},{12,0,541},{141,0,379},{134,0,837},{4,11 +,78},{5,11,96},{5,11,182},{7,11,1724},{7,11,1825},{8,0,1013},{10,11,394},{10,11, +471},{11,11,532},{14,11,340},{145,11,88},{134,0,577},{135,11,1964},{132,10,913}, +{134,0,460},{8,0,891},{10,0,901},{10,0,919},{10,0,932},{12,0,715},{12,0,728},{12 +,0,777},{14,0,457},{144,0,103},{5,0,82},{5,0,131},{7,0,1755},{8,0,31},{9,0,168}, +{9,0,764},{139,0,869},{136,10,475},{5,10,1016},{6,0,605},{9,11,601},{9,11,619},{ +10,11,505},{10,11,732},{11,11,355},{140,11,139},{7,10,602},{8,10,179},{10,10,781 +},{140,10,126},{134,0,1246},{6,10,329},{138,10,111},{6,11,215},{7,11,1028},{7,11 +,1473},{7,11,1721},{9,11,424},{138,11,779},{5,0,278},{137,0,68},{6,0,932},{6,0, +1084},{144,0,86},{4,0,163},{5,0,201},{5,0,307},{5,0,310},{6,0,335},{7,0,284},{7, +0,1660},{136,0,165},{136,0,781},{134,0,707},{6,0,33},{135,0,1244},{5,10,821},{6, +10,1687},{6,11,67},{7,11,258},{7,11,1630},{9,11,354},{9,11,675},{10,11,830},{14, +11,80},{145,11,80},{6,11,141},{7,11,225},{9,11,59},{9,11,607},{10,11,312},{11,11 +,687},{12,11,555},{13,11,373},{13,11,494},{148,11,58},{134,0,1113},{5,10,71},{7, +10,1407},{9,0,388},{9,10,704},{10,10,261},{10,10,619},{11,10,547},{11,10,619},{ +143,10,157},{7,0,1953},{136,0,720},{138,0,203},{7,10,2008},{9,10,337},{138,10, +517},{6,0,326},{7,0,677},{137,0,425},{139,11,81},{7,0,1316},{7,0,1412},{7,0,1839 +},{9,0,589},{11,0,241},{11,0,676},{11,0,811},{11,0,891},{12,0,140},{12,0,346},{ +12,0,479},{13,0,140},{13,0,381},{14,0,188},{18,0,30},{148,0,108},{5,0,416},{6,10 +,86},{6,10,603},{7,10,292},{7,10,561},{8,10,257},{8,10,382},{9,10,721},{9,10,778 +},{11,10,581},{140,10,466},{4,10,486},{133,10,491},{134,0,1300},{132,10,72},{6, +10,265},{7,0,847},{7,11,430},{139,11,46},{5,11,602},{6,11,106},{7,11,1786},{7,11 +,1821},{7,11,2018},{9,11,418},{137,11,763},{5,0,358},{7,0,535},{7,0,1184},{10,0, +662},{13,0,212},{13,0,304},{13,0,333},{145,0,98},{5,11,65},{6,11,416},{7,11,1720 +},{7,11,1924},{8,11,677},{10,11,109},{11,11,14},{11,11,70},{11,11,569},{11,11, +735},{15,11,153},{148,11,80},{6,0,1823},{8,0,839},{8,0,852},{8,0,903},{10,0,940} +,{12,0,707},{140,0,775},{135,11,1229},{6,0,1522},{140,0,654},{136,11,595},{139,0 +,163},{141,0,314},{132,0,978},{4,0,601},{6,0,2035},{137,10,234},{5,10,815},{6,10 +,1688},{134,10,1755},{133,0,946},{136,0,434},{6,10,197},{136,10,205},{7,0,411},{ +7,0,590},{8,0,631},{9,0,323},{10,0,355},{11,0,491},{12,0,143},{12,0,402},{13,0, +73},{14,0,408},{15,0,107},{146,0,71},{7,0,1467},{8,0,328},{10,0,544},{11,0,955}, +{12,0,13},{13,0,320},{145,0,83},{142,0,410},{11,0,511},{13,0,394},{14,0,298},{14 +,0,318},{146,0,103},{6,10,452},{7,10,312},{138,10,219},{138,10,589},{4,10,333},{ +9,10,176},{12,10,353},{141,10,187},{135,11,329},{132,11,469},{5,0,835},{134,0, +483},{134,11,1743},{5,11,929},{6,11,340},{8,11,376},{136,11,807},{134,10,1685},{ +132,0,677},{5,11,218},{7,11,1610},{138,11,83},{5,11,571},{135,11,1842},{132,11, +455},{137,0,70},{135,0,1405},{7,10,135},{8,10,7},{8,10,62},{9,10,243},{10,10,658 +},{10,10,697},{11,10,456},{139,10,756},{9,10,395},{138,10,79},{137,0,108},{6,11, +161},{7,11,372},{137,11,597},{132,11,349},{132,0,777},{132,0,331},{135,10,631},{ +133,0,747},{6,11,432},{6,11,608},{139,11,322},{138,10,835},{5,11,468},{7,11,1809 +},{10,11,325},{11,11,856},{12,11,345},{143,11,104},{133,11,223},{7,10,406},{7,10 +,459},{8,10,606},{139,10,726},{132,11,566},{142,0,68},{4,11,59},{135,11,1394},{6 +,11,436},{139,11,481},{4,11,48},{5,11,271},{135,11,953},{139,11,170},{5,11,610}, +{136,11,457},{133,11,755},{135,11,1217},{133,10,612},{132,11,197},{132,0,505},{4 +,10,372},{7,10,482},{8,10,158},{9,10,602},{9,10,615},{10,10,245},{10,10,678},{10 +,10,744},{11,10,248},{139,10,806},{133,0,326},{5,10,854},{135,10,1991},{4,0,691} +,{146,0,16},{6,0,628},{9,0,35},{10,0,680},{10,0,793},{11,0,364},{13,0,357},{143, +0,164},{138,0,654},{6,0,32},{7,0,385},{7,0,757},{7,0,1916},{8,0,37},{8,0,94},{8, +0,711},{9,0,541},{10,0,162},{10,0,795},{11,0,989},{11,0,1010},{12,0,14},{142,0, +308},{133,11,217},{6,0,152},{6,0,349},{6,0,1682},{7,0,1252},{8,0,112},{9,0,435}, +{9,0,668},{10,0,290},{10,0,319},{10,0,815},{11,0,180},{11,0,837},{12,0,240},{13, +0,152},{13,0,219},{142,0,158},{4,0,581},{134,0,726},{5,10,195},{135,10,1685},{6, +0,126},{7,0,573},{8,0,397},{142,0,44},{138,0,89},{7,10,1997},{8,10,730},{139,10, +1006},{134,0,1531},{134,0,1167},{5,0,926},{5,10,751},{140,0,203},{4,11,165},{7, +11,1398},{135,11,1829},{7,0,1232},{137,0,531},{135,10,821},{134,0,943},{133,0, +670},{4,0,880},{139,0,231},{134,0,1617},{135,0,1957},{5,11,9},{7,11,297},{7,11, +966},{140,11,306},{6,0,975},{134,0,985},{5,10,950},{5,10,994},{134,10,351},{12, +11,21},{151,11,7},{5,11,146},{6,11,411},{138,11,721},{7,0,242},{135,0,1942},{6, +11,177},{135,11,467},{5,0,421},{7,10,47},{137,10,684},{5,0,834},{7,0,1202},{8,0, +14},{9,0,481},{137,0,880},{138,0,465},{4,10,350},{6,0,688},{137,0,834},{132,0, +855},{4,0,357},{6,0,172},{7,0,143},{137,0,413},{133,11,200},{132,0,590},{7,10, +1812},{13,10,259},{13,10,356},{14,10,242},{147,10,114},{133,10,967},{4,10,473},{ +7,10,623},{8,10,808},{9,10,871},{9,10,893},{11,0,114},{11,10,431},{12,10,112},{ +12,10,217},{12,10,243},{12,10,562},{12,10,663},{12,10,683},{13,10,141},{13,10, +197},{13,10,227},{13,10,406},{13,10,487},{14,10,156},{14,10,203},{14,10,224},{14 +,10,256},{18,10,58},{150,10,0},{138,10,286},{4,10,222},{7,10,286},{136,10,629},{ +5,0,169},{7,0,333},{136,0,45},{134,11,481},{132,0,198},{4,0,24},{4,11,84},{5,0, +140},{5,0,185},{7,0,1500},{7,11,1482},{10,11,76},{10,11,142},{11,0,565},{139,0, +838},{133,0,585},{141,10,306},{133,11,1015},{4,11,315},{5,11,507},{135,11,1370}, +{136,10,146},{6,0,691},{134,0,1503},{4,0,334},{133,0,593},{4,10,465},{135,10, +1663},{142,11,173},{135,0,913},{6,11,1722},{140,0,116},{134,0,1360},{132,0,802}, +{8,11,222},{8,11,476},{9,11,238},{11,11,516},{11,11,575},{15,11,109},{146,11,100 +},{6,0,308},{7,10,138},{7,10,517},{9,0,673},{139,10,238},{132,0,709},{6,0,1876}, +{6,0,1895},{9,0,994},{9,0,1006},{12,0,829},{12,0,888},{12,0,891},{146,0,185},{ +148,10,94},{4,0,228},{133,0,897},{5,10,495},{7,0,1840},{7,10,834},{9,10,733},{ +139,10,378},{133,10,559},{6,10,21},{6,10,1737},{7,10,1444},{136,10,224},{4,0,608 +},{133,0,497},{6,11,40},{135,11,1781},{134,0,1573},{135,0,2039},{6,0,540},{136,0 +,136},{4,0,897},{5,0,786},{133,10,519},{6,0,1878},{6,0,1884},{9,0,938},{9,0,948} +,{9,0,955},{9,0,973},{9,0,1012},{12,0,895},{12,0,927},{143,0,254},{134,0,1469},{ +133,0,999},{4,0,299},{135,0,1004},{4,0,745},{133,0,578},{136,11,574},{133,0,456} +,{134,0,1457},{4,10,402},{135,0,1679},{7,0,693},{8,0,180},{8,10,323},{8,10,479}, +{140,0,163},{11,10,580},{142,10,201},{5,10,59},{135,10,672},{132,11,354},{146,10 +,34},{4,0,755},{135,11,1558},{7,0,1740},{146,0,48},{4,10,85},{135,10,549},{139,0 +,338},{133,10,94},{134,0,1091},{135,11,469},{5,11,830},{12,0,695},{12,0,704},{14 +,11,338},{20,0,113},{148,11,81},{135,0,1464},{6,10,11},{135,10,187},{135,0,975}, +{4,10,522},{141,0,335},{134,0,1979},{5,11,496},{135,11,203},{4,10,52},{135,10, +661},{7,0,1566},{8,0,269},{9,0,212},{9,0,718},{14,0,15},{14,0,132},{142,0,227},{ +4,0,890},{4,10,383},{5,0,805},{5,0,819},{5,0,961},{5,10,520},{6,0,396},{6,0,1631 +},{6,0,1678},{7,0,1967},{7,0,2041},{9,0,630},{11,0,8},{11,0,1019},{12,0,176},{13 +,0,225},{14,0,292},{149,0,24},{134,11,547},{135,11,1748},{5,11,88},{137,11,239}, +{146,11,128},{7,11,650},{135,11,1310},{4,10,281},{5,10,38},{7,10,194},{7,10,668} +,{7,10,1893},{137,10,397},{135,0,1815},{9,10,635},{139,10,559},{7,0,1505},{10,0, +190},{10,0,634},{11,0,792},{12,0,358},{140,0,447},{5,0,0},{6,0,536},{7,0,604},{ +13,0,445},{145,0,126},{7,11,1076},{9,11,80},{11,11,78},{11,11,421},{11,11,534},{ +140,11,545},{8,0,966},{10,0,1023},{14,11,369},{146,11,72},{135,11,1641},{6,0,232 +},{6,0,412},{7,0,1074},{8,0,9},{8,0,157},{8,0,786},{9,0,196},{9,0,352},{9,0,457} +,{10,0,337},{11,0,232},{11,0,877},{12,0,480},{140,0,546},{135,0,958},{4,0,382},{ +136,0,579},{4,0,212},{135,0,1206},{4,11,497},{5,11,657},{135,11,1584},{132,0,681 +},{8,0,971},{138,0,965},{5,10,448},{136,10,535},{14,0,16},{146,0,44},{11,0,584}, +{11,0,616},{11,11,584},{11,11,616},{14,0,275},{142,11,275},{136,11,13},{7,10,610 +},{135,10,1501},{7,11,642},{8,11,250},{11,11,123},{11,11,137},{13,11,48},{142,11 +,95},{133,0,655},{17,0,67},{147,0,74},{134,0,751},{134,0,1967},{6,0,231},{136,0, +423},{5,0,300},{138,0,1016},{4,10,319},{5,10,699},{138,10,673},{6,0,237},{6,10, +336},{7,0,611},{8,0,100},{8,10,552},{9,0,416},{9,10,285},{10,10,99},{11,0,335},{ +11,10,568},{12,0,173},{146,0,101},{134,0,1370},{7,10,1406},{9,10,218},{141,10, +222},{133,10,256},{135,0,1208},{14,11,213},{148,11,38},{6,0,1219},{135,11,1642}, +{13,0,417},{14,0,129},{143,0,15},{10,11,545},{140,11,301},{17,10,39},{148,10,36} +,{133,0,199},{4,11,904},{133,11,794},{12,0,427},{146,0,38},{134,0,949},{7,10,634 +},{136,0,665},{132,10,618},{135,10,259},{132,10,339},{133,11,761},{141,10,169},{ +132,10,759},{5,0,688},{7,0,539},{135,0,712},{7,11,386},{138,11,713},{134,0,1186} +,{6,11,7},{6,11,35},{7,11,147},{7,11,1069},{7,11,1568},{7,11,1575},{7,11,1917},{ +8,11,43},{8,11,208},{9,11,128},{9,11,866},{10,11,20},{11,11,981},{147,11,33},{7, +11,893},{8,10,482},{141,11,424},{6,0,312},{6,0,1715},{10,0,584},{11,0,546},{11,0 +,692},{12,0,259},{12,0,295},{13,0,46},{141,0,154},{5,10,336},{6,10,341},{6,10, +478},{6,10,1763},{136,10,386},{137,0,151},{132,0,588},{152,0,4},{6,11,322},{9,11 +,552},{11,11,274},{13,11,209},{13,11,499},{14,11,85},{15,11,126},{145,11,70},{ +135,10,73},{4,0,231},{5,0,61},{6,0,104},{7,0,729},{7,0,964},{7,0,1658},{140,0, +414},{6,0,263},{138,0,757},{135,10,1971},{4,0,612},{133,0,561},{132,0,320},{135, +10,1344},{8,11,83},{8,11,817},{9,11,28},{9,11,29},{9,11,885},{10,11,387},{11,11, +633},{11,11,740},{13,11,235},{13,11,254},{15,11,143},{143,11,146},{5,10,396},{ +134,10,501},{140,11,49},{132,0,225},{4,10,929},{5,10,799},{8,10,46},{136,10,740} +,{4,0,405},{7,0,817},{14,0,58},{17,0,37},{146,0,124},{133,0,974},{4,11,412},{133 +,11,581},{4,10,892},{133,10,770},{4,0,996},{134,0,2026},{4,0,527},{5,0,235},{7,0 +,1239},{11,0,131},{140,0,370},{7,11,421},{9,0,16},{141,0,386},{7,0,956},{7,0, +1157},{7,0,1506},{7,0,1606},{7,0,1615},{7,0,1619},{7,0,1736},{7,0,1775},{8,0,590 +},{9,0,324},{9,0,736},{9,0,774},{9,0,776},{9,0,784},{10,0,567},{10,0,708},{11,0, +518},{11,0,613},{11,0,695},{11,0,716},{11,0,739},{11,0,770},{11,0,771},{11,0,848 +},{11,0,857},{11,0,931},{11,0,947},{12,0,326},{12,0,387},{12,0,484},{12,0,528},{ +12,0,552},{12,0,613},{13,0,189},{13,0,256},{13,0,340},{13,0,432},{13,0,436},{13, +0,440},{13,0,454},{14,0,174},{14,0,220},{14,0,284},{14,0,390},{145,0,121},{135, +10,158},{9,0,137},{138,0,221},{4,11,110},{10,11,415},{10,11,597},{142,11,206},{ +141,11,496},{135,11,205},{151,10,25},{135,11,778},{7,10,2001},{7,11,1656},{9,11, +369},{10,11,338},{10,11,490},{11,11,154},{11,11,545},{11,11,775},{13,11,77},{141 +,11,274},{4,11,444},{10,11,146},{140,11,9},{7,0,390},{138,0,140},{135,0,1144},{ +134,0,464},{7,10,1461},{140,10,91},{132,10,602},{4,11,283},{135,11,1194},{5,0, +407},{11,0,204},{11,0,243},{11,0,489},{12,0,293},{19,0,37},{20,0,73},{150,0,38}, +{7,0,1218},{136,0,303},{4,10,13},{5,0,325},{5,10,567},{7,10,1498},{8,0,5},{8,0, +227},{9,0,105},{9,10,124},{10,0,585},{11,10,521},{12,0,614},{140,10,405},{135,10 +,1006},{6,11,1720},{7,0,800},{138,0,12},{135,0,1783},{132,10,735},{138,10,812},{ +4,10,170},{135,10,323},{6,0,621},{13,0,504},{144,0,89},{5,10,304},{135,10,1403}, +{137,11,216},{6,0,920},{6,0,1104},{9,11,183},{139,11,286},{4,0,376},{133,10,742} +,{134,0,218},{8,0,641},{11,0,388},{140,0,580},{7,0,454},{7,0,782},{8,0,768},{140 +,0,686},{137,11,33},{133,10,111},{144,0,0},{10,0,676},{140,0,462},{6,0,164},{136 +,11,735},{133,10,444},{150,0,50},{7,11,1862},{12,11,491},{12,11,520},{13,11,383} +,{14,11,244},{146,11,12},{5,11,132},{9,11,486},{9,11,715},{10,11,458},{11,11,373 +},{11,11,668},{11,11,795},{11,11,897},{12,11,272},{12,11,424},{12,11,539},{12,11 +,558},{14,11,245},{14,11,263},{14,11,264},{14,11,393},{142,11,403},{8,10,123},{ +15,10,6},{144,10,7},{6,0,285},{8,0,654},{11,0,749},{12,0,190},{12,0,327},{13,0, +120},{13,0,121},{13,0,327},{15,0,47},{146,0,40},{5,11,8},{6,11,89},{6,11,400},{7 +,11,1569},{7,11,1623},{7,11,1850},{8,11,218},{8,11,422},{9,11,570},{138,11,626}, +{6,11,387},{7,11,882},{141,11,111},{6,0,343},{6,11,224},{7,0,195},{7,11,877},{9, +0,226},{9,11,647},{10,0,197},{10,0,575},{11,0,502},{139,0,899},{5,10,937},{135, +10,100},{135,11,790},{150,0,29},{147,0,8},{134,0,1812},{149,0,8},{135,11,394},{7 +,0,1125},{9,0,143},{11,0,61},{14,0,405},{150,0,21},{10,11,755},{147,11,29},{9,11 +,378},{141,11,162},{135,10,922},{5,10,619},{133,10,698},{134,0,1327},{6,0,1598}, +{137,0,575},{9,11,569},{12,11,12},{12,11,81},{12,11,319},{13,11,69},{14,11,259}, +{16,11,87},{17,11,1},{17,11,21},{17,11,24},{18,11,15},{18,11,56},{18,11,59},{18, +11,127},{18,11,154},{19,11,19},{148,11,31},{6,0,895},{135,11,1231},{5,0,959},{7, +11,124},{136,11,38},{5,11,261},{7,11,78},{7,11,199},{8,11,815},{9,11,126},{138, +11,342},{5,10,917},{134,10,1659},{5,11,595},{7,0,1759},{135,11,1863},{136,0,173} +,{134,0,266},{142,0,261},{132,11,628},{5,10,251},{5,10,956},{8,10,268},{9,10,214 +},{146,10,142},{7,11,266},{136,11,804},{135,11,208},{6,11,79},{7,11,1021},{135, +11,1519},{11,11,704},{141,11,396},{5,10,346},{5,10,711},{136,10,390},{136,11,741 +},{134,11,376},{134,0,1427},{6,0,1033},{6,0,1217},{136,0,300},{133,10,624},{6,11 +,100},{7,11,244},{7,11,632},{7,11,1609},{8,11,178},{8,11,638},{141,11,58},{5,10, +783},{6,0,584},{7,10,1998},{135,10,2047},{5,0,427},{5,0,734},{7,0,478},{136,0,52 +},{7,0,239},{11,0,217},{142,0,165},{134,0,1129},{6,0,168},{6,0,1734},{7,0,20},{7 +,0,1056},{8,0,732},{9,0,406},{9,0,911},{138,0,694},{132,10,594},{133,11,791},{7, +11,686},{8,11,33},{8,11,238},{10,11,616},{11,11,467},{11,11,881},{13,11,217},{13 +,11,253},{142,11,268},{137,11,476},{134,0,418},{133,0,613},{132,0,632},{132,11, +447},{7,0,32},{7,0,984},{8,0,85},{8,0,709},{9,0,579},{9,0,847},{9,0,856},{10,0, +799},{11,0,258},{11,0,1007},{12,0,331},{12,0,615},{13,0,188},{13,0,435},{14,0,8} +,{15,0,165},{16,0,27},{16,11,35},{148,0,40},{4,11,128},{5,11,415},{6,11,462},{7, +11,294},{7,11,578},{10,11,710},{139,11,86},{5,0,694},{136,0,909},{5,10,37},{6,10 +,39},{6,10,451},{7,0,1109},{7,10,218},{7,10,1166},{7,10,1687},{8,10,662},{11,0,7 +},{144,10,2},{136,11,587},{6,11,427},{7,11,1018},{138,11,692},{4,11,195},{6,10, +508},{135,11,802},{4,0,167},{135,0,82},{5,0,62},{6,0,24},{6,0,534},{7,0,74},{7,0 +,678},{7,0,684},{7,0,1043},{7,0,1072},{8,0,280},{8,0,541},{8,0,686},{9,0,258},{ +10,0,519},{11,0,252},{140,0,282},{138,0,33},{4,0,359},{133,11,738},{7,0,980},{7, +10,635},{7,10,796},{8,10,331},{9,0,328},{9,10,330},{9,10,865},{10,10,119},{10,10 +,235},{11,10,111},{11,10,129},{11,10,240},{12,10,31},{12,10,66},{12,10,222},{12, +10,269},{12,10,599},{12,10,684},{12,10,689},{12,10,691},{13,0,186},{13,0,364},{ +142,10,345},{137,10,527},{6,0,596},{7,0,585},{135,10,702},{134,11,1683},{133,0, +211},{6,0,145},{141,0,336},{134,0,1130},{6,10,37},{7,0,873},{7,10,1666},{8,10, +195},{8,10,316},{9,10,178},{9,10,276},{9,10,339},{9,10,536},{10,10,102},{10,10, +362},{10,10,785},{11,10,55},{11,10,149},{11,10,773},{13,10,416},{13,10,419},{14, +10,38},{14,10,41},{142,10,210},{8,0,840},{136,0,841},{132,0,263},{5,11,3},{8,11, +578},{9,11,118},{10,11,705},{12,11,383},{141,11,279},{132,0,916},{133,11,229},{ +133,10,645},{8,11,102},{10,11,578},{10,11,672},{12,11,496},{13,11,408},{14,11, +121},{15,0,155},{16,0,79},{145,11,106},{4,0,599},{5,0,592},{6,0,1634},{7,0,5},{7 +,0,55},{7,0,67},{7,0,97},{7,0,691},{7,0,979},{7,0,1600},{7,0,1697},{8,0,207},{8, +0,214},{8,0,231},{8,0,294},{8,0,336},{8,0,428},{8,0,471},{8,0,622},{8,0,626},{8, +0,679},{8,0,759},{8,0,829},{9,0,11},{9,0,246},{9,0,484},{9,0,573},{9,0,706},{9,0 +,762},{9,0,798},{9,0,855},{9,0,870},{9,0,912},{10,0,303},{10,0,335},{10,0,424},{ +10,0,461},{10,0,543},{10,0,759},{10,0,814},{11,0,59},{11,0,199},{11,0,235},{11,0 +,590},{11,0,631},{11,0,929},{11,0,963},{11,0,987},{12,0,114},{12,0,182},{12,0, +226},{12,0,332},{12,0,439},{12,0,575},{12,0,598},{12,0,675},{13,0,8},{13,0,125}, +{13,0,194},{13,0,287},{14,0,197},{14,0,383},{15,0,53},{17,0,63},{19,0,46},{19,0, +98},{19,0,106},{148,0,85},{4,10,290},{135,0,1356},{6,10,70},{7,10,1292},{10,10, +762},{139,10,288},{150,11,55},{4,0,593},{8,11,115},{8,11,350},{9,11,489},{10,11, +128},{11,11,306},{12,11,373},{14,11,30},{17,11,79},{147,11,80},{135,11,1235},{ +134,0,1392},{4,11,230},{133,11,702},{147,0,126},{7,10,131},{7,10,422},{8,10,210} +,{140,10,573},{134,0,1179},{139,11,435},{139,10,797},{134,11,1728},{4,0,162},{18 +,11,26},{19,11,42},{20,11,43},{21,11,0},{23,11,27},{152,11,14},{132,10,936},{5, +10,453},{6,0,765},{134,10,441},{133,0,187},{135,0,1286},{6,0,635},{6,0,904},{6,0 +,1210},{134,0,1489},{4,0,215},{8,0,890},{9,0,38},{10,0,923},{11,0,23},{11,0,127} +,{139,0,796},{6,0,1165},{134,0,1306},{7,0,716},{13,0,97},{141,0,251},{132,10,653 +},{136,0,657},{146,10,80},{5,11,622},{7,11,1032},{11,11,26},{11,11,213},{11,11, +707},{12,11,380},{13,11,226},{141,11,355},{5,11,70},{6,0,299},{6,11,334},{9,11, +171},{11,11,637},{12,11,202},{14,11,222},{145,11,42},{142,0,134},{4,11,23},{5,11 +,313},{5,11,1014},{6,11,50},{6,11,51},{7,11,142},{7,11,384},{9,11,783},{139,11, +741},{4,11,141},{7,11,559},{8,11,640},{9,11,460},{12,11,183},{141,11,488},{136, +11,614},{7,10,1368},{8,10,232},{8,10,361},{10,10,682},{138,10,742},{137,10,534}, +{6,0,1082},{140,0,658},{137,10,27},{135,0,2002},{142,10,12},{4,0,28},{5,0,440},{ +7,0,248},{11,0,833},{140,0,344},{7,10,736},{139,10,264},{134,10,1657},{134,0, +1654},{138,0,531},{5,11,222},{9,11,140},{138,11,534},{6,0,634},{6,0,798},{134,0, +840},{138,11,503},{135,10,127},{133,0,853},{5,11,154},{7,11,1491},{10,11,379},{ +138,11,485},{6,0,249},{7,0,1234},{139,0,573},{133,11,716},{7,11,1570},{140,11, +542},{136,10,364},{138,0,527},{4,11,91},{5,11,388},{5,11,845},{6,11,206},{6,11, +252},{6,11,365},{7,11,136},{7,11,531},{8,11,264},{136,11,621},{134,0,1419},{135, +11,1441},{7,0,49},{7,0,392},{8,0,20},{8,0,172},{8,0,690},{9,0,383},{9,0,845},{10 +,0,48},{11,0,293},{11,0,832},{11,0,920},{11,0,984},{141,0,221},{5,0,858},{133,0, +992},{5,0,728},{137,10,792},{5,10,909},{9,10,849},{138,10,805},{7,0,525},{7,0, +1579},{8,0,497},{136,0,573},{6,0,268},{137,0,62},{135,11,576},{134,0,1201},{5,11 +,771},{5,11,863},{5,11,898},{6,11,1632},{6,11,1644},{134,11,1780},{133,11,331},{ +7,0,193},{7,0,1105},{7,10,397},{8,10,124},{8,10,619},{9,10,305},{10,0,495},{11, +10,40},{12,10,349},{13,10,134},{13,10,295},{14,10,155},{15,10,120},{146,10,105}, +{138,0,106},{5,11,107},{6,0,859},{7,11,201},{136,11,518},{6,11,446},{135,11,1817 +},{4,10,262},{7,10,342},{141,0,23},{133,10,641},{137,11,851},{6,0,925},{137,0, +813},{132,11,504},{6,0,613},{136,0,223},{4,10,99},{6,10,250},{6,10,346},{8,10, +127},{138,10,81},{136,0,953},{132,10,915},{139,11,892},{5,10,75},{9,10,517},{10, +10,470},{12,10,155},{141,10,224},{4,0,666},{7,0,1017},{7,11,996},{138,11,390},{5 +,11,883},{133,11,975},{14,10,83},{142,11,83},{4,0,670},{5,11,922},{134,11,1707}, +{135,0,216},{7,11,787},{9,0,40},{139,0,136},{5,10,954},{5,11,993},{7,11,515},{ +137,11,91},{139,0,259},{6,10,304},{7,0,1114},{8,10,418},{9,0,310},{9,0,682},{10, +0,440},{11,10,341},{11,10,675},{141,0,40},{9,10,410},{11,10,425},{142,0,296},{10 +,11,377},{12,11,363},{13,11,68},{13,11,94},{14,11,108},{142,11,306},{7,0,1401},{ +135,0,1476},{4,0,296},{6,0,475},{7,0,401},{7,0,1410},{7,0,1594},{7,0,1674},{8,0, +63},{8,0,660},{137,0,74},{4,0,139},{4,0,388},{140,0,188},{132,0,797},{132,11,766 +},{5,11,103},{7,11,921},{8,11,580},{8,11,593},{8,11,630},{138,11,28},{4,11,911}, +{5,11,867},{133,11,1013},{134,10,14},{134,0,1572},{134,10,1708},{5,10,113},{6,10 +,243},{7,10,1865},{11,10,161},{16,10,37},{17,10,99},{149,0,39},{7,11,1563},{141, +11,182},{5,11,135},{6,11,519},{7,11,1722},{10,11,271},{11,11,261},{145,11,54},{ +132,10,274},{134,0,1594},{4,11,300},{5,11,436},{135,11,484},{4,0,747},{6,0,290}, +{7,0,649},{7,0,1479},{135,0,1583},{133,11,535},{147,11,82},{133,0,232},{137,0, +887},{135,10,166},{136,0,521},{4,0,14},{7,0,472},{7,0,1801},{10,0,748},{141,0, +458},{134,0,741},{134,0,992},{9,10,304},{144,0,111},{4,0,425},{5,11,387},{7,11, +557},{12,11,547},{142,11,86},{135,11,1747},{5,10,654},{135,11,1489},{4,11,6},{5, +11,708},{7,0,789},{136,11,75},{6,10,273},{10,10,188},{13,10,377},{146,10,77},{4, +11,303},{6,0,1593},{7,11,619},{10,11,547},{10,11,687},{11,11,122},{140,11,601},{ +134,0,1768},{135,10,410},{138,11,772},{11,0,233},{139,10,524},{5,0,943},{134,0, +1779},{134,10,1785},{136,11,529},{132,0,955},{5,0,245},{6,0,576},{7,0,582},{136, +0,225},{132,10,780},{142,0,241},{134,0,1943},{4,11,106},{7,11,310},{7,11,1785},{ +10,11,690},{139,11,717},{134,0,1284},{5,11,890},{133,11,988},{6,11,626},{142,11, +431},{10,11,706},{145,11,32},{137,11,332},{132,11,698},{135,0,709},{5,10,948},{ +138,11,17},{136,0,554},{134,0,1564},{139,10,941},{132,0,443},{134,0,909},{134,11 +,84},{142,0,280},{4,10,532},{5,10,706},{135,10,662},{132,0,729},{5,10,837},{6,10 +,1651},{139,10,985},{135,10,1861},{4,0,348},{152,11,3},{5,11,986},{6,11,130},{7, +11,1582},{8,11,458},{10,11,101},{10,11,318},{138,11,823},{134,0,758},{4,0,298},{ +137,0,848},{4,10,330},{7,10,933},{7,10,2012},{136,10,292},{7,11,1644},{137,11, +129},{6,0,1422},{7,10,767},{137,0,829},{5,0,164},{7,0,121},{142,0,189},{7,0,812} +,{7,0,1261},{7,0,1360},{9,0,632},{140,0,352},{135,11,1788},{139,0,556},{135,11, +997},{145,10,114},{4,0,172},{9,0,611},{9,10,883},{10,0,436},{12,0,673},{141,0, +255},{10,10,274},{139,0,530},{133,0,844},{134,0,984},{4,10,703},{7,10,207},{13,0 +,232},{146,0,35},{132,10,571},{9,0,263},{10,0,147},{138,0,492},{7,11,1756},{137, +11,98},{5,10,873},{5,10,960},{8,10,823},{137,10,881},{133,0,537},{132,0,859},{7, +11,1046},{139,11,160},{137,0,842},{139,10,283},{5,10,33},{6,10,470},{139,10,424} +,{6,11,45},{7,11,433},{8,11,129},{9,11,21},{10,11,392},{11,11,79},{12,11,499},{ +13,11,199},{141,11,451},{135,0,1291},{135,10,1882},{7,11,558},{136,11,353},{134, +0,1482},{5,0,230},{5,0,392},{6,0,420},{9,0,568},{140,0,612},{6,0,262},{7,10,90}, +{7,10,664},{7,10,830},{7,10,1380},{7,10,2025},{8,10,448},{8,10,828},{8,11,81},{9 +,11,189},{9,11,201},{11,11,478},{11,11,712},{141,11,338},{142,0,31},{5,11,353},{ +151,11,26},{132,0,753},{4,0,0},{5,0,41},{7,0,1459},{7,0,1469},{7,0,1859},{9,0, +549},{139,0,905},{9,10,417},{137,10,493},{135,11,1113},{133,0,696},{141,11,448}, +{134,10,295},{132,0,834},{4,0,771},{5,10,1019},{6,11,25},{7,11,855},{7,11,1258}, +{144,11,32},{134,0,1076},{133,0,921},{133,0,674},{4,11,4},{7,11,1118},{7,11,1320 +},{7,11,1706},{8,11,277},{9,11,622},{10,11,9},{11,11,724},{12,11,350},{12,11,397 +},{13,11,28},{13,11,159},{15,11,89},{18,11,5},{19,11,9},{20,11,34},{150,11,47},{ +134,10,208},{6,0,444},{136,0,308},{6,0,180},{7,0,1137},{8,0,751},{139,0,805},{4, +0,183},{7,0,271},{11,0,824},{11,0,952},{13,0,278},{13,0,339},{13,0,482},{14,0, +424},{148,0,99},{7,11,317},{135,11,569},{4,0,19},{5,0,477},{5,0,596},{6,0,505},{ +7,0,1221},{11,0,907},{12,0,209},{141,0,214},{135,0,1215},{6,0,271},{7,0,398},{7, +10,448},{7,10,1629},{7,10,1813},{8,0,387},{8,10,442},{9,10,710},{10,0,344},{10, +10,282},{138,10,722},{11,10,844},{12,10,104},{140,10,625},{134,11,255},{133,10, +787},{134,0,1645},{11,11,956},{151,11,3},{6,0,92},{6,0,188},{7,0,209},{7,0,1269} +,{7,0,1524},{7,0,1876},{8,0,661},{10,0,42},{10,0,228},{11,0,58},{11,0,1020},{12, +0,58},{12,0,118},{141,0,32},{4,0,459},{133,0,966},{4,11,536},{7,11,1141},{10,11, +723},{139,11,371},{140,0,330},{134,0,1557},{7,11,285},{135,11,876},{136,10,491}, +{135,11,560},{6,0,18},{7,0,179},{7,0,932},{8,0,548},{8,0,757},{9,0,54},{9,0,65}, +{9,0,532},{9,0,844},{10,0,113},{10,0,117},{10,0,315},{10,0,560},{10,0,622},{10,0 +,798},{11,0,153},{11,0,351},{11,0,375},{12,0,78},{12,0,151},{12,0,392},{12,0,666 +},{14,0,248},{143,0,23},{4,11,690},{134,0,1742},{4,10,403},{5,10,441},{7,10,450} +,{10,10,840},{11,10,101},{12,10,193},{141,10,430},{133,0,965},{134,0,182},{10,0, +65},{10,0,488},{138,0,497},{135,11,1346},{6,0,973},{6,0,1158},{10,11,200},{19,11 +,2},{151,11,22},{4,11,190},{133,11,554},{133,10,679},{7,0,328},{137,10,326},{133 +,11,1001},{9,0,588},{138,0,260},{133,11,446},{135,10,1128},{135,10,1796},{147,11 +,119},{134,0,1786},{6,0,1328},{6,0,1985},{8,0,962},{138,0,1017},{135,0,308},{4, +10,574},{7,10,350},{7,10,1024},{8,10,338},{9,10,677},{10,10,808},{139,0,508},{ +138,11,752},{135,10,1081},{137,11,96},{7,10,1676},{135,10,2037},{136,0,588},{132 +,11,304},{133,0,614},{140,0,793},{136,0,287},{137,10,297},{141,10,37},{6,11,53}, +{6,11,199},{7,11,1408},{8,11,32},{8,11,93},{9,11,437},{10,11,397},{10,11,629},{ +11,11,593},{11,11,763},{13,11,326},{145,11,35},{134,11,105},{9,11,320},{10,11, +506},{138,11,794},{5,11,114},{5,11,255},{141,11,285},{140,0,290},{7,11,2035},{8, +11,19},{9,11,89},{138,11,831},{134,0,1136},{6,10,306},{7,0,719},{7,10,1140},{7, +10,1340},{8,0,796},{8,0,809},{8,0,834},{8,10,133},{138,10,449},{139,10,1011},{5, +0,210},{6,0,213},{7,0,60},{10,0,364},{139,0,135},{5,0,607},{8,0,326},{136,0,490} +,{138,11,176},{132,0,701},{5,0,472},{7,0,380},{137,0,758},{135,0,1947},{6,0,1079 +},{138,0,278},{138,11,391},{5,10,329},{8,10,260},{139,11,156},{4,0,386},{7,0,41} +,{8,0,405},{8,0,728},{9,0,497},{11,0,110},{11,0,360},{15,0,37},{144,0,84},{5,0, +46},{7,0,1452},{7,0,1480},{8,0,634},{140,0,472},{136,0,961},{4,0,524},{136,0,810 +},{10,0,238},{141,0,33},{132,10,657},{152,10,7},{133,0,532},{5,0,997},{135,10, +1665},{7,11,594},{7,11,851},{7,11,1858},{9,11,411},{9,11,574},{9,11,666},{9,11, +737},{10,11,346},{10,11,712},{11,11,246},{11,11,432},{11,11,517},{11,11,647},{11 +,11,679},{11,11,727},{12,11,304},{12,11,305},{12,11,323},{12,11,483},{12,11,572} +,{12,11,593},{12,11,602},{13,11,95},{13,11,101},{13,11,171},{13,11,315},{13,11, +378},{13,11,425},{13,11,475},{14,11,63},{14,11,380},{14,11,384},{15,11,133},{18, +11,112},{148,11,72},{5,11,955},{136,11,814},{134,0,1301},{5,10,66},{7,10,1896},{ +136,10,288},{133,11,56},{134,10,1643},{6,0,1298},{148,11,100},{5,0,782},{5,0,829 +},{6,0,671},{6,0,1156},{6,0,1738},{137,11,621},{4,0,306},{5,0,570},{5,10,91},{5, +10,648},{5,10,750},{5,10,781},{6,10,54},{6,10,112},{6,10,402},{6,10,1732},{7,0, +1347},{7,10,315},{7,10,749},{7,10,1900},{9,10,78},{9,10,508},{10,10,611},{10,10, +811},{11,10,510},{11,10,728},{13,10,36},{14,10,39},{16,10,83},{17,10,124},{148, +10,30},{8,10,570},{9,11,477},{141,11,78},{4,11,639},{10,10,322},{10,10,719},{10, +11,4},{11,10,407},{11,11,638},{12,11,177},{148,11,57},{7,0,1823},{139,0,693},{5, +11,758},{7,0,759},{8,10,125},{8,10,369},{8,10,524},{10,10,486},{11,10,13},{11,10 +,381},{11,10,736},{11,10,766},{11,10,845},{13,10,114},{13,10,292},{142,10,47},{6 +,10,1684},{6,10,1731},{7,0,1932},{7,10,356},{8,10,54},{8,10,221},{9,10,225},{9, +10,356},{10,10,77},{10,10,446},{10,10,731},{12,10,404},{141,10,491},{135,11,552} +,{135,11,1112},{4,0,78},{5,0,96},{5,0,182},{6,0,1257},{7,0,1724},{7,0,1825},{10, +0,394},{10,0,471},{11,0,532},{14,0,340},{145,0,88},{139,11,328},{135,0,1964},{ +132,10,411},{4,10,80},{5,10,44},{137,11,133},{5,11,110},{6,11,169},{6,11,1702},{ +7,11,400},{8,11,538},{9,11,184},{9,11,524},{140,11,218},{4,0,521},{5,10,299},{7, +10,1083},{140,11,554},{6,11,133},{9,11,353},{12,11,628},{146,11,79},{6,0,215},{7 +,0,584},{7,0,1028},{7,0,1473},{7,0,1721},{9,0,424},{138,0,779},{7,0,857},{7,0, +1209},{7,10,1713},{9,10,537},{10,10,165},{12,10,219},{140,10,561},{4,10,219},{6, +11,93},{7,10,1761},{7,11,1422},{7,11,1851},{8,11,673},{9,10,86},{9,11,529},{140, +11,43},{137,11,371},{136,0,671},{5,0,328},{135,0,918},{132,0,529},{9,11,25},{10, +11,467},{138,11,559},{4,11,335},{135,11,942},{134,0,716},{134,0,1509},{6,0,67},{ +7,0,258},{7,0,1630},{9,0,354},{9,0,675},{10,0,830},{12,10,428},{14,0,80},{145,0, +80},{134,0,1112},{6,0,141},{7,0,225},{9,0,59},{9,0,607},{10,0,312},{11,0,687},{ +12,0,555},{13,0,373},{13,0,494},{148,0,58},{133,10,514},{8,11,39},{10,11,773},{ +11,11,84},{12,11,205},{142,11,1},{5,11,601},{5,11,870},{136,0,783},{136,11,594}, +{4,10,55},{5,10,301},{6,10,571},{14,10,49},{146,10,102},{132,11,181},{134,11, +1652},{133,10,364},{4,11,97},{5,11,147},{6,11,286},{7,11,1362},{141,11,176},{4, +10,76},{7,10,1550},{9,10,306},{9,10,430},{9,10,663},{10,10,683},{11,10,427},{11, +10,753},{12,10,334},{12,10,442},{14,10,258},{14,10,366},{143,10,131},{137,10,52} +,{6,0,955},{134,0,1498},{6,11,375},{7,11,169},{7,11,254},{136,11,780},{7,0,430}, +{11,0,46},{14,0,343},{142,11,343},{135,0,1183},{5,0,602},{7,0,2018},{7,11,1447}, +{9,0,418},{137,0,803},{7,11,1044},{136,0,677},{139,11,285},{4,10,656},{135,10, +779},{135,10,144},{5,11,629},{135,11,1549},{135,10,1373},{138,11,209},{7,10,554} +,{7,10,605},{141,10,10},{5,10,838},{5,10,841},{134,10,1649},{133,10,1012},{6,0, +1357},{134,0,1380},{144,0,53},{6,0,590},{7,10,365},{7,10,1357},{7,10,1497},{8,10 +,154},{141,10,281},{133,10,340},{132,11,420},{135,0,329},{147,11,32},{4,0,469},{ +10,11,429},{139,10,495},{8,10,261},{9,10,144},{9,10,466},{10,10,370},{12,10,470} +,{13,10,144},{142,10,348},{142,0,460},{4,11,325},{9,10,897},{138,11,125},{6,0, +1743},{6,10,248},{9,10,546},{10,10,535},{11,10,681},{141,10,135},{4,0,990},{5,0, +929},{6,0,340},{8,0,376},{8,0,807},{8,0,963},{8,0,980},{138,0,1007},{134,0,1603} +,{140,0,250},{4,11,714},{133,11,469},{134,10,567},{136,10,445},{5,0,218},{7,0, +1610},{8,0,646},{10,0,83},{11,11,138},{140,11,40},{7,0,1512},{135,0,1794},{135, +11,1216},{4,11,718},{11,0,0},{144,0,78},{133,0,571},{132,0,455},{134,0,1012},{5, +11,124},{5,11,144},{6,11,548},{7,11,15},{7,11,153},{137,11,629},{142,11,10},{6, +11,75},{7,11,1531},{8,11,416},{9,11,240},{9,11,275},{10,11,100},{11,11,658},{11, +11,979},{12,11,86},{13,11,468},{14,11,66},{14,11,207},{15,11,20},{15,11,25},{144 +,11,58},{132,10,577},{5,11,141},{5,11,915},{6,11,1783},{7,11,211},{7,11,698},{7, +11,1353},{9,11,83},{9,11,281},{10,11,376},{10,11,431},{11,11,543},{12,11,664},{ +13,11,280},{13,11,428},{14,11,61},{14,11,128},{17,11,52},{145,11,81},{6,0,161},{ +7,0,372},{137,0,597},{132,0,349},{10,11,702},{139,11,245},{134,0,524},{134,10, +174},{6,0,432},{9,0,751},{139,0,322},{147,11,94},{4,11,338},{133,11,400},{5,0, +468},{10,0,325},{11,0,856},{12,0,345},{143,0,104},{133,0,223},{132,0,566},{4,11, +221},{5,11,659},{5,11,989},{7,11,697},{7,11,1211},{138,11,284},{135,11,1070},{4, +0,59},{135,0,1394},{5,10,878},{5,10,972},{6,0,436},{139,0,481},{4,0,48},{5,0,271 +},{135,0,953},{5,0,610},{136,0,457},{4,0,773},{5,0,618},{137,0,756},{133,0,755}, +{135,0,1217},{138,11,507},{132,10,351},{132,0,197},{143,11,78},{4,11,188},{7,11, +805},{11,11,276},{142,11,293},{5,11,884},{139,11,991},{132,10,286},{7,10,438},{7 +,10,627},{7,10,1516},{8,10,40},{9,10,56},{9,10,294},{10,0,259},{10,0,428},{11,10 +,969},{11,10,995},{146,10,148},{4,0,356},{5,0,217},{5,0,492},{5,0,656},{8,0,544} +,{136,11,544},{5,0,259},{6,0,1230},{7,0,414},{7,0,854},{142,0,107},{132,0,1007}, +{15,0,14},{144,0,5},{4,10,738},{134,0,1580},{132,11,596},{132,0,673},{133,10,866 +},{6,0,1843},{135,11,1847},{4,0,165},{7,0,1398},{135,0,1829},{135,11,1634},{147, +11,65},{6,0,885},{6,0,1009},{137,0,809},{133,10,116},{132,10,457},{136,11,770},{ +9,0,498},{10,11,361},{12,0,181},{142,11,316},{134,11,595},{5,0,9},{7,0,297},{7,0 +,966},{140,0,306},{4,11,89},{5,11,489},{6,11,315},{7,11,553},{7,11,1745},{138,11 +,243},{134,0,1487},{132,0,437},{5,0,146},{6,0,411},{138,0,721},{5,10,527},{6,10, +189},{135,10,859},{11,10,104},{11,10,554},{15,10,60},{143,10,125},{6,11,1658},{9 +,11,3},{10,11,154},{11,11,641},{13,11,85},{13,11,201},{141,11,346},{6,0,177},{ +135,0,467},{134,0,1377},{134,10,116},{136,11,645},{4,11,166},{5,11,505},{6,11, +1670},{137,11,110},{133,10,487},{4,10,86},{5,10,667},{5,10,753},{6,10,316},{6,10 +,455},{135,10,946},{133,0,200},{132,0,959},{6,0,1928},{134,0,1957},{139,11,203}, +{150,10,45},{4,10,79},{7,10,1773},{10,10,450},{11,10,589},{13,10,332},{13,10,493 +},{14,10,183},{14,10,334},{14,10,362},{14,10,368},{14,10,376},{14,10,379},{19,10 +,90},{19,10,103},{19,10,127},{148,10,90},{6,0,1435},{135,11,1275},{134,0,481},{7 +,11,445},{8,11,307},{8,11,704},{10,11,41},{10,11,439},{11,11,237},{11,11,622},{ +140,11,201},{135,11,869},{4,0,84},{7,0,1482},{10,0,76},{138,0,142},{11,11,277},{ +144,11,14},{135,11,1977},{4,11,189},{5,11,713},{136,11,57},{133,0,1015},{138,11, +371},{4,0,315},{5,0,507},{135,0,1370},{4,11,552},{142,10,381},{9,0,759},{16,0,31 +},{16,0,39},{16,0,75},{18,0,24},{20,0,42},{152,0,1},{134,0,712},{134,0,1722},{ +133,10,663},{133,10,846},{8,0,222},{8,0,476},{9,0,238},{11,0,516},{11,0,575},{15 +,0,109},{146,0,100},{5,10,378},{7,0,1402},{7,0,1414},{8,10,465},{9,10,286},{10, +10,185},{10,10,562},{10,10,635},{11,10,31},{11,10,393},{12,0,456},{13,10,312},{ +18,10,65},{18,10,96},{147,10,89},{4,0,986},{6,0,1958},{6,0,2032},{8,0,934},{138, +0,985},{7,10,1880},{9,10,680},{139,10,798},{134,10,1770},{145,11,49},{132,11,614 +},{132,10,648},{5,10,945},{6,10,1656},{6,10,1787},{7,10,167},{8,10,824},{9,10, +391},{10,10,375},{139,10,185},{138,11,661},{7,0,1273},{135,11,1945},{7,0,706},{7 +,0,1058},{138,0,538},{7,10,1645},{8,10,352},{137,10,249},{132,10,152},{11,0,92}, +{11,0,196},{11,0,409},{11,0,450},{11,0,666},{11,0,777},{12,0,262},{13,0,385},{13 +,0,393},{15,0,115},{16,0,45},{145,0,82},{133,10,1006},{6,0,40},{135,0,1781},{9, +11,614},{139,11,327},{5,10,420},{135,10,1449},{135,0,431},{7,10,832},{138,0,97}, +{6,0,423},{7,0,665},{135,0,1210},{7,0,237},{8,0,664},{9,0,42},{9,0,266},{9,0,380 +},{9,0,645},{10,0,177},{138,0,276},{5,10,351},{135,0,264},{5,10,40},{7,10,598},{ +7,10,1638},{8,0,213},{9,10,166},{9,10,640},{9,10,685},{9,10,773},{11,10,215},{13 +,10,65},{14,10,172},{14,10,317},{145,10,6},{5,11,84},{134,11,163},{8,10,60},{9, +10,343},{139,10,769},{137,0,455},{133,11,410},{8,0,906},{12,0,700},{12,0,706},{ +140,0,729},{21,11,33},{150,11,40},{7,10,1951},{8,10,765},{8,10,772},{140,10,671} +,{7,10,108},{8,10,219},{8,10,388},{9,10,639},{9,10,775},{11,10,275},{140,10,464} +,{5,11,322},{7,11,1941},{8,11,186},{9,11,262},{10,11,187},{14,11,208},{146,11, +130},{139,0,624},{5,11,227},{8,0,574},{140,11,29},{7,11,1546},{11,11,299},{142, +11,407},{5,10,15},{6,10,56},{7,10,1758},{8,10,500},{9,10,730},{11,10,331},{13,10 +,150},{142,10,282},{7,11,1395},{8,11,486},{9,11,236},{9,11,878},{10,11,218},{11, +11,95},{19,11,17},{147,11,31},{135,11,2043},{4,0,354},{146,11,4},{140,11,80},{ +135,0,1558},{134,10,1886},{5,10,205},{6,10,438},{137,10,711},{133,11,522},{133, +10,534},{7,0,235},{7,0,1475},{15,0,68},{146,0,120},{137,10,691},{4,0,942},{6,0, +1813},{8,0,917},{10,0,884},{12,0,696},{12,0,717},{12,0,723},{12,0,738},{12,0,749 +},{12,0,780},{16,0,97},{146,0,169},{6,10,443},{8,11,562},{9,10,237},{9,10,571},{ +9,10,695},{10,10,139},{11,10,715},{12,10,417},{141,10,421},{135,0,957},{133,0, +830},{134,11,1771},{146,0,23},{5,0,496},{6,0,694},{7,0,203},{7,11,1190},{137,11, +620},{137,11,132},{6,0,547},{134,0,1549},{8,11,258},{9,11,208},{137,11,359},{4,0 +,864},{5,0,88},{137,0,239},{135,11,493},{4,11,317},{135,11,1279},{132,11,477},{4 +,10,578},{5,11,63},{133,11,509},{7,0,650},{135,0,1310},{7,0,1076},{9,0,80},{11,0 +,78},{11,0,421},{11,0,534},{140,0,545},{132,11,288},{5,10,923},{12,0,553},{142,0 +,118},{7,0,274},{11,0,479},{139,0,507},{8,11,89},{8,11,620},{9,11,49},{10,11,774 +},{11,11,628},{12,11,322},{143,11,124},{4,0,497},{135,0,1584},{4,10,924},{7,0, +261},{7,0,1115},{7,0,1354},{7,0,1404},{7,0,1588},{7,0,1705},{7,0,1902},{9,0,465} +,{10,0,248},{10,0,349},{10,0,647},{11,0,527},{11,0,660},{11,0,669},{12,0,529},{ +141,0,305},{133,10,665},{136,0,13},{6,0,791},{138,11,120},{7,0,642},{8,0,250},{ +11,0,123},{11,0,137},{13,0,48},{142,0,95},{4,10,265},{7,10,807},{135,10,950},{5, +10,93},{140,10,267},{135,0,1429},{4,0,949},{10,0,885},{10,0,891},{10,0,900},{10, +0,939},{12,0,760},{142,0,449},{139,11,366},{132,0,818},{134,11,85},{135,10,994}, +{5,10,233},{5,10,320},{6,10,140},{7,0,330},{136,10,295},{4,0,1004},{8,0,982},{ +136,0,993},{133,10,978},{4,10,905},{6,10,1701},{137,10,843},{10,0,545},{140,0, +301},{6,0,947},{134,0,1062},{134,0,1188},{4,0,904},{5,0,794},{152,10,6},{134,0, +1372},{135,11,608},{5,11,279},{6,11,235},{7,11,468},{8,11,446},{9,11,637},{10,11 +,717},{11,11,738},{140,11,514},{132,10,509},{5,11,17},{6,11,371},{137,11,528},{ +132,0,693},{4,11,115},{5,11,669},{6,11,407},{8,11,311},{11,11,10},{141,11,5},{7, +10,273},{9,11,381},{139,0,377},{135,0,695},{7,0,386},{138,0,713},{135,10,1041},{ +134,0,1291},{6,0,7},{6,0,35},{7,0,147},{7,0,1069},{7,0,1568},{7,0,1575},{7,0, +1917},{8,0,43},{8,0,208},{9,0,128},{9,0,866},{10,0,20},{11,0,981},{147,0,33},{7, +0,893},{141,0,424},{139,10,234},{150,11,56},{5,11,779},{5,11,807},{6,11,1655},{ +134,11,1676},{5,10,802},{7,10,2021},{136,10,805},{4,11,196},{5,10,167},{5,10,899 +},{5,11,558},{5,11,949},{6,10,410},{137,10,777},{137,10,789},{134,10,1705},{8,0, +904},{140,0,787},{6,0,322},{9,0,552},{11,0,274},{13,0,209},{13,0,499},{14,0,85}, +{15,0,126},{145,0,70},{135,10,10},{5,10,11},{6,10,117},{6,10,485},{7,10,1133},{9 +,10,582},{9,10,594},{11,10,21},{11,10,818},{12,10,535},{141,10,86},{4,10,264},{7 +,10,1067},{8,10,204},{8,10,385},{139,10,953},{132,11,752},{138,10,56},{133,10, +470},{6,0,1808},{8,0,83},{8,0,742},{8,0,817},{9,0,28},{9,0,29},{9,0,885},{10,0, +387},{11,0,633},{11,0,740},{13,0,235},{13,0,254},{15,0,143},{143,0,146},{140,0, +49},{134,0,1832},{4,11,227},{5,11,159},{5,11,409},{7,11,80},{10,11,294},{10,11, +479},{12,11,418},{14,11,50},{14,11,249},{142,11,295},{7,11,1470},{8,11,66},{8,11 +,137},{8,11,761},{9,11,638},{11,11,80},{11,11,212},{11,11,368},{11,11,418},{12, +11,8},{13,11,15},{16,11,61},{17,11,59},{19,11,28},{148,11,84},{139,10,1015},{138 +,11,468},{135,0,421},{6,0,415},{7,0,1049},{137,0,442},{6,11,38},{7,11,1220},{8, +11,185},{8,11,256},{9,11,22},{9,11,331},{10,11,738},{11,11,205},{11,11,540},{11, +11,746},{13,11,399},{13,11,465},{14,11,88},{142,11,194},{139,0,289},{133,10,715} +,{4,0,110},{10,0,415},{10,0,597},{142,0,206},{4,11,159},{6,11,115},{7,11,252},{7 +,11,257},{7,11,1928},{8,11,69},{9,11,384},{10,11,91},{10,11,615},{12,11,375},{14 +,11,235},{18,11,117},{147,11,123},{5,11,911},{136,11,278},{7,0,205},{7,0,2000},{ +8,10,794},{9,10,400},{10,10,298},{142,10,228},{135,11,1774},{4,11,151},{7,11, +1567},{8,11,351},{137,11,322},{136,10,724},{133,11,990},{7,0,1539},{7,11,1539},{ +11,0,512},{13,0,205},{19,0,30},{22,0,36},{151,0,19},{5,11,194},{7,11,1662},{9,11 +,90},{140,11,180},{6,10,190},{7,10,768},{135,10,1170},{134,0,1340},{4,0,283},{ +135,0,1194},{133,11,425},{133,11,971},{12,0,549},{14,10,67},{147,10,60},{135,10, +1023},{134,0,1720},{138,11,587},{5,11,72},{6,11,264},{7,11,21},{7,11,46},{7,11, +2013},{8,11,215},{8,11,513},{10,11,266},{139,11,22},{5,0,319},{135,0,534},{6,10, +137},{9,10,75},{9,10,253},{10,10,194},{138,10,444},{6,11,239},{7,0,1180},{7,11, +118},{10,11,95},{11,11,603},{13,11,443},{14,11,160},{15,11,4},{148,0,112},{134, +11,431},{5,11,874},{6,11,1677},{11,10,643},{12,10,115},{143,11,0},{134,0,967},{6 +,11,65},{7,11,939},{7,11,1172},{7,11,1671},{9,11,540},{10,11,696},{11,11,265},{ +11,11,732},{11,11,928},{11,11,937},{12,11,399},{13,11,438},{149,11,19},{137,11, +200},{135,0,1940},{5,10,760},{7,10,542},{8,10,135},{136,10,496},{140,11,44},{7, +11,1655},{136,11,305},{7,10,319},{7,10,355},{7,10,763},{10,10,389},{145,10,43},{ +136,0,735},{138,10,786},{137,11,19},{132,11,696},{5,0,132},{9,0,486},{9,0,715},{ +10,0,458},{11,0,373},{11,0,668},{11,0,795},{11,0,897},{12,0,272},{12,0,424},{12, +0,539},{12,0,558},{14,0,245},{14,0,263},{14,0,264},{14,0,393},{142,0,403},{10,0, +38},{139,0,784},{132,0,838},{4,11,302},{135,11,1766},{133,0,379},{4,11,726},{5,0 +,8},{5,11,630},{6,0,89},{6,0,400},{7,0,1569},{7,0,1623},{7,0,1850},{8,0,218},{8, +0,422},{9,0,570},{138,0,626},{4,0,1017},{138,0,660},{6,0,387},{7,0,882},{141,0, +111},{6,0,224},{7,0,877},{137,0,647},{4,10,58},{5,10,286},{6,10,319},{7,10,402}, +{7,10,1254},{7,10,1903},{8,10,356},{140,10,408},{135,0,790},{4,10,389},{9,0,510} +,{9,10,181},{10,0,53},{10,10,29},{10,10,816},{11,10,311},{11,10,561},{12,10,67}, +{141,10,181},{142,0,458},{6,11,118},{7,11,215},{7,11,1521},{140,11,11},{134,0, +954},{135,0,394},{134,0,1367},{5,10,373},{133,11,225},{132,0,882},{7,0,1409},{ +135,10,1972},{135,10,1793},{4,11,370},{5,11,756},{135,11,1326},{150,11,13},{7,11 +,354},{10,11,410},{139,11,815},{6,11,1662},{7,11,48},{8,11,771},{10,11,116},{13, +11,104},{14,11,105},{14,11,184},{15,11,168},{19,11,92},{148,11,68},{7,0,124},{ +136,0,38},{5,0,261},{7,0,78},{7,0,199},{8,0,815},{9,0,126},{10,0,342},{140,0,647 +},{4,0,628},{140,0,724},{7,0,266},{7,10,1651},{8,0,804},{145,10,89},{135,0,208}, +{134,0,1178},{6,0,79},{135,0,1519},{132,10,672},{133,10,737},{136,0,741},{132,11 +,120},{4,0,710},{6,0,376},{134,0,606},{134,0,1347},{134,0,1494},{6,0,850},{6,0, +1553},{137,0,821},{5,10,145},{134,11,593},{7,0,1311},{140,0,135},{4,0,467},{5,0, +405},{134,0,544},{5,11,820},{135,11,931},{6,0,100},{7,0,244},{7,0,632},{7,0,1609 +},{8,0,178},{8,0,638},{141,0,58},{4,10,387},{135,10,1288},{6,11,151},{6,11,1675} +,{7,11,383},{151,11,10},{132,0,481},{135,10,550},{134,0,1378},{6,11,1624},{11,11 +,11},{12,11,422},{13,11,262},{142,11,360},{133,0,791},{4,11,43},{5,11,344},{133, +11,357},{7,0,1227},{140,0,978},{7,0,686},{8,0,33},{8,0,238},{10,0,616},{11,0,467 +},{11,0,881},{13,0,217},{13,0,253},{142,0,268},{137,0,857},{7,11,148},{8,0,467}, +{8,0,1006},{8,11,284},{141,11,63},{4,10,576},{135,10,1263},{133,11,888},{5,10, +919},{134,10,1673},{20,10,37},{148,11,37},{132,0,447},{132,11,711},{4,0,128},{5, +0,415},{6,0,462},{7,0,294},{7,0,578},{10,0,710},{139,0,86},{4,10,82},{5,10,333}, +{5,10,904},{6,10,207},{7,10,325},{7,10,1726},{8,10,101},{10,10,778},{139,10,220} +,{136,0,587},{137,11,440},{133,10,903},{6,0,427},{7,0,1018},{138,0,692},{4,0,195 +},{135,0,802},{140,10,147},{134,0,1546},{134,0,684},{132,10,705},{136,0,345},{11 +,11,678},{140,11,307},{133,0,365},{134,0,1683},{4,11,65},{5,11,479},{5,11,1004}, +{7,11,1913},{8,11,317},{9,11,302},{10,11,612},{141,11,22},{138,0,472},{4,11,261} +,{135,11,510},{134,10,90},{142,0,433},{151,0,28},{4,11,291},{7,11,101},{9,11,515 +},{12,11,152},{12,11,443},{13,11,392},{142,11,357},{140,0,997},{5,0,3},{8,0,578} +,{9,0,118},{10,0,705},{141,0,279},{135,11,1266},{7,10,813},{12,10,497},{141,10, +56},{133,0,229},{6,10,125},{135,10,1277},{8,0,102},{10,0,578},{10,0,672},{12,0, +496},{13,0,408},{14,0,121},{17,0,106},{151,10,12},{6,0,866},{134,0,1080},{136,0, +1022},{4,11,130},{135,11,843},{5,11,42},{5,11,879},{7,11,245},{7,11,324},{7,11, +1532},{11,11,463},{11,11,472},{13,11,363},{144,11,52},{150,0,55},{4,11,134},{5, +11,372},{8,0,115},{8,0,350},{9,0,489},{10,0,128},{11,0,306},{12,0,373},{14,0,30} +,{17,0,79},{147,0,80},{134,0,657},{134,0,933},{135,11,1147},{4,0,230},{133,0,702 +},{134,0,1728},{4,0,484},{18,0,26},{19,0,42},{20,0,43},{21,0,0},{23,0,27},{152,0 +,14},{7,0,185},{135,0,703},{6,0,417},{7,10,1106},{9,10,770},{10,0,618},{11,10, +112},{140,10,413},{134,0,803},{132,11,644},{134,0,1262},{7,11,540},{12,10,271},{ +145,10,109},{135,11,123},{132,0,633},{134,11,623},{4,11,908},{5,11,359},{5,11, +508},{6,11,1723},{7,11,343},{7,11,1996},{135,11,2026},{135,0,479},{7,10,304},{9, +10,646},{9,10,862},{10,0,262},{11,10,696},{12,10,208},{15,10,79},{147,10,108},{4 +,11,341},{135,11,480},{134,0,830},{5,0,70},{5,0,622},{6,0,334},{7,0,1032},{9,0, +171},{11,0,26},{11,0,213},{11,0,637},{11,0,707},{12,0,202},{12,0,380},{13,0,226} +,{13,0,355},{14,0,222},{145,0,42},{135,10,981},{143,0,217},{137,11,114},{4,0,23} +,{4,0,141},{5,0,313},{5,0,1014},{6,0,50},{6,0,51},{7,0,142},{7,0,384},{7,0,559}, +{8,0,640},{9,0,460},{9,0,783},{11,0,741},{12,0,183},{141,0,488},{141,0,360},{7,0 +,1586},{7,11,1995},{8,11,299},{11,11,890},{140,11,674},{132,10,434},{6,10,550},{ +135,0,652},{5,10,553},{7,0,766},{138,10,824},{7,0,737},{8,0,298},{136,10,452},{4 +,11,238},{5,11,503},{6,11,179},{7,11,2003},{8,11,381},{8,11,473},{9,11,149},{10, +11,183},{15,11,45},{143,11,86},{133,10,292},{5,0,222},{9,0,655},{138,0,534},{138 +,10,135},{4,11,121},{5,11,156},{5,11,349},{9,11,136},{10,11,605},{14,11,342},{ +147,11,107},{137,0,906},{6,0,1013},{134,0,1250},{6,0,1956},{6,0,2009},{8,0,991}, +{144,0,120},{135,11,1192},{138,0,503},{5,0,154},{7,0,1491},{10,0,379},{138,0,485 +},{6,0,1867},{6,0,1914},{6,0,1925},{9,0,917},{9,0,925},{9,0,932},{9,0,951},{9,0, +1007},{9,0,1013},{12,0,806},{12,0,810},{12,0,814},{12,0,816},{12,0,824},{12,0, +832},{12,0,837},{12,0,863},{12,0,868},{12,0,870},{12,0,889},{12,0,892},{12,0,900 +},{12,0,902},{12,0,908},{12,0,933},{12,0,942},{12,0,949},{12,0,954},{15,0,175},{ +15,0,203},{15,0,213},{15,0,218},{15,0,225},{15,0,231},{15,0,239},{15,0,248},{15, +0,252},{18,0,190},{18,0,204},{18,0,215},{18,0,216},{18,0,222},{18,0,225},{18,0, +230},{18,0,239},{18,0,241},{21,0,42},{21,0,43},{21,0,44},{21,0,45},{21,0,46},{21 +,0,53},{24,0,27},{152,0,31},{133,0,716},{135,0,844},{4,0,91},{5,0,388},{5,0,845} +,{6,0,206},{6,0,252},{6,0,365},{7,0,136},{7,0,531},{136,0,621},{7,10,393},{10,10 +,603},{139,10,206},{6,11,80},{6,11,1694},{7,11,173},{7,11,1974},{9,11,547},{10, +11,730},{14,11,18},{150,11,39},{137,0,748},{4,11,923},{134,11,1711},{4,10,912},{ +137,10,232},{7,10,98},{7,10,1973},{136,10,716},{5,10,733},{142,0,103},{132,11, +595},{5,11,240},{6,11,459},{7,11,12},{7,11,114},{7,11,502},{7,11,1751},{7,11, +1753},{7,11,1805},{8,11,658},{9,11,1},{11,11,959},{12,0,158},{13,11,446},{14,11, +211},{18,0,8},{19,0,62},{20,0,6},{22,0,4},{23,0,2},{151,0,9},{135,0,576},{5,0, +771},{5,0,863},{5,0,898},{6,0,648},{6,0,1632},{6,0,1644},{134,0,1780},{133,0,331 +},{7,11,633},{7,11,905},{7,11,909},{7,11,1538},{9,11,767},{140,11,636},{140,0, +632},{5,0,107},{7,0,201},{136,0,518},{6,0,446},{6,11,490},{135,0,1817},{9,0,851} +,{141,0,510},{7,11,250},{8,11,506},{136,11,507},{4,0,504},{137,10,72},{132,11, +158},{4,11,140},{7,11,362},{8,11,209},{9,11,10},{9,11,160},{9,11,503},{10,11,689 +},{11,11,350},{11,11,553},{11,11,725},{12,11,252},{12,11,583},{13,11,192},{13,11 +,352},{14,11,269},{14,11,356},{148,11,50},{6,11,597},{135,11,1318},{135,10,1454} +,{5,0,883},{5,0,975},{8,0,392},{148,0,7},{6,11,228},{7,11,1341},{9,11,408},{138, +11,343},{11,10,600},{11,11,348},{12,11,99},{13,10,245},{18,11,1},{18,11,11},{147 +,11,4},{134,11,296},{5,0,922},{134,0,1707},{132,11,557},{4,11,548},{7,10,164},{7 +,10,1571},{9,10,107},{140,10,225},{7,11,197},{8,11,142},{8,11,325},{9,11,150},{9 +,11,596},{10,11,350},{10,11,353},{11,11,74},{11,11,315},{14,11,423},{143,11,141} +,{5,0,993},{7,0,515},{137,0,91},{4,0,131},{5,10,484},{5,10,510},{6,10,434},{7,10 +,1000},{7,10,1098},{8,0,200},{136,10,2},{152,0,10},{4,11,62},{5,11,83},{6,11,399 +},{6,11,579},{7,11,692},{7,11,846},{7,11,1015},{7,11,1799},{8,11,403},{9,11,394} +,{10,11,133},{12,11,4},{12,11,297},{12,11,452},{16,11,81},{18,11,19},{18,11,25}, +{21,11,14},{22,11,12},{151,11,18},{140,11,459},{132,11,177},{7,0,1433},{9,0,365} +,{137,11,365},{132,10,460},{5,0,103},{5,11,411},{6,0,2004},{7,0,921},{7,11,653}, +{8,0,580},{8,0,593},{8,0,630},{138,0,28},{4,10,932},{133,10,891},{4,0,911},{5,0, +867},{5,0,1013},{7,0,2034},{8,0,798},{136,0,813},{7,11,439},{10,11,727},{11,11, +260},{139,11,684},{136,10,625},{5,11,208},{7,11,753},{135,11,1528},{5,0,461},{6, +10,76},{7,0,1925},{12,0,39},{13,0,265},{141,0,439},{6,0,853},{8,10,92},{137,10, +221},{5,0,135},{6,0,519},{7,0,1722},{10,0,271},{11,0,261},{145,0,54},{139,11,814 +},{14,0,338},{148,0,81},{4,0,300},{133,0,436},{5,0,419},{5,0,687},{7,0,864},{7, +11,864},{137,0,470},{5,11,242},{137,0,836},{134,0,1937},{4,10,763},{133,11,953}, +{132,10,622},{132,0,393},{133,10,253},{7,10,546},{8,0,357},{10,0,745},{14,0,426} +,{17,0,94},{147,0,57},{5,11,615},{146,11,37},{9,10,73},{10,10,110},{14,10,185},{ +145,10,119},{7,10,624},{7,10,916},{10,10,256},{11,0,703},{139,10,87},{133,11,290 +},{5,10,212},{12,10,35},{141,10,382},{132,11,380},{5,11,52},{7,11,277},{9,11,368 +},{139,11,791},{133,0,387},{10,11,138},{139,11,476},{4,0,6},{5,0,708},{136,0,75} +,{7,0,1351},{9,0,581},{10,0,639},{11,0,453},{140,0,584},{132,0,303},{138,0,772}, +{135,10,1175},{4,0,749},{5,10,816},{6,11,256},{7,11,307},{7,11,999},{7,11,1481}, +{7,11,1732},{7,11,1738},{8,11,265},{9,11,414},{11,11,316},{12,11,52},{13,11,420} +,{147,11,100},{135,11,1296},{5,10,869},{5,10,968},{6,0,1065},{6,10,1626},{8,10, +734},{136,10,784},{4,10,542},{6,10,1716},{6,10,1727},{7,10,1082},{7,10,1545},{8, +10,56},{8,10,118},{8,10,412},{8,10,564},{9,10,888},{9,10,908},{10,10,50},{10,10, +423},{11,10,685},{11,10,697},{11,10,933},{12,10,299},{13,10,126},{13,10,136},{13 +,10,170},{141,10,190},{134,0,226},{4,0,106},{5,11,723},{7,0,310},{139,0,717},{4, +10,232},{5,0,890},{5,0,988},{9,10,202},{10,10,474},{140,10,433},{6,0,626},{142,0 +,431},{10,0,706},{150,0,44},{6,10,108},{7,10,1003},{7,10,1181},{8,10,111},{8,10, +343},{141,0,51},{132,0,698},{5,11,109},{6,11,1784},{7,11,1895},{12,11,296},{140, +11,302},{134,0,828},{134,10,1712},{138,0,17},{4,10,133},{5,11,216},{7,0,1929},{7 +,10,711},{7,10,1298},{7,10,1585},{7,11,1879},{9,11,141},{9,11,270},{9,11,679},{ +10,11,159},{10,11,553},{11,11,197},{11,11,438},{12,11,538},{12,11,559},{13,11, +193},{13,11,423},{14,11,144},{14,11,166},{14,11,167},{15,11,67},{147,11,84},{141 +,11,127},{7,11,1872},{137,11,81},{6,10,99},{7,10,1808},{145,10,57},{134,11,391}, +{5,0,689},{6,0,84},{6,10,574},{7,0,1250},{7,10,428},{10,10,669},{11,10,485},{11, +10,840},{12,10,300},{142,10,250},{7,11,322},{136,11,249},{7,11,432},{135,11,1649 +},{135,10,1871},{137,10,252},{6,11,155},{140,11,234},{7,0,871},{19,0,27},{147,11 +,27},{140,0,498},{5,0,986},{6,0,130},{138,0,823},{6,0,1793},{6,10,323},{7,0,1582 +},{7,10,1564},{8,0,458},{10,0,101},{10,0,318},{10,0,945},{12,0,734},{16,0,104},{ +146,0,177},{5,11,632},{138,11,526},{7,10,461},{8,10,775},{138,0,435},{6,11,144}, +{7,11,948},{7,11,1042},{7,11,1857},{8,11,235},{8,11,461},{9,11,453},{9,11,530},{ +10,11,354},{17,11,77},{19,11,99},{148,11,79},{138,0,966},{7,0,1644},{137,0,129}, +{135,0,997},{136,0,502},{5,11,196},{6,11,486},{7,11,212},{8,11,309},{136,11,346} +,{7,10,727},{146,10,73},{132,0,823},{132,11,686},{135,0,1927},{4,0,762},{7,0, +1756},{137,0,98},{136,10,577},{4,11,30},{5,11,43},{24,0,8},{152,11,8},{7,0,1046} +,{139,0,160},{4,10,413},{5,10,677},{7,0,492},{7,11,492},{8,10,432},{140,10,280}, +{6,0,45},{7,0,433},{8,0,129},{9,0,21},{10,0,392},{11,0,79},{12,0,499},{13,0,199} +,{141,0,451},{7,0,558},{136,0,353},{4,11,220},{7,11,1535},{9,11,93},{139,11,474} +,{7,10,646},{7,10,1730},{11,10,446},{141,10,178},{133,0,785},{134,0,1145},{8,0, +81},{9,0,189},{9,0,201},{11,0,478},{11,0,712},{141,0,338},{5,0,353},{151,0,26},{ +4,10,395},{139,0,762},{134,0,2024},{4,0,611},{133,0,606},{9,10,174},{10,10,164}, +{11,10,440},{11,10,841},{143,10,98},{134,10,426},{10,10,608},{139,10,1002},{138, +10,250},{6,0,25},{7,0,855},{7,0,1258},{144,0,32},{7,11,1725},{138,11,393},{5,11, +263},{134,11,414},{5,10,476},{134,0,2011},{4,0,4},{6,11,178},{6,11,1750},{7,0, +1118},{7,0,1320},{7,0,1706},{8,0,277},{8,11,251},{9,0,622},{9,11,690},{10,0,9},{ +10,11,155},{10,11,196},{10,11,373},{11,0,724},{11,11,698},{12,0,350},{12,0,397}, +{13,0,28},{13,0,159},{13,11,155},{15,0,89},{18,0,5},{19,0,9},{20,0,34},{20,11,93 +},{150,0,47},{5,11,97},{137,11,393},{5,10,76},{6,10,458},{6,10,497},{7,0,764},{7 +,10,868},{9,10,658},{10,10,594},{11,0,461},{11,10,566},{12,0,172},{12,10,338},{ +141,10,200},{134,0,1449},{138,11,40},{134,11,1639},{134,0,1445},{4,10,526},{6,0, +1168},{7,10,1029},{135,10,1054},{4,11,191},{7,11,934},{8,11,647},{145,11,97},{ +132,10,636},{6,0,233},{7,10,660},{7,10,1124},{17,10,31},{19,10,22},{151,10,14},{ +6,10,1699},{136,11,110},{12,11,246},{15,11,162},{19,11,64},{20,11,8},{20,11,95}, +{22,11,24},{152,11,17},{5,11,165},{9,11,346},{138,11,655},{5,11,319},{135,11,534 +},{134,0,255},{8,11,128},{9,0,216},{139,11,179},{9,0,183},{139,0,286},{11,0,956} +,{151,0,3},{4,0,536},{7,0,1141},{10,0,723},{139,0,371},{4,10,279},{7,10,301},{ +137,10,362},{5,11,57},{6,11,101},{6,11,1663},{7,0,285},{7,11,132},{7,11,1048},{7 +,11,1154},{7,11,1415},{7,11,1507},{12,11,493},{15,11,105},{151,11,15},{5,11,459} +,{7,10,1743},{7,11,1073},{8,11,241},{136,11,334},{4,10,178},{133,10,399},{135,0, +560},{132,0,690},{135,0,1246},{18,0,157},{147,0,63},{10,0,599},{11,0,33},{12,0, +571},{149,0,1},{6,11,324},{6,11,520},{7,11,338},{7,11,1616},{7,11,1729},{8,11, +228},{9,11,69},{139,11,750},{7,0,1862},{12,0,491},{12,0,520},{13,0,383},{142,0, +244},{135,11,734},{134,10,1692},{6,10,202},{7,11,705},{10,0,448},{11,0,630},{12, +10,360},{17,0,117},{17,10,118},{18,10,27},{148,10,67},{4,11,73},{6,11,612},{7,11 +,927},{7,11,1822},{8,11,217},{9,11,472},{9,11,765},{9,11,766},{10,11,408},{11,11 +,51},{11,11,793},{12,11,266},{15,11,158},{20,11,89},{150,11,32},{4,0,190},{133,0 +,554},{133,0,1001},{5,11,389},{8,11,636},{137,11,229},{5,0,446},{7,10,872},{10, +10,516},{139,10,167},{137,10,313},{132,10,224},{134,0,1313},{5,10,546},{7,10,35} +,{8,10,11},{8,10,12},{9,10,315},{9,10,533},{10,10,802},{11,10,166},{12,10,525},{ +142,10,243},{6,0,636},{137,0,837},{5,10,241},{8,10,242},{9,10,451},{10,10,667},{ +11,10,598},{140,10,429},{22,10,46},{150,11,46},{136,11,472},{11,0,278},{142,0,73 +},{141,11,185},{132,0,868},{134,0,972},{4,10,366},{137,10,516},{138,0,1010},{5, +11,189},{6,10,1736},{7,11,442},{7,11,443},{8,11,281},{12,11,174},{13,11,83},{141 +,11,261},{139,11,384},{6,11,2},{7,11,191},{7,11,446},{7,11,758},{7,11,1262},{7, +11,1737},{8,11,22},{8,11,270},{8,11,612},{9,11,4},{9,11,167},{9,11,312},{9,11, +436},{10,11,156},{10,11,216},{10,11,311},{10,11,623},{11,11,72},{11,11,330},{11, +11,455},{12,11,101},{12,11,321},{12,11,504},{12,11,530},{12,11,543},{13,11,17},{ +13,11,156},{13,11,334},{14,11,48},{15,11,70},{17,11,60},{148,11,64},{6,10,331},{ +136,10,623},{135,0,1231},{132,0,304},{6,11,60},{7,11,670},{7,11,1327},{8,11,411} +,{8,11,435},{9,11,653},{9,11,740},{10,11,385},{11,11,222},{11,11,324},{11,11,829 +},{140,11,611},{6,11,166},{7,0,506},{7,11,374},{135,11,1174},{14,11,43},{146,11, +21},{135,11,1694},{135,10,1888},{5,11,206},{134,11,398},{135,11,50},{150,0,26},{ +6,0,53},{6,0,199},{7,0,1408},{8,0,32},{8,0,93},{10,0,397},{10,0,629},{11,0,593}, +{11,0,763},{13,0,326},{145,0,35},{134,0,105},{132,10,394},{4,0,843},{138,0,794}, +{11,0,704},{141,0,396},{5,0,114},{5,0,255},{141,0,285},{6,0,619},{7,0,898},{7,0, +1092},{7,10,1931},{8,0,485},{18,0,28},{147,0,116},{7,10,574},{7,10,1719},{137,0, +145},{7,0,2035},{8,0,19},{9,0,89},{138,0,831},{132,10,658},{6,11,517},{7,11,1159 +},{10,11,621},{139,11,192},{7,0,1933},{7,11,1933},{9,10,781},{10,10,144},{11,10, +385},{13,10,161},{13,10,228},{13,10,268},{148,10,107},{136,10,374},{10,11,223},{ +139,11,645},{135,0,1728},{7,11,64},{7,11,289},{136,11,245},{4,10,344},{6,10,498} +,{139,10,323},{136,0,746},{135,10,1063},{137,10,155},{4,0,987},{6,0,1964},{6,0, +1974},{6,0,1990},{136,0,995},{133,11,609},{133,10,906},{134,0,1550},{134,0,874}, +{5,11,129},{6,11,61},{135,11,947},{4,0,1018},{6,0,1938},{6,0,2021},{134,0,2039}, +{132,0,814},{11,0,126},{139,0,287},{134,0,1264},{5,0,955},{136,0,814},{141,11, +506},{132,11,314},{6,0,981},{139,11,1000},{5,0,56},{8,0,892},{8,0,915},{140,0, +776},{148,0,100},{10,0,4},{10,0,13},{11,0,638},{148,0,57},{148,11,74},{4,10,616} +,{133,0,738},{133,11,637},{136,10,692},{133,0,758},{132,10,305},{137,11,590},{5, +11,280},{135,11,1226},{134,11,494},{135,0,1112},{133,11,281},{5,10,214},{7,10, +603},{8,10,611},{9,10,686},{10,10,88},{11,10,459},{11,10,496},{12,10,463},{12,10 +,590},{13,0,44},{142,0,214},{139,0,328},{135,11,1064},{137,0,133},{7,0,168},{13, +0,196},{141,0,237},{134,10,1703},{134,0,1152},{135,0,1245},{5,0,110},{6,0,169},{ +6,0,1702},{7,0,400},{8,0,538},{9,0,184},{9,0,524},{140,0,218},{6,0,1816},{10,0, +871},{12,0,769},{140,0,785},{132,11,630},{7,11,33},{7,11,120},{8,11,489},{9,11, +319},{10,11,820},{11,11,1004},{12,11,379},{13,11,117},{13,11,412},{14,11,25},{15 +,11,52},{15,11,161},{16,11,47},{149,11,2},{6,0,133},{8,0,413},{9,0,353},{139,0, +993},{145,10,19},{4,11,937},{133,11,801},{134,0,978},{6,0,93},{6,0,1508},{7,0, +1422},{7,0,1851},{8,0,673},{9,0,529},{140,0,43},{4,10,737},{6,0,317},{10,0,512}, +{11,10,294},{12,10,60},{12,10,437},{13,10,64},{13,10,380},{142,10,430},{7,11, +1591},{9,0,371},{144,11,43},{6,10,1758},{8,10,520},{9,10,345},{9,10,403},{142,10 +,350},{5,0,526},{10,10,242},{138,10,579},{9,0,25},{10,0,467},{138,0,559},{5,10, +139},{7,10,1168},{138,10,539},{4,0,335},{135,0,942},{140,0,754},{132,11,365},{11 +,0,182},{142,0,195},{142,11,29},{5,11,7},{139,11,774},{4,11,746},{135,11,1090},{ +8,0,39},{10,0,773},{11,0,84},{12,0,205},{142,0,1},{5,0,601},{5,0,870},{5,11,360} +,{136,11,237},{132,0,181},{136,0,370},{134,0,1652},{4,10,107},{7,10,613},{8,0, +358},{8,10,439},{8,10,504},{9,10,501},{10,10,383},{139,10,477},{132,10,229},{137 +,11,785},{4,0,97},{5,0,147},{6,0,286},{7,0,1362},{141,0,176},{4,10,903},{6,0,537 +},{7,0,788},{135,0,1816},{140,10,71},{6,0,743},{134,0,1223},{6,0,375},{7,0,169}, +{7,0,254},{7,11,1493},{136,0,780},{4,10,47},{6,10,373},{7,0,1714},{7,10,452},{7, +10,543},{7,10,1856},{9,10,6},{11,10,257},{139,10,391},{6,0,896},{136,0,1003},{ +135,0,1447},{137,11,341},{5,10,980},{134,10,1754},{145,11,22},{4,11,277},{5,11, +608},{6,11,493},{7,11,457},{140,11,384},{7,10,536},{7,10,1331},{136,10,143},{140 +,0,744},{7,11,27},{135,11,316},{5,10,19},{6,10,533},{146,0,126},{4,0,788},{5,11, +552},{5,11,586},{5,11,676},{6,11,448},{8,11,244},{11,0,41},{11,11,1},{11,11,41}, +{13,11,3},{16,11,54},{17,11,4},{146,11,13},{4,0,985},{4,11,401},{6,0,1801},{137, +11,264},{5,10,395},{5,10,951},{134,10,1776},{5,0,629},{135,0,1549},{11,10,663},{ +12,10,210},{13,10,166},{13,10,310},{14,10,373},{147,10,43},{9,11,543},{10,11,524 +},{11,11,30},{12,11,524},{14,11,315},{16,11,18},{20,11,26},{148,11,65},{4,11,205 +},{5,11,623},{7,11,104},{136,11,519},{5,0,293},{134,0,601},{7,11,579},{9,11,41}, +{9,11,244},{9,11,669},{10,11,5},{11,11,861},{11,11,951},{139,11,980},{132,11,717 +},{132,10,695},{7,10,497},{9,10,387},{147,10,81},{132,0,420},{142,0,37},{6,0, +1134},{6,0,1900},{12,0,830},{12,0,878},{12,0,894},{15,0,221},{143,0,245},{132,11 +,489},{7,0,1570},{140,0,542},{8,0,933},{136,0,957},{5,10,284},{6,0,1371},{6,10, +49},{6,10,350},{7,0,31},{7,10,377},{7,10,1693},{8,0,373},{8,10,678},{9,10,161},{ +9,10,585},{9,10,671},{9,10,839},{11,10,912},{141,10,427},{135,11,892},{4,0,325}, +{138,0,125},{139,11,47},{132,10,597},{138,0,323},{6,0,1547},{7,11,1605},{9,11, +473},{11,11,962},{146,11,139},{139,10,908},{7,11,819},{9,11,26},{9,11,392},{10, +11,152},{10,11,226},{11,11,19},{12,11,276},{12,11,426},{12,11,589},{13,11,460},{ +15,11,97},{19,11,48},{148,11,104},{135,11,51},{4,0,718},{135,0,1216},{6,0,1896}, +{6,0,1905},{6,0,1912},{7,11,761},{7,11,1051},{9,0,947},{9,0,974},{9,11,545},{12, +0,809},{12,0,850},{12,0,858},{12,0,874},{12,0,887},{12,0,904},{12,0,929},{12,0, +948},{12,0,952},{15,0,198},{15,0,206},{15,0,220},{15,0,227},{15,0,247},{18,0,188 +},{21,0,48},{21,0,50},{24,0,25},{152,0,29},{5,0,124},{5,0,144},{6,0,548},{7,0,15 +},{7,0,153},{137,0,629},{135,11,606},{135,10,2014},{7,10,2007},{9,10,101},{9,10, +450},{9,11,46},{10,10,66},{10,10,842},{11,10,536},{140,10,587},{6,0,75},{7,0, +1531},{8,0,416},{9,0,240},{9,0,275},{10,0,100},{11,0,658},{11,0,979},{12,0,86},{ +14,0,207},{15,0,20},{143,0,25},{5,0,141},{5,0,915},{6,0,1783},{7,0,211},{7,0,698 +},{7,0,1353},{9,0,83},{9,0,281},{10,0,376},{10,0,431},{11,0,543},{12,0,664},{13, +0,280},{13,0,428},{14,0,61},{14,0,128},{17,0,52},{145,0,81},{132,11,674},{135,0, +533},{149,0,6},{132,11,770},{133,0,538},{5,11,79},{7,11,1027},{7,11,1477},{139, +11,52},{139,10,62},{4,0,338},{133,0,400},{5,11,789},{134,11,195},{4,11,251},{4, +11,688},{7,11,513},{7,11,1284},{9,11,87},{138,11,365},{134,10,1766},{6,0,0},{7,0 +,84},{11,0,895},{145,0,11},{139,0,892},{4,0,221},{5,0,659},{7,0,697},{7,0,1211}, +{138,0,284},{133,0,989},{133,11,889},{4,11,160},{5,11,330},{7,11,1434},{136,11, +174},{6,10,1665},{7,10,256},{7,10,1388},{10,10,499},{139,10,670},{4,10,22},{5,10 +,10},{7,0,848},{136,10,97},{138,0,507},{133,10,481},{4,0,188},{135,0,805},{5,0, +884},{6,0,732},{139,0,991},{135,11,968},{11,11,636},{15,11,145},{17,11,34},{19, +11,50},{151,11,20},{6,10,134},{7,0,959},{7,10,437},{9,10,37},{14,10,285},{14,10, +371},{144,0,60},{7,10,486},{8,10,155},{11,10,93},{140,10,164},{134,0,1653},{5,10 +,591},{135,0,337},{5,11,374},{6,0,1989},{8,0,922},{136,0,978},{132,0,638},{138,0 +,500},{133,11,731},{5,10,380},{5,10,650},{136,10,310},{138,11,381},{4,10,364},{7 +,10,1156},{7,10,1187},{137,10,409},{137,11,224},{140,0,166},{134,10,482},{4,11, +626},{5,11,642},{6,11,425},{10,11,202},{139,11,141},{4,10,781},{6,10,487},{7,10, +926},{8,10,263},{139,10,500},{135,0,418},{4,10,94},{135,10,1265},{136,0,760},{ +132,10,417},{136,11,835},{5,10,348},{134,10,522},{6,0,1277},{134,0,1538},{139,11 +,541},{135,11,1597},{5,11,384},{8,11,455},{140,11,48},{136,0,770},{5,11,264},{ +134,11,184},{4,0,89},{5,0,489},{6,0,315},{7,0,553},{7,0,1745},{138,0,243},{4,10, +408},{4,10,741},{135,10,500},{134,0,1396},{133,0,560},{6,0,1658},{9,0,3},{10,0, +154},{11,0,641},{13,0,85},{13,0,201},{141,0,346},{135,11,1595},{5,11,633},{6,11, +28},{7,11,219},{135,11,1323},{9,11,769},{140,11,185},{135,11,785},{7,11,359},{8, +11,243},{140,11,175},{138,0,586},{6,10,73},{135,0,1271},{132,11,105},{4,0,166},{ +5,0,505},{134,0,1670},{133,10,576},{4,11,324},{138,11,104},{142,10,231},{6,0,637 +},{7,10,1264},{7,10,1678},{11,10,945},{12,10,341},{12,10,471},{12,10,569},{23,11 +,21},{151,11,23},{8,11,559},{141,11,109},{134,0,1947},{7,0,445},{8,0,307},{8,0, +704},{10,0,41},{10,0,439},{11,0,237},{11,0,622},{140,0,201},{135,11,963},{135,0, +1977},{4,0,189},{5,0,713},{136,0,57},{138,0,371},{135,10,538},{132,0,552},{5,10, +413},{134,0,883},{4,11,758},{134,0,923},{138,11,215},{136,10,495},{7,10,54},{8, +10,312},{10,10,191},{10,10,614},{140,10,567},{7,11,351},{139,11,128},{6,10,468}, +{7,0,875},{7,10,1478},{8,10,530},{142,10,290},{135,0,1788},{5,11,918},{145,0,49} +,{12,11,398},{20,11,39},{21,11,11},{150,11,41},{6,10,484},{7,10,822},{138,0,661} +,{135,0,1945},{134,0,794},{137,10,900},{135,10,1335},{6,10,1724},{135,10,2022},{ +132,11,340},{134,0,1135},{4,0,784},{133,0,745},{5,0,84},{134,0,163},{133,0,410}, +{4,0,976},{5,11,985},{7,11,509},{7,11,529},{145,11,96},{132,10,474},{134,0,703}, +{135,11,1919},{5,0,322},{8,0,186},{9,0,262},{10,0,187},{142,0,208},{135,10,1504} +,{133,0,227},{5,10,305},{9,0,560},{141,0,208},{132,11,247},{7,0,1395},{8,0,486}, +{9,0,236},{9,0,878},{10,0,218},{11,0,95},{19,0,17},{147,0,31},{7,0,2043},{8,0, +672},{141,0,448},{4,11,184},{5,11,390},{6,11,337},{7,11,23},{7,11,494},{7,11,618 +},{7,11,1456},{8,11,27},{8,11,599},{10,11,153},{139,11,710},{135,0,466},{135,10, +1236},{4,10,480},{6,0,167},{6,10,302},{6,10,1642},{7,0,186},{7,0,656},{7,10,837} +,{7,10,1547},{7,10,1657},{8,10,429},{9,10,228},{10,0,643},{13,10,289},{13,10,343 +},{147,10,101},{134,0,1428},{134,0,1440},{5,0,412},{7,10,278},{10,10,739},{11,10 +,708},{141,10,348},{134,0,1118},{136,0,562},{148,11,46},{9,0,316},{139,0,256},{ +134,0,1771},{135,0,1190},{137,0,132},{10,11,227},{11,11,497},{11,11,709},{140,11 +,415},{143,0,66},{6,11,360},{7,11,1664},{136,11,478},{144,10,28},{4,0,317},{135, +0,1279},{5,0,63},{133,0,509},{136,11,699},{145,10,36},{134,0,1475},{11,11,343},{ +142,11,127},{132,11,739},{132,0,288},{135,11,1757},{8,0,89},{8,0,620},{9,0,608}, +{11,0,628},{12,0,322},{143,0,124},{134,0,1225},{4,11,67},{5,11,422},{6,10,363},{ +7,0,1189},{7,10,1955},{7,11,1037},{7,11,1289},{7,11,1555},{8,10,725},{9,11,741}, +{145,11,108},{134,0,1468},{6,0,689},{134,0,1451},{138,0,120},{151,0,1},{137,10, +805},{142,0,329},{5,10,813},{135,10,2046},{135,0,226},{138,11,96},{5,10,712},{7, +0,1855},{11,10,17},{13,10,321},{144,10,67},{6,10,320},{7,10,781},{7,10,1921},{9, +0,461},{9,10,55},{10,10,186},{10,10,273},{10,10,664},{10,10,801},{11,10,996},{11 +,10,997},{13,10,157},{142,10,170},{8,10,271},{8,11,203},{11,11,823},{11,11,846}, +{12,11,482},{13,11,133},{13,11,277},{13,11,302},{13,11,464},{14,11,205},{142,11, +221},{135,0,1346},{4,11,449},{133,11,718},{134,0,85},{7,10,103},{7,10,863},{11, +10,184},{14,0,299},{145,10,62},{4,11,355},{6,11,311},{9,11,256},{138,11,404},{ +137,10,659},{138,11,758},{133,11,827},{5,11,64},{140,11,581},{134,0,1171},{4,11, +442},{7,11,1047},{7,11,1352},{135,11,1643},{132,0,980},{5,11,977},{6,11,288},{7, +11,528},{135,11,1065},{5,0,279},{6,0,235},{7,0,468},{8,0,446},{9,0,637},{10,0, +717},{11,0,738},{140,0,514},{132,0,293},{11,10,337},{142,10,303},{136,11,285},{4 +,11,254},{5,0,17},{6,0,371},{9,0,528},{140,0,364},{5,10,77},{7,10,1455},{10,10, +843},{147,10,73},{150,0,5},{132,10,458},{6,11,12},{7,11,1219},{145,11,73},{135, +10,1420},{6,10,109},{138,10,382},{135,11,125},{6,10,330},{7,10,1084},{139,10,142 +},{6,11,369},{6,11,502},{7,11,1036},{8,11,348},{9,11,452},{10,11,26},{11,11,224} +,{11,11,387},{11,11,772},{12,11,95},{12,11,629},{13,11,195},{13,11,207},{13,11, +241},{14,11,260},{14,11,270},{143,11,140},{132,11,269},{5,11,480},{7,11,532},{7, +11,1197},{7,11,1358},{8,11,291},{11,11,349},{142,11,396},{150,0,48},{10,0,601},{ +13,0,353},{141,0,376},{5,0,779},{5,0,807},{6,0,1655},{134,0,1676},{142,11,223},{ +4,0,196},{5,0,558},{133,0,949},{148,11,15},{135,11,1764},{134,0,1322},{132,0,752 +},{139,0,737},{135,11,657},{136,11,533},{135,0,412},{4,0,227},{5,0,159},{5,0,409 +},{7,0,80},{8,0,556},{10,0,479},{12,0,418},{14,0,50},{14,0,123},{14,0,192},{14,0 +,249},{14,0,295},{143,0,27},{7,0,1470},{8,0,66},{8,0,137},{8,0,761},{9,0,638},{ +11,0,80},{11,0,212},{11,0,368},{11,0,418},{12,0,8},{13,0,15},{16,0,61},{17,0,59} +,{19,0,28},{148,0,84},{135,10,1985},{4,11,211},{4,11,332},{5,11,335},{6,11,238}, +{7,11,269},{7,11,811},{7,11,1797},{8,10,122},{8,11,836},{9,11,507},{141,11,242}, +{6,0,683},{134,0,1252},{4,0,873},{132,10,234},{134,0,835},{6,0,38},{7,0,1220},{8 +,0,185},{8,0,256},{9,0,22},{9,0,331},{10,0,738},{11,0,205},{11,0,540},{11,0,746} +,{13,0,465},{14,0,88},{142,0,194},{138,0,986},{5,11,1009},{12,11,582},{146,11, +131},{4,0,159},{6,0,115},{7,0,252},{7,0,257},{7,0,1928},{8,0,69},{9,0,384},{10,0 +,91},{10,0,615},{12,0,375},{14,0,235},{18,0,117},{147,0,123},{133,0,911},{136,0, +278},{5,10,430},{5,10,932},{6,10,131},{7,10,417},{9,10,522},{11,10,314},{141,10, +390},{14,10,149},{14,10,399},{143,10,57},{4,0,151},{7,0,1567},{136,0,749},{5,11, +228},{6,11,203},{7,11,156},{8,11,347},{137,11,265},{132,10,507},{10,0,989},{140, +0,956},{133,0,990},{5,0,194},{6,0,927},{7,0,1662},{9,0,90},{140,0,564},{4,10,343 +},{133,10,511},{133,0,425},{7,10,455},{138,10,591},{4,0,774},{7,11,476},{7,11, +1592},{138,11,87},{5,0,971},{135,10,1381},{5,11,318},{147,11,121},{5,11,291},{7, +11,765},{9,11,389},{140,11,548},{134,10,575},{4,0,827},{12,0,646},{12,0,705},{12 +,0,712},{140,0,714},{139,0,752},{137,0,662},{5,0,72},{6,0,264},{7,0,21},{7,0,46} +,{7,0,2013},{8,0,215},{8,0,513},{10,0,266},{139,0,22},{139,11,522},{6,0,239},{7, +0,118},{10,0,95},{11,0,603},{13,0,443},{14,0,160},{143,0,4},{6,0,431},{134,0,669 +},{7,10,1127},{7,10,1572},{10,10,297},{10,10,422},{11,10,764},{11,10,810},{12,10 +,264},{13,10,102},{13,10,300},{13,10,484},{14,10,147},{14,10,229},{17,10,71},{18 +,10,118},{147,10,120},{5,0,874},{6,0,1677},{10,11,525},{11,11,82},{143,0,0},{6,0 +,65},{7,0,939},{7,0,1172},{7,0,1671},{9,0,540},{10,0,696},{11,0,265},{11,0,732}, +{11,0,928},{11,0,937},{141,0,438},{134,0,1350},{136,11,547},{132,11,422},{5,11, +355},{145,11,0},{137,11,905},{5,0,682},{135,0,1887},{132,0,809},{4,0,696},{133, +11,865},{6,0,1074},{6,0,1472},{14,10,35},{142,10,191},{5,11,914},{134,11,1625},{ +133,11,234},{135,11,1383},{137,11,780},{132,10,125},{4,0,726},{133,0,630},{8,0, +802},{136,0,838},{132,10,721},{6,0,1337},{7,0,776},{8,10,145},{147,0,56},{132,0, +970},{7,10,792},{8,10,147},{10,10,821},{139,10,1021},{139,10,970},{8,0,940},{137 +,0,797},{135,11,1312},{7,10,1999},{7,11,816},{7,11,1241},{9,0,248},{9,11,283},{9 +,11,520},{10,0,400},{10,11,213},{10,11,307},{10,11,463},{10,11,671},{10,11,746}, +{11,11,401},{11,11,794},{12,11,517},{18,11,107},{147,11,115},{6,0,1951},{134,0, +2040},{135,11,339},{5,10,168},{5,10,930},{8,10,74},{9,10,623},{12,10,500},{12,10 +,579},{13,0,41},{143,0,93},{6,0,118},{7,0,215},{7,0,1521},{140,0,11},{6,10,220}, +{7,10,1101},{141,10,105},{6,11,421},{7,11,61},{7,11,1540},{10,11,11},{138,11,501 +},{7,0,615},{138,0,251},{140,11,631},{135,0,1044},{6,10,19},{7,10,1413},{139,10, +428},{133,0,225},{7,10,96},{8,10,401},{8,10,703},{137,10,896},{145,10,116},{6,11 +,102},{7,11,72},{15,11,142},{147,11,67},{7,10,1961},{7,10,1965},{8,10,702},{136, +10,750},{7,10,2030},{8,10,150},{8,10,737},{12,10,366},{151,11,30},{4,0,370},{5,0 +,756},{7,0,1326},{135,11,823},{8,10,800},{9,10,148},{9,10,872},{9,10,890},{11,10 +,309},{11,10,1001},{13,10,267},{141,10,323},{6,0,1662},{7,0,48},{8,0,771},{10,0, +116},{13,0,104},{14,0,105},{14,0,184},{15,0,168},{19,0,92},{148,0,68},{7,11,1870 +},{138,0,209},{7,11,68},{8,11,48},{8,11,88},{8,11,582},{8,11,681},{9,11,373},{9, +11,864},{11,11,157},{11,11,336},{11,11,843},{148,11,27},{134,0,930},{4,11,88},{5 +,11,137},{5,11,174},{5,11,777},{6,11,1664},{6,11,1725},{7,11,77},{7,11,426},{7, +11,1317},{7,11,1355},{8,11,126},{8,11,563},{9,11,523},{9,11,750},{10,11,310},{10 +,11,836},{11,11,42},{11,11,318},{11,11,731},{12,11,68},{12,11,92},{12,11,507},{ +12,11,692},{13,11,81},{13,11,238},{13,11,374},{18,11,138},{19,11,78},{19,11,111} +,{20,11,55},{20,11,77},{148,11,92},{4,11,938},{135,11,1831},{5,10,547},{7,10,424 +},{8,11,617},{138,11,351},{6,0,1286},{6,11,1668},{7,11,1499},{8,11,117},{9,11, +314},{138,11,174},{6,0,759},{6,0,894},{7,11,707},{139,11,563},{4,0,120},{135,0, +1894},{9,0,385},{149,0,17},{138,0,429},{133,11,403},{5,0,820},{135,0,931},{5,10, +133},{138,0,199},{6,0,151},{6,0,1675},{7,0,383},{151,0,10},{6,0,761},{136,10,187 +},{8,0,365},{10,10,0},{10,10,818},{139,10,988},{4,11,44},{5,11,311},{6,11,156},{ +7,11,639},{7,11,762},{7,11,1827},{9,11,8},{9,11,462},{148,11,83},{4,11,346},{7, +11,115},{9,11,180},{9,11,456},{138,11,363},{136,10,685},{7,0,1086},{145,0,46},{6 +,0,1624},{11,0,11},{12,0,422},{13,0,444},{142,0,360},{6,0,1020},{6,0,1260},{134, +0,1589},{4,0,43},{5,0,344},{5,0,357},{14,0,472},{150,0,58},{6,0,1864},{6,0,1866} +,{6,0,1868},{6,0,1869},{6,0,1874},{6,0,1877},{6,0,1903},{6,0,1911},{9,0,920},{9, +0,921},{9,0,924},{9,0,946},{9,0,959},{9,0,963},{9,0,970},{9,0,997},{9,0,1008},{9 +,0,1017},{12,0,795},{12,0,797},{12,0,798},{12,0,800},{12,0,803},{12,0,811},{12,0 +,820},{12,0,821},{12,0,839},{12,0,841},{12,0,848},{12,0,911},{12,0,921},{12,0, +922},{12,0,925},{12,0,937},{12,0,944},{12,0,945},{12,0,953},{15,0,184},{15,0,191 +},{15,0,199},{15,0,237},{15,0,240},{15,0,243},{15,0,246},{18,0,203},{21,0,40},{ +21,0,52},{21,0,57},{24,0,23},{24,0,28},{152,0,30},{134,0,725},{145,11,58},{133,0 +,888},{137,10,874},{4,0,711},{8,10,774},{10,10,670},{140,10,51},{144,11,40},{6, +11,185},{7,11,1899},{139,11,673},{137,10,701},{137,0,440},{4,11,327},{5,11,478}, +{7,11,1332},{8,11,753},{140,11,227},{4,10,127},{5,10,350},{6,10,356},{8,10,426}, +{9,10,572},{10,10,247},{139,10,312},{5,11,1020},{133,11,1022},{4,11,103},{133,11 +,401},{6,0,1913},{6,0,1926},{6,0,1959},{9,0,914},{9,0,939},{9,0,952},{9,0,979},{ +9,0,990},{9,0,998},{9,0,1003},{9,0,1023},{12,0,827},{12,0,834},{12,0,845},{12,0, +912},{12,0,935},{12,0,951},{15,0,172},{15,0,174},{18,0,198},{149,0,63},{4,11,499 +},{5,0,958},{5,0,987},{135,11,1421},{6,10,59},{6,10,1762},{7,0,885},{9,10,603},{ +141,10,397},{10,11,62},{141,11,164},{4,0,847},{135,0,326},{11,0,276},{142,0,293} +,{4,0,65},{4,11,96},{5,0,479},{5,0,1004},{7,0,1913},{8,0,317},{9,0,302},{10,0, +612},{141,0,22},{4,0,261},{135,0,510},{135,0,1514},{6,10,111},{7,10,4},{8,10,163 +},{8,10,776},{138,10,566},{4,0,291},{9,0,515},{12,0,152},{12,0,443},{13,0,392},{ +142,0,357},{7,11,399},{135,11,1492},{4,0,589},{139,0,282},{6,11,563},{135,10, +1994},{5,10,297},{135,10,1038},{4,0,130},{7,0,843},{135,0,1562},{5,0,42},{5,0, +879},{7,0,245},{7,0,324},{7,0,1532},{11,0,463},{11,0,472},{13,0,363},{144,0,52}, +{4,0,134},{133,0,372},{133,0,680},{136,10,363},{6,0,1997},{8,0,935},{136,0,977}, +{4,0,810},{135,0,1634},{135,10,1675},{4,11,910},{5,11,832},{135,0,1390},{7,10, +808},{8,11,266},{139,11,578},{132,0,644},{4,0,982},{138,0,867},{132,10,280},{135 +,0,540},{140,10,54},{135,0,123},{134,0,1978},{4,10,421},{133,10,548},{6,0,623},{ +136,0,789},{4,0,908},{5,0,359},{5,0,508},{6,0,1723},{7,0,343},{7,0,1996},{135,0, +2026},{134,0,1220},{4,0,341},{135,0,480},{6,10,254},{9,10,109},{138,10,103},{134 +,0,888},{8,11,528},{137,11,348},{4,11,20},{5,11,616},{7,0,1995},{8,0,299},{11,0, +890},{140,0,674},{135,11,1094},{134,10,1630},{4,0,238},{5,0,503},{6,0,179},{7,0, +2003},{8,0,381},{8,0,473},{9,0,149},{10,0,788},{15,0,45},{15,0,86},{20,0,110},{ +150,0,57},{133,10,671},{4,11,26},{5,11,429},{6,11,245},{7,11,704},{7,11,1379},{ +135,11,1474},{4,0,121},{5,0,156},{5,0,349},{9,0,431},{10,0,605},{142,0,342},{7, +11,943},{139,11,614},{132,10,889},{132,11,621},{7,10,1382},{7,10,1910},{135,11, +1382},{132,10,627},{133,10,775},{133,11,542},{133,11,868},{136,11,433},{6,0,1373 +},{7,0,1011},{11,10,362},{11,10,948},{140,10,388},{6,0,80},{7,0,173},{7,11,1495} +,{9,0,547},{10,0,730},{14,0,18},{150,0,39},{6,0,1694},{135,0,1974},{140,0,196},{ +4,0,923},{6,0,507},{6,0,1711},{7,10,451},{8,10,389},{12,10,490},{13,10,16},{13, +10,215},{13,10,351},{18,10,132},{147,10,125},{6,0,646},{134,0,1047},{135,10,841} +,{136,10,566},{6,0,1611},{135,0,1214},{139,0,926},{132,11,525},{132,0,595},{5,0, +240},{6,0,459},{7,0,12},{7,0,114},{7,0,949},{7,0,1753},{7,0,1805},{8,0,658},{9,0 +,1},{11,0,959},{141,0,446},{5,10,912},{134,10,1695},{132,0,446},{7,11,62},{12,11 +,45},{147,11,112},{5,10,236},{6,10,572},{8,10,492},{11,10,618},{144,10,56},{5,10 +,190},{136,10,318},{135,10,1376},{4,11,223},{6,11,359},{11,11,3},{13,11,108},{14 +,11,89},{144,11,22},{132,11,647},{134,0,490},{134,0,491},{134,0,1584},{135,11, +685},{138,11,220},{7,0,250},{136,0,507},{132,0,158},{4,0,140},{7,0,362},{8,0,209 +},{9,0,10},{9,0,160},{9,0,503},{9,0,614},{10,0,689},{11,0,327},{11,0,553},{11,0, +725},{11,0,767},{12,0,252},{12,0,583},{13,0,192},{14,0,269},{14,0,356},{148,0,50 +},{19,0,1},{19,0,26},{150,0,9},{132,11,109},{6,0,228},{7,0,1341},{9,0,408},{138, +0,343},{4,0,373},{5,0,283},{6,0,480},{7,0,609},{10,0,860},{138,0,878},{6,0,779}, +{134,0,1209},{4,0,557},{7,11,263},{7,11,628},{136,11,349},{132,0,548},{7,0,197}, +{8,0,142},{8,0,325},{9,0,150},{9,0,596},{10,0,350},{10,0,353},{11,0,74},{11,0, +315},{12,0,662},{12,0,681},{14,0,423},{143,0,141},{4,11,40},{10,11,67},{11,11, +117},{11,11,768},{139,11,935},{7,11,992},{8,11,301},{9,11,722},{12,11,63},{13,11 +,29},{14,11,161},{143,11,18},{6,0,1490},{138,11,532},{5,0,580},{7,0,378},{7,0, +674},{7,0,1424},{15,0,83},{15,11,83},{16,0,11},{144,11,11},{6,0,1057},{6,0,1335} +,{7,10,85},{7,10,247},{8,10,585},{10,0,316},{138,10,163},{4,0,169},{5,0,83},{6,0 +,399},{6,0,579},{6,0,1513},{7,0,692},{7,0,846},{7,0,1015},{7,0,1799},{8,0,403},{ +9,0,394},{10,0,133},{12,0,4},{12,0,297},{12,0,452},{16,0,81},{18,0,25},{21,0,14} +,{22,0,12},{151,0,18},{134,0,1106},{7,0,1546},{11,0,299},{142,0,407},{134,0,1192 +},{132,0,177},{5,0,411},{135,0,653},{7,0,439},{10,0,727},{11,0,260},{139,0,684}, +{138,10,145},{147,10,83},{5,0,208},{7,0,753},{135,0,1528},{137,11,617},{135,10, +1922},{135,11,825},{4,10,124},{10,10,457},{11,0,422},{11,10,121},{11,10,169},{11 +,10,870},{12,10,214},{13,0,389},{14,10,187},{143,10,77},{11,0,615},{11,11,615},{ +15,0,58},{143,11,58},{9,0,618},{138,0,482},{6,0,1952},{6,0,1970},{142,0,505},{7, +10,1193},{135,11,1838},{133,0,242},{135,10,1333},{6,10,107},{7,10,638},{7,10, +1632},{137,10,396},{133,0,953},{5,10,370},{134,10,1756},{5,11,28},{6,11,204},{10 +,11,320},{10,11,583},{13,11,502},{14,11,72},{14,11,274},{14,11,312},{14,11,344}, +{15,11,159},{16,11,62},{16,11,69},{17,11,30},{18,11,42},{18,11,53},{18,11,84},{ +18,11,140},{19,11,68},{19,11,85},{20,11,5},{20,11,45},{20,11,101},{22,11,7},{150 +,11,20},{4,11,558},{6,11,390},{7,11,162},{7,11,689},{9,11,360},{138,11,653},{11, +0,802},{141,0,67},{133,10,204},{133,0,290},{5,10,970},{134,10,1706},{132,0,380}, +{5,0,52},{7,0,277},{9,0,368},{139,0,791},{5,11,856},{6,11,1672},{6,11,1757},{6, +11,1781},{7,11,1150},{7,11,1425},{7,11,1453},{140,11,513},{5,11,92},{7,10,3},{10 +,11,736},{140,11,102},{4,0,112},{5,0,653},{5,10,483},{5,10,685},{6,10,489},{7,10 +,1204},{136,10,394},{132,10,921},{5,10,1007},{134,0,1028},{5,11,590},{9,11,213}, +{145,11,91},{135,10,1696},{10,0,138},{139,0,476},{5,0,725},{5,0,727},{135,0,1811 +},{4,0,979},{6,0,1821},{6,0,1838},{8,0,876},{8,0,883},{8,0,889},{8,0,893},{8,0, +895},{10,0,934},{12,0,720},{14,0,459},{148,0,123},{135,11,551},{4,0,38},{6,0,435 +},{7,0,307},{7,0,999},{7,0,1481},{7,0,1732},{7,0,1738},{8,0,371},{9,0,414},{11,0 +,316},{12,0,52},{13,0,420},{147,0,100},{135,0,1296},{132,10,712},{134,10,1629},{ +133,0,723},{134,0,651},{136,11,191},{9,11,791},{10,11,93},{11,11,301},{16,11,13} +,{17,11,23},{18,11,135},{19,11,12},{20,11,1},{20,11,12},{148,11,14},{136,11,503} +,{6,11,466},{135,11,671},{6,0,1200},{134,0,1330},{135,0,1255},{134,0,986},{5,0, +109},{6,0,1784},{7,0,1895},{12,0,296},{140,0,302},{135,11,983},{133,10,485},{134 +,0,660},{134,0,800},{4,10,285},{5,0,216},{5,0,294},{5,10,317},{6,0,591},{6,10, +301},{7,0,1879},{7,10,7},{8,10,153},{9,0,141},{9,0,270},{9,0,679},{10,0,159},{10 +,10,766},{11,0,197},{11,0,438},{11,10,468},{12,0,538},{12,0,559},{12,10,467},{13 +,10,143},{14,0,144},{14,0,167},{143,0,67},{136,0,945},{134,0,1090},{137,0,81},{ +12,11,468},{19,11,96},{148,11,24},{134,0,391},{138,11,241},{7,0,322},{136,0,249} +,{134,0,1412},{135,11,795},{5,0,632},{138,0,526},{136,10,819},{6,0,144},{7,0,948 +},{7,0,1042},{7,11,954},{8,0,235},{8,0,461},{9,0,453},{9,0,796},{10,0,354},{145, +0,77},{139,10,917},{6,0,940},{134,0,1228},{4,0,362},{7,0,52},{135,0,303},{6,11, +549},{8,11,34},{8,11,283},{9,11,165},{138,11,475},{7,11,370},{7,11,1007},{7,11, +1177},{135,11,1565},{5,11,652},{5,11,701},{135,11,449},{5,0,196},{6,0,486},{7,0, +212},{8,0,309},{136,0,346},{6,10,1719},{6,10,1735},{7,10,2016},{7,10,2020},{8,10 +,837},{137,10,852},{6,11,159},{6,11,364},{7,11,516},{7,11,1439},{137,11,518},{ +135,0,1912},{135,0,1290},{132,0,686},{141,11,151},{138,0,625},{136,0,706},{138, +10,568},{139,0,412},{4,0,30},{133,0,43},{8,10,67},{138,10,419},{7,0,967},{141,0, +11},{12,0,758},{14,0,441},{142,0,462},{10,10,657},{14,10,297},{142,10,361},{139, +10,729},{4,0,220},{135,0,1535},{7,11,501},{9,11,111},{10,11,141},{11,11,332},{13 +,11,43},{13,11,429},{14,11,130},{14,11,415},{145,11,102},{4,0,950},{6,0,1859},{7 +,0,11},{8,0,873},{12,0,710},{12,0,718},{12,0,748},{12,0,765},{148,0,124},{5,11, +149},{5,11,935},{136,11,233},{142,11,291},{134,0,1579},{7,0,890},{8,10,51},{9,10 +,868},{10,10,833},{12,10,481},{12,10,570},{148,10,106},{141,0,2},{132,10,445},{ +136,11,801},{135,0,1774},{7,0,1725},{138,0,393},{5,0,263},{134,0,414},{132,11, +322},{133,10,239},{7,0,456},{7,10,1990},{8,10,130},{139,10,720},{137,0,818},{5, +10,123},{6,10,530},{7,10,348},{135,10,1419},{135,10,2024},{6,0,178},{6,0,1750},{ +8,0,251},{9,0,690},{10,0,155},{10,0,196},{10,0,373},{11,0,698},{13,0,155},{148,0 +,93},{5,0,97},{137,0,393},{134,0,674},{11,0,223},{140,0,168},{132,10,210},{139, +11,464},{6,0,1639},{146,0,159},{139,11,2},{7,0,934},{8,0,647},{17,0,97},{19,0,59 +},{150,0,2},{132,0,191},{4,10,430},{5,0,165},{7,11,357},{9,0,346},{10,0,655},{ +139,0,885},{133,0,877},{5,10,213},{133,11,406},{8,0,128},{139,0,179},{6,11,69},{ +135,11,117},{135,0,1297},{11,11,43},{13,11,72},{141,11,142},{135,11,1830},{142,0 +,164},{5,0,57},{6,0,101},{6,0,586},{6,0,1663},{7,0,132},{7,0,1154},{7,0,1415},{7 +,0,1507},{12,0,493},{15,0,105},{151,0,15},{5,0,459},{7,0,1073},{8,0,241},{136,0, +334},{133,11,826},{133,10,108},{5,10,219},{10,11,132},{11,11,191},{11,11,358},{ +139,11,460},{6,0,324},{6,0,520},{7,0,338},{7,0,1729},{8,0,228},{139,0,750},{4,10 +,193},{5,10,916},{7,10,364},{10,10,398},{10,10,726},{11,10,317},{11,10,626},{12, +10,142},{12,10,288},{12,10,678},{13,10,313},{15,10,113},{18,10,114},{21,0,30},{ +150,0,53},{6,11,110},{135,11,1681},{135,0,910},{6,10,241},{7,10,907},{8,10,832}, +{9,10,342},{10,10,729},{11,10,284},{11,10,445},{11,10,651},{11,10,863},{13,10, +398},{146,10,99},{5,11,1000},{7,0,705},{7,11,733},{9,0,734},{137,11,583},{4,0,73 +},{6,0,612},{7,0,927},{7,0,1822},{8,0,217},{9,0,765},{9,0,766},{10,0,408},{11,0, +51},{11,0,793},{12,0,266},{15,0,158},{20,0,89},{150,0,32},{4,11,297},{6,11,529}, +{7,0,1330},{7,11,152},{7,11,713},{7,11,1845},{8,11,710},{8,11,717},{140,11,639}, +{5,0,389},{136,0,636},{134,0,1409},{4,10,562},{9,10,254},{139,10,879},{134,0,893 +},{132,10,786},{4,11,520},{135,11,575},{136,0,21},{140,0,721},{136,0,959},{7,11, +1428},{7,11,1640},{9,11,169},{9,11,182},{9,11,367},{9,11,478},{9,11,506},{9,11, +551},{9,11,648},{9,11,651},{9,11,697},{9,11,705},{9,11,725},{9,11,787},{9,11,794 +},{10,11,198},{10,11,214},{10,11,267},{10,11,275},{10,11,456},{10,11,551},{10,11 +,561},{10,11,613},{10,11,627},{10,11,668},{10,11,675},{10,11,691},{10,11,695},{ +10,11,707},{10,11,715},{11,11,183},{11,11,201},{11,11,244},{11,11,262},{11,11, +352},{11,11,439},{11,11,493},{11,11,572},{11,11,591},{11,11,608},{11,11,611},{11 +,11,646},{11,11,674},{11,11,711},{11,11,751},{11,11,761},{11,11,776},{11,11,785} +,{11,11,850},{11,11,853},{11,11,862},{11,11,865},{11,11,868},{11,11,898},{11,11, +902},{11,11,903},{11,11,910},{11,11,932},{11,11,942},{11,11,957},{11,11,967},{11 +,11,972},{12,11,148},{12,11,195},{12,11,220},{12,11,237},{12,11,318},{12,11,339} +,{12,11,393},{12,11,445},{12,11,450},{12,11,474},{12,11,509},{12,11,533},{12,11, +591},{12,11,594},{12,11,597},{12,11,621},{12,11,633},{12,11,642},{13,11,59},{13, +11,60},{13,11,145},{13,11,239},{13,11,250},{13,11,273},{13,11,329},{13,11,344},{ +13,11,365},{13,11,372},{13,11,387},{13,11,403},{13,11,414},{13,11,456},{13,11, +478},{13,11,483},{13,11,489},{14,11,55},{14,11,57},{14,11,81},{14,11,90},{14,11, +148},{14,11,239},{14,11,266},{14,11,321},{14,11,326},{14,11,327},{14,11,330},{14 +,11,347},{14,11,355},{14,11,401},{14,11,411},{14,11,414},{14,11,416},{14,11,420} +,{15,11,61},{15,11,74},{15,11,87},{15,11,88},{15,11,94},{15,11,96},{15,11,116},{ +15,11,149},{15,11,154},{16,11,50},{16,11,63},{16,11,73},{17,11,2},{17,11,66},{17 +,11,92},{17,11,103},{17,11,112},{18,11,50},{18,11,54},{18,11,82},{18,11,86},{18, +11,90},{18,11,111},{18,11,115},{18,11,156},{19,11,40},{19,11,79},{20,11,78},{149 +,11,22},{137,11,170},{134,0,1433},{135,11,1307},{139,11,411},{5,0,189},{7,0,442} +,{7,0,443},{8,0,281},{12,0,174},{141,0,261},{6,10,216},{7,10,901},{7,10,1343},{ +136,10,493},{5,11,397},{6,11,154},{7,10,341},{7,11,676},{8,11,443},{8,11,609},{9 +,11,24},{9,11,325},{10,11,35},{11,10,219},{11,11,535},{11,11,672},{11,11,1018},{ +12,11,637},{144,11,30},{6,0,2},{7,0,191},{7,0,446},{7,0,1262},{7,0,1737},{8,0,22 +},{8,0,270},{8,0,612},{9,0,4},{9,0,312},{9,0,436},{9,0,626},{10,0,216},{10,0,311 +},{10,0,521},{10,0,623},{11,0,72},{11,0,330},{11,0,455},{12,0,321},{12,0,504},{ +12,0,530},{12,0,543},{13,0,17},{13,0,156},{13,0,334},{14,0,131},{17,0,60},{148,0 +,64},{7,0,354},{10,0,410},{139,0,815},{139,10,130},{7,10,1734},{137,11,631},{10, +10,115},{11,10,420},{12,0,425},{13,10,404},{14,10,346},{15,0,112},{143,10,54},{6 +,0,60},{6,0,166},{7,0,374},{7,0,670},{7,0,1327},{8,0,411},{8,0,435},{9,0,653},{9 +,0,740},{10,0,385},{11,0,222},{11,0,324},{11,0,829},{140,0,611},{7,0,1611},{13,0 +,14},{15,0,44},{19,0,13},{148,0,76},{133,11,981},{4,11,56},{7,11,1791},{8,11,607 +},{8,11,651},{11,11,465},{11,11,835},{12,11,337},{141,11,480},{5,10,1011},{6,0, +1478},{136,10,701},{139,0,596},{5,0,206},{134,0,398},{4,10,54},{5,10,666},{7,10, +1039},{7,10,1130},{9,10,195},{138,10,302},{7,0,50},{9,11,158},{138,11,411},{135, +11,1120},{6,0,517},{6,10,1669},{7,0,1159},{10,0,621},{139,0,192},{4,0,592},{6,0, +600},{135,0,1653},{10,0,223},{139,0,645},{136,11,139},{7,0,64},{136,0,245},{142, +0,278},{6,11,622},{135,11,1030},{136,0,604},{134,0,1502},{138,0,265},{141,11,168 +},{7,0,1763},{140,0,310},{7,10,798},{139,11,719},{7,11,160},{10,11,624},{142,11, +279},{132,11,363},{7,10,122},{9,10,259},{10,10,84},{11,10,470},{12,10,541},{141, +10,379},{5,0,129},{6,0,61},{135,0,947},{134,0,1356},{135,11,1191},{13,0,505},{ +141,0,506},{5,10,82},{5,10,131},{7,10,1755},{8,10,31},{9,10,168},{9,10,764},{11, +0,1000},{139,10,869},{134,0,966},{134,10,605},{134,11,292},{5,11,177},{6,11,616} +,{7,11,827},{9,11,525},{138,11,656},{135,11,1486},{138,11,31},{5,10,278},{137,10 +,68},{4,10,163},{5,10,201},{5,10,307},{5,10,310},{6,10,335},{7,10,284},{136,10, +165},{6,0,839},{135,10,1660},{136,10,781},{6,10,33},{135,10,1244},{133,0,637},{4 +,11,161},{133,11,631},{137,0,590},{7,10,1953},{136,10,720},{5,0,280},{7,0,1226}, +{138,10,203},{134,0,1386},{5,0,281},{6,0,1026},{6,10,326},{7,10,677},{137,10,425 +},{7,11,1557},{135,11,1684},{135,0,1064},{9,11,469},{9,11,709},{12,11,512},{14, +11,65},{145,11,12},{134,0,917},{10,11,229},{11,11,73},{11,11,376},{139,11,433},{ +7,0,555},{7,10,1316},{7,10,1412},{7,10,1839},{9,0,192},{9,10,589},{11,10,241},{ +11,10,676},{11,10,811},{11,10,891},{12,10,140},{12,10,346},{12,10,479},{13,0,30} +,{13,0,49},{13,10,381},{14,10,188},{15,0,150},{16,0,76},{18,10,30},{148,0,52},{ +149,0,15},{6,0,1882},{6,0,1883},{6,0,1897},{9,0,945},{9,0,1014},{9,0,1020},{12,0 +,823},{12,0,842},{12,0,866},{12,0,934},{15,0,242},{146,0,208},{6,0,965},{134,0, +1499},{7,0,33},{7,0,120},{8,0,489},{9,0,319},{10,0,820},{11,0,1004},{12,0,379},{ +12,0,679},{13,0,117},{13,0,412},{14,0,25},{15,0,52},{15,0,161},{16,0,47},{149,0, +2},{6,11,558},{7,11,651},{8,11,421},{9,11,0},{138,11,34},{4,0,937},{5,0,801},{5, +10,358},{7,0,473},{7,10,1184},{10,10,662},{13,10,212},{13,10,304},{13,10,333},{ +145,10,98},{132,0,877},{6,0,693},{134,0,824},{132,0,365},{7,11,1832},{138,11,374 +},{5,0,7},{139,0,774},{4,0,734},{5,0,662},{134,0,430},{4,0,746},{135,0,1090},{5, +0,360},{8,0,237},{10,0,231},{147,0,124},{138,11,348},{6,11,6},{7,11,81},{7,11, +771},{7,11,1731},{9,11,405},{138,11,421},{6,0,740},{137,0,822},{133,10,946},{7,0 +,1485},{136,0,929},{7,10,411},{8,10,631},{9,10,323},{10,10,355},{11,10,491},{12, +10,143},{12,10,402},{13,10,73},{14,10,408},{15,10,107},{146,10,71},{135,10,590}, +{5,11,881},{133,11,885},{150,11,25},{4,0,852},{5,11,142},{134,11,546},{7,10,1467 +},{8,10,328},{10,10,544},{11,10,955},{13,10,320},{145,10,83},{9,0,17},{10,0,291} +,{11,10,511},{13,10,394},{14,10,298},{14,10,318},{146,10,103},{5,11,466},{11,11, +571},{12,11,198},{13,11,283},{14,11,186},{15,11,21},{143,11,103},{134,0,1001},{4 +,11,185},{5,11,257},{5,11,839},{5,11,936},{7,11,171},{9,11,399},{10,11,258},{10, +11,395},{10,11,734},{11,11,1014},{12,11,23},{13,11,350},{14,11,150},{147,11,6},{ +143,0,35},{132,0,831},{5,10,835},{134,10,483},{4,0,277},{5,0,608},{6,0,493},{7,0 +,457},{7,11,404},{7,11,1377},{7,11,1430},{7,11,2017},{8,11,149},{8,11,239},{8,11 +,512},{8,11,793},{8,11,818},{9,11,474},{9,11,595},{10,11,122},{10,11,565},{10,11 +,649},{10,11,783},{11,11,239},{11,11,295},{11,11,447},{11,11,528},{11,11,639},{ +11,11,800},{11,11,936},{12,0,384},{12,11,25},{12,11,73},{12,11,77},{12,11,157},{ +12,11,316},{12,11,390},{12,11,391},{12,11,394},{12,11,395},{12,11,478},{12,11, +503},{12,11,592},{12,11,680},{13,11,50},{13,11,53},{13,11,132},{13,11,198},{13, +11,275},{13,11,322},{13,11,415},{14,11,71},{14,11,257},{14,11,395},{15,11,71},{ +15,11,136},{17,11,123},{18,11,93},{147,11,58},{134,0,1351},{7,0,27},{135,0,316}, +{136,11,712},{136,0,984},{133,0,552},{137,0,264},{132,0,401},{6,0,710},{6,0,1111 +},{134,0,1343},{134,0,1211},{9,0,543},{10,0,524},{11,0,108},{11,0,653},{12,0,524 +},{13,0,123},{14,0,252},{16,0,18},{19,0,38},{20,0,26},{20,0,65},{21,0,3},{151,0, +11},{4,0,205},{5,0,623},{7,0,104},{8,0,519},{137,0,716},{132,10,677},{4,11,377}, +{152,11,13},{135,11,1673},{7,0,579},{9,0,41},{9,0,244},{9,0,669},{10,0,5},{11,0, +861},{11,0,951},{139,0,980},{132,0,717},{136,0,1011},{132,0,805},{4,11,180},{135 +,11,1906},{132,10,777},{132,10,331},{132,0,489},{4,11,491},{5,10,747},{134,0, +1024},{135,11,1182},{4,11,171},{138,11,234},{4,11,586},{7,11,1186},{138,11,631}, +{135,0,892},{135,11,336},{9,11,931},{10,11,334},{148,11,71},{137,0,473},{6,0,864 +},{11,11,926},{140,0,659},{7,0,819},{9,0,26},{9,0,392},{10,0,152},{10,0,226},{11 +,0,19},{12,0,276},{12,0,426},{12,0,589},{13,0,460},{15,0,97},{19,0,48},{148,0, +104},{135,0,51},{133,10,326},{4,10,691},{146,10,16},{9,0,130},{10,10,680},{10,10 +,793},{11,0,765},{141,10,357},{133,11,765},{6,10,32},{7,10,385},{7,10,757},{7,10 +,1916},{8,0,229},{8,10,94},{8,10,711},{9,10,541},{10,10,162},{10,10,795},{11,10, +989},{11,10,1010},{12,10,14},{142,10,308},{7,11,474},{137,11,578},{132,0,674},{ +132,0,770},{5,0,79},{7,0,1027},{7,0,1477},{139,0,52},{133,11,424},{134,0,1666},{ +6,0,409},{6,10,349},{6,10,1682},{7,10,1252},{8,10,112},{8,11,714},{9,10,435},{9, +10,668},{10,10,290},{10,10,319},{10,10,815},{11,10,180},{11,10,837},{12,10,240}, +{13,10,152},{13,10,219},{142,10,158},{5,0,789},{134,0,195},{4,0,251},{4,0,688},{ +7,0,513},{135,0,1284},{132,10,581},{9,11,420},{10,11,269},{10,11,285},{10,11,576 +},{11,11,397},{13,11,175},{145,11,90},{6,10,126},{7,10,573},{8,10,397},{142,10, +44},{132,11,429},{133,0,889},{4,0,160},{5,0,330},{7,0,1434},{136,0,174},{7,11,18 +},{7,11,699},{7,11,1966},{8,11,752},{9,11,273},{9,11,412},{9,11,703},{10,11,71}, +{10,11,427},{10,11,508},{146,11,97},{6,0,872},{134,0,899},{133,10,926},{134,0, +1126},{134,0,918},{4,11,53},{5,11,186},{135,11,752},{7,0,268},{136,0,569},{134,0 +,1224},{6,0,1361},{7,10,1232},{137,10,531},{8,11,575},{10,11,289},{139,11,319},{ +133,10,670},{132,11,675},{133,0,374},{135,10,1957},{133,0,731},{11,0,190},{11,11 +,190},{15,0,49},{143,11,49},{4,0,626},{5,0,506},{5,0,642},{6,0,425},{10,0,202},{ +139,0,141},{137,0,444},{7,10,242},{135,10,1942},{6,11,209},{8,11,468},{9,11,210} +,{11,11,36},{12,11,28},{12,11,630},{13,11,21},{13,11,349},{14,11,7},{145,11,13}, +{4,11,342},{135,11,1179},{5,10,834},{7,10,1202},{8,10,14},{9,10,481},{137,10,880 +},{4,11,928},{133,11,910},{4,11,318},{4,11,496},{7,11,856},{139,11,654},{136,0, +835},{7,0,1526},{138,10,465},{151,0,17},{135,0,477},{4,10,357},{6,10,172},{7,10, +143},{137,10,413},{6,0,1374},{138,0,994},{4,10,590},{146,0,76},{7,0,287},{8,0, +355},{9,0,293},{137,0,743},{134,0,1389},{7,11,915},{8,11,247},{147,11,0},{4,11, +202},{5,11,382},{6,11,454},{7,11,936},{7,11,1803},{8,11,758},{9,11,375},{9,11, +895},{10,11,743},{10,11,792},{11,11,978},{11,11,1012},{142,11,109},{5,0,384},{8, +0,455},{140,0,48},{132,11,390},{5,10,169},{7,10,333},{136,10,45},{5,0,264},{134, +0,184},{138,11,791},{133,11,717},{132,10,198},{6,11,445},{7,11,332},{137,11,909} +,{136,0,1001},{4,10,24},{5,10,140},{5,10,185},{7,10,1500},{11,10,565},{139,10, +838},{134,11,578},{5,0,633},{6,0,28},{135,0,1323},{132,0,851},{136,11,267},{7,0, +359},{8,0,243},{140,0,175},{4,10,334},{133,10,593},{141,11,87},{136,11,766},{10, +0,287},{10,11,287},{12,0,138},{140,11,138},{4,0,105},{132,0,740},{140,10,116},{ +134,0,857},{135,11,1841},{6,0,1402},{137,0,819},{132,11,584},{132,10,709},{133, +10,897},{5,0,224},{13,0,174},{146,0,52},{135,10,1840},{4,10,608},{133,10,497},{ +139,11,60},{4,0,758},{135,0,1649},{4,11,226},{4,11,326},{135,11,1770},{5,11,426} +,{8,11,30},{9,11,2},{11,11,549},{147,11,122},{135,10,2039},{6,10,540},{136,10, +136},{4,0,573},{4,10,897},{5,10,786},{136,0,655},{7,0,351},{139,0,128},{133,10, +999},{4,10,299},{135,10,1004},{133,0,918},{132,11,345},{4,11,385},{7,11,265},{ +135,11,587},{133,10,456},{136,10,180},{6,0,687},{134,0,1537},{4,11,347},{5,11, +423},{5,11,996},{135,11,1329},{132,10,755},{7,11,1259},{9,11,125},{11,11,65},{ +140,11,285},{5,11,136},{6,11,136},{136,11,644},{134,0,1525},{4,0,1009},{135,0, +1139},{139,10,338},{132,0,340},{135,10,1464},{8,0,847},{10,0,861},{10,0,876},{10 +,0,889},{10,0,922},{10,0,929},{10,0,933},{12,0,784},{140,0,791},{139,0,176},{9, +11,134},{10,11,2},{10,11,27},{10,11,333},{11,11,722},{143,11,1},{4,11,433},{133, +11,719},{5,0,985},{7,0,509},{7,0,529},{145,0,96},{132,0,615},{4,10,890},{5,10, +805},{5,10,819},{5,10,961},{6,10,396},{6,10,1631},{6,10,1678},{7,10,1967},{7,10, +2041},{9,10,630},{11,10,8},{11,10,1019},{12,10,176},{13,10,225},{14,10,292},{149 +,10,24},{135,0,1919},{134,0,1131},{144,11,21},{144,11,51},{135,10,1815},{4,0,247 +},{7,10,1505},{10,10,190},{10,10,634},{11,10,792},{12,10,358},{140,10,447},{5,10 +,0},{6,10,536},{7,10,604},{13,10,445},{145,10,126},{4,0,184},{5,0,390},{6,0,337} +,{7,0,23},{7,0,494},{7,0,618},{7,0,1456},{8,0,27},{8,0,599},{10,0,153},{139,0, +710},{6,10,232},{6,10,412},{7,10,1074},{8,10,9},{8,10,157},{8,10,786},{9,10,196} +,{9,10,352},{9,10,457},{10,10,337},{11,10,232},{11,10,877},{12,10,480},{140,10, +546},{7,10,958},{141,0,38},{4,10,382},{136,10,579},{4,10,212},{135,10,1206},{4, +11,555},{8,11,536},{138,11,288},{11,11,139},{139,11,171},{9,11,370},{138,11,90}, +{132,0,1015},{134,0,1088},{5,10,655},{135,11,977},{134,0,1585},{17,10,67},{147, +10,74},{10,0,227},{11,0,497},{11,0,709},{140,0,415},{6,0,360},{7,0,1664},{136,0, +478},{6,10,231},{7,0,95},{136,10,423},{140,11,65},{4,11,257},{135,11,2031},{135, +11,1768},{133,10,300},{139,11,211},{136,0,699},{6,10,237},{7,10,611},{8,10,100}, +{9,10,416},{11,10,335},{12,10,173},{146,10,101},{14,0,26},{146,0,150},{6,0,581}, +{135,0,1119},{135,10,1208},{132,0,739},{6,11,83},{6,11,1733},{135,11,1389},{137, +0,869},{4,0,67},{5,0,422},{7,0,1037},{7,0,1289},{7,0,1555},{9,0,741},{145,0,108} +,{133,10,199},{12,10,427},{146,10,38},{136,0,464},{142,0,42},{8,11,501},{9,11, +696},{138,0,96},{134,11,592},{4,0,512},{4,0,966},{5,0,342},{6,0,1855},{8,0,869}, +{8,0,875},{8,0,901},{144,0,26},{8,0,203},{11,0,823},{11,0,846},{12,0,482},{13,0, +277},{13,0,302},{13,0,464},{14,0,205},{142,0,221},{4,0,449},{133,0,718},{7,11, +1718},{9,11,95},{9,11,274},{10,11,279},{10,11,317},{10,11,420},{11,11,303},{11, +11,808},{12,11,134},{12,11,367},{13,11,149},{13,11,347},{14,11,349},{14,11,406}, +{18,11,22},{18,11,89},{18,11,122},{147,11,47},{133,11,26},{4,0,355},{6,0,311},{9 +,0,256},{138,0,404},{132,11,550},{6,10,312},{6,10,1715},{10,0,758},{10,10,584},{ +11,10,546},{11,10,692},{12,10,259},{12,10,295},{13,10,46},{141,10,154},{136,11, +822},{4,11,902},{5,0,827},{5,11,809},{6,11,122},{135,11,896},{5,0,64},{140,0,581 +},{4,0,442},{6,0,739},{7,0,1047},{7,0,1352},{7,0,1643},{7,11,1911},{9,11,449},{ +10,11,192},{138,11,740},{135,11,262},{132,10,588},{133,11,620},{4,11,34},{5,0, +977},{5,11,574},{6,0,288},{7,0,528},{7,11,279},{7,11,1624},{136,11,601},{4,10, +231},{5,10,61},{6,0,1375},{6,10,104},{7,10,729},{7,10,964},{7,10,1658},{140,10, +414},{6,10,263},{138,10,757},{132,10,320},{4,0,254},{5,11,332},{7,0,1309},{135, +11,1309},{6,11,261},{8,11,182},{139,11,943},{132,10,225},{6,0,12},{135,0,1219},{ +4,0,275},{6,11,1721},{12,0,376},{141,11,490},{4,11,933},{133,11,880},{4,10,405}, +{6,0,951},{6,0,1109},{6,0,1181},{7,0,154},{7,10,817},{14,10,58},{17,10,37},{146, +10,124},{5,10,974},{134,0,1520},{134,0,1753},{6,0,369},{6,0,502},{7,0,1036},{8,0 +,348},{9,0,452},{10,0,26},{11,0,224},{11,0,387},{11,0,772},{12,0,95},{12,0,629}, +{13,0,195},{13,0,207},{13,0,241},{14,0,260},{14,0,270},{143,0,140},{132,0,269},{ +5,0,480},{7,0,532},{7,0,1197},{7,0,1358},{8,0,291},{11,0,349},{142,0,396},{5,10, +235},{7,10,1239},{11,10,131},{140,10,370},{7,10,956},{7,10,1157},{7,10,1506},{7, +10,1606},{7,10,1615},{7,10,1619},{7,10,1736},{7,10,1775},{8,10,590},{9,10,324},{ +9,10,736},{9,10,774},{9,10,776},{9,10,784},{10,10,567},{10,10,708},{11,10,518},{ +11,10,613},{11,10,695},{11,10,716},{11,10,739},{11,10,770},{11,10,771},{11,10, +848},{11,10,857},{11,10,931},{11,10,947},{12,10,326},{12,10,387},{12,10,484},{12 +,10,528},{12,10,552},{12,10,613},{13,10,189},{13,10,256},{13,10,340},{13,10,432} +,{13,10,436},{13,10,440},{13,10,454},{14,10,174},{14,10,220},{14,10,284},{14,10, +390},{145,10,121},{8,11,598},{9,11,664},{138,11,441},{9,10,137},{138,10,221},{ +133,11,812},{148,0,15},{134,0,1341},{4,11,137},{6,0,1017},{7,11,1178},{135,11, +1520},{7,10,390},{138,10,140},{7,11,1260},{135,11,1790},{137,11,191},{135,10, +1144},{6,0,1810},{7,0,657},{8,0,886},{10,0,857},{14,0,440},{144,0,96},{6,11,1661 +},{7,11,1975},{7,11,2009},{7,11,2011},{136,0,533},{6,0,1453},{134,10,464},{132, +11,715},{5,10,407},{11,10,204},{11,10,243},{11,10,489},{12,10,293},{19,10,37},{ +20,10,73},{150,10,38},{133,11,703},{4,0,211},{5,10,325},{7,0,1483},{8,10,5},{8, +10,227},{9,10,105},{10,10,585},{140,10,614},{4,0,332},{5,0,335},{6,0,238},{7,0, +269},{7,0,811},{7,0,1797},{8,0,836},{9,0,507},{141,0,242},{5,11,89},{7,11,1915}, +{9,11,185},{9,11,235},{9,11,496},{10,11,64},{10,11,270},{10,11,403},{10,11,469}, +{10,11,529},{10,11,590},{11,11,140},{11,11,860},{13,11,1},{13,11,422},{14,11,341 +},{14,11,364},{17,11,93},{18,11,113},{19,11,97},{147,11,113},{133,11,695},{5,11, +6},{6,10,621},{6,11,183},{7,11,680},{7,11,978},{7,11,1013},{7,11,1055},{12,11, +230},{13,10,504},{13,11,172},{16,0,19},{146,11,29},{136,0,156},{133,0,1009},{6, +11,29},{139,11,63},{134,0,820},{134,10,218},{7,10,454},{7,10,782},{8,10,768},{ +140,10,686},{5,0,228},{6,0,203},{7,0,156},{8,0,347},{9,0,265},{15,11,8},{18,0,39 +},{18,11,39},{20,0,54},{20,11,54},{21,0,31},{21,11,31},{22,0,3},{22,11,3},{23,0, +0},{151,11,0},{7,0,1131},{135,0,1468},{144,10,0},{134,0,1276},{10,10,676},{140, +10,462},{132,11,311},{134,11,1740},{7,11,170},{8,11,90},{8,11,177},{8,11,415},{ +11,11,714},{142,11,281},{134,10,164},{6,0,1792},{138,0,849},{150,10,50},{5,0,291 +},{5,0,318},{7,0,765},{8,11,522},{9,0,389},{12,0,548},{142,11,328},{11,11,91},{ +13,11,129},{15,11,101},{145,11,125},{4,11,494},{6,11,74},{7,11,44},{7,11,407},{8 +,11,551},{12,11,17},{15,11,5},{148,11,11},{4,11,276},{133,11,296},{6,10,343},{7, +10,195},{7,11,1777},{9,10,226},{10,10,197},{10,10,575},{11,10,502},{139,10,899}, +{10,0,525},{139,0,82},{4,11,7},{5,11,90},{5,11,158},{6,11,542},{7,11,221},{7,11, +1574},{9,11,490},{10,11,540},{11,11,443},{11,11,757},{142,0,453},{135,0,666},{22 +,10,29},{150,11,29},{4,0,422},{147,10,8},{5,0,355},{145,0,0},{6,0,1873},{7,11, +588},{9,0,918},{9,11,175},{138,11,530},{143,11,31},{7,10,1125},{9,10,143},{11,0, +165},{14,10,405},{150,10,21},{9,0,260},{137,0,905},{5,11,872},{6,11,57},{6,11, +479},{6,11,562},{7,11,471},{7,11,1060},{9,11,447},{9,11,454},{141,11,6},{138,11, +704},{133,0,865},{5,0,914},{134,0,1625},{133,0,234},{5,11,31},{6,11,614},{7,0, +1383},{145,11,61},{7,11,1200},{138,11,460},{6,11,424},{135,11,1866},{136,0,306}, +{5,10,959},{12,11,30},{13,11,148},{14,11,87},{14,11,182},{16,11,42},{18,11,92},{ +148,11,70},{6,0,1919},{6,0,1921},{9,0,923},{9,0,930},{9,0,941},{9,0,949},{9,0, +987},{9,0,988},{9,0,992},{12,0,802},{12,0,815},{12,0,856},{12,0,885},{12,0,893}, +{12,0,898},{12,0,919},{12,0,920},{12,0,941},{12,0,947},{15,0,183},{15,0,185},{15 +,0,189},{15,0,197},{15,0,202},{15,0,233},{15,11,156},{18,0,218},{18,0,219},{146, +0,233},{135,10,1759},{136,10,173},{5,11,13},{6,11,142},{13,0,163},{13,0,180},{18 +,0,78},{148,0,35},{134,10,266},{6,11,97},{7,11,116},{8,11,322},{8,11,755},{9,11, +548},{10,11,714},{11,11,884},{141,11,324},{135,0,1312},{9,0,814},{137,11,676},{ +133,0,707},{135,0,1493},{6,0,421},{7,0,61},{7,0,1540},{10,0,11},{138,0,501},{7, +11,866},{7,11,1163},{12,0,733},{140,0,766},{137,0,341},{142,0,98},{145,11,115},{ +135,11,1111},{136,10,300},{136,0,1014},{8,11,1},{9,11,112},{138,11,326},{132,11, +730},{5,11,488},{6,11,527},{7,11,489},{7,11,1636},{8,11,121},{8,11,144},{8,11, +359},{9,11,193},{9,11,241},{9,11,336},{9,11,882},{11,11,266},{11,11,372},{11,11, +944},{12,11,401},{140,11,641},{6,0,971},{134,0,1121},{6,0,102},{7,0,72},{15,0, +142},{147,0,67},{151,0,30},{135,0,823},{134,0,1045},{5,10,427},{5,10,734},{7,10, +478},{136,10,52},{7,0,1930},{11,10,217},{142,10,165},{6,0,1512},{135,0,1870},{9, +11,31},{10,11,244},{10,11,699},{12,11,149},{141,11,497},{133,11,377},{145,11,101 +},{10,11,158},{13,11,13},{13,11,137},{13,11,258},{14,11,111},{14,11,225},{14,11, +253},{14,11,304},{14,11,339},{14,11,417},{146,11,33},{6,0,87},{6,10,1734},{7,10, +20},{7,10,1056},{8,10,732},{9,10,406},{9,10,911},{138,10,694},{134,0,1243},{137, +0,245},{7,0,68},{8,0,48},{8,0,88},{8,0,582},{8,0,681},{9,0,373},{9,0,864},{11,0, +157},{11,0,336},{11,0,843},{148,0,27},{8,11,663},{144,11,8},{133,10,613},{4,0,88 +},{5,0,137},{5,0,174},{5,0,777},{6,0,1664},{6,0,1725},{7,0,77},{7,0,426},{7,0, +1317},{7,0,1355},{8,0,126},{8,0,563},{9,0,523},{9,0,750},{10,0,310},{10,0,836},{ +11,0,42},{11,0,318},{11,0,731},{12,0,68},{12,0,92},{12,0,507},{12,0,692},{13,0, +81},{13,0,238},{13,0,374},{14,0,436},{18,0,138},{19,0,78},{19,0,111},{20,0,55},{ +20,0,77},{148,0,92},{141,0,418},{4,0,938},{137,0,625},{138,0,351},{5,11,843},{7, +10,32},{7,10,984},{8,10,85},{8,10,709},{9,10,579},{9,10,847},{9,10,856},{10,10, +799},{11,10,258},{11,10,1007},{12,10,331},{12,10,615},{13,10,188},{13,10,435},{ +14,10,8},{15,10,165},{16,10,27},{148,10,40},{6,0,1668},{7,0,1499},{8,0,117},{9,0 +,314},{138,0,174},{135,0,707},{132,11,554},{133,11,536},{5,0,403},{5,11,207},{9, +11,79},{11,11,625},{145,11,7},{132,11,424},{136,11,785},{4,10,167},{135,10,82},{ +9,0,7},{9,11,7},{23,0,6},{151,11,6},{5,10,62},{6,0,282},{6,10,534},{7,10,74},{7, +10,678},{7,10,684},{7,10,1043},{7,10,1072},{8,10,280},{8,10,541},{8,10,686},{9, +10,258},{10,10,519},{11,10,252},{140,10,282},{138,10,33},{132,10,359},{4,0,44},{ +5,0,311},{6,0,156},{7,0,639},{7,0,762},{7,0,1827},{9,0,8},{9,0,462},{148,0,83},{ +7,11,769},{9,11,18},{138,11,358},{4,0,346},{4,11,896},{6,11,1777},{7,0,115},{9,0 +,180},{9,0,456},{138,0,363},{133,10,211},{7,0,761},{7,0,1051},{137,0,545},{6,10, +145},{141,10,336},{7,11,750},{9,11,223},{11,11,27},{11,11,466},{12,11,624},{14, +11,265},{146,11,61},{6,0,752},{6,0,768},{6,0,1195},{6,0,1254},{6,0,1619},{137,0, +835},{6,0,1936},{8,0,930},{136,0,960},{132,10,263},{132,11,249},{4,10,916},{140, +0,653},{4,11,603},{133,11,661},{4,11,11},{6,11,128},{7,11,231},{7,11,1533},{8,0, +344},{138,11,725},{134,0,1483},{134,0,875},{6,0,185},{7,0,1899},{9,0,875},{139,0 +,673},{15,10,155},{144,10,79},{4,10,599},{6,10,1634},{7,0,93},{7,0,210},{7,0, +1223},{7,10,67},{7,10,691},{7,10,979},{7,10,1697},{8,0,451},{8,0,460},{8,10,207} +,{8,10,214},{8,10,231},{8,10,294},{8,10,336},{8,10,428},{8,10,471},{8,10,622},{8 +,10,626},{8,10,679},{8,10,759},{8,10,829},{9,10,11},{9,10,246},{9,10,484},{9,10, +573},{9,10,706},{9,10,762},{9,10,798},{9,10,855},{9,10,870},{9,10,912},{10,10, +303},{10,10,335},{10,10,424},{10,10,461},{10,10,543},{10,10,759},{10,10,814},{11 +,0,353},{11,0,475},{11,10,59},{11,10,235},{11,10,590},{11,10,929},{11,10,963},{ +11,10,987},{12,10,114},{12,10,182},{12,10,226},{12,10,332},{12,10,439},{12,10, +575},{12,10,598},{12,10,675},{13,10,8},{13,10,125},{13,10,194},{13,10,287},{14, +10,197},{14,10,383},{15,10,53},{17,10,63},{19,10,46},{19,10,98},{19,10,106},{148 +,10,85},{132,11,476},{4,0,327},{5,0,478},{7,0,1332},{136,0,753},{5,0,1020},{133, +0,1022},{135,11,1807},{4,0,103},{133,0,401},{4,0,499},{135,0,1421},{10,0,207},{ +13,0,164},{147,10,126},{9,11,20},{10,11,324},{139,11,488},{132,0,96},{9,11,280}, +{138,11,134},{135,0,968},{133,10,187},{135,10,1286},{5,11,112},{6,11,103},{134, +11,150},{4,10,215},{8,0,914},{9,10,38},{10,0,3},{11,10,23},{11,10,127},{139,10, +796},{135,0,399},{6,0,563},{137,0,224},{6,0,704},{134,0,1214},{4,11,708},{8,11, +15},{9,11,50},{9,11,386},{11,11,18},{11,11,529},{140,11,228},{4,11,563},{7,11, +109},{7,11,592},{7,11,637},{7,11,770},{7,11,1701},{8,11,436},{8,11,463},{9,11,60 +},{9,11,335},{9,11,904},{10,11,73},{11,11,434},{12,11,585},{13,11,331},{18,11, +110},{148,11,60},{134,0,1559},{132,11,502},{6,11,347},{138,11,161},{4,11,33},{5, +11,102},{5,11,500},{6,11,284},{7,11,1079},{7,11,1423},{7,11,1702},{8,11,470},{9, +11,554},{9,11,723},{139,11,333},{7,11,246},{135,11,840},{6,11,10},{8,11,571},{9, +11,739},{143,11,91},{5,11,626},{8,0,861},{10,0,905},{12,0,730},{140,0,789},{134, +0,946},{5,0,746},{12,0,333},{12,11,333},{14,0,332},{142,11,332},{5,11,18},{6,11, +526},{13,11,24},{13,11,110},{19,11,5},{147,11,44},{4,0,910},{5,0,832},{135,10, +2002},{10,11,768},{139,11,787},{4,11,309},{5,11,462},{7,11,970},{135,11,1097},{4 +,10,28},{5,10,440},{7,10,248},{11,10,833},{140,10,344},{134,10,1654},{6,0,632},{ +6,0,652},{6,0,1272},{6,0,1384},{134,0,1560},{134,11,1704},{5,10,853},{134,0,1393 +},{6,10,249},{7,10,1234},{139,10,573},{5,11,86},{7,11,743},{9,11,85},{10,11,281} +,{10,11,432},{11,11,490},{12,11,251},{13,11,118},{14,11,378},{146,11,143},{5,11, +524},{133,11,744},{134,0,1514},{10,0,201},{142,0,319},{7,0,717},{7,10,392},{8,10 +,20},{8,10,172},{8,10,690},{9,10,383},{9,10,845},{10,0,510},{11,10,293},{11,10, +832},{11,10,920},{11,10,984},{141,10,221},{134,0,1381},{5,10,858},{133,10,992},{ +8,0,528},{137,0,348},{10,11,107},{140,11,436},{4,0,20},{133,0,616},{134,0,1251}, +{132,11,927},{10,11,123},{12,11,670},{13,11,371},{14,11,142},{146,11,94},{134,0, +1163},{7,11,1149},{137,11,156},{134,0,307},{133,11,778},{7,0,1091},{135,0,1765}, +{5,11,502},{6,10,268},{137,10,62},{8,11,196},{10,11,283},{139,11,406},{4,0,26},{ +5,0,429},{6,0,245},{7,0,704},{7,0,1379},{135,0,1474},{133,11,855},{132,0,881},{4 +,0,621},{135,11,1596},{7,11,1400},{9,11,446},{138,11,45},{6,0,736},{138,10,106}, +{133,0,542},{134,0,348},{133,0,868},{136,0,433},{135,0,1495},{138,0,771},{6,10, +613},{136,10,223},{138,0,215},{141,0,124},{136,11,391},{135,11,172},{132,10,670} +,{140,0,55},{9,10,40},{139,10,136},{7,0,62},{147,0,112},{132,0,856},{132,11,568} +,{11,10,259},{140,0,270},{8,0,572},{137,0,698},{4,11,732},{9,10,310},{137,10,682 +},{142,10,296},{134,0,939},{136,11,733},{135,11,1435},{7,10,1401},{135,10,1476}, +{4,10,296},{6,0,352},{7,10,401},{7,10,1410},{7,10,1594},{7,10,1674},{8,10,63},{8 +,10,660},{137,10,74},{4,11,428},{133,11,668},{4,10,139},{4,10,388},{140,10,188}, +{7,11,2015},{140,11,665},{132,0,647},{146,0,10},{138,0,220},{142,0,464},{132,0, +109},{134,0,1746},{4,10,747},{6,0,515},{6,11,1623},{6,11,1681},{7,10,649},{7,10, +1479},{135,10,1583},{133,10,232},{135,0,566},{137,10,887},{4,0,40},{10,0,67},{11 +,0,117},{11,0,768},{139,0,935},{132,0,801},{7,0,992},{8,0,301},{9,0,722},{12,0, +63},{13,0,29},{14,0,161},{143,0,18},{139,0,923},{6,11,1748},{8,11,715},{9,11,802 +},{10,11,46},{10,11,819},{13,11,308},{14,11,351},{14,11,363},{146,11,67},{137,11 +,745},{4,10,14},{7,0,1145},{7,10,1801},{10,10,748},{141,10,458},{4,11,63},{5,11, +347},{134,11,474},{135,0,568},{4,10,425},{7,11,577},{7,11,1432},{9,11,475},{9,11 +,505},{9,11,526},{9,11,609},{9,11,689},{9,11,726},{9,11,735},{9,11,738},{10,11, +556},{10,11,674},{10,11,684},{11,11,89},{11,11,202},{11,11,272},{11,11,380},{11, +11,415},{11,11,505},{11,11,537},{11,11,550},{11,11,562},{11,11,640},{11,11,667}, +{11,11,688},{11,11,847},{11,11,927},{11,11,930},{11,11,940},{12,11,144},{12,11, +325},{12,11,329},{12,11,389},{12,11,403},{12,11,451},{12,11,515},{12,11,604},{12 +,11,616},{12,11,626},{13,11,66},{13,11,131},{13,11,167},{13,11,236},{13,11,368}, +{13,11,411},{13,11,434},{13,11,453},{13,11,461},{13,11,474},{14,11,59},{14,11,60 +},{14,11,139},{14,11,152},{14,11,276},{14,11,353},{14,11,402},{15,11,28},{15,11, +81},{15,11,123},{15,11,152},{18,11,136},{148,11,88},{137,0,247},{135,11,1622},{9 +,11,544},{11,11,413},{144,11,25},{4,0,645},{6,10,1768},{7,0,825},{135,11,89},{ +140,0,328},{5,10,943},{134,10,1779},{134,0,1363},{5,10,245},{6,10,576},{7,10,582 +},{136,10,225},{134,0,1280},{5,11,824},{133,11,941},{7,11,440},{8,11,230},{139, +11,106},{5,0,28},{6,0,204},{10,0,320},{10,0,583},{13,0,502},{14,0,72},{14,0,274} +,{14,0,312},{14,0,344},{15,0,159},{16,0,62},{16,0,69},{17,0,30},{18,0,42},{18,0, +53},{18,0,84},{18,0,140},{19,0,68},{19,0,85},{20,0,5},{20,0,45},{20,0,101},{22,0 +,7},{150,0,20},{4,0,558},{6,0,390},{7,0,162},{7,0,689},{9,0,360},{138,0,653},{ +134,0,764},{6,0,862},{137,0,833},{5,0,856},{6,0,1672},{6,0,1757},{134,0,1781},{5 +,0,92},{10,0,736},{140,0,102},{6,0,1927},{6,0,1944},{8,0,924},{8,0,948},{10,0, +967},{138,0,978},{134,0,1479},{5,0,590},{8,0,360},{9,0,213},{138,0,63},{134,0, +1521},{6,0,709},{134,0,891},{132,10,443},{13,0,477},{14,0,120},{148,0,61},{4,11, +914},{5,11,800},{133,11,852},{10,11,54},{141,11,115},{4,11,918},{133,11,876},{ +139,11,152},{4,11,92},{133,11,274},{135,11,1901},{9,11,800},{10,11,693},{11,11, +482},{11,11,734},{139,11,789},{4,10,298},{137,0,483},{6,0,1213},{141,11,498},{ +135,11,1451},{133,11,743},{4,0,1022},{10,0,1000},{12,0,957},{12,0,980},{12,0, +1013},{14,0,481},{144,0,116},{4,11,49},{7,11,280},{7,11,1633},{8,0,503},{145,0, +29},{135,0,1712},{134,0,466},{136,11,47},{5,10,164},{7,10,121},{142,10,189},{7, +10,812},{7,10,1261},{7,10,1360},{9,10,632},{140,10,352},{139,10,556},{132,0,731} +,{5,11,272},{5,11,908},{5,11,942},{7,11,1008},{7,11,1560},{8,11,197},{9,11,47},{ +11,11,538},{139,11,742},{4,10,172},{9,10,611},{10,10,436},{12,10,673},{141,10, +255},{133,10,844},{10,0,484},{11,0,754},{12,0,457},{14,0,171},{14,0,389},{146,0, +153},{9,10,263},{10,10,147},{138,10,492},{137,11,891},{138,0,241},{133,10,537},{ +6,0,2005},{136,0,964},{137,10,842},{151,11,8},{4,11,407},{132,11,560},{135,11, +1884},{6,0,1100},{134,0,1242},{135,0,954},{5,10,230},{5,10,392},{6,10,420},{9,10 +,568},{140,10,612},{4,11,475},{11,11,35},{11,11,90},{13,11,7},{13,11,71},{13,11, +177},{142,11,422},{136,11,332},{135,0,1958},{6,0,549},{8,0,34},{8,0,283},{9,0, +165},{138,0,475},{10,0,952},{12,0,966},{140,0,994},{5,0,652},{5,0,701},{135,0, +449},{4,0,655},{7,0,850},{17,0,75},{146,0,137},{4,0,146},{5,10,41},{7,0,1618},{7 +,10,1459},{7,10,1469},{7,10,1859},{8,0,670},{9,10,549},{139,10,905},{133,10,696} +,{6,0,159},{6,0,364},{7,0,516},{137,0,518},{135,0,1439},{6,11,222},{7,11,636},{7 +,11,1620},{8,11,409},{9,11,693},{139,11,77},{13,0,151},{141,11,45},{4,10,771},{4 +,11,336},{134,0,1027},{139,11,392},{10,11,121},{11,11,175},{149,11,16},{8,0,950} +,{138,0,983},{133,10,921},{135,0,993},{6,10,180},{7,10,1137},{8,10,751},{139,10, +805},{7,0,501},{9,0,111},{10,0,141},{11,0,332},{13,0,43},{13,0,429},{14,0,130},{ +14,0,415},{145,0,102},{4,10,183},{5,11,882},{7,10,271},{11,10,824},{11,10,952},{ +13,10,278},{13,10,339},{13,10,482},{14,10,424},{148,10,99},{4,10,19},{5,10,477}, +{5,10,596},{6,10,505},{7,10,1221},{11,10,907},{12,10,209},{141,10,214},{135,10, +1215},{133,0,452},{132,11,426},{5,0,149},{136,0,233},{133,0,935},{6,11,58},{7,11 +,654},{7,11,745},{7,11,1969},{8,11,240},{8,11,675},{9,11,479},{9,11,731},{10,11, +330},{10,11,593},{10,11,817},{11,11,32},{11,11,133},{11,11,221},{145,11,68},{7, +11,102},{9,11,538},{12,0,582},{146,0,131},{136,0,801},{134,10,1645},{132,0,70},{ +6,10,92},{6,10,188},{7,10,1269},{7,10,1524},{7,10,1876},{10,10,228},{139,10,1020 +},{4,10,459},{133,10,966},{138,0,369},{12,10,330},{144,0,36},{141,11,366},{6,10, +18},{7,0,721},{7,10,932},{8,10,757},{9,10,54},{9,10,65},{9,10,844},{10,0,236},{ +10,10,113},{10,10,315},{10,10,798},{11,10,153},{12,0,204},{12,10,151},{12,10,392 +},{12,10,666},{142,10,248},{7,0,241},{8,10,548},{9,10,532},{10,0,430},{10,10,117 +},{11,10,351},{11,10,375},{143,10,23},{134,10,1742},{133,10,965},{133,11,566},{6 +,11,48},{135,11,63},{134,10,182},{10,10,65},{10,10,488},{138,10,497},{6,11,114}, +{7,11,1224},{7,11,1556},{136,11,3},{134,0,1817},{8,11,576},{137,11,267},{6,0, +1078},{144,0,16},{9,10,588},{138,10,260},{138,0,1021},{5,0,406},{134,0,2022},{ +133,11,933},{6,0,69},{135,0,117},{7,0,1830},{136,11,427},{4,0,432},{135,0,824},{ +134,10,1786},{133,0,826},{139,11,67},{133,11,759},{135,10,308},{137,0,816},{133, +0,1000},{4,0,297},{6,0,529},{7,0,152},{7,0,713},{7,0,1845},{8,0,710},{8,0,717},{ +12,0,639},{140,0,685},{7,0,423},{136,10,588},{136,10,287},{136,0,510},{134,0, +1048},{6,0,618},{7,11,56},{7,11,1989},{8,11,337},{8,11,738},{9,11,600},{10,11, +483},{12,11,37},{13,11,447},{142,11,92},{4,0,520},{135,0,575},{8,0,990},{138,0, +977},{135,11,774},{9,11,347},{11,11,24},{140,11,170},{136,11,379},{140,10,290},{ +132,11,328},{4,0,321},{134,0,569},{4,11,101},{135,11,1171},{5,11,833},{7,0,723}, +{7,0,1135},{136,11,744},{7,10,719},{8,10,809},{136,10,834},{8,0,921},{136,10,796 +},{5,10,210},{6,10,213},{7,10,60},{10,10,364},{139,10,135},{5,0,397},{5,10,607}, +{6,0,154},{7,0,676},{8,0,443},{8,0,609},{8,10,326},{8,10,490},{9,0,24},{9,0,325} +,{10,0,35},{11,0,535},{11,0,672},{11,0,1018},{12,0,637},{144,0,30},{4,10,701},{5 +,10,472},{6,11,9},{6,11,397},{7,11,53},{7,11,1742},{9,10,758},{10,11,632},{11,11 +,828},{140,11,146},{135,10,380},{135,10,1947},{148,11,109},{10,10,278},{138,11, +278},{134,0,856},{4,10,386},{7,0,139},{8,10,405},{8,10,728},{9,10,497},{11,10, +110},{11,10,360},{15,10,37},{144,10,84},{141,0,282},{133,0,981},{5,0,288},{7,10, +1452},{7,10,1480},{8,10,634},{140,10,472},{7,0,1890},{8,11,367},{10,11,760},{14, +11,79},{20,11,17},{152,11,0},{4,10,524},{136,10,810},{4,0,56},{7,0,1791},{8,0, +607},{8,0,651},{11,0,465},{11,0,835},{12,0,337},{141,0,480},{10,10,238},{141,10, +33},{11,11,417},{12,11,223},{140,11,265},{9,0,158},{10,0,411},{140,0,261},{133, +10,532},{133,10,997},{12,11,186},{12,11,292},{14,11,100},{146,11,70},{6,0,1403}, +{136,0,617},{134,0,1205},{139,0,563},{4,0,242},{134,0,333},{4,11,186},{5,11,157} +,{8,11,168},{138,11,6},{132,0,369},{133,11,875},{5,10,782},{5,10,829},{134,10, +1738},{134,0,622},{135,11,1272},{6,0,1407},{7,11,111},{136,11,581},{7,10,1823},{ +139,10,693},{7,0,160},{10,0,624},{142,0,279},{132,0,363},{10,11,589},{12,11,111} +,{13,11,260},{14,11,82},{18,11,63},{147,11,45},{7,11,1364},{7,11,1907},{141,11, +158},{4,11,404},{4,11,659},{135,11,675},{13,11,211},{14,11,133},{14,11,204},{15, +11,64},{15,11,69},{15,11,114},{16,11,10},{19,11,23},{19,11,35},{19,11,39},{19,11 +,51},{19,11,71},{19,11,75},{152,11,15},{4,10,78},{5,10,96},{5,10,182},{7,10,1724 +},{7,10,1825},{10,10,394},{10,10,471},{11,10,532},{14,10,340},{145,10,88},{135, +10,1964},{133,11,391},{11,11,887},{14,11,365},{142,11,375},{5,11,540},{6,11,1697 +},{7,11,222},{136,11,341},{134,11,78},{9,0,601},{9,0,619},{10,0,505},{10,0,732}, +{11,0,355},{140,0,139},{134,0,292},{139,0,174},{5,0,177},{6,0,616},{7,0,827},{9, +0,525},{138,0,656},{6,10,215},{7,10,1028},{7,10,1473},{7,10,1721},{9,10,424},{10 +,0,31},{138,10,779},{135,10,584},{136,11,293},{134,0,685},{135,11,1868},{133,11, +460},{6,10,67},{7,0,647},{7,10,1630},{9,10,354},{9,10,675},{10,10,830},{14,10,80 +},{145,10,80},{4,0,161},{133,0,631},{6,10,141},{7,10,225},{9,10,59},{9,10,607},{ +10,10,312},{11,10,687},{12,10,555},{13,10,373},{13,10,494},{148,10,58},{7,11,965 +},{7,11,1460},{135,11,1604},{136,10,783},{134,11,388},{4,11,511},{6,0,722},{6,0, +1267},{9,11,333},{9,11,379},{10,11,602},{11,11,441},{11,11,723},{11,11,976},{140 +,11,357},{134,0,1797},{135,0,1684},{5,11,938},{8,11,707},{9,0,469},{9,0,709},{12 +,0,512},{14,0,65},{145,0,12},{7,0,1230},{136,0,531},{10,0,229},{11,0,73},{11,0, +376},{139,0,433},{12,0,268},{12,0,640},{142,0,119},{7,10,430},{139,10,46},{6,0, +558},{7,0,651},{8,0,421},{9,0,0},{10,0,34},{139,0,1008},{5,10,602},{6,0,106},{7, +0,1786},{7,0,1821},{7,10,2018},{9,0,102},{9,0,763},{137,10,418},{5,0,65},{6,0, +416},{7,0,1720},{7,0,1924},{8,10,677},{10,0,109},{11,0,14},{11,0,70},{11,0,569}, +{11,0,735},{15,0,153},{148,0,80},{135,11,1625},{137,11,772},{136,0,595},{6,11, +469},{7,11,1709},{138,11,515},{7,0,1832},{138,0,374},{9,0,106},{9,0,163},{9,0, +296},{10,0,167},{10,0,172},{10,0,777},{139,0,16},{6,0,6},{7,0,81},{7,0,771},{7,0 +,1731},{9,0,405},{138,0,421},{4,11,500},{135,11,938},{5,11,68},{134,11,383},{5,0 +,881},{133,0,885},{6,0,854},{6,0,1132},{6,0,1495},{6,0,1526},{6,0,1533},{134,0, +1577},{4,11,337},{6,11,353},{7,11,1934},{8,11,488},{137,11,429},{7,11,236},{7,11 +,1795},{8,11,259},{9,11,135},{9,11,177},{10,11,825},{11,11,115},{11,11,370},{11, +11,405},{11,11,604},{12,11,10},{12,11,667},{12,11,669},{13,11,76},{14,11,310},{ +15,11,76},{15,11,147},{148,11,23},{5,0,142},{134,0,546},{4,11,15},{5,11,22},{6, +11,244},{7,11,40},{7,11,200},{7,11,906},{7,11,1199},{9,11,616},{10,11,716},{11, +11,635},{11,11,801},{140,11,458},{5,0,466},{7,10,329},{11,0,571},{12,0,198},{13, +0,283},{14,0,186},{15,0,21},{143,0,103},{4,0,185},{5,0,257},{5,0,839},{5,0,936}, +{7,11,1735},{9,0,399},{10,0,258},{10,0,395},{10,0,734},{11,0,1014},{12,0,23},{13 +,0,350},{14,0,150},{147,0,6},{12,11,36},{141,11,337},{5,11,598},{7,11,791},{8,11 +,108},{137,11,123},{132,10,469},{7,0,404},{7,0,1377},{7,0,1430},{7,0,2017},{8,0, +149},{8,0,239},{8,0,512},{8,0,793},{8,0,818},{9,0,474},{9,0,595},{10,0,122},{10, +0,565},{10,0,649},{10,0,783},{11,0,239},{11,0,295},{11,0,447},{11,0,528},{11,0, +639},{11,0,800},{12,0,25},{12,0,77},{12,0,157},{12,0,256},{12,0,316},{12,0,390}, +{12,0,391},{12,0,395},{12,0,478},{12,0,503},{12,0,592},{12,0,680},{13,0,50},{13, +0,53},{13,0,132},{13,0,198},{13,0,322},{13,0,415},{13,0,511},{14,0,71},{14,0,395 +},{15,0,71},{15,0,136},{17,0,123},{18,0,93},{147,0,58},{136,0,712},{134,10,1743} +,{5,10,929},{6,10,340},{8,10,376},{136,10,807},{6,0,1848},{8,0,860},{10,0,856},{ +10,0,859},{10,0,925},{10,0,941},{140,0,762},{6,0,629},{6,0,906},{9,0,810},{140,0 +,652},{5,10,218},{7,10,1610},{138,10,83},{7,10,1512},{135,10,1794},{4,0,377},{4, +11,155},{7,11,1689},{11,10,0},{16,10,78},{152,0,13},{4,11,164},{5,11,151},{5,11, +730},{5,11,741},{7,11,498},{7,11,870},{7,11,1542},{12,11,213},{14,11,36},{14,11, +391},{17,11,111},{18,11,6},{18,11,46},{18,11,151},{19,11,36},{20,11,32},{20,11, +56},{20,11,69},{20,11,102},{21,11,4},{22,11,8},{22,11,10},{22,11,14},{150,11,31} +,{5,10,571},{135,0,1842},{4,10,455},{4,11,624},{135,11,1752},{134,0,1501},{4,11, +492},{5,11,451},{6,10,161},{7,10,372},{137,10,597},{132,10,349},{4,0,180},{135,0 +,1906},{135,11,835},{141,11,70},{132,0,491},{137,10,751},{6,10,432},{139,10,322} +,{4,0,171},{138,0,234},{6,11,113},{135,11,436},{4,0,586},{7,0,1186},{138,0,631}, +{5,10,468},{10,10,325},{11,10,856},{12,10,345},{143,10,104},{5,10,223},{10,11, +592},{10,11,753},{12,11,317},{12,11,355},{12,11,465},{12,11,469},{12,11,560},{12 +,11,578},{141,11,243},{132,10,566},{135,11,520},{4,10,59},{135,10,1394},{6,10, +436},{139,10,481},{4,10,48},{5,10,271},{7,10,953},{7,11,1878},{9,0,931},{10,0, +334},{148,0,71},{5,10,610},{8,10,457},{139,0,170},{133,10,755},{6,0,1587},{135, +10,1217},{4,10,197},{149,11,26},{133,11,585},{137,11,521},{133,0,765},{133,10, +217},{139,11,586},{133,0,424},{9,11,752},{12,11,610},{13,11,431},{16,11,59},{146 +,11,109},{136,0,714},{4,11,307},{135,0,685},{9,0,420},{10,0,269},{10,0,285},{10, +0,576},{11,0,397},{13,0,175},{145,0,90},{132,0,429},{133,11,964},{9,11,463},{138 +,11,595},{7,0,18},{7,0,699},{7,0,1966},{8,0,752},{9,0,273},{9,0,412},{9,0,703},{ +10,0,71},{10,0,427},{138,0,508},{4,10,165},{7,10,1398},{135,10,1829},{4,0,53},{5 +,0,186},{7,0,752},{7,0,828},{142,0,116},{8,0,575},{10,0,289},{139,0,319},{132,0, +675},{134,0,1424},{4,11,75},{5,11,180},{6,11,500},{7,11,58},{7,11,710},{138,11, +645},{133,11,649},{6,11,276},{7,11,282},{7,11,879},{7,11,924},{8,11,459},{9,11, +599},{9,11,754},{11,11,574},{12,11,128},{12,11,494},{13,11,52},{13,11,301},{15, +11,30},{143,11,132},{6,0,647},{134,0,1095},{5,10,9},{7,10,297},{7,10,966},{140, +10,306},{132,11,200},{134,0,1334},{5,10,146},{6,10,411},{138,10,721},{6,0,209},{ +6,0,1141},{6,0,1288},{8,0,468},{9,0,210},{11,0,36},{12,0,28},{12,0,630},{13,0,21 +},{13,0,349},{14,0,7},{145,0,13},{6,10,177},{135,10,467},{4,0,342},{135,0,1179}, +{10,11,454},{140,11,324},{4,0,928},{133,0,910},{6,11,225},{7,0,1838},{137,11,211 +},{16,0,101},{20,0,115},{20,0,118},{148,0,122},{4,0,496},{135,0,856},{4,0,318},{ +7,11,718},{11,0,654},{139,11,102},{8,11,58},{9,11,724},{11,11,809},{13,11,113},{ +145,11,72},{5,10,200},{6,11,345},{135,11,1247},{8,11,767},{8,11,803},{9,11,301}, +{137,11,903},{7,0,915},{7,11,1949},{8,0,247},{8,11,674},{147,0,0},{4,0,202},{5,0 +,382},{6,0,454},{7,0,936},{7,0,1803},{8,0,758},{9,0,375},{9,0,895},{10,0,743},{ +10,0,792},{11,0,978},{11,0,1012},{142,0,109},{7,0,1150},{7,0,1425},{7,0,1453},{ +140,0,513},{134,11,259},{138,0,791},{11,0,821},{12,0,110},{12,0,153},{18,0,41},{ +150,0,19},{134,10,481},{132,0,796},{6,0,445},{8,11,254},{137,0,909},{10,0,776},{ +13,0,345},{142,0,425},{4,10,84},{7,10,1482},{10,10,76},{138,10,142},{135,11,742} +,{5,10,1015},{134,0,578},{4,10,315},{5,10,507},{6,0,1387},{135,10,1370},{4,0,438 +},{133,0,555},{136,0,766},{133,11,248},{134,10,1722},{4,11,116},{5,11,95},{5,11, +445},{7,11,1688},{8,11,29},{9,11,272},{11,11,509},{139,11,915},{135,0,541},{133, +11,543},{8,10,222},{8,10,476},{9,10,238},{11,10,516},{11,10,575},{15,10,109},{ +146,10,100},{6,0,880},{134,0,1191},{5,11,181},{136,11,41},{134,0,1506},{132,11, +681},{7,11,25},{8,11,202},{138,11,536},{139,0,983},{137,0,768},{132,0,584},{9,11 +,423},{140,11,89},{8,11,113},{9,11,877},{10,11,554},{11,11,83},{12,11,136},{147, +11,109},{7,10,706},{7,10,1058},{138,10,538},{133,11,976},{4,11,206},{135,11,746} +,{136,11,526},{140,0,737},{11,10,92},{11,10,196},{11,10,409},{11,10,450},{11,10, +666},{11,10,777},{12,10,262},{13,10,385},{13,10,393},{15,10,115},{16,10,45},{145 +,10,82},{4,0,226},{4,0,326},{4,11,319},{5,11,699},{7,0,1770},{138,11,673},{6,10, +40},{135,10,1781},{5,0,426},{8,0,30},{9,0,2},{11,0,549},{147,0,122},{6,0,1161},{ +134,0,1329},{138,10,97},{6,10,423},{7,10,665},{135,10,1210},{7,11,13},{8,11,226} +,{10,11,537},{11,11,570},{11,11,605},{11,11,799},{11,11,804},{12,11,85},{12,11, +516},{12,11,623},{13,11,112},{13,11,361},{14,11,77},{14,11,78},{17,11,28},{147, +11,110},{132,11,769},{132,11,551},{132,11,728},{147,0,117},{9,11,57},{9,11,459}, +{10,11,425},{11,11,119},{12,11,184},{12,11,371},{13,11,358},{145,11,51},{5,11, +188},{5,11,814},{8,11,10},{9,11,421},{9,11,729},{10,11,609},{139,11,689},{134,11 +,624},{135,11,298},{135,0,462},{4,0,345},{139,10,624},{136,10,574},{4,0,385},{7, +0,265},{135,0,587},{4,11,528},{134,0,808},{133,0,398},{132,10,354},{4,0,347},{5, +0,423},{5,0,996},{135,0,1329},{135,10,1558},{7,0,1259},{9,0,125},{139,0,65},{5,0 +,136},{6,0,136},{136,0,644},{5,11,104},{6,11,173},{135,11,1631},{135,0,469},{133 +,10,830},{4,0,278},{5,0,465},{135,0,1367},{7,11,810},{8,11,138},{8,11,342},{9,11 +,84},{10,11,193},{11,11,883},{140,11,359},{5,10,496},{135,10,203},{4,0,433},{133 +,0,719},{6,10,547},{134,11,95},{5,10,88},{137,10,239},{6,11,406},{10,11,409},{10 +,11,447},{11,11,44},{140,11,100},{134,0,1423},{7,10,650},{135,10,1310},{134,0, +749},{135,11,1243},{135,0,1363},{6,0,381},{7,0,645},{7,0,694},{7,10,1076},{8,0, +546},{9,10,80},{11,10,78},{11,10,421},{11,10,534},{140,10,545},{134,11,1636},{ +135,11,1344},{7,10,274},{11,10,479},{11,10,507},{140,0,277},{4,11,282},{6,0,705} +,{6,0,783},{6,0,1275},{6,0,1481},{7,11,1034},{11,11,398},{11,11,634},{12,11,1},{ +12,11,79},{12,11,544},{14,11,237},{17,11,10},{146,11,20},{134,0,453},{4,0,555},{ +4,10,497},{7,10,1584},{8,0,536},{10,0,288},{139,0,1005},{5,11,118},{5,11,499},{6 +,11,476},{7,11,600},{7,11,888},{135,11,1096},{138,0,987},{7,0,1107},{7,10,261},{ +7,10,1115},{7,10,1354},{7,10,1588},{7,10,1705},{7,10,1902},{9,10,465},{10,10,248 +},{10,10,349},{10,10,647},{11,10,527},{11,10,660},{11,10,669},{12,10,529},{141, +10,305},{7,11,296},{7,11,596},{8,11,560},{8,11,586},{9,11,612},{11,11,100},{11, +11,304},{12,11,46},{13,11,89},{14,11,112},{145,11,122},{9,0,370},{138,0,90},{136 +,10,13},{132,0,860},{7,10,642},{8,10,250},{11,10,123},{11,10,137},{13,10,48},{ +142,10,95},{135,10,1429},{137,11,321},{132,0,257},{135,0,2031},{7,0,1768},{7,11, +1599},{7,11,1723},{8,11,79},{8,11,106},{8,11,190},{8,11,302},{8,11,383},{9,11, +119},{9,11,233},{9,11,298},{9,11,419},{9,11,471},{10,11,181},{10,11,406},{11,11, +57},{11,11,85},{11,11,120},{11,11,177},{11,11,296},{11,11,382},{11,11,454},{11, +11,758},{11,11,999},{12,11,27},{12,11,98},{12,11,131},{12,11,245},{12,11,312},{ +12,11,446},{12,11,454},{13,11,25},{13,11,98},{13,11,426},{13,11,508},{14,11,6},{ +14,11,163},{14,11,272},{14,11,277},{14,11,370},{15,11,95},{15,11,138},{15,11,167 +},{17,11,18},{17,11,38},{20,11,96},{149,11,32},{5,11,722},{134,11,1759},{145,11, +16},{6,0,1071},{134,0,1561},{10,10,545},{140,10,301},{6,0,83},{6,0,1733},{135,0, +1389},{4,0,835},{135,0,1818},{133,11,258},{4,10,904},{133,10,794},{134,0,2006},{ +5,11,30},{7,11,495},{8,11,134},{9,11,788},{140,11,438},{135,11,2004},{137,0,696} +,{5,11,50},{6,11,439},{7,11,780},{135,11,1040},{7,11,772},{7,11,1104},{7,11,1647 +},{11,11,269},{11,11,539},{11,11,607},{11,11,627},{11,11,706},{11,11,975},{12,11 +,248},{12,11,311},{12,11,434},{12,11,600},{12,11,622},{13,11,297},{13,11,367},{ +13,11,485},{14,11,69},{14,11,409},{143,11,108},{5,11,1},{6,11,81},{138,11,520},{ +7,0,1718},{9,0,95},{9,0,274},{10,0,279},{10,0,317},{10,0,420},{11,0,303},{11,0, +808},{12,0,134},{12,0,367},{13,0,149},{13,0,347},{14,0,349},{14,0,406},{18,0,22} +,{18,0,89},{18,0,122},{147,0,47},{5,11,482},{8,11,98},{9,11,172},{10,11,222},{10 +,11,700},{10,11,822},{11,11,302},{11,11,778},{12,11,50},{12,11,127},{12,11,396}, +{13,11,62},{13,11,328},{14,11,122},{147,11,72},{7,10,386},{138,10,713},{6,10,7}, +{6,10,35},{7,10,147},{7,10,1069},{7,10,1568},{7,10,1575},{7,10,1917},{8,10,43},{ +8,10,208},{9,10,128},{9,10,866},{10,10,20},{11,10,981},{147,10,33},{133,0,26},{ +132,0,550},{5,11,2},{7,11,1494},{136,11,589},{6,11,512},{7,11,797},{8,11,253},{9 +,11,77},{10,11,1},{10,11,129},{10,11,225},{11,11,118},{11,11,226},{11,11,251},{ +11,11,430},{11,11,701},{11,11,974},{11,11,982},{12,11,64},{12,11,260},{12,11,488 +},{140,11,690},{7,10,893},{141,10,424},{134,0,901},{136,0,822},{4,0,902},{5,0, +809},{134,0,122},{6,0,807},{134,0,1366},{5,11,748},{6,11,553},{135,0,262},{133,0 +,620},{4,0,34},{5,0,574},{7,0,279},{7,0,1624},{136,0,601},{6,10,322},{9,0,170},{ +9,10,552},{11,10,274},{13,10,209},{13,10,499},{14,10,85},{15,10,126},{145,10,70} +,{132,0,537},{4,11,12},{7,11,420},{7,11,522},{7,11,809},{8,11,797},{141,11,88},{ +133,0,332},{8,10,83},{8,10,742},{8,10,817},{9,10,28},{9,10,29},{9,10,885},{10,10 +,387},{11,10,633},{11,10,740},{13,10,235},{13,10,254},{15,10,143},{143,10,146},{ +6,0,1909},{9,0,964},{12,0,822},{12,0,854},{12,0,865},{12,0,910},{12,0,938},{15,0 +,169},{15,0,208},{15,0,211},{18,0,205},{18,0,206},{18,0,220},{18,0,223},{152,0, +24},{140,10,49},{5,11,528},{135,11,1580},{6,0,261},{8,0,182},{139,0,943},{134,0, +1721},{4,0,933},{133,0,880},{136,11,321},{5,11,266},{9,11,290},{9,11,364},{10,11 +,293},{11,11,606},{142,11,45},{4,11,50},{6,0,1609},{6,11,510},{6,11,594},{9,11, +121},{10,11,49},{10,11,412},{139,11,834},{7,0,895},{136,11,748},{132,11,466},{4, +10,110},{10,10,415},{10,10,597},{142,10,206},{133,0,812},{135,11,281},{6,0,1890} +,{6,0,1902},{6,0,1916},{7,10,205},{7,10,2000},{9,0,929},{9,0,942},{9,0,975},{9,0 +,984},{9,0,986},{9,0,1011},{9,0,1019},{12,0,804},{12,0,851},{12,0,867},{12,0,916 +},{12,0,923},{15,0,194},{15,0,204},{15,0,210},{15,0,222},{15,0,223},{15,0,229},{ +15,0,250},{18,0,179},{18,0,186},{146,0,192},{132,11,667},{135,0,778},{4,0,137},{ +7,0,1178},{135,0,1520},{134,0,1314},{4,11,242},{134,11,333},{6,0,1661},{7,0,1975 +},{7,0,2009},{135,0,2011},{134,0,1591},{4,10,283},{135,10,1194},{11,0,820},{150, +0,51},{4,11,39},{5,11,36},{7,11,1843},{8,11,407},{11,11,144},{140,11,523},{134, +10,1720},{4,11,510},{7,11,29},{7,11,66},{7,11,1980},{10,11,487},{10,11,809},{146 +,11,9},{5,0,89},{7,0,1915},{9,0,185},{9,0,235},{10,0,64},{10,0,270},{10,0,403},{ +10,0,469},{10,0,529},{10,0,590},{11,0,140},{11,0,860},{13,0,1},{13,0,422},{14,0, +341},{14,0,364},{17,0,93},{18,0,113},{19,0,97},{147,0,113},{133,0,695},{6,0,987} +,{134,0,1160},{5,0,6},{6,0,183},{7,0,680},{7,0,978},{7,0,1013},{7,0,1055},{12,0, +230},{13,0,172},{146,0,29},{134,11,570},{132,11,787},{134,11,518},{6,0,29},{139, +0,63},{132,11,516},{136,11,821},{132,0,311},{134,0,1740},{7,0,170},{8,0,90},{8,0 +,177},{8,0,415},{8,10,735},{11,0,714},{142,0,281},{134,0,1961},{135,11,1405},{4, +11,10},{7,11,917},{139,11,786},{5,10,132},{9,10,486},{9,10,715},{10,10,458},{11, +10,373},{11,10,668},{11,10,795},{11,10,897},{12,10,272},{12,10,424},{12,10,539}, +{12,10,558},{14,10,245},{14,10,263},{14,10,264},{14,10,393},{142,10,403},{11,0, +91},{13,0,129},{15,0,101},{145,0,125},{135,0,1132},{4,0,494},{6,0,74},{7,0,44},{ +7,0,407},{12,0,17},{15,0,5},{148,0,11},{133,10,379},{5,0,270},{5,11,684},{6,10, +89},{6,10,400},{7,10,1569},{7,10,1623},{7,10,1850},{8,10,218},{8,10,422},{9,10, +570},{138,10,626},{4,0,276},{133,0,296},{6,0,1523},{134,11,27},{6,10,387},{7,10, +882},{141,10,111},{6,10,224},{7,10,877},{137,10,647},{135,10,790},{4,0,7},{5,0, +90},{5,0,158},{6,0,542},{7,0,221},{7,0,1574},{9,0,490},{10,0,540},{11,0,443},{ +139,0,757},{7,0,588},{9,0,175},{138,0,530},{135,10,394},{142,11,23},{134,0,786}, +{135,0,580},{7,0,88},{136,0,627},{5,0,872},{6,0,57},{7,0,471},{9,0,447},{137,0, +454},{6,11,342},{6,11,496},{8,11,275},{137,11,206},{4,11,909},{133,11,940},{4,11 +,891},{134,0,735},{7,10,1409},{8,0,845},{136,0,916},{5,0,31},{134,0,614},{11,0, +458},{12,0,15},{140,0,432},{8,0,330},{140,0,477},{4,0,530},{4,11,687},{5,0,521}, +{7,0,1200},{138,0,460},{6,0,424},{135,0,1866},{9,0,569},{12,0,12},{12,0,81},{12, +0,319},{13,0,69},{14,0,259},{16,0,87},{17,0,1},{17,0,21},{17,0,24},{18,0,15},{18 +,0,56},{18,0,59},{18,0,127},{18,0,154},{19,0,19},{148,0,31},{7,0,1302},{136,10, +38},{134,11,253},{5,10,261},{7,10,78},{7,10,199},{8,10,815},{9,10,126},{138,10, +342},{5,0,595},{135,0,1863},{6,11,41},{141,11,160},{5,0,13},{134,0,142},{6,0,97} +,{7,0,116},{7,11,1304},{8,0,322},{8,0,755},{9,0,548},{10,0,714},{10,11,477},{11, +0,884},{141,0,324},{132,10,628},{134,11,1718},{7,10,266},{136,10,804},{135,10, +208},{6,10,79},{7,0,1021},{135,10,1519},{7,0,1472},{135,0,1554},{6,11,362},{146, +11,51},{7,0,1071},{7,0,1541},{7,0,1767},{7,0,1806},{11,0,162},{11,0,242},{11,0, +452},{12,0,605},{15,0,26},{144,0,44},{136,10,741},{133,11,115},{145,0,115},{134, +10,376},{6,0,1406},{134,0,1543},{5,11,193},{12,11,178},{13,11,130},{145,11,84},{ +135,0,1111},{5,11,705},{8,0,1},{9,0,650},{9,11,606},{138,0,326},{5,0,488},{6,0, +527},{7,0,489},{7,0,1636},{8,0,121},{8,0,144},{8,0,359},{9,0,193},{9,0,241},{9,0 +,336},{9,0,882},{11,0,266},{11,0,372},{11,0,944},{12,0,401},{140,0,641},{135,11, +174},{6,0,267},{7,10,244},{7,10,632},{7,10,1609},{8,10,178},{8,10,638},{141,10, +58},{134,0,1983},{134,0,1155},{134,0,1575},{134,0,1438},{9,0,31},{10,0,244},{10, +0,699},{12,0,149},{141,0,497},{133,0,377},{4,11,122},{5,11,796},{5,11,952},{6,11 +,1660},{6,11,1671},{8,11,567},{9,11,687},{9,11,742},{10,11,686},{11,11,356},{11, +11,682},{140,11,281},{145,0,101},{11,11,0},{144,11,78},{5,10,791},{5,11,179},{7, +11,1095},{135,11,1213},{8,11,372},{9,11,122},{138,11,175},{7,10,686},{8,10,33},{ +8,10,238},{10,10,616},{11,10,467},{11,10,881},{13,10,217},{13,10,253},{142,10, +268},{4,11,66},{7,11,722},{7,11,904},{137,0,476},{7,11,352},{137,11,684},{135,0, +2023},{135,0,1836},{132,10,447},{5,0,843},{144,0,35},{137,11,779},{141,11,35},{4 +,10,128},{5,10,415},{6,10,462},{7,10,294},{7,10,578},{10,10,710},{139,10,86},{ +132,0,554},{133,0,536},{136,10,587},{5,0,207},{9,0,79},{11,0,625},{145,0,7},{6, +10,427},{7,0,1371},{138,10,692},{4,0,424},{4,10,195},{135,10,802},{5,11,564},{ +136,0,785},{135,0,336},{4,0,896},{6,0,1777},{134,11,556},{137,11,103},{134,10, +1683},{7,11,544},{8,11,719},{138,11,61},{138,10,472},{4,11,5},{5,11,498},{136,11 +,637},{7,0,750},{9,0,223},{11,0,27},{11,0,466},{12,0,624},{14,0,265},{146,0,61}, +{12,0,238},{12,11,238},{18,0,155},{146,11,155},{151,10,28},{133,11,927},{5,10,3} +,{8,10,578},{9,10,118},{10,10,705},{12,0,383},{141,10,279},{4,11,893},{5,11,780} +,{133,11,893},{4,0,603},{133,0,661},{4,0,11},{5,10,229},{5,11,238},{6,0,128},{7, +0,231},{7,0,1533},{7,11,1350},{138,0,725},{8,10,102},{10,10,578},{10,10,672},{12 +,10,496},{13,10,408},{14,10,121},{145,10,106},{132,0,476},{134,0,1552},{134,11, +1729},{8,10,115},{8,10,350},{9,10,489},{10,10,128},{11,10,306},{12,10,373},{14, +10,30},{17,10,79},{19,10,80},{150,10,55},{135,0,1807},{4,0,680},{4,11,60},{7,11, +760},{7,11,1800},{8,11,314},{9,11,700},{139,11,487},{4,10,230},{5,10,702},{148, +11,94},{132,11,228},{139,0,435},{9,0,20},{10,0,324},{10,0,807},{139,0,488},{6,10 +,1728},{136,11,419},{4,10,484},{18,10,26},{19,10,42},{20,10,43},{21,10,0},{23,10 +,27},{152,10,14},{135,0,1431},{133,11,828},{5,0,112},{6,0,103},{6,0,150},{7,0, +1303},{7,11,176},{7,11,178},{7,11,1110},{9,0,292},{10,0,481},{10,11,481},{20,0, +13},{148,11,13},{138,0,356},{4,11,51},{5,11,39},{6,11,4},{7,11,591},{7,11,849},{ +7,11,951},{7,11,1129},{7,11,1613},{7,11,1760},{7,11,1988},{9,11,434},{10,11,754} +,{11,11,25},{11,11,37},{139,11,414},{6,0,1963},{134,0,2000},{132,10,633},{5,11, +902},{134,0,1244},{135,11,928},{140,0,18},{138,0,204},{135,11,1173},{134,0,867}, +{4,0,708},{8,0,15},{9,0,50},{9,0,386},{11,0,18},{11,0,529},{140,0,228},{134,11, +270},{4,0,563},{7,0,109},{7,0,592},{7,0,637},{7,0,770},{8,0,463},{9,0,60},{9,0, +335},{9,0,904},{10,0,73},{11,0,434},{12,0,585},{13,0,331},{18,0,110},{148,0,60}, +{132,0,502},{14,11,359},{19,11,52},{148,11,47},{6,11,377},{7,11,1025},{9,11,613} +,{145,11,104},{5,10,70},{5,10,622},{6,0,347},{6,10,334},{7,10,1032},{9,10,171},{ +10,0,161},{11,10,26},{11,10,213},{11,10,637},{11,10,707},{12,10,202},{12,10,380} +,{13,10,226},{13,10,355},{14,10,222},{145,10,42},{132,11,416},{4,0,33},{5,0,102} +,{6,0,284},{7,0,1079},{7,0,1423},{7,0,1702},{8,0,470},{9,0,554},{9,0,723},{11,0, +333},{142,11,372},{5,11,152},{5,11,197},{7,11,340},{7,11,867},{10,11,548},{10,11 +,581},{11,11,6},{12,11,3},{12,11,19},{14,11,110},{142,11,289},{7,0,246},{135,0, +840},{6,0,10},{8,0,571},{9,0,739},{143,0,91},{4,10,23},{4,10,141},{5,10,313},{5, +10,1014},{6,0,465},{6,10,50},{7,0,1465},{7,10,142},{7,10,559},{8,10,640},{9,10, +460},{9,10,783},{11,10,741},{12,10,183},{141,10,488},{133,0,626},{136,0,614},{ +138,0,237},{7,11,34},{7,11,190},{8,11,28},{8,11,141},{8,11,444},{8,11,811},{9,11 +,468},{11,11,334},{12,11,24},{12,11,386},{140,11,576},{133,11,757},{5,0,18},{6,0 +,526},{13,0,24},{13,0,110},{19,0,5},{147,0,44},{6,0,506},{134,11,506},{135,11, +1553},{4,0,309},{5,0,462},{7,0,970},{7,0,1097},{7,11,1385},{11,11,582},{11,11, +650},{11,11,901},{11,11,949},{12,11,232},{12,11,236},{13,11,413},{13,11,501},{18 +,11,116},{22,0,30},{150,0,33},{5,10,222},{9,0,140},{138,10,534},{6,0,1056},{137, +10,906},{134,0,1704},{138,10,503},{134,0,1036},{5,10,154},{7,10,1491},{10,10,379 +},{138,10,485},{4,11,383},{133,10,716},{134,0,1315},{5,0,86},{7,0,743},{9,0,85}, +{10,0,281},{10,0,432},{11,0,825},{12,0,251},{13,0,118},{142,0,378},{4,10,91},{5, +10,388},{5,10,845},{6,10,206},{6,10,252},{6,10,365},{7,10,136},{7,10,531},{8,0, +264},{136,10,621},{5,0,524},{133,0,744},{5,11,277},{141,11,247},{132,11,435},{10 +,0,107},{140,0,436},{132,0,927},{10,0,123},{12,0,670},{146,0,94},{7,0,1149},{9,0 +,156},{138,0,957},{5,11,265},{6,11,212},{135,11,28},{133,0,778},{133,0,502},{8,0 +,196},{10,0,283},{139,0,406},{135,10,576},{136,11,535},{134,0,1312},{5,10,771},{ +5,10,863},{5,10,898},{6,10,1632},{6,10,1644},{134,10,1780},{5,0,855},{5,10,331}, +{135,11,1487},{132,11,702},{5,11,808},{135,11,2045},{7,0,1400},{9,0,446},{138,0, +45},{140,10,632},{132,0,1003},{5,11,166},{8,11,739},{140,11,511},{5,10,107},{7, +10,201},{136,10,518},{6,10,446},{135,10,1817},{134,0,1532},{134,0,1097},{4,11, +119},{5,11,170},{5,11,447},{7,11,1708},{7,11,1889},{9,11,357},{9,11,719},{12,11, +486},{140,11,596},{9,10,851},{141,10,510},{4,10,504},{7,0,612},{8,0,545},{8,0, +568},{8,0,642},{9,0,717},{10,0,541},{10,0,763},{11,0,449},{12,0,489},{13,0,153}, +{13,0,296},{14,0,138},{14,0,392},{15,0,50},{16,0,6},{16,0,12},{148,0,9},{4,11, +450},{135,11,1158},{5,10,883},{5,10,975},{8,10,392},{11,0,54},{13,0,173},{13,0, +294},{148,10,7},{13,0,455},{15,0,99},{15,0,129},{144,0,68},{135,0,172},{132,11, +754},{5,10,922},{134,10,1707},{134,0,1029},{17,11,39},{148,11,36},{4,0,568},{5, +10,993},{7,10,515},{137,10,91},{132,0,732},{10,0,617},{138,11,617},{134,0,974},{ +7,0,989},{10,0,377},{12,0,363},{13,0,68},{13,0,94},{14,0,108},{142,0,306},{136,0 +,733},{132,0,428},{7,0,1789},{135,11,1062},{7,0,2015},{140,0,665},{135,10,1433}, +{5,0,287},{7,10,921},{8,10,580},{8,10,593},{8,10,630},{138,10,28},{138,0,806},{4 +,10,911},{5,10,867},{5,10,1013},{7,10,2034},{8,10,798},{136,10,813},{134,0,1539} +,{8,11,523},{150,11,34},{135,11,740},{7,11,238},{7,11,2033},{8,11,120},{8,11,188 +},{8,11,659},{9,11,598},{10,11,466},{12,11,342},{12,11,588},{13,11,503},{14,11, +246},{143,11,92},{7,0,1563},{141,0,182},{5,10,135},{6,10,519},{7,10,1722},{10,10 +,271},{11,10,261},{145,10,54},{14,10,338},{148,10,81},{4,10,300},{5,10,436},{135 +,0,484},{145,11,114},{6,0,1623},{134,0,1681},{133,11,640},{4,11,201},{7,11,1744} +,{8,11,602},{11,11,247},{11,11,826},{145,11,65},{8,11,164},{146,11,62},{6,0,1833 +},{6,0,1861},{136,0,878},{134,0,1569},{8,10,357},{10,10,745},{14,10,426},{17,10, +94},{147,10,57},{12,0,93},{12,0,501},{13,0,362},{14,0,151},{15,0,40},{15,0,59},{ +16,0,46},{17,0,25},{18,0,14},{18,0,134},{19,0,25},{19,0,69},{20,0,16},{20,0,19}, +{20,0,66},{21,0,23},{21,0,25},{150,0,42},{6,0,1748},{8,0,715},{9,0,802},{10,0,46 +},{10,0,819},{13,0,308},{14,0,351},{14,0,363},{146,0,67},{132,0,994},{4,0,63},{ +133,0,347},{132,0,591},{133,0,749},{7,11,1577},{10,11,304},{10,11,549},{11,11, +424},{12,11,365},{13,11,220},{13,11,240},{142,11,33},{133,0,366},{5,10,387},{7,0 +,557},{12,0,547},{142,0,86},{135,0,1747},{132,11,907},{5,11,100},{10,11,329},{12 +,11,416},{149,11,29},{4,10,6},{5,10,708},{136,10,75},{7,10,1351},{9,10,581},{10, +10,639},{11,10,453},{140,10,584},{4,10,303},{135,0,89},{138,10,772},{132,11,176} +,{5,11,636},{5,11,998},{8,11,26},{137,11,358},{7,11,9},{7,11,1508},{9,11,317},{ +10,11,210},{10,11,292},{10,11,533},{11,11,555},{12,11,526},{12,11,607},{13,11, +263},{13,11,459},{142,11,271},{134,0,1463},{6,0,772},{6,0,1137},{139,11,595},{7, +0,977},{139,11,66},{138,0,893},{20,0,48},{148,11,48},{5,0,824},{133,0,941},{134, +11,295},{4,10,106},{7,0,1543},{7,0,1785},{10,0,690},{139,10,717},{7,0,440},{8,0, +230},{139,0,106},{5,10,890},{133,10,988},{6,10,626},{142,10,431},{10,11,127},{ +141,11,27},{10,10,706},{17,0,32},{150,10,44},{132,0,216},{137,0,332},{4,10,698}, +{136,11,119},{139,11,267},{138,10,17},{11,11,526},{11,11,939},{141,11,290},{7,11 +,1167},{11,11,934},{13,11,391},{145,11,76},{139,11,39},{134,10,84},{4,0,914},{5, +0,800},{133,0,852},{10,0,416},{141,0,115},{7,0,564},{142,0,168},{4,0,918},{133,0 +,876},{134,0,1764},{152,0,3},{4,0,92},{5,0,274},{7,11,126},{136,11,84},{140,10, +498},{136,11,790},{5,10,986},{6,10,130},{7,10,1582},{8,0,501},{8,10,458},{10,10, +101},{10,10,318},{138,10,823},{6,11,64},{12,11,377},{141,11,309},{5,0,743},{138, +0,851},{4,0,49},{7,0,280},{135,0,1633},{134,0,879},{136,0,47},{7,10,1644},{137, +10,129},{132,0,865},{134,0,1202},{9,11,34},{139,11,484},{135,10,997},{5,0,272},{ +5,0,908},{5,0,942},{8,0,197},{9,0,47},{11,0,538},{139,0,742},{6,11,1700},{7,11, +26},{7,11,293},{7,11,382},{7,11,1026},{7,11,1087},{7,11,2027},{8,11,24},{8,11, +114},{8,11,252},{8,11,727},{8,11,729},{9,11,30},{9,11,199},{9,11,231},{9,11,251} +,{9,11,334},{9,11,361},{9,11,488},{9,11,712},{10,11,55},{10,11,60},{10,11,232},{ +10,11,332},{10,11,384},{10,11,396},{10,11,504},{10,11,542},{10,11,652},{11,11,20 +},{11,11,48},{11,11,207},{11,11,291},{11,11,298},{11,11,342},{11,11,365},{11,11, +394},{11,11,620},{11,11,705},{11,11,1017},{12,11,123},{12,11,340},{12,11,406},{ +12,11,643},{13,11,61},{13,11,269},{13,11,311},{13,11,319},{13,11,486},{14,11,234 +},{15,11,62},{15,11,85},{16,11,71},{18,11,119},{148,11,105},{6,0,1455},{150,11, +37},{135,10,1927},{135,0,1911},{137,0,891},{7,10,1756},{137,10,98},{7,10,1046},{ +139,10,160},{132,0,761},{6,11,379},{7,11,270},{7,11,1116},{8,11,176},{8,11,183}, +{9,11,432},{9,11,661},{12,11,247},{12,11,617},{146,11,125},{6,10,45},{7,10,433}, +{8,10,129},{9,10,21},{10,10,392},{11,10,79},{12,10,499},{13,10,199},{141,10,451} +,{4,0,407},{5,11,792},{133,11,900},{132,0,560},{135,0,183},{7,10,558},{8,10,353} +,{141,0,490},{4,0,475},{5,10,785},{6,0,731},{11,0,35},{13,0,71},{13,0,177},{142, +0,422},{8,10,81},{9,10,189},{9,10,201},{11,10,478},{11,10,712},{141,10,338},{4,0 +,418},{4,0,819},{133,10,353},{151,10,26},{4,11,901},{133,11,776},{132,0,575},{7, +0,818},{16,0,92},{17,0,14},{17,0,45},{18,0,75},{148,0,18},{6,0,222},{7,0,636},{7 +,0,1620},{8,0,409},{9,0,693},{139,0,77},{6,10,25},{7,10,855},{7,10,1258},{144,10 +,32},{6,0,1880},{6,0,1887},{6,0,1918},{6,0,1924},{9,0,967},{9,0,995},{9,0,1015}, +{12,0,826},{12,0,849},{12,0,857},{12,0,860},{12,0,886},{12,0,932},{18,0,228},{18 +,0,231},{146,0,240},{134,0,633},{134,0,1308},{4,11,37},{5,11,334},{135,11,1253}, +{4,10,4},{7,10,1118},{7,10,1320},{7,10,1706},{8,10,277},{9,10,622},{10,0,86},{11 +,10,724},{12,10,350},{12,10,397},{13,10,28},{13,10,159},{15,10,89},{18,10,5},{19 +,10,9},{20,10,34},{150,10,47},{132,11,508},{137,11,448},{12,11,107},{146,11,31}, +{132,0,817},{134,0,663},{133,0,882},{134,0,914},{132,11,540},{132,11,533},{136, +11,608},{8,0,885},{138,0,865},{132,0,426},{6,0,58},{7,0,745},{7,0,1969},{8,0,399 +},{8,0,675},{9,0,479},{9,0,731},{10,0,330},{10,0,593},{10,0,817},{11,0,32},{11,0 +,133},{11,0,221},{145,0,68},{134,10,255},{7,0,102},{137,0,538},{137,10,216},{7, +11,253},{136,11,549},{135,11,912},{9,10,183},{139,10,286},{11,10,956},{151,10,3} +,{8,11,527},{18,11,60},{147,11,24},{4,10,536},{7,10,1141},{10,10,723},{139,10, +371},{133,11,920},{7,0,876},{135,10,285},{135,10,560},{132,10,690},{142,11,126}, +{11,10,33},{12,10,571},{149,10,1},{133,0,566},{4,11,483},{9,0,139},{10,0,399},{ +11,0,469},{12,0,634},{141,0,223},{6,0,48},{135,0,63},{7,10,1862},{12,10,491},{12 +,10,520},{13,10,383},{14,10,244},{146,0,12},{135,11,1665},{132,11,448},{9,11,495 +},{146,11,104},{6,0,114},{7,0,1224},{7,0,1556},{136,0,3},{4,10,190},{133,10,554} +,{5,10,1001},{8,0,576},{137,0,267},{133,10,446},{133,0,933},{139,11,1009},{8,11, +653},{13,11,93},{147,11,14},{6,0,692},{6,0,821},{134,0,1077},{5,11,172},{135,11, +801},{138,0,752},{4,0,375},{134,0,638},{134,0,1011},{140,11,540},{5,11,260},{137 +,0,96},{139,11,587},{135,10,1231},{4,10,304},{12,0,30},{13,0,148},{14,0,87},{14, +0,182},{16,0,42},{148,0,70},{6,0,1398},{7,0,56},{7,0,1989},{8,0,337},{8,0,738},{ +9,0,600},{12,0,37},{13,0,447},{142,0,92},{138,0,666},{5,0,394},{7,0,487},{136,0, +246},{6,10,53},{6,10,199},{7,10,1408},{8,10,32},{8,10,93},{9,0,437},{10,10,397}, +{10,10,629},{11,10,593},{11,10,763},{13,10,326},{145,10,35},{134,10,105},{9,0, +320},{10,0,506},{138,10,794},{7,11,57},{8,11,167},{8,11,375},{9,11,82},{9,11,561 +},{10,11,620},{10,11,770},{11,10,704},{141,10,396},{5,10,114},{5,10,255},{6,0, +1003},{141,10,285},{7,0,866},{135,0,1163},{133,11,531},{132,0,328},{7,10,2035},{ +8,10,19},{9,10,89},{138,10,831},{8,11,194},{136,11,756},{136,0,1000},{5,11,453}, +{134,11,441},{4,0,101},{5,0,833},{7,0,1171},{136,0,744},{133,0,726},{136,10,746} +,{138,0,176},{6,0,9},{6,0,397},{7,0,53},{7,0,1742},{10,0,632},{11,0,828},{140,0, +146},{135,11,22},{145,11,64},{132,0,839},{11,0,417},{12,0,223},{140,0,265},{4,11 +,102},{7,11,815},{7,11,1699},{139,11,964},{5,10,955},{136,10,814},{6,0,1931},{6, +0,2007},{18,0,246},{146,0,247},{8,0,198},{11,0,29},{140,0,534},{135,0,1771},{6,0 +,846},{7,11,1010},{11,11,733},{11,11,759},{12,11,563},{13,11,34},{14,11,101},{18 +,11,45},{146,11,129},{4,0,186},{5,0,157},{8,0,168},{138,0,6},{132,11,899},{133, +10,56},{148,10,100},{133,0,875},{5,0,773},{5,0,991},{6,0,1635},{134,0,1788},{6,0 +,1274},{9,0,477},{141,0,78},{4,0,639},{6,11,52},{7,0,111},{8,0,581},{9,11,104},{ +9,11,559},{10,10,4},{10,10,13},{11,10,638},{12,0,177},{12,11,308},{19,11,87},{ +148,10,57},{132,11,604},{4,11,301},{133,10,738},{133,10,758},{134,0,1747},{7,11, +1440},{11,11,854},{11,11,872},{11,11,921},{12,11,551},{13,11,472},{142,11,367},{ +7,0,1364},{7,0,1907},{141,0,158},{134,0,873},{4,0,404},{4,0,659},{7,0,552},{135, +0,675},{135,10,1112},{139,10,328},{7,11,508},{137,10,133},{133,0,391},{5,10,110} +,{6,10,169},{6,10,1702},{7,10,400},{8,10,538},{9,10,184},{9,10,524},{140,10,218} +,{6,11,310},{7,11,1849},{8,11,72},{8,11,272},{8,11,431},{9,11,12},{9,11,351},{10 +,11,563},{10,11,630},{10,11,810},{11,11,367},{11,11,599},{11,11,686},{140,11,672 +},{5,0,540},{6,0,1697},{136,0,668},{132,0,883},{134,0,78},{6,10,133},{9,10,353}, +{11,10,993},{12,0,628},{146,0,79},{6,11,181},{7,11,537},{8,11,64},{9,11,127},{10 +,11,496},{12,11,510},{141,11,384},{6,10,93},{7,10,1422},{7,10,1851},{8,10,673},{ +9,10,529},{140,10,43},{137,10,371},{134,0,1460},{134,0,962},{4,11,244},{135,11, +233},{9,10,25},{10,10,467},{138,10,559},{4,10,335},{135,10,942},{133,0,460},{135 +,11,334},{134,11,1650},{4,0,199},{139,0,34},{5,10,601},{8,10,39},{10,10,773},{11 +,10,84},{12,10,205},{142,10,1},{133,10,870},{134,0,388},{14,0,474},{148,0,120},{ +133,11,369},{139,0,271},{4,0,511},{4,10,181},{9,0,333},{9,0,379},{10,0,602},{11, +0,441},{11,0,723},{11,0,976},{140,0,357},{134,0,608},{134,10,1652},{9,11,338},{ +150,0,49},{140,0,988},{134,0,617},{5,0,938},{136,0,707},{132,10,97},{5,10,147},{ +6,10,286},{7,10,1362},{141,10,176},{6,0,756},{134,0,1149},{133,11,896},{6,10,375 +},{7,10,169},{7,10,254},{136,10,780},{134,0,1583},{135,10,1447},{139,0,285},{7, +11,1117},{8,11,393},{136,11,539},{135,0,344},{6,0,469},{7,0,1709},{138,0,515},{5 +,10,629},{135,10,1549},{5,11,4},{5,11,810},{6,11,13},{6,11,538},{6,11,1690},{6, +11,1726},{7,11,499},{7,11,1819},{8,11,148},{8,11,696},{8,11,791},{12,11,125},{13 +,11,54},{143,11,9},{135,11,1268},{137,0,404},{132,0,500},{5,0,68},{134,0,383},{ +11,0,216},{139,0,340},{4,11,925},{5,11,803},{8,11,698},{138,11,828},{4,0,337},{6 +,0,353},{7,0,1934},{8,0,488},{137,0,429},{7,0,236},{7,0,1795},{8,0,259},{9,0,135 +},{9,0,177},{9,0,860},{10,0,825},{11,0,115},{11,0,370},{11,0,405},{11,0,604},{12 +,0,10},{12,0,667},{12,0,669},{13,0,76},{14,0,310},{15,0,76},{15,0,147},{148,0,23 +},{4,0,15},{4,0,490},{5,0,22},{6,0,244},{7,0,40},{7,0,200},{7,0,906},{7,0,1199}, +{9,0,616},{10,0,716},{11,0,635},{11,0,801},{140,0,458},{4,10,420},{140,0,756},{ +134,0,1504},{5,11,383},{134,0,757},{6,0,1266},{135,0,1735},{5,0,598},{7,0,791},{ +7,10,1570},{8,0,108},{9,0,123},{140,10,542},{142,11,410},{9,11,660},{138,11,347} +}; diff --git a/packages/runtime/vendor/brotli/c/enc/static_init.c b/packages/runtime/vendor/brotli/c/enc/static_init.c new file mode 100644 index 000000000..c0dff3a3a --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/static_init.c @@ -0,0 +1,59 @@ +/* Copyright 2025 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +#include "static_init.h" + +#include "../common/platform.h" +#include "../common/static_init.h" + +#if (BROTLI_STATIC_INIT != BROTLI_STATIC_INIT_NONE) +#include "../common/dictionary.h" +#include "dictionary_hash.h" +#include "static_dict_lut.h" +#endif + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +#if (BROTLI_STATIC_INIT != BROTLI_STATIC_INIT_NONE) +static BROTLI_BOOL DoBrotliEncoderStaticInit(void) { + const BrotliDictionary* dict = BrotliGetDictionary(); + BROTLI_BOOL ok = BrotliEncoderInitStaticDictionaryLut( + dict, kStaticDictionaryBuckets, kStaticDictionaryWords); + if (!ok) return BROTLI_FALSE; + ok = BrotliEncoderInitDictionaryHash(dict, kStaticDictionaryHashWords, + kStaticDictionaryHashLengths); + if (!ok) return BROTLI_FALSE; + return BROTLI_TRUE; +} +#endif /* BROTLI_STATIC_INIT_NONE */ + +#if (BROTLI_STATIC_INIT == BROTLI_STATIC_INIT_EARLY) +static BROTLI_BOOL kEarlyInitOk; +static __attribute__((constructor)) void BrotliEncoderStaticInitEarly(void) { + kEarlyInitOk = DoBrotliEncoderStaticInit(); +} +#elif (BROTLI_STATIC_INIT == BROTLI_STATIC_INIT_LAZY) +static BROTLI_BOOL kLazyInitOk; +void BrotliEncoderLazyStaticInitInner(void) { + kLazyInitOk = DoBrotliEncoderStaticInit(); +} +#endif /* BROTLI_STATIC_INIT_EARLY */ + +BROTLI_BOOL BrotliEncoderEnsureStaticInit(void) { +#if (BROTLI_STATIC_INIT == BROTLI_STATIC_INIT_NONE) + return BROTLI_TRUE; +#elif (BROTLI_STATIC_INIT == BROTLI_STATIC_INIT_EARLY) + return kEarlyInitOk; +#else + return kLazyInitOk; +#endif +} + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif diff --git a/packages/runtime/vendor/brotli/c/enc/static_init.h b/packages/runtime/vendor/brotli/c/enc/static_init.h new file mode 100644 index 000000000..e5cb23916 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/static_init.h @@ -0,0 +1,30 @@ +/* Copyright 2025 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* Central point for static initialization. */ + +#ifndef THIRD_PARTY_BROTLI_ENC_STATIC_INIT_H_ +#define THIRD_PARTY_BROTLI_ENC_STATIC_INIT_H_ + +#include "../common/platform.h" +#include "../common/static_init.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +#if (BROTLI_STATIC_INIT == BROTLI_STATIC_INIT_LAZY) +BROTLI_INTERNAL void BrotliEncoderLazyStaticInitInner(void); +BROTLI_INTERNAL void BrotliEncoderLazyStaticInit(void); +#endif /* BROTLI_STATIC_INIT */ + +BROTLI_INTERNAL BROTLI_BOOL BrotliEncoderEnsureStaticInit(void); + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif + +#endif // THIRD_PARTY_BROTLI_ENC_STATIC_INIT_H_ diff --git a/packages/runtime/vendor/brotli/c/enc/utf8_util.c b/packages/runtime/vendor/brotli/c/enc/utf8_util.c new file mode 100644 index 000000000..d46c3644e --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/utf8_util.c @@ -0,0 +1,85 @@ +/* Copyright 2013 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* Heuristics for deciding about the UTF8-ness of strings. */ + +#include "utf8_util.h" + +#include "../common/platform.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +static size_t BrotliParseAsUTF8( + int* symbol, const uint8_t* input, size_t size) { + /* ASCII */ + if ((input[0] & 0x80) == 0) { + *symbol = input[0]; + if (*symbol > 0) { + return 1; + } + } + /* 2-byte UTF8 */ + if (size > 1u && + (input[0] & 0xE0) == 0xC0 && + (input[1] & 0xC0) == 0x80) { + *symbol = (((input[0] & 0x1F) << 6) | + (input[1] & 0x3F)); + if (*symbol > 0x7F) { + return 2; + } + } + /* 3-byte UFT8 */ + if (size > 2u && + (input[0] & 0xF0) == 0xE0 && + (input[1] & 0xC0) == 0x80 && + (input[2] & 0xC0) == 0x80) { + *symbol = (((input[0] & 0x0F) << 12) | + ((input[1] & 0x3F) << 6) | + (input[2] & 0x3F)); + if (*symbol > 0x7FF) { + return 3; + } + } + /* 4-byte UFT8 */ + if (size > 3u && + (input[0] & 0xF8) == 0xF0 && + (input[1] & 0xC0) == 0x80 && + (input[2] & 0xC0) == 0x80 && + (input[3] & 0xC0) == 0x80) { + *symbol = (((input[0] & 0x07) << 18) | + ((input[1] & 0x3F) << 12) | + ((input[2] & 0x3F) << 6) | + (input[3] & 0x3F)); + if (*symbol > 0xFFFF && *symbol <= 0x10FFFF) { + return 4; + } + } + /* Not UTF8, emit a special symbol above the UTF8-code space */ + *symbol = 0x110000 | input[0]; + return 1; +} + +/* Returns 1 if at least min_fraction of the data is UTF8-encoded.*/ +BROTLI_BOOL BrotliIsMostlyUTF8( + const uint8_t* data, const size_t pos, const size_t mask, + const size_t length, const double min_fraction) { + size_t size_utf8 = 0; + size_t i = 0; + while (i < length) { + int symbol; + size_t bytes_read = + BrotliParseAsUTF8(&symbol, &data[(pos + i) & mask], length - i); + i += bytes_read; + if (symbol < 0x110000) size_utf8 += bytes_read; + } + return TO_BROTLI_BOOL((double)size_utf8 > min_fraction * (double)length); +} + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif diff --git a/packages/runtime/vendor/brotli/c/enc/utf8_util.h b/packages/runtime/vendor/brotli/c/enc/utf8_util.h new file mode 100644 index 000000000..cae3dba7a --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/utf8_util.h @@ -0,0 +1,31 @@ +/* Copyright 2013 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* Heuristics for deciding about the UTF8-ness of strings. */ + +#ifndef BROTLI_ENC_UTF8_UTIL_H_ +#define BROTLI_ENC_UTF8_UTIL_H_ + +#include "../common/platform.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +static const double kMinUTF8Ratio = 0.75; + +/* Returns 1 if at least min_fraction of the bytes between pos and + pos + length in the (data, mask) ring-buffer is UTF8-encoded, otherwise + returns 0. */ +BROTLI_INTERNAL BROTLI_BOOL BrotliIsMostlyUTF8( + const uint8_t* data, const size_t pos, const size_t mask, + const size_t length, const double min_fraction); + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif + +#endif /* BROTLI_ENC_UTF8_UTIL_H_ */ diff --git a/packages/runtime/vendor/brotli/c/enc/write_bits.h b/packages/runtime/vendor/brotli/c/enc/write_bits.h new file mode 100644 index 000000000..0cdc3372b --- /dev/null +++ b/packages/runtime/vendor/brotli/c/enc/write_bits.h @@ -0,0 +1,86 @@ +/* Copyright 2010 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* Write bits into a byte array. */ + +#ifndef BROTLI_ENC_WRITE_BITS_H_ +#define BROTLI_ENC_WRITE_BITS_H_ + +#include "../common/platform.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +/* This function writes bits into bytes in increasing addresses, and within + a byte least-significant-bit first. + + The function can write up to 56 bits in one go with WriteBits + Example: let's assume that 3 bits (Rs below) have been written already: + + BYTE-0 BYTE+1 BYTE+2 + + 0000 0RRR 0000 0000 0000 0000 + + Now, we could write 5 or less bits in MSB by just shifting by 3 + and OR'ing to BYTE-0. + + For n bits, we take the last 5 bits, OR that with high bits in BYTE-0, + and locate the rest in BYTE+1, BYTE+2, etc. */ +static BROTLI_INLINE void BrotliWriteBits(size_t n_bits, + uint64_t bits, + size_t* BROTLI_RESTRICT pos, + uint8_t* BROTLI_RESTRICT array) { + BROTLI_LOG(("WriteBits %2d 0x%08x%08x %10d\n", (int)n_bits, + (uint32_t)(bits >> 32), (uint32_t)(bits & 0xFFFFFFFF), + (int)*pos)); + BROTLI_DCHECK((bits >> n_bits) == 0); + BROTLI_DCHECK(n_bits <= 56); +#if defined(BROTLI_LITTLE_ENDIAN) + /* This branch of the code can write up to 56 bits at a time, + 7 bits are lost by being perhaps already in *p and at least + 1 bit is needed to initialize the bit-stream ahead (i.e. if 7 + bits are in *p and we write 57 bits, then the next write will + access a byte that was never initialized). */ + { + uint8_t* p = &array[*pos >> 3]; + uint64_t v = (uint64_t)(*p); /* Zero-extend 8 to 64 bits. */ + v |= bits << (*pos & 7); + BROTLI_UNALIGNED_STORE64LE(p, v); /* Set some bits. */ + *pos += n_bits; + } +#else + /* implicit & 0xFF is assumed for uint8_t arithmetics */ + { + uint8_t* array_pos = &array[*pos >> 3]; + const size_t bits_reserved_in_first_byte = (*pos & 7); + size_t bits_left_to_write; + bits <<= bits_reserved_in_first_byte; + *array_pos++ |= (uint8_t)bits; + for (bits_left_to_write = n_bits + bits_reserved_in_first_byte; + bits_left_to_write >= 9; + bits_left_to_write -= 8) { + bits >>= 8; + *array_pos++ = (uint8_t)bits; + } + *array_pos = 0; + *pos += n_bits; + } +#endif +} + +static BROTLI_INLINE void BrotliWriteBitsPrepareStorage( + size_t pos, uint8_t* array) { + BROTLI_LOG(("WriteBitsPrepareStorage %10d\n", (int)pos)); + BROTLI_DCHECK((pos & 7) == 0); + array[pos >> 3] = 0; +} + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif + +#endif /* BROTLI_ENC_WRITE_BITS_H_ */ diff --git a/packages/runtime/vendor/brotli/c/include/brotli/decode.h b/packages/runtime/vendor/brotli/c/include/brotli/decode.h new file mode 100644 index 000000000..8d436db73 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/include/brotli/decode.h @@ -0,0 +1,409 @@ +/* Copyright 2013 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/** + * @file + * API for Brotli decompression. + */ + +#ifndef BROTLI_DEC_DECODE_H_ +#define BROTLI_DEC_DECODE_H_ + +#include +#include +#include /* IWYU pragma: export */ + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +/** + * Opaque structure that holds decoder state. + * + * Allocated and initialized with ::BrotliDecoderCreateInstance. + * Cleaned up and deallocated with ::BrotliDecoderDestroyInstance. + */ +typedef struct BrotliDecoderStateStruct BrotliDecoderState; + +/** + * Result type for ::BrotliDecoderDecompress and + * ::BrotliDecoderDecompressStream functions. + */ +typedef enum { + /** Decoding error, e.g. corrupted input or memory allocation problem. */ + BROTLI_DECODER_RESULT_ERROR = 0, + /** Decoding successfully completed. */ + BROTLI_DECODER_RESULT_SUCCESS = 1, + /** Partially done; should be called again with more input. */ + BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT = 2, + /** Partially done; should be called again with more output. */ + BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT = 3 +} BrotliDecoderResult; + +/** + * Template that evaluates items of ::BrotliDecoderErrorCode. + * + * Example: @code {.cpp} + * // Log Brotli error code. + * switch (brotliDecoderErrorCode) { + * #define CASE_(PREFIX, NAME, CODE) \ + * case BROTLI_DECODER ## PREFIX ## NAME: \ + * LOG(INFO) << "error code:" << #NAME; \ + * break; + * #define NEWLINE_ + * BROTLI_DECODER_ERROR_CODES_LIST(CASE_, NEWLINE_) + * #undef CASE_ + * #undef NEWLINE_ + * default: LOG(FATAL) << "unknown brotli error code"; + * } + * @endcode + */ +#define BROTLI_DECODER_ERROR_CODES_LIST(BROTLI_ERROR_CODE, SEPARATOR) \ + BROTLI_ERROR_CODE(_, NO_ERROR, 0) SEPARATOR \ + /* Same as BrotliDecoderResult values */ \ + BROTLI_ERROR_CODE(_, SUCCESS, 1) SEPARATOR \ + BROTLI_ERROR_CODE(_, NEEDS_MORE_INPUT, 2) SEPARATOR \ + BROTLI_ERROR_CODE(_, NEEDS_MORE_OUTPUT, 3) SEPARATOR \ + \ + /* Errors caused by invalid input */ \ + BROTLI_ERROR_CODE(_ERROR_FORMAT_, EXUBERANT_NIBBLE, -1) SEPARATOR \ + BROTLI_ERROR_CODE(_ERROR_FORMAT_, RESERVED, -2) SEPARATOR \ + BROTLI_ERROR_CODE(_ERROR_FORMAT_, EXUBERANT_META_NIBBLE, -3) SEPARATOR \ + BROTLI_ERROR_CODE(_ERROR_FORMAT_, SIMPLE_HUFFMAN_ALPHABET, -4) SEPARATOR \ + BROTLI_ERROR_CODE(_ERROR_FORMAT_, SIMPLE_HUFFMAN_SAME, -5) SEPARATOR \ + BROTLI_ERROR_CODE(_ERROR_FORMAT_, CL_SPACE, -6) SEPARATOR \ + BROTLI_ERROR_CODE(_ERROR_FORMAT_, HUFFMAN_SPACE, -7) SEPARATOR \ + BROTLI_ERROR_CODE(_ERROR_FORMAT_, CONTEXT_MAP_REPEAT, -8) SEPARATOR \ + BROTLI_ERROR_CODE(_ERROR_FORMAT_, BLOCK_LENGTH_1, -9) SEPARATOR \ + BROTLI_ERROR_CODE(_ERROR_FORMAT_, BLOCK_LENGTH_2, -10) SEPARATOR \ + BROTLI_ERROR_CODE(_ERROR_FORMAT_, TRANSFORM, -11) SEPARATOR \ + BROTLI_ERROR_CODE(_ERROR_FORMAT_, DICTIONARY, -12) SEPARATOR \ + BROTLI_ERROR_CODE(_ERROR_FORMAT_, WINDOW_BITS, -13) SEPARATOR \ + BROTLI_ERROR_CODE(_ERROR_FORMAT_, PADDING_1, -14) SEPARATOR \ + BROTLI_ERROR_CODE(_ERROR_FORMAT_, PADDING_2, -15) SEPARATOR \ + BROTLI_ERROR_CODE(_ERROR_FORMAT_, DISTANCE, -16) SEPARATOR \ + \ + /* -17 code is reserved */ \ + \ + BROTLI_ERROR_CODE(_ERROR_, COMPOUND_DICTIONARY, -18) SEPARATOR \ + BROTLI_ERROR_CODE(_ERROR_, DICTIONARY_NOT_SET, -19) SEPARATOR \ + BROTLI_ERROR_CODE(_ERROR_, INVALID_ARGUMENTS, -20) SEPARATOR \ + \ + /* Memory allocation problems */ \ + BROTLI_ERROR_CODE(_ERROR_ALLOC_, CONTEXT_MODES, -21) SEPARATOR \ + /* Literal, insert and distance trees together */ \ + BROTLI_ERROR_CODE(_ERROR_ALLOC_, TREE_GROUPS, -22) SEPARATOR \ + /* -23..-24 codes are reserved for distinct tree groups */ \ + BROTLI_ERROR_CODE(_ERROR_ALLOC_, CONTEXT_MAP, -25) SEPARATOR \ + BROTLI_ERROR_CODE(_ERROR_ALLOC_, RING_BUFFER_1, -26) SEPARATOR \ + BROTLI_ERROR_CODE(_ERROR_ALLOC_, RING_BUFFER_2, -27) SEPARATOR \ + /* -28..-29 codes are reserved for dynamic ring-buffer allocation */ \ + BROTLI_ERROR_CODE(_ERROR_ALLOC_, BLOCK_TYPE_TREES, -30) SEPARATOR \ + \ + /* "Impossible" states */ \ + BROTLI_ERROR_CODE(_ERROR_, UNREACHABLE, -31) + +/** + * Error code for detailed logging / production debugging. + * + * See ::BrotliDecoderGetErrorCode and ::BROTLI_LAST_ERROR_CODE. + */ +typedef enum { +#define BROTLI_COMMA_ , +#define BROTLI_ERROR_CODE_ENUM_ITEM_(PREFIX, NAME, CODE) \ + BROTLI_DECODER ## PREFIX ## NAME = CODE + BROTLI_DECODER_ERROR_CODES_LIST(BROTLI_ERROR_CODE_ENUM_ITEM_, BROTLI_COMMA_) +} BrotliDecoderErrorCode; +#undef BROTLI_ERROR_CODE_ENUM_ITEM_ +#undef BROTLI_COMMA_ + +/** + * The value of the last error code, negative integer. + * + * All other error code values are in the range from ::BROTLI_LAST_ERROR_CODE + * to @c -1. There are also 4 other possible non-error codes @c 0 .. @c 3 in + * ::BrotliDecoderErrorCode enumeration. + */ +#define BROTLI_LAST_ERROR_CODE BROTLI_DECODER_ERROR_UNREACHABLE + +/** Options to be used with ::BrotliDecoderSetParameter. */ +typedef enum BrotliDecoderParameter { + /** + * Disable "canny" ring buffer allocation strategy. + * + * Ring buffer is allocated according to window size, despite the real size of + * the content. + */ + BROTLI_DECODER_PARAM_DISABLE_RING_BUFFER_REALLOCATION = 0, + /** + * Flag that determines if "Large Window Brotli" is used. + */ + BROTLI_DECODER_PARAM_LARGE_WINDOW = 1 +} BrotliDecoderParameter; + +/** + * Sets the specified parameter to the given decoder instance. + * + * @param state decoder instance + * @param param parameter to set + * @param value new parameter value + * @returns ::BROTLI_FALSE if parameter is unrecognized, or value is invalid + * @returns ::BROTLI_TRUE if value is accepted + */ +BROTLI_DEC_API BROTLI_BOOL BrotliDecoderSetParameter( + BrotliDecoderState* state, BrotliDecoderParameter param, uint32_t value); + +/** + * Adds LZ77 prefix dictionary, adds or replaces built-in static dictionary and + * transforms. + * + * Attached dictionary ownership is not transferred. + * Data provided to this method should be kept accessible until + * decoding is finished and decoder instance is destroyed. + * + * @note Dictionaries can NOT be attached after actual decoding is started. + * + * @param state decoder instance + * @param type dictionary data format + * @param data_size length of memory region pointed by @p data + * @param data dictionary data in format corresponding to @p type + * @returns ::BROTLI_FALSE if dictionary is corrupted, + * or dictionary count limit is reached + * @returns ::BROTLI_TRUE if dictionary is accepted / attached + */ +BROTLI_DEC_API BROTLI_BOOL BrotliDecoderAttachDictionary( + BrotliDecoderState* state, BrotliSharedDictionaryType type, + size_t data_size, const uint8_t data[BROTLI_ARRAY_PARAM(data_size)]); + +/** + * Creates an instance of ::BrotliDecoderState and initializes it. + * + * The instance can be used once for decoding and should then be destroyed with + * ::BrotliDecoderDestroyInstance, it cannot be reused for a new decoding + * session. + * + * @p alloc_func and @p free_func @b MUST be both zero or both non-zero. In the + * case they are both zero, default memory allocators are used. @p opaque is + * passed to @p alloc_func and @p free_func when they are called. @p free_func + * has to return without doing anything when asked to free a NULL pointer. + * + * @param alloc_func custom memory allocation function + * @param free_func custom memory free function + * @param opaque custom memory manager handle + * @returns @c 0 if instance can not be allocated or initialized + * @returns pointer to initialized ::BrotliDecoderState otherwise + */ +BROTLI_DEC_API BrotliDecoderState* BrotliDecoderCreateInstance( + brotli_alloc_func alloc_func, brotli_free_func free_func, void* opaque); + +/** + * Deinitializes and frees ::BrotliDecoderState instance. + * + * @param state decoder instance to be cleaned up and deallocated + */ +BROTLI_DEC_API void BrotliDecoderDestroyInstance(BrotliDecoderState* state); + +/** + * Performs one-shot memory-to-memory decompression. + * + * Decompresses the data in @p encoded_buffer into @p decoded_buffer, and sets + * @p *decoded_size to the decompressed length. + * + * @param encoded_size size of @p encoded_buffer + * @param encoded_buffer compressed data buffer with at least @p encoded_size + * addressable bytes + * @param[in, out] decoded_size @b in: size of @p decoded_buffer; \n + * @b out: length of decompressed data written to + * @p decoded_buffer + * @param decoded_buffer decompressed data destination buffer + * @returns ::BROTLI_DECODER_RESULT_ERROR if input is corrupted, memory + * allocation failed, or @p decoded_buffer is not large enough; + * @returns ::BROTLI_DECODER_RESULT_SUCCESS otherwise + */ +BROTLI_DEC_API BrotliDecoderResult BrotliDecoderDecompress( + size_t encoded_size, + const uint8_t encoded_buffer[BROTLI_ARRAY_PARAM(encoded_size)], + size_t* decoded_size, + uint8_t decoded_buffer[BROTLI_ARRAY_PARAM(*decoded_size)]); + +/** + * Decompresses the input stream to the output stream. + * + * The values @p *available_in and @p *available_out must specify the number of + * bytes addressable at @p *next_in and @p *next_out respectively. + * When @p *available_out is @c 0, @p next_out is allowed to be @c NULL. + * + * After each call, @p *available_in will be decremented by the amount of input + * bytes consumed, and the @p *next_in pointer will be incremented by that + * amount. Similarly, @p *available_out will be decremented by the amount of + * output bytes written, and the @p *next_out pointer will be incremented by + * that amount. + * + * @p total_out, if it is not a null-pointer, will be set to the number + * of bytes decompressed since the last @p state initialization. + * + * @note Input is never overconsumed, so @p next_in and @p available_in could be + * passed to the next consumer after decoding is complete. + * + * @param state decoder instance + * @param[in, out] available_in @b in: amount of available input; \n + * @b out: amount of unused input + * @param[in, out] next_in pointer to the next compressed byte + * @param[in, out] available_out @b in: length of output buffer; \n + * @b out: remaining size of output buffer + * @param[in, out] next_out output buffer cursor; + * can be @c NULL if @p available_out is @c 0 + * @param[out] total_out number of bytes decompressed so far; can be @c NULL + * @returns ::BROTLI_DECODER_RESULT_ERROR if input is corrupted, memory + * allocation failed, arguments were invalid, etc.; + * use ::BrotliDecoderGetErrorCode to get detailed error code + * @returns ::BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT decoding is blocked until + * more input data is provided + * @returns ::BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT decoding is blocked until + * more output space is provided + * @returns ::BROTLI_DECODER_RESULT_SUCCESS decoding is finished, no more + * input might be consumed and no more output will be produced + */ +BROTLI_DEC_API BrotliDecoderResult BrotliDecoderDecompressStream( + BrotliDecoderState* state, size_t* available_in, const uint8_t** next_in, + size_t* available_out, uint8_t** next_out, size_t* total_out); + +/** + * Checks if decoder has more output. + * + * @param state decoder instance + * @returns ::BROTLI_TRUE, if decoder has some unconsumed output + * @returns ::BROTLI_FALSE otherwise + */ +BROTLI_DEC_API BROTLI_BOOL BrotliDecoderHasMoreOutput( + const BrotliDecoderState* state); + +/** + * Acquires pointer to internal output buffer. + * + * This method is used to make language bindings easier and more efficient: + * -# push data to ::BrotliDecoderDecompressStream, + * until ::BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT is reported + * -# use ::BrotliDecoderTakeOutput to peek bytes and copy to language-specific + * entity + * + * Also this could be useful if there is an output stream that is able to + * consume all the provided data (e.g. when data is saved to file system). + * + * @attention After every call to ::BrotliDecoderTakeOutput @p *size bytes of + * output are considered consumed for all consecutive calls to the + * instance methods; returned pointer becomes invalidated as well. + * + * @note Decoder output is not guaranteed to be contiguous. This means that + * after the size-unrestricted call to ::BrotliDecoderTakeOutput, + * immediate next call to ::BrotliDecoderTakeOutput may return more data. + * + * @param state decoder instance + * @param[in, out] size @b in: number of bytes caller is ready to take, @c 0 if + * any amount could be handled; \n + * @b out: amount of data pointed by returned pointer and + * considered consumed; \n + * out value is never greater than in value, unless it is @c 0 + * @returns pointer to output data + */ +BROTLI_DEC_API const uint8_t* BrotliDecoderTakeOutput( + BrotliDecoderState* state, size_t* size); + +/** + * Checks if instance has already consumed input. + * + * Instance that returns ::BROTLI_FALSE is considered "fresh" and could be + * reused. + * + * @param state decoder instance + * @returns ::BROTLI_TRUE if decoder has already used some input bytes + * @returns ::BROTLI_FALSE otherwise + */ +BROTLI_DEC_API BROTLI_BOOL BrotliDecoderIsUsed(const BrotliDecoderState* state); + +/** + * Checks if decoder instance reached the final state. + * + * @param state decoder instance + * @returns ::BROTLI_TRUE if decoder is in a state where it reached the end of + * the input and produced all of the output + * @returns ::BROTLI_FALSE otherwise + */ +BROTLI_DEC_API BROTLI_BOOL BrotliDecoderIsFinished( + const BrotliDecoderState* state); + +/** + * Acquires a detailed error code. + * + * Should be used only after ::BrotliDecoderDecompressStream returns + * ::BROTLI_DECODER_RESULT_ERROR. + * + * See also ::BrotliDecoderErrorString + * + * @param state decoder instance + * @returns last saved error code + */ +BROTLI_DEC_API BrotliDecoderErrorCode BrotliDecoderGetErrorCode( + const BrotliDecoderState* state); + +/** + * Converts error code to a c-string. + */ +BROTLI_DEC_API const char* BrotliDecoderErrorString(BrotliDecoderErrorCode c); + +/** + * Gets a decoder library version. + * + * Look at BROTLI_MAKE_HEX_VERSION for more information. + */ +BROTLI_DEC_API uint32_t BrotliDecoderVersion(void); + +/** + * Callback to fire on metadata block start. + * + * After this callback is fired, if @p size is not @c 0, it is followed by + * ::brotli_decoder_metadata_chunk_func as more metadata block contents become + * accessible. + * + * @param opaque callback handle + * @param size size of metadata block + */ +typedef void (*brotli_decoder_metadata_start_func)(void* opaque, size_t size); + +/** + * Callback to fire on metadata block chunk becomes available. + * + * This function can be invoked multiple times per metadata block; block should + * be considered finished when sum of @p size matches the announced metadata + * block size. Chunks contents pointed by @p data are transient and shouln not + * be accessed after leaving the callback. + * + * @param opaque callback handle + * @param data pointer to metadata contents + * @param size size of metadata block chunk, at least @c 1 + */ +typedef void (*brotli_decoder_metadata_chunk_func)(void* opaque, + const uint8_t* data, + size_t size); + +/** + * Sets callback for receiving metadata blocks. + * + * @param state decoder instance + * @param start_func callback on metadata block start + * @param chunk_func callback on metadata block chunk + * @param opaque callback handle + */ +BROTLI_DEC_API void BrotliDecoderSetMetadataCallbacks( + BrotliDecoderState* state, + brotli_decoder_metadata_start_func start_func, + brotli_decoder_metadata_chunk_func chunk_func, void* opaque); + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif + +#endif /* BROTLI_DEC_DECODE_H_ */ diff --git a/packages/runtime/vendor/brotli/c/include/brotli/encode.h b/packages/runtime/vendor/brotli/c/include/brotli/encode.h new file mode 100644 index 000000000..ac26ea8e6 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/include/brotli/encode.h @@ -0,0 +1,505 @@ +/* Copyright 2013 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/** + * @file + * API for Brotli compression. + */ + +#ifndef BROTLI_ENC_ENCODE_H_ +#define BROTLI_ENC_ENCODE_H_ + +#include +#include +#include /* IWYU pragma: export */ + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +/** Minimal value for ::BROTLI_PARAM_LGWIN parameter. */ +#define BROTLI_MIN_WINDOW_BITS 10 +/** + * Maximal value for ::BROTLI_PARAM_LGWIN parameter. + * + * @note equal to @c BROTLI_MAX_DISTANCE_BITS constant. + */ +#define BROTLI_MAX_WINDOW_BITS 24 +/** + * Maximal value for ::BROTLI_PARAM_LGWIN parameter + * in "Large Window Brotli" (32-bit). + */ +#define BROTLI_LARGE_MAX_WINDOW_BITS 30 +/** Minimal value for ::BROTLI_PARAM_LGBLOCK parameter. */ +#define BROTLI_MIN_INPUT_BLOCK_BITS 16 +/** Maximal value for ::BROTLI_PARAM_LGBLOCK parameter. */ +#define BROTLI_MAX_INPUT_BLOCK_BITS 24 +/** Minimal value for ::BROTLI_PARAM_QUALITY parameter. */ +#define BROTLI_MIN_QUALITY 0 +/** Maximal value for ::BROTLI_PARAM_QUALITY parameter. */ +#define BROTLI_MAX_QUALITY 11 + +/** Options for ::BROTLI_PARAM_MODE parameter. */ +typedef enum BrotliEncoderMode { + /** + * Default compression mode. + * + * In this mode compressor does not know anything in advance about the + * properties of the input. + */ + BROTLI_MODE_GENERIC = 0, + /** Compression mode for UTF-8 formatted text input. */ + BROTLI_MODE_TEXT = 1, + /** Compression mode used in WOFF 2.0. */ + BROTLI_MODE_FONT = 2 +} BrotliEncoderMode; + +/** Default value for ::BROTLI_PARAM_QUALITY parameter. */ +#define BROTLI_DEFAULT_QUALITY 11 +/** Default value for ::BROTLI_PARAM_LGWIN parameter. */ +#define BROTLI_DEFAULT_WINDOW 22 +/** Default value for ::BROTLI_PARAM_MODE parameter. */ +#define BROTLI_DEFAULT_MODE BROTLI_MODE_GENERIC + +/** Operations that can be performed by streaming encoder. */ +typedef enum BrotliEncoderOperation { + /** + * Process input. + * + * Encoder may postpone producing output, until it has processed enough input. + */ + BROTLI_OPERATION_PROCESS = 0, + /** + * Produce output for all processed input. + * + * Actual flush is performed when input stream is depleted and there is enough + * space in output stream. This means that client should repeat + * ::BROTLI_OPERATION_FLUSH operation until @p available_in becomes @c 0, and + * ::BrotliEncoderHasMoreOutput returns ::BROTLI_FALSE. If output is acquired + * via ::BrotliEncoderTakeOutput, then operation should be repeated after + * output buffer is drained. + * + * @warning Until flush is complete, client @b SHOULD @b NOT swap, + * reduce or extend input stream. + * + * When flush is complete, output data will be sufficient for decoder to + * reproduce all the given input. + */ + BROTLI_OPERATION_FLUSH = 1, + /** + * Finalize the stream. + * + * Actual finalization is performed when input stream is depleted and there is + * enough space in output stream. This means that client should repeat + * ::BROTLI_OPERATION_FINISH operation until @p available_in becomes @c 0, and + * ::BrotliEncoderHasMoreOutput returns ::BROTLI_FALSE. If output is acquired + * via ::BrotliEncoderTakeOutput, then operation should be repeated after + * output buffer is drained. + * + * @warning Until finalization is complete, client @b SHOULD @b NOT swap, + * reduce or extend input stream. + * + * Helper function ::BrotliEncoderIsFinished checks if stream is finalized and + * output fully dumped. + * + * Adding more input data to finalized stream is impossible. + */ + BROTLI_OPERATION_FINISH = 2, + /** + * Emit metadata block to stream. + * + * Metadata is opaque to Brotli: neither encoder, nor decoder processes this + * data or relies on it. It may be used to pass some extra information from + * encoder client to decoder client without interfering with main data stream. + * + * @note Encoder may emit empty metadata blocks internally, to pad encoded + * stream to byte boundary. + * + * @warning Until emitting metadata is complete client @b SHOULD @b NOT swap, + * reduce or extend input stream. + * + * @warning The whole content of input buffer is considered to be the content + * of metadata block. Do @b NOT @e append metadata to input stream, + * before it is depleted with other operations. + * + * Stream is soft-flushed before metadata block is emitted. Metadata block + * @b MUST be no longer than than 16MiB. + */ + BROTLI_OPERATION_EMIT_METADATA = 3 +} BrotliEncoderOperation; + +/** Options to be used with ::BrotliEncoderSetParameter. */ +typedef enum BrotliEncoderParameter { + /** + * Tune encoder for specific input. + * + * ::BrotliEncoderMode enumerates all available values. + */ + BROTLI_PARAM_MODE = 0, + /** + * The main compression speed-density lever. + * + * The higher the quality, the slower the compression. Range is + * from ::BROTLI_MIN_QUALITY to ::BROTLI_MAX_QUALITY. + */ + BROTLI_PARAM_QUALITY = 1, + /** + * Recommended sliding LZ77 window size. + * + * Encoder may reduce this value, e.g. if input is much smaller than + * window size. + * + * Window size is `(1 << value) - 16`. + * + * Range is from ::BROTLI_MIN_WINDOW_BITS to ::BROTLI_MAX_WINDOW_BITS. + */ + BROTLI_PARAM_LGWIN = 2, + /** + * Recommended input block size. + * + * Encoder may reduce this value, e.g. if input is much smaller than input + * block size. + * + * Range is from ::BROTLI_MIN_INPUT_BLOCK_BITS to + * ::BROTLI_MAX_INPUT_BLOCK_BITS. + * + * @note Bigger input block size allows better compression, but consumes more + * memory. \n The rough formula of memory used for temporary input + * storage is `3 << lgBlock`. + */ + BROTLI_PARAM_LGBLOCK = 3, + /** + * Flag that affects usage of "literal context modeling" format feature. + * + * This flag is a "decoding-speed vs compression ratio" trade-off. + */ + BROTLI_PARAM_DISABLE_LITERAL_CONTEXT_MODELING = 4, + /** + * Estimated total input size for all ::BrotliEncoderCompressStream calls. + * + * The default value is 0, which means that the total input size is unknown. + */ + BROTLI_PARAM_SIZE_HINT = 5, + /** + * Flag that determines if "Large Window Brotli" is used. + */ + BROTLI_PARAM_LARGE_WINDOW = 6, + /** + * Recommended number of postfix bits (NPOSTFIX). + * + * Encoder may change this value. + * + * Range is from 0 to ::BROTLI_MAX_NPOSTFIX. + */ + BROTLI_PARAM_NPOSTFIX = 7, + /** + * Recommended number of direct distance codes (NDIRECT). + * + * Encoder may change this value. + * + * Range is from 0 to (15 << NPOSTFIX) in steps of (1 << NPOSTFIX). + */ + BROTLI_PARAM_NDIRECT = 8, + /** + * Number of bytes of input stream already processed by a different instance. + * + * @note It is important to configure all the encoder instances with same + * parameters (except this one) in order to allow all the encoded parts + * obey the same restrictions implied by header. + * + * If offset is not 0, then stream header is omitted. + * In any case output start is byte aligned, so for proper streams stitching + * "predecessor" stream must be flushed. + * + * Range is not artificially limited, but all the values greater or equal to + * maximal window size have the same effect. Values greater than 2**30 are not + * allowed. + */ + BROTLI_PARAM_STREAM_OFFSET = 9 +} BrotliEncoderParameter; + +/** + * Opaque structure that holds encoder state. + * + * Allocated and initialized with ::BrotliEncoderCreateInstance. + * Cleaned up and deallocated with ::BrotliEncoderDestroyInstance. + */ +typedef struct BrotliEncoderStateStruct BrotliEncoderState; + +/** + * Sets the specified parameter to the given encoder instance. + * + * @param state encoder instance + * @param param parameter to set + * @param value new parameter value + * @returns ::BROTLI_FALSE if parameter is unrecognized, or value is invalid + * @returns ::BROTLI_FALSE if value of parameter can not be changed at current + * encoder state (e.g. when encoding is started, window size might be + * already encoded and therefore it is impossible to change it) + * @returns ::BROTLI_TRUE if value is accepted + * @warning invalid values might be accepted in case they would not break + * encoding process. + */ +BROTLI_ENC_API BROTLI_BOOL BrotliEncoderSetParameter( + BrotliEncoderState* state, BrotliEncoderParameter param, uint32_t value); + +/** + * Creates an instance of ::BrotliEncoderState and initializes it. + * + * @p alloc_func and @p free_func @b MUST be both zero or both non-zero. In the + * case they are both zero, default memory allocators are used. @p opaque is + * passed to @p alloc_func and @p free_func when they are called. @p free_func + * has to return without doing anything when asked to free a NULL pointer. + * + * @param alloc_func custom memory allocation function + * @param free_func custom memory free function + * @param opaque custom memory manager handle + * @returns @c 0 if instance can not be allocated or initialized + * @returns pointer to initialized ::BrotliEncoderState otherwise + */ +BROTLI_ENC_API BrotliEncoderState* BrotliEncoderCreateInstance( + brotli_alloc_func alloc_func, brotli_free_func free_func, void* opaque); + +/** + * Deinitializes and frees ::BrotliEncoderState instance. + * + * @param state decoder instance to be cleaned up and deallocated + */ +BROTLI_ENC_API void BrotliEncoderDestroyInstance(BrotliEncoderState* state); + +/* Opaque type for pointer to different possible internal structures containing + dictionary prepared for the encoder */ +typedef struct BrotliEncoderPreparedDictionaryStruct + BrotliEncoderPreparedDictionary; + +/** + * Prepares a shared dictionary from the given file format for the encoder. + * + * @p alloc_func and @p free_func @b MUST be both zero or both non-zero. In the + * case they are both zero, default memory allocators are used. @p opaque is + * passed to @p alloc_func and @p free_func when they are called. @p free_func + * has to return without doing anything when asked to free a NULL pointer. + * + * @warning Created instance is "lean"; it does not contain copy of @p data, + * rather it contains only pointer to it; therefore, + * @p data @b MUST outlive the created instance. + * + * @param type type of dictionary stored in data + * @param data_size size of @p data buffer + * @param data pointer to the dictionary data + * @param quality the maximum Brotli quality to prepare the dictionary for, + * use BROTLI_MAX_QUALITY by default + * @param alloc_func custom memory allocation function + * @param free_func custom memory free function + * @param opaque custom memory manager handle + */ +BROTLI_ENC_API BrotliEncoderPreparedDictionary* +BrotliEncoderPrepareDictionary(BrotliSharedDictionaryType type, + size_t data_size, const uint8_t data[BROTLI_ARRAY_PARAM(data_size)], + int quality, + brotli_alloc_func alloc_func, brotli_free_func free_func, void* opaque); + +BROTLI_ENC_API void BrotliEncoderDestroyPreparedDictionary( + BrotliEncoderPreparedDictionary* dictionary); + +/** + * Attaches a prepared dictionary of any type to the encoder. Can be used + * multiple times to attach multiple dictionaries. The dictionary type was + * determined by BrotliEncoderPrepareDictionary. Multiple raw prefix + * dictionaries and/or max 1 serialized dictionary with custom words can be + * attached. + * + * @returns ::BROTLI_FALSE in case of error + * @returns ::BROTLI_TRUE otherwise + */ +BROTLI_ENC_API BROTLI_BOOL BrotliEncoderAttachPreparedDictionary( + BrotliEncoderState* state, + const BrotliEncoderPreparedDictionary* dictionary); + +/** + * Calculates the output size bound for the given @p input_size. + * + * @warning Result is only valid if quality is at least @c 2 and, in + * case ::BrotliEncoderCompressStream was used, no flushes + * (::BROTLI_OPERATION_FLUSH) were performed. + * + * @param input_size size of projected input + * @returns @c 0 if result does not fit @c size_t + */ +BROTLI_ENC_API size_t BrotliEncoderMaxCompressedSize(size_t input_size); + +/** + * Performs one-shot memory-to-memory compression. + * + * Compresses the data in @p input_buffer into @p encoded_buffer, and sets + * @p *encoded_size to the compressed length. + * + * @note If ::BrotliEncoderMaxCompressedSize(@p input_size) returns non-zero + * value, then output is guaranteed to be no longer than that. + * + * @note If @p lgwin is greater than ::BROTLI_MAX_WINDOW_BITS then resulting + * stream might be incompatible with RFC 7932; to decode such streams, + * decoder should be configured with + * ::BROTLI_DECODER_PARAM_LARGE_WINDOW = @c 1 + * + * @param quality quality parameter value, e.g. ::BROTLI_DEFAULT_QUALITY + * @param lgwin lgwin parameter value, e.g. ::BROTLI_DEFAULT_WINDOW + * @param mode mode parameter value, e.g. ::BROTLI_DEFAULT_MODE + * @param input_size size of @p input_buffer + * @param input_buffer input data buffer with at least @p input_size + * addressable bytes + * @param[in, out] encoded_size @b in: size of @p encoded_buffer; \n + * @b out: length of compressed data written to + * @p encoded_buffer, or @c 0 if compression fails + * @param encoded_buffer compressed data destination buffer + * @returns ::BROTLI_FALSE in case of compression error + * @returns ::BROTLI_FALSE if output buffer is too small + * @returns ::BROTLI_TRUE otherwise + */ +BROTLI_ENC_API BROTLI_BOOL BrotliEncoderCompress( + int quality, int lgwin, BrotliEncoderMode mode, size_t input_size, + const uint8_t input_buffer[BROTLI_ARRAY_PARAM(input_size)], + size_t* encoded_size, + uint8_t encoded_buffer[BROTLI_ARRAY_PARAM(*encoded_size)]); + +/** + * Compresses input stream to output stream. + * + * The values @p *available_in and @p *available_out must specify the number of + * bytes addressable at @p *next_in and @p *next_out respectively. + * When @p *available_out is @c 0, @p next_out is allowed to be @c NULL. + * + * After each call, @p *available_in will be decremented by the amount of input + * bytes consumed, and the @p *next_in pointer will be incremented by that + * amount. Similarly, @p *available_out will be decremented by the amount of + * output bytes written, and the @p *next_out pointer will be incremented by + * that amount. + * + * @p total_out, if it is not a null-pointer, will be set to the number + * of bytes compressed since the last @p state initialization. + * + * + * + * Internally workflow consists of 3 tasks: + * -# (optionally) copy input data to internal buffer + * -# actually compress data and (optionally) store it to internal buffer + * -# (optionally) copy compressed bytes from internal buffer to output stream + * + * Whenever all 3 tasks can't move forward anymore, or error occurs, this + * method returns the control flow to caller. + * + * @p op is used to perform flush, finish the stream, or inject metadata block. + * See ::BrotliEncoderOperation for more information. + * + * Flushing the stream means forcing encoding of all input passed to encoder and + * completing the current output block, so it could be fully decoded by stream + * decoder. To perform flush set @p op to ::BROTLI_OPERATION_FLUSH. + * Under some circumstances (e.g. lack of output stream capacity) this operation + * would require several calls to ::BrotliEncoderCompressStream. The method must + * be called again until both input stream is depleted and encoder has no more + * output (see ::BrotliEncoderHasMoreOutput) after the method is called. + * + * Finishing the stream means encoding of all input passed to encoder and + * adding specific "final" marks, so stream decoder could determine that stream + * is complete. To perform finish set @p op to ::BROTLI_OPERATION_FINISH. + * Under some circumstances (e.g. lack of output stream capacity) this operation + * would require several calls to ::BrotliEncoderCompressStream. The method must + * be called again until both input stream is depleted and encoder has no more + * output (see ::BrotliEncoderHasMoreOutput) after the method is called. + * + * @warning When flushing and finishing, @p op should not change until operation + * is complete; input stream should not be swapped, reduced or + * extended as well. + * + * @param state encoder instance + * @param op requested operation + * @param[in, out] available_in @b in: amount of available input; \n + * @b out: amount of unused input + * @param[in, out] next_in pointer to the next input byte + * @param[in, out] available_out @b in: length of output buffer; \n + * @b out: remaining size of output buffer + * @param[in, out] next_out compressed output buffer cursor; + * can be @c NULL if @p available_out is @c 0 + * @param[out] total_out number of bytes produced so far; can be @c NULL + * @returns ::BROTLI_FALSE if there was an error + * @returns ::BROTLI_TRUE otherwise + */ +BROTLI_ENC_API BROTLI_BOOL BrotliEncoderCompressStream( + BrotliEncoderState* state, BrotliEncoderOperation op, size_t* available_in, + const uint8_t** next_in, size_t* available_out, uint8_t** next_out, + size_t* total_out); + +/** + * Checks if encoder instance reached the final state. + * + * @param state encoder instance + * @returns ::BROTLI_TRUE if encoder is in a state where it reached the end of + * the input and produced all of the output + * @returns ::BROTLI_FALSE otherwise + */ +BROTLI_ENC_API BROTLI_BOOL BrotliEncoderIsFinished(BrotliEncoderState* state); + +/** + * Checks if encoder has more output. + * + * @param state encoder instance + * @returns ::BROTLI_TRUE, if encoder has some unconsumed output + * @returns ::BROTLI_FALSE otherwise + */ +BROTLI_ENC_API BROTLI_BOOL BrotliEncoderHasMoreOutput( + BrotliEncoderState* state); + +/** + * Acquires pointer to internal output buffer. + * + * This method is used to make language bindings easier and more efficient: + * -# push data to ::BrotliEncoderCompressStream, + * until ::BrotliEncoderHasMoreOutput returns BROTLI_TRUE + * -# use ::BrotliEncoderTakeOutput to peek bytes and copy to language-specific + * entity + * + * Also this could be useful if there is an output stream that is able to + * consume all the provided data (e.g. when data is saved to file system). + * + * @attention After every call to ::BrotliEncoderTakeOutput @p *size bytes of + * output are considered consumed for all consecutive calls to the + * instance methods; returned pointer becomes invalidated as well. + * + * @note Encoder output is not guaranteed to be contiguous. This means that + * after the size-unrestricted call to ::BrotliEncoderTakeOutput, + * immediate next call to ::BrotliEncoderTakeOutput may return more data. + * + * @param state encoder instance + * @param[in, out] size @b in: number of bytes caller is ready to take, @c 0 if + * any amount could be handled; \n + * @b out: amount of data pointed by returned pointer and + * considered consumed; \n + * out value is never greater than in value, unless it is @c 0 + * @returns pointer to output data + */ +BROTLI_ENC_API const uint8_t* BrotliEncoderTakeOutput( + BrotliEncoderState* state, size_t* size); + +/* Returns the estimated peak memory usage (in bytes) of the BrotliCompress() + function, not counting the memory needed for the input and output. */ +BROTLI_ENC_EXTRA_API size_t BrotliEncoderEstimatePeakMemoryUsage( + int quality, int lgwin, size_t input_size); +/* Returns 0 if dictionary is not valid; otherwise returns allocation size. */ +BROTLI_ENC_EXTRA_API size_t BrotliEncoderGetPreparedDictionarySize( + const BrotliEncoderPreparedDictionary* dictionary); + +/** + * Gets an encoder library version. + * + * Look at BROTLI_MAKE_HEX_VERSION for more information. + */ +BROTLI_ENC_API uint32_t BrotliEncoderVersion(void); + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif + +#endif /* BROTLI_ENC_ENCODE_H_ */ diff --git a/packages/runtime/vendor/brotli/c/include/brotli/port.h b/packages/runtime/vendor/brotli/c/include/brotli/port.h new file mode 100644 index 000000000..11cefd791 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/include/brotli/port.h @@ -0,0 +1,302 @@ +/* Copyright 2016 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* Macros for compiler / platform specific API declarations. */ + +#ifndef BROTLI_COMMON_PORT_H_ +#define BROTLI_COMMON_PORT_H_ + +/* The following macros were borrowed from https://github.com/nemequ/hedley + * with permission of original author - Evan Nemerson */ + +/* >>> >>> >>> hedley macros */ + +#define BROTLI_MAKE_VERSION(major, minor, revision) \ + (((major) * 1000000) + ((minor) * 1000) + (revision)) + +#if defined(__GNUC__) && defined(__GNUC_PATCHLEVEL__) +#define BROTLI_GNUC_VERSION \ + BROTLI_MAKE_VERSION(__GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__) +#elif defined(__GNUC__) +#define BROTLI_GNUC_VERSION BROTLI_MAKE_VERSION(__GNUC__, __GNUC_MINOR__, 0) +#endif + +#if defined(BROTLI_GNUC_VERSION) +#define BROTLI_GNUC_VERSION_CHECK(major, minor, patch) \ + (BROTLI_GNUC_VERSION >= BROTLI_MAKE_VERSION(major, minor, patch)) +#else +#define BROTLI_GNUC_VERSION_CHECK(major, minor, patch) (0) +#endif + +#if defined(_MSC_FULL_VER) && (_MSC_FULL_VER >= 140000000) +#define BROTLI_MSVC_VERSION \ + BROTLI_MAKE_VERSION((_MSC_FULL_VER / 10000000), \ + (_MSC_FULL_VER % 10000000) / 100000, \ + (_MSC_FULL_VER % 100000) / 100) +#elif defined(_MSC_FULL_VER) +#define BROTLI_MSVC_VERSION \ + BROTLI_MAKE_VERSION((_MSC_FULL_VER / 1000000), \ + (_MSC_FULL_VER % 1000000) / 10000, \ + (_MSC_FULL_VER % 10000) / 10) +#elif defined(_MSC_VER) +#define BROTLI_MSVC_VERSION \ + BROTLI_MAKE_VERSION(_MSC_VER / 100, _MSC_VER % 100, 0) +#endif + +#if !defined(_MSC_VER) +#define BROTLI_MSVC_VERSION_CHECK(major, minor, patch) (0) +#elif defined(_MSC_VER) && (_MSC_VER >= 1400) +#define BROTLI_MSVC_VERSION_CHECK(major, minor, patch) \ + (_MSC_FULL_VER >= ((major * 10000000) + (minor * 100000) + (patch))) +#elif defined(_MSC_VER) && (_MSC_VER >= 1200) +#define BROTLI_MSVC_VERSION_CHECK(major, minor, patch) \ + (_MSC_FULL_VER >= ((major * 1000000) + (minor * 10000) + (patch))) +#else +#define BROTLI_MSVC_VERSION_CHECK(major, minor, patch) \ + (_MSC_VER >= ((major * 100) + (minor))) +#endif + +#if defined(__INTEL_COMPILER) && defined(__INTEL_COMPILER_UPDATE) +#define BROTLI_INTEL_VERSION \ + BROTLI_MAKE_VERSION(__INTEL_COMPILER / 100, \ + __INTEL_COMPILER % 100, \ + __INTEL_COMPILER_UPDATE) +#elif defined(__INTEL_COMPILER) +#define BROTLI_INTEL_VERSION \ + BROTLI_MAKE_VERSION(__INTEL_COMPILER / 100, __INTEL_COMPILER % 100, 0) +#endif + +#if defined(BROTLI_INTEL_VERSION) +#define BROTLI_INTEL_VERSION_CHECK(major, minor, patch) \ + (BROTLI_INTEL_VERSION >= BROTLI_MAKE_VERSION(major, minor, patch)) +#else +#define BROTLI_INTEL_VERSION_CHECK(major, minor, patch) (0) +#endif + +#if defined(__PGI) && \ + defined(__PGIC__) && defined(__PGIC_MINOR__) && defined(__PGIC_PATCHLEVEL__) +#define BROTLI_PGI_VERSION \ + BROTLI_MAKE_VERSION(__PGIC__, __PGIC_MINOR__, __PGIC_PATCHLEVEL__) +#endif + +#if defined(BROTLI_PGI_VERSION) +#define BROTLI_PGI_VERSION_CHECK(major, minor, patch) \ + (BROTLI_PGI_VERSION >= BROTLI_MAKE_VERSION(major, minor, patch)) +#else +#define BROTLI_PGI_VERSION_CHECK(major, minor, patch) (0) +#endif + +#if defined(__SUNPRO_C) && (__SUNPRO_C > 0x1000) +#define BROTLI_SUNPRO_VERSION \ + BROTLI_MAKE_VERSION( \ + (((__SUNPRO_C >> 16) & 0xf) * 10) + ((__SUNPRO_C >> 12) & 0xf), \ + (((__SUNPRO_C >> 8) & 0xf) * 10) + ((__SUNPRO_C >> 4) & 0xf), \ + (__SUNPRO_C & 0xf) * 10) +#elif defined(__SUNPRO_C) +#define BROTLI_SUNPRO_VERSION \ + BROTLI_MAKE_VERSION((__SUNPRO_C >> 8) & 0xf, \ + (__SUNPRO_C >> 4) & 0xf, \ + (__SUNPRO_C) & 0xf) +#elif defined(__SUNPRO_CC) && (__SUNPRO_CC > 0x1000) +#define BROTLI_SUNPRO_VERSION \ + BROTLI_MAKE_VERSION( \ + (((__SUNPRO_CC >> 16) & 0xf) * 10) + ((__SUNPRO_CC >> 12) & 0xf), \ + (((__SUNPRO_CC >> 8) & 0xf) * 10) + ((__SUNPRO_CC >> 4) & 0xf), \ + (__SUNPRO_CC & 0xf) * 10) +#elif defined(__SUNPRO_CC) +#define BROTLI_SUNPRO_VERSION \ + BROTLI_MAKE_VERSION((__SUNPRO_CC >> 8) & 0xf, \ + (__SUNPRO_CC >> 4) & 0xf, \ + (__SUNPRO_CC) & 0xf) +#endif + +#if defined(BROTLI_SUNPRO_VERSION) +#define BROTLI_SUNPRO_VERSION_CHECK(major, minor, patch) \ + (BROTLI_SUNPRO_VERSION >= BROTLI_MAKE_VERSION(major, minor, patch)) +#else +#define BROTLI_SUNPRO_VERSION_CHECK(major, minor, patch) (0) +#endif + +#if defined(__CC_ARM) && defined(__ARMCOMPILER_VERSION) +#define BROTLI_ARM_VERSION \ + BROTLI_MAKE_VERSION((__ARMCOMPILER_VERSION / 1000000), \ + (__ARMCOMPILER_VERSION % 1000000) / 10000, \ + (__ARMCOMPILER_VERSION % 10000) / 100) +#elif defined(__CC_ARM) && defined(__ARMCC_VERSION) +#define BROTLI_ARM_VERSION \ + BROTLI_MAKE_VERSION((__ARMCC_VERSION / 1000000), \ + (__ARMCC_VERSION % 1000000) / 10000, \ + (__ARMCC_VERSION % 10000) / 100) +#endif + +#if defined(BROTLI_ARM_VERSION) +#define BROTLI_ARM_VERSION_CHECK(major, minor, patch) \ + (BROTLI_ARM_VERSION >= BROTLI_MAKE_VERSION(major, minor, patch)) +#else +#define BROTLI_ARM_VERSION_CHECK(major, minor, patch) (0) +#endif + +#if defined(__ibmxl__) +#define BROTLI_IBM_VERSION \ + BROTLI_MAKE_VERSION(__ibmxl_version__, \ + __ibmxl_release__, \ + __ibmxl_modification__) +#elif defined(__xlC__) && defined(__xlC_ver__) +#define BROTLI_IBM_VERSION \ + BROTLI_MAKE_VERSION(__xlC__ >> 8, __xlC__ & 0xff, (__xlC_ver__ >> 8) & 0xff) +#elif defined(__xlC__) +#define BROTLI_IBM_VERSION BROTLI_MAKE_VERSION(__xlC__ >> 8, __xlC__ & 0xff, 0) +#endif + +#if defined(BROTLI_IBM_VERSION) +#define BROTLI_IBM_VERSION_CHECK(major, minor, patch) \ + (BROTLI_IBM_VERSION >= BROTLI_MAKE_VERSION(major, minor, patch)) +#else +#define BROTLI_IBM_VERSION_CHECK(major, minor, patch) (0) +#endif + +#if defined(__TI_COMPILER_VERSION__) +#define BROTLI_TI_VERSION \ + BROTLI_MAKE_VERSION((__TI_COMPILER_VERSION__ / 1000000), \ + (__TI_COMPILER_VERSION__ % 1000000) / 1000, \ + (__TI_COMPILER_VERSION__ % 1000)) +#endif + +#if defined(BROTLI_TI_VERSION) +#define BROTLI_TI_VERSION_CHECK(major, minor, patch) \ + (BROTLI_TI_VERSION >= BROTLI_MAKE_VERSION(major, minor, patch)) +#else +#define BROTLI_TI_VERSION_CHECK(major, minor, patch) (0) +#endif + +#if defined(__IAR_SYSTEMS_ICC__) +#if __VER__ > 1000 +#define BROTLI_IAR_VERSION \ + BROTLI_MAKE_VERSION((__VER__ / 1000000), \ + (__VER__ / 1000) % 1000, \ + (__VER__ % 1000)) +#else +#define BROTLI_IAR_VERSION BROTLI_MAKE_VERSION(VER / 100, __VER__ % 100, 0) +#endif +#endif + +#if defined(BROTLI_IAR_VERSION) +#define BROTLI_IAR_VERSION_CHECK(major, minor, patch) \ + (BROTLI_IAR_VERSION >= BROTLI_MAKE_VERSION(major, minor, patch)) +#else +#define BROTLI_IAR_VERSION_CHECK(major, minor, patch) (0) +#endif + +#if defined(__TINYC__) +#define BROTLI_TINYC_VERSION \ + BROTLI_MAKE_VERSION(__TINYC__ / 1000, (__TINYC__ / 100) % 10, __TINYC__ % 100) +#endif + +#if defined(BROTLI_TINYC_VERSION) +#define BROTLI_TINYC_VERSION_CHECK(major, minor, patch) \ + (BROTLI_TINYC_VERSION >= BROTLI_MAKE_VERSION(major, minor, patch)) +#else +#define BROTLI_TINYC_VERSION_CHECK(major, minor, patch) (0) +#endif + +#if defined(__has_attribute) +#define BROTLI_GNUC_HAS_ATTRIBUTE(attribute, major, minor, patch) \ + __has_attribute(attribute) +#else +#define BROTLI_GNUC_HAS_ATTRIBUTE(attribute, major, minor, patch) \ + BROTLI_GNUC_VERSION_CHECK(major, minor, patch) +#endif + +#if defined(__has_builtin) +#define BROTLI_GNUC_HAS_BUILTIN(builtin, major, minor, patch) \ + __has_builtin(builtin) +#else +#define BROTLI_GNUC_HAS_BUILTIN(builtin, major, minor, patch) \ + BROTLI_GNUC_VERSION_CHECK(major, minor, patch) +#endif + +#if defined(__has_feature) +#define BROTLI_HAS_FEATURE(feature) __has_feature(feature) +#else +#define BROTLI_HAS_FEATURE(feature) (0) +#endif + +#if defined(_WIN32) || defined(__CYGWIN__) +#define BROTLI_PUBLIC +#elif BROTLI_GNUC_VERSION_CHECK(3, 3, 0) || \ + BROTLI_TI_VERSION_CHECK(8, 0, 0) || \ + BROTLI_INTEL_VERSION_CHECK(16, 0, 0) || \ + BROTLI_ARM_VERSION_CHECK(4, 1, 0) || \ + BROTLI_IBM_VERSION_CHECK(13, 1, 0) || \ + BROTLI_SUNPRO_VERSION_CHECK(5, 11, 0) || \ + (BROTLI_TI_VERSION_CHECK(7, 3, 0) && \ + defined(__TI_GNU_ATTRIBUTE_SUPPORT__) && defined(__TI_EABI__)) +#define BROTLI_PUBLIC __attribute__ ((visibility ("default"))) +#else +#define BROTLI_PUBLIC +#endif + +#if defined(_WIN32) || defined(__CYGWIN__) +#define BROTLI_INTERNAL +#elif BROTLI_GNUC_VERSION_CHECK(3, 3, 0) || \ + BROTLI_TI_VERSION_CHECK(8, 0, 0) || \ + BROTLI_INTEL_VERSION_CHECK(16, 0, 0) || \ + BROTLI_ARM_VERSION_CHECK(4, 1, 0) || \ + BROTLI_IBM_VERSION_CHECK(13, 1, 0) || \ + BROTLI_SUNPRO_VERSION_CHECK(5, 11, 0) || \ + (BROTLI_TI_VERSION_CHECK(7, 3, 0) && \ + defined(__TI_GNU_ATTRIBUTE_SUPPORT__) && defined(__TI_EABI__)) +#define BROTLI_INTERNAL __attribute__ ((visibility ("hidden"))) +#else +#define BROTLI_INTERNAL +#endif + +#if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) && \ + !defined(__STDC_NO_VLA__) && !defined(__cplusplus) && \ + !defined(__PGI) && !defined(__PGIC__) && !defined(__TINYC__) && \ + !defined(__clang__) +#define BROTLI_ARRAY_PARAM(name) (name) +#else +#define BROTLI_ARRAY_PARAM(name) +#endif + +/* <<< <<< <<< end of hedley macros. */ + +#if defined(BROTLI_SHARED_COMPILATION) +#if defined(_WIN32) +#if defined(BROTLICOMMON_SHARED_COMPILATION) +#define BROTLI_COMMON_API __declspec(dllexport) +#else /* !BROTLICOMMON_SHARED_COMPILATION */ +#define BROTLI_COMMON_API __declspec(dllimport) +#endif /* BROTLICOMMON_SHARED_COMPILATION */ +#if defined(BROTLIDEC_SHARED_COMPILATION) +#define BROTLI_DEC_API __declspec(dllexport) +#else /* !BROTLIDEC_SHARED_COMPILATION */ +#define BROTLI_DEC_API __declspec(dllimport) +#endif /* BROTLIDEC_SHARED_COMPILATION */ +#if defined(BROTLIENC_SHARED_COMPILATION) +#define BROTLI_ENC_API __declspec(dllexport) +#else /* !BROTLIENC_SHARED_COMPILATION */ +#define BROTLI_ENC_API __declspec(dllimport) +#endif /* BROTLIENC_SHARED_COMPILATION */ +#else /* !_WIN32 */ +#define BROTLI_COMMON_API BROTLI_PUBLIC +#define BROTLI_DEC_API BROTLI_PUBLIC +#define BROTLI_ENC_API BROTLI_PUBLIC +#endif /* _WIN32 */ +#else /* BROTLI_SHARED_COMPILATION */ +#define BROTLI_COMMON_API +#define BROTLI_DEC_API +#define BROTLI_ENC_API +#endif + +#if defined(BROTLI_BUILD_ENC_EXTRA_API) +#define BROTLI_ENC_EXTRA_API BROTLI_ENC_API +#else +#define BROTLI_ENC_EXTRA_API BROTLI_INTERNAL +#endif + +#endif /* BROTLI_COMMON_PORT_H_ */ diff --git a/packages/runtime/vendor/brotli/c/include/brotli/shared_dictionary.h b/packages/runtime/vendor/brotli/c/include/brotli/shared_dictionary.h new file mode 100644 index 000000000..2970c2dcc --- /dev/null +++ b/packages/runtime/vendor/brotli/c/include/brotli/shared_dictionary.h @@ -0,0 +1,100 @@ +/* Copyright 2017 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/* (Opaque) Shared Dictionary definition and utilities. */ + +#ifndef BROTLI_COMMON_SHARED_DICTIONARY_H_ +#define BROTLI_COMMON_SHARED_DICTIONARY_H_ + +#include +#include + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +#define SHARED_BROTLI_MIN_DICTIONARY_WORD_LENGTH 4 +#define SHARED_BROTLI_MAX_DICTIONARY_WORD_LENGTH 31 +#define SHARED_BROTLI_NUM_DICTIONARY_CONTEXTS 64 +#define SHARED_BROTLI_MAX_COMPOUND_DICTS 15 + +/** + * Opaque structure that holds shared dictionary data. + * + * Allocated and initialized with ::BrotliSharedDictionaryCreateInstance. + * Cleaned up and deallocated with ::BrotliSharedDictionaryDestroyInstance. + */ +typedef struct BrotliSharedDictionaryStruct BrotliSharedDictionary; + +/** + * Input data type for ::BrotliSharedDictionaryAttach. + */ +typedef enum BrotliSharedDictionaryType { + /** Raw LZ77 prefix dictionary. */ + BROTLI_SHARED_DICTIONARY_RAW = 0, + /** Serialized shared dictionary. + * + * DO NOT USE: methods accepting this value will fail. + */ + BROTLI_SHARED_DICTIONARY_SERIALIZED = 1 +} BrotliSharedDictionaryType; + +/** + * Creates an instance of ::BrotliSharedDictionary. + * + * Fresh instance has default word dictionary and transforms + * and no LZ77 prefix dictionary. + * + * @p alloc_func and @p free_func @b MUST be both zero or both non-zero. In the + * case they are both zero, default memory allocators are used. @p opaque is + * passed to @p alloc_func and @p free_func when they are called. @p free_func + * has to return without doing anything when asked to free a NULL pointer. + * + * @param alloc_func custom memory allocation function + * @param free_func custom memory free function + * @param opaque custom memory manager handle + * @returns @c 0 if instance can not be allocated or initialized + * @returns pointer to initialized ::BrotliSharedDictionary otherwise + */ +BROTLI_COMMON_API BrotliSharedDictionary* BrotliSharedDictionaryCreateInstance( + brotli_alloc_func alloc_func, brotli_free_func free_func, void* opaque); + +/** + * Deinitializes and frees ::BrotliSharedDictionary instance. + * + * @param dict shared dictionary instance to be cleaned up and deallocated + */ +BROTLI_COMMON_API void BrotliSharedDictionaryDestroyInstance( + BrotliSharedDictionary* dict); + +/** + * Attaches dictionary to a given instance of ::BrotliSharedDictionary. + * + * Dictionary to be attached is represented in a serialized format as a region + * of memory. + * + * Provided data it partially referenced by a resulting (compound) dictionary, + * and should be kept untouched, while at least one compound dictionary uses it. + * This way memory overhead is kept minimal by the cost of additional resource + * management. + * + * @param dict dictionary to extend + * @param type type of dictionary to attach + * @param data_size size of @p data + * @param data serialized dictionary of type @p type, with at least @p data_size + * addressable bytes + * @returns ::BROTLI_TRUE if provided dictionary is successfully attached + * @returns ::BROTLI_FALSE otherwise + */ +BROTLI_COMMON_API BROTLI_BOOL BrotliSharedDictionaryAttach( + BrotliSharedDictionary* dict, BrotliSharedDictionaryType type, + size_t data_size, const uint8_t data[BROTLI_ARRAY_PARAM(data_size)]); + +#if defined(__cplusplus) || defined(c_plusplus) +} /* extern "C" */ +#endif + +#endif /* BROTLI_COMMON_SHARED_DICTIONARY_H_ */ diff --git a/packages/runtime/vendor/brotli/c/include/brotli/types.h b/packages/runtime/vendor/brotli/c/include/brotli/types.h new file mode 100644 index 000000000..2dc61ff40 --- /dev/null +++ b/packages/runtime/vendor/brotli/c/include/brotli/types.h @@ -0,0 +1,83 @@ +/* Copyright 2013 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +/** + * @file + * Common types used in decoder and encoder API. + */ + +#ifndef BROTLI_COMMON_TYPES_H_ +#define BROTLI_COMMON_TYPES_H_ + +#include /* IWYU pragma: export */ + +#if defined(_MSC_VER) && (_MSC_VER < 1600) +typedef __int8 int8_t; +typedef unsigned __int8 uint8_t; +typedef __int16 int16_t; +typedef unsigned __int16 uint16_t; +typedef __int32 int32_t; +typedef unsigned __int32 uint32_t; +typedef unsigned __int64 uint64_t; +typedef __int64 int64_t; +#else +#include /* IWYU pragma: export */ +#endif /* defined(_MSC_VER) && (_MSC_VER < 1600) */ + +/** + * A portable @c bool replacement. + * + * ::BROTLI_BOOL is a "documentation" type: actually it is @c int, but in API it + * denotes a type, whose only values are ::BROTLI_TRUE and ::BROTLI_FALSE. + * + * ::BROTLI_BOOL values passed to Brotli should either be ::BROTLI_TRUE or + * ::BROTLI_FALSE, or be a result of ::TO_BROTLI_BOOL macros. + * + * ::BROTLI_BOOL values returned by Brotli should not be tested for equality + * with @c true, @c false, ::BROTLI_TRUE, ::BROTLI_FALSE, but rather should be + * evaluated, for example: @code{.cpp} + * if (SomeBrotliFunction(encoder, BROTLI_TRUE) && + * !OtherBrotliFunction(decoder, BROTLI_FALSE)) { + * bool x = !!YetAnotherBrotliFunction(encoder, TO_BROLTI_BOOL(2 * 2 == 4)); + * DoSomething(x); + * } + * @endcode + */ +#define BROTLI_BOOL int +/** Portable @c true replacement. */ +#define BROTLI_TRUE 1 +/** Portable @c false replacement. */ +#define BROTLI_FALSE 0 +/** @c bool to ::BROTLI_BOOL conversion macros. */ +#define TO_BROTLI_BOOL(X) (!!(X) ? BROTLI_TRUE : BROTLI_FALSE) + +#define BROTLI_MAKE_UINT64_T(high, low) ((((uint64_t)(high)) << 32) | low) + +#define BROTLI_UINT32_MAX (~((uint32_t)0)) +#define BROTLI_SIZE_MAX (~((size_t)0)) + +/** + * Allocating function pointer type. + * + * @param opaque custom memory manager handle provided by client + * @param size requested memory region size; can not be @c 0 + * @returns @c 0 in the case of failure + * @returns a valid pointer to a memory region of at least @p size bytes + * long otherwise + */ +typedef void* (*brotli_alloc_func)(void* opaque, size_t size); + +/** + * Deallocating function pointer type. + * + * This function @b SHOULD do nothing if @p address is @c 0. + * + * @param opaque custom memory manager handle provided by client + * @param address memory region pointer returned by ::brotli_alloc_func, or @c 0 + */ +typedef void (*brotli_free_func)(void* opaque, void* address); + +#endif /* BROTLI_COMMON_TYPES_H_ */ diff --git a/packages/runtime/vendor/v8-ieee754/LICENSE b/packages/runtime/vendor/v8-ieee754/LICENSE new file mode 100644 index 000000000..9258b08c2 --- /dev/null +++ b/packages/runtime/vendor/v8-ieee754/LICENSE @@ -0,0 +1,75 @@ +This license applies to all parts of V8 that are not externally +maintained libraries. The externally maintained libraries used by V8 +are: + + - PCRE test suite, located in + test/mjsunit/third_party/regexp-pcre/regexp-pcre.js. This is based on the + test suite from PCRE-7.3, which is copyrighted by the University + of Cambridge and Google, Inc. The copyright notice and license + are embedded in regexp-pcre.js. + + - Layout tests, located in test/mjsunit/third_party/object-keys. These are + based on layout tests from webkit.org which are copyrighted by + Apple Computer, Inc. and released under a 3-clause BSD license. + + - Strongtalk assembler, the basis of the files assembler-arm-inl.h, + assembler-arm.cc, assembler-arm.h, assembler-ia32-inl.h, + assembler-ia32.cc, assembler-ia32.h, assembler-x64-inl.h, + assembler-x64.cc, assembler-x64.h, assembler.cc and assembler.h. + This code is copyrighted by Sun Microsystems Inc. and released + under a 3-clause BSD license. + + - Valgrind client API header, located at third_party/valgrind/valgrind.h + This is released under the BSD license. + + - The Wasm C/C++ API headers, located at third_party/wasm-api/wasm.{h,hh} + This is released under the Apache license. The API's upstream prototype + implementation also formed the basis of V8's implementation in + src/wasm/c-api.cc. + +These libraries have their own licenses; we recommend you read them, +as their terms may differ from the terms below. + +Further license information can be found in LICENSE files located in +sub-directories. + +Copyright 2014, the V8 project authors. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +--- + +src/scr_ieee754.c is a port of V8's src/base/ieee754.cc, which V8 adapted +from fdlibm. fdlibm's notice: + +==================================================== +Copyright (C) 1993 by Sun Microsystems, Inc. All rights reserved. + +Developed at SunSoft, a Sun Microsystems, Inc. business. +Permission to use, copy, modify, and distribute this +software is freely granted, provided that this notice +is preserved. +==================================================== diff --git a/packages/threads/package.json b/packages/threads/package.json index 11b8ef624..6ebcf5b8b 100644 --- a/packages/threads/package.json +++ b/packages/threads/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/threads", - "version": "0.4.2", + "version": "0.4.5", "description": "Zero-copy publication of immutable object graphs to worker threads (scriptc), with a deep-freezing Node.js implementation", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/types/package.json b/packages/types/package.json index 689c7b72f..b2aa805be 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/types", - "version": "0.4.2", + "version": "0.4.5", "description": "Contract types for scriptc: type-only annotations (Int32, NoThrow, Immutable) that scriptc checks and optimizes, with no runtime semantics", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9235b8cb3..c7fdea566 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -93,19 +93,68 @@ importers: specifier: workspace:* version: link:../cli-win32-x64-msvc - packages/cli-darwin-arm64: {} + packages/cli-darwin-arm64: + dependencies: + '@scriptc/llvm-darwin-arm64': + specifier: workspace:* + version: link:../llvm-darwin-arm64 + '@scriptc/runtime-darwin-arm64': + specifier: workspace:* + version: link:../runtime-darwin-arm64 - packages/cli-darwin-x64: {} + packages/cli-darwin-x64: + dependencies: + '@scriptc/llvm-darwin-x64': + specifier: workspace:* + version: link:../llvm-darwin-x64 + '@scriptc/runtime-darwin-x64': + specifier: workspace:* + version: link:../runtime-darwin-x64 - packages/cli-linux-arm64-gnu: {} + packages/cli-linux-arm64-gnu: + dependencies: + '@scriptc/llvm-linux-arm64-gnu': + specifier: workspace:* + version: link:../llvm-linux-arm64-gnu + '@scriptc/runtime-linux-arm64-gnu': + specifier: workspace:* + version: link:../runtime-linux-arm64-gnu - packages/cli-linux-arm64-musl: {} + packages/cli-linux-arm64-musl: + dependencies: + '@scriptc/llvm-linux-arm64-musl': + specifier: workspace:* + version: link:../llvm-linux-arm64-musl + '@scriptc/runtime-linux-arm64-musl': + specifier: workspace:* + version: link:../runtime-linux-arm64-musl - packages/cli-linux-x64-gnu: {} + packages/cli-linux-x64-gnu: + dependencies: + '@scriptc/llvm-linux-x64-gnu': + specifier: workspace:* + version: link:../llvm-linux-x64-gnu + '@scriptc/runtime-linux-x64-gnu': + specifier: workspace:* + version: link:../runtime-linux-x64-gnu - packages/cli-linux-x64-musl: {} + packages/cli-linux-x64-musl: + dependencies: + '@scriptc/llvm-linux-x64-musl': + specifier: workspace:* + version: link:../llvm-linux-x64-musl + '@scriptc/runtime-linux-x64-musl': + specifier: workspace:* + version: link:../runtime-linux-x64-musl - packages/cli-win32-x64-msvc: {} + packages/cli-win32-x64-msvc: + dependencies: + '@scriptc/llvm-win32-x64-msvc': + specifier: workspace:* + version: link:../llvm-win32-x64-msvc + '@scriptc/runtime-win32-x64-msvc': + specifier: workspace:* + version: link:../runtime-win32-x64-msvc packages/compiler: dependencies: @@ -113,8 +162,8 @@ importers: specifier: workspace:* version: link:../runtime tsc-ts: - specifier: 0.0.8 - version: 0.0.8 + specifier: 0.0.10 + version: 0.0.10 typescript: specifier: 7.0.2 version: 7.0.2 @@ -1375,8 +1424,8 @@ packages: cpu: [x64] os: [win32] - '@scriptc/threads@0.4.2': - resolution: {integrity: sha512-nR9FUxpOpgtTmJu6dejttElBL+JJTAZzJ2DZM+eNoujmLx9VpdlAVdgj3cu/MDsTUg/1sIUBq1ME1csC/Aa6yg==} + '@scriptc/threads@0.4.5': + resolution: {integrity: sha512-wHYrCItM68xYAwjnvM6VrRLDWd9+lAEQNjKh6PyLA8RdE7D8ts/VMoTFLmXRu2uvRywJuY18WPSCoEFRf3qfiA==} engines: {node: '>=24.0.0'} '@sinclair/typebox@0.25.24': @@ -1392,42 +1441,42 @@ packages: '@ts-morph/common@0.11.1': resolution: {integrity: sha512-7hWZS0NRpEsNV8vWJzg7FEz6V8MaLNeJOmwmghqUXTpzk16V1LLZhdo+4QvE/+zv4cVci0OviuJFnqhEfoV3+g==} - '@tsc-ts/darwin-arm64@0.0.8': - resolution: {integrity: sha512-+hB9yWk58lwZHGrVEzqEC4D7MQW7Vd2z7blV3TULIKZPTMwdf8aT7XFAmfP48PoGff0H9yuQKyRXJl9I+sQv9w==} + '@tsc-ts/darwin-arm64@0.0.10': + resolution: {integrity: sha512-mRctKijCzWRxJgxiVAdsk+x9WB7kZz9GBbJ4eVP95bJFkIz9D11uw3xC0m6jWYNoaShF6zn48eLMTYOyJNxBdA==} cpu: [arm64] os: [darwin] - '@tsc-ts/darwin-x64@0.0.8': - resolution: {integrity: sha512-yz4UddicqMXe+T7nV7ftpf155lkiK6gIPNwSB8rH9dlwiV6a17CO/vpSk627QLMqnfGNicB6YwLMk/SJ9moPdw==} + '@tsc-ts/darwin-x64@0.0.10': + resolution: {integrity: sha512-oV78/mi3ot8CG3BA9QxrsamvNXutpschgae+qV5skU4gAqimmBsBHy/MWTf9SCssvtWeqrX4uZvcwE0h/AJ+MQ==} cpu: [x64] os: [darwin] - '@tsc-ts/linux-arm64-gnu@0.0.8': - resolution: {integrity: sha512-LHqfT9mQ4tusPdWmnanCvC8/YBOF5bHM5h5863a3t2HTgFVdps1Qwb0yzrW5DDbojIhQ0Z+GupxtXVUQcV0Q/Q==} + '@tsc-ts/linux-arm64-gnu@0.0.10': + resolution: {integrity: sha512-O5msxFydn9r64QVm9WDWG8CKkXLjXWo12XeJWDncHlQYK80SoM4T8/C1UTDLtb6A2P+NQ5a+Xq4AaHmNb8VIqg==} cpu: [arm64] os: [linux] libc: [glibc] - '@tsc-ts/linux-arm64-musl@0.0.8': - resolution: {integrity: sha512-I0CC3xZMwoQM6OiVsubRHb0VI/RrJpneb33kDQhadYHq3taPos71KwP+CbCXNu88rNsDsyn7D1oCVfJb73g4Mw==} + '@tsc-ts/linux-arm64-musl@0.0.10': + resolution: {integrity: sha512-v9N5HuqcEW97MzwzLqPNAN5tfAcXIAlLRtp7BSJJd5VKfBu6DplqNZzwCH+//NuAQOtWGSr/Dq565JFPmDRt5w==} cpu: [arm64] os: [linux] libc: [musl] - '@tsc-ts/linux-x64-gnu@0.0.8': - resolution: {integrity: sha512-PLERuwUuxT92x0BZAMQubiQP5jhlqoLmswaZWqcvfKAZyBp0TbWiVOmuNnDbJozvBaNUrfdGPb9suiQ+Me7M6A==} + '@tsc-ts/linux-x64-gnu@0.0.10': + resolution: {integrity: sha512-sRTPB/50uAJSpMjlA3sc9GTAySnLwIlf1txR8eHlcVpKE6q0QgWvEkKqCSGtFXkn8IQ8z8O68jrAk9WjmRTaYQ==} cpu: [x64] os: [linux] libc: [glibc] - '@tsc-ts/linux-x64-musl@0.0.8': - resolution: {integrity: sha512-zDl36l1uyWc0mQniOAMZmkVD9TrlFExHlDT0fL8JGD4XLgx+bRe+To38yP0KSBehXBzjDNn8xrEoJ+3RP6mYwQ==} + '@tsc-ts/linux-x64-musl@0.0.10': + resolution: {integrity: sha512-U4qZwP0JzrR/l4kzhh+w+y6o2guEEinC/wminfXXDhez2DtjNTOrM1GxMPfZ7JkCIcKEruL7iCC2telapFk5LQ==} cpu: [x64] os: [linux] libc: [musl] - '@tsc-ts/win32-x64-msvc@0.0.8': - resolution: {integrity: sha512-9gO+pFoLiIxITlAE2P6JL8ihZwns7z/2Raw5+38RqLzHmnSsRN47+H4p/4EqQsLW5/TTsAo9uSeSkf3EuRg+XA==} + '@tsc-ts/win32-x64-msvc@0.0.10': + resolution: {integrity: sha512-TsiK6k0ljFW3pNR5lvF7SL0nv3aGxr31ZbBCfIXcBrqAvpumINOy/G0ESfTvZWx8llwOQt3PxBS414gF/J0tFA==} cpu: [x64] os: [win32] @@ -2592,8 +2641,8 @@ packages: ts-toolbelt@6.15.5: resolution: {integrity: sha512-FZIXf1ksVyLcfr7M317jbB67XFJhOO1YqdTcuGaq9q5jLUoTikukZ+98TPjKiP2jC5CgmYdWWYs0s2nLSU0/1A==} - tsc-ts@0.0.8: - resolution: {integrity: sha512-EiyDziYOryBg/YEvev/mKCn+hD4M6VFjH7VP9nv2WadDKYaPrdb+a3hP+wYr/UsDWNE7AdL7YGnjZppeXlXy6g==} + tsc-ts@0.0.10: + resolution: {integrity: sha512-oF5hZbeNgJXwJ868adnvBAihxYUwra1rj6Sa2+UhuhW477t5MmGnOsZgGTSGz511uTfuUxDz6e/gLIzvvScbGw==} engines: {node: '>=20'} hasBin: true @@ -3454,7 +3503,7 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.62.2': optional: true - '@scriptc/threads@0.4.2': {} + '@scriptc/threads@0.4.5': {} '@sinclair/typebox@0.25.24': {} @@ -3469,25 +3518,25 @@ snapshots: mkdirp: 1.0.4 path-browserify: 1.0.1 - '@tsc-ts/darwin-arm64@0.0.8': + '@tsc-ts/darwin-arm64@0.0.10': optional: true - '@tsc-ts/darwin-x64@0.0.8': + '@tsc-ts/darwin-x64@0.0.10': optional: true - '@tsc-ts/linux-arm64-gnu@0.0.8': + '@tsc-ts/linux-arm64-gnu@0.0.10': optional: true - '@tsc-ts/linux-arm64-musl@0.0.8': + '@tsc-ts/linux-arm64-musl@0.0.10': optional: true - '@tsc-ts/linux-x64-gnu@0.0.8': + '@tsc-ts/linux-x64-gnu@0.0.10': optional: true - '@tsc-ts/linux-x64-musl@0.0.8': + '@tsc-ts/linux-x64-musl@0.0.10': optional: true - '@tsc-ts/win32-x64-msvc@0.0.8': + '@tsc-ts/win32-x64-msvc@0.0.10': optional: true '@tybys/wasm-util@0.10.3': @@ -4828,17 +4877,17 @@ snapshots: ts-toolbelt@6.15.5: {} - tsc-ts@0.0.8: + tsc-ts@0.0.10: dependencies: - '@scriptc/threads': 0.4.2 + '@scriptc/threads': 0.4.5 optionalDependencies: - '@tsc-ts/darwin-arm64': 0.0.8 - '@tsc-ts/darwin-x64': 0.0.8 - '@tsc-ts/linux-arm64-gnu': 0.0.8 - '@tsc-ts/linux-arm64-musl': 0.0.8 - '@tsc-ts/linux-x64-gnu': 0.0.8 - '@tsc-ts/linux-x64-musl': 0.0.8 - '@tsc-ts/win32-x64-msvc': 0.0.8 + '@tsc-ts/darwin-arm64': 0.0.10 + '@tsc-ts/darwin-x64': 0.0.10 + '@tsc-ts/linux-arm64-gnu': 0.0.10 + '@tsc-ts/linux-arm64-musl': 0.0.10 + '@tsc-ts/linux-x64-gnu': 0.0.10 + '@tsc-ts/linux-x64-musl': 0.0.10 + '@tsc-ts/win32-x64-msvc': 0.0.10 tslib@2.8.1: optional: true diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index e74d40442..b678a8730 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -7,15 +7,15 @@ minimumReleaseAgeExclude: - "effect@4.0.0" - "@effect/platform-node@4.0.0" - "@effect/platform-node-shared@4.0.0" - - "@tsc-ts/darwin-arm64@0.0.8" - - "@tsc-ts/darwin-x64@0.0.8" - - "@tsc-ts/linux-arm64-gnu@0.0.8" - - "@tsc-ts/linux-arm64-musl@0.0.8" - - "@tsc-ts/linux-x64-gnu@0.0.8" - - "@tsc-ts/linux-x64-musl@0.0.8" - - "@tsc-ts/win32-x64-msvc@0.0.8" - - "tsc-ts@0.0.8" - - "@scriptc/threads@0.4.2" + - "@tsc-ts/darwin-arm64@0.0.10" + - "@tsc-ts/darwin-x64@0.0.10" + - "@tsc-ts/linux-arm64-gnu@0.0.10" + - "@tsc-ts/linux-arm64-musl@0.0.10" + - "@tsc-ts/linux-x64-gnu@0.0.10" + - "@tsc-ts/linux-x64-musl@0.0.10" + - "@tsc-ts/win32-x64-msvc@0.0.10" + - "tsc-ts@0.0.10" + - "@scriptc/threads@0.4.5" allowBuilds: esbuild: true overrides: diff --git a/scripts/bench-runtime.mjs b/scripts/bench-runtime.mjs index 61134fa4c..0845d57b8 100644 --- a/scripts/bench-runtime.mjs +++ b/scripts/bench-runtime.mjs @@ -40,6 +40,10 @@ const { values } = parseArgs({ layouts: { type: "string", default: "1" }, "baseline-optimization": { type: "string", default: "release" }, "candidate-optimization": { type: "string", default: "release" }, + "baseline-target-cpu": { type: "string" }, + "candidate-target-cpu": { type: "string" }, + "baseline-cpu-dispatch": { type: "boolean", default: false }, + "candidate-cpu-dispatch": { type: "boolean", default: false }, json: { type: "string" }, "no-lock": { type: "boolean", default: false }, node: { type: "boolean", default: true }, @@ -51,6 +55,8 @@ if (values.help) { console.log(`Usage: node scripts/bench-runtime.mjs [--candidate=] [--baseline=] [--workloads=a,b] [--runs=15] [--warmup=2] [--timeout=120] [--layouts=1] [--json=] [--baseline-optimization=release] [--candidate-optimization=release] + [--baseline-target-cpu=] [--candidate-target-cpu=] + [--baseline-cpu-dispatch] [--candidate-cpu-dispatch] [--no-lock] [--no-node] [--keep] Each checkout must have a built CLI (packages/cli/dist/bootstrap.js) and native @@ -69,7 +75,11 @@ Workload names are the file names in benchmarks/runtime/workloads/*.json; for ex --baseline-optimization/--candidate-optimization pick each contender's --optimization posture (default release). The same checkout may be both contenders, e.g. --baseline=. --candidate-optimization=speed compares speed with -release. cold_build_ms records each contender's first, uncached build.`); +release. --baseline-target-cpu/--candidate-target-cpu pass --target-cpu to each +contender's builds, e.g. --baseline=. --candidate-target-cpu=x86-64-v3 compares +x86-64-v3 code with the baseline. --baseline-cpu-dispatch/--candidate-cpu-dispatch +pass --cpu-dispatch, and SCRIPTC_CPU_LEVEL in the environment reaches every timed +run. cold_build_ms records each contender's first, uncached build.`); process.exit(0); } const runs = Number(values.runs); @@ -174,6 +184,16 @@ function generateInput(kind) { ["sku-" + i, "category-" + (i % 17), "product " + i, i % 40, 199 + (i % 5000)].join("\t"), ).join("\n") + "\n", ); + } else if (kind === "line-reader") { + // About 67 MB of access-log lines; every 50th path is not ASCII. + writeFileSync( + path, + Array.from( + { length: 600_000 }, + (_, i) => + `10.${i % 251}.${i % 7}.${i % 13} - - [24/Oct/2026:${String(i % 24).padStart(2, "0")}:${String(i % 60).padStart(2, "0")}:00 +0000] "${i % 9 === 0 ? "POST" : "GET"} /${i % 50 === 0 ? "caf\u00e9" : "api"}/items/${i % 997}?page=${i % 31} HTTP/1.1" ${i % 23 === 0 ? 404 : 200} ${(i * 7919) % 50000} "scriptc-bench/1.0"`, + ).join("\n") + "\n", + ); } else if (kind === "cli-config") { writeFileSync(path, JSON.stringify(deployManifest(), null, 2) + "\n"); } else throw new Error(`unknown input kind ${kind}`); @@ -428,7 +448,7 @@ function revision(root) { }); return r.stdout.trim() + (dirty.stdout.trim() ? "+dirty" : ""); } -function build(label, root, workload, layout, optimization) { +function build(label, root, workload, layout, optimization, targetCpu, cpuDispatch) { const cli = cliFor(root); const out = join(work, label, layout === 0 ? workload.name : `${workload.name}.layout${layout}`); mkdirSync(dirname(out), { recursive: true }); @@ -444,7 +464,16 @@ function build(label, root, workload, layout, optimization) { const entry = join(suiteRoot, workload.entry); const result = run( process.execPath, - [cli, "build", entry, `--optimization=${optimization}`, "-o", out], + [ + cli, + "build", + entry, + `--optimization=${optimization}`, + ...(targetCpu === undefined ? [] : [`--target-cpu=${targetCpu}`]), + ...(cpuDispatch ? ["--cpu-dispatch"] : []), + "-o", + out, + ], { env, timeout: 900_000, @@ -467,6 +496,8 @@ const contenders = [ label: "baseline", root: resolve(values.baseline), optimization: values["baseline-optimization"], + targetCpu: values["baseline-target-cpu"], + cpuDispatch: values["baseline-cpu-dispatch"], }, ] : []), @@ -474,6 +505,8 @@ const contenders = [ label: "candidate", root: resolve(values.candidate), optimization: values["candidate-optimization"], + targetCpu: values["candidate-target-cpu"], + cpuDispatch: values["candidate-cpu-dispatch"], }, ]; const report = { @@ -580,7 +613,7 @@ try { entry.results[c.label] = result; result.binaries = []; for (let layout = 0; layout < layouts && result.status === undefined; layout++) { - const built = build(c.label, c.root, w, layout, c.optimization); + const built = build(c.label, c.root, w, layout, c.optimization, c.targetCpu, c.cpuDispatch); result.build_ms += Math.round(built.buildMs); if (layout === 0) result.cold_build_ms = Math.round(built.buildMs); if (!built.ok) { diff --git a/scripts/bootstrap-seed.mjs b/scripts/bootstrap-seed.mjs index f0993f126..6bce075f5 100644 --- a/scripts/bootstrap-seed.mjs +++ b/scripts/bootstrap-seed.mjs @@ -25,6 +25,19 @@ export function verifyBootstrapSeed(directory, revision, sanitize) { } } +/** The environment a bootstrap seed builds with, over the inherited one. The + * bootstrap compares the seed's LLVM with the native compiler's own emission, + * which has no function layout. The layout only orders the functions, so the + * seed is built without it. */ +export function seedBuildEnvironment(sanitize) { + return { + SCRIPTC_SAN: sanitize ? "1" : "", + SCRIPTC_NATIVE_EMIT_IR: "1", + SCRIPTC_NATIVE_OPTIMIZATION: sanitize ? "dev" : "release", + SCRIPTC_NATIVE_LAYOUT: "", + }; +} + async function main() { const directory = process.env.SCRIPTC_BOOTSTRAP_SEED_DIRECTORY; const revision = process.env.GITHUB_SHA; @@ -47,11 +60,7 @@ async function main() { { cwd: root, stdio: "inherit", - env: { - ...process.env, - SCRIPTC_NATIVE_EMIT_IR: "1", - SCRIPTC_NATIVE_OPTIMIZATION: sanitize ? "dev" : "release", - }, + env: { ...process.env, ...seedBuildEnvironment(sanitize) }, }, ); writeFileSync( diff --git a/scripts/build-llvm-dist.mjs b/scripts/build-llvm-dist.mjs new file mode 100644 index 000000000..9398b46a4 --- /dev/null +++ b/scripts/build-llvm-dist.mjs @@ -0,0 +1,96 @@ +#!/usr/bin/env node +/* Builds the LLVM distribution the Linux release helpers link + * (native/llvm-codegen/llvm-dist.mjs): LLVM_VERSION from its release source + * tarball, with ThinLTO and native/llvm-codegen/llvm.profdata.gz, the + * helper's targets, and static libraries only. clang and lld of the same + * version must be on PATH (LLVM's release archive for the host has both). + * Writes the install tree and, with --archive, a .tar.xz of it and its + * .sha256. The llvm-dist workflow runs it for each Linux architecture; it + * takes about 8 minutes on 32 cores and about an hour on 4. + * + * With --instrumented it builds the IR-instrumented libraries that + * scripts/refresh-llvm-profile.mjs collects the profile with instead. + * + * node scripts/build-llvm-dist.mjs --source --prefix + * [--archive ] [--work ] [--jobs N] [--instrumented] + */ +import { spawnSync } from "node:child_process"; +import { createHash } from "node:crypto"; +import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs"; +import { cpus, tmpdir } from "node:os"; +import { basename, dirname, join, resolve } from "node:path"; +import { parseArgs } from "node:util"; +import { llvmCmakeArgs, writeLlvmProfile } from "../native/llvm-codegen/llvm-dist.mjs"; + +const { values } = parseArgs({ + options: { + source: { type: "string" }, + prefix: { type: "string" }, + archive: { type: "string" }, + work: { type: "string" }, + jobs: { type: "string", default: String(cpus().length) }, + instrumented: { type: "boolean", default: false }, + help: { type: "boolean", default: false }, + }, +}); +if (values.help || values.source === undefined || values.prefix === undefined) { + console.log( + "Usage: node scripts/build-llvm-dist.mjs --source --prefix [--archive ] [--work ] [--jobs N] [--instrumented]", + ); + process.exit(values.help ? 0 : 2); +} +if (process.platform !== "linux") throw new Error("the LLVM distribution is built on Linux"); +const source = resolve(values.source); +if (!existsSync(join(source, "llvm/CMakeLists.txt"))) + throw new Error(`${source} is not an llvm-project source tree`); +const prefix = resolve(values.prefix); +const work = resolve(values.work ?? mkdtempSync(join(tmpdir(), "scriptc-llvm-dist-"))); +const build = join(work, values.instrumented ? "llvm-instrumented" : "llvm"); +mkdirSync(build, { recursive: true }); + +function run(command, args, log, env = process.env) { + console.log(`$ ${command} ${args.join(" ")}`); + const result = spawnSync(command, args, { encoding: "utf8", maxBuffer: 1 << 30, env }); + if (log !== undefined) writeFileSync(log, result.stdout + result.stderr); + if (result.status !== 0) { + process.stderr.write((result.stdout + result.stderr).slice(-8000)); + throw new Error(`${command} failed with status ${result.status}`); + } + return result.stdout + result.stderr; +} + +const profile = values.instrumented ? undefined : writeLlvmProfile(work); +run("cmake", llvmCmakeArgs({ source, build, prefix, profile, instrumented: values.instrumented })); +const log = join(work, "build.log"); +const output = run("ninja", ["-C", build, "-j", values.jobs, "install"], log); + +if (profile !== undefined) { + // clang reports, per file, how many of its functions the profile no longer + // matches; on a host other than the one it was collected on, that count + // shows how much of the profile still applies. + let files = 0, + functions = 0, + mismatched = 0; + for (const match of output.matchAll(/of (\d+) functions?, (\d+) ha(?:ve|s) mismatched data/g)) { + files++; + functions += Number(match[1]); + mismatched += Number(match[2]); + } + console.log( + files === 0 + ? "profile: no file reports functions with mismatched profile data" + : `profile: ${mismatched} of ${functions} functions in ${files} files have mismatched profile data (build log ${log})`, + ); +} + +if (values.archive !== undefined) { + const archive = resolve(values.archive); + mkdirSync(dirname(archive), { recursive: true }); + run("tar", ["-C", dirname(prefix), "-cJf", archive, basename(prefix)], undefined, { + ...process.env, + XZ_OPT: "-T0", + }); + const digest = createHash("sha256").update(readFileSync(archive)).digest("hex"); + writeFileSync(`${archive}.sha256`, `${digest} ${basename(archive)}\n`); + console.log(`wrote ${archive} (${digest})`); +} diff --git a/scripts/build-native-cli.mts b/scripts/build-native-cli.mts index e539cea1d..3dbe22bf3 100644 --- a/scripts/build-native-cli.mts +++ b/scripts/build-native-cli.mts @@ -1,25 +1,41 @@ /** Build the production CLI seed and its relocatable compiler assets. */ -import { execFileSync } from "node:child_process"; -import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { execFileSync, spawnSync } from "node:child_process"; +import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; import { basename, dirname, join, relative, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { gunzipSync } from "node:zlib"; import { compile } from "../packages/compiler/src/index.js"; -import { NATIVE_TARGETS, nativeCodegenTarget } from "../packages/compiler/src/backend/targets.js"; +import { + NATIVE_TARGETS, + executableStripLinkerArgs, + nativeCodegenTarget, +} from "../packages/compiler/src/backend/targets.js"; import { tsgoExecutable } from "../packages/compiler/src/frontend/ts7/rpc-api.js"; +import { prebuiltLibDiagnosticsText } from "../packages/compiler/src/frontend/ts7/lib-diagnostics.js"; import { stageNativeCliAssets } from "./native-cli-assets.mjs"; import type { NativeToolchainManifest } from "../packages/compiler/src/native/toolchain.js"; const root = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const output = resolve(process.argv[2] ?? join(root, ".scriptc/native-cli")); +// --platform-packages builds an npm platform package's distribution, which +// depends on the helper and runtime pack packages instead of bundling them. +const platformPackages = process.argv.includes("--platform-packages"); const optimization = process.env["SCRIPTC_NATIVE_OPTIMIZATION"] === "dev" ? "dev" : "release"; const sanitize = process.env["SCRIPTC_SAN"] === "1"; // SCRIPTC_NATIVE_PROFILE_GENERATE= builds a compiler that writes a // raw profile per run (scripts/refresh-cli-profile.mjs). Optimized builds // otherwise use the committed profile, or the indexed profile that -// SCRIPTC_NATIVE_PROFILE_USE names; an empty SCRIPTC_NATIVE_PROFILE_USE -// builds without one. +// SCRIPTC_NATIVE_PROFILE_USE names (gzip-compressed when the name ends in +// .gz, as refresh-cli-profile.mjs writes it); an empty +// SCRIPTC_NATIVE_PROFILE_USE builds without one. const profileGenerate = process.env["SCRIPTC_NATIVE_PROFILE_GENERATE"] || undefined; +// Below this share of matching functions, the build warns that the profile +// has lost most of its gain. A profile refreshed on the same commit matched +// 92.7% of the native compiler's profiled functions, one refreshed 1.5 days +// and about 200 commits earlier 87.2%, and that one kept about half of the +// fresh profile's gain on a cold tsc-ts build. +const PROFILE_MATCH_WARNING = 0.9; const committedProfile = join(root, "packages/compiler/native/cli.profdata.gz"); const requestedProfile = process.env["SCRIPTC_NATIVE_PROFILE_USE"]; const useProfile = @@ -28,6 +44,15 @@ const useProfile = !sanitize && requestedProfile !== "" && (requestedProfile !== undefined || existsSync(committedProfile)); +// Builds lay out the compiler's functions with the committed layout +// (scripts/refresh-cli-layout.mjs), or the one SCRIPTC_NATIVE_LAYOUT names; +// an empty SCRIPTC_NATIVE_LAYOUT, and instrumented builds, use none. +const committedLayout = join(root, "packages/compiler/native/cli.layout.gz"); +const requestedLayout = process.env["SCRIPTC_NATIVE_LAYOUT"]; +const functionLayout = + profileGenerate !== undefined || requestedLayout === "" + ? undefined + : (requestedLayout ?? (existsSync(committedLayout) ? committedLayout : undefined)); let target = nativeCodegenTarget(); if (target === null || target.platform === "wasi") throw new Error("a supported native host is required to build the compiler"); @@ -45,10 +70,11 @@ const seed = join(dirname(output), ".scriptc", basename(output) + "-seed"); for (const dir of [bin, lib, seed]) mkdirSync(dir, { recursive: true }); let profileUse: string | undefined; if (useProfile) { - profileUse = requestedProfile; - if (profileUse === undefined) { + profileUse = requestedProfile ?? committedProfile; + if (profileUse.endsWith(".gz")) { + const compressed = profileUse; profileUse = join(seed, "cli.profdata"); - writeFileSync(profileUse, gunzipSync(readFileSync(committedProfile))); + writeFileSync(profileUse, gunzipSync(readFileSync(compressed))); } } const compiler = process.env["SCRIPTC_CC"] ?? target.defaultLinker; @@ -84,12 +110,14 @@ const ffi = join(seed, "compiler.ffi.json"); writeFileSync(ffi, JSON.stringify({ ffi_format: 6, functions, libraries }, null, 2) + "\n"); const executable = join(bin, "scriptc" + target.outputSuffixes.exe); const comptime = join(lib, "scriptc-comptime" + target.outputSuffixes.exe); +const strip = process.env["SCRIPTC_NATIVE_KEEP_SYMBOLS"] !== "1"; execFileSync( compiler, [ ...compilerArgs, ...cFlags, "-O2", + ...(strip ? ["-g0", ...executableStripLinkerArgs(target.platform, true)] : []), "-I", join(root, "packages/runtime/vendor/quickjs-ng"), join(nativeSources, "comptime.c"), @@ -106,16 +134,20 @@ execFileSync( ], { stdio: "inherit" }, ); +// The Windows link writes the debug database beside the executable. +if (strip && target.platform === "win32") + rmSync(comptime.replace(/\.exe$/, ".pdb"), { force: true }); const result = await compile(join(root, "packages/compiler/src/native/cli.ts"), { outDir: seed, outPath: executable, backend: "llvm", optimization, - strip: process.env["SCRIPTC_NATIVE_KEEP_SYMBOLS"] !== "1", + strip, ffiProfilePath: ffi, sanitize, ...(profileGenerate === undefined ? {} : { profileGenerate }), ...(profileUse === undefined ? {} : { profileUse }), + ...(functionLayout === undefined ? {} : { functionLayout }), emitIr: process.env["SCRIPTC_NATIVE_EMIT_IR"] === "1", }); if (!result.ok) @@ -127,6 +159,25 @@ if (!result.ok) ) .join("\n"), ); +// How well the profile still describes the compiler: a function whose +// control flow changed since the profile was collected is optimized as +// without one. The share falls as compiler code changes, and with it the +// profile's gain (refresh it with scripts/refresh-cli-profile.mjs). +const match = useProfile && "artifact" in result ? result.artifact.profileMatch : undefined; +if (match !== undefined) { + const profiled = match.annotated + match.mismatched; + const share = profiled === 0 ? 0 : match.annotated / profiled; + console.log( + `native compiler profile: ${match.annotated} of ${profiled} profiled functions match (${(100 * share).toFixed(1)}%); ${match.missing} functions have no profile` + + (match.unknownPartitions > 0 + ? `; ${match.unknownPartitions} partitions reused without counts` + : ""), + ); + if (match.unknownPartitions === 0 && share < PROFILE_MATCH_WARNING) + console.warn( + `warning: the native compiler profile matches ${(100 * share).toFixed(1)}% of the functions it describes; run scripts/refresh-cli-profile.mjs`, + ); +} const compilerVersion = ( JSON.parse(readFileSync(join(root, "packages/compiler/package.json"), "utf8")) as { version: string; @@ -138,6 +189,8 @@ const assets = stageNativeCliAssets({ target, ts7: tsgoExecutable(), compilerVersion, + strip, + platformPackages, }); // A cross-libc seed may use Zig on the build host. The installed compiler // selects the destination platform's ordinary linker and SDK. @@ -154,4 +207,60 @@ const manifest: NativeToolchainManifest = { comptime: relative(bin, comptime), }; writeFileSync(executable + ".json", JSON.stringify(manifest, null, 2) + "\n"); +// The prebuilt lib diagnostics (frontend/ts7/lib-diagnostics.ts): the +// declaration file sets the distribution's tsc-ts reports no diagnostics in +// for programs without a tsconfig, recorded by the built compiler itself. A +// compiler this host cannot run ships without them. +const prebuiltKeys = recordPrebuiltLibDiagnostics(executable); +if (prebuiltKeys === null) { + console.warn( + "warning: the built compiler did not run here; it ships without prebuilt lib diagnostics", + ); +} else { + const prebuilt = join(lib, "declarations", "lib-diagnostics.json"); + const checker = ( + JSON.parse(readFileSync(join(lib, "tsc-ts", "package.json"), "utf8")) as { version: string } + ).version; + writeFileSync(prebuilt, prebuiltLibDiagnosticsText(checker, prebuiltKeys)); + manifest.lib_diagnostics = relative(bin, prebuilt); + writeFileSync(executable + ".json", JSON.stringify(manifest, null, 2) + "\n"); +} console.log(executable); + +/** The content keys the built compiler records building, each in an empty + * cache, a module without imports (the program it lowers) and a module with + * a type error (which also checks the program without scriptc's overrides); + * null when it cannot run. */ +function recordPrebuiltLibDiagnostics(compiler: string): string[] | null { + const work = mkdtempSync(join(tmpdir(), "scriptc-lib-diagnostics-")); + try { + const record = join(work, "keys.txt"); + const probes = [ + { name: "clean", source: "export {};\n", status: 0 }, + { name: "error", source: 'export const n: number = "x";\n', status: 1 }, + ]; + for (const probe of probes) { + const dir = join(work, probe.name); + mkdirSync(dir); + writeFileSync(join(dir, "main.ts"), probe.source); + const env: NodeJS.ProcessEnv = { + ...process.env, + SCRIPTC_CACHE_DIR: join(dir, "cache"), + SCRIPTC_LIB_DIAGNOSTICS_RECORD: record, + }; + delete env["SCRIPTC_TIMING"]; + const run = spawnSync( + compiler, + ["build", join(dir, "main.ts"), "--optimization", "dev", "-o", join(dir, "main")], + { cwd: dir, env, encoding: "utf8" }, + ); + if (run.status !== probe.status) return null; + } + if (!existsSync(record)) return null; + return readFileSync(record, "utf8") + .split("\n") + .filter((key) => key !== ""); + } finally { + rmSync(work, { recursive: true, force: true }); + } +} diff --git a/scripts/check-llvm-dist.mjs b/scripts/check-llvm-dist.mjs new file mode 100644 index 000000000..cce490c9a --- /dev/null +++ b/scripts/check-llvm-dist.mjs @@ -0,0 +1,105 @@ +#!/usr/bin/env node +/* Compares two LLVM helpers on the helper inputs of this checkout + * (scripts/llvm-helper-inputs.mjs): every input must give identical objects + * with both, and the script prints each input group's median wall time per + * helper over the rounds, with the candidate/baseline ratio. The llvm-dist + * workflow runs it with the helper linked against LLVM's release archive as + * the baseline and the one linked against the profiled ThinLTO distribution + * as the candidate, on each architecture it builds. Exits with status 1 when + * an object differs. + * + * node scripts/check-llvm-dist.mjs --baseline --candidate + * [--rounds N] [--work ] [--tsc-ts ] [--extra ] + */ +import { spawnSync } from "node:child_process"; +import { mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; +import { performance } from "node:perf_hooks"; +import { parseArgs } from "node:util"; +import { captureHelperInputs, helperInputPrograms } from "./llvm-helper-inputs.mjs"; + +const { values } = parseArgs({ + options: { + baseline: { type: "string" }, + candidate: { type: "string" }, + rounds: { type: "string", default: "3" }, + work: { type: "string" }, + "tsc-ts": { type: "string" }, + extra: { type: "string", multiple: true, default: [] }, + help: { type: "boolean", default: false }, + }, +}); +if (values.help || values.baseline === undefined || values.candidate === undefined) { + console.log( + "Usage: node scripts/check-llvm-dist.mjs --baseline --candidate [--rounds N] [--work ] [--tsc-ts ] [--extra ]", + ); + process.exit(values.help ? 0 : 2); +} +const helpers = { baseline: resolve(values.baseline), candidate: resolve(values.candidate) }; +const work = resolve(values.work ?? mkdtempSync(join(tmpdir(), "scriptc-llvm-check-"))); +const inputs = await captureHelperInputs( + helperInputPrograms({ + tscTs: values["tsc-ts"] === undefined ? undefined : resolve(values["tsc-ts"]), + extra: values.extra.map((dir) => resolve(dir)), + }), + join(work, "inputs"), +); + +function emit(helper, input, out) { + rmSync(out, { recursive: true, force: true }); + mkdirSync(out, { recursive: true }); + const start = performance.now(); + const result = spawnSync(helper, input.args(out), { encoding: "utf8" }); + const ms = performance.now() - start; + if (result.status !== 0) + throw new Error(`${helper} failed on ${input.name}: ${result.stdout}${result.stderr}`); + return ms; +} + +const times = new Map(); // `${group}|${helper}` -> per-round totals +const group = (name) => (name === "tsc-ts" ? name : name.split("-")[0]); +const rounds = Number(values.rounds); +let differing = 0; +for (let round = 0; round < rounds; round++) { + const order = round % 2 === 0 ? ["baseline", "candidate"] : ["candidate", "baseline"]; + for (const input of inputs) { + for (const which of order) { + const ms = emit(helpers[which], input, join(work, which)); + const key = `${group(input.name)}|${which}`; + const totals = times.get(key) ?? new Array(rounds).fill(0); + totals[round] += ms; + times.set(key, totals); + } + if (round === 0) { + for (const file of readdirSync(join(work, "baseline"))) { + const a = readFileSync(join(work, "baseline", file)); + const b = readFileSync(join(work, "candidate", file)); + if (!a.equals(b)) { + differing++; + console.log(`${input.name}: ${file} differs between the helpers`); + } + } + } + } +} + +const median = (xs) => { + const s = [...xs].sort((x, y) => x - y); + return s.length % 2 ? s[s.length >> 1] : (s[s.length / 2 - 1] + s[s.length / 2]) / 2; +}; +console.log(`${inputs.length} inputs, ${rounds} rounds; median wall time per group`); +console.log("| group | baseline | candidate | candidate/baseline |"); +console.log("| --- | ---: | ---: | ---: |"); +for (const name of [...new Set(inputs.map((input) => group(input.name)))]) { + const b = median(times.get(`${name}|baseline`)), + c = median(times.get(`${name}|candidate`)); + console.log( + `| ${name} | ${(b / 1000).toFixed(2)} s | ${(c / 1000).toFixed(2)} s | ${(c / b).toFixed(3)} |`, + ); +} +if (differing > 0) { + console.log(`${differing} objects differ between the helpers`); + process.exit(1); +} +console.log("every object is identical between the helpers"); diff --git a/scripts/ci-test-costs.json b/scripts/ci-test-costs.json index 4c4964579..99d47e972 100644 --- a/scripts/ci-test-costs.json +++ b/scripts/ci-test-costs.json @@ -409,6 +409,7 @@ "tests/harness/stdlib-surface.test.ts": 48, "tests/harness/strip-vt-linear.test.ts": 1795, "tests/harness/surface-manifest.test.ts": 5309, + "tests/harness/teardown-segments.test.ts": 2600, "tests/harness/terminal-geometry.test.ts": 2730, "tests/harness/test262-runner.test.ts": 207, "tests/harness/three-library.test.ts": 251122, @@ -828,6 +829,7 @@ "tests/harness/stdlib-surface.test.ts": 46, "tests/harness/strip-vt-linear.test.ts": 2546, "tests/harness/surface-manifest.test.ts": 5951, + "tests/harness/teardown-segments.test.ts": 5, "tests/harness/terminal-geometry.test.ts": 3270, "tests/harness/test262-runner.test.ts": 230, "tests/harness/three-library.test.ts": 361470, diff --git a/scripts/ci-test-plan.mjs b/scripts/ci-test-plan.mjs index 2b76b52d6..88a77e32e 100644 --- a/scripts/ci-test-plan.mjs +++ b/scripts/ci-test-plan.mjs @@ -42,22 +42,38 @@ export const testCosts = JSON.parse( readFileSync(new URL("./ci-test-costs.json", import.meta.url), "utf8"), ); +// Side-queue files whose process tree, run alone, peaks far above a plain +// vitest run (about 1 GB): megabytes at the peak, measured with CI's settings. +// They run beside the corpus task's three workers, so no two of them share a +// shard while there are shards without one. +export const memoryHeavyFiles = { + "tests/harness/decl-boundary.test.ts": 1838, +}; + // The shard (1-based) of each measured side-queue file and isolated suite: // longest first to the shard with the least work so far, counting about a // second per file for vitest to import it. Ties go to the highest shard, so the // longest files start from the last shard while the cost-balanced corpus and // three.js cases (tests/harness/shard.ts) place their longest from the first. -// An isolated suite without a measurement weighs as much as the median -// measured file. Files without a measurement are left to vitest's --shard, -// which splits them by count. +// An isolated suite or memory-heavy file without a measurement weighs as much +// as the median measured file. Memory-heavy files are placed after the others, +// each on the shard with the least memory-heavy work so far and, among those, +// the least work. Files without a +// measurement are left to vitest's --shard, which splits them by count. export function sideOwners(flavor, count) { const costs = testCosts[flavor]; const known = Object.values(costs).sort((a, b) => a - b); const median = known.length === 0 ? 0 : known[Math.floor(known.length / 2)]; const weight = (file) => (costs[file] ?? median) + 1000; - const files = [...new Set([...Object.keys(costs), ...isolatedFiles.map(([, file]) => file)])]; + const heavy = Object.keys(memoryHeavyFiles).sort( + (a, b) => memoryHeavyFiles[b] - memoryHeavyFiles[a] || (a < b ? -1 : a > b ? 1 : 0), + ); + const files = [ + ...new Set([...Object.keys(costs), ...isolatedFiles.map(([, file]) => file)]), + ].filter((file) => !(file in memoryHeavyFiles)); files.sort((a, b) => weight(b) - weight(a) || (a < b ? -1 : a > b ? 1 : 0)); const loads = new Array(count).fill(0); + const memory = new Array(count).fill(0); const owners = new Map(); for (const file of files) { let lightest = count - 1; @@ -66,6 +82,18 @@ export function sideOwners(flavor, count) { loads[lightest] += weight(file); owners.set(file, lightest + 1); } + for (const file of heavy) { + let lightest = count - 1; + for (let shard = count - 2; shard >= 0; shard--) + if ( + memory[shard] < memory[lightest] || + (memory[shard] === memory[lightest] && loads[shard] < loads[lightest]) + ) + lightest = shard; + memory[lightest] += memoryHeavyFiles[file]; + loads[lightest] += weight(file); + owners.set(file, lightest + 1); + } return owners; } @@ -106,6 +134,15 @@ export const leakSuppressions = fileURLToPath( new URL("../tests/harness/lsan.supp", import.meta.url), ); +// The sanitized three.js nodes build runs in its own Linux job +// (three_nodes_san in .github/workflows/ci.yml). Beside the corpus's other +// workers it took 860 to more than 1200 seconds, past its test timeout. +export const sanitizedNodesJobEnv = { SCRIPTC_THREE_SANITIZED_NODES_JOB: "1" }; + +function corpusThreeEnv(flavor, platform) { + return flavor === "san" && platform === "linux" ? sanitizedNodesJobEnv : {}; +} + function corpusLeakEnv(flavor, platform) { if (flavor !== "san" || platform !== "linux") return {}; return { @@ -218,6 +255,7 @@ export function ciTestPlan({ env: { ...env, ...corpusLeakEnv(flavor, platform), + ...corpusThreeEnv(flavor, platform), SCRIPTC_TEST_WORKERS: String(workers - sideWorkers), SCRIPTC_LLVM_TEST_MODE: "dev", }, diff --git a/scripts/generate-ir-fields.mjs b/scripts/generate-ir-fields.mjs new file mode 100644 index 000000000..5b5ad011a --- /dev/null +++ b/scripts/generate-ir-fields.mjs @@ -0,0 +1,215 @@ +import { readFileSync, writeFileSync } from "node:fs"; +import { createRequire } from "node:module"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; + +// Generates packages/compiler/src/ir/fields.generated.ts from the node types +// in ir.ts: for each IrExpr and IrStmt kind, the fields that can hold a +// string, read by name. Code that would enumerate a node's keys reads them +// through this switch instead: the native compiler enumerates and reads an +// untyped object's keys through its dynamic representation, one conversion +// per node. `--check` fails when the file is stale. +const root = fileURLToPath(new URL("..", import.meta.url)); +const require = createRequire(join(root, "package.json")); +const ts = require("typescript"); +const irPath = join(root, "packages/compiler/src/ir/ir.ts"); +const target = join(root, "packages/compiler/src/ir/fields.generated.ts"); + +const program = ts.createProgram([irPath], { strict: true, target: ts.ScriptTarget.ES2022 }); +const checker = program.getTypeChecker(); +const source = program.getSourceFile(irPath); +const aliases = new Map(); +ts.forEachChild(source, (node) => { + if (ts.isTypeAliasDeclaration(node)) aliases.set(node.name.text, node); +}); + +/** Whether a value of `type` can be a string. */ +function holdsString(type) { + if (type.isUnion()) return type.types.some(holdsString); + return (type.flags & (ts.TypeFlags.StringLike | ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0; +} + +/** Whether every value of `type` is a string. */ +function onlyString(type) { + if (type.isUnion()) return type.types.every(onlyString); + return (type.flags & ts.TypeFlags.StringLike) !== 0; +} + +/** Each kind's string fields, in declaration order, merged over the unions: + * a kind both unions declare lists the fields of both. */ +const kinds = new Map(); +for (const name of ["IrExpr", "IrStmt"]) { + const alias = aliases.get(name); + const union = checker.getTypeAtLocation(alias.name); + for (const member of union.isUnion() ? union.types : [union]) { + const kindSymbol = member.getProperty("kind"); + const kindType = checker.getTypeOfSymbolAtLocation(kindSymbol, alias); + if (!kindType.isStringLiteral()) throw new Error(`${name} member without a literal kind`); + const fields = kinds.get(kindType.value) ?? []; + for (const property of member.getProperties()) { + const type = checker.getTypeOfSymbolAtLocation(property, alias); + if (!holdsString(type)) continue; + const known = fields.find((field) => field.name === property.name); + const always = onlyString(type) && (property.flags & ts.SymbolFlags.Optional) === 0; + if (known === undefined) fields.push({ name: property.name, always }); + else known.always &&= always; + } + kinds.set(kindType.value, fields); + } +} + +const lines = [ + "// Generated from packages/compiler/src/ir/ir.ts by scripts/generate-ir-fields.mjs.", + "// Run `pnpm generate:ir-fields` after changing the fields of an IR node.", + 'import type { IrExpr, IrStmt, IrType } from "./ir.js";', + "", + "/** Calls `visit` with each string field of `node` (including `kind`) and its", + " * name: the string values `for (const key in node)` finds on a node, read", + " * by name in the order ir.ts declares them. */", + "export function eachStringField(", + " node: IrExpr | IrStmt,", + " visit: (value: string, field: string) => void,", + "): void {", + " switch (node.kind) {", +]; +for (const [kind, fields] of [...kinds].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))) { + lines.push(` case ${JSON.stringify(kind)}: {`); + for (const { name, always } of fields) { + const read = `node.${name}`; + if (always) lines.push(` visit(${read}, ${JSON.stringify(name)});`); + else + lines.push(` if (typeof ${read} === "string") visit(${read}, ${JSON.stringify(name)});`); + } + lines.push(" return;", " }"); +} +lines.push(" }", "}", ""); + +// The deep walkers: every field of every object under an IR node, as a +// recursive key walk finds them, read by name. +const members = new Map(); +for (const name of ["IrExpr", "IrStmt", "IrType"]) { + const union = checker.getTypeAtLocation(aliases.get(name).name); + members.set(name, new Set(union.isUnion() ? union.types : [union])); +} +const nullish = ts.TypeFlags.Null | ts.TypeFlags.Undefined | ts.TypeFlags.Void; +const scalar = + ts.TypeFlags.NumberLike | + ts.TypeFlags.BooleanLike | + ts.TypeFlags.BigIntLike | + ts.TypeFlags.ESSymbolLike; + +/** The walker an IR union member belongs to, or null. */ +function irKind(type) { + for (const [name, set] of members) if (set.has(type)) return name; + return null; +} + +let temp = 0; +/** Lines that walk `expr` of `type`: strings to `walker.string`, IR nodes to + * their walkers, arrays by element and other objects by field. */ +function walkLines(expr, type, indent, depth) { + if (depth > 12) throw new Error(`field type nests too deep at ${expr}`); + const parts = type.isUnion() ? type.types : [type]; + const present = parts.filter((part) => (part.flags & nullish) === 0); + const optional = present.length !== parts.length; + const strings = present.filter((part) => (part.flags & ts.TypeFlags.StringLike) !== 0); + const objects = present.filter((part) => (part.flags & (ts.TypeFlags.StringLike | scalar)) === 0); + for (const part of present) + if ((part.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0) + throw new Error(`untyped field ${expr}`); + const body = []; + if (objects.length > 0) { + const kinds = new Set(objects.map(irKind)); + if (kinds.size === 1 && !kinds.has(null)) { + const [kind] = kinds; + body.push(`walk${kind.slice(2)}Fields(${expr}, walker, skipType);`); + } else if (objects.every((part) => checker.isArrayType(part))) { + if (objects.length !== 1) throw new Error(`several array types at ${expr}`); + const item = `item${temp++}`; + body.push(`for (const ${item} of ${expr}) {`); + body.push(...walkLines(item, checker.getTypeArguments(objects[0])[0], " ", depth + 1)); + body.push("}"); + } else if (kinds.has(null) && objects.length === 1) { + for (const property of checker.getPropertiesOfType(objects[0])) { + if (property.name === "loc") continue; + const fieldType = checker.getTypeOfSymbol(property); + const field = `${expr}.${property.name}`; + const lines = walkLines(field, fieldType, "", depth + 1); + if (lines.length === 0) continue; + if (property.name === "type") + body.push("if (!skipType) {", ...lines.map((l) => ` ${l}`), "}"); + else body.push(...lines); + } + } else throw new Error(`mixed object types at ${expr}: ${checker.typeToString(type)}`); + } + let lines = body; + if (strings.length > 0 && objects.length > 0) + lines = [ + `if (typeof ${expr} === "string") walker.string(${expr});`, + ...(body.length > 0 ? [`else {`, ...body.map((l) => ` ${l}`), "}"] : []), + ]; + else if (strings.length > 0) lines = [`walker.string(${expr});`]; + if (lines.length > 0 && optional && !(strings.length > 0 && objects.length > 0)) + lines = [ + `if (${expr} !== undefined && ${expr} !== null) {`, + ...lines.map((l) => ` ${l}`), + "}", + ]; + else if (lines.length > 0 && optional) + lines = [ + `if (${expr} !== undefined && ${expr} !== null) {`, + ...lines.map((l) => ` ${l}`), + "}", + ]; + return lines.map((line) => indent + line); +} + +lines.push( + "/** What a field walk reports (walkExprFields): each expression and", + " * statement node before its fields, and each string a field holds. */", + "export interface IrFieldWalker {", + " node(node: IrExpr | IrStmt): void;", + " string(value: string): void;", + "}", + "", +); +for (const name of ["IrExpr", "IrStmt", "IrType"]) { + const short = name.slice(2); + const nodeCall = name === "IrType" ? [] : [" walker.node(node);"]; + lines.push( + `/** Walks every field of \`node\` but \`loc\` (and \`type\` with \`skipType\`), and`, + " * every field of each object under it: the strings and nodes a recursive", + " * `for (const key in value)` walk with the same skips finds, read by name in", + " * the order ir.ts declares the fields. */", + `export function walk${short}Fields(node: ${name}, walker: IrFieldWalker, skipType: boolean): void {`, + ...nodeCall, + " switch (node.kind) {", + ); + const byKind = new Map(); + for (const member of members.get(name)) { + const kind = checker.getTypeOfSymbol(member.getProperty("kind")).value; + if (byKind.has(kind)) throw new Error(`${name} kind ${kind} declared twice`); + byKind.set(kind, member); + } + for (const [kind, member] of [...byKind].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))) { + lines.push(` case ${JSON.stringify(kind)}: {`); + for (const property of member.getProperties()) { + if (property.name === "loc") continue; + const fieldType = checker.getTypeOfSymbol(property); + const field = `node.${property.name}`; + const walk = walkLines(field, fieldType, "", 0); + if (walk.length === 0) continue; + if (property.name === "type") + lines.push(" if (!skipType) {", ...walk.map((l) => ` ${l}`), " }"); + else lines.push(...walk.map((l) => ` ${l}`)); + } + lines.push(" return;", " }"); + } + lines.push(" }", "}", ""); +} +const text = lines.join("\n"); + +if (process.argv.includes("--check")) { + if (readFileSync(target, "utf8") !== text) + throw new Error("fields.generated.ts is stale; run node scripts/generate-ir-fields.mjs"); +} else writeFileSync(target, text); diff --git a/scripts/llvm-helper-inputs.mjs b/scripts/llvm-helper-inputs.mjs new file mode 100644 index 000000000..160d7b50e --- /dev/null +++ b/scripts/llvm-helper-inputs.mjs @@ -0,0 +1,114 @@ +/* Inputs of the LLVM helper, captured once and replayed with any helper: + * scripts/refresh-llvm-profile.mjs trains the helper's profile on them and + * scripts/check-llvm-dist.mjs compares helpers on them. Each input is a + * program's LLVM text from `scriptc build --emit llvm` and the `emit` + * arguments the compiler passes for an executable build of it + * (native-codegen.ts), partitions included. The partition cache is left out, + * so every replay compiles every partition. */ +import { spawnSync } from "node:child_process"; +import { existsSync, mkdirSync, readFileSync, readdirSync, statSync } from "node:fs"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = fileURLToPath(new URL("..", import.meta.url)); +const EFFECT_FIXTURES = [ + "async-sleep", + "async-promise-typed", + "async-timeout", + "async-resume-timer", + "async-yield", +]; + +/** The programs: tsc-ts (when a checkout is given), five Effect fixtures, + * the runtime benchmark workloads, every 50th numbered corpus program, and + * `extra` directories of standalone programs. */ +export function helperInputPrograms({ tscTs, extra = [] } = {}) { + const programs = []; + if (tscTs !== undefined) + programs.push({ name: "tsc-ts", cwd: join(tscTs, "packages/tsc-ts"), entry: "src/main.ts" }); + for (const fixture of EFFECT_FIXTURES) + programs.push({ + name: `effect-${fixture}`, + cwd: root, + entry: `tests/fixtures/effect/${fixture}.ts`, + args: ["--npm-static", "effect,fast-check,pure-rand"], + }); + const suite = join(root, "benchmarks/runtime"); + for (const file of readdirSync(join(suite, "workloads")) + .filter((f) => f.endsWith(".json")) + .sort()) { + const { entry } = JSON.parse(readFileSync(join(suite, "workloads", file), "utf8")); + // Variants of a workload share its entry and differ only in arguments. + if (!programs.some((program) => program.cwd === suite && program.entry === entry)) + programs.push({ name: `wl-${file.slice(0, -5)}`, cwd: suite, entry }); + } + const corpus = join(root, "tests/corpus"); + for (const file of readdirSync(corpus).sort()) { + const number = /^(\d+)-.*\.ts$/.exec(file)?.[1]; + if (number !== undefined && Number(number) % 50 === 0) + programs.push({ name: `corpus-${file.slice(0, -3)}`, cwd: corpus, entry: file }); + } + for (const dir of extra) + for (const file of readdirSync(dir) + .filter((f) => f.endsWith(".ts")) + .sort()) + programs.push({ name: `extra-${file.slice(0, -3)}`, cwd: dir, entry: file }); + return programs; +} + +/** Writes `/.ll` for each program with this checkout's compiler + * and returns the inputs that compiled: `{ name, input, outputs, args(out) }`, + * where `args(out)` gives the helper's arguments writing into `out`. */ +export async function captureHelperInputs(programs, dir) { + const { nativeProgramPartitions } = await import( + join(root, "packages/compiler/dist/backend/native-codegen-core.js") + ); + const cli = join(root, "packages/cli/dist/main.js"); + const target = + process.arch === "arm64" ? "aarch64-unknown-linux-gnu" : "x86_64-unknown-linux-gnu"; + mkdirSync(dir, { recursive: true }); + const inputs = []; + for (const program of programs) { + const input = join(dir, `${program.name}.ll`); + if (!existsSync(input)) { + const built = spawnSync( + process.execPath, + [cli, "build", program.entry, "--emit", "llvm", "-o", input, ...(program.args ?? [])], + { cwd: program.cwd, encoding: "utf8" }, + ); + if (built.status !== 0) { + console.log(`skipped ${program.name}: the compiler refused it`); + continue; + } + } + const partitions = nativeProgramPartitions(statSync(input).size); + const source = join(program.cwd, program.entry); + inputs.push({ + name: program.name, + input, + partitions, + args: (out) => [ + "emit", + "--input", + input, + ...Array.from({ length: partitions }, (_, i) => [ + "--output", + join(out, `${program.name}.part${i}.o`), + ]).flat(), + "--filetype", + "obj", + "--target", + target, + "--opt-level", + "2", + "--relocation-model", + "pic", + "--diagnostic-format", + "json", + "--source-path", + source, + ], + }); + } + return inputs; +} diff --git a/scripts/lowering-order.mjs b/scripts/lowering-order.mjs index 81611e6a4..6d68530cb 100644 --- a/scripts/lowering-order.mjs +++ b/scripts/lowering-order.mjs @@ -16,13 +16,16 @@ * [--seeds 1,2,3] [--jobs 8] [--entry ]... [--out ] * [--fail-on names|content] * + * --stride and --offset sample the top-level corpus programs by number, or by + * a hash of the name of a program without one. + * * Needs a built compiler (pnpm build). Writes /results.json and prints * a ranking of fields by the programs whose IR they change. With --fail-on, * it exits with status 1 when some program's IR differs between orders: * in any way (names), or beyond names and ids (content). */ import { spawn } from "node:child_process"; import { createHash } from "node:crypto"; -import { mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs"; +import { mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs"; import { cpus, tmpdir } from "node:os"; import { basename, join, resolve } from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; @@ -306,12 +309,25 @@ if (values["fail-on"] !== undefined && !["names", "content"].includes(values["fa const out = resolve(values.out); mkdirSync(out, { recursive: true }); const seeds = values.seeds.split(",").filter(Boolean); +/** A top-level corpus program's sampling key: its number, or, for a program + * named without one, a stable hash of its name (FNV-1a). */ +const programKey = (name) => + /^\d+-/.test(name) + ? Number.parseInt(name, 10) + : [...name].reduce((h, c) => Math.imul(h ^ c.charCodeAt(0), 16777619) >>> 0, 2166136261); +/** Numbered programs by number, then the named ones by name. */ +const programOrder = (a, b) => + /^\d+-/.test(a) !== /^\d+-/.test(b) + ? /^\d+-/.test(a) + ? -1 + : 1 + : programKey(a) - programKey(b) || (a < b ? -1 : a > b ? 1 : 0); const corpus = join(root, "tests/corpus"); const programs = [ ...readdirSync(corpus) - .filter((name) => /^\d+-.*\.ts$/.test(name)) - .filter((name) => Number.parseInt(name, 10) % Number(values.stride) === Number(values.offset)) - .sort((a, b) => Number.parseInt(a, 10) - Number.parseInt(b, 10)) + .filter((name) => /^[\w-]+\.ts$/.test(name) && statSync(join(corpus, name)).isFile()) + .filter((name) => programKey(name) % Number(values.stride) === Number(values.offset)) + .sort(programOrder) .slice(0, Number(values.limit)) .map((name) => join(corpus, name)), ...values.entry.map((entry) => resolve(entry)), diff --git a/scripts/native-cli-assets.mts b/scripts/native-cli-assets.mts index bdecc1da2..21734e938 100644 --- a/scripts/native-cli-assets.mts +++ b/scripts/native-cli-assets.mts @@ -1,25 +1,46 @@ +import { spawnSync } from "node:child_process"; import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs"; import { basename, dirname, join, relative } from "node:path"; import type { NativeTargetSpec } from "../packages/compiler/src/backend/targets.js"; +import { copyPackage, llvmNotice } from "./native-cli-bundle.mjs"; import type { NativeToolchainManifest } from "../packages/compiler/src/native/toolchain.js"; import { tscTsExecutable, tscTsPlatformPackage, } from "../packages/compiler/src/frontend/ts7/checker-backend.js"; -/** Copy release payloads explicitly; development caches and build trees never ship. */ -function copyPackage(source: string, destination: string, members: readonly string[]): void { - mkdirSync(destination, { recursive: true }); - for (const member of ["package.json", ...members]) { - cpSync(join(source, member), join(destination, member), { - recursive: true, - filter: (path) => ![".cache", "node_modules", ".DS_Store"].includes(basename(path)), - }); +/** Removes the symbol table and debug sections of a copied executable. + * llvm-strip rewrites the linker-signed ad hoc code signature of a Mach-O + * executable, which arm64 macOS requires to run it. Windows executables keep + * their debug information in separate .pdb files, which are not copied. + * Without llvm-strip on PATH the executable keeps its symbols; the release + * hosts install it with LLVM. */ +function stripExecutable(path: string, platform: string): void { + if (platform === "win32") return; + const result = spawnSync("llvm-strip", [path], { stdio: "inherit" }); + if ((result.error as NodeJS.ErrnoException | undefined)?.code === "ENOENT") { + console.warn(`llvm-strip is not on PATH; ${basename(path)} keeps its symbols`); + return; } - for (const name of readdirSync(source)) { - if (/^(?:LICENSE|COPYING|NOTICE|SCRIPTC_LICENSE|THIRD_PARTY_NOTICES)(?:\..*)?$/.test(name)) { - cpSync(join(source, name), join(destination, name)); + if (result.error !== undefined) throw result.error; + if (result.status !== 0) throw new Error(`llvm-strip ${path} exited with ${result.status}`); +} + +/** Rewrites CRLF line endings to LF in the text files under `directory`. A + * Windows checkout converts the repository's LF text files to CRLF; this + * restores the files the other hosts ship. Text is what Git's conversion + * treats as text: no NUL byte in the first 8000 bytes. */ +function normalizeLineEndings(directory: string): void { + for (const entry of readdirSync(directory, { withFileTypes: true })) { + const path = join(directory, entry.name); + if (entry.isDirectory()) { + normalizeLineEndings(path); + continue; } + if (!entry.isFile()) continue; + const bytes = readFileSync(path); + if (bytes.subarray(0, 8000).includes(0) || !bytes.includes("\r\n")) continue; + writeFileSync(path, bytes.toString("latin1").replaceAll("\r\n", "\n"), "latin1"); } } @@ -29,6 +50,12 @@ export function stageNativeCliAssets(options: { target: NativeTargetSpec; ts7: string; compilerVersion: string; + /** Strip the copied tsc-ts executable. */ + strip: boolean; + /** Leave the LLVM helper and host runtime pack out: an npm platform + * package depends on their packages, and the compiler resolves them + * beside itself. */ + platformPackages: boolean; }): Pick< NativeToolchainManifest, | "ts7" @@ -47,16 +74,18 @@ export function stageNativeCliAssets(options: { cpSync(join(repository, "LICENSE"), join(output, "LICENSE")); const helper = join(lib, "llvm"); const runtime = join(lib, target.runtimePackPackage.replace("@scriptc/", "")); - copyPackage( - join(repository, "packages", target.helper.packageName.replace("@scriptc/", "")), - helper, - ["bin"], - ); - copyPackage( - join(repository, "packages", target.runtimePackPackage.replace("@scriptc/", "")), - runtime, - ["artifacts", "runtime-pack.json"], - ); + if (!options.platformPackages) { + copyPackage( + join(repository, "packages", target.helper.packageName.replace("@scriptc/", "")), + helper, + ["bin"], + ); + copyPackage( + join(repository, "packages", target.runtimePackPackage.replace("@scriptc/", "")), + runtime, + ["artifacts", "runtime-pack.json"], + ); + } const typescript = join(lib, "typescript"); copyPackage(dirname(dirname(options.ts7)), typescript, ["lib"]); // tsc-ts, the default checker, from the tsc-ts package the compiler depends on. @@ -67,17 +96,25 @@ export function stageNativeCliAssets(options: { ); const tscTs = join(lib, "tsc-ts"); copyPackage(dirname(dirname(tscTsSource)), tscTs, ["lib"]); + if (options.strip) stripExecutable(join(tscTs, "lib", basename(tscTsSource)), target.platform); const sources = join(lib, "runtime-sources"); copyPackage(join(repository, "packages/runtime"), sources, ["src", "vendor"]); + normalizeLineEndings(sources); const declarations = join(lib, "declarations"); mkdirSync(declarations, { recursive: true }); for (const name of ["scriptc.d.ts", "scriptc-overrides.d.ts", "scriptc-node-fallback.d.ts"]) { cpSync(join(repository, "packages/compiler/ambient", name), join(declarations, name)); } + cpSync( + join(repository, "packages/compiler/ambient/node-fallback"), + join(declarations, "node-fallback"), + { recursive: true }, + ); // Package shims (packageShimRoot): the built-in npm package implementations. cpSync(join(repository, "packages/compiler/shims"), join(declarations, "shims"), { recursive: true, }); + normalizeLineEndings(declarations); // This optional Node host runs the built WASI module, never the compiler. const wasi = join(lib, "wasi"); mkdirSync(join(wasi, "cli"), { recursive: true }); @@ -100,7 +137,7 @@ export function stageNativeCliAssets(options: { `scriptc ${options.compilerVersion} includes the existing TypeScript ${identity.version}, tsc-ts ${tscTsIdentity.version}, LLVM, and C runtime dependencies.`, "TypeScript licensing: lib/typescript/LICENSE and lib/typescript/NOTICE.txt.", "tsc-ts licensing and notices: lib/tsc-ts/LICENSE and lib/tsc-ts/THIRD_PARTY_NOTICES.md.", - "LLVM licensing and notices: lib/llvm/LICENSE and lib/llvm/THIRD_PARTY_NOTICES.", + llvmNotice(options.platformPackages ? target.helper.packageName : undefined), "Runtime dependency licenses: lib/runtime-sources/vendor/*/LICENSE*.", "", ].join("\n"), @@ -108,9 +145,13 @@ export function stageNativeCliAssets(options: { return { ts7: relative(bin, join(typescript, "lib", basename(options.ts7))), tsc_ts: relative(bin, join(tscTs, "lib", basename(tscTsSource))), - llvm_package: relative(bin, helper), - runtime_pack: relative(bin, runtime), - runtime_packs: [{ target: target.name, path: relative(bin, runtime) }], + ...(options.platformPackages + ? {} + : { + llvm_package: relative(bin, helper), + runtime_pack: relative(bin, runtime), + runtime_packs: [{ target: target.name, path: relative(bin, runtime) }], + }), runtime_sources: relative(bin, sources), declarations: relative(bin, declarations), wasi_node_runner: relative(bin, join(wasi, "cli/wasi-runner.js")), diff --git a/scripts/native-cli-bundle.mjs b/scripts/native-cli-bundle.mjs new file mode 100644 index 000000000..62762aadd --- /dev/null +++ b/scripts/native-cli-bundle.mjs @@ -0,0 +1,72 @@ +import { cpSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs"; +import { basename, dirname, join, resolve } from "node:path"; + +/** Copy release payloads explicitly; development caches and build trees never ship. */ +export function copyPackage(source, destination, members) { + mkdirSync(destination, { recursive: true }); + for (const member of ["package.json", ...members]) { + cpSync(join(source, member), join(destination, member), { + recursive: true, + filter: (path) => ![".cache", "node_modules", ".DS_Store"].includes(basename(path)), + }); + } + for (const name of readdirSync(source)) { + if (/^(?:LICENSE|COPYING|NOTICE|SCRIPTC_LICENSE|THIRD_PARTY_NOTICES)(?:\..*)?$/.test(name)) { + cpSync(join(source, name), join(destination, name)); + } + } +} + +/** The LLVM helper and host runtime pack packages a native CLI package + * depends on. */ +export function platformPackagesOf(identity) { + const names = Object.keys(identity.dependencies ?? {}); + const helper = names.find((name) => name.startsWith("@scriptc/llvm-")); + const runtime = names.find((name) => name.startsWith("@scriptc/runtime-")); + if (helper === undefined || runtime === undefined) + throw new Error(`${identity.name} must depend on its LLVM helper and runtime pack packages`); + return { helper, runtime }; +} + +/** Copies the helper and runtime pack packages from `packages` (the + * workspace's packages directory) into a distribution's lib directory, as a + * standalone archive needs them, and returns the manifest paths, relative to + * the distribution's bin directory in the target's path style. */ +export function bundlePlatformPackages({ packages, distribution, helper, runtime, windows }) { + const short = (name) => name.replace("@scriptc/", ""); + copyPackage(join(packages, short(helper)), join(distribution, "lib", "llvm"), ["bin"]); + copyPackage(join(packages, short(runtime)), join(distribution, "lib", short(runtime)), [ + "artifacts", + "runtime-pack.json", + ]); + const path = (name) => ["..", "lib", name].join(windows ? "\\" : "/"); + return { llvm_package: path("llvm"), runtime_pack: path(short(runtime)) }; +} + +/** The THIRD_PARTY_NOTICES line for the LLVM helper, bundled or as a dependency. */ +export function llvmNotice(helper) { + return helper === undefined + ? "LLVM licensing and notices: lib/llvm/LICENSE and lib/llvm/THIRD_PARTY_NOTICES." + : `LLVM licensing and notices: LICENSE and THIRD_PARTY_NOTICES in the ${helper} package.`; +} + +/** Turns a distribution that bundles its LLVM helper and runtime pack into + * an npm platform package's: removes both from lib and from the manifest at + * `manifestPath`, as `build-native-cli.mts --platform-packages` builds it. */ +export function unbundlePlatformPackages(distribution, manifestPath, helper) { + const manifest = JSON.parse(readFileSync(manifestPath, "utf8")); + for (const path of [manifest.llvm_package, manifest.runtime_pack]) + rmSync(resolve(dirname(manifestPath), path.replaceAll("\\", "/")), { + recursive: true, + force: true, + }); + delete manifest.llvm_package; + delete manifest.runtime_pack; + delete manifest.runtime_packs; + writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n"); + const notices = join(distribution, "THIRD_PARTY_NOTICES"); + writeFileSync( + notices, + readFileSync(notices, "utf8").replace(llvmNotice(undefined), llvmNotice(helper)), + ); +} diff --git a/scripts/package-native-cli.mjs b/scripts/package-native-cli.mjs index 6d1c05991..9b8ea5880 100644 --- a/scripts/package-native-cli.mjs +++ b/scripts/package-native-cli.mjs @@ -13,6 +13,7 @@ import { import { tmpdir } from "node:os"; import { basename, dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; +import { bundlePlatformPackages, llvmNotice, platformPackagesOf } from "./native-cli-bundle.mjs"; const repository = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const packages = resolve(process.argv[2] ?? join(repository, "packages")); @@ -39,6 +40,27 @@ for (const name of readdirSync(packages) const binary = join(bin, name.includes("win32-") ? "scriptc.exe" : "scriptc"); const manifest = JSON.parse(readFileSync(binary + ".json", "utf8")); const hostPath = (value) => (name.includes("win32-") ? value.replaceAll("\\", "/") : value); + // The standalone archive bundles the LLVM helper and runtime pack that + // the npm platform package depends on. + if (manifest.llvm_package === undefined) { + const { helper, runtime } = platformPackagesOf(identity); + const paths = bundlePlatformPackages({ + packages, + distribution, + helper, + runtime, + windows: name.includes("win32-"), + }); + manifest.llvm_package = paths.llvm_package; + manifest.runtime_pack = paths.runtime_pack; + manifest.runtime_packs = [{ target: manifest.target, path: paths.runtime_pack }]; + writeFileSync(binary + ".json", JSON.stringify(manifest, null, 2) + "\n"); + const notices = join(distribution, "THIRD_PARTY_NOTICES"); + writeFileSync( + notices, + readFileSync(notices, "utf8").replace(llvmNotice(helper), llvmNotice(undefined)), + ); + } // GitHub artifact transport drops executable modes. Restore the native // tools before validating and creating the standalone archive. for (const path of [ diff --git a/scripts/refresh-cli-layout.mjs b/scripts/refresh-cli-layout.mjs new file mode 100644 index 000000000..cb396015f --- /dev/null +++ b/scripts/refresh-cli-layout.mjs @@ -0,0 +1,275 @@ +#!/usr/bin/env node +/* Refreshes packages/compiler/native/cli.layout.gz, the function layout that + * builds of the native compiler use (scripts/build-native-cli.mts). The + * layout lists the functions the compiler runs, in tiers: tier 1 for + * `--version` and rebuilds of unchanged programs, then the functions of the + * other builds by the share of training builds that run them (COLD_TIERS). + * The emitter writes the listed functions first, by tier, so the code a build + * runs sits together in the compiler's executable and a run maps fewer of its + * pages: a function nearly every build runs sits with the others that do, not + * among functions only a few programs need. + * + * Builds an instrumented native compiler for this Linux host (or reuses one + * with --instrumented), builds each training program cold and then again + * unchanged, and reads the functions each kind of run executed from the raw + * profiles. The training programs are the runtime benchmark workloads at + * `speed` and every seventh corpus program at the default optimization. A + * refresh takes about 15 minutes on 16 cores, most of it for the instrumented + * build. A stale layout lists some functions that no longer exist and misses + * new ones, which then keep their usual order. + * + * node scripts/refresh-cli-layout.mjs [--jobs 8] [--instrumented ] + */ +import { spawn, spawnSync } from "node:child_process"; +import { createHash } from "node:crypto"; +import { + appendFileSync, + mkdirSync, + mkdtempSync, + readFileSync, + readdirSync, + rmSync, + statSync, + writeFileSync, +} from "node:fs"; +import { cpus, tmpdir } from "node:os"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { parseArgs } from "node:util"; +import { constants, gzipSync, inflateSync } from "node:zlib"; + +const root = fileURLToPath(new URL("..", import.meta.url)); +const { values } = parseArgs({ + options: { + jobs: { type: "string", default: String(Math.max(1, Math.floor(cpus().length / 2))) }, + output: { type: "string", default: join(root, "packages/compiler/native/cli.layout.gz") }, + work: { type: "string" }, + instrumented: { type: "string" }, + help: { type: "boolean", default: false }, + }, +}); +if (values.help) { + console.log( + "Usage: node scripts/refresh-cli-layout.mjs [--jobs N] [--output ] [--work ] [--instrumented ]", + ); + process.exit(0); +} +if (process.platform !== "linux" || process.arch !== "x64") + throw new Error("the compiler layout is collected on Linux x64"); +const jobs = Number(values.jobs); +const output = resolve(values.output); +mkdirSync(dirname(output), { recursive: true }); +const work = resolve(values.work ?? mkdtempSync(join(tmpdir(), "scriptc-cli-layout-"))); +const raw = join(work, "raw"); +const builds = join(work, "builds"); +rmSync(raw, { recursive: true, force: true }); +for (const dir of [raw, builds]) mkdirSync(dir, { recursive: true }); +const log = join(work, "training.log"); + +const cli = resolve(values.instrumented ?? join(work, "cli")); +if (values.instrumented === undefined) { + console.log(`building the instrumented compiler in ${cli}`); + const built = spawnSync( + process.execPath, + [ + "--max-old-space-size=8192", + "--import", + "tsx", + join(root, "scripts/build-native-cli.mts"), + cli, + ], + { + cwd: root, + stdio: "inherit", + env: { ...process.env, SCRIPTC_NATIVE_PROFILE_GENERATE: join(raw, "build-%4m.profraw") }, + }, + ); + if (built.status !== 0) throw new Error("the instrumented compiler did not build"); +} +const scriptc = join(cli, "bin/scriptc"); +// Phase timing shows whether a rebuild hit both caches. It is on for every +// run, so the rebuild's environment matches the first build's. +const baseEnv = { ...process.env, SCRIPTC_TIMING: "1" }; +delete baseEnv["SCRIPTC_LLVM_HELPER"]; + +/** Runs the instrumented compiler, writing its profile to `options.profile`. */ +function run(args, options) { + return new Promise((done) => { + const child = spawn(scriptc, args, { + cwd: options.cwd, + stdio: ["ignore", "pipe", "pipe"], + env: { + ...baseEnv, + SCRIPTC_CACHE_DIR: options.cache, + LLVM_PROFILE_FILE: options.profile, + }, + }); + let text = ""; + child.stdout.on("data", (chunk) => (text += chunk)); + child.stderr.on("data", (chunk) => (text += chunk)); + child.on("close", (code) => { + appendFileSync(log, `$ scriptc ${args.join(" ")}\n${text}exit ${code}\n`); + done({ ok: code === 0, text }); + }); + }); +} + +async function pool(tasks) { + let next = 0; + let passed = 0; + await Promise.all( + Array.from({ length: jobs }, async () => { + while (next < tasks.length) if (await tasks[next++]()) passed++; + }), + ); + return passed; +} + +const programNumber = (name) => Number.parseInt(name, 10); +const training = [ + ...readdirSync(join(root, "benchmarks/runtime/workloads")) + .filter((name) => name.endsWith(".ts")) + .map((name) => ({ + file: join(root, "benchmarks/runtime/workloads", name), + optimization: "speed", + })), + ...readdirSync(join(root, "tests/corpus")) + .filter((name) => /^\d+-.*\.ts$/.test(name) && programNumber(name) % 7 === 0) + .map((name) => ({ file: join(root, "tests/corpus", name), optimization: "release" })), +]; + +// Tier 1 collects each run that took the warm path from its own profile; a +// rebuild that missed a cache (it shows no `frontend-cache-hit` or +// `executable-cache-hit` phase) is left out. The first builds each write +// their own profile (cold--*), merged across the build's processes. +const warm = new Set(); +let skipped = 0; +async function warmRun(args, cache, name, cacheHits) { + const profile = join(raw, `warm-${name}.profraw`); + const { ok, text } = await run(args, { cwd: root, cache, profile }); + if (ok && cacheHits.every((phase) => text.includes(`"phase":"${phase}"`))) + for (const fn of executedFunctions(profile)) warm.add(fn); + else skipped++; + rmSync(profile, { force: true }); +} + +const started = Date.now(); +await warmRun(["--version"], join(work, "cache-version"), "version", []); +console.log(`training: ${training.length} programs, cold then unchanged, ${jobs} at a time`); +const passed = await pool( + training.map((program, index) => async () => { + const cache = join(work, `cache-${index}`); + const out = join(builds, `program-${index}`); + const args = ["build", program.file, `--optimization=${program.optimization}`, "-o", out]; + const { ok } = await run(args, { + cwd: root, + cache, + profile: join(raw, `cold-${index}-%1m.profraw`), + }); + if (ok) + await warmRun(args, cache, String(index), ["frontend-cache-hit", "executable-cache-hit"]); + rmSync(cache, { recursive: true, force: true }); + rmSync(out, { force: true }); + return ok; + }), +); +console.log( + `trained in ${Math.round((Date.now() - started) / 1000)} s; ${passed} of ${training.length} programs built, ${skipped} rebuilds left out of tier 1 (see ${log})`, +); + +/** Functions a raw profile (LLVM's format version 10) records as run: the + * names whose counters are not all zero, without the file prefix of + * internal functions. */ +function executedFunctions(path) { + const buf = readFileSync(path); + const u64 = (offset) => Number(buf.readBigUInt64LE(offset)); + if (buf.readBigUInt64LE(0) !== 0xff6c70726f667281n) + throw new Error(`${path} is not a 64-bit raw profile`); + const version = Number(buf.readBigUInt64LE(8) & 0xffffffffn); + if (version !== 10) throw new Error(`${path} has raw profile version ${version}, expected 10`); + const [binaryIdsBytes, records, beforeCounters, counters, afterCounters] = [2, 3, 4, 5, 6].map( + (field) => u64(field * 8), + ); + const [bitmapBytes, afterBitmap, namesBytes] = [7, 8, 9].map((field) => u64(field * 8)); + const valueKinds = u64(15 * 8) + 1; + const recordBytes = Math.ceil((6 * 8 + 4 + 2 * valueKinds + 4) / 8) * 8; + const dataStart = 16 * 8 + binaryIdsBytes; + const countersStart = dataStart + records * recordBytes + beforeCounters; + const namesStart = countersStart + counters * 8 + afterCounters + bitmapBytes + afterBitmap; + const countersDelta = buf.readBigInt64LE(10 * 8); + const names = new Map(); + let at = namesStart; + const uleb = () => { + let value = 0; + let scale = 1; + for (;;) { + const byte = buf[at++]; + value += (byte & 0x7f) * scale; + if ((byte & 0x80) === 0) return value; + scale *= 128; + } + }; + while (at < namesStart + namesBytes) { + const plain = uleb(); + const packed = uleb(); + if (plain === 0) break; + const bytes = + packed === 0 ? buf.subarray(at, at + plain) : inflateSync(buf.subarray(at, at + packed)); + at += packed === 0 ? plain : packed; + for (const name of bytes.toString("latin1").split("\x01")) + names.set(createHash("md5").update(name, "latin1").digest().readBigUInt64LE(0), name); + } + const ran = []; + for (let index = 0; index < records; index++) { + const record = dataStart + index * recordBytes; + const counterOffset = Number( + buf.readBigInt64LE(record + 16) - (countersDelta - BigInt(index * recordBytes)), + ); + const count = buf.readUInt32LE(record + 48); + for (let c = 0; c < count; c++) { + if (buf.readBigUInt64LE(countersStart + counterOffset + c * 8) !== 0n) { + const name = names.get(buf.readBigUInt64LE(record)); + if (name !== undefined) ran.push(name.slice(name.indexOf(";") + 1)); + break; + } + } + } + return ran; +} + +// The cold tiers, by the least share of training builds that run a function: +// tier 2 for functions at least 90% of the builds run, tier 3 for 60%, and so +// on; tier 7 holds the functions fewer than 3% of the builds run. +const COLD_TIERS = [0.9, 0.6, 0.3, 0.1, 0.03]; +const coldRuns = new Map(); +for (const name of readdirSync(raw)) { + const program = /^cold-(\d+)-/.exec(name)?.[1]; + if (program === undefined) continue; + const ran = coldRuns.get(program) ?? new Set(); + for (const fn of executedFunctions(join(raw, name))) ran.add(fn); + coldRuns.set(program, ran); +} +const runs = new Map(); +for (const ran of coldRuns.values()) for (const fn of ran) runs.set(fn, (runs.get(fn) ?? 0) + 1); +const coldTier = (count) => { + const share = count / coldRuns.size; + const index = COLD_TIERS.findIndex((least) => share >= least); + return 2 + (index < 0 ? COLD_TIERS.length : index); +}; +const lines = [ + ...[...warm].sort().map((name) => `1 ${name}`), + ...[...runs] + .filter(([name]) => !warm.has(name)) + .map(([name, count]) => [coldTier(count), name]) + .sort((a, b) => a[0] - b[0] || (a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : 0)) + .map(([tier, name]) => `${tier} ${name}`), +]; +writeFileSync(output, gzipSync(`${lines.join("\n")}\n`, { level: constants.Z_BEST_COMPRESSION })); +const perTier = new Map(); +for (const line of lines) + perTier.set(line.split(" ")[0], (perTier.get(line.split(" ")[0]) ?? 0) + 1); +console.log( + `wrote ${output}: ${[...perTier].map(([tier, count]) => `tier ${tier}: ${count}`).join(", ")} functions, from ${coldRuns.size} builds (${statSync(output).size} bytes)`, +); +rmSync(builds, { recursive: true, force: true }); +if (values.work === undefined) rmSync(work, { recursive: true, force: true }); diff --git a/scripts/refresh-cli-profile.mjs b/scripts/refresh-cli-profile.mjs index e9554e911..dde4e21b9 100644 --- a/scripts/refresh-cli-profile.mjs +++ b/scripts/refresh-cli-profile.mjs @@ -1,12 +1,15 @@ #!/usr/bin/env node -/* Refreshes packages/compiler/native/cli.profdata.gz, the profile that release - * builds of the native compiler optimize with (scripts/build-native-cli.mts). +/* Refreshes packages/compiler/native/cli.profdata.gz, the profile that builds + * of the native compiler optimize with (scripts/build-native-cli.mts). The + * release workflow's cli-profile job runs it with --output on the release + * commit, and release builds use that profile instead of the committed one. * * Builds an instrumented native compiler for this Linux host, runs it on the * training builds below, and merges the raw profiles it writes into one * sparse indexed profile. The training builds are tsc-ts (a cold `speed` - * build, then a rebuild after an edit), every seventh corpus program at the - * default optimization and a further slice in `dev`, and the runtime + * build, then a rebuild after an edit), every seventh top-level corpus + * program (by number, or by a hash of the name of a program without one) at + * the default optimization and a further slice in `dev`, and the runtime * benchmark workloads. Run it on a quiet Linux x64 host after changes that * move much compiler code; a stale profile only loses part of its gain. A * refresh takes about 11 minutes on 16 cores. @@ -26,7 +29,7 @@ import { writeFileSync, } from "node:fs"; import { cpus, tmpdir } from "node:os"; -import { join, resolve } from "node:path"; +import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { parseArgs } from "node:util"; import { constants, gzipSync } from "node:zlib"; @@ -55,6 +58,9 @@ const tscTsPackage = join(tscTs, "packages/tsc-ts"); if (!existsSync(join(tscTsPackage, "src/main.ts"))) throw new Error(`${tscTs} is not a tsc-ts checkout with installed dependencies`); const jobs = Number(values.jobs); +// The output's directory is created up front, before the instrumented build. +const profileOutput = resolve(values.output); +mkdirSync(dirname(profileOutput), { recursive: true }); const work = resolve(values.work ?? mkdtempSync(join(tmpdir(), "scriptc-cli-profile-"))); const raw = join(work, "raw"); const builds = join(work, "builds"); @@ -121,16 +127,30 @@ async function pool(tasks) { return passed; } -const programNumber = (name) => Number.parseInt(name, 10); +/** A top-level corpus program's sampling key: its number, or, for a program + * named without one, a stable hash of its name (FNV-1a). */ +const programKey = (name) => + /^\d+-/.test(name) + ? Number.parseInt(name, 10) + : [...name].reduce((h, c) => Math.imul(h ^ c.charCodeAt(0), 16777619) >>> 0, 2166136261); +/** Numbered programs by number, then the named ones by name. */ +const programOrder = (a, b) => + /^\d+-/.test(a) !== /^\d+-/.test(b) + ? /^\d+-/.test(a) + ? -1 + : 1 + : programKey(a) - programKey(b) || (a < b ? -1 : a > b ? 1 : 0); const corpus = readdirSync(join(root, "tests/corpus")) - .filter((name) => /^\d+-.*\.ts$/.test(name)) - .sort((a, b) => programNumber(a) - programNumber(b)); + .filter( + (name) => /^[\w-]+\.ts$/.test(name) && statSync(join(root, "tests/corpus", name)).isFile(), + ) + .sort(programOrder); const training = [ ...corpus - .filter((name) => programNumber(name) % 7 === 0) + .filter((name) => programKey(name) % 7 === 0) .map((name) => ({ file: join(root, "tests/corpus", name), optimization: "release" })), ...corpus - .filter((name) => programNumber(name) % 14 === 7) + .filter((name) => programKey(name) % 14 === 7) .map((name) => ({ file: join(root, "tests/corpus", name), optimization: "dev" })), ...readdirSync(join(root, "benchmarks/runtime/workloads")) .filter((name) => name.endsWith(".ts")) @@ -196,10 +216,12 @@ const merged = spawnSync(scriptc, ["merge-profiles", "--sparse", "-o", indexed, }); if (merged.status !== 0) throw new Error("merge-profiles failed"); // The indexed profile compresses to about an eighth of its size. -const output = resolve(values.output); -writeFileSync(output, gzipSync(readFileSync(indexed), { level: constants.Z_BEST_COMPRESSION })); +writeFileSync( + profileOutput, + gzipSync(readFileSync(indexed), { level: constants.Z_BEST_COMPRESSION }), +); console.log( - `wrote ${output} (${statSync(output).size} bytes; ${statSync(indexed).size} uncompressed)`, + `wrote ${profileOutput} (${statSync(profileOutput).size} bytes; ${statSync(indexed).size} uncompressed)`, ); // A work directory the caller named stays for inspection. if (values.work === undefined) rmSync(work, { recursive: true, force: true }); diff --git a/scripts/refresh-llvm-profile.mjs b/scripts/refresh-llvm-profile.mjs new file mode 100644 index 000000000..48f730cd0 --- /dev/null +++ b/scripts/refresh-llvm-profile.mjs @@ -0,0 +1,112 @@ +#!/usr/bin/env node +/* Refreshes native/llvm-codegen/llvm.profdata.gz, the profile the release + * LLVM distribution and the Linux helpers' own sources are optimized with + * (native/llvm-codegen/llvm-dist.mjs). The profile follows LLVM's sources + * and the kind of code scriptc emits rather than scriptc's commits, so it + * needs a refresh when LLVM is bumped, or when the emitted code changes + * enough that the distribution check (scripts/check-llvm-dist.mjs) loses + * much of its gain. + * + * Builds LLVM with IR instrumentation (scripts/build-llvm-dist.mjs + * --instrumented) and an instrumented helper against it, then runs that + * helper on the training inputs (scripts/llvm-helper-inputs.mjs): tsc-ts, + * five Effect fixtures, the runtime benchmark workloads and every 50th + * corpus program. The merged profile is written gzip-compressed. Needs a + * built checkout on Linux x64, clang, lld and llvm-profdata of LLVM_VERSION + * on PATH, and the LLVM release sources. About 20 minutes on 32 cores. + * + * node scripts/refresh-llvm-profile.mjs --source + * --tsc-ts [--work ] [--jobs N] [--output ] + */ +import { spawnSync } from "node:child_process"; +import { mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs"; +import { cpus, tmpdir } from "node:os"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { parseArgs } from "node:util"; +import { constants, gzipSync } from "node:zlib"; +import { llvmProfile } from "../native/llvm-codegen/llvm-dist.mjs"; +import { buildLinuxHelper } from "../native/llvm-codegen/linux-helper.mjs"; +import { captureHelperInputs, helperInputPrograms } from "./llvm-helper-inputs.mjs"; + +const root = fileURLToPath(new URL("..", import.meta.url)); +const { values } = parseArgs({ + options: { + source: { type: "string" }, + "tsc-ts": { type: "string" }, + work: { type: "string" }, + jobs: { type: "string", default: String(cpus().length) }, + output: { type: "string", default: llvmProfile }, + help: { type: "boolean", default: false }, + }, +}); +if (values.help || values.source === undefined || values["tsc-ts"] === undefined) { + console.log( + "Usage: node scripts/refresh-llvm-profile.mjs --source --tsc-ts [--work ] [--jobs N] [--output ]", + ); + process.exit(values.help ? 0 : 2); +} +if (process.platform !== "linux" || process.arch !== "x64") + throw new Error("the LLVM profile is collected on Linux x64"); +const work = resolve(values.work ?? mkdtempSync(join(tmpdir(), "scriptc-llvm-profile-"))); +const prefix = join(work, "llvm-instrumented-install"); +const helper = join(work, "helper", "scriptc-llvm-codegen"); +const raw = join(work, "raw"); + +function run(command, args, options = {}) { + const result = spawnSync(command, args, { stdio: "inherit", ...options }); + if (result.status !== 0) + throw new Error(`${command} ${args[0]} failed with status ${result.status}`); +} + +console.log(`building the instrumented LLVM in ${work}`); +run(process.execPath, [ + join(root, "scripts/build-llvm-dist.mjs"), + "--instrumented", + "--source", + resolve(values.source), + "--prefix", + prefix, + "--work", + work, + "--jobs", + values.jobs, +]); +const manifest = JSON.parse( + readFileSync(join(root, "packages/llvm-linux-x64-gnu/package.json"), "utf8"), +); +process.env.LLVM_DIR = join(prefix, "lib/cmake/llvm"); +process.env.SCRIPTC_LLVM_PROFILE_GENERATE = "1"; +delete process.env.SCRIPTC_LLVM_THINLTO; +buildLinuxHelper({ + arch: "x64", + libc: "gnu", + version: manifest.version, + build: join(work, "helper-build"), + output: helper, +}); + +const inputs = await captureHelperInputs( + helperInputPrograms({ tscTs: resolve(values["tsc-ts"]) }), + join(work, "inputs"), +); +rmSync(raw, { recursive: true, force: true }); +mkdirSync(raw, { recursive: true }); +const out = join(work, "objects"); +for (const input of inputs) { + rmSync(out, { recursive: true, force: true }); + mkdirSync(out, { recursive: true }); + const result = spawnSync(helper, input.args(out), { + env: { ...process.env, LLVM_PROFILE_FILE: join(raw, "helper-%p.profraw") }, + encoding: "utf8", + }); + console.log(`${input.name}: ${input.partitions} partitions, status ${result.status}`); + if (result.status !== 0) throw new Error(`${input.name}: ${result.stdout}${result.stderr}`); +} + +const merged = join(work, "llvm.profdata"); +run("llvm-profdata", ["merge", "-o", merged, ...readdirSync(raw).map((file) => join(raw, file))]); +const output = resolve(values.output); +mkdirSync(dirname(output), { recursive: true }); +writeFileSync(output, gzipSync(readFileSync(merged), { level: constants.Z_BEST_COMPRESSION })); +console.log(`wrote ${output} from ${inputs.length} inputs`); diff --git a/scripts/smoke-native-install.mjs b/scripts/smoke-native-install.mjs index 2325f658b..06ff9e3f0 100644 --- a/scripts/smoke-native-install.mjs +++ b/scripts/smoke-native-install.mjs @@ -54,7 +54,27 @@ function bytes(directory) { return size; } try { - const platformTarball = pack(platform); + // A platform package depends on its LLVM helper and runtime pack packages. + // Pack them from this checkout, and pack the platform package with the + // exact versions pnpm would publish for its workspace dependencies. + const dependencies = Object.keys(identity.dependencies ?? {}); + let packed = platform; + if (dependencies.length > 0) { + packed = join(root, "platform"); + mkdirSync(packed); + cpSync(join(platform, "dist"), join(packed, "dist"), { recursive: true }); + writeFileSync( + join(packed, "package.json"), + JSON.stringify({ + ...identity, + dependencies: Object.fromEntries(dependencies.map((name) => [name, identity.version])), + }), + ); + } + const platformTarball = pack(packed); + const dependencyTarballs = dependencies.map((name) => + pack(join(repository, "packages", name.replace("@scriptc/", ""))), + ); const wrapper = join(root, "wrapper"); mkdirSync(wrapper); cpSync(join(repository, "packages/cli/scripts"), join(wrapper, "scripts"), { recursive: true }); @@ -77,7 +97,11 @@ try { const tarball = pack(wrapper); const project = join(root, "project"); mkdirSync(project); - command(npm, ["install", "--offline", "--no-audit", "--no-fund", tarball], project); + command( + npm, + ["install", "--offline", "--no-audit", "--no-fund", tarball, ...dependencyTarballs], + project, + ); const executable = join(project, "node_modules/scriptc/bin/scriptc.exe"); assert.equal( command(executable, ["--version"], project, { ...env, PATH: "" }).trim(), @@ -99,8 +123,11 @@ try { ); command(executable, ["build", entry, "-o", binary], project, compileEnv); assert.equal(command(binary, [], project, { PATH: "" }), "true true true\n"); - const installed = readdirSync(join(project, "node_modules/@scriptc")); - assert.deepEqual(installed, [identity.name.slice("@scriptc/".length)]); + const installed = readdirSync(join(project, "node_modules/@scriptc")).sort(); + assert.deepEqual( + installed, + [identity.name, ...dependencies].map((name) => name.slice("@scriptc/".length)).sort(), + ); console.log( JSON.stringify({ package: identity.name, diff --git a/scripts/verify-native-cli.mjs b/scripts/verify-native-cli.mjs index 92de57938..fc94927be 100644 --- a/scripts/verify-native-cli.mjs +++ b/scripts/verify-native-cli.mjs @@ -1,5 +1,6 @@ import { execFileSync } from "node:child_process"; import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs"; +import { createRequire } from "node:module"; import { dirname, isAbsolute, join, relative, resolve, sep, win32 } from "node:path"; import { verifyLinuxAbi } from "./verify-linux-abi.mjs"; @@ -26,11 +27,30 @@ const within = (path) => { if (child === ".." || child.startsWith(".." + sep) || isAbsolute(child)) throw new Error(`distribution references external asset: ${path}`); }; +// An npm platform package depends on its LLVM helper and runtime pack +// packages at its own version instead of bundling them. +const platformPackages = toolchain.llvm_package === undefined; +const dependencies = identity.dependencies ?? {}; +const dependency = (prefix) => { + const name = Object.keys(dependencies).find((key) => key.startsWith(prefix)); + if (name === undefined || ![identity.version, "workspace:*"].includes(dependencies[name])) + throw new Error( + `${identity.name} must depend on its ${prefix}* package at ${identity.version}`, + ); + return dirname(createRequire(join(root, "package.json")).resolve(`${name}/package.json`)); +}; +if (platformPackages && (toolchain.runtime_pack !== undefined || toolchain.runtime_packs)) + throw new Error("a platform package's runtime pack is its dependency"); +const helperRoot = platformPackages + ? dependency("@scriptc/llvm-") + : resolve(dirname(binary), hostPath(toolchain.llvm_package)); +const runtimeRoot = platformPackages + ? dependency("@scriptc/runtime-") + : resolve(dirname(binary), hostPath(toolchain.runtime_pack)); for (const name of [ "ts7", "tsc_ts", - "llvm_package", - "runtime_pack", + ...(platformPackages ? [] : ["llvm_package", "runtime_pack"]), "runtime_sources", "declarations", "comptime", @@ -41,14 +61,18 @@ for (const name of [ throw new Error(`native toolchain needs a relative ${name}`); within(resolve(dirname(binary), hostPath(value))); } -const pack = JSON.parse( - readFileSync( - resolve(dirname(binary), hostPath(toolchain.runtime_pack), "runtime-pack.json"), - "utf8", - ), -); +// Optional: a compiler this host could not run while building ships without it. +if (toolchain.lib_diagnostics !== undefined) { + const value = toolchain.lib_diagnostics; + if (typeof value !== "string" || absolutePath(value)) + throw new Error("native toolchain needs a relative lib_diagnostics"); + within(resolve(dirname(binary), hostPath(value))); +} +const pack = JSON.parse(readFileSync(join(runtimeRoot, "runtime-pack.json"), "utf8")); if (pack.version !== identity.version || pack.target.name !== toolchain.target) throw new Error("native compiler runtime identity mismatch"); +if (JSON.parse(readFileSync(join(helperRoot, "package.json"), "utf8")).version !== identity.version) + throw new Error("native compiler LLVM helper version mismatch"); const seen = new Set(); for (const runtime of toolchain.runtime_packs ?? []) { if (seen.has(runtime.target)) throw new Error(`duplicate runtime target: ${runtime.target}`); @@ -84,9 +108,7 @@ if (process.argv.includes("--run")) { ]) { verifyLinuxAbi(executable, { staticOnly: target.endsWith("-musl") }); } - verifyLinuxAbi(resolve(dirname(binary), toolchain.llvm_package, "bin/scriptc-llvm-codegen"), { - staticOnly: true, - }); + verifyLinuxAbi(join(helperRoot, "bin/scriptc-llvm-codegen"), { staticOnly: true }); } const version = execFileSync(binary, ["--version"], { encoding: "utf8", @@ -104,5 +126,12 @@ if (process.argv.includes("--run")) { }).trim(); if (!tscTsVersion.startsWith("Version ")) throw new Error(`the distribution's tsc-ts reported ${JSON.stringify(tscTsVersion)}`); + // The release strips tsc-ts and the comptime evaluator; arm64 macOS runs + // only executables whose linker-generated code signature matches their + // contents. + if (process.platform === "darwin" && process.arch === "arm64") { + for (const executable of [binary, tscTs, resolve(dirname(binary), toolchain.comptime)]) + execFileSync("codesign", ["--verify", "--strict", executable], { stdio: "inherit" }); + } } console.log(`${identity.name}@${identity.version}: native distribution verified`); diff --git a/scripts/wait-npm-available.mjs b/scripts/wait-npm-available.mjs new file mode 100644 index 000000000..26d7db391 --- /dev/null +++ b/scripts/wait-npm-available.mjs @@ -0,0 +1,150 @@ +#!/usr/bin/env node +// Waits until every package of a release resolves on the npm registry. +// +// usage: wait-npm-available.mjs [--interval ] [--timeout ] +// +// Each line of the uploads file is `@`, optionally followed by +// the Unix time (seconds) at which npm accepted the upload. Every interval +// (120 seconds by default), each package that has not resolved yet is asked +// for with `npm view @ version --prefer-online`. A package +// resolves when npm prints its version. Each package is reported once it +// resolves, with the time from upload to the check that found it. +// +// Exits with 0 once every package resolves. When packages are still missing +// at the timeout (2 hours by default), prints their names and versions and +// exits with 1. The script only reads from the registry. +import { execFile } from "node:child_process"; +import { appendFileSync, readFileSync } from "node:fs"; +import { setTimeout as delay } from "node:timers/promises"; + +function usage() { + throw new Error( + "usage: wait-npm-available.mjs [--interval ] [--timeout ]", + ); +} + +const args = process.argv.slice(2); +let file; +let intervalSeconds = 120; +let timeoutSeconds = 2 * 60 * 60; +for (let i = 0; i < args.length; i++) { + const arg = args[i]; + if (arg === "--interval" || arg === "--timeout") { + const value = Number(args[++i]); + if (!Number.isFinite(value) || value <= 0) usage(); + if (arg === "--interval") intervalSeconds = value; + else timeoutSeconds = value; + } else if (file === undefined) { + file = arg; + } else { + usage(); + } +} +if (file === undefined) usage(); + +const packages = []; +for (const line of readFileSync(file, "utf8").split("\n")) { + const fields = line.trim().split(/\s+/); + if (fields[0] === "") continue; + const spec = fields[0]; + const at = spec.lastIndexOf("@"); + if (at <= 0 || at === spec.length - 1 || fields.length > 2) { + throw new Error(`malformed upload line: ${line}`); + } + const acceptedAt = fields[1] === undefined ? null : Number(fields[1]) * 1000; + if (acceptedAt !== null && !Number.isFinite(acceptedAt)) { + throw new Error(`malformed upload time: ${line}`); + } + packages.push({ spec, version: spec.slice(at + 1), acceptedAt, availableAt: null }); +} +if (packages.length === 0) throw new Error(`${file} names no packages`); + +function resolves(pkg) { + return new Promise((resolve) => { + execFile( + "npm", + ["view", pkg.spec, "version", "--prefer-online"], + { encoding: "utf8", timeout: 60_000 }, + (error, stdout, stderr) => { + if (error === null && stdout.trim() === pkg.version) { + resolve(true); + return; + } + const code = /npm error code (\S+)/.exec(stderr)?.[1]; + if (error !== null && code !== "E404") { + const reason = code ?? error.code ?? error.message.split("\n")[0]; + console.log(`${pkg.spec}: npm view failed (${reason}); checking again`); + } + resolve(false); + }, + ); + }); +} + +function duration(ms) { + const seconds = Math.max(0, Math.round(ms / 1000)); + const minutes = Math.floor(seconds / 60); + return minutes === 0 ? `${seconds}s` : `${minutes}m ${String(seconds % 60).padStart(2, "0")}s`; +} + +function uploadToAvailable(pkg) { + return pkg.acceptedAt === null + ? "upload time not recorded" + : duration(pkg.availableAt - pkg.acceptedAt); +} + +function summary(missing) { + const summaryFile = process.env.GITHUB_STEP_SUMMARY; + if (!summaryFile) return; + const rows = packages.map( + (pkg) => + `| \`${pkg.spec}\` | ${pkg.availableAt === null ? "missing" : uploadToAvailable(pkg)} |`, + ); + const heading = + missing.length === 0 + ? `Every package resolves on npm (${packages.length}).` + : `${missing.length} of ${packages.length} packages do not resolve on npm after ${duration(timeoutSeconds * 1000)}.`; + appendFileSync( + summaryFile, + [heading, "", "| Package | Upload to available |", "| --- | --- |", ...rows, ""].join("\n"), + ); +} + +const started = Date.now(); +const deadline = started + timeoutSeconds * 1000; +for (;;) { + const pending = packages.filter((pkg) => pkg.availableAt === null); + const checked = []; + // A few checks at a time keep each round short without flooding the registry. + for (let i = 0; i < pending.length; i += 8) { + const batch = pending.slice(i, i + 8); + const results = await Promise.all(batch.map(resolves)); + const now = Date.now(); + batch.forEach((pkg, j) => { + if (results[j]) { + pkg.availableAt = now; + checked.push(pkg); + } + }); + } + for (const pkg of checked) console.log(`${pkg.spec} is available (${uploadToAvailable(pkg)})`); + const missing = packages.filter((pkg) => pkg.availableAt === null); + if (missing.length === 0) { + console.log(`Every package resolves on npm (${packages.length}).`); + summary(missing); + process.exit(0); + } + const now = Date.now(); + if (now >= deadline) { + console.log( + `${missing.length} of ${packages.length} packages do not resolve on npm after ${duration(now - started)}:`, + ); + for (const pkg of missing) console.log(`::error::${pkg.spec} does not resolve on npm`); + summary(missing); + process.exit(1); + } + console.log( + `${missing.length} of ${packages.length} packages do not resolve yet; checking again in ${duration(Math.min(intervalSeconds * 1000, deadline - now))}`, + ); + await delay(Math.min(intervalSeconds * 1000, deadline - now)); +} diff --git a/scripts/zero-cost-compare.mjs b/scripts/zero-cost-compare.mjs index ff3acf2e9..affe4ee34 100644 --- a/scripts/zero-cost-compare.mjs +++ b/scripts/zero-cost-compare.mjs @@ -405,8 +405,14 @@ export function nodeIslandSymbols(names) { * not yet traced to its typed root. bench/thread-slots, * bench/channel-messages and bench/worker-messages, added later, link them * through worker messages: - * node:worker_threads values are dyn objects. bench/dynamic-methods is - * untyped JavaScript. Every other bench workload is fully typed and must link none. The gate and + * node:worker_threads values are dyn objects. bench/json-unions, added + * later too, decodes JSON.parse trees. bench/throw-chains, also added + * later, tests caught values with `instanceof`, as bench/validate-errors + * does. bench/dynamic-methods and + * bench/dynamic-properties are untyped JavaScript. bench/regex-replacers, + * also added later, passes checked-dynamic values to replace callbacks + * whose parameters the lib types `any`: captures a match can leave unset + * and results the checker types `any`. Every other bench workload is fully typed and must link none. The gate and * tests/harness/bench-dynamic-tier.test.ts enforce that, and the test also * fails when a listed workload stops linking them, so remove a workload here * when a leak fix makes its binary clean. */ @@ -421,10 +427,15 @@ export const DYNAMIC_WORKLOADS = new Set([ "bench/thread-slots", "bench/channel-messages", "bench/dynamic-methods", + "bench/dynamic-properties", "bench/worker-messages", "bench/typed-array-scan-workers", "bench/log-summary", "bench/inventory-report", + "bench/line-reader", + "bench/json-unions", + "bench/throw-chains", + "bench/regex-replacers", ]); /** Names of the functions (text and weak symbols) in `nm` output. Mach-O diff --git a/scripts/zero-cost-gate.mjs b/scripts/zero-cost-gate.mjs index 891c8a021..053c2f0d0 100644 --- a/scripts/zero-cost-gate.mjs +++ b/scripts/zero-cost-gate.mjs @@ -80,7 +80,8 @@ runtime pack). Both compile the same program files, taken from --programs --set picks the good-code set: bench (benchmarks/runtime workloads and the benchmarks/builds applications), self (packages/compiler/src/native/cli.ts, -LLVM only), corpus (tests/corpus TypeScript programs; --corpus=all adds +LLVM only; the candidate's own compiler source must also build with the +candidate), corpus (tests/corpus TypeScript programs; --corpus=all adds JavaScript entries). --quick is --set=bench --timing-runs=0, for CI. --shard=i/n keeps every n-th corpus program; --only filters program ids. @@ -634,6 +635,30 @@ if (timingRuns > 0) { } } +// The self program is the baseline's compiler source (the programs come +// from one checkout), so a change that adds compiler code the self-hosted +// frontend cannot lower would pass unnoticed. The candidate's own compiler +// source must still build with the candidate (LLVM only, not compared). +if (sets.has("self") && programsRoot !== trees.candidate) { + const entry = join(trees.candidate, "packages/compiler/src/native/cli.ts"); + const result = await buildProgram( + { id: "self/candidate-source", entry, kind: "source" }, + "candidate", + ); + report.candidateSelf = result.ok + ? { ok: true, buildMs: result.ms } + : { ok: false, error: result.error }; + console.log( + result.ok + ? `self candidate compiler source builds with the candidate (${fmtMs(result.ms)})` + : `self candidate compiler source FAILS to build with the candidate`, + ); + if (!result.ok) { + failed = true; + report.failures.push(`self/candidate-source: ${result.error.split("\n")[0]}`); + } +} + const counts = {}; for (const p of report.programs) counts[p.verdict] = (counts[p.verdict] ?? 0) + 1; if (!report.programs.some((p) => ["identical", "allowed", "differs"].includes(p.verdict))) { diff --git a/tests/corpus-node-islands-order/e04-throw-in-io-step/main.ts b/tests/corpus-node-islands-order/e04-throw-in-io-step/main.ts index 3b6f06687..8110c3744 100644 --- a/tests/corpus-node-islands-order/e04-throw-in-io-step/main.ts +++ b/tests/corpus-node-islands-order/e04-throw-in-io-step/main.ts @@ -1,4 +1,5 @@ // @requires-libnode +// @order: race // X { G R { L i-io E } } B 30 L h-sync T 10 { L h-t10 } import { callSync } from "@scriptc/node"; import { readFile, writeSync } from "node:fs"; diff --git a/tests/corpus-node-islands-order/p12-island-io-vs-busy/main.ts b/tests/corpus-node-islands-order/p12-island-io-vs-busy/main.ts index 7b342dd4d..477175473 100644 --- a/tests/corpus-node-islands-order/p12-island-io-vs-busy/main.ts +++ b/tests/corpus-node-islands-order/p12-island-io-vs-busy/main.ts @@ -1,4 +1,5 @@ // @requires-libnode +// @order: race // X { R { L i-io } } B 30 L h-sync T 0 { L h-t0 } import { callSync } from "@scriptc/node"; import { readFile, writeSync } from "node:fs"; diff --git a/tests/corpus-node-islands-order/s03-load-in-immediate/main.ts b/tests/corpus-node-islands-order/s03-load-in-immediate/main.ts index abbe2e637..f4e453ab4 100644 --- a/tests/corpus-node-islands-order/s03-load-in-immediate/main.ts +++ b/tests/corpus-node-islands-order/s03-load-in-immediate/main.ts @@ -1,4 +1,5 @@ // @requires-libnode +// @order: race // Switch: Node loads inside an immediate while later immediates of the same check // phase, and timers, are pending. import { callSync } from "@scriptc/node"; diff --git a/tests/corpus/1004-json-parse-errors.ts b/tests/corpus/1004-json-parse-errors.ts index e65a9752e..aad0817d7 100644 --- a/tests/corpus/1004-json-parse-errors.ts +++ b/tests/corpus/1004-json-parse-errors.ts @@ -1,9 +1,7 @@ // JSON.parse syntax errors are CATCHABLE, exactly like Node's SyntaxError. -// The message text itself is unobservable here (the supported catch form is -// bindingless) and its V8-flavored shape is deliberately approximate — -// exact-message assertions live in the runtime C tests (test_json.c), per -// SEMANTICS.md. This program only proves the throw/catch/recover behavior -// matches Node for every syntax-error class. +// This program proves the throw/catch/recover behavior matches Node for +// every syntax-error class; 7993-json-parse-error-messages pins the +// messages themselves. function attempt(raw: string, label: string): void { try { diff --git a/tests/corpus/1006-json-fs-config.ts b/tests/corpus/1006-json-fs-config.ts index f2b1d48c0..1783202c9 100644 --- a/tests/corpus/1006-json-fs-config.ts +++ b/tests/corpus/1006-json-fs-config.ts @@ -11,7 +11,7 @@ function tail(path: string): string { } return path.slice(i + 1); } -const path = "tmp-1006-" + tail(process.argv[1]) + ".json"; +const path = "tmp-1006-" + tail(process.argv[1]) + "-" + process.pid + ".json"; type Config = { name: string; diff --git a/tests/corpus/1110-math-members.ts b/tests/corpus/1110-math-members.ts index 80dd78209..5f07821c5 100644 --- a/tests/corpus/1110-math-members.ts +++ b/tests/corpus/1110-math-members.ts @@ -1,20 +1,17 @@ -// Math members execute in the island engine; results exit as ordinary -// static numbers. Members with exact IEEE results print directly. The -// transcendentals round through toFixed(9): the engine uses the system -// libm and Node uses V8's fdlibm port, which can differ in the last ulp -// (tan(1), asin(0.5), acos(0.5) do on macOS). +// Math members print the values Node prints, the transcendentals +// included: they are V8's fdlibm functions. console.log(Math.floor(7.6), Math.ceil(7.2), Math.round(7.5), Math.round(-7.5), Math.trunc(-7.9)); console.log(Math.abs(-3.5), Math.sign(-12), Math.sign(0), Math.sign(4.2)); console.log(Math.sqrt(144), Math.sqrt(2), Math.pow(2, 10), Math.pow(2, 0.5)); console.log(Math.hypot(3, 4), Math.min(2, -9), Math.max(2, -9)); console.log(Math.log2(1024), Math.log10(100000), Math.exp(0), Math.log(1)); console.log(Math.PI, Math.E); -console.log(Math.cbrt(27).toFixed(9), Math.exp(1).toFixed(9), Math.log(10).toFixed(9)); -console.log(Math.sin(1).toFixed(9), Math.cos(1).toFixed(9), Math.tan(1).toFixed(9)); -console.log(Math.asin(0.5).toFixed(9), Math.acos(0.5).toFixed(9)); -console.log(Math.atan(1).toFixed(9), Math.atan2(1, 1).toFixed(9), Math.atan2(-1, -1).toFixed(9)); -// Island results chain into static arithmetic like any number. +console.log(Math.cbrt(27), Math.exp(1), Math.log(10)); +console.log(Math.sin(1), Math.cos(1), Math.tan(1)); +console.log(Math.asin(0.5), Math.acos(0.5)); +console.log(Math.atan(1), Math.atan2(1, 1), Math.atan2(-1, -1)); +// Math results chain into static arithmetic like any number. const area = Math.PI * Math.pow(2, 2); console.log(area.toFixed(6), Math.floor(area) + 1); -// Non-finite edges: strict exits accept every engine number. +// Non-finite results. console.log(Math.log(0), Math.log(-1), Math.sqrt(-1), Math.pow(0, -1)); diff --git a/tests/corpus/1354-builtin-bare-specifiers.ts b/tests/corpus/1354-builtin-bare-specifiers.ts index cd0db6984..826c3e7a0 100644 --- a/tests/corpus/1354-builtin-bare-specifiers.ts +++ b/tests/corpus/1354-builtin-bare-specifiers.ts @@ -15,7 +15,7 @@ function tail(path: string): string { return path.slice(i + 1); } -const scratch = "tmp-1354-" + tail(process.argv[1]); +const scratch = "tmp-1354-" + tail(process.argv[1]) + "-" + process.pid; if (existsSync(scratch)) { if (existsSync(j(scratch, "f.txt"))) { rmSync(j(scratch, "f.txt")); diff --git a/tests/corpus/1357-fsp-roundtrip.ts b/tests/corpus/1357-fsp-roundtrip.ts index 4ccae7f3a..7368da360 100644 --- a/tests/corpus/1357-fsp-roundtrip.ts +++ b/tests/corpus/1357-fsp-roundtrip.ts @@ -15,7 +15,7 @@ function tail(path: string): string { } async function main(): Promise { - const scratch = "tmp-1357-" + tail(process.argv[1]); + const scratch = "tmp-1357-" + tail(process.argv[1]) + "-" + process.pid; if (existsSync(scratch)) { if (existsSync(scratch + "/a.txt")) { rmSync(scratch + "/a.txt"); diff --git a/tests/corpus/1561-forof-strings.ts b/tests/corpus/1561-forof-strings.ts index 9d16efd9a..0ea09e755 100644 --- a/tests/corpus/1561-forof-strings.ts +++ b/tests/corpus/1561-forof-strings.ts @@ -1,7 +1,7 @@ // for-of over STRINGS: JS's string iterator walks code POINTS — each pass // binds the whole character, so astral chars arrive as one two-unit string -// (where charAt/slice would truncate the halves). Desugars over the UTF-16 -// cursor with the cpAt step intrinsic. +// (where charAt/slice would truncate the halves). Desugars over a byte +// cursor with the cpAtByte step intrinsic. for (const ch of "héllo 🎉x") console.log(ch, ch.length); // The empty string iterates zero times. diff --git a/tests/corpus/2570-buffer-arg-validation.cjs b/tests/corpus/2570-buffer-arg-validation.cjs index d49593a30..c0980e495 100644 --- a/tests/corpus/2570-buffer-arg-validation.cjs +++ b/tests/corpus/2570-buffer-arg-validation.cjs @@ -1,4 +1,3 @@ -// @no-deprecation // Buffer's invalid-argument ladders, Node-exact: Buffer.compare / // buf.compare / buf.equals over non-buffer values and bad offsets throw // ERR_INVALID_ARG_TYPE / ERR_OUT_OF_RANGE with Node's argument names and @@ -7,6 +6,8 @@ // real answers. Plus the deprecated `new Buffer(number, enc)` string-arm // rejection and the ERR_* codes on the write/swap/fill range ladders. 'use strict'; +// The deprecated constructor's DEP0005 report carries a process id. +process.noDeprecation = true; const show = (fn) => { try { console.log('ret', fn()); diff --git a/tests/corpus/2844-node-disposable-resources.ts b/tests/corpus/2844-node-disposable-resources.ts index 042f617ce..ebeda87a2 100644 --- a/tests/corpus/2844-node-disposable-resources.ts +++ b/tests/corpus/2844-node-disposable-resources.ts @@ -14,7 +14,7 @@ async function main(): Promise { console.log(timeout.hasRef(), immediate.hasRef()); } - const path = "/tmp/scriptc-disposable-file.txt"; + const path = `/tmp/scriptc-disposable-file-${process.pid}.txt`; await writeFile(path, "resource"); { await using handle = await open(path, "r"); diff --git a/tests/corpus/3068-debug-variables/main.ts b/tests/corpus/3068-debug-variables/main.ts index d9399b49d..2e7cef4d6 100644 --- a/tests/corpus/3068-debug-variables/main.ts +++ b/tests/corpus/3068-debug-variables/main.ts @@ -21,7 +21,7 @@ function inspect(input: number, enabled: boolean, fallback = 9): void { for (let i = 0; i < bytes.length; i++) { console.log("loop", i, bytes[i]); // debug: loop } - for (const item of [6, 7] as (number | undefined)[]) { + for (const item of [6, 7, undefined]) { console.log("item"); // debug: item console.log(item); } diff --git a/tests/corpus/5025-lifetime-region-shared-candidate.ts b/tests/corpus/5025-lifetime-region-shared-candidate.ts new file mode 100644 index 000000000..7b5084a05 --- /dev/null +++ b/tests/corpus/5025-lifetime-region-shared-candidate.ts @@ -0,0 +1,85 @@ +// LifetimeRegion (@scriptc/experimental): freeing a region whose objects +// own a cycle runs a limited cycle pass over what the region owned. That +// pass also reaches a shared object the region pointed at, which is a +// candidate of its own (it lost a reference) and stays referenced from +// outside. The shared object must stay a valid candidate after the pass: +// it dies afterwards, later passes run, and the program prints the same +// as under Node.js. +import type { ProgramLifetime } from "@scriptc/types"; +import { LifetimeRegion } from "@scriptc/experimental"; + +class Shared { + readonly label: string; + peers: Shared[] = []; + constructor(label: string) { + this.label = label; + } +} + +// Ordinary objects the region's instance owns: a cycle that points back +// into the region and at the shared object. +class Owned { + holder: Holder | undefined = undefined; + shared: Shared | undefined = undefined; + partner: Owned | undefined = undefined; +} + +class Holder implements ProgramLifetime { + owned: Owned | undefined = undefined; + readonly id: number; + constructor(id: number) { + this.id = id; + } +} + +// Cyclic garbage, so the collector runs passes of its own. +class Ring { + next: Ring | undefined = undefined; + readonly value: number; + constructor(value: number) { + this.value = value; + } +} + +function churn(n: number): number { + let total = 0; + for (let i = 0; i < n; i++) { + const a = new Ring(i); + const b = new Ring(i + 1); + a.next = b; + b.next = a; + total += a.value + b.next.value; + } + return total; +} + +function round(k: number): string { + let shared: Shared | undefined = new Shared(`shared ${k}`); + // A second reference, dropped below: the shared object becomes a candidate. + let keep: Shared[] = [shared]; + shared.peers.push(new Shared(`peer ${k}`)); + const region = new LifetimeRegion(); + region.run(() => { + const holder = new Holder(k); + const owned = new Owned(); + const partner = new Owned(); + owned.partner = partner; + partner.partner = owned; + owned.holder = holder; + owned.shared = shared; + holder.owned = owned; + }); + keep = []; + region.release(); + const label = shared.label + " / " + shared.peers[0]!.label + " / " + keep.length; + // The shared object dies, and the passes that follow read the candidate buffers. + shared = undefined; + churn(2000); + return label; +} + +for (let k = 0; k < 20; k++) { + const label = round(k); + if (k % 5 === 0) console.log(label); +} +console.log("churn", churn(10000)); diff --git a/tests/corpus/5566-fs-dirent-dyn-boundaries/main.mjs b/tests/corpus/5566-fs-dirent-dyn-boundaries/main.mjs index 95bcbecee..77bdf8905 100644 --- a/tests/corpus/5566-fs-dirent-dyn-boundaries/main.mjs +++ b/tests/corpus/5566-fs-dirent-dyn-boundaries/main.mjs @@ -8,7 +8,7 @@ function tail(path) { while (i >= 0 && path.charAt(i) !== "/" && path.charAt(i) !== "\\") i = i - 1; return path.slice(i + 1); } -const dir = `/tmp/scr-dirent-dyn-${tail(process.argv[1] === undefined ? "x" : process.argv[1])}`; +const dir = `/tmp/scr-dirent-dyn-${tail(process.argv[1] === undefined ? "x" : process.argv[1])}-${process.pid}`; if (fs.existsSync(dir)) fs.rmSync(dir, { recursive: true, force: true }); fs.mkdirSync(`${dir}/sub`, { recursive: true }); fs.writeFileSync(`${dir}/a.txt`, "x"); diff --git a/tests/corpus/5570-fs-cp-async.ts b/tests/corpus/5570-fs-cp-async.ts index d83750095..23747bcb3 100644 --- a/tests/corpus/5570-fs-cp-async.ts +++ b/tests/corpus/5570-fs-cp-async.ts @@ -88,8 +88,13 @@ const stamp = new Date("2021-05-06T07:08:09.123Z"); fs.utimesSync("file.txt", stamp, stamp); await fsp.cp("file.txt", "p-stamped.txt", { preserveTimestamps: true }); console.log(new Date(fs.statSync("p-stamped.txt").mtimeMs).toISOString()); +// The source tree gets fixed times: its own times come from the clock, and +// whether a copy equals them depends on their sub-millisecond digits. +fs.utimesSync("src/a.txt", stamp, stamp); +fs.utimesSync("src", stamp, stamp); await fsp.cp("src", "p-stamped-dir", { recursive: true, preserveTimestamps: true }); -console.log(fs.statSync("p-stamped-dir").mtimeMs === fs.statSync("src").mtimeMs); +for (const path of ["p-stamped-dir", "p-stamped-dir/a.txt"]) + console.log(path, new Date(fs.statSync(path).mtimeMs).toISOString()); const rejected = await fsp.cp("missing", "x").then( () => "fulfilled", diff --git a/tests/corpus/5965-date-objects.ts b/tests/corpus/5965-date-objects.ts new file mode 100644 index 000000000..c4bb01fb9 --- /dev/null +++ b/tests/corpus/5965-date-objects.ts @@ -0,0 +1,115 @@ +// Dates are objects with identity: arrays of Dates, `Date | undefined` +// and `Date | null` slots, === by identity, includes/indexOf/lastIndexOf, +// at/find/pop answering undefined, Map and Set keys and values, join, +// String() and default sort through toString(), and Dates in records, +// tuples, class fields, closures, async results and generators. +export {}; +process.env.TZ = "America/Chicago"; + +const base = new Date(Date.UTC(2026, 3, 1, 12)); +const same = base; +const copy = new Date(base); +const equalTime = new Date(base.getTime()); +console.log(base === same, base === copy, copy === equalTime, base !== copy); +console.log(copy.getTime() === base.getTime(), base < new Date(base.getTime() + 1), base >= copy); + +// Arrays. +const dates: Date[] = [base, new Date(Date.UTC(2025, 0, 1)), new Date(Date.UTC(2026, 0, 1))]; +console.log(dates.length, dates.includes(base), dates.includes(copy), dates.indexOf(dates[2]!)); +console.log(dates.indexOf(equalTime), dates.lastIndexOf(base), dates.indexOf(base, 1)); +console.log(dates.join(" | ")); +console.log(String(dates)); +console.log(`${dates}`); +console.log([...dates].sort().map((d) => d.toISOString())); +console.log(dates.toSorted((a, b) => a.getTime() - b.getTime()).map((d) => d.getUTCFullYear())); +const first = dates.at(0); +const missing = dates.at(10); +console.log(first === base, missing, missing === undefined, first?.toISOString(), missing?.toISOString()); +const found = dates.find((d) => d.getUTCFullYear() === 2025); +console.log(found === dates[1], dates.find((d) => d.getTime() < 0), dates.findLast((d) => d > base)); +const stack = [...dates]; +console.log(stack.pop() === dates[2], stack.pop()?.getUTCFullYear(), stack.pop() === base, stack.pop()); +const latest = dates.reduce((m, d) => (d > m ? d : m)); +console.log(latest === base, dates.filter((d) => d < base).length, dates.map((d) => d.getUTCMonth())); +const grid: Date[][] = [[base], [copy, base]]; +console.log(grid[1]!.indexOf(base), grid.flat().length, grid.flat().includes(copy)); +const holes: (Date | undefined)[] = [base, undefined, copy]; +console.log(holes.join(","), holes.indexOf(undefined), holes.filter((d) => d !== undefined).length); + +// Nullable slots. +let maybe: Date | null = null; +console.log(maybe === null, maybe); +maybe = base; +console.log(maybe === base, maybe.getUTCHours()); +function stamp(d?: Date): string { + return d === undefined ? "none" : d.toISOString(); +} +function withDefault(d: Date = new Date(0)): number { + return d.getTime(); +} +console.log(stamp(), stamp(base), withDefault(), withDefault(base) === base.getTime()); + +// Collections. +const notes = new Map([[base, "base"]]); +notes.set(copy, "copy"); +console.log(notes.get(base), notes.get(copy), notes.get(equalTime), notes.size); +const byName = new Map([["start", base]]); +console.log(byName.get("start") === base, byName.get("end")); +const seen = new Set([base, same, copy]); +console.log(seen.size, seen.has(base), seen.has(equalTime)); + +// Records, tuples, classes and closures. +interface Event { + name: string; + at: Date; + ends?: Date; +} +const events: Event[] = [ + { name: "a", at: base }, + { name: "b", at: copy, ends: base }, +]; +console.log(events.map((e) => `${e.name}@${e.at.toISOString()} ends=${e.ends?.toISOString() ?? "-"}`)); +console.log(events[1]!.ends === events[0]!.at, events[1]!.at === events[0]!.at); +const pair: [Date, string] = [base, "x"]; +console.log(pair[0] === base, pair[1]); +class Window { + from: Date; + to: Date | null = null; + later!: Date; + constructor(from: Date) { + this.from = from; + } + span(): number { + return this.to === null ? 0 : this.to.getTime() - this.from.getTime(); + } +} +const w = new Window(base); +w.later = copy; +console.log(w.span(), w.later === copy); +w.to = new Date(base.getTime() + 60000); +console.log(w.span(), w.to === w.from); +let captured: Date | undefined; +const remember = (d: Date): void => { + captured = d; +}; +remember(base); +console.log(captured === base, captured?.getUTCDate()); + +// Async results, generators and unknown. +async function later(d: Date): Promise { + await null; + return new Date(d.getTime() + 1); +} +const next = await later(base); +console.log(next.getTime() - base.getTime(), next === base); +function* ticks(start: Date, n: number): Generator { + for (let i = 0; i < n; i++) yield new Date(start.getTime() + i * 1000); +} +console.log([...ticks(base, 3)].map((d) => d.getUTCSeconds())); +const held: unknown = base; +console.log((held as Date) === base, held instanceof Date, String(held) === String(base)); +const bag: unknown = { at: base, list: dates }; +console.log(JSON.stringify(bag)); +console.log(JSON.stringify(events), JSON.stringify([base, new Date(NaN)])); +const byKey: { [k: string]: Date } = { boot: base }; +console.log(byKey["boot"] === base, byKey["missing"], Object.keys(byKey)); diff --git a/tests/corpus/5966-date-time-values.ts b/tests/corpus/5966-date-time-values.ts new file mode 100644 index 000000000..342b4662c --- /dev/null +++ b/tests/corpus/5966-date-time-values.ts @@ -0,0 +1,80 @@ +// Date locals that are only read for their time value (method receivers, +// String() and template operands, relational and string `+` operands, +// `new Date(d)`) print and compare as Dates; locals whose identity a use +// can observe keep it. +export {}; +process.env.TZ = "America/New_York"; + +function describe(ms: number): string { + const d = new Date(ms); + const later = new Date(ms + 3_600_000); + const copy = new Date(d); + return [ + d.toISOString(), + d.toUTCString(), + String(d), + `${later}`, + "at " + d, + d < later, + later >= d, + d.getHours(), + d.getUTCDay(), + d.getTimezoneOffset(), + copy.getTime() === d.getTime(), + d.toJSON(), + ].join(" | "); +} +console.log(describe(Date.UTC(2026, 2, 8, 6, 30))); +console.log(describe(Date.UTC(2026, 10, 1, 5, 30))); +function invalid(): string { + const d = new Date(NaN); + return [String(d), d.getTime(), String(d.toJSON()), d < new Date(0), `${d}`].join(" | "); +} +console.log(invalid()); + +function sum(days: number): number { + let total = 0; + for (let i = 0; i < days; i++) { + const day = new Date(Date.UTC(2026, 0, 1) + i * 86_400_000); + total += day.getUTCDate() + day.getUTCMonth() + day.toISOString().length; + } + return total; +} +console.log(sum(400)); + +// Identity-observing uses keep the Date object. +const kept: Date[] = []; +function identities(ms: number): string { + const a = new Date(ms); + const b = a; + const stored = new Date(ms); + kept.push(stored); + const captured = new Date(ms); + const read = (): number => captured.getTime(); + const passed = new Date(ms); + return [a === b, kept.includes(stored), kept[0] === stored, read(), same(passed, passed)].join(","); +} +function same(x: Date, y: Date): boolean { + return x === y; +} +console.log(identities(5)); +const held: unknown = kept[0]; +console.log(held instanceof Date, (held as Date) === kept[0]); + +// Dates in blocks and loops at module level, read for their time value or +// kept, stored and captured. +const moduleKept: Date[] = []; +let moduleTotal = ""; +for (let i = 0; i < 3; i++) { + const read = new Date(Date.UTC(2026, 2, 8 + i, 6, 30)); + const kept = new Date(read.getTime()); + moduleKept.push(kept); + moduleTotal += `${read.toISOString()} ${read.getHours()} ${String(read)}\n`; +} +{ + const block = new Date(Date.UTC(2026, 10, 1, 5, 59)); + const captured = new Date(block); + const later = (): string => captured.toISOString(); + moduleTotal += `${block.getTimezoneOffset()} ${later()} ${captured === moduleKept[0]}`; +} +console.log(moduleTotal, moduleKept.map((d) => d.getUTCDate()).join(",")); diff --git a/tests/corpus/5967-date-alias-mutation/clock.js b/tests/corpus/5967-date-alias-mutation/clock.js new file mode 100644 index 000000000..75c89bee5 --- /dev/null +++ b/tests/corpus/5967-date-alias-mutation/clock.js @@ -0,0 +1,6 @@ +// A JavaScript module mutates the Dates it is handed (Date setters run on +// the checked-dynamic Date object). +export function advance(date, ms) { + date.setTime(date.getTime() + ms); + return date; +} diff --git a/tests/corpus/5967-date-alias-mutation/main.ts b/tests/corpus/5967-date-alias-mutation/main.ts new file mode 100644 index 000000000..f99580c45 --- /dev/null +++ b/tests/corpus/5967-date-alias-mutation/main.ts @@ -0,0 +1,22 @@ +// A Date local initialized from an existing Date is another reference to +// the same object: a mutation through the other reference (a JavaScript +// module's setTime) shows through the local. A relational comparison reads +// both operands' times after both operands evaluate, so a right operand +// that mutates the left Date is seen. +import { advance } from "./clock.js"; + +function aliases(start: Date): string { + const alias = start; + advance(start, 60_000); + const stamp = alias.toISOString(); + const other = new Date(start.getTime() + 1); + const moved = start < (advance(start, 5), other); + const after = start <= (advance(start, 10), other); + return `${stamp} ${alias.getTime() === start.getTime()} ${moved} ${after}`; +} +console.log(aliases(new Date(Date.UTC(2026, 0, 15, 12)))); + +const fresh = new Date(0); +const copy = new Date(fresh); +advance(fresh, 1000); +console.log(fresh.getTime(), copy.getTime(), fresh < copy, copy < fresh); diff --git a/tests/corpus/6067-view-holes-in-typed-arrays.ts b/tests/corpus/6067-view-holes-in-typed-arrays.ts new file mode 100644 index 000000000..831b3be7a --- /dev/null +++ b/tests/corpus/6067-view-holes-in-typed-arrays.ts @@ -0,0 +1,115 @@ +// Holes written through an `unknown[]` or `any` view of a typed array are +// holes of the typed array: for...of visits them as undefined and the find +// family passes them to its callback, as in Node. The views reach the array +// through a binding, an `any` parameter, a generic instantiated with +// `unknown`, a record field, a class field and a nested array. + +const numbers: number[] = [1, 2]; +const numbersView: unknown[] = numbers; +numbersView[3] = 5; +for (const value of numbers) console.log("numbers", value, typeof value); +console.log(numbers.findIndex((value) => value === undefined)); +console.log(numbers.findLastIndex((value) => value === undefined)); +console.log(numbers.find((value) => value !== undefined && value > 1)); + +const grown: string[] = ["a"]; +const grownView: unknown[] = grown; +grownView.length = 3; +for (const value of grown) console.log("grown", value); + +function growAny(values: any): void { + values[2] = 9; +} +const passed: number[] = [7]; +growAny(passed); +for (const value of passed) console.log("passed", value); + +function keep(value: T): T { + return value; +} +const kept: number[] = [3]; +(keep(kept) as unknown[])[2] = 4; +for (const value of kept) console.log("kept", value); + +const boxed: number[] = [1]; +const box: unknown = { values: boxed }; +(box as { values: unknown[] }).values[2] = 3; +for (const value of boxed) console.log("boxed", value); + +class Holder { + values: number[] = [1, 2]; +} +const holder = new Holder(); +const holderView: unknown = holder; +(holderView as { values: unknown[] }).values[3] = 4; +for (const value of holder.values) console.log("holder", value); + +const nested: number[][] = [[1]]; +const nestedView: unknown[][] = nested; +nestedView[0]![2] = 3; +for (const value of nested[0]!) console.log("nested", value); + +// A named property written through a view is not an element. +const named: number[] = [1, 2]; +const bag = named as unknown as Record; +bag["label"] = "x"; +for (const value of named) console.log("named", value); +console.log(named.length, bag["label"]); + +// Views that reach dynamic code through class field initializers, +// parameter defaults, logical assignments, yield and throw, and through +// standard library calls that keep their argument or pass it to a +// callback: Promise.resolve, push onto an `unknown[]`, Map.prototype.set +// and a JSON.stringify replacer. +const fieldNumbers: number[] = [1, 2]; +class FieldView { + view: unknown[] = fieldNumbers; +} +new FieldView().view[3] = 4; +for (const value of fieldNumbers) console.log("field", value); +const defaultNumbers: number[] = [1]; +function extend(view: unknown[] = defaultNumbers): void { + view.length = 3; +} +extend(); +console.log(defaultNumbers.findIndex((value) => value === undefined), defaultNumbers.length); +let assigned: unknown[] | undefined; +const assignedNumbers: number[] = [5]; +assigned ??= assignedNumbers; +assigned[2] = 6; +for (const value of assignedNumbers) console.log("assigned", value); +const yieldedNumbers: number[] = [7]; +function* yieldViews(): Generator { + yield yieldedNumbers; +} +for (const view of yieldViews()) view[2] = 8; +console.log(yieldedNumbers.findLastIndex((value) => value === undefined), yieldedNumbers.length); +const thrownNumbers: number[] = [9]; +try { + throw thrownNumbers; +} catch (error) { + (error as unknown[])[2] = 10; +} +for (const value of thrownNumbers) console.log("thrown", value); +const retainedNumbers: number[] = [12]; +const retained: unknown[] = []; +retained.push(retainedNumbers); +(retained[0] as unknown[])[2] = 13; +for (const value of retainedNumbers) console.log("kept", value); +const mappedNumbers: number[] = [14]; +const byName = new Map(); +byName.set("a", mappedNumbers); +(byName.get("a") as unknown[])[2] = 15; +console.log(mappedNumbers.findIndex((value) => value === undefined), mappedNumbers.length); +const replacedNumbers: number[] = [16]; +JSON.stringify({ replacedNumbers: replacedNumbers }, (key, value: unknown) => { + if (key === "replacedNumbers") (value as unknown[])[2] = 17; + return value; +}); +for (const value of replacedNumbers) console.log("replaced", value); +const resolvedNumbers: number[] = [11]; +void Promise.resolve(resolvedNumbers).then((view) => { + (view as unknown[])[2] = 18; + for (const value of resolvedNumbers) console.log("resolved", value); +}); + diff --git a/tests/corpus/6068-foreign-elements-through-views.ts b/tests/corpus/6068-foreign-elements-through-views.ts new file mode 100644 index 000000000..1b23f6fa7 --- /dev/null +++ b/tests/corpus/6068-foreign-elements-through-views.ts @@ -0,0 +1,202 @@ +// A number[] or string[] seen as `unknown[]` holds the values written +// through the view that its element type cannot hold, as in Node. The view +// and the typed array are the same array: length, the view's reads and +// iteration, printing, JSON, join, searches, copies, Math over a spread, +// sorting and truncation see the written values. + +const numbers: number[] = [1, 2]; +const numbersView: unknown[] = numbers; +numbersView.push("x"); +numbersView[0] = true; +console.log(numbers.length, numbersView.length, numbers === numbersView); +console.log(numbersView[0], numbersView[2], typeof numbersView[2]); +console.log(numbers, String(numbers), numbers.join("|"), JSON.stringify(numbers)); +for (const value of numbersView) console.log("value", value, typeof value); +console.log(numbers.indexOf(2), numbers.includes(2)); +console.log([...numbers], numbers.slice(1), numbers.slice(1, 2), numbers.concat([9])); + +// Math over a spread applies ToNumber to every element, as in Node. +const digits: number[] = [5]; +const digitsView: unknown[] = digits; +digitsView.push("4", null); +console.log(Math.max(...digits), Math.min(...digits), Math.hypot(...digits)); + +// Present undefined, null and objects keep their own values. +const mixed: number[] = [1]; +const mixedView: unknown[] = mixed; +mixedView.push(undefined, null, { a: 1 }, [2, 3]); +console.log(mixed, mixed.length, 1 in mixed, mixedView[2] === null); +console.log(JSON.stringify(mixed), mixed.join("-")); + +// Writing a number over a foreign element makes it a number again. +const replaced: number[] = [1, 2]; +const replacedView: unknown[] = replaced; +replacedView.push("x"); +replaced[2] = 5; +console.log(replaced, replacedView, replaced[2] + 1); + +// Default sort orders every element by its string, as in Node. +const sorted: number[] = [10, 9, 1]; +const sortedView: unknown[] = sorted; +sortedView.push("b", "a"); +console.log(sorted.sort(), sorted.length, sortedView[4]); + +// Truncation drops written values; growing again leaves holes. +const truncated: number[] = [1]; +const truncatedView: unknown[] = truncated; +truncatedView.push("x", "y"); +truncated.length = 1; +truncated.length = 3; +console.log(truncated, truncatedView.length); + +// string[] holds values of other types the same way. +const words: string[] = ["a"]; +const wordsView: unknown[] = words; +wordsView.push(1, false); +console.log(words, words.join("-"), words.length, typeof wordsView[1]); + +// A conversion that changes the array does not change the spread's values, +// and toSorted reads holes as undefined. +const grown: number[] = [3, 1]; +const grownView: unknown[] = grown; +grownView.push({ + valueOf: () => { + for (let i = 0; i < 64; i++) grown.push(i); + grown.length = 1; + return 2; + }, +}); +console.log(Math.max(...grown), Math.min(...grown), grown.length); +const holey: string[] = ["c", "a"]; +const holeyView: unknown[] = holey; +holeyView[4] = 7; +holeyView[6] = undefined; +console.log(holey.toSorted(), holey.length); +holey.sort(); +console.log(holey, holey.length); + +// Conversions during an in-place sort that change the array's length: +// elements added past the sorted length stay. +const appended: number[] = [3, 1, 2]; +const appendedView: unknown[] = appended; +let appendedCalls = 0; +appendedView[1] = { + toString(): string { + if (appendedCalls++ === 0) appended.push(7, 8); + return "1"; + }, +}; +appended.sort(); +console.log(appended.length, appended.slice(3), String(appendedView[0]), appended[2]); +const reduced: number[] = [3, 1, 2]; +const reducedView: unknown[] = reduced; +reducedView[4] = 4; +let reducedCalls = 0; +reducedView[1] = { + toString(): string { + if (reducedCalls++ === 0) reduced.length = 1; + return "1"; + }, +}; +reduced.sort(); +console.log(reduced.length, 3 in reduced, 4 in reduced, reduced[1], reduced[2]); +const padded: number[] = [3, 1, 2]; +const paddedView: unknown[] = padded; +paddedView.length = 5; +let paddedCalls = 0; +paddedView[1] = { + toString(): string { + if (paddedCalls++ === 0) padded.push(9); + return "1"; + }, +}; +padded.sort(); +console.log(padded.length, 3 in padded, 4 in padded, padded[5]); +const spread: number[] = [4, 3]; +const spreadView: unknown[] = spread; +let spreadCalls = 0; +spreadView.push({ + toString(): string { + if (spreadCalls++ === 0) { + spreadView[1000] = 1; + for (let i = 0; i < 64; i++) spread.push(i); + } + return "2"; + }, +}); +console.log(spread.toSorted().length, spread.length); +spread.sort(); +console.log(spread.length, spread[0], spread[1], spread[3], 999 in spread, spread[1000]); +const grownMath: number[] = [1]; +const grownMathView: unknown[] = grownMath; +grownMathView[10] = { + valueOf(): number { + grownMathView[5000] = 7; + for (let i = 0; i < 256; i++) grownMath.push(i); + return 3; + }, +}; +console.log(Math.max(...grownMath), grownMath.length); + +// A conversion that removes the element it converts. +const shrinking: number[] = [1]; +const shrinkingView: unknown[] = shrinking; +shrinkingView.push({ + toString: () => { + shrinking.length = 0; + return "gone"; + }, +}); +console.log(shrinking.join("+"), shrinking.length); +const cut: string[] = ["a", "b"]; +const cutView: unknown[] = cut; +cutView.splice(1, 0, { toString: () => ((cut.length = 0), "x") }); +console.log(JSON.stringify(cut.join("-")), cut.length); + +// JSON of elements whose getters change the array or record holding them. +const fillers: number[][] = []; +const rows: number[][] = [[1], [2]]; +const rowView: unknown[] = rows[0]!; +rowView.push({ + get grown() { + for (let i = 0; i < 64; i++) rows.push([i]); + rows.length = 3; + // Reuse the memory the old storage held. + for (let i = 0; i < 64; i++) fillers.push([i, i]); + return true; + }, +}); +console.log(JSON.stringify(rows), rows.length); +const holder = { values: [5] as number[], name: "h" }; +const holderView: unknown[] = holder.values; +holderView.push({ + get replaced() { + holder.values = [9]; + for (let i = 0; i < 64; i++) fillers.push([i]); + return true; + }, +}); +console.log(JSON.stringify(holder), JSON.stringify(holder)); + +// toSorted keeps the full length and reads holes as undefined; indexOf, +// lastIndexOf and includes skip the undefined and foreign elements. +const searched: string[] = ["b", "a"]; +const searchedView: unknown[] = searched; +searchedView[9] = 4; +searchedView[5] = undefined; +console.log(searched.indexOf("b"), searched.lastIndexOf("b"), searched.lastIndexOf("a", 8)); +console.log(searched.includes("a"), searched.lastIndexOf("z")); +const searchedSorted = searched.toSorted(); +console.log(searchedSorted.length, searchedSorted.slice(0, 4), searchedSorted.lastIndexOf("b")); +console.log(searchedSorted.indexOf("a"), searchedSorted.includes("b")); +const counts: number[] = [3, 1]; +const countsView: unknown[] = counts; +countsView[6] = "x"; +countsView[4] = 2; +const countsSorted = counts.toSorted(); +console.log(countsSorted.length, countsSorted, countsSorted.lastIndexOf(3), counts.lastIndexOf(2)); +const far: number[] = [5]; +const farView: unknown[] = far; +farView[70000] = "z"; +const farSorted = far.toSorted(); +console.log(farSorted.length, farSorted.slice(0, 3), farSorted.slice(69999), farSorted.lastIndexOf(5)); diff --git a/tests/corpus/6069-foreign-element-writers.ts b/tests/corpus/6069-foreign-element-writers.ts new file mode 100644 index 000000000..c3accaa09 --- /dev/null +++ b/tests/corpus/6069-foreign-element-writers.ts @@ -0,0 +1,54 @@ +// Values of other types written into typed arrays by code that sees them as +// `any` or `unknown[]`: an `any` parameter, a record field, methods that +// reorder the whole array through the view, and an `unknown[]` parameter. The typed +// array keeps every written value, and the view reads them back. + +function label(values: any): void { + values.push("label"); + values[0] = "first"; +} +const labelled: number[] = [1, 2]; +label(labelled); +console.log(labelled, labelled.length, JSON.stringify(labelled)); + +interface Box { + values: unknown[]; +} +const boxed: number[] = [1, 2, 3]; +const box: Box = { values: boxed }; +box.values[1] = "two"; +box.values.push(null); +console.log(boxed, box.values === boxed); + +// reverse and sort through the view rewrite the typed array. +const filled: number[] = [1, 2, 3, 4]; +const filledView: unknown[] = filled; +filledView[1] = "z"; +filledView[2] = "z"; +console.log(filled); +filledView.reverse(); +console.log(filled, filled[0], filled[3]); +filledView.sort(); +console.log(filled); + +function setAt(values: unknown[], index: number, value: unknown): void { + values[index] = value; +} +const assigned: number[] = [1, 2]; +setAt(assigned, 1, "b"); +setAt(assigned, 3, "d"); +console.log(assigned, assigned.length); + +// The view's own methods and iteration see the same elements. +const shared: number[] = [1, 2]; +const sharedView: unknown[] = shared; +sharedView.push("x"); +console.log(sharedView.map((value) => typeof value).join(",")); +console.log(sharedView.filter((value) => typeof value === "number").length); +for (const value of sharedView) console.log("view", value); +for (const key in shared) console.log("key", key); +console.log(Object.keys(sharedView), sharedView.indexOf("x"), sharedView.includes("x")); + +// Writing a number through the view stores an ordinary element. +sharedView[2] = 3; +console.log(shared, shared[2] + 1); diff --git a/tests/corpus/6305-json-discriminant-dispatch.ts b/tests/corpus/6305-json-discriminant-dispatch.ts new file mode 100644 index 000000000..ac4d992da --- /dev/null +++ b/tests/corpus/6305-json-discriminant-dispatch.ts @@ -0,0 +1,123 @@ +// JSON cast into a union with many string discriminant literals: the cast +// reads the discriminant once and tests only the arm whose literal it +// holds, plus the arms that are not records. An arm can list several +// literals, and values that are not objects reach the other arms in +// canonical order. Valid casts only (see 1002-json-parse-cast.ts); JSON keys +// and record fields alphabetical for stringify parity. + +type Expr = + | { kind: "num"; value: number } + | { kind: "str"; text: string } + | { kind: "neg"; operand: Expr } + | { kind: "add" | "mul"; left: Expr; right: Expr } + | { args: Expr[]; kind: "call"; name: string } + | { kind: "method"; name: string; target: Expr } + | { kind: "list"; items: Expr[] }; + +function show(e: Expr): string { + switch (e.kind) { + case "num": + return String(e.value); + case "str": + return JSON.stringify(e.text); + case "neg": + return `-(${show(e.operand)})`; + case "add": + return `(${show(e.left)} + ${show(e.right)})`; + case "mul": + return `(${show(e.left)} * ${show(e.right)})`; + case "call": + return `${e.name}(${e.args.map(show).join(", ")})`; + case "method": + return `${show(e.target)}.${e.name}()`; + case "list": + return `[${e.items.map(show).join(", ")}]`; + } +} + +function evaluate(e: Expr): number { + switch (e.kind) { + case "num": + return e.value; + case "str": + return e.text.length; + case "neg": + return -evaluate(e.operand); + case "add": + return evaluate(e.left) + evaluate(e.right); + case "mul": + return evaluate(e.left) * evaluate(e.right); + case "call": + return e.args.reduce((sum, arg) => sum + evaluate(arg), 0); + case "method": + return evaluate(e.target); + case "list": + return e.items.length; + } +} + +const source = + '{"kind":"add","left":{"kind":"mul","left":{"kind":"num","value":3},' + + '"right":{"kind":"neg","operand":{"kind":"num","value":4}}},' + + '"right":{"args":[{"kind":"str","text":"abc"},{"kind":"num","value":2}],"kind":"call","name":"sum"}}'; +const tree = JSON.parse(source) as Expr; +console.log(show(tree)); +console.log(evaluate(tree)); +console.log(JSON.stringify(tree) === source); + +const method = JSON.parse( + '{"kind":"method","name":"size","target":{"items":[{"kind":"num","value":1}],"kind":"list"}}', +) as Expr; +console.log(show(method), evaluate(method)); + +// A deep list of every arm, built from text. +const parts: string[] = []; +for (let i = 0; i < 200; i++) { + parts.push( + i % 3 === 0 + ? `{"kind":"num","value":${i}}` + : i % 3 === 1 + ? `{"kind":"neg","operand":{"kind":"num","value":${i}}}` + : `{"kind":"mul","left":{"kind":"num","value":${i}},"right":{"kind":"str","text":"xy"}}`, + ); +} +const list = JSON.parse(`{"items":[${parts.join(",")}],"kind":"list"}`) as Expr; +if (list.kind === "list") { + let total = 0; + for (const item of list.items) total += evaluate(item); + console.log(list.items.length, total); +} + +// Arms that are not records: a value that is not an object can only be one +// of them. +type Token = + | { kind: "ident"; name: string } + | { kind: "number"; value: number } + | { kind: "string"; value: string } + | { kind: "punct"; symbol: string } + | { kind: "comment"; line: number; text: string } + | string + | number; + +function describe(token: Token): string { + if (typeof token === "string") return `raw ${token}`; + if (typeof token === "number") return `count ${token}`; + switch (token.kind) { + case "ident": + return `ident ${token.name}`; + case "number": + return `number ${token.value}`; + case "string": + return `string ${JSON.stringify(token.value)}`; + case "punct": + return `punct ${token.symbol}`; + case "comment": + return `comment ${token.line}: ${token.text}`; + } +} + +const tokens = JSON.parse( + '[{"kind":"ident","name":"x"},{"kind":"punct","symbol":"="},{"kind":"number","value":42},' + + '{"kind":"string","value":"hi"},{"kind":"comment","line":3,"text":"note"},"spare",7]', +) as Token[]; +for (const token of tokens) console.log(describe(token)); diff --git a/tests/corpus/6306-discriminant-getter-dispatch.ts b/tests/corpus/6306-discriminant-getter-dispatch.ts new file mode 100644 index 000000000..4f0a1244f --- /dev/null +++ b/tests/corpus/6306-discriminant-getter-dispatch.ts @@ -0,0 +1,55 @@ +// A checked cast into a union with many string discriminant literals reads +// the discriminant once, but a getter that runs while an arm's fields are +// checked can change it. The cast then continues with the arms after the +// one that failed, testing each arm's literal again, and matches the arm +// the new discriminant names. Each getter changes the discriminant on its +// first call only; the program reads the getter once itself, so Node (where +// the cast runs no code) reaches the same state. Valid casts only (see +// 1002-json-parse-cast.ts). + +type Shape = + | { kind: "circle"; radius: number } + | { kind: "square"; side: number } + | { kind: "rect"; height: number; width: number } + | { kind: "tri"; a: number; b: number; c: number }; + +function area(shape: Shape): number { + switch (shape.kind) { + case "circle": + return Math.round(Math.PI * shape.radius * shape.radius); + case "square": + return shape.side * shape.side; + case "rect": + return shape.width * shape.height; + case "tri": { + const s = (shape.a + shape.b + shape.c) / 2; + return Math.sqrt(s * (s - shape.a) * (s - shape.b) * (s - shape.c)); + } + } +} + +// The circle arm reads `radius`, whose getter turns the value into a square. +let squareReads = 0; +const square: any = { + kind: "circle", + get radius() { + squareReads++; + if (squareReads === 1) square.kind = "square"; + return "none"; + }, + side: 3, +}; +const asSquare = square as Shape; +void square.radius; +console.log(square.kind, asSquare.kind, area(asSquare)); + +// Values without getters take the arm their discriminant names. +const plain: any[] = [ + { kind: "rect", width: 2, height: 5 }, + { kind: "tri", a: 3, b: 4, c: 5 }, + { kind: "circle", radius: 2 }, +]; +for (const value of plain) { + const shape = value as Shape; + console.log(shape.kind, area(shape)); +} diff --git a/tests/corpus/6307-number-map-get-set-fusion.ts b/tests/corpus/6307-number-map-get-set-fusion.ts new file mode 100644 index 000000000..d373782ef --- /dev/null +++ b/tests/corpus/6307-number-map-get-set-fusion.ts @@ -0,0 +1,82 @@ +// A number-keyed Map read followed by a write of the same key: the write +// goes through the entry the read found when the map and the key's bits +// are the same, and through an ordinary set otherwise (a miss, -0 against +// 0, another map). Values, sizes and insertion order match Node for +// directly indexed keys, hashed keys, fractional keys, NaN and signed +// zeros. + +function show(map: Map): string { + const parts: string[] = []; + map.forEach((value, key) => { + parts.push(`${Object.is(key, -0) ? "-0" : String(key)}=${value}`); + }); + return `${map.size} [${parts.join(" ")}]`; +} + +// The counting update over small integer keys (direct index) and wide keys +// (hashed). +const keys = [3, 1, 3, 2, 1, 3, 1e12, 2 ** 40 + 1, 1e12, 0.5, 0.5, -7, 2 ** 40 + 1]; +const counts = new Map(); +for (const key of keys) counts.set(key, (counts.get(key) ?? 0) + 1); +console.log(show(counts)); + +// The read as a declaration, then the write. +const sums = new Map(); +for (let i = 0; i < 40; i++) { + const key = (i * 7) % 11; + const current = sums.get(key); + sums.set(key, current === undefined ? i : current + i); +} +console.log(show(sums)); + +// Signed zeros: -0 and 0 are one key, which the map stores as 0. +const zeros = new Map(); +const zeroKeys = [-0, 0, -0, 0, 0]; +for (const key of zeroKeys) zeros.set(key, (zeros.get(key) ?? 0) + 1); +console.log(show(zeros), Object.is([...zeros.keys()][0], -0)); +const plusFirst = new Map(); +for (const key of [0, -0, -0]) plusFirst.set(key, (plusFirst.get(key) ?? 0) + 1); +console.log(show(plusFirst)); + +// NaN is one key whatever its producer. +const nans = new Map(); +const nanKeys = [NaN, 0 / 0, Math.sqrt(-1), Number("x")]; +for (const key of nanKeys) nans.set(key, (nans.get(key) ?? 0) + 1); +console.log(show(nans)); + +// A read of one map and a write of another take the ordinary set. +const source = new Map([ + [1, 10], + [2, 20], +]); +const target = new Map([[2, 5]]); +for (const key of [1, 2, 3]) target.set(key, (source.get(key) ?? 0) + 1); +console.log(show(source), show(target)); + +// Overwrites keep the insertion order; deleted keys come back at the end. +const order = new Map(); +for (const key of [5, 4, 3, 2, 1]) order.set(key, (order.get(key) ?? 0) + 1); +order.delete(3); +for (const key of [1, 3, 5]) order.set(key, (order.get(key) ?? 0) + 10); +console.log(show(order)); + +// Boolean values. +const seen = new Map(); +for (const key of [1, 2, 1, 1e9, 1e9]) seen.set(key, !(seen.get(key) ?? false)); +const flags: string[] = []; +seen.forEach((value, key) => flags.push(`${key}:${value}`)); +console.log(flags.join(" ")); + +// A large table, where the hashed probe and growth run. +const big = new Map(); +for (let i = 0; i < 50000; i++) { + const key = (i * 2654435761) % 100003; + big.set(key, (big.get(key) ?? 0) + 1); +} +let total = 0; +let most = 0; +big.forEach((value) => { + total += value; + if (value > most) most = value; +}); +console.log(big.size, total, most); diff --git a/tests/corpus/6308-number-map-integer-keys.ts b/tests/corpus/6308-number-map-integer-keys.ts new file mode 100644 index 000000000..75efb3a72 --- /dev/null +++ b/tests/corpus/6308-number-map-integer-keys.ts @@ -0,0 +1,77 @@ +// Number-keyed Map and Set operations whose keys the compiler knows are +// integers (loop counters, `| 0` results, typed-array bytes, integer +// arithmetic) test the map's direct index on the integer. Results must +// match Node while the index covers the keys, after a key outside it moves +// the map to hashing, for negative keys and for keys past the index, and +// when a double key and an integer key name the same entry. + +const counts = new Map(); +for (let i = 0; i < 200; i++) { + const key = (i * 7) % 13; + counts.set(key, (counts.get(key) ?? 0) + 1); +} +const dense: string[] = []; +for (let k = -2; k < 16; k++) dense.push(`${k}:${counts.has(k) ? counts.get(k) : "-"}`); +console.log(dense.join(" ")); + +// A key past the direct index moves the map to hashing; integer reads still +// find every key. +counts.set(1_000_000, 5); +let found = 0; +for (let k = -2; k < 16; k++) if (counts.has(k)) found += counts.get(k)!; +console.log(found, counts.get(1_000_000), counts.has(999_999), counts.size); + +// Negative keys, and keys built with `| 0` and shifts. +const signed = new Map(); +for (let i = -5; i <= 5; i++) signed.set((i * 3) | 0, `v${i}`); +const reads: string[] = []; +for (let i = -16; i <= 16; i += 4) reads.push(`${i}:${signed.get(i) ?? "-"}`); +console.log(reads.join(" "), signed.get(-0), signed.get(0)); + +// Bytes of a typed array as keys, in a Set. +const bytes = new Uint8Array([3, 250, 3, 0, 17, 250, 255, 0]); +const seen = new Set(); +let repeats = 0; +for (let i = 0; i < bytes.length; i++) { + if (seen.has(bytes[i]!)) repeats++; + seen.add(bytes[i]!); +} +console.log([...seen].join(","), repeats); + +// A double key and an integer key name the same entry. +const mixed = new Map(); +mixed.set(2.0, 1); +mixed.set(0.5, 2); +for (let i = 0; i < 4; i++) mixed.set(i, (mixed.get(i) ?? 0) + 10); +console.log([...mixed.entries()].map(([k, v]) => `${k}=${v}`).join(" "), mixed.get(0.5)); + +// Deletion through integer keys, then reinsertion order. +const order = new Map(); +for (let i = 0; i < 10; i++) order.set(i, i * i); +for (let i = 0; i < 10; i += 3) order.delete(i); +for (let i = 9; i >= 0; i -= 2) order.set(i, -i); +console.log([...order.entries()].map(([k, v]) => `${k}=${v}`).join(" ")); + +// Large integer keys near 2^53, from integer arithmetic. +const wide = new Map(); +let key = 2 ** 52; +for (let i = 0; i < 5; i++) { + wide.set(key, i); + key = key * 2 - 1; +} +console.log([...wide.keys()].join(" "), wide.get(2 ** 52), wide.get(2 ** 53 - 1)); + +// A lookup whose key is an assignment of a wide integer constant, in a +// branch, then a lookup of the same constant after the branch. +const far = new Map(); +far.set(1099511627776, 7); +function probeFar(taken: boolean): number { + let k = 0; + let hits = 0; + if (taken) { + if (far.has((k = 1099511627776))) hits += 1; + } + hits += far.get(1099511627776) ?? -1; + return hits + k; +} +console.log(probeFar(true), probeFar(false)); diff --git a/tests/corpus/6310-small-number-map-direct-index.ts b/tests/corpus/6310-small-number-map-direct-index.ts new file mode 100644 index 000000000..5614a56a1 --- /dev/null +++ b/tests/corpus/6310-small-number-map-direct-index.ts @@ -0,0 +1,77 @@ +// Number-keyed Maps and Sets with two to four small integer keys use the +// direct index. Results must match Node through overwrites, deletes and +// re-adds, a fractional, negative or large key that moves the map to +// hashing, clear, and mutation during a for-of or forEach walk. + +const hist = new Map(); +const codes = [0, 3, 1, 3, 2, 0, 3, 3, 1]; +for (const c of codes) hist.set(c, (hist.get(c) ?? 0) + 1); +console.log([...hist].map(([k, v]) => `${k}:${v}`).join(" "), hist.size, hist.has(4), hist.get(-0)); + +// Delete and re-add: the re-added key moves to the end. +hist.delete(3); +hist.set(3, 9); +hist.delete(0); +console.log([...hist.keys()].join(","), hist.get(3), hist.has(0)); + +// A key the direct index cannot hold moves the map to hashing. +for (const odd of [0.5, -1, 2 ** 40]) { + const m = new Map([ + [1, "a"], + [2, "b"], + ]); + m.set(odd, "c"); + m.set(1, "A"); + console.log([...m].map(([k, v]) => `${k}=${v}`).join(" "), m.get(2), m.get(odd), m.size); +} + +// Two keys, then clear, then reuse. +const pair = new Map(); +pair.set(5, 50); +pair.set(7, 70); +pair.clear(); +pair.set(7, 1); +pair.set(1, 2); +console.log([...pair].map(([k, v]) => `${k}=${v}`).join(" "), pair.size); + +// A walk that deletes ahead and adds keys. +const walk = new Map([ + [0, 0], + [1, 10], + [2, 20], +]); +const seen: number[] = []; +for (const [k, v] of walk) { + seen.push(v); + if (k === 0) { + walk.delete(2); + walk.set(3, 30); + } + if (k === 3) walk.set(0, -1); +} +console.log(seen.join(","), [...walk].map(([k, v]) => `${k}=${v}`).join(" ")); + +// Sets of small integers, forEach with deletion and addition. +const set = new Set([2, 0]); +set.add(1); +set.add(2); +const order: number[] = []; +set.forEach((v) => { + order.push(v); + if (v === 2) { + set.delete(1); + set.add(3); + } +}); +console.log(order.join(","), [...set].join(","), set.has(1), set.size); + +// Many small maps, each with a few keys. +let total = 0; +for (let i = 0; i < 1000; i++) { + const m = new Map(); + m.set(i % 3, i); + m.set((i + 1) % 3, 1); + if (i % 5 === 0) m.delete(i % 3); + total += (m.get(i % 3) ?? 100) + (m.get((i + 1) % 3) ?? 0) + m.size; +} +console.log(total); diff --git a/tests/corpus/6311-typed-array-integer-products.ts b/tests/corpus/6311-typed-array-integer-products.ts new file mode 100644 index 000000000..28a074a4a --- /dev/null +++ b/tests/corpus/6311-typed-array-integer-products.ts @@ -0,0 +1,83 @@ +// Products and sums of typed-array element reads that only reach bitwise +// operators compute in 32-bit integers. Results must match Node for reads +// past the end (NaN, which converts to 0), negative factors, products that +// can be -0, sums that wrap past int32, and products near 2^53. + +const rgba = new Uint8Array([10, 20, 30, 255, 200, 100, 50, 0, 255, 255, 255, 128, 0, 0, 0, 1]); +const pixels = rgba.length / 4; +const gray = new Uint8Array(pixels); +for (let i = 0; i < gray.length; i++) { + const j = i * 4; + gray[i] = (rgba[j]! * 77 + rgba[j + 1]! * 150 + rgba[j + 2]! * 29) >> 8; +} +console.log("gray", gray.join(",")); + +// Reads past the end are NaN: the sum is NaN and converts to 0. +const tail: number[] = []; +for (let i = pixels - 2; i < pixels + 2; i++) { + const j = i * 4; + tail.push((rgba[j]! * 77 + rgba[j + 1]! * 150 + rgba[j + 2]! * 29) >> 8); + tail.push((rgba[j + 3]! * 3 - rgba[j]!) | 0); +} +console.log("tail", tail.join(",")); + +// Signed elements and negative factors, including products that are -0. +const signed = new Int8Array([0, -1, 1, -128, 127, 0, 5, -5]); +const mixed: number[] = []; +for (let i = 0; i < signed.length; i++) { + mixed.push((signed[i]! * -3) | 0); + mixed.push((signed[i]! * signed[(i + 1) % signed.length]! + signed[i]! * 1000) >> 1); + mixed.push(Object.is(signed[i]! * -1, -0) ? 1 : 0); +} +console.log("signed", mixed.join(",")); + +// Unsigned 16-bit products whose sum passes int32 and wraps. +const wide = new Uint16Array([65535, 65535, 1, 40000, 50000, 12345]); +const wrapped: number[] = []; +for (let i = 0; i + 1 < wide.length; i++) { + wrapped.push((wide[i]! * wide[i + 1]! + wide[i]! * wide[i]!) | 0); + wrapped.push((wide[i]! * wide[i + 1]!) >>> 0); + wrapped.push((wide[i]! * 65537 - wide[i + 1]!) ^ 0x5a5a); +} +console.log("uint16", wrapped.join(",")); + +// Unsigned 32-bit elements times constants just below and above 2^53. +const big = new Uint32Array([0xffffffff, 0x80000000, 3, 0]); +const near: number[] = []; +for (let i = 0; i < big.length; i++) { + near.push((big[i]! * 2097151) | 0); + near.push((big[i]! * 2097153) | 0); + near.push((big[i]! * 2097151 + big[(i + 1) % big.length]!) >>> 0); +} +console.log("uint32", near.join(",")); + +// A float element is never an exact integer. +const floats = new Float64Array([1.5, -2.25, 3]); +const fractional: number[] = []; +for (let i = 0; i < floats.length; i++) fractional.push((floats[i]! * 3 + floats[i]! * 2) | 0); +console.log("float", fractional.join(",")); + +// Clamped bytes and a sum used as a number keep their double values. +const clamped = new Uint8ClampedArray([0, 128, 255]); +let total = 0; +for (let i = 0; i < clamped.length; i++) { + total += clamped[i]! * 77 + clamped[i]! * 0.5; + total += (clamped[i]! * 77 + clamped[(i + 1) % 3]! * 150) >> 8; +} +console.log("clamped", total); + +// A larger image, so the loop runs its vectorized body. +const image = new Uint8Array(4 * 1000); +for (let i = 0; i < image.length; i++) image[i] = (i * 37 + (i >> 3)) & 255; +const luma = new Uint8Array(1000); +for (let i = 0; i < luma.length; i++) { + const j = i * 4; + luma[i] = (image[j]! * 77 + image[j + 1]! * 150 + image[j + 2]! * 29) >> 8; +} +let sum = 0; +let mix = 0; +for (let i = 0; i < luma.length; i++) { + sum += luma[i]!; + mix = (mix * 31 + luma[i]!) | 0; +} +console.log("luma", sum, mix); diff --git a/tests/corpus/6312-typed-array-read-indices.ts b/tests/corpus/6312-typed-array-read-indices.ts new file mode 100644 index 000000000..be6789331 --- /dev/null +++ b/tests/corpus/6312-typed-array-read-indices.ts @@ -0,0 +1,87 @@ +// Locals assigned from integer typed-array reads and used as indices, loop +// starts and loop bounds. A read past the end is undefined, written here as +// NaN (`?? NaN`): an index of NaN reads undefined and writes nothing, and a +// loop that starts at NaN never runs. Results must match Node. + +// A permutation chain with in-place reversals, as fannkuch-redux does. +function flips(perm: Int32Array): number { + const pp = new Int32Array(perm.length); + pp.set(perm); + let count = 0; + let p0 = pp[0]!; + while (pp[p0] !== 0) { + count++; + for (let i = 1, j = p0 - 1; i < j; i++, j--) { + const t = pp[i]!; + pp[i] = pp[j]!; + pp[j] = t; + } + const t = pp[p0]!; + pp[p0] = p0; + p0 = t; + } + return count; +} +const perms = [ + [3, 1, 2, 0], + [2, 0, 3, 1, 4], + [4, 3, 2, 1, 0], + [1, 2, 3, 4, 5, 0], +]; +console.log("flips", perms.map((p) => flips(Int32Array.from(p))).join(",")); + +// Indices that leave the array: a value past the end, a negative value, and +// one above 2^31 in a Uint32Array. +const links = new Int32Array([2, 5, -1, 0, 1]); +const seen: string[] = []; +let at = links[0]!; +for (let step = 0; step < 6; step++) { + const next = links[at] ?? NaN; + seen.push(`${at}->${next}`); + links[at] = step; + at = next; +} +console.log("links", seen.join(" "), links.join(",")); + +const wide = new Uint32Array([1, 0xfffffff0, 3, 0]); +const hops: number[] = []; +let w = wide[0]!; +for (let step = 0; step < 4; step++) { + const v = wide[w] ?? NaN; + hops.push(v); + w = v; +} +console.log("uint32", hops.join(","), w); + +// A loop whose bounds come from reads, one of them past the end. +const bounds = new Uint8Array([5, 2, 9]); +const marks: number[] = []; +for (let k = 0; k < 4; k++) { + const top = bounds[k] ?? NaN; + let sum = 0; + for (let i = 0, j = top - 1; i < j; i++, j--) sum += i * 10 + j; + for (let i = top - 1; i >= 0; i -= 2) sum += i; + marks.push(sum); +} +console.log("bounds", marks.join(",")); + +// Sums and products of read locals used as indices. +const table = new Int16Array([3, -2, 7, 1, 0, 4, 6, 2]); +const picks: string[] = []; +for (let i = 0; i < table.length; i++) { + const a = table[i]!; + const b = table[(i + 1) % table.length]!; + picks.push(`${table[a + b] ?? -100}/${table[a * 2] ?? -100}/${table[b - a] ?? -100}`); +} +console.log("sums", picks.join(" ")); + +// A float element as an index keeps its double meaning. +const floats = new Float64Array([1.5, 2, 0]); +const out = new Int32Array([10, 20, 30]); +const read: string[] = []; +for (let i = 0; i < floats.length; i++) { + const f = floats[i]!; + read.push(String(out[f] ?? -1)); + out[f] = i; +} +console.log("floats", read.join(","), out.join(",")); diff --git a/tests/corpus/6343-inspect-class-values.ts b/tests/corpus/6343-inspect-class-values.ts new file mode 100644 index 000000000..5e9931c8c --- /dev/null +++ b/tests/corpus/6343-inspect-class-values.ts @@ -0,0 +1,31 @@ +// Classes reached through `unknown` print as Node prints them: +// `[class C]`, `[class C extends B]` with the runtime class's base, and +// `[class (anonymous)]`, at the top level and inside arrays and records. +import { inspect } from "node:util"; + +class A { + x = 1; +} +class B extends A { + y = 2; +} +class C extends B {} +class MyErr extends Error {} +const Anon = [class {}][0]!; +const Sub = class extends A {}; + +const a: unknown = A; +const b: unknown = B; +console.log(a, b); +console.log([a], { b }); +const values: unknown[] = [C, MyErr, Anon, Sub]; +console.log(values); +console.log(inspect({ deep: { values } }, { depth: 0 }), inspect(values, { depth: 0 })); +console.log(String(typeof values[0])); + +// A slot typed as the base holds a subclass at runtime. +const pick = (k: number): typeof A => (k === 0 ? A : k === 1 ? B : C); +for (let k = 0; k < 3; k++) { + const picked: unknown = pick(k); + console.log(k, picked); +} diff --git a/tests/corpus/6344-inspect-function-properties.cjs b/tests/corpus/6344-inspect-function-properties.cjs new file mode 100644 index 000000000..5cab04172 --- /dev/null +++ b/tests/corpus/6344-inspect-function-properties.cjs @@ -0,0 +1,62 @@ +'use strict'; +// Functions and classes reached as dynamic values print their enumerable +// own properties after the tag, as Node prints them: a class's static +// properties, properties assigned to a function, cycles through them, +// and `[Function]` past the depth limit, where a cycle still shows. +const util = require('util'); + +function f() {} +f.x = 1; +f.label = 'f'; +function g() {} +g.self = g; +function plain() {} +class A {} +class S extends A { + static get hidden() { + return 1; + } +} +S.count = 3; +function show(value) { + console.log(value); +} +show(f); +show(g); +show(S); +show(A); +show(plain); +function shallow(value) { + console.log(util.inspect({ value }, { depth: 0 })); +} +shallow(f); +shallow(S); +shallow(A); +console.log(util.inspect(g, { depth: 0 })); +shallow(g); + +// A class that reaches itself through its static properties is one cycle, +// whichever dynamic box of it the inspection meets. +class Loop { + static get hidden() { + return 1; + } +} +Loop.self = Loop; +Loop.label = 'loop'; +class Left { + static get hidden() { + return 2; + } +} +class Right { + static get hidden() { + return 3; + } +} +Left.other = Right; +Right.other = Left; +show(Loop); +show(Left); +show([Loop, Loop]); +show({ a: Left, b: Right }); diff --git a/tests/corpus/6357-format-o-hidden.ts b/tests/corpus/6357-format-o-hidden.ts new file mode 100644 index 000000000..44ed1d772 --- /dev/null +++ b/tests/corpus/6357-format-o-hidden.ts @@ -0,0 +1,35 @@ +// util.format's %o over values reached through `unknown` renders with +// showHidden, as Node does: arrays show their hidden `[length]` (with +// Node's grouping), at every depth, inside Maps and records too, ahead of +// an array's named properties. +import { format } from "node:util"; +const cases: [string, unknown][] = [ + ["ten", Array.from({ length: 10 }, (_, i) => i)], + ["wide", Array.from({ length: 120 }, (_, i) => i)], + ["hundred", Array.from({ length: 100 }, (_, i) => i)], + ["hundred-one", Array.from({ length: 101 }, (_, i) => i)], + ["hundred-strs", Array.from({ length: 100 }, (_, i) => "s" + i)], + ["seven", ["aaaaaaaaaaaaaaaaaaaa", "b", "c", "d", "e", "f", "g"]], + ["strs", Array.from({ length: 30 }, (_, i) => "s" + i)], + ["holes", [1, , 3]], + ["empty", []], + ["nested", { list: [[]], m: new Map([[1, [2]]]) }], + ["deep", [[[[[[1]]]]]]], + ["date", [new Date(0)]], +]; +for (const [k, v] of cases) console.log(k, format("%o", v)); +// The hidden `[length]` comes before an array's named properties, and the +// grid's last-entry rule counts both. +type Bag = Record; +const named = [1, 2]; +(named as unknown as Bag).foo = "x"; +const tagged = Array.from({ length: 100 }, (_, i) => i); +(tagged as unknown as Bag).tag = "t"; +const bare: number[] = []; +(bare as unknown as Bag).k = 1; +for (const [k, v] of [ + ["named", named], + ["tagged", tagged], + ["bare", bare], +] as [string, unknown][]) + console.log(k, format("%o", v)); diff --git a/tests/corpus/6358-format-o-hidden-js.cjs b/tests/corpus/6358-format-o-hidden-js.cjs new file mode 100644 index 000000000..df54b2714 --- /dev/null +++ b/tests/corpus/6358-format-o-hidden-js.cjs @@ -0,0 +1,9 @@ +'use strict'; +// %o shows an object's non-enumerable own properties as `[key]`, symbol +// keys included. +const util = require('util'); +const hidden = Object.defineProperty(JSON.parse('{"shown":1}'), 'secret', { value: [2], enumerable: false }); +Object.defineProperty(hidden, 'a-b', { value: 'x', enumerable: false }); +Object.defineProperty(hidden, Symbol('tag'), { value: 3, enumerable: false }); +console.log(util.format('%o and %O', hidden, hidden)); +console.log(util.format('%o', [hidden])); diff --git a/tests/corpus/6611-child-properties-across-modules/definer.cjs b/tests/corpus/6611-child-properties-across-modules/definer.cjs index 8b1c8f1a3..ea58bc046 100644 --- a/tests/corpus/6611-child-properties-across-modules/definer.cjs +++ b/tests/corpus/6611-child-properties-across-modules/definer.cjs @@ -21,5 +21,5 @@ module.exports = function run() { console.log(e instanceof TypeError, e.message.length, e.message.slice(0, 30), e.message.slice(-40)); } child.on("close", (code) => console.log("close", code, child.killedBy)); - child.kill("SIGTERM"); + child.stdout.once("data", () => child.kill("SIGTERM")); }; diff --git a/tests/corpus/6611-child-properties-across-modules/main.cjs b/tests/corpus/6611-child-properties-across-modules/main.cjs index a3f422c64..3dfb6756a 100644 --- a/tests/corpus/6611-child-properties-across-modules/main.cjs +++ b/tests/corpus/6611-child-properties-across-modules/main.cjs @@ -5,17 +5,24 @@ // and writes a getter-only property with a long name. const patched = require("./patcher.cjs"); const definer = require("./definer.cjs"); -const child = patched(); -console.log(child.label); -child.label = "x"; -console.log(child.stored, child.same); -try { - child.same = 1; -} catch (e) { - console.log(e instanceof TypeError, e.message); +if (process.argv.at(2) === "child") { + // The child reports that it started, then waits for the parent's SIGTERM. + // It exits after 30 seconds without one. + process.stdout.write("ready"); + setTimeout(() => {}, 30000); +} else { + const child = patched(); + console.log(child.label); + child.label = "x"; + console.log(child.stored, child.same); + try { + child.same = 1; + } catch (e) { + console.log(e instanceof TypeError, e.message); + } + child.on("close", (code) => { + console.log("close", code, child.killedBy); + }); + child.stdout.once("data", () => child.kill("SIGTERM")); + child.on("close", () => definer()); } -child.on("close", (code) => { - console.log("close", code, child.killedBy); -}); -child.kill("SIGTERM"); -child.on("exit", () => definer()); diff --git a/tests/corpus/6611-child-properties-across-modules/spawner.cjs b/tests/corpus/6611-child-properties-across-modules/spawner.cjs index d0c52d9b7..5701145a8 100644 --- a/tests/corpus/6611-child-properties-across-modules/spawner.cjs +++ b/tests/corpus/6611-child-properties-across-modules/spawner.cjs @@ -1,5 +1,5 @@ "use strict"; const { spawn } = require("child_process"); module.exports = function start() { - return spawn(process.execPath, ["-e", "process.stdout.write('ok')"]); + return spawn(process.execPath, [process.argv[1], "child"]); }; diff --git a/tests/corpus/6928-class-iterator-protocol.ts b/tests/corpus/6928-class-iterator-protocol.ts new file mode 100644 index 000000000..2c2eecccc --- /dev/null +++ b/tests/corpus/6928-class-iterator-protocol.ts @@ -0,0 +1,112 @@ +// for-of and spread over classes whose [Symbol.iterator]() returns an iterator object, +// with return() called on early exits as Node's IteratorClose does. +class Bag { + readonly items: string[]; + constructor(items: string[]) { this.items = items; } + [Symbol.iterator]() { + let i = 0; + const items = this.items; + return { + next(): IteratorResult { + return i < items.length ? { value: items[i++]!, done: false } : { value: undefined, done: true }; + }, + return(): IteratorResult { + console.log("return at", i); + return { value: undefined, done: true }; + }, + }; + } +} +const bag = new Bag(["a", "b", "c", "d"]); +function firstLong(): string | undefined { + for (const s of bag) if (s >= "b") return s; + return undefined; +} +console.log(firstLong()); +try { + for (const s of bag) if (s === "c") throw new Error("stop at " + s); +} catch (e) { + console.log((e as Error).message); +} +outer: for (const n of [1, 2]) { + for (const s of bag) { + if (s === "b") continue outer; + console.log(n, s); + } +} +let all = ""; +for (const s of bag) all += s; +console.log(all); + +class Counter { + n = 0; + [Symbol.iterator](): CounterIterator { return new CounterIterator(3); } +} +class CounterIterator { + i = 0; + readonly limit: number; + constructor(limit: number) { this.limit = limit; } + next(): { value: number; done: boolean } { + return this.i < this.limit ? { value: this.i++, done: false } : { value: -1, done: true }; + } + return(): { value: number; done: boolean } { + console.log("class return", this.i); + return { value: -1, done: true }; + } +} +for (const v of new Counter()) { if (v === 1) break; console.log("v", v); } +console.log([...new Counter()].join()); + +class Fussy { + [Symbol.iterator]() { + let i = 0; + return { + next(): IteratorResult { return { value: i++, done: i > 5 }; }, + return(): IteratorResult { throw new Error("return failed"); }, + }; + } +} +try { + for (const v of new Fussy()) if (v === 1) break; +} catch (e) { + console.log("break:", (e as Error).message); +} +try { + for (const v of new Fussy()) if (v === 1) throw new Error("body failed"); +} catch (e) { + console.log("throw:", (e as Error).message); +} + +class Range { + readonly n: number; + constructor(n: number) { this.n = n; } + [Symbol.iterator]() { + let i = 0; + const n = this.n; + return { + next(): IteratorResult { return i < n ? { value: i++, done: false } : { value: undefined, done: true }; }, + }; + } +} +let total = 0; +for (const v of new Range(4)) total += v; +console.log(total, [...new Range(3)].join()); + +// An object-literal iterator that replaces its next() while it runs: the +// loop and the spread keep calling the next() they read when it opened. +class Swapping { + [Symbol.iterator]() { + let i = 0; + const it = { + next(): { value: number; done: boolean } { + i++; + if (i === 2) it.next = () => ({ value: -1, done: true }); + return { value: i, done: i > 4 }; + }, + }; + return it; + } +} +const swapped: number[] = []; +for (const v of new Swapping()) swapped.push(v); +console.log(swapped.join(), [...new Swapping()].join()); diff --git a/tests/corpus/6929-class-iterator-protocol.mjs b/tests/corpus/6929-class-iterator-protocol.mjs new file mode 100644 index 000000000..6a7827f15 --- /dev/null +++ b/tests/corpus/6929-class-iterator-protocol.mjs @@ -0,0 +1,97 @@ +// for-of and spread in JavaScript over classes whose [Symbol.iterator]() returns an +// iterator object, with return() called on early exits as Node's IteratorClose does. +class Bag { + + constructor(items) { this.items = items; } + [Symbol.iterator]() { + let i = 0; + const items = this.items; + return { + next() { + return i < items.length ? { value: items[i++], done: false } : { value: undefined, done: true }; + }, + return() { + console.log("return at", i); + return { value: undefined, done: true }; + }, + }; + } +} +const bag = new Bag(["a", "b", "c", "d"]); +function firstLong() { + for (const s of bag) if (s >= "b") return s; + return undefined; +} +console.log(firstLong()); +try { + for (const s of bag) if (s === "c") throw new Error("stop at " + s); +} catch (e) { + console.log(e.message); +} +outer: for (const n of [1, 2]) { + for (const s of bag) { + if (s === "b") continue outer; + console.log(n, s); + } +} +let all = ""; +for (const s of bag) all += s; +console.log(all); + +class Counter { + n = 0; + [Symbol.iterator]() { return new CounterIterator(3); } +} +class CounterIterator { + i = 0; + + constructor(limit) { this.limit = limit; } + next() { + return this.i < this.limit ? { value: this.i++, done: false } : { value: -1, done: true }; + } + return() { + console.log("class return", this.i); + return { value: -1, done: true }; + } +} +for (const v of new Counter()) { if (v === 1) break; console.log("v", v); } +console.log([...new Counter()].join()); + +class Fussy { + [Symbol.iterator]() { + let i = 0; + return { + next() { return { value: i++, done: i > 5 }; }, + return() { if (i >= 0) throw new Error("return failed"); return { value: undefined, done: true }; }, + }; + } +} +try { + for (const v of new Fussy()) if (v === 1) break; +} catch (e) { + console.log("break:", e.message); +} +try { + for (const v of new Fussy()) if (v === 1) throw new Error("body failed"); +} catch (e) { + console.log("throw:", e.message); +} + +// An object-literal iterator that replaces its next() while it runs: the +// loop and the spread keep calling the next() they read when it opened. +class Swapping { + [Symbol.iterator]() { + let i = 0; + const it = { + next() { + i++; + if (i === 2) it.next = () => ({ value: -1, done: true }); + return { value: i, done: i > 4 }; + }, + }; + return it; + } +} +const swapped = []; +for (const v of new Swapping()) swapped.push(v); +console.log(swapped.join(), [...new Swapping()].join()); diff --git a/tests/corpus/6935-class-async-iterator-protocol.ts b/tests/corpus/6935-class-async-iterator-protocol.ts new file mode 100644 index 000000000..25626a773 --- /dev/null +++ b/tests/corpus/6935-class-async-iterator-protocol.ts @@ -0,0 +1,100 @@ +// for await over classes whose [Symbol.asyncIterator]() or [Symbol.iterator]() +// returns an iterator object: early exits await return(), as in Node. +class Ticks { + readonly n: number; + constructor(n: number) { this.n = n; } + [Symbol.asyncIterator]() { + let i = 0; + const n = this.n; + return { + async next(): Promise> { + return i < n ? { value: i++, done: false } : { value: undefined, done: true }; + }, + async return(): Promise> { + console.log("closed at", i); + return { value: undefined, done: true }; + }, + }; + } +} +class Pages { + [Symbol.asyncIterator](): PageCursor { return new PageCursor(); } +} +class PageCursor { + page = 0; + async next(): Promise<{ value: string; done: boolean }> { + this.page++; + return this.page <= 3 ? { value: "page " + this.page, done: false } : { value: "", done: true }; + } +} +class Letters { + [Symbol.iterator]() { + const letters = ["x", "y", "z"]; + let i = 0; + return { + next(): IteratorResult { return i < letters.length ? { value: letters[i++]!, done: false } : { value: undefined, done: true }; }, + }; + } +} +async function main(): Promise { + let sum = 0; + for await (const v of new Ticks(4)) sum += v * 2; + console.log("sum", sum); + for await (const v of new Ticks(5)) { + if (v === 2) break; + console.log("early", v); + } + try { + for await (const v of new Ticks(5)) if (v === 1) throw new Error("body " + v); + } catch (e) { + console.log("caught", (e as Error).message); + } + async function find(): Promise { + for await (const v of new Ticks(9)) if (v > 2) return v; + return -1; + } + console.log("found", await find()); + for await (const p of new Pages()) console.log(p); + for await (const l of new Letters()) console.log("letter", l); + // return()'s value is awaited when the loop leaves early: a pending + // promise settles first, and a rejection rejects the loop after a break + // (after a throw from the body, the body's error wins). + for await (const v of new Closing((resolve) => setTimeout(() => resolve(1), 5))) + if (v === 1) break; + console.log("after break"); + try { + for await (const v of new Closing((_, reject) => reject(new Error("closing failed")))) + if (v === 1) break; + } catch (e) { + console.log("break:", (e as Error).message); + } + try { + for await (const v of new Closing((_, reject) => reject(new Error("closing failed")))) + if (v === 1) throw new Error("body failed"); + } catch (e) { + console.log("throw:", (e as Error).message); + } +} +class Closing { + readonly settle: (resolve: (v: number) => void, reject: (e: Error) => void) => void; + constructor(settle: (resolve: (v: number) => void, reject: (e: Error) => void) => void) { + this.settle = settle; + } + [Symbol.iterator]() { + let i = 0; + const settle = this.settle; + return { + next(): IteratorResult { + return { value: i++, done: false }; + }, + return(): IteratorResult { + console.log("return called"); + const value = new Promise((resolve, reject) => + settle((v) => (console.log("value settled"), resolve(v)), reject), + ); + return { value: value as unknown as number, done: true }; + }, + }; + } +} +main(); diff --git a/tests/corpus/6935-fs-url-paths-ordered.ts b/tests/corpus/6935-fs-url-paths-ordered.ts new file mode 100644 index 000000000..8d39c40ff --- /dev/null +++ b/tests/corpus/6935-fs-url-paths-ordered.ts @@ -0,0 +1,132 @@ +// URL paths that Node converts inside a try or after checking other +// arguments: existsSync answers false for a URL it cannot convert, the +// fs.promises functions return a promise rejected with the conversion's +// error, copyFileSync and renameSync check a string source before they +// convert a URL destination, and readFileSync checks a runtime encoding +// before it converts the URL. +import * as fs from "node:fs"; +import * as fsp from "node:fs/promises"; +import * as os from "node:os"; +import * as path from "node:path"; +import { pathToFileURL } from "node:url"; + +const dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "fs-url-ordered-"))); +const at = (name: string): URL => pathToFileURL(path.join(dir, name)); +const scheme = new URL("http://example.com/a.txt"); +const host = new URL("file://example.com/a.txt"); + +function report(label: string, error: unknown): void { + if (error instanceof Error) { + const code = (error as NodeJS.ErrnoException).code; + console.log(label, error.name, code, error.message.split(dir).join("")); + } +} + +function attempt(label: string, run: () => void): void { + try { + run(); + console.log(label, "returned"); + } catch (error) { + report(label, error); + } +} + +async function settle(label: string, run: () => Promise): Promise { + try { + await run(); + console.log(label, "fulfilled"); + } catch (error) { + report(label, error); + } +} + +async function main(): Promise { + fs.writeFileSync(at("a b.txt"), "hello"); + console.log(fs.existsSync(at("a b.txt")), fs.existsSync(at("missing"))); + console.log(fs.existsSync(scheme), fs.existsSync(host), fs.existsSync(new URL("file:///tmp/a%2Fb"))); + console.log(fs.existsSync(new URL("file:///tmp/a%00b"))); + + console.log(await fsp.readFile(at("a b.txt"), "utf8")); + console.log((await fsp.readFile(at("a b.txt"))).length); + await fsp.writeFile(at("w.txt"), "written"); + await fsp.appendFile(at("w.txt"), "!"); + console.log((await fsp.stat(at("w.txt"))).size, (await fsp.lstat(at("w.txt"))).isFile()); + await fsp.access(at("w.txt")); + await fsp.chmod(at("w.txt"), 0o644); + await fsp.truncate(at("w.txt"), 3); + await fsp.copyFile(at("w.txt"), at("copy.txt")); + await fsp.rename(at("copy.txt"), at("moved.txt")); + console.log(await fsp.readFile(at("moved.txt"), "utf8")); + await fsp.mkdir(at("sub/deep"), { recursive: true }); + console.log(await fsp.readdir(at("sub"))); + console.log(path.dirname(await fsp.mkdtemp(at("tmp-"))) === dir); + console.log((await fsp.realpath(at("sub/../sub"))).split(dir).join("")); + // An array literal lowers its elements more than once. + console.log([await fsp.realpath(at("sub"))].length, [fs.existsSync(scheme)][0]); + // A later argument with effects runs once per call there, and the call + // receives its value. + let dataCalls = 0; + const data = (): string => { + dataCalls++; + return `data ${dataCalls}`; + }; + console.log([await fsp.writeFile(at("w.txt"), data())].length, dataCalls); + console.log(await fsp.readFile(at("w.txt"), "utf8")); + const code = (error: unknown): string => String((error as { code?: string }).code); + const outcomes = [ + await fsp.writeFile(scheme, data()).then(() => "fulfilled", code), + await fsp.appendFile(at("w.txt"), data()).then(() => "fulfilled", code), + ]; + console.log(outcomes, dataCalls); + console.log(await fsp.readFile(at("w.txt"), "utf8")); + await fsp.rmdir(at("sub/deep")); + await fsp.unlink(at("moved.txt")); + await fsp.rm(at("w.txt")); + await fsp.rm(at("sub"), { recursive: true, force: true }); + console.log(fs.existsSync(at("sub"))); + + // The promise is returned; the error arrives as its rejection. + const pending = fsp.stat(scheme); + console.log("returned a promise"); + await pending.catch((error: unknown) => report("stat scheme", error)); + await settle("readFile host", () => fsp.readFile(host, "utf8")); + await settle("copyFile both", () => fsp.copyFile(scheme, host)); + await settle("rename dest", () => fsp.rename(at("a b.txt"), host)); + await settle("writeFile separator", () => fsp.writeFile(new URL("file:///tmp/a%2fb"), "x")); + await settle("unlink missing", () => fsp.unlink(at("missing"))); + await settle("rm scheme options", () => fsp.rm(scheme, { recursive: true, force: true })); + + // Arguments inside type-only wrappers are evaluated once. + let modeCalls = 0; + const countedMode = (): number => { + modeCalls++; + return 0o644; + }; + await settle("chmod satisfies", () => fsp.chmod(scheme, countedMode() satisfies number)); + await settle("chmod as", () => fsp.chmod(at("a b.txt"), countedMode() as number)); + await settle("chmod non-null", () => fsp.chmod(host, countedMode()!)); + console.log("mode calls", modeCalls); + await settle("copyFile as", () => fsp.copyFile(scheme, at("c.txt") as URL)); + await settle("copyFile dest as", () => fsp.copyFile(at("a b.txt"), host as URL)); + + attempt("copyFileSync source first", () => fs.copyFileSync("a\0b", scheme)); + attempt("copyFileSync dest", () => fs.copyFileSync(path.join(dir, "a b.txt"), host)); + attempt("renameSync source first", () => fs.renameSync("a\0b", host)); + fs.copyFileSync(path.join(dir, "a b.txt"), at("copied.txt")); + console.log(fs.readFileSync(at("copied.txt"), "utf8")); + + const encodings = ["utf8", "latin1", "base64", "bogus"]; + for (const encoding of encodings) { + attempt(`readFileSync ${encoding}`, () => { + console.log(fs.readFileSync(at("a b.txt"), encoding as BufferEncoding)); + }); + attempt(`readFileSync ${encoding} scheme`, () => { + console.log(fs.readFileSync(scheme, encoding as BufferEncoding)); + }); + } + + fs.rmSync(dir, { recursive: true, force: true }); + console.log("done", fs.existsSync(dir)); +} + +void main(); diff --git a/tests/corpus/6936-class-async-iterator-protocol.mjs b/tests/corpus/6936-class-async-iterator-protocol.mjs new file mode 100644 index 000000000..4cc67b0d1 --- /dev/null +++ b/tests/corpus/6936-class-async-iterator-protocol.mjs @@ -0,0 +1,99 @@ +// for await in JavaScript over classes whose [Symbol.asyncIterator]() or [Symbol.iterator]() +// returns an iterator object: early exits await return(), as in Node. +class Ticks { + + constructor(n) { this.n = n; } + [Symbol.asyncIterator]() { + let i = 0; + const n = this.n; + return { + async next() { + return i < n ? { value: i++, done: false } : { value: undefined, done: true }; + }, + async return() { + console.log("closed at", i); + return { value: undefined, done: true }; + }, + }; + } +} +class Pages { + [Symbol.asyncIterator]() { return new PageCursor(); } +} +class PageCursor { + page = 0; + async next() { + this.page++; + return this.page <= 3 ? { value: "page " + this.page, done: false } : { value: "", done: true }; + } +} +class Letters { + [Symbol.iterator]() { + const letters = ["x", "y", "z"]; + let i = 0; + return { + next() { return i < letters.length ? { value: letters[i++], done: false } : { value: undefined, done: true }; }, + }; + } +} +async function main() { + let sum = 0; + for await (const v of new Ticks(4)) sum += v * 2; + console.log("sum", sum); + for await (const v of new Ticks(5)) { + if (v === 2) break; + console.log("early", v); + } + try { + for await (const v of new Ticks(5)) if (v === 1) throw new Error("body " + v); + } catch (e) { + console.log("caught", e.message); + } + async function find() { + for await (const v of new Ticks(9)) if (v > 2) return v; + return -1; + } + console.log("found", await find()); + for await (const p of new Pages()) console.log(p); + for await (const l of new Letters()) console.log("letter", l); + // return()'s value is awaited when the loop leaves early: a pending + // promise settles first, and a rejection rejects the loop after a break + // (after a throw from the body, the body's error wins). + for await (const v of new Closing((resolve) => setTimeout(() => resolve(1), 5))) + if (v === 1) break; + console.log("after break"); + try { + for await (const v of new Closing((_, reject) => reject(new Error("closing failed")))) + if (v === 1) break; + } catch (e) { + console.log("break:", e.message); + } + try { + for await (const v of new Closing((_, reject) => reject(new Error("closing failed")))) + if (v === 1) throw new Error("body failed"); + } catch (e) { + console.log("throw:", e.message); + } +} +class Closing { + constructor(settle) { + this.settle = settle; + } + [Symbol.iterator]() { + let i = 0; + const settle = this.settle; + return { + next() { + return { value: i++, done: false }; + }, + return() { + console.log("return called"); + const value = new Promise((resolve, reject) => + settle((v) => (console.log("value settled"), resolve(v)), reject), + ); + return { value, done: true }; + }, + }; + } +} +main(); diff --git a/tests/corpus/6936-x509-certificate-fields.ts b/tests/corpus/6936-x509-certificate-fields.ts new file mode 100644 index 000000000..de4b78f9a --- /dev/null +++ b/tests/corpus/6936-x509-certificate-fields.ts @@ -0,0 +1,189 @@ +// X509Certificate fields match Node: subject and issuer (one attribute +// per line, RFC 2253 escaping), subjectAltName and infoAccess (undefined +// without the extension), serialNumber, the SHA-1/256/512 fingerprints, ca, +// the signature algorithm, the validity window as strings and Dates, and +// raw, over PEM Buffer, PEM string and DER input. +import * as crypto from "node:crypto"; + +// An RSA CA with a multi-attribute subject. +const CA = `-----BEGIN CERTIFICATE----- +MIIEITCCAwmgAwIBAgIIASNFZ4mrze8wDQYJKoZIhvcNAQELBQAwgZwxCzAJBgNV +BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNp +c2NvMRQwEgYDVQQKDAtFeGFtcGxlIE9yZzERMA8GA1UECwwIRGV2IFRlYW0xGDAW +BgNVBAMMD0V4YW1wbGUgUm9vdCBDQTEdMBsGCSqGSIb3DQEJARYOY2FAZXhhbXBs +ZS5jb20wIBcNMjYxMDIxMTYzMzUyWhgPMjEyNjA5MjcxNjMzNTJaMIGcMQswCQYD +VQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5j +aXNjbzEUMBIGA1UECgwLRXhhbXBsZSBPcmcxETAPBgNVBAsMCERldiBUZWFtMRgw +FgYDVQQDDA9FeGFtcGxlIFJvb3QgQ0ExHTAbBgkqhkiG9w0BCQEWDmNhQGV4YW1w +bGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0YGcCr0pxR8d +6qDdLuILcoT7vhtAjyHwfkoChnH6N3TLWzDUugnBp9NbDb7gDLqbRT4uTDoNb6zS +tEJZ4Q7/hhd0jDvoPmOy0Y3Kyj9uNsdf25GdbvwwEnlddELJCZprTZ4OhEg6w2s0 +Yy4SlviLzDXLmsggTypxeS+K8JHT9onMipi5L4GFu8n1QZbzXvSS4BkRgMXEyrtr +FOE9qSo2qkubFyd/WNKRTRIpTSc5/GGsqJ0FKDDJnN5adotcUucbJFn4noNnFUWh +M8mhBEj7ENDdJsD2EVFsI6H958jA2SbNNu4ngzAOs2AKNBYBfQK02ZQAdRNOSL3K +5FkfbII5LwIDAQABo2MwYTAdBgNVHQ4EFgQUP+yQyItpNYEls26Jfv+NnciGaK8w +HwYDVR0jBBgwFoAUP+yQyItpNYEls26Jfv+NnciGaK8wDwYDVR0TAQH/BAUwAwEB +/zAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQELBQADggEBABDw3Va/C+RFc/Hs +TJnHtQJtxFL0ZhnHUqg4DcDA3m+JDdIDNL8sNpLEbtn/pr0VeEWgG+MImWAQlrUW +IAZkLj9/Zg1wSO0oZ86ZyZJZmp54lUYXgG17CNl4g+F7VHt3bBwUJHTbWye/FKM6 +5OVZetCYKVj7Z+5BYuf7cumzvRqcn32fXidB/kjRX14SJ4JDPn2yFZWCr2ygIqWQ +SOuEmWbNAbYGaVZkQr2Ma8U28DWY2jb0f6nP8jLt49t9o6N/e6Fwd6I5t/mx9Mfj +0403omkxmr9vqN2kBamzVnun+2mDkRZcKPCr72Q4sfbvmDi/W5Nf1Iu2cQW8VfDO +SqpyT54= +-----END CERTIFICATE----- +`; + +// An EC leaf with alternative names, access descriptions and extended key usage, signed by CA. +const LEAF = `-----BEGIN CERTIFICATE----- +MIIDuzCCAqOgAwIBAgIPAPAN+s4AESIzRFVmd4iZMA0GCSqGSIb3DQEBDAUAMIGc +MQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2Fu +IEZyYW5jaXNjbzEUMBIGA1UECgwLRXhhbXBsZSBPcmcxETAPBgNVBAsMCERldiBU +ZWFtMRgwFgYDVQQDDA9FeGFtcGxlIFJvb3QgQ0ExHTAbBgkqhkiG9w0BCQEWDmNh +QGV4YW1wbGUuY29tMB4XDTI2MTAyMTE2MzM1MloXDTM2MTAxODE2MzM1MlowJzER +MA8GA1UECgwITGVhZiBPcmcxEjAQBgNVBAMMCWxvY2FsaG9zdDBZMBMGByqGSM49 +AgEGCCqGSM49AwEHA0IABJVfVy9EH/VHQuRqpFFnXk0F00ncDht6APgVPDoybK8H +PlcM4T5H1yAOMHnlH29LK1jtMqU1aAOiiQRzPdCvqoujggE3MIIBMzAJBgNVHRME +AjAAMGgGA1UdEQRhMF+CCWxvY2FsaG9zdIINKi5leGFtcGxlLmNvbYcEfwAAAYcQ +AAAAAAAAAAAAAAAAAAAAAYERYWRtaW5AZXhhbXBsZS5jb22GGGh0dHBzOi8vZXhh +bXBsZS5jb20vcGF0aDBdBggrBgEFBQcBAQRRME8wIwYIKwYBBQUHMAGGF2h0dHA6 +Ly9vY3NwLmV4YW1wbGUuY29tMCgGCCsGAQUFBzAChhxodHRwOi8vY2EuZXhhbXBs +ZS5jb20vY2EuY3J0MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAdBgNV +HQ4EFgQUdKIx6ULFLPNP/kzwUFjSa5D19RswHwYDVR0jBBgwFoAUP+yQyItpNYEl +s26Jfv+NnciGaK8wDQYJKoZIhvcNAQEMBQADggEBABmQ8f2VsXNaMM9X1MEhi27D +LjwcOaaGEINNZq7xu/HL6UljMb1Odsuz66DkiH3Exd3MrWy6OlOpJJF0VzsqMsoD +0CRBxXF28o6Jil3vOGVwlYNmRidpkAkj4VyWDvv8FCS9VbsBtXaSHwu9wcCwePE0 +cGJRfTKoZ6KJ8vQ57DBEZ/ZszuhehOZYqrTmoUdjZ123kXdxQvK+ALn4SlIXHBhN +9unhd+8i00EmkXO/REw8paMcMVVGR+ioOEjyNHqZo6vkRqgpKggn1Yq8cTTDOMnS +P1o42EQZLlbuvx0kqInBqwqjDx4nZiPaifoDc9BF6iZPBvm8dhrEfghfB1tkNk0= +-----END CERTIFICATE----- +`; + +// Escaped subject characters, UTF-8 and a comma in a DNS name. +const ODD = `-----BEGIN CERTIFICATE----- +MIICXzCCAeWgAwIBAgIBATAKBggqhkjOPQQDAjBvMQ0wCwYDVQQDDARhLCBiMQ4w +DAYDVQQHDAUjbGVhZDESMBAGA1UECAwJICBzcGFjZSAgMSYwJAYDVQQKDB1RdW90 +ZSBxIGJhY2tcc2xhc2g7IHNlbWkgPGx0PjESMBAGA1UECwwJQ2Fmw6kg4oKsMB4X +DTI2MTAyMTE2MzM1MloXDTI3MTAyMTE2MzM1MlowbzENMAsGA1UEAwwEYSwgYjEO +MAwGA1UEBwwFI2xlYWQxEjAQBgNVBAgMCSAgc3BhY2UgIDEmMCQGA1UECgwdUXVv +dGUgcSBiYWNrXHNsYXNoOyBzZW1pIDxsdD4xEjAQBgNVBAsMCUNhZsOpIOKCrDB2 +MBAGByqGSM49AgEGBSuBBAAiA2IABMLLzwDsSeezCzScnC9xTIFl35dFAX+1Uagk +0y4Y72rOOgVFA13SMIbMfv2InIyg2vXOdqn0y7RqiaaqqnAYZB2RHHL0SD7t2MVL +K3BQajy7Oi6IeDDRGK1jM4D6nk/1AKNVMFMwMgYDVR0RBCswKYILYSxiLmV4YW1w +bGWCDXBsYWluLmV4YW1wbGWBBXhAeS56hwQKAAABMB0GA1UdDgQWBBSR4U6nIpTt +5rLoIZZxfxSyx9Uc9zAKBggqhkjOPQQDAgNoADBlAjEAlznFL0Nrt1jjZrT3/GN0 +yN/8gxO3SawyqoWrfP3uk1GScTh0PlrTEOrpSyuq3wl5AjBRntVI1ZoUM46pCwIw +0Vl3c3lns7OljcQFiR7o8yRhEW9ejdiR2W6PLBrjCly6U3I= +-----END CERTIFICATE----- +`; + +// A multi-valued RDN and serial number 0. +const MULTI = `-----BEGIN CERTIFICATE----- +MIICNzCCAaCgAwIBAgIBADANBgkqhkiG9w0BAQsFADA3MSgwCgYDVQQLDANyZG4w +DAYDVQQDDAVtdWx0aTAMBgNVBAoMBXRocmVlMQswCQYDVQQGEwJERTAeFw0yNjEw +MjExNjM0MDVaFw0yNjEwMzExNjM0MDVaMDcxKDAKBgNVBAsMA3JkbjAMBgNVBAMM +BW11bHRpMAwGA1UECgwFdGhyZWUxCzAJBgNVBAYTAkRFMIGfMA0GCSqGSIb3DQEB +AQUAA4GNADCBiQKBgQDdv64EOPdpcWdjVMAlEQvNzm3ZbQYXrhOwRF9yzQp25SuA +3uKvStZOt8hyTifAd30oDXIfgyYI39xKHEkHfARTV1E2Jwyiz86/pDHmFypPFJuI +v/vOjYLLeisiVi8TFfHgmoGqGvySR83fwiUnoOOWj9Bqt/5EVBQbBeWrU9qH6QID +AQABo1MwUTAdBgNVHQ4EFgQUZrGU0gkgqCkmegRtu6vQ3h7kpPUwHwYDVR0jBBgw +FoAUZrGU0gkgqCkmegRtu6vQ3h7kpPUwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG +9w0BAQsFAAOBgQAMzFkzJElc9VJYhGx7AngO8Yve22iCTOVLO3/be8w55klb/HJX +vfESQz+EJLYVaP5Q/uChoE0I27NytkeUSpJFwmjyqoQq2j6kJWGKONqhQSnXDrms +HMPA7iik/C5hOqpwzDZ98wNU/e86TrqZxzpFkwbnZx9MSfiGr690s5BfpA== +-----END CERTIFICATE----- +`; + +// An Ed25519 certificate with otherName, directoryName, registeredID and IPv6 names, a BMPString subject, a negative serial and keyUsage without keyCertSign. +const NAMES = `-----BEGIN CERTIFICATE----- +MIICSDCCAfqgAwIBAgIB+zAFBgMrZXAwLDEVMBMGA1UEAx4MAGIAbQBwACAA6U4t +MRMwEQYDVQQKHgoAcABsAGEAaQBuMB4XDTI2MTAyMTE2NDQzM1oXDTI2MTEyMDE2 +NDQzM1owLDEVMBMGA1UEAx4MAGIAbQBwACAA6U4tMRMwEQYDVQQKHgoAcABsAGEA +aQBuMCowBQYDK2VwAyEAP+2Vcp8nwu2AhFvSczYTxGs1jV/ckA76Jqvwj9QDGqij +ggE/MIIBOzCBtwYDVR0RBIGvMIGsoBsGCisGAQQBgjcUAgOgDQwLdXNlckBkb21h +aW6gEAYDKgMEoAkMB3Vua25vd26kRTBDMQswCQYDVQQGEwJVSzEZMBcGA1UECh4Q +AEQAaQByACwAIABPAHIAZzEZMBcGA1UEAx4QAGQAaQByACAAbgBhAG0AZYgEKgME +BYcQ/oAAAAAAAAAAAAAAAAEAAocQIAENuAAAAAAAAP8AAEKDKYYKaHR0cDovL3gv +cTAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIHgDBFBggrBgEFBQcBAQQ5MDcwIAYI +KwYBBQUHMAWGFGh0dHA6Ly9yZXBvLmV4YW1wbGUvMBMGBCoDBAWCC29kZC5leGFt +cGxlMB0GA1UdDgQWBBQYMJ0fQJ1Nx0PmatIfTJusAEA9WTAFBgMrZXADQQCixubE +nuunBZYYPXF52+joQSX/a5DFkOEDmzRTuH7IIpPFWPo37rmJOSdPA/PV8v9clzYF +LP3EjM4YOAatcbsO +-----END CERTIFICATE----- +`; + +// An RSA-PSS signature and a control character in the subject. +const PSS = `-----BEGIN CERTIFICATE----- +MIIDdTCCAiigAwIBAgICAIAwQgYJKoZIhvcNAQEKMDWgDzANBglghkgBZQMEAgEF +AKEcMBoGCSqGSIb3DQEBCDANBglghkgBZQMEAgEFAKIEAgIA3jAeMQwwCgYDVQQD +DANwc3MxDjAMBgNVBAoMBWN0bAF4MB4XDTI2MTAyMTE2NDQzM1oXDTI2MTEyMDE2 +NDQzM1owHjEMMAoGA1UEAwwDcHNzMQ4wDAYDVQQKDAVjdGwBeDCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAL2pno8Ybwihdv3hnMhWPPt85SXQmFAJpFoS +E+sT9H6FbwFCIyPkY+SWk8H7y8V+D+388tNvJZmGjX3uvWpK2Wx6anSB3/WMpkUR +2ExP4cpSxZNr9EFzC/cdqLvsxJcHMHkc/uJuFbtjsuiH1ygOe3nyhWSNiPwGdwHE +cKkBFkzW3j2C/4Jkgt4k+sMe3gy5litKtMi4zZB8ixCm5oGhveGhUZy0Kb01twgZ +RP4FEd32sSRqq3NCx4cB0kyreTuzmk6U7BkxcdPzaj6DifjRznTQmjMdoCNZQRCj +G3KBOMHHyvmV2B4lNGDuMlcSap/TpYq7n32AH5sDvELrUnPwVzsCAwEAAaNTMFEw +HQYDVR0OBBYEFC/QJvAQsbQ25sErc1To2G1VuNZ0MB8GA1UdIwQYMBaAFC/QJvAQ +sbQ25sErc1To2G1VuNZ0MA8GA1UdEwEB/wQFMAMBAf8wQgYJKoZIhvcNAQEKMDWg +DzANBglghkgBZQMEAgEFAKEcMBoGCSqGSIb3DQEBCDANBglghkgBZQMEAgEFAKIE +AgIA3gOCAQEAu3lP2Q3Bb5fWUQtgFr/82Oke1ZRW/GRbj03mZ30izADHC7yk0OYm +02f7B5sub04Jk6yuRVmEBjJHSPNwAo0RJWtwvD77KLhG8aObr7Z3+pkfFvpGzqeS +MQ4FtK2MPps3kA1a5D4XLwkwDJtHWZVuVyXpPgxkptfLzZgrieKrpNyOKR8Pz/PO +Wlwv6bF7SXvAV8Vrd7KDvTqwQ5iq9UFhP6GeMDWezYtzLdjlvje7MhmZ96zgSY6m +qumNWOPHmiN4eoOE4M69B+0fa3WyMzubW0nVGM/sDhtS0k5zHs6Y4RLn11HolhXH +LvbvDgqtl/m5i0uHxEld+Z1uhzJscqFQRg== +-----END CERTIFICATE----- +`; + +// A version 1 certificate without extensions. +const V1 = `-----BEGIN CERTIFICATE----- +MIIBSTCB7wIUI3kYI5NpR07YqZe6RwOx22je9IowCgYIKoZIzj0EAwIwJzERMA8G +A1UECgwITGVhZiBPcmcxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0yNjEwMjExNjMz +NTJaFw0yNjExMjAxNjMzNTJaMCcxETAPBgNVBAoMCExlYWYgT3JnMRIwEAYDVQQD +DAlsb2NhbGhvc3QwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASVX1cvRB/1R0Lk +aqRRZ15NBdNJ3A4begD4FTw6MmyvBz5XDOE+R9cgDjB55R9vSytY7TKlNWgDookE +cz3Qr6qLMAoGCCqGSM49BAMCA0kAMEYCIQCFfFOw9qzzfSOQO1zRnT0sOXibjLWl +hHDyqecxhY7gtQIhAPMgqg+p1JXV5bgGpqgDfP3sE53luxMGlnAC39uhwBUh +-----END CERTIFICATE----- +`; + + +function show(label: string, cert: crypto.X509Certificate): void { + console.log(label, "subject", JSON.stringify(cert.subject)); + console.log(label, "issuer", JSON.stringify(cert.issuer)); + console.log(label, "subjectAltName", JSON.stringify(cert.subjectAltName), typeof cert.subjectAltName); + console.log(label, "infoAccess", JSON.stringify(cert.infoAccess), cert.infoAccess === undefined); + console.log(label, "serialNumber", cert.serialNumber); + console.log(label, "fingerprints", cert.fingerprint, cert.fingerprint256); + console.log(label, "fingerprint512", cert.fingerprint512); + console.log(label, "ca", cert.ca); + console.log(label, "signature", cert.signatureAlgorithm, cert.signatureAlgorithmOid); + console.log(label, "validity", cert.validFrom, "|", cert.validTo); + console.log(label, "dates", cert.validFromDate.toISOString(), cert.validToDate.getTime()); + console.log(label, "raw", cert.raw.length, Buffer.isBuffer(cert.raw), cert.raw.subarray(0, 4).toString("hex")); +} + +const pems: [string, string][] = [ + ["ca", CA], + ["leaf", LEAF], + ["odd", ODD], + ["multi", MULTI], + ["names", NAMES], + ["pss", PSS], + ["v1", V1], +]; +for (const [label, pem] of pems) show(label, new crypto.X509Certificate(Buffer.from(pem))); + +// A PEM string, and the DER bytes of raw. +const fromString = new crypto.X509Certificate(LEAF); +show("string", fromString); +const fromDer = new crypto.X509Certificate(fromString.raw); +console.log("der", fromDer.subject === fromString.subject, fromDer.fingerprint256 === fromString.fingerprint256); +console.log("der raw copy", fromDer.raw !== fromString.raw, fromDer.raw.equals(fromString.raw)); + +try { + new crypto.X509Certificate(Buffer.from("not a certificate")); +} catch (e) { + const err = e as NodeJS.ErrnoException; + console.log("garbage", err.name, err.code); +} diff --git a/tests/corpus/6937-class-value-arrays.mjs b/tests/corpus/6937-class-value-arrays.mjs new file mode 100644 index 000000000..8fcfcb815 --- /dev/null +++ b/tests/corpus/6937-class-value-arrays.mjs @@ -0,0 +1,53 @@ +// Arrays of class values that mix a base class and its subclasses: the +// elements construct, compare and name like the classes themselves. +class Animal { + speak() { + return "..."; + } +} +class Dog extends Animal { + speak() { + return "woof"; + } +} +class Cat extends Animal { + speak() { + return "meow"; + } +} +const kinds = [Dog, Cat, Animal]; +console.log(kinds.length, kinds.map((C) => C.name).join()); +console.log(new kinds[0]().speak(), new kinds[1]().speak(), new kinds[2]().speak()); +console.log(kinds[0] === Dog, kinds.indexOf(Cat), kinds.includes(Animal)); +const made = kinds.map((C) => new C()); +console.log(made.map((a) => a.speak()).join(), made[0] instanceof Animal, made[1] instanceof Dog); +try { + new kinds[5](); +} catch (e) { + console.log(e.name, e.message); +} +const byName = { dog: Dog, cat: Cat }; +console.log(new byName.dog().speak(), new byName["cat"]().speak()); +// The arguments are evaluated before the missing element throws. +class Point { + constructor(x, y) { + this.sum = x + y; + } +} +class Point3 extends Point {} +const points = [Point3, Point]; +const evaluated = []; +const arg = (v) => (evaluated.push(v), v); +console.log(new points[0](arg(1), arg(2)).sum, evaluated.join()); +try { + new points[2](arg(3), arg(4)); +} catch (e) { + console.log(e.name, e.message, evaluated.join()); +} +// Spread arguments too, through a valid index and past the end. +console.log(new points[1](...[5, 6]).sum); +try { + new points[3](...[arg(7), arg(8)]); +} catch (e) { + console.log(e.name, e.message, evaluated.join()); +} diff --git a/tests/corpus/6938-class-value-arrays.ts b/tests/corpus/6938-class-value-arrays.ts new file mode 100644 index 000000000..db695bc0a --- /dev/null +++ b/tests/corpus/6938-class-value-arrays.ts @@ -0,0 +1,48 @@ +// Arrays of class values that mix a base class and its subclasses, with the +// element type the checker infers (the base class): elements construct and +// dispatch to the subclass, and an index past the end throws Node's TypeError. +class Shape { + area(): number { + return 0; + } +} +class Square extends Shape { + area(): number { + return 4; + } +} +class Circle extends Shape { + area(): number { + return 3; + } +} +const shapes = [Square, Circle, Shape]; +console.log(shapes.length, new shapes[0]().area(), new shapes[1]().area(), new shapes[2]().area()); +let total = 0; +for (const S of shapes) total += new S().area(); +console.log(total, shapes[1] === Circle, shapes.map((S) => S.name).join()); +try { + new shapes[3](); +} catch (e) { + console.log((e as Error).name, (e as Error).message); +} +// A class value checked into a `typeof Shape` slot: a subclass whose +// constructor takes the same arguments constructs, and one whose constructor +// takes an argument the slot does not pass throws a TypeError (Node's from +// reading the missing argument, the compiled program's from the check). +class Sized extends Shape { + label: string; + constructor(side: number) { + super(); + this.label = side.toFixed(1); + } +} +const values: any[] = [Square, Sized, Shape]; +for (const value of values) { + try { + const S: typeof Shape = value; + console.log(new S().area()); + } catch (e) { + console.log((e as Error).name); + } +} diff --git a/tests/corpus/7012-dictionary-views-keep-objects.ts b/tests/corpus/7012-dictionary-views-keep-objects.ts new file mode 100644 index 000000000..0df47cbb8 --- /dev/null +++ b/tests/corpus/7012-dictionary-views-keep-objects.ts @@ -0,0 +1,98 @@ +// A record or class instance converted to a string-keyed dictionary is +// the same object: writes through the dictionary reach it, and the +// dictionary is === to it. +const key = process.argv.length > 99 ? "never" : "added"; +type Point = { a: string }; + +// A type-literal record stored straight in a dictionary binding. +const direct: Point = { a: "x" }; +const directTable: Record = direct; +directTable[key] = "y"; +console.log(JSON.stringify(direct), (directTable as unknown) === direct); + +// The same record through a `{}` view. +const viewed: Point = { a: "x" }; +const view: {} = viewed; +const viewTable: Record = view; +viewTable[key] = "y"; +console.log(JSON.stringify(viewed)); + +// A declared index type, and a parameter. +interface Labels { + [name: string]: string; +} +function label(table: Labels, name: string): void { + table[name] = "set"; +} +const passed: Point = { a: "x" }; +label(passed, key); +console.log(JSON.stringify(passed)); + +// A `{}` parameter converted inside the function, called with a record +// and with a dictionary. +function viaView(value: {}): void { + const table: Record = value; + table[key] = "y"; +} +const record: Point = { a: "x" }; +viaView(record); +const dictionary: Record = { a: "x" }; +viaView(dictionary); +console.log(JSON.stringify(record), JSON.stringify(dictionary)); + +// A class instance through a `{}` view: a computed write replaces a method. +class Greeter { + name = "g"; + greet(): string { + return "hello " + this.name; + } +} +const greeter = new Greeter(); +const greeterView: {} = greeter; +const slots: Record = greeterView; +slots[process.argv.length > 99 ? "never" : "greet"] = "replaced"; +slots["name"] = "renamed"; +console.log(typeof greeter.greet, greeter.name, JSON.stringify(greeter)); +// A write under a runtime string key replaces a method too, and a direct +// call reaches the replacement, which is not a function. +class Counter { + n = 0; + tick(): number { + return ++this.n; + } +} +const counter = new Counter(); +const counterView: {} = counter; +const counterSlots: Record = counterView; +const methodKey: string = process.argv.length > 99 ? "never" : "tick"; +counterSlots[methodKey] = "replaced"; +try { + console.log(counter.tick()); +} catch (error) { + console.log((error as Error).name, (error as Error).message); +} +try { + console.log(greeter.greet()); +} catch (error) { + console.log((error as Error).name, (error as Error).message); +} + +// Deleting through the dictionary removes the key from the object. +const pruned: { a: string; b?: string } = { a: "x", b: "y" }; +const prunedTable: Record = pruned; +delete prunedTable[process.argv.length > 99 ? "never" : "b"]; +console.log(JSON.stringify(pruned), "b" in pruned); + +// A dictionary literal whose methods read `this` keeps its live object. +interface Table { + [key: string]: () => number; +} +const table: Table = { + base() { + return 2; + }, + twice() { + return this.base() * 2; + }, +}; +console.log(table.twice(), Object.keys(table).join(",")); diff --git a/tests/corpus/7013-dictionary-view-parameter.ts b/tests/corpus/7013-dictionary-view-parameter.ts new file mode 100644 index 000000000..2bfc34a76 --- /dev/null +++ b/tests/corpus/7013-dictionary-view-parameter.ts @@ -0,0 +1,30 @@ +// A record passed to a `{}` parameter and converted to a dictionary inside +// the function is the same object: the write through the dictionary +// reaches the caller's record. A dot write through a dictionary view of a +// class instance replaces its method. +const key = process.argv.length > 99 ? "never" : "added"; +function stamp(value: {}): void { + const table: Record = value; + table[key] = "y"; +} +const record: { a: string } = { a: "x" }; +stamp(record); +console.log(JSON.stringify(record), key in record); + +// A dot write through the dictionary view replaces a method too. +class Lamp { + on = false; + toggle(): boolean { + this.on = !this.on; + return this.on; + } +} +const lamp = new Lamp(); +const lampView: {} = lamp; +const lampSlots: Record = lampView; +lampSlots.toggle = "replaced"; +try { + console.log(lamp.toggle()); +} catch (error) { + console.log((error as Error).name, (error as Error).message); +} diff --git a/tests/corpus/7035-promise-all-tuple-turns.ts b/tests/corpus/7035-promise-all-tuple-turns.ts new file mode 100644 index 000000000..c0fc6a5a3 --- /dev/null +++ b/tests/corpus/7035-promise-all-tuple-turns.ts @@ -0,0 +1,80 @@ +// Promise.all over entries of different types settles, and resumes its +// awaits and then callbacks, on the same microtask turns as Node, whether +// the result is awaited at once, stored first, chained with then, or +// returned from an async function. The first rejection in settlement +// order wins, later ones count as handled, and record entries keep their +// identity. +const log: string[] = []; +function later(label: string, turns: number, value: T, fail = false): Promise { + return new Promise((resolve, reject) => { + let left = turns; + const step = (): void => { + if (left-- > 0) return queueMicrotask(step); + log.push(`settle ${label}`); + if (fail) reject(new Error(label)); + else resolve(value); + }; + step(); + }); +} +async function num(): Promise { + return 1; +} +async function text(): Promise { + return "s"; +} +async function both(): Promise<[number, string]> { + return Promise.all([num(), text()]); +} +const thenable: any = { + get then() { + log.push("then getter"); + return (resolve: (v: number) => void) => resolve(1); + }, +}; +function evaluated(): Promise { + log.push("second entry evaluated"); + return Promise.resolve("s"); +} +interface Item { + id: number; +} +const item: Item = { id: 7 }; + +async function main(): Promise { + let ticking = true; + void (async () => { + for (let i = 0; i < 40 && ticking; i++) { + log.push(`t${i}`); + await null; + } + })(); + const [a, b] = await Promise.all([num(), text()]); + log.push(`awaited ${a}${b}`); + const stored = Promise.all([text(), num(), Promise.resolve(item)]); + const [c, d, e] = await stored; + log.push(`stored ${c}${d} ${e === item}`); + Promise.all([text(), num()]).then(([f, g]) => log.push(`then ${f}${g}`)); + const [h, i] = await both(); + log.push(`returned ${h}${i}`); + const mixed = await Promise.all([later("x", 3, "x"), 5, later("y", 1, true), undefined]); + log.push(`mixed ${mixed[0]} ${mixed[1]} ${mixed[2]} ${mixed[3]}`); + // A dynamic entry's `then` getter runs after the whole array evaluated. + const [fromThenable, second] = await Promise.all([thenable, evaluated()]); + log.push(`thenable ${fromThenable} ${second}`); + const raw: any = JSON.parse('{"value":41}'); + const [dynamic, counted] = await Promise.all([raw.value, later("z", 2, 2)]); + log.push(`dynamic ${dynamic + counted}`); + // A Date entry's payload is its time value. + const [when, label] = await Promise.all([later("date", 1, new Date(86400000)), text()]); + log.push(`date ${when.toISOString()} ${label}`); + try { + await Promise.all([later("late", 4, 1, true), later("early", 2, "e", true), num()]); + } catch (err) { + log.push(`rejected ${(err as Error).message}`); + } + await later("tail", 6, 0); + ticking = false; + console.log(log.join("\n")); +} +void main(); diff --git a/tests/corpus/7090-x509-check-names.ts b/tests/corpus/7090-x509-check-names.ts new file mode 100644 index 000000000..31ec7244f --- /dev/null +++ b/tests/corpus/7090-x509-check-names.ts @@ -0,0 +1,141 @@ +// X509Certificate.checkHost, checkEmail and checkIP match Node: the +// subject alternative names of the asked kind first, then the subject's +// common name or emailAddress when the certificate has none (or as the +// subject option says), the wildcard rules and their options, case rules, +// IPv4 and IPv6 parsing, and an invalid subject option. +import { X509Certificate, type X509CheckOptions } from "node:crypto"; + +// DNS names with every wildcard form, an IP address and an email name. +const WILD = `-----BEGIN CERTIFICATE----- +MIIB/TCCAaOgAwIBAgIBCTAKBggqhkjOPQQDAjAXMRUwEwYDVQQDDAwqLmNuLmV4 +YW1wbGUwIBcNMjYxMDIyMTAwMTMxWhgPMjEyNjA5MjgxMDAxMzFaMBcxFTATBgNV +BAMMDCouY24uZXhhbXBsZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABGJ1KKs0 +givD5gd5LXXbPc37WfT64D0ePORDm7dB9mxEYJ6K8IQxBKzn5xQT+e/lZZ1raRBZ +q/E8aW4cW1iZLXSjgd0wgdowgbgGA1UdEQSBsDCBrYIOZiouZXhhbXBsZS5jb22C +Dypvby5leGFtcGxlLm5ldIIXKi54bi0tYmNoZXIta3ZhLmV4YW1wbGWCDXhuLS0q +LmV4YW1wbGWCBSouY29tgg9hLiouZXhhbXBsZS5vcmeCASqCDyouTWl4ZWQuRXhh +bXBsZYIMLmRvdC5leGFtcGxlgg9iKnouZXhhbXBsZS5lZHWHBMCoAQGBEU1peGVk +QEV4YW1wbGUuQ09NMB0GA1UdDgQWBBR8lJ0Uw8GcVD1cGUSprHt346HjDDAKBggq +hkjOPQQDAgNIADBFAiALkTbNe6bIavd8T9qKlAqzmEPQrFRZUVoOHSQPI/zV5QIh +AM0et8JtbWP+4ySiUPPqCXtlup00GfhcqGXy+lSl32vL +-----END CERTIFICATE----- +`; + +// No alternative names; the subject has a wildcard CN and an emailAddress. +const CN = `-----BEGIN CERTIFICATE----- +MIIBVDCB+gIBCjAKBggqhkjOPQQDAjA1MRcwFQYDVQQDDA5zZWNvbmQuZXhhbXBs +ZTEaMBgGCSqGSIb3DQEJARYLYUBiLmV4YW1wbGUwIBcNMjYxMDIyMTAwMTMxWhgP +MjEyNjA5MjgxMDAxMzFaMDUxFzAVBgNVBAMMDnNlY29uZC5leGFtcGxlMRowGAYJ +KoZIhvcNAQkBFgthQGIuZXhhbXBsZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IA +BGJ1KKs0givD5gd5LXXbPc37WfT64D0ePORDm7dB9mxEYJ6K8IQxBKzn5xQT+e/l +ZZ1raRBZq/E8aW4cW1iZLXQwCgYIKoZIzj0EAwIDSQAwRgIhAIE0IFtuCNllBodV +go5WWgb8h1McrS/o31V5K0JrpF7QAiEAil8AEHuMiVtef/txZsEKPPojDKfPTYC8 +zplMuFd9gQA= +-----END CERTIFICATE----- +`; + +// A leaf with DNS, email, IPv4, IPv6 and URI names. +const LEAF = `-----BEGIN CERTIFICATE----- +MIIDqjCCApKgAwIBAgICCwswDQYJKoZIhvcNAQELBQAwPjELMAkGA1UEBhMCVVMx +FDASBgNVBAoMC0V4YW1wbGUgT3JnMRkwFwYDVQQDDBBFeGFtcGxlIFJTQSBSb290 +MCAXDTI2MTAyMjA5NDY0MVoYDzIxMjYwOTI4MDk0NjQxWjBfMQswCQYDVQQGEwJV +UzEUMBIGA1UECgwLRXhhbXBsZSBPcmcxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNv +bTEgMB4GCSqGSIb3DQEJARYRYWRtaW5AZXhhbXBsZS5jb20wWTATBgcqhkjOPQIB +BggqhkjOPQMBBwNCAARidSirNIIrw+YHeS112z3N+1n0+uA9HjzkQ5u3QfZsRGCe +ivCEMQSs5+cUE/nv5WWda2kQWavxPGluHFtYmS10o4IBWDCCAVQwCQYDVR0TBAIw +ADALBgNVHQ8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMHsG +A1UdEQR0MHKCDSouZXhhbXBsZS5jb22CC2V4YW1wbGUuY29tghV4bi0tYmNoZXIt +a3ZhLmV4YW1wbGWBD29wc0BleGFtcGxlLmNvbYcECgAAAYcQIAENuAAAAAAAAAAA +AAAAAYYUaHR0cHM6Ly9leGFtcGxlLmNvbS8wXgYIKwYBBQUHAQEEUjBQMCQGCCsG +AQUFBzABhhhodHRwOi8vb2NzcC5leGFtcGxlLmNvbS8wKAYIKwYBBQUHMAKGHGh0 +dHA6Ly9jYS5leGFtcGxlLmNvbS9jYS5jcnQwHQYDVR0OBBYEFHyUnRTDwZxUPVwZ +RKmse3fjoeMMMB8GA1UdIwQYMBaAFAgaWVveRbA7VKqcHzjMqBPzi1/LMA0GCSqG +SIb3DQEBCwUAA4IBAQAfMTcpidoH47PXOQVxRttfrG7K9BcntyXP4+5hUAoSfMr6 +Rxy7X/9ba7R0yCGlNsUCusObaBpbdvwX8v9TiTYKVL+C9rEVGrTHnh6kbgZaaCQc +L3oWbfRrYnEBYuSlFuadjVvX42aqzHV28E4ITgywCxwubu2hAbacbcUHpnRSqLDq +lV+VZSQA/6cPNct7PR6AkJU80D+5wVOk7RVi4rsfbw1wFDeksCHu+0EThs9zjzL+ +S7HTCjB6AOHpbu1Sx+0d5YEKSE7EPF+dSFATNbhjTHzYQySnHK+Lm8nON8wmY1rC +7opOhvUlid1jA1d2BKO2ZxlxLir4pyIehTC1ddG2 +-----END CERTIFICATE----- +`; + +// A self-signed certificate without alternative names. +const PLAIN = `-----BEGIN CERTIFICATE----- +MIIC8jCCAdoCAQcwDQYJKoZIhvcNAQELBQAwPjEaMBgGA1UEAwwRcGxhaW4uZXhh +bXBsZS5vcmcxIDAeBgkqhkiG9w0BCQEWEXBsYWluQGV4YW1wbGUub3JnMCAXDTI2 +MTAyMjA5NDY0MVoYDzIxMjYwOTI4MDk0NjQxWjA+MRowGAYDVQQDDBFwbGFpbi5l +eGFtcGxlLm9yZzEgMB4GCSqGSIb3DQEJARYRcGxhaW5AZXhhbXBsZS5vcmcwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCq01NcNAy9Ng04mVuAFvmZUvsm +rY7aOaMgtODmptulgZ2qw25E09RgGYCn1S+4IYDu9hH9X4s+28Ag7Khmj4nmeoXN +of5095wche8DR2AAUElqyr/Nmgm2kTLdIJX4wl0eWXHIlmSPdOUNovA6JjGKH99b +8W7a2TqY/TsSadpPSvvN2esr7Jr4KgW7MXPUOnwNbcBOVcQP9rtFrduAWz01q0HM +02LboclnUnk/e58BXAj8vgdVojljDcc0bhbdz/Hh1/5gaaNILjJNOd4AgwZaCT02 +KojGz4Nb6MSgFmON2iMgAZjY1fXXkJ7wsfVPytjW73e7uV/RFuqSHQZenxupAgMB +AAEwDQYJKoZIhvcNAQELBQADggEBAJX1DGuiSLoCKBDI3oCgAu7X86G01ovEgZJr +8JrmGHW5q7ujjo53t46h8XPm3LJ8UV9/MZ+Yry43xB2txXYv3b3g/i+7JdTWLgUD +INA5VWvVkEZfm0lBEpU1Agpy4lV5uUCKZTgP/3MY5hnJ0YhQIs4BOjbwueMRFshL +yyHVb73MMbpWaZ7+T368seafa+ekeuY6gQly/HacuszjoYd6KKV3n8eDewenClCF +HoOF1Y4I+CvmR+a6B3iBczJ+a4tceFIIWsZUIeqzvGHpM7G9YwlmbvGprfLgR8Nv +igUib/D9tU3hdL6VWN5nMjdXV/8Le4nDnBjVBjYDkVbsnmT91AQ= +-----END CERTIFICATE----- +`; + +const wild = new X509Certificate(WILD); +const cn = new X509Certificate(Buffer.from(CN)); +const leaf = new X509Certificate(LEAF); +const plain = new X509Certificate(PLAIN); + +const hosts = [ + "foo.example.com", "f.example.com", "fx.example.com", "a.b.example.com", "foo.example.net", + "oo.example.net", "x.xn--bcher-kva.example", "xn--abc.example", "example.com", + "a.b.example.org", "x", "a.mixed.example", "A.MIXED.EXAMPLE", "a.dot.example", ".dot.example", + "dot.example", "bz.example.edu", "baz.example.edu", "b.z.example.edu", "x.cn.example", + "second.example", "192.168.1.1", ".example.com", ".mixed.example", "a.b.mixed.example", +]; +const opts: [string, X509CheckOptions | undefined][] = [ + ["default", undefined], + ["nowild", { wildcards: false }], + ["nopartial", { partialWildcards: false }], + ["multi", { multiLabelWildcards: true }], + ["single", { singleLabelSubdomains: true }], + ["always", { subject: "always" }], + ["never", { subject: "never" }], +]; +for (const [label, o] of opts) { + console.log(label, hosts.map((h) => `${h}=${wild.checkHost(h, o)}`).join(" ")); + console.log(label, "cn", hosts.map((h) => `${h}=${cn.checkHost(h, o)}`).join(" ")); +} + +for (const h of ["www.example.com", "example.com", "EXAMPLE.COM", "xn--bcher-kva.example", "", "foo.example.com."]) { + console.log("leaf", JSON.stringify(h), leaf.checkHost(h), leaf.checkHost(h, { subject: "always" })); +} +console.log("plain", plain.checkHost("plain.example.org"), plain.checkHost("plain.example.org", { subject: "never" })); +const found: string | undefined = leaf.checkHost("api.example.com"); +console.log("typed", found === undefined ? "none" : found.toUpperCase()); + +for (const e of ["Mixed@example.com", "mixed@example.com", "Mixed@EXAMPLE.COM", "a@b.example", "a@B.EXAMPLE", "A@b.example"]) { + console.log("email", e, wild.checkEmail(e), cn.checkEmail(e), cn.checkEmail(e, { subject: "never" })); +} +for (const e of ["ops@example.com", "OPS@EXAMPLE.COM", "admin@example.com"]) console.log("email", e, leaf.checkEmail(e)); +console.log("email plain", plain.checkEmail("plain@example.org"), plain.checkEmail("plain@example.org", { subject: "always" })); + +const ips = [ + "192.168.1.1", "192.168.001.001", "192.168.1.1.", "1.2.3", "::", "::ffff:192.168.1.1", + "0:0:0:0:0:0:0:0", "1::2::3", "fe80::1%eth0", " 192.168.1.1", "192.168.1.256", + "10.0.0.1", "2001:db8::1", "2001:DB8:0:0:0:0:0:1", "2001:db8:0::0:1", "::ffff:10.0.0.1", "", +]; +for (const ip of ips) { + try { + console.log("ip", JSON.stringify(ip), wild.checkIP(ip), leaf.checkIP(ip)); + } catch (e) { + const err = e as NodeJS.ErrnoException; + console.log("ip", JSON.stringify(ip), err.name, err.code, err.message); + } +} + +try { + leaf.checkHost("www.example.com", { subject: "sometimes" as "always" }); +} catch (e) { + const err = e as NodeJS.ErrnoException; + console.log("subject", err.name, err.code, err.message); +} diff --git a/tests/corpus/7091-x509-certificate-keys.ts b/tests/corpus/7091-x509-certificate-keys.ts new file mode 100644 index 000000000..b9b89ae1e --- /dev/null +++ b/tests/corpus/7091-x509-certificate-keys.ts @@ -0,0 +1,332 @@ +// X509Certificate.publicKey, verify, checkIssued and checkPrivateKey match +// Node: the public key as a cached KeyObject, signature checks for RSA +// PKCS#1, RSA-PSS, ECDSA and Ed25519 signatures, issuer checks by name, +// key identifier and key type, private key matches, and the pkey errors. +import { + X509Certificate, + constants, + createPrivateKey, + createPublicKey, + createSecretKey, + generateKeyPairSync, + sign, + verify, + type KeyObject, +} from "node:crypto"; + +// An RSA root and the EC leaf it signed. +const ROOT = `-----BEGIN CERTIFICATE----- +MIIDPTCCAiWgAwIBAgIDChHOMA0GCSqGSIb3DQEBCwUAMD4xCzAJBgNVBAYTAlVT +MRQwEgYDVQQKDAtFeGFtcGxlIE9yZzEZMBcGA1UEAwwQRXhhbXBsZSBSU0EgUm9v +dDAgFw0yNjEwMjIwOTQ2NDFaGA8yMTI2MDkyODA5NDY0MVowPjELMAkGA1UEBhMC +VVMxFDASBgNVBAoMC0V4YW1wbGUgT3JnMRkwFwYDVQQDDBBFeGFtcGxlIFJTQSBS +b290MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwsfidupnXQcn8PUF +XaCKRYw8XKI1GUn29U5/IUGjAnJVaRmcB6BPTmblbKt4y7gLvU4h1Hxuypx8FZSZ +30eZOS1ixyrymDthG5zUHqzXk5zAGsNvZeXjN3YxTC9vX5CpQ9Uz2i+bLOi3PJsE +qyKVZ+GGT9fpIaALG11YrWmXTUko/a0AKGot0w8O/RaMpXQpkbeyTBF6f4pHNMqx +QGy+KlpC/+yIJ9psT6qyU33HF3Wa+DOy+0txAFEQNw9m1j7adOLnU0zBpkM/bnP2 +fSwxMkDB2Di2P5H6PkdqLT4zBf/BLdg7WqpxxSqqgVmaSXhEqUQ6xjf51yLR3wy/ +xBWhMQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAd +BgNVHQ4EFgQUCBpZW95FsDtUqpwfOMyoE/OLX8swDQYJKoZIhvcNAQELBQADggEB +AChUtpNi8dPPxA4sf4EpZhEcYBN53X3WUcG3KJGtSf8JbPKru9lK9O+sZtJADbph +WoMPdWh8g+fEMk5D6l8Y5pDk/OR++oWNY7XCLFNyigmUGn+4SopJynBQiDk2dYPV +b6swDPPiV85TuAHVn79l766rtFdHdYwRe5HvfEkiXax7Egonlx7NR73xxm/XgvHQ +jhKbTZN9k/DEjkQuo5rt9ldVupUMM0iMhDMKX5oWEx9nvRwHPbjESS+Shy/FA4nA +2WHWO++4IU+imO40an1B/8SmFF+X9hj3UE6qaZLynVZRUiouBZibwZCcdCpibFM3 +kmB2s+NZXRvzhCjIxO97Ib4= +-----END CERTIFICATE----- +`; +const LEAF = `-----BEGIN CERTIFICATE----- +MIIDqjCCApKgAwIBAgICCwswDQYJKoZIhvcNAQELBQAwPjELMAkGA1UEBhMCVVMx +FDASBgNVBAoMC0V4YW1wbGUgT3JnMRkwFwYDVQQDDBBFeGFtcGxlIFJTQSBSb290 +MCAXDTI2MTAyMjA5NDY0MVoYDzIxMjYwOTI4MDk0NjQxWjBfMQswCQYDVQQGEwJV +UzEUMBIGA1UECgwLRXhhbXBsZSBPcmcxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNv +bTEgMB4GCSqGSIb3DQEJARYRYWRtaW5AZXhhbXBsZS5jb20wWTATBgcqhkjOPQIB +BggqhkjOPQMBBwNCAARidSirNIIrw+YHeS112z3N+1n0+uA9HjzkQ5u3QfZsRGCe +ivCEMQSs5+cUE/nv5WWda2kQWavxPGluHFtYmS10o4IBWDCCAVQwCQYDVR0TBAIw +ADALBgNVHQ8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMHsG +A1UdEQR0MHKCDSouZXhhbXBsZS5jb22CC2V4YW1wbGUuY29tghV4bi0tYmNoZXIt +a3ZhLmV4YW1wbGWBD29wc0BleGFtcGxlLmNvbYcECgAAAYcQIAENuAAAAAAAAAAA +AAAAAYYUaHR0cHM6Ly9leGFtcGxlLmNvbS8wXgYIKwYBBQUHAQEEUjBQMCQGCCsG +AQUFBzABhhhodHRwOi8vb2NzcC5leGFtcGxlLmNvbS8wKAYIKwYBBQUHMAKGHGh0 +dHA6Ly9jYS5leGFtcGxlLmNvbS9jYS5jcnQwHQYDVR0OBBYEFHyUnRTDwZxUPVwZ +RKmse3fjoeMMMB8GA1UdIwQYMBaAFAgaWVveRbA7VKqcHzjMqBPzi1/LMA0GCSqG +SIb3DQEBCwUAA4IBAQAfMTcpidoH47PXOQVxRttfrG7K9BcntyXP4+5hUAoSfMr6 +Rxy7X/9ba7R0yCGlNsUCusObaBpbdvwX8v9TiTYKVL+C9rEVGrTHnh6kbgZaaCQc +L3oWbfRrYnEBYuSlFuadjVvX42aqzHV28E4ITgywCxwubu2hAbacbcUHpnRSqLDq +lV+VZSQA/6cPNct7PR6AkJU80D+5wVOk7RVi4rsfbw1wFDeksCHu+0EThs9zjzL+ +S7HTCjB6AOHpbu1Sx+0d5YEKSE7EPF+dSFATNbhjTHzYQySnHK+Lm8nON8wmY1rC +7opOhvUlid1jA1d2BKO2ZxlxLir4pyIehTC1ddG2 +-----END CERTIFICATE----- +`; +// A self-signed RSA certificate and a self-signed Ed25519 certificate. +const PLAIN = `-----BEGIN CERTIFICATE----- +MIIC8jCCAdoCAQcwDQYJKoZIhvcNAQELBQAwPjEaMBgGA1UEAwwRcGxhaW4uZXhh +bXBsZS5vcmcxIDAeBgkqhkiG9w0BCQEWEXBsYWluQGV4YW1wbGUub3JnMCAXDTI2 +MTAyMjA5NDY0MVoYDzIxMjYwOTI4MDk0NjQxWjA+MRowGAYDVQQDDBFwbGFpbi5l +eGFtcGxlLm9yZzEgMB4GCSqGSIb3DQEJARYRcGxhaW5AZXhhbXBsZS5vcmcwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCq01NcNAy9Ng04mVuAFvmZUvsm +rY7aOaMgtODmptulgZ2qw25E09RgGYCn1S+4IYDu9hH9X4s+28Ag7Khmj4nmeoXN +of5095wche8DR2AAUElqyr/Nmgm2kTLdIJX4wl0eWXHIlmSPdOUNovA6JjGKH99b +8W7a2TqY/TsSadpPSvvN2esr7Jr4KgW7MXPUOnwNbcBOVcQP9rtFrduAWz01q0HM +02LboclnUnk/e58BXAj8vgdVojljDcc0bhbdz/Hh1/5gaaNILjJNOd4AgwZaCT02 +KojGz4Nb6MSgFmON2iMgAZjY1fXXkJ7wsfVPytjW73e7uV/RFuqSHQZenxupAgMB +AAEwDQYJKoZIhvcNAQELBQADggEBAJX1DGuiSLoCKBDI3oCgAu7X86G01ovEgZJr +8JrmGHW5q7ujjo53t46h8XPm3LJ8UV9/MZ+Yry43xB2txXYv3b3g/i+7JdTWLgUD +INA5VWvVkEZfm0lBEpU1Agpy4lV5uUCKZTgP/3MY5hnJ0YhQIs4BOjbwueMRFshL +yyHVb73MMbpWaZ7+T368seafa+ekeuY6gQly/HacuszjoYd6KKV3n8eDewenClCF +HoOF1Y4I+CvmR+a6B3iBczJ+a4tceFIIWsZUIeqzvGHpM7G9YwlmbvGprfLgR8Nv +igUib/D9tU3hdL6VWN5nMjdXV/8Le4nDnBjVBjYDkVbsnmT91AQ= +-----END CERTIFICATE----- +`; +const ED = `-----BEGIN CERTIFICATE----- +MIIBljCCAUigAwIBAgIUNFuRt3zKS19UAUMdLWewqHrGBigwBQYDK2VwMC8xFDAS +BgNVBAoMC0V4YW1wbGUgT3JnMRcwFQYDVQQDDA5lZC5leGFtcGxlLm5ldDAgFw0y +NjEwMjIxMDEwMTJaGA8yMTI2MDkyODEwMTAxMlowLzEUMBIGA1UECgwLRXhhbXBs +ZSBPcmcxFzAVBgNVBAMMDmVkLmV4YW1wbGUubmV0MCowBQYDK2VwAyEAOCSLKkel +kOH/5aqUwI3PNJVUUNyhgu2s19LFC254fNCjdDByMB0GA1UdDgQWBBR+50zUcqL8 +739jh6/rEYojULDmlTAfBgNVHSMEGDAWgBR+50zUcqL8739jh6/rEYojULDmlTAP +BgNVHRMBAf8EBTADAQH/MB8GA1UdEQQYMBaCDmVkLmV4YW1wbGUubmV0hwTAAAIH +MAUGAytlcANBAC/c0Bi0SVUnHCg/1pn7MaCgVivSHuoISi2SCq/0EnLefRdgsIir +0QxjVnxeuKN2Y1yfEO/R1W8Nwovo1l2M1gQ= +-----END CERTIFICATE----- +`; +// A second RSA root with its EC leaf, a self-signed RSA-PSS certificate +// and a self-signed v1 certificate with the second leaf's subject. +const ROOT2 = `-----BEGIN CERTIFICATE----- +MIIEITCCAwmgAwIBAgIIASNFZ4mrze8wDQYJKoZIhvcNAQELBQAwgZwxCzAJBgNV +BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNp +c2NvMRQwEgYDVQQKDAtFeGFtcGxlIE9yZzERMA8GA1UECwwIRGV2IFRlYW0xGDAW +BgNVBAMMD0V4YW1wbGUgUm9vdCBDQTEdMBsGCSqGSIb3DQEJARYOY2FAZXhhbXBs +ZS5jb20wIBcNMjYxMDIxMTYzMzUyWhgPMjEyNjA5MjcxNjMzNTJaMIGcMQswCQYD +VQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5j +aXNjbzEUMBIGA1UECgwLRXhhbXBsZSBPcmcxETAPBgNVBAsMCERldiBUZWFtMRgw +FgYDVQQDDA9FeGFtcGxlIFJvb3QgQ0ExHTAbBgkqhkiG9w0BCQEWDmNhQGV4YW1w +bGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0YGcCr0pxR8d +6qDdLuILcoT7vhtAjyHwfkoChnH6N3TLWzDUugnBp9NbDb7gDLqbRT4uTDoNb6zS +tEJZ4Q7/hhd0jDvoPmOy0Y3Kyj9uNsdf25GdbvwwEnlddELJCZprTZ4OhEg6w2s0 +Yy4SlviLzDXLmsggTypxeS+K8JHT9onMipi5L4GFu8n1QZbzXvSS4BkRgMXEyrtr +FOE9qSo2qkubFyd/WNKRTRIpTSc5/GGsqJ0FKDDJnN5adotcUucbJFn4noNnFUWh +M8mhBEj7ENDdJsD2EVFsI6H958jA2SbNNu4ngzAOs2AKNBYBfQK02ZQAdRNOSL3K +5FkfbII5LwIDAQABo2MwYTAdBgNVHQ4EFgQUP+yQyItpNYEls26Jfv+NnciGaK8w +HwYDVR0jBBgwFoAUP+yQyItpNYEls26Jfv+NnciGaK8wDwYDVR0TAQH/BAUwAwEB +/zAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQELBQADggEBABDw3Va/C+RFc/Hs +TJnHtQJtxFL0ZhnHUqg4DcDA3m+JDdIDNL8sNpLEbtn/pr0VeEWgG+MImWAQlrUW +IAZkLj9/Zg1wSO0oZ86ZyZJZmp54lUYXgG17CNl4g+F7VHt3bBwUJHTbWye/FKM6 +5OVZetCYKVj7Z+5BYuf7cumzvRqcn32fXidB/kjRX14SJ4JDPn2yFZWCr2ygIqWQ +SOuEmWbNAbYGaVZkQr2Ma8U28DWY2jb0f6nP8jLt49t9o6N/e6Fwd6I5t/mx9Mfj +0403omkxmr9vqN2kBamzVnun+2mDkRZcKPCr72Q4sfbvmDi/W5Nf1Iu2cQW8VfDO +SqpyT54= +-----END CERTIFICATE----- +`; +const LEAF2 = `-----BEGIN CERTIFICATE----- +MIIDuzCCAqOgAwIBAgIPAPAN+s4AESIzRFVmd4iZMA0GCSqGSIb3DQEBDAUAMIGc +MQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2Fu +IEZyYW5jaXNjbzEUMBIGA1UECgwLRXhhbXBsZSBPcmcxETAPBgNVBAsMCERldiBU +ZWFtMRgwFgYDVQQDDA9FeGFtcGxlIFJvb3QgQ0ExHTAbBgkqhkiG9w0BCQEWDmNh +QGV4YW1wbGUuY29tMB4XDTI2MTAyMTE2MzM1MloXDTM2MTAxODE2MzM1MlowJzER +MA8GA1UECgwITGVhZiBPcmcxEjAQBgNVBAMMCWxvY2FsaG9zdDBZMBMGByqGSM49 +AgEGCCqGSM49AwEHA0IABJVfVy9EH/VHQuRqpFFnXk0F00ncDht6APgVPDoybK8H +PlcM4T5H1yAOMHnlH29LK1jtMqU1aAOiiQRzPdCvqoujggE3MIIBMzAJBgNVHRME +AjAAMGgGA1UdEQRhMF+CCWxvY2FsaG9zdIINKi5leGFtcGxlLmNvbYcEfwAAAYcQ +AAAAAAAAAAAAAAAAAAAAAYERYWRtaW5AZXhhbXBsZS5jb22GGGh0dHBzOi8vZXhh +bXBsZS5jb20vcGF0aDBdBggrBgEFBQcBAQRRME8wIwYIKwYBBQUHMAGGF2h0dHA6 +Ly9vY3NwLmV4YW1wbGUuY29tMCgGCCsGAQUFBzAChhxodHRwOi8vY2EuZXhhbXBs +ZS5jb20vY2EuY3J0MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAdBgNV +HQ4EFgQUdKIx6ULFLPNP/kzwUFjSa5D19RswHwYDVR0jBBgwFoAUP+yQyItpNYEl +s26Jfv+NnciGaK8wDQYJKoZIhvcNAQEMBQADggEBABmQ8f2VsXNaMM9X1MEhi27D +LjwcOaaGEINNZq7xu/HL6UljMb1Odsuz66DkiH3Exd3MrWy6OlOpJJF0VzsqMsoD +0CRBxXF28o6Jil3vOGVwlYNmRidpkAkj4VyWDvv8FCS9VbsBtXaSHwu9wcCwePE0 +cGJRfTKoZ6KJ8vQ57DBEZ/ZszuhehOZYqrTmoUdjZ123kXdxQvK+ALn4SlIXHBhN +9unhd+8i00EmkXO/REw8paMcMVVGR+ioOEjyNHqZo6vkRqgpKggn1Yq8cTTDOMnS +P1o42EQZLlbuvx0kqInBqwqjDx4nZiPaifoDc9BF6iZPBvm8dhrEfghfB1tkNk0= +-----END CERTIFICATE----- +`; +const PSS = `-----BEGIN CERTIFICATE----- +MIIDdTCCAiigAwIBAgICAIAwQgYJKoZIhvcNAQEKMDWgDzANBglghkgBZQMEAgEF +AKEcMBoGCSqGSIb3DQEBCDANBglghkgBZQMEAgEFAKIEAgIA3jAeMQwwCgYDVQQD +DANwc3MxDjAMBgNVBAoMBWN0bAF4MB4XDTI2MTAyMTE2NDQzM1oXDTI2MTEyMDE2 +NDQzM1owHjEMMAoGA1UEAwwDcHNzMQ4wDAYDVQQKDAVjdGwBeDCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAL2pno8Ybwihdv3hnMhWPPt85SXQmFAJpFoS +E+sT9H6FbwFCIyPkY+SWk8H7y8V+D+388tNvJZmGjX3uvWpK2Wx6anSB3/WMpkUR +2ExP4cpSxZNr9EFzC/cdqLvsxJcHMHkc/uJuFbtjsuiH1ygOe3nyhWSNiPwGdwHE +cKkBFkzW3j2C/4Jkgt4k+sMe3gy5litKtMi4zZB8ixCm5oGhveGhUZy0Kb01twgZ +RP4FEd32sSRqq3NCx4cB0kyreTuzmk6U7BkxcdPzaj6DifjRznTQmjMdoCNZQRCj +G3KBOMHHyvmV2B4lNGDuMlcSap/TpYq7n32AH5sDvELrUnPwVzsCAwEAAaNTMFEw +HQYDVR0OBBYEFC/QJvAQsbQ25sErc1To2G1VuNZ0MB8GA1UdIwQYMBaAFC/QJvAQ +sbQ25sErc1To2G1VuNZ0MA8GA1UdEwEB/wQFMAMBAf8wQgYJKoZIhvcNAQEKMDWg +DzANBglghkgBZQMEAgEFAKEcMBoGCSqGSIb3DQEBCDANBglghkgBZQMEAgEFAKIE +AgIA3gOCAQEAu3lP2Q3Bb5fWUQtgFr/82Oke1ZRW/GRbj03mZ30izADHC7yk0OYm +02f7B5sub04Jk6yuRVmEBjJHSPNwAo0RJWtwvD77KLhG8aObr7Z3+pkfFvpGzqeS +MQ4FtK2MPps3kA1a5D4XLwkwDJtHWZVuVyXpPgxkptfLzZgrieKrpNyOKR8Pz/PO +Wlwv6bF7SXvAV8Vrd7KDvTqwQ5iq9UFhP6GeMDWezYtzLdjlvje7MhmZ96zgSY6m +qumNWOPHmiN4eoOE4M69B+0fa3WyMzubW0nVGM/sDhtS0k5zHs6Y4RLn11HolhXH +LvbvDgqtl/m5i0uHxEld+Z1uhzJscqFQRg== +-----END CERTIFICATE----- +`; +const V1 = `-----BEGIN CERTIFICATE----- +MIIBSTCB7wIUI3kYI5NpR07YqZe6RwOx22je9IowCgYIKoZIzj0EAwIwJzERMA8G +A1UECgwITGVhZiBPcmcxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0yNjEwMjExNjMz +NTJaFw0yNjExMjAxNjMzNTJaMCcxETAPBgNVBAoMCExlYWYgT3JnMRIwEAYDVQQD +DAlsb2NhbGhvc3QwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASVX1cvRB/1R0Lk +aqRRZ15NBdNJ3A4begD4FTw6MmyvBz5XDOE+R9cgDjB55R9vSytY7TKlNWgDookE +cz3Qr6qLMAoGCCqGSM49BAMCA0kAMEYCIQCFfFOw9qzzfSOQO1zRnT0sOXibjLWl +hHDyqecxhY7gtQIhAPMgqg+p1JXV5bgGpqgDfP3sE53luxMGlnAC39uhwBUh +-----END CERTIFICATE----- +`; + +const ROOT_KEY = `-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDCx+J26mddByfw +9QVdoIpFjDxcojUZSfb1Tn8hQaMCclVpGZwHoE9OZuVsq3jLuAu9TiHUfG7KnHwV +lJnfR5k5LWLHKvKYO2EbnNQerNeTnMAaw29l5eM3djFML29fkKlD1TPaL5ss6Lc8 +mwSrIpVn4YZP1+khoAsbXVitaZdNSSj9rQAoai3TDw79FoyldCmRt7JMEXp/ikc0 +yrFAbL4qWkL/7Ign2mxPqrJTfccXdZr4M7L7S3EAURA3D2bWPtp04udTTMGmQz9u +c/Z9LDEyQMHYOLY/kfo+R2otPjMF/8Et2DtaqnHFKqqBWZpJeESpRDrGN/nXItHf +DL/EFaExAgMBAAECggEADsvlabgryvbYs2bBlhe0eU0KZtURqI6kWoUwhiO40N5D +m7zMzA2DCMcrRYKPJhagmaxeSIh9WBnkGTWVLM+BFcVbXBrom+Ti3xieSohBxh5h +v4a6Jyj8BkINw6ZU1RMlsJf5HOmB5br19H4oMCHr7JzGubkCpkHEpjsEmgPL8Rxs +LYb9palDyTD1z5xVAQDAkvnCYQMX2DsiH8RuO4Ih0OoWMIsAGasS1dTQCkOzN9wF +cMg+hmJvQLN5xLYI+VPy/iZn+bmerCx8/NQBrbo+zibmnv7cDK1U9emVD70/Nd1d +B7ZEm+a4gReaxWVQGEkFMvs29u/TDWXYRnw0PGjUYwKBgQDrmKEx+ZiZwNNk/S3l +s8FgVrLFDr/+t5X33jaD5nVuSN6/AoR3qfh9piIyCrdNKdHicAcCnWP6skUFYLq3 +/tyMBATX3j+f3PHW3i2ZUAK26hgfp66F/3IyowePZbBDlGSmGsffVlc8on5fCk1Q +k3G0znktbrMCNxeeY9GSi1CHewKBgQDTpled6pZepZ54OGnKVJFo6DbQC0l4HQ6y +Rwv9BMtAz+AxWXda1oYK5bbJZrCmHyVP85VxkzRHOMxicv7M2kR9y2zZsJD7INqr +Nvt+lo2yGtIGv7kOdwWzvs4Pn7aCWSApzYBjuKrpP1xDzaMHhusAQtkk77YGCZCu +cCAQ/FjEQwKBgBq/1KWI8lOauiXAPULUWwNmNJv9Jb10+LkQ6N2m//RKn8fOMF4p +1UXm5AEMiG1hbywnjhQ07JIdRYrrxad4b3ASUL51xIGRI4BC76h1lilQgPavIjii +nUO0kPouEIpLwfW+Gq137yW6SWDfGZIpe4KM4uNOq1J4EN1x9eyOltEbAoGAP4bj +L5QVFE2T1jPglQlzJ9DjeuiYYcQfACXRFF3XABolCcr3xz0yuJygk60OAp7AnVHY +nmF3xJToy/gGQMiW3Ect4d1cdvQyjkePjzxzRW1Fcx8i1ZOmE4CG6yueJVvJCqMT +EVuBeUc6U2Oqtnyplsd509tbgE5oreMbzN1ha28CgYAHpG10ZQRDeqWFIo/ttk6Q +Q1Gf53eqZFCgh8GO1hBJ7+Fp+Ec5S9UPMBDjZb6GET7ifF8Zit/lR4gN2PuELwJA +YaXTIehCugH7Q3oTjNcQtls6tW+/+jEGjbjSHyZvwk8EFHc0J7PWRszB36s+qM+O +YPorNipExSeWti1506ljLQ== +-----END PRIVATE KEY----- +`; +const LEAF_KEY = `-----BEGIN EC PRIVATE KEY----- +MHcCAQEEINqbifA6kN/DSN0cMD2s7fXiNtPXZqipFrbCs/3mCGbtoAoGCCqGSM49 +AwEHoUQDQgAEYnUoqzSCK8PmB3ktdds9zftZ9PrgPR485EObt0H2bERgnorwhDEE +rOfnFBP57+VlnWtpEFmr8TxpbhxbWJktdA== +-----END EC PRIVATE KEY----- +`; +const PLAIN_KEY = `-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCq01NcNAy9Ng04 +mVuAFvmZUvsmrY7aOaMgtODmptulgZ2qw25E09RgGYCn1S+4IYDu9hH9X4s+28Ag +7Khmj4nmeoXNof5095wche8DR2AAUElqyr/Nmgm2kTLdIJX4wl0eWXHIlmSPdOUN +ovA6JjGKH99b8W7a2TqY/TsSadpPSvvN2esr7Jr4KgW7MXPUOnwNbcBOVcQP9rtF +rduAWz01q0HM02LboclnUnk/e58BXAj8vgdVojljDcc0bhbdz/Hh1/5gaaNILjJN +Od4AgwZaCT02KojGz4Nb6MSgFmON2iMgAZjY1fXXkJ7wsfVPytjW73e7uV/RFuqS +HQZenxupAgMBAAECggEAAxY5ACZHoaanpAFvbBfeMEVV2PvTZEGc30MtejWDLBE0 +yGhqJm++e3MShlXdnCHs2sSTDCVNFphxdxbXryzCVOobX10hRDtSNJi9+izwwOiy +fu3FfAFf0g77IJBNbXaTqK/8lJ+VFxuJZQ7v8NkOVZzgVCC/fFBnFOskhV9ozLx6 +Uk+5Rg/UFenUhhHqrzwxCLKR3TAX8pBdidmbJF61ypxt6Cn3pOA/pB1gUBi76nau +w4kefdNj8AXzSjzOfVEg0quywfeoOq6ESdy3p4RQMt6NZ3wnCgzr/uuWjVQzJbJU +Mg2TVlrKzbiinel2ZqKV8lpymhFTlRaWNzK7PzD91QKBgQDaSDpdQIk2+CJMAVzU +LsI5tdZa0cMDbAZzxBmg1W6kwIrMzYVeMo7RPBarhCuQ/3gJFVvA7LdvXR87BbYa +UuDXbvO4Gv+2wqxxX961sWK85VqhwAqXhFbkS9KNJ0twmlmAcX9lvl0Zn4ciO3c+ +AMJT4JN4WR6wI/dt1TCHLa1wtQKBgQDIV9bgKinpUzhSy9byTH0CwghwudAPx2CA +9gJ6VcjyUKeBdgJ0PQMTwppyrPs0fz+NgJ6+LiTvp9BS9vrXmtnjbD1oAxFcC9uU +NL7BJSRY+wgmnPgbldA25xq1TRJmB8YMr1CO4gYKGSNM7I1HlNv4dvMcS295RhTG +cdDkl4T7pQKBgQCzJmygdIMplmWvs76Ydvf5nYAcceR4PoTdJqlSE1oH5e8FP/ih +4yTMHWENzCBllQbTjbAMmzvCLs+fBYT7/eGDbxCgnagaO5SESIbk4H2FQ0grUQ5g +ZMFcBc79Aq8r78JdZMmGm8KVcRTbx6qKPN1BMiRWte416o78meiGSbr7sQKBgQCJ +2AY8TkZwB5ZlQWMva3lIAIEWLoL9mEzJ7npd+dA9CUl3Rd8CY0hOvELQTDy7LiJo +FesjJ1S6oFgx/UUyO5VXaQ4HFrVHA9Gc832cHxYXMwG0r6P2NuoAHJq/y7uxAABA +kg93gARvxkQHlWrPqZiAKDe1bCHS62k38O+tmKMjEQKBgHF7a9tqZVoc8CqUL/I8 +FXRMRmdzC4RoWC2NDqMazW9qxXpFZmPux1q9ds6ikoG0v3pAxZmRH0eMTIujtLPn +chtAnvTvkg5Q+/xa429PFThJ2b04zsHCmnzfdMaIPvqhBqoPmyAmptjUeMB+yzQU +d3AigXwTBk4l4yPleDD4qUbO +-----END PRIVATE KEY----- +`; +const ED_KEY = `-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEID8VP7HLPAW9FXkVxo3eYbqZ8zkXsPFi3c2RwdDdwy/O +-----END PRIVATE KEY----- +`; + +const certs: [string, X509Certificate][] = [ + ["root", new X509Certificate(ROOT)], + ["leaf", new X509Certificate(Buffer.from(LEAF))], + ["plain", new X509Certificate(PLAIN)], + ["ed", new X509Certificate(ED)], + ["root2", new X509Certificate(ROOT2)], + ["leaf2", new X509Certificate(LEAF2)], + ["pss", new X509Certificate(PSS)], + ["v1", new X509Certificate(V1)], +]; + +function describe(key: KeyObject): string { + const details = key.asymmetricKeyDetails ?? {}; + const size = key.asymmetricKeyType === "ed25519" ? "32 bytes" : `${details.modulusLength ?? details.namedCurve}`; + return `${key.type} ${key.asymmetricKeyType} ${size}`; +} + +for (const [name, cert] of certs) { + const key = cert.publicKey; + const spki = key.export({ type: "spki", format: "der" }); + console.log(name, describe(key), key === cert.publicKey, spki.length, createPublicKey(cert.toString()).equals(key)); +} + +for (const [name, cert] of certs) { + const by: string[] = []; + const issued: string[] = []; + for (const [other, signer] of certs) { + if (cert.verify(signer.publicKey)) by.push(other); + if (cert.checkIssued(signer)) issued.push(other); + } + console.log(name, "verifies with", by.join(",") || "-", "issued by", issued.join(",") || "-"); +} + +const keys: [string, KeyObject][] = [ + ["root", createPrivateKey(ROOT_KEY)], + ["leaf", createPrivateKey(Buffer.from(LEAF_KEY))], + ["plain", createPrivateKey({ key: PLAIN_KEY, format: "pem" })], + ["ed", createPrivateKey(ED_KEY)], + ["fresh", generateKeyPairSync("ec", { namedCurve: "prime256v1" }).privateKey], +]; +for (const [name, cert] of certs) { + console.log(name, "private", keys.filter(([, key]) => cert.checkPrivateKey(key)).map(([k]) => k).join(",") || "-"); +} + +const leaf = certs[1]![1]; +function attempt(label: string, f: () => boolean): void { + try { + console.log(label, f()); + } catch (e) { + const err = e as NodeJS.ErrnoException; + console.log(label, err.name, err.code, err.message); + } +} +attempt("verify private", () => leaf.verify(keys[0]![1])); +attempt("verify secret", () => leaf.verify(createSecretKey(Buffer.alloc(16)))); +attempt("check public", () => leaf.checkPrivateKey(leaf.publicKey)); +attempt("check secret", () => leaf.checkPrivateKey(createSecretKey(Buffer.alloc(16)))); + +// A PSS signature check on the root's public key leaves no padding behind +// for the certificate checks that follow. +const root = certs[0]![1]; +const message = Buffer.from("pss"); +const pssSignature = sign("sha256", message, { key: keys[0]![1], padding: constants.RSA_PKCS1_PSS_PADDING }); +const pssValid = verify("sha256", message, { key: root.publicKey, padding: constants.RSA_PKCS1_PSS_PADDING }, pssSignature); +console.log("after pss", pssValid, root.verify(root.publicKey), leaf.verify(root.publicKey)); + +// An outer signatureAlgorithm that differs from the TBSCertificate's, here +// without its NULL parameters, does not verify. +function header(der: Buffer, at: number): [number, number] { + const first = der[at + 1]!; + if (first < 0x80) return [2, first]; + let length = 0; + for (let i = 0; i < (first & 0x7f); i++) length = length * 256 + der[at + 2 + i]!; + return [2 + (first & 0x7f), length]; +} +const rootDer = root.raw; +const [outerHeader] = header(rootDer, 0); +const [tbsHeader, tbsLength] = header(rootDer, outerHeader); +const algAt = outerHeader + tbsHeader + tbsLength; +const [algHeader, algLength] = header(rootDer, algAt); +const algorithm = rootDer.subarray(algAt, algAt + algHeader + algLength); +const oid = algorithm.subarray(2, 2 + 2 + algorithm[3]!); +const bare = Buffer.concat([Buffer.from([0x30, oid.length]), oid]); +const body = Buffer.concat([rootDer.subarray(outerHeader, algAt), bare, rootDer.subarray(algAt + algHeader + algLength)]); +const altered = new X509Certificate(Buffer.concat([Buffer.from([0x30, 0x82, body.length >> 8, body.length & 0xff]), body])); +console.log("outer algorithm", algorithm.toString("hex"), "->", bare.toString("hex"), altered.subject.split("\n").join("/"), altered.verify(root.publicKey)); diff --git a/tests/corpus/7092-x509-legacy-object.ts b/tests/corpus/7092-x509-legacy-object.ts new file mode 100644 index 000000000..6b423860a --- /dev/null +++ b/tests/corpus/7092-x509-legacy-object.ts @@ -0,0 +1,158 @@ +// X509Certificate.toLegacyObject and toString match Node: the +// legacy object's members in Node's order (subject and issuer objects +// with repeated attributes as arrays, alternative names, access +// descriptions by method, the RSA modulus, exponent and SPKI or the EC +// point and curve names, the validity window, fingerprints, extended key +// usages, serial number and raw bytes), its inspect output, and the PEM +// text of toString, String() and template literals. +import { X509Certificate } from "node:crypto"; + +// An EC leaf with every alternative name kind, and the RSA root that signed it. +const LEAF = `-----BEGIN CERTIFICATE----- +MIIDqjCCApKgAwIBAgICCwswDQYJKoZIhvcNAQELBQAwPjELMAkGA1UEBhMCVVMx +FDASBgNVBAoMC0V4YW1wbGUgT3JnMRkwFwYDVQQDDBBFeGFtcGxlIFJTQSBSb290 +MCAXDTI2MTAyMjA5NDY0MVoYDzIxMjYwOTI4MDk0NjQxWjBfMQswCQYDVQQGEwJV +UzEUMBIGA1UECgwLRXhhbXBsZSBPcmcxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNv +bTEgMB4GCSqGSIb3DQEJARYRYWRtaW5AZXhhbXBsZS5jb20wWTATBgcqhkjOPQIB +BggqhkjOPQMBBwNCAARidSirNIIrw+YHeS112z3N+1n0+uA9HjzkQ5u3QfZsRGCe +ivCEMQSs5+cUE/nv5WWda2kQWavxPGluHFtYmS10o4IBWDCCAVQwCQYDVR0TBAIw +ADALBgNVHQ8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMHsG +A1UdEQR0MHKCDSouZXhhbXBsZS5jb22CC2V4YW1wbGUuY29tghV4bi0tYmNoZXIt +a3ZhLmV4YW1wbGWBD29wc0BleGFtcGxlLmNvbYcECgAAAYcQIAENuAAAAAAAAAAA +AAAAAYYUaHR0cHM6Ly9leGFtcGxlLmNvbS8wXgYIKwYBBQUHAQEEUjBQMCQGCCsG +AQUFBzABhhhodHRwOi8vb2NzcC5leGFtcGxlLmNvbS8wKAYIKwYBBQUHMAKGHGh0 +dHA6Ly9jYS5leGFtcGxlLmNvbS9jYS5jcnQwHQYDVR0OBBYEFHyUnRTDwZxUPVwZ +RKmse3fjoeMMMB8GA1UdIwQYMBaAFAgaWVveRbA7VKqcHzjMqBPzi1/LMA0GCSqG +SIb3DQEBCwUAA4IBAQAfMTcpidoH47PXOQVxRttfrG7K9BcntyXP4+5hUAoSfMr6 +Rxy7X/9ba7R0yCGlNsUCusObaBpbdvwX8v9TiTYKVL+C9rEVGrTHnh6kbgZaaCQc +L3oWbfRrYnEBYuSlFuadjVvX42aqzHV28E4ITgywCxwubu2hAbacbcUHpnRSqLDq +lV+VZSQA/6cPNct7PR6AkJU80D+5wVOk7RVi4rsfbw1wFDeksCHu+0EThs9zjzL+ +S7HTCjB6AOHpbu1Sx+0d5YEKSE7EPF+dSFATNbhjTHzYQySnHK+Lm8nON8wmY1rC +7opOhvUlid1jA1d2BKO2ZxlxLir4pyIehTC1ddG2 +-----END CERTIFICATE----- +`; +const ROOT = `-----BEGIN CERTIFICATE----- +MIIDPTCCAiWgAwIBAgIDChHOMA0GCSqGSIb3DQEBCwUAMD4xCzAJBgNVBAYTAlVT +MRQwEgYDVQQKDAtFeGFtcGxlIE9yZzEZMBcGA1UEAwwQRXhhbXBsZSBSU0EgUm9v +dDAgFw0yNjEwMjIwOTQ2NDFaGA8yMTI2MDkyODA5NDY0MVowPjELMAkGA1UEBhMC +VVMxFDASBgNVBAoMC0V4YW1wbGUgT3JnMRkwFwYDVQQDDBBFeGFtcGxlIFJTQSBS +b290MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwsfidupnXQcn8PUF +XaCKRYw8XKI1GUn29U5/IUGjAnJVaRmcB6BPTmblbKt4y7gLvU4h1Hxuypx8FZSZ +30eZOS1ixyrymDthG5zUHqzXk5zAGsNvZeXjN3YxTC9vX5CpQ9Uz2i+bLOi3PJsE +qyKVZ+GGT9fpIaALG11YrWmXTUko/a0AKGot0w8O/RaMpXQpkbeyTBF6f4pHNMqx +QGy+KlpC/+yIJ9psT6qyU33HF3Wa+DOy+0txAFEQNw9m1j7adOLnU0zBpkM/bnP2 +fSwxMkDB2Di2P5H6PkdqLT4zBf/BLdg7WqpxxSqqgVmaSXhEqUQ6xjf51yLR3wy/ +xBWhMQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAd +BgNVHQ4EFgQUCBpZW95FsDtUqpwfOMyoE/OLX8swDQYJKoZIhvcNAQELBQADggEB +AChUtpNi8dPPxA4sf4EpZhEcYBN53X3WUcG3KJGtSf8JbPKru9lK9O+sZtJADbph +WoMPdWh8g+fEMk5D6l8Y5pDk/OR++oWNY7XCLFNyigmUGn+4SopJynBQiDk2dYPV +b6swDPPiV85TuAHVn79l766rtFdHdYwRe5HvfEkiXax7Egonlx7NR73xxm/XgvHQ +jhKbTZN9k/DEjkQuo5rt9ldVupUMM0iMhDMKX5oWEx9nvRwHPbjESS+Shy/FA4nA +2WHWO++4IU+imO40an1B/8SmFF+X9hj3UE6qaZLynVZRUiouBZibwZCcdCpibFM3 +kmB2s+NZXRvzhCjIxO97Ib4= +-----END CERTIFICATE----- +`; +// An EC leaf with access descriptions and extended key usages. +const ACCESS = `-----BEGIN CERTIFICATE----- +MIIDuzCCAqOgAwIBAgIPAPAN+s4AESIzRFVmd4iZMA0GCSqGSIb3DQEBDAUAMIGc +MQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2Fu +IEZyYW5jaXNjbzEUMBIGA1UECgwLRXhhbXBsZSBPcmcxETAPBgNVBAsMCERldiBU +ZWFtMRgwFgYDVQQDDA9FeGFtcGxlIFJvb3QgQ0ExHTAbBgkqhkiG9w0BCQEWDmNh +QGV4YW1wbGUuY29tMB4XDTI2MTAyMTE2MzM1MloXDTM2MTAxODE2MzM1MlowJzER +MA8GA1UECgwITGVhZiBPcmcxEjAQBgNVBAMMCWxvY2FsaG9zdDBZMBMGByqGSM49 +AgEGCCqGSM49AwEHA0IABJVfVy9EH/VHQuRqpFFnXk0F00ncDht6APgVPDoybK8H +PlcM4T5H1yAOMHnlH29LK1jtMqU1aAOiiQRzPdCvqoujggE3MIIBMzAJBgNVHRME +AjAAMGgGA1UdEQRhMF+CCWxvY2FsaG9zdIINKi5leGFtcGxlLmNvbYcEfwAAAYcQ +AAAAAAAAAAAAAAAAAAAAAYERYWRtaW5AZXhhbXBsZS5jb22GGGh0dHBzOi8vZXhh +bXBsZS5jb20vcGF0aDBdBggrBgEFBQcBAQRRME8wIwYIKwYBBQUHMAGGF2h0dHA6 +Ly9vY3NwLmV4YW1wbGUuY29tMCgGCCsGAQUFBzAChhxodHRwOi8vY2EuZXhhbXBs +ZS5jb20vY2EuY3J0MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAdBgNV +HQ4EFgQUdKIx6ULFLPNP/kzwUFjSa5D19RswHwYDVR0jBBgwFoAUP+yQyItpNYEl +s26Jfv+NnciGaK8wDQYJKoZIhvcNAQEMBQADggEBABmQ8f2VsXNaMM9X1MEhi27D +LjwcOaaGEINNZq7xu/HL6UljMb1Odsuz66DkiH3Exd3MrWy6OlOpJJF0VzsqMsoD +0CRBxXF28o6Jil3vOGVwlYNmRidpkAkj4VyWDvv8FCS9VbsBtXaSHwu9wcCwePE0 +cGJRfTKoZ6KJ8vQ57DBEZ/ZszuhehOZYqrTmoUdjZ123kXdxQvK+ALn4SlIXHBhN +9unhd+8i00EmkXO/REw8paMcMVVGR+ioOEjyNHqZo6vkRqgpKggn1Yq8cTTDOMnS +P1o42EQZLlbuvx0kqInBqwqjDx4nZiPaifoDc9BF6iZPBvm8dhrEfghfB1tkNk0= +-----END CERTIFICATE----- +`; +// A multi-valued RDN, an Ed25519 key and names that need escaping. +const MULTI = `-----BEGIN CERTIFICATE----- +MIICNzCCAaCgAwIBAgIBADANBgkqhkiG9w0BAQsFADA3MSgwCgYDVQQLDANyZG4w +DAYDVQQDDAVtdWx0aTAMBgNVBAoMBXRocmVlMQswCQYDVQQGEwJERTAeFw0yNjEw +MjExNjM0MDVaFw0yNjEwMzExNjM0MDVaMDcxKDAKBgNVBAsMA3JkbjAMBgNVBAMM +BW11bHRpMAwGA1UECgwFdGhyZWUxCzAJBgNVBAYTAkRFMIGfMA0GCSqGSIb3DQEB +AQUAA4GNADCBiQKBgQDdv64EOPdpcWdjVMAlEQvNzm3ZbQYXrhOwRF9yzQp25SuA +3uKvStZOt8hyTifAd30oDXIfgyYI39xKHEkHfARTV1E2Jwyiz86/pDHmFypPFJuI +v/vOjYLLeisiVi8TFfHgmoGqGvySR83fwiUnoOOWj9Bqt/5EVBQbBeWrU9qH6QID +AQABo1MwUTAdBgNVHQ4EFgQUZrGU0gkgqCkmegRtu6vQ3h7kpPUwHwYDVR0jBBgw +FoAUZrGU0gkgqCkmegRtu6vQ3h7kpPUwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG +9w0BAQsFAAOBgQAMzFkzJElc9VJYhGx7AngO8Yve22iCTOVLO3/be8w55klb/HJX +vfESQz+EJLYVaP5Q/uChoE0I27NytkeUSpJFwmjyqoQq2j6kJWGKONqhQSnXDrms +HMPA7iik/C5hOqpwzDZ98wNU/e86TrqZxzpFkwbnZx9MSfiGr690s5BfpA== +-----END CERTIFICATE----- +`; +const ED = `-----BEGIN CERTIFICATE----- +MIICSDCCAfqgAwIBAgIB+zAFBgMrZXAwLDEVMBMGA1UEAx4MAGIAbQBwACAA6U4t +MRMwEQYDVQQKHgoAcABsAGEAaQBuMB4XDTI2MTAyMTE2NDQzM1oXDTI2MTEyMDE2 +NDQzM1owLDEVMBMGA1UEAx4MAGIAbQBwACAA6U4tMRMwEQYDVQQKHgoAcABsAGEA +aQBuMCowBQYDK2VwAyEAP+2Vcp8nwu2AhFvSczYTxGs1jV/ckA76Jqvwj9QDGqij +ggE/MIIBOzCBtwYDVR0RBIGvMIGsoBsGCisGAQQBgjcUAgOgDQwLdXNlckBkb21h +aW6gEAYDKgMEoAkMB3Vua25vd26kRTBDMQswCQYDVQQGEwJVSzEZMBcGA1UECh4Q +AEQAaQByACwAIABPAHIAZzEZMBcGA1UEAx4QAGQAaQByACAAbgBhAG0AZYgEKgME +BYcQ/oAAAAAAAAAAAAAAAAEAAocQIAENuAAAAAAAAP8AAEKDKYYKaHR0cDovL3gv +cTAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIHgDBFBggrBgEFBQcBAQQ5MDcwIAYI +KwYBBQUHMAWGFGh0dHA6Ly9yZXBvLmV4YW1wbGUvMBMGBCoDBAWCC29kZC5leGFt +cGxlMB0GA1UdDgQWBBQYMJ0fQJ1Nx0PmatIfTJusAEA9WTAFBgMrZXADQQCixubE +nuunBZYYPXF52+joQSX/a5DFkOEDmzRTuH7IIpPFWPo37rmJOSdPA/PV8v9clzYF +LP3EjM4YOAatcbsO +-----END CERTIFICATE----- +`; +const ODD = `-----BEGIN CERTIFICATE----- +MIICXzCCAeWgAwIBAgIBATAKBggqhkjOPQQDAjBvMQ0wCwYDVQQDDARhLCBiMQ4w +DAYDVQQHDAUjbGVhZDESMBAGA1UECAwJICBzcGFjZSAgMSYwJAYDVQQKDB1RdW90 +ZSBxIGJhY2tcc2xhc2g7IHNlbWkgPGx0PjESMBAGA1UECwwJQ2Fmw6kg4oKsMB4X +DTI2MTAyMTE2MzM1MloXDTI3MTAyMTE2MzM1MlowbzENMAsGA1UEAwwEYSwgYjEO +MAwGA1UEBwwFI2xlYWQxEjAQBgNVBAgMCSAgc3BhY2UgIDEmMCQGA1UECgwdUXVv +dGUgcSBiYWNrXHNsYXNoOyBzZW1pIDxsdD4xEjAQBgNVBAsMCUNhZsOpIOKCrDB2 +MBAGByqGSM49AgEGBSuBBAAiA2IABMLLzwDsSeezCzScnC9xTIFl35dFAX+1Uagk +0y4Y72rOOgVFA13SMIbMfv2InIyg2vXOdqn0y7RqiaaqqnAYZB2RHHL0SD7t2MVL +K3BQajy7Oi6IeDDRGK1jM4D6nk/1AKNVMFMwMgYDVR0RBCswKYILYSxiLmV4YW1w +bGWCDXBsYWluLmV4YW1wbGWBBXhAeS56hwQKAAABMB0GA1UdDgQWBBSR4U6nIpTt +5rLoIZZxfxSyx9Uc9zAKBggqhkjOPQQDAgNoADBlAjEAlznFL0Nrt1jjZrT3/GN0 +yN/8gxO3SawyqoWrfP3uk1GScTh0PlrTEOrpSyuq3wl5AjBRntVI1ZoUM46pCwIw +0Vl3c3lns7OljcQFiR7o8yRhEW9ejdiR2W6PLBrjCly6U3I= +-----END CERTIFICATE----- +`; + +const certs: [string, X509Certificate][] = [ + ["leaf", new X509Certificate(LEAF)], + ["root", new X509Certificate(Buffer.from(ROOT))], + ["access", new X509Certificate(ACCESS)], + ["multi", new X509Certificate(MULTI)], + ["ed", new X509Certificate(ED)], + ["odd", new X509Certificate(ODD)], +]; + +for (const [name, cert] of certs) { + const legacy = cert.toLegacyObject(); + const shown = JSON.stringify(legacy, (key: string, value: unknown) => { + if (value === undefined) return ""; + if (key === "raw" || key === "pubkey") return `<${(value as { data: number[] }).data.length} bytes>`; + return value; + }); + console.log(name, shown); + const absent = Object.keys(legacy).filter((key) => (legacy as unknown as Record)[key] === undefined); + console.log(name, "absent", absent.join(","), Buffer.isBuffer(legacy.raw), legacy.raw.equals(cert.raw), legacy.raw === cert.raw); + const spki: Buffer = cert.publicKey.export({ type: "spki", format: "der" }); + if (legacy.pubkey !== undefined) console.log(name, "pubkey", legacy.pubkey.equals(spki)); +} + +console.log(certs[0]![1].toLegacyObject()); +console.log(certs[3]![1].toLegacyObject()); + +for (const [name, cert] of certs) { + const pem = cert.toString(); + console.log(name, pem.length, pem === String(cert), `${cert}` === pem, new X509Certificate(pem).fingerprint256 === cert.fingerprint256); +} +console.log(certs[3]![1].toString()); diff --git a/tests/corpus/7093-x509-methods-js.mjs b/tests/corpus/7093-x509-methods-js.mjs new file mode 100644 index 000000000..a8144d071 --- /dev/null +++ b/tests/corpus/7093-x509-methods-js.mjs @@ -0,0 +1,158 @@ +// X509Certificate's methods from JavaScript match Node: publicKey, +// verify, checkIssued, checkPrivateKey, checkHost, checkEmail, checkIP, +// toLegacyObject, toString and toJSON, including the TypeErrors for +// arguments of the wrong type. +import { X509Certificate, createPrivateKey, createPublicKey, createSecretKey } from "node:crypto"; + +// An RSA root, the EC leaf it signed, and a self-signed RSA certificate. +const ROOT = `-----BEGIN CERTIFICATE----- +MIIDPTCCAiWgAwIBAgIDChHOMA0GCSqGSIb3DQEBCwUAMD4xCzAJBgNVBAYTAlVT +MRQwEgYDVQQKDAtFeGFtcGxlIE9yZzEZMBcGA1UEAwwQRXhhbXBsZSBSU0EgUm9v +dDAgFw0yNjEwMjIwOTQ2NDFaGA8yMTI2MDkyODA5NDY0MVowPjELMAkGA1UEBhMC +VVMxFDASBgNVBAoMC0V4YW1wbGUgT3JnMRkwFwYDVQQDDBBFeGFtcGxlIFJTQSBS +b290MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwsfidupnXQcn8PUF +XaCKRYw8XKI1GUn29U5/IUGjAnJVaRmcB6BPTmblbKt4y7gLvU4h1Hxuypx8FZSZ +30eZOS1ixyrymDthG5zUHqzXk5zAGsNvZeXjN3YxTC9vX5CpQ9Uz2i+bLOi3PJsE +qyKVZ+GGT9fpIaALG11YrWmXTUko/a0AKGot0w8O/RaMpXQpkbeyTBF6f4pHNMqx +QGy+KlpC/+yIJ9psT6qyU33HF3Wa+DOy+0txAFEQNw9m1j7adOLnU0zBpkM/bnP2 +fSwxMkDB2Di2P5H6PkdqLT4zBf/BLdg7WqpxxSqqgVmaSXhEqUQ6xjf51yLR3wy/ +xBWhMQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAd +BgNVHQ4EFgQUCBpZW95FsDtUqpwfOMyoE/OLX8swDQYJKoZIhvcNAQELBQADggEB +AChUtpNi8dPPxA4sf4EpZhEcYBN53X3WUcG3KJGtSf8JbPKru9lK9O+sZtJADbph +WoMPdWh8g+fEMk5D6l8Y5pDk/OR++oWNY7XCLFNyigmUGn+4SopJynBQiDk2dYPV +b6swDPPiV85TuAHVn79l766rtFdHdYwRe5HvfEkiXax7Egonlx7NR73xxm/XgvHQ +jhKbTZN9k/DEjkQuo5rt9ldVupUMM0iMhDMKX5oWEx9nvRwHPbjESS+Shy/FA4nA +2WHWO++4IU+imO40an1B/8SmFF+X9hj3UE6qaZLynVZRUiouBZibwZCcdCpibFM3 +kmB2s+NZXRvzhCjIxO97Ib4= +-----END CERTIFICATE----- +`; +const LEAF = `-----BEGIN CERTIFICATE----- +MIIDqjCCApKgAwIBAgICCwswDQYJKoZIhvcNAQELBQAwPjELMAkGA1UEBhMCVVMx +FDASBgNVBAoMC0V4YW1wbGUgT3JnMRkwFwYDVQQDDBBFeGFtcGxlIFJTQSBSb290 +MCAXDTI2MTAyMjA5NDY0MVoYDzIxMjYwOTI4MDk0NjQxWjBfMQswCQYDVQQGEwJV +UzEUMBIGA1UECgwLRXhhbXBsZSBPcmcxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNv +bTEgMB4GCSqGSIb3DQEJARYRYWRtaW5AZXhhbXBsZS5jb20wWTATBgcqhkjOPQIB +BggqhkjOPQMBBwNCAARidSirNIIrw+YHeS112z3N+1n0+uA9HjzkQ5u3QfZsRGCe +ivCEMQSs5+cUE/nv5WWda2kQWavxPGluHFtYmS10o4IBWDCCAVQwCQYDVR0TBAIw +ADALBgNVHQ8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMHsG +A1UdEQR0MHKCDSouZXhhbXBsZS5jb22CC2V4YW1wbGUuY29tghV4bi0tYmNoZXIt +a3ZhLmV4YW1wbGWBD29wc0BleGFtcGxlLmNvbYcECgAAAYcQIAENuAAAAAAAAAAA +AAAAAYYUaHR0cHM6Ly9leGFtcGxlLmNvbS8wXgYIKwYBBQUHAQEEUjBQMCQGCCsG +AQUFBzABhhhodHRwOi8vb2NzcC5leGFtcGxlLmNvbS8wKAYIKwYBBQUHMAKGHGh0 +dHA6Ly9jYS5leGFtcGxlLmNvbS9jYS5jcnQwHQYDVR0OBBYEFHyUnRTDwZxUPVwZ +RKmse3fjoeMMMB8GA1UdIwQYMBaAFAgaWVveRbA7VKqcHzjMqBPzi1/LMA0GCSqG +SIb3DQEBCwUAA4IBAQAfMTcpidoH47PXOQVxRttfrG7K9BcntyXP4+5hUAoSfMr6 +Rxy7X/9ba7R0yCGlNsUCusObaBpbdvwX8v9TiTYKVL+C9rEVGrTHnh6kbgZaaCQc +L3oWbfRrYnEBYuSlFuadjVvX42aqzHV28E4ITgywCxwubu2hAbacbcUHpnRSqLDq +lV+VZSQA/6cPNct7PR6AkJU80D+5wVOk7RVi4rsfbw1wFDeksCHu+0EThs9zjzL+ +S7HTCjB6AOHpbu1Sx+0d5YEKSE7EPF+dSFATNbhjTHzYQySnHK+Lm8nON8wmY1rC +7opOhvUlid1jA1d2BKO2ZxlxLir4pyIehTC1ddG2 +-----END CERTIFICATE----- +`; +const PLAIN = `-----BEGIN CERTIFICATE----- +MIIC8jCCAdoCAQcwDQYJKoZIhvcNAQELBQAwPjEaMBgGA1UEAwwRcGxhaW4uZXhh +bXBsZS5vcmcxIDAeBgkqhkiG9w0BCQEWEXBsYWluQGV4YW1wbGUub3JnMCAXDTI2 +MTAyMjA5NDY0MVoYDzIxMjYwOTI4MDk0NjQxWjA+MRowGAYDVQQDDBFwbGFpbi5l +eGFtcGxlLm9yZzEgMB4GCSqGSIb3DQEJARYRcGxhaW5AZXhhbXBsZS5vcmcwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCq01NcNAy9Ng04mVuAFvmZUvsm +rY7aOaMgtODmptulgZ2qw25E09RgGYCn1S+4IYDu9hH9X4s+28Ag7Khmj4nmeoXN +of5095wche8DR2AAUElqyr/Nmgm2kTLdIJX4wl0eWXHIlmSPdOUNovA6JjGKH99b +8W7a2TqY/TsSadpPSvvN2esr7Jr4KgW7MXPUOnwNbcBOVcQP9rtFrduAWz01q0HM +02LboclnUnk/e58BXAj8vgdVojljDcc0bhbdz/Hh1/5gaaNILjJNOd4AgwZaCT02 +KojGz4Nb6MSgFmON2iMgAZjY1fXXkJ7wsfVPytjW73e7uV/RFuqSHQZenxupAgMB +AAEwDQYJKoZIhvcNAQELBQADggEBAJX1DGuiSLoCKBDI3oCgAu7X86G01ovEgZJr +8JrmGHW5q7ujjo53t46h8XPm3LJ8UV9/MZ+Yry43xB2txXYv3b3g/i+7JdTWLgUD +INA5VWvVkEZfm0lBEpU1Agpy4lV5uUCKZTgP/3MY5hnJ0YhQIs4BOjbwueMRFshL +yyHVb73MMbpWaZ7+T368seafa+ekeuY6gQly/HacuszjoYd6KKV3n8eDewenClCF +HoOF1Y4I+CvmR+a6B3iBczJ+a4tceFIIWsZUIeqzvGHpM7G9YwlmbvGprfLgR8Nv +igUib/D9tU3hdL6VWN5nMjdXV/8Le4nDnBjVBjYDkVbsnmT91AQ= +-----END CERTIFICATE----- +`; +const ROOT_KEY = `-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDCx+J26mddByfw +9QVdoIpFjDxcojUZSfb1Tn8hQaMCclVpGZwHoE9OZuVsq3jLuAu9TiHUfG7KnHwV +lJnfR5k5LWLHKvKYO2EbnNQerNeTnMAaw29l5eM3djFML29fkKlD1TPaL5ss6Lc8 +mwSrIpVn4YZP1+khoAsbXVitaZdNSSj9rQAoai3TDw79FoyldCmRt7JMEXp/ikc0 +yrFAbL4qWkL/7Ign2mxPqrJTfccXdZr4M7L7S3EAURA3D2bWPtp04udTTMGmQz9u +c/Z9LDEyQMHYOLY/kfo+R2otPjMF/8Et2DtaqnHFKqqBWZpJeESpRDrGN/nXItHf +DL/EFaExAgMBAAECggEADsvlabgryvbYs2bBlhe0eU0KZtURqI6kWoUwhiO40N5D +m7zMzA2DCMcrRYKPJhagmaxeSIh9WBnkGTWVLM+BFcVbXBrom+Ti3xieSohBxh5h +v4a6Jyj8BkINw6ZU1RMlsJf5HOmB5br19H4oMCHr7JzGubkCpkHEpjsEmgPL8Rxs +LYb9palDyTD1z5xVAQDAkvnCYQMX2DsiH8RuO4Ih0OoWMIsAGasS1dTQCkOzN9wF +cMg+hmJvQLN5xLYI+VPy/iZn+bmerCx8/NQBrbo+zibmnv7cDK1U9emVD70/Nd1d +B7ZEm+a4gReaxWVQGEkFMvs29u/TDWXYRnw0PGjUYwKBgQDrmKEx+ZiZwNNk/S3l +s8FgVrLFDr/+t5X33jaD5nVuSN6/AoR3qfh9piIyCrdNKdHicAcCnWP6skUFYLq3 +/tyMBATX3j+f3PHW3i2ZUAK26hgfp66F/3IyowePZbBDlGSmGsffVlc8on5fCk1Q +k3G0znktbrMCNxeeY9GSi1CHewKBgQDTpled6pZepZ54OGnKVJFo6DbQC0l4HQ6y +Rwv9BMtAz+AxWXda1oYK5bbJZrCmHyVP85VxkzRHOMxicv7M2kR9y2zZsJD7INqr +Nvt+lo2yGtIGv7kOdwWzvs4Pn7aCWSApzYBjuKrpP1xDzaMHhusAQtkk77YGCZCu +cCAQ/FjEQwKBgBq/1KWI8lOauiXAPULUWwNmNJv9Jb10+LkQ6N2m//RKn8fOMF4p +1UXm5AEMiG1hbywnjhQ07JIdRYrrxad4b3ASUL51xIGRI4BC76h1lilQgPavIjii +nUO0kPouEIpLwfW+Gq137yW6SWDfGZIpe4KM4uNOq1J4EN1x9eyOltEbAoGAP4bj +L5QVFE2T1jPglQlzJ9DjeuiYYcQfACXRFF3XABolCcr3xz0yuJygk60OAp7AnVHY +nmF3xJToy/gGQMiW3Ect4d1cdvQyjkePjzxzRW1Fcx8i1ZOmE4CG6yueJVvJCqMT +EVuBeUc6U2Oqtnyplsd509tbgE5oreMbzN1ha28CgYAHpG10ZQRDeqWFIo/ttk6Q +Q1Gf53eqZFCgh8GO1hBJ7+Fp+Ec5S9UPMBDjZb6GET7ifF8Zit/lR4gN2PuELwJA +YaXTIehCugH7Q3oTjNcQtls6tW+/+jEGjbjSHyZvwk8EFHc0J7PWRszB36s+qM+O +YPorNipExSeWti1506ljLQ== +-----END PRIVATE KEY----- +`; +const LEAF_KEY = `-----BEGIN EC PRIVATE KEY----- +MHcCAQEEINqbifA6kN/DSN0cMD2s7fXiNtPXZqipFrbCs/3mCGbtoAoGCCqGSM49 +AwEHoUQDQgAEYnUoqzSCK8PmB3ktdds9zftZ9PrgPR485EObt0H2bERgnorwhDEE +rOfnFBP57+VlnWtpEFmr8TxpbhxbWJktdA== +-----END EC PRIVATE KEY----- +`; + +const ca = new X509Certificate(ROOT); +const leaf = new X509Certificate(Buffer.from(LEAF)); +const plain = new X509Certificate(PLAIN); +const caKey = createPrivateKey(ROOT_KEY); +const leafKey = createPrivateKey(LEAF_KEY); +function t(label, f) { + try { + const v = f(); + console.log(label, typeof v === "string" ? JSON.stringify(v) : v); + } catch (e) { + console.log(label, "THROW", e.name, e.code, e.message); + } +} +t("pub", () => [leaf.publicKey.asymmetricKeyType, ca.publicKey.asymmetricKeyType, ca.publicKey.asymmetricKeyDetails.modulusLength]); +t("pub same", () => leaf.publicKey === leaf.publicKey); +t("pub equals", () => leaf.publicKey.equals(createPublicKey(leafKey))); +t("verify ca", () => leaf.verify(ca.publicKey)); +t("verify self", () => leaf.verify(leaf.publicKey)); +t("verify ca self", () => ca.verify(ca.publicKey)); +t("verify private", () => leaf.verify(caKey)); +t("verify string", () => leaf.verify("x")); +t("verify secret", () => leaf.verify(createSecretKey(Buffer.alloc(16)))); +t("issued", () => [leaf.checkIssued(ca), ca.checkIssued(leaf), ca.checkIssued(ca), plain.checkIssued(plain), leaf.checkIssued(plain)]); +t("issued bad", () => leaf.checkIssued({})); +t("pk", () => [leaf.checkPrivateKey(leafKey), leaf.checkPrivateKey(caKey), ca.checkPrivateKey(caKey)]); +t("pk public", () => leaf.checkPrivateKey(leaf.publicKey)); +t("pk string", () => leaf.checkPrivateKey("x")); +for (const h of ["www.example.com", "foo.example.com", "a.b.example.com", "example.com", "EXAMPLE.COM", "xn--bcher-kva.example", "other.org", "10.0.0.1", "", "foo.example.com."]) t("host " + h, () => leaf.checkHost(h)); +t("host plain", () => plain.checkHost("plain.example.org")); +t("host plain never", () => plain.checkHost("plain.example.org", { subject: "never" })); +t("host leaf always", () => leaf.checkHost("www.example.com", { subject: "always" })); +t("host nowild", () => leaf.checkHost("foo.example.com", { wildcards: false })); +t("host partial", () => leaf.checkHost("foo.example.com", { partialWildcards: false })); +t("host multi", () => leaf.checkHost("a.b.example.com", { multiLabelWildcards: true })); +t("host single", () => leaf.checkHost("example.com", { singleLabelSubdomains: true })); +t("host num", () => leaf.checkHost(1)); +t("host opts bad", () => leaf.checkHost("x", { subject: "sometimes" })); +t("host opts type", () => leaf.checkHost("x", 5)); +t("host wild type", () => leaf.checkHost("x", { wildcards: "yes" })); +for (const e of ["ops@example.com", "OPS@EXAMPLE.COM", "admin@example.com", "nobody@example.com"]) t("email " + e, () => leaf.checkEmail(e)); +t("email plain", () => plain.checkEmail("plain@example.org")); +t("email plain never", () => plain.checkEmail("plain@example.org", { subject: "never" })); +t("email num", () => leaf.checkEmail(1)); +for (const ip of ["10.0.0.1", "2001:db8::1", "2001:DB8:0:0:0:0:0:1", "10.0.0.2", "::ffff:10.0.0.1", "bogus", ""]) t("ip " + ip, () => leaf.checkIP(ip)); +t("ip num", () => leaf.checkIP(1)); +t("ip opts", () => leaf.checkIP("10.0.0.1", {})); +for (const cert of [leaf, ca, plain]) { + const legacy = cert.toLegacyObject(); + console.log(Object.keys(legacy).join(","), legacy.subject.CN, legacy.bits, legacy.raw.length); +} +t("pem", () => [leaf.toString() === leaf.toJSON(), String(ca) === `${ca}`, ca.toString().split("\n").length]); +t("pem round trip", () => new X509Certificate(plain.toJSON()).fingerprint === plain.fingerprint); diff --git a/tests/corpus/7095-tls-peer-certificates.ts b/tests/corpus/7095-tls-peer-certificates.ts new file mode 100644 index 000000000..eee3c9ac8 --- /dev/null +++ b/tests/corpus/7095-tls-peer-certificates.ts @@ -0,0 +1,188 @@ +// tlsSocket.getPeerCertificate([detailed]), getCertificate(), +// getPeerX509Certificate() and getX509Certificate() match Node: the +// legacy certificate objects, {} before a handshake or without a +// certificate and null once the socket is destroyed; the detailed +// issuerCertificate chain from the certificates the peer sent and then +// the trust anchors, in any order, ending at a self-issued root that is its +// own issuer; and the certificates served by SNI answers, secureContext +// wraps and https servers. +import { X509Certificate } from "node:crypto"; +import { readFileSync } from "node:fs"; +import * as https from "node:https"; +import * as net from "node:net"; +import type { AddressInfo } from "node:net"; +import * as tls from "node:tls"; + +const d = "tests/fixtures/server/certs"; +const read = (f: string): Buffer => readFileSync(`${d}/${f}`); +const ca = read("ca.pem"), ca2 = read("ca2.pem"), cert = read("localhost.pem"), key = read("localhost-key.pem"); +const self = read("selfsigned.pem"), selfKey = read("selfsigned-key.pem"), alt = read("alt.pem"), altKey = read("alt-key.pem"); + +function describe(label: string, o: tls.PeerCertificate | tls.DetailedPeerCertificate | object | null): string { + if (o === null) return `${label} null`; + const keys = Object.keys(o); + if (keys.length === 0) return `${label} {}`; + const p = o as tls.DetailedPeerCertificate; + const issuer = p.issuerCertificate; + const link = issuer ? ` issuerCert:${issuer.subject.CN} self:${issuer === issuer.issuerCertificate} same:${issuer === p}` : ""; + return `${label} ${keys.join(",")} ${p.subject.CN} ${p.issuer.CN}${link}`; +} +function show(label: string, o: tls.PeerCertificate | tls.DetailedPeerCertificate | object | null): void { + console.log(describe(label, o)); +} +// A server's lines print once its client has the reply, so the output +// order does not depend on which side's callback runs first. +const serverLines: string[] = []; +function flushServer(): void { + for (const line of serverLines.splice(0)) console.log(line); +} +await new Promise((done) => { +const server = tls.createServer({ cert, key }, (s) => { + serverLines.push(describe("server peer", s.getPeerCertificate())); + serverLines.push(describe("server peer detailed", s.getPeerCertificate(true))); + serverLines.push(`server peerX509 ${s.getPeerX509Certificate()?.subject}`); + serverLines.push(describe("server own", s.getCertificate())); + serverLines.push(`server ownX509 ${s.getX509Certificate()?.subject}`); + s.end("hi"); +}); +server.listen(0, () => { + const c = tls.connect({ port: (server.address() as AddressInfo).port, ca, servername: "localhost" }, () => { + show("client peer", c.getPeerCertificate()); + const det = c.getPeerCertificate(true); + show("client peer detailed", det); + show("client issuer", det.issuerCertificate); + const x = c.getPeerX509Certificate(); + if (x) console.log("client peerX509", x.subject, x.issuer, x === c.getPeerX509Certificate(), x.checkIssued(new X509Certificate(ca)), x.checkHost("localhost")); + show("client own", c.getCertificate()); + console.log("client ownX509", c.getX509Certificate()?.subject); + console.log("peer raw eq", c.getPeerCertificate().raw.equals(x!.raw), c.getPeerCertificate() === c.getPeerCertificate()); + }); + c.on("data", () => {}); + c.on("end", () => { + flushServer(); + show("after end peer", c.getPeerCertificate()); + c.destroy(); + }); + c.on("close", () => { + show("after close peer", c.getPeerCertificate()); + console.log("after close X509", c.getPeerX509Certificate()?.subject, c.getCertificate()); + server.close(() => done()); + }); + show("before handshake", c.getPeerCertificate()); + console.log("before X509", c.getPeerX509Certificate()?.subject, c.getCertificate(), c.getX509Certificate()?.subject); +}); +}); + +function chain(o: tls.DetailedPeerCertificate | null): string { + if (o === null) return "null"; + if (Object.keys(o).length === 0) return "{}"; + const names: string[] = []; + let at: tls.DetailedPeerCertificate | undefined = o; + for (let i = 0; at !== undefined && i < 6; i++) { + names.push(at.subject.CN as string); + const next: tls.DetailedPeerCertificate | undefined = at.issuerCertificate; + if (next === at) { + names.push("(self)"); + break; + } + at = next; + } + return names.join(" -> "); +} +async function run(label: string, served: Buffer, servedKey: Buffer, trust: Buffer | null): Promise { + await new Promise((done) => { + const server = tls.createServer({ cert: served, key: servedKey }, (s) => s.end()); + server.listen(0, () => { + const port = (server.address() as AddressInfo).port; + const socket = + trust === null + ? tls.connect({ port, servername: "localhost", rejectUnauthorized: false }) + : tls.connect({ port, servername: "localhost", ca: trust, rejectUnauthorized: false }); + socket.on("secureConnect", () => { + console.log(label, socket.authorized, chain(socket.getPeerCertificate(true))); + console.log(label, "abbreviated", "issuerCertificate" in socket.getPeerCertificate(), "issuerCertificate" in socket.getPeerCertificate(false)); + socket.destroy(); + console.log(label, "destroyed", socket.getPeerCertificate(), socket.getPeerX509Certificate()?.subject, socket.getCertificate()); + server.close(() => done()); + }); + socket.on("error", (e) => console.log(label, "error", e.message)); + }); + }); +} +await run("leaf", cert, key, ca); +await run("sent chain", Buffer.concat([cert, ca]), key, ca); +await run("sent chain, default store", Buffer.concat([cert, ca]), key, null); +await run("out of order", Buffer.concat([cert, ca2, ca]), key, ca); +await run("self-signed", self, selfKey, null); +await run("wrong ca", cert, key, ca2); +await run("leaf, default store", cert, key, null); + +// SNI and secureContext: the certificate the server socket serves. +await new Promise((done) => { + const altCtx = tls.createSecureContext({ cert: alt, key: altKey }); + const server = tls.createServer({ cert, key, SNICallback: (name: string, cb: (err: Error | null, ctx?: tls.SecureContext) => void) => cb(null, name === "alt.localhost" ? altCtx : undefined) }, (s) => { + serverLines.push(`sni own ${(s.getCertificate() as tls.PeerCertificate).subject.CN} ${s.getX509Certificate()?.subject}`); + s.end("x"); + }); + server.listen(0, () => { + const port = (server.address() as AddressInfo).port; + const a = tls.connect({ port, ca, servername: "alt.localhost" }, () => { + console.log("sni peer", a.getPeerCertificate().subject.CN, a.getPeerX509Certificate()?.subjectAltName); + }); + a.resume(); + a.on("end", () => { + flushServer(); + a.destroy(); + const b = tls.connect({ port, ca, servername: "localhost" }, () => { + console.log("sni default peer", b.getPeerCertificate().subject.CN); + }); + b.resume(); + b.on("end", () => { + flushServer(); + b.destroy(); + server.close(() => done()); + }); + }); + }); +}); + +// new tls.TLSSocket over a net connection, with a secureContext. +await new Promise((done) => { + const context = tls.createSecureContext({ cert: alt, key: altKey }); + const server = net.createServer((raw) => { + const secure = new tls.TLSSocket(raw, { isServer: true, secureContext: context }); + console.log("wrap before", (secure.getCertificate() as tls.PeerCertificate).subject?.CN, secure.getX509Certificate()?.subject, JSON.stringify(secure.getPeerCertificate())); + secure.on("secure", () => { + serverLines.push(`wrap secure ${(secure.getCertificate() as tls.PeerCertificate).subject.CN} ${secure.getPeerX509Certificate()?.subject}`); + secure.end("x"); + }); + }); + server.listen(0, () => { + const c = tls.connect({ port: (server.address() as AddressInfo).port, ca, servername: "alt.localhost" }, () => { + console.log("wrap client", chain(c.getPeerCertificate(true))); + }); + c.resume(); + c.on("end", () => { + flushServer(); + c.destroy(); + server.close(() => done()); + }); + }); +}); + +// https: both sides of a request. +await new Promise((done) => { + const server = https.createServer({ cert, key }, (req, res) => { + const s = req.socket as tls.TLSSocket; + console.log("https server", (s.getCertificate() as tls.PeerCertificate).subject.CN, JSON.stringify(s.getPeerCertificate())); + res.end("ok"); + }); + server.listen(0, () => { + https.get({ port: (server.address() as AddressInfo).port, ca, host: "localhost", path: "/" }, (res) => { + const s = res.socket as tls.TLSSocket; + console.log("https client", chain(s.getPeerCertificate(true)), s.getPeerX509Certificate()?.issuer); + res.resume(); + res.on("end", () => server.close(() => done())); + }); + }); +}); diff --git a/tests/corpus/7096-tls-peer-certificates.mjs b/tests/corpus/7096-tls-peer-certificates.mjs new file mode 100644 index 000000000..ef1bfeb2a --- /dev/null +++ b/tests/corpus/7096-tls-peer-certificates.mjs @@ -0,0 +1,35 @@ +// tlsSocket.getPeerCertificate from JavaScript matches Node: only a +// detailed argument that is true itself adds the issuerCertificate chain, +// and the legacy object is a plain object with a Buffer raw and arrays for +// extended key usages. +import * as tls from "node:tls"; +import { readFileSync } from "node:fs"; + +const d = "tests/fixtures/server/certs"; +const ca = readFileSync(`${d}/ca.pem`); +const cert = readFileSync(`${d}/localhost.pem`); +const key = readFileSync(`${d}/localhost-key.pem`); + +const server = tls.createServer({ cert, key }, (s) => s.end("x")); +server.listen(0, () => { + const c = tls.connect({ port: server.address().port, ca, servername: "localhost" }, () => { + for (const arg of [true, false, 1, "yes", null, undefined, {}]) { + const peer = c.getPeerCertificate(arg); + console.log(String(arg), "issuerCertificate" in peer, peer.issuer.CN); + } + const legacy = c.getPeerCertificate(); + console.log(Object.keys(legacy).length, Buffer.isBuffer(legacy.raw), legacy.ext_key_usage, JSON.stringify(legacy.subject)); + const detailed = c.getPeerCertificate(true); + const root = detailed.issuerCertificate; + console.log(root.subject.CN, root.issuerCertificate === root, root.ca, Buffer.isBuffer(root.raw)); + const x509 = c.getPeerX509Certificate(); + if (x509) console.log(x509.subject, x509.checkHost("localhost"), x509.checkIssued(x509), legacy.raw.equals(x509.raw)); + console.log(c.getCertificate(), c.getX509Certificate() === undefined); + }); + c.resume(); + c.on("end", () => { + c.destroy(); + console.log(c.getPeerCertificate(), c.getPeerX509Certificate() === undefined, c.getCertificate()); + server.close(); + }); +}); diff --git a/tests/corpus/7097-tls-client-certificates.ts b/tests/corpus/7097-tls-client-certificates.ts new file mode 100644 index 000000000..b45560084 --- /dev/null +++ b/tests/corpus/7097-tls-client-certificates.ts @@ -0,0 +1,84 @@ +// Client certificates match Node: tls.createServer's requestCert, +// rejectUnauthorized and ca against clients that send no certificate, a +// valid one, one without the clientAuth purpose and a self-signed one +// (socket.authorized and authorizationError on the server, the peer +// certificate, the client's own certificate), the client's chain completed +// from its ca, and a server that requires a certificate rejecting a client +// without one in TLS 1.3 and TLS 1.2 (the client's events and codes). +import { readFileSync } from "node:fs"; +import type { AddressInfo } from "node:net"; +import * as tls from "node:tls"; + +const D = "tests/fixtures/server/certs"; +const r = (f: string): Buffer => readFileSync(`${D}/${f}`); +const ca = r("ca.pem"), ca2 = r("ca2.pem"), cert = r("localhost.pem"), key = r("localhost-key.pem"); +const client = r("client.pem"), clientKey = r("client-key.pem"); +const self = r("selfsigned.pem"), selfKey = r("selfsigned-key.pem"); + +type Handler = (s: tls.TLSSocket) => void; +const servers: [string, (h: Handler) => tls.Server][] = [ + ["request", (h) => tls.createServer({ cert, key, requestCert: true, rejectUnauthorized: false, ca }, h)], + ["reject", (h) => tls.createServer({ cert, key, requestCert: true, ca }, h)], + ["request-noca", (h) => tls.createServer({ cert, key, requestCert: true, rejectUnauthorized: false }, h)], + ["request-ca2", (h) => tls.createServer({ cert, key, requestCert: true, rejectUnauthorized: false, ca: ca2 }, h)], + // an empty Buffer is an empty trust store; an empty string is no ca + ["request-empty", (h) => tls.createServer({ cert, key, requestCert: true, rejectUnauthorized: false, ca: Buffer.alloc(0) }, h)], + ["request-emptystr", (h) => tls.createServer({ cert, key, requestCert: true, rejectUnauthorized: false, ca: "" }, h)], + ["plain", (h) => tls.createServer({ cert, key }, h)], +]; +const clients: [string, Buffer | null, Buffer | null][] = [ + ["none", null, null], + ["valid", client, clientKey], + ["serverEku", cert, key], + ["self", self, selfKey], +]; +async function run(label: string, make: (h: Handler) => tls.Server, name: string, clientCert: Buffer | null, clientKeyPem: Buffer | null): Promise { + await new Promise((done) => { + const events: string[] = []; + const server = make((s) => { + events.push(`server secureConnection authorized=${s.authorized} error=${s.authorizationError} peer=${(s.getPeerCertificate() as tls.PeerCertificate).subject?.CN} x509=${s.getPeerX509Certificate()?.subject}`); + s.end("ok"); + }); + server.listen(0, () => { + const port = (server.address() as AddressInfo).port; + const c = + clientCert === null || clientKeyPem === null + ? tls.connect({ port, ca, servername: "localhost" }) + : tls.connect({ port, ca, servername: "localhost", cert: clientCert, key: clientKeyPem }); + c.on("secureConnect", () => events.push(`client secureConnect own=${(c.getCertificate() as tls.PeerCertificate | null)?.subject?.CN}`)); + c.on("data", (d: Buffer) => events.push(`client data ${d}`)); + c.on("end", () => events.push("client end")); + c.on("error", (e: NodeJS.ErrnoException) => events.push(`client error ${e.code}`)); + c.on("close", () => { + setTimeout(() => { + console.log(label, name, "|", events.join(" ; ")); + server.close(() => done()); + }, 50); + }); + }); + }); +} +for (const [label, make] of servers) { + for (const [name, c, k] of clients) await run(label, make, name, c, k); +} + +// A server that requires a certificate, per protocol version: TLS 1.3 +// rejects after the client's handshake, TLS 1.2 during it. +for (const maxVersion of ["TLSv1.3", "TLSv1.2"] as const) { + await new Promise((done) => { + const server = tls.createServer({ cert, key, requestCert: true, ca, maxVersion }, (s) => s.end("ok")); + server.listen(0, () => { + const events: string[] = []; + const c = tls.connect({ port: (server.address() as AddressInfo).port, ca, servername: "localhost" }); + c.on("secureConnect", () => events.push("secureConnect")); + c.on("data", () => events.push("data")); + c.on("end", () => events.push("end")); + c.on("error", (e: NodeJS.ErrnoException) => events.push(`error ${e.code}`)); + c.on("close", (hadError: boolean) => { + events.push(`close ${hadError}`); + console.log(maxVersion, events.join(" ; ")); + server.close(() => done()); + }); + }); + }); +} diff --git a/tests/corpus/7098-tls-client-certificates-paths.ts b/tests/corpus/7098-tls-client-certificates-paths.ts new file mode 100644 index 000000000..b71e72b6f --- /dev/null +++ b/tests/corpus/7098-tls-client-certificates-paths.ts @@ -0,0 +1,161 @@ +// Client certificates on the other paths match Node: a server's (and a +// SecureContext's) ca completes the certificate chain it sends, as +// OpenSSL's automatic chain does; new tls.TLSSocket with requestCert and +// rejectUnauthorized, where Node reports no verdict; and https on both +// sides, including an agent that pools sockets per client certificate. +import { readFileSync } from "node:fs"; +import * as http from "node:http"; +import * as https from "node:https"; +import * as net from "node:net"; +import type { AddressInfo } from "node:net"; +import * as tls from "node:tls"; + +const D = "tests/fixtures/server/certs"; +const r = (f: string): Buffer => readFileSync(`${D}/${f}`); +const ca = r("ca.pem"), ca2 = r("ca2.pem"), cert = r("localhost.pem"), key = r("localhost-key.pem"); +const client = r("client.pem"), clientKey = r("client-key.pem"); + +// A server's ca completes the chain it sends. +for (const [label, make] of [ + ["no ca", () => tls.createServer({ cert, key }, (s) => s.end())], + ["ca", () => tls.createServer({ cert, key, ca }, (s) => s.end())], + ["other ca", () => tls.createServer({ cert, key, ca: ca2 }, (s) => s.end())], + // An SNI answer's own ca does not complete its chain. + ["sni ctx ca, server no ca", () => { + const context = tls.createSecureContext({ cert, key, ca }); + return tls.createServer({ cert, key, SNICallback: (name: string, cb: (err: Error | null, ctx?: tls.SecureContext) => void) => cb(null, context) }, (s) => s.end()); + }], +] as [string, () => tls.Server][]) { + await new Promise((done) => { + const server = make(); + server.listen(0, () => { + const c = tls.connect({ port: (server.address() as AddressInfo).port, ca: ca2, servername: "localhost", rejectUnauthorized: false }, () => { + const names: string[] = []; + let p: tls.DetailedPeerCertificate | undefined = c.getPeerCertificate(true); + for (let i = 0; p !== undefined && i < 4; i++) { + names.push(p.subject.CN as string); + if (p.issuerCertificate === p) break; + p = p.issuerCertificate; + } + console.log(label, c.authorizationError, names.join(" -> ")); + c.destroy(); + server.close(() => done()); + }); + }); + }); +} + +// A TLSSocket's secureContext sends the chain its ca completes. +for (const [label, context] of [ + ["wrap ctx ca", tls.createSecureContext({ cert, key, ca })], + ["wrap ctx", tls.createSecureContext({ cert, key })], +] as [string, tls.SecureContext][]) { + await new Promise((done) => { + const server = net.createServer((raw) => { + const s = new tls.TLSSocket(raw, { isServer: true, secureContext: context }); + s.on("secure", () => s.end()); + }); + server.listen(0, () => { + const c = tls.connect({ port: (server.address() as AddressInfo).port, ca: ca2, servername: "localhost", rejectUnauthorized: false }, () => { + const names: string[] = []; + let p: tls.DetailedPeerCertificate | undefined = c.getPeerCertificate(true); + for (let i = 0; p !== undefined && i < 4; i++) { + names.push(p.subject.CN as string); + if (p.issuerCertificate === p) break; + p = p.issuerCertificate; + } + console.log(label, c.authorizationError, names.join(" -> ")); + c.destroy(); + server.close(() => done()); + }); + }); + }); +} + +// new tls.TLSSocket with client certificates: Node reports no verdict there. +for (const [label, wrap] of [ + ["wrap request", (raw: net.Socket) => new tls.TLSSocket(raw, { isServer: true, cert, key, requestCert: true, ca })], + ["wrap reject", (raw: net.Socket) => new tls.TLSSocket(raw, { isServer: true, cert, key, requestCert: true, rejectUnauthorized: true, ca })], +] as [string, (raw: net.Socket) => tls.TLSSocket][]) { + for (const withCert of [false, true]) { + await new Promise((done) => { + const events: string[] = []; + const server = net.createServer((raw) => { + const s = wrap(raw); + s.on("secure", () => { + events.push(`wrap secure authorized=${s.authorized} error=${s.authorizationError} peer=${(s.getPeerCertificate() as tls.PeerCertificate).subject?.CN}`); + s.end("ok"); + }); + s.on("error", (e: NodeJS.ErrnoException) => events.push(`wrap error ${e.code}`)); + }); + server.listen(0, () => { + const port = (server.address() as AddressInfo).port; + const c = withCert + ? tls.connect({ port, ca, servername: "localhost", cert: client, key: clientKey }) + : tls.connect({ port, ca, servername: "localhost" }); + c.on("secureConnect", () => events.push("client secureConnect")); + c.on("data", (d: Buffer) => events.push(`client data ${d}`)); + c.on("error", (e: NodeJS.ErrnoException) => events.push(`client error ${e.code}`)); + c.on("close", () => { + setTimeout(() => { + console.log(label, withCert ? "cert" : "none", "|", events.join(" ; ")); + server.close(() => done()); + }, 50); + }); + }); + }); + } +} + +// https with client certificates, on both sides. +await new Promise((done) => { + const server = https.createServer({ cert, key, requestCert: true, rejectUnauthorized: false, ca }, (req, res) => { + const s = req.socket as tls.TLSSocket; + res.end(`${s.authorized} ${s.authorizationError} ${(s.getPeerCertificate() as tls.PeerCertificate).subject?.CN}`); + }); + server.listen(0, () => { + const port = (server.address() as AddressInfo).port; + https.get({ port, ca, host: "localhost", path: "/", cert: client, key: clientKey }, (res) => { + let body = ""; + res.on("data", (d: Buffer) => (body += d)); + res.on("end", () => { + console.log("https with cert", body); + https.get({ port, ca, host: "localhost", path: "/", agent: false }, (res2) => { + let body2 = ""; + res2.on("data", (d: Buffer) => (body2 += d)); + res2.on("end", () => { + console.log("https without cert", body2); + server.close(() => done()); + }); + }); + }); + }); + }); +}); + +// An agent pools sockets per client certificate. +let connections = 0; +const server = https.createServer({ cert, key, requestCert: true, rejectUnauthorized: false, ca }, (req, res) => { + const s = req.socket as tls.TLSSocket; + res.end(`${s.authorized} ${(s.getPeerCertificate() as tls.PeerCertificate).subject?.CN}`); +}); +server.on("secureConnection", () => connections++); +function get(port: number, withCert: boolean): Promise { + return new Promise((resolve) => { + const done = (res: http.IncomingMessage) => { + let body = ""; + res.on("data", (d: Buffer) => (body += d)); + res.on("end", () => resolve(body)); + }; + if (withCert) https.get({ port, ca, host: "localhost", path: "/", cert: client, key: clientKey }, done); + else https.get({ port, ca, host: "localhost", path: "/" }, done); + }); +} +server.listen(0, async () => { + // the agent keeps one socket per client certificate + const port = (server.address() as AddressInfo).port; + for (const withCert of [true, true, false, false, true]) console.log(withCert, await get(port, withCert)); + console.log("connections", connections); + server.close(); + https.globalAgent.destroy(); +}); diff --git a/tests/corpus/7099-tls-client-certificates.mjs b/tests/corpus/7099-tls-client-certificates.mjs new file mode 100644 index 000000000..0c9eeed2f --- /dev/null +++ b/tests/corpus/7099-tls-client-certificates.mjs @@ -0,0 +1,34 @@ +// Client certificates from JavaScript match Node: a server that requests +// one reads socket.authorized, authorizationError and the peer +// certificate's chain, and a client sends its cert and key. +import * as tls from "node:tls"; +import { readFileSync } from "node:fs"; + +const D = "tests/fixtures/server/certs"; +const ca = readFileSync(`${D}/ca.pem`); +const cert = readFileSync(`${D}/localhost.pem`); +const key = readFileSync(`${D}/localhost-key.pem`); +const clientCert = readFileSync(`${D}/client.pem`); +const clientKey = readFileSync(`${D}/client-key.pem`); + +const lines = []; +const server = tls.createServer({ cert, key, ca, requestCert: true, rejectUnauthorized: false }, (socket) => { + const peer = socket.getPeerCertificate(true); + lines.push(`server ${socket.authorized} ${socket.authorizationError} ${peer.subject?.CN} ${peer.issuerCertificate?.subject.CN} ${peer.ext_key_usage}`); + socket.end("hello"); +}); +server.listen(0, () => { + const port = server.address().port; + const first = tls.connect({ port, ca, servername: "localhost", cert: clientCert, key: clientKey }, () => { + lines.push(`client ${first.authorized} ${first.getCertificate().subject.CN} ${first.getX509Certificate()?.subject}`); + }); + first.resume(); + first.on("end", () => { + const second = tls.connect({ port, ca, servername: "localhost" }, () => lines.push(`anonymous ${second.authorized}`)); + second.resume(); + second.on("end", () => { + for (const line of lines) console.log(line); + server.close(); + }); + }); +}); diff --git a/tests/corpus/7110-als-completion-callbacks.ts b/tests/corpus/7110-als-completion-callbacks.ts new file mode 100644 index 000000000..4b4e97e0d --- /dev/null +++ b/tests/corpus/7110-als-completion-callbacks.ts @@ -0,0 +1,135 @@ +// The completion callback of a callback-style API runs with the +// AsyncLocalStorage stores that were active when the call started the +// operation, as in Node. +import { AsyncLocalStorage } from "node:async_hooks"; +import { execFile } from "node:child_process"; +import * as crypto from "node:crypto"; +import * as dgram from "node:dgram"; +import * as dns from "node:dns"; +import { readFile, rename, writeFileSync, unlinkSync } from "node:fs"; +import * as http from "node:http"; +import * as net from "node:net"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import * as zlib from "node:zlib"; + +const store = new AsyncLocalStorage(); + +// Starts one operation inside run(name) and waits for its callback. +function step(name: string, start: (done: () => void) => void): Promise { + return new Promise((resolve) => { + store.run(name, () => + start(() => { + console.log(name, store.getStore()); + resolve(); + }), + ); + }); +} + +const dir = tmpdir(); +const file = join(dir, `scriptc-als-${process.pid}.txt`); +writeFileSync(file, "hello"); + +await step("fs.readFile", (done) => readFile(file, "utf8", () => done())); +await step("fs.rename", (done) => rename(file, `${file}.moved`, () => done())); +unlinkSync(`${file}.moved`); + +await step("crypto.randomBytes", (done) => crypto.randomBytes(4, () => done())); +await step("crypto.randomInt", (done) => crypto.randomInt(10, () => done())); +await step("crypto.randomFill", (done) => crypto.randomFill(new Uint8Array(4), () => done())); +await step("crypto.pbkdf2", (done) => crypto.pbkdf2("p", "s", 1, 8, "sha256", () => done())); +await step("crypto.scrypt", (done) => crypto.scrypt("p", "s", 8, () => done())); +await step("crypto.hkdf", (done) => crypto.hkdf("sha256", "k", "s", "i", 8, () => done())); +await step("crypto.generateKey", (done) => + crypto.generateKey("hmac", { length: 64 }, () => done()), +); + +await step("zlib.gzip", (done) => zlib.gzip(Buffer.from("hello"), () => done())); +await step("zlib.deflate options", (done) => + zlib.deflate(Buffer.from("hello"), { level: 1 }, () => done()), +); + +await step("dns.lookup", (done) => dns.lookup("localhost", () => done())); +await step("child_process.execFile", (done) => execFile("true", [], () => done())); + +// A server's listen callback, a client connection and an HTTP response +// each run in the stores of their own call. +const server = net.createServer((socket) => socket.end()); +await step("net listen", (done) => server.listen(0, "127.0.0.1", () => done())); +const port = (server.address() as net.AddressInfo).port; +await step("net.connect", (done) => { + const socket = net.connect(port, "127.0.0.1", () => { + socket.destroy(); + done(); + }); +}); +server.close(); + +const web = http.createServer((_req, res) => res.end("ok")); +await new Promise((resolve) => web.listen(0, "127.0.0.1", () => resolve())); +const webPort = (web.address() as net.AddressInfo).port; +await step("http.get", (done) => + http.get(`http://127.0.0.1:${webPort}/`, (res) => { + res.resume(); + done(); + }), +); +await step("http.request", (done) => + http + .request({ host: "127.0.0.1", port: webPort, path: "/", method: "GET" }, (res) => { + res.resume(); + done(); + }) + .end(), +); +web.close(); + +const optionsServer = net.createServer(); +await step("net listen options", (done) => + optionsServer.listen({ port: 0, host: "127.0.0.1" }, () => done()), +); +optionsServer.close(); +const pathServer = net.createServer(); +const socketPath = join(dir, `scriptc-als-${process.pid}.sock`); +await step("net listen path", (done) => pathServer.listen(socketPath, () => done())); +pathServer.close(); + +const udp = dgram.createSocket("udp4"); +await step("dgram bind", (done) => udp.bind(0, "127.0.0.1", () => done())); +const peer = dgram.createSocket("udp4"); +await new Promise((resolve) => peer.bind(0, "127.0.0.1", () => resolve())); +await step("dgram connect", (done) => udp.connect(peer.address().port, "127.0.0.1", () => done())); +peer.close(); +await step("dgram close", (done) => udp.close(() => done())); + +await step("process.stdout.write", (done) => process.stdout.write("", () => done())); + +// Nested: a callback starts another operation in a different store. +await step("outer", (done) => + crypto.randomBytes(1, () => { + console.log("outer callback", store.getStore()); + store.run("inner", () => + crypto.randomBytes(1, () => { + console.log("inner callback", store.getStore()); + done(); + }), + ); + }), +); + +// A callback whose call ran with no store sees no store, and a store the +// callback enters does not reach the next callback. +await new Promise((resolve) => + crypto.randomBytes(1, () => { + console.log("no store", store.getStore()); + store.enterWith("entered"); + resolve(); + }), +); +await new Promise((resolve) => + crypto.randomBytes(1, () => { + console.log("after enterWith", store.getStore()); + resolve(); + }), +); diff --git a/tests/corpus/7111-als-bind-snapshot-options.ts b/tests/corpus/7111-als-bind-snapshot-options.ts new file mode 100644 index 000000000..cad93cd5c --- /dev/null +++ b/tests/corpus/7111-als-bind-snapshot-options.ts @@ -0,0 +1,66 @@ +// AsyncLocalStorage.bind, AsyncLocalStorage.snapshot and the constructor +// options, as in Node. +import { AsyncLocalStorage } from "node:async_hooks"; + +const store = new AsyncLocalStorage(); + +// bind(fn) runs fn in the context active at the bind, with its arguments. +const add = store.run("bound", () => + AsyncLocalStorage.bind((a: number, b: number) => `${store.getStore()}:${a + b}`), +); +console.log(add(1, 2)); +console.log(store.run("other", () => add(3, 4))); + +// A function bound outside any store sees no store inside run(). +const plain = AsyncLocalStorage.bind(() => store.getStore()); +console.log(store.run("ignored", () => plain())); + +// The bound function keeps its result type. +const length = store.run("len", () => AsyncLocalStorage.bind((s: string) => s.length)); +console.log(length("four") * 2); + +// snapshot() runs a function in the context of the snapshot. +const snap = store.run("snapshot", () => AsyncLocalStorage.snapshot()); +console.log(snap(() => store.getStore())); +console.log(store.run("later", () => snap(() => store.getStore()))); +console.log(snap((x: number, y: number) => x * y, 6, 7)); + +// A store entered inside the bound call ends with it. +const enter = store.run("start", () => + AsyncLocalStorage.bind(() => { + store.enterWith("entered"); + return store.getStore(); + }), +); +console.log(enter(), store.getStore()); + +// The default value and the name. +const config = new AsyncLocalStorage<{ level: number }>({ + defaultValue: { level: 1 }, + name: "config", +}); +console.log(config.name, config.getStore()?.level); +config.run({ level: 2 }, () => console.log("run", config.getStore()?.level)); +config.exit(() => console.log("exit", config.getStore())); +config.disable(); +console.log("disabled", config.getStore()?.level); +const unnamed = new AsyncLocalStorage(); +console.log(JSON.stringify(unnamed.name), unnamed.getStore()); +const empty = new AsyncLocalStorage({}); +console.log(JSON.stringify(empty.name), empty.getStore()); +// A class instance's fields are options too. +class StoreOptions { + defaultValue = 7; + name = "from-class"; +} +const fromClass = new AsyncLocalStorage(new StoreOptions()); +console.log(fromClass.name, fromClass.getStore()); + +// Bound functions and snapshots carry the context across asynchronous work. +const later = store.run("timer", () => AsyncLocalStorage.bind(() => store.getStore())); +await new Promise((resolve) => + setTimeout(() => { + console.log("in timer", later(), store.getStore()); + resolve(); + }, 1), +); diff --git a/tests/corpus/7112-als-bind-snapshot-dynamic.mjs b/tests/corpus/7112-als-bind-snapshot-dynamic.mjs new file mode 100644 index 000000000..1e20aefd3 --- /dev/null +++ b/tests/corpus/7112-als-bind-snapshot-dynamic.mjs @@ -0,0 +1,94 @@ +// AsyncLocalStorage.bind, snapshot and the constructor options over +// untyped values, as in Node. +import { AsyncLocalStorage } from "node:async_hooks"; +import { readFile } from "node:fs"; + +const store = new AsyncLocalStorage(); + +function sum(a, b) { + return [this?.tag, a + b, store.getStore()]; +} +const bound = store.run("bound", () => AsyncLocalStorage.bind(sum)); +console.log(bound.name, bound.length, bound === sum, typeof bound); +console.log(JSON.stringify(bound.call({ tag: "receiver" }, 1, 2))); +console.log(JSON.stringify(store.run("other", () => bound(3, 4)))); + +const snap = store.run("snapshot", () => AsyncLocalStorage.snapshot()); +console.log(snap.name, snap.length, typeof snap); +console.log(snap((a, b) => `${store.getStore()} ${a + b}`, 2, 3)); + +for (const attempt of [ + () => AsyncLocalStorage.bind(5), + () => AsyncLocalStorage.bind("fn"), + () => snap(5), + () => snap(), +]) { + try { + console.log("ok", attempt()); + } catch (error) { + console.log(error.name, error.code, error.message); + } +} + +for (const options of [1, "x", null, true]) { + try { + new AsyncLocalStorage(options); + console.log("ok", options); + } catch (error) { + console.log(error.name, error.code, error.message); + } +} +const named = new AsyncLocalStorage({ name: "named", defaultValue: 0 }); +console.log(named.name, named.getStore()); +named.run(5, () => console.log(named.getStore())); +console.log(new AsyncLocalStorage(undefined).name === ""); + +// The options are read as JavaScript reads properties: inherited values +// and getters count, defaultValue is read once and name twice, and the +// name becomes a string. +const inherited = new AsyncLocalStorage(Object.create({ defaultValue: "inherited", name: "proto" })); +console.log(inherited.getStore(), inherited.name); +const reads = []; +const viaGetters = new AsyncLocalStorage({ + get defaultValue() { + reads.push("defaultValue"); + return "getter"; + }, + get name() { + reads.push("name"); + return "getter-name"; + }, +}); +console.log(viaGetters.getStore(), viaGetters.name, reads.join(",")); +const coerced = [5, null, { toString: () => "object" }].map((name) => new AsyncLocalStorage({ name }).name); +console.log(JSON.stringify(coerced)); +for (const options of [ + { + get defaultValue() { + throw new Error("from a getter"); + }, + }, + { name: Symbol("s") }, +]) { + try { + new AsyncLocalStorage(options); + console.log("constructed"); + } catch (error) { + console.log(error.name, error.message); + } +} + +// A snapshot calls its callback with an undefined receiver, also when the +// snapshot itself is called as a method. +const kind = function () { + return this === undefined ? "undefined" : typeof this; +}; +const holder = { snap, tag: "holder" }; +console.log(snap(kind), holder.snap(kind), snap.call({ tag: "receiver" }, kind)); + +// An untyped callback runs in the stores of its call. +store.run("readFile", () => + readFile(import.meta.filename, "utf8", (error, text) => + console.log("readFile", store.getStore(), error === null, text.length > 0), + ), +); diff --git a/tests/corpus/7119-als-net-event-context.mjs b/tests/corpus/7119-als-net-event-context.mjs new file mode 100644 index 000000000..d17d1c31c --- /dev/null +++ b/tests/corpus/7119-als-net-event-context.mjs @@ -0,0 +1,127 @@ +// AsyncLocalStorage contexts of node:net events. A client socket's events +// run in the context of the net.connect() call, whatever context the +// listener was registered in, also after a destroy() from elsewhere. The +// server's 'listening', 'connection' and 'error' events run in the +// context of the listen() call. An accepted socket's own events run with +// no store. write() and end() callbacks, and 'finish', run in the context +// of their call. server.close(callback) runs in the close() call's +// context when no connection is open, and with no store when it waits for +// the last connection. Lines are sorted at the end: events on different +// sockets may interleave. +import { AsyncLocalStorage } from "node:async_hooks"; +import net from "node:net"; + +const store = new AsyncLocalStorage(); +const lines = []; +const log = (what) => lines.push(`${what}: ${store.getStore() ?? "none"}`); + +/** @returns {Promise} */ +function echoScenario() { + return new Promise((resolve) => { + const server = store.run("create", () => + net.createServer((sock) => { + log("server connection callback"); + sock.on("data", () => { + log("accepted data"); + store.run("in-data", () => sock.write("pong")); + }); + sock.on("end", () => { + log("accepted end"); + sock.end(); + }); + sock.on("close", () => log("accepted close")); + }), + ); + server.on("connection", () => log("server connection event")); + server.on("close", () => { + log("server close event"); + resolve(); + }); + store.run("listen", () => + server.listen(0, () => { + log("listen callback"); + const client = store.run("connect", () => net.connect(server.address().port)); + store.run("register", () => { + client.on("connect", () => { + log("client connect"); + store.run("write", () => client.write("ping", () => log("write callback"))); + }); + client.on("data", () => { + log("client data"); + store.run("end-call", () => client.end(() => log("end callback"))); + }); + client.on("finish", () => log("client finish")); + client.on("end", () => log("client end")); + client.on("close", () => { + log("client close"); + store.run("close-call", () => server.close(() => log("server close callback"))); + }); + }); + }), + ); + }); +} + +/** @returns {Promise} */ +function destroyScenario() { + return new Promise((resolve) => { + const server = net.createServer((sock) => sock.on("data", () => {})); + server.listen(0, () => { + const client = store.run("connect-2", () => net.connect(server.address().port)); + client.on("error", () => {}); + client.on("connect", () => { + client.on("close", () => { + log("close after a destroy elsewhere"); + server.close(() => resolve()); + }); + setTimeout(() => store.run("destroy-call", () => client.destroy()), 10); + }); + }); + }); +} + +/** @returns {Promise} */ +function closeScenarios() { + return new Promise((resolve) => { + let pending = 3; + const done = () => { + if (--pending === 0) resolve(); + }; + const waiting = net.createServer((sock) => { + store.run("conn", () => waiting.close(() => { + log("close callback that waited"); + done(); + })); + setTimeout(() => store.run("end", () => sock.end()), 20); + }); + store.run("listen-3", () => + waiting.listen(0, () => { + const client = net.connect(waiting.address().port); + client.on("end", () => client.end()); + }), + ); + const idle = net.createServer(); + store.run("listen-4", () => + idle.listen(0, () => { + store.run("close-now", () => idle.close(() => { + log("close callback of an idle server"); + done(); + })); + }), + ); + const taken = net.createServer(); + taken.listen(0, () => { + const clash = net.createServer(); + clash.on("error", (e) => { + log(`listen error ${e.code}`); + taken.close(() => done()); + }); + store.run("clash-listen", () => clash.listen(taken.address().port)); + }); + }); +} + +await echoScenario(); +await destroyScenario(); +await closeScenarios(); +for (const line of lines.sort()) console.log(line); diff --git a/tests/corpus/7160-json-parse-deep-unions.ts b/tests/corpus/7160-json-parse-deep-unions.ts new file mode 100644 index 000000000..bcbf5ea9d --- /dev/null +++ b/tests/corpus/7160-json-parse-deep-unions.ts @@ -0,0 +1,164 @@ +// JSON.parse into recursive discriminated unions: deep and wide trees, +// unions with primitive arms beside the records, unions whose records share +// a literal, and unions with a record arm that has no literal. + +type Expr = + | { kind: "num"; value: number } + | { kind: "str"; text: string } + | { kind: "add"; left: Expr; right: Expr } + | { kind: "neg"; operand: Expr } + | { kind: "call"; callee: string; args: Expr[] } + | { kind: "cond"; test: Expr; then: Expr; otherwise: Expr | null } + | { kind: "label"; inner: Expr; name?: string }; + +type Stmt = + | { kind: "expr"; expr: Expr } + | { kind: "ret"; value: Expr | undefined } + | { kind: "block"; body: Stmt[] }; + +interface Fn { + name: string; + body: Stmt[]; +} + +function evaluate(e: Expr): number { + switch (e.kind) { + case "num": + return e.value; + case "str": + return e.text.length; + case "add": + return evaluate(e.left) + evaluate(e.right); + case "neg": + return -evaluate(e.operand); + case "call": + return e.args.reduce((sum, arg) => sum + evaluate(arg), e.callee.length); + case "cond": + return evaluate(e.test) !== 0 + ? evaluate(e.then) + : e.otherwise === null + ? 0 + : evaluate(e.otherwise); + case "label": + return evaluate(e.inner) + (e.name?.length ?? 0); + } +} + +function depth(e: Expr): number { + switch (e.kind) { + case "num": + case "str": + return 1; + case "add": + return 1 + Math.max(depth(e.left), depth(e.right)); + case "neg": + return 1 + depth(e.operand); + case "call": + return 1 + e.args.reduce((max, arg) => Math.max(max, depth(arg)), 0); + case "cond": + return 1 + Math.max(depth(e.test), depth(e.then), e.otherwise ? depth(e.otherwise) : 0); + case "label": + return 1 + depth(e.inner); + } +} + +function chain(n: number): string { + let text = '{"kind":"num","value":1}'; + for (let i = 0; i < n; i++) { + text = + i % 3 === 0 + ? `{"kind":"neg","operand":${text}}` + : i % 3 === 1 + ? `{"kind":"add","left":${text},"right":{"kind":"str","text":"ab"}}` + : `{"kind":"label","inner":${text}${i % 2 === 0 ? ',"name":"x"' : ""}}`; + } + return text; +} + +function wide(n: number, level: number): string { + if (level === 0) return `{"kind":"num","value":${n}}`; + const args: string[] = []; + for (let i = 0; i < 3; i++) args.push(wide(n + i, level - 1)); + return `{"kind":"call","callee":"f${level}","args":[${args.join(",")}]}`; +} + +for (const n of [0, 1, 5, 60, 400]) { + const e = JSON.parse(chain(n)) as Expr; + console.log("chain", n, e.kind, depth(e), evaluate(e)); +} + +const tree = JSON.parse(wide(1, 6)) as Expr; +console.log("wide", depth(tree), evaluate(tree)); + +const cond = JSON.parse( + '{"kind":"cond","test":{"kind":"num","value":0},"then":{"kind":"num","value":5},"otherwise":null}', +) as Expr; +console.log("cond", evaluate(cond)); + +const fn = JSON.parse( + JSON.stringify({ + name: "main", + body: [ + { kind: "expr", expr: JSON.parse(chain(30)) }, + { kind: "block", body: [{ kind: "ret", value: JSON.parse(wide(2, 3)) }, { kind: "ret" }] }, + ], + }), +) as Fn; +let statements = 0; +const visit = (stmts: Stmt[]): void => { + for (const s of stmts) { + statements++; + if (s.kind === "block") visit(s.body); + else if (s.kind === "expr") console.log("stmt expr", evaluate(s.expr)); + else console.log("stmt ret", s.value === undefined ? "none" : evaluate(s.value)); + } +}; +visit(fn.body); +console.log("statements", statements); + +// Primitive arms beside the records. +type Value = number | string | null | { kind: "list"; items: Value[] } | { kind: "pair"; first: Value; second: Value }; +function show(v: Value): string { + if (v === null || typeof v === "number" || typeof v === "string") return JSON.stringify(v); + return v.kind === "list" ? `[${v.items.map(show).join(" ")}]` : `(${show(v.first)} . ${show(v.second)})`; +} +const values = JSON.parse( + '[1,"a",null,{"kind":"list","items":[2,{"kind":"pair","first":"b","second":{"kind":"list","items":[null,3]}}]}]', +) as Value[]; +console.log("values", values.map(show).join(" | ")); + +// Two records that share a literal: the arm follows the other fields. +type Shared = { kind: "a"; count: number } | { kind: "a"; name: string } | { kind: "b"; next: Shared[] }; +function describe(s: Shared): string { + if (s.kind === "b") return `b(${s.next.map(describe).join(",")})`; + return "count" in s ? `count=${s.count}` : `name=${s.name}`; +} +const shared = JSON.parse( + '{"kind":"b","next":[{"kind":"a","count":1},{"kind":"a","name":"n"},{"kind":"b","next":[{"kind":"a","name":"m"}]}]}', +) as Shared; +console.log("shared", describe(shared)); + +// A record arm without a literal beside records that have one. +type Loose = { kind: "leaf"; value: number } | { kind: "node"; children: Loose[] } | { weight: number; child: Loose }; +function total(l: Loose): number { + if ("weight" in l) return l.weight * total(l.child); + return l.kind === "leaf" ? l.value : l.children.reduce((sum, c) => sum + total(c), 0); +} +const loose = JSON.parse( + '{"kind":"node","children":[{"kind":"leaf","value":2},{"weight":3,"child":{"kind":"node","children":[{"kind":"leaf","value":4},{"weight":2,"child":{"kind":"leaf","value":5}}]}}]}', +) as Loose; +console.log("loose", total(loose)); + +// Optional and nullable members holding records and arrays of records. +type Box = { value: number; child?: Box; rest: Box[] | null }; +function weigh(b: Box): number { + let total = b.value; + if (b.child !== undefined) total += 2 * weigh(b.child); + for (const r of b.rest ?? []) total += weigh(r); + return total; +} +let boxText = '{"value":1,"rest":null}'; +for (let i = 2; i < 120; i++) + boxText = i % 2 === 0 ? `{"value":${i},"child":${boxText},"rest":null}` : `{"value":${i},"rest":[${boxText},{"value":1,"rest":[]}]}`; +const boxes = JSON.parse(boxText) as Box; +console.log("boxes", weigh(boxes) % 1000003); diff --git a/tests/corpus/7161-dynamic-values-deep-unions.ts b/tests/corpus/7161-dynamic-values-deep-unions.ts new file mode 100644 index 000000000..3e85b2689 --- /dev/null +++ b/tests/corpus/7161-dynamic-values-deep-unions.ts @@ -0,0 +1,73 @@ +// Dynamic values cast into recursive discriminated unions: trees built as +// untyped objects, trees that mix typed records into untyped ones, and casts +// of the same values twice. + +type Node = + | { kind: "leaf"; value: number } + | { kind: "pair"; left: Node; right: Node } + | { kind: "wrap"; inner: Node; tag: string } + | { kind: "many"; items: Node[] }; + +function sum(n: Node): number { + switch (n.kind) { + case "leaf": + return n.value; + case "pair": + return sum(n.left) + sum(n.right); + case "wrap": + return sum(n.inner) + n.tag.length; + case "many": + return n.items.reduce((total, item) => total + sum(item), 0); + } +} + +function height(n: Node): number { + switch (n.kind) { + case "leaf": + return 1; + case "pair": + return 1 + Math.max(height(n.left), height(n.right)); + case "wrap": + return 1 + height(n.inner); + case "many": + return 1 + n.items.reduce((max, item) => Math.max(max, height(item)), 0); + } +} + +function untyped(levels: number): any { + let node: any = { kind: "leaf", value: 1 }; + for (let i = 0; i < levels; i++) { + node = + i % 3 === 0 + ? { kind: "wrap", inner: node, tag: "t" } + : i % 3 === 1 + ? { kind: "pair", left: node, right: { kind: "leaf", value: i } } + : { kind: "many", items: [node, { kind: "leaf", value: 2 }] }; + } + return node; +} + +for (const levels of [0, 2, 10, 90, 300]) { + const value: unknown = untyped(levels); + const node = value as Node; + console.log("untyped", levels, node.kind, height(node), sum(node)); +} + +// Typed records inside an untyped tree. +const typedLeaf: Node = { kind: "leaf", value: 7 }; +const typedPair: Node = { kind: "pair", left: typedLeaf, right: { kind: "leaf", value: 3 } }; +const mixed: any = { kind: "many", items: [typedPair, untyped(20), { kind: "wrap", inner: typedLeaf, tag: "xy" }] }; +const fromMixed = mixed as Node; +console.log("mixed", height(fromMixed), sum(fromMixed)); + +// The same untyped value cast twice gives equal trees. +const again: any = untyped(40); +const first = again as Node; +const second = again as Node; +console.log("twice", sum(first) === sum(second), height(first), height(second)); + +// A tree that changes between casts. +const changing: any = { kind: "pair", left: { kind: "leaf", value: 1 }, right: untyped(5) }; +console.log("before", sum(changing as Node)); +changing.left = { kind: "wrap", inner: { kind: "leaf", value: 10 }, tag: "abc" }; +console.log("after", sum(changing as Node)); diff --git a/tests/corpus/7186-namespace-object-values.js b/tests/corpus/7186-namespace-object-values.js new file mode 100644 index 000000000..33c30e12f --- /dev/null +++ b/tests/corpus/7186-namespace-object-values.js @@ -0,0 +1,27 @@ +// Math, JSON and Reflect read as values are objects with Node's own +// properties: typeof, String, `in`, aliasing and member calls through +// the value behave as in Node, and every spelling is the same object. +function describe(value) { + return [typeof value, String(value), Object.prototype.toString.call(value)].join(" "); +} +console.log(describe(Math), "|", describe(JSON), "|", describe(Reflect)); + +const m = Math; +console.log(m.abs(-2), m.max(1, 5, 3), m.PI.toFixed(5), "trunc" in m, m === Math); +console.log(m === globalThis.Math, JSON === globalThis.JSON); +const { Math: fromGlobal } = globalThis; +console.log(fromGlobal === Math, fromGlobal.sqrt(81)); + +const known = new Set([Math, JSON, Reflect]); +console.log(known.has(Math), known.has(globalThis.JSON), known.size); + +const parse = JSON.parse; +console.log(parse("[1,2]").length, JSON.stringify({ a: [1] })); + +console.log(typeof Math === "object" && typeof Reflect === "object"); +console.log(Math.max.length, Math.min.length, Math.hypot.length, Math.abs.length); + +const r = Reflect; +const ownKeys = r.ownKeys; +console.log(ownKeys === Reflect.ownKeys, r.ownKeys === Reflect.ownKeys, typeof ownKeys); +console.log(r.ownKeys({ a: 1, b: 2 }).length, ownKeys({ c: 3 })[0], ownKeys.name, ownKeys.length); diff --git a/tests/corpus/7187-namespace-object-reflection.js b/tests/corpus/7187-namespace-object-reflection.js new file mode 100644 index 000000000..d62699dea --- /dev/null +++ b/tests/corpus/7187-namespace-object-reflection.js @@ -0,0 +1,23 @@ +// Reflection over the namespace objects answers Node's property +// attributes: members are own, non-enumerable properties, the Math +// constants are read-only, and Symbol.toStringTag names the namespace. +function show(object, key) { + const d = Object.getOwnPropertyDescriptor(object, key); + if (d === undefined) return `${String(key)}: none`; + const value = typeof d.value === "function" ? `fn/${d.value.length}/${d.value.name}` : String(d.value); + return `${String(key)}: ${value} ${d.writable} ${d.enumerable} ${d.configurable}`; +} +function inspect(object) { + return [ + Object.hasOwn(object, "parse"), + Object.hasOwn(object, "abs"), + Object.keys(object).length, + Object.getOwnPropertyNames(object).length, + ].join(" "); +} +console.log(show(Math, "abs"), "|", show(Math, "PI"), "|", show(Math, "max")); +console.log(show(Math, Symbol.toStringTag), "|", show(JSON, "stringify"), "|", show(Reflect, "apply")); +console.log(inspect(Math), "|", inspect(JSON), "|", inspect(Reflect)); +console.log(Object.getOwnPropertyNames(JSON).join(",")); +console.log(Object.getOwnPropertyNames(Math).slice(0, 5).join(","), Object.getOwnPropertyNames(Math).at(-1)); +console.log(Object.prototype.hasOwnProperty.call(Math, "random"), Object.keys(Reflect).length); diff --git a/tests/corpus/7188-namespace-object-extensions.js b/tests/corpus/7188-namespace-object-extensions.js new file mode 100644 index 000000000..0a33f7155 --- /dev/null +++ b/tests/corpus/7188-namespace-object-extensions.js @@ -0,0 +1,16 @@ +// Namespace objects take new properties and attribute-only redefinitions +// as in Node, and their members keep the values static calls use. +const m = Math; +m.tau = 2 * Math.PI; +console.log(m.tau.toFixed(4), Math.tau === m.tau, "tau" in Math, Object.keys(Math).join(",")); +Object.defineProperty(Math, "hypot", { enumerable: true }); +Object.defineProperty(Math, "trunc", { value: Math.trunc, writable: false }); +const d = Object.getOwnPropertyDescriptor(Math, "trunc"); +console.log(Object.keys(Math).join(","), d.writable, d.enumerable, d.configurable, Math.trunc(-1.5)); +console.log(Math.hypot(3, 4), m.hypot === Math.hypot, Object.getOwnPropertyDescriptor(m, "hypot").enumerable); +const j = JSON; +j.label = "json"; +console.log(j.label, Object.keys(JSON).join(","), Reflect.ownKeys(j).length); +const r = Reflect; +r.extra = [1, 2]; +console.log(Reflect.extra.length, Object.getOwnPropertyNames(Reflect).at(-1), "extra" in r); diff --git a/tests/corpus/7189-non-null-assertion-nullish-destination.ts b/tests/corpus/7189-non-null-assertion-nullish-destination.ts new file mode 100644 index 000000000..4cd7e291f --- /dev/null +++ b/tests/corpus/7189-non-null-assertion-nullish-destination.ts @@ -0,0 +1,57 @@ +// `!` has no runtime effect: a value asserted non-null that still holds +// null or undefined reaches a return value, an arrow function's result or +// an argument whose declared type holds it (or is any or unknown), unchanged. +interface Shape { + size: number; +} + +const shapes = new Map(); +function shapeOf(key: string): Shape | null { + if (shapes.has(key)) return shapes.get(key)!; + const result = key.length > 3 ? { size: key.length } : null; + shapes.set(key, result); + return result; +} +console.log(shapeOf("ab"), shapeOf("abcd")); +console.log(shapeOf("ab"), shapeOf("abcd"), shapes.size); + +const counts = new Map([["a", 1]]); +function countOf(key: string): number | undefined { + return counts.get(key)!; +} +console.log(countOf("a"), countOf("b")); + +function describe(shape: Shape | null): string { + return shape === null ? "none" : `size ${shape.size}`; +} +console.log(describe(shapes.get("ab")!), describe(shapes.get("abcd")!)); + +class Slot { + readonly label: string; + constructor(shape: Shape | null) { + this.label = shape === null ? "empty" : `slot ${shape.size}`; + } +} +console.log(new Slot(shapes.get("ab")!).label, new Slot(shapes.get("abcd")!).label); + +const lookup = (key: string): Shape | null => shapes.get(key)!; +console.log(lookup("ab"), lookup("abcd")?.size); + +const names = new Map([["a", null], ["b", "bee"]]); +const amounts = new Map([["a", undefined], ["b", 2]]); +function kindOf(value: unknown): string { + return value === null ? "null" : typeof value; +} +console.log(String(names.get("a")!), String(names.get("b")!)); +console.log(Number(amounts.get("a")!), Number(amounts.get("b")!)); +console.log(kindOf(names.get("a")!), kindOf(amounts.get("a")!), kindOf(names.get("b")!)); + +// TextDecoder.decode reads the asserted Uint8Array; an undefined input +// decodes no bytes (text-decoder-undefined-input.ts). +const chunks = new Map([["a", new TextEncoder().encode("chunk")]]); +interface Read { + done: boolean; + value?: Uint8Array; +} +const read: Read = { done: false, value: chunks.get("a") }; +console.log(new TextDecoder().decode(chunks.get("a")!), new TextDecoder().decode(read.value!).slice(0, 3)); diff --git a/tests/corpus/7260-teardown-segments.ts b/tests/corpus/7260-teardown-segments.ts new file mode 100644 index 000000000..cbd2b144d --- /dev/null +++ b/tests/corpus/7260-teardown-segments.ts @@ -0,0 +1,179 @@ +// Teardowns that hand their blocks back in order. +// +// A typed teardown of a type with children of its own type keeps the blocks +// it frees in one segment and frees the segment when the chain's first +// teardown returns, so the next structure built of that type reuses them in +// order. This program rebuilds structures of such types for many +// generations and reads every value of each generation before dropping it: +// a block handed out twice, or one freed while still in use, shows up as a +// wrong checksum. The shapes cover the ways a segment is put together: +// record literals (allocated before their fields) and constructors whose +// arguments build the children first, lists built by prepending and by +// appending, trees far deeper than one direct teardown chain and lists +// longer than the runtime's whole depth budget (whose tails wait in its +// queue), subtrees still owned elsewhere when their parent goes, records of +// one type holding records of another, and nodes of a cycle-capable class +// that are still candidates of the cycle collector when the last reference +// goes, and rings of them that only the collector frees. + +interface Node { + left: Node | null; + right: Node | null; + value: number; +} + +function literalTree(depth: number, value: number): Node { + return depth === 0 + ? { left: null, right: null, value } + : { left: literalTree(depth - 1, value * 2), right: literalTree(depth - 1, value * 2 + 1), value }; +} + +function sum(node: Node | null): number { + return node === null ? 0 : node.value + sum(node.left) + sum(node.right); +} + +class Branch { + readonly left: Branch | null; + readonly right: Branch | null; + readonly label: string; + constructor(left: Branch | null, right: Branch | null, label: string) { + this.left = left; + this.right = right; + this.label = label; + } +} + +function classTree(depth: number, label: string): Branch { + return depth === 0 + ? new Branch(null, null, label) + : new Branch(classTree(depth - 1, label + "l"), classTree(depth - 1, label + "r"), label); +} + +function labels(branch: Branch | null): number { + return branch === null ? 0 : branch.label.length + labels(branch.left) + labels(branch.right); +} + +interface Cell { + next: Cell | null; + n: number; +} + +function prepended(count: number, base: number): Cell | null { + let head: Cell | null = null; + for (let i = 0; i < count; i++) head = { next: head, n: base + i }; + return head; +} + +function appended(count: number, base: number): Cell | null { + if (count === 0) return null; + const head: Cell = { next: null, n: base }; + let tail = head; + for (let i = 1; i < count; i++) { + const cell: Cell = { next: null, n: base + i }; + tail.next = cell; + tail = cell; + } + return head; +} + +function cells(head: Cell | null): [number, number] { + let count = 0; + let total = 0; + for (let cell = head; cell !== null; cell = cell.next) { + count++; + total += cell.n; + } + return [count, total]; +} + +// Generations of literal trees: shallow, one chain deep, and several chains +// deep. Each generation is checked against the sum the shape must have. +for (const depth of [0, 1, 5, 16, 17, 18]) { + let ok = true; + for (let generation = 0; generation < 6; generation++) { + const tree = literalTree(depth, 1); + const expected = ((2 ** (depth + 1)) * (2 ** (depth + 1) - 1)) / 2; + if (sum(tree) !== expected) ok = false; + } + console.log(`literal trees of depth ${depth}: ${ok}`); +} + +// Generations of class trees built children first. +for (const depth of [3, 12, 17]) { + let total = 0; + for (let generation = 0; generation < 4; generation++) total += labels(classTree(depth, "t")); + console.log(`class trees of depth ${depth}: ${total}`); +} + +// Lists both ways, alternating, so each reuses the other's blocks; the long +// ones outlast the runtime's depth budget. +for (const count of [1, 15, 16, 17, 1000, 3000, 40000]) { + const seen: string[] = []; + for (let generation = 0; generation < 4; generation++) { + const list = generation % 2 === 0 ? prepended(count, generation) : appended(count, generation); + const [length, total] = cells(list); + seen.push(`${length}/${total}`); + } + console.log(`lists of ${count}: ${seen.join(" ")}`); +} + +// Subtrees owned elsewhere survive their parent's teardown, and the blocks +// freed around them are not handed out while they live. +const kept: Node[] = []; +for (let generation = 0; generation < 5; generation++) { + const tree = literalTree(10, generation + 1); + if (tree.left !== null && tree.left.right !== null) kept.push(tree.left.right); + if (tree.right !== null) kept.push(tree.right); + // Building the next tree reuses the dropped blocks. + literalTree(10, 7); +} +console.log(`kept subtrees: ${kept.length} ${kept.map(sum).join(",")}`); + +// A record of one type holding trees of another: the outer teardown frees +// its own blocks as before and tears each inner tree down as a segment. +interface Forest { + first: Node; + second: Node | null; + rest: Forest | null; +} + +function forest(count: number): Forest | null { + let out: Forest | null = null; + for (let i = 0; i < count; i++) out = { first: literalTree(4, i), second: i % 3 === 0 ? null : literalTree(2, i), rest: out }; + return out; +} + +let forestTotal = 0; +for (let generation = 0; generation < 3; generation++) { + for (let f = forest(500); f !== null; f = f.rest) forestTotal += sum(f.first) + sum(f.second); +} +console.log(`forests: ${forestTotal}`); + +// A cycle-capable class: releases of shared links make nodes candidates of +// the cycle collector, which are then torn down while still buffered, and +// the peer links close some rings, which the collector frees. +class Ring { + next: Ring | null = null; + peer: Ring | null = null; + id: number; + constructor(id: number) { + this.id = id; + } +} + +let ringTotal = 0; +for (let generation = 0; generation < 3; generation++) { + let head: Ring | null = null; + const peers: Ring[] = []; + for (let i = 0; i < 2000; i++) { + const ring: Ring = new Ring(i); + ring.next = head; + if (i % 7 === 0) peers.push(ring); + head = ring; + } + for (let r = head; r !== null; r = r.next) if (r.id % 5 === 0) r.peer = peers[r.id % peers.length] ?? null; + for (const peer of peers) ringTotal += peer.id; + peers.length = 0; + for (let r = head; r !== null; r = r.next) ringTotal += r.peer === null ? 0 : r.peer.id; +} +console.log(`rings: ${ringTotal}`); diff --git a/tests/corpus/7264-array-dense-paired-loops.ts b/tests/corpus/7264-array-dense-paired-loops.ts new file mode 100644 index 000000000..59cedf60b --- /dev/null +++ b/tests/corpus/7264-array-dense-paired-loops.ts @@ -0,0 +1,120 @@ +// Two-counter loops that index number arrays at both counters +// (`for (let i = lo, j = hi; i < j; i++, j--)`), and element copies through +// `a[i]!` reads, both inside and outside the ranges a single entry test can +// cover: filled prefixes, holes, present undefined, reads past the end, +// fractional and negative starts, int32 arrays and long checked arrays. + +function reversePrefix(a: number[], end: number): void { + for (let i = 0, j = end; i < j; i++, j--) { + const t = a[i]!; + a[i] = a[j]!; + a[j] = t; + } +} + +function show(label: string, a: number[]): void { + console.log(label, a.length, JSON.stringify(a), a); +} + +// The whole prefix is present: the entry test passes. +const plain: number[] = []; +for (let k = 0; k < 8; k++) plain[k] = k * 3; +reversePrefix(plain, 5); +show("plain", plain); +reversePrefix(plain, 7); +show("whole", plain); +reversePrefix(plain, 0); +show("empty range", plain); + +// The high start is past the end: the reads there miss, the stores extend. +const short = [1, 2, 3]; +reversePrefix(short, 5); +show("past the end", short); + +// A hole inside the range. +const holey: number[] = [10, 20]; +holey[3] = 40; +holey[4] = 50; +reversePrefix(holey, 4); +show("hole", holey); + +// A present undefined inside the range, copied by a mirror loop. +const loose: number[] = [7, 8, 9, 10]; +(loose as (number | undefined)[])[2] = undefined; +for (let i = 0, j = 3; i < j; i++, j--) loose[i] = loose[j]!; +show("undefined", loose); + +// Fractional and negative starts index non-element properties. +const fractional = [1, 2, 3, 4]; +reversePrefix(fractional, 2.5); +show("fractional", fractional); +const negative = [5, 6, 7]; +for (let i = -1, j = 2; i < j; i++, j--) negative[i] = negative[j]!; +show("negative", negative); + +// A local int32 array (every stored element is a small integer). +function int32Flips(start: number): void { + const counts = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; + let flips = 0; + for (let first = counts[start]!; first !== 0; first = counts[0]!) { + for (let i = 0, j = first; i < j; i++, j--) { + const t = counts[i]!; + counts[i] = counts[j]!; + counts[j] = t; + } + flips++; + if (flips > 20) break; + } + console.log("int32", flips, counts.join(",")); +} +int32Flips(3); +int32Flips(9); + +// A local long array with checked int32 stores, widened by a fraction. +function longChecked(n: number, fraction: number): void { + const long = new Array(n).fill(0); + for (let k = 0; k < long.length; k++) long[k] = k; + for (let i = 0, j = n - 1; i < j; i++, j--) { + const t = long[i]!; + long[i] = long[j]!; + long[j] = t; + } + console.log("long", long[0], long[1], long[n - 2], long[n - 1]); + long[10] = fraction; + for (let i = 0, j = 20; i < j; i++, j--) { + const t = long[i]!; + long[i] = long[j]!; + long[j] = t; + } + console.log("widened", long.slice(0, 12).join(","), long[20]); +} +longChecked(5000, 0.5); + +// Element copies through `a[i]!`: the source is shorter than the range. +function copy(to: number[], from: number[], n: number): void { + for (let i = 0; i < n; i++) to[i] = from[i]!; +} +const into = [0, 0, 0, 0, 0, 0]; +copy(into, [1, 2, 3, 4, 5, 6], 6); +show("copy", into); +copy(into, [9, 8], 4); +show("short source", into); +const gappy: number[] = [1]; +gappy[2] = 3; +gappy[3] = 4; +copy(into, gappy, 4); +show("gappy source", into); + +// Two names for one array. +function aliased(n: number): void { + const same: number[] = []; + for (let k = 0; k < n; k++) same[k] = k + 1; + const alias = same; + for (let i = 0, j = n - 1; i < j; i++, j--) { + const t = alias[i]!; + same[i] = alias[j]!; + same[j] = t; + } + show("alias", same); +} +aliased(5); diff --git a/tests/corpus/7304-string-optional-char-reads.ts b/tests/corpus/7304-string-optional-char-reads.ts new file mode 100644 index 000000000..6e47abb75 --- /dev/null +++ b/tests/corpus/7304-string-optional-char-reads.ts @@ -0,0 +1,135 @@ +// s[i] with an index not proven in bounds, tested against one-unit +// literals: equality, inequality and ordered comparisons on either side, +// peeks at i - 1 and i + 1, tests for undefined, switches, character +// locals with and without other uses, fractional, negative, -0, NaN and +// infinite indices, reads past the end, a two-unit literal, Object.is, a +// class-based lexer with its position in a field, and ASCII, Latin-1, BMP, +// surrogate pairs, lone surrogates and empty strings. + +class Lexer { + pos = 0; + src: string; + constructor(src: string) { + this.src = src; + } + skipSpaces(): void { + const src = this.src; + let pos = this.pos; + while (pos < src.length && (src[pos] === " " || src[pos] === "\t")) pos++; + this.pos = pos; + } + word(): string { + const src = this.src; + const start = this.pos; + let pos = start; + while (pos < src.length) { + const c = src[pos]; + if ((c < "a" || c > "z") && (c < "0" || c > "9")) break; + pos++; + } + this.pos = pos; + return src.slice(start, pos); + } + operator(): string { + const src = this.src; + const c = src[this.pos]; + switch (c) { + case "=": + this.pos += src[this.pos + 1] === "=" ? 2 : 1; + return src[this.pos - 1] === "=" && src[this.pos - 2] === "=" ? "==" : "="; + case "+": + case "-": + this.pos++; + return c; + case undefined: + return ""; + default: + this.pos++; + return "?"; + } + } + tokens(): string[] { + const out: string[] = []; + while (this.pos < this.src.length) { + this.skipSpaces(); + const w = this.word(); + if (w !== "") out.push(w); + else if (this.pos < this.src.length) out.push(this.operator()); + } + return out; + } +} + +function tests(src: string, pos: number): string { + const bits: number[] = []; + bits.push(src[pos] === " " ? 1 : 0, src[pos] !== "x" ? 1 : 0); + bits.push(src[pos] < "a" ? 1 : 0, src[pos] <= "a" ? 1 : 0); + bits.push(src[pos] > "a" ? 1 : 0, src[pos] >= "a" ? 1 : 0); + bits.push("m" < src[pos] ? 1 : 0, "m" >= src[pos] ? 1 : 0); + bits.push(src[pos + 1] === "b" ? 1 : 0, src[pos - 1] > "\0" ? 1 : 0); + bits.push(src[pos] === undefined ? 1 : 0, src[pos] !== undefined ? 1 : 0); + bits.push(src[pos * 2] === undefined ? 1 : 0, src[pos] === "ab" ? 1 : 0); + bits.push(Object.is(src[pos], "a") ? 1 : 0); + return bits.join(""); +} + +function local(src: string, pos: number): string { + const c = src[pos]; + let n = 0; + if (c === undefined) n += 1; + if (c !== undefined && c >= "a") n += 2; + if ("z" > c) n += 4; + switch (c) { + case "é": + n += 8; + break; + case "b": + n += 16; + break; + default: + n += 32; + } + return String(n); +} + +function kept(src: string, pos: number): string { + const c = src[pos]; + if (c === " ") return "space"; + return c ?? "end"; +} + +const samples = ["abc", "", " ", "a b\tc", "café", "日本 語", "😀x", "\uD800z", "ab=b"]; +const indices = [0, 1, 2, 3, -1, -0, 0.5, 1.5, NaN, Infinity, 10]; +for (const s of samples) { + const row: string[] = []; + for (const i of indices) row.push(`${tests(s, i)}:${local(s, i)}:${kept(s, i)}`); + console.log(JSON.stringify(s), row.join(" ")); +} + +const programs = [ + "let x = a + b - c", + " alpha==beta gamma = delta1 ", + "x=y==z+", + "", + "abc é def", + "\tq\t=\t9", +]; +for (const p of programs) console.log(JSON.stringify(p), JSON.stringify(new Lexer(p).tokens())); + +let text = ""; +for (let k = 0; k < 30; k++) text += (k % 4 === 0 ? "==" : "w" + k) + " "; +console.log(new Lexer(text).tokens().length, new Lexer(text).tokens().slice(0, 6).join(",")); + +// A character local tested inside another read's index or receiver. +function nested(src: string, other: string): number { + let n = 0; + for (let pos = 0; pos <= src.length; pos += 1.5) { + const c = src[pos]; + if (src[c === "a" ? pos + 1 : pos] === "b") n++; + if ((c === "x" ? other : src)[pos] === "y") n += 2; + const d = src[c === "c" ? pos - 1 : pos]; + if (d === "c") n += 10; + } + return n; +} +console.log(nested("abcab", "xyz"), nested("cab", "yy"), nested("xyxy", "yyyy"), nested("", "")); diff --git a/tests/corpus/7305-string-unit-loop-versions.ts b/tests/corpus/7305-string-unit-loop-versions.ts new file mode 100644 index 000000000..c7600167d --- /dev/null +++ b/tests/corpus/7305-string-unit-loop-versions.ts @@ -0,0 +1,219 @@ +// charCodeAt scans of strings that are not ASCII, repeated so the runtime +// holds their UTF-16 units: counts, hashes, neighbor reads, descending +// loops, reads past the end, line starts pushed to a number array, helper +// functions that read no string, a helper that reads another string, a +// second non-ASCII string read in the same loop, two texts scanned in +// turn, a short text, a text appended to between scans, surrogate pairs +// and lone surrogates, ASCII texts beside them, and while loops over a +// counter below the length: word and identifier scans, peeks past the +// counter, and a lexer class whose position is a field. Loops also read +// the length in their bodies, and loops over a fresh text that no scan +// has published run from its end and over its last units. + +function isWordUnit(c: number): boolean { + return (c >= 97 && c <= 122) || (c >= 65 && c <= 90) || c > 127; +} + +function isWordOrDigit(c: number): boolean { + return isWordUnit(c) || (c >= 48 && c <= 57); +} + +function spaces(s: string): number { + let n = 0; + for (let i = 0; i < s.length; i++) if (s.charCodeAt(i) === 32) n++; + return n; +} + +function hash(s: string): number { + let h = 7; + for (let i = 0; i < s.length; i++) h = (Math.imul(h, 31) + s.charCodeAt(i)) | 0; + return h; +} + +function rises(s: string): number { + let n = 0; + for (let i = 1; i < s.length - 1; i++) + if (s.charCodeAt(i) > s.charCodeAt(i - 1) && s.charCodeAt(i) >= s.charCodeAt(i + 1)) n++; + return n; +} + +function backwards(s: string): number { + let h = 0; + for (let i = s.length - 1; i >= 0; i -= 3) h = (h * 7 + s.charCodeAt(i)) % 1000003; + return h; +} + +function pastEnd(s: string): string { + let sum = 0; + for (let i = 0; i <= s.length; i++) sum += s.charCodeAt(i); + return String(sum); +} + +function lineStarts(s: string): number[] { + const starts = [0]; + for (let i = 0; i < s.length; i++) if (s.charCodeAt(i) === 10) starts.push(i + 1); + return starts; +} + +function wordUnits(s: string): number { + let n = 0; + for (let i = 0; i < s.length; i++) if (isWordOrDigit(s.charCodeAt(i))) n++; + return n; +} + +function codeOf(t: string, i: number): number { + return t.charCodeAt(i % t.length); +} + +function mixed(s: string, t: string): number { + let n = 0; + for (let i = 0; i < s.length; i++) n = (n + s.charCodeAt(i) * codeOf(t, i)) % 1000003; + return n; +} + +function sameAt(a: string, b: string): number { + let n = 0; + const end = Math.min(a.length, b.length); + for (let i = 0; i < end; i++) if (a.charCodeAt(i) === b.charCodeAt(i)) n++; + return n; +} + +function surrogates(s: string): string { + let high = 0, + low = 0; + for (let i = 0; i < s.length; i++) { + const c = s.charCodeAt(i); + if (c >= 0xd800 && c < 0xdc00) high++; + else if (c >= 0xdc00 && c < 0xe000) low++; + } + return `${high}/${low}`; +} + +function wordCount(s: string): number { + let pos = 0, + n = 0; + const end = s.length; + while (pos < end) { + while (pos < end && s.charCodeAt(pos) === 32) pos++; + if (pos >= end) break; + n++; + while (pos < end && s.charCodeAt(pos) !== 32) pos++; + } + return n; +} + +function wordUnitRuns(s: string): number { + let pos = 0, + total = 0; + while (pos < s.length) { + if (isWordUnit(s.charCodeAt(pos))) { + const start = pos; + while (pos < s.length && isWordOrDigit(s.charCodeAt(pos))) pos++; + total += pos - start; + } else pos = (pos + 1) | 0; + } + return total; +} + +function doubled(s: string): number { + let pos = 0, + n = 0; + while (pos < s.length) { + if (s.charCodeAt(pos) === s.charCodeAt(pos + 1)) n++; + pos += 1; + } + return n; +} + +class Scanner { + pos = 0; + src: string; + constructor(src: string) { + this.src = src; + } + line(): number { + const src = this.src; + const start = this.pos; + let pos = start; + while (pos < src.length && src.charCodeAt(pos) !== 10) pos++; + this.pos = pos + 1; + return pos - start; + } +} + +function lineLengths(s: string): number { + const scanner = new Scanner(s); + let total = 0, + lines = 0; + while (scanner.pos < s.length) { + total = (total * 31 + scanner.line()) % 1000003; + lines++; + } + return total * 1000 + lines; +} + +function remaining(s: string): number { + let pos = 0; + let sum = 0; + while (pos < s.length) { + if (s.charCodeAt(pos) === 32) sum += s.length - pos; + pos++; + } + return sum; +} + +function lastLineLength(s: string): number { + let last = s.length; + for (let i = 0; i < s.length; i++) if (s.charCodeAt(i) === 10) last = s.length - i - 1; + return last; +} + +function from(s: string, start: number): number { + let pos = start; + let sum = 0; + while (pos < s.length) { + sum = sum * 3 + s.charCodeAt(pos); + pos++; + } + return sum; +} + +const words = ["Größe", "naïve", "façade", "東京", "ok", "✓", "mañana", "plain", "😀", "end\n"]; +function text(count: number, seed: number): string { + let out = ""; + for (let k = 0; k < count; k++) out += words[(k * 7 + seed) % words.length] + (k % 5 === 0 ? " " : " "); + return out; +} + +const big = text(3000, 1); +const other = text(2000, 4); +const ascii = big.replace(/[^\x00-\x7f]/g, "x"); +const short = "café ✓ 東京"; +const lone = "a\uD800b\uDFFFc " + big.slice(0, 500) + "\uD83D"; +for (let pass = 0; pass < 3; pass++) { + console.log( + pass, + spaces(big), + hash(big), + rises(big), + backwards(big), + pastEnd(big), + lineStarts(big).length, + wordUnits(big), + ); + console.log(" ", spaces(other), hash(other), spaces(big), hash(other), wordUnits(other)); + console.log(" ", mixed(big, other), sameAt(big, other), surrogates(big), surrogates(lone)); + console.log(" ", spaces(ascii), hash(ascii), spaces(short), hash(short), hash(lone), rises(lone)); + console.log(" ", wordCount(big), wordUnitRuns(big), doubled(big), lineLengths(big)); + console.log(" ", wordCount(ascii), wordUnitRuns(other), doubled(lone), lineLengths(lone), wordCount("")); + const fresh = text(40 + pass, pass) + "ü"; + console.log(" ", remaining(big), remaining(ascii), lastLineLength(big), lastLineLength(other)); + console.log(" ", from(fresh, fresh.length), from(fresh, fresh.length - 3), from(big, big.length - 4)); +} + +let grown = text(1500, 2); +for (let round = 0; round < 3; round++) { + console.log("grown", round, grown.length, spaces(grown), hash(grown), hash(grown)); + grown += text(200, round) + "Ünïcödé"; +} +console.log("empty", spaces(""), hash(""), pastEnd(""), lineStarts("").length); diff --git a/tests/corpus/7306-dunder-exports/dep.ts b/tests/corpus/7306-dunder-exports/dep.ts new file mode 100644 index 000000000..6d54eb365 --- /dev/null +++ b/tests/corpus/7306-dunder-exports/dep.ts @@ -0,0 +1,19 @@ +export const __x = 1; +export const ___y = "three"; +export let __count = 0; +export function __bump(): number { + __count++; + return __count; +} +export class __Point { + a: number; + b: number; + constructor(a: number, b: number) { + this.a = a; + this.b = b; + } + sum(): number { + return this.a + this.b; + } +} +export const plain = "plain"; diff --git a/tests/corpus/7306-dunder-exports/facade.ts b/tests/corpus/7306-dunder-exports/facade.ts new file mode 100644 index 000000000..89ab6eb32 --- /dev/null +++ b/tests/corpus/7306-dunder-exports/facade.ts @@ -0,0 +1,2 @@ +export { __x, __x as __alias, ___y as renamed } from "./dep.ts"; +export * from "./star.ts"; diff --git a/tests/corpus/7306-dunder-exports/main.ts b/tests/corpus/7306-dunder-exports/main.ts new file mode 100644 index 000000000..415bc4a77 --- /dev/null +++ b/tests/corpus/7306-dunder-exports/main.ts @@ -0,0 +1,20 @@ +// Exports whose names start with "__": named imports of a const, a function, +// a class and a live let, a name with three underscores, aliased re-exports, +// names reached through `export *`, namespace member reads and keys, and +// CommonJS exports read through require. +import { __x, ___y, __count, __bump, __Point, plain } from "./dep.ts"; +import { __alias, renamed, __starred, __twice } from "./facade.ts"; +import * as facade from "./facade.ts"; +import * as depNs from "./dep.ts"; +import { createRequire } from "node:module"; + +console.log(__x, ___y, plain); +console.log(__count, __bump(), __bump(), __count); +console.log(new __Point(2, 3).sum()); +console.log(__alias, renamed, __starred, __twice(21)); +console.log(facade.__x, facade.__starred, facade.__twice(2)); +console.log(Object.keys(depNs).join(",")); + +const require = createRequire(import.meta.url); +const table = require("./table.cjs"); +console.log(table.__a, table.__hello("dunder"), table.b); diff --git a/tests/corpus/7306-dunder-exports/star.ts b/tests/corpus/7306-dunder-exports/star.ts new file mode 100644 index 000000000..6ca21d73d --- /dev/null +++ b/tests/corpus/7306-dunder-exports/star.ts @@ -0,0 +1,4 @@ +export const __starred = "from export *"; +export function __twice(n: number): number { + return n * 2; +} diff --git a/tests/corpus/7306-dunder-exports/table.cjs b/tests/corpus/7306-dunder-exports/table.cjs new file mode 100644 index 000000000..e775925ff --- /dev/null +++ b/tests/corpus/7306-dunder-exports/table.cjs @@ -0,0 +1,5 @@ +exports.__a = 10; +exports.__hello = function (name) { + return "hello " + name; +}; +exports.b = 20; diff --git a/tests/corpus/7307-string-unit-loop-evictions.ts b/tests/corpus/7307-string-unit-loop-evictions.ts new file mode 100644 index 000000000..94e98e5b4 --- /dev/null +++ b/tests/corpus/7307-string-unit-loop-evictions.ts @@ -0,0 +1,73 @@ +// A units copy must not run program code that indexes other strings: a +// comparator sort or a copy from an untyped source can evict the scanned +// text's UTF-16 units while the loop reads them. + +function bigText(seed: number): string { + let s = ""; + for (let i = 0; i < 9000; i++) s += String.fromCharCode(0xe9 + ((i * seed) % 7)) + "ab "; + return s; +} + +const others = [bigText(3), bigText(5), bigText(11), bigText(13), bigText(17)]; + +let touched = 0; +function touchOthers(): void { + for (let k = 0; k < others.length; k++) { + const o = others[k]; + for (let j = 0; j < 80; j++) touched += o.charCodeAt((j * 997 + k) % o.length); + } +} + +function spaces(s: string): number { + let n = 0; + for (let i = 0; i < s.length; i++) if (s.charCodeAt(i) === 32) n++; + return n; +} + +function sortedWhileScanning( + s: string, + values: Float64Array, + compare: (a: number, b: number) => number, +): number { + let sum = 0; + for (let i = 0; i < s.length; i++) { + sum += s.charCodeAt(i); + if (i % 4096 === 100) values.sort(compare); + } + return sum; +} + +function copiedWhileScanning(s: string, values: Float64Array, source: any): number { + let sum = 0; + for (let i = 0; i < s.length; i++) { + sum += s.charCodeAt(i); + if (i % 4096 === 100) values.set(source); + } + return sum; +} + +const text = bigText(7); +// Repeated scans make the runtime hold the text's units. +let warm = 0; +for (let r = 0; r < 4; r++) warm += spaces(text); +console.log("warm", warm); + +const values = new Float64Array([3, 1, 2]); +const byValue = (a: number, b: number): number => { + touchOthers(); + return a - b; +}; +console.log("sorted", sortedWhileScanning(text, values, byValue), values.join(",")); +for (let r = 0; r < 2; r++) warm += spaces(text); + +const source: any = { + length: 3, + get 0() { + touchOthers(); + return 9; + }, + 1: 8, + 2: 7, +}; +console.log("copied", copiedWhileScanning(text, values, source), values.join(",")); +console.log("touched", touched > 0, spaces(text)); diff --git a/tests/corpus/7309-globalthis-buffer-aliases.ts b/tests/corpus/7309-globalthis-buffer-aliases.ts new file mode 100644 index 000000000..bf0b9ba5c --- /dev/null +++ b/tests/corpus/7309-globalthis-buffer-aliases.ts @@ -0,0 +1,43 @@ +// Aliases of the global Buffer constructor read through a cast of +// globalThis, the capability-probe spelling of code that also runs where +// Buffer is absent: typed and `as any` casts, `if`, ternary, typeof and `!` +// guards, member calls through the alias, and typeof of the alias. +type RuntimeBuffer = { + byteLength(value: string, encoding: "utf8"): number; + from(value: string, encoding: "utf8"): Uint8Array; + isBuffer(value: unknown): boolean; +}; + +const runtimeBuffer = (globalThis as { Buffer?: RuntimeBuffer }).Buffer; +const anyBuffer = (globalThis as any).Buffer; +const text = "héllo wörld"; + +if (runtimeBuffer) { + console.log("typed", runtimeBuffer.byteLength(text, "utf8")); +} else { + console.log("typed: no Buffer"); +} +if (typeof anyBuffer === "function") { + console.log("any", anyBuffer.byteLength(text, "utf8")); +} +const bytes = runtimeBuffer ? runtimeBuffer.from(text, "utf8") : undefined; +console.log("from", bytes ? bytes.length : -1); +console.log("isBuffer", runtimeBuffer ? runtimeBuffer.isBuffer(bytes) : false); +console.log("typeof", typeof runtimeBuffer, typeof anyBuffer, !runtimeBuffer); + +function utf8Length(value: string): number { + return runtimeBuffer ? runtimeBuffer.byteLength(value, "utf8") : value.length; +} +console.log("function", utf8Length("✓✓"), utf8Length("abc")); + +// A copy of an alias, and an alias whose cast read is guarded with `||`, +// alias the same constructor. +const copied = runtimeBuffer; +const fallback: RuntimeBuffer = { + byteLength: (value: string) => value.length, + from: (value: string) => new Uint8Array(value.length), + isBuffer: () => false, +}; +const orFallback = (globalThis as { Buffer?: RuntimeBuffer }).Buffer || fallback; +console.log("copy", copied ? copied.byteLength(text, "utf8") : -1, typeof copied, !copied); +console.log("or", orFallback.byteLength(text, "utf8"), orFallback.isBuffer(bytes), typeof orFallback); diff --git a/tests/corpus/7310-any-destructure-string-keys.ts b/tests/corpus/7310-any-destructure-string-keys.ts new file mode 100644 index 000000000..4ab24ec77 --- /dev/null +++ b/tests/corpus/7310-any-destructure-string-keys.ts @@ -0,0 +1,60 @@ +// Destructuring an `any` source read through a string-literal key +// (`const { value } = events["a.b"]`): reads, renames, defaults, rest, a +// nested pattern, the assignment form, a template-literal key, chained +// string keys, and V8's TypeError for undefined and null sources, whose +// message spells the key as a property name with its text decoded. +const events: any = { + "a.b": { value: 1, label: "first", extra: true, nested: { deep: "d" } }, + "x-y": { value: 2 }, + outer: { "in.ner": { value: 3 } }, + "nul": null, +}; + +const { value } = events["a.b"]; +const { value: renamed, missing = "fallback", ...rest } = events["a.b"]; +const { + nested: { deep }, +} = events["a.b"]; +console.log(value, renamed, missing, JSON.stringify(rest), deep); + +let assigned: unknown; +({ value: assigned } = events[`x-y`]); +const { value: chained } = events["outer"]["in.ner"]; +console.log(assigned, chained); + +function attempt(label: string, f: () => unknown): void { + try { + console.log(label, f()); + } catch (e) { + console.log(label, e instanceof TypeError, (e as Error).message); + } +} +attempt("missing", () => { + const { value } = events["no.such"]; + return value; +}); +attempt("null", () => { + const { value } = events["nul"]; + return value; +}); +attempt("quotes", () => { + const { value } = events["it's \"quoted\""]; + return value; +}); +attempt("escapes", () => { + const { value } = events["tab\there\nnext"]; + return value; +}); +attempt("accents", () => { + const { value } = events["été ✓"]; + return value; +}); +attempt("template", () => { + const { value } = events[`plain`]; + return value; +}); +attempt("assignment", () => { + let target: unknown; + ({ value: target } = events["gone"]); + return target; +}); diff --git a/tests/corpus/7380-als-http-event-context.mjs b/tests/corpus/7380-als-http-event-context.mjs new file mode 100644 index 000000000..fd6ef6225 --- /dev/null +++ b/tests/corpus/7380-als-http-event-context.mjs @@ -0,0 +1,155 @@ +// AsyncLocalStorage contexts of node:http, node:https and node:tls events. +// A server's 'request' event and its requests' and responses' events run +// in the context of the server's listen() call. A client request's +// events, its response callback and its response's events run in the +// context of the http.request() call, also on a keep-alive socket that +// an earlier request used. TLS client sockets run in the tls.connect() +// call's context, and a TLS server's connections in its listen() +// context. Lines are sorted at the end: events on different sockets may +// interleave. +import { AsyncLocalStorage } from "node:async_hooks"; +import * as fs from "node:fs"; +import http from "node:http"; +import https from "node:https"; +import tls from "node:tls"; + +const store = new AsyncLocalStorage(); +const lines = []; +const log = (what) => lines.push(`${what}: ${store.getStore() ?? "none"}`); + +/** @returns {Promise} */ +function httpScenario() { + return new Promise((resolve) => { + const server = store.run("create", () => + http.createServer((req, res) => { + log("request callback"); + req.on("data", () => log("request data")); + req.on("end", () => { + log("request end"); + res.end("ok"); + }); + res.on("finish", () => log("response finish")); + res.on("close", () => log("response close")); + }), + ); + server.on("request", () => log("request event")); + store.run("listen", () => + server.listen(0, () => { + const port = server.address().port; + const req = store.run("request-call", () => + http.request({ port, method: "POST" }, (res) => { + log("response callback"); + res.on("data", () => log("client response data")); + res.on("end", () => log("client response end")); + }), + ); + store.run("register", () => { + req.on("socket", () => log("client socket")); + req.on("finish", () => log("client finish")); + req.on("close", () => { + log("client close"); + server.close(() => resolve()); + }); + }); + store.run("end-call", () => req.end("body")); + }), + ); + }); +} + +/** @returns {Promise} */ +function keepAliveScenario() { + return new Promise((resolve) => { + const agent = new http.Agent({ keepAlive: true, maxSockets: 1 }); + const server = http.createServer((req, res) => res.end("ok")); + server.listen(0, () => { + const port = server.address().port; + let done = 0; + for (const name of ["first", "second"]) { + store.run(name, () => { + const req = http.get({ port, agent }, (res) => { + log(`${name} response`); + res.on("data", () => {}); + res.on("end", () => { + log(`${name} response end`); + if (++done === 2) { + agent.destroy(); + server.close(() => resolve()); + } + }); + }); + req.on("finish", () => log(`${name} finish`)); + req.on("close", () => log(`${name} close`)); + }); + } + }); + }); +} + +const key = fs.readFileSync("tests/fixtures/server/certs/localhost-key.pem"); +const cert = fs.readFileSync("tests/fixtures/server/certs/localhost.pem"); +const ca = fs.readFileSync("tests/fixtures/server/certs/ca.pem", "utf8"); + +/** @returns {Promise} */ +function httpsScenario() { + return new Promise((resolve) => { + const server = https.createServer({ key, cert }, (req, res) => { + log("https request"); + req.resume(); + res.on("finish", () => log("https response finish")); + res.end("ok"); + }); + store.run("https-listen", () => + server.listen(0, () => { + const port = server.address().port; + store.run("https-get", () => + https + .get({ port, host: "localhost", ca }, (res) => { + log("https response"); + res.on("data", () => {}); + res.on("end", () => log("https response end")); + }) + .on("close", () => { + log("https client close"); + server.close(() => resolve()); + }), + ); + }), + ); + }); +} + +/** @returns {Promise} */ +function tlsScenario() { + return new Promise((resolve) => { + const server = tls.createServer({ key, cert }, (sock) => { + log("tls secureConnection"); + sock.on("data", () => { + log("tls server data"); + sock.end("bye"); + }); + }); + store.run("tls-listen", () => + server.listen(0, () => { + const client = store.run("tls-connect", () => + tls.connect({ port: server.address().port, host: "localhost", ca }, () => { + log("tls secureConnect"); + client.write("hi"); + }), + ); + client.on("data", () => log("tls client data")); + client.on("end", () => log("tls client end")); + client.on("close", () => { + log("tls client close"); + server.close(() => resolve()); + }); + }), + ); + }); +} + +await httpScenario(); +await keepAliveScenario(); +await httpsScenario(); +await tlsScenario(); +for (const line of lines.sort()) console.log(line); diff --git a/tests/corpus/7380-cycle-deferred-folded-key/core.ts b/tests/corpus/7380-cycle-deferred-folded-key/core.ts new file mode 100644 index 000000000..7f4f4ab14 --- /dev/null +++ b/tests/corpus/7380-cycle-deferred-folded-key/core.ts @@ -0,0 +1,5 @@ +import { describe, kind, mark } from "./util.ts"; + +export const version = 3; +export const marked = mark({}); +console.log("core body", kind(), describe()); diff --git a/tests/corpus/7380-cycle-deferred-folded-key/main.ts b/tests/corpus/7380-cycle-deferred-folded-key/main.ts new file mode 100644 index 000000000..92617eba3 --- /dev/null +++ b/tests/corpus/7380-cycle-deferred-folded-key/main.ts @@ -0,0 +1,10 @@ +// A cycle member runs user code before it declares a constant that only +// functions read, one of them as a computed key and one under `typeof` +// (both resolved from the constant's literal type). Nothing calls those +// functions before the declaration ran, so they read the constant +// normally once the other member's top level calls them. +import { marked } from "./core.ts"; +import { describe, kind, mark } from "./util.ts"; + +console.log(JSON.stringify(marked), kind()); +console.log(JSON.stringify(mark({ seen: false })), describe()); diff --git a/tests/corpus/7380-cycle-deferred-folded-key/util.ts b/tests/corpus/7380-cycle-deferred-folded-key/util.ts new file mode 100644 index 000000000..a038d9e63 --- /dev/null +++ b/tests/corpus/7380-cycle-deferred-folded-key/util.ts @@ -0,0 +1,23 @@ +import { version } from "./core.ts"; + +export function makeCounter(): () => number { + let n = 0; + return () => ++n; +} +const next = makeCounter(); +console.log("util body", next(), next()); + +export const TAG = "~tagged"; + +export function mark(box: Record): Record { + box[TAG] = true; + return box; +} + +export function kind(): string { + return typeof TAG; +} + +export function describe(): string { + return `core v${version}`; +} diff --git a/tests/corpus/7381-als-async-generator-steps.ts b/tests/corpus/7381-als-async-generator-steps.ts new file mode 100644 index 000000000..4f6ace58d --- /dev/null +++ b/tests/corpus/7381-als-async-generator-steps.ts @@ -0,0 +1,133 @@ +// AsyncLocalStorage contexts of async generator steps. Each step starts in +// the context of the next(), return() or throw() call that resumes it, +// whatever context created the generator; a store the step enters before +// its first await is the caller's after the call, and after an await the +// step keeps its own copy. A request queued behind a running step starts +// in that step's context, and a return() at a yield unwinds in the +// return() call's context. Covers yield*, for await with break, and two +// generators resumed in turn. +import { AsyncLocalStorage } from "node:async_hooks"; + +const store = new AsyncLocalStorage(); +const lines: string[] = []; +const log = (what: string): void => { + lines.push(`${what}: ${store.getStore() ?? "none"}`); +}; + +async function* steps(): AsyncGenerator { + log("body start"); + yield 1; + log("after the first yield"); + await null; + log("after an await"); + store.enterWith("inside"); + yield 2; + log("after the second yield"); + yield 3; + log("body end"); +} +const it = store.run("create", () => steps()); +await store.run("next-1", () => it.next()); +log("caller after next-1"); +await store.run("next-2", () => it.next()); +log("caller after next-2"); +await store.run("next-3", () => it.next()); +await store.run("next-4", () => it.next()); + +async function* prefixes(): AsyncGenerator { + store.enterWith("prefix"); + yield 1; + store.enterWith("step-2"); + await null; + log("after an await in step 2"); + yield 2; +} +await store.run("outer", async () => { + const gen = prefixes(); + const first = gen.next(); + log("synchronously after next"); + await first; + log("after awaiting next"); + const second = gen.next(); + log("synchronously after the second next"); + await second; + log("after awaiting the second next"); +}); + +async function* queued(): AsyncGenerator { + log("queued start"); + yield 1; + log("queued resumed"); + yield 2; +} +const q = queued(); +await Promise.all([store.run("q-1", () => q.next()), store.run("q-2", () => q.next())]); + +async function* inner(): AsyncGenerator { + log("inner start"); + yield "a"; + log("inner resumed"); + yield "b"; +} +async function* outerGen(): AsyncGenerator { + log("outer start"); + yield* inner(); + log("outer after yield*"); +} +const delegating = outerGen(); +await store.run("o-1", () => delegating.next()); +await store.run("o-2", () => delegating.next()); +await store.run("o-3", () => delegating.next()); + +async function* catching(): AsyncGenerator { + try { + yield 1; + } catch (e) { + log(`caught ${(e as Error).message}`); + yield 2; + } +} +const c = catching(); +await store.run("t-1", () => c.next()); +await store.run("t-2", () => c.throw(new Error("boom"))); + +async function* finallyGen(): AsyncGenerator { + try { + yield 1; + } finally { + log("finally"); + } +} +const f = finallyGen(); +await store.run("n", () => f.next()); +await store.run("return-call", () => f.return()); + +async function* endless(): AsyncGenerator { + let i = 0; + try { + while (true) { + log(`endless ${i}`); + yield i++; + } + } finally { + log("endless finally"); + } +} +await store.run("loop", async () => { + for await (const v of endless()) { + if (v === 1) break; + } +}); + +async function* named(name: string): AsyncGenerator { + log(`${name} 1`); + yield 1; + log(`${name} 2`); + yield 2; +} +const a = named("A"); +const b = named("B"); +await Promise.all([store.run("a-1", () => a.next()), store.run("b-1", () => b.next())]); +await Promise.all([store.run("b-2", () => b.next()), store.run("a-2", () => a.next())]); + +for (const line of lines) console.log(line); diff --git a/tests/corpus/7381-cycle-deferred-reached-tdz/alpha.ts b/tests/corpus/7381-cycle-deferred-reached-tdz/alpha.ts new file mode 100644 index 000000000..8dc002c27 --- /dev/null +++ b/tests/corpus/7381-cycle-deferred-reached-tdz/alpha.ts @@ -0,0 +1,13 @@ +import { probe } from "./beta.ts"; + +export function report(): string { + return `alpha ${NAME}`; +} + +export function late(): string { + return `${OTHER} after ${probe}`; +} + +export const NAME = "ready"; +export const OTHER = "other"; +console.log("alpha body", report()); diff --git a/tests/corpus/7381-cycle-deferred-reached-tdz/beta.ts b/tests/corpus/7381-cycle-deferred-reached-tdz/beta.ts new file mode 100644 index 000000000..c3d1551ff --- /dev/null +++ b/tests/corpus/7381-cycle-deferred-reached-tdz/beta.ts @@ -0,0 +1,12 @@ +import { report } from "./alpha.ts"; + +function attempt(): string { + try { + return report(); + } catch (error) { + return error instanceof ReferenceError ? `ReferenceError: ${error.message}` : "other error"; + } +} + +export const probe = attempt(); +console.log("beta body", probe); diff --git a/tests/corpus/7381-cycle-deferred-reached-tdz/main.ts b/tests/corpus/7381-cycle-deferred-reached-tdz/main.ts new file mode 100644 index 000000000..6a0b7e3ca --- /dev/null +++ b/tests/corpus/7381-cycle-deferred-reached-tdz/main.ts @@ -0,0 +1,7 @@ +// The first cycle member to run calls a function of the other member +// before that member declared the constant the function reads: Node's +// temporal-dead-zone ReferenceError. A second function of the same member +// reads another constant but runs only after the cycle finished. +import { late, report } from "./alpha.ts"; + +console.log(report(), late()); diff --git a/tests/corpus/7381-function-value-arguments-length.ts b/tests/corpus/7381-function-value-arguments-length.ts new file mode 100644 index 000000000..059ff1487 --- /dev/null +++ b/tests/corpus/7381-function-value-arguments-length.ts @@ -0,0 +1,27 @@ +// `arguments.length` in a function declaration counts the arguments its +// caller passes: a direct call passes the declared parameters, and a call +// through the function as a value passes the parameters of the function +// type it was passed as (3 for map and filter, 4 for reduce, 2 for sort). +function scaled(value: number): number { + return value * 10 + arguments.length; +} +function total(acc: number, value: number): number { + return acc + value + arguments.length; +} +function compare(a: number, b: number): number { + return arguments.length === 2 ? a - b : 0; +} +console.log([1, 2].map(scaled).join(), scaled(3)); +console.log([5, 6].filter(scaled).length, [1, 2].reduce(total, 0), total(1, 2)); +console.log([3, 1, 2].sort(compare).join()); + +function apply(f: (a: number, b: number, c: number) => number): number { + return f(1, 2, 3); +} +console.log(apply(scaled)); + +// A direct call inside a call through the value sees its own count. +function depth(n: number): number { + return n <= 0 ? arguments.length : depth(n - 1) + arguments.length; +} +console.log([2].map(depth).join(), depth(2)); diff --git a/tests/corpus/7382-als-async-generator-steps.mjs b/tests/corpus/7382-als-async-generator-steps.mjs new file mode 100644 index 000000000..bbabdb592 --- /dev/null +++ b/tests/corpus/7382-als-async-generator-steps.mjs @@ -0,0 +1,43 @@ +// AsyncLocalStorage contexts of async generator steps from JavaScript: +// steps start in the context of the call that resumes them, a store a step +// enters before its first await is the caller's, queued requests start in +// the running step's context, and return() at a yield unwinds in the +// return() call's context. +import { AsyncLocalStorage } from "node:async_hooks"; +const s = new AsyncLocalStorage(); +const lines = []; +const g = (t) => lines.push(`${t}: ${s.getStore() ?? "none"}`); +async function* gen() { + g("body start"); + yield 1; + g("after first yield"); + await null; + g("after await"); + s.enterWith("inside"); + yield 2; + g("after second yield"); + yield 3; + g("body end"); +} +const it = s.run("create", () => gen()); +await s.run("next1", () => it.next()); +g("caller after next1"); +await s.run("next2", () => it.next()); +g("caller after next2"); +await s.run("next3", () => it.next()); +await s.run("next4", () => it.next()); +// concurrent next calls queued +async function* gen2() { g("g2 start"); yield 1; g("g2 resumed"); yield 2; } +const it2 = gen2(); +const p1 = s.run("q1", () => it2.next()); +const p2 = s.run("q2", () => it2.next()); +await Promise.all([p1, p2]); +// for await +async function* gen3() { for (let i = 0; i < 2; i++) { g("g3 step " + i); yield i; } } +await s.run("for-await", async () => { for await (const v of gen3()) g("loop body " + v); }); +// return/throw +async function* gen4() { try { yield 1; } finally { g("g4 finally"); } } +const it4 = gen4(); +await s.run("n", () => it4.next()); +await s.run("ret", () => it4.return()); +for (const line of lines) console.log(line); diff --git a/tests/corpus/7382-cycle-static-this-tdz/alpha.ts b/tests/corpus/7382-cycle-static-this-tdz/alpha.ts new file mode 100644 index 000000000..ff8eb4a87 --- /dev/null +++ b/tests/corpus/7382-cycle-static-this-tdz/alpha.ts @@ -0,0 +1,11 @@ +import { Probe, Lazy } from "./beta.ts"; + +export { Probe }; +export const label = "ready"; +export const other = "other"; + +export function later(): string { + return `${Lazy.describe()} ${other}`; +} + +console.log("alpha body", Probe.result, Lazy.value); diff --git a/tests/corpus/7382-cycle-static-this-tdz/beta.ts b/tests/corpus/7382-cycle-static-this-tdz/beta.ts new file mode 100644 index 000000000..428907967 --- /dev/null +++ b/tests/corpus/7382-cycle-static-this-tdz/beta.ts @@ -0,0 +1,26 @@ +import { label, other } from "./alpha.ts"; + +function guard(read: () => string): string { + try { + return read(); + } catch (error) { + return error instanceof ReferenceError ? `ReferenceError: ${error.message}` : "other error"; + } +} + +export class Probe { + static helper(): string { + return guard(() => `label ${label}`); + } + static result = this.helper(); +} + +export class Lazy { + static read(): string { + return guard(() => `other ${other}`); + } + static describe(): string { + return `lazy ${other}`; + } + static value = [0].map(() => this.read()).join(""); +} diff --git a/tests/corpus/7382-cycle-static-this-tdz/main.ts b/tests/corpus/7382-cycle-static-this-tdz/main.ts new file mode 100644 index 000000000..e3bbf1581 --- /dev/null +++ b/tests/corpus/7382-cycle-static-this-tdz/main.ts @@ -0,0 +1,8 @@ +// The first cycle member to run builds classes whose static initializers +// call a method through `this`, directly and from an arrow function; both +// methods read constants of the other member before it declared them: +// Node's temporal-dead-zone ReferenceError. A third method reads a constant +// only after the cycle finished. +import { label, Probe, later } from "./alpha.ts"; + +console.log(label, Probe.result, later()); diff --git a/tests/corpus/7383-als-stream-events.ts b/tests/corpus/7383-als-stream-events.ts new file mode 100644 index 000000000..077e15346 --- /dev/null +++ b/tests/corpus/7383-als-stream-events.ts @@ -0,0 +1,204 @@ +// AsyncLocalStorage contexts of stream events and callbacks. A stream +// step that Node defers with process.nextTick runs in the context of the +// code that scheduled it; other events run in the context of the call that +// emits them: push(), write(), end(), destroy() or pipe(). stream.finished +// and stream.pipeline callbacks run in the context of their call. fs read +// and write streams and zlib streams run their events in the context that +// created them, as Node's handles do. +import { AsyncLocalStorage } from "node:async_hooks"; +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; +import { finished, pipeline, Readable, Transform, Writable } from "node:stream"; +import { pipeline as pipelinePromise } from "node:stream/promises"; +import * as zlib from "node:zlib"; + +const store = new AsyncLocalStorage(); +const lines: string[] = []; +const log = (what: string): void => { + lines.push(`${what}: ${store.getStore() ?? "none"}`); +}; +const step = (fn: (done: () => void) => void): Promise => + new Promise((resolve) => fn(resolve)); + +// pipe(): the destination's writes and 'finish' follow the pipe() call. +await step((done) => { + const source = new Readable({ read(): void {} }); + const sink = new Writable({ + write(chunk: Buffer, encoding: BufferEncoding, cb: (error?: Error | null) => void): void { + log("pipe write"); + cb(); + }, + }); + sink.on("finish", () => { + log("pipe finish"); + done(); + }); + store.run("pipe", () => source.pipe(sink)); + store.run("push", () => { + source.push("a"); + source.push(null); + }); +}); + +// A Transform's steps follow write() and end(); 'end' follows end(). +await step((done) => { + const upper = new Transform({ + transform(chunk: Buffer, encoding: BufferEncoding, cb: (error?: Error | null, data?: string) => void): void { + log("transform"); + cb(null, chunk.toString().toUpperCase()); + }, + flush(cb: (error?: Error | null, data?: string) => void): void { + log("flush"); + cb(null, "!"); + }, + }); + upper.on("data", () => log("transform data")); + upper.on("end", () => { + log("transform end"); + done(); + }); + store.run("write", () => upper.write("a")); + store.run("end", () => upper.end()); +}); + +// A write that completes later: its callback, _final, 'prefinish', the +// end() callback, 'finish' and 'close' follow the write() call. +await step((done) => { + const slow = new Writable({ + write(chunk: Buffer, encoding: BufferEncoding, cb: (error?: Error | null) => void): void { + log("slow write"); + setImmediate(cb); + }, + final(cb: (error?: Error | null) => void): void { + log("final"); + cb(); + }, + }); + slow.on("prefinish", () => log("prefinish")); + slow.on("finish", () => log("finish")); + slow.on("close", () => { + log("close"); + done(); + }); + store.run("slow-write", () => slow.write("a", () => log("write callback"))); + store.run("slow-end", () => slow.end(() => log("end callback"))); +}); + +// 'readable' and 'end' follow the push() that triggers them. +await step((done) => { + const source = new Readable({ read(): void {} }); + source.on("readable", () => { + log("readable"); + while (source.read() !== null) {} + }); + source.on("end", () => { + log("readable end"); + done(); + }); + store.run("push-x", () => source.push("x")); + setTimeout(() => store.run("push-eof", () => source.push(null)), 5); +}); + +// destroy(): 'error' and 'close' follow the destroy() call. +await step((done) => { + const source = new Readable({ read(): void {} }); + source.on("error", () => log("destroy error")); + source.on("close", () => { + log("destroy close"); + done(); + }); + store.run("destroy", () => source.destroy(new Error("stopped"))); +}); + +// finished() and pipeline() callbacks follow their call, also when another +// context ends or destroys the stream. +await step((done) => { + const source = new Readable({ read(): void {} }); + store.run("finished", () => + finished(source, () => { + log("finished callback"); + done(); + }), + ); + source.resume(); + store.run("finished-eof", () => source.push(null)); +}); +await step((done) => { + const source = new Readable({ read(): void {} }); + const sink = new Writable({ + write(chunk: Buffer, encoding: BufferEncoding, cb: (error?: Error | null) => void): void { + cb(); + }, + }); + store.run("pipeline", () => + pipeline(source, sink, (err: NodeJS.ErrnoException | null) => { + log(`pipeline callback ${err ? err.message : "ok"}`); + done(); + }), + ); + setTimeout(() => store.run("pipeline-destroy", () => source.destroy(new Error("boom"))), 5); +}); + +// stream/promises pipeline: the stages follow the call; after the await +// the caller's context is back. +await store.run("promise-pipeline", () => + pipelinePromise( + Readable.from(["a"]), + new Writable({ + write(chunk: Buffer, encoding: BufferEncoding, cb: (error?: Error | null) => void): void { + log("promise pipeline write"); + cb(); + }, + }), + ), +); +log("after the promise pipeline"); + +// fs streams: the events follow the context that created the stream. +const file = path.join(os.tmpdir(), `scriptc-7383-${process.pid}.txt`); +await step((done) => { + const out = store.run("fs-write-create", () => fs.createWriteStream(file)); + out.on("finish", () => log("fs finish")); + out.on("close", () => { + log("fs write close"); + done(); + }); + store.run("fs-write", () => out.write("hello", () => log("fs write callback"))); + store.run("fs-end", () => out.end()); +}); +await step((done) => { + const input = store.run("fs-read-create", () => fs.createReadStream(file)); + store.run("fs-listen", () => { + input.on("open", () => log("fs open")); + input.on("data", () => log("fs data")); + input.on("end", () => log("fs end")); + input.on("close", () => { + log("fs read close"); + done(); + }); + }); +}); +fs.unlinkSync(file); + +// zlib streams: the events follow the context that created the stream. +await step((done) => { + const gzip = store.run("gzip-create", () => zlib.createGzip()); + const gunzip = store.run("gunzip-create", () => zlib.createGunzip()); + const parts: Buffer[] = []; + const sink = new Writable({ + write(chunk: Buffer, encoding: BufferEncoding, cb: (error?: Error | null) => void): void { + log("gunzip output"); + parts.push(chunk); + cb(); + }, + }); + store.run("zlib-pipeline", () => + pipeline(Readable.from(["hello ", "world"]), gzip, gunzip, sink, () => { + log(`zlib pipeline callback ${Buffer.concat(parts).toString()}`); + done(); + }), + ); +}); + +for (const line of lines) console.log(line); diff --git a/tests/corpus/7383-cycle-class-expression-name-tdz/alpha.ts b/tests/corpus/7383-cycle-class-expression-name-tdz/alpha.ts new file mode 100644 index 000000000..ff8eb4a87 --- /dev/null +++ b/tests/corpus/7383-cycle-class-expression-name-tdz/alpha.ts @@ -0,0 +1,11 @@ +import { Probe, Lazy } from "./beta.ts"; + +export { Probe }; +export const label = "ready"; +export const other = "other"; + +export function later(): string { + return `${Lazy.describe()} ${other}`; +} + +console.log("alpha body", Probe.result, Lazy.value); diff --git a/tests/corpus/7383-cycle-class-expression-name-tdz/beta.ts b/tests/corpus/7383-cycle-class-expression-name-tdz/beta.ts new file mode 100644 index 000000000..d1d89ad1a --- /dev/null +++ b/tests/corpus/7383-cycle-class-expression-name-tdz/beta.ts @@ -0,0 +1,26 @@ +import { label, other } from "./alpha.ts"; + +function guard(read: () => string): string { + try { + return read(); + } catch (error) { + return error instanceof ReferenceError ? `ReferenceError: ${error.message}` : "other error"; + } +} + +export const Probe = class ProbeImpl { + static helper(): string { + return guard(() => `label ${label}`); + } + static result = ProbeImpl.helper(); +}; + +export const Lazy = class LazyImpl { + static read(): string { + return guard(() => `other ${other}`); + } + static describe(): string { + return `lazy ${other}`; + } + static value = [0].map(() => LazyImpl.read()).join(""); +}; diff --git a/tests/corpus/7383-cycle-class-expression-name-tdz/main.ts b/tests/corpus/7383-cycle-class-expression-name-tdz/main.ts new file mode 100644 index 000000000..93b7a47ee --- /dev/null +++ b/tests/corpus/7383-cycle-class-expression-name-tdz/main.ts @@ -0,0 +1,9 @@ +// The first cycle member to run builds classes from named class +// expressions whose static initializers call a method through the inner +// name, directly and from an arrow function; both methods read constants +// of the other member before it declared them: Node's temporal-dead-zone +// ReferenceError. A third method reads a constant only after the cycle +// finished. +import { label, Probe, later } from "./alpha.ts"; + +console.log(label, Probe.result, later()); diff --git a/tests/corpus/7384-als-stream-events.mjs b/tests/corpus/7384-als-stream-events.mjs new file mode 100644 index 000000000..6d67bf26f --- /dev/null +++ b/tests/corpus/7384-als-stream-events.mjs @@ -0,0 +1,214 @@ +// AsyncLocalStorage contexts of stream events and callbacks, from +// JavaScript. Stream steps that Node defers with process.nextTick run in +// the context that scheduled them: resume() after a 'data' listener, +// uncork() from a tick, a write after end(), a push() from a user tick. +// Other events follow the call that emits them. stream.finished callbacks +// run in the context of their call, with stream/promises' finished() +// resolving in the context that awaits it. readline lines follow the +// createInterface() call that resumes the input, and zlib streams run +// their events and flush() callbacks in the context that created them. +import { AsyncLocalStorage } from "node:async_hooks"; +import readline from "node:readline"; +import { Duplex, finished, PassThrough, Readable, Writable } from "node:stream"; +import { finished as finishedPromise } from "node:stream/promises"; +import zlib from "node:zlib"; + +const store = new AsyncLocalStorage(); +const lines = []; +const log = (what) => lines.push(`${what}: ${store.getStore() ?? "none"}`); +const step = (fn) => new Promise((resolve) => fn(resolve)); + +// pipe() and unpipe() events, and writes through a PassThrough. +await step((done) => { + const through = new PassThrough(); + const sink = new Writable({ write(chunk, encoding, cb) { log("pipe write"); cb(); } }); + sink.on("pipe", () => log("pipe event")); + sink.on("unpipe", () => log("unpipe event")); + store.run("pipe", () => through.pipe(sink)); + store.run("through-write", () => through.write("q")); + setTimeout(() => { + store.run("unpipe", () => through.unpipe(sink)); + done(); + }, 5); +}); + +// cork() and uncork() from a tick: the writes follow the tick's context. +await step((done) => { + const sink = new Writable({ write(chunk, encoding, cb) { log("corked write"); cb(); } }); + sink.on("finish", () => { + log("corked finish"); + done(); + }); + store.run("cork", () => { + sink.cork(); + sink.write("a"); + sink.write("b"); + }); + store.run("uncork", () => process.nextTick(() => sink.uncork())); + setTimeout(() => store.run("corked-end", () => sink.end()), 5); +}); + +// A write after end(): its callback and 'error' follow the write() call. +await step((done) => { + const sink = new Writable({ write(chunk, encoding, cb) { cb(); } }); + sink.on("error", () => log("write after end error")); + sink.end(); + store.run("late-write", () => + sink.write("x", () => { + log("late write callback"); + done(); + }), + ); +}); + +// A failing _write: the callback, 'error' and 'close' follow write(). +await step((done) => { + const sink = new Writable({ write(chunk, encoding, cb) { cb(new Error("bad")); } }); + sink.on("error", () => log("failed write error")); + sink.on("close", () => { + log("failed write close"); + done(); + }); + store.run("failed-write", () => sink.write("x", () => log("failed write callback"))); +}); + +// A Duplex that pushes from _write: 'data' and 'end' follow the resume() +// tick, which the 'data' listener scheduled with no store. +await step((done) => { + const duplex = new Duplex({ + read() {}, + write(chunk, encoding, cb) { + log("duplex write"); + this.push(chunk); + cb(); + }, + }); + duplex.on("data", () => { + log("duplex data"); + duplex.push(null); + }); + duplex.on("end", () => { + log("duplex end"); + done(); + }); + store.run("duplex-write", () => duplex.write("z")); +}); + +// A push() from a user tick, a 'data' listener registered in a store with +// setEncoding(), and PassThrough's 'end' and 'close' after end(). +await step((done) => { + const source = new Readable({ read() {} }); + source.on("data", () => log("tick data")); + source.on("end", () => { + log("tick end"); + done(); + }); + store.run("user-tick", () => + process.nextTick(() => { + log("user tick"); + source.push("x"); + }), + ); + setTimeout(() => store.run("tick-eof", () => source.push(null)), 5); +}); +await step((done) => { + const source = new Readable({ read() {} }); + source.setEncoding("utf8"); + store.run("data-listener", () => source.on("data", () => log("text data"))); + source.on("end", () => { + log("text end"); + done(); + }); + source.push("hi"); + source.push(null); +}); +await step((done) => { + const through = new PassThrough(); + through.on("end", () => log("through end")); + through.on("close", () => { + log("through close"); + done(); + }); + through.resume(); + store.run("through-end", () => through.end("last")); +}); + +// finished() on a Writable, and stream/promises' finished(). +await step((done) => { + const sink = new Writable({ write(chunk, encoding, cb) { setTimeout(cb, 1); } }); + store.run("finished-writable", () => + finished(sink, () => { + log("finished callback"); + done(); + }), + ); + sink.on("finish", () => log("writable finish")); + store.run("writable-end", () => { + sink.write("a"); + sink.end(); + }); +}); +await step((done) => { + const source = new Readable({ read() {} }); + source.resume(); + store.run("finished-promise", () => finishedPromise(source)).then(() => { + log("finished promise"); + done(); + }); + setTimeout(() => store.run("promise-eof", () => source.push(null)), 5); +}); + +// Readable.from() over an async generator: the reads follow the resume() +// tick, scheduled with no store. +await step((done) => { + async function* letters() { + log("generator start"); + yield "a"; + log("generator end"); + } + const source = store.run("from", () => Readable.from(letters())); + source.on("data", () => log("from data")); + source.on("end", () => { + log("from end"); + done(); + }); +}); + +// readline over a Readable: createInterface() resumes the input. +await step((done) => { + const input = Readable.from(["a\nb\n"]); + const rl = store.run("readline", () => readline.createInterface({ input })); + rl.on("line", (line) => log(`line ${line}`)); + rl.on("close", () => { + log("readline close"); + done(); + }); +}); + +// zlib streams: events and flush() callbacks follow the creation context. +await step((done) => { + const gzip = store.run("gzip-create", () => zlib.createGzip()); + store.run("gzip-listen", () => { + gzip.on("data", () => log("gzip data")); + gzip.on("end", () => { + log("gzip end"); + done(); + }); + }); + store.run("gzip-write", () => { + gzip.write("hello"); + gzip.end(); + }); +}); +await step((done) => { + const deflate = store.run("deflate-create", () => zlib.createDeflate()); + store.run("deflate-flush", () => deflate.flush(() => log("deflate flush callback"))); + deflate.on("data", () => {}); + deflate.on("end", () => { + log("deflate end"); + done(); + }); + store.run("deflate-end", () => deflate.end("x")); +}); + +for (const line of lines) console.log(line); diff --git a/tests/corpus/7385-als-child-process-events.ts b/tests/corpus/7385-als-child-process-events.ts new file mode 100644 index 000000000..54fd80c3a --- /dev/null +++ b/tests/corpus/7385-als-child-process-events.ts @@ -0,0 +1,104 @@ +// AsyncLocalStorage contexts of node:child_process events. A child's +// 'spawn', 'error', 'exit' and 'close' events and its stdout and stderr +// 'data' and 'end' events run in the context of the spawn() call, +// whatever context registered the listener or killed the child. stdin's +// 'finish' runs in the context of the end() call, and its 'drain' and +// 'error' in the context of the write() call. Lines are sorted at the end: +// the stdout and stderr events of one child may interleave. +import { AsyncLocalStorage } from "node:async_hooks"; +import { spawn } from "node:child_process"; + +const store = new AsyncLocalStorage(); +const lines: string[] = []; +const log = (what: string): void => { + lines.push(`${what}: ${store.getStore() ?? "none"}`); +}; +const step = (fn: (done: () => void) => void): Promise => + new Promise((resolve) => fn(resolve)); + +await step((done) => { + const child = store.run("spawn", () => spawn("/bin/sh", ["-c", "printf out; printf err >&2"])); + store.run("listen", () => { + child.on("spawn", () => log("spawn event")); + child.stdout!.on("data", () => log("stdout data")); + child.stdout!.on("end", () => log("stdout end")); + child.stderr!.on("data", () => log("stderr data")); + child.on("exit", () => log("exit")); + child.on("close", () => { + log("close"); + done(); + }); + }); +}); + +await step((done) => { + const child = store.run("spawn-cat", () => spawn("/bin/cat")); + child.stdout!.on("data", () => log("cat data")); + store.run("stdin-write", () => { + child.stdin!.write("x"); + }); + child.stdin!.on("finish", () => log("stdin finish")); + store.run("stdin-end", () => { + child.stdin!.end(); + }); + child.on("close", () => { + log("cat close"); + done(); + }); +}); + +await step((done) => { + const child = store.run("spawn-missing", () => spawn("/nonexistent/command")); + child.on("error", () => log("spawn error")); + child.on("close", () => { + log("missing close"); + done(); + }); +}); + +await step((done) => { + const child = store.run("spawn-sleep", () => spawn("/bin/sleep", ["5"])); + child.on("exit", () => log("killed exit")); + child.on("close", () => { + log("killed close"); + done(); + }); + setTimeout(() => store.run("kill", () => child.kill()), 20); +}); + +// A write that fills the pipe: 'drain' runs in the write() call's context. +await step((done) => { + const child = store.run("spawn-slow", () => + spawn("/bin/sh", ["-c", "sleep 0.2; cat > /dev/null"]), + ); + child.stdin!.on("drain", () => { + log("stdin drain"); + child.stdin!.end(); + }); + child.on("close", () => { + log("slow close"); + done(); + }); + store.run("large-write", () => { + child.stdin!.write(Buffer.alloc(1 << 22, 97)); + }); +}); + +// A write to a child that closed its stdin: 'error' runs in the write() +// call's context. +await step((done) => { + const child = store.run("spawn-closed", () => spawn("/bin/sh", ["-c", "exec 0<&-; sleep 0.1"])); + child.stdin!.on("error", () => log("stdin error")); + child.on("close", () => { + log("closed close"); + done(); + }); + setTimeout(() => { + store.run("late-write", () => { + child.stdin!.write(Buffer.alloc(1 << 20, 97)); + }); + }, 50); +}); + +lines.sort(); +for (const line of lines) console.log(line); diff --git a/tests/corpus/7386-als-child-fork-events/main.mjs b/tests/corpus/7386-als-child-fork-events/main.mjs new file mode 100644 index 000000000..18b410643 --- /dev/null +++ b/tests/corpus/7386-als-child-fork-events/main.mjs @@ -0,0 +1,25 @@ +// AsyncLocalStorage contexts of a forked child's IPC events. The child's +// 'message', 'disconnect', 'exit' and 'close' events run in the context of +// the fork() call, whatever context registered the listener; a send() +// callback runs in the context of the send() call. +import { AsyncLocalStorage } from "node:async_hooks"; +import childProcess from "node:child_process"; + +const store = new AsyncLocalStorage(); +const lines = []; +const log = (what) => lines.push(`${what}: ${store.getStore() ?? "none"}`); + +const child = store.run("fork", () => + childProcess.fork(new URL("./worker.mjs", import.meta.url), []), +); +store.run("listen", () => { + child.on("message", (message) => log(`message ${JSON.stringify(message)}`)); +}); +child.on("disconnect", () => log("disconnect")); +store.run("send", () => child.send("hi", () => log("send callback"))); +setTimeout(() => store.run("send-bye", () => child.send("bye")), 50); +child.on("exit", () => log("exit")); +child.on("close", () => { + log("close"); + for (const line of lines) console.log(line); +}); diff --git a/tests/corpus/7386-als-child-fork-events/worker.mjs b/tests/corpus/7386-als-child-fork-events/worker.mjs new file mode 100644 index 000000000..79f4bfdc6 --- /dev/null +++ b/tests/corpus/7386-als-child-fork-events/worker.mjs @@ -0,0 +1,4 @@ +process.on("message", (m) => { + process.send({ echo: m }); + if (m === "bye") process.disconnect(); +}); diff --git a/tests/corpus/7387-als-http2-events.mjs b/tests/corpus/7387-als-http2-events.mjs new file mode 100644 index 000000000..4dd17916e --- /dev/null +++ b/tests/corpus/7387-als-http2-events.mjs @@ -0,0 +1,91 @@ +// AsyncLocalStorage contexts of node:http2 events. A client session's +// 'connect' and 'close' run in the context of the http2.connect() call, +// and a client stream's 'response', 'data', 'end' and 'close' in the +// context of the request() call. A server's 'session' and 'stream' events +// run in the context of its listen() call; a server stream's own 'data', +// 'end' and 'close' events run with no store, as does a server.close() +// callback that waits for the sessions. Lines are sorted at the end: +// events on the two sides may interleave. +import { AsyncLocalStorage } from "node:async_hooks"; +import http2 from "node:http2"; + +const store = new AsyncLocalStorage(); +const lines = []; +const log = (what) => lines.push(`${what}: ${store.getStore() ?? "none"}`); + +/** @returns {Promise} */ +function requestScenario() { + return new Promise((resolve) => { + const server = store.run("create", () => http2.createServer()); + server.on("session", () => log("server session")); + server.on("stream", (stream) => { + log("server stream"); + stream.on("data", () => log("server stream data")); + stream.on("end", () => { + log("server stream end"); + store.run("respond", () => { + stream.respond({ ":status": 200 }); + stream.end("ok"); + }); + }); + }); + store.run("listen", () => + server.listen(0, () => { + log("listening"); + const client = store.run("connect", () => + http2.connect(`http://localhost:${server.address().port}`), + ); + client.on("connect", () => log("client connect")); + client.on("close", () => log("client session close")); + const req = store.run("request", () => client.request({ ":method": "POST", ":path": "/" })); + req.on("response", () => log("client response")); + req.on("data", () => log("client data")); + req.on("end", () => { + log("client end"); + store.run("close", () => { + client.close(); + server.close(() => { + log("server close callback"); + resolve(); + }); + }); + }); + store.run("write", () => { + req.write("x"); + req.end(); + }); + }), + ); + }); +} + +/** @returns {Promise} */ +function resetScenario() { + return new Promise((resolve) => { + const server = store.run("reset-create", () => http2.createServer()); + server.on("stream", (stream) => { + log("reset server stream"); + stream.on("close", () => log("reset server stream close")); + store.run("reset", () => stream.close(http2.constants.NGHTTP2_CANCEL)); + }); + store.run("reset-listen", () => + server.listen(0, () => { + const client = store.run("reset-connect", () => + http2.connect(`http://localhost:${server.address().port}`), + ); + const req = store.run("reset-request", () => client.request({ ":path": "/" })); + req.on("close", () => { + log("reset client close"); + client.close(); + server.close(() => resolve()); + }); + req.end(); + }), + ); + }); +} + +await requestScenario(); +await resetScenario(); +lines.sort(); +for (const line of lines) console.log(line); diff --git a/tests/corpus/7388-als-http2-compat-events.mjs b/tests/corpus/7388-als-http2-compat-events.mjs new file mode 100644 index 000000000..da44dc378 --- /dev/null +++ b/tests/corpus/7388-als-http2-compat-events.mjs @@ -0,0 +1,49 @@ +// AsyncLocalStorage contexts of the node:http2 compatibility API. The +// createServer() request handler runs in the context of the server's +// listen() call. The request's 'end' and the response's 'finish' and +// 'close' follow the server stream, which runs with no store, whatever +// context calls res.end(). A client stream's 'response', 'data' and +// 'close' run in the context of the request() call, and the client +// session's 'close' in the context of the http2.connect() call. Lines are +// sorted at the end: events on the two sides may interleave. +import { AsyncLocalStorage } from "node:async_hooks"; +import http2 from "node:http2"; + +const store = new AsyncLocalStorage(); +const lines = []; +const log = (what) => lines.push(`${what}: ${store.getStore() ?? "none"}`); + +const server = store.run("create", () => + http2.createServer((req, res) => { + log("request handler"); + req.resume(); + req.on("end", () => { + log("request end"); + res.on("finish", () => log("response finish")); + res.on("close", () => log("response close")); + store.run("respond", () => res.end("done")); + }); + }), +); +store.run("listen", () => + server.listen(0, () => { + const client = store.run("connect", () => + http2.connect(`http://localhost:${server.address().port}`), + ); + client.on("close", () => { + log("client session close"); + server.close(() => { + lines.sort(); + for (const line of lines) console.log(line); + }); + }); + const req = store.run("request", () => client.request({ ":method": "POST", ":path": "/x" })); + req.on("response", () => log("client response")); + req.on("data", () => log("client data")); + req.on("close", () => { + log("client stream close"); + store.run("close", () => client.close()); + }); + store.run("write", () => req.end("body")); + }), +); diff --git a/tests/corpus/7389-als-http2-secure-events.mjs b/tests/corpus/7389-als-http2-secure-events.mjs new file mode 100644 index 000000000..0772b5267 --- /dev/null +++ b/tests/corpus/7389-als-http2-secure-events.mjs @@ -0,0 +1,94 @@ +// AsyncLocalStorage contexts of node:http2 events over TLS +// (http2.createSecureServer and an https: authority). They follow corpus +// 7387's rules, with one difference that comes from the transport: a TLS +// server's connections run in the context of its listen() call, so a +// server.close() callback that waits for the sessions runs there too. +// Lines are sorted at the end: events on the two sides may interleave. +import { AsyncLocalStorage } from "node:async_hooks"; +import fs from "node:fs"; +import http2 from "node:http2"; + +const key = fs.readFileSync("tests/fixtures/server/certs/localhost-key.pem"); +const cert = fs.readFileSync("tests/fixtures/server/certs/localhost.pem"); +const ca = fs.readFileSync("tests/fixtures/server/certs/ca.pem", "utf8"); + +const store = new AsyncLocalStorage(); +const lines = []; +const log = (what) => lines.push(`${what}: ${store.getStore() ?? "none"}`); + +/** @returns {Promise} */ +function requestScenario() { + return new Promise((resolve) => { + const server = store.run("create", () => http2.createSecureServer({ key, cert })); + server.on("session", () => log("server session")); + server.on("stream", (stream) => { + log("server stream"); + stream.on("data", () => log("server stream data")); + stream.on("end", () => { + log("server stream end"); + store.run("respond", () => { + stream.respond({ ":status": 200 }); + stream.end("ok"); + }); + }); + }); + store.run("listen", () => + server.listen(0, () => { + log("listening"); + const client = store.run("connect", () => + http2.connect(`https://localhost:${server.address().port}`, { ca }), + ); + client.on("connect", () => log("client connect")); + client.on("close", () => log("client session close")); + const req = store.run("request", () => client.request({ ":method": "POST", ":path": "/" })); + req.on("response", () => log("client response")); + req.on("data", () => log("client data")); + req.on("end", () => { + log("client end"); + store.run("close", () => { + client.close(); + server.close(() => { + log("server close callback"); + resolve(); + }); + }); + }); + store.run("write", () => { + req.write("x"); + req.end(); + }); + }), + ); + }); +} + +/** @returns {Promise} */ +function resetScenario() { + return new Promise((resolve) => { + const server = store.run("reset-create", () => http2.createSecureServer({ key, cert })); + server.on("stream", (stream) => { + log("reset server stream"); + stream.on("close", () => log("reset server stream close")); + store.run("reset", () => stream.close(http2.constants.NGHTTP2_CANCEL)); + }); + store.run("reset-listen", () => + server.listen(0, () => { + const client = store.run("reset-connect", () => + http2.connect(`https://localhost:${server.address().port}`, { ca }), + ); + const req = store.run("reset-request", () => client.request({ ":path": "/" })); + req.on("close", () => { + log("reset client close"); + client.close(); + server.close(() => resolve()); + }); + req.end(); + }), + ); + }); +} + +await requestScenario(); +await resetScenario(); +lines.sort(); +for (const line of lines) console.log(line); diff --git a/tests/corpus/7390-record-key-read-presence.ts b/tests/corpus/7390-record-key-read-presence.ts new file mode 100644 index 000000000..76bb355ca --- /dev/null +++ b/tests/corpus/7390-record-key-read-presence.ts @@ -0,0 +1,49 @@ +// Keyed reads of string-indexed records whose values are numbers or +// booleans, consumed by `??`, `!`, comparisons with undefined and typeof: +// declared fields, an optional declared field, index-signature entries and +// missing keys. A stored NaN is a value, so `??` keeps it; -0 survives the +// read; a missing key is undefined. +interface Scores { + base: number; + bonus?: number; + [name: string]: number | undefined; +} + +const scores: Scores = { base: 10 }; +scores["alice"] = 3; +scores["nan"] = NaN; +scores["negzero"] = -0; + +const keys = ["base", "bonus", "alice", "nan", "negzero", "missing"]; +for (const key of keys) { + const read = scores[key]; + console.log( + key, + scores[key] ?? "default", + scores[key] === undefined, + typeof scores[key], + Object.is(scores[key] ?? 1, -0), + (scores[key] ?? 0) + 1, + read, + ); +} +scores.bonus = 5; +console.log("bonus", scores["bonus"] ?? "default", scores["bonus"]! * 2); + +const counts: Record = {}; +for (const word of "a b a c b a d".split(" ")) counts[word] = (counts[word] ?? 0) + 1; +let total = 0; +for (const word in counts) total += counts[word]!; +console.log(JSON.stringify(counts), total, counts["zzz"] ?? -1); + +const flags: Record = { on: true, off: false }; +for (const key of ["on", "off", "unset"]) { + console.log(key, flags[key] ?? "unset", flags[key] === undefined, typeof flags[key]); +} +let seen = 0; +for (let i = 0; i < 1000; i++) { + const key = "k" + (i % 7); + flags[key] = !(flags[key] ?? false); + if (flags[key]!) seen++; +} +console.log(seen, Object.keys(flags).length); diff --git a/tests/corpus/7391-als-response-end-and-finished.mjs b/tests/corpus/7391-als-response-end-and-finished.mjs new file mode 100644 index 000000000..91730201e --- /dev/null +++ b/tests/corpus/7391-als-response-end-and-finished.mjs @@ -0,0 +1,54 @@ +// AsyncLocalStorage contexts that follow the call rather than the resource. +// An HTTP server response ended inside another store runs its end() +// callback, 'finish' and 'close' in the end() call's context, not the +// server's listen() context. A stream.finished() callback registered with +// no store runs with no store, also when another context destroys the +// stream; one registered inside a store runs in that store. +import { AsyncLocalStorage } from "node:async_hooks"; +import http from "node:http"; +import { Readable, finished } from "node:stream"; + +const store = new AsyncLocalStorage(); +const lines = []; +const log = (what) => lines.push(`${what}: ${store.getStore() ?? "none"}`); + +/** @returns {Promise} */ +function responseScenario() { + return new Promise((resolve) => { + const server = http.createServer((req, res) => { + log("request"); + res.on("finish", () => log("response finish")); + res.on("close", () => log("response close")); + store.run("handler-end", () => res.end("ok", () => log("response end callback"))); + }); + store.run("listen", () => + server.listen(0, () => { + http.get(`http://localhost:${server.address().port}/`, (response) => { + response.resume(); + response.on("end", () => server.close(() => resolve())); + }); + }), + ); + }); +} +await responseScenario(); + +/** @returns {Promise} */ +function finishedScenario(registration) { + return new Promise((resolve) => { + const stream = new Readable({ read() {} }); + const register = () => + finished(stream, () => { + log(`finished registered ${registration ?? "outside"}`); + resolve(); + }); + if (registration === undefined) register(); + else store.run(registration, register); + store.run("destroyer", () => stream.destroy()); + }); +} +await finishedScenario(undefined); +await finishedScenario("inside"); + +lines.sort(); +for (const line of lines) console.log(line); diff --git a/tests/corpus/7392-non-null-operands.ts b/tests/corpus/7392-non-null-operands.ts new file mode 100644 index 000000000..60928ddb6 --- /dev/null +++ b/tests/corpus/7392-non-null-operands.ts @@ -0,0 +1,137 @@ +// Non-null assertions on values that still hold undefined or null, used as +// operands of operators that convert them: arithmetic, comparisons, string +// conversion, Math functions, and loose equality with null. `!` +// performs no runtime check, so the operator sees undefined or null and +// converts it as JavaScript does. + +interface Settings { + limit?: number; + label?: string; + enabled?: boolean; + ratio: number | null; + note?: string | null; +} + +const empty: Settings = { ratio: null }; +const full: Settings = { limit: 4, label: "abc", enabled: true, ratio: 0.5, note: "n" }; +const counts: Record = { a: 1, b: 2 }; +const sizes = new Map([["small", 1]]); +const values = [3, 1, 4]; + +function show(v: unknown): string { + return Object.is(v, -0) ? "-0" : String(v); +} + +function line(label: string, parts: unknown[]): void { + console.log(label.padEnd(12), parts.map(show).join(" ")); +} + +for (const s of [empty, full]) { + const tag = s === empty ? "empty" : "full"; + line(`${tag} arith`, [ + s.limit! + 1, + s.limit! - 1, + s.limit! * 2, + s.limit! / 2, + s.limit! % 3, + s.limit! ** 2, + -s.limit!, + +s.limit!, + s.ratio! + 1, + s.ratio! * 4, + -s.ratio!, + s.limit! - s.ratio!, + ]); + line(`${tag} bits`, [s.limit! | 0, s.limit! & 6, s.limit! << 1, s.limit! >>> 0, ~s.limit!, s.ratio! ^ 1]); + line(`${tag} compare`, [ + s.limit! < 5, + s.limit! >= 0, + s.ratio! < 1, + s.ratio! > -1, + s.limit! === 4, + s.limit! !== 4, + s.ratio! == 0, + s.label! < "b", + s.label! >= "a", + s.label! === "abc", + s.enabled! === true, + s.limit! > s.ratio!, + ]); + line(`${tag} strings`, [ + `${s.limit!}`, + `${s.ratio!}`, + `${s.enabled!}`, + `[${s.label!}]`, + `${s.note!}`, + "n=" + s.limit!, + s.limit! + "!", + s.label! + "?", + "<" + s.note!, + ]); + line(`${tag} convert`, [ + Math.round(s.limit!), + Math.max(s.limit!, 1), + Math.min(s.ratio!, 1), + !s.enabled!, + !s.ratio!, + ]); +} + +line("record", [counts["z"]! + 1, counts["a"]! + 1, counts["z"]! > 0, `${counts["z"]!}`]); +line("map", [sizes.get("large")! * 2, sizes.get("small")! * 2, `${sizes.get("large")!}`]); +line("find", [values.find((v) => v > 9)! + 1, values.find((v) => v > 3)! + 1]); + +let total = 0; +let text = ""; +let mask = 7; +for (const key of ["a", "b", "c", "a"]) { + total += counts[key]!; + text += counts[key]!; + text += ","; + mask &= counts[key]!; +} +line("compound", [total, text, mask]); + +const tally: Record = {}; +for (const word of ["x", "y", "x"]) tally[word] = tally[word]! + 1; +line("tally", [tally["x"], tally["y"]]); + +type Cell = number | string | null | undefined; +for (const cell of [3, "4", null, undefined] as Cell[]) { + const m = new Map([["k", cell]]); + line("cell", [`${m.get("k")!}`, m.get("k")! === 3, String(m.get("k")!)]); +} + +// Loose equality with null tests both unit arms of a value read once. +const optional = new Map([["one", 1], ["none", null]]); +let reads = 0; +function lookup(key: string): string | null | undefined { + reads++; + return key === "a" ? "x" : key === "b" ? null : undefined; +} +for (const key of ["one", "none", "missing"]) { + line(`loose ${key}`, [optional.get(key)! == null, optional.get(key)! != undefined, optional.get(key)! == 1]); +} +line("loose call", [lookup("a") == null, lookup("b") == null, lookup("c") != null, reads]); + +// `+` with an untyped partner concatenates or adds depending on what the +// partner holds when it runs. +const untypedText: any = "x"; +const untypedNumber: any = 5; +const untypedObject: any = { toString: () => "obj" }; +for (const s of [empty, full]) { + let joined: any = "t"; + let summed: any = 1; + joined += s.limit!; + summed += s.limit!; + line(`untyped ${s === empty ? "empty" : "full"}`, [ + s.limit! + untypedText, + untypedText + s.limit!, + s.limit! + untypedNumber, + untypedNumber + s.limit!, + s.label! + untypedText, + s.limit! + untypedObject, + joined, + summed, + ]); +} diff --git a/tests/corpus/7394-non-null-typed-array-reads.ts b/tests/corpus/7394-non-null-typed-array-reads.ts new file mode 100644 index 000000000..bab1bcf9c --- /dev/null +++ b/tests/corpus/7394-non-null-typed-array-reads.ts @@ -0,0 +1,80 @@ +// Non-null assertions on typed-array element reads. `!` performs no +// runtime check: an index outside the array reads undefined, as in Node, +// wherever the use tells undefined from NaN (equality, string conversion, +// typeof, Object.is, an array element), and NaN where it converts the value +// to a number. + +const int32 = new Int32Array([1, -2, 3]); +const float64 = new Float64Array([1.5, NaN, -0]); +const uint8 = new Uint8Array([7, 255]); + +function show(v: unknown): string { + return Object.is(v, -0) ? "-0" : String(v); +} + +function line(label: string, parts: unknown[]): void { + console.log(label.padEnd(12), parts.map(show).join(" ")); +} + +for (const k of [0, 1, 2, 3, -1, 1.5]) { + line(`int32 ${k}`, [ + int32[k]! === int32[k]!, + int32[k]! !== int32[0]!, + int32[k]! === undefined, + int32[k]! == null, + Object.is(int32[k]!, undefined), + typeof int32[k]!, + `${int32[k]!}`, + "v=" + int32[k]!, + String(int32[k]!), + int32[k]! + 1, + int32[k]! < 2, + [int32[k]!].length, + ]); + line(`float64 ${k}`, [ + float64[k]! === float64[k]!, + float64[k]! === undefined, + Object.is(float64[k]!, NaN), + typeof float64[k]!, + `${float64[k]!}`, + float64[k]! * 2, + -float64[k]!, + ]); + line(`uint8 ${k}`, [ + uint8[k]! === uint8[k]!, + uint8[k]! == undefined, + typeof uint8[k]!, + `${uint8[k]!}`, + uint8[k]! | 0, + uint8[k]! > 100, + ]); +} + +const optional: number | undefined = uint8[5]!; +const listed = [int32[9]!, float64[0]!]; +line("slots", [optional, listed[0], listed[1], listed.length]); + +let total = 0; +for (let i = 0; i <= int32.length; i++) total += int32[i]!; +line("sum", [total]); + +// Arrays that store an asserted read past the end hold undefined, which +// their callbacks see. +const pushed: number[] = []; +pushed.push(int32[7]!); +pushed.push(uint8[1]!); +const literal = [float64[5]!, float64[0]!]; +const assigned: number[] = [1, 2]; +assigned[0] = uint8[9]!; +let visited = 0; +pushed.forEach((v) => { + visited += v; +}); +line("callbacks", [ + pushed.map((v) => v * 2).join(","), + visited, + literal.map((v) => typeof v).join(","), + literal.filter((v) => v === undefined).length, + assigned.map((v) => v + 1).join(","), + assigned.some((v) => v === undefined), +]); diff --git a/tests/corpus/7404-frozen-records.ts b/tests/corpus/7404-frozen-records.ts new file mode 100644 index 000000000..acf9b393b --- /dev/null +++ b/tests/corpus/7404-frozen-records.ts @@ -0,0 +1,124 @@ +// Object.freeze of records that other references still hold: writes +// through any reference then throw Node's TypeError (this module is strict +// mode code; 7405 covers sloppy mode). Covers static freezes, freezes +// through untyped values, Object.isFrozen, keyed writes and deletes, +// optional-field deletes, Object.assign, tuples, and frozen records that are +// released or collected as cycles. +export {}; + +type Counter = { count: number; label: string }; +type Link = { name: string; next?: Link }; + +function report(e: unknown): void { + console.log((e as Error).name + ": " + (e as Error).message); +} + +function freezeUnknown(value: unknown): unknown { + return Object.freeze(value); +} + +function isFrozenUnknown(value: unknown): boolean { + return Object.isFrozen(value); +} + +function deepFreeze(value: unknown): unknown { + if (typeof value === "object" && value !== null) { + Object.freeze(value); + for (const key of Object.keys(value)) deepFreeze((value as Record)[key]); + } + return value; +} + +type Config = { name: string; limits: { max: number; min: number } }; + +function bump(c: Counter): void { + c.count += 1; +} + +function writes(): void { + const a: Counter = { count: 1, label: "a" }; + const alias = a; + console.log(Object.isFrozen(a), isFrozenUnknown(a)); + const frozen = Object.freeze(alias); + console.log(frozen === a, Object.isFrozen(a), isFrozenUnknown(alias)); + try { bump(a); } catch (e) { report(e); } + try { a.label = "b"; } catch (e) { report(e); } + console.log(a.count, a.label, JSON.stringify(a)); + + const viaUnknown: Counter = { count: 10, label: "u" }; + const back = freezeUnknown(viaUnknown); + console.log(back === viaUnknown, Object.isFrozen(viaUnknown)); + try { viaUnknown.count = 11; } catch (e) { report(e); } + console.log(viaUnknown.count); + + const dict: Record = { one: 1 }; + const dictAlias = dict; + Object.freeze(dictAlias); + try { dict["one"] = 2; } catch (e) { report(e); } + try { dict["two"] = 2; } catch (e) { report(e); } + try { delete dict["one"]; } catch (e) { report(e); } + delete dict["three"]; + console.log(JSON.stringify(dict), Object.isFrozen(dict)); + + const optional: { id: number; note?: string } = { id: 1, note: "n" }; + const optionalAlias = optional; + Object.freeze(optionalAlias); + try { delete optional.note; } catch (e) { report(e); } + console.log(JSON.stringify(optional)); + const bare: { id: number; note?: string } = { id: 2 }; + Object.freeze(bare); + delete bare.note; + console.log(JSON.stringify(bare)); + + const target: Counter = { count: 0, label: "t" }; + Object.freeze(target); + try { Object.assign(target, { count: 5, label: "x" }); } catch (e) { report(e); } + console.log(JSON.stringify(target)); + + const pair: [string, number] = ["p", 1]; + Object.freeze(pair); + try { pair[1] = 2; } catch (e) { report(e); } + console.log(pair[0], pair[1]); +} + +function deepFreezes(): void { + const config: Config = JSON.parse('{"name":"c","limits":{"max":2,"min":1}}'); + const limits = config.limits; + console.log(Object.isFrozen(config), Object.isFrozen(limits)); + deepFreeze(config); + console.log(Object.isFrozen(config), Object.isFrozen(limits), isFrozenUnknown(config.limits)); + try { config.limits.max = 3; } catch (e) { report(e); } + try { limits.min = 0; } catch (e) { report(e); } + try { config.name = "d"; } catch (e) { report(e); } + console.log(JSON.stringify(config)); +} + +function lifetimes(): void { + const list: Counter[] = []; + for (let i = 0; i < 4; i++) { + const item: Counter = { count: i, label: "x" + i }; + list.push(item); + if (i % 2 === 1) Object.freeze(item); + } + for (const item of list) { + try { item.count = item.count * 10; } catch (e) { console.log(item.label, (e as Error).message); } + } + console.log(JSON.stringify(list)); + const kept = list[1]!; + list.length = 0; + console.log(kept.count, Object.isFrozen(kept)); + + for (let round = 0; round < 3; round++) { + const first: Link = { name: "first" + round }; + const second: Link = { name: "second" + round, next: first }; + first.next = second; + Object.freeze(first); + Object.freeze(second); + try { first.name = "changed"; } catch (e) { report(e); } + console.log(first.next?.name, second.next?.name); + } +} + +writes(); +deepFreezes(); +lifetimes(); diff --git a/tests/corpus/7405-frozen-records-sloppy.cjs b/tests/corpus/7405-frozen-records-sloppy.cjs new file mode 100644 index 000000000..2b4e29213 --- /dev/null +++ b/tests/corpus/7405-frozen-records-sloppy.cjs @@ -0,0 +1,53 @@ +// Writes to frozen records in sloppy-mode code leave them unchanged, where +// strict mode code throws (7404). The records are typed through JSDoc, so +// the writes are native record writes. + +/** @typedef {{ count: number, label: string }} Counter */ +/** @typedef {{ id: number, note?: string }} Note */ +/** @typedef {{ kind: "a", name: string }} Small */ +/** @typedef {{ kind: "b", name: string, size: number }} Large */ + +/** @param {Counter} c */ +function bump(c) { + c.count += 1; + c.label = c.label + "!"; +} + +/** @param {Note} n */ +function dropNote(n) { + delete n.note; +} + +/** @param {number} count @returns {Counter} */ +function counter(count) { + return { count: count, label: "c" + count }; +} + +const open = counter(1); +const frozen = Object.freeze(counter(2)); +bump(open); +bump(frozen); +console.log(JSON.stringify(open), JSON.stringify(frozen), Object.isFrozen(frozen)); + +/** @type {Note} */ +const note = { id: 1, note: "n" }; +Object.freeze(note); +dropNote(note); +console.log(JSON.stringify(note)); + +/** @param {Small | Large} shape */ +function rename(shape) { + shape.name = "renamed"; +} + +/** @param {boolean} large @returns {Small | Large} */ +function shapeOf(large) { + return large ? { kind: "b", name: "b", size: 1 } : { kind: "a", name: "a" }; +} + +const openShape = shapeOf(true); +const frozenShape = shapeOf(false); +Object.freeze(frozenShape); +rename(openShape); +rename(frozenShape); +console.log(openShape.name, frozenShape.name); diff --git a/tests/corpus/7406-frozen-containers.ts b/tests/corpus/7406-frozen-containers.ts new file mode 100644 index 000000000..a8ba8e35c --- /dev/null +++ b/tests/corpus/7406-frozen-containers.ts @@ -0,0 +1,142 @@ +// Object.freeze of arrays, Maps and Sets that other references still hold. +// A frozen array refuses every write through any reference with Node's +// TypeError for the operation's first write (this module is strict mode +// code). A frozen Map or Set only stops being extensible: its entries stay +// writable, and Object.isFrozen, isSealed and isExtensible report it. +export {}; + +function report(e: unknown): void { + console.log((e as Error).name + ": " + (e as Error).message); +} + +function freezeUnknown(value: unknown): unknown { + return Object.freeze(value); +} + +function writeAny(value: any): void { + value[0] = 7; +} + +function pushAny(value: any): void { + value.push(8); +} + +function integrity(value: unknown): string { + return [Object.isFrozen(value), Object.isSealed(value), Object.isExtensible(value)].join(" "); +} + +function arrays(): void { + const a: number[] = [1, 2, 3]; + const alias = a; + console.log(Object.isFrozen(a), integrity(a)); + Object.freeze(alias); + console.log(Object.isFrozen(a), integrity(a)); + try { a[0] = 9; } catch (e) { report(e); } + try { a[5] = 9; } catch (e) { report(e); } + try { a.length = 0; } catch (e) { report(e); } + try { a.push(4); } catch (e) { report(e); } + try { a.pop(); } catch (e) { report(e); } + try { a.shift(); } catch (e) { report(e); } + try { a.unshift(0); } catch (e) { report(e); } + try { a.splice(0, 1); } catch (e) { report(e); } + try { a.splice(1, 0, 7); } catch (e) { report(e); } + try { a.reverse(); } catch (e) { report(e); } + try { a.sort(); } catch (e) { report(e); } + try { a.fill(0, 1); } catch (e) { report(e); } + try { a.copyWithin(1, 0); } catch (e) { report(e); } + console.log(a.join(), a.length); + + const one: string[] = ["x"]; + Object.freeze(one); + console.log(one.reverse().join(), one.sort().join(), one.copyWithin(0, 1).join()); + try { one.splice(0, 1); } catch (e) { report(e); } + try { one.shift(); } catch (e) { report(e); } + const none: string[] = []; + Object.freeze(none); + console.log(none.reverse().length, none.fill("y").length); + try { none.pop(); } catch (e) { report(e); } + try { none.splice(0, 0, "z"); } catch (e) { report(e); } + + let calls = 0; + const byValue = (x: number, y: number): number => { + calls++; + return x - y; + }; + const unsorted: number[] = [3, 1, 2]; + Object.freeze(unsorted); + try { unsorted.sort(byValue); } catch (e) { report(e); } + const single: number[] = [5]; + Object.freeze(single); + console.log(unsorted.join(), calls > 0, single.sort(byValue).join()); + + // A comparator that freezes the array stops the sort's write-back. + const freezing: number[] = [3, 1, 2]; + try { + freezing.sort((x, y) => { + Object.freeze(freezing); + return x - y; + }); + } catch (e) { report(e); } + console.log(freezing.join(), Object.isFrozen(freezing)); + + const open: number[] = [4, 5]; + open.push(6); + open[0] = 1; + console.log(open.join(), Object.isFrozen(open)); +} + +function throughUnknown(): void { + const a: number[] = [1, 2]; + console.log(integrity(a)); + const back = freezeUnknown(a); + console.log(back === a, Object.isFrozen(a), integrity(a)); + try { a.push(3); } catch (e) { report(e); } + try { a[1] = 5; } catch (e) { report(e); } + try { writeAny(a); } catch (e) { report(e); } + try { pushAny(a); } catch (e) { report(e); } + console.log(a.join()); + + const rows: number[][] = [[1], [2]]; + freezeUnknown(rows[1]); + rows[0]!.push(10); + try { rows[1]!.push(20); } catch (e) { report(e); } + console.log(JSON.stringify(rows), Object.isFrozen(rows), Object.isFrozen(rows[1])); +} + +function collections(): void { + const m = new Map([["x", 1]]); + console.log(integrity(m)); + const frozen = Object.freeze(m); + m.set("y", 2); + m.delete("x"); + console.log(frozen === m, Object.isFrozen(m), Object.isSealed(m), Object.isExtensible(m), m.size); + const s = new Set([1]); + freezeUnknown(s); + s.add(2); + console.log(integrity(s), s.size); + const sealed = new Set(); + Object.seal(sealed); + const fresh = new Map(); + Object.preventExtensions(fresh); + console.log(Object.isFrozen(sealed), Object.isExtensible(fresh), integrity(new Map())); +} + +function lifetimes(): void { + const kept: string[][] = []; + for (let round = 0; round < 3; round++) { + const row = ["r" + round, "s" + round]; + if (round !== 1) Object.freeze(row); + kept.push(row); + } + for (const row of kept) { + try { row[0] = "changed"; } catch (e) { console.log(row[1], (e as Error).message); } + } + console.log(JSON.stringify(kept)); + kept.length = 0; + console.log(kept.length); +} + +arrays(); +throughUnknown(); +collections(); +lifetimes(); diff --git a/tests/corpus/7407-frozen-class-instances.ts b/tests/corpus/7407-frozen-class-instances.ts new file mode 100644 index 000000000..6a3a14c54 --- /dev/null +++ b/tests/corpus/7407-frozen-class-instances.ts @@ -0,0 +1,181 @@ +// Object.freeze, Object.seal and Object.preventExtensions of class instances +// held in untyped values. The operation changes the instance itself: writes +// through its typed references, through untyped references and through its +// methods then throw Node's TypeError (this module is strict mode code), and +// Object.isFrozen, isSealed and isExtensible report the state through either +// reference. +export {}; + +function report(e: unknown): void { + console.log((e as Error).name + ": " + (e as Error).message); +} + +function freezeUnknown(value: unknown): unknown { + return Object.freeze(value); +} + +function sealUnknown(value: unknown): unknown { + return Object.seal(value); +} + +function preventUnknown(value: unknown): unknown { + return Object.preventExtensions(value); +} + +function integrity(value: unknown): string { + return [Object.isFrozen(value), Object.isSealed(value), Object.isExtensible(value)].join(" "); +} + +function writeAny(value: any, key: string, next: unknown): void { + value[key] = next; +} + +function deleteAny(value: any, key: string): void { + delete value[key]; +} + +class Point { + x: number; + y: number; + constructor(x: number, y: number) { + this.x = x; + this.y = y; + } + move(dx: number): void { + this.x += dx; + } +} + +class Labeled extends Point { + label: string; + constructor(x: number, y: number, label: string) { + super(x, y); + this.label = label; + } + rename(label: string): void { + this.label = label; + } +} + +class Counter { + count = 0; + tags: string[] = []; + bump(): number { + return ++this.count; + } +} + +function frozen(): void { + const p = new Point(1, 2); + console.log(integrity(p)); + const same = freezeUnknown(p); + console.log(same === p, integrity(p), Object.isFrozen(p)); + try { p.x = 5; } catch (e) { report(e); } + try { p.move(1); } catch (e) { report(e); } + try { writeAny(p, "y", 9); } catch (e) { report(e); } + try { writeAny(p, "z", 1); } catch (e) { report(e); } + console.log(p.x, p.y, JSON.stringify(p)); + + const l = new Labeled(3, 4, "a"); + const base: Point = l; + freezeUnknown(base); + console.log(integrity(l), integrity(base)); + try { l.rename("b"); } catch (e) { report(e); } + try { base.x = 0; } catch (e) { report(e); } + try { writeAny(l, "label", "c"); } catch (e) { report(e); } + console.log(l.x, l.label, l instanceof Labeled); + freezeUnknown(l); + console.log(integrity(l)); +} + +function sealed(): void { + const c = new Counter(); + sealUnknown(c); + console.log(integrity(c)); + console.log(c.bump(), c.bump()); + c.tags.push("kept"); + try { writeAny(c, "extra", 1); } catch (e) { report(e); } + writeAny(c, "count", 10); + console.log(c.count, c.tags.join(), JSON.stringify(c)); + freezeUnknown(c); + console.log(integrity(c)); + try { c.bump(); } catch (e) { report(e); } + c.tags.push("still"); + console.log(c.count, c.tags.join()); +} + +function notExtensible(): void { + const l = new Labeled(0, 0, "n"); + preventUnknown(l); + console.log(integrity(l)); + l.move(2); + l.rename("m"); + try { writeAny(l, "extra", true); } catch (e) { report(e); } + console.log(l.x, l.label, Object.keys(l).join()); + sealUnknown(l); + console.log(integrity(l)); + writeAny(l, "y", 7); + console.log(l.y); +} + +function untouched(): void { + const p = new Point(5, 6); + const q = new Point(7, 8); + freezeUnknown(q); + p.x = 50; + writeAny(p, "y", 60); + console.log(p.x, p.y, integrity(p), integrity(q)); +} + +function added(): void { + const p = new Point(1, 1); + writeAny(p, "extra", 1); + freezeUnknown(p); + console.log(integrity(p), Object.keys(p).join()); + try { deleteAny(p, "extra"); } catch (e) { report(e); } + console.log((p as any).extra, JSON.stringify(p)); +} + +const tag = Symbol("tag"); + +class Tagged { + [tag] = 1; + name = "n"; + bump(): void { + this[tag] += 1; + } +} + +class Subtagged extends Tagged { + extra = 0; +} + +function symbols(): void { + const t = new Tagged(); + freezeUnknown(t); + console.log(integrity(t), Object.getOwnPropertyDescriptor(t, tag)?.writable); + try { t[tag] = 5; } catch (e) { report(e); } + try { t.bump(); } catch (e) { report(e); } + console.log(t[tag]); + + const s = new Subtagged(); + sealUnknown(s); + s[tag] = 3; + console.log(integrity(s), s[tag], Object.getOwnPropertyDescriptor(s, tag)?.configurable); + freezeUnknown(s); + try { s[tag] = 4; } catch (e) { report(e); } + console.log(integrity(s), s[tag]); + + const u = new Tagged(); + Object.freeze(u); + console.log(integrity(u)); + try { u[tag] = 9; } catch (e) { report(e); } + console.log(u[tag]); +} + +frozen(); +sealed(); +notExtensible(); +untouched(); +added(); +symbols(); diff --git a/tests/corpus/7408-frozen-js-class-symbol-fields.js b/tests/corpus/7408-frozen-js-class-symbol-fields.js new file mode 100644 index 000000000..9ce5a1c53 --- /dev/null +++ b/tests/corpus/7408-frozen-js-class-symbol-fields.js @@ -0,0 +1,32 @@ +// A JavaScript class whose instances track which fields they have (one +// field is created by a method) and carry a symbol-keyed field. Freezing +// or sealing such an instance makes its symbol entry read-only; later +// reflection rebuilds the instance's view and takes the field's value. +const tag = Symbol("tag"); +class Box { + constructor(v) { + this.v = v; + this[tag] = v; + } + later() { + this.w = 3; + } +} + +const frozen = new Box(1); +frozen.extra = 2; +Object.freeze(frozen); +console.log(Object.isFrozen(frozen)); +console.log(Object.isFrozen(frozen), Object.isSealed(frozen), Object.isExtensible(frozen)); +console.log(frozen[tag], frozen.v, frozen.extra, frozen.w); +console.log(JSON.stringify(Object.getOwnPropertyDescriptor(frozen, tag))); + +const sealed = new Box(2); +sealed.later(); +Object.seal(sealed); +console.log(Object.isSealed(sealed), Object.isFrozen(sealed), sealed[tag], sealed.w); +console.log(JSON.stringify(Object.getOwnPropertyDescriptor(sealed, tag))); + +const closed = new Box(4); +Object.preventExtensions(closed); +console.log(Object.isExtensible(closed), Object.isSealed(closed), closed[tag]); diff --git a/tests/corpus/7440-tls-server-add-context.ts b/tests/corpus/7440-tls-server-add-context.ts new file mode 100644 index 000000000..3cb29ebb3 --- /dev/null +++ b/tests/corpus/7440-tls-server-add-context.ts @@ -0,0 +1,119 @@ +// tls.Server's addContext and setSecureContext match Node: SNI names +// matched against exact and wildcard patterns (a "*" covers one label, +// case-insensitively, the newest match winning), the default context when +// nothing matches or an SNICallback is set, setSecureContext replacing the +// default context, the argument errors, the chain each context sends +// (completed from the default context's ca), and the store that verifies +// client certificates after each change. +import { readFileSync } from "node:fs"; +import type { AddressInfo } from "node:net"; +import * as tls from "node:tls"; + +const D = "tests/fixtures/server/certs"; +const r = (f: string): Buffer => readFileSync(`${D}/${f}`); +const ca = r("ca.pem"), ca2 = r("ca2.pem"), cert = r("localhost.pem"), key = r("localhost-key.pem"); +const alt = r("alt.pem"), altKey = r("alt-key.pem"), self = r("selfsigned.pem"), selfKey = r("selfsigned-key.pem"); +const client = r("client.pem"), clientKey = r("client-key.pem"); + +const listen = (server: tls.Server): Promise => + new Promise((d) => server.listen(0, () => d((server.address() as AddressInfo).port))); + +function peer(port: number, servername: string): Promise { + return new Promise((res) => { + const c = tls.connect({ port, servername, rejectUnauthorized: false }, () => { + const p = c.getPeerCertificate(); + res(`${p.subject.CN}${p.subject.CN === "localhost" && p.issuer.CN === "localhost" ? "(self)" : ""}`); + c.destroy(); + }); + c.on("error", (e: NodeJS.ErrnoException) => res(`error ${e.code}`)); + }); +} + +function chain(c: tls.TLSSocket): string { + const names: string[] = []; + let p: tls.DetailedPeerCertificate | undefined = c.getPeerCertificate(true); + for (let i = 0; p !== undefined && i < 4; i++) { + names.push(p.subject.CN as string); + if (p.issuerCertificate === p) break; + p = p.issuerCertificate; + } + return names.join(" -> "); +} + +function look(port: number, servername: string): Promise { + return new Promise((res) => { + const c = tls.connect({ port, servername, ca: ca2, rejectUnauthorized: false }, () => { + const line = `${c.authorizationError} ${chain(c)}`; + c.destroy(); + res(line); + }); + }); +} + +function attempt(label: string, run: () => void): void { + try { + run(); + console.log(label, "ok"); + } catch (e) { + const err = e as NodeJS.ErrnoException; + console.log(label, err.name, err.code, err.message.split("\n")[0]); + } +} + +{ + const server = tls.createServer({ cert, key }); + server.addContext("alt.localhost", { cert: alt, key: altKey }); + server.addContext("*.wild.test", tls.createSecureContext({ cert: self, key: selfKey })); + server.addContext("a.b.wild.test", { cert: alt, key: altKey }); + const port = await listen(server); + const names = ["alt.localhost", "ALT.localhost", "x.wild.test", "a.b.wild.test", "c.b.wild.test", "wild.test", ".wild.test", "other", "alt.localhostx"]; + for (const name of names) console.log("sni", name, await peer(port, name)); + attempt("addContext empty", () => server.addContext("", { cert, key })); + attempt("addContext string ctx", () => server.addContext("x.test", "nope" as unknown as tls.SecureContextOptions)); + server.addContext("alt.localhost", { cert: self, key: selfKey }); + console.log("override", await peer(port, "alt.localhost")); + server.setSecureContext({ cert: alt, key: altKey }); + console.log("setSecureContext default", await peer(port, "nomatch"), await peer(port, "alt.localhost")); + attempt("setSecureContext string", () => server.setSecureContext("x" as unknown as tls.SecureContextOptions)); + server.close(); +} + +{ + const server = tls.createServer({ cert, key, SNICallback: (n: string, cb: (err: Error | null, ctx?: tls.SecureContext) => void) => cb(null, undefined) }); + server.addContext("alt.localhost", { cert: alt, key: altKey }); + console.log("with SNICallback", await peer(await listen(server), "alt.localhost")); + server.close(); +} + +{ + const server = tls.createServer({ cert, key, ca }); + server.addContext("alt.localhost", { cert: alt, key: altKey }); + const port = await listen(server); + console.log("server ca, sni", await look(port, "alt.localhost")); + server.setSecureContext({ cert, key }); + console.log("after set without ca, sni", await look(port, "alt.localhost")); + console.log("after set without ca, default", await look(port, "localhost")); + server.close(); +} + +{ + const lines: string[] = []; + const server = tls.createServer({ cert, key, ca, requestCert: true, rejectUnauthorized: false }, (s) => { + lines.push(`server ${s.authorized} ${s.authorizationError}`); + s.end(); + }); + const port = await listen(server); + const once = (servername: string): Promise => + new Promise((res) => { + const c = tls.connect({ port, servername, ca, cert: client, key: clientKey }); + c.on("data", () => {}); + c.on("close", () => res()); + }); + await once("localhost"); + server.setSecureContext({ cert, key, ca: ca2 }); + await once("localhost"); + server.addContext("alt.localhost", { cert: alt, key: altKey, ca }); + await once("alt.localhost"); + console.log(lines.join(" | ")); + server.close(); +} diff --git a/tests/corpus/7441-tls-client-error.ts b/tests/corpus/7441-tls-client-error.ts new file mode 100644 index 000000000..431ea9019 --- /dev/null +++ b/tests/corpus/7441-tls-client-error.ts @@ -0,0 +1,93 @@ +// tls.Server's 'tlsClientError' event matches Node: the error a failed +// handshake reports (OpenSSL's codes and messages for an HTTP or proxy +// request, a client that rejects the certificate, a missing required +// certificate, an unsupported version and no common ALPN protocol; a +// "socket hang up" ECONNRESET for a client that leaves), the socket it +// passes, what the client sees, and the same event on https.Server. +import * as https from "node:https"; +import * as net from "node:net"; +import type { AddressInfo } from "node:net"; +import { readFileSync } from "node:fs"; +import * as tls from "node:tls"; + +const D = "tests/fixtures/server/certs"; +const r = (f: string): Buffer => readFileSync(`${D}/${f}`); +const ca = r("ca.pem"), ca2 = r("ca2.pem"), cert = r("localhost.pem"), key = r("localhost-key.pem"); +const clean = (m: string): string => m.replace(/^[0-9A-F]+:/, "").split(":../")[0]!; + +type Drive = (port: number, client: string[], fin: () => void) => void; + +async function scenario(label: string, make: (h: (s: tls.TLSSocket) => void) => tls.Server, drive: Drive): Promise { + await new Promise((done) => { + const events: string[] = []; + const client: string[] = []; + const server = make((s) => { + events.push("secureConnection"); + s.end("ok"); + }); + server.on("tlsClientError", (err: Error, socket: tls.TLSSocket) => { + events.push(`tlsClientError ${err.name} ${(err as NodeJS.ErrnoException).code} [${clean(err.message)}] destroyed=${socket.destroyed} remote=${typeof socket.remoteAddress}`); + socket.on("close", () => events.push("server socket close")); + }); + server.listen(0, () => { + drive((server.address() as AddressInfo).port, client, () => + setTimeout(() => { + console.log(label, "|", events.join(" ; "), "| client:", client.join(" ; ")); + server.close(() => done()); + }, 100), + ); + }); + }); +} + +const raw = (bytes: Buffer | null, endFirst: boolean): Drive => (port, client, fin) => { + const s = net.connect(port, "127.0.0.1", () => { + if (bytes !== null) s.write(bytes); + if (endFirst) s.end(); + }); + s.on("data", (d: Buffer) => client.push(`data ${d.length}`)); + s.on("end", () => client.push("end")); + s.on("error", (e: NodeJS.ErrnoException) => client.push(`error ${e.code}`)); + s.on("close", (hadError: boolean) => { + client.push(`close ${hadError}`); + fin(); + }); +}; + +const tlsc = (trust: Buffer, maxVersion: "TLSv1.2" | "TLSv1.3", alpn: boolean): Drive => (port, client, fin) => { + const c = alpn + ? tls.connect({ port, servername: "localhost", ca: trust, maxVersion, ALPNProtocols: ["http/1.1"] }) + : tls.connect({ port, servername: "localhost", ca: trust, maxVersion }); + c.on("secureConnect", () => client.push("secureConnect")); + c.on("error", (e: NodeJS.ErrnoException) => client.push(`error ${e.code}`)); + c.on("data", () => {}); + c.on("close", (hadError: boolean) => { + client.push(`close ${hadError}`); + fin(); + }); +}; + +const plain = (h: (s: tls.TLSSocket) => void): tls.Server => tls.createServer({ cert, key }, h); +await scenario("close before hello", plain, raw(null, true)); +await scenario("http request", plain, raw(Buffer.from("GET / HTTP/1.1\r\nHost: x\r\n\r\n"), false)); +await scenario("connect request", plain, raw(Buffer.from("CONNECT x:443 HTTP/1.1\r\n\r\n"), false)); +await scenario("half hello", plain, raw(Buffer.from([0x16, 0x03, 0x01, 0x02, 0x00, 0x01]), true)); +await scenario("client rejects cert", plain, tlsc(ca2, "TLSv1.3", false)); +await scenario("no cert, required", (h) => tls.createServer({ cert, key, requestCert: true, ca }, h), tlsc(ca, "TLSv1.3", false)); +await scenario("version", (h) => tls.createServer({ cert, key, minVersion: "TLSv1.3" }, h), tlsc(ca, "TLSv1.2", false)); +await scenario("alpn", (h) => tls.createServer({ cert, key, ALPNProtocols: ["h2"] }, h), tlsc(ca, "TLSv1.3", true)); +await scenario("ok", plain, tlsc(ca, "TLSv1.3", false)); + +{ + const server = https.createServer({ cert, key }, (req, res) => res.end("ok")); + server.on("tlsClientError", (err: Error, socket: tls.TLSSocket) => console.log("https tlsClientError", (err as NodeJS.ErrnoException).code, socket.destroyed)); + await new Promise((d) => server.listen(0, () => d())); + await new Promise((res) => { + const s = net.connect((server.address() as AddressInfo).port, "127.0.0.1", () => s.write("GET / HTTP/1.1\r\nHost: x\r\n\r\n")); + s.on("data", () => {}); + s.on("error", (e: NodeJS.ErrnoException) => console.log("https client error", e.code)); + s.on("close", () => res()); + }); + await new Promise((d) => setTimeout(d, 50)); + server.close(); +} diff --git a/tests/corpus/7442-tls-server-contexts.mjs b/tests/corpus/7442-tls-server-contexts.mjs new file mode 100644 index 000000000..97f797129 --- /dev/null +++ b/tests/corpus/7442-tls-server-contexts.mjs @@ -0,0 +1,51 @@ +// tls.Server's addContext, setSecureContext and 'tlsClientError' from +// JavaScript match Node: a wildcard SNI context, the default context +// replaced, and the errors a plain-text client and one that leaves +// before its hello report. +import * as net from "node:net"; +import * as tls from "node:tls"; +import { readFileSync } from "node:fs"; + +const D = "tests/fixtures/server/certs"; +const cert = readFileSync(`${D}/localhost.pem`); +const key = readFileSync(`${D}/localhost-key.pem`); +const alt = readFileSync(`${D}/alt.pem`); +const altKey = readFileSync(`${D}/alt-key.pem`); + +const server = tls.createServer({ cert, key }, (socket) => socket.end()); +server.addContext("*.localhost", { cert: alt, key: altKey }); +server.on("tlsClientError", (err, socket) => { + const reason = err.message.replace(/^[0-9A-F]+:/, "").split(":../")[0]; + console.log("tlsClientError", err.code, reason, socket.destroyed); +}); + +function peer(port, servername) { + return new Promise((resolve) => { + const c = tls.connect({ port, servername, rejectUnauthorized: false }, () => { + resolve(c.getPeerCertificate().subject.CN); + c.end(); + }); + }); +} + +function raw(port, bytes) { + return new Promise((resolve) => { + const s = net.connect(port, "127.0.0.1", () => { + if (bytes) s.write(bytes); + else s.end(); + }); + s.on("data", () => {}); + s.on("error", (err) => console.log("client error", err.code)); + s.on("close", () => setTimeout(resolve, 50)); + }); +} + +server.listen(0, async () => { + const { port } = server.address(); + console.log("sni", await peer(port, "alt.localhost"), await peer(port, "localhost")); + server.setSecureContext({ cert: alt, key: altKey }); + console.log("after setSecureContext", await peer(port, "example.test")); + await raw(port, "GET / HTTP/1.1\r\nHost: x\r\n\r\n"); + await raw(port, null); + server.close(); +}); diff --git a/tests/corpus/7443-tls-final-flight.ts b/tests/corpus/7443-tls-final-flight.ts new file mode 100644 index 000000000..b4de4b4d4 --- /dev/null +++ b/tests/corpus/7443-tls-final-flight.ts @@ -0,0 +1,114 @@ +// The last handshake flight is flushed when Node flushes it: after the +// callbacks that see the handshake finish and their nextTick callbacks, +// before the immediates they queue. A destroy() in secureConnect or a tick +// drops a TLS 1.3 client's Finished (the server reports a hang-up), one in +// secureConnection drops a TLS 1.2 server's Finished (the client ends, then +// fails), and data written beside it is dropped too; end() and a destroy +// in an immediate let it go out. A client whose server closes or resets +// mid-handshake gets Node's events, from tls.connect and https.request. +import { readFileSync } from "node:fs"; +import * as https from "node:https"; +import * as net from "node:net"; +import type { AddressInfo } from "node:net"; +import * as tls from "node:tls"; + +const D = "tests/fixtures/server/certs"; +const cert = readFileSync(`${D}/localhost.pem`), key = readFileSync(`${D}/localhost-key.pem`); +type Version = "TLSv1.2" | "TLSv1.3"; +const hows = ["destroy", "tick", "immediate", "write+destroy", "end"]; + +function act(s: tls.TLSSocket, how: string): void { + if (how === "destroy") s.destroy(); + else if (how === "tick") process.nextTick(() => s.destroy()); + else if (how === "immediate") setImmediate(() => s.destroy()); + else if (how === "write+destroy") { + s.write("hi"); + s.destroy(); + } else s.end("bye"); +} + +const listen = (server: net.Server): Promise => + new Promise((d) => server.listen(0, "127.0.0.1", () => d((server.address() as AddressInfo).port))); + +// the client acts in secureConnect; the server reports what arrived +for (const v of ["TLSv1.2", "TLSv1.3"] as Version[]) { + for (const how of hows) { + const ev: string[] = []; + const server = tls.createServer({ cert, key, maxVersion: v }, (s) => { + ev.push("secureConnection"); + s.on("data", (d: Buffer) => ev.push(`data ${d}`)); + // A client that closes before it reads the server's TLS 1.3 session + // tickets resets the connection. Whether the tickets are read first + // depends on the platform's timing, so that reset is not printed. + s.on("error", (e: NodeJS.ErrnoException) => { + if (e.code !== "ECONNRESET") ev.push(`server socket error ${e.code}`); + }); + }); + server.on("tlsClientError", (e: Error) => ev.push(`tlsClientError ${(e as NodeJS.ErrnoException).code}`)); + const port = await listen(server); + await new Promise((res) => { + const c = tls.connect({ port, host: "127.0.0.1", rejectUnauthorized: false }, () => act(c, how)); + c.on("close", () => setTimeout(res, 100)); + }); + console.log("client", v, how, "|", ev.join(" ; ")); + server.close(); + } +} + +// the server acts in secureConnection; the client reports what arrived +for (const v of ["TLSv1.2", "TLSv1.3"] as Version[]) { + for (const how of hows) { + const ev: string[] = []; + const server = tls.createServer({ cert, key, maxVersion: v }, (s) => { + s.on("error", () => {}); + act(s, how); + }); + const port = await listen(server); + await new Promise((res) => { + const c = tls.connect({ port, host: "127.0.0.1", rejectUnauthorized: false }, () => ev.push("secureConnect")); + c.on("data", (d: Buffer) => ev.push(`data ${d}`)); + c.on("end", () => ev.push("end")); + c.on("error", (e: NodeJS.ErrnoException) => ev.push(`error ${e.code} ${e.message}`)); + c.on("close", (hadError: boolean) => { + ev.push(`close ${hadError}`); + setTimeout(res, 50); + }); + }); + console.log("server", v, how, "|", ev.join(" ; ")); + server.close(); + } +} + +// a peer that closes or resets after the client's hello +for (const how of ["fin", "rst"]) { + for (const via of ["tls.connect", "https.request"]) { + const ev: string[] = []; + const server = net.createServer((s) => { + s.once("data", () => { + if (how === "rst") s.resetAndDestroy(); + else s.end(); + }); + }); + const port = await listen(server); + await new Promise((res) => { + if (via === "tls.connect") { + const c = tls.connect({ port, host: "127.0.0.1", rejectUnauthorized: false }); + c.on("end", () => ev.push("end")); + c.on("error", (e: NodeJS.ErrnoException) => ev.push(`error ${e.code} ${e.message}`)); + c.on("close", (hadError: boolean) => { + ev.push(`close ${hadError}`); + res(); + }); + } else { + const req = https.request({ host: "127.0.0.1", port, path: "/", rejectUnauthorized: false, agent: false }); + req.on("error", (e: NodeJS.ErrnoException) => { + ev.push(`request error ${e.code} ${e.message}`); + res(); + }); + req.end(); + } + }); + console.log("peer", how, via, "|", ev.join(" ; ")); + server.close(); + } +} diff --git a/tests/corpus/7444-tls-final-flight.mjs b/tests/corpus/7444-tls-final-flight.mjs new file mode 100644 index 000000000..e21de782c --- /dev/null +++ b/tests/corpus/7444-tls-final-flight.mjs @@ -0,0 +1,28 @@ +// The last handshake flight from JavaScript: a TLS 1.3 client that +// destroys its socket in secureConnect or a tick never sends its Finished, +// so the server reports a hang-up; one that waits for an immediate, or +// ends the socket, completes the server's handshake. +import * as tls from "node:tls"; +import { readFileSync } from "node:fs"; + +const D = "tests/fixtures/server/certs"; +const cert = readFileSync(`${D}/localhost.pem`); +const key = readFileSync(`${D}/localhost-key.pem`); + +for (const how of ["destroy", "tick", "immediate", "end"]) { + const events = []; + const server = tls.createServer({ cert, key }, () => events.push("secureConnection")); + server.on("tlsClientError", (err) => events.push(`tlsClientError ${err.code}`)); + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + await new Promise((resolve) => { + const client = tls.connect({ port: server.address().port, host: "127.0.0.1", rejectUnauthorized: false }, () => { + if (how === "destroy") client.destroy(); + else if (how === "tick") process.nextTick(() => client.destroy()); + else if (how === "immediate") setImmediate(() => client.destroy()); + else client.end(); + }); + client.on("close", () => setTimeout(resolve, 100)); + }); + console.log(how, events.join(" ; ")); + server.close(); +} diff --git a/tests/corpus/7449-crypto-raw-keys.mjs b/tests/corpus/7449-crypto-raw-keys.mjs new file mode 100644 index 000000000..0795dbb0b --- /dev/null +++ b/tests/corpus/7449-crypto-raw-keys.mjs @@ -0,0 +1,120 @@ +// The raw-public, raw-private and raw-seed key formats: generateKeyPair +// encodings, KeyObject.export and createPublicKey/createPrivateKey with +// asymmetricKeyType and namedCurve, for EC keys (uncompressed and +// compressed points, scalars at the curve's size) and Ed25519 keys, and +// Node's errors for the other key types and formats. +import * as crypto from "node:crypto"; + +function describe(value) { + if (Buffer.isBuffer(value)) return `Buffer(${value.length})`; + if (value instanceof crypto.KeyObject) return `${value.type} ${value.asymmetricKeyType} ${JSON.stringify(value.asymmetricKeyDetails)}`; + if (value && typeof value === "object") return Object.entries(value).map(([key, field]) => `${key}=${describe(field)}`).join(" "); + return String(value); +} + +function attempt(label, run) { + try { + console.log(label, describe(run())); + } catch (e) { + console.log(label, "throws", e.name, e.code, e.message); + } +} + +// generateKeyPair encodings +for (const namedCurve of ["P-256", "P-384", "P-521", "secp256k1"]) { + attempt(`ec ${namedCurve} raw-public`, () => crypto.generateKeyPairSync("ec", { namedCurve, publicKeyEncoding: { format: "raw-public" } })); + attempt(`ec ${namedCurve} compressed`, () => crypto.generateKeyPairSync("ec", { namedCurve, publicKeyEncoding: { format: "raw-public", type: "compressed" } })); + attempt(`ec ${namedCurve} raw-private`, () => crypto.generateKeyPairSync("ec", { namedCurve, privateKeyEncoding: { format: "raw-private" } })); +} +attempt("ec raw-seed", () => crypto.generateKeyPairSync("ec", { namedCurve: "P-256", privateKeyEncoding: { format: "raw-seed" } })); +attempt("ed25519 raw", () => + crypto.generateKeyPairSync("ed25519", { publicKeyEncoding: { format: "raw-public" }, privateKeyEncoding: { format: "raw-private" } }), +); +attempt("ed25519 raw-seed", () => crypto.generateKeyPairSync("ed25519", { privateKeyEncoding: { format: "raw-seed" } })); +attempt("rsa raw-public", () => crypto.generateKeyPairSync("rsa", { modulusLength: 1024, publicKeyEncoding: { format: "raw-public" } })); + +// export +const ec = crypto.generateKeyPairSync("ec", { namedCurve: "P-256" }); +const ed = crypto.generateKeyPairSync("ed25519"); +const rsa = crypto.generateKeyPairSync("rsa", { modulusLength: 1024 }); +const point = Buffer.from(ec.publicKey.export({ format: "raw-public" })); +const compressed = Buffer.from(ec.publicKey.export({ format: "raw-public", type: "compressed" })); +const scalar = Buffer.from(ec.privateKey.export({ format: "raw-private" })); +const jwk = ec.privateKey.export({ format: "jwk" }); +console.log("ec point", point.length, point[0], point.subarray(1, 33).toString("base64url") === jwk.x, point.subarray(33).toString("base64url") === jwk.y); +console.log("ec compressed", compressed.length, compressed[0] === 2 + (point[64] & 1), compressed.subarray(1).equals(point.subarray(1, 33))); +console.log("ec scalar", scalar.length, scalar.toString("base64url") === jwk.d); +console.log("ec uncompressed", Buffer.from(ec.publicKey.export({ format: "raw-public", type: "uncompressed" })).equals(point)); +const edPublic = Buffer.from(ed.publicKey.export({ format: "raw-public" })); +const edSeed = Buffer.from(ed.privateKey.export({ format: "raw-private" })); +const edJwk = ed.privateKey.export({ format: "jwk" }); +console.log("ed25519", edPublic.toString("base64url") === edJwk.x, edSeed.toString("base64url") === edJwk.d); +attempt("ed25519 raw-public with a type", () => ed.publicKey.export({ format: "raw-public", type: "x" })); +attempt("ec bad point type", () => ec.publicKey.export({ format: "raw-public", type: "x" })); +attempt("ec null point type", () => ec.publicKey.export({ format: "raw-public", type: null })); +attempt("private raw-public", () => ec.privateKey.export({ format: "raw-public" })); +attempt("public raw-private", () => ec.publicKey.export({ format: "raw-private" })); +attempt("ec raw-seed", () => ec.privateKey.export({ format: "raw-seed" })); +attempt("ed25519 raw-seed", () => ed.privateKey.export({ format: "raw-seed" })); +attempt("rsa raw-public", () => rsa.publicKey.export({ format: "raw-public" })); +attempt("rsa raw-private", () => rsa.privateKey.export({ format: "raw-private" })); +for (const format of [undefined, 5, "jwk", "raw-private", "raw-seed"]) + attempt(`passphrase with ${format}`, () => ec.privateKey.export({ format, passphrase: "x" })); + +// import +const pub = (key) => () => crypto.createPublicKey(key); +const priv = (key) => () => crypto.createPrivateKey(key); +attempt("ec point", pub({ key: point, format: "raw-public", asymmetricKeyType: "ec", namedCurve: "P-256" })); +attempt("ec compressed point", pub({ key: compressed, format: "raw-public", asymmetricKeyType: "ec", namedCurve: "prime256v1" })); +attempt("ec scalar", priv({ key: scalar, format: "raw-private", asymmetricKeyType: "ec", namedCurve: "P-256" })); +attempt("ec scalar as public", pub({ key: scalar, format: "raw-private", asymmetricKeyType: "ec", namedCurve: "P-256" })); +const reread = crypto.createPublicKey({ key: compressed, format: "raw-public", asymmetricKeyType: "ec", namedCurve: "P-256" }); +console.log("ec reread", reread.equals(ec.publicKey), Buffer.from(reread.export({ format: "raw-public" })).equals(point)); +const rereadPrivate = crypto.createPrivateKey({ key: scalar, format: "raw-private", asymmetricKeyType: "ec", namedCurve: "P-256" }); +console.log("ec private reread", crypto.createPublicKey(rereadPrivate).equals(ec.publicKey)); +attempt("ed25519 public", pub({ key: edPublic, format: "raw-public", asymmetricKeyType: "ed25519" })); +attempt("ed25519 seed", priv({ key: edSeed, format: "raw-private", asymmetricKeyType: "ed25519" })); +console.log("ed25519 reread", crypto.createPublicKey({ key: edSeed, format: "raw-private", asymmetricKeyType: "ed25519" }).equals(ed.publicKey)); + +// fixed keys: the EC scalar 1 is the generator, and an Ed25519 seed of +// zeros has a known public key +const one = Buffer.concat([Buffer.alloc(31), Buffer.from([1])]); +const generator = crypto.createPrivateKey({ key: one, format: "raw-private", asymmetricKeyType: "ec", namedCurve: "P-256" }); +console.log("generator", Buffer.from(crypto.createPublicKey(generator).export({ format: "raw-public", type: "compressed" })).toString("hex")); +const zeros = crypto.createPrivateKey({ key: Buffer.alloc(32), format: "raw-private", asymmetricKeyType: "ed25519" }); +console.log("zero seed", Buffer.from(crypto.createPublicKey(zeros).export({ format: "raw-public" })).toString("hex")); +const message = Buffer.from("raw keys"); +console.log("zero seed signature", crypto.sign(null, message, zeros).toString("hex").slice(0, 32)); + +// import errors +attempt("raw-public to createPrivateKey", priv({ key: point, format: "raw-public", asymmetricKeyType: "ec", namedCurve: "P-256" })); +attempt("string data", pub({ key: "abc", format: "raw-public", asymmetricKeyType: "ec", namedCurve: "P-256" })); +attempt("no asymmetricKeyType", pub({ key: point, format: "raw-public" })); +attempt("number asymmetricKeyType", pub({ key: point, format: "raw-public", asymmetricKeyType: 5 })); +attempt("no namedCurve", pub({ key: point, format: "raw-public", asymmetricKeyType: "ec" })); +attempt("unknown asymmetricKeyType", pub({ key: point, format: "raw-public", asymmetricKeyType: "nope" })); +attempt("Ed25519 spelling", pub({ key: edPublic, format: "raw-public", asymmetricKeyType: "Ed25519" })); +attempt("unknown curve", pub({ key: point, format: "raw-public", asymmetricKeyType: "ec", namedCurve: "nope" })); +attempt("lower-case NIST curve", pub({ key: point, format: "raw-public", asymmetricKeyType: "ec", namedCurve: "p-256" })); +attempt("other curve", pub({ key: point, format: "raw-public", asymmetricKeyType: "ec", namedCurve: "P-384" })); +attempt("short point", pub({ key: point.subarray(0, 64), format: "raw-public", asymmetricKeyType: "ec", namedCurve: "P-256" })); +attempt("off-curve point", pub({ key: Buffer.concat([Buffer.from([4]), Buffer.alloc(64, 1)]), format: "raw-public", asymmetricKeyType: "ec", namedCurve: "P-256" })); +attempt("bad compressed x", pub({ key: Buffer.concat([Buffer.from([2]), Buffer.alloc(32, 0xff)]), format: "raw-public", asymmetricKeyType: "ec", namedCurve: "P-256" })); +attempt("short scalar", priv({ key: scalar.subarray(0, 31), format: "raw-private", asymmetricKeyType: "ec", namedCurve: "P-256" })); +attempt("long scalar", priv({ key: Buffer.concat([Buffer.alloc(1), scalar]), format: "raw-private", asymmetricKeyType: "ec", namedCurve: "P-256" })); +attempt("ec raw-seed", priv({ key: scalar, format: "raw-seed", asymmetricKeyType: "ec", namedCurve: "nope" })); +attempt("ed25519 raw-seed", priv({ key: edSeed, format: "raw-seed", asymmetricKeyType: "ed25519" })); +attempt("short ed25519 key", pub({ key: edPublic.subarray(0, 31), format: "raw-public", asymmetricKeyType: "ed25519" })); +attempt("short ed25519 seed", priv({ key: edSeed.subarray(0, 31), format: "raw-private", asymmetricKeyType: "ed25519" })); +for (const asymmetricKeyType of ["rsa", "rsa-pss", "dsa", "dh"]) + attempt(`${asymmetricKeyType} raw`, pub({ key: edPublic, format: "raw-public", asymmetricKeyType })); + +// generateKeyPair passes an encoding the key type has no form for to its +// callback +crypto.generateKeyPair("ed25519", { privateKeyEncoding: { format: "raw-seed" } }, (...args) => { + console.log("async raw-seed", args.length, args[0].code); + crypto.generateKeyPair("ec", { namedCurve: "P-384", publicKeyEncoding: { format: "raw-public", type: "compressed" } }, (err, publicKey, privateKey) => { + console.log("async raw-public", err, describe(publicKey), describe(privateKey)); + }); +}); +console.log("returned"); diff --git a/tests/corpus/7450-crypto-x25519.mjs b/tests/corpus/7450-crypto-x25519.mjs new file mode 100644 index 000000000..180221390 --- /dev/null +++ b/tests/corpus/7450-crypto-x25519.mjs @@ -0,0 +1,90 @@ +// X25519 KeyObjects: generateKeyPair, PKCS#8, SubjectPublicKeyInfo, JWK +// and raw keys, RFC 7748's test vectors through crypto.diffieHellman, and +// Node's errors for signatures and incompatible encodings. +import * as crypto from "node:crypto"; + +function describe(value) { + if (Buffer.isBuffer(value)) return `Buffer(${value.length})`; + if (value instanceof crypto.KeyObject) return `${value.type} ${value.asymmetricKeyType} ${JSON.stringify(value.asymmetricKeyDetails)}`; + if (typeof value === "string") return value.split("\n")[0]; + if (value && typeof value === "object") return Object.entries(value).map(([key, field]) => `${key}=${describe(field)}`).join(" "); + return String(value); +} + +function attempt(label, run) { + try { + console.log(label, describe(run())); + } catch (e) { + console.log(label, "throws", e.name, e.code, e.message); + } +} + +// RFC 7748, section 6.1 +const alicePrivate = Buffer.from("77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a", "hex"); +const alicePublic = "8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a"; +const bobPrivate = Buffer.from("5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb", "hex"); +const bobPublic = "de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f"; +const alice = crypto.createPrivateKey({ key: alicePrivate, format: "raw-private", asymmetricKeyType: "x25519" }); +const bob = crypto.createPrivateKey({ key: bobPrivate, format: "raw-private", asymmetricKeyType: "x25519" }); +console.log("alice public", Buffer.from(crypto.createPublicKey(alice).export({ format: "raw-public" })).toString("hex") === alicePublic); +console.log("bob public", Buffer.from(crypto.createPublicKey(bob).export({ format: "raw-public" })).toString("hex") === bobPublic); +const shared = crypto.diffieHellman({ privateKey: alice, publicKey: crypto.createPublicKey(bob) }); +console.log("shared", shared.toString("hex")); +console.log("both ways", crypto.diffieHellman({ privateKey: bob, publicKey: crypto.createPublicKey(alice) }).equals(shared)); +console.log("raw-private kept as given", Buffer.from(alice.export({ format: "raw-private" })).equals(alicePrivate)); + +// encodings +console.log("details", describe(alice), describe(crypto.createPublicKey(alice))); +const spki = alice.export({ format: "der", type: "pkcs8" }); +console.log("pkcs8", spki.length, spki.subarray(0, 16).toString("hex")); +const pub = crypto.createPublicKey(alice); +const spkiDer = pub.export({ format: "der", type: "spki" }); +console.log("spki", spkiDer.length, spkiDer.subarray(0, 12).toString("hex")); +console.log("pem", describe(alice.export({ format: "pem", type: "pkcs8" })), describe(pub.export({ format: "pem", type: "spki" }))); +const jwk = alice.export({ format: "jwk" }); +console.log("jwk", Object.keys(jwk).join(), jwk.kty, jwk.crv, jwk.x === Buffer.from(alicePublic, "hex").toString("base64url"), jwk.d === alicePrivate.toString("base64url")); +console.log("public jwk", JSON.stringify(pub.export({ format: "jwk" }))); +const fromPem = crypto.createPrivateKey(alice.export({ format: "pem", type: "pkcs8" })); +console.log("pem round trip", fromPem.equals(alice), crypto.createPublicKey(pub.export({ format: "pem", type: "spki" })).equals(pub)); +console.log("jwk round trip", crypto.createPrivateKey({ key: jwk, format: "jwk" }).equals(alice), crypto.createPublicKey({ key: pub.export({ format: "jwk" }), format: "jwk" }).equals(pub)); +console.log("public of pkcs8", crypto.createPublicKey({ key: spki, format: "der", type: "pkcs8" }).equals(pub)); +console.log("not ed25519", alice.equals(crypto.generateKeyPairSync("ed25519").privateKey)); + +// generateKeyPair +const pair = crypto.generateKeyPairSync("x25519"); +console.log("generated", describe(pair)); +const clamped = Buffer.from(pair.privateKey.export({ format: "raw-private" })); +console.log("generated clamped", (clamped[0] & 7) === 0, (clamped[31] & 0xc0) === 0x40); +attempt("pem pair", () => + crypto.generateKeyPairSync("x25519", { + publicKeyEncoding: { type: "spki", format: "pem" }, + privateKeyEncoding: { type: "pkcs8", format: "der" }, + }), +); +attempt("raw pair", () => + crypto.generateKeyPairSync("x25519", { publicKeyEncoding: { format: "raw-public" }, privateKeyEncoding: { format: "raw-private" } }), +); +attempt("pkcs1", () => crypto.generateKeyPairSync("x25519", { publicKeyEncoding: { type: "pkcs1", format: "pem" } })); +attempt("sec1", () => crypto.generateKeyPairSync("x25519", { privateKeyEncoding: { type: "sec1", format: "pem" } })); +attempt("raw-seed", () => crypto.generateKeyPairSync("x25519", { privateKeyEncoding: { format: "raw-seed" } })); +attempt("X25519 spelling", () => crypto.generateKeyPairSync("X25519")); + +// what X25519 keys cannot do +const data = Buffer.from("x25519"); +attempt("sign", () => crypto.sign(null, data, alice)); +attempt("sign sha256", () => crypto.sign("sha256", data, alice)); +attempt("verify", () => crypto.verify(null, data, pub, Buffer.alloc(64))); +attempt("createSign", () => crypto.createSign("sha256").update(data).sign(alice)); +attempt("createVerify", () => crypto.createVerify("sha256").update(data).verify(pub, Buffer.alloc(64))); +attempt("hmac", () => crypto.createHmac("sha256", alice)); +attempt("export pkcs1", () => pub.export({ format: "pem", type: "pkcs1" })); +attempt("export raw-seed", () => alice.export({ format: "raw-seed" })); +attempt("short raw key", () => crypto.createPublicKey({ key: Buffer.alloc(31), format: "raw-public", asymmetricKeyType: "x25519" })); + +crypto.generateKeyPair("x25519", (err, publicKey, privateKey) => { + console.log("async", err, describe(publicKey), describe(privateKey)); + crypto.diffieHellman({ privateKey, publicKey: crypto.createPublicKey(bob) }, (err2, secret) => { + console.log("async secret", err2, describe(secret)); + }); +}); +console.log("returned"); diff --git a/tests/corpus/7451-crypto-diffie-hellman.ts b/tests/corpus/7451-crypto-diffie-hellman.ts new file mode 100644 index 000000000..9648cd32b --- /dev/null +++ b/tests/corpus/7451-crypto-diffie-hellman.ts @@ -0,0 +1,55 @@ +// crypto.diffieHellman with typed KeyObjects: ECDH on every supported +// curve, checked against createECDH, X25519, the callback form, and +// Node's errors for incompatible key types, mismatched curves and a +// small-order X25519 public key. +import * as crypto from "node:crypto"; + +function attempt(label: string, run: () => Buffer): void { + try { + const secret = run(); + console.log(label, secret.length); + } catch (e) { + const error = e as NodeJS.ErrnoException; + console.log(label, "throws", error.name, error.code, error.message); + } +} + +for (const namedCurve of ["prime256v1", "secp384r1", "secp521r1", "secp256k1"]) { + const a = crypto.generateKeyPairSync("ec", { namedCurve }); + const ecdh = crypto.createECDH(namedCurve); + const ecdhPublic = ecdh.generateKeys(); + const peer = crypto.createPublicKey({ key: ecdhPublic, format: "raw-public", asymmetricKeyType: "ec", namedCurve } as unknown as crypto.PublicKeyInput); + const secret = crypto.diffieHellman({ privateKey: a.privateKey, publicKey: peer }); + // The SubjectPublicKeyInfo ends with the uncompressed point. + const spki = a.publicKey.export({ format: "der", type: "spki" }); + const own = spki.subarray(spki.length - ecdhPublic.length); + console.log(namedCurve, secret.length, secret.equals(ecdh.computeSecret(own))); +} + +const x = crypto.generateKeyPairSync("x25519"); +const y = crypto.generateKeyPairSync("x25519"); +const xy = crypto.diffieHellman({ privateKey: x.privateKey, publicKey: y.publicKey }); +console.log("x25519", xy.length, xy.equals(crypto.diffieHellman({ privateKey: y.privateKey, publicKey: x.publicKey }))); +attempt("public key given as the private key", () => crypto.diffieHellman({ privateKey: x.privateKey, publicKey: y.privateKey })); + +const p256 = crypto.generateKeyPairSync("ec", { namedCurve: "P-256" }); +const p384 = crypto.generateKeyPairSync("ec", { namedCurve: "P-384" }); +const rsa = crypto.generateKeyPairSync("rsa", { modulusLength: 1024 }); +const ed = crypto.generateKeyPairSync("ed25519"); +attempt("x25519 and ec", () => crypto.diffieHellman({ privateKey: x.privateKey, publicKey: p256.publicKey })); +attempt("ec and x25519", () => crypto.diffieHellman({ privateKey: p256.privateKey, publicKey: x.publicKey })); +attempt("rsa", () => crypto.diffieHellman({ privateKey: rsa.privateKey, publicKey: rsa.publicKey })); +attempt("ed25519", () => crypto.diffieHellman({ privateKey: ed.privateKey, publicKey: ed.publicKey })); +attempt("P-256 and P-384", () => crypto.diffieHellman({ privateKey: p256.privateKey, publicKey: p384.publicKey })); +attempt("public as private", () => crypto.diffieHellman({ privateKey: x.publicKey, publicKey: y.publicKey })); +const zero = crypto.createPublicKey({ key: Buffer.alloc(32), format: "raw-public", asymmetricKeyType: "x25519" } as unknown as crypto.PublicKeyInput); +attempt("small-order point", () => crypto.diffieHellman({ privateKey: x.privateKey, publicKey: zero })); + +crypto.diffieHellman({ privateKey: x.privateKey, publicKey: y.publicKey }, (err, secret) => { + console.log("callback", err, secret.equals(xy)); + crypto.diffieHellman({ privateKey: x.privateKey, publicKey: zero }, (...args: unknown[]) => { + const error = args[0] as NodeJS.ErrnoException; + console.log("callback error", args.length, error.code, error.message); + }); +}); +console.log("returned"); diff --git a/tests/corpus/7452-crypto-diffie-hellman-errors.mjs b/tests/corpus/7452-crypto-diffie-hellman-errors.mjs new file mode 100644 index 000000000..d8adc6442 --- /dev/null +++ b/tests/corpus/7452-crypto-diffie-hellman-errors.mjs @@ -0,0 +1,61 @@ +// crypto.diffieHellman's argument checks in Node's order, key inputs other +// than KeyObjects, and the own properties of the OpenSSL errors it and +// X25519 signatures throw. +import * as crypto from "node:crypto"; + +function attempt(label, run) { + try { + const result = run(); + console.log(label, Buffer.isBuffer(result) ? `Buffer(${result.length})` : String(result)); + } catch (e) { + console.log(label, "throws", e.name, e.code, e.message, JSON.stringify(Object.keys(e))); + } +} + +const x = crypto.generateKeyPairSync("x25519"); +const y = crypto.generateKeyPairSync("x25519"); +const ec = crypto.generateKeyPairSync("ec", { namedCurve: "P-256" }); +const other = crypto.generateKeyPairSync("ec", { namedCurve: "P-384" }); +const dh = (options, callback) => () => + callback === undefined ? crypto.diffieHellman(options) : crypto.diffieHellman(options, callback); + +attempt("no options", dh()); +attempt("number options", dh(5)); +attempt("array options", dh([])); +attempt("number callback", dh({ privateKey: x.privateKey, publicKey: y.publicKey }, 5)); +attempt("callback before keys", dh({}, 5)); +attempt("no private key", dh({ publicKey: y.publicKey })); +attempt("no public key", dh({ privateKey: x.privateKey })); +attempt("number public key", dh({ privateKey: x.privateKey, publicKey: 2 })); +attempt("number private key", dh({ privateKey: 1, publicKey: y.publicKey })); +attempt("null private key", dh({ privateKey: null, publicKey: y.publicKey })); +attempt("public key object as private", dh({ privateKey: x.publicKey, publicKey: y.publicKey })); +attempt("types checked first", dh({ privateKey: x.privateKey, publicKey: ec.publicKey })); + +// key inputs other than KeyObjects +const pkcs8 = x.privateKey.export({ format: "pem", type: "pkcs8" }); +const spki = y.publicKey.export({ format: "pem", type: "spki" }); +attempt("pem keys", dh({ privateKey: pkcs8, publicKey: spki })); +attempt("der key objects", dh({ + privateKey: { key: x.privateKey.export({ format: "der", type: "pkcs8" }), format: "der", type: "pkcs8" }, + publicKey: { key: y.publicKey.export({ format: "jwk" }), format: "jwk" }, +})); +attempt("public key from a private pem", dh({ privateKey: x.privateKey, publicKey: pkcs8 })); +attempt("same result", () => + crypto.diffieHellman({ privateKey: pkcs8, publicKey: spki }).equals(crypto.diffieHellman({ privateKey: x.privateKey, publicKey: y.publicKey })), +); +attempt("mixed types from pem", dh({ privateKey: x.privateKey, publicKey: ec.publicKey.export({ format: "pem", type: "spki" }) })); +attempt("mismatched curves", dh({ privateKey: ec.privateKey, publicKey: other.publicKey })); +const zero = crypto.createPublicKey({ key: Buffer.alloc(32), format: "raw-public", asymmetricKeyType: "x25519" }); +attempt("small-order point", dh({ privateKey: x.privateKey, publicKey: zero })); +attempt("sign", () => crypto.sign(null, Buffer.from("x"), x.privateKey)); + +// the callback form: argument errors throw, job errors arrive without +// the OpenSSL properties +attempt("callback returns", dh({ privateKey: x.privateKey, publicKey: y.publicKey }, (err, secret) => { + console.log("callback", err, secret.length); + crypto.diffieHellman({ privateKey: ec.privateKey, publicKey: other.publicKey }, (error) => { + console.log("callback mismatch", error.code, error.message, JSON.stringify(Object.keys(error))); + }); +})); +attempt("callback with a bad key throws", dh({ privateKey: 1, publicKey: y.publicKey }, () => console.log("never"))); diff --git a/tests/corpus/7453-untyped-overload-results.cjs b/tests/corpus/7453-untyped-overload-results.cjs new file mode 100644 index 000000000..21923827d --- /dev/null +++ b/tests/corpus/7453-untyped-overload-results.cjs @@ -0,0 +1,85 @@ +// An untyped argument can match any overload of a call at run time. The +// result follows the overload the value selects, through bindings, +// destructuring and function returns. +const crypto = require("crypto"); +const fs = require("fs"); +const os = require("os"); +const path = require("path"); + +const ecPair = (options) => crypto.generateKeyPairSync("ec", options); +const objects = ecPair({ namedCurve: "P-256" }); +console.log(typeof objects.privateKey, objects.privateKey.type, objects.publicKey.asymmetricKeyType); +const pem = ecPair({ + namedCurve: "P-256", + publicKeyEncoding: { type: "spki", format: "pem" }, + privateKeyEncoding: { type: "pkcs8", format: "pem" }, +}); +console.log(typeof pem.privateKey, pem.publicKey.split("\n")[0]); + +function privateKeyKind(options) { + const pair = crypto.generateKeyPairSync("ec", options); + return typeof pair.privateKey; +} +console.log(privateKeyKind({ namedCurve: "P-384" })); + +function spread(options) { + const { privateKey, publicKey } = crypto.generateKeyPairSync("ec", { + namedCurve: "P-256", + ...options, + }); + return [typeof privateKey, Buffer.isBuffer(publicKey)]; +} +console.log(spread({}), spread({ publicKeyEncoding: { type: "spki", format: "der" } })); + +const digest = (encoding) => crypto.createHash("sha256").update("x").digest(encoding); +console.log(typeof digest("hex"), Buffer.isBuffer(digest(undefined)), digest(undefined).length); +const hmac = (encoding) => crypto.createHmac("sha256", "k").update("v").digest(encoding); +console.log(hmac("base64"), Buffer.isBuffer(hmac())); +function digestLength(encoding) { + let value = crypto.createHash("sha1").update("y").digest(encoding); + return Buffer.isBuffer(value) ? `buffer ${value.length}` : `string ${value.length}`; +} +console.log(digestLength(undefined), digestLength("hex")); +async function digestLater(encoding) { + return crypto.createHash("md5").update("z").digest(encoding); +} + +const key = crypto.generateKeyPairSync("ed25519").publicKey; +const exported = (options) => key.export(options); +console.log( + Buffer.isBuffer(exported({ format: "der", type: "spki" })), + typeof exported({ format: "pem", type: "spki" }), + exported({ format: "jwk" }).kty, +); + +const file = path.join(fs.mkdtempSync(path.join(os.tmpdir(), "overloads-")), "text.txt"); +fs.writeFileSync(file, "hello"); +const read = (options) => fs.readFileSync(file, options); +const text = read("utf8"); +const bytes = read(undefined); +const latin1 = read({ encoding: "latin1" }); +console.log(text, Buffer.isBuffer(bytes), bytes.length, latin1); +console.log(read("utf8"), read({ encoding: "hex" })); + +/** + * @overload + * @param {string} value + * @returns {string} + */ +/** + * @overload + * @param {number} value + * @returns {number} + */ +/** @param {string | number} value */ +function same(value) { + return value; +} +const viaSame = (value) => same(value); +console.log(typeof viaSame("s"), typeof viaSame(3), viaSame(3) + 1); + +(async () => { + const later = await digestLater(undefined); + console.log(Buffer.isBuffer(later), typeof (await digestLater("hex"))); + fs.rmSync(path.dirname(file), { recursive: true }); +})(); diff --git a/tests/corpus/7454-tls-peer-embedded-pem.ts b/tests/corpus/7454-tls-peer-embedded-pem.ts new file mode 100644 index 000000000..1937320e9 --- /dev/null +++ b/tests/corpus/7454-tls-peer-embedded-pem.ts @@ -0,0 +1,46 @@ +// A peer certificate whose extension carries another certificate's PEM as +// text: getPeerX509Certificate() and getX509Certificate() give the +// certificate the handshake delivered, and its fields, not the embedded +// one. new X509Certificate() of the same DER reads the PEM inside it first, +// as Node's PEM-before-DER parse does. +import { X509Certificate } from "node:crypto"; +import { readFileSync } from "node:fs"; +import type { AddressInfo } from "node:net"; +import * as tls from "node:tls"; + +const D = "tests/fixtures/server/certs"; +const cert = readFileSync(`${D}/embedded.pem`), key = readFileSync(`${D}/embedded-key.pem`); +const pem = new X509Certificate(cert); + +function describe(label: string, x: X509Certificate | undefined): void { + if (!x) return console.log(label, "undefined"); + console.log(label, x.subject, x.serialNumber, x.fingerprint256 === pem.fingerprint256, x.fingerprint === pem.fingerprint, x.validTo, x.raw.length); +} + +// raw is the certificate SEQUENCE, without bytes after it, and PEM armor +// counts only at the start of a line +const trailing = new X509Certificate(Buffer.concat([pem.raw, Buffer.from([1, 2, 3])])); +console.log("trailing bytes", trailing.raw.length === pem.raw.length, trailing.fingerprint256 === pem.fingerprint256, trailing.fingerprint === pem.fingerprint, trailing.validFrom === pem.validFrom); +for (const [label, input] of [["after a line", Buffer.concat([Buffer.from("text\n"), cert])], ["inside a line", Buffer.concat([Buffer.from("text"), cert])]] as const) { + try { + console.log(label, new X509Certificate(input).subject); + } catch (e) { + console.log(label, (e as NodeJS.ErrnoException).code); + } +} + +const server = tls.createServer({ cert, key }, (s) => { + describe("server own", s.getX509Certificate()); + s.end(); +}); +server.listen(0, "127.0.0.1", () => { + const c = tls.connect({ port: (server.address() as AddressInfo).port, host: "127.0.0.1", rejectUnauthorized: false }, () => { + const peer = c.getPeerX509Certificate(); + describe("client peer", peer); + const legacy = c.getPeerCertificate(); + console.log("legacy", legacy.subject.CN, legacy.fingerprint256 === pem.fingerprint256); + if (peer) describe("rebuilt from raw", new X509Certificate(peer.raw)); + }); + c.resume(); + c.on("close", () => server.close()); +}); diff --git a/tests/corpus/7455-tls-als-contexts.mjs b/tests/corpus/7455-tls-als-contexts.mjs new file mode 100644 index 000000000..bc59a78d4 --- /dev/null +++ b/tests/corpus/7455-tls-als-contexts.mjs @@ -0,0 +1,48 @@ +// The AsyncLocalStorage store in TLS server and client events: a server's +// connections run in its listen() context, except 'tlsClientError', which +// Node emits with no store; a client's events run in its connect() context. +import * as tls from "node:tls"; +import { AsyncLocalStorage } from "node:async_hooks"; +import { readFileSync } from "node:fs"; + +const D = "tests/fixtures/server/certs"; +const cert = readFileSync(`${D}/localhost.pem`); +const key = readFileSync(`${D}/localhost-key.pem`); +const als = new AsyncLocalStorage(); +const serverEvents = []; +const clientEvents = []; +const server = tls.createServer({ cert, key }, (socket) => { + serverEvents.push(`secureConnection ${als.getStore()}`); + socket.on("data", () => { + serverEvents.push(`data ${als.getStore()}`); + socket.end("pong"); + }); +}); +server.on("tlsClientError", (err) => serverEvents.push(`tlsClientError ${err.code} ${als.getStore()}`)); +await als.run("listen", () => new Promise((resolve) => server.listen(0, "127.0.0.1", () => { + serverEvents.push(`listening ${als.getStore()}`); + resolve(undefined); +}))); +const { port } = server.address(); +await als.run("client", () => new Promise((resolve) => { + const client = tls.connect({ port, host: "127.0.0.1", rejectUnauthorized: false }, () => { + clientEvents.push(`secureConnect ${als.getStore()}`); + client.write("ping"); + }); + client.on("data", () => clientEvents.push(`data ${als.getStore()}`)); + client.on("close", () => { + clientEvents.push(`close ${als.getStore()}`); + resolve(undefined); + }); +})); +await als.run("untrusted", () => new Promise((resolve) => { + const client = tls.connect({ port, host: "127.0.0.1" }); + client.on("error", (err) => { + clientEvents.push(`error ${err.code} ${als.getStore()}`); + setTimeout(resolve, 100); + }); +})); +server.close(() => { + console.log("server:", serverEvents.join(" ; ")); + console.log("client:", clientEvents.join(" ; ")); +}); diff --git a/tests/corpus/7503-pipeline-untyped-sources.mjs b/tests/corpus/7503-pipeline-untyped-sources.mjs new file mode 100644 index 000000000..e676fd607 --- /dev/null +++ b/tests/corpus/7503-pipeline-untyped-sources.mjs @@ -0,0 +1,102 @@ +// pipeline() over untyped iterable sources: arrays, strings, Sets, generator +// objects, async generator objects, class iterables and object values, +// into Writables, a Transform chain, a function stage and the promise form. +// Each source is pumped as Node pumps an iterable, on the same ticks. +import { pipeline, Writable, Transform } from "node:stream"; +import { pipeline as pipelineP } from "node:stream/promises"; + +const log = []; +function show(chunk) { + return typeof chunk === "string" ? chunk : Buffer.isBuffer(chunk) ? chunk.toString() : JSON.stringify(chunk); +} +function objectSink(label) { + return new Writable({ + objectMode: true, + write(chunk, _enc, cb) { + log.push(`${label} write ${show(chunk)}`); + cb(); + }, + final(cb) { log.push(`${label} final`); cb(); }, + }); +} +function sink(label, objectMode = false) { + if (objectMode) return objectSink(label); + return new Writable({ + write(chunk, _enc, cb) { + log.push(`${label} write ${typeof chunk === "string" ? chunk : Buffer.isBuffer(chunk) ? chunk.toString() : JSON.stringify(chunk)}`); + cb(); + }, + final(cb) { log.push(`${label} final`); cb(); }, + }); +} +function* gen() { + log.push("gen start"); + yield "g1"; + log.push("gen mid"); + yield "g2"; + log.push("gen end"); +} +async function* agen() { + yield "a1"; + await null; + yield "a2"; +} +const sources = { + array: () => ["x", "y"], + string: () => "hey", + set: () => new Set(["s1", "s2"]), + gen: () => gen(), + agen: () => agen(), + iterable: () => ({ *[Symbol.iterator]() { yield "i1"; yield "i2"; } }), + objects: () => [{ n: 1 }, { n: 2 }], +}; +const names = Object.keys(sources); +async function run(name) { + const src = sources[name](); + const objectMode = name === "objects"; + await new Promise((resolve) => { + queueMicrotask(() => log.push(`${name} microtask`)); + pipeline(src, sink(name, objectMode), (err) => { + log.push(`${name} done ${err ? err.code ?? err.message : err}`); + resolve(undefined); + }); + log.push(`${name} after call`); + }); +} +for (const n of names) await run(n); + +// a transform chain and a function stage after an untyped source +const src2 = ["p", "q"]; +await new Promise((resolve) => + pipeline( + src2, + new Transform({ transform(c, _e, cb) { cb(null, c.toString().toUpperCase()); } }), + sink("transform"), + (err) => { log.push(`transform done ${err}`); resolve(undefined); }, + ), +); +const src3 = new Set(["f1", "f2"]); +await new Promise((resolve) => + pipeline( + src3, + async function* (source) { for await (const c of source) yield c + "!"; }, + sink("fnstage"), + (err) => { log.push(`fnstage done ${err}`); resolve(undefined); }, + ), +); +// promises form +const src4 = ["m", "n"]; +await pipelineP(src4, sink("promises")); +log.push("promises resolved"); +// an error thrown by the source +function* bad() { yield "ok"; throw new Error("source failed"); } +const src5 = bad(); +await new Promise((resolve) => + pipeline(src5, sink("bad"), (err) => { log.push(`bad done ${err && err.message}`); resolve(undefined); }), +); +// a source of non-string values into a byte-mode writable +const src6 = [1, 2]; +await new Promise((resolve) => + pipeline(src6, sink("numbers"), (err) => { log.push(`numbers done ${err && err.code}`); resolve(undefined); }), +); +console.log(log.join("\n")); diff --git a/tests/corpus/7504-pipeline-untyped-source-errors.mjs b/tests/corpus/7504-pipeline-untyped-source-errors.mjs new file mode 100644 index 000000000..9dd671606 --- /dev/null +++ b/tests/corpus/7504-pipeline-untyped-source-errors.mjs @@ -0,0 +1,171 @@ +// pipeline() over untyped iterable sources: a destination that fails closes +// the source, backpressure waits for drain, an abort signal stops the pump, +// async generator and Map sources reach object-mode destinations, and a +// sync source's promise values, its final value included, are awaited. +import { pipeline, Writable, PassThrough } from "node:stream"; +import { pipeline as pipelineP } from "node:stream/promises"; + +function* counted(label, n) { + try { + for (let i = 0; i < n; i++) { + console.log(label, "yield", i); + yield `${label}${i}`; + } + } finally { + console.log(label, "finally"); + } +} +function pick(v) { + return v; +} + +// the destination fails on its second write: the source's finally runs +await new Promise((resolve) => { + let n = 0; + const dst = new Writable({ + write(chunk, _e, cb) { + n++; + console.log("fail write", String(chunk)); + cb(n === 2 ? new Error("write failed") : null); + }, + }); + pipeline(pick(counted("f", 5)), dst, (err) => { + console.log("fail done", err && err.message); + resolve(undefined); + }); +}); + +// backpressure: a one-byte highWaterMark and asynchronous writes +await new Promise((resolve) => { + const dst = new Writable({ + highWaterMark: 1, + write(chunk, _e, cb) { + console.log("slow write", String(chunk)); + setTimeout(cb, 1); + }, + }); + pipeline(pick(counted("b", 3)), dst, (err) => { + console.log("slow done", err); + resolve(undefined); + }); +}); + +// an abort signal +const ac = new AbortController(); +try { + await pipelineP( + pick(counted("s", 10)), + new Writable({ + write(chunk, _e, cb) { + console.log("signal write", String(chunk)); + if (String(chunk) === "s1") ac.abort(); + cb(); + }, + }), + { signal: ac.signal }, + ); +} catch (err) { + console.log("signal rejected", err.name); +} + +// an untyped async generator into a PassThrough read with for await +const out = new PassThrough({ objectMode: true }); +const both = pipelineP( + pick((async function* () { + yield "u1"; + yield "u2"; + })()), + out, +); +for await (const c of out) console.log("passthrough", c); +await both; +console.log("passthrough resolved"); + +// a Map source of entries into an object-mode sink +await new Promise((resolve) => { + pipeline( + pick(new Map([["k", 1]])), + new Writable({ objectMode: true, write(c, _e, cb) { console.log("map write", JSON.stringify(c)); cb(); } }), + (err) => { console.log("map done", err); resolve(undefined); }, + ); +}); + +// sync sources whose values are promises: each value is awaited, as +// for await over a sync iterator does, and a rejection closes the source +await pipelineP( + pick([Promise.resolve("p1"), "p2", new Promise((r) => setTimeout(() => r("p3"), 1))]), + new Writable({ write(c, _e, cb) { console.log("byte write", String(c)); cb(); } }), +); +function* promised() { + try { + yield Promise.resolve({ n: 1 }); + yield Promise.reject(new Error("value rejected")); + yield "never"; + } finally { + console.log("promised finally"); + } +} +try { + await pipelineP( + pick(promised()), + new Writable({ objectMode: true, write(c, _e, cb) { console.log("object write", JSON.stringify(c)); cb(); } }), + ); +} catch (err) { + console.log("promised rejected", err.message); +} + +// a sync source's final value is awaited too: a rejected one rejects the +// pipeline, and a resolved one is not written +function* finalRejects() { + yield "f1"; + return Promise.reject(new Error("final value rejected")); +} +function* finalResolves() { + yield "r1"; + return Promise.resolve("not written"); +} +for (const [label, source] of [["finalRejects", finalRejects], ["finalResolves", finalResolves]]) { + try { + await pipelineP( + pick(source()), + new Writable({ objectMode: true, write(c, _e, cb) { console.log(label, "write", c); cb(); } }), + ); + console.log(label, "resolved"); + } catch (err) { + console.log(label, "rejected", err.message); + } +} + +// the final result's value is read for a sync iterator only: an async +// iterator's done result is not read, so a throwing value getter there is +// never called +function finalResult(label, i) { + if (i === 1) return { value: `${label}1`, done: false }; + return { + done: true, + get value() { + throw new Error(`${label} final value read`); + }, + }; +} +function asyncFinal() { + let i = 0; + return { [Symbol.asyncIterator]() { return { async next() { i++; return finalResult("async", i); } }; } }; +} +function syncFinal() { + let i = 0; + return { [Symbol.iterator]() { return { next() { i++; return finalResult("sync", i); } }; } }; +} +async function runFinal(label, source) { + try { + await pipelineP( + pick(source), + new Writable({ objectMode: true, write(c, _e, cb) { console.log(label, "write", c); cb(); } }), + ); + console.log(label, "resolved"); + } catch (err) { + console.log(label, "rejected", err.message); + } +} +await runFinal("async final", asyncFinal()); +await runFinal("sync final", syncFinal()); diff --git a/tests/corpus/7505-pipeline-untyped-sources.cjs b/tests/corpus/7505-pipeline-untyped-sources.cjs new file mode 100644 index 000000000..6dd67b4ff --- /dev/null +++ b/tests/corpus/7505-pipeline-untyped-sources.cjs @@ -0,0 +1,40 @@ +// pipeline() over untyped sources in CommonJS: a source that a function +// answers, which may be an array, a generator, a stream, a string, or a +// Buffer, whose bytes are numbers that a byte-mode Writable refuses. +const { pipeline, Readable, Writable } = require("node:stream"); + +function sink(label) { + const parts = []; + return new Writable({ + write(chunk, _enc, cb) { + parts.push(String(chunk)); + cb(); + }, + final(cb) { + console.log(label, parts.join("|")); + cb(); + }, + }); +} + +function* letters() { + yield "a"; + yield "b"; +} + +const sources = [ + () => ["one", "two"], + () => letters(), + () => Readable.from(["r1", "r2"]), + () => "xyz", + () => Buffer.from("buf"), +]; + +function run(i) { + if (i === sources.length) return; + pipeline(sources[i](), sink(`source ${i}`), (err) => { + console.log(`source ${i} done`, err === undefined ? "ok" : err.code); + run(i + 1); + }); +} +run(0); diff --git a/tests/corpus/7506-pipeline-any-source.ts b/tests/corpus/7506-pipeline-any-source.ts new file mode 100644 index 000000000..84eec0cf4 --- /dev/null +++ b/tests/corpus/7506-pipeline-any-source.ts @@ -0,0 +1,48 @@ +// pipeline() in TypeScript over sources typed any or unknown, and over a +// Set, a Map and an async generator object, which are pumped as iterables. +import { pipeline, Writable } from "node:stream"; +import { pipeline as pipelineP } from "node:stream/promises"; + +function sink(label: string): Writable { + const parts: string[] = []; + return new Writable({ + objectMode: true, + write(chunk: unknown, _enc, cb) { + parts.push(JSON.stringify(chunk)); + cb(); + }, + final(cb) { + console.log(label, parts.join(" ")); + cb(); + }, + }); +} + +async function* ticks(): AsyncGenerator { + yield "t1"; + await new Promise((r) => setTimeout(r, 1)); + yield "t2"; +} + +async function main(): Promise { + const anySource: any = ["any1", "any2"]; + await pipelineP(anySource, sink("any")); + const unknownSource: unknown = new Set([1, 2, 3]); + await pipelineP(unknownSource as any, sink("unknown")); + await pipelineP(new Set(["s"]), sink("set")); + await pipelineP( + new Map([ + ["a", 1], + ["b", 2], + ]), + sink("map"), + ); + await pipelineP(ticks(), sink("async generator")); + await new Promise((resolve) => { + pipeline(anySource, sink("callback"), (err?: Error | null) => { + console.log("callback done", err); + resolve(); + }); + }); +} +void main(); diff --git a/tests/corpus/7507-stream-write-error-drain.mjs b/tests/corpus/7507-stream-write-error-drain.mjs new file mode 100644 index 000000000..8298bcb42 --- /dev/null +++ b/tests/corpus/7507-stream-write-error-drain.mjs @@ -0,0 +1,37 @@ +// A write whose callback fails synchronously: write() answers false and +// writableNeedDrain stays false, so no 'drain' follows the error, and a +// pipeline pumping a generator stops at the failed write. +import { pipeline, Writable } from "node:stream"; + +const w = new Writable({ + write(_chunk, _enc, cb) { + cb(new Error("bad write")); + }, +}); +w.on("error", (e) => console.log("error event", e.message)); +w.on("drain", () => console.log("drain")); +const r = w.write("a"); +console.log("returned", r, "needDrain", w.writableNeedDrain, "errored", w.errored.message); + +function* source() { + try { + for (let i = 0; i < 4; i++) { + console.log("yield", i); + yield `c${i}`; + } + } finally { + console.log("source finally"); + } +} +let writes = 0; +pipeline( + source(), + new Writable({ + write(chunk, _enc, cb) { + writes++; + console.log("write", String(chunk)); + cb(writes === 2 ? new Error("second write failed") : null); + }, + }), + (err) => console.log("pipeline done", err.message), +); diff --git a/tests/corpus/7508-pipeline-untyped-source-functions.mjs b/tests/corpus/7508-pipeline-untyped-source-functions.mjs new file mode 100644 index 000000000..987221ffd --- /dev/null +++ b/tests/corpus/7508-pipeline-untyped-source-functions.mjs @@ -0,0 +1,85 @@ +// pipeline() over untyped first stages that are functions or +// { readable, writable } pairs: a source function is called with +// { signal } while the pipeline is set up and its iterable result is +// pumped; a result that is not iterable throws ERR_INVALID_RETURN_VALUE +// from the callback form and rejects the promise form; a pair pumps its +// readable side. +import { pipeline, Writable, Readable, PassThrough } from "node:stream"; +import { pipeline as pipelineP } from "node:stream/promises"; + +function pick(v) { + return v; +} +function sink(label) { + return new Writable({ + objectMode: true, + write(chunk, _enc, cb) { + console.log(label, "write", JSON.stringify(String(chunk))); + cb(); + }, + final(cb) { + console.log(label, "final"); + cb(); + }, + }); +} + +const arraySource = pick(function (opts) { + console.log("array source called", typeof opts, typeof opts.signal, opts.signal.aborted); + return ["a1", "a2"]; +}); +const before = pipelineP(arraySource, sink("array")); +console.log("after pipeline() returned"); +await before; +console.log("array resolved"); + +await pipelineP(pick(() => "xy"), sink("string")); + +await pipelineP( + pick(async function* ({ signal }) { + console.log("generator source", typeof signal.aborted); + yield "g1"; + await new Promise((r) => setTimeout(r, 1)); + yield "g2"; + }), + pick(async function* (source) { + for await (const chunk of source) yield `[${chunk}]`; + }), + sink("generator"), +); + +await new Promise((resolve) => { + pipeline(pick(() => new Set(["s1", "s2"])), sink("set"), (err) => { + console.log("set done", err); + resolve(undefined); + }); +}); + +// a result that is not iterable +try { + pipeline(pick(() => 5), sink("five"), () => console.log("five callback")); +} catch (err) { + console.log("five threw", err.name, err.code, err.message); +} +try { + await pipelineP(pick(() => ({ value: 1 })), sink("object")); +} catch (err) { + console.log("object rejected", err.name, err.code, err.message); +} + +// a source function that throws +try { + await pipelineP( + pick(() => { + throw new Error("source failed"); + }), + sink("throws"), + ); +} catch (err) { + console.log("throws rejected", err.message); +} + +// a { readable, writable } pair pumps its readable side +const side = new PassThrough(); +await pipelineP(pick({ readable: Readable.from(["p1", "p2"]), writable: side }), sink("pair")); +console.log("done"); diff --git a/tests/corpus/7509-pipeline-untyped-function-stages.mjs b/tests/corpus/7509-pipeline-untyped-function-stages.mjs new file mode 100644 index 000000000..590334f90 --- /dev/null +++ b/tests/corpus/7509-pipeline-untyped-function-stages.mjs @@ -0,0 +1,117 @@ +// pipeline() with untyped stages after the first that are functions: a +// transform is called with the previous stream or generator and +// { signal } while the pipeline is set up, and must return an async +// iterable; an async function destination's value resolves the promise +// form and reaches the callback; an async generator destination feeds +// the PassThrough that pipeline() returns. Results of the wrong kind throw +// ERR_INVALID_RETURN_VALUE naming the stage. A stage after an untyped +// source function or transform receives that stage's own iterator, as +// Node's makeAsyncIterable answers an iterable itself. +import { pipeline, Writable, Readable, PassThrough } from "node:stream"; +import { pipeline as pipelineP } from "node:stream/promises"; + +function pick(v) { + return v; +} +function sink(label) { + return new Writable({ + objectMode: true, + write(chunk, _enc, cb) { + console.log(label, "write", JSON.stringify(String(chunk))); + cb(); + }, + }); +} +const upper = pick(async function* (source, { signal }) { + console.log("upper called", source instanceof Readable, typeof signal.aborted); + for await (const chunk of source) yield String(chunk).toUpperCase(); +}); +const join = pick(async (source) => { + let text = ""; + for await (const chunk of source) text += chunk; + return text; +}); + +await pipelineP(Readable.from(["a", "b"]), upper, sink("transform")); +console.log("joined", await pipelineP(Readable.from(["x", "y"]), upper, join)); +console.log("two transforms", await pipelineP(pick(["m", "n"]), upper, upper, join)); + +await new Promise((resolve) => { + pipeline(Readable.from(["c1", "c2"]), upper, join, (err, value) => { + console.log("callback", err, value); + resolve(undefined); + }); +}); + +// an async generator destination +const wrap = pick(async function* (source) { + for await (const chunk of source) yield `<${chunk}>`; +}); +const out = pipeline(Readable.from(["p1", "p2"]), wrap, (err, value) => { + console.log("generator destination done", err, value); +}); +console.log("returns a PassThrough", out instanceof PassThrough); +for await (const chunk of out) console.log("passthrough", chunk); + +// an async generator destination in the promise form: the PassThrough +// takes its values and the promise resolves to undefined +const promised = await pipelineP(Readable.from(["q1", "q2"]), wrap); +console.log("generator destination promise", promised, typeof promised); + +// a destination whose promise rejects +try { + await pipelineP( + Readable.from(["r"]), + pick(async (source) => { + for await (const chunk of source) console.log("rejecting after", chunk); + throw new Error("destination failed"); + }), + ); +} catch (err) { + console.log("rejected", err.message); +} + +// results of the wrong kind +try { + await pipelineP(Readable.from(["i"]), pick(() => 5), sink("number")); +} catch (err) { + console.log("transform result", err.name, err.code, err.message); +} +try { + await pipelineP(Readable.from(["i"]), upper, pick(() => ["sync"]), sink("array")); +} catch (err) { + console.log("second transform result", err.code, err.message); +} +try { + pipeline(Readable.from(["i"]), pick(() => 7), () => console.log("never")); +} catch (err) { + console.log("destination result", err.name, err.code, err.message); +} +// the iterators handed from stage to stage +let transformIter = null; +const handed = await pipelineP( + pick(function* () { + yield "s1"; + yield "s2"; + }), + pick((source) => { + // a sync generator, not an async iterator over it + console.log( + "transform gets the source's iterator", + typeof source[Symbol.asyncIterator], + typeof source[Symbol.iterator], + ); + transformIter = (async function* () { + for await (const chunk of source) yield `${chunk}!`; + })(); + return transformIter; + }), + pick(async (source) => { + console.log("destination gets the transform's iterator", source === transformIter); + let text = ""; + for await (const chunk of source) text += chunk; + return text; + }), +); +console.log("handed", handed); +console.log("done"); diff --git a/tests/corpus/7510-pipeline-untyped-later-iterables.mjs b/tests/corpus/7510-pipeline-untyped-later-iterables.mjs new file mode 100644 index 000000000..7fe6e27b3 --- /dev/null +++ b/tests/corpus/7510-pipeline-untyped-later-iterables.mjs @@ -0,0 +1,64 @@ +// pipeline() with an untyped stage after the first that is not a stream or +// a function: Node takes Duplex.from of it, so an iterable becomes the +// readable stage the next stream is piped from, the stages before it are +// not read and are destroyed when the pipeline finishes, and a value +// Duplex.from rejects throws ERR_INVALID_ARG_TYPE. +import { pipeline, Writable, Readable, Duplex } from "node:stream"; +import { pipeline as pipelineP } from "node:stream/promises"; + +function pick(v) { + return v; +} +function sink(label) { + return new Writable({ + objectMode: true, + write(chunk, _enc, cb) { + console.log(label, "write", JSON.stringify(String(chunk))); + cb(); + }, + }); +} +function watched(label, values) { + const source = Readable.from(values); + source.on("error", (err) => console.log(label, "error", err.code)); + source.on("close", () => console.log(label, "close", source.destroyed)); + return source; +} + +await pipelineP(watched("first", ["unread"]), pick(["z1", "z2"]), sink("array")); +console.log("array resolved"); +await new Promise((r) => setTimeout(r, 5)); + +await new Promise((resolve) => { + pipeline(watched("second", ["unread"]), pick("text"), sink("string"), (err) => { + console.log("string done", err); + resolve(undefined); + }); +}); +await new Promise((r) => setTimeout(r, 5)); + +// a generator stage before the iterable is never read +await pipelineP( + Readable.from(["s"]), + pick(async function* (source) { + console.log("generator stage called"); + for await (const chunk of source) yield chunk; + }), + pick(new Set(["set1", "set2"])), + sink("set"), +); +console.log("set resolved"); + +// the iterable as the last stage is what pipeline() returns +const last = pipeline(Readable.from(["u"]), pick(["tail"]), () => console.log("never")); +console.log(last instanceof Duplex, last.readable, last.writable); +for await (const chunk of last) console.log("last", chunk); + +for (const value of [42, { plain: true }, null]) { + try { + await pipelineP(Readable.from(["i"]), pick(value), sink("rejected")); + } catch (err) { + console.log(err.name, err.code, err.message); + } +} +console.log("done"); diff --git a/tests/corpus/7511-pipeline-untyped-stages.cjs b/tests/corpus/7511-pipeline-untyped-stages.cjs new file mode 100644 index 000000000..124372e12 --- /dev/null +++ b/tests/corpus/7511-pipeline-untyped-stages.cjs @@ -0,0 +1,50 @@ +// A CommonJS helper that passes its untyped parameters to pipeline(): the +// same call runs over streams, function stages and iterables, whichever +// each argument is at run time. +const { pipeline, Transform, Writable, Readable } = require("node:stream"); + +function run(label, source, middle, destination) { + return new Promise((resolve) => { + pipeline(source, middle, destination, (err, value) => { + console.log(label, "done", err ? err.code || err.message : err, value); + resolve(undefined); + }); + }); +} +function sink(label) { + return new Writable({ + objectMode: true, + write(chunk, _enc, cb) { + console.log(label, "write", String(chunk)); + cb(); + }, + }); +} +const double = new Transform({ + objectMode: true, + transform(chunk, _enc, cb) { + cb(null, String(chunk) + String(chunk)); + }, +}); +async function* exclaim(source) { + for await (const chunk of source) yield `${chunk}!`; +} +async function count(source) { + let n = 0; + for await (const _ of source) n++; + return n; +} + +(async () => { + await run("streams", Readable.from(["a", "b"]), double, sink("streams")); + await run("function middle", Readable.from(["c"]), exclaim, sink("function middle")); + await run("function ends", () => ["d", "e", "f"], exclaim, count); + await run("iterable middle", Readable.from(["skipped"]), ["g"], sink("iterable middle")); + await run("generator into stream", ["h"], exclaim, new Transform({ + objectMode: true, + transform(chunk, _enc, cb) { + console.log("transform saw", chunk); + cb(null, chunk); + }, + }).on("data", (d) => console.log("data", d))); +})(); diff --git a/tests/corpus/7512-pipeline-any-stages.ts b/tests/corpus/7512-pipeline-any-stages.ts new file mode 100644 index 000000000..a2039a533 --- /dev/null +++ b/tests/corpus/7512-pipeline-any-stages.ts @@ -0,0 +1,45 @@ +// pipeline() in TypeScript with stages after the first typed any or +// unknown: functions run as function stages, streams are piped, and an +// iterable is taken as Duplex.from of it. +import { pipeline, Readable, Writable, PassThrough } from "node:stream"; +import { pipeline as pipelineP } from "node:stream/promises"; + +function sink(label: string): Writable { + return new Writable({ + objectMode: true, + write(chunk: unknown, _enc, cb) { + console.log(label, JSON.stringify(chunk)); + cb(); + }, + }); +} + +async function main(): Promise { + const shout: any = async function* (source: any) { + for await (const chunk of source) yield String(chunk).toUpperCase(); + }; + await pipelineP(Readable.from(["a", "b"]), shout, sink("function")); + + const total: unknown = async (source: any) => { + let sum = 0; + for await (const chunk of source) sum += Number(chunk); + return sum; + }; + const sum = await pipelineP(Readable.from(["1", "2", "3"]), total as any); + console.log("sum", sum); + + const relay: any = new PassThrough({ objectMode: true }); + await pipelineP(Readable.from([{ k: 1 }]), relay, sink("stream")); + + const values: any = ["v1", "v2"]; + await pipelineP(Readable.from(["unread"]), values, sink("iterable")); + + await new Promise((resolve) => { + pipeline(Readable.from(["x"]), shout, sink("callback"), (err: NodeJS.ErrnoException | null) => { + console.log("callback done", err == null); + resolve(); + }); + }); +} + +main().then(() => console.log("done")); diff --git a/tests/corpus/7513-pipeline-untyped-stage-errors.mjs b/tests/corpus/7513-pipeline-untyped-stage-errors.mjs new file mode 100644 index 000000000..ade4370b4 --- /dev/null +++ b/tests/corpus/7513-pipeline-untyped-stage-errors.mjs @@ -0,0 +1,86 @@ +// Untyped pipeline function stages next to typed stages: an untyped stage +// after a typed generator stage or a Transform, the { signal } an untyped +// stage receives aborting when the pipeline fails, and an untyped stage +// that throws while the pipeline is set up. +import { pipeline, Writable, Readable, Transform } from "node:stream"; +import { pipeline as pipelineP } from "node:stream/promises"; + +function pick(v) { + return v; +} +function sink(label) { + return new Writable({ + objectMode: true, + write(chunk, _enc, cb) { + console.log(label, "write", JSON.stringify(String(chunk))); + cb(); + }, + }); +} + +await pipelineP( + Readable.from(["a", "b"]), + async function* (source) { + for await (const chunk of source) yield `${chunk}1`; + }, + pick(async function* (source) { + for await (const chunk of source) yield `${chunk}2`; + }), + sink("after generator"), +); + +await pipelineP( + Readable.from(["q"]), + new Transform({ + transform(chunk, _enc, cb) { + cb(null, chunk); + }, + }), + pick(async (source) => { + for await (const chunk of source) console.log("destination saw", String(chunk)); + }), +); + +let seen; +try { + await pipelineP( + Readable.from(["x", "y"]), + pick(async function* (source, { signal }) { + seen = signal; + signal.addEventListener("abort", () => console.log("signal abort event")); + for await (const chunk of source) yield chunk; + }), + new Writable({ + write(_chunk, _enc, cb) { + cb(new Error("write failed")); + }, + }), + ); +} catch (err) { + console.log("rejected", err.message, seen.aborted); +} + +try { + pipeline( + Readable.from(["t"]), + pick(() => { + throw new Error("stage threw"); + }), + sink("never"), + () => console.log("never called"), + ); +} catch (err) { + console.log("callback form threw", err.message); +} +try { + await pipelineP( + Readable.from(["t"]), + pick(() => { + throw new Error("stage threw again"); + }), + sink("never"), + ); +} catch (err) { + console.log("promise form rejected", err.message); +} +console.log("done"); diff --git a/tests/corpus/7514-stream-after-write-order.ts b/tests/corpus/7514-stream-after-write-order.ts new file mode 100644 index 000000000..1c14de6eb --- /dev/null +++ b/tests/corpus/7514-stream-after-write-order.ts @@ -0,0 +1,102 @@ +// Write callbacks of synchronous writes run in Node's order: consecutive +// writes with the same callback complete in one afterWrite step, ahead of +// a process.nextTick queued between them, and a write with another +// callback (or none) starts the next step; end's callback and 'finish' +// follow. A write made from a write callback starts a new afterWrite step +// even when its callback matches a later queued one. A PassThrough read by +// 'data' runs each write's own callback. +import { PassThrough, Writable } from "node:stream"; + +const log: string[] = []; + +function run(): Promise { + return new Promise((resolve) => { + const w = new Writable({ + highWaterMark: 4, + write(chunk: Buffer, _enc, cb) { + log.push(`write ${String(chunk)}`); + cb(); + }, + }); + const shared = (): void => { + log.push("shared callback"); + }; + w.on("finish", () => { + log.push("finish"); + resolve(); + }); + w.write("a", shared); + w.write("b", shared); + process.nextTick(() => log.push("tick between")); + w.write("c", shared); + w.write("d", () => log.push("own callback d")); + w.write("e"); + w.write("f"); + queueMicrotask(() => log.push("microtask")); + w.write("g", shared); + w.write("h", shared); + process.nextTick(() => log.push("tick after")); + w.end(() => log.push("end callback")); + }); +} + +function nested(): Promise { + return new Promise((resolve) => { + const w = new Writable({ + write(chunk: Buffer, _enc, cb) { + log.push(`nested write ${String(chunk)}`); + cb(); + }, + }); + let laterCalls = 0; + const later = (): void => { + log.push("later callback"); + if (++laterCalls === 2) resolve(); + }; + w.write("a", () => { + log.push("first callback"); + w.write("from first", later); + }); + process.nextTick(() => log.push("nested tick 1")); + w.write("b", later); + process.nextTick(() => log.push("nested tick 2")); + }); +} + +function passThrough(): Promise { + return new Promise((resolve) => { + const pt = new PassThrough({ highWaterMark: 8 }); + let bytes = 0; + pt.on("data", (chunk: Buffer) => { + bytes += chunk.length; + }); + pt.on("end", () => { + log.push(`passthrough end ${bytes}`); + resolve(); + }); + let i = 0; + const more = (): void => { + while (i < 40) { + i++; + const n = i; + if (!pt.write(`chunk${n}`, () => { + if (n % 10 === 0) log.push(`callback ${n}`); + })) { + pt.once("drain", more); + return; + } + } + pt.end(); + }; + more(); + }); +} + +async function main(): Promise { + await run(); + await nested(); + await passThrough(); + console.log(log.join("\n")); +} + +main(); diff --git a/tests/corpus/7515-net-socket-backpressure.ts b/tests/corpus/7515-net-socket-backpressure.ts new file mode 100644 index 000000000..6524e6d54 --- /dev/null +++ b/tests/corpus/7515-net-socket-backpressure.ts @@ -0,0 +1,157 @@ +// net.Socket write() backpressure, as in Node 24: write answers false when +// the writes not yet completely sent reach the 65536-byte high-water mark, +// each counting in full, which a write made before the connection does; +// writableNeedDrain stays true until 'drain', and 'drain' fires once the +// buffered bytes are sent. The same holds while a custom lookup is pending, +// writableLength counts a large write the kernel took in part in full, +// untyped code reads the same properties, and a write whose send fails +// after the peer reset the connection answers false. An echo client that +// waits for 'drain' when write answers false moves every byte. +// +// The server picks each connection's handling from `mode` when it accepts +// it. The lookup client's bytes fit in the kernel's buffers, so its 'drain' +// can come before the server accepts that connection; the program waits for +// the accept before it changes `mode`. +import { connect, createServer, Socket } from "node:net"; +import type { LookupAddress, LookupAllOptions, LookupOneOptions } from "node:dns"; + +const CHUNK = 65536; +const TOTAL = 64 * CHUNK; +const chunk = Buffer.alloc(CHUNK, 120); + +function untyped(socket: any): string { + return `${socket.writableLength} ${socket.writableNeedDrain} ${socket.writableHighWaterMark}`; +} + +function echo(port: number): Promise { + return new Promise((resolve) => { + const client = connect(port, "127.0.0.1"); + console.log("small write before connect", client.write("hi"), client.writableLength); + console.log("large write before connect", client.write(Buffer.alloc(70000, 121)), client.writableLength); + console.log("need drain", client.writableNeedDrain, "untyped", untyped(client)); + let sent = 70002; + let received = 0; + const pump = (): void => { + while (sent < TOTAL) { + sent += CHUNK; + if (!client.write(chunk)) { + client.once("drain", pump); + return; + } + } + client.end(); + }; + client.once("drain", () => { + console.log("drain fired, need drain", client.writableNeedDrain, "pending", client.writableLength); + pump(); + }); + client.on("data", (data: Buffer) => { + received += data.length; + }); + client.on("end", () => { + console.log("received", received, "of", sent); + resolve(); + }); + }); +} + +function lookupPending(port: number): Promise { + return new Promise((resolve) => { + const client = connect({ + port, + host: "localhost", + lookup: ( + _host: string, + _options: LookupOneOptions | LookupAllOptions | number | undefined, + callback: (err: NodeJS.ErrnoException | null, address: string | LookupAddress[], family?: number) => void, + ) => { + const answer: LookupAddress[] = [{ address: "127.0.0.1", family: 4 }]; + setTimeout(() => callback(null, answer), 20); + }, + }); + console.log("lookup write", client.write(Buffer.alloc(70000, 97))); + console.log("lookup state", client.writableLength, client.writableNeedDrain, "untyped", untyped(client)); + client.once("drain", () => { + console.log("lookup drain", client.writableLength, client.writableNeedDrain); + client.destroy(); + resolve(); + }); + }); +} + +function largeWrite(port: number): Promise { + return new Promise((resolve) => { + const client = connect(port, "127.0.0.1", () => { + const size = 16 * 1024 * 1024; + console.log("large write", client.write(Buffer.alloc(size, 98))); + console.log("large state", client.writableLength === size, client.writableNeedDrain); + client.once("drain", () => { + console.log("large drain", client.writableLength, client.writableNeedDrain); + client.destroy(); + resolve(); + }); + setTimeout(() => { + if (paused !== null) paused.resume(); + }, 30); + }); + }); +} + +function afterReset(port: number): Promise { + return new Promise((resolve) => { + const client = connect(port, "127.0.0.1", () => { + client.pause(); + setTimeout(() => { + console.log("first write after the peer closed", client.write("a")); + setTimeout(() => { + console.log("write after reset", client.write("b")); + }, 50); + }, 50); + }); + client.on("error", () => {}); + client.on("close", () => { + console.log("closed, destroyed", client.destroyed); + resolve(); + }); + }); +} + +let paused: Socket | null = null; +let mode = "echo"; +let accepted: (() => void) | null = null; + +/** Resolves when the server accepts its next connection. */ +function nextConnection(): Promise { + return new Promise((resolve) => { + accepted = resolve; + }); +} + +const server = createServer((socket: Socket) => { + if (mode === "echo") socket.pipe(socket); + else if (mode === "drain") socket.resume(); + else if (mode === "pause") { + socket.pause(); + paused = socket; + } else socket.destroy(); + if (accepted !== null) { + const resolve = accepted; + accepted = null; + resolve(); + } +}); + +server.listen(0, "127.0.0.1", async () => { + const addr = server.address(); + const port = typeof addr === "object" && addr !== null ? addr.port : 0; + await echo(port); + mode = "drain"; + const lookupAccepted = nextConnection(); + await lookupPending(port); + await lookupAccepted; + mode = "pause"; + await largeWrite(port); + mode = "end"; + await afterReset(port); + server.close(); +}); diff --git a/tests/corpus/7516-readable-empty-push.ts b/tests/corpus/7516-readable-empty-push.ts new file mode 100644 index 000000000..27d88e3ed --- /dev/null +++ b/tests/corpus/7516-readable-empty-push.ts @@ -0,0 +1,69 @@ +// readable.push() of an empty Buffer outside object mode adds nothing, as +// in Node: the chunks pushed after it still reach 'data' listeners, +// readline and read(), and push(null) still ends the stream. In object +// mode an empty Buffer is a value like any other. +import { createInterface } from "node:readline"; +import { Readable } from "node:stream"; + +function lines(chunks: string[]): Promise { + return new Promise((resolve) => { + const src = new Readable({ read() {} }); + const rl = createInterface({ input: src, crlfDelay: Infinity }); + rl.on("line", (line: string) => console.log("line", JSON.stringify(line))); + rl.on("close", () => { + console.log("readline closed"); + resolve(); + }); + for (const chunk of chunks) src.push(Buffer.from(chunk, "utf8")); + src.push(null); + }); +} + +function data(): Promise { + return new Promise((resolve) => { + const src = new Readable({ read() {} }); + src.on("data", (chunk: Buffer) => console.log("data", JSON.stringify(String(chunk)))); + src.on("end", () => { + console.log("data end"); + resolve(); + }); + console.log("push a", src.push(Buffer.from("a"))); + console.log("push empty", src.push(Buffer.alloc(0))); + console.log("push b", src.push(Buffer.from("b"))); + src.push(null); + }); +} + +function paused(): void { + const src = new Readable({ read() {} }); + src.push(Buffer.from("x")); + src.push(Buffer.alloc(0)); + src.push(Buffer.from("y")); + console.log("readableLength", src.readableLength); + console.log("read", JSON.stringify(String(src.read()))); +} + +function objectMode(): Promise { + return new Promise((resolve) => { + const src = new Readable({ objectMode: true, read() {} }); + src.on("data", (chunk: Buffer) => console.log("object chunk length", chunk.length)); + src.on("end", () => { + console.log("object end"); + resolve(); + }); + src.push(Buffer.alloc(0)); + src.push(Buffer.from("z")); + src.push(null); + }); +} + +async function main(): Promise { + await lines(["a\nb", "", "c\n"]); + await lines(["", "", "only\n"]); + await data(); + paused(); + await objectMode(); + console.log("done"); +} + +main(); diff --git a/tests/corpus/7517-stream-kept-callbacks.ts b/tests/corpus/7517-stream-kept-callbacks.ts new file mode 100644 index 000000000..9b4067529 --- /dev/null +++ b/tests/corpus/7517-stream-kept-callbacks.ts @@ -0,0 +1,139 @@ +// Option callbacks that keep their `callback` argument: a Writable whose +// write() keeps every third callback and calls it later, a write that +// calls its callback and keeps it (`cb(); keep(cb)`) and calls it again +// after later writes (ERR_MULTIPLE_CALLBACK), and a Transform that calls +// back synchronously, from setImmediate, and from a promise. Every write +// and transform completes in Node's order. Every write and writev call +// receives the same callback, as in Node (a Set, a WeakSet and a WeakMap +// find it again); a transform call receives a new one each time. +import { Transform, Writable } from "node:stream"; + +type Done = (error?: Error | null) => void; + +const kept: Done[] = []; +const log: string[] = []; + +const deferred = new Writable({ + highWaterMark: 4, + write(chunk: Buffer, _encoding: string, callback: Done) { + const text = chunk.toString(); + if (Number(text) % 3 === 0) { + kept.push(callback); + log.push(`kept ${text}`); + setImmediate(() => { + const cb = kept.shift(); + log.push(`late ${text}`); + if (cb) cb(); + }); + return; + } + log.push(`write ${text}`); + callback(); + }, +}); +for (let i = 1; i <= 10; i++) { + deferred.write(String(i), () => log.push(`done ${i}`)); +} +deferred.end(() => { + console.log(log.join("\n")); + twice(); +}); + +function twice(): void { + let saved: Done | null = null; + const w = new Writable({ + write(chunk: Buffer, _encoding: string, callback: Done) { + const text = chunk.toString(); + console.log("twice write", text); + callback(); + if (text === "a") saved = callback; + }, + }); + w.on("error", (error: Error & { code?: string }) => { + console.log("twice error", error.code); + }); + w.write("a"); + w.write("b"); + w.write("c"); + setImmediate(() => { + const cb = saved; + if (cb) cb(); + setImmediate(transforms); + }); +} + +function transforms(): void { + let n = 0; + const t = new Transform({ + transform(chunk: Buffer, _encoding: string, callback: (error?: Error | null, data?: Buffer) => void) { + const k = n++; + const out = Buffer.from(`[${chunk.toString()}]`); + if (k % 3 === 0) callback(null, out); + else if (k % 3 === 1) setImmediate(() => callback(null, out)); + else Promise.resolve().then(() => callback(null, out)); + }, + }); + const parts: string[] = []; + t.on("data", (chunk: Buffer) => parts.push(chunk.toString())); + t.on("end", () => { + console.log("transform", parts.join("")); + weakKeys(); + }); + for (let i = 0; i < 9; i++) t.write(String(i)); + t.end(); +} + +function weakKeys(): void { + const seen = new WeakSet(); + const notes = new WeakMap(); + const held = new Set(); + const w = new Writable({ + write(chunk: Buffer, _encoding: string, callback: Done) { + const text = chunk.toString(); + console.log("weak write", text, seen.has(callback), notes.get(callback) ?? "none", held.has(callback)); + seen.add(callback); + notes.set(callback, text); + if (text === "q") held.add(callback); + callback(); + }, + }); + for (const text of ["p", "q", "r", "s"]) w.write(text); + w.end(batches); +} + +function batches(): void { + const seen = new WeakSet(); + let first: Done | null = null; + const w = new Writable({ + writev(chunks: Array<{ chunk: Buffer }>, callback: Done) { + const text = chunks.map((c) => c.chunk.toString()).join(""); + console.log("writev", text, seen.has(callback), first === callback); + seen.add(callback); + if (first === null) first = callback; + setImmediate(() => callback()); + }, + }); + w.write("a"); + w.write("b"); + w.write("c"); + w.end(weakTransform); +} + +function weakTransform(): void { + type TransformDone = (error?: Error | null, data?: Buffer) => void; + const seen = new WeakSet(); + const held = new Set(); + const t = new Transform({ + transform(chunk: Buffer, _encoding: string, callback: TransformDone) { + const text = chunk.toString(); + console.log("weak transform", text, seen.has(callback), held.has(callback)); + seen.add(callback); + if (text === "x") held.add(callback); + callback(null, chunk); + }, + }); + t.resume(); + t.on("end", () => console.log("weak transform end")); + for (const text of ["x", "y", "z"]) t.write(text); + t.end(); +} diff --git a/tests/corpus/7518-stream-dropped-callbacks.ts b/tests/corpus/7518-stream-dropped-callbacks.ts new file mode 100644 index 000000000..faf273d1c --- /dev/null +++ b/tests/corpus/7518-stream-dropped-callbacks.ts @@ -0,0 +1,54 @@ +// Streams dropped before they end, after their option callbacks ran: a +// Writable, an object-mode Transform read in part, and a Writable whose +// destroy() never calls back. Each stream keeps the last `callback` it +// passed, which holds the stream, so dropping one leaves a cycle; the +// sanitized lane's exit audit checks that it is reclaimed. +import { Readable, Transform, Writable } from "node:stream"; + +type Done = (error?: Error | null) => void; + +let written = 0; +function writeSome(n: number): void { + const w = new Writable({ + write(_chunk: Buffer, _encoding: string, callback: Done) { + written++; + callback(); + }, + }); + for (let i = 0; i < n; i++) w.write("x"); +} + +let transformed = 0; +function transformSome(): void { + const t = new Transform({ + objectMode: true, + transform(chunk: number, _encoding: string, callback: (error?: Error | null, data?: number) => void) { + transformed++; + callback(null, chunk * 2); + }, + }); + Readable.from([1, 2, 3, 4, 5, 6]).pipe(t); + t.once("data", (value: number) => console.log("first", value)); +} + +function destroyNoCallback(): void { + const w = new Writable({ + write(_chunk: Buffer, _encoding: string, callback: Done) { + callback(); + }, + destroy(_error: Error | null, _callback: Done) { + console.log("destroy called"); + }, + }); + w.write("y", () => { + w.destroy(); + console.log("destroyed", w.destroyed, w.closed); + }); +} + +for (let i = 0; i < 20; i++) writeSome(5); +transformSome(); +destroyNoCallback(); +setImmediate(() => { + setImmediate(() => console.log("written", written, "transformed", transformed)); +}); diff --git a/tests/corpus/7519-net-socket-drain-listener-close.ts b/tests/corpus/7519-net-socket-drain-listener-close.ts new file mode 100644 index 000000000..e1b51e7b4 --- /dev/null +++ b/tests/corpus/7519-net-socket-drain-listener-close.ts @@ -0,0 +1,62 @@ +// 'drain' listeners that capture their socket do not keep it after it +// closes: a once('drain') listener still pending when the peer disconnects +// before the buffer drains, and an on('drain') listener on a socket that +// drained and then closed. The sanitized lane's exit audit checks that +// both sockets are freed. +import { connect, createServer, Socket } from "node:net"; + +function disconnectBeforeDrain(): Promise { + return new Promise((resolve) => { + const server = createServer((socket: Socket) => { + socket.on("error", () => {}); + socket.once("drain", () => console.log("unexpected drain", socket.writableLength)); + console.log("server write", socket.write(Buffer.alloc(16 * 1024 * 1024, 97))); + socket.on("close", () => { + console.log("server socket closed, need drain", socket.writableNeedDrain); + server.close(() => resolve()); + }); + }); + server.listen(0, "127.0.0.1", () => { + const addr = server.address(); + const port = typeof addr === "object" && addr !== null ? addr.port : 0; + const client = connect(port, "127.0.0.1", () => { + client.pause(); + setTimeout(() => client.destroy(), 30); + }); + }); + }); +} + +function drainThenClose(): Promise { + return new Promise((resolve) => { + const server = createServer((socket: Socket) => { + socket.pause(); + setTimeout(() => socket.resume(), 30); + }); + server.listen(0, "127.0.0.1", () => { + const addr = server.address(); + const port = typeof addr === "object" && addr !== null ? addr.port : 0; + const client = connect(port, "127.0.0.1", () => { + let drains = 0; + client.on("drain", () => { + drains++; + console.log("drain", drains, client.writableLength); + client.end(); + }); + console.log("client write", client.write(Buffer.alloc(16 * 1024 * 1024, 98))); + }); + client.on("close", () => { + console.log("client closed"); + server.close(() => resolve()); + }); + }); + }); +} + +async function main(): Promise { + await disconnectBeforeDrain(); + await drainThenClose(); + console.log("done"); +} + +main(); diff --git a/tests/corpus/7520-util-format-runtime.ts b/tests/corpus/7520-util-format-runtime.ts new file mode 100644 index 000000000..d4a7f9ea6 --- /dev/null +++ b/tests/corpus/7520-util-format-runtime.ts @@ -0,0 +1,110 @@ +// util.format, util.formatWithOptions and console output with format +// strings known only at run time, spread arguments and options objects: +// every substitution, the argument cursor, %% and %c, missing and extra +// arguments, the space-joined form of strings without a '%', and Node's +// inspectOptions validation. +import * as util from "node:util"; + +class Point { + x: number; + y: number; + constructor(x: number, y: number) { + this.x = x; + this.y = y; + } +} + +interface Row { + id: number; + tags: string[]; + nested: { deep: { deeper: { deepest: number } } }; +} + +const formats: string[] = [ + "%s and %s", + "%d|%i|%f", + "%j", + "%O / %o", + "100%% of %s", + "%c%s%c", + "no specifiers", + "trailing %", + "%x %s", + "%s", +]; +const row: Row = { id: 7, tags: ["a", "b"], nested: { deep: { deeper: { deepest: 1 } } } }; + +for (const format of formats) { + console.log(util.format(format, "first", 42, row)); + console.log(util.format(format, 3.5, "12.75")); + console.log(util.format(format)); +} + +function pick(i: number): string { + return ["%s=%d", "%j", "%O", "plain"][i % 4]!; +} +for (let i = 0; i < 4; i++) { + console.log(util.format(pick(i), new Point(1, 2), -0, [1, 2, 3])); + console.log(pick(i), new Map([["k", new Point(3, 4)]]), new Set([1n, 2n])); + console.error(pick(i), "to stderr", null, undefined); +} + +const label: string = process.argv.length > 99 ? "" : "count: %d of %s"; +console.log(label, 5, "five", "extra"); +console.log(util.format(label, 10n, Symbol("sym"))); +console.log(util.format(label, new Point(0, 0), [new Point(5, 6)])); +console.log(util.format(pick(0), { toString: "not a function" }, true)); + +// Arguments that are all strings, numbers, booleans, null or undefined. +const prim: string = process.argv.length > 99 ? "" : "%s|%d|%i|%f|%j|%O|%o|%c|%%"; +console.log(prim, -0, "12.5", -0, true, 'q"uote', "s", 3, "css", "rest", null, undefined); +console.log(prim, NaN, null, undefined, "0x10", Infinity, NaN, false); +console.log(util.format(prim, "a\nb", " 7 ", "-3.9e2", "", -Infinity, 'it\'s', 1e21)); +console.log(util.format(label, 2.5)); + +// A union first argument formats only when it holds a string. +const maybe: string | number = process.argv.length > 99 ? 1 : "%s/%s"; +const notString: string | number = process.argv.length > 99 ? "%s" : 9; +console.log(util.format(maybe, "u", "v")); +console.log(util.format(notString, "u", "v")); +console.log(maybe, "w", "x"); + +// An unknown first argument. +const loose: unknown = process.argv.length > 99 ? 0 : "%s-%i"; +console.log(util.format(loose, "a", "7.9")); +console.log(loose, "b", 3.2); + +// Spread arguments. +const values: unknown[] = ["%s + %s = %d", 1, 2, 3, "tail"]; +console.log(util.format(...values)); +console.log(util.format("%s|%s", ...values)); +console.log(...values); +console.log("prefix", ...new Set(["x", "y"]), "suffix"); +console.error(...[1, "two", [3]]); +console.log(util.format(...([] as string[]))); + +// formatWithOptions with an options object. +const deep = { a: { b: { c: { d: { e: 1 } } } } }; +const depth = process.argv.length > 99 ? 1 : 0; +console.log(util.formatWithOptions({ depth }, "%O", deep)); +console.log(util.formatWithOptions({ depth: null }, deep)); +console.log(util.formatWithOptions({ depth: undefined }, "x", deep)); +console.log(util.formatWithOptions({ depth: 0 }, "%s|%O", deep, deep)); +console.log(util.formatWithOptions({ compact: false }, "%s", "v", { a: 1, b: [1, 2] })); +console.log(util.formatWithOptions({ breakLength: 20 }, { alpha: 1, beta: 2, gamma: 3 })); +console.log(util.formatWithOptions({ sorted: true }, "%O", { b: 1, a: 2, c: { z: 1, y: 2 } })); +console.log(util.formatWithOptions({ colors: true }, "%s %d %O", "s", 2, 3), "|"); +console.log(util.formatWithOptions({ colors: true }, 1, "plain", null, undefined, true, 4n)); +const options: util.InspectOptions = { depth: 1, compact: 1 }; +console.log(util.formatWithOptions(options, "%O", deep)); +console.log(util.formatWithOptions(options, ...values)); +console.log(util.formatWithOptions([] as unknown as util.InspectOptions, "%s", 1)); +const invalid: unknown[] = [null, 1, "x", undefined, () => {}]; +for (const bad of invalid) { + try { + util.formatWithOptions(bad as util.InspectOptions, "a"); + } catch (error) { + const e = error as NodeJS.ErrnoException; + console.log(e.name, e.code, e.message); + } +} diff --git a/tests/corpus/7521-util-format-runtime.cjs b/tests/corpus/7521-util-format-runtime.cjs new file mode 100644 index 000000000..559d09243 --- /dev/null +++ b/tests/corpus/7521-util-format-runtime.cjs @@ -0,0 +1,64 @@ +// util.format, util.formatWithOptions and console output over untyped +// values: format strings held in variables and parameters, spread +// arguments, %s over objects with and without their own toString, %d and +// %i coercions, %j over toJSON, and options objects built at run time. +"use strict"; +const util = require("util"); + +function log(message, ...rest) { + console.log(message, ...rest); +} + +function formatAll(format, values) { + return util.format(format, ...values); +} + +const messages = ["%s: %d items", "%s", "%i%%", "%j", "plain text", 42]; +for (const message of messages) { + log(message, "cart", "3.7", { total: 9 }); + console.log(formatAll(message, ["a", 1n])); +} + +const withToString = { + name: "custom", + toString() { + return `<${this.name}>`; + }, +}; +const inherited = Object.create(withToString); +inherited.name = "inherited"; +const fmt = "%s / %s / %s"; +console.log(util.format(fmt, withToString, inherited, [1, [2, [3]]])); +console.log(fmt, new Date(0), new Error("boom").message, null, Symbol("s")); + +const numeric = "%d %i %f"; +console.log(util.format(numeric, "0x1f", "12.9px", "1e3")); +console.log(util.format(numeric, { valueOf: () => 5 }, true, [])); +console.log(util.format(numeric, 7n, 8n, 9)); + +const json = "%j|%j|%j"; +const circular = { name: "loop" }; +circular.self = circular; +console.log(util.format(json, { toJSON: () => ({ replaced: 1 }) }, circular, undefined)); + +const options = {}; +options.depth = 0; +options.breakLength = 40; +console.log(util.formatWithOptions(options, "%O", { a: { b: 1 } }, ["x", { y: { z: 2 } }])); +options.depth = null; +options.unknownOption = true; +console.log(util.formatWithOptions(options, { a: { b: { c: { d: 1 } } } })); +console.log(util.formatWithOptions({ compact: 1 }, ...[{ k: [1, 2] }, "end"])); +for (const bad of [null, 0, "options", () => {}]) { + try { + util.formatWithOptions(bad, "x"); + } catch (error) { + console.log(error.name, error.code, error.message); + } +} + +const args = ["%s=%s", "key"]; +args.push("value", "rest"); +console.log(...args); +console.warn(...args); +console.log(util.format(...args.slice(1))); diff --git a/tests/corpus/7522-promisify-untyped.cjs b/tests/corpus/7522-promisify-untyped.cjs new file mode 100644 index 000000000..2e1476eac --- /dev/null +++ b/tests/corpus/7522-promisify-untyped.cjs @@ -0,0 +1,64 @@ +// util.promisify over JavaScript functions whose types are not known: +// arrows, function expressions, methods and functions chosen at run time. +// The callback's error rejects and its first value fulfills, a +// synchronous throw rejects, the call's receiver reaches the function, +// the wrapper keeps the original's name and length, and arguments that +// are not functions throw Node's ERR_INVALID_ARG_TYPE. +"use strict"; +const util = require("util"); + +function pick(kind) { + if (kind === "double") return (value, callback) => setImmediate(() => callback(null, value * 2)); + if (kind === "fail") return function failing(callback) { callback(new RangeError("failed")); }; + if (kind === "throw") return () => { throw new TypeError("thrown"); }; + return null; +} + +const store = { + prefix: "item", + read(key, callback) { + setTimeout(() => callback(null, `${this.prefix}:${key}`, "ignored"), 1); + }, +}; + +async function main() { + const double = util.promisify(pick("double")); + console.log("double:", await double(21), double.name, double.length); + try { + await util.promisify(pick("fail"))(); + } catch (error) { + console.log("rejected:", error.name, error.message); + } + try { + await util.promisify(pick("throw"))(); + } catch (error) { + console.log("sync:", error.name, error.message); + } + const read = util.promisify(store.read); + console.log("unbound:", await read.call(store, "a")); + store.readAsync = read; + console.log("method:", await store.readAsync("b")); + const bound = util.promisify(store.read.bind({ prefix: "bound" })); + console.log("bound:", await bound("c")); + const named = util.promisify(function lookup(a, b, callback) { + callback(null, [a, b]); + }); + console.log("named:", named.name, named.length, await named(1, 2)); + const values = []; + for (const n of [1, 2, 3]) values.push(await double(n)); + console.log("each:", values); + for (const value of [pick("none"), 42, "fn", {}, undefined]) { + try { + util.promisify(value); + } catch (error) { + console.log(error.name, error.code, error.message); + } + } + try { + util.promisify(); + } catch (error) { + console.log(error.code, error.message); + } +} + +main(); diff --git a/tests/corpus/7523-promisify-untyped.ts b/tests/corpus/7523-promisify-untyped.ts new file mode 100644 index 000000000..65d1d01c1 --- /dev/null +++ b/tests/corpus/7523-promisify-untyped.ts @@ -0,0 +1,38 @@ +// util.promisify over untyped functions in TypeScript: values typed `any` +// and functions whose parameters are `any` promisify at run time, and +// their promises settle with the callback's value (not the type of a +// builtin target's promisified form). +import { promisify } from "node:util"; + +const loose: any = (value: number, callback: (error: Error | null, result?: number) => void) => + callback(null, value + 1); +const looseAsync = promisify(loose); + +function legacy(a: any, b: any, callback: any): void { + setImmediate(() => (b === 0 ? callback(new RangeError("divide by zero")) : callback(null, a / b))); +} +const divide = promisify(legacy); + +const handlers: Record = { + echo: (text: string, callback: (error: null, value: string) => void) => callback(null, text), + later: (callback: (error: null, value: string[]) => void) => + setTimeout(() => callback(null, ["a", "b"]), 1), +}; + +async function main(): Promise { + console.log("loose:", await looseAsync(1)); + looseAsync(2).then((value: unknown) => console.log("then:", value)); + console.log("divide:", await divide(10, 4)); + try { + await divide(1, 0); + } catch (error) { + console.log("rejected:", (error as Error).message); + } + for (const name of ["echo", "later"]) { + const fn = promisify(handlers[name]); + console.log(`${name}:`, name === "echo" ? await fn("hi") : await fn()); + } + console.log("name:", divide.name, "length:", divide.length); +} + +main(); diff --git a/tests/corpus/7524-inspect-custom.ts b/tests/corpus/7524-inspect-custom.ts new file mode 100644 index 000000000..642ce75a9 --- /dev/null +++ b/tests/corpus/7524-inspect-custom.ts @@ -0,0 +1,91 @@ +// [util.inspect.custom] methods on classes: the method's string replaces +// the instance in util.inspect, console output and util.format, with +// Node's depth argument (negative past the depth limit, null for depth: +// null), options object and util.inspect; a multi-line result indents at +// its position, a returned value renders in the instance's place, and +// console.dir, customInspect: false, String() and JSON skip the method. +import * as util from "node:util"; +import { inspect } from "node:util"; + +class Money { + cents: number; + constructor(cents: number) { + this.cents = cents; + } + [inspect.custom](): string { + return `Money(${(this.cents / 100).toFixed(2)})`; + } +} + +class Probe { + label: string; + constructor(label: string) { + this.label = label; + } + [util.inspect.custom](depth: number | null, options: util.InspectOptionsStylized): string { + const keys = Object.keys(options).join(","); + return `Probe<${this.label} depth=${depth} optionsDepth=${options.depth} compact=${options.compact}> ${keys}`; + } +} + +class Lines { + [inspect.custom](): string { + return "first line\nsecond line"; + } +} + +class Point { + x: number; + y: number; + constructor(x: number, y: number) { + this.x = x; + this.y = y; + } + [inspect.custom](): { x: number; y: number; sum: number } { + return { x: this.x, y: this.y, sum: this.x + this.y }; + } +} + +class Wrapper { + inner: { values: number[]; nested: { deep: { deeper: number } } }; + constructor() { + this.inner = { values: [1, 2, 3], nested: { deep: { deeper: 4 } } }; + } + [inspect.custom](depth: number, options: util.InspectOptionsStylized, show: typeof inspect): string { + if (depth < 0) return options.stylize("[Wrapper]", "special"); + return `Wrapper ${show(this.inner, { ...options, depth: options.depth == null ? null : depth - 1 })}`; + } +} + +class Base { + name = "base"; + [inspect.custom](): string { + return `Custom(${this.name})`; + } +} +class Derived extends Base { + extra = 1; +} + +console.log(util.inspect(new Money(1234))); +console.log(new Money(5), [new Money(1), new Money(2)]); +console.log(util.inspect({ wallet: { cash: new Money(99) } })); +console.log(util.inspect(new Probe("top"))); +console.log(util.inspect({ a: { b: new Probe("nested") } })); +console.log(util.inspect([new Probe("past depth")], { depth: 0 })); +console.log(util.inspect(new Probe("unbounded"), { depth: null })); +console.log(util.inspect(new Probe("infinite"), { depth: Infinity })); +console.log(util.inspect([new Probe("undefined")], { depth: undefined })); +console.log(util.inspect(new Probe("compact"), { compact: false, breakLength: 40 })); +console.log(util.inspect({ key: new Lines(), list: [new Lines()] })); +console.log(util.inspect(new Map([["k", new Lines()]]))); +console.log(util.inspect(new Point(1, 2)), util.inspect({ p: new Point(3, 4) })); +console.log(util.inspect(new Wrapper()), util.inspect({ w: new Wrapper() }, { depth: 0 })); +console.log(util.inspect(new Derived()), util.inspect([new Base(), new Derived()])); +console.log(util.format("%s | %O", new Money(10), new Money(20))); +console.log("%s and %d", new Money(1), 2); +console.log(util.inspect(new Money(7), { customInspect: false })); +console.dir(new Money(8)); +console.log(String(new Money(9)), JSON.stringify(new Money(10))); +const values: unknown[] = [new Money(11), { money: new Money(12) }]; +console.log(values, util.inspect(values[1]), [new Lines(), values[0]]); diff --git a/tests/corpus/7525-inspect-custom.cjs b/tests/corpus/7525-inspect-custom.cjs new file mode 100644 index 000000000..9c7302fac --- /dev/null +++ b/tests/corpus/7525-inspect-custom.cjs @@ -0,0 +1,75 @@ +// [util.inspect.custom] in JavaScript: methods on classes and object +// literals (also keyed by Symbol.for('nodejs.util.inspect.custom')), the +// CommonJS call form (0, util_1.inspect)(value), results that are the +// object itself or another value, a non-function member under the key, +// and the options and util.inspect a method receives. +"use strict"; +const util_1 = require("util"); + +class Temperature { + constructor(degrees) { + this.degrees = degrees; + } + [util_1.inspect.custom](depth, options, inspect) { + return `Temperature<${this.degrees}> ${typeof options.stylize} ${inspect === util_1.inspect}`; + } +} + +const literal = { + id: 1, + [util_1.inspect.custom](depth) { + return `literal at depth ${depth}`; + }, +}; +const registered = { + [Symbol.for("nodejs.util.inspect.custom")]() { + return { replaced: [1, 2], nested: { x: 1 } }; + }, +}; +const self = { + v: 1, + [util_1.inspect.custom]() { + return this; + }, +}; +const notFunction = { x: 1, [util_1.inspect.custom]: "not a function" }; +const styled = { + [util_1.inspect.custom](depth, options) { + return options.stylize("styled", "special") + options.stylize(5, "number"); + }, +}; +const nesting = { + data: { a: { b: { c: 1 } } }, + [util_1.inspect.custom](depth, options, inspect) { + return `nesting ${inspect(this.data, { ...options, depth: 0 })}`; + }, +}; + +console.log((0, util_1.inspect)(new Temperature(21))); +console.log((0, util_1.inspect)({ t: new Temperature(22), list: [literal] })); +console.log(util_1.inspect([[literal]], { depth: 0 })); +console.log(util_1.inspect(registered), util_1.inspect({ r: registered })); +console.log(util_1.inspect(self), util_1.inspect(notFunction)); +console.log(util_1.inspect(styled), JSON.stringify(util_1.inspect(styled, { colors: true }))); +console.log(util_1.inspect(nesting), util_1.inspect({ n: nesting })); +console.log(literal, [registered]); +console.log((0, util_1.format)("%s / %O", literal, literal)); +console.log(util_1.inspect(literal, { customInspect: false })); +console.dir(literal); +console.log(util_1.inspect.custom === Symbol.for("nodejs.util.inspect.custom"), util_1.inspect.custom); + +const throwing = { + [util_1.inspect.custom]() { + throw new RangeError("from custom"); + }, +}; +try { + console.log([throwing]); +} catch (error) { + console.log("caught", error.name, error.message); +} +try { + util_1.inspect({ t: throwing }); +} catch (error) { + console.log("caught", error.message); +} diff --git a/tests/corpus/7526-inspect-options-runtime.ts b/tests/corpus/7526-inspect-options-runtime.ts new file mode 100644 index 000000000..b9f0c61d7 --- /dev/null +++ b/tests/corpus/7526-inspect-options-runtime.ts @@ -0,0 +1,62 @@ +// util.inspect with colors, option values and options objects known only +// at run time, and the legacy inspect(value, showHidden, depth, colors) +// form: Node's ANSI styles per value kind, color-aware line breaking and +// grouping, runtime depth and layout options, and colored output from +// util.formatWithOptions. +import * as util from "node:util"; + +class Point { + x: number; + y: number; + constructor(x: number, y: number) { + this.x = x; + this.y = y; + } +} + +const sparse: number[] = [1, 2, 3]; +sparse.length = 5; +const values: unknown[] = [ + 1, + -0, + "text", + "it's", + null, + undefined, + true, + 10n, + Symbol("sym"), + [1, "two", [3, [4, [5]]]], + { a: 1, "b-c": "d", nested: { deep: { deeper: { deepest: 1 } } } }, + new Map([["k", { v: 1 }]]), + new Set(["s", 2]), + new Date(0), + new Uint8Array([1, 2, 3]), + Buffer.from("hi"), + function named() {}, + new Point(1, 2), + sparse, + Object.create(null), + Array.from({ length: 30 }, (_, i) => i * 37), + "a long string with a newline\nand more text than fits on one line of eighty characters", +]; +for (const value of values) { + console.log(JSON.stringify(util.inspect(value, { colors: true }))); +} +const depth = process.argv.length > 99 ? 5 : 1; +const breakLength = process.argv.length > 99 ? 80 : 30; +console.log(util.inspect(values[10], { depth })); +console.log(util.inspect(values[10], { depth: depth - 1, breakLength })); +console.log(util.inspect(values[20], { breakLength, compact: depth })); +console.log(util.inspect({ b: 2, a: 1 }, { sorted: depth > 0 })); +console.log(util.inspect(values[10], { depth: undefined })); +const options: util.InspectOptions = { depth: 0, colors: true }; +console.log(JSON.stringify(util.inspect(values[10], options))); +options.colors = false; +console.log(util.inspect(values[10], options)); +console.log(util.inspect(values[10], false, 0)); +console.log(util.inspect(values[10], false, null, false)); +console.log(JSON.stringify(util.inspect(values[11], false, 1, true))); +console.log(util.inspect(values[10], undefined, depth)); +console.log(JSON.stringify(util.formatWithOptions({ colors: true }, "%s %O", "x", values[12]))); +console.log(JSON.stringify(util.formatWithOptions({ colors: true }, values[10], values[9]))); diff --git a/tests/corpus/7527-buffer-from-array-buffer.ts b/tests/corpus/7527-buffer-from-array-buffer.ts new file mode 100644 index 000000000..fb650e9f2 --- /dev/null +++ b/tests/corpus/7527-buffer-from-array-buffer.ts @@ -0,0 +1,33 @@ +// Buffer.from(arrayBuffer, byteOffset, length): Node's fromArrayBuffer over +// typed and untyped ArrayBuffers and SharedArrayBuffers: the offset and +// length conversions, the shared memory, zero and negative lengths, and +// ERR_BUFFER_OUT_OF_BOUNDS for an offset or length past the end. +const ab = new ArrayBuffer(6); +new Uint8Array(ab).set([1, 2, 3, 4, 5, 6]); +const offsets: unknown[] = [undefined, NaN, "1", 1.5, -1, 0, 6, 7, null, true]; +const lengths: unknown[] = [undefined, 0, -1, 2.5, "2", 6, 9, NaN, null]; +for (const o of offsets) { + for (const l of lengths) { + try { + const b = Buffer.from(ab, o as number, l as number); + console.log(String(o), String(l), b, b.byteOffset, b.length); + } catch (e) { + const err = e as NodeJS.ErrnoException; + console.log(String(o), String(l), err.name, err.code, err.message); + } + } +} +const typed = Buffer.from(ab, 2, 2); +typed[0] = 99; +console.log(new Uint8Array(ab)[2], Buffer.from(ab, 4).toString("hex"), Buffer.isBuffer(Buffer.from(ab, 1, 1))); +const shared = new SharedArrayBuffer(4); +console.log(Buffer.from(shared, 1, 2).length); +const loose: any = new ArrayBuffer(4); +const pairs: [unknown, unknown][] = [[1, 2], [4, undefined], [5, 1], [0, 5], ["2", "1"]]; +for (const [o, l] of pairs) { + try { + console.log(Buffer.from(loose, o as number, l as number).toString("hex")); + } catch (e) { + console.log((e as NodeJS.ErrnoException).code, (e as Error).message); + } +} diff --git a/tests/corpus/7528-buffer-from-array-buffer.cjs b/tests/corpus/7528-buffer-from-array-buffer.cjs new file mode 100644 index 000000000..f3d185b1a --- /dev/null +++ b/tests/corpus/7528-buffer-from-array-buffer.cjs @@ -0,0 +1,46 @@ +// Buffer.from(arrayBuffer, byteOffset, length) in JavaScript, with offsets +// and lengths of every kind and an ArrayBuffer held in an untyped slot. +const ab = new ArrayBuffer(6); +new Uint8Array(ab).set([1, 2, 3, 4, 5, 6]); +const offsets = [undefined, NaN, "1", 1.5, -1, 0, 6, 7, null, true]; +const lengths = [undefined, 0, -1, 2.5, "2", 6, 9, NaN, null]; +for (const o of offsets) { + for (const l of lengths) { + try { + const b = Buffer.from(ab, o, l); + console.log(String(o), String(l), b, b.byteOffset, b.length); + } catch (e) { + const err = e; + console.log(String(o), String(l), err.name, err.code, err.message); + } + } +} +const typed = Buffer.from(ab, 2, 2); +typed[0] = 99; +console.log(new Uint8Array(ab)[2], Buffer.from(ab, 4).toString("hex"), Buffer.isBuffer(Buffer.from(ab, 1, 1))); +const shared = new SharedArrayBuffer(4); +console.log(Buffer.from(shared, 1, 2).length); +const slots = { buffer: new ArrayBuffer(3) }; +new Uint8Array(slots.buffer).set([9, 8, 7]); +for (const args of [[1], [1, 1], [0, 0], [3, 0], [2, 5], [-0.5, 2]]) { + try { + console.log(args.join(","), Buffer.from(slots.buffer, args[0], args[1])); + } catch (e) { + console.log(args.join(","), e.code, e.message); + } +} +// A string's second argument is its encoding, with or without a third. +const encoded = JSON.parse('"aGk="'); +console.log( + Buffer.from(encoded, "base64", 1).toString(), + Buffer.from(encoded, "latin1", 2).length, + Buffer.from(encoded, undefined, 3).toString(), + Buffer.from(encoded, "", 3).toString(), +); +for (const text of [encoded, JSON.parse('""')]) { + try { + console.log(Buffer.from(text, "bogus", 1)); + } catch (e) { + console.log(e.code, e.message); + } +} diff --git a/tests/corpus/7529-buffer-constructor.cjs b/tests/corpus/7529-buffer-constructor.cjs new file mode 100644 index 000000000..3181cebec --- /dev/null +++ b/tests/corpus/7529-buffer-constructor.cjs @@ -0,0 +1,80 @@ +// The deprecated Buffer constructor, called and constructed, as Node's +// Buffer function runs it: the arguments evaluate in order, the first call +// reports DEP0005 on the next tick (once), a number allocates zero-filled +// bytes, and other values convert as Buffer.from does. +// The program re-runs itself and prints the child's output with process +// ids normalized. +"use strict"; +const { spawnSync } = require("node:child_process"); +const { Buffer: Imported } = require("buffer"); + +function child() { + process.on("warning", (w) => console.log("listener", w.name, w.code, w.message.length)); + const B = Buffer; + + const show = (label, fn) => { + try { + const b = fn(); + console.log(label, b, b.length, Buffer.isBuffer(b)); + } catch (e) { + console.log(label, e.name, e.code, e.message); + } + }; + + // The throwing form still reports the warning, after the catch. + show("number+encoding", () => new Buffer(3, "utf8")); + show("size", () => new Buffer(4)); + show("fraction", () => new Buffer(2.7)); + show("zero", () => Buffer(0)); + show("negative", () => new Buffer(-1)); + show("nan", () => new Buffer(NaN)); + show("string", () => new Buffer("héllo")); + show("hex", () => new Buffer("6869", "hex")); + show("base64", () => Buffer("aGk=", "base64")); + show("empty encoding", () => new Buffer("x", "")); + show("bad encoding", () => new Buffer("x", "nope")); + show("array", () => new Buffer([1, 2, 300, -1])); + show("array-like", () => new Buffer({ length: 2, 0: 7, 1: 8 })); + show("buffer", () => new B(Buffer.from("copy"))); + show("typed", () => Buffer(new Uint16Array([1, 258]))); + const storage = new Uint8Array([10, 20, 30, 40, 50]).buffer; + show("arraybuffer", () => new Buffer(storage)); + show("offset", () => new Buffer(storage, 1)); + show("range", () => Buffer(storage, 1, 3)); + show("range past end", () => new Buffer(storage, 6)); + show("string third", () => new Buffer("abc", "latin1", 1)); + show("undefined", () => new Buffer(undefined)); + show("null", () => Buffer(null)); + show("none", () => new Buffer()); + show("imported", () => new Imported("imp")); + + console.log(Buffer(2), Buffer("ab").length, Buffer("hi").toString("hex"), new Buffer([5])); + console.log(Buffer.isBuffer(Buffer(1)), Buffer(1) instanceof Uint8Array, new Buffer(1) instanceof Uint8Array); + + // The view shares the ArrayBuffer's memory. + const view = new Buffer(storage, 2, 2); + new Uint8Array(storage)[2] = 99; + console.log(view); + + const order = []; + const arg = (v) => (order.push(v), v); + new Buffer(arg("a"), arg("utf8")); + Buffer(arg(2)); + console.log(order); + + console.log("sync end"); + process.nextTick(() => console.log("tick")); + setTimeout(() => { + new Buffer(1); + console.log("timer"); + }, 0); +} + +if (process.argv[2] === "child") { + child(); +} else { + const run = spawnSync(process.execPath, [process.argv[1], "child"], { encoding: "utf8" }); + console.log(`status: ${run.status}`); + console.log(run.stdout); + console.log(run.stderr.replace(/\(node:\d+\)/g, "(node:)")); +} diff --git a/tests/corpus/7530-buffer-constructor.ts b/tests/corpus/7530-buffer-constructor.ts new file mode 100644 index 000000000..14eac61d5 --- /dev/null +++ b/tests/corpus/7530-buffer-constructor.ts @@ -0,0 +1,42 @@ +// The deprecated `new Buffer(...)` in TypeScript, over the forms +// @types/node declares: strings with an encoding, sizes, arrays and +// ArrayBuffers. The first call reports DEP0005 on the next tick. +// The program re-runs itself and prints the child's output with process +// ids normalized. +import { spawnSync } from "node:child_process"; + +function child(): void { + process.on("warning", (w) => console.log("listener", w.name, (w as { code?: string }).code)); + + function sized(n: number): Buffer { + return new Buffer(n); + } + + const encodings: BufferEncoding[] = ["utf8", "hex", "base64", "latin1"]; + for (const encoding of encodings) { + console.log(encoding, new Buffer("6869", encoding)); + } + console.log(sized(3), sized(0).length); + console.log(new Buffer([104, 105]).toString(), new Buffer(new Uint8Array([1, 2]))); + const storage = new ArrayBuffer(4); + const view = new Buffer(storage); + view[1] = 5; + console.log(new Uint8Array(storage), view.buffer === storage); + const text: string = "typed"; + console.log(new Buffer(text).toString("hex")); + try { + sized(-2); + } catch (e) { + console.log((e as Error).name, (e as { code?: string }).code); + } + console.log("sync end"); +} + +if (process.argv[2] === "child") { + child(); +} else { + const run = spawnSync(process.execPath, [process.argv[1]!, "child"], { encoding: "utf8" }); + console.log(`status: ${run.status}`); + console.log(run.stdout); + console.log(run.stderr.replace(/\(node:\d+\)/g, "(node:)")); +} diff --git a/tests/corpus/7531-buffer-constructor-dependency/main.cjs b/tests/corpus/7531-buffer-constructor-dependency/main.cjs new file mode 100644 index 000000000..75b130bc6 --- /dev/null +++ b/tests/corpus/7531-buffer-constructor-dependency/main.cjs @@ -0,0 +1,36 @@ +// The deprecated Buffer constructor called from a dependency under +// node_modules: Node reports no DEP0005 for those calls, and the program's +// own first call reports it once, on the next tick. +// The program re-runs itself and prints the child's output with process +// ids normalized. +"use strict"; +const { spawnSync } = require("node:child_process"); +const legacy = require("legacy-buf"); + +function child() { + process.on("warning", (w) => console.log("listener", w.name, w.code)); + + console.log(legacy.construct("hi", "utf8"), legacy.construct(3), legacy.call([1, 2])); + const storage = new Uint8Array([1, 2, 3, 4]).buffer; + console.log(legacy.construct(storage, 1, 2), legacy.many(5)); + try { + legacy.construct(4, "hex"); + } catch (e) { + console.log(e.code); + } + process.nextTick(() => { + console.log("tick"); + console.log(new Buffer("own")); + console.log(Buffer(2), new Buffer([9])); + setTimeout(() => console.log("timer"), 0); + }); +} + +if (process.argv[2] === "child") { + child(); +} else { + const run = spawnSync(process.execPath, [process.argv[1], "child"], { encoding: "utf8" }); + console.log(`status: ${run.status}`); + console.log(run.stdout); + console.log(run.stderr.replace(/\(node:\d+\)/g, "(node:)")); +} diff --git a/tests/corpus/7531-buffer-constructor-dependency/node_modules/legacy-buf/index.d.ts b/tests/corpus/7531-buffer-constructor-dependency/node_modules/legacy-buf/index.d.ts new file mode 100644 index 000000000..ef314d625 --- /dev/null +++ b/tests/corpus/7531-buffer-constructor-dependency/node_modules/legacy-buf/index.d.ts @@ -0,0 +1,3 @@ +export declare function construct(value: unknown, encodingOrOffset?: unknown, length?: unknown): Buffer; +export declare function call(value: unknown): Buffer; +export declare function many(count: number): number; diff --git a/tests/corpus/7531-buffer-constructor-dependency/node_modules/legacy-buf/index.js b/tests/corpus/7531-buffer-constructor-dependency/node_modules/legacy-buf/index.js new file mode 100644 index 000000000..062e4e41d --- /dev/null +++ b/tests/corpus/7531-buffer-constructor-dependency/node_modules/legacy-buf/index.js @@ -0,0 +1,15 @@ +"use strict"; +// A dependency that keeps the deprecated Buffer constructor. +exports.construct = function (value, encodingOrOffset, length) { + if (length !== undefined) return new Buffer(value, encodingOrOffset, length); + if (encodingOrOffset !== undefined) return new Buffer(value, encodingOrOffset); + return new Buffer(value); +}; +exports.call = function (value) { + return Buffer(value); +}; +exports.many = function (count) { + let total = 0; + for (let i = 0; i < count; i++) total += new Buffer(1).length; + return total; +}; diff --git a/tests/corpus/7531-buffer-constructor-dependency/node_modules/legacy-buf/package.json b/tests/corpus/7531-buffer-constructor-dependency/node_modules/legacy-buf/package.json new file mode 100644 index 000000000..db2277b60 --- /dev/null +++ b/tests/corpus/7531-buffer-constructor-dependency/node_modules/legacy-buf/package.json @@ -0,0 +1,5 @@ +{ + "name": "legacy-buf", + "version": "1.0.0", + "main": "index.js" +} diff --git a/tests/corpus/7532-buffer-constructor-check-limit/main.cjs b/tests/corpus/7532-buffer-constructor-check-limit/main.cjs new file mode 100644 index 000000000..c08690a83 --- /dev/null +++ b/tests/corpus/7532-buffer-constructor-check-limit/main.cjs @@ -0,0 +1,10 @@ +// Node checks the caller of the deprecated Buffer constructor for its first +// 10000 calls only: once a dependency under node_modules has made them, the +// program's own calls report no DEP0005. +"use strict"; +process.on("warning", (w) => console.log("listener", w.code)); +const legacy = require("legacy-buf"); + +console.log(legacy.many(10000)); +console.log(new Buffer("after"), Buffer(1)); +setTimeout(() => console.log("timer"), 0); diff --git a/tests/corpus/7532-buffer-constructor-check-limit/node_modules/legacy-buf/index.d.ts b/tests/corpus/7532-buffer-constructor-check-limit/node_modules/legacy-buf/index.d.ts new file mode 100644 index 000000000..ef314d625 --- /dev/null +++ b/tests/corpus/7532-buffer-constructor-check-limit/node_modules/legacy-buf/index.d.ts @@ -0,0 +1,3 @@ +export declare function construct(value: unknown, encodingOrOffset?: unknown, length?: unknown): Buffer; +export declare function call(value: unknown): Buffer; +export declare function many(count: number): number; diff --git a/tests/corpus/7532-buffer-constructor-check-limit/node_modules/legacy-buf/index.js b/tests/corpus/7532-buffer-constructor-check-limit/node_modules/legacy-buf/index.js new file mode 100644 index 000000000..062e4e41d --- /dev/null +++ b/tests/corpus/7532-buffer-constructor-check-limit/node_modules/legacy-buf/index.js @@ -0,0 +1,15 @@ +"use strict"; +// A dependency that keeps the deprecated Buffer constructor. +exports.construct = function (value, encodingOrOffset, length) { + if (length !== undefined) return new Buffer(value, encodingOrOffset, length); + if (encodingOrOffset !== undefined) return new Buffer(value, encodingOrOffset); + return new Buffer(value); +}; +exports.call = function (value) { + return Buffer(value); +}; +exports.many = function (count) { + let total = 0; + for (let i = 0; i < count; i++) total += new Buffer(1).length; + return total; +}; diff --git a/tests/corpus/7532-buffer-constructor-check-limit/node_modules/legacy-buf/package.json b/tests/corpus/7532-buffer-constructor-check-limit/node_modules/legacy-buf/package.json new file mode 100644 index 000000000..db2277b60 --- /dev/null +++ b/tests/corpus/7532-buffer-constructor-check-limit/node_modules/legacy-buf/package.json @@ -0,0 +1,5 @@ +{ + "name": "legacy-buf", + "version": "1.0.0", + "main": "index.js" +} diff --git a/tests/corpus/7533-buffer-value.cjs b/tests/corpus/7533-buffer-value.cjs new file mode 100644 index 000000000..3fd500ec2 --- /dev/null +++ b/tests/corpus/7533-buffer-value.cjs @@ -0,0 +1,101 @@ +// The Buffer function read as a value: one identity across the global, the +// buffer module and its aliases, Node's own enumerable members in Node's +// order, statics that apply Node's argument handling, instanceof, and a +// poolSize property the pool reads. +"use strict"; +const buffer = require("buffer"); +const nodeBuffer = require("node:buffer"); +const util = require("util"); + +// bn.js keeps the module's Buffer in a variable assigned under try. +var BnBuffer; +try { + BnBuffer = require("buffer").Buffer; +} catch (e) {} +const pass = (value) => value; +const Stored = pass(require("buffer").Buffer); +const Alias = Buffer; +const table = { Buffer: buffer.Buffer }; + +console.log(typeof Stored, Stored.name, Stored.length, Alias.name, Alias.length); +console.log(Stored === Buffer, Stored === globalThis.Buffer, Stored === buffer.Buffer, Stored === nodeBuffer.Buffer); +console.log(Stored === Alias, table.Buffer === Stored, Stored.constructor === Function); +console.log(Stored); +console.log(Object.keys(Stored)); +console.log(util.inspect({ nested: { Buffer: Stored } }, { depth: 1 })); + +// Statics through the value. +const show = (label, run) => { + try { + console.log(label, run()); + } catch (e) { + console.log(label, e.name, e.code, e.message); + } +}; +show("from string", () => Stored.from("héllo")); +show("from hex", () => Stored.from("6869", "hex")); +show("from empty encoding", () => Stored.from("x", "")); +show("from bad encoding", () => Stored.from("x", "nope")); +show("from array", () => Stored.from([1, 2, 300])); +const storage = new Uint8Array([1, 2, 3, 4, 5]).buffer; +show("from range", () => Stored.from(storage, 1, 3)); +show("from number", () => Stored.from(5)); +show("alloc", () => Stored.alloc(3, "ab")); +show("alloc fill", () => Stored.alloc(2, 7)); +show("alloc string size", () => Stored.alloc("3")); +show("allocUnsafe", () => Stored.allocUnsafe(4).fill(9)); +show("allocUnsafe negative", () => Stored.allocUnsafe(-1)); +show("allocUnsafe string", () => Stored.allocUnsafe("4")); +show("allocUnsafeSlow", () => Stored.allocUnsafeSlow(2).fill(1)); +show("isBuffer", () => [Stored.isBuffer(Buffer.alloc(1)), Stored.isBuffer(new Uint8Array(1)), Stored.isBuffer("x")]); +show("isEncoding", () => [Stored.isEncoding("UTF-8"), Stored.isEncoding(""), Stored.isEncoding("nope"), Stored.isEncoding(1)]); +show("compare", () => Stored.compare(Buffer.from("a"), Buffer.from("b"))); +show("compare bad", () => Stored.compare("a", Buffer.from("b"))); +show("concat", () => Stored.concat([Buffer.from("ab"), Buffer.from("cd")])); +show("concat length", () => Stored.concat([Buffer.from("ab"), Buffer.from("cd")], 3)); +show("concat empty", () => Stored.concat([], "x")); +show("concat bad length", () => Stored.concat([Buffer.from("a")], "x")); +show("concat bad list", () => Stored.concat("x")); +show("byteLength", () => [Stored.byteLength("héllo"), Stored.byteLength("aGk=", "base64"), Stored.byteLength(new Uint16Array(3))]); + +// Member values keep one identity, with Node's names and lengths. +const members = [ + [Buffer.from, Stored.from], + [Buffer.alloc, Stored.alloc], + [Buffer.allocUnsafe, Stored.allocUnsafe], + [Buffer.isBuffer, Stored.isBuffer], + [Buffer.compare, Stored.compare], + [Buffer.concat, Stored.concat], + [Buffer.byteLength, Stored.byteLength], + [Buffer.isEncoding, Stored.isEncoding], +]; +console.log(members.map(([a, b]) => `${a.name}/${a.length}/${a === b}`).join(" ")); +const isBuffer = require("buffer").Buffer.isBuffer; +console.log(isBuffer(Buffer.from("x")), isBuffer(null), typeof Buffer.allocUnsafe); + +// A bn.js-style allocator over Buffer or Array. +function allocate(ArrayType, size) { + if (ArrayType.allocUnsafe) return ArrayType.allocUnsafe(size); + return new ArrayType(size); +} +function toArrayLike(ArrayType, size) { + const made = allocate(ArrayType, size); + made.fill(4); + return made; +} +function toBuffer(size) { + return BnBuffer ? toArrayLike(BnBuffer, size) : null; +} +const made = toBuffer(3); +console.log(made, toArrayLike(Array, 2), made instanceof Stored, made instanceof Buffer); +console.log(new Uint8Array(2) instanceof Stored, Buffer.from("x") instanceof Alias, pass("x") instanceof Stored); + +// poolSize: one setting through the value and the static spelling. +console.log(Stored.poolSize, Buffer.poolSize); +Buffer.poolSize = 1024; +console.log(Stored.poolSize); +const small = Buffer.allocUnsafe(600); +console.log(small.buffer.byteLength, Buffer.allocUnsafe(400).buffer.byteLength); +Stored.poolSize = 2048; +console.log(Buffer.poolSize, Buffer.allocUnsafe(600).buffer.byteLength); +Stored.poolSize = 65536; diff --git a/tests/corpus/7534-buffer-prototype-members.cjs b/tests/corpus/7534-buffer-prototype-members.cjs new file mode 100644 index 000000000..2fd347373 --- /dev/null +++ b/tests/corpus/7534-buffer-prototype-members.cjs @@ -0,0 +1,29 @@ +// Buffer.prototype members read as values, as smart-buffer probes and +// calls them: own methods are functions with Node's names and lengths that +// run on a Buffer receiver, the parent and offset getters read on the +// prototype answer undefined, constructor is Buffer, and absent names are +// undefined. +"use strict"; +const buffer_1 = require("buffer"); + +function check(method) { + if (typeof buffer_1.Buffer.prototype[method] === "undefined") return `${method}: missing`; + return `${method}: ${typeof buffer_1.Buffer.prototype[method]}`; +} +console.log(["writeBigInt64BE", "readBigUInt64LE", "readUInt8", "nope", "parent", "offset"].map(check)); +console.log(Buffer.prototype.constructor === Buffer, Buffer.prototype["constructor"] === buffer_1.Buffer); +console.log(Buffer.prototype[0], Buffer.prototype[undefined], Buffer.prototype.toJSON === Buffer.prototype.toJSON); + +const names = ["readUInt8", "readUint8", "readFloatLE", "writeUInt16LE", "readUIntLE", "toString", "toLocaleString", "slice", "hexSlice"]; +console.log(names.map((n) => `${n}=${JSON.stringify(Buffer.prototype[n].name)}/${Buffer.prototype[n].length}`).join(" ")); + +const buf = Buffer.alloc(8); +function write(func, value, offset) { + return func.call(buf, value, offset); +} +console.log(write(Buffer.prototype.writeUInt16LE, 513, 0), write(Buffer.prototype.writeInt8, -2, 2), buf); +console.log(Buffer.prototype.readUInt16LE.call(buf, 0), Buffer.prototype.readInt8.call(buf, 2)); +console.log(Buffer.prototype.toString.call(buf, "hex", 0, 3), Buffer.prototype.toLocaleString.call(Buffer.from("hi"))); +console.log(Buffer.prototype.slice.call(buf, 0, 2), Buffer.prototype.equals.call(buf, Buffer.alloc(8))); +console.log(Buffer.prototype.fill.call(Buffer.alloc(3), 1), Buffer.prototype.indexOf.call(Buffer.from("abc"), "c")); +console.log(Buffer.prototype.write.call(buf, "zz", 4), buf, Buffer.prototype.toJSON.call(Buffer.from([1]))); diff --git a/tests/corpus/7535-inspect-function-properties.cjs b/tests/corpus/7535-inspect-function-properties.cjs new file mode 100644 index 000000000..5e3f8c6a6 --- /dev/null +++ b/tests/corpus/7535-inspect-function-properties.cjs @@ -0,0 +1,29 @@ +// util.inspect of functions with own enumerable properties, as Node's +// formatRaw prints them: the function's base then its keys (symbols +// included), `[Function]` past the depth limit, and the plain base when +// every own property is hidden. +"use strict"; +const util = require("util"); + +function named() {} +named.x = 1; +const arrow = () => {}; +arrow.a = "s"; +arrow[Symbol("k")] = 2; +Object.defineProperty(arrow, "hidden", { value: 3, enumerable: false }); +function hiddenOnly() {} +Object.defineProperty(hiddenOnly, "secret", { value: 1, enumerable: false }); +function big() {} +for (let i = 0; i < 8; i++) big["key" + i] = i; +function self() {} +self.me = self; + +const pass = (value) => value; + +console.log(named, arrow, hiddenOnly); +console.log(util.inspect(named, { depth: 0 })); +console.log(util.inspect(pass({ wrap: named }), { depth: 0 })); +console.log(util.inspect(pass([named, arrow]))); +console.log(util.inspect(big)); +console.log(util.inspect(self)); +console.log(util.inspect(pass({ a: { b: { f: big } } }))); diff --git a/tests/corpus/7536-inspect-custom-errors.ts b/tests/corpus/7536-inspect-custom-errors.ts new file mode 100644 index 000000000..affdd587b --- /dev/null +++ b/tests/corpus/7536-inspect-custom-errors.ts @@ -0,0 +1,23 @@ +// [util.inspect.custom] on Error subclasses: console.log, util.inspect and +// %O call the method for typed values, values held as unknown and errors +// inside containers; a subclass without its own method inherits it, and +// errors without one print as before. +import * as util from "node:util"; + +class Tagged extends Error { + [util.inspect.custom](depth: number): string { + return `Tagged<${this.message}> at depth ${depth}`; + } +} +class Nested extends Tagged {} +class Plain extends Error {} + +const tagged = new Tagged("a"); +console.log(tagged); +console.log(util.inspect(tagged), util.format("%O", new Nested("b"))); +const held: unknown = new Nested("c"); +console.log(held); +const list: unknown[] = [tagged, { inner: new Tagged("d") }]; +console.log(list); +const plain: unknown = new Plain("plain"); +console.log(String(plain), plain instanceof Error); diff --git a/tests/corpus/7537-inspect-custom-errors.cjs b/tests/corpus/7537-inspect-custom-errors.cjs new file mode 100644 index 000000000..3d17f985a --- /dev/null +++ b/tests/corpus/7537-inspect-custom-errors.cjs @@ -0,0 +1,26 @@ +// An Effect-style error class in JavaScript: a method keyed by +// Symbol.for("nodejs.util.inspect.custom") on an Error subclass, called by +// console.log and util.inspect for direct, caught and contained values, +// and the class still constructs and throws when nothing inspects it. +"use strict"; +const util = require("util"); +const NodeInspectSymbol = Symbol.for("nodejs.util.inspect.custom"); + +class YieldableError extends Error { + toJSON() { + return { _tag: "YieldableError", message: this.message }; + } + [NodeInspectSymbol]() { + return `YieldableError: ${this.message}`; + } +} + +const direct = new YieldableError("direct"); +console.log(direct, direct instanceof Error, JSON.stringify(direct)); +try { + throw new YieldableError("boom"); +} catch (e) { + console.log(e instanceof YieldableError, e.message); + console.log(e); + console.log(util.inspect([e]), util.inspect({ e })); +} diff --git a/tests/corpus/7538-inspect-custom-prototypes.mjs b/tests/corpus/7538-inspect-custom-prototypes.mjs new file mode 100644 index 000000000..7a4bfb1a4 --- /dev/null +++ b/tests/corpus/7538-inspect-custom-prototypes.mjs @@ -0,0 +1,44 @@ +// [util.inspect.custom] methods the runtime inspect walk calls: a class +// method returning an object (Effect's `return this.toJSON()`) that no +// typed code inspects, and a method an Effect-style class inherits from a +// constructor function's prototype object. +const NodeInspectSymbol = Symbol.for("nodejs.util.inspect.custom"); + +class Exit { + constructor(value) { + this.value = value; + } + toJSON() { + return { _id: "Exit", value: this.value }; + } + [NodeInspectSymbol]() { + return this.toJSON(); + } +} +const exits = [new Exit(1), new Exit("two")]; +console.log(JSON.stringify(exits)); +console.log(exits); + +const CommitPrototype = { + toJSON() { + return { _id: "Commit" }; + }, + [NodeInspectSymbol]() { + return this.toJSON(); + }, +}; +const Base = (function () { + function Base() {} + Base.prototype = CommitPrototype; + return Base; +})(); +class Effectable extends Base {} +class RefImpl extends Effectable { + constructor(ref) { + super(); + this.ref = ref; + } +} +const ref = new RefImpl(3); +console.log(ref.ref); +console.log(ref, [ref]); diff --git a/tests/corpus/7580-int32-arrays-across-calls.ts b/tests/corpus/7580-int32-arrays-across-calls.ts new file mode 100644 index 000000000..df180b264 --- /dev/null +++ b/tests/corpus/7580-int32-arrays-across-calls.ts @@ -0,0 +1,166 @@ +// Number arrays of proven int32s that reach other functions: passed to +// helpers called directly, along chains of helpers and recursion, and held +// in module globals that several functions read. A helper that also +// receives an array of other numbers or one that is not a fresh array, +// stores a fraction through its parameter, or escapes as a value keeps +// every array it can reach in doubles, as does a global an async function +// reads. Each case uses its own arrays and helpers. + +const data: number[] = []; +let seed = 7; +for (let i = 0; i < 2000; i++) { + seed = (Math.imul(seed, 48271) + 11) & 0x7fffffff; + data.push(seed % 1000); +} + +function fnv(values: number[]): number { + let h = 2166136261; + for (let i = 0; i < values.length; i++) { + h ^= values[i]! & 0xff; + h = Math.imul(h, 16777619) >>> 0; + } + return h; +} + +function histogram(values: number[], buckets: number): number[] { + const counts: number[] = new Array(buckets).fill(0); + for (let i = 0; i < values.length; i++) { + const b = values[i]! % buckets; + counts[b] = counts[b]! + 1; + } + return counts; +} + +// A helper that writes through its parameter and calls another helper. +function scale(values: number[], factor: number): number { + for (let i = 0; i < values.length; i++) values[i] = (values[i]! * factor) & 0xffff; + return sum(values); +} + +function sum(values: number[]): number { + let s = 0; + for (let i = 0; i < values.length; i++) s = (s + values[i]!) | 0; + return s; +} + +// Recursion passes the array on, and the elements index another array. +function chase(next: number[], at: number, steps: number): number { + if (steps === 0) return at; + return chase(next, next[at]!, steps - 1); +} + +function pushAll(target: number[], count: number): void { + for (let i = 0; i < count; i++) target.push((i * 3) & 0xffff); +} + +// `n` is a power of two: the masked elements are indexes of `perm`. +function locals(n: number): string { + const perm: number[] = []; + for (let i = 0; i < n; i++) perm.push((i * 37 + 11) & (n - 1)); + const copy: number[] = []; + pushAll(copy, n); + const s1 = scale(copy, 5); + return `locals ${chase(perm, 0, 1000)} ${s1} ${copy[n - 1]} ${copy.length} ${sum(perm)}`; +} + +// Read only through the element reader (`a[i] ?? 0`) and states. +function readSparse(values: number[], at: number): string { + return `${values[at] ?? -1} ${at in values} ${values.length}`; +} + +// The same helper receives an array of other numbers too: the arrays it +// can reach keep doubles. +function mean(values: number[]): number { + let s = 0; + for (let i = 0; i < values.length; i++) s += values[i]!; + return s / values.length; +} + +// A store through the parameter that is not an int32. +function halve(values: number[]): void { + for (let i = 0; i < values.length; i++) values[i] = values[i]! / 2; +} + +// A function used as a value keeps its parameters. +function first(values: number[]): number { + return values[0]!; +} + +let reassigned: number[] = [1, 2, 3]; +function regrow(n: number): number { + reassigned = []; + for (let i = 0; i < n; i++) reassigned.push(i & 15); + let s = 0; + for (let i = 0; i < reassigned.length; i++) s += reassigned[i]!; + return s; +} + +// Not a fresh array: the result of a call. +function total(values: number[]): number { + let s = 0; + for (let i = 0; i < values.length; i++) s += values[i]!; + return s; +} + +const awaited: number[] = [4, 5, 6]; +async function later(): Promise { + return awaited[0]! + awaited.length; +} + +// A helper whose record result its callers only project: the backend may +// call its result-slot entry instead, so its parameter keeps doubles. +interface Span { + lo: number; + hi: number; +} +function spanAt(values: number[], i: number): Span { + return { lo: values[i]!, hi: values[i + 1]! }; +} +const spans: number[] = [5, 9, 2, 7, 11]; +function spanTotal(): number { + let total = 0; + for (let i = 0; i < 4; i++) { + const span = spanAt(spans, i); + total += span.hi * 10 - span.lo; + } + return total; +} + +const ints: number[] = [3, 4, 5]; +const fractions: number[] = [0.5, 1.5, 2.5]; +const halved: number[] = [8, 6, 4]; +halve(halved); +const pick: (values: number[]) => number = first; +const viaValue: number[] = [9, 8, 7]; + +// An index that calls a function reassigning the global: the read uses the +// array the global held before the index ran, as in Node. +let swapped: number[] = [1, 2, 3]; +function swapIn(): number { + swapped = []; + for (let i = 0; i < 4; i++) swapped.push(i + 10); + return 1; +} +function fillSwapped(n: number): number { + swapped = []; + for (let i = 0; i < n; i++) swapped.push(i & 15); + let s = 0; + for (let i = 0; i < swapped.length; i++) s += swapped[i]!; + return s; +} +function readSwapped(): number | undefined { + return swapped[swapIn()]; +} + +console.log(`fnv ${fnv(data)} ${fnv(data)}`); +const h = histogram(data, 10); +console.log(`hist ${h.length} ${h[0]} ${h[9]} ${total(h)}`); +console.log(locals(128)); +console.log(readSparse(data, 5), readSparse(data, 5000)); +console.log(`mean ${mean(ints)} ${mean(fractions)}`); +console.log(`halved ${halved[0]} ${halved[2]} ${halved.length}`); +console.log(`value ${pick(viaValue)} ${viaValue.length}`); +console.log(`regrow ${regrow(40)} ${reassigned.length} ${reassigned[39]} ${regrow(5)}`); +console.log(`span ${spanTotal()}`); +console.log(`swap ${fillSwapped(6)} ${readSwapped()} ${swapped[1]}`); +void later().then((v) => console.log(`later ${v}`)); diff --git a/tests/corpus/7621-class-methods-foreign-receivers.mjs b/tests/corpus/7621-class-methods-foreign-receivers.mjs new file mode 100644 index 000000000..70826a192 --- /dev/null +++ b/tests/corpus/7621-class-methods-foreign-receivers.mjs @@ -0,0 +1,218 @@ +// Prototype methods called with a receiver that is not an instance of +// their class (`C.prototype.m.call(other)`): `#private` members throw +// Node's TypeErrors for such a receiver, and `super.m()` looks the method up +// on the base class's prototype and calls it with the receiver. +function report(label, run) { + try { + console.log(label, run()); + } catch (error) { + console.log(label, error.constructor.name, error.message); + } +} + +class Counter { + #count = 0; + #step = 1; + constructor(name) { + this.name = name; + } + read() { + return this.#count; + } + bump() { + this.#count += this.#step; + return this.#count; + } + reset(value) { + this.#count = value; + return this.name; + } + #label() { + return `${this.name}#${this.#count}`; + } + label() { + return this.#label(); + } + owns(value) { + return #count in value; + } + ownsSelf() { + return #count in this; + } +} + +const counter = new Counter("c"); +console.log(counter.bump(), counter.bump(), counter.read(), counter.label()); +console.log(counter.owns(counter), counter.owns({}), counter.ownsSelf(), counter.reset(7), counter.read()); + +const plain = { name: "plain" }; +report("read", () => Counter.prototype.read.call(plain)); +report("bump", () => Counter.prototype.bump.call(plain)); +report("reset", () => Counter.prototype.reset.call(plain, 3)); +report("label", () => Counter.prototype.label.call(plain)); +report("ownsSelf", () => Counter.prototype.ownsSelf.call(plain)); +report("owns primitive", () => Counter.prototype.owns.call(counter, 5)); +report("label undefined", () => Counter.prototype.label.call(undefined)); +report("label null", () => Counter.prototype.label.call(null)); +report("label number", () => Counter.prototype.label.call(4)); +report("read other instance", () => Counter.prototype.read.call(new Counter("d"))); +report("label other instance", () => Counter.prototype.label.call(new Counter("e"))); + +const Anonymous = class { + #m() { + return 1; + } + call() { + return this.#m(); + } +}; +const Named = class Inner { + #m() { + return 2; + } + call() { + return this.#m(); + } +}; +report("anonymous", () => Anonymous.prototype.call.call({})); +report("named", () => Named.prototype.call.call({})); +console.log(new Anonymous().call(), new Named().call()); + +class Shape { + constructor(name) { + this.name = name; + } + describe() { + return `shape ${this.name}`; + } + area() { + return 0; + } +} +class Square extends Shape { + describe() { + return `square: ${super.describe()} area ${this.area()}`; + } + area() { + return this.size * this.size; + } + both() { + return [super.describe(), super.area()].join("|"); + } +} +class Tiny extends Square { + #tag = "t"; + describe() { + return `tiny<${super.describe()}>`; + } + tag() { + return `${this.#tag}:${super.both()}`; + } +} + +const square = new Square("s"); +square.size = 3; +console.log(square.describe(), square.both()); +const tiny = new Tiny("t"); +tiny.size = 1; +console.log(tiny.describe(), tiny.tag()); +const foreign = { + name: "f", + size: 2, + area() { + return 40; + }, +}; +report("square describe", () => Square.prototype.describe.call(foreign)); +report("square both", () => Square.prototype.both.call(foreign)); +report("tiny describe", () => Tiny.prototype.describe.call(foreign)); +report("tiny tag", () => Tiny.prototype.tag.call(foreign)); +report("square describe on tiny", () => Square.prototype.describe.call(tiny)); + +// A base method that builds `new this.constructor()` on a subclass instance +// builds the subclass, also once the base prototype is complete (a super call +// above reads it) and `copy` is read through its prototype slot. +class Node3 { + constructor() { + this.a = 0; + } + copy(source) { + this.a = source.a; + return this; + } + clone() { + return new this.constructor().copy(this); + } +} +class Mesh3 extends Node3 { + constructor(geometry) { + super(); + this.geometry = geometry; + } + copy(source) { + super.copy(source); + this.geometry = source.geometry; + return this; + } +} +const copyValue = Node3.prototype.copy; +const mesh = new Mesh3({ id: 1 }); +mesh.a = 5; +const meshClone = mesh.clone(); +console.log( + meshClone instanceof Mesh3, + meshClone.constructor === Mesh3, + mesh.constructor === Mesh3, + meshClone.a, + JSON.stringify(meshClone.geometry), + typeof copyValue, +); + +// Methods that use a private accessor or return a private method keep their own receiver, as values too. +class Vault { + #v = 1; + get #value() { + return this.#v * 10; + } + set #value(x) { + this.#v = x; + } + #hidden() { + return "hidden " + this.#v; + } + read() { + return this.#value; + } + bump() { + this.#value = this.#v + 1; + return this.#v; + } + hiddenValue() { + return this.#hidden; + } +} +const vault = new Vault(); +const readVault = Vault.prototype.read; +const bumpVault = Vault.prototype.bump; +const hiddenOf = Vault.prototype.hiddenValue; +console.log(readVault.call(vault), bumpVault.call(vault), readVault.call(vault), typeof hiddenOf.call(vault)); +console.log(hiddenOf.call(vault).call(vault)); +try { + readVault.call({}); +} catch (error) { + console.log(error.constructor.name); +} + +// A method value whose body calls super with spread arguments. +class Summer { + add(...xs) { + return xs.reduce((sum, x) => sum + x, 0); + } +} +class Doubler extends Summer { + add(...xs) { + return super.add(...xs) * 2; + } +} +const addValue = Doubler.prototype.add; +console.log(addValue.call(new Doubler(), 1, 2, 3), addValue.apply(new Doubler(), [4, 5])); diff --git a/tests/corpus/7623-key-order-javascript-built-records/lib.js b/tests/corpus/7623-key-order-javascript-built-records/lib.js new file mode 100644 index 000000000..a981505fc --- /dev/null +++ b/tests/corpus/7623-key-order-javascript-built-records/lib.js @@ -0,0 +1,18 @@ +// Records built in JavaScript in another key order than the TypeScript +// literals of the same type: a replacement method installed through a +// computed key (beside another computed write that installs a function +// returning a primitive), and a function stored on the global object, read +// through a typed view, that calls a callback inside its options record. +export function install(target, name) { + target[name] = () => ({ a: 1, b: 2 }); +} + +// A later computed write of a function that returns a primitive does not +// hide the record-returning one above. +export function installLabel(target, name) { + target[name] = () => "label"; +} + +globalThis.__eachRow = function (options) { + options.visit({ x: 5, y: 6 }); +}; diff --git a/tests/corpus/7623-key-order-javascript-built-records/main.ts b/tests/corpus/7623-key-order-javascript-built-records/main.ts new file mode 100644 index 000000000..1615351d7 --- /dev/null +++ b/tests/corpus/7623-key-order-javascript-built-records/main.ts @@ -0,0 +1,53 @@ +// Copies and enumerations of records JavaScript builds follow the order +// JavaScript wrote, also when the TypeScript literals of the type write +// another order and spread their own type. Each case uses its own type. +import { install, installLabel } from "./lib.js"; + +interface Pair { + a: number; + b: number; +} + +interface Row { + x: number; + y: number; +} + +interface Options { + visit: (row: Row) => void; +} + +class Store { + get(): Pair { + return { b: 2, a: 1 }; + } +} + +function showPair(label: string, value: Pair): void { + const copy = { ...value }; + console.log(label, Object.keys(value).join(), JSON.stringify(copy), Object.keys(copy).join()); +} + +function showRow(label: string, value: Row): void { + const copy = { ...value }; + console.log(label, Object.keys(value).join(), JSON.stringify(copy), Object.keys(copy).join()); +} + +// A replaceable method whose replacement returns a record JavaScript builds. +const store = new Store(); +showPair("method", store.get()); +showPair("self copy", { ...store.get(), b: 3 }); +install(store, "get"); +showPair("replaced", store.get()); +const labels: { label(): string } = { label: () => "plain" }; +installLabel(labels, "label"); +console.log(labels.label()); + +// A JavaScript function, checked into a typed slot, calls a native callback +// it receives inside its options record with a record it builds. +const literal: Row = { y: 20, x: 10 }; +showRow("literal", literal); +showRow("self copy", { ...literal, y: 21 }); +const eachRow = (globalThis as typeof globalThis & { __eachRow: (options: Options) => void }) + .__eachRow; +eachRow({ visit: (row: Row) => showRow("checked function", row) }); diff --git a/tests/corpus/7624-key-order-boxed-callback-records/lib.js b/tests/corpus/7624-key-order-boxed-callback-records/lib.js new file mode 100644 index 000000000..47ba5b99c --- /dev/null +++ b/tests/corpus/7624-key-order-boxed-callback-records/lib.js @@ -0,0 +1,7 @@ +// A JavaScript function that calls a callback inside its options object +// with records it builds, in another key order than the TypeScript literals +// of the callback's parameter type. +export function each(options) { + options.visit({ a: 1, b: 2 }); + options.visit({ a: 3, b: 4 }); +} diff --git a/tests/corpus/7624-key-order-boxed-callback-records/main.ts b/tests/corpus/7624-key-order-boxed-callback-records/main.ts new file mode 100644 index 000000000..4cdfeff37 --- /dev/null +++ b/tests/corpus/7624-key-order-boxed-callback-records/main.ts @@ -0,0 +1,27 @@ +// A native callback handed to JavaScript inside an options record receives +// records JavaScript builds: copies and enumerations of them follow the +// order JavaScript wrote, also when the TypeScript literals of the type +// write another order and spread their own type. +import { each } from "./lib.js"; + +interface Row { + a: number; + b: number; +} + +function show(label: string, row: Row): void { + const copy = { ...row }; + console.log(label, Object.keys(row).join(), JSON.stringify(copy), Object.keys(copy).join()); +} + +const literal: Row = { b: 20, a: 10 }; +show("literal", literal); +show("self copy", { ...literal, b: 21 }); +const seen: Row[] = []; +each({ + visit: (row: Row) => { + seen.push(row); + show("callback", row); + }, +}); +console.log(JSON.stringify(seen), JSON.stringify(seen.map((row) => ({ ...row, a: row.a * 2 })))); diff --git a/tests/corpus/7642-element-assignment-order.ts b/tests/corpus/7642-element-assignment-order.ts new file mode 100644 index 000000000..e020d05fc --- /dev/null +++ b/tests/corpus/7642-element-assignment-order.ts @@ -0,0 +1,45 @@ +// An element assignment evaluates its receiver and index before its value, +// also when the value is an optional element read or a call that assigns +// the bindings the receiver and index read. +const a = [10, 20, 30, 40]; +const b = [1, 2, 3, 4]; +let n = 0; +a[n++] = b[n]!; +console.log(a.join(), n); +a[n] = b[n++]; +console.log(a.join(), n); + +const optional: (number | undefined)[] = [undefined, 5, undefined]; +let k = 0; +optional[k++] = optional[k]; +console.log(optional, k); + +// A call in the value assigns the receiver and the index through closures. +let target: (number | undefined)[] = [0, 0, 0]; +const first = target; +let at = 0; +function swap(): number | undefined { + target = [9, 9, 9]; + at = 2; + return b[3]; +} +target[at] = swap(); +console.log(first, target, at); + +// The index and receiver are not evaluated twice. +let reads = 0; +function index(): number { + reads++; + return 1; +} +const c: (number | undefined)[] = [0, 0]; +c[index()] = optional[1]; +console.log(c, reads); + +// The index assigns the receiver's binding: the store uses the receiver +// evaluated before it. +let receiver: (number | undefined)[] = [0, 0]; +const original = receiver; +const replacement: (number | undefined)[] = [7, 7]; +receiver[((receiver = replacement), 1)] = optional[1]; +console.log(original, replacement, receiver === replacement); diff --git a/tests/corpus/7660-pointer-union-forwarded-arguments.ts b/tests/corpus/7660-pointer-union-forwarded-arguments.ts new file mode 100644 index 000000000..664f79fcb --- /dev/null +++ b/tests/corpus/7660-pointer-union-forwarded-arguments.ts @@ -0,0 +1,127 @@ +// Records and class instances passed to `T | undefined` and `T | null` +// parameters. The callee receives the object pointer itself, so a binding +// that the callee only reads keeps its frame storage; callees that keep, +// return or compare the argument still see one shared object. +interface Vec { + x: number; + y: number; +} +interface Tagged { + name: string; + at: Vec; +} +function vec(x: number, y: number): Vec { + return { x, y }; +} +function len(v: Vec | undefined): number { + return v === undefined ? -1 : Math.sqrt(v.x * v.x + v.y * v.y); +} +function sum(a: Vec | undefined, b: Vec): Vec { + return a === undefined ? b : { x: a.x + b.x, y: a.y + b.y }; +} +function label(t: Tagged | undefined): string { + return t === undefined ? "none" : t.name + "@" + t.at.x + "," + t.at.y; +} +function depth(v: Vec | undefined, n: number): number { + if (v === undefined || n === 0) return 0; + return v.x + depth(v, n - 1); +} + +const kept: (Vec | undefined)[] = []; +function keep(v: Vec | undefined): number { + kept.push(v); + return kept.length; +} +function same(a: Vec | undefined, b: Vec): boolean { + return a === b; +} +function orNull(v: Vec | undefined): Vec | undefined { + return v !== undefined && v.x < 0 ? undefined : v; +} + +class Cell { + value: number; + next: Cell | null; + constructor(value: number, next: Cell | null) { + this.value = value; + this.next = next; + } +} +function cellTotal(c: Cell | null): number { + let total = 0; + for (let at = c; at !== null; at = at.next) total += at.value; + return total; +} +function fail(v: Vec | undefined): number { + if (v !== undefined && v.x > 100) throw new Error("too far: " + v.x); + return v === undefined ? 0 : v.x; +} + +function projections(): void { + const a = vec(3, 4); + const b = sum(a, vec(1, 1)); + console.log("len", len(a), len(b), len(undefined)); + const c = sum(sum(a, b), a); + console.log("sum", c.x, c.y, sum(undefined, a).x); + console.log("depth", depth(a, 5), depth(undefined, 3)); + let total = 0; + for (let i = 0; i < 1000; i++) { + const p = vec(i, i + 1); + total += len(p) + len(sum(p, a)); + } + console.log("loop", total.toFixed(6)); +} + +function mutated(): void { + const p = vec(1, 2); + console.log("before", len(p)); + p.x = 6; + p.y = 8; + console.log("after", len(p)); +} + +function strings(): void { + const prefix = ["a", "b", "c"].join("-"); + const t: Tagged = { name: prefix + "!", at: vec(2, 5) }; + console.log("label", label(t), label(undefined)); + const u: Tagged = { name: label(t), at: t.at }; + console.log("nested", label(u)); +} + +function escapes(): void { + const a = vec(7, 8); + const count = keep(a); + const b = vec(9, 10); + console.log("kept", count, kept[0] === a, kept[0]!.x, keep(b), kept[1]!.y); + a.x = 70; + console.log("shared", kept[0]!.x); + const c = vec(1, 1); + console.log("same", same(c, c), same(c, vec(1, 1)), same(undefined, c)); + const d = vec(-1, 2); + const e = vec(4, 5); + console.log("returned", orNull(d), orNull(e) === e, orNull(e)!.y); +} + +function classes(): void { + const tail = new Cell(3, null); + const list = new Cell(1, new Cell(2, tail)); + console.log("cells", cellTotal(list), cellTotal(tail), cellTotal(null)); +} + +function throwing(): void { + const near = vec(5, 0); + const far = vec(500, 0); + try { + console.log("near", fail(near)); + console.log("far", fail(far)); + } catch (e) { + console.log("caught", (e as Error).message, near.x, far.x); + } +} + +projections(); +mutated(); +strings(); +escapes(); +classes(); +throwing(); diff --git a/tests/corpus/7661-node-call-modules/lib/entry.cjs b/tests/corpus/7661-node-call-modules/lib/entry.cjs new file mode 100644 index 000000000..3fa55af87 --- /dev/null +++ b/tests/corpus/7661-node-call-modules/lib/entry.cjs @@ -0,0 +1,14 @@ +"use strict"; +const { label } = require("./util"); +const settings = require("../settings.json"); + +exports.describe = (name) => `${label(name)} (${settings.mode})`; + +exports.hasOptional = () => { + try { + require("./not-shipped.cjs"); + return true; + } catch (error) { + return error.code !== "MODULE_NOT_FOUND"; + } +}; diff --git a/tests/corpus/7661-node-call-modules/lib/esm.mjs b/tests/corpus/7661-node-call-modules/lib/esm.mjs new file mode 100644 index 000000000..467987471 --- /dev/null +++ b/tests/corpus/7661-node-call-modules/lib/esm.mjs @@ -0,0 +1,5 @@ +import { factor } from "./factor.mjs"; + +export function twice(n) { + return n * factor; +} diff --git a/tests/corpus/7661-node-call-modules/lib/factor.mjs b/tests/corpus/7661-node-call-modules/lib/factor.mjs new file mode 100644 index 000000000..adb2e40cb --- /dev/null +++ b/tests/corpus/7661-node-call-modules/lib/factor.mjs @@ -0,0 +1 @@ +export const factor = 2; diff --git a/tests/corpus/7661-node-call-modules/lib/util.js b/tests/corpus/7661-node-call-modules/lib/util.js new file mode 100644 index 000000000..ea7711f79 --- /dev/null +++ b/tests/corpus/7661-node-call-modules/lib/util.js @@ -0,0 +1,5 @@ +// An ES module by the nearest package.json ("type": "module"), loaded with +// require() from a CommonJS module. +export function label(name) { + return `module of ${name}`; +} diff --git a/tests/corpus/7661-node-call-modules/main.ts b/tests/corpus/7661-node-call-modules/main.ts new file mode 100644 index 000000000..7f6f4ac76 --- /dev/null +++ b/tests/corpus/7661-node-call-modules/main.ts @@ -0,0 +1,12 @@ +// @requires-libnode +// The modules island calls load ship with the program: a module of the +// program and the modules it loads, and whole packages with their +// dependencies, including files a package reads or loads by computed names. +import { callSync } from "@scriptc/node"; + +console.log(callSync("./lib/entry.cjs", "describe", "scriptc")); +console.log(callSync("greeter", "greet", "world")); +console.log(callSync("greeter", "fromData")); +console.log(callSync("greeter", "byName", "shout", "computed")); +console.log(callSync("./lib/entry.cjs", "hasOptional")); +console.log(callSync("./lib/esm.mjs", "twice", 21)); diff --git a/tests/corpus/7661-node-call-modules/node_modules/greeter/data/message.txt b/tests/corpus/7661-node-call-modules/node_modules/greeter/data/message.txt new file mode 100644 index 000000000..8e4d1b0c2 --- /dev/null +++ b/tests/corpus/7661-node-call-modules/node_modules/greeter/data/message.txt @@ -0,0 +1 @@ +read from a file the package ships diff --git a/tests/corpus/7661-node-call-modules/node_modules/greeter/index.js b/tests/corpus/7661-node-call-modules/node_modules/greeter/index.js new file mode 100644 index 000000000..4e3f7bc55 --- /dev/null +++ b/tests/corpus/7661-node-call-modules/node_modules/greeter/index.js @@ -0,0 +1,8 @@ +"use strict"; +const fs = require("node:fs"); +const path = require("node:path"); +const shout = require("shout"); + +exports.greet = (who) => shout(`hello, ${who}`); +exports.fromData = () => fs.readFileSync(path.join(__dirname, "data", "message.txt"), "utf8").trim(); +exports.byName = (name, text) => require(name)(text); diff --git a/tests/corpus/7661-node-call-modules/node_modules/greeter/package.json b/tests/corpus/7661-node-call-modules/node_modules/greeter/package.json new file mode 100644 index 000000000..07b35f1af --- /dev/null +++ b/tests/corpus/7661-node-call-modules/node_modules/greeter/package.json @@ -0,0 +1 @@ +{ "name": "greeter", "version": "1.0.0", "main": "index.js", "dependencies": { "shout": "1.0.0" } } diff --git a/tests/corpus/7661-node-call-modules/node_modules/shout/index.js b/tests/corpus/7661-node-call-modules/node_modules/shout/index.js new file mode 100644 index 000000000..6401aa6da --- /dev/null +++ b/tests/corpus/7661-node-call-modules/node_modules/shout/index.js @@ -0,0 +1,2 @@ +"use strict"; +module.exports = (text) => `${text.toUpperCase()}!`; diff --git a/tests/corpus/7661-node-call-modules/node_modules/shout/package.json b/tests/corpus/7661-node-call-modules/node_modules/shout/package.json new file mode 100644 index 000000000..d32731e21 --- /dev/null +++ b/tests/corpus/7661-node-call-modules/node_modules/shout/package.json @@ -0,0 +1 @@ +{ "name": "shout", "version": "1.0.0", "main": "index.js" } diff --git a/tests/corpus/7661-node-call-modules/settings.json b/tests/corpus/7661-node-call-modules/settings.json new file mode 100644 index 000000000..42201c528 --- /dev/null +++ b/tests/corpus/7661-node-call-modules/settings.json @@ -0,0 +1 @@ +{ "mode": "shipped" } diff --git a/tests/corpus/7662-node-call-exit-events/listeners.cjs b/tests/corpus/7662-node-call-exit-events/listeners.cjs new file mode 100644 index 000000000..c21ad324f --- /dev/null +++ b/tests/corpus/7662-node-call-exit-events/listeners.cjs @@ -0,0 +1,13 @@ +"use strict"; +let beforeExit = 0; + +exports.register = () => { + process.on("beforeExit", (code) => { + beforeExit++; + console.log("beforeExit", code, beforeExit); + if (beforeExit === 1) setTimeout(() => console.log("work from beforeExit"), 1); + }); + process.on("exit", (code) => console.log("exit", code)); + process.exitCode = 4; + return true; +}; diff --git a/tests/corpus/7662-node-call-exit-events/main.ts b/tests/corpus/7662-node-call-exit-events/main.ts new file mode 100644 index 000000000..15bb6ad5d --- /dev/null +++ b/tests/corpus/7662-node-call-exit-events/main.ts @@ -0,0 +1,9 @@ +// @exit: 4 +// @requires-libnode +// The program and the Node.js it loads end as one process: Node's +// 'beforeExit' and 'exit' run when the program ends, work that 'beforeExit' +// schedules runs first, and process.exitCode set in Node is the program's. +import { callSync } from "@scriptc/node"; + +console.log("registered", callSync("./listeners.cjs", "register")); +console.log("main done"); diff --git a/tests/corpus/7663-pointer-union-stored-payload.ts b/tests/corpus/7663-pointer-union-stored-payload.ts new file mode 100644 index 000000000..92ac68bfb --- /dev/null +++ b/tests/corpus/7663-pointer-union-stored-payload.ts @@ -0,0 +1,83 @@ +// Records passed to `T | undefined` parameters whose callee keeps the +// narrowed record: pushed into a global array (also through a non-null +// assertion), kept through `??`, stored in a nullable class field or a record +// field, or passed on to a callee that keeps it. Each record must outlive its +// caller's frame. A callee that only reads the narrowed record lets it stay +// where it is. +interface Tagged { + id: number; + tags: string[]; +} +interface Slot { + held: Tagged | undefined; +} +const kept: Tagged[] = []; +const slot: Slot = { held: undefined }; +function pushNarrowed(t: Tagged | undefined): void { + if (t !== undefined) kept.push(t); +} +function pushAsserted(t: Tagged | undefined): void { + kept.push(t!); +} +function keepDefaulted(t: Tagged | undefined): void { + kept.push(t ?? { id: -1, tags: [] }); +} +function storeInSlot(t: Tagged | undefined): void { + if (t !== undefined) slot.held = t; +} +function keepRecord(t: Tagged): void { + kept.push(t); +} +function passOn(t: Tagged | undefined): void { + if (t !== undefined) keepRecord(t); +} +function readOnly(t: Tagged | undefined): number { + return t === undefined ? 0 : t.id + t.tags.length; +} +class Holder { + last: Tagged | undefined = undefined; + take(t: Tagged | undefined): void { + this.last = t; + } +} +const holder = new Holder(); +function fill(round: number): number { + let read = 0; + for (let i = 0; i < 3; i++) { + const a: Tagged = { id: round * 10 + i, tags: ["a", String(i)] }; + pushNarrowed(a); + const b: Tagged = { id: round * 10 + i + 100, tags: ["b"] }; + keepDefaulted(b); + const c: Tagged = { id: round * 10 + i + 200, tags: ["c"] }; + storeInSlot(c); + const d: Tagged = { id: round * 10 + i + 300, tags: ["d", "d"] }; + holder.take(d); + const e: Tagged = { id: round * 10 + i + 400, tags: [] }; + passOn(e); + const g: Tagged = { id: round * 10 + i + 600, tags: ["g", "g", "g"] }; + pushAsserted(g); + const f: Tagged = { id: round * 10 + i + 500, tags: ["f"] }; + read += readOnly(f); + } + return read; +} +// Reuses the stack the callers above ran on, and allocates collectable +// records, before the kept ones are read. +function churn(n: number): number { + let total = 0; + for (let i = 0; i < n; i++) { + const t: Tagged = { id: i, tags: [String(i), String(i + 1)] }; + total += t.tags.length + readOnly(t); + } + return total; +} +let reads = 0; +for (let round = 0; round < 4; round++) { + reads += fill(round); + reads += churn(200); +} +console.log("reads", reads); +console.log("kept", kept.length); +for (const t of kept) console.log(t.id, t.tags.join("+")); +console.log("slot", slot.held === undefined ? "none" : slot.held.id, slot.held?.tags.join("+")); +console.log("holder", holder.last === undefined ? "none" : holder.last.id, holder.last?.tags.join("+")); diff --git a/tests/corpus/7680-typed-array-search.ts b/tests/corpus/7680-typed-array-search.ts new file mode 100644 index 000000000..7672c3712 --- /dev/null +++ b/tests/corpus/7680-typed-array-search.ts @@ -0,0 +1,70 @@ +// TypedArray.prototype.indexOf, lastIndexOf and includes on every numeric +// element kind: strict equality (NaN never found) for indexOf/lastIndexOf, +// SameValueZero (NaN found, -0 equals 0) for includes, and fromIndex as +// ToIntegerOrInfinity, counted from the end when negative. +const bytes = new Uint8Array([1, 2, 10, 3, 10, 255]); +console.log("u8", bytes.indexOf(10), bytes.indexOf(10, 3), bytes.indexOf(10, 5), bytes.lastIndexOf(10)); +console.log("u8 from", bytes.indexOf(10, -2), bytes.indexOf(10, -100), bytes.indexOf(1, Infinity), bytes.indexOf(10, 2.9)); +console.log("u8 last", bytes.lastIndexOf(10, 3), bytes.lastIndexOf(10, -3), bytes.lastIndexOf(1, -100), bytes.lastIndexOf(255, 99)); +console.log("u8 values", bytes.indexOf(256 + 10), bytes.indexOf(-246), bytes.indexOf(10.5), bytes.includes(255), bytes.includes(0)); +console.log("u8 includes from", bytes.includes(1, 1), bytes.includes(255, -1), bytes.includes(2, -5)); + +const clamped = new Uint8ClampedArray([0, 300, -5, 127.5]); +console.log("u8c", clamped.indexOf(255), clamped.indexOf(0, 1), clamped.lastIndexOf(0), clamped.indexOf(128)); + +const signed = new Int8Array([-1, 127, -128, 0]); +console.log("i8", signed.indexOf(-1), signed.indexOf(255), signed.indexOf(-128), signed.lastIndexOf(0)); + +const shorts = new Int16Array([5, -300, 5, 7]); +const ushorts = new Uint16Array([65535, 1, 65535]); +console.log("16", shorts.indexOf(-300), shorts.lastIndexOf(5), ushorts.indexOf(-1), ushorts.lastIndexOf(65535, 1)); + +const ints = new Int32Array([-2147483648, 0, 2147483647, 0]); +const uints = new Uint32Array([4294967295, 3]); +console.log("32", ints.indexOf(2147483647), ints.lastIndexOf(0), uints.indexOf(4294967295), uints.includes(-1)); + +const floats = new Float64Array([1.5, NaN, -0, 0, Infinity]); +console.log("f64", floats.indexOf(NaN), floats.lastIndexOf(NaN), floats.includes(NaN), floats.indexOf(0), floats.indexOf(-0)); +console.log("f64 more", floats.lastIndexOf(0), floats.indexOf(Infinity), floats.includes(-Infinity), floats.includes(NaN, 2)); + +const singles = new Float32Array([1.1, 0.5, NaN]); +console.log("f32", singles.indexOf(1.1), singles.indexOf(Math.fround(1.1)), singles.indexOf(0.5), singles.includes(NaN), singles.indexOf(NaN)); + +const empty = new Float64Array(0); +console.log("empty", empty.indexOf(0), empty.lastIndexOf(0), empty.includes(0), empty.includes(0, -1)); + +// Views search their own window of the buffer. +const buffer = new ArrayBuffer(16); +const whole = new Uint8Array(buffer); +whole.set([9, 9, 1, 2, 9, 3, 9, 9]); +const view = new Uint8Array(buffer, 2, 4); +const sub = whole.subarray(3, 6); +console.log("views", view.indexOf(9), view.lastIndexOf(9), view.indexOf(1), sub.indexOf(9), sub.includes(1), sub.lastIndexOf(3)); +const words = new Uint16Array(buffer, 2, 3); +console.log("word view", words.indexOf(0x0201), words.length); + +// Shared memory searches the same way. +const shared = new Int32Array(new SharedArrayBuffer(16)); +shared[1] = 7; +shared[3] = 7; +console.log("shared", shared.indexOf(7), shared.lastIndexOf(7), shared.includes(0), shared.indexOf(7, 2)); + +// A loop that finds every occurrence, as a line splitter does. +const text = new TextEncoder().encode("one\ntwo\n\nthree\n"); +const lines: number[] = []; +for (let at = text.indexOf(10); at !== -1; at = text.indexOf(10, at + 1)) lines.push(at); +console.log("lines", lines.join(",")); + +// A Buffer passed as a Uint8Array keeps Buffer's search: a number needle +// is taken as a byte, and a NaN byteOffset searches the whole buffer. +function searchView(view: Uint8Array): string { + return [view.indexOf(97 + 256), view.lastIndexOf(98, NaN), view.includes(353), view.indexOf(98, -1)].join(","); +} +console.log(searchView(Buffer.from("abcab")), searchView(new Uint8Array([97, 98, 99, 97, 98]))); + +// An explicit undefined fromIndex converts to NaN, which searches from 0: +// lastIndexOf then looks at index 0 only. +const values = new Int16Array([3, 1, 3, 2, 3]); +const absent: number | undefined = process.argv.length > 99 ? 1 : undefined; +console.log("undefined from", values.indexOf(3, undefined), values.lastIndexOf(3, undefined), values.lastIndexOf(1, undefined), values.includes(2, undefined)); +console.log("maybe undefined from", values.indexOf(3, absent), values.lastIndexOf(3, absent), values.includes(1, absent)); diff --git a/tests/corpus/7681-typed-array-search-dynamic.mjs b/tests/corpus/7681-typed-array-search-dynamic.mjs new file mode 100644 index 000000000..643ba0cfa --- /dev/null +++ b/tests/corpus/7681-typed-array-search-dynamic.mjs @@ -0,0 +1,28 @@ +// TypedArray search on untyped values (parameters of untyped functions and +// object members): needles that are not numbers are never found (strict +// equality), fromIndex may be undefined, a string or a fraction, and +// Buffers keep their own indexOf, which wraps number needles and takes +// strings. +function views(bytes, floats, shorts) { + return { bytes, floats, shorts }; +} +const { bytes, floats, shorts } = views( + new Uint8Array([1, 2, 10, 3, 10]), + new Float64Array([0.5, NaN, -0]), + new Int16Array([-1, 300, -1]), +); +console.log(bytes.indexOf(10), bytes.indexOf(10, 3), bytes.lastIndexOf(10), bytes.includes(3), bytes.includes(4)); +console.log(bytes.lastIndexOf(1, undefined), bytes.lastIndexOf(10), bytes.indexOf(10, undefined), bytes.includes(1, undefined)); +console.log(bytes.indexOf(10, '3'), bytes.indexOf(10, 2.9), bytes.indexOf(10, -1), bytes.lastIndexOf(10, -2), bytes.indexOf(10, null)); +console.log(bytes.indexOf('10'), bytes.includes('1'), bytes.indexOf(10n), bytes.indexOf(), bytes.includes(), bytes.indexOf(266)); +console.log(floats.indexOf(NaN), floats.includes(NaN), floats.indexOf(0), floats.lastIndexOf(-0), floats.includes(0.5, 1)); +console.log(shorts.indexOf(-1), shorts.lastIndexOf(-1), shorts.indexOf(300, -2), shorts.includes(65535)); +const holder = { view: bytes.subarray(2) }; +console.log(holder.view.indexOf(10), holder.view.lastIndexOf(10), holder.view.includes(1)); +const buffer = Buffer.from([1, 2, 10, 3, 10]); +const boxed = [buffer]; +console.log(boxed[0].indexOf(10), boxed[0].indexOf(266), boxed[0].indexOf('\n'), boxed[0].includes(Buffer.from([3, 10]))); +console.log(boxed[0].subarray(3).indexOf(10), Buffer.isBuffer(boxed[0].subarray(3)), boxed[0].lastIndexOf(10, -2)); +let count = 0; +for (let at = bytes.indexOf(10); at !== -1; at = bytes.indexOf(10, at + 1)) count++; +console.log('count', count); diff --git a/tests/corpus/7682-constructor-function-globals.js b/tests/corpus/7682-constructor-function-globals.js new file mode 100644 index 000000000..58292e4ac --- /dev/null +++ b/tests/corpus/7682-constructor-function-globals.js @@ -0,0 +1,55 @@ +// Module bindings that hold `new F()` of an ordinary constructor function +// (const, let and var), read and reassigned by functions declared elsewhere +// in the module, such as the head and tail of a linked list. +'use strict'; +function Segment(prev) { + this.prev = prev; + this.next = null; + this.data = new Array(4); + this.pos = 0; +} +const head = new Segment(); +let current = head; +var spare = new Segment(head); + +function append(value) { + if (current.pos === current.data.length) { + current = current.next || (current.next = new Segment(current)); + } + current.data[current.pos++] = value; +} + +function collect() { + const out = []; + for (let node = head; node; node = node.next) out.push(node.data.slice(0, node.pos).join('')); + return out.join('|'); +} + +function reset() { + current = head; + for (let node = head; node; node = node.next) node.pos = 0; +} + +for (const ch of 'abcdefghij') append(ch); +console.log(collect(), current === head, current.prev === head.next ? 'two' : 'more'); +reset(); +append('z'); +console.log(collect(), current === head, head.next !== null); +console.log(spare.prev === head, spare instanceof Segment, typeof spare.data); +spare = new Segment(spare); +console.log(spare.prev.prev === head); + +function Counter(start) { + this.value = start; +} +Counter.prototype.bump = function () { + return ++this.value; +}; +let counter = new Counter(10); +function bumpTwice() { + counter.bump(); + return counter.bump(); +} +console.log(bumpTwice(), counter.value); +counter = new Counter(0); +console.log(bumpTwice()); diff --git a/tests/corpus/7700-util-deprecated-helpers.cjs b/tests/corpus/7700-util-deprecated-helpers.cjs new file mode 100644 index 000000000..39ec5d65f --- /dev/null +++ b/tests/corpus/7700-util-deprecated-helpers.cjs @@ -0,0 +1,30 @@ +// util.isArray and util._extend, the deprecated helpers: Node's answers +// (_extend assigns from the last own key to the first and leaves +// non-object sources alone), and DEP0044/DEP0060 once each on the next +// tick, shared with util.deprecate's codes. The program re-runs itself and +// prints the child's output with process ids normalized. +"use strict"; +const { spawnSync } = require("node:child_process"); +const util = require("util"); +function child() { + process.on("warning", (w) => console.log("listener", w.name, w.code)); + console.log(util.isArray([1]), util.isArray("x"), util.isArray({ length: 0 }), util.isArray()); + console.log(util._extend({ a: 1 }, { b: 2, c: 3 })); + console.log(Object.keys(util._extend({}, { x: 1, y: 2, z: 3 }))); + console.log(util._extend({ a: 1 }, null), util._extend({ a: 1 }, "str"), util._extend({ a: 1 }, () => 1)); + console.log(util._extend([0], [5, 6]), util._extend({ k: 1 })); + const t = { keep: true }; + console.log(util._extend(t, { keep: false }) === t, t); + try { util._extend(null, { a: 1 }); } catch (e) { console.log(e.name, e.message); } + console.log(util._extend(null, {})); + util.isArray([]); + const dep = util.deprecate(() => "wrapped", "a custom DEP0060", "DEP0060"); + console.log(dep(), "sync end"); +} +if (process.argv[2] === "child") child(); +else { + const run = spawnSync(process.execPath, [process.argv[1], "child"], { encoding: "utf8" }); + console.log(`status: ${run.status}`); + console.log(run.stdout); + console.log(run.stderr.replace(/\(node:\d+\)/g, "(node:)")); +} diff --git a/tests/corpus/7701-util-deprecated-helpers.ts b/tests/corpus/7701-util-deprecated-helpers.ts new file mode 100644 index 000000000..badc8ec05 --- /dev/null +++ b/tests/corpus/7701-util-deprecated-helpers.ts @@ -0,0 +1,21 @@ +// util.isArray and util._extend over typed values: isArray answers by the +// runtime value, and _extend writes into the typed target and answers the +// target itself. +import * as util from "node:util"; + +process.noDeprecation = true; +const nums: number[] = [1, 2]; +const text: string = "x"; +const anything: unknown = [3]; +console.log(util.isArray(nums), util.isArray(text), util.isArray(new Map()), util.isArray(anything)); + +interface Opts { + a: number; + b?: string; +} +const base: Opts = { a: 1 }; +const merged = util._extend(base, { b: "two" }); +console.log(merged, merged === base, base.b); +const plain = util._extend({}, { x: 1, y: 2 }); +console.log(plain, Object.keys(plain)); +console.log(util._extend({ keep: 1 }, null), util._extend({ keep: 2 }, undefined)); diff --git a/tests/corpus/7702-util-signal-exit-code.cjs b/tests/corpus/7702-util-signal-exit-code.cjs new file mode 100644 index 000000000..000a342df --- /dev/null +++ b/tests/corpus/7702-util-signal-exit-code.cjs @@ -0,0 +1,17 @@ +// util.convertProcessSignalToExitCode: 128 plus the os.constants.signals +// number, aliases included, and ERR_INVALID_ARG_VALUE listing every signal +// name for unknown names, other cases and non-strings. +"use strict"; +const util = require("util"); +const os = require("os"); + +for (const v of ["SIGKILL", "SIGTERM", "SIGINT", "SIGHUP", "SIGSYS", "SIGIOT", "nope", "sigint", 9, undefined, null]) { + try { + console.log(util.convertProcessSignalToExitCode(v)); + } catch (e) { + console.log(e.name, e.code, e.message); + } +} +console.log(Object.keys(os.constants.signals).every((name) => util.convertProcessSignalToExitCode(name) === 128 + os.constants.signals[name])); +const convert = util.convertProcessSignalToExitCode; +console.log(convert("SIGPIPE"), typeof convert); diff --git a/tests/corpus/7703-util-aborted.cjs b/tests/corpus/7703-util-aborted.cjs new file mode 100644 index 000000000..66a28c1cd --- /dev/null +++ b/tests/corpus/7703-util-aborted.cjs @@ -0,0 +1,17 @@ +// util.aborted(signal, resource): a promise that rejects with Node's +// ERR_INVALID_ARG_TYPE for a non-AbortSignal or a non-object resource, +// resolves with undefined for an aborted signal and with the abort event +// later, in Node's microtask order. +const util = require("util"); +const ac = new AbortController(); +const p = util.aborted(ac.signal, {}); +console.log(p instanceof Promise); +p.then(/** @param {any} v */ (v) => console.log("later", typeof v, v && v.type, v instanceof Event)); +const done = new AbortController(); done.abort(); +util.aborted(done.signal, {}).then((v) => console.log("already", v)); +util.aborted({}, {}).catch((e) => console.log(e.name, e.code, e.message)); +util.aborted(ac.signal, 1).catch((e) => console.log(e.name, e.code, e.message)); +util.aborted(ac.signal, null).catch((e) => console.log(e.name, e.code, e.message)); +util.aborted(ac.signal, () => {}).then(() => console.log("fn resource ok")); +util.aborted(undefined).catch((e) => console.log(e.name, e.code, e.message)); +setTimeout(() => ac.abort("why"), 1); diff --git a/tests/corpus/7704-util-aborted.ts b/tests/corpus/7704-util-aborted.ts new file mode 100644 index 000000000..18af30182 --- /dev/null +++ b/tests/corpus/7704-util-aborted.ts @@ -0,0 +1,21 @@ +// util.aborted in TypeScript: the listener resists stopImmediatePropagation, +// and the promise settles two microtask turns after the signal's own +// promise, as an async function's result adopts it. +import { aborted } from "node:util"; +const ac = new AbortController(); +ac.signal.addEventListener("abort", (e) => { + console.log("first listener stops propagation"); + e.stopImmediatePropagation(); +}); +const resource = { name: "job" }; +const order: string[] = []; +aborted(ac.signal, resource).then(() => order.push("aborted")); +Promise.resolve().then(() => order.push("tick 1")).then(() => order.push("tick 2")); +ac.abort(); +setTimeout(() => console.log(order), 0); +const done = new AbortController(); +done.abort(); +const seq: string[] = []; +aborted(done.signal, resource).then(() => seq.push("already")); +Promise.resolve().then(() => seq.push("a")).then(() => seq.push("b")).then(() => seq.push("c")); +setTimeout(() => console.log(seq), 0); diff --git a/tests/corpus/7705-util-diff.cjs b/tests/corpus/7705-util-diff.cjs new file mode 100644 index 000000000..489586a3b --- /dev/null +++ b/tests/corpus/7705-util-diff.cjs @@ -0,0 +1,14 @@ +// util.diff over strings (one entry per UTF-16 code unit, astral pairs as +// two) and arrays of strings: Node's Myers diff entries in order, identical +// values answering [] before validation, and ERR_INVALID_ARG_TYPE naming +// the argument or the array element. +const util = require("util"); +const show = (a, e) => { + try { console.log(JSON.stringify(util.diff(a, e))); } catch (err) { console.log(err.name, err.code, err.message); } +}; +show("abc", "abd"); show("kitten", "sitting"); show("", "x"); show("same", "same"); show("", ""); +show(["a", "b", "c"], ["a", "c", "d"]); show(["x"], []); show([], ["y", "z"]); show([], []); +const arr = ["p"]; show(arr, arr); show(1, 1); show(1, 2); show("a", 2); show(["a", 1], ["a"]); show(["a"], [, "b"]); +show(null, "x"); show("x", undefined); show("héllo😀", "hello😀"); show({}, "x"); +show("the quick brown fox", "the quack brown box"); +console.log(util.diff(["line 1", "line 2"], ["line 1", "line 3"])); diff --git a/tests/corpus/7706-util-diff.ts b/tests/corpus/7706-util-diff.ts new file mode 100644 index 000000000..7effff734 --- /dev/null +++ b/tests/corpus/7706-util-diff.ts @@ -0,0 +1,13 @@ +// util.diff with typed arrays of strings and DiffEntry tuples. +import { diff, type DiffEntry } from "node:util"; +const before: string[] = ["alpha", "beta", "gamma", "delta"]; +const after: string[] = ["alpha", "gamma", "delta", "epsilon"]; +const entries: DiffEntry[] = diff(before, after); +for (const [op, value] of entries) console.log(op === 0 ? " " : op === 1 ? "-" : "+", value); +const counts = { kept: 0, actual: 0, expected: 0 }; +for (const entry of diff("abcdef", "azcdxf")) { + if (entry[0] === 0) counts.kept++; + else if (entry[0] === 1) counts.actual++; + else counts.expected++; +} +console.log(counts, diff("x", "x").length); diff --git a/tests/corpus/7707-util-mime-type.cjs b/tests/corpus/7707-util-mime-type.cjs new file mode 100644 index 000000000..e860d1d30 --- /dev/null +++ b/tests/corpus/7707-util-mime-type.cjs @@ -0,0 +1,87 @@ +// util.MIMEType and util.MIMEParams: parsing, serialization, setters, +// parameter maps, iteration, errors, inspect, JSON and %s formatting. +const util = require("util"); +const { MIMEType, MIMEParams, inspect, format } = util; +const show = (label, run) => { + try { console.log(label, run()); } catch (e) { console.log(label, e.name, e.code, e.message); } +}; +const inputs = [ + "text/javascript", " TEXT/HTML ; Charset=UTF-8 ", "application/json;a=1;b=2;a=3", "text/plain;x", "text/plain;x=", + 'text/plain;q="a\\"b\\\\c";r="unterminated', 'text/plain;q="ends with \\', "text/plain;;;k=v;;", "text/plain; k = v", + "text/plain;k=\"\"", "text/plain;é=1;v=é", "text/plain;k=vĀ", "image/svg+xml;charset=utf-8", + "text", "text/", "/plain", "te xt/plain", "text/pla in", "té/x", "", " \t\r\n", +]; +for (const input of inputs) { + show(JSON.stringify(input), () => { + const m = new MIMEType(input); + return [m.type, m.subtype, m.essence, String(m), m.params.toString(), [...m.params]]; + }); +} +show("parse ok", () => String(MIMEType.parse("Text/Plain;A=B"))); +show("parse bad", () => MIMEType.parse("nope")); +show("illegal", () => new MIMEType("text/plain", 1)); +show("null second", () => String(new MIMEType("text/plain", null))); +show("object input", () => String(new MIMEType({ toString: () => "a/b;c=d" }))); +const m = new MIMEType("text/plain;charset=ascii"); +m.type = "Application"; m.subtype = "JSON"; +show("setters", () => String(m)); +show("bad type", () => { m.type = "a b"; }); +show("bad subtype", () => { m.subtype = ""; }); +const p = m.params; +show("same params", () => p === m.params); +p.set("Charset", "utf-8"); p.set("q", "1 2"); p.set("empty", ""); +show("params", () => [String(m), p.get("CHARSET"), p.get("none"), p.has("q"), p.toJSON()]); +show("bad name", () => p.set("a b", "x")); +show("bad value", () => p.set("ok", "Ā")); +p.delete("q"); +show("after delete", () => [...p.keys()].join(",")); +show("entries", () => [...p.entries()]); +show("values", () => [...p.values()]); +const fresh = new MIMEParams(); +fresh.set("b", "2"); fresh.set("a", "1"); fresh.set("b", "3"); +show("fresh", () => [String(fresh), [...fresh]]); +show("json", () => JSON.stringify({ m, p: fresh })); +show("instanceof", () => [m instanceof MIMEType, p instanceof MIMEParams, p instanceof MIMEType, {} instanceof MIMEType]); +console.log(m, p); +console.log(inspect({ nested: { m } }, { depth: 0 })); +const live = new MIMEParams(); +live.set("a", "1"); live.set("b", "2"); live.set("c", "3"); +const seen = []; +for (const [k] of live) { seen.push(k); if (k === "a") { live.delete("b"); live.set("d", "4"); } } +console.log(seen, `${m}`); + +{ + show("number input", () => new MIMEType(5)); + show("undefined input", () => new MIMEType()); + const m = new MIMEType("a/b"); + const p = m.params; + p.set(1, 2); p.set("semi", "x;y"); p.set("quote", 'say "hi"'); p.set("back", "a\\b"); p.set("space", "a b"); + show("quoted out", () => p.toString()); + show("get coerced", () => [p.get(1), p.has(1), p.get(), p.has()]); + p.delete(1); + show("deleted", () => [...p.keys()]); + show("typeof", () => [typeof m, typeof p, typeof MIMEType]); + show("keys", () => [Object.keys(m), Object.keys(p), m.foo, p.size]); + show("upper set", () => { m.type = "TEXT"; m.subtype = "X-Thing+JSON"; return m.essence; }); + show("empty type", () => { m.type = ""; }); + show("type with slash", () => { m.type = "a/b"; }); + show("set name empty", () => p.set("", "v")); + show("format", () => format("%s|%j|%o", m, m, p)); + show("inspect arr", () => inspect([m, p, { m }])); + show("concat", () => "x" + m + p); + show("parse throws?", () => [MIMEType.parse("a/b;c=\"d"), MIMEType.parse("")]); + show("parse coerces", () => String(MIMEType.parse({ toString() { return "x/y;z=1"; } }))); + show("params iter", () => { const it = p[Symbol.iterator](); return [it.next(), it.next().value]; }); + show("entries next", () => { const it = p.entries(); return it.next(); }); + const bare = new MIMEType("a/b"); + console.log(Object.keys(bare), bare.foo, bare.params.size); + console.log(format("%s|%s", bare, bare.params), "%s", new MIMEType("c/d;e=f")); + // Every argument evaluates before the constructor or parse runs. + const order = []; + const note = (label) => { order.push(label); return label; }; + const coerced = { toString() { order.push("coerce"); return "a/b"; } }; + show("surplus, then coercion", () => { new MIMEType(coerced, undefined, note("third")); return order.splice(0).join(","); }); + show("surplus, then parse error", () => { try { new MIMEType("bad", undefined, note("x")); } catch (e) { order.push(e.code); } return order.splice(0).join(","); }); + show("parse surplus", () => { MIMEType.parse(coerced, note("second")); return order.splice(0).join(","); }); + show("params surplus", () => { new MIMEParams(note("p")); return order.splice(0).join(","); }); +} diff --git a/tests/corpus/7708-util-mime-type.ts b/tests/corpus/7708-util-mime-type.ts new file mode 100644 index 000000000..a3db92555 --- /dev/null +++ b/tests/corpus/7708-util-mime-type.ts @@ -0,0 +1,44 @@ +// util.MIMEType and util.MIMEParams through their TypeScript types. +import { MIMEType, MIMEParams, format, inspect } from "node:util"; + +function describe(m: MIMEType): string { + return `${m.essence} [${m.params.toString()}]`; +} + +const types: MIMEType[] = []; +for (const s of ["text/html;charset=utf-8", "application/ld+json", "multipart/form-data; boundary=abc"]) { + types.push(new MIMEType(s)); +} +for (const m of types) console.log(describe(m), m.type.length, m.subtype.toUpperCase()); + +const parsed: MIMEType | null = MIMEType.parse("bad"); +console.log(parsed === null, MIMEType.parse("a/b")?.essence); + +const params: MIMEParams = types[0].params; +params.set("level", "1"); +let joined = ""; +for (const [name, value] of params) joined += `${name}:${value} `; +console.log(joined.trim(), params.has("level"), params.get("missing") ?? "none"); +const keys: string[] = [...params.keys()]; +const values: string[] = Array.from(params.values()); +console.log(keys, values, JSON.stringify(types[0]), JSON.stringify(params)); + +try { + new MIMEType("nope"); +} catch (e) { + const err = e as NodeJS.ErrnoException; + console.log(err instanceof TypeError, err.code, err.message); +} +try { + types[1].type = "bad type"; +} catch (e) { + console.log((e as Error).message); +} +types[1].subtype = "xml"; +console.log(String(types[1]), types[1] instanceof MIMEType, inspect(types[1]), inspect(params)); +const empty = new MIMEParams(); +console.log(empty.toString() === "", [...empty].length, `${types[2]}`); +const seeded = new Map(params); +const pairs: [string, string][] = [...params.entries()]; +console.log(seeded.get("level"), seeded.size, pairs, [...empty.values()]); +console.log(format("%s / %s", types[0], params), `${params}`); diff --git a/tests/corpus/7709-function-own-properties.cjs b/tests/corpus/7709-function-own-properties.cjs new file mode 100644 index 000000000..7bf6fc77f --- /dev/null +++ b/tests/corpus/7709-function-own-properties.cjs @@ -0,0 +1,22 @@ +// Function values with own properties: inspect shows their own name and +// enumerable members. +const util = require("node:util"); + +function named() {} +named.x = 1; +named.list = [1, 2]; +console.log(named); + +const dep = util.deprecate(function inner(a, b) {}, "unused"); +dep.y = 2; +console.log(dep, Object.keys(dep)); +/** @type {any} */ +const held = dep; +console.log({ g: held }, [held]); +console.log({ a: { b: { c: { f: held } } } }); +Object.defineProperty(dep, "name", { value: "renamed" }); +Object.defineProperty(dep, "hidden", { value: 3, enumerable: false }); +console.log(dep, util.inspect(held, { compact: false })); + +const plain = util.callbackify(async () => 1); +console.log(plain); diff --git a/tests/corpus/7710-util-debounce.cjs b/tests/corpus/7710-util-debounce.cjs new file mode 100644 index 000000000..5ae49004d --- /dev/null +++ b/tests/corpus/7710-util-debounce.cjs @@ -0,0 +1,109 @@ +// util.debounce: trailing and leading calls, flush, cancel, rejectOnCancel, +// receivers, re-entry, options.signal, validation, the returned +// function's own members, and unref'd timers at exit. +const util = require("node:util"); + +const settle = (label, p) => p.then((v) => console.log(label, "ok", v), (e) => console.log(label, "err", e.name, e.message, e.cause instanceof Error ? e.cause.message : e.cause)); + +async function main() { + // Trailing calls collapse onto the last arguments. + let calls = 0; + const add = util.debounce(function (a, b) { calls++; return [this === add, a + b]; }, 20); + const p1 = add(1, 2), p2 = add(3, 4), p3 = add(5, 6); + console.log(add.pendingCount, add.pending === p3, p1 === p2); + console.log(await p1, await p2, await p3, calls, add.pendingCount, add.pending); + + // leading: the first call runs at once, later ones trail. + const lead = util.debounce((x) => x * 10, 15, { leading: true }); + const l1 = lead(1), l2 = lead(2); + console.log(lead.pendingCount); + console.log(await l1, await l2); + + // flush runs the pending call now; cancel rejects it. + const f = util.debounce((x) => `ran ${x}`, 1000); + const f1 = f("a"); + f.flush(); + console.log(await f1, f.pendingCount); + f.flush(); + const c1 = f("b"); + f.cancel(); + await settle("cancel", c1); + const c2 = f("c"); + f.cancel(new Error("stop")); + await settle("cancel-reason", c2); + f.cancel(); + + // rejectOnCancel rejects superseded calls. + const r = util.debounce((x) => x, 10, { rejectOnCancel: true }); + const r1 = r(1), r2 = r(2); + await settle("superseded", r1); + await settle("latest", r2); + + // A throwing fn rejects every waiting call. + const t = util.debounce(() => { throw new TypeError("bad"); }, 5); + const t1 = t(), t2 = t(); + await settle("throw1", t1); + await settle("throw2", t2); + + // ref and unref answer the function. + console.log(f.ref() === f, f.unref() === f, f.ref() === f); + + // options.signal: aborting rejects pending calls and every later call. + const ac = new AbortController(); + const s = util.debounce((x) => x, 1000, { signal: ac.signal }); + const s1 = s(1); + ac.abort("reason!"); + await settle("abort-pending", s1); + await settle("abort-later", s(2)); + try { + util.debounce(() => 1, 5, { signal: AbortSignal.abort("pre") }); + } catch (e) { + console.log("pre-aborted", e.name, e.code, e.message, e.cause); + } + + // Ordering against microtasks and a timer of the same delay. + const order = []; + const o = util.debounce(() => order.push("debounced"), 10); + o().then(() => order.push("then")); + setTimeout(() => order.push("timer"), 10); + Promise.resolve().then(() => order.push("micro")); + await new Promise((resolve) => setTimeout(resolve, 40)); + console.log(order); +} +main().then(tail); + +function tail() { + const tryIt = (run) => { try { run(); } catch (e) { console.log(e.name, e.code, e.message); } }; + tryIt(() => util.debounce(1, 5)); + tryIt(() => util.debounce(() => 1, -1)); + tryIt(() => util.debounce(() => 1, 1.5)); + tryIt(() => util.debounce(() => 1, "5")); + tryIt(() => util.debounce(() => 1, 5, null)); + tryIt(() => util.debounce(() => 1, 5, [1])); + tryIt(() => util.debounce(() => 1, 5, { leading: 1 })); + tryIt(() => util.debounce(() => 1, 5, { rejectOnCancel: "yes" })); + tryIt(() => util.debounce(() => 1, 5, { signal: {} })); + + const d = util.debounce(function named(a, b) { return a + b; }, 5); + console.log(typeof d, d.name, d.length, Object.keys(d), Object.getOwnPropertyNames(d)); + console.log(d); + const p = d(1, 2); + console.log(d.pending === p, d.pendingCount); + d.flush(); + + const holder = {}; + holder.run = util.debounce(function () { return this === holder.run; }, 5); + holder.run().then((same) => console.log("receiver is the function", same)); + let depth = 0; + const again = util.debounce(function (n) { + depth++; + if (n > 0) again(n - 1).then((v) => console.log("inner", v)); + return n; + }, 5); + again(2).then((v) => console.log("outer", v, depth)); + + const unrefd = util.debounce(() => console.log("never"), 500); + unrefd(); + unrefd.unref(); + process.on("exit", () => console.log("exit", unrefd.pendingCount)); +} diff --git a/tests/corpus/7711-util-throttle.cjs b/tests/corpus/7711-util-throttle.cjs new file mode 100644 index 000000000..f776e7174 --- /dev/null +++ b/tests/corpus/7711-util-throttle.cjs @@ -0,0 +1,101 @@ +// util.throttle: limits per interval, queueing, overflow 'drop', maxPending, +// concurrency, strict windows, cancel, options.signal, validation and the +// returned function's own members. +const util = require("node:util"); + +const settle = (label, p) => p.then((v) => console.log(label, "ok", v), (e) => console.log(label, "err", e.name, e.code, e.message)); +const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + +async function main() { + // Two calls per 60 ms; the rest queue in order. + const seen = []; + const t = util.throttle(function (x) { seen.push(x); return x * 2; }, 2, 60); + const ps = [1, 2, 3, 4, 5].map((x) => t(x)); + console.log(seen, t.pendingCount, t.activeCount, t.pending === ps[4], t.hasImmediateCapacity()); + console.log(await Promise.all(ps), seen); + + // overflow 'drop' rejects with ERR_THROTTLED once the window is full. + const d = util.throttle((x) => x, 1, 1000, { overflow: "drop" }); + await settle("drop-first", d(1)); + await settle("drop-second", d(2)); + d.cancel(); + await settle("after-cancel", d(3)); + + // maxPending bounds the queue. + const m = util.throttle((x) => x, 1, 30, { maxPending: 1 }); + const m1 = m(1), m2 = m(2), m3 = m(3); + await settle("max1", m1); + await settle("max3", m3); + await settle("max2", m2); + + // concurrency counts unsettled async calls. + const order = []; + const c = util.throttle(async (x) => { order.push(`start ${x}`); await sleep(15); order.push(`end ${x}`); return x; }, 10, 1000, { concurrency: 1 }); + const ca = c("a"), cb = c("b"); + console.log(c.activeCount, c.pendingCount); + console.log(await ca, await cb, order, c.activeCount); + + // A queue that never empties while calls are taken and others arrive. + const q = util.throttle(async (x) => { await null; return x; }, 1000, 1000, { concurrency: 1 }); + const queued = []; + for (let i = 0; i < 5; i++) queued.push(q(i)); + const results = []; + for (let i = 5; i < 25; i++) { + results.push(await queued[i - 5]); + queued.push(q(i)); + } + for (let i = 20; i < 25; i++) results.push(await queued[i]); + console.log(results.join(","), q.pendingCount, q.activeCount); + + // A throwing fn rejects that call only. + const e = util.throttle((x) => { if (x === 2) throw new RangeError("two"); return x; }, 5, 10); + await settle("e1", e(1)); + await settle("e2", e(2)); + await settle("e3", e(3)); + + // strict keeps a rolling window. + const s = util.throttle((x) => x, 1, 25, { strict: true }); + const s1 = s(1), s2 = s(2); + console.log(s.hasImmediateCapacity(), await s1, await s2); + + // cancel rejects the queue and resets the limiter. + const k = util.throttle((x) => x, 1, 1000); + const k1 = k(1), k2 = k(2); + k.cancel("why"); + await settle("k1", k1); + await settle("k2", k2); + console.log(k.hasImmediateCapacity(), k.ref() === k, k.unref() === k); + await settle("k3", k(3)); + + // options.signal + const ac = new AbortController(); + const a = util.throttle((x) => x, 1, 1000, { signal: ac.signal }); + await settle("a1", a(1)); + const a2 = a(2); + ac.abort(); + await settle("a2", a2); + await settle("a3", a(3)); + console.log(a.hasImmediateCapacity(), a.pendingCount); +} +main().then(tail); + +function tail() { + const tryIt = (run) => { try { run(); } catch (e) { console.log(e.name, e.code, e.message); } }; + tryIt(() => util.throttle(null, 1, 5)); + tryIt(() => util.throttle(() => 1, 0, 5)); + tryIt(() => util.throttle(() => 1, 1.5, 5)); + tryIt(() => util.throttle(() => 1, 1, 2 ** 31)); + tryIt(() => util.throttle(() => 1, 1, 5, "options")); + tryIt(() => util.throttle(() => 1, 1, 5, { concurrency: 0 })); + tryIt(() => util.throttle(() => 1, 1, 5, { concurrency: "2" })); + tryIt(() => util.throttle(() => 1, 1, 5, { overflow: "x" })); + tryIt(() => util.throttle(() => 1, 1, 5, { maxPending: -1 })); + tryIt(() => util.throttle(() => 1, 1, 5, { strict: "y" })); + tryIt(() => util.throttle(() => 1, 1, 5, { signal: AbortSignal.abort("pre") })); + + const t = util.throttle(() => 1, 1, 50, { overflow: "drop" }); + console.log(Object.keys(t), Object.getOwnPropertyNames(t), t.name, t.length); + console.log(t); + t(); + t().catch((e) => console.log(e.name, e.code, e.message, Object.keys(e))); +} diff --git a/tests/corpus/7712-util-debounce-throttle.ts b/tests/corpus/7712-util-debounce-throttle.ts new file mode 100644 index 000000000..007635bac --- /dev/null +++ b/tests/corpus/7712-util-debounce-throttle.ts @@ -0,0 +1,27 @@ +// util.debounce and util.throttle through their TypeScript types. +import { debounce, throttle } from "node:util"; + +async function main(): Promise { + const save = debounce((text: string, times: number): string => text.repeat(times), 10); + const first = save("a", 1); + const last = save("b", 3); + console.log(save.pendingCount, save.pending !== null); + const results: string[] = await Promise.all([first, last]); + console.log(results, save.pendingCount); + + const doubled = throttle(async (n: number): Promise => n * 2, 2, 20); + const values: number[] = await Promise.all([doubled(1), doubled(2), doubled(3)]); + console.log(values, doubled.activeCount, doubled.hasImmediateCapacity()); + + const later = debounce((n: number) => n, 1000); + const pending = later(5); + later.cancel("stopped"); + try { + await pending; + } catch (e) { + const err = e as Error; + console.log(err.name, err.message, err.cause); + } + console.log(later.ref() === later, later.unref() === later); +} +main(); diff --git a/tests/corpus/7713-buffer-numeric-untyped-arguments.cjs b/tests/corpus/7713-buffer-numeric-untyped-arguments.cjs new file mode 100644 index 000000000..81c9d767a --- /dev/null +++ b/tests/corpus/7713-buffer-numeric-untyped-arguments.cjs @@ -0,0 +1,22 @@ +// Buffer's numeric read/write methods with arguments that are not numbers +// (untyped JavaScript): Node validates them at runtime. +const buf = Buffer.from([1, 2, 3, 4, 5, 6, 7, 8]); +const attempts = [ + () => buf.readUInt8("0"), + () => buf.readUInt16LE("1"), + () => buf.readInt32BE(true), + () => buf.readDoubleLE(null), + () => buf.writeUInt8(1, "0"), + () => buf.writeUInt16BE("258", 0), + () => buf.writeInt8("x", 0), + () => buf.readUInt8(undefined), + () => buf.readUInt8(1), +]; +for (const attempt of attempts) { + try { + console.log("ok", attempt()); + } catch (error) { + console.log(error.name, error.code, error.message); + } +} +console.log(Array.from(buf).join(",")); diff --git a/tests/corpus/7714-zlib-brotli-compress.cjs b/tests/corpus/7714-zlib-brotli-compress.cjs new file mode 100644 index 000000000..9f12f35f8 --- /dev/null +++ b/tests/corpus/7714-zlib-brotli-compress.cjs @@ -0,0 +1,91 @@ +// zlib's Brotli compression over the vendored brotli 1.2.0, the version +// Node bundles: compressed bytes match Node's for the same parameters, and +// the options, errors, callback, promisified and stream forms follow Node. +const zlib = require("node:zlib"); +const { promisify } = require("node:util"); + +const text = Buffer.from("hello hello hello brotli ".repeat(20)); +const hex = (b) => b.toString("hex"); +const tryIt = (label, run) => { + try { + const r = run(); + console.log(label, "ok", Buffer.isBuffer(r) ? hex(r) : r); + } catch (e) { + console.log(label, e.name, e.code, e.errno, e.message); + } +}; + +console.log(hex(zlib.brotliCompressSync("")), hex(zlib.brotliCompressSync("a"))); +for (const quality of [0, 1, 4, 5, 9, 11]) { + const out = zlib.brotliCompressSync(text, { params: { [zlib.constants.BROTLI_PARAM_QUALITY]: quality } }); + console.log("quality", quality, out.length, hex(out)); +} +console.log("text mode", hex(zlib.brotliCompressSync(text, { params: { [zlib.constants.BROTLI_PARAM_MODE]: zlib.constants.BROTLI_MODE_TEXT } }))); +console.log("font mode", hex(zlib.brotliCompressSync(text, { params: { [zlib.constants.BROTLI_PARAM_MODE]: zlib.constants.BROTLI_MODE_FONT } }))); +console.log( + "window", + hex(zlib.brotliCompressSync(text, { + params: { [zlib.constants.BROTLI_PARAM_LGWIN]: 10, [zlib.constants.BROTLI_PARAM_SIZE_HINT]: text.length }, + })), +); +console.log("dictionary", hex(zlib.brotliCompressSync("hello world", { dictionary: Buffer.from("hello world") }))); +console.log("arraybuffer dictionary", hex(zlib.brotliCompressSync("hello world", { dictionary: new Uint8Array([104, 101, 108, 108, 111]).buffer }))); +console.log("finishFlush", hex(zlib.brotliCompressSync("hello", { finishFlush: zlib.constants.BROTLI_OPERATION_FLUSH }))); +tryIt("boolean param", () => zlib.brotliCompressSync("x", { params: { [zlib.constants.BROTLI_PARAM_DISABLE_LITERAL_CONTEXT_MODELING]: true } })); +tryIt("fractional quality", () => zlib.brotliCompressSync("hello hello hello", { params: { 1: 4.7 } })); +tryIt("negative quality", () => zlib.brotliCompressSync("hello hello hello", { params: { 1: -1 } })); +tryIt("quality above 11", () => zlib.brotliCompressSync("hello hello hello", { params: { 1: 12 } })); +tryIt("unknown param", () => zlib.brotliCompressSync("x", { params: { 9999: 1 } })); +tryIt("param 9", () => zlib.brotliCompressSync("x", { params: { 9: 0 } })); +tryIt("repeated param", () => zlib.brotliCompressSync("x", { params: { 1: 5, "01": 4 } })); +tryIt("fractional param key", () => zlib.brotliCompressSync("x", { params: { "1.5": 5 } })); +tryIt("negative param key", () => zlib.brotliCompressSync("x", { params: { "-1": 5 } })); +tryIt("string param value", () => zlib.brotliCompressSync("x", { params: { 1: "5" } })); +tryIt("string dictionary", () => zlib.brotliCompressSync("x", { dictionary: "abc" })); +tryIt("chunkSize", () => zlib.brotliCompressSync(text, { chunkSize: 10 })); +tryIt("flush", () => zlib.brotliCompressSync(text, { flush: 4 })); +tryIt("finishFlush range", () => zlib.brotliCompressSync(text, { finishFlush: 99 })); +tryIt("rejectGarbageAfterEnd", () => zlib.brotliCompressSync(text, { rejectGarbageAfterEnd: 1 })); +tryIt("maxOutputLength", () => zlib.brotliCompressSync(text, { maxOutputLength: 8 })); +tryIt("number input", () => zlib.brotliCompressSync(JSON.parse("5"))); + +zlib.brotliCompress(text, (err, out) => { + console.log("callback", err, hex(out)); + zlib.brotliCompress("abc", { params: { [zlib.constants.BROTLI_PARAM_QUALITY]: 2 } }, (err2, out2) => { + console.log("callback options", err2, hex(out2)); + promised(); + }); +}); + +function promised() { + const compress = promisify(zlib.brotliCompress); + compress("promised").then((out) => { + console.log("promisified", hex(out)); + streams(); + }); +} + +function streams() { + const chunks = []; + const stream = zlib.createBrotliCompress({ params: { [zlib.constants.BROTLI_PARAM_QUALITY]: 5 } }); + console.log(stream instanceof zlib.BrotliCompress, stream instanceof zlib.Gzip); + stream.on("data", (chunk) => chunks.push(chunk)); + stream.on("end", () => { + console.log("stream", hex(Buffer.concat(chunks)), stream.bytesWritten); + const second = new zlib.BrotliCompress(); + const parts = []; + second.on("data", (chunk) => parts.push(chunk)); + second.on("end", () => { + console.log("class", hex(Buffer.concat(parts))); + try { zlib.createBrotliCompress({ params: { 99: 1 } }); } catch (e) { console.log(e.code, e.message); } + try { zlib.createBrotliCompress({ flush: 9 }); } catch (e) { console.log(e.code, e.message); } + }); + second.reset(); + second.end("reset first"); + }); + stream.write(text.subarray(0, 100)); + stream.flush(() => { + console.log("flushed", chunks.length > 0); + stream.end(text.subarray(100)); + }); +} diff --git a/tests/corpus/7715-zlib-brotli-decompress.cjs b/tests/corpus/7715-zlib-brotli-decompress.cjs new file mode 100644 index 000000000..c2f826f75 --- /dev/null +++ b/tests/corpus/7715-zlib-brotli-decompress.cjs @@ -0,0 +1,96 @@ +// zlib's Brotli decompression: fixed blobs Node produced, Brotli's own +// error codes (ERR_ + the decoder's error name) and zlib's for truncated +// input, trailing data, maxOutputLength, dictionaries, the decoder +// parameters, and the callback, promisified and stream forms. +const zlib = require("node:zlib"); +const { promisify } = require("node:util"); +const { pipeline, Readable, Writable } = require("node:stream"); + +const hello = Buffer.from("1b1600f88d946ede44558696206c6f350b32b54006543b", "hex"); +const repeated = Buffer.from("1b8713f825f0e2b14020680100", "hex"); +const withDictionary = Buffer.from("1b0a00f8250002980820", "hex"); +const tryIt = (label, run) => { + try { + const r = run(); + console.log(label, "ok", Buffer.isBuffer(r) ? r.length + " " + r.toString().slice(0, 24) : r); + } catch (e) { + console.log(label, e.name, e.code, e.errno, e.message); + } +}; + +tryIt("hello", () => zlib.brotliDecompressSync(hello)); +tryIt("repeated", () => zlib.brotliDecompressSync(repeated)); +tryIt("string input", () => zlib.brotliDecompressSync("abc")); +tryIt("arraybuffer input", () => zlib.brotliDecompressSync(hello.buffer.slice(hello.byteOffset, hello.byteOffset + hello.length))); +for (const hex of ["ff", "ffffffff", "ce", "0b008061", "3b00", "06", "2f", "1bffff00ffffffffffff"]) { + tryIt(hex, () => zlib.brotliDecompressSync(Buffer.from(hex, "hex"))); +} +tryIt("truncated", () => zlib.brotliDecompressSync(hello.subarray(0, 10))); +tryIt("trailing data", () => zlib.brotliDecompressSync(Buffer.concat([hello, Buffer.from("junk")]))); +tryIt("trailing data rejected", () => zlib.brotliDecompressSync(Buffer.concat([hello, Buffer.from("junk")]), { rejectGarbageAfterEnd: true })); +tryIt("maxOutputLength", () => zlib.brotliDecompressSync(repeated, { maxOutputLength: 100 })); +tryIt("small chunks", () => zlib.brotliDecompressSync(repeated, { chunkSize: 64 })); +tryIt("partial with flush", () => zlib.brotliDecompressSync(hello.subarray(0, 8), { finishFlush: zlib.constants.BROTLI_OPERATION_FLUSH })); +tryIt("dictionary", () => zlib.brotliDecompressSync(withDictionary, { dictionary: Buffer.from("hello world") })); +tryIt("missing dictionary", () => zlib.brotliDecompressSync(withDictionary)); +tryIt("large window", () => zlib.brotliDecompressSync(hello, { params: { [zlib.constants.BROTLI_DECODER_PARAM_LARGE_WINDOW]: 1 } })); +tryIt("encoder param", () => zlib.brotliDecompressSync(hello, { params: { [zlib.constants.BROTLI_PARAM_LGWIN]: 10 } })); +tryIt("round trip", () => zlib.brotliDecompressSync(zlib.brotliCompressSync("round trip ".repeat(100))).toString() === "round trip ".repeat(100)); + +zlib.brotliDecompress(hello, (err, out) => { + console.log("callback", err, out.toString()); + zlib.brotliDecompress(Buffer.from([1, 2, 3]), (err2) => { + console.log("callback error", err2.code, err2.errno, err2.message); + promised(); + }); +}); + +function promised() { + const decompress = promisify(zlib.brotliDecompress); + decompress(repeated, { maxOutputLength: 10 }).catch((err) => { + console.log("promisified", err.code, err.message); + streams(); + }); +} + +function streams() { + const out = []; + const stream = zlib.createBrotliDecompress(); + console.log(stream instanceof zlib.BrotliDecompress, stream instanceof zlib.BrotliCompress); + stream.on("data", (chunk) => out.push(chunk)); + stream.on("end", () => { + console.log("stream", Buffer.concat(out).toString(), stream.bytesWritten); + const bad = new zlib.BrotliDecompress(); + bad.on("error", (err) => { + console.log("stream error", err.code, err.errno, err.message); + piped(); + }); + bad.write(Buffer.from("ffff", "hex")); + bad.end(); + }); + stream.write(hello.subarray(0, 5)); + stream.end(hello.subarray(5)); +} + +function piped() { + const sink = []; + pipeline( + Readable.from([Buffer.from("one "), Buffer.from("two "), Buffer.from("three")]), + zlib.createBrotliCompress(), + zlib.createBrotliDecompress(), + new Writable({ + write(chunk, _encoding, callback) { + sink.push(chunk); + callback(); + }, + }), + (err) => { + console.log("pipeline", err, Buffer.concat(sink).toString()); + try { + new zlib.BrotliDecompress({ params: { 3: 1 } }); + } catch (e) { + console.log(e.code, e.message); + } + }, + ); +} diff --git a/tests/corpus/7716-zlib-brotli.ts b/tests/corpus/7716-zlib-brotli.ts new file mode 100644 index 000000000..812cf6fae --- /dev/null +++ b/tests/corpus/7716-zlib-brotli.ts @@ -0,0 +1,47 @@ +// zlib's Brotli forms through their TypeScript types: typed options, +// dictionaries, maxOutputLength, the callback and promisified forms, and the +// BrotliCompress and BrotliDecompress classes. +import * as zlib from "node:zlib"; +import { promisify } from "node:util"; + +const input: string = "typed brotli ".repeat(64); +const compressed: Buffer = zlib.brotliCompressSync(input, { + params: { [zlib.constants.BROTLI_PARAM_QUALITY]: 4, [zlib.constants.BROTLI_PARAM_MODE]: zlib.constants.BROTLI_MODE_TEXT }, +}); +console.log(compressed.toString("base64")); +console.log(zlib.brotliDecompressSync(compressed).toString() === input); +const dict: Buffer = Buffer.from("typed brotli"); +const withDict = zlib.brotliCompressSync(input, { dictionary: dict }); +console.log(withDict.length, zlib.brotliDecompressSync(withDict, { dictionary: dict }).length); +try { + zlib.brotliDecompressSync(compressed, { maxOutputLength: 16 }); +} catch (e) { + const err = e as NodeJS.ErrnoException; + console.log(err.code, err.message); +} +zlib.brotliCompress(Buffer.from("abc"), (err, out) => { + console.log(err, out.toString("hex")); + zlib.brotliDecompress(out, { finishFlush: zlib.constants.BROTLI_OPERATION_FINISH }, (err2, back) => { + console.log(err2, back.toString()); + main(); + }); +}); +const compressAsync = promisify(zlib.brotliCompress); +const decompressAsync = promisify(zlib.brotliDecompress); +async function main(): Promise { + const packed = await compressAsync("promised"); + console.log((await decompressAsync(packed)).toString()); + try { + await decompressAsync(Buffer.from([0xff])); + } catch (e) { + console.log((e as NodeJS.ErrnoException).code); + } + const stream: zlib.BrotliCompress = new zlib.BrotliCompress(); + const parts: Buffer[] = []; + stream.on("data", (c: Buffer) => parts.push(c)); + stream.on("end", () => console.log("stream", Buffer.concat(parts).equals(zlib.brotliCompressSync("streamed")), stream.bytesWritten)); + stream.end("streamed"); + const dec = zlib.createBrotliDecompress(); + dec.reset(); + console.log(dec instanceof zlib.BrotliDecompress); +} diff --git a/tests/corpus/7717-math-fdlibm.ts b/tests/corpus/7717-math-fdlibm.ts new file mode 100644 index 000000000..91ce02adf --- /dev/null +++ b/tests/corpus/7717-math-fdlibm.ts @@ -0,0 +1,97 @@ +// Math's transcendental functions print the values Node prints: each line +// digests the result bits of one function over a few thousand inputs, so +// a difference in the last bit of any result changes the line. Sample +// values print in full, and pow's special cases follow V8's. +const bits = new Float64Array(1); +const words = new Uint32Array(bits.buffer); + +function digest(values: number[]): string { + let h1 = 0x811c9dc5; + let h2 = 0x01000193; + for (const v of values) { + // NaN prints as NaN; its bits are not part of the result. + bits[0] = Number.isNaN(v) ? 0 : v; + h1 = Math.imul(h1 ^ words[0], 0x01000193) >>> 0; + h2 = Math.imul(h2 ^ words[1], 0x01000193) >>> 0; + } + return `${h1}:${h2}`; +} + +let seed = 12345; +function next(): number { + seed = (seed * 1103515245 + 12345) % 2147483648; + return seed / 2147483648; +} + +const wide: number[] = []; +const unit: number[] = []; +const small: number[] = []; +for (let i = 0; i < 3000; i++) { + wide.push((next() - 0.5) * Math.pow(10, Math.floor(next() * 12) - 4)); + unit.push(next() * 2 - 1); + small.push((next() - 0.5) * 40); +} +const positive = wide.map((x) => Math.abs(x)); +const edges = [0, -0, 1, -1, 0.5, -0.5, 2, Math.PI, Math.E, 1e-300, 5e-324, 1e300, Infinity, -Infinity, NaN]; + +const unary: [string, (x: number) => number, number[]][] = [ + ["sin", Math.sin, wide], + ["cos", Math.cos, wide], + ["tan", Math.tan, wide], + ["asin", Math.asin, unit], + ["acos", Math.acos, unit], + ["atan", Math.atan, wide], + ["sinh", Math.sinh, small], + ["cosh", Math.cosh, small], + ["tanh", Math.tanh, small], + ["asinh", Math.asinh, wide], + ["acosh", Math.acosh, positive.map((x) => x + 1)], + ["atanh", Math.atanh, unit], + ["exp", Math.exp, small], + ["expm1", Math.expm1, small], + ["log", Math.log, positive], + ["log1p", Math.log1p, positive], + ["log2", Math.log2, positive], + ["log10", Math.log10, positive], + ["cbrt", Math.cbrt, wide], + ["sqrt", Math.sqrt, positive], +]; +for (const [name, fn, inputs] of unary) { + console.log(name, digest(inputs.map(fn)), digest(edges.map(fn)), fn(0.3), fn(0.75)); +} + +// Math.sin(i) and Math.cos(i * 0.37) for i below 4096: main's libm calls +// printed 284 of these 8192 values differently from Node. +const sines: number[] = []; +const cosines: number[] = []; +for (let i = 0; i < 4096; i++) { + sines.push(Math.sin(i)); + cosines.push(Math.cos(i * 0.37)); +} +console.log("sin(i)", digest(sines), "cos(i*0.37)", digest(cosines), Math.cos(2.59)); + +const atan2: number[] = []; +const pow: number[] = []; +for (let i = 0; i < wide.length; i++) { + atan2.push(Math.atan2(wide[i], small[i])); + pow.push(Math.pow(positive[i] % 50, small[i] / 4)); +} +console.log("atan2", digest(atan2), Math.atan2(2.5, 3), Math.atan2(-0, -1)); +console.log("pow", digest(pow), Math.pow(1.1, 0.3), 1.0000001 ** 1e7); + +// V8 answers these itself: x * x, the square root, and NaN for ±1 ** ±Infinity. +const squares = wide.map((x) => x ** 2); +const roots = positive.map((x) => Math.pow(x, 0.5)); +console.log("pow 2", digest(squares), "pow 0.5", digest(roots)); +// The same exponents when they are only known at run time. +const two = Number(process.argv[2] ?? "2"); +const half = two / 4; +console.log("pow runtime 2", digest(wide.map((x) => x ** two)), "0.5", digest(positive.map((x) => Math.pow(x, half)))); +console.log(Math.pow(-Infinity, 0.5), Math.pow(-0, 0.5), (-8) ** 0.5, 1 ** Infinity, (-1) ** -Infinity, NaN ** 0); +// Rounding is exact: Math.floor, Math.ceil and Math.trunc over the same +// inputs and over halves, signed zeros included. +const halves = wide.map((x, i) => Math.round(x * 4) / 4 - (i % 2 === 0 ? 0 : 0.5)); +console.log("floor", digest(wide.map(Math.floor)), digest(halves.map(Math.floor)), digest(edges.map(Math.floor))); +console.log("ceil", digest(wide.map(Math.ceil)), digest(halves.map(Math.ceil)), digest(edges.map(Math.ceil))); +console.log("trunc", digest(wide.map(Math.trunc)), digest(halves.map(Math.trunc)), Object.is(Math.ceil(-0.5), -0), Object.is(Math.floor(-0), -0)); +console.log(Math.hypot(3, 4), Math.hypot(1e200, 1e200), 7 % 2.5, -7.5 % 2, Math.floor(-0.5), Math.ceil(-0.5), Math.round(2.5), Math.trunc(-4.7)); diff --git a/tests/corpus/7718-math-fdlibm.cjs b/tests/corpus/7718-math-fdlibm.cjs new file mode 100644 index 000000000..35d26f7e8 --- /dev/null +++ b/tests/corpus/7718-math-fdlibm.cjs @@ -0,0 +1,12 @@ +// Math functions and ** over untyped values print the values Node prints +// (V8's fdlibm results, and V8's pow special cases). +const values = JSON.parse("[0.3, 0.75, 2.5, -7.25, 1e-8, 123456.789, 1e21]"); +const rows = []; +for (const x of values) { + rows.push([Math.sin(x), Math.cos(x), Math.tan(x), Math.exp(x / 100), Math.cbrt(x), Math.atan2(x, 3)].join(" ")); + rows.push([Math.log(Math.abs(x)), Math.log10(Math.abs(x)), Math.sinh(x / 1e5), Math.cosh(x / 1e5), Math.expm1(x / 1e6)].join(" ")); + rows.push([x ** 2, x ** 0.5, Math.abs(x) ** 0.3, x % 1.5].join(" ")); +} +console.log(rows.join("\n")); +const edges = JSON.parse('[1, -1, null]'); +console.log(edges[0] ** Infinity, edges[1] ** -Infinity, Number(edges[2]) ** 0.5, (-Infinity) ** 0.5); diff --git a/tests/corpus/7720-array-subclass-basics.ts b/tests/corpus/7720-array-subclass-basics.ts new file mode 100644 index 000000000..1e5130f9d --- /dev/null +++ b/tests/corpus/7720-array-subclass-basics.ts @@ -0,0 +1,44 @@ +// A class that extends Array: construction (one number argument sets the +// length, other arguments become the elements), length and index writes, +// Array.isArray, instanceof, constructor, inherited methods, the subclass's +// own fields and methods, and how console.log and JSON show the instance. +class Stack extends Array { + pushes = 0; + + top(): number | undefined { + return this[this.length - 1]; + } + + add(value: number): this { + this.push(value); + this.pushes++; + return this; + } +} + +const s = new Stack(); +s.add(1).add(2).add(3); +console.log(s.length, s.top(), s.pushes); +console.log(Array.isArray(s), s instanceof Stack, s instanceof Array, s.constructor === Stack); +console.log(s); +console.log(JSON.stringify(s)); +s[5] = 9; +console.log(s.length, s[4], s); +s.length = 2; +console.log(s, s.indexOf(2), s.includes(3), s.join("-")); +s.reverse(); +console.log(s.top(), s); + +const sized = new Stack(3); +console.log(sized.length, sized, 0 in sized); +const listed = new Stack(4, 5); +console.log(listed.length, listed); + +try { + new Stack(-1); +} catch (e) { + console.log((e as Error).name, (e as Error).message); +} + +for (const v of listed) console.log("item", v); +console.log([...listed, 6], Object.keys(listed)); diff --git a/tests/corpus/7721-array-subclass-species.ts b/tests/corpus/7721-array-subclass-species.ts new file mode 100644 index 000000000..03e0d19ab --- /dev/null +++ b/tests/corpus/7721-array-subclass-species.ts @@ -0,0 +1,37 @@ +// Array.prototype methods that create a new array construct it through the +// subclass (ArraySpeciesCreate): map, filter, slice, splice, concat, flat +// and flatMap return an instance of the subclass, built by calling its +// constructor with the length the specification passes. Methods that +// return the receiver keep it, and Array.of / Array.from inherited as +// statics construct the subclass too. +class Logged extends Array { + constructor(...args: number[]) { + super(...args); + console.log("construct", args.length === 1 ? `length ${args[0]}` : `items ${args.join(",")}`); + } + + sum(): number { + let total = 0; + for (const v of this) total += v; + return total; + } +} + +const base = Logged.of(1, 2, 3, 4); +console.log(base instanceof Logged, base); +const doubled = base.map((v) => v * 2); +console.log(doubled instanceof Logged, (doubled as Logged).sum(), doubled); +const even = base.filter((v) => v % 2 === 0); +console.log(even instanceof Logged, even); +const part = base.slice(1, 3); +console.log(part instanceof Logged, part); +const removed = base.splice(0, 1); +console.log(removed instanceof Logged, removed, base); +const joined = base.concat([7, 8]); +console.log(joined instanceof Logged, joined.length, joined); +const flat = base.flatMap((v) => [v, v]); +console.log(flat instanceof Logged, flat.length); +const filled = base.fill(0, 0, 1); +console.log(filled === base, base); +const from = Logged.from([5, 6]); +console.log(from instanceof Logged, (from as Logged).sum()); diff --git a/tests/corpus/7722-array-subclass-zero-array.js b/tests/corpus/7722-array-subclass-zero-array.js new file mode 100644 index 000000000..1ed52d386 --- /dev/null +++ b/tests/corpus/7722-array-subclass-zero-array.js @@ -0,0 +1,16 @@ +// lru-cache's ZeroArray: a JavaScript class extending Array whose +// constructor fills the new array with zeros, constructed directly and +// through map (species construction runs the constructor again). +class ZeroArray extends Array { + constructor(size) { + super(size); + this.fill(0); + } +} + +const z = new ZeroArray(4); +z[1] = 7; +console.log(z, z.length, z instanceof ZeroArray, Array.isArray(z)); +const m = z.map((v) => v + 1); +console.log(m, m instanceof ZeroArray); +console.log(z.slice(1) instanceof ZeroArray, String(z)); diff --git a/tests/corpus/7723-array-subclass-as-array.ts b/tests/corpus/7723-array-subclass-as-array.ts new file mode 100644 index 000000000..5faf5b13f --- /dev/null +++ b/tests/corpus/7723-array-subclass-as-array.ts @@ -0,0 +1,21 @@ +// A subclass instance passed where a plain array is expected is the same +// object: writes through either view are seen through the other, and +// species construction still applies in code typed with the plain array. +class Path extends Array { + label = "path"; +} + +function addSegment(parts: string[], segment: string): string[] { + parts.push(segment); + return parts.filter((part) => part.length > 0); +} + +const p = new Path(); +p.push("a", "", "b"); +const filtered = addSegment(p, "c"); +console.log(p.length, filtered.length, filtered instanceof Path, (filtered as Path).label); +const view: string[] = p; +view[0] = "z"; +console.log(p[0], view === p, p.label); +console.log(p.map((s) => s.toUpperCase())); +console.log(Object.keys(p)); diff --git a/tests/corpus/7724-array-subclass-cycles.ts b/tests/corpus/7724-array-subclass-cycles.ts new file mode 100644 index 000000000..9a1a7b813 --- /dev/null +++ b/tests/corpus/7724-array-subclass-cycles.ts @@ -0,0 +1,36 @@ +// An Array subclass whose elements and fields refer back to the instance: +// the cycles are collected once nothing outside them is reachable, and +// the elements and fields read back as Node reads them. +class Member { + name: string; + group: Group | undefined; + constructor(name: string, group: Group | undefined) { + this.name = name; + this.group = group; + } +} + +class Group extends Array { + owner: Member | undefined = undefined; + + add(name: string): Member { + const member = new Member(name, this); + this.push(member); + return member; + } +} + +function build(round: number): number { + const g = new Group(); + const lead = g.add(`lead${round}`); + g.add(`member${round}`); + g.owner = lead; + return g.length + (g[0]!.group === g ? 1 : 0) + (g.owner === lead ? 1 : 0); +} + +let total = 0; +for (let i = 0; i < 200; i++) total += build(i); +console.log(total); +const kept = new Group(); +kept.add("a").group?.add("b"); +console.log(kept.length, kept.map((m) => m.name).join(","), kept[1]!.group === kept); diff --git a/tests/corpus/7725-array-subclass-operations.ts b/tests/corpus/7725-array-subclass-operations.ts new file mode 100644 index 000000000..94cfa8b04 --- /dev/null +++ b/tests/corpus/7725-array-subclass-operations.ts @@ -0,0 +1,46 @@ +// Array operations on an Array subclass instance: iteration, spread, +// destructuring, the mutators that return the instance itself, searches, +// length writes, Array.isArray through unknown, instanceof through unknown, +// and JSON. +class Row extends Array { + tag = "row"; + #reads = 0; + + static of2(a: string, b: string): Row { + const row = new Row(); + row.push(a, b); + return row; + } + + get size(): number { + this.#reads++; + return this.length; + } + + get reads(): number { + return this.#reads; + } + + label(): string { + return `${this.tag}:${this.join("|")}`; + } +} + +const r = new Row(); +console.log(r.push("b", "a", "c"), r.length, r.label()); +const [first, ...rest] = r; +console.log(first, rest, [...r].length); +for (const [i, v] of r.entries()) console.log(i, v); +console.log(r.sort() === r, r.label(), r.reverse() === r, r.label()); +console.log(r.fill("x", 1) === r, r.label(), r.indexOf("x"), r.lastIndexOf("x"), r.at(-1)); +console.log(r.pop(), r.shift(), r.length, r.unshift("z"), r.label()); +r.length = 0; +console.log(r.length, r.label(), JSON.stringify({ r })); +const u: unknown = r; +console.log(Array.isArray(u), u instanceof Row, u instanceof Array); +const rows: Row[] = [new Row(), new Row()]; +rows[1]!.push("q"); +console.log(rows.map((row) => row.label()).join(" ")); +console.log({ nested: rows[1] }); +const pair = Row.of2("m", "n"); +console.log(pair.size, pair.size, pair.reads, pair.label(), pair instanceof Row); diff --git a/tests/corpus/7726-array-subclass-expressions.js b/tests/corpus/7726-array-subclass-expressions.js new file mode 100644 index 000000000..d2e5d2a04 --- /dev/null +++ b/tests/corpus/7726-array-subclass-expressions.js @@ -0,0 +1,21 @@ +// Classes extending Array reached through expressions rather than +// declarations and locals: a construction used in place, a class +// expression held by a binding, and an instanceof test whose class the +// program never constructs. +class Sized extends Array { + describe() { + return `sized ${this.length}`; + } +} + +console.log(new Sized(3).length, new Sized(2).describe(), new Sized(1, 2).join("-")); +console.log(new Sized(4).fill(1), (new Sized(2)).length); + +const Holder = class extends Array {}; +const held = new Holder(); +held.push("a", "b"); +console.log(held instanceof Holder, held instanceof Array, [] instanceof Holder, held.length); + +class Unbuilt extends Array {} +const values = [[1, 2], "text", new Sized(1)]; +for (const value of values) console.log(value instanceof Unbuilt, Array.isArray(value)); diff --git a/tests/corpus/7728-array-subclass-species-order.ts b/tests/corpus/7728-array-subclass-species-order.ts new file mode 100644 index 000000000..666477cf0 --- /dev/null +++ b/tests/corpus/7728-array-subclass-species-order.ts @@ -0,0 +1,52 @@ +// map, filter and flatMap on a class that extends Array construct their +// result through the class before the first callback runs, with the +// receiver's length for map and 0 for filter and flatMap. +class Logged extends Array { + constructor(...args: number[]) { + super(...args); + console.log("construct", args.length === 1 ? `length ${args[0]}` : `${args.length} items`); + } +} + +const source = Logged.from([1, 2, 3]); +console.log("map"); +const mapped = source.map((value) => { + console.log(" callback", value); + return value * 10; +}); +console.log(mapped, mapped instanceof Logged); + +console.log("filter"); +const kept = source.filter((value) => { + console.log(" callback", value); + return value !== 2; +}); +console.log(kept, kept instanceof Logged); + +console.log("flatMap"); +const spread = source.flatMap((value) => { + console.log(" callback", value); + return [value, -value]; +}); +console.log(spread.length, spread instanceof Logged); + +console.log("map to strings"); +const labels = source.map((value) => { + console.log(" callback", value); + return `#${value}`; +}); +console.log(labels, labels instanceof Logged); + +console.log("throwing callback"); +try { + source.map((value) => { + if (value === 2) throw new Error(`stopped at ${value}`); + return value; + }); +} catch (error) { + console.log((error as Error).message); +} + +console.log("plain receiver"); +const plain: number[] = [4, 5]; +console.log(plain.map((value) => value + 1), plain.filter((value) => value > 4)); diff --git a/tests/corpus/7730-array-subclass-retyped-trace.ts b/tests/corpus/7730-array-subclass-retyped-trace.ts new file mode 100644 index 000000000..d1546c0af --- /dev/null +++ b/tests/corpus/7730-array-subclass-retyped-trace.ts @@ -0,0 +1,33 @@ +// A cycle-capable class that extends Array (its elements and its own field +// can close a cycle) whose map results take other element types: the +// collector traces and tears down each instance by the elements it holds. +// Garbage cycles make the collector run while the results are alive. +class Holder { + group: Group | null = null; +} + +class Group extends Array { + owner: Holder | null = null; +} + +function churn(rounds: number): number { + let made = 0; + for (let i = 0; i < rounds; i++) { + const holder = new Holder(); + const group = new Group(); + group.owner = holder; + holder.group = group; + group.push(holder); + made++; + } + return made; +} + +const groups = new Group(); +groups.push(new Holder(), null, new Holder()); +const numbers = groups.map((_, i) => i * 1.5 + 1e6); +const labels = groups.map((_, i) => `label-${i}`); +console.log(churn(20000)); +console.log(numbers instanceof Group, numbers.length, numbers.join(",")); +console.log(labels instanceof Group, labels.join(",")); +console.log(churn(20000), numbers[2], labels[1]); diff --git a/tests/corpus/7731-array-subclass-deep-release.ts b/tests/corpus/7731-array-subclass-deep-release.ts new file mode 100644 index 000000000..6e4b4c352 --- /dev/null +++ b/tests/corpus/7731-array-subclass-deep-release.ts @@ -0,0 +1,17 @@ +// Releasing a long chain of instances of a class that extends Array, each +// holding the next one, tears them down without growing the native stack. +class Link extends Array { + next: Link | null = null; +} + +let head: Link | null = new Link(); +let tail = head; +for (let i = 0; i < 300000; i++) { + const link = new Link(); + link.push(i); + tail.next = link; + tail = link; +} +console.log(tail.length, tail[0]); +head = null; +console.log("released", head); diff --git a/tests/corpus/7732-array-subclass-species-bound.ts b/tests/corpus/7732-array-subclass-species-bound.ts new file mode 100644 index 000000000..715c51165 --- /dev/null +++ b/tests/corpus/7732-array-subclass-species-bound.ts @@ -0,0 +1,32 @@ +// map, filter and flatMap read the receiver's length before the species +// constructor runs: elements the constructor appends to the receiver are +// not visited. +let watched: Growing | null = null; + +class Growing extends Array { + constructor(...args: number[]) { + super(...args); + if (watched !== null) watched.push(watched.length * 100); + } +} + +const source = Growing.from([1, 2, 3]); +watched = source; +const visits: number[] = []; +const doubled = source.map((value) => { + visits.push(value); + return value * 2; +}); +console.log(doubled.join(","), visits.join(","), source.join(",")); +visits.length = 0; +const kept = source.filter((value) => { + visits.push(value); + return value > 1; +}); +console.log(kept.join(","), visits.join(","), source.length); +visits.length = 0; +const spread = source.flatMap((value) => { + visits.push(value); + return [value]; +}); +console.log(spread.length, visits.length, source.length); diff --git a/tests/corpus/7733-array-subclass-optional-length.ts b/tests/corpus/7733-array-subclass-optional-length.ts new file mode 100644 index 000000000..302fc22cb --- /dev/null +++ b/tests/corpus/7733-array-subclass-optional-length.ts @@ -0,0 +1,26 @@ +// A class that extends Array whose constructor's length parameter is +// optional or has a default takes the species length argument: map, +// filter and slice construct their results through it. +class Optional extends Array { + constructor(length?: number) { + super(length ?? 0); + } +} + +class Defaulted extends Array { + tag: string; + constructor(length = 0, tag = "none") { + super(length); + this.tag = tag; + } +} + +const optional = Optional.from([1, 2, 3]); +const mapped = optional.map((value) => value * 10); +console.log(mapped instanceof Optional, mapped.join(","), optional.slice(1) instanceof Optional); +console.log(optional.filter((value) => value !== 2).join(",")); + +const defaulted = new Defaulted(2).fill("x"); +const sliced = defaulted.slice(0, 1); +if (sliced instanceof Defaulted) console.log(sliced.length, sliced.tag); +console.log(defaulted.map((s) => s + "!").join(",")); diff --git a/tests/corpus/7736-key-order-replaceable-method-result/main.ts b/tests/corpus/7736-key-order-replaceable-method-result/main.ts new file mode 100644 index 000000000..5ebbfa761 --- /dev/null +++ b/tests/corpus/7736-key-order-replaceable-method-result/main.ts @@ -0,0 +1,106 @@ +import { Registry, describe, titleIndex } from "./registry.js"; + +// Records keep the key order their object literals write when values of +// the same type also pass through checked conversions that build none of +// them: +// - a typed method the program may replace on its instance (the +// computed-key write in registry.js makes every method name replaceable) +// returns the records it stores, checked back from the guarded call; +// - an index spec from JavaScript, checked into a type whose function +// field takes a Book, receives Books but never builds one. +// JSON, Object.keys and spreads of those records follow insertion order, +// not the interface's declared order. +interface Book { + id: string; + title: string; + author: string; + createdAt: string; + updatedAt: string; +} + +type Draft = { title: string; author: string }; + +interface IndexSpec { + name: string; + key: (row: T) => string; +} + +class Repository { + readonly #rows = new Map(); + + insert(row: T): T { + this.#rows.set(row.id, row); + return row; + } + + require(id: string): T { + const row = this.#rows.get(id); + if (row === undefined) throw new Error(`missing ${id}`); + return row; + } + + find(id: string): T | undefined { + return this.#rows.get(id); + } + + all(): T[] { + return [...this.#rows.values()]; + } +} + +class Service { + readonly #books = new Repository(); + readonly #index: IndexSpec = titleIndex; + #clock = 0; + + #now(): string { + this.#clock++; + return `t${this.#clock}`; + } + + create(draft: Draft, id: string): Book { + const at = this.#now(); + return this.#books.insert({ ...draft, id, createdAt: at, updatedAt: at }); + } + + update(id: string, patch: Partial): Book { + const current = this.#books.require(id); + return this.#books.insert({ ...current, ...patch, updatedAt: this.#now() }); + } + + get(id: string): Book { + return this.#books.require(id); + } + + maybe(id: string): Book | undefined { + return this.#books.find(id); + } + + list(): Book[] { + return this.#books.all(); + } + + indexed(): string[] { + return this.list().map((book) => `${this.#index.name}=${this.#index.key(book)}`); + } +} + +const registry = new Registry(); +console.log(describe(registry)); + +const service = new Service(); +console.log(JSON.stringify(service.create({ title: "Dune", author: "Herbert" }, "b1"))); +console.log(JSON.stringify(service.create({ title: "Earthsea", author: "Le Guin" }, "b2"))); +console.log(JSON.stringify(service.update("b1", { title: "Dune Messiah" }))); +console.log(Object.keys(service.get("b2")).join(",")); +const found = service.maybe("b1"); +if (found !== undefined) console.log(JSON.stringify(found)); +console.log(JSON.stringify(service.list())); +const copy = { ...service.get("b1") }; +console.log(Object.keys(copy).join(",")); +console.log(service.indexed().join(" ")); +try { + service.get("missing"); +} catch (error) { + console.log((error as Error).message); +} diff --git a/tests/corpus/7736-key-order-replaceable-method-result/registry.js b/tests/corpus/7736-key-order-replaceable-method-result/registry.js new file mode 100644 index 000000000..d6da1edc5 --- /dev/null +++ b/tests/corpus/7736-key-order-replaceable-method-result/registry.js @@ -0,0 +1,21 @@ +// Installs one handler per method name with a computed-key write, the way +// router classes attach `get`, `post`, ... in their constructor. The +// compiler cannot tell which names such a write replaces. +const METHODS = ["get", "post"]; + +export class Registry { + constructor() { + METHODS.forEach((method) => { + this[method] = (...args) => `${method}:${args.length}`; + }); + } +} + +export function describe(registry) { + return `${registry.get(1, 2)} ${registry.post()}`; +} + +export const titleIndex = { + name: "title", + key: (row) => row.title.toLowerCase(), +}; diff --git a/tests/corpus/7737-key-order-self-spread.ts b/tests/corpus/7737-key-order-self-spread.ts new file mode 100644 index 000000000..5577f7531 --- /dev/null +++ b/tests/corpus/7737-key-order-self-spread.ts @@ -0,0 +1,34 @@ +// A literal that starts by spreading a value of its own type copies that +// value's keys in their order, then overwrites some of them. It must not +// stop the type from taking the order its other literals write: the +// records below enumerate as created (title before id), not in the +// interface's declared order. +interface Book { + id: string; + title: string; + tags: string[]; + updatedAt: string; + version: number; +} + +type Draft = { title: string; tags: string[] }; + +function create(draft: Draft, id: string): Book { + return { ...draft, id, updatedAt: "t1", version: 1 }; +} + +function update(current: Book, patch: Partial): Book { + return { ...current, ...patch, updatedAt: "t2", version: current.version + 1 }; +} + +function touch(current: Book): Book { + return { ...current, version: current.version + 1 }; +} + +const book = create({ title: "Dune", tags: ["sf"] }, "b1"); +console.log(JSON.stringify(book)); +const updated = update(book, { title: "Dune Messiah" }); +console.log(JSON.stringify(updated)); +console.log(Object.keys(touch(updated)).join(",")); +console.log(Object.keys(update(updated, { tags: [] })).join(" ")); +for (const key in touch(book)) console.log(key); diff --git a/tests/corpus/7742-response-clone-tee.ts b/tests/corpus/7742-response-clone-tee.ts new file mode 100644 index 000000000..56c5e74e6 --- /dev/null +++ b/tests/corpus/7742-response-clone-tee.ts @@ -0,0 +1,212 @@ +// Response.clone and Request.clone on checked-dynamic handles: the body +// tees, so the original and the clone each read every chunk; a clone of a +// used or locked body throws; canceling both branches cancels the source +// with both reasons; the clone's headers are a copy. +const enc = new TextEncoder(); +const dec = new TextDecoder(); + +function describe(e: unknown): string { + const err = e as { name?: string; message?: string }; + return `${String(err.name)}: ${String(err.message)}`; +} + +async function readAll(stream: any): Promise { + const reader = stream.getReader(); + let out = ""; + for (;;) { + const { done, value } = await reader.read(); + if (done) return out; + out += dec.decode(value); + } +} + +async function main(): Promise { + const r: any = new Response("hello world", { status: 201, headers: { "x-a": "1" } }); + const c: any = r.clone(); + console.log(c.status, c.headers.get("x-a"), c.bodyUsed, r.bodyUsed, c.body === r.body); + console.log(await c.text(), "|", await r.text()); + console.log("used", c.bodyUsed, r.bodyUsed); + try { + r.clone(); + } catch (e) { + console.log("clone used:", describe(e), e instanceof TypeError); + } + + const locked: any = new Response("abc"); + locked.body.getReader(); + try { + locked.clone(); + } catch (e) { + console.log("clone locked:", describe(e)); + } + + const empty: any = new Response(null, { status: 204, statusText: "Nothing" }); + const emptyClone: any = empty.clone(); + console.log("null body", emptyClone.body, emptyClone.status, emptyClone.statusText, JSON.stringify(await emptyClone.text())); + + // Headers are copied: changing the clone's leaves the original's alone. + const h: any = new Response("x", { headers: { "x-b": "2" } }); + const hc: any = h.clone(); + hc.headers.set("x-b", "3"); + console.log("headers", h.headers.get("x-b"), hc.headers.get("x-b"), h.headers === hc.headers); + + // A pulled source: both branches see each chunk (the same object). + let n = 0; + const source: any = new ReadableStream({ + start(): void { + console.log("start"); + }, + pull(controller: any): void { + n++; + console.log("pull", n); + if (n <= 3) controller.enqueue(enc.encode(`chunk${n};`)); + else controller.close(); + }, + }); + const streamed: any = new Response(source); + const streamedClone: any = streamed.clone(); + console.log("cloned"); + const r1 = streamed.body.getReader(); + const r2 = streamedClone.body.getReader(); + const a = await r1.read(); + const b = await r2.read(); + console.log("same chunk", a.value === b.value, dec.decode(a.value)); + console.log("rest 1", await readAllFrom(r1)); + console.log("rest 2", await readAllFrom(r2)); + + // One branch read to the end first; the other buffers. + const big: any = new Response(new ReadableStream({ + start(controller: any): void { + controller.enqueue(enc.encode("a")); + controller.enqueue(enc.encode("b")); + controller.close(); + }, + })); + const bigClone: any = big.clone(); + console.log("clone first", await bigClone.text()); + console.log("then original", await readAll(big.body)); + + // Canceling both branches cancels the source with both reasons. + const cancelSource: any = new ReadableStream({ + pull(controller: any): void { + controller.enqueue(enc.encode("z")); + }, + cancel(reason: unknown): void { + console.log("source cancel", JSON.stringify(reason)); + }, + }); + const cr: any = new Response(cancelSource); + const crc: any = cr.clone(); + const p1 = cr.body.cancel("first"); + console.log("one canceled"); + const p2 = crc.body.cancel("second"); + await p1; + await p2; + console.log("both canceled", cr.bodyUsed, crc.bodyUsed); + + // An erroring source errors both branches. + const failing: any = new ReadableStream({ + pull(controller: any): void { + controller.error(new Error("source failed")); + }, + }); + const fr: any = new Response(failing); + const frc: any = fr.clone(); + for (const branch of [fr, frc]) { + try { + await branch.text(); + } catch (e) { + console.log("branch error:", describe(e)); + } + } + + // Request.clone: the body tees and the signal follows the original's. + const q: any = new Request("http://example.test/path", { method: "POST", body: "data" }); + const qc: any = q.clone(); + console.log("request", qc.method, qc.url, await qc.text(), await q.text(), qc.signal === q.signal); + try { + q.clone(); + } catch (e) { + console.log("request clone used:", describe(e)); + } + const ac = new AbortController(); + const q2: any = new Request("http://example.test/", { signal: ac.signal }); + const q2c: any = q2.clone(); + console.log("before abort", q2c.signal.aborted); + ac.abort("why"); + console.log("after abort", q2c.signal.aborted, q2c.signal.reason); + const q3: any = new Request("http://example.test/", { signal: AbortSignal.abort("early") }); + console.log("aborted clone", q3.clone().signal.aborted, q3.clone().signal.reason); + + // clone is a method value like the body readers. + console.log(typeof r.clone, "clone" in r, r.clone === c.clone); +} + +async function readAllFrom(reader: any): Promise { + let out = ""; + for (;;) { + const { done, value } = await reader.read(); + if (done) return out; + out += dec.decode(value); + } +} + + +// Tee branches error when their source errors: after both buffered a +// chunk and with no source read pending, reads reject once the error's +// reaction ran (a read issued at once still takes the buffered chunk, as in +// Node); a clone of an errored body errors both branches. +async function readFirst(label: string, r: any): Promise { + try { + const chunk = await r.body.getReader().read(); + console.log(label, "read", JSON.stringify(chunk.value ? [...chunk.value] : null), chunk.done); + } catch (error) { + console.log(label, "rejects", (error as Error).message); + } +} + +function erroring(): { res: any; error: (reason: Error) => void } { + let ctrl: ReadableStreamDefaultController | undefined; + const source = new ReadableStream({ + start(c) { + ctrl = c; + c.enqueue(new Uint8Array([1, 2])); + }, + }); + return { res: new Response(source), error: (reason) => ctrl!.error(reason) }; +} + +const tick = (): Promise => new Promise((resolve) => setTimeout(resolve, 10)); + +async function sourceErrors(): Promise { + // Error, then read after the reaction: both branches reject. + { + const { res, error } = erroring(); + const clone = res.clone(); + await tick(); + error(new Error("late")); + await tick(); + await readFirst("later original", res); + await readFirst("later clone", clone); + } + // Error, then read at once: the first read takes the buffered chunk. + { + const { res, error } = erroring(); + const clone = res.clone(); + await tick(); + error(new Error("soon")); + await readFirst("at once original", res); + await readFirst("at once clone", clone); + } + // A body that errored before the clone. + { + const { res, error } = erroring(); + error(new Error("before")); + const clone = res.clone(); + await readFirst("errored original", res); + await readFirst("errored clone", clone); + } +} + +await main(); +await sourceErrors(); diff --git a/tests/corpus/7742-weak-collections-many-keys.ts b/tests/corpus/7742-weak-collections-many-keys.ts new file mode 100644 index 000000000..2cb2b103e --- /dev/null +++ b/tests/corpus/7742-weak-collections-many-keys.ts @@ -0,0 +1,70 @@ +// Weak collections with many keys: entries found among tens of thousands +// in WeakMaps and WeakSets that share keys, deleted and replaced, and gone +// with their keys, so blocks a new key reuses are not found under it. +// Keys in reference cycles die by the cycle collector. +class Key { + readonly id: number; + peer: Key | null = null; + constructor(id: number) { + this.id = id; + } +} + +const n = 40000; +let keys: Key[] = []; +for (let i = 0; i < n; i++) keys.push(new Key(i)); +const values = new WeakMap(); +const labels = new WeakMap(); +const marked = new WeakSet(); +for (const k of keys) { + values.set(k, k.id * 2); + if (k.id % 3 === 0) labels.set(k, "k" + k.id); + if (k.id % 2 === 0) marked.add(k); +} +let sum = 0; +let labelled = 0; +let counted = 0; +for (const k of keys) { + sum += values.get(k) ?? 0; + if (labels.has(k)) labelled++; + if (marked.has(k)) counted++; +} +console.log("filled", sum, labelled, counted); + +// Deleting every fourth key from one collection leaves the others alone. +let deleted = 0; +for (const k of keys) if (k.id % 4 === 0 && values.delete(k)) deleted++; +let remaining = 0; +for (const k of keys) if (values.has(k)) remaining++; +console.log("deleted", deleted, remaining, values.delete(keys[0]!), marked.has(keys[0]!), labels.get(keys[3]!)); + +// Overwriting keeps one entry per key. +for (const k of keys) if (k.id % 5 === 0) values.set(k, -1); +let negative = 0; +for (const k of keys) if (values.get(k) === -1) negative++; +console.log("overwritten", negative); + +// Dropping the keys ends their entries; new keys are not found. +keys = []; +const fresh: Key[] = []; +for (let i = 0; i < n; i++) fresh.push(new Key(n + i)); +let stale = 0; +for (const k of fresh) if (values.has(k) || labels.has(k) || marked.has(k)) stale++; +console.log("fresh", fresh.length, stale); + +// Keys that reach each other die together through the cycle collector. +for (let round = 0; round < 3; round++) { + let ring: Key[] = []; + for (let i = 0; i < 1000; i++) ring.push(new Key(i)); + for (let i = 0; i < ring.length; i++) ring[i]!.peer = ring[(i + 1) % ring.length]!; + for (const k of ring) { + values.set(k, round); + marked.add(k); + } + ring = []; +} +const after: Key[] = []; +for (let i = 0; i < 3000; i++) after.push(new Key(i)); +let found = 0; +for (const k of after) if (values.has(k) || marked.has(k)) found++; +console.log("cycles", found); diff --git a/tests/corpus/7743-transform-stream-identity.ts b/tests/corpus/7743-transform-stream-identity.ts new file mode 100644 index 000000000..3664c8ff1 --- /dev/null +++ b/tests/corpus/7743-transform-stream-identity.ts @@ -0,0 +1,239 @@ +// The identity TransformStream: writes wait for reads (the readable side's +// high-water mark is 0), the writer's desiredSize/ready track the writable +// side's queue, close/abort/cancel propagate across the pair (in either +// order when they race), and the readable side feeds a Response body (the +// StreamingApi pattern of Hono's stream helpers). +const enc = new TextEncoder(); +const dec = new TextDecoder(); + +function describe(e: unknown): string { + if (e instanceof Error) { + const code = (e as NodeJS.ErrnoException).code; + return `${e.name}: ${e.message}${code === undefined ? "" : ` [${code}]`}`; + } + return `value ${String(e)}`; +} + +async function basics(): Promise { + const ts = new TransformStream(); + const writer = ts.writable.getWriter(); + const reader = ts.readable.getReader(); + console.log("desired", writer.desiredSize, ts.writable.locked, ts.readable.locked); + const written = writer.write("a").then(() => console.log("write a done")); + console.log("desired after write", writer.desiredSize); + void writer.ready.then(() => console.log("ready")); + const first = await reader.read(); + console.log("read", first.done, first.value); + await written; + console.log("desired", writer.desiredSize); + const closing = writer.close().then(() => console.log("closed")); + const last = await reader.read(); + console.log("read", last.done, last.value); + await closing; + await writer.closed; + console.log("writer.closed resolved"); + try { + await writer.write("b"); + } catch (e) { + console.log("write after close:", describe(e)); + } + try { + ts.writable.getWriter(); + } catch (e) { + console.log("getWriter locked:", describe(e)); + } + writer.releaseLock(); + try { + await writer.write("c"); + } catch (e) { + console.log("released write:", describe(e)); + } + try { + await writer.closed; + } catch (e) { + console.log("released closed:", describe(e)); + } +} + +async function abortWriter(): Promise { + const ts = new TransformStream(); + const writer = ts.writable.getWriter(); + const reader = ts.readable.getReader(); + const pending = writer.write("z").catch((e: unknown) => console.log("pending write:", describe(e))); + await writer.abort("boom").then(() => console.log("abort resolved")); + await pending; + try { + await reader.read(); + } catch (e) { + console.log("read after abort:", describe(e)); + } + try { + await writer.closed; + } catch (e) { + console.log("closed after abort:", describe(e)); + } + try { + await writer.ready; + } catch (e) { + console.log("ready after abort:", describe(e)); + } + console.log("desired after abort", writer.desiredSize); +} + +async function cancelReadable(): Promise { + const ts = new TransformStream(); + const writer = ts.writable.getWriter(); + const pending = writer.write("q").catch((e: unknown) => console.log("write rejected by cancel:", describe(e))); + await ts.readable.cancel("nope").then(() => console.log("cancel resolved")); + await pending; + try { + await writer.closed; + } catch (e) { + console.log("writer.closed after cancel:", describe(e)); + } +} + +async function intoResponse(): Promise { + const ts = new TransformStream(); + const writer = ts.writable.getWriter(); + void (async (): Promise => { + for (const s of ["x", "y", "z"]) { + await writer.write(enc.encode(s)); + console.log("wrote", s); + } + await writer.close(); + console.log("writer closed"); + })(); + console.log("text", await new Response(ts.readable).text()); +} + +/** Hono's StreamingApi: a writer on the writable side and a pull-based + * ReadableStream over a reader of the readable side. */ +interface StreamingApi { + readonly responseReadable: ReadableStream; + write(input: string): Promise; + close(): Promise; + state(): string; +} + +function streamingApi(writable: WritableStream, readable: ReadableStream): StreamingApi { + const writer = writable.getWriter(); + const reader = readable.getReader(); + let closed = false; + let aborted = false; + const responseReadable = new ReadableStream({ + async pull(controller): Promise { + const { done, value } = await reader.read(); + if (done) controller.close(); + else controller.enqueue(value); + }, + cancel: (): void => { + if (!closed) aborted = true; + }, + }); + return { + responseReadable, + async write(input: string): Promise { + try { + await writer.write(enc.encode(input)); + } catch { + // a closed or aborted stream drops the write + } + }, + async close(): Promise { + closed = true; + try { + await writer.close(); + } catch { + // already closed + } + }, + state: (): string => `closed=${String(closed)} aborted=${String(aborted)}`, + }; +} + +async function streamedResponse(): Promise { + const { readable, writable } = new TransformStream(); + const api = streamingApi(writable, readable); + void (async (): Promise => { + try { + for (let i = 0; i < 3; i++) { + await api.write(`data: ${i}\n\n`); + } + } finally { + await api.close(); + } + })(); + const response = new Response(api.responseReadable, { headers: { "content-type": "text/event-stream" } }); + const reader = response.body!.getReader(); + let chunks = 0; + let text = ""; + for (;;) { + const { done, value } = await reader.read(); + if (done) break; + chunks++; + text += dec.decode(value); + } + console.log("streamed", chunks, JSON.stringify(text), api.state()); +} + +async function writableMethods(): Promise { + const a = new TransformStream(); + await a.writable.abort("r").then(() => console.log("writable.abort ok")); + const b = new TransformStream(); + const writer = b.writable.getWriter(); + try { + await b.writable.close(); + } catch (e) { + console.log("close locked:", describe(e)); + } + try { + await b.writable.abort(); + } catch (e) { + console.log("abort locked:", describe(e)); + } + writer.releaseLock(); + const reader = b.readable.getReader(); + const closing = b.writable.close().then(() => console.log("writable.close ok")); + const end = await reader.read(); + console.log("readable done", end.done); + await closing; +} + +// writable.abort() and readable.cancel() share the transform's finish +// promise: whichever runs second answers the first one's result. +function settled(label: string, p: Promise): Promise { + return p.then( + () => console.log(label, "fulfilled"), + (e: unknown) => console.log(label, "rejected", describe(e)), + ); +} + +async function abortCancelRaces(): Promise { + const a = new TransformStream(); + const aWriter = a.writable.getWriter(); + await new Promise((r) => setTimeout(r, 0)); + const aborted = settled("abort then cancel: abort", aWriter.abort("abort first")); + const cancelled = settled("abort then cancel: cancel", a.readable.cancel("cancel second")); + await aborted; + await cancelled; + const b = new TransformStream(); + const bWriter = b.writable.getWriter(); + await new Promise((r) => setTimeout(r, 0)); + const cancelledFirst = settled("cancel then abort: cancel", b.readable.cancel("cancel first")); + const abortedSecond = settled("cancel then abort: abort", bWriter.abort("abort second")); + await cancelledFirst; + await abortedSecond; +} + +async function main(): Promise { + await basics(); + await abortWriter(); + await cancelReadable(); + await intoResponse(); + await streamedResponse(); + await writableMethods(); + await abortCancelRaces(); +} + +await main(); diff --git a/tests/corpus/7745-typed-array-unions.ts b/tests/corpus/7745-typed-array-unions.ts new file mode 100644 index 000000000..a7fc6f2cb --- /dev/null +++ b/tests/corpus/7745-typed-array-unions.ts @@ -0,0 +1,93 @@ +// Unions of typed-array classes: every element operation runs on each arm. + +type UintArray = Uint8Array | Uint16Array | Uint32Array; +type Samples = Int16Array | Float32Array | Float64Array; + +function uintArray(max: number, size: number): UintArray { + return max <= 2 ** 8 + ? new Uint8Array(size) + : max <= 2 ** 16 + ? new Uint16Array(size) + : new Uint32Array(size); +} + +function samples(kind: number, size: number): Samples { + return kind === 0 + ? new Int16Array(size) + : kind === 1 + ? new Float32Array(size) + : new Float64Array(size); +} + +function sum(xs: UintArray): number { + let total = 0; + for (let i = 0; i < xs.length; i++) total += xs[i]!; + return total; +} + +function describe(xs: UintArray): string { + if (xs instanceof Uint8Array) return `u8 ${xs.length}`; + if (xs instanceof Uint16Array) return `u16 ${xs[0]}`; + return `u32 ${xs.BYTES_PER_ELEMENT}`; +} + +for (const max of [200, 60000, 5000000]) { + const xs = uintArray(max, 6); + // Stores convert to each arm's element kind (wrapping past its range). + for (let i = 0; i < xs.length; i++) xs[i] = i * 70000 + 255; + console.log(xs.length, xs.byteLength, xs.byteOffset, xs.BYTES_PER_ELEMENT); + console.log(xs[0], xs[5], xs[6], xs[-1], sum(xs)); + xs[1] += 3; + xs[2]++; + --xs[3]; + xs[4] = -1; + xs[5] = 2.75; + console.log(xs[1], xs[2], xs[3], xs[4], xs[5]); + // Writes past the end are ignored, and reads there are undefined. + xs[10] = 9; + console.log(xs[10] === undefined, xs[10] ?? "missing"); + let iterated = 0; + for (const v of xs) iterated += v; + console.log(iterated, [...xs].length, Array.from(xs)[1]); + const part = xs.subarray(1, 4); + part[0] = 42; + console.log(part.length, xs[1], xs.slice(2, 4).length, xs.slice(2, 4)[0] === xs[2]); + xs.fill(7, 4); + xs.copyWithin(0, 4, 5); + console.log(xs.join("-"), xs.toReversed()[0]); + xs.set([1, 2], 2); + console.log(xs[2], xs[3], describe(xs)); +} + +for (const kind of [0, 1, 2]) { + const s = samples(kind, 4); + s[0] = 1.5; + s[1] = -40000; + s[2] = 0.1; + s[3] = s[0]! * 3; + console.log(s.BYTES_PER_ELEMENT, s[0], s[1], s[2], s[3]); + console.log(s); + console.log(s instanceof Float32Array, s instanceof Int16Array); +} + +// A stored value that reassigns the receiver or index stores into the +// receiver and at the index evaluated before it. +type Wide = Uint8Array | Uint16Array; +function rebound(wide: boolean): void { + const before: Wide = wide ? new Uint16Array(4) : new Uint8Array(4); + let u: Wide = before; + let i = 0; + const swap = (): number => { + u = new Uint16Array(4); + i = 2; + return 300; + }; + u[i] = swap(); + console.log(before, u, i); + const target = u; + target[1] = 5; + u[i] += swap(); + console.log(target, u, i); +} +rebound(false); +rebound(true); diff --git a/tests/corpus/7746-key-order-iterable-iterator.ts b/tests/corpus/7746-key-order-iterable-iterator.ts new file mode 100644 index 000000000..d9d4ea15a --- /dev/null +++ b/tests/corpus/7746-key-order-iterable-iterator.ts @@ -0,0 +1,109 @@ +// Records that leave a typed collection through an `IterableIterator` +// method or a `Generator` keep the order their literals create (title +// before id), including copies that spread a value of their own type. The +// iterators stay live and hand out the stored objects themselves. +interface Book { + id: string; + title: string; + version: number; +} +type BookDraft = Omit; +type BookPatch = Partial; + +class Repo { + private readonly rows = new Map(); + put(row: T): void { + this.rows.set(row.id, row); + } + get(id: string): T | undefined { + return this.rows.get(id); + } + all(): IterableIterator { + return this.rows.values(); + } + ids(): IterableIterator { + return this.rows.keys(); + } +} + +function* newest(books: readonly Book[]): Generator { + for (let i = books.length - 1; i >= 0; i--) yield books[i]!; +} + +const books = new Repo(); +let n = 0; +function create(draft: BookDraft): Book { + const book: Book = { ...draft, id: "b" + ++n, version: 1 }; + books.put(book); + return book; +} +function update(id: string, patch: BookPatch): Book { + const current = books.get(id)!; + const next: Book = { ...current, ...patch, version: current.version + 1 }; + books.put(next); + return next; +} + +create({ title: "Dune" }); +const live = books.all(); +console.log(JSON.stringify(create({ title: "Emma" }))); +console.log(JSON.stringify(update("b1", { title: "Dune II" }))); +const listed = [...live]; +console.log(JSON.stringify(listed)); +console.log(listed.length, listed[0] === books.get("b1"), [...books.ids()].join()); +for (const book of books.all()) console.log(Object.keys(book).join()); +const ordered = [...newest(listed)]; +console.log(JSON.stringify(ordered), ordered[0] === listed[1]); +const step = newest(listed).next(); +console.log(step.done, JSON.stringify(step.value)); + +// Deleting the current entry before leaving a loop over an iterator-typed +// method: the iterator parks after that entry and continues with the next. +class Store { + readonly rows = new Map(); + readonly tags = new Set(); + values(): IterableIterator { + return this.rows.values(); + } + keys(): IterableIterator { + return this.rows.keys(); + } + tagValues(): IterableIterator { + return this.tags.values(); + } + anyPairs(): IterableIterator { + return this.rows.entries(); + } + unknownPairs(): IterableIterator { + return this.rows.entries(); + } + tagAny(): IterableIterator { + return this.tags.entries(); + } +} +const store = new Store(); +for (const [k, v] of [["a", 1], ["b", 2], ["c", 3]] as const) store.rows.set(k, v); +for (const t of ["x", "y", "z"]) store.tags.add(t); +console.log(JSON.stringify([...store.anyPairs()]), JSON.stringify([...store.unknownPairs()])); +console.log(JSON.stringify([...store.tagAny()])); +const values = store.values(); +for (const v of values) { + if (v === 2) { + store.rows.delete("b"); + break; + } +} +console.log([...values].join(), [...store.rows.keys()].join()); +for (const k of store.keys()) { + store.rows.delete(k); + break; +} +console.log([...store.rows.keys()].join()); +const tags = store.tagValues(); +for (const t of tags) { + if (t === "y") { + store.tags.delete("y"); + break; + } +} +console.log([...tags].join(), [...store.tags].join()); diff --git a/tests/corpus/7747-text-codec-untyped-slot.js b/tests/corpus/7747-text-codec-untyped-slot.js new file mode 100644 index 000000000..cdf8345da --- /dev/null +++ b/tests/corpus/7747-text-codec-untyped-slot.js @@ -0,0 +1,69 @@ +// TextEncoder and TextDecoder instances held in untyped storage (hono's +// StreamingApi keeps `this.encoder = new TextEncoder()` in a JavaScript +// class field) and called from dynamic code, with their identity, streaming +// state, properties, method values and argument errors. +class StreamingApi { + /** @type {any} */ encoder; + /** @type {any} */ decoder; + /** @type {any} */ latin; + /** @type {any} */ strict; + constructor() { + this.encoder = new TextEncoder(); + this.decoder = new TextDecoder(); + this.latin = new TextDecoder("latin1"); + this.strict = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }); + } + write(input) { + if (typeof input === "string") input = this.encoder.encode(input); + return input; + } +} + +const show = (error) => console.log(error.name, error.code); +const api = new StreamingApi(); +console.log(api.write("héllo 😀")); +console.log(api.encoder.encoding, typeof api.encoder, api.encoder === api.encoder); +console.log(String(api.encoder), JSON.stringify(api.encoder), JSON.stringify({ d: api.decoder })); +console.log(api.encoder.encode(), api.encoder.encode(42), api.encoder.encode(undefined)); +const dest = new Uint8Array(4); +console.log(api.encoder.encodeInto("héllo", dest), dest); +try { + api.encoder.encodeInto(1, dest); +} catch (error) { + show(error); +} + +console.log(api.decoder.encoding, api.decoder.fatal, api.decoder.ignoreBOM); +console.log(api.latin.encoding, api.strict.fatal, api.strict.ignoreBOM); +const euro = api.encoder.encode("€uro"); +const head = api.decoder.decode(euro.subarray(0, 2), { stream: true }); +console.log(JSON.stringify(head), api.decoder.decode(euro.subarray(2))); +console.log(api.latin.decode(new Uint8Array([0xe9, 0x41]))); +console.log(JSON.stringify(api.strict.decode(new Uint8Array([0xef, 0xbb, 0xbf, 0x41])))); +try { + api.strict.decode(new Uint8Array([0xff])); +} catch (error) { + show(error); +} +try { + api.decoder.decode("text"); +} catch (error) { + show(error); +} +console.log(JSON.stringify(api.decoder.decode()), JSON.stringify(api.decoder.decode(undefined, null))); + +const encode = api.encoder.encode; +try { + encode("x"); +} catch (error) { + console.log(error.name, error.code, error.message); +} +console.log(typeof api.encoder.encode, api.encoder.encode.length, api.encoder.encodeInto.length); +console.log(api.decoder.decode.length, encode === api.encoder.encode); +console.log(api.encoder.encode.call(api.encoder, "ab")); +console.log("encode" in api.encoder, "decode" in api.decoder, api.encoder.missing); + +console.log(api.encoder instanceof TextEncoder, api.decoder instanceof TextDecoder); +console.log(api.encoder instanceof TextDecoder, JSON.parse("{}") instanceof TextEncoder); +const codecs = [api.encoder, api.decoder]; +console.log(codecs.indexOf(api.decoder), codecs.includes(api.encoder)); diff --git a/tests/corpus/7747-typed-array-class-values.ts b/tests/corpus/7747-typed-array-class-values.ts new file mode 100644 index 000000000..523896c96 --- /dev/null +++ b/tests/corpus/7747-typed-array-class-values.ts @@ -0,0 +1,42 @@ +// Typed-array unions chosen through class values, and held in fields, +// parameters, returns and records. + +type UintArray = Uint8Array | Uint16Array | Uint32Array; + +const pick = (max: number): { new (n: number): UintArray } => + max <= 2 ** 8 ? Uint8Array : max <= 2 ** 16 ? Uint16Array : Uint32Array; + +class Stack { + heap: UintArray; + length = 0; + constructor(max: number) { + const Heap = pick(max); + this.heap = new Heap(max); + } + push(n: number): void { + this.heap[this.length++] = n; + } + pop(): number | undefined { + return this.heap[--this.length]; + } +} + +for (const max of [10, 1000, 100000]) { + const stack = new Stack(max); + for (let i = 1; i <= 4; i++) stack.push(i * 250); + console.log(stack.heap.BYTES_PER_ELEMENT, stack.pop(), stack.pop(), stack.length); +} + +function grow(xs: UintArray, by: number): UintArray { + const Next = pick(xs.length * by); + const out = new Next(xs.length * by); + out.set(xs); + return out; +} + +const record: { data: UintArray; label: string } = { data: new Uint8Array([1, 2, 3]), label: "r" }; +record.data = grow(record.data, 100); +record.data[299] = 65535; +console.log(record.label, record.data.length, record.data[0], record.data[2], record.data[299]); +record.data = grow(record.data, 1000); +console.log(record.data.BYTES_PER_ELEMENT, record.data.length, record.data[1]); diff --git a/tests/corpus/7748-npm-any-bytes-receiver/main.ts b/tests/corpus/7748-npm-any-bytes-receiver/main.ts new file mode 100644 index 000000000..58301765a --- /dev/null +++ b/tests/corpus/7748-npm-any-bytes-receiver/main.ts @@ -0,0 +1,33 @@ +// Typed arrays the checker types `any` in package JavaScript: hono's etag +// digest (hono/dist/middleware/etag/digest.js) merges multi-chunk bodies +// through `nextChunk.set(...)`, where `nextChunk`'s initializer depends on +// `chunk`, which is assigned `nextChunk`. Method calls and property reads on +// such values run on the native bytes, as in Node. +import { generateDigest } from "etag-digest"; +import { grow } from "chunk-buffers"; + +const enc = new TextEncoder(); +const sha1 = (body: Uint8Array): Promise => crypto.subtle.digest({ name: "SHA-1" }, body); + +function chunked(parts: string[]): ReadableStream { + let i = 0; + return new ReadableStream({ + pull(controller): void { + if (i < parts.length) controller.enqueue(enc.encode(parts[i++]!)); + else controller.close(); + }, + }); +} + +async function main(): Promise { + console.log("single", await generateDigest(chunked([JSON.stringify({ books: 400 })]), sha1)); + console.log("chunked", await generateDigest(chunked(["alpha\n", "beta\n", "gamma\n", "delta\n"]), sha1)); + const large = ["x".repeat(200000), "y".repeat(100000), "z".repeat(5)]; + console.log("large", await generateDigest(chunked(large), sha1)); + console.log("exact", await generateDigest(chunked(["a".repeat(262144), "b"]), sha1)); + console.log("empty", await generateDigest(chunked([]), sha1)); + console.log("null", await generateDigest(null, sha1)); + console.log(grow([new Uint8Array([1, 2, 3]), new Uint8Array([4]), new Uint8Array([5])])); +} + +await main(); diff --git a/tests/corpus/7748-npm-any-bytes-receiver/node_modules/chunk-buffers/index.d.ts b/tests/corpus/7748-npm-any-bytes-receiver/node_modules/chunk-buffers/index.d.ts new file mode 100644 index 000000000..26fe364a0 --- /dev/null +++ b/tests/corpus/7748-npm-any-bytes-receiver/node_modules/chunk-buffers/index.d.ts @@ -0,0 +1 @@ +export declare function grow(parts: Uint8Array[]): Uint8Array | undefined; diff --git a/tests/corpus/7748-npm-any-bytes-receiver/node_modules/chunk-buffers/index.js b/tests/corpus/7748-npm-any-bytes-receiver/node_modules/chunk-buffers/index.js new file mode 100644 index 000000000..a0a1fea79 --- /dev/null +++ b/tests/corpus/7748-npm-any-bytes-receiver/node_modules/chunk-buffers/index.js @@ -0,0 +1,18 @@ +// `next` is referenced by its own initializer's inference (through +// `chunk = next`), so the checker types it `any` while it is a Uint8Array. +export function grow(parts) { + let chunk; + for (const value of parts) { + if (!chunk) chunk = value.slice(0); + else { + const next = new Uint8Array(new ArrayBuffer(Math.max(8, chunk.byteLength * 2))); + next.set(chunk.subarray(0, 2)); + next.set([9], 2); + next[3] = next.byteLength; + console.log(next.length, next[0], next.subarray(1, 3), next.at(-1), next.byteOffset); + console.log(next.slice(2, 4), next.buffer.byteLength, next.BYTES_PER_ELEMENT); + chunk = next; + } + } + return chunk; +} diff --git a/tests/corpus/7748-npm-any-bytes-receiver/node_modules/chunk-buffers/package.json b/tests/corpus/7748-npm-any-bytes-receiver/node_modules/chunk-buffers/package.json new file mode 100644 index 000000000..74e354d34 --- /dev/null +++ b/tests/corpus/7748-npm-any-bytes-receiver/node_modules/chunk-buffers/package.json @@ -0,0 +1 @@ +{ "name": "chunk-buffers", "version": "1.0.0", "type": "module", "main": "index.js", "types": "index.d.ts" } diff --git a/tests/corpus/7748-npm-any-bytes-receiver/node_modules/etag-digest/index.d.ts b/tests/corpus/7748-npm-any-bytes-receiver/node_modules/etag-digest/index.d.ts new file mode 100644 index 000000000..79d9dc396 --- /dev/null +++ b/tests/corpus/7748-npm-any-bytes-receiver/node_modules/etag-digest/index.d.ts @@ -0,0 +1 @@ +export declare const generateDigest: (stream: ReadableStream | null, generator: (body: Uint8Array) => ArrayBuffer | Promise) => Promise; diff --git a/tests/corpus/7748-npm-any-bytes-receiver/node_modules/etag-digest/index.js b/tests/corpus/7748-npm-any-bytes-receiver/node_modules/etag-digest/index.js new file mode 100644 index 000000000..bf438a5ea --- /dev/null +++ b/tests/corpus/7748-npm-any-bytes-receiver/node_modules/etag-digest/index.js @@ -0,0 +1,60 @@ +// hono/dist/middleware/etag/digest.js (hono 4.13.13) +const mergeBuffers = (buffer1, buffer2) => { + if (!buffer1) return buffer2; + const merged = new Uint8Array(new ArrayBuffer(buffer1.byteLength + buffer2.byteLength)); + merged.set(new Uint8Array(buffer1), 0); + merged.set(buffer2, buffer1.byteLength); + return merged; +}; +const CHUNK_SIZE = 262144; +const generateDigest = async (stream, generator) => { + if (!stream) return null; + let result = void 0; + let chunk; + let chunkLength = 0; + const digest = async (body) => { + result = await generator(mergeBuffers(result, body)); + }; + const reader = stream.getReader(); + for (;;) { + const { value, done } = await reader.read(); + if (done) break; + let offset = 0; + while (offset < value.byteLength) { + const remaining = value.byteLength - offset; + if (chunkLength === 0 && remaining >= CHUNK_SIZE) { + await digest(value.subarray(offset, offset + CHUNK_SIZE)); + offset += CHUNK_SIZE; + continue; + } + const requiredLength = chunkLength + remaining; + if (requiredLength < CHUNK_SIZE) { + if (!chunk) chunk = value.slice(offset); + else { + if (chunk.byteLength < requiredLength) { + const nextChunk = new Uint8Array(new ArrayBuffer(Math.min(CHUNK_SIZE, Math.max(requiredLength, chunk.byteLength * 2)))); + nextChunk.set(chunk.subarray(0, chunkLength)); + chunk = nextChunk; + } + chunk.set(value.subarray(offset), chunkLength); + } + chunkLength = requiredLength; + break; + } + const length = CHUNK_SIZE - chunkLength; + if (chunk?.byteLength !== CHUNK_SIZE) { + const nextChunk = new Uint8Array(/* @__PURE__ */ new ArrayBuffer(CHUNK_SIZE)); + if (chunk) nextChunk.set(chunk.subarray(0, chunkLength)); + chunk = nextChunk; + } + chunk.set(value.subarray(offset, offset + length), chunkLength); + await digest(chunk); + chunkLength = 0; + offset += length; + } + } + if (chunk && chunkLength > 0) await digest(chunk.subarray(0, chunkLength)); + if (!result) return null; + return Array.prototype.map.call(new Uint8Array(result), (x) => x.toString(16).padStart(2, "0")).join(""); +}; +export { generateDigest }; diff --git a/tests/corpus/7748-npm-any-bytes-receiver/node_modules/etag-digest/package.json b/tests/corpus/7748-npm-any-bytes-receiver/node_modules/etag-digest/package.json new file mode 100644 index 000000000..f15495eed --- /dev/null +++ b/tests/corpus/7748-npm-any-bytes-receiver/node_modules/etag-digest/package.json @@ -0,0 +1 @@ +{ "name": "etag-digest", "version": "1.0.0", "type": "module", "main": "index.js", "types": "index.d.ts" } diff --git a/tests/corpus/7748-typed-array-number-array-unions.ts b/tests/corpus/7748-typed-array-number-array-unions.ts new file mode 100644 index 000000000..706ed37b5 --- /dev/null +++ b/tests/corpus/7748-typed-array-number-array-unions.ts @@ -0,0 +1,78 @@ +// Unions of typed arrays and number arrays, as a cache picks its index +// arrays by capacity: a linked list over next/prev index arrays. + +type NumberArray = Uint8Array | Uint16Array | Uint32Array | number[]; + +function indexArray(max: number): NumberArray { + if (max <= 2 ** 8) return new Uint8Array(max); + if (max <= 2 ** 16) return new Uint16Array(max); + if (max <= 2 ** 32) return new Uint32Array(max); + return new Array(4).fill(0); +} + +class Order { + next: NumberArray; + prev: NumberArray; + head = 0; + tail = 0; + size = 0; + + constructor(max: number, force = false) { + this.next = force ? new Array(max).fill(0) : indexArray(max); + this.prev = force ? new Array(max).fill(0) : indexArray(max); + } + + push(index: number): void { + if (this.size === 0) { + this.head = this.tail = index; + } else { + this.next[this.tail] = index; + this.prev[index] = this.tail; + this.tail = index; + } + this.size++; + } + + moveToTail(index: number): void { + if (index === this.tail) return; + if (index === this.head) this.head = this.next[index]!; + else this.next[this.prev[index]!] = this.next[index]!; + this.prev[this.next[index]!] = this.prev[index]!; + this.next[this.tail] = index; + this.prev[index] = this.tail; + this.tail = index; + } + + walk(): number[] { + const out: number[] = []; + for (let i = this.head, n = 0; n < this.size; n++, i = this.next[i]!) out.push(i); + return out; + } +} + +for (const [max, force] of [ + [8, false], + [300, false], + [70000, false], + [8, true], +] as const) { + const order = new Order(max, force); + for (let i = 0; i < 6; i++) order.push(i); + order.moveToTail(0); + order.moveToTail(3); + order.moveToTail(5); + console.log(max, force, order.walk().join(","), order.next.length, order.prev[3]); +} + +// The number-array arm reads holes and missing indexes as undefined, +// and writes past its length grow it. +const arms: NumberArray[] = [new Uint8Array(3), [1, 2, 3]]; +for (const a of arms) { + a[1] = 300; + a[2] += 0.5; + a[5] = 8; + console.log(a.length, a[1], a[2], a[4], a[5]); + let total = 0; + for (const v of a) total += v ?? -1; + console.log(total, [...a].length, Array.from(a).length); +} diff --git a/tests/corpus/7749-text-codec-unknown-roundtrip.ts b/tests/corpus/7749-text-codec-unknown-roundtrip.ts new file mode 100644 index 000000000..b7c18b3ef --- /dev/null +++ b/tests/corpus/7749-text-codec-unknown-roundtrip.ts @@ -0,0 +1,36 @@ +// TypeScript codecs crossing 'unknown' slots: boxed by reference and +// checked back, keeping identity and a decoder's streaming state. +const holder: { codec: unknown; decoder: unknown } = { + codec: new TextEncoder(), + decoder: new TextDecoder("utf-16le"), +}; + +const encoder = holder.codec as TextEncoder; +console.log(encoder.encode("typed"), encoder === (holder.codec as TextEncoder)); +const decoder = holder.decoder as TextDecoder; +const bytes = new Uint8Array([0x68, 0x00, 0x69]); +console.log(JSON.stringify(decoder.decode(bytes, { stream: true }))); +console.log(JSON.stringify(decoder.decode(new Uint8Array([0x00]))), decoder.encoding); + +function store(value: unknown): unknown[] { + return [value, value]; +} +const pair = store(encoder); +console.log(pair[0] === pair[1], pair[0] === holder.codec); + +const maybe: unknown = Math.random() > 2 ? 1 : new TextDecoder(); +if (typeof maybe === "object" && maybe !== null) console.log((maybe as TextDecoder).encoding); + +// Codecs inside arrays and records cross as their handles too. +const codecs: TextEncoder[] = [encoder, new TextEncoder()]; +const asUnknown: unknown = codecs; +const back = asUnknown as TextEncoder[]; +console.log(back.length, back[0] === encoder, back[1]!.encode("x")); +const streaming = new TextDecoder(); +const nested: { name: string; decoder: TextDecoder } = { name: "nested", decoder: streaming }; +const boxed: unknown = nested; +const unboxed = boxed as { name: string; decoder: TextDecoder }; +console.log(unboxed.name, unboxed.decoder === streaming); +console.log(JSON.stringify(unboxed.decoder.decode(new Uint8Array([0xe2, 0x82]), { stream: true }))); +console.log(JSON.stringify(streaming.decode(new Uint8Array([0xac])))); +console.log(JSON.stringify(boxed)); diff --git a/tests/corpus/7749-typed-array-element-searches.ts b/tests/corpus/7749-typed-array-element-searches.ts new file mode 100644 index 000000000..7cae267ec --- /dev/null +++ b/tests/corpus/7749-typed-array-element-searches.ts @@ -0,0 +1,22 @@ +// TypedArray indexOf, lastIndexOf and includes on every element kind, on +// Buffers (their own search), and on unions of typed arrays: strict +// equality for indexOf/lastIndexOf, SameValueZero for includes. +const f = new Float64Array([1, NaN, -0, 0, 2.5, NaN, 1]); +console.log(f.indexOf(NaN), f.includes(NaN), f.lastIndexOf(NaN)); +console.log(f.indexOf(0), f.indexOf(-0), f.lastIndexOf(0), f.lastIndexOf(-0), f.includes(-0)); +console.log(f.indexOf(1, 1), f.indexOf(1, -1), f.indexOf(1, -100), f.indexOf(1, 100), f.indexOf(1, 1.9)); +console.log(f.lastIndexOf(1, 5), f.lastIndexOf(1, -2), f.lastIndexOf(1, -100), f.lastIndexOf(1, 100), f.lastIndexOf(1, undefined)); +console.log(f.includes(2.5, 4), f.includes(2.5, 5), f.includes(2.5, -3), f.includes(NaN, 6)); +const u = new Uint8Array([0, 255, 3, 255]); +console.log(u.indexOf(255), u.indexOf(-1), u.indexOf(511), u.indexOf(3.5), u.includes(NaN), u.lastIndexOf(255)); +const i = new Int16Array([-5, 7, -5]); +console.log(i.indexOf(-5), i.lastIndexOf(-5), i.includes(7), i.indexOf(65531)); +const h = new Float32Array([0.1, 0.5, 16777217]); +console.log(h.indexOf(0.1), h.indexOf(0.5), h.includes(16777216), h.indexOf(16777217)); +const c = new Uint8ClampedArray([300, -4]); +console.log(c.indexOf(255), c.indexOf(0), new Uint32Array(0).indexOf(0), new Uint32Array(0).includes(0)); +const b = Buffer.from([1, 2, 3, 2]); +console.log(b.indexOf(2), b.indexOf(258), b.lastIndexOf(2), b.includes("\u0003")); +type UintArray = Uint8Array | Uint16Array | Uint32Array; +const mk = (n: number): UintArray => (n > 1 ? new Uint16Array([1, 700, 1]) : new Uint8Array([1, 188, 1])); +for (const x of [mk(1), mk(2)]) console.log(x.indexOf(700), x.lastIndexOf(1), x.includes(188), x.indexOf(1, 1)); diff --git a/tests/corpus/7750-key-order-javascript-spread-sources/lib.js b/tests/corpus/7750-key-order-javascript-spread-sources/lib.js new file mode 100644 index 000000000..c8fc0d066 --- /dev/null +++ b/tests/corpus/7750-key-order-javascript-spread-sources/lib.js @@ -0,0 +1,14 @@ +// Records JavaScript builds in its own key order, handed to TypeScript +// through typed slots: a draft from a "validator", a stored row, and a +// replacement method installed through a computed key. +export function validate(input) { + return { year: input.year, title: input.title, tags: input.tags }; +} + +export function storeRow(rows, id) { + rows.set(id, { title: "Stored", id, tags: [], year: 1999, createdAt: "t0", version: 1 }); +} + +export function replace(target, name) { + target[name] = (id) => ({ version: 7, id, year: 2001, title: "Replaced", tags: ["r"], createdAt: "t1" }); +} diff --git a/tests/corpus/7750-key-order-javascript-spread-sources/main.ts b/tests/corpus/7750-key-order-javascript-spread-sources/main.ts new file mode 100644 index 000000000..073ff6c5e --- /dev/null +++ b/tests/corpus/7750-key-order-javascript-spread-sources/main.ts @@ -0,0 +1,122 @@ +// Literals that start by spreading a record JavaScript built keep that +// record's key order, as Node does: a draft from JavaScript spread into a +// new record, a self copy of a record JavaScript stored or returned, and +// a wider view of either. Native drafts and copies keep the literal order. +import { replace, storeRow, validate } from "./lib.js"; + +interface Book { + id: string; + title: string; + year: number; + tags: readonly string[]; + createdAt: string; + version: number; +} + +type Draft = Omit; +type Patch = Partial; + +interface Availability { + copies: number; + onLoan: number; +} + +type BookView = Book & { availability: Availability }; + +class Repository { + private readonly rows = new Map(); + private sequence = 0; + + create(draft: Draft): Book { + const book: Book = { + ...draft, + tags: [...draft.tags].sort(), + id: `bk_${++this.sequence}`, + createdAt: "t" + this.sequence, + version: 1, + }; + this.rows.set(book.id, book); + return book; + } + + require(id: string): Book { + const row = this.rows.get(id); + if (row === undefined) throw new Error("missing " + id); + return row; + } + + update(id: string, patch: Patch): Book { + const current = this.require(id); + const next: Book = { ...current, ...patch, version: current.version + 1 }; + this.rows.set(id, next); + return next; + } + + view(book: Book): BookView { + return { ...book, availability: { copies: 2, onLoan: book.version } }; + } + + table(): Map { + return this.rows; + } +} + +function show(label: string, value: Book): void { + console.log(label, JSON.stringify(value)); + console.log(" keys", Object.keys(value).join()); +} + +function showView(label: string, value: BookView): void { + console.log(label, JSON.stringify(value)); + console.log(" keys", Object.keys(value).join()); +} + +const repo = new Repository(); + +// A native draft: the literal's order. +const native = repo.create({ title: "Native", year: 1990, tags: ["b", "a"] }); +show("native", native); +showView("native view", repo.view(native)); +show("native update", repo.update(native.id, { year: 1991 })); + +// A draft JavaScript built in another order. +const draft: Draft = validate({ title: "Checked", year: 2020, tags: ["z", "y"] }); +const checked = repo.create(draft); +show("checked", checked); +showView("checked view", repo.view(checked)); +show("checked update", repo.update(checked.id, { title: "Checked 2" })); +showView("checked update view", repo.view(repo.require(checked.id))); + +// A row JavaScript stored into the typed map. +storeRow(repo.table(), "js_1"); +show("stored", repo.require("js_1")); +show("stored update", repo.update("js_1", { tags: ["s"] })); +showView("stored view", repo.view(repo.require("js_1"))); + +// A replacement method returning rows JavaScript builds. +replace(repo, "require"); +show("replaced", repo.require("x")); +show("replaced update", repo.update("x", { year: 2002 })); +showView("replaced view", repo.view(repo.require("y"))); + +// The copies are independent records. +const again = repo.view(checked); +showView("again", again); +console.log(native.version, checked.version, again.availability.onLoan); + +// An integer-named key the spread source lacks comes first, as Node +// creates integer keys first whatever the literal's order. +interface Ranked { + year: number; + title: string; + tags: readonly string[]; + id: string; + "1": string; +} +function rank(source: Draft, id: string): Ranked { + return { ...source, id, "1": "gold" }; +} +for (const ranked of [rank(draft, "r1"), rank({ title: "Native", year: 1990, tags: [] }, "r2")]) { + console.log("ranked", JSON.stringify(ranked)); + console.log(" keys", Object.keys(ranked).join()); +} diff --git a/tests/corpus/7750-typed-array-union-inspect.ts b/tests/corpus/7750-typed-array-union-inspect.ts new file mode 100644 index 000000000..aebb0dbdf --- /dev/null +++ b/tests/corpus/7750-typed-array-union-inspect.ts @@ -0,0 +1,14 @@ +// Printing u8 byte views whose static type does not say what they are: +// union arms, elements and fields print as Node does by the value's kind. +type UintArray = Uint8Array | Uint16Array | Uint32Array; +const mk = (n: number): UintArray => (n > 1 ? new Uint16Array([1, 700]) : new Uint8Array([1, 188])); +const a = mk(1), b = mk(2); +console.log(a, b); +console.log([a, b]); +const bufArm: Uint8Array | Uint16Array = Buffer.from([1, 2, 3]); +console.log(bufArm); +const u8s: Uint8Array[] = [new Uint8Array([9]), Buffer.from([10, 11])]; +console.log(u8s); +const rec: { bytes: Uint8Array; view: DataView } = { bytes: new Uint8Array(2), view: new DataView(new ArrayBuffer(2)) }; +console.log(rec); +console.log(new Uint8Array([5, 6]), Buffer.from([7])); diff --git a/tests/corpus/7751-class-well-known-symbol-fields.ts b/tests/corpus/7751-class-well-known-symbol-fields.ts new file mode 100644 index 000000000..d57fce3f8 --- /dev/null +++ b/tests/corpus/7751-class-well-known-symbol-fields.ts @@ -0,0 +1,115 @@ +// Class fields keyed by well-known symbols, Symbol.toStringTag first (the +// lru-cache shape), beside toStringTag getters and module symbol keys. +// String conversion, Object.prototype.toString and util.inspect read the +// tag as Node does, through typed values and through `unknown`. + +const limit = Symbol("limit"); + +class Cache { + size = 0; + [limit] = 10; + [Symbol.toStringTag] = "LRUCache"; +} + +class Sub extends Cache { + extra = 1; +} + +class Retag extends Cache { + constructor() { + super(); + this[Symbol.toStringTag] = "Retagged"; + } +} + +class Tagged { + x = 1; + get [Symbol.toStringTag](): string { + return "Custom"; + } +} + +class TaggedSub extends Tagged { + y = 2; +} + +class Named { + get [Symbol.toStringTag](): string { + return "Named"; + } +} + +class Blank { + get [Symbol.toStringTag](): string { + return ""; + } +} + +class Plain { + n = 3; +} + +const c = new Cache(); +console.log(String(c), `${c}`, Object.prototype.toString.call(c), c[Symbol.toStringTag]); +console.log(c); +console.log(Object.keys(c), JSON.stringify(c)); +console.log(new Sub(), String(new Sub())); +const r: Cache = new Retag(); +console.log(String(r), r); +c[Symbol.toStringTag] = "Other"; +console.log(`${c}`, c); + +const t = new Tagged(); +console.log(String(t), `${t}`, t, [t], { deep: { deeper: { t } } }); +const ts: Tagged = new TaggedSub(); +console.log(String(ts), ts); +console.log(new Named(), new Blank(), String(new Blank())); +console.log(String(new Plain()), new Plain()); + +const values: unknown[] = [new Cache(), new Tagged(), new TaggedSub(), new Plain()]; +for (const v of values) { + console.log(Object.prototype.toString.call(v), String(v), (v as { [Symbol.toStringTag]?: unknown })[Symbol.toStringTag]); +} +console.log(values); + +const literal: unknown = { [Symbol.toStringTag]: "Literal", a: 1 }; +console.log(Object.prototype.toString.call(literal), String(literal), `${literal}`); + +// util.inspect ignores a Symbol.toStringTag getter that throws, typed or +// held as an untyped value. +class Throwing { + get [Symbol.toStringTag](): string { + throw new Error("tag"); + } +} +const throwing: unknown = new Throwing(); +console.log(new Throwing(), [new Throwing()], throwing, [throwing]); + +// A conversion to "[object Object]" still evaluates its receiver. +let made = 0; +function make(): Plain { + made++; + return new Plain(); +} +function fail(): Plain { + throw new Error("receiver"); +} +console.log(Object.prototype.toString.call(make()), String(make()), made); +try { + console.log(Object.prototype.toString.call(fail())); +} catch (error) { + console.log((error as Error).message); +} + +// ToString of a class with valueOf and no toString reads its tag; the +// default hint calls valueOf. +class Valued { + valueOf(): number { + return 1; + } + get [Symbol.toStringTag](): string { + return "Valued"; + } +} +const valued: unknown = new Valued(); +console.log(String(valued), `${valued}`, [valued].join(), valued + "", Object.prototype.toString.call(valued)); diff --git a/tests/corpus/7752-typed-array-dynamic-methods.js b/tests/corpus/7752-typed-array-dynamic-methods.js new file mode 100644 index 000000000..d4f077b49 --- /dev/null +++ b/tests/corpus/7752-typed-array-dynamic-methods.js @@ -0,0 +1,137 @@ +// %TypedArray%.prototype methods called on typed arrays held as untyped +// values, as JavaScript sources and Test262's testTypedArray.js use them. +const kinds = [Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array]; + +function show(label, value) { + console.log(label, value); +} + +// TA arrives untyped, as Test262's testWithTypedArrayConstructors passes it. +function check(TA) { + const a = new TA([3, 1, 2, 1, 0]); + show(TA.name + " search", [a.indexOf(1), a.lastIndexOf(1), a.includes(2), a.indexOf(1, 2), a.lastIndexOf(1, -3), a.includes(3, 1), a.indexOf(9), a.indexOf("1")]); + show(" map", a.map((v, i) => v * 2 + i)); + show(" filter", a.filter((v) => v > 0)); + show(" some/every", [a.some((v) => v > 2), a.every((v) => v >= 0), a.every((v) => v > 0)]); + show(" find", [a.find((v) => v < 2), a.findIndex((v) => v < 2), a.findLast((v) => v < 2), a.findLastIndex((v) => v < 2), a.find((v) => v > 5), a.findLastIndex((v) => v > 5)]); + show(" reduce", [a.reduce((s, v) => s + v), a.reduceRight((s, v, i) => s + "" + v + i, ""), a.reduce((s, v) => s * 10 + v, 0)]); + const seen = []; + a.forEach(function (v, i, arr) { seen.push([v, i, arr === a, this.tag]); }, { tag: "t" }); + show(" forEach", seen.map((x) => x.join(":")).join(" ")); + show(" sorted", [a.toSorted().join(), a.toSorted((x, y) => y - x).join(), a.toReversed().join(), a.with(1, 7).join(), a.with(-1, 9).join(), a.join()]); + show(" iterators", [[...a.keys()].join(), [...a.values()].join(), [...a.entries()].map((e) => e.join("=")).join(" ")]); + const b = a.slice(); + b.sort(); + show(" sort", [b.join(), b.reverse().join(), b.sort((x, y) => x - y) === b]); +} +kinds.forEach(check); + +// Edge cases over untyped constructors. +function edges(F, C, I, U, S) { + const f = new F([NaN, -0, 0, 1, -Infinity, NaN]); + show("float search", [f.indexOf(NaN), f.includes(NaN), f.indexOf(-0), f.lastIndexOf(0), f.includes(-0)]); + show("float sort", Array.from(f.toSorted(), (v) => Object.is(v, -0) ? "-0" : String(v)).join()); + show("clamped map", new C([1, 2]).map((v) => v * 200)); + show("wrapping map", new I([1, 2]).map((v) => v * 100)); + const live = new U([1, 2, 3]); + show("live", live.map((v, i, arr) => { if (i === 0) arr[2] = 9; return v; })); + for (const bad of [() => new I(2).forEach(1), () => new I(0).reduce((a, b) => a), () => new I(2).with(5, 1), () => new I(2).sort("x")]) { + try { + bad(); + show("no throw", ""); + } catch (e) { + show(e.constructor.name, e.message.length > 0); + } + } + let order = []; + new S([5, 1, 4]).sort((x, y) => { order.push(x + "" + y); return x - y; }); + show("comparator calls", order.length > 0); +} +edges(Float64Array, Uint8ClampedArray, Int8Array, Uint8Array, Int16Array); + +// Method values, and the constructors' from and of. +function run(TA) { + const s = new TA([1, 2, 3]); + console.log(typeof s.findLast, typeof s.with, typeof s.toSorted, typeof s.map, typeof s.at); + const m = s.findLast; + console.log(m.call(s, (v) => v < 3)); + console.log(typeof TA.from, typeof TA.of); +} +run(Int16Array); +function statics(TA) { + console.log(TA.from([1, 2, 300]), TA.from({ length: 2, 0: "7", 1: 8 }), TA.from(new Set([4, 5]), (v, k) => v * 10 + k)); + console.log(TA.of(1, 2, 3), TA.of(), TA.from({ [Symbol.iterator]: function* () { yield 9; yield 8; } })); + try { TA.from([1], 5); } catch (e) { console.log(e.constructor.name); } + try { TA.from(null); } catch (e) { console.log(e.constructor.name); } +} +statics(Uint8Array); +statics(Float32Array); + +// TypedArray.from and TypedArray.of with other constructors as this. +function customs(TA) { + let calls = []; + function Big(n) { calls.push(n); return new TA(n + 2); } + const fromBig = TA.from.call(Big, [1, 2, 3], (v) => v * 2); + console.log(fromBig, calls); + console.log(TA.of.call(Big, 7, 8)); + function attempt(label, C) { + try { TA.from.call(C, [1, 2]); console.log(label, "no throw"); } catch (e) { console.log(label, e.constructor.name); } + try { TA.of.call(C, 1, 2); console.log(label, "no throw"); } catch (e) { console.log(label, e.constructor.name); } + } + attempt("small", function () { return new TA(1); }); + attempt("plain", function () { return {}; }); + attempt("arrow", () => new TA(5)); + attempt("number", 5); + const order = []; + const arrayLike = { length: 2, get 0() { order.push("get0"); return 1; }, get 1() { order.push("get1"); return 2; } }; + TA.from.call(function (n) { order.push("create" + n); return new TA(n); }, arrayLike, (v) => { order.push("map" + v); return v; }); + console.log(order.join(" ")); +} +customs(Int32Array); + +// Reducers and comparators run with an undefined this, also through method +// values; a Symbol.iterator getter is read once. +function review(TA) { + "use strict"; + const ta = new TA([3, 1, 2]); + const seen = []; + const reduce = ta.reduce, reduceRight = ta.reduceRight, sort = ta.sort, toSorted = ta.toSorted; + reduce.call(ta, function (a, b) { seen.push(this === undefined); return a + b; }); + reduceRight.call(ta, function (a, b) { seen.push(this === undefined); return a + b; }, 0); + sort.call(ta.slice(), function (x, y) { seen.push(this === undefined); return x - y; }); + toSorted.call(ta, function (x, y) { seen.push(this === undefined); return y - x; }); + console.log(seen.every((v) => v), seen.length > 0); + let gets = 0; + const source = {}; + Object.defineProperty(source, Symbol.iterator, { get() { gets++; return function* () { yield 4; yield 5; }; } }); + console.log(new TA(source), gets); + console.log(TA.from(source), gets); +} +review(Uint8Array); +function plainIterator(TA) { + let gets = 0, nexts = 0; + const source = {}; + Object.defineProperty(source, Symbol.iterator, { + get() { + gets++; + return function () { + let i = 0; + return { next() { nexts++; return i < 2 ? { value: 10 + i++, done: false } : { value: undefined, done: true }; } }; + }; + }, + }); + console.log(new TA(source), TA.from(source, (v) => v * 2), gets, nexts); +} +plainIterator(Int16Array); +// ToIndex of a Symbol or BigInt length, toLocaleString, and Symbol descriptions +// that are not strings. +function small(TA) { + for (const bad of [Symbol("s"), 1n]) { + try { new TA(bad); console.log("no throw"); } catch (e) { console.log(e.constructor.name); } + } + console.log(new TA([1, 1234.5, -0, 0]).toLocaleString(), new TA(0).toLocaleString(), typeof new TA(1).toLocaleString); +} +small(Float64Array); +small(Uint16Array); +const s = Symbol(1), t = Symbol(true), u = Symbol(2.5); +console.log(s.description, t.description, u.toString(), typeof s.description); diff --git a/tests/corpus/7753-typed-array-named-properties.js b/tests/corpus/7753-typed-array-named-properties.js new file mode 100644 index 000000000..e37181a5e --- /dev/null +++ b/tests/corpus/7753-typed-array-named-properties.js @@ -0,0 +1,183 @@ +// Named properties on typed arrays, Buffers and DataViews (assignment, +// Object.defineProperty, reflection, deletion, integrity levels, JSON and +// inspection), and the Symbol.species constructor of map, filter, slice and +// subarray. +function box(value) { + const holder = {}; + holder.value = value; + return holder.value; +} + +function expandos(TA) { + const a = new TA([1, 2]); + a.foo = 1; + a.bar = "x"; + a[5] = 9; + a["1.5"] = 7; + console.log(a, a.foo, "foo" in a, a.hasOwnProperty("bar"), Object.keys(a), Object.entries(a).length); + console.log(Object.getOwnPropertyNames(a), JSON.stringify(Object.getOwnPropertyDescriptor(a, "foo")), JSON.stringify(Object.getOwnPropertyDescriptor(a, "0"))); + Object.defineProperty(a, "hidden", { value: 3, enumerable: false }); + Object.defineProperty(a, "0", { value: 8 }); + console.log(a, a.hidden, Object.keys(a)); + try { Object.defineProperty(a, "1", { value: 1, configurable: false }); } catch (e) { console.log(e.constructor.name, e.message); } + try { Object.defineProperty(a, "7", { value: 1 }); } catch (e) { console.log(e.constructor.name, e.message); } + try { Object.defineProperty(a, "-0", { value: 1 }); } catch (e) { console.log(e.constructor.name, e.message); } + try { Object.defineProperty(a, "0", { get() { return 1; } }); } catch (e) { console.log(e.constructor.name, e.message); } + // An element's descriptor is read through its getters, and must be an object. + const got = []; + Object.defineProperty(a, "1", { get value() { got.push("value"); return 6; }, get enumerable() { got.push("enumerable"); return true; } }); + console.log(a[1], got); + // Each field is read once, through its getter; the stored value is the + // getter's result, and a getter-produced `writable: false` is refused. + let valueReads = 0; + Object.defineProperty(a, "1", { get value() { valueReads++; return 40 + valueReads; } }); + console.log(a[1], valueReads); + const order = []; + try { + Object.defineProperty(a, "1", { + get writable() { order.push("writable"); return false; }, + get value() { order.push("value"); return 2; }, + get configurable() { order.push("configurable"); return true; }, + }); + } catch (e) { console.log(e.constructor.name, e.message); } + console.log(a[1], order); + for (const bad of [null, true, 1n, Symbol("d")]) { + try { Object.defineProperty(a, "1", bad); } catch (e) { console.log(e.constructor.name); } + } + for (const bad of [5, "x", undefined]) { + try { Object.defineProperty(a, "0", bad); } catch (e) { console.log(e.constructor.name); } + } + try { Object.defineProperty(a, "9", 5); } catch (e) { console.log(e.constructor.name); } + try { Object.defineProperty(a, "0", { get configurable() { throw new RangeError("read"); } }); } catch (e) { console.log(e.constructor.name, e.message); } + try { Object.defineProperty(a, "0", { value: 1, get: undefined }); } catch (e) { console.log(e.constructor.name); } + console.log(a[0], a[1]); + let reads = 0; + Object.defineProperty(a, "g", { get() { reads++; return this.length; }, enumerable: true }); + console.log(a.g, reads, new TA(0)); + const e = new TA(0); + e.k = { deep: [1] }; + console.log(e); +} + +function species(TA, Other) { + const s = new TA([1, 2, 3, 4]); + const calls = []; + s.constructor = {}; + s.constructor[Symbol.species] = function (n) { calls.push(n); return new Other(n); }; + const m = s.map((v) => v * 1.5), f = s.filter((v) => v > 1), sl = s.slice(1, 3); + console.log(m, f, sl, calls); + s.constructor[Symbol.species] = function (buffer, offset, length) { calls.push([offset, length]); return new TA(buffer, offset, length); }; + const sub = s.subarray(1, 3); + sub[0] = 50; + console.log(sub, s[1], calls.length); + s.constructor[Symbol.species] = function () { return {}; }; + try { s.map((v) => v); } catch (err) { console.log(err.constructor.name); } + s.constructor[Symbol.species] = function () { return new TA(1); }; + try { s.slice(); } catch (err) { console.log(err.constructor.name); } + s.constructor = 5; + try { s.filter(() => true); } catch (err) { console.log(err.constructor.name); } + s.constructor = undefined; + console.log(s.map((v) => v + 1)); + const t = new TA([3, 1]); + Object.defineProperty(t, "constructor", { get() { calls.push("get"); return undefined; } }); + console.log(t.slice(), calls[calls.length - 1]); + const u = new TA([1, 2, 3]); + u.constructor = { [Symbol.species]: Uint16Array }; + console.log(u.filter(() => true), u.subarray(1), u.subarray(1).constructor === Uint16Array); + // A built-in constructor set as `constructor` is its own species. + const w = new TA([1, 2, 3]); + w.constructor = Float64Array; + const wm = w.map((v) => v / 2), ws = w.slice(1); + console.log(wm, wm.constructor === Float64Array, ws, ws.constructor === Float64Array, w.filter((v) => v > 1)); + w.constructor = TA; + console.log(w.map((v) => v / 2).constructor === TA); + // slice copies the source after the species constructor ran. + const x = new TA([1, 2, 3, 4]); + x.constructor = {}; + x.constructor[Symbol.species] = function (n) { x[1] = 40; x[2] = 30; return new Other(n); }; + console.log(x.slice(1, 3), x.slice(-1), x.slice(2, 1)); +} + +function reflection(TA) { + const a = new TA([1, 2]); + a.foo = 1; + a[Symbol.for("s")] = 2; + const keys = []; + for (const k in a) keys.push(k); + console.log(keys, { ...a }, JSON.stringify(a), Object.values(a)); + console.log(a.propertyIsEnumerable("foo"), Object.hasOwn(a, "foo"), Object.getOwnPropertySymbols(a), a[Symbol.for("s")]); + console.log(Object.assign(new TA(2), { x: 1, 0: 9 }), Object.assign({}, a)); + delete a.foo; + delete a[5]; + delete a.length; + console.log(a, "foo" in a, a.length); + try { delete a[0]; } catch (err) { console.log(err.message); } + Object.defineProperty(a, "fixed", { value: 1 }); + try { delete a.fixed; } catch (err) { console.log(err.message); } + try { a.fixed = 2; } catch (err) { console.log(err.message); } + console.log(a.subarray(0, 1).fixed, a.slice().fixed); + let seen; + Object.defineProperty(a, "s", { set(v) { seen = [v, this === a]; } }); + a.s = 7; + console.log(seen); + a.map = 5; + try { a.map((v) => v); } catch (err) { console.log(err.message); } + for (const key of ["length", "byteLength", "byteOffset", "buffer"]) { + try { a[key] = 1; } catch (err) { console.log(err.message); } + } + try { a[Symbol.toStringTag] = "x"; } catch (err) { console.log(err.message); } + // Cycles through a view's own properties are collected. + const loop = new TA(1); + loop.self = loop; + loop.size = () => loop.length; + console.log(loop.self === loop, loop.size()); +} + +function integrity(TA) { + const results = []; + const attempt = (f) => { try { f(); results.push("ok"); } catch (err) { results.push(err.message); } }; + const sealed = new TA(1); + sealed.x = 1; + attempt(() => Object.seal(sealed)); + console.log(results, Object.isExtensible(sealed), Object.isSealed(sealed), JSON.stringify(Object.getOwnPropertyDescriptor(sealed, "x"))); + attempt(() => Object.seal(sealed)); + const frozen = new TA(1); + frozen.x = 1; + attempt(() => Object.freeze(frozen)); + attempt(() => Object.freeze(frozen)); + console.log(results, Object.isFrozen(frozen), JSON.stringify(Object.getOwnPropertyDescriptor(frozen, "x"))); + const closed = new TA(1); + Object.defineProperty(closed, "q", { value: 1 }); + Object.preventExtensions(closed); + try { closed.z = 1; } catch (err) { console.log(err.message); } + try { closed[Symbol("n")] = 1; } catch (err) { console.log(err.message); } + console.log(Object.isExtensible(closed), Object.isSealed(closed), Object.isFrozen(closed)); + const empty = new TA(0); + empty.k = 1; + Object.freeze(empty); + try { empty.k = 2; } catch (err) { console.log(err.message); } + console.log(Object.isFrozen(empty), Object.isSealed(empty), empty.k); +} + +expandos(Uint8Array); +expandos(Float64Array); +species(Int16Array, Float32Array); +reflection(Uint8Array); +reflection(Int32Array); +integrity(Uint16Array); + +const view = box(new DataView(new ArrayBuffer(2))); +view[0] = 5; +view.x = 1; +console.log(view, Object.keys(view), view[0], JSON.stringify(view), "0" in view); +try { view.byteLength = 1; } catch (err) { console.log(err.message); } +const buffer = box(Buffer.from([1, 2])); +buffer.foo = 1; +buffer[Symbol.for("k")] = 2; +console.log(buffer, Object.keys(buffer), JSON.stringify(buffer)); +for (const key of ["parent", "offset", "length"]) { + try { buffer[key] = 1; } catch (err) { console.log(err.message); } +} +const empty = box(Buffer.alloc(0)); +empty.foo = { a: [1, { b: 2 }] }; +console.log(empty); diff --git a/tests/corpus/7757-speculative-key-chains.ts b/tests/corpus/7757-speculative-key-chains.ts new file mode 100644 index 000000000..1ef08a942 --- /dev/null +++ b/tests/corpus/7757-speculative-key-chains.ts @@ -0,0 +1,124 @@ +// Speculative integer loops over writes that call small pure number +// functions, use Math.pow(2, n) and Math.floor(a / 2^n), read bytes, or +// call other functions: every fallback gives Node's numbers. +function trim(value: number, length: number): number { + return value - Math.floor(value / Math.pow(2, length)) * Math.pow(2, length); +} +function maybeTrim(value: number | undefined, length: number): number { + return (value ?? 0) - Math.floor((value ?? 0) / Math.pow(2, length)) * Math.pow(2, length); +} + +function chain(codes: Uint8Array, length: number): number { + const mask = length * 2 - 2; + let key = 0; + let sum = 0; + for (let i = 0; i < codes.length; ++i) { + sum = (sum + key) % 1000000007; + key = trim(key, mask) * 4 + codes[i]!; + } + return sum * 31 + key; +} + +function optionalChain(codes: Uint8Array, length: number): number { + let key = 0; + let sum = 0; + for (let i = 0; i < codes.length; ++i) { + sum = (sum * 3 + key) % 1000003; + key = maybeTrim(key, length * 2) * 4 + codes[i]!; + } + return sum + key; +} + +// Exponents past 52, negative and fractional values, and NaN reads leave +// the integer copy and finish in the double one. +function shifts(start: number, exponent: number, steps: number): number[] { + const out: number[] = []; + let v = start; + for (let i = 0; i < steps; i++) { + v = Math.floor(v / Math.pow(2, exponent)) * 3 + i * 7; + out.push(v); + } + return out; +} + +let calls = 0; +function noisy(x: number): number { + calls++; + return x % 5 === 0 ? x / 2 : x * 2; +} +// A call that the write cannot expand runs once per iteration, also on +// the iteration whose check fails. +function hoisted(n: number): number { + let acc = 1; + for (let i = 0; i < n; i++) acc = (acc * 7 + noisy(i)) % 1000003; + return acc; +} + +// A check that fails before a write with a hoisted call: that write still +// makes its call on the iteration that bails. +function tally(x: number): number { + calls++; + return x * 2 + 1; +} +function chained(n: number): number { + let a = 1; + let b = 1; + for (let i = 0; i < n; i++) { + a = a * 3 + i; + b = (b * 7 + tally(i) + (a % 11)) % 1000003; + } + return b; +} + +// A divisor next to a power of two is not one: 2^52 + 1 divides as itself. +function nearPowers(start: number, steps: number): number[] { + const out: number[] = []; + let v = start; + for (let i = 0; i < steps; i++) { + v = Math.floor(v / 4503599627370497) * 4 + Math.floor(v / 4503599627370496) + i; + out.push(v); + } + return out; +} + +// A helper that calls a function with effects is not expanded: its +// argument is read before the call runs. +let level = 0; +function raise(): number { + level += 100; + return 1; +} +function lifted(x: number): number { + return raise() + x; +} +function effectfulHelper(n: number): number { + let acc = 0; + for (let i = 0; i < n; i++) acc = (acc * 3 + lifted(level)) % 1000003; + return acc; +} + +// A call whose argument assigns a local the write also reads stays where it +// is: `i` is read before `i++` runs. +function stepped(n: number): number { + let acc = 1; + let i = 0; + while (i < n) acc = (acc * 3 + i + tally(i++)) % 1000003; + return acc; +} + +const codes = new Uint8Array(5000); +let seed = 7; +for (let i = 0; i < codes.length; i++) { + seed = (seed * 1103515245 + 12345) % 2147483648; + codes[i] = seed % 4; +} +for (const length of [1, 2, 3, 6, 12, 18, 27]) console.log(length, chain(codes, length), optionalChain(codes, length)); +console.log(shifts(1000, 3, 6), shifts(-1000, 2, 6), shifts(5, 53, 3), shifts(7.5, 1, 3), shifts(2 ** 52, 1, 3)); +console.log(hoisted(20), calls); +const short = codes.subarray(0, 3); +console.log(chain(short, 2), chain(new Uint8Array(0), 2)); +calls = 0; +console.log(chained(34), chained(60), calls); +console.log(nearPowers(4503599627370496, 3), nearPowers(9007199254740991, 2)); +console.log(effectfulHelper(10), level); +console.log(stepped(40)); diff --git a/tests/corpus/7790-js-class-integrity-layouts.js b/tests/corpus/7790-js-class-integrity-layouts.js new file mode 100644 index 000000000..f889c02f8 --- /dev/null +++ b/tests/corpus/7790-js-class-integrity-layouts.js @@ -0,0 +1,139 @@ +"use strict"; +// Object.freeze, Object.seal and Object.preventExtensions next to JavaScript +// classes whose fields all exist from construction. The integrity state of +// a class instance lives beside its fields, so classes keep fixed layouts +// whether or not a program freezes values; frozen instances still refuse +// writes and additions as in Node. + +class Point { + constructor(x, y) { + this.x = x; + this.y = y; + } + norm() { + return Math.abs(this.x) + Math.abs(this.y); + } +} + +class Point3 extends Point { + constructor(x, y, z) { + super(x, y); + this.z = z; + } + norm() { + return super.norm() + Math.abs(this.z); + } +} + +class Status { + constructor() { + this.value = "valid"; + } + dirty() { + if (this.value === "valid") this.value = "dirty"; + } +} + +/** @param {any} target @param {string} key @param {unknown} value */ +function put(target, key, value) { + target[key] = value; +} + +/** @param {any} value */ +function describe(value) { + const keys = Object.keys(value); + const parts = []; + for (const key in value) parts.push(`${key}=${String(value[key])}`); + return `${keys.join(",")} | ${parts.join(" ")} | ${JSON.stringify(value)}`; +} + +/** @param {() => void} fn */ +function attempt(label, fn) { + try { + fn(); + console.log(label, "ok"); + } catch (e) { + console.log(label, e instanceof TypeError ? "TypeError" : "other", e.message); + } +} + +// The zod shape: freeze whatever value a result carries. Here it is always +// plain data, never a class instance. +/** @param {{ status: string, value: any }} data */ +function freezeResult(data) { + if (data.status === "valid") data.value = Object.freeze(data.value); + return data; +} + +const results = [ + freezeResult({ status: "valid", value: { a: 1, list: [1, 2] } }), + freezeResult({ status: "valid", value: [3, 4] }), + freezeResult({ status: "aborted", value: { b: 2 } }), + freezeResult({ status: "valid", value: "text" }), +]; +for (const result of results) console.log(result.status, Object.isFrozen(result.value), JSON.stringify(result.value)); +/** @param {() => void} fn */ +function throws(fn) { + try { + fn(); + return false; + } catch (e) { + return e instanceof TypeError; + } +} +console.log(throws(() => put(results[0].value, "a", 9)), throws(() => put(results[2].value, "b", 3))); +console.log(JSON.stringify(results[2].value)); + +// Class instances that only ever travel through untyped code. +const status = new Status(); +status.dirty(); +const p = new Point(1, -2); +const q = new Point3(3, 4, -5); +put(p, "tag", "p"); +put(q, "w", 6); +console.log(describe(status), status.value); +console.log(describe(p), p.norm()); +console.log(describe(q), q.norm()); +console.log(Object.isFrozen(p), Object.isExtensible(q), Object.isSealed(status)); + +// Statically typed freeze, seal and preventExtensions of instances. +const frozen = new Point(5, 6); +Object.freeze(frozen); +attempt("frozen field write", () => { + /** @type {Point} */ (frozen).x = 50; +}); +attempt("frozen field write untyped", () => put(frozen, "y", 60)); +attempt("frozen addition", () => put(frozen, "extra", 1)); +console.log(describe(frozen), Object.isFrozen(frozen), Object.isSealed(frozen), Object.isExtensible(frozen)); +console.log(JSON.stringify(Object.getOwnPropertyDescriptor(frozen, "x"))); + +const sealed = new Point3(1, 2, 3); +put(sealed, "before", true); +Object.seal(sealed); +attempt("sealed field write", () => { + sealed.z = 30; +}); +attempt("sealed added write", () => put(sealed, "before", false)); +attempt("sealed addition", () => put(sealed, "after", 1)); +console.log(describe(sealed), Object.isFrozen(sealed), Object.isSealed(sealed), Object.isExtensible(sealed)); +console.log(JSON.stringify(Object.getOwnPropertyDescriptor(sealed, "z"))); + +const closed = new Status(); +Object.preventExtensions(closed); +closed.dirty(); +attempt("closed addition", () => put(closed, "more", 1)); +console.log(describe(closed), Object.isFrozen(closed), Object.isExtensible(closed)); + +// A subclass instance frozen through its base type. +/** @type {Point} */ +const asBase = new Point3(7, 8, 9); +Object.freeze(asBase); +attempt("frozen subclass field", () => put(asBase, "z", 90)); +console.log(describe(asBase), asBase.norm(), Object.isFrozen(asBase)); + +// Fresh instances of the same classes stay writable. +const fresh = new Point3(0, 0, 0); +fresh.x = 1; +put(fresh, "y", 2); +put(fresh, "added", 3); +console.log(describe(fresh), fresh.norm(), Object.isFrozen(fresh)); diff --git a/tests/corpus/7791-js-class-accessor-layouts.js b/tests/corpus/7791-js-class-accessor-layouts.js new file mode 100644 index 000000000..6c87989cb --- /dev/null +++ b/tests/corpus/7791-js-class-accessor-layouts.js @@ -0,0 +1,85 @@ +"use strict"; +// Getters and setters on JavaScript classes. An accessor whose name no +// field uses lives on the prototype beside the fields; an accessor that +// shares a field's name decides what an assignment does. Both behave as in +// Node, for direct use and for instances passed to untyped code. + +class LazyPath { + constructor(parent, value, path, key) { + this._cachedPath = []; + this.parent = parent; + this.data = value; + this._path = path; + this._key = key; + } + get path() { + if (!this._cachedPath.length) { + if (Array.isArray(this._key)) this._cachedPath.push(...this._path, ...this._key); + else this._cachedPath.push(...this._path, this._key); + } + return this._cachedPath; + } +} + +class Temperature { + constructor(celsius) { + this.celsius = celsius; + } + get fahrenheit() { + return this.celsius * 1.8 + 32; + } + set fahrenheit(value) { + this.celsius = (value - 32) / 1.8; + } +} + +class Labeled extends Temperature { + constructor(celsius, label) { + super(celsius); + this.label = label; + } + get description() { + return `${this.label}: ${this.celsius.toFixed(1)}C`; + } +} + +/** @param {any} target @param {string} key @param {unknown} value */ +function put(target, key, value) { + target[key] = value; +} + +/** @param {any} value */ +function describe(value) { + const parts = []; + for (const key in value) parts.push(key); + return `${Object.keys(value).join(",")} | ${parts.join(",")} | ${JSON.stringify(value)}`; +} + +/** @param {any} value @param {string} key */ +function probe(value, key) { + return `${key}: in=${key in value} own=${Object.prototype.hasOwnProperty.call(value, key)} value=${JSON.stringify(value[key])}`; +} + +const root = { common: true }; +const paths = []; +for (let i = 0; i < 3; i++) paths.push(new LazyPath(root, { n: i }, ["items"], i)); +paths.push(new LazyPath(root, null, ["a"], ["b", "c"])); +for (const item of paths) console.log(item.path.join("."), item.data && item.data.n, item.parent === root); +console.log(describe(paths[0])); +console.log(probe(paths[1], "path"), probe(paths[1], "_key")); +put(paths[2], "extra", 1); +console.log(describe(paths[2]), paths[2].path.length); + +const t = new Temperature(100); +console.log(t.fahrenheit, describe(t)); +t.fahrenheit = 32; +console.log(t.celsius, t.fahrenheit); +put(t, "fahrenheit", 212); +console.log(t.celsius, probe(t, "fahrenheit"), probe(t, "celsius")); +put(t, "note", "added"); +console.log(describe(t)); + +const l = new Labeled(20, "room"); +console.log(l.description, l.fahrenheit, describe(l)); +l.fahrenheit = 50; +console.log(l.description, probe(l, "description")); diff --git a/tests/corpus/7792-untyped-property-reads-mixed-receivers.js b/tests/corpus/7792-untyped-property-reads-mixed-receivers.js new file mode 100644 index 000000000..6a2785541 --- /dev/null +++ b/tests/corpus/7792-untyped-property-reads-mixed-receivers.js @@ -0,0 +1,111 @@ +"use strict"; +// One untyped property read site meets class instances (fixed and tracked +// layouts, getters, own properties added later), plain objects, arrays, +// strings, functions, class constructors with statics, null prototypes and +// optional chains. Each receiver kind answers as in Node. + +class Status { + constructor() { + this.value = "valid"; + } +} + +class Lazy { + constructor(key) { + this.key = key; + } + get value() { + return `lazy:${this.key}`; + } + get length() { + return 7; + } +} + +class Tracked { + constructor(list) { + list.push(this); + this.value = list.length; + } + static value = "static"; +} + +function plainFunction() {} +plainFunction.value = "fn"; + +/** @param {any} target */ +function readValue(target) { + return target.value; +} +/** @param {any} target */ +function readLength(target) { + return target.length; +} +/** @param {any} target */ +function readOptional(target) { + return target?.value; +} +/** @param {any} target @param {unknown} value */ +function writeValue(target, value) { + target.value = value; +} + +const list = []; +const added = new Status(); +writeValue(added, "dirty"); +/** @type {any} */ +const extra = new Lazy("x"); +extra.other = 1; +const nullProto = Object.create(null); +nullProto.value = "bare"; +const inherited = Object.create({ value: "from prototype" }); +const receivers = [ + new Status(), + added, + new Lazy("k"), + extra, + new Tracked(list), + { value: 1, length: 2 }, + { other: true }, + [1, 2, 3], + "text", + 42, + true, + plainFunction, + Tracked, + nullProto, + inherited, +]; +for (const receiver of receivers) { + console.log(typeof receiver, JSON.stringify(readValue(receiver)), JSON.stringify(readLength(receiver)), JSON.stringify(readOptional(receiver))); +} +console.log(readOptional(null), readOptional(undefined)); +for (const bad of [null, undefined]) { + try { + readValue(bad); + } catch (e) { + console.log(e instanceof TypeError, e.message); + } +} + +const plain = { value: 0 }; +const arr = []; +writeValue(plain, 5); +writeValue(arr, "on array"); +writeValue(inherited, "own now"); +writeValue(plainFunction, "fn2"); +const status = new Status(); +writeValue(status, "aborted"); +console.log(plain.value, arr.value, Object.keys(inherited), plainFunction.value, status.value, JSON.stringify(status)); +try { + writeValue("text", 1); +} catch (e) { + console.log(e instanceof TypeError, e.message); +} +const frozen = Object.freeze({ value: "frozen" }); +try { + writeValue(frozen, 2); +} catch (e) { + console.log(e instanceof TypeError); +} +console.log(frozen.value); diff --git a/tests/corpus/7793-untyped-key-writes.js b/tests/corpus/7793-untyped-key-writes.js new file mode 100644 index 000000000..93158145e --- /dev/null +++ b/tests/corpus/7793-untyped-key-writes.js @@ -0,0 +1,83 @@ +"use strict"; +// Untyped property writes through one site: new keys append in order, +// existing keys keep their position, own and inherited setters run, +// getter-only and read-only properties refuse the write, and +// non-extensible objects refuse new keys, as in Node. + +/** @param {any} target @param {string} key @param {unknown} value */ +function put(target, key, value) { + target[key] = value; +} + +/** @param {() => void} fn */ +function attempt(label, fn) { + try { + fn(); + console.log(label, "ok"); + } catch (e) { + console.log(label, e instanceof TypeError ? "TypeError" : String(e)); + } +} + +const order = {}; +for (const key of ["b", "a", "c", "a", "b", "d"]) put(order, key, key.charCodeAt(0)); +console.log(JSON.stringify(order), Object.keys(order).join("")); + +const log = []; +const withSetter = { + _v: 0, + get v() { + return this._v; + }, + set v(value) { + log.push(`own ${value}`); + this._v = value * 2; + }, + get onlyGetter() { + return "g"; + }, +}; +put(withSetter, "v", 4); +console.log(withSetter.v, JSON.stringify(log)); +attempt("getter-only", () => put(withSetter, "onlyGetter", 1)); + +const proto = { + /** @param {number} value */ + set inherited(value) { + log.push(`inherited ${value} on ${this === child}`); + }, +}; +Object.defineProperty(proto, "fixed", { value: 1, writable: false }); +const child = Object.create(proto); +put(child, "inherited", 7); +attempt("inherited read-only", () => put(child, "fixed", 2)); +put(child, "own", 3); +console.log(JSON.stringify(log), Object.keys(child).join(","), child.fixed); + +/** @type {any} */ +const sealed = { x: 1 }; +Object.seal(sealed); +put(sealed, "x", 2); +attempt("sealed add", () => put(sealed, "y", 3)); +/** @type {any} */ +const closed = { x: 1 }; +Object.preventExtensions(closed); +put(closed, "x", 5); +attempt("closed add", () => put(closed, "z", 1)); +const frozen = Object.freeze({ x: 1 }); +attempt("frozen write", () => put(frozen, "x", 9)); +console.log(JSON.stringify(sealed), JSON.stringify(closed), JSON.stringify(frozen)); + +const readOnly = {}; +Object.defineProperty(readOnly, "k", { value: "kept", writable: false, enumerable: true }); +attempt("own read-only", () => put(readOnly, "k", "changed")); +console.log(readOnly.k); + +const error = new Error("boom"); +put(error, "cause", "why"); +put(error, "code", "E_X"); +console.log(error.cause, error.code); + +const wide = {}; +for (let i = 0; i < 40; i++) put(wide, `k${i % 25}`, i); +console.log(Object.keys(wide).length, wide.k0, wide.k24, Object.keys(wide).slice(0, 3).join(",")); diff --git a/tests/corpus/7794-untyped-array-iteration.js b/tests/corpus/7794-untyped-array-iteration.js new file mode 100644 index 000000000..ba7fe0810 --- /dev/null +++ b/tests/corpus/7794-untyped-array-iteration.js @@ -0,0 +1,86 @@ +"use strict"; +// for...of, spread and destructuring over untyped arrays: elements are read +// live, so pushes and truncation during the loop show, holes read as +// undefined, an own Symbol.iterator replaces the builtin one, and keys() +// and entries() keep their own shapes, as in Node. + +/** @param {any} list */ +function collect(list) { + const out = []; + for (const item of list) out.push(item); + return out; +} + +/** @param {any} list */ +function growWhileIterating(list) { + const seen = []; + for (const item of list) { + seen.push(item); + if (item === 2) list.push(99); + if (seen.length > 10) break; + } + return seen; +} + +/** @param {any} list */ +function shrinkWhileIterating(list) { + const seen = []; + for (const item of list) { + seen.push(item); + list.length = 2; + } + return seen; +} + +const parsed = JSON.parse('[1, "two", {"three": 3}, [4], null, true]'); +console.log(JSON.stringify(collect(parsed))); +console.log(JSON.stringify(collect([]))); +console.log(JSON.stringify(growWhileIterating(JSON.parse("[1, 2, 3]")))); +console.log(JSON.stringify(shrinkWhileIterating(JSON.parse("[1, 2, 3, 4, 5]")))); + +/** @type {any} */ +const holes = [1, , 3]; +holes.length = 5; +console.log(collect(holes).map((x) => String(x)).join(",")); + +/** @type {any} */ +const custom = JSON.parse("[1, 2, 3]"); +custom[Symbol.iterator] = function* () { + yield "custom"; + yield this.length; +}; +console.log(JSON.stringify(collect(custom))); + +/** @type {any} */ +const data = JSON.parse('[10, 20, 30]'); +console.log(JSON.stringify([...data]), JSON.stringify([...data.keys()]), JSON.stringify([...data.entries()])); +const [first, , third = "default", fourth = "missing"] = data; +console.log(first, third, fourth); + +let total = 0; +for (const value of data) { + if (value === 20) continue; + total += value; +} +console.log(total); + +/** @param {any} list */ +function nested(list) { + const pairs = []; + for (const a of list) for (const b of list) pairs.push(`${a}${b}`); + return pairs.join(" "); +} +console.log(nested(JSON.parse('["x", "y"]'))); + +/** @param {any} list */ +function earlyReturn(list) { + for (const item of list) if (item > 1) return item; + return -1; +} +console.log(earlyReturn(JSON.parse("[0, 1, 2, 3]")), earlyReturn(JSON.parse("[0]"))); + +try { + console.log(collect(null)); +} catch (e) { + console.log(e instanceof TypeError, e.message); +} diff --git a/tests/corpus/7795-untyped-global-constructors.js b/tests/corpus/7795-untyped-global-constructors.js new file mode 100644 index 000000000..0b464b7b2 --- /dev/null +++ b/tests/corpus/7795-untyped-global-constructors.js @@ -0,0 +1,29 @@ +"use strict"; +// Global constructors read as values in untyped code: instanceof tests, +// identity comparisons and constructor names, as zod's type sniffing uses +// them, answer as in Node for every global the lookup knows. + +/** @param {unknown} data */ +function kind(data) { + if (typeof Map !== "undefined" && data instanceof Map) return "map"; + if (typeof Set !== "undefined" && data instanceof Set) return "set"; + if (typeof Date !== "undefined" && data instanceof Date) return "date"; + if (data instanceof TypeError) return "type-error"; + if (data instanceof RangeError) return "range-error"; + if (data instanceof Error) return "error"; + if (data instanceof Promise) return "promise"; + if (Array.isArray(data)) return "array"; + return typeof data; +} + +const values = [new Map(), new Set([1]), new Date(0), new TypeError("t"), new RangeError("r"), new SyntaxError("s"), new Error("e"), Promise.resolve(1), [1], { a: 1 }, "s", 1, null]; +for (const value of values) console.log(kind(value)); + +const ctors = [Map, Set, Date, Error, TypeError, RangeError, SyntaxError, ReferenceError, EvalError, URIError, Object, Promise, WeakMap, WeakSet, RegExp]; +console.log(ctors.map((ctor) => ctor.name).join(",")); +console.log(ctors.every((ctor, i) => ctors.indexOf(ctor) === i)); +const again = [Map, Set, Date, Error, TypeError]; +console.log(again.every((ctor, i) => ctor === ctors[i])); +/** @type {any} */ +const err = new TypeError("x"); +console.log(err.constructor === TypeError, err.constructor === Error, err instanceof Error); diff --git a/tests/corpus/7796-boxed-subclass-identity.js b/tests/corpus/7796-boxed-subclass-identity.js new file mode 100644 index 000000000..12ed8b3c5 --- /dev/null +++ b/tests/corpus/7796-boxed-subclass-identity.js @@ -0,0 +1,106 @@ +"use strict"; +// Class instances of a hierarchy cross into untyped code through their +// base type many times. Each crossing names the same object: identity, +// Map, Set and WeakMap keys, properties added through untyped code, and +// field writes from either side stay one object, and every subclass keeps +// its own members, as in Node. + +class Shape { + constructor(name) { + this.name = name; + this.hits = 0; + } + area() { + return 0; + } + describe() { + return `${this.name}:${this.area()}`; + } +} + +class Square extends Shape { + constructor(side) { + super("square"); + this.side = side; + } + area() { + return this.side * this.side; + } +} + +class Cube extends Square { + constructor(side) { + super(side); + this.name = "cube"; + } + area() { + return 6 * super.area(); + } + get volume() { + return this.side ** 3; + } +} + +class Circle extends Shape { + constructor(r) { + super("circle"); + this.r = r; + } + area() { + return Math.round(Math.PI * this.r * this.r); + } +} + +/** @param {any} value */ +function untyped(value) { + return value; +} +/** @param {any} value @param {string} key @param {unknown} extra */ +function tag(value, key, extra) { + value[key] = extra; + value.hits++; + return value; +} + +/** @param {Shape} shape */ +function crossAsBase(shape) { + return untyped(shape); +} + +const shapes = [new Shape("plain"), new Square(2), new Cube(3), new Circle(1)]; +const seen = new Map(); +const weak = new WeakMap(); +const set = new Set(); +for (let round = 0; round < 50; round++) { + for (const shape of shapes) { + const boxed = crossAsBase(shape); + if (round === 0) { + seen.set(boxed, shape.name); + weak.set(boxed, round); + } + set.add(boxed); + if (boxed !== shape || untyped(boxed) !== crossAsBase(shape)) console.log("identity lost", shape.name); + } +} +console.log(seen.size, set.size, shapes.map((s) => seen.get(crossAsBase(s))).join(","), shapes.every((s) => weak.has(untyped(s)))); + +for (const shape of shapes) { + const boxed = crossAsBase(shape); + tag(boxed, "color", shape.name.length); + shape.hits += 10; + console.log(boxed.describe(), boxed.area(), boxed.hits, shape.hits, boxed.color, Object.keys(boxed).join(",")); + console.log(boxed instanceof Shape, boxed instanceof Square, boxed instanceof Cube, boxed instanceof Circle, boxed.constructor.name); +} + +/** @type {Shape} */ +const asBase = shapes[2]; +const cube = crossAsBase(asBase); +console.log(cube.volume, JSON.stringify(cube), "side" in cube, "volume" in cube, Object.prototype.hasOwnProperty.call(cube, "volume")); +/** @type {any} */ (shapes[1]).side = 5; +console.log(crossAsBase(shapes[1]).area(), shapes[1].area(), untyped(shapes[1]).side); + +const many = []; +for (let i = 0; i < 1000; i++) many.push(crossAsBase(i % 2 ? new Square(i) : new Circle(i))); +let squares = 0; +for (const item of many) if (item instanceof Square) squares += item.side; +console.log(many.length, squares, many[999].describe(), many[998].describe()); diff --git a/tests/corpus/7902-versioned-loop-slow-copies.ts b/tests/corpus/7902-versioned-loop-slow-copies.ts new file mode 100644 index 000000000..91b769f47 --- /dev/null +++ b/tests/corpus/7902-versioned-loop-slow-copies.ts @@ -0,0 +1,139 @@ +// Loops whose versioning guard can fail, so they run the ordinary copy: +// arrays whose holes were filled later, counted loops that read past the +// end, arrays that change length inside the loop, typed-array windows past +// the end, fractional and NaN bounds, and callbacks that are replaced. The +// loops keep integer counters and sums that would also suit integer +// speculation, with values past 2^31 and 2^53, negative zero and fractions. + +function sum(values: number[]): string { + let total = 0; + let count = 0; + let seed = 12345; + let grow = 1; + for (let i = 0; i < values.length; i++) { + total += values[i]!; + count++; + seed = (seed * 1103515245 + 12345) % 2147483648; + grow = grow * 7 + i; + } + return `${total} ${count} ${seed} ${grow}`; +} + +function product(values: number[], limit: number): string { + let p = 1; + let steps = 0; + for (let i = 0; i < limit; i++) { + p *= values[i] ?? 1; + steps += 3; + } + return `${p} ${steps}`; +} + +function shrinking(values: number[]): string { + let total = 0; + for (let i = 0; i < values.length; i++) { + total += values[i]! * i; + if (values[i]! % 3 === 0) values.pop(); + } + return `${total} ${values.length}`; +} + +function windowSum(a: Int32Array, from: number, to: number): string { + let total = 0; + let mix = 1; + for (let i = from; i < to; i++) { + total += a[i] ?? -1; + mix = (mix * 31 + 17) % 65521; + } + return `${total} ${mix}`; +} + +function bytes(a: Uint8Array, start: number, n: number): string { + let hash = 0; + let steps = 0; + for (let i = start; i < start + n; i++) { + hash = (hash * 31 + (a[i] ?? 7)) | 0; + steps++; + } + return `${hash} ${steps}`; +} + +function counted(values: Float64Array, n: number, step: number): string { + let total = 0; + let odd = 0; + for (let i = 0; i < n; i += step) { + values[i] = i * 2; + total += i; + odd = odd * 2 + 1; + } + return `${total} ${odd} ${values.join()}`; +} + +function mapped(values: number[], f: (x: number) => number): string { + let total = 0; + let k = 0; + for (let i = 0; i < values.length; i++) { + total += f(values[i]!); + k = k * 10 + 1; + } + return `${total} ${k}`; +} + +function lastNegativeZero(values: number[]): string { + let last = 0; + let zero = 0; + for (let i = 0; i < values.length; i++) { + last = values[i]! * -1; + zero = zero * -1; + } + return `${Object.is(last, -0) ? "-0" : String(last)} ${Object.is(zero, -0) ? "-0" : zero}`; +} + +/** An array written out of order: index 2 first, then the hole at 1. */ +function filledLate(): number[] { + const values: number[] = [1]; + values[2] = 3; + values[1] = 2; + return values; +} + +console.log(sum([1, 2, 3, 4, 5])); +console.log(sum(filledLate())); +console.log(sum([1, 2.5, 3])); +console.log(sum([2147483647, 2147483647, 2147483647])); +console.log(sum([9007199254740991, 2, 2])); +console.log(sum(Array.from({ length: 40 }, (_, i) => (i % 2 === 0 ? i : -i)))); + +console.log(product([1, 2, 3, 4], 4)); +console.log(product([1, 2, 3, 4], 6)); +console.log(product([65536, 65536, 65536], 3)); +console.log(product([0, -1], 2), product([-1, 0], 2)); + +console.log(shrinking([1, 2, 3, 4, 5, 6, 7, 8, 9])); +console.log(shrinking([3, 3, 3, 3])); + +const ints = new Int32Array([10, 20, 30, 40]); +console.log(windowSum(ints, 0, 4), windowSum(ints, 2, 7), windowSum(ints, -2, 2)); +console.log(windowSum(ints, 0.5, 3.5), windowSum(ints, 0, NaN)); + +const text = new Uint8Array([104, 101, 108, 108, 111]); +console.log(bytes(text, 0, 5), bytes(text, 3, 5), bytes(text, -1, 3)); + +console.log(counted(new Float64Array(12), 10, 1), counted(new Float64Array(12), 10.5, 1)); +console.log(counted(new Float64Array(12), 10, 2.5), counted(new Float64Array(4), NaN, 1)); +console.log(counted(new Float64Array(4), 8, 1), counted(new Float64Array(4), 3, 0.75)); +console.log(counted(new Float64Array(60), 60, 1).split(" ").slice(0, 2).join(" ")); + +let calls = 0; +const double = (x: number): number => { + calls++; + return x * 2; +}; +const half = (x: number): number => { + calls++; + return x / 2; +}; +console.log(mapped([1, 2, 3], double), mapped([1, 2, 3], half), calls); +console.log(mapped(filledLate(), double), mapped([1, 2.5], (x) => x * x), calls); + +console.log(lastNegativeZero([1, 2, 0]), lastNegativeZero([1, 2, 3]), lastNegativeZero([])); diff --git a/tests/corpus/7940-typed-array-prototype-set.mjs b/tests/corpus/7940-typed-array-prototype-set.mjs new file mode 100644 index 000000000..4f1b2301d --- /dev/null +++ b/tests/corpus/7940-typed-array-prototype-set.mjs @@ -0,0 +1,40 @@ +// Uint8Array.prototype.set borrowed through .call on untyped receivers. +let cached = null; +function memory() { + if (cached === null) cached = new Uint8Array(4); + return cached; +} +function copy(target, source, offset) { + return Uint8Array.prototype.set.call(target, source, offset); +} +console.log(copy(memory(), new Uint8Array([3, 4]), 1)); +console.log(String(memory())); +copy(memory(), [9], undefined); +console.log(String(memory())); + +// Every typed array shares %TypedArray%.prototype.set. +const wide = new Int16Array(3); +copy(wide, [1, -2], 1); +console.log(String(wide)); +Float64Array.prototype.set.call(memory(), new Uint8Array([7]), 3); +console.log(String(memory())); + +const order = []; +const arg = (label, value) => { + order.push(label); + return value; +}; +for (const target of [{}, [0, 0], "ab", null, new DataView(new ArrayBuffer(2))]) { + order.length = 0; + try { + Uint8Array.prototype.set.call(arg("target", target), arg("source", [1]), arg("offset", 0)); + console.log("set"); + } catch (e) { + console.log(e instanceof TypeError, e.message, order.join(",")); + } +} +try { + copy(memory(), [1, 2], 3); +} catch (e) { + console.log(e instanceof RangeError, e.message); +} diff --git a/tests/corpus/7941-typed-array-prototype-set.ts b/tests/corpus/7941-typed-array-prototype-set.ts new file mode 100644 index 000000000..db880e38a --- /dev/null +++ b/tests/corpus/7941-typed-array-prototype-set.ts @@ -0,0 +1,21 @@ +// Uint8Array.prototype.set borrowed through .call on typed receivers. +const target = new Uint8Array(4); +Uint8Array.prototype.set.call(target, new Uint8Array([3, 4]), 1); +console.log(String(target)); +Uint8Array.prototype.set.call(target, [5]); +console.log(String(target)); + +const wide = new Int32Array(3); +Uint8Array.prototype.set.call(wide, new Uint8Array([200, 100]), 1); +console.log(String(wide)); + +function fill(view: Uint8Array, bytes: number[], at: number): void { + Uint8Array.prototype.set.call(view, bytes, at); +} +fill(target, [8, 9], 2); +console.log(String(target)); +try { + fill(target, [1, 2, 3], 2); +} catch (e) { + console.log(e instanceof RangeError, (e as Error).message); +} diff --git a/tests/corpus/7942-dictionary-store-method-calls.ts b/tests/corpus/7942-dictionary-store-method-calls.ts new file mode 100644 index 000000000..0056c26f6 --- /dev/null +++ b/tests/corpus/7942-dictionary-store-method-calls.ts @@ -0,0 +1,45 @@ +// Stores into dictionaries whose values cannot be functions leave class +// methods in place. +interface Data { + items: string[]; +} +class Store { + data: Data = { items: [] }; + get(): Data { + return this.data; + } +} +class Counter { + n = 0; + add(): number { + return ++this.n; + } +} +interface Settings { + groups: Record>; +} +interface Tree { + [key: string]: Tree; +} +function addGroup(settings: Settings, name: string): void { + settings.groups[name] = {}; +} +function grow(tree: Tree, name: string): Tree { + tree[name] = {}; + return tree[name]!; +} +const settings: Settings = { groups: {} }; +addGroup(settings, "colours"); +addGroup(settings, "fonts"); +settings.groups["colours"]!["fg"] = "red"; +console.log(Object.keys(settings.groups).join(","), JSON.stringify(settings.groups)); +const tree: Tree = {}; +grow(grow(tree, "a"), "b"); +console.log(JSON.stringify(tree)); +const store = new Store(); +store.get().items.push("a"); +store.get().items.push("b"); +const counter = new Counter(); +let total = 0; +for (let i = 0; i < 1000; i++) total += counter.add(); +console.log(store.data.items.length, total, typeof counter.add); diff --git a/tests/corpus/7981-typed-array-paired-loops.ts b/tests/corpus/7981-typed-array-paired-loops.ts new file mode 100644 index 000000000..d13eecbd6 --- /dev/null +++ b/tests/corpus/7981-typed-array-paired-loops.ts @@ -0,0 +1,57 @@ +// Two-counter loops over typed arrays (`for (let i = lo, j = hi; i < j; +// i++, j--)`), with starts inside the arrays and outside them: past the +// end, negative, fractional and NaN. Views over one buffer, other element +// types, inclusive conditions and steps other than one. + +function reverse(a: Int32Array, lo: number, hi: number): string { + for (let i = lo, j = hi; i < j; i++, j--) { + const t = a[i]!; + a[i] = a[j]!; + a[j] = t; + } + return Array.from(a).join(); +} + +function mirror(a: Float64Array, b: Float64Array, hi: number): number { + let sum = 0; + for (let i = 0, j = hi; i <= j; i++, j--) { + b[i] = a[j]! * 2; + sum += a[i]! - b[j]!; + } + return sum; +} + +function skipping(a: Uint8ClampedArray, hi: number): string { + for (let i = 0, j = hi; i < j; i += 2, j -= 3) { + a[i] = a[j]! + 200; + a[j] = i - 10; + } + return Array.from(a).join(); +} + +function palindrome(text: Uint16Array, n: number): boolean { + for (let i = 0, j = n - 1; i < j; i++, j--) if (text[i] !== text[j]) return false; + return true; +} + +console.log(reverse(new Int32Array([1, 2, 3, 4, 5, 6]), 0, 5)); +console.log(reverse(new Int32Array([1, 2, 3, 4, 5, 6]), 1, 3)); +console.log(reverse(new Int32Array([1, 2, 3, 4]), 0, 6)); +console.log(reverse(new Int32Array([1, 2, 3, 4]), -2, 2)); +console.log(reverse(new Int32Array([1, 2, 3, 4]), 0.5, 3.5)); +console.log(reverse(new Int32Array([1, 2, 3, 4]), 0, NaN)); +console.log(reverse(new Int32Array([1, 2, 3, 4]), 3, 0)); + +const buffer = new ArrayBuffer(48); +const whole = new Float64Array(buffer); +for (let k = 0; k < whole.length; k++) whole[k] = k + 0.5; +const tail = new Float64Array(buffer, 16, 3); +console.log(mirror(whole, tail, 4), Array.from(whole).join()); +console.log(mirror(tail, tail, 2), Array.from(whole).join()); +console.log(mirror(tail, whole, 7), Array.from(whole).join()); + +console.log(skipping(new Uint8ClampedArray([10, 20, 30, 40, 50, 60, 70, 80]), 7)); +console.log(skipping(new Uint8ClampedArray([10, 20, 30]), 9)); + +const word = new Uint16Array([114, 97, 99, 101, 99, 97, 114]); +console.log(palindrome(word, 7), palindrome(word, 6), palindrome(word, 9), palindrome(word, 0)); diff --git a/tests/corpus/7982-integer-view-compares.ts b/tests/corpus/7982-integer-view-compares.ts new file mode 100644 index 000000000..5978eaf86 --- /dev/null +++ b/tests/corpus/7982-integer-view-compares.ts @@ -0,0 +1,85 @@ +// Loops whose integer locals take part in compares and typed-array indexes as +// `i + 1`, `i - 1` and `2 * i + 1`: heap sift-downs, neighbor reads at both +// ends of an array, a binary search, and counters near 2^53, where the sum +// of a counter and a constant is still an exact double. + +function siftDown(a: Int32Array, start: number, end: number): void { + let root = start; + while (2 * root + 1 < end) { + let child = 2 * root + 1; + if (child + 1 < end && a[child]! < a[child + 1]!) child++; + if (a[root]! >= a[child]!) return; + const t = a[root]!; + a[root] = a[child]!; + a[child] = t; + root = child; + } +} + +function heapSort(a: Int32Array): string { + for (let start = (a.length >> 1) - 1; start >= 0; start--) siftDown(a, start, a.length); + for (let end = a.length - 1; end > 0; end--) { + const t = a[0]!; + a[0] = a[end]!; + a[end] = t; + siftDown(a, 0, end); + } + return Array.from(a).join(); +} + +function neighbors(a: Float64Array): string { + const out: number[] = []; + let i = 0; + while (i < a.length) { + out.push(a[i - 1]! + a[i + 1]!); + i = i + 1; + } + return out.join(); +} + +function lowerBound(a: Int32Array, value: number): number { + let lo = 0; + let hi = a.length; + while (lo < hi) { + const mid = lo + ((hi - lo) >> 1); + if (a[mid]! < value) lo = mid + 1; + else hi = mid; + } + return lo; +} + +function nearLimit(start: number): string { + const seen: string[] = []; + let i = start; + let n = 0; + while (n < 4) { + seen.push(`${i + 1 > 9007199254740991}:${i + 1 === i + 2}:${i - 1 < i}`); + i = i + 1; + n = n + 1; + } + return seen.join(" "); +} + +function atLimit(start: number, end: number): string { + // A counter of integer locals only, up to 2^53 - 1: `i + 2` may be + // 2^53 + 1, whose double is 2^53, so `i + 1 === i + 2` holds at the end. + let same = 0; + let above = 0; + let mixed = 0; + for (let i = start; i <= end; i++) { + if (i + 1 === i + 2) same++; + if (i + 2 > 9007199254740992) above++; + mixed += (i * 2) % 3; + } + return `${same} ${above} ${mixed}`; +} + +console.log(heapSort(new Int32Array([5, -3, 9, 0, 9, 2147483647, -2147483648, 7, 1]))); +console.log(heapSort(new Int32Array([])), heapSort(new Int32Array([4]))); +console.log(neighbors(new Float64Array([1, 2, 3, 4]))); +const sorted = new Int32Array([-5, -1, 0, 0, 3, 8, 8, 13]); +console.log([-6, -1, 0, 1, 8, 13, 14].map((v) => lowerBound(sorted, v)).join()); +console.log(nearLimit(9007199254740988)); +console.log(nearLimit(-9007199254740991)); +console.log(atLimit(9007199254740988, 9007199254740991)); +console.log(atLimit(-9007199254740991, -9007199254740988)); diff --git a/tests/corpus/7983-integer-compares.ts b/tests/corpus/7983-integer-compares.ts new file mode 100644 index 000000000..2603a39a1 --- /dev/null +++ b/tests/corpus/7983-integer-compares.ts @@ -0,0 +1,166 @@ +// Compares of integer values that are not doubles in the generated code: +// typed-array elements and char codes, which read as NaN past the end, and +// sums such as `2 * i + 1` whose double is the integer rounded once, near and +// past 2^53. + +function countPairs(a: Int32Array): string { + // `j` runs one past the end, where `a[j]` is NaN and every order compare + // is false. + let lt = 0; + let ge = 0; + let eq = 0; + let ne = 0; + for (let i = 0; i < a.length; i++) { + for (let j = 0; j <= a.length; j++) { + if (a[i]! < a[j]!) lt++; + if (a[i]! >= a[j]!) ge++; + if (a[i]! === a[j]!) eq++; + if (a[i]! !== a[j]!) ne++; + } + } + return `${lt} ${ge} ${eq} ${ne}`; +} + +function pastTheEnd(a: Int32Array, b: Uint8Array, c: Uint32Array): string { + const n = a.length; + const out: boolean[] = []; + out.push(a[n]! < a[0]!, a[n]! > a[0]!, a[n]! <= a[n]!, a[n]! === a[0]!, a[0]! !== a[n]!); + out.push(a[n]! < 0, a[n]! >= -1, a[-1]! < a[0]!, b[n]! <= b[0]!, b[0]! !== b[n]!); + out.push(c[n]! < c[0]!, c[n]! >= c[n]!, c[0]! === c[0]!, c[n]! !== c[0]!); + out.push(a[0]! < b[n]!, b[0]! >= a[n]!, a[0]! < c[n]!); + return out.map((v) => (v ? 1 : 0)).join(""); +} + +function unsigned(c: Uint32Array): string { + // Elements above 2^31 compare as unsigned values. + let below = 0; + let above = 0; + let same = 0; + for (let i = 0; i < c.length; i++) { + for (let j = 0; j < c.length; j++) { + if (c[i]! < c[j]!) below++; + if (c[i]! > c[j]!) above++; + if (c[i]! === c[j]!) same++; + } + } + return `${below} ${above} ${same}`; +} + +function mixed(a: Int32Array, b: Uint8Array, c: Uint32Array, d: Int16Array): string { + let count = 0; + for (let i = 0; i < a.length; i++) { + if (a[i]! < b[i]!) count += 1; + if (a[i]! < c[i]!) count += 10; + if (c[i]! > d[i]!) count += 100; + if (b[i]! >= d[i]!) count += 1000; + if (a[i]! === c[i]!) count += 10000; + } + return String(count); +} + +function charCodes(s: string): string { + // `s.charCodeAt(s.length)` is NaN. + let rising = 0; + let falls = 0; + let equal = 0; + let differ = 0; + for (let i = 0; i < s.length; i++) { + if (s.charCodeAt(i) < s.charCodeAt(i + 1)) rising++; + if (s.charCodeAt(i) >= s.charCodeAt(i + 1)) falls++; + if (s.charCodeAt(i) === s.charCodeAt(i + 1)) equal++; + if (s.charCodeAt(i) !== s.charCodeAt(i + 1)) differ++; + } + return `${rising} ${falls} ${equal} ${differ}`; +} + +function heapChildren(start: number, end: number, steps: number): string { + // `2 * i + 1` passes 2^53 for `i` at or above 2^52, where its double + // rounds; `end` stays a safe integer. + const out: string[] = []; + for (let i = start; i < start + steps; i++) { + const child = 2 * i + 1; + out.push( + [2 * i + 1 < end, 2 * i + 1 <= end, 2 * i + 1 > end, 2 * i + 1 === end, 2 * i + 1 !== end] + .map((v) => (v ? 1 : 0)) + .join("") + ` ${child}`, + ); + } + return out.join(" "); +} + +function countNear(start: number, end: number, steps: number): string { + // The same compares in a loop over integer locals only, stopping early if + // `i` reaches `end`. + let lt = 0; + let le = 0; + let gt = 0; + let eq = 0; + let ne = 0; + for (let i = start; i < start + steps && i !== end; i++) { + if (2 * i + 1 < end) lt++; + if (2 * i + 1 <= end) le++; + if (2 * i + 1 > end) gt++; + if (2 * i + 1 === end) eq++; + if (2 * i + 1 !== end) ne++; + } + return `${lt} ${le} ${gt} ${eq} ${ne}`; +} + +function descend(root: number, end: number): number { + // A sift-down's index walk: doubles `root` until `2 * root + 1` reaches `end`. + let steps = 0; + while (2 * root + 1 < end) { + root = 2 * root + 1; + steps++; + } + return steps * 1000 + (root % 1000); +} + +function scaled(i: number, end: number): string { + // Products by a power of two never round: 4 * i and 1024 * i compare exactly. + return [4 * i < end, 4 * i === end, 1024 * i - 1 < end, 3 * i + 1 < end, 2 * i - 1 > -end] + .map((v) => (v ? 1 : 0)) + .join(""); +} + +function roundedProducts(): string { + // 3 * (2^52 - 1) is 13510798882111485, which rounds to 13510798882111484 = + // 4 * 3377699720527871: the products' doubles are equal and the integers + // are not. A product by 4 never rounds. + let equal = 0; + let below = 0; + for (let i = 1; i <= 8; i++) { + for (let j = 3377699720527869; j <= 3377699720527873; j++) { + if (i * 4503599627370495 === j * 4) equal++; + if (i * 4503599627370495 < j * 4) below++; + } + } + return `${equal} ${below}`; +} + +const a = new Int32Array([5, -3, 2147483647, -2147483648, 5, 0]); +const b = new Uint8Array([0, 255, 7, 128, 5, 0]); +const c = new Uint32Array([4294967295, 2147483648, 2147483647, 0, 1, 4294967295]); +const d = new Int16Array([-32768, 32767, -1, 1, 5, 0]); +console.log(countPairs(a)); +console.log(countPairs(new Int32Array(0))); +console.log(pastTheEnd(a, b, c)); +console.log(unsigned(c)); +console.log(mixed(a, b, c, d)); +console.log(charCodes("abcddcbaÿ￿\u0000")); +console.log(charCodes("")); +const p52 = 2 ** 52; +const safe = Number.MAX_SAFE_INTEGER; +console.log(heapChildren(p52 - 2, safe, 5)); +console.log(heapChildren(p52 - 2, safe - 1, 5)); +console.log(heapChildren(p52 + 100, safe, 3)); +console.log(heapChildren(-p52 - 2, -safe, 5)); +console.log(heapChildren(0, 4, 4)); +console.log(countNear(p52 - 3, safe, 6), countNear(p52 - 3, safe - 2, 6), countNear(p52, safe, 4)); +console.log(countNear(-p52 - 3, -safe, 6), countNear(-p52 + 1, -safe + 4, 4), countNear(0, 7, 6)); +console.log(descend(0, safe), descend(1, safe), descend(p52 - 1, safe), descend(p52, safe)); +console.log(descend(3, 100), descend(0, 1), descend(5, 0)); +for (const i of [0, 1, p52 / 2 - 1, p52 / 2, p52 / 1024, p52 / 2 + 1, p52, -p52]) { + console.log(scaled(i, safe), scaled(i, safe - 1), scaled(i, 2 ** 51), scaled(i, 4)); +} +console.log(roundedProducts()); diff --git a/tests/corpus/7984-untyped-array-methods.mjs b/tests/corpus/7984-untyped-array-methods.mjs new file mode 100644 index 000000000..a68ec305e --- /dev/null +++ b/tests/corpus/7984-untyped-array-methods.mjs @@ -0,0 +1,120 @@ +// Untyped calls of push, pop, shift, at, includes and indexOf: plain arrays +// take a direct path; arrays with holes, frozen, sealed or non-extensible +// arrays, Array.prototype, proxies, and arrays whose own property or +// prototype defines the method take the full dispatch. Each call is made +// both for its result and as a statement, whose result is dropped. +function attempt(label, f) { + try { + console.log(label, f()); + } catch (e) { + console.log(label, "threw", e.constructor.name, e.message); + } +} +function push(a, v) { + return a.push(v); +} +function pushOnly(a, v) { + a.push(v); + return a.length; +} +function push2(a, v, w) { + return a.push(v, w); +} +function pop(a) { + return a.pop(); +} +function shift(a) { + return a.shift(); +} +function at(a, i) { + return a.at(i); +} +function includes(a, v) { + return a.includes(v); +} +function indexOf(a, v) { + return a.indexOf(v); +} +function indexFrom(a, v, from) { + return a.indexOf(v, from); +} +function len(a) { + return a.length; +} + +const holes = JSON.parse("[1, 2, 3]"); +delete holes[1]; +attempt("holes push", () => push(holes, 4)); +attempt("holes push statement", () => pushOnly(holes, 5)); +attempt("holes includes", () => [includes(holes, undefined), indexOf(holes, undefined), at(holes, 1)]); +attempt("holes pop", () => [pop(holes), len(holes)]); +attempt("holes shift", () => [shift(holes), len(holes), 1 in holes]); + +const frozen = Object.freeze(JSON.parse("[1, 2]")); +attempt("frozen push", () => push(frozen, 3)); +attempt("frozen push statement", () => pushOnly(frozen, 3)); +attempt("frozen pop", () => pop(frozen)); +attempt("frozen shift", () => shift(frozen)); +attempt("frozen includes", () => [includes(frozen, 2), indexOf(frozen, 2), at(frozen, -1)]); + +const sealed = Object.seal(JSON.parse("[1, 2]")); +attempt("sealed push", () => push(sealed, 3)); +attempt("sealed push statement", () => pushOnly(sealed, 3)); +attempt("sealed pop", () => pop(sealed)); +attempt("sealed shift", () => [shift(sealed), JSON.stringify(sealed)]); +attempt("sealed includes", () => [includes(sealed, 2), at(sealed, 0)]); + +const fixed = Object.preventExtensions(JSON.parse("[1, 2]")); +attempt("fixed push", () => push(fixed, 3)); +attempt("fixed push statement", () => pushOnly(fixed, 3)); +attempt("fixed pop", () => [pop(fixed), len(fixed)]); +attempt("fixed shift", () => [shift(fixed), len(fixed)]); + +attempt("prototype includes", () => [includes(Array.prototype, 1), indexOf(Array.prototype, 1), at(Array.prototype, 0)]); + +const handler = new Proxy({ push(v) { return "proxied push " + v; }, at(i) { return "proxied at " + i; } }, {}); +attempt("proxy push", () => push(handler, 7)); +attempt("proxy push statement", () => pushOnly(handler, 7)); +attempt("proxy at", () => at(handler, 1)); + +const own = JSON.parse("[1, 2]"); +own.push = function (v) { + return "own push " + v; +}; +own.includes = function (v) { + return "own includes " + v; +}; +attempt("own push", () => [push(own, 9), len(own)]); +attempt("own push statement", () => pushOnly(own, 9)); +attempt("own includes", () => [includes(own, 1), indexOf(own, 1)]); +const notCallable = JSON.parse("[1, 2]"); +notCallable.pop = 5; +attempt("own non-callable pop", () => pop(notCallable)); + +const customProto = Object.create(Array.prototype); +customProto.push = function (v) { + return "proto push " + v + " onto " + this.length; +}; +const reproto = JSON.parse("[1, 2]"); +Object.setPrototypeOf(reproto, customProto); +attempt("reproto push", () => push(reproto, 9)); +attempt("reproto push statement", () => pushOnly(reproto, 9)); +attempt("reproto pop", () => [pop(reproto), len(reproto)]); + +const plain = JSON.parse("[]"); +for (let i = 0; i < 5; i++) push2(plain, i, i * 2); +for (let i = 0; i < 3; i++) pushOnly(plain, "s" + i); +attempt("plain", () => [len(plain), pop(plain), shift(plain), at(plain, -2), at(plain, 1.9), at(plain, -0), at(plain, NaN), at(plain, 100), at(plain, -100)]); +attempt("plain search", () => [includes(plain, 8), indexOf(plain, 8), includes(plain, "s1"), indexOf(plain, "s1"), indexOf(plain, "s9"), indexFrom(plain, 2, 3), includes(plain)]); +attempt("plain at string", () => [at(plain, "1"), at(plain, undefined), at(plain, { valueOf() { return 2; } })]); +const nans = JSON.parse("[1]"); +pushOnly(nans, NaN); +pushOnly(nans, -0); +attempt("nan", () => [includes(nans, NaN), indexOf(nans, NaN), includes(nans, 0), indexOf(nans, 0), indexOf(nans, -0)]); +const empty = JSON.parse("[]"); +attempt("empty", () => [pop(empty), shift(empty), at(empty, 0), includes(empty, undefined), indexOf(empty, undefined), len(empty)]); +const objects = JSON.parse("[]"); +const key = { k: 1 }; +pushOnly(objects, key); +pushOnly(objects, { k: 1 }); +attempt("objects", () => [includes(objects, key), indexOf(objects, key), indexOf(objects, { k: 1 }), at(objects, 0) === key]); diff --git a/tests/corpus/7985-untyped-collection-types.mjs b/tests/corpus/7985-untyped-collection-types.mjs new file mode 100644 index 000000000..de4e581c2 --- /dev/null +++ b/tests/corpus/7985-untyped-collection-types.mjs @@ -0,0 +1,125 @@ +// Untyped Maps, Sets and arrays whose uses prove their key, value and +// element types compile as typed collections; any other use keeps them +// untyped. Both must answer as Node does, including SameValueZero keys, +// lookups of another type, and every use that refuses the typing. + +// Typed: counters, membership sets and stacks used only through their +// methods. +const counts = new Map(); +const seen = new Set(); +const stack = []; +const words = []; +for (let i = 0; i < 40; i++) { + const key = "k" + (i % 7); + counts.set(key, (counts.get(key) ?? 0) + 1); + seen.add(i % 5); + stack.push(i * 2); + if (stack.length > 6) stack.shift(); + words.push(key); +} +console.log(counts.size, counts.get("k3"), counts.get("missing"), counts.has("k6"), counts.delete("k0"), counts.size); +console.log(seen.size, seen.has(4), seen.has(5), seen.delete(4), seen.has(4)); +console.log(stack.length, stack.at(-1), stack.at(0), stack.pop(), stack.includes(70), stack.indexOf(72), words.join("|").length); + +// SameValueZero keys and NaN elements on typed collections. +const byNumber = new Map(); +byNumber.set(NaN, "nan"); +byNumber.set(-0, "zero"); +byNumber.set(1.5, "x"); +console.log(byNumber.get(NaN), byNumber.get(0), byNumber.get(-0), byNumber.size, byNumber.has(1.5)); +const nums = []; +nums.push(NaN); +nums.push(-0); +console.log(nums.includes(NaN), nums.indexOf(NaN), nums.includes(0), nums.indexOf(0)); +const flags = new Set(); +flags.add(true); +console.log(flags.has(true), flags.has(false), flags.size); + +// Captured by a closure that only uses it. +const cache = new Map(); +const latest = () => cache.get("c") ?? -1; +console.log(latest()); +for (let i = 0; i < 3; i++) cache.set("c", i * 10); +console.log(latest(), cache.size); + +// Lookups of another type: the typing refuses, and the answers stay Node's. +const strings = new Map(); +strings.set("1", "one"); +console.log(strings.get(1), strings.get("1"), strings.has(1)); +const numbers = []; +numbers.push(1); +console.log(numbers.includes("1"), numbers.indexOf("1"), numbers.at("0")); + +// Mixed key, value and element types. +const mixedKeys = new Map(); +mixedKeys.set(1, "number"); +mixedKeys.set("1", "string"); +console.log(mixedKeys.size, mixedKeys.get(1), mixedKeys.get("1")); +const mixedValues = new Map(); +mixedValues.set("a", 1); +mixedValues.set("b", "two"); +console.log(mixedValues.get("a"), mixedValues.get("b")); +const mixedElements = []; +mixedElements.push(1); +mixedElements.push("two"); +console.log(mixedElements.join(","), mixedElements.length); + +// Escapes and other uses that keep the collection untyped. +const passed = new Map(); +passed.set("x", 1); +function sizeOf(m) { + return m.size; +} +console.log(sizeOf(passed)); +function make() { + const made = new Set(); + made.add("y"); + return made; +} +console.log(make().has("y")); +const stored = []; +stored.push(3); +const holder = { list: stored }; +holder.list.push(4); +console.log(stored.length, holder.list.join("+")); +const iterated = new Map(); +iterated.set("a", 1); +iterated.set("b", 2); +for (const [k, v] of iterated) console.log("entry", k, v); +const eachSet = new Set(); +eachSet.add(3); +eachSet.forEach((v) => console.log("member", v)); +const shown = new Map(); +shown.set("s", 1); +console.log(shown); +const chained = new Map(); +chained.set("a", 1).set("b", 2); +console.log(chained.size); +const optional = new Map(); +optional.set("o", 1); +console.log(optional?.get("o"), optional.get?.("o")); +const indexed = []; +indexed.push(1); +indexed[3] = 2; +console.log(indexed.length, indexed.join(",")); +const truncated = []; +truncated.push(1, 2, 3); +truncated.length = 1; +console.log(truncated.length, truncated.at(0)); +const spread = []; +spread.push(5); +console.log(Math.max(...spread)); +const unread = new Map(); +console.log(unread.size, unread.get("none")); +export const exported = new Map(); +exported.set("e", 1); +console.log(exported.get("e")); +// Exported through an export list, under its own name or another: an +// importer sees an untyped Map. +const listed = new Map(); +listed.set("l", 1); +console.log(listed.get("l")); +const renamed = new Map(); +renamed.set("r", 1); +console.log(renamed.get("r")); +export { listed, renamed as alias }; diff --git a/tests/corpus/7990-inferred-return-overlapping-records.ts b/tests/corpus/7990-inferred-return-overlapping-records.ts new file mode 100644 index 000000000..c2d1c9ea0 --- /dev/null +++ b/tests/corpus/7990-inferred-return-overlapping-records.ts @@ -0,0 +1,59 @@ +// Functions whose inferred return type is a union of overlapping records. +// TypeScript's subtype reduction infers `{ status }` for a function that +// returns `{ status }` in one branch and `{ status, value }` in another, so +// the inferred type is narrower than the values. Returned values keep their +// fields, chained returns keep them, and bindings typed by the reduced type +// read their declared fields. +type Aborted = { status: string }; +const ABORTED: Aborted = { status: "aborted" }; + +function inner(ok: boolean, value: unknown) { + if (!ok) return ABORTED; + return { status: "valid", value }; +} + +// Returns inner's union through a call: inferred as `{ status }` alone. +function outer(ok: boolean, value: unknown) { + if (value === "stop") return ABORTED; + return inner(ok, value); +} + +function merge(entries: [string, number][]) { + const result: Record = {}; + for (const [key, n] of entries) { + if (key === "__proto__") return ABORTED; + result[key] = n; + } + return { status: "valid", value: result }; +} + +function parse(entries: [string, number][]) { + if (entries.length === 0) return ABORTED; + return merge(entries); +} + +console.log(JSON.stringify(outer(true, [1, 2]))); +console.log(JSON.stringify(outer(false, 1))); +console.log(JSON.stringify(outer(true, "stop"))); +console.log(JSON.stringify(parse([["a", 1], ["b", 2]]))); +console.log(JSON.stringify(parse([]))); + +// Into untyped positions: the value keeps every field. +const seen: unknown[] = []; +seen.push(outer(true, { nested: true })); +seen.push(parse([["c", 3]])); +console.log(JSON.stringify(seen)); +const loose: unknown = outer(true, "kept"); +console.log(JSON.stringify(loose)); + +// Bindings typed by the reduced type: declared fields read as in Node. +const r = outer(true, 42); +console.log(r.status); +let status = ""; +for (const ok of [true, false]) status += outer(ok, 0).status + " "; +console.log(status.trim()); +function statusOf(ok: boolean): string { + const local = outer(ok, null); + return local.status; +} +console.log(statusOf(true), statusOf(false)); diff --git a/tests/corpus/7991-unknown-class-union-casts.ts b/tests/corpus/7991-unknown-class-union-casts.ts new file mode 100644 index 000000000..6560abbc2 --- /dev/null +++ b/tests/corpus/7991-unknown-class-union-casts.ts @@ -0,0 +1,70 @@ +// Checked casts from `unknown` to unions of classes: each instance selects +// its own arm, whatever the order of the arms, including subclasses, arrays +// of instances, instances inside records, and values of no listed class. +class Circle { + readonly r: number; + constructor(r: number) { + this.r = r; + } + area(): number { + return Math.round(Math.PI * this.r * this.r); + } +} +class Square { + readonly side: number; + constructor(side: number) { + this.side = side; + } + area(): number { + return this.side * this.side; + } +} +class Rect { + readonly w: number; + readonly h: number; + constructor(w: number, h: number) { + this.w = w; + this.h = h; + } + area(): number { + return this.w * this.h; + } +} +class Tile extends Square { + label(): string { + return `tile ${this.side}`; + } +} + +type Shape = Circle | Square | Rect; + +function describe(value: unknown): string { + try { + const shape = value as Shape; + const kind = shape instanceof Circle ? "circle" : shape instanceof Rect ? "rect" : "square"; + return `${kind} ${shape.area()}`; + } catch (error) { + return `rejected: ${(error as Error).name}`; + } +} + +const values: unknown[] = [new Circle(2), new Square(3), new Rect(2, 5), new Tile(4), { side: 3 }, 7, null]; +for (const value of values) console.log(describe(value)); + +const reversed = values.slice(0, 4).reverse() as Shape[]; +console.log(reversed.map((s) => s.area()).join(",")); + +const boxed: unknown = { first: new Rect(1, 2), second: new Circle(1) }; +const pair = boxed as { first: Shape; second: Shape }; +console.log(pair.first.area(), pair.second.area(), pair.first instanceof Rect); + +const tiles: unknown = [new Tile(1), new Tile(2)]; +const asSquares = tiles as Square[]; +console.log(asSquares.map((t) => t.area()).join(" "), asSquares[0] instanceof Tile); + +let total = 0; +for (let i = 0; i < 1000; i++) { + const v: unknown = i % 3 === 0 ? new Circle(1) : i % 3 === 1 ? new Square(2) : new Rect(1, 3); + total += (v as Shape).area(); +} +console.log(total); diff --git a/tests/corpus/7993-json-parse-error-messages.ts b/tests/corpus/7993-json-parse-error-messages.ts new file mode 100644 index 000000000..76605e501 --- /dev/null +++ b/tests/corpus/7993-json-parse-error-messages.ts @@ -0,0 +1,30 @@ +// JSON.parse syntax error messages, exactly as Node's V8 writes them: the +// expected token at the end of the input inside objects and arrays, the +// position as a UTF-16 offset with its line and column (\n, \r and \r\n +// end lines), the unexpected character named with the source (whole when +// short, ten units of context on each side when longer), and the special +// sources that name only themselves. +const inputs: string[] = [ + "", " ", "[", "[{", "{", '{"a"', '{"a":', '{"a":1', '{"a":1,', '{"a":1,}', "{,", "{1:2}", "{'a':1}", + '{"a" 1}', '{"a":1 "b":2}', "[1", "[1,", "[1,]", "[1 2]", "[,1]", '"abc', '"a\\', '"\\u12', '"\\u12x4"', + '"\\u12"', '"\\x"', "tru", "nul", "fals", "t1", 'n"', "-", "-x", "1.", "1.e", "1e", "1e+", "01", "-01", + "[01]", "+1", "1 2", "{} x", "[]]", "\u0001", '"\u0001"', '{"a":[1,{"b":', '[{"a":1},', '{"a":{"b":{}', + "[[[", " [ \n { \n", "\n\n{", "\r\n\r\n{", "\r\r{", '{"a":tru}', "NaN", "Infinity", "undefined", + "[object Object]", " NaN", "-Infinity", "[-]", + // Positions count UTF-16 units: non-ASCII text before the error. + '{"é":1 x}', '["😀" 1]', '{"😀😀":', '"é\u0001"', "é", + // Long sources: the unexpected character with its context. + '{"name":"some long value","other":x}', '[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,]', + 'x{"name":"some long value","other":1}', '{"name":"some long value","other":1}x', '{"a":"0123456789abcdef"x}', + '["😀😀😀😀😀😀", x, "😀😀😀😀😀😀"]', '["😀😀😀😀😀😀",x]', '{"k":[true,false,null,"s",1.5e3,-2] ]', + '\n\n {\n "a": [1, 2,\n 3 4]\n }', +]; +for (const text of inputs) { + try { + JSON.parse(text); + console.log(JSON.stringify(text), "parsed"); + } catch (e) { + const err = e as Error; + console.log(JSON.stringify(text), err.name, JSON.stringify(err.message)); + } +} diff --git a/tests/corpus/8000-optional-element-arrays-share-storage.ts b/tests/corpus/8000-optional-element-arrays-share-storage.ts new file mode 100644 index 000000000..fa31fcad2 --- /dev/null +++ b/tests/corpus/8000-optional-element-arrays-share-storage.ts @@ -0,0 +1,61 @@ +// A number[] or string[] seen as `(T | undefined)[]` is the same array, as +// in Node: undefined written through either name is visible through both, +// whether the wider view is a cast, a binding or a parameter. + +const c: number[] = [1.5, 2]; +(c as (number | undefined)[])[1] = undefined; +console.log(c); +const e: number[] = [1.5, 2]; +const f = e as (number | undefined)[]; +f[1] = undefined; +console.log(e, f, e === f); + +function clearFirst(values: (number | undefined)[]): void { + values[0] = undefined; + values.push(undefined, 4); +} +const a: number[] = [1, 2, 3]; +clearFirst(a); +console.log(a, a.length, a.indexOf(4), a.includes(undefined as unknown as number)); + +const names: string[] = ["x"]; +const wider: (string | undefined)[] = names; +wider.push(undefined); +wider.unshift(undefined); +console.log(names, wider === names, names.length, JSON.stringify(names)); + +// The wider arrays themselves: literals, pushes, splices and copies keep +// their undefined elements. +const lit: (number | undefined)[] = [1, undefined, 3]; +const maybe = (i: number): number | undefined => (i % 2 ? i : undefined); +const built: (number | undefined)[] = []; +for (let i = 0; i < 4; i++) built.push(maybe(i)); +built.splice(1, 0, undefined); +console.log(lit, built, lit.indexOf(undefined), built.filter((x): boolean => x === undefined).length); +console.log(built.map((x) => x ?? -1), built.find((x) => x !== undefined), [...built].reverse()); +let sum = 0; +for (const x of built) sum += x ?? 0; +console.log(sum, built.join("-"), JSON.stringify(built)); + +const flags: (boolean | undefined)[] = [true, undefined, false]; +const strs: (string | undefined)[] = ["a", undefined]; +console.log(flags, flags.filter(Boolean).length, strs.map((s) => s?.toUpperCase())); + +// Values from other containers keep their undefined. +const m = new Map([["a", undefined], ["b", 2]]); +const fromMap = [...m.values()]; +const fromSet = Array.from(new Set(["s", undefined])); +const sized = Array.from({ length: 2 }); +console.log(fromMap, fromSet, sized, sized.length); +const dict: { [key: string]: number | undefined } = { a: 1, b: undefined }; +console.log(Object.values(dict)); + +// An asserted element that is undefined throws a catchable TypeError. +function lengthAt(items: (string | undefined)[], i: number): number { + try { + return items[i]!.length; + } catch { + return -1; + } +} +console.log(lengthAt(strs, 0), lengthAt(strs, 1)); diff --git a/tests/corpus/8001-optional-element-writers.ts b/tests/corpus/8001-optional-element-writers.ts new file mode 100644 index 000000000..5dff693c3 --- /dev/null +++ b/tests/corpus/8001-optional-element-writers.ts @@ -0,0 +1,103 @@ +// Every way a value reaches a `(T | undefined)[]` of numbers, strings or +// booleans keeps its undefined: callbacks, generators, Array.of, rest +// packs, destructuring, class fields and copies into Sets and Maps. + +const slots = new Map([["a", 1]]); +const looked = ["x", "a", "y"].map((key) => slots.get(key)); +console.log(looked[0] === undefined, looked, looked.length); +const fromMapped = Array.from(["a", "b"], (key) => slots.get(key)); +console.log(fromMapped[1] === undefined, fromMapped); +const objects: { n?: number }[] = [{ n: 1 }, {}]; +const fields = objects.map((o) => o.n); +function pick(o: { n?: number }): number | undefined { + return o.n; +} +console.log(fields[1] === undefined, objects.map(pick)[1] === undefined, fields); +function wrap(value: T): T[] { + return [value]; +} +console.log(wrap(undefined)[0] === undefined); + +function* maybe(): Generator { + yield undefined; + yield 2; +} +const drained = [...maybe()]; +console.log(drained[0] === undefined, drained, Array.from(maybe())); +const yielding = function* (): Generator { + yield "s"; + yield undefined; +}; +console.log([...yielding()]); +console.log(Array.of(undefined, 1), Array.of(true)); + +function sum(...values: (number | undefined)[]): number { + return values.reduce((total, v) => total + (v ?? 0), 0); +} +const spread: (number | undefined)[] = [undefined, 4]; +console.log(sum(1, undefined, 2), sum(...spread), sum(...spread, undefined, 1)); + +const [first, second = 7, third] = [3, undefined, 1] as (number | undefined)[]; +console.log(first, second, third); +const strings: (string | undefined)[] = ["a", undefined, "a"]; +const seen = new Set(strings); +console.log(seen.size, seen.has(undefined), [...seen]); +for (const [i, s] of strings.entries()) console.log(i, s); +for (const key in strings) console.log("key", key); + +class Holder { + values: (number | undefined)[] = []; + add(value: number | undefined): void { + this.values.push(value); + } + first(): number | undefined { + return this.values[0]; + } +} +const holder = new Holder(); +holder.add(undefined); +holder.add(2); +console.log(holder.first(), holder.values, holder.values.length); + +const xs: (number | undefined)[] = [3, undefined, 1]; +xs.forEach((x, i) => console.log(i, x, typeof x)); +console.log(xs.findIndex((x) => x === undefined), xs.findLast((x) => x !== undefined)); +console.log(xs.some((x) => x === undefined), xs.every((x) => x !== undefined)); +console.log(xs.reduce((acc, x) => acc + (x ?? 10), 0), xs.join("/"), xs.at(1), xs.at(-1)); +console.log(xs.toSorted((a, b) => (a ?? 0) - (b ?? 0)), [...xs].sort()); +xs[1] ??= 5; +console.log(xs); +const flags: (boolean | undefined)[] = [undefined, true]; +console.log(flags.map((b) => !b), flags.indexOf(undefined), flags.includes(true), String(flags)); +const nested: (number | undefined)[][] = [[undefined], [1]]; +console.log(nested.flat(), nested[0]![0]); +const byKey = new Map([["k", [undefined, 1]]]); +console.log(byKey.get("k")![0], byKey); + +// Promise.all over `Promise[]` settles on Node's turn, with +// its undefined values. +const lookup = async (key: string): Promise => { + if (key === "x") throw new Error("missing " + key); + return slots.get(key); +}; +const turns: string[] = []; +const pending: Promise[] = [lookup("a"), lookup("b")]; +void Promise.all(pending).then((vs) => turns.push("all " + vs.join(",") + " " + (vs[1] === undefined))); +const failing: Promise[] = [lookup("a"), lookup("x")]; +Promise.all(failing).then( + () => turns.push("unexpected"), + (error: unknown) => turns.push("rejected " + (error as Error).message), +); +void Promise.all([] as Promise[]).then((vs) => turns.push("empty " + vs.length)); +void Promise.resolve() + .then(() => turns.push("t1")) + .then(() => turns.push("t2")) + .then(() => turns.push("t3")) + .then(() => console.log(turns.join(" "))); + +// Index reads through a union of arrays, and searches with a number +// needle, see the present undefined elements. +const pairs: ((null | undefined)[] | (number | undefined)[])[] = [[null, undefined], [undefined, 0]]; +for (const pair of pairs) console.log(pair[0], pair[1], pair[2]); +const sparse: (number | undefined)[] = [1, , undefined, 1]; +console.log(sparse.lastIndexOf(undefined), sparse.lastIndexOf(1, 2), sparse.indexOf(1, 1)); diff --git a/tests/corpus/8002-while-byte-length-companions.ts b/tests/corpus/8002-while-byte-length-companions.ts new file mode 100644 index 000000000..e37ec9e57 --- /dev/null +++ b/tests/corpus/8002-while-byte-length-companions.ts @@ -0,0 +1,201 @@ +// While loops bounded by a typed array or Buffer length, with companion +// counters that only grow: reversed copies with line breaks, steps of two, +// early exits, skipped passes, nested loops and loops inside loops. + +function reverseLines( + line: Uint8Array, + target: Uint8Array, + start: number, +): number { + const len = line.length; + const right = len - 1; + let read = 0; + let written = 0; + let count = 0; + let lines = 0; + while (read < len) { + let c = line[right - read]!; + read++; + if (c === 10) { + if (read === len) break; + c = line[right - read]!; + read++; + } + target[written] = c + 1; + written++; + count++; + if ((count + start - lines) % 5 === 0) { + target[written] = 10; + written++; + count++; + lines++; + } + } + return written * 1000 + lines * 10 + count; +} + +const text = Buffer.from("ABCDEFGHIJ\nKLMNOPQRST\nUVWXYZ\n"); +const out = new Uint8Array(64); +for (const start of [0, 1, 3, 7]) { + const summary = reverseLines(text, out, start); + console.log( + start, + summary, + Buffer.from(out.subarray(0, Math.floor(summary / 1000))).toString("latin1"), + ); +} + +function pairs(values: Uint16Array): number[] { + let i = 0; + let sums = 0; + let odd = 0; + const result: number[] = []; + while (i < values.length) { + i += 2; + sums++; + if ((values[i - 2]! & 1) === 1) odd += 3; + result.push(values[i - 2]! + (values[i - 1] ?? -1)); + } + result.push(i, sums, odd); + return result; +} +console.log(pairs(new Uint16Array([1, 2, 3, 4, 5]))); +console.log(pairs(new Uint16Array(0))); +console.log(pairs(new Uint16Array([65535, 65535]))); + +function firstZero(bytes: Uint8Array): string { + let i = 0; + let seen = 0; + let total = -5; + while (i < bytes.length) { + i++; + if (bytes[i - 1] === 0) break; + seen++; + total += bytes[i - 1]!; + } + return `${i} ${seen} ${total}`; +} +console.log( + firstZero(new Uint8Array([3, 4, 0, 9])), + firstZero(new Uint8Array([1, 2])), + firstZero(new Uint8Array(0)), +); + +function skipping(bytes: Uint8Array): string { + let i = 0; + let kept = 0; + let skipped = 0; + while (i < bytes.length) { + const b = bytes[i]!; + i++; + if (b > 100) { + skipped++; + continue; + } + kept++; + } + return `${i} ${kept} ${skipped}`; +} +console.log(skipping(new Uint8Array([1, 200, 3, 250, 255, 7]))); + +function repeated(bytes: Uint8Array): string { + let i = 0; + let inner = 0; + let outer = 0; + while (i < bytes.length) { + i++; + outer++; + let j = 0; + while (j < bytes[i - 1]!) { + j++; + inner++; + } + } + return `${i} ${outer} ${inner}`; +} +console.log(repeated(new Uint8Array([2, 0, 5, 1]))); + +function grid(rows: Float64Array[]): string { + let total = 0; + let cells = 0; + for (const row of rows) { + let k = 0; + let doubled = 0; + while (k < row.length) { + k++; + cells++; + doubled += 2; + total += row[k - 1]! * doubled; + } + total += k * 0.5; + } + return `${cells} ${total}`; +} +console.log( + grid([ + new Float64Array([0.5, 1.5]), + new Float64Array(0), + new Float64Array([-2, 3, 4.25]), + ]), +); + +function views(buffer: ArrayBuffer): string { + const view = new DataView(buffer, 2); + let offset = 0; + let words = 0; + const parts: string[] = []; + while (offset < view.byteLength) { + offset += 2; + words++; + if (offset <= view.byteLength) + parts.push(view.getUint16(offset - 2, true).toString(16)); + } + return `${offset} ${words} ${parts.join(",")}`; +} +const backing = new Uint8Array([9, 9, 1, 2, 3, 4, 5]).buffer; +console.log(views(backing)); + +function captured(bytes: Uint8Array): number { + let i = 0; + let n = 0; + const read = (): number => n; + while (i < bytes.length) { + i++; + n++; + } + return read() + i; +} +console.log(captured(new Uint8Array(4))); + +function fromNegative(bytes: Int8Array): string { + let i = 0; + let level = -3; + let low = -0; + while (i < bytes.length) { + i++; + level++; + if (bytes[i - 1]! < 0) low++; + } + return `${level} ${low} ${Object.is(low, -0)}`; +} +console.log( + fromNegative(new Int8Array([-1, 2, -3])), + fromNegative(new Int8Array(0)), +); + +function throwing(bytes: Uint8Array): number { + let i = 0; + let n = 0; + while (i < bytes.length) { + i++; + n++; + if (bytes[i - 1] === 7) throw new Error(`stopped at ${i} after ${n}`); + } + return n; +} +try { + console.log(throwing(new Uint8Array([1, 2]))); + console.log(throwing(new Uint8Array([1, 7, 3]))); +} catch (error) { + console.log((error as Error).message); +} diff --git a/tests/corpus/8003-while-reversed-byte-reads.ts b/tests/corpus/8003-while-reversed-byte-reads.ts new file mode 100644 index 000000000..1407b9324 --- /dev/null +++ b/tests/corpus/8003-while-reversed-byte-reads.ts @@ -0,0 +1,183 @@ +// While loops that read a typed array or Buffer backwards from its length +// (`line[len - 1 - i]`, `line[right - i]`), with increments, guards that +// exit at the length, calls in the body, reads at or past either end, and +// counters advanced by nested loops. + +function complement(code: number): number { + switch (code) { + case 65: + return 84; + case 67: + return 71; + case 71: + return 67; + case 84: + return 65; + } + return 63; +} + +function reverseComplement(line: Uint8Array, target: Uint8Array): string { + const len = line.length; + const right = len - 1; + let ileft = 0; + let iright = 0; + let c: number; + while (iright < len) { + c = line[right - iright]!; + iright++; + if (c === 10) { + if (iright === len) break; + c = line[right - iright]!; + iright++; + } + target[ileft] = complement(c); + ileft++; + } + return Buffer.from(target.subarray(0, ileft)).toString("latin1"); +} + +const out = new Uint8Array(32); +console.log(reverseComplement(Buffer.from("ACGT\nTTGA\n"), out)); +console.log(reverseComplement(Buffer.from("\nGATTACA"), out)); +console.log(reverseComplement(Buffer.from("\n"), out)); +console.log(reverseComplement(Buffer.from("A\n\nC"), out)); +console.log(JSON.stringify(reverseComplement(new Uint8Array(0), out))); + +// Reads one past the end: the first pass reads line[len], which is undefined. +function inclusive(line: Uint8Array): string { + const len = line.length; + let i = 0; + const seen: (number | undefined)[] = []; + while (i <= len) { + seen.push(line[len - i]); + i++; + } + return JSON.stringify(seen.map((v) => (v === undefined ? "u" : v))); +} +console.log(inclusive(new Uint8Array([1, 2, 3])), inclusive(new Uint8Array(0))); + +// A read after an increment with no guard reads line[-1] on the last pass. +function unguarded(line: Uint8Array): string { + const right = line.length - 1; + let i = 0; + let total = 0; + let missing = 0; + while (i < line.length) { + total += line[right - i]!; + i++; + const next = line[right - i]; + if (next === undefined) missing++; + else total += next * 100; + } + return `${total} ${missing}`; +} +console.log(unguarded(new Uint8Array([1, 2, 3])), unguarded(new Uint8Array([9]))); + +// Guards in other forms: >=, !==, and a guard in an else branch. +function guards(line: Uint16Array): number[] { + const len = line.length; + const result: number[] = []; + let i = 0; + while (i < len) { + i += 1; + if (i >= len) { + result.push(-1); + break; + } + result.push(line[len - 1 - i]!); + } + let j = 0; + while (len > j) { + j++; + if (j !== len) result.push(line[len - 1 - j]! * 10); + else result.push(-2); + } + let k = 0; + while (k < len) { + k++; + if (line[0] === 0) { + result.push(-3); + } else if (k === len) { + break; + } else { + result.push(line[len - k - 1]! * 100); + } + } + return result; +} +console.log(guards(new Uint16Array([5, 6, 7]))); +console.log(guards(new Uint16Array([0, 1]))); +console.log(guards(new Uint16Array(0))); + +// A nested loop advances the counter, so the read after it can fall +// outside the array. +function skipRuns(line: Uint8Array): string { + const len = line.length; + let i = 0; + const parts: string[] = []; + while (i < len) { + const c = line[len - 1 - i]!; + i++; + while (i < len && line[len - 1 - i] === c) i++; + const after = line[len - 1 - i]; + parts.push(`${c}:${after === undefined ? "end" : after}`); + } + return parts.join(" "); +} +console.log(skipRuns(Buffer.from("aabbbc")), skipRuns(Buffer.from("zz"))); + +// A condition with <= against the last index, a labeled block, and +// counters that start past zero or past the end. +function labeled(line: Int8Array, start: number): number[] { + const len = line.length; + const right = len - 1; + const result: number[] = []; + let i = start; + while (i <= right) { + found: { + if (line[len - 1 - i]! < 0) break found; + result.push(line[len - 1 - i]!); + } + i += 2; + } + result.push(i); + return result; +} +console.log(labeled(new Int8Array([1, -2, 3, -4, 5]), 0)); +console.log(labeled(new Int8Array([1, -2, 3, -4, 5]), 1)); +console.log(labeled(new Int8Array([1, -2, 3]), 5)); + +// An exception thrown from a call in the body. +function throwing(line: Uint8Array): string { + const len = line.length; + let i = 0; + let total = 0; + try { + while (i < len) { + const c = line[len - 1 - i]!; + i++; + if (c === 0) throw new Error(`zero after ${total}`); + total += c; + } + } catch (error) { + return (error as Error).message; + } + return String(total); +} +console.log(throwing(new Uint8Array([4, 0, 2, 3])), throwing(new Uint8Array([1, 2]))); + +// An increment inside an if condition's comma expression: both reads are +// at index -1 on the last pass. +function commaIncrement(line: Uint8Array): string { + const len = line.length; + let i = 0; + const out: (number | undefined)[] = []; + while (i < len) { + if ((i++, i > 0)) out.push(line[len - 1 - i]); + const v = line[len - 1 - i]!; + out.push(v); + } + return JSON.stringify(out); +} +console.log(commaIncrement(new Uint8Array([1, 2, 3])), commaIncrement(new Uint8Array(0))); diff --git a/tests/corpus/8004-optional-element-conversions.ts b/tests/corpus/8004-optional-element-conversions.ts new file mode 100644 index 000000000..a592a4f32 --- /dev/null +++ b/tests/corpus/8004-optional-element-conversions.ts @@ -0,0 +1,82 @@ +// `(T | undefined)[]` values built by checked conversions, rest parameters +// and union index reads: nested arrays converted beside their non-optional +// form, a union arm that is an optional-element array, undefined arguments +// and tuple spreads packed into a rest parameter, and an index expression +// that reassigns the receiver. + +const plainRows = JSON.parse("[[1, 2], [3]]") as number[][]; +console.log(plainRows); +const optionalSource: unknown = (JSON.parse("[[1, null], [null, 4]]") as unknown[][]).map((row) => + row.map((v) => (v === null ? undefined : v)), +); +const optionalRows = optionalSource as (number | undefined)[][]; +console.log(optionalRows, optionalRows[0]![1], optionalRows[1]!.length); + +const armSource: unknown = [1, undefined, 3]; +const arm = armSource as (number | undefined)[] | string; +console.log(typeof arm === "string" ? arm : arm.length, arm); +const stringArm = ["a", undefined] as unknown as (string | undefined)[] | number; +console.log(stringArm); +const textArm = "text" as unknown as (string | undefined)[] | string; +console.log(textArm); + +function flags(...values: (boolean | undefined)[]): string { + let text = ""; + for (const value of values) text += value === undefined ? "u" : value ? "t" : "f"; + return `${values.length}:${text}:${String(values[1])}`; +} +console.log(flags(true, undefined)); +const flagPair: [boolean, boolean | undefined] = [false, undefined]; +console.log(flags(...flagPair), flags(true, ...flagPair), flags(...flagPair, true)); +function label(first: string, ...rest: (string | undefined)[]): string { + return `${first}/${rest.length}/${rest.map((v) => v ?? "-").join("")}`; +} +const words: [string, string | undefined, string | undefined] = ["a", undefined, "c"]; +console.log(label(...words), label("b", undefined)); +// Spread tuple fields are read before later arguments run. +const changing: [boolean, boolean | undefined] = [true, undefined]; +function flip(pair: [boolean, boolean | undefined], second: boolean | undefined): boolean { + pair[0] = !pair[0]; + pair[1] = second; + return pair[0]; +} +console.log(flags(...changing, flip(changing, true))); +console.log(flags(...changing, flip(changing, undefined), ...changing)); +const swapped: [string, string | undefined] = ["x", "y"]; +function swap(pair: [string, string | undefined]): string { + pair[0] = "w"; + pair[1] = undefined; + return "v"; +} +console.log(label("z", ...swapped, swap(swapped), ...swapped)); +// Tuple spreads together with array and Set spreads keep undefined +// elements, and every item is read in argument order. +const mixedPair: [boolean, boolean | undefined] = [true, undefined]; +const mixedMore: (boolean | undefined)[] = [false, undefined]; +console.log(flags(...mixedPair, ...mixedMore), flags(...mixedMore, ...mixedPair), flags(true, ...mixedPair, ...mixedMore, undefined)); +const mixedChanging: [boolean, boolean | undefined] = [true, undefined]; +console.log(flags(...mixedChanging, ...mixedMore, flip(mixedChanging, true), ...mixedChanging)); +const mixedWords: [string, string | undefined] = ["a", undefined]; +console.log(label("z", ...["b", undefined, "c"], ...mixedWords), label("y", ...mixedWords, ...new Set(["s", undefined]))); +// A tuple returned by a call keeps its string fields alive until every +// field is packed beside the other spreads. +function makeTuple(n: number): [string, string | undefined, string] { + return ["p" + n.toString(16).repeat(4), n % 2 ? undefined : "q" + n, "r".repeat((n % 7) + 3) + n]; +} +for (let i = 0; i < 3; i++) { + console.log(label("z", ...["m", undefined], ...makeTuple(i), ...mixedWords), label("y", ...makeTuple(i + 10), ...new Set(["s" + i]))); +} + +function pick(start: (number | undefined)[] | string[], other: (number | undefined)[] | string[]): string { + let a = start; + const read = a[((a = other), 0)]; + return `${String(read)} ${String(a[0])}`; +} +console.log(pick([1, undefined, 3], ["x", "y"]), pick([undefined, 2], ["z"])); +function counted(start: (number | undefined)[] | string[]): string { + let b = start; + let i = 1; + const read = b[((b = ["q"]), i--)]; + return `${String(read)} ${b.length} ${i}`; +} +console.log(counted([4, undefined]), counted(["s", "t"])); diff --git a/tests/corpus/8005-buffer-encoding-paths.ts b/tests/corpus/8005-buffer-encoding-paths.ts new file mode 100644 index 000000000..014f22abf --- /dev/null +++ b/tests/corpus/8005-buffer-encoding-paths.ts @@ -0,0 +1,105 @@ +// Buffer latin1, ascii, hex and base64 conversions in both directions: +// ASCII, binary and mixed contents at sizes around eight-byte words and the +// pool threshold, strings with code points past U+00FF, invalid hex and +// base64 text, and the lengths and characters of the strings produced. + +let seed = 12345; +function next(): number { + seed = (seed * 1103515245 + 12345) & 0x7fffffff; + return seed >>> 8; +} + +function fill(size: number, kind: string): Buffer { + const buf = Buffer.alloc(size); + for (let i = 0; i < size; i++) { + const r = next(); + if (kind === "ascii") buf[i] = 32 + (r % 95); + else if (kind === "binary") buf[i] = r & 255; + else if (kind === "high") buf[i] = 128 + (r % 128); + else buf[i] = r % 23 === 0 ? 160 + (r % 96) : 97 + (r % 26); + } + return buf; +} + +function digest(text: string): number { + let h = 0; + for (let i = 0; i < text.length; i++) h = (h * 31 + text.charCodeAt(i)) >>> 0; + return h; +} + +const sizes = [0, 1, 7, 8, 9, 15, 16, 17, 63, 64, 65, 4095, 4096, 4097, 70001]; +for (const kind of ["ascii", "binary", "high", "sparse"]) { + const rows: string[] = []; + for (const size of sizes) { + const buf = fill(size, kind); + const latin1 = buf.toString("latin1"); + const back = Buffer.from(latin1, "latin1"); + const ascii = Buffer.from(latin1, "ascii"); + const binary = Buffer.from(buf.toString("binary"), "binary"); + const hex = buf.toString("hex"); + const b64 = buf.toString("base64"); + const url = buf.toString("base64url"); + rows.push( + [ + size, + latin1.length, + digest(latin1), + back.equals(buf), + ascii.equals(buf), + binary.equals(buf), + Buffer.from(hex, "hex").equals(buf), + Buffer.from(b64, "base64").equals(buf), + Buffer.from(url, "base64url").equals(buf), + hex.length, + b64.length, + Buffer.byteLength(latin1, "latin1"), + size > 2 ? latin1.charCodeAt(size - 2) : -1, + size > 4 ? latin1.slice(1, 4).length : -1, + ].join(","), + ); + } + console.log(kind, rows.join(" ")); +} + +// Strings with code points past U+00FF keep the low byte of each UTF-16 +// unit, and an astral code point gives its two surrogates' low bytes. +const wide = [ + "abcéÿ", + "ĀŁ߿", + "xࠀ€￿y", + "\u{1F600}a\u{10FFFF}", + "é".repeat(9) + "€" + "è".repeat(9), + "aaaaaaaébbbbbbbb中cccccccc\u{1F600}", + "ÀÁÂÃÄÅÆÇࠀÈ", +]; +for (const text of wide) { + console.log( + Buffer.from(text, "latin1").toString("hex"), + Buffer.from(text, "ascii").toString("hex"), + Buffer.byteLength(text, "latin1"), + ); +} +const mixed = "é".repeat(5000) + "中" + "z".repeat(5000) + "ÿ".repeat(3000); +const mixedBytes = Buffer.from(mixed, "latin1"); +console.log(mixedBytes.length, mixedBytes[0], mixedBytes[5000], mixedBytes[5001], mixedBytes[13000]); + +// Hex stops at the first invalid pair and drops an odd last digit. +for (const text of ["0aFf", "0aF", "0a1g22", "zz00", "", "ABCDEF0123456789", "0aİ0b", "00a"]) { + console.log(JSON.stringify(text), Buffer.from(text, "hex").toString("hex"), Buffer.byteLength(text, "hex")); +} +const longHex = "0123456789abcdef".repeat(600) + "0x" + "ff".repeat(10); +console.log(Buffer.from(longHex, "hex").length); + +// base64 skips characters outside both alphabets and stops at padding. +for (const text of ["aGVsbG8=", "aGVsbG8", "aGV sbG8=", "aGV\nsbG8", "-_-_", "+/+/", "aGVsbG8=d29ybGQ=", "a", "ab", "abc", "aébc", "====", "YQ"]) { + console.log(JSON.stringify(text), Buffer.from(text, "base64").toString("hex"), Buffer.from(text, "base64url").toString("hex")); +} + +// Strings produced by hex and base64 behave as ASCII text afterwards. +const produced = fill(5000, "binary"); +const producedHex = produced.toString("hex"); +const joined = producedHex + "é" + produced.toString("base64"); +console.log(joined.length, joined.indexOf("é"), digest(joined.slice(9990, 10020))); +const latinText = fill(9000, "high").toString("latin1"); +const appended = latinText + "€"; +console.log(latinText.length, appended.length, appended.charCodeAt(8999), appended.charCodeAt(9000), appended.slice(8998).length); diff --git a/tests/corpus/8006-buffer-base64-decode.ts b/tests/corpus/8006-buffer-base64-decode.ts new file mode 100644 index 000000000..3d7b20303 --- /dev/null +++ b/tests/corpus/8006-buffer-base64-decode.ts @@ -0,0 +1,66 @@ +// Buffer.from(text, "base64" | "base64url") over complete eight-character +// blocks and the text around them: every length from 0 to 40, an invalid +// character at each position of a block, padding and text after it, both +// alphabets, line breaks every 76 characters, sizes around the pool +// threshold, and characters past U+00FF. + +let seed = 7; +function next(): number { + seed = (seed * 1103515245 + 12345) & 0x7fffffff; + return seed >>> 8; +} +function bytes(size: number): Buffer { + const buf = Buffer.alloc(size); + for (let i = 0; i < size; i++) buf[i] = next() & 255; + return buf; +} + +const rows: string[] = []; +for (let size = 0; size <= 30; size++) { + const buf = bytes(size); + const text = buf.toString("base64"); + const url = buf.toString("base64url"); + rows.push( + `${size}:${text.length}:${Buffer.from(text, "base64").equals(buf)}:${Buffer.from(url, "base64url").equals(buf)}:${Buffer.from(url, "base64").equals(buf)}`, + ); +} +console.log(rows.join(" ")); + +const block = Buffer.from("0123456789abcdefghijklmnopqrstuv").toString("base64"); +for (let at = 0; at < 16; at++) { + for (const bad of [" ", "\n", "*", "=", "é", "中"]) { + const text = block.slice(0, at) + bad + block.slice(at); + console.log(at, JSON.stringify(bad), Buffer.from(text, "base64").toString("hex")); + } +} + +for (const text of [ + "QUJDREVGR0g=", + "QUJDREVGR0", + "QUJDREVGR0g=QUJD", + "QUJD=REVGR0hJ", + "QUJDREVGR0hJSktM", + "QUJDREVGR0hJSktMTQ", + "QUJDREVGR0hJSktMTQ==", + "--__--__", + "++//++//", + "+-/_+-/_+-/_", + "Q U J D R E V G R 0 h J", + "QUJD\tREVG\r\nR0hJ", + "=QUJDREVGR0hJ", + "", +]) { + console.log(JSON.stringify(text), Buffer.from(text, "base64").toString("hex"), Buffer.from(text, "base64url").length); +} + +for (const size of [4095, 4096, 4097, 6144, 6145, 70000]) { + const buf = bytes(size); + const text = buf.toString("base64"); + const wrapped = text.replace(/.{76}/g, "$&\n"); + const decoded = Buffer.from(text, "base64"); + const unwrapped = Buffer.from(wrapped, "base64"); + console.log(size, decoded.length, decoded.equals(buf), unwrapped.equals(buf), Buffer.byteLength(text, "base64")); +} + +const wide = "QUJDREVGR0hJİSktM"; +console.log(Buffer.from(wide, "base64").toString("hex"), Buffer.from("ŁłQUJD", "base64").toString("hex")); diff --git a/tests/corpus/8007-buffer-fill-swap-utf16le.ts b/tests/corpus/8007-buffer-fill-swap-utf16le.ts new file mode 100644 index 000000000..850c5b964 --- /dev/null +++ b/tests/corpus/8007-buffer-fill-swap-utf16le.ts @@ -0,0 +1,110 @@ +// Buffer fill with values of more than one byte, swap16/32/64, and +// toString("utf16le"): pattern lengths that do and do not divide the +// filled range, offsets and ends, patterns that are views of the buffer +// being filled, string encodings, sizes around word and pool boundaries, +// and utf16le text with ASCII runs, other BMP characters, surrogate pairs, +// lone surrogates and an odd last byte. + +function show(buf: Uint8Array): string { + return Buffer.from(buf).toString("hex"); +} + +const fills: string[] = []; +for (const size of [0, 1, 2, 3, 7, 8, 9, 31, 32, 33]) { + const buf = Buffer.alloc(size); + buf.fill("ab"); + fills.push(buf.toString("latin1")); + buf.fill(Buffer.from([1, 2, 3])); + fills.push(show(buf)); +} +console.log(fills.join(" ")); + +const ranged = Buffer.alloc(20); +ranged.fill("xyz", 3, 17); +console.log(ranged.toString("latin1").replace(/\0/g, ".")); +ranged.fill(Buffer.from("0123456789"), 5); +console.log(ranged.toString("latin1").replace(/\0/g, ".")); +ranged.fill("long pattern longer than the range", 2, 6); +console.log(ranged.toString("latin1").replace(/\0/g, ".")); +ranged.fill("é中", 0, 11); +console.log(show(ranged)); +ranged.fill("6869", 0, 7, "hex"); +ranged.fill("aGk=", 10, 15, "base64"); +console.log(show(ranged)); +ranged.fill("éa", 0, 20, "latin1"); +console.log(show(ranged)); +ranged.fill("ab", 0, 9, "utf16le"); +console.log(show(ranged)); + +// Patterns that are views of the buffer being filled. +const self = Buffer.from([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); +self.fill(self.subarray(1, 3)); +console.log(show(self)); +const later = Buffer.from([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); +later.fill(later.subarray(6, 9), 2); +console.log(show(later)); +const whole = Buffer.from([9, 8, 7, 6]); +whole.fill(whole); +console.log(show(whole)); +const typed = new Uint8Array(12); +typed.set([5, 6, 7], 0); +Buffer.from(typed.buffer).fill(typed.subarray(0, 3), 3); +console.log(show(typed)); + +const big = Buffer.alloc(70001); +big.fill("0123456"); +console.log(big.length, big.toString("latin1", 0, 10), big.toString("latin1", 69990), big.toString("latin1", 35000, 35010)); +try { + Buffer.alloc(4).fill(Buffer.alloc(0)); +} catch (error) { + console.log((error as NodeJS.ErrnoException).code); +} +console.log(show(Buffer.alloc(5).fill(""))); + +// swap16/32/64. +for (const size of [0, 8, 16, 24, 64, 1000]) { + const buf = Buffer.alloc(size); + for (let i = 0; i < size; i++) buf[i] = (i * 37 + 11) & 255; + const a = Buffer.from(buf).swap16(); + const b = Buffer.from(buf).swap32(); + const c = Buffer.from(buf).swap64(); + console.log(size, show(a.subarray(0, 8)), show(b.subarray(0, 8)), show(c.subarray(0, 8)), show(c.subarray(c.length - 8)), a.swap16().equals(buf)); +} +for (const [size, width] of [[3, 16], [6, 32], [12, 64]] as const) { + try { + const buf = Buffer.alloc(size); + if (width === 16) buf.swap16(); + else if (width === 32) buf.swap32(); + else buf.swap64(); + } catch (error) { + console.log((error as NodeJS.ErrnoException).code, (error as Error).message); + } +} +const view = Buffer.from([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]).subarray(1, 9); +view.swap32(); +console.log(show(view)); + +// toString("utf16le"). +const texts = [ + "plain ascii text", + "abcéÿĀ߿ࠀ中￿", + "ascii run then 中文 and more ascii after it", + "\u{1F600}\u{10FFFF}a\u{10000}", + "lone \ud800 high and \udc00 low and \ud83d", + "x".repeat(4097) + "é" + "y".repeat(3), +]; +for (const text of texts) { + const bytes = Buffer.from(text, "utf16le"); + const back = bytes.toString("utf16le"); + console.log(back === text, back.length, bytes.length, JSON.stringify(back.slice(0, 24)), back.charCodeAt(back.length - 1)); +} +const odd = Buffer.from([0x61, 0, 0x62, 0, 0x63]); +console.log(JSON.stringify(odd.toString("utf16le")), odd.toString("utf16le").length); +const pairSplit = Buffer.from([0x3d, 0xd8, 0x00, 0xde, 0x3d, 0xd8]); +const split = pairSplit.toString("utf16le"); +console.log(split.length, split.codePointAt(0), split.charCodeAt(2)); +const units = Buffer.alloc(40); +for (let i = 0; i < 40; i++) units[i] = (i * 53 + 7) & 255; +const decoded = units.toString("utf16le"); +console.log(decoded.length, [...decoded].map((c) => c.codePointAt(0)).join(",")); +console.log(units.toString("utf16le", 3, 21).length, units.toString("ucs2", 0, 8) === units.toString("utf16le", 0, 8)); diff --git a/tests/corpus/8008-string-index-paths.ts b/tests/corpus/8008-string-index-paths.ts new file mode 100644 index 000000000..a9a748ad8 --- /dev/null +++ b/tests/corpus/8008-string-index-paths.ts @@ -0,0 +1,102 @@ +// UTF-16 index operations on long non-ASCII strings: slices and substrings +// walking forward and backward, interleaved on two strings, at astral +// characters and their halves, with negative and out-of-range indexes; +// lastIndexOf, startsWith and endsWith with positions; and normalize on +// text already in each form, composed and decomposed text, Hangul, +// compatibility characters and combining marks. + +const unit = "Ünïcödé tëxt with ß and 日本語 and émoji 😀 mixed in, "; +const big = unit.repeat(3000); +const other = "ascii words then 中文 and 🎉🎉 and ü, ".repeat(4000); + +function digest(text: string): number { + let h = 0; + for (let i = 0; i < text.length; i++) h = (h * 31 + text.charCodeAt(i)) >>> 0; + return h; +} + +let forward = 0; +for (let i = 0; i + 30 < big.length; i += 7) { + forward = (forward * 33 + digest(big.slice(i, i + 20)) + digest(big.substring(i + 3, i + 9))) >>> 0; +} +let backward = 0; +for (let i = big.length - 1; i > 30; i -= 11) { + backward = (backward * 33 + digest(big.slice(i - 25, i)) + big.substring(i, i - 5).length) >>> 0; +} +let interleaved = 0; +for (let i = 0; i + 40 < other.length && i + 40 < big.length; i += 13) { + interleaved = (interleaved * 33 + digest(big.slice(i, i + 9)) + digest(other.slice(i + 1, i + 40))) >>> 0; +} +console.log(big.length, forward, backward, interleaved); + +// Indexes at astral characters and their halves. +const emojiAt = big.indexOf("😀", 100000); +console.log( + emojiAt, + JSON.stringify(big.slice(emojiAt, emojiAt + 1)), + JSON.stringify(big.slice(emojiAt + 1, emojiAt + 3)), + JSON.stringify(big.substring(emojiAt - 1, emojiAt + 1)), + big.charCodeAt(emojiAt + 1), + JSON.stringify(big.at(emojiAt + 1)), + JSON.stringify(big.charAt(emojiAt)), +); +console.log( + JSON.stringify(big.slice(-5)), + JSON.stringify(big.slice(-30, -20)), + big.slice(big.length - 3, big.length + 10).length, + JSON.stringify(big.substring(-10, 4)), + big.slice(5, 2).length, + JSON.stringify(big.slice(NaN, 3)), +); +for (const at of [0, 1, 52, 99999, 150000, big.length - 1, big.length, -1, -2, -big.length]) { + console.log(at, JSON.stringify(big.at(at)), JSON.stringify(big.charAt(at)), big.charCodeAt(at)); +} + +// lastIndexOf, startsWith and endsWith with positions. +console.log( + big.lastIndexOf("日本"), + big.lastIndexOf("日本", 100000), + big.lastIndexOf("😀", 50), + big.lastIndexOf("absent"), + big.lastIndexOf("é", 0), + big.lastIndexOf("Ü"), + big.lastIndexOf(""), + big.lastIndexOf("", 10), +); +let starts = 0; +let ends = 0; +for (let i = 0; i < big.length; i += 17) { + if (big.startsWith("tëxt", i)) starts++; + if (big.endsWith("😀", i)) ends++; +} +console.log(starts, ends, big.startsWith("Ü"), big.endsWith("in, "), big.startsWith("ünï"), big.endsWith("in, ", big.length - 1)); + +// normalize. +const samples = [ + "plain ascii", + "précomposé café naïve", + "décomposé café", + "日本語のテキスト 한국어 문장", + "각 각", + "fullwidth カタカナ fi ① ℌ ²", + "ậ ậ ͅ", + "Ω Å Å Ω K", + "が が か゚", + "emoji 😀 and \ud800 lone", + unit, +]; +for (const sample of samples) { + const forms = ["NFC", "NFD", "NFKC", "NFKD"].map((form) => sample.normalize(form)); + console.log( + forms.map((text) => `${text.length}:${digest(text)}`).join(" "), + forms.map((text) => text === sample).join(","), + ); +} +const longNfc = big.normalize(); +const longNfd = big.normalize("NFD"); +console.log(longNfc === big, longNfc.length, longNfd.length, digest(longNfd.slice(0, 100)), longNfd.normalize("NFC") === big); +try { + "x".normalize("NFX"); +} catch (error) { + console.log((error as Error).name, (error as Error).message); +} diff --git a/tests/corpus/8009-normalize-hangul-jungseong-o-yae.ts b/tests/corpus/8009-normalize-hangul-jungseong-o-yae.ts new file mode 100644 index 000000000..b7cb5418a --- /dev/null +++ b/tests/corpus/8009-normalize-hangul-jungseong-o-yae.ts @@ -0,0 +1,28 @@ +// U+11A7 (HANGUL JUNGSEONG O-YAE) is a vowel that composes with nothing: +// after an LV syllable, after a leading consonant, and around trailing +// consonants it stays as it is in every normalization form, as in Node. + +const samples = [ + "가ᆧ", + "가ᆧ", + "가ᆧᆨ", + "각ᆧ", + "ᄀᆧ", + "ᆧ", + "ᆧᆧ", + "x가ᆧy각z", + "café 가ᆧ é", + "ガᆧ가", +]; + +function points(text: string): string { + return Array.from(text, (ch) => ch.codePointAt(0)!.toString(16)).join("."); +} + +for (const sample of samples) { + console.log( + points(sample), + ["NFC", "NFD", "NFKC", "NFKD"].map((form) => points(sample.normalize(form))).join(" "), + ); +} +console.log("가ᆧ".normalize() === "가ᆧ", "가ᆧ".normalize().length); diff --git a/tests/corpus/8010-http-header-raw-bytes.ts b/tests/corpus/8010-http-header-raw-bytes.ts new file mode 100644 index 000000000..d8408f335 --- /dev/null +++ b/tests/corpus/8010-http-header-raw-bytes.ts @@ -0,0 +1,72 @@ +// Header values hold the raw bytes a client sent, one character per byte +// (Node decodes them as latin1). Bytes that are not well-formed UTF-8 (lone +// continuation bytes, runs of them, truncated leads, overlong leads and +// bytes no sequence uses) at every offset within an eight-byte word encode +// to the same bytes as in Node with latin1, ascii, binary, hex, base64 and +// utf16le. Strings built in the program take the same encodings with two-, +// three- and four-byte characters at every offset. +import { createServer } from "node:http"; +import { connect } from "node:net"; + +const patterns: number[][] = [ + [0x80], + [0x80, 0x81, 0xbf, 0x90], + [0xc3], + [0xc0, 0x80, 0xc1, 0xbf], + [0xe9, 0x41, 0xe9], + [0xf0, 0x9f, 0x41], + [0xe2, 0x82], + [0xff, 0xfe, 0xf5], + [0x78, 0xe9, 0x79, 0x80, 0x80, 0x7a, 0xdf], +]; +const encodings = ["latin1", "ascii", "binary", "hex", "base64", "utf16le"] as const; + +function hex(bytes: Buffer): string { + return bytes.toString("hex"); +} + +function describe(label: string, value: string): void { + console.log(label, encodings.map((encoding) => `${encoding}=${hex(Buffer.from(value, encoding))}`).join(" ")); +} + +const names: string[] = []; +const head: Buffer[] = [Buffer.from("GET / HTTP/1.1\r\nHost: x\r\nConnection: close\r\n")]; +for (let p = 0; p < patterns.length; p++) { + for (let offset = 0; offset < 10; offset++) { + for (const tail of ["", "bcdefghij"]) { + const name = `x-${p}-${offset}-${tail.length}`; + names.push(name); + head.push(Buffer.from(`${name}: ${"a".repeat(offset)}`)); + head.push(Buffer.from(patterns[p]!)); + head.push(Buffer.from(`${tail}\r\n`)); + } + } +} +head.push(Buffer.from("\r\n")); + +const server = createServer((req, res) => { + for (const name of names) { + const value = req.headers[name]; + if (typeof value === "string") describe(name, value); + else console.log(name, "missing"); + } + res.end("ok"); +}); + +server.listen(0, "127.0.0.1", () => { + const address = server.address(); + const port = typeof address === "object" && address !== null ? address.port : 0; + const socket = connect({ port, host: "127.0.0.1" }, () => { + socket.write(Buffer.concat(head)); + }); + socket.on("data", () => {}); + socket.on("close", () => { + server.close(); + for (const text of ["é", "éü", "߀", "😀", "é😀ü", "߿\u0080"]) { + for (let offset = 0; offset < 17; offset++) { + const value = "a".repeat(offset) + text + "b".repeat(offset % 9) + text; + describe(`text ${offset} ${JSON.stringify(text)}`, value); + } + } + }); +}); diff --git a/tests/corpus/8011-foreign-element-tojson.ts b/tests/corpus/8011-foreign-element-tojson.ts new file mode 100644 index 000000000..60db9c9bd --- /dev/null +++ b/tests/corpus/8011-foreign-element-tojson.ts @@ -0,0 +1,72 @@ +// JSON.stringify of a typed array runs the toJSON of a foreign element +// (an object written through an unknown[] view) with the element's index +// as its key, as Node does, including when the hook grows, truncates or +// replaces the arrays holding it. A hook or getter that throws stops the +// stringify with its exception. +const nums: number[] = [1, 2, 3]; +const view: unknown[] = nums; +view[1] = { + toJSON(key: string) { + return `v${key}`; + }, +}; +view[2] = { toJSON: 5 }; +console.log(JSON.stringify(nums), JSON.stringify(view)); + +const words: string[] = ["a", "b"]; +const wordsView: unknown[] = words; +wordsView[0] = { toJSON: () => undefined }; +wordsView[1] = { toJSON: () => ({ nested: [1, 2] }) }; +console.log(JSON.stringify(words), JSON.stringify({ words })); + +// The hook grows the parent past its storage and then truncates it. +const outer: number[][] = [[1, 2], [3], [4, 5, 6]]; +const innerView: unknown[] = outer[1]!; +let calls = 0; +innerView[1] = { + toJSON() { + calls++; + for (let i = 0; i < 300; i++) outer.push([i, i + 1]); + if (calls === 1) outer.length = 2; + return "hooked"; + }, +}; +console.log(JSON.stringify(outer), outer.length, calls); + +// The hook replaces the parent's elements two levels up. +const deep: number[][][] = [[[1], [2]], [[3]]]; +const deepView: unknown[] = deep[0]![1]!; +deepView[0] = { + toJSON() { + for (let i = 0; i < 200; i++) deep.push([[i]]); + deep[0] = []; + return 9; + }, +}; +console.log(JSON.stringify(deep).length, deep.length); + +// A throwing hook or getter stops the stringify. +const thrown: number[] = [1, 2]; +const thrownView: unknown[] = thrown; +thrownView[1] = { + toJSON() { + throw new Error("from toJSON"); + }, +}; +try { + console.log(JSON.stringify(thrown)); +} catch (error) { + console.log("caught", (error as Error).message); +} +thrownView[1] = { + get a() { + throw new Error("from getter"); + }, +}; +try { + console.log(JSON.stringify(thrown)); +} catch (error) { + console.log("caught", (error as Error).message); +} +const after: unknown[] = [1, { b: 2 }]; +console.log(JSON.stringify(after)); diff --git a/tests/corpus/8014-foreign-element-callbacks.ts b/tests/corpus/8014-foreign-element-callbacks.ts new file mode 100644 index 000000000..188a3119d --- /dev/null +++ b/tests/corpus/8014-foreign-element-callbacks.ts @@ -0,0 +1,118 @@ +// Callbacks over a number[] or string[] that holds elements written +// through an unknown[] view, when the callback never reads the element: +// it declares no element parameter, or never references the one it +// declares. The methods visit every element as in Node, and filter keeps +// a written element as it is. + +const numbers: number[] = [1, 2]; +const numbersView: unknown[] = numbers; +numbersView.push("x", undefined, { k: 1 }); +numbers.push(5); + +// Inline callbacks (fused loops). +console.log(numbers.filter(() => true)); +console.log(numbers.filter((_, i) => i % 2 === 0)); +console.log(numbers.map(() => 0)); +console.log(numbers.map((_, i) => i * 10)); +console.log(numbers.some(() => false), numbers.every((_v) => true)); +console.log(numbers.findIndex((_, i) => i === 2), numbers.findLastIndex((_, i) => i < 3)); +console.log(numbers.find(() => false), numbers.findLast((_, i) => i === 0)); +console.log(numbers.reduce((s) => s + 1, 0), numbers.reduce((s, v) => s + 1, 100)); +console.log(numbers.reduceRight((s, _v, i) => s + i, 0)); +let visits = 0; +numbers.forEach((_, i) => { + visits += i; +}); +console.log(visits); +console.log(numbers.filter((_, i) => i > 1).map((_, i) => i)); +console.log(numbers.flatMap(() => [7]), numbers.flatMap((_, i) => [i, -i])); +console.log(numbers.filter(() => true).length, typeof (numbers.filter(() => true) as unknown[])[2]); + +// Callback values (shared helpers). +const always = (): boolean => true; +const never = (): boolean => false; +const count = (s: number): number => s + 1; +console.log(numbers.filter(always)); +console.log(numbers.some(never), numbers.every(always), numbers.find(never)); +console.log(numbers.findIndex(never), numbers.reduce(count, 0)); +console.log(numbers.map(always)); + +// A kept written element is the same value in the result. +const kept = numbers.filter(always) as unknown[]; +console.log(kept[2] === "x", kept[3] === undefined, 3 in kept, kept[4], kept.length); + +// string[] the same way. +const words: string[] = ["a", "b"]; +const wordsView: unknown[] = words; +wordsView.push(7, null); +console.log(words.filter(() => true), words.map((_, i) => `#${i}`)); +console.log(words.filter((_w, i) => i !== 1), words.reduce((s) => s + 1, 0)); + +// Holes stay holes and are skipped as in Node. +const sparse: number[] = [1]; +const sparseView: unknown[] = sparse; +sparseView[3] = "z"; +console.log(sparse.filter(() => true), sparse.map((_, i) => i), sparse.findIndex((_, i) => i === 1)); + +// A parameter used only through a shorthand property is still read. +const plain = [1, 2, 3]; +console.log(plain.map((v) => ({ v })), plain.reduce((acc, v) => ({ sum: acc.sum + v, v }), { sum: 0, v: 0 })); +console.log(plain.filter((v) => [{ v }].length > 0 && v > 1)); + +// A callback that ignores its element still receives the element as it was +// before the callback ran: filter keeps it, and find returns it, even when +// the callback overwrites or removes the slot. +const changing: number[] = [1, 2, 3]; +const changingView: unknown[] = changing; +changingView.push("x"); +let at = 0; +console.log( + changing.filter(() => { + changingView[at] = at === 1 ? "w" : 100 + at; + at++; + return true; + }), + changing, +); +const found: number[] = [1, 2]; +(found as unknown[]).push("y"); +let step = 0; +console.log( + found.find(() => { + found[step] = 50; + step++; + return step === 2; + }), + found, +); +const shrinking: number[] = [4, 5]; +(shrinking as unknown[]).push("z", 6); +console.log( + shrinking.findLast(() => { + shrinking.length = 1; + return true; + }), + shrinking.length, +); +const cut: number[] = [7, 8, 9]; +(cut as unknown[]).push("q"); +console.log( + cut.filter((_v, i) => { + if (i === 1) cut.length = 1; + return true; + }), + cut.length, +); + +// Arrays proven to hold no undefined keep their elements in filter results +// when the predicate ignores them. +const dense = [1, 2, 3, 4]; +let calls = 0; +console.log( + dense.filter((_v, i) => { + calls++; + return i % 2 === 0; + }), + calls, + dense.filter((_v, i) => i > 1), +); diff --git a/tests/corpus/8020-date-setters.ts b/tests/corpus/8020-date-setters.ts new file mode 100644 index 000000000..6bfeff5c8 --- /dev/null +++ b/tests/corpus/8020-date-setters.ts @@ -0,0 +1,115 @@ +// Date setters: every local and UTC setter with each argument count, the +// time values they return, normalization of out-of-range fields, local +// setters across a daylight saving transition, setters on invalid dates, +// explicit undefined arguments, fractional arguments, and the TimeClip +// range. A setter changes the one Date object every alias holds. +export {}; +process.env.TZ = "America/Chicago"; + +const show = (label: string, d: Date): void => { + console.log(label, d.getTime(), Number.isNaN(d.getTime()) ? "invalid" : d.toISOString()); +}; + +const d = new Date(Date.UTC(2026, 0, 31, 10, 20, 30, 400)); +console.log(d.setMilliseconds(999), d.getMilliseconds()); +console.log(d.setSeconds(5), d.setSeconds(6, 7)); +console.log(d.setMinutes(1), d.setMinutes(2, 3), d.setMinutes(4, 5, 6)); +console.log(d.setHours(7), d.setHours(8, 9), d.setHours(10, 11, 12), d.setHours(13, 14, 15, 16)); +console.log(d.setDate(15), d.setMonth(1), d.setMonth(2, 3)); +console.log(d.setFullYear(2025), d.setFullYear(2024, 1), d.setFullYear(2023, 4, 6)); +show("local", d); + +const u = new Date(Date.UTC(2026, 0, 31)); +console.log(u.setUTCMilliseconds(1), u.setUTCSeconds(2, 3), u.setUTCMinutes(4, 5, 6)); +console.log(u.setUTCHours(7, 8, 9, 10), u.setUTCDate(1), u.setUTCMonth(5, 30)); +console.log(u.setUTCFullYear(1999), u.setUTCFullYear(2000, 1, 29)); +show("utc", u); + +// Fields out of range carry into the next larger field, both ways. +const n = new Date(Date.UTC(2026, 0, 31, 12)); +n.setUTCMonth(1); +show("Jan 31 + month", n); +n.setUTCDate(0); +show("day 0", n); +n.setUTCMonth(13); +show("month 13", n); +n.setUTCHours(-1); +show("hour -1", n); +n.setUTCMinutes(60 * 24 + 1); +show("minutes past a day", n); +n.setUTCSeconds(-0.5); +show("fractional seconds", n); +n.setUTCMilliseconds(1.9); +show("fractional ms", n); + +// Local setters in a zone with daylight saving time: 2026-03-08 02:30 +// does not exist in Chicago and 2026-11-01 01:30 happens twice. +const spring = new Date(2026, 2, 8, 0, 0); +spring.setHours(2, 30); +show("spring gap", spring); +console.log(spring.getHours(), spring.getMinutes(), spring.getTimezoneOffset()); +const fall = new Date(2026, 10, 1, 0, 0); +fall.setHours(1, 30); +show("fall overlap", fall); +fall.setMinutes(fall.getMinutes() + 60); +show("fall overlap + 60 min", fall); +const walk = new Date(2026, 2, 7, 12); +for (let i = 0; i < 3; i++) { + walk.setDate(walk.getDate() + 1); + console.log("walk", walk.getDate(), walk.getHours(), walk.getTimezoneOffset()); +} + +// Invalid dates: setFullYear starts from +0 local time, the others keep NaN. +const bad = new Date(NaN); +console.log(bad.setHours(1), bad.setDate(2), bad.setUTCMonth(3), bad.setMilliseconds(4)); +console.log(bad.setFullYear(2020)); +show("after setFullYear", bad); +const badUtc = new Date("not a date"); +console.log(badUtc.setUTCFullYear(2021, 6)); +show("after setUTCFullYear", badUtc); + +// Explicit undefined and NaN arguments make the date invalid; a missing +// optional argument keeps the current field. +const opt = new Date(Date.UTC(2026, 5, 15, 9)); +console.log(opt.setUTCHours(10, undefined), Number.isNaN(opt.getTime())); +const opt2 = new Date(Date.UTC(2026, 5, 15, 9)); +console.log(opt2.setUTCMonth(Number.NaN), opt2.setUTCMonth(2)); + +// setTime and the TimeClip range. +const t = new Date(0); +console.log(t.setTime(8.64e15), t.setTime(8.64e15 + 1), t.setTime(-8.64e15), t.setTime(1.5)); +console.log(t.setTime(-0), Object.is(t.getTime(), 0), t.setTime(Number.NaN)); +const far = new Date(Date.UTC(275760, 8, 13)); +console.log(far.getTime(), far.setUTCMilliseconds(1)); + +// Every alias sees the change: a second binding, an array slot, a Map +// value, a record field, a closure and a parameter. +const shared = new Date(Date.UTC(2026, 6, 4)); +const alias = shared; +const list = [shared, new Date(Date.UTC(2000, 0, 1))]; +const byName = new Map([["shared", shared]]); +const holder = { when: shared }; +const bump = (): number => shared.setUTCDate(shared.getUTCDate() + 1); +const nextYear = (date: Date): void => { + date.setUTCFullYear(date.getUTCFullYear() + 1); +}; +alias.setUTCHours(6); +list[0]!.setUTCMinutes(30); +byName.get("shared")!.setUTCSeconds(15); +holder.when.setUTCMilliseconds(250); +bump(); +nextYear(shared); +show("shared", shared); +console.log(alias === shared, list[0] === shared, byName.get("shared") === holder.when); +console.log(list.map((x) => x.toISOString()).join(" ")); + +// A Date a loop adjusts in place. +const cursor = new Date(Date.UTC(2026, 0, 1)); +let weekdays = 0; +while (cursor.getUTCFullYear() === 2026) { + const day = cursor.getUTCDay(); + if (day !== 0 && day !== 6) weekdays++; + cursor.setUTCDate(cursor.getUTCDate() + 1); +} +console.log("weekdays in 2026", weekdays); +show("cursor", cursor); diff --git a/tests/corpus/8021-date-coercions.ts b/tests/corpus/8021-date-coercions.ts new file mode 100644 index 000000000..2410853a4 --- /dev/null +++ b/tests/corpus/8021-date-coercions.ts @@ -0,0 +1,47 @@ +// Dates converted to primitives as Node converts them: unary +, Number() +// and relational operators read the time value (the number hint), while + +// with a string, template literals and String() read the text (a Date's +// default hint is a string). Symbol.toPrimitive, valueOf and toString answer +// the same values; == compares two Dates by identity and a Date with a +// primitive through its default primitive. typeof, truthiness and +// instanceof treat a Date as an object, invalid ones included. +export {}; +process.env.TZ = "UTC"; + +const a = new Date(Date.UTC(2026, 9, 23, 8, 30)); +const b = new Date(Date.UTC(2026, 9, 24, 8, 30)); +const bad = new Date(Number.NaN); + +console.log(+a, -a, +b - +a, (+b - +a) / 3600000, Number(a), Math.max(+a, +b) === b.getTime()); +console.log(a + "!"); +console.log(`at ${a}`); +console.log(String(b)); +console.log("" + a === a.toString(), a + "1" === a.toString() + "1"); +console.log(a[Symbol.toPrimitive]("number"), a[Symbol.toPrimitive]("string") === a.toString()); +console.log(a[Symbol.toPrimitive]("default") === a.toString(), a.valueOf() === a.getTime()); +console.log(a < b, a <= b, a > b, a >= b, +b > a.getTime() + 1); + +const same = a; +const twin = new Date(a.getTime()); +console.log(a == same, a == twin, a === twin, a != twin, a.getTime() === twin.getTime()); +console.log(a == (a.toString() as unknown), a == (a.getTime() as unknown)); + +console.log(typeof a, typeof bad, a instanceof Date, bad instanceof Date); +console.log(Object.prototype.toString.call(a), !a, !!bad, bad ? "truthy" : "falsy"); + +// Invalid dates. +console.log(+bad, +bad - +a, `${bad}`, String(bad), bad + "", Number.isNaN(bad.getTime())); +console.log(bad.toJSON(), JSON.stringify({ when: bad, at: a })); +try { + bad.toISOString(); +} catch (error) { + console.log((error as Error).name, (error as Error).message); +} + +// Sorting and summing through the number hint. +const days = [b, a, new Date(Date.UTC(2026, 0, 1))]; +days.sort((x, y) => +x - +y); +console.log(days.map((x) => x.getUTCDate()).join(",")); +let span = 0; +for (let i = 1; i < days.length; i++) span += +days[i]! - +days[i - 1]!; +console.log(span / 86400000); diff --git a/tests/corpus/8022-date-unions.ts b/tests/corpus/8022-date-unions.ts new file mode 100644 index 000000000..dedcd8bf1 --- /dev/null +++ b/tests/corpus/8022-date-unions.ts @@ -0,0 +1,68 @@ +// Dates in unions with other arms: Date | string, Date | number and +// Date | a record, narrowed with instanceof and typeof, in arrays, Map +// values, optional fields and function results. A Date arm keeps its +// identity, so a setter through a union-typed alias changes the original. +export {}; +process.env.TZ = "UTC"; + +type When = Date | string | number; + +function describe(when: When): string { + if (when instanceof Date) return `date ${when.toISOString()}`; + if (typeof when === "string") return `text ${when}`; + return `ms ${when}`; +} + +function normalize(when: When): Date { + return when instanceof Date ? when : new Date(when); +} + +const fixed = new Date(Date.UTC(2026, 9, 23)); +const values: When[] = [fixed, "2026-01-02T03:04:05Z", 0, new Date(Number.NaN)]; +for (const value of values) { + const text = value instanceof Date && Number.isNaN(value.getTime()) ? "invalid" : describe(value); + console.log(text); +} +console.log(normalize(fixed) === fixed, normalize("2000-01-01").getUTCFullYear(), normalize(1e12).getUTCFullYear()); + +const held: When = fixed; +if (held instanceof Date) held.setUTCHours(12); +console.log(fixed.getUTCHours(), held === fixed); + +type Stamp = Date | { label: string; at: number }; +const stamps: Stamp[] = [fixed, { label: "start", at: 5 }]; +for (const stamp of stamps) { + console.log(stamp instanceof Date ? stamp.getUTCDate() : `${stamp.label}@${stamp.at}`); +} + +const byKey = new Map(); +byKey.set("a", fixed); +byKey.set("b", "later"); +for (const [key, value] of byKey) console.log(key, value instanceof Date ? value.getTime() : value); + +interface Task { + name: string; + due?: Date; + done: Date | null; +} +const tasks: Task[] = [ + { name: "write", due: fixed, done: null }, + { name: "ship", done: new Date(Date.UTC(2026, 11, 1)) }, +]; +for (const task of tasks) { + const due = task.due ? task.due.toISOString().slice(0, 10) : "none"; + const done = task.done === null ? "open" : task.done.toISOString().slice(0, 10); + console.log(task.name, due, done); +} +tasks[0]!.due!.setUTCDate(30); +console.log(fixed.getUTCDate(), tasks[0]!.due === fixed); + +function latest(dates: (Date | undefined)[]): Date | string { + let best: Date | undefined; + for (const date of dates) if (date && (best === undefined || date > best)) best = date; + return best ?? "none"; +} +const found = latest([undefined, fixed, new Date(0)]); +console.log(found === fixed, latest([])); + +console.log(JSON.stringify({ values, stamps })); diff --git a/tests/corpus/8023-date-clone-throw.ts b/tests/corpus/8023-date-clone-throw.ts new file mode 100644 index 000000000..b9d569d00 --- /dev/null +++ b/tests/corpus/8023-date-clone-throw.ts @@ -0,0 +1,32 @@ +// Dates through structuredClone and MessageChannel, which copy them as +// new Dates with the same time value, and Dates thrown and rejected as +// values, which keep their identity. +import { MessageChannel } from "node:worker_threads"; +process.env.TZ = "UTC"; + +const at = new Date(Date.UTC(2026, 9, 23, 7)); +const copy = structuredClone(at); +console.log(copy instanceof Date, copy === at, copy.getTime() === at.getTime()); +copy.setUTCFullYear(2000); +console.log(at.getUTCFullYear(), copy.getUTCFullYear()); + +const nested = structuredClone({ at, list: [at, at], invalid: new Date(Number.NaN) }); +console.log(nested.list[0] instanceof Date, nested.at === at, nested.list.length); +console.log(nested.at.toISOString(), Number.isNaN(nested.invalid.getTime())); + +try { + throw at; +} catch (error) { + console.log(error instanceof Date, error === at); +} + +const { port1, port2 } = new MessageChannel(); +port2.on("message", (data: { when: Date; again: Date }) => { + const { when, again } = data; + console.log("message", when instanceof Date, when === at, again instanceof Date, when.toISOString()); + port2.close(); + Promise.reject(at).catch((reason: unknown) => { + console.log("rejected", reason instanceof Date, reason === at); + }); +}); +port1.postMessage({ when: at, again: at }); diff --git a/tests/corpus/8024-date-setters-dynamic.mjs b/tests/corpus/8024-date-setters-dynamic.mjs new file mode 100644 index 000000000..9b6392b60 --- /dev/null +++ b/tests/corpus/8024-date-setters-dynamic.mjs @@ -0,0 +1,24 @@ +// Date setters in JavaScript, on Dates held as untyped values: each setter +// converts its arguments with ToNumber (valueOf runs, undefined is NaN) and +// changes the one Date object every alias holds. +process.env.TZ = "Europe/Berlin"; + +const d = new Date(Date.UTC(2026, 2, 28, 12)); +const alias = d; +const box = { d }; +console.log(d.setHours(1, 30), d.getHours(), d.getTimezoneOffset()); +console.log(alias.setDate(29), box.d.getHours(), d.getTimezoneOffset()); +console.log(d.setMinutes("15"), d.setSeconds(true), d.setMilliseconds(null)); +console.log(d.setUTCFullYear(2030, 11, 31), d.toISOString()); +console.log(d.setMonth(undefined), Number.isNaN(d.getTime())); +console.log(d.setFullYear(2001), d.toISOString()); + +let calls = 0; +const later = { valueOf() { calls++; return 45; } }; +console.log(d.setUTCMinutes(later, later), calls, d.toISOString()); + +const other = new Date(0); +console.log(other.setHours(5), other.getHours(), typeof d.setMonth); +const list = [new Date(0), new Date(86400000)]; +list.forEach((x, i) => x.setUTCHours(i + 1)); +console.log(list.map((x) => x.toISOString()).join(" ")); diff --git a/tests/corpus/8025-date-subclass-setters.ts b/tests/corpus/8025-date-subclass-setters.ts new file mode 100644 index 000000000..a6414a047 --- /dev/null +++ b/tests/corpus/8025-date-subclass-setters.ts @@ -0,0 +1,30 @@ +// Setters on instances of a class declared `extends Date`: each changes +// the instance's time value, which the inherited getters, the subclass's +// own methods and toISOString then read; invalid instances follow the same +// rules as Dates. +export {}; +process.env.TZ = "Asia/Kolkata"; + +class Deadline extends Date { + label: string; + constructor(label: string, ms: number) { + super(ms); + this.label = label; + } + extend(days: number): this { + this.setDate(this.getDate() + days); + return this; + } + describe(): string { + return `${this.label} ${this.toISOString()}`; + } +} + +const due = new Deadline("report", Date.UTC(2026, 9, 23, 18, 30)); +console.log(due.describe()); +console.log(due.setHours(9, 15), due.getHours(), due.getMinutes()); +console.log(due.extend(10).describe()); +console.log(due.setUTCMonth(0, 31), due.setMonth(1), due.getDate(), due.getMonth()); +console.log(due.setFullYear(2024, 1, 29), due.describe()); +console.log(due.setTime(Number.NaN), Number.isNaN(due.getTime()), due.setUTCFullYear(2030)); +console.log(due.describe(), due instanceof Date, due instanceof Deadline); diff --git a/tests/corpus/8026-date-subclass-setters-dynamic.mjs b/tests/corpus/8026-date-subclass-setters-dynamic.mjs new file mode 100644 index 000000000..8b89d6ec8 --- /dev/null +++ b/tests/corpus/8026-date-subclass-setters-dynamic.mjs @@ -0,0 +1,17 @@ +// Setters on instances of a JavaScript class declared `extends Date`, held +// as untyped values: the inherited setters change the instance's time value. +process.env.TZ = "America/Sao_Paulo"; + +class Shift extends Date { + start() { + this.setHours(8, 0, 0, 0); + return this; + } +} + +const shift = new Shift(Date.UTC(2026, 4, 5, 15)); +console.log(shift.start().toISOString(), shift.getHours()); +console.log(shift.setDate(31), shift.getMonth(), shift.setUTCHours(0), shift.toISOString()); +const values = [shift, new Shift(0)]; +for (const value of values) value.setUTCMinutes(45); +console.log(values.map((value) => value.toISOString()).join(" "), values[0] === shift); diff --git a/tests/corpus/8027-date-setter-order.ts b/tests/corpus/8027-date-setter-order.ts new file mode 100644 index 000000000..b87f3a656 --- /dev/null +++ b/tests/corpus/8027-date-setter-order.ts @@ -0,0 +1,66 @@ +// Date setters and evaluation order: a setter called without arguments +// makes the Date invalid, an argument that calls setTime on the same +// Date subclass instance is seen by the setter, a comparison or +// concatenation reads its left Date after the right side runs a setter, +// and toJSON reads its Date after an argument runs a setter. +export {}; +process.env.TZ = "UTC"; + +const show = (label: string, d: Date): void => { + console.log(label, d.getTime(), Number.isNaN(d.getTime()) ? "invalid" : d.toISOString()); +}; + +// No arguments: the first argument is undefined, so NaN. +const local = new Date(Date.UTC(2026, 5, 15, 9, 30)); +// @ts-expect-error: the first argument is required +console.log(local.setHours(), Number.isNaN(local.getTime())); +const utc = new Date(Date.UTC(2026, 5, 15, 9, 30)); +// @ts-expect-error: the first argument is required +console.log(utc.setUTCMonth(), utc.setUTCFullYear(2020)); +show("utc after", utc); +const kept = new Date(Date.UTC(2026, 5, 15)); +const keep = kept; +// @ts-expect-error: the first argument is required +console.log(keep.setMinutes(), kept.getTime()); + +class Stamp extends Date { + label(): string { + return `stamp ${this.toISOString()}`; + } +} +const stamp = new Stamp(Date.UTC(2026, 0, 1)); +// @ts-expect-error: the first argument is required +console.log(stamp.setSeconds(), Number.isNaN(stamp.getTime())); +stamp.setTime(Date.UTC(2026, 0, 2)); +// @ts-expect-error: the first argument is required +console.log(stamp.setUTCDate(), stamp.setUTCFullYear(2024, 1, 29), stamp.label()); + +// An argument that calls setTime on the same instance. +const moved = new Stamp(Date.UTC(2026, 3, 10, 12)); +console.log(moved.setUTCHours((moved.setTime(Date.UTC(2000, 0, 1)), 5)), moved.label()); +const plain = new Date(Date.UTC(2026, 3, 10, 12)); +console.log(plain.setUTCMinutes((plain.setTime(Date.UTC(2001, 0, 1)), 7))); +show("plain", plain); + +// The left operand converts after the right operand runs a setter. +const a = new Date(Date.UTC(2026, 0, 1)); +const b = new Date(Date.UTC(2026, 0, 2)); +console.log(a < (a.setUTCDate(3), b), a > b); +const c = new Date(Date.UTC(2026, 0, 1)); +console.log(c >= (c.setTime(0), new Date(1)), c <= new Date(1)); +const s = new Date(Date.UTC(2026, 0, 1)); +console.log(s + (s.setUTCFullYear(1999), "!")); +const n = new Date(Date.UTC(2026, 0, 1)); +console.log(+n === (n.setTime(5), 5), +n); + +// Locals a function keeps as their time value: a method reads the time +// value after its arguments run a setter. +function locals(): void { + const json = new Date(Date.UTC(2026, 0, 1)); + console.log(json.toJSON(json.setUTCFullYear(1999)), json.toJSON()); + const key = new Date(Date.UTC(2026, 0, 1)); + console.log(key.toJSON(String(key.setTime(Date.UTC(2021, 5, 1))))); + const read = new Date(Date.UTC(2026, 0, 1)); + console.log(read.toJSON(), read.getTime()); +} +locals(); diff --git a/tests/corpus/8028-date-setter-order-dynamic.mjs b/tests/corpus/8028-date-setter-order-dynamic.mjs new file mode 100644 index 000000000..f52e410c1 --- /dev/null +++ b/tests/corpus/8028-date-setter-order-dynamic.mjs @@ -0,0 +1,17 @@ +// Date setters reached through untyped values: without arguments each +// one makes the Date invalid, and an argument that calls setTime on the +// same Date is seen by the setter. +process.env.TZ = "UTC"; + +const names = [ + "setMilliseconds", "setSeconds", "setMinutes", "setHours", "setDate", "setMonth", "setFullYear", + "setUTCMilliseconds", "setUTCSeconds", "setUTCMinutes", "setUTCHours", "setUTCDate", "setUTCMonth", "setUTCFullYear", +]; +for (const name of names) { + const d = new Date(Date.UTC(2026, 5, 15)); + console.log(name, d[name](), d.getTime()); +} +const d = new Date(Date.UTC(2026, 3, 10, 12)); +console.log(d.setUTCHours((d.setTime(Date.UTC(2000, 0, 1)), 5)), d.toISOString()); +const a = new Date(Date.UTC(2026, 0, 1)); +console.log(a < (a.setUTCDate(3), new Date(Date.UTC(2026, 0, 2)))); diff --git a/tests/corpus/8062-assert-message-values.cjs b/tests/corpus/8062-assert-message-values.cjs new file mode 100644 index 000000000..fc37a71e4 --- /dev/null +++ b/tests/corpus/8062-assert-message-values.cjs @@ -0,0 +1,48 @@ +'use strict'; +// node:assert messages that are not string literals: an untyped string, an +// Error to throw, null/undefined (omitted) and falsy values, and +// scalar-union operands against untyped values. +const assert = require('assert'); + +function attempt(label, fn) { + try { + fn(); + console.log(label, 'passed'); + } catch (e) { + console.log(label, e.name, JSON.stringify(e.message), e.generatedMessage, e.code); + } +} + +const failures = []; +for (const x of ['a', 'b']) if (x === 'c') failures.push(`\n${x}`); +attempt('joined passes', () => assert.strictEqual(failures.length, 0, failures.join(''))); +const untyped = JSON.parse('"untyped message"'); +attempt('untyped', () => assert.strictEqual(1, 2, untyped)); +attempt('untyped deep', () => assert.deepStrictEqual([1], [2], untyped)); +attempt('untyped notStrict', () => assert.notStrictEqual(1, 1, untyped)); +attempt('error message', () => assert.strictEqual(1, 2, new TypeError('own error'))); +attempt('error deep', () => assert.deepStrictEqual({ a: 1 }, { a: 2 }, new RangeError('own range'))); +const none = JSON.parse('null'); +attempt('null message', () => assert.strictEqual(1, 2, none)); +const zero = JSON.parse('0'); +attempt('zero message deep', () => assert.deepStrictEqual([1], [2], zero)); +attempt('passing error message', () => assert.strictEqual(1, 1, new Error('unused'))); +attempt('later string message', () => assert.strictEqual(1, 2, 'plain')); + +// Operand order: both operands evaluate before the message. +const order = []; +const mark = (label, value) => (order.push(label), value); +attempt('order', () => assert.strictEqual(mark('a', 1), mark('b', 2), mark('m', JSON.parse('"m"')))); +console.log(order.join(',')); + +const row = [['a', 'b', true], ['c', 'd', 'x']]; +for (const [, , expected] of row) { + attempt('union operand', () => assert.strictEqual(JSON.parse(JSON.stringify(expected)), expected)); +} + +// assert.fail with a message that is not a string: an Error is thrown as +// is; other values become the message, generated when falsy. +attempt('fail error', () => assert.fail(new RangeError('re'))); +attempt('fail value', () => assert.fail(JSON.parse('{"a":1}'))); +attempt('fail zero', () => assert.fail(zero)); +attempt('fail null', () => assert.fail(JSON.parse('null'))); diff --git a/tests/corpus/8065-cjs-top-level-return/early.js b/tests/corpus/8065-cjs-top-level-return/early.js new file mode 100644 index 000000000..8d7de4ba3 --- /dev/null +++ b/tests/corpus/8065-cjs-top-level-return/early.js @@ -0,0 +1,6 @@ +'use strict'; +exports.ready = true; +console.log('early module start', typeof helper); +if (exports.ready) return; +console.log('early module end'); +function helper() {} diff --git a/tests/corpus/8065-cjs-top-level-return/main.js b/tests/corpus/8065-cjs-top-level-return/main.js new file mode 100644 index 000000000..7743eaac5 --- /dev/null +++ b/tests/corpus/8065-cjs-top-level-return/main.js @@ -0,0 +1,16 @@ +'use strict'; +// A CommonJS module may end its evaluation with a top-level return: the +// rest of the module does not run, while timers, exit handlers and the +// modules it already required keep their effects. +const early = require('./early.js'); +console.log('early exports', early.ready); +setTimeout(() => console.log('timer ran'), 1); +process.on('exit', () => console.log('exit handler')); +for (const value of [1, 2, 3]) { + if (value === 2) return 'ignored value'; + console.log('value', value); +} +console.log('not reached'); +function hoisted() { + return 'hoisted'; +} diff --git a/tests/corpus/8065-cjs-top-level-return/package.json b/tests/corpus/8065-cjs-top-level-return/package.json new file mode 100644 index 000000000..5bbefffba --- /dev/null +++ b/tests/corpus/8065-cjs-top-level-return/package.json @@ -0,0 +1,3 @@ +{ + "type": "commonjs" +} diff --git a/tests/corpus/8066-object-hasown-delete.cjs b/tests/corpus/8066-object-hasown-delete.cjs new file mode 100644 index 000000000..11668b521 --- /dev/null +++ b/tests/corpus/8066-object-hasown-delete.cjs @@ -0,0 +1,23 @@ +'use strict'; +// Object.hasOwn over nullish receivers and as a value, and `delete` of +// properties JavaScript inference types as required. +const o = { a: 1, b: undefined }; +console.log(Object.hasOwn(o, 'a'), Object.hasOwn(o, 'b'), Object.hasOwn(o, 'c')); +console.log(Object.hasOwn([1, 2], 1), Object.hasOwn([1, 2], 2), Object.hasOwn(Object.create({ x: 1 }), 'x')); +for (const value of [null, undefined]) { + try { + Object.hasOwn(value, 'a'); + } catch (e) { + console.log(e.name, e.message); + } +} +const pick = Math.random() < 2 ? { k: 1 } : null; +console.log(Object.hasOwn(pick, 'k')); +const hasOwn = Object.hasOwn; +console.log(hasOwn({ k: 1 }, 'k'), hasOwn({}, 'k'), [{ a: 1 }, {}].map((x) => hasOwn(x, 'a')).join()); +const record = { a: 1, b: 2 }; +delete record.a; +console.log(Object.keys(record).join(), Object.hasOwn(record, 'a'), record.a); +const table = { one: 1, two: 2 }; +for (const key of Object.keys(table)) if (key === 'one') delete table[key]; +console.log(JSON.stringify(table)); diff --git a/tests/corpus/8067-assert-throws-validators.cjs b/tests/corpus/8067-assert-throws-validators.cjs new file mode 100644 index 000000000..189f4221a --- /dev/null +++ b/tests/corpus/8067-assert-throws-validators.cjs @@ -0,0 +1,22 @@ +'use strict'; +// assert.throws and assert.rejects with validation functions, constructor +// functions, and untyped expected values (Node's expectedException). +const assert = require('assert'); +function show(label, fn) { + try { fn(); console.log(label, 'passed'); } catch (e) { console.log(label, e.name, JSON.stringify(e.message), e.generatedMessage, e.operator, e.actual && e.actual.message); } +} +function check(err) { return err.message === 'x'; } +show('named false', () => assert.throws(() => { throw new Error('y'); }, check)); +show('named true', () => assert.throws(() => { throw new Error('x'); }, check)); +show('custom message', () => assert.throws(() => { throw new Error('y'); }, check, 'custom')); +show('returns 1', () => assert.throws(() => { throw new Error('y'); }, () => 1)); +show('non-error thrown', () => assert.throws(() => { throw 'str'; }, (v) => v === 'str')); +show('non-error fails', () => assert.throws(() => { throw 42; }, () => undefined)); +function MyErr(m) { this.message = m; } +show('constructor', () => assert.throws(() => { throw new MyErr('q'); }, MyErr)); +function untyped(expected) { assert.throws(() => { throw new Error('u'); }, expected); } +show('untyped validator', () => untyped((e) => e.message === 'u')); +show('untyped validator fails', () => untyped(function v() { return false; })); +show('untyped object', () => untyped({ message: 'u' })); +assert.rejects(Promise.reject(new Error('r')), (e) => e.message === 'r').then(() => console.log('rejects passed')); +assert.rejects(Promise.reject(new Error('r')), () => false).catch((e) => console.log('rejects', JSON.stringify(e.message), e.operator)); diff --git a/tests/corpus/8076-assert-instance-identity.cjs b/tests/corpus/8076-assert-instance-identity.cjs new file mode 100644 index 000000000..e8cedf3d6 --- /dev/null +++ b/tests/corpus/8076-assert-instance-identity.cjs @@ -0,0 +1,50 @@ +'use strict'; +// assert.strictEqual and notStrictEqual compare class instances by +// reference: two instances, an instance and the same instance read back +// through an untyped value, and an instance a method returns. Symbols +// compare by identity too. +const assert = require('assert'); +const EventEmitter = require('events'); + +class Box { + constructor(v) { + this.v = v; + } + self() { + return this; + } +} + +const b = new Box(1); +const c = new Box(1); +assert.strictEqual(b, b.self()); +assert.notStrictEqual(b, c); +const e = new EventEmitter(); +assert.strictEqual(e, e.setMaxListeners(1)); +const untyped = [e, b][0]; +assert.strictEqual(untyped, e); +assert.strictEqual(e, untyped); +assert.notStrictEqual(untyped, b); + +try { + assert.strictEqual(b, c); +} catch (error) { + console.log(error.code); + console.log(error.message); +} +try { + assert.notStrictEqual(b, b.self(), 'same box'); +} catch (error) { + console.log(error.code, error.message); +} +// A symbol read back through an untyped value is the same symbol. +const symbol = Symbol('s'); +const read = Reflect.get({ k: symbol }, 'k'); +assert.strictEqual(read, symbol); +assert.notStrictEqual(read, Symbol('s')); +try { + assert.strictEqual(read, Symbol('other')); +} catch (error) { + console.log(error.code, error.message.split('\n')[0]); +} +console.log('done'); diff --git a/tests/corpus/8078-instanceof-object-function.cjs b/tests/corpus/8078-instanceof-object-function.cjs new file mode 100644 index 000000000..22f212269 --- /dev/null +++ b/tests/corpus/8078-instanceof-object-function.cjs @@ -0,0 +1,77 @@ +'use strict'; +// `x instanceof Object` and `x instanceof Function` on untyped values follow +// Node: objects are instances of Object unless their prototype chain ends +// in null first, and functions (classes and arrows included) are instances +// of Function. +class Shape {} +class Square extends Shape {} + +const nullProto = Object.create(null); +const underNull = Object.create(nullProto); +const regrown = Object.create(null); +Object.setPrototypeOf(regrown, {}); +const orphan = function orphan() {}; +Object.setPrototypeOf(orphan, null); +function declared() {} + +const values = { + 'plain object': {}, + array: [1, 2], + 'Object.create(null)': nullProto, + 'object under a null-prototype object': underNull, + 'Object.create(null) given a prototype': regrown, + 'Object.create(plain object)': Object.create({ a: 1 }), + 'parsed JSON': JSON.parse('{"a":[1]}'), + 'function declaration': declared, + 'bound function': declared.bind(null), + 'arrow function': () => 1, + 'async function': async () => {}, + class: Square, + 'class instance': new Square(), + 'function with a null prototype': orphan, + map: new Map(), + date: new Date(0), + 'regular expression': /x/, + error: new TypeError('t'), + 'typed array': new Uint8Array(2), + promise: Promise.resolve(1), + number: 1, + string: 's', + boolean: false, + null: null, + undefined: undefined, + symbol: Symbol('s'), + bigint: 10n, +}; + +for (const [label, value] of Object.entries(values)) { + console.log(`${label}: Object ${value instanceof Object}, Function ${value instanceof Function}`); +} + +function describe(value) { + if (!(value instanceof Object)) return 'primitive or null-prototype'; + return value instanceof Function ? 'function' : 'object'; +} +console.log([nullProto, {}, describe, 0].map(describe).join(', ')); + +// Prototype chains through functions and objects, and Object.prototype +// itself, whose own prototype is null. +const base = function base() {}; +const fromFunction = {}; +Object.setPrototypeOf(fromFunction, base); +const throughObject = {}; +Object.setPrototypeOf(throughObject, fromFunction); +const nullRooted = function nullRooted() {}; +Object.setPrototypeOf(nullRooted, null); +const reparented = function reparented() {}; +Object.setPrototypeOf(reparented, fromFunction); +const chains = { + objectPrototype: Object.getPrototypeOf({}), + fromFunction, + throughObject, + nullRooted, + reparented, +}; +for (const [label, value] of Object.entries(chains)) { + console.log(`${label}: Object ${value instanceof Object}, Function ${value instanceof Function}`); +} diff --git a/tests/corpus/8100-typed-array-prototype-set-data-view.ts b/tests/corpus/8100-typed-array-prototype-set-data-view.ts new file mode 100644 index 000000000..23f3824cd --- /dev/null +++ b/tests/corpus/8100-typed-array-prototype-set-data-view.ts @@ -0,0 +1,30 @@ +// A borrowed TypedArray.prototype.set over a DataView target throws Node's +// TypeError after evaluating its arguments, and leaves the view unchanged. +const view = new DataView(new ArrayBuffer(4)); +const order: string[] = []; +function arg(label: string, value: T): T { + order.push(label); + return value; +} +try { + Uint8Array.prototype.set.call(arg("target", view), arg("source", [1, 2]), arg("offset", 1)); + console.log("set"); +} catch (e) { + console.log(e instanceof TypeError, (e as Error).message, order.join(",")); +} +console.log(view.getUint8(1), view.getUint8(2)); + +function fill(target: DataView | Uint8Array, bytes: number[]): string { + try { + Uint8Array.prototype.set.call(target, bytes); + return "set"; + } catch (e) { + return (e as Error).message; + } +} +console.log(fill(view, [7]), view.getUint8(0)); +const bytes = new Uint8Array(2); +console.log(fill(bytes, [7]), bytes[0]); +const buffer = Buffer.alloc(3); +Uint8Array.prototype.set.call(buffer, [4, 5], 1); +console.log(buffer.toString("hex")); diff --git a/tests/corpus/8101-url-host-and-path-parsing.js b/tests/corpus/8101-url-host-and-path-parsing.js new file mode 100644 index 000000000..6b04dd9ef --- /dev/null +++ b/tests/corpus/8101-url-host-and-path-parsing.js @@ -0,0 +1,103 @@ +// WHATWG URL host and path parsing: special hosts are percent-decoded and +// checked for forbidden domain code points (and read as IPv4 when they end +// in a number), opaque hosts and paths percent-encode the C0 control set, +// file: URLs take no port or credentials, empty hosts refuse credentials and +// ports, and host-less paths that start with "//" round-trip. Cases from the +// WPT url test data. +const parses = [ + ["file://example:1/", null], + ["non-special:opaque ?hi", null], + ["non-special:opaque #hi", null], + ["non-special:opaque \t\t \t#hi", null], + ["non-special:opaque \t\t #hi", null], + ["non-special:opaque\t\t \r #hi", null], + ["sc://faß.ExAmPlE/", null], + ["http://%30%78%63%30%2e%30%32%35%30.01", "http://other.com/"], + ["http://%30%78%63%30%2e%30%32%35%30.01%2e", "http://other.com/"], + ["sc://ñ.test/", null], + ["sc://%/", null], + ["sc://@/", null], + ["sc://te@s:t@/", null], + ["sc://:/", null], + ["sc://:12/", null], + ["x", "sc://ñ"], + ["wow:\uFFFF", null], + ["http://a\u007Fb/", null], + ["sc://\u0001\u0002\u0003\u0004\u0005\u0006\u0007\b\u000b\f\u000e\u000f\u0010\u0011\u0012\u0013\u0014\u0015\u0016\u0017\u0018\u0019\u001a\u001b\u001c\u001d\u001e\u001f\u007F!\"$%&'()*+,-.;=_`{}~/", null], + ["asdf://%43%7C/", null], + ["C|", "file://host/dir/file"], + ["C|", "file://host/D:/dir1/dir2/file"], + ["C|#", "file://host/dir/file"], + ["C|?", "file://host/dir/file"], + ["C|/", "file://host/dir/file"], + ["C|\n/", "file://host/dir/file"], + ["C|\\", "file://host/dir/file"], + ["C|/", "file://host/"], + ["file:C:/", "file://host/"], + ["sc://ñ", null], + ["sc://ñ?x", null], + ["sc://ñ#x", null], + ["#x", "sc://ñ"], + ["?x", "sc://ñ"], + ["", "non-spec:/..//p"], + ["path", "non-spec:/..//p"], + ["../path", "non-spec:/.//p"], + ["non-special://%E2%80%A0/", null], + ["non-special://H%4fSt/path", null], + ["non-special:cannot-be-a-base-url-\u0000\u0001\u001f\u001e~\u007F\u0080", null], + ["foo://!\"$%&'()*+,-.;=_`{}~/", null], + ["foo://host/ !\"$%&'()*+,-./:;<=>@[\\]^_`{|}~", null], + ["wss://host/ !\"$%&'()*+,-./:;<=>@[\\]^_`{|}~", null], + ["non-special:\u0000y", null], + ["non-special:x/\u0000y", null], + ["non-special:\uFFFFy", null], + ["non-special:x/\uFFFFy", null], + ["data://:443", null], + ["javascript://:443", null], + ["mailto://:443", null], + ["intent://:443", null], + ["urn://:443", null], + ["turn://:443", null], + ["stun://:443", null], +]; +function describe(url) { + return [url.href, url.protocol, url.username, url.password, url.host, url.hostname, url.port, url.pathname, url.search, url.hash].join(" | "); +} +for (const [input, base] of parses) { + let line; + try { + line = describe(base === null ? new URL(input) : new URL(input, base)); + } catch (e) { + line = `throws ${e.code}`; + } + console.log(JSON.stringify(input), JSON.stringify(base), "=>", line); +} + +const writes = [ + ["host", "sc://x/", "ß"], + ["host", "file://y/", "loc%41lhost"], + ["hostname", "file://y/", "loc%41lhost"], + ["pathname", "a:/", "\u0000\u0001\t\n\r\u001f !\"#$%&'()*+,-./09:;<=>?@AZ[\\]^_`az{|}~\u007F\u0080\u0081Éé"], + ["search", "data:space ?query", ""], + ["search", "sc:space ?query", ""], + ["search", "data:space ?query#fragment", ""], + ["search", "sc:space ?query#fragment", ""], + ["hash", "data:space #fragment", ""], + ["hash", "sc:space #fragment", ""], + ["hash", "data:space ?query#fragment", ""], + ["hash", "sc:space ?query#fragment", ""], +]; +for (const [name, href, value] of writes) { + const url = new URL(href); + if (name === "host") url.host = value; + else if (name === "hostname") url.hostname = value; + else if (name === "port") url.port = value; + else if (name === "pathname") url.pathname = value; + else if (name === "protocol") url.protocol = value; + else if (name === "username") url.username = value; + else if (name === "password") url.password = value; + else if (name === "search") url.search = value; + else if (name === "hash") url.hash = value; + else if (name === "href") url.href = value; + console.log(name, JSON.stringify(href), JSON.stringify(value), "=>", describe(url)); +} diff --git a/tests/corpus/8102-url-path-to-file-url-unc-hosts.js b/tests/corpus/8102-url-path-to-file-url-unc-hosts.js new file mode 100644 index 000000000..a96d1252e --- /dev/null +++ b/tests/corpus/8102-url-path-to-file-url-unc-hosts.js @@ -0,0 +1,36 @@ +// pathToFileURL(path, { windows: true }) parses a UNC servername with the URL +// host parser after dropping tabs and newlines and stopping at '#' and '?': +// forbidden host code points throw ERR_INVALID_URL, and the +// name is lowercased, percent-decoded and read as IPv4 when it ends in a +// number. "~" in a path is percent-encoded. +import url from "node:url"; +const paths = [ + "\\\\exa mple\\share\\file.txt", + "\\\\host name\\share\\file.txt", + "\\\\host@name\\share\\file.txt", + "\\\\host:name\\share\\file.txt", + "\\\\host[name\\share\\file.txt", + "\\\\host]name\\share\\file.txt", + "\\\\h", line, URL.canParse(input, base ?? undefined)); +} +const writes = [ + ["host", "https://x/", "ß"], + ["host", "https://example.com/", "a%C2%ADb"], + ["hostname", "https://example.com/", "a%C2%ADb"], + ["hostname", "https://example.com/", "BÜCHER.example"], +]; +for (const [name, href, value] of writes) { + const target = new URL(href); + if (name === "host") target.host = value; + else target.hostname = value; + console.log(name, JSON.stringify(value), "=>", describe(target)); +} +for (const path of ["\\\\münchen\\share\\f", "\\\\BÜCHER\\s\\f"]) { + console.log(JSON.stringify(path), url.pathToFileURL(path, { windows: true }).href); +} diff --git a/tests/corpus/8106-shared-typed-array-set.ts b/tests/corpus/8106-shared-typed-array-set.ts new file mode 100644 index 000000000..7e20aad72 --- /dev/null +++ b/tests/corpus/8106-shared-typed-array-set.ts @@ -0,0 +1,97 @@ +// TypedArray.prototype.set where the source, the target or both are views of +// a SharedArrayBuffer: same element kinds (bytes copied as is, NaN payloads +// included), different kinds (converted per element), offsets, overlapping +// ranges of one buffer, Buffer targets and out-of-bounds offsets. +function bits(label: string, buffer: ArrayBufferLike): void { + const words = new Uint32Array(buffer); + const out: string[] = []; + for (let i = 0; i < words.length; i++) out.push(words[i]!.toString(16)); + console.log(label, out.join(",")); +} + +const shared = new SharedArrayBuffer(32); +const bytes = new Uint8Array(shared); + +// Private source into a shared target, then shared into shared and shared into private. +bytes.set(new Uint8Array([1, 2, 3, 4, 5]), 2); +console.log("private into shared", bytes.subarray(0, 8).join(",")); +const other = new Uint8Array(new SharedArrayBuffer(8)); +other.set(bytes.subarray(2, 7)); +console.log("shared into shared", other.join(",")); +const local = new Uint8Array(12); +local.set(other, 3); +console.log("shared into private", local.join(",")); + +// Overlapping ranges of the same buffer, forward and backward. +const overlap = new Uint8Array(new SharedArrayBuffer(10)); +overlap.set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); +overlap.set(overlap.subarray(0, 6), 3); +console.log("overlap forward", overlap.join(",")); +overlap.set(overlap.subarray(4, 10), 1); +console.log("overlap backward", overlap.join(",")); + +// Same kind, wider elements: a signaling NaN keeps its bits. +const floatBits = new Uint32Array(new SharedArrayBuffer(8)); +floatBits[0] = 0x7fa00001; +floatBits[1] = 0xffc00002; +const floatsShared = new Float32Array(floatBits.buffer); +const floatsLocal = new Float32Array(2); +floatsLocal.set(floatsShared); +bits("f32 shared into private", floatsLocal.buffer); +const floatsBack = new Float32Array(new SharedArrayBuffer(8)); +floatsBack.set(floatsLocal); +bits("f32 private into shared", floatsBack.buffer); + +const doubleBits = new Uint32Array(new SharedArrayBuffer(16)); +doubleBits[0] = 5; +doubleBits[1] = 0x7ff40000; +doubleBits[2] = 0; +doubleBits[3] = 0xc0090000; +const doublesShared = new Float64Array(doubleBits.buffer); +const doublesCopy = new Float64Array(new SharedArrayBuffer(16)); +doublesCopy.set(doublesShared); +bits("f64 shared into shared", doublesCopy.buffer); +console.log("f64 values", doublesCopy[1]); + +const signed = new Int16Array(new SharedArrayBuffer(8)); +signed.set(new Int16Array([-32768, -1, 32767]), 1); +console.log("i16 private into shared", signed.join(",")); + +// Different kinds convert each element. +const clamped = new Uint8ClampedArray(new SharedArrayBuffer(4)); +clamped.set(new Int16Array([300, -5, 128, 255])); +console.log("i16 into shared clamped", clamped.join(",")); +const wrapped = new Uint8Array(new SharedArrayBuffer(4)); +wrapped.set(new Int16Array(signed.buffer, 2, 3)); +console.log("shared i16 into shared u8", wrapped.join(",")); +const narrowed = new Int8Array(4); +narrowed.set(new Float64Array(doublesShared.buffer, 8, 1)); +narrowed.set(new Uint8Array([200, 127]), 2); +console.log("f64 and u8 into i8", narrowed.join(",")); +const fromClamped = new Uint8Array(new SharedArrayBuffer(4)); +fromClamped.set(clamped); +console.log("shared clamped into shared u8", fromClamped.join(",")); + +// Buffer views over shared memory take the same paths. +const buf = Buffer.from(shared, 16, 8); +buf.set(Buffer.from("scriptc"), 1); +console.log("buffer", buf.toString("latin1", 1, 8), bytes[17], bytes[23]); +const into = Buffer.alloc(4); +into.set(buf.subarray(1, 4)); +console.log("buffer into private", into.toString("hex")); + +// Offsets past the end throw before writing. +for (const offset of [-1, 30, 31]) { + try { + bytes.set(new Uint8Array([9, 9]), offset); + console.log("set at", offset, bytes[30], bytes[31]); + } catch (error) { + console.log("set at", offset, (error as Error).name, (error as Error).message); + } +} +try { + floatsBack.set(new Float32Array(3)); +} catch (error) { + console.log("too long", (error as Error).name, (error as Error).message); +} +console.log("unchanged", floatsBack.join(",")); diff --git a/tests/corpus/8107-shared-typed-array-set-workers.ts b/tests/corpus/8107-shared-typed-array-set-workers.ts new file mode 100644 index 000000000..6be32668b --- /dev/null +++ b/tests/corpus/8107-shared-typed-array-set-workers.ts @@ -0,0 +1,63 @@ +// TypedArray.prototype.set on SharedArrayBuffer views in a program with +// workers: the same cases on the main thread and in a worker (same element +// kinds with NaN payloads, overlapping ranges of one buffer, converted +// kinds), and a worker's set into a buffer that the main thread reads after +// a message. +import { isMainThread, parentPort, Worker, workerData } from "node:worker_threads"; + +interface Job { + shared: SharedArrayBuffer; +} + +function hex(buffer: ArrayBufferLike): string { + const words = new Uint32Array(buffer); + const out: string[] = []; + for (let i = 0; i < words.length; i++) out.push(words[i]!.toString(16)); + return out.join(","); +} + +function cases(where: string): string[] { + const lines: string[] = []; + const bytes = new Uint8Array(new SharedArrayBuffer(12)); + bytes.set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]); + bytes.set(bytes.subarray(0, 8), 4); + lines.push(`${where} overlap forward ${bytes.join(",")}`); + bytes.set(bytes.subarray(5, 12), 0); + lines.push(`${where} overlap backward ${bytes.join(",")}`); + + const floatBits = new Uint32Array(new SharedArrayBuffer(8)); + floatBits[0] = 0x7f800001; + floatBits[1] = 0x7fc12345; + const local = new Float32Array(2); + local.set(new Float32Array(floatBits.buffer)); + const back = new Float32Array(new SharedArrayBuffer(12)); + back.set(local, 1); + lines.push(`${where} f32 ${hex(local.buffer)} ${hex(back.buffer)}`); + + const halves = new Uint16Array(new SharedArrayBuffer(8)); + halves.set(new Uint16Array([65535, 1, 2]), 1); + const signed = new Int16Array(4); + signed.set(halves); + const clamped = new Uint8ClampedArray(new SharedArrayBuffer(4)); + clamped.set(signed); + lines.push(`${where} kinds ${halves.join(",")} ${signed.join(",")} ${clamped.join(",")}`); + return lines; +} + +if (isMainThread) { + for (const line of cases("main")) console.log(line); + const shared = new SharedArrayBuffer(16); + const job: Job = { shared }; + const worker = new Worker(new URL(import.meta.url), { workerData: job }); + worker.on("message", (message: string) => { + console.log(message); + console.log("worker wrote", new Uint8Array(shared).join(",")); + void worker.terminate(); + }); +} else { + const { shared } = workerData as Job; + const target = new Uint8Array(shared); + target.set(new Uint8Array([9, 8, 7]), 2); + target.set(target.subarray(2, 5), 10); + parentPort!.postMessage(cases("worker").join("\n")); +} diff --git a/tests/corpus/8110-builtin-prototype-own-members.js b/tests/corpus/8110-builtin-prototype-own-members.js new file mode 100644 index 000000000..9ebdefc02 --- /dev/null +++ b/tests/corpus/8110-builtin-prototype-own-members.js @@ -0,0 +1,73 @@ +"use strict"; +// Own members of the builtin prototypes and the constructor links that +// reach them, read in untyped code: Object.prototype has its own +// isPrototypeOf, hasOwnProperty and constructor; a plain object's +// constructor is Object, whose prototype is Object.prototype; an array's +// constructor is Array, whose prototype owns its methods and constructor. + +const has = (object, key) => Object.prototype.hasOwnProperty.call(object, key); + +const objectKeys = ["constructor", "hasOwnProperty", "isPrototypeOf", "propertyIsEnumerable", "toString", "toLocaleString", "valueOf", "__proto__", "__defineGetter__", "__lookupSetter__", "missing", "length"]; +for (const key of objectKeys) console.log("Object.prototype", key, has(Object.prototype, key), Object.hasOwn(Object.prototype, key)); +console.log(Object.getOwnPropertyNames(Object.prototype).join(",")); +console.log(Object.prototype.hasOwnProperty("isPrototypeOf"), Object.prototype.hasOwnProperty("nope")); + +const arrayKeys = ["constructor", "push", "map", "length", "at", "hasOwnProperty", "isPrototypeOf", "missing"]; +for (const key of arrayKeys) console.log("Array.prototype", key, has(Array.prototype, key)); +const arrayNames = Object.getOwnPropertyNames(Array.prototype); +console.log(arrayNames.length, arrayNames.includes("constructor"), arrayNames.includes("length"), arrayNames.includes("hasOwnProperty")); + +/** @param {any} value */ +function links(label, value) { + const ctor = value.constructor; + const prototype = ctor.prototype; + console.log(label, typeof ctor, ctor.name, typeof prototype, prototype === Object.getPrototypeOf(value), has(prototype, "constructor"), has(prototype, "isPrototypeOf")); +} +links("object", {}); +links("object-with-keys", { a: 1, b: [2] }); +links("parsed", JSON.parse('{"x":{"y":1}}')); +links("nested", JSON.parse('{"x":{"y":1}}').x); +links("array", []); +links("array-of", [1, 2, 3]); +const error = new Error("boom"); +console.log("error", typeof error.constructor, error.constructor.name, error.constructor.prototype === Error.prototype, has(error.constructor.prototype, "isPrototypeOf")); + +/** @param {any} value */ +const ctorOf = (value) => value.constructor; +console.log(ctorOf({}) === Object, ctorOf({}).prototype === Object.prototype, ctorOf([]) === Array, ctorOf([]).prototype === Array.prototype); +console.log(Object.prototype.constructor === Object, Object.prototype.constructor.prototype === Object.prototype); +console.log(Object.getPrototypeOf({}) === Object.prototype, has(Object.getPrototypeOf({ k: 1 }), "isPrototypeOf")); + +const O = ctorOf({}); +const descriptor = Object.getOwnPropertyDescriptor(O, "prototype"); +console.log(typeof O.prototype, O.prototype === Object.prototype, descriptor.writable, descriptor.enumerable, descriptor.configurable); +const A = ctorOf([]); +console.log(A.prototype === Array.prototype, Object.keys(A).length, A.name, A.length); +const E = ctorOf(error); +console.log(E.prototype === Error.prototype, E.prototype.name, E.name); + +// A check shaped like zod's isPlainObject: the constructor's prototype must +// be an object that has its own isPrototypeOf. +/** @param {unknown} o */ +function isPlainObject(o) { + if (typeof o !== "object" || o === null || Array.isArray(o)) return false; + const ctor = /** @type {any} */ (o).constructor; + if (ctor === undefined) return true; + if (typeof ctor !== "function") return true; + const prot = ctor.prototype; + if (typeof prot !== "object" || prot === null || Array.isArray(prot)) return false; + return Object.prototype.hasOwnProperty.call(prot, "isPrototypeOf"); +} +class Point { + constructor() { + this.x = 1; + } +} +const samples = [{}, { a: 1 }, JSON.parse("{}"), Object.create(null), [], new Point(), new Error("e"), { constructor: 5 }, { constructor: undefined }, "s", 1, null]; +console.log(samples.map((sample) => isPlainObject(sample)).join(",")); + +// A constructor stored on Array.prototype stays one own name, after length. +function Replacement() {} +Array.prototype.constructor = Replacement; +const replacedNames = Object.getOwnPropertyNames(Array.prototype); +console.log(replacedNames.slice(0, 2).join(","), replacedNames.filter((name) => name === "constructor").length, has(Array.prototype, "constructor")); diff --git a/tests/corpus/8111-plain-object-check-unknown.ts b/tests/corpus/8111-plain-object-check-unknown.ts new file mode 100644 index 000000000..267f3d65e --- /dev/null +++ b/tests/corpus/8111-plain-object-check-unknown.ts @@ -0,0 +1,54 @@ +// A plain-object check over unknown values, the way schema libraries +// accept records: the value's constructor must be absent, not a function, +// or one whose prototype has its own isPrototypeOf. Values that crossed +// JSON.parse and object literals pass; arrays, class instances and errors +// fail. + +function isObject(data: unknown): data is Record { + return typeof data === "object" && data !== null && !Array.isArray(data); +} + +function isPlainObject(o: unknown): o is Record { + if (!isObject(o)) return false; + const ctor: unknown = o.constructor; + if (ctor === undefined) return true; + if (typeof ctor !== "function") return true; + const prot: unknown = (ctor as { prototype?: unknown }).prototype; + if (!isObject(prot)) return false; + if (Object.prototype.hasOwnProperty.call(prot, "isPrototypeOf") === false) return false; + return true; +} + +class Point { + x = 1; +} + +const inputs: [string, unknown][] = [ + ["literal", { a: 1 }], + ["empty", {}], + ["parsed", JSON.parse('{"k":[1,2],"m":{"n":true}}')], + ["parsed-nested", (JSON.parse('{"m":{"n":true}}') as { m: unknown }).m], + ["null-proto", Object.create(null)], + ["array", [1, 2]], + ["instance", new Point()], + ["error", new Error("x")], + ["ctor-number", { constructor: 3 }], + ["string", "s"], + ["null", null], +]; + +for (const [label, value] of inputs) { + console.log(label, isPlainObject(value)); +} + +const records: Record = {}; +for (const [label, value] of inputs) { + if (!isPlainObject(value)) continue; + records[label] = Object.keys(value).length; +} +console.log(JSON.stringify(records)); + +const proto: unknown = Object.getPrototypeOf({}); +console.log(proto === Object.prototype, Object.prototype.hasOwnProperty.call(proto, "isPrototypeOf")); +console.log(Object.hasOwn(Object.prototype, "hasOwnProperty"), Object.hasOwn(Object.prototype, "nope")); +console.log(Object.getOwnPropertyNames(Object.prototype).length); diff --git a/tests/corpus/8112-error-factory-own-key-order.js b/tests/corpus/8112-error-factory-own-key-order.js new file mode 100644 index 000000000..3e7cf277a --- /dev/null +++ b/tests/corpus/8112-error-factory-own-key-order.js @@ -0,0 +1,73 @@ +"use strict"; +// Errors built by a constructor factory, the way zod 4 builds ZodError: a +// class extending the parent passed in, then an initializer that assigns +// `name`, defines non-enumerable fields and an enumerable `message` +// accessor, and a second initializer that reassigns `name`. JSON and +// Object.keys list `name` before `message`, as they were created, and +// `name` holds the last assignment. + +const messageDescriptor = { + get() { + return this.state.message ?? JSON.stringify(this.state.issues); + }, + set(value) { + this.state.message = value; + }, + enumerable: true, + configurable: true, +}; + +function factory(name, initializer, parent) { + const Parent = parent ?? Object; + class Definition extends Parent {} + Object.defineProperty(Definition, "name", { value: name }); + function construct(def) { + const instance = parent ? new Definition() : this; + Object.defineProperty(instance, "state", { value: { issues: def }, enumerable: false }); + initializer(instance, def); + return instance; + } + Object.defineProperty(construct, "init", { value: initializer }); + Object.defineProperty(construct, "name", { value: name }); + return construct; +} + +const baseInit = (instance, def) => { + instance.name = "$BaseError"; + Object.defineProperty(instance, "issues", { value: def, enumerable: false }); + Object.defineProperty(instance, "message", messageDescriptor); +}; +const BaseError = factory("$BaseError", baseInit, Error); +const ReportError = factory( + "ReportError", + (instance, def) => { + BaseError.init(instance, def); + instance.name = "ReportError"; + }, + Error, +); + +const report = new ReportError([{ code: "too_small", path: ["title"] }]); +console.log(report.name, report instanceof Error, Object.keys(report).join(",")); +console.log(JSON.stringify(report)); +console.log(JSON.stringify({ success: false, error: report })); +report.message = "replaced"; +console.log(JSON.stringify(report), report.message); +report.name = "Renamed"; +console.log(JSON.stringify(report), report.name); + +const base = new BaseError([1, 2]); +console.log(base.name, Object.keys(base).join(","), JSON.stringify(base)); + +// The same steps on a class extending Error directly. +function build(flag) { + class Direct extends Error {} + const instance = flag ? new Direct() : {}; + instance.name = "First"; + Object.defineProperty(instance, "message", messageDescriptor); + Object.defineProperty(instance, "state", { value: { issues: ["i"] }, enumerable: false }); + instance.name = "Second"; + return instance; +} +const direct = build(true); +console.log(Object.keys(direct).join(","), JSON.stringify(direct), direct.name); diff --git a/tests/corpus/8113-error-class-field-before-name.js b/tests/corpus/8113-error-class-field-before-name.js new file mode 100644 index 000000000..e10a9456b --- /dev/null +++ b/tests/corpus/8113-error-class-field-before-name.js @@ -0,0 +1,78 @@ +"use strict"; +// Error subclasses with a property bag that a later dynamic definition or +// write commits. Object.keys and JSON keep the order in which an instance +// created its properties: a class field before an own `name` that is +// assigned after construction, a `name` a constructor assigns before a +// later property, and a `message` that Error's constructor created before +// all of them. + +const messageDescriptor = { + get() { + return "described"; + }, + enumerable: true, + configurable: true, +}; + +// A factory-built class: the field, then an initializer that assigns `name` +// and defines `message`. +function factory(initializer) { + class Definition extends Error { + code = "E_FIELD"; + } + return function construct() { + const instance = new Definition(); + initializer(instance); + return instance; + }; +} +const Fielded = factory((instance) => { + instance.name = "Fielded"; + Object.defineProperty(instance, "message", messageDescriptor); +}); +const fielded = new Fielded(); +console.log(Object.keys(fielded).join(","), JSON.stringify(fielded)); +fielded.extra = 1; +console.log(Object.keys(fielded).join(","), JSON.stringify(fielded)); +fielded.name = "Renamed"; +console.log(Object.keys(fielded).join(","), JSON.stringify(fielded), fielded.name); + +// A class whose constructor passes a message to Error and then assigns +// `name`; the message is redefined after construction. +class Direct extends Error { + code = "E_DIRECT"; + constructor() { + super("constructed"); + this.name = "Direct"; + } +} +function redefine(instance) { + Object.defineProperty(instance, "message", messageDescriptor); + instance.extra = 2; + return instance; +} +const direct = redefine(new Direct()); +console.log(Object.keys(direct).join(","), JSON.stringify(direct), direct.name); + +// Constructors that assign `name` and then another property, which their +// instances create in that order, with and without a message passed to +// Error, before the message is redefined. +class Named extends Error { + constructor() { + super(); + this.name = "Named"; + this.detail = "named"; + } +} +const named = redefine(new Named()); +console.log(Object.keys(named).join(","), JSON.stringify(named), named.name); + +class NamedMessage extends Error { + constructor() { + super("constructed"); + this.name = "NamedMessage"; + this.detail = "named"; + } +} +const namedMessage = redefine(new NamedMessage()); +console.log(Object.keys(namedMessage).join(","), JSON.stringify(namedMessage), namedMessage.name); diff --git a/tests/corpus/8114-record-field-slices.ts b/tests/corpus/8114-record-field-slices.ts new file mode 100644 index 000000000..110cd3acb --- /dev/null +++ b/tests/corpus/8114-record-field-slices.ts @@ -0,0 +1,139 @@ +// Slices stored in record fields: records built from a literal or a factory +// whose fields are only read for their length, comparisons and keyword +// searches, next to records that are kept after the iteration that built +// them (pushed into an array, held in an outer binding, captured by a +// closure, read into a kept array, returned from a helper, captured by a +// closure the factory stores beside it). Slices of non-ASCII text and slices +// longer than a short token are included. +interface Token { + kind: string; + text: string; + pos: number; +} + +const KEYWORDS = ["let", "const", "if", "else", "return"]; + +function token(kind: string, text: string, pos: number): Token { + return { kind, text, pos }; +} + +interface LazyToken { + text: string; + upper: () => string; +} + +function lazyToken(text: string): LazyToken { + return { text, upper: () => text.toUpperCase() }; +} + +function classify(text: string): string { + return KEYWORDS.includes(text) ? "keyword" : "ident"; +} + +function weight(t: Token): number { + return t.kind === "keyword" ? t.text.length * 3 : t.text.length + (t.pos & 7); +} + +function isReturn(t: Token): boolean { + return t.text === "return"; +} + +function textOf(t: Token): string { + return t.text; +} + +/** Calls `visit` with the start and end of every run of word characters. */ +function words(source: string, visit: (start: number, end: number) => void): void { + let start = -1; + for (let i = 0; i <= source.length; i++) { + const c = i < source.length ? source.charCodeAt(i) : 32; + const word = c > 32 && c !== 44 && c !== 59; + if (word) { + if (start < 0) start = i; + } else if (start >= 0) { + visit(start, i); + start = -1; + } + } +} + +function lentTokens(source: string): string { + let sum = 0; + let returns = 0; + let longest = 0; + for (let i = 0, start = -1; i <= source.length; i++) { + const c = i < source.length ? source.charCodeAt(i) : 32; + if (c > 32 && c !== 44 && c !== 59) { + if (start < 0) start = i; + continue; + } + if (start < 0) continue; + const text = source.slice(start, i); + const t = token(classify(text), text, start); + sum += weight(t); + if (isReturn(t)) returns++; + const literal = { text, pos: start }; + if (literal.text.length > longest) longest = literal.text.length; + start = -1; + } + return `lent ${sum} ${returns} ${longest}`; +} + +function keptTokens(source: string): string { + const kept: Token[] = []; + const texts: string[] = []; + const later: (() => string)[] = []; + const returned: string[] = []; + const uppers: (() => string)[] = []; + let lazyLength = 0; + let last: Token | null = null; + for (let i = 0, start = -1; i <= source.length; i++) { + const c = i < source.length ? source.charCodeAt(i) : 32; + if (c > 32 && c !== 44 && c !== 59) { + if (start < 0) start = i; + continue; + } + if (start < 0) continue; + const text = source.slice(start, i); + const pushed = token(classify(text), text, start); + kept.push(pushed); + const held = { kind: "held", text, pos: start }; + last = held; + const read = token("read", source.slice(start, i), start); + texts.push(read.text); + const captured = { kind: "captured", text: source.slice(start, i), pos: start }; + later.push(() => captured.text); + const viaHelper = token("helper", source.slice(start, i), start); + returned.push(textOf(viaHelper)); + const lazyText = source.slice(start, i); + const lazy = lazyToken(lazyText); + lazyLength += lazy.text.length; + uppers.push(lazy.upper); + start = -1; + } + return [ + `kept ${kept.map((t) => `${t.kind}:${t.text}`).join(" ")}`, + `last ${last === null ? "none" : last.text}`, + `texts ${texts.join("|")}`, + `later ${later.map((f) => f()).join("|")}`, + `returned ${returned.join("|")}`, + `upper ${lazyLength} ${uppers.map((f) => f()).join("|")}`, + ].join("\n"); +} + +const ascii = "let total = count; if (total) return total; else return 0;"; +const mixed = "lét naïve = café; return 東京, x"; +const long = `const ${"identifier".repeat(12)} = ${"v".repeat(150)};`; + +for (const source of [ascii, mixed, long]) { + console.log(lentTokens(source)); + console.log(keptTokens(source)); +} + +let count = 0; +words(ascii, (start, end) => { + const text = ascii.slice(start, end); + const t = token(classify(text), text, start); + count += weight(t); +}); +console.log("callback", count); diff --git a/tests/corpus/8115-error-prototype-identity.js b/tests/corpus/8115-error-prototype-identity.js new file mode 100644 index 000000000..5c98e1209 --- /dev/null +++ b/tests/corpus/8115-error-prototype-identity.js @@ -0,0 +1,55 @@ +"use strict"; +// Object.getPrototypeOf of an Error is Error.prototype, whose own prototype +// is Object.prototype: with or without a message, after its name is +// reassigned, and when it was thrown and caught. + +const describe = (label, value) => { + const proto = Object.getPrototypeOf(value); + console.log(label, proto === Error.prototype, proto === Object.prototype, Object.getPrototypeOf(proto) === Object.prototype); +}; + +describe("message", new Error("boom")); +describe("empty", new Error()); +const renamed = new Error("x"); +renamed.name = "Custom"; +describe("renamed", renamed); +const coded = new Error("y"); +coded.code = "E_CODE"; +describe("coded", coded); +try { + throw new Error("thrown"); +} catch (error) { + describe("caught", error); +} + +// Walks up the chain, as error serializers and type sniffers do. +/** @param {unknown} value */ +function chainLength(value) { + let length = 0; + for (let current = Object.getPrototypeOf(value); current !== null; current = Object.getPrototypeOf(current)) length++; + return length; +} +console.log(chainLength(new Error("a")), chainLength({}), chainLength([])); + +/** @param {unknown} value */ +function kind(value) { + if (typeof value !== "object" || value === null) return typeof value; + const proto = Object.getPrototypeOf(value); + if (proto === Object.prototype || proto === null) return "plain"; + if (proto === Error.prototype) return "error"; + if (proto === Array.prototype) return "array"; + return "other"; +} +const values = [new Error("e"), {}, Object.create(null), [], JSON.parse('{"a":1}'), "s"]; +console.log(values.map(kind).join(",")); +const proto = Object.getPrototypeOf(new Error("m")); +console.log(proto.name, JSON.stringify(proto.message), Object.prototype.hasOwnProperty.call(proto, "name")); + +// After Object.setPrototypeOf, the prototype is the object that was set. +const custom = {}; +const moved = Object.setPrototypeOf(new Error("moved"), custom); +console.log(Object.getPrototypeOf(moved) === custom, Object.getPrototypeOf(moved) === Error.prototype); +const named = { kind: "named" }; +const movedNamed = new Error(); +Object.setPrototypeOf(movedNamed, named); +console.log(Object.getPrototypeOf(movedNamed) === named, Object.getPrototypeOf(movedNamed).kind); diff --git a/tests/corpus/8116-destructured-results.ts b/tests/corpus/8116-destructured-results.ts new file mode 100644 index 000000000..71c884d49 --- /dev/null +++ b/tests/corpus/8116-destructured-results.ts @@ -0,0 +1,146 @@ +// Small functions returning a tuple or record that the caller destructures +// at once: string slices and numbers in the fields, a parameter that may be +// undefined at run time (a missing array element passed with `!`), fields +// the caller does not read, object destructuring in another order than the +// literal, arguments and fields with side effects, non-ASCII and astral +// receivers, slices that escape into arrays, closures and returns, bodies +// with loops and locals, a recursive producer, sites inside switch, try and +// loops, and an async caller. +const log: string[] = []; +function note(label: string, value: number): number { + log.push(label); + return value; +} + +function split(text: string, at: number): [string, string] { + return [text.slice(0, at), text.slice(at)]; +} + +function divmod(a: number, b: number): [number, number] { + return [Math.floor(a / b), a % b]; +} + +interface Name { + first: string; + last: string; +} + +function name(full: string): Name { + const space = full.indexOf(" "); + return { first: full.slice(0, space), last: full.slice(space + 1) }; +} + +function trimmed(text: string): [number, number] { + let start = 0; + let end = text.length; + while (start < end && text.charCodeAt(start) === 32) start++; + while (end > start && text.charCodeAt(end - 1) === 32) end--; + return [start, end]; +} + +function noted(a: number, b: number): [number, number] { + return [note("first field", a), note("second field", b)]; +} + +function digits(n: number): [number, string] { + if (n < 10) return [1, String(n)]; + const [count, rest] = digits(Math.floor(n / 10)); + return [count + 1, rest + String(n % 10)]; +} + +const texts = ["alpha", "be", "", "x", "héllo", "a😀b"]; +let total = 0; +const kept: string[] = []; +const later: (() => string)[] = []; +for (let i = 0; i < 40; i++) { + const [q, r] = divmod(i * 31 - 200, 7); + const [head, tail] = split(texts[i % texts.length]!, r % 4); + total += q + r + head.length * 2 + tail.length; + if (i % 9 === 0) kept.push(head); + if (i % 11 === 0) later.push(() => tail); +} +console.log("total", total); +console.log("kept", kept.join("|")); +console.log("later", later.map((f) => f()).join("|")); + +for (const [a, b] of [ + [7, 2], + [-7, 2], + [7.5, 2], + [1, 0], + [-1, 0], + [0, -3], +]) { + const [q, r] = divmod(a!, b!); + console.log("divmod", a, b, q, r, Object.is(r, -0)); +} +const [only] = divmod(17, 5); +console.log("only", only); + +const { last, first } = name("Ada Lovelace"); +console.log("name", first, last); +const { first: alone } = name("Grace Hopper"); +console.log("alone", alone); + +const [left, right] = trimmed(" padded text "); +console.log("trimmed", left, right); + +const [x, y] = noted(note("first argument", 1), note("second argument", 2)); +console.log("order", x, y, log.join(", ")); + +const [count, spelled] = digits(90210); +console.log("digits", count, spelled); + +for (const text of ["héllo", "a😀b", "日本語"]) { + for (const at of [0, 1, 2, 3, -1, 10]) { + const [head, tail] = split(text, at); + console.log("unicode", text, at, head, tail, head.length, tail.length); + } +} + +function tails(): string[] { + const out: string[] = []; + for (const text of texts) { + const [, tail] = split(text, 1); + out.push(tail); + } + return out; +} +console.log("tails", tails().join("|")); + +function lastTail(text: string): string { + const [, tail] = split(text, 2); + return tail; +} +console.log("lastTail", lastTail("returned")); + +for (const kind of ["one", "two", "other"]) { + switch (kind) { + case "one": { + const [h, t] = split("switch", 3); + console.log("switch", kind, h, t); + break; + } + case "two": { + const [q, r] = divmod(-9, 4); + console.log("switch", kind, q, r); + break; + } + default: + console.log("switch", kind); + } +} + +try { + const [head, tail] = split(texts[99]!, 1); + console.log("unreachable", head, tail); +} catch (error) { + console.log("missing", (error as Error).name, (error as Error).message); +} + +async function asyncCaller(text: string): Promise { + await Promise.resolve(); + const [head, tail] = split(text, 3); + return `${head}-${tail}`; +} +asyncCaller("awaited").then((value) => console.log("async", value)); diff --git a/tests/corpus/8117-scalar-slice-bounds.ts b/tests/corpus/8117-scalar-slice-bounds.ts new file mode 100644 index 000000000..412221368 --- /dev/null +++ b/tests/corpus/8117-scalar-slice-bounds.ts @@ -0,0 +1,25 @@ +// slice() and substring() results read only for their length and code +// units, so no string is built: ASCII, non-ASCII, astral and empty +// receivers, with integer, negative, fractional, -0, NaN, infinite and huge +// bounds, one bound or two, and substring's swapped bounds. +const receivers = ["", "a", "hello world", "héllo", "a😀b", "日本語テキスト"]; +const bounds = [0, 1, 2, -1, -2, -0, 0.5, -0.5, 3.7, -3.7, NaN, Infinity, -Infinity, 1e20, -1e20]; + +function sliceFacts(text: string, start: number, end: number): string { + const both = text.slice(start, end); + const from = text.slice(start); + const sub = text.substring(start, end); + const subFrom = text.substring(start); + return [both.length, both.charCodeAt(0), from.length, from.charCodeAt(from.length - 1), sub.length, sub.charCodeAt(0), subFrom.length].join(","); +} + +let lines = 0; +for (const text of receivers) { + const facts: string[] = []; + for (const start of bounds) { + for (const end of bounds) facts.push(sliceFacts(text, start, end)); + } + console.log(JSON.stringify(text), facts.length, facts.join(" ")); + lines++; +} +console.log("receivers", lines); diff --git a/tests/corpus/8118-replacer-params.ts b/tests/corpus/8118-replacer-params.ts new file mode 100644 index 000000000..14579ec84 --- /dev/null +++ b/tests/corpus/8118-replacer-params.ts @@ -0,0 +1,43 @@ +// Inline replace callbacks whose parameters after the match are left +// untyped: captures every match sets, captures that can be unset (in an +// alternation, an optional or zero-repeat group, a lookahead), the offset, +// the subject and the groups object, reassigned parameters, named groups, +// patterns with classes, escapes and lookbehind, function expressions and +// replaceAll. +const lines = ["id7,tag12;tag3", "x9 y10", "", "naïve 😀 42", "a-b-c"]; +for (const line of lines) { + console.log(line.replace(/tag(\d+)/g, (_m, d) => String(Number(d) + 1))); + console.log(line.replace(/(\w)(\d+)/g, (m, letter, digits, offset, subject) => + `[${letter}:${digits.length}@${offset}/${subject.length}]`)); + console.log(line.replace(/(a)|(\d)/g, (m, a, digit) => `<${typeof a},${typeof digit}>`)); + console.log(line.replace(/(x)?(\d)/g, (m, x, d) => `${x === undefined ? "-" : x}${d}`)); + console.log(line.replace(/(?:(t)a)*g/g, (m, t, offset) => `{${t}|${offset}}`)); + console.log(line.replace(/(?=(\d))\d/g, (m, ahead, offset) => `${ahead}${offset}`)); + console.log(line.replace(/(?\d)(\d)?/g, (m, n, rest, offset, subject, groups) => + `${n}${rest ?? "_"}${offset}${typeof subject}${JSON.stringify(groups)}`)); + console.log(line.replace(/([\(\)\[\]]|[a-c])(-)/g, (m, c, dash) => c.toUpperCase() + dash.length)); + console.log(line.replace(/(?<=-)(\w)/g, (m, c, offset) => c + offset.toFixed(1))); + console.log(line.replaceAll(/(\d)/g, function (m, d, offset) { + return `${d * 2}:${offset + 1}`; + })); + console.log(line.replace(/(\d)/g, (m, d) => { + d = d.length > 0 ? 5 : 6; + return String(d + 1); + })); + console.log(line.replace(/(\d)\1?/g, (m, d, offset, subject) => `${d}${offset}${subject === line}`)); + console.log(line.replace(/(.)/u, (m, c, offset, subject, extra) => `${c.codePointAt(0)}${offset}${extra}`)); + console.log(line.replace(/(\d){0,2}x/g, (m, d) => `${d}`)); + console.log(line.replace(/(\w)(\w)/g, (m, a, b, offset) => + [a.toUpperCase(), b.toLowerCase(), a.trim(), a.charCodeAt(0), b.codePointAt(0), a.slice(0, 1), + b.substring(0), a.repeat(2), b.padStart(3, "."), a.padEnd(2, "!"), b.includes("1"), + a.startsWith("t"), b.endsWith("g"), a.indexOf("a"), a.length, parseInt(b, 36), + parseFloat(b), Number(b), String(offset), offset - 1, offset * 2, offset % 3, offset < 4, + a === b, a !== "x", a + offset, offset + 1, `${offset}`].join(","))); + console.log(line.replace(/(\d+)/g, (m, d) => (d))); + console.log(line.replace(/(\d+)/g, function (m, d) { return d + d; })); +} +let count = 0; +console.log("tag1 tag2".replace(/tag(\d)/g, (m, d) => { + count += Number(d); + return d.repeat(2); +}), count); diff --git a/tests/corpus/8120-inspect-custom-nested.ts b/tests/corpus/8120-inspect-custom-nested.ts new file mode 100644 index 000000000..283f9e9cc --- /dev/null +++ b/tests/corpus/8120-inspect-custom-nested.ts @@ -0,0 +1,67 @@ +// util.inspect and util.format calls inside [util.inspect.custom] methods +// start from their own defaults: no indentation from the outer render, the +// outer circular references kept, and no colors from the outer options. +import * as util from "node:util"; + +class Wide { + v: number; + constructor(v: number) { + this.v = v; + } + [util.inspect.custom](): string { + return util.inspect({ a: { b: [1, 2, { c: this.v }] }, s: "x".repeat(50), t: "y".repeat(30) }); + } +} +console.log({ outer: { inner: new Wide(1) } }); +type Holder = { first: Wide; list: Wide[]; self?: Holder }; +const holder: Holder = { first: new Wide(2), list: [new Wide(3)] }; +holder.self = holder; +console.log(holder); +console.log(util.inspect({ deep: { deeper: { c: new Wide(4) } } })); + +const shared = { k: 1 }; +class Shares { + [util.inspect.custom](): string { + return "S<" + util.inspect(shared) + ">"; + } +} +console.log({ a: shared, c: new Shares(), b: shared }); +type Loop = { c: Shares; me?: Loop; again?: Loop }; +const loop: Loop = { c: new Shares() }; +loop.me = loop; +loop.again = loop; +console.log(loop); + +class Formats { + [util.inspect.custom](): string { + return util.format("%s|%O|%o|%j|%d", "a", "b", "c", "d", 5); + } +} +console.log(JSON.stringify(util.inspect(new Formats(), { colors: true }))); +console.log(JSON.stringify(util.inspect({ f: new Formats() }, { colors: true }))); + +class Leaf { + [util.inspect.custom](depth: unknown, options: any): string { + return options.stylize("leaf", "special") + util.inspect({ z: 1, a: [1, 2] }); + } +} +class Branch { + leaf = new Leaf(); + [util.inspect.custom](): string { + return "B(" + util.inspect({ b: 2, a: { leaf: this.leaf, s: "q" } }) + ")"; + } +} +class Throws { + [util.inspect.custom](): string { + throw new Error("nope"); + } +} +const opts = { sorted: true, breakLength: 20, compact: false, colors: true }; +console.log(JSON.stringify(util.inspect({ y: new Branch(), x: [new Leaf()] }, opts))); +console.log(util.inspect({ k: { j: new Branch() } }, { depth: 0 })); +try { + console.log(util.inspect({ a: { b: new Throws() } })); +} catch (e) { + console.log("caught", (e as Error).message); +} +console.log({ after: { deep: { x: 1 } }, list: [1, 2, 3] }); diff --git a/tests/corpus/8121-inspect-custom-nested.cjs b/tests/corpus/8121-inspect-custom-nested.cjs new file mode 100644 index 000000000..aa5c75ca5 --- /dev/null +++ b/tests/corpus/8121-inspect-custom-nested.cjs @@ -0,0 +1,69 @@ +// util.inspect and util.format calls inside [util.inspect.custom] methods +// start from their own defaults: no indentation from the outer render and +// no colors from the outer options. +const util = require("node:util"); + +class Wide { + constructor(v) { + this.v = v; + } + [util.inspect.custom]() { + return util.inspect({ a: { b: [1, 2, { c: this.v }] }, s: "x".repeat(50), t: "y".repeat(30) }); + } +} +console.log({ outer: { inner: new Wide(1) } }); +console.log({ first: new Wide(2), list: [new Wide(3)] }); +console.log(util.inspect({ deep: { deeper: { c: new Wide(4) } } })); + +const shared = { k: 1 }; +class Shares { + [util.inspect.custom]() { + return "S<" + util.inspect(shared) + ">"; + } +} +console.log({ a: shared, c: new Shares(), b: shared }); +class Formats { + [util.inspect.custom]() { + return util.format("%s|%O|%o|%j|%d", "a", "b", "c", "d", 5); + } +} +console.log(JSON.stringify(util.inspect(new Formats(), { colors: true }))); +console.log(JSON.stringify(util.inspect({ f: new Formats() }, { colors: true }))); +class FormatsObjects { + [util.inspect.custom]() { + return ( + util.format("%O %O", { s: "b" }, [1]) + + " " + + util.format("x", "y", 3) + + " " + + util.formatWithOptions({}, "%O", "z") + ); + } +} +console.log(JSON.stringify(util.inspect([new FormatsObjects()], { colors: true }))); + +class Leaf { + [util.inspect.custom](depth, options) { + return options.stylize("leaf", "special") + util.inspect({ z: 1, a: [1, 2] }); + } +} +class Branch { + leaf = new Leaf(); + [util.inspect.custom]() { + return "B(" + util.inspect({ b: 2, a: { leaf: this.leaf, s: "q" } }) + ")"; + } +} +class Throws { + [util.inspect.custom]() { + throw new Error("nope"); + } +} +const opts = { sorted: true, breakLength: 20, compact: false, colors: true }; +console.log(JSON.stringify(util.inspect({ y: new Branch(), x: [new Leaf()] }, opts))); +console.log(util.inspect({ k: { j: new Branch() } }, { depth: 0 })); +try { + console.log(util.inspect({ a: { b: new Throws() } })); +} catch (e) { + console.log("caught", e.message); +} +console.log({ after: { deep: { x: 1 } }, list: [1, 2, 3] }); diff --git a/tests/corpus/8122-inspect-custom-runtime-only.ts b/tests/corpus/8122-inspect-custom-runtime-only.ts new file mode 100644 index 000000000..d84dfa63d --- /dev/null +++ b/tests/corpus/8122-inspect-custom-runtime-only.ts @@ -0,0 +1,34 @@ +// Classes whose [util.inspect.custom] method runs only from the runtime +// inspect walk (instances held as unknown or in parsed JSON) compile with +// the functions and methods that method calls. +import * as util from "node:util"; +function helper(n: number): string { + return "H" + n; +} +class Base { + [util.inspect.custom](): string { + return helper(1) + this.tag(); + } + tag(): string { + return "-base"; + } +} +class Derived extends Base { + tag(): string { + return "-derived"; + } +} +class Box { + item: T; + constructor(item: T) { + this.item = item; + } + [util.inspect.custom](): string { + return "Box(" + helper(2) + ")"; + } +} +const xs: unknown[] = [new Derived(), new Box(3)]; +console.log(xs); +const j = JSON.parse('{"a":1}'); +j.d = new Derived(); +console.log(j); diff --git a/tests/corpus/8125-inspect-custom-throws.ts b/tests/corpus/8125-inspect-custom-throws.ts new file mode 100644 index 000000000..33077a32b --- /dev/null +++ b/tests/corpus/8125-inspect-custom-throws.ts @@ -0,0 +1,32 @@ +// A [util.inspect.custom] method that throws inside an outer inspect: an +// enclosing custom method that catches the error, and the walk around it, +// continue with their own indentation, circular references and colors. +import * as util from "node:util"; +class Throws { + [util.inspect.custom](): string { + throw new Error("inner"); + } +} +class Catches { + [util.inspect.custom](): string { + let note = ""; + try { + note = util.inspect({ a: { b: [new Throws()] } }); + } catch (e) { + note = "caught " + (e as Error).message; + } + return note + " then " + util.inspect({ x: { y: [1, 2] } }); + } +} +const shared = { k: 1 }; +type Outer = { first: { deep: Catches }; list: unknown[]; s: object; t: object; me?: Outer }; +const outer: Outer = { first: { deep: new Catches() }, list: [new Catches(), { z: 3 }], s: shared, t: shared }; +outer.me = outer; +console.log(util.inspect(outer, { breakLength: 50 })); +console.log(JSON.stringify(util.inspect([new Catches(), "s"], { colors: true }))); +try { + console.log(util.inspect({ a: { b: new Throws() } })); +} catch (e) { + console.log("outer caught", (e as Error).message); +} +console.log({ after: { deep: { x: 1 } }, list: [1, 2, 3] }); diff --git a/tests/corpus/8126-inspect-custom-throws.cjs b/tests/corpus/8126-inspect-custom-throws.cjs new file mode 100644 index 000000000..8d4e97608 --- /dev/null +++ b/tests/corpus/8126-inspect-custom-throws.cjs @@ -0,0 +1,30 @@ +// A [util.inspect.custom] method that throws inside an outer inspect: an +// enclosing custom method that catches the error, and the walk around it, +// continue with their own indentation and colors. +const util = require("node:util"); +class Throws { + [util.inspect.custom]() { + throw new Error("inner"); + } +} +class Catches { + [util.inspect.custom]() { + let note = ""; + try { + note = util.inspect({ a: { b: [new Throws()] } }); + } catch (e) { + note = "caught " + e.message; + } + return note + " then " + util.inspect({ x: { y: [1, 2] } }); + } +} +const shared = { k: 1 }; +const outer = { first: { deep: new Catches() }, list: [new Catches(), { z: 3 }], s: shared, t: shared }; +console.log(util.inspect(outer, { breakLength: 50 })); +console.log(JSON.stringify(util.inspect([new Catches(), "s"], { colors: true }))); +try { + console.log(util.inspect({ a: { b: new Throws() } })); +} catch (e) { + console.log("outer caught", e.message); +} +console.log({ after: { deep: { x: 1 } }, list: [1, 2, 3] }); diff --git a/tests/corpus/8129-brotli-long-param-name.cjs b/tests/corpus/8129-brotli-long-param-name.cjs new file mode 100644 index 000000000..c3d1e1712 --- /dev/null +++ b/tests/corpus/8129-brotli-long-param-name.cjs @@ -0,0 +1,23 @@ +// A Brotli params key that names no parameter: ERR_BROTLI_INVALID_PARAM +// carries the whole key, however long, in Node's message. +const zlib = require("node:zlib"); +for (const key of ["k".repeat(5000), "é".repeat(300) + "x", "9", "-1", "1.5", "x"]) { + try { + zlib.brotliCompressSync("data", { params: { [key]: 1 } }); + console.log("accepted", key.length); + } catch (err) { + const suffix = " is not a valid Brotli parameter"; + console.log( + err.name, + err.code, + err.message.length, + err.message === key + suffix, + err.message.slice(-suffix.length - 3), + ); + } +} +try { + zlib.createBrotliDecompress({ params: { ["p".repeat(100000)]: 0 } }); +} catch (err) { + console.log(err.name, err.code, err.message.length, err.message.startsWith("pppp")); +} diff --git a/tests/corpus/8160-assert-expected-from-fields.cjs b/tests/corpus/8160-assert-expected-from-fields.cjs new file mode 100644 index 000000000..4c16bd78f --- /dev/null +++ b/tests/corpus/8160-assert-expected-from-fields.cjs @@ -0,0 +1,58 @@ +// Expected errors stored in an object literal's fields: a JavaScript +// object keeps its fields checked-dynamic, and assert.throws, assert.match +// and the expected-object form convert them to the regex or string the +// fields hold. +"use strict"; +const assert = require("assert"); + +const errMessages = { + auth: / auth/, + state: / state/, + length: /Invalid initialization vector/, + code: "ERR_CRYPTO_INVALID_IV", +}; + +const error = (message, code) => { + const e = new Error(message); + if (code) e.code = code; + return e; +}; + +assert.throws(() => { + throw error("Unsupported state or unable to authenticate data"); +}, errMessages.auth); +assert.throws(() => { + throw error("Invalid state for operation getAuthTag"); +}, errMessages.state); +assert.throws( + () => { + throw error("Invalid initialization vector", "ERR_CRYPTO_INVALID_IV"); + }, + { code: errMessages.code, message: errMessages.length }, +); +assert.match("unable to auth", errMessages.auth); +assert.doesNotMatch("no match here", errMessages.state); + +const report = (label, run) => { + try { + run(); + console.log(label, "passed"); + } catch (error) { + console.log(label, error.name, error.code, error.message.split("\n")[0]); + } +}; +report("wrong message", () => + assert.throws(() => { + throw error("something else"); + }, errMessages.length), +); +report("wrong code", () => + assert.throws( + () => { + throw error("Invalid initialization vector", "ERR_OTHER"); + }, + { code: errMessages.code }, + ), +); +report("match", () => assert.match("plain text", errMessages.auth)); +console.log("done"); diff --git a/tests/corpus/8161-global-computed-reads.cjs b/tests/corpus/8161-global-computed-reads.cjs new file mode 100644 index 000000000..19dfdc3e7 --- /dev/null +++ b/tests/corpus/8161-global-computed-reads.cjs @@ -0,0 +1,98 @@ +// @no-node-shims +// Builtin globals read through the global object by computed names, and +// builtin module members that are global objects: each answers what the +// global's own spelling reads, so identity holds across the spellings. +// for-in over the global object visits Node's enumerable globals first and +// the program's own global properties after them. +"use strict"; +const { atob: bufferAtob, btoa: bufferBtoa } = require("buffer"); +const timers = require("timers"); +const { performance: perf } = require("perf_hooks"); +const util = require("util"); +const url = require("url"); +const { MessageChannel } = require("worker_threads"); +const web = require("stream/web"); + +const names = [ + "setTimeout", + "setInterval", + "setImmediate", + "clearTimeout", + "clearInterval", + "clearImmediate", + "queueMicrotask", + "global", + "globalThis", + "fetch", + "Map", + "Object", + "Promise", + "Buffer", + "crypto", +]; +for (const name of names) { + const value = globalThis[name]; + console.log(name, typeof value, value === globalThis[name]); +} +console.log( + globalThis["setTimeout"] === setTimeout, + globalThis["Map"] === Map, + globalThis["crypto"] === crypto, + globalThis["Buffer"] === Buffer, +); +console.log(timers.setTimeout === setTimeout, timers.clearInterval === globalThis["clearInterval"]); +console.log( + bufferAtob === atob, + bufferBtoa === globalThis["btoa"], + perf === performance, + util.TextEncoder === TextEncoder, + url.URL === URL, + MessageChannel === globalThis["MessageChannel"], + web.ReadableStream === ReadableStream, +); +console.log(globalThis["noSuchGlobal"], typeof globalThis["Temporal"]); +// A snapshot of the global object in a function reads the same values. +function readThroughSnapshot(name) { + const g = globalThis; + return g[name]; +} +console.log( + readThroughSnapshot("setTimeout") === setTimeout, + readThroughSnapshot("Map") === Map, + typeof readThroughSnapshot("structuredClone"), + readThroughSnapshot("noSuchGlobal"), +); +const pick = (name) => globalThis[name]; +console.log(pick("Math") === Math, pick("JSON") === JSON, pick("Reflect") === Reflect, pick("Math").max(1, 3)); + +const before = []; +for (const key in globalThis) before.push(key); +globalThis.ownA = 1; +globalThis["own" + "B"] = { b: 2 }; +const after = []; +for (const key in globalThis) after.push(key); +console.log(after.length - before.length, after.slice(-2).join(",")); +console.log(before.slice(0, 9).join(",")); +console.log(globalThis.ownA, globalThis.ownB.b, globalThis["ownB"] === globalThis.ownB); + +// The harness's leak check: every enumerated global is a known value. +const known = new Set([ + global, + clearImmediate, + setImmediate, + clearInterval, + clearTimeout, + setInterval, + setTimeout, + queueMicrotask, + structuredClone, + bufferAtob, + bufferBtoa, + performance, + fetch, + crypto, + globalThis.ownA, + globalThis.ownB, +]); +const leaked = after.filter((key) => key !== "navigator" && !known.has(globalThis[key])); +console.log("leaked", leaked.length); diff --git a/tests/corpus/8162-navigator-global.cjs b/tests/corpus/8162-navigator-global.cjs new file mode 100644 index 000000000..3944d6efd --- /dev/null +++ b/tests/corpus/8162-navigator-global.cjs @@ -0,0 +1,47 @@ +// @no-node-shims +// Node 24's navigator global: its members are getters on +// Navigator.prototype answering the process's values, and the object +// itself owns no properties. It is one of the global object's enumerable +// properties. +"use strict"; +const os = require("os"); +const util = require("util"); + +console.log(typeof navigator, navigator.userAgent); +console.log(navigator.hardwareConcurrency === os.availableParallelism()); +console.log(typeof navigator.language, navigator.language.length > 0); +console.log(navigator.languages.length, navigator.languages[0] === navigator.language); +console.log(navigator.languages === navigator.languages, Object.isFrozen(navigator.languages)); +const platforms = { + linux: { x64: "Linux x86_64", arm64: "Linux arm64" }, + darwin: { x64: "MacIntel", arm64: "MacIntel" }, +}; +console.log(navigator.platform === platforms[process.platform]?.[process.arch]); + +const value = navigator; +console.log(value === navigator, value === globalThis.navigator, value === globalThis["navigator"]); +console.log(value.userAgent, value.platform === navigator.platform, value.missing); +console.log(Object.keys(value), JSON.stringify(value), util.inspect(value)); +console.log(String(value), Object.prototype.toString.call(value)); + +const keys = []; +for (const key in globalThis) keys.push(key); +console.log(keys.join(",")); +const known = new Set([ + global, + clearImmediate, + setImmediate, + clearInterval, + clearTimeout, + setInterval, + setTimeout, + queueMicrotask, + structuredClone, + atob, + btoa, + performance, + fetch, + crypto, + navigator, +]); +console.log("leaked", keys.filter((key) => !known.has(globalThis[key])).length); diff --git a/tests/corpus/8163-data-function-values.cjs b/tests/corpus/8163-data-function-values.cjs new file mode 100644 index 000000000..d752f8405 --- /dev/null +++ b/tests/corpus/8163-data-function-values.cjs @@ -0,0 +1,32 @@ +// atob, btoa and structuredClone as JavaScript values: function values +// whose calls run what the direct calls run, the same value through every +// spelling. +"use strict"; +const buffer = require("buffer"); + +const fns = [atob, btoa, structuredClone]; +for (const fn of fns) console.log(typeof fn, fn.name, fn.length); +console.log(buffer.atob === atob, buffer.btoa === globalThis.btoa, globalThis["structuredClone"] === structuredClone); + +const pick = (name) => ({ atob, btoa, structuredClone })[name]; +const encode = pick("btoa"); +const decode = pick("atob"); +console.log(encode("scriptc"), decode(encode("scriptc")), decode("aGk=")); +const clone = pick("structuredClone"); +const original = { list: [1, { deep: true }], text: "t" }; +const copy = clone(original); +console.log(copy !== original, copy.list[1].deep, copy.list !== original.list, copy.text); + +const report = (label, run) => { + try { + console.log(label, run()); + } catch (error) { + console.log(label, error.name, error.code, error.message); + } +}; +report("atob()", () => decode()); +report("btoa()", () => encode()); +report("structuredClone()", () => clone()); +report("atob invalid", () => decode("a")); +report("btoa wide", () => encode("Ā")); +report("clone symbol", () => clone(Symbol("s"))); diff --git a/tests/corpus/8164-atob-validation.cjs b/tests/corpus/8164-atob-validation.cjs new file mode 100644 index 000000000..89d46e3b1 --- /dev/null +++ b/tests/corpus/8164-atob-validation.cjs @@ -0,0 +1,35 @@ +// atob checks its input as Node does: only ASCII whitespace and the base64 +// alphabet are allowed, '=' only at the end and at most twice, and a +// length the forgiving-base64 rules refuse is a different error. +"use strict"; +const inputs = [ + "@@@", + "a", + "ab", + "ab=", + "ab==", + "ab=c", + "abc==", + "a===", + "ab===", + " Y W J j ", + "YQ", + "Y Q = =", + "YWJjZA==", + "YWJjZA=", + "\tYW\nJj\fZA\r==", + "é", + "YW-J", + "", +]; +const decode = (label, input) => { + try { + console.log(label, JSON.stringify(atob(input))); + } catch (error) { + console.log(label, error.name, error.code, error.message); + } +}; +for (const input of inputs) decode(JSON.stringify(input), input); +decode("null", null); +decode("undefined", undefined); +decode("12", 12); diff --git a/tests/corpus/8165-rsa-keygen-lengths.cjs b/tests/corpus/8165-rsa-keygen-lengths.cjs new file mode 100644 index 000000000..35e2cb28f --- /dev/null +++ b/tests/corpus/8165-rsa-keygen-lengths.cjs @@ -0,0 +1,40 @@ +// RSA key generation within OpenSSL's limits: any modulus length from +// 512 bits (an odd length from 2048 bits yields one bit less, as +// OpenSSL's SP 800-56B generation does) and odd exponents up to 2^32 - 1. +"use strict"; +const crypto = require("crypto"); + +const cases = [ + [512, 65537], + [513, 65537], + [1023, 3], + [1024, 0x10001], + [2049, 65537], + [768, 4294967295], + [520, 17], +]; +for (const [modulusLength, publicExponent] of cases) { + const { publicKey, privateKey } = crypto.generateKeyPairSync("rsa", { modulusLength, publicExponent }); + const details = privateKey.asymmetricKeyDetails; + const data = Buffer.from("rsa key lengths"); + const signature = crypto.sign("sha256", data, privateKey); + console.log( + modulusLength, + details.modulusLength, + details.publicExponent, + publicKey.asymmetricKeyDetails.modulusLength, + signature.length, + crypto.verify("sha256", data, publicKey, signature), + ); +} +for (const [modulusLength, publicExponent] of [[511, 65537], [1024, 1], [1024, 4], [1024, 0]]) { + try { + crypto.generateKeyPairSync("rsa", { modulusLength, publicExponent }); + console.log(modulusLength, publicExponent, "generated"); + } catch (error) { + console.log(modulusLength, publicExponent, error.message); + } +} +crypto.generateKeyPair("rsa", { modulusLength: 515, publicExponent: 3 }, (error, publicKey) => { + console.log("async", error, publicKey.asymmetricKeyDetails.modulusLength); +}); diff --git a/tests/corpus/8166-rsa-pss-keys.cjs b/tests/corpus/8166-rsa-pss-keys.cjs new file mode 100644 index 000000000..aad90f6aa --- /dev/null +++ b/tests/corpus/8166-rsa-pss-keys.cjs @@ -0,0 +1,104 @@ +// RSA-PSS keys (id-RSASSA-PSS): generation with and without +// RSASSA-PSS-params, their details, SubjectPublicKeyInfo and PKCS#8 in +// OpenSSL's encoding, import of both, and the restrictions on signatures: +// PSS padding only, the key's digest and MGF1 digest, and at least its +// salt length. The deprecated hash and mgf1Hash options run in a child +// process, whose DEP0154 reports are printed with process ids normalized. +"use strict"; +const { spawnSync } = require("child_process"); +const c = require("crypto"); +const fmt = (v) => JSON.stringify(v, (k, x) => (typeof x === "bigint" ? `${x}n` : x)); +const tryIt = (label, fn) => { + try { + const r = fn(); + console.log(label, typeof r === "string" ? r.split("\n")[0] : Buffer.isBuffer(r) ? `buffer ${r.length > 0}` : fmt(r)); + } catch (e) { + console.log(label, e.name, e.code, e.message); + } +}; +const algOf = (spki) => { + // The AlgorithmIdentifier: the SEQUENCE after the outer header. + const start = spki[1] & 0x80 ? 2 + (spki[1] & 0x7f) : 2; + const len = spki[start + 1]; + return spki.subarray(start, start + 2 + len).toString("hex"); +}; +const report = (label, options) => tryIt(label, () => c.generateKeyPairSync("rsa-pss", { modulusLength: 512, ...options }).publicKey.asymmetricKeyDetails); + +function main() { + const cases = { + none: {}, + sha256: { hashAlgorithm: "sha256" }, + full: { hashAlgorithm: "sha256", mgf1HashAlgorithm: "sha384", saltLength: 16 }, + sha1: { hashAlgorithm: "sha1" }, + salt: { saltLength: 10 }, + s512: { hashAlgorithm: "sha512", saltLength: 0 }, + mgf: { mgf1HashAlgorithm: "sha256" }, + }; + for (const [name, options] of Object.entries(cases)) { + const { publicKey, privateKey } = c.generateKeyPairSync("rsa-pss", { modulusLength: ["full", "sha1", "salt", "mgf"].includes(name) ? 512 : 1024, ...options }); + const spki = publicKey.export({ format: "der", type: "spki" }); + const pkcs8 = privateKey.export({ format: "der", type: "pkcs8" }); + console.log(name, publicKey.asymmetricKeyType, fmt(publicKey.asymmetricKeyDetails), fmt(privateKey.asymmetricKeyDetails)); + console.log(name, "spki alg", algOf(spki), "pkcs8 alg", pkcs8.indexOf(Buffer.from(algOf(spki), "hex")) > 0); + tryIt(`${name} pkcs1`, () => privateKey.export({ format: "pem", type: "pkcs1" })); + tryIt(`${name} jwk`, () => publicKey.export({ format: "jwk" })); + tryIt(`${name} pem`, () => privateKey.export({ format: "pem", type: "pkcs8" })); + const reimported = c.createPublicKey(publicKey.export({ format: "pem", type: "spki" })); + const fromPrivate = c.createPublicKey(privateKey.export({ format: "pem", type: "pkcs8" })); + const privateAgain = c.createPrivateKey({ key: pkcs8, format: "der", type: "pkcs8" }); + console.log(name, reimported.equals(publicKey), fromPrivate.equals(publicKey), privateAgain.asymmetricKeyType); + const data = Buffer.from("rsa-pss"); + const signature = c.sign(null, data, privateAgain); + console.log(name, c.verify(null, data, publicKey, signature), c.verify(null, data, reimported, signature)); + } + + const data = Buffer.from("restricted"); + const { publicKey, privateKey } = c.generateKeyPairSync("rsa-pss", { + modulusLength: 512, + hashAlgorithm: "sha256", + saltLength: 16, + }); + tryIt("sign sha1", () => c.sign("sha1", data, privateKey)); + tryIt("sign pkcs1", () => c.sign("sha256", data, { key: privateKey, padding: c.constants.RSA_PKCS1_PADDING })); + tryIt("sign salt 8", () => c.sign("sha256", data, { key: privateKey, saltLength: 8 })); + tryIt("verify salt 8", () => c.verify("sha256", data, { key: publicKey, saltLength: 8 }, Buffer.alloc(64))); + for (const saltLength of [16, 20, 30]) { + const signature = c.sign("sha256", data, { key: privateKey, saltLength }); + console.log( + saltLength, + c.verify("sha256", data, publicKey, signature), + c.verify("sha256", data, { key: publicKey, saltLength: c.constants.RSA_PSS_SALTLEN_AUTO }, signature), + ); + } + const signer = c.createSign("sha256").update(data); + console.log("createVerify", c.createVerify("sha256").update(data).verify(publicKey, signer.sign(privateKey))); + const free = c.generateKeyPairSync("rsa-pss", { modulusLength: 1024 }); + const freeSignature = c.sign("sha512", data, free.privateKey); + console.log("free sha512", c.verify("sha512", data, free.publicKey, freeSignature)); + + report("bad digest", { hashAlgorithm: "nope" }); + report("bad mgf1", { mgf1HashAlgorithm: "nope" }); + report("negative salt", { saltLength: -1 }); + report("upper", { hashAlgorithm: "SHA256" }); + report("alias", { hashAlgorithm: "RSA-SHA256" }); +} + +if (process.argv[2] === "deprecated") { + report("hash", { hash: "sha256" }); + report("conflict", { hashAlgorithm: "sha256", hash: "sha1" }); + report("mgf1Hash", { hashAlgorithm: "sha256", mgf1Hash: "sha384" }); + report("hash type", { hash: 5 }); + // Node drops deprecations while noDeprecation is set when it reports + // them, so the flag is set once the reports above are out. + setTimeout(() => { + process.noDeprecation = true; + report("quiet", { hash: "sha512" }); + console.log("end of turn"); + }); +} else { + main(); + const child = spawnSync(process.execPath, [process.argv[1], "deprecated"], { encoding: "utf8" }); + console.log(`status: ${child.status}`); + console.log(child.stdout); + console.log(child.stderr.replace(/\(node:\d+\)/g, "(node:)")); +} diff --git a/tests/corpus/8167-encrypted-private-keys.cjs b/tests/corpus/8167-encrypted-private-keys.cjs new file mode 100644 index 000000000..7b274e500 --- /dev/null +++ b/tests/corpus/8167-encrypted-private-keys.cjs @@ -0,0 +1,143 @@ +// Encrypted private keys: PKCS#8 as PBES2 and PKCS#1 and SEC1 as +// traditional PEM with the CBC and ECB ciphers, import with a passphrase, +// generateKeyPair with a cipher, and OpenSSL's errors for missing and +// wrong passphrases, unknown ciphers, and ciphers a form refuses. +"use strict"; +const c = require("crypto"); +const show = (e) => + `${e.name} ${e.code} ${e.message}${e.library ? ` [${e.library}: ${e.reason}]` : ""}${ + e.opensslErrorStack ? ` ${JSON.stringify(e.opensslErrorStack)}` : "" + }`; +const tryIt = (label, fn) => { + try { + console.log(label, fn()); + } catch (e) { + console.log(label, show(e)); + } +}; +// The PEM label and headers, without the random IV. +const head = (pem) => + pem + .split("\n") + .filter((l) => l.startsWith("-----BEGIN") || l.includes(":")) + .map((l) => l.replace(/,[0-9A-F]+$/, "")) + .join(" | "); + +const ec = c.generateKeyPairSync("ec", { namedCurve: "P-256" }); +const rsa = c.generateKeyPairSync("rsa", { modulusLength: 1024 }); +const same = (key, pair) => c.createPublicKey(key).equals(pair.publicKey); + +for (const cipher of ["aes-128-cbc", "aes-192-cbc", "aes-256-cbc", "aes-128-ecb", "aes-256-ecb", "des-ede3-cbc", "aes256", "AES-128-CBC", "2.16.840.1.101.3.4.1.42"]) { + for (const format of ["pem", "der"]) { + tryIt(`pkcs8 ${format} ${cipher}`, () => { + const out = ec.privateKey.export({ type: "pkcs8", format, cipher, passphrase: "top secret" }); + const key = c.createPrivateKey({ key: out, format, type: format === "der" ? "pkcs8" : undefined, passphrase: "top secret" }); + return `${format === "pem" ? head(out) : Buffer.isBuffer(out)} ${key.asymmetricKeyType} ${same(key, ec)}`; + }); + } +} +for (const cipher of ["aes-128-cbc", "aes-256-cbc", "des-ede3-cbc", "des3"]) { + tryIt(`sec1 ${cipher}`, () => { + const out = ec.privateKey.export({ type: "sec1", format: "pem", cipher, passphrase: Buffer.from("pw") }); + return `${head(out)} ${same(c.createPrivateKey({ key: out, passphrase: "pw" }), ec)}`; + }); + tryIt(`pkcs1 ${cipher}`, () => { + const out = rsa.privateKey.export({ type: "pkcs1", format: "pem", cipher, passphrase: "pw" }); + return `${head(out)} ${same(c.createPrivateKey({ key: out, passphrase: Buffer.from("pw") }), rsa)}`; + }); +} +tryIt("empty passphrase", () => { + const out = rsa.privateKey.export({ type: "pkcs8", format: "pem", cipher: "aes-128-cbc", passphrase: "" }); + return same(c.createPrivateKey({ key: out, passphrase: "" }), rsa); +}); + +// Reading encrypted keys. These are fixed encryptions of one P-256 key +// with the passphrase "pw": decrypting with a wrong passphrase gives +// random-looking bytes, which a fresh encryption would make fail at +// different steps from run to run. +const fixedKey = [ + "-----BEGIN PRIVATE KEY-----", + "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgExcc5/xZ3vYL2Dpg", + "XnB7E8gQhcW1bkUcIGJWBGvrrGChRANCAAR0wxNUH1yo/U9nqFY4Gt5SVzPjvWq6", + "NzlStUC6Njk9BdR7SvBlNIGDAFlOfMwhfZbvvvMP+rLMR3RtBg3Ketxe", + "-----END PRIVATE KEY-----", +].join("\n") + "\n"; +const p8pem = [ + "-----BEGIN ENCRYPTED PRIVATE KEY-----", + "MIH0MF8GCSqGSIb3DQEFDTBSMDEGCSqGSIb3DQEFDDAkBBBkpqs2611cRjGDZTd4", + "FmNeAgIIADAMBggqhkiG9w0CCQUAMB0GCWCGSAFlAwQBKgQQRhL9yxS4i0ngtke+", + "frhURASBkN0dk+GQNCGU5cFC+MMswOr9XwGaQnLbJo7VnhCje3//JaVJDjexrd69", + "YZliX7pKGNghfFf+eBAeOAkwbAK39T8KQPwTJVJ+Z5O485N2fl5dPlr3gaEA3+cm", + "LQgasmL7Zv9UOH2iwvWllFYL3bnwr7VsqTCW2xe53qTB+dWVurP0cCiZNGEnUzrf", + "4bvWNrW0nA==", + "-----END ENCRYPTED PRIVATE KEY-----", +].join("\n") + "\n"; +const p8der = Buffer.from("MIH0MF8GCSqGSIb3DQEFDTBSMDEGCSqGSIb3DQEFDDAkBBAOrVavYYEvy3KUHwK0hbQnAgIIADAMBggqhkiG9w0CCQUAMB0GCWCGSAFlAwQBKgQQmEnqHOLegUirb49LD8oNhASBkGUwe3SZciScH/tWuoK5JWujckNHRKROwu81a5AGjIKxkvqCm+Qp9Gr4rPsb2vbv3bgfojbEKvSGvtuqB/X/jpN5UMZwWI3D9Ocd1+jEq1FqDb3rb3qxcoO9jmwuFFzNeYy4sW29XFuKTxu4aJHT1sEtMzIVlRLv53aXzpp9eQ9j47EOTWFqIpkt02TRFuazCQ==", "base64"); +const sec1 = [ + "-----BEGIN EC PRIVATE KEY-----", + "Proc-Type: 4,ENCRYPTED", + "DEK-Info: AES-128-CBC,E73A95A6783C45F914B397FE018521D2", + "", + "nsuhQFMPr+QFITcpmr2JWs+Zep4wxoCWMEk1jzyyXSJddfebIXOFg4xmtK8YJXIy", + "vqa6pvAIiak0iEZjVSn20+JAg2wqQ0sLaFPhBmhcZk+xuQoLD8CcYvROO5VtVELm", + "DkgOUcJ1oEfZJIRM6U2oDNFmj2NB/SnSQUf1niy7ba8=", + "-----END EC PRIVATE KEY-----", +].join("\n") + "\n"; +const fixedPublic = c.createPublicKey(fixedKey); +tryIt("public from encrypted", () => c.createPublicKey({ key: p8pem, passphrase: "pw" }).equals(fixedPublic)); +tryIt("public from traditional", () => c.createPublicKey({ key: sec1, passphrase: "pw" }).type); +tryIt("pkcs8 pem no passphrase", () => c.createPrivateKey(p8pem)); +tryIt("pkcs8 der no passphrase", () => c.createPrivateKey({ key: p8der, format: "der", type: "pkcs8" })); +tryIt("sec1 no passphrase", () => c.createPrivateKey(sec1)); +tryIt("public no passphrase", () => c.createPublicKey(p8pem)); +tryIt("pkcs8 pem wrong", () => c.createPrivateKey({ key: p8pem, passphrase: "nope" })); +tryIt("pkcs8 der wrong", () => c.createPrivateKey({ key: p8der, format: "der", type: "pkcs8", passphrase: "nope" })); +tryIt("sec1 wrong", () => c.createPrivateKey({ key: sec1, passphrase: "nope" })); +tryIt("passphrase type", () => c.createPrivateKey({ key: p8pem, passphrase: 5 })); +tryIt("dek unknown", () => c.createPrivateKey({ key: sec1.replace(/DEK-Info: AES-128-CBC/, "DEK-Info: NOPE-CBC"), passphrase: "pw" })); +tryIt("dek legacy", () => + c.createPrivateKey({ key: sec1.replace(/DEK-Info: AES-128-CBC,([0-9A-F]{16})[0-9A-F]+/, "DEK-Info: DES-CBC,$1"), passphrase: "pw" }), +); +tryIt("dek legacy no passphrase", () => + c.createPrivateKey(sec1.replace(/DEK-Info: AES-128-CBC,([0-9A-F]{16})[0-9A-F]+/, "DEK-Info: DES-CBC,$1")), +); + +// Export options. +const opts = (extra) => ({ type: "pkcs8", format: "pem", ...extra }); +tryIt("unknown cipher", () => ec.privateKey.export(opts({ cipher: "nope", passphrase: "x" }))); +tryIt("cipher without passphrase", () => ec.privateKey.export(opts({ cipher: "aes-128-cbc" }))); +tryIt("passphrase without cipher", () => ec.privateKey.export(opts({ passphrase: "x" }))); +tryIt("cipher type", () => ec.privateKey.export(opts({ cipher: 5, passphrase: "x" }))); +tryIt("sec1 der", () => ec.privateKey.export({ type: "sec1", format: "der", cipher: "aes-128-cbc", passphrase: "x" })); +tryIt("pkcs1 der", () => rsa.privateKey.export({ type: "pkcs1", format: "der", cipher: "aes-128-cbc", passphrase: "x" })); +// OpenSSL refuses these ciphers for PKCS#8, and the last ones for +// traditional PEM too (the P-256 key's PKCS#8 and SEC1 lengths are not +// multiples of 8, which key wrap needs). +for (const cipher of ["aes-128-gcm", "aes-256-ccm", "aes-128-ctr", "chacha20-poly1305", "aes-128-xts", "des-ede3-cfb", "2.16.840.1.101.3.4.1.6"]) + tryIt(`refused pkcs8 ${cipher}`, () => ec.privateKey.export(opts({ cipher, passphrase: "x" })).length > 0); +for (const cipher of ["aes-128-wrap", "id-aes256-wrap", "des-cbc", "rc4"]) { + tryIt(`refused pkcs8 ${cipher}`, () => ec.privateKey.export(opts({ cipher, passphrase: "x" })).length > 0); + tryIt(`refused sec1 ${cipher}`, () => ec.privateKey.export({ type: "sec1", format: "pem", cipher, passphrase: "x" })); +} +for (const cipher of ["aes-128-ecb", "aes-256-ecb", "des-ede3"]) + tryIt(`refused sec1 ${cipher}`, () => ec.privateKey.export({ type: "sec1", format: "pem", cipher, passphrase: "x" })); + +// generateKeyPair with a cipher. +const encoding = (cipher, type = "pkcs8") => ({ + namedCurve: "P-256", + publicKeyEncoding: { type: "spki", format: "pem" }, + privateKeyEncoding: { type, format: "pem", cipher, passphrase: "gen" }, +}); +tryIt("keygen sync", () => { + const { publicKey, privateKey } = c.generateKeyPairSync("ec", encoding("aes-256-cbc")); + return `${head(privateKey)} ${c.createPublicKey({ key: privateKey, passphrase: "gen" }).equals(c.createPublicKey(publicKey))}`; +}); +tryIt("keygen unknown", () => c.generateKeyPairSync("ec", encoding("nope"))); +tryIt("keygen gcm", () => c.generateKeyPairSync("ec", encoding("aes-128-gcm"))); +tryIt("keygen sec1 ecb", () => c.generateKeyPairSync("ec", encoding("aes-128-ecb", "sec1"))); +// One job at a time: Node's jobs finish in either order. +c.generateKeyPair("ec", encoding("des-ede3-cbc", "sec1"), (err, publicKey, privateKey) => { + console.log("keygen async", err, head(privateKey)); + console.log("keygen async key", c.createPublicKey({ key: privateKey, passphrase: "gen" }).equals(c.createPublicKey(publicKey))); + c.generateKeyPair("ec", encoding("aes-128-ccm"), (err) => console.log("keygen async ccm", show(err))); +}); diff --git a/tests/corpus/8168-navigator-language-values.cjs b/tests/corpus/8168-navigator-language-values.cjs new file mode 100644 index 000000000..41368a1ef --- /dev/null +++ b/tests/corpus/8168-navigator-language-values.cjs @@ -0,0 +1,8 @@ +// @no-node-shims +// navigator.language, navigator.languages and navigator.platform read +// without the navigator object ever being taken as a value: the runtime +// caches the answers and releases them at exit. +"use strict"; +console.log(typeof navigator.language, navigator.language.length > 0); +console.log(navigator.languages.length, navigator.languages[0] === navigator.language); +console.log(Object.isFrozen(navigator.languages), typeof navigator.platform); diff --git a/tests/corpus/8176-tls-empty-context-ca.ts b/tests/corpus/8176-tls-empty-context-ca.ts new file mode 100644 index 000000000..510c9881a --- /dev/null +++ b/tests/corpus/8176-tls-empty-context-ca.ts @@ -0,0 +1,73 @@ +// An explicitly empty ca is an empty trust store for client certificates, +// in the server's options, in setSecureContext and in addContext, even when +// the client certificate's issuer is a default CA +// (tls.setDefaultCACertificates); an omitted ca trusts the default CAs. +// Each server has requestCert, with rejectUnauthorized false (the server's +// verdict) and true (the client is dropped). +import { readFileSync } from "node:fs"; +import type { AddressInfo } from "node:net"; +import * as tls from "node:tls"; + +const D = "tests/fixtures/server/certs"; +const r = (f: string): Buffer => readFileSync(`${D}/${f}`); +const ca = r("ca.pem"), cert = r("localhost.pem"), key = r("localhost-key.pem"); +const alt = r("alt.pem"), altKey = r("alt-key.pem"); +const client = r("client.pem"), clientKey = r("client-key.pem"); + +tls.setDefaultCACertificates([ca.toString()]); + +function listen(server: tls.Server): Promise { + return new Promise((res) => server.listen(0, () => res((server.address() as AddressInfo).port))); +} + +type Handler = (s: tls.TLSSocket) => void; + +async function run(label: string, make: (h: Handler) => tls.Server, servername: string): Promise { + const events: string[] = []; + const server = make((s) => { + events.push(`server authorized=${s.authorized} error=${s.authorizationError}`); + s.end("ok"); + }); + const port = await listen(server); + await new Promise((done) => { + const c = tls.connect({ port, servername, ca, cert: client, key: clientKey }); + c.on("secureConnect", () => events.push("client secureConnect")); + c.on("data", (d: Buffer) => events.push(`client data ${d}`)); + c.on("error", (e: NodeJS.ErrnoException) => events.push(`client error ${e.code}`)); + c.on("close", () => setTimeout(done, 50)); + }); + console.log(label, "|", events.join(" ; ")); + await new Promise((res) => server.close(() => res())); +} + +for (const reject of [false, true]) { + const opts = { cert, key, requestCert: true, rejectUnauthorized: reject }; + const tag = reject ? "reject" : "request"; + await run(`${tag} omitted`, (h) => tls.createServer(opts, h), "localhost"); + await run(`${tag} empty array`, (h) => tls.createServer({ ...opts, ca: [] }, h), "localhost"); + await run(`${tag} setSecureContext empty`, (h) => { + const server = tls.createServer(opts, h); + server.setSecureContext({ cert, key, ca: [] }); + return server; + }, "localhost"); + await run(`${tag} setSecureContext omitted`, (h) => { + const server = tls.createServer({ ...opts, ca: [] }, h); + server.setSecureContext({ cert, key }); + return server; + }, "localhost"); + await run(`${tag} addContext empty array`, (h) => { + const server = tls.createServer(opts, h); + server.addContext("alt.localhost", { cert: alt, key: altKey, ca: [] }); + return server; + }, "alt.localhost"); + await run(`${tag} addContext empty Buffer`, (h) => { + const server = tls.createServer(opts, h); + server.addContext("alt.localhost", tls.createSecureContext({ cert: alt, key: altKey, ca: Buffer.alloc(0) })); + return server; + }, "alt.localhost"); + await run(`${tag} addContext omitted`, (h) => { + const server = tls.createServer({ ...opts, ca: [] }, h); + server.addContext("alt.localhost", { cert: alt, key: altKey }); + return server; + }, "alt.localhost"); +} diff --git a/tests/corpus/8177-tls-empty-client-ca.ts b/tests/corpus/8177-tls-empty-client-ca.ts new file mode 100644 index 000000000..0fb596ddb --- /dev/null +++ b/tests/corpus/8177-tls-empty-client-ca.ts @@ -0,0 +1,104 @@ +// An explicitly empty ca is an empty trust store for a client's view of +// the server certificate, in tls.connect, https.request and http2.connect, +// even when the server certificate's issuer is a default CA +// (tls.setDefaultCACertificates); an omitted ca, or an empty string, trusts +// the default CAs. +import { readFileSync } from "node:fs"; +import * as http2 from "node:http2"; +import * as https from "node:https"; +import type { AddressInfo } from "node:net"; +import * as tls from "node:tls"; + +const D = "tests/fixtures/server/certs"; +const r = (f: string): Buffer => readFileSync(`${D}/${f}`); +const ca = r("ca.pem"), key = r("localhost-key.pem"); +// The servers send their issuer too, so each client sees the same chain. +const cert = Buffer.concat([r("localhost.pem"), ca]); + +tls.setDefaultCACertificates([ca.toString()]); + +function listen(server: tls.Server | https.Server | http2.Http2SecureServer): Promise { + return new Promise((res) => server.listen(0, () => res((server.address() as AddressInfo).port))); +} + +async function watch(label: string, c: tls.TLSSocket): Promise { + const events: string[] = []; + await new Promise((done) => { + c.on("secureConnect", () => { + events.push(`secureConnect authorized=${c.authorized} error=${c.authorizationError}`); + c.end(); + }); + c.on("error", (e: NodeJS.ErrnoException) => events.push(`error ${e.code}`)); + c.on("close", () => done()); + }); + console.log("tls.connect", label, "|", events.join(" ; ")); +} + +{ + const server = tls.createServer({ cert, key }, (s) => s.end()); + const port = await listen(server); + const host = "localhost"; + await watch("omitted", tls.connect({ port, host })); + await watch("empty string", tls.connect({ port, host, ca: "" })); + await watch("empty Buffer", tls.connect({ port, host, ca: Buffer.alloc(0) })); + await watch("empty array", tls.connect({ port, host, ca: [] })); + await watch( + "empty Buffer, rejectUnauthorized false", + tls.connect({ port, host, ca: Buffer.alloc(0), rejectUnauthorized: false }), + ); + await watch("empty array, rejectUnauthorized false", tls.connect({ port, host, ca: [], rejectUnauthorized: false })); + server.close(); +} + +function report(label: string, req: ReturnType, done: () => void): void { + req.on("response", (res) => { + console.log("https.request", label, "| status", res.statusCode); + res.resume(); + res.on("end", done); + }); + req.on("error", (e: NodeJS.ErrnoException) => { + console.log("https.request", label, "| error", e.code); + done(); + }); + req.end(); +} + +{ + const server = https.createServer({ cert, key }, (_req, res) => res.end("ok")); + const port = await listen(server); + await new Promise((done) => + report("omitted", https.request({ host: "localhost", port, path: "/", agent: false }), done), + ); + await new Promise((done) => + report("empty string", https.request({ host: "localhost", port, path: "/", agent: false, ca: "" }), done), + ); + await new Promise((done) => + report( + "empty Buffer", + https.request({ host: "localhost", port, path: "/", agent: false, ca: Buffer.alloc(0) }), + done, + ), + ); + const options: unknown = { host: "localhost", port, path: "/", agent: false, ca: [] }; + await new Promise((done) => + report("empty array in a runtime record", https.request(options as https.RequestOptions), done), + ); + server.close(); +} + +{ + const server = http2.createSecureServer({ cert, key }, (_req, res) => res.end("ok")); + const port = await listen(server); + for (const [label, ca] of [["omitted", undefined], ["empty Buffer", Buffer.alloc(0)]] as const) { + await new Promise((done) => { + const session = ca === undefined ? http2.connect(`https://localhost:${port}`) : http2.connect(`https://localhost:${port}`, { ca }); + session.on("connect", () => { + console.log("http2.connect", label, "| connect"); + session.close(); + }); + session.on("error", (e: NodeJS.ErrnoException) => console.log("http2.connect", label, "| error", e.code)); + session.on("close", () => done()); + }); + } + server.close(); +} diff --git a/tests/corpus/8178-rsa-pss-auto-min-salt.cjs b/tests/corpus/8178-rsa-pss-auto-min-salt.cjs new file mode 100644 index 000000000..78e288591 --- /dev/null +++ b/tests/corpus/8178-rsa-pss-auto-min-salt.cjs @@ -0,0 +1,59 @@ +// Verifying with RSA_PSS_SALTLEN_AUTO against an RSA-PSS key whose +// parameters set a minimum salt length recovers the salt length from the +// signature and does not enforce that minimum, as in Node: a valid +// signature with a shorter salt verifies. The default salt length (the +// key's minimum) accepts only that length. The keys use +// an MGF1 digest equal to the message digest and a different one. The +// stricter public key is the generated one with its minimum salt length +// raised from 16 to 32 in its DER. +const c = require("node:crypto"); + +const data = Buffer.from("restricted salt"); + +function raiseMinimumSalt(publicKey) { + const der = publicKey.export({ format: "der", type: "spki" }); + const from = Buffer.from([0xa2, 0x03, 0x02, 0x01, 0x10]); + const at = der.indexOf(from); + if (at < 0) throw new Error("no saltLength field"); + der[at + 4] = 0x20; + return c.createPublicKey({ key: der, format: "der", type: "spki" }); +} + +function attempt(label, fn) { + try { + console.log(label, fn()); + } catch (e) { + console.log(label, "throws", e.code, e.message); + } +} + +for (const mgf1HashAlgorithm of ["sha256", "sha384"]) { + const { privateKey, publicKey } = c.generateKeyPairSync("rsa-pss", { + modulusLength: 1024, + hashAlgorithm: "sha256", + mgf1HashAlgorithm, + saltLength: 16, + }); + const strict = raiseMinimumSalt(publicKey); + const { mgf1HashAlgorithm: mgf1, saltLength: minimum } = strict.asymmetricKeyDetails; + console.log(mgf1HashAlgorithm, "strict key", mgf1, minimum); + for (const saltLength of [16, 20, 32, 40]) { + const signature = c.sign("sha256", data, { key: privateKey, saltLength }); + const label = `${mgf1HashAlgorithm} salt ${saltLength}`; + attempt(`${label} auto, key minimum 16:`, () => + c.verify("sha256", data, { key: publicKey, saltLength: c.constants.RSA_PSS_SALTLEN_AUTO }, signature), + ); + attempt(`${label} auto, key minimum 32:`, () => + c.verify("sha256", data, { key: strict, saltLength: c.constants.RSA_PSS_SALTLEN_AUTO }, signature), + ); + attempt(`${label} default, key minimum 32:`, () => c.verify("sha256", data, strict, signature)); + attempt(`${label} digest length, key minimum 32:`, () => + c.verify("sha256", data, { key: strict, saltLength: c.constants.RSA_PSS_SALTLEN_DIGEST }, signature), + ); + const v = c.createVerify("sha256"); + v.update(data); + attempt(`${label} createVerify auto, key minimum 32:`, () => + v.verify({ key: strict, saltLength: c.constants.RSA_PSS_SALTLEN_AUTO }, signature), + ); + } +} diff --git a/tests/corpus/8179-tls-reject-unauthorized-values.mjs b/tests/corpus/8179-tls-reject-unauthorized-values.mjs new file mode 100644 index 000000000..4e87099d4 --- /dev/null +++ b/tests/corpus/8179-tls-reject-unauthorized-values.mjs @@ -0,0 +1,55 @@ +// rejectUnauthorized is off only when it is false, as in Node: on a server +// with requestCert, null, 0, "" and undefined keep rejecting clients that +// send no certificate, and in tls.connect they keep rejecting a server +// certificate that does not verify. +import { readFileSync } from "node:fs"; +import * as tls from "node:tls"; + +const D = "tests/fixtures/server/certs"; +const r = (f) => readFileSync(`${D}/${f}`); +const ca = r("ca.pem"), cert = r("localhost.pem"), key = r("localhost-key.pem"); +const self = r("selfsigned.pem"), selfKey = r("selfsigned-key.pem"); + +const values = [ + ["false", false], + ["true", true], + ["null", null], + ["0", 0], + ['""', ""], + ["undefined", undefined], +]; + +for (const [label, value] of values) { + const events = []; + const server = tls.createServer({ cert, key, requestCert: true, rejectUnauthorized: value }, (s) => { + events.push(`server authorized=${s.authorized} error=${s.authorizationError}`); + s.end("ok"); + }); + const port = await new Promise((res) => server.listen(0, () => res(server.address().port))); + await new Promise((done) => { + const c = tls.connect({ port, ca, servername: "localhost" }); + c.on("secureConnect", () => events.push("client secureConnect")); + c.on("data", (d) => events.push(`client data ${d}`)); + c.on("error", (e) => events.push(`client error ${e.code}`)); + c.on("close", () => setTimeout(() => done(undefined), 50)); + }); + console.log("rejectUnauthorized", label, "|", events.join(" ; ")); + await new Promise((res) => server.close(() => res(undefined))); +} + +{ + const server = tls.createServer({ cert: self, key: selfKey }, (s) => s.end("ok")); + const port = await new Promise((res) => server.listen(0, () => res(server.address().port))); + for (const [label, value] of values) { + const events = []; + await new Promise((done) => { + const c = tls.connect({ port, servername: "localhost", rejectUnauthorized: value }); + c.on("secureConnect", () => events.push(`secureConnect authorized=${c.authorized} error=${c.authorizationError}`)); + c.on("data", (d) => events.push(`data ${d}`)); + c.on("error", (e) => events.push(`error ${e.code}`)); + c.on("close", () => done(undefined)); + }); + console.log("tls.connect rejectUnauthorized", label, "|", events.join(" ; ")); + } + server.close(); +} diff --git a/tests/corpus/8180-pow2-quotients.ts b/tests/corpus/8180-pow2-quotients.ts new file mode 100644 index 000000000..f98d4e46e --- /dev/null +++ b/tests/corpus/8180-pow2-quotients.ts @@ -0,0 +1,57 @@ +// x / Math.pow(2, n) over every integer exponent from -1080 to 1030, against +// the same divisor read back from an array (an ordinary division): zeros, +// subnormals, values next to the limits, infinities and NaN, and a divisor of +// 2 ** n that is subnormal, zero or Infinity. Non-integer, signed-zero, +// infinite and NaN exponents give divisors that are not powers of two. +function bits(x: number): string { + const view = new DataView(new ArrayBuffer(8)); + view.setFloat64(0, x); + return view.getBigUint64(0).toString(16).padStart(16, "0"); +} + +const values = [ + 1, -1, 3, 0.1, -7.25, 0, -0, 5e-324, -5e-324, 2.2250738585072014e-308, 1.7976931348623157e308, + -1.7976931348623157e308, 4503599627370497, 123456789.123, Infinity, -Infinity, NaN, +]; +const divisors: number[] = []; +for (let n = -1080; n <= 1030; n++) divisors.push(Math.pow(2, n)); + +const scratch = new DataView(new ArrayBuffer(8)); +let digest = 0; +let mismatches = 0; +for (let n = -1080; n <= 1030; n++) { + const divisor = divisors[n + 1080]!; + for (const x of values) { + const quotient = x / Math.pow(2, n); + const plain = x / divisor; + scratch.setFloat64(0, quotient); + digest = (Math.imul(digest, 31) + scratch.getUint32(0)) | 0; + digest = (Math.imul(digest, 31) + scratch.getUint32(4)) | 0; + if (!Object.is(quotient, plain)) { + mismatches++; + if (mismatches <= 5) console.log("mismatch", x, n, bits(quotient), bits(plain)); + } + } +} +console.log("digest", (digest >>> 0).toString(16), "mismatches", mismatches); + +for (const n of [-1075, -1074, -1023, -1022, -1, 0, 1, 52, 1022, 1023, 1024]) { + console.log(n, bits(3 / Math.pow(2, n)), bits(-5e-324 / Math.pow(2, n)), bits(1.7976931348623157e308 / Math.pow(2, n))); +} +for (const e of [0.5, -0.5, -0, 1e-300, 1023.0000001, -1074.5, Infinity, -Infinity, NaN, 2 ** 31, -(2 ** 31)]) { + console.log("exponent", e, bits(10 / Math.pow(2, e)), bits(-0 / Math.pow(2, e))); +} + +// A divisor that changes every pass, and one computed once. +let total = 0; +for (let i = 0; i < 100000; i++) { + const shift = (i % 40) - 20; + total += (i * 7 + 3) / Math.pow(2, shift); +} +console.log("total", total); +let key = 12345; +const width = 34; +for (let i = 0; i < 100000; i++) { + key = (key - Math.floor(key / Math.pow(2, width)) * Math.pow(2, width)) * 4 + (i % 4); +} +console.log("key", key); diff --git a/tests/corpus/8181-map-get-borrows.ts b/tests/corpus/8181-map-get-borrows.ts new file mode 100644 index 000000000..1a6c71667 --- /dev/null +++ b/tests/corpus/8181-map-get-borrows.ts @@ -0,0 +1,172 @@ +// Map.get results held in locals across the operations that can and cannot +// release the entry's value: an update on the absent branch, a write, delete +// or clear before a later read, a call that may change the map, loops, +// string keys, values that are stored, returned or compared, a local declared +// from the read, a read from a temporary map, and a key that replaces the +// field holding the map. +class Counter { + num: number; + label: string; + constructor(num: number, label: string) { + this.num = num; + this.label = label; + } +} + +function count(keys: number[]): Map { + const counts = new Map(); + for (const key of keys) { + const cur = counts.get(key); + if (cur === undefined) counts.set(key, new Counter(1, `k${key}`)); + else cur.num++; + } + return counts; +} + +function report(counts: Map): string { + const parts: string[] = []; + for (const [key, value] of counts) parts.push(`${key}:${value.num}:${value.label}`); + return parts.join(" "); +} + +console.log("count", report(count([3, 1, 3, 2, 3, 1, 9]))); + +// A write replaces the entry's value while the old one is still read. +function replaceThenRead(m: Map): string { + const old = m.get(1); + m.set(1, new Counter(100, "new")); + return old === undefined ? "none" : `${old.num}:${old.label}`; +} +const replaced = new Map([[1, new Counter(5, "old")]]); +console.log("replace", replaceThenRead(replaced), replaced.get(1)!.label); + +// Deleting and clearing release the value before the read. +function deleteThenRead(m: Map): string { + const held = m.get(2); + m.delete(2); + return held === undefined ? "none" : held.label; +} +function clearThenRead(m: Map): string { + const held = m.get(2); + m.clear(); + return held === undefined ? "none" : `${held.label}:${m.size}`; +} +console.log("delete", deleteThenRead(new Map([[2, new Counter(1, "two")]]))); +console.log("clear", clearThenRead(new Map([[2, new Counter(1, "two")]]))); + +// A call that may change the map runs between the read and the use. +let shared = new Map([[7, new Counter(7, "seven")]]); +function mutate(): void { + shared.set(7, new Counter(70, "seventy")); + shared = new Map(); +} +function callBetween(): string { + const held = shared.get(7); + mutate(); + return held === undefined ? "none" : `${held.num}:${held.label}`; +} +console.log("call", callBetween()); + +// Reads on both branches, a write on one of them, and a read after the join. +function branches(m: Map, k: number, write: boolean): string { + const held = m.get(k); + if (held === undefined) return "absent"; + if (write) m.set(k, new Counter(0, "zero")); + else held.num += 10; + return `${held.num}:${held.label}`; +} +const branchMap = new Map([[4, new Counter(4, "four")]]); +console.log("branches", branches(branchMap, 4, false), branches(branchMap, 4, true), branches(branchMap, 5, true)); + +// The value escapes into an array and a returned object. +function escape(m: Map): Counter[] { + const kept: Counter[] = []; + for (let k = 0; k < 4; k++) { + const value = m.get(k % 2); + if (value !== undefined) kept.push(value); + } + m.clear(); + return kept; +} +const escapes = escape(new Map([[0, new Counter(0, "zero")], [1, new Counter(1, "one")]])); +console.log("escape", escapes.map((c) => c.label).join(",")); +function returned(m: Map): Counter | undefined { + const value = m.get(3); + return value; +} +const returnMap = new Map([[3, new Counter(3, "three")]]); +const got = returned(returnMap); +returnMap.clear(); +console.log("returned", got?.label); + +// String keys, including keys built at run time. +function words(text: string): string { + const seen = new Map(); + for (const word of text.split(" ")) { + const entry = seen.get(word); + if (entry === undefined) seen.set(word, new Counter(1, word.toUpperCase())); + else entry.num++; + } + const parts: string[] = []; + for (const [word, entry] of seen) parts.push(`${word}=${entry.num}/${entry.label}`); + return parts.join(" "); +} +console.log("words", words("a b a c b a")); +const prefix = "k"; +const byName = new Map([["k1", new Counter(1, "one")]]); +const named = byName.get(prefix + "1"); +byName.set("k1", new Counter(2, "two")); +console.log("named", named?.label, byName.get("k1")?.label); + +// A loop whose body writes the map after the read on every pass. +function loopWrites(m: Map): number { + let total = 0; + for (let i = 0; i < 5; i++) { + const value = m.get(i % 2); + if (value !== undefined) total += value.num; + m.set(i % 2, new Counter(i, "loop")); + if (value !== undefined) total += value.num * 100; + } + return total; +} +console.log("loop", loopWrites(new Map([[0, new Counter(1, "a")], [1, new Counter(2, "b")]]))); + +// A local declared from the read is read after a write that replaces the +// entry; it reads the value the map held when the local was declared. +function aliasThenWrite(m: Map): string { + const held = m.get(1); + const copy = held; + m.set(1, new Counter(10, "ten")); + m.delete(1); + const filler = [new Counter(11, "filler"), new Counter(12, "filler")]; + return copy === undefined ? "none" : `${copy.num}:${copy.label}:${filler.length}`; +} +console.log("alias", aliasThenWrite(new Map([[1, new Counter(1, "one")]]))); + +// A read from a map that no local holds: the map is released after the +// declaration, and the value stays readable. +function freshMap(): Map { + return new Map([[1, new Counter(5, "five")]]); +} +function fromTemporary(): string { + const held = freshMap().get(1); + const filler = [new Counter(6, "filler"), new Counter(7, "filler")]; + return held === undefined ? "none" : `${held.num}:${held.label}:${filler.length}`; +} +console.log("temporary", fromTemporary()); + +// A key whose evaluation replaces the field that holds the map: the read +// uses the old map, which only the statement still holds. +class Holder { + m = new Map([[1, new Counter(9, "nine")]]); + key(): number { + this.m = new Map(); + return 1; + } + read(): string { + const held = this.m.get(this.key()); + const filler = [new Counter(10, "filler"), new Counter(11, "filler")]; + return held === undefined ? "none" : `${held.num}:${held.label}:${filler.length}:${this.m.size}`; + } +} +console.log("replaced by key", new Holder().read()); diff --git a/tests/corpus/8181-x509-raw-writes.ts b/tests/corpus/8181-x509-raw-writes.ts new file mode 100644 index 000000000..17704096d --- /dev/null +++ b/tests/corpus/8181-x509-raw-writes.ts @@ -0,0 +1,20 @@ +// A certificate's raw Buffer is the same object at every read, and writing +// into it changes neither the certificate's fields, checks and PEM nor its +// public key, as in Node: the methods read the certificate itself. +import { X509Certificate } from "node:crypto"; +import { readFileSync } from "node:fs"; + +const cert = new X509Certificate(readFileSync("tests/fixtures/server/certs/localhost.pem")); +const ca = new X509Certificate(readFileSync("tests/fixtures/server/certs/ca.pem")); +const raw = cert.raw; +console.log("same Buffer", raw === cert.raw); +const report = (): string => + `${cert.fingerprint256}\n${cert.subject}\n${cert.checkHost("localhost")}\n` + + `${cert.checkIssued(ca)} ${ca.checkIssued(cert)} ${cert.verify(ca.publicKey)}\n` + + `${cert.toString()}\n${cert.publicKey.export({ format: "pem", type: "spki" })}`; +const before = report(); +raw.fill(0); +console.log("unchanged", report() === before); +console.log("raw now", cert.raw.every((b) => b === 0), cert.raw.length); +const other = new X509Certificate(readFileSync("tests/fixtures/server/certs/localhost.pem")); +console.log("fresh certificate raw", other.raw[0], other.raw.equals(cert.raw)); diff --git a/tests/corpus/8182-typed-array-alias-scopes.ts b/tests/corpus/8182-typed-array-alias-scopes.ts new file mode 100644 index 000000000..f56face07 --- /dev/null +++ b/tests/corpus/8182-typed-array-alias-scopes.ts @@ -0,0 +1,134 @@ +// Loops that read one typed array and write another, where the two views +// may share bytes: overlapping and disjoint views of one buffer, the same +// array passed twice, views of different element types over one buffer, +// and empty views, and passes whose views an enclosing loop swaps. A loop +// over views that overlap must see its own writes, and each pass the +// previous pass's writes. +function shiftAdd(dst: Float64Array, src: Float64Array, n: number): void { + for (let i = 0; i < n; i++) dst[i] = src[i]! + 1; +} + +function mixed(bytes: Uint8Array, words: Float64Array, n: number): number { + let sum = 0; + for (let i = 0; i < n; i++) { + words[i] = i * 0.5 + 1; + sum += bytes[i]!; + } + return sum; +} + +function lanes(zr: Float64Array, zi: Float64Array, tr: Float64Array, ti: Float64Array): number { + for (let step = 0; step < 6; step++) + for (let k = 0; k < 4; k++) { + const r = zr[k]!; + const i = zi[k]!; + zi[k] = 2 * r * i + 0.25; + zr[k] = tr[k]! - ti[k]! - 0.5; + tr[k] = zr[k]! * zr[k]!; + ti[k] = zi[k]! * zi[k]!; + } + let sum = 0; + for (let k = 0; k < 4; k++) sum += zr[k]! + zi[k]! + tr[k]! + ti[k]!; + return sum; +} + +function show(a: Float64Array): string { + const parts: string[] = []; + for (let i = 0; i < a.length; i++) parts.push(a[i]!.toFixed(3)); + return parts.join(","); +} + +// Disjoint arrays. +const a = new Float64Array([1, 2, 3, 4]); +const b = new Float64Array(4); +shiftAdd(b, a, 4); +console.log("disjoint", show(b)); + +// One array as both views: each element reads its own old value. +const same = new Float64Array([1, 2, 3, 4]); +shiftAdd(same, same, 4); +console.log("same", show(same)); + +// Overlapping views of one buffer: the destination starts one element +// after the source, so every read sees the previous iteration's write. +const buffer = new ArrayBuffer(8 * 8); +const whole = new Float64Array(buffer); +whole.fill(1); +shiftAdd(new Float64Array(buffer, 8, 6), new Float64Array(buffer, 0, 6), 6); +console.log("overlap forward", show(whole)); +whole.fill(1); +shiftAdd(new Float64Array(buffer, 0, 6), new Float64Array(buffer, 8, 6), 6); +console.log("overlap backward", show(whole)); + +// Adjacent views of one buffer share no bytes. +whole.set([1, 2, 3, 4, 5, 6, 7, 8]); +shiftAdd(new Float64Array(buffer, 32, 4), new Float64Array(buffer, 0, 4), 4); +console.log("adjacent", show(whole)); + +// A byte view over the doubles it writes. +const shared = new ArrayBuffer(64); +console.log("mixed", mixed(new Uint8Array(shared), new Float64Array(shared), 8), show(new Float64Array(shared))); +console.log("mixed disjoint", mixed(new Uint8Array(new ArrayBuffer(64)), new Float64Array(8), 8)); + +// Empty views. +shiftAdd(new Float64Array(0), new Float64Array(0), 0); +console.log("empty", new Float64Array(0).length); + +// Four lanes in separate arrays, and the same arrays aliased. +const lane = (): Float64Array => new Float64Array([0.1, -0.2, 0.3, -0.4]); +console.log("lanes", lanes(lane(), lane(), lane(), lane()).toFixed(6)); +const one = lane(); +console.log("lanes aliased", lanes(one, one, lane(), lane()).toFixed(6)); +const pool = new Float64Array(16); +pool.set([0.1, -0.2, 0.3, -0.4, 0.1, -0.2, 0.3, -0.4, 0.1, -0.2, 0.3, -0.4, 0.1, -0.2, 0.3, -0.4]); +console.log( + "lanes subarrays", + lanes(pool.subarray(0, 4), pool.subarray(4, 8), pool.subarray(8, 12), pool.subarray(12, 16)).toFixed(6), +); +console.log( + "lanes overlapping", + lanes(pool.subarray(0, 4), pool.subarray(2, 6), pool.subarray(8, 12), pool.subarray(9, 13)).toFixed(6), +); + +// Ping-pong passes: an enclosing loop swaps the two views after every pass, +// so each pass reads what the previous one wrote. +function pingPongPairs(a: Float64Array, b: Float64Array, steps: number): string { + let cur = a; + let next = b; + for (let s = 0; s < steps; s++) { + for (let i = 0; i < 2; i++) next[i] = cur[i]! + cur[1 - i]! * 2 + s; + const t = cur; + cur = next; + next = t; + } + return Array.from(cur).join(","); +} +function pingPongWords(a: Int32Array, b: Int32Array): string { + let cur = a; + let next = b; + for (let s = 0; s < 4; s++) { + for (let i = 0; i < 4; i++) next[i] = (cur[i]! * 3 + cur[(i + 1) & 3]! + 1) | 0; + const t = cur; + cur = next; + next = t; + } + return Array.from(cur).join(","); +} +function pingPongRows(a: Float64Array, b: Float64Array, steps: number): number { + let cur = a; + let next = b; + for (let s = 0; s < steps; s++) { + for (let i = 0; i < cur.length; i++) next[i] = cur[i]! * 0.5 + 1; + const t = cur; + cur = next; + next = t; + } + let sum = 0; + for (let i = 0; i < cur.length; i++) sum += cur[i]!; + return sum; +} +for (let k = 0; k < 3; k++) { + console.log("ping-pong pairs", pingPongPairs(new Float64Array([1, 2]), new Float64Array(2), 3 + k)); + console.log("ping-pong words", pingPongWords(new Int32Array([1, 2, 3, 4]), new Int32Array(4))); +} +console.log("ping-pong rows", pingPongRows(new Float64Array(37).fill(8), new Float64Array(37), 9)); diff --git a/tests/corpus/8183-key-passphrase-encoding.cjs b/tests/corpus/8183-key-passphrase-encoding.cjs new file mode 100644 index 000000000..ca817f386 --- /dev/null +++ b/tests/corpus/8183-key-passphrase-encoding.cjs @@ -0,0 +1,33 @@ +// A string passphrase is decoded with the key's encoding, as in Node: +// createPrivateKey's { encoding } and the encoding option of an encrypted +// export both apply to it. +const c = require("node:crypto"); + +const { privateKey } = c.generateKeyPairSync("ec", { namedCurve: "P-256" }); +const pass = "pässwörd"; +const hex = Buffer.from(pass).toString("hex"); +const pem = privateKey.export({ format: "pem", type: "pkcs8", cipher: "aes-256-cbc", passphrase: hex, encoding: "hex" }); +const der = privateKey.export({ format: "der", type: "pkcs8", cipher: "aes-128-cbc", passphrase: pass }); + +// A wrong passphrase usually fails the decryption's padding check +// (ERR_OSSL_BAD_DECRYPT); with a fresh key and IV each run, Node's +// OpenSSL now and then accepts the padding and rejects the decrypted +// structure instead (ERR_OSSL_UNSUPPORTED). Both are reported as one. +function attempt(label, options) { + try { + const key = c.createPrivateKey(options); + console.log(label, key.asymmetricKeyType, key.equals(privateKey)); + } catch (e) { + const wrong = e.code === "ERR_OSSL_BAD_DECRYPT" || e.code === "ERR_OSSL_UNSUPPORTED"; + console.log(label, "throws", wrong ? "a decryption error" : e.code); + } +} + +attempt("utf8 passphrase", { key: pem, passphrase: pass }); +attempt("hex passphrase, encoding hex", { key: Buffer.from(pem), passphrase: hex, encoding: "hex" }); +attempt("base64 passphrase, encoding base64", { key: Buffer.from(pem), passphrase: Buffer.from(pass).toString("base64"), encoding: "base64" }); +attempt("latin1 passphrase, encoding latin1", { key: Buffer.from(pem), passphrase: Buffer.from(pass).toString("latin1"), encoding: "latin1" }); +attempt("hex passphrase, no encoding", { key: pem, passphrase: hex }); +attempt("Buffer passphrase, encoding hex", { key: Buffer.from(pem), passphrase: Buffer.from(pass), encoding: "hex" }); +attempt("der, hex passphrase, encoding hex", { key: der, format: "der", type: "pkcs8", passphrase: hex, encoding: "hex" }); +attempt("hex key string, encoding hex", { key: Buffer.from(pem).toString("hex"), passphrase: hex, encoding: "hex" }); diff --git a/tests/corpus/8190-assert-function-values.cjs b/tests/corpus/8190-assert-function-values.cjs new file mode 100644 index 000000000..f27bf22dc --- /dev/null +++ b/tests/corpus/8190-assert-function-values.cjs @@ -0,0 +1,45 @@ +'use strict'; +// assert methods are function values: stored, passed and called through +// aliases, they assert as the methods do, and every read is the same +// function. A constant bound to a method and called directly reports +// the call as Node quotes it. +const assert = require('assert'); +const { strictEqual, deepStrictEqual } = require('assert'); +const strict = require('assert/strict'); +const EventEmitter = require('events'); +console.log(typeof assert.fail, assert.fail === assert.fail, strict.equal === assert.strictEqual, strictEqual === assert.strictEqual); +const ok = assert.ok; +ok(1); +const tryIt = (label, fn) => { try { fn(); console.log(label, 'passed'); } catch (e) { console.log(label, e.name, e.code, JSON.stringify(e.message), e.generatedMessage); } }; +tryIt('ok', () => ok(0)); +tryIt('ok none', () => ok()); +tryIt('ok msg', () => ok(false, 'nope')); +tryIt('ok err', () => ok(false, new TypeError('own'))); +tryIt('fail', () => assert.fail.call(null)); +tryIt('fail msg', () => (0, assert.fail)('boom')); +tryIt('equal', () => assert.equal.call(null, 1, '1')); +tryIt('equal fail', () => [assert.equal][0](1, 2)); +tryIt('notEqual fail', () => [assert.notEqual][0](1, '1')); +tryIt('strict', () => strictEqual(1, 2)); +tryIt('deep', () => deepStrictEqual({ a: 1 }, { a: 1 })); +tryIt('deep fail', () => [deepStrictEqual][0]({ a: 1 }, { a: 2 }, 'differs')); +tryIt('ifError', () => [assert.ifError][0](null)); +const checks = [assert.ok, assert.fail, assert.strictEqual]; +checks.forEach((check, i) => console.log(i, typeof check, check === checks[i], check.name)); +[1, 'x', true].forEach((value) => assert.ok(value)); +console.log('done'); + +// The comparison methods need both operands, as Node's ERR_MISSING_ARGS +// TypeError says, also through function values. +const comparisons = [assert.equal, assert.notEqual, assert.strictEqual, assert.notStrictEqual, + assert.deepEqual, assert.notDeepEqual, assert.deepStrictEqual, assert.notDeepStrictEqual]; +for (const compare of comparisons) { + for (const args of [[], [1]]) { + try { + compare(...args); + console.log(compare.name, args.length, 'passed'); + } catch (e) { + console.log(compare.name, args.length, e.name, e.code, e.message); + } + } +} diff --git a/tests/corpus/8191-assert-function-values.ts b/tests/corpus/8191-assert-function-values.ts new file mode 100644 index 000000000..8ef24153b --- /dev/null +++ b/tests/corpus/8191-assert-function-values.ts @@ -0,0 +1,16 @@ +// assert methods read as values in TypeScript: a constant alias called +// directly, a stored value, and a method passed as a callback. +import assert from "node:assert"; +const eq = assert.strictEqual; +eq(1, 1); +const fail = assert.fail; +const checks: unknown[] = [assert.ok, assert.fail]; +console.log(typeof checks[0], checks[1] === fail); +try { + eq(1, 2, "one is not two"); +} catch (error) { + console.log((error as Error).message); +} +const run = (fn: (v: unknown) => void) => fn(true); +run(assert.ok); +console.log("done"); diff --git a/tests/corpus/8193-counted-loop-products.ts b/tests/corpus/8193-counted-loop-products.ts new file mode 100644 index 000000000..d347aee76 --- /dev/null +++ b/tests/corpus/8193-counted-loop-products.ts @@ -0,0 +1,59 @@ +// Nested counted loops whose bounds arrive at run time, with the counters' +// products reaching bitwise operators. Starts and limits take the integer +// copy or the double copy: negative, fractional, NaN, -0, and starts or +// limits past 2^25, past which the guarded copy no longer proves products +// exact. +function a(i: number, j: number): number { + return (((i + j) * (i + j + 1)) >>> 1) + i + 1; +} + +function rows(start: number, end: number, n: number): number[] { + const out: number[] = []; + for (let i = start; i < end; i++) { + let t = 0; + for (let j = 0; j < n; j++) t += 1 / a(i, j); + out.push(t); + } + return out; +} + +function mixed(start: number, end: number, n: number): number { + let h = 0; + for (let i = start; i < end; i++) { + for (let j = start; j < n; j++) h = (h + ((i - j) * (j - i + 3) | 0)) | 0; + } + return h; +} + +function wide(start: number, count: number): number[] { + const out: number[] = []; + for (let i = start; i < start + count; i++) out.push(((i * (i + 1)) >>> 0) ^ (i * i) | 0); + return out; +} + +const args = process.argv.slice(2).map(Number); +const big = 2 ** 25; +for (const [start, end, n] of [ + [0, 4, 6], + [-3, 2, 5], + [1.5, 4, 3], + [-0, 2, 2], + [NaN, 2, 2], + [big - 2, big + 2, 3], + [0, 3, args.length > 0 ? big + 1 : 4], +]) { + console.log(start, end, n, rows(start!, end!, Math.min(n!, 8)).map((x) => x.toFixed(12)).join(" ")); + console.log("mixed", mixed(start!, end!, Math.min(n!, 8))); +} +for (const start of [0, -5, 2 ** 26, 2 ** 31 - 2, 2 ** 52, -(2 ** 40), 0.5]) console.log(start, wide(start, 4).join(" ")); +console.log(Object.is(((-0 * 5) >>> 0), 0), a(-1, 0), a(0, -1), a(2 ** 30, 2 ** 30)); + +// Single-expression functions inlined at calls that pass literals and +// locals, also read as values and called with other arguments. +function mix(x: number, y: number): number { + return ((x * 31 + y) ^ (y << 3)) % 1000; +} +const table = [mix, (x: number, y: number) => x - y]; +let acc = 0; +for (let k = 0; k < 5; k++) acc += mix(k, 7) + mix(3, k) + table[k % 2]!(k, 2) + mix(k * 2, args.length); +console.log(acc, mix(2 ** 40, -0), mix(NaN, 1), mix(Infinity, 2), [1, 2, 3].map((v) => mix(v, v)).join()); diff --git a/tests/corpus/8194-row-pair-sums.ts b/tests/corpus/8194-row-pair-sums.ts new file mode 100644 index 000000000..f28242ff5 --- /dev/null +++ b/tests/corpus/8194-row-pair-sums.ts @@ -0,0 +1,90 @@ +// Row sums (`for i { t = 0; for j t += term; out[i] = t }`) over typed +// arrays, run with even and odd row counts, an empty range, fractional, +// negative-zero and out-of-int32 bounds, and output arrays that are the +// input array or overlap it in one buffer, so a row may read an earlier +// row's result. +function rows(u: Float64Array, v: Float64Array, start: number, end: number, n: number): void { + for (let i = start; i < end; i++) { + let t = 0; + for (let j = 0; j < n; j++) t += u[j]! / ((((i + j) * (i + j + 1)) >>> 1) + i + 1); + v[i] = t; + } +} + +function weighted(u: Float32Array, w: Int32Array, v: Float64Array, start: number, end: number): void { + for (let i = start; i < end; i++) { + let t = 0.5; + for (let j = 0; j < u.length; j++) t += u[j]! * w[(i + j) % w.length]! - i * 0.25; + v[i] = t; + } +} + +const show = (label: string, v: Float64Array): void => { + const parts: string[] = []; + for (const x of v) parts.push(Object.is(x, -0) ? "-0" : x.toPrecision(17)); + console.log(label, parts.join(" ")); +}; + +const n = Number(process.argv[2] ?? 7); +const u = new Float64Array(n).map((_, k) => k + 1); +for (const [start, end] of [ + [0, n], + [0, n - 1], + [1, n], + [2, 2], + [3, 1], + [-0, 3], + [0.5, 4], + [0, 4.5], + [2 ** 31 - 3, 2 ** 31 + 1], + [NaN, 3], +]) { + const v = new Float64Array(n + 2); + rows(u, v, start!, end!, n); + show(`rows ${start} ${end}`, v); +} + +// The output is the input: each row reads the rows stored before it. +const same = new Float64Array(n).map((_, k) => k + 1); +rows(same, same, 0, n, n); +show("same", same); + +// Overlapping views of one buffer, offset by one element. +const buffer = new ArrayBuffer(8 * (n + 1)); +const lo = new Float64Array(buffer, 0, n).map((_, k) => k + 1); +const hi = new Float64Array(buffer, 8, n); +rows(lo, hi, 0, n, n); +show("overlap", new Float64Array(buffer)); + +// Disjoint views of one buffer. +const shared = new ArrayBuffer(16 * n); +const a = new Float64Array(shared, 0, n).map((_, k) => n - k); +const b = new Float64Array(shared, 8 * n, n); +rows(a, b, 0, n, n); +show("disjoint", b); + +const f = new Float32Array(n).map((_, k) => k / 3); +const w = new Int32Array([3, -1, 4, 1, -5]); +const out = new Float64Array(n); +weighted(f, w, out, 0, n); +show("weighted", out); +weighted(f, w, out, 1, n - 1); +show("weighted inner", out); + +// Arrays a program assigns only when the row range is nonempty: an empty or +// one-row range never reads them, and the paired copy does not either. +function lazyRows(n: number, start: number, end: number): number { + let u!: Float64Array; + let v!: Float64Array; + if (end > start) { + u = new Float64Array(n).fill(1); + v = new Float64Array(end); + } + for (let i = start; i < end; i++) { + let t = 0; + for (let j = 0; j < n; j++) t += u[j]! / (i + j + 1); + v[i] = t; + } + return end > start ? v[end - 1]! : -1; +} +console.log("lazy", lazyRows(4, 3, 3), lazyRows(4, 2, 3), lazyRows(4, 0, 5), lazyRows(0, 5, 5)); diff --git a/tests/corpus/8195-wrapping-products-near-2-53.ts b/tests/corpus/8195-wrapping-products-near-2-53.ts new file mode 100644 index 000000000..c42104290 --- /dev/null +++ b/tests/corpus/8195-wrapping-products-near-2-53.ts @@ -0,0 +1,25 @@ +// Products that reach bitwise operators with operands whose ranges are +// known, around 2^53: below it the double product is exact, and from it on +// the double product rounds, so the 32-bit result comes from the rounded +// value (3 * 3002399751580331 is 2^53 + 1, which rounds to 2^53). +const pairs: [number, number][] = [ + [3, 3002399751580331], + [-3, 3002399751580331], + [2 ** 26, 2 ** 27], + [94906265, 94906267], + [94906267, 94906267], + [7, 1286742750677284], + [65535, 137443147777], +]; +for (const [a, b] of pairs) console.log(a, b, (a * b) | 0, (a * b) >>> 0, (a * b) ^ 1, (a * b) << 3); + +// The same products with literal operands, which the compiler sees whole. +console.log((3 * 3002399751580331) | 0, (3 * 3002399751580331) >>> 0); +console.log((-3 * 3002399751580331) | 0, (2 ** 26 * 2 ** 27) | 0); +console.log((94906267 * 94906267) | 0, (94906265 * 94906267) >>> 0); + +// A counted loop whose counter multiplies a constant past 2^53. +const step = 1286742750677284; +let h = 0; +for (let k = 5; k < 9; k++) h = (h + ((k * step) | 0)) | 0; +console.log(h); diff --git a/tests/corpus/8196-counted-loops-integer-starts.ts b/tests/corpus/8196-counted-loops-integer-starts.ts new file mode 100644 index 000000000..d608bc4b5 --- /dev/null +++ b/tests/corpus/8196-counted-loops-integer-starts.ts @@ -0,0 +1,50 @@ +// Counted, paired and dense array loops whose start is an int32 value +// (an int32 array element, `p0 | 0` or one minus either): starts at -1, 0, +// inside the array, at its end, and at the int32 extremes, where the loop +// must not run or must stop at the array's end as Node does. Number arrays +// and an Int32Array both run their paired loops. +function reverse(pp: number[], p0: number): number { + let swaps = 0; + for (let i = 1, j = (p0 | 0) - 1; i < j; i++, j--) { + const t = pp[i]!; + pp[i] = pp[j]!; + pp[j] = t; + swaps++; + } + return swaps; +} + +function reverseBytes(values: Int32Array, p0: number): number { + let swaps = 0; + for (let i = 0, j = (p0 | 0) - 1; i < j; i++, j--) { + const t = values[i]!; + values[i] = values[j]!; + values[j] = t; + swaps++; + } + return swaps; +} + +function sumFrom(values: number[], start: number): number { + let total = 0; + for (let k = start - 1; k < values.length; k++) total += values[k] ?? 100; + return total; +} + +function countdown(values: Int32Array, from: number): number { + let total = 0; + for (let k = from; k >= 0; k--) total += values[k] ?? 1000; + return total; +} + +const starts: number[] = [0, 1, 2, 5, 9, 10, 11, 2147483647, -2147483648]; +const ints = new Int32Array(starts); +for (let s = 0; s < ints.length; s++) { + const p0 = ints[s]!; + const pp = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; + const swaps = p0 <= 11 ? reverse(pp, p0) : 0; + const bytes = new Int32Array(pp); + const byteSwaps = p0 <= 11 ? reverseBytes(bytes, p0) : 0; + console.log(p0, swaps, pp.join(""), p0 <= 11 && p0 >= -1 ? sumFrom(pp, p0) : "-", p0 <= 11 ? countdown(ints, p0) : "-"); + console.log(" bytes", byteSwaps, bytes.join("")); +} diff --git a/tests/corpus/8210-require-computed-missing/main.ts b/tests/corpus/8210-require-computed-missing/main.ts new file mode 100644 index 000000000..685222157 --- /dev/null +++ b/tests/corpus/8210-require-computed-missing/main.ts @@ -0,0 +1,7 @@ +// A package that probes for optional files through createRequire with +// computed specifiers. A specifier that names no file throws Node's error, +// so the package takes its absent branch as in Node. +import { probeAddon, requireErrors } from "optional-probe"; + +console.log(probeAddon("clipboard")); +for (const line of requireErrors()) console.log(line); diff --git a/tests/corpus/8210-require-computed-missing/node_modules/optional-probe/index.d.ts b/tests/corpus/8210-require-computed-missing/node_modules/optional-probe/index.d.ts new file mode 100644 index 000000000..4eb024b0c --- /dev/null +++ b/tests/corpus/8210-require-computed-missing/node_modules/optional-probe/index.d.ts @@ -0,0 +1,2 @@ +export declare function probeAddon(name: string): string; +export declare function requireErrors(): string[]; diff --git a/tests/corpus/8210-require-computed-missing/node_modules/optional-probe/index.js b/tests/corpus/8210-require-computed-missing/node_modules/optional-probe/index.js new file mode 100644 index 000000000..c824302ba --- /dev/null +++ b/tests/corpus/8210-require-computed-missing/node_modules/optional-probe/index.js @@ -0,0 +1,46 @@ +import { createRequire } from "node:module"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const importer = fileURLToPath(import.meta.url); +const load = createRequire(import.meta.url); +const here = dirname(importer); + +// Looks for a prebuilt addon in each packaging location. +export function probeAddon(name) { + const codes = []; + const dirs = [join(here, "prebuilds", `${process.platform}-${process.arch}`), join(here, "native"), here]; + for (const dir of dirs) { + try { + const addon = load(join(dir, `${name}.node`)); + return `loaded ${typeof addon}`; + } catch (error) { + codes.push(error.code); + } + } + return `absent: ${codes.join(", ")}`; +} + +function describe(spec) { + try { + load(spec); + return "loaded"; + } catch (error) { + return `${error.name} ${error.code}: ${error.message.replaceAll(importer, "")}`; + } +} + +export function requireErrors() { + const specs = [ + "./missing.js", + "./missing", + "../missing-directory/", + "scriptc-corpus-missing-package", + "@scriptc-corpus/missing/sub", + "node:not-a-builtin", + "", + 42, + undefined, + ]; + return specs.map((spec) => `${typeof spec === "string" ? JSON.stringify(spec) : String(spec)} -> ${describe(spec)}`); +} diff --git a/tests/corpus/8210-require-computed-missing/node_modules/optional-probe/package.json b/tests/corpus/8210-require-computed-missing/node_modules/optional-probe/package.json new file mode 100644 index 000000000..50ad53129 --- /dev/null +++ b/tests/corpus/8210-require-computed-missing/node_modules/optional-probe/package.json @@ -0,0 +1 @@ +{ "name": "optional-probe", "version": "1.0.0", "type": "module", "main": "index.js", "types": "index.d.ts" } diff --git a/tests/corpus/8211-require-resolve-computed/main.ts b/tests/corpus/8211-require-resolve-computed/main.ts new file mode 100644 index 000000000..ab94d0625 --- /dev/null +++ b/tests/corpus/8211-require-resolve-computed/main.ts @@ -0,0 +1,9 @@ +// require.resolve with requests computed at run time, called directly and +// read as a value, resolves like Node: builtins, relative files with +// Node's extension search, package "main" and "exports" entries (a +// package.json that starts with a byte order mark included, written at run +// time), a package's own name, and MODULE_NOT_FOUND for requests that name +// nothing. +import { resolveAll } from "resolve-probe"; + +for (const line of resolveAll()) console.log(line); diff --git a/tests/corpus/8211-require-resolve-computed/node_modules/@scope/scoped/lib/node.js b/tests/corpus/8211-require-resolve-computed/node_modules/@scope/scoped/lib/node.js new file mode 100644 index 000000000..c414b6b26 --- /dev/null +++ b/tests/corpus/8211-require-resolve-computed/node_modules/@scope/scoped/lib/node.js @@ -0,0 +1 @@ +module.exports = "node"; diff --git a/tests/corpus/8211-require-resolve-computed/node_modules/@scope/scoped/lib/other.js b/tests/corpus/8211-require-resolve-computed/node_modules/@scope/scoped/lib/other.js new file mode 100644 index 000000000..7f4090ad9 --- /dev/null +++ b/tests/corpus/8211-require-resolve-computed/node_modules/@scope/scoped/lib/other.js @@ -0,0 +1 @@ +module.exports = "other"; diff --git a/tests/corpus/8211-require-resolve-computed/node_modules/@scope/scoped/package.json b/tests/corpus/8211-require-resolve-computed/node_modules/@scope/scoped/package.json new file mode 100644 index 000000000..25326e6e5 --- /dev/null +++ b/tests/corpus/8211-require-resolve-computed/node_modules/@scope/scoped/package.json @@ -0,0 +1 @@ +{ "name": "@scope/scoped", "version": "1.0.0", "exports": { "node": "./lib/node.js", "default": "./lib/other.js" } } diff --git a/tests/corpus/8211-require-resolve-computed/node_modules/plain-index/index.js b/tests/corpus/8211-require-resolve-computed/node_modules/plain-index/index.js new file mode 100644 index 000000000..0d24e266f --- /dev/null +++ b/tests/corpus/8211-require-resolve-computed/node_modules/plain-index/index.js @@ -0,0 +1 @@ +module.exports = "index"; diff --git a/tests/corpus/8211-require-resolve-computed/node_modules/resolve-probe/data.json b/tests/corpus/8211-require-resolve-computed/node_modules/resolve-probe/data.json new file mode 100644 index 000000000..6d94e13bd --- /dev/null +++ b/tests/corpus/8211-require-resolve-computed/node_modules/resolve-probe/data.json @@ -0,0 +1 @@ +{ "kind": "data" } diff --git a/tests/corpus/8211-require-resolve-computed/node_modules/resolve-probe/index.d.ts b/tests/corpus/8211-require-resolve-computed/node_modules/resolve-probe/index.d.ts new file mode 100644 index 000000000..1b8cc8a0e --- /dev/null +++ b/tests/corpus/8211-require-resolve-computed/node_modules/resolve-probe/index.d.ts @@ -0,0 +1 @@ +export declare function resolveAll(): string[]; diff --git a/tests/corpus/8211-require-resolve-computed/node_modules/resolve-probe/index.js b/tests/corpus/8211-require-resolve-computed/node_modules/resolve-probe/index.js new file mode 100644 index 000000000..8d581c90e --- /dev/null +++ b/tests/corpus/8211-require-resolve-computed/node_modules/resolve-probe/index.js @@ -0,0 +1,71 @@ +import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs"; +import { createRequire } from "node:module"; +import { tmpdir } from "node:os"; +import { dirname, join, relative } from "node:path"; +import { fileURLToPath } from "node:url"; + +const importer = fileURLToPath(import.meta.url); +const root = dirname(dirname(dirname(importer))); +const load = createRequire(import.meta.url); + +function show(resolved) { + return resolved.startsWith("/") || /^[A-Za-z]:\\/.test(resolved) ? relative(root, resolved).replaceAll("\\", "/") : resolved; +} + +function describe(resolve, request) { + try { + return show(resolve(request)); + } catch (error) { + return `${error.name} ${error.code}: ${error.message.replaceAll(importer, "")}`; + } +} + +const requests = [ + "fs", + "node:fs", + "node:test", + "./data.json", + "./data", + "./index", + "resolve-probe", + "resolve-probe/data", + "with-exports", + "with-exports/feature", + "with-exports/lib/helper", + "with-main", + "plain-index", + "@scope/scoped", + "./missing", + "scriptc-corpus-missing-package", + "", + 42, +]; + +// A package whose package.json starts with a byte order mark, written to a +// fresh directory and resolved by its absolute path: "main" is still read. +// The directory is named by its real path (the temporary directory is a +// symbolic link on macOS), so the resolved path is relative to it. +function resolveWithByteOrderMark() { + const dir = realpathSync(mkdtempSync(join(tmpdir(), "scriptc-bom-"))); + try { + const pkg = join(dir, "with-bom"); + mkdirSync(join(pkg, "lib"), { recursive: true }); + const manifest = JSON.stringify({ name: "with-bom", main: "lib/entry.js" }); + writeFileSync(join(pkg, "package.json"), "\uFEFF" + manifest); + writeFileSync(join(pkg, "lib", "entry.js"), 'module.exports = "with-bom";\n'); + return relative(pkg, load.resolve(pkg)); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +} + +export function resolveAll(options = {}) { + const resolve = options.resolve ?? load.resolve; + const label = (request) => (typeof request === "string" ? JSON.stringify(request) : String(request)); + return [ + ...requests.map((request) => `call ${label(request)} -> ${describe((r) => load.resolve(r), request)}`), + ...requests.slice(0, 4).map((request) => `value ${label(request)} -> ${describe(resolve, request)}`), + `options ${describe((r) => load.resolve(r, {}), "with-main")}`, + `byte order mark -> ${resolveWithByteOrderMark()}`, + ]; +} diff --git a/tests/corpus/8211-require-resolve-computed/node_modules/resolve-probe/package.json b/tests/corpus/8211-require-resolve-computed/node_modules/resolve-probe/package.json new file mode 100644 index 000000000..d0935f646 --- /dev/null +++ b/tests/corpus/8211-require-resolve-computed/node_modules/resolve-probe/package.json @@ -0,0 +1 @@ +{ "name": "resolve-probe", "version": "1.0.0", "type": "module", "exports": { ".": { "types": "./index.d.ts", "default": "./index.js" }, "./data": "./data.json" } } diff --git a/tests/corpus/8211-require-resolve-computed/node_modules/with-exports/cjs.cjs b/tests/corpus/8211-require-resolve-computed/node_modules/with-exports/cjs.cjs new file mode 100644 index 000000000..2cd122bdc --- /dev/null +++ b/tests/corpus/8211-require-resolve-computed/node_modules/with-exports/cjs.cjs @@ -0,0 +1 @@ +module.exports = "cjs"; diff --git a/tests/corpus/8211-require-resolve-computed/node_modules/with-exports/esm.mjs b/tests/corpus/8211-require-resolve-computed/node_modules/with-exports/esm.mjs new file mode 100644 index 000000000..06c676e54 --- /dev/null +++ b/tests/corpus/8211-require-resolve-computed/node_modules/with-exports/esm.mjs @@ -0,0 +1 @@ +export default "esm"; diff --git a/tests/corpus/8211-require-resolve-computed/node_modules/with-exports/lib/feature.js b/tests/corpus/8211-require-resolve-computed/node_modules/with-exports/lib/feature.js new file mode 100644 index 000000000..33535fc6f --- /dev/null +++ b/tests/corpus/8211-require-resolve-computed/node_modules/with-exports/lib/feature.js @@ -0,0 +1 @@ +module.exports = "feature"; diff --git a/tests/corpus/8211-require-resolve-computed/node_modules/with-exports/lib/helper.js b/tests/corpus/8211-require-resolve-computed/node_modules/with-exports/lib/helper.js new file mode 100644 index 000000000..4f51f7bfa --- /dev/null +++ b/tests/corpus/8211-require-resolve-computed/node_modules/with-exports/lib/helper.js @@ -0,0 +1 @@ +module.exports = "helper"; diff --git a/tests/corpus/8211-require-resolve-computed/node_modules/with-exports/main.js b/tests/corpus/8211-require-resolve-computed/node_modules/with-exports/main.js new file mode 100644 index 000000000..0470f8201 --- /dev/null +++ b/tests/corpus/8211-require-resolve-computed/node_modules/with-exports/main.js @@ -0,0 +1 @@ +module.exports = "main"; diff --git a/tests/corpus/8211-require-resolve-computed/node_modules/with-exports/package.json b/tests/corpus/8211-require-resolve-computed/node_modules/with-exports/package.json new file mode 100644 index 000000000..09d74787c --- /dev/null +++ b/tests/corpus/8211-require-resolve-computed/node_modules/with-exports/package.json @@ -0,0 +1 @@ +{ "name": "with-exports", "version": "1.0.0", "main": "./main.js", "exports": { ".": { "import": "./esm.mjs", "require": "./cjs.cjs" }, "./feature": "./lib/feature.js", "./lib/*": "./lib/*.js", "./private/*": null } } diff --git a/tests/corpus/8211-require-resolve-computed/node_modules/with-main/lib/entry.js b/tests/corpus/8211-require-resolve-computed/node_modules/with-main/lib/entry.js new file mode 100644 index 000000000..6555fae7c --- /dev/null +++ b/tests/corpus/8211-require-resolve-computed/node_modules/with-main/lib/entry.js @@ -0,0 +1 @@ +module.exports = "entry"; diff --git a/tests/corpus/8211-require-resolve-computed/node_modules/with-main/package.json b/tests/corpus/8211-require-resolve-computed/node_modules/with-main/package.json new file mode 100644 index 000000000..8c473085d --- /dev/null +++ b/tests/corpus/8211-require-resolve-computed/node_modules/with-main/package.json @@ -0,0 +1 @@ +{ "name": "with-main", "version": "1.0.0", "main": "lib/entry" } diff --git a/tests/corpus/8212-native-helper-probe-no-display/main.ts b/tests/corpus/8212-native-helper-probe-no-display/main.ts new file mode 100644 index 000000000..a9e021017 --- /dev/null +++ b/tests/corpus/8212-native-helper-probe-no-display/main.ts @@ -0,0 +1,18 @@ +// A terminal package that loads prebuilt native helpers the way pi-tui does: +// it builds candidate paths (through require.resolve read as a value) and +// requires each one inside a try block. On Linux without DISPLAY the +// clipboard helper is never loaded, and on other platforms no helper for +// the platform is installed, so the package uses its JavaScript fallback as +// in Node. +import { getNativeClipboard, getNativeModuleCandidates, isNativeModifierPressed } from "native-helper"; +import { relative } from "node:path"; + +delete process.env["DISPLAY"]; +console.log("clipboard", typeof getNativeClipboard()); +console.log("modifier", isNativeModifierPressed("shift")); +const root = import.meta.dirname; +for (const candidate of getNativeModuleCandidates("native/helper.node")) { + console.log("candidate", relative(root, candidate).replaceAll("\\", "/")); +} +const custom = getNativeModuleCandidates("native/helper.node", { resolvePackage: () => "/opt/helper/dist/index.js" }); +console.log("custom", custom[0]!.replaceAll("\\", "/")); diff --git a/tests/corpus/8212-native-helper-probe-no-display/node_modules/native-helper/dist/index.d.ts b/tests/corpus/8212-native-helper-probe-no-display/node_modules/native-helper/dist/index.d.ts new file mode 100644 index 000000000..8803ba552 --- /dev/null +++ b/tests/corpus/8212-native-helper-probe-no-display/node_modules/native-helper/dist/index.d.ts @@ -0,0 +1,11 @@ +export interface NativeHelper { + getText(): string; + getImage(): Uint8Array | undefined; + isModifierPressed?(key: string): boolean; +} +export declare function getNativeClipboard(): NativeHelper | undefined; +export declare function isNativeModifierPressed(key: string): boolean; +export declare function getNativeModuleCandidates( + nativePath: string, + options?: { moduleUrl?: string; resolvePackage?: (id: string) => string; execPath?: string }, +): string[]; diff --git a/tests/corpus/8212-native-helper-probe-no-display/node_modules/native-helper/dist/index.js b/tests/corpus/8212-native-helper-probe-no-display/node_modules/native-helper/dist/index.js new file mode 100644 index 000000000..874130379 --- /dev/null +++ b/tests/corpus/8212-native-helper-probe-no-display/node_modules/native-helper/dist/index.js @@ -0,0 +1,2 @@ +export { getNativeClipboard, isNativeModifierPressed } from "./native-platform.js"; +export { getNativeModuleCandidates } from "./native-module-path.js"; diff --git a/tests/corpus/8212-native-helper-probe-no-display/node_modules/native-helper/dist/native-module-path.js b/tests/corpus/8212-native-helper-probe-no-display/node_modules/native-helper/dist/native-module-path.js new file mode 100644 index 000000000..2a82b41f7 --- /dev/null +++ b/tests/corpus/8212-native-helper-probe-no-display/node_modules/native-helper/dist/native-module-path.js @@ -0,0 +1,20 @@ +import { createRequire } from "node:module"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const moduleRequire = createRequire(import.meta.url); + +// Where a prebuilt helper can live: next to the installed package, next to +// this module, and next to the executable. +export function getNativeModuleCandidates(nativePath, options = {}) { + const moduleDir = dirname(fileURLToPath(options.moduleUrl ?? import.meta.url)); + const candidates = []; + try { + const entry = (options.resolvePackage ?? moduleRequire.resolve)("native-helper"); + candidates.push(join(dirname(entry), "..", nativePath)); + } catch { + // A standalone executable has no installed package. + } + candidates.push(join(moduleDir, "..", nativePath), join(moduleDir, nativePath)); + return Array.from(new Set(candidates)); +} diff --git a/tests/corpus/8212-native-helper-probe-no-display/node_modules/native-helper/dist/native-platform.js b/tests/corpus/8212-native-helper-probe-no-display/node_modules/native-helper/dist/native-platform.js new file mode 100644 index 000000000..9aaed1dc1 --- /dev/null +++ b/tests/corpus/8212-native-helper-probe-no-display/node_modules/native-helper/dist/native-platform.js @@ -0,0 +1,48 @@ +import { createRequire } from "node:module"; +import * as path from "node:path"; +import { getNativeModuleCandidates } from "./native-module-path.js"; + +const load = createRequire(import.meta.url); +const helpers = new Map(); + +function loadHelper(platform, suffix = "") { + const arch = process.arch; + if (arch !== "x64" && arch !== "arm64") return undefined; + const nativePath = path.join("native", platform, "prebuilds", `${platform}-${arch}`, `${platform}-helper${suffix}.node`); + if (helpers.has(nativePath)) return helpers.get(nativePath); + for (const modulePath of getNativeModuleCandidates(nativePath)) { + try { + const helper = load(modulePath); + if (typeof helper?.getText === "function") { + helpers.set(nativePath, helper); + return helper; + } + } catch { + // Try the next location. + } + } + helpers.set(nativePath, undefined); + return undefined; +} + +function getPlatformHelper() { + if (process.platform !== "darwin" && process.platform !== "win32") return undefined; + return loadHelper(process.platform); +} + +// The clipboard helper opens the X display, so it loads only when one is set. +export function getNativeClipboard() { + if (process.platform !== "linux") return getPlatformHelper(); + if (!process.env.DISPLAY) return undefined; + return loadHelper("linux", "-x11"); +} + +export function isNativeModifierPressed(key) { + const helper = getPlatformHelper(); + if (!helper?.isModifierPressed) return false; + try { + return helper.isModifierPressed(key) === true; + } catch { + return false; + } +} diff --git a/tests/corpus/8212-native-helper-probe-no-display/node_modules/native-helper/native/linux/prebuilds/linux-arm64/linux-helper-x11.node b/tests/corpus/8212-native-helper-probe-no-display/node_modules/native-helper/native/linux/prebuilds/linux-arm64/linux-helper-x11.node new file mode 100644 index 000000000..72d2c39bd --- /dev/null +++ b/tests/corpus/8212-native-helper-probe-no-display/node_modules/native-helper/native/linux/prebuilds/linux-arm64/linux-helper-x11.node @@ -0,0 +1 @@ +placeholder for a prebuilt X11 clipboard helper diff --git a/tests/corpus/8212-native-helper-probe-no-display/node_modules/native-helper/native/linux/prebuilds/linux-x64/linux-helper-x11.node b/tests/corpus/8212-native-helper-probe-no-display/node_modules/native-helper/native/linux/prebuilds/linux-x64/linux-helper-x11.node new file mode 100644 index 000000000..72d2c39bd --- /dev/null +++ b/tests/corpus/8212-native-helper-probe-no-display/node_modules/native-helper/native/linux/prebuilds/linux-x64/linux-helper-x11.node @@ -0,0 +1 @@ +placeholder for a prebuilt X11 clipboard helper diff --git a/tests/corpus/8212-native-helper-probe-no-display/node_modules/native-helper/package.json b/tests/corpus/8212-native-helper-probe-no-display/node_modules/native-helper/package.json new file mode 100644 index 000000000..475c1d5af --- /dev/null +++ b/tests/corpus/8212-native-helper-probe-no-display/node_modules/native-helper/package.json @@ -0,0 +1 @@ +{ "name": "native-helper", "version": "1.0.0", "type": "module", "main": "dist/index.js", "types": "dist/index.d.ts" } diff --git a/tests/corpus/8213-cycle-star-reexport/main.ts b/tests/corpus/8213-cycle-star-reexport/main.ts new file mode 100644 index 000000000..04cd7cea1 --- /dev/null +++ b/tests/corpus/8213-cycle-star-reexport/main.ts @@ -0,0 +1,11 @@ +// A module of an import cycle re-exports another member with `export *` +// (pi-coding-agent's sdk.js → agent-session-runtime.js → +// agent-session-services.js → sdk.js). The re-exported names are live +// bindings of the member that declares them, so they read the same values, +// and the same temporal dead zone errors, as in Node. +import { createSession, describeRuntime, Runtime, SDK_VERSION } from "./sdk.ts"; +import { servicesReport } from "./services.ts"; + +console.log(servicesReport()); +console.log(createSession("main").describe()); +console.log(describeRuntime(), new Runtime("direct").name, SDK_VERSION); diff --git a/tests/corpus/8213-cycle-star-reexport/runtime.ts b/tests/corpus/8213-cycle-star-reexport/runtime.ts new file mode 100644 index 000000000..f3653d255 --- /dev/null +++ b/tests/corpus/8213-cycle-star-reexport/runtime.ts @@ -0,0 +1,17 @@ +import { createServices, servicesLabel } from "./services.ts"; + +export class Runtime { + readonly name: string; + constructor(name: string) { + this.name = name; + } + describe(): string { + return `${this.name} runtime with ${servicesLabel}`; + } +} + +export const runtimeLabel = "runtime"; + +export function describeRuntime(): string { + return `${runtimeLabel} (${typeof createServices})`; +} diff --git a/tests/corpus/8213-cycle-star-reexport/sdk.ts b/tests/corpus/8213-cycle-star-reexport/sdk.ts new file mode 100644 index 000000000..7d67384c4 --- /dev/null +++ b/tests/corpus/8213-cycle-star-reexport/sdk.ts @@ -0,0 +1,9 @@ +import { createServices } from "./services.ts"; + +export * from "./runtime.ts"; + +export const SDK_VERSION = "1.4.0"; + +export function createSession(name: string) { + return createServices(name); +} diff --git a/tests/corpus/8213-cycle-star-reexport/services.ts b/tests/corpus/8213-cycle-star-reexport/services.ts new file mode 100644 index 000000000..62e6b684e --- /dev/null +++ b/tests/corpus/8213-cycle-star-reexport/services.ts @@ -0,0 +1,27 @@ +import { Runtime, SDK_VERSION, describeRuntime, runtimeLabel } from "./sdk.ts"; + +export const servicesLabel = "services"; + +function probe(read: () => unknown): string { + try { + return String(read()); + } catch (error) { + return `${(error as Error).name}: ${(error as Error).message}`; + } +} + +// This module runs first: sdk.ts and runtime.ts have not run yet. +const early = [ + probe(() => SDK_VERSION), + probe(() => runtimeLabel), + probe(() => typeof describeRuntime), + probe(() => new Runtime("early").name), +]; + +export function servicesReport(): string { + return `early ${early.join(" | ")}; late ${SDK_VERSION} ${runtimeLabel}`; +} + +export function createServices(name: string): Runtime { + return new Runtime(name); +} diff --git a/tests/corpus/8214-unshimmed-package-version/main.ts b/tests/corpus/8214-unshimmed-package-version/main.ts new file mode 100644 index 000000000..d582a339b --- /dev/null +++ b/tests/corpus/8214-unshimmed-package-version/main.ts @@ -0,0 +1,5 @@ +// A package that scriptc implements at some major versions (chalk 5) +// compiles from its own source at a version without an implementation. +import chalk, { level } from "chalk"; + +console.log(chalk.version, level, chalk.red("red"), chalk.bold("bold")); diff --git a/tests/corpus/8214-unshimmed-package-version/node_modules/chalk/package.json b/tests/corpus/8214-unshimmed-package-version/node_modules/chalk/package.json new file mode 100644 index 000000000..7ab814eb0 --- /dev/null +++ b/tests/corpus/8214-unshimmed-package-version/node_modules/chalk/package.json @@ -0,0 +1 @@ +{ "name": "chalk", "version": "6.0.0", "type": "module", "exports": { "types": "./source/index.d.ts", "default": "./source/index.js" }, "imports": { "#styles": "./source/styles.js" } } diff --git a/tests/corpus/8214-unshimmed-package-version/node_modules/chalk/source/index.d.ts b/tests/corpus/8214-unshimmed-package-version/node_modules/chalk/source/index.d.ts new file mode 100644 index 000000000..84dd75758 --- /dev/null +++ b/tests/corpus/8214-unshimmed-package-version/node_modules/chalk/source/index.d.ts @@ -0,0 +1,3 @@ +export declare const level: number; +declare const chalk: { version: number; red(text: string): string; bold(text: string): string }; +export default chalk; diff --git a/tests/corpus/8214-unshimmed-package-version/node_modules/chalk/source/index.js b/tests/corpus/8214-unshimmed-package-version/node_modules/chalk/source/index.js new file mode 100644 index 000000000..2d85a4ae3 --- /dev/null +++ b/tests/corpus/8214-unshimmed-package-version/node_modules/chalk/source/index.js @@ -0,0 +1,16 @@ +import { styles } from "#styles"; + +export const level = 0; + +function paint(name, text) { + const [open, close] = styles[name]; + return level > 0 ? `\u001B[${open}m${text}\u001B[${close}m` : `${text}`; +} + +const chalk = { + version: 6, + red: (text) => paint("red", text), + bold: (text) => paint("bold", text), +}; + +export default chalk; diff --git a/tests/corpus/8214-unshimmed-package-version/node_modules/chalk/source/styles.js b/tests/corpus/8214-unshimmed-package-version/node_modules/chalk/source/styles.js new file mode 100644 index 000000000..03e60b5e0 --- /dev/null +++ b/tests/corpus/8214-unshimmed-package-version/node_modules/chalk/source/styles.js @@ -0,0 +1 @@ +export const styles = { red: [31, 39], bold: [1, 22] }; diff --git a/tests/corpus/8215-js-any-logical-module-bindings/main.ts b/tests/corpus/8215-js-any-logical-module-bindings/main.ts new file mode 100644 index 000000000..3a1d5eb17 --- /dev/null +++ b/tests/corpus/8215-js-any-logical-module-bindings/main.ts @@ -0,0 +1,8 @@ +// Module bindings of a JavaScript package whose initializers combine an +// untyped value with a string, number or boolean through `||`, `&&`, `??` +// or `?:` (pi-coding-agent's config.js): other modules and functions read +// the values as in Node. +import { banner, usage } from "app-config"; + +console.log(banner); +console.log(usage("check")); diff --git a/tests/corpus/8215-js-any-logical-module-bindings/node_modules/app-config/dist/config.js b/tests/corpus/8215-js-any-logical-module-bindings/node_modules/app-config/dist/config.js new file mode 100644 index 000000000..faecb8b2b --- /dev/null +++ b/tests/corpus/8215-js-any-logical-module-bindings/node_modules/app-config/dist/config.js @@ -0,0 +1,18 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; + +let pkg = {}; +try { + pkg = JSON.parse(readFileSync(join(import.meta.dirname, "..", "package.json"), "utf-8")); +} +catch (e) { + if (e.code !== "ENOENT") + throw e; +} +const configName = pkg.appConfig?.name; +export const APP_NAME = configName || "app"; +export const APP_TITLE = configName ? APP_NAME : "App"; +export const CONFIG_DIR = pkg.appConfig?.configDir || ".config"; +export const VERSION = pkg.version ?? "0.0.0"; +export const RETRIES = pkg.appConfig?.retries ?? 3; +export const isBundled = typeof APP_BUNDLED !== "undefined" && APP_BUNDLED; diff --git a/tests/corpus/8215-js-any-logical-module-bindings/node_modules/app-config/dist/index.d.ts b/tests/corpus/8215-js-any-logical-module-bindings/node_modules/app-config/dist/index.d.ts new file mode 100644 index 000000000..4c4706926 --- /dev/null +++ b/tests/corpus/8215-js-any-logical-module-bindings/node_modules/app-config/dist/index.d.ts @@ -0,0 +1,2 @@ +export declare const banner: string; +export declare function usage(kind: "check" | "list"): string; diff --git a/tests/corpus/8215-js-any-logical-module-bindings/node_modules/app-config/dist/index.js b/tests/corpus/8215-js-any-logical-module-bindings/node_modules/app-config/dist/index.js new file mode 100644 index 000000000..251303802 --- /dev/null +++ b/tests/corpus/8215-js-any-logical-module-bindings/node_modules/app-config/dist/index.js @@ -0,0 +1 @@ +export { banner, usage } from "./usage.js"; diff --git a/tests/corpus/8215-js-any-logical-module-bindings/node_modules/app-config/dist/usage.js b/tests/corpus/8215-js-any-logical-module-bindings/node_modules/app-config/dist/usage.js new file mode 100644 index 000000000..736cef3c0 --- /dev/null +++ b/tests/corpus/8215-js-any-logical-module-bindings/node_modules/app-config/dist/usage.js @@ -0,0 +1,12 @@ +import { APP_NAME, APP_TITLE, CONFIG_DIR, RETRIES, VERSION, isBundled } from "./config.js"; + +const USAGE = { + check: `${APP_NAME} check --dir ${CONFIG_DIR}`, + list: `${APP_NAME} list`, +}; + +export const banner = `${APP_TITLE} ${VERSION} (bundled: ${isBundled}, retries: ${RETRIES})`; + +export function usage(kind) { + return `${USAGE[kind]} [${APP_NAME.toUpperCase()}_HOME=${CONFIG_DIR}] ${typeof isBundled} ${RETRIES + 1}`; +} diff --git a/tests/corpus/8215-js-any-logical-module-bindings/node_modules/app-config/package.json b/tests/corpus/8215-js-any-logical-module-bindings/node_modules/app-config/package.json new file mode 100644 index 000000000..918204988 --- /dev/null +++ b/tests/corpus/8215-js-any-logical-module-bindings/node_modules/app-config/package.json @@ -0,0 +1 @@ +{ "name": "app-config", "version": "2.1.0", "type": "module", "main": "dist/index.js", "types": "dist/index.d.ts", "appConfig": { "configDir": ".app" } } diff --git a/tests/corpus/8216-module-namespace-live-values/main.ts b/tests/corpus/8216-module-namespace-live-values/main.ts new file mode 100644 index 000000000..419ce772e --- /dev/null +++ b/tests/corpus/8216-module-namespace-live-values/main.ts @@ -0,0 +1,11 @@ +// A module namespace object stored as an untyped value, here in a registry +// keyed by module name (pi-coding-agent's VIRTUAL_MODULES): reads see the +// exports' current values, both registry entries are the same object, and +// writes throw Node's TypeError. +import { addThrough, bumpThrough, describe, same, writeThrough } from "ns-registry"; + +console.log(describe()); +bumpThrough(); +bumpThrough(); +console.log(describe()); +console.log(same(), writeThrough(), addThrough()); diff --git a/tests/corpus/8216-module-namespace-live-values/node_modules/ns-registry/counter.js b/tests/corpus/8216-module-namespace-live-values/node_modules/ns-registry/counter.js new file mode 100644 index 000000000..b971201d8 --- /dev/null +++ b/tests/corpus/8216-module-namespace-live-values/node_modules/ns-registry/counter.js @@ -0,0 +1,7 @@ +export let count = 0; +export var legacy = "legacy"; +export const label = "counter"; +export function bump() { + count++; + legacy = `legacy ${count}`; +} diff --git a/tests/corpus/8216-module-namespace-live-values/node_modules/ns-registry/index.d.ts b/tests/corpus/8216-module-namespace-live-values/node_modules/ns-registry/index.d.ts new file mode 100644 index 000000000..451d646f7 --- /dev/null +++ b/tests/corpus/8216-module-namespace-live-values/node_modules/ns-registry/index.d.ts @@ -0,0 +1,5 @@ +export declare function describe(): string; +export declare function bumpThrough(): void; +export declare function same(): boolean; +export declare function writeThrough(): string; +export declare function addThrough(): string; diff --git a/tests/corpus/8216-module-namespace-live-values/node_modules/ns-registry/index.js b/tests/corpus/8216-module-namespace-live-values/node_modules/ns-registry/index.js new file mode 100644 index 000000000..51558bcba --- /dev/null +++ b/tests/corpus/8216-module-namespace-live-values/node_modules/ns-registry/index.js @@ -0,0 +1,40 @@ +import * as counter from "./counter.js"; + +export const registry = { + counter: counter, + "alias/counter": counter, +}; + +export function describe() { + const ns = registry["counter"]; + return `${ns.count} ${ns.legacy} ${ns.label} ${typeof ns.bump} ${typeof ns}`; +} + +export function bumpThrough() { + registry.counter.bump(); +} + +export function same() { + return registry.counter === registry["alias/counter"]; +} + +function assign(target, key, value) { + try { + target[key] = value; + } catch (error) { + return `${error.name}: ${error.message}`; + } + return "no error"; +} + +function lookup(name) { + return registry[name]; +} + +export function writeThrough() { + return assign(lookup("counter"), "count", 5); +} + +export function addThrough() { + return assign(lookup("alias/counter"), "extra", 5); +} diff --git a/tests/corpus/8216-module-namespace-live-values/node_modules/ns-registry/package.json b/tests/corpus/8216-module-namespace-live-values/node_modules/ns-registry/package.json new file mode 100644 index 000000000..30fae6921 --- /dev/null +++ b/tests/corpus/8216-module-namespace-live-values/node_modules/ns-registry/package.json @@ -0,0 +1 @@ +{ "name": "ns-registry", "version": "1.0.0", "type": "module", "main": "index.js", "types": "index.d.ts" } diff --git a/tests/corpus/8217-intl-segmenter-word-construct/main.ts b/tests/corpus/8217-intl-segmenter-word-construct/main.ts new file mode 100644 index 000000000..d0c2247e7 --- /dev/null +++ b/tests/corpus/8217-intl-segmenter-word-construct/main.ts @@ -0,0 +1,10 @@ +// A package builds its segmenters at module load (pi-tui, grok-mermaid): +// a grapheme segmenter with a locale tag, one with empty options, and a +// word segmenter that only some paths use. Grapheme segmentation matches +// Node; the word segmenter constructs, and this program never segments +// with it. +import { columns, describe, plainColumns } from "text-width"; + +console.log(describe()); +console.log(columns("éa👍🏽🇫🇷"), columns("abc")); +console.log(plainColumns("a\u0301b\u{1F44D}\u{1F3FD}")); diff --git a/tests/corpus/8217-intl-segmenter-word-construct/node_modules/text-width/index.d.ts b/tests/corpus/8217-intl-segmenter-word-construct/node_modules/text-width/index.d.ts new file mode 100644 index 000000000..89b16ddc0 --- /dev/null +++ b/tests/corpus/8217-intl-segmenter-word-construct/node_modules/text-width/index.d.ts @@ -0,0 +1,3 @@ +export declare function columns(text: string): string; +export declare function describe(): string; +export declare function plainColumns(text: string): number; diff --git a/tests/corpus/8217-intl-segmenter-word-construct/node_modules/text-width/index.js b/tests/corpus/8217-intl-segmenter-word-construct/node_modules/text-width/index.js new file mode 100644 index 000000000..c7619bd81 --- /dev/null +++ b/tests/corpus/8217-intl-segmenter-word-construct/node_modules/text-width/index.js @@ -0,0 +1,16 @@ +const graphemes = new Intl.Segmenter("en", { granularity: "grapheme" }); +const words = new Intl.Segmenter(undefined, { granularity: "word" }); +const sentences = new Intl.Segmenter(undefined, { granularity: "sentence" }); +const plain = new Intl.Segmenter(undefined, {}); + +export function columns(text) { + return [...graphemes.segment(text)].map((part) => part.segment).join("|"); +} + +export function plainColumns(text) { + return [...plain.segment(text)].length; +} + +export function describe() { + return `${typeof words} ${typeof sentences}`; +} diff --git a/tests/corpus/8217-intl-segmenter-word-construct/node_modules/text-width/package.json b/tests/corpus/8217-intl-segmenter-word-construct/node_modules/text-width/package.json new file mode 100644 index 000000000..4f3af53f4 --- /dev/null +++ b/tests/corpus/8217-intl-segmenter-word-construct/node_modules/text-width/package.json @@ -0,0 +1 @@ +{ "name": "text-width", "version": "1.0.0", "type": "module", "main": "index.js", "types": "index.d.ts" } diff --git a/tests/corpus/8218-cjs-namespace-imports/client/index.mjs b/tests/corpus/8218-cjs-namespace-imports/client/index.mjs new file mode 100644 index 000000000..f0d49451f --- /dev/null +++ b/tests/corpus/8218-cjs-namespace-imports/client/index.mjs @@ -0,0 +1,12 @@ +import * as transport from './transport.js' + +export function connect (origin) { + const pool = new transport.Pool(origin, transport.defaults) + console.log(transport.setGlobalDispatcher(pool), pool.connections, transport.defaults.keepAlive) + return pool +} + +export function describe (pool) { + const format = transport.format + return format(pool.request('/status')) +} diff --git a/tests/corpus/8218-cjs-namespace-imports/client/package.json b/tests/corpus/8218-cjs-namespace-imports/client/package.json new file mode 100644 index 000000000..c866a30a1 --- /dev/null +++ b/tests/corpus/8218-cjs-namespace-imports/client/package.json @@ -0,0 +1 @@ +{ "name": "ns-client", "version": "1.0.0", "main": "index.mjs" } diff --git a/tests/corpus/8218-cjs-namespace-imports/client/transport.js b/tests/corpus/8218-cjs-namespace-imports/client/transport.js new file mode 100644 index 000000000..8d9efbe36 --- /dev/null +++ b/tests/corpus/8218-cjs-namespace-imports/client/transport.js @@ -0,0 +1,26 @@ +'use strict' +// A CommonJS module of the package: member exports, a class, and a table +// whose getter stops Node's lexer before the names after it. +class Pool { + constructor (origin, options) { + this.origin = origin + this.connections = options.connections + this.requests = 0 + } + + request (path) { + this.requests += 1 + return `${this.origin}${path} #${this.requests}` + } +} + +function setGlobalDispatcher (dispatcher) { + exports.current = dispatcher + return dispatcher.origin +} + +exports.Pool = Pool +exports.setGlobalDispatcher = setGlobalDispatcher +exports.current = null +exports.defaults = { connections: 4, keepAlive: true } +exports.format = function format (value) { return `[${value}]` } diff --git a/tests/corpus/8218-cjs-namespace-imports/main.mjs b/tests/corpus/8218-cjs-namespace-imports/main.mjs new file mode 100644 index 000000000..7d2d0e862 --- /dev/null +++ b/tests/corpus/8218-cjs-namespace-imports/main.mjs @@ -0,0 +1,13 @@ +// Namespace imports of CommonJS modules in JavaScript: member reads, calls, +// constructions and a method read as a value, through a package's ES +// module and directly. Each name is one Node's lexer detects. +import * as table from './table.cjs' +import { connect, describe } from './client/index.mjs' + +console.log(table.add(20, 22), table.scale) +const add = table.add +console.log(add(1, 2)) + +const pool = connect('http://localhost') +console.log(describe(pool)) +console.log(describe(pool)) diff --git a/tests/corpus/8218-cjs-namespace-imports/table.cjs b/tests/corpus/8218-cjs-namespace-imports/table.cjs new file mode 100644 index 000000000..38696c6f9 --- /dev/null +++ b/tests/corpus/8218-cjs-namespace-imports/table.cjs @@ -0,0 +1,10 @@ +'use strict' +const add = (a, b) => a + b +const scale = 10 +let calls = 0 +module.exports = { + add, + scale, + get calls () { return calls }, + bump () { calls += 1 } +} diff --git a/tests/corpus/8219-cjs-stable-var-exports/lib/core.js b/tests/corpus/8219-cjs-stable-var-exports/lib/core.js new file mode 100644 index 000000000..1210659ac --- /dev/null +++ b/tests/corpus/8219-cjs-stable-var-exports/lib/core.js @@ -0,0 +1,31 @@ +// highlight.js's lib/core.js shape: the module builds one instance and +// exports the var that holds it. +'use strict'; + +const HLJS = function (options) { + const languages = new Map(); + const prefix = options.classPrefix || 'hljs-'; + + function registerLanguage(name, keywords) { + languages.set(name, keywords); + } + + function listLanguages() { + return Array.from(languages.keys()); + } + + function highlight(code, name) { + const keywords = languages.get(name); + if (keywords === undefined) throw new Error(`Unknown language: "${name}"`); + return code + .split(' ') + .map((word) => (keywords.includes(word) ? `${word}` : word)) + .join(' '); + } + + return { registerLanguage, listLanguages, highlight, versionString: '10.7.3' }; +}; + +var highlight = HLJS({}); + +module.exports = highlight; diff --git a/tests/corpus/8219-cjs-stable-var-exports/lib/index.js b/tests/corpus/8219-cjs-stable-var-exports/lib/index.js new file mode 100644 index 000000000..5f7726ea2 --- /dev/null +++ b/tests/corpus/8219-cjs-stable-var-exports/lib/index.js @@ -0,0 +1,8 @@ +// highlight.js's lib/index.js shape: require the core instance, register +// languages on it and export the same var. +var hljs = require('./core'); + +hljs.registerLanguage('javascript', ['const', 'let', 'function', 'return']); +hljs.registerLanguage('python', ['def', 'return', 'lambda']); + +module.exports = hljs; diff --git a/tests/corpus/8219-cjs-stable-var-exports/lib/options.js b/tests/corpus/8219-cjs-stable-var-exports/lib/options.js new file mode 100644 index 000000000..17512396f --- /dev/null +++ b/tests/corpus/8219-cjs-stable-var-exports/lib/options.js @@ -0,0 +1,12 @@ +// A let that is never reassigned, exported in a table. +'use strict'; + +let tabReplace = ' '; +let calls = []; + +function render(text) { + calls.push(text); + return text.split('\t').join(tabReplace); +} + +module.exports = { tabReplace, calls, render }; diff --git a/tests/corpus/8219-cjs-stable-var-exports/main.js b/tests/corpus/8219-cjs-stable-var-exports/main.js new file mode 100644 index 000000000..8607165a6 --- /dev/null +++ b/tests/corpus/8219-cjs-stable-var-exports/main.js @@ -0,0 +1,21 @@ +// A var and a let exported by reference, where neither changes after the +// export: importers see the same instance and value as Node. +'use strict'; + +const hljs = require('./lib'); +const core = require('./lib/core'); +const options = require('./lib/options'); + +console.log(hljs === core, hljs.versionString); +console.log(hljs.listLanguages()); +console.log(hljs.highlight('const x = function', 'javascript')); +hljs.registerLanguage('ruby', ['def', 'end']); +console.log(core.listLanguages().length, core.highlight('def run end', 'ruby')); +try { + hljs.highlight('x', 'cobol'); +} catch (error) { + console.log(error.message); +} + +console.log(JSON.stringify(options.tabReplace), options.render('a\tb'), options.render('\tc')); +console.log(options.calls); diff --git a/tests/corpus/8219-cjs-stable-var-exports/package.json b/tests/corpus/8219-cjs-stable-var-exports/package.json new file mode 100644 index 000000000..5bbefffba --- /dev/null +++ b/tests/corpus/8219-cjs-stable-var-exports/package.json @@ -0,0 +1,3 @@ +{ + "type": "commonjs" +} diff --git a/tests/corpus/8220-js-class-static-destructuring/classes/comparator.js b/tests/corpus/8220-js-class-static-destructuring/classes/comparator.js new file mode 100644 index 000000000..3b128847c --- /dev/null +++ b/tests/corpus/8220-js-class-static-destructuring/classes/comparator.js @@ -0,0 +1,22 @@ +'use strict' +// semver's Comparator: a static getter returning a module symbol. +const ANY = Symbol('SemVer ANY') + +class Comparator { + static get ANY () { + return ANY + } + + static loose = false + + static parse (text) { + return new Comparator(text) + } + + constructor (text) { + this.semver = text === '' ? ANY : text + this.value = text === '' ? '' : `>=${text}` + } +} + +module.exports = Comparator diff --git a/tests/corpus/8220-js-class-static-destructuring/formdata.js b/tests/corpus/8220-js-class-static-destructuring/formdata.js new file mode 100644 index 000000000..851d3ba28 --- /dev/null +++ b/tests/corpus/8220-js-class-static-destructuring/formdata.js @@ -0,0 +1,26 @@ +'use strict' +// undici's FormData: statics that read private instance state, taken off +// the class before the class is exposed. +class FormData { + #state = [] + + append (name, value) { + this.#state.push({ name, value }) + } + + static getFormDataState (formData) { + return formData.#state + } + + static setFormDataState (formData, state) { + formData.#state = state + } + + static get boundary () { + return undefined + } +} + +const { getFormDataState, setFormDataState, boundary = '----formdata-default' } = FormData + +module.exports = { FormData, getFormDataState, setFormDataState, boundary } diff --git a/tests/corpus/8220-js-class-static-destructuring/main.js b/tests/corpus/8220-js-class-static-destructuring/main.js new file mode 100644 index 000000000..6cc17b94e --- /dev/null +++ b/tests/corpus/8220-js-class-static-destructuring/main.js @@ -0,0 +1,39 @@ +'use strict' +// Object patterns over a class value read its static getters, fields and +// methods as Node does, defaults included. +const Comparator = require('./classes/comparator') +const { outside, ANY, any, isLoose, name } = require('./ranges/outside') +const { FormData, getFormDataState, setFormDataState, boundary } = require('./formdata') + +console.log(typeof ANY, ANY === Comparator.ANY, any === ANY, isLoose, name) +console.log(outside(''), outside('1.2.3')) +console.log(Comparator.parse('').semver === ANY, Comparator.parse('2.0.0').semver === ANY) + +const form = new FormData() +form.append('a', 1) +form.append('b', 'two') +console.log(getFormDataState(form)) +setFormDataState(form, [{ name: 'c', value: true }]) +console.log(getFormDataState(form), boundary) + +class Defaults { + static count = 0 + static missing = undefined + static nested = { inner: 'value' } +} +const { count = 5, missing = 'fallback', nested: { inner, other = 'other' } } = Defaults +console.log(count, missing, inner, other) + +// Inherited statics, and a static getter reading the class it is read +// through. +class Base { + static tag = 'base' + static get label () { return `label:${this.tag}` } + static make () { return this.tag } +} +class Sub extends Base { + static tag = 'sub' +} +const { tag, label, make, name: subName } = Sub +const { label: baseLabel } = Base +console.log(tag, label, typeof make, subName, baseLabel) diff --git a/tests/corpus/8220-js-class-static-destructuring/package.json b/tests/corpus/8220-js-class-static-destructuring/package.json new file mode 100644 index 000000000..5bbefffba --- /dev/null +++ b/tests/corpus/8220-js-class-static-destructuring/package.json @@ -0,0 +1,3 @@ +{ + "type": "commonjs" +} diff --git a/tests/corpus/8220-js-class-static-destructuring/ranges/outside.js b/tests/corpus/8220-js-class-static-destructuring/ranges/outside.js new file mode 100644 index 000000000..a8ffe06f2 --- /dev/null +++ b/tests/corpus/8220-js-class-static-destructuring/ranges/outside.js @@ -0,0 +1,13 @@ +'use strict' +// semver's ranges/outside.js and ranges/subset.js destructure the class's +// statics at module load. +const Comparator = require('../classes/comparator') +const { ANY } = Comparator +const { ANY: any, loose: isLoose = true, parse, name } = Comparator + +const outside = (text) => { + const comparator = parse(text) + return comparator.semver === ANY ? 'any' : comparator.value +} + +module.exports = { outside, ANY, any, isLoose, name } diff --git a/tests/corpus/8221-js-recursive-generator-returns/main.ts b/tests/corpus/8221-js-recursive-generator-returns/main.ts new file mode 100644 index 000000000..72b9a7b10 --- /dev/null +++ b/tests/corpus/8221-js-recursive-generator-returns/main.ts @@ -0,0 +1,10 @@ +// A JavaScript package's parser of mutually recursive generator methods that +// return `yield*` of each other (yaml 2.9.0's parse/parser.js): the checker +// cannot infer their results. Tokens, values passed to next() and return +// values reach each generator in the same order as in Node. +import { parseTokens, drive, spreadTypes, walkAll } from "token-parser"; + +for (const token of parseTokens("a { b [ c d ] e } f")) console.log(token); +console.log(drive("{ x } y ! z")); +console.log(spreadTypes("[ 1 2 3 ]")); +console.log(await walkAll("a ( b c ) d ( e )")); diff --git a/tests/corpus/8221-js-recursive-generator-returns/node_modules/token-parser/dist/index.d.ts b/tests/corpus/8221-js-recursive-generator-returns/node_modules/token-parser/dist/index.d.ts new file mode 100644 index 000000000..763a34ae4 --- /dev/null +++ b/tests/corpus/8221-js-recursive-generator-returns/node_modules/token-parser/dist/index.d.ts @@ -0,0 +1,4 @@ +export declare function parseTokens(source: string): string[]; +export declare function drive(source: string): string; +export declare function spreadTypes(source: string): string; +export declare function walkAll(source: string): Promise; diff --git a/tests/corpus/8221-js-recursive-generator-returns/node_modules/token-parser/dist/index.js b/tests/corpus/8221-js-recursive-generator-returns/node_modules/token-parser/dist/index.js new file mode 100644 index 000000000..aab64a1ac --- /dev/null +++ b/tests/corpus/8221-js-recursive-generator-returns/node_modules/token-parser/dist/index.js @@ -0,0 +1,131 @@ +'use strict' + +class Parser { + constructor (source) { + this.tokens = source.split(' ') + this.index = 0 + this.stack = [] + } + + * parse () { + while (this.index < this.tokens.length) { + const done = yield * this.step() + if (done) return 'stopped' + } + return yield * this.end() + } + + * step () { + const token = this.tokens[this.index++] + if (token === '{') return yield * this.blockMap(token) + if (token === '[') return yield * this.flowSeq(token) + if (token === '!') return true + return yield * this.scalar(token) + } + + * blockMap (open) { + this.stack.push(open) + yield { type: 'map-start', depth: this.stack.length } + while (this.index < this.tokens.length && this.tokens[this.index] !== '}') { + const stop = yield * this.step() + if (stop) return stop + } + this.index++ + this.stack.pop() + const reply = yield { type: 'map-end', depth: this.stack.length + 1 } + if (reply !== undefined) yield { type: 'reply', value: reply } + return false + } + + * flowSeq (open) { + this.stack.push(open) + let count = 0 + while (this.index < this.tokens.length && this.tokens[this.index] !== ']') { + yield * this.step() + count++ + } + this.index++ + this.stack.pop() + yield { type: 'seq', count } + return false + } + + * scalar (token) { + const value = yield { type: 'scalar', source: token } + if (value !== undefined) return yield * this.scalar(String(value)) + return false + } + + * end () { + yield { type: 'end', depth: this.stack.length } + return this.index + } +} + +function parseTokens (source) { + const tokens = [] + for (const token of new Parser(source).parse()) tokens.push(JSON.stringify(token)) + return tokens +} + +// next(value) reaches the innermost delegated generator, and return values +// pass back through each yield*. +function drive (source) { + const parser = new Parser(source).parse() + const log = [] + let result = parser.next() + while (!result.done) { + const value = result.value + log.push(JSON.stringify(value)) + result = parser.next(value.type === 'map-end' ? 'ack' : value.source === 'x' ? 'x2' : undefined) + } + log.push('return ' + result.value) + return log.join('\n') +} + +function spreadTypes (source) { + const tokens = [...new Parser(source).parse()] + return tokens.length + ' ' + tokens.map((token) => token.type).join(',') +} + +// Async generator methods with the same mutual recursion. +class AsyncWalker { + constructor (source) { + this.tokens = source.split(' ') + this.index = 0 + } + + async * walk () { + while (this.index < this.tokens.length) yield * this.next() + return this.index + } + + async * next () { + const token = this.tokens[this.index++] + await null + if (token === '(') return yield * this.group() + yield token + return token + } + + async * group () { + const inner = [] + while (this.tokens[this.index] !== ')') inner.push(yield * this.next()) + this.index++ + yield 'group:' + inner.join('+') + return inner.length + } +} + +async function walkAll (source) { + const seen = [] + const walker = new AsyncWalker(source).walk() + let result = await walker.next() + while (!result.done) { + seen.push(result.value) + result = await walker.next() + } + return seen.join(' ') + ' -> ' + result.value +} + +module.exports = { parseTokens, drive, spreadTypes, walkAll } diff --git a/tests/corpus/8221-js-recursive-generator-returns/node_modules/token-parser/package.json b/tests/corpus/8221-js-recursive-generator-returns/node_modules/token-parser/package.json new file mode 100644 index 000000000..1fa77c2eb --- /dev/null +++ b/tests/corpus/8221-js-recursive-generator-returns/node_modules/token-parser/package.json @@ -0,0 +1 @@ +{ "name": "token-parser", "version": "1.0.0", "main": "dist/index.js", "types": "dist/index.d.ts" } diff --git a/tests/corpus/8222-js-class-static-reflection.js b/tests/corpus/8222-js-class-static-reflection.js new file mode 100644 index 000000000..74027cc64 --- /dev/null +++ b/tests/corpus/8222-js-class-static-reflection.js @@ -0,0 +1,94 @@ +'use strict' +// A class's statics extracted, then removed with Reflect.deleteProperty, and +// redefined with Object.defineProperties (undici's FormData and Response). + +const kEnumerableProperty = { __proto__: null, enumerable: true } + +class FormData { + #state = [] + append (name, value) { this.#state.push({ name, value }) } + static getFormDataState (formData) { return formData.#state } + static setFormDataState (formData, state) { formData.#state = state } +} + +const getFormDataState = FormData.getFormDataState +const setFormDataState = FormData.setFormDataState +console.log(Reflect.deleteProperty(FormData, 'getFormDataState')) +console.log(Reflect.deleteProperty(FormData, 'setFormDataState')) +console.log(Reflect.deleteProperty(FormData, 'missing')) +console.log(typeof FormData.getFormDataState, typeof FormData.setFormDataState) + +const form = new FormData() +form.append('a', 1) +form.append('b', 'two') +console.log(getFormDataState(form)) +setFormDataState(form, [{ name: 'c', value: true }]) +console.log(getFormDataState(form).length, getFormDataState(form)[0].name) +try { + FormData.getFormDataState(form) +} catch (error) { + console.log(error.constructor.name, error.message) +} + +class Response { + #status + constructor (status) { this.#status = status } + get status () { return this.#status } + static json () { return new Response(200) } + static error () { return new Response(0) } + static getResponseState (response) { return response.#status } +} + +const getResponseState = Response.getResponseState +Reflect.deleteProperty(Response, 'getResponseState') +Object.defineProperties(Response, { + json: kEnumerableProperty, + error: kEnumerableProperty +}) +const response = Response.json() +console.log(response.status, Response.error().status, getResponseState(response)) +for (const key of ['json', 'error', 'getResponseState']) { + const descriptor = Object.getOwnPropertyDescriptor(Response, key) + console.log(key, descriptor === undefined ? 'absent' : [descriptor.enumerable, descriptor.writable, descriptor.configurable, typeof descriptor.value]) +} + +// Statics a subclass inherits, a static field and a static accessor. +class Base { + static helper () { return 'helper' } + static count = 3 + static get label () { return 'label' } +} +class Child extends Base {} +console.log(Child.helper(), Child.count, Child.label) +console.log(Reflect.deleteProperty(Base, 'helper'), Reflect.deleteProperty(Base, 'count')) +console.log(typeof Child.helper, Child.count, typeof Base.helper, Base.count) +console.log(Reflect.deleteProperty(Base, 'label'), Base.label, Child.label) +Base.helper = () => 'replaced' +console.log(Base.helper(), Child.helper()) +Base.count = 4 +const countKey = { toString () { return 'count' } } +console.log(Reflect.deleteProperty(Base, countKey), Base.count) +class Key { + constructor (name) { this.name = name } + toString () { return this.name } +} +Base.count = 5 +console.log(Reflect.deleteProperty(Base, new Key('count')), Base.count) + +// Checked-dynamic objects and primitive targets. +const parsed = JSON.parse('{"a":1,"b":2}') +console.log(Reflect.deleteProperty(parsed, 'a'), parsed) +const frozen = Object.freeze(JSON.parse('{"a":1}')) +console.log(Reflect.deleteProperty(frozen, 'a'), frozen) +const attempt = (run) => { + try { + run() + } catch (error) { + console.log(error.constructor.name, error.message) + } +} +attempt(() => Reflect.deleteProperty(1, 'a')) +attempt(() => Reflect.deleteProperty('text', 'a')) +attempt(() => Reflect.deleteProperty(null, 'a')) +attempt(() => Reflect.deleteProperty(undefined, 'a')) +attempt(() => Reflect.deleteProperty(true, 'a')) diff --git a/tests/corpus/8223-argument-count-destructured-parameter.ts b/tests/corpus/8223-argument-count-destructured-parameter.ts new file mode 100644 index 000000000..def8c5ba8 --- /dev/null +++ b/tests/corpus/8223-argument-count-destructured-parameter.ts @@ -0,0 +1,14 @@ +// A function with a destructured parameter that reads arguments.length, +// called through a value (map passes three arguments) and directly. The +// count is taken before the parameters bind, so each call sees its own. + +function probe({ v, w }: { v: number; w: string }): string { + return `${arguments.length}:${v}${w}`; +} + +console.log([{ v: 1, w: "a" }, { v: 2, w: "b" }].map(probe)); +console.log(probe({ v: 3, w: "c" })); +const call = (f: (item: { v: number; w: string }, index: number) => string): string => + f({ v: 4, w: "d" }, 0); +console.log(call(probe)); +console.log(probe({ v: 5, w: "e" })); diff --git a/tests/corpus/8230-function-record-dispatch.ts b/tests/corpus/8230-function-record-dispatch.ts new file mode 100644 index 000000000..3acde5bd7 --- /dev/null +++ b/tests/corpus/8230-function-record-dispatch.ts @@ -0,0 +1,107 @@ +// Records of functions used as dispatch tables: keyed reads consumed at once +// (`t[k]!(…)`, `t[k]?.(…)`, a checked local), missing keys, handlers that +// declare fewer parameters than the table's type, handlers that replace, +// delete or re-add table entries (their own included) while they run, and +// key order after such mutation. +type Handler = (x: number, y: number) => number; + +let log: string[] = []; + +function make(tag: string): Handler { + let calls = 0; + return (x, y) => { + calls++; + log.push(tag + ":" + calls); + return x * 10 + y; + }; +} + +const table: Record = { + add: (x, y) => x + y, + neg: (x) => -x, + pow: (x, y) => x ** y, + made: make("made"), +}; + +function keys(): string[] { + return process.argv.length > 99 ? [] : ["add", "neg", "pow", "made", "missing", ""]; +} + +for (const k of keys()) { + const h = table[k] as Handler | undefined; + console.log(JSON.stringify(k), typeof h, h === undefined, h !== undefined && h === table[k]); + console.log(" optional", table[k]?.(3, 4) ?? "none"); + if (h) console.log(" checked", h(2, 5)); + try { + console.log(" asserted", table[k]!(5, 2)); + } catch (e) { + console.log(" asserted threw", (e as Error).name); + } +} + +// A handler that replaces its own entry while it runs: the replaced closure +// keeps running with its captured state. +function selfReplacing(): Handler { + const state = { hits: 0, label: "first" }; + return (x, y) => { + table["swap"] = make("second"); + state.hits++; + log.push(state.label + " still running after replace, hits " + state.hits); + return x + y + state.hits; + }; +} +table["swap"] = selfReplacing(); +console.log("swap 1", table["swap"]!(1, 2)); +console.log("swap 2", table["swap"]?.(1, 2)); +console.log("swap 3", table["swap"]!(1, 2)); + +// A handler that deletes its own entry. +function selfDeleting(name: string): Handler { + const captured = [name, "captured"]; + return (x) => { + delete table[name]; + log.push(captured.join(" ") + " after delete"); + return x; + }; +} +table["gone"] = selfDeleting("gone"); +const goneKey = keys()[0] === "add" ? "gone" : "add"; +console.log("gone", table[goneKey]?.(7, 0) ?? "none", table[goneKey]?.(7, 0) ?? "none"); + +// A checked local keeps its closure after the entry changes. +table["kept"] = make("kept-old"); +const kept = table["kept"] as Handler | undefined; +table["kept"] = make("kept-new"); +delete table["kept"]; +if (kept) console.log("kept", kept(4, 4)); +console.log("kept entry", table["kept"] === undefined); + +// Iteration while handlers add and delete keys: key order follows inserts. +const order: Record = { + b: (x) => x + 1, + a: (x) => { + delete order["c"]; + order["z"] = (v) => v * 2; + return x + 2; + }, + c: (x) => x + 3, + "1": (x) => x + 4, +}; +for (const k in order) console.log("in", k, order[k]?.(1, 0)); +console.log("keys", Object.keys(order).join(",")); +for (const k of Object.keys(order)) { + order[k + k] = (x) => x; + console.log("of", k, order[k]!(10, 0)); +} +console.log("keys", Object.keys(order).join(",")); + +// Reassigning the table binding itself from inside a handler. +let current: Record = { + go: (x) => { + current = { go: (y) => y * 100 }; + const local = [x, x]; + return local[0]! + local[1]!; + }, +}; +console.log("rebind", current["go"]!(3, 0), current["go"]!(3, 0)); +console.log(log.join("\n")); diff --git a/tests/corpus/8230-readline-interface-constructor.cjs b/tests/corpus/8230-readline-interface-constructor.cjs new file mode 100644 index 000000000..6d7eea0e4 --- /dev/null +++ b/tests/corpus/8230-readline-interface-constructor.cjs @@ -0,0 +1,51 @@ +// `new readline.Interface(...)`: Node's createInterface calls this +// constructor with the same arguments, and a JavaScript file may call it +// directly. Its declaration is protected, which JavaScript does not +// enforce. +"use strict"; +const readline = require("readline"); +const readlinePromises = require("readline/promises"); +const { Interface } = require("readline"); +const { PassThrough } = require("stream"); + +// The options form. +const a = new PassThrough(); +const rlA = new readline.Interface({ input: a, terminal: false }); +console.log("instanceof", rlA instanceof readline.Interface); +const linesA = []; +rlA.on("line", (line) => { + linesA.push(line); +}); +rlA.on("close", () => { + console.log("options", JSON.stringify(linesA)); +}); +a.write("one\ntwo\n"); +a.end("three"); + +// The positional form, through a destructured binding. +const b = new PassThrough(); +const outB = new PassThrough(); +const rlB = new Interface(b, outB); +const linesB = []; +rlB.on("line", (line) => { + linesB.push(line); +}); +rlB.on("close", () => { + console.log("positional", JSON.stringify(linesB)); +}); +b.write("four\r\nfive\n"); +b.end(); + +// readline/promises' Interface. +const c = new PassThrough(); +const rlC = new readlinePromises.Interface({ input: c, crlfDelay: Infinity }); +console.log("promises instanceof", rlC instanceof readlinePromises.Interface); +const linesC = []; +rlC.on("line", (line) => { + linesC.push(line); +}); +rlC.on("close", () => { + console.log("promises", JSON.stringify(linesC)); +}); +c.end("six\nseven"); + diff --git a/tests/corpus/8231-builtin-member-binding-new.cjs b/tests/corpus/8231-builtin-member-binding-new.cjs new file mode 100644 index 000000000..df1b2ebb3 --- /dev/null +++ b/tests/corpus/8231-builtin-member-binding-new.cjs @@ -0,0 +1,60 @@ +// `new` of a builtin class bound through a member of a require() result +// (`const SD = require("string_decoder").StringDecoder`, the spelling of +// Node's own tests): the binding resolves to the class, as the +// destructured `const { StringDecoder } = require(...)` does, also for the +// X509Certificate record. +"use strict"; +const SD = require("string_decoder").StringDecoder; +const Readable = require("stream").Readable; +const PassThrough = require("stream").PassThrough; +const Transform = require("stream").Transform; +const X509 = require("crypto").X509Certificate; + +const decoder = new SD("utf8"); +console.log(decoder.encoding); +console.log(JSON.stringify(decoder.write(Buffer.from([0xe2, 0x82])))); +console.log(decoder.end(Buffer.from([0xac]))); +const hex = new SD("hex"); +console.log(hex.encoding, hex.write(Buffer.from("hi"))); +console.log(new SD().encoding); + +const r = new Readable({ read() {} }); +r.on("data", (chunk) => console.log("readable", String(chunk))); +r.on("end", () => console.log("readable end")); +r.push("abc"); +r.push(null); + +const p = new PassThrough(); +p.on("data", (chunk) => console.log("passthrough", String(chunk))); +p.end("xyz"); + +const upper = new Transform({ + transform(chunk, encoding, callback) { + callback(null, String(chunk).toUpperCase()); + }, +}); +upper.on("data", (chunk) => console.log("transform", String(chunk))); +upper.end("shout"); + +// The X509Certificate record constructs through the same binding. +const PEM = `-----BEGIN CERTIFICATE----- +MIIDEzCCAfugAwIBAgIUMsBCFtPck8TATMJzfikgW963q3AwDQYJKoZIhvcNAQEL +BQAwGDEWMBQGA1UEAwwNdHNuYXRpdmUtdGVzdDAgFw0yNjA3MTcxNzUyMTFaGA8y +MTI2MDYyMzE3NTIxMVowGDEWMBQGA1UEAwwNdHNuYXRpdmUtdGVzdDCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBALRspe2XjtKvNU7Ws5JfqzmTCPmWtfw0 +1Uha271hNp/30VstOxWLbpqPrXDvIwJMN4o8CRp92Y1hcmS36m9QS5qfXJFw+n+S +SWGpFdMNvp6hro0OoNBA+anuKWoTsMyHSwe8ND0fyobQbgAXJ0+snmuK0E2ii+CM +IoUqYbK57nHeyHfISLupgrRoNGXjDHv4v+RSr3MJDag6BLA+/yVjkBOBOT6kaE4K +oyNXYYsfFxGnB/4pDSanV/UsfJ4wHXkVn18ndZHIzTsZMUkV4BecF8UaMrP+u8NI +c5wce0uWLFFEneK4q15iVvk9TaY0g1Uj69ryRT3L58W7RYYuHu7cVB0CAwEAAaNT +MFEwHQYDVR0OBBYEFLT2GqaTmxDhntPxMWaUZvA3s9V5MB8GA1UdIwQYMBaAFLT2 +GqaTmxDhntPxMWaUZvA3s9V5MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL +BQADggEBAFcTLVJIyhbIKHfOnGM5T+Kzu5IoDlqzr+Rdx/0IKFLVDqEA9ihEme07 +K/d246pOW5LBceVDY2eMikDpIOjYPEoe9C+Ftv5zrCaDw1wtXrq8a7/eS5B+3TzL +k8c1SVVDPIbmViAZNRmnkwCPBsIF9xIfMFhxz4BTJrm/Lt2Hv1GcRBQx/yRK89sj +JiGnsTna//4mPGRGYw+dRCq4tbyr8HbBKdJmbzOCb4CPKfwAMKUpd4e0Ao7jh/BU +EML7B3BEzv5Jw/BT7B39wAfDyhq32PnRnZefF+7pAmpkklHbckBtZtWn02zwQazv +5z62tszZee2i4BouJiyFnb30F6Pebs4= +-----END CERTIFICATE-----`; +const cert = new X509(PEM); +console.log(cert.fingerprint, cert.validFrom, cert.validTo); diff --git a/tests/corpus/8231-function-table-shapes.ts b/tests/corpus/8231-function-table-shapes.ts new file mode 100644 index 000000000..108c4cee0 --- /dev/null +++ b/tests/corpus/8231-function-table-shapes.ts @@ -0,0 +1,108 @@ +// Function dispatch through other table shapes: Map, arrays of +// handlers, object literals of methods (with and without `this`), handler +// mutation of the Map during calls, and adapters for handlers that declare +// fewer parameters than the table type. +type Op = (acc: number, arg: number) => number; + +const trace: string[] = []; +const byName = new Map(); +byName.set("add", (acc, arg) => acc + arg); +byName.set("half", (acc) => acc / 2); +byName.set("reset", () => { + byName.delete("reset"); + byName.set("reset", (acc) => acc - 1); + trace.push("reset replaced itself"); + return 0; +}); +byName.set("clear", (acc) => { + const keep = [acc, "before clear"]; + byName.clear(); + trace.push(keep.join(" ") + ", size " + byName.size); + return acc; +}); + +function program(): string[] { + return process.argv.length > 99 ? [] : ["add", "half", "nop", "reset", "reset", "add", "clear", "add"]; +} + +let acc = 8; +for (const name of program()) { + const handler = byName.get(name); + if (handler !== undefined) acc = handler(acc, 3); + else trace.push("no " + name); + console.log(name, acc, byName.get(name)?.(1, 1) ?? "-"); +} + +const byCode: Op[] = [(a, b) => a - b, (a) => a * 3, (a, b) => Math.max(a, b)]; +let total = 1; +for (let i = 0; i < 9; i++) { + total = byCode[i % 4]?.(total, i) ?? total + 100; + if (i % 4 < byCode.length) total = byCode[i % 4]!(total, 1); +} +console.log("array", total); + +const counter = { + base: 5, + bump(by: number): number { + this.base += by; + return this.base; + }, + twice(by: number): number { + return this.bump(by) + this.bump(by); + }, + plain(by: number): number { + return by * 2; + }, +}; +console.log("methods", counter.bump(1), counter.twice(2), counter.plain(3), counter["plain"](4)); + +// Methods that read `this`, called through computed keys and through a +// signature adapter (a method declaring fewer parameters than its type). +interface Account { + balance: number; + deposit: (amount: number, note: string) => number; + withdraw: (amount: number, note: string) => number; + peek: (amount: number, note: string) => number; +} +const account: Account = { + balance: 10, + deposit(amount) { + this.balance += amount; + return this.balance; + }, + withdraw(amount, note) { + this.balance -= amount; + trace.push("withdraw " + note); + return this.balance; + }, + peek: (amount) => amount, +}; +for (const op of ["deposit", "withdraw", "peek", "deposit"] as const) { + console.log(op, account[op](5, "n-" + op), account.balance); +} +const detached = account.peek; +console.log("detached", detached(3, "x")); + +interface Machine { + state: string; + on: Record string>; +} +const machine: Machine = { + state: "idle", + on: { + idle: (m, input) => (input > 0 ? "running" : m.state), + running: (m, input) => { + if (input < 0) { + m.on["stopped"] = () => "idle"; + return "stopped"; + } + return m.state; + }, + }, +}; +for (const input of [0, 1, 2, -1, 5, 3]) { + const next = machine.on[machine.state]?.(machine, input) ?? machine.state; + console.log("machine", machine.state, input, "->", next); + machine.state = next; +} +console.log(trace.join("\n")); diff --git a/tests/corpus/8232-native-streams-untyped.cjs b/tests/corpus/8232-native-streams-untyped.cjs new file mode 100644 index 000000000..fb1c1f0c1 --- /dev/null +++ b/tests/corpus/8232-native-streams-untyped.cjs @@ -0,0 +1,80 @@ +// Native streams passed to untyped helpers: push, unshift, the listener +// members, setDefaultEncoding and readableFlowing answer through the +// untyped value as on the typed one, also for a program subclass and in +// object mode. +const { Readable, Writable } = require("stream"); + +function feed(s) { + console.log("push", s.push("a"), s.push("b", "utf8"), s.unshift("z")); + console.log("counts", s.listenerCount("data"), s.eventNames().join(",")); + s.on("custom", (x, y) => { + console.log("custom", x, y); + }); + s.emit("custom", 1, 2); + s.removeAllListeners("custom"); + console.log("after removeAll", s.listenerCount("custom"), s.readableFlowing); + s.push(null); +} + +const r = new Readable({ read() {} }); +const chunks = []; +r.on("data", (c) => chunks.push(String(c))); +r.on("end", () => console.log("end", chunks.join("|"))); +feed(r); + +class Source extends Readable { + _read() {} +} +const sub = new Source(); +sub.on("data", (c) => console.log("source data", String(c))); +console.log("source push", sub instanceof Readable); +(function (s) { + console.log(s.push("from untyped"), s.readableLength); + s.push(null); +})(sub); + +const objects = new Readable({ objectMode: true, read() {} }); +console.log("flowing before", objects.readableFlowing); +objects.on("data", (c) => console.log("object data", c)); +(function (s) { + console.log(s.push({ n: 1 }), s.push(undefined), s.push(2), s.readableFlowing); + s.push(null); +})(objects); + +function drain(w) { + w.setDefaultEncoding("latin1"); + w.write("é"); + w.end(); +} +const w = new Writable({ + write(c, e, cb) { + console.log("wrote", c.length, e); + cb(); + }, +}); +w.on("finish", () => console.log("finish")); +drain(w); + +// unshift(string, encoding) puts the decoded bytes at the front; an +// unknown encoding throws. +const bytes = new Readable({ read() {} }); +const parts = []; +bytes.on("data", (c) => parts.push(c.toString("hex"))); +bytes.on("end", () => console.log("unshift bytes", parts.join("|"))); +(function (s) { + console.log(s.unshift("e9", "hex"), s.unshift("aGk=", "base64"), s.unshift("é", "latin1"), s.unshift("x", "utf8")); + try { + s.unshift("x", "bogus"); + } catch (e) { + console.log(e.code, e.message); + } + s.push(null); +})(bytes); +const text = new Readable({ read() {} }); +text.setEncoding("utf8"); +text.on("data", (c) => console.log("unshift text", JSON.stringify(c))); +(function (s) { + s.unshift("c3a9", "hex"); + s.unshift("41", "hex"); + s.push(null); +})(text); diff --git a/tests/corpus/8232-optional-function-values.ts b/tests/corpus/8232-optional-function-values.ts new file mode 100644 index 000000000..5035c5f51 --- /dev/null +++ b/tests/corpus/8232-optional-function-values.ts @@ -0,0 +1,75 @@ +// Values typed `F | undefined` for a function type F: identity and +// equality (signature adapters compare as the function they adapt), typeof, +// truthiness, storage in arrays, records, class fields and Maps, optional +// parameters and returns. +type Unary = (x: number) => number; +type Binary = (x: number, y: number) => number; + +const double: Unary = (x) => x * 2; +const asBinary: Binary = double; +const maybe = (flag: boolean): Unary | undefined => (flag ? double : undefined); + +const a = maybe(true); +const b = maybe(true); +const none = maybe(false); +console.log(a === b, a === none, none === undefined, typeof a, typeof none, !!a, !!none); +console.log(a === double, (asBinary as unknown) === (double as unknown)); + +const slots: (Unary | undefined)[] = [double, undefined, (x) => x + 1]; +console.log(slots.map((f) => (f ? f(5) : "empty")).join(" "), slots.length); +console.log(slots.indexOf(double), slots.includes(undefined), slots.filter((f) => f !== undefined).length); + +interface Hooks { + before?: Unary; + after?: Unary; +} +const hooks: Hooks = { after: (x) => x - 1 }; +console.log(hooks.before?.(1) ?? "no before", hooks.after?.(1) ?? "no after", "before" in hooks); + +class Button { + onClick: Unary | undefined; + clicks = 0; + click(): number { + this.clicks++; + return this.onClick?.(this.clicks) ?? -1; + } +} +const button = new Button(); +console.log(button.click()); +button.onClick = (n) => n * 100; +console.log(button.click(), button.click()); +button.onClick = undefined; +console.log(button.click(), button.onClick === undefined); + +const registry = new Map([ + ["set", double], + ["unset", undefined], +]); +console.log(registry.get("set")?.(4), registry.get("unset")?.(4), registry.has("unset"), registry.get("other") === undefined); + +function run(x: number, transform?: Unary, combine?: Binary): number { + const t = transform ? transform(x) : x; + return combine === undefined ? t : combine(t, x); +} +console.log(run(3), run(3, double), run(3, undefined, (p, q) => p * q), run(3, (x) => -x, asBinary)); + +function pick(name: string): Binary | undefined { + switch (name) { + case "mul": + return (x, y) => x * y; + case "first": + return (x) => x; + default: + return undefined; + } +} +for (const name of ["mul", "first", "none"]) { + const f = pick(name); + console.log(name, f?.(6, 7), f === undefined ? "undefined" : typeof f); +} + +let late: Unary | undefined; +const callLate = (): number => late?.(9) ?? 0; +console.log(callLate()); +late = (x) => x + 0.5; +console.log(callLate()); diff --git a/tests/corpus/8233-string-decoder-values.cjs b/tests/corpus/8233-string-decoder-values.cjs new file mode 100644 index 000000000..475a31d57 --- /dev/null +++ b/tests/corpus/8233-string-decoder-values.cjs @@ -0,0 +1,33 @@ +// A StringDecoder passed to untyped code keeps its identity and its +// pending bytes, and answers write, end and encoding as in Node. +const { StringDecoder } = require("string_decoder"); + +function feed(decoder, chunks) { + let out = ""; + for (const chunk of chunks) out += decoder.write(chunk); + return out + decoder.end(); +} + +const euro = Buffer.from("€ and 😀"); +const utf8 = new StringDecoder("utf-8"); +const parts = []; +for (let i = 0; i < euro.length; i++) parts.push(euro.subarray(i, i + 1)); +console.log(feed(utf8, parts), utf8.encoding); + +const hex = new StringDecoder("hex"); +console.log(feed(hex, [Buffer.from([1, 2]), Buffer.from([255])]), hex.encoding); + +const utf16 = new StringDecoder("ucs2"); +const pending = [Buffer.from([0x61]), Buffer.from([0x00, 0x62]), Buffer.from([0x00])]; +console.log(JSON.stringify(feed(utf16, pending)), utf16.encoding); + +function passThrough(d) { + return d; +} +const kept = passThrough(utf8); +console.log(kept === utf8, typeof kept, kept.write("text"), kept.end(Buffer.from([0xf0, 0x9f]))); +try { + kept.write(null); +} catch (e) { + console.log(e.code, e.message); +} diff --git a/tests/corpus/8234-handle-typeof.ts b/tests/corpus/8234-handle-typeof.ts new file mode 100644 index 000000000..f18240130 --- /dev/null +++ b/tests/corpus/8234-handle-typeof.ts @@ -0,0 +1,24 @@ +// typeof of the objects the runtime keeps as numeric handles answers +// "object", as Node's does: readline interfaces, timers, async-local +// stores, diagnostics channels and event-loop histograms. +import * as readline from "node:readline"; +import { PassThrough } from "node:stream"; +import { AsyncLocalStorage } from "node:async_hooks"; +import * as dc from "node:diagnostics_channel"; +import { monitorEventLoopDelay } from "node:perf_hooks"; + +const rl = readline.createInterface({ input: new PassThrough(), terminal: false }); +const timeout = setTimeout(() => {}, 1); +const interval = setInterval(() => {}, 1000); +const immediate = setImmediate(() => {}); +const store = new AsyncLocalStorage(); +const channel = dc.channel("handles"); +const tracing = dc.tracingChannel("handles"); +const histogram = monitorEventLoopDelay(); +const wrapped: Number = 5; +console.log(typeof rl, typeof timeout, typeof interval, typeof immediate); +console.log(typeof store, typeof channel, typeof tracing, typeof histogram, typeof wrapped); +clearTimeout(timeout); +clearInterval(interval); +clearImmediate(immediate); +rl.close(); diff --git a/tests/corpus/8240-asserted-callee-errors.ts b/tests/corpus/8240-asserted-callee-errors.ts new file mode 100644 index 000000000..91537ed70 --- /dev/null +++ b/tests/corpus/8240-asserted-callee-errors.ts @@ -0,0 +1,25 @@ +// A non-null assertion is erased in JavaScript, so calling an asserted callee +// that holds undefined throws the call's own TypeError, with V8's spelling of +// the callee: element keys, literal keys as names, parentheses and asserted +// links dropped, call results as f(...). +type F = (x: number) => number; +const table: Record = { inc: (x) => x + 1 }; +const obj: { cb?: F; inner?: { cb?: F } } = { inner: {} }; +const arr: (F | undefined)[] = [undefined]; +let maybe: F | undefined; +function get(): F | undefined { return undefined; } +const key = process.argv.length > 99 ? "inc" : "missing"; +const cases: [string, () => number][] = [ + ["computed", () => table[key]!(1)], + ["literal key", () => table["missing"]!(1)], + ["dotted", () => obj.cb!(1)], + ["nested", () => obj.inner!.cb!(1)], + ["element", () => arr[0]!(1)], + ["local", () => maybe!(1)], + ["parens", () => (table[key])!(1)], + ["call result", () => get()!(1)], + ["parens outside", () => (table[key]!)(1)], +]; +for (const [name, run] of cases) { + try { console.log(name, run()); } catch (e) { console.log(name, (e as Error).name + ": " + (e as Error).message); } +} diff --git a/tests/corpus/8241-literal-result-slices.ts b/tests/corpus/8241-literal-result-slices.ts new file mode 100644 index 000000000..05deb40a5 --- /dev/null +++ b/tests/corpus/8241-literal-result-slices.ts @@ -0,0 +1,90 @@ +// Destructured results of small literal producers: string slices, numbers +// and booleans, with evaluation order, unread fields, throws and every +// slice() and substring() bound kind. +let trace = ""; +function mark(tag: string, value: T): T { + trace += tag; + return value; +} + +function split(text: string, at: number): [string, string] { + return [text.slice(0, at), text.slice(at)]; +} +function cut(text: string, from: number, to: number): [string, number, boolean] { + return [text.substring(from, to), text.length, from > to]; +} +function divmod(a: number, b: number): [number, number] { + return [Math.floor(a / b), a % b]; +} +function pair(text: string): { head: string; size: number } { + return { size: mark("s", text.length), head: mark("h", text.slice(1, -1)) }; +} + +const bounds = [0, 1, 2, 3, -1, -2, -3, -10, 10, 0.5, -0.5, 1.9, -1.9, -0, NaN, Infinity, -Infinity]; +const texts = ["", "a", "ab", "abcdef", "héllo", "東京ok", "a\u{1F600}b"]; +let lengths = 0; +let units = 0; +for (const text of texts) { + for (const at of bounds) { + const [head, tail] = split(text, at); + lengths = lengths * 7 + head.length * 3 + tail.length; + lengths %= 1000000007; + if (head.length > 0) units += head.charCodeAt(head.length - 1); + if (tail.length > 0) units += tail.charCodeAt(0); + for (const to of bounds) { + const [part, size, swapped] = cut(text, at, to); + lengths = (lengths * 5 + part.length + size + (swapped ? 1 : 0)) % 1000000007; + } + } +} +console.log("lengths", lengths, "units", units); + +// The pieces themselves, as strings. +const [left, right] = split("scriptc", 3); +console.log(left, right, `${left}|${right}`); + +// Arguments, then fields in the literal's order, then the bindings. +trace = ""; +const { head, size } = pair(mark("a", "[inner]")); +console.log("order", trace, head.length, size); + +// Unread fields still run. +trace = ""; +const [onlyHead] = split(mark("x", "unread"), mark("y", 2)); +const { size: onlySize } = pair("<>"); +console.log("unread", trace, onlyHead, onlySize); + +// A mutable destructured binding. +let [first, rest] = split("mutable", 1); +first += "!"; +console.log("let", first, rest.length); + +// Numbers and a throw from an argument. +let sum = 0; +for (let i = 0; i < 50; i++) { + const [q, r] = divmod(i * 31, 17); + sum += q * 3 + r; +} +console.log("divmod", sum); +const list: (string | undefined)[] = ["xy", undefined]; +for (const item of list) { + try { + const [a, b] = split(item!, 1); + console.log("ok", a.length, b.length); + } catch (error) { + console.log("threw", error instanceof TypeError); + } +} + +// The producer keeps working for every other caller. +const whole = split("kept", 2); +console.log("kept", whole[0], whole[1], whole.length); + +// Fields that write a parameter: each inlined call writes its own copy. +function bump(a: number, s: string): [number, number, string] { + return [a++, (a = a * 2), s.slice(a - 3)]; +} +for (const start of [2, 3]) { + const [before, doubled, tailOf] = bump(start, "abcdefgh"); + console.log("writes", start, before, doubled, tailOf); +} diff --git a/tests/corpus/8242-string-scan-republish.ts b/tests/corpus/8242-string-scan-republish.ts new file mode 100644 index 000000000..9800bb099 --- /dev/null +++ b/tests/corpus/8242-string-scan-republish.ts @@ -0,0 +1,108 @@ +// Token scans that alternate between two non-ASCII strings: each inner loop +// starts at a token and runs while the class holds, so every token enters a +// loop over one of the strings again. Also a string extended in place +// between scans, strings rebuilt with the same length and new content, and +// slices of a scanned string. + +function isWord(c: number): boolean { + return (c >= 97 && c <= 122) || (c >= 65 && c <= 90) || c >= 0xc0; +} + +function isDigit(c: number): boolean { + return c >= 48 && c <= 57; +} + +// The kinds and lengths of s's tokens, hashed. +function lex(s: string): number { + let h = 0, + tokens = 0, + pos = 0; + const end = s.length; + while (pos < end) { + const c = s.charCodeAt(pos); + let start = pos; + if (isWord(c)) { + while (pos < end && isWord(s.charCodeAt(pos))) pos++; + h = (h * 31 + 1) | 0; + } else if (isDigit(c)) { + while (pos < end && isDigit(s.charCodeAt(pos))) pos++; + h = (h * 31 + 2) | 0; + } else if (c === 32) { + while (pos < end && s.charCodeAt(pos) === 32) pos++; + start = pos; + } else { + pos++; + h = (h * 31 + c) | 0; + } + h = (h * 31 + (pos - start)) | 0; + tokens++; + } + return h ^ tokens; +} + +// Two strings lexed token by token in turn: each token's loop runs over a +// different receiver than the one before it. +function interleave(a: string, b: string): number { + let i = 0, + j = 0, + h = 0; + while (i < a.length || j < b.length) { + const fromA = i < a.length; + while (i < a.length && a.charCodeAt(i) !== 32) { + h = (h * 31 + a.charCodeAt(i)) | 0; + i++; + } + i++; + while (j < b.length && b.charCodeAt(j) !== 32) { + h = (h * 37 + b.charCodeAt(j)) | 0; + j++; + } + j++; + if (fromA) h = (h + 1) | 0; + } + return h; +} + +// The sum of every unit of s. +function units(s: string): number { + let sum = 0; + for (let i = 0; i < s.length; i++) sum += s.charCodeAt(i); + return sum; +} + +function text(seed: number, words: number): string { + const parts = ["café", "naïve", "façade", "x", "😀", "Ωmega", "über", "ab"]; + let s = ""; + for (let k = 0; k < words; k++) { + s += parts[(seed + k * 7) % parts.length]!; + s += k % 5 === 0 ? " " + ((seed * k) % 1000) + " " : k % 3 === 0 ? "; " : " "; + } + return s; +} + +const first = text(1, 300), + second = text(4, 280); +for (let pass = 0; pass < 3; pass++) { + console.log(lex(first), lex(second), lex(first), units(second), units(first)); + console.log(interleave(first, second), interleave(second, first)); +} + +// Extended in place between scans: the second scan reads the new units. +let grown = text(2, 120); +console.log(lex(grown), units(grown), grown.length); +for (let round = 0; round < 4; round++) { + grown += round % 2 === 0 ? " é1 ü22 " : " 😀 tail "; + console.log(lex(grown), units(grown), grown.length, interleave(grown, first)); +} + +// Same length, different content, built after the previous one is dropped. +for (let round = 0; round < 6; round++) { + const s = round % 2 === 0 ? "é".repeat(200) + " a" : "ü".repeat(200) + " b"; + console.log(lex(s), units(s), units(s.slice(0, 100)), lex(second)); +} + +// Slices of a scanned string, scanned between scans of the whole. +for (const at of [0, 7, 101, first.length - 9]) { + const part = first.slice(at, at + 64); + console.log(lex(part), units(part), lex(first), units(part.slice(3))); +} diff --git a/tests/corpus/8243-json-parse-discriminated-unions.ts b/tests/corpus/8243-json-parse-discriminated-unions.ts new file mode 100644 index 000000000..df3fd6dd8 --- /dev/null +++ b/tests/corpus/8243-json-parse-discriminated-unions.ts @@ -0,0 +1,119 @@ +// JSON.parse cast to discriminated unions of records: arrays of them, +// whole documents, members (with null and optional arms) and recursive +// trees. The discriminant appears first, last or between other members, +// escaped, repeated, and beside undeclared members; values hold escapes +// and non-ASCII text. + +type Event = + | { kind: "click"; x: number; y: number } + | { kind: "focus" | "blur"; target: string } + | { key: string; kind: "key"; repeat: boolean } + | { kind: "load"; ms: number; next: Event | null; note?: string }; + +type Expr = + | { kind: "num"; value: number } + | { kind: "neg"; operand: Expr } + | { items: Expr[]; kind: "max" } + | { body: Expr; kind: "let"; name: string; value: Expr }; + +function describe(e: Event): string { + switch (e.kind) { + case "click": + return `click ${e.x},${e.y}`; + case "focus": + case "blur": + return `${e.kind} ${e.target}`; + case "key": + return `key ${e.key}${e.repeat ? " (repeat)" : ""}`; + case "load": + return `load ${e.ms}${e.note === undefined ? "" : ` "${e.note}"`}${e.next === null ? "" : ` then ${describe(e.next)}`}`; + } +} + +function evaluate(e: Expr, env: Map): number { + switch (e.kind) { + case "num": + return e.value; + case "neg": + return -evaluate(e.operand, env); + case "max": { + let best = -Infinity; + for (const item of e.items) best = Math.max(best, evaluate(item, env)); + return best; + } + case "let": { + const inner = new Map(env); + inner.set(e.name, evaluate(e.value, env)); + return evaluate(e.body, inner) + (inner.get(e.name) ?? 0); + } + } +} + +const events = JSON.parse(`[ + {"kind":"click","x":1,"y":2}, + {"x":3,"y":4,"kind":"click"}, + {"y":6,"kind":"click","x":5}, + {"kind":"focus","target":"input"}, + {"target":"button","kind":"blur"}, + {"key":"Enter","kind":"key","repeat":false}, + {"repeat":true,"key":"\\u00e9","kind":"key"}, + {"kind":"load","ms":12,"next":null}, + {"kind":"load","ms":7,"note":"caf\\u00e9 \\ud83d\\ude00","next":{"kind":"blur","target":"tab"}}, + {"next":{"kind":"load","ms":1,"next":{"kind":"click","x":0,"y":-1}},"ms":30,"kind":"load"}, + {"k\\u0069nd":"focus","target":"escaped key"}, + {"kind":"bl\\u0075r","target":"escaped value"}, + {"kind":"click","kind":"focus","target":"repeated"}, + {"kind":"click","x":8,"y":9,"extra":{"kind":"not an event","list":[1,[2]]}}, + { "kind" : "key" , "key" : "space" , "repeat" : true } +]`) as Event[]; +console.log(events.length); +for (const e of events) console.log(describe(e)); +console.log(JSON.stringify(events[12]), JSON.stringify(events[13])); + +const empty = JSON.parse("[]") as Event[]; +console.log(empty.length); + +const single = JSON.parse('{"target":"whole document","kind":"focus"}') as Event; +console.log(describe(single)); + +const tree = JSON.parse(`{ + "body": {"items": [{"kind":"num","value":4}, {"kind":"neg","operand":{"kind":"num","value":-9}}, {"items":[],"kind":"max"}], "kind": "max"}, + "kind": "let", + "name": "x", + "value": {"kind":"neg","operand":{"kind":"neg","operand":{"kind":"num","value":2.5}}} +}`) as Expr; +console.log(evaluate(tree, new Map())); + +const forest = JSON.parse( + `[${Array.from({ length: 50 }, (_, i) => + i % 3 === 0 + ? `{"kind":"num","value":${i}}` + : i % 3 === 1 + ? `{"operand":{"kind":"num","value":${i}},"kind":"neg"}` + : `{"items":[{"kind":"num","value":${i}},{"kind":"num","value":${50 - i}}],"kind":"max"}`, + ).join(",")}]`, +) as Expr[]; +let total = 0; +for (const t of forest) total += evaluate(t, new Map()); +console.log(forest.length, total); + +// Parsing the same text again builds separate values. +const again = JSON.parse('[{"kind":"num","value":1},{"kind":"num","value":2}]') as Expr[]; +const twice = JSON.parse('[{"kind":"num","value":1},{"kind":"num","value":2}]') as Expr[]; +console.log(again.length + twice.length, again[0] === twice[0]); + +// Malformed text throws Node's SyntaxError, including text that ends +// inside the members before the discriminant or inside its value. +for (const text of [ + '[{"kind":"click","x":1,"y":2},', + '[{"x":1,"kind"', + '[{"x":[1,2,"kind":"click"}]', + '[{"kind":"cl', + '[{"target":"a" "kind":"focus"}]', +]) { + try { + console.log((JSON.parse(text) as Event[]).length); + } catch (error) { + console.log(error instanceof SyntaxError, (error as Error).message); + } +} diff --git a/tests/corpus/8244-speculative-floor-mod-powers.ts b/tests/corpus/8244-speculative-floor-mod-powers.ts new file mode 100644 index 000000000..db28dcde0 --- /dev/null +++ b/tests/corpus/8244-speculative-floor-mod-powers.ts @@ -0,0 +1,88 @@ +// Integer loops whose writes take a floor-mod by a power of two +// (`v - Math.floor(v / 2^n) * 2^n`) or multiply by one: negative and zero +// values, exponents 0, 52 and above, literal powers, both factor orders, +// fractional values and results past 2^53, which leave the integer copy. + +function trim(value: number, bits: number): number { + return value - Math.floor(value / Math.pow(2, bits)) * Math.pow(2, bits); +} + +function low3(value: number): number { + return value - Math.floor(value / 8) * 8; +} + +function rolling(bytes: Uint8Array, bits: number): number { + let key = 0; + let sum = 0; + for (let i = 0; i < bytes.length; i++) { + key = trim(key, bits) * 4 + bytes[i]!; + sum = (sum + key) % 1000003; + } + return key * 7 + sum; +} + +function stepped(start: number, step: number, bits: number, count: number): number { + let v = start; + let h = 0; + for (let i = 0; i < count; i++) { + v = trim(v, bits) * 3 - step; + h = (h * 31 + v) % 999983; + } + return h * 1000 + v; +} + +function literal(start: number, count: number): number { + let v = start; + let h = 0; + for (let i = 0; i < count; i++) { + v = low3(v) * 16 - i; + h = (h * 17 + v) % 65521; + } + return h * 100 + v; +} + +function scaled(start: number, bits: number, count: number): number { + let v = start; + let h = 0; + for (let i = 0; i < count; i++) { + v = Math.pow(2, bits) * v + 1; + h = (h * 7 + v) % 10007; + } + return h + v; +} + +function shifted(start: number, count: number): number { + let v = start; + let h = 0; + for (let i = 0; i < count; i++) { + v = v * 1024 - i; + h = (h * 3 + v) % 101; + } + return h + v; +} + +const bytes = new Uint8Array(4000); +for (let i = 0; i < bytes.length; i++) bytes[i] = (i * 7 + (i >> 3)) & 3; + +for (const bits of [0, 1, 2, 10, 22, 34, 50, 52, 53, 60]) console.log("rolling", bits, rolling(bytes, bits)); +for (const [start, step, bits] of [ + [0, 1, 3], + [-5, 3, 2], + [-1, -1, 0], + [123456789, 7, 20], + [-9007199254740991, 1, 52], + [2.5, 1, 4], + [7, 0.5, 3], +] as const) + console.log("stepped", start, step, bits, stepped(start, step, bits, 50)); +for (const start of [0, 13, -13, 4096, -7.25]) console.log("literal", start, literal(start, 40)); +for (const [start, bits] of [ + [1, 3], + [-2, 5], + [0, 52], + [3, 20], + [-1, 0], +] as const) + console.log("scaled", start, bits, scaled(start, bits, 6)); +for (const start of [0, 1, -1, 3, 1e6]) console.log("shifted", start, shifted(start, 7)); +console.log(Object.is(stepped(0, 0, 3, 5), 0), Object.is(scaled(0, 2, 3), 21 + 1)); diff --git a/tests/corpus/8260-class-prototype-views.mjs b/tests/corpus/8260-class-prototype-views.mjs new file mode 100644 index 000000000..0f7b080bf --- /dev/null +++ b/tests/corpus/8260-class-prototype-views.mjs @@ -0,0 +1,178 @@ +// A class's prototype as the prototype of other objects: ES5 constructors +// that inherit from a class through Object.create, Object.setPrototypeOf and +// util.inherits, and plain objects that adopt a class prototype. Inherited +// methods, accessors and calls between methods run with the object as the +// receiver. +import util from "node:util"; + +class Shape { + constructor(name) { + this.name = name; + } + get label() { + return `${this.name} (${this.sides()} sides)`; + } + sides() { + return 0; + } + describe() { + return `${this.label}: area ${this.area().toFixed(2)}`; + } + area() { + return 0; + } +} + +class Polygon extends Shape { + sides() { + return this.points.length; + } + perimeter() { + let total = 0; + for (let i = 0; i < this.points.length; i++) { + const [x1, y1] = this.points[i]; + const [x2, y2] = this.points[(i + 1) % this.points.length]; + total += Math.hypot(x2 - x1, y2 - y1); + } + return total; + } +} + +// Object.create(Class.prototype) for an ES5 subclass. +function Square(size) { + this.name = "square"; + this.size = size; + this.points = [ + [0, 0], + [size, 0], + [size, size], + [0, size], + ]; +} +Square.prototype = Object.create(Polygon.prototype); +Square.prototype.constructor = Square; +Square.prototype.area = function () { + return this.size * this.size; +}; + +const square = new Square(3); +console.log(square.describe()); +console.log(square.perimeter(), square.sides(), square.label); +console.log(square instanceof Square, square instanceof Polygon, square instanceof Shape); +console.log(Object.getPrototypeOf(Square.prototype) === Polygon.prototype); +console.log(Object.getPrototypeOf(Polygon.prototype) === Shape.prototype); +console.log(Object.keys(square).join(",")); +const inherited = []; +for (const key in square) inherited.push(key); +console.log(inherited.join(",")); + +// Object.setPrototypeOf on an ES5 constructor's prototype. +function Circle(radius) { + this.name = "circle"; + this.radius = radius; +} +Circle.prototype.area = function () { + return Math.PI * this.radius * this.radius; +}; +Object.setPrototypeOf(Circle.prototype, Shape.prototype); +const circle = new Circle(2); +console.log(circle.describe(), circle instanceof Shape, circle instanceof Polygon); + +// util.inherits with a class as the superCtor. +function Triangle(a, b, c) { + this.name = "triangle"; + this.points = [a, b, c]; +} +util.inherits(Triangle, Polygon); +Triangle.prototype.area = function () { + const [[x1, y1], [x2, y2], [x3, y3]] = this.points; + return Math.abs((x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2)) / 2); +}; +const triangle = new Triangle([0, 0], [4, 0], [0, 3]); +console.log(Triangle.super_ === Polygon, triangle instanceof Polygon); +console.log(triangle.describe(), triangle.perimeter()); + +// A plain object that adopts a class prototype (rehydrating parsed data). +const parsed = JSON.parse('{"name":"lot","points":[[0,0],[2,0],[2,1]]}'); +Object.setPrototypeOf(parsed, Polygon.prototype); +console.log(parsed.sides(), parsed.perimeter().toFixed(3), parsed.label, parsed instanceof Polygon); +const fresh = Object.create(Shape.prototype); +fresh.name = "blank"; +console.log(fresh.describe(), fresh instanceof Shape, Object.getPrototypeOf(fresh) === Shape.prototype); + +// Native instances keep their own methods and answers. +const native = new Polygon("native"); +native.points = [ + [0, 0], + [1, 0], + [0, 1], +]; +console.log(native.describe(), native.perimeter().toFixed(3), native instanceof Polygon); + +// util.inspect names each object by its prototype's constructor. +function Plain() { + this.v = 1; +} +Object.setPrototypeOf(Plain.prototype, Shape.prototype); +console.log(new Square(1).size, fresh, new Plain(), Object.prototype.toString.call(fresh)); +console.log(util.inspect(new Plain()), util.inspect(parsed, { depth: 0 })); + +// A subclass keeps its own constructor below a base whose prototype other +// objects inherit: `new this.constructor()` in a base method builds the +// subclass, also while the subclass's prototype holds a replaceable method. +class Disc extends Shape { + area() { + return 3; + } + rebuild() { + return new this.constructor("copy of " + this.name); + } +} +const areaValue = Shape.prototype.area; +const disc = new Disc("d"); +const rebuilt = disc.rebuild(); +console.log(disc.constructor === Disc, rebuilt instanceof Disc, rebuilt.area(), rebuilt.name, typeof areaValue); + +// Deleting a reflected prototype's constructor: instances read the +// constructor their base class's prototype holds, then Object's; a +// replacement answers again. +class Root { + hi() { + return "hi"; + } +} +class Middle extends Root {} +class Tip extends Middle {} +const members = [new Middle(), new Tip(), new Root(), {}]; +const constructorNames = () => members.map((member) => member.constructor.name).join(","); +console.log(constructorNames(), Object.getOwnPropertyNames(Object.getPrototypeOf(new Middle())).join("|")); +delete Object.getPrototypeOf(new Middle()).constructor; +console.log(constructorNames(), Object.getOwnPropertyNames(Object.getPrototypeOf(new Middle())).join("|")); +console.log(new Middle().constructor === Root, new Tip().constructor === Tip); +delete Object.getPrototypeOf(new Root()).constructor; +console.log(constructorNames(), new Middle().constructor === Object); +Middle.prototype.constructor = function Named() {}; +console.log(constructorNames()); + +// A reflected prototype whose constructor is an accessor: an instance's read runs the getter with the +// instance as `this`, also where the instance's class inherits it. +class Facet { + constructor(tag) { + this.tag = tag; + } +} +class Tile extends Facet {} +class Brick extends Tile {} +const facets = [new Tile("s"), new Brick("c"), new Facet("p")]; +const facetTags = () => + facets.map((facet) => (typeof facet.constructor === "string" ? facet.constructor : facet.constructor.name)).join(","); +Object.defineProperty(Tile.prototype, "constructor", { + get() { + return "on " + this.tag; + }, + configurable: true, +}); +delete Object.getPrototypeOf(new Brick("x")).constructor; +console.log(facetTags()); +delete Object.getPrototypeOf(new Tile("x")).constructor; +console.log(facetTags()); diff --git a/tests/corpus/8270-fs-promises-member.cjs b/tests/corpus/8270-fs-promises-member.cjs new file mode 100644 index 000000000..86448f012 --- /dev/null +++ b/tests/corpus/8270-fs-promises-member.cjs @@ -0,0 +1,25 @@ +'use strict'; +// fs.promises is node:fs/promises, as in Node: through a binding, an +// inline require, destructuring and a direct member call. +const fs = require('fs'); +const path = require('path'); +const os = require('os'); + +const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'scriptc-fsp-')); +const file = path.join(dir, 'data.txt'); +const fsPromises = fs.promises; +const { readFile, stat } = require('fs').promises; +const { open } = fs.promises; + +(async () => { + await fsPromises.writeFile(file, 'abc'); + await fs.promises.appendFile(file, 'def'); + console.log(await readFile(file, 'utf8')); + console.log((await stat(file)).size); + const handle = await open(file, 'r'); + const { bytesRead, buffer } = await handle.read(Buffer.alloc(3), 0, 3, 1); + console.log(bytesRead, buffer.toString()); + await handle.close(); + await fsPromises.rm(dir, { recursive: true }); + console.log(fs.existsSync(dir)); +})(); diff --git a/tests/corpus/8271-cjs-top-level-return.cjs b/tests/corpus/8271-cjs-top-level-return.cjs new file mode 100644 index 000000000..9f5cc6d23 --- /dev/null +++ b/tests/corpus/8271-cjs-top-level-return.cjs @@ -0,0 +1,20 @@ +'use strict'; +// A return at the top level of the entry CommonJS module ends its +// evaluation, as in Node's module wrapper: from a block, a loop and a try +// statement, whose finally block still runs. +const fs = require('fs'); + +console.log('start'); +for (const name of ['a', 'b', 'c']) { + if (name === 'c') { + try { + console.log('returning at', name); + process.on('exit', (code) => console.log('exit', code)); + if (!fs.existsSync('/scriptc-missing-path')) return; + } finally { + console.log('finally runs'); + } + } + console.log('visited', name); +} +console.log('not reached'); diff --git a/tests/corpus/8272-fs-stream-call-without-new.cjs b/tests/corpus/8272-fs-stream-call-without-new.cjs new file mode 100644 index 000000000..91fe0a6c0 --- /dev/null +++ b/tests/corpus/8272-fs-stream-call-without-new.cjs @@ -0,0 +1,21 @@ +'use strict'; +// fs.ReadStream and fs.WriteStream called without new construct their +// stream, as in Node. +const fs = require('fs'); +const path = require('path'); +const os = require('os'); + +const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'scriptc-streams-')); +const target = path.join(dir, 'out.txt'); +const writer = fs.WriteStream(target); +writer.on('finish', () => { + const reader = fs.ReadStream(target, { encoding: 'utf8' }); + let text = ''; + reader.on('data', (chunk) => (text += chunk)); + reader.on('end', () => { + console.log(text); + fs.rmSync(dir, { recursive: true }); + }); +}); +writer.write('hello '); +writer.end('streams'); diff --git a/tests/corpus/8273-fs-read-write-argument-checks.cjs b/tests/corpus/8273-fs-read-write-argument-checks.cjs new file mode 100644 index 000000000..b27564837 --- /dev/null +++ b/tests/corpus/8273-fs-read-write-argument-checks.cjs @@ -0,0 +1,80 @@ +'use strict'; +// fs.read, fs.readSync and fs.write check their arguments in Node's order +// and with Node's errors: the fd, the buffer, the offset, `length |= 0`, +// number and bigint positions, and the buffer window. A length with a +// valueOf method is converted once. +const fs = require('fs'); +const path = require('path'); +const os = require('os'); + +const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'scriptc-fs-args-')); +const file = path.join(dir, 'data.txt'); +fs.writeFileSync(file, 'abcdef'); +const fd = fs.openSync(file, 'r+'); +const noop = () => {}; + +function attempt(name, fn) { + try { + fn(); + console.log(name, 'ok'); + } catch (error) { + console.log(name, error.name, error.code, error.message); + } +} + +attempt('read offset -1', () => fs.read(fd, Buffer.alloc(3), -1, 3, 0, noop)); +attempt('read offset NaN', () => fs.read(fd, Buffer.alloc(3), NaN, 3, 0, noop)); +attempt('read length -1', () => fs.read(fd, Buffer.alloc(3), 0, -1, 0, noop)); +attempt('read length 4', () => fs.read(fd, Buffer.alloc(3), 0, 4, 0, noop)); +attempt('read length 1.5', () => fs.read(fd, Buffer.alloc(3), 0, 1.5, 0, noop)); +attempt('read length "x"', () => fs.read(fd, Buffer.alloc(3), 0, 'x', 0, noop)); +attempt('read position 1.5', () => fs.read(fd, Buffer.alloc(3), 0, 3, 1.5, noop)); +attempt('read position -2', () => fs.read(fd, Buffer.alloc(3), 0, 3, -2, noop)); +attempt('read position ""', () => fs.read(fd, Buffer.alloc(3), 0, 3, '', noop)); +attempt('read position 2n ** 63n', () => fs.read(fd, Buffer.alloc(3), 0, 3, 2n ** 63n, noop)); +attempt('read position -2n', () => fs.read(fd, Buffer.alloc(3), 0, 3, -2n, noop)); +attempt('read empty buffer', () => fs.read(fd, Buffer.alloc(0), 0, 1, 0, noop)); +attempt('read fd true', () => fs.read(true, Buffer.alloc(3), 0, 3, 0, noop)); +attempt('read buffer number', () => fs.read(fd, 3, 0, 3, 0, noop)); + +const read = (position) => { + const buffer = Buffer.alloc(3); + const count = fs.readSync(fd, buffer, 0, 3, position); + return `${count} ${buffer.toString()}`; +}; +attempt('readSync position 1n', () => console.log(read(1n))); +attempt('readSync position 0n', () => console.log(read(0n))); +attempt('readSync position 2n ** 63n', () => read(2n ** 63n)); +attempt('readSync offset 4', () => fs.readSync(fd, Buffer.alloc(3), 4, 1, 0)); +attempt('readSync buffer number', () => fs.readSync(fd, 3, 0, 1, 0)); +attempt('readSync fd null', () => fs.readSync(null, Buffer.alloc(3), 0, 1, 0)); +console.log(fs.readSync(fd, Buffer.alloc(3), 0, 0.5, 0), fs.readSync(fd, Buffer.alloc(3), 0, 2.5, 0)); +let syncCalls = 0; +const syncLength = { valueOf: () => (++syncCalls === 1 ? 2 : 3) }; +const syncTarget = Buffer.alloc(3); +const syncCount = fs.readSync(fd, syncTarget, 0, syncLength, 0); +console.log('readSync valueOf', syncCount, syncCalls, syncTarget.toString('utf8', 0, syncCount)); + +attempt('write fd null', () => fs.write(null, Buffer.alloc(1), 0, 1, noop)); +attempt('write fd "1"', () => fs.write('1', 'x', noop)); +attempt('write offset -1', () => fs.write(fd, Buffer.alloc(0), -1, noop)); +attempt('write offset NaN', () => fs.write(fd, Buffer.from('abcd'), NaN, 4, 0, noop)); +attempt('write offset 5', () => fs.write(fd, Buffer.from('abcd'), 5, noop)); +attempt('write length 5', () => fs.write(fd, Buffer.from('abcd'), 0, 5, 0, noop)); +attempt('write length -1', () => fs.write(fd, Buffer.alloc(0), 0, -1, noop)); +attempt('write data number', () => fs.write(fd, 4, noop)); +attempt('write no callback', () => fs.write(fd, Buffer.from('a'), 0, 1, 0)); + +fs.read(fd, Buffer.alloc(2), 0, 2, 2n, (error, count, buffer) => { + console.log('read 2n', error, count, buffer.toString()); + fs.write(fd, Buffer.from('XY'), 0, 2, 0, (error2, written) => { + console.log('wrote', error2, written, fs.readFileSync(file, 'utf8')); + let calls = 0; + const length = { valueOf: () => (++calls === 1 ? 1 : 3) }; + fs.read(fd, Buffer.alloc(3), 0, length, 0, (error3, count3, buffer3) => { + console.log('read valueOf', error3, count3, calls, buffer3.toString('utf8', 0, count3)); + fs.closeSync(fd); + fs.rmSync(dir, { recursive: true }); + }); + }); +}); diff --git a/tests/corpus/8290-child-channel-members/main.ts b/tests/corpus/8290-child-channel-members/main.ts new file mode 100644 index 000000000..78ed52de1 --- /dev/null +++ b/tests/corpus/8290-child-channel-members/main.ts @@ -0,0 +1,28 @@ +import { fork, spawn } from "node:child_process"; + +// `send`, `disconnect` and `channel` exist only on a child created with an +// IPC channel, and stay after the channel closes; the other members are +// on every child. +const describe = (label: string, child: ReturnType): string => + `${label}: send ${"send" in child} disconnect ${"disconnect" in child} channel ${"channel" in child} kill ${"kill" in child} pid ${"pid" in child}`; + +const lines: string[] = []; +const plain = spawn("/bin/sh", ["-c", "exit 0"], { stdio: "ignore" }); +lines.push(describe("spawn", plain)); +const forked = fork(new URL("./worker.ts", import.meta.url), [], { + stdio: ["ignore", "ignore", "inherit", "ipc"], +}); +lines.push(describe("fork", forked)); +forked.once("message", () => { + forked.disconnect(); + lines.push(describe("fork after disconnect", forked)); +}); +forked.once("exit", () => { + lines.push(describe("fork after exit", forked)); +}); +plain.once("exit", () => { + lines.push(describe("spawn after exit", plain)); +}); +process.once("exit", () => { + console.log(lines.sort().join("\n")); +}); diff --git a/tests/corpus/8290-child-channel-members/worker.ts b/tests/corpus/8290-child-channel-members/worker.ts new file mode 100644 index 000000000..96401ea1a --- /dev/null +++ b/tests/corpus/8290-child-channel-members/worker.ts @@ -0,0 +1 @@ +process.send?.({ value: 1 }); diff --git a/tests/corpus/8291-frozen-record-union-keys.ts b/tests/corpus/8291-frozen-record-union-keys.ts new file mode 100644 index 000000000..b6173ef4e --- /dev/null +++ b/tests/corpus/8291-frozen-record-union-keys.ts @@ -0,0 +1,65 @@ +// Frozen records in record-only unions used as Set elements and Map keys: +// the union's release frees a frozen record when it drops the last +// reference, for unions the cycle collector traces and ones it does not. +type Leaf = { kind: "leaf"; label: string }; +type Pair = { kind: "pair"; left: Node; right: Node }; +type Node = Leaf | Pair; + +type Name = { kind: "name"; text: string }; +type Count = { kind: "count"; value: number; unit: string }; +type Key = Name | Count; + +function leaf(label: string): Node { + const built: Leaf = { kind: "leaf", label }; + return Object.freeze(built); +} + +function pair(left: Node, right: Node): Node { + const built: Pair = { kind: "pair", left, right }; + return Object.freeze(built); +} + +function key(i: number): Key { + if (i % 2 === 0) { + const name: Name = { kind: "name", text: "n" + i }; + return Object.freeze(name); + } + const count: Count = { kind: "count", value: i, unit: "u" + i }; + return Object.freeze(count); +} + +function nodes(n: number): number { + const seen = new Set(); + const weight = new Map(); + let last = leaf("start"); + for (let i = 0; i < n; i++) { + const next = i % 3 === 0 ? leaf("l" + i) : pair(last, leaf("r" + i)); + seen.add(next); + weight.set(next, i); + last = next; + } + let total = 0; + for (const node of seen) total += (weight.get(node) ?? 0) + (node.kind === "pair" ? 1 : 0); + seen.clear(); + weight.clear(); + return total + (last.kind === "pair" ? 1 : 0); +} + +function keys(n: number): number { + const seen = new Set(); + const size = new Map(); + for (let i = 0; i < n; i++) { + const k = key(i); + seen.add(k); + size.set(k, k.kind === "name" ? k.text.length : k.value); + } + let total = 0; + for (const k of seen) total += size.get(k) ?? 0; + seen.clear(); + size.clear(); + return total; +} + +let sum = 0; +for (let r = 0; r < 50; r++) sum += nodes(40) + keys(40); +console.log(sum); diff --git a/tests/corpus/8300-native-array-capsule-reads/lib.cjs b/tests/corpus/8300-native-array-capsule-reads/lib.cjs new file mode 100644 index 000000000..6f386216b --- /dev/null +++ b/tests/corpus/8300-native-array-capsule-reads/lib.cjs @@ -0,0 +1,36 @@ +"use strict"; +// Reads and writes the arrays the typed program passes in. +exports.total = function (xs) { + let s = 0; + for (let i = 0; i < xs.length; i++) s += xs[i]; + return s; +}; +exports.at = function (xs, i) { + return xs[i]; +}; +exports.describe = function (xs, keys) { + const out = []; + for (const k of keys) { + const v = xs[k]; + out.push(typeof v === "number" && Object.is(v, -0) ? "-0" : String(v)); + } + return out.join(" "); +}; +exports.has = function (xs, i) { + return i in xs; +}; +exports.write = function (xs, i, v) { + xs[i] = v; + return xs[i]; +}; +exports.grow = function (xs, v) { + xs.push(v); + return xs.length; +}; +exports.cut = function (xs, n) { + xs.length = n; + return xs.length; +}; +exports.lengthOf = function (xs) { + return xs.length; +}; diff --git a/tests/corpus/8300-native-array-capsule-reads/main.ts b/tests/corpus/8300-native-array-capsule-reads/main.ts new file mode 100644 index 000000000..5b1513ee4 --- /dev/null +++ b/tests/corpus/8300-native-array-capsule-reads/main.ts @@ -0,0 +1,59 @@ +// A typed program passes its arrays to untyped CommonJS functions, which +// read their elements and lengths, write them and resize them. Element reads +// cover integral, negative, fractional, non-finite and out-of-range indexes, +// string keys, holes, frozen arrays and arrays of other element types. +import { createRequire } from "node:module"; + +interface Lib { + total(xs: readonly unknown[]): number; + at(xs: readonly unknown[], i: unknown): unknown; + describe(xs: readonly unknown[], keys: unknown[]): string; + has(xs: readonly unknown[], i: number): boolean; + write(xs: unknown[], i: number, v: unknown): unknown; + grow(xs: unknown[], v: unknown): number; + cut(xs: unknown[], n: number): number; + lengthOf(xs: readonly unknown[]): number; +} +const require = createRequire(import.meta.url); +const lib: Lib = require("./lib.cjs"); + +// Element reads of a number[] at integral, negative, fractional, +// non-finite and out-of-range indexes, and through string keys. +const nums: number[] = [1, 2.5, -0, NaN, Infinity, -7]; +console.log(lib.total([1, 2, 3, 4]), lib.total(nums), lib.total([])); +console.log(lib.describe(nums, [0, 1, 2, 3, 4, 5, 6, -1, 1.5, -0, NaN, Infinity, 2 ** 32, "1", "01", "length"])); +console.log(lib.at(nums, 5), lib.at(nums, 6), typeof lib.at(nums, 2), Object.is(lib.at(nums, 2), -0)); + +// Holes: below and past the hole-free prefix, and an array cut longer. +const holey: number[] = [1, 2]; +holey[4] = 5; +console.log(lib.describe(holey, [0, 1, 2, 3, 4, 5]), lib.has(holey, 3), lib.has(holey, 4), lib.lengthOf(holey)); +const longer: number[] = [3, 4, 5]; +longer.length = 6; +console.log(lib.describe(longer, [0, 2, 3, 5]), lib.lengthOf(longer)); + +// Writes from the package and from the program are seen by both. +const live: number[] = [10, 20, 30]; +console.log(lib.write(live, 0, 11), lib.write(live, 3, 40), lib.grow(live, 50), live.join(",")); +live.push(60); +live[1] = 21; +console.log(lib.total(live), lib.lengthOf(live), lib.at(live, 5)); +console.log(lib.cut(live, 2), lib.total(live), live.length, lib.at(live, 2)); + +// Frozen and empty arrays. +const frozen: readonly number[] = Object.freeze([4, 5, 6]); +console.log(lib.total(frozen), lib.at(frozen, 1), lib.at([], 0), lib.lengthOf([])); + +// Arrays of other element types read through the same sites. +const words: string[] = ["a", "b", "c"]; +const flags: boolean[] = [true, false]; +const points: { x: number }[] = [{ x: 1 }, { x: 2 }]; +console.log(lib.describe(words, [0, 2, 3]), lib.describe(flags, [0, 1, 2]), (lib.at(points, 1) as { x: number }).x); +console.log(lib.at(words, 1.5), lib.has(words, 2), lib.lengthOf(points)); + +// A loop over a larger array. +const big: number[] = []; +for (let i = 0; i < 1000; i++) big.push(i % 7); +let sum = 0; +for (let round = 0; round < 50; round++) sum += lib.total(big); +console.log(sum); diff --git a/tests/corpus/8300-native-array-capsule-reads/package.json b/tests/corpus/8300-native-array-capsule-reads/package.json new file mode 100644 index 000000000..5ffd9800b --- /dev/null +++ b/tests/corpus/8300-native-array-capsule-reads/package.json @@ -0,0 +1 @@ +{ "type": "module" } diff --git a/tests/corpus/8302-class-super-method-values/lib.cjs b/tests/corpus/8302-class-super-method-values/lib.cjs new file mode 100644 index 000000000..4a8b1640a --- /dev/null +++ b/tests/corpus/8302-class-super-method-values/lib.cjs @@ -0,0 +1,33 @@ +"use strict"; +// Subclasses whose methods call super, read and called through computed and +// literal keys, as values called with call(), and directly. +class Base { + constructor(n) { this.n = n; } + scale(k) { return this.n * k; } + label() { return "base:" + this.n; } + plain(k) { return k; } +} +class Child extends Base { + scale(k) { return super.scale(k) * 10; } + label() { return "child(" + super.label() + ")"; } + plain(k) { return k * 100; } +} +class Grandchild extends Child { + scale(k) { return super.scale(k) + 1; } +} +class Sibling extends Base { + scale(k) { return -k; } + plain(k) { return super.plain(k) + 0.5; } +} +exports.make = function (i) { + switch (i % 4) { + case 0: return new Base(i); + case 1: return new Child(i); + case 2: return new Grandchild(i); + default: return new Sibling(i); + } +}; +exports.computed = function (value, name, arg) { return value[name](arg); }; +exports.viaCall = function (value, name, arg) { const f = value[name]; return f.call(value, arg); }; +exports.literal = function (value, arg) { return [value["scale"](arg), value["plain"](arg), value["label"]()].join(" "); }; +exports.direct = function (value, arg) { return [value.scale(arg), value.plain(arg), value.label()].join(" "); }; diff --git a/tests/corpus/8302-class-super-method-values/main.ts b/tests/corpus/8302-class-super-method-values/main.ts new file mode 100644 index 000000000..1a7edbb49 --- /dev/null +++ b/tests/corpus/8302-class-super-method-values/main.ts @@ -0,0 +1,26 @@ +// Methods of JavaScript subclasses that call super, read from instances +// through computed keys, literal keys and as values, and called directly. +import { createRequire } from "node:module"; + +interface Lib { + make(i: number): unknown; + computed(value: unknown, name: string, arg: number): unknown; + viaCall(value: unknown, name: string, arg: number): unknown; + literal(value: unknown, arg: number): string; + direct(value: unknown, arg: number): string; +} +const require = createRequire(import.meta.url); +const lib: Lib = require("./lib.cjs"); + +for (let i = 0; i < 8; i++) { + const value = lib.make(i); + console.log( + i, + lib.computed(value, "scale", 3), + lib.computed(value, "plain", 3), + lib.computed(value, "label", 0), + lib.viaCall(value, "scale", 2), + lib.viaCall(value, "label", 0), + ); + console.log(i, lib.literal(value, 5), "|", lib.direct(value, 5)); +} diff --git a/tests/corpus/8302-class-super-method-values/package.json b/tests/corpus/8302-class-super-method-values/package.json new file mode 100644 index 000000000..5ffd9800b --- /dev/null +++ b/tests/corpus/8302-class-super-method-values/package.json @@ -0,0 +1 @@ +{ "type": "module" } diff --git a/tests/corpus/8320-assign-only-deferred-binding/main.ts b/tests/corpus/8320-assign-only-deferred-binding/main.ts new file mode 100644 index 000000000..d35a863ae --- /dev/null +++ b/tests/corpus/8320-assign-only-deferred-binding/main.ts @@ -0,0 +1,8 @@ +import { installCount, useFormat, useScanner } from "./scanner.ts"; + +useScanner((bytes, start) => start + bytes.length * 2); +console.log("scanner installed"); +const formatter = useFormat((value) => `[${value}]`); +console.log(formatter(7)); +useScanner((bytes, start) => start); +console.log("installs", installCount()); diff --git a/tests/corpus/8320-assign-only-deferred-binding/scanner.ts b/tests/corpus/8320-assign-only-deferred-binding/scanner.ts new file mode 100644 index 000000000..601df60ba --- /dev/null +++ b/tests/corpus/8320-assign-only-deferred-binding/scanner.ts @@ -0,0 +1,21 @@ +// Module bindings that only assignments use after their declarations. +export function defaultEnd(bytes: Uint8Array, start: number): number { + return start + bytes.length; +} + +let valueEnd: (bytes: Uint8Array, start: number) => number = defaultEnd; +let format: (value: number) => string = (value) => `#${value}`; +let installs = 0; + +export function useScanner(scan: (bytes: Uint8Array, start: number) => number): void { + valueEnd = (bytes, start) => scan(bytes, start) + 1; + installs++; +} + +export function useFormat(next: (value: number) => string): (value: number) => string { + return (format = next); +} + +export function installCount(): number { + return installs; +} diff --git a/tests/corpus/8321-typed-array-constructor-kind.js b/tests/corpus/8321-typed-array-constructor-kind.js new file mode 100644 index 000000000..277128a93 --- /dev/null +++ b/tests/corpus/8321-typed-array-constructor-kind.js @@ -0,0 +1,23 @@ +// A built-in typed-array constructor set as a view's `constructor` is the +// species of map, filter, slice and subarray when the program never names +// the species symbol. +function kinds(TA, Other) { + const a = new TA([1, 2, 3, 4]); + a.constructor = Other; + const m = a.map((v) => v * 1.5), f = a.filter((v) => v > 1), s = a.slice(1, 3); + console.log(m, f, s, m.constructor === Other, f.constructor === Other, s.constructor === Other); + try { + const sub = a.subarray(0, 2); + console.log(sub, sub.constructor === Other); + } catch (err) { + console.log(err.constructor.name); + } + a.constructor = TA; + console.log(a.map((v) => v / 2).constructor === TA); + a.constructor = {}; + console.log(a.slice().constructor === TA); +} +kinds(Uint8Array, Float64Array); +kinds(Int16Array, Uint8Array); +kinds(Float32Array, Int32Array); +kinds(Float64Array, Uint8ClampedArray); diff --git a/tests/corpus/8326-never-promise-instanceof.ts b/tests/corpus/8326-never-promise-instanceof.ts new file mode 100644 index 000000000..6e01e3510 --- /dev/null +++ b/tests/corpus/8326-never-promise-instanceof.ts @@ -0,0 +1,26 @@ +// `instanceof Promise` on values that are never promises (records, arrays, +// dates, collections, regular expressions, byte arrays, null, and unions of +// them) is false, after the operand is evaluated. +let evaluated = 0; +function next(): { v: number } { + evaluated++; + return { v: evaluated }; +} + +const record = { v: 1 }; +console.log("record", record instanceof Promise); +console.log("call", next() instanceof Promise, evaluated); +console.log("array", [1, 2] instanceof Promise); +console.log("date", new Date(0) instanceof Promise); +console.log("map", new Map() instanceof Promise); +console.log("set", new Set() instanceof Promise); +console.log("regex", /a/ instanceof Promise); +console.log("bytes", new Uint8Array(2) instanceof Promise); + +function pick(i: number): { v: number } | number[] | null { + return i === 0 ? { v: 0 } : i === 1 ? [i] : null; +} +for (let i = 0; i < 3; i++) console.log("union", i, pick(i) instanceof Promise); + +const promised: unknown = Promise.resolve(1); +console.log("promise", promised instanceof Promise); diff --git a/tests/corpus/8330-optional-element-undefined-receivers.ts b/tests/corpus/8330-optional-element-undefined-receivers.ts new file mode 100644 index 000000000..b9433e66c --- /dev/null +++ b/tests/corpus/8330-optional-element-undefined-receivers.ts @@ -0,0 +1,75 @@ +// Optional element access on bindings whose type admits undefined: an +// undefined receiver short-circuits to undefined without evaluating the +// key, and a present receiver reads the element as a plain access does. + +function first(kinds: string[] | undefined, i: number): string { + const kind = kinds?.[i]; + return kind === undefined ? "none" : String(kind); +} +console.log(first(undefined, 0)); +console.log(first(["a", "b"], 1)); +console.log(first(["a"], 5)); + +function orDefault(values: number[] | undefined, i: number): number { + return values?.[i] ?? -1; +} +console.log(orDefault(undefined, 0), orDefault([4, 5], 1), orDefault([4], 3)); + +function isMissing(flags: boolean[] | undefined, i: number): boolean { + return flags?.[i] === undefined; +} +console.log(isMissing(undefined, 0), isMissing([true], 0), isMissing([false], 2)); + +function head(pair: [string, number] | undefined): string { + return pair?.[0] ?? "empty"; +} +console.log(head(undefined), head(["x", 1])); + +function lookup(table: Record | undefined, key: string): number | undefined { + return table?.[key]; +} +console.log(lookup(undefined, "a"), lookup({ a: 1 }, "a"), lookup({ a: 1 }, "b")); + +function width(words: string[] | undefined, i: number): number { + return words?.[i]?.length ?? 0; +} +console.log(width(undefined, 0), width(["abc"], 0), width(["abc"], 1)); + +// The key is not evaluated when the receiver is undefined. +let keys = 0; +function nextKey(): number { + keys++; + return 0; +} +function counted(items: string[] | undefined): string | undefined { + return items?.[nextKey()]; +} +console.log(counted(undefined), keys); +console.log(counted(["k"]), keys); + +// Local bindings, including one reassigned between reads. +function maybe(present: boolean): number[] | undefined { + return present ? [7, 8] : undefined; +} +let current = maybe(false); +console.log(current?.[0]); +current = maybe(true); +console.log(current?.[1]); +current = maybe(false); +console.log(current?.[1]); + +function local(present: boolean): string { + const items: string[] | undefined = present ? ["p", "q"] : undefined; + const second = items?.[1]; + return second ?? "absent"; +} +console.log(local(true), local(false)); + +// A sparse array reads its holes as undefined. +const sparse: number[] = [1]; +sparse[3] = 4; +function at(values: number[] | undefined, i: number): string { + const value = values?.[i]; + return value === undefined ? "hole" : String(value); +} +console.log(at(sparse, 1), at(sparse, 3), at(undefined, 3)); diff --git a/tests/corpus/8331-map-number-hashed-integer-keys.ts b/tests/corpus/8331-map-number-hashed-integer-keys.ts new file mode 100644 index 000000000..8af7293e9 --- /dev/null +++ b/tests/corpus/8331-map-number-hashed-integer-keys.ts @@ -0,0 +1,102 @@ +// Number-keyed Map and Set lookups whose keys are integers the compiler +// tracks as integers (loop counters, literals, integer arithmetic) on +// hashed storage, and lookups of absent keys on small bucket tables. + +// Keys spread past the direct index's range, so the maps hash them. +const spread = new Map(); +for (let i = 0; i < 500; i++) spread.set(i * 104729 - 26000000, i); +let found = 0, + absent = 0, + total = 0; +for (let i = -10; i < 520; i++) { + const v = spread.get(i * 104729 - 26000000); + if (v === undefined) absent++; + else { + found++; + total += v; + } +} +console.log("spread", spread.size, found, absent, total); +console.log("spread literals", spread.get(-26000000), spread.get(-25895271), spread.get(0), spread.has(-1)); + +// Zero stored through -0 and through an integer; read back through both. +const zeros = new Map(); +for (let i = 1; i <= 20; i++) zeros.set(i * 1000003, "k" + i); +zeros.set(-0, "minus zero"); +let z = 0; +console.log("zeros", zeros.size, zeros.get(z), zeros.get(0), zeros.get(-0), zeros.get(z * -1)); +zeros.set(z, "zero"); +console.log("zeros overwrite", zeros.size, zeros.get(-0), [...zeros.keys()].includes(0)); + +// Large and negative integer keys. +const wide = new Map(); +for (let i = 0; i < 64; i++) wide.set(2 ** 40 + i * 3, i); +for (let i = 0; i < 64; i++) wide.set(-(2 ** 33) - i, -i); +let wideUp = 0, + wideDown = 0, + wideMissing = 0; +for (let i = 0; i < 200; i++) { + const a = wide.get(2 ** 40 + i); + const b = wide.get(-(2 ** 33) - i); + if (a === undefined) wideMissing++; + else wideUp += a; + if (b === undefined) wideMissing++; + else wideDown += b; +} +console.log("wide", wide.size, wideUp, wideDown, wideMissing, wide.get(1099511627776), wide.get(-8589934592)); + +// Absent keys on bucket tables just past the linear scan, and as they grow. +for (const size of [9, 12, 16, 33, 64, 100, 257, 400]) { + const m = new Map(); + for (let i = 0; i < size; i++) m.set(i * 7919 + 1000000, i); + let hits = 0, + misses = 0; + for (let i = 0; i < size * 4; i++) { + if (m.has(i * 7919 + 1000000)) hits++; + else misses++; + if (m.get(i * 7919 + 1000001) !== undefined) hits += 1000; + } + for (let i = 0; i < size; i += 2) m.delete(i * 7919 + 1000000); + let after = 0; + for (let i = 0; i < size; i++) if (m.has(i * 7919 + 1000000)) after++; + console.log("size", size, hits, misses, m.size, after, m.get(1000000), m.get(1007919)); +} + +// Small maps that scan their entries, with integer keys. +const tiny = new Map(); +for (let i = 0; i < 8; i++) tiny.set(i * 5000011, "t" + i); +let tinyHits = 0; +for (let i = 0; i < 40; i++) if (tiny.has(i * 5000011)) tinyHits++; +console.log("tiny", tiny.size, tinyHits, tiny.get(0), tiny.get(35000077), tiny.has(5000012)); + +// Sets with integer keys. +const seen = new Set(); +let dupes = 0; +for (let i = 0; i < 3000; i++) { + const k = ((i * 2654435761) % 1000003) - 500000; + if (seen.has(k)) dupes++; + else seen.add(k); +} +let present = 0; +for (let i = -500000; i < 500003; i += 997) if (seen.has(i)) present++; +console.log("set", seen.size, dupes, present, seen.has(-500000), seen.has(500002)); + +// A counting loop like a k-mer histogram: get, then update the found entry. +class Count { + n = 0; +} +const counts = new Map(); +let key = 0; +for (let i = 0; i < 20000; i++) { + key = ((key * 4) % 4194304) + (i % 7 === 0 ? 3 : i % 3); + const c = counts.get(key); + if (c === undefined) { + const fresh = new Count(); + fresh.n = 1; + counts.set(key, fresh); + } else c.n++; +} +let most = 0, + mostKey = -1; +for (const [k, c] of counts) if (c.n > most) [most, mostKey] = [c.n, k]; +console.log("counts", counts.size, most, mostKey); diff --git a/tests/corpus/8332-worker-shared-counted-loops.ts b/tests/corpus/8332-worker-shared-counted-loops.ts new file mode 100644 index 000000000..010531c13 --- /dev/null +++ b/tests/corpus/8332-worker-shared-counted-loops.ts @@ -0,0 +1,128 @@ +// Counted loops over typed arrays backed by a SharedArrayBuffer in a program +// with workers: loops to a length and to a bound, ascending, descending and +// by two, reads and writes at the counter and next to it, nested loops, and +// each element type. The worker and the main thread run the same loops over +// the same shared memory, one after the other. +import { isMainThread, parentPort, Worker, workerData } from "node:worker_threads"; + +interface Shared { + bytes: Uint8Array; + shorts: Int16Array; + words: Uint32Array; + ints: Int32Array; + floats: Float32Array; + doubles: Float64Array; +} + +function codes(a: Uint8Array): void { + for (let i = 0; i < a.length; ++i) { + const x = a[i]; + if (x === 97) a[i] = 0; + else if (x === 116) a[i] = 1; + else if (x === 99) a[i] = 2; + else a[i] = 3; + } +} + +function checksum(a: Uint8Array): number { + let t = 0; + for (let i = 0; i < a.length; i++) t = (t * 31 + a[i]!) % 1000003; + return t; +} + +function shiftLeft(a: Uint8Array): void { + for (let i = 0; i < a.length - 1; i++) a[i] = a[i + 1]!; +} + +function descend(a: Int16Array, n: number): number { + let t = 0; + for (let i = n - 1; i >= 0; i--) { + a[i] = a[i]! - i * 3; + t += a[i]!; + } + return t; +} + +function everyOther(a: Uint32Array): number { + let t = 0; + for (let i = 0; i < a.length; i += 2) { + a[i] = a[i]! * 2 + 4000000000; + t += a[i]! % 1000; + } + return t; +} + +function grid(a: Int32Array, n: number): number { + let t = 0; + for (let i = 0; i < n; i++) for (let j = 0; j < n; j++) t += a[i]! * a[j]! - j; + return t; +} + +function mix(f: Float32Array, d: Float64Array, n: number): number { + for (let i = 0; i < n; i++) { + f[i] = f[i]! * 0.5 + i; + d[i] = d[i]! + f[i]! / 3; + } + let t = 0; + for (let i = 0; i < n; i++) t += d[i]!; + return Math.round(t * 1000) / 1000; +} + +function fill(s: Shared): void { + const letters = [97, 116, 99, 103]; + let seed = 7; + for (let i = 0; i < s.bytes.length; i++) { + seed = (seed * 1103515245 + 12345) % 2147483648; + s.bytes[i] = letters[(seed >> 16) % 4]!; + } + for (let i = 0; i < s.shorts.length; i++) s.shorts[i] = (i * 37) % 2000 - 1000; + for (let i = 0; i < s.words.length; i++) s.words[i] = i * 2654435761; + for (let i = 0; i < s.ints.length; i++) s.ints[i] = (i % 7) - 3; + for (let i = 0; i < s.floats.length; i++) s.floats[i] = i / 4; + for (let i = 0; i < s.doubles.length; i++) s.doubles[i] = i * 1.25; +} + +function run(where: string, s: Shared): string { + codes(s.bytes); + const coded = checksum(s.bytes); + shiftLeft(s.bytes); + const shifted = checksum(s.bytes); + const down = descend(s.shorts, s.shorts.length); + const other = everyOther(s.words); + const cells = grid(s.ints, 40); + const mixed = mix(s.floats, s.doubles, s.floats.length); + return `${where}: ${coded} ${shifted} ${down} ${other} ${cells} ${mixed} ${s.bytes[0]} ${s.words[2]}`; +} + +function views(buffers: SharedArrayBuffer[]): Shared { + return { + bytes: new Uint8Array(buffers[0]!), + shorts: new Int16Array(buffers[1]!), + words: new Uint32Array(buffers[2]!), + ints: new Int32Array(buffers[3]!), + floats: new Float32Array(buffers[4]!), + doubles: new Float64Array(buffers[5]!), + }; +} + +if (isMainThread) { + const buffers = [ + new SharedArrayBuffer(10007), + new SharedArrayBuffer(2 * 515), + new SharedArrayBuffer(4 * 333), + new SharedArrayBuffer(4 * 40), + new SharedArrayBuffer(4 * 129), + new SharedArrayBuffer(8 * 129), + ]; + const shared = views(buffers); + fill(shared); + const worker = new Worker(new URL(import.meta.url), { workerData: buffers }); + worker.on("message", (line: string) => { + console.log(line); + console.log(run("main", shared)); + fill(shared); + console.log(run("main again", shared)); + }); +} else { + parentPort!.postMessage(run("worker", views(workerData as SharedArrayBuffer[]))); +} diff --git a/tests/corpus/8333-array-subclass-view-writes.ts b/tests/corpus/8333-array-subclass-view-writes.ts new file mode 100644 index 000000000..5d03d8423 --- /dev/null +++ b/tests/corpus/8333-array-subclass-view-writes.ts @@ -0,0 +1,27 @@ +// An Array subclass written through an unknown[] view: a number pushed +// through the view, and values its element type cannot hold, which the +// array keeps as written. The instance keeps its class and its fields, and +// the species results of slice and concat keep the written values. +class Scores extends Array { + label = "scores"; +} + +const plain = new Scores(); +plain.push(1, 2); +const plainView: unknown[] = plain; +plainView.push(3); +console.log(plain instanceof Scores, plain.label, plain.length, JSON.stringify(plain)); + +const scores = new Scores(); +scores.push(1, 2); +const view: unknown[] = scores; +view.push("x", { a: 1 }); +console.log(scores instanceof Scores, scores.label, scores.length, JSON.stringify(scores), scores.join("|")); +const tail = scores.slice(1); +console.log(tail instanceof Scores, tail.length, JSON.stringify(tail)); +const joined = scores.concat([9]); +console.log(joined instanceof Scores, joined.length, JSON.stringify(joined)); +scores[2] = 7; +console.log(JSON.stringify(scores), view[3]); +scores.length = 1; +console.log(scores instanceof Scores, scores.label, JSON.stringify(scores), JSON.stringify(tail)); diff --git a/tests/corpus/8334-array-subclass-inspect-depth.ts b/tests/corpus/8334-array-subclass-inspect-depth.ts new file mode 100644 index 000000000..1430ebcfc --- /dev/null +++ b/tests/corpus/8334-array-subclass-inspect-depth.ts @@ -0,0 +1,13 @@ +// util.inspect of Array subclass instances: the `Name(length)` prefix, and +// the `[Name]` placeholder past the depth limit, plain and with colors. +import { inspect } from "node:util"; + +class Scores extends Array {} + +const s = new Scores(); +s.push(1, 2); +console.log(inspect(new Scores()), inspect(s)); +console.log(inspect([s], { depth: 0 }), inspect([[1]], { depth: 0 })); +console.log(JSON.stringify(inspect([s], { depth: 0, colors: true }))); +console.log(JSON.stringify(inspect([[1]], { depth: 0, colors: true }))); +console.log(JSON.stringify(inspect(s, { colors: true }))); diff --git a/tests/corpus/8344-readline-chunk-splitting.ts b/tests/corpus/8344-readline-chunk-splitting.ts new file mode 100644 index 000000000..85eff8cdc --- /dev/null +++ b/tests/corpus/8344-readline-chunk-splitting.ts @@ -0,0 +1,41 @@ +// Line splitting over stream chunks: an unfinished line joined across many +// chunks, a line longer than a chunk, empty lines, LF-only +// chunks next to chunks with CR and CRLF, an ASCII chunk after a chunk that +// ends inside a UTF-8 sequence, and text written with rl.write(). Each line +// event shows the line's length and its first and last characters. +import { createInterface } from "node:readline"; +import { Readable } from "node:stream"; + +function show(tag: string, line: string): void { + console.log(tag, line.length, JSON.stringify(line.slice(0, 12)), JSON.stringify(line.slice(-6))); +} + +function feed(tag: string, chunks: (string | Buffer)[]): Promise { + return new Promise((resolve) => { + const src = new Readable({ read() {} }); + const rl = createInterface({ input: src, crlfDelay: Infinity }); + rl.on("line", (line: string) => show(tag, line)); + rl.on("close", () => { + console.log(tag, "closed"); + resolve(); + }); + for (const chunk of chunks) src.push(typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk); + src.push(null); + }); +} + +const long = "x".repeat(70000) + "y"; +await feed("joined", ["par", "tial ", "line", " continues", "\nnext\n", "tail"]); +await feed("long", [long.slice(0, 30000), long.slice(30000, 65536), long.slice(65536) + "\nshort\n"]); +await feed("empty", ["\n\n", "a\n\n", "\n", "b"]); +await feed("mixed", ["lf only\nstill lf\n", "cr\rcrlf\r\nlf\n", "end\r", "\nafter\r", "\r\n", "done"]); +const euro = Buffer.from("€uro\n", "utf8"); +await feed("utf8", [euro.subarray(0, 2), Buffer.concat([euro.subarray(2), Buffer.from("plain ascii\nmore")])]); +await feed("utf8-lines", ["café one\nnaïve two\n", "ascii three\n", "中文 four"]); + +const writer = createInterface({ input: new Readable({ read() {} }), crlfDelay: Infinity }); +writer.on("line", (line: string) => show("write", line)); +writer.write("first part "); +writer.write("second part\nthird\r\nfourth\rfifth"); +writer.write("\n"); +writer.close(); diff --git a/tests/corpus/8345-replace-callback-untyped-params.ts b/tests/corpus/8345-replace-callback-untyped-params.ts new file mode 100644 index 000000000..18ddb3389 --- /dev/null +++ b/tests/corpus/8345-replace-callback-untyped-params.ts @@ -0,0 +1,90 @@ +// String replace/replaceAll with inline callbacks whose parameters have no +// annotations, so the lib types every parameter after the match `any`. +// Each such parameter holds what Node passes at its position: the captures, +// the offset, the subject, the groups object and then undefined. Captures a +// match can leave unset hold undefined, and a result that is not a string +// converts with ToString. +const vars: Record = { name: "world", place: "earth" }; +const template = "Hello {name}, welcome to {place}! {missing} {name}."; +console.log(template.replace(/\{(\w+)\}/g, (_m, key) => vars[key] ?? `<${key}>`)); +console.log(template.replace(/\{(\w+)\}/, (_m, key) => key.toUpperCase())); +console.log("some_snake_case_name".replace(/_([a-z])/g, (_m, c) => c.toUpperCase())); +console.log("2024-01-15 and 1999-07-04".replace(/(\d{4})-(\d{2})-(\d{2})/g, (_m, y, mo, d) => `${d}/${mo}/${y}`)); +console.log("a1b22".replace(/\d+/g, (m, offset, subject) => `[${m}@${offset}/${subject.length}]`)); +console.log("x1y2".replace(/(\d)/g, (m, digit, offset, subject) => `${m}${digit}${offset}${subject}`)); +console.log("k=v".replace(/(\w)=(\w)/, function (m, k, v, at, all) { + return `${v}=${k}:${at}:${all}:${m}`; +})); + +console.log("k=v".replace(/(\w)=(\w)/, function (this: void, m, k, v, at) { + return `${typeof this}:${v}=${k}:${at}:${m}`; +})); + +// A parameter the body assigns keeps `any`. +console.log("a1".replace(/(\d)/, (_m, d) => { + d = Number(d) + 1; + return String(d * 2); +})); + +// Captures that a match can leave unset. +console.log("ab".replace(/(a)|(b)/g, (m, a, b) => `${m}:${a}:${b};`)); +console.log("b xb".replace(/(x)?b/g, (m, x, at) => `${m}:${x}:${at}`)); +console.log("ab".replace(/((a)|b)/g, (m, outer, inner) => `${outer}:${inner};`)); +console.log("ab".replace(/(?:(a)|b)/g, (m, a, at) => `${m}:${a}:${at};`)); +console.log("aab".replace(/(a){0,2}b/, (m, a, at) => `${m}:${a}:${at}`)); +console.log("ab".replace(/(?=(a))a|b/g, (m, look, at) => `${m}:${look}:${at};`)); +console.log("ab".replace(/(?!(x))a/g, (m, x, at) => `${m}:${x}:${at};`)); +console.log("ab".replace(/(a)*b/, (m, a, at) => `${m}:${a}:${at}`)); +console.log("a(b)".replace(/\((b)\)/, (m, b, at) => `${m}:${b}:${at}`)); +console.log("a|b".replace(/[|(](b)?/, (m, b, at) => `${m}:${b}:${at}`)); + +// Annotated `string | undefined` captures hold undefined when unset. +console.log("2024-01-15 1999-07".replace(/(\d{4})-(\d{2})(-(\d{2}))?/g, (_m, y: string, mo: string, _x: string | undefined, d: string | undefined) => `${d ?? "??"}/${mo}/${y}`)); +console.log("ab".replace(/(a)|(b)/g, (m: string, a: string | undefined, b: string | undefined, at: number) => `${m}:${a ?? "-"}:${b ?? "-"}:${at};`)); +console.log("abc".replace(/(a)(b)(c)/, (m: string, a: string, b: string, c: string, at: number, all: string) => `${c}${b}${a}${m}${at}${all}`)); +console.log("abcd".replace(/(a)(b)(c)(d)/, (m, a, b, c, d, at, all) => `${d}${c}${b}${a}${m}${at}${all}`)); + +// Named groups pass the groups object after the subject; later parameters +// are undefined. +console.log("2024-01".replace(/(?\d{4})-(?\d{2})/, (m, y, mo, at, all, groups) => `${groups.m}.${groups.y}:${y}${mo}:${at}:${all}`)); +console.log("ab".replace(/(?a)|(?b)/g, (m, a, b, at, all, groups) => `${m}:${JSON.stringify(groups)};`)); +console.log("ab".replace(/b/, (m, at, all, extra, more) => `${m}:${at}:${all}:${extra}:${more}`)); +console.log("ab".replace(/(a)(b)/, (m, a, b, at, all, extra, more) => `${m}${a}${b}${at}${all}${extra}${more}`)); + +// An offset passed to parseFloat converts with ToString, and captures +// passed to parseInt and parseFloat are their text. +console.log("a1b22".replace(/\d+/g, (m, at) => `<${parseFloat(at) + parseInt(m, at + 10)}>`)); +console.log("1.5 2e1".replace(/(\S+)/g, (_m, d) => String(parseFloat(d) + parseInt(d) + Number(d)))); + +// Results that are not strings convert with ToString. +const counts: Record = { a: 1, b: true, c: null, d: undefined, e: { toString: () => "E" } }; +console.log("a b c d e".replace(/\w/g, (m) => counts[m])); +console.log("abc".replace(/(\w)/g, (m, c) => counts[c])); + +// String search patterns pass (match, offset, subject). +console.log("one two one".replaceAll("one", (m, at, subject) => `${m.length}@${at}/${subject.length}`)); +console.log("one two one".replace("two", (m, at, subject, extra) => `${m}${at}${subject.length}${extra}`)); + +// A throw stops the replacement. +try { + "a1b2c3".replace(/(\d)/g, (_m, d) => { + if (d === "2") throw new Error("stop at " + d); + return "#"; + }); +} catch (error) { + console.log((error as Error).message); +} +try { + "a1".replace(/(\d)/g, (_m, d) => counts.e.missing.field + d); +} catch (error) { + console.log(error instanceof TypeError); +} +try { + "ab".replace(/(a)|b/g, (m, a) => a.toUpperCase()); +} catch (error) { + console.log(error instanceof TypeError, (error as Error).message); +} + +let total = 0; +for (let i = 0; i < 2000; i++) total += template.replace(/\{(\w+)\}/g, (_m, key) => vars[key] ?? String(i)).length; +console.log(total); diff --git a/tests/corpus/8346-replace-callback-untyped-params.mjs b/tests/corpus/8346-replace-callback-untyped-params.mjs new file mode 100644 index 000000000..2552113f3 --- /dev/null +++ b/tests/corpus/8346-replace-callback-untyped-params.mjs @@ -0,0 +1,32 @@ +// JavaScript string replace/replaceAll with inline callbacks. Each +// parameter holds what Node passes at its position: the captures, the +// offset, the subject, the groups object and then undefined. Captures a +// match can leave unset hold undefined, and a result that is not a string +// converts with ToString. +const vars = { name: "world", place: "earth" }; +const template = "Hello {name}, welcome to {place}! {missing} {name}."; +console.log(template.replace(/\{(\w+)\}/g, (_m, key) => vars[key] ?? `<${key}>`)); +console.log("some_snake_case_name".replace(/_([a-z])/g, (_m, c) => c.toUpperCase())); +console.log("2024-01-15 and 1999-07-04".replace(/(\d{4})-(\d{2})-(\d{2})/g, (_m, y, mo, d) => `${d}/${mo}/${y}`)); +console.log("a1b22".replace(/\d+/g, (m, offset, subject) => `[${m}@${offset}/${subject.length}]`)); +console.log("k=v".replace(/(\w)=(\w)/, function (m, k, v, at, all) { + return `${this === undefined}:${v}=${k}:${at}:${all}:${m}`; +})); +console.log("ab".replace(/(a)|(b)/g, (m, a, b) => `${m}:${a}:${b};`)); +console.log("b xb".replace(/(x)?b/g, (m, x, at) => `${m}:${x}:${at}`)); +console.log("2024-01".replace(/(?\d{4})-(?\d{2})/, (m, y, mo, at, all, groups) => `${groups.m}.${groups.y}:${y}${mo}:${at}:${all}`)); +console.log("ab".replace(/b/, (m, at, all, extra, more) => `${m}:${at}:${all}:${extra}:${more}`)); +const counts = { a: 1, b: true, c: null, d: undefined, e: { toString: () => "E" } }; +console.log("a b c d e".replace(/\w/g, (m) => counts[m])); +console.log("1-2".replace(/(\d)/g, (_m, d) => Number(d) * 10)); +console.log("one two one".replaceAll("one", (m, at, subject) => `${m.length}@${at}/${subject.length}`)); +let seen = ""; +console.log("xyz".replace(/(y)/, (m, y, at, subject) => { + seen = typeof m + typeof y + typeof at + typeof subject; + return y + at; +}), seen); +try { + "ab".replace(/(a)|b/g, (m, a) => a.toUpperCase()); +} catch (error) { + console.log(error instanceof TypeError, error.message); +} diff --git a/tests/corpus/8347-string-iteration-code-points.ts b/tests/corpus/8347-string-iteration-code-points.ts new file mode 100644 index 000000000..3a1286140 --- /dev/null +++ b/tests/corpus/8347-string-iteration-code-points.ts @@ -0,0 +1,96 @@ +// The string iterator: for-of, spread, Array.from, yield* and +// [Symbol.iterator]() walk code points. Astral characters come back whole, +// a lone surrogate is a one-unit character, and halves joined by +// concatenation form one character, as in Node. +function unitCodes(c: string): string { + const codes: string[] = []; + for (let k = 0; k < c.length; k++) codes.push(c.charCodeAt(k).toString(16)); + return `${c.length}:${codes.join(".")}`; +} +const show = (chars: string[]): string => chars.map(unitCodes).join(" "); + +const samples = [ + "", + "abc", + "aé😀b", + "\u0000\u007f\u0080߿ࠀ￿", + "\u{10000}\u{10ffff}", + "é ñ", + "\ud800", + "\udc00", + "x\ud83d", + "\ude00x", + "\udc00\ud800", + "\ud83d" + "\ude00", + "a\ud83d" + "\ude00b\ud83d", + "日本語テキスト", +]; +for (const s of samples) { + const looped: string[] = []; + for (const c of s) looped.push(c); + console.log(JSON.stringify(s), show(looped), show([...s]), show(Array.from(s)), [...s].length, s.length); +} + +// Loop control, heads and bindings. +const text = "a😀b\ud800c"; +let out = ""; +outer: for (const c of text) { + if (c === "b") continue; + for (const d of "xy") { + if (d === "y" && c === "c") continue outer; + out += c + d; + } + if (c === "\ud800") out += "|"; +} +console.log(out); +for (var v of "hé") out += v; +console.log(out); +let last = ""; +for (last of "q😀") out += last.length; +console.log(out, last); +for (const [first] of ["😀z", "ab"].join("")) out += first; +console.log(out); +let source = "abc"; +for (const c of source) { + source += c; + out += source.length; +} +console.log(out, source); +let n = 0; +for (const c of "stop😀here") { + if (c === "😀") break; + n += c.length; +} +console.log(n); + +// Characters as values: lengths, Map keys and code points. +const counts = new Map(); +for (const c of "abracadabra😀😀é") counts.set(c, (counts.get(c) ?? 0) + 1); +console.log([...counts].map(([k, v]) => `${k}=${v}`).join(",")); +let points = 0; +for (const c of "😀é\ud800a") points += c.codePointAt(0)!; +console.log(points); + +// Generators and explicit iterators. +function* letters(s: string): Generator { + yield* s; +} +console.log(show([...letters("x😀\udc00y")])); +const it = "o😀"[Symbol.iterator](); +let step = it.next(); +while (!step.done) { + out += step.value.length; + step = it.next(); +} +console.log(out); + +// A long string built from mixed pieces. +let long = ""; +for (let i = 0; i < 2000; i++) long += i % 3 === 0 ? "a" : i % 3 === 1 ? "é" : "😀"; +let units = 0; +let chars = 0; +for (const c of long) { + units += c.length; + chars++; +} +console.log(units, chars, long.length, [...long].length); diff --git a/tests/corpus/8348-string-iteration-worker-characters.ts b/tests/corpus/8348-string-iteration-worker-characters.ts new file mode 100644 index 000000000..52a38fb78 --- /dev/null +++ b/tests/corpus/8348-string-iteration-worker-characters.ts @@ -0,0 +1,44 @@ +import { Worker, isMainThread, parentPort } from "node:worker_threads"; + +// The one-character strings of U+0080..U+07FF that string iteration returns +// are shared by every thread. A worker that is the first to iterate them +// and exits without sharing anything else leaves them in use: the main +// thread iterates the same characters after the worker's memory goes back. + +const text = "aé ñ ß ü Ω ж א ع ߿"; + +function codes(s: string): string { + const out: string[] = []; + for (const c of s) out.push(`${c.length}:${c.charCodeAt(0).toString(16)}`); + return out.join(" "); +} + +function runWorker(): Promise { + return new Promise((resolve, reject) => { + const worker = new Worker(new URL(import.meta.url)); + let count = -1; + worker.on("message", (value: number) => { + count = value; + }); + worker.on("error", reject); + worker.on("exit", () => resolve(count)); + }); +} + +async function main(): Promise { + console.log("worker", await runWorker()); + // The worker's memory goes back shortly after it exits. + await new Promise((resolve) => setTimeout(resolve, 300)); + console.log("main", codes(text)); + console.log("spread", [...text].join("|")); + console.log("from", Array.from("ñandú").map((c) => c.length).join(",")); +} + +if (isMainThread) { + void main(); +} else { + let count = 0; + for (const c of text) count += c.length; + count += [...text].length + Array.from("ñandú").length; + parentPort!.postMessage(count); +} diff --git a/tests/corpus/8350-worker-shared-loop-bounds.ts b/tests/corpus/8350-worker-shared-loop-bounds.ts new file mode 100644 index 000000000..e5fd1ea9e --- /dev/null +++ b/tests/corpus/8350-worker-shared-loop-bounds.ts @@ -0,0 +1,86 @@ +import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads"; + +// Typed-array loops in a worker program that run to a count passed in, not +// to a view's length, over views of a SharedArrayBuffer, of a thread's own +// buffer, and both in one loop. Counts within the views, past their ends +// (reads give undefined, writes are dropped) and over subarrays at an +// offset. A worker's writes through shared views are visible to the parent. + +interface Job { + shared: SharedArrayBuffer; + n: number; +} + +function rowSums(u: Float64Array, v: Float64Array, n: number, rows: number): void { + for (let i = 0; i < rows; i++) { + let t = 0; + for (let j = 0; j < n; j++) t += u[j]! / (i + j + 1); + v[i] = t; + } +} + +function copyScaled(src: Int32Array, dst: Int32Array, n: number, k: number): number { + let sum = 0; + for (let i = 0; i < n; i++) { + dst[i] = (src[i]! * k) | 0; + sum += dst[i]!; + } + return sum; +} + +function sumTo(view: Float64Array, n: number): number { + let total = 0; + for (let i = 0; i < n; i++) total += view[i]!; + return total; +} + +function fixed(view: Float64Array, count: number): string { + const parts: string[] = []; + for (let i = 0; i < count; i++) parts.push(view[i]!.toFixed(6)); + return parts.join(" "); +} + +function run(label: string, u: Float64Array, v: Float64Array, ints: Int32Array, out: Int32Array, n: number): string { + rowSums(u, v, n, 4); + const scaled = copyScaled(ints, out, n, 3); + return `${label}: rows ${fixed(v, 4)}; scaled ${scaled}; tail ${sumTo(u, n + 3)}`; +} + +function views(buffer: ArrayBufferLike, n: number): { u: Float64Array; v: Float64Array; ints: Int32Array; out: Int32Array } { + const u = new Float64Array(buffer, 0, n); + const v = new Float64Array(buffer, 8 * n, n); + const ints = new Int32Array(buffer, 16 * n, n); + const out = new Int32Array(buffer, 20 * n, n); + for (let i = 0; i < n; i++) { + u[i] = 1 + i * 0.25; + ints[i] = i * 7 - 20; + } + return { u, v, ints, out }; +} + +if (isMainThread) { + const n = 12; + const shared = new SharedArrayBuffer(24 * n); + const own = views(new ArrayBuffer(24 * n), n); + console.log(run("own", own.u, own.v, own.ints, own.out, n)); + const sh = views(shared, n); + console.log(run("shared", sh.u, sh.v, sh.ints, sh.out, n)); + console.log(run("shared, count past the end", sh.u, sh.v, sh.ints, sh.out, n + 5)); + const mixed = new Float64Array(n); + rowSums(sh.u, mixed, n, 4); + console.log("mixed", fixed(mixed, 4)); + console.log("subarray", sumTo(sh.u.subarray(3), n - 3), sumTo(sh.u.subarray(3), n)); + const worker = new Worker(new URL(import.meta.url), { workerData: { shared, n } satisfies Job }); + worker.on("message", (line: string) => { + console.log(line); + console.log("parent sees", sh.out.subarray(0, 5).join(","), sh.v[0]!.toFixed(6)); + }); +} else { + const { shared, n } = workerData as Job; + const u = new Float64Array(shared, 0, n); + const v = new Float64Array(shared, 8 * n, n); + const ints = new Int32Array(shared, 16 * n, n); + const out = new Int32Array(shared, 20 * n, n); + for (let i = 0; i < n; i++) ints[i] = i * 5 + 1; + parentPort!.postMessage(run("worker", u, v, ints, out, n)); +} diff --git a/tests/corpus/8351-regex-literal-replacers.ts b/tests/corpus/8351-regex-literal-replacers.ts new file mode 100644 index 000000000..12968d4d9 --- /dev/null +++ b/tests/corpus/8351-regex-literal-replacers.ts @@ -0,0 +1,72 @@ +// Global substitutions of regex literals with literal replacements: matches +// at every offset around 16-character blocks and at the end of the +// subject, empty matches, alternation order, case-insensitive and +// multiline flags, word boundaries, replacements that are empty, longer +// than the match or hold quotes and backslashes, outputs that grow far past +// the subject, subjects without a match, and subjects outside ASCII (which +// take the general loop). replace and replaceAll. +const substitutions: [string, (s: string) => string][] = [ + ["<[^>]*>", (s) => s.replace(/<[^>]*>/g, "|")], + ["a[NSt]|BY", (s) => s.replace(/a[NSt]|BY/g, "<2>")], + ["tHa[Nt]", (s) => s.replace(/tHa[Nt]/g, "<4>")], + ["\\|[^|][^|]*\\|", (s) => s.replace(/\|[^|][^|]*\|/g, "-")], + ["x*", (s) => s.replace(/x*/g, "-")], + ["\\b", (s) => s.replace(/\b/g, "^")], + ["^ab", (s) => s.replace(/^ab/gm, "Z")], + ["ab|abc|b", (s) => s.replace(/ab|abc|b/g, "#")], + ["AB (i)", (s) => s.replace(/AB/gi, "")], + ["(a)(b)?", (s) => s.replace(/(a)(b)?/g, "q")], + ["\\d+", (s) => s.replace(/\d+/g, '"\\\n')], + ["[^a-z]", (s) => s.replace(/[^a-z]/g, "")], + ["a", (s) => s.replace(/a/g, "<<<>>>")], + ["cat (replaceAll)", (s) => s.replaceAll(/cat/g, "dog")], + ["[aeiou]{2} (replaceAll)", (s) => s.replaceAll(/[aeiou]{2}/g, "~")], +]; + +function hash(text: string, h: number): number { + for (let i = 0; i < text.length; i++) h = (h * 31 + text.charCodeAt(i)) >>> 0; + return h; +} + +const filler = "qwertyuiop asdfghjkl zxcvbnm"; +const needles = ["<2>", "", "aN", "BY", "tHat", "|ab|", "||", "xx", "ab", "abc", "AB", "123", "cat", "aeio", "a"]; +let checksum = 0; +let lines = 0; +for (let length = 0; length <= 70; length += 7) { + for (const needle of needles) { + for (const offset of [0, 1, 14, 15, 16, 17, 31, 32, 33]) { + if (offset + needle.length > length) continue; + let subject = ""; + while (subject.length < length) subject += filler; + subject = subject.slice(0, offset) + needle + subject.slice(offset + needle.length, length); + for (const [name, sub] of substitutions) { + const out = sub(subject); + checksum = hash(out, checksum); + lines++; + if (length === 35 && (offset === 15 || offset === 16)) console.log(name, offset, JSON.stringify(out)); + } + } + } +} +console.log("lines", lines, "checksum", checksum); + +// Dense matches, matches that end the subject, and an output many times +// larger than the subject. +const dense = "<1><22><333>".repeat(40) + "tail"; +for (const [name, sub] of substitutions) console.log(name, JSON.stringify(sub(dense).slice(-40)), sub(dense).length); +const many = "a".repeat(5000); +console.log("grow", many.replace(/a/g, "<<<>>>").length, many.replace(/a/g, "<<<>>>").slice(4990)); +console.log("empty", JSON.stringify("".replace(/x*/g, "-")), JSON.stringify("".replace(/a/g, "b"))); +console.log("none", "zzzz".replace(/a[bc]/g, "!"), "zzzz".replaceAll(/a[bc]/g, "!")); + +// Subjects outside ASCII run the general loop with the same results. +for (const subject of ["héllo wörld ", "日本<語>", "a\u{1F600}b", "cat café cat"]) { + console.log(JSON.stringify(subject.replace(/<[^>]*>/g, "|")), JSON.stringify(subject.replaceAll(/cat/g, "dog")), JSON.stringify(subject.replace(/x*/g, "-"))); +} + +// A long DNA-like subject, as in regex-redux. +let dna = ""; +for (let i = 0; i < 4000; i++) dna += "acgt"[(i * 7 + (i >> 2)) % 4] + (i % 61 === 0 ? "tHaN" : i % 37 === 0 ? "aNBY" : ""); +const step1 = dna.replace(/tHa[Nt]/g, "<4>").replace(/a[NSt]|BY/g, "<2>"); +const step2 = step1.replace(/<[^>]*>/g, "|").replace(/\|[^|][^|]*\|/g, "-"); +console.log("dna", dna.length, step1.length, step2.length, hash(step2, 0)); diff --git a/tests/corpus/8355-row-pair-columns.ts b/tests/corpus/8355-row-pair-columns.ts new file mode 100644 index 000000000..dccd9ca6d --- /dev/null +++ b/tests/corpus/8355-row-pair-columns.ts @@ -0,0 +1,94 @@ +// Row sums whose inner loop runs two columns per step: even, odd, zero, one +// and two columns, column ranges that start above zero or at or past their +// end, fractional, negative, NaN and very large column bounds, a column +// bound read from an array's length, terms that read two elements per +// column, row bounds at and past 2^25, and row and column starts of -0 +// with terms that tell -0 from +0. Every sum adds its columns in order, +// so each has the bits of the loop as written. +function rows(u: Float64Array, v: Float64Array, start: number, end: number, from: number, to: number): void { + for (let i = start; i < end; i++) { + let t = 0; + for (let j = from; j < to; j++) t += u[j]! / ((((i + j) * (i + j + 1)) >>> 1) + i + 1); + v[i] = t; + } +} + +function pairs(u: Float64Array, w: Float32Array, v: Float64Array, end: number): void { + for (let i = 0; i < end; i++) { + let t = -0.25; + for (let j = 0; j < u.length; j++) t += u[j]! * w[j + 1]! - (i + 1) / (j + 3); + v[i] = t; + } +} + +// 1 / (i * 4 - j) is -Infinity at a -0 row and +0 column, and +// 1 / (j - i * 4) at a -0 column and +0 row: the first pass sees the sign. +function signs(v: Float64Array, start: number, end: number, from: number, to: number): void { + for (let i = start; i < end; i++) { + let t = 0; + for (let j = from; j < to; j++) t += 1 / (i * 4 - j) + 1 / (j - i * 4) + 1 / (i * 4 + j + 1); + v[i] = t; + } +} + +const show = (label: string, v: Float64Array): void => { + const parts: string[] = []; + for (const x of v) parts.push(Object.is(x, -0) ? "-0" : x.toPrecision(17)); + console.log(label, parts.join(" ")); +}; + +const n = 9; +const u = new Float64Array(n).map((_, k) => 1 / (k + 1) + k); +for (const [from, to] of [ + [0, n], + [0, n - 1], + [0, 0], + [0, 1], + [0, 2], + [3, n], + [4, n], + [5, 5], + [7, 2], + [0.5, 6], + [0, 6.5], + [-2, 5], + [-0, 4], + [NaN, 4], + [0, NaN], + [0, n + 3], + [2 ** 25, 2 ** 25 + 3], +]) { + const v = new Float64Array(6); + rows(u, v, 0, 6, from!, to!); + show(`columns ${from} ${to}`, v); +} +for (const [start, end] of [ + [2 ** 25 - 3, 2 ** 25 + 1], + [2 ** 25, 2 ** 25 + 2], + [1, 6], +]) { + const v = new Float64Array(8); + rows(u, v, start!, end!, 0, n); + show(`rows ${start} ${end}`, v); +} + +const w = new Float32Array(n + 1).map((_, k) => (k % 4) - 1.5); +for (const end of [0, 1, 4, 5]) { + const v = new Float64Array(5); + pairs(u, w, v, end); + show(`two reads ${end}`, v); +} +const short = new Float64Array(4).map((_, k) => k * 2.5); +const out = new Float64Array(5); +pairs(short, w, out, 5); +show("two reads short", out); +for (const [start, from] of [ + [0, 0], + [-0, 0], + [0, -0], + [-0, -0], +]) { + const v = new Float64Array(4); + signs(v, start!, 4, from!, 5); + show(`signs ${Object.is(start, -0) ? "-0" : start} ${Object.is(from, -0) ? "-0" : from}`, v); +} diff --git a/tests/corpus/8356-shared-view-loop-versions.ts b/tests/corpus/8356-shared-view-loop-versions.ts new file mode 100644 index 000000000..28f4307fb --- /dev/null +++ b/tests/corpus/8356-shared-view-loop-versions.ts @@ -0,0 +1,93 @@ +import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads"; + +// Loops over typed-array views of shared memory in a worker program: row +// sums and element-wise passes whose accesses are in bounds, the same loops +// running past a view's end, a loop that mixes a shared view with one the +// thread owns, and views of one SharedArrayBuffer that overlap, written in +// an order that shows whether each read sees the previous write. The worker +// writes its results into shared memory, which the main thread reads after +// the worker reports. + +interface Job { + sab: SharedArrayBuffer; + n: number; +} + +function rowSums(u: Float64Array, v: Float64Array, start: number, end: number, n: number): void { + for (let i = start; i < end; i++) { + let t = 0; + for (let j = 0; j < n; j++) t = t + u[j]! / (((i + j) * (i + j + 1)) / 2 + i + 1); + v[i] = t; + } +} + +function scaled(src: Float64Array, dst: Float64Array, k: number, count: number): void { + for (let i = 0; i < count; i++) dst[i] = src[i]! * k + 1; +} + +function mixed(shared: Int32Array, owned: Int32Array, count: number): number { + let sum = 0; + for (let i = 0; i < count; i++) { + owned[i] = shared[i]! * 3; + shared[i] = owned[i]! - i; + sum = (sum + shared[i]! + owned[i]!) | 0; + } + return sum; +} + +function shift(a: Float64Array, b: Float64Array, count: number): void { + for (let i = 0; i < count; i++) a[i + 1] = b[i]! + 1; +} + +function checksum(values: Float64Array): string { + let h = 0; + for (let i = 0; i < values.length; i++) h = (Math.imul(h, 31) + Math.round(values[i]! * 1e6)) >>> 0; + return h.toString(16); +} + +function work({ sab, n }: Job): string[] { + const out: string[] = []; + const u = new Float64Array(sab, 0, n); + const v = new Float64Array(sab, n * 8, n); + for (let i = 0; i < n; i++) u[i] = 1 + (i % 5) / 4; + rowSums(u, v, 0, n, n); + out.push(`rows ${checksum(v)}`); + // Past the end of u: the reads beyond it are undefined, so the sums are NaN. + rowSums(u, v, 0, 4, n + 3); + out.push(`past ${v[0]} ${v[3]} ${checksum(v.subarray(4))}`); + scaled(u, v, 0.5, n); + out.push(`scaled ${checksum(v)}`); + scaled(u, v, 2, n + 2); + out.push(`scaled past ${checksum(v)}`); + const ints = new Int32Array(sab, 2 * n * 8, 32); + for (let i = 0; i < 32; i++) ints[i] = i * 7 - 50; + const own = new Int32Array(32); + out.push(`mixed ${mixed(ints, own, 32)} ${mixed(ints, own, 34)} ${ints[5]} ${own[31]}`); + // a[i + 1] lands on b[i + 1] when b starts one element after a. + const a = new Float64Array(sab, 0, 24); + const b = new Float64Array(sab, 8, 24); + for (let i = 0; i < 25; i++) u[i] = i; + shift(a, b, 20); + out.push(`overlap ${Array.from(a.subarray(0, 8)).join(",")}`); + shift(b, a, 20); + out.push(`overlap back ${Array.from(b.subarray(0, 8)).join(",")}`); + return out; +} + +if (isMainThread) { + const n = 40; + const sab = new SharedArrayBuffer(2 * n * 8 + 32 * 4); + const worker = new Worker(new URL(import.meta.url), { workerData: { sab, n } satisfies Job }); + worker.on("message", (lines: string[]) => { + for (const line of lines) console.log("worker", line); + const v = new Float64Array(sab, n * 8, n); + const ints = new Int32Array(sab, 2 * n * 8, 32); + console.log("main sees", checksum(v), ints[0], ints[31]); + // The same passes on the main thread, over a fresh buffer. + const again = new SharedArrayBuffer(2 * n * 8 + 32 * 4); + for (const line of work({ sab: again, n })) console.log("main", line); + }); + worker.on("exit", (code: number) => console.log("exit", code)); +} else { + parentPort!.postMessage(work(workerData as Job)); +} diff --git a/tests/corpus/8360-record-prototype-method-calls.ts b/tests/corpus/8360-record-prototype-method-calls.ts new file mode 100644 index 000000000..980e4b5d2 --- /dev/null +++ b/tests/corpus/8360-record-prototype-method-calls.ts @@ -0,0 +1,56 @@ +// Object.prototype's methods called by name on typed records: the built-in +// with the record as its receiver, or an own entry that shadows it. +interface Point { + x: number; + y: number; +} +const counts: Record = { apple: 2, pear: 0 }; +const point: Point = { x: 1, y: 2 }; +const handlers: Record number> = { double: (n) => n * 2 }; +const own: Record string> = { + toString: () => "own toString", + valueOf: () => "own valueOf", +}; + +function line(...parts: unknown[]): void { + console.log(parts.map(String).join(" | ")); +} + +line("toString", counts.toString(), point.toString(), handlers.toString(), String(counts)); +line("toLocaleString", counts.toLocaleString(), point.toLocaleString()); +line("valueOf", counts.valueOf() === counts, point.valueOf() === point); +line( + "hasOwnProperty", + counts.hasOwnProperty("apple"), + counts.hasOwnProperty("pear"), + counts.hasOwnProperty("plum"), + counts.hasOwnProperty("toString"), + point.hasOwnProperty("x"), + point.hasOwnProperty("z"), + handlers.hasOwnProperty("double"), + handlers.hasOwnProperty("valueOf"), +); +line("propertyIsEnumerable", counts.propertyIsEnumerable("apple"), point.propertyIsEnumerable("toString")); +line("isPrototypeOf", counts.isPrototypeOf(point), point.isPrototypeOf(counts)); + +// Own entries shadow the built-ins; toLocaleString calls the own toString. +line("own", own.toString(), own.toLocaleString(), own.valueOf(), own.hasOwnProperty("toString")); +delete own["toString"]; +line("own deleted", own.toString(), own.toLocaleString()); +own.toString = () => "own again"; +line("own again", own.toString(), `${String(own.toString())}`); + +// Keys added at run time are own keys too. +const later: Record = {}; +for (const word of "toString valueOf plain toString".split(" ")) { + later[word] = (later.hasOwnProperty(word) ? later[word]! : 0) + 1; +} +line("later", JSON.stringify(later), later.hasOwnProperty("toString"), later.hasOwnProperty("constructor")); + +// A null-prototype record has no inherited methods. +const groups = Object.groupBy([1, 2, 3], (n): string => (n % 2 ? "odd" : "even")); +try { + line("groups", groups.toString()); +} catch (e) { + line("groups threw", (e as Error).name + ": " + (e as Error).message); +} diff --git a/tests/corpus/8361-record-function-table-inherited.ts b/tests/corpus/8361-record-function-table-inherited.ts new file mode 100644 index 000000000..eecf74f02 --- /dev/null +++ b/tests/corpus/8361-record-function-table-inherited.ts @@ -0,0 +1,40 @@ +// Records of functions used as dispatch tables meet Object.prototype's method +// names: a keyed read answers the inherited method, bound to the record when +// the read is called at once and a plain function value otherwise. +type Command = (arg: string) => unknown; +const commands: Record = { + echo: (arg) => arg, + shout: (arg) => arg.toUpperCase(), +}; +const inputs = process.argv.length > 99 ? [] : ["echo", "toString", "valueOf", "hasOwnProperty", "isPrototypeOf", "propertyIsEnumerable", "toLocaleString", "shout", "nope"]; +function show(v: unknown): string { + return v === commands ? "" : typeof v + ":" + String(v); +} +for (const name of inputs) { + const handler = commands[name]; + if (!handler) { + console.log(name, "unknown handler"); + continue; + } + console.log(name, typeof handler, "direct", show(commands[name](name === "hasOwnProperty" ? "echo" : "x"))); + console.log(name, "optional", show(commands[name]?.("shout"))); + try { + console.log(name, "detached", show(handler("echo"))); + } catch (e) { + console.log(name, "detached threw", (e as Error).name + ": " + (e as Error).message); + } +} +console.log(typeof commands["toString"], typeof commands["valueOf"], commands["toString"] === undefined, commands["toString"] === commands["toString"]); +console.log(String(commands["toString"]("x")), commands["hasOwnProperty"]("shout"), commands["hasOwnProperty"]("nope")); +const literal: Record = {}; +console.log(Object.keys(literal).length, literal["isPrototypeOf"]?.("x"), typeof literal.toString); + +// A word count whose words are not Object.prototype names keeps `?? 0`. +const freq: Record = {}; +for (const w of "a rose is a rose is a rose".split(" ")) freq[w] = (freq[w] ?? 0) + 1; +console.log(JSON.stringify(freq), freq["tulip"] ?? 0); + +// Read by name without a call: a plain function value. +const counts: Record = { a: 1 }; +const named = counts.toString; +console.log(typeof counts.toString, typeof commands.valueOf, String(named())); diff --git a/tests/corpus/8362-record-inherited-order.ts b/tests/corpus/8362-record-inherited-order.ts new file mode 100644 index 000000000..33a72eb91 --- /dev/null +++ b/tests/corpus/8362-record-inherited-order.ts @@ -0,0 +1,40 @@ +// Object.prototype's methods on typed records keep JavaScript's evaluation +// order and key conversions. + +// A function-table key that reassigns the receiver reads the table +// evaluated before the key. +let table: Record string> = { a: () => "first" }; +const other: Record string> = { a: () => "second" }; +function key(): string { + table = other; + return "a"; +} +console.log(String(table[key()]())); + +// hasOwnProperty and propertyIsEnumerable with a symbol key on a record of +// functions test the symbol key. +const fns: Record number> = { double: (n) => n * 2 }; +const sym = Symbol("s"); +console.log(fns.hasOwnProperty(sym as unknown as string), fns.propertyIsEnumerable(sym as unknown as string)); + +// A call by name reads the method before its arguments run: an argument +// that adds, replaces or deletes the own method does not change the callee. +type Has = Record boolean>; +function add(r: Has): string { + r.hasOwnProperty = () => true; + return "zzz"; +} +function swap(r: Has): string { + r.hasOwnProperty = (key) => key === "b"; + return "a"; +} +function drop(r: Has): string { + delete r["hasOwnProperty"]; + return "zzz"; +} +const added: Has = {}; +console.log(added.hasOwnProperty(add(added)), added.hasOwnProperty("zzz")); +const swapped: Has = { hasOwnProperty: (key) => key === "a" }; +console.log(swapped.hasOwnProperty(swap(swapped)), swapped.hasOwnProperty("a")); +const removed: Has = { hasOwnProperty: () => true }; +console.log(removed.hasOwnProperty(drop(removed)), removed.hasOwnProperty("zzz")); diff --git a/tests/corpus/8400-iterative-teardown.ts b/tests/corpus/8400-iterative-teardown.ts new file mode 100644 index 000000000..69ec13824 --- /dev/null +++ b/tests/corpus/8400-iterative-teardown.ts @@ -0,0 +1,96 @@ +// Teardown of self-recursive records and classes: shared and immortal +// children, deep chains, weak-collection keys, and trees rebuilt from freed +// blocks. + +interface TreeNode { + left: TreeNode | null; + right: TreeNode | null; +} + +class ListNode { + next: ListNode | null; + value: number; + constructor(next: ListNode | null, value: number) { + this.next = next; + this.value = value; + } +} + +class Pair { + a: Pair | null; + b: Pair | null; + constructor(a: Pair | null, b: Pair | null) { + this.a = a; + this.b = b; + } +} + +function build(depth: number): TreeNode { + return depth > 0 ? { left: build(depth - 1), right: build(depth - 1) } : { left: null, right: null }; +} + +function count(node: TreeNode | null): number { + if (node === null) return 0; + return 1 + count(node.left) + count(node.right); +} + +// Trees torn down and rebuilt from their freed blocks. +let total = 0; +for (let depth = 0; depth <= 12; depth++) { + for (let i = 0; i < 4; i++) total += count(build(depth)); +} +console.log("trees", total); + +// A child shared by two parents survives the first parent's teardown. +const shared = build(3); +let left: TreeNode | null = { left: shared, right: build(2) }; +let right: TreeNode | null = { left: build(1), right: shared }; +left = null; +console.log("shared after first parent", count(shared), count(right)); +right = null; +console.log("shared after second parent", count(shared)); + +// A node held from outside keeps its subtree when the tree around it dies. +let outer: TreeNode | null = build(6); +const kept = outer.left!.right!; +outer = null; +console.log("kept subtree", count(kept)); + +// A chain far deeper than any call stack. +let head: ListNode | null = null; +for (let i = 0; i < 2000000; i++) head = new ListNode(head, i); +let sum = 0; +for (let n = head; n !== null; n = n.next) sum += n.value % 7; +head = null; +console.log("chain", sum); + +// A list whose middle node is still referenced keeps the tail from it. +let list: ListNode | null = null; +let middle: ListNode | null = null; +for (let i = 0; i < 1000; i++) { + list = new ListNode(list, i); + if (i === 500) middle = list; +} +list = null; +let tail = 0; +for (let n = middle; n !== null; n = n.next) tail++; +console.log("tail from middle", tail); +middle = null; + +// Nodes used as WeakMap keys: their entries go away when the tree dies. +const weak = new WeakMap(); +let root: Pair | null = new Pair(new Pair(null, null), new Pair(new Pair(null, null), null)); +const probe = root.b!; +weak.set(root, 1); +weak.set(root.a!, 2); +weak.set(probe, 3); +root = null; +console.log("weak probe", weak.get(probe), weak.has(probe)); + +// Immortal leaves shared by many nodes. +const leaf: TreeNode = { left: null, right: null }; +let wide: TreeNode | null = { left: leaf, right: leaf }; +for (let i = 0; i < 100; i++) wide = { left: wide, right: leaf }; +console.log("wide", count(wide)); +wide = null; +console.log("leaf", count(leaf)); diff --git a/tests/corpus/957-builtins-namespace.ts b/tests/corpus/957-builtins-namespace.ts index 4b27bfe79..f6980cda6 100644 --- a/tests/corpus/957-builtins-namespace.ts +++ b/tests/corpus/957-builtins-namespace.ts @@ -17,7 +17,7 @@ function tail(p: string): string { return p.slice(i + 1); } -const scratch = "tmp-957-" + tail(process.argv[1]); +const scratch = "tmp-957-" + tail(process.argv[1]) + "-" + process.pid; if (fs.existsSync(scratch)) { fs.rmSync(scratch + "/a.txt"); fs.rmdirSync(scratch); diff --git a/tests/corpus/992-fs-roundtrip.ts b/tests/corpus/992-fs-roundtrip.ts index a039c936f..92b527fb1 100644 --- a/tests/corpus/992-fs-roundtrip.ts +++ b/tests/corpus/992-fs-roundtrip.ts @@ -32,7 +32,7 @@ function freshDir(dir: string): void { } mkdirSync(dir); } -const scratch = "tmp-992-" + tail(process.argv[1]); +const scratch = "tmp-992-" + tail(process.argv[1]) + "-" + process.pid; freshDir(scratch); const file = scratch + "/notes.txt"; diff --git a/tests/corpus/993-fs-readdir.ts b/tests/corpus/993-fs-readdir.ts index e948d3878..b0409f2ef 100644 --- a/tests/corpus/993-fs-readdir.ts +++ b/tests/corpus/993-fs-readdir.ts @@ -26,7 +26,7 @@ function freshDir(dir: string): void { } mkdirSync(dir); } -const scratch = "tmp-993-" + tail(process.argv[1]); +const scratch = "tmp-993-" + tail(process.argv[1]) + "-" + process.pid; freshDir(scratch); console.log(readdirSync(scratch).length); // fresh dir is empty diff --git a/tests/corpus/994-fs-errors.ts b/tests/corpus/994-fs-errors.ts index 74a1ed74e..d317c1aba 100644 --- a/tests/corpus/994-fs-errors.ts +++ b/tests/corpus/994-fs-errors.ts @@ -30,7 +30,7 @@ function freshDir(dir: string): void { } mkdirSync(dir); } -const scratch = "tmp-994-" + tail(process.argv[1]); +const scratch = "tmp-994-" + tail(process.argv[1]) + "-" + process.pid; freshDir(scratch); let caught = 0; diff --git a/tests/corpus/996-fs-rc-stress.ts b/tests/corpus/996-fs-rc-stress.ts index d6a8f0ba3..063845309 100644 --- a/tests/corpus/996-fs-rc-stress.ts +++ b/tests/corpus/996-fs-rc-stress.ts @@ -28,7 +28,7 @@ function freshDir(dir: string): void { } mkdirSync(dir); } -const scratch = "tmp-996-" + tail(process.argv[1]); +const scratch = "tmp-996-" + tail(process.argv[1]) + "-" + process.pid; freshDir(scratch); // Contents into an array, back through map/filter/join. diff --git a/tests/corpus/997-fs-modules/store.ts b/tests/corpus/997-fs-modules/store.ts index 65e59df1b..1b2a0f1fe 100644 --- a/tests/corpus/997-fs-modules/store.ts +++ b/tests/corpus/997-fs-modules/store.ts @@ -17,7 +17,7 @@ export function scratchDir(tag: string): string { while (i >= 0 && script.charAt(i) !== "/" && script.charAt(i) !== "\\") { i = i - 1; } - const dir = tag + script.slice(i + 1); + const dir = tag + script.slice(i + 1) + "-" + process.pid; if (existsSync(dir)) { for (const name of readdirSync(dir)) { rmSync(dir + "/" + name); diff --git a/tests/corpus/absent-property-reads.mjs b/tests/corpus/absent-property-reads.mjs new file mode 100644 index 000000000..cd568b11c --- /dev/null +++ b/tests/corpus/absent-property-reads.mjs @@ -0,0 +1,87 @@ +// Reads of properties an object lacks, from the same untyped sites many +// times: they answer undefined until the object, its prototype, or the +// prototype link changes, and Object.prototype members still answer. +function read(object) { + return object.flag; +} +function readName(object) { + return object.name; +} +function describe(object) { + return [object.toString === Object.prototype.toString, typeof object.constructor, typeof object.hasOwnProperty].join(); +} +function callIt(object) { + try { + return object.run(); + } catch (e) { + return `${e.name}: ${e.message}`; + } +} + +const seen = []; +const plain = { name: "plain" }; +for (let i = 0; i < 3; i++) seen.push(read(plain)); +plain.flag = "added"; +seen.push(read(plain)); +delete plain.flag; +seen.push(read(plain)); +console.log(seen.join()); + +// A prototype that gains, changes and loses the key. +const base = { kind: "base" }; +const child = Object.create(base); +child.name = "child"; +const fromProto = []; +for (let i = 0; i < 3; i++) fromProto.push(read(child)); +base.flag = "inherited"; +fromProto.push(read(child)); +base.flag = "changed"; +fromProto.push(read(child)); +delete base.flag; +fromProto.push(read(child)); +console.log(fromProto.join()); + +// Objects of one shape with different prototypes, read from one site. +const left = Object.create({ flag: "left" }); +const right = Object.create({ other: 1 }); +left.name = "l"; +right.name = "r"; +const mixed = []; +for (let i = 0; i < 3; i++) mixed.push(read(left), read(right)); +console.log(mixed.join()); + +// Changing the prototype link of an object whose absent read was cached. +const moved = { name: "moved" }; +for (let i = 0; i < 3; i++) read(moved); +Object.setPrototypeOf(moved, { flag: "after setPrototypeOf" }); +console.log(read(moved)); +Object.setPrototypeOf(moved, null); +console.log(read(moved), readName(moved)); + +// A longer prototype chain. +const grand = { flag: "grand" }; +const parent = Object.create(grand); +const deep = Object.create(parent); +console.log(read(deep), read(Object.create(Object.create({})))); + +// Object.prototype members and missing methods still answer as in Node. +const bare = { name: "bare" }; +for (let i = 0; i < 3; i++) describe(bare); +console.log(describe(bare), describe({ other: 2 })); +for (let i = 0; i < 3; i++) callIt(bare); +console.log(callIt(bare)); +bare.run = () => "ran"; +console.log(callIt(bare)); + +// Many shapes through one site. +const shapes = []; +for (let i = 0; i < 12; i++) { + const object = { name: `s${i}` }; + object[`k${i}`] = i; + if (i % 3 === 0) object.flag = i; + shapes.push(object); +} +let total = 0; +for (let round = 0; round < 4; round++) + for (const object of shapes) total += read(object) === undefined ? 1 : 100; +console.log(total, shapes.map(readName).join()); diff --git a/tests/corpus/array-prototype-call-native-arrays.ts b/tests/corpus/array-prototype-call-native-arrays.ts new file mode 100644 index 000000000..d5bfec507 --- /dev/null +++ b/tests/corpus/array-prototype-call-native-arrays.ts @@ -0,0 +1,65 @@ +// Array.prototype methods called with .call on native arrays run the arrays' own +// methods, which mutate the array in place: copyWithin, fill, reverse, +// sort, push, pop, splice, and the read-only join, indexOf and includes. +const optional: (number | undefined)[] = [1, 2, 3, 4]; +Array.prototype.copyWithin.call(optional, 0, 2, 4); +console.log(optional.join(",")); +const nums: number[] = [5, 1, 4, 2, 3]; +Array.prototype.fill.call(nums, 0, 0, 1); +console.log(nums.join(",")); +console.log(Array.prototype.reverse.call(nums) === nums, nums.join(",")); +Array.prototype.sort.call(nums, (a: number, b: number) => a - b); +console.log(nums.join(",")); +console.log(Array.prototype.push.call(nums, 9, 10), nums.join(",")); +console.log(Array.prototype.pop.call(nums), nums.length); +console.log(Array.prototype.splice.call(nums, 1, 2).join(","), nums.join(",")); +console.log(Array.prototype.join.call(nums, "-"), Array.prototype.indexOf.call(nums, 4)); +const words: string[] = ["c", "a", "b"]; +Array.prototype.sort.call(words); +console.log(words.join(","), Array.prototype.includes.call(words, "a")); +const records: { id: number }[] = [{ id: 1 }, { id: 2 }, { id: 3 }]; +Array.prototype.copyWithin.call(records, 1, 0, 1); +console.log(records.map((r) => r.id).join(","), records[0] === records[1]); + +// The callback methods bind `this` to their thisArg, for native arrays +// through .call and for dynamic arrays called directly. +type Scale = { k: number; seen: number[] }; +const scale: Scale = { k: 10, seen: [] }; +const values: number[] = [1, 2, 3, 4]; +Array.prototype.forEach.call(values, function (this: Scale, v: number) { + this.seen.push(v); +}, scale); +console.log(scale.seen.join(",")); +console.log(Array.prototype.map.call(values, function (this: Scale, v: number) { + return v * this.k; +}, scale).join(",")); +console.log(Array.prototype.filter.call(values, function (this: Scale, v: number) { + return v * this.k > 15; +}, scale).join(",")); +console.log( + Array.prototype.some.call(values, function (this: Scale, v: number) { return v === this.k / 5; }, scale), + Array.prototype.every.call(values, function (this: Scale, v: number) { return v < this.k; }, scale), + Array.prototype.find.call(values, function (this: Scale, v: number) { return v * this.k === 30; }, scale), + Array.prototype.findIndex.call(values, function (this: Scale, v: number) { return v * this.k === 30; }, scale), + Array.prototype.findLast.call(values, function (this: Scale, v: number) { return v * this.k < 30; }, scale), + Array.prototype.findLastIndex.call(values, function (this: Scale, v: number) { return v * this.k < 30; }, scale), +); +console.log(Array.prototype.flatMap.call(values, function (this: unknown, v: number) { + return [v, v * (this as Scale).k]; +}, scale).join(",")); +const parsed: any = JSON.parse("[1, 2, 3]"); +const sum = { total: 0 }; +parsed.forEach(function (this: { total: number }, v: number) { + this.total += v; +}, sum); +console.log(sum.total, parsed.map(function (this: Scale, v: number) { return v * this.k; }, scale).join(",")); +class Counter { + total = 0; + addAll(items: number[]): number { + Array.prototype.forEach.call(items, (v: number) => { + this.total += v; + }); + return this.total; + } +} +console.log(new Counter().addAll(values)); diff --git a/tests/corpus/boxed-record-identity.ts b/tests/corpus/boxed-record-identity.ts new file mode 100644 index 000000000..91bb6c13c --- /dev/null +++ b/tests/corpus/boxed-record-identity.ts @@ -0,0 +1,68 @@ +// A native record, array or class instance seen as an untyped value is the +// same object: every box of it is one value, untyped writes and expandos +// show through typed reads and through other boxes, and it keys Map, Set +// and WeakMap entries as itself. + +type Point = { x: number; y: number; tag?: string }; +type Line = { a: Point; b: Point; pts: Point[] }; + +const p: Point = { x: 1, y: 2 }; +const u1: any = p; +const u2: any = p; +console.log("same box", u1 === u2, u1 === (p as unknown)); + +// Expandos show through another box, JSON and Object.keys, and survive the +// call that added them. +u1.extra = 5; +console.log("expando via other box", u2.extra, JSON.stringify(u2), Object.keys(u2).join(",")); +function tag(o: any): void { + o.note = "n"; +} +tag(p); +console.log("expando after call", (u2 as { note: string }).note); + +// Untyped writes reach typed reads, also of optional fields, and deletes. +u1.x = 10; +console.log("typed sees write", p.x); +u1.tag = "t"; +console.log("optional via dyn", p.tag, Object.keys(p).join(",")); +delete u1.tag; +console.log("delete optional", p.tag, "tag" in p, Object.keys(p).join(",")); + +// Nested records and arrays are the typed program's own objects. +const line: Line = { a: { x: 0, y: 0 }, b: { x: 1, y: 1 }, pts: [{ x: 2, y: 2 }] }; +const ul: any = line; +ul.a.x = 7; +ul.pts[0].y = 9; +ul.pts.push({ x: 3, y: 3 }); +console.log("nested", line.a.x, line.pts[0]!.y, line.pts.length, ul.a === line.a, ul.pts === line.pts); + +// Collection keys. +const m = new Map(); +m.set(p, 1); +console.log("map key", m.get(u1), m.has(p as unknown)); +const wm = new WeakMap(); +wm.set(u1, 2); +console.log("weak key", wm.get(p)); +const s = new Set([u1, u2, p]); +console.log("set size", s.size); + +// Class instances and arrays. +class Box { + v = 1; +} +const bx = new Box(); +const ub: any = bx; +ub.extra = 3; +const ub2: any = bx; +console.log("class expando", ub2.extra, ub === ub2, JSON.stringify(bx)); +const arr: number[] = [1, 2]; +const ua: any = arr; +ua.label = "L"; +const ua2: any = arr; +console.log("array expando", ua2.label, ua === ua2); + +// A box cast back to its own type is the original. +const back = u2 as Point; +back.y = 20; +console.log("cast back", back === p, p.y); diff --git a/tests/corpus/checked-union-record-assertions.ts b/tests/corpus/checked-union-record-assertions.ts new file mode 100644 index 000000000..f5d43cee1 --- /dev/null +++ b/tests/corpus/checked-union-record-assertions.ts @@ -0,0 +1,69 @@ +// Assertions whose target no static conversion reaches: a union asserted to +// the one layout some of its arms convert into, and records or arrays whose +// member types the assertion narrows. The value is checked against the +// asserted type; these programs assert truthfully, so they run as in Node. + +type Api = "alpha" | "beta" | (string & {}); + +interface Streams { + stream: (x: number) => string; + fetch?: (x: number) => string; +} + +interface Options { + name: string; + api?: Streams | Partial>; +} + +function isStreams(value: unknown): value is Streams { + return typeof value === "object" && value !== null && "stream" in value && typeof value.stream === "function"; +} + +function run(input: Options): string[] { + const single = input.api && isStreams(input.api) ? input.api : undefined; + const byApi = single || !input.api ? undefined : (input.api as Partial>); + const all = single ? [single] : Object.values(byApi ?? {}).filter((entry) => entry !== undefined); + return all.map((entry) => `${input.name}:${entry.stream(all.length)}`); +} + +const shared: Streams = { stream: (x) => `shared${x}` }; +console.log(run({ name: "single", api: shared })); +console.log(run<"alpha" | "beta">({ name: "map", api: { alpha: { stream: (x) => `alpha${x}` }, beta: shared } })); +console.log(run<"alpha">({ name: "none" })); + +type KnownKind = "alpha" | "beta" | "gamma"; + +interface AlphaOpts { + fast?: boolean; +} +interface BetaOpts { + depth?: number; +} + +interface Item { + id: string; + kind: TKind; + opts?: TKind extends "alpha" ? AlphaOpts : TKind extends "beta" ? BetaOpts : never; +} + +interface Other { + id: string; + kind: string; + size: number; +} + +const alpha: Item<"alpha"> = { id: "a1", kind: "alpha", opts: { fast: true } }; +const gamma: Item<"gamma"> = { id: "g1", kind: "gamma" }; +const other: Other = { id: "o1", kind: "other", size: 2 }; + +const everything: (Item | Other)[] = [alpha, gamma, other, { id: "g2", kind: "gamma" }]; +const gammas = everything.filter((entry) => entry.kind === "gamma") as Item<"gamma">[]; +console.log(gammas.length, gammas.map((entry) => `${entry.id}:${entry.opts === undefined}`).join(",")); + +const items: Item[] = [gamma, alpha]; +const first = items[0]! as Item<"gamma">; +console.log(first.id, first.kind, first.opts === undefined); + +const stored: { list: (Item | Other)[] } = { list: [gamma] }; +const restored = stored.list as Item<"gamma">[]; +console.log(JSON.stringify(restored)); diff --git a/tests/corpus/class-abort-controller-fields.ts b/tests/corpus/class-abort-controller-fields.ts new file mode 100644 index 000000000..b82f11a0a --- /dev/null +++ b/tests/corpus/class-abort-controller-fields.ts @@ -0,0 +1,52 @@ +// Class fields typed AbortController and AbortSignal (plain, optional, +// `| undefined`, `| null`, in arrays) hold the live handles: aborting +// through the field is seen through every other reference, and optional +// calls on a nullable field skip an empty slot. +class Editor { + private abort: AbortController | undefined; + private signal: AbortSignal | null = null; + private readonly lifetime = new AbortController(); + history: AbortController[] = []; + last?: AbortSignal; + start(): AbortSignal { + this.abort?.abort(); + this.abort = new AbortController(); + this.history.push(this.abort); + this.signal = this.abort.signal; + this.last = this.signal; + return this.signal; + } + cancel(reason?: string): boolean { + this.abort?.abort(reason); + return this.signal?.aborted ?? false; + } + reason(): unknown { + return this.signal?.reason; + } + dispose(): boolean { + this.lifetime.abort(); + return this.lifetime.signal.aborted; + } +} + +const idle = new Editor(); +console.log(idle.cancel(), idle.reason()); + +const e = new Editor(); +const first = e.start(); +let events = 0; +first.addEventListener("abort", () => { + events++; +}); +const second = e.start(); +console.log(first.aborted, second.aborted, events, e.history.length); +console.log(e.cancel("stop"), e.reason(), second.aborted, second.reason); +console.log(e.history[0]!.signal === first, e.last === second, e.dispose()); + +// A nullable local takes the same optional call. +let maybe: AbortController | undefined = e.history.length > 5 ? new AbortController() : undefined; +maybe?.abort(); +console.log(maybe?.signal.aborted); +maybe = new AbortController(); +maybe?.abort("late"); +console.log(maybe?.signal.aborted, maybe?.signal.reason); diff --git a/tests/corpus/class-brand-probe-keys.ts b/tests/corpus/class-brand-probe-keys.ts new file mode 100644 index 000000000..77b5ff138 --- /dev/null +++ b/tests/corpus/class-brand-probe-keys.ts @@ -0,0 +1,68 @@ +// Untyped reads of class instances dispatch on the capsule's class key. +// These classes have keys of equal length, keys that share their last +// eight bytes, and short keys, so every stage of the brand probe decides. +class FooHandlerA { + kind = "foo-a"; + label(): string { + return `FooHandlerA:${this.kind}`; + } +} +class BarHandlerA { + kind = "bar-a"; + label(): string { + return `BarHandlerA:${this.kind}`; + } +} +class FooHandlerB extends FooHandlerA { + override kind = "foo-b"; + extra = 2; +} +class Q { + kind = "q"; + label(): string { + return "Q"; + } +} +class R { + kind = "r"; + label(): string { + return "R"; + } +} +class LongerClassNameWithSharedTail { + kind = "long"; + label(): string { + return "long"; + } +} +class OtherClassNameWithSharedTail { + kind = "other"; + label(): string { + return "other"; + } +} + +const values: any[] = [ + new FooHandlerA(), + new BarHandlerA(), + new FooHandlerB(), + new Q(), + new R(), + new LongerClassNameWithSharedTail(), + new OtherClassNameWithSharedTail(), + { kind: "plain", label: () => "plain" }, +]; + +for (const value of values) { + const extra = value.extra; + console.log(value.kind, value.label(), extra === undefined ? "-" : extra, value.constructor.name); +} + +// Writes through the untyped view reach the right instance. +for (const value of values) value.kind = `${value.kind}!`; +console.log(values.map((value) => value.kind).join(",")); + +// Computed reads go through the same tables. +const key: string = ["kind", "label"][values.length % 2]; +console.log(values.map((value) => typeof value[key]).join(",")); +console.log(values.filter((value) => value instanceof FooHandlerA).length); diff --git a/tests/corpus/constant-choice-chains.ts b/tests/corpus/constant-choice-chains.ts new file mode 100644 index 000000000..02757d3e5 --- /dev/null +++ b/tests/corpus/constant-choice-chains.ts @@ -0,0 +1,128 @@ +// if/else-if chains that store one of several literals to one typed-array +// element, chosen by comparing one number with integer literals: the +// subject's values that no case names, repeated and negative cases, +// results that do not fit the element, and chains that keep their arms. + +function bases(arr: Uint8Array): void { + for (let i = 0; i < arr.length; ++i) { + const x = arr[i]; + if (x === 97) arr[i] = 0; + else if (x === 116) arr[i] = 1; + else if (x === 99) arr[i] = 2; + else arr[i] = 3; + } +} + +const sequence = new Uint8Array(64); +for (let i = 0; i < sequence.length; i++) sequence[i] = "actgACTGnx"[i % 10]!.charCodeAt(0); +bases(sequence); +console.log("bases", sequence.join("")); + +// A number subject: NaN, -0, fractions, infinities and values far outside the cases. +const subjects = [0, -0, 1, 2, 3, 4, -1, 0.5, 2.0000001, NaN, Infinity, -Infinity, 2 ** 40, -(2 ** 40), 1e300]; +function classify(out: Int16Array, at: number, v: number): void { + if (v === 0) out[at] = 10; + else if (v === 1) out[at] = -20; + else if (v === 2) out[at] = 30; + else if (v === 1) out[at] = 99; // never reached + else if (v === -1) out[at] = 40; + else out[at] = -50; +} +const classes = new Int16Array(subjects.length); +for (let i = 0; i < subjects.length; i++) classify(classes, i, subjects[i]!); +console.log("classes", classes.join(" ")); + +// The literal on the left, and results the element converts: 300 wraps in a +// Uint8Array, -1 is 255, 1.5 truncates, and a clamped array rounds and clamps. +function convert(u8: Uint8Array, c: Uint8ClampedArray, f: Float64Array, i: Int8Array, v: number): void { + if (5 === v) u8[0] = 300; + else if (6 === v) u8[0] = -1; + else if (7 === v) u8[0] = 1.5; + else u8[0] = 8589934599; // 2 ** 33 + 7 + if (v === 5) c[0] = 300; + else if (v === 6) c[0] = -1; + else if (v === 7) c[0] = 2.5; + else c[0] = 3.5; + if (v === 5) f[0] = 0.25; + else if (v === 6) f[0] = -0; + else if (v === 7) f[0] = 1e300; + else f[0] = NaN; + if (v === 5) i[0] = 128; + else if (v === 6) i[0] = -129; + else if (v === 7) i[0] = -128; + else i[0] = 127; +} +for (const v of [5, 6, 7, 8, NaN]) { + const u8 = new Uint8Array(1); + const c = new Uint8ClampedArray(1); + const f = new Float64Array(1); + const i = new Int8Array(1); + convert(u8, c, f, i, v); + console.log("convert", v, u8[0], c[0], Object.is(f[0], -0) ? "-0" : f[0], i[0]); +} + +// An index past the end ignores the store, whatever the chain chooses. +const short = new Uint8Array(2); +for (let k = 0; k < 4; k++) { + const v = k + 1; + if (v === 1) short[k] = 11; + else if (v === 2) short[k] = 22; + else if (v === 3) short[k] = 33; + else short[k] = 44; +} +console.log("short", short.join(",")); + +// The index is the compared local. Inside each arm the range analysis +// proves its value there; on the final else's path it is past the end, also +// past 2^32 and below -2^32, and the store is ignored. +function indexed(flag: boolean): string { + const a = new Uint8Array(4); + let j = 0; + if (flag) j = 4294967297; + if (j === 0) a[j] = 11; + else if (j === 1) a[j] = 22; + else if (j === 2) a[j] = 33; + else a[j] = 44; + return a.join(","); +} +function indexedLoop(): string { + const a = new Uint8Array(8); + for (let k = 0; k < 4; k++) { + let j = 1; + if (k === 3) j = 5; + else if (k === 2) j = 4294967298; + else if (k === 1) j = -4294967294; + if (j === 1) a[j] = 11; + else if (j === 2) a[j] = 22; + else if (j === 3) a[j] = 33; + else a[j] = 44; + } + return a.join(","); +} +console.log("indexed", indexed(false), indexed(true), indexedLoop()); + +// Chains that keep their arms: an arm with a second statement, no final +// else, a different index, and a subject that is not a local. +let calls = 0; +function count(): number { + calls++; + return 9; +} +const kept = new Uint8Array(8); +for (let k = 0; k < 8; k++) { + const v = k % 5; + if (v === 0) kept[k] = 1; + else if (v === 1) { + kept[k] = 2; + calls += 100; + } else if (v === 2) kept[k] = 3; + else kept[k] = 4; + if (v === 0) kept[0] += 1; + else if (v === 1) kept[0] += 2; + else if (v === 2) kept[0] += 3; + if (count() === v) kept[k] = 7; + else if (v === 3) kept[(k + 1) % 8] = 5; + else if (v === 4) kept[k] = 6; + else kept[k] = kept[k]!; +} +console.log("kept", kept.join(","), calls); diff --git a/tests/corpus/dyn-cast-identity-fresh.ts b/tests/corpus/dyn-cast-identity-fresh.ts new file mode 100644 index 000000000..f0ef0fd53 --- /dev/null +++ b/tests/corpus/dyn-cast-identity-fresh.ts @@ -0,0 +1,21 @@ +// Casts of values nothing else holds yet: a parse result and a response +// body. They convert outright; the results are ordinary records. + +type Point = { x: number; y: number }; +type Doc = { title: string; points: Point[] }; + +const parsed = JSON.parse('{"title":"t","points":[{"x":1,"y":2},{"x":3,"y":4}]}') as Doc; +parsed.points[0]!.x = 10; +parsed.points.push({ x: 5, y: 6 }); +console.log("parse", parsed.title, parsed.points.length, JSON.stringify(parsed.points[0])); + +const again = JSON.parse('{"x":1,"y":2}') as Point; +const other = JSON.parse('{"x":1,"y":2}') as Point; +console.log("separate parses", again === other, again.x === other.x); + +async function main(): Promise { + const body = (await new Response('{"title":"r","points":[{"x":7,"y":8}]}').json()) as Doc; + body.points[0]!.y = 80; + console.log("response", body.title, JSON.stringify(body.points)); +} +main(); diff --git a/tests/corpus/dyn-cast-identity-holders.ts b/tests/corpus/dyn-cast-identity-holders.ts new file mode 100644 index 000000000..55b311462 --- /dev/null +++ b/tests/corpus/dyn-cast-identity-holders.ts @@ -0,0 +1,62 @@ +// Checked casts of dynamic values held by parameters, fields, closures and +// containers keep one identity with the holder's value. + +type Point = { x: number; y: number }; + +// A parameter: the caller's value and the cast result are one object. +function asPoint(value: unknown): Point { + return value as Point; +} +const shared: unknown = JSON.parse('{"x":1,"y":2}'); +const fromParam = asPoint(shared); +fromParam.x = 5; +console.log("parameter", (shared as Point).x, asPoint(shared) === fromParam); + +// A field of a class instance. +class Holder { + data: unknown; + constructor(data: unknown) { + this.data = data; + } +} +const holder = new Holder(JSON.parse('{"x":3,"y":4}')); +const fromField = holder.data as Point; +fromField.y = 40; +console.log("field", JSON.stringify(holder.data), (holder.data as Point) === fromField); + +// A value captured by a closure. +const captured: unknown = JSON.parse('{"x":0,"y":0}'); +const bump = (): void => { + (captured as Point).x++; +}; +bump(); +bump(); +console.log("closure", (captured as Point).x); + +// Elements of an unknown[] container. +const list: unknown[] = [JSON.parse('{"x":1,"y":1}'), JSON.parse('{"x":2,"y":2}')]; +const second = list[1] as Point; +second.x = 20; +console.log("container", JSON.stringify(list), (list[1] as Point) === second); + +// A union target with a record arm. +const maybe: unknown = JSON.parse('{"x":7,"y":8}'); +const viaUnion = maybe as Point | undefined; +if (viaUnion) viaUnion.x = 70; +console.log("union", (maybe as Point).x); + +// A record whose field is an array, mutated through both casts. +type Bag = { items: number[] }; +const bagRaw: unknown = JSON.parse('{"items":[1]}'); +const bag = bagRaw as Bag; +const items = (bagRaw as { items: unknown }).items as number[]; +items.push(2); +bag.items.push(3); +console.log("record array", bag.items.join(","), items.length, bag.items === items); + +// A native value boxed as unknown and cast back to its own type. +const nativeList: number[] = [1, 2]; +const boxedList: unknown = nativeList; +const backList = boxedList as number[]; +backList.push(3); +console.log("native array", nativeList.length, backList === nativeList); diff --git a/tests/corpus/dyn-cast-identity-workers.ts b/tests/corpus/dyn-cast-identity-workers.ts new file mode 100644 index 000000000..d9562e763 --- /dev/null +++ b/tests/corpus/dyn-cast-identity-workers.ts @@ -0,0 +1,30 @@ +// Structured clones a worker receives: workerData and message payloads. +// Two casts of workerData are one object; a message payload converts once. +import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads"; + +type Job = { name: string; point: { x: number; y: number }; tags: string[] }; + +if (isMainThread) { + const worker = new Worker(new URL(import.meta.url), { + workerData: { name: "job", point: { x: 1, y: 2 }, tags: ["a"] }, + }); + worker.on("message", (message: unknown) => { + if (typeof message === "string") { + console.log(message); + return; + } + const reply = message as Job; + reply.tags.push("main"); + console.log("reply", reply.name, reply.point.x, reply.tags.join(",")); + }); +} else { + const job = workerData as Job; + const same = workerData as Job; + job.point.x = 10; + same.tags.push("worker"); + // Node forwards a worker's stdout to the parent asynchronously, so the + // worker's line goes through the port: messages on one port keep their + // order, and the line prints before the reply. + parentPort!.postMessage(`worker ${same.point.x} ${job.tags.join(",")} ${job === same}`); + parentPort!.postMessage(job); +} diff --git a/tests/corpus/dyn-cast-identity.ts b/tests/corpus/dyn-cast-identity.ts new file mode 100644 index 000000000..5ac30c52f --- /dev/null +++ b/tests/corpus/dyn-cast-identity.ts @@ -0,0 +1,71 @@ +// A checked cast of a dynamic value to a record or array (`value as T`) +// keeps the value's identity: every cast of the same source, and the source +// itself, read and write one object. + +type Point = { x: number; y: number }; + +// Two casts of one parsed object alias each other and the source. +const raw: unknown = JSON.parse('{"x":1,"y":2}'); +const a = raw as Point; +const b = raw as Point; +a.x = 10; +b.y = 20; +console.log("aliasing", a.x, a.y, b.x, b.y); +console.log("identity", a === b, (raw as Point) === a); +console.log("source", JSON.stringify(raw)); + +// Nested objects keep their identity through the outer and inner casts. +type Outer = { name: string; inner: { v: number } }; +const nestedRaw: unknown = JSON.parse('{"name":"o","inner":{"v":1}}'); +const outer = nestedRaw as Outer; +const inner = (nestedRaw as { inner: unknown }).inner as { v: number }; +inner.v = 2; +console.log("nested", outer.inner.v, outer.inner === inner); +outer.inner.v = 3; +console.log("nested back", inner.v); + +// Arrays: pushes and element writes show through every cast. +const listRaw: unknown = JSON.parse("[1,2]"); +const first = listRaw as number[]; +const second = listRaw as number[]; +first.push(3); +second[0] = 9; +console.log("array", first.length, second.length, first[0], first === second, JSON.stringify(listRaw)); + +// Arrays of records. +type Item = { id: number; tags: string[] }; +const itemsRaw: unknown = JSON.parse('[{"id":1,"tags":["a"]},{"id":2,"tags":[]}]'); +const items = itemsRaw as Item[]; +const again = itemsRaw as Item[]; +items[0]!.tags.push("b"); +again[1]!.id = 7; +console.log("array of records", again[0]!.tags.join(","), items[1]!.id, items[0] === again[0]); + +// The source stays an ordinary object for untyped reads and writes. +const bagRaw: unknown = JSON.parse('{"y":2,"x":1,"extra":"e"}'); +const point = bagRaw as Point; +const bag = bagRaw as Record; +bag.z = 3; +console.log("added", JSON.stringify(bagRaw), Object.keys(bag).join(",")); +delete bag.extra; +console.log("deleted", JSON.stringify(bagRaw), JSON.stringify(point)); +for (const k in bag) console.log("key", k, bag[k]); +point.x = 5; +console.log("typed write", bag.x); + +// A native record boxed as unknown comes back as itself. +const native: Point = { x: 1, y: 2 }; +const boxed: unknown = native; +const unboxed = boxed as Point; +unboxed.x = 10; +console.log("native", native.x, unboxed === native); + +// A fresh parse result has no other holder. +const fresh = JSON.parse('{"x":4,"y":5}') as Point; +console.log("fresh", fresh.x + fresh.y); + +// A frozen source keeps working as a copy. +const frozen: unknown = Object.freeze(JSON.parse('{"x":1,"y":2}')); +const held = [frozen]; +const thawed = frozen as Point; +console.log("frozen", thawed.x, held.length, Object.isFrozen(frozen)); diff --git a/tests/corpus/dyn-literal-identity.ts b/tests/corpus/dyn-literal-identity.ts new file mode 100644 index 000000000..915b8f73e --- /dev/null +++ b/tests/corpus/dyn-literal-identity.ts @@ -0,0 +1,83 @@ +// Object and array literals in untyped contexts (an `unknown` or `any` +// slot, an `unknown[]` element, a thrown value, an `unknown`/`any` +// parameter) keep one identity through later checked casts. + +type Counter = { count: number }; +type Item = { name: string; tags: string[] }; + +// The original example: a write through the cast shows through the holder. +const holder: any = { count: 1 }; +const item = holder as Counter; +item.count++; +console.log("any holder", holder.count, item === holder); + +const box: unknown = { count: 10 }; +const viewA = box as Counter; +const viewB = box as Counter; +viewA.count += 5; +console.log("unknown holder", viewB.count, viewA === viewB, JSON.stringify(box)); + +// Elements of an unknown[] array literal. +const values: unknown[] = [ + { name: "a", tags: ["x"] }, + { name: "b", tags: [] }, +]; +const second = values[1] as Item; +second.tags.push("y"); +console.log("unknown[] element", JSON.stringify(values), (values[1] as Item) === second); + +// Nested literals inside an untyped literal. +const tree: unknown = { name: "root", children: [{ name: "leaf", tags: ["t"] }] }; +const root = tree as { name: string; children: Item[] }; +const leaf = (tree as { children: unknown[] }).children[0] as Item; +leaf.tags.push("u"); +console.log("nested", root.children[0]!.tags.join(","), root.children[0] === leaf); + +// A thrown literal. +try { + throw { code: "E_BUSY", attempts: [1] }; +} catch (error) { + const failure = error as { code: string; attempts: number[] }; + failure.attempts.push(2); + console.log("thrown", failure.code, (error as { attempts: number[] }).attempts.length, failure === error); +} + +// Literals passed to unknown and any parameters. +function remember(value: unknown): Counter { + const counter = value as Counter; + counter.count *= 2; + return value as Counter; +} +console.log("unknown parameter", remember({ count: 4 }).count); + +function relabel(value: any): Item { + const target = value as Item; + target.name = target.name.toUpperCase(); + return value; +} +const relabeled = relabel({ name: "pkg", tags: [] }); +console.log("any parameter", relabeled.name, relabeled.tags.length); + +// A literal returned as unknown, and one stored in an unknown field. +function make(): unknown { + return { count: 0 }; +} +const made = make(); +(made as Counter).count = 7; +console.log("returned", (made as Counter).count); + +class Slot { + value: unknown = { name: "slot", tags: ["s"] }; +} +const slot = new Slot(); +(slot.value as Item).tags.push("t"); +console.log("field", JSON.stringify(slot.value)); + +// Untyped literals keep JavaScript's own key order and extra keys. +const extra: unknown = { name: "e", z: 1, tags: [], a: 2 }; +const typed = extra as Item; +typed.tags.push("k"); +console.log("extra keys", JSON.stringify(extra), Object.keys(typed).join(",")); + +// Literals handed to library calls are unchanged. +console.log(JSON.stringify({ count: 3 }), [{ n: 1 }].length); diff --git a/tests/corpus/dyn-union-function-arms.ts b/tests/corpus/dyn-union-function-arms.ts new file mode 100644 index 000000000..e85cc2e6b --- /dev/null +++ b/tests/corpus/dyn-union-function-arms.ts @@ -0,0 +1,48 @@ +// A union of a function type with a value the checked-dynamic tree holds +// (AbortSignal, `unknown`) rides that representation: the function is held +// by reference, `typeof x === "function"` narrows it back out, and calls, +// identity and the other arm's members behave as in Node. + +type Progress = (loaded: number, total: number) => void; + +function listAll( + dirOrProgress?: string | Progress, + progressOrSignal?: Progress | AbortSignal, + signal?: AbortSignal, +): string { + const dir = typeof dirOrProgress === "string" ? dirOrProgress : "/default"; + const progress = + typeof dirOrProgress === "function" + ? dirOrProgress + : typeof progressOrSignal === "function" + ? progressOrSignal + : undefined; + const abortSignal = + typeof dirOrProgress === "string" || typeof progressOrSignal === "function" + ? signal + : (progressOrSignal ?? signal); + progress?.(1, 3); + progress?.(3, 3); + return `${dir} aborted=${abortSignal?.aborted ?? "none"}`; +} + +const seen: string[] = []; +const report: Progress = (loaded, total) => seen.push(`${loaded}/${total}`); +const controller = new AbortController(); +console.log(listAll()); +console.log(listAll("/x", report)); +console.log(listAll(report, controller.signal)); +controller.abort(); +console.log(listAll("/y", undefined, controller.signal)); +console.log(listAll(report, controller.signal), seen.join(" ")); + +function same(handler: Progress | AbortSignal | undefined): boolean { + return handler === report; +} +console.log(same(report), same(controller.signal), same(undefined), same((a, b) => void (a + b))); + +function describe(value: unknown | ((n: number) => string)): string { + if (typeof value === "function") return `function -> ${(value as (n: number) => string)(7)}`; + return `value ${JSON.stringify(value)}`; +} +console.log(describe((n: number) => `n=${n}`), describe({ a: 1 }), describe("s")); diff --git a/tests/corpus/empty-mapped-alias-types.ts b/tests/corpus/empty-mapped-alias-types.ts new file mode 100644 index 000000000..43f3ed831 --- /dev/null +++ b/tests/corpus/empty-mapped-alias-types.ts @@ -0,0 +1,33 @@ +// Mapped types instantiated over concrete arguments whose key filter keeps +// no key (a per-kind model catalog with no models of that kind, +// `Record`, `Partial<{}>`) are the empty object type: values flow +// through them like any `{}`-typed value. +type Groups = Record>; +type Key = { [A in keyof G]: keyof G[A] }[keyof G] & string; +type KeyOf = Extract, `${T}:${string}`>; +type Id = K extends `${string}:${infer M}` ? M : never; +type Catalog = { + [K in KeyOf as Id]: { id: Id; name: string }; +}; + +const data = { api: { "chat:a": { id: "a", name: "A" }, "chat:b": { id: "b", name: "B" } } }; + +const chat: Catalog = { a: { id: "a", name: "A" }, b: { id: "b", name: data.api["chat:b"].name } }; +const image: Catalog = {}; +const video: Catalog = image; +console.log(chat.a.name, chat.b.id, JSON.stringify(image), video === image); + +const none: Record = {}; +const partial: Partial<{}> = { extra: 1 } as {}; +console.log(JSON.stringify(none), JSON.stringify(partial)); + +function describe(value: Catalog): string { + return JSON.stringify(value); +} +const catalogs: Catalog[] = [image, {}, none]; +console.log(describe(image), describe({}), catalogs.map(describe).join(" ")); + +// A module-level alias without type arguments whose filter keeps no key. +type Flags = { [K in "a" | "b" as K extends "c" ? K : never]: boolean }; +const flags: Flags = {}; +console.log(JSON.stringify(flags), JSON.stringify([flags, none])); diff --git a/tests/corpus/evolving-let-shorthand-reads.js b/tests/corpus/evolving-let-shorthand-reads.js new file mode 100644 index 000000000..201171bcc --- /dev/null +++ b/tests/corpus/evolving-let-shorthand-reads.js @@ -0,0 +1,41 @@ +// An initializer-less JavaScript `let` read through a shorthand property +// (`{ width }`) still reads undefined on the paths that never assign it, as +// npm packages do when they build an options or size object. +function size(tty) { + let width; + if (tty) width = 80; + return { width }; +} + +/** + * @param {boolean} tty + * @returns {{ width: number | undefined }} + */ +function documented(tty) { + /** @type {number | undefined} */ + let width; + if (tty) width = 80; + return { width }; +} + +function captured(tty) { + let width; + const read = () => ({ width }); + const before = read().width; + if (tty) width = 80; + return [before, read().width]; +} + +function nested(flags) { + let columns; + let rows; + if (flags.columns) columns = 120; + if (flags.rows) rows = 40; + return { size: { columns, rows } }; +} + +console.log(size(true).width, size(false).width); +console.log(documented(true).width, documented(false).width); +console.log(JSON.stringify(size(false)), "width" in size(false)); +console.log(captured(true), captured(false)); +console.log(nested({ columns: true }), nested({ rows: true }), nested({})); diff --git a/tests/corpus/fetch-headers-copy-cache.ts b/tests/corpus/fetch-headers-copy-cache.ts new file mode 100644 index 000000000..cbbc7fc6d --- /dev/null +++ b/tests/corpus/fetch-headers-copy-cache.ts @@ -0,0 +1,125 @@ +// Headers copies and the cached sort-and-combine list: copies are +// independent of their source, reads see every mutation, and live +// iterators re-read the current list after a change. +function dump(label: string, h: Headers): void { + const parts: string[] = []; + h.forEach((value, name) => parts.push(`${name}=${value}`)); + console.log(`${label}: ${parts.join(" | ")}`); +} + +// The corpus lib has no DOM iterable typings for Headers, so iterators +// are reached through an untyped view. +const iter = (h: Headers): any => h; + +const source = new Headers({ "X-B": " two ", "x-a": "one", Accept: "text/html" }); +source.append("accept", "application/json"); +source.append("Set-Cookie", "a=1"); +source.append("set-cookie", "b=2"); +dump("source", source); + +// A Headers copy keeps the combined values and stays separate. +const copy = new Headers(source); +source.set("x-a", "changed"); +copy.append("x-c", "three"); +dump("source after set", source); +dump("copy after append", copy); +console.log(JSON.stringify(copy.get("accept")), JSON.stringify(copy.get("set-cookie"))); +console.log(JSON.stringify(copy.getSetCookie())); + +// Copying into an empty Response and a Request, then appending a repeat. +const response = new Response("body", { headers: copy, status: 201 }); +response.headers.append("accept", "*/*"); +response.headers.append("cookie", "c=3"); +response.headers.append("Cookie", "d=4"); +dump("response", response.headers); +dump("copy unchanged", copy); +const clone = new Response(response.body, response); +dump("response clone", clone.headers); +const request = new Request("http://example.test/", { headers: copy }); +console.log("request x-b", JSON.stringify(request.headers.get("X-B"))); + +// Every read after a mutation sees it: get, has, iteration, spread. +const h = new Headers([["x-z", "z"], ["x-m", "m"]]); +console.log([...iter(h).entries()].map(([n, v]) => `${n}:${v}`).join(",")); +h.delete("x-z"); +console.log([...iter(h).keys()].join(","), h.has("x-z")); +h.delete("x-missing"); +console.log([...iter(h).values()].join(",")); +h.append("x-a", "first"); +console.log([...iter(h).entries()].map(([n, v]) => `${n}:${v}`).join(",")); +h.set("X-M", "again"); +console.log(JSON.stringify(h.get("x-m")), [...iter(h).keys()].join(",")); + +// A live iterator: mutations between steps change what comes next. +const it = iter(h).entries(); +console.log(JSON.stringify(it.next().value)); +h.append("x-0", "before"); +console.log(JSON.stringify(it.next().value)); +h.delete("x-m"); +h.append("x-zz", "after"); +console.log(JSON.stringify(it.next()), JSON.stringify(it.next())); + +// forEach that mutates as it walks. +const walk = new Headers([["b", "2"], ["d", "4"]]); +const seen: string[] = []; +walk.forEach((value, name) => { + seen.push(`${name}=${value}`); + if (name === "b") { + walk.append("c", "3"); + walk.append("a", "1"); + } + if (name === "c") walk.set("d", "four"); +}); +console.log(seen.join(" | ")); + +// Invalid input in a copy source is still rejected (plain object source). +try { + new Response("x", { headers: { "bad name": "v" } }); +} catch (error) { + console.log((error as Error).name, (error as Error).message); +} +try { + new Headers(source).append("x", "a\nb"); +} catch (error) { + console.log((error as Error).name, (error as Error).message); +} + +// Appending an empty value leaves the separator's space at the end of the +// combined value; a copy trims it again, as Node's does. A leading +// separator has no whitespace to trim. +const spaced = new Headers(); +spaced.append("a", "x"); +spaced.append("a", ""); +spaced.append("b", ""); +spaced.append("b", "y"); +spaced.append("c", ""); +spaced.append("c", ""); +spaced.append("cookie", "k=v"); +spaced.append("cookie", ""); +const spacedList = (headers: Headers): string => { + const out: string[] = []; + headers.forEach((value, name) => out.push(`${name}=${JSON.stringify(value)}`)); + return out.join(" "); +}; +console.log(spacedList(spaced)); +console.log(spacedList(new Headers(spaced))); +console.log(spacedList(new Headers(new Headers(spaced)))); +console.log(spacedList(new Response("", { headers: spaced }).headers)); +console.log(spacedList(new Request("http://example.test/", { headers: spaced }).headers)); + +// The same through duplicate constructor entries, in a Headers object and +// in a Response's own headers. +const entries: [string, string][] = [ + ["a", "x"], + ["a", ""], + ["c", ""], + ["c", ""], + ["d", "plain"], +]; +const built = new Headers(entries); +console.log(spacedList(built)); +console.log(spacedList(new Headers(built))); +const builtResponse = new Response("", { headers: entries }); +console.log(spacedList(builtResponse.headers)); +console.log(spacedList(new Headers(builtResponse.headers))); +console.log(spacedList(new Request("http://example.test/", { headers: built }).headers)); diff --git a/tests/corpus/fs-readdir-recursive.ts b/tests/corpus/fs-readdir-recursive.ts new file mode 100644 index 000000000..39fd7f5cc --- /dev/null +++ b/tests/corpus/fs-readdir-recursive.ts @@ -0,0 +1,52 @@ +// readdirSync(path, { recursive: true }) lists every entry below the path, +// relative to it: the path's own entries first, then each directory's in the +// order its entry was listed. A symlink to a directory is followed. + +import { mkdirSync, mkdtempSync, readdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +const root = mkdtempSync(join(tmpdir(), "readdir-recursive-")); +writeFileSync(join(root, "a.txt"), "a"); +mkdirSync(join(root, "b", "d"), { recursive: true }); +writeFileSync(join(root, "b", "c.txt"), "c"); +writeFileSync(join(root, "b", "d", "e.txt"), "e"); +mkdirSync(join(root, "empty")); +mkdirSync(join(root, "z")); +writeFileSync(join(root, "z", "y.txt"), "y"); +symlinkSync(join(root, "b", "d"), join(root, "link")); + +console.log(readdirSync(root, { recursive: true }).join(" ")); +console.log(readdirSync(root, { recursive: false }).join(" ")); +console.log(readdirSync(`${root}/b/`, { recursive: true }).join(" ")); + +let deep = false; +for (const flag of [false, true]) { + deep = flag; + console.log(deep, readdirSync(join(root, "z"), { recursive: deep }).length); +} +console.log(readdirSync(join(root, "empty"), { recursive: true }).length); + +// A flag expression typed `true` or `false` still evaluates, once. +let probes = 0; +function yes(): true { + probes++; + return true; +} +function no(): false { + probes++; + return false; +} +console.log( + readdirSync(join(root, "b"), { recursive: yes() }).length, + readdirSync(join(root, "b"), { recursive: no() }).length, + probes, +); + +try { + readdirSync(join(root, "missing"), { recursive: true }); +} catch (e) { + console.log((e as NodeJS.ErrnoException).code); +} + +rmSync(root, { recursive: true }); diff --git a/tests/corpus/generic-alias-cast-instances.ts b/tests/corpus/generic-alias-cast-instances.ts new file mode 100644 index 000000000..11a098214 --- /dev/null +++ b/tests/corpus/generic-alias-cast-instances.ts @@ -0,0 +1,103 @@ +// Casts to a generic type alias spelled over the enclosing function's own +// type parameters (`x as Catalog`). The checker keeps such an alias +// symbolic inside the generic body; each instance maps it with its own bound +// type arguments. + +type Groups = Record>; + +type EntryKey = { [A in keyof G]: keyof G[A] }[keyof G] & string; +type KeyOfKind = Extract, `${K}:${string}`>; +type EntryId = K extends `${string}:${infer I}` ? I : never; + +type Catalog = { + [Key in KeyOfKind as EntryId]: { id: EntryId; label: string; owner: P }; +}; + +function flatten(groups: Groups, kind: string): Record { + return Object.fromEntries( + Object.values(groups) + .flatMap((entries) => Object.entries(entries)) + .filter(([key]) => key.startsWith(kind + ":")) + .map(([, entry]) => [(entry as { id: string }).id, entry]), + ); +} + +function toolCatalog( + _owner: P, + groups: G, +): Catalog { + return flatten(groups, "tool") as Catalog; +} + +function partCatalog( + _owner: P, + groups: G, +): Catalog { + return flatten(groups, "part") as Catalog; +} + +const workshop = { + bench: { + "tool:saw": { id: "saw", label: "Hand saw", owner: "acme" }, + "part:bolt": { id: "bolt", label: "Hex bolt", owner: "acme" }, + }, + shelf: { + "tool:drill": { id: "drill", label: "Cordless drill", owner: "acme" }, + }, +}; + +const tools = toolCatalog("acme", workshop); +console.log(tools.saw.label, tools.drill.id, tools.drill.owner); +console.log(Object.keys(tools).join(","), JSON.stringify(tools.saw)); + +const parts = partCatalog("acme", workshop); +console.log(parts.bolt.label, Object.keys(parts).length); + +// A second instantiation with different groups gets its own layout. +const garden = { + shed: { + "tool:rake": { id: "rake", label: "Leaf rake", owner: "green" }, + "tool:hoe": { id: "hoe", label: "Garden hoe", owner: "green" }, + }, +}; +const gardenTools = toolCatalog("green", garden); +console.log(gardenTools.rake.label, gardenTools.hoe.label, Object.keys(gardenTools).join(",")); + +// No entry of the kind: the alias resolves to the empty record. +const gardenParts = partCatalog("green", garden); +console.log(Object.keys(gardenParts).length, JSON.stringify(gardenParts)); + +// A homomorphic mapped alias over the bound type, cast inside the body. +type Boxed = { [K in keyof T]: { value: T[K] } }; + +function boxAll>(values: T): Boxed { + const out: Record = {}; + for (const key of Object.keys(values)) out[key] = { value: values[key] }; + return out as Boxed; +} + +const boxed = boxAll({ width: 3, height: 4 }); +console.log(boxed.width.value * boxed.height.value); + +// The alias nested in an array type of the signature. +function boxEach>(rows: T[]): Boxed[] { + return rows.map((row) => { + const out: Record = {}; + for (const key of Object.keys(row)) out[key] = { value: row[key] }; + return out as Boxed; + }); +} + +const rows = boxEach([ + { x: 1, y: 2 }, + { x: 5, y: 8 }, +]); +console.log(rows.map((r) => r.x.value + r.y.value).join(" ")); + +// A generic body forwarding its parameters to another generic cast. +function ownedTools(groups: G): Catalog { + return toolCatalog("shared", groups); +} + +const shared = ownedTools({ rack: { "tool:clamp": { id: "clamp", label: "C-clamp", owner: "shared" } } }); +console.log(shared.clamp.label, shared.clamp.owner); diff --git a/tests/corpus/generic-interface-void-calls.ts b/tests/corpus/generic-interface-void-calls.ts new file mode 100644 index 000000000..63c137c10 --- /dev/null +++ b/tests/corpus/generic-interface-void-calls.ts @@ -0,0 +1,65 @@ +// Calling a void generic method through an interface value that classes +// implement (the receiver is a checked-dynamic object, and the call +// dispatches by name to the class's erased method) discards the result, as +// JavaScript does, instead of converting it to `void`. + +interface Message { + content: string; + details?: T; +} + +interface Channel { + name: string; + publish(message: Message): void; + count(items: T[]): number; +} + +const delivered: string[] = []; + +class LogChannel implements Channel { + name: string; + constructor(name: string) { + this.name = name; + } + publish(message: Message): void { + delivered.push(`${this.name}:${message.content}:${message.details === undefined ? "-" : JSON.stringify(message.details)}`); + } + count(items: T[]): number { + return items.length; + } +} + +class LoudChannel extends LogChannel { + publish(message: Message): void { + super.publish({ content: message.content.toUpperCase(), details: message.details }); + } +} + +const channels: Channel[] = [new LogChannel("quiet"), new LoudChannel("loud")]; +for (const channel of channels) { + channel.publish({ content: "hello", details: [1, 2] }); + channel.publish({ content: "bare" }); + const result = channel.publish({ content: "typed", details: "x" }); + console.log(channel.name, result === undefined, channel.count(["a", "b", "c"])); +} +console.log(delivered.join(" | ")); + +// A class may implement the void member with a method that returns a value; +// JavaScript passes that value on. +class CountingChannel implements Channel { + name = "counting"; + sent = 0; + publish(message: Message): number { + this.sent++; + return message.content.length; + } + count(items: T[]): number { + return items.length * 2; + } +} + +const counted: Channel[] = [new CountingChannel(), new LogChannel("plain")]; +for (const channel of counted) { + console.log(channel.name, channel.publish({ content: "four" })); + console.log(`sent ${channel.publish({ content: "again", details: 2 })}`); +} diff --git a/tests/corpus/generic-member-lambda-impls.ts b/tests/corpus/generic-member-lambda-impls.ts new file mode 100644 index 000000000..fd25bae96 --- /dev/null +++ b/tests/corpus/generic-member-lambda-impls.ts @@ -0,0 +1,40 @@ +// An object literal implements an interface's generic method with a plain +// lambda (`send: (m) => …` for `send(m: Message): string`). The +// lambda takes the contextual signature's type parameters as its own and +// lowers once, erased, like a generic lambda: its parameters hold whatever +// each call passes. + +interface Message { + content: string; + details?: T; +} + +interface Sender { + name: string; + send(message: Message): string; + notify(message: Message): void; + pick(keys: K[], fallback: K): K; +} + +const log: string[] = []; + +function makeSender(name: string): Sender { + return { + name, + send: (message) => `${name} sent ${message.content} (${message.details === undefined ? "no details" : JSON.stringify(message.details)})`, + notify: (message) => { + log.push(`${name} notified ${message.content}`); + }, + pick: (keys, fallback) => keys[0] ?? fallback, + }; +} + +const senders: Sender[] = [makeSender("first"), makeSender("second")]; +for (const sender of senders) { + console.log(sender.send({ content: "hi", details: { n: 3 } })); + console.log(sender.send({ content: "bare" })); + console.log(sender.send({ content: "list", details: [1, 2] })); + sender.notify({ content: "ping", details: true }); + console.log(sender.pick(["a", "b"], "z"), sender.pick([], "z")); +} +console.log(log.join("; ")); diff --git a/tests/corpus/instanceof-primitive-wrappers-typed.ts b/tests/corpus/instanceof-primitive-wrappers-typed.ts new file mode 100644 index 000000000..dc065e569 --- /dev/null +++ b/tests/corpus/instanceof-primitive-wrappers-typed.ts @@ -0,0 +1,14 @@ +// Typed operands tested against the primitive wrapper constructors: always +// false, with the operand's side effects kept. +let reads = 0; +function pick(n: number): unknown { + reads++; + return n === 0 ? "text" : n === 1 ? 7 : n === 2 ? { ok: true } : [n]; +} +const seen: string[] = []; +for (let i = 0; i < 4; i++) { + const value = pick(i); + seen.push(`${value instanceof String}/${value instanceof Number}/${value instanceof Boolean}`); +} +const record: object = { a: 1 }; +console.log(seen.join(" "), record instanceof Object, record instanceof String, reads); diff --git a/tests/corpus/instanceof-primitive-wrappers.mjs b/tests/corpus/instanceof-primitive-wrappers.mjs new file mode 100644 index 000000000..11bcaba07 --- /dev/null +++ b/tests/corpus/instanceof-primitive-wrappers.mjs @@ -0,0 +1,24 @@ +// `instanceof` against the primitive wrapper constructors (Boolean, Number, +// String, BigInt, Symbol), as typebox's value guards test: false for +// primitives and for every object, and the operand still evaluates. +function IsBoolean(value) { + return value instanceof Boolean; +} +function IsNumber(value) { + return value instanceof Number; +} +function IsString(value) { + return value instanceof String; +} +function IsUint8Array(value) { + return value instanceof globalThis.Uint8Array; +} +const values = [true, 1, "s", 2n, Symbol("k"), null, undefined, new Uint8Array(2), {}, [], () => 1, new Date(0)]; +for (const v of values) + console.log(typeof v, IsBoolean(v), IsNumber(v), IsString(v), IsUint8Array(v), v instanceof BigInt, v instanceof Symbol); +let calls = 0; +const next = () => { + calls++; + return { n: calls }; +}; +console.log(next() instanceof Number, next() instanceof globalThis.String, calls); diff --git a/tests/corpus/intersection-restated-members.ts b/tests/corpus/intersection-restated-members.ts new file mode 100644 index 000000000..249b9ddfb --- /dev/null +++ b/tests/corpus/intersection-restated-members.ts @@ -0,0 +1,82 @@ +// An intersection that restates a member with a separately written but +// structurally equal type maps as that one type. Restating an optional member +// as required makes it required; the checker keeps both element literals +// (`A[] & B[]`), and their arrays, tuples, and maps share one representation. + +interface Options { + root: string; + files?: Array<{ path: string; size: number }>; +} +type Normalized = Options & { files: Array<{ path: string; size: number }> }; + +function normalize(options: Options): Normalized { + return { ...options, files: options.files ?? [] }; +} + +function totalSize(n: Normalized): number { + let total = 0; + for (const f of n.files) total += f.size; + return total; +} + +const plain = normalize({ root: "/a" }); +console.log(plain.root, plain.files.length, totalSize(plain)); +const full = normalize({ + root: "/b", + files: [ + { path: "x.txt", size: 3 }, + { path: "y.txt", size: 4 }, + ], +}); +full.files.push({ path: "z.txt", size: 5 }); +console.log(full.files.map((f) => f.path).join(","), totalSize(full)); +console.log(JSON.stringify(full)); + +let maybe: Normalized | undefined; +if (process.argv.length < 99) maybe = { root: "/", files: [{ path: "a", size: 1 }] }; +console.log(maybe?.root, maybe?.files[0]?.path); + +// Nested records whose inner arrays are restated. +interface Settings { + name: string; + meta?: { tags: Array<{ key: string; value: string }> }; +} +type Resolved = Settings & { meta: { tags: Array<{ key: string; value: string }> } }; +const resolved: Resolved = { name: "s", meta: { tags: [{ key: "k", value: "v" }] } }; +resolved.meta.tags.push({ key: "k2", value: "v2" }); +for (const t of resolved.meta.tags) console.log(`${t.key}=${t.value}`); + +// Tuples, nested arrays, and maps. +interface Layout { + pair?: [{ x: number }, string]; + grid?: Array>; + index?: Map; +} +type Filled = Layout & { + pair: [{ x: number }, string]; + grid: Array>; + index: Map; +}; +const filled: Filled = { + pair: [{ x: 7 }, "seven"], + grid: [[{ cell: 1 }, { cell: 2 }], [{ cell: 3 }]], + index: new Map([["home", { hits: 2 }]]), +}; +filled.index.set("about", { hits: 1 }); +console.log(filled.pair[0].x, filled.pair[1]); +console.log(filled.grid.map((row) => row.map((c) => c.cell).join("+")).join(" | ")); +for (const [k, v] of filled.index) console.log(k, v.hits); + +// Both sides required, and both sides optional (the member stays optional). +type Twice = { items: Array<{ n: number }> } & { items: Array<{ n: number }> }; +const twice: Twice = { items: [{ n: 1 }, { n: 2 }] }; +console.log(twice.items.reduce((s, i) => s + i.n, 0)); +type BothOptional = { extra?: Array<{ n: number }> } & { extra?: Array<{ n: number }> }; +const none: BothOptional = {}; +const some: BothOptional = { extra: [{ n: 9 }] }; +console.log(none.extra === undefined, some.extra?.[0]?.n); + +// A restated member read through a function value. +type Handler = (event: Normalized) => string; +const describe: Handler = (event) => `${event.root}:${event.files.length}`; +console.log(describe(full), describe(plain)); diff --git a/tests/corpus/localized-failure-cycles.ts b/tests/corpus/localized-failure-cycles.ts new file mode 100644 index 000000000..2db499707 --- /dev/null +++ b/tests/corpus/localized-failure-cycles.ts @@ -0,0 +1,29 @@ +// Localized failure, stage L2: reference cycles through dynamic slots are +// collected (instance -> dynamic field -> capsule -> instance). +type Peer = Link | Map string> | AbortSignal | (() => number); + +class Link { + id: number; + peer: Peer | undefined; + constructor(id: number) { + this.id = id; + this.peer = undefined; + } + peerId(): number { + const p = this.peer; + return p instanceof Link ? p.id : -1; + } +} + +let total = 0; +for (let i = 0; i < 20000; i++) { + const a = new Link(i); + const b = new Link(i + 1); + a.peer = b; + b.peer = a; + total += a.peerId() - b.peerId(); +} +const keep = new Link(7); +keep.peer = () => keep.id * 2; +const f = keep.peer; +console.log(total, typeof f === "function" ? f() : 0, keep.peerId()); diff --git a/tests/corpus/localized-failure-declarations.ts b/tests/corpus/localized-failure-declarations.ts new file mode 100644 index 000000000..c4acdfbaa --- /dev/null +++ b/tests/corpus/localized-failure-declarations.ts @@ -0,0 +1,64 @@ +// Localized failure, stage L2: fields, parameters and returns whose +// declared type has no static lowering become checked dynamic slots, so +// their class or function still compiles. +interface ToolDefinition { + name: string; + execute(id: string, params: any): string; +} +type Progress = (done: number, total: number) => void; +type ProgressOrSignal = Progress | AbortSignal | Map string> | Set; + +class Session { + // A field: the class registers, the field holds the array by reference. + private tools: ToolDefinition[] = []; + state: any = { count: 0 }; + readonly label: string; + constructor(label: string) { + this.label = label; + } + add(tool: ToolDefinition): void { + this.tools.push(tool); + } + run(id: string, params: unknown): string[] { + const out: string[] = []; + for (const tool of this.tools) out.push(tool.execute(id, params)); + this.state.count = this.state.count + 1; + return out; + } + count(): number { + return this.tools.length; + } + // A return slot. + first(): ToolDefinition | ProgressOrSignal | undefined { + return this.tools[0]; + } +} + +// A parameter: the function registers and is called from typed code. +function listAll(dir: string, onProgress?: ProgressOrSignal): number { + if (typeof onProgress === "function") onProgress(1, dir.length); + else if (onProgress instanceof Set) return onProgress.size; + return dir.length; +} + +// A base class with a dynamic parameter, and an override. +class Base { + notify(target: ProgressOrSignal | undefined): string { + return target === undefined ? "base:none" : "base:" + typeof target; + } +} +class Derived extends Base { + override notify(target: ProgressOrSignal | undefined): string { + return "derived/" + super.notify(target); + } +} + +const echo: ToolDefinition = { name: "echo", execute: (id, params) => `${id}:${JSON.stringify(params)}` }; +const s = new Session("main"); +s.add(echo); +s.add({ name: "len", execute: (id) => String(id.length) }); +console.log(s.label, s.count(), s.run("abc", { k: 1 }).join(" | "), s.state.count); +console.log("same tool", s.first() === echo); +console.log(listAll("hello"), listAll("hello", (d, t) => console.log("progress", d, t)), listAll("x", new Set([1, 2]))); +const objs: Base[] = [new Base(), new Derived()]; +for (const o of objs) console.log(o.notify(undefined), o.notify((a, b) => a + b)); diff --git a/tests/corpus/localized-failure-forward-capture.ts b/tests/corpus/localized-failure-forward-capture.ts new file mode 100644 index 000000000..ba43a037d --- /dev/null +++ b/tests/corpus/localized-failure-forward-capture.ts @@ -0,0 +1,19 @@ +// Localized failure, stage L1: a statement that declares a binding an +// earlier closure captures, and then a binding whose declared type has no +// static lowering, still initializes the captured binding. +type Callbacks = ((done: number) => void) | AbortSignal | Map string> | Set; + +function run(): void { + const read = (): number => total; + const total = 3, + hook: Callbacks = new Set([1, 2]); + console.log(read(), hook instanceof Set, hook instanceof Set && hook.size); + let late = (): string => label; + let label = "first", + other: Callbacks = new Set([3]); + console.log(late(), other instanceof Set); + label = "second"; + late = (): string => label + "!"; + console.log(late()); +} +run(); diff --git a/tests/corpus/localized-failure-function-slots.ts b/tests/corpus/localized-failure-function-slots.ts new file mode 100644 index 000000000..0a7fdf641 --- /dev/null +++ b/tests/corpus/localized-failure-function-slots.ts @@ -0,0 +1,28 @@ +// Localized failure, stage L2: parameters and returns of arrows, function +// expressions and generic instances become checked dynamic slots. +type Hook = Map string> | AbortSignal | (() => number) | Set; + +interface Model { + id: T; + hook: Hook; +} + +function retry(label: string, request: () => Promise | Hook): T | string { + const r = request(); + return typeof r === "function" ? label + ":" + String(r()) : label + ":other"; +} + +function describe(model: Model): string { + return `${String(model.id)}/${model.hook instanceof Set ? model.hook.size : typeof model.hook}`; +} + +const run = (hook: Hook, n: number): string => (typeof hook === "function" ? String(hook() + n) : "set"); +const make = function (size: number): Hook { + return new Set(Array.from({ length: size }, (_, i) => `k${i}`)); +}; + +console.log(run(() => 40, 2), run(new Set(["a"]), 1)); +const h = make(3); +console.log(h instanceof Set ? h.size : -1); +console.log(retry("a", () => () => 7), retry("b", () => new Set())); +console.log(describe({ id: 5, hook: new Set(["x", "y"]) }), describe({ id: "s", hook: () => 1 })); diff --git a/tests/corpus/localized-failure-held-values.ts b/tests/corpus/localized-failure-held-values.ts new file mode 100644 index 000000000..ee52498c1 --- /dev/null +++ b/tests/corpus/localized-failure-held-values.ts @@ -0,0 +1,70 @@ +// Localized failure, stage L1: values of every kind held in a local or +// module global whose declared type has no static lowering keep their +// identity and behavior. +class Point { + x: number; + y: number; + constructor(x: number, y: number) { + this.x = x; + this.y = y; + } + norm(): number { + return Math.abs(this.x) + Math.abs(this.y); + } +} +interface Cfg { + name: string; + depth: number; +} +type Anything = + | Point + | Cfg + | number[] + | Map string> + | AbortSignal + | (() => number) + | Promise; + +const p = new Point(3, -4); +const cfg: Cfg = { name: "a", depth: 1 }; +const xs = [1, 2, 3]; +let counter = 0; +const tick = (): number => ++counter; + +// A module global holding a class instance. +let held: Anything = p; +if (held instanceof Point) { + console.log("point", held.norm(), held === p); + held.x = 10; +} +console.log("typed alias", p.x, p.norm()); + +// A record: mutation through the hole is visible in the typed alias. +held = cfg; +if (!(held instanceof Point) && !Array.isArray(held) && typeof held === "object" && "depth" in held) { + held.depth = 5; + console.log("record", held.name, held === cfg); +} +console.log("typed alias", cfg.depth, JSON.stringify(cfg), Object.keys(cfg).join(",")); + +// An array: pushes through the hole show in typed code. +held = xs; +if (Array.isArray(held)) { + held.push(4); + console.log("array", held.length, held === xs); +} +console.log("typed alias", xs.length, xs.join("+")); + +function main(): void { + // A local holding a closure, called through the hole. + let local: Anything = tick; + if (typeof local === "function") { + console.log("closure", local(), local(), counter, local === tick); + } + // A promise, awaited through the hole. + local = Promise.resolve(42); + if (local instanceof Promise) { + local.then((v) => console.log("promise", v)); + } +} +main(); diff --git a/tests/corpus/localized-failure-locals.ts b/tests/corpus/localized-failure-locals.ts new file mode 100644 index 000000000..418b86d87 --- /dev/null +++ b/tests/corpus/localized-failure-locals.ts @@ -0,0 +1,43 @@ +// Localized failure, stage L1: locals, loop variables and module globals +// whose declared type has no static lowering hold checked-dynamic values. +type Progress = (done: number, total: number) => void; +type Callbacks = Progress | AbortSignal | Map string> | Set; + +const report: Progress = (done, total) => console.log(`progress ${done}/${total}`); +const seen = new Set([1, 2, 3]); + +// A module global, written with typed values of several arms. +let current: Callbacks | undefined = report; + +function describe(label: string): void { + // A local of the same declared type. + const local: Callbacks | undefined = current; + if (local === undefined) { + console.log(label, "none"); + } else if (typeof local === "function") { + local(1, 2); + console.log(label, "function", local === report); + } else if (local instanceof Set) { + console.log(label, "set", local.size, local.has(2), local === seen); + local.add(4); + } else { + console.log(label, "other"); + } +} + +describe("first"); +current = seen; +describe("second"); +console.log("typed alias sees the write", seen.size, seen.has(4)); +current = undefined; +describe("third"); + +// A loop variable over an array whose element type does not compile. +const all: (Callbacks | number)[] = [report, seen, 7]; +for (const item of all) { + if (typeof item === "number") console.log("number", item + 1); + else if (typeof item === "function") item(3, 4); + else console.log("object", item instanceof Set); +} +all.push(9); +console.log(all.length); diff --git a/tests/corpus/localized-failure-pi-shapes.ts b/tests/corpus/localized-failure-pi-shapes.ts new file mode 100644 index 000000000..9879606eb --- /dev/null +++ b/tests/corpus/localized-failure-pi-shapes.ts @@ -0,0 +1,37 @@ +// Localized failure, stage L2: pi-shaped declarations. +interface Token { + text: string; +} +interface AgentMessage { + role: "user" | "assistant"; + content: string[]; +} +type CreateRuntime = (cwd: string) => Promise<{ session: Session; extra: Map string> | AbortSignal }>; + +class Session { + messages: AgentMessage[] = []; + // A field whose union mentions a stdlib type with no lowering: the class + // registers; constructing a WeakRef would still be refused. + cachedTokens?: WeakRef<{ source: string; tokens: Token[] }> | undefined; + rendererState: any = {}; + effectState: Record = {}; + private readonly createRuntime: CreateRuntime | undefined; + constructor(createRuntime: CreateRuntime | undefined) { + this.createRuntime = createRuntime; + } + hasRuntime(): boolean { + return this.createRuntime !== undefined; + } + clear(): AgentMessage[] { + // An inferred return over a spread clone. + return this.messages.map((message) => ({ ...message, content: [] })); + } +} + +const s = new Session(undefined); +const withRuntime = new Session(async (cwd) => ({ session: s, extra: new Map() })); +s.messages.push({ role: "user", content: ["hi"] }, { role: "assistant", content: ["yo", "!"] }); +s.rendererState.mode = "fast"; +s.effectState["k"] = 3; +console.log(s.hasRuntime(), s.cachedTokens === undefined, s.rendererState.mode, s.effectState["k"]); +console.log(JSON.stringify(s.clear()), s.messages[1]!.content.length, withRuntime.hasRuntime()); diff --git a/tests/corpus/localized-failure-registered-statics.ts b/tests/corpus/localized-failure-registered-statics.ts new file mode 100644 index 000000000..9ca49d4f2 --- /dev/null +++ b/tests/corpus/localized-failure-registered-statics.ts @@ -0,0 +1,46 @@ +// Localized failure, stage L2: a class that registers with dynamic slots +// runs its static field initializers where its statement evaluates, and a +// generic class's static methods compile once it registers. +type Progress = (done: number, total: number) => void; +type Callbacks = Progress | AbortSignal | Map string> | Set; + +function sideEffect(label: string): number { + console.log("static init", label); + return label.length; +} + +// Registers at its statement: the initializers run before the next line. +class Registry { + state: any = { n: 1 }; + static count = sideEffect("registry"); + static names: string[] = ["a", "b"]; + read(): number { + return this.state.n; + } +} + +console.log("after the class statement"); + +// Never referenced: its initializer runs all the same. +class Unused { + slot: any = null; + static ready = sideEffect("unused"); +} + +// A generic class whose static method has a dynamic parameter. +class Box { + value: T; + constructor(v: T) { + this.value = v; + } + static describe(target: Callbacks | undefined): string { + return target === undefined ? "none" : typeof target; + } + static label(): string { + return "box"; + } +} + +const r = new Registry(); +console.log(r.read(), Registry.count, Registry.names.join(",")); +console.log(Box.label(), Box.describe(undefined), Box.describe(new Set([1])), new Box(3).value); diff --git a/tests/corpus/method-signature-any-params.ts b/tests/corpus/method-signature-any-params.ts new file mode 100644 index 000000000..e505ae78c --- /dev/null +++ b/tests/corpus/method-signature-any-params.ts @@ -0,0 +1,39 @@ +// A method or call signature of an interface whose parameter is typed `any` +// (directly, or through an alias that resolves to `any`) takes whatever the +// caller passes, as a function type's `any` parameter already does: the +// interface compiles, and implementations receive the checked-dynamic value. + +type Loose = any; + +interface Tool { + name: string; + execute(id: string, params: any, extra?: Loose): Promise; + describe(value: Loose): string; +} + +interface Formatter { + (value: any): string; +} + +const tools: Tool[] = [ + { + name: "count", + execute: async (id, params) => `${id} has ${Object.keys(params ?? {}).length} keys`, + describe: (value) => `count(${Array.isArray(value) ? "array" : typeof value})`, + }, +]; + +const format: Formatter = (value) => (typeof value === "number" ? value.toFixed(1) : String(value)); + +async function main(): Promise { + const inputs: unknown[] = [{ a: 1, b: [2, 3] }, "text", 42, null, [1, 2]]; + for (const tool of tools) { + for (const input of inputs) { + console.log(tool.name, await tool.execute("call", input), tool.describe(input)); + } + console.log(tool.name, await tool.execute("extra", { k: true }, { more: "yes" })); + } + console.log(format(3), format("x"), format(undefined)); +} + +main().catch((e: unknown) => console.log("failed", e instanceof Error ? e.message : String(e))); diff --git a/tests/corpus/native-array-dynamic-reads/main.ts b/tests/corpus/native-array-dynamic-reads/main.ts new file mode 100644 index 000000000..0641114bf --- /dev/null +++ b/tests/corpus/native-array-dynamic-reads/main.ts @@ -0,0 +1,32 @@ +// Native arrays and iterators from typed code, read by untyped code: for-of +// over them, their length, an iterator's next read as a value and called, +// generators and async generators beside them, and an array whose +// iteration a Symbol.iterator use elsewhere in the program does not change. +import { asyncKinds, describe, lengths, sumAll, stepper } from "./reads.js"; + +interface Item { + sku: string; + qty: number; +} +const numbers: number[] = [1, 2, 3, 4]; +const items: Item[] = [ + { sku: "a", qty: 2 }, + { sku: "b", qty: 5 }, +]; +const empty: string[] = []; +function* counting(): Generator { + yield 10; + yield 20; +} +console.log(sumAll(numbers), sumAll(items.map((i) => i.qty)), sumAll(empty.map(Number))); +console.log(lengths([numbers, items, empty]).join()); +numbers.push(5); +console.log(lengths([numbers]).join(), sumAll(numbers)); +console.log(describe(items), describe(counting())); +console.log(stepper(numbers).join(), stepper(counting()).join()); +const iterable = { *[Symbol.iterator]() { yield "own"; } }; +console.log([...iterable].join(), sumAll(numbers)); +async function* later(): AsyncGenerator { + yield 1; +} +asyncKinds(later(), numbers).then((kinds) => console.log(kinds.join())); diff --git a/tests/corpus/native-array-dynamic-reads/reads.js b/tests/corpus/native-array-dynamic-reads/reads.js new file mode 100644 index 000000000..67e1a824a --- /dev/null +++ b/tests/corpus/native-array-dynamic-reads/reads.js @@ -0,0 +1,29 @@ +export function sumAll(values) { + let total = 0; + for (const value of values) total += value; + return total; +} +export function lengths(arrays) { + const out = []; + for (const array of arrays) out.push(array.length); + return out; +} +export function describe(values) { + const parts = []; + for (const value of values) parts.push(typeof value === "object" ? value.sku : value); + return parts.join("+"); +} +export function stepper(values) { + const iterator = values[Symbol.iterator](); + const next = iterator.next; + const out = []; + for (let step = next.call(iterator); !step.done; step = next.call(iterator)) out.push(step.value); + out.push(typeof iterator.next, iterator.next === next); + return out; +} +export async function asyncKinds(generator, array) { + const out = []; + for await (const value of generator) out.push(value); + for (const value of array) out.push(value); + return out; +} diff --git a/tests/corpus/native-worker-length-loop-reassign.ts b/tests/corpus/native-worker-length-loop-reassign.ts new file mode 100644 index 000000000..7ec152b16 --- /dev/null +++ b/tests/corpus/native-worker-length-loop-reassign.ts @@ -0,0 +1,58 @@ +// Loops to a typed array's length in a program that starts workers, where +// the body assigns the loop's array: the condition reads the new array's +// length on the next iteration, shorter or longer. +import { Worker, isMainThread, parentPort } from "node:worker_threads"; + +function shrink(values: Float64Array): string { + let total = 0; + let steps = 0; + for (let i = 0; i < values.length; i++) { + total += values[i]!; + steps++; + if (i === 100) values = values.subarray(0, 50); + } + return `shrink ${steps} ${total}`; +} + +function grow(values: Uint8Array, longer: Uint8Array): string { + let total = 0; + let steps = 0; + for (let i = 0; i < values.length; i++) { + total += values[i]!; + steps++; + if (i === 10) values = longer; + } + return `grow ${steps} ${total}`; +} + +function swap(a: Int32Array, b: Int32Array): string { + let total = 0; + let steps = 0; + for (let i = 0; i < a.length; i++) { + total += a[i]!; + steps++; + if (i % 70 === 69) { + const t = a; + a = b; + b = t; + } + } + return `swap ${steps} ${total}`; +} + +function run(): string[] { + const values = new Float64Array(300).map((_, i) => i * 0.5); + const short = new Uint8Array(20).fill(3); + const long = new Uint8Array(200).map((_, i) => i & 7); + const a = new Int32Array(100).map((_, i) => i); + const b = new Int32Array(160).map((_, i) => -i); + return [shrink(values), grow(short, long), swap(a, b)]; +} + +if (isMainThread) { + for (const line of run()) console.log(line); + const worker = new Worker(new URL(import.meta.url)); + worker.on("message", (lines: string[]) => { + for (const line of lines) console.log("worker", line); + }); +} else parentPort!.postMessage(run()); diff --git a/tests/corpus/native-worker-terminate-bounded-loops.ts b/tests/corpus/native-worker-terminate-bounded-loops.ts new file mode 100644 index 000000000..ec1e4c736 --- /dev/null +++ b/tests/corpus/native-worker-terminate-bounded-loops.ts @@ -0,0 +1,102 @@ +// worker.terminate() stops a worker whose loop runs short inner loops: +// counted loops of a few iterations, a two-counter loop, an inner loop +// whose length grows past 64 iterations, an inner loop too long to finish, +// a nest of loops with constant bounds, and an inner loop whose counter +// stops changing at 2 ** 53 (`j + 1 === j` there), so it never ends. +import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads"; + +function counted(values: number[]): number { + let acc = 0; + for (let round = 0; ; round++) { + for (let i = 0; i < values.length; i++) acc = (acc + values[i]! * i) % 1000003; + for (let i = values.length - 1; i >= 0; i -= 2) acc = (acc * 7 + values[i]!) % 1000003; + if (round === 200) parentPort!.postMessage("ready"); + } + return acc; +} + +function paired(values: number[]): number { + let flips = 0; + for (let round = 0; ; round++) { + for (let i = 0, j = values.length - 1; i < j; i++, j--) { + const t = values[i]!; + values[i] = values[j]!; + values[j] = t; + flips++; + } + if (round === 200) parentPort!.postMessage("ready"); + } + return flips; +} + +function growing(): number { + let acc = 0; + for (let n = 1; ; n++) { + const limit = n % 200; + for (let i = 0; i < limit; i++) acc = (acc + i) % 1000003; + if (n === 200) parentPort!.postMessage("ready"); + } + return acc; +} + +function long(limit: number): number { + let acc = 0; + for (let round = 0; ; round++) { + for (let i = 0; i < limit; i++) { + acc = (acc * 7 + i) % 1000003; + if (i === 100000) parentPort!.postMessage("ready"); + } + } + return acc; +} + +const ROUNDS = 50; + +function constant(row: Float64Array): number { + let acc = 0; + for (let y = 0; ; y++) { + for (let i = 0; i < 8; i += 2) { + const c = row[i]!; + for (let j = 0; j < ROUNDS; j++) { + acc = (acc * 3 + j + c) % 1000003; + if (acc === -1) break; + } + } + if (y === 200) parentPort!.postMessage("ready"); + } + return acc; +} + +function stalled(): number { + let acc = 0; + for (let round = 0; ; round++) { + if (round === 0) parentPort!.postMessage("ready"); + for (let j = 9007199254740990; j < 9007199254741002; j++) acc = (acc + (j % 7)) % 1000003; + } + return acc; +} + +if (isMainThread) { + for (const kind of ["counted", "paired", "growing", "long", "constant", "stalled"]) { + await new Promise((resolve) => { + const worker = new Worker(new URL(import.meta.url), { workerData: kind }); + worker.once("message", () => { + void worker.terminate().then((code: number) => console.log(kind, "terminate", code)); + }); + worker.once("exit", (code: number) => { + console.log(kind, "exit", code); + resolve(); + }); + }); + } +} else { + const values = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8]; + let result = 0; + if (workerData === "counted") result = counted(values); + else if (workerData === "paired") result = paired(values); + else if (workerData === "growing") result = growing(); + else if (workerData === "long") result = long(1e15); + else if (workerData === "stalled") result = stalled(); + else result = constant(new Float64Array(8).fill(0.25)); + console.log("unexpected result", result); +} diff --git a/tests/corpus/native-worker-terminate-counted-loops.ts b/tests/corpus/native-worker-terminate-counted-loops.ts new file mode 100644 index 000000000..55972c4f6 --- /dev/null +++ b/tests/corpus/native-worker-terminate-counted-loops.ts @@ -0,0 +1,66 @@ +// worker.terminate() stops a worker in counted loops over a typed array +// with long bounds: ascending and descending counters, a step above one, a +// second counter that steps with the first, and loops to a typed array's +// length run again and again. +import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads"; + +function ascending(values: Float64Array, limit: number): number { + let acc = 0; + for (let i = 0; i < limit; i++) { + acc = (acc + values[i & 1023]!) % 1000003; + if (i === 5000) parentPort!.postMessage("ready"); + } + return acc; +} + +function descending(values: Float64Array, limit: number): number { + let acc = 0; + for (let i = limit; i > 0; i -= 3) { + acc = (acc * 31 + i + values[i & 1023]!) % 1000003; + if (i === limit - 15000) parentPort!.postMessage("ready"); + } + return acc; +} + +function paired(values: Float64Array, limit: number): number { + let acc = 0; + for (let i = 0, j = 7; i < limit; i++, j += 2) { + acc = (acc + i * j + values[j & 1023]!) % 1000003; + if (i === 5000) parentPort!.postMessage("ready"); + } + return acc; +} + +function lengths(bytes: Uint8Array): number { + let acc = 0; + for (let round = 0; ; round++) { + for (let i = 0; i < bytes.length; ++i) bytes[i] = bytes[i] === 97 ? 0 : (bytes[i]! + round) & 127; + for (let i = 0; i < bytes.length; i++) acc = (acc + bytes[i]!) % 1000003; + if (round === 50) parentPort!.postMessage("ready"); + } + return acc; +} + +if (isMainThread) { + for (const kind of ["ascending", "descending", "paired", "lengths"]) { + await new Promise((resolve) => { + const worker = new Worker(new URL(import.meta.url), { workerData: kind }); + worker.once("message", () => { + void worker.terminate().then((code: number) => console.log(kind, "terminate", code)); + }); + worker.once("exit", (code: number) => { + console.log(kind, "exit", code); + resolve(); + }); + }); + } +} else { + const values = new Float64Array(1024).map((_, i) => i); + const limit = 1e15; + let result = 0; + if (workerData === "ascending") result = ascending(values, limit); + else if (workerData === "descending") result = descending(values, limit); + else if (workerData === "paired") result = paired(values, limit); + else result = lengths(new Uint8Array(1 << 16).fill(97)); + console.log("unexpected result", result); +} diff --git a/tests/corpus/net-unix-socket-connect-default-import.mjs b/tests/corpus/net-unix-socket-connect-default-import.mjs new file mode 100644 index 000000000..7e58c18e2 --- /dev/null +++ b/tests/corpus/net-unix-socket-connect-default-import.mjs @@ -0,0 +1,22 @@ +// net.createConnection(path) through the default import of node:net, as a +// JavaScript module reads it: a client of a Unix domain socket server. +import net from "node:net"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +const dir = fs.mkdtempSync(path.join(os.tmpdir(), "scriptc-unix-default-")); +const socketPath = path.join(dir, "server.sock"); +const server = net.createServer((socket) => socket.end("hello")); +server.listen(socketPath, () => { + const sock = net.createConnection(socketPath); + sock.once("connect", () => console.log("connected")); + sock.on("data", (chunk) => console.log("data", String(chunk))); + sock.once("error", (e) => console.log("error", String(e))); + sock.on("close", () => { + server.close(() => { + fs.rmSync(dir, { recursive: true, force: true }); + console.log("closed"); + }); + }); +}); diff --git a/tests/corpus/net-unix-socket-connect.ts b/tests/corpus/net-unix-socket-connect.ts new file mode 100644 index 000000000..77832d60d --- /dev/null +++ b/tests/corpus/net-unix-socket-connect.ts @@ -0,0 +1,69 @@ +// net.createConnection and net.connect to a Unix domain socket: a path +// argument, the `path` option, a connect listener, data both ways, the +// error for a path nothing listens on, and a numeric string, which Node +// treats as a localhost port. +import * as net from "node:net"; +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; + +const dir = fs.mkdtempSync(path.join(os.tmpdir(), "scriptc-unix-connect-")); +const socketPath = path.join(dir, "server.sock"); +const missingPath = path.join(dir, "missing.sock"); + +const server = net.createServer((socket) => { + socket.on("data", (chunk) => { + socket.write(`echo:${chunk.toString()}`); + }); +}); + +function exchange(label: string, socket: net.Socket, message: string): Promise { + return new Promise((resolve) => { + socket.on("data", (chunk) => { + console.log(label, chunk.toString()); + socket.end(); + }); + socket.on("close", () => resolve()); + socket.write(message); + }); +} + +server.listen(socketPath, async () => { + console.log("listening"); + const first = net.createConnection(socketPath); + first.once("connect", () => console.log("first connected")); + await exchange("first", first, "one"); + + let listenerCalls = 0; + const second = net.connect(socketPath, () => { + listenerCalls++; + }); + await exchange("second", second, "two"); + console.log("second listener calls", listenerCalls); + + const third = net.createConnection({ path: socketPath }); + await exchange("third", third, "three"); + + await new Promise((resolve) => { + const missing = net.createConnection(missingPath); + missing.once("connect", () => console.log("missing connected")); + missing.once("error", (error: NodeJS.ErrnoException) => { + console.log("missing", error.code, error.message === `connect ENOENT ${missingPath}`); + resolve(); + }); + }); + + server.close(() => { + const tcp = net.createServer((socket) => socket.end("tcp")); + tcp.listen(0, "localhost", () => { + const { port } = tcp.address() as net.AddressInfo; + const client = net.createConnection(String(port)); + client.on("data", (chunk) => console.log("numeric string", chunk.toString())); + client.on("close", () => { + tcp.close(); + fs.rmSync(dir, { recursive: true, force: true }); + console.log("done"); + }); + }); + }); +}); diff --git a/tests/corpus/never-narrowed-literal-argument.ts b/tests/corpus/never-narrowed-literal-argument.ts new file mode 100644 index 000000000..a9bb5489f --- /dev/null +++ b/tests/corpus/never-narrowed-literal-argument.ts @@ -0,0 +1,46 @@ +// A string-literal union narrowed to `never` passes to a `never` parameter: +// the exhaustiveness-check idiom over if-chains and switches compiles, and the +// unreachable call never runs. + +function unreachable(_: never): never { + throw new Error("unreachable"); +} + +type Kind = "a" | "b"; +function viaIf(kind: Kind): number { + if (kind === "a") return 1; + if (kind === "b") return 2; + return unreachable(kind); +} + +type Shape = "circle" | "square" | "triangle"; +function corners(shape: Shape): number { + switch (shape) { + case "circle": + return 0; + case "square": + return 4; + case "triangle": + return 3; + default: + return unreachable(shape); + } +} + +function asserted(kind: Kind): string { + if (kind === "a" || kind === "b") return kind.toUpperCase(); + return unreachable(kind as never); +} + +type Level = 1 | 2; +function viaNumbers(level: Level): string { + if (level === 1) return "one"; + if (level === 2) return "two"; + return unreachable(level); +} + +const kinds: Kind[] = ["a", "b"]; +console.log(kinds.map(viaIf).join(","), viaIf(Date.now() > 0 ? "a" : "b")); +const shapes: Shape[] = ["circle", "square", "triangle"]; +console.log(shapes.map(corners).join(",")); +console.log(kinds.map(asserted).join(""), viaNumbers(1), viaNumbers(2)); diff --git a/tests/corpus/object-literal-members.mjs b/tests/corpus/object-literal-members.mjs new file mode 100644 index 000000000..4bd211435 --- /dev/null +++ b/tests/corpus/object-literal-members.mjs @@ -0,0 +1,67 @@ +// Object literals built in untyped functions, many times through the same +// sites: key order (integer-like keys first), members named +// like Object.prototype members and Error fields, __proto__ members, wide +// literals, literals that differ only in a later member, and objects +// changed after they are built. +function point(x, y) { + return { x, y, label: "p" + x }; +} +function ordered(i) { + return { b: i, 2: "two", a: i + 1, 1: "one", "01": "zero-one" }; +} +function named(i) { + return { constructor: i, toString: "s" + i, message: "m" + i, name: "n" + i, code: i, stack: "st" }; +} +function withProto(proto, i) { + if (proto === null) return { __proto__: null, own: i }; + const o = { own: i }; + Object.setPrototypeOf(o, proto); + return o; +} +function wide(i) { + return { + k0: i, k1: i, k2: i, k3: i, k4: i, k5: i, k6: i, k7: i, k8: i, k9: i, + k10: i, k11: i, k12: i, k13: i, k14: i, k15: i, k16: i, k17: i, k18: i, k19: i, + k20: i, k21: i, k22: i, k23: i, k24: i, k25: i, k26: i, k27: i, k28: i, k29: i, + k30: i, k31: i, k32: i, k33: i, k34: i, k35: i, k36: i, k37: i, k38: i, k39: i, + k40: i, k41: i, k42: i, k43: i, k44: i, k45: i, k46: i, k47: i, k48: i, k49: i, + k50: i, k51: i, k52: i, k53: i, k54: i, k55: i, k56: i, k57: i, k58: i, k59: i, + k60: i, k61: i, k62: i, k63: i, k64: i, k65: i, k66: i, k67: i, k68: i, k69: i, + }; +} +function variant(i) { + const head = { kind: "v", n: i }; + return i % 2 ? { ...head, odd: true } : { ...head, even: true, n: -i }; +} + +let sum = 0; +const points = []; +for (let i = 0; i < 2000; i++) { + const p = point(i, i * 2); + sum += p.x + p.y; + if (i % 500 === 0) points.push(p); +} +points[0].z = 9; +delete points[1].label; +console.log(sum, JSON.stringify(points), Object.keys(points[2]).join()); + +const o = [ordered(1), ordered(2)]; +console.log(Object.keys(o[1]).join(), JSON.stringify(o[0]), o[1]["01"], o[1][1]); + +const n = [named(1), named(2)]; +console.log(JSON.stringify(n[1]), typeof n[0].toString, n[1].constructor, n[1] instanceof Error); + +const proto = { greet() { return "hi " + this.own; } }; +const w = [withProto(proto, 1), withProto(proto, 2), withProto(null, 3)]; +console.log(w[1].greet(), Object.getPrototypeOf(w[0]) === proto, Object.getPrototypeOf(w[2]), Object.keys(w[1]).join()); + +let wideSum = 0; +for (let i = 0; i < 50; i++) { + const big = wide(i); + wideSum += big.k0 + big.k69; +} +console.log(wideSum, Object.keys(wide(1)).length, Object.keys(wide(2))[69]); + +const v = []; +for (let i = 0; i < 6; i++) v.push(variant(i)); +console.log(JSON.stringify(v)); diff --git a/tests/corpus/recursive-generic-method-members.ts b/tests/corpus/recursive-generic-method-members.ts new file mode 100644 index 000000000..1f4d8372d --- /dev/null +++ b/tests/corpus/recursive-generic-method-members.ts @@ -0,0 +1,58 @@ +// A recursive record that declares a generic method compiles. Erasing the +// method's own type parameters does not depend on any enclosing generic +// binding, so the record still closes its recursive knot: a parent +// interface whose child is a subinterface declaring a generic method, and a +// list node whose generic method sits beside a self-referencing field. + +interface Message { + content: string; + details?: T; +} + +interface TreeNode { + name: string; + child?: SendingNode; +} + +interface SendingNode extends TreeNode { + send(message: Message): string; +} + +interface ListNode { + value: number; + next: ListNode | null; + map(f: (value: number) => U): string; +} + +function describe(node: TreeNode): string { + const inner = node.child; + return inner ? `${node.name} > ${describe(inner)}` : node.name; +} + +function leaf(name: string): SendingNode { + return { name, send: () => `${name} got a message` }; +} + +const tree: SendingNode = { + name: "root", + child: { name: "middle", child: leaf("leaf"), send: () => "middle got a message" }, + send: () => "root got a message", +}; +console.log(describe(tree)); +console.log(tree.send({ content: "hello" })); +const middle = tree.child; +if (middle !== undefined) console.log(middle.send({ content: "hi", details: 42 })); + +function cons(value: number, next: ListNode | null): ListNode { + return { value, next, map: () => `node ${value}` }; +} + +let list: ListNode | null = cons(1, cons(2, cons(3, null))); +const values: number[] = []; +let labels = ""; +while (list !== null) { + values.push(list.value); + labels += `[${list.map((v) => v * 2)}]`; + list = list.next; +} +console.log(values.join(","), labels); diff --git a/tests/corpus/recursive-optional-callback-params.ts b/tests/corpus/recursive-optional-callback-params.ts new file mode 100644 index 000000000..0c81235d6 --- /dev/null +++ b/tests/corpus/recursive-optional-callback-params.ts @@ -0,0 +1,107 @@ +// An optional parameter whose type reaches back to the interface that +// declares the method keeps its whole union. A command context's +// `newSession(options?)` takes callbacks typed by a subinterface of the +// context, so mapping the context passes through the options union while +// that union is still being mapped; the parameter still holds the options +// record, not only `undefined`. + +interface CommandContext { + cwd: string; + newSession(options?: { + parent?: string; + withSession?: (ctx: ReplacedContext) => Promise; + }): Promise<{ cancelled: boolean }>; + fork( + entryId: string, + options?: { position?: "before" | "at"; withSession?: (ctx: ReplacedContext) => Promise }, + ): Promise<{ cancelled: boolean }>; +} + +interface ReplacedContext extends CommandContext { + depth: number; + send(content: string): Promise; +} + +interface Command { + description?: string; + handler: (args: string, ctx: CommandContext) => Promise; +} + +const log: string[] = []; +const commands = new Map(); + +function register(name: string, command: Command): void { + commands.set(name, command); +} + +register("new", { + description: "start a session", + handler: async (args, ctx) => { + const first = await ctx.newSession({ + parent: args, + withSession: async (next) => { + await next.send(`hello from ${next.cwd} at depth ${next.depth}`); + const nested = await next.newSession({ + withSession: async (inner) => { + await inner.send(`nested at depth ${inner.depth}`); + }, + }); + log.push(`nested cancelled=${nested.cancelled}`); + }, + }); + log.push(`first cancelled=${first.cancelled}`); + const bare = await ctx.newSession(); + log.push(`bare cancelled=${bare.cancelled}`); + }, +}); + +register("fork", { + handler: async (args, ctx) => { + const r = await ctx.fork(args, { + position: "at", + withSession: async (next) => next.send(`forked ${args} at depth ${next.depth}`), + }); + log.push(`fork cancelled=${r.cancelled}`); + const plain = await ctx.fork(args); + log.push(`plain fork cancelled=${plain.cancelled}`); + }, +}); + +function makeContext(depth: number): ReplacedContext { + return { + cwd: `/work/${depth}`, + depth, + newSession: async (options) => { + log.push(`newSession depth=${depth} parent=${options?.parent ?? "-"}`); + if (options?.withSession) { + await options.withSession(makeContext(depth + 1)); + return { cancelled: false }; + } + return { cancelled: true }; + }, + fork: async (entryId, options) => { + log.push(`fork ${entryId} position=${options?.position ?? "-"}`); + const callback = options?.withSession; + if (callback === undefined) return { cancelled: true }; + await callback(makeContext(depth + 1)); + return { cancelled: false }; + }, + send: async (content) => { + log.push(`send: ${content}`); + }, + }; +} + +async function run(): Promise { + const root: CommandContext = makeContext(0); + for (const [name, command] of commands) { + log.push(`run ${name} (${command.description ?? "no description"})`); + await command.handler(`entry-${name}`, root); + } + for (const line of log) console.log(line); +} + +run().catch((e: unknown) => { + console.log("failed", e instanceof Error ? e.message : String(e)); + process.exitCode = 1; +}); diff --git a/tests/corpus/rest-tuple-shorthand-property.ts b/tests/corpus/rest-tuple-shorthand-property.ts new file mode 100644 index 000000000..d67afa443 --- /dev/null +++ b/tests/corpus/rest-tuple-shorthand-property.ts @@ -0,0 +1,23 @@ +// A rest parameter typed as a fixed tuple, read through a shorthand property +// (`{ xs }`), directly or through a const alias, is the tuple of the call's +// arguments. + +function wrap(...xs: [number]) { + return { xs }; +} +function pair(...args: [string, number]) { + const kept = args; + return { kept, size: args.length }; +} +function add(a: number, b: number): number { + return a + b; +} +function forward(...args: [number, number]): number { + return add(...args); +} + +const w = wrap(1); +console.log(w.xs[0], w.xs.length, JSON.stringify(w)); +const p = pair("a", 2); +console.log(p.kept[0], p.kept[1], p.size, JSON.stringify(p)); +console.log(forward(3, 4)); diff --git a/tests/corpus/speculative-loop-view-headers.ts b/tests/corpus/speculative-loop-view-headers.ts new file mode 100644 index 000000000..a1b92c1d9 --- /dev/null +++ b/tests/corpus/speculative-loop-view-headers.ts @@ -0,0 +1,36 @@ +// Speculative integer loops over typed-array views held by a const local +// and by a parameter. The fast copy, the copy a failed check resumes in and +// the ordinary copy all read and write the same views: a product that +// leaves the safe integers fails the fast copy's check partway through, +// and writes past the end are ignored. +function fill(buffer: ArrayBuffer, count: number, seed: number): number { + const view = new Float64Array(buffer); + let l = seed; + for (let i = 0; i < count; i++) { + l = (l * 3877 + 29573) % 139968; + view[i] = l; + } + return l; +} + +function grow(out: Int32Array, count: number, start: number): number { + let x = start; + for (let i = 0; i < count; i++) { + x = x * 3 + 1; + out[i] = x % 1000; + } + return x; +} + +function sum(view: Float64Array | Int32Array): number { + let s = 0; + for (const v of view) s += v; + return s; +} + +const buffer = new ArrayBuffer(8 * 64); +console.log(fill(buffer, 64, 42), sum(new Float64Array(buffer))); +console.log(fill(buffer, 100, 7), new Float64Array(buffer)[63]); +const out = new Int32Array(40); +console.log(grow(out, 60, 1), sum(out), out[0], out[39]); +console.log(grow(out, 10, 5), out.join(",")); diff --git a/tests/corpus/split-cjs-require-binding/chain-wrap.cjs b/tests/corpus/split-cjs-require-binding/chain-wrap.cjs new file mode 100644 index 000000000..435dcaeab --- /dev/null +++ b/tests/corpus/split-cjs-require-binding/chain-wrap.cjs @@ -0,0 +1,3 @@ +var chained; +chained = require("./chain.cjs"); +module.exports = chained; diff --git a/tests/corpus/split-cjs-require-binding/chain.cjs b/tests/corpus/split-cjs-require-binding/chain.cjs new file mode 100644 index 000000000..a279f456e --- /dev/null +++ b/tests/corpus/split-cjs-require-binding/chain.cjs @@ -0,0 +1 @@ +module.exports = require("./whole.cjs"); diff --git a/tests/corpus/split-cjs-require-binding/lib.cjs b/tests/corpus/split-cjs-require-binding/lib.cjs new file mode 100644 index 000000000..b318eb85c --- /dev/null +++ b/tests/corpus/split-cjs-require-binding/lib.cjs @@ -0,0 +1,5 @@ +console.log("lib: init"); +exports.n = 7; +exports.f = function (x) { + return x * 2; +}; diff --git a/tests/corpus/split-cjs-require-binding/main.cjs b/tests/corpus/split-cjs-require-binding/main.cjs new file mode 100644 index 000000000..974421bd3 --- /dev/null +++ b/tests/corpus/split-cjs-require-binding/main.cjs @@ -0,0 +1,55 @@ +console.log("main: start"); +var lib, whole; +lib = require("./lib.cjs"); +whole = require("./whole.cjs"); +console.log("main: after", lib.n, lib.f(3), whole(4)); +exports.fromLib = lib.f; +exports.whole = whole; +console.log("main: exported", exports.fromLib(5), whole(6)); +console.log("main: identity", exports.fromLib === lib.f, exports.whole === whole); +const re = require("./reexport.cjs"); +console.log("main: reexport", re(7)); +let later; +later = require("./lib.cjs"); +console.log("main: let", later.n); +const rx = require("./reexport-extra.cjs"); +console.log("main: reexport extra", rx(8), rx.extra); +const splitObject = { whole }; +console.log("main: shorthand value", splitObject.whole(10)); +var libAgain; +libAgain = require("./lib.cjs"); +const { n: splitN, f: splitF } = libAgain; +console.log("main: destructured split", splitN, splitF(4)); +var chained; +chained = require("./chain-wrap.cjs"); +console.log("main: chained", chained(11)); + +// Any later write, including a destructuring target, keeps ordinary storage. +var shorthand; +shorthand = require("./whole.cjs"); +({ shorthand } = { shorthand: "shorthand" }); +console.log("main: shorthand write", shorthand); +var renamed; +renamed = require("./whole.cjs"); +({ value: renamed } = { value: "renamed" }); +console.log("main: renamed write", renamed); +var nested; +nested = require("./whole.cjs"); +({ a: { nested } } = { a: { nested: "nested" } }); +console.log("main: nested write", nested); +var rest; +rest = require("./whole.cjs"); +({ ...rest } = { a: 1, b: 2 }); +console.log("main: rest write", rest.a, rest.b); +var array; +array = require("./whole.cjs"); +[array] = ["array"]; +console.log("main: array write", array); +var arrayRest; +arrayRest = require("./whole.cjs"); +[...arrayRest] = [1, 2]; +console.log("main: array-rest write", arrayRest[0], arrayRest[1]); +var each; +each = require("./whole.cjs"); +for ({ each } of [{ each: "each" }]) {} +console.log("main: for-of write", each); diff --git a/tests/corpus/split-cjs-require-binding/package.json b/tests/corpus/split-cjs-require-binding/package.json new file mode 100644 index 000000000..a3c15a7a6 --- /dev/null +++ b/tests/corpus/split-cjs-require-binding/package.json @@ -0,0 +1 @@ +{ "type": "commonjs" } diff --git a/tests/corpus/split-cjs-require-binding/reexport-extra.cjs b/tests/corpus/split-cjs-require-binding/reexport-extra.cjs new file mode 100644 index 000000000..584de98fe --- /dev/null +++ b/tests/corpus/split-cjs-require-binding/reexport-extra.cjs @@ -0,0 +1,6 @@ +var whole; +whole = require("./whole.cjs"); +console.log("extra: before", whole.extra, whole(9)); +module.exports = whole; +module.exports.extra = 1; +console.log("extra: after", whole.extra, module.exports.extra); diff --git a/tests/corpus/split-cjs-require-binding/reexport.cjs b/tests/corpus/split-cjs-require-binding/reexport.cjs new file mode 100644 index 000000000..9a931bdef --- /dev/null +++ b/tests/corpus/split-cjs-require-binding/reexport.cjs @@ -0,0 +1,4 @@ +console.log("reexport: init"); +var whole; +whole = require("./whole.cjs"); +module.exports = whole; diff --git a/tests/corpus/split-cjs-require-binding/whole.cjs b/tests/corpus/split-cjs-require-binding/whole.cjs new file mode 100644 index 000000000..31b476c4e --- /dev/null +++ b/tests/corpus/split-cjs-require-binding/whole.cjs @@ -0,0 +1,4 @@ +console.log("whole: init"); +module.exports = function whole(x) { + return x + 1; +}; diff --git a/tests/corpus/split-cjs-require-cycle/even.cjs b/tests/corpus/split-cjs-require-cycle/even.cjs new file mode 100644 index 000000000..94d74ea87 --- /dev/null +++ b/tests/corpus/split-cjs-require-cycle/even.cjs @@ -0,0 +1,10 @@ +var odd; +exports.name = "even"; +exports.isEven = function (n) { + return n === 0 ? true : odd.isOdd(n - 1); +}; +exports.describe = function (n) { + return n + " is " + (exports.isEven(n) ? "even" : "odd") + " (asked " + odd.name + ")"; +}; +odd = require("./odd.cjs"); +console.log("even: loaded"); diff --git a/tests/corpus/split-cjs-require-cycle/main.cjs b/tests/corpus/split-cjs-require-cycle/main.cjs new file mode 100644 index 000000000..9ec7997db --- /dev/null +++ b/tests/corpus/split-cjs-require-cycle/main.cjs @@ -0,0 +1,12 @@ +// Split require bindings (`var m; m = require("./x.cjs")`) that close a +// CommonJS cycle: each module publishes its exports before its require, and +// reads the other module's binding only in functions that run after both +// modules finish, the cycle shape the const form compiles too. One +// assignment parenthesizes its require. +console.log("main: start"); +var even, odd; +even = require("./even.cjs"); +odd = require("./odd.cjs"); +console.log(even.isEven(10), odd.isOdd(7), even.isEven(3)); +console.log(even.describe(4), "|", even.describe(9)); +console.log("odd calls", odd.calls(), even.name, odd.name); diff --git a/tests/corpus/split-cjs-require-cycle/odd.cjs b/tests/corpus/split-cjs-require-cycle/odd.cjs new file mode 100644 index 000000000..d449c28e8 --- /dev/null +++ b/tests/corpus/split-cjs-require-cycle/odd.cjs @@ -0,0 +1,12 @@ +var even; +let calls = 0; +exports.name = "odd"; +exports.isOdd = function (n) { + calls++; + return n === 0 ? false : even.isEven(n - 1); +}; +exports.calls = function () { + return calls; +}; +even = (require("./even.cjs")); +console.log("odd: loaded"); diff --git a/tests/corpus/split-cjs-require-cycle/package.json b/tests/corpus/split-cjs-require-cycle/package.json new file mode 100644 index 000000000..a3c15a7a6 --- /dev/null +++ b/tests/corpus/split-cjs-require-cycle/package.json @@ -0,0 +1 @@ +{ "type": "commonjs" } diff --git a/tests/corpus/string-last-index-of-ascii.ts b/tests/corpus/string-last-index-of-ascii.ts new file mode 100644 index 000000000..8dcf405f5 --- /dev/null +++ b/tests/corpus/string-last-index-of-ascii.ts @@ -0,0 +1,21 @@ +// lastIndexOf on ASCII receivers: every position form, empty and long +// needles, needles with non-ASCII text, and receivers built at run time. +const ids = ["bk_00042", "mb_00410", "plain", "_lead", "trail_", "a_b_c_d", ""]; +for (const id of ids) console.log(JSON.stringify(id), id.lastIndexOf("_"), Number(id.slice(id.lastIndexOf("_") + 1))); + +const text = "abcabcabc"; +const positions = [Infinity, -Infinity, NaN, -0, 0, 1, 2.9, 3, 5, 8, 9, 100, -5]; +for (const needle of ["abc", "c", "a", "", "abcabcabcabc", "cab", "x", "é", "\ud83d", "😀"]) { + const row = positions.map((position) => text.lastIndexOf(needle, position)); + console.log(JSON.stringify(needle), text.lastIndexOf(needle), row.join(",")); +} + +// Receivers built at run time (concatenation, repeat, join, slices). +const built = ["x", "y"].join("-").repeat(3) + String(42); +console.log(built, built.lastIndexOf("-"), built.lastIndexOf("x-", 3), built.lastIndexOf("42", 6)); +const slice = built.slice(2, 9); +console.log(slice, slice.lastIndexOf("y"), slice.lastIndexOf("y", 0), slice.lastIndexOf("-y-")); + +// A non-ASCII receiver keeps UTF-16 positions. +const mixed = "é_ü_😀_z"; +console.log(mixed.lastIndexOf("_"), mixed.lastIndexOf("_", 3), mixed.lastIndexOf("😀"), mixed.lastIndexOf("\ude00")); diff --git a/tests/corpus/string-plus-element-read-return.ts b/tests/corpus/string-plus-element-read-return.ts new file mode 100644 index 000000000..ec07fa3d5 --- /dev/null +++ b/tests/corpus/string-plus-element-read-return.ts @@ -0,0 +1,42 @@ +// A function declared to return a string concatenates a string with an +// element of a readonly string array. A missing element reads as undefined, +// which `+` turns into "undefined" next to a string, so the result is always +// a string and its call can be used inline. + +const PAD: readonly string[] = ["00", "01", "02"]; + +function label(i: number): string { + return "x-" + PAD[i]; +} +function date(y: number, m: number, d: number): string { + return y + "-" + PAD[m] + "-" + PAD[d]; +} +function suffix(i: number): string { + return PAD[i] + "!"; +} + +console.log(label(1).length, label(1), label(7)); +console.log(date(2026, 1, 2).length, date(2026, 2, 9)); +console.log(suffix(0).toUpperCase(), suffix(5)); + +// Two possibly missing reads add to NaN when both are missing. +function pair(i: number, j: number): string { + return PAD[i] + PAD[j]; +} +const both = pair(8, 9); +console.log(pair(0, 1), typeof both, String(both)); + +// A missing element held in a binding or returned by a function is undefined, +// so two of them still add to NaN. +function at(i: number): string { + return PAD[i]; +} +function viaBinding(i: number, j: number): string { + const head = PAD[i]; + return head + PAD[j]; +} +function viaCall(i: number, j: number): string { + return at(i) + PAD[j]; +} +console.log(viaBinding(0, 1), String(viaBinding(5, 6)), typeof viaBinding(5, 6)); +console.log(viaCall(0, 2), String(viaCall(5, 6)), viaBinding(0, 9)); diff --git a/tests/corpus/symbol-keyed-recursive-record.ts b/tests/corpus/symbol-keyed-recursive-record.ts new file mode 100644 index 000000000..c26d7282e --- /dev/null +++ b/tests/corpus/symbol-keyed-recursive-record.ts @@ -0,0 +1,35 @@ +// A recursive record keyed by unique symbols, built from literals whose +// child arrays start empty, holds the nodes later pushed into those arrays. + +const kValue: unique symbol = Symbol("value"); +const kChildren: unique symbol = Symbol("children"); + +interface TreeNode { + [kChildren]: TreeNode[]; + [kValue]: number; +} + +function node(value: number): TreeNode { + return { [kValue]: value, [kChildren]: [] }; +} + +const root = node(0); +root[kChildren].push(node(1)); +root[kChildren].push(node(2)); +root[kChildren][0]![kChildren].push(node(3)); +console.log(root[kChildren].length, root[kChildren][0]![kValue], root[kChildren][1]![kValue]); + +function sum(n: TreeNode): number { + let total = n[kValue]; + for (const child of n[kChildren]) total += sum(child); + return total; +} +function depth(n: TreeNode): number { + return 1 + Math.max(0, ...n[kChildren].map(depth)); +} +console.log(sum(root), depth(root)); + +// A literal that starts with children. +const seeded: TreeNode = { [kValue]: 10, [kChildren]: [node(11)] }; +seeded[kChildren].push(node(12)); +console.log(seeded[kChildren].map((c) => c[kValue]).join(","), sum(seeded)); diff --git a/tests/corpus/symbol-property-key-reuse.mjs b/tests/corpus/symbol-property-key-reuse.mjs new file mode 100644 index 000000000..f4fc2a0c4 --- /dev/null +++ b/tests/corpus/symbol-property-key-reuse.mjs @@ -0,0 +1,69 @@ +// Symbol-keyed properties read and written many times: on plain objects, on +// prototypes and class instances, with Symbol.for symbols, and with symbols +// created in a loop (each a distinct key, even after earlier ones are +// unreachable), plus the same symbol converted to a checked value repeatedly. +const tag = Symbol("tag"); +const shared = Symbol.for("shared"); +const proto = { [tag]: "from proto" }; +const child = Object.create(proto); +child[shared] = 0; +for (let i = 0; i < 1000; i++) child[shared] += i; +console.log(child[tag], child[shared], Object.getOwnPropertySymbols(child).length); + +const count = Symbol("count"); +class Counter { + constructor() { + this[count] = 0; + } + bump() { + this[count]++; + return this; + } + get count() { + return this[count]; + } +} +const c = new Counter(); +for (let i = 0; i < 500; i++) c.bump(); +console.log(c.count, c[count], count.toString()); + +const keys = []; +const holder = {}; +for (let i = 0; i < 300; i++) { + const key = Symbol(`k${i}`); + holder[key] = i; + if (i % 100 === 0) keys.push(key); +} +let sum = 0; +for (const key of Object.getOwnPropertySymbols(holder)) sum += holder[key]; +console.log(sum, keys.map((k) => holder[k]).join(), keys[1].description); + +// Symbols that become unreachable between rounds: a new symbol never reads +// a property stored under an earlier one. +const scratch = {}; +let stale = 0; +for (let round = 0; round < 200; round++) { + const key = Symbol("round"); + if (scratch[key] !== undefined) stale++; + scratch[key] = round; +} +console.log(stale, Object.getOwnPropertySymbols(scratch).length); + +// A symbol stored, deleted and dropped each round, so its storage can be +// reused by the next round's symbol: the new symbol still reads nothing. +const persist = {}; +let reused = 0; +for (let round = 0; round < 200; round++) { + const key = Symbol("dropped"); + if (key in persist) reused++; + persist[key] = round; + delete persist[key]; +} +console.log(reused, Object.getOwnPropertySymbols(persist).length); + +const viaFor = {}; +for (let i = 0; i < 10; i++) viaFor[Symbol.for("same")] = i; +console.log(viaFor[Symbol.for("same")], Object.getOwnPropertySymbols(viaFor).length); +console.log(Symbol.for("same") === Symbol.for("same"), Symbol("x") === Symbol("x"), tag === tag); +const boxes = [tag, tag, shared]; +console.log(boxes[0] === boxes[1], boxes.indexOf(shared), new Set(boxes).size); diff --git a/tests/corpus/text-decoder-undefined-input.ts b/tests/corpus/text-decoder-undefined-input.ts new file mode 100644 index 000000000..32bca7f3a --- /dev/null +++ b/tests/corpus/text-decoder-undefined-input.ts @@ -0,0 +1,38 @@ +// TextDecoder.decode(undefined) decodes no bytes, as decode() does: it +// returns "" and, after streamed input, flushes the pending bytes. +const decoder = new TextDecoder(); +const encoded = new TextEncoder().encode("bytes"); +const inputs = new Map([ + ["present", encoded], + ["absent", undefined], +]); + +console.log(JSON.stringify(decoder.decode(undefined))); +console.log(JSON.stringify(decoder.decode(inputs.get("present"))), JSON.stringify(decoder.decode(inputs.get("absent")))); +console.log(JSON.stringify(decoder.decode(inputs.get("absent")!)), JSON.stringify(decoder.decode(inputs.get("missing")!))); +console.log(JSON.stringify(new TextDecoder("utf-8").decode(undefined))); +console.log(JSON.stringify(new TextDecoder("utf-8").decode(inputs.get("absent")))); + +let calls = 0; +function nothing(): undefined { + calls++; + return undefined; +} +console.log(JSON.stringify(decoder.decode(nothing())), calls); + +interface Chunk { + done: boolean; + value?: Uint8Array; +} +const chunks: Chunk[] = [{ done: false, value: new Uint8Array([0xe2, 0x82]) }, { done: true }]; +const streaming = new TextDecoder(); +let text = ""; +for (const chunk of chunks) text += streaming.decode(chunk.value, { stream: !chunk.done }); +console.log(JSON.stringify(text), text.length); + +// Other typed arrays decode their bytes, with or without the assertion. +const wide = new Map([["present", new Uint16Array([0x6968])]]); +const asserted = wide.get("present")!; +console.log(JSON.stringify(decoder.decode(asserted)), JSON.stringify(decoder.decode(wide.get("present")!))); +console.log(JSON.stringify(decoder.decode(wide.get("present"))), JSON.stringify(decoder.decode(wide.get("absent")))); +console.log(JSON.stringify(new TextDecoder("utf-8").decode(wide.get("absent")!))); diff --git a/tests/corpus/typed-array-length-after-release.ts b/tests/corpus/typed-array-length-after-release.ts new file mode 100644 index 000000000..81fad4c77 --- /dev/null +++ b/tests/corpus/typed-array-length-after-release.ts @@ -0,0 +1,56 @@ +// A typed array's length and byteLength read just before the view's last +// reference goes away: the values stay those of the old view after a field +// replaces it, a map entry is deleted, or a local is rebound. +function grow(slots: Int32Array): Int32Array { + const next = new Int32Array(slots.length * 2); + next.set(slots); + return next; +} + +class Table { + slots: Int32Array = new Int32Array(4); + count = 0; + add(value: number): number { + const slots = this.slots; + slots[this.count % slots.length] = value; + this.count++; + if (this.count === slots.length) this.slots = grow(slots); + return this.slots.length; + } + swap(): number { + const slots = this.slots; + this.slots = new Int32Array(1); + return slots.length; + } + swapBytes(): number { + const slots = this.slots; + const bytes = slots.byteLength; + this.slots = new Int32Array(2); + return bytes + slots.length; + } +} + +const table = new Table(); +let size = 0; +for (let i = 0; i < 9; i++) size = table.add(i); +console.log(size, table.swap(), table.slots.length, table.add(5)); +console.log(table.swapBytes(), table.slots.length); + +const views = new Map(); +views.set("a", new Float64Array(6)); +function take(key: string): number { + const view = views.get(key); + if (view === undefined) return -1; + const length = view.length; + views.delete(key); + return length + view.byteLength; +} +console.log(take("a"), take("a"), views.size); + +let current = new Uint8Array(3); +function rebind(): number { + const old = current; + current = new Uint8Array(old.length + 1); + return old.length * 10 + current.length; +} +console.log(rebind(), rebind(), current.length); diff --git a/tests/corpus/typed-array-masked-index-stores.ts b/tests/corpus/typed-array-masked-index-stores.ts new file mode 100644 index 000000000..88a25b4c5 --- /dev/null +++ b/tests/corpus/typed-array-masked-index-stores.ts @@ -0,0 +1,40 @@ +// Several typed array accesses in one counted loop share an index that is +// not an affine function of the counter (a masked or remaindered counter). +// The loop versioning analysis records that such an index has no affine +// shape and reads the recorded result again at each later access. +const length = 64; +const bytes = new Uint8Array(length); +const words = new Int32Array(16); +const totals = new Float64Array(16); + +for (let i = 0; i < bytes.length; i++) { + const slot = i & 15; + words[slot] = words[slot]! + i; + totals[slot] = totals[slot]! + i * 0.5; + bytes[i] = slot; +} +console.log(Array.from(words).join(",")); +console.log(Array.from(totals).join(",")); +console.log(bytes.reduce((sum, value) => sum + value, 0)); + +const ring = new Uint16Array(8); +const seen = new Uint8Array(8); +for (let i = 0; i < length; i++) { + const at = i % 8; + ring[at] = ring[at]! * 3 + i; + seen[at] = seen[at]! + 1; +} +console.log(Array.from(ring).join(","), Array.from(seen).join(",")); + +function scatter(source: Uint8Array, mask: number): Uint32Array { + const out = new Uint32Array(mask + 1); + const counts = new Uint32Array(mask + 1); + for (let i = 0; i < source.length; i++) { + const bucket = (source[i]! + i) & mask; + out[bucket] = out[bucket]! + source[i]!; + counts[bucket] = counts[bucket]! + 1; + } + for (let i = 0; i < out.length; i++) out[i] = out[i]! * 10 + counts[i]!; + return out; +} +console.log(Array.from(scatter(bytes, 7)).join(",")); diff --git a/tests/corpus/typeof-ambient-undefined-names.ts b/tests/corpus/typeof-ambient-undefined-names.ts new file mode 100644 index 000000000..94713319e --- /dev/null +++ b/tests/corpus/typeof-ambient-undefined-names.ts @@ -0,0 +1,21 @@ +// typeof on names the program declares (`declare const`, `declare function`) +// but nothing defines at run time answers "undefined"; reading them throws +// ReferenceError. +declare const Bun: { randomUUIDv7: () => string } | undefined; +declare const Deno: { version: { deno: string } }; +declare function webkitRequestAnimationFrame(callback: () => void): number; +console.log("start"); +console.log(typeof Bun, typeof Deno, typeof webkitRequestAnimationFrame); +console.log(typeof Bun !== "undefined" ? Bun?.randomUUIDv7() : "no Bun"); +const runtime = typeof Deno === "object" ? "deno" : typeof Bun === "object" ? "bun" : "node"; +console.log(runtime); +try { + console.log(Bun); +} catch (error) { + console.log((error as Error).name, (error as Error).message); +} +try { + console.log(typeof Deno.version); +} catch (error) { + console.log((error as Error).name, (error as Error).message); +} diff --git a/tests/corpus/undeclared-key-optional-chain/main.cjs b/tests/corpus/undeclared-key-optional-chain/main.cjs new file mode 100644 index 000000000..de7b3351a --- /dev/null +++ b/tests/corpus/undeclared-key-optional-chain/main.cjs @@ -0,0 +1,11 @@ +// A package typed from its JavaScript returns profiles without a key its +// declarations promise ("a;b"). An optional read of that key answers +// undefined for a profile, and for a missing one (the chain reads the +// nullish receiver as undefined), in presence tests and in values. +const { loadProfiles } = require("hostilekit"); +const profiles = loadProfiles(); +const present = profiles[0]; +const missing = profiles[5]; +console.log(present?.["a;b"] === undefined, present?.["a;b"] ?? "none", typeof present?.["a;b"]); +console.log(missing?.["a;b"] === undefined, missing?.["a;b"] ?? "none", typeof missing?.["a;b"]); +console.log(present["a;b"] === undefined, present.id); diff --git a/tests/corpus/undeclared-key-optional-chain/node_modules/hostilekit/index.d.ts b/tests/corpus/undeclared-key-optional-chain/node_modules/hostilekit/index.d.ts new file mode 100644 index 000000000..4bf22dfea --- /dev/null +++ b/tests/corpus/undeclared-key-optional-chain/node_modules/hostilekit/index.d.ts @@ -0,0 +1,2 @@ +export type Profile = { id: number; "a;b": string }; +export declare function loadProfiles(): Profile[]; diff --git a/tests/corpus/undeclared-key-optional-chain/node_modules/hostilekit/index.js b/tests/corpus/undeclared-key-optional-chain/node_modules/hostilekit/index.js new file mode 100644 index 000000000..316501256 --- /dev/null +++ b/tests/corpus/undeclared-key-optional-chain/node_modules/hostilekit/index.js @@ -0,0 +1,5 @@ +"use strict"; +function loadProfiles() { + return [{ id: 1 }]; +} +exports.loadProfiles = loadProfiles; diff --git a/tests/corpus/undeclared-key-optional-chain/node_modules/hostilekit/package.json b/tests/corpus/undeclared-key-optional-chain/node_modules/hostilekit/package.json new file mode 100644 index 000000000..2fdbc5b41 --- /dev/null +++ b/tests/corpus/undeclared-key-optional-chain/node_modules/hostilekit/package.json @@ -0,0 +1 @@ +{ "name": "hostilekit", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" } diff --git a/tests/corpus/union-clone-empty-array-overrides.ts b/tests/corpus/union-clone-empty-array-overrides.ts new file mode 100644 index 000000000..98ddac59c --- /dev/null +++ b/tests/corpus/union-clone-empty-array-overrides.ts @@ -0,0 +1,92 @@ +// Spreading a union of records and overriding a shared field with an empty +// array literal keeps each value's own variant: the `[]` (typed `never[]`) +// becomes an empty array of the selected arm's element type, also when that +// arm's field is a union with one array arm (`string | Part[]`). + +interface TextPart { + type: "text"; + text: string; +} +interface ImagePart { + type: "image"; + data: string; +} +interface UserEntry { + role: "user"; + content: string | (TextPart | ImagePart)[]; + timestamp: number; +} +interface AssistantEntry { + role: "assistant"; + content: TextPart[]; + model: string; + timestamp: number; +} +interface ToolEntry { + role: "tool"; + toolCallId: string; + content: (TextPart | ImagePart)[]; + isError: boolean; + timestamp: number; +} +interface SystemEntry { + role: "system"; + content: string; + timestamp: number; +} +interface NoteEntry { + role: "note"; + text: string; + timestamp: number; +} +type Entry = UserEntry | AssistantEntry | ToolEntry | SystemEntry | NoteEntry; + +function repair(entry: Entry, clear: boolean): Entry[] { + if (entry.role === "system" && clear) return [{ ...entry, content: "" }]; + if ((entry.role === "user" || entry.role === "assistant" || entry.role === "tool") && clear) { + return [{ ...entry, content: [] }]; + } + return [entry]; +} + +interface Circle { + kind: "circle"; + tags: string[]; + radius: number; +} +interface Square { + kind: "square"; + tags: string[]; + side: number; +} +type Shape = Circle | Square; + +function cleared(shape: Shape): Shape { + return { ...shape, tags: [] }; +} + +const entries: Entry[] = [ + { role: "user", content: [{ type: "text", text: "hello" }], timestamp: 1 }, + { role: "assistant", content: [{ type: "text", text: "hi" }], model: "m", timestamp: 2 }, + { role: "tool", toolCallId: "t1", content: [{ type: "image", data: "AA==" }], isError: false, timestamp: 3 }, + { role: "note", text: "hi", timestamp: 4 }, + { role: "system", content: "be brief", timestamp: 5 }, + { role: "user", content: "kept", timestamp: 6 }, +]; +entries.forEach((entry, index) => { + for (const fixed of repair(entry, index % 2 === 0 || entry.role === "system")) { + console.log(fixed.role, JSON.stringify(fixed), fixed === entry); + } +}); +console.log(JSON.stringify(entries[0])); + +const shapes: Shape[] = [ + { kind: "circle", tags: ["round", "red"], radius: 2 }, + { kind: "square", tags: ["blue"], side: 3 }, +]; +for (const shape of shapes) { + const copy = cleared(shape); + copy.tags.push("fresh"); + const area = copy.kind === "circle" ? copy.radius * copy.radius * 3 : copy.side * copy.side; + console.log(copy.kind, area, copy.tags.join("+"), shape.tags.join("+"), copy !== shape); +} diff --git a/tests/corpus/union-promise-function-arm-widening.ts b/tests/corpus/union-promise-function-arm-widening.ts new file mode 100644 index 000000000..d3b8f618d --- /dev/null +++ b/tests/corpus/union-promise-function-arm-widening.ts @@ -0,0 +1,77 @@ +// A promise or function value flowing into a union whose only arm of that +// kind has a wider payload or result: the value converts into that arm. +// Promises keep their identity under === and !== against the union. + +const chains = new Map>(); + +async function serialize(key: string, task: () => Promise): Promise { + const previous = chains.get(key) ?? Promise.resolve(); + const queued = (async () => { + await previous.catch(() => {}); + return task(); + })(); + const tail = queued.catch(() => {}); + chains.set(key, tail); + void tail.then(() => { + if (chains.get(key) === tail) chains.delete(key); + console.log(`settled ${key}: ${chains.get(key) !== tail}`); + }); + return queued; +} + +interface Store { + pending?: Promise; + lookup?: (name: string) => Promise; + scale?: (n: number) => number | string; +} + +async function findName(name: string): Promise { + return name.length > 3 ? name.toUpperCase() : null; +} + +function double(n: number): number { + return n * 2; +} + +async function failing(): Promise { + throw new Error("task failed"); +} + +async function main(): Promise { + const a = await serialize("alpha", async () => 1); + const b = await serialize("alpha", async () => "two"); + const c = await serialize("beta", async (): Promise => true); + console.log(a, b, c, chains.size); + try { + await serialize("alpha", failing); + } catch (e) { + console.log("caught", (e as Error).message); + } + for (let i = 0; i < 4; i++) await Promise.resolve(); + console.log("remaining", chains.size); + + const store: Store = { lookup: findName, scale: double }; + const work: Promise = Promise.resolve(41); + store.pending = work; + console.log(store.pending === work, store.pending !== work); + const other: Promise = Promise.resolve(41); + console.log(store.pending === other); + console.log(await store.pending); + + // One promise in two optional slots: each holds its own conversion. + const second: Store = {}; + second.pending = work; + console.log(store.pending === second.pending, store.pending !== second.pending); + const slots: Store[] = [store, second, { pending: other }]; + console.log(slots.map((slot) => slot.pending === slots[0]!.pending).join(",")); + second.lookup = findName; + console.log(store.lookup === second.lookup); + + const lookup = store.lookup; + if (lookup) console.log(await lookup("abcd"), await lookup("ab")); + console.log(store.scale?.(21)); + const empty: Store = {}; + console.log(empty.pending === work, empty.lookup === undefined, empty.scale?.(1)); +} + +main(); diff --git a/tests/corpus/union-record-kept-fields-arm.ts b/tests/corpus/union-record-kept-fields-arm.ts new file mode 100644 index 000000000..69887cf49 --- /dev/null +++ b/tests/corpus/union-record-kept-fields-arm.ts @@ -0,0 +1,56 @@ +// A record flowing into a discriminated union where one record arm holds it +// by dropping fields and another keeps every field but declares an optional +// field the record lacks. The record's discriminant selects the arm that keeps +// its fields. + +type Loc = { file: string; start: number }; + +type Expr = + | { kind: "ref"; type: number; loc: Loc } + | { + kind: "closure"; + fnName: string; + captures: string[]; + adapts?: true; + type: number; + loc: Loc; + } + | { kind: "call"; callee: string; args: Expr[]; type: number; loc: Loc }; + +function closureOf(name: string, loc: Loc): Expr { + const closure = { + kind: "closure" as const, + fnName: name, + captures: [], + type: 1, + loc, + }; + return closure; +} + +function describe(expr: Expr): string { + if (expr.kind === "closure") + return `closure ${expr.fnName}/${expr.captures.length} ${expr.adapts ?? "-"}`; + if (expr.kind === "call") + return `call ${expr.callee}(${expr.args.map(describe).join(", ")})`; + return `ref ${expr.type}`; +} + +const loc: Loc = { file: "a.ts", start: 3 }; +const first = closureOf("pick", loc); +console.log(first.kind, describe(first), JSON.stringify(first)); + +const exprs: Expr[] = [first]; +const captured = { + kind: "closure" as const, + fnName: "inner", + captures: ["x"], + type: 2, + loc, +}; +exprs.push(captured); +exprs.push({ kind: "ref", type: 7, loc }); +console.log( + describe({ kind: "call", callee: "run", args: exprs, type: 0, loc }), +); +console.log(exprs.map((e) => e.kind).join(",")); diff --git a/tests/corpus/union-record-kept-fields-no-discriminant.ts b/tests/corpus/union-record-kept-fields-no-discriminant.ts new file mode 100644 index 000000000..45370f152 --- /dev/null +++ b/tests/corpus/union-record-kept-fields-no-discriminant.ts @@ -0,0 +1,39 @@ +// A record flowing into a union without a discriminant where one record arm +// holds it by dropping fields and another keeps every field but declares an +// optional field the record lacks. The record converts into the arm that +// keeps its fields, and the optional field stays absent. + +type Loc = { file: string; start: number }; + +type Slot = + | { type: number; loc: Loc } + | { + fnName: string; + captures: string[]; + adapts?: true; + type: number; + loc: Loc; + }; + +const loc: Loc = { file: "a.ts", start: 3 }; + +function slotOf(name: string): Slot { + const slot = { fnName: name, captures: [], type: 1, loc }; + return slot; +} + +const slot = slotOf("stored"); +console.log("fnName" in slot, "adapts" in slot, JSON.stringify(slot)); +if ("fnName" in slot) + console.log(slot.fnName, slot.adapts, slot.captures.length); + +function describe(value: Slot): string { + return "fnName" in value + ? `${value.fnName}/${value.captures.length}` + : `type ${value.type}`; +} + +const inner = { fnName: "inner", captures: ["x"], type: 2, loc }; +const slots: Slot[] = [slot, { type: 7, loc }]; +slots.push(inner); +console.log(slots.map(describe).join(", "), JSON.stringify(slots[2])); diff --git a/tests/corpus/union-record-lossless-arm.ts b/tests/corpus/union-record-lossless-arm.ts new file mode 100644 index 000000000..b90d5486e --- /dev/null +++ b/tests/corpus/union-record-lossless-arm.ts @@ -0,0 +1,55 @@ +// A record flowing into a union where several record arms can hold it by a +// width conversion: the arm that keeps every one of its fields is the +// value's own layout, widened, and wins over arms that drop fields. + +interface FastOpts { + fast?: boolean; + strict?: boolean; +} +interface DepthOpts { + depth?: number; +} +interface StrictOpts { + strict?: boolean; +} + +interface Wide { + id: string; + window: number; + kind?: string; + limit: number; + opts?: FastOpts | DepthOpts | StrictOpts; +} + +interface Judge { + id: string; + window: number; + kind: string; + opts?: StrictOpts; +} + +interface Narrow { + id: string; + window: number; + kind?: string; + limit: number; + opts?: FastOpts | DepthOpts; +} + +function show(entry: Wide | Judge): string { + const limit = "limit" in entry ? ` limit=${entry.limit}` : ""; + return `${entry.kind ?? "wide"} ${entry.id} window=${entry.window}${limit} ${JSON.stringify(entry.opts ?? null)}`; +} + +const narrow: Narrow = { id: "n1", window: 4, limit: 2, opts: { depth: 3 } }; +console.log(show(narrow)); + +const entries: (Wide | Judge)[] = [narrow, { id: "j1", window: 1, kind: "judge", opts: { strict: true } }]; +const plain: Narrow = { id: "n2", window: 8, kind: "custom", limit: 5 }; +entries.push(plain); +for (const entry of entries) console.log(show(entry)); + +let latest: Wide | Judge | undefined; +for (const item of [narrow, plain]) latest = item; +console.log(latest === undefined ? "none" : show(latest)); + diff --git a/tests/corpus/union-tuple-element-read.ts b/tests/corpus/union-tuple-element-read.ts new file mode 100644 index 000000000..141609d2b --- /dev/null +++ b/tests/corpus/union-tuple-element-read.ts @@ -0,0 +1,45 @@ +// Literal-index reads from a union of tuples whose element types differ: +// each tuple's own element is read, and the checker's narrowing of the +// tuple (or of the element) applies as usual. + +type Ok = readonly [true, T]; +type Err = readonly [false, E]; +type Result = Ok | Err; + +function pick(n: number): Result { + return n > 0 ? [true, n] : [false, "neg"]; +} + +for (const n of [1, -1, 5]) { + const r = pick(n); + console.log(r[0], r[1]); + if (r[0]) console.log(r[1] + 1); + else console.log(r[1].length); +} + +function describe(r: Result): string { + const value = r[1]; + return `${r[0] ? "ok" : "err"}:${typeof value}:${value}`; +} +console.log(describe(pick(2)), describe(pick(0))); + +function isOk(r: Result): boolean { + return r[0]; +} +function payload(r: Result): T | E { + return r[1]; +} +console.log(isOk(pick(3)), isOk(pick(-3)), payload(pick(4)), payload(pick(-4))); + +type Triple = readonly [string, number, boolean] | readonly [number, string, boolean]; +const triples: Triple[] = [ + ["a", 1, true], + [2, "b", false], +]; +for (const t of triples) console.log(typeof t[0], typeof t[1], t[2]); + +// A read through a single tuple view of the union. +function flag(r: Result): boolean { + return (r as readonly [boolean, T | E])[0]; +} +console.log(flag(pick(1)), flag(pick(-1))); diff --git a/tests/corpus/var-object-filled-later.js b/tests/corpus/var-object-filled-later.js new file mode 100644 index 000000000..009d8b54d --- /dev/null +++ b/tests/corpus/var-object-filled-later.js @@ -0,0 +1,48 @@ +// A function-scoped `var` initialized with an object literal that later +// assignments fill in: tsc types the binding by the members those +// assignments add, but the object only gains them after its initializer. +"use strict"; +function load(id) { + var p = {}; + console.log(p.count, "count" in p, Object.keys(p).length); + p.id = id; + p.count = id * 2; + p.label = "item-" + id; + return p; +} +const p = load(7); +console.log(p.id + p.count, p.label.toUpperCase(), JSON.stringify(p), Object.keys(p).join(",")); + +function partial(name) { + var entry = { name: name }; + entry.size = name.length; + entry.tags = [name, name + "!"]; + return entry; +} +const e = partial("abc"); +console.log(e.size * 10, e.tags.join("|"), JSON.stringify(e)); + +function nested(depth) { + var node = {}; + node.depth = depth; + node.child = depth > 0 ? nested(depth - 1) : null; + return node; +} +console.log(JSON.stringify(nested(2))); + +function rows(n) { + var out = []; + for (var i = 0; i < n; i++) { + var row = {}; + row.index = i; + row.square = i * i; + out.push(row); + } + return out; +} +console.log(rows(4).map(function (r) { return r.index + ":" + r.square; }).join(" ")); + +var top = {}; +top.ready = true; +top.total = rows(3).reduce(function (s, r) { return s + r.square; }, 0); +console.log(top.ready, top.total, JSON.stringify(top)); diff --git a/tests/corpus/weak-keys-freed-reused.mjs b/tests/corpus/weak-keys-freed-reused.mjs new file mode 100644 index 000000000..b24823526 --- /dev/null +++ b/tests/corpus/weak-keys-freed-reused.mjs @@ -0,0 +1,32 @@ +// Weak collection keys that become unreachable leave the collection, and +// new objects allocated afterwards (which can reuse the freed storage) are +// not found in it: plain objects, arrays, class instances and functions, +// alongside many objects that were never keys. +class Point { + constructor(x) { + this.x = x; + } +} +const map = new WeakMap(); +const set = new WeakSet(); +const kept = []; +function fill(round) { + for (let i = 0; i < 200; i++) { + const key = i % 4 === 0 ? { i } : i % 4 === 1 ? [i] : i % 4 === 2 ? new Point(i) : () => i; + map.set(key, round * 1000 + i); + set.add(key); + if (i % 50 === 0) kept.push(key); + } +} +let stale = 0; +let found = 0; +for (let round = 0; round < 20; round++) { + fill(round); + // Objects that were never keys, freed and reallocated in between. + for (let i = 0; i < 500; i++) { + const other = i % 2 ? { other: i } : new Point(-i); + if (map.has(other) || set.has(other)) stale++; + } +} +for (const key of kept) if (map.has(key) && set.has(key)) found++; +console.log(stale, found, kept.length, map.get(kept[0]), map.get(kept[kept.length - 1])); diff --git a/tests/coverage-fixtures/cjs-mutable-exports/main.js b/tests/coverage-fixtures/cjs-mutable-exports/main.js new file mode 100644 index 000000000..ac16cc4ec --- /dev/null +++ b/tests/coverage-fixtures/cjs-mutable-exports/main.js @@ -0,0 +1,5 @@ +const reassigned = require('./reassigned'); +const written = require('./written'); +const redeclared = require('./redeclared'); +const stable = require('./stable'); +console.log(reassigned.n, written.tab, redeclared.twice, stable.n); diff --git a/tests/coverage-fixtures/cjs-mutable-exports/package.json b/tests/coverage-fixtures/cjs-mutable-exports/package.json new file mode 100644 index 000000000..a3c15a7a6 --- /dev/null +++ b/tests/coverage-fixtures/cjs-mutable-exports/package.json @@ -0,0 +1 @@ +{ "type": "commonjs" } diff --git a/tests/coverage-fixtures/cjs-mutable-exports/reassigned.js b/tests/coverage-fixtures/cjs-mutable-exports/reassigned.js new file mode 100644 index 000000000..6c77f3b43 --- /dev/null +++ b/tests/coverage-fixtures/cjs-mutable-exports/reassigned.js @@ -0,0 +1,4 @@ +// Reassigned after the export: Node's importers keep the first object. +var counter = { n: 1 }; +module.exports = counter; +counter = { n: 2 }; diff --git a/tests/coverage-fixtures/cjs-mutable-exports/redeclared.js b/tests/coverage-fixtures/cjs-mutable-exports/redeclared.js new file mode 100644 index 000000000..3150903f6 --- /dev/null +++ b/tests/coverage-fixtures/cjs-mutable-exports/redeclared.js @@ -0,0 +1,4 @@ +// Declared twice: the second declaration assigns the binding again. +var twice = 1; +module.exports = { twice }; +var twice = 2; diff --git a/tests/coverage-fixtures/cjs-mutable-exports/stable.js b/tests/coverage-fixtures/cjs-mutable-exports/stable.js new file mode 100644 index 000000000..c2846bbc9 --- /dev/null +++ b/tests/coverage-fixtures/cjs-mutable-exports/stable.js @@ -0,0 +1,3 @@ +// Initialized before the export and never assigned. +var stable = { n: 3 }; +module.exports = stable; diff --git a/tests/coverage-fixtures/cjs-mutable-exports/written.js b/tests/coverage-fixtures/cjs-mutable-exports/written.js new file mode 100644 index 000000000..8e0c29cd2 --- /dev/null +++ b/tests/coverage-fixtures/cjs-mutable-exports/written.js @@ -0,0 +1,4 @@ +// Written by a function: the table holds the value at the export. +let tab = ' '; +function setTab(next) { tab = next; } +module.exports = { tab, setTab }; diff --git a/tests/coverage-fixtures/cjs-namespace-reexports/impl.cjs b/tests/coverage-fixtures/cjs-namespace-reexports/impl.cjs new file mode 100644 index 000000000..3172d6685 --- /dev/null +++ b/tests/coverage-fixtures/cjs-namespace-reexports/impl.cjs @@ -0,0 +1,6 @@ +"use strict"; +exports.version = 1; +exports.current = null; +exports.set = (value) => { + exports.current = value; +}; diff --git a/tests/coverage-fixtures/cjs-namespace-reexports/main.mjs b/tests/coverage-fixtures/cjs-namespace-reexports/main.mjs new file mode 100644 index 000000000..2ce528d48 --- /dev/null +++ b/tests/coverage-fixtures/cjs-namespace-reexports/main.mjs @@ -0,0 +1,7 @@ +// A namespace import of a CommonJS wrapper that re-exports another module: +// Node's namespace keeps impl.cjs's values from the end of its evaluation, +// so `ns.current` reads null after `ns.set(5)`. +import * as ns from "./wrapper.cjs"; + +ns.set(5); +console.log(ns.current, ns.version); diff --git a/tests/coverage-fixtures/cjs-namespace-reexports/package.json b/tests/coverage-fixtures/cjs-namespace-reexports/package.json new file mode 100644 index 000000000..a3c15a7a6 --- /dev/null +++ b/tests/coverage-fixtures/cjs-namespace-reexports/package.json @@ -0,0 +1 @@ +{ "type": "commonjs" } diff --git a/tests/coverage-fixtures/cjs-namespace-reexports/wrapper.cjs b/tests/coverage-fixtures/cjs-namespace-reexports/wrapper.cjs new file mode 100644 index 000000000..ab637bab5 --- /dev/null +++ b/tests/coverage-fixtures/cjs-namespace-reexports/wrapper.cjs @@ -0,0 +1,2 @@ +"use strict"; +module.exports = require("./impl.cjs"); diff --git a/tests/coverage-fixtures/localized-failure.ts b/tests/coverage-fixtures/localized-failure.ts new file mode 100644 index 000000000..04a5b8037 --- /dev/null +++ b/tests/coverage-fixtures/localized-failure.ts @@ -0,0 +1,68 @@ +// The localized-failure report of `scriptc coverage --explain-dynamic`: +// type failures that would become checked dynamic slots, and the uses +// each one rejects today. +interface Tool { + name: string; + hooks: Map string> | AbortSignal | (() => void) | Set; +} +type Progress = (done: number, total: number) => void; +type Callbacks = Progress | AbortSignal | Map string> | Set; + +// A field: rejects the class, so every use of an instance fails. +class Session { + private tools: Tool[] = []; + count(): number { + return this.tools.length; + } + label(): string { + return "s"; + } +} + +// A parameter: rejects the function, so every call fails. +function listAll(dir: string, progress?: Callbacks): number { + return dir.length; +} + +// The dyn class field policy. +class Holder { + state: any = {}; + get(): number { + return 1; + } +} + +// A local: rejects its declaration statement, so its uses fail. +function pick(n: number): boolean { + const callbacks: Callbacks | undefined = n > 1 ? new Set() : undefined; + return callbacks === undefined; +} + +// Refused: a value the compiler cannot produce stays an error. +class Cache { + tokens: WeakRef = new WeakRef({}); + size(): number { + return 0; + } +} + +const s = new Session(); +console.log(s.count(), s.label()); +console.log(listAll("abc")); +console.log(new Holder().get()); +console.log(pick(1), new Cache().size()); + +// A class extending one the dyn class field policy rejects: its rejection +// and the uses of the derived class are that field's cascade. +class Base { + state: any = {}; + get(): number { + return 1; + } +} +class Derived extends Base { + twice(): number { + return 2; + } +} +console.log(new Derived().twice()); diff --git a/tests/coverage-fixtures/no-divergences.ts b/tests/coverage-fixtures/no-divergences.ts index 3739336e3..70409d319 100644 --- a/tests/coverage-fixtures/no-divergences.ts +++ b/tests/coverage-fixtures/no-divergences.ts @@ -1,6 +1,6 @@ // Ordinary code: identity-preserving calls, declaration-ordered literals, -// Latin-script comparisons, related-class casts, ISO dates and views of the -// global object report no divergence. +// Latin-script comparisons, related-class casts, ISO dates, views of the +// global object and a dictionary held as `{}` report no divergence. interface Item { name: string; qty: number; @@ -38,3 +38,17 @@ const COUNT = Symbol.for("no-divergences.count"); const globalState = globalThis as { [COUNT]?: { n: number } }; globalState[COUNT] = { n: 1 }; console.log(globalState[COUNT]?.n); + +// A dictionary held as `{}` comes back out as itself. +const counts: Record = { a: "x" }; +const opaque: {} = counts; +const again: Record = opaque; +again[counts["a"] ?? "a"] = "y"; +console.log(JSON.stringify(counts)); + +// A record stored in a dictionary binding and written through it stays +// the same object. +const named: { first: string } = { first: "a" }; +const byName: Record = named; +byName[named.first] = "b"; +console.log(JSON.stringify(named)); diff --git a/tests/coverage-fixtures/node-islands-config/main.ts b/tests/coverage-fixtures/node-islands-config/main.ts new file mode 100644 index 000000000..5bacea88a --- /dev/null +++ b/tests/coverage-fixtures/node-islands-config/main.ts @@ -0,0 +1,4 @@ +// Unknown values in scriptc.packages or scriptc.nodeIslands are SC8004: a +// build warns about them and goes on (it does not use the settings), and +// coverage reports no Node islands until they are valid. +console.log("configured"); diff --git a/tests/diagnostics/node-islands-config/package.json b/tests/coverage-fixtures/node-islands-config/package.json similarity index 100% rename from tests/diagnostics/node-islands-config/package.json rename to tests/coverage-fixtures/node-islands-config/package.json diff --git a/tests/coverage-fixtures/startup-fences.js b/tests/coverage-fixtures/startup-fences.js index 22aad4e0a..5e1d00b62 100644 --- a/tests/coverage-fixtures/startup-fences.js +++ b/tests/coverage-fixtures/startup-fences.js @@ -2,8 +2,8 @@ // a fence inside a function runs when the function is called. "use strict"; var value = JSON.parse("{}"); -console.log(value instanceof Object); +console.log(value instanceof WeakRef); function check(other) { - return other instanceof Object; + return other instanceof WeakRef; } console.log(check([])); diff --git a/tests/diagnostics/any-destructure-unspelled-keys.ts b/tests/diagnostics/any-destructure-unspelled-keys.ts new file mode 100644 index 000000000..6680de854 --- /dev/null +++ b/tests/diagnostics/any-destructure-unspelled-keys.ts @@ -0,0 +1,12 @@ +// Destructuring an `any` source keeps SC1031 where the source has no +// compiled spelling for V8's TypeError: a key computed by an expression, an +// optional element read, a template key with a substitution, and a string key +// holding NUL (the message is a C string). String-literal keys compile +// (tests/corpus/7310-any-destructure-string-keys.ts). +const events: any = { ab: { value: 1 } }; +const k = "a"; +const { value: computed } = events["a" + "b"]; +const { value: optional } = events?.["ab"]; +const { value: template } = events[`${k}b`]; +const { value: nul } = events["a\0b"]; +console.log(computed, optional, template, nul); diff --git a/tests/diagnostics/array-subclass-fences.ts b/tests/diagnostics/array-subclass-fences.ts new file mode 100644 index 000000000..478e615f9 --- /dev/null +++ b/tests/diagnostics/array-subclass-fences.ts @@ -0,0 +1,25 @@ +// Array subclasses that do not compile yet, each refused where it is +// declared: a generic subclass, a subclass of a class that extends Array, +// a class that declares its own Symbol.species, and a class whose elements +// are its own instances. +class Bag extends Array { + firstOr(fallback: T): T { + return this.length > 0 ? this[0]! : fallback; + } +} + +class Base extends Array {} +class Derived extends Base {} + +class Speciated extends Array { + static get [Symbol.species](): ArrayConstructor { + return Array; + } +} + +class Nested extends Array {} + +console.log(new Bag().firstOr(1)); +console.log(new Derived().length); +console.log(new Speciated().length); +console.log(new Nested().length); diff --git a/tests/diagnostics/array-subclass-retyped-members.ts b/tests/diagnostics/array-subclass-retyped-members.ts new file mode 100644 index 000000000..308d434b8 --- /dev/null +++ b/tests/diagnostics/array-subclass-retyped-members.ts @@ -0,0 +1,24 @@ +// A species result of another element type is still an instance of the +// class, so `instanceof` narrows it back. Its elements are not the class's, +// so the class's fields and methods on it are refused where they are read. +class Holder { + tag = "h"; +} + +class Group extends Array { + owner: Holder | null = null; + + firstTag(): string { + const first = this[0]; + return first === null || first === undefined ? "none" : first.tag; + } +} + +const groups = new Group(); +groups.push(new Holder(), null); +const numbers = groups.map((_, i) => i + 1); +if (numbers instanceof Group) { + numbers.owner = new Holder(); + console.log(numbers.owner.tag); + console.log(numbers.firstTag()); +} diff --git a/tests/diagnostics/assert-fences.ts b/tests/diagnostics/assert-fences.ts index 823c64c40..7cf96bc5b 100644 --- a/tests/diagnostics/assert-fences.ts +++ b/tests/diagnostics/assert-fences.ts @@ -5,8 +5,8 @@ // (typed arrays, class instances), namespace-object calls, unsupported // throws/rejects expectations (properties outside the static error // surface, empty shapes, non-literal objects, doesNotReject shapes — -// Node's own TypeError), ifError over uncomparable types, and assert -// functions as values. Every reached site terminates in a named +// Node's own TypeError), ifError over uncomparable types, and assert.throws +// as a value. Every reached site terminates in a named // diagnostic — never a miscompare, never silence. import assert from "node:assert"; import * as assertNs from "node:assert"; @@ -47,8 +47,8 @@ assert.throws(() => { throw new Error("x"); }, {}); // Node rejects the empty sh void assert.doesNotReject(async () => {}, { name: "TypeError" }); // Node's own TypeError (function or RegExp only) assert.ifError(new Map()); // no honest ifError rendering for this type -const eq = assert.strictEqual; // assert functions as values -eq(1, 1); +const throwsFn = assert.throws; // assert.throws as a value: it takes the function to run +throwsFn(() => {}); interface Tree { label: string; kids: Tree[] } const tree: Tree = { label: "t", kids: [] }; diff --git a/tests/diagnostics/component-fences.ts b/tests/diagnostics/component-fences.ts index 6d88f9af5..0d382bf28 100644 --- a/tests/diagnostics/component-fences.ts +++ b/tests/diagnostics/component-fences.ts @@ -5,9 +5,9 @@ const byFlag = new Map(); console.log(byFlag); -// Map values have no slot for Dates. -const stamps = new Map(); -console.log(stamps); +// Map values have no slot for URLSearchParams. +const queries = new Map(); +console.log(queries); // Boolean Set elements remain outside the supported element domain. const flags = new Set(); diff --git a/tests/diagnostics/contract-int32-escapes.ts b/tests/diagnostics/contract-int32-escapes.ts index fcbbcbfd9..57ed4ce39 100644 --- a/tests/diagnostics/contract-int32-escapes.ts +++ b/tests/diagnostics/contract-int32-escapes.ts @@ -14,4 +14,5 @@ class Box { const box = new Box(); const loose: any = box; loose.size = 0.5; -console.log([1.5, 2].map(double).join(","), box.size); +const pick: (x: number) => number = double; +console.log([1.5, 2].map(pick).join(","), box.size); diff --git a/tests/diagnostics/contract-int32.ts b/tests/diagnostics/contract-int32.ts index f6e900626..20231c535 100644 --- a/tests/diagnostics/contract-int32.ts +++ b/tests/diagnostics/contract-int32.ts @@ -29,8 +29,10 @@ function bump(): void { const sizes = [1, 2, 3]; const len: Int32 = sizes.length; +const weights = [1.5, 2]; +const shifted = weights.map((w: Int32) => w + 1); const span = new Span(1.5); span.advance(); bump(); -console.log(width(span), half(7), count, len); +console.log(width(span), half(7), count, len, shifted); diff --git a/tests/diagnostics/date-union-fences.ts b/tests/diagnostics/date-union-fences.ts deleted file mode 100644 index 478df74d6..000000000 --- a/tests/diagnostics/date-union-fences.ts +++ /dev/null @@ -1,22 +0,0 @@ -// Compiler-synthesized `T | undefined` slots must observe the same Date -// union fence as source-written unions. Each construct below used to reach -// IR validation as an SC9001 internal compiler error. - -function defaultDate(value: Date = new Date(0)): number { - return value.getTime(); -} -defaultDate(); - -function* dates(): Generator { - yield new Date(0); -} -for (const value of dates()) console.log(value.getTime()); - -class DeferredDate { - value!: Date; - initialize(): void { - this.value = new Date(0); - } -} -const deferred = new DeferredDate(); -deferred.initialize(); diff --git a/tests/diagnostics/fs-url-path-fences.ts b/tests/diagnostics/fs-url-path-fences.ts index 080c561e1..6b65bfac0 100644 --- a/tests/diagnostics/fs-url-path-fences.ts +++ b/tests/diagnostics/fs-url-path-fences.ts @@ -1,15 +1,16 @@ // URL paths whose conversion would run in a different order than Node's. import * as fs from "node:fs"; +import * as fsp from "node:fs/promises"; const url = new URL("file:///tmp/fs-url-path-fences"); -const encoding: BufferEncoding = process.argv.length > 9 ? "latin1" : "utf8"; const recursive = process.argv.length > 9; -// Node validates the string source before converting the URL destination. -fs.copyFileSync("/tmp/source", url); // Node validates the options before the path. -console.log(fs.readFileSync(url, encoding)); fs.mkdirSync(url, { recursive }); +void fsp.mkdir(url, { recursive }); // A literal's members are lowered one by one, so they cannot be evaluated // ahead of the conversion as one value. console.log(fs.statSync(url, { throwIfNoEntry: process.argv.length > 9 })); +// fs.promises checks the string source inside the call, after the +// conversion would have run. +void fsp.copyFile("/tmp/source", url); diff --git a/tests/diagnostics/function-value-arguments-length.ts b/tests/diagnostics/function-value-arguments-length.ts new file mode 100644 index 000000000..ab0b53f82 --- /dev/null +++ b/tests/diagnostics/function-value-arguments-length.ts @@ -0,0 +1,36 @@ +// `arguments.length` has no single value when the calls pass varying +// counts: a function expression or declaration passed where the expected +// function type has optional or rest parameters. Methods and nested +// functions used as values, and arrows inside a function used as a value, +// keep the fence. +const replaced = "a-b".replace(/-/, function (match) { + return String(arguments.length) + match; +}); +function call(f: (a: number, b?: number) => number): number { + return f(1, 2); +} +const counted = call(function (a) { + return arguments.length + a; +}); +function offset(value: number): number { + return value + arguments.length; +} +const offsets = call(offset); +class Scale { + static by(value: number): number { + return value * arguments.length; + } +} +const scaled = [2].map(Scale.by); +function outer(): number[] { + function inner(value: number): number { + return value + arguments.length; + } + return [1].map(inner); +} +function deferred(value: number): number { + const read = (): number => arguments.length; + return value + read(); +} +const late = [1].map(deferred); +export { replaced, counted, offsets, scaled, outer, late }; diff --git a/tests/diagnostics/globalthis-buffer-alias-values.ts b/tests/diagnostics/globalthis-buffer-alias-values.ts new file mode 100644 index 000000000..29214f5c3 --- /dev/null +++ b/tests/diagnostics/globalthis-buffer-alias-values.ts @@ -0,0 +1,18 @@ +// An alias of the Buffer global read through a cast of globalThis compiles +// in conditions, typeof and member calls +// (tests/corpus/7309-globalthis-buffer-aliases.ts). Its other value uses +// keep a named SC1090 refusal: the alias has the cast's static type. +type RuntimeBuffer = { from(value: string, encoding: "utf8"): Uint8Array }; +const runtimeBuffer = (globalThis as { Buffer?: RuntimeBuffer }).Buffer; +function keep(value: unknown): unknown { + return value; +} +const bytes = runtimeBuffer && runtimeBuffer.from("abc", "utf8"); +keep(runtimeBuffer); +keep(bytes); +// A copy of the alias, and an alias whose cast read is guarded with `||`, +// carry the cast's type too. +const copied = runtimeBuffer; +const orFallback = (globalThis as { Buffer?: RuntimeBuffer }).Buffer || { from: () => new Uint8Array(0) }; +keep(copied); +keep(orFallback); diff --git a/tests/diagnostics/index-signature-values.ts b/tests/diagnostics/index-signature-values.ts index 6c53b141c..1f3a01ba5 100644 --- a/tests/diagnostics/index-signature-values.ts +++ b/tests/diagnostics/index-signature-values.ts @@ -8,10 +8,6 @@ const bySymbol: { [s: symbol]: number } = {}; console.log(bySymbol); -// Storage is supported, but composite typed-array inspection is not. -const buffers: Record = {}; -console.log(buffers); - // Dual signatures whose value types intern DIFFERENTLY cannot share the // one store (tsc requires only assignability, but the store gives one // answer per key). diff --git a/tests/diagnostics/inspect-custom-fences.ts b/tests/diagnostics/inspect-custom-fences.ts new file mode 100644 index 000000000..404f6ddab --- /dev/null +++ b/tests/diagnostics/inspect-custom-fences.ts @@ -0,0 +1,37 @@ +// [util.inspect.custom] methods keep their boundaries: a class whose +// subclass declares the method without declaring it itself, a method with +// more than three parameters, a method returning a class instance, and %o +// over instances with one. +import { format, inspect } from "node:util"; + +class Plain { + a = 1; +} +class Fancy extends Plain { + [inspect.custom](): string { + return "fancy"; + } +} +const items: Plain[] = [new Fancy()]; +console.log(inspect(items)); + +class Wide { + [inspect.custom](a: number, b: unknown, c: unknown, d: unknown): string { + return `${a}${String(b)}${String(c)}${String(d)}`; + } +} +console.log(inspect(new Wide())); + +class Echo { + [inspect.custom](): Echo { + return new Echo(); + } +} +console.log(inspect(new Echo())); + +class Shown { + [inspect.custom](): string { + return "shown"; + } +} +console.log(format("%o", new Shown())); diff --git a/tests/diagnostics/intl-segmenter.ts b/tests/diagnostics/intl-segmenter.ts index 42bdae846..0cf30eabb 100644 --- a/tests/diagnostics/intl-segmenter.ts +++ b/tests/diagnostics/intl-segmenter.ts @@ -1,5 +1,5 @@ -// The native implementation covers default Unicode grapheme segmentation. -new Intl.Segmenter("en-US"); +// The native implementation covers Unicode grapheme segmentation. +new Intl.Segmenter(["en-US", "de"]); new Intl.Segmenter(undefined, { granularity: "word" }); new Intl.Segmenter(undefined, { granularity: "sentence" }); // End of the diagnostic fixture. diff --git a/tests/diagnostics/maybe-promise-fences.ts b/tests/diagnostics/maybe-promise-fences.ts index 1dcccaf08..e6fdf98fa 100644 --- a/tests/diagnostics/maybe-promise-fences.ts +++ b/tests/diagnostics/maybe-promise-fences.ts @@ -26,7 +26,3 @@ console.log(maybe(true)); // must be that arm's own promise type. const widened = Promise.resolve(maybe(false)); console.log(typeof widened); - -// `instanceof Promise` on a value that is never a promise. -const record = { v: 1 }; -console.log(record instanceof Promise); diff --git a/tests/diagnostics/node-fallback-fence.ts b/tests/diagnostics/node-fallback-fence.ts index 86034f1ac..b044e22d3 100644 --- a/tests/diagnostics/node-fallback-fence.ts +++ b/tests/diagnostics/node-fallback-fence.ts @@ -30,9 +30,9 @@ const raw = readFileSync("/etc/hosts").toString(tty ? "hex" : "latin1"); // runt const intervalWithSignal = promiseInterval(1, "tick", {}); // AbortSignal options stay fenced const sleepWithSignal = promiseSleep(1, "tick", { signal: AbortSignal.timeout(5) }); // resolve values and AbortSignal options lower // The computed encoding is now part of the static Buffer surface. The -// one-shot zlib/raw/gzip codecs take Node's options, validated at run -// time; Brotli remains an explicit member fence. +// one-shot zlib/raw/gzip and Brotli codecs take Node's options, validated +// at run time. import { brotliCompressSync, deflateSync } from "node:zlib"; const packed = deflateSync("data", { level: 9, strategy: 1 }); // options lower -const brotli = brotliCompressSync(Buffer.from("data")); // outside the lowered family: fenced +const brotli = brotliCompressSync(Buffer.from("data")); // Brotli lowers too // The remaining imports continue the declared-but-not-lowered surface. diff --git a/tests/diagnostics/node-islands-config/main.ts b/tests/diagnostics/node-islands-config/main.ts deleted file mode 100644 index cc7b3735f..000000000 --- a/tests/diagnostics/node-islands-config/main.ts +++ /dev/null @@ -1,3 +0,0 @@ -// An unknown value in scriptc.packages or scriptc.nodeIslands is SC8004: -// the configuration the project asked for is never silently ignored. -console.log("configured"); diff --git a/tests/diagnostics/promise-all-generic-mixed-union.ts b/tests/diagnostics/promise-all-generic-mixed-union.ts new file mode 100644 index 000000000..8324f6a23 --- /dev/null +++ b/tests/diagnostics/promise-all-generic-mixed-union.ts @@ -0,0 +1,8 @@ +// Promise.all over a generic array instantiated with a union of a value and +// a promise, with an element read of its result: the call is refused by name. +async function f(xs: V[]): Promise { + const values = await Promise.all(xs); + values[0]; +} +f>(["x"]); +export {}; diff --git a/tests/diagnostics/split-cjs-require-cycle/b.js b/tests/diagnostics/split-cjs-require-cycle/b.js new file mode 100644 index 000000000..ad5a7441a --- /dev/null +++ b/tests/diagnostics/split-cjs-require-cycle/b.js @@ -0,0 +1,7 @@ +var a; +a = require('./main.js'); +exports.fromB = 'b-value'; +exports.readA = function () { + return a.early + '/' + a.late; +}; +console.log('b sees a.early:', a.early, 'a.late:', a.late); diff --git a/tests/diagnostics/split-cjs-require-cycle/main.js b/tests/diagnostics/split-cjs-require-cycle/main.js new file mode 100644 index 000000000..286de459a --- /dev/null +++ b/tests/diagnostics/split-cjs-require-cycle/main.js @@ -0,0 +1,11 @@ +// A split require binding (`var b; b = require("./b.js")`) whose require +// closes a CommonJS cycle that top-level code can observe: b.js reads a.js's +// partial exports while a.js is still evaluating, which the compiled export +// table does not model. Preflight refuses it with SC1016, as it refuses the +// `const b = require("./b.js")` form; tests/corpus/split-cjs-require-cycle +// compiles the cycle shape both forms admit. +exports.early = 'a-early'; +var b; +b = require('./b.js'); +exports.late = 'a-late'; +console.log('a sees', b.fromB, b.readA()); diff --git a/tests/diagnostics/split-cjs-require-cycle/package.json b/tests/diagnostics/split-cjs-require-cycle/package.json new file mode 100644 index 000000000..a3c15a7a6 --- /dev/null +++ b/tests/diagnostics/split-cjs-require-cycle/package.json @@ -0,0 +1 @@ +{ "type": "commonjs" } diff --git a/tests/diagnostics/split-cjs-require-early/lib.js b/tests/diagnostics/split-cjs-require-early/lib.js new file mode 100644 index 000000000..2a9669e12 --- /dev/null +++ b/tests/diagnostics/split-cjs-require-early/lib.js @@ -0,0 +1,8 @@ +'use strict'; + +/** @param {string} who */ +function greet(who) { + return `hello ${who}`; +} + +module.exports = { greet }; diff --git a/tests/diagnostics/split-cjs-require-early/main.js b/tests/diagnostics/split-cjs-require-early/main.js new file mode 100644 index 000000000..ae1d8d770 --- /dev/null +++ b/tests/diagnostics/split-cjs-require-early/main.js @@ -0,0 +1,21 @@ +// A split require binding (`let lib;` then `lib = require(...)`) that code +// above the assignment can reach: Node reads undefined there, a value the +// compiled module alias cannot hold, so the assignment keeps a named SC1013 +// fence. Assigned before any such code, the binding compiles +// (tests/corpus/split-cjs-require-binding). +'use strict'; + +let lib; +console.log('boot', helper()); +lib = require('./lib.js'); + +// The same with the require parenthesized. +let wrapped; +console.log('peek', wrapped === undefined); +wrapped = (require('./lib.js')); + +function helper() { + return lib === undefined ? 'not loaded' : lib.greet('late'); +} + +console.log(helper()); diff --git a/tests/diagnostics/split-cjs-require-early/package.json b/tests/diagnostics/split-cjs-require-early/package.json new file mode 100644 index 000000000..a3c15a7a6 --- /dev/null +++ b/tests/diagnostics/split-cjs-require-early/package.json @@ -0,0 +1 @@ +{ "type": "commonjs" } diff --git a/tests/diagnostics/stdlib-fence.ts b/tests/diagnostics/stdlib-fence.ts index 1fdc01675..0952436e1 100644 --- a/tests/diagnostics/stdlib-fence.ts +++ b/tests/diagnostics/stdlib-fence.ts @@ -21,9 +21,6 @@ const fmt = new Intl.NumberFormat(); // Constructors. const d = new Date(...([2024, 0] as const)); // spread field arguments stay fenced -const mutableDate = new Date(0); -mutableDate.setUTCFullYear(2024); // mutation is outside the read-only Date slice -const sameDate = mutableDate === mutableDate; // scalar storage must not fake object identity const wr = new WeakRef({}); const px = new Proxy({ a: 1 }, {}); const buf = new ArrayBuffer(8); @@ -74,21 +71,6 @@ function pick(a: string | number, b: number | boolean): boolean { } const picked = pick(1, 2); -// Date values deliberately have no tagged-union payload yet. Nullable -// Date positions must fence in the frontend instead of reaching a backend -// scalar/ref mismatch. -function optionalDate(value: Date | undefined): number { - return value === undefined ? -1 : value.getTime(); -} -optionalDate(undefined); -function throwDate(): void { - throw new Date(0); // the exception cell cannot preserve Date's object kind -} -throwDate(); -function* dateGenerator(): Generator { - yield 1; -} -dateGenerator().throw(new Date(0)); // Related classes may store one object under different union tags. A // structural arm beside them leaves no identity-preserving comparison. class FenceBase { diff --git a/tests/diagnostics/unions.ts b/tests/diagnostics/unions.ts index ccf62f40e..ddacb8015 100644 --- a/tests/diagnostics/unions.ts +++ b/tests/diagnostics/unions.ts @@ -60,14 +60,6 @@ const bareN = null; // honest text (JS prints "[object Object]") and stays fenced. const templated = `${mkAB()}`; -// `x == null` lowers as the null-or-undefined tag test — but a union with -// BOTH unit arms needs the operand twice, so an effectful operand (a call) -// gets its own fence pointing at a const binding. -function mkNullish(): string | null | undefined { - return null; -} -const looseCall = mkNullish() == null; - // A DECLARED `T | string` param infers nothing for T (no union // unification); the body's use of T then fails with the instantiation. function tagOr(x: T | string): T { diff --git a/tests/diagnostics/untyped-map-into-interface.ts b/tests/diagnostics/untyped-map-into-interface.ts new file mode 100644 index 000000000..ba5e26a90 --- /dev/null +++ b/tests/diagnostics/untyped-map-into-interface.ts @@ -0,0 +1,12 @@ +// An untyped `new Map()` satisfies a record interface only through `any`; +// its flow into the interface-typed result is refused at build time, like a +// typed Map is. +interface Cache { + get(key: string): number | undefined; + set(key: string, value: number): unknown; +} +function make(c?: Cache): Cache { + return c ?? new Map(); +} +console.log(make().get("a")); +export {}; diff --git a/tests/diagnostics/utility-types.ts b/tests/diagnostics/utility-types.ts index c6d10648b..cad1fcca4 100644 --- a/tests/diagnostics/utility-types.ts +++ b/tests/diagnostics/utility-types.ts @@ -5,9 +5,7 @@ // fields + an overflow map; number keys canonicalize to their JS string // spelling; function/Map/Set/nested-record values ride the same store — // differential corpus); what stays rejected: -// - symbol-keyed index signatures (no lowering for symbol keys); -// - index-signature value types outside the overflow-map slice (Dates and -// other lib API objects, even when the value itself has a representation). +// - symbol-keyed index signatures (no lowering for symbol keys). // (Readonly bindings compile, holding the Date as a checked value. // Dot access/writes to index-signature keys compile now — the overflow // path in dot spelling — and self-referential mapped types intern as @@ -15,9 +13,5 @@ const bySymbol: { [s: symbol]: string } = {}; console.log(bySymbol); -const dateBag: { [k: string]: Date } = {}; -const boot = dateBag["boot"]; -console.log(boot); - const stamped: Readonly = new Date(0); console.log(stamped.getTime()); diff --git a/tests/diagnostics/x509-fences.ts b/tests/diagnostics/x509-fences.ts new file mode 100644 index 000000000..416a88b59 --- /dev/null +++ b/tests/diagnostics/x509-fences.ts @@ -0,0 +1,21 @@ +// X509Certificate keeps its fields as prototype getters, so Node's own-key +// walks over a certificate see none of them. Those walks are refused by +// name instead of listing the fields. +import { X509Certificate } from "node:crypto"; +import { readFileSync } from "node:fs"; + +const cert = new X509Certificate(readFileSync("cert.pem")); +console.log(Object.keys(cert)); +console.log(Object.entries(cert)); +console.log({ ...cert }); +console.log(Object.prototype.hasOwnProperty.call(cert, "subject")); +for (const key in cert) console.log(key); + +// A certificate in an unknown or any slot would cross as a plain object of +// its fields, which Node's certificate is not. +const held: unknown = cert; +console.log(held); +function describe(value: unknown): string { + return typeof value; +} +console.log(describe([cert])); diff --git a/tests/fixtures/commander-calc/package.json b/tests/fixtures/commander-calc/package.json index f0ea76992..59f181b32 100644 --- a/tests/fixtures/commander-calc/package.json +++ b/tests/fixtures/commander-calc/package.json @@ -4,5 +4,15 @@ "type": "module", "dependencies": { "commander": "15.0.0" + }, + "scriptc": { + "typePatches": { + "commander@^15.0.0": { + "note": "off a TTY, _getOutputContext() returns helpWidth undefined although the @typedef says number", + "declarations": { + "HelpContext.helpWidth": "| undefined" + } + } + } } } diff --git a/tests/fixtures/event-loop/stdin-for-await-buffers.mjs b/tests/fixtures/event-loop/stdin-for-await-buffers.mjs new file mode 100644 index 000000000..d64ae460a --- /dev/null +++ b/tests/fixtures/event-loop/stdin-for-await-buffers.mjs @@ -0,0 +1,10 @@ +// for await over process.stdin yields Buffers: Buffer.isBuffer holds and +// indexOf takes a byte and a start offset (only chunking-independent facts +// are printed). +const chunks = []; +for await (const chunk of process.stdin) chunks.push(chunk); +console.log('buffers', chunks.every((chunk) => Buffer.isBuffer(chunk))); +const all = Buffer.concat(chunks); +const newlines = []; +for (let at = all.indexOf(10); at !== -1; at = all.indexOf(10, at + 1)) newlines.push(at); +console.log('newlines', newlines.join(','), all.subarray(newlines[0] + 1).indexOf('#')); diff --git a/tests/fixtures/event-loop/stdio-readable-backpressure.mjs b/tests/fixtures/event-loop/stdio-readable-backpressure.mjs new file mode 100644 index 000000000..df2afb18f --- /dev/null +++ b/tests/fixtures/event-loop/stdio-readable-backpressure.mjs @@ -0,0 +1,27 @@ +// 'readable', 'data' and resume() together, with no read() for a while: +// 'readable' keeps the stream paused, so it stops reading at its read-ahead +// mark until a read drains it, and 'data' sees what read() returns. +const input = process.stdin; +let draining = false; +let peak = 0; +let dataBytes = 0; +let readBytes = 0; +const drain = () => { + let chunk; + while ((chunk = input.read()) !== null) readBytes += chunk.length; +}; +input.on('data', (chunk) => { + dataBytes += chunk.length; +}); +input.on('readable', () => { + peak = Math.max(peak, input.readableLength); + if (draining) drain(); +}); +input.resume(); +setTimeout(() => { + peak = Math.max(peak, input.readableLength); + console.log('held below 128 KiB while not draining', peak <= 131072); + draining = true; + drain(); +}, 400); +input.on('end', () => console.log('read', readBytes, 'data', dataBytes)); diff --git a/tests/fixtures/event-loop/stdio-readable-large-size.mjs b/tests/fixtures/event-loop/stdio-readable-large-size.mjs new file mode 100644 index 000000000..f1f44edce --- /dev/null +++ b/tests/fixtures/event-loop/stdio-readable-large-size.mjs @@ -0,0 +1,9 @@ +// read(size) above the 64 KiB read-ahead mark: 'readable' keeps firing as +// input arrives until the requested size is buffered. +const input = process.stdin; +const sizes = []; +input.on('readable', () => { + let chunk; + while ((chunk = input.read(150000)) !== null) sizes.push(chunk.length); +}); +input.on('end', () => console.log(sizes.join(','))); diff --git a/tests/fixtures/event-loop/stdio-readable-once-rest.mjs b/tests/fixtures/event-loop/stdio-readable-once-rest.mjs new file mode 100644 index 000000000..81eae144b --- /dev/null +++ b/tests/fixtures/event-loop/stdio-readable-once-rest.mjs @@ -0,0 +1,12 @@ +// A once('readable') listener that reads part of the input: the rest stays +// buffered for a 'data' listener added afterwards. +const input = process.stdin; +input.once('readable', () => { + const head = input.read(3); + console.log('head', head === null ? null : head.toString('ascii')); + setTimeout(() => { + const rest = []; + input.on('data', (chunk) => rest.push(chunk.toString('ascii'))); + input.on('end', () => console.log('rest', rest.join(''))); + }, 50); +}); diff --git a/tests/fixtures/event-loop/stdio-readable-size.mjs b/tests/fixtures/event-loop/stdio-readable-size.mjs new file mode 100644 index 000000000..9ff76dafe --- /dev/null +++ b/tests/fixtures/event-loop/stdio-readable-size.mjs @@ -0,0 +1,18 @@ +// Fixed-size reads of stdin: read(n) answers null until n bytes are +// buffered, and the rest once the input ended. +const input = process.stdin; +const chunks = []; +const counts = []; +input.on('readable', () => { + let reads = 0; + let chunk; + while ((chunk = input.read(4)) !== null) { + chunks.push(chunk.toString('ascii')); + reads++; + } + counts.push(reads); +}); +input.on('end', () => { + console.log(chunks.join('|')); + console.log(counts.join(',')); +}); diff --git a/tests/fixtures/event-loop/stdio-readable.mjs b/tests/fixtures/event-loop/stdio-readable.mjs new file mode 100644 index 000000000..324a4b57b --- /dev/null +++ b/tests/fixtures/event-loop/stdio-readable.mjs @@ -0,0 +1,19 @@ +// process.stdin in paused mode through 'readable': each event drains the +// buffer with read(), one more event follows EOF, and 'end' comes after the +// read that finds the buffer empty. +const input = process.stdin; +const events = []; +let text = ''; +input.on('readable', () => { + let reads = 0; + let chunk; + while ((chunk = input.read()) !== null) { + text += String(chunk); + reads++; + } + events.push(`readable ${reads}`); +}); +input.on('end', () => { + console.log(events.join(', ')); + console.log(JSON.stringify(text), input.readableLength); +}); diff --git a/tests/fixtures/fetch/static-stream/main.mts b/tests/fixtures/fetch/static-stream/main.mts index 9ff8c050b..0ae400bc3 100644 --- a/tests/fixtures/fetch/static-stream/main.mts +++ b/tests/fixtures/fetch/static-stream/main.mts @@ -259,6 +259,22 @@ try { console.log("consumed request reader:", (error as Error).name); } +// A TransformStream's readable side as the request body: fetch's upload +// reads it, so writes to the writable side do not wait on backpressure. +const uploadTransform = new TransformStream(); +const uploadWriter = uploadTransform.writable.getWriter(); +const transformPosted = fetch(`${process.argv[2]}/post-echo`, { + method: "POST", + body: uploadTransform.readable, + duplex: "half", +}); +for (const part of ["trans", "form ", "body"]) { + await uploadWriter.write(new TextEncoder().encode(part)); +} +await uploadWriter.close(); +const transformResult = (await (await transformPosted).json()) as { body: string }; +console.log("transform request body:", transformResult.body); + const prelockedRequestBody = ReadableStream.from([ Buffer.from("prelocked request"), ]); diff --git a/tests/fixtures/node-island/gc-stress.ts b/tests/fixtures/node-island/gc-stress.ts new file mode 100644 index 000000000..8c71099d1 --- /dev/null +++ b/tests/fixtures/node-island/gc-stress.ts @@ -0,0 +1,24 @@ +// Full garbage collections in Node during island calls +// (tests/harness/node-island.test.ts): from the main stack, from deep native +// recursion, from async functions and from timers, with deep JavaScript +// recursion on Node's side. +import { call, callSync } from "@scriptc/node"; + +function nested(depth: number): number { + if (depth === 0) return callSync("./gc.cjs", "deep", 2000); + return nested(depth - 1) + 1; +} + +async function inFiber(n: number): Promise { + const later = await call("./gc.cjs", "later", n); + return later + callSync("./gc.cjs", "churn", 1); +} + +console.log("churn", callSync("./gc.cjs", "churn", 20)); +console.log("nested", nested(3000)); +console.log("deep", callSync("./gc.cjs", "deep", 8000)); +const results = await Promise.all([1, 2, 3, 4].map((n) => inFiber(n))); +console.log("fibers", results.join(",")); +setTimeout(() => { + console.log("timer", callSync("./gc.cjs", "churn", 5)); +}, 1); diff --git a/tests/fixtures/node-island/gc.cjs b/tests/fixtures/node-island/gc.cjs new file mode 100644 index 000000000..f8f5a32fa --- /dev/null +++ b/tests/fixtures/node-island/gc.cjs @@ -0,0 +1,41 @@ +"use strict"; +// Garbage-collection pressure inside Node island calls +// (tests/harness/node-island.test.ts): full collections while the call +// runs on Node's stack, at depth on both sides. +const v8 = require("node:v8"); +const vm = require("node:vm"); +v8.setFlagsFromString("--expose-gc"); +const gc = vm.runInNewContext("gc"); + +const kept = []; + +// Allocates `rounds` batches of objects that survive until the next batch, +// with a full collection after each. +exports.churn = (rounds) => { + let total = 0; + for (let r = 0; r < rounds; r++) { + const batch = []; + for (let i = 0; i < 100000; i++) batch.push({ i, s: `item ${i}`, tags: [i, i + 1] }); + kept[r % 4] = batch; + total += batch.length; + gc(); + } + return total; +}; + +// Recurses `depth` frames in JavaScript and collects at the bottom. +exports.deep = (depth) => { + if (depth === 0) { + gc(); + return 0; + } + const local = { depth, text: String(depth) }; + return 1 + exports.deep(depth - 1) + (local.depth > 0 ? 0 : 1); +}; + +// Collects after a timer, from Node's loop. +exports.later = async (n) => { + await new Promise((resolve) => setTimeout(resolve, 1)); + exports.churn(1); + return n; +}; diff --git a/tests/fixtures/node-island/helper.cjs b/tests/fixtures/node-island/helper.cjs index 903532b0c..92c4ac77b 100644 --- a/tests/fixtures/node-island/helper.cjs +++ b/tests/fixtures/node-island/helper.cjs @@ -1,3 +1,23 @@ "use strict"; exports.answer = () => 42; exports.echo = (value) => value; +// An error whose message cannot be read. +exports.unreadable = () => { + const error = new Error(); + Object.defineProperty(error, "message", { + get() { + throw Symbol("message"); + }, + }); + throw error; +}; +// A fork without an execPath of its own: in a program it would start the +// program again. +exports.forkSelf = () => { + try { + require("node:child_process").fork(__filename); + return "forked"; + } catch (error) { + return error.message; + } +}; diff --git a/tests/fixtures/node-island/lifecycle.ts b/tests/fixtures/node-island/lifecycle.ts index f18ec640d..262ab7a6b 100644 --- a/tests/fixtures/node-island/lifecycle.ts +++ b/tests/fixtures/node-island/lifecycle.ts @@ -16,4 +16,12 @@ if (mode !== "never") { console.log("mapped after", mapped()); } if (mode === "exit") process.exit(5); +if (mode === "fork") console.log(callSync("./helper.cjs", "forkSelf")); +if (mode === "unreadable") { + try { + callSync("./helper.cjs", "unreadable"); + } catch (error) { + console.log("caught", error instanceof Error, JSON.stringify((error as Error).message)); + } +} console.log("done"); diff --git a/tests/fixtures/node-island/worker.ts b/tests/fixtures/node-island/worker.ts new file mode 100644 index 000000000..c96196c8f --- /dev/null +++ b/tests/fixtures/node-island/worker.ts @@ -0,0 +1,18 @@ +// A call into Node from a worker thread (tests/harness/node-island.test.ts): +// Node islands run on the main thread only, so the worker's call throws. +import { Worker, isMainThread, parentPort } from "node:worker_threads"; +import { callSync } from "@scriptc/node"; + +if (isMainThread) { + console.log("main", callSync("./helper.cjs", "answer")); + const worker = new Worker(new URL(import.meta.url)); + worker.on("message", (message: string) => console.log("worker", message)); + worker.on("exit", (code: number) => console.log("exit", code)); +} else { + try { + callSync("./helper.cjs", "answer"); + parentPort!.postMessage("called"); + } catch (error) { + parentPort!.postMessage((error as Error).message); + } +} diff --git a/tests/fixtures/node-types/diffie-hellman.ts b/tests/fixtures/node-types/diffie-hellman.ts new file mode 100644 index 000000000..d14fd5ffc --- /dev/null +++ b/tests/fixtures/node-types/diffie-hellman.ts @@ -0,0 +1,19 @@ +import { type KeyObject, createPublicKey, diffieHellman, generateKeyPair, generateKeyPairSync } from "node:crypto"; + +const alice = generateKeyPairSync("x25519"); +const bob = generateKeyPairSync("x25519"); +const secret: Buffer = diffieHellman({ privateKey: alice.privateKey, publicKey: bob.publicKey }); +console.log(alice.publicKey.asymmetricKeyType, secret.length, secret.equals(diffieHellman({ privateKey: bob.privateKey, publicKey: alice.publicKey }))); +const exported = alice.publicKey.export({ type: "spki", format: "pem" }); +const pem = typeof exported === "string" ? exported : exported.toString(); +const reread: KeyObject = createPublicKey(pem); +console.log(reread.equals(alice.publicKey), pem.split("\n")[0]); +const ec = generateKeyPairSync("ec", { namedCurve: "P-384" }); +const peer = generateKeyPairSync("ec", { namedCurve: "P-384" }); +console.log("ecdh", diffieHellman({ privateKey: ec.privateKey, publicKey: peer.publicKey }).length); +generateKeyPair("x25519", undefined, (err, publicKey, privateKey) => { + console.log("generated", err, publicKey.type, privateKey.asymmetricKeyType); + diffieHellman({ privateKey, publicKey: bob.publicKey }, (error, shared) => { + console.log("callback", error, shared.length); + }); +}); diff --git a/tests/fixtures/node-types/fenced.ts b/tests/fixtures/node-types/fenced.ts index 7c468de01..d94c7c19a 100644 --- a/tests/fixtures/node-types/fenced.ts +++ b/tests/fixtures/node-types/fenced.ts @@ -45,9 +45,8 @@ console.log(win32.sep); const u = new URL("https://example.com/x?a=1"); console.log(URL.createObjectURL(new Blob(["data"]))); u.searchParams.get("a"); -/* The one-shot zlib/raw/gzip codecs take Node's options (the deflateSync - * call lowers), while Brotli remains a member-qualified fence with the - * lowered family named. */ +/* The one-shot zlib/raw/gzip and Brotli codecs take Node's options (the + * deflateSync and brotliCompressSync calls lower). */ import { brotliCompressSync, deflateSync } from "zlib"; deflateSync("data", { level: 9, strategy: 1 }); brotliCompressSync(Buffer.from("data")); @@ -99,12 +98,9 @@ void callbackified; import { createECDH } from "node:crypto"; createECDH("sect163k1"); createECDH("prime256v1").computeSecret(Buffer.alloc(65), "hex"); -/* DiffieHellman's Object.prototype members, and crypto.diffieHellman over - * asymmetric KeyObjects. */ -import { createDiffieHellman, diffieHellman, type KeyObject } from "node:crypto"; +/* DiffieHellman's Object.prototype members. */ +import { createDiffieHellman } from "node:crypto"; createDiffieHellman(Buffer.from([23])).toString(); -declare const dhKey: KeyObject; -diffieHellman({ privateKey: dhKey, publicKey: dhKey }); /* WebCrypto beyond the secret-key slice: @types/node declares the whole * SubtleCrypto surface, and the methods outside the slice, such as * getPublicKey, fence by name. */ @@ -112,8 +108,8 @@ async function publicKeyWithWebCrypto(key: import("node:crypto").webcrypto.Crypt await crypto.subtle.getPublicKey(key, ["verify"]); } void publicKeyWithWebCrypto; -/* Node's navigator global: typeof navigator and navigator.userAgent - * compile; the other members fence by name. */ -const cores = navigator.hardwareConcurrency; -void cores; +/* Node's navigator global: navigator.locks (the Web Locks API) fences + * by name. */ +const locks = navigator.locks; +void locks; // End of the declared-but-not-lowered surface. diff --git a/tests/fixtures/node-types/fs-url-fallible.ts b/tests/fixtures/node-types/fs-url-fallible.ts new file mode 100644 index 000000000..da86879d5 --- /dev/null +++ b/tests/fixtures/node-types/fs-url-fallible.ts @@ -0,0 +1,19 @@ +import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { readFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { pathToFileURL } from "node:url"; + +const dir = mkdtempSync(join(tmpdir(), "node-types-url-")); +const file = pathToFileURL(join(dir, "a.txt")); +writeFileSync(file, "hi"); +// existsSync answers false for a URL it cannot convert. +console.log(existsSync(file), existsSync(new URL("http://example.com/"))); +// fs.promises functions reject for a URL they cannot convert. +readFile(file, "utf8") + .then((text) => console.log(text)) + .then(() => readFile(new URL("file://example.com/a.txt"), "utf8")) + .catch((error: unknown) => { + if (error instanceof Error) console.log((error as NodeJS.ErrnoException).code); + rmSync(dir, { recursive: true }); + }); diff --git a/tests/fixtures/node-types/fs-url-path-fences.ts b/tests/fixtures/node-types/fs-url-path-fences.ts deleted file mode 100644 index 4ae142189..000000000 --- a/tests/fixtures/node-types/fs-url-path-fences.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { existsSync } from "node:fs"; -import { readFile, stat } from "node:fs/promises"; - -const url = new URL("file:///tmp/node-types-url-fences"); -// existsSync answers false for a URL it cannot convert. -console.log(existsSync(url)); -// The fs.promises functions reject for a URL they cannot convert. -void readFile(url, "utf8"); -void stat(url); diff --git a/tests/fixtures/node-types/navigator.ts b/tests/fixtures/node-types/navigator.ts index 2f1ee2d9f..901f0e290 100644 --- a/tests/fixtures/node-types/navigator.ts +++ b/tests/fixtures/node-types/navigator.ts @@ -1,6 +1,10 @@ /* Node 24's navigator global under @types/node: the runtime probe - * libraries use (jose's user-agent check) folds to Node's values. */ + * libraries use (jose's user-agent check) folds to Node's values, and the + * other members read the process's values. */ const browser = !navigator.userAgent.startsWith("Mozilla/5.0 "); console.log(typeof navigator, navigator.userAgent, browser); console.log(typeof navigator !== "undefined" && navigator.userAgent === "Cloudflare-Workers"); console.log(navigator.userAgent.split("/")[0], navigator.userAgent.length); +console.log(navigator.hardwareConcurrency > 0, Number.isInteger(navigator.hardwareConcurrency)); +console.log(typeof navigator.language, navigator.languages.length, navigator.languages[0] === navigator.language); +console.log(navigator.platform.length > 0, navigator.languages === navigator.languages); diff --git a/tests/fixtures/node-types/tls-client-certificates.ts b/tests/fixtures/node-types/tls-client-certificates.ts new file mode 100644 index 000000000..3268369f9 --- /dev/null +++ b/tests/fixtures/node-types/tls-client-certificates.ts @@ -0,0 +1,32 @@ +// Under @types/node: a mutual-TLS server that requires a client +// certificate signed by its CA, and a client that presents one. +import { readFileSync } from "node:fs"; +import type { AddressInfo } from "node:net"; +import * as tls from "node:tls"; + +const certs = "tests/fixtures/server/certs"; +const ca = readFileSync(`${certs}/ca.pem`); + +const cert = readFileSync(`${certs}/localhost.pem`); +const key = readFileSync(`${certs}/localhost-key.pem`); +let serverLine = ""; +const server = tls.createServer({ cert, key, ca, requestCert: true, rejectUnauthorized: true }, (socket: tls.TLSSocket) => { + const peer: tls.PeerCertificate = socket.getPeerCertificate(); + serverLine = `server ${socket.authorized} ${String(socket.authorizationError)} ${peer.subject.CN}`; + socket.end("welcome"); +}); +server.listen(0, "127.0.0.1", () => { + const port = (server.address() as AddressInfo).port; + const clientCert = readFileSync(`${certs}/client.pem`); + const clientKey = readFileSync(`${certs}/client-key.pem`); + const client = tls.connect({ port, host: "127.0.0.1", servername: "localhost", ca, cert: clientCert, key: clientKey }, () => { + console.log("client", client.authorized, client.getX509Certificate()?.subject); + }); + let body = ""; + client.on("data", (chunk: Buffer) => (body += chunk.toString())); + client.on("end", () => { + console.log(serverLine); + console.log("body", body); + server.close(); + }); +}); diff --git a/tests/fixtures/node-types/tls-peer-certificates.ts b/tests/fixtures/node-types/tls-peer-certificates.ts new file mode 100644 index 000000000..d082cfead --- /dev/null +++ b/tests/fixtures/node-types/tls-peer-certificates.ts @@ -0,0 +1,37 @@ +// Under @types/node: a TLS client reads the server's certificate as the +// legacy object, its detailed issuer chain and an X509Certificate, and the +// server reads the certificate it serves. +import { readFileSync } from "node:fs"; +import type { AddressInfo } from "node:net"; +import * as tls from "node:tls"; + +const certs = "tests/fixtures/server/certs"; +const ca = readFileSync(`${certs}/ca.pem`); +const cert = readFileSync(`${certs}/localhost.pem`); +const key = readFileSync(`${certs}/localhost-key.pem`); + +// The server's line prints once the client has its reply, so the output +// order does not depend on which side's callback runs first. +let serverLine = ""; +const server = tls.createServer({ cert, key }, (socket: tls.TLSSocket) => { + const own = socket.getCertificate() as tls.PeerCertificate; + serverLine = `server ${own.subject.CN} ${socket.getX509Certificate()?.subject} ${Object.keys(socket.getPeerCertificate()).length}`; + socket.end("done"); +}); +server.listen(0, "127.0.0.1", () => { + const port = (server.address() as AddressInfo).port; + const client = tls.connect({ port, host: "127.0.0.1", ca, servername: "localhost" }, () => { + const peer: tls.PeerCertificate = client.getPeerCertificate(); + const detailed: tls.DetailedPeerCertificate = client.getPeerCertificate(true); + console.log("client", peer.subject.CN, peer.subjectaltname, peer.bits, detailed.issuerCertificate.subject.CN); + console.log("root", detailed.issuerCertificate.issuerCertificate === detailed.issuerCertificate); + const x509 = client.getPeerX509Certificate(); + if (x509 !== undefined) console.log("x509", x509.subject, x509.checkIP("127.0.0.1"), x509.fingerprint256 === peer.fingerprint256); + }); + client.resume(); + client.on("end", () => { + console.log(serverLine); + client.destroy(); + server.close(); + }); +}); diff --git a/tests/fixtures/node-types/tls-server-contexts.ts b/tests/fixtures/node-types/tls-server-contexts.ts new file mode 100644 index 000000000..ff0425a56 --- /dev/null +++ b/tests/fixtures/node-types/tls-server-contexts.ts @@ -0,0 +1,38 @@ +// Under @types/node: a tls.Server that answers an SNI name from +// addContext, replaces its default context with setSecureContext, and +// reports a plain-text client through 'tlsClientError'. +import { readFileSync } from "node:fs"; +import * as net from "node:net"; +import type { AddressInfo } from "node:net"; +import * as tls from "node:tls"; + +const certs = "tests/fixtures/server/certs"; +const cert = readFileSync(`${certs}/localhost.pem`); +const key = readFileSync(`${certs}/localhost-key.pem`); +const alt = readFileSync(`${certs}/alt.pem`); +const altKey = readFileSync(`${certs}/alt-key.pem`); + +const server: tls.Server = tls.createServer({ cert, key }, (socket: tls.TLSSocket) => socket.end()); +server.addContext("*.localhost", tls.createSecureContext({ cert: alt, key: altKey })); +server.on("tlsClientError", (err: Error, socket: tls.TLSSocket) => { + console.log("tlsClientError", (err as NodeJS.ErrnoException).code, socket.destroyed); +}); + +function subject(port: number, servername: string): Promise { + return new Promise((resolve) => { + const client = tls.connect({ port, host: "127.0.0.1", servername, rejectUnauthorized: false }, () => { + resolve(String(client.getPeerCertificate().subject.CN)); + client.end(); + }); + }); +} + +server.listen(0, "127.0.0.1", async () => { + const port = (server.address() as AddressInfo).port; + console.log("sni", await subject(port, "alt.localhost"), await subject(port, "localhost")); + server.setSecureContext({ cert: alt, key: altKey }); + console.log("default", await subject(port, "localhost")); + const raw = net.connect(port, "127.0.0.1", () => raw.write("GET / HTTP/1.1\r\nHost: x\r\n\r\n")); + raw.on("data", () => {}); + raw.on("close", () => setTimeout(() => server.close(), 50)); +}); diff --git a/tests/fixtures/node-types/x509-methods.ts b/tests/fixtures/node-types/x509-methods.ts new file mode 100644 index 000000000..8736ec990 --- /dev/null +++ b/tests/fixtures/node-types/x509-methods.ts @@ -0,0 +1,20 @@ +// Under @types/node: X509Certificate's methods over the test certificates: +// the issuer, the signature, the host and IP names, the private key, the +// legacy object and the PEM text. +import { X509Certificate, createPrivateKey } from "node:crypto"; +import { readFileSync } from "node:fs"; +import type { PeerCertificate } from "node:tls"; + +const certs = "tests/fixtures/server/certs"; +const ca = new X509Certificate(readFileSync(`${certs}/ca.pem`)); +const leaf = new X509Certificate(readFileSync(`${certs}/localhost.pem`)); +const key = createPrivateKey(readFileSync(`${certs}/localhost-key.pem`)); + +console.log(leaf.checkIssued(ca), leaf.verify(ca.publicKey), ca.verify(leaf.publicKey)); +const host: string | undefined = leaf.checkHost("localhost"); +console.log(host, leaf.checkHost("example.com"), leaf.checkIP("127.0.0.1"), leaf.checkIP("::1")); +console.log(leaf.checkEmail("root@localhost", { subject: "always" }), leaf.checkPrivateKey(key)); +const legacy: PeerCertificate = leaf.toLegacyObject(); +console.log(legacy.subject.CN, legacy.subjectaltname, legacy.bits, legacy.exponent, legacy.raw.equals(leaf.raw)); +console.log(leaf.publicKey.asymmetricKeyType, leaf.publicKey === leaf.publicKey); +console.log(leaf.toString() === readFileSync(`${certs}/localhost.pem`, "latin1"), String(ca).split("\n")[0]); diff --git a/tests/fixtures/npm-static/addon-probe-cli.ts b/tests/fixtures/npm-static/addon-probe-cli.ts new file mode 100644 index 000000000..aa21df05d --- /dev/null +++ b/tests/fixtures/npm-static/addon-probe-cli.ts @@ -0,0 +1,6 @@ +import { locate, probe, resolveHelper } from "addon-probe"; +import { join } from "node:path"; + +console.log(join("a", "b"), [probe("clipboard"), probe("keys"), resolveHelper("addon-probe-helper")].join(",")); +console.log(resolveHelper("other", (id) => `custom:${id}`)); +console.log(locate("helper")); diff --git a/tests/fixtures/npm-static/addon-probe-installed-cli.ts b/tests/fixtures/npm-static/addon-probe-installed-cli.ts new file mode 100644 index 000000000..5568ce086 --- /dev/null +++ b/tests/fixtures/npm-static/addon-probe-installed-cli.ts @@ -0,0 +1,4 @@ +import { loadInstalled } from "addon-probe"; + +console.log("before"); +console.log(loadInstalled(process.argv[2] === "module" ? "module" : "addon")); diff --git a/tests/fixtures/npm-static/declared-members-cli.ts b/tests/fixtures/npm-static/declared-members-cli.ts new file mode 100644 index 000000000..107ffc54d --- /dev/null +++ b/tests/fixtures/npm-static/declared-members-cli.ts @@ -0,0 +1,21 @@ +// A consumer reads members a package's declarations promise but its +// JavaScript adds in ways the inferred literal type misses: absent keys of +// index signatures read undefined, and expando properties read their values, +// as in Node. +import { counts, fonts, settings, stats } from "declared-members"; + +const key = process.argv.length > 10 ? "size" : "missing"; + +const s = settings(); +console.log(s["font"] ?? "default font", s["size"] ?? "default size", s.family); +console.log(s["font"] === undefined, typeof s["weight"], s.weight ?? "dot miss"); +console.log(s[key] ?? "dynamic miss", s[key], Object.keys(s)); + +const c = counts(); +console.log(c["chars"] ?? -1, c["lines"], c[key] === undefined); + +const st = stats([4, 9, 2]); +console.log(st.max, st.sum, st.count, st); + +const f = fonts(); +console.log(f["title"]?.name ?? "no title", f[key] === undefined, f["body"]?.name, f.code.name); diff --git a/tests/fixtures/npm-static/node_modules/addon-probe/index.d.ts b/tests/fixtures/npm-static/node_modules/addon-probe/index.d.ts new file mode 100644 index 000000000..ca349ec0e --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/addon-probe/index.d.ts @@ -0,0 +1,4 @@ +export declare function probe(name: string): string; +export declare function resolveHelper(name: string, resolvePackage?: (id: string) => string): string; +export declare function loadInstalled(kind: "addon" | "module"): string; +export declare function locate(name: string): string; diff --git a/tests/fixtures/npm-static/node_modules/addon-probe/index.js b/tests/fixtures/npm-static/node_modules/addon-probe/index.js new file mode 100644 index 000000000..acd2bf814 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/addon-probe/index.js @@ -0,0 +1,62 @@ +import { createRequire } from "node:module"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const load = createRequire(import.meta.url); +const here = dirname(fileURLToPath(import.meta.url)); + +// Looks for a prebuilt addon in each packaging location, the way +// packages with optional native helpers do. +export function probe(name) { + for (const dir of ["prebuilds", "native"]) { + try { + const addon = load(join(here, dir, `${name}.node`)); + if (typeof addon?.run === "function") return "loaded"; + } catch { + // Try the next location. + } + } + return "absent"; +} + +export function resolveHelper(name, resolvePackage) { + try { + return (resolvePackage ?? load.resolve)(name); + } catch { + return "unresolved"; + } +} + +// Requires a file that is installed: an addon, or a module that is not part +// of the compiled program. +export function loadInstalled(kind) { + try { + return typeof load(join(here, kind === "addon" ? "native/installed.node" : "installed.js")); + } catch { + return "fallback"; + } +} + +let probes = 0; +function candidate(name) { + probes++; + return `./${name}-${probes}.node`; +} + +// Resolves computed candidate paths: the arguments run before the lookup +// fails, as an assignment and as a statement of their own. +export function locate(name) { + let where = "none"; + try { + where = load.resolve(candidate(name)); + } catch { + where = `missing after ${probes}`; + } + try { + load.resolve(candidate(name)); + where += ",found"; + } catch { + where += `,missing after ${probes}`; + } + return where; +} diff --git a/tests/fixtures/npm-static/node_modules/addon-probe/installed.js b/tests/fixtures/npm-static/node_modules/addon-probe/installed.js new file mode 100644 index 000000000..8476dc24e --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/addon-probe/installed.js @@ -0,0 +1 @@ +export const installed = true; diff --git a/tests/fixtures/npm-static/node_modules/addon-probe/native/installed.node b/tests/fixtures/npm-static/node_modules/addon-probe/native/installed.node new file mode 100644 index 000000000..3d1ca6ec7 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/addon-probe/native/installed.node @@ -0,0 +1 @@ +placeholder for a prebuilt addon diff --git a/tests/fixtures/npm-static/node_modules/addon-probe/package.json b/tests/fixtures/npm-static/node_modules/addon-probe/package.json new file mode 100644 index 000000000..a2f7406f5 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/addon-probe/package.json @@ -0,0 +1,7 @@ +{ + "name": "addon-probe", + "version": "1.0.0", + "type": "module", + "main": "index.js", + "types": "index.d.ts" +} diff --git a/tests/fixtures/npm-static/node_modules/declared-members/index.d.ts b/tests/fixtures/npm-static/node_modules/declared-members/index.d.ts new file mode 100644 index 000000000..8061fd5cb --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/declared-members/index.d.ts @@ -0,0 +1,7 @@ +export declare function settings(): { [key: string]: string }; +export declare function counts(): Record; +export declare function stats(xs: number[]): { count: number; sum: number; max: number }; +export interface Font { + name: string; +} +export declare function fonts(): Record; diff --git a/tests/fixtures/npm-static/node_modules/declared-members/index.js b/tests/fixtures/npm-static/node_modules/declared-members/index.js new file mode 100644 index 000000000..381bd4a46 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/declared-members/index.js @@ -0,0 +1,34 @@ +"use strict"; +// Each function returns an object literal whose JavaScript-inferred type +// lacks members that index.d.ts promises. + +// An index signature over a literal built empty, then assigned. +function settings() { + const out = {}; + out.size = "12px"; + out.family = "serif"; + return out; +} + +// An index signature over a literal with fixed keys. +function counts() { + return { lines: 3, words: 12 }; +} + +// An expando on a non-empty literal: the checker's literal type has no `max`. +function stats(xs) { + const out = { count: xs.length, sum: 0 }; + for (const x of xs) out.sum += x; + out.max = Math.max(...xs); + return out; +} + +// An index signature whose values are a declared interface. +function fonts() { + return { body: { name: "serif" }, code: { name: "mono" } }; +} + +exports.settings = settings; +exports.counts = counts; +exports.stats = stats; +exports.fonts = fonts; diff --git a/tests/fixtures/npm-static/node_modules/declared-members/package.json b/tests/fixtures/npm-static/node_modules/declared-members/package.json new file mode 100644 index 000000000..4a42ebdf2 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/declared-members/package.json @@ -0,0 +1,6 @@ +{ + "name": "declared-members", + "version": "1.0.0", + "main": "index.js", + "types": "index.d.ts" +} diff --git a/tests/fixtures/npm-static/node_modules/word-segments/index.d.ts b/tests/fixtures/npm-static/node_modules/word-segments/index.d.ts new file mode 100644 index 000000000..2fd45310e --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/word-segments/index.d.ts @@ -0,0 +1 @@ +export declare function splitWords(text: string): string; diff --git a/tests/fixtures/npm-static/node_modules/word-segments/index.js b/tests/fixtures/npm-static/node_modules/word-segments/index.js new file mode 100644 index 000000000..a67aa5381 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/word-segments/index.js @@ -0,0 +1,9 @@ +const words = new Intl.Segmenter(undefined, { granularity: "word" }); + +export function splitWords(text) { + try { + return [...words.segment(text)].map((part) => part.segment).join("|"); + } catch (error) { + return `${error.code}: ${error.message}`; + } +} diff --git a/tests/fixtures/npm-static/node_modules/word-segments/package.json b/tests/fixtures/npm-static/node_modules/word-segments/package.json new file mode 100644 index 000000000..37b0d6f73 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/word-segments/package.json @@ -0,0 +1 @@ +{ "name": "word-segments", "version": "1.0.0", "type": "module", "main": "index.js", "types": "index.d.ts" } diff --git a/tests/fixtures/npm-static/word-segments-cli.ts b/tests/fixtures/npm-static/word-segments-cli.ts new file mode 100644 index 000000000..28e3bd5e1 --- /dev/null +++ b/tests/fixtures/npm-static/word-segments-cli.ts @@ -0,0 +1,3 @@ +import { splitWords } from "word-segments"; + +console.log(splitWords("two words")); diff --git a/tests/fixtures/npm/cases/hono-zod-default-hook/main.ts b/tests/fixtures/npm/cases/hono-zod-default-hook/main.ts new file mode 100644 index 000000000..9981e7154 --- /dev/null +++ b/tests/fixtures/npm/cases/hono-zod-default-hook/main.ts @@ -0,0 +1,38 @@ +// @hono/zod-validator without a hook: a failed check answers 400 with the +// whole safeParse result serialized, so the ZodError's own enumerable +// properties (name, then message) and their order reach the client. +import { zValidator } from "@hono/zod-validator"; +import { Hono } from "hono"; +import { z } from "zod"; + +const Item = z.object({ title: z.string().min(2), qty: z.number().int().positive() }); + +const app = new Hono(); +app.post("/items", zValidator("json", Item), (c) => { + const item = c.req.valid("json"); + return c.json({ created: item.title, qty: item.qty }, 201); +}); +app.get("/items", zValidator("query", z.object({ limit: z.coerce.number().int().max(5) })), (c) => { + return c.json({ limit: c.req.valid("query").limit }); +}); + +const post = (body: unknown) => + app.request("/items", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) }); + +const show = async (label: string, res: Response | Promise) => { + const r = await res; + console.log(label, r.status, await r.text()); +}; + +await show("ok", post({ title: "pen", qty: 2 })); +await show("bad-title", post({ title: "p", qty: 2 })); +await show("bad-both", post({ title: 3, qty: -1 })); +await show("query-ok", app.request("/items?limit=4")); +await show("query-bad", app.request("/items?limit=9")); + +const result = Item.safeParse({ title: "x", qty: 0 }); +if (!result.success) { + const e = result.error; + console.log(e.name, e instanceof Error, Object.keys(e).join(",")); + console.log(JSON.stringify(e)); +} diff --git a/tests/fixtures/npm/cases/hono-zod-default-hook/package.json b/tests/fixtures/npm/cases/hono-zod-default-hook/package.json new file mode 100644 index 000000000..a734aaddb --- /dev/null +++ b/tests/fixtures/npm/cases/hono-zod-default-hook/package.json @@ -0,0 +1 @@ +{ "name": "hono-zod-default-hook-case", "private": true, "type": "module" } diff --git a/tests/fixtures/npm/cases/hono-zod-validator/lying.ts b/tests/fixtures/npm/cases/hono-zod-validator/lying.ts new file mode 100644 index 000000000..fa44c2e12 --- /dev/null +++ b/tests/fixtures/npm/cases/hono-zod-validator/lying.ts @@ -0,0 +1,42 @@ +// Native-only (decl-boundary.test.ts): `c.req.valid()` is a checked +// boundary. z.custom checks nothing, so a body its declared type +// misdescribes fails the cast with a path TypeError, which hono's error +// handler reports; Node does not check. A union discriminated by boolean +// literals keeps both arms. +import { zValidator } from "@hono/zod-validator"; +import { Hono } from "hono"; +import { z } from "zod"; + +const Liar = z.custom<{ n: number }>(); +const Outcome = z.discriminatedUnion("ok", [ + z.object({ ok: z.literal(true), value: z.number() }), + z.object({ ok: z.literal(false), reason: z.string() }), +]); + +const app = new Hono(); +app.onError((err, c) => c.text(`${err instanceof TypeError} ${err.message}`, 500)); +app.post("/liar", zValidator("json", Liar), (c) => c.text(String(c.req.valid("json").n + 1))); +app.post("/outcome", zValidator("json", Outcome), (c) => { + const outcome = c.req.valid("json"); + return c.text(outcome.ok ? `value ${outcome.value + 1}` : `reason ${outcome.reason}`); +}); + +const requests: [string, unknown][] = [ + ["/liar", { n: 1 }], + ["/liar", { n: "x" }], + ["/outcome", { ok: true, value: 41 }], + ["/outcome", { ok: false, reason: "late" }], +]; + +async function main(): Promise { + for (const [path, body] of requests) { + const res = await app.request(path, { + method: "POST", + body: JSON.stringify(body), + headers: { "content-type": "application/json" }, + }); + console.log(path, res.status, await res.text()); + } +} + +void main(); diff --git a/tests/fixtures/npm/cases/hono-zod-validator/main.ts b/tests/fixtures/npm/cases/hono-zod-validator/main.ts new file mode 100644 index 000000000..9de69c482 --- /dev/null +++ b/tests/fixtures/npm/cases/hono-zod-validator/main.ts @@ -0,0 +1,96 @@ +// A Hono app validated by @hono/zod-validator and zod 4.6, all compiled +// from their JavaScript: `c.req.valid(target)` hands the handler the +// parsed value, typed by the schema's z.infer through hono's declarations. +// Requests go through app.request, so no socket is involved. +import { zValidator } from "@hono/zod-validator"; +import { Hono, type Context } from "hono"; +import { z } from "zod"; + +const Book = z.strictObject({ + title: z.string().trim().min(1), + year: z.number().int().min(1450), + tags: z.array(z.string()).max(4).default([]), + rating: z.number().min(0).max(5).optional(), +}); +type Book = z.infer; +const ListQuery = z.object({ + minYear: z.coerce.number().int().optional(), + sort: z.enum(["title", "year"]).default("title"), + limit: z.coerce.number().int().min(1).max(10).default(3), +}); +const IdParam = z.object({ id: z.string().regex(/^b\d+$/) }); + +// Failed checks answer with the issues' paths and codes. +const reject = ( + result: { success: true } | { success: false; error: z.core.$ZodError }, + c: Context, +): Response | undefined => { + if (result.success) return undefined; + const paths = result.error.issues.map((i) => `${i.path.join(".")}:${i.code}`); + return c.json({ error: "invalid", paths }, 400); +}; + +const books = new Map(); +let nextId = 1; + +const app = new Hono(); +app.post( + "/books", + zValidator("json", Book, (result, c) => { + if (!result.success) { + const paths = result.error.issues.map((i) => `${i.path.join(".")}:${i.code}`); + return c.json({ error: "invalid", paths }, 400); + } + }), + (c) => { + const book = c.req.valid("json"); + book.tags.push("new"); + const id = `b${nextId++}`; + books.set(id, book); + const score = (book.rating ?? 0) * 2 + book.tags.length; + return c.json({ id, title: book.title.toUpperCase(), year: book.year, score }, 201); + }, +); +app.get("/books", zValidator("query", ListQuery, reject), (c) => { + const { minYear, sort, limit } = c.req.valid("query"); + const rows = [...books.values()] + .filter((b) => minYear === undefined || b.year >= minYear) + .sort((a, b) => (sort === "year" ? a.year - b.year : a.title < b.title ? -1 : 1)) + .slice(0, limit) + .map((b) => `${b.title}/${b.year}`); + return c.json({ sort, limit, rows }); +}); +app.get("/books/:id", zValidator("param", IdParam, reject), (c) => { + const id = c.req.valid("param").id; + const book = books.get(id); + if (book === undefined) return c.json({ error: "missing", id }, 404); + return c.json({ id, ...book }); +}); + +async function call(method: string, path: string, body?: unknown): Promise { + const init: RequestInit = { method }; + if (body !== undefined) { + init.body = JSON.stringify(body); + init.headers = { "content-type": "application/json" }; + } + const res = await app.request(path, init); + console.log(method, path, res.status, await res.text()); +} + +async function main(): Promise { + await call("POST", "/books", { title: " Dune ", year: 1965, tags: ["sf"], rating: 4.5 }); + await call("POST", "/books", { title: "Emma", year: 1815 }); + await call("POST", "/books", { title: "Ulysses", year: 1922, tags: ["modern"] }); + await call("POST", "/books", { title: "", year: 1200, extra: true }); + await call("POST", "/books", { title: "Bad", year: "x", tags: [1, 2, 3, 4, 5] }); + await call("GET", "/books"); + await call("GET", "/books?minYear=1900&sort=year&limit=5"); + await call("GET", "/books?limit=99"); + await call("GET", "/books/b2"); + await call("GET", "/books/b9"); + await call("GET", "/books/nope"); + const kept = books.get("b1")!; + console.log("stored", kept.tags.join(","), kept.title); +} + +void main(); diff --git a/tests/fixtures/npm/cases/hono-zod-validator/package.json b/tests/fixtures/npm/cases/hono-zod-validator/package.json new file mode 100644 index 000000000..27e5f1b4b --- /dev/null +++ b/tests/fixtures/npm/cases/hono-zod-validator/package.json @@ -0,0 +1 @@ +{ "name": "hono-zod-validator-case", "private": true, "type": "module" } diff --git a/tests/fixtures/npm/cases/legacy-namespaces/main.ts b/tests/fixtures/npm/cases/legacy-namespaces/main.ts new file mode 100644 index 000000000..2f9483ba9 --- /dev/null +++ b/tests/fixtures/npm/cases/legacy-namespaces/main.ts @@ -0,0 +1,26 @@ +// A namespace-style library in the pre-module idiom (guarded namespace +// declarations, constructor functions with prototype methods, enums as +// objects, a revealing module, a handler registry, exported through +// `module.exports = { app }`), used from TypeScript. +import { app } from "legacy-ns"; + +const v = new app.geom.Vec(3, 4); +console.log(v.length(), v.add(new app.geom.Vec(1, 1)).scale(2).x, v instanceof app.geom.Vec); +console.log(Object.keys(app).join(","), Object.keys(app.math).join(","), Object.keys(app.geom.Kind).join(",")); +console.log(app.math.clamp(12, 0, 10), app.math.lerp(0, 10, 0.25), app.text.pad(7, 3), app.text.label(1, 42)); + +const shapes = []; +for (let i = 0; i < 12; i++) { + const center = new app.geom.Vec(app.math.lerp(-5, 5, (i % 4) / 3), app.math.lerp(-5, 5, Math.floor(i / 4) / 2)); + shapes.push(new app.geom.Shape(i % 3, center, 1 + (i % 4))); +} +let area = 0; +let hits = 0; +for (let step = 0; step < 200; step++) { + const probe = new app.geom.Vec(app.math.clamp(Math.sin(step) * 7, -5, 5), app.math.clamp(Math.cos(step * 0.7) * 7, -5, 5)); + for (const s of shapes) { + area += app.registry.run("area", s, probe); + hits += app.registry.run("hits", s, probe); + } +} +console.log(Math.round(area), hits, shapes[4].contains(shapes[4].center), shapes[0].area()); diff --git a/tests/fixtures/npm/cases/legacy-namespaces/node_modules/legacy-ns/index.d.ts b/tests/fixtures/npm/cases/legacy-namespaces/node_modules/legacy-ns/index.d.ts new file mode 100644 index 000000000..16c9fa62a --- /dev/null +++ b/tests/fixtures/npm/cases/legacy-namespaces/node_modules/legacy-ns/index.d.ts @@ -0,0 +1,2 @@ +declare const app: any; +export { app }; diff --git a/tests/fixtures/npm/cases/legacy-namespaces/node_modules/legacy-ns/index.js b/tests/fixtures/npm/cases/legacy-namespaces/node_modules/legacy-ns/index.js new file mode 100644 index 000000000..2efa926f5 --- /dev/null +++ b/tests/fixtures/npm/cases/legacy-namespaces/node_modules/legacy-ns/index.js @@ -0,0 +1,97 @@ +// A namespace-style geometry and text library in the pre-module idiom: +// guarded namespace declarations, constructor functions with prototype +// methods, enums as objects, a revealing module, and a static registry. +var app = app || {}; +app.math = app.math || {}; +app.geom = app.geom || {}; + +app.math.EPSILON = 1e-9; + +app.math.clamp = function (value, lo, hi) { + return value < lo ? lo : value > hi ? hi : value; +}; + +app.math.lerp = function (a, b, t) { + return a + (b - a) * t; +}; + +app.math.hypot = function (x, y) { + return Math.sqrt(x * x + y * y); +}; + +app.geom.Kind = { POINT: 0, CIRCLE: 1, RECT: 2 }; + +/** @constructor */ +app.geom.Vec = function (x, y) { + this.x = x; + this.y = y; +}; + +app.geom.Vec.prototype.add = function (other) { + return new app.geom.Vec(this.x + other.x, this.y + other.y); +}; + +app.geom.Vec.prototype.scale = function (k) { + return new app.geom.Vec(this.x * k, this.y * k); +}; + +app.geom.Vec.prototype.length = function () { + return app.math.hypot(this.x, this.y); +}; + +/** @constructor */ +app.geom.Shape = function (kind, center, size) { + this.kind = kind; + this.center = center; + this.size = size; +}; + +app.geom.Shape.prototype.area = function () { + switch (this.kind) { + case app.geom.Kind.CIRCLE: + return Math.PI * this.size * this.size; + case app.geom.Kind.RECT: + return this.size * this.size; + default: + return 0; + } +}; + +app.geom.Shape.prototype.contains = function (p) { + var d = p.add(this.center.scale(-1)).length(); + return d <= this.size + app.math.EPSILON; +}; + +app.text = (function () { + var digits = "0123456789"; + function pad(n, width) { + var s = ""; + var v = n; + do { + s = digits.charAt(v % 10) + s; + v = Math.floor(v / 10); + } while (v > 0); + while (s.length < width) s = "0" + s; + return s; + } + function label(kind, n) { + return (kind === app.geom.Kind.CIRCLE ? "c" : kind === app.geom.Kind.RECT ? "r" : "p") + pad(n, 4); + } + return { pad: pad, label: label }; +})(); + +app.registry = { + handlers: { + area: function (shape) { + return shape.area(); + }, + hits: function (shape, probe) { + return shape.contains(probe) ? 1 : 0; + }, + }, + run: function (name, shape, probe) { + return app.registry.handlers[name](shape, probe); + }, +}; + +module.exports = { app }; diff --git a/tests/fixtures/npm/cases/legacy-namespaces/node_modules/legacy-ns/package.json b/tests/fixtures/npm/cases/legacy-namespaces/node_modules/legacy-ns/package.json new file mode 100644 index 000000000..cde18c613 --- /dev/null +++ b/tests/fixtures/npm/cases/legacy-namespaces/node_modules/legacy-ns/package.json @@ -0,0 +1 @@ +{ "name": "legacy-ns", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" } diff --git a/tests/fixtures/npm/cases/legacy-namespaces/package.json b/tests/fixtures/npm/cases/legacy-namespaces/package.json new file mode 100644 index 000000000..140c08a1c --- /dev/null +++ b/tests/fixtures/npm/cases/legacy-namespaces/package.json @@ -0,0 +1 @@ +{ "name": "legacy-namespaces-case", "private": true, "type": "module" } diff --git a/tests/fixtures/npm/cases/shape-overlay-facts/main.ts b/tests/fixtures/npm/cases/shape-overlay-facts/main.ts new file mode 100644 index 000000000..2c83bcb85 --- /dev/null +++ b/tests/fixtures/npm/cases/shape-overlay-facts/main.ts @@ -0,0 +1,12 @@ +// The shape overlay's facts: results that include undefined, literals that +// hold comment-like text, a var declared in a branch no call takes, and +// a var declared in two such branches. +import { banner, hoisted, lookup, lookupOrReturn, shared } from "shape-facts"; + +console.log(lookup("b")); +console.log(lookup("z")); +console.log(lookupOrReturn("a")); +console.log(lookupOrReturn("y")); +console.log(banner(7)); +console.log(hoisted("note:")); +console.log(shared("level:")); diff --git a/tests/fixtures/npm/cases/shape-overlay-facts/node_modules/shape-facts/index.d.ts b/tests/fixtures/npm/cases/shape-overlay-facts/node_modules/shape-facts/index.d.ts new file mode 100644 index 000000000..12ff3f2d8 --- /dev/null +++ b/tests/fixtures/npm/cases/shape-overlay-facts/node_modules/shape-facts/index.d.ts @@ -0,0 +1,5 @@ +export declare function lookup(key: string): string; +export declare function lookupOrReturn(key: string): string; +export declare function banner(n: number): string; +export declare function hoisted(label: string): string; +export declare function shared(label: string): string; diff --git a/tests/fixtures/npm/cases/shape-overlay-facts/node_modules/shape-facts/index.js b/tests/fixtures/npm/cases/shape-overlay-facts/node_modules/shape-facts/index.js new file mode 100644 index 000000000..8cfa73620 --- /dev/null +++ b/tests/fixtures/npm/cases/shape-overlay-facts/node_modules/shape-facts/index.js @@ -0,0 +1,70 @@ +// Functions whose results include the undefined of a fall-through or a +// bare return, comment-like text in literals, a branch no call takes +// that declares a var read outside it, and two such branches that declare +// the same var. + +const entries = [ + { key: "a", value: 1 }, + { key: "b", value: 2 }, +]; + +function find(key) { + for (const entry of entries) { + if (entry.key === key) return entry; + } +} + +function findOrReturn(key) { + for (const entry of entries) if (entry.key === key) return entry; + return; +} + +export function lookup(key) { + const hit = find(key); + if (hit) { + return "found " + hit.key + "=" + hit.value; + } else { + return "missing " + key; + } +} + +export function lookupOrReturn(key) { + const hit = findOrReturn(key); + if (hit) { + return "found " + hit.key + "=" + hit.value; + } else { + return "missing " + key; + } +} + +export function banner(n) { + const quoted = "/** @type {number} */"; + const template = `/** @returns {string} ${n} */`; + const pattern = /\/\*\* @param \*\//; + return quoted + " " + template + " " + pattern.source; +} + +function noteOf(extra) { + if (extra) { + var note = "set"; + } + return typeof note + " " + String(note); +} + +export function hoisted(label) { + return label + " " + noteOf(undefined); +} + +function levelOf(warn, fail) { + if (warn) { + var level = "warn"; + } + if (fail) { + var level = "fail"; + } + return typeof level + " " + String(level); +} + +export function shared(label) { + return label + " " + levelOf(undefined, undefined); +} diff --git a/tests/fixtures/npm/cases/shape-overlay-facts/node_modules/shape-facts/package.json b/tests/fixtures/npm/cases/shape-overlay-facts/node_modules/shape-facts/package.json new file mode 100644 index 000000000..317269e91 --- /dev/null +++ b/tests/fixtures/npm/cases/shape-overlay-facts/node_modules/shape-facts/package.json @@ -0,0 +1 @@ +{ "name": "shape-facts", "version": "1.0.0", "type": "module", "main": "index.js", "types": "index.d.ts" } diff --git a/tests/fixtures/npm/cases/shape-overlay-facts/package.json b/tests/fixtures/npm/cases/shape-overlay-facts/package.json new file mode 100644 index 000000000..d15818a37 --- /dev/null +++ b/tests/fixtures/npm/cases/shape-overlay-facts/package.json @@ -0,0 +1 @@ +{ "name": "shape-overlay-facts-case", "private": true, "type": "module" } diff --git a/tests/fixtures/npm/cases/type-patch-dts/main.ts b/tests/fixtures/npm/cases/type-patch-dts/main.ts new file mode 100644 index 000000000..1e442054b --- /dev/null +++ b/tests/fixtures/npm/cases/type-patch-dts/main.ts @@ -0,0 +1,15 @@ +// A package whose .d.ts says `email: string` for a value that is missing, +// patched to `string | undefined` (package.json scriptc.typePatches): the +// profiles cross the declaration boundary as native records, and user +// code handles the missing email the patched type now makes visible. +import { findProfile, loadProfiles } from "profilekit"; + +const profiles = loadProfiles(); +for (const p of profiles) console.log(p.id, p.name, p.email ?? "(no email)", "email" in p); +console.log(JSON.stringify(profiles)); +const emails = profiles.filter((p) => p.email !== undefined).map((p) => p.email!.toUpperCase()); +console.log(emails.join(",")); +const found = findProfile(3); +console.log(found === undefined ? "none" : found.name); +const missing = findProfile(9); +console.log(missing === undefined ? "none" : missing.name); diff --git a/tests/fixtures/npm/cases/type-patch-dts/node_modules/profilekit/index.d.ts b/tests/fixtures/npm/cases/type-patch-dts/node_modules/profilekit/index.d.ts new file mode 100644 index 000000000..445d78ea2 --- /dev/null +++ b/tests/fixtures/npm/cases/type-patch-dts/node_modules/profilekit/index.d.ts @@ -0,0 +1,10 @@ +// The declarations say every profile has an email and every lookup finds +// one; the JavaScript leaves email out for guests and returns undefined +// for unknown ids. +export type Profile = { + id: number; + name: string; + email: string; +}; +export declare function loadProfiles(): Profile[]; +export declare function findProfile(id: number): Profile; diff --git a/tests/fixtures/npm/cases/type-patch-dts/node_modules/profilekit/index.js b/tests/fixtures/npm/cases/type-patch-dts/node_modules/profilekit/index.js new file mode 100644 index 000000000..a20a082f6 --- /dev/null +++ b/tests/fixtures/npm/cases/type-patch-dts/node_modules/profilekit/index.js @@ -0,0 +1,14 @@ +"use strict"; +function row(id, name, email) { + const profile = { id: id, name: name }; + if (email !== null) profile.email = email; + return profile; +} +function loadProfiles() { + return [row(1, "ada", "ada@example.com"), row(2, "guest", null), row(3, "lin", "lin@example.com")]; +} +function findProfile(id) { + return loadProfiles().find((p) => p.id === id); +} +exports.loadProfiles = loadProfiles; +exports.findProfile = findProfile; diff --git a/tests/fixtures/npm/cases/type-patch-dts/node_modules/profilekit/package.json b/tests/fixtures/npm/cases/type-patch-dts/node_modules/profilekit/package.json new file mode 100644 index 000000000..9982f9eef --- /dev/null +++ b/tests/fixtures/npm/cases/type-patch-dts/node_modules/profilekit/package.json @@ -0,0 +1 @@ +{ "name": "profilekit", "version": "3.1.0", "main": "index.js", "types": "index.d.ts" } diff --git a/tests/fixtures/npm/cases/type-patch-dts/package.json b/tests/fixtures/npm/cases/type-patch-dts/package.json new file mode 100644 index 000000000..822491420 --- /dev/null +++ b/tests/fixtures/npm/cases/type-patch-dts/package.json @@ -0,0 +1,15 @@ +{ + "name": "type-patch-dts-case", + "private": true, + "type": "module", + "scriptc": { + "typePatches": { + "profilekit@^3.0.0": { + "declarations": { + "Profile.email": "| undefined", + "findProfile()": "| undefined" + } + } + } + } +} diff --git a/tests/fixtures/npm/cases/type-patch-files/main.ts b/tests/fixtures/npm/cases/type-patch-files/main.ts new file mode 100644 index 000000000..32e041ce0 --- /dev/null +++ b/tests/fixtures/npm/cases/type-patch-files/main.ts @@ -0,0 +1,10 @@ +// A package declaration file replaced by a local one (scriptc.typePatches +// files), plus a declaration patch on another file of the same package. +import { parseLog, summarize } from "eventlog"; + +const entries = parseLog("10 info ada started\n12 warn - disk low\n15 error lin crashed hard\n"); +for (const e of entries) console.log(e.at, e.level.toUpperCase(), e.user ?? "(system)", e.message); +console.log(entries.filter((e) => e.level !== "info").length, JSON.stringify(entries[1])); +const all = summarize(entries); +const none = summarize([]); +console.log(all.count, all.last ?? "-", none.count, none.last ?? "-"); diff --git a/tests/fixtures/npm/cases/type-patch-files/node_modules/eventlog/dist/entry.d.ts b/tests/fixtures/npm/cases/type-patch-files/node_modules/eventlog/dist/entry.d.ts new file mode 100644 index 000000000..0a42b298f --- /dev/null +++ b/tests/fixtures/npm/cases/type-patch-files/node_modules/eventlog/dist/entry.d.ts @@ -0,0 +1,8 @@ +// Out of date: the JavaScript reports levels as strings and has no +// `user` for system entries. +export type Entry = { + at: number; + level: number; + user: string; + message: string; +}; diff --git a/tests/fixtures/npm/cases/type-patch-files/node_modules/eventlog/dist/index.d.ts b/tests/fixtures/npm/cases/type-patch-files/node_modules/eventlog/dist/index.d.ts new file mode 100644 index 000000000..c16d59d46 --- /dev/null +++ b/tests/fixtures/npm/cases/type-patch-files/node_modules/eventlog/dist/index.d.ts @@ -0,0 +1,4 @@ +export type { Entry } from "./entry"; +import type { Entry } from "./entry"; +export declare function parseLog(text: string): Entry[]; +export declare function summarize(entries: Entry[]): { count: number; last: string }; diff --git a/tests/fixtures/npm/cases/type-patch-files/node_modules/eventlog/dist/index.js b/tests/fixtures/npm/cases/type-patch-files/node_modules/eventlog/dist/index.js new file mode 100644 index 000000000..4c10d88f4 --- /dev/null +++ b/tests/fixtures/npm/cases/type-patch-files/node_modules/eventlog/dist/index.js @@ -0,0 +1,17 @@ +"use strict"; +function parseLog(text) { + const rows = String(text) + .split("\n") + .filter(Boolean) + .map((line) => { + const [at, level, user, ...rest] = line.split(" "); + return { at: Number(at), level: level, user: user === "-" ? undefined : user, message: rest.join(" ") }; + }); + // Entries travel as JSON (the log format's own wire form). + return JSON.parse(JSON.stringify(rows)); +} +function summarize(entries) { + return { count: entries.length, last: entries.length === 0 ? undefined : entries[entries.length - 1].message }; +} +exports.parseLog = parseLog; +exports.summarize = summarize; diff --git a/tests/fixtures/npm/cases/type-patch-files/node_modules/eventlog/package.json b/tests/fixtures/npm/cases/type-patch-files/node_modules/eventlog/package.json new file mode 100644 index 000000000..27ebf4625 --- /dev/null +++ b/tests/fixtures/npm/cases/type-patch-files/node_modules/eventlog/package.json @@ -0,0 +1 @@ +{ "name": "eventlog", "version": "2.3.1", "main": "dist/index.js", "types": "dist/index.d.ts" } diff --git a/tests/fixtures/npm/cases/type-patch-files/package.json b/tests/fixtures/npm/cases/type-patch-files/package.json new file mode 100644 index 000000000..92a38c108 --- /dev/null +++ b/tests/fixtures/npm/cases/type-patch-files/package.json @@ -0,0 +1,13 @@ +{ + "name": "type-patch-files-case", + "private": true, + "type": "module", + "scriptc": { + "typePatches": { + "eventlog@^2.0.0": { + "files": { "dist/entry.d.ts": "./type-patches/entry.d.ts" }, + "declarations": { "summarize().last": "| undefined" } + } + } + } +} diff --git a/tests/fixtures/npm/cases/type-patch-files/type-patches/entry.d.ts b/tests/fixtures/npm/cases/type-patch-files/type-patches/entry.d.ts new file mode 100644 index 000000000..380338e96 --- /dev/null +++ b/tests/fixtures/npm/cases/type-patch-files/type-patches/entry.d.ts @@ -0,0 +1,7 @@ +// The project's replacement for eventlog's dist/entry.d.ts. +export type Entry = { + at: number; + level: "info" | "warn" | "error"; + user?: string; + message: string; +}; diff --git a/tests/fixtures/npm/cases/type-patch-types-entry/main.ts b/tests/fixtures/npm/cases/type-patch-types-entry/main.ts new file mode 100644 index 000000000..3d0ab5ac9 --- /dev/null +++ b/tests/fixtures/npm/cases/type-patch-types-entry/main.ts @@ -0,0 +1,9 @@ +// A package without declarations, typed by a local declaration file +// (scriptc.typePatches types): user code is checked against it, and the +// values cross the declaration boundary as native records. +import { centroid, point, type Point } from "pointjs"; + +const points: Point[] = [point(0, 0), point(4, 0), point(4, 3)]; +const c = centroid(points); +console.log(c === null ? "none" : `${c.x.toFixed(2)},${c.y.toFixed(2)}`); +console.log(centroid([]) === null, JSON.stringify(points.map((p) => p.x + p.y))); diff --git a/tests/fixtures/npm/cases/type-patch-types-entry/node_modules/pointjs/lib/point.js b/tests/fixtures/npm/cases/type-patch-types-entry/node_modules/pointjs/lib/point.js new file mode 100644 index 000000000..f324e9acb --- /dev/null +++ b/tests/fixtures/npm/cases/type-patch-types-entry/node_modules/pointjs/lib/point.js @@ -0,0 +1,17 @@ +"use strict"; +// Plain JavaScript without declarations. +function point(x, y) { + return { x: x, y: y }; +} +function centroid(points) { + if (points.length === 0) return null; + var sx = 0; + var sy = 0; + for (var i = 0; i < points.length; i++) { + sx += points[i].x; + sy += points[i].y; + } + return point(sx / points.length, sy / points.length); +} +exports.point = point; +exports.centroid = centroid; diff --git a/tests/fixtures/npm/cases/type-patch-types-entry/node_modules/pointjs/package.json b/tests/fixtures/npm/cases/type-patch-types-entry/node_modules/pointjs/package.json new file mode 100644 index 000000000..917576875 --- /dev/null +++ b/tests/fixtures/npm/cases/type-patch-types-entry/node_modules/pointjs/package.json @@ -0,0 +1 @@ +{ "name": "pointjs", "version": "1.4.0", "main": "lib/point.js" } diff --git a/tests/fixtures/npm/cases/type-patch-types-entry/package.json b/tests/fixtures/npm/cases/type-patch-types-entry/package.json new file mode 100644 index 000000000..06846a01d --- /dev/null +++ b/tests/fixtures/npm/cases/type-patch-types-entry/package.json @@ -0,0 +1,10 @@ +{ + "name": "type-patch-types-entry-case", + "private": true, + "type": "module", + "scriptc": { + "typePatches": { + "pointjs@1.x": { "types": "./type-patches/pointjs.d.ts" } + } + } +} diff --git a/tests/fixtures/npm/cases/type-patch-types-entry/type-patches/pointjs.d.ts b/tests/fixtures/npm/cases/type-patch-types-entry/type-patches/pointjs.d.ts new file mode 100644 index 000000000..30ae94a5e --- /dev/null +++ b/tests/fixtures/npm/cases/type-patch-types-entry/type-patches/pointjs.d.ts @@ -0,0 +1,4 @@ +// The project's declarations for pointjs, which ships none. +export type Point = { x: number; y: number }; +export declare function point(x: number, y: number): Point; +export declare function centroid(points: Point[]): Point | null; diff --git a/tests/fixtures/npm/cases/type-patch-types-twin/main.ts b/tests/fixtures/npm/cases/type-patch-types-twin/main.ts new file mode 100644 index 000000000..702b51972 --- /dev/null +++ b/tests/fixtures/npm/cases/type-patch-types-twin/main.ts @@ -0,0 +1,11 @@ +// A plain-JavaScript package typed by an @types twin that says `max: +// number` and `mean: number`; the project's type patches (keyed by the +// runtime package and its installed version) make them `number | +// undefined` and `number | null`. +import { totals } from "tallyjs"; + +for (const values of [[3, 9, 4], []]) { + const t = totals(values); + console.log(t.count, t.sum, t.max ?? "no max", t.mean === null ? "no mean" : t.mean.toFixed(2)); + console.log(JSON.stringify(t), Object.keys(t).join("|")); +} diff --git a/tests/fixtures/npm/cases/type-patch-types-twin/node_modules/@types/tallyjs/index.d.ts b/tests/fixtures/npm/cases/type-patch-types-twin/node_modules/@types/tallyjs/index.d.ts new file mode 100644 index 000000000..4a7b81dcc --- /dev/null +++ b/tests/fixtures/npm/cases/type-patch-types-twin/node_modules/@types/tallyjs/index.d.ts @@ -0,0 +1,7 @@ +export type Totals = { + count: number; + sum: number; + max: number; + mean: number; +}; +export declare function totals(values: number[]): Totals; diff --git a/tests/fixtures/npm/cases/type-patch-types-twin/node_modules/@types/tallyjs/package.json b/tests/fixtures/npm/cases/type-patch-types-twin/node_modules/@types/tallyjs/package.json new file mode 100644 index 000000000..fed3cc37a --- /dev/null +++ b/tests/fixtures/npm/cases/type-patch-types-twin/node_modules/@types/tallyjs/package.json @@ -0,0 +1 @@ +{ "name": "@types/tallyjs", "version": "0.4.0", "types": "index.d.ts" } diff --git a/tests/fixtures/npm/cases/type-patch-types-twin/node_modules/tallyjs/index.js b/tests/fixtures/npm/cases/type-patch-types-twin/node_modules/tallyjs/index.js new file mode 100644 index 000000000..88d5ffc8f --- /dev/null +++ b/tests/fixtures/npm/cases/type-patch-types-twin/node_modules/tallyjs/index.js @@ -0,0 +1,11 @@ +"use strict"; +// Plain JavaScript; @types/tallyjs describes it, except that an empty +// list has no max (undefined) and no mean (null). +function totals(values) { + var sum = 0; + for (var i = 0; i < values.length; i++) sum += values[i]; + var max = values.length === 0 ? undefined : Math.max.apply(null, values); + var mean = values.length === 0 ? null : sum / values.length; + return { count: values.length, sum: sum, max: max, mean: mean }; +} +exports.totals = totals; diff --git a/tests/fixtures/npm/cases/type-patch-types-twin/node_modules/tallyjs/package.json b/tests/fixtures/npm/cases/type-patch-types-twin/node_modules/tallyjs/package.json new file mode 100644 index 000000000..ad80ad430 --- /dev/null +++ b/tests/fixtures/npm/cases/type-patch-types-twin/node_modules/tallyjs/package.json @@ -0,0 +1 @@ +{ "name": "tallyjs", "version": "0.4.2", "main": "index.js" } diff --git a/tests/fixtures/npm/cases/type-patch-types-twin/package.json b/tests/fixtures/npm/cases/type-patch-types-twin/package.json new file mode 100644 index 000000000..53ec04ede --- /dev/null +++ b/tests/fixtures/npm/cases/type-patch-types-twin/package.json @@ -0,0 +1,15 @@ +{ + "name": "type-patch-types-twin-case", + "private": true, + "type": "module", + "scriptc": { + "typePatches": { + "tallyjs@~0.4.0": { + "declarations": { + "Totals.max": "| undefined", + "Totals.mean": "number | null" + } + } + } + } +} diff --git a/tests/fixtures/npm/cases/types-keyed-construction/main.ts b/tests/fixtures/npm/cases/types-keyed-construction/main.ts new file mode 100644 index 000000000..b163f3d1c --- /dev/null +++ b/tests/fixtures/npm/cases/types-keyed-construction/main.ts @@ -0,0 +1,12 @@ +// A plain-JavaScript package typed by an @types package builds its result +// key by key (`var p = {}; p.id = id; ...`): the object crosses the +// declaration boundary with every field, including the declared fields +// whose keys are not identifiers. +import { loadProfile, type Profile } from "profilekit"; + +const p: Profile = loadProfile(7); +console.log(p["a;b"].toUpperCase(), p["a;b"].length); +console.log(p["my-key"], p["my-key"].length, p["x y"].length); +console.log(p.id, p.count, JSON.stringify(p)); +const keys: string[] = Object.keys(p); +console.log(keys.join("|")); diff --git a/tests/fixtures/npm/cases/types-keyed-construction/node_modules/@types/profilekit/index.d.ts b/tests/fixtures/npm/cases/types-keyed-construction/node_modules/@types/profilekit/index.d.ts new file mode 100644 index 000000000..2da797940 --- /dev/null +++ b/tests/fixtures/npm/cases/types-keyed-construction/node_modules/@types/profilekit/index.d.ts @@ -0,0 +1,8 @@ +export interface Profile { + id: number; + "a;b": string; + "my-key": string; + "x y": string; + count: number; +} +export declare function loadProfile(id: number): Profile; diff --git a/tests/fixtures/npm/cases/types-keyed-construction/node_modules/@types/profilekit/package.json b/tests/fixtures/npm/cases/types-keyed-construction/node_modules/@types/profilekit/package.json new file mode 100644 index 000000000..a7dd89b11 --- /dev/null +++ b/tests/fixtures/npm/cases/types-keyed-construction/node_modules/@types/profilekit/package.json @@ -0,0 +1 @@ +{ "name": "@types/profilekit", "version": "1.0.0", "types": "index.d.ts" } diff --git a/tests/fixtures/npm/cases/types-keyed-construction/node_modules/profilekit/index.js b/tests/fixtures/npm/cases/types-keyed-construction/node_modules/profilekit/index.js new file mode 100644 index 000000000..8d8df9336 --- /dev/null +++ b/tests/fixtures/npm/cases/types-keyed-construction/node_modules/profilekit/index.js @@ -0,0 +1,12 @@ +"use strict"; +// Plain JavaScript: the profile is built key by key. +function loadProfile(id) { + var p = {}; + p.id = id; + p["a;b"] = "semi" + id; + p["my-key"] = "dash"; + p["x y"] = "space"; + p.count = id * 2; + return p; +} +exports.loadProfile = loadProfile; diff --git a/tests/fixtures/npm/cases/types-keyed-construction/node_modules/profilekit/package.json b/tests/fixtures/npm/cases/types-keyed-construction/node_modules/profilekit/package.json new file mode 100644 index 000000000..340a0a6c5 --- /dev/null +++ b/tests/fixtures/npm/cases/types-keyed-construction/node_modules/profilekit/package.json @@ -0,0 +1 @@ +{ "name": "profilekit", "version": "1.0.0", "main": "index.js" } diff --git a/tests/fixtures/npm/cases/types-keyed-construction/package.json b/tests/fixtures/npm/cases/types-keyed-construction/package.json new file mode 100644 index 000000000..24992c953 --- /dev/null +++ b/tests/fixtures/npm/cases/types-keyed-construction/package.json @@ -0,0 +1 @@ +{ "name": "types-keyed-construction-case", "private": true, "type": "module" } diff --git a/tests/fixtures/npm/cases/types-undeclared-quoted-key/main.ts b/tests/fixtures/npm/cases/types-undeclared-quoted-key/main.ts new file mode 100644 index 000000000..d069face6 --- /dev/null +++ b/tests/fixtures/npm/cases/types-undeclared-quoted-key/main.ts @@ -0,0 +1,20 @@ +// A package typed from its JavaScript returns profiles without a key its +// declarations promise ("a;b"). A literal-key read of that key answers +// undefined, as Node's does; it never reads another field. +import { loadProfiles } from "hostilekit"; + +const profiles = loadProfiles(); +for (const p of profiles) { + console.log(p.id, p["a;b"] === undefined, typeof p["a;b"], "a;b" in p); + try { + console.log(p["a;b"].toUpperCase()); + } catch (e) { + console.log("upper threw", e instanceof TypeError); + } + try { + console.log(p["a;b"].length); + } catch (e) { + console.log("length threw", e instanceof TypeError); + } + console.log(p.id.toFixed(1), JSON.stringify(p)); +} diff --git a/tests/fixtures/npm/cases/types-undeclared-quoted-key/node_modules/hostilekit/index.d.ts b/tests/fixtures/npm/cases/types-undeclared-quoted-key/node_modules/hostilekit/index.d.ts new file mode 100644 index 000000000..4bf22dfea --- /dev/null +++ b/tests/fixtures/npm/cases/types-undeclared-quoted-key/node_modules/hostilekit/index.d.ts @@ -0,0 +1,2 @@ +export type Profile = { id: number; "a;b": string }; +export declare function loadProfiles(): Profile[]; diff --git a/tests/fixtures/npm/cases/types-undeclared-quoted-key/node_modules/hostilekit/index.js b/tests/fixtures/npm/cases/types-undeclared-quoted-key/node_modules/hostilekit/index.js new file mode 100644 index 000000000..316501256 --- /dev/null +++ b/tests/fixtures/npm/cases/types-undeclared-quoted-key/node_modules/hostilekit/index.js @@ -0,0 +1,5 @@ +"use strict"; +function loadProfiles() { + return [{ id: 1 }]; +} +exports.loadProfiles = loadProfiles; diff --git a/tests/fixtures/npm/cases/types-undeclared-quoted-key/node_modules/hostilekit/package.json b/tests/fixtures/npm/cases/types-undeclared-quoted-key/node_modules/hostilekit/package.json new file mode 100644 index 000000000..2fdbc5b41 --- /dev/null +++ b/tests/fixtures/npm/cases/types-undeclared-quoted-key/node_modules/hostilekit/package.json @@ -0,0 +1 @@ +{ "name": "hostilekit", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" } diff --git a/tests/fixtures/npm/cases/types-undeclared-quoted-key/package.json b/tests/fixtures/npm/cases/types-undeclared-quoted-key/package.json new file mode 100644 index 000000000..afdb3cf5e --- /dev/null +++ b/tests/fixtures/npm/cases/types-undeclared-quoted-key/package.json @@ -0,0 +1 @@ +{ "name": "types-undeclared-quoted-key-case", "private": true, "type": "module" } diff --git a/tests/fixtures/npm/cases/var-export-redeclared/main.ts b/tests/fixtures/npm/cases/var-export-redeclared/main.ts new file mode 100644 index 000000000..23f014d29 --- /dev/null +++ b/tests/fixtures/npm/cases/var-export-redeclared/main.ts @@ -0,0 +1,3 @@ +// An importer sees the value module.exports holds, not the redeclared binding. +import { ns } from "redeclared-ns"; +console.log(ns.version); diff --git a/tests/fixtures/npm/cases/var-export-redeclared/node_modules/redeclared-ns/index.d.ts b/tests/fixtures/npm/cases/var-export-redeclared/node_modules/redeclared-ns/index.d.ts new file mode 100644 index 000000000..1db3d1444 --- /dev/null +++ b/tests/fixtures/npm/cases/var-export-redeclared/node_modules/redeclared-ns/index.d.ts @@ -0,0 +1 @@ +export declare const ns: { version: number }; diff --git a/tests/fixtures/npm/cases/var-export-redeclared/node_modules/redeclared-ns/index.js b/tests/fixtures/npm/cases/var-export-redeclared/node_modules/redeclared-ns/index.js new file mode 100644 index 000000000..45f29a690 --- /dev/null +++ b/tests/fixtures/npm/cases/var-export-redeclared/node_modules/redeclared-ns/index.js @@ -0,0 +1,5 @@ +// A namespace exported by reference, then redeclared with a new value: +// Node copies the first object into module.exports. +var ns = { version: 1 }; +module.exports = { ns }; +var ns = { version: 2 }; diff --git a/tests/fixtures/npm/cases/var-export-redeclared/node_modules/redeclared-ns/package.json b/tests/fixtures/npm/cases/var-export-redeclared/node_modules/redeclared-ns/package.json new file mode 100644 index 000000000..c18908fd8 --- /dev/null +++ b/tests/fixtures/npm/cases/var-export-redeclared/node_modules/redeclared-ns/package.json @@ -0,0 +1 @@ +{ "name": "redeclared-ns", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" } diff --git a/tests/fixtures/npm/cases/var-export-redeclared/package.json b/tests/fixtures/npm/cases/var-export-redeclared/package.json new file mode 100644 index 000000000..023d0b3d9 --- /dev/null +++ b/tests/fixtures/npm/cases/var-export-redeclared/package.json @@ -0,0 +1 @@ +{ "name": "var-export-redeclared-case", "private": true, "type": "module" } diff --git a/tests/fixtures/npm/cases/zod4-boundary/main.ts b/tests/fixtures/npm/cases/zod4-boundary/main.ts new file mode 100644 index 000000000..b4079d1be --- /dev/null +++ b/tests/fixtures/npm/cases/zod4-boundary/main.ts @@ -0,0 +1,146 @@ +// Values that cross from zod 4.6 (compiled from its JavaScript) into code +// typed by zod's declarations: z.infer, z.input and z.output, identity, +// aliasing and mutation after parse, values zod hands back unchanged, +// callbacks zod calls, typed array work, and caught failures. +import { z } from "zod"; + +const show = (label: string, value: unknown): void => { + console.log(label + " " + JSON.stringify(value)); +}; + +// Typed records: field reads, writes and array work on parsed data. +const Item = z.object({ + sku: z.string(), + price: z.number().nonnegative(), + qty: z.number().int(), + tags: z.array(z.string()).default([]), + note: z.string().optional(), + owner: z.string().nullable(), +}); +type Item = z.infer; +const items: Item[] = z.array(Item).parse([ + { sku: "a", price: 2.5, qty: 4, tags: ["x"], owner: null }, + { sku: "b", price: 10, qty: 1, owner: "ann", note: "fragile" }, + { sku: "c", price: 0.5, qty: 10, tags: ["x", "y"], owner: null }, +]); +const total = items.reduce((sum, it) => sum + it.price * it.qty, 0); +console.log("total", total); +const byTag = new Map(); +for (const it of items) for (const t of it.tags) byTag.set(t, (byTag.get(t) ?? 0) + it.qty); +console.log("byTag", [...byTag].map(([k, v]) => `${k}=${v}`).join(",")); +const sorted = [...items].sort((a, b) => b.price - a.price).map((it) => it.sku); +console.log("sorted", sorted.join(",")); +console.log("optional", items.map((it) => `${it.note ?? "-"}/${it.owner ?? "nobody"}`).join(",")); +console.log("keys", items.map((it) => Object.keys(it).join("+")).join(" ")); + +// Mutation after parse is visible through every reference. +const first = items[0]!; +first.qty += 1; +first.tags.push("z"); +console.log("mutated", items[0]!.qty, items[0]!.tags.join("|"), first === items[0]); +show("item", items[0]); + +// Strict objects reject unknown keys; the error is zod's, caught. +const Strict = z.strictObject({ name: z.string().trim().min(1), email: z.email() }); +type Strict = z.infer; +const person: Strict = Strict.parse({ name: " Ada ", email: "ada@example.com" }); +console.log("strict", person.name.length, person.email.toUpperCase()); +const rejected = Strict.safeParse({ name: "x", email: "x@example.com", extra: 1 }); +console.log("strict bad", rejected.success, rejected.error?.issues.map((i) => i.code).join(",")); + +// z.input and z.output differ across a transform and a default. +const Config = z.object({ + port: z.string().transform((s) => Number(s)), + host: z.string().default("localhost"), + debug: z.enum(["true", "false"]).transform((v) => v === "true"), +}); +type ConfigIn = z.input; +type ConfigOut = z.output; +const raw: ConfigIn = { port: "8080", debug: "true" }; +const config: ConfigOut = Config.parse(raw); +console.log("config", config.port + 1, config.host, config.debug, typeof config.port); + +// A value zod hands back unchanged keeps its identity and its mutations. +const Point = z.custom<{ x: number; y: number }>(); +const original = { x: 1, y: 2 }; +const same = Point.parse(original); +same.x = 10; +console.log("custom", original.x, same === original); +original.y = 20; +console.log("custom back", same.y); + +// A shared object returned by a transform: every parse sees the same one. +const shared = { hits: 0 }; +const Counter = z.string().transform(() => shared); +const c1 = Counter.parse("a"); +const c2 = Counter.parse("b"); +c1.hits += 1; +console.log("shared", c2.hits, shared.hits, c1 === c2); + +// A refine callback reads typed fields of the value zod checks. +const Range = z + .object({ lo: z.number(), hi: z.number() }) + .refine((r) => r.lo <= r.hi, { message: "lo must not exceed hi" }); +console.log("range ok", JSON.stringify(Range.parse({ lo: 1, hi: 3 }))); +const bad = Range.safeParse({ lo: 5, hi: 3 }); +console.log("range bad", bad.success ? "?" : bad.error.issues[0]?.message); + +// safeParse data, a discriminated union, and loose objects' extra keys. +const Event = z.discriminatedUnion("type", [ + z.object({ type: z.literal("click"), x: z.number(), y: z.number() }), + z.object({ type: z.literal("key"), key: z.string() }), +]); +type Event = z.infer; +const describe = (e: Event): string => (e.type === "click" ? `click@${e.x},${e.y}` : `key:${e.key}`); +const r = Event.safeParse({ type: "key", key: "Enter" }); +if (r.success) console.log("event", describe(r.data)); +const events = z.array(Event).parse([ + { type: "click", x: 1, y: 2 }, + { type: "key", key: "a" }, +]); +console.log("events", events.map(describe).join(" ")); +const Loose = z.looseObject({ id: z.number() }); +const loose = Loose.parse({ extra: true, id: 3, more: "m" }); +console.log("loose", loose.id, Object.keys(loose).join(","), JSON.stringify(loose)); + +// A union discriminated by boolean literals, read through a safeParse +// result narrowed to its success arm. +const Outcome = z.discriminatedUnion("ok", [ + z.object({ ok: z.literal(true), value: z.number() }), + z.object({ ok: z.literal(false), reason: z.string() }), +]); +type Outcome = z.infer; +const outcomes: Outcome[] = []; +for (const input of [{ ok: true, value: 41 }, { ok: false, reason: "late" }, { ok: "maybe" }]) { + const parsed = Outcome.safeParse(input); + if (parsed.success) outcomes.push(parsed.data); + else console.log("outcome bad", parsed.error.issues.length); +} +console.log("outcomes", outcomes.map((o) => (o.ok ? `value ${o.value + 1}` : `reason ${o.reason}`)).join(", ")); +const Flags = z.object({ beta: z.boolean().optional(), mode: z.enum(["a", "b"]) }); +const flagged = Flags.safeParse({ beta: true, mode: "b" }); +if (flagged.success) { + const flags = flagged.data; + console.log("flags", flags.beta === true ? "beta on" : "beta off", flags.mode); +} + +// Tuples, brands and coerced query-style input. +const Pair = z.tuple([z.string(), z.number()]); +const [label, count] = Pair.parse(["n", 4]); +console.log("tuple", label, count + 1); +const UserId = z.string().brand<"UserId">(); +const uid = UserId.parse("u_1"); +console.log("brand", uid.length, uid); +const Query = z.object({ page: z.coerce.number().int().min(1).default(1), q: z.string().optional() }); +const query = Query.parse({ page: "3" }); +console.log("query", query.page * 10, query.q === undefined, "q" in query); + +// async parse +async function main(): Promise { + const it = await Item.parseAsync({ sku: "d", price: 1, qty: 2, owner: null }); + console.log("async", it.sku, it.price * it.qty, it.tags.length); + const failed = await Item.safeParseAsync({ sku: 1, price: -1, qty: 0.5, owner: null }); + console.log("async bad", failed.success, failed.error?.issues.map((i) => i.path.join(".")).join(",")); +} + +void main(); diff --git a/tests/fixtures/npm/cases/zod4-boundary/package.json b/tests/fixtures/npm/cases/zod4-boundary/package.json new file mode 100644 index 000000000..39bd81280 --- /dev/null +++ b/tests/fixtures/npm/cases/zod4-boundary/package.json @@ -0,0 +1 @@ +{ "name": "zod4-boundary-case", "private": true, "type": "module" } diff --git a/tests/fixtures/npm/cases/zod4-record/main.ts b/tests/fixtures/npm/cases/zod4-record/main.ts new file mode 100644 index 000000000..b43eaebab --- /dev/null +++ b/tests/fixtures/npm/cases/zod4-record/main.ts @@ -0,0 +1,37 @@ +// zod 4's z.record and z.partialRecord accept only plain objects. Its check +// reads the value's constructor, that constructor's prototype, and whether +// the prototype has its own isPrototypeOf, so it needs Object.prototype's +// reflection to match Node. +import { z } from "zod"; + +const Scores = z.record(z.string(), z.number().int()); +const Flags = z.record(z.enum(["a", "b"]), z.boolean()); +const Partial = z.partialRecord(z.enum(["x", "y", "z"]), z.string()); +const Nested = z.object({ name: z.string(), meta: z.record(z.string(), z.record(z.string(), z.number())) }); + +const show = (label: string, schema: z.ZodType, value: unknown): void => { + const r = schema.safeParse(value); + if (r.success) { + console.log(label, "ok", JSON.stringify(r.data)); + } else { + console.log(label, "fail", r.error.issues.map((i) => `${i.path.join(".")}:${i.code}:${i.message}`).join(" | ")); + } +}; + +show("scores", Scores, { alice: 3, bob: 7 }); +show("scores-empty", Scores, {}); +show("scores-bad-value", Scores, { alice: 1.5 }); +show("scores-array", Scores, [1, 2]); +show("scores-null-proto", Scores, Object.assign(Object.create(null), { k: 1 })); +show("scores-parsed", Scores, JSON.parse('{"x":1,"y":2}')); +show("flags", Flags, { a: true, b: false }); +show("flags-missing", Flags, { a: true }); +show("flags-extra", Flags, { a: true, b: true, c: false }); +show("partial", Partial, { y: "why" }); +show("nested", Nested, { name: "n", meta: { one: { two: 2 } } }); +show("nested-bad", Nested, { name: "n", meta: { one: [] } }); + +class Point { + x = 1; +} +show("class-instance", Scores, new Point()); diff --git a/tests/fixtures/npm/cases/zod4-record/package.json b/tests/fixtures/npm/cases/zod4-record/package.json new file mode 100644 index 000000000..82c365084 --- /dev/null +++ b/tests/fixtures/npm/cases/zod4-record/package.json @@ -0,0 +1 @@ +{ "name": "zod4-record-case", "private": true, "type": "module" } diff --git a/tests/fixtures/npm/node_modules/@hono/zod-validator/dist/index.d.mts b/tests/fixtures/npm/node_modules/@hono/zod-validator/dist/index.d.mts new file mode 100644 index 000000000..6028477a3 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/zod-validator/dist/index.d.mts @@ -0,0 +1,43 @@ +import { InferInput } from "hono/validator"; +import { Context, Env, Input, MiddlewareHandler, TypedResponse, ValidationTargets } from "hono"; +import * as v3 from "zod/v3"; +import { ZodSafeParseResult } from "zod/v4"; +import * as v4 from "zod/v4/core"; +//#region src/index.d.ts +type ZodSchema = v3.ZodType | v4.$ZodType; +type ZodError = T extends v4.$ZodType ? v4.$ZodError> : v3.ZodError; +type ZodSafeParseResult$1 = T3 extends v4.$ZodType ? ZodSafeParseResult : v3.SafeParseReturnType; +type zInput = T extends v3.ZodType ? v3.input : T extends v4.$ZodType ? v4.input : never; +type zOutput = T extends v3.ZodType ? v3.output : T extends v4.$ZodType ? v4.output : never; +type zInfer = T extends v3.ZodType ? v3.infer : T extends v4.$ZodType ? v4.infer : never; +type ZodValidatorFailureBody = T extends v4.$ZodType ? Extract>, { + success: false; +}> : T extends v3.ZodType ? Extract, v3.output>, { + success: false; +}> : never; +type ZodValidatorFailureResponse = Response & TypedResponse, 400, "json">; +type Hook = (result: ({ + success: true; + data: T; +} | { + success: false; + error: ZodError; + data: T; +}) & { + target: Target; +}, c: Context) => Response | void | TypedResponse | Promise>; +type HasUndefined = undefined extends T ? true : false; +type ExtractValidationResponse = VF extends ((value: any, c: any) => infer R) ? R extends Promise ? PR extends TypedResponse ? Response & TypedResponse : PR extends Response ? PR : PR extends undefined ? never : never : R extends TypedResponse ? Response & TypedResponse : R extends Response ? R : R extends undefined ? never : never : never; +type ValidationFunctionOption = { + validationFunction: (schema: T, value: ValidationTargets[Target]) => ZodSafeParseResult$1 | Promise>; +}; +type DefaultInput = { + in: HasUndefined extends true ? { [K in Target]?: [In] extends [ValidationTargets[K]] ? In : InferInput; } : { [K in Target]: [In] extends [ValidationTargets[K]] ? In : InferInput; }; + out: { [K in Target]: Out; }; +}; +declare function zValidatorFunction, Out = zOutput, I extends Input = DefaultInput, V extends I = I>(target: Target, schema: T, hook?: undefined, options?: ValidationFunctionOption): MiddlewareHandler>; +declare function zValidatorFunction, In = zInput, Out = zOutput, I extends Input = DefaultInput, V extends I = I, InferredValue = zInfer>(target: Target, schema: T, hook?: HookFn, options?: ValidationFunctionOption): MiddlewareHandler>; +declare const zValidator: typeof zValidatorFunction; +//#endregion +export { Hook, zValidator }; +//# sourceMappingURL=index.d.mts.map \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/zod-validator/dist/index.mjs b/tests/fixtures/npm/node_modules/@hono/zod-validator/dist/index.mjs new file mode 100644 index 000000000..946d23d2a --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/zod-validator/dist/index.mjs @@ -0,0 +1,28 @@ +import { validator } from "hono/validator"; +//#region src/index.ts +function zValidatorFunction(target, schema, hook, options) { + const caseInsensitiveKeymap = target === "header" && ("_def" in schema || "_zod" in schema) ? Object.fromEntries(Object.keys("in" in schema ? schema.in.shape : schema.shape).map((key) => [key.toLowerCase(), key])) : void 0; + return validator(target, async (value, c) => { + let validatorValue = value; + if (caseInsensitiveKeymap) validatorValue = Object.fromEntries(Object.entries(value).map(([key, value]) => [caseInsensitiveKeymap[key] || key, value])); + const result = options && options.validationFunction ? await options.validationFunction(schema, validatorValue) : await schema.safeParseAsync(validatorValue); + if (hook) { + const hookResult = await hook({ + data: validatorValue, + ...result, + target + }, c); + if (hookResult) { + if (hookResult instanceof Response) return hookResult; + if ("response" in hookResult) return hookResult.response; + } + } + if (!result.success) return c.json(result, 400); + return result.data; + }); +} +const zValidator = zValidatorFunction; +//#endregion +export { zValidator }; + +//# sourceMappingURL=index.mjs.map \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/zod-validator/package.json b/tests/fixtures/npm/node_modules/@hono/zod-validator/package.json new file mode 100644 index 000000000..31355f555 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/zod-validator/package.json @@ -0,0 +1,14 @@ +{ + "name": "@hono/zod-validator", + "version": "0.9.1", + "description": "Test fixture: the ESM build and its declarations.", + "type": "module", + "license": "MIT", + "types": "./dist/index.d.mts", + "exports": { + ".": { + "types": "./dist/index.d.mts", + "import": "./dist/index.mjs" + } + } +} diff --git a/tests/fixtures/npm/node_modules/hono/dist/compose.js b/tests/fixtures/npm/node_modules/hono/dist/compose.js new file mode 100644 index 000000000..ec576df0f --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/compose.js @@ -0,0 +1,50 @@ +//#region src/compose.ts +/** +* Compose middleware functions into a single function based on `koa-compose` package. +* +* @template E - The environment type. +* +* @param {[[Function, unknown], unknown][] | [[Function]][]} middleware - An array of middleware functions and their corresponding parameters. +* @param {ErrorHandler} [onError] - An optional error handler function. +* @param {NotFoundHandler} [onNotFound] - An optional not-found handler function. +* +* @returns {(context: Context, next?: Next) => Promise} - A composed middleware function. +*/ +const compose = (middleware, onError, onNotFound) => { + return (context, next) => { + let index = -1; + return dispatch(0); + /** + * Dispatch the middleware functions. + * + * @param {number} i - The current index in the middleware array. + * + * @returns {Promise} - A promise that resolves to the context. + */ + async function dispatch(i) { + if (i <= index) throw new Error("next() called multiple times"); + index = i; + let res; + let isError = false; + let handler; + if (middleware[i]) { + handler = middleware[i][0][0]; + context.req.routeIndex = i; + } else handler = i === middleware.length && next || void 0; + if (handler) try { + res = await handler(context, () => dispatch(i + 1)); + } catch (err) { + if (err instanceof Error && onError) { + context.error = err; + res = await onError(err, context); + isError = true; + } else throw err; + } + else if (context.finalized === false && onNotFound) res = await onNotFound(context); + if (res && (context.finalized === false || isError)) context.res = res; + return context; + } + }; +}; +//#endregion +export { compose }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/context.js b/tests/fixtures/npm/node_modules/hono/dist/context.js new file mode 100644 index 000000000..7f8184af5 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/context.js @@ -0,0 +1,381 @@ +import { HonoRequest } from "./request.js"; +import { HtmlEscapedCallbackPhase, resolveCallback } from "./utils/html.js"; +//#region src/context.ts +const TEXT_PLAIN = "text/plain; charset=UTF-8"; +const setDefaultContentType = (contentType, headers) => { + return { + "Content-Type": contentType, + ...headers + }; +}; +const createResponseInstance = (body, init) => new Response(body, init); +var Context = class { + #rawRequest; + #req; + /** + * `.env` can get bindings (environment variables, secrets, KV namespaces, D1 database, R2 bucket etc.) in Cloudflare Workers. + * + * @see {@link https://hono.dev/docs/api/context#env} + * + * @example + * ```ts + * // Environment object for Cloudflare Workers + * app.get('*', async c => { + * const counter = c.env.COUNTER + * }) + * ``` + */ + env = {}; + #var; + finalized = false; + /** + * `.error` can get the error object from the middleware if the Handler throws an error. + * + * @see {@link https://hono.dev/docs/api/context#error} + * + * @example + * ```ts + * app.use('*', async (c, next) => { + * await next() + * if (c.error) { + * // do something... + * } + * }) + * ``` + */ + error; + #status; + #executionCtx; + #res; + #layout; + #renderer; + #notFoundHandler; + #preparedHeaders; + #matchResult; + #path; + /** + * Creates an instance of the Context class. + * + * @param req - The Request object. + * @param options - Optional configuration options for the context. + */ + constructor(req, options) { + this.#rawRequest = req; + if (options) { + this.#executionCtx = options.executionCtx; + this.env = options.env; + this.#notFoundHandler = options.notFoundHandler; + this.#path = options.path; + this.#matchResult = options.matchResult; + } + } + /** + * `.req` is the instance of {@link HonoRequest}. + */ + get req() { + this.#req ??= new HonoRequest(this.#rawRequest, this.#path, this.#matchResult); + return this.#req; + } + /** + * @see {@link https://hono.dev/docs/api/context#event} + * The FetchEvent associated with the current request. + * + * @throws Will throw an error if the context does not have a FetchEvent. + */ + get event() { + if (this.#executionCtx && "respondWith" in this.#executionCtx) return this.#executionCtx; + else throw Error("This context has no FetchEvent"); + } + /** + * @see {@link https://hono.dev/docs/api/context#executionctx} + * The ExecutionContext associated with the current request. + * + * @throws Will throw an error if the context does not have an ExecutionContext. + */ + get executionCtx() { + if (this.#executionCtx) return this.#executionCtx; + else throw Error("This context has no ExecutionContext"); + } + /** + * @see {@link https://hono.dev/docs/api/context#res} + * The Response object for the current request. + */ + get res() { + return this.#res ||= createResponseInstance(null, { headers: this.#preparedHeaders ??= new Headers() }); + } + /** + * Sets the Response object for the current request. + * + * @param _res - The Response object to set. + */ + set res(_res) { + if (this.#res && _res) { + _res = createResponseInstance(_res.body, _res); + for (const [k, v] of this.#res.headers.entries()) { + if (k === "content-type") continue; + if (k === "set-cookie") { + const cookies = this.#res.headers.getSetCookie(); + _res.headers.delete("set-cookie"); + for (const cookie of cookies) _res.headers.append("set-cookie", cookie); + } else _res.headers.set(k, v); + } + } + this.#res = _res; + this.finalized = true; + } + /** + * `.render()` can create a response within a layout. + * + * @see {@link https://hono.dev/docs/api/context#render-setrenderer} + * + * @example + * ```ts + * app.get('/', (c) => { + * return c.render('Hello!') + * }) + * ``` + */ + render = (...args) => { + this.#renderer ??= (content) => this.html(content); + return this.#renderer(...args); + }; + /** + * Sets the layout for the response. + * + * @param layout - The layout to set. + * @returns The layout function. + */ + setLayout = (layout) => this.#layout = layout; + /** + * Gets the current layout for the response. + * + * @returns The current layout function. + */ + getLayout = () => this.#layout; + /** + * `.setRenderer()` can set the layout in the custom middleware. + * + * @see {@link https://hono.dev/docs/api/context#render-setrenderer} + * + * @example + * ```tsx + * app.use('*', async (c, next) => { + * c.setRenderer((content) => { + * return c.html( + * + * + *

{content}

+ * + * + * ) + * }) + * await next() + * }) + * ``` + */ + setRenderer = (renderer) => { + this.#renderer = renderer; + }; + /** + * `.header()` can set headers. + * + * @see {@link https://hono.dev/docs/api/context#header} + * + * @example + * ```ts + * app.get('/welcome', (c) => { + * // Set headers + * c.header('X-Message', 'Hello!') + * c.header('Content-Type', 'text/plain') + * + * // Append multiple headers using the append option (e.g. Vary) + * c.header('Vary', 'Accept-Encoding', { append: true }) + * c.header('Vary', 'User-Agent', { append: true }) + * + * return c.body('Thank you for coming') + * }) + * ``` + */ + header = (name, value, options) => { + if (this.finalized) this.#res = createResponseInstance(this.#res.body, this.#res); + const headers = this.#res ? this.#res.headers : this.#preparedHeaders ??= new Headers(); + if (value === void 0) headers.delete(name); + else if (options?.append) headers.append(name, value); + else headers.set(name, value); + }; + status = (status) => { + this.#status = status; + }; + /** + * `.set()` can set the value specified by the key. + * + * @see {@link https://hono.dev/docs/api/context#set-get} + * + * @example + * ```ts + * app.use('*', async (c, next) => { + * c.set('message', 'Hono is hot!!') + * await next() + * }) + * ``` + */ + set = (key, value) => { + this.#var ??= /* @__PURE__ */ new Map(); + this.#var.set(key, value); + }; + /** + * `.get()` can use the value specified by the key. + * + * @see {@link https://hono.dev/docs/api/context#set-get} + * + * @example + * ```ts + * app.get('/', (c) => { + * const message = c.get('message') + * return c.text(`The message is "${message}"`) + * }) + * ``` + */ + get = (key) => { + return this.#var ? this.#var.get(key) : void 0; + }; + /** + * `.var` can access the value of a variable. + * + * @see {@link https://hono.dev/docs/api/context#var} + * + * @example + * ```ts + * const result = c.var.client.oneMethod() + * ``` + */ + get var() { + if (!this.#var) return {}; + return Object.fromEntries(this.#var); + } + #newResponse(data, arg, headers) { + let responseHeaders = this.#res ? new Headers(this.#res.headers) : this.#preparedHeaders; + if (typeof arg === "object" && arg.headers) { + responseHeaders ??= new Headers(); + for (const [key, value] of new Headers(arg.headers)) if (key === "set-cookie") responseHeaders.append(key, value); + else responseHeaders.set(key, value); + } + if (headers) { + if (!responseHeaders) { + let count = 0; + for (const k in headers) if (++count > 1 || typeof headers[k] !== "string") { + responseHeaders = new Headers(); + break; + } + } + if (responseHeaders) for (const k in headers) { + const v = headers[k]; + if (typeof v === "string") responseHeaders.set(k, v); + else { + responseHeaders.delete(k); + for (const v2 of v) responseHeaders.append(k, v2); + } + } + } + const status = typeof arg === "number" ? arg : arg?.status ?? this.#status; + return createResponseInstance(data, { + status, + headers: responseHeaders ?? headers + }); + } + newResponse = (...args) => this.#newResponse(...args); + /** + * `.body()` can return the HTTP response. + * You can set headers with `.header()` and set HTTP status code with `.status`. + * This can also be set in `.text()`, `.json()` and so on. + * + * @see {@link https://hono.dev/docs/api/context#body} + * + * @example + * ```ts + * app.get('/welcome', (c) => { + * // Set headers + * c.header('X-Message', 'Hello!') + * c.header('Content-Type', 'text/plain') + * // Set HTTP status code + * c.status(201) + * + * // Return the response body + * return c.body('Thank you for coming') + * }) + * ``` + */ + body = (data, arg, headers) => this.#newResponse(data, arg, headers); + /** + * `.text()` can render text as `Content-Type:text/plain`. + * + * @see {@link https://hono.dev/docs/api/context#text} + * + * @example + * ```ts + * app.get('/say', (c) => { + * return c.text('Hello!') + * }) + * ``` + */ + text = (text, arg, headers) => { + return !this.#preparedHeaders && !this.#status && !arg && !headers && !this.finalized ? new Response(text) : this.#newResponse(text, arg, setDefaultContentType(TEXT_PLAIN, headers)); + }; + /** + * `.json()` can render JSON as `Content-Type:application/json`. + * + * @see {@link https://hono.dev/docs/api/context#json} + * + * @example + * ```ts + * app.get('/api', (c) => { + * return c.json({ message: 'Hello!' }) + * }) + * ``` + */ + json = (object, arg, headers) => { + return this.#newResponse(JSON.stringify(object), arg, setDefaultContentType("application/json", headers)); + }; + html = (html, arg, headers) => { + const res = (html) => this.#newResponse(html, arg, setDefaultContentType("text/html; charset=UTF-8", headers)); + return typeof html === "object" ? resolveCallback(html, HtmlEscapedCallbackPhase.Stringify, false, {}).then(res) : res(html); + }; + /** + * `.redirect()` can Redirect, default status code is 302. + * + * @see {@link https://hono.dev/docs/api/context#redirect} + * + * @example + * ```ts + * app.get('/redirect', (c) => { + * return c.redirect('/') + * }) + * app.get('/redirect-permanently', (c) => { + * return c.redirect('/', 301) + * }) + * ``` + */ + redirect = (location, status) => { + const locationString = String(location); + this.header("Location", !/[^\x00-\xFF]/.test(locationString) ? locationString : encodeURI(locationString)); + return this.newResponse(null, status ?? 302); + }; + /** + * `.notFound()` can return the Not Found Response. + * + * @see {@link https://hono.dev/docs/api/context#notfound} + * + * @example + * ```ts + * app.get('/notfound', (c) => { + * return c.notFound() + * }) + * ``` + */ + notFound = () => { + this.#notFoundHandler ??= () => createResponseInstance(); + return this.#notFoundHandler(this); + }; +}; +//#endregion +export { Context, TEXT_PLAIN }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/helper/cookie/index.js b/tests/fixtures/npm/node_modules/hono/dist/helper/cookie/index.js new file mode 100644 index 000000000..48f9cbfb7 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/helper/cookie/index.js @@ -0,0 +1,82 @@ +import { parse, parseSigned, serialize, serializeSigned } from "../../utils/cookie.js"; +//#region src/helper/cookie/index.ts +const getCookie = (c, key, prefix) => { + const cookie = c.req.raw.headers.get("Cookie"); + if (typeof key === "string") { + if (!cookie) return; + let finalKey = key; + if (prefix === "secure") finalKey = "__Secure-" + key; + else if (prefix === "host") finalKey = "__Host-" + key; + return parse(cookie, finalKey)[finalKey]; + } + if (!cookie) return {}; + return parse(cookie); +}; +const getSignedCookie = async (c, secret, key, prefix) => { + const cookie = c.req.raw.headers.get("Cookie"); + if (typeof key === "string") { + if (!cookie) return; + let finalKey = key; + if (prefix === "secure") finalKey = "__Secure-" + key; + else if (prefix === "host") finalKey = "__Host-" + key; + return (await parseSigned(cookie, secret, finalKey))[finalKey]; + } + if (!cookie) return {}; + return await parseSigned(cookie, secret); +}; +const generateCookie = (name, value, opt) => { + let cookie; + if (opt?.prefix === "secure") cookie = serialize("__Secure-" + name, value, { + path: "/", + ...opt, + secure: true + }); + else if (opt?.prefix === "host") cookie = serialize("__Host-" + name, value, { + ...opt, + path: "/", + secure: true, + domain: void 0 + }); + else cookie = serialize(name, value, { + path: "/", + ...opt + }); + return cookie; +}; +const setCookie = (c, name, value, opt) => { + const cookie = generateCookie(name, value, opt); + c.header("Set-Cookie", cookie, { append: true }); +}; +const generateSignedCookie = async (name, value, secret, opt) => { + let cookie; + if (opt?.prefix === "secure") cookie = await serializeSigned("__Secure-" + name, value, secret, { + path: "/", + ...opt, + secure: true + }); + else if (opt?.prefix === "host") cookie = await serializeSigned("__Host-" + name, value, secret, { + ...opt, + path: "/", + secure: true, + domain: void 0 + }); + else cookie = await serializeSigned(name, value, secret, { + path: "/", + ...opt + }); + return cookie; +}; +const setSignedCookie = async (c, name, value, secret, opt) => { + const cookie = await generateSignedCookie(name, value, secret, opt); + c.header("set-cookie", cookie, { append: true }); +}; +const deleteCookie = (c, name, opt) => { + const deletedCookie = getCookie(c, name, opt?.prefix); + setCookie(c, name, "", { + ...opt, + maxAge: 0 + }); + return deletedCookie; +}; +//#endregion +export { deleteCookie, generateCookie, generateSignedCookie, getCookie, getSignedCookie, setCookie, setSignedCookie }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/hono-base.js b/tests/fixtures/npm/node_modules/hono/dist/hono-base.js new file mode 100644 index 000000000..7bc399a89 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/hono-base.js @@ -0,0 +1,350 @@ +import { compose } from "./compose.js"; +import { getPath, getPathNoStrict, mergePath } from "./utils/url.js"; +import { Context } from "./context.js"; +import { METHODS } from "./router.js"; +import { COMPOSED_HANDLER } from "./utils/constants.js"; +//#region src/hono-base.ts +/** +* @module +* This module is the base module for the Hono object. +*/ +const notFoundHandler = (c) => { + return c.text("404 Not Found", 404); +}; +const errorHandler = (err, c) => { + if ("getResponse" in err) { + const res = err.getResponse(); + return c.newResponse(res.body, res); + } + console.error(err); + return c.text("Internal Server Error", 500); +}; +var Hono = class Hono { + get; + post; + put; + delete; + options; + patch; + query; + all; + on; + use; + router; + getPath; + _basePath = "/"; + #path = "/"; + routes = []; + constructor(options = {}) { + [...METHODS, "all"].forEach((method) => { + this[method] = (args1, ...args) => { + const methodName = method.toUpperCase(); + if (typeof args1 === "string") this.#path = args1; + else this.#addRoute(methodName, this.#path, args1); + args.forEach((handler) => { + this.#addRoute(methodName, this.#path, handler); + }); + return this; + }; + }); + this.on = (method, path, ...handlers) => { + for (const p of [path].flat()) { + this.#path = p; + for (const m of [method].flat()) { + const methodName = m.toUpperCase(); + for (const handler of handlers) this.#addRoute(methodName, this.#path, handler); + } + } + return this; + }; + this.use = (arg1, ...handlers) => { + if (typeof arg1 === "string") this.#path = arg1; + else { + this.#path = "*"; + handlers.unshift(arg1); + } + handlers.forEach((handler) => { + this.#addRoute("ALL", this.#path, handler); + }); + return this; + }; + const { strict, ...optionsWithoutStrict } = options; + Object.assign(this, optionsWithoutStrict); + this.getPath = strict ?? true ? options.getPath ?? getPath : getPathNoStrict; + } + #clone() { + const clone = new Hono({ + router: this.router, + getPath: this.getPath + }); + clone.errorHandler = this.errorHandler; + clone.#notFoundHandler = this.#notFoundHandler; + clone.routes = this.routes; + return clone; + } + #notFoundHandler = notFoundHandler; + errorHandler = errorHandler; + /** + * `.route()` allows grouping other Hono instance in routes. + * + * @see {@link https://hono.dev/docs/api/routing#grouping} + * + * @param {string} path - base Path + * @param {Hono} app - other Hono instance + * @returns {Hono} routed Hono instance + * + * @example + * ```ts + * const app = new Hono() + * const app2 = new Hono() + * + * app2.get("/user", (c) => c.text("user")) + * app.route("/api", app2) // GET /api/user + * ``` + */ + route(path, app) { + const subApp = this.basePath(path); + app.routes.map((r) => { + let handler; + if (app.errorHandler === errorHandler) handler = r.handler; + else { + handler = async (c, next) => (await compose([], app.errorHandler)(c, () => r.handler(c, next))).res; + handler[COMPOSED_HANDLER] = r.handler; + } + subApp.#addRoute(r.method, r.path, handler, r.basePath); + }); + return this; + } + /** + * `.basePath()` allows base paths to be specified. + * + * @see {@link https://hono.dev/docs/api/routing#base-path} + * + * @param {string} path - base Path + * @returns {Hono} changed Hono instance + * + * @example + * ```ts + * const api = new Hono().basePath('/api') + * ``` + */ + basePath(path) { + const subApp = this.#clone(); + subApp._basePath = mergePath(this._basePath, path); + return subApp; + } + /** + * `.onError()` handles an error and returns a customized Response. + * + * @see {@link https://hono.dev/docs/api/hono#error-handling} + * + * @param {ErrorHandler} handler - request Handler for error + * @returns {Hono} changed Hono instance + * + * @example + * ```ts + * app.onError((err, c) => { + * console.error(`${err}`) + * return c.text('Custom Error Message', 500) + * }) + * ``` + */ + onError = (handler) => { + this.errorHandler = handler; + return this; + }; + /** + * `.notFound()` allows you to customize a Not Found Response. + * + * @see {@link https://hono.dev/docs/api/hono#not-found} + * + * @param {NotFoundHandler} handler - request handler for not-found + * @returns {Hono} changed Hono instance + * + * @example + * ```ts + * app.notFound((c) => { + * return c.text('Custom 404 Message', 404) + * }) + * ``` + */ + notFound = (handler) => { + this.#notFoundHandler = handler; + return this; + }; + /** + * `.mount()` allows you to mount applications built with other frameworks into your Hono application. + * + * @deprecated Use `mount()` from `hono/mount` instead. `.mount()` will be removed in v5. + * + * @see {@link https://hono.dev/docs/api/hono#mount} + * + * @param {string} path - base Path + * @param {Function} applicationHandler - other Request Handler + * @param {MountOptions} [options] - options of `.mount()` + * @returns {Hono} mounted Hono instance + * + * @example + * ```ts + * import { Router as IttyRouter } from 'itty-router' + * import { Hono } from 'hono' + * // Create itty-router application + * const ittyRouter = IttyRouter() + * // GET /itty-router/hello + * ittyRouter.get('/hello', () => new Response('Hello from itty-router')) + * + * const app = new Hono() + * app.mount('/itty-router', ittyRouter.handle) + * ``` + * + * @example + * ```ts + * const app = new Hono() + * // Send the request to another application without modification. + * app.mount('/app', anotherApp, { + * replaceRequest: (req) => req, + * }) + * ``` + */ + mount(path, applicationHandler, options) { + let replaceRequest; + let optionHandler; + if (options) { + if (typeof options === "function") optionHandler = options; + else { + optionHandler = options.optionHandler; + if (options.replaceRequest === false) replaceRequest = (request) => request; + else replaceRequest = options.replaceRequest; + } + } + const getOptions = optionHandler ? (c) => { + const options = optionHandler(c); + return Array.isArray(options) ? options : [options]; + } : (c) => { + let executionContext = void 0; + try { + executionContext = c.executionCtx; + } catch {} + return [c.env, executionContext]; + }; + replaceRequest ||= (() => { + const mergedPath = mergePath(this._basePath, path); + const pathPrefixLength = mergedPath === "/" ? 0 : mergedPath.length; + return (request) => { + const url = new URL(request.url); + url.pathname = this.getPath(request).slice(pathPrefixLength) || "/"; + return new Request(url, request); + }; + })(); + const handler = async (c, next) => { + const res = await applicationHandler(replaceRequest(c.req.raw), ...getOptions(c)); + if (res) return res; + await next(); + }; + this.#addRoute("ALL", mergePath(path, "*"), handler); + return this; + } + #addRoute(method, path, handler, baseRoutePath) { + path = mergePath(this._basePath, path); + const r = { + basePath: baseRoutePath !== void 0 ? mergePath(this._basePath, baseRoutePath) : this._basePath, + path, + method, + handler + }; + this.router.add(method, path, [handler, r]); + this.routes.push(r); + } + #handleError(err, c) { + if (err instanceof Error) return this.errorHandler(err, c); + throw err; + } + #dispatch(request, executionCtx, env, method) { + if (method === "HEAD") return (async () => new Response(null, await this.#dispatch(request, executionCtx, env, "GET")))(); + const path = this.getPath(request, { env }); + const matchResult = this.router.match(method, path); + const c = new Context(request, { + path, + matchResult, + env, + executionCtx, + notFoundHandler: this.#notFoundHandler + }); + if (matchResult[0].length === 1) { + let res; + try { + res = matchResult[0][0][0][0](c, async () => { + c.res = await this.#notFoundHandler(c); + }); + } catch (err) { + return this.#handleError(err, c); + } + return res instanceof Promise ? res.then((resolved) => resolved || (c.finalized ? c.res : this.#notFoundHandler(c))).catch((err) => this.#handleError(err, c)) : res ?? this.#notFoundHandler(c); + } + const composed = compose(matchResult[0], this.errorHandler, this.#notFoundHandler); + return (async () => { + try { + const context = await composed(c); + if (!context.finalized) throw new Error("Context is not finalized. Did you forget to return a Response object or `await next()`?"); + return context.res; + } catch (err) { + return this.#handleError(err, c); + } + })(); + } + /** + * `.fetch()` will be entry point of your app. + * + * @see {@link https://hono.dev/docs/api/hono#fetch} + * + * @param {Request} request - request Object of request + * @param {Env} env - env Object + * @param {ExecutionContext} executionCtx - context of execution + * @returns {Response | Promise} response of request + * + */ + fetch = (request, ...rest) => { + return this.#dispatch(request, rest[1], rest[0], request.method); + }; + /** + * `.request()` is a useful method for testing. + * You can pass a URL or pathname to send a GET request. + * app will return a Response object. + * ```ts + * test('GET /hello is ok', async () => { + * const res = await app.request('/hello') + * expect(res.status).toBe(200) + * }) + * ``` + * @see https://hono.dev/docs/api/hono#request + */ + request = (input, requestInit, Env, executionCtx) => { + if (input instanceof Request) return this.fetch(requestInit ? new Request(input, requestInit) : input, Env, executionCtx); + input = input.toString(); + return this.fetch(new Request(/^https?:\/\//.test(input) ? input : `http://localhost${mergePath("/", input)}`, requestInit), Env, executionCtx); + }; + /** + * `.fire()` automatically adds a global fetch event listener. + * This can be useful for environments that adhere to the Service Worker API, such as non-ES module Cloudflare Workers. + * @deprecated + * Use `fire` from `hono/service-worker` instead. + * ```ts + * import { Hono } from 'hono' + * import { fire } from 'hono/service-worker' + * + * const app = new Hono() + * // ... + * fire(app) + * ``` + * @see https://hono.dev/docs/api/hono#fire + * @see https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API + * @see https://developers.cloudflare.com/workers/reference/migrate-to-module-workers/ + */ + fire = () => { + addEventListener("fetch", (event) => { + event.respondWith(this.#dispatch(event.request, event, void 0, event.request.method)); + }); + }; +}; +//#endregion +export { Hono as HonoBase }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/hono.js b/tests/fixtures/npm/node_modules/hono/dist/hono.js new file mode 100644 index 000000000..9602aafc3 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/hono.js @@ -0,0 +1,28 @@ +import { HonoBase as Hono$1 } from "./hono-base.js"; +import { RegExpRouter } from "./router/reg-exp-router/router.js"; +import "./router/reg-exp-router/index.js"; +import { SmartRouter } from "./router/smart-router/router.js"; +import { TrieRouter } from "./router/trie-router/router.js"; +import "./router/trie-router/index.js"; +//#region src/hono.ts +/** +* The Hono class extends the functionality of the HonoBase class. +* It sets up routing and allows for custom options to be passed. +* +* @template E - The environment type. +* @template S - The schema type. +* @template BasePath - The base path type. +*/ +var Hono = class extends Hono$1 { + /** + * Creates an instance of the Hono class. + * + * @param options - Optional configuration options for the Hono instance. + */ + constructor(options = {}) { + super(options); + this.router = options.router ?? new SmartRouter({ routers: [new RegExpRouter(), new TrieRouter()] }); + } +}; +//#endregion +export { Hono }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/http-exception.js b/tests/fixtures/npm/node_modules/hono/dist/http-exception.js new file mode 100644 index 000000000..800b530cd --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/http-exception.js @@ -0,0 +1,55 @@ +//#region src/http-exception.ts +/** +* `HTTPException` must be used when a fatal error such as authentication failure occurs. +* +* @see {@link https://hono.dev/docs/api/exception} +* +* @param {StatusCode} status - status code of HTTPException +* @param {HTTPExceptionOptions} options - options of HTTPException +* @param {HTTPExceptionOptions["res"]} options.res - response of options of HTTPException +* @param {HTTPExceptionOptions["message"]} options.message - message of options of HTTPException +* @param {HTTPExceptionOptions["cause"]} options.cause - cause of options of HTTPException +* +* @example +* ```ts +* import { HTTPException } from 'hono/http-exception' +* +* // ... +* +* app.post('/auth', async (c, next) => { +* // authentication +* if (authorized === false) { +* throw new HTTPException(401, { message: 'Custom error message' }) +* } +* await next() +* }) +* ``` +*/ +var HTTPException = class extends Error { + res; + status; + /** + * Creates an instance of `HTTPException`. + * @param status - HTTP status code for the exception. Defaults to 500. + * @param options - Additional options for the exception. + */ + constructor(status = 500, options) { + super(options?.message, { cause: options?.cause }); + this.res = options?.res; + this.status = status; + } + /** + * Returns the response object associated with the exception. + * If a response object is not provided, a new response is created with the error message and status code. + * @returns The response object. + */ + getResponse() { + if (this.res) return new Response(this.res.body, { + status: this.status, + headers: this.res.headers + }); + return new Response(this.message, { status: this.status }); + } +}; +//#endregion +export { HTTPException }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/index.js b/tests/fixtures/npm/node_modules/hono/dist/index.js new file mode 100644 index 000000000..20c96572c --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/index.js @@ -0,0 +1,20 @@ +import { Context } from "./context.js"; +import { Hono } from "./hono.js"; +//#region src/index.ts +/** +* @module +* +* Hono - Web Framework built on Web Standards +* +* @example +* ```ts +* import { Hono } from 'hono' +* const app = new Hono() +* +* app.get('/', (c) => c.text('Hono!')) +* +* export default app +* ``` +*/ +//#endregion +export { Context, Hono }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/request.js b/tests/fixtures/npm/node_modules/hono/dist/request.js new file mode 100644 index 000000000..bf130a518 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/request.js @@ -0,0 +1,335 @@ +import { HTTPException } from "./http-exception.js"; +import { GET_MATCH_RESULT } from "./request/constants.js"; +import { parseBody } from "./utils/body.js"; +import { getQueryParam, getQueryParams, tryDecodeURIComponent } from "./utils/url.js"; +//#region src/request.ts +var HonoRequest = class { + /** + * `.raw` can get the raw Request object. + * + * @see {@link https://hono.dev/docs/api/request#raw} + * + * @example + * ```ts + * // For Cloudflare Workers + * app.post('/', async (c) => { + * const metadata = c.req.raw.cf?.hostMetadata? + * ... + * }) + * ``` + */ + raw; + #validatedData; + #matchResult; + routeIndex = 0; + /** + * `.path` can get the pathname of the request. + * + * @see {@link https://hono.dev/docs/api/request#path} + * + * @example + * ```ts + * app.get('/about/me', (c) => { + * const pathname = c.req.path // `/about/me` + * }) + * ``` + */ + path; + bodyCache = {}; + constructor(request, path = "/", matchResult = [[]]) { + this.raw = request; + this.path = path; + this.#matchResult = matchResult; + } + param(key) { + return key ? this.#getDecodedParam(key) : this.#getAllDecodedParams(); + } + #getDecodedParam(key) { + const paramKey = this.#matchResult[0][this.routeIndex]?.[1][key]; + const param = this.#getParamValue(paramKey); + return param && tryDecodeURIComponent(param); + } + #getAllDecodedParams() { + const decoded = {}; + const keys = Object.keys(this.#matchResult[0][this.routeIndex]?.[1] ?? {}); + for (const key of keys) { + const value = this.#getParamValue(this.#matchResult[0][this.routeIndex][1][key]); + if (value !== void 0) decoded[key] = tryDecodeURIComponent(value); + } + return decoded; + } + #getParamValue(paramKey) { + return this.#matchResult[1] ? this.#matchResult[1][paramKey] : paramKey; + } + query(key) { + return getQueryParam(this.url, key); + } + queries(key) { + return getQueryParams(this.url, key); + } + header(name) { + if (name) return this.raw.headers.get(name) ?? void 0; + const headerData = Object.create(null); + this.raw.headers.forEach((value, key) => { + headerData[key] = value; + }); + return headerData; + } + async parseBody(options) { + return parseBody(this, options); + } + #cachedBody = (key) => { + const { bodyCache, raw } = this; + const cachedBody = bodyCache[key]; + if (cachedBody) return cachedBody; + for (const anyCachedKey in bodyCache) return bodyCache[anyCachedKey].then((body) => { + if (anyCachedKey === "json") body = JSON.stringify(body); + const contentType = anyCachedKey === "formData" ? void 0 : raw.headers.get("content-type"); + return new Response(body, { headers: contentType ? { "Content-Type": contentType } : void 0 })[key](); + }); + return bodyCache[key] = raw[key](); + }; + /** + * `.json()` can parse Request body of type `application/json` + * + * @see {@link https://hono.dev/docs/api/request#json} + * + * @example + * ```ts + * app.post('/entry', async (c) => { + * const body = await c.req.json() + * }) + * ``` + */ + json() { + return this.#cachedBody("text").then((text) => JSON.parse(text)); + } + /** + * `.text()` can parse Request body of type `text/plain` + * + * @see {@link https://hono.dev/docs/api/request#text} + * + * @example + * ```ts + * app.post('/entry', async (c) => { + * const body = await c.req.text() + * }) + * ``` + */ + text() { + return this.#cachedBody("text"); + } + /** + * `.arrayBuffer()` parse Request body as an `ArrayBuffer` + * + * @see {@link https://hono.dev/docs/api/request#arraybuffer} + * + * @example + * ```ts + * app.post('/entry', async (c) => { + * const body = await c.req.arrayBuffer() + * }) + * ``` + */ + arrayBuffer() { + return this.#cachedBody("arrayBuffer"); + } + /** + * `.bytes()` parses the request body as a `Uint8Array`. + * + * @see {@link https://hono.dev/docs/api/request#bytes} + * + * @example + * ```ts + * app.post('/entry', async (c) => { + * const body = await c.req.bytes() + * }) + * ``` + */ + bytes() { + return this.#cachedBody("arrayBuffer").then((buffer) => new Uint8Array(buffer)); + } + /** + * Parses the request body as a `Blob`. + * @example + * ```ts + * app.post('/entry', async (c) => { + * const body = await c.req.blob(); + * }); + * ``` + * @see https://hono.dev/docs/api/request#blob + */ + blob() { + return this.#cachedBody("blob"); + } + /** + * Parses the request body as `FormData`. + * @example + * ```ts + * app.post('/entry', async (c) => { + * const body = await c.req.formData(); + * }); + * ``` + * @see https://hono.dev/docs/api/request#formdata + */ + formData() { + return this.#cachedBody("formData"); + } + /** + * Adds validated data to the request. + * + * @param target - The target of the validation. + * @param data - The validated data to add. + */ + addValidatedData(target, data) { + (this.#validatedData ??= {})[target] = data; + } + valid(target) { + return this.#validatedData?.[target]; + } + /** + * `.url` can get the request url strings. + * + * @see {@link https://hono.dev/docs/api/request#url} + * + * @example + * ```ts + * app.get('/about/me', (c) => { + * const url = c.req.url // `http://localhost:8787/about/me` + * ... + * }) + * ``` + */ + get url() { + return this.raw.url; + } + /** + * `.method` can get the method name of the request. + * + * @see {@link https://hono.dev/docs/api/request#method} + * + * @example + * ```ts + * app.get('/about/me', (c) => { + * const method = c.req.method // `GET` + * }) + * ``` + */ + get method() { + return this.raw.method; + } + get [GET_MATCH_RESULT]() { + return this.#matchResult; + } + /** + * `.matchedRoutes` can return a matched route in the handler + * + * @deprecated + * + * Use matchedRoutes helper defined in "hono/route" instead. + * + * @see {@link https://hono.dev/docs/api/request#matchedroutes} + * + * @example + * ```ts + * app.use('*', async function logger(c, next) { + * await next() + * c.req.matchedRoutes.forEach(({ handler, method, path }, i) => { + * const name = handler.name || (handler.length < 2 ? '[handler]' : '[middleware]') + * console.log( + * method, + * ' ', + * path, + * ' '.repeat(Math.max(10 - path.length, 0)), + * name, + * i === c.req.routeIndex ? '<- respond from here' : '' + * ) + * }) + * }) + * ``` + */ + get matchedRoutes() { + return this.#matchResult[0].map(([[, route]]) => route); + } + /** + * `.routePath` can retrieve the path registered within the handler + * + * @deprecated + * + * Use routePath helper defined in "hono/route" instead. + * + * @see {@link https://hono.dev/docs/api/request#routepath} + * + * @example + * ```ts + * app.get('/posts/:id', (c) => { + * return c.json({ path: c.req.routePath }) + * }) + * ``` + */ + get routePath() { + return this.#matchResult[0].map(([[, route]]) => route)[this.routeIndex].path; + } +}; +/** +* Clones a HonoRequest's underlying raw Request object. +* +* This utility handles both consumed and unconsumed request bodies: +* - If the request body hasn't been consumed, it uses the native `clone()` method +* - If the request body has been consumed, it reconstructs a new Request using cached body data +* +* This is particularly useful when you need to: +* - Process the same request body multiple times +* - Pass requests to external services after validation +* +* @param req - The HonoRequest object to clone +* @returns A Promise that resolves to a new Request object with the same properties +* @throws {HTTPException} If the request body was consumed directly via `req.raw` +* without using HonoRequest methods (e.g., `req.json()`, `req.text()`), making it +* impossible to reconstruct the body from cache +* +* @example +* ```ts +* // Clone after consuming the body (e.g., after validation) +* app.post('/forward', +* validator('json', (data) => data), +* async (c) => { +* const validated = c.req.valid('json') +* // Body has been consumed, but cloneRawRequest still works +* const clonedReq = await cloneRawRequest(c.req) +* return fetch('http://backend-service.com', clonedReq) +* } +* ) +* ``` +*/ +const cloneRawRequest = async (req) => { + if (!req.raw.bodyUsed) return req.raw.clone(); + const cacheKey = Object.keys(req.bodyCache)[0]; + if (!cacheKey) throw new HTTPException(500, { message: "Cannot clone request: body was already consumed and not cached. Please use HonoRequest methods (e.g., req.json(), req.text()) instead of consuming req.raw directly." }); + let body = await req[cacheKey](); + const headers = req.header(); + if (cacheKey === "json") { + body = JSON.stringify(body); + delete headers["content-length"]; + } else if (body instanceof FormData) { + delete headers["content-type"]; + delete headers["content-length"]; + } + const requestInit = { + body, + cache: req.raw.cache, + credentials: req.raw.credentials, + headers, + integrity: req.raw.integrity, + keepalive: req.raw.keepalive, + method: req.method, + mode: req.raw.mode, + redirect: req.raw.redirect, + referrer: req.raw.referrer, + referrerPolicy: req.raw.referrerPolicy, + signal: req.raw.signal + }; + return new Request(req.url, requestInit); +}; +//#endregion +export { HonoRequest, cloneRawRequest }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/request/constants.js b/tests/fixtures/npm/node_modules/hono/dist/request/constants.js new file mode 100644 index 000000000..0f2734437 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/request/constants.js @@ -0,0 +1,4 @@ +//#region src/request/constants.ts +const GET_MATCH_RESULT = Symbol(); +//#endregion +export { GET_MATCH_RESULT }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/router.js b/tests/fixtures/npm/node_modules/hono/dist/router.js new file mode 100644 index 000000000..52ea7f5c2 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/router.js @@ -0,0 +1,35 @@ +//#region src/router.ts +/** +* @module +* This module provides types definitions and variables for the routers. +*/ +/** +* Constant representing all HTTP methods in uppercase. +*/ +const METHOD_NAME_ALL = "ALL"; +/** +* Constant representing all HTTP methods in lowercase. +*/ +const METHOD_NAME_ALL_LOWERCASE = "all"; +/** +* Array of supported HTTP methods. +*/ +const METHODS = [ + "get", + "post", + "put", + "delete", + "options", + "patch", + "query" +]; +/** +* Error message indicating that a route cannot be added because the matcher is already built. +*/ +const MESSAGE_MATCHER_IS_ALREADY_BUILT = "Can not add a route since the matcher is already built."; +/** +* Error class representing an unsupported path error. +*/ +var UnsupportedPathError = class extends Error {}; +//#endregion +export { MESSAGE_MATCHER_IS_ALREADY_BUILT, METHODS, METHOD_NAME_ALL, METHOD_NAME_ALL_LOWERCASE, UnsupportedPathError }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/index.js b/tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/index.js new file mode 100644 index 000000000..38dfdd48a --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/index.js @@ -0,0 +1,3 @@ +import { RegExpRouter } from "./router.js"; +import { PreparedRegExpRouter, buildInitParams, serializeInitParams } from "./prepared-router.js"; +export { PreparedRegExpRouter, RegExpRouter, buildInitParams, serializeInitParams }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/matcher.js b/tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/matcher.js new file mode 100644 index 000000000..97ac8435c --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/matcher.js @@ -0,0 +1,19 @@ +import "../../router.js"; +//#region src/router/reg-exp-router/matcher.ts +const emptyParam = []; +function match(method, path) { + const matchers = this.buildAllMatchers(); + const match = ((method, path) => { + const matcher = matchers[method] || matchers["ALL"]; + const staticMatch = matcher[2][path]; + if (staticMatch) return staticMatch; + const match = path.match(matcher[0]); + if (!match) return [[], emptyParam]; + const index = match.indexOf("", 1); + return [matcher[1][index], match]; + }); + this.match = match; + return match(method, path); +} +//#endregion +export { emptyParam, match }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/node.js b/tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/node.js new file mode 100644 index 000000000..6738b9e98 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/node.js @@ -0,0 +1,91 @@ +import { createNullObject } from "../utils.js"; +//#region src/router/reg-exp-router/node.ts +const LABEL_REG_EXP_STR = "[^/]+"; +const ONLY_WILDCARD_REG_EXP_STR = ".*"; +const TAIL_WILDCARD_REG_EXP_STR = "(?:|/.*)"; +const PATH_ERROR = Symbol(); +const regExpMetaChars = /* @__PURE__ */ new Set(".\\+*[^]$()"); +/** +* Sort order: +* 1. literal +* 2. special pattern (e.g. :label{[0-9]+}) +* 3. common label pattern (e.g. :label) +* 4. wildcard +*/ +function compareKey(a, b) { + if (a.length === 1) return b.length === 1 ? a < b ? -1 : 1 : -1; + if (b.length === 1) return 1; + if (a === ".*" || a === "(?:|/.*)") return b === "(?:|/.*)" ? -1 : 1; + else if (b === ".*" || b === "(?:|/.*)") return -1; + if (a === "[^/]+") return 1; + else if (b === "[^/]+") return -1; + return a.length === b.length ? a < b ? -1 : 1 : b.length - a.length; +} +var Node = class Node { + #index; + #varIndex; + #children = createNullObject(); + insert(tokens, index, paramMap, context, isStatic) { + let node = this; + for (let i = 0, len = tokens.length; i < len; i++) { + const token = tokens[i]; + const pattern = token.length === 1 ? token === "*" ? i === len - 1 ? [ + "", + "", + ".*" + ] : [ + "", + "", + LABEL_REG_EXP_STR + ] : null : token === "/*" ? [ + "", + "", + TAIL_WILDCARD_REG_EXP_STR + ] : token.match(/^\:([^\{\}]+)(?:\{(.+)\})?$/); + let nextNode; + if (pattern) { + const name = pattern[1]; + let regexpStr = pattern[2] || "[^/]+"; + if (name && pattern[2]) { + if (regexpStr === ".*") throw PATH_ERROR; + regexpStr = regexpStr.replace(/^\((?!\?:)(?=[^)]+\)$)/, "(?:"); + if (/\((?!\?:)/.test(regexpStr)) throw PATH_ERROR; + if (regexpStr.length === 1 && regExpMetaChars.has(regexpStr)) throw PATH_ERROR; + } + nextNode = node.#children[regexpStr]; + if (!nextNode) { + if (regexpStr !== ".*" && regexpStr !== "(?:|/.*)") { + for (const k in node.#children) if ((regexpStr.length > 1 || k.length > 1) && k !== ".*" && k !== "(?:|/.*)") throw PATH_ERROR; + } + nextNode = node.#children[regexpStr] = new Node(); + } + if (name !== "") { + nextNode.#varIndex ??= context.varIndex++; + paramMap.push([name, nextNode.#varIndex]); + } + } else { + nextNode = node.#children[token]; + if (!nextNode) { + for (const k in node.#children) if (k.length > 1 && k !== ".*" && k !== "(?:|/.*)") throw PATH_ERROR; + nextNode = node.#children[token] = new Node(); + } + } + node = nextNode; + } + if (node.#index !== void 0) throw PATH_ERROR; + node.#index = isStatic ? -1 : index; + } + buildRegExpStr() { + const strList = Object.keys(this.#children).sort(compareKey).map((k) => { + const c = this.#children[k]; + const childStr = c.buildRegExpStr(); + return childStr === "" ? "" : (typeof c.#varIndex === "number" ? `(${k})@${c.#varIndex}` : regExpMetaChars.has(k) ? `\\${k}` : k) + childStr; + }).filter(Boolean); + if (typeof this.#index === "number" && this.#index !== -1) strList.unshift(`#${this.#index}`); + if (strList.length === 0) return ""; + if (strList.length === 1) return strList[0]; + return "(?:" + strList.join("|") + ")"; + } +}; +//#endregion +export { LABEL_REG_EXP_STR, Node, ONLY_WILDCARD_REG_EXP_STR, PATH_ERROR, TAIL_WILDCARD_REG_EXP_STR }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/prepared-router.js b/tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/prepared-router.js new file mode 100644 index 000000000..c9a8c1314 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/prepared-router.js @@ -0,0 +1,94 @@ +import "../../router.js"; +import { emptyParam, match } from "./matcher.js"; +import { RegExpRouter } from "./router.js"; +//#region src/router/reg-exp-router/prepared-router.ts +var PreparedRegExpRouter = class { + name = "PreparedRegExpRouter"; + #matchers; + #relocateMap; + constructor(matchers, relocateMap) { + this.#matchers = matchers; + this.#relocateMap = relocateMap; + } + #addWildcard(method, handlerData) { + const matcher = this.#matchers[method]; + matcher[1].forEach((list) => list && list.push(handlerData)); + Object.values(matcher[2]).forEach((list) => list[0].push(handlerData)); + } + #addPath(method, path, handler, indexes, map) { + const matcher = this.#matchers[method]; + if (!map) matcher[2][path][0].push([handler, {}]); + else indexes.forEach((index) => { + if (typeof index === "number") matcher[1][index].push([handler, map]); + else matcher[2][index || path][0].push([handler, map]); + }); + } + add(method, path, handler) { + if (!this.#matchers[method]) { + const all = this.#matchers["ALL"]; + const staticMap = {}; + for (const key in all[2]) staticMap[key] = [all[2][key][0].slice(), emptyParam]; + this.#matchers[method] = [ + all[0], + all[1].map((list) => Array.isArray(list) ? list.slice() : 0), + staticMap + ]; + } + if (path === "/*" || path === "*") { + const handlerData = [handler, {}]; + if (method === "ALL") for (const m in this.#matchers) this.#addWildcard(m, handlerData); + else this.#addWildcard(method, handlerData); + return; + } + const data = this.#relocateMap[path]; + if (!data) throw new Error(`Path ${path} is not registered`); + for (const [indexes, map] of data) if (method === "ALL") for (const m in this.#matchers) this.#addPath(m, path, handler, indexes, map); + else this.#addPath(method, path, handler, indexes, map); + } + buildAllMatchers() { + return this.#matchers; + } + match = match; +}; +const buildInitParams = ({ paths }) => { + const RegExpRouterWithMatcherExport = class extends RegExpRouter { + buildAndExportAllMatchers() { + return this.buildAllMatchers(); + } + }; + const router = new RegExpRouterWithMatcherExport(); + for (const path of paths) router.add("ALL", path, path); + const matchers = router.buildAndExportAllMatchers(); + const all = matchers["ALL"]; + const relocateMap = {}; + for (const path of paths) { + if (path === "/*" || path === "*") continue; + all[1].forEach((list, i) => { + list.forEach(([p, map]) => { + if (p === path) { + if (relocateMap[path]) relocateMap[path][0][1] = { + ...relocateMap[path][0][1], + ...map + }; + else relocateMap[path] = [[[], map]]; + if (relocateMap[path][0][0].findIndex((j) => j === i) === -1) relocateMap[path][0][0].push(i); + } + }); + }); + for (const path2 in all[2]) all[2][path2][0].forEach(([p]) => { + if (p === path) { + relocateMap[path] ||= [[[]]]; + const value = path2 === path ? "" : path2; + if (relocateMap[path][0][0].findIndex((v) => v === value) === -1) relocateMap[path][0][0].push(value); + } + }); + } + for (let i = 0, len = all[1].length; i < len; i++) all[1][i] = all[1][i] ? [] : 0; + for (const path in all[2]) all[2][path][0] = []; + return [matchers, relocateMap]; +}; +const serializeInitParams = ([matchers, relocateMap]) => { + return `[${JSON.stringify(matchers, (_, value) => value instanceof RegExp ? `##${value.toString()}##` : value).replace(/"##(.+?)##"/g, (_, str) => str.replace(/\\\\/g, "\\"))},${JSON.stringify(relocateMap)}]`; +}; +//#endregion +export { PreparedRegExpRouter, buildInitParams, serializeInitParams }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/router.js b/tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/router.js new file mode 100644 index 000000000..21ef235d1 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/router.js @@ -0,0 +1,101 @@ +import { checkOptionalParameter } from "../../utils/url.js"; +import { MESSAGE_MATCHER_IS_ALREADY_BUILT, UnsupportedPathError } from "../../router.js"; +import { createNullObject } from "../utils.js"; +import { emptyParam, match } from "./matcher.js"; +import { LABEL_REG_EXP_STR, PATH_ERROR, TAIL_WILDCARD_REG_EXP_STR } from "./node.js"; +import { Trie } from "./trie.js"; +//#region src/router/reg-exp-router/router.ts +let wildcardRegExpCache = createNullObject(); +function buildWildcardRegExp(path) { + return wildcardRegExpCache[path] ??= new RegExp(`^${path.replace(/\/:[^/{}]+(?:\{\[\^\/]\+})?(?=[/{]|$)|\/?\*$|([.\\+*[^\]$()?{}|])/g, (match, metaChar) => metaChar ? `\\${metaChar}` : match === "/*" ? TAIL_WILDCARD_REG_EXP_STR : match === "*" ? ".*" : `/:${LABEL_REG_EXP_STR}`)}$`); +} +function findMiddleware(middleware, path) { + for (const k of Object.keys(middleware).sort((a, b) => b.length - a.length)) if (buildWildcardRegExp(k).test(path)) return [...middleware[k]]; +} +var RegExpRouter = class { + name = "RegExpRouter"; + #middleware; + #routes; + #tries; + constructor() { + this.#middleware = { ["ALL"]: createNullObject() }; + this.#routes = { ["ALL"]: createNullObject() }; + this.#tries = { ["ALL"]: new Trie() }; + } + #insertPath(method, path) { + try { + this.#tries[method].insert(path, !/\*|\/:/.test(path)); + } catch (e) { + throw e === PATH_ERROR ? new UnsupportedPathError(path) : e; + } + } + add(method, path, handler) { + const middleware = this.#middleware; + const routes = this.#routes; + if (!middleware) throw new Error(MESSAGE_MATCHER_IS_ALREADY_BUILT); + if (!middleware[method]) { + this.#tries[method] = new Trie(); + for (const handlerMap of [middleware, routes]) { + handlerMap[method] = createNullObject(); + for (const p in handlerMap["ALL"]) { + handlerMap[method][p] = [...handlerMap["ALL"][p]]; + this.#insertPath(method, p); + } + } + } + if (path === "/*") path = "*"; + const methods = method === "ALL" ? Object.keys(middleware) : [method]; + if (/\*$/.test(path)) { + const re = buildWildcardRegExp(path); + for (const m of methods) if (!middleware[m][path]) { + this.#insertPath(m, path); + middleware[m][path] = findMiddleware(middleware[m], path) || findMiddleware(middleware["ALL"], path) || []; + } + for (const handlerMap of [middleware, routes]) for (const m of methods) for (const p in handlerMap[m]) re.test(p) && handlerMap[m][p].push([handler, path]); + return; + } + const paths = checkOptionalParameter(path) || [path]; + for (const path of paths) for (const m of methods) { + if (!routes[m][path]) { + this.#insertPath(m, path); + routes[m][path] = findMiddleware(middleware[m], path) || findMiddleware(middleware["ALL"], path) || []; + } + routes[m][path].push([handler, path]); + } + } + match = match; + buildAllMatchers() { + const matchers = createNullObject(); + for (const method of Object.keys(this.#routes)) matchers[method] = this.#buildMatcher(method); + this.#middleware = this.#routes = this.#tries = void 0; + wildcardRegExpCache = createNullObject(); + return matchers; + } + #buildMatcher(method) { + const middleware = this.#middleware[method]; + const routes = this.#routes[method]; + const trie = this.#tries[method]; + const staticMap = createNullObject(); + const handlerData = []; + const [regexp, indexReplacementMap, paramReplacementMap] = trie.buildRegExp(); + for (const r of [middleware, routes]) for (const path in r) { + const handlers = r[path]; + const pathData = trie.paths[path]; + if (!pathData) { + staticMap[path] = [handlers.map(([h]) => [h, createNullObject()]), emptyParam]; + continue; + } + handlerData[pathData[0]] = handlers.map(([h, handlerPath]) => [h, trie.paths[handlerPath][1].reduceRight((map, [key], i) => { + map[key] = paramReplacementMap[pathData[1][i][1]]; + return map; + }, createNullObject())]); + } + return [ + regexp, + indexReplacementMap.map((i) => handlerData[i]), + staticMap + ]; + } +}; +//#endregion +export { RegExpRouter }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/trie.js b/tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/trie.js new file mode 100644 index 000000000..a995678c0 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/trie.js @@ -0,0 +1,73 @@ +import { createNullObject } from "../utils.js"; +import { Node } from "./node.js"; +//#region src/router/reg-exp-router/trie.ts +var Trie = class { + #context = { varIndex: 0 }; + #root = new Node(); + #index = 0; + paths = createNullObject(); + insert(path, isStatic) { + if (isStatic) { + this.#root.insert(path.split(""), 0, [], this.#context, true); + return; + } + const paramAssoc = []; + const groups = []; + let markedPath = path; + for (let i = 0;;) { + let replaced = false; + markedPath = markedPath.replace(/\{[^}]+\}/g, (m) => { + const mark = `@\\${i}`; + groups[i] = [mark, m]; + i++; + replaced = true; + return mark; + }); + if (!replaced) break; + } + /** + * - pattern (:label, :label{0-9]+}, ...) + * - /* wildcard + * - character + */ + const tokens = markedPath.match(/(?::[^\/]+)|(?:\/\*$)|./g) || []; + for (let i = groups.length - 1; i >= 0; i--) { + const [mark] = groups[i]; + for (let j = tokens.length - 1; j >= 0; j--) if (tokens[j].indexOf(mark) !== -1) { + tokens[j] = tokens[j].replace(mark, groups[i][1]); + break; + } + } + this.#root.insert(tokens, this.#index, paramAssoc, this.#context, false); + this.paths[path] = [this.#index++, paramAssoc]; + } + buildRegExp() { + let regexp = this.#root.buildRegExpStr(); + if (regexp === "") return [ + /^$/, + [], + [] + ]; + let captureIndex = 0; + const indexReplacementMap = []; + const paramReplacementMap = []; + regexp = regexp.replace(/#(\d+)|@(\d+)|\.\*\$/g, (_, handlerIndex, paramIndex) => { + if (handlerIndex !== void 0) { + indexReplacementMap[++captureIndex] = Number(handlerIndex); + return "$()"; + } + if (paramIndex !== void 0) { + paramReplacementMap[Number(paramIndex)] = ++captureIndex; + return ""; + } + return ""; + }); + return [ + new RegExp(`^${regexp}`), + indexReplacementMap, + paramReplacementMap + ]; + } +}; +//#endregion +export { Trie }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/router/smart-router/router.js b/tests/fixtures/npm/node_modules/hono/dist/router/smart-router/router.js new file mode 100644 index 000000000..a115ab938 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/router/smart-router/router.js @@ -0,0 +1,49 @@ +import { MESSAGE_MATCHER_IS_ALREADY_BUILT, UnsupportedPathError } from "../../router.js"; +//#region src/router/smart-router/router.ts +var SmartRouter = class { + name = "SmartRouter"; + #routers = []; + #routes = []; + constructor(init) { + this.#routers = init.routers; + } + add(method, path, handler) { + if (!this.#routes) throw new Error(MESSAGE_MATCHER_IS_ALREADY_BUILT); + this.#routes.push([ + method, + path, + handler + ]); + } + match(method, path) { + if (!this.#routes) throw new Error("Fatal error"); + const routers = this.#routers; + const routes = this.#routes; + const len = routers.length; + let i = 0; + let res; + for (; i < len; i++) { + const router = routers[i]; + try { + for (let i = 0, len = routes.length; i < len; i++) router.add(...routes[i]); + res = router.match(method, path); + } catch (e) { + if (e instanceof UnsupportedPathError) continue; + throw e; + } + this.match = router.match.bind(router); + this.#routers = [router]; + this.#routes = void 0; + break; + } + if (i === len) throw new Error("Fatal error"); + this.name = `SmartRouter + ${this.activeRouter.name}`; + return res; + } + get activeRouter() { + if (this.#routes || this.#routers.length !== 1) throw new Error("No active router has been determined yet."); + return this.#routers[0]; + } +}; +//#endregion +export { SmartRouter }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/router/trie-router/index.js b/tests/fixtures/npm/node_modules/hono/dist/router/trie-router/index.js new file mode 100644 index 000000000..8ea5b3150 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/router/trie-router/index.js @@ -0,0 +1,2 @@ +import { TrieRouter } from "./router.js"; +export { TrieRouter }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/router/trie-router/node.js b/tests/fixtures/npm/node_modules/hono/dist/router/trie-router/node.js new file mode 100644 index 000000000..c33c7af1e --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/router/trie-router/node.js @@ -0,0 +1,134 @@ +import { getPattern, splitPath, splitRoutingPath } from "../../utils/url.js"; +import "../../router.js"; +import { createNullObject } from "../utils.js"; +//#region src/router/trie-router/node.ts +const emptyParams = createNullObject(); +let order = 0; +var Node = class Node { + #methods = []; + #children = createNullObject(); + #patterns = []; + #pattern; + #params = emptyParams; + insert(method, path, handler) { + let curNode = this; + const parts = splitRoutingPath(path); + const possibleKeys = /* @__PURE__ */ new Set(); + let i = 0; + for (const p of parts) { + const nextP = parts[++i]; + const pattern = getPattern(p, nextP) || (nextP === void 0 && p && p.indexOf("*") === p.length - 1 ? p : null); + const isParam = Array.isArray(pattern); + const key = isParam ? pattern[0] : pattern || p; + const child = curNode.#children[key] ||= new Node(); + if (pattern && !child.#pattern) { + child.#pattern = pattern; + curNode.#patterns.push(child); + } + curNode = child; + if (isParam) possibleKeys.add(pattern[1]); + } + curNode.#methods.push({ [method]: { + handler, + possibleKeys: [...possibleKeys], + score: ++order + } }); + } + #pushHandlerSets(handlerSets, node, method, nodeParams, params) { + for (let i = 0, len = node.#methods.length; i < len; i++) { + const m = node.#methods[i]; + const handlerSet = m[method] || m["ALL"]; + if (handlerSet) { + handlerSet.params = createNullObject(); + handlerSets.push(handlerSet); + for (let i = 0, len = handlerSet.possibleKeys.length; i < len; i++) { + const key = handlerSet.possibleKeys[i]; + handlerSet.params[key] = params?.[key] && !i ? params[key] : nodeParams[key] ?? params?.[key]; + } + } + } + } + search(method, path) { + const handlerSets = []; + this.#params = emptyParams; + let curNodes = [this]; + const parts = splitPath(path); + const curNodesQueue = []; + const len = parts.length; + let partOffsets = null; + for (let i = 0; i < len; i++) { + const part = parts[i]; + const isLast = i === len - 1; + const tempNodes = []; + for (let j = 0, len2 = curNodes.length; j < len2; j++) { + const node = curNodes[j]; + const nextNode = node.#children[part]; + if (nextNode) { + nextNode.#params = node.#params; + if (isLast) { + if (nextNode.#children["*"]) this.#pushHandlerSets(handlerSets, nextNode.#children["*"], method, node.#params); + this.#pushHandlerSets(handlerSets, nextNode, method, node.#params); + } else tempNodes.push(nextNode); + } + for (const child of node.#patterns) { + const pattern = child.#pattern; + const params = node.#params === emptyParams ? {} : { ...node.#params }; + if (typeof pattern === "string") { + if (pattern === "*" || part.startsWith(pattern.slice(0, -1))) { + this.#pushHandlerSets(handlerSets, child, method, node.#params); + if (pattern === "*") { + child.#params = params; + tempNodes.push(child); + } + } + continue; + } + const [, name, matcher] = pattern; + if (!part && matcher === true) continue; + if (matcher !== true) { + if (!partOffsets) { + partOffsets = []; + let offset = path[0] === "/" ? 1 : 0; + for (let p = 0; p < len; p++) { + partOffsets[p] = offset; + offset += parts[p].length + 1; + } + } + const restPathString = path.slice(partOffsets[i]); + const m = matcher.exec(restPathString); + if (m) { + params[name] = m[0]; + this.#pushHandlerSets(handlerSets, child, method, node.#params, params); + if (m[0].length === restPathString.length && child.#children["*"]) this.#pushHandlerSets(handlerSets, child.#children["*"], method, node.#params, params); + for (const _ in child.#children) { + child.#params = params; + const componentCount = m[0].match(/\//g)?.length ?? 0; + (curNodesQueue[componentCount] ||= []).push(child); + break; + } + continue; + } + } + if (matcher === true || matcher.test(part)) { + params[name] = part; + if (isLast) { + this.#pushHandlerSets(handlerSets, child, method, params, node.#params); + if (child.#children["*"]) this.#pushHandlerSets(handlerSets, child.#children["*"], method, params, node.#params); + } else { + child.#params = params; + tempNodes.push(child); + } + } + } + } + const shifted = curNodesQueue.shift(); + curNodes = shifted ? tempNodes.concat(shifted) : tempNodes; + } + if (handlerSets[1]) handlerSets.sort((a, b) => { + return a.score - b.score; + }); + return [handlerSets.map(({ handler, params }) => [handler, params])]; + } +}; +//#endregion +export { Node }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/router/trie-router/router.js b/tests/fixtures/npm/node_modules/hono/dist/router/trie-router/router.js new file mode 100644 index 000000000..96f4c9351 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/router/trie-router/router.js @@ -0,0 +1,15 @@ +import { checkOptionalParameter } from "../../utils/url.js"; +import { Node } from "./node.js"; +//#region src/router/trie-router/router.ts +var TrieRouter = class { + name = "TrieRouter"; + #node = new Node(); + add(method, path, handler) { + for (const result of checkOptionalParameter(path) || [path]) this.#node.insert(method, result, handler); + } + match(method, path) { + return this.#node.search(method, path); + } +}; +//#endregion +export { TrieRouter }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/router/utils.js b/tests/fixtures/npm/node_modules/hono/dist/router/utils.js new file mode 100644 index 000000000..3f1e9d328 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/router/utils.js @@ -0,0 +1,4 @@ +//#region src/router/utils.ts +const createNullObject = () => Object.create(null); +//#endregion +export { createNullObject }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/types/client/types.d.ts b/tests/fixtures/npm/node_modules/hono/dist/types/client/types.d.ts new file mode 100644 index 000000000..81dc47313 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/types/client/types.d.ts @@ -0,0 +1,204 @@ +import { METHODS, METHOD_NAME_ALL_LOWERCASE } from "../router.js"; +import { HonoBase as Hono } from "../hono-base.js"; +import { StatusCode, SuccessStatusCode } from "../utils/http-status.js"; +import { HasRequiredKeys } from "../utils/types.js"; +import { Endpoint, ExtractSchema, KnownResponseFormat, ResponseFormat, Schema } from "../types.js"; +import { Hono as Hono$1 } from "../hono.js"; +//#region src/client/types.d.ts +/** + * Type representing the '$all' method name + */ +type MethodNameAll = `$${typeof METHOD_NAME_ALL_LOWERCASE}`; +/** + * Type representing all standard HTTP methods prefixed with '$' + * e.g., '$get' | '$post' | '$put' | '$delete' | '$options' | '$patch' | '$query' + */ +type StandardMethods = `$${(typeof METHODS)[number]}`; +/** + * Expands '$all' into all standard HTTP methods. + * If the schema contains '$all', it creates a type where all standard HTTP methods + * point to the same endpoint definition as '$all', while removing '$all' itself. + */ +type ExpandAllMethod = MethodNameAll extends keyof S ? { [M in StandardMethods]: S[MethodNameAll]; } & Omit : S; +type HonoRequest = (typeof Hono$1.prototype)['request']; +export type BuildSearchParamsFn = (query: Record) => URLSearchParams; +export type ClientRequestOptions = { + fetch?: typeof fetch | HonoRequest; + webSocket?: (...args: ConstructorParameters) => WebSocket; + /** + * Standard `RequestInit`, caution that this take highest priority + * and could be used to overwrite things that Hono sets for you, like `body | method | headers`. + * + * If you want to add some headers, use in `headers` instead of `init` + */ + init?: RequestInit; + /** + * Custom function to serialize query parameters into URLSearchParams. + * By default, arrays are serialized as multiple parameters with the same key (e.g., `key=a&key=b`). + * You can provide a custom function to change this behavior, for example to use bracket notation (e.g., `key[]=a&key[]=b`). + * + * @example + * ```ts + * const client = hc('http://localhost', { + * buildSearchParams: (query) => { + * return new URLSearchParams(qs.stringify(query)) + * } + * }) + * ``` + */ + buildSearchParams?: BuildSearchParamsFn; +} & (keyof T extends never ? { + headers?: Record | (() => Record | Promise>); +} : { + headers: T | (() => T | Promise); +}); +export type ClientRequest = { [M in keyof ExpandAllMethod]: ExpandAllMethod[M] extends Endpoint & { + input: infer R; +} ? R extends object ? HasRequiredKeys extends true ? (args: R, options?: ClientRequestOptions) => Promise[M]>> : (args?: R, options?: ClientRequestOptions) => Promise[M]>> : never : never; } & { + $url: (arg?: Arg) => HonoURL; + $path: (arg?: Arg) => BuildPath; +} & (S['$get'] extends { + outputFormat: 'ws'; +} ? S['$get'] extends { + input: infer I; +} ? { + $ws: (args?: I) => WebSocket; +} : {} : {}); +type ClientResponseOfEndpoint = T extends { + output: infer O; + outputFormat: infer F; + status: infer S; +} ? ClientResponse : never; +export interface ClientResponse { + readonly body: ReadableStream | null; + readonly bodyUsed: boolean; + ok: U extends SuccessStatusCode ? true : U extends Exclude ? false : boolean; + redirected: boolean; + status: U; + statusText: string; + type: 'basic' | 'cors' | 'default' | 'error' | 'opaque' | 'opaqueredirect'; + headers: Headers; + url: string; + redirect(url: string, status: number): Response; + clone(): Response; + bytes(): Promise>; + json(): F extends 'text' ? Promise : F extends 'json' ? Promise : Promise; + text(): F extends 'text' ? (T extends string ? Promise : Promise) : Promise; + blob(): Promise; + formData(): Promise; + arrayBuffer(): Promise; +} +type BuildSearch = Arg extends { [K in Key]: infer Query; } ? IsEmptyObject extends true ? '' : `?${string}` : ''; +type BuildPathname

= Arg extends { + param: infer Param; +} ? `${ApplyParam, Param>}` : `/${TrimStartSlash

}`; +type BuildPath

= `${BuildPathname}${BuildSearch}`; +type BuildTypedURL = TypedURL<`${Protocol}:`, Host, Port, BuildPathname, BuildSearch>; +type HonoURL = IsLiteral extends true ? TrimEndSlash extends `${infer Protocol}://${infer Rest}` ? Rest extends `${infer Hostname}/${infer P}` ? ParseHostName extends [infer Host extends string, infer Port extends string] ? BuildTypedURL : never : ParseHostName extends [infer Host extends string, infer Port extends string] ? BuildTypedURL : never : URL : URL; +type ParseHostName = T extends `${infer Host}:${infer Port}` ? [Host, Port] : [T, '']; +type TrimStartSlash = T extends `/${infer R}` ? TrimStartSlash : T; +type TrimEndSlash = T extends `${infer R}/` ? TrimEndSlash : T; +type IsLiteral = [T] extends [never] ? false : string extends T ? false : true; +type ApplyParam = Path extends `${infer Head}/${infer Rest}` ? Head extends `:${infer Param}` ? P extends Record ? IsLiteral extends true ? ApplyParam : ApplyParam : ApplyParam : ApplyParam : Path extends `:${infer Param}` ? P extends Record ? IsLiteral extends true ? `${Result}/${Value & string}` : `${Result}/${Path}` : `${Result}/${Path}` : `${Result}/${Path}`; +type IsEmptyObject = keyof T extends never ? true : false; +export interface TypedURL extends URL { + protocol: Protocol; + hostname: Hostname; + port: Port; + host: Port extends '' ? Hostname : `${Hostname}:${Port}`; + origin: `${Protocol}//${Hostname}${Port extends '' ? '' : `:${Port}`}`; + pathname: Pathname; + search: Search; + href: `${Protocol}//${Hostname}${Port extends '' ? '' : `:${Port}`}${Pathname}${Search}`; +} +export interface Response extends ClientResponse {} +export type Fetch = (args?: InferRequestType, opt?: ClientRequestOptions) => Promise>>; +type InferEndpointType = T extends ((args: infer R, options: any | undefined) => Promise) ? U extends ClientResponse ? { + input: NonNullable; + output: O; + outputFormat: F; + status: S; +} extends Endpoint ? { + input: NonNullable; + output: O; + outputFormat: F; + status: S; +} : never : never : never; +export type InferResponseType = InferResponseTypeFromEndpoint, U>; +type InferResponseTypeFromEndpoint = T extends { + output: infer O; + status: infer S; +} ? S extends U ? O : never : never; +export type InferRequestType = T extends ((args: infer R, options: any | undefined) => Promise>) ? NonNullable : never; +export type InferRequestOptionsType = T extends ((args: any, options: infer R) => Promise>) ? NonNullable : never; +/** + * Filter a ClientResponse type so it only includes responses of specific status codes. + */ +export type FilterClientResponseByStatusCode, U extends number = StatusCode> = T extends ClientResponse ? RC extends U ? ClientResponse : never : never; +type PathToChain = Path extends `/${infer P}` ? PathToChain : Path extends `${infer P}/${infer R}` ? { [K in P]: PathToChain; } : { [K in Path extends '' ? 'index' : Path]: ClientRequest ? E[Original] : never>; }; +export type Client = T extends Hono ? S extends Record ? K extends string ? PathToChain : never : never : never; +export type Callback = (opts: CallbackOptions) => unknown; +interface CallbackOptions { + path: string[]; + args: any[]; +} +export type ObjectType = { + [key: string]: T; +}; +type GlobalResponseDefinition = { [S in StatusCode]?: { [F in KnownResponseFormat]?: unknown; }; }; +type ToEndpoints = { [S in keyof Def & StatusCode]: { [F in keyof Def[S] & KnownResponseFormat]: Omit & { + output: Def[S][F]; + status: S; + outputFormat: F; +}; }[keyof Def[S] & KnownResponseFormat]; }[keyof Def & StatusCode]; +type ModRoute = R extends Endpoint ? R | ToEndpoints : R; +type ModSchema = { [K in keyof D]: { [M in keyof D[K]]: ModRoute; }; }; +export type ApplyGlobalResponse = App extends Hono ? ModSchema, Def> extends (infer S extends Schema) ? Hono$1 : never : never; +type PickRoute = R extends Endpoint ? R extends { + status: U; +} ? R : never : R; +type PickSchema = { [K in keyof D]: { [M in keyof D[K]]: PickRoute; }; }; +/** + * Keep only specific status code responses from all routes of an app. + * Useful when error responses are handled centrally (e.g., via custom fetch) + * and you want the client to only expose success response types. + * + * @example + * ```ts + * type AppSuccessOnly = PickResponseByStatusCode + * const client = hc('http://localhost') + * ``` + */ +export type PickResponseByStatusCode = App extends Hono ? PickSchema, U> extends (infer S extends Schema) ? Hono$1 : never : never; +//#endregion +export {}; diff --git a/tests/fixtures/npm/node_modules/hono/dist/types/context.d.ts b/tests/fixtures/npm/node_modules/hono/dist/types/context.d.ts new file mode 100644 index 000000000..163c86b8c --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/types/context.d.ts @@ -0,0 +1,459 @@ +import { Result } from "./router.js"; +import { ResponseHeader } from "./utils/headers.js"; +import { ContentfulStatusCode, RedirectStatusCode, StatusCode } from "./utils/http-status.js"; +import { InvalidJSONValue, IsAny, JSONParsed, JSONValue } from "./utils/types.js"; +import { Env, FetchEventLike, H, Input, NotFoundHandler, RouterRoute, TypedResponse } from "./types.js"; +import { HonoRequest } from "./request.js"; +import { BaseMime } from "./utils/mime.js"; +//#region src/context.d.ts +type HeaderRecord = Record<'Content-Type', BaseMime> | Record | Record; +/** + * Data type can be a string, ArrayBuffer, Blob, Uint8Array (buffer), or ReadableStream. + */ +export type Data = string | ArrayBuffer | Blob | ReadableStream | Uint8Array; +/** + * Interface for the execution context in a web worker or similar environment. + */ +export interface ExecutionContext { + /** + * Extends the lifetime of the event callback until the promise is settled. + * + * @param promise - A promise to wait for. + */ + waitUntil(promise: Promise): void; + /** + * Allows the event to be passed through to subsequent event listeners. + */ + passThroughOnException(): void; + /** + * For compatibility with Wrangler 4.x. + */ + props: any; + /** + * For compatibility with Wrangler 4.x. + */ + exports?: any; +} +/** + * Interface for context variable mapping. + */ +export interface ContextVariableMap {} +/** + * Interface for context renderer. + */ +export interface ContextRenderer {} +/** + * Interface representing a renderer for content. + * + * @interface DefaultRenderer + * @param {string | Promise} content - The content to be rendered, which can be either a string or a Promise resolving to a string. + * @returns {Response | Promise} - The response after rendering the content, which can be either a Response or a Promise resolving to a Response. + */ +interface DefaultRenderer { + (content: string | Promise): Response | Promise; +} +/** + * Renderer type which can either be a ContextRenderer or DefaultRenderer. + */ +export type Renderer = ContextRenderer extends Function ? ContextRenderer : DefaultRenderer; +/** + * Extracts the props for the renderer. + */ +export type PropsForRenderer = [...Required>] extends [unknown, infer Props] ? Props : unknown; +export type Layout> = (props: T) => any; +/** + * Interface for getting context variables. + * + * @template E - Environment type. + */ +interface Get { + (key: Key): E['Variables'][Key]; + (key: Key): ContextVariableMap[Key]; +} +/** + * Interface for setting context variables. + * + * @template E - Environment type. + */ +interface Set { + (key: Key, value: E['Variables'][Key]): void; + (key: Key, value: ContextVariableMap[Key]): void; +} +/** + * Interface for creating a new response. + */ +interface NewResponse { + (data: Data | null, status?: StatusCode, headers?: HeaderRecord): Response; + (data: Data | null, init?: ResponseOrInit): Response; +} +/** + * Interface for responding with a body. + */ +interface BodyRespond { + (data: T, status?: U, headers?: HeaderRecord): Response & TypedResponse; + (data: T, init?: ResponseOrInit): Response & TypedResponse; + (data: T, status?: U, headers?: HeaderRecord): Response & TypedResponse; + (data: T, init?: ResponseOrInit): Response & TypedResponse; +} +/** + * Interface for responding with text. + * + * @interface TextRespond + * @template T - The type of the text content. + * @template U - The type of the status code. + * + * @param {T} text - The text content to be included in the response. + * @param {U} [status] - An optional status code for the response. + * @param {HeaderRecord} [headers] - An optional record of headers to include in the response. + * + * @returns {Response & TypedResponse} - The response after rendering the text content, typed with the provided text and status code types. + */ +interface TextRespond { + (text: T, status?: U, headers?: HeaderRecord): Response & TypedResponse; + (text: T, init?: ResponseOrInit): Response & TypedResponse; +} +/** + * Interface for responding with JSON. + * + * @interface JSONRespond + * @template T - The type of the JSON value or simplified unknown type. + * @template U - The type of the status code. + * + * @param {T} object - The JSON object to be included in the response. + * @param {U} [status] - An optional status code for the response. + * @param {HeaderRecord} [headers] - An optional record of headers to include in the response. + * + * @returns {JSONRespondReturn} - The response after rendering the JSON object, typed with the provided object and status code types. + */ +interface JSONRespond { + (object: T, status?: U, headers?: HeaderRecord): JSONRespondReturn; + (object: T, init?: ResponseOrInit): JSONRespondReturn; +} +/** + * @template T - The type of the JSON value or simplified unknown type. + * @template U - The type of the status code. + * + * @returns {Response & TypedResponse, U, 'json'>} - The response after rendering the JSON object, typed with the provided object and status code types. + */ +type JSONRespondReturn = Response & TypedResponse, U, 'json'>; +/** + * Interface representing a function that responds with HTML content. + * + * @param html - The HTML content to respond with, which can be a string or a Promise that resolves to a string. + * @param status - (Optional) The HTTP status code for the response. + * @param headers - (Optional) A record of headers to include in the response. + * @param init - (Optional) The response initialization object. + * + * @returns A Response object or a Promise that resolves to a Response object. + */ +interface HTMLRespond { + >(html: T, status?: ContentfulStatusCode, headers?: HeaderRecord): T extends string ? Response : Promise; + >(html: T, init?: ResponseOrInit): T extends string ? Response : Promise; +} +/** + * Options for configuring the context. + * + * @template E - Environment type. + */ +type ContextOptions = { + /** + * Bindings for the environment. + */ + env: E['Bindings']; + /** + * Execution context for the request. + */ + executionCtx?: FetchEventLike | ExecutionContext | undefined; + /** + * Handler for not found responses. + */ + notFoundHandler?: NotFoundHandler; + matchResult?: Result<[H, RouterRoute]>; + path?: string; +}; +interface SetHeadersOptions { + append?: boolean; +} +interface SetHeaders { + (name: 'Content-Type', value?: BaseMime, options?: SetHeadersOptions): void; + (name: ResponseHeader, value?: string, options?: SetHeadersOptions): void; + (name: string, value?: string, options?: SetHeadersOptions): void; +} +type ResponseHeadersInit = [string, string][] | Record<'Content-Type', BaseMime> | Record | Record | Headers; +interface ResponseInit { + headers?: ResponseHeadersInit; + status?: T; + statusText?: string; +} +type ResponseOrInit = ResponseInit | Response; +export declare const TEXT_PLAIN = "text/plain; charset=UTF-8"; +export declare class Context { + + /** + * `.env` can get bindings (environment variables, secrets, KV namespaces, D1 database, R2 bucket etc.) in Cloudflare Workers. + * + * @see {@link https://hono.dev/docs/api/context#env} + * + * @example + * ```ts + * // Environment object for Cloudflare Workers + * app.get('*', async c => { + * const counter = c.env.COUNTER + * }) + * ``` + */ + env: E['Bindings']; + finalized: boolean; + /** + * `.error` can get the error object from the middleware if the Handler throws an error. + * + * @see {@link https://hono.dev/docs/api/context#error} + * + * @example + * ```ts + * app.use('*', async (c, next) => { + * await next() + * if (c.error) { + * // do something... + * } + * }) + * ``` + */ + error: Error | undefined; + /** + * Creates an instance of the Context class. + * + * @param req - The Request object. + * @param options - Optional configuration options for the context. + */ + constructor(req: Request, options?: ContextOptions); + /** + * `.req` is the instance of {@link HonoRequest}. + */ + get req(): HonoRequest; + /** + * @see {@link https://hono.dev/docs/api/context#event} + * The FetchEvent associated with the current request. + * + * @throws Will throw an error if the context does not have a FetchEvent. + */ + get event(): FetchEventLike; + /** + * @see {@link https://hono.dev/docs/api/context#executionctx} + * The ExecutionContext associated with the current request. + * + * @throws Will throw an error if the context does not have an ExecutionContext. + */ + get executionCtx(): ExecutionContext; + /** + * @see {@link https://hono.dev/docs/api/context#res} + * The Response object for the current request. + */ + get res(): Response; + /** + * Sets the Response object for the current request. + * + * @param _res - The Response object to set. + */ + set res(_res: Response | undefined); + /** + * `.render()` can create a response within a layout. + * + * @see {@link https://hono.dev/docs/api/context#render-setrenderer} + * + * @example + * ```ts + * app.get('/', (c) => { + * return c.render('Hello!') + * }) + * ``` + */ + render: Renderer; + /** + * Sets the layout for the response. + * + * @param layout - The layout to set. + * @returns The layout function. + */ + setLayout: (layout: Layout) => Layout; + /** + * Gets the current layout for the response. + * + * @returns The current layout function. + */ + getLayout: () => Layout | undefined; + /** + * `.setRenderer()` can set the layout in the custom middleware. + * + * @see {@link https://hono.dev/docs/api/context#render-setrenderer} + * + * @example + * ```tsx + * app.use('*', async (c, next) => { + * c.setRenderer((content) => { + * return c.html( + * + * + *

{content}

+ * + * + * ) + * }) + * await next() + * }) + * ``` + */ + setRenderer: (renderer: Renderer) => void; + /** + * `.header()` can set headers. + * + * @see {@link https://hono.dev/docs/api/context#header} + * + * @example + * ```ts + * app.get('/welcome', (c) => { + * // Set headers + * c.header('X-Message', 'Hello!') + * c.header('Content-Type', 'text/plain') + * + * // Append multiple headers using the append option (e.g. Vary) + * c.header('Vary', 'Accept-Encoding', { append: true }) + * c.header('Vary', 'User-Agent', { append: true }) + * + * return c.body('Thank you for coming') + * }) + * ``` + */ + header: SetHeaders; + status: (status: StatusCode) => void; + /** + * `.set()` can set the value specified by the key. + * + * @see {@link https://hono.dev/docs/api/context#set-get} + * + * @example + * ```ts + * app.use('*', async (c, next) => { + * c.set('message', 'Hono is hot!!') + * await next() + * }) + * ``` + */ + set: Set extends true ? { + Variables: ContextVariableMap & Record; + } : E>; + /** + * `.get()` can use the value specified by the key. + * + * @see {@link https://hono.dev/docs/api/context#set-get} + * + * @example + * ```ts + * app.get('/', (c) => { + * const message = c.get('message') + * return c.text(`The message is "${message}"`) + * }) + * ``` + */ + get: Get extends true ? { + Variables: ContextVariableMap & Record; + } : E>; + /** + * `.var` can access the value of a variable. + * + * @see {@link https://hono.dev/docs/api/context#var} + * + * @example + * ```ts + * const result = c.var.client.oneMethod() + * ``` + */ + get var(): Readonly extends true ? Record : E['Variables'])>; + newResponse: NewResponse; + /** + * `.body()` can return the HTTP response. + * You can set headers with `.header()` and set HTTP status code with `.status`. + * This can also be set in `.text()`, `.json()` and so on. + * + * @see {@link https://hono.dev/docs/api/context#body} + * + * @example + * ```ts + * app.get('/welcome', (c) => { + * // Set headers + * c.header('X-Message', 'Hello!') + * c.header('Content-Type', 'text/plain') + * // Set HTTP status code + * c.status(201) + * + * // Return the response body + * return c.body('Thank you for coming') + * }) + * ``` + */ + body: BodyRespond; + /** + * `.text()` can render text as `Content-Type:text/plain`. + * + * @see {@link https://hono.dev/docs/api/context#text} + * + * @example + * ```ts + * app.get('/say', (c) => { + * return c.text('Hello!') + * }) + * ``` + */ + text: TextRespond; + /** + * `.json()` can render JSON as `Content-Type:application/json`. + * + * @see {@link https://hono.dev/docs/api/context#json} + * + * @example + * ```ts + * app.get('/api', (c) => { + * return c.json({ message: 'Hello!' }) + * }) + * ``` + */ + json: JSONRespond; + html: HTMLRespond; + /** + * `.redirect()` can Redirect, default status code is 302. + * + * @see {@link https://hono.dev/docs/api/context#redirect} + * + * @example + * ```ts + * app.get('/redirect', (c) => { + * return c.redirect('/') + * }) + * app.get('/redirect-permanently', (c) => { + * return c.redirect('/', 301) + * }) + * ``` + */ + redirect: (location: string | URL, status?: T) => Response & TypedResponse; + /** + * `.notFound()` can return the Not Found Response. + * + * @see {@link https://hono.dev/docs/api/context#notfound} + * + * @example + * ```ts + * app.get('/notfound', (c) => { + * return c.notFound() + * }) + * ``` + */ + notFound: () => ReturnType; +} +//#endregion +export {}; diff --git a/tests/fixtures/npm/node_modules/hono/dist/types/hono-base.d.ts b/tests/fixtures/npm/node_modules/hono/dist/types/hono-base.d.ts new file mode 100644 index 000000000..85f57dfc6 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/types/hono-base.d.ts @@ -0,0 +1,221 @@ +import { Router } from "./router.js"; +import { Env, ErrorHandler, H, HandlerInterface, MergePath, MergeSchemaPath, MiddlewareHandlerInterface, NotFoundHandler, OnHandlerInterface, RouterRoute, Schema } from "./types.js"; +import { Context, ExecutionContext } from "./context.js"; +//#region src/hono-base.d.ts +type GetPath = (request: Request, options?: { + env?: E['Bindings']; +}) => string; +export type HonoOptions = { + /** + * `strict` option specifies whether to distinguish whether the last path is a directory or not. + * + * @see {@link https://hono.dev/docs/api/hono#strict-mode} + * + * @default true + */ + strict?: boolean; + /** + * `router` option specifies which router to use. + * + * @see {@link https://hono.dev/docs/api/hono#router-option} + * + * @example + * ```ts + * const app = new Hono({ router: new RegExpRouter() }) + * ``` + */ + router?: Router<[H, RouterRoute]>; + /** + * `getPath` can handle the host header value. + * + * @see {@link https://hono.dev/docs/api/routing#routing-with-host-header-value} + * + * @example + * ```ts + * const app = new Hono({ + * getPath: (req) => + * '/' + req.headers.get('host') + req.url.replace(/^https?:\/\/[^/]+(\/[^?]*)/, '$1'), + * }) + * + * app.get('/www1.example.com/hello', () => c.text('hello www1')) + * + * // A following request will match the route: + * // new Request('http://www1.example.com/hello', { + * // headers: { host: 'www1.example.com' }, + * // }) + * ``` + */ + getPath?: GetPath; +}; +type MountOptionHandler = (c: Context) => unknown; +type MountReplaceRequest = (originalRequest: Request) => Request; +type MountOptions = MountOptionHandler | { + optionHandler?: MountOptionHandler; + replaceRequest?: MountReplaceRequest | false; +}; +declare class Hono { + + get: HandlerInterface; + post: HandlerInterface; + put: HandlerInterface; + delete: HandlerInterface; + options: HandlerInterface; + patch: HandlerInterface; + query: HandlerInterface; + all: HandlerInterface; + on: OnHandlerInterface; + use: MiddlewareHandlerInterface; + router: Router<[H, RouterRoute]>; + readonly getPath: GetPath; + private _basePath; + routes: RouterRoute[]; + constructor(options?: HonoOptions); + private errorHandler; + /** + * `.route()` allows grouping other Hono instance in routes. + * + * @see {@link https://hono.dev/docs/api/routing#grouping} + * + * @param {string} path - base Path + * @param {Hono} app - other Hono instance + * @returns {Hono} routed Hono instance + * + * @example + * ```ts + * const app = new Hono() + * const app2 = new Hono() + * + * app2.get("/user", (c) => c.text("user")) + * app.route("/api", app2) // GET /api/user + * ``` + */ + route(path: SubPath, app: Hono): Hono> | S, BasePath, CurrentPath>; + /** + * `.basePath()` allows base paths to be specified. + * + * @see {@link https://hono.dev/docs/api/routing#base-path} + * + * @param {string} path - base Path + * @returns {Hono} changed Hono instance + * + * @example + * ```ts + * const api = new Hono().basePath('/api') + * ``` + */ + basePath(path: SubPath): Hono, MergePath>; + /** + * `.onError()` handles an error and returns a customized Response. + * + * @see {@link https://hono.dev/docs/api/hono#error-handling} + * + * @param {ErrorHandler} handler - request Handler for error + * @returns {Hono} changed Hono instance + * + * @example + * ```ts + * app.onError((err, c) => { + * console.error(`${err}`) + * return c.text('Custom Error Message', 500) + * }) + * ``` + */ + onError: (handler: ErrorHandler) => Hono; + /** + * `.notFound()` allows you to customize a Not Found Response. + * + * @see {@link https://hono.dev/docs/api/hono#not-found} + * + * @param {NotFoundHandler} handler - request handler for not-found + * @returns {Hono} changed Hono instance + * + * @example + * ```ts + * app.notFound((c) => { + * return c.text('Custom 404 Message', 404) + * }) + * ``` + */ + notFound: (handler: NotFoundHandler) => Hono; + /** + * `.mount()` allows you to mount applications built with other frameworks into your Hono application. + * + * @deprecated Use `mount()` from `hono/mount` instead. `.mount()` will be removed in v5. + * + * @see {@link https://hono.dev/docs/api/hono#mount} + * + * @param {string} path - base Path + * @param {Function} applicationHandler - other Request Handler + * @param {MountOptions} [options] - options of `.mount()` + * @returns {Hono} mounted Hono instance + * + * @example + * ```ts + * import { Router as IttyRouter } from 'itty-router' + * import { Hono } from 'hono' + * // Create itty-router application + * const ittyRouter = IttyRouter() + * // GET /itty-router/hello + * ittyRouter.get('/hello', () => new Response('Hello from itty-router')) + * + * const app = new Hono() + * app.mount('/itty-router', ittyRouter.handle) + * ``` + * + * @example + * ```ts + * const app = new Hono() + * // Send the request to another application without modification. + * app.mount('/app', anotherApp, { + * replaceRequest: (req) => req, + * }) + * ``` + */ + mount(path: string, applicationHandler: (request: Request, ...args: any) => Response | Promise, options?: MountOptions): Hono; + /** + * `.fetch()` will be entry point of your app. + * + * @see {@link https://hono.dev/docs/api/hono#fetch} + * + * @param {Request} request - request Object of request + * @param {Env} env - env Object + * @param {ExecutionContext} executionCtx - context of execution + * @returns {Response | Promise} response of request + * + */ + fetch: (request: Request, env?: E['Bindings'] | {}, executionCtx?: ExecutionContext) => Response | Promise; + /** + * `.request()` is a useful method for testing. + * You can pass a URL or pathname to send a GET request. + * app will return a Response object. + * ```ts + * test('GET /hello is ok', async () => { + * const res = await app.request('/hello') + * expect(res.status).toBe(200) + * }) + * ``` + * @see https://hono.dev/docs/api/hono#request + */ + request: (input: Request | string | URL, requestInit?: RequestInit, Env?: E["Bindings"] | {}, executionCtx?: ExecutionContext) => Response | Promise; + /** + * `.fire()` automatically adds a global fetch event listener. + * This can be useful for environments that adhere to the Service Worker API, such as non-ES module Cloudflare Workers. + * @deprecated + * Use `fire` from `hono/service-worker` instead. + * ```ts + * import { Hono } from 'hono' + * import { fire } from 'hono/service-worker' + * + * const app = new Hono() + * // ... + * fire(app) + * ``` + * @see https://hono.dev/docs/api/hono#fire + * @see https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API + * @see https://developers.cloudflare.com/workers/reference/migrate-to-module-workers/ + */ + fire: () => void; +} +//#endregion +export { Hono as HonoBase }; +export {}; diff --git a/tests/fixtures/npm/node_modules/hono/dist/types/hono.d.ts b/tests/fixtures/npm/node_modules/hono/dist/types/hono.d.ts new file mode 100644 index 000000000..52ce78cec --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/types/hono.d.ts @@ -0,0 +1,20 @@ +import { HonoBase as Hono$1, HonoOptions } from "./hono-base.js"; +import { BlankEnv, BlankSchema, Env, Schema } from "./types.js"; +//#region src/hono.d.ts +/** + * The Hono class extends the functionality of the HonoBase class. + * It sets up routing and allows for custom options to be passed. + * + * @template E - The environment type. + * @template S - The schema type. + * @template BasePath - The base path type. + */ +export declare class Hono extends Hono$1 { + /** + * Creates an instance of the Hono class. + * + * @param options - Optional configuration options for the Hono instance. + */ + constructor(options?: HonoOptions); +} +//#endregion \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/hono/dist/types/index.d.ts b/tests/fixtures/npm/node_modules/hono/dist/types/index.d.ts new file mode 100644 index 000000000..12bbd7aaf --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/types/index.d.ts @@ -0,0 +1,6 @@ +import { Env, ErrorHandler, Handler, Input, MiddlewareHandler, Next, NotFoundHandler, NotFoundResponse, Schema, ToSchema, TypedResponse, ValidationTargets } from "./types.js"; +import { HonoRequest } from "./request.js"; +import { Context, ContextRenderer, ContextVariableMap, ExecutionContext } from "./context.js"; +import { Hono } from "./hono.js"; +import { ClientRequestOptions, InferRequestType, InferResponseType } from "./client/types.js"; +export { type ClientRequestOptions, Context, type ContextRenderer, type ContextVariableMap, type Env, type ErrorHandler, type ExecutionContext, type Handler, Hono, type HonoRequest, type InferRequestType, type InferResponseType, type Input, type MiddlewareHandler, type Next, type NotFoundHandler, type NotFoundResponse, type Schema, type ToSchema, type TypedResponse, type ValidationTargets }; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/hono/dist/types/request.d.ts b/tests/fixtures/npm/node_modules/hono/dist/types/request.d.ts new file mode 100644 index 000000000..03d14a385 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/types/request.d.ts @@ -0,0 +1,326 @@ +import { GET_MATCH_RESULT } from "./request/constants.js"; +import { Result } from "./router.js"; +import { CustomHeader, RequestHeader } from "./utils/headers.js"; +import { Simplify, UnionToIntersection } from "./utils/types.js"; +import { Input, InputToDataByTarget, ParamKeyToRecord, ParamKeys, RemoveQuestion, RouterRoute, ValidationTargets } from "./types.js"; +import { BodyData, ParseBodyOptions } from "./utils/body.js"; +//#region src/request.d.ts +type Body = { + json: any; + text: string; + arrayBuffer: ArrayBuffer; + blob: Blob; + formData: FormData; +}; +type BodyCache = Partial; +export declare class HonoRequest

{ + + /** + * `.raw` can get the raw Request object. + * + * @see {@link https://hono.dev/docs/api/request#raw} + * + * @example + * ```ts + * // For Cloudflare Workers + * app.post('/', async (c) => { + * const metadata = c.req.raw.cf?.hostMetadata? + * ... + * }) + * ``` + */ + raw: Request; + routeIndex: number; + /** + * `.path` can get the pathname of the request. + * + * @see {@link https://hono.dev/docs/api/request#path} + * + * @example + * ```ts + * app.get('/about/me', (c) => { + * const pathname = c.req.path // `/about/me` + * }) + * ``` + */ + path: string; + bodyCache: BodyCache; + constructor(request: Request, path?: string, matchResult?: Result<[unknown, RouterRoute]>); + /** + * `.req.param()` gets the path parameters. + * + * @see {@link https://hono.dev/docs/api/routing#path-parameter} + * + * @example + * ```ts + * const name = c.req.param('name') + * // or all parameters at once + * const { id, comment_id } = c.req.param() + * ``` + */ + param = ParamKeys

>(key: string extends P ? never : P2 extends `${infer _}?` ? never : P2): string; + param> = RemoveQuestion>>(key: P2): string | undefined; + param(key: string): string | undefined; + param(): Simplify>>>; + /** + * `.query()` can get querystring parameters. + * + * @see {@link https://hono.dev/docs/api/request#query} + * + * @example + * ```ts + * // Query params + * app.get('/search', (c) => { + * const query = c.req.query('q') + * }) + * + * // Get all params at once + * app.get('/search', (c) => { + * const { q, limit, offset } = c.req.query() + * }) + * ``` + */ + query(key: string): string | undefined; + query(): Record; + /** + * `.queries()` can get multiple querystring parameter values, e.g. /search?tags=A&tags=B + * + * @see {@link https://hono.dev/docs/api/request#queries} + * + * @example + * ```ts + * app.get('/search', (c) => { + * // tags will be string[] + * const tags = c.req.queries('tags') + * }) + * ``` + */ + queries(key: string): string[] | undefined; + queries(): Record; + /** + * `.header()` can get the request header value. + * + * @see {@link https://hono.dev/docs/api/request#header} + * + * @example + * ```ts + * app.get('/', (c) => { + * const userAgent = c.req.header('User-Agent') + * }) + * ``` + */ + header(name: RequestHeader): string | undefined; + header(name: string): string | undefined; + header(): Record; + /** + * `.parseBody()` can parse Request body of type `multipart/form-data` or `application/x-www-form-urlencoded` + * + * @see {@link https://hono.dev/docs/api/request#parsebody} + * + * @example + * ```ts + * app.post('/entry', async (c) => { + * const body = await c.req.parseBody() + * }) + * ``` + */ + parseBody, T extends BodyData>(options?: Options): Promise; + parseBody(options?: Partial): Promise; + /** + * `.json()` can parse Request body of type `application/json` + * + * @see {@link https://hono.dev/docs/api/request#json} + * + * @example + * ```ts + * app.post('/entry', async (c) => { + * const body = await c.req.json() + * }) + * ``` + */ + json(): Promise; + /** + * `.text()` can parse Request body of type `text/plain` + * + * @see {@link https://hono.dev/docs/api/request#text} + * + * @example + * ```ts + * app.post('/entry', async (c) => { + * const body = await c.req.text() + * }) + * ``` + */ + text(): Promise; + /** + * `.arrayBuffer()` parse Request body as an `ArrayBuffer` + * + * @see {@link https://hono.dev/docs/api/request#arraybuffer} + * + * @example + * ```ts + * app.post('/entry', async (c) => { + * const body = await c.req.arrayBuffer() + * }) + * ``` + */ + arrayBuffer(): Promise; + /** + * `.bytes()` parses the request body as a `Uint8Array`. + * + * @see {@link https://hono.dev/docs/api/request#bytes} + * + * @example + * ```ts + * app.post('/entry', async (c) => { + * const body = await c.req.bytes() + * }) + * ``` + */ + bytes(): Promise; + /** + * Parses the request body as a `Blob`. + * @example + * ```ts + * app.post('/entry', async (c) => { + * const body = await c.req.blob(); + * }); + * ``` + * @see https://hono.dev/docs/api/request#blob + */ + blob(): Promise; + /** + * Parses the request body as `FormData`. + * @example + * ```ts + * app.post('/entry', async (c) => { + * const body = await c.req.formData(); + * }); + * ``` + * @see https://hono.dev/docs/api/request#formdata + */ + formData(): Promise; + /** + * Adds validated data to the request. + * + * @param target - The target of the validation. + * @param data - The validated data to add. + */ + addValidatedData(target: keyof ValidationTargets, data: {}): void; + /** + * Gets validated data from the request. + * + * @param target - The target of the validation. + * @returns The validated data. + * + * @see https://hono.dev/docs/api/request#valid + */ + valid(target: T): InputToDataByTarget; + /** + * `.url` can get the request url strings. + * + * @see {@link https://hono.dev/docs/api/request#url} + * + * @example + * ```ts + * app.get('/about/me', (c) => { + * const url = c.req.url // `http://localhost:8787/about/me` + * ... + * }) + * ``` + */ + get url(): string; + /** + * `.method` can get the method name of the request. + * + * @see {@link https://hono.dev/docs/api/request#method} + * + * @example + * ```ts + * app.get('/about/me', (c) => { + * const method = c.req.method // `GET` + * }) + * ``` + */ + get method(): string; + get [GET_MATCH_RESULT](): Result<[unknown, RouterRoute]>; + /** + * `.matchedRoutes` can return a matched route in the handler + * + * @deprecated + * + * Use matchedRoutes helper defined in "hono/route" instead. + * + * @see {@link https://hono.dev/docs/api/request#matchedroutes} + * + * @example + * ```ts + * app.use('*', async function logger(c, next) { + * await next() + * c.req.matchedRoutes.forEach(({ handler, method, path }, i) => { + * const name = handler.name || (handler.length < 2 ? '[handler]' : '[middleware]') + * console.log( + * method, + * ' ', + * path, + * ' '.repeat(Math.max(10 - path.length, 0)), + * name, + * i === c.req.routeIndex ? '<- respond from here' : '' + * ) + * }) + * }) + * ``` + */ + get matchedRoutes(): RouterRoute[]; + /** + * `.routePath` can retrieve the path registered within the handler + * + * @deprecated + * + * Use routePath helper defined in "hono/route" instead. + * + * @see {@link https://hono.dev/docs/api/request#routepath} + * + * @example + * ```ts + * app.get('/posts/:id', (c) => { + * return c.json({ path: c.req.routePath }) + * }) + * ``` + */ + get routePath(): string; +} +/** + * Clones a HonoRequest's underlying raw Request object. + * + * This utility handles both consumed and unconsumed request bodies: + * - If the request body hasn't been consumed, it uses the native `clone()` method + * - If the request body has been consumed, it reconstructs a new Request using cached body data + * + * This is particularly useful when you need to: + * - Process the same request body multiple times + * - Pass requests to external services after validation + * + * @param req - The HonoRequest object to clone + * @returns A Promise that resolves to a new Request object with the same properties + * @throws {HTTPException} If the request body was consumed directly via `req.raw` + * without using HonoRequest methods (e.g., `req.json()`, `req.text()`), making it + * impossible to reconstruct the body from cache + * + * @example + * ```ts + * // Clone after consuming the body (e.g., after validation) + * app.post('/forward', + * validator('json', (data) => data), + * async (c) => { + * const validated = c.req.valid('json') + * // Body has been consumed, but cloneRawRequest still works + * const clonedReq = await cloneRawRequest(c.req) + * return fetch('http://backend-service.com', clonedReq) + * } + * ) + * ``` + */ +export declare const cloneRawRequest: (req: HonoRequest) => Promise; +//#endregion +export {}; diff --git a/tests/fixtures/npm/node_modules/hono/dist/types/request/constants.d.ts b/tests/fixtures/npm/node_modules/hono/dist/types/request/constants.d.ts new file mode 100644 index 000000000..0899a5d2b --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/types/request/constants.d.ts @@ -0,0 +1,3 @@ +//#region src/request/constants.d.ts +export declare const GET_MATCH_RESULT: unique symbol; +//#endregion \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/hono/dist/types/router.d.ts b/tests/fixtures/npm/node_modules/hono/dist/types/router.d.ts new file mode 100644 index 000000000..f00b40662 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/types/router.d.ts @@ -0,0 +1,98 @@ +//#region src/router.d.ts +/** + * @module + * This module provides types definitions and variables for the routers. + */ +/** + * Constant representing all HTTP methods in uppercase. + */ +export declare const METHOD_NAME_ALL: "ALL"; +/** + * Constant representing all HTTP methods in lowercase. + */ +export declare const METHOD_NAME_ALL_LOWERCASE: "all"; +/** + * Array of supported HTTP methods. + */ +export declare const METHODS: readonly ["get", "post", "put", "delete", "options", "patch", "query"]; +/** + * Error message indicating that a route cannot be added because the matcher is already built. + */ +export declare const MESSAGE_MATCHER_IS_ALREADY_BUILT = "Can not add a route since the matcher is already built."; +/** + * Interface representing a router. + * + * @template T - The type of the handler. + */ +export interface Router { + /** + * The name of the router. + */ + name: string; + /** + * Adds a route to the router. + * + * @param method - The HTTP method (e.g., 'get', 'post'). + * @param path - The path for the route. + * @param handler - The handler for the route. + */ + add(method: string, path: string, handler: T): void; + /** + * Matches a route based on the given method and path. + * + * @param method - The HTTP method (e.g., 'get', 'post'). + * @param path - The path to match. + * @returns The result of the match. + */ + match(method: string, path: string): Result; +} +/** + * Type representing a map of parameter indices. + */ +export type ParamIndexMap = Record; +/** + * Type representing a stash of parameters. + */ +export type ParamStash = string[]; +/** + * Type representing a map of parameters. + */ +export type Params = Record; +/** + * Type representing the result of a route match. + * + * The result can be in one of two formats: + * 1. An array of handlers with their corresponding parameter index maps, followed by a parameter stash. + * 2. An array of handlers with their corresponding parameter maps. + * + * Example: + * + * [[handler, paramIndexMap][], paramArray] + * ```typescript + * [ + * [ + * [middlewareA, {}], // '*' + * [funcA, {'id': 0}], // '/user/:id/*' + * [funcB, {'id': 0, 'action': 1}], // '/user/:id/:action' + * ], + * ['123', 'abc'] + * ] + * ``` + * + * [[handler, params][]] + * ```typescript + * [ + * [ + * [middlewareA, {}], // '*' + * [funcA, {'id': '123'}], // '/user/:id/*' + * [funcB, {'id': '123', 'action': 'abc'}], // '/user/:id/:action' + * ] + * ] + * ``` + */ +export type Result = [[T, ParamIndexMap][], ParamStash] | [[T, Params][]]; +/** + * Error class representing an unsupported path error. + */ +export declare class UnsupportedPathError extends Error {} +//#endregion \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/hono/dist/types/types.d.ts b/tests/fixtures/npm/node_modules/hono/dist/types/types.d.ts new file mode 100644 index 000000000..4a0be1dea --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/types/types.d.ts @@ -0,0 +1,214 @@ +import { HonoBase as Hono } from "./hono-base.js"; +import { CustomHeader, RequestHeader } from "./utils/headers.js"; +import { StatusCode } from "./utils/http-status.js"; +import { IfAnyThenEmptyObject, IsAny, JSONValue, RemoveBlankRecord, Simplify, UnionToIntersection } from "./utils/types.js"; +import { Context } from "./context.js"; +//#region src/types.d.ts +export type Bindings = object; +export type Variables = object; +export type BlankEnv = {}; +export type Env = { + Bindings?: Bindings; + Variables?: Variables; +}; +export type Next = () => Promise; +export type ExtractInput = I extends Input ? unknown extends I['in'] ? {} : I['in'] : I; +export type Input = { + in?: {}; + out?: {}; + outputFormat?: ResponseFormat; +}; +export type BlankSchema = {}; +export type BlankInput = {}; +export interface RouterRoute { + basePath: string; + path: string; + method: string; + handler: H; +} +export type HandlerResponse = Response | TypedResponse | Promise> | Promise; +export type Handler = any> = (c: Context, next: Next) => R; +export type MiddlewareHandler = Response> = (c: Context, next: Next) => Promise; +export type H = any> = Handler | MiddlewareHandler; +/** + * You can extend this interface to define a custom `c.notFound()` Response type. + * + * @example + * declare module 'hono' { + * interface NotFoundResponse extends Response, TypedResponse {} + * } + */ +export interface NotFoundResponse {} +export type NotFoundHandler = (c: Context) => NotFoundResponse extends Response ? NotFoundResponse | Promise : Response | Promise; +export interface HTTPResponseError extends Error { + getResponse: () => Response; +} +export type ErrorHandler = (err: Error | HTTPResponseError, c: Context) => Response | Promise; +export interface HandlerInterface { +

= any, E2 extends Env = E>(handler: H): Hono, S & ToSchema>, BasePath, CurrentPath>; +

= any, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, M1 extends H = H>(...handlers: [H & M1, H]): Hono, S & ToSchema | MergeMiddlewareResponse>, BasePath, CurrentPath>; +

, R extends HandlerResponse = any, I extends Input = BlankInput, E2 extends Env = E>(path: P, handler: H): Hono, S, M, P, I, BasePath>, BasePath, MergePath>; +

= any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, M1 extends H = H, M2 extends H = H>(...handlers: [H & M1, H & M2, H]): Hono, S & ToSchema | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, CurrentPath>; +

, R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, M1 extends H = H>(path: P, ...handlers: [H & M1, H]): Hono | MergeMiddlewareResponse, S, M, P, I2, BasePath>, BasePath, MergePath>; +

= any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, M1 extends H = H, M2 extends H = H, M3 extends H = H>(...handlers: [H & M1, H & M2, H & M3, H]): Hono, S & ToSchema | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, CurrentPath>; +

, R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, M1 extends H = H, M2 extends H = H>(path: P, ...handlers: [H & M1, H & M2, H]): Hono | MergeMiddlewareResponse | MergeMiddlewareResponse, S, M, P, I3, BasePath>, BasePath, MergePath>; +

= any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H>(...handlers: [H & M1, H & M2, H & M3, H & M4, H]): Hono, S & ToSchema | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, CurrentPath>; +

, R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, M1 extends H = H, M2 extends H = H, M3 extends H = H>(path: P, ...handlers: [H & M1, H & M2, H & M3, H]): Hono | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse, S, M, P, I4, BasePath>, BasePath, MergePath>; +

= any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H, M5 extends H = H>(...handlers: [H & M1, H & M2, H & M3, H & M4, H & M5, H]): Hono, S & ToSchema | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, CurrentPath>; +

, R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H>(path: P, ...handlers: [H & M1, H & M2, H & M3, H & M4, H]): Hono | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse, S, M, P, I5, BasePath>, BasePath, MergePath>; +

= any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, I7 extends Input = I & I2 & I3 & I4 & I5 & I6, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H, M5 extends H = H, M6 extends H = H>(...handlers: [H & M1, H & M2, H & M3, H & M4, H & M5, H & M6, H]): Hono, S & ToSchema | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, CurrentPath>; +

, R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H, M5 extends H = H>(path: P, ...handlers: [H & M1, H & M2, H & M3, H & M4, H & M5, H]): Hono | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse, S, M, P, I6, BasePath>, BasePath, MergePath>; +

= any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, I7 extends Input = I & I2 & I3 & I4 & I5 & I6, I8 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, E9 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H, M5 extends H = H, M6 extends H = H, M7 extends H = H>(...handlers: [H & M1, H & M2, H & M3, H & M4, H & M5, H & M6, H & M7, H]): Hono, S & ToSchema | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, CurrentPath>; +

, R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, I7 extends Input = I & I2 & I3 & I4 & I5 & I6, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H, M5 extends H = H, M6 extends H = H>(path: P, ...handlers: [H & M1, H & M2, H & M3, H & M4, H & M5, H & M6, H]): Hono | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse, S, M, P, I7, BasePath>, BasePath, MergePath>; +

= any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, I7 extends Input = I & I2 & I3 & I4 & I5 & I6, I8 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7, I9 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7 & I8, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, E9 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8]>, E10 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8, E9]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H, M5 extends H = H, M6 extends H = H, M7 extends H = H, M8 extends H = H>(...handlers: [H & M1, H & M2, H & M3, H & M4, H & M5, H & M6, H & M7, H & M8, H]): Hono, S & ToSchema | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, CurrentPath>; +

, R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, I7 extends Input = I & I2 & I3 & I4 & I5 & I6, I8 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, E9 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H, M5 extends H = H, M6 extends H = H, M7 extends H = H>(path: P, ...handlers: [H & M1, H & M2, H & M3, H & M4, H & M5, H & M6, H & M7, H]): Hono | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse, S, M, P, I8, BasePath>, BasePath, MergePath>; +

= any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, I7 extends Input = I & I2 & I3 & I4 & I5 & I6, I8 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7, I9 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7 & I8, I10 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7 & I8 & I9, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, E9 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8]>, E10 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8, E9]>, E11 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8, E9, E10]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H, M5 extends H = H, M6 extends H = H, M7 extends H = H, M8 extends H = H, M9 extends H = H>(...handlers: [H & M1, H & M2, H & M3, H & M4, H & M5, H & M6, H & M7, H & M8, H & M9, H]): Hono, S & ToSchema | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, CurrentPath>; +

, R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, I7 extends Input = I & I2 & I3 & I4 & I5 & I6, I8 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7, I9 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7 & I8, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, E9 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8]>, E10 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8, E9]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H, M5 extends H = H, M6 extends H = H, M7 extends H = H, M8 extends H = H>(path: P, ...handlers: [H & M1, H & M2, H & M3, H & M4, H & M5, H & M6, H & M7, H & M8, H]): Hono | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse, S, M, P, I9, BasePath>, BasePath, MergePath>; +

, R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, I7 extends Input = I & I2 & I3 & I4 & I5 & I6, I8 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7, I9 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7 & I8, I10 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7 & I8 & I9, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, E9 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8]>, E10 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8, E9]>, E11 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8, E9, E10]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H, M5 extends H = H, M6 extends H = H, M7 extends H = H, M8 extends H = H, M9 extends H = H>(path: P, ...handlers: [H & M1, H & M2, H & M3, H & M4, H & M5, H & M6, H & M7, H & M8, H & M9, H]): Hono | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse, S, M, P, I10, BasePath>, BasePath, MergePath>; +

= any>(...handlers: H[]): Hono>, BasePath, CurrentPath>; +

= any>(path: P, ...handlers: [H, I, R>, ...H, I, R>[]]): Hono, I, MergeTypedResponse>, BasePath, MergePath>; +

= any, I extends Input = BlankInput>(path: P): Hono, I, MergeTypedResponse>, BasePath, MergePath>; +} +export interface MiddlewareHandlerInterface { + (...handlers: MiddlewareHandler>[]): Hono, S, BasePath, MergePath>; + (handler: MiddlewareHandler>): Hono, S, BasePath, MergePath>; + , P extends string = MergePath>(...handlers: [MiddlewareHandler, MiddlewareHandler]): Hono, S, BasePath, P>; +

, E2 extends Env = E>(path: P, handler: MiddlewareHandler): Hono, S, BasePath, MergedPath>; + , E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, P extends string = MergePath>(...handlers: [MiddlewareHandler, MiddlewareHandler, MiddlewareHandler]): Hono, S, BasePath, P>; +

, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>>(path: P, ...handlers: [MiddlewareHandler, MiddlewareHandler]): Hono, S, BasePath, MergedPath>; + , E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, P extends string = MergePath>(...handlers: [MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler]): Hono, S, BasePath, P>; +

, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>>(path: P, ...handlers: [MiddlewareHandler, MiddlewareHandler, MiddlewareHandler]): Hono, S, BasePath, MergedPath>; + , E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, P extends string = MergePath>(...handlers: [MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler]): Hono, S, BasePath, P>; +

, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>>(path: P, ...handlers: [MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler]): Hono, S, BasePath, MergedPath>; + , E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, P extends string = MergePath>(...handlers: [MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler]): Hono, S, BasePath, P>; +

, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>>(path: P, ...handlers: [MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler]): Hono, S, BasePath, MergedPath>; + , E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, P extends string = MergePath>(...handlers: [MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler]): Hono, S, BasePath, P>; +

, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>>(path: P, ...handlers: [MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler]): Hono, S, BasePath, MergedPath>; + , E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, E9 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8]>, P extends string = MergePath>(...handlers: [MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler]): Hono, S, BasePath, P>; +

, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>>(path: P, ...handlers: [MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler]): Hono, S, BasePath, MergedPath>; + , E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, E9 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8]>, E10 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8, E9]>, P extends string = MergePath>(...handlers: [MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler]): Hono, S, BasePath, P>; +

, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, E9 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8]>>(path: P, ...handlers: [MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler]): Hono, S, BasePath, MergedPath>; + , E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, E9 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8]>, E10 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8, E9]>, E11 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8, E9, E10]>, P extends string = MergePath>(...handlers: [MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler]): Hono, S, BasePath, P>; +

, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, E9 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8]>, E10 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8, E9]>>(path: P, ...handlers: [MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler]): Hono, S, BasePath, MergedPath>; +

(path: P, ...handlers: MiddlewareHandler>[]): Hono>; +} +export interface OnHandlerInterface { + , R extends HandlerResponse = any, I extends Input = BlankInput, E2 extends Env = E>(method: M, path: P, handler: H): Hono, S & ToSchema, I, MergeTypedResponse>, BasePath, MergePath>; + , R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, M1 extends H = H>(method: M, path: P, ...handlers: [H & M1, H]): Hono, S & ToSchema, I2, MergeTypedResponse | MergeMiddlewareResponse>, BasePath, MergePath>; + , R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, M1 extends H = H, M2 extends H = H>(method: M, path: P, ...handlers: [H & M1, H & M2, H]): Hono, S & ToSchema, I3, MergeTypedResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, MergePath>; + , R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, M1 extends H = H, M2 extends H = H, M3 extends H = H>(method: M, path: P, ...handlers: [H & M1, H & M2, H & M3, H]): Hono, S & ToSchema, I4, MergeTypedResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, MergePath>; + , R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H>(method: M, path: P, ...handlers: [H & M1, H & M2, H & M3, H & M4, H]): Hono, S & ToSchema, I5, MergeTypedResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, MergePath>; + , R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H, M5 extends H = H>(method: M, path: P, ...handlers: [H & M1, H & M2, H & M3, H & M4, H & M5, H]): Hono, S & ToSchema, I6, MergeTypedResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, MergePath>; + , R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, I7 extends Input = I & I2 & I3 & I4 & I5 & I6, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H, M5 extends H = H, M6 extends H = H>(method: M, path: P, ...handlers: [H & M1, H & M2, H & M3, H & M4, H & M5, H & M6, H]): Hono, S & ToSchema, I7, MergeTypedResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, MergePath>; + , R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, I7 extends Input = I & I2 & I3 & I4 & I5 & I6, I8 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, E9 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H, M5 extends H = H, M6 extends H = H, M7 extends H = H>(method: M, path: P, ...handlers: [H & M1, H & M2, H & M3, H & M4, H & M5, H & M6, H & M7, H]): Hono, S & ToSchema, I8, MergeTypedResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, MergePath>; + , R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, I7 extends Input = I & I2 & I3 & I4 & I5 & I6, I8 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7, I9 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7 & I8, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, E9 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8]>, E10 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8, E9]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H, M5 extends H = H, M6 extends H = H, M7 extends H = H, M8 extends H = H>(method: M, path: P, ...handlers: [H & M1, H & M2, H & M3, H & M4, H & M5, H & M6, H & M7, H & M8, H]): Hono, S & ToSchema, I9, MergeTypedResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, MergePath>; + , R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, I7 extends Input = I & I2 & I3 & I4 & I5 & I6, I8 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7, I9 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7 & I8, I10 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7 & I8 & I9, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, E9 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8]>, E10 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8, E9]>, E11 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8, E9, E10]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H, M5 extends H = H, M6 extends H = H, M7 extends H = H, M8 extends H = H, M9 extends H = H>(method: M, path: P, ...handlers: [H & M1, H & M2, H & M3, H & M4, H & M5, H & M6, H & M7, H & M8, H & M9, H]): Hono, S & ToSchema, I10, MergeTypedResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, MergePath>; + = any, I extends Input = BlankInput>(method: M, path: P, ...handlers: [H, I, R>, ...H, I, R>[]]): Hono, I, MergeTypedResponse>, BasePath, MergePath>; + , R extends HandlerResponse = any, I extends Input = BlankInput, E2 extends Env = E>(methods: M[], path: P, handler: H): Hono, S & ToSchema, I, MergeTypedResponse>, BasePath, MergePath>; + , R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, M1 extends H = H>(methods: M[], path: P, ...handlers: [H & M1, H]): Hono, S & ToSchema, I2, MergeTypedResponse | MergeMiddlewareResponse>, BasePath, MergePath>; + , R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, M1 extends H = H, M2 extends H = H>(methods: M[], path: P, ...handlers: [H & M1, H & M2, H]): Hono, S & ToSchema, I3, MergeTypedResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, MergePath>; + , R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, M1 extends H = H, M2 extends H = H, M3 extends H = H>(methods: M[], path: P, ...handlers: [H & M1, H & M2, H & M3, H]): Hono, S & ToSchema, I4, MergeTypedResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, MergePath>; + , R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H>(methods: M[], path: P, ...handlers: [H & M1, H & M2, H & M3, H & M4, H]): Hono, S & ToSchema, I5, MergeTypedResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, MergePath>; + , R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H, M5 extends H = H>(methods: M[], path: P, ...handlers: [H & M1, H & M2, H & M3, H & M4, H & M5, H]): Hono, S & ToSchema, I6, MergeTypedResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, MergePath>; + , R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, I7 extends Input = I & I2 & I3 & I4 & I5 & I6, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H, M5 extends H = H, M6 extends H = H>(methods: M[], path: P, ...handlers: [H & M1, H & M2, H & M3, H & M4, H & M5, H & M6, H]): Hono, S & ToSchema, I7, MergeTypedResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, MergePath>; + , R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, I7 extends Input = I & I2 & I3 & I4 & I5 & I6, I8 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, E9 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H, M5 extends H = H, M6 extends H = H, M7 extends H = H>(methods: M[], path: P, ...handlers: [H & M1, H & M2, H & M3, H & M4, H & M5, H & M6, H & M7, H]): Hono, S & ToSchema, I8, MergeTypedResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, MergePath>; + , R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, I7 extends Input = I & I2 & I3 & I4 & I5 & I6, I8 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7, I9 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7 & I8, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, E9 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8]>, E10 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8, E9]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H, M5 extends H = H, M6 extends H = H, M7 extends H = H, M8 extends H = H>(methods: M[], path: P, ...handlers: [H & M1, H & M2, H & M3, H & M4, H & M5, H & M6, H & M7, H & M8, H]): Hono, S & ToSchema, I9, MergeTypedResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, MergePath>; + , R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, I7 extends Input = I & I2 & I3 & I4 & I5 & I6, I8 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7, I9 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7 & I8, I10 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7 & I8 & I9, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, E9 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8]>, E10 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8, E9]>, E11 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8, E9, E10]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H, M5 extends H = H, M6 extends H = H, M7 extends H = H, M8 extends H = H, M9 extends H = H>(methods: M[], path: P, ...handlers: [H & M1, H & M2, H & M3, H & M4, H & M5, H & M6, H & M7, H & M8, H & M9, H]): Hono, S & ToSchema, I10, MergeTypedResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, MergePath>; + = any, I extends Input = BlankInput>(methods: M[], path: P, ...handlers: [H, I, R>, ...H, I, R>[]]): Hono, I, MergeTypedResponse>, BasePath, MergePath>; + = any, E2 extends Env = E>(methods: M | M[], paths: Ps, ...handlers: H, I, R>[]): Hono, I, MergeTypedResponse>, BasePath, Ps extends [...string[], infer LastPath extends string] ? MergePath : never>; +} +type ToSchemaOutput = RorO extends TypedResponse ? { + output: unknown extends T ? {} : T; + outputFormat: I extends { + outputFormat: string; + } ? I['outputFormat'] : F; + status: U; +} : { + output: unknown extends RorO ? {} : RorO; + outputFormat: unknown extends RorO ? 'json' : I extends { + outputFormat: string; + } ? I['outputFormat'] : 'json'; + status: StatusCode; +}; +export type ToSchema = IsAny extends true ? { [K in P]: { [K2 in M as AddDollar]: { + input: AddParam, P>; + output: {}; + outputFormat: ResponseFormat; + status: StatusCode; +}; }; } : [RorO] extends [never] ? {} : [RorO] extends [Promise] ? {} : { [K in P]: { [K2 in M as AddDollar]: Simplify<{ + input: AddParam, P>; +} & ToSchemaOutput>; }; }; +export type Schema = { + [Path: string]: { + [Method: `$${Lowercase}`]: Endpoint; + }; +}; +type AddSchemaIfHasResponse = [Merged] extends [Promise] ? S : S & ToSchema, I, Merged>; +export type Endpoint = { + input: any; + output: any; + outputFormat: ResponseFormat; + status: StatusCode; +}; +type ExtractParams = string extends Path ? Record : Path extends `${infer _Start}:${infer Param}/${infer Rest}` ? { [K in Param | keyof ExtractParams<`/${Rest}`>]: string; } : Path extends `${infer _Start}:${infer Param}` ? { [K in Param]: string; } : never; +type FlattenIfIntersect = T extends (infer O) ? { [K in keyof O]: O[K]; } : never; +export type MergeSchemaPath = { [P in keyof OrigSchema as MergePath]: [OrigSchema[P]] extends [Record] ? { [M in keyof OrigSchema[P]]: MergeEndpointParamsWithPath; } : never; }; +type MergeEndpointParamsWithPath = T extends unknown ? { + input: T['input'] extends { + param: infer _; + } ? ExtractParams extends never ? T['input'] : FlattenIfIntersect as K extends `${infer Prefix}{${infer _}}` ? Prefix : K]: string; }; + }> : RemoveBlankRecord> extends never ? T['input'] : T['input'] & { + param: { [K in keyof ExtractParams as K extends `${infer Prefix}{${infer _}}` ? Prefix : K]: string; }; + }; + output: T['output']; + outputFormat: T['outputFormat']; + status: T['status']; +} : never; +export type AddParam = ParamKeys

extends never ? I : I extends { + param: infer _; +} ? I : I & { + param: UnionToIntersection>>; +}; +type AddDollar = `$${Lowercase}`; +export type MergePath = B extends '' ? MergePath : A extends '' ? B : A extends '/' ? B : A extends `${infer P}/` ? B extends `/${infer Q}` ? `${P}/${Q}` : `${P}/${B}` : B extends `/${infer Q}` ? Q extends '' ? A : `${A}/${Q}` : `${A}/${B}`; +export type KnownResponseFormat = 'json' | 'text' | 'redirect'; +export type ResponseFormat = KnownResponseFormat | string; +export type TypedResponse = { + _data: T; + _status: U; + _format: F; +}; +type MergeTypedResponse = T extends Promise ? T : T extends Promise ? T2 extends TypedResponse ? T2 : TypedResponse : T extends TypedResponse ? T : TypedResponse; +type ExtractTypedResponseOnly = T extends TypedResponse ? T : never; +type MergeMiddlewareResponse = T extends ((c: any, next: any) => Promise) ? Exclude extends never ? never : Exclude extends Response | TypedResponse ? ExtractTypedResponseOnly> : never : T extends ((c: any, next: any) => infer R) ? R extends Response | TypedResponse ? ExtractTypedResponseOnly : never : never; +export type FormValue = string | Blob; +export type ParsedFormValue = string | File; +export type ValidationTargets = { + json: any; + form: Record; + query: Record; + param: Record; + header: Record; + cookie: Record; +}; +type ParamKey = Component extends `:${infer NameWithPattern}` ? NameWithPattern extends `${infer Name}{${infer Rest}` ? Rest extends `${infer _Pattern}?` ? `${Name}?` : Name : NameWithPattern : never; +export type ParamKeys = Path extends `${infer Component}/${infer Rest}` ? ParamKey | ParamKeys : ParamKey; +export type ParamKeyToRecord = T extends `${infer R}?` ? Record : { [K in T]: string; }; +export type InputToDataByTarget = T extends { [K in Target]: infer R; } ? R : never; +export type RemoveQuestion = T extends `${infer R}?` ? R : T; +export type ExtractSchema = UnionToIntersection ? S : never>; +export type ExtractSchemaForStatusCode = { [Path in keyof ExtractSchema]: { [Method in keyof ExtractSchema[Path]]: Extract[Path][Method], { + status: Status; +}>; }; }; +export type ExtractHandlerResponse = T extends ((c: any, next: any) => Promise) ? Exclude extends never ? never : Exclude extends Response | TypedResponse ? Exclude : never : T extends ((c: any, next: any) => infer R) ? R extends Response | TypedResponse ? R : never : never; +type ProcessHead = IfAnyThenEmptyObject; +export type IntersectNonAnyTypes = T extends [infer Head, ...infer Rest] ? ProcessHead & IntersectNonAnyTypes : {}; +export declare abstract class FetchEventLike { + abstract readonly request: Request; + abstract respondWith(promise: Response | Promise): void; + abstract passThroughOnException(): void; + abstract waitUntil(promise: Promise): void; +} +//#endregion +export {}; diff --git a/tests/fixtures/npm/node_modules/hono/dist/types/utils/body.d.ts b/tests/fixtures/npm/node_modules/hono/dist/types/utils/body.d.ts new file mode 100644 index 000000000..9bbf8cafd --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/types/utils/body.d.ts @@ -0,0 +1,75 @@ +import { HonoRequest } from "../request.js"; +//#region src/utils/body.d.ts +type BodyDataValueDot = { + [x: string]: string | File | BodyDataValueDot; +}; +type BodyDataValueDotAll = { + [x: string]: string | File | (string | File)[] | BodyDataValueDotAll; +}; +type SimplifyBodyData = { [K in keyof T]: string | File | (string | File)[] | BodyDataValueDotAll extends T[K] ? string | File | (string | File)[] | BodyDataValueDotAll : string | File | BodyDataValueDot extends T[K] ? string | File | BodyDataValueDot : string | File | (string | File)[] extends T[K] ? string | File | (string | File)[] : string | File; } & {}; +type BodyDataValueComponent = string | File | (T extends { + all: false; +} ? never : T extends { + all: true; +} | { + all: boolean; +} ? (string | File)[] : never); +type BodyDataValueObject = { + [key: string]: BodyDataValueComponent | BodyDataValueObject; +}; +type BodyDataValue = BodyDataValueComponent | (T extends { + dot: false; +} ? never : T extends { + dot: true; +} | { + dot: boolean; +} ? BodyDataValueObject : never); +export type BodyData = {}> = SimplifyBodyData>>; +export type ParseBodyOptions = { + /** + * Determines whether all fields with multiple values should be parsed as arrays. + * @default false + * @example + * const data = new FormData() + * data.append('file', 'aaa') + * data.append('file', 'bbb') + * data.append('message', 'hello') + * + * If all is false: + * parseBody should return { file: 'bbb', message: 'hello' } + * + * If all is true: + * parseBody should return { file: ['aaa', 'bbb'], message: 'hello' } + */ + all: boolean; + /** + * Determines whether all fields with dot notation should be parsed as nested objects. + * @default false + * @example + * const data = new FormData() + * data.append('obj.key1', 'value1') + * data.append('obj.key2', 'value2') + * + * If dot is false: + * parseBody should return { 'obj.key1': 'value1', 'obj.key2': 'value2' } + * + * If dot is true: + * parseBody should return { obj: { key1: 'value1', key2: 'value2' } } + */ + dot: boolean; +}; +/** + * Parses the body of a request based on the provided options. + * + * @template T - The type of the parsed body data. + * @param {HonoRequest | Request} request - The request object to parse. + * @param {Partial} [options] - Options for parsing the body. + * @returns {Promise} The parsed body data. + */ +interface ParseBody { + , T extends BodyData>(request: HonoRequest | Request, options?: Options): Promise; + (request: HonoRequest | Request, options?: Partial): Promise; +} +export declare const parseBody: ParseBody; +//#endregion +export {}; diff --git a/tests/fixtures/npm/node_modules/hono/dist/types/utils/headers.d.ts b/tests/fixtures/npm/node_modules/hono/dist/types/utils/headers.d.ts new file mode 100644 index 000000000..d3edf8c77 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/types/utils/headers.d.ts @@ -0,0 +1,10 @@ +//#region src/utils/headers.d.ts +/** + * @module + * HTTP Headers utility. + */ +export type RequestHeader = 'A-IM' | 'Accept' | 'Accept-Additions' | 'Accept-CH' | 'Accept-Charset' | 'Accept-Datetime' | 'Accept-Encoding' | 'Accept-Features' | 'Accept-Language' | 'Accept-Patch' | 'Accept-Post' | 'Accept-Query' | 'Accept-Ranges' | 'Accept-Signature' | 'Access-Control' | 'Access-Control-Allow-Credentials' | 'Access-Control-Allow-Headers' | 'Access-Control-Allow-Methods' | 'Access-Control-Allow-Origin' | 'Access-Control-Expose-Headers' | 'Access-Control-Max-Age' | 'Access-Control-Request-Headers' | 'Access-Control-Request-Method' | 'Activate-Storage-Access' | 'Age' | 'Allow' | 'ALPN' | 'Alt-Svc' | 'Alt-Used' | 'Alternates' | 'AMP-Cache-Transform' | 'Apply-To-Redirect-Ref' | 'Authentication-Control' | 'Authentication-Info' | 'Authorization' | 'Available-Dictionary' | 'C-Ext' | 'C-Man' | 'C-Opt' | 'C-PEP' | 'C-PEP-Info' | 'Cache-Control' | 'Cache-Group-Invalidation' | 'Cache-Groups' | 'Cache-Status' | 'Cal-Managed-ID' | 'CalDAV-Timezones' | 'Capsule-Protocol' | 'CDN-Cache-Control' | 'CDN-Loop' | 'Cert-Not-After' | 'Cert-Not-Before' | 'Clear-Site-Data' | 'Client-Cert' | 'Client-Cert-Chain' | 'Close' | 'CMCD-Object' | 'CMCD-Request' | 'CMCD-Session' | 'CMCD-Status' | 'CMSD-Dynamic' | 'CMSD-Static' | 'Concealed-Auth-Export' | 'Configuration-Context' | 'Connection' | 'Content-Base' | 'Content-Digest' | 'Content-Disposition' | 'Content-Encoding' | 'Content-ID' | 'Content-Language' | 'Content-Length' | 'Content-Location' | 'Content-MD5' | 'Content-Range' | 'Content-Script-Type' | 'Content-Security-Policy' | 'Content-Security-Policy-Report-Only' | 'Content-Style-Type' | 'Content-Type' | 'Content-Version' | 'Cookie' | 'Cookie2' | 'Cross-Origin-Embedder-Policy' | 'Cross-Origin-Embedder-Policy-Report-Only' | 'Cross-Origin-Opener-Policy' | 'Cross-Origin-Opener-Policy-Report-Only' | 'Cross-Origin-Resource-Policy' | 'CTA-Common-Access-Token' | 'DASL' | 'Date' | 'DAV' | 'Default-Style' | 'Delta-Base' | 'Deprecation' | 'Depth' | 'Derived-From' | 'Destination' | 'Detached-JWS' | 'Differential-ID' | 'Dictionary-ID' | 'Digest' | 'DPoP' | 'DPoP-Nonce' | 'Early-Data' | 'EDIINT-Features' | 'ETag' | 'Expect' | 'Expect-CT' | 'Expires' | 'Ext' | 'Forwarded' | 'From' | 'GetProfile' | 'Hobareg' | 'Host' | 'HTTP2-Settings' | 'If' | 'If-Match' | 'If-Modified-Since' | 'If-None-Match' | 'If-Range' | 'If-Schedule-Tag-Match' | 'If-Unmodified-Since' | 'IM' | 'Include-Referred-Token-Binding-ID' | 'Incremental' | 'Isolation' | 'Keep-Alive' | 'Label' | 'Last-Event-ID' | 'Last-Modified' | 'Link' | 'Link-Template' | 'Location' | 'Lock-Token' | 'Man' | 'Max-Forwards' | 'Memento-Datetime' | 'Meter' | 'Method-Check' | 'Method-Check-Expires' | 'MIME-Version' | 'Negotiate' | 'NEL' | 'OData-EntityId' | 'OData-Isolation' | 'OData-MaxVersion' | 'OData-Version' | 'Opt' | 'Optional-WWW-Authenticate' | 'Ordering-Type' | 'Origin' | 'Origin-Agent-Cluster' | 'OSCORE' | 'OSLC-Core-Version' | 'Overwrite' | 'P3P' | 'PEP' | 'PEP-Info' | 'Permissions-Policy' | 'PICS-Label' | 'Ping-From' | 'Ping-To' | 'Position' | 'Pragma' | 'Prefer' | 'Preference-Applied' | 'Priority' | 'ProfileObject' | 'Protocol' | 'Protocol-Info' | 'Protocol-Query' | 'Protocol-Request' | 'Proxy-Authenticate' | 'Proxy-Authentication-Info' | 'Proxy-Authorization' | 'Proxy-Features' | 'Proxy-Instruction' | 'Proxy-Status' | 'Public' | 'Public-Key-Pins' | 'Public-Key-Pins-Report-Only' | 'Range' | 'Redirect-Ref' | 'Referer' | 'Referer-Root' | 'Referrer-Policy' | 'Refresh' | 'Repeatability-Client-ID' | 'Repeatability-First-Sent' | 'Repeatability-Request-ID' | 'Repeatability-Result' | 'Replay-Nonce' | 'Reporting-Endpoints' | 'Repr-Digest' | 'Retry-After' | 'Safe' | 'Schedule-Reply' | 'Schedule-Tag' | 'Sec-Fetch-Dest' | 'Sec-Fetch-Mode' | 'Sec-Fetch-Site' | 'Sec-Fetch-Storage-Access' | 'Sec-Fetch-User' | 'Sec-GPC' | 'Sec-Purpose' | 'Sec-Token-Binding' | 'Sec-WebSocket-Accept' | 'Sec-WebSocket-Extensions' | 'Sec-WebSocket-Key' | 'Sec-WebSocket-Protocol' | 'Sec-WebSocket-Version' | 'Security-Scheme' | 'Server' | 'Server-Timing' | 'Set-Cookie' | 'Set-Cookie2' | 'Set-Txn' | 'SetProfile' | 'Signature' | 'Signature-Input' | 'SLUG' | 'SoapAction' | 'Status-URI' | 'Strict-Transport-Security' | 'Sunset' | 'Surrogate-Capability' | 'Surrogate-Control' | 'TCN' | 'TE' | 'Timeout' | 'Timing-Allow-Origin' | 'Topic' | 'Traceparent' | 'Tracestate' | 'Trailer' | 'Transfer-Encoding' | 'TTL' | 'Unencoded-Digest' | 'Upgrade' | 'Urgency' | 'URI' | 'Use-As-Dictionary' | 'User-Agent' | 'Variant-Vary' | 'Vary' | 'Via' | 'Want-Content-Digest' | 'Want-Digest' | 'Want-Repr-Digest' | 'Want-Unencoded-Digest' | 'Warning' | 'WWW-Authenticate' | 'X-Content-Type-Options' | 'X-Frame-Options'; +export type ResponseHeader = 'Accept-Query' | 'Access-Control-Allow-Credentials' | 'Access-Control-Allow-Headers' | 'Access-Control-Allow-Methods' | 'Access-Control-Allow-Origin' | 'Access-Control-Expose-Headers' | 'Access-Control-Max-Age' | 'Activate-Storage-Access' | 'Age' | 'Allow' | 'Cache-Control' | 'Cache-Group-Invalidation' | 'Cache-Groups' | 'Clear-Site-Data' | 'Content-Disposition' | 'Content-Encoding' | 'Content-Language' | 'Content-Length' | 'Content-Location' | 'Content-Range' | 'Content-Security-Policy' | 'Content-Security-Policy-Report-Only' | 'Content-Type' | 'Cookie' | 'Cross-Origin-Embedder-Policy' | 'Cross-Origin-Opener-Policy' | 'Cross-Origin-Resource-Policy' | 'Date' | 'ETag' | 'Expires' | 'Incremental' | 'Last-Modified' | 'Location' | 'Permissions-Policy' | 'Pragma' | 'Retry-After' | 'Save-Data' | 'Sec-CH-Prefers-Color-Scheme' | 'Sec-CH-Prefers-Reduced-Motion' | 'Sec-CH-UA' | 'Sec-CH-UA-Arch' | 'Sec-CH-UA-Bitness' | 'Sec-CH-UA-Form-Factor' | 'Sec-CH-UA-Full-Version' | 'Sec-CH-UA-Full-Version-List' | 'Sec-CH-UA-Mobile' | 'Sec-CH-UA-Model' | 'Sec-CH-UA-Platform' | 'Sec-CH-UA-Platform-Version' | 'Sec-CH-UA-WoW64' | 'Sec-Fetch-Dest' | 'Sec-Fetch-Mode' | 'Sec-Fetch-Site' | 'Sec-Fetch-User' | 'Sec-GPC' | 'Server' | 'Server-Timing' | 'Service-Worker-Navigation-Preload' | 'Set-Cookie' | 'Set-Txn' | 'Strict-Transport-Security' | 'Timing-Allow-Origin' | 'Trailer' | 'Transfer-Encoding' | 'Unencoded-Digest' | 'Upgrade' | 'Vary' | 'Want-Unencoded-Digest' | 'Warning' | 'WWW-Authenticate' | 'X-Content-Type-Options' | 'X-DNS-Prefetch-Control' | 'X-Frame-Options' | 'X-Permitted-Cross-Domain-Policies' | 'X-Powered-By' | 'X-Robots-Tag' | 'X-XSS-Protection'; +export type AcceptHeader = 'Accept' | 'Accept-Charset' | 'Accept-Encoding' | 'Accept-Language' | 'Accept-Patch' | 'Accept-Post' | 'Accept-Ranges'; +export type CustomHeader = string & {}; +//#endregion \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/hono/dist/types/utils/http-status.d.ts b/tests/fixtures/npm/node_modules/hono/dist/types/utils/http-status.d.ts new file mode 100644 index 000000000..4c48304e2 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/types/utils/http-status.d.ts @@ -0,0 +1,34 @@ +//#region src/utils/http-status.d.ts +/** + * @module + * HTTP Status utility. + */ +export type InfoStatusCode = 100 | 101 | 102 | 103; +export type SuccessStatusCode = 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 226; +export type DeprecatedStatusCode = 305 | 306; +export type RedirectStatusCode = 300 | 301 | 302 | 303 | 304 | DeprecatedStatusCode | 307 | 308; +export type ClientErrorStatusCode = 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 421 | 422 | 423 | 424 | 425 | 426 | 428 | 429 | 431 | 451; +export type ServerErrorStatusCode = 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 510 | 511; +/** + * `UnofficialStatusCode` can be used to specify an unofficial status code. + * @example + * + * ```ts + * app.get('/unknown', (c) => { + * return c.text("Unknown Error", 520 as UnofficialStatusCode) + * }) + * ``` + */ +export type UnofficialStatusCode = -1; +/** + * @deprecated + * Use `UnofficialStatusCode` instead. + */ +export type UnOfficalStatusCode = UnofficialStatusCode; +/** + * If you want to use an unofficial status, use `UnofficialStatusCode`. + */ +export type StatusCode = InfoStatusCode | SuccessStatusCode | RedirectStatusCode | ClientErrorStatusCode | ServerErrorStatusCode | UnofficialStatusCode; +export type ContentlessStatusCode = 101 | 204 | 205 | 304; +export type ContentfulStatusCode = Exclude; +//#endregion \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/hono/dist/types/utils/mime.d.ts b/tests/fixtures/npm/node_modules/hono/dist/types/utils/mime.d.ts new file mode 100644 index 000000000..b4ea3597e --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/types/utils/mime.d.ts @@ -0,0 +1,73 @@ +//#region src/utils/mime.d.ts +/** + * @module + * MIME utility. + */ +export declare const getMimeType: (filename: string, mimes?: Record) => string | undefined; +export declare const getExtension: (mimeType: string) => string | undefined; +/** + * Union types for BaseMime + */ +export type BaseMime = (typeof _baseMimes)[keyof typeof _baseMimes]; +declare const _baseMimes: { + readonly aac: "audio/aac"; + readonly avi: "video/x-msvideo"; + readonly avif: "image/avif"; + readonly av1: "video/av1"; + readonly bin: "application/octet-stream"; + readonly bmp: "image/bmp"; + readonly css: "text/css; charset=utf-8"; + readonly csv: "text/csv; charset=utf-8"; + readonly eot: "application/vnd.ms-fontobject"; + readonly epub: "application/epub+zip"; + readonly gif: "image/gif"; + readonly gz: "application/gzip"; + readonly htm: "text/html; charset=utf-8"; + readonly html: "text/html; charset=utf-8"; + readonly ico: "image/x-icon"; + readonly ics: "text/calendar; charset=utf-8"; + readonly jpeg: "image/jpeg"; + readonly jpg: "image/jpeg"; + readonly js: "text/javascript; charset=utf-8"; + readonly json: "application/json"; + readonly jsonld: "application/ld+json"; + readonly map: "application/json"; + readonly mid: "audio/x-midi"; + readonly midi: "audio/x-midi"; + readonly mjs: "text/javascript; charset=utf-8"; + readonly mp3: "audio/mpeg"; + readonly mp4: "video/mp4"; + readonly mpeg: "video/mpeg"; + readonly oga: "audio/ogg"; + readonly ogv: "video/ogg"; + readonly ogx: "application/ogg"; + readonly opus: "audio/opus"; + readonly otf: "font/otf"; + readonly pdf: "application/pdf"; + readonly png: "image/png"; + readonly rtf: "application/rtf"; + readonly svg: "image/svg+xml; charset=utf-8"; + readonly tif: "image/tiff"; + readonly tiff: "image/tiff"; + readonly ts: "video/mp2t"; + readonly ttf: "font/ttf"; + readonly txt: "text/plain; charset=utf-8"; + readonly wasm: "application/wasm"; + readonly webm: "video/webm"; + readonly weba: "audio/webm"; + readonly webmanifest: "application/manifest+json"; + readonly webp: "image/webp"; + readonly woff: "font/woff"; + readonly woff2: "font/woff2"; + readonly xhtml: "application/xhtml+xml; charset=utf-8"; + readonly xml: "application/xml; charset=utf-8"; + readonly zip: "application/zip"; + readonly '3gp': "video/3gpp"; + readonly '3g2': "video/3gpp2"; + readonly gltf: "model/gltf+json"; + readonly glb: "model/gltf-binary"; +}; +declare const baseMimes: Record; +//#endregion +export { baseMimes as mimes }; +export {}; diff --git a/tests/fixtures/npm/node_modules/hono/dist/types/utils/types.d.ts b/tests/fixtures/npm/node_modules/hono/dist/types/utils/types.d.ts new file mode 100644 index 000000000..2a4ad3781 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/types/utils/types.d.ts @@ -0,0 +1,62 @@ +//#region src/utils/types.d.ts +/** + * @module + * Types utility. + */ +export type Expect = T; +export type Equal = (() => T extends X ? 1 : 2) extends (() => T extends Y ? 1 : 2) ? true : false; +export type NotEqual = true extends Equal ? false : true; +export type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never; +export type RemoveBlankRecord = T extends Record ? (K extends string ? T : never) : never; +export type IfAnyThenEmptyObject = 0 extends 1 & T ? {} : T; +export type JSONPrimitive = string | boolean | number | null; +export type JSONArray = (JSONPrimitive | JSONObject | JSONArray)[]; +export type JSONObject = { + [key: string]: JSONPrimitive | JSONArray | JSONObject | object | InvalidJSONValue; +}; +export type InvalidJSONValue = undefined | symbol | ((...args: unknown[]) => unknown); +type InvalidToNull = T extends InvalidJSONValue ? null : T; +type IsInvalid = T extends InvalidJSONValue ? true : false; +/** + * symbol keys are omitted through `JSON.stringify` + */ +type OmitSymbolKeys = { [K in keyof T as K extends symbol ? never : K]: T[K]; }; +export type JSONValue = JSONObject | JSONArray | JSONPrimitive; +/** + * Convert a type to a JSON-compatible type. + * + * Non-JSON values such as `Date` implement `.toJSON()`, + * so they can be transformed to a value assignable to `JSONObject` + * + * `JSON.stringify()` throws a `TypeError` when it encounters a `bigint` value, + * unless a custom `replacer` function or `.toJSON()` method is provided. + * + * This behavior can be controlled by the `TError` generic type parameter, + * which defaults to `bigint | ReadonlyArray`. + * You can set it to `never` to disable this check. + */ +export type JSONParsed> = T extends { + toJSON(): infer J; +} ? (() => J) extends (() => JSONPrimitive) ? J : (() => J) extends (() => { + toJSON(): unknown; +}) ? {} : JSONParsed : T extends JSONPrimitive ? T : T extends InvalidJSONValue ? never : T extends ReadonlyArray ? { [K in keyof T]: JSONParsed, TError>; } extends (infer A) ? A extends ReadonlyArray ? A : JSONParsed, TError>[] : never : T extends Set | Map | Record ? {} : T extends object ? T[keyof T] extends TError ? never : { [K in keyof OmitSymbolKeys as IsInvalid extends true ? never : K]: boolean extends IsInvalid ? JSONParsed | undefined : JSONParsed; } : T extends unknown ? T extends TError ? never : JSONValue : never; +/** + * Useful to flatten the type output to improve type hints shown in editors. And also to transform an interface into a type to aide with assignability. + * @copyright from sindresorhus/type-fest + */ +export type Simplify = { [KeyType in keyof T]: T[KeyType]; } & {}; +/** + * A simple extension of Simplify that will deeply traverse array elements. + */ +export type SimplifyDeepArray = T extends any[] ? { [E in keyof T]: SimplifyDeepArray; } : Simplify; +export type InterfaceToType = T extends Function ? T : { [K in keyof T]: InterfaceToType; }; +export type RequiredKeysOf = Exclude<{ [Key in keyof BaseType]: BaseType extends Record ? Key : never; }[keyof BaseType], undefined>; +export type HasRequiredKeys = RequiredKeysOf extends never ? false : true; +export type IsAny = boolean extends (T extends never ? true : false) ? true : false; +/** + * String literal types with auto-completion + * @see https://github.com/Microsoft/TypeScript/issues/29729 + */ +export type StringLiteralUnion = T | (string & Record); +//#endregion +export {}; diff --git a/tests/fixtures/npm/node_modules/hono/dist/types/validator/index.d.ts b/tests/fixtures/npm/node_modules/hono/dist/types/validator/index.d.ts new file mode 100644 index 000000000..3a494895e --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/types/validator/index.d.ts @@ -0,0 +1,3 @@ +import { InferInput } from "./utils.js"; +import { ValidationFunction, validator } from "./validator.js"; +export { type InferInput, type ValidationFunction, validator }; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/hono/dist/types/validator/utils.d.ts b/tests/fixtures/npm/node_modules/hono/dist/types/validator/utils.d.ts new file mode 100644 index 000000000..13159be3c --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/types/validator/utils.d.ts @@ -0,0 +1,27 @@ +import { UnionToIntersection } from "../utils/types.js"; +import { FormValue, ParsedFormValue, ValidationTargets } from "../types.js"; +//#region src/validator/utils.d.ts +/** + * Checks if T is a literal union type (e.g., 'asc' | 'desc') + * that should be preserved in input types. + * Returns true for union literals, false for single literals or wide types. + */ +export type IsLiteralUnion = [Exclude] extends [Base] ? [Exclude] extends [UnionToIntersection>] ? false : true : false; +type IsOptionalUnion = [unknown] extends [T] ? false : undefined extends T ? true : false; +type SimplifyDeep = { [K in keyof T]: T[K]; } & {}; +type InferInputInner = SimplifyDeep<{ [K in keyof Output]: IsLiteralUnion extends true ? Output[K] : IsOptionalUnion extends true ? Output[K] : Target extends 'form' ? T | T[] : Target extends 'query' ? string | string[] : Target extends 'param' ? string : Target extends 'header' ? string : Target extends 'cookie' ? string : unknown; }>; +/** + * Utility type to infer input types for validation targets. + * Preserves literal union types (e.g., 'asc' | 'desc') while using + * the default ValidationTargets type for other values. + * + * @example + * ```ts + * // In @hono/zod-validator or similar: + * type Input = InferInput, 'query'> + * // { orderBy: 'asc' | 'desc', page: string | string[] } + * ``` + */ +export type InferInput = [Exclude] extends [never] ? {} : [Exclude] extends [object] ? undefined extends Output ? SimplifyDeep, Target, T>> | undefined : SimplifyDeep> : {}; +//#endregion +export {}; diff --git a/tests/fixtures/npm/node_modules/hono/dist/types/validator/validator.d.ts b/tests/fixtures/npm/node_modules/hono/dist/types/validator/validator.d.ts new file mode 100644 index 000000000..85e09a1a3 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/types/validator/validator.d.ts @@ -0,0 +1,18 @@ +import { Env, FormValue, MiddlewareHandler, TypedResponse, ValidationTargets } from "../types.js"; +import { Context } from "../context.js"; +import { InferInput } from "./utils.js"; +//#region src/validator/validator.d.ts +type ValidationTargetKeysWithBody = 'form' | 'json'; +type ValidationTargetByMethod = M extends 'get' | 'head' ? Exclude : keyof ValidationTargets; +export type ValidationFunction = (value: InputType, c: Context) => OutputType | TypedResponse | Promise | Promise; +export type ExtractValidationResponse = VF extends ((value: any, c: any) => infer R) ? R extends Promise ? PR extends TypedResponse ? TypedResponse : PR extends Response ? PR : PR extends undefined ? never : never : R extends TypedResponse ? TypedResponse : R extends Response ? R : R extends undefined ? never : never : never; +export declare const validator: , P2 extends string = P, VF extends (value: unknown extends InputType ? ValidationTargets[U] : InputType, c: Context) => any = (value: unknown extends InputType ? ValidationTargets[U] : InputType, c: Context) => any, V extends { + in: { [K in U]: K extends "json" ? unknown extends InputType ? ExtractValidatorOutput : InputType : InferInput, K, FormValue>; }; + out: { [K in U]: ExtractValidatorOutput; }; +} = { + in: { [K in U]: K extends "json" ? unknown extends InputType ? ExtractValidatorOutput : InputType : InferInput, K, FormValue>; }; + out: { [K in U]: ExtractValidatorOutput; }; +}, E extends Env = any>(target: U, validationFunc: VF) => MiddlewareHandler>; +export type ExtractValidatorOutput = VF extends ((value: any, c: any) => infer R) ? R extends Promise ? PR extends Response | TypedResponse ? never : PR : R extends Response | TypedResponse ? never : R : never; +//#endregion +export {}; diff --git a/tests/fixtures/npm/node_modules/hono/dist/utils/body.js b/tests/fixtures/npm/node_modules/hono/dist/utils/body.js new file mode 100644 index 000000000..6fe8e3031 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/utils/body.js @@ -0,0 +1,96 @@ +import { bufferToFormData } from "./buffer.js"; +//#region src/utils/body.ts +const MAX_NESTED_OBJECTS = 1e4; +const isRawRequest = (request) => "headers" in request; +const parseBody = async (request, options = Object.create(null)) => { + const { all = false, dot = false } = options; + const mediaType = (isRawRequest(request) ? request.headers : request.raw.headers).get("Content-Type")?.split(";")[0].trim().toLowerCase(); + if (mediaType === "multipart/form-data" || mediaType === "application/x-www-form-urlencoded") return parseFormData(request, { + all, + dot + }); + return {}; +}; +/** +* Parses form data from a request. +* +* @template T - The type of the parsed body data. +* @param {HonoRequest | Request} request - The request object containing form data. +* @param {ParseBodyOptions} options - Options for parsing the form data. +* @returns {Promise} The parsed body data. +*/ +async function parseFormData(request, options) { + if (!isRawRequest(request) && request.bodyCache.formData) return convertFormDataToBodyData(await request.bodyCache.formData, options); + const headers = isRawRequest(request) ? request.headers : request.raw.headers; + const arrayBuffer = await request.arrayBuffer(); + const formDataPromise = bufferToFormData(arrayBuffer, headers.get("Content-Type") || ""); + if (!isRawRequest(request)) request.bodyCache.formData = formDataPromise; + const formData = await formDataPromise; + if (formData) return convertFormDataToBodyData(formData, options); + return {}; +} +/** +* Converts form data to body data based on the provided options. +* +* @template T - The type of the parsed body data. +* @param {FormData} formData - The form data to convert. +* @param {ParseBodyOptions} options - Options for parsing the form data. +* @returns {T} The converted body data. +*/ +function convertFormDataToBodyData(formData, options) { + const form = Object.create(null); + const nestingState = { count: 0 }; + formData.forEach((value, key) => { + if (!(options.all || key.endsWith("[]"))) form[key] = value; + else handleParsingAllValues(form, key, value); + }); + if (options.dot) Object.entries(form).forEach(([key, value]) => { + if (key.includes(".")) { + handleParsingNestedValues(form, key, value, nestingState); + delete form[key]; + } + }); + return form; +} +/** +* Handles parsing all values for a given key, supporting multiple values as arrays. +* +* @param {BodyData} form - The form data object. +* @param {string} key - The key to parse. +* @param {FormDataEntryValue} value - The value to assign. +*/ +const handleParsingAllValues = (form, key, value) => { + if (form[key] !== void 0) { + if (Array.isArray(form[key])) form[key].push(value); + else form[key] = [form[key], value]; + } else if (!key.endsWith("[]")) form[key] = value; + else form[key] = [value]; +}; +/** +* Handles parsing nested values using dot notation keys. +* +* @param {BodyData} form - The form data object. +* @param {string} key - The dot notation key. +* @param {BodyDataValue} value - The value to assign. +*/ +const handleParsingNestedValues = (form, key, value, state) => { + if (/(?:^|\.)__proto__\./.test(key)) return; + let nestedForm = form; + const keys = key.split(".", 34); + if (keys.length > 33) throwNestingLimitExceeded(); + keys.forEach((key, index) => { + if (index === keys.length - 1) nestedForm[key] = value; + else { + if (!nestedForm[key] || typeof nestedForm[key] !== "object" || Array.isArray(nestedForm[key]) || nestedForm[key] instanceof File) { + if (state.count++ >= MAX_NESTED_OBJECTS) throwNestingLimitExceeded(); + nestedForm[key] = Object.create(null); + } + nestedForm = nestedForm[key]; + } + }); +}; +const throwNestingLimitExceeded = () => { + throw new Error("Nesting limit exceeded"); +}; +//#endregion +export { parseBody }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/utils/buffer.js b/tests/fixtures/npm/node_modules/hono/dist/utils/buffer.js new file mode 100644 index 000000000..8c6506452 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/utils/buffer.js @@ -0,0 +1,51 @@ +import { sha256 } from "./crypto.js"; +//#region src/utils/buffer.ts +/** +* @module +* Buffer utility. +*/ +const equal = (a, b) => { + if (a === b) return true; + if (a.byteLength !== b.byteLength) return false; + const va = new DataView(a); + const vb = new DataView(b); + let i = va.byteLength; + while (i--) if (va.getUint8(i) !== vb.getUint8(i)) return false; + return true; +}; +const constantTimeEqualString = (a, b) => { + const aLen = a.length; + const bLen = b.length; + const maxLen = Math.max(aLen, bLen); + let out = aLen ^ bLen; + for (let i = 0; i < maxLen; i++) { + const aChar = i < aLen ? a.charCodeAt(i) : 0; + const bChar = i < bLen ? b.charCodeAt(i) : 0; + out |= aChar ^ bChar; + } + return out === 0; +}; +const timingSafeEqualString = async (a, b, hashFunction) => { + if (!hashFunction) hashFunction = sha256; + const [sa, sb] = await Promise.all([hashFunction(a), hashFunction(b)]); + if (sa == null || sb == null || typeof sa !== "string" || typeof sb !== "string") return false; + const hashEqual = constantTimeEqualString(sa, sb); + const originalEqual = constantTimeEqualString(a, b); + return hashEqual && originalEqual; +}; +const timingSafeEqual = async (a, b, hashFunction) => { + if (typeof a === "string" && typeof b === "string") return timingSafeEqualString(a, b, hashFunction); + if (!hashFunction) hashFunction = sha256; + const [sa, sb] = await Promise.all([hashFunction(a), hashFunction(b)]); + if (!sa || !sb || typeof sa !== "string" || typeof sb !== "string") return false; + return timingSafeEqualString(sa, sb); +}; +const bufferToString = (buffer) => { + if (buffer instanceof ArrayBuffer) return new TextDecoder("utf-8").decode(buffer); + return buffer; +}; +const bufferToFormData = (arrayBuffer, contentType) => { + return new Response(arrayBuffer, { headers: { "Content-Type": contentType.replace(/^[^;]+/, (mediaType) => mediaType.toLowerCase()) } }).formData(); +}; +//#endregion +export { bufferToFormData, bufferToString, equal, timingSafeEqual }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/utils/constants.js b/tests/fixtures/npm/node_modules/hono/dist/utils/constants.js new file mode 100644 index 000000000..8b0615b69 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/utils/constants.js @@ -0,0 +1,7 @@ +//#region src/utils/constants.ts +/** +* Constant used to mark a composed handler. +*/ +const COMPOSED_HANDLER = "__COMPOSED_HANDLER"; +//#endregion +export { COMPOSED_HANDLER }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/utils/cookie.js b/tests/fixtures/npm/node_modules/hono/dist/utils/cookie.js new file mode 100644 index 000000000..fd541131c --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/utils/cookie.js @@ -0,0 +1,125 @@ +import { tryDecodeURIComponent } from "./url.js"; +//#region src/utils/cookie.ts +/** +* @module +* Cookie utility. +*/ +const algorithm = { + name: "HMAC", + hash: "SHA-256" +}; +const getCryptoKey = async (secret) => { + const secretBuf = typeof secret === "string" ? new TextEncoder().encode(secret) : secret; + return await crypto.subtle.importKey("raw", secretBuf, algorithm, false, ["sign", "verify"]); +}; +const makeSignature = async (value, secret) => { + const key = await getCryptoKey(secret); + const signature = await crypto.subtle.sign(algorithm.name, key, new TextEncoder().encode(value)); + return btoa(String.fromCharCode(...new Uint8Array(signature))); +}; +const verifySignature = async (base64Signature, value, secret) => { + try { + const signatureBinStr = atob(base64Signature); + const signature = new Uint8Array(signatureBinStr.length); + for (let i = 0, len = signatureBinStr.length; i < len; i++) signature[i] = signatureBinStr.charCodeAt(i); + return await crypto.subtle.verify(algorithm, secret, signature, new TextEncoder().encode(value)); + } catch { + return false; + } +}; +const validCookieNameRegEx = /^[\w!#$%&'*.^`|~+-]+$/; +const relaxedCookieNameRegEx = /^[!#-:<>-[\]-~]+$/; +const validCookieValueRegEx = /^[ !#-:<-[\]-~]*$/; +const trimCookieWhitespace = (value) => { + let start = 0; + let end = value.length; + while (start < end) { + const charCode = value.charCodeAt(start); + if (charCode !== 32 && charCode !== 9) break; + start++; + } + while (end > start) { + const charCode = value.charCodeAt(end - 1); + if (charCode !== 32 && charCode !== 9) break; + end--; + } + return start === 0 && end === value.length ? value : value.slice(start, end); +}; +const parse = (cookie, name) => { + if (name && cookie.indexOf(name) === -1) return {}; + const pairs = cookie.split(";"); + const parsedCookie = Object.create(null); + for (const pairStr of pairs) { + const valueStartPos = pairStr.indexOf("="); + if (valueStartPos === -1) continue; + const cookieName = trimCookieWhitespace(pairStr.substring(0, valueStartPos)); + if (name && name !== cookieName || !relaxedCookieNameRegEx.test(cookieName) || cookieName in parsedCookie) continue; + let cookieValue = trimCookieWhitespace(pairStr.substring(valueStartPos + 1)); + if (cookieValue.startsWith("\"") && cookieValue.endsWith("\"")) cookieValue = cookieValue.slice(1, -1); + if (validCookieValueRegEx.test(cookieValue)) { + parsedCookie[cookieName] = tryDecodeURIComponent(cookieValue); + if (name) break; + } + } + return parsedCookie; +}; +const parseSigned = async (cookie, secret, name) => { + const parsedCookie = Object.create(null); + const secretKey = await getCryptoKey(secret); + for (const [key, value] of Object.entries(parse(cookie, name))) { + const signatureStartPos = value.lastIndexOf("."); + if (signatureStartPos < 0) continue; + const signedValue = value.substring(0, signatureStartPos); + const signature = value.substring(signatureStartPos + 1); + if (signature.length !== 44 || !signature.endsWith("=")) continue; + parsedCookie[key] = await verifySignature(signature, signedValue, secretKey) ? signedValue : false; + } + return parsedCookie; +}; +const _serialize = (name, value, opt = {}) => { + if (!validCookieNameRegEx.test(name)) throw new Error("Invalid cookie name"); + let cookie = `${name}=${value}`; + if (name.startsWith("__Secure-") && !opt.secure) throw new Error("__Secure- Cookie must have Secure attributes"); + if (name.startsWith("__Host-")) { + if (!opt.secure) throw new Error("__Host- Cookie must have Secure attributes"); + if (opt.path !== "/") throw new Error("__Host- Cookie must have Path attributes with \"/\""); + if (opt.domain) throw new Error("__Host- Cookie must not have Domain attributes"); + } + for (const key of [ + "domain", + "path", + "sameSite", + "priority" + ]) if (opt[key] && /[;\r\n]/.test(opt[key])) throw new Error(`${key} must not contain ";", "\\r", or "\\n"`); + if (opt && typeof opt.maxAge === "number" && opt.maxAge >= 0) { + if (opt.maxAge > 3456e4) throw new Error("Cookies Max-Age SHOULD NOT be greater than 400 days (34560000 seconds) in duration."); + cookie += `; Max-Age=${opt.maxAge | 0}`; + } + if (opt.domain && opt.prefix !== "host") cookie += `; Domain=${opt.domain}`; + if (opt.path) cookie += `; Path=${opt.path}`; + if (opt.expires) { + if (opt.expires.getTime() - Date.now() > 3456e7) throw new Error("Cookies Expires SHOULD NOT be greater than 400 days (34560000 seconds) in the future."); + cookie += `; Expires=${opt.expires.toUTCString()}`; + } + if (opt.httpOnly) cookie += "; HttpOnly"; + if (opt.secure) cookie += "; Secure"; + if (opt.sameSite) cookie += `; SameSite=${opt.sameSite.charAt(0).toUpperCase() + opt.sameSite.slice(1)}`; + if (opt.priority) cookie += `; Priority=${opt.priority.charAt(0).toUpperCase() + opt.priority.slice(1)}`; + if (opt.partitioned) { + if (!opt.secure) throw new Error("Partitioned Cookie must have Secure attributes"); + cookie += "; Partitioned"; + } + return cookie; +}; +const serialize = (name, value, opt) => { + value = encodeURIComponent(value); + return _serialize(name, value, opt); +}; +const serializeSigned = async (name, value, secret, opt = {}) => { + const signature = await makeSignature(value, secret); + value = `${value}.${signature}`; + value = encodeURIComponent(value); + return _serialize(name, value, opt); +}; +//#endregion +export { parse, parseSigned, serialize, serializeSigned }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/utils/crypto.js b/tests/fixtures/npm/node_modules/hono/dist/utils/crypto.js new file mode 100644 index 000000000..548867c49 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/utils/crypto.js @@ -0,0 +1,34 @@ +//#region src/utils/crypto.ts +const sha256 = async (data) => { + return await createHash(data, { + name: "SHA-256", + alias: "sha256" + }); +}; +const sha1 = async (data) => { + return await createHash(data, { + name: "SHA-1", + alias: "sha1" + }); +}; +const md5 = async (data) => { + return await createHash(data, { + name: "MD5", + alias: "md5" + }); +}; +const createHash = async (data, algorithm) => { + let sourceBuffer; + if (ArrayBuffer.isView(data) || data instanceof ArrayBuffer) sourceBuffer = data; + else { + if (typeof data === "object") data = JSON.stringify(data); + sourceBuffer = new TextEncoder().encode(String(data)); + } + if (crypto && crypto.subtle) { + const buffer = await crypto.subtle.digest({ name: algorithm.name }, sourceBuffer); + return Array.prototype.map.call(new Uint8Array(buffer), (x) => ("00" + x.toString(16)).slice(-2)).join(""); + } + return null; +}; +//#endregion +export { createHash, md5, sha1, sha256 }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/utils/html.js b/tests/fixtures/npm/node_modules/hono/dist/utils/html.js new file mode 100644 index 000000000..7615ebdbd --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/utils/html.js @@ -0,0 +1,103 @@ +//#region src/utils/html.ts +/** +* @module +* HTML utility. +*/ +const HtmlEscapedCallbackPhase = { + Stringify: 1, + BeforeStream: 2, + Stream: 3 +}; +const raw = (value, callbacks) => { + const escapedString = new String(value); + escapedString.isEscaped = true; + escapedString.callbacks = callbacks; + return escapedString; +}; +const escapeRe = /[&<>'"]/; +const stringBufferToString = async (buffer, callbacks) => { + let str = ""; + callbacks ||= []; + const resolvedBuffer = await Promise.all(buffer); + for (let i = resolvedBuffer.length - 1;; i--) { + str += resolvedBuffer[i]; + i--; + if (i < 0) break; + let r = resolvedBuffer[i]; + if (typeof r === "object") callbacks.push(...r.callbacks || []); + const isEscaped = r.isEscaped; + r = await (typeof r === "object" ? r.toString() : r); + if (typeof r === "object") callbacks.push(...r.callbacks || []); + if (r.isEscaped ?? isEscaped) str += r; + else { + const buf = [str]; + escapeToBuffer(r, buf); + str = buf[0]; + } + } + return raw(str, callbacks); +}; +const escapeToBuffer = (str, buffer) => { + const match = str.search(escapeRe); + if (match === -1) { + buffer[0] += str; + return; + } + let escape; + let index; + let lastIndex = 0; + for (index = match; index < str.length; index++) { + switch (str.charCodeAt(index)) { + case 34: + escape = """; + break; + case 39: + escape = "'"; + break; + case 38: + escape = "&"; + break; + case 60: + escape = "<"; + break; + case 62: + escape = ">"; + break; + default: continue; + } + buffer[0] += str.substring(lastIndex, index) + escape; + lastIndex = index + 1; + } + buffer[0] += str.substring(lastIndex, index); +}; +const resolveCallbackSync = (str) => { + const callbacks = str.callbacks; + if (!callbacks?.length) return str; + const buffer = [str]; + const context = {}; + callbacks.forEach((c) => c({ + phase: HtmlEscapedCallbackPhase.Stringify, + buffer, + context + })); + return buffer[0]; +}; +const resolveCallback = async (str, phase, preserveCallbacks, context, buffer) => { + if (typeof str === "object" && !(str instanceof String)) { + if (!(str instanceof Promise)) str = str.toString(); + if (str instanceof Promise) str = await str; + } + const callbacks = str.callbacks; + if (!callbacks?.length) return Promise.resolve(str); + if (buffer) buffer[0] += str; + else buffer = [str]; + const resStr = Promise.all(callbacks.map((c) => c({ + phase, + buffer, + context + }))).then((res) => Promise.all(res.filter(Boolean).map((str) => resolveCallback(str, phase, false, context, buffer))).then(() => buffer[0])); + if (preserveCallbacks) return raw(await resStr, callbacks); + else return resStr; +}; +//#endregion +export { HtmlEscapedCallbackPhase, escapeToBuffer, raw, resolveCallback, resolveCallbackSync, stringBufferToString }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/utils/url.js b/tests/fixtures/npm/node_modules/hono/dist/utils/url.js new file mode 100644 index 000000000..8433d4cb9 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/utils/url.js @@ -0,0 +1,201 @@ +//#region src/utils/url.ts +const splitPath = (path) => { + const paths = path.split("/"); + if (paths[0] === "") paths.shift(); + return paths; +}; +const splitRoutingPath = (routePath) => { + const { groups, path } = extractGroupsFromPath(routePath); + const paths = splitPath(path); + return replaceGroupMarks(paths, groups); +}; +const extractGroupsFromPath = (path) => { + const groups = []; + path = path.replace(/\{[^}]+\}/g, (match, index) => { + const mark = `@${index}`; + groups.push([mark, match]); + return mark; + }); + return { + groups, + path + }; +}; +const replaceGroupMarks = (paths, groups) => { + for (let i = groups.length - 1; i >= 0; i--) { + const [mark] = groups[i]; + for (let j = paths.length - 1; j >= 0; j--) if (paths[j].includes(mark)) { + paths[j] = paths[j].replace(mark, groups[i][1]); + break; + } + } + return paths; +}; +const patternCache = {}; +const getPattern = (label, next) => { + if (label === "*") return "*"; + const match = label.match(/^\:([^\{\}]+)(?:\{(.+)\})?$/); + if (match) { + const cacheKey = `${label}#${next}`; + if (!patternCache[cacheKey]) { + if (match[2]) patternCache[cacheKey] = next && next[0] !== ":" && next[0] !== "*" ? [ + cacheKey, + match[1], + new RegExp(`^${match[2]}(?=/${next})`) + ] : [ + label, + match[1], + new RegExp(`^${match[2]}$`) + ]; + else patternCache[cacheKey] = [ + label, + match[1], + true + ]; + } + return patternCache[cacheKey]; + } + return null; +}; +const tryDecode = (str, decoder) => { + try { + return decoder(str); + } catch { + return str.replace(/(?:%[0-9A-Fa-f]{2})+/g, (match) => { + try { + return decoder(match); + } catch { + return match; + } + }); + } +}; +/** +* Try to apply decodeURI() to given string. +* If it fails, skip invalid percent encoding or invalid UTF-8 sequences, and apply decodeURI() to the rest as much as possible. +* @param str The string to decode. +* @returns The decoded string that sometimes contains undecodable percent encoding. +* @example +* tryDecodeURI('Hello%20World') // 'Hello World' +* tryDecodeURI('Hello%20World/%A4%A2') // 'Hello World/%A4%A2' +*/ +const tryDecodeURI = (str) => tryDecode(str, decodeURI); +const getPath = (request) => { + const url = request.url; + const start = url.indexOf("/", url.indexOf(":") + 4); + let i = start; + for (; i < url.length; i++) { + const charCode = url.charCodeAt(i); + if (charCode === 37) { + const queryIndex = url.indexOf("?", i); + const hashIndex = url.indexOf("#", i); + const end = queryIndex === -1 ? hashIndex === -1 ? void 0 : hashIndex : hashIndex === -1 ? queryIndex : Math.min(queryIndex, hashIndex); + const path = url.slice(start, end); + return tryDecodeURI(path.includes("%25") ? path.replace(/%25/g, "%2525") : path); + } else if (charCode === 63 || charCode === 35) break; + } + return url.slice(start, i); +}; +/** +* @deprecated +* Use the `URL` API instead. +*/ +const getQueryStrings = (url) => { + const queryIndex = url.indexOf("?", 8); + if (queryIndex === -1) return ""; + const hashIndex = url.indexOf("#", 8); + return hashIndex === -1 ? url.slice(queryIndex) : queryIndex < hashIndex ? url.slice(queryIndex, hashIndex) : ""; +}; +const getPathNoStrict = (request) => { + const result = getPath(request); + return result.length > 1 && result.at(-1) === "/" ? result.slice(0, -1) : result; +}; +/** +* Merge paths. +* @param {string[]} ...paths - The paths to merge. +* @returns {string} The merged path. +* @example +* mergePath('/api', '/users') // '/api/users' +* mergePath('/api/', '/users') // '/api/users' +* mergePath('/api', '/') // '/api' +* mergePath('/api/', '/') // '/api/' +*/ +const mergePath = (base, sub, ...rest) => { + if (rest.length) sub = mergePath(sub, ...rest); + return `${base?.[0] === "/" ? "" : "/"}${base}${sub === "/" ? "" : `${base?.at(-1) === "/" ? "" : "/"}${sub?.[0] === "/" ? sub.slice(1) : sub}`}`; +}; +const checkOptionalParameter = (path) => { + if (path.charCodeAt(path.length - 1) !== 63 || !path.includes(":")) return null; + const segments = path.split("/"); + const results = []; + let basePath = ""; + segments.forEach((segment) => { + if (segment !== "" && !/\:/.test(segment)) basePath += "/" + segment; + else if (/\:/.test(segment)) { + if (segment.charCodeAt(segment.length - 1) === 63) { + if (results.length === 0 && basePath === "") results.push("/"); + else results.push(basePath); + const optionalSegment = segment.slice(0, -1); + basePath += "/" + optionalSegment; + results.push(basePath); + } else basePath += "/" + segment; + } + }); + return results.filter((v, i, a) => a.indexOf(v) === i); +}; +const tryDecodeURIComponent = (str) => str.indexOf("%") !== -1 ? tryDecode(str, decodeURIComponent_) : str; +const _decodeURI = (value) => { + if (value.indexOf("+") !== -1) value = value.replace(/\+/g, " "); + return tryDecodeURIComponent(value); +}; +const _getQueryParam = (url, key, multiple) => { + const hashIndex = url.indexOf("#", 8); + if (hashIndex !== -1) url = url.slice(0, hashIndex); + let encoded; + if (!multiple && key && key.indexOf("%") === -1 && key.indexOf("+") === -1) { + let keyIndex = url.indexOf("?", 8); + if (keyIndex === -1) return; + if (!url.startsWith(key, keyIndex + 1)) keyIndex = url.indexOf(`&${key}`, keyIndex + 1); + while (keyIndex !== -1) { + const trailingKeyCode = url.charCodeAt(keyIndex + key.length + 1); + if (trailingKeyCode === 61) { + const valueIndex = keyIndex + key.length + 2; + const endIndex = url.indexOf("&", valueIndex); + return _decodeURI(url.slice(valueIndex, endIndex === -1 ? void 0 : endIndex)); + } else if (trailingKeyCode == 38 || isNaN(trailingKeyCode)) return ""; + keyIndex = url.indexOf(`&${key}`, keyIndex + 1); + } + encoded = /[%+]/.test(url); + if (!encoded) return; + } + const results = Object.create(null); + encoded ??= /[%+]/.test(url); + let keyIndex = url.indexOf("?", 8); + while (keyIndex !== -1) { + const nextKeyIndex = url.indexOf("&", keyIndex + 1); + let valueIndex = url.indexOf("=", keyIndex); + if (valueIndex > nextKeyIndex && nextKeyIndex !== -1) valueIndex = -1; + let name = url.slice(keyIndex + 1, valueIndex === -1 ? nextKeyIndex === -1 ? void 0 : nextKeyIndex : valueIndex); + if (encoded) name = _decodeURI(name); + keyIndex = nextKeyIndex; + if (name === "") continue; + let value; + if (valueIndex === -1) value = ""; + else { + value = url.slice(valueIndex + 1, nextKeyIndex === -1 ? void 0 : nextKeyIndex); + if (encoded) value = _decodeURI(value); + } + if (multiple) { + if (!(results[name] && Array.isArray(results[name]))) results[name] = []; + results[name].push(value); + } else results[name] ??= value; + } + return key ? results[key] : results; +}; +const getQueryParam = _getQueryParam; +const getQueryParams = (url, key) => { + return _getQueryParam(url, key, true); +}; +const decodeURIComponent_ = decodeURIComponent; +//#endregion +export { checkOptionalParameter, decodeURIComponent_, getPath, getPathNoStrict, getPattern, getQueryParam, getQueryParams, getQueryStrings, mergePath, splitPath, splitRoutingPath, tryDecode, tryDecodeURI, tryDecodeURIComponent }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/validator/index.js b/tests/fixtures/npm/node_modules/hono/dist/validator/index.js new file mode 100644 index 000000000..6e744aca6 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/validator/index.js @@ -0,0 +1,2 @@ +import { validator } from "./validator.js"; +export { validator }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/validator/validator.js b/tests/fixtures/npm/node_modules/hono/dist/validator/validator.js new file mode 100644 index 000000000..1cc282071 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/validator/validator.js @@ -0,0 +1,64 @@ +import { HTTPException } from "../http-exception.js"; +import { bufferToFormData } from "../utils/buffer.js"; +import { getCookie } from "../helper/cookie/index.js"; +//#region src/validator/validator.ts +const jsonRegex = /^application\/([a-z-\.]+\+)?json(;\s*[a-zA-Z0-9\-]+\=([^;]+))*$/i; +const multipartRegex = /^multipart\/form-data(;\s?boundary=[a-zA-Z0-9'"()+_,\-./:=?]+)?$/i; +const urlencodedRegex = /^application\/x-www-form-urlencoded(;\s*[a-zA-Z0-9\-]+\=([^;]+))*$/i; +const validator = (target, validationFunc) => { + return async (c, next) => { + let value = {}; + const contentType = c.req.header("Content-Type"); + switch (target) { + case "json": + if (!contentType || !jsonRegex.test(contentType)) break; + try { + value = await c.req.json(); + } catch { + throw new HTTPException(400, { message: "Malformed JSON in request body" }); + } + break; + case "form": { + if (!contentType || !(multipartRegex.test(contentType) || urlencodedRegex.test(contentType))) break; + let formData; + if (c.req.bodyCache.formData) formData = await c.req.bodyCache.formData; + else try { + const arrayBuffer = await c.req.arrayBuffer(); + formData = await bufferToFormData(arrayBuffer, contentType); + c.req.bodyCache.formData = formData; + } catch (e) { + let message = "Malformed FormData request."; + message += e instanceof Error ? ` ${e.message}` : ` ${String(e)}`; + throw new HTTPException(400, { message }); + } + const form = Object.create(null); + formData.forEach((value, key) => { + if (key.endsWith("[]")) (form[key] ??= []).push(value); + else if (Array.isArray(form[key])) form[key].push(value); + else if (Object.hasOwn(form, key)) form[key] = [form[key], value]; + else form[key] = value; + }); + value = form; + break; + } + case "query": + value = Object.fromEntries(Object.entries(c.req.queries()).map(([k, v]) => { + return v.length === 1 ? [k, v[0]] : [k, v]; + })); + break; + case "param": + value = c.req.param(); + break; + case "header": + value = c.req.header(); + break; + case "cookie": value = getCookie(c); + } + const res = await validationFunc(value, c); + if (res instanceof Response) return res; + c.req.addValidatedData(target, res); + return await next(); + }; +}; +//#endregion +export { validator }; diff --git a/tests/fixtures/npm/node_modules/hono/package.json b/tests/fixtures/npm/node_modules/hono/package.json index 6264c824a..6c7dec6e9 100644 --- a/tests/fixtures/npm/node_modules/hono/package.json +++ b/tests/fixtures/npm/node_modules/hono/package.json @@ -1,10 +1,18 @@ { "name": "hono", "version": "4.13.13", - "description": "Test fixture: only the hono/ws helper that @hono/node-server's index imports.", + "description": "Test fixture: the hono core and validator (with their declarations) and the hono/ws helper that @hono/node-server's index imports.", "type": "module", "license": "MIT", "exports": { + ".": { + "types": "./dist/types/index.d.ts", + "import": "./dist/index.js" + }, + "./validator": { + "types": "./dist/types/validator/index.d.ts", + "import": "./dist/validator/index.js" + }, "./ws": { "import": "./dist/helper/websocket/index.js" } diff --git a/tests/fixtures/npm/node_modules/zod/LICENSE b/tests/fixtures/npm/node_modules/zod/LICENSE new file mode 100644 index 000000000..c06579627 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Colin McDonnell + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/tests/fixtures/npm/node_modules/zod/index.d.cts b/tests/fixtures/npm/node_modules/zod/index.d.cts new file mode 100644 index 000000000..b8f3021a8 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/index.d.cts @@ -0,0 +1,3 @@ +import * as z from "./v4/classic/external.cjs"; +export * from "./v4/classic/external.cjs"; +export { z, z as default }; diff --git a/tests/fixtures/npm/node_modules/zod/index.js b/tests/fixtures/npm/node_modules/zod/index.js new file mode 100644 index 000000000..4102a4715 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/index.js @@ -0,0 +1,7 @@ +import * as z from "./v4/classic/external.js"; +export * from "./v4/classic/external.js"; +// Aliasing the binding, instead of `export default z`, keeps the default export a +// re-export of `z` rather than a fresh namespace value. Rollup and Webpack can then +// shake `import z from "zod"` as they already shake `import { z } from "zod"` — without +// this they pull in every locale. See #6050. +export { z, z as default }; diff --git a/tests/fixtures/npm/node_modules/zod/package.json b/tests/fixtures/npm/node_modules/zod/package.json new file mode 100644 index 000000000..58b9916bd --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/package.json @@ -0,0 +1,23 @@ +{ + "name": "zod", + "version": "4.6.7", + "description": "Test fixture: zod 4's classic API, its core and locales, and the declarations @hono/zod-validator reads (the files the npm cases load).", + "type": "module", + "license": "MIT", + "types": "./index.d.cts", + "exports": { + ".": { + "types": "./index.d.cts", + "import": "./index.js" + }, + "./v3": { + "types": "./v3/index.d.cts" + }, + "./v4": { + "types": "./v4/index.d.cts" + }, + "./v4/core": { + "types": "./v4/core/index.d.cts" + } + } +} diff --git a/tests/fixtures/npm/node_modules/zod/v3/ZodError.d.cts b/tests/fixtures/npm/node_modules/zod/v3/ZodError.d.cts new file mode 100644 index 000000000..384b36487 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v3/ZodError.d.cts @@ -0,0 +1,164 @@ +import type { Primitive } from "./helpers/typeAliases.cjs"; +import { util, type ZodParsedType } from "./helpers/util.cjs"; +import type { TypeOf, ZodType } from "./index.cjs"; +type allKeys = T extends any ? keyof T : never; +export type inferFlattenedErrors, U = string> = typeToFlattenedError, U>; +export type typeToFlattenedError = { + formErrors: U[]; + fieldErrors: { + [P in allKeys]?: U[]; + }; +}; +export declare const ZodIssueCode: { + invalid_type: "invalid_type"; + too_big: "too_big"; + too_small: "too_small"; + not_multiple_of: "not_multiple_of"; + unrecognized_keys: "unrecognized_keys"; + invalid_union: "invalid_union"; + custom: "custom"; + invalid_literal: "invalid_literal"; + invalid_union_discriminator: "invalid_union_discriminator"; + invalid_enum_value: "invalid_enum_value"; + invalid_arguments: "invalid_arguments"; + invalid_return_type: "invalid_return_type"; + invalid_date: "invalid_date"; + invalid_string: "invalid_string"; + invalid_intersection_types: "invalid_intersection_types"; + not_finite: "not_finite"; +}; +export type ZodIssueCode = keyof typeof ZodIssueCode; +export type ZodIssueBase = { + path: (string | number)[]; + message?: string | undefined; +}; +export interface ZodInvalidTypeIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_type; + expected: ZodParsedType; + received: ZodParsedType; +} +export interface ZodInvalidLiteralIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_literal; + expected: unknown; + received: unknown; +} +export interface ZodUnrecognizedKeysIssue extends ZodIssueBase { + code: typeof ZodIssueCode.unrecognized_keys; + keys: string[]; +} +export interface ZodInvalidUnionIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_union; + unionErrors: ZodError[]; +} +export interface ZodInvalidUnionDiscriminatorIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_union_discriminator; + options: Primitive[]; +} +export interface ZodInvalidEnumValueIssue extends ZodIssueBase { + received: string | number; + code: typeof ZodIssueCode.invalid_enum_value; + options: (string | number)[]; +} +export interface ZodInvalidArgumentsIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_arguments; + argumentsError: ZodError; +} +export interface ZodInvalidReturnTypeIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_return_type; + returnTypeError: ZodError; +} +export interface ZodInvalidDateIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_date; +} +export type StringValidation = "email" | "url" | "emoji" | "uuid" | "nanoid" | "regex" | "cuid" | "cuid2" | "ulid" | "datetime" | "date" | "time" | "duration" | "ip" | "cidr" | "base64" | "jwt" | "base64url" | { + includes: string; + position?: number | undefined; +} | { + startsWith: string; +} | { + endsWith: string; +}; +export interface ZodInvalidStringIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_string; + validation: StringValidation; +} +export interface ZodTooSmallIssue extends ZodIssueBase { + code: typeof ZodIssueCode.too_small; + minimum: number | bigint; + inclusive: boolean; + exact?: boolean; + type: "array" | "string" | "number" | "set" | "date" | "bigint"; +} +export interface ZodTooBigIssue extends ZodIssueBase { + code: typeof ZodIssueCode.too_big; + maximum: number | bigint; + inclusive: boolean; + exact?: boolean; + type: "array" | "string" | "number" | "set" | "date" | "bigint"; +} +export interface ZodInvalidIntersectionTypesIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_intersection_types; +} +export interface ZodNotMultipleOfIssue extends ZodIssueBase { + code: typeof ZodIssueCode.not_multiple_of; + multipleOf: number | bigint; +} +export interface ZodNotFiniteIssue extends ZodIssueBase { + code: typeof ZodIssueCode.not_finite; +} +export interface ZodCustomIssue extends ZodIssueBase { + code: typeof ZodIssueCode.custom; + params?: { + [k: string]: any; + }; +} +export type DenormalizedError = { + [k: string]: DenormalizedError | string[]; +}; +export type ZodIssueOptionalMessage = ZodInvalidTypeIssue | ZodInvalidLiteralIssue | ZodUnrecognizedKeysIssue | ZodInvalidUnionIssue | ZodInvalidUnionDiscriminatorIssue | ZodInvalidEnumValueIssue | ZodInvalidArgumentsIssue | ZodInvalidReturnTypeIssue | ZodInvalidDateIssue | ZodInvalidStringIssue | ZodTooSmallIssue | ZodTooBigIssue | ZodInvalidIntersectionTypesIssue | ZodNotMultipleOfIssue | ZodNotFiniteIssue | ZodCustomIssue; +export type ZodIssue = ZodIssueOptionalMessage & { + fatal?: boolean | undefined; + message: string; +}; +export declare const quotelessJson: (obj: any) => string; +type recursiveZodFormattedError = T extends [any, ...any[]] ? { + [K in keyof T]?: ZodFormattedError; +} : T extends any[] ? { + [k: number]: ZodFormattedError; +} : T extends object ? { + [K in keyof T]?: ZodFormattedError; +} : unknown; +export type ZodFormattedError = { + _errors: U[]; +} & recursiveZodFormattedError>; +export type inferFormattedError, U = string> = ZodFormattedError, U>; +export declare class ZodError extends Error { + issues: ZodIssue[]; + get errors(): ZodIssue[]; + constructor(issues: ZodIssue[]); + format(): ZodFormattedError; + format(mapper: (issue: ZodIssue) => U): ZodFormattedError; + static create: (issues: ZodIssue[]) => ZodError; + static assert(value: unknown): asserts value is ZodError; + toString(): string; + get message(): string; + get isEmpty(): boolean; + addIssue: (sub: ZodIssue) => void; + addIssues: (subs?: ZodIssue[]) => void; + flatten(): typeToFlattenedError; + flatten(mapper?: (issue: ZodIssue) => U): typeToFlattenedError; + get formErrors(): typeToFlattenedError; +} +type stripPath = T extends any ? util.OmitKeys : never; +export type IssueData = stripPath & { + path?: (string | number)[]; + fatal?: boolean | undefined; +}; +export type ErrorMapCtx = { + defaultError: string; + data: any; +}; +export type ZodErrorMap = (issue: ZodIssueOptionalMessage, _ctx: ErrorMapCtx) => { + message: string; +}; +export {}; diff --git a/tests/fixtures/npm/node_modules/zod/v3/errors.d.cts b/tests/fixtures/npm/node_modules/zod/v3/errors.d.cts new file mode 100644 index 000000000..205f3a096 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v3/errors.d.cts @@ -0,0 +1,5 @@ +import type { ZodErrorMap } from "./ZodError.cjs"; +import defaultErrorMap from "./locales/en.cjs"; +export { defaultErrorMap }; +export declare function setErrorMap(map: ZodErrorMap): void; +export declare function getErrorMap(): ZodErrorMap; diff --git a/tests/fixtures/npm/node_modules/zod/v3/external.d.cts b/tests/fixtures/npm/node_modules/zod/v3/external.d.cts new file mode 100644 index 000000000..ced9c1a85 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v3/external.d.cts @@ -0,0 +1,6 @@ +export * from "./errors.cjs"; +export * from "./helpers/parseUtil.cjs"; +export * from "./helpers/typeAliases.cjs"; +export * from "./helpers/util.cjs"; +export * from "./types.cjs"; +export * from "./ZodError.cjs"; diff --git a/tests/fixtures/npm/node_modules/zod/v3/helpers/enumUtil.d.cts b/tests/fixtures/npm/node_modules/zod/v3/helpers/enumUtil.d.cts new file mode 100644 index 000000000..fad28a711 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v3/helpers/enumUtil.d.cts @@ -0,0 +1,8 @@ +export declare namespace enumUtil { + type UnionToIntersectionFn = (T extends unknown ? (k: () => T) => void : never) extends (k: infer Intersection) => void ? Intersection : never; + type GetUnionLast = UnionToIntersectionFn extends () => infer Last ? Last : never; + type UnionToTuple = [T] extends [never] ? Tuple : UnionToTuple>, [GetUnionLast, ...Tuple]>; + type CastToStringTuple = T extends [string, ...string[]] ? T : never; + export type UnionToTupleString = CastToStringTuple>; + export {}; +} diff --git a/tests/fixtures/npm/node_modules/zod/v3/helpers/errorUtil.d.cts b/tests/fixtures/npm/node_modules/zod/v3/helpers/errorUtil.d.cts new file mode 100644 index 000000000..bc9b033ff --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v3/helpers/errorUtil.d.cts @@ -0,0 +1,9 @@ +export declare namespace errorUtil { + type ErrMessage = string | { + message?: string | undefined; + }; + const errToObj: (message?: ErrMessage) => { + message?: string | undefined; + }; + const toString: (message?: ErrMessage) => string | undefined; +} diff --git a/tests/fixtures/npm/node_modules/zod/v3/helpers/parseUtil.d.cts b/tests/fixtures/npm/node_modules/zod/v3/helpers/parseUtil.d.cts new file mode 100644 index 000000000..f34a63d7f --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v3/helpers/parseUtil.d.cts @@ -0,0 +1,78 @@ +import type { IssueData, ZodErrorMap, ZodIssue } from "../ZodError.cjs"; +import type { ZodParsedType } from "./util.cjs"; +export declare const makeIssue: (params: { + data: any; + path: (string | number)[]; + errorMaps: ZodErrorMap[]; + issueData: IssueData; +}) => ZodIssue; +export type ParseParams = { + path: (string | number)[]; + errorMap: ZodErrorMap; + async: boolean; +}; +export type ParsePathComponent = string | number; +export type ParsePath = ParsePathComponent[]; +export declare const EMPTY_PATH: ParsePath; +export interface ParseContext { + readonly common: { + readonly issues: ZodIssue[]; + readonly contextualErrorMap?: ZodErrorMap | undefined; + readonly async: boolean; + }; + readonly path: ParsePath; + readonly schemaErrorMap?: ZodErrorMap | undefined; + readonly parent: ParseContext | null; + readonly data: any; + readonly parsedType: ZodParsedType; +} +export type ParseInput = { + data: any; + path: (string | number)[]; + parent: ParseContext; +}; +export declare function addIssueToContext(ctx: ParseContext, issueData: IssueData): void; +export type ObjectPair = { + key: SyncParseReturnType; + value: SyncParseReturnType; +}; +export declare class ParseStatus { + value: "aborted" | "dirty" | "valid"; + dirty(): void; + abort(): void; + static mergeArray(status: ParseStatus, results: SyncParseReturnType[]): SyncParseReturnType; + static mergeObjectAsync(status: ParseStatus, pairs: { + key: ParseReturnType; + value: ParseReturnType; + }[]): Promise>; + static mergeObjectSync(status: ParseStatus, pairs: { + key: SyncParseReturnType; + value: SyncParseReturnType; + alwaysSet?: boolean; + }[]): SyncParseReturnType; +} +export interface ParseResult { + status: "aborted" | "dirty" | "valid"; + data: any; +} +export type INVALID = { + status: "aborted"; +}; +export declare const INVALID: INVALID; +export type DIRTY = { + status: "dirty"; + value: T; +}; +export declare const DIRTY: (value: T) => DIRTY; +export type OK = { + status: "valid"; + value: T; +}; +export declare const OK: (value: T) => OK; +export type SyncParseReturnType = OK | DIRTY | INVALID; +export type AsyncParseReturnType = Promise>; +export type ParseReturnType = SyncParseReturnType | AsyncParseReturnType; +export declare const isAborted: (x: ParseReturnType) => x is INVALID; +export declare const isDirty: (x: ParseReturnType) => x is OK | DIRTY; +export declare const isValid: (x: ParseReturnType) => x is OK; +export declare const isAsync: (x: ParseReturnType) => x is AsyncParseReturnType; diff --git a/tests/fixtures/npm/node_modules/zod/v3/helpers/partialUtil.d.cts b/tests/fixtures/npm/node_modules/zod/v3/helpers/partialUtil.d.cts new file mode 100644 index 000000000..3f9e46a2a --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v3/helpers/partialUtil.d.cts @@ -0,0 +1,8 @@ +import type { ZodArray, ZodNullable, ZodObject, ZodOptional, ZodRawShape, ZodTuple, ZodTupleItems, ZodTypeAny } from "../types.cjs"; +export declare namespace partialUtil { + type DeepPartial = T extends ZodObject ? ZodObject<{ + [k in keyof T["shape"]]: ZodOptional>; + }, T["_def"]["unknownKeys"], T["_def"]["catchall"]> : T extends ZodArray ? ZodArray, Card> : T extends ZodOptional ? ZodOptional> : T extends ZodNullable ? ZodNullable> : T extends ZodTuple ? { + [k in keyof Items]: Items[k] extends ZodTypeAny ? DeepPartial : never; + } extends infer PI ? PI extends ZodTupleItems ? ZodTuple : never : never : T; +} diff --git a/tests/fixtures/npm/node_modules/zod/v3/helpers/typeAliases.d.cts b/tests/fixtures/npm/node_modules/zod/v3/helpers/typeAliases.d.cts new file mode 100644 index 000000000..32df022ca --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v3/helpers/typeAliases.d.cts @@ -0,0 +1,2 @@ +export type Primitive = string | number | symbol | bigint | boolean | null | undefined; +export type Scalars = Primitive | Primitive[]; diff --git a/tests/fixtures/npm/node_modules/zod/v3/helpers/util.d.cts b/tests/fixtures/npm/node_modules/zod/v3/helpers/util.d.cts new file mode 100644 index 000000000..eeaaeee1c --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v3/helpers/util.d.cts @@ -0,0 +1,85 @@ +export declare namespace util { + type AssertEqual = (() => V extends T ? 1 : 2) extends () => V extends U ? 1 : 2 ? true : false; + export type isAny = 0 extends 1 & T ? true : false; + export const assertEqual: (_: AssertEqual) => void; + export function assertIs(_arg: T): void; + export function assertNever(_x: never): never; + export type Omit = Pick>; + export type OmitKeys = Pick>; + export type MakePartial = Omit & Partial>; + export type Exactly = T & Record, never>; + export type InexactPartial = { + [k in keyof T]?: T[k] | undefined; + }; + export const arrayToEnum: (items: U) => { [k in U[number]]: k; }; + export const getValidEnumValues: (obj: any) => any[]; + export const objectValues: (obj: any) => any[]; + export const objectKeys: ObjectConstructor["keys"]; + export const find: (arr: T[], checker: (arg: T) => any) => T | undefined; + export type identity = objectUtil.identity; + export type flatten = objectUtil.flatten; + export type noUndefined = T extends undefined ? never : T; + export const isInteger: NumberConstructor["isInteger"]; + export function joinValues(array: T, separator?: string): string; + export const jsonStringifyReplacer: (_: string, value: any) => any; + export {}; +} +export declare namespace objectUtil { + export type MergeShapes = keyof U & keyof V extends never ? U & V : { + [k in Exclude]: U[k]; + } & V; + type optionalKeys = { + [k in keyof T]: undefined extends T[k] ? k : never; + }[keyof T]; + type requiredKeys = { + [k in keyof T]: undefined extends T[k] ? never : k; + }[keyof T]; + export type addQuestionMarks = { + [K in requiredKeys]: T[K]; + } & { + [K in optionalKeys]?: T[K]; + } & { + [k in keyof T]?: unknown; + }; + export type identity = T; + export type flatten = identity<{ + [k in keyof T]: T[k]; + }>; + export type noNeverKeys = { + [k in keyof T]: [T[k]] extends [never] ? never : k; + }[keyof T]; + export type noNever = identity<{ + [k in noNeverKeys]: k extends keyof T ? T[k] : never; + }>; + export const mergeShapes: (first: U, second: T) => T & U; + export type extendShape = keyof A & keyof B extends never ? A & B : { + [K in keyof A as K extends keyof B ? never : K]: A[K]; + } & { + [K in keyof B]: B[K]; + }; + export {}; +} +export declare const ZodParsedType: { + string: "string"; + nan: "nan"; + number: "number"; + integer: "integer"; + float: "float"; + boolean: "boolean"; + date: "date"; + bigint: "bigint"; + symbol: "symbol"; + function: "function"; + undefined: "undefined"; + null: "null"; + array: "array"; + object: "object"; + unknown: "unknown"; + promise: "promise"; + void: "void"; + never: "never"; + map: "map"; + set: "set"; +}; +export type ZodParsedType = keyof typeof ZodParsedType; +export declare const getParsedType: (data: any) => ZodParsedType; diff --git a/tests/fixtures/npm/node_modules/zod/v3/index.d.cts b/tests/fixtures/npm/node_modules/zod/v3/index.d.cts new file mode 100644 index 000000000..b0662babe --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v3/index.d.cts @@ -0,0 +1,4 @@ +import * as z from "./external.cjs"; +export * from "./external.cjs"; +export { z }; +export default z; diff --git a/tests/fixtures/npm/node_modules/zod/v3/locales/en.d.cts b/tests/fixtures/npm/node_modules/zod/v3/locales/en.d.cts new file mode 100644 index 000000000..e146a11c5 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v3/locales/en.d.cts @@ -0,0 +1,3 @@ +import { type ZodErrorMap } from "../ZodError.cjs"; +declare const errorMap: ZodErrorMap; +export = errorMap; diff --git a/tests/fixtures/npm/node_modules/zod/v3/standard-schema.d.cts b/tests/fixtures/npm/node_modules/zod/v3/standard-schema.d.cts new file mode 100644 index 000000000..7ddff3b7f --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v3/standard-schema.d.cts @@ -0,0 +1,102 @@ +/** + * The Standard Schema interface. + */ +export type StandardSchemaV1 = { + /** + * The Standard Schema properties. + */ + readonly "~standard": StandardSchemaV1.Props; +}; +export declare namespace StandardSchemaV1 { + /** + * The Standard Schema properties interface. + */ + export interface Props { + /** + * The version number of the standard. + */ + readonly version: 1; + /** + * The vendor name of the schema library. + */ + readonly vendor: string; + /** + * Validates unknown input values. + */ + readonly validate: (value: unknown) => Result | Promise>; + /** + * Inferred types associated with the schema. + */ + readonly types?: Types | undefined; + } + /** + * The result interface of the validate function. + */ + export type Result = SuccessResult | FailureResult; + /** + * The result interface if validation succeeds. + */ + export interface SuccessResult { + /** + * The typed output value. + */ + readonly value: Output; + /** + * The non-existent issues. + */ + readonly issues?: undefined; + } + /** + * The result interface if validation fails. + */ + export interface FailureResult { + /** + * The issues of failed validation. + */ + readonly issues: ReadonlyArray; + } + /** + * The issue interface of the failure output. + */ + export interface Issue { + /** + * The error message of the issue. + */ + readonly message: string; + /** + * The path of the issue, if any. + */ + readonly path?: ReadonlyArray | undefined; + } + /** + * The path segment interface of the issue. + */ + export interface PathSegment { + /** + * The key representing a path segment. + */ + readonly key: PropertyKey; + } + /** + * The Standard Schema types interface. + */ + export interface Types { + /** + * The input type of the schema. + */ + readonly input: Input; + /** + * The output type of the schema. + */ + readonly output: Output; + } + /** + * Infers the input type of a Standard Schema. + */ + export type InferInput = NonNullable["input"]; + /** + * Infers the output type of a Standard Schema. + */ + export type InferOutput = NonNullable["output"]; + export {}; +} diff --git a/tests/fixtures/npm/node_modules/zod/v3/types.d.cts b/tests/fixtures/npm/node_modules/zod/v3/types.d.cts new file mode 100644 index 000000000..f1013a74f --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v3/types.d.cts @@ -0,0 +1,1034 @@ +import { type IssueData, type StringValidation, type ZodCustomIssue, ZodError, type ZodErrorMap } from "./ZodError.cjs"; +import type { enumUtil } from "./helpers/enumUtil.cjs"; +import { errorUtil } from "./helpers/errorUtil.cjs"; +import { type AsyncParseReturnType, INVALID, type ParseContext, type ParseInput, type ParseParams, type ParseReturnType, ParseStatus, type SyncParseReturnType } from "./helpers/parseUtil.cjs"; +import type { partialUtil } from "./helpers/partialUtil.cjs"; +import type { Primitive } from "./helpers/typeAliases.cjs"; +import { util, type objectUtil } from "./helpers/util.cjs"; +import type { StandardSchemaV1 } from "./standard-schema.cjs"; +export interface RefinementCtx { + addIssue: (arg: IssueData) => void; + path: (string | number)[]; +} +export type ZodRawShape = { + [k: string]: ZodTypeAny; +}; +export type ZodTypeAny = ZodType; +export type TypeOf> = T["_output"]; +export type input> = T["_input"]; +export type output> = T["_output"]; +export type { TypeOf as infer }; +export type CustomErrorParams = Partial>; +export interface ZodTypeDef { + errorMap?: ZodErrorMap | undefined; + description?: string | undefined; +} +export type RawCreateParams = { + errorMap?: ZodErrorMap | undefined; + invalid_type_error?: string | undefined; + required_error?: string | undefined; + message?: string | undefined; + description?: string | undefined; +} | undefined; +export type ProcessedCreateParams = { + errorMap?: ZodErrorMap | undefined; + description?: string | undefined; +}; +export type SafeParseSuccess = { + success: true; + data: Output; + error?: never; +}; +export type SafeParseError = { + success: false; + error: ZodError; + data?: never; +}; +export type SafeParseReturnType = SafeParseSuccess | SafeParseError; +export declare abstract class ZodType { + readonly _type: Output; + readonly _output: Output; + readonly _input: Input; + readonly _def: Def; + get description(): string | undefined; + "~standard": StandardSchemaV1.Props; + abstract _parse(input: ParseInput): ParseReturnType; + _getType(input: ParseInput): string; + _getOrReturnCtx(input: ParseInput, ctx?: ParseContext | undefined): ParseContext; + _processInputParams(input: ParseInput): { + status: ParseStatus; + ctx: ParseContext; + }; + _parseSync(input: ParseInput): SyncParseReturnType; + _parseAsync(input: ParseInput): AsyncParseReturnType; + parse(data: unknown, params?: util.InexactPartial): Output; + safeParse(data: unknown, params?: util.InexactPartial): SafeParseReturnType; + "~validate"(data: unknown): StandardSchemaV1.Result | Promise>; + parseAsync(data: unknown, params?: util.InexactPartial): Promise; + safeParseAsync(data: unknown, params?: util.InexactPartial): Promise>; + /** Alias of safeParseAsync */ + spa: (data: unknown, params?: util.InexactPartial) => Promise>; + refine(check: (arg: Output) => arg is RefinedOutput, message?: string | CustomErrorParams | ((arg: Output) => CustomErrorParams)): ZodEffects; + refine(check: (arg: Output) => unknown | Promise, message?: string | CustomErrorParams | ((arg: Output) => CustomErrorParams)): ZodEffects; + refinement(check: (arg: Output) => arg is RefinedOutput, refinementData: IssueData | ((arg: Output, ctx: RefinementCtx) => IssueData)): ZodEffects; + refinement(check: (arg: Output) => boolean, refinementData: IssueData | ((arg: Output, ctx: RefinementCtx) => IssueData)): ZodEffects; + _refinement(refinement: RefinementEffect["refinement"]): ZodEffects; + superRefine(refinement: (arg: Output, ctx: RefinementCtx) => arg is RefinedOutput): ZodEffects; + superRefine(refinement: (arg: Output, ctx: RefinementCtx) => void): ZodEffects; + superRefine(refinement: (arg: Output, ctx: RefinementCtx) => Promise): ZodEffects; + constructor(def: Def); + optional(): ZodOptional; + nullable(): ZodNullable; + nullish(): ZodOptional>; + array(): ZodArray; + promise(): ZodPromise; + or(option: T): ZodUnion<[this, T]>; + and(incoming: T): ZodIntersection; + transform(transform: (arg: Output, ctx: RefinementCtx) => NewOut | Promise): ZodEffects; + default(def: util.noUndefined): ZodDefault; + default(def: () => util.noUndefined): ZodDefault; + brand(brand?: B): ZodBranded; + catch(def: Output): ZodCatch; + catch(def: (ctx: { + error: ZodError; + input: Input; + }) => Output): ZodCatch; + describe(description: string): this; + pipe(target: T): ZodPipeline; + readonly(): ZodReadonly; + isOptional(): boolean; + isNullable(): boolean; +} +export type IpVersion = "v4" | "v6"; +export type ZodStringCheck = { + kind: "min"; + value: number; + message?: string | undefined; +} | { + kind: "max"; + value: number; + message?: string | undefined; +} | { + kind: "length"; + value: number; + message?: string | undefined; +} | { + kind: "email"; + message?: string | undefined; +} | { + kind: "url"; + message?: string | undefined; +} | { + kind: "emoji"; + message?: string | undefined; +} | { + kind: "uuid"; + message?: string | undefined; +} | { + kind: "nanoid"; + message?: string | undefined; +} | { + kind: "cuid"; + message?: string | undefined; +} | { + kind: "includes"; + value: string; + position?: number | undefined; + message?: string | undefined; +} | { + kind: "cuid2"; + message?: string | undefined; +} | { + kind: "ulid"; + message?: string | undefined; +} | { + kind: "startsWith"; + value: string; + message?: string | undefined; +} | { + kind: "endsWith"; + value: string; + message?: string | undefined; +} | { + kind: "regex"; + regex: RegExp; + message?: string | undefined; +} | { + kind: "trim"; + message?: string | undefined; +} | { + kind: "toLowerCase"; + message?: string | undefined; +} | { + kind: "toUpperCase"; + message?: string | undefined; +} | { + kind: "jwt"; + alg?: string; + message?: string | undefined; +} | { + kind: "datetime"; + offset: boolean; + local: boolean; + precision: number | null; + message?: string | undefined; +} | { + kind: "date"; + message?: string | undefined; +} | { + kind: "time"; + precision: number | null; + message?: string | undefined; +} | { + kind: "duration"; + message?: string | undefined; +} | { + kind: "ip"; + version?: IpVersion | undefined; + message?: string | undefined; +} | { + kind: "cidr"; + version?: IpVersion | undefined; + message?: string | undefined; +} | { + kind: "base64"; + message?: string | undefined; +} | { + kind: "base64url"; + message?: string | undefined; +}; +export interface ZodStringDef extends ZodTypeDef { + checks: ZodStringCheck[]; + typeName: ZodFirstPartyTypeKind.ZodString; + coerce: boolean; +} +export declare function datetimeRegex(args: { + precision?: number | null; + offset?: boolean; + local?: boolean; +}): RegExp; +export declare class ZodString extends ZodType { + _parse(input: ParseInput): ParseReturnType; + protected _regex(regex: RegExp, validation: StringValidation, message?: errorUtil.ErrMessage): ZodEffects; + _addCheck(check: ZodStringCheck): ZodString; + email(message?: errorUtil.ErrMessage): ZodString; + url(message?: errorUtil.ErrMessage): ZodString; + emoji(message?: errorUtil.ErrMessage): ZodString; + uuid(message?: errorUtil.ErrMessage): ZodString; + nanoid(message?: errorUtil.ErrMessage): ZodString; + cuid(message?: errorUtil.ErrMessage): ZodString; + cuid2(message?: errorUtil.ErrMessage): ZodString; + ulid(message?: errorUtil.ErrMessage): ZodString; + base64(message?: errorUtil.ErrMessage): ZodString; + base64url(message?: errorUtil.ErrMessage): ZodString; + jwt(options?: { + alg?: string; + message?: string | undefined; + }): ZodString; + ip(options?: string | { + version?: IpVersion; + message?: string | undefined; + }): ZodString; + cidr(options?: string | { + version?: IpVersion; + message?: string | undefined; + }): ZodString; + datetime(options?: string | { + message?: string | undefined; + precision?: number | null; + offset?: boolean; + local?: boolean; + }): ZodString; + date(message?: string): ZodString; + time(options?: string | { + message?: string | undefined; + precision?: number | null; + }): ZodString; + duration(message?: errorUtil.ErrMessage): ZodString; + regex(regex: RegExp, message?: errorUtil.ErrMessage): ZodString; + includes(value: string, options?: { + message?: string; + position?: number; + }): ZodString; + startsWith(value: string, message?: errorUtil.ErrMessage): ZodString; + endsWith(value: string, message?: errorUtil.ErrMessage): ZodString; + min(minLength: number, message?: errorUtil.ErrMessage): ZodString; + max(maxLength: number, message?: errorUtil.ErrMessage): ZodString; + length(len: number, message?: errorUtil.ErrMessage): ZodString; + /** + * Equivalent to `.min(1)` + */ + nonempty(message?: errorUtil.ErrMessage): ZodString; + trim(): ZodString; + toLowerCase(): ZodString; + toUpperCase(): ZodString; + get isDatetime(): boolean; + get isDate(): boolean; + get isTime(): boolean; + get isDuration(): boolean; + get isEmail(): boolean; + get isURL(): boolean; + get isEmoji(): boolean; + get isUUID(): boolean; + get isNANOID(): boolean; + get isCUID(): boolean; + get isCUID2(): boolean; + get isULID(): boolean; + get isIP(): boolean; + get isCIDR(): boolean; + get isBase64(): boolean; + get isBase64url(): boolean; + get minLength(): number | null; + get maxLength(): number | null; + static create: (params?: RawCreateParams & { + coerce?: true; + }) => ZodString; +} +export type ZodNumberCheck = { + kind: "min"; + value: number; + inclusive: boolean; + message?: string | undefined; +} | { + kind: "max"; + value: number; + inclusive: boolean; + message?: string | undefined; +} | { + kind: "int"; + message?: string | undefined; +} | { + kind: "multipleOf"; + value: number; + message?: string | undefined; +} | { + kind: "finite"; + message?: string | undefined; +}; +export interface ZodNumberDef extends ZodTypeDef { + checks: ZodNumberCheck[]; + typeName: ZodFirstPartyTypeKind.ZodNumber; + coerce: boolean; +} +export declare class ZodNumber extends ZodType { + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams & { + coerce?: boolean; + }) => ZodNumber; + gte(value: number, message?: errorUtil.ErrMessage): ZodNumber; + min: (value: number, message?: errorUtil.ErrMessage) => ZodNumber; + gt(value: number, message?: errorUtil.ErrMessage): ZodNumber; + lte(value: number, message?: errorUtil.ErrMessage): ZodNumber; + max: (value: number, message?: errorUtil.ErrMessage) => ZodNumber; + lt(value: number, message?: errorUtil.ErrMessage): ZodNumber; + protected setLimit(kind: "min" | "max", value: number, inclusive: boolean, message?: string): ZodNumber; + _addCheck(check: ZodNumberCheck): ZodNumber; + int(message?: errorUtil.ErrMessage): ZodNumber; + positive(message?: errorUtil.ErrMessage): ZodNumber; + negative(message?: errorUtil.ErrMessage): ZodNumber; + nonpositive(message?: errorUtil.ErrMessage): ZodNumber; + nonnegative(message?: errorUtil.ErrMessage): ZodNumber; + multipleOf(value: number, message?: errorUtil.ErrMessage): ZodNumber; + step: (value: number, message?: errorUtil.ErrMessage) => ZodNumber; + finite(message?: errorUtil.ErrMessage): ZodNumber; + safe(message?: errorUtil.ErrMessage): ZodNumber; + get minValue(): number | null; + get maxValue(): number | null; + get isInt(): boolean; + get isFinite(): boolean; +} +export type ZodBigIntCheck = { + kind: "min"; + value: bigint; + inclusive: boolean; + message?: string | undefined; +} | { + kind: "max"; + value: bigint; + inclusive: boolean; + message?: string | undefined; +} | { + kind: "multipleOf"; + value: bigint; + message?: string | undefined; +}; +export interface ZodBigIntDef extends ZodTypeDef { + checks: ZodBigIntCheck[]; + typeName: ZodFirstPartyTypeKind.ZodBigInt; + coerce: boolean; +} +export declare class ZodBigInt extends ZodType { + _parse(input: ParseInput): ParseReturnType; + _getInvalidInput(input: ParseInput): INVALID; + static create: (params?: RawCreateParams & { + coerce?: boolean; + }) => ZodBigInt; + gte(value: bigint, message?: errorUtil.ErrMessage): ZodBigInt; + min: (value: bigint, message?: errorUtil.ErrMessage) => ZodBigInt; + gt(value: bigint, message?: errorUtil.ErrMessage): ZodBigInt; + lte(value: bigint, message?: errorUtil.ErrMessage): ZodBigInt; + max: (value: bigint, message?: errorUtil.ErrMessage) => ZodBigInt; + lt(value: bigint, message?: errorUtil.ErrMessage): ZodBigInt; + protected setLimit(kind: "min" | "max", value: bigint, inclusive: boolean, message?: string): ZodBigInt; + _addCheck(check: ZodBigIntCheck): ZodBigInt; + positive(message?: errorUtil.ErrMessage): ZodBigInt; + negative(message?: errorUtil.ErrMessage): ZodBigInt; + nonpositive(message?: errorUtil.ErrMessage): ZodBigInt; + nonnegative(message?: errorUtil.ErrMessage): ZodBigInt; + multipleOf(value: bigint, message?: errorUtil.ErrMessage): ZodBigInt; + get minValue(): bigint | null; + get maxValue(): bigint | null; +} +export interface ZodBooleanDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodBoolean; + coerce: boolean; +} +export declare class ZodBoolean extends ZodType { + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams & { + coerce?: boolean; + }) => ZodBoolean; +} +export type ZodDateCheck = { + kind: "min"; + value: number; + message?: string | undefined; +} | { + kind: "max"; + value: number; + message?: string | undefined; +}; +export interface ZodDateDef extends ZodTypeDef { + checks: ZodDateCheck[]; + coerce: boolean; + typeName: ZodFirstPartyTypeKind.ZodDate; +} +export declare class ZodDate extends ZodType { + _parse(input: ParseInput): ParseReturnType; + _addCheck(check: ZodDateCheck): ZodDate; + min(minDate: Date, message?: errorUtil.ErrMessage): ZodDate; + max(maxDate: Date, message?: errorUtil.ErrMessage): ZodDate; + get minDate(): Date | null; + get maxDate(): Date | null; + static create: (params?: RawCreateParams & { + coerce?: boolean; + }) => ZodDate; +} +export interface ZodSymbolDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodSymbol; +} +export declare class ZodSymbol extends ZodType { + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams) => ZodSymbol; +} +export interface ZodUndefinedDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodUndefined; +} +export declare class ZodUndefined extends ZodType { + _parse(input: ParseInput): ParseReturnType; + params?: RawCreateParams; + static create: (params?: RawCreateParams) => ZodUndefined; +} +export interface ZodNullDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodNull; +} +export declare class ZodNull extends ZodType { + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams) => ZodNull; +} +export interface ZodAnyDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodAny; +} +export declare class ZodAny extends ZodType { + _any: true; + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams) => ZodAny; +} +export interface ZodUnknownDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodUnknown; +} +export declare class ZodUnknown extends ZodType { + _unknown: true; + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams) => ZodUnknown; +} +export interface ZodNeverDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodNever; +} +export declare class ZodNever extends ZodType { + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams) => ZodNever; +} +export interface ZodVoidDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodVoid; +} +export declare class ZodVoid extends ZodType { + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams) => ZodVoid; +} +export interface ZodArrayDef extends ZodTypeDef { + type: T; + typeName: ZodFirstPartyTypeKind.ZodArray; + exactLength: { + value: number; + message?: string | undefined; + } | null; + minLength: { + value: number; + message?: string | undefined; + } | null; + maxLength: { + value: number; + message?: string | undefined; + } | null; +} +export type ArrayCardinality = "many" | "atleastone"; +export type arrayOutputType = Cardinality extends "atleastone" ? [T["_output"], ...T["_output"][]] : T["_output"][]; +export declare class ZodArray extends ZodType, ZodArrayDef, Cardinality extends "atleastone" ? [T["_input"], ...T["_input"][]] : T["_input"][]> { + _parse(input: ParseInput): ParseReturnType; + get element(): T; + min(minLength: number, message?: errorUtil.ErrMessage): this; + max(maxLength: number, message?: errorUtil.ErrMessage): this; + length(len: number, message?: errorUtil.ErrMessage): this; + nonempty(message?: errorUtil.ErrMessage): ZodArray; + static create: (schema: El, params?: RawCreateParams) => ZodArray; +} +export type ZodNonEmptyArray = ZodArray; +export type UnknownKeysParam = "passthrough" | "strict" | "strip"; +export interface ZodObjectDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodObject; + shape: () => T; + catchall: Catchall; + unknownKeys: UnknownKeys; +} +export type mergeTypes = { + [k in keyof A | keyof B]: k extends keyof B ? B[k] : k extends keyof A ? A[k] : never; +}; +export type objectOutputType = objectUtil.flatten>> & CatchallOutput & PassthroughType; +export type baseObjectOutputType = { + [k in keyof Shape]: Shape[k]["_output"]; +}; +export type objectInputType = objectUtil.flatten> & CatchallInput & PassthroughType; +export type baseObjectInputType = objectUtil.addQuestionMarks<{ + [k in keyof Shape]: Shape[k]["_input"]; +}>; +export type CatchallOutput = ZodType extends T ? unknown : { + [k: string]: T["_output"]; +}; +export type CatchallInput = ZodType extends T ? unknown : { + [k: string]: T["_input"]; +}; +export type PassthroughType = T extends "passthrough" ? { + [k: string]: unknown; +} : unknown; +export type deoptional = T extends ZodOptional ? deoptional : T extends ZodNullable ? ZodNullable> : T; +export type SomeZodObject = ZodObject; +export type noUnrecognized = { + [k in keyof Obj]: k extends keyof Shape ? Obj[k] : never; +}; +export declare class ZodObject, Input = objectInputType> extends ZodType, Input> { + _cached: { + shape: T; + keys: string[]; + } | null; + _getCached(): { + shape: T; + keys: string[]; + }; + _parse(input: ParseInput): ParseReturnType; + get shape(): T; + strict(message?: errorUtil.ErrMessage): ZodObject; + strip(): ZodObject; + passthrough(): ZodObject; + /** + * @deprecated In most cases, this is no longer needed - unknown properties are now silently stripped. + * If you want to pass through unknown properties, use `.passthrough()` instead. + */ + nonstrict: () => ZodObject; + extend(augmentation: Augmentation): ZodObject, UnknownKeys, Catchall>; + /** + * @deprecated Use `.extend` instead + * */ + augment: (augmentation: Augmentation) => ZodObject, UnknownKeys, Catchall>; + /** + * Prior to zod@1.0.12 there was a bug in the + * inferred type of merged objects. Please + * upgrade if you are experiencing issues. + */ + merge(merging: Incoming): ZodObject, Incoming["_def"]["unknownKeys"], Incoming["_def"]["catchall"]>; + setKey(key: Key, schema: Schema): ZodObject; + catchall(index: Index): ZodObject; + pick>(mask: Mask): ZodObject>, UnknownKeys, Catchall>; + omit>(mask: Mask): ZodObject, UnknownKeys, Catchall>; + /** + * @deprecated + */ + deepPartial(): partialUtil.DeepPartial; + partial(): ZodObject<{ + [k in keyof T]: ZodOptional; + }, UnknownKeys, Catchall>; + partial>(mask: Mask): ZodObject : T[k]; + }>, UnknownKeys, Catchall>; + required(): ZodObject<{ + [k in keyof T]: deoptional; + }, UnknownKeys, Catchall>; + required>(mask: Mask): ZodObject : T[k]; + }>, UnknownKeys, Catchall>; + keyof(): ZodEnum>; + static create: (shape: Shape, params?: RawCreateParams) => ZodObject, objectInputType>; + static strictCreate: (shape: Shape, params?: RawCreateParams) => ZodObject; + static lazycreate: (shape: () => Shape, params?: RawCreateParams) => ZodObject; +} +export type AnyZodObject = ZodObject; +export type ZodUnionOptions = Readonly<[ZodTypeAny, ...ZodTypeAny[]]>; +export interface ZodUnionDef> extends ZodTypeDef { + options: T; + typeName: ZodFirstPartyTypeKind.ZodUnion; +} +export declare class ZodUnion extends ZodType, T[number]["_input"]> { + _parse(input: ParseInput): ParseReturnType; + get options(): T; + static create: >(types: Options, params?: RawCreateParams) => ZodUnion; +} +export type ZodDiscriminatedUnionOption = ZodObject<{ + [key in Discriminator]: ZodTypeAny; +} & ZodRawShape, UnknownKeysParam, ZodTypeAny>; +export interface ZodDiscriminatedUnionDef[] = ZodDiscriminatedUnionOption[]> extends ZodTypeDef { + discriminator: Discriminator; + options: Options; + optionsMap: Map>; + typeName: ZodFirstPartyTypeKind.ZodDiscriminatedUnion; +} +export declare class ZodDiscriminatedUnion[]> extends ZodType, ZodDiscriminatedUnionDef, input> { + _parse(input: ParseInput): ParseReturnType; + get discriminator(): Discriminator; + get options(): Options; + get optionsMap(): Map>; + /** + * The constructor of the discriminated union schema. Its behaviour is very similar to that of the normal z.union() constructor. + * However, it only allows a union of objects, all of which need to share a discriminator property. This property must + * have a different value for each object in the union. + * @param discriminator the name of the discriminator property + * @param types an array of object schemas + * @param params + */ + static create, + ...ZodDiscriminatedUnionOption[] + ]>(discriminator: Discriminator, options: Types, params?: RawCreateParams): ZodDiscriminatedUnion; +} +export interface ZodIntersectionDef extends ZodTypeDef { + left: T; + right: U; + typeName: ZodFirstPartyTypeKind.ZodIntersection; +} +export declare class ZodIntersection extends ZodType, T["_input"] & U["_input"]> { + _parse(input: ParseInput): ParseReturnType; + static create: (left: TSchema, right: USchema, params?: RawCreateParams) => ZodIntersection; +} +export type ZodTupleItems = [ZodTypeAny, ...ZodTypeAny[]]; +export type AssertArray = T extends any[] ? T : never; +export type OutputTypeOfTuple = AssertArray<{ + [k in keyof T]: T[k] extends ZodType ? T[k]["_output"] : never; +}>; +export type OutputTypeOfTupleWithRest = Rest extends ZodTypeAny ? [...OutputTypeOfTuple, ...Rest["_output"][]] : OutputTypeOfTuple; +export type InputTypeOfTuple = AssertArray<{ + [k in keyof T]: T[k] extends ZodType ? T[k]["_input"] : never; +}>; +export type InputTypeOfTupleWithRest = Rest extends ZodTypeAny ? [...InputTypeOfTuple, ...Rest["_input"][]] : InputTypeOfTuple; +export interface ZodTupleDef extends ZodTypeDef { + items: T; + rest: Rest; + typeName: ZodFirstPartyTypeKind.ZodTuple; +} +export type AnyZodTuple = ZodTuple<[ZodTypeAny, ...ZodTypeAny[]] | [], ZodTypeAny | null>; +export declare class ZodTuple extends ZodType, ZodTupleDef, InputTypeOfTupleWithRest> { + _parse(input: ParseInput): ParseReturnType; + get items(): T; + rest(rest: RestSchema): ZodTuple; + static create: (schemas: Items, params?: RawCreateParams) => ZodTuple; +} +export interface ZodRecordDef extends ZodTypeDef { + valueType: Value; + keyType: Key; + typeName: ZodFirstPartyTypeKind.ZodRecord; +} +export type KeySchema = ZodType; +export type RecordType = [string] extends [K] ? Record : [number] extends [K] ? Record : [symbol] extends [K] ? Record : [BRAND] extends [K] ? Record : Partial>; +export declare class ZodRecord extends ZodType, ZodRecordDef, RecordType> { + get keySchema(): Key; + get valueSchema(): Value; + _parse(input: ParseInput): ParseReturnType; + get element(): Value; + static create(valueType: Value, params?: RawCreateParams): ZodRecord; + static create(keySchema: Keys, valueType: Value, params?: RawCreateParams): ZodRecord; +} +export interface ZodMapDef extends ZodTypeDef { + valueType: Value; + keyType: Key; + typeName: ZodFirstPartyTypeKind.ZodMap; +} +export declare class ZodMap extends ZodType, ZodMapDef, Map> { + get keySchema(): Key; + get valueSchema(): Value; + _parse(input: ParseInput): ParseReturnType; + static create: (keyType: KeySchema, valueType: ValueSchema, params?: RawCreateParams) => ZodMap; +} +export interface ZodSetDef extends ZodTypeDef { + valueType: Value; + typeName: ZodFirstPartyTypeKind.ZodSet; + minSize: { + value: number; + message?: string | undefined; + } | null; + maxSize: { + value: number; + message?: string | undefined; + } | null; +} +export declare class ZodSet extends ZodType, ZodSetDef, Set> { + _parse(input: ParseInput): ParseReturnType; + min(minSize: number, message?: errorUtil.ErrMessage): this; + max(maxSize: number, message?: errorUtil.ErrMessage): this; + size(size: number, message?: errorUtil.ErrMessage): this; + nonempty(message?: errorUtil.ErrMessage): ZodSet; + static create: (valueType: ValueSchema, params?: RawCreateParams) => ZodSet; +} +export interface ZodFunctionDef = ZodTuple, Returns extends ZodTypeAny = ZodTypeAny> extends ZodTypeDef { + args: Args; + returns: Returns; + typeName: ZodFirstPartyTypeKind.ZodFunction; +} +export type OuterTypeOfFunction, Returns extends ZodTypeAny> = Args["_input"] extends Array ? (...args: Args["_input"]) => Returns["_output"] : never; +export type InnerTypeOfFunction, Returns extends ZodTypeAny> = Args["_output"] extends Array ? (...args: Args["_output"]) => Returns["_input"] : never; +export declare class ZodFunction, Returns extends ZodTypeAny> extends ZodType, ZodFunctionDef, InnerTypeOfFunction> { + _parse(input: ParseInput): ParseReturnType; + parameters(): Args; + returnType(): Returns; + args[0]>(...items: Items): ZodFunction, Returns>; + returns>(returnType: NewReturnType): ZodFunction; + implement>(func: F): ReturnType extends Returns["_output"] ? (...args: Args["_input"]) => ReturnType : OuterTypeOfFunction; + strictImplement(func: InnerTypeOfFunction): InnerTypeOfFunction; + validate: >(func: F) => ReturnType extends Returns["_output"] ? (...args: Args["_input"]) => ReturnType : OuterTypeOfFunction; + static create(): ZodFunction, ZodUnknown>; + static create>(args: T): ZodFunction; + static create(args: T, returns: U): ZodFunction; + static create, U extends ZodTypeAny = ZodUnknown>(args: T, returns: U, params?: RawCreateParams): ZodFunction; +} +export interface ZodLazyDef extends ZodTypeDef { + getter: () => T; + typeName: ZodFirstPartyTypeKind.ZodLazy; +} +export declare class ZodLazy extends ZodType, ZodLazyDef, input> { + get schema(): T; + _parse(input: ParseInput): ParseReturnType; + static create: (getter: () => Inner, params?: RawCreateParams) => ZodLazy; +} +export interface ZodLiteralDef extends ZodTypeDef { + value: T; + typeName: ZodFirstPartyTypeKind.ZodLiteral; +} +export declare class ZodLiteral extends ZodType, T> { + _parse(input: ParseInput): ParseReturnType; + get value(): T; + static create: (value: Value, params?: RawCreateParams) => ZodLiteral; +} +export type ArrayKeys = keyof any[]; +export type Indices = Exclude; +export type EnumValues = readonly [T, ...T[]]; +export type Values = { + [k in T[number]]: k; +}; +export interface ZodEnumDef extends ZodTypeDef { + values: T; + typeName: ZodFirstPartyTypeKind.ZodEnum; +} +export type Writeable = { + -readonly [P in keyof T]: T[P]; +}; +export type FilterEnum = Values extends [] ? [] : Values extends [infer Head, ...infer Rest] ? Head extends ToExclude ? FilterEnum : [Head, ...FilterEnum] : never; +export type typecast = A extends T ? A : never; +declare function createZodEnum>(values: T, params?: RawCreateParams): ZodEnum>; +declare function createZodEnum(values: T, params?: RawCreateParams): ZodEnum; +export declare class ZodEnum extends ZodType, T[number]> { + _cache: Set | undefined; + _parse(input: ParseInput): ParseReturnType; + get options(): T; + get enum(): Values; + get Values(): Values; + get Enum(): Values; + extract(values: ToExtract, newDef?: RawCreateParams): ZodEnum>; + exclude(values: ToExclude, newDef?: RawCreateParams): ZodEnum>, [string, ...string[]]>>; + static create: typeof createZodEnum; +} +export interface ZodNativeEnumDef extends ZodTypeDef { + values: T; + typeName: ZodFirstPartyTypeKind.ZodNativeEnum; +} +export type EnumLike = { + [k: string]: string | number; + [nu: number]: string; +}; +export declare class ZodNativeEnum extends ZodType, T[keyof T]> { + _cache: Set | undefined; + _parse(input: ParseInput): ParseReturnType; + get enum(): T; + static create: (values: Elements, params?: RawCreateParams) => ZodNativeEnum; +} +export interface ZodPromiseDef extends ZodTypeDef { + type: T; + typeName: ZodFirstPartyTypeKind.ZodPromise; +} +export declare class ZodPromise extends ZodType, ZodPromiseDef, Promise> { + unwrap(): T; + _parse(input: ParseInput): ParseReturnType; + static create: (schema: Inner, params?: RawCreateParams) => ZodPromise; +} +export type Refinement = (arg: T, ctx: RefinementCtx) => any; +export type SuperRefinement = (arg: T, ctx: RefinementCtx) => void | Promise; +export type RefinementEffect = { + type: "refinement"; + refinement: (arg: T, ctx: RefinementCtx) => any; +}; +export type TransformEffect = { + type: "transform"; + transform: (arg: T, ctx: RefinementCtx) => any; +}; +export type PreprocessEffect = { + type: "preprocess"; + transform: (arg: T, ctx: RefinementCtx) => any; +}; +export type Effect = RefinementEffect | TransformEffect | PreprocessEffect; +export interface ZodEffectsDef extends ZodTypeDef { + schema: T; + typeName: ZodFirstPartyTypeKind.ZodEffects; + effect: Effect; +} +export declare class ZodEffects, Input = input> extends ZodType, Input> { + innerType(): T; + sourceType(): T; + _parse(input: ParseInput): ParseReturnType; + static create: (schema: I, effect: Effect, params?: RawCreateParams) => ZodEffects; + static createWithPreprocess: (preprocess: (arg: unknown, ctx: RefinementCtx) => unknown, schema: I, params?: RawCreateParams) => ZodEffects; +} +export { ZodEffects as ZodTransformer }; +export interface ZodOptionalDef extends ZodTypeDef { + innerType: T; + typeName: ZodFirstPartyTypeKind.ZodOptional; +} +export type ZodOptionalType = ZodOptional; +export declare class ZodOptional extends ZodType, T["_input"] | undefined> { + _parse(input: ParseInput): ParseReturnType; + unwrap(): T; + static create: (type: Inner, params?: RawCreateParams) => ZodOptional; +} +export interface ZodNullableDef extends ZodTypeDef { + innerType: T; + typeName: ZodFirstPartyTypeKind.ZodNullable; +} +export type ZodNullableType = ZodNullable; +export declare class ZodNullable extends ZodType, T["_input"] | null> { + _parse(input: ParseInput): ParseReturnType; + unwrap(): T; + static create: (type: Inner, params?: RawCreateParams) => ZodNullable; +} +export interface ZodDefaultDef extends ZodTypeDef { + innerType: T; + defaultValue: () => util.noUndefined; + typeName: ZodFirstPartyTypeKind.ZodDefault; +} +export declare class ZodDefault extends ZodType, ZodDefaultDef, T["_input"] | undefined> { + _parse(input: ParseInput): ParseReturnType; + removeDefault(): T; + static create: (type: Inner, params: RawCreateParams & { + default: Inner["_input"] | (() => util.noUndefined); + }) => ZodDefault; +} +export interface ZodCatchDef extends ZodTypeDef { + innerType: T; + catchValue: (ctx: { + error: ZodError; + input: unknown; + }) => T["_input"]; + typeName: ZodFirstPartyTypeKind.ZodCatch; +} +export declare class ZodCatch extends ZodType, unknown> { + _parse(input: ParseInput): ParseReturnType; + removeCatch(): T; + static create: (type: Inner, params: RawCreateParams & { + catch: Inner["_output"] | (() => Inner["_output"]); + }) => ZodCatch; +} +export interface ZodNaNDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodNaN; +} +export declare class ZodNaN extends ZodType { + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams) => ZodNaN; +} +export interface ZodBrandedDef extends ZodTypeDef { + type: T; + typeName: ZodFirstPartyTypeKind.ZodBranded; +} +export declare const BRAND: unique symbol; +export type BRAND = { + [BRAND]: { + [k in T]: true; + }; +}; +export declare class ZodBranded extends ZodType, ZodBrandedDef, T["_input"]> { + _parse(input: ParseInput): ParseReturnType; + unwrap(): T; +} +export interface ZodPipelineDef extends ZodTypeDef { + in: A; + out: B; + typeName: ZodFirstPartyTypeKind.ZodPipeline; +} +export declare class ZodPipeline extends ZodType, A["_input"]> { + _parse(input: ParseInput): ParseReturnType; + static create(a: ASchema, b: BSchema): ZodPipeline; +} +type BuiltIn = (((...args: any[]) => any) | (new (...args: any[]) => any)) | { + readonly [Symbol.toStringTag]: string; +} | Date | Error | Generator | Promise | RegExp; +type MakeReadonly = T extends Map ? ReadonlyMap : T extends Set ? ReadonlySet : T extends [infer Head, ...infer Tail] ? readonly [Head, ...Tail] : T extends Array ? ReadonlyArray : T extends BuiltIn ? T : Readonly; +export interface ZodReadonlyDef extends ZodTypeDef { + innerType: T; + typeName: ZodFirstPartyTypeKind.ZodReadonly; +} +export declare class ZodReadonly extends ZodType, ZodReadonlyDef, MakeReadonly> { + _parse(input: ParseInput): ParseReturnType; + static create: (type: Inner, params?: RawCreateParams) => ZodReadonly; + unwrap(): T; +} +type CustomParams = CustomErrorParams & { + fatal?: boolean; +}; +export declare function custom(check?: (data: any) => any, _params?: string | CustomParams | ((input: any) => CustomParams), +/** + * @deprecated + * + * Pass `fatal` into the params object instead: + * + * ```ts + * z.string().custom((val) => val.length > 5, { fatal: false }) + * ``` + * + */ +fatal?: boolean): ZodType; +export { ZodType as Schema, ZodType as ZodSchema }; +export declare const late: { + object: (shape: () => Shape, params?: RawCreateParams) => ZodObject; +}; +export declare enum ZodFirstPartyTypeKind { + ZodString = "ZodString", + ZodNumber = "ZodNumber", + ZodNaN = "ZodNaN", + ZodBigInt = "ZodBigInt", + ZodBoolean = "ZodBoolean", + ZodDate = "ZodDate", + ZodSymbol = "ZodSymbol", + ZodUndefined = "ZodUndefined", + ZodNull = "ZodNull", + ZodAny = "ZodAny", + ZodUnknown = "ZodUnknown", + ZodNever = "ZodNever", + ZodVoid = "ZodVoid", + ZodArray = "ZodArray", + ZodObject = "ZodObject", + ZodUnion = "ZodUnion", + ZodDiscriminatedUnion = "ZodDiscriminatedUnion", + ZodIntersection = "ZodIntersection", + ZodTuple = "ZodTuple", + ZodRecord = "ZodRecord", + ZodMap = "ZodMap", + ZodSet = "ZodSet", + ZodFunction = "ZodFunction", + ZodLazy = "ZodLazy", + ZodLiteral = "ZodLiteral", + ZodEnum = "ZodEnum", + ZodEffects = "ZodEffects", + ZodNativeEnum = "ZodNativeEnum", + ZodOptional = "ZodOptional", + ZodNullable = "ZodNullable", + ZodDefault = "ZodDefault", + ZodCatch = "ZodCatch", + ZodPromise = "ZodPromise", + ZodBranded = "ZodBranded", + ZodPipeline = "ZodPipeline", + ZodReadonly = "ZodReadonly" +} +export type ZodFirstPartySchemaTypes = ZodString | ZodNumber | ZodNaN | ZodBigInt | ZodBoolean | ZodDate | ZodUndefined | ZodNull | ZodAny | ZodUnknown | ZodNever | ZodVoid | ZodArray | ZodObject | ZodUnion | ZodDiscriminatedUnion | ZodIntersection | ZodTuple | ZodRecord | ZodMap | ZodSet | ZodFunction | ZodLazy | ZodLiteral | ZodEnum | ZodEffects | ZodNativeEnum | ZodOptional | ZodNullable | ZodDefault | ZodCatch | ZodPromise | ZodBranded | ZodPipeline | ZodReadonly | ZodSymbol; +declare abstract class Class { + constructor(..._: any[]); +} +declare const instanceOfType: (cls: T, params?: CustomParams) => ZodType, ZodTypeDef, InstanceType>; +declare const stringType: (params?: RawCreateParams & { + coerce?: true; +}) => ZodString; +declare const numberType: (params?: RawCreateParams & { + coerce?: boolean; +}) => ZodNumber; +declare const nanType: (params?: RawCreateParams) => ZodNaN; +declare const bigIntType: (params?: RawCreateParams & { + coerce?: boolean; +}) => ZodBigInt; +declare const booleanType: (params?: RawCreateParams & { + coerce?: boolean; +}) => ZodBoolean; +declare const dateType: (params?: RawCreateParams & { + coerce?: boolean; +}) => ZodDate; +declare const symbolType: (params?: RawCreateParams) => ZodSymbol; +declare const undefinedType: (params?: RawCreateParams) => ZodUndefined; +declare const nullType: (params?: RawCreateParams) => ZodNull; +declare const anyType: (params?: RawCreateParams) => ZodAny; +declare const unknownType: (params?: RawCreateParams) => ZodUnknown; +declare const neverType: (params?: RawCreateParams) => ZodNever; +declare const voidType: (params?: RawCreateParams) => ZodVoid; +declare const arrayType: (schema: El, params?: RawCreateParams) => ZodArray; +declare const objectType: (shape: Shape, params?: RawCreateParams) => ZodObject, objectInputType>; +declare const strictObjectType: (shape: Shape, params?: RawCreateParams) => ZodObject; +declare const unionType: >(types: Options, params?: RawCreateParams) => ZodUnion; +declare const discriminatedUnionType: typeof ZodDiscriminatedUnion.create; +declare const intersectionType: (left: TSchema, right: USchema, params?: RawCreateParams) => ZodIntersection; +declare const tupleType: (schemas: Items, params?: RawCreateParams) => ZodTuple; +declare const recordType: typeof ZodRecord.create; +declare const mapType: (keyType: KeySchema, valueType: ValueSchema, params?: RawCreateParams) => ZodMap; +declare const setType: (valueType: ValueSchema, params?: RawCreateParams) => ZodSet; +declare const functionType: typeof ZodFunction.create; +declare const lazyType: (getter: () => Inner, params?: RawCreateParams) => ZodLazy; +declare const literalType: (value: Value, params?: RawCreateParams) => ZodLiteral; +declare const enumType: typeof createZodEnum; +declare const nativeEnumType: (values: Elements, params?: RawCreateParams) => ZodNativeEnum; +declare const promiseType: (schema: Inner, params?: RawCreateParams) => ZodPromise; +declare const effectsType: (schema: I, effect: Effect, params?: RawCreateParams) => ZodEffects; +declare const optionalType: (type: Inner, params?: RawCreateParams) => ZodOptional; +declare const nullableType: (type: Inner, params?: RawCreateParams) => ZodNullable; +declare const preprocessType: (preprocess: (arg: unknown, ctx: RefinementCtx) => unknown, schema: I, params?: RawCreateParams) => ZodEffects; +declare const pipelineType: typeof ZodPipeline.create; +declare const ostring: () => ZodOptional; +declare const onumber: () => ZodOptional; +declare const oboolean: () => ZodOptional; +export declare const coerce: { + string: (typeof ZodString)["create"]; + number: (typeof ZodNumber)["create"]; + boolean: (typeof ZodBoolean)["create"]; + bigint: (typeof ZodBigInt)["create"]; + date: (typeof ZodDate)["create"]; +}; +export { anyType as any, arrayType as array, bigIntType as bigint, booleanType as boolean, dateType as date, discriminatedUnionType as discriminatedUnion, effectsType as effect, enumType as enum, functionType as function, instanceOfType as instanceof, intersectionType as intersection, lazyType as lazy, literalType as literal, mapType as map, nanType as nan, nativeEnumType as nativeEnum, neverType as never, nullType as null, nullableType as nullable, numberType as number, objectType as object, oboolean, onumber, optionalType as optional, ostring, pipelineType as pipeline, preprocessType as preprocess, promiseType as promise, recordType as record, setType as set, strictObjectType as strictObject, stringType as string, symbolType as symbol, effectsType as transformer, tupleType as tuple, undefinedType as undefined, unionType as union, unknownType as unknown, voidType as void, }; +export declare const NEVER: never; diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/checks.d.cts b/tests/fixtures/npm/node_modules/zod/v4/classic/checks.d.cts new file mode 100644 index 000000000..757d60e6e --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/checks.d.cts @@ -0,0 +1 @@ +export { _lt as lt, _lte as lte, _gt as gt, _gte as gte, _positive as positive, _negative as negative, _nonpositive as nonpositive, _nonnegative as nonnegative, _multipleOf as multipleOf, _maxSize as maxSize, _minSize as minSize, _size as size, _maxLength as maxLength, _minLength as minLength, _length as length, _regex as regex, _lowercase as lowercase, _uppercase as uppercase, _includes as includes, _startsWith as startsWith, _endsWith as endsWith, _property as property, _properties as properties, _mime as mime, _overwrite as overwrite, _normalize as normalize, _trim as trim, _toLowerCase as toLowerCase, _toUpperCase as toUpperCase, _slugify as slugify, type $RefinementCtx as RefinementCtx, } from "../core/index.cjs"; diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/checks.js b/tests/fixtures/npm/node_modules/zod/v4/classic/checks.js new file mode 100644 index 000000000..50a08b884 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/checks.js @@ -0,0 +1 @@ +export { _lt as lt, _lte as lte, _gt as gt, _gte as gte, _positive as positive, _negative as negative, _nonpositive as nonpositive, _nonnegative as nonnegative, _multipleOf as multipleOf, _maxSize as maxSize, _minSize as minSize, _size as size, _maxLength as maxLength, _minLength as minLength, _length as length, _regex as regex, _lowercase as lowercase, _uppercase as uppercase, _includes as includes, _startsWith as startsWith, _endsWith as endsWith, _property as property, _properties as properties, _mime as mime, _overwrite as overwrite, _normalize as normalize, _trim as trim, _toLowerCase as toLowerCase, _toUpperCase as toUpperCase, _slugify as slugify, } from "../core/index.js"; diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/coerce.d.cts b/tests/fixtures/npm/node_modules/zod/v4/classic/coerce.d.cts new file mode 100644 index 000000000..231b91745 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/coerce.d.cts @@ -0,0 +1,17 @@ +import * as core from "../core/index.cjs"; +import * as schemas from "./schemas.cjs"; +export interface ZodCoercedString extends schemas._ZodString> { +} +export declare function string(params?: string | core.$ZodStringParams): ZodCoercedString; +export interface ZodCoercedNumber extends schemas._ZodNumber> { +} +export declare function number(params?: string | core.$ZodNumberParams): ZodCoercedNumber; +export interface ZodCoercedBoolean extends schemas._ZodBoolean> { +} +export declare function boolean(params?: string | core.$ZodBooleanParams): ZodCoercedBoolean; +export interface ZodCoercedBigInt extends schemas._ZodBigInt> { +} +export declare function bigint(params?: string | core.$ZodBigIntParams): ZodCoercedBigInt; +export interface ZodCoercedDate extends schemas._ZodDate> { +} +export declare function date(params?: string | core.$ZodDateParams): ZodCoercedDate; diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/coerce.js b/tests/fixtures/npm/node_modules/zod/v4/classic/coerce.js new file mode 100644 index 000000000..141cf8936 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/coerce.js @@ -0,0 +1,17 @@ +import * as core from "../core/index.js"; +import * as schemas from "./schemas.js"; +export function string(params) { + return core._coercedString(schemas.ZodString, params); +} +export function number(params) { + return core._coercedNumber(schemas.ZodNumber, params); +} +export function boolean(params) { + return core._coercedBoolean(schemas.ZodBoolean, params); +} +export function bigint(params) { + return core._coercedBigint(schemas.ZodBigInt, params); +} +export function date(params) { + return core._coercedDate(schemas.ZodDate, params); +} diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/compat.d.cts b/tests/fixtures/npm/node_modules/zod/v4/classic/compat.d.cts new file mode 100644 index 000000000..8f1b3d8c8 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/compat.d.cts @@ -0,0 +1,48 @@ +import * as core from "../core/index.cjs"; +import type { ZodType } from "./schemas.cjs"; +/** @deprecated Use `z.output` instead. */ +export type TypeOf = core.output; +/** @deprecated Use `z.output` instead. */ +export type Infer = core.output; +/** @deprecated Use `z.core.$ZodTypes` instead */ +export type ZodFirstPartySchemaTypes = core.$ZodTypes; +/** @deprecated Use the raw string literal codes instead, e.g. "invalid_type". */ +export declare const ZodIssueCode: { + readonly invalid_type: "invalid_type"; + readonly too_big: "too_big"; + readonly too_small: "too_small"; + readonly invalid_format: "invalid_format"; + readonly not_multiple_of: "not_multiple_of"; + readonly unrecognized_keys: "unrecognized_keys"; + readonly invalid_union: "invalid_union"; + readonly invalid_key: "invalid_key"; + readonly invalid_element: "invalid_element"; + readonly invalid_value: "invalid_value"; + readonly custom: "custom"; +}; +/** @deprecated Use `z.core.$ZodFlattenedError>` instead; it takes an inferred type, not a schema. */ +export type inferFlattenedErrors = core.$ZodFlattenedError, U>; +/** @deprecated Use `z.core.$ZodFormattedError>` instead; it takes an inferred type, not a schema. */ +export type inferFormattedError, U = string> = core.$ZodFormattedError, U>; +/** Use `z.$brand` instead */ +export type BRAND = { + [core.$brand]: { + [k in T]: true; + }; +}; +export { $brand, config } from "../core/index.cjs"; +/** @deprecated Use `z.config(params)` instead. */ +export declare function setErrorMap(map: core.$ZodErrorMap): void; +/** @deprecated Use `z.config()` instead. */ +export declare function getErrorMap(): core.$ZodErrorMap | undefined; +/** @deprecated Use z.ZodType (without generics) instead. */ +export type ZodTypeAny = core.$ZodTypeInternals> = ZodType; +/** @deprecated Use `z.ZodType` */ +export type ZodSchema = core.$ZodTypeInternals> = ZodType; +/** @deprecated Use `z.ZodType` */ +export type Schema = core.$ZodTypeInternals> = ZodType; +/** Included for Zod 3 compatibility */ +export type ZodRawShape = core.$ZodShape; +/** @deprecated Do not use. Stub definition, only included for zod-to-json-schema compatibility. */ +export declare enum ZodFirstPartyTypeKind { +} diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/compat.js b/tests/fixtures/npm/node_modules/zod/v4/classic/compat.js new file mode 100644 index 000000000..2179ea904 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/compat.js @@ -0,0 +1,31 @@ +// Zod 3 compat layer +import * as core from "../core/index.js"; +/** @deprecated Use the raw string literal codes instead, e.g. "invalid_type". */ +export const ZodIssueCode = { + invalid_type: "invalid_type", + too_big: "too_big", + too_small: "too_small", + invalid_format: "invalid_format", + not_multiple_of: "not_multiple_of", + unrecognized_keys: "unrecognized_keys", + invalid_union: "invalid_union", + invalid_key: "invalid_key", + invalid_element: "invalid_element", + invalid_value: "invalid_value", + custom: "custom", +}; +export { $brand, config } from "../core/index.js"; +/** @deprecated Use `z.config(params)` instead. */ +export function setErrorMap(map) { + core.config({ + customError: map, + }); +} +/** @deprecated Use `z.config()` instead. */ +export function getErrorMap() { + return core.config().customError; +} +/** @deprecated Do not use. Stub definition, only included for zod-to-json-schema compatibility. */ +export var ZodFirstPartyTypeKind; +(function (ZodFirstPartyTypeKind) { +})(ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {})); diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/deep-partial.d.cts b/tests/fixtures/npm/node_modules/zod/v4/classic/deep-partial.d.cts new file mode 100644 index 000000000..674ecf18e --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/deep-partial.d.cts @@ -0,0 +1,14 @@ +import type * as core from "../core/index.cjs"; +import * as schemas from "./schemas.cjs"; +/** Distributes `.partial()` through every object in a schema type, preserving the wrappers around them. */ +export type DeepPartial = T extends schemas.ZodObject ? schemas.ZodObject<{ + -readonly [K in keyof Shape]: schemas.ZodOptional>; +}, Config> : T extends schemas.ZodDiscriminatedUnion ? schemas.ZodUnion<{ + -readonly [I in keyof Options]: Options[I] extends core.SomeType ? DeepPartial : Options[I]; +}> : T extends schemas.ZodUnion ? schemas.ZodUnion<{ + -readonly [I in keyof Options]: Options[I] extends core.SomeType ? DeepPartial : Options[I]; +}> : T extends schemas.ZodArray ? schemas.ZodArray> : T extends schemas.ZodTuple ? schemas.ZodTuple<{ + -readonly [K in keyof Items]: Items[K] extends core.SomeType ? DeepPartial : Items[K]; +}, Rest extends core.SomeType ? DeepPartial : Rest> : T extends schemas.ZodIntersection ? schemas.ZodIntersection, DeepPartial> : T extends schemas.ZodRecord ? schemas.ZodRecord> : T extends schemas.ZodMap ? schemas.ZodMap> : T extends schemas.ZodSet ? schemas.ZodSet> : T extends schemas.ZodOptional ? schemas.ZodOptional> : T extends schemas.ZodNullable ? schemas.ZodNullable> : T extends schemas.ZodDefault ? schemas.ZodDefault> : T extends schemas.ZodPrefault ? schemas.ZodPrefault> : T extends schemas.ZodNonOptional ? schemas.ZodNonOptional> : T extends schemas.ZodCatch ? schemas.ZodCatch> : T extends schemas.ZodReadonly ? schemas.ZodReadonly> : T extends schemas.ZodSuccess ? schemas.ZodSuccess> : T extends schemas.ZodPromise ? schemas.ZodPromise> : T extends schemas.ZodCodec ? schemas.ZodCodec, DeepPartial> : T extends schemas.ZodPreprocess ? schemas.ZodPreprocess> : T extends schemas.ZodPipe ? schemas.ZodPipe, DeepPartial> : T extends schemas.ZodLazy ? schemas.ZodLazy> : T; +/** Returns a copy of the schema with every nested object's properties made optional. */ +export declare function deepPartial(schema: T): DeepPartial; diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/deep-partial.js b/tests/fixtures/npm/node_modules/zod/v4/classic/deep-partial.js new file mode 100644 index 000000000..a87bb625c --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/deep-partial.js @@ -0,0 +1,13 @@ +import { visit } from "../core/visit.js"; +import * as schemas from "./schemas.js"; +/** Returns a copy of the schema with every nested object's properties made optional. */ +export function deepPartial(schema) { + return visit(schema, { + object: (s) => s.partial(), + // Every partialed option now admits `undefined`, which the constructor rejects as a duplicate. + union: (s) => { + const def = s._zod.def; + return def.discriminator === undefined ? s : schemas.union(def.options); + }, + }); +} diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/errors.d.cts b/tests/fixtures/npm/node_modules/zod/v4/classic/errors.d.cts new file mode 100644 index 000000000..472102f6a --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/errors.d.cts @@ -0,0 +1,29 @@ +import * as core from "../core/index.cjs"; +import { $ZodError } from "../core/index.cjs"; +/** @deprecated Use `z.core.$ZodIssue` from `@zod/core` instead, especially if you are building a library on top of Zod. */ +export type ZodIssue = core.$ZodIssue; +/** An Error-like class used to store Zod validation issues. */ +export interface ZodError extends $ZodError { + /** @deprecated Use the `z.treeifyError(err)` function instead. */ + format(): core.$ZodFormattedError; + format(mapper: (issue: core.$ZodIssue) => U): core.$ZodFormattedError; + /** @deprecated Use the `z.treeifyError(err)` function instead. */ + flatten(): core.$ZodFlattenedError; + flatten(mapper: (issue: core.$ZodIssue) => U): core.$ZodFlattenedError; + /** @deprecated Push directly to `.issues` instead. */ + addIssue(issue: core.$ZodIssue): void; + /** @deprecated Push directly to `.issues` instead. */ + addIssues(issues: core.$ZodIssue[]): void; + /** @deprecated Check `err.issues.length === 0` instead. */ + isEmpty: boolean; +} +export declare const ZodError: core.$constructor; +export declare const ZodRealError: core.$constructor; +/** @deprecated Use `z.core.$ZodFlattenedError` instead. */ +export type ZodFlattenedError = core.$ZodFlattenedError; +/** @deprecated Use `z.core.$ZodFormattedError` instead. */ +export type ZodFormattedError = core.$ZodFormattedError; +/** @deprecated Use `z.core.$ZodErrorMap` instead. */ +export type ZodErrorMap = core.$ZodErrorMap; +/** @deprecated Use `z.core.$ZodRawIssue` instead. */ +export type IssueData = core.$ZodRawIssue; diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/errors.js b/tests/fixtures/npm/node_modules/zod/v4/classic/errors.js new file mode 100644 index 000000000..e3c02587a --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/errors.js @@ -0,0 +1,57 @@ +import * as core from "../core/index.js"; +import { $ZodError } from "../core/index.js"; +import * as util from "../core/util.js"; +/* Prototypes that already carry the lazy helper methods. Seeded with the + * intrinsics so that `init` on a foreign object — it accepts any object — + * can never install an accessor onto a prototype we do not own. */ +const _installedErrorProtos = /* @__PURE__ */ new WeakSet([Object.prototype, Error.prototype]); +/* Helper methods live as non-enumerable lazy getters on the shared + * prototype instead of own properties on every instance. On first + * access the getter allocates the per-instance closure and caches it + * as a non-enumerable own property, so detached usage still works and + * the allocation only happens for methods actually touched. */ +function _lazyMethod(proto, key, make) { + Object.defineProperty(proto, key, { + configurable: true, + enumerable: false, + get() { + const value = make(this); + Object.defineProperty(this, key, { value, configurable: true, writable: true }); + return value; + }, + set(value) { + Object.defineProperty(this, key, { value, configurable: true, writable: true }); + }, + }); +} +const initializer = (inst, issues) => { + $ZodError.init(inst, issues); + inst.name = "ZodError"; + const proto = Object.getPrototypeOf(inst); + if (_installedErrorProtos.has(proto)) + return; + _installedErrorProtos.add(proto); + _lazyMethod(proto, "format", (self) => (mapper) => core.formatError(self, mapper)); + _lazyMethod(proto, "flatten", (self) => (mapper) => core.flattenError(self, mapper)); + _lazyMethod(proto, "addIssue", (self) => (issue) => { + self.issues.push(issue); + self.message = JSON.stringify(self.issues, util.jsonStringifyReplacer, 2); + }); + _lazyMethod(proto, "addIssues", (self) => (issues) => { + self.issues.push(...issues); + self.message = JSON.stringify(self.issues, util.jsonStringifyReplacer, 2); + }); + Object.defineProperty(proto, "isEmpty", { + configurable: true, + enumerable: false, + get() { + return this.issues.length === 0; + }, + }); +}; +export const ZodError = /*@__PURE__*/ core.$constructor("ZodError", initializer); +export const ZodRealError = /*@__PURE__*/ core.$constructor("ZodError", initializer, undefined, { + Parent: Error, +}); +// /** @deprecated Use `z.core.$ZodErrorMapCtx` instead. */ +// export type ErrorMapCtx = core.$ZodErrorMapCtx; diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/external.d.cts b/tests/fixtures/npm/node_modules/zod/v4/classic/external.d.cts new file mode 100644 index 000000000..7e9ce5e74 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/external.d.cts @@ -0,0 +1,19 @@ +export * as core from "../core/index.cjs"; +export * from "./schemas.cjs"; +export * from "./checks.cjs"; +export * from "./errors.cjs"; +export * from "./parse.cjs"; +export * from "./compat.cjs"; +export type { infer } from "../core/index.cjs"; +export type { JSONType } from "../core/util.cjs"; +export type { CompileOptions } from "../core/index.cjs"; +export { globalRegistry, type GlobalMeta, registry, config, memoizer, $output, $input, $brand, clone, regexes, treeifyError, prettifyError, formatError, flattenError, TimePrecision, util, NEVER, INVALID, toZod, compile, withParser, ZodCompileAsyncError, ZodCompileUnsupportedError, getDiscriminatedOption, } from "../core/index.cjs"; +export { toJSONSchema } from "../core/json-schema-processors.cjs"; +export { fromJSONSchema } from "./from-json-schema.cjs"; +export { deepPartial } from "./deep-partial.cjs"; +export { input, output } from "./in-out.cjs"; +export * as locales from "../locales/index.cjs"; +export { ZodISODateTime, ZodISODate, ZodISOTime, ZodISODuration } from "./iso.cjs"; +export * as iso from "./iso.cjs"; +export type { ZodCoercedString, ZodCoercedNumber, ZodCoercedBigInt, ZodCoercedBoolean, ZodCoercedDate, } from "./coerce.cjs"; +export * as coerce from "./coerce.cjs"; diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/external.js b/tests/fixtures/npm/node_modules/zod/v4/classic/external.js new file mode 100644 index 000000000..435be167f --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/external.js @@ -0,0 +1,17 @@ +export * as core from "../core/index.js"; +export * from "./schemas.js"; +export * from "./checks.js"; +export * from "./errors.js"; +export * from "./parse.js"; +export * from "./compat.js"; +export { globalRegistry, registry, config, memoizer, $output, $input, $brand, clone, regexes, treeifyError, prettifyError, formatError, flattenError, TimePrecision, util, NEVER, INVALID, toZod, compile, withParser, ZodCompileAsyncError, ZodCompileUnsupportedError, getDiscriminatedOption, } from "../core/index.js"; +export { toJSONSchema } from "../core/json-schema-processors.js"; +export { fromJSONSchema } from "./from-json-schema.js"; +export { deepPartial } from "./deep-partial.js"; +// Types and values both, from one module — re-exporting the types from core would collide. +export { input, output } from "./in-out.js"; +export * as locales from "../locales/index.js"; +// iso must be exported from top-level https://github.com/colinhacks/zod/issues/4491 +export { ZodISODateTime, ZodISODate, ZodISOTime, ZodISODuration } from "./iso.js"; +export * as iso from "./iso.js"; +export * as coerce from "./coerce.js"; diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/from-json-schema.d.cts b/tests/fixtures/npm/node_modules/zod/v4/classic/from-json-schema.d.cts new file mode 100644 index 000000000..dc028bc01 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/from-json-schema.d.cts @@ -0,0 +1,12 @@ +import type * as JSONSchema from "../core/json-schema.cjs"; +import { type $ZodRegistry } from "../core/registries.cjs"; +import type { ZodType } from "./schemas.cjs"; +type JSONSchemaVersion = "draft-2020-12" | "draft-7" | "draft-4" | "openapi-3.0"; +interface FromJSONSchemaParams { + defaultTarget?: JSONSchemaVersion; + registry?: $ZodRegistry; +} +/** + * Converts a JSON Schema to a Zod schema. This function should be considered semi-experimental. It's behavior is liable to change. */ +export declare function fromJSONSchema(schema: JSONSchema.JSONSchema | boolean, params?: FromJSONSchemaParams): ZodType; +export {}; diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/from-json-schema.js b/tests/fixtures/npm/node_modules/zod/v4/classic/from-json-schema.js new file mode 100644 index 000000000..1f79b5c85 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/from-json-schema.js @@ -0,0 +1,881 @@ +import { globalRegistry } from "../core/registries.js"; +import { assignProp, isPlainObject } from "../core/util.js"; +import * as _checks from "./checks.js"; +import * as _iso from "./iso.js"; +import * as _schemas from "./schemas.js"; +// Local z object to avoid circular dependency with ../index.js +const z = { + ..._schemas, + ..._checks, + iso: _iso, +}; +// Keys that are recognized and handled by the conversion logic +const RECOGNIZED_KEYS = /*@__PURE__*/ new Set([ + // Schema identification + "$schema", + "$ref", + "$defs", + "definitions", + // Core schema keywords + "$id", + "id", + "$comment", + "$anchor", + "$vocabulary", + "$dynamicRef", + "$dynamicAnchor", + // Type + "type", + "enum", + "const", + // Composition + "anyOf", + "oneOf", + "allOf", + "not", + // Object + "properties", + "required", + "additionalProperties", + "patternProperties", + "propertyNames", + "minProperties", + "maxProperties", + // Array + "items", + "prefixItems", + "additionalItems", + "minItems", + "maxItems", + "uniqueItems", + "contains", + "minContains", + "maxContains", + // String + "minLength", + "maxLength", + "pattern", + "format", + // Number + "minimum", + "maximum", + "exclusiveMinimum", + "exclusiveMaximum", + "multipleOf", + // Already handled metadata + "description", + "default", + // Content + "contentEncoding", + "contentMediaType", + "contentSchema", + // Unsupported (error-throwing) + "unevaluatedItems", + "unevaluatedProperties", + "if", + "then", + "else", + "dependentSchemas", + "dependentRequired", + // OpenAPI + "nullable", + "readOnly", +]); +function detectVersion(schema, defaultTarget) { + const $schema = schema.$schema; + if ($schema === "https://json-schema.org/draft/2020-12/schema") { + return "draft-2020-12"; + } + if ($schema === "http://json-schema.org/draft-07/schema#") { + return "draft-7"; + } + if ($schema === "http://json-schema.org/draft-04/schema#") { + return "draft-4"; + } + // Use defaultTarget if provided, otherwise default to draft-2020-12 + return defaultTarget ?? "draft-2020-12"; +} +// Positional schemas constrain the elements that are present; only minItems makes them required. +function applyMinItems(items, minItems) { + return items.map((item, index) => (index < minItems ? item : item.optional())); +} +// Inverse of the encoding applied to $ref pointer segments in to-json-schema.ts. Per RFC 6901 the `~1` replacement must run before `~0`. +function decodeJSONPointerSegment(segment) { + return segment.replace(/~1/g, "/").replace(/~0/g, "~"); +} +function resolveRef(ref, ctx) { + if (!ref.startsWith("#")) { + throw new Error("External $ref is not supported, only local refs (#/...) are allowed"); + } + const path = ref.slice(1).split("/").filter(Boolean); + // Handle root reference "#" + if (path.length === 0) { + return ctx.rootSchema; + } + const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions"; + if (path[0] === defsKey) { + const key = path[1] === undefined ? undefined : decodeJSONPointerSegment(path[1]); + if (!key || !ctx.defs[key]) { + throw new Error(`Reference not found: ${ref}`); + } + return ctx.defs[key]; + } + throw new Error(`Reference not found: ${ref}`); +} +/** + * Enforces the keywords that constrain an object's own keys — `propertyNames`, + * `minProperties`, `maxProperties` — before `objectSchema` runs. The guard has + * to see the raw input: an object parse drops `__proto__` and can add keys from + * a property `default`, so its output is not the set of names the instance + * actually carried. + */ +function checkObjectGuards(objectSchema, guards) { + // An identity transform, not z.any(), so `toJSONSchema` reports the object on both the input and the output side of the pipe. + const guard = z + .transform((value) => value) + .check((payload) => { + const value = payload.value; + if (typeof value !== "object" || value === null || Array.isArray(value)) + return; + const keys = Object.getOwnPropertyNames(value); + if (guards.minProperties !== undefined && keys.length < guards.minProperties) { + payload.issues.push({ + origin: "object", + code: "too_small", + minimum: guards.minProperties, + inclusive: true, + message: `Too small: expected object to have >=${guards.minProperties} properties`, + input: value, + inst: objectSchema, + continue: true, + }); + } + if (guards.maxProperties !== undefined && keys.length > guards.maxProperties) { + payload.issues.push({ + origin: "object", + code: "too_big", + maximum: guards.maxProperties, + inclusive: true, + message: `Too big: expected object to have <=${guards.maxProperties} properties`, + input: value, + inst: objectSchema, + continue: true, + }); + } + if (guards.keySchema) { + for (const key of keys) { + const result = guards.keySchema.safeParse(key); + if (result.success) + continue; + payload.issues.push({ + code: "invalid_key", + origin: "record", + issues: result.error.issues, + input: key, + path: [key], + continue: true, + }); + } + } + }); + return guard.pipe(objectSchema); +} +/** + * An injective string for JSON data: two values share a key exactly when `uniqueItems` calls them + * equal, so duplicate detection is a Map lookup rather than a pairwise walk. Strings and keys are + * length-prefixed so no value can spell another one. `null` means "never equal to anything" — a + * cycle or a NaN, neither of which JSON can express. + */ +function canonicalKey(value, seen) { + if (value === null) + return "z"; + const type = typeof value; + if (type !== "object") { + if (type === "number" && Number.isNaN(value)) + return null; + const raw = String(value); + return `${type[0]}${raw.length}:${raw}`; + } + if (seen.has(value)) + return null; + seen.add(value); + try { + if (Array.isArray(value)) { + const parts = []; + for (const item of value) { + const key = canonicalKey(item, seen); + if (key === null) + return null; + parts.push(key); + } + return `a${parts.length}:[${parts.join(",")}]`; + } + const keys = Object.keys(value).sort(); + const parts = []; + for (const k of keys) { + const key = canonicalKey(value[k], seen); + if (key === null) + return null; + parts.push(`${k.length}:${k}=${key}`); + } + return `o${parts.length}:{${parts.join(",")}}`; + } + finally { + seen.delete(value); + } +} +// keywords whose value is a schema or an array of them +const SCHEMA_KEYWORDS = /*@__PURE__*/ new Set([ + "items", + "prefixItems", + "additionalItems", + "additionalProperties", + "contains", + "propertyNames", + "not", + "if", + "then", + "else", + "allOf", + "anyOf", + "oneOf", + "unevaluatedItems", + "unevaluatedProperties", + "contentSchema", +]); +// keywords whose value maps names to schemas. draft-7 `dependencies` also allows an array of property names, which holds no schema to walk +const SCHEMA_MAP_KEYWORDS = /*@__PURE__*/ new Set([ + "properties", + "patternProperties", + "dependentSchemas", + "dependencies", + "$defs", + "definitions", +]); +/** + * True when a subschema holds a `$ref` anywhere a schema can appear. Only those positions are + * walked: `$ref` under `default` or an `x-` annotation is instance data with an ordinary key, and + * reading it as a reference would drop a constraint that resolves perfectly well. + */ +function containsRef(value) { + if (typeof value !== "object" || value === null) + return false; + if (Array.isArray(value)) + return value.some(containsRef); + if (typeof value.$ref === "string") + return true; + return Object.entries(value).some(([key, sub]) => { + if (SCHEMA_KEYWORDS.has(key)) + return containsRef(sub); + if (!SCHEMA_MAP_KEYWORDS.has(key) || typeof sub !== "object" || sub === null) + return false; + return Object.values(sub).some(containsRef); + }); +} +function plural(n) { + return n === 1 ? "element" : "elements"; +} +/** + * Enforces `uniqueItems` and the `contains` family before `arraySchema` runs, for the same reason + * `checkObjectGuards` runs pre-pipe: an item parse can apply a nested `default`, so the parsed + * array is not the instance the keywords are defined over. A guard issue aborts the pipe, so + * `minItems`/`maxItems` are not also reported on the same parse — instance correctness is worth + * more than co-reporting two issues. + */ +function checkArrayGuards(arraySchema, guards) { + // An identity transform, not z.any(), so `toJSONSchema` reports the array on both the input and the output side of the pipe. + const guard = z + .transform((value) => value) + .check((payload) => { + const items = payload.value; + if (!Array.isArray(items)) + return; + if (guards.uniqueItems === true) { + const firstSeen = new Map(); + for (let i = 0; i < items.length; i++) { + const key = canonicalKey(items[i], new Set()); + if (key === null) + continue; + const first = firstSeen.get(key); + if (first === undefined) { + firstSeen.set(key, i); + continue; + } + payload.issues.push({ + code: "custom", + message: `Array items must be unique: element at index ${i} duplicates the one at index ${first}`, + input: items, + path: [i], + continue: true, + }); + } + } + if (guards.containsSchema) { + const minContains = guards.minContains ?? 1; + // one past the ceiling already proves the failure, and nothing above it changes the verdict + const ceiling = guards.maxContains !== undefined ? guards.maxContains + 1 : Number.POSITIVE_INFINITY; + let matches = 0; + for (const item of items) { + if (guards.containsSchema.safeParse(item).success && ++matches >= ceiling) + break; + } + if (matches < minContains) { + // the scan ran to the end, so this count is exact + payload.issues.push({ + code: "custom", + message: `Array must contain at least ${minContains} matching ${plural(minContains)}; found ${matches}`, + input: items, + continue: true, + }); + } + if (guards.maxContains !== undefined && matches > guards.maxContains) { + payload.issues.push({ + code: "custom", + message: `Array must contain at most ${guards.maxContains} matching ${plural(guards.maxContains)}`, + input: items, + continue: true, + }); + } + } + }); + return guard.pipe(arraySchema); +} +function getTupleRest(restSchema, ctx) { + if (restSchema === false) { + return undefined; + } + if (restSchema === undefined || restSchema === true) { + return z.any(); + } + return convertSchema(restSchema, ctx); +} +// the RFC 3339 full-time keyword, narrower than `z.iso.time()`; local because sharing it via `timeSource` pins that builder into every bundle (+139 B gzipped on a mini `z.boolean()`) +const fullTime = /^(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/; +function convertBaseSchema(schema, ctx) { + // Handle unsupported features + if (schema.not !== undefined) { + // Special case: { not: {} } represents never + if (typeof schema.not === "object" && Object.keys(schema.not).length === 0) { + return z.never(); + } + throw new Error("not is not supported in Zod (except { not: {} } for never)"); + } + if (schema.unevaluatedItems !== undefined) { + throw new Error("unevaluatedItems is not supported"); + } + if (schema.unevaluatedProperties !== undefined) { + throw new Error("unevaluatedProperties is not supported"); + } + if (schema.if !== undefined || schema.then !== undefined || schema.else !== undefined) { + throw new Error("Conditional schemas (if/then/else) are not supported"); + } + if (schema.dependentSchemas !== undefined || schema.dependentRequired !== undefined) { + throw new Error("dependentSchemas and dependentRequired are not supported"); + } + // Handle $ref + if (schema.$ref) { + const refPath = schema.$ref; + if (ctx.refs.has(refPath)) { + return ctx.refs.get(refPath); + } + if (ctx.processing.has(refPath)) { + // Circular reference - use lazy + return z.lazy(() => { + if (!ctx.refs.has(refPath)) { + throw new Error(`Circular reference not resolved: ${refPath}`); + } + return ctx.refs.get(refPath); + }); + } + ctx.processing.add(refPath); + const resolved = resolveRef(refPath, ctx); + const zodSchema = convertSchema(resolved, ctx); + ctx.refs.set(refPath, zodSchema); + ctx.processing.delete(refPath); + return zodSchema; + } + // Handle enum + if (schema.enum !== undefined) { + const enumValues = schema.enum; + // Special case: OpenAPI 3.0 null representation { type: "string", nullable: true, enum: [null] } + if (ctx.version === "openapi-3.0" && + schema.nullable === true && + enumValues.length === 1 && + enumValues[0] === null) { + return z.null(); + } + if (enumValues.length === 0) { + return z.never(); + } + if (enumValues.length === 1) { + return z.literal(enumValues[0]); + } + // Check if all values are strings + if (enumValues.every((v) => typeof v === "string")) { + return z.enum(enumValues); + } + // Mixed types - use union of literals + const literalSchemas = enumValues.map((v) => z.literal(v)); + if (literalSchemas.length < 2) { + return literalSchemas[0]; + } + return z.union([literalSchemas[0], literalSchemas[1], ...literalSchemas.slice(2)]); + } + // Handle const + if (schema.const !== undefined) { + return z.literal(schema.const); + } + // Handle type + const type = schema.type; + if (Array.isArray(type)) { + // Expand type array into anyOf union + const typeSchemas = type.map((t) => { + const typeSchema = { ...schema, type: t }; + return convertBaseSchema(typeSchema, ctx); + }); + if (typeSchemas.length === 0) { + return z.never(); + } + if (typeSchemas.length === 1) { + return typeSchemas[0]; + } + return z.union(typeSchemas); + } + if (!type) { + // No type specified - empty schema (any) + return z.any(); + } + let zodSchema; + switch (type) { + case "string": { + let stringSchema = z.string(); + // Apply format using .check() with Zod format functions + if (schema.format) { + const format = schema.format; + // Map common formats to Zod check functions + if (format === "email") { + stringSchema = stringSchema.check(z.email()); + } + else if (format === "uri" || format === "uri-reference") { + stringSchema = stringSchema.check(z.url()); + } + else if (format === "uuid" || format === "guid") { + stringSchema = stringSchema.check(z.uuid()); + } + else if (format === "date-time") { + stringSchema = stringSchema.check(z.iso.datetime({ offset: true })); + } + else if (format === "date") { + stringSchema = stringSchema.check(z.iso.date()); + } + else if (format === "time") { + stringSchema = stringSchema.check(z.regex(fullTime)); + } + else if (format === "duration") { + stringSchema = stringSchema.check(z.iso.duration()); + } + else if (format === "hostname") { + stringSchema = stringSchema.check(z.hostname()); + } + else if (format === "ipv4") { + stringSchema = stringSchema.check(z.ipv4()); + } + else if (format === "ipv6") { + stringSchema = stringSchema.check(z.ipv6()); + } + else if (format === "mac") { + stringSchema = stringSchema.check(z.mac()); + } + else if (format === "cidr") { + stringSchema = stringSchema.check(z.cidrv4()); + } + else if (format === "cidr-v6") { + stringSchema = stringSchema.check(z.cidrv6()); + } + else if (format === "base64") { + stringSchema = stringSchema.check(z.base64()); + } + else if (format === "base64url") { + stringSchema = stringSchema.check(z.base64url()); + } + else if (format === "e164") { + stringSchema = stringSchema.check(z.e164()); + } + else if (format === "credit_card") { + stringSchema = stringSchema.check(z.creditCard()); + } + else if (format === "iban") { + stringSchema = stringSchema.check(z.iban()); + } + else if (format === "jwt") { + stringSchema = stringSchema.check(z.jwt()); + } + else if (format === "emoji") { + stringSchema = stringSchema.check(z.emoji()); + } + else if (format === "nanoid") { + stringSchema = stringSchema.check(z.nanoid()); + } + else if (format === "cuid") { + stringSchema = stringSchema.check(z.cuid()); + } + else if (format === "cuid2") { + stringSchema = stringSchema.check(z.cuid2()); + } + else if (format === "ulid") { + stringSchema = stringSchema.check(z.ulid()); + } + else if (format === "xid") { + stringSchema = stringSchema.check(z.xid()); + } + else if (format === "ksuid") { + stringSchema = stringSchema.check(z.ksuid()); + } + // Note: json-string format is not currently supported by Zod + // Custom formats are ignored - keep as plain string + } + // Apply constraints + if (typeof schema.minLength === "number") { + stringSchema = stringSchema.min(schema.minLength); + } + if (typeof schema.maxLength === "number") { + stringSchema = stringSchema.max(schema.maxLength); + } + if (schema.pattern) { + // JSON Schema patterns are not implicitly anchored (match anywhere in string) + stringSchema = stringSchema.regex(new RegExp(schema.pattern)); + } + zodSchema = stringSchema; + break; + } + case "number": + case "integer": { + let numberSchema = type === "integer" ? z.number().int() : z.number(); + // Apply constraints + // In draft-04, `exclusiveMinimum: true` makes the sibling `minimum` exclusive rather than an independent bound, so the inclusive `.min()` is skipped; emitting it too would be dominated by the exclusive check and report a second, weaker issue. + if (typeof schema.minimum === "number" && schema.exclusiveMinimum !== true) { + numberSchema = numberSchema.min(schema.minimum); + } + if (typeof schema.maximum === "number" && schema.exclusiveMaximum !== true) { + numberSchema = numberSchema.max(schema.maximum); + } + if (typeof schema.exclusiveMinimum === "number") { + numberSchema = numberSchema.gt(schema.exclusiveMinimum); + } + else if (schema.exclusiveMinimum === true && typeof schema.minimum === "number") { + numberSchema = numberSchema.gt(schema.minimum); + } + if (typeof schema.exclusiveMaximum === "number") { + numberSchema = numberSchema.lt(schema.exclusiveMaximum); + } + else if (schema.exclusiveMaximum === true && typeof schema.maximum === "number") { + numberSchema = numberSchema.lt(schema.maximum); + } + if (typeof schema.multipleOf === "number") { + numberSchema = numberSchema.multipleOf(schema.multipleOf); + } + zodSchema = numberSchema; + break; + } + case "boolean": { + zodSchema = z.boolean(); + break; + } + case "null": { + zodSchema = z.null(); + break; + } + case "object": { + const shape = {}; + const properties = schema.properties || {}; + const requiredSet = new Set(schema.required || []); + const additionalSchema = typeof schema.additionalProperties === "object" + ? convertSchema(schema.additionalProperties, ctx) + : undefined; + // Convert properties - mark optional ones + for (const [key, propSchema] of Object.entries(properties)) { + const propZodSchema = convertSchema(propSchema, ctx); + // If not in required array, make it optional. assignProp so a __proto__ key becomes an own property instead of hitting the inherited setter + assignProp(shape, key, requiredSet.has(key) ? propZodSchema : propZodSchema.optional()); + } + // Handle patternProperties + if (schema.patternProperties) { + // patternProperties: keys matching pattern must satisfy corresponding schema. Use loose records so non-matching keys pass through + const patternProps = schema.patternProperties; + const patternKeys = Object.keys(patternProps); + const looseRecords = []; + for (const pattern of patternKeys) { + const patternValue = convertSchema(patternProps[pattern], ctx); + const keySchema = z.string().regex(new RegExp(pattern)); + looseRecords.push(z.looseRecord(keySchema, patternValue)); + } + // Build intersection: object schema + all pattern property records + const schemasToIntersect = []; + if (Object.keys(shape).length > 0) { + // Use passthrough so patternProperties can validate additional keys + schemasToIntersect.push(z.object(shape).passthrough()); + } + schemasToIntersect.push(...looseRecords); + if (schemasToIntersect.length === 0) { + zodSchema = z.object({}).passthrough(); + } + else if (schemasToIntersect.length === 1) { + zodSchema = schemasToIntersect[0]; + } + else { + // Chain intersections: (A & B) & C & D ... + let result = z.intersection(schemasToIntersect[0], schemasToIntersect[1]); + for (let i = 2; i < schemasToIntersect.length; i++) { + result = z.intersection(result, schemasToIntersect[i]); + } + zodSchema = result; + } + // When additionalProperties is false, reject keys that are neither defined in properties nor matched by any patternProperty. + if (schema.additionalProperties === false) { + const propertyKeys = Object.keys(shape); + const patterns = patternKeys.map((p) => new RegExp(p)); + const basePatternSchema = zodSchema; + zodSchema = zodSchema.check((payload) => { + if (!isPlainObject(payload.value)) + return; + const unrecognized = []; + for (const key of Object.keys(payload.value)) { + if (propertyKeys.includes(key)) + continue; + if (patterns.some((regex) => regex.test(key))) + continue; + unrecognized.push(key); + } + if (unrecognized.length) { + payload.issues.push({ + code: "unrecognized_keys", + keys: unrecognized, + input: payload.value, + inst: basePatternSchema, + }); + } + }); + } + } + else { + // Handle additionalProperties. In JSON Schema, additionalProperties defaults to true (allow any extra properties). In Zod, objects strip unknown keys by default, so we need to handle this explicitly + const objectSchema = z.object(shape); + if (schema.additionalProperties === false) { + // Strict mode - no extra properties allowed + zodSchema = objectSchema.strict(); + } + else if (additionalSchema) { + // Extra properties must match the specified schema + zodSchema = objectSchema.catchall(additionalSchema); + } + else { + // additionalProperties is true or undefined - allow any extra properties (passthrough) + zodSchema = objectSchema.passthrough(); + } + } + // propertyNames constrains key *names* only, and says nothing about which keys are required or how their values validate. Layering it on top of the result keeps properties/patternProperties/additionalProperties composing underneath. `true` allows every name, so it needs no guard. + const hasKeyGuard = schema.propertyNames !== undefined && schema.propertyNames !== true; + const minProperties = typeof schema.minProperties === "number" ? schema.minProperties : undefined; + const maxProperties = typeof schema.maxProperties === "number" ? schema.maxProperties : undefined; + if (hasKeyGuard || minProperties !== undefined || maxProperties !== undefined) { + let keySchema; + if (hasKeyGuard) { + // Keys are always strings, so a propertyNames subschema that omits `type` still constrains them — without this it would convert to z.any(). + const keyJSONSchema = typeof schema.propertyNames === "object" && schema.propertyNames.type === undefined + ? { type: "string", ...schema.propertyNames } + : schema.propertyNames; + keySchema = convertSchema(keyJSONSchema, ctx); + } + zodSchema = checkObjectGuards(zodSchema, { keySchema, minProperties, maxProperties }); + } + break; + } + case "array": { + // Check if this is a tuple (prefixItems or items as array) + const prefixItems = schema.prefixItems; + const items = schema.items; + if (prefixItems && Array.isArray(prefixItems)) { + // Tuple with prefixItems (draft-2020-12) + const minItems = typeof schema.minItems === "number" ? schema.minItems : 0; + const tupleItems = prefixItems.map((item) => convertSchema(item, ctx)); + const positionalItems = applyMinItems(tupleItems, minItems); + const rest = !Array.isArray(items) ? getTupleRest(items, ctx) : undefined; + const tupleSchema = z.tuple(positionalItems); + zodSchema = rest ? tupleSchema.rest(rest) : tupleSchema; + // Apply minItems/maxItems constraints to tuples + if (typeof schema.minItems === "number") { + zodSchema = zodSchema.check(z.minLength(schema.minItems)); + } + if (typeof schema.maxItems === "number") { + zodSchema = zodSchema.check(z.maxLength(schema.maxItems)); + } + } + else if (Array.isArray(items)) { + // Tuple with items array (draft-7) + const minItems = typeof schema.minItems === "number" ? schema.minItems : 0; + const tupleItems = items.map((item) => convertSchema(item, ctx)); + const positionalItems = applyMinItems(tupleItems, minItems); + const rest = getTupleRest(schema.additionalItems, ctx); + const tupleSchema = z.tuple(positionalItems); + zodSchema = rest ? tupleSchema.rest(rest) : tupleSchema; + // Apply minItems/maxItems constraints to tuples + if (typeof schema.minItems === "number") { + zodSchema = zodSchema.check(z.minLength(schema.minItems)); + } + if (typeof schema.maxItems === "number") { + zodSchema = zodSchema.check(z.maxLength(schema.maxItems)); + } + } + else if (items !== undefined) { + // Regular array + const element = convertSchema(items, ctx); + let arraySchema = z.array(element); + // Apply constraints + if (typeof schema.minItems === "number") { + arraySchema = arraySchema.min(schema.minItems); + } + if (typeof schema.maxItems === "number") { + arraySchema = arraySchema.max(schema.maxItems); + } + zodSchema = arraySchema; + } + else { + // No items specified - array of any + zodSchema = z.array(z.any()); + } + // minContains/maxContains only constrain anything when `contains` itself is present + if (schema.uniqueItems === true || schema.contains !== undefined) { + zodSchema = checkArrayGuards(zodSchema, { + uniqueItems: schema.uniqueItems === true, + containsSchema: schema.contains !== undefined ? convertSchema(schema.contains, ctx) : undefined, + minContains: typeof schema.minContains === "number" ? schema.minContains : undefined, + maxContains: typeof schema.maxContains === "number" ? schema.maxContains : undefined, + }); + } + break; + } + default: + throw new Error(`Unsupported type: ${type}`); + } + return zodSchema; +} +function convertSchema(schema, ctx) { + if (typeof schema === "boolean") { + return schema ? z.any() : z.never(); + } + // Convert base schema first (ignoring composition keywords) + let baseSchema = convertBaseSchema(schema, ctx); + const hasExplicitType = schema.type || schema.enum !== undefined || schema.const !== undefined; + // Process composition keywords LAST (they can appear together) + // Handle anyOf - wrap base schema with union + if (schema.anyOf && Array.isArray(schema.anyOf)) { + const options = schema.anyOf.map((s) => convertSchema(s, ctx)); + const anyOfUnion = z.union(options); + baseSchema = hasExplicitType ? z.intersection(baseSchema, anyOfUnion) : anyOfUnion; + } + // Handle oneOf - exclusive union (exactly one must match) + if (schema.oneOf && Array.isArray(schema.oneOf)) { + const options = schema.oneOf.map((s) => convertSchema(s, ctx)); + const oneOfUnion = z.xor(options); + baseSchema = hasExplicitType ? z.intersection(baseSchema, oneOfUnion) : oneOfUnion; + } + // Handle allOf - wrap base schema with intersection + if (schema.allOf && Array.isArray(schema.allOf)) { + if (schema.allOf.length === 0) { + baseSchema = hasExplicitType ? baseSchema : z.any(); + } + else { + let result = hasExplicitType ? baseSchema : convertSchema(schema.allOf[0], ctx); + const startIdx = hasExplicitType ? 0 : 1; + for (let i = startIdx; i < schema.allOf.length; i++) { + result = z.intersection(result, convertSchema(schema.allOf[i], ctx)); + } + baseSchema = result; + } + } + // Handle nullable (OpenAPI 3.0) + if (schema.nullable === true && ctx.version === "openapi-3.0") { + baseSchema = z.nullable(baseSchema); + } + // Handle readOnly + if (schema.readOnly === true) { + baseSchema = z.readonly(baseSchema); + } + // Apply `default` so it wraps the fully-composed schema. This ensures `parse(undefined) -> default` works regardless of which branch of `convertBaseSchema` produced the inner schema (enum/const/not/typed/etc.). + if (schema.default !== undefined) { + baseSchema = baseSchema.default(schema.default); + } + // Collect non-description annotation metadata into the user-supplied registry. Description is handled separately below via `.describe()` to preserve the contract that `schema.description` reads from globalRegistry. + const extraMeta = {}; + const coreMetadataKeys = ["$id", "id", "$comment", "$anchor", "$vocabulary", "$dynamicRef", "$dynamicAnchor"]; + for (const key of coreMetadataKeys) { + if (key in schema) { + extraMeta[key] = schema[key]; + } + } + const contentMetadataKeys = ["contentEncoding", "contentMediaType", "contentSchema"]; + for (const key of contentMetadataKeys) { + if (key in schema) { + extraMeta[key] = schema[key]; + } + } + // `propertyNames` and `contains` are enforced by a guard, which `toJSONSchema` cannot infer, so the original keyword is carried as metadata to keep the round-trip lossless. Only where it was actually applied: on any other type it is inert, and on a `$ref` the metadata would land on the target every reference shares. A carried subschema is copied verbatim while the root `$defs` stay behind, so one holding a `$ref` is dropped rather than emitted as a dangling reference. + if (schema.type === "object" && schema.$ref === undefined) { + if (schema.propertyNames !== undefined && !containsRef(schema.propertyNames)) { + extraMeta.propertyNames = schema.propertyNames; + } + for (const key of ["minProperties", "maxProperties"]) { + if (schema[key] !== undefined) + extraMeta[key] = schema[key]; + } + } + if (schema.type === "array" && schema.$ref === undefined) { + if (schema.contains !== undefined && !containsRef(schema.contains)) { + extraMeta.contains = schema.contains; + } + for (const key of ["uniqueItems", "minContains", "maxContains"]) { + if (schema[key] !== undefined) + extraMeta[key] = schema[key]; + } + } + for (const key of Object.keys(schema)) { + if (!RECOGNIZED_KEYS.has(key)) { + assignProp(extraMeta, key, schema[key]); + } + } + if (Object.keys(extraMeta).length > 0) { + ctx.registry.add(baseSchema, extraMeta); + } + // Apply description last. `.describe()` clones the schema and sets `_zod.parent` on the clone, so registry lookups on the returned reference still resolve `extraMeta` via parent inheritance. + if (schema.description) { + baseSchema = baseSchema.describe(schema.description); + } + return baseSchema; +} +/** + * Converts a JSON Schema to a Zod schema. This function should be considered semi-experimental. It's behavior is liable to change. */ +export function fromJSONSchema(schema, params) { + // Handle boolean schemas + if (typeof schema === "boolean") { + return schema ? z.any() : z.never(); + } + // Normalize input via a JSON round-trip. This guarantees the converter walks a plain, finite, JSON-valid object graph: cyclic inputs fail here, getter/Proxy-based properties are materialized into static values, and class instances collapse to plain objects. + let normalized; + try { + normalized = JSON.parse(JSON.stringify(schema)); + } + catch { + throw new Error("fromJSONSchema input is not valid JSON (possibly cyclic); use $defs/$ref for recursive schemas"); + } + const version = detectVersion(normalized, params?.defaultTarget); + const defs = (normalized.$defs || normalized.definitions || {}); + const ctx = { + version, + defs, + refs: new Map(), + processing: new Set(), + rootSchema: normalized, + registry: params?.registry ?? globalRegistry, + }; + return convertSchema(normalized, ctx); +} diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/in-out.d.cts b/tests/fixtures/npm/node_modules/zod/v4/classic/in-out.d.cts new file mode 100644 index 000000000..09b530fe2 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/in-out.d.cts @@ -0,0 +1,8 @@ +import type * as core from "../core/index.cjs"; +import * as schemas from "./schemas.cjs"; +export type input = core.input; +export type output = core.output; +/** Returns a copy of the schema with every pipe replaced by its input side. A codec's checks are dropped: they constrain the decoded value the input side never produces. */ +export declare function input(schema: T): schemas.ZodType, core.input>; +/** Returns a copy of the schema with every pipe replaced by its output side, carrying over the pipe's own checks. */ +export declare function output(schema: T): schemas.ZodType, core.output>; diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/in-out.js b/tests/fixtures/npm/node_modules/zod/v4/classic/in-out.js new file mode 100644 index 000000000..4a0002688 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/in-out.js @@ -0,0 +1,36 @@ +import { clone, mergeDefs } from "../core/util.js"; +import { visit } from "../core/visit.js"; +import * as schemas from "./schemas.js"; +/** Appends a pipe's own checks to the side that replaces it, mirroring how `.check()` clones. */ +function withChecks(side, checks) { + if (!checks?.length) + return side; + const def = side._zod.def; + return clone(side, mergeDefs(def, { checks: [...(def.checks ?? []), ...checks] }), { parent: true }); +} +/** The out side, carrying the pipe's own checks: those run against the decoded value, which is what `out` produces. */ +function outSide(def) { + return withChecks(def.out, def.checks); +} +/** The in side. `z.preprocess` pipes a transform into a schema, and a bare transform validates nothing, so the schema it feeds is the real input side — the resolution `toJSONSchema` already makes for this case. */ +function inSide(def) { + return def.in._zod.traits.has("$ZodTransform") ? outSide(def) : def.in; +} +/** Returns a copy of the schema with every pipe replaced by its input side. A codec's checks are dropped: they constrain the decoded value the input side never produces. */ +export function input(schema) { + return visit(schema, { + pipe: (s) => inSide(s._zod.def), + // A default value belongs to the output side, so a rewritten inner type leaves it stranded. `.default()` widens the declared input type with `undefined`, and `optional` is what carries that across. + default: (s, rewritten) => (rewritten ? schemas.optional(s._zod.def.innerType) : s), + // A catch value is output-side too, but `.catch()` leaves the declared input type alone, so the inner schema stands on its own. + catch: (s, rewritten) => (rewritten ? s._zod.def.innerType : s), + }); +} +/** Returns a copy of the schema with every pipe replaced by its output side, carrying over the pipe's own checks. */ +export function output(schema) { + return visit(schema, { + pipe: (s) => outSide(s._zod.def), + // A prefault value is fed through the schema, which makes it input-side, so a rewritten inner type leaves it stranded. + prefault: (s, rewritten) => (rewritten ? s._zod.def.innerType : s), + }); +} diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/index.d.cts b/tests/fixtures/npm/node_modules/zod/v4/classic/index.d.cts new file mode 100644 index 000000000..b5fc2a17b --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/index.d.cts @@ -0,0 +1,3 @@ +import * as z from "./external.cjs"; +export * from "./external.cjs"; +export { z, z as default }; diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/iso.d.cts b/tests/fixtures/npm/node_modules/zod/v4/classic/iso.d.cts new file mode 100644 index 000000000..db764d9f8 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/iso.d.cts @@ -0,0 +1,7 @@ +import * as core from "../core/index.cjs"; +import { ZodISODate, ZodISODateTime, ZodISODuration, ZodISOTime } from "./schemas.cjs"; +export { ZodISODate, ZodISODateTime, ZodISODuration, ZodISOTime } from "./schemas.cjs"; +export declare function datetime(params?: string | core.$ZodISODateTimeParams): ZodISODateTime; +export declare function date(params?: string | core.$ZodISODateParams): ZodISODate; +export declare function time(params?: string | core.$ZodISOTimeParams): ZodISOTime; +export declare function duration(params?: string | core.$ZodISODurationParams): ZodISODuration; diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/iso.js b/tests/fixtures/npm/node_modules/zod/v4/classic/iso.js new file mode 100644 index 000000000..d38f24ddd --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/iso.js @@ -0,0 +1,15 @@ +import * as core from "../core/index.js"; +import { ZodISODate, ZodISODateTime, ZodISODuration, ZodISOTime } from "./schemas.js"; +export { ZodISODate, ZodISODateTime, ZodISODuration, ZodISOTime } from "./schemas.js"; +export function datetime(params) { + return core._isoDateTime(ZodISODateTime, params); +} +export function date(params) { + return core._isoDate(ZodISODate, params); +} +export function time(params) { + return core._isoTime(ZodISOTime, params); +} +export function duration(params) { + return core._isoDuration(ZodISODuration, params); +} diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/parse.d.cts b/tests/fixtures/npm/node_modules/zod/v4/classic/parse.d.cts new file mode 100644 index 000000000..408566b52 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/parse.d.cts @@ -0,0 +1,44 @@ +import * as core from "../core/index.cjs"; +import { type ZodError } from "./errors.cjs"; +export type ZodSafeParseResult = ZodSafeParseSuccess | ZodSafeParseError; +export type ZodSafeParseSuccess = { + success: true; + data: T; + error?: never; +}; +export type ZodSafeParseError = { + success: false; + data?: never; + error: ZodError; +}; +export declare const parse: (schema: T, value: unknown, _ctx?: core.ParseContext, _params?: { + callee?: core.util.AnyFunc; + Err?: core.$ZodErrorClass; +}) => core.output; +export declare const parseAsync: (schema: T, value: unknown, _ctx?: core.ParseContext, _params?: { + callee?: core.util.AnyFunc; + Err?: core.$ZodErrorClass; +}) => Promise>; +export declare const safeParse: (schema: T, value: unknown, _ctx?: core.ParseContext) => ZodSafeParseResult>; +export declare const safeParseAsync: (schema: T, value: unknown, _ctx?: core.ParseContext) => Promise>>; +export { validate, validateAsync } from "../core/index.cjs"; +export declare const encode: (schema: T, value: core.output, _ctx?: core.ParseContext, _params?: { + callee?: core.util.AnyFunc; + Err?: core.$ZodErrorClass; +}) => core.input; +export declare const decode: (schema: T, value: core.input, _ctx?: core.ParseContext, _params?: { + callee?: core.util.AnyFunc; + Err?: core.$ZodErrorClass; +}) => core.output; +export declare const encodeAsync: (schema: T, value: core.output, _ctx?: core.ParseContext, _params?: { + callee?: core.util.AnyFunc; + Err?: core.$ZodErrorClass; +}) => Promise>; +export declare const decodeAsync: (schema: T, value: core.input, _ctx?: core.ParseContext, _params?: { + callee?: core.util.AnyFunc; + Err?: core.$ZodErrorClass; +}) => Promise>; +export declare const safeEncode: (schema: T, value: core.output, _ctx?: core.ParseContext) => ZodSafeParseResult>; +export declare const safeDecode: (schema: T, value: core.input, _ctx?: core.ParseContext) => ZodSafeParseResult>; +export declare const safeEncodeAsync: (schema: T, value: core.output, _ctx?: core.ParseContext) => Promise>>; +export declare const safeDecodeAsync: (schema: T, value: core.input, _ctx?: core.ParseContext) => Promise>>; diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/parse.js b/tests/fixtures/npm/node_modules/zod/v4/classic/parse.js new file mode 100644 index 000000000..514aacc64 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/parse.js @@ -0,0 +1,16 @@ +import * as core from "../core/index.js"; +import { ZodRealError } from "./errors.js"; +export const parse = /* @__PURE__ */ core._parse(ZodRealError); +export const parseAsync = /* @__PURE__ */ core._parseAsync(ZodRealError); +export const safeParse = /* @__PURE__ */ core._safeParse(ZodRealError); +export const safeParseAsync = /* @__PURE__ */ core._safeParseAsync(ZodRealError); +export { validate, validateAsync } from "../core/index.js"; +// Codec functions +export const encode = /* @__PURE__ */ core._encode(ZodRealError); +export const decode = /* @__PURE__ */ core._decode(ZodRealError); +export const encodeAsync = /* @__PURE__ */ core._encodeAsync(ZodRealError); +export const decodeAsync = /* @__PURE__ */ core._decodeAsync(ZodRealError); +export const safeEncode = /* @__PURE__ */ core._safeEncode(ZodRealError); +export const safeDecode = /* @__PURE__ */ core._safeDecode(ZodRealError); +export const safeEncodeAsync = /* @__PURE__ */ core._safeEncodeAsync(ZodRealError); +export const safeDecodeAsync = /* @__PURE__ */ core._safeDecodeAsync(ZodRealError); diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/schemas.d.cts b/tests/fixtures/npm/node_modules/zod/v4/classic/schemas.d.cts new file mode 100644 index 000000000..226a3ae25 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/schemas.d.cts @@ -0,0 +1,816 @@ +import type { $ZodBigIntFormats } from "../core/checks.cjs"; +import * as core from "../core/index.cjs"; +import { util, type $ZodNumberFormats } from "../core/index.cjs"; +import type { StandardSchemaWithJSONProps } from "../core/standard-schema.cjs"; +import * as parse from "./parse.cjs"; +export type ZodStandardSchemaWithJSON = StandardSchemaWithJSONProps, core.output>; +export interface ZodType = core.$ZodTypeInternals> extends core.$ZodType { + def: Internals["def"]; + type: Internals["def"]["type"]; + /** @deprecated Use `.def` instead. */ + _def: Internals["def"]; + /** @deprecated Use `z.output` instead. */ + _output: Internals["output"]; + /** @deprecated Use `z.input` instead. */ + _input: Internals["input"]; + "~standard": ZodStandardSchemaWithJSON; + /** Converts this schema to a JSON Schema representation. */ + toJSONSchema(params?: core.ToJSONSchemaParams): core.ZodStandardJSONSchemaPayload; + check(...checks: (core.CheckFn> | core.$ZodCheck>)[]): this; + with(...checks: (core.CheckFn> | core.$ZodCheck>)[]): this; + clone(def?: Internals["def"], params?: { + parent: boolean; + }): this; + register(registry: R, ...meta: this extends R["_schema"] ? undefined extends R["_meta"] ? [core.$replace?] : [core.$replace] : ["Incompatible schema"]): this; + brand(value?: T): PropertyKey extends T ? this : core.$ZodBranded; + parse(data: unknown, params?: core.ParseContext): core.output; + safeParse(data: unknown, params?: core.ParseContext): parse.ZodSafeParseResult>; + parseAsync(data: unknown, params?: core.ParseContext): Promise>; + safeParseAsync(data: unknown, params?: core.ParseContext): Promise>>; + spa: (data: unknown, params?: core.ParseContext) => Promise>>; + validate(data: unknown, params?: core.ParseContext): data is core.input; + validateAsync(data: unknown, params?: core.ParseContext): Promise; + encode(data: core.output, params?: core.ParseContext): core.input; + decode(data: core.input, params?: core.ParseContext): core.output; + encodeAsync(data: core.output, params?: core.ParseContext): Promise>; + decodeAsync(data: core.input, params?: core.ParseContext): Promise>; + safeEncode(data: core.output, params?: core.ParseContext): parse.ZodSafeParseResult>; + safeDecode(data: core.input, params?: core.ParseContext): parse.ZodSafeParseResult>; + safeEncodeAsync(data: core.output, params?: core.ParseContext): Promise>>; + safeDecodeAsync(data: core.input, params?: core.ParseContext): Promise>>; + refine) => unknown | Promise>(check: Ch, params?: string | core.$ZodCustomParams): Ch extends (arg: any) => arg is infer R ? this & ZodType> : this; + superRefine(refinement: (arg: core.output, ctx: core.$RefinementCtx>) => void | Promise, params?: core.$ZodSuperRefineParams): this; + overwrite(fn: (x: core.output) => core.output): this; + optional(): ZodOptional; + exactOptional(): ZodExactOptional; + nonoptional(params?: string | core.$ZodNonOptionalParams): ZodNonOptional; + nullable(): ZodNullable; + nullish(): ZodOptional>; + default(def: util.NoUndefined>): ZodDefault; + default(def: () => util.NoUndefined>): ZodDefault; + prefault(def: () => core.input): ZodPrefault; + prefault(def: core.input): ZodPrefault; + array(): ZodArray; + or(option: T): ZodUnion<[this, T]>; + and(incoming: T): ZodIntersection; + transform(transform: (arg: core.output, ctx: core.$RefinementCtx>) => NewOut | Promise): ZodPipe, core.output>>; + catch(def: core.output): ZodCatch; + catch(def: (ctx: core.$ZodCatchCtx) => core.output): ZodCatch; + pipe>>(target: T | core.$ZodType>): ZodPipe; + readonly(): ZodReadonly; + /** Returns a new instance that has been registered in `z.globalRegistry` with the specified description */ + describe(description: string): this; + description?: string; + /** Returns the metadata associated with this instance in `z.globalRegistry` */ + meta(): core.$replace | undefined; + /** Returns a new instance that has been registered in `z.globalRegistry` with the specified metadata */ + meta(data: core.$replace): this; + /** @deprecated Try safe-parsing `undefined` (this is what `isOptional` does internally): + * + * ```ts + * const schema = z.string().optional(); + * const isOptional = schema.safeParse(undefined).success; // true + * ``` + */ + isOptional(): boolean; + /** + * @deprecated Try safe-parsing `null` (this is what `isNullable` does internally): + * + * ```ts + * const schema = z.string().nullable(); + * const isNullable = schema.safeParse(null).success; // true + * ``` + */ + isNullable(): boolean; + apply(fn: (schema: this, ...args: TArgs) => T, ...args: TArgs): T; +} +export interface _ZodType extends ZodType { +} +export declare const ZodType: core.$constructor; +export interface _ZodString = core.$ZodStringInternals> extends _ZodType { + format: string | null; + minLength: number | null; + maxLength: number | null; + regex(regex: RegExp, params?: string | core.$ZodCheckRegexParams): this; + includes(value: string, params?: string | core.$ZodCheckIncludesParams): this; + startsWith(value: string, params?: string | core.$ZodCheckStartsWithParams): this; + endsWith(value: string, params?: string | core.$ZodCheckEndsWithParams): this; + min(minLength: number, params?: string | core.$ZodCheckMinLengthParams): this; + max(maxLength: number, params?: string | core.$ZodCheckMaxLengthParams): this; + length(len: number, params?: string | core.$ZodCheckLengthEqualsParams): this; + nonempty(params?: string | core.$ZodCheckMinLengthParams): this; + lowercase(params?: string | core.$ZodCheckLowerCaseParams): this; + uppercase(params?: string | core.$ZodCheckUpperCaseParams): this; + trim(): this; + normalize(form?: "NFC" | "NFD" | "NFKC" | "NFKD" | (string & {})): this; + toLowerCase(): this; + toUpperCase(): this; + slugify(): this; +} +/** @internal */ +export declare const _ZodString: core.$constructor<_ZodString>; +export interface ZodString extends _ZodString> { + /** @deprecated Use `z.email()` instead. */ + email(params?: string | core.$ZodCheckEmailParams): this; + /** @deprecated Use `z.url()` instead. */ + url(params?: string | core.$ZodCheckURLParams): this; + /** @deprecated Use `z.jwt()` instead. */ + jwt(params?: string | core.$ZodCheckJWTParams): this; + /** @deprecated Use `z.emoji()` instead. */ + emoji(params?: string | core.$ZodCheckEmojiParams): this; + /** @deprecated Use `z.guid()` instead. */ + guid(params?: string | core.$ZodCheckGUIDParams): this; + /** @deprecated Use `z.uuid()` instead. */ + uuid(params?: string | core.$ZodCheckUUIDParams): this; + /** @deprecated Use `z.uuid()` instead. */ + uuidv4(params?: string | core.$ZodCheckUUIDParams): this; + /** @deprecated Use `z.uuid()` instead. */ + uuidv6(params?: string | core.$ZodCheckUUIDParams): this; + /** @deprecated Use `z.uuid()` instead. */ + uuidv7(params?: string | core.$ZodCheckUUIDParams): this; + /** @deprecated Use `z.nanoid()` instead. */ + nanoid(params?: string | core.$ZodCheckNanoIDParams): this; + /** @deprecated Use `z.guid()` instead. */ + guid(params?: string | core.$ZodCheckGUIDParams): this; + /** + * @deprecated CUID v1 is deprecated by its authors due to information leakage + * (timestamps embedded in the id). Use `z.cuid2()` instead. + * See https://github.com/paralleldrive/cuid. + */ + cuid(params?: string | core.$ZodCheckCUIDParams): this; + /** @deprecated Use `z.cuid2()` instead. */ + cuid2(params?: string | core.$ZodCheckCUID2Params): this; + /** @deprecated Use `z.ulid()` instead. */ + ulid(params?: string | core.$ZodCheckULIDParams): this; + /** @deprecated Use `z.base64()` instead. */ + base64(params?: string | core.$ZodCheckBase64Params): this; + /** @deprecated Use `z.base64url()` instead. */ + base64url(params?: string | core.$ZodCheckBase64URLParams): this; + /** @deprecated Use `z.xid()` instead. */ + xid(params?: string | core.$ZodCheckXIDParams): this; + /** @deprecated Use `z.ksuid()` instead. */ + ksuid(params?: string | core.$ZodCheckKSUIDParams): this; + /** @deprecated Use `z.ipv4()` instead. */ + ipv4(params?: string | core.$ZodCheckIPv4Params): this; + /** @deprecated Use `z.ipv6()` instead. */ + ipv6(params?: string | core.$ZodCheckIPv6Params): this; + /** @deprecated Use `z.cidrv4()` instead. */ + cidrv4(params?: string | core.$ZodCheckCIDRv4Params): this; + /** @deprecated Use `z.cidrv6()` instead. */ + cidrv6(params?: string | core.$ZodCheckCIDRv6Params): this; + /** @deprecated Use `z.e164()` instead. */ + e164(params?: string | core.$ZodCheckE164Params): this; + /** @deprecated Use `z.iso.datetime()` instead. */ + datetime(params?: string | core.$ZodCheckISODateTimeParams): this; + /** @deprecated Use `z.iso.date()` instead. */ + date(params?: string | core.$ZodCheckISODateParams): this; + /** @deprecated Use `z.iso.time()` instead. */ + time(params?: string | core.$ZodCheckISOTimeParams): this; + /** @deprecated Use `z.iso.duration()` instead. */ + duration(params?: string | core.$ZodCheckISODurationParams): this; +} +export declare const ZodString: core.$constructor; +export declare function string(params?: string | core.$ZodStringParams): ZodString; +export declare function string(params?: string | core.$ZodStringParams): core.$ZodType; +export interface ZodStringFormat extends _ZodString> { +} +export declare const ZodStringFormat: core.$constructor; +export interface ZodISODateTime extends ZodStringFormat { + _zod: core.$ZodISODateTimeInternals; +} +export declare const ZodISODateTime: core.$constructor; +export interface ZodISODate extends ZodStringFormat { + _zod: core.$ZodISODateInternals; +} +export declare const ZodISODate: core.$constructor; +export interface ZodISOTime extends ZodStringFormat { + _zod: core.$ZodISOTimeInternals; +} +export declare const ZodISOTime: core.$constructor; +export interface ZodISODuration extends ZodStringFormat { + _zod: core.$ZodISODurationInternals; +} +export declare const ZodISODuration: core.$constructor; +export interface ZodEmail extends ZodStringFormat<"email"> { + _zod: core.$ZodEmailInternals; +} +export declare const ZodEmail: core.$constructor; +export declare function email(params?: string | core.$ZodEmailParams): ZodEmail; +export interface ZodGUID extends ZodStringFormat<"guid"> { + _zod: core.$ZodGUIDInternals; +} +export declare const ZodGUID: core.$constructor; +export declare function guid(params?: string | core.$ZodGUIDParams): ZodGUID; +export interface ZodUUID extends ZodStringFormat<"uuid"> { + _zod: core.$ZodUUIDInternals; +} +export declare const ZodUUID: core.$constructor; +export declare function uuid(params?: string | core.$ZodUUIDParams): ZodUUID; +export declare function uuidv4(params?: string | core.$ZodUUIDv4Params): ZodUUID; +export declare function uuidv6(params?: string | core.$ZodUUIDv6Params): ZodUUID; +export declare function uuidv7(params?: string | core.$ZodUUIDv7Params): ZodUUID; +export interface ZodURL extends ZodStringFormat<"url"> { + _zod: core.$ZodURLInternals; +} +export declare const ZodURL: core.$constructor; +export declare function url(params?: string | core.$ZodURLParams): ZodURL; +export declare function httpUrl(params?: string | Omit): ZodURL; +export interface ZodEmoji extends ZodStringFormat<"emoji"> { + _zod: core.$ZodEmojiInternals; +} +export declare const ZodEmoji: core.$constructor; +export declare function emoji(params?: string | core.$ZodEmojiParams): ZodEmoji; +export interface ZodNanoID extends ZodStringFormat<"nanoid"> { + _zod: core.$ZodNanoIDInternals; +} +export declare const ZodNanoID: core.$constructor; +export declare function nanoid(params?: string | core.$ZodNanoIDParams): ZodNanoID; +/** + * @deprecated CUID v1 is deprecated by its authors due to information leakage + * (timestamps embedded in the id). Use {@link ZodCUID2} instead. + * See https://github.com/paralleldrive/cuid. + */ +export interface ZodCUID extends ZodStringFormat<"cuid"> { + _zod: core.$ZodCUIDInternals; +} +/** + * @deprecated CUID v1 is deprecated by its authors due to information leakage + * (timestamps embedded in the id). Use {@link ZodCUID2} instead. + * See https://github.com/paralleldrive/cuid. + */ +export declare const ZodCUID: core.$constructor; +/** + * Validates a CUID v1 string. + * + * @deprecated CUID v1 is deprecated by its authors due to information leakage + * (timestamps embedded in the id). Use {@link cuid2 | `z.cuid2()`} instead. + * See https://github.com/paralleldrive/cuid. + */ +export declare function cuid(params?: string | core.$ZodCUIDParams): ZodCUID; +export interface ZodCUID2 extends ZodStringFormat<"cuid2"> { + _zod: core.$ZodCUID2Internals; +} +export declare const ZodCUID2: core.$constructor; +export declare function cuid2(params?: string | core.$ZodCUID2Params): ZodCUID2; +export interface ZodULID extends ZodStringFormat<"ulid"> { + _zod: core.$ZodULIDInternals; +} +export declare const ZodULID: core.$constructor; +export declare function ulid(params?: string | core.$ZodULIDParams): ZodULID; +export interface ZodXID extends ZodStringFormat<"xid"> { + _zod: core.$ZodXIDInternals; +} +export declare const ZodXID: core.$constructor; +export declare function xid(params?: string | core.$ZodXIDParams): ZodXID; +export interface ZodKSUID extends ZodStringFormat<"ksuid"> { + _zod: core.$ZodKSUIDInternals; +} +export declare const ZodKSUID: core.$constructor; +export declare function ksuid(params?: string | core.$ZodKSUIDParams): ZodKSUID; +export interface ZodIPv4 extends ZodStringFormat<"ipv4"> { + _zod: core.$ZodIPv4Internals; +} +export declare const ZodIPv4: core.$constructor; +export declare function ipv4(params?: string | core.$ZodIPv4Params): ZodIPv4; +export interface ZodMAC extends ZodStringFormat<"mac"> { + _zod: core.$ZodMACInternals; +} +export declare const ZodMAC: core.$constructor; +export declare function mac(params?: string | core.$ZodMACParams): ZodMAC; +export interface ZodIPv6 extends ZodStringFormat<"ipv6"> { + _zod: core.$ZodIPv6Internals; +} +export declare const ZodIPv6: core.$constructor; +export declare function ipv6(params?: string | core.$ZodIPv6Params): ZodIPv6; +export interface ZodCIDRv4 extends ZodStringFormat<"cidrv4"> { + _zod: core.$ZodCIDRv4Internals; +} +export declare const ZodCIDRv4: core.$constructor; +export declare function cidrv4(params?: string | core.$ZodCIDRv4Params): ZodCIDRv4; +export interface ZodCIDRv6 extends ZodStringFormat<"cidrv6"> { + _zod: core.$ZodCIDRv6Internals; +} +export declare const ZodCIDRv6: core.$constructor; +export declare function cidrv6(params?: string | core.$ZodCIDRv6Params): ZodCIDRv6; +export interface ZodBase64 extends ZodStringFormat<"base64"> { + _zod: core.$ZodBase64Internals; +} +export declare const ZodBase64: core.$constructor; +export declare function base64(params?: string | core.$ZodBase64Params): ZodBase64; +export interface ZodBase64URL extends ZodStringFormat<"base64url"> { + _zod: core.$ZodBase64URLInternals; +} +export declare const ZodBase64URL: core.$constructor; +export declare function base64url(params?: string | core.$ZodBase64URLParams): ZodBase64URL; +export interface ZodE164 extends ZodStringFormat<"e164"> { + _zod: core.$ZodE164Internals; +} +export declare const ZodE164: core.$constructor; +export declare function e164(params?: string | core.$ZodE164Params): ZodE164; +export interface ZodCreditCard extends ZodStringFormat<"credit_card"> { + _zod: core.$ZodCreditCardInternals; +} +export declare const ZodCreditCard: core.$constructor; +export declare function creditCard(params?: string | core.$ZodCreditCardParams): ZodCreditCard; +export interface ZodIBAN extends ZodStringFormat<"iban"> { + _zod: core.$ZodIBANInternals; +} +export declare const ZodIBAN: core.$constructor; +export declare function iban(params?: string | core.$ZodIBANParams): ZodIBAN; +export interface ZodJWT extends ZodStringFormat<"jwt"> { + _zod: core.$ZodJWTInternals; +} +export declare const ZodJWT: core.$constructor; +export declare function jwt(params?: string | core.$ZodJWTParams): ZodJWT; +export interface ZodCustomStringFormat extends ZodStringFormat, core.$ZodCustomStringFormat { + _zod: core.$ZodCustomStringFormatInternals; + "~standard": ZodStandardSchemaWithJSON; +} +export declare const ZodCustomStringFormat: core.$constructor; +export declare function stringFormat(format: Format, fnOrRegex: ((arg: string) => util.MaybeAsync) | RegExp, _params?: string | core.$ZodStringFormatParams): ZodCustomStringFormat; +export declare function hostname(_params?: string | core.$ZodStringFormatParams): ZodCustomStringFormat<"hostname">; +export declare function hex(_params?: string | core.$ZodStringFormatParams): ZodCustomStringFormat<"hex">; +export declare function currencyCode(_params?: string | core.$ZodStringFormatParams): ZodCustomStringFormat<"currency_code">; +export declare function hash(alg: Alg, params?: { + enc?: Enc; +} & core.$ZodStringFormatParams): ZodCustomStringFormat<`${Alg}_${Enc}`>; +export interface _ZodNumber extends _ZodType { + gt(value: number, params?: string | core.$ZodCheckGreaterThanParams): this; + /** Identical to .min() */ + gte(value: number, params?: string | core.$ZodCheckGreaterThanParams): this; + min(value: number, params?: string | core.$ZodCheckGreaterThanParams): this; + lt(value: number, params?: string | core.$ZodCheckLessThanParams): this; + /** Identical to .max() */ + lte(value: number, params?: string | core.$ZodCheckLessThanParams): this; + max(value: number, params?: string | core.$ZodCheckLessThanParams): this; + /** Consider `z.int()` instead. This API is considered *legacy*; it will never be removed but a better alternative exists. */ + int(params?: string | core.$ZodCheckNumberFormatParams): this; + /** @deprecated This is now identical to `.int()`. Only numbers in the safe integer range are accepted. */ + safe(params?: string | core.$ZodCheckNumberFormatParams): this; + positive(params?: string | core.$ZodCheckGreaterThanParams): this; + nonnegative(params?: string | core.$ZodCheckGreaterThanParams): this; + negative(params?: string | core.$ZodCheckLessThanParams): this; + nonpositive(params?: string | core.$ZodCheckLessThanParams): this; + multipleOf(value: number, params?: string | core.$ZodCheckMultipleOfParams): this; + /** @deprecated Use `.multipleOf()` instead. */ + step(value: number, params?: string | core.$ZodCheckMultipleOfParams): this; + /** @deprecated In v4 and later, z.number() does not allow infinite values by default. This is a no-op. */ + finite(params?: unknown): this; + minValue: number | null; + maxValue: number | null; + /** @deprecated Check the `format` property instead. */ + isInt: boolean; + /** @deprecated Number schemas no longer accept infinite values, so this always returns `true`. */ + isFinite: boolean; + format: string | null; +} +export interface ZodNumber extends _ZodNumber> { +} +export declare const ZodNumber: core.$constructor; +export declare function number(params?: string | core.$ZodNumberParams): ZodNumber; +export interface ZodNumberFormat extends ZodNumber { + _zod: core.$ZodNumberFormatInternals; +} +export declare const ZodNumberFormat: core.$constructor; +export interface ZodInt extends ZodNumberFormat<"safeint"> { +} +export declare function int(params?: string | core.$ZodCheckNumberFormatParams): ZodInt; +export interface ZodFloat32 extends ZodNumberFormat<"float32"> { +} +export declare function float32(params?: string | core.$ZodCheckNumberFormatParams): ZodFloat32; +export interface ZodFloat64 extends ZodNumberFormat<"float64"> { +} +export declare function float64(params?: string | core.$ZodCheckNumberFormatParams): ZodFloat64; +export interface ZodInt32 extends ZodNumberFormat<"int32"> { +} +export declare function int32(params?: string | core.$ZodCheckNumberFormatParams): ZodInt32; +export interface ZodUInt32 extends ZodNumberFormat<"uint32"> { +} +export declare function uint32(params?: string | core.$ZodCheckNumberFormatParams): ZodUInt32; +export interface _ZodBoolean extends _ZodType { +} +export interface ZodBoolean extends _ZodBoolean> { +} +export declare const ZodBoolean: core.$constructor; +export declare function boolean(params?: string | core.$ZodBooleanParams): ZodBoolean; +export interface _ZodBigInt extends _ZodType { + gte(value: bigint, params?: string | core.$ZodCheckGreaterThanParams): this; + /** Alias of `.gte()` */ + min(value: bigint, params?: string | core.$ZodCheckGreaterThanParams): this; + gt(value: bigint, params?: string | core.$ZodCheckGreaterThanParams): this; + /** Alias of `.lte()` */ + lte(value: bigint, params?: string | core.$ZodCheckLessThanParams): this; + max(value: bigint, params?: string | core.$ZodCheckLessThanParams): this; + lt(value: bigint, params?: string | core.$ZodCheckLessThanParams): this; + positive(params?: string | core.$ZodCheckGreaterThanParams): this; + negative(params?: string | core.$ZodCheckLessThanParams): this; + nonpositive(params?: string | core.$ZodCheckLessThanParams): this; + nonnegative(params?: string | core.$ZodCheckGreaterThanParams): this; + multipleOf(value: bigint, params?: string | core.$ZodCheckMultipleOfParams): this; + minValue: bigint | null; + maxValue: bigint | null; + format: string | null; +} +export interface ZodBigInt extends _ZodBigInt> { +} +export declare const ZodBigInt: core.$constructor; +export declare function bigint(params?: string | core.$ZodBigIntParams): ZodBigInt; +export interface ZodBigIntFormat extends ZodBigInt { + _zod: core.$ZodBigIntFormatInternals; +} +export declare const ZodBigIntFormat: core.$constructor; +export interface ZodInt64 extends ZodBigIntFormat<"int64"> { +} +export declare function int64(params?: string | core.$ZodBigIntFormatParams): ZodInt64; +export interface ZodUInt64 extends ZodBigIntFormat<"uint64"> { +} +export declare function uint64(params?: string | core.$ZodBigIntFormatParams): ZodUInt64; +export interface ZodSymbol extends _ZodType { +} +export declare const ZodSymbol: core.$constructor; +export declare function symbol(params?: string | core.$ZodSymbolParams): ZodSymbol; +export interface ZodUndefined extends _ZodType { +} +export declare const ZodUndefined: core.$constructor; +declare function _undefined(params?: string | core.$ZodUndefinedParams): ZodUndefined; +export { _undefined as undefined }; +export interface ZodNull extends _ZodType { +} +export declare const ZodNull: core.$constructor; +declare function _null(params?: string | core.$ZodNullParams): ZodNull; +export { _null as null }; +export interface ZodAny extends _ZodType { +} +export declare const ZodAny: core.$constructor; +export declare function any(): ZodAny; +export interface ZodUnknown extends _ZodType { +} +export declare const ZodUnknown: core.$constructor; +export declare function unknown(): ZodUnknown; +export interface ZodNever extends _ZodType { +} +export declare const ZodNever: core.$constructor; +export declare function never(params?: string | core.$ZodNeverParams): ZodNever; +export interface ZodVoid extends _ZodType { +} +export declare const ZodVoid: core.$constructor; +declare function _void(params?: string | core.$ZodVoidParams): ZodVoid; +export { _void as void }; +export interface _ZodDate extends _ZodType { + min(value: number | Date, params?: string | core.$ZodCheckGreaterThanParams): this; + max(value: number | Date, params?: string | core.$ZodCheckLessThanParams): this; + /** @deprecated Not recommended. */ + minDate: Date | null; + /** @deprecated Not recommended. */ + maxDate: Date | null; +} +export interface ZodDate extends _ZodDate> { +} +export declare const ZodDate: core.$constructor; +export declare function date(params?: string | core.$ZodDateParams): ZodDate; +export interface ZodArray extends _ZodType>, core.$ZodArray { + element: T; + min(minLength: number, params?: string | core.$ZodCheckMinLengthParams): this; + nonempty(params?: string | core.$ZodCheckMinLengthParams): this; + max(maxLength: number, params?: string | core.$ZodCheckMaxLengthParams): this; + length(len: number, params?: string | core.$ZodCheckLengthEqualsParams): this; + unwrap(): T; + "~standard": ZodStandardSchemaWithJSON; +} +export declare const ZodArray: core.$constructor; +export declare function array(element: T, params?: string | core.$ZodArrayParams): ZodArray; +export declare function keyof(schema: T): ZodEnum>; +export type SafeExtendShape = { + [K in keyof Ext]: K extends keyof Base ? core.output extends core.output ? core.input extends core.input ? Ext[K] : never : never : Ext[K]; +}; +export interface ZodObject< +/** @ts-ignore Cast variance */ +out Shape extends core.$ZodShape = core.$ZodLooseShape, out Config extends core.$ZodObjectConfig = core.$strip> extends _ZodType>, core.$ZodObject { + "~standard": ZodStandardSchemaWithJSON; + shape: Shape; + keyof(): ZodEnum>; + /** Define a schema to validate all unrecognized keys. This overrides the existing strict/loose behavior. */ + catchall(schema: T): ZodObject>; + /** @deprecated Use `z.looseObject()` or `.loose()` instead. */ + passthrough(): ZodObject; + /** Consider `z.looseObject(A.shape)` instead */ + loose(): ZodObject; + /** Consider `z.strictObject(A.shape)` instead */ + strict(): ZodObject; + /** This is the default behavior. This method call is likely unnecessary. */ + strip(): ZodObject; + extend(shape: U): ZodObject>, Config>; + safeExtend(shape: SafeExtendShape & Partial>): ZodObject>, Config>; + /** + * @deprecated Use [`A.extend(B.shape)`](https://zod.dev/api?id=extend) instead. + */ + merge(other: U): ZodObject, U["_zod"]["config"]>; + pick>(mask: M & Record, never>): ZodObject>>, Config>; + omit>(mask: M & Record, never>): ZodObject>>, Config>; + partial(): ZodObject<{ + -readonly [k in keyof Shape]: ZodOptional; + }, Config>; + partial>(mask: M & Record, never>): ZodObject<{ + -readonly [k in keyof Shape]: k extends keyof M ? ZodOptional : Shape[k]; + }, Config>; + exactPartial(): ZodObject<{ + -readonly [k in keyof Shape]: ZodExactOptional; + }, Config>; + exactPartial>(mask: M & Record, never>): ZodObject<{ + -readonly [k in keyof Shape]: k extends keyof M ? ZodExactOptional : Shape[k]; + }, Config>; + required(): ZodObject<{ + -readonly [k in keyof Shape]: ZodNonOptional; + }, Config>; + required>(mask: M & Record, never>): ZodObject<{ + -readonly [k in keyof Shape]: k extends keyof M ? ZodNonOptional : Shape[k]; + }, Config>; +} +export declare const ZodObject: core.$constructor; +export declare function object>>(shape?: T, params?: string | core.$ZodObjectParams): ZodObject, core.$strip>; +export declare function strictObject(shape: T, params?: string | core.$ZodObjectParams): ZodObject, core.$strict>; +export declare function looseObject(shape: T, params?: string | core.$ZodObjectParams): ZodObject, core.$loose>; +export interface ZodUnion extends _ZodType>, core.$ZodUnion { + "~standard": ZodStandardSchemaWithJSON; + options: T; +} +export declare const ZodUnion: core.$constructor; +export declare function union(options: T, params?: string | core.$ZodUnionParams): ZodUnion; +export interface ZodXor extends _ZodType>, core.$ZodXor { + "~standard": ZodStandardSchemaWithJSON; + options: T; +} +export declare const ZodXor: core.$constructor; +/** Creates an exclusive union (XOR) where exactly one option must match. + * Unlike regular unions that succeed when any option matches, xor fails if + * zero or more than one option matches the input. */ +export declare function xor(options: T, params?: string | core.$ZodXorParams): ZodXor; +export interface ZodDiscriminatedUnion extends ZodUnion, core.$ZodDiscriminatedUnion { + "~standard": ZodStandardSchemaWithJSON; + _zod: core.$ZodDiscriminatedUnionInternals; + def: core.$ZodDiscriminatedUnionDef; +} +export declare const ZodDiscriminatedUnion: core.$constructor; +export declare function discriminatedUnion(discriminator: Disc, options: Types, params?: string | core.$ZodDiscriminatedUnionParams): ZodDiscriminatedUnion; +export interface ZodIntersection extends _ZodType>, core.$ZodIntersection { + "~standard": ZodStandardSchemaWithJSON; +} +export declare const ZodIntersection: core.$constructor; +export declare function intersection(left: T, right: U): ZodIntersection; +export interface ZodTuple extends _ZodType>, core.$ZodTuple { + "~standard": ZodStandardSchemaWithJSON; + rest(rest: Rest): ZodTuple; + partial(): ZodTuple<{ + -readonly [k in keyof T]: ZodOptional; + }, Rest>; +} +export declare const ZodTuple: core.$constructor; +export declare function tuple(items: T, params?: string | core.$ZodTupleParams): ZodTuple; +export declare function tuple(items: T, rest: Rest, params?: string | core.$ZodTupleParams): ZodTuple; +export declare function tuple(items: [], params?: string | core.$ZodTupleParams): ZodTuple<[], null>; +export interface ZodRecord extends _ZodType>, core.$ZodRecord { + "~standard": ZodStandardSchemaWithJSON; + keyType: Key; + valueType: Value; +} +export declare const ZodRecord: core.$constructor; +export declare function record(keyType: Key, valueType: Value, params?: string | core.$ZodRecordParams): ZodRecord; +export declare function partialRecord(keyType: Key, valueType: Value, params?: string | core.$ZodRecordParams): ZodRecord; +export declare function looseRecord(keyType: Key, valueType: Value, params?: string | core.$ZodRecordParams): ZodRecord; +export interface ZodMap extends _ZodType>, core.$ZodMap { + "~standard": ZodStandardSchemaWithJSON; + keyType: Key; + valueType: Value; + min(minSize: number, params?: string | core.$ZodCheckMinSizeParams): this; + nonempty(params?: string | core.$ZodCheckMinSizeParams): this; + max(maxSize: number, params?: string | core.$ZodCheckMaxSizeParams): this; + size(size: number, params?: string | core.$ZodCheckSizeEqualsParams): this; +} +export declare const ZodMap: core.$constructor; +export declare function map(keyType: Key, valueType: Value, params?: string | core.$ZodMapParams): ZodMap; +export interface ZodSet extends _ZodType>, core.$ZodSet { + "~standard": ZodStandardSchemaWithJSON; + min(minSize: number, params?: string | core.$ZodCheckMinSizeParams): this; + nonempty(params?: string | core.$ZodCheckMinSizeParams): this; + max(maxSize: number, params?: string | core.$ZodCheckMaxSizeParams): this; + size(size: number, params?: string | core.$ZodCheckSizeEqualsParams): this; +} +export declare const ZodSet: core.$constructor; +export declare function set(valueType: Value, params?: string | core.$ZodSetParams): ZodSet; +export interface ZodEnum< +/** @ts-ignore Cast variance */ +out T extends util.EnumLike = util.EnumLike> extends _ZodType>, core.$ZodEnum { + "~standard": ZodStandardSchemaWithJSON; + enum: T; + options: Array; + extract(values: U, params?: string | core.$ZodEnumParams): ZodEnum>>; + exclude(values: U, params?: string | core.$ZodEnumParams): ZodEnum>>; +} +export declare const ZodEnum: core.$constructor; +declare function _enum(values: T, params?: string | core.$ZodEnumParams): ZodEnum>; +declare function _enum(entries: T, params?: string | core.$ZodEnumParams): ZodEnum; +export { _enum as enum }; +/** @deprecated This API has been merged into `z.enum()`. Use `z.enum()` instead. + * + * ```ts + * enum Colors { red, green, blue } + * z.enum(Colors); + * ``` + */ +export declare function nativeEnum(entries: T, params?: string | core.$ZodEnumParams): ZodEnum; +export interface ZodLiteral extends _ZodType>, core.$ZodLiteral { + "~standard": ZodStandardSchemaWithJSON; + values: Set; + /** @legacy Use `.values` instead. Accessing this property will throw an error if the literal accepts multiple values. */ + value: T; +} +export declare const ZodLiteral: core.$constructor; +export declare function literal>(value: T, params?: string | core.$ZodLiteralParams): ZodLiteral; +export declare function literal(value: T, params?: string | core.$ZodLiteralParams): ZodLiteral; +export interface ZodFile extends _ZodType, core.$ZodFile { + "~standard": ZodStandardSchemaWithJSON; + min(size: number, params?: string | core.$ZodCheckMinSizeParams): this; + max(size: number, params?: string | core.$ZodCheckMaxSizeParams): this; + mime(types: util.MimeTypes | Array, params?: string | core.$ZodCheckMimeTypeParams): this; +} +export declare const ZodFile: core.$constructor; +export declare function file(params?: string | core.$ZodFileParams): ZodFile; +export interface ZodTransform extends _ZodType>, core.$ZodTransform { + "~standard": ZodStandardSchemaWithJSON; +} +export declare const ZodTransform: core.$constructor; +export declare function transform(fn: (input: I, ctx: core.$RefinementCtx) => O): ZodTransform, I>; +export interface ZodOptional extends _ZodType>, core.$ZodOptional { + "~standard": ZodStandardSchemaWithJSON; + unwrap(): T; +} +export declare const ZodOptional: core.$constructor; +export declare function optional(innerType: T): ZodOptional; +export interface ZodExactOptional extends _ZodType>, core.$ZodExactOptional { + "~standard": ZodStandardSchemaWithJSON; + unwrap(): T; +} +export declare const ZodExactOptional: core.$constructor; +export declare function exactOptional(innerType: T): ZodExactOptional; +export interface ZodNullable extends _ZodType>, core.$ZodNullable { + "~standard": ZodStandardSchemaWithJSON; + unwrap(): T; +} +export declare const ZodNullable: core.$constructor; +export declare function nullable(innerType: T): ZodNullable; +export declare function nullish(innerType: T): ZodOptional>; +export interface ZodDefault extends _ZodType>, core.$ZodDefault { + "~standard": ZodStandardSchemaWithJSON; + unwrap(): T; + /** @deprecated Use `.unwrap()` instead. */ + removeDefault(): T; +} +export declare const ZodDefault: core.$constructor; +export declare function _default(innerType: T, defaultValue: util.NoUndefined> | (() => util.NoUndefined>)): ZodDefault; +export interface ZodPrefault extends _ZodType>, core.$ZodPrefault { + "~standard": ZodStandardSchemaWithJSON; + unwrap(): T; +} +export declare const ZodPrefault: core.$constructor; +export declare function prefault(innerType: T, defaultValue: core.input | (() => core.input)): ZodPrefault; +export interface ZodNonOptional extends _ZodType>, core.$ZodNonOptional { + "~standard": ZodStandardSchemaWithJSON; + unwrap(): T; +} +export declare const ZodNonOptional: core.$constructor; +export declare function nonoptional(innerType: T, params?: string | core.$ZodNonOptionalParams): ZodNonOptional; +export interface ZodSuccess extends _ZodType>, core.$ZodSuccess { + "~standard": ZodStandardSchemaWithJSON; + unwrap(): T; +} +export declare const ZodSuccess: core.$constructor; +export declare function success(innerType: T): ZodSuccess; +export interface ZodCatch extends _ZodType>, core.$ZodCatch { + "~standard": ZodStandardSchemaWithJSON; + unwrap(): T; + /** @deprecated Use `.unwrap()` instead. */ + removeCatch(): T; +} +export declare const ZodCatch: core.$constructor; +declare function _catch(innerType: T, catchValue: core.output | ((ctx: core.$ZodCatchCtx) => core.output)): ZodCatch; +export { _catch as catch }; +export interface ZodNaN extends _ZodType, core.$ZodNaN { + "~standard": ZodStandardSchemaWithJSON; +} +export declare const ZodNaN: core.$constructor; +export declare function nan(params?: string | core.$ZodNaNParams): ZodNaN; +export interface ZodPipe extends _ZodType>, core.$ZodPipe { + "~standard": ZodStandardSchemaWithJSON; + in: A; + out: B; +} +export declare const ZodPipe: core.$constructor; +export declare function pipe> = core.$ZodType>>(in_: A, out: B | core.$ZodType>): ZodPipe; +export interface ZodCodec extends ZodPipe, core.$ZodCodec { + "~standard": ZodStandardSchemaWithJSON; + _zod: core.$ZodCodecInternals; + def: core.$ZodCodecDef; +} +export declare const ZodCodec: core.$constructor; +export declare function codec(in_: A, out: B, params: { + decode: (value: core.output, payload: core.ParsePayload>) => core.util.MaybeAsync>; + encode: (value: core.input, payload: core.ParsePayload>) => core.util.MaybeAsync>; +}): ZodCodec; +export declare function invertCodec(codec: ZodCodec): ZodCodec; +export interface ZodPreprocess extends ZodPipe, B>, core.$ZodPreprocess { + "~standard": ZodStandardSchemaWithJSON; + _zod: core.$ZodPreprocessInternals; + def: core.$ZodPreprocessDef; +} +export declare const ZodPreprocess: core.$constructor; +export interface ZodReadonly extends _ZodType>, core.$ZodReadonly { + "~standard": ZodStandardSchemaWithJSON; + unwrap(): T; +} +export declare const ZodReadonly: core.$constructor; +export declare function readonly(innerType: T): ZodReadonly; +export interface ZodTemplateLiteral